From e8c520c297940933d6d402a13b76622878d7cef3 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Thu, 22 Oct 2026 19:56:09 +0000 Subject: [PATCH 001/189] chore: Add more permissive header to add resource files that requires no separate notice term file --- .../util/resource/RawStringResources.kt | 23 +++++++++++++++++++ .../patches/util/resource/RawXmlResources.kt | 23 +++++++++++++++++++ .../patches/util/resource/StringResource.kt | 23 +++++++++++++++++++ 3 files changed, 69 insertions(+) diff --git a/patches/src/main/kotlin/app/morphe/patches/util/resource/RawStringResources.kt b/patches/src/main/kotlin/app/morphe/patches/util/resource/RawStringResources.kt index 0700ff4fb2..86c607bed5 100644 --- a/patches/src/main/kotlin/app/morphe/patches/util/resource/RawStringResources.kt +++ b/patches/src/main/kotlin/app/morphe/patches/util/resource/RawStringResources.kt @@ -1,3 +1,26 @@ +/* + * Copyright 2025 Morphe. + * https://github.com/MorpheApp/morphe-patches + * + * File-Specific License Notice (GPLv3 Section 7 Terms) + * + * This file is part of the Morphe project and is licensed under + * the GNU General Public License version 3 (GPLv3), with the Additional + * Terms under Section 7 described in the LICENSE file. + * + * https://www.gnu.org/licenses/gpl-3.0.html + * + * Section 7b: Notice Preservation + * ------------------------------- + * This entire comment block must be preserved in all copies, + * distributions, and derivative works of this file, in both + * original and modified source forms. + * + * Portions of this software are provided "AS IS" by the Morphe software project. + * Any express or implied warranties, including the implied warranties of + * merchantability and fitness for a particular purpose, are disclaimed. + */ + package app.morphe.patches.util.resource import java.io.File diff --git a/patches/src/main/kotlin/app/morphe/patches/util/resource/RawXmlResources.kt b/patches/src/main/kotlin/app/morphe/patches/util/resource/RawXmlResources.kt index 948913db40..7c03c4e571 100644 --- a/patches/src/main/kotlin/app/morphe/patches/util/resource/RawXmlResources.kt +++ b/patches/src/main/kotlin/app/morphe/patches/util/resource/RawXmlResources.kt @@ -1,3 +1,26 @@ +/* + * Copyright 2025 Morphe. + * https://github.com/MorpheApp/morphe-patches + * + * File-Specific License Notice (GPLv3 Section 7 Terms) + * + * This file is part of the Morphe project and is licensed under + * the GNU General Public License version 3 (GPLv3), with the Additional + * Terms under Section 7 described in the LICENSE file. + * + * https://www.gnu.org/licenses/gpl-3.0.html + * + * Section 7b: Notice Preservation + * ------------------------------- + * This entire comment block must be preserved in all copies, + * distributions, and derivative works of this file, in both + * original and modified source forms. + * + * Portions of this software are provided "AS IS" by the Morphe software project. + * Any express or implied warranties, including the implied warranties of + * merchantability and fitness for a particular purpose, are disclaimed. + */ + package app.morphe.patches.util.resource /** diff --git a/patches/src/main/kotlin/app/morphe/patches/util/resource/StringResource.kt b/patches/src/main/kotlin/app/morphe/patches/util/resource/StringResource.kt index e3ccdaf136..25c35decee 100644 --- a/patches/src/main/kotlin/app/morphe/patches/util/resource/StringResource.kt +++ b/patches/src/main/kotlin/app/morphe/patches/util/resource/StringResource.kt @@ -1,3 +1,26 @@ +/* + * Copyright 2025 Morphe. + * https://github.com/MorpheApp/morphe-patches + * + * File-Specific License Notice (GPLv3 Section 7 Terms) + * + * This file is part of the Morphe project and is licensed under + * the GNU General Public License version 3 (GPLv3), with the Additional + * Terms under Section 7 described in the LICENSE file. + * + * https://www.gnu.org/licenses/gpl-3.0.html + * + * Section 7b: Notice Preservation + * ------------------------------- + * This entire comment block must be preserved in all copies, + * distributions, and derivative works of this file, in both + * original and modified source forms. + * + * Portions of this software are provided "AS IS" by the Morphe software project. + * Any express or implied warranties, including the implied warranties of + * merchantability and fitness for a particular purpose, are disclaimed. + */ + package app.morphe.patches.util.resource import org.w3c.dom.Document From 0d5663d3875e74b81376e2c7dd4ff2fe43397877 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Thu, 22 Oct 2026 21:10:27 +0000 Subject: [PATCH 002/189] chore: Change to patcher implementation of code instead of patches-library --- gradle/libs.versions.toml | 2 +- .../app/morphe/patches/all/misc/clone/CloneAppPatch.kt | 2 +- .../all/misc/updates/DisablePlayStoreUpdatesPatch.kt | 6 +++--- .../music/interaction/crossfade/CrossfadePatch.kt | 2 +- .../DisableDislikeRedirectionPatch.kt | 2 +- .../music/interaction/downloads/DownloadsPatch.kt | 2 +- .../patches/music/interaction/jam/Fingerprints.kt | 2 +- .../app/morphe/patches/music/interaction/jam/JamABI.kt | 2 +- .../music/interaction/jam/JamBridgeInstaller.kt | 2 +- .../morphe/patches/music/interaction/jam/JamUIABI.kt | 4 ++-- .../patches/music/interaction/jam/JamUIInstaller.kt | 2 +- .../remember/shufflestate/RememberShuffleStatePatch.kt | 4 ++-- .../music/interaction/scrobbling/ScrobblingPatch.kt | 8 ++++---- .../music/layout/branding/CustomBrandingPatch.kt | 4 ++-- .../music/layout/branding/header/ChangeHeaderPatch.kt | 2 +- .../patches/music/layout/buttons/HideButtonsPatch.kt | 4 ++-- .../hide/flyoutmenu/HideFlyoutMenuComponentsPatch.kt | 2 +- .../hide/settingsmenu/HideSettingsMenuFilterPatch.kt | 4 ++-- .../morphe/patches/music/layout/lyrics/LyricsPatch.kt | 2 +- .../layout/miniplayer/ChangeMiniplayerColorPatch.kt | 4 ++-- .../layout/miniplayer/EnableForcedMiniplayerPatch.kt | 2 +- .../miniplayer/EnableSwipeToDismissMiniplayerPatch.kt | 4 ++-- .../returnyoutubedislike/ReturnYouTubeDislikePatch.kt | 4 ++-- .../music/layout/sponsorblock/SponsorBlockPatch.kt | 2 +- .../music/layout/startpage/ChangeStartPagePatch.kt | 2 +- .../patches/music/misc/androidauto/AndroidAutoPatch.kt | 10 +++++----- .../music/misc/androidauto/AndroidAutoSearchPatch.kt | 4 ++-- .../music/misc/audio/EnableExclusiveAudioPlayback.kt | 2 +- .../misc/audiovideoswitch/QueueSwitchInstaller.kt | 2 +- .../misc/backgroundplayback/BackgroundPlaybackPatch.kt | 2 +- .../patches/music/misc/settings/SettingsPatch.kt | 4 ++-- .../music/video/information/VideoInformationPatch.kt | 2 +- .../patches/reddit/layout/copytext/CopyTextPatch.kt | 2 +- .../reddit/layout/flair/ShowFlairsInHomeFeedPatch.kt | 2 +- .../reddit/layout/fullwidth/FullWidthFeedMediaPatch.kt | 2 +- .../mediaviewer/HideMediaViewerComponentsPatch.kt | 4 ++-- .../reddit/layout/mediaviewer/MediaViewerFadePatch.kt | 6 +++--- .../layout/navigation/HideNavigationButtonsPatch.kt | 4 ++-- .../reddit/layout/post/CustomizePostLayoutPatch.kt | 8 ++++---- .../reddit/layout/trending/HideTrendingShelvesPatch.kt | 2 +- .../RememberCollapsedCommentsPatch.kt | 6 +++--- .../misc/downloadlocation/DownloadLocationPatch.kt | 2 +- .../patches/reddit/misc/guest/StartAsGuestPatch.kt | 2 +- .../openlink/ScreenNavigatorMethodResolverPatch.kt | 2 +- .../patches/reddit/misc/redgifs/RedgifsAudioPatch.kt | 2 +- .../patches/reddit/misc/settings/SettingsPatch.kt | 10 +++++----- .../morphe/patches/shared/ad/HideFullscreenAdsPatch.kt | 6 +++--- .../shared/layout/branding/BaseCustomBrandingPatch.kt | 2 +- .../patches/shared/layout/theme/BaseThemePatch.kt | 4 ++-- .../misc/audio/tracks/ForceOriginalAudioPatch.kt | 2 +- .../shared/misc/checks/ExperimentalAppNoticePatch.kt | 2 +- .../shared/misc/debugging/EnableDebuggingPatch.kt | 4 ++-- .../shared/misc/fix/bitmap/FixRecycledBitmapPatch.kt | 2 +- .../patches/shared/misc/gms/GmsCoreSupportPatch.kt | 4 ++-- .../shared/misc/litho/filter/LithoFilterPatch.kt | 6 +++--- .../shared/misc/litho/node/TreeNodeElementHookPatch.kt | 6 +++--- .../shared/misc/litho/relayout/LithoRelayoutPatch.kt | 2 +- .../shared/misc/potoken/PoTokenProviderPatch.kt | 2 +- .../patches/shared/misc/proxy/BaseNetworkProxyPatch.kt | 2 +- .../patches/shared/misc/request/BuildRequestPatch.kt | 2 +- .../patches/shared/misc/settings/SettingsPatch.kt | 2 +- .../shared/misc/settingsmenu/HideMatchingHelper.kt | 2 +- .../patches/shared/misc/spans/InclusiveSpanPatch.kt | 2 +- .../shared/misc/spoof/SpoofVideoStreamsPatch.kt | 4 ++-- .../shared/misc/spoof/UserAgentClientSpoofPatch.kt | 2 +- .../misc/spoof/appversion/BaseSpoofAppVersionPatch.kt | 2 +- .../shared/misc/textcomponent/TextComponentPatch.kt | 6 +++--- .../shared/misc/videoinformation/Fingerprints.kt | 2 +- .../app/morphe/patches/youtube/ad/HideAdsPatch.kt | 4 ++-- .../youtube/interaction/ask/AskFlyoutMenuPatch.kt | 4 ++-- .../interaction/channelsearch/ChannelSearchPatch.kt | 4 ++-- .../youtube/interaction/downloads/DownloadsPatch.kt | 4 ++-- .../hapticfeedback/DisableHapticFeedbackPatch.kt | 4 ++-- .../interaction/seekbar/EnableSlideToSeekPatch.kt | 2 +- .../interaction/seekbar/EnableTapToSeekPatch.kt | 2 +- .../seekbar/SeekbarThumbnailPreviewPatch.kt | 2 +- .../youtube/interaction/shorts/SkipLiveShortsPatch.kt | 4 ++-- .../layout/branding/header/ChangeHeaderPatch.kt | 2 +- .../layout/buttons/navigation/NavigationBarPatch.kt | 4 ++-- .../buttons/overlay/HidePlayerOverlayButtonsPatch.kt | 4 ++-- .../layout/captions/AutoTranslateCaptionsPatch.kt | 6 +++--- .../youtube/layout/captions/CaptionsInPipPatch.kt | 6 +++--- .../layout/captions/PreferredCaptionLanguagePatch.kt | 2 +- .../layout/captions/TranslatedCaptionPensPatch.kt | 2 +- .../youtube/layout/feedcards/FeedCardLayoutPatch.kt | 2 +- .../patches/youtube/layout/flyout/FlyoutPatch.kt | 8 ++++---- .../youtube/layout/formfactor/ChangeFormFactorPatch.kt | 4 ++-- .../layout/hide/ambientmode/AmbientModePatch.kt | 4 ++-- .../hide/autoplaypreview/HideAutoplayPreviewPatch.kt | 2 +- .../HideEndScreenSuggestedVideoPatch.kt | 2 +- .../layout/hide/general/HideLayoutComponentsPatch.kt | 6 +++--- .../layout/hide/infocards/HideInfoCardsPatch.kt | 2 +- .../hide/relatedvideos/HideRelatedVideosPatch.kt | 2 +- .../hide/settingsmenu/HideSettingsMenuFilterPatch.kt | 2 +- .../layout/hide/shorts/HideShortsComponentsPatch.kt | 6 +++--- .../signintotvpopup/DisableSignInToTVPopupPatch.kt | 2 +- .../youtube/layout/miniplayer/MiniplayerPatch.kt | 8 ++++---- .../layout/music/OverrideYouTubeMusicButtonsPatch.kt | 2 +- .../layout/originaltitles/VideoTitlesHookPatch.kt | 4 ++-- .../fullscreen/DisableFullscreenGesturesPatch.kt | 2 +- .../layout/player/icons/PlayerIconStylePatch.kt | 2 +- .../layout/playlistautoplay/NavigationIntentHook.kt | 2 +- .../returnyoutubedislike/ReturnYouTubeDislikePatch.kt | 4 ++-- .../layout/sharesheet/OpenSystemShareSheetPatch.kt | 6 +++--- .../layout/shortsautoplay/ShortsAutoplayPatch.kt | 2 +- .../morphe/patches/youtube/layout/theme/ThemePatch.kt | 4 ++-- .../youtube/misc/backgesture/BackGesturePatch.kt | 2 +- .../youtube/misc/contexthook/ClientContextHookPatch.kt | 2 +- .../misc/engagement/EngagementPanelHookPatch.kt | 2 +- .../fix/backtoexitgesture/FixBackToExitGesturePatch.kt | 2 +- .../playbackspeed/FIxPlaybackSpeedWhilePlayingPatch.kt | 2 +- .../misc/fix/preference/FixPreferenceIconPatch.kt | 4 ++-- .../videoactionbar/RestoreOldVideoActionBarPatch.kt | 4 ++-- .../youtube/misc/imageurlhook/CronetImageURLHook.kt | 2 +- .../youtube/misc/links/OpenLinksExternallyPatch.kt | 2 +- .../misc/litho/rendernext/DisableRenderNextPatch.kt | 2 +- .../youtube/misc/loadvideo/LoadVideoHookPatch.kt | 4 ++-- .../youtube/misc/navigation/NavigationBarHookPatch.kt | 4 ++-- .../misc/playercontrols/LegacyPlayerControlsPatch.kt | 8 ++++---- .../patches/youtube/misc/settings/SettingsPatch.kt | 2 +- .../patches/youtube/shared/OpenVideoIntentPatch.kt | 2 +- .../youtube/video/codecs/DisableVideoCodecsPatch.kt | 2 +- .../patches/youtube/video/format/VideoFormatPatch.kt | 8 ++++---- .../youtube/video/information/VideoInformationPatch.kt | 4 ++-- .../playerresponse/PlayerResponseMethodHookPatch.kt | 2 +- .../video/quality/AdvancedVideoQualityMenuPatch.kt | 2 +- .../video/quality/HidePremiumVideoQualityPatch.kt | 2 +- .../youtube/video/quality/RememberVideoQualityPatch.kt | 6 +++--- .../patches/youtube/video/series/HistoryPatch.kt | 4 ++-- .../youtube/video/series/NativeHistoryMatching.kt | 2 +- .../video/speed/custom/CustomPlaybackSpeedPatch.kt | 4 ++-- .../music/interaction/jam/JamPatchRegressionTest.kt | 2 +- 132 files changed, 220 insertions(+), 220 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f6d4e14f70..55efe0293b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,7 +2,7 @@ [versions] morphe-patcher = "2.0.0" -morphe-patches-library = "2.0.0" +morphe-patches-library = "2.0.1-dev.1" smali = "4220ae75f7" agp = "9.1.0" annotation = "1.10.0" diff --git a/patches/src/main/kotlin/app/morphe/patches/all/misc/clone/CloneAppPatch.kt b/patches/src/main/kotlin/app/morphe/patches/all/misc/clone/CloneAppPatch.kt index 8785883504..8960da064e 100644 --- a/patches/src/main/kotlin/app/morphe/patches/all/misc/clone/CloneAppPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/all/misc/clone/CloneAppPatch.kt @@ -37,11 +37,11 @@ import app.morphe.patcher.patch.booleanOption import app.morphe.patcher.patch.bytecodePatch import app.morphe.patcher.patch.resourcePatch import app.morphe.patcher.patch.stringOption +import app.morphe.patcher.util.matchAllMethodIndicesForEach import app.morphe.patches.util.resource.editText import app.morphe.patches.util.resource.transformStringResources import app.morphe.util.asSequence import app.morphe.util.findElementByAttributeValue -import app.morphe.util.matchAllMethodIndicesForEach import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction import org.w3c.dom.Element diff --git a/patches/src/main/kotlin/app/morphe/patches/all/misc/updates/DisablePlayStoreUpdatesPatch.kt b/patches/src/main/kotlin/app/morphe/patches/all/misc/updates/DisablePlayStoreUpdatesPatch.kt index bb3e3d0f69..689979ed4a 100644 --- a/patches/src/main/kotlin/app/morphe/patches/all/misc/updates/DisablePlayStoreUpdatesPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/all/misc/updates/DisablePlayStoreUpdatesPatch.kt @@ -34,10 +34,10 @@ import app.morphe.patcher.fieldAccess import app.morphe.patcher.methodCall import app.morphe.patcher.patch.bytecodePatch import app.morphe.patcher.patch.resourcePatch -import app.morphe.util.getFreeRegisterProvider +import app.morphe.patcher.util.getFreeRegisterProvider +import app.morphe.patcher.util.matchAllMethodIndicesForEach +import app.morphe.patcher.util.returnEarly import app.morphe.util.getNode -import app.morphe.util.matchAllMethodIndicesForEach -import app.morphe.util.returnEarly import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction diff --git a/patches/src/main/kotlin/app/morphe/patches/music/interaction/crossfade/CrossfadePatch.kt b/patches/src/main/kotlin/app/morphe/patches/music/interaction/crossfade/CrossfadePatch.kt index 665aa70187..d3052a7c87 100644 --- a/patches/src/main/kotlin/app/morphe/patches/music/interaction/crossfade/CrossfadePatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/music/interaction/crossfade/CrossfadePatch.kt @@ -18,6 +18,7 @@ import app.morphe.patcher.literal import app.morphe.patcher.methodCall import app.morphe.patcher.opcode import app.morphe.patcher.patch.bytecodePatch +import app.morphe.patcher.util.getReference import app.morphe.patcher.util.proxy.mutableTypes.MutableClass import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable import app.morphe.patches.music.misc.extension.sharedExtensionPatch @@ -31,7 +32,6 @@ import app.morphe.patches.shared.misc.settings.preference.NonInteractivePreferen import app.morphe.patches.shared.misc.settings.preference.PreferenceScreenPreference import app.morphe.patches.shared.misc.settings.preference.SwitchPreference import app.morphe.patches.shared.misc.settings.preference.screenInfoPreferenceCategory -import app.morphe.util.getReference import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation diff --git a/patches/src/main/kotlin/app/morphe/patches/music/interaction/dislikeredirection/DisableDislikeRedirectionPatch.kt b/patches/src/main/kotlin/app/morphe/patches/music/interaction/dislikeredirection/DisableDislikeRedirectionPatch.kt index 4f3eb63db2..db0c916fd8 100644 --- a/patches/src/main/kotlin/app/morphe/patches/music/interaction/dislikeredirection/DisableDislikeRedirectionPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/music/interaction/dislikeredirection/DisableDislikeRedirectionPatch.kt @@ -10,6 +10,7 @@ package app.morphe.patches.music.interaction.dislikeredirection import app.morphe.patcher.extensions.InstructionExtensions.addInstructionsWithLabels import app.morphe.patcher.extensions.InstructionExtensions.getInstruction import app.morphe.patcher.patch.bytecodePatch +import app.morphe.patcher.util.getReference import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod import app.morphe.patcher.util.smali.ExternalLabel import app.morphe.patches.music.misc.extension.sharedExtensionPatch @@ -19,7 +20,6 @@ import app.morphe.patches.music.misc.settings.PreferenceScreen import app.morphe.patches.music.misc.settings.settingsPatch import app.morphe.patches.music.shared.Constants.COMPATIBILITY_YOUTUBE_MUSIC import app.morphe.patches.shared.misc.settings.preference.SwitchPreference -import app.morphe.util.getReference import app.morphe.util.indexOfFirstInstructionOrThrow import app.morphe.util.indexOfFirstInstructionReversedOrThrow import com.android.tools.smali.dexlib2.Opcode diff --git a/patches/src/main/kotlin/app/morphe/patches/music/interaction/downloads/DownloadsPatch.kt b/patches/src/main/kotlin/app/morphe/patches/music/interaction/downloads/DownloadsPatch.kt index 659b7e8c29..fdb16eb1a4 100644 --- a/patches/src/main/kotlin/app/morphe/patches/music/interaction/downloads/DownloadsPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/music/interaction/downloads/DownloadsPatch.kt @@ -10,6 +10,7 @@ package app.morphe.patches.music.interaction.downloads import app.morphe.patcher.extensions.InstructionExtensions.addInstructionsWithLabels import app.morphe.patcher.patch.bytecodePatch import app.morphe.patcher.patch.resourcePatch +import app.morphe.patcher.util.cloneParameters import app.morphe.patches.music.misc.settings.PreferenceScreen import app.morphe.patches.music.misc.settings.settingsPatch import app.morphe.patches.music.shared.Constants.COMPATIBILITY_YOUTUBE_MUSIC @@ -21,7 +22,6 @@ import app.morphe.patches.shared.misc.settings.preference.SwitchPreference import app.morphe.patches.shared.misc.settings.preference.TextPreference import app.morphe.patches.shared.misc.textcomponent.hookSpannableString import app.morphe.patches.shared.misc.textcomponent.textComponentPatch -import app.morphe.util.cloneParameters private val OFFLINE_PLAYBACK_PERMISSIONS = listOf( "android.permission.WAKE_LOCK", diff --git a/patches/src/main/kotlin/app/morphe/patches/music/interaction/jam/Fingerprints.kt b/patches/src/main/kotlin/app/morphe/patches/music/interaction/jam/Fingerprints.kt index dd9d41bbb9..b2249b5f74 100644 --- a/patches/src/main/kotlin/app/morphe/patches/music/interaction/jam/Fingerprints.kt +++ b/patches/src/main/kotlin/app/morphe/patches/music/interaction/jam/Fingerprints.kt @@ -19,8 +19,8 @@ import app.morphe.patcher.patch.BytecodePatchContext import app.morphe.patcher.resource.ResourceType import app.morphe.patcher.resourceLiteral import app.morphe.patcher.string +import app.morphe.patcher.util.getReference import app.morphe.util.findInstructionIndicesReversed -import app.morphe.util.getReference import app.morphe.util.indexOfFirstInstruction import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode diff --git a/patches/src/main/kotlin/app/morphe/patches/music/interaction/jam/JamABI.kt b/patches/src/main/kotlin/app/morphe/patches/music/interaction/jam/JamABI.kt index 7c39e7b787..e6289fd5a5 100644 --- a/patches/src/main/kotlin/app/morphe/patches/music/interaction/jam/JamABI.kt +++ b/patches/src/main/kotlin/app/morphe/patches/music/interaction/jam/JamABI.kt @@ -16,10 +16,10 @@ import app.morphe.patcher.instanceOf import app.morphe.patcher.methodCall import app.morphe.patcher.newInstance import app.morphe.patcher.patch.BytecodePatchContext +import app.morphe.patcher.util.getReference import app.morphe.util.findInstructionIndicesReversed import app.morphe.util.findInstructionIndicesReversedOrThrow import app.morphe.util.getMutableMethod -import app.morphe.util.getReference import app.morphe.util.indexOfFirstInstruction import app.morphe.util.indexOfFirstInstructionReversedOrThrow import com.android.tools.smali.dexlib2.AccessFlags diff --git a/patches/src/main/kotlin/app/morphe/patches/music/interaction/jam/JamBridgeInstaller.kt b/patches/src/main/kotlin/app/morphe/patches/music/interaction/jam/JamBridgeInstaller.kt index e224b31e7f..b8a76e921a 100644 --- a/patches/src/main/kotlin/app/morphe/patches/music/interaction/jam/JamBridgeInstaller.kt +++ b/patches/src/main/kotlin/app/morphe/patches/music/interaction/jam/JamBridgeInstaller.kt @@ -9,11 +9,11 @@ package app.morphe.patches.music.interaction.jam import app.morphe.patcher.extensions.InstructionExtensions.addInstructions import app.morphe.patcher.patch.BytecodePatchContext +import app.morphe.patcher.util.numberOfParameterRegisters import app.morphe.patcher.util.proxy.mutableTypes.MutableClass import app.morphe.util.cloneMutable import app.morphe.util.findInstructionIndicesReversedOrThrow import app.morphe.util.getMutableMethod -import app.morphe.util.numberOfParameterRegisters import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.iface.reference.MethodReference diff --git a/patches/src/main/kotlin/app/morphe/patches/music/interaction/jam/JamUIABI.kt b/patches/src/main/kotlin/app/morphe/patches/music/interaction/jam/JamUIABI.kt index edca92f7f9..5729dfb5cf 100644 --- a/patches/src/main/kotlin/app/morphe/patches/music/interaction/jam/JamUIABI.kt +++ b/patches/src/main/kotlin/app/morphe/patches/music/interaction/jam/JamUIABI.kt @@ -13,10 +13,10 @@ import app.morphe.patcher.extensions.InstructionExtensions.getInstruction import app.morphe.patcher.fieldAccess import app.morphe.patcher.methodCall import app.morphe.patcher.patch.BytecodePatchContext +import app.morphe.patcher.util.getReference +import app.morphe.patcher.util.p0Register import app.morphe.util.findInstructionIndicesReversed import app.morphe.util.findInstructionIndicesReversedOrThrow -import app.morphe.util.getReference -import app.morphe.util.p0Register import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.iface.ClassDef diff --git a/patches/src/main/kotlin/app/morphe/patches/music/interaction/jam/JamUIInstaller.kt b/patches/src/main/kotlin/app/morphe/patches/music/interaction/jam/JamUIInstaller.kt index da81b632b7..06f0562cda 100644 --- a/patches/src/main/kotlin/app/morphe/patches/music/interaction/jam/JamUIInstaller.kt +++ b/patches/src/main/kotlin/app/morphe/patches/music/interaction/jam/JamUIInstaller.kt @@ -14,11 +14,11 @@ import app.morphe.patcher.extensions.InstructionExtensions.instructions import app.morphe.patcher.extensions.InstructionExtensions.replaceInstruction import app.morphe.patcher.methodCall import app.morphe.patcher.patch.BytecodePatchContext +import app.morphe.patcher.util.matchAllMethodIndicesForEach import app.morphe.patcher.util.smali.ExternalLabel import app.morphe.util.findInstructionIndicesReversedOrThrow import app.morphe.util.getMutableMethod import app.morphe.util.indexOfFirstInstructionOrThrow -import app.morphe.util.matchAllMethodIndicesForEach import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.iface.Method diff --git a/patches/src/main/kotlin/app/morphe/patches/music/interaction/remember/shufflestate/RememberShuffleStatePatch.kt b/patches/src/main/kotlin/app/morphe/patches/music/interaction/remember/shufflestate/RememberShuffleStatePatch.kt index 98817502bd..bbf382a8f4 100644 --- a/patches/src/main/kotlin/app/morphe/patches/music/interaction/remember/shufflestate/RememberShuffleStatePatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/music/interaction/remember/shufflestate/RememberShuffleStatePatch.kt @@ -13,6 +13,8 @@ import app.morphe.patcher.extensions.InstructionExtensions.addInstructions import app.morphe.patcher.extensions.InstructionExtensions.getInstruction import app.morphe.patcher.methodCall import app.morphe.patcher.patch.bytecodePatch +import app.morphe.patcher.util.getReference +import app.morphe.patcher.util.numberOfParameterRegistersLogical import app.morphe.patches.music.misc.extension.sharedExtensionPatch import app.morphe.patches.music.misc.playservice.versionCheckPatch import app.morphe.patches.music.misc.settings.PreferenceScreen @@ -23,11 +25,9 @@ import app.morphe.patches.music.video.information.musicVideoInformationPatch import app.morphe.patches.shared.misc.settings.preference.SwitchPreference import app.morphe.util.addStaticFieldToExtension import app.morphe.util.cloneMutable -import app.morphe.util.getReference import app.morphe.util.indexOfFirstInstruction import app.morphe.util.indexOfFirstInstructionOrThrow import app.morphe.util.indexOfFirstInstructionReversedOrThrow -import app.morphe.util.numberOfParameterRegistersLogical import app.morphe.util.toPublicAccessFlags import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction diff --git a/patches/src/main/kotlin/app/morphe/patches/music/interaction/scrobbling/ScrobblingPatch.kt b/patches/src/main/kotlin/app/morphe/patches/music/interaction/scrobbling/ScrobblingPatch.kt index 788952e9bf..2b93933e06 100644 --- a/patches/src/main/kotlin/app/morphe/patches/music/interaction/scrobbling/ScrobblingPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/music/interaction/scrobbling/ScrobblingPatch.kt @@ -10,26 +10,26 @@ package app.morphe.patches.music.interaction.scrobbling import app.morphe.patcher.extensions.InstructionExtensions.addInstructions import app.morphe.patcher.extensions.InstructionExtensions.getInstruction import app.morphe.patcher.patch.bytecodePatch +import app.morphe.patcher.util.getFreeRegisterProvider +import app.morphe.patcher.util.getReference import app.morphe.patches.music.misc.extension.sharedExtensionPatch import app.morphe.patches.music.misc.settings.PreferenceScreen import app.morphe.patches.music.misc.settings.settingsPatch import app.morphe.patches.music.shared.Constants.COMPATIBILITY_YOUTUBE_MUSIC import app.morphe.patches.music.shared.hookMediaSessionArgument import app.morphe.patches.music.video.information.musicVideoInformationPatch +import app.morphe.patches.shared.MediaSessionSetMetadataFingerprint +import app.morphe.patches.shared.MediaSessionSetPlaybackStateFingerprint import app.morphe.patches.shared.layout.returnyoutubedislike.DislikeFingerprint import app.morphe.patches.shared.layout.returnyoutubedislike.EndpointServiceNameFingerprint import app.morphe.patches.shared.layout.returnyoutubedislike.likeEndpointParserFingerprint import app.morphe.patches.shared.layout.returnyoutubedislike.requestParameterCheckFingerprint -import app.morphe.patches.shared.MediaSessionSetMetadataFingerprint -import app.morphe.patches.shared.MediaSessionSetPlaybackStateFingerprint import app.morphe.patches.shared.misc.settings.preference.NonInteractivePreference import app.morphe.patches.shared.misc.settings.preference.PreferenceCategory import app.morphe.patches.shared.misc.settings.preference.PreferenceScreenPreference.Sorting import app.morphe.patches.shared.misc.settings.preference.SwitchPreference import app.morphe.patches.shared.misc.settings.preference.TextPreference import app.morphe.patches.shared.misc.settings.preference.screenInfoPreferenceCategory -import app.morphe.util.getFreeRegisterProvider -import app.morphe.util.getReference import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction import com.android.tools.smali.dexlib2.iface.reference.FieldReference diff --git a/patches/src/main/kotlin/app/morphe/patches/music/layout/branding/CustomBrandingPatch.kt b/patches/src/main/kotlin/app/morphe/patches/music/layout/branding/CustomBrandingPatch.kt index 88c6eba0e8..866c39685a 100644 --- a/patches/src/main/kotlin/app/morphe/patches/music/layout/branding/CustomBrandingPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/music/layout/branding/CustomBrandingPatch.kt @@ -14,6 +14,8 @@ import app.morphe.patcher.extensions.InstructionExtensions.addInstructions import app.morphe.patcher.extensions.InstructionExtensions.getInstruction import app.morphe.patcher.extensions.InstructionExtensions.replaceInstruction import app.morphe.patcher.patch.bytecodePatch +import app.morphe.patcher.util.getReference +import app.morphe.patcher.util.returnEarly import app.morphe.patches.music.misc.extension.sharedExtensionPatch import app.morphe.patches.music.misc.gms.Constants.MUSIC_MAIN_ACTIVITY_NAME import app.morphe.patches.music.misc.gms.Constants.MUSIC_PACKAGE_NAME @@ -26,9 +28,7 @@ import app.morphe.patches.shared.layout.branding.baseCustomBrandingPatch import app.morphe.patches.shared.misc.lottie.LOTTIE_ANIMATION_VIEW_CLASS_TYPE import app.morphe.patches.shared.misc.lottie.lottieAnimationPatch import app.morphe.patches.shared.misc.settings.preference.ListPreference -import app.morphe.util.getReference import app.morphe.util.indexOfFirstInstructionOrThrow -import app.morphe.util.returnEarly import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction diff --git a/patches/src/main/kotlin/app/morphe/patches/music/layout/branding/header/ChangeHeaderPatch.kt b/patches/src/main/kotlin/app/morphe/patches/music/layout/branding/header/ChangeHeaderPatch.kt index a77e829bf1..819d58fce7 100644 --- a/patches/src/main/kotlin/app/morphe/patches/music/layout/branding/header/ChangeHeaderPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/music/layout/branding/header/ChangeHeaderPatch.kt @@ -12,10 +12,10 @@ import app.morphe.patcher.extensions.InstructionExtensions.getInstruction import app.morphe.patcher.patch.bytecodePatch import app.morphe.patcher.resource.ResourceType import app.morphe.patcher.resourceLiteral +import app.morphe.patcher.util.matchAllMethodIndicesForEach import app.morphe.patches.music.misc.settings.PreferenceScreen import app.morphe.patches.music.shared.Constants.COMPATIBILITY_YOUTUBE_MUSIC import app.morphe.patches.shared.layout.branding.header.baseChangeHeaderPatch -import app.morphe.util.matchAllMethodIndicesForEach import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction private val targetResourceDirectoryNames = mapOf( diff --git a/patches/src/main/kotlin/app/morphe/patches/music/layout/buttons/HideButtonsPatch.kt b/patches/src/main/kotlin/app/morphe/patches/music/layout/buttons/HideButtonsPatch.kt index f5d1b485fc..7612321b5e 100644 --- a/patches/src/main/kotlin/app/morphe/patches/music/layout/buttons/HideButtonsPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/music/layout/buttons/HideButtonsPatch.kt @@ -17,15 +17,15 @@ import app.morphe.patcher.extensions.InstructionExtensions.getInstruction import app.morphe.patcher.patch.bytecodePatch import app.morphe.patcher.resource.ResourceType import app.morphe.patcher.resource.resourceId +import app.morphe.patcher.util.findFreeRegister +import app.morphe.patcher.util.p0Register import app.morphe.patches.music.misc.extension.sharedExtensionPatch import app.morphe.patches.music.misc.settings.PreferenceScreen import app.morphe.patches.music.misc.settings.settingsPatch import app.morphe.patches.music.shared.Constants.COMPATIBILITY_YOUTUBE_MUSIC import app.morphe.patches.shared.misc.settings.preference.SwitchPreference -import app.morphe.util.findFreeRegister import app.morphe.util.indexOfFirstInstructionOrThrow import app.morphe.util.indexOfFirstLiteralInstructionOrThrow -import app.morphe.util.p0Register import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction diff --git a/patches/src/main/kotlin/app/morphe/patches/music/layout/hide/flyoutmenu/HideFlyoutMenuComponentsPatch.kt b/patches/src/main/kotlin/app/morphe/patches/music/layout/hide/flyoutmenu/HideFlyoutMenuComponentsPatch.kt index 482b5c0247..cac4e60203 100644 --- a/patches/src/main/kotlin/app/morphe/patches/music/layout/hide/flyoutmenu/HideFlyoutMenuComponentsPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/music/layout/hide/flyoutmenu/HideFlyoutMenuComponentsPatch.kt @@ -13,6 +13,7 @@ import app.morphe.patcher.extensions.InstructionExtensions.getInstruction import app.morphe.patcher.patch.bytecodePatch import app.morphe.patcher.resource.ResourceType import app.morphe.patcher.resource.resourceId +import app.morphe.patcher.util.getReference import app.morphe.patcher.util.smali.ExternalLabel import app.morphe.patches.music.misc.extension.sharedExtensionPatch import app.morphe.patches.music.misc.litho.filter.lithoFilterPatch @@ -22,7 +23,6 @@ import app.morphe.patches.music.shared.Constants.COMPATIBILITY_YOUTUBE_MUSIC import app.morphe.patches.shared.misc.litho.filter.addLithoFilter import app.morphe.patches.shared.misc.settings.preference.PreferenceScreenPreference import app.morphe.patches.shared.misc.settings.preference.SwitchPreference -import app.morphe.util.getReference import app.morphe.util.indexOfFirstInstructionOrThrow import app.morphe.util.indexOfFirstLiteralInstructionOrThrow import com.android.tools.smali.dexlib2.Opcode diff --git a/patches/src/main/kotlin/app/morphe/patches/music/layout/hide/settingsmenu/HideSettingsMenuFilterPatch.kt b/patches/src/main/kotlin/app/morphe/patches/music/layout/hide/settingsmenu/HideSettingsMenuFilterPatch.kt index 78c3200ba7..b0f9b40335 100644 --- a/patches/src/main/kotlin/app/morphe/patches/music/layout/hide/settingsmenu/HideSettingsMenuFilterPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/music/layout/hide/settingsmenu/HideSettingsMenuFilterPatch.kt @@ -9,6 +9,8 @@ package app.morphe.patches.music.layout.hide.settingsmenu import app.morphe.patcher.extensions.InstructionExtensions.addInstructionsWithLabels import app.morphe.patcher.patch.bytecodePatch +import app.morphe.patcher.util.getFreeRegisterProvider +import app.morphe.patcher.util.getReference import app.morphe.patches.music.misc.extension.sharedExtensionPatch import app.morphe.patches.music.misc.settings.PreferenceScreen import app.morphe.patches.music.misc.settings.SETTINGS_HEADERS_FRAGMENT_CLASS @@ -20,8 +22,6 @@ import app.morphe.patches.shared.misc.settingsmenu.HIDE_MATCHING_METHOD import app.morphe.patches.shared.misc.settingsmenu.SETTINGS_MENU_FILTER_CLASS import app.morphe.patches.shared.misc.settingsmenu.injectHideMatchingHelper import app.morphe.patches.shared.misc.settingsmenu.injectSettingsMenuFilterHook -import app.morphe.util.getFreeRegisterProvider -import app.morphe.util.getReference import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction import com.android.tools.smali.dexlib2.iface.reference.FieldReference diff --git a/patches/src/main/kotlin/app/morphe/patches/music/layout/lyrics/LyricsPatch.kt b/patches/src/main/kotlin/app/morphe/patches/music/layout/lyrics/LyricsPatch.kt index c2ff1432a0..2eae714ebe 100644 --- a/patches/src/main/kotlin/app/morphe/patches/music/layout/lyrics/LyricsPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/music/layout/lyrics/LyricsPatch.kt @@ -12,6 +12,7 @@ import app.morphe.patcher.extensions.InstructionExtensions.addInstruction import app.morphe.patcher.extensions.InstructionExtensions.getInstruction import app.morphe.patcher.patch.bytecodePatch import app.morphe.patcher.patch.resourcePatch +import app.morphe.patcher.util.getReference import app.morphe.patches.all.misc.resources.addResourcesPatch import app.morphe.patches.music.misc.extension.sharedExtensionPatch import app.morphe.patches.music.misc.litho.filter.lithoFilterPatch @@ -33,7 +34,6 @@ import app.morphe.patches.shared.misc.settings.preference.TextPreference import app.morphe.patches.shared.misc.settings.preference.screenInfoPreferenceCategory import app.morphe.util.ResourceGroup import app.morphe.util.copyResources -import app.morphe.util.getReference import app.morphe.util.indexOfFirstInstructionOrThrow import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode diff --git a/patches/src/main/kotlin/app/morphe/patches/music/layout/miniplayer/ChangeMiniplayerColorPatch.kt b/patches/src/main/kotlin/app/morphe/patches/music/layout/miniplayer/ChangeMiniplayerColorPatch.kt index 34c26d4a80..cabfe0db7d 100644 --- a/patches/src/main/kotlin/app/morphe/patches/music/layout/miniplayer/ChangeMiniplayerColorPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/music/layout/miniplayer/ChangeMiniplayerColorPatch.kt @@ -12,6 +12,8 @@ package app.morphe.patches.music.layout.miniplayer import app.morphe.patcher.extensions.InstructionExtensions.addInstructions import app.morphe.patcher.extensions.InstructionExtensions.getInstruction import app.morphe.patcher.patch.bytecodePatch +import app.morphe.patcher.util.findFreeRegister +import app.morphe.patcher.util.getReference import app.morphe.patches.music.misc.extension.sharedExtensionPatch import app.morphe.patches.music.misc.playservice.is_9_32_or_greater import app.morphe.patches.music.misc.settings.PreferenceScreen @@ -19,8 +21,6 @@ import app.morphe.patches.music.misc.settings.settingsPatch import app.morphe.patches.music.shared.Constants.COMPATIBILITY_YOUTUBE_MUSIC import app.morphe.patches.shared.misc.settings.preference.SwitchPreference import app.morphe.util.addInstructionsAtControlFlowLabel -import app.morphe.util.findFreeRegister -import app.morphe.util.getReference import app.morphe.util.indexOfFirstInstructionOrThrow import app.morphe.util.indexOfFirstInstructionReversedOrThrow import com.android.tools.smali.dexlib2.AccessFlags diff --git a/patches/src/main/kotlin/app/morphe/patches/music/layout/miniplayer/EnableForcedMiniplayerPatch.kt b/patches/src/main/kotlin/app/morphe/patches/music/layout/miniplayer/EnableForcedMiniplayerPatch.kt index ebdf181991..608e20fd4f 100644 --- a/patches/src/main/kotlin/app/morphe/patches/music/layout/miniplayer/EnableForcedMiniplayerPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/music/layout/miniplayer/EnableForcedMiniplayerPatch.kt @@ -10,12 +10,12 @@ package app.morphe.patches.music.layout.miniplayer import app.morphe.patcher.extensions.InstructionExtensions.addInstructions import app.morphe.patcher.extensions.InstructionExtensions.getInstruction import app.morphe.patcher.patch.bytecodePatch +import app.morphe.patcher.util.getReference import app.morphe.patches.music.misc.extension.sharedExtensionPatch import app.morphe.patches.music.misc.settings.PreferenceScreen import app.morphe.patches.music.misc.settings.settingsPatch import app.morphe.patches.music.shared.Constants.COMPATIBILITY_YOUTUBE_MUSIC import app.morphe.patches.shared.misc.settings.preference.SwitchPreference -import app.morphe.util.getReference import app.morphe.util.indexOfFirstInstructionOrThrow import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction diff --git a/patches/src/main/kotlin/app/morphe/patches/music/layout/miniplayer/EnableSwipeToDismissMiniplayerPatch.kt b/patches/src/main/kotlin/app/morphe/patches/music/layout/miniplayer/EnableSwipeToDismissMiniplayerPatch.kt index a84c1505c6..c82fad5ca0 100644 --- a/patches/src/main/kotlin/app/morphe/patches/music/layout/miniplayer/EnableSwipeToDismissMiniplayerPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/music/layout/miniplayer/EnableSwipeToDismissMiniplayerPatch.kt @@ -17,6 +17,8 @@ import app.morphe.patcher.fieldAccess import app.morphe.patcher.methodCall import app.morphe.patcher.opcode import app.morphe.patcher.patch.bytecodePatch +import app.morphe.patcher.util.findFreeRegister +import app.morphe.patcher.util.getReference import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable import app.morphe.patcher.util.smali.ExternalLabel import app.morphe.patches.music.misc.extension.sharedExtensionPatch @@ -26,8 +28,6 @@ import app.morphe.patches.music.misc.settings.settingsPatch import app.morphe.patches.music.shared.Constants.COMPATIBILITY_YOUTUBE_MUSIC import app.morphe.patches.shared.misc.settings.preference.SwitchPreference import app.morphe.util.addInstructionsAtControlFlowLabel -import app.morphe.util.findFreeRegister -import app.morphe.util.getReference import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation diff --git a/patches/src/main/kotlin/app/morphe/patches/music/layout/returnyoutubedislike/ReturnYouTubeDislikePatch.kt b/patches/src/main/kotlin/app/morphe/patches/music/layout/returnyoutubedislike/ReturnYouTubeDislikePatch.kt index bf7fcc313f..0dadc3c741 100644 --- a/patches/src/main/kotlin/app/morphe/patches/music/layout/returnyoutubedislike/ReturnYouTubeDislikePatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/music/layout/returnyoutubedislike/ReturnYouTubeDislikePatch.kt @@ -11,6 +11,8 @@ import app.morphe.patcher.extensions.InstructionExtensions.addInstructions import app.morphe.patcher.extensions.InstructionExtensions.getInstruction import app.morphe.patcher.patch.bytecodePatch import app.morphe.patcher.patch.resourcePatch +import app.morphe.patcher.util.getFreeRegisterProvider +import app.morphe.patcher.util.getReference import app.morphe.patches.music.misc.settings.PreferenceScreen import app.morphe.patches.music.misc.settings.settingsPatch import app.morphe.patches.music.shared.Constants.COMPATIBILITY_YOUTUBE_MUSIC @@ -28,8 +30,6 @@ import app.morphe.patches.shared.misc.settings.preference.SwitchPreference import app.morphe.patches.shared.misc.settings.preference.screenInfoPreferenceCategory import app.morphe.patches.shared.misc.textcomponent.hookSpannableString import app.morphe.patches.shared.misc.textcomponent.textComponentPatch -import app.morphe.util.getFreeRegisterProvider -import app.morphe.util.getReference import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction import com.android.tools.smali.dexlib2.iface.reference.FieldReference diff --git a/patches/src/main/kotlin/app/morphe/patches/music/layout/sponsorblock/SponsorBlockPatch.kt b/patches/src/main/kotlin/app/morphe/patches/music/layout/sponsorblock/SponsorBlockPatch.kt index 7dc16045e2..62e86c6d51 100644 --- a/patches/src/main/kotlin/app/morphe/patches/music/layout/sponsorblock/SponsorBlockPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/music/layout/sponsorblock/SponsorBlockPatch.kt @@ -9,6 +9,7 @@ package app.morphe.patches.music.layout.sponsorblock import app.morphe.patcher.extensions.InstructionExtensions.addInstructions import app.morphe.patcher.patch.bytecodePatch +import app.morphe.patcher.util.findFreeRegister import app.morphe.patches.all.misc.resources.addAppResources import app.morphe.patches.all.misc.resources.addResourcesPatch import app.morphe.patches.music.misc.extension.sharedExtensionPatch @@ -26,7 +27,6 @@ import app.morphe.patches.shared.misc.settings.preference.SwitchPreference import app.morphe.patches.shared.misc.settings.preference.TextPreference import app.morphe.patches.shared.misc.settings.preference.screenInfoPreferenceCategory import app.morphe.patches.youtube.layout.sponsorblock.categoryPreference -import app.morphe.util.findFreeRegister import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction import com.android.tools.smali.dexlib2.iface.reference.FieldReference diff --git a/patches/src/main/kotlin/app/morphe/patches/music/layout/startpage/ChangeStartPagePatch.kt b/patches/src/main/kotlin/app/morphe/patches/music/layout/startpage/ChangeStartPagePatch.kt index 6fdafbe1f7..4fcc48e355 100644 --- a/patches/src/main/kotlin/app/morphe/patches/music/layout/startpage/ChangeStartPagePatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/music/layout/startpage/ChangeStartPagePatch.kt @@ -11,6 +11,7 @@ import app.morphe.patcher.extensions.InstructionExtensions.addInstruction import app.morphe.patcher.extensions.InstructionExtensions.addInstructions import app.morphe.patcher.extensions.InstructionExtensions.getInstruction import app.morphe.patcher.patch.bytecodePatch +import app.morphe.patcher.util.getReference import app.morphe.patches.music.misc.extension.sharedExtensionPatch import app.morphe.patches.music.misc.settings.PreferenceScreen import app.morphe.patches.music.misc.settings.settingsPatch @@ -18,7 +19,6 @@ import app.morphe.patches.music.shared.Constants.COMPATIBILITY_YOUTUBE_MUSIC import app.morphe.patches.music.shared.MusicActivityOnCreateFingerprint import app.morphe.patches.shared.misc.settings.preference.ListPreference import app.morphe.util.findInstructionIndicesReversed -import app.morphe.util.getReference import app.morphe.util.indexOfFirstInstructionReversed import app.morphe.util.indexOfFirstStringInstructionOrThrow import com.android.tools.smali.dexlib2.Opcode diff --git a/patches/src/main/kotlin/app/morphe/patches/music/misc/androidauto/AndroidAutoPatch.kt b/patches/src/main/kotlin/app/morphe/patches/music/misc/androidauto/AndroidAutoPatch.kt index f34395e856..a708375144 100644 --- a/patches/src/main/kotlin/app/morphe/patches/music/misc/androidauto/AndroidAutoPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/music/misc/androidauto/AndroidAutoPatch.kt @@ -16,8 +16,13 @@ import app.morphe.patcher.extensions.InstructionExtensions.instructions import app.morphe.patcher.patch.BytecodePatchContext import app.morphe.patcher.patch.PatchException import app.morphe.patcher.patch.bytecodePatch +import app.morphe.patcher.util.addFreeRegisters +import app.morphe.patcher.util.findFreeRegister +import app.morphe.patcher.util.getReference +import app.morphe.patcher.util.p0Register import app.morphe.patcher.util.proxy.mutableTypes.MutableClass import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod +import app.morphe.patcher.util.returnEarly import app.morphe.patches.music.interaction.jam.addBridge import app.morphe.patches.music.interaction.jam.decode import app.morphe.patches.music.interaction.jam.implementsType @@ -27,13 +32,8 @@ import app.morphe.patches.music.misc.extension.sharedExtensionPatch import app.morphe.patches.music.misc.playservice.is_9_20_or_greater import app.morphe.patches.music.misc.settings.settingsPatch import app.morphe.patches.music.shared.Constants.COMPATIBILITY_YOUTUBE_MUSIC -import app.morphe.util.addFreeRegisters import app.morphe.util.cloneMutable -import app.morphe.util.findFreeRegister import app.morphe.util.findMutableMethodOf -import app.morphe.util.getReference -import app.morphe.util.p0Register -import app.morphe.util.returnEarly import app.morphe.util.toPublicAccessFlags import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode diff --git a/patches/src/main/kotlin/app/morphe/patches/music/misc/androidauto/AndroidAutoSearchPatch.kt b/patches/src/main/kotlin/app/morphe/patches/music/misc/androidauto/AndroidAutoSearchPatch.kt index b1a258ca66..f242381650 100644 --- a/patches/src/main/kotlin/app/morphe/patches/music/misc/androidauto/AndroidAutoSearchPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/music/misc/androidauto/AndroidAutoSearchPatch.kt @@ -10,11 +10,11 @@ package app.morphe.patches.music.misc.androidauto import app.morphe.patcher.extensions.InstructionExtensions.addInstructions import app.morphe.patcher.extensions.InstructionExtensions.instructions import app.morphe.patcher.patch.BytecodePatchContext +import app.morphe.patcher.util.findFreeRegister +import app.morphe.patcher.util.getReference import app.morphe.patches.music.interaction.jam.addReferenceGetter import app.morphe.patches.music.interaction.jam.implementsType import app.morphe.util.addInstructionsAtControlFlowLabel -import app.morphe.util.findFreeRegister -import app.morphe.util.getReference import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.iface.reference.FieldReference import com.android.tools.smali.dexlib2.iface.reference.MethodReference diff --git a/patches/src/main/kotlin/app/morphe/patches/music/misc/audio/EnableExclusiveAudioPlayback.kt b/patches/src/main/kotlin/app/morphe/patches/music/misc/audio/EnableExclusiveAudioPlayback.kt index a0affe3362..6ca0d8b2d6 100644 --- a/patches/src/main/kotlin/app/morphe/patches/music/misc/audio/EnableExclusiveAudioPlayback.kt +++ b/patches/src/main/kotlin/app/morphe/patches/music/misc/audio/EnableExclusiveAudioPlayback.kt @@ -17,7 +17,7 @@ import app.morphe.patches.music.misc.extension.sharedExtensionPatch import app.morphe.patches.music.misc.playservice.is_9_32_or_greater import app.morphe.patches.music.misc.settings.settingsPatch import app.morphe.patches.music.shared.Constants.COMPATIBILITY_YOUTUBE_MUSIC -import app.morphe.util.returnEarly +import app.morphe.patcher.util.returnEarly import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction @Suppress("unused") diff --git a/patches/src/main/kotlin/app/morphe/patches/music/misc/audiovideoswitch/QueueSwitchInstaller.kt b/patches/src/main/kotlin/app/morphe/patches/music/misc/audiovideoswitch/QueueSwitchInstaller.kt index 4e903229cb..1de3af1d25 100644 --- a/patches/src/main/kotlin/app/morphe/patches/music/misc/audiovideoswitch/QueueSwitchInstaller.kt +++ b/patches/src/main/kotlin/app/morphe/patches/music/misc/audiovideoswitch/QueueSwitchInstaller.kt @@ -9,12 +9,12 @@ package app.morphe.patches.music.misc.audiovideoswitch import app.morphe.patcher.extensions.InstructionExtensions.addInstructions import app.morphe.patcher.patch.BytecodePatchContext +import app.morphe.patcher.util.cloneParameters import app.morphe.patches.music.interaction.jam.addBridge import app.morphe.patches.music.interaction.jam.addReferenceGetter import app.morphe.patches.music.interaction.jam.decode import app.morphe.patches.music.interaction.jam.installNativeAccessor import app.morphe.patches.music.interaction.jam.invokeKind -import app.morphe.util.cloneParameters import app.morphe.util.findInstructionIndicesReversedOrThrow import com.android.tools.smali.dexlib2.Opcode diff --git a/patches/src/main/kotlin/app/morphe/patches/music/misc/backgroundplayback/BackgroundPlaybackPatch.kt b/patches/src/main/kotlin/app/morphe/patches/music/misc/backgroundplayback/BackgroundPlaybackPatch.kt index 190cdbbde6..6837779c02 100644 --- a/patches/src/main/kotlin/app/morphe/patches/music/misc/backgroundplayback/BackgroundPlaybackPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/music/misc/backgroundplayback/BackgroundPlaybackPatch.kt @@ -1,11 +1,11 @@ package app.morphe.patches.music.misc.backgroundplayback import app.morphe.patcher.patch.bytecodePatch +import app.morphe.patcher.util.returnEarly import app.morphe.patches.music.misc.extension.sharedExtensionPatch import app.morphe.patches.music.misc.settings.settingsPatch import app.morphe.patches.music.shared.Constants.COMPATIBILITY_YOUTUBE_MUSIC import app.morphe.patches.shared.misc.fix.bitmap.fixRecycledBitmapPatch -import app.morphe.util.returnEarly val backgroundPlaybackPatch = bytecodePatch( name = "Remove background playback restrictions", diff --git a/patches/src/main/kotlin/app/morphe/patches/music/misc/settings/SettingsPatch.kt b/patches/src/main/kotlin/app/morphe/patches/music/misc/settings/SettingsPatch.kt index 67e7cd8a07..9cc0f9d78d 100644 --- a/patches/src/main/kotlin/app/morphe/patches/music/misc/settings/SettingsPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/music/misc/settings/SettingsPatch.kt @@ -11,6 +11,8 @@ import app.morphe.patcher.extensions.InstructionExtensions.addInstruction import app.morphe.patcher.extensions.InstructionExtensions.addInstructionsWithLabels import app.morphe.patcher.patch.bytecodePatch import app.morphe.patcher.patch.resourcePatch +import app.morphe.patcher.util.getFreeRegisterProvider +import app.morphe.patcher.util.getReference import app.morphe.patches.all.misc.clone.setOrGetFallbackPackageName import app.morphe.patches.all.misc.crashlog.crashLogPatch import app.morphe.patches.all.misc.fix.openurllinks.removeLinkVerification @@ -47,8 +49,6 @@ import app.morphe.patches.youtube.misc.settings.modifyActivityForSettingsInjecti import app.morphe.util.ResourceGroup import app.morphe.util.copyResources import app.morphe.util.copyXmlNode -import app.morphe.util.getFreeRegisterProvider -import app.morphe.util.getReference import app.morphe.util.inputStreamFromBundledResource import app.morphe.util.insertLiteralOverride import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction diff --git a/patches/src/main/kotlin/app/morphe/patches/music/video/information/VideoInformationPatch.kt b/patches/src/main/kotlin/app/morphe/patches/music/video/information/VideoInformationPatch.kt index 720c148b82..73ab9dbfc8 100644 --- a/patches/src/main/kotlin/app/morphe/patches/music/video/information/VideoInformationPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/music/video/information/VideoInformationPatch.kt @@ -11,6 +11,7 @@ import app.morphe.patcher.extensions.InstructionExtensions.addInstruction import app.morphe.patcher.extensions.InstructionExtensions.addInstructions import app.morphe.patcher.extensions.InstructionExtensions.getInstruction import app.morphe.patcher.patch.bytecodePatch +import app.morphe.patcher.util.getReference import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable import app.morphe.patches.music.misc.extension.sharedExtensionPatch @@ -19,7 +20,6 @@ import app.morphe.patches.shared.misc.videoinformation.PlayerControllerSetTimeRe import app.morphe.patches.shared.misc.videoinformation.seekSourceUnknownField import app.morphe.util.addInstructionsToEnd import app.morphe.util.addStaticFieldToExtension -import app.morphe.util.getReference import app.morphe.util.indexOfFirstInstructionOrThrow import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode diff --git a/patches/src/main/kotlin/app/morphe/patches/reddit/layout/copytext/CopyTextPatch.kt b/patches/src/main/kotlin/app/morphe/patches/reddit/layout/copytext/CopyTextPatch.kt index 14292c1d51..702327c72d 100644 --- a/patches/src/main/kotlin/app/morphe/patches/reddit/layout/copytext/CopyTextPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/reddit/layout/copytext/CopyTextPatch.kt @@ -13,12 +13,12 @@ import app.morphe.patcher.extensions.InstructionExtensions.addInstructionsWithLa import app.morphe.patcher.extensions.InstructionExtensions.getInstruction import app.morphe.patcher.patch.PatchException import app.morphe.patcher.patch.bytecodePatch +import app.morphe.patcher.util.findFreeRegister import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable import app.morphe.patches.reddit.misc.settings.settingsPatch import app.morphe.patches.reddit.misc.version.is_2026_24_0_or_greater import app.morphe.patches.reddit.misc.version.versionCheckPatch import app.morphe.patches.reddit.shared.Constants.COMPATIBILITY_REDDIT -import app.morphe.util.findFreeRegister import app.morphe.util.setExtensionIsPatchIncluded import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation diff --git a/patches/src/main/kotlin/app/morphe/patches/reddit/layout/flair/ShowFlairsInHomeFeedPatch.kt b/patches/src/main/kotlin/app/morphe/patches/reddit/layout/flair/ShowFlairsInHomeFeedPatch.kt index 0e7478975a..403e959883 100644 --- a/patches/src/main/kotlin/app/morphe/patches/reddit/layout/flair/ShowFlairsInHomeFeedPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/reddit/layout/flair/ShowFlairsInHomeFeedPatch.kt @@ -10,6 +10,7 @@ package app.morphe.patches.reddit.layout.flair import app.morphe.patcher.extensions.InstructionExtensions.addInstruction import app.morphe.patcher.extensions.InstructionExtensions.addInstructions import app.morphe.patcher.patch.bytecodePatch +import app.morphe.patcher.util.p0Register import app.morphe.patcher.util.proxy.mutableTypes.MutableClass import app.morphe.patcher.util.proxy.mutableTypes.MutableField import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable @@ -19,7 +20,6 @@ import app.morphe.patches.reddit.misc.version.versionCheckPatch import app.morphe.patches.reddit.shared.Constants.COMPATIBILITY_REDDIT import app.morphe.util.addInstructionsAtControlFlowLabel import app.morphe.util.findInstructionIndicesReversedOrThrow -import app.morphe.util.p0Register import app.morphe.util.setExtensionIsPatchIncluded import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode diff --git a/patches/src/main/kotlin/app/morphe/patches/reddit/layout/fullwidth/FullWidthFeedMediaPatch.kt b/patches/src/main/kotlin/app/morphe/patches/reddit/layout/fullwidth/FullWidthFeedMediaPatch.kt index b142355774..4f85d4b99d 100644 --- a/patches/src/main/kotlin/app/morphe/patches/reddit/layout/fullwidth/FullWidthFeedMediaPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/reddit/layout/fullwidth/FullWidthFeedMediaPatch.kt @@ -15,6 +15,7 @@ import app.morphe.patcher.extensions.InstructionExtensions.replaceInstruction import app.morphe.patcher.fieldAccess import app.morphe.patcher.methodCall import app.morphe.patcher.patch.bytecodePatch +import app.morphe.patcher.util.registersUsed import app.morphe.patches.reddit.misc.settings.settingsPatch import app.morphe.patches.reddit.misc.version.is_2026_24_0_or_greater import app.morphe.patches.reddit.misc.version.is_2026_36_0_or_greater @@ -22,7 +23,6 @@ import app.morphe.patches.reddit.misc.version.is_2026_38_0_or_greater import app.morphe.patches.reddit.misc.version.versionCheckPatch import app.morphe.patches.reddit.shared.Constants.COMPATIBILITY_REDDIT import app.morphe.util.findInstructionIndicesReversedOrThrow -import app.morphe.util.registersUsed import app.morphe.util.setExtensionIsPatchIncluded import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction diff --git a/patches/src/main/kotlin/app/morphe/patches/reddit/layout/mediaviewer/HideMediaViewerComponentsPatch.kt b/patches/src/main/kotlin/app/morphe/patches/reddit/layout/mediaviewer/HideMediaViewerComponentsPatch.kt index 366abada0e..1b8c359906 100644 --- a/patches/src/main/kotlin/app/morphe/patches/reddit/layout/mediaviewer/HideMediaViewerComponentsPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/reddit/layout/mediaviewer/HideMediaViewerComponentsPatch.kt @@ -13,6 +13,8 @@ import app.morphe.patcher.extensions.InstructionExtensions.addInstructionsWithLa import app.morphe.patcher.extensions.InstructionExtensions.getInstruction import app.morphe.patcher.extensions.InstructionExtensions.replaceInstruction import app.morphe.patcher.patch.bytecodePatch +import app.morphe.patcher.util.findFreeRegister +import app.morphe.patcher.util.returnEarly import app.morphe.patcher.util.smali.ExternalLabel import app.morphe.patches.reddit.misc.settings.settingsPatch import app.morphe.patches.reddit.misc.version.is_2026_24_0_or_greater @@ -20,10 +22,8 @@ import app.morphe.patches.reddit.misc.version.is_2026_32_0_or_greater import app.morphe.patches.reddit.misc.version.is_2026_38_0_or_greater import app.morphe.patches.reddit.misc.version.versionCheckPatch import app.morphe.patches.reddit.shared.Constants.COMPATIBILITY_REDDIT -import app.morphe.util.findFreeRegister import app.morphe.util.findInstructionIndicesReversedOrThrow import app.morphe.util.removeFlags -import app.morphe.util.returnEarly import app.morphe.util.setExtensionIsPatchIncluded import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode diff --git a/patches/src/main/kotlin/app/morphe/patches/reddit/layout/mediaviewer/MediaViewerFadePatch.kt b/patches/src/main/kotlin/app/morphe/patches/reddit/layout/mediaviewer/MediaViewerFadePatch.kt index 8283a543a6..06a9cf4ed4 100644 --- a/patches/src/main/kotlin/app/morphe/patches/reddit/layout/mediaviewer/MediaViewerFadePatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/reddit/layout/mediaviewer/MediaViewerFadePatch.kt @@ -11,16 +11,16 @@ import app.morphe.patcher.extensions.InstructionExtensions.addInstructions import app.morphe.patcher.extensions.InstructionExtensions.addInstructionsWithLabels import app.morphe.patcher.extensions.InstructionExtensions.getInstruction import app.morphe.patcher.patch.bytecodePatch +import app.morphe.patcher.util.findFreeRegister +import app.morphe.patcher.util.p0Register +import app.morphe.patcher.util.registersUsed import app.morphe.patcher.util.smali.ExternalLabel import app.morphe.patches.reddit.misc.settings.settingsPatch import app.morphe.patches.reddit.misc.version.is_2026_24_0_or_greater import app.morphe.patches.reddit.misc.version.is_2026_38_0_or_greater import app.morphe.patches.reddit.misc.version.versionCheckPatch import app.morphe.patches.reddit.shared.Constants.COMPATIBILITY_REDDIT -import app.morphe.util.findFreeRegister import app.morphe.util.findInstructionIndicesReversedOrThrow -import app.morphe.util.p0Register -import app.morphe.util.registersUsed import app.morphe.util.setExtensionIsPatchIncluded import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction import java.util.logging.Logger diff --git a/patches/src/main/kotlin/app/morphe/patches/reddit/layout/navigation/HideNavigationButtonsPatch.kt b/patches/src/main/kotlin/app/morphe/patches/reddit/layout/navigation/HideNavigationButtonsPatch.kt index fff5f7cca8..fbf7669140 100644 --- a/patches/src/main/kotlin/app/morphe/patches/reddit/layout/navigation/HideNavigationButtonsPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/reddit/layout/navigation/HideNavigationButtonsPatch.kt @@ -12,15 +12,15 @@ import app.morphe.patcher.extensions.InstructionExtensions.addInstructionsWithLa import app.morphe.patcher.extensions.InstructionExtensions.getInstruction import app.morphe.patcher.extensions.InstructionExtensions.replaceInstruction import app.morphe.patcher.patch.bytecodePatch +import app.morphe.patcher.util.findFreeRegister +import app.morphe.patcher.util.getReference import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable import app.morphe.patcher.util.smali.ExternalLabel import app.morphe.patches.reddit.misc.settings.settingsPatch import app.morphe.patches.reddit.misc.version.is_2026_25_0_or_greater import app.morphe.patches.reddit.misc.version.versionCheckPatch import app.morphe.patches.reddit.shared.Constants.COMPATIBILITY_REDDIT -import app.morphe.util.findFreeRegister import app.morphe.util.findInstructionIndicesReversedOrThrow -import app.morphe.util.getReference import app.morphe.util.setExtensionIsPatchIncluded import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation diff --git a/patches/src/main/kotlin/app/morphe/patches/reddit/layout/post/CustomizePostLayoutPatch.kt b/patches/src/main/kotlin/app/morphe/patches/reddit/layout/post/CustomizePostLayoutPatch.kt index 87fa60808f..fd54772e39 100644 --- a/patches/src/main/kotlin/app/morphe/patches/reddit/layout/post/CustomizePostLayoutPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/reddit/layout/post/CustomizePostLayoutPatch.kt @@ -12,7 +12,11 @@ import app.morphe.patcher.extensions.InstructionExtensions.addInstructionsWithLa import app.morphe.patcher.extensions.InstructionExtensions.getInstruction import app.morphe.patcher.patch.BytecodePatchContext import app.morphe.patcher.patch.bytecodePatch +import app.morphe.patcher.util.findFreeRegister +import app.morphe.patcher.util.getFreeRegisterProvider +import app.morphe.patcher.util.getReference import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable +import app.morphe.patcher.util.registersUsed import app.morphe.patcher.util.smali.ExternalLabel import app.morphe.patches.reddit.misc.flag.featureFlagHookPatch import app.morphe.patches.reddit.misc.flag.hookFeatureFlag @@ -21,10 +25,6 @@ import app.morphe.patches.reddit.misc.version.is_2026_39_0_or_greater import app.morphe.patches.reddit.misc.version.versionCheckPatch import app.morphe.patches.reddit.shared.Constants.COMPATIBILITY_REDDIT import app.morphe.util.addInstructionsAtControlFlowLabel -import app.morphe.util.findFreeRegister -import app.morphe.util.getFreeRegisterProvider -import app.morphe.util.getReference -import app.morphe.util.registersUsed import app.morphe.util.setExtensionIsPatchIncluded import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation diff --git a/patches/src/main/kotlin/app/morphe/patches/reddit/layout/trending/HideTrendingShelvesPatch.kt b/patches/src/main/kotlin/app/morphe/patches/reddit/layout/trending/HideTrendingShelvesPatch.kt index 4e18f80c8e..06ddb9f8e9 100644 --- a/patches/src/main/kotlin/app/morphe/patches/reddit/layout/trending/HideTrendingShelvesPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/reddit/layout/trending/HideTrendingShelvesPatch.kt @@ -11,6 +11,7 @@ import app.morphe.patcher.Fingerprint import app.morphe.patcher.extensions.InstructionExtensions.addInstructions import app.morphe.patcher.extensions.InstructionExtensions.addInstructionsWithLabels import app.morphe.patcher.patch.bytecodePatch +import app.morphe.patcher.util.findFreeRegister import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable import app.morphe.patches.reddit.misc.settings.settingsPatch import app.morphe.patches.reddit.misc.version.is_2026_11_0_or_greater @@ -18,7 +19,6 @@ import app.morphe.patches.reddit.misc.version.is_2026_16_0_or_greater import app.morphe.patches.reddit.misc.version.is_2026_21_0_or_greater import app.morphe.patches.reddit.misc.version.versionCheckPatch import app.morphe.patches.reddit.shared.Constants.COMPATIBILITY_REDDIT -import app.morphe.util.findFreeRegister import app.morphe.util.setExtensionIsPatchIncluded import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation diff --git a/patches/src/main/kotlin/app/morphe/patches/reddit/misc/collapsedcomments/RememberCollapsedCommentsPatch.kt b/patches/src/main/kotlin/app/morphe/patches/reddit/misc/collapsedcomments/RememberCollapsedCommentsPatch.kt index daa39c77a1..0f996b1ce7 100644 --- a/patches/src/main/kotlin/app/morphe/patches/reddit/misc/collapsedcomments/RememberCollapsedCommentsPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/reddit/misc/collapsedcomments/RememberCollapsedCommentsPatch.kt @@ -12,13 +12,13 @@ import app.morphe.patcher.extensions.InstructionExtensions.addInstructions import app.morphe.patcher.extensions.InstructionExtensions.getInstruction import app.morphe.patcher.patch.PatchException import app.morphe.patcher.patch.bytecodePatch +import app.morphe.patcher.util.cloneParameters +import app.morphe.patcher.util.numberOfParameterRegistersLogical import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable +import app.morphe.patcher.util.registersUsed import app.morphe.patches.reddit.misc.settings.settingsPatch import app.morphe.patches.reddit.shared.Constants.COMPATIBILITY_REDDIT import app.morphe.util.cloneMutable -import app.morphe.util.cloneParameters -import app.morphe.util.numberOfParameterRegistersLogical -import app.morphe.util.registersUsed import app.morphe.util.removeFlags import app.morphe.util.setExtensionIsPatchIncluded import com.android.tools.smali.dexlib2.AccessFlags diff --git a/patches/src/main/kotlin/app/morphe/patches/reddit/misc/downloadlocation/DownloadLocationPatch.kt b/patches/src/main/kotlin/app/morphe/patches/reddit/misc/downloadlocation/DownloadLocationPatch.kt index b667a2e38d..8b83cb9d42 100644 --- a/patches/src/main/kotlin/app/morphe/patches/reddit/misc/downloadlocation/DownloadLocationPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/reddit/misc/downloadlocation/DownloadLocationPatch.kt @@ -14,12 +14,12 @@ import app.morphe.patcher.extensions.InstructionExtensions.getInstruction import app.morphe.patcher.extensions.InstructionExtensions.replaceInstruction import app.morphe.patcher.patch.bytecodePatch import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable +import app.morphe.patcher.util.registersUsed import app.morphe.patches.reddit.misc.settings.settingsPatch import app.morphe.patches.reddit.shared.Constants.COMPATIBILITY_REDDIT import app.morphe.patches.reddit.shared.LinkConstructorFingerprint import app.morphe.util.fiveRegisters import app.morphe.util.indexOfFirstInstructionReversedOrThrow -import app.morphe.util.registersUsed import app.morphe.util.setExtensionIsPatchIncluded import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode diff --git a/patches/src/main/kotlin/app/morphe/patches/reddit/misc/guest/StartAsGuestPatch.kt b/patches/src/main/kotlin/app/morphe/patches/reddit/misc/guest/StartAsGuestPatch.kt index d4f11b9b76..49153bf7c3 100644 --- a/patches/src/main/kotlin/app/morphe/patches/reddit/misc/guest/StartAsGuestPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/reddit/misc/guest/StartAsGuestPatch.kt @@ -8,8 +8,8 @@ package app.morphe.patches.reddit.misc.guest import app.morphe.patcher.patch.bytecodePatch +import app.morphe.patcher.util.returnEarly import app.morphe.patches.reddit.shared.Constants.COMPATIBILITY_REDDIT -import app.morphe.util.returnEarly @Suppress("unused") val startAsGuestPatch = bytecodePatch( diff --git a/patches/src/main/kotlin/app/morphe/patches/reddit/misc/openlink/ScreenNavigatorMethodResolverPatch.kt b/patches/src/main/kotlin/app/morphe/patches/reddit/misc/openlink/ScreenNavigatorMethodResolverPatch.kt index bdd9050030..fd649e20ac 100644 --- a/patches/src/main/kotlin/app/morphe/patches/reddit/misc/openlink/ScreenNavigatorMethodResolverPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/reddit/misc/openlink/ScreenNavigatorMethodResolverPatch.kt @@ -9,12 +9,12 @@ package app.morphe.patches.reddit.misc.openlink import app.morphe.patcher.extensions.InstructionExtensions.getInstruction import app.morphe.patcher.patch.bytecodePatch +import app.morphe.patcher.util.getReference import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod import app.morphe.patches.reddit.misc.settings.settingsPatch import app.morphe.patches.reddit.misc.version.is_2026_11_0_or_greater import app.morphe.patches.reddit.misc.version.versionCheckPatch import app.morphe.util.getMutableMethod -import app.morphe.util.getReference import app.morphe.util.indexOfFirstInstructionOrThrow import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction import com.android.tools.smali.dexlib2.iface.reference.MethodReference diff --git a/patches/src/main/kotlin/app/morphe/patches/reddit/misc/redgifs/RedgifsAudioPatch.kt b/patches/src/main/kotlin/app/morphe/patches/reddit/misc/redgifs/RedgifsAudioPatch.kt index 27744b2f3d..82bbf63655 100644 --- a/patches/src/main/kotlin/app/morphe/patches/reddit/misc/redgifs/RedgifsAudioPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/reddit/misc/redgifs/RedgifsAudioPatch.kt @@ -11,13 +11,13 @@ import app.morphe.patcher.extensions.InstructionExtensions.addInstruction import app.morphe.patcher.extensions.InstructionExtensions.addInstructions import app.morphe.patcher.extensions.InstructionExtensions.addInstructionsWithLabels import app.morphe.patcher.patch.bytecodePatch +import app.morphe.patcher.util.registersUsed import app.morphe.patches.reddit.misc.settings.settingsPatch import app.morphe.patches.reddit.misc.version.is_2026_33_0_or_greater import app.morphe.patches.reddit.misc.version.versionCheckPatch import app.morphe.patches.reddit.shared.Constants.COMPATIBILITY_REDDIT import app.morphe.patches.reddit.shared.LinkConstructorFingerprint import app.morphe.util.indexOfFirstInstructionReversedOrThrow -import app.morphe.util.registersUsed import app.morphe.util.setExtensionIsPatchIncluded import com.android.tools.smali.dexlib2.Opcode diff --git a/patches/src/main/kotlin/app/morphe/patches/reddit/misc/settings/SettingsPatch.kt b/patches/src/main/kotlin/app/morphe/patches/reddit/misc/settings/SettingsPatch.kt index 3fdd9a6efd..cce3de26b1 100644 --- a/patches/src/main/kotlin/app/morphe/patches/reddit/misc/settings/SettingsPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/reddit/misc/settings/SettingsPatch.kt @@ -15,6 +15,11 @@ import app.morphe.patcher.patch.PatchException import app.morphe.patcher.patch.bytecodePatch import app.morphe.patcher.patch.resourcePatch import app.morphe.patcher.string +import app.morphe.patcher.util.cloneParameters +import app.morphe.patcher.util.findFreeRegister +import app.morphe.patcher.util.p0Register +import app.morphe.patcher.util.registersUsed +import app.morphe.patcher.util.returnEarly import app.morphe.patches.all.misc.crashlog.crashLogPatch import app.morphe.patches.all.misc.fix.openurllinks.removeLinkVerification import app.morphe.patches.all.misc.resources.addAppResources @@ -36,12 +41,7 @@ import app.morphe.patches.shared.misc.checks.experimentalAppNoticePatch import app.morphe.patches.util.resource.editText import app.morphe.patches.util.resource.transformStringResources import app.morphe.util.ResourceGroup -import app.morphe.util.cloneParameters import app.morphe.util.copyResources -import app.morphe.util.findFreeRegister -import app.morphe.util.p0Register -import app.morphe.util.registersUsed -import app.morphe.util.returnEarly import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction diff --git a/patches/src/main/kotlin/app/morphe/patches/shared/ad/HideFullscreenAdsPatch.kt b/patches/src/main/kotlin/app/morphe/patches/shared/ad/HideFullscreenAdsPatch.kt index e822271030..fcdff82a0e 100644 --- a/patches/src/main/kotlin/app/morphe/patches/shared/ad/HideFullscreenAdsPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/shared/ad/HideFullscreenAdsPatch.kt @@ -11,12 +11,12 @@ import app.morphe.patcher.extensions.InstructionExtensions.addInstruction import app.morphe.patcher.extensions.InstructionExtensions.addInstructions import app.morphe.patcher.extensions.InstructionExtensions.getInstruction import app.morphe.patcher.patch.bytecodePatch +import app.morphe.patcher.util.cloneParameters +import app.morphe.patcher.util.findFreeRegister +import app.morphe.patcher.util.getReference import app.morphe.patches.shared.misc.settings.preference.BasePreferenceScreen import app.morphe.patches.shared.misc.settings.preference.SwitchPreference import app.morphe.util.addInstructionsAtControlFlowLabel -import app.morphe.util.cloneParameters -import app.morphe.util.findFreeRegister -import app.morphe.util.getReference import app.morphe.util.indexOfFirstInstructionReversedOrThrow import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction diff --git a/patches/src/main/kotlin/app/morphe/patches/shared/layout/branding/BaseCustomBrandingPatch.kt b/patches/src/main/kotlin/app/morphe/patches/shared/layout/branding/BaseCustomBrandingPatch.kt index 9ad468b31a..41f749849b 100644 --- a/patches/src/main/kotlin/app/morphe/patches/shared/layout/branding/BaseCustomBrandingPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/shared/layout/branding/BaseCustomBrandingPatch.kt @@ -24,6 +24,7 @@ import app.morphe.patcher.patch.bytecodePatch import app.morphe.patcher.patch.folderOption import app.morphe.patcher.patch.resourcePatch import app.morphe.patcher.patch.stringOption +import app.morphe.patcher.util.returnEarly import app.morphe.patches.all.misc.clone.setOrGetFallbackPackageName import app.morphe.patches.shared.misc.fix.bitmap.fixRecycledBitmapPatch import app.morphe.patches.shared.misc.settings.preference.BasePreference @@ -40,7 +41,6 @@ import app.morphe.util.copyResources import app.morphe.util.findElementByAttributeValueOrThrow import app.morphe.util.inputStreamFromBundledResource import app.morphe.util.removeFromParent -import app.morphe.util.returnEarly import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction import org.w3c.dom.Element diff --git a/patches/src/main/kotlin/app/morphe/patches/shared/layout/theme/BaseThemePatch.kt b/patches/src/main/kotlin/app/morphe/patches/shared/layout/theme/BaseThemePatch.kt index e72d8753d7..e99fd37765 100644 --- a/patches/src/main/kotlin/app/morphe/patches/shared/layout/theme/BaseThemePatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/shared/layout/theme/BaseThemePatch.kt @@ -21,14 +21,14 @@ import app.morphe.patcher.patch.ResourcePatchContext import app.morphe.patcher.patch.bytecodePatch import app.morphe.patcher.patch.colorOption import app.morphe.patcher.patch.resourcePatch +import app.morphe.patcher.util.matchAllMethodIndicesForEach +import app.morphe.patcher.util.returnEarly import app.morphe.patches.shared.misc.settings.overrideThemeColors import app.morphe.patches.util.resource.appendResourceElements import app.morphe.patches.util.resource.editText import app.morphe.patches.util.resource.forEachRawResourceElement import app.morphe.patches.util.resource.transformRawResourceElements import app.morphe.util.inputStreamFromBundledResource -import app.morphe.util.matchAllMethodIndicesForEach -import app.morphe.util.returnEarly import app.morphe.util.setExtensionIsPatchIncluded import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction import java.util.Locale diff --git a/patches/src/main/kotlin/app/morphe/patches/shared/misc/audio/tracks/ForceOriginalAudioPatch.kt b/patches/src/main/kotlin/app/morphe/patches/shared/misc/audio/tracks/ForceOriginalAudioPatch.kt index 2171660e8c..4b6363e03b 100644 --- a/patches/src/main/kotlin/app/morphe/patches/shared/misc/audio/tracks/ForceOriginalAudioPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/shared/misc/audio/tracks/ForceOriginalAudioPatch.kt @@ -18,6 +18,7 @@ import app.morphe.patcher.extensions.InstructionExtensions.getInstruction import app.morphe.patcher.patch.BytecodePatchBuilder import app.morphe.patcher.patch.BytecodePatchContext import app.morphe.patcher.patch.bytecodePatch +import app.morphe.patcher.util.getReference import app.morphe.patcher.util.proxy.mutableTypes.MutableField.Companion.toMutable import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable import app.morphe.patches.shared.FormatStreamModelToStringFingerprint @@ -26,7 +27,6 @@ import app.morphe.patches.shared.misc.settings.preference.SwitchPreference import app.morphe.util.addInstructionsAtControlFlowLabel import app.morphe.util.cloneMutable import app.morphe.util.findMethodFromToString -import app.morphe.util.getReference import app.morphe.util.indexOfFirstInstructionReversedOrThrow import app.morphe.util.insertLiteralOverride import com.android.tools.smali.dexlib2.AccessFlags diff --git a/patches/src/main/kotlin/app/morphe/patches/shared/misc/checks/ExperimentalAppNoticePatch.kt b/patches/src/main/kotlin/app/morphe/patches/shared/misc/checks/ExperimentalAppNoticePatch.kt index 887b4958d0..c61b07f408 100644 --- a/patches/src/main/kotlin/app/morphe/patches/shared/misc/checks/ExperimentalAppNoticePatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/shared/misc/checks/ExperimentalAppNoticePatch.kt @@ -3,8 +3,8 @@ package app.morphe.patches.shared.misc.checks import app.morphe.patcher.Fingerprint import app.morphe.patcher.extensions.InstructionExtensions.addInstruction import app.morphe.patcher.patch.bytecodePatch +import app.morphe.patcher.util.returnEarly import app.morphe.patches.shared.misc.settings.RecommendedAppVersionUtilsFingerprint -import app.morphe.util.returnEarly private const val EXTENSION_CLASS = "Lapp/morphe/extension/shared/patches/ExperimentalAppNoticePatch;" diff --git a/patches/src/main/kotlin/app/morphe/patches/shared/misc/debugging/EnableDebuggingPatch.kt b/patches/src/main/kotlin/app/morphe/patches/shared/misc/debugging/EnableDebuggingPatch.kt index 51c92d91d2..77844a9751 100644 --- a/patches/src/main/kotlin/app/morphe/patches/shared/misc/debugging/EnableDebuggingPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/shared/misc/debugging/EnableDebuggingPatch.kt @@ -16,6 +16,8 @@ import app.morphe.patcher.patch.BytecodePatchBuilder import app.morphe.patcher.patch.BytecodePatchContext import app.morphe.patcher.patch.bytecodePatch import app.morphe.patcher.patch.resourcePatch +import app.morphe.patcher.util.cloneParameters +import app.morphe.patcher.util.p0Register import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod import app.morphe.patches.shared.misc.settings.preference.BasePreference import app.morphe.patches.shared.misc.settings.preference.BasePreferenceScreen @@ -25,10 +27,8 @@ import app.morphe.patches.shared.misc.settings.preference.PreferenceScreenPrefer import app.morphe.patches.shared.misc.settings.preference.SwitchPreference import app.morphe.util.ResourceGroup import app.morphe.util.addInstructionsAtControlFlowLabel -import app.morphe.util.cloneParameters import app.morphe.util.copyResources import app.morphe.util.findInstructionIndicesReversedOrThrow -import app.morphe.util.p0Register import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction diff --git a/patches/src/main/kotlin/app/morphe/patches/shared/misc/fix/bitmap/FixRecycledBitmapPatch.kt b/patches/src/main/kotlin/app/morphe/patches/shared/misc/fix/bitmap/FixRecycledBitmapPatch.kt index 2c1c6a23c9..7cccb2cc42 100644 --- a/patches/src/main/kotlin/app/morphe/patches/shared/misc/fix/bitmap/FixRecycledBitmapPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/shared/misc/fix/bitmap/FixRecycledBitmapPatch.kt @@ -11,8 +11,8 @@ import app.morphe.patcher.Fingerprint import app.morphe.patcher.extensions.InstructionExtensions.replaceInstruction import app.morphe.patcher.methodCall import app.morphe.patcher.patch.bytecodePatch +import app.morphe.patcher.util.matchAllMethodIndicesForEach import app.morphe.util.fiveRegisters -import app.morphe.util.matchAllMethodIndicesForEach private const val EXTENSION_CLASS = "Lapp/morphe/extension/shared/patches/FixRecycledBitmapPatch;" diff --git a/patches/src/main/kotlin/app/morphe/patches/shared/misc/gms/GmsCoreSupportPatch.kt b/patches/src/main/kotlin/app/morphe/patches/shared/misc/gms/GmsCoreSupportPatch.kt index 355478080b..471de1b480 100644 --- a/patches/src/main/kotlin/app/morphe/patches/shared/misc/gms/GmsCoreSupportPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/shared/misc/gms/GmsCoreSupportPatch.kt @@ -25,6 +25,8 @@ import app.morphe.patcher.patch.ResourcePatchContext import app.morphe.patcher.patch.bytecodePatch import app.morphe.patcher.patch.resourcePatch import app.morphe.patcher.string +import app.morphe.patcher.util.matchAllMethodIndicesForEach +import app.morphe.patcher.util.returnEarly import app.morphe.patches.all.misc.clone.cloneAppPatch import app.morphe.patches.all.misc.clone.setOrGetFallbackPackageName import app.morphe.patches.all.misc.gms.gmsCorePackageQueriesPatch @@ -35,8 +37,6 @@ import app.morphe.patches.shared.misc.settings.preference.BasePreferenceScreen import app.morphe.patches.shared.misc.settings.preference.IntentPreference import app.morphe.patches.shared.misc.settings.preference.SwitchPreference import app.morphe.util.indexOfFirstStringInstructionOrThrow -import app.morphe.util.matchAllMethodIndicesForEach -import app.morphe.util.returnEarly import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction21c import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction diff --git a/patches/src/main/kotlin/app/morphe/patches/shared/misc/litho/filter/LithoFilterPatch.kt b/patches/src/main/kotlin/app/morphe/patches/shared/misc/litho/filter/LithoFilterPatch.kt index a61142f445..d839719e05 100644 --- a/patches/src/main/kotlin/app/morphe/patches/shared/misc/litho/filter/LithoFilterPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/shared/misc/litho/filter/LithoFilterPatch.kt @@ -24,14 +24,14 @@ import app.morphe.patcher.patch.BytecodePatch import app.morphe.patcher.patch.BytecodePatchBuilder import app.morphe.patcher.patch.bytecodePatch import app.morphe.patcher.string +import app.morphe.patcher.util.getFreeRegisterProvider +import app.morphe.patcher.util.getReference import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable +import app.morphe.patcher.util.returnEarly import app.morphe.patches.shared.misc.litho.context.EXTENSION_CONTEXT_INTERFACE import app.morphe.util.addInstructionsAtControlFlowLabel -import app.morphe.util.getFreeRegisterProvider -import app.morphe.util.getReference import app.morphe.util.insertLiteralOverride -import app.morphe.util.returnEarly import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation diff --git a/patches/src/main/kotlin/app/morphe/patches/shared/misc/litho/node/TreeNodeElementHookPatch.kt b/patches/src/main/kotlin/app/morphe/patches/shared/misc/litho/node/TreeNodeElementHookPatch.kt index e6f2ebc1c8..ab9532eae6 100644 --- a/patches/src/main/kotlin/app/morphe/patches/shared/misc/litho/node/TreeNodeElementHookPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/shared/misc/litho/node/TreeNodeElementHookPatch.kt @@ -13,15 +13,15 @@ import app.morphe.patcher.extensions.InstructionExtensions.getInstruction import app.morphe.patcher.patch.BytecodePatch import app.morphe.patcher.patch.BytecodePatchBuilder import app.morphe.patcher.patch.bytecodePatch +import app.morphe.patcher.util.getFreeRegisterProvider +import app.morphe.patcher.util.getReference +import app.morphe.patcher.util.p0Register import app.morphe.patcher.util.proxy.mutableTypes.MutableClass import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable import app.morphe.patches.shared.misc.litho.context.EXTENSION_CONTEXT_INTERFACE import app.morphe.util.addInstructionsAtControlFlowLabel -import app.morphe.util.getFreeRegisterProvider -import app.morphe.util.getReference import app.morphe.util.indexOfFirstInstructionOrThrow -import app.morphe.util.p0Register import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation diff --git a/patches/src/main/kotlin/app/morphe/patches/shared/misc/litho/relayout/LithoRelayoutPatch.kt b/patches/src/main/kotlin/app/morphe/patches/shared/misc/litho/relayout/LithoRelayoutPatch.kt index eb24860f98..b862266ee3 100644 --- a/patches/src/main/kotlin/app/morphe/patches/shared/misc/litho/relayout/LithoRelayoutPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/shared/misc/litho/relayout/LithoRelayoutPatch.kt @@ -12,8 +12,8 @@ import app.morphe.patcher.extensions.InstructionExtensions.addInstructions import app.morphe.patcher.extensions.InstructionExtensions.getInstruction import app.morphe.patcher.patch.PatchException import app.morphe.patcher.patch.bytecodePatch +import app.morphe.patcher.util.getReference import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable -import app.morphe.util.getReference import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction diff --git a/patches/src/main/kotlin/app/morphe/patches/shared/misc/potoken/PoTokenProviderPatch.kt b/patches/src/main/kotlin/app/morphe/patches/shared/misc/potoken/PoTokenProviderPatch.kt index 2ef8d227d0..de50609833 100644 --- a/patches/src/main/kotlin/app/morphe/patches/shared/misc/potoken/PoTokenProviderPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/shared/misc/potoken/PoTokenProviderPatch.kt @@ -15,13 +15,13 @@ import app.morphe.patcher.patch.InstallerType import app.morphe.patcher.patch.PatchAvailability import app.morphe.patcher.patch.bytecodePatch import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable +import app.morphe.patcher.util.registersUsed import app.morphe.patches.all.misc.clone.setOrGetFallbackPackageName import app.morphe.patches.shared.misc.settings.preference.BasePreferenceScreen import app.morphe.patches.shared.misc.settings.preference.NonInteractivePreference import app.morphe.patches.shared.misc.settings.preference.PreferenceScreenPreference import app.morphe.patches.shared.misc.settings.preference.SwitchPreference import app.morphe.util.addInstructionsAtControlFlowLabel -import app.morphe.util.registersUsed import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation import com.android.tools.smali.dexlib2.immutable.ImmutableMethod diff --git a/patches/src/main/kotlin/app/morphe/patches/shared/misc/proxy/BaseNetworkProxyPatch.kt b/patches/src/main/kotlin/app/morphe/patches/shared/misc/proxy/BaseNetworkProxyPatch.kt index 58ef33344d..c912b09772 100644 --- a/patches/src/main/kotlin/app/morphe/patches/shared/misc/proxy/BaseNetworkProxyPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/shared/misc/proxy/BaseNetworkProxyPatch.kt @@ -12,13 +12,13 @@ import app.morphe.patcher.extensions.InstructionExtensions.getInstruction import app.morphe.patcher.patch.BytecodePatchBuilder import app.morphe.patcher.patch.BytecodePatchContext import app.morphe.patcher.patch.bytecodePatch +import app.morphe.patcher.util.returnEarly import app.morphe.patches.shared.misc.settings.preference.BasePreferenceScreen import app.morphe.patches.shared.misc.settings.preference.InputType import app.morphe.patches.shared.misc.settings.preference.PreferenceScreenPreference import app.morphe.patches.shared.misc.settings.preference.SwitchPreference import app.morphe.patches.shared.misc.settings.preference.TextPreference import app.morphe.util.findInstructionIndicesReversedOrThrow -import app.morphe.util.returnEarly import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction import java.util.logging.Logger diff --git a/patches/src/main/kotlin/app/morphe/patches/shared/misc/request/BuildRequestPatch.kt b/patches/src/main/kotlin/app/morphe/patches/shared/misc/request/BuildRequestPatch.kt index af09d7c85d..ee9e003cb8 100644 --- a/patches/src/main/kotlin/app/morphe/patches/shared/misc/request/BuildRequestPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/shared/misc/request/BuildRequestPatch.kt @@ -10,7 +10,7 @@ package app.morphe.patches.shared.misc.request import app.morphe.patcher.extensions.InstructionExtensions.addInstructions import app.morphe.patcher.patch.bytecodePatch import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod -import app.morphe.util.registersUsed +import app.morphe.patcher.util.registersUsed import java.lang.ref.WeakReference private lateinit var buildRequestMethod: WeakReference diff --git a/patches/src/main/kotlin/app/morphe/patches/shared/misc/settings/SettingsPatch.kt b/patches/src/main/kotlin/app/morphe/patches/shared/misc/settings/SettingsPatch.kt index 4efcea95f5..7e87880e70 100644 --- a/patches/src/main/kotlin/app/morphe/patches/shared/misc/settings/SettingsPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/shared/misc/settings/SettingsPatch.kt @@ -4,6 +4,7 @@ import app.morphe.patcher.patch.PatchException import app.morphe.patcher.patch.ResourcePatchContext import app.morphe.patcher.patch.bytecodePatch import app.morphe.patcher.patch.resourcePatch +import app.morphe.patcher.util.returnEarly import app.morphe.patches.all.misc.resources.addAppResources import app.morphe.patches.all.misc.resources.addResourcesPatch import app.morphe.patches.shared.layout.branding.addLicensePatch @@ -16,7 +17,6 @@ import app.morphe.util.copyResources import app.morphe.util.forEachChildElement import app.morphe.util.getNode import app.morphe.util.insertFirst -import app.morphe.util.returnEarly import org.w3c.dom.Node const val MORPHE_SETTINGS_INTENT = "morphe_settings_intent" diff --git a/patches/src/main/kotlin/app/morphe/patches/shared/misc/settingsmenu/HideMatchingHelper.kt b/patches/src/main/kotlin/app/morphe/patches/shared/misc/settingsmenu/HideMatchingHelper.kt index 39090a9a89..112bd8ac85 100644 --- a/patches/src/main/kotlin/app/morphe/patches/shared/misc/settingsmenu/HideMatchingHelper.kt +++ b/patches/src/main/kotlin/app/morphe/patches/shared/misc/settingsmenu/HideMatchingHelper.kt @@ -10,8 +10,8 @@ package app.morphe.patches.shared.misc.settingsmenu import app.morphe.patcher.extensions.InstructionExtensions.addInstructionsWithLabels import app.morphe.patcher.patch.BytecodePatchContext import app.morphe.patcher.patch.PatchException +import app.morphe.patcher.util.getReference import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable -import app.morphe.util.getReference import app.morphe.util.toPublicAccessFlags import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation diff --git a/patches/src/main/kotlin/app/morphe/patches/shared/misc/spans/InclusiveSpanPatch.kt b/patches/src/main/kotlin/app/morphe/patches/shared/misc/spans/InclusiveSpanPatch.kt index 81b6f7d2ab..4f3c61b7c6 100644 --- a/patches/src/main/kotlin/app/morphe/patches/shared/misc/spans/InclusiveSpanPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/shared/misc/spans/InclusiveSpanPatch.kt @@ -14,13 +14,13 @@ import app.morphe.patcher.extensions.InstructionExtensions.addInstructions import app.morphe.patcher.extensions.InstructionExtensions.getInstruction import app.morphe.patcher.extensions.InstructionExtensions.replaceInstruction import app.morphe.patcher.patch.bytecodePatch +import app.morphe.patcher.util.getReference import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable import app.morphe.patches.shared.SpannableStringBuilderFingerprint import app.morphe.patches.shared.misc.textcomponent.hookSpannableString import app.morphe.patches.shared.misc.textcomponent.textComponentPatch import app.morphe.util.fiveRegisters -import app.morphe.util.getReference import app.morphe.util.indexOfFirstInstructionReversedOrThrow import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode diff --git a/patches/src/main/kotlin/app/morphe/patches/shared/misc/spoof/SpoofVideoStreamsPatch.kt b/patches/src/main/kotlin/app/morphe/patches/shared/misc/spoof/SpoofVideoStreamsPatch.kt index 4d3da73808..49adb30347 100644 --- a/patches/src/main/kotlin/app/morphe/patches/shared/misc/spoof/SpoofVideoStreamsPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/shared/misc/spoof/SpoofVideoStreamsPatch.kt @@ -21,7 +21,9 @@ import app.morphe.patcher.patch.BytecodePatchBuilder import app.morphe.patcher.patch.BytecodePatchContext import app.morphe.patcher.patch.bytecodePatch import app.morphe.patcher.patch.resourcePatch +import app.morphe.patcher.util.getReference import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable +import app.morphe.patcher.util.registersUsed import app.morphe.patches.shared.BuildInnerTubeProtoRequestUriFingerprint import app.morphe.patches.shared.BuildInnerTubeProtoRequestUriLegacyFingerprint import app.morphe.patches.shared.misc.fix.proto.fixProtoLibraryPatch @@ -32,10 +34,8 @@ import app.morphe.util.ResourceGroup import app.morphe.util.addInstructionsAtControlFlowLabel import app.morphe.util.copyResources import app.morphe.util.findInstructionIndicesReversedOrThrow -import app.morphe.util.getReference import app.morphe.util.indexOfFirstInstructionOrThrow import app.morphe.util.insertLiteralOverride -import app.morphe.util.registersUsed import app.morphe.util.setExtensionIsPatchIncluded import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode diff --git a/patches/src/main/kotlin/app/morphe/patches/shared/misc/spoof/UserAgentClientSpoofPatch.kt b/patches/src/main/kotlin/app/morphe/patches/shared/misc/spoof/UserAgentClientSpoofPatch.kt index c77e07a502..eab865ba0b 100644 --- a/patches/src/main/kotlin/app/morphe/patches/shared/misc/spoof/UserAgentClientSpoofPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/shared/misc/spoof/UserAgentClientSpoofPatch.kt @@ -5,8 +5,8 @@ import app.morphe.patcher.extensions.InstructionExtensions.getInstructionOrNull import app.morphe.patcher.extensions.InstructionExtensions.replaceInstruction import app.morphe.patcher.methodCall import app.morphe.patcher.patch.bytecodePatch +import app.morphe.patcher.util.getReference import app.morphe.util.findInstructionIndicesReversedOrThrow -import app.morphe.util.getReference import app.morphe.util.indexOfFirstInstruction import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction21c diff --git a/patches/src/main/kotlin/app/morphe/patches/shared/misc/spoof/appversion/BaseSpoofAppVersionPatch.kt b/patches/src/main/kotlin/app/morphe/patches/shared/misc/spoof/appversion/BaseSpoofAppVersionPatch.kt index 25e8ca61b7..5085c2181e 100644 --- a/patches/src/main/kotlin/app/morphe/patches/shared/misc/spoof/appversion/BaseSpoofAppVersionPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/shared/misc/spoof/appversion/BaseSpoofAppVersionPatch.kt @@ -15,11 +15,11 @@ import app.morphe.patcher.extensions.InstructionExtensions.getInstruction import app.morphe.patcher.patch.BytecodePatchBuilder import app.morphe.patcher.patch.BytecodePatchContext import app.morphe.patcher.patch.bytecodePatch +import app.morphe.patcher.util.returnEarly import app.morphe.patches.shared.misc.settings.preference.BasePreferenceScreen import app.morphe.patches.shared.misc.settings.preference.ListPreference import app.morphe.patches.shared.misc.settings.preference.SwitchPreference import app.morphe.patches.shared.misc.settings.preference.noTitleUnsortedPreferenceCategory -import app.morphe.util.returnEarly import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction @Suppress("unused") diff --git a/patches/src/main/kotlin/app/morphe/patches/shared/misc/textcomponent/TextComponentPatch.kt b/patches/src/main/kotlin/app/morphe/patches/shared/misc/textcomponent/TextComponentPatch.kt index fa298cb61e..b427170987 100644 --- a/patches/src/main/kotlin/app/morphe/patches/shared/misc/textcomponent/TextComponentPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/shared/misc/textcomponent/TextComponentPatch.kt @@ -14,6 +14,9 @@ import app.morphe.patcher.fieldAccess import app.morphe.patcher.newInstance import app.morphe.patcher.patch.PatchException import app.morphe.patcher.patch.bytecodePatch +import app.morphe.patcher.util.cloneParameters +import app.morphe.patcher.util.findFreeRegister +import app.morphe.patcher.util.numberOfParameterRegistersLogical import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod import app.morphe.patches.shared.LithoSpannableStringCreationFingerprint import app.morphe.patches.shared.SpannableStringBuilderFingerprint @@ -25,11 +28,8 @@ import app.morphe.patches.shared.misc.litho.context.conversionContextClassDef import app.morphe.patches.shared.misc.litho.context.conversionContextPatch import app.morphe.patches.youtube.layout.returnyoutubedislike.TextComponentDataFingerprint import app.morphe.util.addInstructionsAtControlFlowLabel -import app.morphe.util.cloneParameters -import app.morphe.util.findFreeRegister import app.morphe.util.indexOfFirstInstructionOrThrow import app.morphe.util.insertLiteralOverride -import app.morphe.util.numberOfParameterRegistersLogical import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.iface.Field import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction diff --git a/patches/src/main/kotlin/app/morphe/patches/shared/misc/videoinformation/Fingerprints.kt b/patches/src/main/kotlin/app/morphe/patches/shared/misc/videoinformation/Fingerprints.kt index b93876ea76..c3e1186b51 100644 --- a/patches/src/main/kotlin/app/morphe/patches/shared/misc/videoinformation/Fingerprints.kt +++ b/patches/src/main/kotlin/app/morphe/patches/shared/misc/videoinformation/Fingerprints.kt @@ -7,7 +7,7 @@ import app.morphe.patcher.fieldAccess import app.morphe.patcher.methodCall import app.morphe.patcher.patch.BytecodePatchContext import app.morphe.patcher.string -import app.morphe.util.getReference +import app.morphe.patcher.util.getReference import app.morphe.util.indexOfFirstInstructionOrThrow import app.morphe.util.indexOfFirstStringInstructionOrThrow import com.android.tools.smali.dexlib2.AccessFlags diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/ad/HideAdsPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/ad/HideAdsPatch.kt index d23117534a..6980285103 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/ad/HideAdsPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/ad/HideAdsPatch.kt @@ -19,7 +19,9 @@ import app.morphe.patcher.patch.bytecodePatch import app.morphe.patcher.patch.resourcePatch import app.morphe.patcher.resource.ResourceType import app.morphe.patcher.resourceLiteral +import app.morphe.patcher.util.matchAllMethodIndicesForEach import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod +import app.morphe.patcher.util.registersUsed import app.morphe.patches.shared.ad.hideFullscreenAdsPatch import app.morphe.patches.shared.misc.litho.filter.addLithoFilter import app.morphe.patches.shared.misc.proto.hookElement @@ -38,8 +40,6 @@ import app.morphe.patches.youtube.misc.settings.PreferenceScreen import app.morphe.patches.youtube.misc.settings.settingsPatch import app.morphe.patches.youtube.shared.BuildClientContextBodyConstructorFingerprint import app.morphe.patches.youtube.shared.Constants.COMPATIBILITY_YOUTUBE -import app.morphe.util.matchAllMethodIndicesForEach -import app.morphe.util.registersUsed import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/ask/AskFlyoutMenuPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/ask/AskFlyoutMenuPatch.kt index d6be848cc2..74cb55bc49 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/ask/AskFlyoutMenuPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/ask/AskFlyoutMenuPatch.kt @@ -19,9 +19,11 @@ import app.morphe.patcher.fieldAccess import app.morphe.patcher.methodCall import app.morphe.patcher.patch.BytecodePatchContext import app.morphe.patcher.patch.bytecodePatch +import app.morphe.patcher.util.getReference import app.morphe.patcher.util.proxy.mutableTypes.MutableClass import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable +import app.morphe.patcher.util.registersUsed import app.morphe.patches.shared.misc.litho.filter.addLithoFilter import app.morphe.patches.shared.misc.settings.preference.SwitchPreference import app.morphe.patches.youtube.layout.flyout.FeedFlyoutBufferObjectFingerprint @@ -37,10 +39,8 @@ import app.morphe.patches.youtube.misc.settings.settingsPatch import app.morphe.patches.youtube.shared.Constants.COMPATIBILITY_YOUTUBE import app.morphe.util.findInstructionIndicesReversedOrThrow import app.morphe.util.findMutableMethodOf -import app.morphe.util.getReference import app.morphe.util.indexOfFirstInstructionOrThrow import app.morphe.util.indexOfFirstInstructionReversedOrThrow -import app.morphe.util.registersUsed import app.morphe.util.setExtensionIsPatchIncluded import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/channelsearch/ChannelSearchPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/channelsearch/ChannelSearchPatch.kt index dbb9dbd9b9..18c897ccab 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/channelsearch/ChannelSearchPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/channelsearch/ChannelSearchPatch.kt @@ -15,6 +15,8 @@ import app.morphe.patcher.extensions.InstructionExtensions.getInstruction import app.morphe.patcher.fieldAccess import app.morphe.patcher.patch.bytecodePatch import app.morphe.patcher.patch.resourcePatch +import app.morphe.patcher.util.findFreeRegister +import app.morphe.patcher.util.getReference import app.morphe.patches.all.misc.resources.addResourcesPatch import app.morphe.patches.shared.misc.settings.preference.SwitchPreference import app.morphe.patches.youtube.misc.settings.PreferenceScreen @@ -24,8 +26,6 @@ import app.morphe.patches.youtube.misc.toolbar.toolBarHookPatch import app.morphe.patches.youtube.shared.Constants.COMPATIBILITY_YOUTUBE import app.morphe.util.ResourceGroup import app.morphe.util.copyResources -import app.morphe.util.findFreeRegister -import app.morphe.util.getReference import app.morphe.util.indexOfFirstInstructionOrThrow import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/downloads/DownloadsPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/downloads/DownloadsPatch.kt index d4b5dcc6ac..9f9f7b8d09 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/downloads/DownloadsPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/downloads/DownloadsPatch.kt @@ -14,7 +14,9 @@ import app.morphe.patcher.extensions.InstructionExtensions.addInstructions import app.morphe.patcher.extensions.InstructionExtensions.addInstructionsWithLabels import app.morphe.patcher.patch.bytecodePatch import app.morphe.patcher.patch.resourcePatch +import app.morphe.patcher.util.getReference import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable +import app.morphe.patcher.util.registersUsed import app.morphe.patches.shared.misc.fix.proto.fixProtoLibraryPatch import app.morphe.patches.shared.misc.fix.proto.parseByteArrayWithRegistryMethodRef import app.morphe.patches.shared.misc.settings.preference.ListPreference @@ -31,8 +33,6 @@ import app.morphe.patches.youtube.misc.settings.settingsPatch import app.morphe.patches.youtube.misc.spoof.spoofVideoStreamsPatch import app.morphe.patches.youtube.shared.Constants.COMPATIBILITY_YOUTUBE import app.morphe.patches.youtube.video.information.videoInformationPatch -import app.morphe.util.getReference -import app.morphe.util.registersUsed import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation import com.android.tools.smali.dexlib2.iface.reference.FieldReference diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/hapticfeedback/DisableHapticFeedbackPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/hapticfeedback/DisableHapticFeedbackPatch.kt index 6fb2466c4d..51f2d617a6 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/hapticfeedback/DisableHapticFeedbackPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/hapticfeedback/DisableHapticFeedbackPatch.kt @@ -10,14 +10,14 @@ import app.morphe.patcher.fieldAccess import app.morphe.patcher.methodCall import app.morphe.patcher.patch.bytecodePatch import app.morphe.patcher.string +import app.morphe.patcher.util.getReference +import app.morphe.patcher.util.matchAllMethodIndicesForEach import app.morphe.patches.shared.misc.settings.preference.PreferenceScreenPreference import app.morphe.patches.shared.misc.settings.preference.SwitchPreference import app.morphe.patches.youtube.misc.settings.PreferenceScreen import app.morphe.patches.youtube.misc.settings.settingsPatch import app.morphe.patches.youtube.shared.Constants.COMPATIBILITY_YOUTUBE import app.morphe.util.fiveRegisters -import app.morphe.util.getReference -import app.morphe.util.matchAllMethodIndicesForEach import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/seekbar/EnableSlideToSeekPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/seekbar/EnableSlideToSeekPatch.kt index 76e8c44804..c8de8a5f35 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/seekbar/EnableSlideToSeekPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/seekbar/EnableSlideToSeekPatch.kt @@ -5,13 +5,13 @@ import app.morphe.patcher.extensions.InstructionExtensions.getInstruction import app.morphe.patcher.methodCall import app.morphe.patcher.patch.PatchException import app.morphe.patcher.patch.bytecodePatch +import app.morphe.patcher.util.getReference import app.morphe.patches.shared.misc.settings.preference.SwitchPreference import app.morphe.patches.youtube.misc.extension.sharedExtensionPatch import app.morphe.patches.youtube.misc.playservice.versionCheckPatch import app.morphe.patches.youtube.misc.settings.PreferenceScreen import app.morphe.patches.youtube.misc.settings.settingsPatch import app.morphe.util.findInstructionIndicesReversed -import app.morphe.util.getReference import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction import com.android.tools.smali.dexlib2.iface.reference.MethodReference diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/seekbar/EnableTapToSeekPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/seekbar/EnableTapToSeekPatch.kt index 5a2292cf2b..8f9ff65ddb 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/seekbar/EnableTapToSeekPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/seekbar/EnableTapToSeekPatch.kt @@ -7,7 +7,7 @@ import app.morphe.patches.shared.misc.settings.preference.SwitchPreference import app.morphe.patches.youtube.misc.extension.sharedExtensionPatch import app.morphe.patches.youtube.misc.settings.PreferenceScreen import app.morphe.patches.youtube.misc.settings.settingsPatch -import app.morphe.util.findFreeRegister +import app.morphe.patcher.util.findFreeRegister import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction import com.android.tools.smali.dexlib2.iface.reference.MethodReference diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/seekbar/SeekbarThumbnailPreviewPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/seekbar/SeekbarThumbnailPreviewPatch.kt index 9de4b24154..09764e5661 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/seekbar/SeekbarThumbnailPreviewPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/seekbar/SeekbarThumbnailPreviewPatch.kt @@ -21,7 +21,7 @@ import app.morphe.patches.youtube.misc.settings.PreferenceScreen import app.morphe.patches.youtube.misc.settings.settingsPatch import app.morphe.patches.youtube.shared.Constants.COMPATIBILITY_YOUTUBE import app.morphe.patches.youtube.shared.SeekbarOnDrawFingerprint -import app.morphe.util.getReference +import app.morphe.patcher.util.getReference import app.morphe.util.insertLiteralOverride import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction import com.android.tools.smali.dexlib2.iface.reference.FieldReference diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/shorts/SkipLiveShortsPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/shorts/SkipLiveShortsPatch.kt index 6b5f9edfe4..3b2990d780 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/shorts/SkipLiveShortsPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/shorts/SkipLiveShortsPatch.kt @@ -9,6 +9,8 @@ package app.morphe.patches.youtube.interaction.shorts import app.morphe.patcher.extensions.InstructionExtensions.addInstructions import app.morphe.patcher.patch.bytecodePatch +import app.morphe.patcher.util.findFreeRegister +import app.morphe.patcher.util.getReference import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable import app.morphe.patches.shared.misc.settings.preference.SwitchPreference import app.morphe.patches.youtube.layout.shortsautoplay.ReelPlaybackFingerprint @@ -23,8 +25,6 @@ import app.morphe.patches.youtube.misc.settings.PreferenceScreen import app.morphe.patches.youtube.misc.settings.settingsPatch import app.morphe.patches.youtube.shared.Constants.COMPATIBILITY_YOUTUBE import app.morphe.patches.youtube.video.information.videoInformationPatch -import app.morphe.util.findFreeRegister -import app.morphe.util.getReference import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation import com.android.tools.smali.dexlib2.iface.reference.FieldReference diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/branding/header/ChangeHeaderPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/branding/header/ChangeHeaderPatch.kt index a1a2f021ba..59bfdbee2a 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/branding/header/ChangeHeaderPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/branding/header/ChangeHeaderPatch.kt @@ -17,12 +17,12 @@ import app.morphe.patcher.resource.ResourceType import app.morphe.patcher.resource.resourceId import app.morphe.patcher.resourceLiteral import app.morphe.patcher.util.Document +import app.morphe.patcher.util.matchAllMethodIndicesForEach import app.morphe.patches.shared.layout.branding.header.CUSTOM_HEADER_RESOURCE_NAME import app.morphe.patches.shared.layout.branding.header.baseChangeHeaderPatch import app.morphe.patches.youtube.misc.settings.PreferenceScreen import app.morphe.patches.youtube.shared.Constants.COMPATIBILITY_YOUTUBE import app.morphe.util.findElementByAttributeValueOrThrow -import app.morphe.util.matchAllMethodIndicesForEach import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/buttons/navigation/NavigationBarPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/buttons/navigation/NavigationBarPatch.kt index f294724a74..c119494944 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/buttons/navigation/NavigationBarPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/buttons/navigation/NavigationBarPatch.kt @@ -21,6 +21,8 @@ import app.morphe.patcher.patch.PatchException import app.morphe.patcher.patch.booleanOption import app.morphe.patcher.patch.bytecodePatch import app.morphe.patcher.patch.resourcePatch +import app.morphe.patcher.util.getFreeRegisterProvider +import app.morphe.patcher.util.getReference import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable import app.morphe.patches.shared.misc.fix.proto.fixProtoLibraryPatch import app.morphe.patches.shared.misc.fix.proto.immutableMethodRef @@ -53,8 +55,6 @@ import app.morphe.util.addInstructionsAtControlFlowLabel import app.morphe.util.containsLiteralInstruction import app.morphe.util.findElementByAttributeValueOrThrow import app.morphe.util.fiveRegisters -import app.morphe.util.getFreeRegisterProvider -import app.morphe.util.getReference import app.morphe.util.indexOfFirstInstructionOrThrow import app.morphe.util.indexOfFirstInstructionReversedOrThrow import app.morphe.util.insertLiteralOverride diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/buttons/overlay/HidePlayerOverlayButtonsPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/buttons/overlay/HidePlayerOverlayButtonsPatch.kt index 66e119fc20..ee81ff6b90 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/buttons/overlay/HidePlayerOverlayButtonsPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/buttons/overlay/HidePlayerOverlayButtonsPatch.kt @@ -17,6 +17,8 @@ import app.morphe.patcher.extensions.InstructionExtensions.getInstruction import app.morphe.patcher.patch.bytecodePatch import app.morphe.patcher.resource.ResourceType import app.morphe.patcher.resource.resourceId +import app.morphe.patcher.util.findFreeRegister +import app.morphe.patcher.util.getReference import app.morphe.patcher.util.smali.ExternalLabel import app.morphe.patches.shared.misc.settings.preference.NonInteractivePreference import app.morphe.patches.shared.misc.settings.preference.SwitchPreference @@ -30,8 +32,6 @@ import app.morphe.patches.youtube.misc.settings.settingsPatch import app.morphe.patches.youtube.shared.Constants.COMPATIBILITY_YOUTUBE import app.morphe.patches.youtube.shared.LayoutConstructorFingerprint import app.morphe.util.addInstructionsAtControlFlowLabel -import app.morphe.util.findFreeRegister -import app.morphe.util.getReference import app.morphe.util.indexOfFirstInstructionOrThrow import app.morphe.util.indexOfFirstResourceIdOrThrow import app.morphe.util.insertLiteralOverride diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/captions/AutoTranslateCaptionsPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/captions/AutoTranslateCaptionsPatch.kt index 0786e35da9..012cc65343 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/captions/AutoTranslateCaptionsPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/captions/AutoTranslateCaptionsPatch.kt @@ -13,14 +13,14 @@ import app.morphe.patcher.extensions.InstructionExtensions.addInstructions import app.morphe.patcher.extensions.InstructionExtensions.getInstruction import app.morphe.patcher.extensions.InstructionExtensions.replaceInstruction import app.morphe.patcher.patch.bytecodePatch +import app.morphe.patcher.util.getReference +import app.morphe.patcher.util.matchAllMethodIndicesForEach +import app.morphe.patcher.util.registersUsed import app.morphe.patches.shared.misc.settings.preference.SwitchPreference import app.morphe.patches.youtube.misc.extension.sharedExtensionPatch import app.morphe.patches.youtube.misc.settings.settingsPatch import app.morphe.util.addInstructionsAtControlFlowLabel import app.morphe.util.findInstructionIndicesReversedOrThrow -import app.morphe.util.getReference -import app.morphe.util.matchAllMethodIndicesForEach -import app.morphe.util.registersUsed import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction import com.android.tools.smali.dexlib2.iface.reference.FieldReference diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/captions/CaptionsInPipPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/captions/CaptionsInPipPatch.kt index 3a2b613d18..7981b63e28 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/captions/CaptionsInPipPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/captions/CaptionsInPipPatch.kt @@ -12,6 +12,9 @@ import app.morphe.patcher.extensions.InstructionExtensions.getInstruction import app.morphe.patcher.extensions.InstructionExtensions.replaceInstruction import app.morphe.patcher.patch.PatchException import app.morphe.patcher.patch.bytecodePatch +import app.morphe.patcher.util.findFreeRegister +import app.morphe.patcher.util.getReference +import app.morphe.patcher.util.registersUsed import app.morphe.patches.shared.misc.settings.preference.SwitchPreference import app.morphe.patches.youtube.misc.extension.sharedExtensionPatch import app.morphe.patches.youtube.misc.playservice.is_20_29_or_greater @@ -21,9 +24,6 @@ import app.morphe.patches.youtube.misc.playservice.versionCheckPatch import app.morphe.patches.youtube.misc.settings.settingsPatch import app.morphe.patches.youtube.shared.PlayerTypeEnumFingerprint import app.morphe.util.addInstructionsAtControlFlowLabel -import app.morphe.util.findFreeRegister -import app.morphe.util.getReference -import app.morphe.util.registersUsed import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction import com.android.tools.smali.dexlib2.iface.reference.MethodReference diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/captions/PreferredCaptionLanguagePatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/captions/PreferredCaptionLanguagePatch.kt index 96152ea268..7cd7e937de 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/captions/PreferredCaptionLanguagePatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/captions/PreferredCaptionLanguagePatch.kt @@ -10,6 +10,7 @@ package app.morphe.patches.youtube.layout.captions import app.morphe.patcher.Fingerprint import app.morphe.patcher.extensions.InstructionExtensions.addInstructions import app.morphe.patcher.patch.bytecodePatch +import app.morphe.patcher.util.getReference import app.morphe.patches.shared.misc.settings.preference.ListPreference import app.morphe.patches.youtube.misc.extension.sharedExtensionPatch import app.morphe.patches.youtube.misc.playservice.is_21_02_or_greater @@ -19,7 +20,6 @@ import app.morphe.patches.youtube.shared.hookVideoStarted import app.morphe.patches.youtube.shared.startVideoInformerPatch import app.morphe.patches.youtube.video.information.onCreateHook import app.morphe.patches.youtube.video.information.videoInformationPatch -import app.morphe.util.getReference import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction import com.android.tools.smali.dexlib2.iface.reference.FieldReference diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/captions/TranslatedCaptionPensPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/captions/TranslatedCaptionPensPatch.kt index 75d608ee51..dadd62e3ad 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/captions/TranslatedCaptionPensPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/captions/TranslatedCaptionPensPatch.kt @@ -12,8 +12,8 @@ package app.morphe.patches.youtube.layout.captions import app.morphe.patcher.extensions.InstructionExtensions.addInstruction import app.morphe.patcher.extensions.InstructionExtensions.addInstructions import app.morphe.patcher.patch.bytecodePatch +import app.morphe.patcher.util.getReference import app.morphe.patches.youtube.misc.extension.sharedExtensionPatch -import app.morphe.util.getReference import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction import com.android.tools.smali.dexlib2.iface.reference.FieldReference diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/feedcards/FeedCardLayoutPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/feedcards/FeedCardLayoutPatch.kt index 212a3be4f0..7b0ab3ea11 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/feedcards/FeedCardLayoutPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/feedcards/FeedCardLayoutPatch.kt @@ -13,6 +13,7 @@ import app.morphe.patcher.extensions.InstructionExtensions.addInstructions import app.morphe.patcher.extensions.InstructionExtensions.addInstructionsWithLabels import app.morphe.patcher.extensions.InstructionExtensions.getInstruction import app.morphe.patcher.patch.bytecodePatch +import app.morphe.patcher.util.getReference import app.morphe.patcher.util.proxy.mutableTypes.MutableClass import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable import app.morphe.patches.shared.layout.theme.lithoColorHookPatch @@ -31,7 +32,6 @@ import app.morphe.patches.youtube.misc.settings.settingsPatch import app.morphe.patches.youtube.shared.Constants.COMPATIBILITY_YOUTUBE import app.morphe.patches.youtube.shared.hookVideoIntentCommand import app.morphe.patches.youtube.shared.openVideoIntentPatch -import app.morphe.util.getReference import app.morphe.util.indexOfFirstInstructionReversedOrThrow import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/flyout/FlyoutPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/flyout/FlyoutPatch.kt index cb6e82d1df..958d5d3e5a 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/flyout/FlyoutPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/flyout/FlyoutPatch.kt @@ -15,6 +15,10 @@ import app.morphe.patcher.extensions.InstructionExtensions.getInstruction import app.morphe.patcher.fieldAccess import app.morphe.patcher.patch.bytecodePatch import app.morphe.patcher.string +import app.morphe.patcher.util.cloneParameters +import app.morphe.patcher.util.findFreeRegister +import app.morphe.patcher.util.getReference +import app.morphe.patcher.util.numberOfParameterRegisters import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable import app.morphe.patches.shared.misc.proto.hookElement import app.morphe.patches.youtube.layout.hide.general.ContextualMenuItemBuilderFingerprint @@ -23,11 +27,7 @@ import app.morphe.patches.youtube.misc.proto.elementProtoParserHookPatch import app.morphe.patches.youtube.shared.hookVideoStarted import app.morphe.patches.youtube.shared.startVideoInformerPatch import app.morphe.patches.youtube.video.information.videoInformationPatch -import app.morphe.util.cloneParameters -import app.morphe.util.findFreeRegister import app.morphe.util.findInstructionIndicesReversedOrThrow -import app.morphe.util.getReference -import app.morphe.util.numberOfParameterRegisters import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/formfactor/ChangeFormFactorPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/formfactor/ChangeFormFactorPatch.kt index 539ed80d84..b1427b913a 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/formfactor/ChangeFormFactorPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/formfactor/ChangeFormFactorPatch.kt @@ -14,6 +14,8 @@ import app.morphe.patcher.extensions.InstructionExtensions.addInstructions import app.morphe.patcher.extensions.InstructionExtensions.addInstructionsWithLabels import app.morphe.patcher.extensions.InstructionExtensions.getInstruction import app.morphe.patcher.patch.bytecodePatch +import app.morphe.patcher.util.findFreeRegister +import app.morphe.patcher.util.registersUsed import app.morphe.patches.shared.misc.settings.preference.BasePreference import app.morphe.patches.shared.misc.settings.preference.ListPreference import app.morphe.patches.shared.misc.settings.preference.SwitchPreference @@ -29,8 +31,6 @@ import app.morphe.patches.youtube.misc.playservice.versionCheckPatch import app.morphe.patches.youtube.misc.settings.PreferenceScreen import app.morphe.patches.youtube.misc.settings.settingsPatch import app.morphe.patches.youtube.shared.Constants.COMPATIBILITY_YOUTUBE -import app.morphe.util.findFreeRegister -import app.morphe.util.registersUsed import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction private const val EXTENSION_CLASS = diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/ambientmode/AmbientModePatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/ambientmode/AmbientModePatch.kt index a6bf2be8de..a4d4e9e64f 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/ambientmode/AmbientModePatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/ambientmode/AmbientModePatch.kt @@ -16,6 +16,8 @@ import app.morphe.patcher.extensions.InstructionExtensions.getInstruction import app.morphe.patcher.extensions.InstructionExtensions.replaceInstruction import app.morphe.patcher.methodCall import app.morphe.patcher.patch.bytecodePatch +import app.morphe.patcher.util.matchAllMethodIndicesForEach +import app.morphe.patcher.util.matchSingle import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable import app.morphe.patches.shared.misc.fix.proto.fixProtoLibraryPatch @@ -32,8 +34,6 @@ import app.morphe.patches.youtube.shared.Constants.COMPATIBILITY_YOUTUBE import app.morphe.util.findFieldFromToString import app.morphe.util.findInstructionIndicesReversedOrThrow import app.morphe.util.indexOfFirstInstructionOrThrow -import app.morphe.util.matchAllMethodIndicesForEach -import app.morphe.util.matchSingle import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/autoplaypreview/HideAutoplayPreviewPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/autoplaypreview/HideAutoplayPreviewPatch.kt index 4bfb567395..530d8c319b 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/autoplaypreview/HideAutoplayPreviewPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/autoplaypreview/HideAutoplayPreviewPatch.kt @@ -3,6 +3,7 @@ package app.morphe.patches.youtube.layout.hide.autoplaypreview import app.morphe.patcher.extensions.InstructionExtensions.addInstructionsWithLabels import app.morphe.patcher.extensions.InstructionExtensions.getInstruction import app.morphe.patcher.patch.bytecodePatch +import app.morphe.patcher.util.getReference import app.morphe.patcher.util.smali.ExternalLabel import app.morphe.patches.shared.misc.settings.preference.SwitchPreference import app.morphe.patches.youtube.misc.extension.sharedExtensionPatch @@ -12,7 +13,6 @@ import app.morphe.patches.youtube.misc.settings.PreferenceScreen import app.morphe.patches.youtube.misc.settings.settingsPatch import app.morphe.patches.youtube.shared.Constants.COMPATIBILITY_YOUTUBE import app.morphe.patches.youtube.shared.LayoutConstructorFingerprint -import app.morphe.util.getReference import app.morphe.util.indexOfFirstInstructionOrThrow import app.morphe.util.indexOfFirstResourceIdOrThrow import com.android.tools.smali.dexlib2.Opcode diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/endscreensuggestedvideo/HideEndScreenSuggestedVideoPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/endscreensuggestedvideo/HideEndScreenSuggestedVideoPatch.kt index cce94f07d7..6868ba1b6a 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/endscreensuggestedvideo/HideEndScreenSuggestedVideoPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/endscreensuggestedvideo/HideEndScreenSuggestedVideoPatch.kt @@ -6,11 +6,11 @@ import app.morphe.patcher.extensions.InstructionExtensions.addInstructionsWithLa import app.morphe.patcher.fieldAccess import app.morphe.patcher.methodCall import app.morphe.patcher.patch.bytecodePatch +import app.morphe.patcher.util.getReference import app.morphe.patches.shared.misc.settings.preference.SwitchPreference import app.morphe.patches.youtube.misc.extension.sharedExtensionPatch import app.morphe.patches.youtube.misc.settings.PreferenceScreen import app.morphe.patches.youtube.shared.Constants.COMPATIBILITY_YOUTUBE -import app.morphe.util.getReference import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.iface.reference.FieldReference diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/general/HideLayoutComponentsPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/general/HideLayoutComponentsPatch.kt index 63a71a2cfe..4ac09b4133 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/general/HideLayoutComponentsPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/general/HideLayoutComponentsPatch.kt @@ -19,7 +19,10 @@ import app.morphe.patcher.extensions.InstructionExtensions.removeInstruction import app.morphe.patcher.extensions.InstructionExtensions.replaceInstruction import app.morphe.patcher.methodCall import app.morphe.patcher.patch.bytecodePatch +import app.morphe.patcher.util.findFreeRegister +import app.morphe.patcher.util.getReference import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable +import app.morphe.patcher.util.registersUsed import app.morphe.patcher.util.smali.ExternalLabel import app.morphe.patches.shared.misc.fix.proto.fixProtoLibraryPatch import app.morphe.patches.shared.misc.litho.filter.addLithoFilter @@ -65,14 +68,11 @@ import app.morphe.patches.youtube.shared.ModernRelateVideoOverlayFingerprint import app.morphe.patches.youtube.shared.RelateVideoOverlayLayoutParamFingerprint import app.morphe.patches.youtube.video.speed.custom.TapAndHoldSpeedFingerprint import app.morphe.util.addInstructionsAtControlFlowLabel -import app.morphe.util.findFreeRegister import app.morphe.util.findInstructionIndicesReversedOrThrow import app.morphe.util.fiveRegisters -import app.morphe.util.getReference import app.morphe.util.indexOfFirstInstructionOrThrow import app.morphe.util.indexOfFirstInstructionReversedOrThrow import app.morphe.util.insertLiteralOverride -import app.morphe.util.registersUsed import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/infocards/HideInfoCardsPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/infocards/HideInfoCardsPatch.kt index df33858404..039089f6cc 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/infocards/HideInfoCardsPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/infocards/HideInfoCardsPatch.kt @@ -5,6 +5,7 @@ import app.morphe.patcher.extensions.InstructionExtensions.addInstructionsWithLa import app.morphe.patcher.extensions.InstructionExtensions.getInstruction import app.morphe.patcher.methodCall import app.morphe.patcher.patch.bytecodePatch +import app.morphe.patcher.util.findFreeRegister import app.morphe.patcher.util.smali.ExternalLabel import app.morphe.patches.shared.misc.litho.filter.addLithoFilter import app.morphe.patches.shared.misc.settings.preference.SwitchPreference @@ -13,7 +14,6 @@ import app.morphe.patches.youtube.misc.litho.filter.lithoFilterPatch import app.morphe.patches.youtube.misc.settings.PreferenceScreen import app.morphe.patches.youtube.misc.settings.settingsPatch import app.morphe.patches.youtube.shared.Constants.COMPATIBILITY_YOUTUBE -import app.morphe.util.findFreeRegister import app.morphe.util.indexOfFirstInstructionOrThrow import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/relatedvideos/HideRelatedVideosPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/relatedvideos/HideRelatedVideosPatch.kt index d45ba0f67d..2e7d868151 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/relatedvideos/HideRelatedVideosPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/relatedvideos/HideRelatedVideosPatch.kt @@ -15,6 +15,7 @@ import app.morphe.patcher.fieldAccess import app.morphe.patcher.methodCall import app.morphe.patcher.patch.bytecodePatch import app.morphe.patcher.string +import app.morphe.patcher.util.getReference import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable import app.morphe.patches.shared.misc.fix.proto.fixProtoLibraryPatch import app.morphe.patches.shared.misc.fix.proto.immutableMethodRef @@ -25,7 +26,6 @@ import app.morphe.patches.youtube.misc.settings.PreferenceScreen import app.morphe.patches.youtube.misc.settings.settingsPatch import app.morphe.patches.youtube.shared.Constants.COMPATIBILITY_YOUTUBE import app.morphe.patches.youtube.shared.WatchNextResponseParserFingerprint -import app.morphe.util.getReference import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/settingsmenu/HideSettingsMenuFilterPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/settingsmenu/HideSettingsMenuFilterPatch.kt index 569668ef5a..7de02547cc 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/settingsmenu/HideSettingsMenuFilterPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/settingsmenu/HideSettingsMenuFilterPatch.kt @@ -9,6 +9,7 @@ package app.morphe.patches.youtube.layout.hide.settingsmenu import app.morphe.patcher.extensions.InstructionExtensions.getInstruction import app.morphe.patcher.patch.bytecodePatch +import app.morphe.patcher.util.findFreeRegister import app.morphe.patches.shared.misc.settings.preference.NonInteractivePreference import app.morphe.patches.shared.misc.settingsmenu.HIDE_MATCHING_METHOD import app.morphe.patches.shared.misc.settingsmenu.SETTINGS_MENU_FILTER_CLASS @@ -19,7 +20,6 @@ import app.morphe.patches.youtube.misc.settings.PreferenceScreen import app.morphe.patches.youtube.misc.settings.settingsPatch import app.morphe.patches.youtube.shared.Constants.COMPATIBILITY_YOUTUBE import app.morphe.util.addInstructionsAtControlFlowLabel -import app.morphe.util.findFreeRegister import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/shorts/HideShortsComponentsPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/shorts/HideShortsComponentsPatch.kt index 5bdfc44542..142b84d7c4 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/shorts/HideShortsComponentsPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/shorts/HideShortsComponentsPatch.kt @@ -18,6 +18,9 @@ import app.morphe.patcher.patch.bytecodePatch import app.morphe.patcher.patch.resourcePatch import app.morphe.patcher.resource.ResourceType import app.morphe.patcher.resourceLiteral +import app.morphe.patcher.util.getReference +import app.morphe.patcher.util.matchAllMethodIndicesForEach +import app.morphe.patcher.util.returnLate import app.morphe.patches.shared.misc.litho.filter.addLithoFilter import app.morphe.patches.shared.misc.settings.preference.PreferenceCategory import app.morphe.patches.shared.misc.settings.preference.SwitchPreference @@ -36,11 +39,8 @@ import app.morphe.patches.youtube.misc.settings.settingsPatch import app.morphe.patches.youtube.shared.Constants.COMPATIBILITY_YOUTUBE import app.morphe.util.addInstructionsAtControlFlowLabel import app.morphe.util.findElementByAttributeValueOrThrow -import app.morphe.util.getReference import app.morphe.util.indexOfFirstInstructionOrThrow -import app.morphe.util.matchAllMethodIndicesForEach import app.morphe.util.removeFromParent -import app.morphe.util.returnLate import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction import com.android.tools.smali.dexlib2.iface.reference.MethodReference diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/signintotvpopup/DisableSignInToTVPopupPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/signintotvpopup/DisableSignInToTVPopupPatch.kt index 6d317a685b..ed74e95a7a 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/signintotvpopup/DisableSignInToTVPopupPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/signintotvpopup/DisableSignInToTVPopupPatch.kt @@ -13,12 +13,12 @@ package app.morphe.patches.youtube.layout.hide.signintotvpopup import app.morphe.patcher.extensions.InstructionExtensions.addInstructionsWithLabels import app.morphe.patcher.extensions.InstructionExtensions.getInstruction import app.morphe.patcher.patch.bytecodePatch +import app.morphe.patcher.util.findFreeRegister import app.morphe.patches.shared.misc.settings.preference.SwitchPreference import app.morphe.patches.youtube.misc.extension.sharedExtensionPatch import app.morphe.patches.youtube.misc.settings.PreferenceScreen import app.morphe.patches.youtube.misc.settings.settingsPatch import app.morphe.patches.youtube.shared.Constants.COMPATIBILITY_YOUTUBE -import app.morphe.util.findFreeRegister import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction private const val EXTENSION_CLASS = diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/miniplayer/MiniplayerPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/miniplayer/MiniplayerPatch.kt index c34b996926..691313fccc 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/miniplayer/MiniplayerPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/miniplayer/MiniplayerPatch.kt @@ -17,6 +17,10 @@ import app.morphe.patcher.extensions.InstructionExtensions.getInstruction import app.morphe.patcher.extensions.InstructionExtensions.replaceInstruction import app.morphe.patcher.methodCall import app.morphe.patcher.patch.bytecodePatch +import app.morphe.patcher.util.cloneParameters +import app.morphe.patcher.util.findFreeRegister +import app.morphe.patcher.util.getReference +import app.morphe.patcher.util.numberOfParameterRegisters import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable import app.morphe.patcher.util.smali.ExternalLabel @@ -48,14 +52,10 @@ import app.morphe.patches.youtube.video.format.hookAdaptiveFormat import app.morphe.patches.youtube.video.format.videoFormatPatch import app.morphe.patches.youtube.video.information.videoInformationPatch import app.morphe.util.addInstructionsAtControlFlowLabel -import app.morphe.util.cloneParameters -import app.morphe.util.findFreeRegister import app.morphe.util.findInstructionIndicesReversedOrThrow -import app.morphe.util.getReference import app.morphe.util.indexOfFirstInstructionOrThrow import app.morphe.util.indexOfFirstLiteralInstructionOrThrow import app.morphe.util.insertLiteralOverride -import app.morphe.util.numberOfParameterRegisters import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/music/OverrideYouTubeMusicButtonsPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/music/OverrideYouTubeMusicButtonsPatch.kt index 3bfceef7f8..280b72e5d3 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/music/OverrideYouTubeMusicButtonsPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/music/OverrideYouTubeMusicButtonsPatch.kt @@ -13,6 +13,7 @@ import app.morphe.patcher.extensions.InstructionExtensions.replaceInstruction import app.morphe.patcher.methodCall import app.morphe.patcher.patch.bytecodePatch import app.morphe.patcher.patch.resourcePatch +import app.morphe.patcher.util.matchAllMethodIndicesForEach import app.morphe.patches.shared.misc.settings.preference.SwitchPreference import app.morphe.patches.shared.misc.settings.preference.TextPreference import app.morphe.patches.shared.misc.settings.preference.noTitleUnsortedPreferenceCategory @@ -20,7 +21,6 @@ import app.morphe.patches.youtube.misc.settings.PreferenceScreen import app.morphe.patches.youtube.misc.settings.settingsPatch import app.morphe.patches.youtube.shared.Constants.COMPATIBILITY_YOUTUBE import app.morphe.util.fiveRegisters -import app.morphe.util.matchAllMethodIndicesForEach import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.iface.instruction.RegisterRangeInstruction diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/originaltitles/VideoTitlesHookPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/originaltitles/VideoTitlesHookPatch.kt index 8d3bdd4fb4..bc11b13bc5 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/originaltitles/VideoTitlesHookPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/originaltitles/VideoTitlesHookPatch.kt @@ -11,6 +11,8 @@ import app.morphe.patcher.extensions.InstructionExtensions.addInstruction import app.morphe.patcher.extensions.InstructionExtensions.addInstructions import app.morphe.patcher.extensions.InstructionExtensions.getInstruction import app.morphe.patcher.patch.bytecodePatch +import app.morphe.patcher.util.findFreeRegister +import app.morphe.patcher.util.getReference import app.morphe.patches.shared.MediaSessionSetMetadataFingerprint import app.morphe.patches.shared.misc.litho.relayout.lithoRelayoutPatch import app.morphe.patches.shared.misc.proto.hookElement @@ -20,8 +22,6 @@ import app.morphe.patches.youtube.misc.extension.sharedExtensionPatch import app.morphe.patches.youtube.misc.proto.elementProtoParserHookPatch import app.morphe.patches.youtube.video.videoid.hookVideoId import app.morphe.patches.youtube.video.videoid.videoIdPatch -import app.morphe.util.findFreeRegister -import app.morphe.util.getReference import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/player/fullscreen/DisableFullscreenGesturesPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/player/fullscreen/DisableFullscreenGesturesPatch.kt index 83f74665cf..7b70d6d3f4 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/player/fullscreen/DisableFullscreenGesturesPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/player/fullscreen/DisableFullscreenGesturesPatch.kt @@ -15,6 +15,7 @@ import app.morphe.patcher.extensions.InstructionExtensions.addInstructions import app.morphe.patcher.extensions.InstructionExtensions.addInstructionsWithLabels import app.morphe.patcher.extensions.InstructionExtensions.getInstruction import app.morphe.patcher.patch.bytecodePatch +import app.morphe.patcher.util.findFreeRegister import app.morphe.patches.shared.misc.settings.preference.PreferenceScreenPreference import app.morphe.patches.shared.misc.settings.preference.PreferenceScreenPreference.Sorting import app.morphe.patches.shared.misc.settings.preference.SwitchPreference @@ -26,7 +27,6 @@ import app.morphe.patches.youtube.misc.settings.PreferenceScreen import app.morphe.patches.youtube.misc.settings.settingsPatch import app.morphe.patches.youtube.shared.Constants.COMPATIBILITY_YOUTUBE import app.morphe.util.addInstructionsAtControlFlowLabel -import app.morphe.util.findFreeRegister import app.morphe.util.toPublicAccessFlags import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/player/icons/PlayerIconStylePatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/player/icons/PlayerIconStylePatch.kt index f6d647ed93..7a9ff7fb2b 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/player/icons/PlayerIconStylePatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/player/icons/PlayerIconStylePatch.kt @@ -19,6 +19,7 @@ import app.morphe.patcher.patch.filePathOption import app.morphe.patcher.patch.resourcePatch import app.morphe.patcher.resource.ResourceType import app.morphe.patcher.resource.resourceId +import app.morphe.patcher.util.getReference import app.morphe.patcher.util.proxy.mutableTypes.MutableClass import app.morphe.patches.shared.misc.settings.preference.ListPreference import app.morphe.patches.youtube.misc.extension.sharedExtensionPatch @@ -31,7 +32,6 @@ import app.morphe.util.ResourceGroup import app.morphe.util.copyResources import app.morphe.util.findInstructionIndicesReversed import app.morphe.util.fiveRegisters -import app.morphe.util.getReference import app.morphe.util.indexOfFirstInstructionReversed import app.morphe.util.inputStreamFromBundledResource import com.android.tools.smali.dexlib2.Opcode diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/playlistautoplay/NavigationIntentHook.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/playlistautoplay/NavigationIntentHook.kt index a488306972..f6d3b1cf70 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/playlistautoplay/NavigationIntentHook.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/playlistautoplay/NavigationIntentHook.kt @@ -10,7 +10,7 @@ package app.morphe.patches.youtube.layout.playlistautoplay import app.morphe.patcher.Fingerprint import app.morphe.patcher.extensions.InstructionExtensions.addInstructionsWithLabels import app.morphe.patcher.patch.BytecodePatchContext -import app.morphe.util.cloneParameters +import app.morphe.patcher.util.cloneParameters import com.android.tools.smali.dexlib2.AccessFlags /** diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/returnyoutubedislike/ReturnYouTubeDislikePatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/returnyoutubedislike/ReturnYouTubeDislikePatch.kt index 33db75fb11..a6f25465ed 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/returnyoutubedislike/ReturnYouTubeDislikePatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/returnyoutubedislike/ReturnYouTubeDislikePatch.kt @@ -13,6 +13,8 @@ package app.morphe.patches.youtube.layout.returnyoutubedislike import app.morphe.patcher.extensions.InstructionExtensions.addInstructions import app.morphe.patcher.extensions.InstructionExtensions.getInstruction import app.morphe.patcher.patch.bytecodePatch +import app.morphe.patcher.util.getFreeRegisterProvider +import app.morphe.patcher.util.getReference import app.morphe.patches.shared.layout.returnyoutubedislike.DislikeFingerprint import app.morphe.patches.shared.layout.returnyoutubedislike.EndpointServiceNameFingerprint import app.morphe.patches.shared.layout.returnyoutubedislike.hookLikeDislikeButtons @@ -36,8 +38,6 @@ import app.morphe.patches.youtube.shared.Constants.COMPATIBILITY_YOUTUBE import app.morphe.patches.youtube.video.videoid.hookPlayerResponseVideoId import app.morphe.patches.youtube.video.videoid.hookVideoId import app.morphe.patches.youtube.video.videoid.videoIdPatch -import app.morphe.util.getFreeRegisterProvider -import app.morphe.util.getReference import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction import com.android.tools.smali.dexlib2.iface.reference.FieldReference diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/sharesheet/OpenSystemShareSheetPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/sharesheet/OpenSystemShareSheetPatch.kt index a1731e55c5..7af9883b03 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/sharesheet/OpenSystemShareSheetPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/sharesheet/OpenSystemShareSheetPatch.kt @@ -14,6 +14,9 @@ import app.morphe.patcher.extensions.InstructionExtensions.getInstruction import app.morphe.patcher.extensions.InstructionExtensions.instructions import app.morphe.patcher.patch.PatchException import app.morphe.patcher.patch.bytecodePatch +import app.morphe.patcher.util.findFreeRegister +import app.morphe.patcher.util.getReference +import app.morphe.patcher.util.p0Register import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable import app.morphe.patches.shared.misc.settings.preference.SwitchPreference @@ -21,9 +24,6 @@ import app.morphe.patches.youtube.misc.extension.sharedExtensionPatch import app.morphe.patches.youtube.misc.settings.PreferenceScreen import app.morphe.patches.youtube.misc.settings.settingsPatch import app.morphe.patches.youtube.shared.Constants.COMPATIBILITY_YOUTUBE -import app.morphe.util.findFreeRegister -import app.morphe.util.getReference -import app.morphe.util.p0Register import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/shortsautoplay/ShortsAutoplayPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/shortsautoplay/ShortsAutoplayPatch.kt index ad5942d127..8dd12efc76 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/shortsautoplay/ShortsAutoplayPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/shortsautoplay/ShortsAutoplayPatch.kt @@ -12,6 +12,7 @@ import app.morphe.patcher.extensions.InstructionExtensions.addInstructionsWithLa import app.morphe.patcher.extensions.InstructionExtensions.getInstruction import app.morphe.patcher.methodCall import app.morphe.patcher.patch.bytecodePatch +import app.morphe.patcher.util.getReference import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable import app.morphe.patches.shared.misc.settings.preference.SwitchPreference import app.morphe.patches.youtube.misc.extension.sharedExtensionPatch @@ -23,7 +24,6 @@ import app.morphe.patches.youtube.misc.settings.PreferenceScreen import app.morphe.patches.youtube.misc.settings.settingsPatch import app.morphe.patches.youtube.shared.Constants.COMPATIBILITY_YOUTUBE import app.morphe.util.findInstructionIndicesReversedOrThrow -import app.morphe.util.getReference import app.morphe.util.indexOfFirstInstructionOrThrow import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/theme/ThemePatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/theme/ThemePatch.kt index acbda41e6e..013c667991 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/theme/ThemePatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/theme/ThemePatch.kt @@ -20,6 +20,8 @@ import app.morphe.patcher.patch.PatchException import app.morphe.patcher.patch.resourcePatch import app.morphe.patcher.resource.ResourceType import app.morphe.patcher.resourceLiteral +import app.morphe.patcher.util.matchAllMethodIndicesForEach +import app.morphe.patcher.util.registersUsed import app.morphe.patches.shared.layout.theme.STYLE_DEFAULT_COLOR_NAMES_DARK import app.morphe.patches.shared.layout.theme.STYLE_DEFAULT_COLOR_NAMES_LIGHT import app.morphe.patches.shared.layout.theme.THEME_COLOR_EXTENSION_CLASS @@ -55,8 +57,6 @@ import app.morphe.patches.youtube.shared.LayoutConstructorFingerprint import app.morphe.util.findInstructionIndicesReversedOrThrow import app.morphe.util.forEachChildElement import app.morphe.util.insertLiteralOverride -import app.morphe.util.matchAllMethodIndicesForEach -import app.morphe.util.registersUsed import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/misc/backgesture/BackGesturePatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/backgesture/BackGesturePatch.kt index f30d61b270..b5d747e7d5 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/misc/backgesture/BackGesturePatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/backgesture/BackGesturePatch.kt @@ -9,11 +9,11 @@ package app.morphe.patches.youtube.misc.backgesture import app.morphe.patcher.extensions.InstructionExtensions.addInstruction import app.morphe.patcher.patch.bytecodePatch +import app.morphe.patcher.util.getReference import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod import app.morphe.patches.youtube.misc.playservice.is_20_40_or_greater import app.morphe.patches.youtube.misc.playservice.versionCheckPatch import app.morphe.util.addInstructionsAtControlFlowLabel -import app.morphe.util.getReference import app.morphe.util.indexOfFirstInstructionOrThrow import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.iface.reference.MethodReference diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/misc/contexthook/ClientContextHookPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/contexthook/ClientContextHookPatch.kt index 1bd86526e4..ca017b1d31 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/misc/contexthook/ClientContextHookPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/contexthook/ClientContextHookPatch.kt @@ -13,6 +13,7 @@ import app.morphe.patcher.extensions.InstructionExtensions.getInstruction import app.morphe.patcher.fieldAccess import app.morphe.patcher.methodCall import app.morphe.patcher.patch.bytecodePatch +import app.morphe.patcher.util.cloneParameters import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable import app.morphe.patches.youtube.misc.extension.sharedExtensionPatch import app.morphe.patches.youtube.misc.playservice.is_21_20_or_greater @@ -20,7 +21,6 @@ import app.morphe.patches.youtube.misc.playservice.is_21_21_or_greater import app.morphe.patches.youtube.misc.playservice.versionCheckPatch import app.morphe.patches.youtube.shared.CLIENT_INFO_CLASS import app.morphe.util.addInstructionsAtControlFlowLabel -import app.morphe.util.cloneParameters import app.morphe.util.findInstructionIndicesReversedOrThrow import app.morphe.util.indexOfFirstInstructionOrThrow import com.android.tools.smali.dexlib2.AccessFlags diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/misc/engagement/EngagementPanelHookPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/engagement/EngagementPanelHookPatch.kt index ef2477bdfe..38194d4e8f 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/misc/engagement/EngagementPanelHookPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/engagement/EngagementPanelHookPatch.kt @@ -3,10 +3,10 @@ package app.morphe.patches.youtube.misc.engagement import app.morphe.patcher.extensions.InstructionExtensions.addInstructionsWithLabels import app.morphe.patcher.extensions.InstructionExtensions.getInstruction import app.morphe.patcher.patch.bytecodePatch +import app.morphe.patcher.util.getReference import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod import app.morphe.patches.youtube.misc.extension.sharedExtensionPatch import app.morphe.patches.youtube.shared.EngagementPanelControllerFingerprint -import app.morphe.util.getReference import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction import com.android.tools.smali.dexlib2.iface.reference.FieldReference import java.lang.ref.WeakReference diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/misc/fix/backtoexitgesture/FixBackToExitGesturePatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/fix/backtoexitgesture/FixBackToExitGesturePatch.kt index cc7b4d4a66..7778981f9d 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/misc/fix/backtoexitgesture/FixBackToExitGesturePatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/fix/backtoexitgesture/FixBackToExitGesturePatch.kt @@ -12,6 +12,7 @@ package app.morphe.patches.youtube.misc.fix.backtoexitgesture import app.morphe.patcher.extensions.InstructionExtensions.addInstruction import app.morphe.patcher.patch.bytecodePatch +import app.morphe.patcher.util.getReference import app.morphe.patches.shared.misc.settings.preference.SwitchPreference import app.morphe.patches.youtube.misc.backgesture.addBackPressedHook import app.morphe.patches.youtube.misc.backgesture.addPredictiveBackGestureHook @@ -22,7 +23,6 @@ import app.morphe.patches.youtube.misc.playservice.versionCheckPatch import app.morphe.patches.youtube.misc.settings.PreferenceScreen import app.morphe.patches.youtube.misc.settings.settingsPatch import app.morphe.util.addInstructionsAtControlFlowLabel -import app.morphe.util.getReference import app.morphe.util.indexOfFirstInstructionOrThrow import app.morphe.util.insertLiteralOverride import com.android.tools.smali.dexlib2.Opcode diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/misc/fix/playbackspeed/FIxPlaybackSpeedWhilePlayingPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/fix/playbackspeed/FIxPlaybackSpeedWhilePlayingPatch.kt index 538928534b..5c65f6a045 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/misc/fix/playbackspeed/FIxPlaybackSpeedWhilePlayingPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/fix/playbackspeed/FIxPlaybackSpeedWhilePlayingPatch.kt @@ -13,10 +13,10 @@ package app.morphe.patches.youtube.misc.fix.playbackspeed import app.morphe.patcher.extensions.InstructionExtensions.addInstructionsWithLabels import app.morphe.patcher.extensions.InstructionExtensions.getInstruction import app.morphe.patcher.patch.bytecodePatch +import app.morphe.patcher.util.findFreeRegister import app.morphe.patcher.util.smali.ExternalLabel import app.morphe.patches.youtube.misc.extension.sharedExtensionPatch import app.morphe.patches.youtube.misc.playertype.playerTypeHookPatch -import app.morphe.util.findFreeRegister import app.morphe.util.indexOfFirstInstructionOrThrow import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/misc/fix/preference/FixPreferenceIconPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/fix/preference/FixPreferenceIconPatch.kt index 1a2fece12f..5f8e435e03 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/misc/fix/preference/FixPreferenceIconPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/fix/preference/FixPreferenceIconPatch.kt @@ -10,14 +10,14 @@ package app.morphe.patches.youtube.misc.fix.preference import app.morphe.patcher.extensions.InstructionExtensions.addInstructionsWithLabels import app.morphe.patcher.extensions.InstructionExtensions.getInstruction import app.morphe.patcher.patch.bytecodePatch +import app.morphe.patcher.util.findFreeRegister +import app.morphe.patcher.util.getReference import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable import app.morphe.patches.youtube.misc.extension.sharedExtensionPatch import app.morphe.patches.youtube.misc.playservice.is_21_14_or_greater import app.morphe.patches.youtube.misc.playservice.versionCheckPatch import app.morphe.util.addInstructionsAtControlFlowLabel -import app.morphe.util.findFreeRegister -import app.morphe.util.getReference import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/misc/fix/videoactionbar/RestoreOldVideoActionBarPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/fix/videoactionbar/RestoreOldVideoActionBarPatch.kt index 3b1506af7e..852092863d 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/misc/fix/videoactionbar/RestoreOldVideoActionBarPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/fix/videoactionbar/RestoreOldVideoActionBarPatch.kt @@ -12,7 +12,9 @@ import app.morphe.patcher.extensions.InstructionExtensions.addInstructions import app.morphe.patcher.extensions.InstructionExtensions.getInstruction import app.morphe.patcher.methodCall import app.morphe.patcher.patch.bytecodePatch +import app.morphe.patcher.util.getReference import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable +import app.morphe.patcher.util.registersUsed import app.morphe.patches.shared.BuildInnerTubeProtoRequestUriFingerprint import app.morphe.patches.shared.BuildInnerTubeProtoRequestUriLegacyFingerprint import app.morphe.patches.shared.misc.fix.proto.fixProtoLibraryPatch @@ -33,10 +35,8 @@ import app.morphe.patches.youtube.misc.settings.settingsPatch import app.morphe.patches.youtube.shared.ModernRelateVideoOverlayFingerprint import app.morphe.patches.youtube.shared.RelateVideoOverlayLayoutParamFingerprint import app.morphe.util.findInstructionIndicesReversedOrThrow -import app.morphe.util.getReference import app.morphe.util.indexOfFirstInstructionReversedOrThrow import app.morphe.util.insertLiteralOverride -import app.morphe.util.registersUsed import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/misc/imageurlhook/CronetImageURLHook.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/imageurlhook/CronetImageURLHook.kt index ec754af563..7c51ef9281 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/misc/imageurlhook/CronetImageURLHook.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/imageurlhook/CronetImageURLHook.kt @@ -14,10 +14,10 @@ import app.morphe.patcher.extensions.InstructionExtensions.addInstruction import app.morphe.patcher.extensions.InstructionExtensions.addInstructions import app.morphe.patcher.extensions.InstructionExtensions.instructions import app.morphe.patcher.patch.bytecodePatch +import app.morphe.patcher.util.getReference import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable import app.morphe.patches.youtube.misc.extension.sharedExtensionPatch -import app.morphe.util.getReference import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/misc/links/OpenLinksExternallyPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/links/OpenLinksExternallyPatch.kt index 84003c0c77..a757eef72d 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/misc/links/OpenLinksExternallyPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/links/OpenLinksExternallyPatch.kt @@ -5,10 +5,10 @@ import app.morphe.patcher.extensions.InstructionExtensions.addInstructions import app.morphe.patcher.extensions.InstructionExtensions.getInstruction import app.morphe.patcher.patch.bytecodePatch import app.morphe.patcher.string +import app.morphe.patcher.util.matchAllMethodIndicesForEach import app.morphe.patches.shared.misc.settings.preference.SwitchPreference import app.morphe.patches.youtube.misc.settings.PreferenceScreen import app.morphe.patches.youtube.shared.Constants.COMPATIBILITY_YOUTUBE -import app.morphe.util.matchAllMethodIndicesForEach import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction private const val EXTENSION_CLASS = "Lapp/morphe/extension/youtube/patches/OpenLinksExternallyPatch;" diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/misc/litho/rendernext/DisableRenderNextPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/litho/rendernext/DisableRenderNextPatch.kt index b3296cb9b1..b82ae7d4c9 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/misc/litho/rendernext/DisableRenderNextPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/litho/rendernext/DisableRenderNextPatch.kt @@ -10,12 +10,12 @@ package app.morphe.patches.youtube.misc.litho.rendernext import app.morphe.patcher.extensions.InstructionExtensions.addInstruction import app.morphe.patcher.extensions.InstructionExtensions.getInstruction import app.morphe.patcher.patch.bytecodePatch +import app.morphe.patcher.util.returnEarly import app.morphe.patches.youtube.misc.playservice.is_20_29_or_greater import app.morphe.patches.youtube.misc.playservice.is_20_47_or_greater import app.morphe.patches.youtube.misc.playservice.is_21_30_or_greater import app.morphe.patches.youtube.misc.playservice.versionCheckPatch import app.morphe.util.insertLiteralOverride -import app.morphe.util.returnEarly import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction /** diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/misc/loadvideo/LoadVideoHookPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/loadvideo/LoadVideoHookPatch.kt index 4dead02038..52d9771154 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/misc/loadvideo/LoadVideoHookPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/loadvideo/LoadVideoHookPatch.kt @@ -10,12 +10,12 @@ package app.morphe.patches.youtube.misc.loadvideo import app.morphe.patcher.extensions.InstructionExtensions.addInstruction import app.morphe.patcher.extensions.InstructionExtensions.addInstructions import app.morphe.patcher.patch.bytecodePatch +import app.morphe.patcher.util.getReference import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable +import app.morphe.patcher.util.returnEarly import app.morphe.patches.youtube.misc.extension.sharedExtensionPatch import app.morphe.patches.youtube.shared.YOUTUBE_MAIN_ACTIVITY_CLASS_TYPE -import app.morphe.util.getReference import app.morphe.util.indexOfFirstInstructionReversedOrThrow -import app.morphe.util.returnEarly import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/misc/navigation/NavigationBarHookPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/navigation/NavigationBarHookPatch.kt index 77ccc02033..80b74a44a6 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/misc/navigation/NavigationBarHookPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/navigation/NavigationBarHookPatch.kt @@ -17,6 +17,8 @@ import app.morphe.patcher.extensions.InstructionExtensions.instructions import app.morphe.patcher.patch.PatchException import app.morphe.patcher.patch.bytecodePatch import app.morphe.patcher.patch.resourcePatch +import app.morphe.patcher.util.findFreeRegister +import app.morphe.patcher.util.getReference import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable import app.morphe.patches.youtube.misc.backgesture.addBackPressedHook @@ -29,8 +31,6 @@ import app.morphe.patches.youtube.misc.playservice.versionCheckPatch import app.morphe.patches.youtube.shared.ActionBarSearchResultsFingerprint import app.morphe.util.ResourceGroup import app.morphe.util.copyResources -import app.morphe.util.findFreeRegister -import app.morphe.util.getReference import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/misc/playercontrols/LegacyPlayerControlsPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/playercontrols/LegacyPlayerControlsPatch.kt index e0086edfc5..7f61fb28c2 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/misc/playercontrols/LegacyPlayerControlsPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/playercontrols/LegacyPlayerControlsPatch.kt @@ -22,6 +22,10 @@ import app.morphe.patcher.patch.PatchException import app.morphe.patcher.patch.bytecodePatch import app.morphe.patcher.patch.resourcePatch import app.morphe.patcher.util.Document +import app.morphe.patcher.util.findFreeRegister +import app.morphe.patcher.util.getReference +import app.morphe.patcher.util.matchAllMethodIndicesForEach +import app.morphe.patcher.util.p0Register import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod import app.morphe.patches.shared.layout.theme.lithoColorHookPatch import app.morphe.patches.shared.layout.theme.lithoDrawableColorHook @@ -52,14 +56,10 @@ import app.morphe.patches.youtube.misc.settings.settingsPatch import app.morphe.util.copyXmlNode import app.morphe.util.findElementByAttributeValue import app.morphe.util.findElementByAttributeValueOrThrow -import app.morphe.util.findFreeRegister import app.morphe.util.findInstructionIndicesReversed import app.morphe.util.getMutableMethod -import app.morphe.util.getReference import app.morphe.util.inputStreamFromBundledResource import app.morphe.util.insertLiteralOverride -import app.morphe.util.matchAllMethodIndicesForEach -import app.morphe.util.p0Register import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/misc/settings/SettingsPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/settings/SettingsPatch.kt index 7cde02c10a..7e178f0799 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/misc/settings/SettingsPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/settings/SettingsPatch.kt @@ -16,6 +16,7 @@ import app.morphe.patcher.extensions.InstructionExtensions.getInstruction import app.morphe.patcher.patch.BytecodePatchContext import app.morphe.patcher.patch.bytecodePatch import app.morphe.patcher.patch.resourcePatch +import app.morphe.patcher.util.getFreeRegisterProvider import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable import app.morphe.patches.all.misc.clone.setOrGetFallbackPackageName import app.morphe.patches.all.misc.crashlog.crashLogPatch @@ -62,7 +63,6 @@ import app.morphe.util.addInstructionsAtControlFlowLabel import app.morphe.util.copyResources import app.morphe.util.findElementByAttributeValueOrThrow import app.morphe.util.findInstructionIndicesReversedOrThrow -import app.morphe.util.getFreeRegisterProvider import app.morphe.util.insertLiteralOverride import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/shared/OpenVideoIntentPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/shared/OpenVideoIntentPatch.kt index f8c696b4a2..d0ceee8ebb 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/shared/OpenVideoIntentPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/shared/OpenVideoIntentPatch.kt @@ -10,6 +10,7 @@ package app.morphe.patches.youtube.shared import app.morphe.patcher.extensions.InstructionExtensions.addInstruction import app.morphe.patcher.extensions.InstructionExtensions.addInstructionsWithLabels import app.morphe.patcher.patch.bytecodePatch +import app.morphe.patcher.util.getFreeRegisterProvider import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod import app.morphe.patches.all.misc.resources.addResourcesPatch import app.morphe.patches.youtube.misc.playservice.is_21_20_or_greater @@ -18,7 +19,6 @@ import app.morphe.patches.youtube.misc.settings.settingsPatch import app.morphe.patches.youtube.shared.Constants.COMPATIBILITY_YOUTUBE import app.morphe.patches.youtube.video.information.PlaybackStartDescriptorToStringFingerprint import app.morphe.util.addInstructionsAtControlFlowLabel -import app.morphe.util.getFreeRegisterProvider import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction import java.lang.ref.WeakReference diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/video/codecs/DisableVideoCodecsPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/video/codecs/DisableVideoCodecsPatch.kt index d2b7772507..0445b6bbca 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/video/codecs/DisableVideoCodecsPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/video/codecs/DisableVideoCodecsPatch.kt @@ -14,6 +14,7 @@ import app.morphe.patcher.extensions.InstructionExtensions.addInstructionsWithLa import app.morphe.patcher.extensions.InstructionExtensions.getInstruction import app.morphe.patcher.extensions.InstructionExtensions.replaceInstruction import app.morphe.patcher.patch.bytecodePatch +import app.morphe.patcher.util.matchAllMethodIndicesForEach import app.morphe.patches.shared.misc.settings.preference.ListPreference import app.morphe.patches.shared.misc.settings.preference.SwitchPreference import app.morphe.patches.shared.misc.settings.preference.noTitleUnsortedPreferenceCategory @@ -21,7 +22,6 @@ import app.morphe.patches.youtube.misc.extension.sharedExtensionPatch import app.morphe.patches.youtube.misc.settings.PreferenceScreen import app.morphe.patches.youtube.misc.settings.settingsPatch import app.morphe.patches.youtube.shared.Constants.COMPATIBILITY_YOUTUBE -import app.morphe.util.matchAllMethodIndicesForEach import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction private const val EXTENSION_CLASS = diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/video/format/VideoFormatPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/video/format/VideoFormatPatch.kt index b404ed9c08..a525ea6773 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/video/format/VideoFormatPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/video/format/VideoFormatPatch.kt @@ -10,15 +10,15 @@ package app.morphe.patches.youtube.video.format import app.morphe.patcher.extensions.InstructionExtensions.addInstructions import app.morphe.patcher.extensions.InstructionExtensions.getInstruction import app.morphe.patcher.patch.bytecodePatch +import app.morphe.patcher.util.cloneParameters +import app.morphe.patcher.util.findFreeRegister +import app.morphe.patcher.util.getReference +import app.morphe.patcher.util.numberOfParameterRegistersLogical import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable import app.morphe.patches.shared.FormatStreamModelToStringFingerprint import app.morphe.patches.youtube.misc.extension.sharedExtensionPatch -import app.morphe.util.cloneParameters -import app.morphe.util.findFreeRegister import app.morphe.util.findMethodFromToString -import app.morphe.util.getReference -import app.morphe.util.numberOfParameterRegistersLogical import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/video/information/VideoInformationPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/video/information/VideoInformationPatch.kt index c5bd74c1ad..bf4587f7f5 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/video/information/VideoInformationPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/video/information/VideoInformationPatch.kt @@ -18,6 +18,8 @@ import app.morphe.patcher.extensions.InstructionExtensions.replaceInstruction import app.morphe.patcher.patch.BytecodePatchContext import app.morphe.patcher.patch.PatchException import app.morphe.patcher.patch.bytecodePatch +import app.morphe.patcher.util.getReference +import app.morphe.patcher.util.p0Register import app.morphe.patcher.util.proxy.mutableTypes.MutableClass import app.morphe.patcher.util.proxy.mutableTypes.MutableField import app.morphe.patcher.util.proxy.mutableTypes.MutableField.Companion.toMutable @@ -48,9 +50,7 @@ import app.morphe.patches.youtube.video.videoid.hookPlayerResponseVideoId import app.morphe.patches.youtube.video.videoid.hookVideoId import app.morphe.patches.youtube.video.videoid.videoIdPatch import app.morphe.util.addInstructionsAtControlFlowLabel -import app.morphe.util.getReference import app.morphe.util.indexOfFirstInstructionOrThrow -import app.morphe.util.p0Register import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/video/playerresponse/PlayerResponseMethodHookPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/video/playerresponse/PlayerResponseMethodHookPatch.kt index 89eb24fd37..6cda90df2c 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/video/playerresponse/PlayerResponseMethodHookPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/video/playerresponse/PlayerResponseMethodHookPatch.kt @@ -14,12 +14,12 @@ import app.morphe.patcher.Fingerprint import app.morphe.patcher.extensions.InstructionExtensions.addInstruction import app.morphe.patcher.extensions.InstructionExtensions.addInstructions import app.morphe.patcher.patch.bytecodePatch +import app.morphe.patcher.util.p0Register import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod import app.morphe.patches.youtube.misc.extension.sharedExtensionPatch import app.morphe.patches.youtube.misc.playservice.is_20_26_or_greater import app.morphe.patches.youtube.misc.playservice.is_20_46_or_greater import app.morphe.patches.youtube.misc.playservice.versionCheckPatch -import app.morphe.util.p0Register import java.lang.ref.WeakReference private val hooks = mutableSetOf() diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/video/quality/AdvancedVideoQualityMenuPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/video/quality/AdvancedVideoQualityMenuPatch.kt index 40bafa1857..5a24a9fd26 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/video/quality/AdvancedVideoQualityMenuPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/video/quality/AdvancedVideoQualityMenuPatch.kt @@ -14,6 +14,7 @@ import app.morphe.patcher.extensions.InstructionExtensions.addInstruction import app.morphe.patcher.extensions.InstructionExtensions.addInstructions import app.morphe.patcher.extensions.InstructionExtensions.addInstructionsWithLabels import app.morphe.patcher.patch.bytecodePatch +import app.morphe.patcher.util.findFreeRegister import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable import app.morphe.patches.shared.misc.litho.filter.addLithoFilter import app.morphe.patches.shared.misc.settings.preference.SwitchPreference @@ -22,7 +23,6 @@ import app.morphe.patches.youtube.misc.litho.filter.lithoFilterPatch import app.morphe.patches.youtube.misc.recyclerviewtree.addRecyclerViewTreeHook import app.morphe.patches.youtube.misc.recyclerviewtree.recyclerViewTreeHookPatch import app.morphe.patches.youtube.misc.settings.settingsPatch -import app.morphe.util.findFreeRegister import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation import com.android.tools.smali.dexlib2.immutable.ImmutableMethod diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/video/quality/HidePremiumVideoQualityPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/video/quality/HidePremiumVideoQualityPatch.kt index 86189f28a8..546eb99dc1 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/video/quality/HidePremiumVideoQualityPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/video/quality/HidePremiumVideoQualityPatch.kt @@ -3,12 +3,12 @@ package app.morphe.patches.youtube.video.quality import app.morphe.patcher.extensions.InstructionExtensions.addInstructions import app.morphe.patcher.extensions.InstructionExtensions.getInstruction import app.morphe.patcher.patch.bytecodePatch +import app.morphe.patcher.util.getReference import app.morphe.patches.shared.misc.settings.preference.SwitchPreference import app.morphe.patches.youtube.misc.extension.sharedExtensionPatch import app.morphe.patches.youtube.misc.settings.settingsPatch import app.morphe.patches.youtube.video.information.EXTENSION_VIDEO_QUALITY_INTERFACE import app.morphe.patches.youtube.video.information.videoInformationPatch -import app.morphe.util.getReference import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction import com.android.tools.smali.dexlib2.iface.reference.FieldReference diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/video/quality/RememberVideoQualityPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/video/quality/RememberVideoQualityPatch.kt index c94f4fbcd5..daf102d912 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/video/quality/RememberVideoQualityPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/video/quality/RememberVideoQualityPatch.kt @@ -31,10 +31,10 @@ import app.morphe.patches.youtube.video.information.EXTENSION_VIDEO_QUALITY_INTE import app.morphe.patches.youtube.video.information.videoInformationPatch import app.morphe.util.addInstructionsAtControlFlowLabel import app.morphe.util.findFieldFromToString -import app.morphe.util.findFreeRegister +import app.morphe.patcher.util.findFreeRegister import app.morphe.util.findInstructionIndicesReversedOrThrow -import app.morphe.util.getFreeRegisterProvider -import app.morphe.util.getReference +import app.morphe.patcher.util.getFreeRegisterProvider +import app.morphe.patcher.util.getReference import app.morphe.util.indexOfFirstInstructionReversedOrThrow import app.morphe.util.insertLiteralOverride import com.android.tools.smali.dexlib2.AccessFlags diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/video/series/HistoryPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/video/series/HistoryPatch.kt index 7e67d33c4f..1872947927 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/video/series/HistoryPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/video/series/HistoryPatch.kt @@ -13,15 +13,15 @@ import app.morphe.patcher.fieldAccess import app.morphe.patcher.methodCall import app.morphe.patcher.patch.BytecodePatchContext import app.morphe.patcher.patch.PatchException +import app.morphe.patcher.util.getReference import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable +import app.morphe.patcher.util.registersUsed import app.morphe.patches.youtube.layout.buttons.navigation.PivotBarRendererFingerprint import app.morphe.patches.youtube.layout.buttons.navigation.PivotBarRendererListFingerprint import app.morphe.patches.youtube.misc.backgesture.YouTubeMainActivityOnBackPressedFingerprint import app.morphe.util.findInstructionIndicesReversedOrThrow -import app.morphe.util.getReference import app.morphe.util.indexOfFirstInstructionOrThrow import app.morphe.util.indexOfFirstInstructionReversedOrThrow -import app.morphe.util.registersUsed import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/video/series/NativeHistoryMatching.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/video/series/NativeHistoryMatching.kt index 3c13cb56f5..53e29150d3 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/video/series/NativeHistoryMatching.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/video/series/NativeHistoryMatching.kt @@ -9,7 +9,7 @@ package app.morphe.patches.youtube.video.series import app.morphe.patcher.patch.BytecodePatchContext import app.morphe.patcher.patch.PatchException -import app.morphe.util.getReference +import app.morphe.patcher.util.getReference import com.android.tools.smali.dexlib2.iface.ClassDef import com.android.tools.smali.dexlib2.iface.Method import com.android.tools.smali.dexlib2.iface.reference.FieldReference diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/video/speed/custom/CustomPlaybackSpeedPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/video/speed/custom/CustomPlaybackSpeedPatch.kt index 6e1020b1f3..d7ab7951ab 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/video/speed/custom/CustomPlaybackSpeedPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/video/speed/custom/CustomPlaybackSpeedPatch.kt @@ -18,9 +18,11 @@ import app.morphe.patcher.extensions.InstructionExtensions.instructions import app.morphe.patcher.extensions.InstructionExtensions.replaceInstruction import app.morphe.patcher.patch.bytecodePatch import app.morphe.patcher.patch.resourcePatch +import app.morphe.patcher.util.getReference import app.morphe.patcher.util.proxy.mutableTypes.MutableField import app.morphe.patcher.util.proxy.mutableTypes.MutableField.Companion.toMutable import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable +import app.morphe.patcher.util.returnEarly import app.morphe.patches.shared.misc.litho.context.conversionContextPatch import app.morphe.patches.shared.misc.litho.filter.addLithoFilter import app.morphe.patches.shared.misc.settings.preference.InputType @@ -46,10 +48,8 @@ import app.morphe.util.ResourceGroup import app.morphe.util.addInstructionsAtControlFlowLabel import app.morphe.util.copyResources import app.morphe.util.findInstructionIndicesReversedOrThrow -import app.morphe.util.getReference import app.morphe.util.indexOfFirstLiteralInstructionOrThrow import app.morphe.util.insertLiteralOverride -import app.morphe.util.returnEarly import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation diff --git a/patches/src/test/kotlin/app/morphe/patches/music/interaction/jam/JamPatchRegressionTest.kt b/patches/src/test/kotlin/app/morphe/patches/music/interaction/jam/JamPatchRegressionTest.kt index b3fa1ea522..49e5af29ff 100644 --- a/patches/src/test/kotlin/app/morphe/patches/music/interaction/jam/JamPatchRegressionTest.kt +++ b/patches/src/test/kotlin/app/morphe/patches/music/interaction/jam/JamPatchRegressionTest.kt @@ -12,9 +12,9 @@ import app.morphe.patcher.PatcherConfig import app.morphe.patcher.extensions.InstructionExtensions.addInstructions import app.morphe.patcher.patch.PatchException import app.morphe.patcher.patch.bytecodePatch +import app.morphe.patcher.util.matchSingle import app.morphe.patcher.util.proxy.mutableTypes.MutableField.Companion.toMutable import app.morphe.util.getMutableMethod -import app.morphe.util.matchSingle import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.immutable.ImmutableField import kotlinx.coroutines.runBlocking From 18acc41800e299a58e8ce22c17a33bea2b2c9128 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Thu, 22 Oct 2026 21:10:46 +0000 Subject: [PATCH 003/189] chore: Update gradle --- gradle/wrapper/gradle-wrapper.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a78c88e539..5907d39c37 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionSha256Sum=dce76f55f8e251a3a1f130eb120f30b3d271de2b76c9b0729d316b5a1b6dc01f -distributionUrl=https\://services.gradle.org/distributions/gradle-9.8.1-bin.zip +distributionSha256Sum=21bd5da2de49b642904077f38c0efdc7778e92b3ed0889392dca61b97e389607 +distributionUrl=https\://services.gradle.org/distributions/gradle-9.8.2-bin.zip networkTimeout=10000 retries=0 retryBackOffMs=500 From 0345136b552c49b70c01fcc19a18762f57d3b7ad Mon Sep 17 00:00:00 2001 From: 0xrxL <127248639+0xrxL@users.noreply.github.com> Date: Thu, 22 Oct 2026 23:37:06 +0200 Subject: [PATCH 004/189] fix(YouTube): Miniplayer covered by the keyboard and empty band above the navigation bar --- .../patches/MinimalMiniplayerPatch.java | 780 ++++++++++-------- .../youtube/patches/NavigationBarPatch.java | 55 +- .../layout/buttons/navigation/Fingerprints.kt | 26 + .../buttons/navigation/NavigationBarPatch.kt | 115 ++- 4 files changed, 590 insertions(+), 386 deletions(-) diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/MinimalMiniplayerPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/MinimalMiniplayerPatch.java index 80c9eb38d1..082c1086aa 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/MinimalMiniplayerPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/MinimalMiniplayerPatch.java @@ -16,6 +16,7 @@ import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; import android.annotation.SuppressLint; +import android.app.Activity; import android.content.res.ColorStateList; import android.graphics.Color; import android.graphics.Insets; @@ -34,6 +35,7 @@ import android.view.View; import android.view.ViewGroup; import android.view.ViewParent; +import android.view.ViewTreeObserver; import android.view.WindowInsets; import android.view.animation.DecelerateInterpolator; import android.widget.ImageView; @@ -224,6 +226,7 @@ private static int videoWidthFor(int height) { *

* The legacy bar layout, looked up by YouTube itself and then left unused. */ + @SuppressLint("ClickableViewAccessibility") public static void setLegacyControls(ViewGroup controlsLayout) { if (!ENABLED) return; @@ -301,10 +304,121 @@ public static void setLegacyControls(ViewGroup controlsLayout) { progressBar.setVisibility(View.GONE); } - setBarGestures(controlsLayout); + // YouTube reads a gesture against the shape of its own miniplayer, which the bar is not, + // so the bar answers for itself: a tap or a drag up expands, a drag down closes. + final int slop = Dim.dp20; + + controlsLayout.setClickable(true); + controlsLayout.setContentDescription(ACCESSIBILITY_MINIPLAYER_VIEW_STRING); + controlsLayout.setOnClickListener(v -> expandPlayer()); + + controlsLayout.setOnTouchListener(new View.OnTouchListener() { + float downX; + float downY; + + @Override + public boolean onTouch(View view, MotionEvent event) { + switch (event.getActionMasked()) { + case MotionEvent.ACTION_DOWN: + downX = event.getRawX(); + downY = event.getRawY(); + keepTouchFromParent(view); + return true; + + case MotionEvent.ACTION_UP: + final float draggedUp = downY - event.getRawY(); + + if (draggedUp > slop) { + expandPlayer(); + } else if (-draggedUp > slop) { + closeBar(); + } else if (Math.abs(event.getRawX() - downX) < slop) { + // Goes through the click, so accessibility services see it. + view.performClick(); + } + return true; + + default: + // Held for the whole gesture, the watch layout reads the moves as a pan. + return true; + } + } + }); + + listenToWindowLayout(controlsLayout); + + if (!listenersInstalled) { + listenersInstalled = true; + + PlayerType.getOnChange().addObserver((PlayerType type) -> { + final boolean minimized = type == PlayerType.WATCH_WHILE_MINIMIZED; + + Utils.runOnMainThreadNowOrLater(() -> { + startTicking(minimized); + + if (minimized) { + refreshContents(); + + // Animates whatever shape the player was left in into the bar. A slow + // collapse settles the bounds while the player still reports itself as + // sliding, so nothing else sets them again. + try { + cancelMorph(); + + if (!lastBounds.isEmpty()) { + MiniplayerBoundsController controller = boundsControllerRef.get(); + if (controller == null) { + Logger.printDebug(() -> "No miniplayer bounds controller"); + } else { + morphFrom.set(currentBounds.isEmpty() ? lastBounds : currentBounds); + barBoundsFor(lastBounds); + morphTo.set(barBounds); + barShapeApplied = true; + showControls(true); + + if (morphFrom.equals(morphTo)) { + setBounds(controller, morphTo); + updateVideoClip(); + } else { + setContentAlpha(0f); + runMorph(true, () -> setContentAlpha(1f)); + } + } + } + } catch (Exception ex) { + morphing = false; + Logger.printException(() -> "Apply bar shape failure", ex); + } + } else { + barShapeApplied = false; + + // A dismissal is the bar sliding itself away, anything else grows the + // player back and has to be uncovered, or it stays clipped. + if (type != PlayerType.WATCH_WHILE_SLIDING_MINIMIZED_DISMISSED) { + cancelMorph(); + } + + if (!morphing) { + // Done here as well, YouTube does not always run its own pass in time. + showControls(false); + } + } + + updateVideoClip(); + }); + return Unit.INSTANCE; + }); + } - installListeners(); - updateBar(); + final boolean minimized = PlayerType.getCurrent() == PlayerType.WATCH_WHILE_MINIMIZED; + + updateVideoClip(); + startTicking(minimized); + refreshContents(); + showControls(minimized); + if (minimized && !morphing) { + setContentAlpha(1f); + } Logger.printDebug(() -> "Legacy miniplayer controls attached"); } catch (Exception ex) { @@ -338,6 +452,10 @@ public static int getLegacyControlsVisibility(int original) { public static Rect getMiniplayerBounds(int left, int top, int right, int bottom) { originalBounds.set(left, top, right, bottom); + if (!ENABLED) { + return originalBounds; + } + return getMinimalBarBounds(originalBounds); } @@ -349,7 +467,27 @@ public static Rect getMiniplayerBounds(int left, int top, int right, int bottom) public static Rect getMinimalBarBounds(Rect original) { try { if (!ENABLED) { - return original; + // Keeps a miniplayer other than the bar above the keyboard and the system + // navigation buttons, which YouTube rests it behind when the layout that holds it + // is not shrunk to leave them out. + if (applyingBounds) { + // Ours, already moved. + return original; + } + + if (PlayerType.getCurrent() != PlayerType.WATCH_WHILE_MINIMIZED) { + // Growing into the full player, which is laid out behind them on purpose. + restingBounds.setEmpty(); + return original; + } + + View playerLayout = otherPlayerLayout(); + if (playerLayout != null) { + listenToWindowLayout(playerLayout); + } + + restingBounds.set(original); + return aboveWindowBottom(original, playerLayout); } if (applyingBounds) { @@ -365,8 +503,11 @@ public static Rect getMinimalBarBounds(Rect original) { return currentBounds; } - Rect docked = fullWidthSpan(original); - lastBounds.set(docked); + // Prevents the video from anchoring into one of the display corners by spanning the + // bounds to full display width, making the transition to miniplayer smoother. + updateHorizontalSpan(); + dockedBounds.set(horizontalSpan.left, original.top, horizontalSpan.right, original.bottom); + lastBounds.set(dockedBounds); PlayerType getCurrent = PlayerType.getCurrent(); if (getCurrent == PlayerType.WATCH_WHILE_SLIDING_MINIMIZED_MAXIMIZED) { barBoundsFor(dockedBounds); @@ -391,7 +532,7 @@ public static Rect getMinimalBarBounds(Rect original) { // ends, so it is the bar for the new window, as it would be had nothing changed. final boolean minimized = getCurrent == PlayerType.WATCH_WHILE_MINIMIZED; if (minimized || getCurrent.isMaximizedOrFullscreen()) { - barBoundsFor(docked); + barBoundsFor(dockedBounds); currentBounds.set(barBounds); if (minimized) { barShapeApplied = true; @@ -399,8 +540,8 @@ public static Rect getMinimalBarBounds(Rect original) { return barBounds; } - currentBounds.set(docked); - return docked; + currentBounds.set(dockedBounds); + return dockedBounds; } catch (Exception ex) { Logger.printException(() -> "getMinimalBarBounds failure", ex); } @@ -408,19 +549,47 @@ public static Rect getMinimalBarBounds(Rect original) { return original; } - /** - * Prevents the video from anchoring into one of the display corners by spanning the - * bounds to full display width, making the transition to miniplayer smoother. - */ - private static Rect fullWidthSpan(Rect original) { - updateHorizontalSpan(); - dockedBounds.set(horizontalSpan.left, original.top, horizontalSpan.right, original.bottom); + private static void barBoundsFor(Rect resting) { + View playerLayout = barPlayerLayout(); - return dockedBounds; - } + // A floating miniplayer keeps a margin to the navigation bar, a docked bar sits against it. + // While the navigation bar is away, YouTube's own resting edge is already the right one. + int dockedBottom = resting.bottom; - private static void barBoundsFor(Rect resting) { - final int bottom = barBottomFor(resting) - BAR_MARGIN; + View navigationBar = navigationBarRef.get(); + if (navigationBar == null) { + navigationBar = findInWindow("bottom_bar_container"); + if (navigationBar != null) { + navigationBarRef = new WeakReference<>(navigationBar); + navigationBar.removeOnLayoutChangeListener(navigationBarLayoutListener); + navigationBar.addOnLayoutChangeListener(navigationBarLayoutListener); + } + } + + // A rail beside the content says nothing about where the bar ends. + if (navigationBar != null && navigationBar.isShown() + && navigationBar.getWidth() >= navigationBar.getHeight()) { + navigationBar.getLocationInWindow(windowLocation); + + // Where it rests, not where it is. YouTube slides it away while the feed scrolls, and + // a position taken mid-slide leaves the bar underneath it once it comes back. + final int top = windowLocation[1] - Math.round(navigationBar.getTranslationY()); + + // The bounds are relative to the layout that holds the player, not to the window. That + // layout can start below the status bar, which would otherwise move the bar down by its + // height and put it under the navigation bar. + int origin = 0; + if (playerLayout != null) { + playerLayout.getLocationInWindow(windowLocation); + origin = windowLocation[1]; + } + + // Docked against it even when YouTube rests lower. After a recreated activity its + // resting bounds are still the ones from before the system insets moved everything up. + dockedBottom = top - origin; + } + + final int bottom = Math.min(dockedBottom, windowBottomFor(playerLayout)) - BAR_MARGIN; updateHorizontalSpan(); barBounds.set(horizontalSpan.left + BAR_MARGIN, bottom - BAR_HEIGHT, @@ -434,89 +603,86 @@ private static void barBoundsFor(Rect resting) { * so a bar as wide as the display is cut off there. The window also differs from the display * in split screen and on foldables. */ + @SuppressWarnings("deprecation") private static void updateHorizontalSpan() { horizontalSpan.set(0, 0, Dim.getMetrics().widthPixels, 0); try { - spanWindow(); - } catch (Exception ex) { - Logger.printException(() -> "updateHorizontalSpan failure", ex); - } - } - - @SuppressWarnings("deprecation") - private static void spanWindow() { - View watchPlayer = watchPlayer(); - if (watchPlayer == null || !(watchPlayer.getParent() instanceof View parent)) return; - - final int parentWidth = parent.getWidth(); - View root = parent.getRootView(); - WindowInsets insets = parent.getRootWindowInsets(); - if (parentWidth <= 0 || root == null || insets == null) return; - - int insetLeft; - int insetRight; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - Insets sideInsets = insets.getInsets( - WindowInsets.Type.systemBars() | WindowInsets.Type.displayCutout()); - insetLeft = sideInsets.left; - insetRight = sideInsets.right; - } else { - insetLeft = insets.getSystemWindowInsetLeft(); - insetRight = insets.getSystemWindowInsetRight(); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - DisplayCutout cutout = insets.getDisplayCutout(); - if (cutout != null) { - insetLeft = Math.max(insetLeft, cutout.getSafeInsetLeft()); - insetRight = Math.max(insetRight, cutout.getSafeInsetRight()); + View watchPlayer = watchPlayer(); + if (watchPlayer == null || !(watchPlayer.getParent() instanceof View parent)) return; + + final int parentWidth = parent.getWidth(); + View root = parent.getRootView(); + WindowInsets insets = parent.getRootWindowInsets(); + if (parentWidth <= 0 || root == null || insets == null) return; + + int insetLeft; + int insetRight; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + Insets sideInsets = insets.getInsets( + WindowInsets.Type.systemBars() | WindowInsets.Type.displayCutout()); + insetLeft = sideInsets.left; + insetRight = sideInsets.right; + } else { + insetLeft = insets.getSystemWindowInsetLeft(); + insetRight = insets.getSystemWindowInsetRight(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + DisplayCutout cutout = insets.getDisplayCutout(); + if (cutout != null) { + insetLeft = Math.max(insetLeft, cutout.getSafeInsetLeft()); + insetRight = Math.max(insetRight, cutout.getSafeInsetRight()); + } } } - } - // The insets are relative to the window, the bounds to the layout that holds the player. - parent.getLocationInWindow(windowLocation); - final int left = Math.max(0, insetLeft - windowLocation[0]); - final int right = Math.min(parentWidth, root.getWidth() - insetRight - windowLocation[0]); + // The insets are relative to the window, the bounds to the layout that holds the player. + parent.getLocationInWindow(windowLocation); + final int left = Math.max(0, insetLeft - windowLocation[0]); + final int right = Math.min(parentWidth, root.getWidth() - insetRight - windowLocation[0]); - if (right - left > 2 * BAR_MARGIN) { - horizontalSpan.set(left, 0, right, 0); + if (right - left > 2 * BAR_MARGIN) { + horizontalSpan.set(left, 0, right, 0); + } + } catch (Exception ex) { + Logger.printException(() -> "updateHorizontalSpan failure", ex); } } /** - * A floating miniplayer keeps a margin to the navigation bar, a docked bar sits against it. - * While the navigation bar is away, YouTube's own resting edge is already the right one. + * The lowest a miniplayer can rest in the layout that holds it, above the keyboard and the + * system navigation buttons. That layout is no longer shrunk to leave them out when the + * Navigation bar patch removes the keyboard margin of the page, or draws the window behind + * the system navigation buttons for the translucent navigation bar, and YouTube then rests + * the player behind them. Measured against them directly, so nothing changes when the layout + * already ends above them. + * + * @param playerLayout The layout that holds the player, which the bounds are relative to. */ - private static int barBottomFor(Rect resting) { - View navigationBar = navigationBar(); - if (navigationBar == null || !navigationBar.isShown()) return resting.bottom; - - navigationBar.getLocationInWindow(windowLocation); + @SuppressWarnings("deprecation") + private static int windowBottomFor(@Nullable View playerLayout) { + if (playerLayout == null) return Integer.MAX_VALUE; - // Where it rests, not where it is. YouTube slides it away while the feed scrolls, and - // a position taken mid-slide leaves the bar underneath it once it comes back. - final int top = windowLocation[1] - Math.round(navigationBar.getTranslationY()); + final boolean keyboardMarginRemoved = NavigationBarPatch.isKeyboardMarginRemoved(); + final boolean translucentNavigationBar = NavigationBarPatch.isTranslucentNavigationBarForced(); + if (!keyboardMarginRemoved && !translucentNavigationBar) return Integer.MAX_VALUE; - // A rail beside the content says nothing about where the bar ends. - if (navigationBar.getWidth() < navigationBar.getHeight()) return resting.bottom; + WindowInsets insets = playerLayout.getRootWindowInsets(); + if (insets == null) return Integer.MAX_VALUE; - // Docked against it even when YouTube rests lower. After a recreated activity its - // resting bounds are still the ones from before the system insets moved everything up. - return top - boundsOriginInWindow(); - } - - /** - * The bounds are relative to the layout that holds the player, not to the window. That - * layout can start below the status bar, which would otherwise move the bar down by its - * height and put it under the navigation bar. - */ - private static int boundsOriginInWindow() { - View watchPlayer = watchPlayer(); - if (watchPlayer == null || !(watchPlayer.getParent() instanceof View parent)) return 0; + final int insetBottom; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + insetBottom = insets.getInsets((keyboardMarginRemoved ? WindowInsets.Type.ime() : 0) + | (translucentNavigationBar ? WindowInsets.Type.navigationBars() : 0)).bottom; + } else { + // The translucent navigation bar is only used from Android 12, so this is the keyboard. + insetBottom = insets.getSystemWindowInsetBottom(); + } + if (insetBottom <= 0) return Integer.MAX_VALUE; - parent.getLocationInWindow(windowLocation); + // The insets are relative to the window, the bounds to the layout that holds the player. + playerLayout.getLocationInWindow(windowLocation); - return windowLocation[1]; + return playerLayout.getRootView().getHeight() - insetBottom - windowLocation[1]; } /** @@ -574,45 +740,8 @@ private static boolean inBarMode() { * Injection point. */ public static void setBoundsController(MiniplayerBoundsController controller) { - if (ENABLED) { - boundsControllerRef = new WeakReference<>(controller); - } - } - - /** - * Animates whatever shape the player was left in into the bar. A slow collapse settles the - * bounds while the player still reports itself as sliding, so nothing else sets them again. - */ - private static void applyBarShape() { - try { - cancelMorph(); - - if (lastBounds.isEmpty()) return; - - MiniplayerBoundsController controller = boundsControllerRef.get(); - if (controller == null) { - Logger.printDebug(() -> "No miniplayer bounds controller"); - return; - } - - morphFrom.set(currentBounds.isEmpty() ? lastBounds : currentBounds); - barBoundsFor(lastBounds); - morphTo.set(barBounds); - barShapeApplied = true; - showControls(true); - - if (morphFrom.equals(morphTo)) { - setBounds(controller, morphTo); - updateVideoClip(); - return; - } - - setContentAlpha(0f); - runMorph(true, () -> setContentAlpha(1f)); - } catch (Exception ex) { - morphing = false; - Logger.printException(() -> "applyBarShape failure", ex); - } + // Also kept for the other miniplayer types, to move them above the keyboard. + boundsControllerRef = new WeakReference<>(controller); } private static void setBounds(MiniplayerBoundsController controller, Rect bounds) { @@ -632,8 +761,29 @@ private static void runMorph(boolean fadeInContent, Runnable onEnd) { ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f); animator.setDuration(MORPH_MILLIS); animator.setInterpolator(new DecelerateInterpolator()); - animator.addUpdateListener(value -> - onMorphFrame((float) value.getAnimatedValue(), fadeInContent)); + animator.addUpdateListener(value -> { + MiniplayerBoundsController controller = boundsControllerRef.get(); + if (controller == null) { + cancelMorph(); + return; + } + + final float fraction = (float) value.getAnimatedValue(); + morphCurrent.set( + interpolate(morphFrom.left, morphTo.left, fraction), + interpolate(morphFrom.top, morphTo.top, fraction), + interpolate(morphFrom.right, morphTo.right, fraction), + interpolate(morphFrom.bottom, morphTo.bottom, fraction) + ); + + setBounds(controller, morphCurrent); + updateVideoClip(); + + if (fadeInContent) { + // The text would otherwise sit on top of the video, still wide at this point. + setContentAlpha(Math.max(0f, (fraction - 0.4f) / 0.6f)); + } + }); animator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { @@ -649,29 +799,6 @@ public void onAnimationEnd(Animator animation) { animator.start(); } - private static void onMorphFrame(float fraction, boolean fadeInContent) { - MiniplayerBoundsController controller = boundsControllerRef.get(); - if (controller == null) { - cancelMorph(); - return; - } - - morphCurrent.set( - interpolate(morphFrom.left, morphTo.left, fraction), - interpolate(morphFrom.top, morphTo.top, fraction), - interpolate(morphFrom.right, morphTo.right, fraction), - interpolate(morphFrom.bottom, morphTo.bottom, fraction) - ); - - setBounds(controller, morphCurrent); - updateVideoClip(); - - if (fadeInContent) { - // The text would otherwise sit on top of the video, still wide at this point. - setContentAlpha(Math.max(0f, (fraction - 0.4f) / 0.6f)); - } - } - private static void cancelMorph() { ValueAnimator animator = morphAnimator; morphAnimator = null; @@ -725,52 +852,6 @@ static void setModernOverlayButton(View button) { } } - /** - * YouTube reads a gesture against the shape of its own miniplayer, which the bar is not, so - * the bar answers for itself: a tap or a drag up expands, a drag down closes. - */ - @SuppressLint("ClickableViewAccessibility") - private static void setBarGestures(ViewGroup controlsLayout) { - final int slop = Dim.dp20; - - controlsLayout.setClickable(true); - controlsLayout.setContentDescription(ACCESSIBILITY_MINIPLAYER_VIEW_STRING); - controlsLayout.setOnClickListener(v -> expandPlayer()); - - controlsLayout.setOnTouchListener(new View.OnTouchListener() { - float downX; - float downY; - - @Override - public boolean onTouch(View view, MotionEvent event) { - switch (event.getActionMasked()) { - case MotionEvent.ACTION_DOWN: - downX = event.getRawX(); - downY = event.getRawY(); - keepTouchFromParent(view); - return true; - - case MotionEvent.ACTION_UP: - final float draggedUp = downY - event.getRawY(); - - if (draggedUp > slop) { - expandPlayer(); - } else if (-draggedUp > slop) { - closeBar(); - } else if (Math.abs(event.getRawX() - downX) < slop) { - // Goes through the click, so accessibility services see it. - view.performClick(); - } - return true; - - default: - // Held for the whole gesture, the watch layout reads the moves as a pan. - return true; - } - } - }); - } - /** * The close button hides the miniplayer outright, so the bar sees itself out first. */ @@ -830,105 +911,123 @@ private static boolean clickModernButton(WeakReference buttonRef, String d return button.performClick(); } - private static void installListeners() { - if (listenersInstalled) return; - listenersInstalled = true; - - PlayerType.getOnChange().addObserver((PlayerType type) -> { - final boolean minimized = type == PlayerType.WATCH_WHILE_MINIMIZED; - - Utils.runOnMainThreadNowOrLater(() -> { - startTicking(minimized); - - if (minimized) { - refreshContents(); - applyBarShape(); - } else { - barShapeApplied = false; + /** + * For the views outside the bar layout, which the bar only reaches through the window. + */ + @Nullable + private static View findInWindow(String name) { + ViewGroup controls = controlsRef.get(); + if (controls == null) return null; - // A dismissal is the bar sliding itself away, anything else grows the - // player back and has to be uncovered, or it stays clipped. - if (type != PlayerType.WATCH_WHILE_SLIDING_MINIMIZED_DISMISSED) { - cancelMorph(); - } + return Utils.getChildViewByResourceName(controls.getRootView(), name); + } - if (!morphing) { - // Done here as well, YouTube does not always run its own pass in time. - showControls(false); - } + /** + * Without the translucent navigation bar, a recreated activity lays the navigation bar out + * before the system insets move it up, and a bar placed against that stays underneath it. + */ + private static final View.OnLayoutChangeListener navigationBarLayoutListener = + (view, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> { + if (top != oldTop || bottom != oldBottom) { + // Not from inside the layout pass the change is reported from. + Utils.runOnMainThread(MinimalMiniplayerPatch::reapplyBarBounds); } + }; - updateVideoClip(); - }); - return Unit.INSTANCE; - }); - } + /** + * The window bottom the miniplayer was last placed against. + */ + private static int lastWindowBottom = Integer.MAX_VALUE; - private static void updateBar() { - final boolean minimized = PlayerType.getCurrent() == PlayerType.WATCH_WHILE_MINIMIZED; + private static WeakReference windowObserverRef = new WeakReference<>(null); - updateVideoClip(); - startTicking(minimized); - refreshContents(); - showControls(minimized); - if (minimized && !morphing) { - setContentAlpha(1f); - } - } + /** + * The bounds YouTube last gave a miniplayer other than the bar, while minimized. + */ + private static final Rect restingBounds = new Rect(); + + private static WeakReference otherWatchPlayerRef = new WeakReference<>(null); /** - * The skip button is in the player overlay, so an ad cannot be skipped while it is clipped. + * YouTube asks for the bounds when the navigation bar goes away for the keyboard, which is + * before the keyboard is reported as shown, and does not ask again once it is. */ - private static boolean isSkipAdShown() { - View skipAd = skipAdRef.get(); - if (skipAd == null) { - skipAd = findInWindow("modern_miniplayer_skip_ad_button"); - if (skipAd == null) return false; + private static final ViewTreeObserver.OnGlobalLayoutListener windowLayoutListener = () -> { + final int windowBottom = windowBottomFor(ENABLED ? barPlayerLayout() : otherPlayerLayout()); + if (windowBottom == lastWindowBottom) return; + lastWindowBottom = windowBottom; - skipAdRef = new WeakReference<>(skipAd); + // Not from inside the layout pass the change is reported from. + if (ENABLED) { + Utils.runOnMainThread(MinimalMiniplayerPatch::reapplyBarBounds); + return; } - return skipAd.isShown(); + Utils.runOnMainThread(() -> { + if (restingBounds.isEmpty() || PlayerType.getCurrent() != PlayerType.WATCH_WHILE_MINIMIZED) { + return; + } + + MiniplayerBoundsController controller = boundsControllerRef.get(); + if (controller == null) return; + + applyingBounds = true; + try { + controller.patch_setBounds(aboveWindowBottom(new Rect(restingBounds), otherPlayerLayout())); + } finally { + applyingBounds = false; + } + }); + }; + + private static void listenToWindowLayout(View view) { + ViewTreeObserver observer = view.getViewTreeObserver(); + if (observer == windowObserverRef.get()) return; + windowObserverRef = new WeakReference<>(observer); + + observer.removeOnGlobalLayoutListener(windowLayoutListener); + observer.addOnGlobalLayoutListener(windowLayoutListener); } /** - * For the views outside the bar layout, which the bar only reaches through the window. + * A new rect when moved, as YouTube can keep the one it gets back. */ - @Nullable - private static View findInWindow(String name) { - ViewGroup controls = controlsRef.get(); - if (controls == null) return null; + private static Rect aboveWindowBottom(Rect bounds, @Nullable View playerLayout) { + // The margin YouTube itself keeps around its floating miniplayer. + final int lowestBottom = windowBottomFor(playerLayout) - BAR_MARGIN; + if (bounds.bottom <= lowestBottom) return bounds; + + Rect moved = new Rect(bounds); + moved.offset(0, lowestBottom - bounds.bottom); + return moved; + } - return Utils.getChildViewByResourceName(controls.getRootView(), name); + @Nullable + private static View barPlayerLayout() { + View watchPlayer = watchPlayer(); + return watchPlayer != null && watchPlayer.getParent() instanceof View parent ? parent : null; } + /** + * The other miniplayer types have no bar layout to reach the window through. + */ @Nullable - private static View navigationBar() { - View navigationBar = navigationBarRef.get(); - if (navigationBar != null) return navigationBar; + private static View otherPlayerLayout() { + View watchPlayer = otherWatchPlayerRef.get(); + // A recreated activity brings its own. + if (watchPlayer == null || !watchPlayer.isAttachedToWindow()) { + Activity activity = Utils.getActivity(); + if (activity == null) return null; - navigationBar = findInWindow("bottom_bar_container"); - if (navigationBar != null) { - navigationBarRef = new WeakReference<>(navigationBar); - navigationBar.removeOnLayoutChangeListener(navigationBarLayoutListener); - navigationBar.addOnLayoutChangeListener(navigationBarLayoutListener); + watchPlayer = Utils.getChildViewByResourceName(activity.getWindow().getDecorView(), "watch_player"); + if (watchPlayer == null) return null; + + otherWatchPlayerRef = new WeakReference<>(watchPlayer); } - return navigationBar; + return watchPlayer.getParent() instanceof View parent ? parent : null; } - /** - * Without the translucent navigation bar, a recreated activity lays the navigation bar out - * before the system insets move it up, and a bar placed against that stays underneath it. - */ - private static final View.OnLayoutChangeListener navigationBarLayoutListener = - (view, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> { - if (top != oldTop || bottom != oldBottom) { - // Not from inside the layout pass the change is reported from. - Utils.runOnMainThread(MinimalMiniplayerPatch::reapplyBarBounds); - } - }; - private static void reapplyBarBounds() { if (morphing || lastBounds.isEmpty()) return; @@ -973,12 +1072,9 @@ private static void tick() { * Called by YouTube whenever it changes the miniplayer action button icon. */ public static void setPlaybackIcon(int contentDescriptionId) { - if (ENABLED) { - setPlaying(contentDescriptionId == PAUSE_DESCRIPTION); - } - } + if (!ENABLED) return; - private static void setPlaying(boolean isPlaying) { + final boolean isPlaying = contentDescriptionId == PAUSE_DESCRIPTION; if (playing == isPlaying) return; playing = isPlaying; @@ -1009,7 +1105,40 @@ private static void updateVideoClip() { // Not from showControls(), which YouTube bypasses when it puts the bar up itself. // Called for every bounds change and tick, and does nothing once the order is set. if (getCurrentMiniplayerType() == MINIMAL_BAR_2) { - setBarDrawsOverPlayer(inBarMode()); + // Type 2 draws the bar on the video, and the player holds a SurfaceView that punches + // a hole through whatever was drawn before it, so the bar has to come after the player + // in the watch layout. Elevation is no use, this layout draws its children through + // its own drawChild and does not order them by it. + // The order is put back when the bar goes away. It also decides who gets a touch first, + // and a bar left in front swallows the taps that should reach the expanded player. + final boolean over = inBarMode(); + if (barDrawsOverPlayer != over) { + View barContainer = barContainerRef.get(); + if (barContainer != null && barContainer.getParent() instanceof ViewGroup parent) { + if (over) { + barIndexInParent = parent.indexOfChild(barContainer); + + originalBarBackground = barContainer.getBackground(); + // The video is the background, the controls on top of it already dim it. + barContainer.setBackground(null); + parent.bringChildToFront(barContainer); + } else { + barContainer.setBackground(originalBarBackground); + originalBarBackground = null; + + final int childCount = parent.getChildCount(); + if (parent.indexOfChild(barContainer) == childCount - 1) { + // Moving everything the bar jumped over back to the front, in order, + // leaves the bar itself at the index it started from. + for (int i = barIndexInParent; i < childCount - 1; i++) { + parent.bringChildToFront(parent.getChildAt(barIndexInParent)); + } + } + } + } + + barDrawsOverPlayer = over; + } } final int height = currentBounds.height(); @@ -1021,7 +1150,20 @@ private static void updateVideoClip() { // Only a bar is far wider than the video it holds. Going by the state instead would // uncover the video for as long as the player still holds the bar shape. Ads are // uncovered as well, their skip button is part of the overlay this hides. - if (!inBarMode() || height <= 0 || width <= videoWidth || isSkipAdShown()) { + boolean uncover = !inBarMode() || height <= 0 || width <= videoWidth; + if (!uncover) { + View skipAd = skipAdRef.get(); + if (skipAd == null) { + skipAd = findInWindow("modern_miniplayer_skip_ad_button"); + if (skipAd != null) { + skipAdRef = new WeakReference<>(skipAd); + } + } + + uncover = skipAd != null && skipAd.isShown(); + } + + if (uncover) { watchPlayer.setClipBounds(null); return; } @@ -1045,45 +1187,6 @@ private static void updateVideoClip() { watchPlayer.setClipBounds(clipBounds); } - /** - * Type 2 draws the bar on the video, and the player holds a {@code SurfaceView} that punches - * a hole through whatever was drawn before it, so the bar has to come after the player in the - * watch layout. Elevation is no use, this layout draws its children through its own - * {@code drawChild} and does not order them by it. - *

- * The order is put back when the bar goes away. It also decides who gets a touch first, and - * a bar left in front swallows the taps that should reach the expanded player. - */ - private static void setBarDrawsOverPlayer(boolean over) { - if (barDrawsOverPlayer == over) return; - - View barContainer = barContainerRef.get(); - if (barContainer != null && barContainer.getParent() instanceof ViewGroup parent) { - if (over) { - barIndexInParent = parent.indexOfChild(barContainer); - - originalBarBackground = barContainer.getBackground(); - // The video is the background, the controls on top of it already dim it. - barContainer.setBackground(null); - parent.bringChildToFront(barContainer); - } else { - barContainer.setBackground(originalBarBackground); - originalBarBackground = null; - - final int childCount = parent.getChildCount(); - if (parent.indexOfChild(barContainer) == childCount - 1) { - // Moving everything the bar jumped over back to the front, in order, leaves - // the bar itself at the index it started from. - for (int i = barIndexInParent; i < childCount - 1; i++) { - parent.bringChildToFront(parent.getChildAt(barIndexInParent)); - } - } - } - } - - barDrawsOverPlayer = over; - } - @Nullable private static View watchPlayer() { View watchPlayer = watchPlayerRef.get(); @@ -1155,7 +1258,27 @@ private static void updatePlayPauseIcon(boolean morph) { ImageView playPause = playPauseRef.get(); if (playPause == null) return; - if (morph && startIconMorph(playPause)) return; + if (morph) { + // The app ships the drawable its own player uses to morph between the two icons. + final String name = playing + ? "player_play_pause_vector_transition" + : "player_pause_play_vector_transition"; + + Drawable drawable = getDrawable(playPause, name + "_delhi", name); + if (drawable instanceof AnimatedVectorDrawable iconMorph) { + // A player icon style swaps its own icons instead, and starts that on its first draw. + if (playing) { + PlayPauseIcons.setPlayToPause(playPause, iconMorph); + } else { + PlayPauseIcons.setPauseToPlay(playPause, iconMorph); + } + if (playPause.getDrawable() == iconMorph) { + iconMorph.start(); + } + + return; + } + } if (playing) { setIcon(playPause, PlayPauseIcons.PAUSE, @@ -1203,31 +1326,6 @@ private static void styleButton(View button) { ViewAnimations.applyPressEffect(button); } - /** - * The app ships the drawable its own player uses to morph between the two icons. - */ - private static boolean startIconMorph(ImageView view) { - final String name = playing - ? "player_play_pause_vector_transition" - : "player_pause_play_vector_transition"; - - Drawable drawable = getDrawable(view, name + "_delhi", name); - - if (!(drawable instanceof AnimatedVectorDrawable morph)) return false; - - // A player icon style swaps its own icons instead, and starts that on its first draw. - if (playing) { - PlayPauseIcons.setPlayToPause(view, morph); - } else { - PlayPauseIcons.setPauseToPlay(view, morph); - } - if (view.getDrawable() == morph) { - morph.start(); - } - - return true; - } - private static void setOverlayTextColor(@Nullable TextView view, int color) { if (view != null) { view.setTextColor(color); diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/NavigationBarPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/NavigationBarPatch.java index 2da094c67e..3d07bcb136 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/NavigationBarPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/NavigationBarPatch.java @@ -228,21 +228,54 @@ public static void hideNavigationBar(View view) { * (https://github.com/MorpheApp/morphe-patches/issues/3864). */ public static boolean useTranslucentNavigationBar(boolean original) { + boolean translucent = original; try { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { - return false; - } - - Boolean edgeToEdge = HideStatusBarPatch.isEdgeToEdgeTopBar(); - if (edgeToEdge == null) { - // Not read yet, or the Hide status bar patch is not included. - return original; + translucent = false; + } else { + Boolean edgeToEdge = HideStatusBarPatch.isEdgeToEdgeTopBar(); + // Null if not read yet, or the Hide status bar patch is not included. + if (edgeToEdge != null) { + translucent = edgeToEdge; + } } - return edgeToEdge; } catch (Exception ex) { Logger.printException(() -> "useTranslucentNavigationBar failure", ex); - return original; } + + translucentNavigationBarForced = translucent; + return translucent; + } + + /** + * If {@link #useTranslucentNavigationBar(boolean)} turned the translucent navigation bar on. + * The fixes for what its feature flags break are only needed then, and stay off if the flags + * are no longer turned on by this patch. + */ + private static volatile boolean translucentNavigationBarForced; + + /** + * Injection point. + */ + public static boolean isTranslucentNavigationBarForced() { + return translucentNavigationBarForced; + } + + /** + * If the keyboard height was ever removed from the bottom margin of the layout that holds + * the page, the player and the navigation bar, so the keyboard covers them. + */ + private static volatile boolean keyboardMarginRemoved; + + /** + * Injection point. + */ + public static void setKeyboardMarginRemoved() { + keyboardMarginRemoved = true; + } + + static boolean isKeyboardMarginRemoved() { + return keyboardMarginRemoved; } /** @@ -253,10 +286,12 @@ public static boolean useTranslucentNavigationBar(boolean original) { * the keyboard, so the bar grows as tall as the keyboard and covers the page * (https://github.com/MorpheApp/morphe-patches/issues/3909). The page above then ends * the height of the system bars below the top of the keyboard, so only that much room is left. + * Only with the feature flags turned on by {@link #useTranslucentNavigationBar(boolean)}. */ public static int getNavigationBarBottomPadding(View navigationBar, int original) { try { - if (original != 0 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + if (translucentNavigationBarForced && original != 0 + && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { WindowInsets insets = navigationBar.getRootWindowInsets(); if (insets != null && insets.isVisible(WindowInsets.Type.ime())) { return Math.min(original, insets.getInsets(WindowInsets.Type.systemBars()).bottom); diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/buttons/navigation/Fingerprints.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/buttons/navigation/Fingerprints.kt index f79fce116e..88417e3598 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/buttons/navigation/Fingerprints.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/buttons/navigation/Fingerprints.kt @@ -207,6 +207,32 @@ internal object FeedListPaddingFingerprint : Fingerprint( ) ) +/** + * Sets the bottom padding of the list of a browse page laid out behind the navigation bar, + * to the navigation bar height plus the bottom window inset, once the window insets are known. + */ +internal object BrowseListBottomPaddingFingerprint : Fingerprint( + returnType = "V", + parameters = listOf("Ljava/lang/Object;"), + filters = listOf( + methodCall( + definingClass = "Landroid/support/v7/widget/RecyclerView;", + name = "getPaddingRight" + ), + fieldAccess( + opcode = Opcode.IGET, + definingClass = "Landroid/graphics/Rect;", + name = "bottom", + location = MatchAfterWithin(6) + ), + methodCall( + definingClass = "Landroid/support/v7/widget/RecyclerView;", + name = "setPadding", + location = MatchAfterWithin(3) + ) + ) +) + /** * Draws the blurred background of the translucent navigation bar. If the canvas is hardware * accelerated, a copy of the watch layout, with the player, is drawn into a blurred render node, diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/buttons/navigation/NavigationBarPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/buttons/navigation/NavigationBarPatch.kt index c119494944..b480dba329 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/buttons/navigation/NavigationBarPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/buttons/navigation/NavigationBarPatch.kt @@ -207,6 +207,12 @@ val navigationBarPatch = bytecodePatch( // Hide navigation bar addBottomBarContainerHook("$EXTENSION_CLASS->hideNavigationBar(Landroid/view/View;)V") + // region Translucent navigation bar + + // Turning on the feature flags of the translucent navigation bar, and the fixes for what + // they break. Every fix checks that the flags are turned on by this patch, so the fixes do + // nothing if the flags are left to the account. + // The translucent navigation bar comes from three feature flags that not every account // gets, so all of them are turned on whenever the window is drawn edge to edge. The bar then // looks the same on every account, and the opaque option below paints over the same @@ -240,6 +246,9 @@ val navigationBarPatch = bytecodePatch( addInstructionsWithLabels( index + 1, $$""" + invoke-static { }, $$EXTENSION_CLASS->isTranslucentNavigationBarForced()Z + move-result v$$sdkRegister + if-eqz v$$sdkRegister, :draw_blur sget v$$sdkRegister, Landroid/os/Build$VERSION;->SDK_INT:I const/16 v$$minSdkRegister, 0x22 if-ge v$$sdkRegister, v$$minSdkRegister, :draw_blur @@ -251,41 +260,6 @@ val navigationBarPatch = bytecodePatch( } } - // With the translucent navigation bar, the navigation bar makes room for the system - // navigation buttons. If the navigation bar is hidden in the Shorts player, nothing makes - // that room, so the Shorts player makes it itself. The first parameter is the layout - // of the Shorts player fragment. - ShortsBottomBarContainerFingerprint.method.apply { - addInstruction( - 0, - "invoke-static { p1 }, $EXTENSION_CLASS->setShortsPlayerBottomInset(Landroid/view/View;)V" - ) - - // The height of the navigation bar is passed to the object that reserves that room below - // the Shorts player, right after the view. Other patches can change the height after - // it's read, so the room is replaced where it's passed. - val getHeightIndex = indexOfFirstInstructionOrThrow { - getReference()?.name == "getHeight" - } - val reserveRoomIndex = indexOfFirstInstructionOrThrow(getHeightIndex, Opcode.INVOKE_DIRECT_RANGE) - val reserveRoomInstruction = getInstruction(reserveRoomIndex) - val parameterTypes = reserveRoomInstruction.getReference()!! - .parameterTypes.map { it.toString() } - val heightParameterIndex = parameterTypes.indices.first { index -> - parameterTypes[index] == "Landroid/view/View;" && parameterTypes.getOrNull(index + 1) == "I" - } + 1 - // The first register is the instance. - val heightRegister = reserveRoomInstruction.startRegister + 1 + heightParameterIndex - - addInstructions( - reserveRoomIndex, - """ - invoke-static/range { v$heightRegister .. v$heightRegister }, $EXTENSION_CLASS->getShortsPlayerBottomRoom(I)I - move-result v$heightRegister - """ - ) - } - // With the keyboard open, the bottom inset also holds the keyboard, so the bar covering // the system navigation buttons grows as tall as the keyboard and covers the page. // The padding is first stored in a field of the bar, and the same register is then passed @@ -323,12 +297,17 @@ val navigationBarPatch = bytecodePatch( (this as BuilderInstruction21t).target.location.index == insetIndex } val keyboardRegister = getInstruction(keyboardBranchIndex).registerA + val freeRegister = getFreeRegisterProvider(insetIndex + 1, 1).getFreeRegister4Bit() addInstructionsAtControlFlowLabel( insetIndex + 1, """ if-eqz v$keyboardRegister, :keyboard_hidden + invoke-static { }, $EXTENSION_CLASS->isTranslucentNavigationBarForced()Z + move-result v$freeRegister + if-eqz v$freeRegister, :keyboard_hidden const/4 v$marginRegister, 0x0 + invoke-static { }, $EXTENSION_CLASS->setKeyboardMarginRemoved()V :keyboard_hidden nop """ @@ -358,6 +337,9 @@ val navigationBarPatch = bytecodePatch( addInstructionsWithLabels( index, $$""" + invoke-static { }, $$EXTENSION_CLASS->isTranslucentNavigationBarForced()Z + move-result v$$imeRegister + if-eqz v$$imeRegister, :keep_padding # The inset types are only on Android 11 and later. sget v$$imeRegister, Landroid/os/Build$VERSION;->SDK_INT:I const/16 v$$systemBarsRegister, 0x1e @@ -390,6 +372,69 @@ val navigationBarPatch = bytecodePatch( } } + // When a browse page is laid out behind the navigation bar, its list is set to draw into + // its padding, and the padding is set later, once the window insets are known. If the list + // is refreshed in between, as when coming back from the Shorts player, it's set to clip to + // its padding again, so the bottom padding is left as an empty band between the feed and + // the navigation bar. The list is set to draw into its padding again with the padding. + BrowseListBottomPaddingFingerprint.let { + it.method.apply { + val index = it.instructionMatches.last().index + val listRegister = getInstruction(index).registerC + val freeRegister = getFreeRegisterProvider(index + 1, 1).getFreeRegister4Bit() + + addInstructionsWithLabels( + index + 1, + """ + invoke-static { }, $EXTENSION_CLASS->isTranslucentNavigationBarForced()Z + move-result v$freeRegister + if-eqz v$freeRegister, :keep_clip_to_padding + const/4 v$freeRegister, 0x0 + invoke-virtual { v$listRegister, v$freeRegister }, Landroid/support/v7/widget/RecyclerView;->setClipToPadding(Z)V + :keep_clip_to_padding + nop + """ + ) + } + } + + // endregion + + // With the translucent navigation bar, the navigation bar makes room for the system + // navigation buttons. If the navigation bar is hidden in the Shorts player, nothing makes + // that room, so the Shorts player makes it itself. The first parameter is the layout + // of the Shorts player fragment. + ShortsBottomBarContainerFingerprint.method.apply { + addInstruction( + 0, + "invoke-static { p1 }, $EXTENSION_CLASS->setShortsPlayerBottomInset(Landroid/view/View;)V" + ) + + // The height of the navigation bar is passed to the object that reserves that room below + // the Shorts player, right after the view. Other patches can change the height after + // it's read, so the room is replaced where it's passed. + val getHeightIndex = indexOfFirstInstructionOrThrow { + getReference()?.name == "getHeight" + } + val reserveRoomIndex = indexOfFirstInstructionOrThrow(getHeightIndex, Opcode.INVOKE_DIRECT_RANGE) + val reserveRoomInstruction = getInstruction(reserveRoomIndex) + val parameterTypes = reserveRoomInstruction.getReference()!! + .parameterTypes.map { it.toString() } + val heightParameterIndex = parameterTypes.indices.first { index -> + parameterTypes[index] == "Landroid/view/View;" && parameterTypes.getOrNull(index + 1) == "I" + } + 1 + // The first register is the instance. + val heightRegister = reserveRoomInstruction.startRegister + 1 + heightParameterIndex + + addInstructions( + reserveRoomIndex, + """ + invoke-static/range { v$heightRegister .. v$heightRegister }, $EXTENSION_CLASS->getShortsPlayerBottomRoom(I)I + move-result v$heightRegister + """ + ) + } + // Paint over the translucent status bar and navigation bar, instead of turning off the // feature flags that drive them. Those flags also switch the app out of edge to edge, which // moves the whole window layout and breaks everything measuring against it. From b8e1fddd8a9d7146f2ccc4d1522a86b134cfe7a6 Mon Sep 17 00:00:00 2001 From: 0xrxL <127248639+0xrxL@users.noreply.github.com> Date: Fri, 23 Oct 2026 01:43:12 +0200 Subject: [PATCH 005/189] fix(YouTube - Navigation bar): Translucent navigation bar is not shown without the edge to edge layout --- .../youtube/patches/NavigationBarPatch.java | 194 ++++++++++-------- .../layout/buttons/navigation/Fingerprints.kt | 8 +- .../buttons/navigation/NavigationBarPatch.kt | 21 +- 3 files changed, 126 insertions(+), 97 deletions(-) diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/NavigationBarPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/NavigationBarPatch.java index 3d07bcb136..ae1da742d3 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/NavigationBarPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/NavigationBarPatch.java @@ -146,8 +146,21 @@ public static void navigationTabCreated(NavigationBar.NavigationButton button, V return; } - if (HIDE_NAVIGATION_NEW_CONTENT_DOT) { - hideNewContentDot(tabView); + // The dot is a stub that is inflated when the tab has new content, and the app shows and + // hides it again later. It is hidden right before every draw, so no frame shows it. The + // theme patch keeps the color of the dot the same way. + if (HIDE_NAVIGATION_NEW_CONTENT_DOT && newContentDotId != 0) { + try { + tabView.getViewTreeObserver().addOnPreDrawListener(() -> { + View dot = tabView.findViewById(newContentDotId); + if (dot != null && !(dot instanceof ViewStub) && dot.getVisibility() == View.VISIBLE) { + dot.setVisibility(View.INVISIBLE); + } + return true; + }); + } catch (Exception ex) { + Logger.printException(() -> "navigationTabCreated new content dot failure", ex); + } } if (Boolean.TRUE.equals(shouldHideMap.get(button))) { @@ -162,29 +175,6 @@ public static void hideNavigationButtonLabels(TextView navigationLabelsView) { Utils.hideViewUnderCondition(Settings.HIDE_NAVIGATION_BUTTON_LABELS, navigationLabelsView); } - /** - * The dot is a stub that is inflated when the tab has new content, and the app shows and - * hides it again later. It is hidden right before every draw, so no frame shows it. The - * theme patch keeps the color of the dot the same way. - */ - private static void hideNewContentDot(View tabView) { - try { - if (newContentDotId == 0) { - return; - } - - tabView.getViewTreeObserver().addOnPreDrawListener(() -> { - View dot = tabView.findViewById(newContentDotId); - if (dot != null && !(dot instanceof ViewStub) && dot.getVisibility() == View.VISIBLE) { - dot.setVisibility(View.INVISIBLE); - } - return true; - }); - } catch (Exception ex) { - Logger.printException(() -> "hideNewContentDot failure", ex); - } - } - /** * Injection point. */ @@ -261,6 +251,41 @@ public static boolean isTranslucentNavigationBarForced() { return translucentNavigationBarForced; } + /** + * Injection point. + *

+ * The blurred, translucent background of the navigation bar and the feed laid out behind it + * only change how the bar looks, not the room left for the system navigation buttons, so they + * are turned on from Android 12 also when the window is not drawn edge to edge. If + * the translucent navigation bar is turned off, they follow + * {@link #useTranslucentNavigationBar(boolean)} instead. + */ + public static boolean useTranslucentNavigationBarBackground(boolean original) { + final boolean translucent; + if (!DISABLE_TRANSLUCENT_NAVIGATION && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + translucent = true; + } else { + translucent = useTranslucentNavigationBar(original); + } + + translucentNavigationBarBackgroundForced = translucent; + return translucent; + } + + /** + * If {@link #useTranslucentNavigationBarBackground(boolean)} turned the translucent background + * of the navigation bar on. The fixes for what the blur and the feed behind the navigation bar + * break are only needed then. + */ + private static volatile boolean translucentNavigationBarBackgroundForced; + + /** + * Injection point. + */ + public static boolean isTranslucentNavigationBarBackgroundForced() { + return translucentNavigationBarBackgroundForced; + } + /** * If the keyboard height was ever removed from the bottom margin of the layout that holds * the page, the player and the navigation bar, so the keyboard covers them. @@ -721,12 +746,64 @@ public static void fixToolbarCreateButtonUpload(List rawButtonList) continue; } - ButtonRenderer createButton = openUploadInCreationModes(buttons.getButtonRenderer()); - if (createButton == null) { + // The upload buttons of the create button open the upload endpoint in the creation modes. + // Nothing is changed if the create button has no upload button. + ButtonRenderer createButton = buttons.getButtonRenderer(); + NavigationEndpoint endpoint = createButton.getNavigationEndpoint(); + if (!endpoint.hasCreationEntrySheetEndpoint()) { + return; + } + CreationEntrySheetEndpoint sheetEndpoint = endpoint.getCreationEntrySheetEndpoint(); + ButtonSheetRenderer sheet = sheetEndpoint.getButtonSheetSupportedRenderers().getButtonSheetRenderer(); + ButtonSheetRenderer.Builder sheetBuilder = sheet.toBuilder(); + boolean replaced = false; + + for (int j = 0, count = sheet.getButtonsCount(); j < count; j++) { + Buttons button = sheet.getButtons(j); + ButtonRenderer buttonRenderer = button.getButtonRenderer(); + NavigationEndpoint uploadEndpoint = buttonRenderer.getNavigationEndpoint(); + if (!button.hasButtonRenderer() || !uploadEndpoint.hasShortsCreationEndpoint()) { + continue; + } + + // The only mode button of the creation modes opens the upload endpoint. + ButtonRenderer modeButton = buttonRenderer.toBuilder() + .clearNavigationEndpoint() + .setCommand(uploadEndpoint) + .build(); + CreationModesSwitcherRenderer switcher = CreationModesSwitcherRenderer.newBuilder() + .addModeButtons(Buttons.newBuilder().setButtonRenderer(modeButton)) + .addModes(CreationMode.CREATION_MODE_UPLOADS) + .setDefaultMode(CreationMode.CREATION_MODE_UPLOADS) + .build(); + CreationModesCommand creationModesCommand = CreationModesCommand.newBuilder() + .setCreationModesControlsRenderer(CreationModesControlsRendererHolder.newBuilder() + .setCreationModesControlsRenderer(CreationModesControlsRenderer.newBuilder() + .setCreationModesSwitcherRenderer(CreationModesSwitcherRendererHolder.newBuilder() + .setCreationModesSwitcherRenderer(switcher)))) + .build(); + + NavigationEndpoint creationModesEndpoint = uploadEndpoint.toBuilder() + .clearShortsCreationEndpoint() + .setCreationModesCommand(creationModesCommand) + .build(); + sheetBuilder.setButtons(j, button.toBuilder().setButtonRenderer( + buttonRenderer.toBuilder().setNavigationEndpoint(creationModesEndpoint))); + replaced = true; + } + if (!replaced) { return; } + + CreationEntrySheetEndpoint creationModesSheetEndpoint = sheetEndpoint.toBuilder() + .setButtonSheetSupportedRenderers(sheetEndpoint.getButtonSheetSupportedRenderers().toBuilder() + .setButtonSheetRenderer(sheetBuilder)) + .build(); + ButtonRenderer creationModesCreateButton = createButton.toBuilder() + .setNavigationEndpoint(endpoint.toBuilder().setCreationEntrySheetEndpoint(creationModesSheetEndpoint)) + .build(); rawButtonList.set(i, parseWithRegistry(rawButtons, - buttons.toBuilder().setButtonRenderer(createButton).build().toByteArray(), + buttons.toBuilder().setButtonRenderer(creationModesCreateButton).build().toByteArray(), getGeneratedRegistry())); Logger.printDebug(() -> "Toolbar create button opens the upload in the creation modes"); return; @@ -753,67 +830,6 @@ private static ExtensionRegistryLite getGeneratedRegistry() { return null; // Modified during patching. } - /** - * @return The create button whose upload buttons open the upload endpoint in the creation modes, - * or null if the create button has no upload button. - */ - @Nullable - private static ButtonRenderer openUploadInCreationModes(ButtonRenderer createButton) { - NavigationEndpoint endpoint = createButton.getNavigationEndpoint(); - if (!endpoint.hasCreationEntrySheetEndpoint()) { - return null; - } - CreationEntrySheetEndpoint sheetEndpoint = endpoint.getCreationEntrySheetEndpoint(); - ButtonSheetRenderer sheet = sheetEndpoint.getButtonSheetSupportedRenderers().getButtonSheetRenderer(); - ButtonSheetRenderer.Builder sheetBuilder = sheet.toBuilder(); - boolean replaced = false; - - for (int i = 0, count = sheet.getButtonsCount(); i < count; i++) { - Buttons button = sheet.getButtons(i); - ButtonRenderer buttonRenderer = button.getButtonRenderer(); - NavigationEndpoint uploadEndpoint = buttonRenderer.getNavigationEndpoint(); - if (!button.hasButtonRenderer() || !uploadEndpoint.hasShortsCreationEndpoint()) { - continue; - } - - // The only mode button of the creation modes opens the upload endpoint. - ButtonRenderer modeButton = buttonRenderer.toBuilder() - .clearNavigationEndpoint() - .setCommand(uploadEndpoint) - .build(); - CreationModesSwitcherRenderer switcher = CreationModesSwitcherRenderer.newBuilder() - .addModeButtons(Buttons.newBuilder().setButtonRenderer(modeButton)) - .addModes(CreationMode.CREATION_MODE_UPLOADS) - .setDefaultMode(CreationMode.CREATION_MODE_UPLOADS) - .build(); - CreationModesCommand creationModesCommand = CreationModesCommand.newBuilder() - .setCreationModesControlsRenderer(CreationModesControlsRendererHolder.newBuilder() - .setCreationModesControlsRenderer(CreationModesControlsRenderer.newBuilder() - .setCreationModesSwitcherRenderer(CreationModesSwitcherRendererHolder.newBuilder() - .setCreationModesSwitcherRenderer(switcher)))) - .build(); - - NavigationEndpoint creationModesEndpoint = uploadEndpoint.toBuilder() - .clearShortsCreationEndpoint() - .setCreationModesCommand(creationModesCommand) - .build(); - sheetBuilder.setButtons(i, button.toBuilder().setButtonRenderer( - buttonRenderer.toBuilder().setNavigationEndpoint(creationModesEndpoint))); - replaced = true; - } - if (!replaced) { - return null; - } - - CreationEntrySheetEndpoint creationModesSheetEndpoint = sheetEndpoint.toBuilder() - .setButtonSheetSupportedRenderers(sheetEndpoint.getButtonSheetSupportedRenderers().toBuilder() - .setButtonSheetRenderer(sheetBuilder)) - .build(); - return createButton.toBuilder() - .setNavigationEndpoint(endpoint.toBuilder().setCreationEntrySheetEndpoint(creationModesSheetEndpoint)) - .build(); - } - /** * Injection point. */ diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/buttons/navigation/Fingerprints.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/buttons/navigation/Fingerprints.kt index 88417e3598..012df45d26 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/buttons/navigation/Fingerprints.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/buttons/navigation/Fingerprints.kt @@ -135,10 +135,16 @@ internal object PivotBarChangedFingerprint : Fingerprint( */ internal val TRANSLUCENT_NAVIGATION_BAR_FEATURE_FLAGS = listOf( 45630927L, // Blurred, translucent background of the navigation bar. - 45632194L, // Navigation bar also covers the system navigation buttons. + NAVIGATION_BAR_COVERS_SYSTEM_BUTTONS_FLAG, 45631257L // Feed laid out behind the navigation bar instead of above it. ) +/** + * Navigation bar also covers the system navigation buttons, the only one of + * [TRANSLUCENT_NAVIGATION_BAR_FEATURE_FLAGS] that changes the room left for them. + */ +internal const val NAVIGATION_BAR_COVERS_SYSTEM_BUTTONS_FLAG = 45632194L + internal object TranslucentNavigationBarFeatureFlagsFingerprint : Fingerprint( filters = listOf( anyInstruction(*TRANSLUCENT_NAVIGATION_BAR_FEATURE_FLAGS.map { literal(it) }.toTypedArray()) diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/buttons/navigation/NavigationBarPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/buttons/navigation/NavigationBarPatch.kt index b480dba329..d10ff0f0f5 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/buttons/navigation/NavigationBarPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/buttons/navigation/NavigationBarPatch.kt @@ -214,15 +214,22 @@ val navigationBarPatch = bytecodePatch( // nothing if the flags are left to the account. // The translucent navigation bar comes from three feature flags that not every account - // gets, so all of them are turned on whenever the window is drawn edge to edge. The bar then - // looks the same on every account, and the opaque option below paints over the same - // translucent bar everywhere. + // gets, so they are turned on to make the bar look the same on every account, and the opaque + // option below paints over the same translucent bar everywhere. The flag that makes the bar + // cover the system navigation buttons is turned on only when the window is drawn edge to + // edge. The blur and the feed behind the bar do not change the room left for the system + // navigation buttons, so they are also turned on without it. TranslucentNavigationBarFeatureFlagsFingerprint.matchAll().forEach { match -> match.method.apply { TRANSLUCENT_NAVIGATION_BAR_FEATURE_FLAGS.forEach { flag -> if (containsLiteralInstruction(flag)) { + val override = if (flag == NAVIGATION_BAR_COVERS_SYSTEM_BUTTONS_FLAG) { + "useTranslucentNavigationBar" + } else { + "useTranslucentNavigationBarBackground" + } // Overrides every read of the flag in the method. - insertLiteralOverride(flag, "$EXTENSION_CLASS->useTranslucentNavigationBar(Z)Z") + insertLiteralOverride(flag, "$EXTENSION_CLASS->$override(Z)Z") } } } @@ -246,7 +253,7 @@ val navigationBarPatch = bytecodePatch( addInstructionsWithLabels( index + 1, $$""" - invoke-static { }, $$EXTENSION_CLASS->isTranslucentNavigationBarForced()Z + invoke-static { }, $$EXTENSION_CLASS->isTranslucentNavigationBarBackgroundForced()Z move-result v$$sdkRegister if-eqz v$$sdkRegister, :draw_blur sget v$$sdkRegister, Landroid/os/Build$VERSION;->SDK_INT:I @@ -337,7 +344,7 @@ val navigationBarPatch = bytecodePatch( addInstructionsWithLabels( index, $$""" - invoke-static { }, $$EXTENSION_CLASS->isTranslucentNavigationBarForced()Z + invoke-static { }, $$EXTENSION_CLASS->isTranslucentNavigationBarBackgroundForced()Z move-result v$$imeRegister if-eqz v$$imeRegister, :keep_padding # The inset types are only on Android 11 and later. @@ -386,7 +393,7 @@ val navigationBarPatch = bytecodePatch( addInstructionsWithLabels( index + 1, """ - invoke-static { }, $EXTENSION_CLASS->isTranslucentNavigationBarForced()Z + invoke-static { }, $EXTENSION_CLASS->isTranslucentNavigationBarBackgroundForced()Z move-result v$freeRegister if-eqz v$freeRegister, :keep_clip_to_padding const/4 v$freeRegister, 0x0 From f80398e107587c7fb9f995f508a65ad1953b2734 Mon Sep 17 00:00:00 2001 From: 0xrxL <127248639+0xrxL@users.noreply.github.com> Date: Fri, 23 Oct 2026 01:43:12 +0200 Subject: [PATCH 006/189] fix(YouTube - Hide status bar): Video player is moved below the display cutout in portrait --- .../youtube/patches/HideStatusBarPatch.java | 25 ++++++++++++++++++- .../layout/statusbar/HideStatusBarPatch.kt | 2 ++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/HideStatusBarPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/HideStatusBarPatch.java index 3a7fad73c0..fe25ed011c 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/HideStatusBarPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/HideStatusBarPatch.java @@ -29,6 +29,8 @@ import app.morphe.extension.shared.Logger; import app.morphe.extension.youtube.settings.Settings; +import app.morphe.extension.youtube.shared.PlayerType; +import kotlin.Unit; @SuppressWarnings({"unused", "deprecation"}) public final class HideStatusBarPatch { @@ -65,6 +67,14 @@ public void onActivityDestroyed(@NonNull Activity a) {} private static boolean activityLifecycleCallbacksRegistered; + /** + * If a video is watched in the player that fills the screen in portrait, not in fullscreen. + * The display cutout inset is then left out, so the video is laid out at the top of the screen + * instead of below the cutout. It is added back in the miniplayer, in fullscreen and without + * a video. + */ + private static volatile boolean isWatchingVideo; + /** * Value of the edge to edge top bar feature flag used by the whole app, or null if not read yet. */ @@ -157,6 +167,19 @@ public static void initialize(Activity activity) { if (!activityLifecycleCallbacksRegistered) { activityLifecycleCallbacksRegistered = true; activity.getApplication().registerActivityLifecycleCallbacks(ACTIVITY_LIFECYCLE_CALLBACKS); + + // The display cutout inset depends on the player, so it is applied again + // when the player fills the screen or leaves it. + PlayerType.getOnChange().addObserver((PlayerType type) -> { + final boolean watchingVideo = type == PlayerType.WATCH_WHILE_MAXIMIZED; + if (watchingVideo != isWatchingVideo) { + isWatchingVideo = watchingVideo; + for (Activity hiddenStatusBarActivity : activities) { + hiddenStatusBarActivity.getWindow().getDecorView().requestApplyInsets(); + } + } + return Unit.INSTANCE; + }); } hideStatusBar(activity); } catch (Exception ex) { @@ -244,7 +267,7 @@ public static void setSystemWindowInsetTop(Rect systemWindowInsets, @Nullable Vi return; } - systemWindowInsets.top = getDisplayCutoutInsetTop(insets); + systemWindowInsets.top = isWatchingVideo ? 0 : getDisplayCutoutInsetTop(insets); } catch (Exception ex) { Logger.printException(() -> "setSystemWindowInsetTop failure", ex); } diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/statusbar/HideStatusBarPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/statusbar/HideStatusBarPatch.kt index 082f68e35d..55863012d3 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/statusbar/HideStatusBarPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/statusbar/HideStatusBarPatch.kt @@ -12,6 +12,7 @@ import app.morphe.patcher.extensions.InstructionExtensions.getInstruction import app.morphe.patcher.patch.bytecodePatch import app.morphe.patches.shared.misc.settings.preference.SwitchPreference import app.morphe.patches.youtube.misc.extension.sharedExtensionPatch +import app.morphe.patches.youtube.misc.playertype.playerTypeHookPatch import app.morphe.patches.youtube.misc.settings.PreferenceScreen import app.morphe.patches.youtube.misc.settings.settingsPatch import app.morphe.patches.youtube.shared.Constants.COMPATIBILITY_YOUTUBE @@ -30,6 +31,7 @@ val hideStatusBarPatch = bytecodePatch( dependsOn( sharedExtensionPatch, settingsPatch, + playerTypeHookPatch, ) compatibleWith(COMPATIBILITY_YOUTUBE) From 01e767c4c3a386e4b7881ce7fc867a516082db23 Mon Sep 17 00:00:00 2001 From: 0xrxL <127248639+0xrxL@users.noreply.github.com> Date: Fri, 23 Oct 2026 02:00:15 +0200 Subject: [PATCH 007/189] feat(YouTube - Navigation bar): Add "Disable toolbar navigation tabs" setting --- .../youtube/patches/ClassicSubscriptionsLayoutPatch.java | 9 ++++++--- .../app/morphe/extension/youtube/settings/Settings.java | 1 + .../layout/buttons/navigation/NavigationBarPatch.kt | 9 ++++++++- .../resources/addresources/values/youtube/strings.xml | 2 ++ 4 files changed, 17 insertions(+), 4 deletions(-) diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/ClassicSubscriptionsLayoutPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/ClassicSubscriptionsLayoutPatch.java index fc484cc95c..e0c664fbb7 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/ClassicSubscriptionsLayoutPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/ClassicSubscriptionsLayoutPatch.java @@ -58,6 +58,9 @@ public interface PivotBarItemInterface { private static final boolean DISABLE_ICON_ONLY_NAVIGATION_BUTTONS = Settings.DISABLE_ICON_ONLY_NAVIGATION_BUTTONS.get(); + private static final boolean DISABLE_TOOLBAR_NAVIGATION_TABS = + Settings.DISABLE_TOOLBAR_NAVIGATION_TABS.get(); + /** * Icon of the Subscriptions navigation button, with the same style of the Home button. */ @@ -78,7 +81,7 @@ private static boolean isFeedTab(TabRenderer tabRenderer) { * @return Only the selected tab, if the tabs are the Home and Subscriptions feeds. */ public static List filterBrowseTabs(List tabs) { - if (tabs == null || tabs.size() <= 1) return tabs; + if (!DISABLE_TOOLBAR_NAVIGATION_TABS || tabs == null || tabs.size() <= 1) return tabs; try { int selectedIndex = 0; @@ -182,7 +185,7 @@ private static String getPivotIdentifier(Object pivotBarItem) { * @return If the Subscriptions button is missing. */ public static boolean needsSubscriptionsPivotBarItem(List list) { - if (list == null || list.isEmpty()) return false; + if (!DISABLE_TOOLBAR_NAVIGATION_TABS || list == null || list.isEmpty()) return false; PivotBarItemRenderer homeRenderer = null; for (Object item : list) { @@ -250,6 +253,6 @@ public static List addSubscriptionsPivotBarItem(List list, @Null * Navigation bar used before the guide response is loaded. */ public static boolean hideOfflineSubscriptionsButton(boolean original) { - return false; + return !DISABLE_TOOLBAR_NAVIGATION_TABS && original; } } diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/Settings.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/Settings.java index 9a1cf5e95b..684db715cf 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/Settings.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/Settings.java @@ -566,6 +566,7 @@ public class Settings extends SharedYouTubeSettings { public static final BooleanSetting SHOW_SETTINGS_BUTTON_TYPE = new BooleanSetting("morphe_show_settings_button_type", FALSE, true, parent(SHOW_SETTINGS_BUTTON)); public static final BooleanSetting HIDE_NAVIGATION_BUTTON_LABELS = new BooleanSetting("morphe_hide_navigation_button_labels", FALSE, true, parentNot(HIDE_NAVIGATION_BAR)); public static final BooleanSetting DISABLE_ICON_ONLY_NAVIGATION_BUTTONS = new BooleanSetting("morphe_disable_icon_only_navigation_buttons", TRUE, true, parentNot(HIDE_NAVIGATION_BAR)); + public static final BooleanSetting DISABLE_TOOLBAR_NAVIGATION_TABS = new BooleanSetting("morphe_disable_toolbar_navigation_tabs", TRUE, true); public static final BooleanSetting HIDE_NAVIGATION_NEW_CONTENT_DOT = new BooleanSetting("morphe_hide_navigation_new_content_dot", FALSE, true, parentNot(HIDE_NAVIGATION_BAR)); public static final BooleanSetting NARROW_NAVIGATION_BUTTONS = new BooleanSetting("morphe_narrow_navigation_buttons", FALSE, true, parentNot(HIDE_NAVIGATION_BAR)); public static final BooleanSetting DISABLE_AUTO_HIDE_NAVIGATION_BAR = new BooleanSetting("morphe_disable_auto_hide_navigation_bar", FALSE, true, parentNot(HIDE_NAVIGATION_BAR)); diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/buttons/navigation/NavigationBarPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/buttons/navigation/NavigationBarPatch.kt index d10ff0f0f5..32885657b5 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/buttons/navigation/NavigationBarPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/buttons/navigation/NavigationBarPatch.kt @@ -162,7 +162,14 @@ val navigationBarPatch = bytecodePatch( SwitchPreference("morphe_hide_navigation_bar"), SwitchPreference("morphe_narrow_navigation_buttons", summary = true), SwitchPreference("morphe_hide_navigation_button_labels"), - SwitchPreference("morphe_disable_icon_only_navigation_buttons", summary = true), + SwitchPreference("morphe_disable_icon_only_navigation_buttons", summary = true) + ) + + if (is_21_35_or_greater) { + navPreferences += SwitchPreference("morphe_disable_toolbar_navigation_tabs", summary = true) + } + + navPreferences += listOf( SwitchPreference("morphe_hide_navigation_new_content_dot"), SwitchPreference("morphe_navigation_bar_animations", summary = true), SwitchPreference("morphe_disable_translucent_navigation", summary = true) diff --git a/patches/src/main/resources/addresources/values/youtube/strings.xml b/patches/src/main/resources/addresources/values/youtube/strings.xml index fad5d1028d..a466c3a622 100644 --- a/patches/src/main/resources/addresources/values/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values/youtube/strings.xml @@ -907,6 +907,8 @@ To access Settings, enable the Settings button via General → Toolbar → Show Reduces the horizontal spacing between buttons Disable navigation buttons without labels Shows the navigation buttons with labels, if YouTube sends them without labels + Disable toolbar navigation tabs + Shows Subscriptions in the navigation bar, if YouTube moves it to a tab at the top of Home Enable navigation bar animations Animates transitions when switching between tabs Disable auto-hiding navigation bar From 71275419c5e6b8bd6a0a108e3d7815c8ef11a88e Mon Sep 17 00:00:00 2001 From: 0xrxL <127248639+0xrxL@users.noreply.github.com> Date: Fri, 23 Oct 2026 02:24:58 +0200 Subject: [PATCH 008/189] fix(YouTube): Morphe flyout menu items are missing on tablets --- .../extension/youtube/patches/utils/FlyoutUtils.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/FlyoutUtils.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/FlyoutUtils.java index 0e0405eee3..962d008f90 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/FlyoutUtils.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/FlyoutUtils.java @@ -796,11 +796,16 @@ private static FlyoutMenuInfo getFlyoutMenuInfo(Object flyoutPanel, int initialI if (flyoutPanel instanceof PopupWindow checkedPopupWindow) { popupWindow = checkedPopupWindow; - if (checkedPopupWindow.getContentView() instanceof FrameLayout frameLayout) { + View contentView = checkedPopupWindow.getContentView(); + if (contentView instanceof FrameLayout frameLayout) { if (frameLayout.getChildAt(0) instanceof ViewGroup viewGroup && viewGroup.getChildAt(0) instanceof LinearLayout checkedMenuContainer) { menuContainer = checkedMenuContainer; } + } else if (contentView instanceof ViewGroup viewGroup && + viewGroup.getChildAt(0) instanceof LinearLayout checkedMenuContainer) { + // The contextual menu of tablets is wrapped in a view that places it next to its anchor. + menuContainer = checkedMenuContainer; } } else if (flyoutPanel instanceof Dialog checkedDialog) { Window window = checkedDialog.getWindow(); From fbb628ba23ecdcabd00b3bdfbf94aff60f58ac68 Mon Sep 17 00:00:00 2001 From: RAMiK <60112744+RAMiK4@users.noreply.github.com> Date: Fri, 23 Oct 2026 11:23:14 +0400 Subject: [PATCH 009/189] feat(YouTube): Add `Search comments` patch (#3942) Co-authored-by: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> --- .../components/CommentsSearchFilter.java | 597 ++++++++++++++++++ .../extension/youtube/settings/Settings.java | 1 + .../youtube/shared/EngagementPanel.java | 8 + .../patches/shared/misc/bridge/Bridge.kt | 45 ++ .../interaction/ask/AskFlyoutMenuPatch.kt | 35 +- .../searchcomments/Fingerprints.kt | 89 +++ .../searchcomments/SearchCommentsPatch.kt | 133 ++++ .../addresources/values/youtube/strings.xml | 10 + .../morphe_search_comments_button.xml | 12 + 9 files changed, 896 insertions(+), 34 deletions(-) create mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/components/CommentsSearchFilter.java create mode 100644 patches/src/main/kotlin/app/morphe/patches/shared/misc/bridge/Bridge.kt create mode 100644 patches/src/main/kotlin/app/morphe/patches/youtube/interaction/searchcomments/Fingerprints.kt create mode 100644 patches/src/main/kotlin/app/morphe/patches/youtube/interaction/searchcomments/SearchCommentsPatch.kt create mode 100644 patches/src/main/resources/searchcomments/drawable/morphe_search_comments_button.xml diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/components/CommentsSearchFilter.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/components/CommentsSearchFilter.java new file mode 100644 index 0000000000..4939efdeca --- /dev/null +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/components/CommentsSearchFilter.java @@ -0,0 +1,597 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/3942 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.extension.youtube.patches.components; + +import static app.morphe.extension.shared.StringRef.str; + +import android.app.Dialog; +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.text.InputType; +import android.util.Pair; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewParent; +import android.view.Window; +import android.view.WindowManager; +import android.widget.EditText; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.RelativeLayout; + +import androidx.annotation.Nullable; + +import java.lang.ref.WeakReference; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.WeakHashMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import app.morphe.extension.shared.Logger; +import app.morphe.extension.shared.ResourceType; +import app.morphe.extension.shared.ResourceUtils; +import app.morphe.extension.shared.Utils; +import app.morphe.extension.shared.patches.components.BufferAsciiStrings; +import app.morphe.extension.shared.patches.components.ContextInterface; +import app.morphe.extension.shared.patches.components.Filter; +import app.morphe.extension.shared.patches.components.StringFilterGroup; +import app.morphe.extension.shared.theme.ThemeUtils; +import app.morphe.extension.shared.ui.CustomDialog; +import app.morphe.extension.shared.ui.Dim; +import app.morphe.extension.youtube.patches.VideoInformation; +import app.morphe.extension.youtube.settings.Settings; +import app.morphe.extension.youtube.shared.EngagementPanel; + +/** + * Shows only the comments that contain the searched words. + *

+ * YouTube has no search of comments, so only the comments that are loaded are searched. + * Comments are filtered when they are loaded, so after a search the comments are reloaded, + * as when the sort order is changed. + * As the hidden comments do not fill the list, scrolling loads the next comments sooner. + */ +@SuppressWarnings("unused") +public final class CommentsSearchFilter extends Filter { + + /** + * Added during patching to the controller of section lists, such as the list of the comments panel. + */ + public interface SectionListAccess { + View patch_getListView(); + + /** + * Reloads the list as when its sort order is changed, if the list can be reloaded. + * + * @return If the list is reloaded. + */ + boolean patch_reload(); + } + + private static final String SEARCH_BUTTON_TAG = "morphe_search_comments_button"; + + private static final int ID_CLOSE_BUTTON = + ResourceUtils.getIdentifier(ResourceType.ID, "close_button"); + + /** + * Words and phrases of the search, all of which a comment must contain, or empty if not searching. + */ + private static volatile List searchTerms = Collections.emptyList(); + private static volatile String searchText = ""; + /** + * The search applies only to the comments of the video it was made for. + */ + private static volatile String searchVideoId = ""; + + private static final String COMMENT_THREAD_PATH = "comment_thread.eml"; + + /** + * Shortest text of a buffer that is taken as an id, such as the id of a comment + * or the token that loads its replies. + */ + private static final int MIN_ID_LENGTH = 24; + private static final int MAX_IDS_PER_COMMENT = 40; + private static final int MAX_IDS = 4000; + + private static final Pattern SEARCH_TERM_PATTERN = Pattern.compile("\"([^\"]*)\"|(\\S+)"); + + /** + * The comment of each thread is laid out from its outermost component to its innermost. + * Each component has a buffer of its own, and only the buffer of the outermost component + * holds all of the comment, such as its author and its text. The comment is shown or hidden + * as a whole by the outermost component, and its inner components are never hidden alone. + *

+ * Litho lays out on more than one thread, so each thread keeps its own comment. + */ + private static final ThreadLocal lastComment = new ThreadLocal<>(); + + /** + * Comments of the ids found in their buffers. The button that shows the replies of a comment + * is not inside the comment, and it's found by the ids it shares with its comment, + * as it can be laid out apart from its comment, such as when scrolling back to it. + */ + private static final Map commentIds = Collections.synchronizedMap( + Utils.createSizeRestrictedMap(MAX_IDS)); + + /** + * Section lists by their list view, to reload the comments of the panel that is searched. + */ + private static final Map> sectionLists = + Collections.synchronizedMap(new WeakHashMap<>()); + + /** + * Marks an id found in comments that are shown and in comments that are hidden, + * such as an id of the layout, which tells nothing of the comment. + */ + private static final CommentDecision AMBIGUOUS_ID = new CommentDecision("", Collections.emptyList(), false); + + private static final class CommentDecision { + final String path; + final List terms; + final boolean hidden; + /** + * If the replies of the hidden comment, which are laid out after it, are not yet hidden. + */ + boolean hideReplies; + + CommentDecision(String path, List terms, boolean hidden) { + this.path = path; + this.terms = terms; + this.hidden = hidden; + this.hideReplies = hidden; + } + } + + private final StringFilterGroup commentThread = new StringFilterGroup( + Settings.SEARCH_COMMENTS, + COMMENT_THREAD_PATH + ); + + /** + * The button that shows the replies of a comment is not inside the comment. + */ + private final StringFilterGroup commentReplies = new StringFilterGroup( + Settings.SEARCH_COMMENTS, + "repl" + ); + + public CommentsSearchFilter() { + addPathCallbacks(commentThread, commentReplies); + } + + @Override + public boolean isFiltered(ContextInterface contextInterface, + String identifier, + String accessibility, + CharSequence path, + byte[] buffer, + BufferAsciiStrings asciiStrings, + StringFilterGroup matchedGroup, + FilterContentType contentType, + int contentIndex) { + List terms = searchTerms; + if (terms.isEmpty() || !isSearchForCurrentVideo()) { + return false; + } + + CommentDecision last = lastComment.get(); + + if (matchedGroup == commentReplies) { + if (Utils.contains(path, COMMENT_THREAD_PATH)) { + return false; // Part of a comment. + } + return isRepliesHidden(path, buffer, terms, last); + } + + if (last != null && last.terms == terms + && path.length() > last.path.length() && Utils.startsWith(path, last.path)) { + // Inner component of the comment that was already searched. + return last.hidden; + } + + if (buffer.length == 0) { + return false; // Nothing to search, so the comment is left for its inner components. + } + + // The buffer is not text, but the text of the comment is in it as UTF-8. + String text = foldCase(new String(buffer, StandardCharsets.UTF_8)); + final boolean hidden = !containsAllTerms(text, terms); + String commentPath = path.toString(); + CommentDecision decision = new CommentDecision(commentPath, terms, hidden); + lastComment.set(decision); + rememberIds(buffer, decision); + Logger.printDebug(() -> (hidden ? "Hiding comment: " : "Showing comment: ") + commentPath); + return hidden; + } + + private static boolean isRepliesHidden(CharSequence path, byte[] buffer, + List terms, @Nullable CommentDecision last) { + CommentDecision comment = findCommentOfReplies(buffer, terms); + if (comment == null) { + // No id is shared with a comment, so the replies are of the comment laid out before them. + if (last == null || last.terms != terms || !last.hideReplies) { + return false; + } + comment = last; + } else if (!comment.hidden) { + return false; + } + + comment.hideReplies = false; + String repliesPath = path.toString(); + Logger.printDebug(() -> "Hiding replies of hidden comment: " + repliesPath); + return true; + } + + /** + * @return The comment that shares an id with the replies, preferring a shown comment + * so the replies of a shown comment are never hidden, or null if none is found. + */ + @Nullable + private static CommentDecision findCommentOfReplies(byte[] buffer, List terms) { + CommentDecision hiddenComment = null; + for (String id : findIds(buffer)) { + CommentDecision decision = commentIds.get(id); + if (decision == null || decision == AMBIGUOUS_ID || decision.terms != terms) { + continue; + } + if (!decision.hidden) { + return decision; + } + hiddenComment = decision; + } + return hiddenComment; + } + + private static void rememberIds(byte[] buffer, CommentDecision decision) { + List ids = findIds(buffer); + synchronized (commentIds) { + for (String id : ids) { + CommentDecision existing = commentIds.get(id); + if (existing == AMBIGUOUS_ID) { + continue; // Stays ambiguous until the search changes. + } + if (existing == null || existing.terms != decision.terms + // The same comment, such as when scrolled back to, or a comment of the same result. + || existing.hidden == decision.hidden) { + commentIds.put(id, decision); + } else { + commentIds.put(id, AMBIGUOUS_ID); + } + } + } + } + + /** + * @return Texts of the buffer that look like ids, such as the id of the comment. + */ + private static List findIds(byte[] buffer) { + List ids = new ArrayList<>(); + int start = -1; + for (int i = 0, length = buffer.length; i <= length; i++) { + if (i < length && isIdCharacter(buffer[i])) { + if (start < 0) start = i; + continue; + } + if (start >= 0) { + if (i - start >= MIN_ID_LENGTH) { + ids.add(new String(buffer, start, i - start, StandardCharsets.US_ASCII)); + if (ids.size() >= MAX_IDS_PER_COMMENT) break; + } + start = -1; + } + } + return ids; + } + + private static boolean isIdCharacter(byte b) { + return (b >= 'A' && b <= 'Z') + || (b >= 'a' && b <= 'z') + || (b >= '0' && b <= '9') + || b == '-' || b == '_'; + } + + private static boolean containsAllTerms(String text, List terms) { + for (String term : terms) { + if (!text.contains(term)) { + return false; + } + } + return true; + } + + /** + * Ignores the case, and the dotted and dotless i of Turkish, whatever the language of the device. + * Otherwise, with a Turkish device "IPHONE" is not found when searching "iphone". + */ + private static String foldCase(String text) { + return text.toLowerCase(Locale.ROOT) + .replace("̇", "") // Combining dot above, left by lowercasing the dotted capital I. + .replace('ı', 'i'); // Dotless small i. + } + + /** + * @return Words and quoted phrases of the search, all of which a comment must contain. + */ + private static List parseSearch(String text) { + List terms = new ArrayList<>(); + Matcher matcher = SEARCH_TERM_PATTERN.matcher(foldCase(text)); + while (matcher.find()) { + String phrase = matcher.group(1); + String term = phrase != null + ? phrase.trim().replaceAll("\\s+", " ") + // An unclosed quote is ignored. + : matcher.group(2).replace("\"", ""); + if (!term.isEmpty()) { + terms.add(term); + } + } + return terms; + } + + private static boolean isSearchForCurrentVideo() { + return searchVideoId.equals(VideoInformation.getVideoId()); + } + + private static boolean isSearching() { + return !searchTerms.isEmpty() && isSearchForCurrentVideo(); + } + + private static void setSearch(String text) { + text = text.trim(); + List terms = parseSearch(text); + + // Set the video first, so a filter thread does not apply the terms to another video. + searchVideoId = VideoInformation.getVideoId(); + searchText = text; + searchTerms = Collections.unmodifiableList(terms); + commentIds.clear(); + Logger.printDebug(() -> "Comments search: " + terms); + } + + /** + * Injection point. + * Called when the controller of a section list is created. + */ + public static void onSectionListCreated(SectionListAccess sectionList) { + if (!Settings.SEARCH_COMMENTS.get()) { + return; + } + + try { + View listView = sectionList.patch_getListView(); + if (listView != null) { + sectionLists.put(listView, new WeakReference<>(sectionList)); + } + } catch (Exception ex) { + Logger.printException(() -> "onSectionListCreated failure", ex); + } + } + + /** + * Reloads the list of the panel of the search button, as when its sort order is changed, + * so the search applies to all of its comments. + * + * @return If the list is reloaded. + */ + private static boolean reloadPanelList(View searchButton) { + SectionListAccess sectionList = findPanelList(searchButton); + if (sectionList == null) { + Logger.printDebug(() -> "Panel list not found"); + return false; + } + return sectionList.patch_reload(); + } + + /** + * @return The shown section list closest to the view, which is the list of the panel of the view. + */ + @Nullable + private static SectionListAccess findPanelList(View view) { + List ancestors = new ArrayList<>(); + for (ViewParent parent = view.getParent(); parent != null; parent = parent.getParent()) { + ancestors.add(parent); + } + + SectionListAccess closestList = null; + int closestDistance = Integer.MAX_VALUE; + synchronized (sectionLists) { + for (Map.Entry> entry : sectionLists.entrySet()) { + View listView = entry.getKey(); + SectionListAccess sectionList = entry.getValue().get(); + if (sectionList == null || !listView.isShown()) { + continue; + } + // The distance to the first ancestor of the view that is also an ancestor of the list. + for (ViewParent parent = listView.getParent(); parent != null; parent = parent.getParent()) { + final int distance = ancestors.indexOf(parent); + if (distance >= 0) { + if (distance < closestDistance) { + closestDistance = distance; + closestList = sectionList; + } + break; + } + } + } + } + return closestList; + } + + /** + * Injection point. + * Called on the main thread when the header of an engagement panel is shown. + */ + public static void onPanelSubheaderShown(View subheader) { + if (subheader == null || !Settings.SEARCH_COMMENTS.get() || ID_CLOSE_BUTTON == 0) { + return; + } + + // The panel is opened after its header is set up. + subheader.post(() -> { + try { + updateSearchButton(subheader); + } catch (Exception ex) { + Logger.printException(() -> "updateSearchButton failure", ex); + } + }); + } + + private static void updateSearchButton(View subheader) { + View closeButton = findCloseButton(subheader); + if (closeButton == null || !(closeButton.getParent() instanceof ViewGroup header)) { + Logger.printDebug(() -> "Panel close button not found"); + return; + } + + View searchButton = header.findViewWithTag(SEARCH_BUTTON_TAG); + if (searchButton == null) { + searchButton = createSearchButton(header, closeButton); + if (searchButton == null) { + return; + } + } + + final boolean isCommentsPanel = isCommentsPanelOpen(); + searchButton.setVisibility(isCommentsPanel ? View.VISIBLE : View.GONE); + if (isCommentsPanel) { + updateSearchButtonIcon(searchButton); + } + } + + /** + * The header of the panel holds the close button, and the subheader is below it. + */ + @Nullable + private static View findCloseButton(View subheader) { + ViewParent parent = subheader.getParent(); + while (parent instanceof ViewGroup group) { + View closeButton = group.findViewById(ID_CLOSE_BUTTON); + if (closeButton != null) { + return closeButton; + } + parent = group.getParent(); + } + return null; + } + + private static boolean isCommentsPanelOpen() { + String panelId = EngagementPanel.getLastOpenedPanelId(); + // The comments of videos and Shorts, and the replies of a comment. + return panelId != null && panelId.contains("comment"); + } + + @Nullable + private static View createSearchButton(ViewGroup header, View closeButton) { + Context context = header.getContext(); + Drawable icon = ResourceUtils.getDrawable("morphe_search_comments_button"); + if (icon == null) { + Logger.printException(() -> "Search comments drawable not found"); + return null; + } + + ViewGroup.LayoutParams params; + if (header instanceof LinearLayout) { + LinearLayout.LayoutParams linearParams = new LinearLayout.LayoutParams(Dim.dp48, Dim.dp48); + linearParams.gravity = Gravity.CENTER_VERTICAL; + params = linearParams; + } else if (header instanceof RelativeLayout && closeButton.getId() != View.NO_ID) { + RelativeLayout.LayoutParams relativeParams = new RelativeLayout.LayoutParams(Dim.dp48, Dim.dp48); + relativeParams.addRule(RelativeLayout.START_OF, closeButton.getId()); + relativeParams.addRule(RelativeLayout.CENTER_VERTICAL); + params = relativeParams; + } else { + Logger.printDebug(() -> "Unsupported panel header layout: " + header.getClass().getName()); + return null; + } + + FrameLayout button = new FrameLayout(context); + button.setTag(SEARCH_BUTTON_TAG); + button.setContentDescription(str("morphe_search_comments_dialog_title")); + button.setClickable(true); + button.setFocusable(true); + + ImageView iconView = new ImageView(context); + iconView.setImageDrawable(icon.mutate()); + iconView.setScaleType(ImageView.ScaleType.FIT_CENTER); + button.addView(iconView, new FrameLayout.LayoutParams(Dim.dp24, Dim.dp24, Gravity.CENTER)); + button.setOnClickListener(view -> showSearchDialog(view.getContext(), view)); + + header.addView(button, Math.max(header.indexOfChild(closeButton), 0), params); + return button; + } + + /** + * The icon is filled while searching, so it's clear why comments are missing. + */ + private static void updateSearchButtonIcon(View searchButton) { + if (searchButton instanceof ViewGroup group && group.getChildAt(0) instanceof ImageView iconView) { + Drawable icon = iconView.getDrawable(); + if (icon != null) { + icon.setTint(ThemeUtils.getAppForegroundColor()); + iconView.setAlpha(isSearching() ? 1.0f : 0.6f); + } + } + } + + private static void showSearchDialog(Context context, View searchButton) { + try { + EditText editText = CustomDialog.createEditText(context); + editText.setHint(str("morphe_search_comments_hint")); + editText.setSingleLine(true); + editText.setInputType(InputType.TYPE_CLASS_TEXT); + String text = isSearching() ? searchText : ""; + editText.setText(text); + editText.setSelection(text.length()); + + Pair dialogPair = CustomDialog.create( + context, + str("morphe_search_comments_dialog_title"), + null, + editText, + str("morphe_search_comments_search"), + () -> { + String newText = editText.getText().toString(); + setSearch(newText); + updateSearchButtonIcon(searchButton); + if (!reloadPanelList(searchButton)) { + // Such as only quotes, which are not a search. + Utils.showToastLong(str(!isSearching() + ? "morphe_search_comments_cleared" + : "morphe_search_comments_applied")); + } + }, + () -> {}, + str("morphe_search_comments_clear"), + () -> { + setSearch(""); + updateSearchButtonIcon(searchButton); + if (!reloadPanelList(searchButton)) { + Utils.showToastLong(str("morphe_search_comments_cleared")); + } + }, + true + ); + Dialog dialog = dialogPair.first; + Window window = dialog.getWindow(); + if (window != null) { + window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE); + } + editText.requestFocus(); + dialog.show(); + } catch (Exception ex) { + Logger.printException(() -> "showSearchDialog failure", ex); + } + } +} diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/Settings.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/Settings.java index 684db715cf..55a5c3ae05 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/Settings.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/Settings.java @@ -341,6 +341,7 @@ public class Settings extends SharedYouTubeSettings { public static final BooleanSetting CHANNEL_SEARCH = new BooleanSetting("morphe_channel_search", TRUE); public static final BooleanSetting COPY_VIDEO_TITLE = new BooleanSetting("morphe_copy_video_title", TRUE); public static final BooleanSetting COPY_COMMENTS = new BooleanSetting("morphe_copy_comments", TRUE); + public static final BooleanSetting SEARCH_COMMENTS = new BooleanSetting("morphe_search_comments", TRUE); // Feed video actions public static final BooleanSetting FEED_LONG_PRESS_MENU = new BooleanSetting("morphe_feed_long_press_menu", FALSE); diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/shared/EngagementPanel.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/shared/EngagementPanel.java index 1eecd5ab64..bbd78bb78b 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/shared/EngagementPanel.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/shared/EngagementPanel.java @@ -51,6 +51,14 @@ public static boolean isDescription() { return checkIdsInQueue(videoDescriptionPanelName); } + /** + * @return The id of the panel opened last, or null if no panel is open. + */ + @Nullable + public static String getLastOpenedPanelId() { + return engagementPanelIds.peekLast(); + } + public static boolean checkIdsInQueue(List ids) { if (ids == null) { return false; diff --git a/patches/src/main/kotlin/app/morphe/patches/shared/misc/bridge/Bridge.kt b/patches/src/main/kotlin/app/morphe/patches/shared/misc/bridge/Bridge.kt new file mode 100644 index 0000000000..6b14aa797e --- /dev/null +++ b/patches/src/main/kotlin/app/morphe/patches/shared/misc/bridge/Bridge.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.patches.shared.misc.bridge + +import app.morphe.patcher.extensions.InstructionExtensions.addInstructionsWithLabels +import app.morphe.patcher.util.proxy.mutableTypes.MutableClass +import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation +import com.android.tools.smali.dexlib2.immutable.ImmutableMethod +import com.android.tools.smali.dexlib2.immutable.ImmutableMethodParameter + +/** + * Adds a public method to an app class, which implements a method of an extension interface added to the class. + * + * @param accessFlags The flags of the method. A default method of an interface cannot be final. + */ +internal fun MutableClass.addBridge( + name: String, + parameters: List, + returnType: String, + registerCount: Int, + smali: String, + accessFlags: Int = AccessFlags.PUBLIC.value or AccessFlags.FINAL.value, +) { + methods.add( + ImmutableMethod( + type, + name, + parameters.map { ImmutableMethodParameter(it, null, null) }, + returnType, + accessFlags, + null, + null, + MutableMethodImplementation(registerCount), + ).toMutable().apply { + addInstructionsWithLabels(0, smali) + } + ) +} diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/ask/AskFlyoutMenuPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/ask/AskFlyoutMenuPatch.kt index 74cb55bc49..68fa1c56c8 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/ask/AskFlyoutMenuPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/ask/AskFlyoutMenuPatch.kt @@ -20,10 +20,9 @@ import app.morphe.patcher.methodCall import app.morphe.patcher.patch.BytecodePatchContext import app.morphe.patcher.patch.bytecodePatch import app.morphe.patcher.util.getReference -import app.morphe.patcher.util.proxy.mutableTypes.MutableClass import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod -import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable import app.morphe.patcher.util.registersUsed +import app.morphe.patches.shared.misc.bridge.addBridge import app.morphe.patches.shared.misc.litho.filter.addLithoFilter import app.morphe.patches.shared.misc.settings.preference.SwitchPreference import app.morphe.patches.youtube.layout.flyout.FeedFlyoutBufferObjectFingerprint @@ -44,15 +43,12 @@ import app.morphe.util.indexOfFirstInstructionReversedOrThrow import app.morphe.util.setExtensionIsPatchIncluded import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode -import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation import com.android.tools.smali.dexlib2.iface.ClassDef import com.android.tools.smali.dexlib2.iface.Method import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction import com.android.tools.smali.dexlib2.iface.reference.FieldReference import com.android.tools.smali.dexlib2.iface.reference.MethodReference import com.android.tools.smali.dexlib2.iface.reference.TypeReference -import com.android.tools.smali.dexlib2.immutable.ImmutableMethod -import com.android.tools.smali.dexlib2.immutable.ImmutableMethodParameter import java.util.logging.Logger private const val EXTENSION_CLASS = @@ -595,35 +591,6 @@ private fun BytecodePatchContext.restoreMenuRowTextConstraints() { } } -/** - * Adds a public method to an app class, which implements a method of an extension interface added to the class. - * - * @param accessFlags The flags of the method. A default method of an interface cannot be final. - */ -private fun MutableClass.addBridge( - name: String, - parameters: List, - returnType: String, - registerCount: Int, - smali: String, - accessFlags: Int = AccessFlags.PUBLIC.value or AccessFlags.FINAL.value, -) { - methods.add( - ImmutableMethod( - type, - name, - parameters.map { ImmutableMethodParameter(it, null, null) }, - returnType, - accessFlags, - null, - null, - MutableMethodImplementation(registerCount), - ).toMutable().apply { - addInstructionsWithLabels(0, smali) - } - ) -} - private fun MethodReference.toReference() = "$definingClass->$name(${parameterTypes.joinToString("")})$returnType" diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/searchcomments/Fingerprints.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/searchcomments/Fingerprints.kt new file mode 100644 index 0000000000..1bb1c02c19 --- /dev/null +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/searchcomments/Fingerprints.kt @@ -0,0 +1,89 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/3942 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.patches.youtube.interaction.searchcomments + +import app.morphe.patcher.Fingerprint +import app.morphe.patcher.InstructionLocation.MatchAfterImmediately +import app.morphe.patcher.InstructionLocation.MatchAfterLast +import app.morphe.patcher.InstructionLocation.MatchFirst +import app.morphe.patcher.fieldAccess +import app.morphe.patcher.methodCall +import app.morphe.patcher.opcode +import app.morphe.patcher.resource.ResourceType +import app.morphe.patcher.resourceLiteral +import app.morphe.patcher.string +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode + +/** + * Sets up the header of an engagement panel, such as the comments panel. + */ +internal object PanelSubheaderFingerprint : Fingerprint( + accessFlags = listOf(AccessFlags.PUBLIC, AccessFlags.STATIC), + returnType = "V", + filters = listOf( + resourceLiteral(ResourceType.ID, "panel_header"), + resourceLiteral(ResourceType.ID, "close_button"), + resourceLiteral(ResourceType.ID, "panel_subheader"), + methodCall( + opcode = Opcode.INVOKE_VIRTUAL, + name = "removeAllViews" + ) + ) +) + +/** + * A method of the controller of a section list, such as the list of the comments panel. + */ +internal object SectionListControllerFingerprint : Fingerprint( + filters = listOf( + string("Number of sectionList models must be equal to the number of section states") + ) +) + +/** + * Creates the controller of a section list. The match is the write of the list view. + */ +internal object SectionListControllerConstructorFingerprint : Fingerprint( + classFingerprint = SectionListControllerFingerprint, + name = "", + filters = listOf( + fieldAccess(opcode = Opcode.IPUT_OBJECT, type = "Landroid/view/View;"), + opcode(Opcode.RETURN_VOID, MatchAfterLast()) + ) +) + +/** + * Refreshes a section list. If the list has a reload continuation, as the comments have + * for their sort order, the list is reloaded with it. + * The first match reads the reload continuation type, the second checks that the list has + * a continuation of the type, and the last reloads the list with it. + */ +internal object SectionListRefreshFingerprint : Fingerprint( + classFingerprint = SectionListControllerFingerprint, + accessFlags = listOf(AccessFlags.PUBLIC), + returnType = "V", + parameters = listOf(), + filters = listOf( + opcode(opcode = Opcode.SGET_OBJECT, location = MatchFirst()), + methodCall( + opcode = Opcode.INVOKE_VIRTUAL, + parameters = listOf("L"), + returnType = "Z", + location = MatchAfterImmediately() + ), + opcode(opcode = Opcode.MOVE_RESULT, location = MatchAfterImmediately()), + opcode(opcode = Opcode.IF_EQZ, location = MatchAfterImmediately()), + methodCall( + opcode = Opcode.INVOKE_VIRTUAL, + parameters = listOf(), + returnType = "V", + location = MatchAfterImmediately() + ) + ) +) diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/searchcomments/SearchCommentsPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/searchcomments/SearchCommentsPatch.kt new file mode 100644 index 0000000000..c9109877ff --- /dev/null +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/searchcomments/SearchCommentsPatch.kt @@ -0,0 +1,133 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/3942 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.patches.youtube.interaction.searchcomments + +import app.morphe.patcher.extensions.InstructionExtensions.addInstruction +import app.morphe.patcher.extensions.InstructionExtensions.getInstruction +import app.morphe.patcher.patch.bytecodePatch +import app.morphe.patcher.patch.resourcePatch +import app.morphe.patcher.util.getReference +import app.morphe.patches.all.misc.resources.addResourcesPatch +import app.morphe.patches.shared.misc.bridge.addBridge +import app.morphe.patches.shared.misc.litho.filter.addLithoFilter +import app.morphe.patches.shared.misc.settings.preference.SwitchPreference +import app.morphe.patches.youtube.misc.engagement.engagementPanelHookPatch +import app.morphe.patches.youtube.misc.extension.sharedExtensionPatch +import app.morphe.patches.youtube.misc.litho.filter.lithoFilterPatch +import app.morphe.patches.youtube.misc.settings.PreferenceScreen +import app.morphe.patches.youtube.misc.settings.settingsPatch +import app.morphe.patches.youtube.shared.Constants.COMPATIBILITY_YOUTUBE +import app.morphe.patches.youtube.video.information.videoInformationPatch +import app.morphe.util.ResourceGroup +import app.morphe.util.copyResources +import app.morphe.util.indexOfFirstInstructionOrThrow +import app.morphe.util.indexOfFirstInstructionReversedOrThrow +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction +import com.android.tools.smali.dexlib2.iface.reference.MethodReference + +private const val EXTENSION_FILTER = + "Lapp/morphe/extension/youtube/patches/components/CommentsSearchFilter;" + +private const val SECTION_LIST_ACCESS_INTERFACE = + $$"Lapp/morphe/extension/youtube/patches/components/CommentsSearchFilter$SectionListAccess;" + +@Suppress("unused") +val searchCommentsPatch = bytecodePatch( + name = "Search comments", + description = "Adds a button to the comments to show only the loaded comments that contain the searched words." +) { + dependsOn( + sharedExtensionPatch, + settingsPatch, + addResourcesPatch, + lithoFilterPatch, + engagementPanelHookPatch, + videoInformationPatch, + resourcePatch { + execute { + copyResources( + "searchcomments", + ResourceGroup("drawable", "morphe_search_comments_button.xml") + ) + } + } + ) + + compatibleWith(COMPATIBILITY_YOUTUBE) + + execute { + PreferenceScreen.GENERAL.addPreferences( + SwitchPreference("morphe_search_comments", summary = true) + ) + + addLithoFilter(EXTENSION_FILTER) + + // The search button is added next to the close button of the comments panel. + PanelSubheaderFingerprint.let { + it.method.apply { + val removeAllViewsIndex = indexOfFirstInstructionReversedOrThrow { + opcode == Opcode.INVOKE_VIRTUAL && + getReference()?.name == "removeAllViews" + } + + val setVisibilityIndex = indexOfFirstInstructionOrThrow(removeAllViewsIndex) { + opcode == Opcode.INVOKE_VIRTUAL && + getReference()?.name == "setVisibility" + } + + val subheaderRegister = getInstruction(setVisibilityIndex).registerC + + addInstruction( + setVisibilityIndex + 1, + "invoke-static { v$subheaderRegister }, $EXTENSION_FILTER->onPanelSubheaderShown(Landroid/view/View;)V" + ) + } + } + + // The comments are reloaded after a search, as when the sort order is changed, + // so the search applies to all of them. + val refreshMatches = SectionListRefreshFingerprint.instructionMatches + val reloadContinuationType = refreshMatches.first().getFieldAccessed() + val hasContinuationMethod = refreshMatches[1].getMethodCalled() + val reloadMethod = refreshMatches.last().getMethodCalled() + val listViewField = SectionListControllerConstructorFingerprint.instructionMatches.first().getFieldAccessed() + + mutableClassDefBy(listViewField.definingClass).apply { + interfaces.add(SECTION_LIST_ACCESS_INTERFACE) + addBridge( + "patch_getListView", listOf(), "Landroid/view/View;", 2, + """ + iget-object v0, p0, $listViewField + return-object v0 + """ + ) + addBridge( + "patch_reload", listOf(), "Z", 2, + """ + sget-object v0, $reloadContinuationType + invoke-virtual { p0, v0 }, $hasContinuationMethod + move-result v0 + if-eqz v0, :no_reload_continuation + invoke-virtual { p0 }, $reloadMethod + :no_reload_continuation + return v0 + """ + ) + } + + SectionListControllerConstructorFingerprint.let { + it.method.apply { + addInstruction( + it.instructionMatches.last().index, + "invoke-static/range { p0 .. p0 }, $EXTENSION_FILTER->onSectionListCreated($SECTION_LIST_ACCESS_INTERFACE)V" + ) + } + } + } +} diff --git a/patches/src/main/resources/addresources/values/youtube/strings.xml b/patches/src/main/resources/addresources/values/youtube/strings.xml index a466c3a622..c61b2b5555 100644 --- a/patches/src/main/resources/addresources/values/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values/youtube/strings.xml @@ -491,6 +491,16 @@ After a video with the Ask button is opened once, the feature works with all vid Show feed thumbnail like count Show feed thumbnail like percentage + + Search comments + Tap the search button in the comments to show only the loaded comments that contain all of the searched words. Put words in quotes to search for a phrase + Search comments + Words to search for + Search + Clear + Change the sort order or open the comments again to search them + Search cleared. Change the sort order or open the comments again to show all comments + Disable \'Sign in to TV\' popup Disable \'Connect your devices\' popup diff --git a/patches/src/main/resources/searchcomments/drawable/morphe_search_comments_button.xml b/patches/src/main/resources/searchcomments/drawable/morphe_search_comments_button.xml new file mode 100644 index 0000000000..e440613bc3 --- /dev/null +++ b/patches/src/main/resources/searchcomments/drawable/morphe_search_comments_button.xml @@ -0,0 +1,12 @@ + + + + From 45992041db85e4fb51900cc5a773380732dc1fda Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Fri, 23 Oct 2026 10:46:27 +0000 Subject: [PATCH 010/189] bump(YouTube): Add support for `21.23.492` --- .../main/kotlin/app/morphe/patches/youtube/shared/Constants.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/shared/Constants.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/shared/Constants.kt index 72a530c3fb..1b09e7e3b3 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/shared/Constants.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/shared/Constants.kt @@ -34,8 +34,7 @@ internal object Constants { ), AppTarget( version = "21.23.492", // Last version for Android 9.0 - minSdk = 28, - isExperimental = true + minSdk = 28 ), AppTarget( version = "21.20.405", From 666508eef06a0b68f828f67669e2b20e3f95acfe Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Fri, 23 Oct 2026 10:52:57 +0000 Subject: [PATCH 011/189] chore: Update translations from Crowdin --- .../values-af-rZA/youtube/strings.xml | 12 ++ .../values-am-rET/youtube/strings.xml | 1 + .../values-ar-rSA/youtube/strings.xml | 11 ++ .../values-as-rIN/youtube/strings.xml | 1 + .../values-az-rAZ/youtube/strings.xml | 11 ++ .../values-be-rBY/youtube/strings.xml | 3 + .../values-bg-rBG/youtube/strings.xml | 11 ++ .../values-bn-rBD/youtube/strings.xml | 11 ++ .../values-bs-rBA/youtube/strings.xml | 1 + .../values-ca-rES/music/strings.xml | 5 + .../values-ca-rES/reddit/strings.xml | 1 + .../values-ca-rES/shared-youtube/strings.xml | 1 + .../values-ca-rES/shared/strings.xml | 2 + .../values-ca-rES/youtube/strings.xml | 8 ++ .../values-ckb-rIR/youtube/strings.xml | 2 + .../values-cs-rCZ/youtube/strings.xml | 11 ++ .../values-da-rDK/youtube/strings.xml | 11 ++ .../values-de-rDE/youtube/strings.xml | 11 ++ .../values-el-rGR/youtube/strings.xml | 11 ++ .../values-es-rES/youtube/strings.xml | 11 ++ .../values-et-rEE/youtube/strings.xml | 1 + .../values-eu-rES/youtube/strings.xml | 1 + .../values-fa-rIR/youtube/strings.xml | 11 ++ .../values-fi-rFI/youtube/strings.xml | 11 ++ .../values-fil-rPH/youtube/strings.xml | 11 ++ .../values-fr-rFR/youtube/strings.xml | 21 +++- .../values-ga-rIE/youtube/strings.xml | 17 ++- .../values-gl-rES/music/strings.xml | 7 ++ .../values-gl-rES/reddit/strings.xml | 1 + .../values-gl-rES/shared/strings.xml | 1 + .../values-gl-rES/sponsorblock/strings.xml | 1 + .../values-gl-rES/youtube/strings.xml | 3 + .../values-gu-rIN/youtube/strings.xml | 2 + .../values-hi-rIN/youtube/strings.xml | 11 ++ .../values-hr-rHR/youtube/strings.xml | 11 ++ .../values-hu-rHU/youtube/strings.xml | 11 ++ .../values-hy-rAM/youtube/strings.xml | 1 + .../values-in-rID/youtube/strings.xml | 11 ++ .../values-is-rIS/youtube/strings.xml | 11 ++ .../values-it-rIT/youtube/strings.xml | 11 ++ .../values-iw-rIL/youtube/strings.xml | 11 ++ .../values-ja-rJP/music/strings.xml | 18 +-- .../values-ja-rJP/youtube/strings.xml | 27 +++-- .../values-ka-rGE/music/strings.xml | 1 + .../values-ka-rGE/reddit/strings.xml | 5 +- .../values-ka-rGE/shared-youtube/strings.xml | 2 + .../values-ka-rGE/shared/strings.xml | 1 + .../values-ka-rGE/youtube/strings.xml | 6 + .../values-kk-rKZ/youtube/strings.xml | 1 + .../values-km-rKH/youtube/strings.xml | 1 + .../values-kmr-rTR/youtube/strings.xml | 1 + .../values-kn-rIN/youtube/strings.xml | 1 + .../values-ko-rKR/youtube/strings.xml | 11 ++ .../values-ky-rKG/youtube/strings.xml | 1 + .../values-lo-rLA/youtube/strings.xml | 1 + .../values-lt-rLT/youtube/strings.xml | 11 ++ .../values-lv-rLV/youtube/strings.xml | 2 + .../values-mai-rIN/youtube/strings.xml | 1 + .../values-mk-rMK/youtube/strings.xml | 1 + .../values-mn-rMN/youtube/strings.xml | 1 + .../values-mr-rIN/youtube/strings.xml | 1 + .../values-ms-rMY/youtube/strings.xml | 2 + .../values-my-rMM/youtube/strings.xml | 1 + .../values-nb-rNO/youtube/strings.xml | 11 ++ .../values-ne-rIN/youtube/strings.xml | 1 + .../values-nl-rNL/youtube/strings.xml | 11 ++ .../values-or-rIN/youtube/strings.xml | 1 + .../values-pa-rIN/youtube/strings.xml | 1 + .../values-pl-rPL/youtube/strings.xml | 25 ++-- .../values-pt-rBR/youtube/strings.xml | 13 +- .../values-pt-rPT/youtube/strings.xml | 11 ++ .../values-ro-rRO/youtube/strings.xml | 11 ++ .../values-ru-rRU/youtube/strings.xml | 11 ++ .../values-si-rLK/youtube/strings.xml | 1 + .../values-sk-rSK/youtube/strings.xml | 11 ++ .../values-sl-rSI/youtube/strings.xml | 12 ++ .../values-sr-rCS/youtube/strings.xml | 11 ++ .../values-sr-rSP/youtube/strings.xml | 11 ++ .../values-sv-rSE/youtube/strings.xml | 11 ++ .../values-sw-rKE/youtube/strings.xml | 1 + .../values-ta-rIN/youtube/strings.xml | 1 + .../values-te-rIN/music/strings.xml | 5 + .../values-te-rIN/shared-youtube/strings.xml | 1 + .../values-te-rIN/shared/strings.xml | 4 + .../values-te-rIN/youtube/strings.xml | 15 +++ .../values-th-rTH/music/strings.xml | 2 +- .../values-th-rTH/reddit/strings.xml | 2 +- .../values-th-rTH/youtube/strings.xml | 111 ++++++++++-------- .../values-tr-rTR/youtube/strings.xml | 23 +++- .../values-uk-rUA/music/strings.xml | 4 +- .../values-uk-rUA/youtube/strings.xml | 45 ++++--- .../values-ur-rIN/youtube/strings.xml | 1 + .../values-uz-rUZ/youtube/strings.xml | 2 + .../values-vi-rVN/youtube/strings.xml | 11 ++ .../values-zh-rCN/youtube/strings.xml | 11 ++ .../values-zh-rTW/music/strings.xml | 6 +- .../values-zh-rTW/youtube/strings.xml | 13 +- .../values-zu-rZA/youtube/strings.xml | 1 + 98 files changed, 696 insertions(+), 115 deletions(-) diff --git a/patches/src/main/resources/addresources/values-af-rZA/youtube/strings.xml b/patches/src/main/resources/addresources/values-af-rZA/youtube/strings.xml index e363833d46..28b01a4f6d 100644 --- a/patches/src/main/resources/addresources/values-af-rZA/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-af-rZA/youtube/strings.xml @@ -454,6 +454,15 @@ Nadat 'n video met die Ask-knoppie een keer oopgemaak is, werk die funksie met a Wys voer-seunbeeldtelling Wys voer-seunbeeldkoers + + Soek kommentaar + Tik op die soekknoppie in die kommentaar om slegs die gelaaide kommentaar te wys wat al die gesoekte woorde bevat. Sit woorde in aanhalingstekens om vir \'n frase te soek + Soek kommentaar + Woorde om te soek + Soek + Klar + Verander die sorteervolgorde of maak die kommentaar weer oop om dit te soek + Soektog gewysig. Verander die sorteervolgorde of maak die kommentaar weer oop om alle kommentaar te wys Deaktiveer \'Aanmeld by TV\'-pop-up Deaktiveer die \'Koppel jou toestelle\'-pop-up @@ -838,6 +847,8 @@ Om Instellings toe te gaan, skakel die Instelling-knoppie in via Algemeen → We Keeps the navigation bar visible while scrolling Deaktiveer navigasieknoppies sonder etikette Wys die navigasieknoppies met etikette as YouTube hulle sonder etikette stuur + Deaktiveer werkbalk-navigasie-oortjies + Wys Inskrywings in die navigasiebalk, indien YouTube dit na \'n oortjie bo-aan Tuis skuif Aktiveer navigasiebalkanimasies Makes the status bar fully opaque Deaktiveer outomatiese verberg van navigasiebalk @@ -1291,6 +1302,7 @@ Video's word met 'n kort voorvoegsel van die gehashte video-ID aangevra, sodat d Videovertraging Vertrag die video sodat dit gesinkroniseer bly met vertraagde klank, soos Bluetooth-koptelefone (0-5000 ms) + Slegs vertraag met Bluetooth-oudio Sleutel slegs video terwyl Bluetooth-klank aangeskakel is, sodat die foonluidspreker in pas bly Wys Videovertraging-knoppie Tik en hou om die vertraging na 0 ms te herstel diff --git a/patches/src/main/resources/addresources/values-am-rET/youtube/strings.xml b/patches/src/main/resources/addresources/values-am-rET/youtube/strings.xml index d1c13bc476..174f25cd1a 100644 --- a/patches/src/main/resources/addresources/values-am-rET/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-am-rET/youtube/strings.xml @@ -64,6 +64,7 @@ Second \"item\" text" + diff --git a/patches/src/main/resources/addresources/values-ar-rSA/youtube/strings.xml b/patches/src/main/resources/addresources/values-ar-rSA/youtube/strings.xml index de03e53d10..5190fcf21f 100644 --- a/patches/src/main/resources/addresources/values-ar-rSA/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ar-rSA/youtube/strings.xml @@ -454,6 +454,15 @@ Second \"item\" text" عرض عدد الإعجابات في الصورة المصغرة بالخلاصة عرض نسبة الإعجابات في الصورة المصغرة بالخلاصة + + البحث في التعليقات + انقر على زر البحث في التعليقات لعرض التعليقات المحمَّلة فقط التي تحتوي على كل الكلمات التي تم البحث عنها. ضع الكلمات بين علامات الاقتباس للبحث عن عبارة محددة + البحث في التعليقات + كلمات للبحث عنها + البحث + مسح + قم بتغيير ترتيب الفرز أو أعد فتح التعليقات للبحث فيها + تم مسح البحث. قم بتغيير ترتيب الفرز أو افتح التعليقات مرة أخرى لعرض كل التعليقات تعطيل نافذة \'تسجيل الدخول إلى التلفزيون\' تعطيل نافذة \'توصيل أجهزتك\' @@ -842,6 +851,8 @@ Mistral Nemo هو الخيار الأرخص، بينما توفر Gemma 4 أعل يقلل المسافة الأفقية بين الأزرار تعطيل أزرار التنقل بدون تسميات يعرض أزرار التنقل مع تسميات، إذا أرسلها YouTube بدون تسميات + تعطيل علامات تبويب التنقل في شريط الأدوات + يعرض الاشتراكات في شريط التنقل، إذا قام YouTube بنقلها إلى علامة تبويب في أعلى الصفحة الرئيسية تمكين تأثير حركة التنقل في شريط التنقل يُفعّل الانتقالات المتحركة عند التبديل بين علامات التبويب تعطيل إخفاء شريط التنقل تلقائيًا diff --git a/patches/src/main/resources/addresources/values-as-rIN/youtube/strings.xml b/patches/src/main/resources/addresources/values-as-rIN/youtube/strings.xml index d1c13bc476..174f25cd1a 100644 --- a/patches/src/main/resources/addresources/values-as-rIN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-as-rIN/youtube/strings.xml @@ -64,6 +64,7 @@ Second \"item\" text" + diff --git a/patches/src/main/resources/addresources/values-az-rAZ/youtube/strings.xml b/patches/src/main/resources/addresources/values-az-rAZ/youtube/strings.xml index 97171c0564..94a264d76d 100644 --- a/patches/src/main/resources/addresources/values-az-rAZ/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-az-rAZ/youtube/strings.xml @@ -454,6 +454,15 @@ Məhdudiyyətlər: Axının miniatürünün bəyənmə sayını göstər Axının miniatürünün bəyənmə faiz nisbətini göstər + + Şərhləri axtar + Yalnız axtarılan sözlərin hamısını ehtiva edən yüklənilmiş şərhləri göstərmək üçün şərhlərdəki axtarış düyməsinə toxun. İfadəni axtarmaq üçün sözləri dırnaq arasında yaz + Şərhləri axtar + Axtarılacaq sözlər + Axtarış + Təmizlə + Onları axtarmaq üçün çeşidləmə sırasını dəyişin və ya şərhləri təkrar açın + Axtarış təmizləndi. Bütün şərhləri göstərmək üçün çeşidləmə sırasını dəyişdir və ya şərhləri təkrar aç \"TV-yə daxil ol\" ani pəncərəsini qapat \"Cihazlarınızı qoşun\" açılan pəncərəsini qapat @@ -838,6 +847,8 @@ Tənzimləmələrə daxil olmaq üçün Ümumi → Alət Cərgəsi→ Tənzimlə Düymələr arasında üfüqi boşluğu azaldır Etiketsiz fəaliyyət düymələrini qapat YouTube etiketsiz göndərirsə, fəaliyyət düymələrini etiketlərlə göstərir + Alət cərgəsi fəaliyyət səhifələrini qapat + YouTube onu Əsas Səhifənin yuxarısındakı vərəqə köçürərsə, fəaliyyət cərgəsində Abunəlikləri göstərir Fəaliyyət cərgəsi animasiyalarını aktivləşdir Tabellər arasında keçərkən keçidləri canlandırır Fəaliyyət cərgəsin avtomatik gizlətməsini qapat diff --git a/patches/src/main/resources/addresources/values-be-rBY/youtube/strings.xml b/patches/src/main/resources/addresources/values-be-rBY/youtube/strings.xml index a54a66d97a..d817afcf08 100644 --- a/patches/src/main/resources/addresources/values-be-rBY/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-be-rBY/youtube/strings.xml @@ -65,6 +65,9 @@ Second \"item\" text" + + Пошук + Ачысціць diff --git a/patches/src/main/resources/addresources/values-bg-rBG/youtube/strings.xml b/patches/src/main/resources/addresources/values-bg-rBG/youtube/strings.xml index b78056fbd7..41cb359be1 100644 --- a/patches/src/main/resources/addresources/values-bg-rBG/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-bg-rBG/youtube/strings.xml @@ -454,6 +454,15 @@ Second \"item\" text" Покажи броя на харесиите към миниатюрата във входа Покажи процентния дял на харесиите към миниатюрата във входа + + Търсене в коментарите + Докоснете бутона за търсене в коментарите, за да покажете само заредените коментари, които съдържат всички търсени думи. Поставете думите в кавички, за да потърсите фраза + Търсене в коментарите + Думи за търсене + Търсене + Изчистване + Променете реда на сортиране или отворете отново коментарите, за да ги потърсите + Търсенето е изчистено. Променете реда на сортиране или отворете коментарите отново, за да покажете всички коментари Изключване на попъпа \'Въведете се в ТВ\' Забранете изскачащия прозорец \'Свържете вашите устройства\' @@ -838,6 +847,8 @@ Second \"item\" text" Скрий менюто „Заключване на екрана“ Деактивиране на навигационните бутони без етикети Показва навигационните бутони с етикети, ако YouTube ги изпраща без етикети + Деактивирай навигационните раздели на лентата с инструменти + Показва \"Абонати\" в навигационната лента, ако YouTube ги премести в раздел в горната част на Начало Превключванията на навигацията са анимирани Анимира преходите при превключване между табовете Забрани автоматичното скриване на навигационната лента diff --git a/patches/src/main/resources/addresources/values-bn-rBD/youtube/strings.xml b/patches/src/main/resources/addresources/values-bn-rBD/youtube/strings.xml index e2a2af9b89..39c532313b 100644 --- a/patches/src/main/resources/addresources/values-bn-rBD/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-bn-rBD/youtube/strings.xml @@ -454,6 +454,15 @@ Second \"item\" text" ফিড থাম্বনেইলের লাইক কাউন্ট দেখান ফিড থাম্বনেইলের লাইক শতাংশ দেখান + + মন্তব্য অনুসন্ধান করুন + শুধুমাত্র লোড হওয়া সেই মন্তব্যগুলো দেখানোর জন্য মন্তব্যের সার্চ বাটনে ট্যাপ করুন যেগুলোতে অনুসন্ধান করা সমস্ত শব্দ রয়েছে। কোনো নির্দিষ্ট শব্দগুচ্ছ অনুসন্ধান করতে শব্দগুলোকে উদ্ধৃতি চিহ্নের মধ্যে রাখুন + মন্তব্য অনুসন্ধান করুন + অনুসন্ধান করার শব্দ + অনুসন্ধান + মুছে ফেলুন + সর্টিং অর্ডার পরিবর্তন করুন অথবা অনুসন্ধান করার জন্য পুনরায় মন্তব্যগুলো খুলুন + অনুসন্ধান মুছে ফেলা হয়েছে। সব মন্তব্য দেখাতে সর্টিং অর্ডার পরিবর্তন করুন অথবা মন্তব্যগুলি আবার খুলুন টিভিতে সাইন ইন পপআপ অক্ষম করুন ডিভাইস সংযোগের পপআপ নিষ্ক্রিয় করুন @@ -838,6 +847,8 @@ OpenRouter.ai ওপেন করতে এখানে ট্যাপ কর বাটনগুলির মধ্যে অনুভূমিক ব্যবধান কমিয়ে দেয় লেবেল ছাড়া নেভিগেশন বাটন নিষ্ক্রিয় করুন YouTube যদি নেভিগেশন বাটনগুলো লেবেল ছাড়া পাঠায়, তাহলে সেগুলো লেবেলসহ দেখায় + টুলবার নেভিগেশন ট্যাব নিষ্ক্রিয় করুন + যদি YouTube এটিকে হোমের উপরের একটি ট্যাবে সরিয়ে নেয়, তবে এটি নেভিগেশন বারে সাবস্ক্রিপশনগুলো দেখাবে নেভিগেশন বার অ্যানিমেশন সক্ষম করুন ট্যাবের মধ্যে পরিবর্তন করার সময় অ্যানিমেশন তৈরি করে স্বয়ংক্রিয়ভাবে নেভিগেশন বার লুকানো নিষ্ক্রিয় করুন diff --git a/patches/src/main/resources/addresources/values-bs-rBA/youtube/strings.xml b/patches/src/main/resources/addresources/values-bs-rBA/youtube/strings.xml index 5eed7a86be..6d1f52e7da 100644 --- a/patches/src/main/resources/addresources/values-bs-rBA/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-bs-rBA/youtube/strings.xml @@ -70,6 +70,7 @@ Second \"item\" text" + diff --git a/patches/src/main/resources/addresources/values-ca-rES/music/strings.xml b/patches/src/main/resources/addresources/values-ca-rES/music/strings.xml index e8dc3ba0f6..ad460303d9 100644 --- a/patches/src/main/resources/addresources/values-ca-rES/music/strings.xml +++ b/patches/src/main/resources/addresources/values-ca-rES/music/strings.xml @@ -18,6 +18,8 @@ Second \"item\" text" + Cancel·lar + Obrir @@ -25,6 +27,8 @@ Second \"item\" text" + Tanca + Eliminar Canal @@ -80,5 +84,6 @@ Second \"item\" text" Introdueix un títol de la cançó i un artista + Automàtic diff --git a/patches/src/main/resources/addresources/values-ca-rES/reddit/strings.xml b/patches/src/main/resources/addresources/values-ca-rES/reddit/strings.xml index 67cd49d12d..dce31d6240 100644 --- a/patches/src/main/resources/addresources/values-ca-rES/reddit/strings.xml +++ b/patches/src/main/resources/addresources/values-ca-rES/reddit/strings.xml @@ -19,5 +19,6 @@ Second \"item\" text" Publicitat Miscel·lània Quant a + Eliminar Desconegut diff --git a/patches/src/main/resources/addresources/values-ca-rES/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-ca-rES/shared-youtube/strings.xml index fd007eeb78..c0dc76b69f 100644 --- a/patches/src/main/resources/addresources/values-ca-rES/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ca-rES/shared-youtube/strings.xml @@ -18,6 +18,7 @@ Second \"item\" text" --> + Deshabilitat diff --git a/patches/src/main/resources/addresources/values-ca-rES/shared/strings.xml b/patches/src/main/resources/addresources/values-ca-rES/shared/strings.xml index 9b1d646965..2a1f9a739a 100644 --- a/patches/src/main/resources/addresources/values-ca-rES/shared/strings.xml +++ b/patches/src/main/resources/addresources/values-ca-rES/shared/strings.xml @@ -29,6 +29,8 @@ Second \"item\" text" Apagat + Activat + Eliminar diff --git a/patches/src/main/resources/addresources/values-ca-rES/youtube/strings.xml b/patches/src/main/resources/addresources/values-ca-rES/youtube/strings.xml index ecff179676..865396451f 100644 --- a/patches/src/main/resources/addresources/values-ca-rES/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ca-rES/youtube/strings.xml @@ -65,6 +65,7 @@ Second \"item\" text" + Eliminar @@ -77,6 +78,9 @@ Second \"item\" text" + + Cercar + Buida @@ -88,6 +92,7 @@ Second \"item\" text" + Eliminar @@ -179,6 +184,7 @@ Second \"item\" text" + Deshabilitat @@ -235,5 +241,7 @@ Second \"item\" text" + Automàtic + Cancel·lar diff --git a/patches/src/main/resources/addresources/values-ckb-rIR/youtube/strings.xml b/patches/src/main/resources/addresources/values-ckb-rIR/youtube/strings.xml index 0eefc162b5..bfb444b40b 100644 --- a/patches/src/main/resources/addresources/values-ckb-rIR/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ckb-rIR/youtube/strings.xml @@ -163,6 +163,8 @@ Second \"item\" text" + + گەڕان diff --git a/patches/src/main/resources/addresources/values-cs-rCZ/youtube/strings.xml b/patches/src/main/resources/addresources/values-cs-rCZ/youtube/strings.xml index 7c4bc649f7..a4c34fd1de 100644 --- a/patches/src/main/resources/addresources/values-cs-rCZ/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-cs-rCZ/youtube/strings.xml @@ -454,6 +454,15 @@ Po jednom otevření videa s tlačítkem Ask funguje funkce se všemi videi, i k Zobrazit počet líbků u miniatury kanálu Zobrazit procento líbků u miniatury kanálu + + Hledat v komentářích + Klepnutím na tlačítko vyhledávání v komentářích zobrazíte pouze načtené komentáře, které obsahují všechna hledaná slova. Pro hledání konkrétní fráze uveďte slova do uvozovek + Hledat v komentářích + Slova k vyhledání + Hledat + Vymazat + Změňte pořadí řazení nebo komentáře znovu otevřete, abyste v nich mohli vyhledávat + Vyhledávání bylo vymazáno. Změňte pořadí řazení nebo komentáře znovu otevřete, abyste zobrazili všechny komentáře Zakázat vyskakovací okno Přihlásit se k televizi Zakázat vyskakovací okno \'Připojit vaše zařízení\' @@ -836,6 +845,8 @@ Pro přístup k Nastavení povolte tlačítko Nastavení přes Obecné → Nást Zmenší vodorovné mezery mezi tlačítky Zakázat navigační tlačítka bez popisků Zobrazí navigační tlačítka s popisky, pokud je YouTube dodává bez popisků + Zakázat navigační karty v nástrojovém panelu + Zobrazí Odběry v navigační liště, pokud YouTube přesune tuto sekci do karty v horní části Domovské stránky Povolit animace navigační lišty Animuje přechody při přepínání mezi kartami Vypnout automatické skrývání navigační lišty diff --git a/patches/src/main/resources/addresources/values-da-rDK/youtube/strings.xml b/patches/src/main/resources/addresources/values-da-rDK/youtube/strings.xml index 6eb03316b6..5b5e0e34f2 100644 --- a/patches/src/main/resources/addresources/values-da-rDK/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-da-rDK/youtube/strings.xml @@ -452,6 +452,15 @@ Efter at en video med Ask-knappen er åbnet én gang, fungerer funktionen med al Vis antal likes på feed-tommbilledet Vis like-procent for feed-tommbilledet + + Søg i kommentarer + Tryk på søgeknappen i kommentarerne for kun at vise de indlæste kommentarer, der indeholder alle de søgte ord. Sæt ord i citationstegn for at søge efter en frase + Søg i kommentarer + Ord der skal søges efter + Søg + Ryd + Skift sorteringsrækkefølgen eller åbn kommentarerne igen for at søge i dem + Søgning ryddet. Skift sorteringsrækkefølgen eller åbn kommentarerne igen for at vise alle kommentarer Deaktiver \'Log ind på TV\' popup Deaktiver pop op-vinduet \'Tilslut dine enheder\' @@ -834,6 +843,8 @@ For at få adgang til Indstillinger skal du aktivere indstillingsknappen via Gen Reducerer den vandrette afstand mellem knapperne Deaktiver navigationsknapper uden etiketter Viser navigationsknapperne med etiketter, hvis YouTube sender dem uden etiketter + Deaktiver navigationstabs i værktøjslinjen + Viser abonnementer i navigationslinjen, hvis YouTube flytter dem til en fane øverst på Hjem Aktivér animationer i navigationslinjen Animerer overgange, når der skiftes mellem faner Deaktiver automatisk skjuling af navigationslinjen diff --git a/patches/src/main/resources/addresources/values-de-rDE/youtube/strings.xml b/patches/src/main/resources/addresources/values-de-rDE/youtube/strings.xml index dfe8c14e36..8a60850d01 100644 --- a/patches/src/main/resources/addresources/values-de-rDE/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-de-rDE/youtube/strings.xml @@ -455,6 +455,15 @@ Nachdem ein Video mit der Ask-Schaltfläche einmal geöffnet wurde, funktioniert Like-Anzahl in Feed-Vorschaubildern anzeigen Like-Verhältnis in Feed-Vorschaubildern anzeigen + + Kommentare durchsuchen + Tippen Sie in den Kommentaren auf die Suchschaltfläche, um nur die geladenen Kommentare anzuzeigen, die alle gesuchten Wörter enthalten. Setzen Sie Wörter in Anführungszeichen, um nach einer Phrase zu suchen + Kommentare durchsuchen + Zu suchende Wörter + Suchen + Löschen + Ändern Sie die Sortierreihenfolge oder öffnen Sie die Kommentare erneut, um sie zu durchsuchen + Suche gelöscht. Ändern Sie die Sortierreihenfolge oder öffnen Sie die Kommentare erneut, um alle Kommentare anzuzeigen Pop-up „Auf dem Fernseher anmelden“ deaktivieren \'Geräte verbinden\'-Pop-up deaktivieren @@ -839,6 +848,8 @@ Um auf Einstellungen zuzugreifen, aktivieren Sie die Schaltfläche „Einstellun Verringert den horizontalen Abstand zwischen den Schaltflächen Navigationsschaltflächen ohne Beschriftungen deaktivieren Zeigt die Navigationsschaltflächen mit Beschriftungen an, wenn YouTube sie ohne Beschriftungen sendet + Navigations-Tabs in der Symbolleiste deaktivieren + Zeigt Abonnements in der Navigationsleiste an, falls YouTube diese in einen Tab oben auf der Startseite verschiebt Navigation bar animations aktivieren Audio Track-Menü-Fußzeile ausblenden Auto-Ausblenden der Navigationsleiste deaktivieren diff --git a/patches/src/main/resources/addresources/values-el-rGR/youtube/strings.xml b/patches/src/main/resources/addresources/values-el-rGR/youtube/strings.xml index 3d56e71cc9..2a743ef856 100644 --- a/patches/src/main/resources/addresources/values-el-rGR/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-el-rGR/youtube/strings.xml @@ -454,6 +454,15 @@ Second \"item\" text" Εμφάνιση μετρητή likes για το thumbnail της feed Εμφάνιση ποσοστού likes για το thumbnail της feed + + Αναζήτηση σχολίων + Πατήστε το κουμπί αναζήτησης στα σχόλια για να εμφανίσετε μόνο τα φορτωμένα σχόλια που περιέχουν όλες τις λέξεις αναζήτησης. Τοποθετήστε τις λέξεις σε εισαγωγικά για να αναζητήσετε μια φράση + Αναζήτηση σχολίων + Λέξεις προς αναζήτηση + Αναζήτηση + Καθαρισμός + Αλλάξτε τη σειρά ταξινόμησης ή ανοίξτε ξανά τα σχόλια για να τα αναζητήσετε + Η αναζήτηση εκκαθαρίστηκε. Αλλάξτε τη σειρά ταξινόμησης ή ανοίξτε ξανά τα σχόλια για να εμφανίσετε όλα τα σχόλια Απενεργοποίηση αναδυόμενου παραθύρου σύνδεσης στην τηλεόραση Απενεργοποίηση αναδυτικού παραθύρου \'Συνδέστε τις συσκευές σας\' @@ -838,6 +847,8 @@ Second \"item\" text" Απόκρυψη κουμπιού Σύμπτυξης Απενεργοποίηση κουμπιών πλοήγησης χωρίς ετικέτες Εμφανίζει τα κουμπιά πλοήγησης με ετικέτες, εάν το YouTube τα στέλνει χωρίς ετικέτες + Απενεργοποίηση καρτελών πλοήγησης στη γραμμή εργαλείων + Εμφανίζει τις Συνδρομές στη γραμμή πλοήγησης, εάν το YouTube τις μεταφέρει σε μια καρτέλα στο πάνω μέρος της Αρχικής Ενεργοποίηση κινούμενων εικόνων στη γραμμή πλοήγησης Απόκρυψη κουμπιού Πλήρης οθόνη Απενεργοποίηση αυτόματης απόκρυψης της γραμμής πλοήγησης diff --git a/patches/src/main/resources/addresources/values-es-rES/youtube/strings.xml b/patches/src/main/resources/addresources/values-es-rES/youtube/strings.xml index ca091eb4d0..388a83bfff 100644 --- a/patches/src/main/resources/addresources/values-es-rES/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-es-rES/youtube/strings.xml @@ -454,6 +454,15 @@ Después de abrir por primera vez un vídeo con el botón «Preguntar», la func Mostrar miniatura del feed como recuento Mostrar miniatura del feed como porcentaje + + Buscar comentarios + Toca el botón de búsqueda en los comentarios para mostrar solo los comentarios cargados que contengan todas las palabras buscadas. Pon las palabras entre comillas para buscar una frase + Buscar comentarios + Palabras a buscar + Buscar + Limpiar + Cambia el orden de clasificación o abre los comentarios de nuevo para buscarlos + Búsqueda borrada. Cambia el orden de clasificación o abre los comentarios de nuevo para mostrar todos los comentarios Desactivar ventana emergente \'Iniciar sesión en TV\' Desactivar ventana emergente \'Conecta tus dispositivos\' @@ -838,6 +847,8 @@ Para acceder a los ajustes, activa el botón de ajustes a través de General → Reduce el espacio horizontal entre los botones Desactivar botones de navegación sin etiquetas Muestra los botones de navegación con etiquetas, si YouTube los envía sin etiquetas + Desactivar pestañas de navegación de la barra de herramientas + Muestra las Suscripciones en la barra de navegación, si YouTube las mueve a una pestaña en la parte superior de Inicio Activar animaciones de barra de navegación Anima las transiciones al cambiar entre pestañas Desactivar ocultación automática de la barra de navegación diff --git a/patches/src/main/resources/addresources/values-et-rEE/youtube/strings.xml b/patches/src/main/resources/addresources/values-et-rEE/youtube/strings.xml index d1c13bc476..174f25cd1a 100644 --- a/patches/src/main/resources/addresources/values-et-rEE/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-et-rEE/youtube/strings.xml @@ -64,6 +64,7 @@ Second \"item\" text" + diff --git a/patches/src/main/resources/addresources/values-eu-rES/youtube/strings.xml b/patches/src/main/resources/addresources/values-eu-rES/youtube/strings.xml index d1c13bc476..174f25cd1a 100644 --- a/patches/src/main/resources/addresources/values-eu-rES/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-eu-rES/youtube/strings.xml @@ -64,6 +64,7 @@ Second \"item\" text" + diff --git a/patches/src/main/resources/addresources/values-fa-rIR/youtube/strings.xml b/patches/src/main/resources/addresources/values-fa-rIR/youtube/strings.xml index 807bbf6e91..9653e93052 100644 --- a/patches/src/main/resources/addresources/values-fa-rIR/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-fa-rIR/youtube/strings.xml @@ -454,6 +454,15 @@ Second \"item\" text" نمایش تعداد لایک‌های بندانگشتی فید نمایش درصد لایک‌های بندانگشتی فید + + جست‌وجوی نظرات + برای نمایش نظرات بارگذاری‌شده‌ای که شامل تمام کلمات جست‌وجو شده هستند، روی دکمه جست‌وجو در نظرات ضربه بزنید. برای جست‌وجوی یک عبارت، کلمات را داخل علامت نقل‌قول قرار دهید + جست‌وجوی نظرات + کلمات مورد نظر برای جست‌وجو + جست‌وجو + پاک‌کردن + ترتیب مرتب‌سازی را تغییر دهید یا برای جست‌وجوی مجدد، نظرات را دوباره باز کنید + جست‌وجو پاک شد. برای نمایش تمام نظرات، ترتیب مرتب‌سازی را تغییر دهید یا نظرات را دوباره باز کنید غیرفعال‌کردن پنجرهٔ بازشوی «ورود به تلویزیون» غیرفعال‌کردن پنجرهٔ بازشوی «اتصال دستگاه‌ها» @@ -838,6 +847,8 @@ Second \"item\" text" فاصلهٔ افقی بین دکمه‌ها را کاهش می‌دهد غیرفعال کردن دکمه‌های ناوبری بدون برچسب اگر YouTube دکمه‌های ناوبری را بدون برچسب ارسال کند، آن‌ها را با برچسب نشان می‌دهد + غیرفعال‌کردن تب‌های پیمایش نوار ابزار + اگر YouTube بخش اشتراکات را به تبی در بالای صفحه اصلی منتقل کرد، این گزینه آن را در نوار پیمایش نمایش می‌دهد فعال‌کردن انیمیشن‌های نوار پیمایش هنگام جابه‌جایی بین برگه‌ها، انتقال‌ها را متحرک می‌کند غیرفعال‌کردن پنهان‌شدن خودکار نوار پیمایش diff --git a/patches/src/main/resources/addresources/values-fi-rFI/youtube/strings.xml b/patches/src/main/resources/addresources/values-fi-rFI/youtube/strings.xml index b609639fe5..3dd0e7ecb2 100644 --- a/patches/src/main/resources/addresources/values-fi-rFI/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-fi-rFI/youtube/strings.xml @@ -454,6 +454,15 @@ Kerran kun video \"Kysy\"-painikkeella avataan, ominaisuus toimii kaikkien video Näytä syötteen pikkukuvan tykkäysmäärä Näytä syötteen pikkukuvan tykkäysprosentti + + Hae kommenteista + Napauta kommenttien haku-painiketta näyttääksesi vain ladatut kommentit, jotka sisältävät kaikki hakusanat. Merkitse sanat lainausmerkeillä hakeaksesi tiettyä fraasia + Hae kommenteista + Hakusanat + Haku + Tyhjennä + Muuta järjestys tai avaa kommentit uudelleen hakeaksesi niistä + Haku tyhjennetty. Muuta järjestys tai avaa kommentit uudelleen näyttääksesi kaikki kommentit Poista käytöstä Kirjaudu televisioon -ponnahdusikkuna Poista \'Yhdistä laitteesi\' ponnahdusikkuna käytöstä @@ -838,6 +847,8 @@ Avataksesi asetukset, ota käyttöön Asetukset-painike kohdasta Yleiset → Ty Kapeuttaa pystysuoraa väliä painikkeiden välillä Poista käytöstä navigointipainikkeet ilman tekstejä Näyttää navigointipainikkeet teksteillä, jos YouTube lähettää ne ilman tekstejä + Poista työkalupalkin navigointivälilehdet käytöstä + Näyttää Tilaukset navigointipalkissa, jos YouTube siirtää sen Etusivun yläreunan välilehdeen Ota käyttöön navigaatiopalkin animaatiot Animoi siirtymät välilehtien välillä Poista automaattisen piilottamisen käytöstä navigointipalkissa diff --git a/patches/src/main/resources/addresources/values-fil-rPH/youtube/strings.xml b/patches/src/main/resources/addresources/values-fil-rPH/youtube/strings.xml index 90d9117182..319df30b4c 100644 --- a/patches/src/main/resources/addresources/values-fil-rPH/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-fil-rPH/youtube/strings.xml @@ -454,6 +454,15 @@ Pagkatapos buksan ang isang video na may Ask button, gagana ang feature sa lahat Ipakita ang bilang ng like sa thumbnail ng feed Ipakita ang porsyento ng like sa thumbnail ng feed + + Maghanap ng mga komento + Pindutin ang search button sa mga komento para ipakita lamang ang mga naiload na komento na naglalaman ng lahat ng mga hinahanap na salita. Maglagay ng mga salita sa quotes para maghanap ng isang parirala + Maghanap ng mga komento + Mga salitang hahanapin + Hanapin + Alisin + Baguhin ang pagkakasunod-sunod ng sort o buksan muli ang mga komento para hanapin ang mga ito + Nalinis na ang search. Baguhin ang pagkakasunod-sunod ng sort o buksan muli ang mga komento para ipakita ang lahat ng mga komento Patayin ang popup na \'Mag-sign in sa TV\' Huwag paganahin ang popup na \'Ikonekta ang iyong mga device\' @@ -838,6 +847,8 @@ Para i-access ang Settings, i-enable ang Settings button sa pamamagitan ng Gener Binabawasan ang pahalang na espasyo sa pagitan ng mga button I-disable ang mga navigation button na walang mga label Ipinapakita ang mga navigation button na may mga label, kung ipinapadala ng YouTube ang mga ito nang walang mga label + I-disable ang mga navigation tab ng toolbar + Ipinapakita ang mga Subscriptions sa navigation bar, kung ililipat ito ng YouTube sa isang tab sa itaas ng Home I-enable ang mga animasyon ng navigation bar Inaanimate ang mga transisyon kapag lumilipat sa pagitan ng mga tab I-disable ang auto-hiding navigation bar diff --git a/patches/src/main/resources/addresources/values-fr-rFR/youtube/strings.xml b/patches/src/main/resources/addresources/values-fr-rFR/youtube/strings.xml index 5b31477d14..66d0a9e5ce 100644 --- a/patches/src/main/resources/addresources/values-fr-rFR/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-fr-rFR/youtube/strings.xml @@ -436,24 +436,33 @@ Les signalements sont publics et sont examinés avant qu'une chaîne ne soit ajo Afficher le bouton \'À regarder plus tard\' sur les miniatures Un bouton \'À regarder plus tard\' est affiché sur les miniatures du flux. Un appui long dessus ouvre le menu \'Enregistrer dans une playlist\' - Ajouter l\'élément \'Demander\' au menu déroulant + Ajouter l\'élément \'Posez une question\' au menu déroulant Ajoute un élément de résumé vidéo au menu déroulant du flux - "'Demander' nécessite initialement un compte connecté dans un pays où 'Demander' est disponible. + "'Posez une question' nécessite initialement un compte connecté dans un pays où 'Posez une question' est disponible. -Après avoir ouvert une fois une vidéo avec le bouton 'Demander', la fonctionnalité fonctionne avec toutes les vidéos, même si le pays actuel ne dispose pas de la fonctionnalité 'Demander'." +Après avoir ouvert une fois une vidéo avec le bouton 'Posez une question', la fonctionnalité fonctionne avec toutes les vidéos, même si le pays actuel ne dispose pas de la fonctionnalité 'Posez une question'." Résumer la vidéo - Impossible d\'ouvrir \'Demander\' pour cette vidéo + Impossible d\'ouvrir \'Posez une question\' pour cette vidéo - \'Demander\' n\'est pas disponible pour votre compte ou dans votre pays actuel + \'Posez une question\' n\'est pas disponible pour votre compte ou dans votre pays actuel Afficher le nombre de vues sur les miniatures Le nombre de vues des vidéos du flux s\'affiche en bas à gauche de leurs miniatures au lieu d\'apparaître sous le titre. Certaines vidéos, notamment celles des étagères, peuvent ne pas afficher le nombre de vues Afficher le nombre j\'aime sur les miniatures du flux Afficher le pourcentage de j\'aime sur les miniatures du flux + + Rechercher des commentaires + Appuyez sur le bouton de recherche dans les commentaires pour afficher uniquement les commentaires déjà chargés qui contiennent tous les mots recherchés. Placez les mots entre guillemets pour rechercher une expression exacte + Rechercher des commentaires + Mots à rechercher + Rechercher + Effacer + Changez l\'ordre de tri ou rouvrez les commentaires pour effectuer une recherche + Recherche effacée. Changez l\"ordre de tri ou rouvrez les commentaires pour afficher tous les commentaires Désactiver le pop-up \'Connectez-vous à la TV\' Désactiver le pop-up \'Associez vos appareils\' @@ -838,6 +847,8 @@ Pour accéder aux paramètres, activez le bouton 'Paramètres' via Général → Réduit l\'espacement horizontal entre les boutons Désactiver les boutons de navigation sans libellés Affiche les boutons de navigation avec leurs libellés, si YouTube les envoie sans libellés + Désactiver les onglets de navigation de la barre d\'outils + Affiche \'Abonnements\' dans la barre de navigation, si YouTube les déplace vers un onglet en haut de \'Accueil\' Activer les animations de la barre de navigation Anime les transitions lors du passage d\'un onglet à un autre Désactiver le masquage automatique de la barre de navigation diff --git a/patches/src/main/resources/addresources/values-ga-rIE/youtube/strings.xml b/patches/src/main/resources/addresources/values-ga-rIE/youtube/strings.xml index 16b87e963d..78597d7166 100644 --- a/patches/src/main/resources/addresources/values-ga-rIE/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ga-rIE/youtube/strings.xml @@ -436,8 +436,8 @@ Tá tuairiscí poiblí agus déantar athbhreithniú orthu sula gcuirtear cainéa Taispeáin an cnaipe \'Féach níos déanaí\' ar mhionsamhla Tá cnaipe \'Féach níos déanaí\' le feiceáil ar mhionsamhla físeán sa fhotha. Má dhéanann tú sconna air agus é a choinneáil brúite, osclófar an rogha \'Sábháil chuig seinmliosta\' - Cuir mír Iarrtas sa roghchlár taobh - Cuireann sé mír achoimre físeáin leis an roghchlár taobh feidsiúil + Cuir an mhír Fiafraigh leis an roghchlár aníos + Cuireann sé mír achoimrithe físe leis an roghchlár aníos sa bhfotha "Gan ach cuntas logáilte istigh i dtír ina bhfuil Iarrtas ar fáil is gá chun Iarrtas a úsáid. @@ -447,13 +447,22 @@ Má oscailtí físeán a bhfuil cnaipe Iarrtas aige uair amháin, beidh an ghné Níorbh fhéidir Iarrtas a oscoinnt don fhíseán seo - Níl Iarrtras ar fáil d’do chuntas nó d’do thír reatha + Níl Ask ar fáil do do chuntas nó don tír ina bhfuil tú faoi láthair. Taispeáin líon na n-amharc ar mhionsamhlacha Taispeántar líon na n-amharc ar fhíseáin sa fhotha ag bun na láimhe clé dá mionsamhla, seachas faoin teideal. I gcás físeáin áirithe, ar nós iad siúd atá i seilfeanna, d’fhéadfadh sé nach dtaispeánfar líon na n-amharc. Taispeáin líon na n-aonán ar mhionsamhail sa bhfotha Taispeáin mionsamhail beatha cosúil le céatadán + + Cuardaigh tráchtanna + Tapáil ar an gcnaipe cuardaigh sna tráchtanna chun na tráchtanna luchtaithe atá ann ar féidir leo gach focal cuardaithe a bheith ann a thaispeáint. Cuir focail i gcomhrátaí chun frása a chuardach + Cuardaigh tráchtanna + Focail le cuardach + Cuardaigh + Glan + Athraigh ord na sórtála nó oscail na tráchtanna arís chun iad a chuardach + Slánaitheadh an cuardaigh. Athraigh ord na sórtála nó oscail na tráchtanna arís chun gach trácht a thaispeáint Díchumasaigh aníos \'Sínigh isteach ar an Teilifís\' Díchumasaigh an preab ‘Ceangail do ghléasanna’ @@ -838,6 +847,8 @@ Chun rochtain a fháil ar na Socruithe, cuir an cnaipe Socruithe ar chumas trí Laghdaíonn an spásáil chothrománach idir cnaipí Díchumasaigh cnaipí nascleanúna gan lipéid Taispeánann sé na cnaipí nascleanúna le lipéid, má sheolann YouTube iad gan lipéid + Díchumasaigh tabanna nascleanúna toolbar + Taispeánann Liostraí in ionad an barra nascleanúna, má bhogann YouTube é go tab ag barr an Phoist Cumasaigh beochana barra nascleanúna Beochan aistrithe agus tú ag aistriú idir cluaisíní Díchumasaigh barra nascleanúna uathoibríoch-fholaigh diff --git a/patches/src/main/resources/addresources/values-gl-rES/music/strings.xml b/patches/src/main/resources/addresources/values-gl-rES/music/strings.xml index 1de493c7af..4b87168f19 100644 --- a/patches/src/main/resources/addresources/values-gl-rES/music/strings.xml +++ b/patches/src/main/resources/addresources/values-gl-rES/music/strings.xml @@ -21,6 +21,7 @@ Second \"item\" text" Cancelar Detalles + Abrir Logotipo da cabeceira @@ -31,10 +32,13 @@ Second \"item\" text" + Pechar + Eliminar Acerca de Anuncios + Fuente Xeral Reprodutor Miscelánea @@ -42,6 +46,7 @@ Second \"item\" text" Ocultar etiqueta \'Obter Music Premium\' Ocultar anuncios de vídeo + Ocultar baldas horizontais @@ -53,6 +58,7 @@ Second \"item\" text" + Agochar o botón «Reprodución do chat en directo» @@ -96,6 +102,7 @@ Second \"item\" text" + Gardado Automático diff --git a/patches/src/main/resources/addresources/values-gl-rES/reddit/strings.xml b/patches/src/main/resources/addresources/values-gl-rES/reddit/strings.xml index 18c0449acf..65bb5f74d1 100644 --- a/patches/src/main/resources/addresources/values-gl-rES/reddit/strings.xml +++ b/patches/src/main/resources/addresources/values-gl-rES/reddit/strings.xml @@ -45,4 +45,5 @@ Second \"item\" text" Restablecer Eliminar Predefinido + Descoñecido diff --git a/patches/src/main/resources/addresources/values-gl-rES/shared/strings.xml b/patches/src/main/resources/addresources/values-gl-rES/shared/strings.xml index 67857dc752..05165c78ac 100644 --- a/patches/src/main/resources/addresources/values-gl-rES/shared/strings.xml +++ b/patches/src/main/resources/addresources/values-gl-rES/shared/strings.xml @@ -65,6 +65,7 @@ Second \"item\" text" 2022 ‑ 2025 2022 ‑ 2025 2019 ‑ 2022 + Cargando… Depuración Mostrar mensaxe toast en caso de erro de Morphe diff --git a/patches/src/main/resources/addresources/values-gl-rES/sponsorblock/strings.xml b/patches/src/main/resources/addresources/values-gl-rES/sponsorblock/strings.xml index 9d2d4ab7d5..70c7c39b36 100644 --- a/patches/src/main/resources/addresources/values-gl-rES/sponsorblock/strings.xml +++ b/patches/src/main/resources/addresources/values-gl-rES/sponsorblock/strings.xml @@ -19,4 +19,5 @@ Second \"item\" text" + Cargando… diff --git a/patches/src/main/resources/addresources/values-gl-rES/youtube/strings.xml b/patches/src/main/resources/addresources/values-gl-rES/youtube/strings.xml index 3740f18480..401ac81534 100644 --- a/patches/src/main/resources/addresources/values-gl-rES/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-gl-rES/youtube/strings.xml @@ -178,6 +178,9 @@ Os cambios inclúen: + + Busca + Limpar diff --git a/patches/src/main/resources/addresources/values-gu-rIN/youtube/strings.xml b/patches/src/main/resources/addresources/values-gu-rIN/youtube/strings.xml index 4b1d89c814..bd048ac1ca 100644 --- a/patches/src/main/resources/addresources/values-gu-rIN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-gu-rIN/youtube/strings.xml @@ -64,6 +64,8 @@ Second \"item\" text" + + શોધો diff --git a/patches/src/main/resources/addresources/values-hi-rIN/youtube/strings.xml b/patches/src/main/resources/addresources/values-hi-rIN/youtube/strings.xml index 7023343af8..88e2286587 100644 --- a/patches/src/main/resources/addresources/values-hi-rIN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-hi-rIN/youtube/strings.xml @@ -454,6 +454,15 @@ Second \"item\" text" फ़ीड थंबनेल लाइक count दिखाएं फ़ीड थंबनेल लाइक प्रतिशत दिखाएं + + कमेंट्स खोजें + केवल उन लोड किए गए कमेंट्स को दिखाने के लिए कमेंट्स में सर्च बटन पर टैप करें जिनमें खोजे गए सभी शब्द हों। किसी वाक्यांश को खोजने के लिए शब्दों को उद्धरण चिह्नों में रखें + कमेंट्स खोजें + खोजने के लिए शब्द + खोजें + साफ़ करें + सॉर्ट क्रम बदलें या उन्हें खोजने के लिए कमेंट्स को फिर से खोलें + खोज हटा दी गई है। सभी कमेंट्स दिखाने के लिए सॉर्ट क्रम बदलें या कमेंट्स को फिर से खोलें \'टीवी पर साइन इन करें\' पॉपअप बंद करें \'कनेक्ट आपके डिवाइस\' पॉपअप अक्षम करें @@ -838,6 +847,8 @@ OpenRouter.ai खोलने के लिए यहां टैप करे स्क्रॉल करते समय नेविगेशन बार को दृश्यमान रखता है बिना लेबल वाले नेविगेशन बटनों को अक्षम करें यदि YouTube उन्हें बिना लेबल भेजता है, तो नेविगेशन बटनों को लेबल के साथ दिखाता है + टूलबार नेविगेशन टैब अक्षम करें + यदि YouTube इसे होम के शीर्ष पर एक टैब में ले जाता है, तो नेविगेशन बार में सब्सक्रिप्शन दिखाएं नेविगेशन बार एनिमेशन सक्षम करें स्टेटस बार को पूरी तरह से अपारदर्शी बनाता है ऑटो-हाइडिंग नेविगेशन बार को निष्क्रिय करें diff --git a/patches/src/main/resources/addresources/values-hr-rHR/youtube/strings.xml b/patches/src/main/resources/addresources/values-hr-rHR/youtube/strings.xml index 112d4290c8..a1df1723fe 100644 --- a/patches/src/main/resources/addresources/values-hr-rHR/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-hr-rHR/youtube/strings.xml @@ -444,6 +444,15 @@ Nakon što se jednom otvori video s gumbom Ask, funkcija radi sa svim videima, Prikaži broj lajkova na minijaturi feeda Prikaži omjer lajkova na minijaturi feeda + + Pretraži komentare + Dodirnite gumb za pretraživanje u komentarima kako biste prikazali samo učitane komentare koji sadrže sve pretraživane riječi. Riječi stavite pod navodnike kako biste pretražili frazu + Pretraži komentare + Riječi za pretraživanje + Traži + Izbriši + Promijenite redoslijed sortiranja ili ponovno otvorite komentare kako biste ih pretražili + Pretraživanje je obrisano. Promijenite redoslijed sortiranja ili ponovno otvorite komentare kako biste prikazali sve komentare Onemogući \'Prijava na TV\' prozor Onemogući iskačući prozor \'Povežite svoje uređaje\' @@ -826,6 +835,8 @@ Da biste to popravili, uključite \"Lažiranje video streamova\" i promijenite \ Smanjuje vodoravni razmak između gumba Onemogući navigacijske gumbe bez natpisa Prikazuje navigacijske gumbe s natpisima ako ih YouTube šalje bez natpisa + Onemogući navigacijske kartice u alatnoj traci + Prikazuje Preplate u navigacijskoj traci ako YouTube pomakne taj odjeljak u karticu na vrhu Početne stranice Animacije na navigacijskoj traci su animirane Animira prijelaze pri prebacivanju između kartica Onemogući automatsko skrivanje navigacijske trake diff --git a/patches/src/main/resources/addresources/values-hu-rHU/youtube/strings.xml b/patches/src/main/resources/addresources/values-hu-rHU/youtube/strings.xml index 99ac0b2d9e..1b24044011 100644 --- a/patches/src/main/resources/addresources/values-hu-rHU/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-hu-rHU/youtube/strings.xml @@ -454,6 +454,15 @@ Egy alkalommal megnyitott, Ask gombbal ellátott videó esetén a funkció minde Videóelképzelés kedvelési száma megjelenítése Videóelképzelés kedvelési arányának százalékos megjelenítése + + Hozzászólások keresése + Koppintson a kereső gombra a hozzászólások között, hogy csak a betöltött, és az összes keresett szót tartalmazó hozzászólások jelenjenek meg. A pontos kifejezések kereséséhez tegye a szavakat idézőjelek közé + Hozzászólások keresése + Keresett szavak + Keresés + Törlés + Módosítsa a rendezési sorrendet, vagy nyissa újra a hozzászólásokat a kereséshez + Keresés törölve. Módosítsa a rendezési sorrendet, vagy nyissa újra a hozzászólásokat az összes hozzászólás megjelenítéséhez TV-bejelentkezési előugró ablak letiltása A \"Csatlakoztassa eszközeit\" felugró ablak letiltása @@ -838,6 +847,8 @@ A Beállítások eléréséhez engedélyezze a Beállítások gombot az Általá Csökkenti a gombok közötti vízszintes távolságot Címkék nélküli navigációs gombok letiltása Megjeleníti a navigációs gombokat címkékkel, ha a YouTube címkék nélkül küldi azokat + Toolbar navigációs lapok kikapcsolása + Megjeleníti az Előfizetéseket a navigációs sávban, ha a YouTube lapra przenti őket a Kezdőlap tetejére Navigációs sáv animációinak engedélyezése Animálja az átmeneteket a lapok között váltáskor Kapcsold ki az automatikus elrejtést a navigációs sávra diff --git a/patches/src/main/resources/addresources/values-hy-rAM/youtube/strings.xml b/patches/src/main/resources/addresources/values-hy-rAM/youtube/strings.xml index d1c13bc476..174f25cd1a 100644 --- a/patches/src/main/resources/addresources/values-hy-rAM/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-hy-rAM/youtube/strings.xml @@ -64,6 +64,7 @@ Second \"item\" text" + diff --git a/patches/src/main/resources/addresources/values-in-rID/youtube/strings.xml b/patches/src/main/resources/addresources/values-in-rID/youtube/strings.xml index 8638d18cef..015ee0ed7e 100644 --- a/patches/src/main/resources/addresources/values-in-rID/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-in-rID/youtube/strings.xml @@ -454,6 +454,15 @@ Setelah video dengan tombol Ask dibuka satu kali, fitur ini berfungsi untuk semu Tampilkan jumlah suka pada thumbnail feed Tampilkan persentase suka pada thumbnail feed + + Cari komentar + Ketuk tombol cari di komentar untuk menampilkan hanya komentar yang dimuat yang mengandung semua kata yang dicari. Letakkan kata dalam tanda kutip untuk mencari frasa + Cari komentar + Kata yang ingin dicari + Telusuri + Hapus + Ubah urutan pengurutan atau buka kembali komentar untuk mencarinya + Pencarian dihapus. Ubah urutan pengurutan atau buka kembali komentar untuk menampilkan semua komentar Nonaktifkan pesan timbul \'Masuk ke TV\' Nonaktifkan popup \'Hubungkan perangkat Anda\' @@ -838,6 +847,8 @@ Untuk mengakses Setelan, aktifkan tombol Setelan melalui Umum → Bilah Alat → Mengurangi jarak horizontal antara tombol Nonaktifkan tombol navigasi tanpa label Menampilkan tombol navigasi dengan label, jika YouTube mengirimkannya tanpa label + Nonaktifkan tab navigasi toolbar + Menampilkan Langganan di bilah navigasi, jika YouTube memindahkannya ke tab di bagian atas Beranda Aktifkan animasi bilah navigasi Menganimasikan transisi saat beralih antar tab Nonaktifkan otomatis menyembunyikan bilah navigasi diff --git a/patches/src/main/resources/addresources/values-is-rIS/youtube/strings.xml b/patches/src/main/resources/addresources/values-is-rIS/youtube/strings.xml index 705de340d1..1cf07d340a 100644 --- a/patches/src/main/resources/addresources/values-is-rIS/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-is-rIS/youtube/strings.xml @@ -444,6 +444,15 @@ Eftir að myndband með Ask hnappi hefur verið opnað einu sinni, virkar eiginl Sýna einkunn einskorðunar viðmyndar fæðis Sýna hlutfall einskorðunar viðmyndar fæðis + + Leita í athugasmdum + Ýttu á leitunarhnappinn í athugasemdum til að sýna aðeins hlaðnar athugasemdir sem innihalda öll leitarlitin. Settu orð í gegnsemi til að leita að setningu + Leita í athugasmdum + Leitaorð + Leita + Hreinsa + Breyttu raðgreiningu eða opnaðu athugasemdirnar aftur til að leita í þeim + Leit hreinsuð. Breyttu raðgreiningu eða opnaðu athugasemdirnar aftur til að sýna allar athugasemdir Gerðu ekki birtu „Innskráningu á sjónvarpi“ á popup glugganum Gerðu \'Tengdu tækin þín\' kynningarglugga óvirkan @@ -824,6 +833,8 @@ Til að laga þetta skaltu kveikja á 'Spoofa myndstreymi' og breyta 'Sjálfgefn Minnkar horna fjarlægð milli hnappa Slökkva á flakkhnöppum án merkimiða Sýnir flakkhnappana með merkimiðum, ef YouTube sendir þá án merkimiða + Óvirkja vísbendingar í tólbúnaðstængslum + Sýnir áskriftir í vistræðisstængslum ef YouTube færir þær í flipa efst á heimasíðu Virkja stýrihnappa hreyfingu Hreyfir umhverfi þegar skipt er á milli flipa Lokað sjálfvirkum hópum diff --git a/patches/src/main/resources/addresources/values-it-rIT/youtube/strings.xml b/patches/src/main/resources/addresources/values-it-rIT/youtube/strings.xml index 1df3e71c67..9686658474 100644 --- a/patches/src/main/resources/addresources/values-it-rIT/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-it-rIT/youtube/strings.xml @@ -454,6 +454,15 @@ Dopo aver aperto una volta un video con il pulsante Chiedi, la funzionalità fun Mostra il conteggio dei mi piace nella miniature del feed Mostra la percentuale di mi piace nella miniature del feed + + Cerca nei commenti + Tocca il pulsante di ricerca nei commenti per mostrare solo i commenti caricati che contengono tutte le parole cercate. Inserisci le parole tra virgolette per cercare una frase + Cerca nei commenti + Parole da cercare + Cerca + Disattiva + Cambia l\'ordine di ordinamento o apri nuovamente i commenti per cercarli + Ricerca cancellata. Cambia l\'ordine di ordinamento o apri nuovamente i commenti per mostrare tutti i commenti Disabilita il popup \"Accedi alla TV\" Disattiva il popup \"Connetti i tuoi dispositivi\" @@ -838,6 +847,8 @@ Per accedere alle Impostazioni, abilita il pulsante delle Impostazioni tramite G Riduce lo spazio orizzontale tra i pulsanti Disattiva i pulsanti di navigazione senza etichette Mostra i pulsanti di navigazione con etichette, se YouTube li invia senza etichette + Disabilita le schede di navigazione della barra degli strumenti + Mostra gli Iscritti nella barra di navigazione, se YouTube li sposta in una scheda nella parte superiore della Home Abilita animazioni della barra di navigazione Anima le transizioni quando si passa tra le schede Disabilita la barra di navigazione a scomparsa automatica diff --git a/patches/src/main/resources/addresources/values-iw-rIL/youtube/strings.xml b/patches/src/main/resources/addresources/values-iw-rIL/youtube/strings.xml index 5f42c00920..57ccd4e134 100644 --- a/patches/src/main/resources/addresources/values-iw-rIL/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-iw-rIL/youtube/strings.xml @@ -454,6 +454,15 @@ Second \"item\" text" הצג מניין לייקים בתמונה מוקטנת של הזרם הצג אחוז לייקים בתמונה מוקטנת של הזרם + + חיפוש תגובות + הקישו על כפתור החיפוש בתגובות כדי להציג רק את התגובות שנטענו ומכילות את כל המילים שחפשתם. הקיפו מילים במרכאות כדי לחפש ביטוי + חיפוש תגובות + מילים לחיפוש + חיפוש + נקה + שנו את סדר המיון או פתחו שוב את התגובות כדי לחפש בהן + החיפוש נוקה. שנו את סדר המיון או פתחו שוב את התגובות כדי להציג את כל התגובות השבת חלון קופץ של כניסה לטלוויזיה השבת את ה-popup של \'חבר את המכשירים שלך\' @@ -838,6 +847,8 @@ Second \"item\" text" מצמצם את המרווח האופקי בין הכפתורים השבת לחצני ניווט ללא תוויות מציג את לחצני הניווט עם תוויות, אם YouTube שולח אותם ללא תוויות + בטל לשוניות ניווט בסרגל הכלים + מציג את \"מנויים\" בסרגל הניווט, אם YouTube מעביר זאת ללשונית בחלק העליון של דף הבית הפעלת אנימציות של שורת הניווט מבצע אנימציות מעברים בעת מעבר בין כרטיסיות השבת הסתרת אוטומטית של סרגל הניווט diff --git a/patches/src/main/resources/addresources/values-ja-rJP/music/strings.xml b/patches/src/main/resources/addresources/values-ja-rJP/music/strings.xml index a317eba7f3..73479e0a23 100644 --- a/patches/src/main/resources/addresources/values-ja-rJP/music/strings.xml +++ b/patches/src/main/resources/addresources/values-ja-rJP/music/strings.xml @@ -537,8 +537,8 @@ SABR再生(動画ストリームのスプーフがオフ)" 曲名とアーティストを入力してください 有効なプロバイダーのどれも、他の曲を検索できません 保存しました - 同期された単語または音節をハイライト - 利用可能な場合は単語ごとの歌詞を優先します。歌詞に単語タイミングがある場合、音楽に合わせて各単語または音節をハイライトします + 同期中の単語や音節をハイライト表示 + 利用可能な場合は単語ごとのタイミング情報を含む歌詞を優先します。単語ごとのタイミング情報を含む歌詞を使用する場合は、楽曲に合わせて各単語または音節をハイライト表示します バックコーラスを分離して表示 「(Oh) I want you」のように括弧で囲まれたバックコーラスを、歌詞の行から分離し、専用の独立した行に薄く表示します 再生済みの歌詞を非表示 @@ -555,9 +555,9 @@ SABR再生(動画ストリームのスプーフがオフ)" メタデータの正規表現 曲名、アーティスト、アルバムに適用される正規表現です。歌詞の検索前と、2つの曲名が一致するかどうかの判定時に使用されます。空にすると両方が無効になります。デフォルトでは、\"Official Video\"、\"Remaster\"、\"【插曲】\"、\"- Topic\" などのよくある不要な部分を除去します。 歌詞フィルタの正規表現 - 歌詞フィルターで正規表現を使用する - 歌詞フィルターで正規表現を使用する - 歌詞フィルターの正規表現 + 元の歌詞テキストのみに適用される正規表現です(翻訳やローマ字表記には適用されません)。無効にする場合は空欄にしてください。 + 情報行のマーカー + 歌詞が誤って下部に振り分けられてしまう場合は、この設定を調整または消去してください。マーカーはカンマ区切りで指定し、マーカーの直後にコロン : が続く行を下部のクレジット情報として表示します。無効にする場合は空欄にしてください 文字サイズ 歌詞の文字サイズ 全体のタイミングオフセット @@ -610,10 +610,10 @@ SABR再生(動画ストリームのスプーフがオフ)" 歌詞はMorpheとは関係のない、後援も認可もされていないサードパーティサービスから提供されています。それぞれは現状で使用されます:歌詞、その利用可能性、ライセンスはそれらのサービスおよびその許諾者に留まり、それらを使用することはサービスの利用規約の対象となる場合があります。有効化されたサービスに送信されるのは、あなたが要求した検索情報だけです - タイトル、アーティスト、そして必要に応じてビデオID。任意のAIエンドポイントとQuickLRC文字起こしは設定後にのみ実行され、歌詞テキストまたはトラックのビデオURLを受信します。" 使用中のプロバイダーのみ有効にするか、デフォルトのままにしてください。定期的なリクエストにより小規模なコミュニティの歌詞サイトが overload する可能性があります - この歌詞を編集 - この歌詞を編集 - この歌詞を編集 - この歌詞を編集 + クレジット行を非表示 + 歌詞欄最下部の、作詞作曲などのクレジット情報を表示する行を非表示にします + 翻訳とローマ字表記を入れ替える + オン: 歌詞の下にローマ字表記を、上に翻訳を表示します。オフ: 歌詞の下に翻訳を、上にローマ字表記を表示します OpenAI互換API AI翻訳 プロバイダー提供の翻訳を優先し、次にOpenAI互換モデル、最後にGoogle翻訳を使用します diff --git a/patches/src/main/resources/addresources/values-ja-rJP/youtube/strings.xml b/patches/src/main/resources/addresources/values-ja-rJP/youtube/strings.xml index f1ee0c57b1..b45c3079b2 100644 --- a/patches/src/main/resources/addresources/values-ja-rJP/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ja-rJP/youtube/strings.xml @@ -432,21 +432,21 @@ AI について扱っているだけのチャンネルや、動画の一部に A コメントを長押しするとコピーできます コメントをクリップボードにコピーしました - 長押ししてビデオメニューを開きます - フィード動画の任意の場所をタップして長押しすると、その動画のメニューが開きます - サムネイルに「後で見る」ボタンを表示する + 長押しで動画のメニューを開く + フィードの動画をどこでもよいので長押しすると、フライアウト メニューを開きます + サムネイルに「後で見る」ボタンを表示 フィード動画のサムネイルに「後で見る」ボタンが表示され、それをタップして長押しすると「プレイリストに保存」が開きます - Ask アイテムをフライアウト メニューに追加 - フィードのフライアウト メニューに動画要約アイテムを追加 + 「動画を要約して」をフライアウト メニューに追加 + 「この動画について質問する」欄を、欄内の「動画を要約して」がタップされた状態で開く項目を、フィードのフライアウト メニューに追加します "Ask は、利用可能な国でサインイン済みのアカウントが必要です。 「Ask」ボタン付きの動画を一度開くと、現在の国に Ask 機能がなくても、すべての動画で機能します。" - 動画を要約 + 動画を要約して - この動画の Ask を開けませんでした + 「この動画について質問する」欄を開けませんでした お使いのアカウントまたは現在地では Ask が利用できません @@ -455,6 +455,15 @@ AI について扱っているだけのチャンネルや、動画の一部に A フィードのサムネイルのいいね数を表示 フィードのサムネイルのいいね比率を表示 + + コメントを検索 + コメントにある検索ボタンをタップすると、検索ワードをすべて含む読み込み済みのコメントのみが表示されます。フレーズを検索するには、単語を引用符で囲んでください + コメントを検索 + 検索する単語 + 検索 + 解除 + 並べ替え順序を変更するか、再度コメントを開いて検索してください + 検索をクリアしました。すべてのコメントを表示するには、並べ替え順序を変更するか、再度コメントを開いてください 「テレビでログイン」ポップアップを無効化 「デバイスの接続」ポップアップを無効化 @@ -807,7 +816,7 @@ APIキーは決して共有しないでください。アクセス権を持つ 文字起こしの不具合を修正 文字起こしの読み込み失敗の原因となる前提条件チェックのエラーを修正します - ピクチャーインピクチャーで字幕を表示 + ピクチャー イン ピクチャーで字幕を表示 ナビゲーション ボタン 画面最下部のバーであるナビゲーション バーに表示するボタンを設定します @@ -839,6 +848,8 @@ APIキーは決して共有しないでください。アクセス権を持つ ナビゲーション ボタンの間隔を詰めて表示します ラベルのないナビゲーションボタンを無効にする YouTubeがラベルなしで送信する場合、ナビゲーションボタンをラベル付きで表示します + ナビゲーション ボタンの最上部への移動を無効化 + YouTube が「登録チャンネル」ボタンをホームの最上部に移動するレイアウトを有効にした場合に、このレイアウトを無効にし、最下部のナビゲーション バーに戻します ナビゲーション バーのアニメーションを有効化 タブの切り替えをアニメーションで表現します ナビゲーション バーの自動非表示を無効化 diff --git a/patches/src/main/resources/addresources/values-ka-rGE/music/strings.xml b/patches/src/main/resources/addresources/values-ka-rGE/music/strings.xml index ccfea4ed71..0c2cf6832f 100644 --- a/patches/src/main/resources/addresources/values-ka-rGE/music/strings.xml +++ b/patches/src/main/resources/addresources/values-ka-rGE/music/strings.xml @@ -25,6 +25,7 @@ Second \"item\" text" + ჩამოტვირთულები diff --git a/patches/src/main/resources/addresources/values-ka-rGE/reddit/strings.xml b/patches/src/main/resources/addresources/values-ka-rGE/reddit/strings.xml index cf7776fdaf..433a84e61b 100644 --- a/patches/src/main/resources/addresources/values-ka-rGE/reddit/strings.xml +++ b/patches/src/main/resources/addresources/values-ka-rGE/reddit/strings.xml @@ -15,4 +15,7 @@ Second \"item\" text" --> - + + ნაგულისხმები + ნაგულისხმები + diff --git a/patches/src/main/resources/addresources/values-ka-rGE/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-ka-rGE/shared-youtube/strings.xml index 29cef6194b..7c826093c6 100644 --- a/patches/src/main/resources/addresources/values-ka-rGE/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ka-rGE/shared-youtube/strings.xml @@ -58,7 +58,9 @@ Second \"item\" text" + ნაგულისხმები + ნაგულისხმები diff --git a/patches/src/main/resources/addresources/values-ka-rGE/shared/strings.xml b/patches/src/main/resources/addresources/values-ka-rGE/shared/strings.xml index 31cdbe4dcb..c0dfadb4dd 100644 --- a/patches/src/main/resources/addresources/values-ka-rGE/shared/strings.xml +++ b/patches/src/main/resources/addresources/values-ka-rGE/shared/strings.xml @@ -18,6 +18,7 @@ Second \"item\" text" + ნაგულისხმები diff --git a/patches/src/main/resources/addresources/values-ka-rGE/youtube/strings.xml b/patches/src/main/resources/addresources/values-ka-rGE/youtube/strings.xml index d1c13bc476..a74924e0d8 100644 --- a/patches/src/main/resources/addresources/values-ka-rGE/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ka-rGE/youtube/strings.xml @@ -64,6 +64,7 @@ Second \"item\" text" + @@ -161,6 +162,7 @@ Second \"item\" text" + ნაგულისხმები @@ -181,6 +183,7 @@ Second \"item\" text" + ნაგულისხმები @@ -201,6 +204,7 @@ Second \"item\" text" + ნაგულისხმები @@ -210,6 +214,7 @@ Second \"item\" text" + ნაგულისხმები @@ -219,5 +224,6 @@ Second \"item\" text" + ნაგულისხმები diff --git a/patches/src/main/resources/addresources/values-kk-rKZ/youtube/strings.xml b/patches/src/main/resources/addresources/values-kk-rKZ/youtube/strings.xml index d1c13bc476..174f25cd1a 100644 --- a/patches/src/main/resources/addresources/values-kk-rKZ/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-kk-rKZ/youtube/strings.xml @@ -64,6 +64,7 @@ Second \"item\" text" + diff --git a/patches/src/main/resources/addresources/values-km-rKH/youtube/strings.xml b/patches/src/main/resources/addresources/values-km-rKH/youtube/strings.xml index cb7ecc4ed1..9111351c42 100644 --- a/patches/src/main/resources/addresources/values-km-rKH/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-km-rKH/youtube/strings.xml @@ -77,6 +77,7 @@ Second \"item\" text" + diff --git a/patches/src/main/resources/addresources/values-kmr-rTR/youtube/strings.xml b/patches/src/main/resources/addresources/values-kmr-rTR/youtube/strings.xml index 2a97b99552..f061257f32 100644 --- a/patches/src/main/resources/addresources/values-kmr-rTR/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-kmr-rTR/youtube/strings.xml @@ -339,6 +339,7 @@ Sînorkirin: + \'Nêzîk kirin li TV\'ê\' asteng bikin Devreyê \'Cihazên te pêvekirin\' pop-up\'ê neçalak bike diff --git a/patches/src/main/resources/addresources/values-kn-rIN/youtube/strings.xml b/patches/src/main/resources/addresources/values-kn-rIN/youtube/strings.xml index 91b0250f8b..22203a72b0 100644 --- a/patches/src/main/resources/addresources/values-kn-rIN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-kn-rIN/youtube/strings.xml @@ -64,6 +64,7 @@ Second \"item\" text" + diff --git a/patches/src/main/resources/addresources/values-ko-rKR/youtube/strings.xml b/patches/src/main/resources/addresources/values-ko-rKR/youtube/strings.xml index 1c9a30d04e..476e6fb507 100644 --- a/patches/src/main/resources/addresources/values-ko-rKR/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ko-rKR/youtube/strings.xml @@ -454,6 +454,15 @@ Second \"item\" text" 피드 썸네일 좋아요 수 표시하기 피드 썸네일 좋아요 비율 표시하기 + + 댓글 검색 + 댓글의 검색 버튼을 탭하면 검색어의 모든 단어가 포함된 로드된 댓글만 표시됩니다. 구문 검색을 하려면 단어를 큰따옴표로 묶으세요 + 댓글 검색 + 검색할 단어 + 검색 + 지우기 + 정렬 순서를 변경하거나 댓글을 다시 열어 검색하세요 + 검색이 지워졌습니다. 정렬 순서를 변경하거나 댓글을 다시 열어 모든 댓글을 표시하세요 \'TV에 로그인\' 팝업 비활성화하기 \'기기 연결\' 팝업 비활성화하기 @@ -840,6 +849,8 @@ API 키는 절대로 다른 사람과 공유해서는 안 됩니다. API 키에 하단바 버튼 사이의 간격을 줄입니다 라벨이 없는 하단바 버튼 비활성화하기 YouTube에서 라벨 없이 하단바 버튼을 보내는 경우 라벨이 있는 하단바 버튼을 표시합니다 + 툴바 탐색 탭 비활성화하기 + YouTube에서 구독 탭이 홈 화면 상단으로 이동한 경우, 구독 탭을 하단바에 표시합니다 하단바 애니메이션 활성화하기 하단바에서 탭 전환 시 애니메이션을 표시합니다 하단바 자동 숨기기 비활성화하기 diff --git a/patches/src/main/resources/addresources/values-ky-rKG/youtube/strings.xml b/patches/src/main/resources/addresources/values-ky-rKG/youtube/strings.xml index 3cb2c74039..be5b5a2e3d 100644 --- a/patches/src/main/resources/addresources/values-ky-rKG/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ky-rKG/youtube/strings.xml @@ -75,6 +75,7 @@ Second \"item\" text" + diff --git a/patches/src/main/resources/addresources/values-lo-rLA/youtube/strings.xml b/patches/src/main/resources/addresources/values-lo-rLA/youtube/strings.xml index d1c13bc476..174f25cd1a 100644 --- a/patches/src/main/resources/addresources/values-lo-rLA/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-lo-rLA/youtube/strings.xml @@ -64,6 +64,7 @@ Second \"item\" text" + diff --git a/patches/src/main/resources/addresources/values-lt-rLT/youtube/strings.xml b/patches/src/main/resources/addresources/values-lt-rLT/youtube/strings.xml index da3fd67bba..6d028a0ff7 100644 --- a/patches/src/main/resources/addresources/values-lt-rLT/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-lt-rLT/youtube/strings.xml @@ -454,6 +454,15 @@ Kartą atvėrus vaizdo įrašą su mygtuku „Ask“, funkcija veikia su visais Rodyti kanalų turinio miniatiūrų like skaičių Rodyti kanalų turinio miniatiūrų like santykį procentais + + Ieškoti komentaruose + Bakstelėkite paieškos mygtuką komentaruose, kad parodytumėte tik įkeltus komentarus, kuriuose yra visi ieškomi žodžiai. Norėdami ieškoti frazės, žodžius apibrėžkite citatomis + Ieškoti komentaruose + Žodžiai paieškai + Ieškoti + Valyti + Pakeiskite rūšiavimo tvarką arba dar kartą atidarykite komentarus, kad ieškotumėte jų + Paieška išvalyta. Pakeiskite rūšiavimo tvarką arba dar kartą atidarykite komentarus, kad pamatytumėte visus komentarus Išjungti iššokantįjį langą „Prisijungti prie TV“ Išjungti iššokantįjį langą „Prijunkite savo įrenginius“ @@ -838,6 +847,8 @@ Norėdami gauti prieigą prie nustatymų, įgalinkite Nustatymų mygtuką: Bendr Sumažina horizontalų tarpą tarp mygtukų Išjungti navigacijos mygtukus be etikečių Rodo navigacijos mygtukus su etiketėmis, jei YouTube juos atsiunčia be etikečių + Išjungti įrankių juostos navigacijos skirtukėlius + Rodo prenumeratas navigacijos juostoje, jei \"YouTube\" perkels jas į skirtukėlį virš pagrindinio puslapio Įjungti naršymo juostos animacijas Animuoja perėjimus keičiant skirtukus Išjungti automatinį naršymo juostos slėpimą diff --git a/patches/src/main/resources/addresources/values-lv-rLV/youtube/strings.xml b/patches/src/main/resources/addresources/values-lv-rLV/youtube/strings.xml index a15d91f328..544c51dd50 100644 --- a/patches/src/main/resources/addresources/values-lv-rLV/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-lv-rLV/youtube/strings.xml @@ -83,6 +83,8 @@ Second \"item\" text" + + Meklēt diff --git a/patches/src/main/resources/addresources/values-mai-rIN/youtube/strings.xml b/patches/src/main/resources/addresources/values-mai-rIN/youtube/strings.xml index 7680ccee98..bc1a10121f 100644 --- a/patches/src/main/resources/addresources/values-mai-rIN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-mai-rIN/youtube/strings.xml @@ -158,6 +158,7 @@ Second \"item\" text" + \'टीव्ही में साइन इन करी\' पॉपअप अक्षम करी diff --git a/patches/src/main/resources/addresources/values-mk-rMK/youtube/strings.xml b/patches/src/main/resources/addresources/values-mk-rMK/youtube/strings.xml index 274032c0d0..9c3a6127b0 100644 --- a/patches/src/main/resources/addresources/values-mk-rMK/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-mk-rMK/youtube/strings.xml @@ -212,6 +212,7 @@ Second \"item\" text" + Исклучи прозорче за најава на ТВ diff --git a/patches/src/main/resources/addresources/values-mn-rMN/youtube/strings.xml b/patches/src/main/resources/addresources/values-mn-rMN/youtube/strings.xml index d1c13bc476..174f25cd1a 100644 --- a/patches/src/main/resources/addresources/values-mn-rMN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-mn-rMN/youtube/strings.xml @@ -64,6 +64,7 @@ Second \"item\" text" + diff --git a/patches/src/main/resources/addresources/values-mr-rIN/youtube/strings.xml b/patches/src/main/resources/addresources/values-mr-rIN/youtube/strings.xml index a0b34d78cc..c41c6d574a 100644 --- a/patches/src/main/resources/addresources/values-mr-rIN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-mr-rIN/youtube/strings.xml @@ -317,6 +317,7 @@ Second \"item\" text" + \'टीव्हीवर साइन इन करा\' पॉपअप अक्षम करा \'कनेक्ट युवर डिव्हाइसेस\' पॉपअप अक्षम करा diff --git a/patches/src/main/resources/addresources/values-ms-rMY/youtube/strings.xml b/patches/src/main/resources/addresources/values-ms-rMY/youtube/strings.xml index 06304f7292..1ac4a6437c 100644 --- a/patches/src/main/resources/addresources/values-ms-rMY/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ms-rMY/youtube/strings.xml @@ -126,6 +126,8 @@ Second \"item\" text" + + Carian diff --git a/patches/src/main/resources/addresources/values-my-rMM/youtube/strings.xml b/patches/src/main/resources/addresources/values-my-rMM/youtube/strings.xml index d1c13bc476..174f25cd1a 100644 --- a/patches/src/main/resources/addresources/values-my-rMM/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-my-rMM/youtube/strings.xml @@ -64,6 +64,7 @@ Second \"item\" text" + diff --git a/patches/src/main/resources/addresources/values-nb-rNO/youtube/strings.xml b/patches/src/main/resources/addresources/values-nb-rNO/youtube/strings.xml index 61db8987bd..5ae342e944 100644 --- a/patches/src/main/resources/addresources/values-nb-rNO/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-nb-rNO/youtube/strings.xml @@ -454,6 +454,15 @@ Etter at en video med Ask-knappen er åpnet én gang, fungerer funksjonen med al Vis antall likninger på strømningsminiatyrbilde Vis andel av likninger på strømningsminiatyrbilde + + Søk i kommentarer + Trykk på søkeknappen i kommentarene for å vise kun lastede kommentarer som inneholder alle de søkte ordene. Sett ord i anførselstegn for å søke etter en frase + Søk i kommentarer + Ord som skal søkes etter + Søk + Tøm + Endre sorteringsrekkefølgen eller åpne kommentarene på nytt for å søke i dem + Søk tømt. Endre sorteringsrekkefølgen eller åpne kommentarene på nytt for å vise alle kommentarer Deaktiver \'Logg på TV\'-pop-up Deaktiver popup-vinduet \'Koble til enhetene dine\' @@ -838,6 +847,8 @@ For å nå Innstillinger, aktiver Innstillingsknappen via Generelt → Verktøyl Reduserer den horisontale avstanden mellom knappene Deaktiver navigasjonsknapper uten etiketter Viser navigasjonsknappene med etiketter, hvis YouTube sender dem uten etiketter + Deaktiver navigasjonsfaner i verktøylinjen + Viser abonnementer i navigasjonsfeltet hvis YouTube flytter det til en fane øverst på Hjem Aktiver navigasjonslinjeanimasjoner Animerer overganger når du bytter mellom faner Deaktiver automatisk skjuling av navigasjonslinje diff --git a/patches/src/main/resources/addresources/values-ne-rIN/youtube/strings.xml b/patches/src/main/resources/addresources/values-ne-rIN/youtube/strings.xml index d1c13bc476..174f25cd1a 100644 --- a/patches/src/main/resources/addresources/values-ne-rIN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ne-rIN/youtube/strings.xml @@ -64,6 +64,7 @@ Second \"item\" text" + diff --git a/patches/src/main/resources/addresources/values-nl-rNL/youtube/strings.xml b/patches/src/main/resources/addresources/values-nl-rNL/youtube/strings.xml index be98776aea..130d759ebd 100644 --- a/patches/src/main/resources/addresources/values-nl-rNL/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-nl-rNL/youtube/strings.xml @@ -454,6 +454,15 @@ Nadat een video met de Ask-knop eenmaal is geopend, werkt de functie voor alle v Aantal likes van feed-miniatuur weergeven Procentage likes van feed-miniatuur weergeven + + Reacties zoeken + Tik op de zoekknop in de reacties om alleen de geladen reacties te tonen die alle gezochte woorden bevatten. Zet woorden tussen aanhalingstekens om op een zin te zoeken + Reacties zoeken + Woorden om op te zoeken + Zoeken + Wissen + Wijzig de sorteervolgorde of open de reacties opnieuw om erin te zoeken + Zoekopdracht gewist. Wijzig de sorteervolgorde of open de reacties opnieuw om alle reacties te tonen Pop-up \'Inloggen op tv\' uitschakelen Schakel de pop-up \'Verbind je apparaten\' uit @@ -836,6 +845,8 @@ Om Instellingen te openen, schakel je de knop Instellingen in via Algemeen → G Vermindert de horizontale ruimte tussen knoppen Navigatieknoppen zonder labels uitschakelen Toont de navigatieknoppen met labels, als YouTube ze zonder labels verzendt + Navigatietabs in werkbalk uitschakelen + Toont Abonnementen in de navigatiebalk, als YouTube deze verplaatst naar een tabblad bovenaan Home Navigatiebalkanimaties inschakelen Maakt de navigatiebalk volledig ondoorzichtig in de lichte modus Automatisch verbergen van navigatiebalk uitschakelen diff --git a/patches/src/main/resources/addresources/values-or-rIN/youtube/strings.xml b/patches/src/main/resources/addresources/values-or-rIN/youtube/strings.xml index d1c13bc476..174f25cd1a 100644 --- a/patches/src/main/resources/addresources/values-or-rIN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-or-rIN/youtube/strings.xml @@ -64,6 +64,7 @@ Second \"item\" text" + diff --git a/patches/src/main/resources/addresources/values-pa-rIN/youtube/strings.xml b/patches/src/main/resources/addresources/values-pa-rIN/youtube/strings.xml index d1c13bc476..174f25cd1a 100644 --- a/patches/src/main/resources/addresources/values-pa-rIN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-pa-rIN/youtube/strings.xml @@ -64,6 +64,7 @@ Second \"item\" text" + diff --git a/patches/src/main/resources/addresources/values-pl-rPL/youtube/strings.xml b/patches/src/main/resources/addresources/values-pl-rPL/youtube/strings.xml index 73cf723a20..5e4ae43f62 100644 --- a/patches/src/main/resources/addresources/values-pl-rPL/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-pl-rPL/youtube/strings.xml @@ -436,24 +436,33 @@ Zgłoszenia są publiczne i podlegają weryfikacji przed dodaniem kanału do lis Pokaż przycisk \"Do obejrzenia\" na miniaturkach Na miniaturkach filmów na liście wyświetlany jest przycisk \"Do obejrzenia\". Przytrzymanie go otwiera opcję \"Zapisz na playliście\" - Dodaj element Ask do menu flyout - Dodaje element podsumowania wideo do menu feed flyout + Dodaj opcję \"Zapytaj\" do menu + Dodaje opcję podsumowania filmu do menu na liście - "Polecana na początku wymaga zalogowanego konta w kraju, w którym dostępna jest usługa Ask. + "Funkcja \"Zapytaj\" wymaga początkowo zalogowania na konto w kraju, w którym jest ona dostępna. -Po jednorazowym otwarciu filmu z przyciskiem Ask funkcja działa ze wszystkimi filmami, nawet jeśli aktualny kraj nie udostępnia usługi Ask." +​Po jednokrotnym otwarciu filmu z przyciskiem \"Zapytaj\" funkcja ta działa we wszystkich filmach, nawet jeśli nie jest dostępna w bieżącym kraju." - Podsumuj wideo + Podsumuj ten film - Nie udało się otworzyć Ask dla tego filmu + Nie udało się otworzyć funkcji \"Zapytaj\" dla tego filmu - Ask nie jest dostępny dla Twojego konta lub bieżącego kraju + Funkcja \"Zapytaj\" nie jest dostępna dla Twojego konta lub bieżącego kraju Pokaż liczbę wyświetleń na miniaturkach Liczba wyświetleń filmów na liście jest wyświetlana w lewym dolnym rogu miniaturki zamiast pod tytułem. Niektóre filmy, np. w sekcjach, mogą nie wyświetlać liczby wyświetleń Pokaż liczbę polubień na miniaturkach na liście Pokaż procent polubień na miniaturkach na liście + + Szukaj w komentarzach + Dotknij przycisku wyszukiwania w komentarzach, aby wyświetlić tylko załadowane komentarze zawierające wszystkie wyszukiwane słowa. Użyj cudzysłowu, aby wyszukać konkretną frazę + Szukaj w komentarzach + Słowa do wyszukiwania + Szukaj + Wyczyść + Zmień kolejność sortowania lub otwórz komentarze ponownie, aby je przeszukać + Wyszukiwanie wyczyszczone. Zmień kolejność sortowania lub otwórz komentarze ponownie, aby wyświetlić wszystkie komentarze Wyłącz komunikat o zalogowaniu się do TV Wyłącz wyskakujące okienko \"Połącz swoje urządzenia\" @@ -838,6 +847,8 @@ Aby przejść do ustawień, włącz przycisk w menu: Ogólne → Pasek narzędzi Zmniejsza poziome odstępy między przyciskami Wyłącz przyciski nawigacji bez etykiet Wyświetla przyciski nawigacji z etykietami, jeśli YouTube przesyła je bez nich + Wyłącz karty nawigacji na pasku narzędzi + Wyświetla Subskrypcje na pasku nawigacji, jeśli YouTube przeniesie je do karty na górze strony głównej Włącz animacje paska nawigacji Animuje przejścia podczas przełączania między kartami Wyłącz automatyczne ukrywanie paska nawigacji diff --git a/patches/src/main/resources/addresources/values-pt-rBR/youtube/strings.xml b/patches/src/main/resources/addresources/values-pt-rBR/youtube/strings.xml index 332b6b075e..915104c4ed 100644 --- a/patches/src/main/resources/addresources/values-pt-rBR/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-pt-rBR/youtube/strings.xml @@ -454,6 +454,15 @@ Depois que um vídeo com o botão Ask for aberto uma vez, o recurso funciona com Mostrar contagem de curtidas da miniatura do feed Mostrar porcentagem de curtidas da miniatura do feed + + Pesquisar comentários + Toque no botão de pesquisa nos comentários para mostrar apenas os comentários carregados que contenham todas as palavras pesquisadas. Coloque as palavras entre aspas para pesquisar uma frase + Pesquisar comentários + Palavras para pesquisar + Procurar + Limpar + Altere a ordem de classificação ou abra os comentários novamente para pesquisá-los + Pesquisa limpa. Altere a ordem de classificação ou abra os comentários novamente para mostrar todos os comentários Desativar pop-up \'Fazer login na TV\' Desativar pop-up \'Conecte seus dispositivos\' @@ -806,7 +815,7 @@ Os 'cookies de legenda' devem ser fornecidos" Corrigir transcrição Resolve uma falha na verificação de pré-condições que pode impedir o carregamento da transcrição - Mostrar legendas na imagem em imagem + Mostrar legendas em modo picture-in-picture Botões de navegação Escolha quais botões aparecem na barra de navegação inferior @@ -838,6 +847,8 @@ Para acessar as Configurações, ative o botão Configurações via Geral → Ba Reduz o espaçamento horizontal entre os botões Desativar botões de navegação sem rótulos Mostra os botões de navegação com rótulos, se o YouTube os enviar sem rótulos + Desativar guias de navegação da barra de ferramentas + Exibe Inscrições na barra de navegação, caso o YouTube as mova para uma guia no topo da Página inicial Ativar animações da barra de navegação Anima as transições ao alternar entre as abas Desativar ocultação automática da barra de navegação diff --git a/patches/src/main/resources/addresources/values-pt-rPT/youtube/strings.xml b/patches/src/main/resources/addresources/values-pt-rPT/youtube/strings.xml index 1eecfe70a3..13ca8236b2 100644 --- a/patches/src/main/resources/addresources/values-pt-rPT/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-pt-rPT/youtube/strings.xml @@ -454,6 +454,15 @@ Depois que um vídeo com o botão Ask for aberto uma vez, o recurso funciona com Mostrar contagem de curtidas na miniatura do feed Mostrar porcentagem de curtidas na miniatura do feed + + Pesquisar comentários + Toque no botão de pesquisa nos comentários para mostrar apenas os comentários carregados que contenham todas as palavras pesquisadas. Coloque as palavras entre aspas para pesquisar uma frase + Pesquisar comentários + Palavras para pesquisar + Pesquisar + Limpar + Altere a ordem de classificação ou abra os comentários novamente para pesquisá-los + Pesquisa limpa. Altere a ordem de classificação ou abra os comentários novamente para mostrar todos os comentários Desativar pop-up \'Entrar na TV\' Desativar popup \'Conecte seus dispositivos\' @@ -838,6 +847,8 @@ Para acessar as Configurações, ative o botão Configurações em Geral → Bar Reduz o espaçamento horizontal entre os botões Desativar botões de navegação sem rótulos Mostra os botões de navegação com rótulos, se o YouTube os enviar sem rótulos + Desativar guias de navegação da barra de ferramentas + Exibe as Inscrições na barra de navegação, caso o YouTube as mova para uma guia no topo da Página Inicial Ativar animações da barra de navegação Anima as transições ao alternar entre as guias Desativar ocultação automática da barra de navegação diff --git a/patches/src/main/resources/addresources/values-ro-rRO/youtube/strings.xml b/patches/src/main/resources/addresources/values-ro-rRO/youtube/strings.xml index 998b2d8882..b8b0852e90 100644 --- a/patches/src/main/resources/addresources/values-ro-rRO/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ro-rRO/youtube/strings.xml @@ -454,6 +454,15 @@ După ce un videoclip cu butonul Ask este deschis o dată, funcția funcționeaz Afișare număr like pentru miniatura feed-ului Afișare procentaj like pentru miniatura feed-ului + + Căutare în comentarii + Apasă butonul de căutare din comentarii pentru a afișa doar comentariile încărcate care conțin toate cuvintele căutate. Pune cuvintele între ghilimele pentru a căuta o frază + Căutare în comentarii + Cuvinte de căutat + Caută + Golește + Schimbă ordinea sortării sau redeschide comentariile pentru a le căuta + Căutarea a fost ștearsă. Schimbă ordinea sortării sau redeschide comentariile pentru a afișa toate comentariile Dezactivare pop-up Conectare la televizor Dezactivează fereastra pop-up \'Conectează-ți dispozitivele\' @@ -838,6 +847,8 @@ Pentru a accesa Setările, activați butonul Setări prin General → Bara de in Ascunde meniul Blocare ecran Dezactivează butoanele de navigare fără etichete Afișează butoanele de navigare cu etichete, dacă YouTube le trimite fără etichete + Dezactivează tab-urile de navigare din bara de instrumente + Afișează Abonările în bara de navigare, dacă YouTube le mută într-un tab în partea superioară a paginii Home Activare animații bară de navigare Ascunde meniul Ascultă cu YouTube Music Dezactivează ascunderea automată a barei de navigare diff --git a/patches/src/main/resources/addresources/values-ru-rRU/youtube/strings.xml b/patches/src/main/resources/addresources/values-ru-rRU/youtube/strings.xml index 2f5552d139..dcd1f3969f 100644 --- a/patches/src/main/resources/addresources/values-ru-rRU/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ru-rRU/youtube/strings.xml @@ -454,6 +454,15 @@ Second \"item\" text" Показывать количество лайков под миниатюрой в ленте Показывать процент лайков под миниатюрой в ленте + + Поиск комментариев + Нажмите кнопку поиска в комментариях, чтобы отобразить только те загруженные комментарии, которые содержат все искомые слова. Чтобы найти фразу, заключите слова в кавычки + Поиск комментариев + Слова для поиска + Поиск + Очистить + Измените порядок сортировки или снова откройте комментарии, чтобы выполнить поиск по ним + Поиск завершён. Измените порядок сортировки или снова откройте комментарии, чтобы отобразить все комментарии Отключить всплывающее окно \"Вход на ТВ\" Отключить всплывающее окно «Подключите ваши устройства» @@ -840,6 +849,8 @@ Second \"item\" text" Уменьшает горизонтальное расстояние между кнопками Отключить кнопки навигации без подписей Показывает кнопки навигации с подписями, если YouTube отправляет их без подписей + Отключить вкладки навигации на панели инструментов + Показывать «Подписки» в панели навигации, если YouTube перенесет их во вкладку в верхней части Главной Включить анимацию панели навигации Анимирует переходы между вкладками Выключить авто скрытие панели навигации diff --git a/patches/src/main/resources/addresources/values-si-rLK/youtube/strings.xml b/patches/src/main/resources/addresources/values-si-rLK/youtube/strings.xml index d1c13bc476..174f25cd1a 100644 --- a/patches/src/main/resources/addresources/values-si-rLK/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-si-rLK/youtube/strings.xml @@ -64,6 +64,7 @@ Second \"item\" text" + diff --git a/patches/src/main/resources/addresources/values-sk-rSK/youtube/strings.xml b/patches/src/main/resources/addresources/values-sk-rSK/youtube/strings.xml index 04f630339c..4d978660fb 100644 --- a/patches/src/main/resources/addresources/values-sk-rSK/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-sk-rSK/youtube/strings.xml @@ -454,6 +454,15 @@ Po jednom otvorení videa s tlačidlom Opýtať sa bude funkcia fungovať pre v Zobraziť počítadlo lajkov pre náhľad feedu Zobraziť percento lajkov pre náhľad feedu + + Vyhľadať v komentároch + Ťuknite na tlačidlo vyhľadávania v komentároch, aby sa zobrazili iba načítané komentáre, ktoré obsahujú všetky hľadané slová. Slová dajte do úvodzoviek, ak chcete hľadať konkrétnu frázu + Vyhľadať v komentároch + Slová na vyhľadanie + Hľadať + Vyčistiť + Zmeňte poradie triedenia alebo komentáre znova otvorte, aby ste v nich mohli vyhľadávať + Vyhľadávanie vymazané. Zmeňte poradie triedenia alebo komentáre znova otvorte, aby sa zobrazili všetky komentáre Zakázať vyskakovacie okno \'Prihlásiť sa do TV\' Zakázať kontextové okno \'Pripojte svoje zariadenia\' @@ -838,6 +847,8 @@ Ak chcete získať prístup k Nastaveniam, povoľte tlačidlo Nastavenia cez Vš Znižuje horizontálnu medzeru medzi tlačidlami Zakázať navigačné tlačidlá bez označení Zobrazuje navigačné tlačidlá s označeniami, ak ich YouTube pošle bez označení + Zakázať navigačné karty nástrojového panela + Zobrazuje Odoberané v navigačnej lište, ak YouTube presunie do karty v hornej časti domovskej stránky Povoliť animácie navigačného panela Animuje prechody pri prechode medzi panelmi Zakázať automatické skrytie navigačného panela diff --git a/patches/src/main/resources/addresources/values-sl-rSI/youtube/strings.xml b/patches/src/main/resources/addresources/values-sl-rSI/youtube/strings.xml index a13a45b96c..e1abd7cae2 100644 --- a/patches/src/main/resources/addresources/values-sl-rSI/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-sl-rSI/youtube/strings.xml @@ -454,6 +454,15 @@ Ko enkrat odprete video s gumbom Zastavi, bo funkcija delovala za vse videe, tud Prikaži število všečkov za sličico predvajalne vrstice Prikaži odstotek všečkov za sličico predvajalne vrstice + + Iskanje komentarjev + Tapnite gumb za iskanje v komentarjih, da izprikažete samo naložene komentarje, ki vsebujejo vse iskane besede. Besede vzemite v narekovaje za iskanje besedne skupine + Iskanje komentarjev + Besede za iskanje + Išči + Počisti + Spremenite vrstni red ali ponovno odprite komentarje za njihovo iskanje + Iskanje počiščeno. Spremenite vrstni red ali ponovno odprite komentarje za prikaz vseh komentarjev Onemogoči pojavni gumb \'Prijava na TV\' Onemogoči pojavno okno \'Povečite svoje naprave\' @@ -838,6 +847,8 @@ Za dostop do Nastavitev omogočajte gumb Nastavitve prek Splošno → Orodna vrs Naredi vrstico stanja popolnoma neprozorno Onemogoči navigacijske gumbe brez oznak Prikaže navigacijske gumbe z oznakami, če jih YouTube pošlje brez oznak + Onemogoči navigacijske zavihke v vrstici z orodji + Prikaže Naročnine v navigacijski vrstici, če YouTube jih premakne v zavihek na vrhu Domov Omogoči animacije navigacijske vrstice Naredi vrstico za navigacijo popolnoma neprozorno v svetlem načinu Onemogoči samodejno skrivanje navigacijske vrstice @@ -1292,6 +1303,7 @@ Videoposnetki so zahtevani s kratko predpono zgoščenega ID-ja videoposnetka, t Zakasnitev videa Zakasni video, da bo sinhroniziran s kasnejočim zvokom, npr. pri brezžičnih slušalkah (0-5000 ms) Zakasni le z Bluetooth zvokom + Zasni video le, ko je povezan Bluetooth zvok, tako da ostane v sinhronizaciji z zvočnikom telefona Prikaži gumb za zakasnitev videa Dolgo pritisnite za ponastavitev zakasnitve na 0 ms Prikaži meni za zakasnitev videa diff --git a/patches/src/main/resources/addresources/values-sr-rCS/youtube/strings.xml b/patches/src/main/resources/addresources/values-sr-rCS/youtube/strings.xml index 118c99f4f7..e644b52968 100644 --- a/patches/src/main/resources/addresources/values-sr-rCS/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-sr-rCS/youtube/strings.xml @@ -454,6 +454,15 @@ Nakon što se jednom otupi video sa dugmetom Ask, funkcija radi sa svim videima, Prikaži broj lajkova na smanjenoj slici feed-a Prikaži procenat lajkova na smanjenoj slici feed-a + + Pretraži komentare + Dodirnite dugme za pretragu u komentarima da biste prikazali samo učitane komentare koji sadrže sve pretražene reči. Stavite reči pod navodnike da biste pretražili frazu + Pretraži komentare + Reči za pretragu + Pretraži + Obriši + Promenite redosled sortiranja ili ponovo otvorite komentare da biste ih pretražili + Pretraga je obrisana. Promenite redosled sortiranja ili ponovo otvorite komentare da biste prikazali sve komentare Onemogući iskačući prozor „Prijava na TV” Onemogućite skočni prozor \'Povežite svoje uređaje\' @@ -838,6 +847,8 @@ Da biste pristupili Podešavanjima, omogućite dugme Podešavanja putem General Smanji horizontalni razmak između dugmadi Onemogući navigaciona dugmad bez oznaka Prikazuje navigaciona dugmad sa oznakama, ako ih YouTube pošalje bez oznaka + Onemogući navigacione kartice u alatnoj traci + Prikazuje Preplate u navigacionoj traci, ako YouTube to premesti u karticu na vrhu Početne stranice Omogući animacije navigacione trake Animira prelaz prilikom prelaska između kartica Onemogući automatsko skrivanje navigacione trake diff --git a/patches/src/main/resources/addresources/values-sr-rSP/youtube/strings.xml b/patches/src/main/resources/addresources/values-sr-rSP/youtube/strings.xml index f753835d0e..3e60d32ad4 100644 --- a/patches/src/main/resources/addresources/values-sr-rSP/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-sr-rSP/youtube/strings.xml @@ -454,6 +454,15 @@ Second \"item\" text" Прикажи број лајкова на сличици садржаја Прикажи проценат лајкова на сличици садржаја + + Претрага коментара + Додирните дугме за претрагу у коментарима да бисте приказали само учитане коментаре који садрже све претраживане речи. Ставите речи под наводнике да бисте претражили фразу + Претрага коментара + Речи за претрагу + Претрага + Очисти + Промените редослед сортирања или поново отворите коментаре да бисте их претражили + Претрага је очишћена. Промените редослед сортирања или поново отворите коментаре да бисте приказали све коментаре Онемогући искачући прозор „Пријава на ТВ” Онемогући искачући прозор \"Повежи своје уређаје\" @@ -836,6 +845,8 @@ Second \"item\" text" Смањује хоризонтални размак између дугмади. Онемогући навигационе дугмиће без натписа Приказује навигационе дугмиће са натписима, ако их YouTube шаље без натписа + Онемогући навигационе картице у alatnoj traci + Приказује Претплате у навигационој траци, ако YouTube их пребаци у картицу на врху Почетне стране Омогући анимације траке за навигацију Анимира прелазе приликом пребацивања између картица. Онемогућите аутоматско скривање траке навигације diff --git a/patches/src/main/resources/addresources/values-sv-rSE/youtube/strings.xml b/patches/src/main/resources/addresources/values-sv-rSE/youtube/strings.xml index ac9c150a5d..a2a1e8a1f1 100644 --- a/patches/src/main/resources/addresources/values-sv-rSE/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-sv-rSE/youtube/strings.xml @@ -454,6 +454,15 @@ När en video med Ask-knappen har öppnats en gång fungerar funktionen med alla Visa antal gillningar för matlårstumnai Visa procentandel för gillningar i matlårstumnai + + Sök i kommentarer + Tryck på sökknappen i kommentarerna för att endast visa de laddade kommentarerna som innehåller alla sökord. Sätt ord inom citattecken för att söka efter en fras + Sök i kommentarer + Ord att söka efter + Sök + Rensa + Ändra sorteringsordningen eller öppna kommentarerna igen för att söka i dem + Sökningen har rensats. Ändra sorteringsordningen eller öppna kommentarerna igen för att visa alla kommentarer Inaktivera popup-fönster om att logga in på TV:n Inaktivera popup-fönstret \'Anslut dina enheter\' @@ -838,6 +847,8 @@ För att komma åt Inställningar aktiverar du Inställningsknappen via Allmänt Minskar det horisontella avståndet mellan knappar Inaktivera navigeringsknappar utan etiketter Visar navigeringsknapparna med etiketter, om YouTube skickar dem utan etiketter + Inaktivera navigeringsflikar i verktygsfältet + Visar Prenumerationer i navigeringsfältet om YouTube flyttar det till en flik högst upp på Hem Aktivera animeringar för navigeringsfältet Animerar övergångarna när du byter mellan flikar Inaktivera att navigeringsfältet döljs automatiskt diff --git a/patches/src/main/resources/addresources/values-sw-rKE/youtube/strings.xml b/patches/src/main/resources/addresources/values-sw-rKE/youtube/strings.xml index d1c13bc476..174f25cd1a 100644 --- a/patches/src/main/resources/addresources/values-sw-rKE/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-sw-rKE/youtube/strings.xml @@ -64,6 +64,7 @@ Second \"item\" text" + diff --git a/patches/src/main/resources/addresources/values-ta-rIN/youtube/strings.xml b/patches/src/main/resources/addresources/values-ta-rIN/youtube/strings.xml index affd75f0d9..8b0c1c2daa 100644 --- a/patches/src/main/resources/addresources/values-ta-rIN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ta-rIN/youtube/strings.xml @@ -74,6 +74,7 @@ Second \"item\" text" + diff --git a/patches/src/main/resources/addresources/values-te-rIN/music/strings.xml b/patches/src/main/resources/addresources/values-te-rIN/music/strings.xml index ccfea4ed71..960b06473d 100644 --- a/patches/src/main/resources/addresources/values-te-rIN/music/strings.xml +++ b/patches/src/main/resources/addresources/values-te-rIN/music/strings.xml @@ -18,6 +18,8 @@ Second \"item\" text" + రద్దుచేయి + తేరు @@ -27,6 +29,8 @@ Second \"item\" text" + ఫీడ్ + సాధారణం @@ -70,5 +74,6 @@ Second \"item\" text" + స్వయం చాలకం diff --git a/patches/src/main/resources/addresources/values-te-rIN/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-te-rIN/shared-youtube/strings.xml index fd007eeb78..d66d3ca855 100644 --- a/patches/src/main/resources/addresources/values-te-rIN/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-te-rIN/shared-youtube/strings.xml @@ -18,6 +18,7 @@ Second \"item\" text" --> + నిలిపివే diff --git a/patches/src/main/resources/addresources/values-te-rIN/shared/strings.xml b/patches/src/main/resources/addresources/values-te-rIN/shared/strings.xml index 31cdbe4dcb..0956fbc829 100644 --- a/patches/src/main/resources/addresources/values-te-rIN/shared/strings.xml +++ b/patches/src/main/resources/addresources/values-te-rIN/shared/strings.xml @@ -18,6 +18,7 @@ Second \"item\" text" + మా గురించి @@ -26,7 +27,10 @@ Second \"item\" text" + ఆపు + చేయి + తొలగించు diff --git a/patches/src/main/resources/addresources/values-te-rIN/youtube/strings.xml b/patches/src/main/resources/addresources/values-te-rIN/youtube/strings.xml index d1c13bc476..25e351a4c3 100644 --- a/patches/src/main/resources/addresources/values-te-rIN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-te-rIN/youtube/strings.xml @@ -18,7 +18,12 @@ Second \"item\" text" + మా గురించి + ప్రకటనలు + ఫీడ్ + సాధారణం + ఆపు @@ -52,6 +57,7 @@ Second \"item\" text" + తొలగించు @@ -64,6 +70,7 @@ Second \"item\" text" + @@ -75,6 +82,7 @@ Second \"item\" text" + తొలగించు @@ -85,6 +93,7 @@ Second \"item\" text" + ఆపు @@ -107,6 +116,7 @@ Second \"item\" text" + ఆపు @@ -164,6 +174,7 @@ Second \"item\" text" + నిలిపివే @@ -175,6 +186,7 @@ Second \"item\" text" + ఆపు @@ -219,5 +231,8 @@ Second \"item\" text" + స్వయం చాలకం + రద్దుచేయి + చర్య రద్దు diff --git a/patches/src/main/resources/addresources/values-th-rTH/music/strings.xml b/patches/src/main/resources/addresources/values-th-rTH/music/strings.xml index e69aaf7f37..2aac986900 100644 --- a/patches/src/main/resources/addresources/values-th-rTH/music/strings.xml +++ b/patches/src/main/resources/addresources/values-th-rTH/music/strings.xml @@ -314,7 +314,7 @@ Second \"item\" text" ซ่อนปุ่ม \'เริ่มวิทยุ\' เมนูแบบเลื่อนออก - เลือกส่วนประกอบที่จะแสดงในเมนูแบบเลื่อนออกของตัวเล่นและคิว + เลือกส่วนประกอบที่จะแสดงในเมนูแบบเลื่อนออกของเครื่องเล่นและคิว ซ่อนส่วนประกอบ 3 คอลัมน์ ซ่อนปุ่มชอบและไม่ชอบ ซ่อนเมนู \'การจับคู่รสนิยม\' diff --git a/patches/src/main/resources/addresources/values-th-rTH/reddit/strings.xml b/patches/src/main/resources/addresources/values-th-rTH/reddit/strings.xml index fb5e8545d2..a4c488381c 100644 --- a/patches/src/main/resources/addresources/values-th-rTH/reddit/strings.xml +++ b/patches/src/main/resources/addresources/values-th-rTH/reddit/strings.xml @@ -120,7 +120,7 @@ Second \"item\" text" %s (ปัจจุบัน) %s (ออกจากระบบ) ผู้ใช้ทั่วไป (ออกจากระบบหรือโหมดไม่ระบุตัวตน) - ซ่อนโอเวอร์เลย์ของตัวแสดงสื่อ + ซ่อนภาพซ้อนทับของตัวแสดงสื่อ ซ่อนชื่อเรื่อง ปุ่ม และตัวควบคุมวิดีโอเมื่อเปิดรูปภาพหรือวิดีโอ แตะที่สื่อเพื่อแสดงสิ่งเหล่านั้น ซ่อนปุ่มดูการสนทนา ซ่อนปุ่ม \"ดูการสนทนา\" ที่ด้านล่างของตัวแสดงสื่อ diff --git a/patches/src/main/resources/addresources/values-th-rTH/youtube/strings.xml b/patches/src/main/resources/addresources/values-th-rTH/youtube/strings.xml index 65a496c96a..62f7956209 100644 --- a/patches/src/main/resources/addresources/values-th-rTH/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-th-rTH/youtube/strings.xml @@ -41,17 +41,17 @@ Second \"item\" text" ปิดการอัปเดตรูปแบบ ป้องกันไม่ให้เซิร์ฟเวอร์อัปเดตเลย์เอาต์ของแอป - "เลย์เอาต์แอปพลิเคชันกลับไปใช้เลย์เอาต์ที่ใช้เมื่อติดตั้งครั้งแรก + "เลย์เอาต์ของแอปกลับไปใช้เลย์เอาต์ที่ใช้เมื่อติดตั้งครั้งแรก เลย์เอาต์ด้านเซิร์ฟเวอร์บางส่วนอาจไม่กลับคืน การเปลี่ยนแปลง ได้แก่: -• ส่วนประกอบในเมนูฟลายเอาท์ของตัวเล่น (หรือการตั้งค่าที่เกี่ยวข้อง) อาจไม่ทำงาน +• ส่วนประกอบในเมนูแบบเลื่อนออกของเครื่องเล่น (หรือการตั้งค่าที่เกี่ยวข้อง) อาจไม่ทำงาน • ตัวเลขหมุนไม่มีการเคลื่อนไหว -• แท็บไลบรารีจะแสดง +• แท็บคลังจะแสดง • ส่วนเพลงของคำอธิบายวิดีโออาจไม่ทำงาน -•ไอเท็มเมนูฟลายเอาท์ของตัวเล่นไม่สามารถซ่อนได้ -• ปุ่มสลับบัญชีอาจไม่ปรากฏบนแท็บไลบรารี" +• รายการเมนูแบบเลื่อนออกของเครื่องเล่นไม่สามารถซ่อนได้ +• ปุ่มสลับบัญชีอาจไม่ปรากฏบนแท็บคลัง" เปิดตัวกรองเมนูแบบเลื่อนออกของฟีด ตัวกรองเมนูแบบเลื่อนออกของฟีด @@ -388,7 +388,7 @@ Second \"item\" text" ซ่อนโฆษณาโดยทั่วไป ซ่อนแบนเนอร์สินค้า ซ่อนป้ายโปรโมตแบบเสียเงิน - ซ่อนป๊อปอัพของตัวเล่น + ซ่อนป๊อปอัพของเครื่องเล่น ซ่อนการ์ดที่สนับสนุนตัวเอง ซ่อนลิงก์ร้านค้า ซ่อนโฆษณาวิดีโอ @@ -436,8 +436,8 @@ Second \"item\" text" แสดงปุ่มดูภายหลังบนภาพย่อ จะแสดงปุ่มดูภายหลังบนภาพย่อของวิดีโอในฟีด การแตะและค้างไว้จะทำให้บันทึกไปยังเพลย์ลิสต์ - เพิ่มรายการ Ask ไปยังเมนูลอย - เพิ่มรายการสรุปวิดีโอไปยังเมนูลอยฟีด + เพิ่มรายการ Ask ไปยังเมนูแบบเลื่อนออก + เพิ่มรายการสรุปวิดีโอไปยังเมนูแบบเลื่อนออกของฟีด "Ask จำเป็นต้องมีการเข้าสู่บัญชีในประเทศที่ Ask มีให้บริการ @@ -454,6 +454,15 @@ Second \"item\" text" แสดงจำนวนคนกดถูกใจของรูปภาพขนาดย่อในฟีด แสดงอัตราส่วนการกดถูกใจของรูปภาพขนาดย่อในฟีด + + ค้นหาความคิดเห็น + แตะปุ่มค้นหาในความคิดเห็นเพื่อแสดงเฉพาะความคิดเห็นที่โหลดแล้วซึ่งมีคำที่ค้นหาครบทุกคำ ใส่คำในเครื่องหมายคำพูดเพื่อค้นหาเป็นวลี + ค้นหาความคิดเห็น + คำที่ต้องการค้นหา + ค้นหา + ล้าง + เปลี่ยนลำดับการจัดเรียงหรือเปิดความคิดเห็นอีกครั้งเพื่อค้นหา + ล้างการค้นหาแล้ว เปลี่ยนลำดับการจัดเรียงหรือเปิดความคิดเห็นอีกครั้งเพื่อแสดงความคิดเห็นทั้งหมด ปิดใช้งานป๊อปอัปเข้าสู่ระบบไปยังทีวี ปิดหน้าต่างแจ้งเตือน \'เชื่อมต่ออุปกรณ์ของคุณ\' @@ -462,7 +471,7 @@ Second \"item\" text" ปิดใช้งานท่าทางแตะสองครั้งด้วยสองนิ้วที่ข้ามส่วนเนื้อหา ซึ่งอาจถูกเรียกใช้งานโดยไม่ได้ตั้งใจด้วยนิ้วเดียว ดาวน์โหลดวิดีโอ พอดแคสต์ หรือเพลงด้วยแอปดาวน์โหลดของบุคคลที่สาม - แสดงปุ่มดาวน์โหลด overlay ของผู้เล่น + แสดงปุ่มดาวน์โหลดซ้อนทับของเครื่องเล่น แสดงปุ่มในเครื่องเล่นวิดีโอที่จะเปิดตัวดาวน์โหลดภายนอกของคุณ การแปลเสียงบรรยาย @@ -681,17 +690,17 @@ Second \"item\" text" เปิดใช้งานท่าทางความสว่างอัตโนมัติ การปัดความสว่างไปยังค่าต่ำสุดจะเปิดใช้งานความสว่างอัตโนมัติของระบบ อัตโนมัติ - เวลาหมดอายุของการแสดงทับการปัด - จำนวนมิลลิวินาทีที่การแสดงทับจะมองเห็น - ความทึบแสงพื้นหลังของสไลด์โอเวอร์เลย์ + เวลาหมดอายุของภาพซ้อนทับการปัด + จำนวนมิลลิวินาทีที่ภาพซ้อนทับจะมองเห็น + ความทึบแสงพื้นหลังของภาพซ้อนทับการปัด ปรับระดับความโปร่งใสของภาพซ้อนทับ (0 คือโปร่งใส 100 คือทึบแสง) - สีความสว่างของการซ้อนทับการปัด + สีความสว่างของภาพซ้อนทับการปัด ตั้งค่าสีของแถบความคืบหน้าของการควบคุมความสว่าง - สีระดับเสียงของการซ้อนทับการปัด + สีระดับเสียงของภาพซ้อนทับการปัด ตั้งค่าสีของแถบความคืบหน้าของการควบคุมระดับเสียง - สีของการซ้อนทับการปัดเปลี่ยนความเร็วในการเล่น + สีความเร็วของภาพซ้อนทับการปัด ตั้งค่าสีของแถบแสดงความคืบหน้าของการควบคุมความเร็วในการเล่น - ขนาดข้อความของการปัด + ขนาดข้อความของภาพซ้อนทับการปัด ปรับขนาดตัวอักษรบนหน้าจอ (1-30) เกณฑ์ขนาดของการปัด ตั้งค่าระยะการปัดที่จำเป็นก่อนที่จะเกิดการปรับ @@ -707,16 +716,16 @@ Second \"item\" text" ค่าที่สูงขึ้นต้องมีการปัดมากขึ้นต่อการปรับความเร็วแต่ละครั้ง ขั้นตอนความเร็ว ความสูงของโซนท่าทางความเร็ว - ความสูงของโซนท่าทางความเร็วคิดเป็นเปอร์เซ็นต์ของความสูงของตัวเล่น (5-75) + ความสูงของโซนท่าทางความเร็วคิดเป็นเปอร์เซ็นต์ของความสูงของเครื่องเล่น (5-75) ความเร็ว - รูปแบบโอเวอร์เลย์การปัด - โอเวอร์เลย์แนวนอน - โอเวอร์เลย์แนวนอน (มินิมอล - ด้านบน) - โอเวอร์เลย์แนวนอน (มินิมอล - ตรงกลาง) - โอเวอร์เลย์แบบวงกลม - โอเวอร์เลย์แบบวงกลม (มินิมอล) - โอเวอร์เลย์แนวตั้ง - โอเวอร์เลย์แนวตั้ง (มินิมอล) + รูปแบบของภาพซ้อนทับการปัด + ภาพซ้อนทับแนวนอน + ภาพซ้อนทับแนวนอน (มินิมอล - ด้านบน) + ภาพซ้อนทับแนวนอน (มินิมอล - ตรงกลาง) + ภาพซ้อนทับแบบวงกลม + ภาพซ้อนทับแบบวงกลม (มินิมอล) + ภาพซ้อนทับแนวตั้ง + ภาพซ้อนทับแนวตั้ง (มินิมอล) เปิดใช้การปัดเพื่อเปลี่ยนวิดีโอ ปัดในโหมดเต็มหน้าจอเพื่อข้ามไปยังวิดีโอถัดไปหรือก่อนหน้า @@ -838,6 +847,8 @@ Second \"item\" text" ทำให้แถบนำทางทึบแสงอย่างสมบูรณ์ในโหมดสว่าง ปิดใช้งานปุ่มนำทางที่ไม่มีป้ายกำกับ แสดงปุ่มนำทางพร้อมป้ายกำกับ หาก YouTube ส่งมาโดยไม่มีป้ายกำกับ + ปิดใช้งานแท็บการนำทางบนแถบเครื่องมือ + แสดงการติดตามในแถบนำทาง หาก YouTube ย้ายไปไว้ในแท็บที่ด้านบนของหน้าหลัก เปิดใช้งานภาพเคลื่อนไหวแถบนำทาง เคลื่อนไหวการเปลี่ยนภาพเมื่อสลับระหว่างแท็บ ปิดใช้งานการซ่อนแถบนำทางอัตโนมัติ @@ -857,7 +868,7 @@ Second \"item\" text" ซ่อนเมนู \'การตั้งค่าเพิ่มเติม\' / เพิ่มเติม - ซ่อนเมนู \'โหมดแอมเบี้ยนท์\' + ซ่อนเมนู \'โหมดแอมเบียนท์\' "เมนู 'แทร็กเสียง' ถูกซ่อน @@ -896,7 +907,7 @@ Second \"item\" text" ซ่อนตัวอย่างเล่นอัตโนมัติ ซ่อนภาพตัวอย่างวิดีโอที่จะเล่นถัดไปขณะเล่นเต็มหน้าจอก่อนเริ่มเล่นอัตโนมัติ - ปุ่มโอเวอร์เลย์ + ปุ่มซ้อนทับ เลือกปุ่มที่จะแสดงบนเครื่องเล่นวิดีโอ ซ่อนปุ่มเล่นอัตโนมัติ @@ -913,12 +924,12 @@ Second \"item\" text" ซ่อนการ์ดหน้าจอสิ้นสุด - โหมดแอมเบี้ยนท์ - จัดการการแสดงผลและข้อจำกัดด้านพลังงานของโหมดแอมเบี้ยนท์ - เลี่ยงข้อจำกัดของโหมดแอมเบี้ยนท์ - อนุญาตให้โหมดแอมเบี้ยนท์ยังคงทำงานอยู่ในโหมดประหยัดพลังงาน - ปิดการใช้งานโหมดพักผ่อน - ปิดใช้งานโหมดแอมเบี้ยนท์ในโหมดเต็มหน้าจอ + โหมดแอมเบียนท์ + จัดการการแสดงผลและข้อจำกัดด้านพลังงานของโหมดแอมเบียนท์ + เลี่ยงข้อจำกัดของโหมดแอมเบียนท์ + อนุญาตให้โหมดแอมเบียนท์ยังคงทำงานอยู่ในโหมดประหยัดพลังงาน + ปิดใช้งานโหมดแอมเบียนท์ + ปิดใช้งานโหมดแอมเบียนท์ในโหมดเต็มหน้าจอ ซ่อนการ์ดข้อมูล @@ -990,12 +1001,12 @@ Second \"item\" text" ซ่อนวิดีโอที่เกี่ยวข้อง - ซ่อนภาพซ้อน \'วิดีโอเพิ่มเติม\' - ซ่อนภาพซ้อน \'วิดีโอเพิ่มเติม\' ที่แสดงเมื่อปัดขึ้นในโหมดเต็มหน้าจอ + ซ่อนภาพซ้อนทับ \'วิดีโอเพิ่มเติม\' + ซ่อนภาพซ้อนทับ \'วิดีโอเพิ่มเติม\' ที่แสดงเมื่อปัดขึ้นในโหมดเต็มหน้าจอ ซ่อนเวลาของวิดีโอ - ปิดใช้งานป๊อปอัพของตัวเล่น + ปิดใช้งานป๊อปอัพของเครื่องเล่น ปิดใช้งานแผงเพลย์ลิสต์และแชทสดไม่ให้เปิดโดยอัตโนมัติ ปิดการเล่นอัตโนมัติของเพลย์ลิสต์ @@ -1013,7 +1024,7 @@ Second \"item\" text" หมุนเครื่องเล่นเป็นแนวนอนเมื่อเข้าสู่โหมดเต็มหน้าจอ ใช้ไม่ได้บนหน้าจอขนาดเล็ก ซึ่งเปิดเต็มหน้าจอในแนวนอนอยู่แล้ว ท่าทางแบบเต็มหน้าจอ - ท่าทางของตัวเล่นในโหมดเต็มหน้าจอ + ท่าทางของเครื่องเล่นในโหมดเต็มหน้าจอ ปิดใช้งานท่าทางปัดขึ้นในโหมดแนวตั้ง ปิดการใช้งานท่าทางปัดลงในโหมดแนวตั้ง ปิดการใช้งานท่าทางปัดลงในโหมดแนวนอน @@ -1036,7 +1047,7 @@ Second \"item\" text" แสดงปุ่มปิดเสียง - ความโปร่งใสของการซ้อนทับของเครื่องเล่น + ความโปร่งใสของภาพซ้อนทับของเครื่องเล่น ปรับระดับความโปร่งใส (0 คือโปร่งใส, 100 คือทึบแสง) แถบค้นหา @@ -1056,22 +1067,22 @@ Second \"item\" text" • โพสต์จากชุมชนจะถูกซ่อนไว้ • จะไม่มีตัวเลือกการเล่นในฟีด • จะไม่มีปุ่มรีมิกซ์และปุ่มเสียงใน Shorts -• อาจไม่สามารถใช้งานโอเวอร์เลย์ 'วิดีโอเพิ่มเติม' ได้ +• อาจไม่สามารถใช้งานภาพซ้อนทับ 'วิดีโอเพิ่มเติม' ได้ • แถบการทำงานของวิดีโอแบบเดิมที่ไม่ยุบจะแสดงขึ้น เลย์เอาต์สำหรับรถยนต์ • ฟีดจะถูกจัดระเบียบตามหัวข้อและช่อง • จะไม่มีตัวเลือกการเล่นในฟีด • จะไม่มีปุ่มรีมิกซ์และปุ่มเสียงใน Shorts -• อาจไม่สามารถใช้งานโอเวอร์เลย์ 'วิดีโอเพิ่มเติม' ได้ +• อาจไม่สามารถใช้งานภาพซ้อนทับ 'วิดีโอเพิ่มเติม' ได้ • Shorts จะเปิดในเครื่องเล่นปกติ • แถบการทำงานของวิดีโอแบบเดิมที่ไม่ยุบจะแสดงขึ้น" เปิดใช้งานรูปแบบแท็บเล็ตในเครื่องเล่น - "คืนรูปแบบตัวเล่นแบบเก่า + "คืนรูปแบบเครื่องเล่นแบบเก่า -ข้อจำกัด: หากปิด 'คืนแถบการทำงานของวิดีโอแบบเก่า' การแสดงผล 'วิดีโอเพิ่มเติม' อาจใช้งานไม่ได้" - การเปิดใช้นี้อาจทำให้บางฟีเจอร์เลย์เอาต์ของตัวเล่นเสีย เช่น ส่วนความคิดเห็นหรือ \'วิดีโอเพิ่มเติม\' ที่แสดงด้านล่างตัวเล่น +ข้อจำกัด: หากปิด 'คืนแถบการทำงานของวิดีโอแบบเก่า' ภาพซ้อนทับ 'วิดีโอเพิ่มเติม' อาจใช้งานไม่ได้" + การเปิดใช้นี้อาจทำให้บางฟีเจอร์เลย์เอาต์ของเครื่องเล่นเสีย เช่น ส่วนความคิดเห็นหรือ \'วิดีโอเพิ่มเติม\' ที่แสดงด้านล่างเครื่องเล่น 21.11.48 - กู้คืนพฤติกรรมคำบรรยายแบบเดิม 20.39.41 - คืนค่าไอคอนเครื่องเล่น Shorts เก่า @@ -1169,11 +1180,11 @@ Second \"item\" text" การเล่นจะหยุดชั่วคราวเมื่อลากมินิเพลเยอร์ออกจากหน้าจอ จดจำตำแหน่งการลาก การลากมินิเพลเยอร์ออกนอกหน้าจอจะรักษา UI สถานะเดิมไว้ - ซ่อนปุ่มโอเวอร์เลย์ + ซ่อนปุ่มซ้อนทับ ขนาดเริ่มต้น ตั้งค่าขนาดเริ่มต้นบนหน้าจอเป็นพิกเซล ขนาดพิกเซลต้องอยู่ระหว่าง %1$s ถึง %2$s - ความทึบแสงของโอเวอร์เลย์ + ความทึบแสงของภาพซ้อนทับ ปรับระดับความโปร่งใส (0 คือโปร่งใส, 100 คือทึบแสง) ปุ่มกลับออกจากฟีด @@ -1187,7 +1198,7 @@ Second \"item\" text" ซ่อนแถบสถานะ ซ่อนแถบสถานะของระบบ ปัดลงจากขอบด้านบนเพื่อแสดงชั่วครู่ แก้ไขแถบสถานะไม่ให้บังแถบด้านบน - คงรูปแบบของแถบสถานะให้สอดคล้องกัน เพื่อไม่ให้เกิดการบังแถบด้านบนและเหลือพื้นที่ว่างเปล่าเหนือตัวเล่น เปิดการใช้งานนี้หากแอปค้างหรือแถบด้านบนแสดงผลผิดปกติ + คงรูปแบบของแถบสถานะให้สอดคล้องกัน เพื่อไม่ให้เกิดการบังแถบด้านบนและเหลือพื้นที่ว่างเปล่าเหนือเครื่องเล่น เปิดการใช้งานนี้หากแอปค้างหรือแถบด้านบนแสดงผลผิดปกติ พื้นหลังธีมสว่าง สีขาว @@ -1260,7 +1271,7 @@ Second \"item\" text" DeArrow ไม่พร้อมใช้งานชั่วคราว - คืนค่าสไตล์ปุ่มของตัวเล่นแบบเก่า + คืนค่าสไตล์ปุ่มของเครื่องเล่นแบบเก่า คืนค่ารูปลักษณ์ตัวควบคุมเครื่องเล่นวิดีโอแบบหนาให้เป็นสไตล์ที่เก่ากว่าและบางกว่า ปลอมแปลงมิติของอุปกรณ์ @@ -1414,7 +1425,7 @@ AVC (H.264) กู้คืนแถบการทำงานของวิดีโอแบบเดิม "แก้ไขปัญหาที่ปุ่มการทำงานของวิดีโอไม่สามารถซ่อนได้ -ข้อจำกัด: ย้อนกลับไปใช้รูปแบบโอเวอร์เลย์ 'วิดีโอเพิ่มเติม' แบบเก่า" +ข้อจำกัด: ย้อนกลับไปใช้รูปแบบภาพซ้อนทับ 'วิดีโอเพิ่มเติม' แบบเก่า" สไตล์ไอคอนเครื่องเล่น อัตโนมัติ @@ -1430,7 +1441,7 @@ AVC (H.264) เพิ่มหน้าซีรีย์ในประวัติและตัวเลือกติดตามซีรีย์ลงในเพลย์ลิสต์ แสดงซีรีส์ในแถบนำทาง แสดงปุ่มติดตามซีรี่ส์ - เพิ่มปุ่มตัวเล่นเพื่อติดตามหรือเปิดซีรี่ส์ของวิดีโอปัจจุบัน + เพิ่มปุ่มเครื่องเล่นเพื่อติดตามหรือเปิดซีรี่ส์ของวิดีโอปัจจุบัน บันทึกความคืบหน้าของซีรี่ส์ที่ติดตาม บันทึกการเล่นบนโทรศัพท์นี้สำหรับซีรี่ส์ที่บันทึกและบุ๊กมาร์ก ยกเลิก @@ -1519,7 +1530,7 @@ AVC (H.264) กรุณารอก่อนรีเฟรชอีกครั้ง ไม่สามารถโหลดตอน ลองอีกครั้งภายหลัง ไม่สามารถเสร็จสิ้นการเปลี่ยนแปลง ลองอีกครั้ง - ไม่สามารถยืนยันการรับชมต่อได้ โปรดตรวจสอบตัวเล่นและลองใหม่อีกครั้ง + ไม่สามารถยืนยันการรับชมต่อได้ โปรดตรวจสอบเครื่องเล่นและลองใหม่อีกครั้ง ติดตามซีรี่ส์ วิดีโอนี้ไม่มีอยู่ในเพลย์ลิสต์นี้ กำลังค้นหาเพลย์ลิสต์... diff --git a/patches/src/main/resources/addresources/values-tr-rTR/youtube/strings.xml b/patches/src/main/resources/addresources/values-tr-rTR/youtube/strings.xml index d94db54a61..c0681673dc 100644 --- a/patches/src/main/resources/addresources/values-tr-rTR/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-tr-rTR/youtube/strings.xml @@ -437,24 +437,33 @@ Raporlar herkese açıktır ve bir kanal listeye eklenmeden önce incelenir. Kal Küçük resimlerde Daha Sonra İzle düğmesini göster Akış videolarının küçük resimlerinde bir Daha Sonra İzle düğmesi gösterilir. Dokunulup basılı tutulduğunda Çalma listesine kaydet açılır - Ask ögesini açılır menüye ekle - Akışın açılır menüsüne bir video özeti öğesi eklenir + Soru ögesini açılır menüye ekle + Akış açılır menüsüne bir video özetlendirme ögesi ekler - "Ask'ın kullanılabilmesi için öncelikle Ask'ın mevcut olduğu bir ülkede oturum açmış olmanız gerekir. + "Soru, ilk başta Soru'nun kullanılabilir olduğu bir ülkeden oturum açılmış bir hesap gerektirir. -Ask düğmesi olan bir video ilk kez açıldığında, özellik geçerli ülke Ask özelliğini sunmasa bile tüm videolarda çalışır." +Soru düğmesine sahip olan bir video bir kez açıldıktan sonra bu özellik, mevcut ülke Soru özelliğine sahip olmasa dahi tüm videolarda çalışmaya başlar." Videoyu özetle - Bu video için Ask kullanılamadı + Bu video için Soru açılamadı - Ask hesabınız veya bulunduğunuz ülke için mevcut değil + Soru, hesabınız veya bulunduğunuz ülke için mevcut değil İzlenme sayısını kapak resimlerinde göster Akış videolarının izlenme sayısı başlığın altı yerine kapak resminin sol altında gösterilir. Raflardaki gibi bazı videolar izlenme sayısı göstermeyebilir Akış kapak fotoğrafı beğeni sayısını göster Akış kapak fotoğrafı beğeni oranını göster + + Yorumları ara + Yalnızca yüklü yorumlarda aranan tüm kelimeleri içeren yorumları görüntülemek için yorumlardaki arama düğmesine dokunun. Bir ifadeyi aramak için kelimeleri tırnak içine alın + Yorumları ara + Aranacak kelimeler + Ara + Temizle + Yorumları aramak için sıralamayı değiştirin veya yorumları tekrar açın + Arama temizlendi. Tüm yorumları görüntülemek için sıralamayı değiştirin veya yorumları tekrar açın \"TV\'ye oturum aç\" penceresini devre dışı bırak \'Cihazlarınızı bağlayın\' açılır penceresini devre dışı bırak @@ -839,6 +848,8 @@ Ayarlar'a erişmek için Ayarlar düğmesini Genel → Araç Çubuğu → Ayarla Düğmeler arasındaki yatay boşluğu azaltır Etiketsiz gezinme düğmelerini devre dışı bırak YouTube bunları etiketsiz gönderirse, gezinme düğmelerini etiketlerle gösterir + Araç çubuğu gezinti sekmelerini devre dışı bırak + YouTube, Abonelikler\'i Ana Sayfa\'nın üstündeki bir sekmeye taşırsa, onu gezinti çubuğunda gösterir Gezinme çubuğu animasyonlarını etkinleştir Sekmeler arasında animasyonlu geçişler yapar Gezinme çubuğunun otomatik gizlenmesini devre dışı bırak diff --git a/patches/src/main/resources/addresources/values-uk-rUA/music/strings.xml b/patches/src/main/resources/addresources/values-uk-rUA/music/strings.xml index 6e7e088788..ebd7326aab 100644 --- a/patches/src/main/resources/addresources/values-uk-rUA/music/strings.xml +++ b/patches/src/main/resources/addresources/values-uk-rUA/music/strings.xml @@ -127,8 +127,8 @@ Second \"item\" text" Завантажувати музичні відео, подкасти або пісні за допомогою сторонніх застосунків для завантаження - Використовуйте зовнішній завантажувач для меню «Завантаження» - Параметр «Завантажити» у меню запускає ваш зовнішній завантажувач + Використовувати зовнішній завантажувач для меню \"Завантажити\" + Пункт \"Завантажити\" в меню натомість відкриває ваш зовнішній завантажувач Завантаження "Немає завантажень Завантажені треки з'являються тут" diff --git a/patches/src/main/resources/addresources/values-uk-rUA/youtube/strings.xml b/patches/src/main/resources/addresources/values-uk-rUA/youtube/strings.xml index 3becab0266..bb23dabe22 100644 --- a/patches/src/main/resources/addresources/values-uk-rUA/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-uk-rUA/youtube/strings.xml @@ -436,24 +436,33 @@ Second \"item\" text" Показувати кнопку \"Дивитися пізніше\" на мініатюрах Кнопка \"Дивитися пізніше\" показується на мініатюрах відео в стрічці. Натискання та утримування її відкриває \"Зберегти в список відтворення\" - Додати елемент «Ask» до спадного меню - Додати елемент узагальнення відео в стрічкове спадне меню + Додати пункт \"Запитати\" у спливне меню + Додає пункт підсумовування відео до спливного меню стрічки - "Спочатку для використання функції «Ask» потрібен обліковий запис із підпискою та країна, де ця функція доступна. + "Для початку роботи функції \"Запитати\" потрібен обліковий запис, у який виконано вхід, у країні, де вона доступна. -Якщо хоча б один раз відкрити відео з кнопкою «Ask», функція стане доступною для всіх відео, навіть якщо у поточній країні вона недоступна." +Після того як ви один раз відкриєте відео з кнопкою \"Запитати\", функція працюватиме з усіма відео, навіть якщо в поточній країні вона недоступна." - Узагальнити відео + Підсумувати відео - Не вдалося відкрити функцію «Ask» для цього відео + Не вдалося відкрити \"Запитати\" для цього відео - Функція «Ask» недоступна для вашого облікового запису або в поточній країні + \"Запитати\" недоступне для вашого облікового запису або поточної країни Показувати кількість переглядів на мініатюрах Кількість переглядів відео в стрічці показується внизу ліворуч на їхніх мініатюрах, а не під назвою. Для деяких відео, наприклад у полицях, кількість переглядів може не показуватися Показувати кількість лайків на мініатюрах стрічки Показувати відсоток лайків на мініатюрах стрічки + + Пошук у коментарях + Натисніть кнопку пошуку в коментарях, щоб показати лише завантажені коментарі, які містять усі шукані слова. Щоб знайти фразу, візьміть слова в лапки + Пошук у коментарях + Слова для пошуку + Пошук + Очистити + Змініть порядок сортування або відкрийте коментарі знову, щоб здійснити пошук + Пошук очищено. Змініть порядок сортування або відкрийте коментарі знову, щоб показати всі коментарі Вимкнути вікно \"Увійти на телевізорі\" Вимкнути вікно \"Підключіть свої пристрої\" @@ -462,7 +471,7 @@ Second \"item\" text" Вимикає жест подвійного натискання двома пальцями для пропуску розділів, який може випадково спрацювати одним пальцем Завантажувати відео, подкасти або пісні за допомогою сторонніх застосунків для завантаження - Показати кнопку завантаження в накладці плеєра + Показувати кнопку завантаження в оверлеї плеєра Показує кнопку у відеоплеєрі, яка відкриває ваш зовнішній завантажувач Голосовий переклад @@ -838,6 +847,8 @@ Second \"item\" text" Зменшує горизонтальні відступи між кнопками Вимкнути кнопки навігації без підписів Показує кнопки навігації з підписами, якщо YouTube надсилає їх без підписів + Вимкнути вкладки навігації на панелі інструментів + Показує «Підписки» на навігаційній панелі, якщо YouTube перенесе їх у вкладку у верхній частині Головної сторінки Увімкнути анімації панелі навігації Анімує переходи між вкладками Вимкнути автоприховування панелі навігації @@ -1123,8 +1134,8 @@ Second \"item\" text" Автоматично переходить до наступного Short замість повторення поточного Автоматичне відтворення у фоновому режимі Автоматично переходить до наступного Short під час фонового відтворення - Пропустити Shorts наживо - Пропускає потоки наживо, які з\'являються в стрічці Shorts + Пропустити прямі трансляції Shorts + Пропускає прямі трансляції, що з\'являються в стрічці Shorts Відтворення в стрічках Завжди ввімкнено @@ -1289,12 +1300,12 @@ Second \"item\" text" Максимальний Затримка відео - Затримує відео, щоб воно було синхронізовано з затриманим звуком, наприклад, через Bluetooth-навушники (0–5000 мс) - Тільки затримка при використанні звуку по Bluetooth - Затримує відео лише під час підключення звуку по Bluetooth, тож динамік телефону залишається синхронізованим - Показати кнопку затримки відео - Натисніть і утримуйте для скидання затримки до 0 мс - Показати меню затримки відео + Затримує відео, щоб воно залишалося синхронізованим із запізнілим звуком, наприклад у Bluetooth-навушниках (0-5000 мс) + Затримувати лише за використання звуку через Bluetooth + Затримує відео лише тоді, коли підключено звук через Bluetooth, тож звук із динаміка телефона залишається синхронізованим + Показувати кнопку \"Затримка відео\" + Натисніть і утримуйте, щоб скинути затримку до 0 мс + Показувати меню \"Затримка відео\" Затримка в мілісекундах @@ -1337,7 +1348,7 @@ Second \"item\" text" Сповіщення про зміну швидкості Показує сповіщення при збереженні нової типової швидкості відтворення Типова швидкість відтворення - Типова швидкість відтворення для Shorts + Типова швидкість відтворення в Shorts Типову швидкість відтворення змінено на %s Використовувати звичайну швидкість для музики Завжди відтворювати музичні відео зі швидкістю 1.0x diff --git a/patches/src/main/resources/addresources/values-ur-rIN/youtube/strings.xml b/patches/src/main/resources/addresources/values-ur-rIN/youtube/strings.xml index a95d6d7a16..e7a28ceb72 100644 --- a/patches/src/main/resources/addresources/values-ur-rIN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ur-rIN/youtube/strings.xml @@ -64,6 +64,7 @@ Second \"item\" text" + diff --git a/patches/src/main/resources/addresources/values-uz-rUZ/youtube/strings.xml b/patches/src/main/resources/addresources/values-uz-rUZ/youtube/strings.xml index 765823f650..1b59e9bc90 100644 --- a/patches/src/main/resources/addresources/values-uz-rUZ/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-uz-rUZ/youtube/strings.xml @@ -331,6 +331,8 @@ Cheklovlar: + + Qidiruv “Televizorga kirish”ni o\'chirish \'Connect your devices\' oynatish xabarini o\'chirish diff --git a/patches/src/main/resources/addresources/values-vi-rVN/youtube/strings.xml b/patches/src/main/resources/addresources/values-vi-rVN/youtube/strings.xml index 0fce2e631f..68f389bb2d 100644 --- a/patches/src/main/resources/addresources/values-vi-rVN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-vi-rVN/youtube/strings.xml @@ -454,6 +454,15 @@ Sau khi mở một video có nút Ask một lần, tính năng này hoạt độ Hiển thị số lượng thích của ảnh bìa nguồn cấp Hiển thị tỷ lệ phần trăm thích của ảnh bìa nguồn cấp + + Tìm kiếm bình luận + Nhấn nút tìm kiếm trong phần bình luận để chỉ hiển thị các bình luận đã tải có chứa tất cả các từ tìm kiếm. Đặt các từ trong dấu ngoặc kép để tìm kiếm một cụm từ + Tìm kiếm bình luận + Từ cần tìm kiếm + Tìm kiếm + Xóa + Thay đổi thứ tự sắp xếp hoặc mở lại phần bình luận để tìm kiếm chúng + Đã xóa tìm kiếm. Thay đổi thứ tự sắp xếp hoặc mở lại phần bình luận để hiển thị tất cả bình luận Tắt cửa sổ \"Đăng nhập vào TV\" Tắt cửa sổ \"Kết nối thiết bị của bạn\" @@ -838,6 +847,8 @@ Cần thiết lập \"Cookie phụ đề\"" Giảm khoảng cách ngang giữa các nút Tắt các nút điều hướng không có nhãn Hiển thị các nút điều hướng có nhãn, nếu YouTube gửi chúng không có nhãn + Vô hiệu hóa các tab điều hướng trên thanh công cụ + Hiển thị Mục đăng ký trong thanh điều hướng, nếu YouTube chuyển nó thành một tab ở đầu Trang chủ Bật hiệu ứng thanh điều hướng Tạo hoạt ảnh khi chuyển tab Tắt tự động ẩn thanh điều hướng diff --git a/patches/src/main/resources/addresources/values-zh-rCN/youtube/strings.xml b/patches/src/main/resources/addresources/values-zh-rCN/youtube/strings.xml index b43b234518..461acd8884 100644 --- a/patches/src/main/resources/addresources/values-zh-rCN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-zh-rCN/youtube/strings.xml @@ -454,6 +454,15 @@ Second \"item\" text" 显示帖子缩略图点赞数 显示帖子缩略图点赞比例 + + 搜索评论 + 点击评论中的搜索按钮,仅显示包含所有搜索词的已加载评论。将词组放在引号中可搜索特定短语 + 搜索评论 + 要搜索的词语 + 搜索 + 清除 + 更改排序顺序或重新打开评论以进行搜索 + 搜索已清除。更改排序顺序或重新打开评论以显示所有评论 禁用登录电视弹出窗口 禁用“连接您的设备”弹出窗口 @@ -838,6 +847,8 @@ Second \"item\" text" 减少按钮之间的水平间距 禁用无标签的导航按钮 如果 YouTube 发送的导航按钮没有标签,则显示带标签的导航按钮 + 禁用工具栏导航标签 + 如果 YouTube 将“订阅”移至主页顶部的标签页,则在导航栏中显示“订阅” 启用导航栏动画 软光上子检查些“通过”软光浩通的新闻 禁用自动隐藏导航栏 diff --git a/patches/src/main/resources/addresources/values-zh-rTW/music/strings.xml b/patches/src/main/resources/addresources/values-zh-rTW/music/strings.xml index 8843041b95..5f4f83b606 100644 --- a/patches/src/main/resources/addresources/values-zh-rTW/music/strings.xml +++ b/patches/src/main/resources/addresources/values-zh-rTW/music/strings.xml @@ -21,9 +21,9 @@ Second \"item\" text" 訂閱內容 影片 其他結果 - 上傳的歌曲 - 上傳的專輯 - 上傳的藝術家 + 上載的歌曲 + 上載的專輯 + 上載的藝人 更新主機的 Jam 修補程式以使用暫停與繼續 Jam 佇列分享 diff --git a/patches/src/main/resources/addresources/values-zh-rTW/youtube/strings.xml b/patches/src/main/resources/addresources/values-zh-rTW/youtube/strings.xml index 2ab5b2b1d4..6fd3ba172d 100644 --- a/patches/src/main/resources/addresources/values-zh-rTW/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-zh-rTW/youtube/strings.xml @@ -62,7 +62,7 @@ Second \"item\" text" 隱藏專輯資訊卡 隱藏藝術家描述下方的專輯卡片 隱藏藝人資訊卡 - 隱藏搜尋結果頂部的藝術家卡片 + 隱藏搜尋結果頂部的藝人卡片 隱藏頻道按鈕 隱藏搜尋結果中頻道個人檔案中的訂閱、查看頻道、社群和 YouTube Music 等按鈕 隱藏社群貼文 @@ -454,6 +454,15 @@ Second \"item\" text" 顯示動態縮圖的按讚數 顯示動態縮圖的按讚比例 + + 搜尋評論 + 點擊評論中的搜尋按鈕,僅顯示包含所有搜尋字詞的已載入評論。將字詞放入引號中以搜尋特定短語 + 搜尋評論 + 要搜尋的字詞 + 搜尋 + 清除 + 更改排序順序或重新開啟評論以進行搜尋 + 搜尋已清除。更改排序順序或重新開啟評論以顯示所有評論 停用「登入電視」彈出視窗 停用「連接您的裝置」快顯視窗 @@ -838,6 +847,8 @@ Second \"item\" text" 減少按鈕之間的水平間距 停用沒有標籤的導覽按鈕 如果 YouTube 傳送的導覽按鈕沒有標籤,則顯示帶有標籤的導覽按鈕 + 停用工具列導覽分頁 + 如果 YouTube 將「訂閱」移至首頁頂端的分頁,則在導覽列中顯示訂閱 啟用導覽列動畫 在切換分頁時,讓轉換效果更生動 停用自動隱藏導航欄 diff --git a/patches/src/main/resources/addresources/values-zu-rZA/youtube/strings.xml b/patches/src/main/resources/addresources/values-zu-rZA/youtube/strings.xml index d1c13bc476..174f25cd1a 100644 --- a/patches/src/main/resources/addresources/values-zu-rZA/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-zu-rZA/youtube/strings.xml @@ -64,6 +64,7 @@ Second \"item\" text" + From 8601a6973d65005cb987d2d6a0ff1c26abcd3b6e Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Fri, 23 Oct 2026 10:58:08 +0000 Subject: [PATCH 012/189] chore: Release v1.49.0-dev.1 [skip ci] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## [1.49.0-dev.1](https://github.com/MorpheApp/morphe-patches/compare/v1.48.0...v1.49.0-dev.1) (2026-10-23) ### 🐛 Bug Fixes * **YouTube - Hide status bar:** Video player is moved below the display cutout in portrait ([f80398e](https://github.com/MorpheApp/morphe-patches/commit/f80398e107587c7fb9f995f508a65ad1953b2734)) * **YouTube - Navigation bar:** Translucent navigation bar is not shown without the edge to edge layout ([b8e1fdd](https://github.com/MorpheApp/morphe-patches/commit/b8e1fddd8a9d7146f2ccc4d1522a86b134cfe7a6)) * **YouTube:** Miniplayer covered by the keyboard and empty band above the navigation bar ([0345136](https://github.com/MorpheApp/morphe-patches/commit/0345136b552c49b70c01fcc19a18762f57d3b7ad)) * **YouTube:** Morphe flyout menu items are missing on tablets ([7127541](https://github.com/MorpheApp/morphe-patches/commit/71275419c5e6b8bd6a0a108e3d7815c8ef11a88e)) ### ✨ New Features * **YouTube - Navigation bar:** Add "Disable toolbar navigation tabs" setting ([01e767c](https://github.com/MorpheApp/morphe-patches/commit/01e767c4c3a386e4b7881ce7fc867a516082db23)) * **YouTube:** Add `Search comments` patch ([#3942](https://github.com/MorpheApp/morphe-patches/issues/3942)) ([fbb628b](https://github.com/MorpheApp/morphe-patches/commit/fbb628ba23ecdcabd00b3bdfbf94aff60f58ac68)) ### 🚀 Updated App Support * **YouTube:** Add support for `21.23.492` ([4599204](https://github.com/MorpheApp/morphe-patches/commit/45992041db85e4fb51900cc5a773380732dc1fda)) --- CHANGELOG.md | 18 +++ README.md | 7 +- gradle.properties | 2 +- patches-bundle.json | 8 +- patches-list.json | 291 ++++++++++++++++++++++++++++---------------- 5 files changed, 212 insertions(+), 114 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ac35887a3d..590a9f9b35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,21 @@ +## [1.49.0-dev.1](https://github.com/MorpheApp/morphe-patches/compare/v1.48.0...v1.49.0-dev.1) (2026-10-23) + +### 🐛 Bug Fixes + +* **YouTube - Hide status bar:** Video player is moved below the display cutout in portrait ([f80398e](https://github.com/MorpheApp/morphe-patches/commit/f80398e107587c7fb9f995f508a65ad1953b2734)) +* **YouTube - Navigation bar:** Translucent navigation bar is not shown without the edge to edge layout ([b8e1fdd](https://github.com/MorpheApp/morphe-patches/commit/b8e1fddd8a9d7146f2ccc4d1522a86b134cfe7a6)) +* **YouTube:** Miniplayer covered by the keyboard and empty band above the navigation bar ([0345136](https://github.com/MorpheApp/morphe-patches/commit/0345136b552c49b70c01fcc19a18762f57d3b7ad)) +* **YouTube:** Morphe flyout menu items are missing on tablets ([7127541](https://github.com/MorpheApp/morphe-patches/commit/71275419c5e6b8bd6a0a108e3d7815c8ef11a88e)) + +### ✨ New Features + +* **YouTube - Navigation bar:** Add "Disable toolbar navigation tabs" setting ([01e767c](https://github.com/MorpheApp/morphe-patches/commit/01e767c4c3a386e4b7881ce7fc867a516082db23)) +* **YouTube:** Add `Search comments` patch ([#3942](https://github.com/MorpheApp/morphe-patches/issues/3942)) ([fbb628b](https://github.com/MorpheApp/morphe-patches/commit/fbb628ba23ecdcabd00b3bdfbf94aff60f58ac68)) + +### 🚀 Updated App Support + +* **YouTube:** Add support for `21.23.492` ([4599204](https://github.com/MorpheApp/morphe-patches/commit/45992041db85e4fb51900cc5a773380732dc1fda)) + ## [1.48.0](https://github.com/MorpheApp/morphe-patches/compare/v1.47.0...v1.48.0) (2026-10-22) ### 🐛 Bug Fixes diff --git a/README.md b/README.md index cf005337dd..7d3ef061f0 100644 --- a/README.md +++ b/README.md @@ -35,14 +35,14 @@ All modifications made by Morphe, along with their dates, can be found in the Gi ## 🩹 Patches list -> **[v1.48.0](https://github.com/MorpheApp/morphe-patches/releases/tag/v1.48.0)**  •  `main`  •  190 patches total +> **[v1.49.0-dev.1](https://github.com/MorpheApp/morphe-patches/releases/tag/v1.49.0-dev.1)**  •  `dev`  •  191 patches total

-📦 YouTube  •  105 patches +📦 YouTube  •  106 patches
**🎯 Supported versions:** -| 🧪 21.42.241 | 🧪 21.41.525 | 🧪 21.28.208 | 🧪 21.23.492 | 21.20.405 | 21.16.256 | 20.51.39 | 20.21.37 | +| 🧪 21.42.241 | 🧪 21.41.525 | 🧪 21.28.208 | 21.23.492 | 21.20.405 | 21.16.256 | 20.51.39 | 20.21.37 | | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | | 💊 Patch | 📜 Description | ⚙️ Options | @@ -135,6 +135,7 @@ All modifications made by Morphe, along with their dates, can be found in the Gi | [Return YouTube Dislike](#return-youtube-dislike) | Adds an option to show the dislike count of videos with Return YouTube Dislike, and the like count and like ratio on the thumbnails of feed videos. | | | [Sanitize sharing links](#sanitize-sharing-links) | Removes the tracking query parameters from shared links. | | | [Save to Watch later](#save-to-watch-later) | Adds an option to display save to Watch later button in the video player. | | +| [Search comments](#search-comments) | Adds a button to the comments to show only the loaded comments that contain the searched words. | | | [Seekbar](#seekbar) | Adds options to show old seekbar thumbnails, disable precise seeking when swiping up on the seekbar, slide to seek instead of playing at 2x speed when pressing and holding, tapping the player seekbar to seek, hiding the video player seekbar, enabling seeking in live streams, and expanding the live stream DVR duration. | | | [Series tracking](#series-tracking) | Adds a series library to History with local progress and episode continuation. This patch works with YouTube 21.02 and newer. | | | [Settings menu filter](#settings-menu-filter) | Adds an option to hide items on the standard YouTube settings screen by their visible name. | | diff --git a/gradle.properties b/gradle.properties index 83533d2755..e81bde1a5c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,4 +3,4 @@ org.gradle.jvmargs = -Xms512M -Xmx2048M org.gradle.parallel = true android.useAndroidX = true kotlin.code.style = official -version = 1.48.0 +version = 1.49.0-dev.1 diff --git a/patches-bundle.json b/patches-bundle.json index b24d87091d..b0c87490c8 100644 --- a/patches-bundle.json +++ b/patches-bundle.json @@ -1,7 +1,7 @@ { - "created_at": "2026-10-22T13:11:33", - "description": "## [1.48.0](https://github.com/MorpheApp/morphe-patches/compare/v1.47.0...v1.48.0) (2026-10-22)\n\n### 🐛 Bug Fixes\n\n* **Downloads:** Allow using in-app downloads with player overlay external download button ([3df6c0a](https://github.com/MorpheApp/morphe-patches/commit/3df6c0a3d9ad097db63709cf5b559223d4575dd6))\n* Export only filtered logs and stream file exports efficiently ([#3746](https://github.com/MorpheApp/morphe-patches/issues/3746)) ([e04c72d](https://github.com/MorpheApp/morphe-patches/commit/e04c72d8e068204c3a2178b75451fb8bfbb4d755))\n* Temporarily revert patches-library update to fix app startup crash ([fd8cc0f](https://github.com/MorpheApp/morphe-patches/commit/fd8cc0fb14540b1e0d6adec903a7c2720a80b2f7))\n* **YouTube - Add to queue:** Fix Premium-style queue bugs ([#3752](https://github.com/MorpheApp/morphe-patches/issues/3752)) ([b07a6ac](https://github.com/MorpheApp/morphe-patches/commit/b07a6ac48bae7e6cd0336d4afdc12e368d5442d4))\n* **YouTube - Ambient mode:** Force Ambient mode when the server turns it off ([0eda16c](https://github.com/MorpheApp/morphe-patches/commit/0eda16c28e7ca4842a9087bba0fb47d146feddfa))\n* **YouTube - Captions:** Restore all translation menu getter routes ([#3818](https://github.com/MorpheApp/morphe-patches/issues/3818)) ([3a6a5b3](https://github.com/MorpheApp/morphe-patches/commit/3a6a5b3ae1d934114668a68ebe56bd512baae18e))\n* **YouTube - Captions:** Restore translations from original caption sources ([#3794](https://github.com/MorpheApp/morphe-patches/issues/3794)) ([46ab900](https://github.com/MorpheApp/morphe-patches/commit/46ab900bd9daa990a4d741a6e7f3dc5611173cdb))\n* **YouTube - Captions:** Show auto-translated captions of styled caption tracks ([#3837](https://github.com/MorpheApp/morphe-patches/issues/3837)) ([aedcf19](https://github.com/MorpheApp/morphe-patches/commit/aedcf198849b226d3b23638e5e810d9ea39999ba))\n* **YouTube - Custom font:** Show custom font picker if no font is selected yet ([8347b98](https://github.com/MorpheApp/morphe-patches/commit/8347b98e007c98065bbe2d546a89bf4f1daa1dce))\n* **YouTube - Default video quality:** Forcely disabled automatic stock quality selection and deprecated the code to handle both Stock/Morphe quality selections ([93e0c2b](https://github.com/MorpheApp/morphe-patches/commit/93e0c2b4db27760a22b1e48aef8b87dc330a92bf))\n* **YouTube - Default video quality:** SABR video quality drops while the video plays ([#3853](https://github.com/MorpheApp/morphe-patches/issues/3853)) ([0488bda](https://github.com/MorpheApp/morphe-patches/commit/0488bda28d4d5844a2a739d3eefd00c28afd43fa))\n* **YouTube - Default video quality:** Video quality drops while the video plays ([9f7b7eb](https://github.com/MorpheApp/morphe-patches/commit/9f7b7ebb10c909f7d2bc480de9fdbd3dd6076e2f))\n* **YouTube - Disable translucent bar:** Replace flag override with a proper patch ([88f77a8](https://github.com/MorpheApp/morphe-patches/commit/88f77a86ae737bc5da476011918d480baeff90b3))\n* **YouTube - Downloads:** Music videos may not download ([0bac4fa](https://github.com/MorpheApp/morphe-patches/commit/0bac4fa12101e5729e5b4c060d9bac10250281aa))\n* **YouTube - Downloads:** Playing video enters picture-in-picture when the external downloader opens ([#3882](https://github.com/MorpheApp/morphe-patches/issues/3882)) ([cf43209](https://github.com/MorpheApp/morphe-patches/commit/cf43209679dbba198e963aa381ce25110a6aa8fb))\n* **YouTube - Feed card layout:** Remove the feed vertical spacing override, which can crash the app ([31a85c7](https://github.com/MorpheApp/morphe-patches/commit/31a85c7d1bfb99658c668444c37fd96e16500398))\n* **YouTube - Feed card layout:** Round the top outer corners of the Shorts shelves ([8effa2e](https://github.com/MorpheApp/morphe-patches/commit/8effa2e103947e57455e2b93629186a411c86f37))\n* **YouTube - Feed card layout:** Shorts shelves of the search results are not cards ([12ed02f](https://github.com/MorpheApp/morphe-patches/commit/12ed02f49d3059875435c6d8cdebe548c7e80dd3))\n* **YouTube - Feed card layout:** Show missing watch progress on feed cards ([#3926](https://github.com/MorpheApp/morphe-patches/issues/3926)) ([096798f](https://github.com/MorpheApp/morphe-patches/commit/096798f5877ffce88bac17adef5a9477931071b0))\n* **YouTube - Feed card layout:** Sort buttons of the channel tabs have a card background ([328121e](https://github.com/MorpheApp/morphe-patches/commit/328121e9a5b6552075db00327c12ecf39feda762))\n* **YouTube - Feed card layout:** Vertical video shelves are not cards ([44564ee](https://github.com/MorpheApp/morphe-patches/commit/44564eeff5d617dab0b0210c149a9c190e9f0021))\n* **YouTube - Feed cards layout:** Cards sometimes lose spacing ([0658f00](https://github.com/MorpheApp/morphe-patches/commit/0658f0083bc5d143b379097b706d4114a845a08f))\n* **YouTube - Feed cards layout:** Keep horizontal layout for search results on tablets ([0108028](https://github.com/MorpheApp/morphe-patches/commit/0108028df245e0664974d345c58a291f95a1245e))\n* **YouTube - Feed cards layout:** Make new style more consistent ([#3801](https://github.com/MorpheApp/morphe-patches/issues/3801)) ([5dc0d0c](https://github.com/MorpheApp/morphe-patches/commit/5dc0d0c71ef9c9c11709e62568eb9c1778e9a4c7))\n* **YouTube - Feed video actions:** Show on the grid of landscape ([#3875](https://github.com/MorpheApp/morphe-patches/issues/3875)) ([db5e7d0](https://github.com/MorpheApp/morphe-patches/commit/db5e7d0f97d44032212e7a4451ab38466fd67506))\n* **YouTube - Feed video actions:** Watch later button opens the share sheet ([6c94f32](https://github.com/MorpheApp/morphe-patches/commit/6c94f32df957beda5721729f12385eea3537c975))\n* **YouTube - Feed view count:** Keep the badge away from the edges of inset thumbnails, show lakh and crore units ([544013a](https://github.com/MorpheApp/morphe-patches/commit/544013adcc8881104c003d8d5e3280f725fc676e))\n* **YouTube - Feed view count:** Show on the grid of landscape, same corners and text size as the other badges ([#3874](https://github.com/MorpheApp/morphe-patches/issues/3874)) ([733b6d1](https://github.com/MorpheApp/morphe-patches/commit/733b6d108f09c23892fbe3201dee55e45224b9c3))\n* **YouTube - Force animations:** Animate page transitions when the server turns them off ([79979bc](https://github.com/MorpheApp/morphe-patches/commit/79979bcae286d7bbb11b6ccc06558e812b07637f))\n* **YouTube - Forced video codec:** Excluded VP9 stream with forced AVC superseeded by Shorts VP9 with SABR restarting fix ([2335a8f](https://github.com/MorpheApp/morphe-patches/commit/2335a8f22eed59d36f1c3c7f0cb8de9fa846207f))\n* **YouTube - Hide layout components:** Video titles being hidden on `20.21.37` ([fead993](https://github.com/MorpheApp/morphe-patches/commit/fead993a68706580163825403b3ffd60a6866333))\n* **YouTube - Hide preview comment:** Removed the timestamp from the Spanned String instead of disabling its tap logic ([cca6aca](https://github.com/MorpheApp/morphe-patches/commit/cca6aca240ef99e55740af20342a5d28deda6275))\n* **YouTube - Jam queue sharing:** Keep the YouTube Music player aligned when Jam queue sharing is off ([#3704](https://github.com/MorpheApp/morphe-patches/issues/3704)) ([fd0db0b](https://github.com/MorpheApp/morphe-patches/commit/fd0db0b04bc12f80acde7fc259d7f72dbf724b13))\n* **YouTube - Minimal comments button:** Hide page selector with new layout ([30a9dd1](https://github.com/MorpheApp/morphe-patches/commit/30a9dd17c87658ae405c6223661e478bce2a9629))\n* **YouTube - Minimal miniplayer:** Apply minimized position based on commit [#9d8b0df43](https://github.com/MorpheApp/morphe-patches/issues/9d8b0df43) insets ([7f9fb1f](https://github.com/MorpheApp/morphe-patches/commit/7f9fb1f7df988afc7a21ffa4735227a404c06444))\n* **YouTube - Minimal miniplayer:** Bar is cut off by the display cutout in landscape ([01267da](https://github.com/MorpheApp/morphe-patches/commit/01267da0f015fb868edb6b59e1086778966c7900))\n* **YouTube - Minimal miniplayer:** Show DeArrow title on type 1 style ([72f7b82](https://github.com/MorpheApp/morphe-patches/commit/72f7b82c8ac8d9032d7834abb10ae9f9b86c5ba1))\n* **YouTube - Miniplayer:** Match minimal miniplayer icons by content description name instead of resource id ([4502949](https://github.com/MorpheApp/morphe-patches/commit/4502949a61f6d5d39f72e4052f083cbe1460bd09))\n* **YouTube - Miniplayer:** Watch transition animation is missing ([779d81c](https://github.com/MorpheApp/morphe-patches/commit/779d81c856133efc0e59a83e81f9628cd05a581f))\n* **YouTube - Navigation bar:** App crashes on Android 12-13 when the player is dragged down to the miniplayer ([#3937](https://github.com/MorpheApp/morphe-patches/issues/3937)) ([a6db8d7](https://github.com/MorpheApp/morphe-patches/commit/a6db8d7aba965471d9a51f27ad7324513e2eb40b))\n* **YouTube - Navigation bar:** bug: Navigation bar is not disabled in shorts player ([03f3aa2](https://github.com/MorpheApp/morphe-patches/commit/03f3aa27bc48cfb23a54c723065c83283eed6bc2))\n* **YouTube - Navigation bar:** Increased the spacing between Shorts player and navigation bar when this latter is hidden ([a6fdc2f](https://github.com/MorpheApp/morphe-patches/commit/a6fdc2f1331f5d2fe14da2abb89dfe4b8799ce7f))\n* **YouTube - Navigation bar:** Navigation bar grows and covers the page when the keyboard is open ([7ec9a54](https://github.com/MorpheApp/morphe-patches/commit/7ec9a549a75798c76439a38400dbc325b7cd78df))\n* **YouTube - Navigation bar:** Navigation bar is raised and an empty bar is left at the bottom ([eca3334](https://github.com/MorpheApp/morphe-patches/commit/eca3334c07df995dbbb4f58ed425591883086554))\n* **YouTube - Navigation bar:** Navigation bar is raised and an empty bar is left at the bottom on Android 11 and lower ([e91d606](https://github.com/MorpheApp/morphe-patches/commit/e91d60686bbc66182eab43e510c04d247e5fb323))\n* **YouTube - New feed cards layout:** Rounded edges and padding was wrong ([7d54662](https://github.com/MorpheApp/morphe-patches/commit/7d5466217e5dc7ea1ea323d4f9d7a6baf9f42f8a))\n* **YouTube - Open system share sheet:** Build link from share entity ([#3735](https://github.com/MorpheApp/morphe-patches/issues/3735)) ([92e9eb9](https://github.com/MorpheApp/morphe-patches/commit/92e9eb9d0fa941719c6cf7c048dae16ef163812c))\n* **YouTube - Reload video:** Videos playing in the feed replace the reloaded video ([92e41c4](https://github.com/MorpheApp/morphe-patches/commit/92e41c43d2a0deb766f081e5b36e1556675e4090))\n* **YouTube - Restore original titles:** Do not reload the same video title once the player is opened ([1648848](https://github.com/MorpheApp/morphe-patches/commit/1648848f2b3b2215f0ed3e6a08ddc0877a9365de))\n* **YouTube - Return YouTube Dislike:** Place the feed thumbnail like count on inset thumbnails and channel posts ([#3902](https://github.com/MorpheApp/morphe-patches/issues/3902)) ([de420c2](https://github.com/MorpheApp/morphe-patches/commit/de420c275ea33a3664e5ab6424903ff54c2359c4))\n* **YouTube - Spoof video streams:** In-app downloads are slow when using spoof streaming ([6a8b5a9](https://github.com/MorpheApp/morphe-patches/commit/6a8b5a976c011ea1d365e90db143c9b4d8745976))\n* **YouTube - Video quality:** Fix VP9 Shorts restarting in a loop with spoofed SABR streams ([ef04cc3](https://github.com/MorpheApp/morphe-patches/commit/ef04cc3c12131d0667e598a935425143d1a6dcf1))\n* **YouTube - Video quality:** Improved Shorts VP9 SABR loop restarting fix ([aeccfab](https://github.com/MorpheApp/morphe-patches/commit/aeccfabc5c4f256654ab209ff0741a3b5fada31c))\n* **YouTube - VideoInformation:** Separated Video and Shorts informations ([e913727](https://github.com/MorpheApp/morphe-patches/commit/e913727f3a115f7be807bc32efbf6c054b8fa35d))\n* **YouTube - VideoInformation:** Videos loaded in the background replace the video and playlist IDs of the player ([0ed3595](https://github.com/MorpheApp/morphe-patches/commit/0ed35954b6db94a12956c98b8db620e09dc65dd5))\n* **YouTube Music - Android Auto:** Fix large collections not loading on 9.15 ([#3849](https://github.com/MorpheApp/morphe-patches/issues/3849)) ([1c18d6a](https://github.com/MorpheApp/morphe-patches/commit/1c18d6a02a119d0147593d3615c46dce3a65ff19))\n* **YouTube Music - Crossfade:** Preserve repeat state on 9.40+ ([#3916](https://github.com/MorpheApp/morphe-patches/issues/3916)) ([ce688de](https://github.com/MorpheApp/morphe-patches/commit/ce688de5f6a0693a0df01e37cf25478eaac2524b))\n* **YouTube Music - Enable exclusive audio playback:** Prevent podcasts stopping in Android Auto ([#3850](https://github.com/MorpheApp/morphe-patches/issues/3850)) ([7a2b926](https://github.com/MorpheApp/morphe-patches/commit/7a2b926302f34d1309fb3896ed6720d0db5ab8a5))\n* **YouTube Music:** Experimental app version does not patch ([12fad56](https://github.com/MorpheApp/morphe-patches/commit/12fad5684a453f5f8e359e6f66a2ab04e6caede8))\n* **YouTube:** \"Play next/last in queue\" does nothing from feeds when no video is playing ([c770414](https://github.com/MorpheApp/morphe-patches/commit/c770414fb0ed43ac8f2af7519f7f9222cae8f780))\n* **YouTube:** Add missing experimental compatibility declaration ([a81a933](https://github.com/MorpheApp/morphe-patches/commit/a81a933274cde895d3b4e026c2d084f35b9b20c5))\n* **YouTube:** App can crash when an observer removes itself while it is notified ([5ee5a72](https://github.com/MorpheApp/morphe-patches/commit/5ee5a724433d6b96b7847e98aaf39c096bd81d5f))\n* **YouTube:** Channel search not working properly ([ab2ecc7](https://github.com/MorpheApp/morphe-patches/commit/ab2ecc7a31f6678a698893663ee707d49fc51648))\n* **YouTube:** Comments button page indicator is invisible with the light theme ([0f7f6d8](https://github.com/MorpheApp/morphe-patches/commit/0f7f6d8f6625d73941e211c51167097288f8682d))\n* **YouTube:** Error toasts on app startup with YouTube 21.40+ ([2151a04](https://github.com/MorpheApp/morphe-patches/commit/2151a04d0dfcc7f54e28af645a0e304f9af2d265))\n* **YouTube:** Fixed some 21.42+ fingerprints ([7bf6e1f](https://github.com/MorpheApp/morphe-patches/commit/7bf6e1fe85a75f9fcac4869d5ef548b21e2e5534))\n* **YouTube:** Flyout menu text is cut when Morphe items are added ([f8b7669](https://github.com/MorpheApp/morphe-patches/commit/f8b7669dfd9b10ed68da3cfae4190cb0f2c7430a))\n* **YouTube:** Force show translucent navigation bar ([e5bb8bf](https://github.com/MorpheApp/morphe-patches/commit/e5bb8bfea55258a6926243fbfb7ed19fffea3925))\n* **YouTube:** History items are hidden when the keyboard is open ([3f38b2d](https://github.com/MorpheApp/morphe-patches/commit/3f38b2ded7f857a0240126388686234d3c786892))\n* **YouTube:** Light/Dark theme colors are wrong ([93cf169](https://github.com/MorpheApp/morphe-patches/commit/93cf169e8fa7f96bef741fe5f02f5654b2ae70f6))\n* **YouTube:** Restore Edge TTS and add voice catalog refresh ([#3840](https://github.com/MorpheApp/morphe-patches/issues/3840)) ([4aa4deb](https://github.com/MorpheApp/morphe-patches/commit/4aa4deb6cd6ad4bf01a0f199e984dd3b33449fab))\n* **YouTube:** Some external links can not be opened ([128b8d4](https://github.com/MorpheApp/morphe-patches/commit/128b8d4104e17a5b2527c3f951026f9ff79b4ea6))\n* **YouTube:** Status bar becomes dark ([9d8b0df](https://github.com/MorpheApp/morphe-patches/commit/9d8b0df43ae3cd4dfc1a537311d4dfc6aa600ac5))\n* **YouTube:** Status bar is opaque, black bar above the player and crash on startup ([b5d9498](https://github.com/MorpheApp/morphe-patches/commit/b5d9498577ea285c8dd490bb823bbbd9c6674624))\n* **YouTube:** Video metadata panel text can not be read with the light theme ([332e18e](https://github.com/MorpheApp/morphe-patches/commit/332e18e82b73cc737fc6c84d1126e6fa915821fb))\n* **YouTube:** Watch page background turns black in light mode ([4667cb0](https://github.com/MorpheApp/morphe-patches/commit/4667cb0ef1d13ee756c87976801f366869d7144d))\n\n### ✨ New Features\n\n* **Reddit - Full width feed media:** Make embedded video players full width ([#3696](https://github.com/MorpheApp/morphe-patches/issues/3696)) ([53b6448](https://github.com/MorpheApp/morphe-patches/commit/53b64487109e05f13d8e0bc581f86e581cd4662b))\n* **Reddit - Redgifs audio:** Download Redgifs videos with audio ([#3869](https://github.com/MorpheApp/morphe-patches/issues/3869)) ([f2ed85e](https://github.com/MorpheApp/morphe-patches/commit/f2ed85e67e518ec9115d51d7d6eb0d208232b646))\n* **Reddit:** Add `Copy text` patch ([#3665](https://github.com/MorpheApp/morphe-patches/issues/3665)) ([3068f23](https://github.com/MorpheApp/morphe-patches/commit/3068f23e357825a77f6d67760f427634e3baee0c))\n* **Reddit:** Add `Customize post layout` patch ([#3541](https://github.com/MorpheApp/morphe-patches/issues/3541)) ([a301bc1](https://github.com/MorpheApp/morphe-patches/commit/a301bc1d07ebd96390566cc2d4503ca48a013775))\n* **Reddit:** Add `Download location` patch ([#3870](https://github.com/MorpheApp/morphe-patches/issues/3870)) ([7d0f9b6](https://github.com/MorpheApp/morphe-patches/commit/7d0f9b67193372a92e7065f5fa4f4e366b2ba41c))\n* **Reddit:** Add `Redgifs audio` patch ([#3714](https://github.com/MorpheApp/morphe-patches/issues/3714)) ([b664711](https://github.com/MorpheApp/morphe-patches/commit/b66471189b52974ead5666ac033b6b2ec867de2e))\n* **Reddit:** Add `Remember collapsed comments` patch ([#3517](https://github.com/MorpheApp/morphe-patches/issues/3517)) ([fbd5a82](https://github.com/MorpheApp/morphe-patches/commit/fbd5a82666abeeabd6c7aeb19b7ca993b1ba85c6))\n* **Reddit:** Add `Show flairs in home feed` patch ([#3260](https://github.com/MorpheApp/morphe-patches/issues/3260)) ([93501dd](https://github.com/MorpheApp/morphe-patches/commit/93501dd4b31a103c37509b1c26f7644ca9082aff))\n* **Reddit:** Add media viewer and feed media patches ([#3516](https://github.com/MorpheApp/morphe-patches/issues/3516)) ([1e905b5](https://github.com/MorpheApp/morphe-patches/commit/1e905b5449e6a5468a02f017ce4afb336e8f668e))\n* **Spoof video streams:** Offer to fix playback when PotHelper no longer works or the default client keeps failing ([#3659](https://github.com/MorpheApp/morphe-patches/issues/3659)) ([f996510](https://github.com/MorpheApp/morphe-patches/commit/f996510cfdc71a78c5c9579efcf7d604e70ae1ea))\n* Update to Patcher 2.0 ([#3945](https://github.com/MorpheApp/morphe-patches/issues/3945)) ([d7c25c2](https://github.com/MorpheApp/morphe-patches/commit/d7c25c26031e77c051973fb71b761fa991ee7ddb))\n* **YouTube - Add to queue:** Add Premium-style queue setting ([#3582](https://github.com/MorpheApp/morphe-patches/issues/3582)) ([1b1b2bc](https://github.com/MorpheApp/morphe-patches/commit/1b1b2bc74bb49c0553225fc7b691c55fbc961a69))\n* **YouTube - Captions:** Add \"Show captions in picture-in-picture\" setting ([#3907](https://github.com/MorpheApp/morphe-patches/issues/3907)) ([6c817e7](https://github.com/MorpheApp/morphe-patches/commit/6c817e7daa49a1b0bf007f31fc7eece9ff3af566))\n* **YouTube - Captions:** Add additional automatic translation languages ([#3754](https://github.com/MorpheApp/morphe-patches/issues/3754)) ([c036e5f](https://github.com/MorpheApp/morphe-patches/commit/c036e5f2e30b467a7c2e3af3ae948c0715efec48))\n* **YouTube - Channel search:** Added loading animation during searching ([d3fc642](https://github.com/MorpheApp/morphe-patches/commit/d3fc6421ca3f8e7ac6cc162d1a51b24c4b087e6e))\n* **YouTube - Disable video codecs:** Add \"Force VP9 codec\" setting ([#3578](https://github.com/MorpheApp/morphe-patches/issues/3578)) ([e1398ca](https://github.com/MorpheApp/morphe-patches/commit/e1398ca7da9dc7829aa8e9d2f798206fff26c1f6))\n* **YouTube - Downloads:** Add \"Download in-app\" setting ([#3905](https://github.com/MorpheApp/morphe-patches/issues/3905)) ([ef266aa](https://github.com/MorpheApp/morphe-patches/commit/ef266aa4bf94bf63e5b85a31673c297bf0ef81ef))\n* **YouTube - Force animations:** Expand the content below the player with its animation when it loads ([b0020b2](https://github.com/MorpheApp/morphe-patches/commit/b0020b2513fffb6954c5925af3213958b947f94f))\n* **YouTube - Hide layout components:** Add \"Hide 'No comments yet' message\" setting ([#3819](https://github.com/MorpheApp/morphe-patches/issues/3819)) ([32168f9](https://github.com/MorpheApp/morphe-patches/commit/32168f9ebff2269c98ea5d9adedda07394342ab9))\n* **YouTube - Hide layout components:** Add \"Hide live chat scroll to bottom button\" setting ([#3826](https://github.com/MorpheApp/morphe-patches/issues/3826)) ([1956988](https://github.com/MorpheApp/morphe-patches/commit/19569880caabbaed642ce9c48e829d22141405dd))\n* **YouTube - Hide status bar:** Add \"Fix status bar covering the top bars\" setting ([caa1be0](https://github.com/MorpheApp/morphe-patches/commit/caa1be0fe5faf3351fa49a02667f03ef4946b34f))\n* **YouTube - Player icon style:** Add Series tracker button icons ([179ff66](https://github.com/MorpheApp/morphe-patches/commit/179ff663d36806e11bd3869df8d1a66287072a7f))\n* **YouTube - Restore original titles:** Restore titles of watch history and fullscreen suggested videos ([f55e541](https://github.com/MorpheApp/morphe-patches/commit/f55e54163bd04b7b6fdd3e015951ad6bf4f8d42f))\n* **YouTube - Return YouTube Dislike:** Add \"Show feed thumbnail like count\" setting ([#3786](https://github.com/MorpheApp/morphe-patches/issues/3786)) ([7b2fd85](https://github.com/MorpheApp/morphe-patches/commit/7b2fd85413a12a955dfcd8ff9928790c2ac993e8))\n* **YouTube - Series tracking:** Add \"Show Series in navigation bar\" setting ([#3814](https://github.com/MorpheApp/morphe-patches/issues/3814)) ([abaca95](https://github.com/MorpheApp/morphe-patches/commit/abaca951b67eeae3dd9f1a9bc8f9c09e72b7c20b))\n* **YouTube Music - Android Auto:** Add Home, Library, and search features ([#3852](https://github.com/MorpheApp/morphe-patches/issues/3852)) ([65e200f](https://github.com/MorpheApp/morphe-patches/commit/65e200f623efc8b651a338360555884b717e05af))\n* **YouTube Music - Third-party lyrics:** Add new providers ([#3717](https://github.com/MorpheApp/morphe-patches/issues/3717)) ([89e31ba](https://github.com/MorpheApp/morphe-patches/commit/89e31ba6d69e26a08b5bdd97b3afb4daa927048f))\n* **YouTube Music - Third-party lyrics:** Custom romanization target ([#3750](https://github.com/MorpheApp/morphe-patches/issues/3750)) ([0dfc834](https://github.com/MorpheApp/morphe-patches/commit/0dfc8347118bb9c10e8dd8275996d31afb5f5d37))\n* **YouTube Music - Third-party lyrics:** Local romanization and alignment ([#3851](https://github.com/MorpheApp/morphe-patches/issues/3851)) ([9adcefd](https://github.com/MorpheApp/morphe-patches/commit/9adcefda0e18fb08a5e70933f243cdbc1d99e502))\n* **YouTube Music:** Add `Custom font` patch ([#3718](https://github.com/MorpheApp/morphe-patches/issues/3718)) ([30a4b85](https://github.com/MorpheApp/morphe-patches/commit/30a4b85885e96aa8216f2b9fb26ed0ba4951e8fe))\n* **YouTube:** Add \"Force animations\" patch, superseding the Miniplayer watch transition fix ([#3872](https://github.com/MorpheApp/morphe-patches/issues/3872)) ([591ad8b](https://github.com/MorpheApp/morphe-patches/commit/591ad8bd917bc168b0144014022438e395624bef))\n* **YouTube:** Add `Ask flyout menu` patch ([#3897](https://github.com/MorpheApp/morphe-patches/issues/3897)) ([04c76d0](https://github.com/MorpheApp/morphe-patches/commit/04c76d0ace8e3e5c6d4e4d015e7bc231199b9c02))\n* **YouTube:** Add `Custom font` patch ([#3673](https://github.com/MorpheApp/morphe-patches/issues/3673)) ([fc8c837](https://github.com/MorpheApp/morphe-patches/commit/fc8c837390b05edbc5262ea786d1308729e80c24))\n* **YouTube:** Add `Feed video actions` patch ([#3758](https://github.com/MorpheApp/morphe-patches/issues/3758)) ([b263c08](https://github.com/MorpheApp/morphe-patches/commit/b263c08af15eb0a72f6ea707bd49597d95209d29))\n* **YouTube:** Add `Feed view count` patch ([#3862](https://github.com/MorpheApp/morphe-patches/issues/3862)) ([cf0ce16](https://github.com/MorpheApp/morphe-patches/commit/cf0ce16d8304c31ca7a89e730c09debe07a67390))\n* **YouTube:** Add `Series tracking` patch ([#3114](https://github.com/MorpheApp/morphe-patches/issues/3114)) ([d98558f](https://github.com/MorpheApp/morphe-patches/commit/d98558fde76a7d2f49579a9f794423cd765211c9))\n* **YouTube:** Add `Video delay` patch ([#3867](https://github.com/MorpheApp/morphe-patches/issues/3867)) ([ec246e2](https://github.com/MorpheApp/morphe-patches/commit/ec246e292e6c7c18373bb486a53c3db205efb83f))\n* **YouTube:** Add playback speed for Shorts and skip live Shorts ([#3847](https://github.com/MorpheApp/morphe-patches/issues/3847)) ([3731d97](https://github.com/MorpheApp/morphe-patches/commit/3731d97f6ec4eaedbccaa7b7e8d4775c8cf9d587))\n* **YouTube:** Added new feed card layout ([#3768](https://github.com/MorpheApp/morphe-patches/issues/3768)) ([02a4c3d](https://github.com/MorpheApp/morphe-patches/commit/02a4c3d36745f133bfae4983aa615984970631a2))\n* **YouTube:** Added new feed cards layout without action buttons ([#3769](https://github.com/MorpheApp/morphe-patches/issues/3769)) ([096b0bb](https://github.com/MorpheApp/morphe-patches/commit/096b0bb3e7a98e00ba56ce3dc4260c0b7106e905))\n\n### 🚀 Updated App Support\n\n* **Reddit:** Add experimental support for `2026.42.0` ([1302ae1](https://github.com/MorpheApp/morphe-patches/commit/1302ae1d890588d388b18d0c1cd54313689a2ea7))\n* **YouTube Music:** Add experimental support for `9.41.52` ([003a445](https://github.com/MorpheApp/morphe-patches/commit/003a445df12a25f0970a6060853d12ccfbcae7b1))\n* **YouTube:** Add experimental support for `21.41.525` ([#3658](https://github.com/MorpheApp/morphe-patches/issues/3658)) ([95eb1fb](https://github.com/MorpheApp/morphe-patches/commit/95eb1fbabe8bdb6251c20b962142cd4daefc1e47))\n* **YouTube:** Add experimental support for `21.42.240` ([463a4ba](https://github.com/MorpheApp/morphe-patches/commit/463a4ba0a33cdc72111bf3a5d2d4698d1d3c6bf5))", - "download_url": "https://github.com/MorpheApp/morphe-patches/releases/download/v1.48.0/patches-1.48.0.mpp", + "created_at": "2026-10-23T10:56:14", + "description": "## [1.49.0-dev.1](https://github.com/MorpheApp/morphe-patches/compare/v1.48.0...v1.49.0-dev.1) (2026-10-23)\n\n### 🐛 Bug Fixes\n\n* **YouTube - Hide status bar:** Video player is moved below the display cutout in portrait ([f80398e](https://github.com/MorpheApp/morphe-patches/commit/f80398e107587c7fb9f995f508a65ad1953b2734))\n* **YouTube - Navigation bar:** Translucent navigation bar is not shown without the edge to edge layout ([b8e1fdd](https://github.com/MorpheApp/morphe-patches/commit/b8e1fddd8a9d7146f2ccc4d1522a86b134cfe7a6))\n* **YouTube:** Miniplayer covered by the keyboard and empty band above the navigation bar ([0345136](https://github.com/MorpheApp/morphe-patches/commit/0345136b552c49b70c01fcc19a18762f57d3b7ad))\n* **YouTube:** Morphe flyout menu items are missing on tablets ([7127541](https://github.com/MorpheApp/morphe-patches/commit/71275419c5e6b8bd6a0a108e3d7815c8ef11a88e))\n\n### ✨ New Features\n\n* **YouTube - Navigation bar:** Add \"Disable toolbar navigation tabs\" setting ([01e767c](https://github.com/MorpheApp/morphe-patches/commit/01e767c4c3a386e4b7881ce7fc867a516082db23))\n* **YouTube:** Add `Search comments` patch ([#3942](https://github.com/MorpheApp/morphe-patches/issues/3942)) ([fbb628b](https://github.com/MorpheApp/morphe-patches/commit/fbb628ba23ecdcabd00b3bdfbf94aff60f58ac68))\n\n### 🚀 Updated App Support\n\n* **YouTube:** Add support for `21.23.492` ([4599204](https://github.com/MorpheApp/morphe-patches/commit/45992041db85e4fb51900cc5a773380732dc1fda))", + "download_url": "https://github.com/MorpheApp/morphe-patches/releases/download/v1.49.0-dev.1/patches-1.49.0-dev.1.mpp", "signature_download_url": "", - "version": "1.48.0" + "version": "1.49.0-dev.1" } diff --git a/patches-list.json b/patches-list.json index a5662d30d3..7cb1f83559 100644 --- a/patches-list.json +++ b/patches-list.json @@ -1,6 +1,6 @@ { "NOTE": "Do NOT manually edit this file. This file is automatically updated when semantic release (release.yml) runs. Manually editing this file can break your releases and break third party tools that use this file.", - "version": "1.48.0", + "version": "1.49.0-dev.1", "patches": [ { "name": "AI channel filter", @@ -44,7 +44,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -123,7 +123,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -202,7 +202,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -454,7 +454,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -533,7 +533,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -612,7 +612,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -691,7 +691,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -770,7 +770,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -849,7 +849,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -996,7 +996,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -1225,7 +1225,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -1304,7 +1304,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -1383,7 +1383,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -1520,7 +1520,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -1694,7 +1694,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -1773,7 +1773,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -2005,7 +2005,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -2305,7 +2305,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -2384,7 +2384,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -2520,7 +2520,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -2657,7 +2657,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -2802,7 +2802,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -2881,7 +2881,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -2960,7 +2960,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -3039,7 +3039,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -3176,7 +3176,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -3255,7 +3255,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -3334,7 +3334,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -3413,7 +3413,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -3549,7 +3549,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -3628,7 +3628,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -3707,7 +3707,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -3843,7 +3843,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -3922,7 +3922,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -4001,7 +4001,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -4080,7 +4080,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -4274,7 +4274,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -4469,7 +4469,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -4722,7 +4722,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -4801,7 +4801,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -4937,7 +4937,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -5016,7 +5016,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -5095,7 +5095,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -5174,7 +5174,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -5311,7 +5311,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -5562,7 +5562,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -5699,7 +5699,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -5892,7 +5892,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -6162,7 +6162,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -6241,7 +6241,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -6435,7 +6435,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -6514,7 +6514,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -6651,7 +6651,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -6788,7 +6788,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -7039,7 +7039,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -7118,7 +7118,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -7197,7 +7197,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -7276,7 +7276,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -7412,7 +7412,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -7491,7 +7491,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -7570,7 +7570,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -7764,7 +7764,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -7901,7 +7901,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -8094,7 +8094,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -8231,7 +8231,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -8368,7 +8368,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -8524,7 +8524,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -8603,7 +8603,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -8682,7 +8682,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -8875,7 +8875,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -8954,7 +8954,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -9033,7 +9033,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -9112,7 +9112,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -9199,7 +9199,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -9336,7 +9336,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -9415,7 +9415,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -9494,7 +9494,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -9631,7 +9631,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -9710,7 +9710,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -9915,7 +9915,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -10051,7 +10051,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -10187,7 +10187,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -10440,7 +10440,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -10634,7 +10634,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -10713,7 +10713,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -10850,7 +10850,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -11044,7 +11044,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -11123,7 +11123,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -11218,6 +11218,85 @@ ], "options": [] }, + { + "name": "Search comments", + "description": "Adds a button to the comments to show only the loaded comments that contain the searched words.", + "default": true, + "category": null, + "compatiblePackages": [ + { + "packageName": "com.google.android.youtube", + "name": "YouTube", + "description": null, + "apkFileType": "APK_REQUIRED", + "appIconColor": "#FF0033", + "signatures": [ + "5aad2bee6db95d17e05a08d7d1e64c10a1511879154483916b6ae6c7fd9cb0c6", + "3d7a1223019aa39d9ea0e3436ab7c0896bfb4fb679f4de5fe7c23f326c8f994a" + ], + "formFactor": "MOBILE", + "targets": [ + { + "version": "21.42.241", + "versionCodes": null, + "isExperimental": true, + "minSdk": 29, + "description": null + }, + { + "version": "21.41.525", + "versionCodes": null, + "isExperimental": true, + "minSdk": 29, + "description": null + }, + { + "version": "21.28.208", + "versionCodes": null, + "isExperimental": true, + "minSdk": 29, + "description": null + }, + { + "version": "21.23.492", + "versionCodes": null, + "isExperimental": false, + "minSdk": 28, + "description": null + }, + { + "version": "21.20.405", + "versionCodes": null, + "isExperimental": false, + "minSdk": 28, + "description": null + }, + { + "version": "21.16.256", + "versionCodes": null, + "isExperimental": false, + "minSdk": 28, + "description": null + }, + { + "version": "20.51.39", + "versionCodes": null, + "isExperimental": false, + "minSdk": 28, + "description": null + }, + { + "version": "20.21.37", + "versionCodes": null, + "isExperimental": false, + "minSdk": 26, + "description": null + } + ] + } + ], + "options": [] + }, { "name": "Seekbar", "description": "Adds options to show old seekbar thumbnails, disable precise seeking when swiping up on the seekbar, slide to seek instead of playing at 2x speed when pressing and holding, tapping the player seekbar to seek, hiding the video player seekbar, enabling seeking in live streams, and expanding the live stream DVR duration.", @@ -11260,7 +11339,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -11339,7 +11418,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -11476,7 +11555,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -11555,7 +11634,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -11634,7 +11713,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -11780,7 +11859,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -11917,7 +11996,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -12054,7 +12133,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -12191,7 +12270,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -12270,7 +12349,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -12479,7 +12558,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -12615,7 +12694,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -12778,7 +12857,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -12964,7 +13043,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -13043,7 +13122,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -13122,7 +13201,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, @@ -13201,7 +13280,7 @@ { "version": "21.23.492", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 28, "description": null }, From d2accff5135170a4288f326d89b25a81f688f063 Mon Sep 17 00:00:00 2001 From: 0xrxL <127248639+0xrxL@users.noreply.github.com> Date: Fri, 23 Oct 2026 14:45:40 +0200 Subject: [PATCH 013/189] fix(YouTube): Morphe flyout menu items are missing in the new popup menu --- .../youtube/patches/utils/FlyoutUtils.java | 33 ++++++++++++------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/FlyoutUtils.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/FlyoutUtils.java index 962d008f90..107e7e5534 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/FlyoutUtils.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/FlyoutUtils.java @@ -796,17 +796,7 @@ private static FlyoutMenuInfo getFlyoutMenuInfo(Object flyoutPanel, int initialI if (flyoutPanel instanceof PopupWindow checkedPopupWindow) { popupWindow = checkedPopupWindow; - View contentView = checkedPopupWindow.getContentView(); - if (contentView instanceof FrameLayout frameLayout) { - if (frameLayout.getChildAt(0) instanceof ViewGroup viewGroup && - viewGroup.getChildAt(0) instanceof LinearLayout checkedMenuContainer) { - menuContainer = checkedMenuContainer; - } - } else if (contentView instanceof ViewGroup viewGroup && - viewGroup.getChildAt(0) instanceof LinearLayout checkedMenuContainer) { - // The contextual menu of tablets is wrapped in a view that places it next to its anchor. - menuContainer = checkedMenuContainer; - } + menuContainer = getPopupMenuContainer(checkedPopupWindow.getContentView()); } else if (flyoutPanel instanceof Dialog checkedDialog) { Window window = checkedDialog.getWindow(); if (window != null) { @@ -837,6 +827,27 @@ private static FlyoutMenuInfo getFlyoutMenuInfo(Object flyoutPanel, int initialI return new FlyoutMenuInfo(menuContainer, adjustedIndex, popupWindow); } + /** + * The menu container of a popup menu is the first LinearLayout found by following the first child + * of the content view. Depending on the app version, the device and A/B tests, the content view is: + * the container itself, a view that places the container next to its anchor (also wrapped + * in a frame on phones), or a frame that sizes the popup window to the container. + * + * @return The menu container, or null if the content view is not a popup menu. + */ + @Nullable + private static LinearLayout getPopupMenuContainer(@Nullable View contentView) { + View view = contentView; + // The deepest container is in a frame, inside the view that places it. + for (int depth = 0; depth < 3 && view != null; depth++) { + if (view instanceof LinearLayout linearLayout) { + return linearLayout; + } + view = view instanceof ViewGroup viewGroup ? viewGroup.getChildAt(0) : null; + } + return null; + } + @SuppressLint("ResourceType") private static View addFlyoutButton( Context context, From 47f511c7ae41de2b2ac83257d5a93c4df1216526 Mon Sep 17 00:00:00 2001 From: 0xrxL <127248639+0xrxL@users.noreply.github.com> Date: Fri, 23 Oct 2026 15:13:19 +0200 Subject: [PATCH 014/189] fix(YouTube - Navigation buttons): Create button is not hidden with the new navigation bar layout --- .../morphe/extension/youtube/shared/NavigationBar.java | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/shared/NavigationBar.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/shared/NavigationBar.java index 11620bf0e9..5067cdd73b 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/shared/NavigationBar.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/shared/NavigationBar.java @@ -435,6 +435,9 @@ public static void navigationTabLoaded(final View navigationButtonGroup) { goingBackCancelled = false; String lastEnumName = lastYTNavigationEnumName; + // The enum is of this tab only. The 'You' tab has no enum, and it's + // created right after the Create tab of the newer layout. + lastYTNavigationEnumName = null; for (NavigationButton buttonType : NavigationButton.values()) { if (buttonType.ytEnumNames.contains(lastEnumName)) { @@ -652,7 +655,12 @@ public enum NavigationButton { * Create new video tab. * This tab will never be in a selected state, even if the Create video UI is on screen. */ - CREATE("CREATION_TAB_LARGE", "CREATION_TAB_LARGE_CAIRO"), + CREATE( + "CREATION_TAB_LARGE", + "CREATION_TAB_LARGE_CAIRO", + // Newer layout, where the tab is a regular tab next to the 'You' tab. + "ADD_CIRCLE_OUTLINE" + ), /** * Only shown when 'Show Search' is turned on. */ From be733a6883cdf4c5fc8176668afb1e7f5c22cb9f Mon Sep 17 00:00:00 2001 From: 0xrxL <127248639+0xrxL@users.noreply.github.com> Date: Fri, 23 Oct 2026 15:33:46 +0200 Subject: [PATCH 015/189] fix(YouTube - Playback speed): Channel speed whitelist does not work for Shorts --- .../speed/RememberPlaybackSpeedPatch.java | 56 ++++++++++++++++--- 1 file changed, 49 insertions(+), 7 deletions(-) diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/playback/speed/RememberPlaybackSpeedPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/playback/speed/RememberPlaybackSpeedPatch.java index 3646c33d22..f122399575 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/playback/speed/RememberPlaybackSpeedPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/playback/speed/RememberPlaybackSpeedPatch.java @@ -46,34 +46,71 @@ public final class RememberPlaybackSpeedPatch { private static long lastTimePitchChanged; + /** + * The speed last set in the Shorts player. + */ + private static volatile float shortsPlaybackSpeed = -2.0f; + static { // The Shorts player does not set a playback speed when a Short starts, // so the Shorts speed is set once when the Shorts player opens. ShortsPlayerState.getOnChange().addObserver((Boolean isOpen) -> { if (isOpen) { - applyShortsPlaybackSpeed(); + applyShortsPlaybackSpeed(true); } return Unit.INSTANCE; }); + + // The app only sets the speed of a Short if it differs from the speed it uses, + // so after a Short of a whitelisted channel the next Short would stay at 1.0x. + // The speed is set again once the channel of the Short is known. + VideoInformation.onChannelIdChange.addObserver((String channelId) -> { + if (ShortsPlayerState.isOpen()) { + applyShortsPlaybackSpeed(false); + } + return Unit.INSTANCE; + }); + } + + /** + * @return The speed of the current Short: 1.0x if its channel is whitelisted, + * otherwise the Shorts default speed. + */ + private static float getShortsPlaybackSpeed(float shortsDefaultSpeed) { + return ChannelWhitelist.isCurrentChannelWhitelisted(WhitelistType.PLAYBACK_SPEED) + ? 1.0f + : shortsDefaultSpeed; } /** - * Sets the Shorts default speed on the player. + * Sets the Shorts speed on the player. * The Shorts player has no playback speed menu interface, so * {@link VideoInformation#changePlaybackSpeed(float)} cannot be used. * The speed set here is kept for every following Short. + * + * @param playerOpened If the Shorts player just opened. The channel is then still the one + * of the last Short played, so the default speed is used, and the + * speed is set again once the channel of the Short is known. */ - private static void applyShortsPlaybackSpeed() { + private static void applyShortsPlaybackSpeed(boolean playerOpened) { try { final float shortsDefaultSpeed = Settings.SHORTS_PLAYBACK_SPEED_DEFAULT.get(); if (shortsDefaultSpeed <= 0 || VideoInformation.isLiveStream()) { return; } + final float speed = playerOpened + ? shortsDefaultSpeed + : getShortsPlaybackSpeed(shortsDefaultSpeed); + if (!playerOpened && speed == shortsPlaybackSpeed) { + return; + } + shortsPlaybackSpeed = speed; + Utils.runOnMainThreadNowOrLater(() -> { - Logger.printDebug(() -> "Shorts speed: applying " + shortsDefaultSpeed); + Logger.printDebug(() -> "Shorts speed: applying " + speed); VideoInformation.setPlaybackParameters( - shortsDefaultSpeed, VideoInformation.getPlaybackAudioPitch()); + speed, VideoInformation.getPlaybackAudioPitch()); }); } catch (Exception ex) { Logger.printException(() -> "applyShortsPlaybackSpeed failure", ex); @@ -90,6 +127,8 @@ public static void onLiveStreamChanged(boolean isLive) { return; } + shortsPlaybackSpeed = 1.0f; + Utils.runOnMainThreadNowOrLater(() -> { Logger.printDebug(() -> "Shorts speed: live stream detected, restoring 1.0x"); VideoInformation.setPlaybackParameters( @@ -220,8 +259,11 @@ public static float getPlaybackSpeedInShorts(float playbackSpeed) { return playbackSpeed; } - Logger.printDebug(() -> "Shorts speed: overriding " + playbackSpeed + " to " + shortsDefaultSpeed); - return shortsDefaultSpeed; + final float speed = getShortsPlaybackSpeed(shortsDefaultSpeed); + shortsPlaybackSpeed = speed; + + Logger.printDebug(() -> "Shorts speed: overriding " + playbackSpeed + " to " + speed); + return speed; } catch (Exception ex) { Logger.printException(() -> "getPlaybackSpeedInShorts failure", ex); return playbackSpeed; From 60efeb58d08ba0e347928c7f4f903a689b474bfd Mon Sep 17 00:00:00 2001 From: 0xrxL <127248639+0xrxL@users.noreply.github.com> Date: Fri, 23 Oct 2026 16:33:12 +0200 Subject: [PATCH 016/189] fix(YouTube - Shorts autoplay): Overlay is out of place when going back to a Short that advanced with the comments open --- .../youtube/patches/ShortsAutoplayPatch.java | 15 ++++++++++++++- .../layout/shortsautoplay/ShortsAutoplayPatch.kt | 2 ++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/ShortsAutoplayPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/ShortsAutoplayPatch.java index f4acb8a4bf..971d8881e5 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/ShortsAutoplayPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/ShortsAutoplayPatch.java @@ -17,6 +17,7 @@ import app.morphe.extension.shared.Logger; import app.morphe.extension.shared.Utils; import app.morphe.extension.youtube.settings.Settings; +import app.morphe.extension.youtube.shared.EngagementPanel; @SuppressWarnings("unused") public class ShortsAutoplayPatch { @@ -95,10 +96,22 @@ public static void setYTShortsRepeatEnum(Enum ytEnum) { */ public static Enum changeShortsRepeatBehavior(Enum original) { try { - final boolean autoplay = isAppInBackgroundPiPMode() + final boolean inBackgroundPiP = isAppInBackgroundPiPMode(); + boolean autoplay = inBackgroundPiP ? Settings.SHORTS_AUTOPLAY_BACKGROUND.get() : Settings.SHORTS_AUTOPLAY.get(); + // Like the app's own auto advance, a Short with a panel open, such as the comments, + // is repeated. Otherwise the Short behind the panel keeps the layout of the open panel, + // and its overlay is out of place when going back to it. + if (autoplay && !inBackgroundPiP) { + String panelId = EngagementPanel.getLastOpenedPanelId(); + if (panelId != null) { + Logger.printDebug(() -> "Not advancing, panel is open: " + panelId); + autoplay = false; + } + } + ShortsLoopBehavior autoPlayBehavior = VersionCheckPatch.IS_21_10_OR_GREATER ? ShortsLoopBehavior.AUTO_ADVANCE : ShortsLoopBehavior.SINGLE_PLAY; diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/shortsautoplay/ShortsAutoplayPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/shortsautoplay/ShortsAutoplayPatch.kt index 8dd12efc76..6f2e0032d9 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/shortsautoplay/ShortsAutoplayPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/shortsautoplay/ShortsAutoplayPatch.kt @@ -15,6 +15,7 @@ import app.morphe.patcher.patch.bytecodePatch import app.morphe.patcher.util.getReference import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable import app.morphe.patches.shared.misc.settings.preference.SwitchPreference +import app.morphe.patches.youtube.misc.engagement.engagementPanelHookPatch import app.morphe.patches.youtube.misc.extension.sharedExtensionPatch import app.morphe.patches.youtube.misc.playservice.is_21_10_or_greater import app.morphe.patches.youtube.misc.playservice.is_21_17_or_greater @@ -45,6 +46,7 @@ val shortsAutoplayPatch = bytecodePatch( sharedExtensionPatch, settingsPatch, versionCheckPatch, + engagementPanelHookPatch, ) compatibleWith(COMPATIBILITY_YOUTUBE) From e0922124073fe53b6ff335893b8208af512f34e7 Mon Sep 17 00:00:00 2001 From: Zahran Ansaria Date: Fri, 23 Oct 2026 21:41:35 +0700 Subject: [PATCH 017/189] feat(YouTube Music - Third-party lyrics): enhance sync engine, providers, AI translation, and RTL word sync (#3877) --- .../music/patches/lyrics/LyricsManager.java | 174 +++- .../music/patches/lyrics/LyricsProviders.java | 9 +- .../patches/lyrics/derive/AiProviders.java | 65 ++ .../patches/lyrics/derive/DeepLClient.java | 123 +++ .../patches/lyrics/derive/GeminiClient.java | 156 +++ .../lyrics/derive/LyricsRomanizer.java | 895 ++++++++++++++++-- .../lyrics/derive/LyricsTranslator.java | 50 +- .../patches/lyrics/derive/OpenAIClient.java | 195 +++- .../patches/lyrics/model/LyricsLine.java | 58 +- .../patches/lyrics/model/LyricsMerge.java | 48 +- .../patches/lyrics/parsers/LRCParser.java | 247 ++++- .../patches/lyrics/parsers/TTMLParser.java | 238 +++-- .../lyrics/requests/BinimumProvider.java | 340 ++++++- .../lyrics/requests/DeezerProvider.java | 3 + .../lyrics/requests/LrcRedProvider.java | 410 ++++++++ .../lyrics/requests/MusixmatchProvider.java | 63 +- .../lyrics/requests/PetitLyricsProvider.java | 288 +++++- .../lyrics/requests/SpotifyProvider.java | 19 +- .../lyrics/romanizers/ScriptDetector.java | 50 + .../patches/lyrics/storage/LyricsCache.java | 78 +- .../patches/lyrics/ui/LyricsLineView.java | 71 +- .../patches/lyrics/ui/LyricsPanelView.java | 84 +- .../patches/lyrics/ui/LyricsSpanBuilder.java | 74 +- .../extension/music/settings/Settings.java | 46 +- .../preference/LyricsAiConfigPreference.java | 468 ++++++--- .../preference/LyricsAiPromptPreference.java | 8 - .../LyricsOrderedListPreference.java | 10 +- .../music/layout/lyrics/LyricsPatch.kt | 33 +- .../addresources/values/music/arrays.xml | 29 +- .../addresources/values/music/strings.xml | 57 +- 30 files changed, 3799 insertions(+), 590 deletions(-) create mode 100644 extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/derive/AiProviders.java create mode 100644 extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/derive/DeepLClient.java create mode 100644 extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/derive/GeminiClient.java create mode 100644 extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/LrcRedProvider.java diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/LyricsManager.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/LyricsManager.java index 73fbc5df08..c26fac0076 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/LyricsManager.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/LyricsManager.java @@ -10,7 +10,9 @@ import android.media.MediaMetadata; import android.media.session.PlaybackState; import android.net.Uri; +import android.os.Looper; import android.os.SystemClock; +import android.view.Choreographer; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -32,15 +34,18 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; +import app.morphe.extension.music.patches.lyrics.derive.LyricsRomanizer; import app.morphe.extension.music.patches.lyrics.derive.SectionDetector; import app.morphe.extension.music.patches.lyrics.model.Lyrics; import app.morphe.extension.music.patches.lyrics.model.LyricsBracketSplitter; import app.morphe.extension.music.patches.lyrics.model.LyricsFilter; import app.morphe.extension.music.patches.lyrics.model.LyricsFilterDefaults; import app.morphe.extension.music.patches.lyrics.model.LyricsLine; +import app.morphe.extension.music.patches.lyrics.model.LyricsMerge; import app.morphe.extension.music.patches.lyrics.model.LyricsPreference; import app.morphe.extension.music.patches.lyrics.model.MetadataCleaner; import app.morphe.extension.music.patches.lyrics.model.TrackInfo; +import app.morphe.extension.music.patches.lyrics.model.Word; import app.morphe.extension.music.patches.lyrics.parsers.CharactersConverter; import app.morphe.extension.music.patches.lyrics.requests.LocalLyricsFetcher; import app.morphe.extension.music.patches.lyrics.requests.LunaBeatProvider; @@ -191,15 +196,97 @@ private void finishFetches(List> futures) { */ private final AtomicInteger requestId = new AtomicInteger(); - private long positionMs; - private long positionUpdatedAtUptimeMs; private long lastVideoTimeSample = -1; - private long lastPlaybackSampleUptimeMs; private float playbackSpeed = 1f; private boolean playing; - private static final long VIDEO_REANCHOR_BEHIND_MS = 1000; - private static final long PLAYBACK_STALE_MS = 500; + /** + * A sample further than this from the estimate is a seek, which the estimate jumps to + * instead of catching up with. + */ + private static final long SEEK_THRESHOLD_MS = 1000; + /** A longer frame gap, such as after the app was in the background, is not extrapolated over. */ + private static final long MAX_FRAME_STEP_MS = 100; + + // The position is sampled a few times a second, and estimated in between on every frame + // so the highlight moves smoothly. The estimate only ever moves forward while playing. + private long lastSamplePosition = -1; + private long lastSampleTimeMs; + private long lastCorrectedSampleTimeMs; + private long lastFrameTimeUptimeMs = -1; + private double estimate; + private long smoothPosition; + private boolean frameCallbackScheduled; + /** Frames are only followed while something reads the position, such as an open lyrics panel. */ + private boolean positionReadSinceFrame; + + private final Choreographer.FrameCallback frameCallback = this::onFrame; + + private void onFrame(long frameTimeNanos) { + frameCallbackScheduled = false; + final long videoTime = VideoInformation.getVideoTime(); + if (videoTime >= 0 && videoTime != lastVideoTimeSample) { + updateSample(videoTime, SystemClock.uptimeMillis()); + } + + if (!playing || lastSamplePosition < 0 || !positionReadSinceFrame) { + lastFrameTimeUptimeMs = -1; + return; + } + positionReadSinceFrame = false; + + final long frameTimeMs = frameTimeNanos / 1_000_000L; + final float speed = Math.max(0.1f, playbackSpeed); + if (lastFrameTimeUptimeMs == -1) { + // Starting or resuming: carry the last sample forward by the time since it was taken. + lastFrameTimeUptimeMs = frameTimeMs; + estimate = Math.max(estimate, lastSamplePosition + + (SystemClock.uptimeMillis() - lastSampleTimeMs) * speed); + } + final long stepMs = Math.max(0, Math.min(MAX_FRAME_STEP_MS, frameTimeMs - lastFrameTimeUptimeMs)); + lastFrameTimeUptimeMs = frameTimeMs; + estimate += stepMs * speed; + + // A new sample pulls a lagging estimate towards it, harder the further behind it is. + if (lastSampleTimeMs != lastCorrectedSampleTimeMs) { + lastCorrectedSampleTimeMs = lastSampleTimeMs; + final double error = lastSamplePosition - estimate; + if (Math.abs(error) > SEEK_THRESHOLD_MS) { + estimate = lastSamplePosition; + } else if (error > 500) { + estimate += error * 0.8; + } else if (error > 100) { + estimate += error * 0.5; + } else if (error > 0) { + estimate += error * 0.3; + } + } + + estimate = Math.max(estimate, smoothPosition); + smoothPosition = Math.round(estimate); + ensureFrameCallback(); + } + + private void ensureFrameCallback() { + if (!playing || frameCallbackScheduled) { + return; + } + if (Looper.myLooper() == Looper.getMainLooper()) { + Choreographer.getInstance().postFrameCallback(frameCallback); + frameCallbackScheduled = true; + } else { + Utils.runOnMainThread(this::ensureFrameCallback); + } + } + + private void stopFrameCallback() { + frameCallbackScheduled = false; + if (Looper.myLooper() == Looper.getMainLooper()) { + Choreographer.getInstance().removeFrameCallback(frameCallback); + } else { + Utils.runOnMainThread(this::stopFrameCallback); + } + } private int lastHighlightedIndex = -1; @@ -331,13 +418,28 @@ public boolean hasLyrics() { return state == State.LOADED && current != null && !current.isEmpty(); } private void resetPosition() { - positionMs = 0; - positionUpdatedAtUptimeMs = SystemClock.uptimeMillis(); + stopFrameCallback(); lastVideoTimeSample = -1; - lastPlaybackSampleUptimeMs = 0; + lastSamplePosition = -1; + lastSampleTimeMs = 0; + lastCorrectedSampleTimeMs = 0; + lastFrameTimeUptimeMs = -1; + estimate = 0; + smoothPosition = 0; lastHighlightedIndex = -1; } + private void updateSample(long newPosition, long now) { + final long prevSample = lastSamplePosition; + lastSamplePosition = newPosition; + lastSampleTimeMs = now; + lastVideoTimeSample = newPosition; + if (prevSample < 0 || Math.abs(newPosition - prevSample) > SEEK_THRESHOLD_MS) { + estimate = newPosition; + smoothPosition = newPosition; + } + } + /** * Current playback position on the released-audio timeline: while the segment switch * is on, time the sponsor block segments have already passed is removed first, then @@ -354,33 +456,31 @@ public long getVideoPositionMs() { private long computeExpectedPositionMs() { final long now = SystemClock.uptimeMillis(); - long expected = positionMs; - if (playing && positionUpdatedAtUptimeMs != 0) { - expected += (long) ((now - positionUpdatedAtUptimeMs) * playbackSpeed); + final long videoTime = VideoInformation.getVideoTime(); + + if (videoTime >= 0 && videoTime != lastVideoTimeSample) { + updateSample(videoTime, now); } - final long videoTime = VideoInformation.getVideoTime(); - if (videoTime > 0 && videoTime != lastVideoTimeSample) { - lastVideoTimeSample = videoTime; - if (videoTime > expected) { - positionMs = videoTime; - positionUpdatedAtUptimeMs = now; - } else if (expected - videoTime > VIDEO_REANCHOR_BEHIND_MS - && now - lastPlaybackSampleUptimeMs > PLAYBACK_STALE_MS) { - positionMs = videoTime; - positionUpdatedAtUptimeMs = now; - } - expected = positionMs; - if (playing && positionUpdatedAtUptimeMs != 0) { - expected += (long) ((now - positionUpdatedAtUptimeMs) * playbackSpeed); + if (!playing || lastSamplePosition < 0) { + if (lastSamplePosition >= 0) { + smoothPosition = lastSamplePosition; + estimate = lastSamplePosition; } + return Math.max(0, lastSamplePosition); } - return expected; + + positionReadSinceFrame = true; + ensureFrameCallback(); + return Math.max(0, smoothPosition); } public int getTemporaryOffsetMs() { return temporaryOffsetMs; } - public void setTemporaryOffsetMs(int ms) { temporaryOffsetMs = ms; } + public void setTemporaryOffsetMs(int ms) { + temporaryOffsetMs = ms; + lastFrameTimeUptimeMs = -1; + } /** Called from the panel when the track is about to change or to go away, not per drag frame. */ public void persistTemporaryOffsetMs() { @@ -642,9 +742,11 @@ public void onSetPlaybackState(@Nullable PlaybackState playbackState) { } playing = playbackState.getState() == PlaybackState.STATE_PLAYING; - positionMs = playbackState.getPosition(); - positionUpdatedAtUptimeMs = SystemClock.uptimeMillis(); - lastPlaybackSampleUptimeMs = positionUpdatedAtUptimeMs; + + final long newPosition = playbackState.getPosition(); + if (newPosition >= 0) { + updateSample(newPosition, SystemClock.uptimeMillis()); + } final float speed = playbackState.getPlaybackSpeed(); // A paused state reports speed zero, which would freeze the extrapolation even @@ -652,6 +754,16 @@ public void onSetPlaybackState(@Nullable PlaybackState playbackState) { if (speed > 0) { playbackSpeed = speed; } + + if (playing) { + lastFrameTimeUptimeMs = -1; + } else { + stopFrameCallback(); + if (lastSamplePosition >= 0) { + smoothPosition = lastSamplePosition; + estimate = lastSamplePosition; + } + } ensureLoadedForCurrentVideo(); } @@ -2206,4 +2318,6 @@ public boolean areLyricsSynced() { Lyrics lyrics = currentLyrics; return lyrics != null && lyrics.synced() && !lyrics.isEmpty(); } + + } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/LyricsProviders.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/LyricsProviders.java index dd327c1c04..30a58f8ddd 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/LyricsProviders.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/LyricsProviders.java @@ -22,6 +22,7 @@ import app.morphe.extension.music.patches.lyrics.requests.GeniusProvider; import app.morphe.extension.music.patches.lyrics.requests.KuGouProvider; import app.morphe.extension.music.patches.lyrics.requests.LRCLIBProvider; +import app.morphe.extension.music.patches.lyrics.requests.LrcRedProvider; import app.morphe.extension.music.patches.lyrics.requests.LunaBeatProvider; import app.morphe.extension.music.patches.lyrics.requests.LunaProvider; import app.morphe.extension.music.patches.lyrics.requests.LyricifyProvider; @@ -40,9 +41,10 @@ public final class LyricsProviders { static final List PROVIDER_ORDER = Arrays.asList( - "YTMusic", "Captions", "LRCLIB", "QQ", "NetEase", "KuGou", - "Luna", "RMM", "PetitLyrics", "bLyrics", "BiniLyrics", - "Unison", "SimpMusic", "AMLL", "LunaBeat", "Lyricify", "Apple", "Musixmatch", "Spotify", "Deezer", "Genius"); + "Apple", "BiniLyrics", "LrcRed", "bLyrics", "Lyricify", "Unison", + "PetitLyrics", "AMLL", "QQ", "SimpMusic", "KuGou", "Musixmatch", + "YTMusic", "LRCLIB", "NetEase", "Luna", "LunaBeat", + "Captions", "Spotify", "Deezer", "RMM", "Genius"); private LyricsProviders() { } @@ -136,6 +138,7 @@ private static LyricsProvider providerFor(String id) { case "Musixmatch" -> new MusixmatchProvider(); case "Deezer" -> new DeezerProvider(); case "Genius" -> new GeniusProvider(); + case "LrcRed" -> new LrcRedProvider(); default -> null; }; } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/derive/AiProviders.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/derive/AiProviders.java new file mode 100644 index 0000000000..152553d47c --- /dev/null +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/derive/AiProviders.java @@ -0,0 +1,65 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/3877 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.extension.music.patches.lyrics.derive; + +import androidx.annotation.Nullable; + +import java.util.List; + +import app.morphe.extension.music.settings.Settings; + +/** + * The translation and romanization providers a user can pick, and the one place a request is + * sent to the AI model the picked provider stands for. + */ +public final class AiProviders { + + public static final String GOOGLE = "google"; + public static final String LOCAL = "local"; + public static final String GEMINI = "gemini"; + public static final String OPENAI_COMPATIBLE = "openai_compatible"; + public static final String DEEPL = "deepl"; + + /** What a model answered, and the model that answered it. */ + record Result(List lines, String model) { + } + + private AiProviders() { + } + + public static boolean isAi(@Nullable String provider) { + return GEMINI.equals(provider) || OPENAI_COMPATIBLE.equals(provider); + } + + /** + * Sends the prompt to the model of an AI provider. Null when the provider is not an AI one, + * has no key yet, or gave no usable answer, so the caller falls back to Google. + */ + @Nullable + static Result mapLines(String provider, String prompt, List lines) { + final String model; + final List mapped; + switch (provider) { + case GEMINI -> { + String apiKey = Settings.LYRICS_GEMINI_API_KEY.get(); + if (apiKey.isEmpty()) return null; + model = Settings.LYRICS_GEMINI_MODEL.get(); + mapped = GeminiClient.mapLines(apiKey, model, prompt, lines); + } + case OPENAI_COMPATIBLE -> { + model = Settings.LYRICS_AI_MODEL.get(); + mapped = OpenAIClient.mapLines(Settings.LYRICS_AI_BASE_URL.get(), + Settings.LYRICS_AI_API_TOKEN.get(), model, prompt, lines); + } + default -> { + return null; + } + } + return mapped == null ? null : new Result(mapped, model); + } +} diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/derive/DeepLClient.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/derive/DeepLClient.java new file mode 100644 index 0000000000..1563f15bc2 --- /dev/null +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/derive/DeepLClient.java @@ -0,0 +1,123 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/3877 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.extension.music.patches.lyrics.derive; + +import androidx.annotation.Nullable; + +import org.json.JSONArray; +import org.json.JSONObject; + +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +import app.morphe.extension.shared.Logger; + +public final class DeepLClient { + + private static final int CONNECT_TIMEOUT_MS = 10_000; + private static final int READ_TIMEOUT_MS = 30_000; + + private static final String API_FREE_URL = "https://api-free.deepl.com/v2/translate"; + private static final String API_PRO_URL = "https://api.deepl.com/v2/translate"; + + private DeepLClient() { + } + + @Nullable + public static List translate(List lines, + String targetLang, + String apiKey) { + if (apiKey.isEmpty() || lines.isEmpty()) { + return null; + } + + try { + String endpoint = apiKey.endsWith(":fx") ? API_FREE_URL : API_PRO_URL; + String deeplLang = normalizeDeepLLanguage(targetLang); + + JSONObject body = new JSONObject(); + JSONArray textArray = new JSONArray(); + for (String line : lines) { + textArray.put(line); + } + body.put("text", textArray); + body.put("target_lang", deeplLang); + + byte[] payload = body.toString().getBytes(StandardCharsets.UTF_8); + + URL url = new URL(endpoint); + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + try { + conn.setRequestMethod("POST"); + conn.setConnectTimeout(CONNECT_TIMEOUT_MS); + conn.setReadTimeout(READ_TIMEOUT_MS); + conn.setRequestProperty("Authorization", "DeepL-Auth-Key " + apiKey); + conn.setRequestProperty("Content-Type", "application/json; charset=utf-8"); + conn.setDoOutput(true); + + try (OutputStream os = conn.getOutputStream()) { + os.write(payload); + } + + int code = conn.getResponseCode(); + if (code != 200) { + Logger.printDebug(() -> "DeepL API error code: " + code); + return null; + } + + JSONObject resp = new JSONObject(OpenAIClient.readResponse(conn)); + JSONArray translations = resp.optJSONArray("translations"); + if (translations == null) { + return null; + } + + List result = new ArrayList<>(lines.size()); + for (int i = 0; i < translations.length(); i++) { + JSONObject item = translations.getJSONObject(i); + result.add(item.optString("text", "")); + } + return result; + } finally { + conn.disconnect(); + } + } catch (Exception ex) { + Logger.printDebug(() -> "DeepL request failed", ex); + return null; + } + } + + private static String normalizeDeepLLanguage(String lang) { + String clean = lang.trim().toUpperCase(Locale.ROOT); + if (clean.equals("EN") || clean.startsWith("EN-")) { + return "EN-US"; + } + if (clean.equals("PT") || clean.equals("PT-BR")) { + return "PT-BR"; + } + if (clean.equals("PT-PT")) { + return "PT-PT"; + } + if (clean.equals("ZH") || clean.equals("ZH-CN") || clean.equals("ZH-HANS") || clean.equals("ZH-SG")) { + return "ZH-HANS"; + } + if (clean.equals("ZH-TW") || clean.equals("ZH-HK") || clean.equals("ZH-HANT")) { + return "ZH-HANT"; + } + if (clean.equals("IN") || clean.equals("ID")) { + return "ID"; + } + // Other languages use the plain two letter code, such as JA, KO or DE. + int hyphen = clean.indexOf('-'); + return hyphen > 0 ? clean.substring(0, hyphen) : clean; + } +} diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/derive/GeminiClient.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/derive/GeminiClient.java new file mode 100644 index 0000000000..ccee474c68 --- /dev/null +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/derive/GeminiClient.java @@ -0,0 +1,156 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/3877 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.extension.music.patches.lyrics.derive; + +import androidx.annotation.Nullable; + +import org.json.JSONArray; +import org.json.JSONObject; + +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +import app.morphe.extension.shared.Logger; + +/** + * The Gemini API speaks its own protocol, not the OpenAI one, so it has a client of its own. + * The prompt and the parsing of the answer are shared with {@link OpenAIClient}. + */ +public final class GeminiClient { + + private static final int CONNECT_TIMEOUT_MS = 10_000; + private static final int READ_TIMEOUT_MS = 45_000; + + private static final String BASE_URL = "https://generativelanguage.googleapis.com/v1beta"; + private static final String MODEL_PREFIX = "models/"; + /** Sent as a header rather than in the query, where it would end up in request logs. */ + private static final String API_KEY_HEADER = "x-goog-api-key"; + + private GeminiClient() { + } + + @Nullable + public static List mapLines(String apiKey, String model, String prompt, List sourceLines) { + if (!OpenAIClient.fitsOneRequest(sourceLines)) { + return null; + } + return OpenAIClient.mapResponse(request(apiKey, model, prompt), sourceLines); + } + + @Nullable + private static String request(String apiKey, String model, String prompt) { + try { + String cleanModel = model.startsWith(MODEL_PREFIX) ? model.substring(MODEL_PREFIX.length()) : model; + + JSONObject root = new JSONObject(); + JSONObject userContent = new JSONObject() + .put("role", "user") + .put("parts", new JSONArray().put(new JSONObject().put("text", prompt))); + root.put("contents", new JSONArray().put(userContent)); + root.put("generationConfig", new JSONObject().put("temperature", 0.2)); + + byte[] payload = root.toString().getBytes(StandardCharsets.UTF_8); + + HttpURLConnection conn = (HttpURLConnection) new URL( + BASE_URL + "/" + MODEL_PREFIX + cleanModel + ":generateContent").openConnection(); + try { + conn.setRequestMethod("POST"); + conn.setConnectTimeout(CONNECT_TIMEOUT_MS); + conn.setReadTimeout(READ_TIMEOUT_MS); + conn.setRequestProperty(API_KEY_HEADER, apiKey); + conn.setRequestProperty("Content-Type", "application/json; charset=utf-8"); + conn.setDoOutput(true); + + try (OutputStream os = conn.getOutputStream()) { + os.write(payload); + } + + int code = conn.getResponseCode(); + if (code != 200) { + Logger.printDebug(() -> "Gemini API error code: " + code); + return null; + } + + JSONObject resp = new JSONObject(OpenAIClient.readResponse(conn)); + JSONArray candidates = resp.optJSONArray("candidates"); + if (candidates == null || candidates.length() == 0) { + return null; + } + JSONObject content = candidates.getJSONObject(0).optJSONObject("content"); + if (content == null) { + return null; + } + JSONArray parts = content.optJSONArray("parts"); + if (parts == null || parts.length() == 0) { + return null; + } + return parts.getJSONObject(0).optString("text", null); + } finally { + conn.disconnect(); + } + } catch (Exception ex) { + Logger.printDebug(() -> "Gemini request failed", ex); + return null; + } + } + + /** The Gemini models that can generate text, empty when none could be listed. */ + public static List fetchModels(String apiKey) { + List models = new ArrayList<>(); + if (apiKey.isEmpty()) { + return models; + } + try { + HttpURLConnection conn = (HttpURLConnection) new URL(BASE_URL + "/models").openConnection(); + try { + conn.setRequestMethod("GET"); + conn.setConnectTimeout(CONNECT_TIMEOUT_MS); + conn.setReadTimeout(READ_TIMEOUT_MS); + conn.setRequestProperty(API_KEY_HEADER, apiKey); + + if (conn.getResponseCode() != 200) { + return models; + } + + JSONArray modelArr = new JSONObject(OpenAIClient.readResponse(conn)).optJSONArray("models"); + if (modelArr == null) { + return models; + } + for (int i = 0; i < modelArr.length(); i++) { + JSONObject m = modelArr.getJSONObject(i); + String name = m.optString("name", ""); + if (name.startsWith(MODEL_PREFIX) && name.contains("gemini") + && supportsGenerateContent(m.optJSONArray("supportedGenerationMethods"))) { + models.add(name.substring(MODEL_PREFIX.length())); + } + } + } finally { + conn.disconnect(); + } + } catch (Exception ex) { + Logger.printDebug(() -> "Gemini fetchModels failed", ex); + } + return models; + } + + private static boolean supportsGenerateContent(@Nullable JSONArray methods) { + if (methods == null) { + return false; + } + for (int i = 0; i < methods.length(); i++) { + if ("generateContent".equals(methods.optString(i))) { + return true; + } + } + return false; + } +} diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/derive/LyricsRomanizer.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/derive/LyricsRomanizer.java index 9aaec37108..ef74254183 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/derive/LyricsRomanizer.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/derive/LyricsRomanizer.java @@ -10,6 +10,8 @@ import androidx.annotation.Nullable; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.Map; @@ -20,8 +22,14 @@ import app.morphe.extension.music.patches.lyrics.model.LyricsLine; import app.morphe.extension.music.patches.lyrics.model.LyricsMerge; import app.morphe.extension.music.patches.lyrics.model.TrackInfo; +import app.morphe.extension.music.patches.lyrics.model.Word; import app.morphe.extension.music.patches.lyrics.requests.LyricsRequests; +import app.morphe.extension.music.patches.lyrics.romanizers.HangulRomanizer; +import app.morphe.extension.music.patches.lyrics.romanizers.JapaneseDictStore; +import app.morphe.extension.music.patches.lyrics.romanizers.KanaRomanizer; import app.morphe.extension.music.patches.lyrics.romanizers.LocalRomanizer; +import app.morphe.extension.music.patches.lyrics.romanizers.PinyinDictStore; +import app.morphe.extension.music.patches.lyrics.romanizers.ScriptDetector; import app.morphe.extension.music.patches.lyrics.storage.LyricsCache; import app.morphe.extension.music.settings.Settings; import app.morphe.extension.shared.Logger; @@ -34,92 +42,184 @@ public interface Callback { void onRomanized(@Nullable List romanizedLines, boolean fromGoogle, boolean fromAI, @Nullable String aiModel, boolean perWord, @Nullable String localDictionary); + } + private static final String RULE_BASED_DICTIONARY = "Local (Rule-based)"; + private static final ExecutorService executor = Executors.newSingleThreadExecutor(); private LyricsRomanizer() { } + @Nullable + public static List getEmbeddedRomanization(Lyrics lyrics) { + if (lyrics == null) return null; + final int lineCount = lyrics.lines() != null ? lyrics.lines().size() : 0; + List embedded = lyrics.romanization(); + if (embedded != null && embedded.size() != lineCount) { + embedded = null; + } + if (embedded == null || embedded.isEmpty()) { + Map> romanizations = lyrics.romanizations(); + if (romanizations != null && !romanizations.isEmpty()) { + embedded = collectMatchingRomanizations(romanizations, lyrics.lines(), + LyricsMerge.hasBackgroundTrack(lyrics, romanizations)); + } + } + if (LyricsMerge.hasText(embedded)) { + return embedded; + } + // Otherwise the line reading is built from the readings of its words. + if (lyrics.lines() != null && LyricsMerge.anyWordHasRomaji(lyrics.lines())) { + List synthesized = new ArrayList<>(lyrics.lines().size()); + for (LyricsLine l : lyrics.lines()) { + if (l.hasWords()) { + StringBuilder sb = new StringBuilder(); + for (Word w : l.words()) { + String r = w.romaji(); + String part = (r != null && !r.trim().isEmpty() && !ScriptDetector.containsCjk(r) && !isDegradedRomajiToken(r)) + ? r.trim() + : (ScriptDetector.containsKana(w.text()) ? KanaRomanizer.romanize(w.text()) : ""); + part = stripParentheses(part).trim(); + if (!part.isEmpty() && !ScriptDetector.containsCjk(part)) { + sb.append(part); + if (w.endsWithSpace() && !part.endsWith(" ")) { + sb.append(' '); + } + } + } + synthesized.add(new LyricsLine(l.startTimeMs(), sb.toString().trim())); + } else { + synthesized.add(new LyricsLine(l.startTimeMs(), "")); + } + } + if (LyricsMerge.hasText(synthesized)) { + return synthesized; + } + } + return null; + } + public static void romanize(TrackInfo track, Lyrics lyrics, String source, Callback callback) { Utils.verifyOnMainThread(); + boolean anyLineHasWords = false; + for (LyricsLine l : lyrics.lines()) { + if (l.hasWords()) { + anyLineHasWords = true; + break; + } + } + final boolean isWordSynced = anyLineHasWords; + + // A reading the provider ships with wins, unless it is broken into fragments. + List embedded = getEmbeddedRomanization(lyrics); + final boolean perWord = LyricsMerge.anyWordHasRomaji(lyrics.lines()); + if ((LyricsMerge.hasText(embedded) || perWord) + && !isProviderRomajiDegraded(embedded, lyrics.lines(), perWord)) { + List result = alignIfWordSynced(lyrics, embedded, isWordSynced); + boolean hasWordRomaji = perWord || (isWordSynced && result != null && LyricsMerge.anyWordHasRomaji(result)); + Utils.runOnMainThread(() -> callback.onRomanized(result, false, false, null, hasWordRomaji, null)); + return; + } + List lines = new ArrayList<>(lyrics.lines().size()); for (LyricsLine line : lyrics.lines()) { - String text = line.text(); - lines.add(text != null ? text : ""); + lines.add(stripParentheses(line.text())); } executor.execute(() -> { try { - // A romanization is read by line index, so one without an entry for every - // line describes a different lyric and would put a reading under the wrong - // line. It is dropped so the line is romanized again. - final int lineCount = lyrics.lines().size(); - List embedded = lyrics.romanization(); - if (embedded != null && embedded.size() != lineCount) { - embedded = null; - } - if (embedded == null || embedded.isEmpty()) { - Map> romanizations = lyrics.romanizations(); - if (romanizations != null && !romanizations.isEmpty()) { - embedded = collectMatchingRomanizations(romanizations, lyrics.lines(), - LyricsMerge.hasBackgroundTrack(lyrics, romanizations)); - } - } + final String provider = Settings.LYRICS_ROMANIZATION_PROVIDER.get(); - final boolean perWord = LyricsMerge.anyWordHasRomaji(lyrics.lines()); - if (LyricsMerge.hasText(embedded) || perWord) { - List result = embedded; - Utils.runOnMainThread( - () -> callback.onRomanized(result, false, false, null, perWord, null)); - return; - } + if (AiProviders.LOCAL.equals(provider)) { + if (!JapaneseDictStore.isReady()) JapaneseDictStore.ensure(null); + if (!PinyinDictStore.isReady()) PinyinDictStore.ensure(null); - if (Settings.LYRICS_USE_AI_ROMANIZATION.get()) { - String baseUrl = Settings.LYRICS_AI_BASE_URL.get(); - String apiToken = Settings.LYRICS_AI_API_TOKEN.get(); - String model = Settings.LYRICS_AI_MODEL.get(); + LocalRomanizer.Result local = LocalRomanizer.romanize(lines); + List result = local != null ? toLines(local.lines) : null; + String dictionary = local != null ? local.dictionary : null; + if (!LyricsMerge.hasText(result)) { + result = ruleBased(lines); + dictionary = result != null ? RULE_BASED_DICTIONARY : null; + } + if (LyricsMerge.hasText(result)) { + final List finalResult = alignIfWordSynced(lyrics, result, isWordSynced); + final String finalDictionary = dictionary; + final boolean hasWordRomaji = isWordSynced && finalResult != null && LyricsMerge.anyWordHasRomaji(finalResult); + Utils.runOnMainThread(() -> callback.onRomanized( + finalResult, false, false, null, hasWordRomaji, finalDictionary)); + return; + } + // Local romanization yielded no text; fall through to Google/online fallback. + } - List aiCached = LyricsCache.getRomanizationAI( - track, source, lines); + if (AiProviders.isAi(provider)) { + List aiCached = LyricsCache.getRomanizationAI(track, source, lines); if (aiCached != null) { - publish(callback, lyrics, aiCached, false, true, model, null); + final List aligned = alignIfWordSynced(lyrics, aiCached, isWordSynced); + final boolean hasWordRomaji = isWordSynced && aligned != null && LyricsMerge.anyWordHasRomaji(aligned); + Utils.runOnMainThread(() -> callback.onRomanized(aligned, false, true, provider, hasWordRomaji, null)); return; } - List aiResult = aiRomanize(lines, LyricsRequests.romanizationLanguage(), track.title(), - track.artist(), baseUrl, apiToken, model); - if (aiResult != null) { - List aiLines = toLines(aiResult); - LyricsCache.putRomanizationAI(track, source, lines, aiLines); - publish(callback, lyrics, aiLines, false, true, model, null); - return; + String prompt = OpenAIClient.renderPrompt(Settings.LYRICS_AI_ROMANIZATION_PROMPT.get(), + "romanization", LyricsRequests.romanizationLanguage(), + track.title(), track.artist(), lines); + AiProviders.Result ai = AiProviders.mapLines(provider, prompt, lines); + if (ai != null && ai.lines() != null && !ai.lines().isEmpty()) { + List aiLines = alignIfWordSynced(lyrics, toLines(ai.lines()), isWordSynced); + if (LyricsMerge.hasText(aiLines)) { + LyricsCache.putRomanizationAI(track, source, lines, aiLines); + final boolean hasWordRomaji = isWordSynced && LyricsMerge.anyWordHasRomaji(aiLines); + Utils.runOnMainThread(() -> callback.onRomanized(aiLines, false, true, ai.model(), hasWordRomaji, null)); + return; + } } } - List romanized = LyricsCache.getRomanization(track, source, lines); - boolean online = romanized != null; - if (romanized == null) { - List romanizedText = romanizeOnline(lines); - if (romanizedText != null) { - romanized = toLines(romanizedText); - LyricsCache.putRomanization(track, source, lines, romanized); - online = true; + List romanized; + boolean online; + if (isWordSynced) { + romanized = romanizeWordSynced(lyrics); + online = LyricsMerge.hasText(romanized); + } else { + romanized = LyricsCache.getRomanization(track, source, lines); + online = romanized != null; + if (romanized == null) { + romanized = romanizeLineSynced(lyrics); + if (LyricsMerge.hasText(romanized)) { + LyricsCache.putRomanization(track, source, lines, romanized); + online = true; + } } } // The last resort. Its reading is not stored, so an online one is used again // as soon as there is one. - LocalRomanizer.Result local = romanized == null - && Settings.LYRICS_LOCAL_ROMANIZATION.get() + LocalRomanizer.Result local = !LyricsMerge.hasText(romanized) + && (Settings.LYRICS_LOCAL_ROMANIZATION.get() || AiProviders.LOCAL.equals(provider) || AiProviders.isAi(provider)) ? LocalRomanizer.romanize(lines) : null; - final List wholeLine = local != null ? toLines(local.lines) : romanized; - final boolean fromGoogle = online && local == null && wholeLine != null; - publish(callback, lyrics, wholeLine, fromGoogle, false, null, - local != null ? local.dictionary : null); - } catch (Throwable ex) { + List result = romanized; + if (local != null) { + result = alignIfWordSynced(lyrics, toLines(local.lines), isWordSynced); + } + if (!LyricsMerge.hasText(result)) { + List ruleBased = ruleBased(lines); + if (ruleBased != null) { + result = alignIfWordSynced(lyrics, ruleBased, isWordSynced); + } + } + + final List finalResult = result; + final boolean fromGoogle = LyricsMerge.hasText(result) && online && local == null; + final boolean hasWordRomaji = isWordSynced && finalResult != null && LyricsMerge.anyWordHasRomaji(finalResult); + Utils.runOnMainThread(() -> callback.onRomanized( + finalResult, fromGoogle, false, null, hasWordRomaji, + local != null ? local.dictionary : null)); + } catch (Exception ex) { Logger.printException(() -> "romanize failure", ex); Utils.runOnMainThread( () -> callback.onRomanized(null, false, false, null, false, null)); @@ -128,26 +228,167 @@ public static void romanize(TrackInfo track, Lyrics lyrics, String source, Callb } /** - * The one place a reading leaves, so every source is offered word alignment alike: the word - * timings are the source's either way, leaving the reading as the only part to be matched. + * Romanizes kana and hangul by rule, with no dictionary, for when nothing better is + * available. Null if no line has either. */ - private static void publish(Callback callback, Lyrics lyrics, - @Nullable List readings, boolean fromGoogle, boolean fromAI, - @Nullable String aiModel, @Nullable String localDictionary) { - final List aligned = readings != null - && Settings.LYRICS_LOCAL_ROMANIZATION.get() && Settings.LYRICS_WORD_SYNC.get() - ? LocalRomanizer.alignWords(lyrics.lines(), readings) : null; - final List result = aligned != null ? aligned : readings; - Utils.runOnMainThread(() -> callback.onRomanized(result, fromGoogle, fromAI, aiModel, - aligned != null, localDictionary)); + @Nullable + private static List ruleBased(List lines) { + List result = new ArrayList<>(lines.size()); + boolean applied = false; + for (String line : lines) { + String rom = line; + if (ScriptDetector.containsKana(line)) { + rom = KanaRomanizer.romanize(line); + } else if (ScriptDetector.contains(line, Character.UnicodeScript.HANGUL)) { + rom = HangulRomanizer.romanize(line); + } + applied |= !rom.equals(line); + result.add(rom); + } + return applied ? toLines(result) : null; } @Nullable - private static List aiRomanize(List lines, String targetLanguage, - String title, String artist, String baseUrl, String apiToken, String model) { - String prompt = OpenAIClient.renderPrompt(Settings.LYRICS_AI_ROMANIZATION_PROMPT.get(), - "romanization", targetLanguage, title, artist, lines); - return OpenAIClient.mapLines(baseUrl, apiToken, model, prompt, lines); + private static List alignIfWordSynced(Lyrics lyrics, @Nullable List lines, + boolean isWordSynced) { + return isWordSynced && lines != null ? alignLinesToWordSync(lyrics, lines) : lines; + } + + private static List romanizeWordSynced(Lyrics lyrics) { + if (lyrics.lines() == null) return Collections.emptyList(); + + List origLines = lyrics.lines(); + List allLines = new ArrayList<>(origLines.size()); + List allSyllables = new ArrayList<>(); + int[] lineStructure = new int[origLines.size()]; + + for (int i = 0; i < origLines.size(); i++) { + LyricsLine line = origLines.get(i); + String text = line.text() != null ? stripParentheses(line.text()).trim() : ""; + allLines.add(text); + List words = line.words(); + lineStructure[i] = words.size(); + for (Word w : words) { + allSyllables.add(w.text() != null ? w.text() : ""); + } + } + + List fullLineResults = romanizeTexts(allLines); + List isolatedResults = !allSyllables.isEmpty() ? romanizeTexts(allSyllables) : Collections.emptyList(); + + List resultLines = new ArrayList<>(origLines.size()); + int globalSyllableIndex = 0; + + for (int i = 0; i < origLines.size(); i++) { + LyricsLine line = origLines.get(i); + String fullLineRom = (i < fullLineResults.size() && fullLineResults.get(i) != null) ? fullLineResults.get(i) : ""; + int count = lineStructure[i]; + List origWords = line.words(); + + if (count == 0 || !line.hasWords()) { + resultLines.add(new LyricsLine(line.startTimeMs(), line.endTimeMs(), + fullLineRom, Collections.emptyList(), line.agentId(), line.isDuet(), line.isBG(), line.songPart())); + continue; + } + + int endSyllableIndex = Math.min(globalSyllableIndex + count, isolatedResults.size()); + List romanizedGuides = (globalSyllableIndex < isolatedResults.size()) + ? new ArrayList<>(isolatedResults.subList(globalSyllableIndex, endSyllableIndex)) + : new ArrayList<>(); + while (romanizedGuides.size() < count) { + romanizedGuides.add(""); + } + + List originalSyllables = new ArrayList<>(count); + for (Word w : origWords) { + originalSyllables.add(w.text() != null ? w.text() : ""); + } + + globalSyllableIndex += count; + + List alignedChunks = alignRomanizationAnchors(fullLineRom, romanizedGuides, originalSyllables); + + List romanizedWords = new ArrayList<>(count); + StringBuilder lineRomBuilder = new StringBuilder(); + + for (int wIdx = 0; wIdx < count; wIdx++) { + Word w = origWords.get(wIdx); + String rom = (wIdx < alignedChunks.size() && alignedChunks.get(wIdx) != null) ? alignedChunks.get(wIdx) : ""; + romanizedWords.add(new Word(w.startMs(), w.endMs(), w.text(), rom, w.endsWithSpace())); + lineRomBuilder.append(rom); + } + + String finalLineRom = !fullLineRom.isEmpty() ? fullLineRom : lineRomBuilder.toString().trim(); + + resultLines.add(new LyricsLine(line.startTimeMs(), line.endTimeMs(), + finalLineRom, romanizedWords, line.agentId(), line.isDuet(), line.isBG(), line.songPart())); + } + + return resultLines; + } + + private static List romanizeLineSynced(Lyrics lyrics) { + if (lyrics.lines() == null) return Collections.emptyList(); + + List origLines = lyrics.lines(); + List texts = new ArrayList<>(origLines.size()); + for (LyricsLine l : origLines) { + String text = l.text(); + texts.add(text != null ? stripParentheses(text).trim() : ""); + } + + List romanizedResults = romanizeTexts(texts); + + List result = new ArrayList<>(origLines.size()); + for (int i = 0; i < origLines.size(); i++) { + LyricsLine origLine = origLines.get(i); + String origText = texts.get(i); + String roma = (i < romanizedResults.size() && romanizedResults.get(i) != null) + ? romanizedResults.get(i) : origText; + if (roma.isEmpty()) { + roma = origText; + } + + result.add(new LyricsLine(origLine.startTimeMs(), origLine.endTimeMs(), + roma, Collections.emptyList(), origLine.agentId(), origLine.isDuet(), origLine.isBG(), origLine.songPart())); + } + return result; + } + + /** + * Romanizes the texts online. A text already in Latin script, or one that could not be + * read, is returned as it is. + */ + private static List romanizeTexts(List texts) { + List indices = new ArrayList<>(); + List toFetch = new ArrayList<>(); + List result = new ArrayList<>(texts.size()); + for (int i = 0; i < texts.size(); i++) { + String text = texts.get(i) != null ? texts.get(i) : ""; + result.add(text); + if (!isPurelyLatinScript(text)) { + indices.add(i); + toFetch.add(text); + } + } + if (toFetch.isEmpty()) { + return result; + } + + final String language = Settings.LYRICS_ROMANIZE_LANGUAGE.get(); + List fetched = LyricsMerge.mapLinesOnline(toFetch, batch -> { + try { + return TextTranslator.romanize(batch, language); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + }); + if (fetched != null) { + for (int k = 0; k < indices.size(); k++) { + result.set(indices.get(k), fetched.get(k)); + } + } + return result; } private static List collectMatchingRomanizations( @@ -197,7 +438,7 @@ private static List collectMatchingRomanizations( if (langLines == null || i >= langLines.size()) continue; String text = langLines.get(i).text(); if (text == null) continue; - text = text.trim(); + text = stripParentheses(text).trim(); if (!text.isEmpty()) { //noinspection SizeReplaceableByIsEmpty if (merged.length() > 0) merged.append('\n'); @@ -248,28 +489,520 @@ private static List collectMatchingRomanizations( return result; } + private static String stripParentheses(@Nullable String text) { + if (text == null) { + return ""; + } + String trimmed = text.trim(); + boolean stripped = true; + while (stripped && trimmed.length() >= 2) { + stripped = false; + char first = trimmed.charAt(0); + char last = trimmed.charAt(trimmed.length() - 1); + if ((first == '(' && last == ')') + || (first == '(' && last == ')') + || (first == '[' && last == ']') + || (first == '【' && last == '】')) { + trimmed = trimmed.substring(1, trimmed.length() - 1).trim(); + stripped = true; + } + } + return trimmed; + } + + private static boolean isMandarinPinyinToken(@Nullable String t) { + if (t == null || t.isEmpty()) return false; + String s = t.trim().toLowerCase(Locale.ROOT); + if (s.contains(" ")) { + for (String part : s.split("\\s+")) { + if (isMandarinPinyinToken(part)) return true; + } + return false; + } + if (s.startsWith("zh") || s.startsWith("x") || s.startsWith("q")) { + return true; + } + if (s.endsWith("ng") && !s.equals("ng")) { + return true; // e.g. chang, zeng, zhang, zhong, etc. + } + if (s.endsWith("ian") || s.endsWith("iao") || s.endsWith("uang") || s.endsWith("iang") + || s.endsWith("ong") || s.endsWith("eng") || s.endsWith("ang") || s.endsWith("uan") + || s.endsWith("iong")) { + return true; + } + if (s.equals("ceng") || s.equals("cuan") || s.equals("zuan") || s.equals("zhe") + || s.equals("jin") || s.equals("dong") || s.equals("sheng") || s.equals("chuang") + || s.equals("xian") || s.equals("qian") || s.equals("tian") || s.equals("lian") + || s.equals("jian") || s.equals("bian") || s.equals("pian") || s.equals("mian") + || s.equals("dian") || s.equals("nian") || s.equals("huan") || s.equals("guan") + || s.equals("kuan") || s.equals("duan") || s.equals("tuan") || s.equals("luan") + || s.equals("ruan") || s.equals("xuan") || s.equals("quan") || s.equals("juan") + || s.equals("yang") || s.equals("ying") || s.equals("yong") || s.equals("wang") + || s.equals("weng") || s.equals("gu") || s.equals("xi")) { + return true; + } + return false; + } + + private static boolean isDegradedRomajiToken(@Nullable String t) { + if (t == null || t.isEmpty()) return false; + String s = t.trim().toLowerCase(Locale.ROOT); + if (s.contains(" ")) { + for (String part : s.split("\\s+")) { + if (isDegradedRomajiToken(part)) return true; + } + return false; + } + // A counter reading left with its digit, such as 1-ri, 1-tsu or 2nin. + if (s.matches("^\\d+-[a-z]+$") || s.matches("^\\d+[a-z]+$")) { + return true; + } + // A sokuon written as a symbol, such as ~tsu or -tsu. + if (s.contains("~tsu") || s.contains("-tsu") || s.contains("~tsu") + || s.endsWith("~") || s.startsWith("~") || s.contains("~") || s.contains("~")) { + return true; + } + // A symbol or punctuation mark on its own. + if (s.equals("-") || s.equals("~") || s.equals("~") || s.equals("^") || s.equals("_")) { + return true; + } + // A pinyin syllable, which has no place in a Japanese reading. + if (isMandarinPinyinToken(s)) { + return true; + } + // A doubled consonant on its own, which is a sokuon split from its mora. + if (s.equals("pp") || s.equals("tt") || s.equals("kk") || s.equals("ss") + || s.equals("cc") || s.equals("dd") || s.equals("bb") || s.equals("ff") + || s.equals("gg") || s.equals("hh") || s.equals("jj") || s.equals("mm") + || s.equals("rr") || s.equals("ww") || s.equals("yy") || s.equals("zz")) { + return true; + } + return false; + } + + public static boolean isProviderRomajiDegraded(@Nullable List embedded, + @Nullable List originalLines, + boolean perWord) { + if (!perWord && (embedded == null || embedded.isEmpty())) { + return true; + } + + int totalTokens = 0; + int shortTokens = 0; + int totalChars = 0; + int cjkCount = 0; + int totalChecked = 0; + int allSingleLetterTokens = 0; + + if (embedded != null && !embedded.isEmpty()) { + for (int li = 0; li < embedded.size(); li++) { + LyricsLine line = embedded.get(li); + String text = line.text(); + if (text == null || text.trim().isEmpty()) { + continue; + } + totalChecked++; + if (ScriptDetector.containsCjk(text)) { + cjkCount++; + } + + LyricsLine origLine = (originalLines != null && li < originalLines.size()) ? originalLines.get(li) : null; + String origText = origLine != null ? origLine.text() : null; + + // A tilde the lyric does not have is an artifact of the reading. + for (int ci = 0; ci < text.length(); ci++) { + char c = text.charAt(ci); + if (c == '~' || c == '~') { + if (origText == null || (!origText.contains("~") && !origText.contains("~"))) { + return true; // symbol artifact like 3 ~ 6 + } + } + } + + int singleLetterTokens = 0; + String[] tokens = text.trim().split("\\s+"); + for (String t : tokens) { + if (!t.isEmpty()) { + if (isDegradedRomajiToken(t)) { + return true; // trigger for pp 1-ri etc + } + totalTokens++; + totalChars += t.length(); + if (t.length() <= 2) { + shortTokens++; + } + if (t.length() == 1 && Character.isLetter(t.charAt(0))) { + singleLetterTokens++; + allSingleLetterTokens++; + } + } + } + // Several lone letters mean the morae were split apart. + if (singleLetterTokens >= 3 && origText != null && ScriptDetector.containsKana(origText)) { + return true; + } + } + if (totalChecked == 0) return true; + if ((double) cjkCount / totalChecked > 0.20) { + return true; + } + if (originalLines != null) { + int origNonEmpty = 0; + for (LyricsLine l : originalLines) { + if (l.text() != null && !l.text().trim().isEmpty()) { + origNonEmpty++; + } + } + if (origNonEmpty > 0 && (double) totalChecked / origNonEmpty < 0.40) { + return true; + } + } + } + + // The per word readings are checked the same way. + if (perWord && originalLines != null) { + int totalWordTokens = 0; + int shortWordTokens = 0; + int charWordTokens = 0; + int allSingleWordTokens = 0; + for (LyricsLine l : originalLines) { + if (!l.hasWords()) continue; + String origLineText = l.text(); + int singleWordTokens = 0; + for (Word w : l.words()) { + String r = w.romaji(); + if (r != null && !r.trim().isEmpty()) { + String origWord = w.text(); + for (int ci = 0; ci < r.length(); ci++) { + char c = r.charAt(ci); + if (c == '~' || c == '~') { + if (origWord == null || (!origWord.contains("~") && !origWord.contains("~"))) { + return true; + } + } + } + + String[] tokens = r.trim().split("\\s+"); + for (String t : tokens) { + if (!t.isEmpty()) { + if (isDegradedRomajiToken(t)) { + return true; // trigger for 1-ri pp etc + } + totalWordTokens++; + charWordTokens += t.length(); + if (t.length() <= 2) { + shortWordTokens++; + } + if (t.length() == 1 && Character.isLetter(t.charAt(0))) { + singleWordTokens++; + allSingleWordTokens++; + } + } + } + } + } + if (singleWordTokens >= 3 && origLineText != null && ScriptDetector.containsKana(origLineText)) { + return true; + } + } + if (totalWordTokens >= 2) { + double avgWordLen = (double) charWordTokens / totalWordTokens; + double singleLetterRatio = (double) allSingleWordTokens / totalWordTokens; + if (avgWordLen < 1.4 || singleLetterRatio > 0.5) { + return true; + } + } + } + + // Readings made of single letter tokens are mora fragments, not words. + if (totalTokens >= 2) { + double avgLen = (double) totalChars / totalTokens; + double singleLetterRatio = (double) allSingleLetterTokens / totalTokens; + if (avgLen < 1.4 || singleLetterRatio > 0.5) { + return true; + } + } + + return false; + } + + private static List alignLinesToWordSync(@Nullable Lyrics lyrics, @Nullable List rLines) { + if (lyrics == null || lyrics.lines() == null || rLines == null) { + return rLines; + } + List origLines = lyrics.lines(); + List out = new ArrayList<>(origLines.size()); + for (int i = 0; i < origLines.size(); i++) { + LyricsLine orig = origLines.get(i); + LyricsLine rLine = (i < rLines.size()) ? rLines.get(i) : null; + if (rLine == null) { + out.add(new LyricsLine(orig.startTimeMs(), "")); + continue; + } + if (!orig.hasWords() || (rLine.hasWords() && rLine.words().size() == orig.words().size())) { + out.add(rLine); + continue; + } + String fullRoma = rLine.text() != null ? rLine.text().trim() : ""; + if (fullRoma.isEmpty()) { + out.add(rLine); + continue; + } + List origWords = orig.words(); + List alignedChunks = alignRomanizationAnchors(origWords, fullRoma); + List rWords = new ArrayList<>(origWords.size()); + for (int w = 0; w < origWords.size(); w++) { + Word ow = origWords.get(w); + String chunk = (w < alignedChunks.size()) ? alignedChunks.get(w) : ""; + rWords.add(new Word(ow.startMs(), ow.endMs(), ow.text(), chunk, ow.endsWithSpace())); + } + out.add(new LyricsLine(orig.startTimeMs(), orig.endTimeMs(), fullRoma, rWords, + orig.agentId(), orig.isDuet(), orig.isBG(), orig.songPart())); + } + return out; + } + private static List toLines(List texts) { List result = new ArrayList<>(texts.size()); for (String text : texts) { if (text == null || text.equals("null")) { text = ""; } - result.add(new LyricsLine(LyricsLine.NO_TIME, text)); + result.add(new LyricsLine(LyricsLine.NO_TIME, stripParentheses(text).trim())); } return result; } - @Nullable - private static List romanizeOnline(List lines) { - final String language = Settings.LYRICS_ROMANIZE_LANGUAGE.get(); - return LyricsMerge.mapLinesOnline(lines, - l -> { - try { - return TextTranslator.romanize(l, language); - } catch (Exception ex) { - throw new RuntimeException(ex); + public static boolean isPurelyLatinScript(@Nullable CharSequence text) { + if (text == null || text.length() == 0) return true; + for (int i = 0; i < text.length(); ) { + int cp = Character.codePointAt(text, i); + i += Character.charCount(cp); + if (Character.isWhitespace(cp) || Character.isDigit(cp)) continue; + int type = Character.getType(cp); + if (type == Character.DASH_PUNCTUATION || type == Character.START_PUNCTUATION + || type == Character.END_PUNCTUATION || type == Character.CONNECTOR_PUNCTUATION + || type == Character.OTHER_PUNCTUATION || type == Character.MATH_SYMBOL + || type == Character.CURRENCY_SYMBOL || type == Character.MODIFIER_SYMBOL + || type == Character.OTHER_SYMBOL) { + continue; + } + Character.UnicodeScript script = Character.UnicodeScript.of(cp); + if (script != Character.UnicodeScript.LATIN && script != Character.UnicodeScript.COMMON + && script != Character.UnicodeScript.INHERITED) { + return false; + } + } + return true; + } + + private static int levenshteinDistance(String s1, String s2) { + int len1 = s1.length(); + int len2 = s2.length(); + int[] prev = new int[len2 + 1]; + int[] curr = new int[len2 + 1]; + for (int j = 0; j <= len2; j++) prev[j] = j; + for (int i = 1; i <= len1; i++) { + curr[0] = i; + char c1 = s1.charAt(i - 1); + for (int j = 1; j <= len2; j++) { + char c2 = s2.charAt(j - 1); + int cost = (c1 == c2) ? 0 : 1; + curr[j] = Math.min(Math.min(curr[j - 1] + 1, prev[j] + 1), prev[j - 1] + cost); + } + System.arraycopy(curr, 0, prev, 0, len2 + 1); + } + return prev[len2]; + } + + + private static List alignRomanizationAnchors( + @Nullable List origWords, + @Nullable String fullText) { + if (origWords == null || origWords.isEmpty()) return Collections.emptyList(); + final int count = origWords.size(); + List originalSyllables = new ArrayList<>(count); + List romanizedGuides = new ArrayList<>(count); + for (Word w : origWords) { + String text = w.text() != null ? w.text() : ""; + originalSyllables.add(text); + String guide = w.romaji(); + if (guide == null || guide.trim().isEmpty() || ScriptDetector.containsCjk(guide)) { + if (isPurelyLatinScript(text)) { + guide = text.trim(); + } else if (ScriptDetector.containsKana(text)) { + guide = KanaRomanizer.romanize(text).trim(); + } else if (ScriptDetector.contains(text, Character.UnicodeScript.HANGUL)) { + guide = HangulRomanizer.romanize(text).trim(); + } else { + guide = ""; + } + } else { + guide = guide.trim(); + } + romanizedGuides.add(guide != null ? guide : ""); + } + return alignRomanizationAnchors(fullText != null ? fullText : "", romanizedGuides, originalSyllables); + } + + /** + * Partitions fullText into M segments that phonetically match the guides while preserving spaces. + */ + private static List alignRomanizationAnchors( + String fullText, + List romanizedGuides, + List originalSyllables) { + final String target = fullText; + final int N = target.length(); + final int M = romanizedGuides.size(); + + if (M == 0) return Collections.emptyList(); + if (N == 0) { + List empty = new ArrayList<>(M); + for (int i = 0; i < M; i++) empty.add(""); + return empty; + } + + boolean anyGuide = false; + for (String g : romanizedGuides) { + if (g != null && !g.trim().isEmpty()) { + anyGuide = true; + break; + } + } + if (!anyGuide && !target.trim().isEmpty()) { + String[] split = target.trim().split("\\s+"); + List chunks = new ArrayList<>(M); + if (split.length == M) { + for (int i = 0; i < M; i++) { + chunks.add(split[i] + (i < M - 1 ? " " : "")); + } + return chunks; + } else if (split.length > 0) { + if (split.length < M) { + for (int i = 0; i < M; i++) { + int tokStart = (i * split.length) / M; + int tokEnd = ((i + 1) * split.length) / M; + String rom = (tokEnd > tokStart && tokStart < split.length) ? split[tokStart] : ""; + chunks.add(rom + (!rom.isEmpty() && i < M - 1 ? " " : "")); + } + } else { + for (int i = 0; i < M; i++) { + int tokStart = (i * split.length) / M; + int tokEnd = (i == M - 1) ? split.length : (((i + 1) * split.length) / M); + StringBuilder sb = new StringBuilder(); + for (int k = tokStart; k < tokEnd && k < split.length; k++) { + if (sb.length() > 0) sb.append(" "); + sb.append(split[k]); + } + if (i < M - 1) sb.append(" "); + chunks.add(sb.toString()); + } + } + return chunks; + } + } + + double[][] dp = new double[M + 1][N + 1]; + int[][] path = new int[M + 1][N + 1]; + + for (int i = 0; i <= M; i++) { + Arrays.fill(dp[i], Double.MAX_VALUE); + Arrays.fill(path[i], -1); + } + + dp[0][0] = 0.0; + + for (int i = 1; i <= M; i++) { + String guideRom = romanizedGuides.get(i - 1); + boolean isLatin = (i - 1 < originalSyllables.size()) && isPurelyLatinScript(originalSyllables.get(i - 1)); + int guideLen = guideRom.length(); + + // A Latin word reads as itself, so its length barely changes; other scripts may change a lot. + int minLen = isLatin ? Math.max(1, guideLen - 1) : 1; + int maxLen = isLatin ? guideLen + 3 : Math.max(guideLen * 3 + 3, 15); + + for (int j = 1; j <= N; j++) { + double bestLocalCost = Double.MAX_VALUE; + int bestPrevJ = -1; + + for (int len = minLen; len <= maxLen; len++) { + int k = j - len; + if (k < 0) break; + + if (dp[i - 1][k] == Double.MAX_VALUE) continue; + + String candidate = target.substring(k, j); + double segmentCost = calculateMatchCost(candidate, guideRom, isLatin); + double totalCost = dp[i - 1][k] + segmentCost; + + if (totalCost < bestLocalCost) { + bestLocalCost = totalCost; + bestPrevJ = k; } - }); + } + + dp[i][j] = bestLocalCost; + path[i][j] = bestPrevJ; + } + } + + List results = new ArrayList<>(Collections.nCopies(M, "")); + int currJ = N; + + // If no partition covers the whole text, the cheapest one that gets furthest is used. + if (dp[M][N] == Double.MAX_VALUE) { + double minEndCost = Double.MAX_VALUE; + for (int k = N; k >= 0; k--) { + if (dp[M][k] < minEndCost) { + minEndCost = dp[M][k]; + currJ = k; + } + } + } + + for (int i = M; i > 0; i--) { + int prevJ = path[i][currJ]; + if (prevJ == -1) { + results.set(i - 1, ""); + } else { + results.set(i - 1, target.substring(prevJ, currJ)); + currJ = prevJ; + } + } + + // Characters no partition covered go to the first or the last piece. + if (currJ > 0 && !results.isEmpty()) { + results.set(0, target.substring(0, currJ) + results.get(0)); + } + int totalLen = 0; + for (String r : results) totalLen += r.length(); + if (totalLen < N && !results.isEmpty()) { + int safeStart = Math.min(target.length(), Math.max(0, totalLen)); + results.set(results.size() - 1, results.get(results.size() - 1) + target.substring(safeStart)); + } + + return results; } + private static double calculateMatchCost(String candidate, String guide, boolean isLatin) { + String cTrim = candidate.trim(); + String gTrim = guide.trim(); + + if (cTrim.isEmpty() && !gTrim.isEmpty()) return 50.0; + + String cLower = cTrim.toLowerCase(Locale.ROOT); + String gLower = gTrim.toLowerCase(Locale.ROOT); + int dist = levenshteinDistance(cLower, gLower); + + if (isLatin) { + return dist * 50.0 + (Math.abs(candidate.length() - guide.length()) * 0.5); + } + + int lenDiff = Math.abs(cTrim.length() - gTrim.length()); + boolean hasTrailingSpace = candidate.endsWith(" "); + double bonus = hasTrailingSpace ? -0.5 : 0.0; + + return dist + (lenDiff * 0.8) + bonus; + } } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/derive/LyricsTranslator.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/derive/LyricsTranslator.java index 4ba721957b..159f9040aa 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/derive/LyricsTranslator.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/derive/LyricsTranslator.java @@ -22,6 +22,7 @@ import app.morphe.extension.music.patches.lyrics.requests.LyricsRequests; import app.morphe.extension.music.patches.lyrics.storage.LyricsCache; import app.morphe.extension.music.settings.Settings; +import app.morphe.extension.shared.Logger; import app.morphe.extension.shared.Utils; import app.morphe.extension.shared.translation.TextTranslator; @@ -101,6 +102,15 @@ private static List embeddedTranslationLines(Lyrics lyrics, List toLyricsLines(List textLines) { + if (textLines == null) return java.util.Collections.emptyList(); + List lines = new ArrayList<>(textLines.size()); + for (String t : textLines) { + lines.add(new LyricsLine(LyricsLine.NO_TIME, t)); + } + return lines; + } + public static void translate(TrackInfo track, Lyrics lyrics, String source, Callback callback) { Utils.verifyOnMainThread(); @@ -119,27 +129,26 @@ public static void translate(TrackInfo track, Lyrics lyrics, String source, Call executor.execute(() -> { try { - if (Settings.LYRICS_USE_AI_TRANSLATION.get()) { - String baseUrl = Settings.LYRICS_AI_BASE_URL.get(); - String apiToken = Settings.LYRICS_AI_API_TOKEN.get(); - String model = Settings.LYRICS_AI_MODEL.get(); - + String provider = Settings.LYRICS_TRANSLATION_PROVIDER.get(); + if (!AiProviders.GOOGLE.equals(provider)) { List aiCached = LyricsCache.getTranslationAI( track, source, language, lines); if (aiCached != null) { - Utils.runOnMainThread( - () -> callback.onTranslated(aiCached, false, true, model)); + Utils.runOnMainThread(() -> callback.onTranslated(aiCached, false, true, provider)); return; } - List aiResult = aiTranslate(lines, language, track.title(), - track.artist(), baseUrl, apiToken, model); - if (aiResult != null) { - LyricsCache.putTranslationAI(track, source, language, lines, aiResult); - Utils.runOnMainThread( - () -> callback.onTranslated(aiResult, false, true, model)); + AiProviders.Result ai = AiProviders.DEEPL.equals(provider) + ? deepLTranslate(lines, language) + : AiProviders.mapLines(provider, OpenAIClient.renderPrompt( + Settings.LYRICS_AI_TRANSLATION_PROMPT.get(), "translation", + language, track.title(), track.artist(), lines), lines); + if (ai != null && ai.lines() != null && !ai.lines().isEmpty() && LyricsMerge.hasText(toLyricsLines(ai.lines()))) { + LyricsCache.putTranslationAI(track, source, language, lines, ai.lines()); + Utils.runOnMainThread(() -> callback.onTranslated(ai.lines(), false, true, ai.model())); return; } + // AI / DeepL request failed or returned empty; gracefully fall through to Google Translate. } List translated = LyricsCache.getTranslation( @@ -154,18 +163,21 @@ public static void translate(TrackInfo track, Lyrics lyrics, String source, Call List result = translated; Utils.runOnMainThread( () -> callback.onTranslated(result, result != null, false, null)); - } catch (Throwable ignored) { + } catch (Exception ex) { + Logger.printException(() -> "translate failure", ex); Utils.runOnMainThread(() -> callback.onTranslated(null, false, false, null)); } }); } @Nullable - private static List aiTranslate(List lines, String language, - String title, String artist, String baseUrl, String apiToken, String model) { - String prompt = OpenAIClient.renderPrompt(Settings.LYRICS_AI_TRANSLATION_PROMPT.get(), - "translation", language, title, artist, lines); - return OpenAIClient.mapLines(baseUrl, apiToken, model, prompt, lines); + private static AiProviders.Result deepLTranslate(List lines, String language) { + String apiKey = Settings.LYRICS_DEEPL_API_KEY.get(); + if (apiKey.isEmpty()) { + return null; + } + List translated = DeepLClient.translate(lines, language, apiKey); + return translated == null ? null : new AiProviders.Result(translated, "DeepL"); } @Nullable diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/derive/OpenAIClient.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/derive/OpenAIClient.java index b249f31294..ce6a42e31c 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/derive/OpenAIClient.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/derive/OpenAIClient.java @@ -81,9 +81,47 @@ public static boolean validateConfig(String baseUrl, String apiToken, String mod } public static String[] fetchReasoningEfforts(String baseUrl, String apiToken, String model) { + final String models = listModels(baseUrl, apiToken); + if (models == null) { + return new String[0]; + } + try { + return parseSupportedEfforts(models, model); + } catch (JSONException e) { + Logger.printDebug(() -> "Could not read supported reasoning efforts", e); + return new String[0]; + } + } + + /** The ids of the models the endpoint lists, empty when it lists none. */ + public static List fetchModels(String baseUrl, @Nullable String apiToken) { + List ids = new ArrayList<>(); + final String models = listModels(baseUrl, apiToken); + if (models == null) { + return ids; + } + try { + JSONArray data = new JSONObject(models).optJSONArray("data"); + if (data != null) { + for (int i = 0; i < data.length(); i++) { + String id = data.getJSONObject(i).optString("id", ""); + if (!id.isEmpty()) { + ids.add(id); + } + } + } + } catch (JSONException e) { + Logger.printDebug(() -> "Could not read the model list", e); + } + return ids; + } + + /** The raw response of the endpoint's model list, or null if it has none. */ + @Nullable + private static String listModels(String baseUrl, @Nullable String apiToken) { final String url = modelsUrl(baseUrl); if (url == null) { - return new String[0]; + return null; } try { HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection(); @@ -95,15 +133,15 @@ public static String[] fetchReasoningEfforts(String baseUrl, String apiToken, St conn.setRequestProperty("Authorization", "Bearer " + apiToken); } if (conn.getResponseCode() != 200) { - return new String[0]; + return null; } - return parseSupportedEfforts(readResponse(conn), model); + return readResponse(conn); } finally { conn.disconnect(); } } catch (Exception e) { - Logger.printDebug(() -> "Could not read supported reasoning efforts", e); - return new String[0]; + Logger.printDebug(() -> "Could not list the models of " + url, e); + return null; } } @@ -315,7 +353,7 @@ static String request(String baseUrl, String apiToken, String model, } } - private static String readResponse(HttpURLConnection conn) throws IOException { + static String readResponse(HttpURLConnection conn) throws IOException { StringBuilder sb = new StringBuilder(512); try (BufferedReader br = new BufferedReader( new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) { @@ -401,8 +439,85 @@ private static StringBuilder joinNumberedBlock(int blockEnd, String[] lines) { return sb; } - private static String stripLineNumber(String line) { - return line.replaceFirst("^\\d+\\.\\s*", ""); + public static boolean isSkipToken(@Nullable String s) { + if (s == null) return true; + String t = s.trim(); + return t.isEmpty() || t.equalsIgnoreCase("SKIP") || t.equalsIgnoreCase("[SKIP]") + || t.equalsIgnoreCase("") || t.equalsIgnoreCase("(SKIP)") + || t.equalsIgnoreCase("SKIP."); + } + + private static final Pattern NUMBERED_LINE_PATTERN = + Pattern.compile("^(?:Line\\s+)?(\\d{1,4})[.)\\]:\\-]\\s*(.*)$", Pattern.CASE_INSENSITIVE); + + @Nullable + private static List parseNumberedLines(String text, int expectedCount) { + if (text == null || expectedCount <= 0) return null; + String[] raw = text.split("\n", -1); + int end = raw.length; + while (end > 0 && raw[end - 1].trim().isEmpty()) { + end--; + } + if (end == 0) return null; + + String[] byIndex = new String[expectedCount]; + int matchedNumberedCount = 0; + boolean anyIndexedNonEmpty = false; + + for (int i = 0; i < end; i++) { + String line = raw[i].trim(); + if (line.isEmpty()) continue; + Matcher m = NUMBERED_LINE_PATTERN.matcher(line); + if (m.matches()) { + int idx = Integer.parseInt(m.group(1)) - 1; + if (idx >= 0 && idx < expectedCount) { + matchedNumberedCount++; + String val = m.group(2).trim(); + if (isSkipToken(val)) { + byIndex[idx] = ""; + } else { + byIndex[idx] = val; + if (!val.isEmpty()) { + anyIndexedNonEmpty = true; + } + } + } + } + } + + if (matchedNumberedCount >= Math.max(1, expectedCount / 2)) { + if (!anyIndexedNonEmpty) { + return null; + } + List indexedList = new ArrayList<>(expectedCount); + for (int i = 0; i < expectedCount; i++) { + indexedList.add(byIndex[i] != null ? byIndex[i] : ""); + } + return indexedList; + } + + List list = new ArrayList<>(expectedCount); + boolean anyNonEmpty = false; + for (int i = 0; i < end && list.size() < expectedCount; i++) { + String line = raw[i].trim(); + if (line.isEmpty()) continue; + line = line.replaceFirst("^\\d+[.)\\]:\\-]\\s*", "").trim(); + if (isSkipToken(line)) { + line = ""; + } else { + anyNonEmpty = true; + } + list.add(line); + } + + if (!anyNonEmpty) { + return null; + } + + while (list.size() < expectedCount) { + list.add(""); + } + return list; } private static boolean isNoteOrEmptyLine(String text) { @@ -413,54 +528,44 @@ private static boolean isNoteOrEmptyLine(String text) { } @Nullable - static List mapLines(String baseUrl, String apiToken, String model, + public static List mapLines(String baseUrl, String apiToken, String model, String prompt, List sourceLines) { + if (!fitsOneRequest(sourceLines)) { + return null; + } + return mapResponse(request(baseUrl, apiToken, model, prompt), sourceLines); + } + + static boolean fitsOneRequest(List sourceLines) { int totalChars = 0; for (String line : sourceLines) { totalChars += line.length() + 1; } - if (totalChars > MAX_CHARS) { - return null; - } - String response = request(baseUrl, apiToken, model, prompt); + return totalChars <= MAX_CHARS; + } + + /** Lines up a numbered model response with the source lines, or null if it says nothing. */ + @Nullable + static List mapResponse(@Nullable String response, List sourceLines) { if (response == null) { return null; } - String[] result = response.split("\n", -1); - int end = result.length; - while (end > 0 && result[end - 1].trim().isEmpty()) { - end--; - } - if (end == 0) { - return null; - } - boolean allSkip = true; - for (int i = 0; i < end; i++) { - result[i] = stripLineNumber(result[i]); - if (!result[i].trim().equalsIgnoreCase("SKIP")) { - allSkip = false; - } - } - if (allSkip) { - return null; - } - if (end > sourceLines.size() + 2 || end < sourceLines.size() - 2) { + List parsed = parseNumberedLines(response, sourceLines.size()); + if (parsed == null) { return null; } List out = new ArrayList<>(sourceLines.size()); + boolean anyValid = false; for (int i = 0; i < sourceLines.size(); i++) { - if (i < end) { - String trimmed = result[i].trim(); - if (trimmed.equalsIgnoreCase("SKIP") || isNoteOrEmptyLine(sourceLines.get(i))) { - out.add(""); - } else { - out.add(trimmed); - } - } else { + String trimmed = parsed.get(i).trim(); + if (isSkipToken(trimmed) || isNoteOrEmptyLine(sourceLines.get(i))) { out.add(""); + } else { + out.add(trimmed); + anyValid = true; } } - return out; + return anyValid ? out : null; } /** @@ -526,8 +631,12 @@ public static String renderPrompt(String template, String task, String language, template = DEFAULT_PROMPT; } StringBuilder linesText = new StringBuilder(); - for (String line : lines) { - linesText.append(Objects.requireNonNullElse(line, "")).append('\n'); + for (int i = 0; i < lines.size(); i++) { + String line = Objects.requireNonNullElse(lines.get(i), "").trim(); + if (!line.matches("^\\d+\\.\\s.*")) { + linesText.append(i + 1).append(". "); + } + linesText.append(line.isEmpty() ? "SKIP" : line).append('\n'); } String count = String.valueOf(lines.size()); Matcher matcher = PROMPT_VARIABLE.matcher(template); diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/LyricsLine.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/LyricsLine.java index fa15fa8e3a..044ed83375 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/LyricsLine.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/LyricsLine.java @@ -10,9 +10,12 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import java.util.ArrayList; import java.util.Collections; import java.util.List; +import app.morphe.extension.music.patches.lyrics.romanizers.ScriptDetector; + /** * A single lyrics line. * @@ -33,8 +36,61 @@ public record LyricsLine(long startTimeMs, long endTimeMs, String text, List 1 && !ScriptDetector.contains(text, Character.UnicodeScript.HAN, + Character.UnicodeScript.HIRAGANA, Character.UnicodeScript.KATAKANA)) { + boolean hasEndedWithSpace = false; + for (Word w : words) { + if (w.endsWithSpace()) { + hasEndedWithSpace = true; + break; + } + } + if (text.indexOf(' ') < 0) { + if (hasEndedWithSpace) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < words.size(); i++) { + Word w = words.get(i); + sb.append(w.text()); + if (w.endsWithSpace() && i < words.size() - 1) { + sb.append(' '); + } + } + text = sb.toString(); + } else { + List updated = new ArrayList<>(words.size()); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < words.size(); i++) { + Word w = words.get(i); + if (i > 0) sb.append(' '); + sb.append(w.text()); + boolean sp = (i < words.size() - 1); + updated.add(new Word(w.startMs(), w.endMs(), w.text(), w.romaji(), sp)); + } + words = updated; + text = sb.toString(); + } + } else if (!hasEndedWithSpace) { + List updated = new ArrayList<>(words.size()); + int cursor = 0; + for (int i = 0; i < words.size(); i++) { + Word w = words.get(i); + int idx = text.indexOf(w.text(), cursor); + boolean sp = false; + if (idx >= 0) { + int end = idx + w.text().length(); + sp = (end < text.length() && Character.isWhitespace(text.charAt(end))); + cursor = end; + } else { + sp = (i < words.size() - 1); + } + updated.add(new Word(w.startMs(), w.endMs(), w.text(), w.romaji(), sp)); + } + words = updated; + } + } + words = Collections.unmodifiableList(words); } public LyricsLine(long startTimeMs, String text, List words, diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/LyricsMerge.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/LyricsMerge.java index 653785a605..d08f8b2fb3 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/LyricsMerge.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/LyricsMerge.java @@ -192,24 +192,52 @@ public static List mergeRomanization(List original, if (orig.isBG()) { winEnd = winStart + BACKGROUND_MATCH_WINDOW_MS; } else { - winEnd = windowEnd(original, i, winStart); + long foundEnd = Long.MAX_VALUE; + for (int nextIdx = i + 1; nextIdx < size; nextIdx++) { + LyricsLine nextOrig = original.get(nextIdx); + if (!nextOrig.isBG() && nextOrig.startTimeMs() > winStart + 50L) { + foundEnd = nextOrig.startTimeMs(); + break; + } + } + winEnd = foundEnd; } - String matched = ""; - while (auxIdx < auxCount) { - long auxStart = sorted.get(auxIdx).startTimeMs(); - if (auxStart < winStart - 500) { - auxIdx++; + LyricsLine matchedLine = null; + int bestIdx = -1; + long bestDiff = Long.MAX_VALUE; + for (int k = auxIdx; k < auxCount; k++) { + LyricsLine cand = sorted.get(k); + long auxStart = cand.startTimeMs(); + if (auxStart < winStart - 500L) { + if (k == auxIdx) { + auxIdx++; + } continue; } if (auxStart >= winEnd) { break; } - matched = sorted.get(auxIdx).text(); - auxIdx++; - break; + long diff = Math.abs(auxStart - winStart); + if (diff < bestDiff && diff <= 2500L) { + bestDiff = diff; + bestIdx = k; + matchedLine = cand; + } + if (auxStart >= winStart && diff <= 150L) { + break; + } + } + if (bestIdx >= 0) { + auxIdx = bestIdx + 1; + } + String matched = matchedLine != null ? matchedLine.text() : ""; + if (matchedLine != null && matchedLine.hasWords()) { + result.add(new LyricsLine(winStart, matchedLine.endTimeMs(), matched == null ? "" : matched, + matchedLine.words(), orig.agentId(), orig.isDuet(), orig.isBG(), orig.songPart())); + } else { + result.add(new LyricsLine(winStart, matched == null ? "" : matched)); } - result.add(new LyricsLine(winStart, matched == null ? "" : matched)); } return result; } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/parsers/LRCParser.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/parsers/LRCParser.java index 79bd1099fb..a96b5f19b2 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/parsers/LRCParser.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/parsers/LRCParser.java @@ -87,6 +87,33 @@ public static List extractCreditMetadata(@Nullable String lrc) { return creditLines; } + public static boolean hasOuterParentheses(@Nullable String text) { + if (text == null) { + return false; + } + String trimmed = text.trim(); + if (trimmed.length() >= 2) { + char first = trimmed.charAt(0); + char last = trimmed.charAt(trimmed.length() - 1); + return (first == '(' && last == ')') + || (first == '(' && last == ')') + || (first == '[' && last == ']') + || (first == '{' && last == '}') + || (first == '<' && last == '>'); + } + return false; + } + + public static String cleanTextForComparison(@Nullable String text) { + if (text == null) { + return ""; + } + return text.replaceAll("[\"'`.,!?:;~()()\\[\\]{}<>«»“”‘’]+", "").trim().toLowerCase(Locale.ROOT); + } + + private static final Pattern SPEAKER_TAG_PATTERN = + Pattern.compile("^(bg|v\\d+|[FMD]|duet|male|female)$", Pattern.CASE_INSENSITIVE); + /** * Parses synced LRC content. * @@ -97,7 +124,11 @@ public static List parseSynced(@Nullable String lrc) { return Collections.emptyList(); } - List lines = new ArrayList<>(); + record RawEntry(long timeMs, String text, List words, @Nullable String agentId, + boolean isDuet, boolean isBg, @Nullable String songPart) { + } + + List rawEntries = new ArrayList<>(); // The LRC "offset" tag shifts every timestamp, and is applied while parsing // because it belongs to the file rather than to the user configured offset. long fileOffsetMs = 0; @@ -109,6 +140,9 @@ public static List parseSynced(@Nullable String lrc) { } List timestamps = new ArrayList<>(1); + String currentSpeaker = null; + boolean lineIsBg = false; + boolean lineIsDuet = false; int index = 0; // A leading bracket that is neither metadata nor a timestamp names the section the line // opens, e.g. [00:10.00][Verse 1] walking down the street. The first one wins. @@ -116,7 +150,6 @@ public static List parseSynced(@Nullable String lrc) { // Enhanced LRC may put the vocal agent in its own leading bracket, [v1:], rather than in // front of the text. A marker in the text still wins, so the two forms agree. String bracketedAgent = null; - boolean isBg = false; while (index < line.length() && line.charAt(index) == '[') { int end = line.indexOf(']', index); @@ -124,14 +157,18 @@ public static List parseSynced(@Nullable String lrc) { break; } - String tag = line.substring(index + 1, end); - if (BG_TAG.equalsIgnoreCase(tag)) { - isBg = true; + String tag = line.substring(index + 1, end).trim(); + if (BG_TAG.equalsIgnoreCase(tag) || "bg".equalsIgnoreCase(tag)) { + lineIsBg = true; } else if (isMetadataTag(tag)) { Long offset = parseOffsetTag(tag); if (offset != null) { fileOffsetMs = offset; } + } else if ("duet".equalsIgnoreCase(tag)) { + lineIsDuet = true; + } else if (SPEAKER_TAG_PATTERN.matcher(tag).matches()) { + currentSpeaker = tag.toLowerCase(Locale.ROOT); } else { long time = parseTimestamp(tag); if (time != LyricsLine.NO_TIME) { @@ -157,8 +194,8 @@ public static List parseSynced(@Nullable String lrc) { String text = body.text.trim(); List words = body.words; String agentMarker = agentMarker(text); - String agentId = null; - boolean isDuet = false; + String agentId = currentSpeaker; + boolean isDuet = lineIsDuet; if (agentMarker != null) { text = text.substring(agentMarker.length()).trim(); words = stripAgentMarker(words, agentMarker); @@ -167,28 +204,195 @@ public static List parseSynced(@Nullable String lrc) { } else if (bracketedAgent != null) { agentId = bracketedAgent.substring(0, bracketedAgent.length() - 1); isDuet = isDuetAgent(agentId); + } else if (agentId != null && agentId.matches(".*\\d+$")) { + isDuet = isDuetAgent(agentId); } + final boolean isBg = lineIsBg || "bg".equalsIgnoreCase(agentId) + || "bg".equalsIgnoreCase(currentSpeaker) || hasOuterParentheses(text); if (!words.isEmpty()) { for (long time : timestamps) { - lines.add(new LyricsLine(Math.max(0, time + fileOffsetMs), LyricsLine.NO_TIME, + rawEntries.add(new RawEntry(Math.max(0, time + fileOffsetMs), text, words, agentId, isDuet, isBg, songPart)); } - } else if (!text.isEmpty()) { + } else { for (long time : timestamps) { - lines.add(new LyricsLine(Math.max(0, time + fileOffsetMs), LyricsLine.NO_TIME, - text, List.of(), agentId, isDuet, isBg, songPart)); + rawEntries.add(new RawEntry(Math.max(0, time + fileOffsetMs), + text, Collections.emptyList(), agentId, isDuet, isBg, songPart)); } } } + if (rawEntries.isEmpty()) { + return Collections.emptyList(); + } + + rawEntries.sort(Comparator.comparingLong(RawEntry::timeMs)); + + List lines = new ArrayList<>(); + final int entryCount = rawEntries.size(); + + for (int i = 0; i < entryCount; i++) { + RawEntry current = rawEntries.get(i); + String text = current.text().trim(); + if (isBreakMarker(text)) { + continue; + } + + long startMs = current.timeMs(); + long endMs = LyricsLine.NO_TIME; + + if (!current.words().isEmpty()) { + endMs = current.words().get(current.words().size() - 1).endMs(); + } + + if (endMs == LyricsLine.NO_TIME) { + RawEntry next = null; + for (int j = i + 1; j < entryCount; j++) { + RawEntry candidate = rawEntries.get(j); + if (candidate.timeMs() > startMs) { + if (!current.isBg() && candidate.isBg()) { + continue; + } + next = candidate; + break; + } + } + + if (next != null && isBreakMarker(next.text())) { + endMs = next.timeMs(); + } else { + endMs = estimateEndMs(startMs, next != null ? next.timeMs() : LyricsLine.NO_TIME, text); + } + } + + lines.add(new LyricsLine(startMs, endMs, text, current.words(), + current.agentId(), current.isDuet(), current.isBg(), current.songPart())); + } + if (lines.isEmpty()) { return Collections.emptyList(); } - lines.sort(Comparator.comparingLong(LyricsLine::startTimeMs)); + markEchoesAsBackground(lines); + resolveDuet(lines); return lines; } + /** An empty line or a lone note marks where singing stops. */ + private static boolean isBreakMarker(String text) { + String trimmed = text.trim(); + return trimmed.isEmpty() || "♪".equals(trimmed) || "♪♪".equals(trimmed); + } + + /** A line repeated right after itself is a backing vocal echoing the lead. */ + private static final long ECHO_WINDOW_MS = 3500; + /** A gap shorter than this is sung through, so the line lasts until the next one starts. */ + private static final long HELD_GAP_MS = 11_000; + private static final long MIN_HOLD_MS = 5000; + private static final long HOLD_PER_CHAR_MS = 180; + /** How long before the next line a long instrumental gap is shown as such. */ + private static final long GAP_LEAD_IN_MS = 7000; + + /** + * When a line that has no end of its own stops being sung: at the next line if that follows + * soon, otherwise after a hold that grows with the text, so a long instrumental gap shows as one. + * + * @param nextStartMs Start of the next line, or {@link LyricsLine#NO_TIME} for the last line. + */ + public static long estimateEndMs(long startMs, long nextStartMs, String text) { + final long hold = Math.max(MIN_HOLD_MS, text.length() * HOLD_PER_CHAR_MS); + if (nextStartMs == LyricsLine.NO_TIME) { + return startMs + hold; + } + if (nextStartMs - startMs < HELD_GAP_MS) { + return nextStartMs; + } + return Math.min(startMs + hold, nextStartMs - GAP_LEAD_IN_MS); + } + + /** Gives every line of a line synced lyric an end, from where the line after it starts. */ + public static void fillEstimatedEnds(List lines) { + for (int i = 0; i < lines.size(); i++) { + LyricsLine cur = lines.get(i); + long nextStart = i + 1 < lines.size() ? lines.get(i + 1).startTimeMs() : LyricsLine.NO_TIME; + lines.set(i, new LyricsLine(cur.startTimeMs(), estimateEndMs(cur.startTimeMs(), nextStart, cur.text()), + cur.text(), cur.words(), cur.agentId(), cur.isDuet(), cur.isBG(), cur.songPart())); + } + } + + /** Marks a line that repeats the one just before it as a background vocal. */ + public static void markEchoesAsBackground(List lines) { + for (int i = 1; i < lines.size(); i++) { + LyricsLine prev = lines.get(i - 1); + LyricsLine curr = lines.get(i); + if (curr.isBG()) continue; + long gap = curr.startTimeMs() - prev.startTimeMs(); + boolean overlaps = prev.endTimeMs() != LyricsLine.NO_TIME && prev.endTimeMs() > curr.startTimeMs(); + if (!overlaps && (gap < 0 || gap > 1200L)) continue; + String cleanCurr = cleanTextForComparison(curr.text()); + if (!cleanCurr.isEmpty() && cleanCurr.equals(cleanTextForComparison(prev.text()))) { + lines.set(i, new LyricsLine(curr.startTimeMs(), curr.endTimeMs(), curr.text(), + curr.words(), curr.agentId(), curr.isDuet(), true, curr.songPart())); + } + } + } + + private static void resolveDuet(List lines) { + String lastPersonSpeaker = null; + boolean lastPersonIsDuet = false; + boolean hasMultipleSpeakers = false; + String firstSpeaker = null; + + for (LyricsLine line : lines) { + String sp = line.agentId(); + if (sp != null && !sp.isEmpty() && !"bg".equalsIgnoreCase(sp) && !"duet".equalsIgnoreCase(sp)) { + if (firstSpeaker == null) { + firstSpeaker = sp; + } else if (!firstSpeaker.equalsIgnoreCase(sp)) { + hasMultipleSpeakers = true; + break; + } + } + } + + if (!hasMultipleSpeakers) { + return; + } + + for (int i = 0; i < lines.size(); i++) { + LyricsLine orig = lines.get(i); + String sp = orig.agentId(); + if (sp == null || sp.isEmpty() || "bg".equalsIgnoreCase(sp) || "duet".equalsIgnoreCase(sp)) { + continue; + } + + boolean isDuet; + if ("v1".equalsIgnoreCase(sp) || "m".equalsIgnoreCase(sp) || "male".equalsIgnoreCase(sp)) { + isDuet = false; + lastPersonSpeaker = sp; + lastPersonIsDuet = false; + } else if ("v2".equalsIgnoreCase(sp) || "f".equalsIgnoreCase(sp) || "female".equalsIgnoreCase(sp)) { + isDuet = true; + lastPersonSpeaker = sp; + lastPersonIsDuet = true; + } else if (lastPersonSpeaker == null) { + isDuet = false; + lastPersonSpeaker = sp; + lastPersonIsDuet = false; + } else if (sp.equalsIgnoreCase(lastPersonSpeaker)) { + isDuet = lastPersonIsDuet; + } else { + isDuet = !lastPersonIsDuet; + lastPersonSpeaker = sp; + lastPersonIsDuet = isDuet; + } + + lines.set(i, new LyricsLine( + orig.startTimeMs(), orig.endTimeMs(), orig.text(), + orig.words(), sp, isDuet, orig.isBG(), orig.songPart())); + } + } + private record BodyParse(String text, List words) { } @@ -245,7 +449,15 @@ private static BodyParse parseBody(String body, long lineStartMs) { return new BodyParse(body, List.of()); } - addWord(words, pendingStart, pending.toString()); + if (!pending.toString().trim().isEmpty()) { + addWord(words, pendingStart, pending.toString()); + } else if (pendingStart != LyricsLine.NO_TIME && !words.isEmpty()) { + int lastIdx = words.size() - 1; + Word last = words.get(lastIdx); + if (pendingStart > last.startMs()) { + words.set(lastIdx, new Word(last.startMs(), pendingStart, last.text(), last.romaji(), last.endsWithSpace())); + } + } if (!prefix.trim().isEmpty()) { words.add(0, new Word(lineStartMs, LyricsLine.NO_TIME, prefix.trim())); @@ -341,10 +553,13 @@ public static String formatLine(LyricsLine line) { String agentMarker = agentMarker(line.agentId() == null ? null : line.agentId() + ":"); if (agentMarker != null) { builder.append(agentMarker); + } else if (line.isDuet()) { + builder.append("[duet]"); } if (line.hasWords()) { final List words = line.words(); final String marks = spacesAfter(line.text(), words); + Word lastWord = null; for (int i = 0; i < words.size(); i++) { final Word word = words.get(i); builder.append('<') @@ -354,6 +569,12 @@ public static String formatLine(LyricsLine line) { if (marks.charAt(i) == SEPARATED) { builder.append(' '); } + lastWord = word; + } + if (lastWord != null && lastWord.endMs() > lastWord.startMs()) { + builder.append('<') + .append(formatWordTimestamp(lastWord.endMs())) + .append('>'); } } else { builder.append(line.text()); diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/parsers/TTMLParser.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/parsers/TTMLParser.java index d56d21f2a7..c974bf576a 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/parsers/TTMLParser.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/parsers/TTMLParser.java @@ -25,6 +25,7 @@ import java.util.regex.Pattern; import app.morphe.extension.music.patches.lyrics.TextTrim; +import app.morphe.extension.music.patches.lyrics.derive.LyricsRomanizer; import app.morphe.extension.music.patches.lyrics.model.Lyrics; import app.morphe.extension.music.patches.lyrics.model.LyricsLine; import app.morphe.extension.music.patches.lyrics.model.LyricsMerge; @@ -198,9 +199,9 @@ public static TtmlResult parse(String ttml) { final List songwriters; if (!hm.songwriters().isEmpty()) { - StringBuilder sb = new StringBuilder("Written by "); + StringBuilder sb = new StringBuilder("Written by: "); for (int i = 0; i < hm.songwriters().size(); i++) { - if (i > 0) sb.append(" · "); + if (i > 0) sb.append(", "); sb.append(hm.songwriters().get(i)); } songwriters = List.of(sb.toString()); @@ -253,7 +254,7 @@ public static TtmlResult parse(String ttml) { final ParsedLine pl = processPElement(p, pBegin, pEnd); - if (pl != null && !pl.text().trim().isEmpty()) { + if (pl != null && (!pl.text().trim().isEmpty() || !pl.bgLines().isEmpty())) { final List lineSidecar = findSidecarRoman(lineId, sidecarRoman); // Read off the sidecar before the words carry a reading, or the @@ -264,10 +265,12 @@ public static TtmlResult parse(String ttml) { alignRomajiToWords(pl.words(), lineSidecar); } - final LyricsLine line = new LyricsLine( - pl.begin(), pl.end(), pl.text(), pl.words(), - agentId, false, false, divSongPart); - lines.add(line); + if (!pl.text().trim().isEmpty()) { + final LyricsLine line = new LyricsLine( + pl.begin(), pl.end(), pl.text(), pl.words(), + agentId, false, false, divSongPart); + lines.add(line); + } mainLineCount = lines.size(); final String bgAgent = pl.bgAgentId() != null ? pl.bgAgentId() : agentId; @@ -279,8 +282,10 @@ public static TtmlResult parse(String ttml) { lines.add(bgLine); } - romanization.add(new LyricsLine( - LyricsLine.NO_TIME, romaText)); + if (!pl.text().trim().isEmpty()) { + romanization.add(new LyricsLine( + LyricsLine.NO_TIME, romaText)); + } for (int i = 0; i < pl.bgLines().size(); i++) { romanization.add(new LyricsLine(LyricsLine.NO_TIME, "")); } @@ -959,6 +964,11 @@ private static ParsedLine processPElement(XmlPullParser p, long pBegin, long pEn bgWords.add(new Word(bgWordBegin, bgWordEnd, text, null, endsWithSpace)); } + } else if (inBg && bgWrapperDepth > 0) { + bgWrapperDepth--; + } else if (inBg) { + flushBgLine(bgLines, bgWords, bgFullText, bgBeginMs, bgEndMs, pBegin, pEnd); + inBg = false; } else if (inRubyText && inRubyContainer) { inRubyText = false; final String text = normalizeText(rubyTextBuf.toString()); @@ -1046,8 +1056,8 @@ private static ParsedLine processPElement(XmlPullParser p, long pBegin, long pEn } else if (event == XmlPullParser.TEXT) { final String raw = p.getText(); if (raw == null || raw.isEmpty()) continue; - final boolean formattingNewline = raw.contains("\n") && raw.trim().isEmpty(); - final boolean spaceBetweenWords = !raw.contains("\n") && raw.trim().isEmpty(); + final boolean isWhitespace = raw.trim().isEmpty(); + final boolean hasNewline = raw.contains("\n"); if (inBgTranslation) { bgTransBuf.append(raw); @@ -1060,52 +1070,83 @@ private static ParsedLine processPElement(XmlPullParser p, long pBegin, long pEn } else if (inRubyText && inRubyContainer) { rubyTextBuf.append(raw); } else if (inBg && bgWrapperDepth == 0) { - // Formatting newlines (whitespace + \n) between BG word spans - if (!formattingNewline) { - bgFullText.append(raw); - } - // Whitespace-only (non-newline) between BG word spans - if (spaceBetweenWords && !bgWords.isEmpty()) { - final Word prev = bgWords.get(bgWords.size() - 1); - if (!prev.endsWithSpace()) { - bgWords.set(bgWords.size() - 1, new Word( - prev.startMs(), prev.endMs(), - prev.text(), prev.romaji(), true)); + if (isWhitespace) { + if (!bgWords.isEmpty()) { + final Word prev = bgWords.get(bgWords.size() - 1); + final boolean cjk = isContinuousCjk(prev.text()); + if (!hasNewline || !cjk) { + if (bgFullText.length() > 0 && bgFullText.charAt(bgFullText.length() - 1) != ' ') { + bgFullText.append(' '); + } + if (!prev.endsWithSpace()) { + bgWords.set(bgWords.size() - 1, new Word( + prev.startMs(), prev.endMs(), + prev.text(), prev.romaji(), true)); + } + } } + } else { + bgFullText.append(raw); } } else if (inTranslation) { transBuf.append(raw); } else if (inRoman) { romanBuf.append(raw); - } else if (inWord) { + } else if (inWord && !inRubyContainer) { wordBuf.append(raw); - } else if (!inBg) { - // Formatting newlines (whitespace + \n) between word spans - skip entirely - if (!formattingNewline) { - fullText.append(raw); - // Whitespace-only (non-newline) between word spans -> trailing space - if (spaceBetweenWords && !words.isEmpty()) { + } else if (!inBg && !inRubyContainer) { + if (isWhitespace) { + if (!words.isEmpty()) { final Word prev = words.get(words.size() - 1); - if (!prev.endsWithSpace()) { - words.set(words.size() - 1, new Word( - prev.startMs(), prev.endMs(), - prev.text(), prev.romaji(), true)); + final boolean cjk = isContinuousCjk(prev.text()); + if (!hasNewline || !cjk) { + if (fullText.length() > 0 && fullText.charAt(fullText.length() - 1) != ' ') { + fullText.append(' '); + } + if (!prev.endsWithSpace()) { + words.set(words.size() - 1, new Word( + prev.startMs(), prev.endMs(), + prev.text(), prev.romaji(), true)); + } } } + } else { + fullText.append(raw); } } } } - final String lineText = normalizeText(fullText.toString()); - if (lineText.trim().isEmpty()) { + final String rawLineText = normalizeText(fullText.toString()); + String lineText = rawLineText; + if (lineText.indexOf(' ') < 0 && words.size() > 1 && !isContinuousCjk(words.get(0).text())) { + StringBuilder reconstructed = new StringBuilder(); + for (int i = 0; i < words.size(); i++) { + final Word w = words.get(i); + if (i > 0) reconstructed.append(' '); + reconstructed.append(w.text()); + final boolean shouldEndWithSpace = (i < words.size() - 1); + if (w.endsWithSpace() != shouldEndWithSpace) { + words.set(i, new Word(w.startMs(), w.endMs(), w.text(), w.romaji(), shouldEndWithSpace)); + } + } + lineText = reconstructed.toString(); + } + + // A background vocal that is still open ends with its line. + if (inBg) { + flushBgLine(bgLines, bgWords, bgFullText, bgBeginMs, bgEndMs, pBegin, pEnd); + inBg = false; + } + + if (lineText.trim().isEmpty() && bgLines.isEmpty()) { return null; } - // Infer time range from children when

has no timing + // The words give the line its range, so a syllable sung past the line's own end is not cut off. long effectiveBegin = pBegin; long effectiveEnd = pEnd; - if ((effectiveBegin == 0 || effectiveEnd == 0) && !words.isEmpty()) { + if (!words.isEmpty()) { long minStart = Long.MAX_VALUE; long maxEnd = 0; for (Word w : words) { @@ -1120,9 +1161,12 @@ private static ParsedLine processPElement(XmlPullParser p, long pBegin, long pEn effectiveEnd = maxEnd; } } + } else if ((effectiveBegin == 0 || effectiveEnd == 0) && !bgLines.isEmpty()) { + effectiveBegin = bgLines.get(0).startTimeMs(); + effectiveEnd = bgLines.get(bgLines.size() - 1).endTimeMs(); } - if (effectiveBegin == 0 && effectiveEnd == 0 && words.isEmpty()) { + if (effectiveBegin == 0 && effectiveEnd == 0 && words.isEmpty() && bgLines.isEmpty()) { effectiveBegin = LyricsLine.NO_TIME; effectiveEnd = LyricsLine.NO_TIME; } @@ -1145,6 +1189,11 @@ private static ParsedLine processPElement(XmlPullParser p, long pBegin, long pEn // Defensive: only reachable if an x-bg span was never closed. flushBgLine(bgLines, bgWords, bgFullText, bgBeginMs, bgEndMs, pBegin, pEnd); } + for (LyricsLine bg : bgLines) { + if (bg.endTimeMs() > effectiveEnd && effectiveEnd > 0) { + effectiveEnd = bg.endTimeMs(); + } + } return new ParsedLine(effectiveBegin, effectiveEnd, lineText, words, bgLines, bgAgentId, @@ -1154,36 +1203,42 @@ private static ParsedLine processPElement(XmlPullParser p, long pBegin, long pEn bgInlineRoma.isEmpty() ? null : bgInlineRoma); } - @SuppressWarnings("SizeReplaceableByIsEmpty") - private static void flushBgLine(List bgLines, List bgWords, - StringBuilder bgFullText, long bgBeginMs, - long bgEndMs, long pBegin, long pEnd) { + private static void flushBgLine( + List bgLines, + List bgWords, + StringBuilder bgFullText, + long bgBeginMs, + long bgEndMs, + long pBegin, + long pEnd) { if (bgWords.isEmpty() && bgFullText.length() == 0) { return; } String bgText = normalizeText(bgFullText.toString()); - if (bgText.trim().isEmpty()) { - return; - } - bgText = bgText.replaceAll("^[((]+", "").replaceAll("[))]+$", "").trim(); - if (bgText.isEmpty()) { - return; - } - stripBgWordParens(bgWords); - - long bgStart = bgBeginMs > 0 ? bgBeginMs : pBegin; - long bgEnd = bgEndMs > 0 ? bgEndMs : pEnd; - if (bgBeginMs <= 0 && bgEndMs <= 0 && !bgWords.isEmpty()) { - long minStart = Long.MAX_VALUE; - long maxEnd = 0; - for (Word w : bgWords) { - if (w.startMs() > 0 && w.startMs() < minStart) minStart = w.startMs(); - if (w.endMs() > 0 && w.endMs() > maxEnd) maxEnd = w.endMs(); + if (!bgText.trim().isEmpty()) { + String cleanBgText = bgText.replaceAll("^[((]+", "").replaceAll("[))]+$", "").trim(); + stripBgWordParens(bgWords); + if (cleanBgText.indexOf(' ') < 0 && bgWords.size() > 1 && !isContinuousCjk(bgWords.get(0).text())) { + StringBuilder reconstructed = new StringBuilder(); + for (int i = 0; i < bgWords.size(); i++) { + final Word w = bgWords.get(i); + if (i > 0) reconstructed.append(' '); + reconstructed.append(w.text()); + final boolean shouldEndWithSpace = (i < bgWords.size() - 1); + if (w.endsWithSpace() != shouldEndWithSpace) { + bgWords.set(i, new Word(w.startMs(), w.endMs(), w.text(), w.romaji(), shouldEndWithSpace)); + } + } + cleanBgText = reconstructed.toString(); + } + if (!cleanBgText.isEmpty()) { + final long bgStart = bgBeginMs > 0 ? bgBeginMs : (bgWords.isEmpty() ? pBegin : bgWords.get(0).startMs()); + final long bgEnd = bgEndMs > 0 ? bgEndMs : (bgWords.isEmpty() ? pEnd : bgWords.get(bgWords.size() - 1).endMs()); + bgLines.add(new LyricsLine(bgStart, bgEnd, cleanBgText, new ArrayList<>(bgWords))); } - if (minStart < Long.MAX_VALUE) bgStart = minStart; - if (maxEnd > 0) bgEnd = maxEnd; } - bgLines.add(new LyricsLine(bgStart, bgEnd, bgText, new ArrayList<>(bgWords))); + bgWords.clear(); + bgFullText.setLength(0); } private static void stripBgWordParens(List bgWords) { @@ -1205,6 +1260,19 @@ private static void stripBgWordParens(List bgWords) { } } + public static boolean isContinuousCjk(@Nullable String s) { + if (s == null || s.isEmpty()) return false; + int cp = s.codePointBefore(s.length()); + Character.UnicodeBlock b = Character.UnicodeBlock.of(cp); + return b == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS + || b == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_A + || b == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_B + || b == Character.UnicodeBlock.CJK_COMPATIBILITY_IDEOGRAPHS + || b == Character.UnicodeBlock.CJK_SYMBOLS_AND_PUNCTUATION + || b == Character.UnicodeBlock.HIRAGANA + || b == Character.UnicodeBlock.KATAKANA; + } + private static void alignRomajiToWords(List words, List sidecar) { if (words.isEmpty() || sidecar.isEmpty()) { @@ -1279,10 +1347,22 @@ private static List findSidecarRoman(String lineId, private static String buildLineRomaji(List words, @Nullable List sidecar) { + if (sidecar != null && !sidecar.isEmpty()) { + final StringBuilder sb = new StringBuilder(); + for (RomajiSyllable s : sidecar) { + if (s.text().isEmpty()) continue; + sb.append(s.text()); + } + final String result = sb.toString().trim(); + if (!result.isEmpty() && LyricsRomanizer.isPurelyLatinScript(result)) { + return result; + } + } + final StringBuilder perWord = new StringBuilder(); boolean hasPerWord = false; for (Word w : words) { - if (w.romaji() != null && !w.romaji().isEmpty()) { + if (w.romaji() != null && !w.romaji().isEmpty() && LyricsRomanizer.isPurelyLatinScript(w.romaji())) { hasPerWord = true; break; } @@ -1290,29 +1370,15 @@ private static String buildLineRomaji(List words, if (hasPerWord) { for (Word w : words) { String r = w.romaji(); - if (r != null && !r.isEmpty()) { - //noinspection SizeReplaceableByIsEmpty - if (perWord.length() > 0) perWord.append(' '); + if (r != null && !r.isEmpty() && LyricsRomanizer.isPurelyLatinScript(r)) { perWord.append(r); + if (w.endsWithSpace() && !r.endsWith(" ")) { + perWord.append(' '); + } } } - //noinspection SizeReplaceableByIsEmpty if (perWord.length() > 0) { - return perWord.toString(); - } - } - - if (sidecar != null && !sidecar.isEmpty()) { - final StringBuilder sb = new StringBuilder(); - for (RomajiSyllable s : sidecar) { - if (s.text().isEmpty()) continue; - //noinspection SizeReplaceableByIsEmpty - if (sb.length() > 0) sb.append(' '); - sb.append(s.text()); - } - final String result = sb.toString().trim(); - if (!result.isEmpty()) { - return result; + return perWord.toString().trim(); } } @@ -1354,9 +1420,10 @@ private static void buildRomanizations(String lineId, final StringBuilder sb = new StringBuilder(); for (Word w : alignedWords) { if (w.romaji() != null && !w.romaji().isEmpty()) { - //noinspection SizeReplaceableByIsEmpty - if (sb.length() > 0) sb.append(' '); sb.append(w.romaji()); + if (w.endsWithSpace() && !w.romaji().endsWith(" ")) { + sb.append(' '); + } } } final String text = sb.toString().trim(); @@ -1367,7 +1434,8 @@ private static void buildRomanizations(String lineId, langLines.add(new LyricsLine(LyricsLine.NO_TIME, "")); } if (langLines.size() == mainIndex) { - langLines.add(new LyricsLine(LyricsLine.NO_TIME, text)); + String lineContent = (!text.isEmpty() && LyricsRomanizer.isPurelyLatinScript(text)) ? text : ""; + langLines.add(new LyricsLine(LyricsLine.NO_TIME, lineContent)); } } padDerivedTo(romanizations, mainIndex + 1 + bgCount); diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/BinimumProvider.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/BinimumProvider.java index bbc9722d90..63dff2af0f 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/BinimumProvider.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/BinimumProvider.java @@ -15,14 +15,27 @@ import java.io.IOException; import java.net.HttpURLConnection; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import app.morphe.extension.music.patches.lyrics.model.Lyrics; +import app.morphe.extension.music.patches.lyrics.model.LyricsLine; import app.morphe.extension.music.patches.lyrics.model.TrackInfo; -import app.morphe.extension.shared.requests.Requester; +import app.morphe.extension.music.patches.lyrics.model.Word; import app.morphe.extension.music.patches.lyrics.parsers.TTMLParser; +import app.morphe.extension.shared.Logger; +import app.morphe.extension.shared.requests.Requester; public final class BinimumProvider implements LyricsProvider { - private static final String BASE_URL = "https://lyrics-api.binimum.org/"; + private static final String API_URL = "https://lyricsplus.binimum.org/v2/lyrics/get"; + private static final String LEGACY_BASE_URL = "https://lyrics-api.binimum.org/"; + + private static final String BROWSER_USER_AGENT = + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " + + "(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"; @Override public String name() { @@ -32,20 +45,86 @@ public String name() { @Nullable @Override public FetchResult fetch(TrackInfo track) throws Exception { - String lyricsUrl = resolveLyricsUrl(track); - if (lyricsUrl == null) { + if (track.title().isEmpty() || track.artist().isEmpty()) { return null; } + // The v2 API is asked first, for the sources it merges. + try { + Lyrics lyrics = fetchFromApi(track); + if (lyrics != null && !lyrics.lines().isEmpty()) { + return FetchResult.of(lyrics, track); + } + } catch (Exception ex) { + Logger.printDebug(() -> "BiniLyrics v2 fetch failed", ex); + } + + // A title with a video tag is asked again without it. + final String cleanTitle = track.title() + .replace("(Official Video)", "") + .replace("(Official Music Video)", "") + .replace("(Official Audio)", "") + .trim(); + if (!cleanTitle.equals(track.title())) { + TrackInfo cleanTrack = new TrackInfo(cleanTitle, track.artist(), track.album(), track.durationSeconds()); + try { + Lyrics lyrics = fetchFromApi(cleanTrack); + if (lyrics != null && !lyrics.lines().isEmpty()) { + return FetchResult.of(lyrics, cleanTrack); + } + } catch (Exception ex) { + Logger.printDebug(() -> "BiniLyrics v2 fetch failed for the cleaned title", ex); + } + } + + // The legacy API is the last resort. + try { + String lyricsUrl = resolveLyricsUrl(track); + if (lyricsUrl != null) { + HttpURLConnection connection = null; + try { + connection = LyricsRequests.openConnection(lyricsUrl); + if (connection.getResponseCode() == Requester.HTTP_STATUS_CODE_SUCCESS) { + String ttml = Requester.parseString(connection); + return FetchResult.blind(TTMLParser.ttmlToLyrics(ttml, name(), lyricsUrl)); + } + } finally { + if (connection != null) { + connection.disconnect(); + } + } + } + } catch (Exception ex) { + Logger.printDebug(() -> "Legacy Binimum LRCRED fetch failed", ex); + } + + return null; + } + + @Nullable + private Lyrics fetchFromApi(TrackInfo track) throws IOException, JSONException { + StringBuilder url = new StringBuilder(API_URL); + url.append("?title=").append(LyricsRequests.encode(track.title())); + url.append("&artist=").append(LyricsRequests.encode(track.artist())); + if (!track.album().isEmpty()) { + url.append("&album=").append(LyricsRequests.encode(track.album())); + } + if (track.durationSeconds() > 0) { + url.append("&duration=").append(track.durationSeconds()); + } + url.append("&source=apple,lyricsplus,qq"); + HttpURLConnection connection = null; try { - connection = LyricsRequests.openConnection(lyricsUrl); - if (connection.getResponseCode() != Requester.HTTP_STATUS_CODE_SUCCESS) { - LyricsRequests.logFailure(name(), connection); + connection = LyricsRequests.openConnection(url.toString()); + connection.setRequestProperty("User-Agent", BROWSER_USER_AGENT); + connection.setRequestProperty("Accept", "application/json"); + final int code = connection.getResponseCode(); + if (code != Requester.HTTP_STATUS_CODE_SUCCESS) { return null; } - String ttml = Requester.parseString(connection); - return FetchResult.blind(TTMLParser.ttmlToLyrics(ttml, name(), null)); + JSONObject root = Requester.parseJSONObject(connection); + return parseLyricsJson(root, url.toString()); } finally { if (connection != null) { connection.disconnect(); @@ -53,13 +132,254 @@ public FetchResult fetch(TrackInfo track) throws Exception { } } + @Nullable + private Lyrics parseLyricsJson(JSONObject root, String sourceUrl) { + if (root == null) { + return null; + } + JSONArray lyricsArray = root.optJSONArray("lyrics"); + if (lyricsArray == null || lyricsArray.length() == 0) { + return null; + } + + JSONObject metadata = root.optJSONObject("metadata"); + List songwriters = null; + JSONArray songPartsJson = null; + if (metadata != null) { + JSONArray sw = metadata.optJSONArray("songWriters"); + if (sw != null && sw.length() > 0) { + songwriters = new ArrayList<>(); + for (int i = 0; i < sw.length(); i++) { + String writer = sw.optString(i); + if (!writer.isEmpty()) { + songwriters.add(writer); + } + } + } + songPartsJson = metadata.optJSONArray("songParts"); + } + + List lines = new ArrayList<>(); + List romanization = new ArrayList<>(); + Map> translations = new HashMap<>(); + boolean hasAnyWordSync = false; + + for (int i = 0; i < lyricsArray.length(); i++) { + JSONObject item = lyricsArray.optJSONObject(i); + if (item == null) continue; + + long lineTime = item.optLong("time", LyricsLine.NO_TIME); + long lineDuration = item.optLong("duration", 0); + long lineEndTime = (lineTime != LyricsLine.NO_TIME && lineDuration > 0) + ? lineTime + lineDuration : LyricsLine.NO_TIME; + String rawLineText = item.optString("text", ""); + + JSONObject element = item.optJSONObject("element"); + String agentId = null; + String songPart = null; + if (element != null) { + String singer = element.optString("singer"); + if (!singer.isEmpty()) agentId = singer; + int songPartIndex = element.optInt("songPartIndex", -1); + if (songPartsJson != null && songPartIndex >= 0 && songPartIndex < songPartsJson.length()) { + JSONObject sp = songPartsJson.optJSONObject(songPartIndex); + if (sp != null) { + String name = sp.optString("name", null); + if (name != null && !name.isEmpty()) songPart = name; + } + } + } + boolean isDuet = isAgentDuet(agentId); + + JSONArray syllabus = item.optJSONArray("syllabus"); + JSONObject transliteration = item.optJSONObject("transliteration"); + String transText = item.optString("translation", null); + + JSONArray transSyls = transliteration != null ? transliteration.optJSONArray("syllabus") : null; + String transliterationText = transliteration != null ? transliteration.optString("text", null) : null; + + if (syllabus != null && syllabus.length() > 0) { + List mainSyls = new ArrayList<>(); + List bgSyls = new ArrayList<>(); + for (int s = 0; s < syllabus.length(); s++) { + JSONObject syl = syllabus.optJSONObject(s); + if (syl == null) continue; + String roma = null; + if (transSyls != null && s < transSyls.length()) { + JSONObject tSyl = transSyls.optJSONObject(s); + if (tSyl != null) { + roma = tSyl.optString("text", null); + if (roma != null && roma.trim().isEmpty()) roma = null; + } + } + boolean isBg = syl.optBoolean("isBackground", false); + WordCandidate cand = new WordCandidate(syl, roma); + if (isBg) { + bgSyls.add(cand); + } else { + mainSyls.add(cand); + } + } + + LyricsLine mainLine = buildLineFromCandidates( + mainSyls, lineTime, lineEndTime, lineDuration, rawLineText, agentId, isDuet, false, songPart); + if (mainLine != null) { + lines.add(mainLine); + if (!mainLine.words().isEmpty()) hasAnyWordSync = true; + } + + LyricsLine bgLine = buildLineFromCandidates( + bgSyls, lineTime, lineEndTime, lineDuration, null, agentId, isDuet, true, songPart); + if (bgLine != null) { + lines.add(bgLine); + if (!bgLine.words().isEmpty()) hasAnyWordSync = true; + } + } else { + // Line synced only, with no word timing. + String text = rawLineText.trim(); + boolean isBg = false; + if ((text.startsWith("(") && text.endsWith(")")) || (text.startsWith("(") && text.endsWith(")"))) { + isBg = true; + text = text.substring(1, text.length() - 1).trim(); + } + if (!text.isEmpty()) { + lines.add(new LyricsLine(lineTime, lineEndTime, text, List.of(), agentId, isDuet, isBg, songPart)); + } + } + + // The transliteration and translation of the line, if any. + if (transliterationText != null && !transliterationText.trim().isEmpty()) { + romanization.add(new LyricsLine(LyricsLine.NO_TIME, transliterationText.trim())); + } + if (transText != null && !transText.trim().isEmpty()) { + translations.computeIfAbsent("default", k -> new ArrayList<>()) + .add(new LyricsLine(LyricsLine.NO_TIME, transText.trim())); + } + } + + if (lines.isEmpty()) { + return null; + } + + boolean isSynced = lines.get(0).startTimeMs() != LyricsLine.NO_TIME; + return new Lyrics(lines, name(), isSynced, + romanization.isEmpty() ? null : romanization, + translations.isEmpty() ? null : translations, + null, + songwriters, + null, + hasAnyWordSync ? "syllable" : "line", + sourceUrl); + } + + @Nullable + private static LyricsLine buildLineFromCandidates( + List candidates, + long lineStart, + long lineEnd, + long lineDuration, + @Nullable String rawFallbackText, + @Nullable String agentId, + boolean isDuet, + boolean isBackground, + @Nullable String songPart + ) { + if (candidates.isEmpty()) { + return null; + } + + List words = new ArrayList<>(candidates.size()); + StringBuilder textBuf = new StringBuilder(); + + for (WordCandidate candidate : candidates) { + long startTime = candidate.syl.optLong("time"); + long duration = candidate.syl.optLong("duration"); + long endTime = startTime + duration; + String text = candidate.syl.optString("text", ""); + boolean endsWithSpace = text.endsWith(" ") || text.endsWith("\t"); + + String cleanWord = isBackground ? text.replaceAll("[()()]", "").trim() : text.trim(); + String cleanRoma = candidate.romaji; + if (isBackground && cleanRoma != null) { + cleanRoma = cleanRoma.replaceAll("[()()]", "").trim(); + } + + if (!cleanWord.isEmpty()) { + words.add(new Word(startTime, endTime, cleanWord, cleanRoma, endsWithSpace)); + textBuf.append(cleanWord); + if (endsWithSpace) { + textBuf.append(' '); + } + } + } + + String fullText = textBuf.toString().trim(); + + // Words with no spaces between them in a spaced script are joined with spaces. + if (fullText.indexOf(' ') < 0 && words.size() > 1 && !TTMLParser.isContinuousCjk(words.get(0).text())) { + StringBuilder reconstructed = new StringBuilder(); + for (int i = 0; i < words.size(); i++) { + Word word = words.get(i); + if (i > 0) { + reconstructed.append(' '); + } + reconstructed.append(word.text()); + boolean hasSpace = (i < words.size() - 1); + words.set(i, new Word(word.startMs(), word.endMs(), word.text(), word.romaji(), hasSpace)); + } + fullText = reconstructed.toString(); + } + + // The last word ends the line, so it has no space after it. + if (!words.isEmpty()) { + Word lastWord = words.get(words.size() - 1); + if (lastWord.endsWithSpace()) { + words.set(words.size() - 1, new Word(lastWord.startMs(), lastWord.endMs(), lastWord.text(), lastWord.romaji(), false)); + } + } + + if (fullText.isEmpty() && rawFallbackText != null && !rawFallbackText.isEmpty()) { + fullText = rawFallbackText.trim(); + } + + if (fullText.isEmpty() && words.isEmpty()) { + return null; + } + + long start = words.isEmpty() ? lineStart : words.get(0).startMs(); + long end = words.isEmpty() ? lineEnd : words.get(words.size() - 1).endMs(); + if (end < start && lineDuration > 0) { + end = start + lineDuration; + } + + return new LyricsLine(start, end, fullText, words, agentId, isDuet, isBackground, songPart); + } + + private static boolean isAgentDuet(@Nullable String agentId) { + if (agentId == null || !agentId.matches("v\\d{1,6}")) { + return false; + } + int num = Integer.parseInt(agentId.substring(1)); + return num > 0 && num % 2 == 0; + } + + private static class WordCandidate { + final JSONObject syl; + final @Nullable String romaji; + + WordCandidate(JSONObject syl, @Nullable String romaji) { + this.syl = syl; + this.romaji = romaji; + } + } + @Nullable private String resolveLyricsUrl(TrackInfo track) throws IOException, JSONException { if (track.title().isEmpty() || track.artist().isEmpty()) { return null; } - StringBuilder url = new StringBuilder(BASE_URL); + StringBuilder url = new StringBuilder(LEGACY_BASE_URL); url.append("?track=").append(LyricsRequests.encode(track.title())); url.append("&artist=").append(LyricsRequests.encode(track.artist())); if (!track.album().isEmpty()) { diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/DeezerProvider.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/DeezerProvider.java index ae48132959..9295cdbcc5 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/DeezerProvider.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/DeezerProvider.java @@ -24,6 +24,7 @@ import app.morphe.extension.music.patches.lyrics.model.Lyrics; import app.morphe.extension.music.patches.lyrics.model.LyricsLine; import app.morphe.extension.music.patches.lyrics.model.TrackInfo; +import app.morphe.extension.music.patches.lyrics.parsers.LRCParser; import app.morphe.extension.music.settings.Settings; import app.morphe.extension.shared.Logger; import app.morphe.extension.shared.requests.Requester; @@ -354,6 +355,8 @@ private Lyrics parseSyncedLyrics(JSONArray syncedLines, long trackId, String raw if (lines.isEmpty()) return null; + LRCParser.fillEstimatedEnds(lines); + String sourceUrl = "https://www.deezer.com/track/" + trackId; return new Lyrics(lines, name(), true, null, null, null, creditLines, rawFormat, "dzr.json", sourceUrl); diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/LrcRedProvider.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/LrcRedProvider.java new file mode 100644 index 0000000000..1f25f167f6 --- /dev/null +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/LrcRedProvider.java @@ -0,0 +1,410 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/3877 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.extension.music.patches.lyrics.requests; + +import androidx.annotation.Nullable; + +import org.json.JSONArray; +import org.json.JSONObject; + +import java.net.HttpURLConnection; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.regex.Pattern; + +import app.morphe.extension.music.patches.lyrics.model.Lyrics; +import app.morphe.extension.music.patches.lyrics.model.LyricsLine; +import app.morphe.extension.music.patches.lyrics.model.TrackInfo; +import app.morphe.extension.music.patches.lyrics.parsers.LRCParser; +import app.morphe.extension.music.patches.lyrics.parsers.TTMLParser; +import app.morphe.extension.shared.Logger; +import app.morphe.extension.shared.requests.Requester; + +/** + * Lyrics from lrc.red, as word synced TTML or as LRC. + */ +public final class LrcRedProvider implements LyricsProvider { + + private static final String BASE_URL = "https://lrc.red/api/v1"; + + private static final String[] MODIFIER_KEYWORDS = { + "slowed", "reverb", "remix", "mixed", "dj mix", "mashup", + "cover", "tribute", "karaoke", "instrumental", "acoustic", + "sped up", "speed up", "live", "edit", "versión", "version", + "bpm", "guitar", "piano", "lofi", "lo-fi", "nightcore", + "club mix", "extended mix", "radio edit" + }; + + private static final Pattern BRACKETED_TAGS = Pattern.compile( + "[(x\\[](?:feat\\.?|ft\\.?|with|prod\\.?|bonus track|remaster(?:ed)?)[^)\\]]*[)\\]]", + Pattern.CASE_INSENSITIVE); + private static final Pattern TRAILING_FEAT = Pattern.compile( + "\\b(?:feat\\.?|ft\\.?|with)\\s+.*$", + Pattern.CASE_INSENSITIVE); + private static final Pattern VIDEO_TAGS = Pattern.compile( + "[(x\\[](?:official(?:\\s+(?:music\\s+)?video|\\s+audio)?|music video|video|audio|lyrics?|visualizer|album version|explicit)[^)\\]]*[)\\]]", + Pattern.CASE_INSENSITIVE); + + @Override + public String name() { + return "LrcRed"; + } + + @Override + public boolean hasCandidates() { + return true; + } + + @Nullable + @Override + public FetchResult fetch(TrackInfo track) throws Exception { + if (track.title().isEmpty()) { + return null; + } + + List candidates = queryCandidates(track); + if (candidates.isEmpty()) { + return null; + } + + for (JSONObject candidate : candidates) { + String lyricsUrl = candidate.optString("lyricsUrl", ""); + if (lyricsUrl.isEmpty()) { + continue; + } + + Lyrics lyrics = downloadLyrics(lyricsUrl); + if (lyrics != null && !lyrics.isEmpty()) { + return FetchResult.of( + lyrics, + candidate.optString("track_name", ""), + candidate.optString("artist_name", ""), + candidate.optLong("duration", 0), + track + ); + } + } + + return null; + } + + @Override + public List fetchCandidates(TrackInfo track) throws Exception { + List candidates = queryCandidates(track); + if (candidates.isEmpty()) { + return Collections.emptyList(); + } + + List scored = new ArrayList<>(); + int count = 0; + for (JSONObject candidate : candidates) { + if (count >= LyricsRequests.MAX_CANDIDATES) { + break; + } + + String lyricsUrl = candidate.optString("lyricsUrl", ""); + if (lyricsUrl.isEmpty()) { + continue; + } + + Lyrics lyrics = downloadLyrics(lyricsUrl); + if (lyrics != null && !lyrics.isEmpty()) { + int score = LyricsRequests.scoreLyricsCandidate( + candidate.optString("track_name", ""), + candidate.optString("artist_name", ""), + candidate.optInt("duration", 0), + lyrics, + track + ); + scored.add(new Lyrics.ScoredLyrics(score, lyrics)); + count++; + } + } + + return Lyrics.sortScoredByScore(scored); + } + + private List queryCandidates(TrackInfo track) { + String mainQuery = (track.title() + " " + track.artist()).trim(); + List results = searchApi(mainQuery, track); + if (!results.isEmpty()) { + return results; + } + + String cleanTitle = cleanBaseTitle(track.title()); + if (!cleanTitle.isEmpty() && !cleanTitle.equalsIgnoreCase(normalize(track.title()))) { + String cleanQuery = (cleanTitle + " " + track.artist()).trim(); + results = searchApi(cleanQuery, track); + if (!results.isEmpty()) { + return results; + } + } + + if (!track.title().isEmpty() && !track.artist().isEmpty()) { + results = searchApi(track.title().trim(), track); + if (!results.isEmpty()) { + return results; + } + } + + return Collections.emptyList(); + } + + private List searchApi(String query, TrackInfo track) { + if (query.isEmpty()) { + return Collections.emptyList(); + } + + HttpURLConnection connection = null; + try { + String url = BASE_URL + "?q=" + LyricsRequests.encode(query); + connection = LyricsRequests.openConnection(url); + if (connection.getResponseCode() != Requester.HTTP_STATUS_CODE_SUCCESS) { + LyricsRequests.logFailure(name(), connection); + return Collections.emptyList(); + } + + JSONObject json = Requester.parseJSONObject(connection); + JSONArray array = json.optJSONArray("results"); + if (array == null || array.length() == 0) { + return Collections.emptyList(); + } + + List scoredList = new ArrayList<>(); + for (int i = 0; i < array.length(); i++) { + JSONObject item = array.optJSONObject(i); + if (item == null) { + continue; + } + int score = scoreCandidate(item, track); + if (score > 0) { + scoredList.add(new ScoredCandidate(item, score)); + } + } + + scoredList.sort((a, b) -> Integer.compare(b.score, a.score)); + + List sorted = new ArrayList<>(scoredList.size()); + for (ScoredCandidate sc : scoredList) { + sorted.add(sc.candidate); + } + return sorted; + } catch (Exception ex) { + Logger.printDebug(() -> "LrcRed search failed for query: " + query, ex); + return Collections.emptyList(); + } finally { + if (connection != null) { + connection.disconnect(); + } + } + } + + private static int scoreCandidate(JSONObject item, TrackInfo track) { + int targetDuration = track.durationSeconds(); + int itemDuration = item.optInt("duration", 0); + + String itemTrackName = item.optString("track_name", ""); + String itemArtistName = item.optString("artist_name", ""); + String itemAlbumName = item.optString("album_name", ""); + String itemTimingType = item.optString("timing_type", ""); + + String targetFullTitle = normalize(track.title()); + String targetBaseTitle = cleanBaseTitle(track.title()); + String targetArtist = normalize(track.artist()); + String targetAlbum = normalize(track.album()); + + String itemFullTitle = normalize(itemTrackName); + String itemBaseTitle = cleanBaseTitle(itemTrackName); + String itemArtist = normalize(itemArtistName); + String itemAlbum = normalize(itemAlbumName); + + int score = 0; + + // Title. + if (!targetFullTitle.isEmpty() && targetFullTitle.equals(itemFullTitle)) { + score += 50; + } else if (!targetBaseTitle.isEmpty() && !itemBaseTitle.isEmpty()) { + if (targetBaseTitle.equals(itemBaseTitle)) { + score += 35; + } else if (itemBaseTitle.contains(targetBaseTitle) || targetBaseTitle.contains(itemBaseTitle)) { + score += 15; + } else { + score -= 40; + } + } + + // Artist. + if (!targetArtist.isEmpty() && !itemArtist.isEmpty()) { + if (targetArtist.equals(itemArtist)) { + score += 30; + } else if (itemArtist.contains(targetArtist) || targetArtist.contains(itemArtist)) { + score += 15; + } else { + List targetArtists = extractArtists(track.artist()); + List itemArtists = extractArtists(itemArtistName); + boolean hasOverlap = false; + for (String ta : targetArtists) { + for (String ia : itemArtists) { + if (ia.equals(ta) || ia.contains(ta) || ta.contains(ia)) { + hasOverlap = true; + break; + } + } + if (hasOverlap) { + break; + } + } + if (hasOverlap) { + score += 10; + } else { + score -= 50; + } + } + } + + // Album. + if (!targetAlbum.isEmpty() && !itemAlbum.isEmpty()) { + if (targetAlbum.equals(itemAlbum)) { + score += 35; + } else if (itemAlbum.contains(targetAlbum) || targetAlbum.contains(itemAlbum)) { + score += 20; + } + + boolean targetIsSingle = targetAlbum.contains("single"); + boolean itemIsSingle = itemAlbum.contains("single"); + if (!targetIsSingle && itemIsSingle) { + score -= 15; + } else if (targetIsSingle && itemIsSingle) { + score += 10; + } + } + + // Duration. + if (targetDuration > 0 && itemDuration > 0) { + int diff = Math.abs(itemDuration - targetDuration); + if (diff == 0) { + score += 25; + } else if (diff <= 1) { + score += 20; + } else if (diff <= 2) { + score += 15; + } else if (diff <= 5) { + score += 5; + } else if (diff > 15) { + score -= 30; + } + } + + // Word sync is preferred over line sync. + if ("word".equalsIgnoreCase(itemTimingType)) { + score += 15; + } else if ("line".equalsIgnoreCase(itemTimingType)) { + score += 8; + } else { + score -= 10; + } + + // A remix, cover or other variant the track is not. + String trackLower = (track.title() + " " + track.album()).toLowerCase(Locale.ROOT); + String itemLower = (itemTrackName + " " + itemAlbumName).toLowerCase(Locale.ROOT); + for (String kw : MODIFIER_KEYWORDS) { + if (itemLower.contains(kw) && !trackLower.contains(kw)) { + score -= 30; + } + } + + return score; + } + + @Nullable + private Lyrics downloadLyrics(String lyricsUrl) { + if (lyricsUrl == null || lyricsUrl.trim().isEmpty()) { + return null; + } + + HttpURLConnection connection = null; + try { + connection = LyricsRequests.openConnection(lyricsUrl); + final int code = connection.getResponseCode(); + if (code != Requester.HTTP_STATUS_CODE_SUCCESS) { + LyricsRequests.logFailure(name(), connection); + return null; + } + + String content = Requester.parseString(connection); + if (content == null || content.trim().isEmpty()) { + return null; + } + + String trimmed = content.trim(); + if (trimmed.startsWith("<") || lyricsUrl.endsWith(".ttml")) { + return TTMLParser.ttmlToLyrics(content, name(), lyricsUrl); + } else { + LRCParser.LrcParseResult lrc = LRCParser.parseSyncedWithCreditLines(content); + if (!lrc.lines.isEmpty()) { + return new Lyrics(lrc.lines, name(), true, null, null, null, + lrc.creditLines.isEmpty() ? null : lrc.creditLines, + content, "lrc", lyricsUrl); + } + List plain = LRCParser.parsePlain(content); + if (!plain.isEmpty()) { + return new Lyrics(plain, name(), false, null, null, null, null, + content, "lrc", lyricsUrl); + } + } + } catch (Exception ex) { + Logger.printDebug(() -> "LrcRed download failed: " + lyricsUrl, ex); + } finally { + if (connection != null) { + connection.disconnect(); + } + } + + return null; + } + + private static String normalize(String str) { + if (str == null) { + return ""; + } + return str.toLowerCase(Locale.ROOT) + .replaceAll("[^\\p{L}\\p{N}\\s]", " ") + .replaceAll("\\s+", " ") + .trim(); + } + + private static String cleanBaseTitle(String str) { + if (str == null) { + return ""; + } + String s = BRACKETED_TAGS.matcher(str).replaceAll(""); + s = TRAILING_FEAT.matcher(s).replaceAll(""); + s = VIDEO_TAGS.matcher(s).replaceAll(""); + return normalize(s); + } + + private static List extractArtists(String str) { + if (str == null || str.isEmpty()) { + return Collections.emptyList(); + } + String[] parts = str.split("(?i)[,&/]|\\b(?:feat\\.?|ft\\.?|with|x)\\b"); + List result = new ArrayList<>(parts.length); + for (String p : parts) { + String norm = normalize(p); + if (!norm.isEmpty()) { + result.add(norm); + } + } + return result; + } + + private record ScoredCandidate(JSONObject candidate, int score) { + } +} diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/MusixmatchProvider.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/MusixmatchProvider.java index 558b0464d4..0789808276 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/MusixmatchProvider.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/MusixmatchProvider.java @@ -43,8 +43,8 @@ public final class MusixmatchProvider extends ScoredSongProvider { private static final String USER_AGENT = "Dalvik/2.1.0 (Linux; U; Android 17)"; private static final String COOKIE = "AWSELB=0; AWSELBCORS=0"; - private static final long MIN_WORD_MS = 40; - private static final long BRIDGING_THRESHOLD_MS = 400; + private static final long MIN_WORD_MS = 100; + private static final long BRIDGING_THRESHOLD_MS = 2500; private static final long REQUEST_THROTTLE_MS = 500; private static final AtomicLong lastRequestTime = new AtomicLong(0); @@ -418,13 +418,14 @@ private Lyrics parseRichsync(String body, @Nullable String sourceUrl) final JSONArray lArr = line.optJSONArray("l"); if (lArr != null && lArr.length() > 0) { words = new ArrayList<>(); + int xCursor = 0; for (int j = 0; j < lArr.length(); j++) { JSONObject w = lArr.optJSONObject(j); if (w == null) { continue; } - final String chunk = LyricsRequests.optString(w, "c"); - if (chunk == null || chunk.isEmpty()) { + final String rawChunk = LyricsRequests.optString(w, "c"); + if (rawChunk == null || rawChunk.isEmpty()) { continue; } final double offset = w.optDouble("o", 0); @@ -437,10 +438,14 @@ private Lyrics parseRichsync(String body, @Nullable String sourceUrl) if (nextW != null) { final double nextOffset = nextW.optDouble("o", offset); final long nextWs = (long) ((lineTs + nextOffset) * 1000); - if (nextWs - ws <= BRIDGING_THRESHOLD_MS) { - we = nextWs; + if (nextWs > ws) { + if (nextWs - ws <= BRIDGING_THRESHOLD_MS) { + we = nextWs; + } else { + we = ws + BRIDGING_THRESHOLD_MS; + } } else { - we = ws; + we = ws + MIN_WORD_MS; } } else { we = lineEndMs; @@ -449,11 +454,43 @@ private Lyrics parseRichsync(String body, @Nullable String sourceUrl) we = lineEndMs; } if (we <= ws) { - we = ws + MIN_WORD_MS; + we = ws + 150L; + } else if (we - ws > BRIDGING_THRESHOLD_MS) { + we = ws + BRIDGING_THRESHOLD_MS; } else if (we - ws < MIN_WORD_MS) { we = ws + MIN_WORD_MS; } - words.add(new Word(ws, we, chunk)); + if (nextIdx < lArr.length()) { + JSONObject nextW = lArr.optJSONObject(nextIdx); + if (nextW != null) { + final double nextOffset = nextW.optDouble("o", offset); + final long nextWs = (long) ((lineTs + nextOffset) * 1000); + if (nextWs > ws && we > nextWs) { + we = nextWs; + } + } + } + + boolean endsWithSpace = rawChunk.endsWith(" ") || rawChunk.endsWith("\t"); + String chunk = endsWithSpace ? rawChunk.stripTrailing() : rawChunk; + + if (x != null && !x.isEmpty()) { + while (xCursor < x.length() && Character.isWhitespace(x.charAt(xCursor))) { + xCursor++; + } + if (xCursor < x.length()) { + int matchLen = chunk.length(); + if (xCursor + matchLen <= x.length() + && x.regionMatches(true, xCursor, chunk, 0, matchLen)) { + chunk = x.substring(xCursor, xCursor + matchLen); + int afterMatch = xCursor + matchLen; + endsWithSpace = (afterMatch < x.length() && Character.isWhitespace(x.charAt(afterMatch))); + xCursor = afterMatch; + } + } + } + + words.add(new Word(ws, we, chunk, null, endsWithSpace)); } } @@ -630,13 +667,11 @@ public static boolean validateToken(String token) { private static String joinWords(List words) { final StringBuilder sb = new StringBuilder(); for (int i = 0; i < words.size(); i++) { - final String t = words.get(i).text(); - //noinspection SizeReplaceableByIsEmpty - if (i > 0 && !t.isEmpty() && !t.startsWith(" ") - && sb.length() > 0 && sb.charAt(sb.length() - 1) != ' ') { + final Word w = words.get(i); + sb.append(w.text()); + if (w.endsWithSpace() && i < words.size() - 1) { sb.append(' '); } - sb.append(t); } return sb.toString(); } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/PetitLyricsProvider.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/PetitLyricsProvider.java index 682e9b673e..78dae9a17e 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/PetitLyricsProvider.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/PetitLyricsProvider.java @@ -27,9 +27,10 @@ import app.morphe.extension.music.patches.lyrics.model.LyricsLine; import app.morphe.extension.music.patches.lyrics.model.TrackInfo; import app.morphe.extension.music.patches.lyrics.model.Word; +import app.morphe.extension.music.patches.lyrics.parsers.LRCParser; +import app.morphe.extension.music.patches.lyrics.romanizers.ScriptDetector; import app.morphe.extension.shared.Logger; import app.morphe.extension.shared.requests.Requester; -import app.morphe.extension.music.patches.lyrics.parsers.LRCParser; public final class PetitLyricsProvider implements LyricsProvider { @@ -528,13 +529,101 @@ private static boolean isValidUtf8(byte[] raw) { } } + private record RawToken(String text, long startMs, long endMs) {} + + private static class RawWsyLine { + final String text; + final List words; + + RawWsyLine(String text, List words) { + this.text = text; + this.words = words; + } + } + + private static boolean isWhitespaceToken(@Nullable String s) { + if (s == null || s.isEmpty()) return true; + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (!Character.isWhitespace(c) && c != 0x2005 && c != 0x3000 && c != 0xA0) { + return false; + } + } + return true; + } + + private static boolean hasOpen(@Nullable String s) { + return s != null && (s.contains("(") || s.contains("(") || s.contains("[")); + } + + private static boolean hasClose(@Nullable String s) { + return s != null && (s.contains(")") || s.contains(")") || s.contains("]")); + } + + private static int indexOfAny(@Nullable String s, String chars) { + if (s == null) return -1; + for (int i = 0; i < s.length(); i++) { + if (chars.indexOf(s.charAt(i)) >= 0) return i; + } + return -1; + } + + private static int lastIndexOfAny(@Nullable String s, String chars) { + if (s == null) return -1; + for (int i = s.length() - 1; i >= 0; i--) { + if (chars.indexOf(s.charAt(i)) >= 0) return i; + } + return -1; + } + + private static void addSanitizedLine(List lines, String text, List words, boolean isBg) { + if (text.isEmpty() || words.isEmpty()) { + return; + } + if (isBg) { + while ((text.startsWith("(") && text.endsWith(")")) + || (text.startsWith("(") && text.endsWith(")")) + || (text.startsWith("[") && text.endsWith("]"))) { + text = text.substring(1, text.length() - 1).trim(); + } + if (!words.isEmpty()) { + Word first = words.get(0); + String ft = first.text().trim(); + if ("(".equals(ft) || "(".equals(ft) || "[".equals(ft)) { + words.remove(0); + } else if (ft.startsWith("(") || ft.startsWith("(") || ft.startsWith("[")) { + words.set(0, new Word(first.startMs(), first.endMs(), ft.substring(1).trim(), first.romaji(), first.endsWithSpace())); + } + } + if (!words.isEmpty()) { + int lastIdx = words.size() - 1; + Word last = words.get(lastIdx); + String lt = last.text().trim(); + if (")".equals(lt) || ")".equals(lt) || "]".equals(lt)) { + words.remove(lastIdx); + } else if (lt.endsWith(")") || lt.endsWith(")") || lt.endsWith("]")) { + words.set(lastIdx, new Word(last.startMs(), last.endMs(), lt.substring(0, lt.length() - 1).trim(), last.romaji(), last.endsWithSpace())); + } + } + } + if (text.isEmpty() || words.isEmpty()) { + return; + } + long start = words.get(0).startMs(); + long endMs = words.get(words.size() - 1).endMs(); + if (endMs <= start) { + endMs = start + Math.max(2500L, text.length() * 200L); + } + lines.add(new LyricsLine(start, endMs, text, new ArrayList<>(words), null, false, isBg, null)); + } + private static List parseWsy(byte[] payload) { try { XmlPullParser parser = XmlPullParserFactory.newInstance().newPullParser(); parser.setInput(new ByteArrayInputStream(payload), "UTF-8"); - List lines = new ArrayList<>(); - List words = new ArrayList<>(); + List rawLines = new ArrayList<>(); + List rawTokens = new ArrayList<>(); StringBuilder lineText = new StringBuilder(); StringBuilder wordText = new StringBuilder(); long wordStart = 0; @@ -550,7 +639,7 @@ private static List parseWsy(byte[] payload) { tag = parser.getName(); if ("line".equals(tag)) { inLine = true; - words.clear(); + rawTokens.clear(); lineText.setLength(0); } else if (inLine && "word".equals(tag)) { inWord = true; @@ -582,27 +671,65 @@ private static List parseWsy(byte[] payload) { if (!text.isEmpty()) { long start = Math.max(0, wordStart); long endMs = Math.max(start, wordEnd); - words.add(new Word(start, endMs, text)); + rawTokens.add(new RawToken(text, start, endMs)); } } else if (inLine && "line".equals(end)) { inLine = false; - if (words.isEmpty()) { + if (rawTokens.isEmpty()) { break; } - String text = lineText.toString().trim(); - if (text.isEmpty()) { - StringBuilder joined = new StringBuilder(); - for (Word w : words) { - joined.append(w.text()); + String lineStr = lineText.toString() + .replace('\u2005', ' ') + .replace('\u3000', ' ') + .replace('\u00A0', ' ') + .trim(); + + List mergedWords = new ArrayList<>(); + for (RawToken tok : rawTokens) { + String s = tok.text(); + if (isWhitespaceToken(s)) { + if (!mergedWords.isEmpty()) { + Word prev = mergedWords.get(mergedWords.size() - 1); + mergedWords.set(mergedWords.size() - 1, new Word( + prev.startMs(), prev.endMs(), prev.text(), prev.romaji(), true)); + } + continue; + } + + boolean canMerge = false; + if (!mergedWords.isEmpty()) { + Word prev = mergedWords.get(mergedWords.size() - 1); + if (!prev.endsWithSpace() && !ScriptDetector.containsCjk(s) && !ScriptDetector.containsCjk(prev.text())) { + canMerge = true; + } + } + + if (canMerge) { + Word prev = mergedWords.get(mergedWords.size() - 1); + mergedWords.set(mergedWords.size() - 1, new Word( + prev.startMs(), Math.max(prev.endMs(), tok.endMs()), + prev.text() + s, null, false)); + } else { + mergedWords.add(new Word(tok.startMs(), tok.endMs(), s, null, false)); } - text = joined.toString().trim(); } - if (text.isEmpty()) { + + if (mergedWords.isEmpty()) { break; } - long start = words.get(0).startMs(); - long endMs = words.get(words.size() - 1).endMs(); - lines.add(new LyricsLine(start, endMs, text, new ArrayList<>(words))); + + if (lineStr.isEmpty()) { + StringBuilder joined = new StringBuilder(); + for (Word w : mergedWords) { + joined.append(w.text()); + if (w.endsWithSpace()) { + joined.append(' '); + } + } + lineStr = joined.toString().trim(); + } + + rawLines.add(new RawWsyLine(lineStr, mergedWords)); } tag = null; } @@ -610,7 +737,112 @@ private static List parseWsy(byte[] payload) { } event = parser.next(); } + + List lines = new ArrayList<>(); + int i = 0; + final int rawCount = rawLines.size(); + while (i < rawCount) { + RawWsyLine cur = rawLines.get(i); + List words = new ArrayList<>(cur.words); + String text = cur.text; + if (text.isEmpty() || words.isEmpty()) { + i++; + continue; + } + + // A bracket opened on one line and closed on the next is one background vocal. + if (hasOpen(text) && !hasClose(text) && i + 1 < rawCount) { + RawWsyLine nxt = rawLines.get(i + 1); + List nxtWords = new ArrayList<>(nxt.words); + if (hasClose(nxt.text) && !hasOpen(nxt.text)) { + int pPos = indexOfAny(text, "((["); + if (pPos == 0) { + List combWords = new ArrayList<>(words); + combWords.addAll(nxtWords); + String combText = (text + " " + nxt.text).trim(); + addSanitizedLine(lines, combText, combWords, true); + i += 2; + continue; + } else { + int splitWi = -1; + for (int wi = 0; wi < words.size(); wi++) { + if (hasOpen(words.get(wi).text())) { + splitWi = wi; + break; + } + } + if (splitWi > 0) { + List mainWords = new ArrayList<>(words.subList(0, splitWi)); + Word lastMain = mainWords.get(mainWords.size() - 1); + mainWords.set(mainWords.size() - 1, new Word( + lastMain.startMs(), lastMain.endMs(), lastMain.text(), lastMain.romaji(), false)); + List secWords = new ArrayList<>(words.subList(splitWi, words.size())); + secWords.addAll(nxtWords); + String mainText = (pPos > 0 ? text.substring(0, pPos) : text).trim(); + String secText = ((pPos >= 0 ? text.substring(pPos) : "") + " " + nxt.text).trim(); + addSanitizedLine(lines, mainText, mainWords, false); + addSanitizedLine(lines, secText, secWords, true); + i += 2; + continue; + } + } + } + } + + // A bracketed part at the start or the end of a line is a second vocal. + int pOpen = indexOfAny(text, "((["); + int pClose = lastIndexOfAny(text, "))]"); + if (pOpen > 0 && pClose > pOpen && pClose >= text.length() - 2) { + int splitWi = -1; + for (int wi = 0; wi < words.size(); wi++) { + if (hasOpen(words.get(wi).text())) { + splitWi = wi; + break; + } + } + if (splitWi > 0) { + List mainWords = new ArrayList<>(words.subList(0, splitWi)); + Word lastMain = mainWords.get(mainWords.size() - 1); + mainWords.set(mainWords.size() - 1, new Word( + lastMain.startMs(), lastMain.endMs(), lastMain.text(), lastMain.romaji(), false)); + List secWords = new ArrayList<>(words.subList(splitWi, words.size())); + String mainText = text.substring(0, pOpen).trim(); + String secText = text.substring(pOpen).trim(); + addSanitizedLine(lines, mainText, mainWords, false); + addSanitizedLine(lines, secText, secWords, true); + i++; + continue; + } + } else if (pOpen == 0 && 0 < pClose && pClose < text.length() - 2) { + int splitWi = -1; + for (int wi = 0; wi < words.size(); wi++) { + if (hasClose(words.get(wi).text())) { + splitWi = wi + 1; + break; + } + } + if (splitWi > 0 && splitWi < words.size()) { + List secWords = new ArrayList<>(words.subList(0, splitWi)); + Word lastSec = secWords.get(secWords.size() - 1); + secWords.set(secWords.size() - 1, new Word( + lastSec.startMs(), lastSec.endMs(), lastSec.text(), lastSec.romaji(), false)); + List mainWords = new ArrayList<>(words.subList(splitWi, words.size())); + String secText = text.substring(0, pClose + 1).trim(); + String mainText = text.substring(pClose + 1).trim(); + addSanitizedLine(lines, secText, secWords, true); + addSanitizedLine(lines, mainText, mainWords, false); + i++; + continue; + } + } + + boolean isBg = LRCParser.hasOuterParentheses(text); + addSanitizedLine(lines, text, words, isBg); + i++; + } + lines.sort(Comparator.comparingLong(LyricsLine::startTimeMs)); + LRCParser.markEchoesAsBackground(lines); return lines; } catch (Exception ex) { Logger.printDebug(() -> "PetitLyrics WSY parse failed", ex); @@ -659,15 +891,16 @@ private static List decodeLsyTimings(byte[] raw) { wraps++; } prev = cs; - out.add(cs + (long) wraps * LSY_WRAP_CS); + out.add((long) (cs + wraps * LSY_WRAP_CS)); } return out; } - private static int deriveLsyKey(int k, boolean switchFlag) { + private static int deriveLsyKey(int protectionId, boolean switchFlag) { if (!switchFlag) { - return k; + return protectionId; } + int k = protectionId; return (k & 0x0003) | ((k & 0x000c) << 2) | ((k & 0x0030) >> 2) @@ -695,7 +928,7 @@ private static List zipLineSync(List timingsCs, String text) { } List lines = new ArrayList<>(count); for (int i = 0; i < count; i++) { - String row = rows[i].trim(); + String row = rows[i].replace('\u2005', ' ').replace('\u3000', ' ').replace('\u00A0', ' ').trim(); if (row.isEmpty()) { continue; } @@ -704,18 +937,23 @@ private static List zipLineSync(List timingsCs, String text) { if (endMs <= startMs) { endMs = startMs + 3000; } - lines.add(new LyricsLine(startMs, endMs, row, List.of())); + boolean isBg = LRCParser.hasOuterParentheses(row); + String lineText = row; + if (isBg && ((row.startsWith("(") && row.endsWith(")")) + || (row.startsWith("(") && row.endsWith(")")) + || (row.startsWith("[") && row.endsWith("]")))) { + lineText = row.substring(1, row.length() - 1).trim(); + } + lines.add(new LyricsLine(startMs, endMs, lineText, List.of(), null, false, isBg, null)); } + LRCParser.markEchoesAsBackground(lines); return lines; } private static String buildLrc(List lines) { StringBuilder sb = new StringBuilder(50 * lines.size()); for (LyricsLine line : lines) { - sb.append('[') - .append(LRCParser.formatCentiseconds(Math.max(0, line.startTimeMs()))) - .append(']') - .append(line.text()) + sb.append(LRCParser.formatLine(line)) .append('\n'); } return sb.toString(); diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/SpotifyProvider.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/SpotifyProvider.java index e410d7f321..4e0754b053 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/SpotifyProvider.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/SpotifyProvider.java @@ -30,6 +30,7 @@ import app.morphe.extension.music.patches.lyrics.model.LyricsLine; import app.morphe.extension.music.patches.lyrics.model.TrackInfo; import app.morphe.extension.music.patches.lyrics.model.Word; +import app.morphe.extension.music.patches.lyrics.parsers.LRCParser; import app.morphe.extension.music.settings.Settings; import app.morphe.extension.shared.Logger; import app.morphe.extension.shared.requests.Requester; @@ -387,6 +388,9 @@ private Lyrics parseSyncedLines(JSONArray linesArr, String providerName, String if (lines.isEmpty()) { return null; } + + LRCParser.fillEstimatedEnds(lines); + return new Lyrics(lines, providerName, true, null, null, null, null, rawJson, "sp.json", sourceUrl); } @@ -421,6 +425,9 @@ private Lyrics parseSyllableLines(JSONArray linesArr, String providerName, if (lines.isEmpty()) { return null; } + + LRCParser.fillEstimatedEnds(lines); + return new Lyrics(lines, providerName, true, null, null, null, null, rawJson, "sp.json", sourceUrl); } @@ -437,13 +444,21 @@ private static List parseSyllables(JSONArray syllablesArr, String lineText final long startMs = parseStartTimeMs(syllable); final long endMs = syllable.optLong("endTimeMs", startMs); final int numChars = syllable.optInt("numChars", 0); - if (numChars <= 0 || charOffset >= lineText.length()) { + if (numChars <= 0) { continue; } + // Spaces before the syllable are not part of it. + while (charOffset < lineText.length() && Character.isWhitespace(lineText.charAt(charOffset))) { + charOffset++; + } + if (charOffset >= lineText.length()) { + break; + } + final int end = Math.min(charOffset + numChars, lineText.length()); final String wordText = lineText.substring(charOffset, end); - final boolean spaceAfter = end < lineText.length(); + final boolean spaceAfter = (end < lineText.length() && Character.isWhitespace(lineText.charAt(end))); words.add(new Word(startMs, endMs, wordText, null, spaceAfter)); charOffset = end; } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/ScriptDetector.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/ScriptDetector.java index 470a1bce28..75a2d46f88 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/ScriptDetector.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/ScriptDetector.java @@ -116,6 +116,56 @@ private static Script classify(int[] found) { return greek > 0 ? Script.GREEK : Script.OTHER; } + public static boolean contains(@Nullable CharSequence text, Character.UnicodeScript... scripts) { + if (text == null) return false; + for (int i = 0; i < text.length(); ) { + final int cp = Character.codePointAt(text, i); + i += Character.charCount(cp); + final Character.UnicodeScript script = Character.UnicodeScript.of(cp); + for (Character.UnicodeScript wanted : scripts) { + if (script == wanted) return true; + } + } + return false; + } + + public static boolean containsKana(@Nullable CharSequence text) { + return contains(text, Character.UnicodeScript.HIRAGANA, Character.UnicodeScript.KATAKANA); + } + + public static boolean containsCjk(@Nullable CharSequence text) { + return contains(text, Character.UnicodeScript.HAN, Character.UnicodeScript.HIRAGANA, + Character.UnicodeScript.KATAKANA, Character.UnicodeScript.HANGUL); + } + + public static boolean containsRtl(@Nullable CharSequence text) { + return contains(text, Character.UnicodeScript.ARABIC, Character.UnicodeScript.HEBREW); + } + + /** + * Checks if a line predominantly consists of RTL characters (Arabic, Hebrew, Persian, etc.). + * Requires RTL characters to meet or exceed LTR characters, avoiding false positives from stray symbols. + */ + public static boolean isLineRtl(@Nullable CharSequence text) { + if (text == null || text.length() == 0) { + return false; + } + int rtlCount = 0; + int ltrCount = 0; + final int len = text.length(); + for (int i = 0; i < len; ) { + int cp = Character.codePointAt(text, i); + i += Character.charCount(cp); + byte dir = Character.getDirectionality(cp); + if (dir == Character.DIRECTIONALITY_RIGHT_TO_LEFT || dir == Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC) { + rtlCount++; + } else if (dir == Character.DIRECTIONALITY_LEFT_TO_RIGHT) { + ltrCount++; + } + } + return rtlCount > 0 && rtlCount >= ltrCount; + } + /** The blocks a modern lyric line is written with. */ static boolean isHan(char c) { return (c >= 0x4E00 && c <= 0x9FFF) diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/storage/LyricsCache.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/storage/LyricsCache.java index 67e3897282..19214225f4 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/storage/LyricsCache.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/storage/LyricsCache.java @@ -25,14 +25,17 @@ import java.util.function.Function; import app.morphe.extension.music.patches.lyrics.LyricsDirectories; +import app.morphe.extension.music.patches.lyrics.derive.OpenAIClient; import app.morphe.extension.music.patches.lyrics.model.Lyrics; import app.morphe.extension.music.patches.lyrics.model.LyricsLine; import app.morphe.extension.music.patches.lyrics.model.LyricsMerge; import app.morphe.extension.music.patches.lyrics.model.LyricsPreference; import app.morphe.extension.music.patches.lyrics.model.TrackInfo; +import app.morphe.extension.music.patches.lyrics.model.Word; import app.morphe.extension.music.patches.lyrics.parsers.LRCParser; import app.morphe.extension.music.patches.lyrics.parsers.TTMLParser; import app.morphe.extension.music.patches.lyrics.requests.LyricsRequests; +import app.morphe.extension.music.patches.lyrics.romanizers.ScriptDetector; import app.morphe.extension.music.settings.Settings; import app.morphe.extension.shared.Logger; @@ -363,12 +366,34 @@ private static String name(@Nullable File file) { return file == null ? "null" : file.getName(); } + @Nullable + private static List sanitizeTranslationList(@Nullable List list, @Nullable File file) { + if (list == null) return null; + boolean anyMeaningful = false; + List sanitized = new ArrayList<>(list.size()); + for (String s : list) { + if (OpenAIClient.isSkipToken(s)) { + sanitized.add(""); + } else { + sanitized.add(s); + anyMeaningful = true; + } + } + if (!anyMeaningful) { + if (file != null) discard(file); + return null; + } + return sanitized; + } + @Nullable public static List getTranslation(TrackInfo track, String source, String language, List sourceLines) { - return readStringList(translationFile(track, source, language, sourceLines), sourceLines); + File file = translationFile(track, source, language, sourceLines); + List raw = readStringList(file, sourceLines); + return sanitizeTranslationList(raw, file); } public static void putTranslation(TrackInfo track, @@ -376,19 +401,32 @@ public static void putTranslation(TrackInfo track, String language, List sourceLines, List lines) { - writeStringList(translationFile(track, source, language, sourceLines), lines); + File file = translationFile(track, source, language, sourceLines); + List clean = sanitizeTranslationList(lines, null); + if (clean != null) { + writeStringList(file, clean); + } else if (file != null) { + discard(file); + } } @Nullable public static List getTranslationAI(TrackInfo track, String source, String language, List sourceLines) { - return readStringList(aiTranslationFile(track, source, language, sourceLines), - sourceLines); + File file = aiTranslationFile(track, source, language, sourceLines); + List raw = readStringList(file, sourceLines); + return sanitizeTranslationList(raw, file); } public static void putTranslationAI(TrackInfo track, String source, String language, List sourceLines, List lines) { - writeStringList(aiTranslationFile(track, source, language, sourceLines), lines); + File file = aiTranslationFile(track, source, language, sourceLines); + List clean = sanitizeTranslationList(lines, null); + if (clean != null) { + writeStringList(file, clean); + } else if (file != null) { + discard(file); + } } @Nullable @@ -729,15 +767,43 @@ private static Lyrics readFromDisk(String key) { String content = String.join("\n", lines.subList(header.contentStart(), lines.size())); - return parseContentLines(content, header.synced(), header.provider(), + Lyrics lyrics = parseContentLines(content, header.synced(), header.provider(), header.songwriters(), header.sourceUrl(), header.formatType(), readRaw(file), key, header.about()); + if (lyrics != null && isCorruptedPetitLyrics(header.provider(), lyrics)) { + discard(file); + return null; + } + return lyrics; } catch (Exception ex) { Logger.printDebug(() -> "Could not read lyrics from disk cache", ex); return null; } } + private static boolean isCorruptedPetitLyrics(@Nullable String provider, Lyrics lyrics) { + if (provider == null || (!provider.equalsIgnoreCase("PetitLyrics") && !provider.toLowerCase(Locale.ROOT).contains("petitlyrics"))) { + return false; + } + int singleLetterTokens = 0; + int totalTokens = 0; + for (LyricsLine line : lyrics.lines()) { + if (line.words() != null && !line.words().isEmpty()) { + for (Word w : line.words()) { + String wt = w.text(); + if (wt != null && !wt.isEmpty()) { + totalTokens++; + if (wt.length() == 1 && Character.isLetter(wt.charAt(0)) + && !ScriptDetector.containsCjk(wt)) { + singleLetterTokens++; + } + } + } + } + } + return totalTokens >= 8 && ((float) singleLetterTokens / totalTokens) > 0.35f; + } + @Nullable private static Lyrics parseContent(File file, List lines, int contentStart, String sidecarKey) { diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsLineView.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsLineView.java index baedb4a1a8..8a3146a864 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsLineView.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsLineView.java @@ -334,17 +334,25 @@ private void ensureWordCache(Layout layout, CharSequence text, Paint paint, layout.getLineRight(line)); final float lead = Math.min( Math.max(layout.getPrimaryHorizontal(snappedStart), lineLeft), lineRight); + final boolean isRtl = isRightToLeftLine(layout, line) || layout.isRtlCharAt(s); final float trail; if (endLine != line) { // measureText reads the raw text, so a smaller size span is worth nothing to // it and the word measures wider than it is drawn. final float width = paint.measureText(text, s, e); - final float measured = layout.isRtlCharAt(s) ? lead - width : lead + width; + final float measured = isRtl ? lead - width : lead + width; trail = Math.min(Math.max(measured, lineLeft), lineRight); } else { - trail = trailOnLine(lineLeft, lineRight, lead, - layout.getPrimaryHorizontal(snappedEnd), - isRightToLeftLine(layout, line)); + final float end = layout.getPrimaryHorizontal(snappedEnd); + if (isRtl) { + // In RTL, text runs right-to-left, so end < lead. + // If end >= lead, the offset wrapped to the start of the next line, + // so the word ends at the line's left boundary. + trail = end >= lead ? lineLeft : Math.min(Math.max(end, lineLeft), lineRight); + } else { + // That offset reports where the next line starts; a word ending its line ends at its right. + trail = end <= lead ? lineRight : Math.min(Math.max(end, lineLeft), lineRight); + } } cachedWordLeadX[i] = lead; cachedWordTrailX[i] = trail; @@ -362,8 +370,8 @@ private void ensureWordCache(Layout layout, CharSequence text, Paint paint, for (int ln = 0; ln < lineCount; ln++) { cachedLineTop[ln] = layout.getLineTop(ln); cachedLineBottom[ln] = layout.getLineBottom(ln); - cachedLineLeft[ln] = layout.getLineLeft(ln); - cachedLineRight[ln] = layout.getLineRight(ln); + cachedLineLeft[ln] = Math.min(layout.getLineLeft(ln), layout.getLineRight(ln)); + cachedLineRight[ln] = Math.max(layout.getLineLeft(ln), layout.getLineRight(ln)); } } @@ -424,9 +432,7 @@ private static int[] clusterBoundaries(CharSequence text) { final int length = text.length(); final int[] boundaries = new int[length + 1]; final BreakIterator iterator = BreakIterator.getCharacterInstance(); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - iterator.setText(text); - } + iterator.setText(text.toString()); int clusterStart = iterator.first(); while (clusterStart != BreakIterator.DONE && clusterStart <= length) { final int next = iterator.next(); @@ -758,13 +764,21 @@ protected void onDraw(Canvas canvas) { // scaled against the whole line, which lit the rest of the line with it. final float lineLeft = layout.getLineLeft(ln); final float lineRight = Math.max(lineLeft, layout.getLineRight(ln)); - final float reach = ln == wrappedEnd - ? Math.min(Math.max(layout.getPrimaryHorizontal(e), lineLeft), - lineRight) - : lineRight; - final float lineEdge = lineLeft + (reach - lineLeft) * lineProgress; - lineMaxSungX[ln] = extendSung(lineMaxSungX[ln], lineEdge, - isRightToLeftLine(layout, ln)); + final boolean isLnRtl = isRightToLeftLine(layout, ln); + final float reach; + final float lineEdge; + if (isLnRtl) { + reach = ln == wrappedEnd + ? Math.min(Math.max(layout.getPrimaryHorizontal(e), lineLeft), lineRight) + : lineLeft; + lineEdge = lineRight + (reach - lineRight) * lineProgress; + } else { + reach = ln == wrappedEnd + ? Math.min(Math.max(layout.getPrimaryHorizontal(e), lineLeft), lineRight) + : lineRight; + lineEdge = lineLeft + (reach - lineLeft) * lineProgress; + } + lineMaxSungX[ln] = extendSung(lineMaxSungX[ln], lineEdge, isLnRtl); } } @@ -841,11 +855,13 @@ protected void onDraw(Canvas canvas) { canvas.save(); final int clipTop = ln == 0 ? -textOriginY() : cachedLineTop[ln]; if (isRightToLeftLine(layout, ln)) { - canvas.clipRect(edge, clipTop, + final float clipLeft = Math.min(edge, cachedLineRight[ln]); + canvas.clipRect(clipLeft, clipTop, cachedLineRight[ln], cachedLineBottom[ln]); } else { + final float clipRight = Math.max(edge, cachedLineLeft[ln]); canvas.clipRect(cachedLineLeft[ln], clipTop, - edge, cachedLineBottom[ln]); + clipRight, cachedLineBottom[ln]); } layout.draw(canvas); canvas.restore(); @@ -875,7 +891,24 @@ private static int lineDrawing(Layout layout, int offset) { } private static boolean isRightToLeftLine(Layout layout, int line) { - return layout.getParagraphDirection(line) == Layout.DIR_RIGHT_TO_LEFT; + if (layout.getParagraphDirection(line) == Layout.DIR_RIGHT_TO_LEFT) { + return true; + } + if (line >= 0 && line < layout.getLineCount()) { + final int start = layout.getLineStart(line); + final int end = layout.getLineEnd(line); + final CharSequence text = layout.getText(); + for (int i = start; i < end && i < text.length(); i++) { + byte dir = Character.getDirectionality(text.charAt(i)); + if (dir == Character.DIRECTIONALITY_RIGHT_TO_LEFT + || dir == Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC) { + return true; + } else if (dir == Character.DIRECTIONALITY_LEFT_TO_RIGHT) { + return false; + } + } + } + return false; } /** Right to left lines fill leftwards, so their furthest point is the smallest one. */ diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsPanelView.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsPanelView.java index 96c5404492..d6691ebf7e 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsPanelView.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsPanelView.java @@ -31,6 +31,9 @@ import android.os.SystemClock; import android.text.Layout; import android.text.Spannable; +import android.text.SpannableString; +import android.text.Spanned; +import android.text.style.AlignmentSpan; import android.text.style.ForegroundColorSpan; import android.util.Pair; import android.util.TypedValue; @@ -72,6 +75,7 @@ import app.morphe.extension.music.patches.lyrics.model.LyricsMerge; import app.morphe.extension.music.patches.lyrics.model.TrackInfo; import app.morphe.extension.music.patches.lyrics.requests.LyricsRequests; +import app.morphe.extension.music.patches.lyrics.romanizers.ScriptDetector; import app.morphe.extension.music.patches.lyrics.storage.LyricsFileSaver; import app.morphe.extension.music.settings.Settings; import app.morphe.extension.music.shared.VideoInformation; @@ -937,9 +941,11 @@ private boolean applySectionLabelsInPlace(@NonNull Lyrics newLyrics) { } TextView tag = createSectionTag(context, songPart); LinearLayout dotsRow = createSectionDots(context); + boolean isRight = i < lineViews.size() + && (lineViews.get(i).getGravity() & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.END; FrameLayout tagRow = new FrameLayout(context); - tagRow.addView(tag, tagLayoutParams()); - tagRow.addView(dotsRow, dotsLayoutParams(context)); + tagRow.addView(tag, tagLayoutParams(isRight)); + tagRow.addView(dotsRow, dotsLayoutParams(context, isRight)); asRow(lineRows.get(i)).addView(tagRow, 0, slotLayoutParams()); sectionTags.set(i, tag); sectionDots.set(i, dotsRow); @@ -979,8 +985,10 @@ private void ensureDotsSlotsExist(Context context, List g continue; } LinearLayout dotsRow = createSectionDots(context); + boolean isRight = index < lineViews.size() + && (lineViews.get(index).getGravity() & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.END; FrameLayout slot = new FrameLayout(context); - slot.addView(dotsRow, dotsLayoutParams(context)); + slot.addView(dotsRow, dotsLayoutParams(context, isRight)); FrameLayout.LayoutParams params = slotLayoutParams(); params.height = 0; params.topMargin = 0; @@ -1162,9 +1170,28 @@ private void buildLyrics(Lyrics newLyrics) { gapAnchors[anchor] = true; } } + // A duet puts its second singer on the opposite side of each line's natural direction. + boolean isDuetSong = false; + for (LyricsLine l : newLyrics.lines()) { + if (l.isDuet()) { + isDuetSong = true; + break; + } + } + final boolean[] lineRightAligned = new boolean[newLyrics.lines().size()]; + for (int i = 0; i < lineRightAligned.length; i++) { + LyricsLine l = newLyrics.lines().get(i); + boolean lineRtl = ScriptDetector.isLineRtl(l.text()); + lineRightAligned[i] = isDuetSong + ? (lineRtl ? !l.isDuet() : l.isDuet()) + : lineRtl; + } + final int duetSideInset = Math.max(Dim.dp(24), + (int) (context.getResources().getDisplayMetrics().widthPixels * 0.09f)); for (int i = 0; i < newLyrics.lines().size(); i++) { LyricsLine line = newLyrics.lines().get(i); + final boolean isRightAligned = lineRightAligned[i]; final LyricsLineView prevView = i < prevCount && prevViews.get(i) instanceof LyricsLineView pv ? pv : null; final boolean reusePrevious = prevView != null && !contentInBuild; @@ -1183,7 +1210,7 @@ private void buildLyrics(Lyrics newLyrics) { LyricsSpanBuilder.BuildResult result = LyricsSpanBuilder.buildLineText(line, gapTimings, i, perWordRomaji, romanizedLines, translatedLines, onlyMode, - baseLinePaint); + baseLinePaint, isRightAligned); lineView.setText(result.text()); lineView.setTranslationBounds(result.transStart(), result.transEnd()); lineView.setRomanizationBounds(result.romaStart(), result.romaEnd(), @@ -1201,7 +1228,12 @@ private void buildLyrics(Lyrics newLyrics) { lineView.romaReveal = prevView.romaReveal; gapOrigStart = prevView.originalTextStart; } else { - lineView.setText(line.text().isEmpty() ? "♪" : line.text()); + String rawText = line.text().isEmpty() ? "♪" : line.text(); + boolean isRtl = ScriptDetector.isLineRtl(rawText); + SpannableString initialText = new SpannableString(rawText); + initialText.setSpan(new AlignmentSpan.Standard(LyricsSpanBuilder.resolveAlignment(isRtl, isRightAligned)), + 0, initialText.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + lineView.setText(initialText); } lineOriginalStarts.add(gapOrigStart); if (reusePrevious) { @@ -1270,13 +1302,23 @@ private void buildLyrics(Lyrics newLyrics) { }); } + if (isRightAligned) { + lineView.setGravity(Gravity.END | Gravity.TOP); + } else { + lineView.setGravity(Gravity.START | Gravity.TOP); + } + LinearLayout lineRow = new LinearLayout(context); lineRow.setOrientation(LinearLayout.VERTICAL); - if (line.isDuet()) { - lineView.setGravity(Gravity.END | Gravity.TOP); + if (isDuetSong) { + if (isRightAligned) { + lineRow.setPadding(duetSideInset, Dim.dp(2), 0, Dim.dp(2)); + } else { + lineRow.setPadding(0, Dim.dp(2), duetSideInset, Dim.dp(2)); + } } else { - lineView.setGravity(Gravity.START | Gravity.TOP); + lineRow.setPadding(0, Dim.dp(2), 0, Dim.dp(2)); } // A background vocal sits under its lead line and carries the same label, so only a @@ -1296,8 +1338,8 @@ private void buildLyrics(Lyrics newLyrics) { if (sectionTag != null) { sectionDotsRow = createSectionDots(context); FrameLayout tagRow = new FrameLayout(context); - tagRow.addView(sectionTag, tagLayoutParams()); - tagRow.addView(sectionDotsRow, dotsLayoutParams(context)); + tagRow.addView(sectionTag, tagLayoutParams(isRightAligned)); + tagRow.addView(sectionDotsRow, dotsLayoutParams(context, isRightAligned)); lineRow.addView(tagRow, slotLayoutParams()); final TextView measuredTag = sectionTag; measuredTag.post(() -> { @@ -1310,7 +1352,7 @@ private void buildLyrics(Lyrics newLyrics) { // dots somewhere to play that matches what a label would have taken. sectionDotsRow = createSectionDots(context); sectionDotSlot = new FrameLayout(context); - sectionDotSlot.addView(sectionDotsRow, dotsLayoutParams(context)); + sectionDotSlot.addView(sectionDotsRow, dotsLayoutParams(context, isRightAligned)); FrameLayout.LayoutParams slotParams = slotLayoutParams(); // Starts fully closed: the dots row inside must not take space until a gap opens // it, and neither must the margin, which a zero-height child still occupies. @@ -1395,7 +1437,7 @@ private void buildLyrics(Lyrics newLyrics) { LyricsSpanBuilder.buildLineText(newLyrics.lines().get(i), allTimings.get(i), i, perWordRomaji, romanizedLines, translatedLines, onlyMode, - baseLinePaint); + baseLinePaint, lineRightAligned[i]); tv.setText(result.text()); lineUnsungSpans.set(i, result.unsungSpan()); lineWordSpans.set(i, LyricsViewSupport.wordSpansFor(result, only, @@ -3721,24 +3763,28 @@ private void applyWordColors(int index, long positionMs, boolean allSung) { } /** - * Where the label sits inside its row: flush to the line's left edge, so the text it draws lands + * Where the label sits inside its row: flush to the line's edge, so the text it draws lands * {@link #SECTION_TAG_PADDING_HORIZONTAL_FACTOR} of the lyrics text size in, a little further * in than the dot row below it starts. */ - private static FrameLayout.LayoutParams tagLayoutParams() { + private static FrameLayout.LayoutParams tagLayoutParams(boolean isRightAligned) { return new FrameLayout.LayoutParams( FrameLayout.LayoutParams.WRAP_CONTENT, FrameLayout.LayoutParams.WRAP_CONTENT, - Gravity.START | Gravity.CENTER_VERTICAL); + (isRightAligned ? Gravity.END : Gravity.START) | Gravity.CENTER_VERTICAL); } /** The row sits near the line's edge, unlike the pill whose padding holds its text further in. */ - private static FrameLayout.LayoutParams dotsLayoutParams(@NonNull Context context) { + private static FrameLayout.LayoutParams dotsLayoutParams(@NonNull Context context, boolean isRightAligned) { FrameLayout.LayoutParams params = new FrameLayout.LayoutParams( FrameLayout.LayoutParams.WRAP_CONTENT, FrameLayout.LayoutParams.WRAP_CONTENT, - Gravity.START | Gravity.CENTER_VERTICAL); - params.leftMargin = LyricsViewSupport.sectionDotInset(context); + (isRightAligned ? Gravity.END : Gravity.START) | Gravity.CENTER_VERTICAL); + if (isRightAligned) { + params.rightMargin = LyricsViewSupport.sectionDotInset(context); + } else { + params.leftMargin = LyricsViewSupport.sectionDotInset(context); + } return params; } @@ -3748,7 +3794,7 @@ private static FrameLayout.LayoutParams dotsLayoutParams(@NonNull Context contex */ private static FrameLayout.LayoutParams slotLayoutParams() { FrameLayout.LayoutParams params = new FrameLayout.LayoutParams( - FrameLayout.LayoutParams.WRAP_CONTENT, + FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT); params.topMargin = Dim.dp24; return params; diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsSpanBuilder.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsSpanBuilder.java index e5fefadd1c..7ada8d06ac 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsSpanBuilder.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsSpanBuilder.java @@ -9,9 +9,11 @@ import android.graphics.Paint; import android.icu.text.BreakIterator; +import android.text.Layout; import android.text.Spannable; import android.text.SpannableString; import android.text.Spanned; +import android.text.style.AlignmentSpan; import android.text.style.ForegroundColorSpan; import android.text.style.LineHeightSpan; import android.text.style.RelativeSizeSpan; @@ -26,6 +28,7 @@ import app.morphe.extension.music.patches.lyrics.model.LyricsLine; import app.morphe.extension.music.patches.lyrics.model.Word; import app.morphe.extension.music.patches.lyrics.romanizers.LocalRomanizer; +import app.morphe.extension.music.patches.lyrics.romanizers.ScriptDetector; import app.morphe.extension.music.settings.Settings; final class LyricsSpanBuilder { @@ -147,6 +150,7 @@ static Reading readingFor(LyricsLine line, int index, // unplaced word keeps the gap. final String own = word.text().trim(); final int found = own.isEmpty() ? -1 : original.indexOf(own, at); + //noinspection SizeReplaceableByIsEmpty if (text.length() > 0 && !sameWrittenWord(at, found, previous, own)) { text.append(' '); } @@ -499,7 +503,7 @@ private static List computeWordTimingsFromWords(LyricsLine line) { */ static BuildResult buildLineText(LyricsLine line, List timings, int index, boolean perWordRomaji, List romanizedLines, List translatedLines, - LyricsPanelView.OnlyMode onlyMode, Paint basePaint) { + LyricsPanelView.OnlyMode onlyMode, Paint basePaint, boolean isRightAligned) { final Paint.FontMetricsInt baseFm = new Paint.FontMetricsInt(); basePaint.getFontMetricsInt(baseFm); String original = line.text(); @@ -529,6 +533,10 @@ static BuildResult buildLineText(LyricsLine line, List timings, int final int transStart = content.isEmpty() ? -1 : 0; final int transEnd = content.isEmpty() ? -1 : content.length(); attachUniformSpacing(text, spacing); + if (!content.isEmpty()) { + text.setSpan(new AlignmentSpan.Standard(resolveAlignment(isRtlText(content), isRightAligned)), + 0, text.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } return new BuildResult(text, null, transStart, transEnd, -1, -1); } @@ -541,6 +549,8 @@ static BuildResult buildLineText(LyricsLine line, List timings, int if (content.isEmpty()) { return new BuildResult(text, null, -1, -1, -1, -1); } + text.setSpan(new AlignmentSpan.Standard(resolveAlignment(isRtlText(content), isRightAligned)), + 0, text.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); // No bounds reported paints the whole line the lyric colour, and none of the reading's fade. final List spans = readsNothing ? timings : roma.spans(); return new BuildResult(text, null, -1, -1, -1, -1, spans); @@ -564,7 +574,12 @@ static BuildResult buildLineText(LyricsLine line, List timings, int // are told apart only by where they sit. applySpans(dual, Collections.emptyList(), 0, smallStart - 1, smallIsRoma ? -1 : smallStart, smallIsRoma ? -1 : smallEnd, - smallIsRoma ? smallStart : -1, smallIsRoma ? smallEnd : -1); + smallIsRoma ? smallStart : -1, smallIsRoma ? smallEnd : -1, + isRightAligned); + } else { + boolean isRtl = isRtlText(dual); + dual.setSpan(new AlignmentSpan.Standard(resolveAlignment(isRtl, isRightAligned)), + 0, dual.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } // A small run carrying the word timings is what is sung, and keeps the reading's // colour, which is what tells it apart: the lyric's colour would leave the two lines @@ -603,7 +618,7 @@ static BuildResult buildLineText(LyricsLine line, List timings, int SpannableString text = new SpannableString(builder.toString()); attachUniformSpacing(text, spacing); ForegroundColorSpan unsungSpan = applySpans(text, timings, originalStart, originalEnd, - romaStart, romaEnd, transStart, transEnd); + romaStart, romaEnd, transStart, transEnd, isRightAligned); return new BuildResult(text, unsungSpan, transStart, transEnd, romaStart, romaEnd, reading.spans()); } @@ -626,7 +641,7 @@ static BuildResult buildLineText(LyricsLine line, List timings, int SpannableString text = new SpannableString(builder.toString()); attachUniformSpacing(text, spacing); ForegroundColorSpan unsungSpan = applySpans(text, timings, originalStart, originalEnd, - romaStart, romaEnd, transStart, transEnd); + romaStart, romaEnd, transStart, transEnd, isRightAligned); return new BuildResult(text, unsungSpan, transStart, transEnd, romaStart, romaEnd, reading.spans()); } @@ -639,9 +654,7 @@ private static void attachUniformSpacing(SpannableString text, Spacing spacing) Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } - @SuppressWarnings("SizeReplaceableByIsEmpty") - /** The line's own translation, null when it has none or it only repeats the original. */ - /** + /** The line's own translation, null when it has none or it only repeats the original * One line of the dual mode: the text to show and where the small run sits in it. The copy * button reads the same call, so the two cannot disagree about which side is the main line. * @@ -875,7 +888,8 @@ static String lineReading(LyricsLine line, int index, @Nullable private static ForegroundColorSpan applySpans(SpannableString text, List timings, int originalStart, int originalEnd, - int romaStart, int romaEnd, int transStart, int transEnd) { + int romaStart, int romaEnd, int transStart, int transEnd, + boolean isRightAligned) { ForegroundColorSpan unsungSpan = null; if (romaStart >= 0) { text.setSpan(new RelativeSizeSpan(TRANSLATION_RELATIVE_SIZE), romaStart, romaEnd, @@ -897,6 +911,50 @@ private static ForegroundColorSpan applySpans(SpannableString text, List= 0 && romaEnd > romaStart) { + int spanEnd = (swap && romaEnd == text.length()) ? romaEnd + : (romaEnd < text.length() && text.charAt(romaEnd - 1) == '\n' ? romaEnd + : Math.min(text.length(), romaEnd + 1)); + text.setSpan(new AlignmentSpan.Standard(resolveAlignment(false, isRightAligned)), + romaStart, spanEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + if (originalStart >= 0 && originalEnd > originalStart) { + int spanEnd = originalEnd; + if (!swap && transStart > originalEnd) { + spanEnd = transStart; + } else if (swap && romaStart > originalEnd) { + spanEnd = romaStart; + } + boolean isOrigRtl = isRtlText(text.subSequence(originalStart, originalEnd)); + text.setSpan(new AlignmentSpan.Standard(resolveAlignment(isOrigRtl, isRightAligned)), + originalStart, spanEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + if (transStart >= 0 && transEnd > transStart) { + int spanEnd = (swap && transEnd < text.length() && text.charAt(transEnd - 1) == '\n') ? transEnd : transEnd; + if (!swap && transEnd < text.length() && text.charAt(transEnd - 1) != '\n') { + spanEnd = Math.min(text.length(), transEnd + 1); + } + boolean isTransRtl = isRtlText(text.subSequence(transStart, transEnd)); + text.setSpan(new AlignmentSpan.Standard(resolveAlignment(isTransRtl, isRightAligned)), + transStart, spanEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + return unsungSpan; } + + static boolean isRtlText(@Nullable CharSequence text) { + if (text == null || text.length() == 0) { + return false; + } + return ScriptDetector.isLineRtl(text); + } + + public static Layout.Alignment resolveAlignment(boolean isRtl, boolean isRightAligned) { + if (isRightAligned) { + return isRtl ? Layout.Alignment.ALIGN_NORMAL : Layout.Alignment.ALIGN_OPPOSITE; + } else { + return isRtl ? Layout.Alignment.ALIGN_OPPOSITE : Layout.Alignment.ALIGN_NORMAL; + } + } } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/settings/Settings.java b/extensions/music/src/main/java/app/morphe/extension/music/settings/Settings.java index fec0d8d77e..10744fa27a 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/settings/Settings.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/settings/Settings.java @@ -15,6 +15,7 @@ import app.morphe.extension.music.patches.CrossfadePatch.CrossFadeDuration; import app.morphe.extension.music.patches.CrossfadePatch.FadeCurve; import app.morphe.extension.music.patches.PlaylistTrackTapPatch.TapAction; +import app.morphe.extension.music.patches.lyrics.derive.AiProviders; import app.morphe.extension.music.patches.lyrics.derive.OpenAIClient; import app.morphe.extension.music.patches.lyrics.model.LyricsFilterDefaults; import app.morphe.extension.music.sponsorblock.MusicSponsorBlockConfig; @@ -194,7 +195,7 @@ public class Settings extends SharedYouTubeSettings { public static final BooleanSetting LYRICS_ENABLED = new BooleanSetting("morphe_music_lyrics_enabled", TRUE, false); public static final BooleanSetting LYRICS_KEEP_SCREEN_ON = new BooleanSetting("morphe_music_lyrics_keep_screen_on", FALSE, false, parent(LYRICS_ENABLED)); public static final String DEFAULT_LYRICS_ORDER = - "YTMusic,-Captions,Apple,LRCLIB,QQ,NetEase,KuGou,Luna,-RMM,-PetitLyrics,-bLyrics,-BiniLyrics,-Unison,-SimpMusic,-AMLL,-LunaBeat,-Lyricify,-Musixmatch,-Spotify,-Deezer,-Genius,"; + "YTMusic,-Captions,Apple,LRCLIB,QQ,NetEase,KuGou,Luna,-PetitLyrics,-bLyrics,-BiniLyrics,-LrcRed,-Unison,-SimpMusic,-AMLL,-LunaBeat,-Lyricify,-Musixmatch,-Spotify,-Deezer,-RMM,-Genius,"; public static final StringSetting LYRICS_SOURCE = new StringSetting("morphe_music_lyrics_source", DEFAULT_LYRICS_ORDER, false, parent(LYRICS_ENABLED)); public static final StringSetting APPLE_MUSIC_TOKEN = new StringSetting("morphe_music_apple_music_token", "", false, parent(LYRICS_ENABLED)); public static final StringSetting SPOTIFY_TOKEN = new StringSetting("morphe_music_spotify_token", "", false, parent(LYRICS_ENABLED)); @@ -206,11 +207,16 @@ public class Settings extends SharedYouTubeSettings { public static final BooleanSetting LYRICS_SHOW_COPY_BUTTON = new BooleanSetting("morphe_music_lyrics_show_copy_button", TRUE, false, parent(LYRICS_ENABLED)); public static final BooleanSetting LYRICS_SHOW_SECTION = new BooleanSetting("morphe_music_lyrics_show_section", FALSE, false, parent(LYRICS_ENABLED)); public static final BooleanSetting LYRICS_SHOW_TRANSLATE_BUTTON = new BooleanSetting("morphe_music_lyrics_show_translate_button", TRUE, false, parent(LYRICS_ENABLED)); + public static final StringSetting LYRICS_TRANSLATION_PROVIDER = new StringSetting("morphe_music_lyrics_translation_provider", AiProviders.GOOGLE, false, parentsAll(parent(LYRICS_ENABLED), parent(LYRICS_SHOW_TRANSLATE_BUTTON))); + public static final StringSetting LYRICS_TRANSLATION_LANGUAGE = new StringSetting("morphe_music_lyrics_translation_language", "DEFAULT", false, parentsAll(parent(LYRICS_ENABLED), parent(LYRICS_SHOW_TRANSLATE_BUTTON))); + public static final BooleanSetting LYRICS_SHOW_ROMANIZE_BUTTON = new BooleanSetting("morphe_music_lyrics_show_romanize_button", TRUE, false, parent(LYRICS_ENABLED)); + public static final StringSetting LYRICS_ROMANIZATION_PROVIDER = new StringSetting("morphe_music_lyrics_romanization_provider", AiProviders.GOOGLE, false, parentsAll(parent(LYRICS_ENABLED), parent(LYRICS_SHOW_ROMANIZE_BUTTON))); public static final StringSetting LYRICS_ROMANIZE_LANGUAGE = new StringSetting("morphe_music_lyrics_romanize_language", "DEFAULT", false, parentsAll(parent(LYRICS_ENABLED), parent(LYRICS_SHOW_ROMANIZE_BUTTON))); - public static final StringSetting LYRICS_TRANSLATION_LANGUAGE = new StringSetting("morphe_music_lyrics_translation_language", "DEFAULT", false, parentsAll(parent(LYRICS_ENABLED), parent(LYRICS_SHOW_TRANSLATE_BUTTON))); - public static final BooleanSetting LYRICS_USE_AI_TRANSLATION = new BooleanSetting("morphe_music_lyrics_use_ai_translation", FALSE, false, parentsAll(parent(LYRICS_ENABLED), parent(LYRICS_SHOW_TRANSLATE_BUTTON))); - public static final BooleanSetting LYRICS_USE_AI_ROMANIZATION = new BooleanSetting("morphe_music_lyrics_use_ai_romanization", FALSE, false, parentsAll(parent(LYRICS_ENABLED), parent(LYRICS_SHOW_ROMANIZE_BUTTON))); + + public static final StringSetting LYRICS_GEMINI_API_KEY = new StringSetting("morphe_music_lyrics_gemini_api_key", "", false, parent(LYRICS_ENABLED)); + public static final StringSetting LYRICS_GEMINI_MODEL = new StringSetting("morphe_music_lyrics_gemini_model", "gemini-2.0-flash", false, parent(LYRICS_ENABLED)); + public static final StringSetting LYRICS_DEEPL_API_KEY = new StringSetting("morphe_music_lyrics_deepl_api_key", "", false, parent(LYRICS_ENABLED)); /** * Romanizes on the device when the provider, an OpenAI-compatible endpoint and Google * Translate have all failed. Off by default: it needs the reading dictionaries, and a user @@ -219,14 +225,14 @@ public class Settings extends SharedYouTubeSettings { public static final BooleanSetting LYRICS_LOCAL_ROMANIZATION = new BooleanSetting("morphe_music_lyrics_local_romanization", FALSE, false, parentsAll(parent(LYRICS_ENABLED), parent(LYRICS_SHOW_ROMANIZE_BUTTON))); public static final BooleanSetting LYRICS_SHOW_REFRESH_BUTTON = new BooleanSetting("morphe_music_lyrics_show_refresh_button", TRUE, false, parent(LYRICS_ENABLED)); public static final BooleanSetting LYRICS_USE_AI_SECTION = new BooleanSetting("morphe_music_lyrics_use_ai_section", FALSE, false, parentsAll(parent(LYRICS_ENABLED), parent(LYRICS_SHOW_SECTION))); - public static final StringSetting LYRICS_AI_BASE_URL = new StringSetting("morphe_music_lyrics_ai_base_url", "https://text.pollinations.ai/openai", false, parent(LYRICS_ENABLED)); + public static final StringSetting LYRICS_AI_BASE_URL = new StringSetting("morphe_music_lyrics_ai_base_url", "https://api.openai.com/v1/chat/completions", false, parent(LYRICS_ENABLED)); public static final StringSetting LYRICS_AI_API_TOKEN = new StringSetting("morphe_music_lyrics_ai_api_token", "", false, parent(LYRICS_ENABLED)); - public static final StringSetting LYRICS_AI_MODEL = new StringSetting("morphe_music_lyrics_ai_model", "openai-fast", false, parent(LYRICS_ENABLED)); + public static final StringSetting LYRICS_AI_MODEL = new StringSetting("morphe_music_lyrics_ai_model", "gpt-4o-mini", false, parent(LYRICS_ENABLED)); public static final StringSetting LYRICS_AI_REASONING_EFFORT = new StringSetting("morphe_music_lyrics_ai_reasoning_effort", "", false, parent(LYRICS_ENABLED)); public static final StringSetting LYRICS_AI_EXTRA_BODY = new StringSetting("morphe_music_lyrics_ai_extra_body", "", false, parent(LYRICS_ENABLED)); - public static final StringSetting LYRICS_AI_TRANSLATION_PROMPT = new StringSetting("morphe_music_lyrics_ai_prompt", OpenAIClient.DEFAULT_PROMPT, false, parent(LYRICS_USE_AI_TRANSLATION)); - public static final StringSetting LYRICS_AI_ROMANIZATION_PROMPT = new StringSetting("morphe_music_lyrics_ai_romanization_prompt", OpenAIClient.DEFAULT_PROMPT, false, parent(LYRICS_USE_AI_ROMANIZATION)); - public static final StringSetting LYRICS_AI_SECTION_PROMPT = new StringSetting("morphe_music_lyrics_ai_section_prompt", OpenAIClient.SECTION_PROMPT, false, parent(LYRICS_USE_AI_SECTION)); + public static final StringSetting LYRICS_AI_TRANSLATION_PROMPT = new StringSetting("morphe_music_lyrics_ai_prompt", OpenAIClient.DEFAULT_PROMPT, false, parentsAll(parent(LYRICS_ENABLED), parent(LYRICS_SHOW_TRANSLATE_BUTTON))); + public static final StringSetting LYRICS_AI_ROMANIZATION_PROMPT = new StringSetting("morphe_music_lyrics_ai_romanization_prompt", OpenAIClient.DEFAULT_PROMPT, false, parentsAll(parent(LYRICS_ENABLED), parent(LYRICS_SHOW_ROMANIZE_BUTTON))); + public static final StringSetting LYRICS_AI_SECTION_PROMPT = new StringSetting("morphe_music_lyrics_ai_section_prompt", OpenAIClient.SECTION_PROMPT, false, parent(LYRICS_ENABLED)); public static final StringSetting LYRICS_TRANSCRIBE_KEY = new StringSetting("morphe_music_lyrics_transcribe_key", "", false, parent(LYRICS_ENABLED)); public static final BooleanSetting LYRICS_HIDE_INFO = new BooleanSetting("morphe_music_lyrics_hide_info", FALSE, false, parent(LYRICS_ENABLED)); public static final BooleanSetting LYRICS_SWAP_TRANS_ROMA = new BooleanSetting("morphe_music_lyrics_swap_trans_roma", FALSE, false, parentsAll(parent(LYRICS_ENABLED), parentsAny(LYRICS_SHOW_TRANSLATE_BUTTON, LYRICS_SHOW_ROMANIZE_BUTTON))); @@ -236,7 +242,7 @@ public class Settings extends SharedYouTubeSettings { public static final BooleanSetting LYRICS_HIDE_PLAYED = new BooleanSetting("morphe_music_lyrics_hide_played", FALSE, false, parent(LYRICS_ENABLED)); public static final BooleanSetting LYRICS_SPLIT_BACKGROUND = new BooleanSetting("morphe_music_lyrics_split_background", FALSE, false, parent(LYRICS_ENABLED)); public static final BooleanSetting LYRICS_HIDE_UNPLAYED = new BooleanSetting("morphe_music_lyrics_hide_unplayed", FALSE, false, parent(LYRICS_ENABLED)); - public static final IntegerSetting LYRICS_TEXT_SIZE = new IntegerSetting("morphe_music_lyrics_text_size", 24, false, parent(LYRICS_ENABLED)); + public static final IntegerSetting LYRICS_TEXT_SIZE = new IntegerSetting("morphe_music_lyrics_text_size", 26, false, parent(LYRICS_ENABLED)); public static final IntegerSetting LYRICS_OFFSET_MS = new IntegerSetting("morphe_music_lyrics_offset_ms", 0, false, parent(LYRICS_ENABLED)); public static final BooleanSetting LYRICS_SB_MATCHING = new BooleanSetting("morphe_music_lyrics_sb_matching", TRUE, false, parent(LYRICS_ENABLED)); public static final BooleanSetting LYRICS_MEDIASESSION = new BooleanSetting("morphe_music_lyrics_mediasession", FALSE, false, parent(LYRICS_ENABLED)); @@ -285,6 +291,8 @@ public class Settings extends SharedYouTubeSettings { private static final BooleanSetting DEPRECATED_HIDE_CATEGORY_BAR = new BooleanSetting("morphe_music_hide_category_bar", FALSE, true); private static final BooleanSetting DEPRECATED_MUSIC_IN_APP_DOWNLOADS = new BooleanSetting("morphe_music_in_app_downloads", FALSE); private static final BooleanSetting DEPRECATED_PERMANENT_REPEAT = new BooleanSetting("morphe_music_permanent_repeat", FALSE, true); + private static final BooleanSetting DEPRECATED_LYRICS_USE_AI_TRANSLATION = new BooleanSetting("morphe_music_lyrics_use_ai_translation", FALSE, false); + private static final BooleanSetting DEPRECATED_LYRICS_USE_AI_ROMANIZATION = new BooleanSetting("morphe_music_lyrics_use_ai_romanization", FALSE, false); static { migrateOldSettingToNew(DEPRECATED_EXTERNAL_DOWNLOADER_FLYOUT_BUTTON, DEPRECATED_EXTERNAL_DOWNLOADER_FLYOUT_MENU); @@ -292,6 +300,14 @@ public class Settings extends SharedYouTubeSettings { migrateOldSettingToNew(DEPRECATED_PERMANENT_REPEAT, REMEMBER_REPEAT_STATE); migrateInAppDownloadsToDownloadMode(); migrateDownloadButtonOverride(); + migrateLyricsAiSwitchToProvider(DEPRECATED_LYRICS_USE_AI_TRANSLATION, LYRICS_TRANSLATION_PROVIDER); + migrateLyricsAiSwitchToProvider(DEPRECATED_LYRICS_USE_AI_ROMANIZATION, LYRICS_ROMANIZATION_PROVIDER); + if ("openrouter".equals(LYRICS_TRANSLATION_PROVIDER.get()) || "openai".equals(LYRICS_TRANSLATION_PROVIDER.get())) { + LYRICS_TRANSLATION_PROVIDER.save(AiProviders.OPENAI_COMPATIBLE); + } + if ("openrouter".equals(LYRICS_ROMANIZATION_PROVIDER.get()) || "openai".equals(LYRICS_ROMANIZATION_PROVIDER.get())) { + LYRICS_ROMANIZATION_PROVIDER.save(AiProviders.OPENAI_COMPATIBLE); + } } static { @@ -314,7 +330,7 @@ public class Settings extends SharedYouTubeSettings { SeekBarPreference.register(new SeekBarConfig(LASTFM_DELAY_SECONDS, 30, 360, 10, "s")); SeekBarPreference.register(new SeekBarConfig(LYRICS_TEXT_SIZE, - 14, 40, 2, "sp")); + 18, 36, 2, "sp")); SeekBarPreference.register(new SeekBarConfig(LYRICS_OFFSET_MS, -2000, 2000, 100, "ms")); SeekBarPreference.register(new SeekBarConfig(PLAYBACK_SPEED, @@ -344,6 +360,14 @@ private static void migrateInAppDownloadsToDownloadMode() { * The menu overrides are not migrated to the new menu switch, which also applies to in-app downloads. * The External downloader mode is still in use, so a marker is the only way to tell this already ran. */ + /** The AI switch used the OpenAI-compatible endpoint, which is now one of the providers to pick. */ + private static void migrateLyricsAiSwitchToProvider(BooleanSetting oldSwitch, StringSetting provider) { + if (oldSwitch.get() && provider.isSetToDefault()) { + provider.save(AiProviders.OPENAI_COMPATIBLE); + } + oldSwitch.resetToDefault(); + } + private static void migrateDownloadButtonOverride() { if (DOWNLOAD_BUTTON_OVERRIDE_MIGRATED.get()) { return; diff --git a/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsAiConfigPreference.java b/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsAiConfigPreference.java index 8c3e8bf0ce..b4e70392bc 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsAiConfigPreference.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsAiConfigPreference.java @@ -12,239 +12,419 @@ import android.app.Activity; import android.app.Dialog; import android.content.Context; +import android.graphics.Typeface; import android.preference.Preference; import android.text.InputType; import android.util.AttributeSet; import android.util.Pair; import android.util.TypedValue; +import android.view.View; +import android.view.ViewGroup; import android.view.WindowManager; import android.widget.Button; import android.widget.EditText; import android.widget.LinearLayout; +import android.widget.ScrollView; import android.widget.TextView; +import androidx.annotation.Nullable; + +import java.util.Collections; +import java.util.List; +import java.util.function.Supplier; + +import app.morphe.extension.music.patches.lyrics.derive.AiProviders; +import app.morphe.extension.music.patches.lyrics.derive.DeepLClient; +import app.morphe.extension.music.patches.lyrics.derive.GeminiClient; import app.morphe.extension.music.patches.lyrics.derive.OpenAIClient; import app.morphe.extension.music.settings.Settings; import app.morphe.extension.shared.Logger; import app.morphe.extension.shared.Utils; +import app.morphe.extension.shared.settings.preference.CustomDialogListPreference; import app.morphe.extension.shared.theme.ThemeUtils; import app.morphe.extension.shared.ui.CustomDialog; import app.morphe.extension.shared.ui.Dim; /** - * The endpoint every AI feature talks to. It is one shared setting rather than a switch of its - * own, so the features next to it stay the only switches a user has to reason about. + * Holds the API keys, models, custom endpoints, and prompt templates for all AI and cloud providers. + * Translation and Romanization provider selection happens in their respective settings sections. */ @SuppressWarnings({"unused", "deprecation"}) public class LyricsAiConfigPreference extends Preference { + private static final String TRANSLATION_LABEL_PREFIX = "morphe_music_lyrics_translation_provider_"; + + private boolean dialogShowing = false; + public LyricsAiConfigPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); - init(); + updateSummary(); } public LyricsAiConfigPreference(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); - init(); + updateSummary(); } public LyricsAiConfigPreference(Context context, AttributeSet attrs) { super(context, attrs); - init(); + updateSummary(); } public LyricsAiConfigPreference(Context context) { super(context); - init(); - } - - private void init() { - setSelectable(true); - setPersistent(false); updateSummary(); } private void updateSummary() { - String model = Settings.LYRICS_AI_MODEL.get(); - setSummary(model.isEmpty() - ? null - : str("morphe_music_lyrics_ai_config_summary_configured", model)); + setSummary(str("morphe_music_lyrics_ai_config_summary")); } + @Override protected void onClick() { showDialog(); } private void showDialog() { + if (dialogShowing) return; Context context = getContext(); if (!(context instanceof Activity activity) || activity.isFinishing() || activity.isDestroyed()) { return; } + dialogShowing = true; + ScrollView scrollView = new ScrollView(context); LinearLayout content = new LinearLayout(context); content.setOrientation(LinearLayout.VERTICAL); + scrollView.addView(content, new ScrollView.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); TextView instruction = new TextView(context); instruction.setText(str("morphe_music_lyrics_ai_config_dialog_instruction")); instruction.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14); instruction.setTextColor(ThemeUtils.getAppForegroundColor()); LinearLayout.LayoutParams instructionParams = new LinearLayout.LayoutParams( - LinearLayout.LayoutParams.MATCH_PARENT, - LinearLayout.LayoutParams.WRAP_CONTENT); + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); instructionParams.bottomMargin = Dim.dp12; content.addView(instruction, instructionParams); - EditText baseUrlInput = ThemedInput.create(context); - baseUrlInput.setHint(str("morphe_music_lyrics_ai_config_base_url_hint")); - String currentBaseUrl = Settings.LYRICS_AI_BASE_URL.get(); - if (!currentBaseUrl.isEmpty()) { - baseUrlInput.setText(currentBaseUrl); - baseUrlInput.setSelection(currentBaseUrl.length()); - } - content.addView(baseUrlInput, new LinearLayout.LayoutParams( - LinearLayout.LayoutParams.MATCH_PARENT, - LinearLayout.LayoutParams.WRAP_CONTENT)); - - LinearLayout.LayoutParams tokenParams = new LinearLayout.LayoutParams( - LinearLayout.LayoutParams.MATCH_PARENT, - LinearLayout.LayoutParams.WRAP_CONTENT); - tokenParams.topMargin = Dim.dp8; - EditText apiTokenInput = ThemedInput.create(context); - apiTokenInput.setHint(str("morphe_music_lyrics_ai_config_api_token_hint")); - apiTokenInput.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); - String currentToken = Settings.LYRICS_AI_API_TOKEN.get(); - if (!currentToken.isEmpty()) { - apiTokenInput.setText(currentToken); - apiTokenInput.setSelection(currentToken.length()); - } - content.addView(apiTokenInput, tokenParams); - - LinearLayout.LayoutParams modelParams = new LinearLayout.LayoutParams( - LinearLayout.LayoutParams.MATCH_PARENT, - LinearLayout.LayoutParams.WRAP_CONTENT); - modelParams.topMargin = Dim.dp8; - EditText modelInput = ThemedInput.create(context); - modelInput.setHint(str("morphe_music_lyrics_ai_config_model_hint")); - String currentModel = Settings.LYRICS_AI_MODEL.get(); - if (!currentModel.isEmpty()) { - modelInput.setText(currentModel); - modelInput.setSelection(currentModel.length()); - } - content.addView(modelInput, modelParams); - - LinearLayout.LayoutParams extraBodyParams = new LinearLayout.LayoutParams( - LinearLayout.LayoutParams.MATCH_PARENT, - LinearLayout.LayoutParams.WRAP_CONTENT); - extraBodyParams.topMargin = Dim.dp8; - EditText extraBodyInput = ThemedInput.create(context); - extraBodyInput.setSingleLine(false); - extraBodyInput.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE); - extraBodyInput.setMinLines(2); - extraBodyInput.setMaxLines(5); - extraBodyInput.setVerticalScrollBarEnabled(true); - extraBodyInput.setHint(str("morphe_music_lyrics_ai_extra_body_hint")); - String currentExtraBody = Settings.LYRICS_AI_EXTRA_BODY.get(); - if (!currentExtraBody.isEmpty()) { - extraBodyInput.setText(currentExtraBody); - extraBodyInput.setSelection(currentExtraBody.length()); - } - content.addView(extraBodyInput, extraBodyParams); - - Pair dialogPair = CustomDialog.create( - context, - str("morphe_music_lyrics_use_ai_title"), - null, - null, - null, - null, - null, - null, - null, - false, - true - ); - - Dialog dialog = dialogPair.first; - LinearLayout mainLayout = dialogPair.second; - - mainLayout.addView(content, mainLayout.getChildCount() - 1, - new LinearLayout.LayoutParams( - LinearLayout.LayoutParams.MATCH_PARENT, - LinearLayout.LayoutParams.WRAP_CONTENT)); - - Runnable resetAction = () -> { - Settings.LYRICS_AI_BASE_URL.resetToDefault(); - Settings.LYRICS_AI_API_TOKEN.resetToDefault(); - Settings.LYRICS_AI_MODEL.resetToDefault(); - Settings.LYRICS_AI_EXTRA_BODY.resetToDefault(); - baseUrlInput.setText(Settings.LYRICS_AI_BASE_URL.get()); - apiTokenInput.setText(""); - modelInput.setText(Settings.LYRICS_AI_MODEL.get()); - extraBodyInput.setText(""); - updateSummary(); - Utils.showToastShort(str("morphe_music_lyrics_ai_config_toast_reset")); - }; - - Runnable saveAction = () -> { - String baseUrl = baseUrlInput.getText().toString().trim(); - String apiToken = apiTokenInput.getText().toString().trim(); - String model = modelInput.getText().toString().trim(); - String extraBody = extraBodyInput.getText().toString().trim(); + final String[] endpointFilter = {"all"}; + content.addView(createLabel(context, "morphe_music_lyrics_ai_config_endpoint_label")); + Button endpointFilterButton = createButton(context, str("morphe_music_lyrics_ai_config_show_all")); + content.addView(endpointFilterButton); - if (baseUrl.isEmpty()) { - Utils.showToastShort(str("morphe_music_lyrics_ai_config_status_empty_url")); + // Gemini. + LinearLayout geminiSection = createSection(context, "morphe_music_lyrics_ai_config_gemini_section"); + EditText geminiKeyInput = addInput(geminiSection, "morphe_music_lyrics_ai_config_api_key_label", + Settings.LYRICS_GEMINI_API_KEY.get(), true); + EditText geminiModelInput = addModelInput(geminiSection, Settings.LYRICS_GEMINI_MODEL.get(), + () -> GeminiClient.fetchModels(geminiKeyInput.getText().toString().trim())); + addTestButton(geminiSection, () -> { + String key = geminiKeyInput.getText().toString().trim(); + String model = geminiModelInput.getText().toString().trim(); + if (key.isEmpty()) { + Utils.showToastShort("Enter Gemini API key first"); return; } - if (!baseUrl.startsWith("http://") && !baseUrl.startsWith("https://")) { + Utils.showToastShort(str("morphe_music_lyrics_ai_config_status_validating")); + Utils.runOnBackgroundThread(() -> { + List res = GeminiClient.mapLines(key, model, "Translate to Japanese:\n1. hello", Collections.singletonList("hello")); + Utils.runOnMainThread(() -> { + if (res != null && !res.isEmpty()) { + Utils.showToastShort("Gemini: Connected! (" + res.get(0) + ")"); + } else { + Utils.showToastShort("Gemini: Connection failed. Check API key and model."); + } + }); + }); + }); + content.addView(geminiSection); + + // A custom OpenAI-compatible endpoint. + LinearLayout compatibleSection = createSection(context, "morphe_music_lyrics_ai_config_openai_compat_section"); + EditText compatibleUrlInput = addInput(compatibleSection, "morphe_music_lyrics_ai_config_base_url_label", + Settings.LYRICS_AI_BASE_URL.get(), false); + compatibleUrlInput.setHint(str("morphe_music_lyrics_ai_config_base_url_hint")); + EditText compatibleTokenInput = addInput(compatibleSection, "morphe_music_lyrics_ai_config_api_token_label", + Settings.LYRICS_AI_API_TOKEN.get(), true); + EditText compatibleModelInput = addModelInput(compatibleSection, Settings.LYRICS_AI_MODEL.get(), + () -> OpenAIClient.fetchModels(compatibleUrlInput.getText().toString().trim(), + compatibleTokenInput.getText().toString().trim())); + EditText extraBodyInput = addInput(compatibleSection, "morphe_music_lyrics_ai_config_extra_body_label", + Settings.LYRICS_AI_EXTRA_BODY.get(), false); + makeMultiLine(extraBodyInput, 2, 5); + addTestButton(compatibleSection, () -> { + String url = compatibleUrlInput.getText().toString().trim(); + String token = compatibleTokenInput.getText().toString().trim(); + String model = compatibleModelInput.getText().toString().trim(); + if (url.isEmpty()) { Utils.showToastShort(str("morphe_music_lyrics_ai_config_status_invalid_url")); return; } - if (model.isEmpty()) { - Utils.showToastShort(str("morphe_music_lyrics_ai_config_status_empty_model")); - return; - } - // Nothing is sent until this parses, so a body that will not is refused here, not saved. - if (!OpenAIClient.isValidExtraBody(extraBody)) { - Utils.showToastShort(str("morphe_music_lyrics_ai_extra_body_invalid")); + Utils.showToastShort(str("morphe_music_lyrics_ai_config_status_validating")); + Utils.runOnBackgroundThread(() -> { + boolean ok = OpenAIClient.validateConfig(url, token, model); + Utils.runOnMainThread(() -> { + if (ok) { + Utils.showToastShort("OpenAI-Compatible: Connected successfully!"); + } else { + Utils.showToastShort("OpenAI-Compatible: Connection failed. Check URL, token, and model."); + } + }); + }); + }); + content.addView(compatibleSection); + + // DeepL. + LinearLayout deepLSection = createSection(context, "morphe_music_lyrics_ai_config_deepl_section"); + EditText deepLKeyInput = addInput(deepLSection, "morphe_music_lyrics_ai_config_deepl_key_label", + Settings.LYRICS_DEEPL_API_KEY.get(), true); + addTestButton(deepLSection, () -> { + String key = deepLKeyInput.getText().toString().trim(); + if (key.isEmpty()) { + Utils.showToastShort("Enter DeepL authentication key first"); return; } - // Written before the endpoint is asked, so one that only takes what this body makes answers yes. - Settings.LYRICS_AI_EXTRA_BODY.save(extraBody); - Utils.showToastShort(str("morphe_music_lyrics_ai_config_status_validating")); Utils.runOnBackgroundThread(() -> { - boolean valid = OpenAIClient.validateConfig(baseUrl, apiToken, model); + List res = DeepLClient.translate(Collections.singletonList("hello"), "JA", key); Utils.runOnMainThread(() -> { - if (!valid) { - Utils.showToastShort(str("morphe_music_lyrics_ai_config_toast_invalid")); + if (res != null && !res.isEmpty()) { + Utils.showToastShort("DeepL: Connected! (" + res.get(0) + ")"); + } else { + Utils.showToastShort("DeepL: Connection failed. Check authentication key."); + } + }); + }); + }); + content.addView(deepLSection); + + // AI Prompt Templates. + LinearLayout promptSection = createSection(context, "morphe_music_lyrics_ai_prompt_dialog_title"); + EditText promptInput = addInput(promptSection, "morphe_music_lyrics_ai_config_prompt_label", + Settings.LYRICS_AI_TRANSLATION_PROMPT.get(), false); + makeMultiLine(promptInput, 3, 6); + EditText promptRomajiInput = addInput(promptSection, "morphe_music_lyrics_ai_config_prompt_romaji_label", + Settings.LYRICS_AI_ROMANIZATION_PROMPT.get(), false); + makeMultiLine(promptRomajiInput, 3, 6); + EditText promptSectionInput = addInput(promptSection, "morphe_music_lyrics_ai_config_prompt_section_label", + Settings.LYRICS_AI_SECTION_PROMPT.get(), false); + makeMultiLine(promptSectionInput, 3, 6); + content.addView(promptSection); + + Runnable updateVisibility = () -> { + boolean showAll = "all".equals(endpointFilter[0]); + geminiSection.setVisibility(showAll || AiProviders.GEMINI.equals(endpointFilter[0]) ? View.VISIBLE : View.GONE); + compatibleSection.setVisibility(showAll || AiProviders.OPENAI_COMPATIBLE.equals(endpointFilter[0]) ? View.VISIBLE : View.GONE); + deepLSection.setVisibility(showAll || AiProviders.DEEPL.equals(endpointFilter[0]) ? View.VISIBLE : View.GONE); + promptSection.setVisibility(showAll || "prompts".equals(endpointFilter[0]) ? View.VISIBLE : View.GONE); + }; + updateVisibility.run(); + + endpointFilterButton.setOnClickListener(v -> { + String[] filterValues = { + "all", + AiProviders.GEMINI, + AiProviders.OPENAI_COMPATIBLE, + AiProviders.DEEPL, + "prompts" + }; + String[] filterLabels = { + str("morphe_music_lyrics_ai_config_show_all"), + str(TRANSLATION_LABEL_PREFIX + AiProviders.GEMINI), + str(TRANSLATION_LABEL_PREFIX + AiProviders.OPENAI_COMPATIBLE), + str(TRANSLATION_LABEL_PREFIX + AiProviders.DEEPL), + str("morphe_music_lyrics_ai_prompt_dialog_title") + }; + CustomDialogListPreference.createListDialog(context, + str("morphe_music_lyrics_ai_config_endpoint_label"), + filterLabels, filterValues, endpointFilter[0], index -> { + endpointFilter[0] = filterValues[index]; + endpointFilterButton.setText(filterLabels[index]); + updateVisibility.run(); + }).show(); + }); + + Pair dialogPair = CustomDialog.create( + context, + str("morphe_music_lyrics_ai_config_dialog_title"), + null, + null, + str("morphe_settings_save"), + () -> { + String extraBody = extraBodyInput.getText().toString().trim(); + if (!OpenAIClient.isValidExtraBody(extraBody)) { + Utils.showToastShort(str("morphe_music_lyrics_ai_extra_body_invalid")); + return; + } + String baseUrl = compatibleUrlInput.getText().toString().trim(); + if (!baseUrl.isEmpty() && !baseUrl.startsWith("http://") && !baseUrl.startsWith("https://")) { + Utils.showToastShort(str("morphe_music_lyrics_ai_config_status_invalid_url")); return; } + Settings.LYRICS_AI_EXTRA_BODY.save(extraBody); + Settings.LYRICS_GEMINI_API_KEY.save(geminiKeyInput.getText().toString().trim()); + Settings.LYRICS_GEMINI_MODEL.save(geminiModelInput.getText().toString().trim()); Settings.LYRICS_AI_BASE_URL.save(baseUrl); - Settings.LYRICS_AI_API_TOKEN.save(apiToken); - Settings.LYRICS_AI_MODEL.save(model); + Settings.LYRICS_AI_API_TOKEN.save(compatibleTokenInput.getText().toString().trim()); + Settings.LYRICS_AI_MODEL.save(compatibleModelInput.getText().toString().trim()); + Settings.LYRICS_AI_TRANSLATION_PROMPT.save(promptInput.getText().toString().trim()); + Settings.LYRICS_AI_ROMANIZATION_PROMPT.save(promptRomajiInput.getText().toString().trim()); + Settings.LYRICS_AI_SECTION_PROMPT.save(promptSectionInput.getText().toString().trim()); + Settings.LYRICS_DEEPL_API_KEY.save(deepLKeyInput.getText().toString().trim()); + updateSummary(); - dialog.dismiss(); Utils.showToastShort(str("morphe_music_lyrics_ai_config_toast_saved")); - }); - }); - }; + }, + null, + str("morphe_settings_reset"), + () -> { + Settings.LYRICS_GEMINI_API_KEY.resetToDefault(); + Settings.LYRICS_GEMINI_MODEL.resetToDefault(); + Settings.LYRICS_AI_BASE_URL.resetToDefault(); + Settings.LYRICS_AI_API_TOKEN.resetToDefault(); + Settings.LYRICS_AI_MODEL.resetToDefault(); + Settings.LYRICS_AI_TRANSLATION_PROMPT.resetToDefault(); + Settings.LYRICS_AI_ROMANIZATION_PROMPT.resetToDefault(); + Settings.LYRICS_AI_SECTION_PROMPT.resetToDefault(); + Settings.LYRICS_AI_EXTRA_BODY.resetToDefault(); + Settings.LYRICS_DEEPL_API_KEY.resetToDefault(); + updateSummary(); + Utils.showToastShort(str("morphe_music_lyrics_ai_config_toast_reset")); + }, + true + ); + + Dialog dialog = dialogPair.first; + LinearLayout mainLayout = dialogPair.second; + + LinearLayout.LayoutParams scrollParams = new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, 0, 1.0f); + scrollParams.bottomMargin = Dim.dp12; + mainLayout.addView(scrollView, mainLayout.getChildCount() - 1, scrollParams); - // Both buttons are built here instead of handed to create() because the save action can - // refuse what it was given, and the input has to survive that: create() dismisses on OK - // unconditionally, so a refusal would close the dialog and drop everything typed into it. - mainLayout.removeViewAt(mainLayout.getChildCount() - 1); - Button saveButton = CustomDialog.createButton(context, dialog, - str("morphe_settings_save"), saveAction, true, false); - Button resetButton = CustomDialog.createButton(context, dialog, - str("morphe_settings_reset"), resetAction, false, false); - mainLayout.addView(CustomDialog.createButtonRow(context, resetButton, saveButton)); + dialog.setOnDismissListener(d -> dialogShowing = false); try { dialog.show(); } catch (WindowManager.BadTokenException ex) { + dialogShowing = false; Logger.printException(() -> "Could not show the AI settings dialog", ex); } } + + private static LinearLayout createSection(Context context, String titleKey) { + LinearLayout section = new LinearLayout(context); + section.setOrientation(LinearLayout.VERTICAL); + + TextView header = new TextView(context); + header.setText(str(titleKey)); + header.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14); + header.setTypeface(Typeface.DEFAULT_BOLD); + header.setTextColor(ThemeUtils.getAppForegroundColor()); + LinearLayout.LayoutParams params = new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); + params.topMargin = Dim.dp16; + section.addView(header, params); + return section; + } + + private static TextView createLabel(Context context, String textKey) { + TextView label = new TextView(context); + label.setText(str(textKey)); + label.setTextSize(TypedValue.COMPLEX_UNIT_SP, 12); + label.setTextColor(ThemeUtils.getAppForegroundColor()); + LinearLayout.LayoutParams params = new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); + params.topMargin = Dim.dp8; + params.bottomMargin = Dim.dp4; + label.setLayoutParams(params); + return label; + } + + private static Button createButton(Context context, String text) { + Button button = new Button(context); + button.setText(text); + button.setAllCaps(false); + button.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14); + button.setTextColor(ThemeUtils.getAppForegroundColor()); + button.setBackground(CustomDialog.createRoundedBackground(10, ThemeUtils.getEditTextBackground())); + button.setPadding(Dim.dp12, Dim.dp8, Dim.dp12, Dim.dp8); + return button; + } + + private static EditText addInput(LinearLayout section, String labelKey, String value, boolean secret) { + Context context = section.getContext(); + section.addView(createLabel(context, labelKey)); + EditText input = ThemedInput.create(context); + if (secret) { + input.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); + } + input.setText(value); + section.addView(input, new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); + return input; + } + + private static void makeMultiLine(EditText input, int minLines, int maxLines) { + input.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE); + input.setSingleLine(false); + input.setMinLines(minLines); + input.setMaxLines(maxLines); + input.setVerticalScrollBarEnabled(true); + } + + /** A model name input with a button that lists the models the provider offers. */ + private static EditText addModelInput(LinearLayout section, String value, Supplier> fetchModels) { + Context context = section.getContext(); + section.addView(createLabel(context, "morphe_music_lyrics_ai_config_model_label")); + + EditText input = ThemedInput.create(context); + input.setText(value); + Button fetch = createButton(context, str("morphe_music_lyrics_ai_config_fetch_models")); + fetch.setOnClickListener(v -> { + Utils.showToastShort(str("morphe_music_lyrics_ai_config_fetching_models")); + Utils.runOnBackgroundThread(() -> { + List models = fetchModels.get(); + Utils.runOnMainThread(() -> showModelPicker(context, models, input)); + }); + }); + + LinearLayout row = new LinearLayout(context); + row.setOrientation(LinearLayout.HORIZONTAL); + row.addView(input, new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1.0f)); + LinearLayout.LayoutParams buttonParams = new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); + buttonParams.leftMargin = Dim.dp8; + row.addView(fetch, buttonParams); + section.addView(row); + return input; + } + + private static void showModelPicker(Context context, @Nullable List models, EditText target) { + if (models == null || models.isEmpty()) { + Utils.showToastShort(str("morphe_music_lyrics_ai_config_fetch_models_failed")); + return; + } + String[] items = models.toArray(new String[0]); + CustomDialogListPreference.createListDialog(context, + str("morphe_music_lyrics_ai_config_fetch_models_success"), items, items, + target.getText().toString().trim(), index -> { + target.setText(items[index]); + target.setSelection(items[index].length()); + }).show(); + } + + private static void addTestButton(LinearLayout section, Runnable onTest) { + Context context = section.getContext(); + Button testButton = createButton(context, str("morphe_music_lyrics_ai_config_test_connection")); + testButton.setOnClickListener(v -> onTest.run()); + LinearLayout.LayoutParams params = new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); + params.topMargin = Dim.dp8; + params.bottomMargin = Dim.dp4; + section.addView(testButton, params); + } } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsAiPromptPreference.java b/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsAiPromptPreference.java index bf408f32a8..9a48c625c0 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsAiPromptPreference.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsAiPromptPreference.java @@ -64,10 +64,6 @@ private BooleanSetting switchSetting() { String key = getKey(); if (key == null) return null; switch (key) { - case "morphe_music_lyrics_use_ai_translation": - return Settings.LYRICS_USE_AI_TRANSLATION; - case "morphe_music_lyrics_use_ai_romanization": - return Settings.LYRICS_USE_AI_ROMANIZATION; case "morphe_music_lyrics_use_ai_section": return Settings.LYRICS_USE_AI_SECTION; default: @@ -79,10 +75,6 @@ private StringSetting promptSetting() { String key = getKey(); if (key == null) return null; switch (key) { - case "morphe_music_lyrics_use_ai_translation": - return Settings.LYRICS_AI_TRANSLATION_PROMPT; - case "morphe_music_lyrics_use_ai_romanization": - return Settings.LYRICS_AI_ROMANIZATION_PROMPT; case "morphe_music_lyrics_use_ai_section": return Settings.LYRICS_AI_SECTION_PROMPT; default: diff --git a/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsOrderedListPreference.java b/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsOrderedListPreference.java index 04cd9cef59..7c35ebf0a9 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsOrderedListPreference.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsOrderedListPreference.java @@ -50,11 +50,11 @@ @SuppressWarnings({"unused", "deprecation"}) public final class LyricsOrderedListPreference extends Preference { - /** Canonical provider ids, in the default priority order, shown in the list. */ private static final List PROVIDER_ORDER = Arrays.asList( - "YTMusic", "Captions", "LRCLIB", "QQ", "NetEase", "KuGou", "Luna", - "RMM", "PetitLyrics", "bLyrics", "BiniLyrics", "Unison", "SimpMusic", "AMLL", - "LunaBeat", "Lyricify", "Apple", "Musixmatch", "Spotify", "Deezer", "Genius"); + "Apple", "BiniLyrics", "LrcRed", "bLyrics", "Lyricify", "Unison", + "PetitLyrics", "AMLL", "QQ", "SimpMusic", "KuGou", "Musixmatch", + "YTMusic", "LRCLIB", "NetEase", "Luna", "LunaBeat", + "Captions", "Spotify", "Deezer", "RMM", "Genius"); /** Friendlier labels for display; ids not present here are shown verbatim. */ private static final Map PROVIDER_LABELS = new HashMap<>(); @@ -70,6 +70,7 @@ public final class LyricsOrderedListPreference extends Preference { PROVIDER_LABELS.put("Spotify", "Spotify *"); PROVIDER_LABELS.put("Deezer", "Deezer *"); PROVIDER_LABELS.put("Genius", "Genius"); + PROVIDER_LABELS.put("LrcRed", "lrc.red"); } private static final Map PROVIDER_URLS = new HashMap<>(); @@ -89,6 +90,7 @@ public final class LyricsOrderedListPreference extends Preference { PROVIDER_URLS.put("Lyricify", "https://lyricify.app/"); PROVIDER_URLS.put("RMM", "https://rmmreviv.al/"); PROVIDER_URLS.put("Genius", "https://genius.com/"); + PROVIDER_URLS.put("LrcRed", "https://lrc.red/"); } private static String providerLabel(String id) { diff --git a/patches/src/main/kotlin/app/morphe/patches/music/layout/lyrics/LyricsPatch.kt b/patches/src/main/kotlin/app/morphe/patches/music/layout/lyrics/LyricsPatch.kt index 2eae714ebe..99d5b82c79 100644 --- a/patches/src/main/kotlin/app/morphe/patches/music/layout/lyrics/LyricsPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/music/layout/lyrics/LyricsPatch.kt @@ -132,19 +132,31 @@ val lyricsPatch = bytecodePatch( SwitchPreference("morphe_music_lyrics_tap_to_seek", summary = true), SwitchPreference("morphe_music_lyrics_keep_screen_on", summary = true), SwitchPreference("morphe_music_lyrics_show_copy_button", summary = true), + SwitchPreference("morphe_music_lyrics_show_refresh_button", summary = true), SwitchPreference("morphe_music_lyrics_show_translate_button", summary = true), - SwitchPreference("morphe_music_lyrics_show_romanize_button", summary = true), - SwitchPreference("morphe_music_lyrics_show_refresh_button", summary = true) + SwitchPreference("morphe_music_lyrics_show_romanize_button", summary = true) ) ), PreferenceCategory( - key = "morphe_music_lyrics_section_languages", + key = "morphe_music_lyrics_section_translation", sorting = Sorting.UNSORTED, preferences = setOf( + ListPreference( + key = "morphe_music_lyrics_translation_provider" + ), ListPreference( key = "morphe_music_lyrics_translation_language", entriesKey = "morphe_language_entries", entryValuesKey = "morphe_language_entry_values" + ) + ) + ), + PreferenceCategory( + key = "morphe_music_lyrics_section_romanization", + sorting = Sorting.UNSORTED, + preferences = setOf( + ListPreference( + key = "morphe_music_lyrics_romanization_provider" ), ListPreference(key = "morphe_music_lyrics_romanize_language"), SwitchPreference( @@ -178,26 +190,15 @@ val lyricsPatch = bytecodePatch( preferences = setOf( NonInteractivePreference( key = "morphe_music_lyrics_ai_config", - titleKey = "morphe_music_lyrics_use_ai_title", + titleKey = "morphe_music_lyrics_ai_config_dialog_title", summaryKey = null, tag = "app.morphe.extension.music.settings.preference.LyricsAiConfigPreference", selectable = true, dependency = "morphe_music_lyrics_enabled" ), - SwitchPreference( - key = "morphe_music_lyrics_use_ai_translation", - summary = true, - tag = "app.morphe.extension.music.settings.preference.LyricsAiPromptPreference" - ), - SwitchPreference( - key = "morphe_music_lyrics_use_ai_romanization", - summary = true, - tag = "app.morphe.extension.music.settings.preference.LyricsAiPromptPreference" - ), SwitchPreference( key = "morphe_music_lyrics_use_ai_section", - summary = true, - tag = "app.morphe.extension.music.settings.preference.LyricsAiPromptPreference" + summary = true ), NonInteractivePreference( key = "morphe_music_lyrics_transcribe_key", diff --git a/patches/src/main/resources/addresources/values/music/arrays.xml b/patches/src/main/resources/addresources/values/music/arrays.xml index b3b1f6f4be..b09ea9e6e8 100644 --- a/patches/src/main/resources/addresources/values/music/arrays.xml +++ b/patches/src/main/resources/addresources/values/music/arrays.xml @@ -225,4 +225,31 @@ ZH YUE - \ No newline at end of file + + + + @string/morphe_music_lyrics_translation_provider_google + @string/morphe_music_lyrics_translation_provider_gemini + @string/morphe_music_lyrics_translation_provider_openai_compatible + @string/morphe_music_lyrics_translation_provider_deepl + + + google + gemini + openai_compatible + deepl + + + + @string/morphe_music_lyrics_romanization_provider_google + @string/morphe_music_lyrics_romanization_provider_local + @string/morphe_music_lyrics_romanization_provider_gemini + @string/morphe_music_lyrics_romanization_provider_openai_compatible + + + google + local + gemini + openai_compatible + + diff --git a/patches/src/main/resources/addresources/values/music/strings.xml b/patches/src/main/resources/addresources/values/music/strings.xml index 6fd2aab61c..699fc1f3e1 100644 --- a/patches/src/main/resources/addresources/values/music/strings.xml +++ b/patches/src/main/resources/addresources/values/music/strings.xml @@ -507,7 +507,6 @@ To ensure scrobbling is synced during background playback, ensure that YouTube M Display Synchronisation Panel - Languages AI endpoint All providers disabled - built-in lyrics and embedded lyrics Reset provider order? @@ -669,33 +668,55 @@ Required cookies: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPISID Hide the credit line shown below the lyrics Swap translation and romanization ON puts romanization below lyrics and translation above; OFF keeps romanization above and translation below - OpenAI-compatible API - AI Translation - Prefers the translation the provider ships with, then your OpenAI-compatible model, and Google Translate last - AI Romanization - Prefers the romanization the provider ships with, then your OpenAI-compatible model, and Google Translate last + Translation + Romanization + Translation provider + Google (Default, Free) + Gemini API + OpenAI Compatible (Custom) + DeepL API + Romanization provider + Google (Default) + Local / Offline Dictionary + Gemini API + OpenAI Compatible (Custom) AI Section Only used when the provider ships no section labels such as Chorus or Verse, which your OpenAI-compatible model then detects -AI Settings - Configure the OpenAI-compatible API endpoint for AI-powered lyrics translation, romanization and section - API endpoint URL (e.g. https://text.pollinations.ai/openai) - API key (leave empty for anonymous access) - Model name (e.g. openai-fast, gpt-4o-mini) + AI & Cloud Endpoints + Configure API keys, models, endpoints, and prompt templates for AI providers. + API endpoint URL (e.g. https://api.openai.com/v1/chat/completions) Prompt Template Placeholders: {language}, {task}, {title}, {artist}, {count}, {lines} Prompt saved Prompt saved, but the AI endpoint is not usable yet. Configure it under AI Settings first Prompt reset - Extra request body (JSON, merged into every AI request) Not valid JSON. Fix it or clear the field before saving - AI settings saved - Could not connect to AI endpoint. Please check your settings - AI settings reset - Model %1$s configured - Base URL cannot be empty + Translation & Romanization settings saved + Settings reset to default Base URL must start with http:// or https:// - Model name cannot be empty + Test Connection + Configure Endpoints + Show All Endpoints Validating connection… + Translation Provider + Romanization Provider + Fetch Models + Fetching models… + Select a Model + Failed to fetch models. Check API key or connection. + Gemini API Settings + OpenAI Compatible (Custom Endpoint) + DeepL API Settings + API key + Model + Base URL + API key (optional) + Translation prompt + Romanization prompt + AI Section prompt + Extra request body (optional JSON) + Authentication key (Free or Pro) + Configure API keys, models, endpoints, and prompts 8.23.51 - Disable bold icons From 7988621db650eb2c32e7cab53731508ad733d5b9 Mon Sep 17 00:00:00 2001 From: pharmacomaniac Date: Fri, 23 Oct 2026 10:42:09 -0400 Subject: [PATCH 018/189] fix(YouTube Music - Downloads): Fix missing metadata in downloaded songs (#3984) --- .../patches/downloads/LocalDownloadManager.java | 14 +++++--------- .../shared/spoof/requests/PlayerRoutes.java | 9 ++++++++- .../spoof/requests/StreamingDataRequest.java | 2 +- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/downloads/LocalDownloadManager.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/downloads/LocalDownloadManager.java index 0a58944794..acc8b7100d 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/downloads/LocalDownloadManager.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/downloads/LocalDownloadManager.java @@ -467,15 +467,11 @@ private static void postDownloadNotification(String key, String title, int progr @Nullable private static ResolvedStream resolveStream(String videoId) { - StreamingDataRequest request = StreamingDataRequest.getRequestForVideoId(videoId); - if (request == null) { - Logger.printDebug(() -> "No cached streaming request for download, resolving: " + videoId); - // The clients are named here because the playback order is only set up while spoofing - // is on, and a download needs a client that answers with plain urls either way. - request = StreamingDataRequest.fetchRequestForDownload(videoId, - SpoofVideoStreamsPatch.getAvailableClients(), - Settings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get()); - } + Logger.printDebug(() -> "Resolving stream and metadata for download: " + videoId); + // Cached playback responses omit videoDetails, so fetch streams and song details together. + StreamingDataRequest request = StreamingDataRequest.fetchRequestForDownload(videoId, + SpoofVideoStreamsPatch.getAvailableClients(), + Settings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get()); StreamingDataRequest.StreamData stream = request.getStream(); if (stream == null) { diff --git a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/spoof/requests/PlayerRoutes.java b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/spoof/requests/PlayerRoutes.java index af1d1de43d..5c72f0a854 100644 --- a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/spoof/requests/PlayerRoutes.java +++ b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/spoof/requests/PlayerRoutes.java @@ -52,6 +52,11 @@ public final class PlayerRoutes { "&alt=proto" ).compile(); + private static final Route.CompiledRoute GET_REEL_STREAMING_DATA_WITH_DETAILS = new Route( + Route.Method.POST, + "reel/reel_item_watch?alt=proto" + ).compile(); + private static final String YT_API_URL = "https://youtubei.googleapis.com/youtubei/v1/"; private PlayerRoutes() { @@ -161,7 +166,9 @@ static HttpURLConnection getPlayerResponseConnectionFromRoute(ClientType clientT boolean includeVideoDetails) throws IOException { Route.CompiledRoute route; if (!clientType.usePlayerEndpoint) { - route = GET_REEL_STREAMING_DATA; + route = includeVideoDetails + ? GET_REEL_STREAMING_DATA_WITH_DETAILS + : GET_REEL_STREAMING_DATA; } else { route = includeVideoDetails ? GET_PLAYER_STREAMING_DATA_WITH_DETAILS diff --git a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/spoof/requests/StreamingDataRequest.java b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/spoof/requests/StreamingDataRequest.java index 256eb94549..235a003049 100644 --- a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/spoof/requests/StreamingDataRequest.java +++ b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/spoof/requests/StreamingDataRequest.java @@ -207,7 +207,7 @@ public static void fetchRequest(String videoId, boolean isInline, Map * The clients are given by the caller instead of taken from the playback order, which stays at From 7259e27ade3bd1f76cd89f2ecad07d86f39d0335 Mon Sep 17 00:00:00 2001 From: pharmacomaniac Date: Fri, 23 Oct 2026 10:44:21 -0400 Subject: [PATCH 019/189] fix(YouTube Music - Downloads): Fix wrong song downloads (#3978) * fix(YouTube Music - Downloads): Fix wrong song downloads from menus * fix(YouTube Music - Downloads): Keep player and menu tap timers separate * fix(YouTube Music - Downloads): Identify Download commands by their parameters * fix(YouTube Music - Downloads): Read the selected song ID from command parameters * fix(YouTube Music - Downloads): Keep collection downloads outside the song tap limit * refactor(YouTube Music - Downloads): Clarify song selection and tap handling * refactor(YouTube Music - Downloads): Simplify the download hook Use v1 for the native handler result and place comments beside the behavior they explain. --- .../music/patches/DownloadsPatch.java | 220 ++++-------------- .../interaction/downloads/DownloadsPatch.kt | 23 +- .../interaction/downloads/Fingerprints.kt | 14 +- 3 files changed, 68 insertions(+), 189 deletions(-) diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/DownloadsPatch.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/DownloadsPatch.java index e2046456e3..75511af3f8 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/DownloadsPatch.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/DownloadsPatch.java @@ -10,6 +10,7 @@ import android.app.Activity; import android.content.Intent; import android.net.Uri; +import android.util.Base64; import android.view.View; import android.view.ViewGroup; @@ -23,6 +24,7 @@ import app.morphe.extension.music.settings.Settings; import app.morphe.extension.music.shared.VideoInformation; import app.morphe.extension.shared.Logger; +import app.morphe.extension.shared.ResourceUtils; import app.morphe.extension.shared.Utils; import app.morphe.extension.shared.patches.DownloadMode; import app.morphe.extension.shared.patches.components.ContextInterface; @@ -44,7 +46,6 @@ public interface ProtocolBufferFieldInterface { "com.google.android.libraries.youtube.rendering.elements.sender_view"; private static final int IGNORE_DOUBLE_CLICK_DURATION_MS = 1000; - private static volatile String cachedFlyoutVideoId = ""; private static volatile String downloadButtonLabel = ""; private static volatile long lastFlyoutDownloadTime; @@ -98,17 +99,7 @@ private static boolean flyoutMenuUsesExternalDownloader() { || Settings.EXTERNAL_DOWNLOADER_MENU.get(); } - /** - * Downloads the track that is playing. - */ - private static void startDownload(boolean externalDownloader) { - startDownload(VideoInformation.getVideoId(), externalDownloader); - } - private static void startDownload(String videoId, boolean externalDownloader) { - cachedFlyoutVideoId = ""; - // Do not clear download button label. - if (!externalDownloader) { LocalDownloadManager.enqueue(videoId); return; @@ -147,74 +138,6 @@ private static boolean isOfflineBrowseCommand(byte[] bytes) { return false; } - /** - * Scans the raw bytes of the Command object looking for the specific - * Protobuf binary signature of an 11-byte String field. - */ - private static String extractVideoIdFromCommand(ProtocolBufferFieldInterface commandObj) { - byte[] bytes = commandObj.toByteArray(); - if (bytes == null) { - return null; - } - - for (int i = 1, lastIndex = bytes.length - 11; i < lastIndex; i++) { - // Protobuf: field tag (wire type 2, length-delimited) followed by length 11 - if (bytes[i] == 11 && (bytes[i - 1] & 0b00000111) == 2) { - if (isLikelyVideoId(bytes, i + 1) && !isBlacklisted(bytes, i + 1)) { - return new String(bytes, i + 1, 11, StandardCharsets.US_ASCII); - } - } - } - return null; - } - - /** - * Checks if the 11 bytes at the given offset are a valid YouTube video ID character set. - */ - private static boolean isLikelyVideoId(byte[] bytes, int offset) { - for (int i = 0; i < 11; i++) { - byte b = bytes[offset + i]; - // YouTube video IDs consist of [A-Za-z0-9_-] - if (!((b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') - || (b >= '0' && b <= '9') || b == '_' || b == '-')) { - return false; - } - } - return true; - } - - /** - * Checks if the potential ID is blacklisted such as common Protobuf keys. - */ - private static boolean isBlacklisted(byte[] bytes, int offset) { - return matchesIgnoreCase(bytes, offset, "yt_") || - matchesIgnoreCase(bytes, offset, "video_") || - containsIgnoreCase(bytes, offset, 11, "download") || - containsIgnoreCase(bytes, offset, 11, "list_item") || - containsIgnoreCase(bytes, offset, 11, "button"); - } - - private static boolean matchesIgnoreCase(byte[] bytes, int offset, String target) { - for (int i = 0, length = target.length(); i < length; i++) { - byte b = bytes[offset + i]; - int lowerB = (b >= 'A' && b <= 'Z') ? (b + 32) : b; - if (lowerB != target.charAt(i)) { - return false; - } - } - return true; - } - - @SuppressWarnings("SameParameterValue") - private static boolean containsIgnoreCase(byte[] bytes, int offset, int len, String target) { - for (int i = 0, lastIndex = len - target.length(); i <= lastIndex; i++) { - if (matchesIgnoreCase(bytes, offset + i, target)) { - return true; - } - } - return false; - } - /** * Determines if the clicked view is inside a Dialog/BottomSheet by comparing * its Window root to the main Activity's Window root. @@ -235,20 +158,13 @@ private static boolean isViewInsideDialog(@Nullable Object viewObj) { /** * Injection point. */ - public static boolean offlineVideoEndpointOnClick(ProtocolBufferFieldInterface endpoint, - @Nullable Map map) { + public static boolean offlineVideoEndpointOnClick(String videoId) { try { if (!isActionButtonOverridden()) { return false; } Utils.verifyOnMainThread(); - String videoId = endpoint == null ? null : extractVideoIdFromCommand(endpoint); - if (videoId == null || videoId.isEmpty()) { - videoId = VideoInformation.getVideoId(); - } - if (videoId.isEmpty()) return false; - long now = System.currentTimeMillis(); if (now - lastMainPlayerDownloadTime < IGNORE_DOUBLE_CLICK_DURATION_MS) return true; lastMainPlayerDownloadTime = now; @@ -263,39 +179,23 @@ public static boolean offlineVideoEndpointOnClick(ProtocolBufferFieldInterface e /** * The label is the localized download text litho gave us, so no hard coded language is needed. */ - private static boolean isDownloadSender(@Nullable Map map) { + private static boolean isPlayerDownloadButton(@Nullable Map map) { if (map == null || downloadButtonLabel.isEmpty() || !(map.get(ELEMENTS_SENDER_VIEW) instanceof ViewGroup senderViewGroup)) { return false; } CharSequence description = senderViewGroup.getContentDescription(); - if (description == null) return false; + if (description == null || description.length() == 0) return false; String value = description.toString().toLowerCase(Locale.ROOT); String label = downloadButtonLabel.toLowerCase(Locale.ROOT); - return value.contains(label) || label.contains(value); - } + if (!value.contains(label) && !label.contains(value)) return false; - public static boolean inAppDownloadButtonOnClick(@Nullable Map map) { - try { - if (!isActionButtonOverridden() - || downloadButtonLabel.isEmpty() || map == null) { - return false; - } - Utils.verifyOnMainThread(); - - if (isDownloadSender(map)) { - final long now = System.currentTimeMillis(); - if (now - lastMainPlayerDownloadTime < IGNORE_DOUBLE_CLICK_DURATION_MS) { - return true; - } - lastMainPlayerDownloadTime = now; - - startDownload(!inAppDownloads()); - return true; - } - } catch (Exception ex) { - Logger.printException(() -> "inAppDownloadButtonOnClick failure", ex); + int actionBarId = ResourceUtils.getIdIdentifier("video_action_bar"); + View view = senderViewGroup; + while (view.getParent() instanceof View parent) { + if (parent.getId() == actionBarId) return true; + view = parent; } return false; } @@ -311,8 +211,8 @@ public static boolean commandResolverOnClick(ProtocolBufferFieldInterface p1, Ma } Utils.verifyOnMainThread(); + byte[] commandBytes = p1.toByteArray(); if (inAppDownloads()) { - byte[] commandBytes = p1.toByteArray(); if (commandBytes != null && isOfflineBrowseCommand(commandBytes)) { openLocalDownloads(); // The local screen is opened on top of the stock one rather than in place of @@ -322,71 +222,46 @@ public static boolean commandResolverOnClick(ProtocolBufferFieldInterface p1, Ma } } - if (inAppDownloadButtonOnClick(map)) { - Logger.printDebug(() -> "inAppDownloadButtonOnClicked"); - cachedFlyoutVideoId = ""; - return true; - } + Object viewObj = map.get(ELEMENTS_SENDER_VIEW); + final boolean inDialog = isViewInsideDialog(viewObj); + // Song menus in search results, playlists and the queue share the player's Download label. + // Only the player action bar button should use the playing song's ID. + if (!inDialog && isPlayerDownloadButton(map)) { + if (!isActionButtonOverridden()) return false; - String p1String = p1.toString(); - Logger.printDebug(() -> "commandResolverOnClick: " + p1String); + String videoId = VideoInformation.getVideoId(); + if (videoId == null || videoId.isEmpty()) return false; - final boolean isMenuOpen = p1String.contains("[98150882]"); - if (isMenuOpen) { - Logger.printDebug(() -> "Flyout isMenuOpen"); - String extractedId = extractVideoIdFromCommand(p1); - if (extractedId != null) { - cachedFlyoutVideoId = extractedId; - Logger.printDebug(() -> "Found flyout isMenuOpen videoId: " + extractedId); - } else { - cachedFlyoutVideoId = ""; + final long now = System.currentTimeMillis(); + if (now - lastMainPlayerDownloadTime < IGNORE_DOUBLE_CLICK_DURATION_MS) { + return true; } - return false; + lastMainPlayerDownloadTime = now; + startDownload(videoId, !inAppDownloads()); + return true; } - final boolean isDownloadClick = Utils.containsAny(p1String, - "[133724106]", "[443434441]"); - if (isDownloadClick) { - Object viewObj = map.get(ELEMENTS_SENDER_VIEW); - + String downloadParams = new ProtoMessage(commandBytes) + .messageField(133724106).stringField(1); + if (!downloadParams.isEmpty()) { if (viewObj == null) { Logger.printDebug(() -> "Ignored programmatic download click (no sender_view)."); return false; } - if (viewObj instanceof ViewGroup senderViewGroup) { - CharSequence cd = senderViewGroup.getContentDescription(); - if (cd != null && !downloadButtonLabel.isEmpty()) { - String cdLower = cd.toString().toLowerCase(Locale.ROOT); - String labelLower = downloadButtonLabel.toLowerCase(Locale.ROOT); - - if (!cdLower.contains(labelLower) && !labelLower.contains(cdLower)) { - Logger.printDebug(() -> "Ignored false positive UI click (Content description mismatch)."); - return false; - } - } - } - Logger.printDebug(() -> "Flyout isDownloadClick"); - final long now = System.currentTimeMillis(); - if (now - lastFlyoutDownloadTime < IGNORE_DOUBLE_CLICK_DURATION_MS) { - return true; - } - - final boolean inDialog = isViewInsideDialog(viewObj); - if (!inDialog && !isActionButtonOverridden() && isDownloadSender(map)) { - // The Download button of the player, which only the download mode replaces. - return false; - } - - String targetId = extractVideoIdFromCommand(p1); - - if (targetId == null && inDialog) { - targetId = cachedFlyoutVideoId; - Logger.printDebug(() -> "Using flyout isDownloadClick videoId: " + cachedFlyoutVideoId); - } - - if (targetId != null && !targetId.isEmpty()) { + // Field 4 contains the video ID selected by this Download command. + // The map's endpoint.tag entry can still refer to a previously opened player menu. + String targetId = new ProtoMessage(Base64.decode( + Uri.decode(downloadParams), Base64.URL_SAFE)).stringField(4); + + if (!targetId.isEmpty()) { + // The song menu stays open after Download, so repeated taps can repeat the request. + // Keep this timer separate from the player button so a menu download of another song is not blocked. + final long now = System.currentTimeMillis(); + if (now - lastFlyoutDownloadTime < IGNORE_DOUBLE_CLICK_DURATION_MS) { + return true; + } lastFlyoutDownloadTime = now; final String flyoutId = targetId; Logger.printDebug(() -> "Flyout download of " + flyoutId @@ -394,18 +269,9 @@ public static boolean commandResolverOnClick(ProtocolBufferFieldInterface p1, Ma startDownload(targetId, flyoutMenuUsesExternalDownloader()); return true; - } else if (inDialog) { - lastFlyoutDownloadTime = now; - // The flyout of a queue row carries no id either, so this can only be - // the track that is playing. - Logger.printDebug(() -> "Now Playing Download Intercepted via Window Check."); - startDownload(flyoutMenuUsesExternalDownloader()); - return true; - } else { - // A download click with no video id is a whole album or playlist, which the - // app only offers with Premium, so the stock UI handles it. - Logger.printDebug(() -> "Playlist Download detected via Window Check. Falling back to native UI"); + // Album and playlist commands have no video ID in field 4. + // Let YouTube Music handle them without consuming the song download timer. return false; } } diff --git a/patches/src/main/kotlin/app/morphe/patches/music/interaction/downloads/DownloadsPatch.kt b/patches/src/main/kotlin/app/morphe/patches/music/interaction/downloads/DownloadsPatch.kt index fdb16eb1a4..67ae2540ee 100644 --- a/patches/src/main/kotlin/app/morphe/patches/music/interaction/downloads/DownloadsPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/music/interaction/downloads/DownloadsPatch.kt @@ -22,6 +22,8 @@ import app.morphe.patches.shared.misc.settings.preference.SwitchPreference import app.morphe.patches.shared.misc.settings.preference.TextPreference import app.morphe.patches.shared.misc.textcomponent.hookSpannableString import app.morphe.patches.shared.misc.textcomponent.textComponentPatch +import app.morphe.util.addInstructionsAtControlFlowLabel +import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction private val OFFLINE_PLAYBACK_PERMISSIONS = listOf( "android.permission.WAKE_LOCK", @@ -103,6 +105,8 @@ val downloadsPatch = bytecodePatch( execute { hookSpannableString(EXTENSION_CLASS) + val (_, downloadCall) = OfflineVideoEndpointFingerprint.instructionMatches + CommandResolverFingerprint.method.cloneParameters().apply { // Add interface to get buffer. mutableClassDefBy(parameterTypes[1].toString()) @@ -121,17 +125,16 @@ val downloadsPatch = bytecodePatch( ) } - OfflineVideoEndpointFingerprint.method.cloneParameters().apply { - mutableClassDefBy(parameterTypes[0].toString()).interfaces.apply { - if (!contains(EXTENSION_PROTOCOL_BUFFER_INTERFACE)) add(EXTENSION_PROTOCOL_BUFFER_INTERFACE) - } - - addInstructionsWithLabels( - 0, + OfflineVideoEndpointFingerprint.method.apply { + val videoIdRegister = (downloadCall.instruction as FiveRegisterInstruction).registerD + // Reuse the ID YTM resolved for its single-video download action. + // v1 is unused by the download call, which is followed by return-void. + addInstructionsAtControlFlowLabel( + downloadCall.index, """ - invoke-static { p1, p2 }, $EXTENSION_CLASS->offlineVideoEndpointOnClick(${EXTENSION_PROTOCOL_BUFFER_INTERFACE}Ljava/util/Map;)Z - move-result v0 - if-eqz v0, :show_native_downloader + invoke-static/range { v$videoIdRegister .. v$videoIdRegister }, $EXTENSION_CLASS->offlineVideoEndpointOnClick(Ljava/lang/String;)Z + move-result v1 + if-eqz v1, :show_native_downloader return-void :show_native_downloader nop diff --git a/patches/src/main/kotlin/app/morphe/patches/music/interaction/downloads/Fingerprints.kt b/patches/src/main/kotlin/app/morphe/patches/music/interaction/downloads/Fingerprints.kt index ad5db8dca4..050f1945b4 100644 --- a/patches/src/main/kotlin/app/morphe/patches/music/interaction/downloads/Fingerprints.kt +++ b/patches/src/main/kotlin/app/morphe/patches/music/interaction/downloads/Fingerprints.kt @@ -9,8 +9,10 @@ package app.morphe.patches.music.interaction.downloads import app.morphe.patcher.Fingerprint import app.morphe.patcher.anyInstruction +import app.morphe.patcher.methodCall import app.morphe.patcher.string import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode internal object CommandResolverFingerprint : Fingerprint( accessFlags = listOf(AccessFlags.PUBLIC, AccessFlags.STATIC, AccessFlags.FINAL), @@ -27,9 +29,17 @@ internal object OfflineVideoEndpointFingerprint : Fingerprint( accessFlags = listOf(AccessFlags.PUBLIC, AccessFlags.FINAL), returnType = "V", parameters = listOf("L", "Ljava/util/Map;"), + strings = listOf("Object is not an offlineable video: %s"), filters = listOf( - anyInstruction( - string("Object is not an offlineable video: %s") + methodCall( + opcode = Opcode.INVOKE_STATIC, + parameters = listOf("Ljava/lang/Object;"), + returnType = "L" + ), + methodCall( + opcodes = listOf(Opcode.INVOKE_INTERFACE, Opcode.INVOKE_VIRTUAL), + parameters = listOf("Ljava/lang/String;", "L", "L", "L"), + returnType = "V" ) ) ) From 011589657684ba34a7523f3b17af152c5962ae6e Mon Sep 17 00:00:00 2001 From: 0xrxL <127248639+0xrxL@users.noreply.github.com> Date: Sat, 24 Oct 2026 02:17:13 +0200 Subject: [PATCH 020/189] fix(YouTube - Navigation bar): System navigation buttons cannot be seen in the Shorts player with the light theme --- .../extension/youtube/patches/NavigationBarPatch.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/NavigationBarPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/NavigationBarPatch.java index ae1da742d3..278775e6c0 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/NavigationBarPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/NavigationBarPatch.java @@ -15,6 +15,8 @@ import android.app.Activity; import android.content.Context; import android.content.Intent; +import android.graphics.Color; +import android.graphics.drawable.ColorDrawable; import android.net.Uri; import android.os.Build; import android.view.MenuItem; @@ -373,6 +375,14 @@ public static void setShortsPlayerBottomInset(View shortsFragmentLayout) { view.post(() -> view.setPadding(view.getPaddingLeft(), view.getPaddingTop(), view.getPaddingRight(), padding)); } + + // The room is drawn with the background of the layout, which follows the app theme. + // The system navigation buttons over the Shorts player are light in both themes, + // so with the light theme they cannot be seen. The room is black like the Shorts player. + if (!(view.getBackground() instanceof ColorDrawable background) + || background.getColor() != Color.BLACK) { + view.setBackgroundColor(Color.BLACK); + } } catch (Exception ex) { Logger.printException(() -> "setShortsPlayerBottomInset layout failure", ex); } From 2576962149ab22aadc16a26853c9b7154b0d9322 Mon Sep 17 00:00:00 2001 From: 0xrxL <127248639+0xrxL@users.noreply.github.com> Date: Sat, 24 Oct 2026 03:33:03 +0200 Subject: [PATCH 021/189] fix(YouTube - Navigation bar): No connection and back online bars are covered by the system navigation buttons --- .../youtube/patches/NavigationBarPatch.java | 108 +++++++++++++++++- .../layout/buttons/navigation/Fingerprints.kt | 22 +++- .../buttons/navigation/NavigationBarPatch.kt | 33 +++++- 3 files changed, 154 insertions(+), 9 deletions(-) diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/NavigationBarPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/NavigationBarPatch.java index 278775e6c0..42ad1b9cd6 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/NavigationBarPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/NavigationBarPatch.java @@ -21,6 +21,7 @@ import android.os.Build; import android.view.MenuItem; import android.view.View; +import android.view.ViewGroup; import android.view.ViewStub; import android.view.WindowInsets; import android.widget.ImageView; @@ -316,18 +317,121 @@ static boolean isKeyboardMarginRemoved() { * Only with the feature flags turned on by {@link #useTranslucentNavigationBar(boolean)}. */ public static int getNavigationBarBottomPadding(View navigationBar, int original) { + int padding = original; try { if (translucentNavigationBarForced && original != 0 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { WindowInsets insets = navigationBar.getRootWindowInsets(); if (insets != null && insets.isVisible(WindowInsets.Type.ime())) { - return Math.min(original, insets.getInsets(WindowInsets.Type.systemBars()).bottom); + padding = Math.min(original, insets.getInsets(WindowInsets.Type.systemBars()).bottom); } } + + padding = setSlimStatusBarRoom(navigationBar, padding); } catch (Exception ex) { Logger.printException(() -> "getNavigationBarBottomPadding failure", ex); } - return original; + return padding; + } + + /** + * The bar shown below the navigation bar when the connection is lost or comes back + * ("No connection", "Back online"). + */ + private static final int SLIM_STATUS_BAR = ResourceUtils.getIdentifier( + ResourceType.ID, "slim_status_bar"); + + private static final int PIVOT_BAR = ResourceUtils.getIdentifier( + ResourceType.ID, "pivot_bar"); + + /** + * The navigation bar, with a method added during patching. + */ + public interface PivotBarAccess { + /** + * Sets the room the navigation bar leaves below itself for the system navigation buttons, + * which is added to its height. + */ + void patch_setSystemButtonsRoom(int room); + } + + /** + * If the slim status bar is shown below the navigation bar. + */ + private static volatile boolean slimStatusBarShown; + + /** + * The room for the system navigation buttons the app last gave the navigation bar. + */ + private static volatile int systemButtonsRoom; + + /** + * When the navigation bar covers the system navigation buttons, the slim status bar is laid out + * below it, behind the buttons, which cover it. While the slim status bar is shown, the room for + * the buttons is left below the slim status bar instead of below the navigation bar. + * Only with the feature flags turned on by {@link #useTranslucentNavigationBar(boolean)}. + * + * @return The room the navigation bar leaves for the system navigation buttons. + */ + private static int setSlimStatusBarRoom(View navigationBar, int room) { + if (!translucentNavigationBarForced || !(navigationBar instanceof PivotBarAccess)) { + return room; + } + + systemButtonsRoom = room; + View slimStatusBar = navigationBar.getParent() instanceof ViewGroup container + ? container.findViewById(SLIM_STATUS_BAR) + : null; + final boolean moveRoom = slimStatusBarShown && slimStatusBar != null && room > 0; + if (slimStatusBar != null) { + // The top padding is the padding of the slim status bar itself. + final int bottomPadding = slimStatusBar.getPaddingTop() + (moveRoom ? room : 0); + if (slimStatusBar.getPaddingBottom() != bottomPadding) { + slimStatusBar.setPadding(slimStatusBar.getPaddingLeft(), slimStatusBar.getPaddingTop(), + slimStatusBar.getPaddingRight(), bottomPadding); + } + } + return moveRoom ? 0 : room; + } + + /** + * Injection point. + *

+ * The window insets are not sent again when the slim status bar is shown or hidden, + * so the room for the system navigation buttons is moved whenever that happens. + * + * @param bottomBarContainer The container of the navigation bar and the slim status bar. + */ + public static void watchSlimStatusBar(View bottomBarContainer) { + if (SLIM_STATUS_BAR == 0 || PIVOT_BAR == 0) return; + + try { + bottomBarContainer.addOnLayoutChangeListener((view, left, top, right, bottom, + oldLeft, oldTop, oldRight, oldBottom) -> { + try { + View slimStatusBar = view.findViewById(SLIM_STATUS_BAR); + final boolean shown = slimStatusBar != null && slimStatusBar.getVisibility() == View.VISIBLE; + if (shown == slimStatusBarShown) return; + slimStatusBarShown = shown; + + if (translucentNavigationBarForced + && view.findViewById(PIVOT_BAR) instanceof PivotBarAccess access) { + View navigationBar = (View) access; + final int room = systemButtonsRoom; + view.post(() -> { + final int navigationBarRoom = setSlimStatusBarRoom(navigationBar, room); + access.patch_setSystemButtonsRoom(navigationBarRoom); + navigationBar.setPadding(navigationBar.getPaddingLeft(), navigationBar.getPaddingTop(), + navigationBar.getPaddingRight(), navigationBarRoom); + }); + } + } catch (Exception ex) { + Logger.printException(() -> "watchSlimStatusBar layout failure", ex); + } + }); + } catch (Exception ex) { + Logger.printException(() -> "watchSlimStatusBar failure", ex); + } } /** diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/buttons/navigation/Fingerprints.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/buttons/navigation/Fingerprints.kt index 012df45d26..eaaf0c9e27 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/buttons/navigation/Fingerprints.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/buttons/navigation/Fingerprints.kt @@ -27,6 +27,8 @@ import app.morphe.patcher.string import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode +internal const val PIVOT_BAR_CLASS = "Lcom/google/android/libraries/youtube/rendering/ui/pivotbar/PivotBar;" + private val SET_VISIBILITY_METHOD_CALL = methodCall( opcode = Opcode.INVOKE_VIRTUAL, smali = "Landroid/view/View;->setVisibility(I)V" @@ -35,7 +37,7 @@ private val SET_VISIBILITY_METHOD_CALL = methodCall( internal object CreatePivotBarFingerprint : Fingerprint( accessFlags = listOf(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR), parameters = listOf( - "Lcom/google/android/libraries/youtube/rendering/ui/pivotbar/PivotBar;", + PIVOT_BAR_CLASS, "Landroid/widget/TextView;", "Ljava/lang/CharSequence;", ), @@ -108,7 +110,7 @@ internal object AutoHideNavigationBarOnDismissMiniplayerFingerprint : Fingerprin ) internal object PivotBarStyleFingerprint : Fingerprint( - definingClass = "Lcom/google/android/libraries/youtube/rendering/ui/pivotbar/PivotBar;", + definingClass = PIVOT_BAR_CLASS, returnType = "V", parameters = listOf("L"), filters = OpcodesFilter.opcodesToFilters( @@ -119,7 +121,7 @@ internal object PivotBarStyleFingerprint : Fingerprint( ) internal object PivotBarChangedFingerprint : Fingerprint( - definingClass = "Lcom/google/android/libraries/youtube/rendering/ui/pivotbar/PivotBar;", + definingClass = PIVOT_BAR_CLASS, name = "onConfigurationChanged", returnType = "V", filters = OpcodesFilter.opcodesToFilters( @@ -154,17 +156,25 @@ internal object TranslucentNavigationBarFeatureFlagsFingerprint : Fingerprint( /** * Sets the bottom padding of the navigation bar to the bottom window inset, the room for * the system navigation buttons when the bar is drawn behind them. The bar height also grows - * by the same value, which is first stored in a field of the bar. + * by the same value, which is first stored in a field of the bar and then added to its height. */ internal object NavigationBarBottomPaddingFingerprint : Fingerprint( filters = listOf( fieldAccess( opcode = Opcode.IPUT, - definingClass = "Lcom/google/android/libraries/youtube/rendering/ui/pivotbar/PivotBar;", + definingClass = PIVOT_BAR_CLASS, type = "I" ), + // Sets the bar height to its content height and the room in the field. + methodCall( + opcode = Opcode.INVOKE_VIRTUAL, + definingClass = PIVOT_BAR_CLASS, + parameters = listOf(), + returnType = "V", + location = MatchAfterImmediately() + ), methodCall( - definingClass = "Lcom/google/android/libraries/youtube/rendering/ui/pivotbar/PivotBar;", + definingClass = PIVOT_BAR_CLASS, name = "setPadding", location = MatchAfterWithin(10) ) diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/buttons/navigation/NavigationBarPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/buttons/navigation/NavigationBarPatch.kt index 32885657b5..f46648bc06 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/buttons/navigation/NavigationBarPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/buttons/navigation/NavigationBarPatch.kt @@ -24,6 +24,7 @@ import app.morphe.patcher.patch.resourcePatch import app.morphe.patcher.util.getFreeRegisterProvider import app.morphe.patcher.util.getReference import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable +import app.morphe.patches.shared.misc.bridge.addBridge import app.morphe.patches.shared.misc.fix.proto.fixProtoLibraryPatch import app.morphe.patches.shared.misc.fix.proto.immutableMethodRef import app.morphe.patches.shared.misc.fix.proto.mutableCopyMethodRef @@ -81,6 +82,9 @@ internal const val EXTENSION_CLASS = private const val EXTENSION_SETTING_INTERFACE = $$"Lapp/morphe/extension/youtube/patches/NavigationBarPatch$SettingsController;" +private const val PIVOT_BAR_ACCESS_INTERFACE = + $$"Lapp/morphe/extension/youtube/patches/NavigationBarPatch$PivotBarAccess;" + private const val EXTENSION_SUBSCRIPTIONS_CLASS = "Lapp/morphe/extension/youtube/patches/ClassicSubscriptionsLayoutPatch;" @@ -274,11 +278,33 @@ val navigationBarPatch = bytecodePatch( } } + val navigationBarBottomPaddingMatches = NavigationBarBottomPaddingFingerprint.matchAll() + + // The room for the system navigation buttons is stored in a field of the bar, which the + // method called right after adds to its height. The extension moves that room below + // the slim status bar while it is shown, so the bar is given a method that sets it. + navigationBarBottomPaddingMatches.first().instructionMatches.let { matches -> + val roomField = matches[0].getFieldAccessed() + val updateHeightMethod = matches[1].getMethodCalled() + + mutableClassDefBy(PIVOT_BAR_CLASS).apply { + interfaces.add(PIVOT_BAR_ACCESS_INTERFACE) + addBridge( + "patch_setSystemButtonsRoom", listOf("I"), "V", 2, + """ + iput p1, p0, $roomField + invoke-virtual { p0 }, $updateHeightMethod + return-void + """ + ) + } + } + // With the keyboard open, the bottom inset also holds the keyboard, so the bar covering // the system navigation buttons grows as tall as the keyboard and covers the page. // The padding is first stored in a field of the bar, and the same register is then passed // to setPadding. Some targets set it in two places. - NavigationBarBottomPaddingFingerprint.matchAll().forEach { match -> + navigationBarBottomPaddingMatches.forEach { match -> match.method.apply { val fieldIndex = match.instructionMatches.first().index val fieldInstruction = getInstruction(fieldIndex) @@ -295,6 +321,11 @@ val navigationBarPatch = bytecodePatch( } } + // The slim status bar ("No connection", "Back online") is laid out below the bar covering + // the system navigation buttons, so the buttons cover it. The room for the buttons is moved + // below it while it is shown. + addBottomBarContainerHook("$EXTENSION_CLASS->watchSlimStatusBar(Landroid/view/View;)V") + // With the translucent navigation bar, the keyboard shrinks the layout that holds the page // and the navigation bar to the top of the keyboard, so the navigation bar is moved above // the keyboard. Without it, the keyboard covers the bottom of the page and the navigation bar From bac5823bdd42beba2dd79f94010e2001b33017f7 Mon Sep 17 00:00:00 2001 From: 0xrxL <127248639+0xrxL@users.noreply.github.com> Date: Sat, 24 Oct 2026 04:30:39 +0200 Subject: [PATCH 022/189] fix(YouTube - Navigation bar): Empty band below the reply bar of the comment replies --- .../youtube/patches/NavigationBarPatch.java | 12 ++++++++ .../components/LayoutComponentsFilter.java | 10 ------- .../layout/buttons/navigation/Fingerprints.kt | 22 ++++++++++++++ .../buttons/navigation/NavigationBarPatch.kt | 30 +++++++++++++++++++ .../layout/hide/general/Fingerprints.kt | 12 -------- .../hide/general/HideLayoutComponentsPatch.kt | 19 ------------ 6 files changed, 64 insertions(+), 41 deletions(-) diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/NavigationBarPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/NavigationBarPatch.java index 42ad1b9cd6..e2f5b1287e 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/NavigationBarPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/NavigationBarPatch.java @@ -254,6 +254,18 @@ public static boolean isTranslucentNavigationBarForced() { return translucentNavigationBarForced; } + /** + * Injection point. + *

+ * With the navigation bar covering the system navigation buttons, the room below the bar fixed + * at the bottom of some pages, such as the reply bar of the comment replies, already holds + * the system navigation buttons. The flag adds the navigation bar height to it as well, so an + * empty band is left between the two bars (https://github.com/MorpheApp/morphe-patches/issues/2300). + */ + public static boolean useFixedBottomBarRoom(boolean original) { + return original && !translucentNavigationBarForced; + } + /** * Injection point. *

diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/components/LayoutComponentsFilter.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/components/LayoutComponentsFilter.java index b8fc93a658..33328d29fb 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/components/LayoutComponentsFilter.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/components/LayoutComponentsFilter.java @@ -678,16 +678,6 @@ public boolean isFiltered(ContextInterface contextInterface, return true; } - /** - * Injection point. - */ - public static boolean disableUIPaddingFeatureFlags(boolean original) { - if (Settings.HIDE_COMPACT_BANNER.get()) { - return false; - } - return original; - } - /** * Injection point. * Called from a different place then the other filters. diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/buttons/navigation/Fingerprints.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/buttons/navigation/Fingerprints.kt index eaaf0c9e27..f60e269aae 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/buttons/navigation/Fingerprints.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/buttons/navigation/Fingerprints.kt @@ -130,6 +130,28 @@ internal object PivotBarChangedFingerprint : Fingerprint( ) ) +/** + * Adds the navigation bar height to the bottom room of the bar fixed at the bottom of some pages, + * such as the reply bar of the comment replies. Only read when the navigation bar covers + * the system navigation buttons. Not present in older targets. + */ +internal object CommentReplyPaddingFeatureFlagFingerprint : Fingerprint( + filters = listOf( + literal(45752241L) + ) +) + +/** + * Changes how the height of the navigation bar and the miniplayer is given to the pages, + * which leaves wrong padding in some pages, such as the incognito search. + * 21.29 and older read it with a single accessor method. + */ +internal object IncognitoSearchPaddingFeatureFlagFingerprint : Fingerprint( + filters = listOf( + literal(45724388L) + ) +) + /** * Feature flags of the translucent navigation bar. 21.29 and older read each of them with a single * accessor method, newer targets read them inline in many classes, sometimes more than once in diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/buttons/navigation/NavigationBarPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/buttons/navigation/NavigationBarPatch.kt index f46648bc06..fa6f936114 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/buttons/navigation/NavigationBarPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/buttons/navigation/NavigationBarPatch.kt @@ -23,6 +23,7 @@ import app.morphe.patcher.patch.bytecodePatch import app.morphe.patcher.patch.resourcePatch import app.morphe.patcher.util.getFreeRegisterProvider import app.morphe.patcher.util.getReference +import app.morphe.patcher.util.matchAllMethodIndicesForEach import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable import app.morphe.patches.shared.misc.bridge.addBridge import app.morphe.patches.shared.misc.fix.proto.fixProtoLibraryPatch @@ -44,6 +45,7 @@ import app.morphe.patches.youtube.misc.navigation.hookNavigationButtonCreated import app.morphe.patches.youtube.misc.navigation.navigationBarHookPatch import app.morphe.patches.youtube.misc.playservice.is_20_26_or_greater import app.morphe.patches.youtube.misc.playservice.is_20_31_or_greater +import app.morphe.patches.youtube.misc.playservice.is_21_07_or_greater import app.morphe.patches.youtube.misc.playservice.is_21_35_or_greater import app.morphe.patches.youtube.misc.playservice.versionCheckPatch import app.morphe.patches.youtube.misc.settings.PreferenceScreen @@ -321,6 +323,34 @@ val navigationBarPatch = bytecodePatch( } } + // With the bar covering the system navigation buttons, a flag adds the navigation bar height + // below the bar fixed at the bottom of some pages, such as the reply bar of the comment + // replies, so an empty band is left between them. The read is followed to its result, + // as the method reuses the register of the flag for another flag right after. + CommentReplyPaddingFeatureFlagFingerprint.matchAllMethodIndicesForEach(requireMatches = false) { index -> + val moveResultIndex = indexOfFirstInstructionOrThrow(index, Opcode.MOVE_RESULT) + val register = getInstruction(moveResultIndex).registerA + + addInstructions( + moveResultIndex + 1, + """ + invoke-static { v$register }, $EXTENSION_CLASS->useFixedBottomBarRoom(Z)Z + move-result v$register + """ + ) + } + + // A flag gives the pages the height of the navigation bar and the miniplayer in another way, + // which leaves wrong padding in some pages, such as the incognito search. It is turned off. + if (is_21_07_or_greater) { + IncognitoSearchPaddingFeatureFlagFingerprint.matchAllMethodIndicesForEach { index -> + val moveResultIndex = indexOfFirstInstructionOrThrow(index, Opcode.MOVE_RESULT) + val register = getInstruction(moveResultIndex).registerA + + addInstruction(moveResultIndex + 1, "const/4 v$register, 0x0") + } + } + // The slim status bar ("No connection", "Back online") is laid out below the bar covering // the system navigation buttons, so the buttons cover it. The room for the buttons is moved // below it while it is shown. diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/general/Fingerprints.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/general/Fingerprints.kt index 412c6eef6f..52d9ffaa68 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/general/Fingerprints.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/general/Fingerprints.kt @@ -930,18 +930,6 @@ internal object HideTimeBarChapterTitleFingerprint : Fingerprint( ) ) -internal object CommentReplyPaddingFeatureFlagFingerprint : Fingerprint( - filters = listOf( - literal(45752241) - ) -) - -internal object IncognitoSearchPaddingFeatureFlagFingerprint : Fingerprint( - filters = listOf( - literal(45724388) - ) -) - /** * Called while the fullscreen "More videos" panel is dragged. * Shows the player controls when the drag goes up while the panel is peeking. diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/general/HideLayoutComponentsPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/general/HideLayoutComponentsPatch.kt index 4ac09b4133..4b3bcb6cf7 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/general/HideLayoutComponentsPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/general/HideLayoutComponentsPatch.kt @@ -52,7 +52,6 @@ import app.morphe.patches.youtube.misc.litho.node.treeNodeElementHookPatch import app.morphe.patches.youtube.misc.navigation.navigationBarHookPatch import app.morphe.patches.youtube.misc.playservice.is_20_26_or_greater import app.morphe.patches.youtube.misc.playservice.is_20_31_or_greater -import app.morphe.patches.youtube.misc.playservice.is_21_07_or_greater import app.morphe.patches.youtube.misc.playservice.is_21_11_or_greater import app.morphe.patches.youtube.misc.playservice.is_21_20_or_greater import app.morphe.patches.youtube.misc.playservice.is_21_25_or_greater @@ -1346,23 +1345,5 @@ val hideLayoutComponentsPatch = bytecodePatch( ) // endregion - - // region disable UI padding feature flags - - if (is_21_07_or_greater) { - listOf( - CommentReplyPaddingFeatureFlagFingerprint, - IncognitoSearchPaddingFeatureFlagFingerprint - ).forEach { fingerprint -> - fingerprint.matchAll().forEach { - it.method.insertLiteralOverride( - it.instructionMatches.first().index, - "$LAYOUT_COMPONENTS_FILTER->disableUIPaddingFeatureFlags(Z)Z" - ) - } - } - } - - // endregion } } From 60489d84d49c85c44b8007af2de39ee6b82147cb Mon Sep 17 00:00:00 2001 From: 0xrxL <127248639+0xrxL@users.noreply.github.com> Date: Sat, 24 Oct 2026 05:10:07 +0200 Subject: [PATCH 023/189] fix(YouTube - Feed card layout): Horizontal shelves lose the card background when their header is scrolled out of the list --- .../youtube/patches/FeedCardLayoutPatch.java | 1769 ++++++++--------- .../layout/feedcards/FeedCardLayoutPatch.kt | 36 +- .../youtube/layout/feedcards/Fingerprints.kt | 18 + 3 files changed, 916 insertions(+), 907 deletions(-) diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FeedCardLayoutPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FeedCardLayoutPatch.java index 8d88d45c6a..a1389ed5c9 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FeedCardLayoutPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FeedCardLayoutPatch.java @@ -312,6 +312,12 @@ public interface ElementItemInterface { GRID_VIDEO_TEMPLATE) : ByteTrieSearch.convertStringsToBytes(CARD_TEMPLATE) ); + /** + * Label of the shelf headers, in the bytes of the converted elements. + */ + private static final ByteTrieSearch shelfHeaderLabelSearch = new ByteTrieSearch( + ByteTrieSearch.convertStringsToBytes(String.valueOf(SHELF_HEADER_LABEL_MARKER)) + ); /** * Thumbnails of the Shorts, the same that the Shorts filter looks for. */ @@ -382,6 +388,22 @@ public void getOutline(View view, Outline outline) { * after the header is scrolled out of the list. */ private static final Map videoShelfRows = new WeakHashMap<>(); + /** + * List items that are horizontal shelves or Shorts carousels with the card layout, found by the converted + * element of the item when the list sets its spacing. Their header, which has the label of the shelf, + * is not mounted while it is scrolled out of the list. The views are recycled, and the list sets + * the spacing of a view again whenever the view shows another element. + */ + private static final Map shelfCardItems = new WeakHashMap<>(); + /** + * The list item whose spacing is set, until its element is known. + */ + private static WeakReference dividerItemView = new WeakReference<>(null); + /** + * Whether the elements are shelves with the card layout, by the bytes of the converted element. + * The arrays are compared by identity. + */ + private static final Map shelfCardElements = Collections.synchronizedMap(new WeakHashMap<>()); /** * Backgrounds of the items of the feed that were not mounted yet when their color was looked up. */ @@ -446,7 +468,45 @@ String getUri() { final long now = System.currentTimeMillis(); if (now - cacheLookupTime > TEMPLATE_LOOKUP_INTERVAL_MILLIS) { cacheLookupTime = now; - String cached = findCachedTemplateUri(name); + // The uri of a template includes the hash of its version, so it's taken from the template + // cache of the app, which has the templates the app received. The last cached version + // of the template is used, or none if the template is not cached. + String cached = null; + try { + File metadata = new File(Utils.getContext().getFilesDir(), "ElementsResourceCacheMetadata"); + if (metadata.isFile()) { + CodedInputStream input = CodedInputStream.newInstance( + Files.readAllBytes(metadata.toPath())); + while (!input.isAtEnd()) { + final int tag = input.readTag(); + if (WireFormat.getTagWireType(tag) != WireFormat.WIRETYPE_LENGTH_DELIMITED) { + input.skipField(tag); + continue; + } + + CodedInputStream entry = CodedInputStream.newInstance(input.readByteArray()); + while (!entry.isAtEnd()) { + final int entryTag = entry.readTag(); + if (WireFormat.getTagFieldNumber(entryTag) == 1 + && WireFormat.getTagWireType(entryTag) + == WireFormat.WIRETYPE_LENGTH_DELIMITED) { + String key = entry.readString(); + if (isTemplate(key, name)) { + cached = key; + } + } else { + entry.skipField(entryTag); + } + } + } + + String found = cached; + Logger.printDebug(() -> "Cached template: " + found); + } + } catch (Exception ex) { + cached = null; + Logger.printException(() -> "getUri failure", ex); + } // An element of the template can be seen while the cache is read. if (uri == null) { uri = cached; @@ -455,48 +515,6 @@ String getUri() { } return uri; } - - /** - * Takes the uri and the fields from the element, if it's an element of the template. - * After the first element, the elements are checked once in a while, to follow a new version. - */ - void update(byte[] bytes) throws IOException { - final long now = System.currentTimeMillis(); - if (fromElement && now - checkTime < TEMPLATE_CHECK_INTERVAL_MILLIS) { - return; - } - if (!search.matches(bytes)) { - return; - } - - ComponentType componentType = Element.parseFrom(bytes).getType().getComponentType(); - String elementUri = componentType.getTemplateConfig().getUriTemplateConfig().getUri(); - if (!isTemplate(elementUri, name)) { - return; - } - checkTime = now; - - SubscriptionConfig config = componentType.getSubscriptionConfig(); - if (config.hasThemeSubscriptionConfig()) { - ThemeSubscriptionConfig theme = config.getThemeSubscriptionConfig(); - if (theme.getModelField() == modelField && theme.getMapping().getResultField() > 0) { - themeField = theme.getMapping().getResultField(); - } - } - if (config.hasCapabilitiesSubscriptionConfig()) { - CapabilitiesSubscriptionConfig capabilities = config.getCapabilitiesSubscriptionConfig(); - if (capabilities.getModelField() == modelField && capabilities.getMapping().getResultField() > 0) { - capabilitiesField = capabilities.getMapping().getResultField(); - } - } - - if (!elementUri.equals(uri)) { - Logger.printDebug(() -> "Template of an element: " + elementUri + " theme field: " + themeField - + " capabilities field: " + capabilitiesField); - } - uri = elementUri; - fromElement = true; - } } /** @@ -516,39 +534,50 @@ private static final class Icons { this.save = save; this.saved = saved; } - - static Icons get() { - if (ResourceUtils.getDrawableIdentifier("yt_delhi_thumbs_up_not_filled_24dp") != 0 - && ResourceUtils.getDrawableIdentifier("yt_delhi_comment_24dp") != 0 - && ResourceUtils.getDrawableIdentifier("yt_delhi_bookmark_not_filled_24dp") != 0) { - return new Icons( - "yt_delhi_thumbs_up_not_filled_24dp", - "yt_delhi_thumbs_up_filled_24dp", - "yt_delhi_comment_24dp", - "yt_delhi_bookmark_not_filled_24dp", - "yt_delhi_bookmark_filled_24dp" - ); - } - - return new Icons( - "yt_outline_thumb_up_black_24", - "yt_fill_thumb_up_black_24", - "yt_outline_message_bubble_left_black_24", - "yt_outline_bookmark_black_24", - "yt_fill_bookmark_black_24" - ); - } } - /** * Injection point. */ public static byte[] setFeedCardLayout(byte[] bytes) { try { if (!elementSearch.matches(bytes)) { - // The elements of the layout of the setting give the current version of its template. - (CARD_LAYOUT ? CARD : CLASSIC).update(bytes); + // The elements of the layout of the setting give the current version of its template: + // the uri and the fields are taken from the element, if it's an element of the template. + // After the first element, the elements are checked once in a while, to follow a new version. + VideoTemplate template = CARD_LAYOUT ? CARD : CLASSIC; + final long now = System.currentTimeMillis(); + if ((!template.fromElement || now - template.checkTime >= TEMPLATE_CHECK_INTERVAL_MILLIS) + && template.search.matches(bytes)) { + ComponentType componentType = Element.parseFrom(bytes).getType().getComponentType(); + String elementUri = componentType.getTemplateConfig().getUriTemplateConfig().getUri(); + if (isTemplate(elementUri, template.name)) { + template.checkTime = now; + + SubscriptionConfig config = componentType.getSubscriptionConfig(); + if (config.hasThemeSubscriptionConfig()) { + ThemeSubscriptionConfig theme = config.getThemeSubscriptionConfig(); + if (theme.getModelField() == template.modelField + && theme.getMapping().getResultField() > 0) { + template.themeField = theme.getMapping().getResultField(); + } + } + if (config.hasCapabilitiesSubscriptionConfig()) { + CapabilitiesSubscriptionConfig capabilities = config.getCapabilitiesSubscriptionConfig(); + if (capabilities.getModelField() == template.modelField + && capabilities.getMapping().getResultField() > 0) { + template.capabilitiesField = capabilities.getMapping().getResultField(); + } + } + + if (!elementUri.equals(template.uri)) { + Logger.printDebug(() -> "Template of an element: " + elementUri + " theme field: " + + template.themeField + " capabilities field: " + template.capabilitiesField); + } + template.uri = elementUri; + template.fromElement = true; + } + } return bytes; } @@ -560,20 +589,529 @@ public static byte[] setFeedCardLayout(byte[] bytes) { ComponentType converted = null; if (CARD_LAYOUT) { if (isTemplate(uri, CLASSIC_TEMPLATE) && model.hasVideoWithContextModel()) { - converted = convertToCard(componentType, model.getVideoWithContextModel()); + VideoWithContextModel classic = model.getVideoWithContextModel(); + toCard: { + VideoWithContextData data = classic.getData(); + VideoData videoData = data.getVideoData(); + VideoThumbnail thumbnail = videoData.getThumbnail(); + + // The compact items are rows of lists, such as the videos of a playlist or the 'Downloads' row + // of the library. The cards have no attachment, such as the description and the 'Buy or rent' + // button of the movies. The horizontal items, such as the search results on tablets, keep their + // layout. + if (data.getCompact() || data.getHorizontal() || !data.getAttachment().isEmpty()) { + break toCard; + } + + // The filters find the hidden Shorts and live streams by the classic layout. + if (thumbnail.getBadgeStyle() == THUMBNAIL_BADGE_STYLE_LIVE + && Settings.HIDE_LIVE_STREAMS.get()) { + break toCard; + } + if ((Settings.HIDE_SHORTS_HOME.get() || Settings.HIDE_SHORTS_SUBSCRIPTIONS.get() + || Settings.HIDE_SHORTS_SEARCH.get() || Settings.HIDE_SHORTS_HISTORY.get()) + && shortsThumbnailSearch.matches(thumbnail.getImage().toByteArray())) { + // The elements are parsed when the feed that shows them is loaded, so the Shorts keep the + // classic layout only in the feed whose setting hides them, the same feeds of the Shorts + // filter, and are cards in the others. + final boolean shortsHidden; + if (PlayerType.getCurrent().isMaximizedOrFullscreen()) { + // The Shorts below the player are treated as the Shorts of the Home feed. + shortsHidden = Settings.HIDE_SHORTS_HOME.get(); + } else if (NavigationBar.isSearchBarActive()) { + // Must check second, as search can be from any tab. + shortsHidden = Settings.HIDE_SHORTS_SEARCH.get(); + } else { + NavigationBar.NavigationButton selectedNavButton = + NavigationBar.NavigationButton.getSelectedNavigationButton(); + if (selectedNavButton == null) { + // Unknown tab, treat the same as home. + shortsHidden = Settings.HIDE_SHORTS_HOME.get(); + } else { + shortsHidden = switch (selectedNavButton) { + case HOME -> Settings.HIDE_SHORTS_HOME.get(); + case SEARCH -> Settings.HIDE_SHORTS_SEARCH.get(); + case SUBSCRIPTIONS -> Settings.HIDE_SHORTS_SUBSCRIPTIONS.get(); + case LIBRARY -> Settings.HIDE_SHORTS_HISTORY.get(); + default -> false; + }; + } + } + if (shortsHidden) { + break toCard; + } + } + + String templateUri = CARD.getUri(); + if (templateUri == null ||!videoData.getLockupMetadata().hasLockupMetadataViewModel()) { + break toCard; + } + + // Playlists open their page, and show the number of videos instead of the duration. + InnertubeCommandHolder onTap = InnertubeCommandHolder.parseFrom(data.getOnTap()); + final boolean isPlaylist = onTap.getInnertubeCommand().hasBrowseEndpoint(); + String contentId; + String badgeText; + String badgeLabel; + if (isPlaylist) { + String browseId = onTap.getInnertubeCommand().getBrowseEndpoint().getBrowseId(); + if (!browseId.startsWith(PLAYLIST_BROWSE_ID_PREFIX) + || thumbnail.getVideoCountText().isEmpty()) { + break toCard; + } + contentId = browseId.substring(PLAYLIST_BROWSE_ID_PREFIX.length()); + badgeText = thumbnail.getVideoCountText(); + badgeLabel = badgeText; + } else { + Matcher videoId = VIDEO_ID_PATTERN.matcher( + thumbnail.getImage().toString(StandardCharsets.ISO_8859_1)); + contentId = videoId.find() ? videoId.group(1) : null; + if (contentId == null) { + break toCard; + } + badgeText = thumbnail.getTimestampText(); + badgeLabel = thumbnail.getTimestampAccessibilityText(); + } + + ThumbnailViewModel.Builder thumbnailViewModel = ThumbnailViewModel.newBuilder() + .setImage(thumbnail.getImage()); + if (!badgeText.isEmpty()) { + ThumbnailBadgeViewModel.Builder badge = ThumbnailBadgeViewModel.newBuilder() + .setText(badgeText) + .setBadgeStyle(THUMBNAIL_BADGE_STYLE_DEFAULT) + .setRendererContext(RendererContext.newBuilder() + .setAccessibilityContext(AccessibilityContext.newBuilder() + .setLabel(badgeLabel))); + // The badge of the videos that are live now is red, with the live icon of the classic + // layout. + if (!isPlaylist && thumbnail.getBadgeStyle() == THUMBNAIL_BADGE_STYLE_LIVE) { + badge.setBadgeStyle(THUMBNAIL_BADGE_STYLE_LIVE); + if (!thumbnail.getBadgeIcon().isEmpty()) { + badge.setIcon(ThumbnailBadgeIcon.newBuilder() + .addSources(ThumbnailBadgeIconSource.newBuilder() + .setClientResource(ClientResource.newBuilder() + .setImageName(thumbnail.getBadgeIcon())))); + } + } + + thumbnailViewModel.addOverlays(ThumbnailOverlay.newBuilder() + .setThumbnailBottomOverlayViewModel(ThumbnailBottomOverlayViewModel.newBuilder() + .addBadges(ThumbnailBadge.newBuilder().setThumbnailBadgeViewModel(badge)))); + } + + // The card thumbnail shows only the badges of the bottom overlays, + // so the watch progress is drawn by the bottom overlay template in an element over the + // thumbnail. + final float progress = thumbnail.getProgress(); + if (SeekbarColorPatch.showWatchHistoryProgressDrawable(progress > 0)) { + String bottomOverlayUri = THUMBNAIL_BOTTOM_OVERLAY.getUri(); + if (bottomOverlayUri != null) { + ThumbnailBottomOverlayViewModel progressBar = + ThumbnailBottomOverlayViewModel.newBuilder() + .setProgressBar(ThumbnailOverlayProgressBar.newBuilder() + .setThumbnailOverlayProgressBarViewModel( + ThumbnailOverlayProgressBarViewModel.newBuilder() + .setProgress(progress) + .setStyle(THUMBNAIL_PROGRESS_BAR_STYLE_EDGE))) + .build(); + ComponentType progressComponent = ComponentType.newBuilder() + .setTemplateConfig(templateConfig(bottomOverlayUri)) + .setModel(ElementModel.newBuilder().setLockupModel(LockupModel.newBuilder() + .setViewModel(LockupViewModelHolder.newBuilder() + .setThumbnailBottomOverlayViewModel(progressBar)) + .setContext(classic.getContext()))) + .setSubscriptionConfig(subscriptionConfig( + componentType.getSubscriptionConfig(), THUMBNAIL_BOTTOM_OVERLAY)) + .build(); + thumbnailViewModel.addOverlays(ThumbnailOverlay.newBuilder() + .setThumbnailElementOverlay(ThumbnailElementOverlay.newBuilder() + .setElement(Element.newBuilder() + .setType(ElementType.newBuilder() + .setComponentType(progressComponent))))); + } + } + + LockupMetadataViewModel.Builder metadata = videoData.getLockupMetadata() + .getLockupMetadataViewModel().toBuilder() + .setTitleStyle(TextStyle.newBuilder().setStyle(CARD_TITLE_STYLE)); + // The classic layout shows the channel, the views and the date in one text separated by spaces, + // and the cards show the channel above the views and the date. + splitRows: { + ContentMetadataViewModel viewModel = metadata.getMetadata().getContentMetadataViewModel(); + MetadataPart part = null; + for (MetadataRow row : viewModel.getMetadataRowsList()) { + if (row.getMetadataPartsCount() > 0) { + if (part != null || row.getMetadataPartsCount() > 1) { + break splitRows; // Already split. + } + part = row.getMetadataParts(0); + } + } + if (part == null) { + break splitRows; + } + + AttributedText text = part.getText(); + String content = text.getContent(); + Matcher separator = METADATA_SEPARATOR_PATTERN.matcher(content); + if (!separator.find()) { + break splitRows; + } + + // The views icon is an attachment on the spaces before the views. + final int channelEnd = separator.start(); + int secondRowStart = separator.end(); + for (AttachmentRun run : text.getAttachmentRunsList()) { + final int start = run.getStartIndex(); + if (start >= channelEnd && start < secondRowStart) { + secondRowStart = start; + } + } + + AttributedText.Builder secondRowText = AttributedText.newBuilder() + .setContent(content.substring(secondRowStart)); + for (AttachmentRun run : text.getAttachmentRunsList()) { + if (run.getStartIndex() >= secondRowStart) { + secondRowText.addAttachmentRuns(run.toBuilder() + .setStartIndex(run.getStartIndex() - secondRowStart)); + } + } + + // The accessibility label is 'Channel, views, date'. + String label = part.getAccessibilityLabel(); + final int labelSeparator = label.indexOf(", "); + + metadata.setMetadata(ContentMetadata.newBuilder() + .setContentMetadataViewModel(viewModel.toBuilder() + .clearMetadataRows() + .addMetadataRows(MetadataRow.newBuilder() + .addMetadataParts(MetadataPart.newBuilder() + .setText(AttributedText.newBuilder() + .setContent(content.substring(0, channelEnd))))) + .addMetadataRows(MetadataRow.newBuilder() + .addMetadataParts(MetadataPart.newBuilder() + .setText(secondRowText) + .setAccessibilityLabel(labelSeparator < 0 + ? label + : label.substring(labelSeparator + 2)))) + .setDelimiter(" ")) + .build()); + } + + // The images of the classic layout have the same format of the images of the cards. + if (videoData.hasDecoratedAvatar()) { + metadata.setImage(videoData.getDecoratedAvatar()); + } else if (videoData.hasAvatarStack()) { + metadata.setImage(videoData.getAvatarStack()); + } else if (videoData.hasAvatar()) { + ChannelAvatar avatar = videoData.getAvatar(); + metadata.setImage(LockupImage.newBuilder() + .setDecoratedAvatarViewModel(DecoratedAvatarViewModel.newBuilder() + .setAvatar(AvatarViewModelHolder.newBuilder() + .setAvatarViewModel(AvatarViewModel.newBuilder() + .setImage(avatar.getImage()) + .setAvatarImageSize(avatar.getAvatarImageSize()))) + .setA11YLabel(avatar.getAccessibilityText()) + .setRendererContext(RendererContext.newBuilder() + .setCommandContext(CommandContext.newBuilder() + .setOnTap(avatar.getEndpoint()))))); + } + + InnertubeCommandHolder menuOnTap = InnertubeCommandHolder.parseFrom(data.getMenuOnTap()); + metadata.setMenuButton(ButtonViewModelHolder.newBuilder() + .setButtonViewModel(button("MORE_VERT", data.getMenuAccessibilityText(), menuOnTap))); + + // The comment button opens the video with the comments, which are opened when the watch page + // is loaded, and the save button saves the video to 'Watch later'. + if (ACTION_BUTTONS && !isPlaylist) { + if (icons == null) { + // The bold icons are not available on older versions of the app. + if (ResourceUtils.getDrawableIdentifier("yt_delhi_thumbs_up_not_filled_24dp") != 0 + && ResourceUtils.getDrawableIdentifier("yt_delhi_comment_24dp") != 0 + && ResourceUtils.getDrawableIdentifier("yt_delhi_bookmark_not_filled_24dp") + != 0) { + icons = new Icons( + "yt_delhi_thumbs_up_not_filled_24dp", + "yt_delhi_thumbs_up_filled_24dp", + "yt_delhi_comment_24dp", + "yt_delhi_bookmark_not_filled_24dp", + "yt_delhi_bookmark_filled_24dp" + ); + } else { + icons = new Icons( + "yt_outline_thumb_up_black_24", + "yt_fill_thumb_up_black_24", + "yt_outline_message_bubble_left_black_24", + "yt_outline_bookmark_black_24", + "yt_fill_bookmark_black_24" + ); + } + } + + // The command of the 'Save to Watch later' item of the menu. + InnertubeCommandHolder saveOnTap = null; + List message = ProtoNode.parse(data.getMenuOnTap().toByteArray()); + if (message != null) { + for (ProtoNode endpoint + : ProtoNode.findMessages(message, PLAYLIST_EDIT_ENDPOINT_FIELD)) { + ProtoNode playlistId = ProtoNode.field( + endpoint.children, PLAYLIST_EDIT_ENDPOINT_PLAYLIST_ID_FIELD); + ProtoNode command = endpoint.getParent(); + if (playlistId != null && WATCH_LATER_PLAYLIST_ID.equals(playlistId.getText()) + && command != null && command.children != null) { + ByteArrayOutputStream commandBytes = new ByteArrayOutputStream(); + CodedOutputStream output = CodedOutputStream.newInstance(commandBytes); + output.writeByteArray(INNERTUBE_COMMAND_FIELD, + ProtoNode.write(command.children)); + output.flush(); + saveOnTap = InnertubeCommandHolder.parseFrom(commandBytes.toByteArray()); + break; + } + } + } + if (saveOnTap == null) { + // Without the item, such as in some menus, the button opens the menu. + saveOnTap = InnertubeCommandHolder.parseFrom(data.getMenuOnTap()); + } + + String entityKey = Base64.encodeToString(LikeStatusEntityKey.newBuilder() + .setVideoId(contentId) + .setEntityType(LIKE_STATUS_ENTITY_TYPE) + .setVersion(1) + .build() + .toByteArray(), + Base64.URL_SAFE | Base64.NO_WRAP).replace("=", "%3D"); + + metadata.addActionButtons(ActionButton.newBuilder() + .setLikeButtonViewModel(LikeButtonViewModel.newBuilder() + .setToggleButtonViewModel(toggleButton( + button(icons.like, "", + likeCommand(contentId, LIKE_STATUS_LIKE)), + button(icons.liked, "", + likeCommand(contentId, LIKE_STATUS_INDIFFERENT)))) + .setLikeStatusEntityKey(entityKey) + .setLikeStatusEntity(LikeStatusEntity.newBuilder() + .setKey(entityKey) + .setLikeStatus(LIKE_STATUS_INDIFFERENT)) + .setLikeCountEnabled(1))) + .addActionButtons(ActionButton.newBuilder() + .setButtonViewModel(button(icons.comment, + str("morphe_feed_card_layout_comments") + + COMMENT_BUTTON_LABEL_MARKER, + InnertubeCommandHolder.parseFrom(data.getOnTap())))) + .addActionButtons(ActionButton.newBuilder() + .setAddToPlaylistButtonViewModel(AddToPlaylistButtonViewModel.newBuilder() + .setToggleButtonViewModel(toggleButton( + button(icons.save, "", saveOnTap), + button(icons.saved, "", saveOnTap))))); + } + + LockupViewModel.Builder lockupViewModel = LockupViewModel.newBuilder() + .setContentImage(ContentImage.newBuilder().setThumbnailViewModel(thumbnailViewModel)) + .setMetadata(LockupMetadata.newBuilder().setLockupMetadataViewModel(metadata)) + .setContentId(contentId) + .setContentType(isPlaylist ? LOCKUP_CONTENT_TYPE_PLAYLIST : LOCKUP_CONTENT_TYPE_VIDEO) + .setRendererContext(RendererContext.newBuilder() + .setAccessibilityContext(AccessibilityContext.newBuilder() + .setLabel(data.getAccessibilityText() + CARD_LABEL_MARKER)) + .setCommandContext(CommandContext.newBuilder().setOnTap(data.getOnTap()))); + // The cards play the video inline (feed autoplay) with the same commands of the classic layout. + if (!data.getInlinePlayback().isEmpty()) { + lockupViewModel.setInlinePlayback(LockupInlinePlayback.newBuilder() + .setInlinePlayback(data.getInlinePlayback())); + } + + LockupModel card = LockupModel.newBuilder() + .setViewModel(LockupViewModelHolder.newBuilder().setLockupViewModel(lockupViewModel)) + .setContext(setCardExperiments(classic.getContext(), true)) + .setEnvironment(classic.getEnvironment()) + .setPadding(LockupPadding.newBuilder() + .setStart(CARD_HORIZONTAL_PADDING_DP) + .setEnd(CARD_HORIZONTAL_PADDING_DP) + .setTop(CARD_VERTICAL_PADDING_DP) + .setBottom(CARD_VERTICAL_PADDING_DP)) + .build(); + + converted = componentType.toBuilder() + .setTemplateConfig(templateConfig(templateUri)) + .setModel(ElementModel.newBuilder().setLockupModel(card)) + .setSubscriptionConfig(subscriptionConfig(componentType.getSubscriptionConfig(), CARD)) + .build(); + } } else if (isTemplate(uri, SHELF_HEADER_TEMPLATE) && model.hasShelfHeaderModel()) { - converted = setModernShelfHeader(componentType, model.getShelfHeaderModel()); + // The shelves of the card layout, such as the Shorts shelf, have the modern header, + // with a bigger title and without the icon. + // The template shows it if the server enables it and the header asks for it. + ShelfHeaderModel header = model.getShelfHeaderModel(); + ShelfHeaderData data = header.getData(); + if (!data.getIcon().toString(StandardCharsets.ISO_8859_1).contains(SHORTS_SHELF_ICON)) { + // The other shelf headers keep their own style, and are only found in the lists by their label, + // to draw a card behind the videos below them. + converted = componentType.toBuilder() + .setModel(ElementModel.newBuilder().setShelfHeaderModel(header.toBuilder() + .setData(data.toBuilder() + .setTitle(data.getTitle() + VIDEO_SHELF_HEADER_LABEL_MARKER) + .setAccessibilityText(data.getAccessibilityText() + + VIDEO_SHELF_HEADER_LABEL_MARKER)))) + .build(); + } else { + converted = componentType.toBuilder() + .setModel(ElementModel.newBuilder().setShelfHeaderModel(header.toBuilder() + .setData(modernShelfHeader(data)))) + .build(); + } } else if (isTemplate(uri, HORIZONTAL_SHELF_TEMPLATE) && model.hasHorizontalShelfModel()) { - converted = setHorizontalShelfCard(componentType, model.getHorizontalShelfModel()); + // The horizontal shelves of videos have the modern header, like the Shorts shelf, + // and the shelf is a card with the header and the videos. + HorizontalShelfModel shelf = model.getHorizontalShelfModel(); + HorizontalShelfData data = shelf.getData(); + if (data.hasHeader()) { + converted = componentType.toBuilder() + .setModel(ElementModel.newBuilder().setHorizontalShelfModel(shelf.toBuilder() + .setData(data.toBuilder() + .setHeader(modernShelfHeader(data.getHeader()))))) + .build(); + } } else if (isTemplate(uri, SHORTS_VIDEO_CELL_TEMPLATE) && model.hasShortsVideoCellModel()) { - converted = setShortsCellLabel(componentType, model.getShortsVideoCellModel()); + // The label of the Shorts ends with an invisible character, to find the Shorts shelf in the lists. + ShortsVideoCellModel cell = model.getShortsVideoCellModel(); + ShortsVideoCellData data = cell.getData(); + converted = componentType.toBuilder() + .setModel(ElementModel.newBuilder().setShortsVideoCellModel(cell.toBuilder() + .setData(data.toBuilder() + .setAccessibilityText(data.getAccessibilityText() + + SHORTS_CELL_LABEL_MARKER)))) + .build(); } else if (isTemplate(uri, SHORTS_SHELF_TEMPLATE) && model.hasShortsShelfModel()) { - converted = setShortsShelfCard(componentType, model.getShortsShelfModel()); + // The shelves of Shorts in a carousel, such as in the search results, have the modern header + // like the other shelves, and the shelf is a card with the header and the Shorts. + ShortsShelfModel shelf = model.getShortsShelfModel(); + if (shelf.hasHeader()) { + converted = componentType.toBuilder() + .setModel(ElementModel.newBuilder().setShortsShelfModel(shelf.toBuilder() + .setHeader(modernShelfHeader(shelf.getHeader())))) + .build(); + } } else if (isTemplate(uri, GRID_VIDEO_TEMPLATE) && model.hasGridVideoModel()) { - converted = setVideoShelfRowLabel(componentType, model.getGridVideoModel()); + // The label of the videos of the vertical shelves ends with an invisible character, + // to draw the shelf below the header as a card. + GridVideoModel video = model.getGridVideoModel(); + converted = componentType.toBuilder() + .setModel(ElementModel.newBuilder().setGridVideoModel(video.toBuilder() + .setAccessibilityText(video.getAccessibilityText() + VIDEO_SHELF_ROW_LABEL_MARKER))) + .build(); } } else if (isTemplate(uri, CARD_TEMPLATE) && model.hasLockupModel()) { - converted = convertToClassic(componentType, model.getLockupModel()); + LockupModel card = model.getLockupModel(); + toClassic: { + LockupViewModel lockupViewModel = card.getViewModel().getLockupViewModel(); + if (lockupViewModel.getContentType() != LOCKUP_CONTENT_TYPE_VIDEO + || !lockupViewModel.getContentImage().hasThumbnailViewModel()) { + break toClassic; + } + + String templateUri = CLASSIC.getUri(); + if (templateUri == null) { + break toClassic; + } + + ThumbnailViewModel thumbnailViewModel = lockupViewModel.getContentImage().getThumbnailViewModel(); + VideoThumbnail.Builder thumbnail = VideoThumbnail.newBuilder() + .setImage(thumbnailViewModel.getImage()); + for (ThumbnailOverlay overlay : thumbnailViewModel.getOverlaysList()) { + for (ThumbnailBadge badge : overlay.getThumbnailBottomOverlayViewModel().getBadgesList()) { + ThumbnailBadgeViewModel badgeViewModel = badge.getThumbnailBadgeViewModel(); + if (!badgeViewModel.getText().isEmpty()) { + thumbnail.setTimestampText(badgeViewModel.getText()) + .setTimestampAccessibilityText(badgeViewModel.getRendererContext() + .getAccessibilityContext().getLabel()); + } + } + } + + LockupMetadataViewModel metadata = lockupViewModel.getMetadata().getLockupMetadataViewModel(); + // Joins the metadata rows of a card into one text, like the classic layout. + ContentMetadata rows = metadata.getMetadata(); + ContentMetadataViewModel viewModel = rows.getContentMetadataViewModel(); + if (viewModel.getMetadataRowsCount() >= 2) { + + StringBuilder content = new StringBuilder(); + StringBuilder label = new StringBuilder(); + AttributedText.Builder text = AttributedText.newBuilder(); + for (MetadataRow row : viewModel.getMetadataRowsList()) { + for (MetadataPart part : row.getMetadataPartsList()) { + String partContent = part.getText().getContent(); + if (partContent.isEmpty()) { + continue; + } + if (content.length() > 0) { + content.append(" "); + } + final int offset = content.length(); + content.append(partContent); + for (AttachmentRun run : part.getText().getAttachmentRunsList()) { + text.addAttachmentRuns(run.toBuilder().setStartIndex(run.getStartIndex() + offset)); + } + + String partLabel = part.getAccessibilityLabel().isEmpty() + ? partContent.trim() + : part.getAccessibilityLabel(); + if (label.length() > 0) { + label.append(", "); + } + label.append(partLabel); + } + } + + rows = ContentMetadata.newBuilder() + .setContentMetadataViewModel(viewModel.toBuilder() + .clearMetadataRows() + .addMetadataRows(MetadataRow.newBuilder() + .addMetadataParts(MetadataPart.newBuilder() + .setText(text.setContent(content.toString())) + .setAccessibilityLabel(label.toString()))) + .clearDelimiter()) + .build(); + } + VideoData.Builder videoData = VideoData.newBuilder() + .setThumbnail(thumbnail) + .setLockupMetadata(LockupMetadata.newBuilder() + .setLockupMetadataViewModel(metadata.toBuilder() + .setMetadata(rows) + .clearImage() + .clearMenuButton() + .clearTitleStyle() + .clearActionButtons())); + if (metadata.getImage().hasDecoratedAvatarViewModel()) { + DecoratedAvatarViewModel avatar = metadata.getImage().getDecoratedAvatarViewModel(); + AvatarViewModel avatarViewModel = avatar.getAvatar().getAvatarViewModel(); + videoData.setAvatar(ChannelAvatar.newBuilder() + .setImage(avatarViewModel.getImage()) + .setAccessibilityText(avatar.getA11YLabel()) + .setAvatarImageSize(avatarViewModel.getAvatarImageSize()) + .setEndpoint(avatar.getRendererContext().getCommandContext().getOnTap())); + } + + ButtonViewModel menuButton = metadata.getMenuButton().getButtonViewModel(); + RendererContext rendererContext = lockupViewModel.getRendererContext(); + VideoWithContextModel classic = VideoWithContextModel.newBuilder() + .setData(VideoWithContextData.newBuilder() + .setVideoData(videoData) + .setAccessibilityText(rendererContext.getAccessibilityContext().getLabel()) + .setMenuAccessibilityText(menuButton.getAccessibilityText()) + .setOnTap(rendererContext.getCommandContext().getOnTap()) + .setMenuOnTap(menuButton.getOnTap().toByteString()) + .setInlinePlayback(lockupViewModel.getInlinePlayback().getInlinePlayback())) + .setContext(setCardExperiments(card.getContext(), false)) + .setEnvironment(card.getEnvironment()) + .build(); + + converted = componentType.toBuilder() + .setTemplateConfig(templateConfig(templateUri)) + .setModel(ElementModel.newBuilder().setVideoWithContextModel(classic)) + .setSubscriptionConfig(subscriptionConfig(componentType.getSubscriptionConfig(), CLASSIC)) + .build(); + } } if (converted == null) { @@ -622,10 +1160,37 @@ public static void onChildAdded(RecyclerView recyclerView, View child) { */ public static int getCardColor(Drawable drawable, int color) { try { - if (CARD_LAYOUT && Color.alpha(color) == 255 && isInCard(drawable)) { - // The theme patch changes only some exact colors, such as the background of the cards. - // The lowest bit of blue is not visible. - return color ^ 1; + if (CARD_LAYOUT && Color.alpha(color) == 255) { + boolean inCard = false; + View view = getDrawableView(drawable); + while (view != null) { + // The card can be drawn before it's added to the list. + CardOutlineProvider outline = getCardOutline(view.getContentDescription()); + if (outline != null) { + setCardOutline(view, outline); + inCard = true; + break; + } + ViewParent parent = view.getParent(); + if (parent instanceof RecyclerView recyclerView) { + if (recyclerView.getId() == getFeedId()) { + // The background is mounted before the content of the item, + // so a bar of chips is found only after the item is mounted. + if (view instanceof ViewGroup group && group.getChildCount() == 0) { + unmountedItemBackgrounds.put(drawable, Boolean.TRUE); + } + inCard = !isChipBar(view); + } + break; + } + view = parent instanceof View parentView ? parentView : null; + } + + if (inCard) { + // The theme patch changes only some exact colors, such as the background of the cards. + // The lowest bit of blue is not visible. + return color ^ 1; + } } } catch (Exception ex) { Logger.printException(() -> "getCardColor failure", ex); @@ -641,9 +1206,20 @@ public static int getCardColor(Drawable drawable, int color) { */ public static Object getDividerItem(@Nullable Object item) { try { - if (CARD_LAYOUT && item instanceof ElementItemInterface element - && isCardElement(element.patch_getElementBytes())) { - return ITEM_WITHOUT_DIVIDER; + if (CARD_LAYOUT && item instanceof ElementItemInterface element) { + byte[] bytes = element.patch_getElementBytes(); + if (bytes != null) { + Boolean card = cardElements.get(bytes); + if (card == null) { + card = CARD.search.matches(bytes) && isCardLabel(Element.parseFrom(bytes).getType() + .getComponentType().getModel().getLockupModel().getViewModel().getLockupViewModel() + .getRendererContext().getAccessibilityContext().getLabel()); + cardElements.put(bytes, card); + } + if (card) { + return ITEM_WITHOUT_DIVIDER; + } + } } } catch (Exception ex) { Logger.printException(() -> "getDividerItem failure", ex); @@ -651,18 +1227,67 @@ && isCardElement(element.patch_getElementBytes())) { return item; } - private static boolean isCardElement(@Nullable byte[] bytes) throws IOException { - if (bytes == null) { - return false; + /** + * Injection point. + * Called when the list sets the spacing of an item, with the view of the item. + */ + public static void setDividerItemView(View view) { + try { + if (!CARD_LAYOUT) { + return; + } + + shelfCardItems.remove(view); + dividerItemView = new WeakReference<>(view); + } catch (Exception ex) { + Logger.printException(() -> "setDividerItemView failure", ex); } - Boolean card = cardElements.get(bytes); - if (card == null) { - card = CARD.search.matches(bytes) && isCardLabel(Element.parseFrom(bytes).getType() - .getComponentType().getModel().getLockupModel().getViewModel().getLockupViewModel() - .getRendererContext().getAccessibilityContext().getLabel()); - cardElements.put(bytes, card); + } + + /** + * Injection point. + * Called with the item of the view of {@link #setDividerItemView(View)}, before the spacing is looked up. + * The item is a shelf with the card layout if the header of its horizontal shelf or Shorts carousel + * has the label of the shelf header. + */ + public static void setDividerItem(@Nullable Object item) { + try { + if (!CARD_LAYOUT) { + return; + } + + View view = dividerItemView.get(); + dividerItemView.clear(); + if (view == null || !(item instanceof ElementItemInterface element)) { + return; + } + byte[] bytes = element.patch_getElementBytes(); + if (bytes == null) { + return; + } + + Boolean shelf = shelfCardElements.get(bytes); + if (shelf == null) { + shelf = false; + // Only the elements with the marker are parsed. + if (shelfHeaderLabelSearch.matches(bytes)) { + ElementModel model = Element.parseFrom(bytes).getType().getComponentType().getModel(); + if (model.hasHorizontalShelfModel()) { + shelf = endsWith(model.getHorizontalShelfModel().getData().getHeader() + .getAccessibilityText(), SHELF_HEADER_LABEL_MARKER); + } else if (model.hasShortsShelfModel()) { + shelf = endsWith(model.getShortsShelfModel().getHeader().getAccessibilityText(), + SHELF_HEADER_LABEL_MARKER); + } + } + shelfCardElements.put(bytes, shelf); + } + if (shelf) { + shelfCardItems.put(view, Boolean.TRUE); + } + } catch (Exception ex) { + Logger.printException(() -> "setDividerItem failure", ex); } - return card; } @Nullable @@ -674,38 +1299,6 @@ private static View getDrawableView(Drawable drawable) { return callback instanceof View view ? view : null; } - private static boolean isInCard(Drawable drawable) { - View view = getDrawableView(drawable); - if (view == null) { - return false; - } - - while (true) { - // The card can be drawn before it's added to the list. - CardOutlineProvider outline = getCardOutline(view.getContentDescription()); - if (outline != null) { - setCardOutline(view, outline); - return true; - } - ViewParent parent = view.getParent(); - if (parent instanceof RecyclerView recyclerView) { - if (recyclerView.getId() != getFeedId()) { - return false; - } - // The background is mounted before the content of the item, - // so a bar of chips is found only after the item is mounted. - if (view instanceof ViewGroup group && group.getChildCount() == 0) { - unmountedItemBackgrounds.put(drawable, Boolean.TRUE); - } - return !isChipBar(view); - } - if (!(parent instanceof View parentView)) { - return false; - } - view = parentView; - } - } - private static void updateCardOutline(View child) { CardOutlineProvider outline = getCardOutline(child.getContentDescription()); // The items of the feed are cards, also the ones that YouTube sends as cards and are not converted. @@ -725,38 +1318,6 @@ private static boolean isChipBar(View item) { return hasNestedList(item, 0) && !isShelfHeader(item); } - /** - * The color of the backgrounds of the items that were not mounted yet is looked up again - * once the item is mounted, if the item is a bar of chips. - */ - private static void updateUnmountedItemBackgrounds() { - if (unmountedItemBackgrounds.isEmpty()) { - return; - } - for (Drawable background : new ArrayList<>(unmountedItemBackgrounds.keySet())) { - View view = getDrawableView(background); - while (view != null && !(view.getParent() instanceof RecyclerView)) { - view = view.getParent() instanceof View parent ? parent : null; - } - if (!(view instanceof ViewGroup item)) { - unmountedItemBackgrounds.remove(background); - continue; - } - if (item.getChildCount() == 0) { - continue; - } - unmountedItemBackgrounds.remove(background); - if (isChipBar(item)) { - // The color is looked up when the bounds change. - Rect bounds = background.copyBounds(); - background.setBounds(0, 0, 0, 0); - background.setBounds(bounds); - background.invalidateSelf(); - updateCardOutline(item); - } - } - } - private static void setCardOutline(View view, @Nullable CardOutlineProvider outline) { ViewOutlineProvider current = view.getOutlineProvider(); if (outline == current) { @@ -807,10 +1368,124 @@ public static void onListDraw(RecyclerView list, Canvas canvas) { try { RecyclerView current = listRef.get(); if (current != null) { - updateUnmountedItemBackgrounds(); - updateLateCardOutlines(current); - updateShortsCellOutlines(current); - updateCardInlinePlayerOutline(current); + // The color of the backgrounds of the items that were not mounted yet is looked up + // again once the item is mounted, if the item is a bar of chips. + if (!unmountedItemBackgrounds.isEmpty()) { + for (Drawable background : new ArrayList<>(unmountedItemBackgrounds.keySet())) { + View view = getDrawableView(background); + while (view != null && !(view.getParent() instanceof RecyclerView)) { + view = view.getParent() instanceof View parent ? parent : null; + } + if (!(view instanceof ViewGroup item)) { + unmountedItemBackgrounds.remove(background); + continue; + } + if (item.getChildCount() == 0) { + continue; + } + unmountedItemBackgrounds.remove(background); + if (isChipBar(item)) { + // The color is looked up when the bounds change. + Rect bounds = background.copyBounds(); + background.setBounds(0, 0, 0, 0); + background.setBounds(bounds); + background.invalidateSelf(); + updateCardOutline(item); + } + } + } + + // The label of a recycled card can be set after the card is laid out, so the card + // keeps the outline of the item it replaced, without the spacing of the cards. + for (int i = 0, count = current.getChildCount(); i < count; i++) { + View item = current.getChildAt(i); + if (item.getOutlineProvider() != CARD_OUTLINE + && isCardLabel(item.getContentDescription())) { + setCardOutline(item, CARD_OUTLINE); + } + } + + // Updates the clips of the Shorts of the Shorts shelves, without drawing the shelves. + shortsCellOutlines.clear(); + for (int i = 0, count = current.getChildCount(); i < count; i++) { + View item = current.getChildAt(i); + if (item.getHeight() > 0 && isShortsRow(item)) { + View above = findItemAbove(current, item); + if (above == null || !isShortsRow(above)) { + drawShortsShelfCard(current, getShortsShelfHeader(above), item, null); + } + } + } + + final boolean feed = current.getId() == getFeedId(); + if (feed) { + // The autoplay of a Short shows the player in a container above the Short, outside + // the list, which clips the player with its own rounded background. It has the clip + // of the Short below it. + View container = inlinePlayerContainer == null ? null : inlinePlayerContainer.get(); + if (container == null || !container.isAttachedToWindow()) { + final long now = System.currentTimeMillis(); + if (shortsCellOutlines.isEmpty() + || now - inlinePlayerSearchTime < INLINE_PLAYER_SEARCH_INTERVAL_MILLIS) { + container = null; + } else { + inlinePlayerSearchTime = now; + container = findInlinePlayerContainer(current.getRootView(), current, 0); + inlinePlayerContainer = container == null + ? null + : new WeakReference<>(container); + } + } + + if (container != null) { + ViewOutlineProvider outline = shortsCellOutlines.get(getScreenBounds(container)); + if (outline != null) { + if (container.getOutlineProvider() != outline) { + container.setOutlineProvider(outline); + } + } else if (container.getOutlineProvider() instanceof ShortsCellOutlineProvider) { + container.setOutlineProvider(ViewOutlineProvider.BACKGROUND); + } + } + } + + if (feed) { + // The autoplay of a video shows the player in a container below the list, outside + // the list, and the card shows it through its thumbnail. The container has square + // corners, so it gets the rounded top corners of the card while it's over + // the thumbnail of a card. + View container = cardInlinePlayerContainer == null + ? null + : cardInlinePlayerContainer.get(); + if (container == null || !container.isAttachedToWindow()) { + final long now = System.currentTimeMillis(); + if (now - cardInlinePlayerSearchTime < INLINE_PLAYER_SEARCH_INTERVAL_MILLIS) { + container = null; + } else { + cardInlinePlayerSearchTime = now; + container = findCardInlinePlayerContainer(current.getRootView(), current, 0); + cardInlinePlayerContainer = container == null + ? null + : new WeakReference<>(container); + } + } + + if (container != null) { + if (isOverCardThumbnail(current, container)) { + if (container.getOutlineProvider() != CARD_INLINE_PLAYER_OUTLINE) { + cardInlinePlayerClipToOutline = container.getClipToOutline(); + container.setOutlineProvider(CARD_INLINE_PLAYER_OUTLINE); + container.setClipToOutline(true); + } + } else if (container.getOutlineProvider() == CARD_INLINE_PLAYER_OUTLINE) { + container.setOutlineProvider(ViewOutlineProvider.BACKGROUND); + // Over a Short, the container keeps its clip, which gets the outline of the + // Short. + container.setClipToOutline(cardInlinePlayerClipToOutline + || shortsCellOutlines.containsKey(getScreenBounds(container))); + } + } + } } } catch (Exception ex) { Logger.printException(() -> "onPreDraw failure", ex); @@ -829,7 +1504,31 @@ public static void onListDraw(RecyclerView list, Canvas canvas) { if (isVideoShelfRow(item)) { View above = findItemAbove(list, item); if (above == null || !isVideoShelfRow(above)) { - drawVideoShelfCard(list, above, item, canvas); + // The videos of a vertical shelf are below the shelf header, and the shelf is one card + // with the header and the videos. The videos are also used in lists without a shelf + // header, so a card is drawn only if the videos are below a shelf header, or were below + // one before the header was scrolled out of the list. + final boolean belowHeader = above != null && isVideoShelfHeader(above); + if (!belowHeader && above != null) { + // Not a shelf. The views are recycled, so they are no longer videos of a shelf. + for (View row = item; row != null && isVideoShelfRow(row); row = findItemBelow(list, row)) { + videoShelfRows.remove(row); + } + } else if (belowHeader || videoShelfRows.containsKey(item)) { + float bottom = item.getY() + item.getHeight(); + View row = item; + while (row != null && isVideoShelfRow(row)) { + videoShelfRows.put(row, Boolean.TRUE); + bottom = row.getY() + row.getHeight(); + row = findItemBelow(list, row); + } + + final float top = belowHeader ? above.getY() : item.getY(); + final float x = item.getX(); + final int horizontalPadding = Dim.dp(VIDEO_SHELF_HORIZONTAL_PADDING_DP); + canvas.drawRoundRect(x + horizontalPadding, top, x + item.getWidth() - horizontalPadding, + bottom, CARD_CORNER_RADIUS, CARD_CORNER_RADIUS, getShelfPaint(list.getContext())); + } } continue; } @@ -845,12 +1544,14 @@ public static void onListDraw(RecyclerView list, Canvas canvas) { } View header = item; - if (!isShelfHeader(header)) { + final boolean shelfHeader = isShelfHeader(header); + if (!shelfHeader && !shelfCardItems.containsKey(item)) { continue; } // The horizontal shelves have the header and the videos in the same list item, - // which is a card like the videos of the feed. + // which is a card like the videos of the feed. The header is not mounted while + // it is scrolled out of the list, and the item is then found by its element. if (hasNestedList(header, 0)) { setCardOutline(header, CARD_OUTLINE); final float x = header.getX(); @@ -863,6 +1564,10 @@ public static void onListDraw(RecyclerView list, Canvas canvas) { continue; } + if (!shelfHeader) { + continue; + } + // The rows of Shorts below the header are not in the list yet. View below = findItemBelow(list, header); if (below == null || !isShortsRow(below)) { @@ -883,40 +1588,6 @@ public static void onListDraw(RecyclerView list, Canvas canvas) { } } - /** - * The videos of a vertical shelf are below the shelf header, and the shelf is one card - * with the header and the videos. The videos are also used in lists without a shelf header, - * so a card is drawn only if the videos are below a shelf header, or were below one before - * the header was scrolled out of the list. - */ - private static void drawVideoShelfCard(RecyclerView list, @Nullable View above, View firstRow, Canvas canvas) { - final boolean belowHeader = above != null && isVideoShelfHeader(above); - if (!belowHeader && above != null) { - // Not a shelf. The views are recycled, so they are no longer videos of a shelf. - for (View item = firstRow; item != null && isVideoShelfRow(item); item = findItemBelow(list, item)) { - videoShelfRows.remove(item); - } - return; - } - if (!belowHeader && !videoShelfRows.containsKey(firstRow)) { - return; - } - - float bottom = firstRow.getY() + firstRow.getHeight(); - View item = firstRow; - while (item != null && isVideoShelfRow(item)) { - videoShelfRows.put(item, Boolean.TRUE); - bottom = item.getY() + item.getHeight(); - item = findItemBelow(list, item); - } - - final float top = belowHeader ? above.getY() : firstRow.getY(); - final float x = firstRow.getX(); - final int horizontalPadding = Dim.dp(VIDEO_SHELF_HORIZONTAL_PADDING_DP); - canvas.drawRoundRect(x + horizontalPadding, top, x + firstRow.getWidth() - horizontalPadding, bottom, - CARD_CORNER_RADIUS, CARD_CORNER_RADIUS, getShelfPaint(list.getContext())); - } - /** * @return The header of the Shorts shelf, which is the Shorts shelf header or another shelf header * above the Shorts, or null if the item above the Shorts is not a shelf header. @@ -1000,63 +1671,6 @@ private static void drawShortsShelfCard(RecyclerView list, @Nullable View header CARD_CORNER_RADIUS, CARD_CORNER_RADIUS, getShelfPaint(list.getContext())); } - /** - * The label of a recycled card can be set after the card is laid out, so the card keeps the outline - * of the item it replaced, without the spacing of the cards. - */ - private static void updateLateCardOutlines(RecyclerView list) { - for (int i = 0, count = list.getChildCount(); i < count; i++) { - View item = list.getChildAt(i); - if (item.getOutlineProvider() != CARD_OUTLINE && isCardLabel(item.getContentDescription())) { - setCardOutline(item, CARD_OUTLINE); - } - } - } - - /** - * Updates the clips of the Shorts of the Shorts shelves, without drawing the shelves. - */ - private static void updateShortsCellOutlines(RecyclerView list) { - shortsCellOutlines.clear(); - for (int i = 0, count = list.getChildCount(); i < count; i++) { - View item = list.getChildAt(i); - if (item.getHeight() > 0 && isShortsRow(item)) { - View above = findItemAbove(list, item); - if (above == null || !isShortsRow(above)) { - drawShortsShelfCard(list, getShortsShelfHeader(above), item, null); - } - } - } - if (list.getId() != getFeedId()) { - return; - } - - // The autoplay of a Short shows the player in a container above the Short, outside the list, - // which clips the player with its own rounded background. It has the clip of the Short below it. - View container = inlinePlayerContainer == null ? null : inlinePlayerContainer.get(); - if (container == null || !container.isAttachedToWindow()) { - final long now = System.currentTimeMillis(); - if (shortsCellOutlines.isEmpty() || now - inlinePlayerSearchTime < INLINE_PLAYER_SEARCH_INTERVAL_MILLIS) { - return; - } - inlinePlayerSearchTime = now; - container = findInlinePlayerContainer(list.getRootView(), list, 0); - inlinePlayerContainer = container == null ? null : new WeakReference<>(container); - if (container == null) { - return; - } - } - - ViewOutlineProvider outline = shortsCellOutlines.get(getScreenBounds(container)); - if (outline != null) { - if (container.getOutlineProvider() != outline) { - container.setOutlineProvider(outline); - } - } else if (container.getOutlineProvider() instanceof ShortsCellOutlineProvider) { - container.setOutlineProvider(ViewOutlineProvider.BACKGROUND); - } - } - /** * @return The view that clips the inline player (a SurfaceView or a TextureView) above a Short, outside the feed. */ @@ -1085,44 +1699,6 @@ private static View findInlinePlayerContainer(View view, RecyclerView feed, int return null; } - /** - * The autoplay of a video shows the player in a container below the list, outside the list, - * and the card shows it through its thumbnail. The container has square corners, - * so it gets the rounded top corners of the card while it's over the thumbnail of a card. - */ - private static void updateCardInlinePlayerOutline(RecyclerView list) { - if (list.getId() != getFeedId()) { - return; - } - - View container = cardInlinePlayerContainer == null ? null : cardInlinePlayerContainer.get(); - if (container == null || !container.isAttachedToWindow()) { - final long now = System.currentTimeMillis(); - if (now - cardInlinePlayerSearchTime < INLINE_PLAYER_SEARCH_INTERVAL_MILLIS) { - return; - } - cardInlinePlayerSearchTime = now; - container = findCardInlinePlayerContainer(list.getRootView(), list, 0); - cardInlinePlayerContainer = container == null ? null : new WeakReference<>(container); - if (container == null) { - return; - } - } - - if (isOverCardThumbnail(list, container)) { - if (container.getOutlineProvider() != CARD_INLINE_PLAYER_OUTLINE) { - cardInlinePlayerClipToOutline = container.getClipToOutline(); - container.setOutlineProvider(CARD_INLINE_PLAYER_OUTLINE); - container.setClipToOutline(true); - } - } else if (container.getOutlineProvider() == CARD_INLINE_PLAYER_OUTLINE) { - container.setOutlineProvider(ViewOutlineProvider.BACKGROUND); - // Over a Short, the container keeps its clip, which gets the outline of the Short. - container.setClipToOutline(cardInlinePlayerClipToOutline - || shortsCellOutlines.containsKey(getScreenBounds(container))); - } - } - /** * @return The highest view with the bounds of the inline player (a SurfaceView or a TextureView) * over the thumbnail of a card, outside the feed. @@ -1344,10 +1920,6 @@ private static Paint getShelfPaint(Context context) { return shelfPaint; } - private static boolean isCommentButtonLabel(@Nullable CharSequence label) { - return endsWith(label, COMMENT_BUTTON_LABEL_MARKER); - } - private static boolean endsWith(@Nullable CharSequence text, char c) { return text != null && text.length() > 0 && text.charAt(text.length() - 1) == c; } @@ -1365,7 +1937,7 @@ public static void onWatchCommand(@Nullable Object command, @Nullable Map commentsVideoId = null; // The app keeps only the known fields of the commands, so the button is found by its label. if (!(map.get(SENDER_VIEW_KEY) instanceof View senderView) - || !isCommentButtonLabel(senderView.getContentDescription()) + || !endsWith(senderView.getContentDescription(), COMMENT_BUTTON_LABEL_MARKER) || !(command instanceof MessageLite message)) { return; } @@ -1469,318 +2041,6 @@ private static int getFeedId() { return feedId; } - /** - * @return If the video is a Shorts or a live stream that a setting hides. - */ - private static boolean isHiddenByFilters(VideoThumbnail thumbnail) { - if (thumbnail.getBadgeStyle() == THUMBNAIL_BADGE_STYLE_LIVE && Settings.HIDE_LIVE_STREAMS.get()) { - return true; - } - return (Settings.HIDE_SHORTS_HOME.get() || Settings.HIDE_SHORTS_SUBSCRIPTIONS.get() - || Settings.HIDE_SHORTS_SEARCH.get() || Settings.HIDE_SHORTS_HISTORY.get()) - && shortsThumbnailSearch.matches(thumbnail.getImage().toByteArray()) - && isShortsHiddenInLoadedFeed(); - } - - /** - * The elements are parsed when the feed that shows them is loaded, so the Shorts keep the classic layout - * only in the feed whose setting hides them, the same feeds of the Shorts filter, and are cards in the others. - * - * @return If a setting hides the Shorts of the feed that is loaded. - */ - private static boolean isShortsHiddenInLoadedFeed() { - // The Shorts below the player are treated as the Shorts of the Home feed. - if (PlayerType.getCurrent().isMaximizedOrFullscreen()) { - return Settings.HIDE_SHORTS_HOME.get(); - } - // Must check second, as search can be from any tab. - if (NavigationBar.isSearchBarActive()) { - return Settings.HIDE_SHORTS_SEARCH.get(); - } - - NavigationBar.NavigationButton selectedNavButton = NavigationBar.NavigationButton.getSelectedNavigationButton(); - if (selectedNavButton == null) { - // Unknown tab, treat the same as home. - return Settings.HIDE_SHORTS_HOME.get(); - } - return switch (selectedNavButton) { - case HOME -> Settings.HIDE_SHORTS_HOME.get(); - case SEARCH -> Settings.HIDE_SHORTS_SEARCH.get(); - case SUBSCRIPTIONS -> Settings.HIDE_SHORTS_SUBSCRIPTIONS.get(); - case LIBRARY -> Settings.HIDE_SHORTS_HISTORY.get(); - default -> false; - }; - } - - @Nullable - private static ComponentType convertToCard(ComponentType componentType, VideoWithContextModel classic) - throws IOException { - VideoWithContextData data = classic.getData(); - VideoData videoData = data.getVideoData(); - VideoThumbnail thumbnail = videoData.getThumbnail(); - - // The compact items are rows of lists, such as the videos of a playlist or the 'Downloads' row of the library. - // The cards have no attachment, such as the description and the 'Buy or rent' button of the movies. - // The horizontal items, such as the search results on tablets, keep their layout. - if (data.getCompact() || data.getHorizontal() || !data.getAttachment().isEmpty()) { - return null; - } - - // The filters find the hidden Shorts and live streams by the classic layout. - if (isHiddenByFilters(thumbnail)) { - return null; - } - - String templateUri = CARD.getUri(); - if (templateUri == null ||!videoData.getLockupMetadata().hasLockupMetadataViewModel()) { - return null; - } - - // Playlists open their page, and show the number of videos instead of the duration. - InnertubeCommandHolder onTap = InnertubeCommandHolder.parseFrom(data.getOnTap()); - final boolean isPlaylist = onTap.getInnertubeCommand().hasBrowseEndpoint(); - String contentId; - String badgeText; - String badgeLabel; - if (isPlaylist) { - String browseId = onTap.getInnertubeCommand().getBrowseEndpoint().getBrowseId(); - if (!browseId.startsWith(PLAYLIST_BROWSE_ID_PREFIX) || thumbnail.getVideoCountText().isEmpty()) { - return null; - } - contentId = browseId.substring(PLAYLIST_BROWSE_ID_PREFIX.length()); - badgeText = thumbnail.getVideoCountText(); - badgeLabel = badgeText; - } else { - contentId = getVideoId(thumbnail.getImage()); - if (contentId == null) { - return null; - } - badgeText = thumbnail.getTimestampText(); - badgeLabel = thumbnail.getTimestampAccessibilityText(); - } - - ThumbnailViewModel.Builder thumbnailViewModel = ThumbnailViewModel.newBuilder() - .setImage(thumbnail.getImage()); - if (!badgeText.isEmpty()) { - ThumbnailBadgeViewModel.Builder badge = ThumbnailBadgeViewModel.newBuilder() - .setText(badgeText) - .setBadgeStyle(THUMBNAIL_BADGE_STYLE_DEFAULT) - .setRendererContext(RendererContext.newBuilder() - .setAccessibilityContext(AccessibilityContext.newBuilder() - .setLabel(badgeLabel))); - // The badge of the videos that are live now is red, with the live icon of the classic layout. - if (!isPlaylist && thumbnail.getBadgeStyle() == THUMBNAIL_BADGE_STYLE_LIVE) { - badge.setBadgeStyle(THUMBNAIL_BADGE_STYLE_LIVE); - if (!thumbnail.getBadgeIcon().isEmpty()) { - badge.setIcon(ThumbnailBadgeIcon.newBuilder() - .addSources(ThumbnailBadgeIconSource.newBuilder() - .setClientResource(ClientResource.newBuilder() - .setImageName(thumbnail.getBadgeIcon())))); - } - } - - thumbnailViewModel.addOverlays(ThumbnailOverlay.newBuilder() - .setThumbnailBottomOverlayViewModel(ThumbnailBottomOverlayViewModel.newBuilder() - .addBadges(ThumbnailBadge.newBuilder().setThumbnailBadgeViewModel(badge)))); - } - - // The card thumbnail shows only the badges of the bottom overlays, - // so the watch progress is drawn by the bottom overlay template in an element over the thumbnail. - final float progress = thumbnail.getProgress(); - if (SeekbarColorPatch.showWatchHistoryProgressDrawable(progress > 0)) { - String bottomOverlayUri = THUMBNAIL_BOTTOM_OVERLAY.getUri(); - if (bottomOverlayUri != null) { - ThumbnailBottomOverlayViewModel progressBar = ThumbnailBottomOverlayViewModel.newBuilder() - .setProgressBar(ThumbnailOverlayProgressBar.newBuilder() - .setThumbnailOverlayProgressBarViewModel(ThumbnailOverlayProgressBarViewModel.newBuilder() - .setProgress(progress) - .setStyle(THUMBNAIL_PROGRESS_BAR_STYLE_EDGE))) - .build(); - ComponentType progressComponent = ComponentType.newBuilder() - .setTemplateConfig(templateConfig(bottomOverlayUri)) - .setModel(ElementModel.newBuilder().setLockupModel(LockupModel.newBuilder() - .setViewModel(LockupViewModelHolder.newBuilder() - .setThumbnailBottomOverlayViewModel(progressBar)) - .setContext(classic.getContext()))) - .setSubscriptionConfig(subscriptionConfig(componentType.getSubscriptionConfig(), THUMBNAIL_BOTTOM_OVERLAY)) - .build(); - thumbnailViewModel.addOverlays(ThumbnailOverlay.newBuilder() - .setThumbnailElementOverlay(ThumbnailElementOverlay.newBuilder() - .setElement(Element.newBuilder() - .setType(ElementType.newBuilder().setComponentType(progressComponent))))); - } - } - - LockupMetadataViewModel.Builder metadata = videoData.getLockupMetadata() - .getLockupMetadataViewModel().toBuilder() - .setTitleStyle(TextStyle.newBuilder().setStyle(CARD_TITLE_STYLE)); - metadata.setMetadata(splitMetadataRows(metadata.getMetadata())); - - // The images of the classic layout have the same format of the images of the cards. - if (videoData.hasDecoratedAvatar()) { - metadata.setImage(videoData.getDecoratedAvatar()); - } else if (videoData.hasAvatarStack()) { - metadata.setImage(videoData.getAvatarStack()); - } else if (videoData.hasAvatar()) { - ChannelAvatar avatar = videoData.getAvatar(); - metadata.setImage(LockupImage.newBuilder() - .setDecoratedAvatarViewModel(DecoratedAvatarViewModel.newBuilder() - .setAvatar(AvatarViewModelHolder.newBuilder() - .setAvatarViewModel(AvatarViewModel.newBuilder() - .setImage(avatar.getImage()) - .setAvatarImageSize(avatar.getAvatarImageSize()))) - .setA11YLabel(avatar.getAccessibilityText()) - .setRendererContext(RendererContext.newBuilder() - .setCommandContext(CommandContext.newBuilder() - .setOnTap(avatar.getEndpoint()))))); - } - - InnertubeCommandHolder menuOnTap = InnertubeCommandHolder.parseFrom(data.getMenuOnTap()); - metadata.setMenuButton(ButtonViewModelHolder.newBuilder() - .setButtonViewModel(button("MORE_VERT", data.getMenuAccessibilityText(), menuOnTap))); - - if (ACTION_BUTTONS && !isPlaylist) { - addActionButtons(metadata, contentId, data); - } - - LockupViewModel.Builder lockupViewModel = LockupViewModel.newBuilder() - .setContentImage(ContentImage.newBuilder().setThumbnailViewModel(thumbnailViewModel)) - .setMetadata(LockupMetadata.newBuilder().setLockupMetadataViewModel(metadata)) - .setContentId(contentId) - .setContentType(isPlaylist ? LOCKUP_CONTENT_TYPE_PLAYLIST : LOCKUP_CONTENT_TYPE_VIDEO) - .setRendererContext(RendererContext.newBuilder() - .setAccessibilityContext(AccessibilityContext.newBuilder() - .setLabel(data.getAccessibilityText() + CARD_LABEL_MARKER)) - .setCommandContext(CommandContext.newBuilder().setOnTap(data.getOnTap()))); - // The cards play the video inline (feed autoplay) with the same commands of the classic layout. - if (!data.getInlinePlayback().isEmpty()) { - lockupViewModel.setInlinePlayback(LockupInlinePlayback.newBuilder() - .setInlinePlayback(data.getInlinePlayback())); - } - - LockupModel card = LockupModel.newBuilder() - .setViewModel(LockupViewModelHolder.newBuilder().setLockupViewModel(lockupViewModel)) - .setContext(setCardExperiments(classic.getContext(), true)) - .setEnvironment(classic.getEnvironment()) - .setPadding(LockupPadding.newBuilder() - .setStart(CARD_HORIZONTAL_PADDING_DP) - .setEnd(CARD_HORIZONTAL_PADDING_DP) - .setTop(CARD_VERTICAL_PADDING_DP) - .setBottom(CARD_VERTICAL_PADDING_DP)) - .build(); - - return componentType.toBuilder() - .setTemplateConfig(templateConfig(templateUri)) - .setModel(ElementModel.newBuilder().setLockupModel(card)) - .setSubscriptionConfig(subscriptionConfig(componentType.getSubscriptionConfig(), CARD)) - .build(); - } - - @Nullable - private static ComponentType convertToClassic(ComponentType componentType, LockupModel card) { - LockupViewModel lockupViewModel = card.getViewModel().getLockupViewModel(); - if (lockupViewModel.getContentType() != LOCKUP_CONTENT_TYPE_VIDEO - || !lockupViewModel.getContentImage().hasThumbnailViewModel()) { - return null; - } - - String templateUri = CLASSIC.getUri(); - if (templateUri == null) { - return null; - } - - ThumbnailViewModel thumbnailViewModel = lockupViewModel.getContentImage().getThumbnailViewModel(); - VideoThumbnail.Builder thumbnail = VideoThumbnail.newBuilder() - .setImage(thumbnailViewModel.getImage()); - for (ThumbnailOverlay overlay : thumbnailViewModel.getOverlaysList()) { - for (ThumbnailBadge badge : overlay.getThumbnailBottomOverlayViewModel().getBadgesList()) { - ThumbnailBadgeViewModel badgeViewModel = badge.getThumbnailBadgeViewModel(); - if (!badgeViewModel.getText().isEmpty()) { - thumbnail.setTimestampText(badgeViewModel.getText()) - .setTimestampAccessibilityText(badgeViewModel.getRendererContext() - .getAccessibilityContext().getLabel()); - } - } - } - - LockupMetadataViewModel metadata = lockupViewModel.getMetadata().getLockupMetadataViewModel(); - VideoData.Builder videoData = VideoData.newBuilder() - .setThumbnail(thumbnail) - .setLockupMetadata(LockupMetadata.newBuilder() - .setLockupMetadataViewModel(metadata.toBuilder() - .setMetadata(joinMetadataRows(metadata.getMetadata())) - .clearImage() - .clearMenuButton() - .clearTitleStyle() - .clearActionButtons())); - if (metadata.getImage().hasDecoratedAvatarViewModel()) { - DecoratedAvatarViewModel avatar = metadata.getImage().getDecoratedAvatarViewModel(); - AvatarViewModel avatarViewModel = avatar.getAvatar().getAvatarViewModel(); - videoData.setAvatar(ChannelAvatar.newBuilder() - .setImage(avatarViewModel.getImage()) - .setAccessibilityText(avatar.getA11YLabel()) - .setAvatarImageSize(avatarViewModel.getAvatarImageSize()) - .setEndpoint(avatar.getRendererContext().getCommandContext().getOnTap())); - } - - ButtonViewModel menuButton = metadata.getMenuButton().getButtonViewModel(); - RendererContext rendererContext = lockupViewModel.getRendererContext(); - VideoWithContextModel classic = VideoWithContextModel.newBuilder() - .setData(VideoWithContextData.newBuilder() - .setVideoData(videoData) - .setAccessibilityText(rendererContext.getAccessibilityContext().getLabel()) - .setMenuAccessibilityText(menuButton.getAccessibilityText()) - .setOnTap(rendererContext.getCommandContext().getOnTap()) - .setMenuOnTap(menuButton.getOnTap().toByteString()) - .setInlinePlayback(lockupViewModel.getInlinePlayback().getInlinePlayback())) - .setContext(setCardExperiments(card.getContext(), false)) - .setEnvironment(card.getEnvironment()) - .build(); - - return componentType.toBuilder() - .setTemplateConfig(templateConfig(templateUri)) - .setModel(ElementModel.newBuilder().setVideoWithContextModel(classic)) - .setSubscriptionConfig(subscriptionConfig(componentType.getSubscriptionConfig(), CLASSIC)) - .build(); - } - - /** - * The shelves of the card layout, such as the Shorts shelf, have the modern header, - * with a bigger title and without the icon. - * The template shows it if the server enables it and the header asks for it. - */ - @Nullable - private static ComponentType setModernShelfHeader(ComponentType componentType, ShelfHeaderModel header) { - ShelfHeaderData data = header.getData(); - if (!data.getIcon().toString(StandardCharsets.ISO_8859_1).contains(SHORTS_SHELF_ICON)) { - // The other shelf headers keep their own style, and are only found in the lists by their label, - // to draw a card behind the videos below them. - return componentType.toBuilder() - .setModel(ElementModel.newBuilder().setShelfHeaderModel(header.toBuilder() - .setData(data.toBuilder() - .setTitle(data.getTitle() + VIDEO_SHELF_HEADER_LABEL_MARKER) - .setAccessibilityText(data.getAccessibilityText() + VIDEO_SHELF_HEADER_LABEL_MARKER)))) - .build(); - } - - return componentType.toBuilder() - .setModel(ElementModel.newBuilder().setShelfHeaderModel(header.toBuilder() - .setData(modernShelfHeader(data)))) - .build(); - } - - /** - * The label of the Shorts ends with an invisible character, to find the Shorts shelf in the lists. - */ - private static ComponentType setShortsCellLabel(ComponentType componentType, ShortsVideoCellModel cell) { - ShortsVideoCellData data = cell.getData(); - return componentType.toBuilder() - .setModel(ElementModel.newBuilder().setShortsVideoCellModel(cell.toBuilder() - .setData(data.toBuilder() - .setAccessibilityText(data.getAccessibilityText() + SHORTS_CELL_LABEL_MARKER)))) - .build(); - } - /** * The label of the header ends with an invisible character, to find the shelves in the lists. */ @@ -1793,212 +2053,6 @@ private static ShelfHeaderData modernShelfHeader(ShelfHeaderData data) { .build(); } - /** - * The horizontal shelves of videos have the modern header, like the Shorts shelf, - * and the shelf is a card with the header and the videos. - */ - @Nullable - private static ComponentType setHorizontalShelfCard(ComponentType componentType, HorizontalShelfModel shelf) { - HorizontalShelfData data = shelf.getData(); - if (!data.hasHeader()) { - return null; - } - - return componentType.toBuilder() - .setModel(ElementModel.newBuilder().setHorizontalShelfModel(shelf.toBuilder() - .setData(data.toBuilder() - .setHeader(modernShelfHeader(data.getHeader()))))) - .build(); - } - - /** - * The label of the videos of the vertical shelves ends with an invisible character, - * to draw the shelf below the header as a card. - */ - private static ComponentType setVideoShelfRowLabel(ComponentType componentType, GridVideoModel video) { - return componentType.toBuilder() - .setModel(ElementModel.newBuilder().setGridVideoModel(video.toBuilder() - .setAccessibilityText(video.getAccessibilityText() + VIDEO_SHELF_ROW_LABEL_MARKER))) - .build(); - } - - /** - * The shelves of Shorts in a carousel, such as in the search results, have the modern header - * like the other shelves, and the shelf is a card with the header and the Shorts. - */ - @Nullable - private static ComponentType setShortsShelfCard(ComponentType componentType, ShortsShelfModel shelf) { - if (!shelf.hasHeader()) { - return null; - } - - return componentType.toBuilder() - .setModel(ElementModel.newBuilder().setShortsShelfModel(shelf.toBuilder() - .setHeader(modernShelfHeader(shelf.getHeader())))) - .build(); - } - - /** - * The classic layout shows the channel, the views and the date in one text separated by spaces, - * and the cards show the channel above the views and the date. - */ - private static ContentMetadata splitMetadataRows(ContentMetadata metadata) { - ContentMetadataViewModel viewModel = metadata.getContentMetadataViewModel(); - MetadataPart part = null; - for (MetadataRow row : viewModel.getMetadataRowsList()) { - if (row.getMetadataPartsCount() > 0) { - if (part != null || row.getMetadataPartsCount() > 1) { - return metadata; // Already split. - } - part = row.getMetadataParts(0); - } - } - if (part == null) { - return metadata; - } - - AttributedText text = part.getText(); - String content = text.getContent(); - Matcher separator = METADATA_SEPARATOR_PATTERN.matcher(content); - if (!separator.find()) { - return metadata; - } - - // The views icon is an attachment on the spaces before the views. - final int channelEnd = separator.start(); - int secondRowStart = separator.end(); - for (AttachmentRun run : text.getAttachmentRunsList()) { - final int start = run.getStartIndex(); - if (start >= channelEnd && start < secondRowStart) { - secondRowStart = start; - } - } - - AttributedText.Builder secondRowText = AttributedText.newBuilder() - .setContent(content.substring(secondRowStart)); - for (AttachmentRun run : text.getAttachmentRunsList()) { - if (run.getStartIndex() >= secondRowStart) { - secondRowText.addAttachmentRuns(run.toBuilder() - .setStartIndex(run.getStartIndex() - secondRowStart)); - } - } - - // The accessibility label is 'Channel, views, date'. - String label = part.getAccessibilityLabel(); - final int labelSeparator = label.indexOf(", "); - - return ContentMetadata.newBuilder() - .setContentMetadataViewModel(viewModel.toBuilder() - .clearMetadataRows() - .addMetadataRows(MetadataRow.newBuilder() - .addMetadataParts(MetadataPart.newBuilder() - .setText(AttributedText.newBuilder() - .setContent(content.substring(0, channelEnd))))) - .addMetadataRows(MetadataRow.newBuilder() - .addMetadataParts(MetadataPart.newBuilder() - .setText(secondRowText) - .setAccessibilityLabel(labelSeparator < 0 - ? label - : label.substring(labelSeparator + 2)))) - .setDelimiter(" ")) - .build(); - } - - /** - * Joins the metadata rows of a card into one text, like the classic layout. - */ - private static ContentMetadata joinMetadataRows(ContentMetadata metadata) { - ContentMetadataViewModel viewModel = metadata.getContentMetadataViewModel(); - if (viewModel.getMetadataRowsCount() < 2) { - return metadata; - } - - StringBuilder content = new StringBuilder(); - StringBuilder label = new StringBuilder(); - AttributedText.Builder text = AttributedText.newBuilder(); - for (MetadataRow row : viewModel.getMetadataRowsList()) { - for (MetadataPart part : row.getMetadataPartsList()) { - String partContent = part.getText().getContent(); - if (partContent.isEmpty()) { - continue; - } - if (content.length() > 0) { - content.append(" "); - } - final int offset = content.length(); - content.append(partContent); - for (AttachmentRun run : part.getText().getAttachmentRunsList()) { - text.addAttachmentRuns(run.toBuilder().setStartIndex(run.getStartIndex() + offset)); - } - - String partLabel = part.getAccessibilityLabel().isEmpty() - ? partContent.trim() - : part.getAccessibilityLabel(); - if (label.length() > 0) { - label.append(", "); - } - label.append(partLabel); - } - } - - return ContentMetadata.newBuilder() - .setContentMetadataViewModel(viewModel.toBuilder() - .clearMetadataRows() - .addMetadataRows(MetadataRow.newBuilder() - .addMetadataParts(MetadataPart.newBuilder() - .setText(text.setContent(content.toString())) - .setAccessibilityLabel(label.toString()))) - .clearDelimiter()) - .build(); - } - - /** - * The comment button opens the video with the comments, which are opened when the watch page is loaded, - * and the save button saves the video to 'Watch later'. - */ - private static void addActionButtons(LockupMetadataViewModel.Builder metadata, String videoId, - VideoWithContextData data) throws IOException { - Icons icons = getIcons(); - InnertubeCommandHolder saveOnTap = findSaveToWatchLaterCommand(data.getMenuOnTap()); - if (saveOnTap == null) { - // Without the item, such as in some menus, the button opens the menu. - saveOnTap = InnertubeCommandHolder.parseFrom(data.getMenuOnTap()); - } - - metadata.addActionButtons(ActionButton.newBuilder() - .setLikeButtonViewModel(likeButton(videoId, icons))) - .addActionButtons(ActionButton.newBuilder() - .setButtonViewModel(button(icons.comment, - str("morphe_feed_card_layout_comments") + COMMENT_BUTTON_LABEL_MARKER, - InnertubeCommandHolder.parseFrom(data.getOnTap())))) - .addActionButtons(ActionButton.newBuilder() - .setAddToPlaylistButtonViewModel(AddToPlaylistButtonViewModel.newBuilder() - .setToggleButtonViewModel(toggleButton( - button(icons.save, "", saveOnTap), - button(icons.saved, "", saveOnTap))))); - } - - private static LikeButtonViewModel likeButton(String videoId, Icons icons) { - String entityKey = Base64.encodeToString(LikeStatusEntityKey.newBuilder() - .setVideoId(videoId) - .setEntityType(LIKE_STATUS_ENTITY_TYPE) - .setVersion(1) - .build() - .toByteArray(), - Base64.URL_SAFE | Base64.NO_WRAP).replace("=", "%3D"); - - return LikeButtonViewModel.newBuilder() - .setToggleButtonViewModel(toggleButton( - button(icons.like, "", likeCommand(videoId, LIKE_STATUS_LIKE)), - button(icons.liked, "", likeCommand(videoId, LIKE_STATUS_INDIFFERENT)))) - .setLikeStatusEntityKey(entityKey) - .setLikeStatusEntity(LikeStatusEntity.newBuilder() - .setKey(entityKey) - .setLikeStatus(LIKE_STATUS_INDIFFERENT)) - .setLikeCountEnabled(1) - .build(); - } - private static InnertubeCommandHolder likeCommand(String videoId, int status) { return InnertubeCommandHolder.newBuilder() .setInnertubeCommand(InnertubeCommand.newBuilder() @@ -2077,89 +2131,4 @@ private static boolean isTemplate(String uri, String template) { return hashStart == template.length() || (hashStart > 0 && TEMPLATE_SUFFIX_PATTERN.matcher(uri.substring(template.length(), hashStart)).matches()); } - - @Nullable - private static String getVideoId(ByteString thumbnailImage) { - Matcher matcher = VIDEO_ID_PATTERN.matcher(thumbnailImage.toString(StandardCharsets.ISO_8859_1)); - return matcher.find() ? matcher.group(1) : null; - } - - private static Icons getIcons() { - if (icons == null) { - icons = Icons.get(); - } - return icons; - } - - /** - * The uri of a template includes the hash of its version, so it's taken from the template cache - * of the app, which has the templates the app received. - * - * @return The last cached version of the template, or null if the template is not cached. - */ - @Nullable - private static String findCachedTemplateUri(String template) { - try { - File metadata = new File(Utils.getContext().getFilesDir(), "ElementsResourceCacheMetadata"); - if (!metadata.isFile()) { - return null; - } - - String uri = null; - CodedInputStream input = CodedInputStream.newInstance(Files.readAllBytes(metadata.toPath())); - while (!input.isAtEnd()) { - final int tag = input.readTag(); - if (WireFormat.getTagWireType(tag) != WireFormat.WIRETYPE_LENGTH_DELIMITED) { - input.skipField(tag); - continue; - } - - CodedInputStream entry = CodedInputStream.newInstance(input.readByteArray()); - while (!entry.isAtEnd()) { - final int entryTag = entry.readTag(); - if (WireFormat.getTagFieldNumber(entryTag) == 1 - && WireFormat.getTagWireType(entryTag) == WireFormat.WIRETYPE_LENGTH_DELIMITED) { - String key = entry.readString(); - if (isTemplate(key, template)) { - uri = key; - } - } else { - entry.skipField(entryTag); - } - } - } - - String found = uri; - Logger.printDebug(() -> "Cached template: " + found); - return uri; - } catch (Exception ex) { - Logger.printException(() -> "findCachedTemplateUri failure", ex); - return null; - } - } - - /** - * @return The command of the 'Save to Watch later' item of the menu, or null if the menu has no such item. - */ - @Nullable - private static InnertubeCommandHolder findSaveToWatchLaterCommand(ByteString menu) throws IOException { - List message = ProtoNode.parse(menu.toByteArray()); - if (message == null) { - return null; - } - - for (ProtoNode endpoint : ProtoNode.findMessages(message, PLAYLIST_EDIT_ENDPOINT_FIELD)) { - ProtoNode playlistId = ProtoNode.field(endpoint.children, PLAYLIST_EDIT_ENDPOINT_PLAYLIST_ID_FIELD); - ProtoNode command = endpoint.getParent(); - if (playlistId != null && WATCH_LATER_PLAYLIST_ID.equals(playlistId.getText()) - && command != null && command.children != null) { - ByteArrayOutputStream bytes = new ByteArrayOutputStream(); - CodedOutputStream output = CodedOutputStream.newInstance(bytes); - output.writeByteArray(INNERTUBE_COMMAND_FIELD, ProtoNode.write(command.children)); - output.flush(); - return InnertubeCommandHolder.parseFrom(bytes.toByteArray()); - } - } - return null; - } } diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/feedcards/FeedCardLayoutPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/feedcards/FeedCardLayoutPatch.kt index 7b0ab3ea11..d932334207 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/feedcards/FeedCardLayoutPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/feedcards/FeedCardLayoutPatch.kt @@ -36,6 +36,7 @@ import app.morphe.util.indexOfFirstInstructionReversedOrThrow import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation +import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction import com.android.tools.smali.dexlib2.iface.reference.FieldReference import com.android.tools.smali.dexlib2.iface.reference.MethodReference import com.android.tools.smali.dexlib2.immutable.ImmutableMethod @@ -119,13 +120,34 @@ val feedCardLayoutPatch = bytecodePatch( // Some lists, such as the Home feed, add a spacing between the items, and the cards have their own spacing. // The item is replaced by an item without spacing. DividerInfoToStringFingerprint.originalClassDefOrNull?.let { dividerInfoClass -> - DividerInfoLookupFingerprint(dividerInfoClass.type).methodOrNull?.addInstructions( - 0, - """ - invoke-static { p1 }, $EXTENSION_CLASS->getDividerItem(Ljava/lang/Object;)Ljava/lang/Object; - move-result-object p1 - """ - ) + DividerInfoLookupFingerprint(dividerInfoClass.type).methodOrNull?.let { dividerInfoLookupMethod -> + dividerInfoLookupMethod.addInstructions( + 0, + """ + invoke-static { p1 }, $EXTENSION_CLASS->getDividerItem(Ljava/lang/Object;)Ljava/lang/Object; + move-result-object p1 + """ + ) + + // The header of a horizontal shelf has the label that finds the shelf in the list, and it is + // not mounted while it is scrolled out of the list. The shelves are also found by their element, + // which the list looks up with the view of the item when it sets the spacing of the item. + DividerItemOffsetsFingerprint(dividerInfoLookupMethod).let { + it.methodOrNull?.apply { + val lookupIndex = it.instructionMatches.first().index + val itemRegister = getInstruction(lookupIndex).registerD + + addInstruction( + lookupIndex, + "invoke-static/range { v$itemRegister .. v$itemRegister }, $EXTENSION_CLASS->setDividerItem(Ljava/lang/Object;)V" + ) + addInstruction( + 0, + "invoke-static/range { p2 .. p2 }, $EXTENSION_CLASS->setDividerItemView(Landroid/view/View;)V" + ) + } + } + } // The cards are found by the bytes of their element. ElementItemConstructorFingerprint.let { diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/feedcards/Fingerprints.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/feedcards/Fingerprints.kt index e8d2022768..8ed7eeef47 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/feedcards/Fingerprints.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/feedcards/Fingerprints.kt @@ -74,6 +74,24 @@ internal class DividerInfoLookupFingerprint(dividerInfoType: String) : Fingerpri ) ) +/** + * Sets the spacing above and below the view of a list item, which is looked up with the item of the view. + * + * @param dividerInfoLookupMethod Method of [DividerInfoLookupFingerprint]. + */ +internal class DividerItemOffsetsFingerprint(dividerInfoLookupMethod: MethodReference) : Fingerprint( + returnType = "V", + parameters = listOf( + "Landroid/graphics/Rect;", + "Landroid/view/View;", + "Landroid/support/v7/widget/RecyclerView;", + "L" + ), + filters = listOf( + methodCall(reference = dividerInfoLookupMethod) + ) +) + /** * List item of an element, which keeps the bytes of the element. */ From bf6fbe0c75728741f7d51a11a20e3bf2312faba9 Mon Sep 17 00:00:00 2001 From: 0xrxL <127248639+0xrxL@users.noreply.github.com> Date: Sat, 24 Oct 2026 05:16:50 +0200 Subject: [PATCH 024/189] fix(YouTube - Hide status bar): Status bar covers the top bar or leaves an empty bar above it (#3997) --- .../youtube/patches/HideStatusBarPatch.java | 151 ++++++++++++++++++ 1 file changed, 151 insertions(+) diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/HideStatusBarPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/HideStatusBarPatch.java index fe25ed011c..7574cd59ea 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/HideStatusBarPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/HideStatusBarPatch.java @@ -16,6 +16,8 @@ import android.os.Bundle; import android.view.DisplayCutout; import android.view.View; +import android.view.ViewGroup; +import android.view.ViewParent; import android.view.Window; import android.view.WindowInsets; import android.view.WindowInsetsController; @@ -28,6 +30,9 @@ import java.util.WeakHashMap; import app.morphe.extension.shared.Logger; +import app.morphe.extension.shared.ResourceType; +import app.morphe.extension.shared.ResourceUtils; +import app.morphe.extension.shared.Utils; import app.morphe.extension.youtube.settings.Settings; import app.morphe.extension.youtube.shared.PlayerType; import kotlin.Unit; @@ -67,6 +72,45 @@ public void onActivityDestroyed(@NonNull Activity a) {} private static boolean activityLifecycleCallbacksRegistered; + /** + * The layout that holds the toolbar of the pages. With the edge to edge top bar, its top padding + * is the room left for the status bar. + */ + private static final int TOOLBAR_CONTAINER = ResourceUtils.getIdentifier( + ResourceType.ID, "toolbar_container"); + + /** + * Main activities whose top bars are kept below the status bar once they are started. + */ + private static final Set topBarActivities = Collections.newSetFromMap(new WeakHashMap<>()); + + /** + * Callback when an activity is started. The window of the main activity is not created + * when it is initialized, so the top bars are watched once it is started. + */ + private static final Application.ActivityLifecycleCallbacks TOP_BAR_LIFECYCLE_CALLBACKS + = new Application.ActivityLifecycleCallbacks() { + + public void onActivityStarted(@NonNull Activity activity) { + try { + if (topBarActivities.remove(activity)) { + keepTopBarsBelowStatusBar(activity.getWindow().getDecorView()); + } + } catch (Exception ex) { + Logger.printException(() -> "onActivityStarted top bar failure", ex); + } + } + + public void onActivityCreated(@NonNull Activity a, @Nullable Bundle b) {} + public void onActivityResumed(@NonNull Activity a) {} + public void onActivityPaused(@NonNull Activity a) {} + public void onActivityStopped(@NonNull Activity a) {} + public void onActivitySaveInstanceState(@NonNull Activity a, @NonNull Bundle b) {} + public void onActivityDestroyed(@NonNull Activity a) {} + }; + + private static boolean topBarLifecycleCallbacksRegistered; + /** * If a video is watched in the player that fills the screen in portrait, not in fullscreen. * The display cutout inset is then left out, so the video is laid out at the top of the screen @@ -160,6 +204,7 @@ public static boolean useStatusBarScrollColor(boolean original) { */ public static void initialize(Activity activity) { if (!Settings.HIDE_STATUS_BAR.get()) { + watchTopBars(activity); return; } @@ -187,6 +232,112 @@ public static void initialize(Activity activity) { } } + /** + * Keeps the top bars of the main activity below the status bar, with the status bar layout fix + * and the status bar not hidden. With the status bar hidden, the top bars are laid out + * by {@link #setSystemWindowInsetTop(Rect, View)} instead. + */ + private static void watchTopBars(Activity activity) { + if (!Settings.FIX_STATUS_BAR_LAYOUT.get() || TOOLBAR_CONTAINER == 0) { + return; + } + + try { + if (!topBarLifecycleCallbacksRegistered) { + topBarLifecycleCallbacksRegistered = true; + activity.getApplication().registerActivityLifecycleCallbacks(TOP_BAR_LIFECYCLE_CALLBACKS); + } + topBarActivities.add(activity); + } catch (Exception ex) { + Logger.printException(() -> "watchTopBars failure", ex); + } + } + + /** + * The room for the status bar above the toolbar comes from many classes, and on some devices + * they do not agree on the edge to edge top bar (https://github.com/MorpheApp/morphe-patches/issues/2969). + * The room is then left out, so the status bar view covers the toolbar and its buttons, + * or it is added twice, leaving an empty bar above the toolbar. + * Whenever the window is laid out with the status bar visible, the top padding of the toolbar + * container is set so that the room above the toolbar is the height of the status bar once. + */ + private static void keepTopBarsBelowStatusBar(View decorView) { + decorView.getViewTreeObserver().addOnGlobalLayoutListener(() -> { + try { + // Only with the edge to edge top bar flag handed to the app by this patch. + if (!Settings.FIX_STATUS_BAR_LAYOUT.get() || Settings.HIDE_STATUS_BAR.get() + || edgeToEdgeTopBarUsed == null) { + return; + } + + WindowInsets insets = decorView.getRootWindowInsets(); + if (insets == null || !isStatusBarVisible(decorView)) { + return; + } + + final int statusBarHeight = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R + ? insets.getInsets(WindowInsets.Type.statusBars()).top + : insets.getSystemWindowInsetTop(); + if (statusBarHeight <= 0) { + return; + } + + // With the edge to edge top bar, the app lays out its whole layout behind the status bar. + // If the layout still makes room for the status bar, it is drawn as a dark bar over + // the top bars, so the layout is drawn behind the status bar the same as the app does. + View content = decorView.findViewById(android.R.id.content); + View layout = content instanceof ViewGroup group && group.getChildCount() > 0 + ? group.getChildAt(0) + : null; + if (layout != null && layout.getPaddingTop() > 0 && edgeToEdgeTopBarUsed) { + Logger.printDebug(() -> "Layout room for the status bar: " + layout.getPaddingTop()); + layout.setFitsSystemWindows(false); + layout.setPadding(layout.getPaddingLeft(), 0, layout.getPaddingRight(), layout.getPaddingBottom()); + layout.requestApplyInsets(); + return; + } + + View toolbarContainer = decorView instanceof ViewGroup decorGroup + ? Utils.getChildView(decorGroup, true, + view -> view.getId() == TOOLBAR_CONTAINER && view.isShown()) + : null; + if (toolbarContainer != null) { + setStatusBarRoom(decorView, toolbarContainer, statusBarHeight); + } + } catch (Exception ex) { + Logger.printException(() -> "keepTopBarsBelowStatusBar failure", ex); + } + }); + } + + private static void setStatusBarRoom(View decorView, View toolbarContainer, int statusBarHeight) { + // The room above the toolbar: the top padding and margin of the toolbar container + // and of every layout that holds it. Scrolling the top bars away only moves them. + int room = toolbarContainer.getPaddingTop(); + for (View view = toolbarContainer; view != decorView; ) { + if (view.getLayoutParams() instanceof ViewGroup.MarginLayoutParams params) { + room += params.topMargin; + } + ViewParent parent = view.getParent(); + if (!(parent instanceof View)) { + break; + } + view = (View) parent; + room += view.getPaddingTop(); + } + + final int roomAbove = room; + final int difference = statusBarHeight - roomAbove; + final int paddingTop = toolbarContainer.getPaddingTop() + difference; + if (difference == 0 || paddingTop < 0) { + return; + } + + Logger.printDebug(() -> "Toolbar room above: " + roomAbove + " status bar: " + statusBarHeight); + toolbarContainer.setPadding(toolbarContainer.getPaddingLeft(), paddingTop, + toolbarContainer.getPaddingRight(), toolbarContainer.getPaddingBottom()); + } + private static void hideStatusBar(Activity activity) { if (!activities.add(activity)) { return; From c0ec5ae17781305f490e589d2d53a6bc01cead46 Mon Sep 17 00:00:00 2001 From: 0xrxL <127248639+0xrxL@users.noreply.github.com> Date: Sat, 24 Oct 2026 05:53:30 +0200 Subject: [PATCH 025/189] fix(YouTube - Feed card layout): Videos of the horizontal shelves are cut by the left edge of the card --- .../youtube/patches/FeedCardLayoutPatch.java | 125 +++++++++++++++++- 1 file changed, 121 insertions(+), 4 deletions(-) diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FeedCardLayoutPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FeedCardLayoutPatch.java index a1389ed5c9..fd3ce6c48d 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FeedCardLayoutPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FeedCardLayoutPatch.java @@ -213,6 +213,11 @@ public interface ElementItemInterface { * with the menu button near the right edge, so their card is wider than the other cards. */ private static final int VIDEO_SHELF_HORIZONTAL_PADDING_DP = 4; + /** + * Spacing between the edges of the horizontal shelf cards and the content of the first and the last item + * of their list. The items of the shelves have different paddings, and some have none. + */ + private static final int SHELF_LIST_PADDING_DP = 8; private static final CardOutlineProvider CARD_OUTLINE = new CardOutlineProvider( Dim.dp(CARD_HORIZONTAL_PADDING_DP), Dim.dp(CARD_VERTICAL_PADDING_DP)); @@ -395,6 +400,10 @@ public void getOutline(View view, Outline outline) { * the spacing of a view again whenever the view shows another element. */ private static final Map shelfCardItems = new WeakHashMap<>(); + /** + * Lists of horizontal shelves whose padding was changed while they were scrolled to the start. + */ + private static final Map shelfListsAtStart = new WeakHashMap<>(); /** * The list item whose spacing is set, until its element is known. */ @@ -1405,6 +1414,79 @@ && isCardLabel(item.getContentDescription())) { } } + // The card of a horizontal shelf clips its list, and the items of some shelves have + // no padding, so their content is cut by the edges of the card. The padding of the list + // moves the content of the first item, and of the last item at the end of the list, + // inside the card. + for (int i = 0, count = current.getChildCount(); i < count; i++) { + View shelf = current.getChildAt(i); + if (shelf.getHeight() <= 0 + || !(shelfCardItems.containsKey(shelf) || isShelfHeader(shelf)) + || !(findNestedList(shelf, 0) instanceof ViewGroup shelfList)) { + continue; + } + + View first = null; + View last = null; + for (int j = 0, listCount = shelfList.getChildCount(); j < listCount; j++) { + View child = shelfList.getChildAt(j); + if (child.getWidth() <= 0) { + continue; + } + if (first == null || child.getLeft() < first.getLeft()) { + first = child; + } + if (last == null || child.getRight() > last.getRight()) { + last = child; + } + } + if (first == null) { + continue; + } + // The list keeps its scroll position when its padding changes, which moves the first + // item before the padding, so the list is scrolled back to its start. + if (shelfListsAtStart.remove(shelfList) != null + && first.getLeft() != shelfList.getPaddingLeft()) { + shelfList.scrollBy(first.getLeft() - shelfList.getPaddingLeft(), 0); + continue; + } + + int[] shelfLocation = new int[2]; + shelf.getLocationOnScreen(shelfLocation); + int[] listLocation = new int[2]; + shelfList.getLocationOnScreen(listLocation); + final int cardInset = Dim.dp(CARD_HORIZONTAL_PADDING_DP + SHELF_LIST_PADDING_DP); + int[] content = new int[2]; + int paddingLeft = shelfList.getPaddingLeft(); + int paddingRight = shelfList.getPaddingRight(); + + // The first item is the leftmost one only while the list is scrolled to the start. + if (!shelfList.canScrollHorizontally(-1) && getContentBounds(first, content)) { + paddingLeft += shelfLocation[0] + cardInset + - (listLocation[0] + first.getLeft() + content[0]); + } + // The last item is at the end of the list only if the items don't fit the list. + if (shelfList.canScrollHorizontally(-1) && !shelfList.canScrollHorizontally(1) + && getContentBounds(last, content)) { + paddingRight += listLocation[0] + last.getLeft() + content[1] + - (shelfLocation[0] + shelf.getWidth() - cardInset); + } + + paddingLeft = Math.max(0, paddingLeft); + paddingRight = Math.max(0, paddingRight); + if (paddingLeft == shelfList.getPaddingLeft() + && paddingRight == shelfList.getPaddingRight()) { + continue; + } + if (paddingLeft != shelfList.getPaddingLeft()) { + shelfListsAtStart.put(shelfList, Boolean.TRUE); + } + // The items are drawn in the padding while scrolling, up to the edges of the card. + shelfList.setClipToPadding(false); + shelfList.setPadding(paddingLeft, shelfList.getPaddingTop(), paddingRight, + shelfList.getPaddingBottom()); + } + // Updates the clips of the Shorts of the Shorts shelves, without drawing the shelves. shortsCellOutlines.clear(); for (int i = 0, count = current.getChildCount(); i < count; i++) { @@ -1843,17 +1925,52 @@ public void getOutline(View view, Outline outline) { } private static boolean hasNestedList(View view, int depth) { + return findNestedList(view, depth) != null; + } + + @Nullable + private static View findNestedList(View view, int depth) { if (depth > 0 && view instanceof RecyclerView) { - return true; + return view; } if (depth < 4 && view instanceof ViewGroup group) { for (int i = 0, count = group.getChildCount(); i < count; i++) { - if (hasNestedList(group.getChildAt(i), depth + 1)) { - return true; + View list = findNestedList(group.getChildAt(i), depth + 1); + if (list != null) { + return list; } } } - return false; + return null; + } + + /** + * @param bounds The left and the right edge of the views inside the item, relative to the item. + * @return If the item has visible views. + */ + private static boolean getContentBounds(View item, int[] bounds) { + bounds[0] = Integer.MAX_VALUE; + bounds[1] = Integer.MIN_VALUE; + addContentBounds(item, 0, bounds, 0); + return bounds[0] < bounds[1]; + } + + /** + * The content is in the views without children, such as the thumbnail and the text of the item. + */ + private static void addContentBounds(View view, int left, int[] bounds, int depth) { + if (view.getVisibility() != View.VISIBLE || view.getWidth() <= 0 || view.getHeight() <= 0) { + return; + } + if (depth < 8 && view instanceof ViewGroup group && group.getChildCount() > 0) { + for (int i = 0, count = group.getChildCount(); i < count; i++) { + View child = group.getChildAt(i); + addContentBounds(child, left + child.getLeft(), bounds, depth + 1); + } + return; + } + bounds[0] = Math.min(bounds[0], left); + bounds[1] = Math.max(bounds[1], left + view.getWidth()); } @Nullable From 64e3e0ec5eb8f98e8f8f7e11ba754fc52c3af47f Mon Sep 17 00:00:00 2001 From: pharmacomaniac Date: Sat, 24 Oct 2026 03:06:36 -0400 Subject: [PATCH 026/189] fix(YouTube - Disable continue watching prompt): Prompt can still appear during playback (#3996) --- .../DisableContinueWatchingPromptPatch.kt | 9 ++++-- .../continuewatching/Fingerprints.kt | 30 +++++++++++++++++++ 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/continuewatching/DisableContinueWatchingPromptPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/continuewatching/DisableContinueWatchingPromptPatch.kt index 3837a7bbcc..facaf00482 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/continuewatching/DisableContinueWatchingPromptPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/continuewatching/DisableContinueWatchingPromptPatch.kt @@ -42,9 +42,12 @@ val disableContinueWatchingPromptPatch = bytecodePatch( SwitchPreference("morphe_disable_continue_watching_prompt") ) - AutoplayInactivityLimitFingerprint.let { - it.method.apply { - val index = it.instructionMatches[3].index + mapOf( + AutoplayInactivityLimitFingerprint to 3, + InactivityLimitFingerprint to 6 + ).forEach { (fingerprint, matchIndex) -> + fingerprint.method.apply { + val index = fingerprint.instructionMatches[matchIndex].index val register = getInstruction(index).registerA addInstructions( diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/continuewatching/Fingerprints.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/continuewatching/Fingerprints.kt index 61739f1feb..13e7e40415 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/continuewatching/Fingerprints.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/continuewatching/Fingerprints.kt @@ -53,6 +53,36 @@ internal object AutoplayInactivityLimitFingerprint : Fingerprint( ) ) +// Playback progress can trigger the prompt independently of the autoplay overlay. +internal object InactivityLimitFingerprint : Fingerprint( + accessFlags = listOf(AccessFlags.PUBLIC, AccessFlags.FINAL), + returnType = "V", + parameters = listOf("Ljava/lang/Object;"), + filters = listOf( + literal(126827209L), + fieldAccess( + opcode = Opcode.SGET_OBJECT, + definingClass = "Ljava/util/concurrent/TimeUnit;", + name = "SECONDS" + ), + methodCall( + opcode = Opcode.INVOKE_VIRTUAL, + definingClass = "Ljava/util/concurrent/TimeUnit;", + name = "toMillis", + location = MatchAfterWithin(5) + ), + opcode(Opcode.CMP_LONG, location = MatchAfterWithin(2)), + opcode(Opcode.IF_LEZ, location = MatchAfterImmediately()), + fieldAccess( + opcode = Opcode.SGET_OBJECT, + definingClass = "Ljava/util/concurrent/TimeUnit;", + name = "MILLISECONDS", + location = MatchAfterImmediately() + ), + opcode(Opcode.IF_GTZ, location = MatchAfterWithin(20)) + ) +) + /** * Autoplay overlay method that decides if the 'Video paused. Continue watching?' confirm dialog * is shown when a video ends. From 6c413e5b7bab279add97ac084b39b0d44c8ca059 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Sat, 24 Oct 2026 08:33:25 +0000 Subject: [PATCH 027/189] bump(YouTube): Add experimental support for `21.43.523` --- .../interaction/channelsearch/Fingerprints.kt | 33 +++++++++++++++++-- .../hide/ambientmode/AmbientModePatch.kt | 17 +++++++--- .../layout/hide/ambientmode/Fingerprints.kt | 29 +++++++++------- .../youtube/layout/miniplayer/Fingerprints.kt | 17 ++++++++-- .../layout/miniplayer/MiniplayerPatch.kt | 2 +- .../youtube/misc/navigation/Fingerprints.kt | 16 +++++---- .../misc/playservice/VersionCheckPatch.kt | 3 ++ .../patches/youtube/shared/Constants.kt | 4 +-- 8 files changed, 91 insertions(+), 30 deletions(-) diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/channelsearch/Fingerprints.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/channelsearch/Fingerprints.kt index ce10dc6780..cabe0f192e 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/channelsearch/Fingerprints.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/channelsearch/Fingerprints.kt @@ -84,7 +84,24 @@ internal object SearchSubmitFingerprint : Fingerprint( accessFlags = listOf(AccessFlags.PUBLIC, AccessFlags.FINAL), filters = listOf( anyInstruction( - methodCall( // 21.31+ + methodCall( // 21.43+ + parameters = listOf( + "Ljava/lang/String;", + "[B", + "Ljava/lang/String;", + "I", + "L", + "L", + "Ljava/lang/String;", + "Ljava/lang/String;", + "Ljava/lang/String;", + "Ljava/lang/String;", + "Z", + "Ljava/lang/String;", + ), + returnType = "V" + ), + methodCall( // 21.31 - 21.42 parameters = listOf( "Ljava/lang/String;", "[B", @@ -118,7 +135,19 @@ internal object SearchSubmitFingerprint : Fingerprint( ) ), custom = { method, _ -> - parametersMatch( // 21.31+ + parametersMatch( // 21.43+ + method.parameters, + listOf( + "Ljava/lang/String;", + "I", + "Ljava/lang/String;", + "Ljava/lang/String;", + "Ljava/lang/String;", + "Ljava/lang/String;", + "Z", + "Ljava/lang/String;", + ) + ) || parametersMatch( // 21.31 - 21.42 method.parameters, listOf( "Ljava/lang/String;", diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/ambientmode/AmbientModePatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/ambientmode/AmbientModePatch.kt index a4d4e9e64f..b1f28d5dd2 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/ambientmode/AmbientModePatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/ambientmode/AmbientModePatch.kt @@ -10,6 +10,7 @@ package app.morphe.patches.youtube.layout.hide.ambientmode +import app.morphe.patcher.Fingerprint import app.morphe.patcher.extensions.InstructionExtensions.addInstructions import app.morphe.patcher.extensions.InstructionExtensions.addInstructionsWithLabels import app.morphe.patcher.extensions.InstructionExtensions.getInstruction @@ -27,6 +28,7 @@ import app.morphe.patches.shared.misc.settings.preference.SwitchPreference import app.morphe.patches.youtube.misc.extension.sharedExtensionPatch import app.morphe.patches.youtube.misc.playservice.is_21_02_or_greater import app.morphe.patches.youtube.misc.playservice.is_21_03_or_greater +import app.morphe.patches.youtube.misc.playservice.is_21_43_or_greater import app.morphe.patches.youtube.misc.playservice.versionCheckPatch import app.morphe.patches.youtube.misc.settings.PreferenceScreen import app.morphe.patches.youtube.misc.settings.settingsPatch @@ -115,10 +117,12 @@ val ambientModePatch = bytecodePatch( } } - val intentActionFingerprints = mutableListOf( - IntentActionBroadcastReceiverFingerprint, - IntentActionSyntheticFingerprint + val intentActionFingerprints = mutableListOf( + IntentActionBroadcastReceiverFingerprint ) + if (!is_21_43_or_greater) { + intentActionFingerprints += IntentActionSyntheticFingerprint + } if (is_21_02_or_greater) { intentActionFingerprints += IntentActionBroadcastReceiverAlternativeFingerprint } @@ -139,8 +143,11 @@ val ambientModePatch = bytecodePatch( } } } - if (is_21_03_or_greater) { + + if (is_21_43_or_greater) { IntentActionSyntheticAlternativeFingerprint.method.hook() + } else if (is_21_03_or_greater) { + IntentActionSyntheticAlternativeLegacyFingerprint.method.hook() } // @@ -150,7 +157,7 @@ val ambientModePatch = bytecodePatch( // Feature flags. 21.30+ inlines the flag lookups and must patch 4 places for each flag. // Each flag is on if ambient mode is not disabled. - AmbientModeFeatureFlagFingerprint.matchAllMethodIndicesForEach { index -> + if (!is_21_43_or_greater) AmbientModeFeatureFlagFingerprint.matchAllMethodIndicesForEach { index -> val moveResultIndex = indexOfFirstInstructionOrThrow(index, Opcode.MOVE_RESULT) val register = getInstruction(moveResultIndex).registerA diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/ambientmode/Fingerprints.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/ambientmode/Fingerprints.kt index 95886f9814..1a7db88da7 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/ambientmode/Fingerprints.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/ambientmode/Fingerprints.kt @@ -17,19 +17,12 @@ private const val GET_ACTION_METHOD_CALL = "Landroid/content/Intent;->getAction( internal const val IS_POWER_SAVE_MODE_METHOD_CALL = "Landroid/os/PowerManager;->isPowerSaveMode()Z" private const val POWER_SAVE_MODE_CHANGED = "android.os.action.POWER_SAVE_MODE_CHANGED" -/** - * The ambient mode controllers (flyout menu item and background) are created if any flag is on. - */ -internal val AMBIENT_MODE_FEATURE_FLAGS = listOf( - 45376186L, - 45372814L, -) - internal object AmbientModeFeatureFlagFingerprint : Fingerprint( filters = listOf( anyInstruction( - *AMBIENT_MODE_FEATURE_FLAGS.map { literal(it) }.toTypedArray() - ), + literal(45376186L), + literal(45372814L) + ) ) ) @@ -200,9 +193,23 @@ internal object IntentActionSyntheticFingerprint : Fingerprint( ) /** - * YT 21.03+ + * 21.43+ */ internal object IntentActionSyntheticAlternativeFingerprint : Fingerprint( + accessFlags = listOf(AccessFlags.PUBLIC, AccessFlags.FINAL), + returnType = "L", + parameters = listOf(), + filters = listOf( + checkCast("Landroid/os/PowerManager;"), + methodCall(smali = IS_POWER_SAVE_MODE_METHOD_CALL), + string(POWER_SAVE_MODE_CHANGED), + ) +) + +/** + * YT 21.03 - 21.42 + */ +internal object IntentActionSyntheticAlternativeLegacyFingerprint : Fingerprint( accessFlags = listOf(AccessFlags.PUBLIC, AccessFlags.FINAL), returnType = "V", parameters = listOf("Ljava/lang/Object;"), diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/miniplayer/Fingerprints.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/miniplayer/Fingerprints.kt index baa542f658..418f42d159 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/miniplayer/Fingerprints.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/miniplayer/Fingerprints.kt @@ -91,10 +91,21 @@ internal object MiniplayerAnimatedExpandFingerprint : Fingerprint( returnType = "V", parameters = listOf("I"), filters = listOf( + literal(45838209L), opcode(opcode = Opcode.IF_NE), - opcode(opcode = Opcode.IGET_OBJECT, location = MatchAfterImmediately()), + fieldAccess( + opcode = Opcode.IGET_OBJECT, + definingClass = "this", + type = "L", + location = MatchAfterImmediately() + ), opcode(opcode = Opcode.MOVE, location = MatchAfterWithin(7)), - opcode(opcode = Opcode.IGET_OBJECT, location = MatchAfterImmediately()) + fieldAccess( + opcode = Opcode.IGET_OBJECT, + definingClass = "this", + type = "L", + location = MatchAfterWithin(7) + ) ) ) @@ -427,7 +438,7 @@ internal object ShowMiniplayerCommandFingerprint: Fingerprint( ), opcode( opcode = Opcode.IF_EQZ, - location = MatchAfterImmediately() + location = MatchAfterWithin(10) ) ) ) diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/miniplayer/MiniplayerPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/miniplayer/MiniplayerPatch.kt index 691313fccc..60cf9f48c8 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/miniplayer/MiniplayerPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/miniplayer/MiniplayerPatch.kt @@ -466,7 +466,7 @@ val miniplayerPatch = bytecodePatch( } else { MiniplayerAnimatedExpandFingerprint.let { it.method.apply { - val insertIndex = it.instructionMatches.first().index + val insertIndex = it.instructionMatches[1].index val labelIndex = it.instructionMatches.last().index val free = findFreeRegister(insertIndex) diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/misc/navigation/Fingerprints.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/navigation/Fingerprints.kt index 887f9232df..f190c4032a 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/misc/navigation/Fingerprints.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/navigation/Fingerprints.kt @@ -15,9 +15,9 @@ import app.morphe.patcher.InstructionLocation.MatchAfterWithin import app.morphe.patcher.checkCast import app.morphe.patcher.methodCall import app.morphe.patcher.opcode -import app.morphe.patcher.string import app.morphe.patcher.resource.ResourceType import app.morphe.patcher.resourceLiteral +import app.morphe.patcher.string import app.morphe.patches.youtube.layout.buttons.navigation.navigationBarPatch import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode @@ -82,13 +82,17 @@ internal object NavigationEnumFingerprint : Fingerprint( ) internal object PivotBarButtonsCreateDrawableViewFingerprint : Fingerprint( - definingClass = "Lcom/google/android/libraries/youtube/rendering/ui/pivotbar/PivotBar;", + classFingerprint = Fingerprint( + definingClass = "Lcom/google/android/libraries/youtube/rendering/ui/pivotbar/PivotBar;", + filters = listOf( + string("Could not load default drawable for pivot bar tab.") + ) + ), accessFlags = listOf(AccessFlags.PUBLIC, AccessFlags.FINAL), returnType = "Landroid/view/View;", - custom = { method, _ -> - // Only one view creation method has a Drawable parameter. - method.parameterTypes.firstOrNull() == "Landroid/graphics/drawable/Drawable;" - } + parameters = listOf( + "L", "Z" ,"I", "L" + ) ) internal object PivotBarButtonsCreateResourceStyledViewFingerprint : Fingerprint( diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/misc/playservice/VersionCheckPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/playservice/VersionCheckPatch.kt index 9296b5564a..87107969f1 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/misc/playservice/VersionCheckPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/playservice/VersionCheckPatch.kt @@ -113,6 +113,8 @@ var is_21_41_or_greater : Boolean by Delegates.notNull() private set var is_21_42_or_greater : Boolean by Delegates.notNull() private set +var is_21_43_or_greater : Boolean by Delegates.notNull() + private set val versionCheckPatch = bytecodePatch { execute { @@ -174,5 +176,6 @@ val versionCheckPatch = bytecodePatch { is_21_40_or_greater = isEqualsOrGreaterThan("21.40.000") is_21_41_or_greater = isEqualsOrGreaterThan("21.41.000") is_21_42_or_greater = isEqualsOrGreaterThan("21.42.000") + is_21_43_or_greater = isEqualsOrGreaterThan("21.43.000") } } diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/shared/Constants.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/shared/Constants.kt index 1b09e7e3b3..9a739172f5 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/shared/Constants.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/shared/Constants.kt @@ -18,12 +18,12 @@ internal object Constants { ), targets = listOf( AppTarget( - version = "21.42.241", + version = "21.43.523", minSdk = 29, isExperimental = true ), AppTarget( - version = "21.41.525", + version = "21.42.241", minSdk = 29, isExperimental = true ), From 3881d65303c19c6f18864feefcb7a6fc4f6e356d Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Sat, 24 Oct 2026 08:46:52 +0000 Subject: [PATCH 028/189] chore: Release v1.49.0-dev.2 [skip ci] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## [1.49.0-dev.2](https://github.com/MorpheApp/morphe-patches/compare/v1.49.0-dev.1...v1.49.0-dev.2) (2026-10-24) ### 🐛 Bug Fixes * **YouTube - Disable continue watching prompt:** Prompt can still appear during playback ([#3996](https://github.com/MorpheApp/morphe-patches/issues/3996)) ([64e3e0e](https://github.com/MorpheApp/morphe-patches/commit/64e3e0ec5eb8f98e8f8f7e11ba754fc52c3af47f)) * **YouTube - Feed card layout:** Horizontal shelves lose the card background when their header is scrolled out of the list ([60489d8](https://github.com/MorpheApp/morphe-patches/commit/60489d84d49c85c44b8007af2de39ee6b82147cb)) * **YouTube - Feed card layout:** Videos of the horizontal shelves are cut by the left edge of the card ([c0ec5ae](https://github.com/MorpheApp/morphe-patches/commit/c0ec5ae17781305f490e589d2d53a6bc01cead46)) * **YouTube - Hide status bar:** Status bar covers the top bar or leaves an empty bar above it ([#3997](https://github.com/MorpheApp/morphe-patches/issues/3997)) ([bf6fbe0](https://github.com/MorpheApp/morphe-patches/commit/bf6fbe0c75728741f7d51a11a20e3bf2312faba9)) * **YouTube - Navigation bar:** Empty band below the reply bar of the comment replies ([bac5823](https://github.com/MorpheApp/morphe-patches/commit/bac5823bdd42beba2dd79f94010e2001b33017f7)) * **YouTube - Navigation bar:** No connection and back online bars are covered by the system navigation buttons ([2576962](https://github.com/MorpheApp/morphe-patches/commit/2576962149ab22aadc16a26853c9b7154b0d9322)) * **YouTube - Navigation bar:** System navigation buttons cannot be seen in the Shorts player with the light theme ([0115896](https://github.com/MorpheApp/morphe-patches/commit/011589657684ba34a7523f3b17af152c5962ae6e)) * **YouTube - Navigation buttons:** Create button is not hidden with the new navigation bar layout ([47f511c](https://github.com/MorpheApp/morphe-patches/commit/47f511c7ae41de2b2ac83257d5a93c4df1216526)) * **YouTube - Playback speed:** Channel speed whitelist does not work for Shorts ([be733a6](https://github.com/MorpheApp/morphe-patches/commit/be733a6883cdf4c5fc8176668afb1e7f5c22cb9f)) * **YouTube - Shorts autoplay:** Overlay is out of place when going back to a Short that advanced with the comments open ([60efeb5](https://github.com/MorpheApp/morphe-patches/commit/60efeb58d08ba0e347928c7f4f903a689b474bfd)) * **YouTube Music - Downloads:** Fix missing metadata in downloaded songs ([#3984](https://github.com/MorpheApp/morphe-patches/issues/3984)) ([7988621](https://github.com/MorpheApp/morphe-patches/commit/7988621db650eb2c32e7cab53731508ad733d5b9)) * **YouTube Music - Downloads:** Fix wrong song downloads ([#3978](https://github.com/MorpheApp/morphe-patches/issues/3978)) ([7259e27](https://github.com/MorpheApp/morphe-patches/commit/7259e27ade3bd1f76cd89f2ecad07d86f39d0335)) * **YouTube:** Morphe flyout menu items are missing in the new popup menu ([d2accff](https://github.com/MorpheApp/morphe-patches/commit/d2accff5135170a4288f326d89b25a81f688f063)) ### ✨ New Features * **YouTube Music - Third-party lyrics:** enhance sync engine, providers, AI translation, and RTL word sync ([#3877](https://github.com/MorpheApp/morphe-patches/issues/3877)) ([e092212](https://github.com/MorpheApp/morphe-patches/commit/e0922124073fe53b6ff335893b8208af512f34e7)) ### 🚀 Updated App Support * **YouTube:** Add experimental support for `21.43.523` ([6c413e5](https://github.com/MorpheApp/morphe-patches/commit/6c413e5b7bab279add97ac084b39b0d44c8ca059)) --- CHANGELOG.md | 26 +++ README.md | 4 +- gradle.properties | 2 +- patches-bundle.json | 8 +- patches-list.json | 426 ++++++++++++++++++++++---------------------- 5 files changed, 246 insertions(+), 220 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 590a9f9b35..5e3f30cd9f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,29 @@ +## [1.49.0-dev.2](https://github.com/MorpheApp/morphe-patches/compare/v1.49.0-dev.1...v1.49.0-dev.2) (2026-10-24) + +### 🐛 Bug Fixes + +* **YouTube - Disable continue watching prompt:** Prompt can still appear during playback ([#3996](https://github.com/MorpheApp/morphe-patches/issues/3996)) ([64e3e0e](https://github.com/MorpheApp/morphe-patches/commit/64e3e0ec5eb8f98e8f8f7e11ba754fc52c3af47f)) +* **YouTube - Feed card layout:** Horizontal shelves lose the card background when their header is scrolled out of the list ([60489d8](https://github.com/MorpheApp/morphe-patches/commit/60489d84d49c85c44b8007af2de39ee6b82147cb)) +* **YouTube - Feed card layout:** Videos of the horizontal shelves are cut by the left edge of the card ([c0ec5ae](https://github.com/MorpheApp/morphe-patches/commit/c0ec5ae17781305f490e589d2d53a6bc01cead46)) +* **YouTube - Hide status bar:** Status bar covers the top bar or leaves an empty bar above it ([#3997](https://github.com/MorpheApp/morphe-patches/issues/3997)) ([bf6fbe0](https://github.com/MorpheApp/morphe-patches/commit/bf6fbe0c75728741f7d51a11a20e3bf2312faba9)) +* **YouTube - Navigation bar:** Empty band below the reply bar of the comment replies ([bac5823](https://github.com/MorpheApp/morphe-patches/commit/bac5823bdd42beba2dd79f94010e2001b33017f7)) +* **YouTube - Navigation bar:** No connection and back online bars are covered by the system navigation buttons ([2576962](https://github.com/MorpheApp/morphe-patches/commit/2576962149ab22aadc16a26853c9b7154b0d9322)) +* **YouTube - Navigation bar:** System navigation buttons cannot be seen in the Shorts player with the light theme ([0115896](https://github.com/MorpheApp/morphe-patches/commit/011589657684ba34a7523f3b17af152c5962ae6e)) +* **YouTube - Navigation buttons:** Create button is not hidden with the new navigation bar layout ([47f511c](https://github.com/MorpheApp/morphe-patches/commit/47f511c7ae41de2b2ac83257d5a93c4df1216526)) +* **YouTube - Playback speed:** Channel speed whitelist does not work for Shorts ([be733a6](https://github.com/MorpheApp/morphe-patches/commit/be733a6883cdf4c5fc8176668afb1e7f5c22cb9f)) +* **YouTube - Shorts autoplay:** Overlay is out of place when going back to a Short that advanced with the comments open ([60efeb5](https://github.com/MorpheApp/morphe-patches/commit/60efeb58d08ba0e347928c7f4f903a689b474bfd)) +* **YouTube Music - Downloads:** Fix missing metadata in downloaded songs ([#3984](https://github.com/MorpheApp/morphe-patches/issues/3984)) ([7988621](https://github.com/MorpheApp/morphe-patches/commit/7988621db650eb2c32e7cab53731508ad733d5b9)) +* **YouTube Music - Downloads:** Fix wrong song downloads ([#3978](https://github.com/MorpheApp/morphe-patches/issues/3978)) ([7259e27](https://github.com/MorpheApp/morphe-patches/commit/7259e27ade3bd1f76cd89f2ecad07d86f39d0335)) +* **YouTube:** Morphe flyout menu items are missing in the new popup menu ([d2accff](https://github.com/MorpheApp/morphe-patches/commit/d2accff5135170a4288f326d89b25a81f688f063)) + +### ✨ New Features + +* **YouTube Music - Third-party lyrics:** enhance sync engine, providers, AI translation, and RTL word sync ([#3877](https://github.com/MorpheApp/morphe-patches/issues/3877)) ([e092212](https://github.com/MorpheApp/morphe-patches/commit/e0922124073fe53b6ff335893b8208af512f34e7)) + +### 🚀 Updated App Support + +* **YouTube:** Add experimental support for `21.43.523` ([6c413e5](https://github.com/MorpheApp/morphe-patches/commit/6c413e5b7bab279add97ac084b39b0d44c8ca059)) + ## [1.49.0-dev.1](https://github.com/MorpheApp/morphe-patches/compare/v1.48.0...v1.49.0-dev.1) (2026-10-23) ### 🐛 Bug Fixes diff --git a/README.md b/README.md index 7d3ef061f0..3e4a43c326 100644 --- a/README.md +++ b/README.md @@ -35,14 +35,14 @@ All modifications made by Morphe, along with their dates, can be found in the Gi ## 🩹 Patches list -> **[v1.49.0-dev.1](https://github.com/MorpheApp/morphe-patches/releases/tag/v1.49.0-dev.1)**  •  `dev`  •  191 patches total +> **[v1.49.0-dev.2](https://github.com/MorpheApp/morphe-patches/releases/tag/v1.49.0-dev.2)**  •  `dev`  •  191 patches total

📦 YouTube  •  106 patches
**🎯 Supported versions:** -| 🧪 21.42.241 | 🧪 21.41.525 | 🧪 21.28.208 | 21.23.492 | 21.20.405 | 21.16.256 | 20.51.39 | 20.21.37 | +| 🧪 21.43.523 | 🧪 21.42.241 | 🧪 21.28.208 | 21.23.492 | 21.20.405 | 21.16.256 | 20.51.39 | 20.21.37 | | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | | 💊 Patch | 📜 Description | ⚙️ Options | diff --git a/gradle.properties b/gradle.properties index e81bde1a5c..b44034b6be 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,4 +3,4 @@ org.gradle.jvmargs = -Xms512M -Xmx2048M org.gradle.parallel = true android.useAndroidX = true kotlin.code.style = official -version = 1.49.0-dev.1 +version = 1.49.0-dev.2 diff --git a/patches-bundle.json b/patches-bundle.json index b0c87490c8..7f5dc0a349 100644 --- a/patches-bundle.json +++ b/patches-bundle.json @@ -1,7 +1,7 @@ { - "created_at": "2026-10-23T10:56:14", - "description": "## [1.49.0-dev.1](https://github.com/MorpheApp/morphe-patches/compare/v1.48.0...v1.49.0-dev.1) (2026-10-23)\n\n### 🐛 Bug Fixes\n\n* **YouTube - Hide status bar:** Video player is moved below the display cutout in portrait ([f80398e](https://github.com/MorpheApp/morphe-patches/commit/f80398e107587c7fb9f995f508a65ad1953b2734))\n* **YouTube - Navigation bar:** Translucent navigation bar is not shown without the edge to edge layout ([b8e1fdd](https://github.com/MorpheApp/morphe-patches/commit/b8e1fddd8a9d7146f2ccc4d1522a86b134cfe7a6))\n* **YouTube:** Miniplayer covered by the keyboard and empty band above the navigation bar ([0345136](https://github.com/MorpheApp/morphe-patches/commit/0345136b552c49b70c01fcc19a18762f57d3b7ad))\n* **YouTube:** Morphe flyout menu items are missing on tablets ([7127541](https://github.com/MorpheApp/morphe-patches/commit/71275419c5e6b8bd6a0a108e3d7815c8ef11a88e))\n\n### ✨ New Features\n\n* **YouTube - Navigation bar:** Add \"Disable toolbar navigation tabs\" setting ([01e767c](https://github.com/MorpheApp/morphe-patches/commit/01e767c4c3a386e4b7881ce7fc867a516082db23))\n* **YouTube:** Add `Search comments` patch ([#3942](https://github.com/MorpheApp/morphe-patches/issues/3942)) ([fbb628b](https://github.com/MorpheApp/morphe-patches/commit/fbb628ba23ecdcabd00b3bdfbf94aff60f58ac68))\n\n### 🚀 Updated App Support\n\n* **YouTube:** Add support for `21.23.492` ([4599204](https://github.com/MorpheApp/morphe-patches/commit/45992041db85e4fb51900cc5a773380732dc1fda))", - "download_url": "https://github.com/MorpheApp/morphe-patches/releases/download/v1.49.0-dev.1/patches-1.49.0-dev.1.mpp", + "created_at": "2026-10-24T08:44:29", + "description": "## [1.49.0-dev.2](https://github.com/MorpheApp/morphe-patches/compare/v1.49.0-dev.1...v1.49.0-dev.2) (2026-10-24)\n\n### 🐛 Bug Fixes\n\n* **YouTube - Disable continue watching prompt:** Prompt can still appear during playback ([#3996](https://github.com/MorpheApp/morphe-patches/issues/3996)) ([64e3e0e](https://github.com/MorpheApp/morphe-patches/commit/64e3e0ec5eb8f98e8f8f7e11ba754fc52c3af47f))\n* **YouTube - Feed card layout:** Horizontal shelves lose the card background when their header is scrolled out of the list ([60489d8](https://github.com/MorpheApp/morphe-patches/commit/60489d84d49c85c44b8007af2de39ee6b82147cb))\n* **YouTube - Feed card layout:** Videos of the horizontal shelves are cut by the left edge of the card ([c0ec5ae](https://github.com/MorpheApp/morphe-patches/commit/c0ec5ae17781305f490e589d2d53a6bc01cead46))\n* **YouTube - Hide status bar:** Status bar covers the top bar or leaves an empty bar above it ([#3997](https://github.com/MorpheApp/morphe-patches/issues/3997)) ([bf6fbe0](https://github.com/MorpheApp/morphe-patches/commit/bf6fbe0c75728741f7d51a11a20e3bf2312faba9))\n* **YouTube - Navigation bar:** Empty band below the reply bar of the comment replies ([bac5823](https://github.com/MorpheApp/morphe-patches/commit/bac5823bdd42beba2dd79f94010e2001b33017f7))\n* **YouTube - Navigation bar:** No connection and back online bars are covered by the system navigation buttons ([2576962](https://github.com/MorpheApp/morphe-patches/commit/2576962149ab22aadc16a26853c9b7154b0d9322))\n* **YouTube - Navigation bar:** System navigation buttons cannot be seen in the Shorts player with the light theme ([0115896](https://github.com/MorpheApp/morphe-patches/commit/011589657684ba34a7523f3b17af152c5962ae6e))\n* **YouTube - Navigation buttons:** Create button is not hidden with the new navigation bar layout ([47f511c](https://github.com/MorpheApp/morphe-patches/commit/47f511c7ae41de2b2ac83257d5a93c4df1216526))\n* **YouTube - Playback speed:** Channel speed whitelist does not work for Shorts ([be733a6](https://github.com/MorpheApp/morphe-patches/commit/be733a6883cdf4c5fc8176668afb1e7f5c22cb9f))\n* **YouTube - Shorts autoplay:** Overlay is out of place when going back to a Short that advanced with the comments open ([60efeb5](https://github.com/MorpheApp/morphe-patches/commit/60efeb58d08ba0e347928c7f4f903a689b474bfd))\n* **YouTube Music - Downloads:** Fix missing metadata in downloaded songs ([#3984](https://github.com/MorpheApp/morphe-patches/issues/3984)) ([7988621](https://github.com/MorpheApp/morphe-patches/commit/7988621db650eb2c32e7cab53731508ad733d5b9))\n* **YouTube Music - Downloads:** Fix wrong song downloads ([#3978](https://github.com/MorpheApp/morphe-patches/issues/3978)) ([7259e27](https://github.com/MorpheApp/morphe-patches/commit/7259e27ade3bd1f76cd89f2ecad07d86f39d0335))\n* **YouTube:** Morphe flyout menu items are missing in the new popup menu ([d2accff](https://github.com/MorpheApp/morphe-patches/commit/d2accff5135170a4288f326d89b25a81f688f063))\n\n### ✨ New Features\n\n* **YouTube Music - Third-party lyrics:** enhance sync engine, providers, AI translation, and RTL word sync ([#3877](https://github.com/MorpheApp/morphe-patches/issues/3877)) ([e092212](https://github.com/MorpheApp/morphe-patches/commit/e0922124073fe53b6ff335893b8208af512f34e7))\n\n### 🚀 Updated App Support\n\n* **YouTube:** Add experimental support for `21.43.523` ([6c413e5](https://github.com/MorpheApp/morphe-patches/commit/6c413e5b7bab279add97ac084b39b0d44c8ca059))", + "download_url": "https://github.com/MorpheApp/morphe-patches/releases/download/v1.49.0-dev.2/patches-1.49.0-dev.2.mpp", "signature_download_url": "", - "version": "1.49.0-dev.1" + "version": "1.49.0-dev.2" } diff --git a/patches-list.json b/patches-list.json index 7cb1f83559..5f849c6439 100644 --- a/patches-list.json +++ b/patches-list.json @@ -1,6 +1,6 @@ { "NOTE": "Do NOT manually edit this file. This file is automatically updated when semantic release (release.yml) runs. Manually editing this file can break your releases and break third party tools that use this file.", - "version": "1.49.0-dev.1", + "version": "1.49.0-dev.2", "patches": [ { "name": "AI channel filter", @@ -21,14 +21,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -100,14 +100,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -179,14 +179,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -431,14 +431,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -510,14 +510,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -589,14 +589,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -668,14 +668,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -747,14 +747,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -826,14 +826,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -973,14 +973,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -1202,14 +1202,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -1281,14 +1281,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -1360,14 +1360,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -1497,14 +1497,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -1671,14 +1671,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -1750,14 +1750,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -1982,14 +1982,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -2282,14 +2282,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -2361,14 +2361,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -2497,14 +2497,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -2634,14 +2634,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -2779,14 +2779,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -2858,14 +2858,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -2937,14 +2937,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -3016,14 +3016,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -3153,14 +3153,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -3232,14 +3232,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -3311,14 +3311,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -3390,14 +3390,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -3526,14 +3526,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -3605,14 +3605,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -3684,14 +3684,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -3820,14 +3820,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -3899,14 +3899,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -3978,14 +3978,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -4057,14 +4057,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -4251,14 +4251,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -4446,14 +4446,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -4699,14 +4699,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -4778,14 +4778,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -4914,14 +4914,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -4993,14 +4993,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -5072,14 +5072,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -5151,14 +5151,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -5288,14 +5288,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -5539,14 +5539,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -5676,14 +5676,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -5869,14 +5869,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -6139,14 +6139,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -6218,14 +6218,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -6412,14 +6412,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -6491,14 +6491,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -6628,14 +6628,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -6765,14 +6765,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -7016,14 +7016,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -7095,14 +7095,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -7174,14 +7174,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -7253,14 +7253,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -7389,14 +7389,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -7468,14 +7468,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -7547,14 +7547,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -7741,14 +7741,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -7878,14 +7878,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -8071,14 +8071,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -8208,14 +8208,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -8345,14 +8345,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -8501,14 +8501,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -8580,14 +8580,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -8659,14 +8659,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -8852,14 +8852,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -8931,14 +8931,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -9010,14 +9010,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -9089,14 +9089,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -9176,14 +9176,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -9313,14 +9313,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -9392,14 +9392,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -9471,14 +9471,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -9608,14 +9608,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -9687,14 +9687,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -9892,14 +9892,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -10028,14 +10028,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -10164,14 +10164,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -10417,14 +10417,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -10611,14 +10611,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -10690,14 +10690,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -10827,14 +10827,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -11021,14 +11021,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -11100,14 +11100,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -11237,14 +11237,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -11316,14 +11316,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -11395,14 +11395,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -11532,14 +11532,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -11611,14 +11611,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -11690,14 +11690,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -11836,14 +11836,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -11973,14 +11973,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -12110,14 +12110,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -12247,14 +12247,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -12326,14 +12326,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -12535,14 +12535,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -12671,14 +12671,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -12834,14 +12834,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -13020,14 +13020,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -13099,14 +13099,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -13178,14 +13178,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -13257,14 +13257,14 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.41.525", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, From 6707f0e1215ba3620586b95e5c0f8b8576339a86 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Sat, 24 Oct 2026 09:04:23 +0000 Subject: [PATCH 029/189] fix(Reddit - Redgifs audio): Error toast is shown when network request fails --- .../reddit/patches/RedgifsAudioPatch.java | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/extensions/reddit/src/main/java/app/morphe/extension/reddit/patches/RedgifsAudioPatch.java b/extensions/reddit/src/main/java/app/morphe/extension/reddit/patches/RedgifsAudioPatch.java index b10c1c9afb..b4132a687d 100644 --- a/extensions/reddit/src/main/java/app/morphe/extension/reddit/patches/RedgifsAudioPatch.java +++ b/extensions/reddit/src/main/java/app/morphe/extension/reddit/patches/RedgifsAudioPatch.java @@ -172,7 +172,7 @@ public static void onLinkCreated(Link link) { *

* The feed is not created from posts, and only the post header has the post url. * - * @param cellId Base64 of "MetadataCell-" and the post id. + * @param cellId "MetadataCell-" and the post id, or Base64 of it in older versions. * @param mediaPath Post url. */ public static void onFeedMetadataCellCreated(@Nullable String cellId, @Nullable String mediaPath) { @@ -183,8 +183,10 @@ public static void onFeedMetadataCellCreated(@Nullable String cellId, @Nullable String id = getRedgifsId(mediaPath); if (id == null) return; - String decodedCellId = new String(Base64.decode(cellId, Base64.DEFAULT), StandardCharsets.UTF_8); - if (!decodedCellId.startsWith(METADATA_CELL_ID_PREFIX)) return; + String decodedCellId = cellId.startsWith(METADATA_CELL_ID_PREFIX) + ? cellId + : decodeBase64CellId(cellId); + if (decodedCellId == null || !decodedCellId.startsWith(METADATA_CELL_ID_PREFIX)) return; postIdToRedgifsId.put(decodedCellId.substring(METADATA_CELL_ID_PREFIX.length()), id); getLookup(id); @@ -193,6 +195,15 @@ public static void onFeedMetadataCellCreated(@Nullable String cellId, @Nullable } } + @Nullable + private static String decodeBase64CellId(String cellId) { + try { + return new String(Base64.decode(cellId, Base64.DEFAULT), StandardCharsets.UTF_8); + } catch (IllegalArgumentException ex) { + return null; + } + } + /** * Injection point. Called when a feed video is created. */ From 68b01cad1f9419b4dc0e9141659b4def6c3660a4 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Sat, 24 Oct 2026 09:48:21 +0000 Subject: [PATCH 030/189] fix(YouTube): 21.43 navigation tabs cannot be hidden --- .../extension/youtube/shared/NavigationBar.java | 8 +++----- .../youtube/misc/navigation/Fingerprints.kt | 17 ++++++++--------- .../misc/navigation/NavigationBarHookPatch.kt | 15 +++++++++------ 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/shared/NavigationBar.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/shared/NavigationBar.java index 5067cdd73b..ddef02246b 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/shared/NavigationBar.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/shared/NavigationBar.java @@ -462,17 +462,15 @@ public static void navigationTabLoaded(final View navigationButtonGroup) { /** * Injection point. *

- * Unique hook just for the 'Create' and 'You' tab. + * Unique hook just for the 'Create' and 'You' tab (all tabs in 21.43+). */ public static void navigationImageResourceTabLoaded(View view) { // 'You' tab has no YT enum name and the enum hook is not called for it. // Compare the last enum to figure out which tab this actually is. - if (NavigationBar.NavigationButton.CREATE.ytEnumNames.contains(lastYTNavigationEnumName)) { - navigationTabLoaded(view); - } else { + if (lastYTNavigationEnumName == null) { lastYTNavigationEnumName = NavigationButton.LIBRARY.ytEnumNames.get(0); - navigationTabLoaded(view); } + navigationTabLoaded(view); } /** diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/misc/navigation/Fingerprints.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/navigation/Fingerprints.kt index f190c4032a..db69082dd5 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/misc/navigation/Fingerprints.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/navigation/Fingerprints.kt @@ -81,18 +81,17 @@ internal object NavigationEnumFingerprint : Fingerprint( } ) +/** + * 21.42 and earlier. + */ internal object PivotBarButtonsCreateDrawableViewFingerprint : Fingerprint( - classFingerprint = Fingerprint( - definingClass = "Lcom/google/android/libraries/youtube/rendering/ui/pivotbar/PivotBar;", - filters = listOf( - string("Could not load default drawable for pivot bar tab.") - ) - ), + definingClass = "Lcom/google/android/libraries/youtube/rendering/ui/pivotbar/PivotBar;", accessFlags = listOf(AccessFlags.PUBLIC, AccessFlags.FINAL), returnType = "Landroid/view/View;", - parameters = listOf( - "L", "Z" ,"I", "L" - ) + custom = { method, _ -> + // Only one view creation method has a Drawable parameter. + method.parameterTypes.firstOrNull() == "Landroid/graphics/drawable/Drawable;" + } ) internal object PivotBarButtonsCreateResourceStyledViewFingerprint : Fingerprint( diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/misc/navigation/NavigationBarHookPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/navigation/NavigationBarHookPatch.kt index 80b74a44a6..8b6237fbef 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/misc/navigation/NavigationBarHookPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/navigation/NavigationBarHookPatch.kt @@ -27,6 +27,7 @@ import app.morphe.patches.youtube.misc.backgesture.backGesturePatch import app.morphe.patches.youtube.misc.extension.sharedExtensionPatch import app.morphe.patches.youtube.misc.playertype.playerTypeHookPatch import app.morphe.patches.youtube.misc.playservice.is_20_28_or_greater +import app.morphe.patches.youtube.misc.playservice.is_21_43_or_greater import app.morphe.patches.youtube.misc.playservice.versionCheckPatch import app.morphe.patches.youtube.shared.ActionBarSearchResultsFingerprint import app.morphe.util.ResourceGroup @@ -128,12 +129,14 @@ val navigationBarHookPatch = bytecodePatch(description = "Hooks the active navig } // Hook the creation of navigation tab views. - val drawableTabMethod = PivotBarButtonsCreateDrawableViewFingerprint.method - addHook(NavigationHook.NAVIGATION_TAB_LOADED) predicate@{ - MethodUtil.methodSignaturesMatch( - getReference() ?: return@predicate false, - drawableTabMethod, - ) + if (!is_21_43_or_greater) { + val drawableTabMethod = PivotBarButtonsCreateDrawableViewFingerprint.method + addHook(NavigationHook.NAVIGATION_TAB_LOADED) predicate@{ + MethodUtil.methodSignaturesMatch( + getReference() ?: return@predicate false, + drawableTabMethod, + ) + } } if (!is_20_28_or_greater) { From a10e59639cbe3e97309548b06e39a843e38f420b Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Sat, 24 Oct 2026 09:55:05 +0000 Subject: [PATCH 031/189] chore: Release v1.49.0-dev.3 [skip ci] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## [1.49.0-dev.3](https://github.com/MorpheApp/morphe-patches/compare/v1.49.0-dev.2...v1.49.0-dev.3) (2026-10-24) ### 🐛 Bug Fixes * **Reddit - Redgifs audio:** Error toast is shown when network request fails ([6707f0e](https://github.com/MorpheApp/morphe-patches/commit/6707f0e1215ba3620586b95e5c0f8b8576339a86)) * **YouTube:** 21.43 navigation tabs cannot be hidden ([68b01ca](https://github.com/MorpheApp/morphe-patches/commit/68b01cad1f9419b4dc0e9141659b4def6c3660a4)) --- CHANGELOG.md | 7 +++++++ README.md | 2 +- gradle.properties | 2 +- patches-bundle.json | 8 ++++---- patches-list.json | 2 +- 5 files changed, 14 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e3f30cd9f..811757e9ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## [1.49.0-dev.3](https://github.com/MorpheApp/morphe-patches/compare/v1.49.0-dev.2...v1.49.0-dev.3) (2026-10-24) + +### 🐛 Bug Fixes + +* **Reddit - Redgifs audio:** Error toast is shown when network request fails ([6707f0e](https://github.com/MorpheApp/morphe-patches/commit/6707f0e1215ba3620586b95e5c0f8b8576339a86)) +* **YouTube:** 21.43 navigation tabs cannot be hidden ([68b01ca](https://github.com/MorpheApp/morphe-patches/commit/68b01cad1f9419b4dc0e9141659b4def6c3660a4)) + ## [1.49.0-dev.2](https://github.com/MorpheApp/morphe-patches/compare/v1.49.0-dev.1...v1.49.0-dev.2) (2026-10-24) ### 🐛 Bug Fixes diff --git a/README.md b/README.md index 3e4a43c326..56101aa7b1 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ All modifications made by Morphe, along with their dates, can be found in the Gi ## 🩹 Patches list -> **[v1.49.0-dev.2](https://github.com/MorpheApp/morphe-patches/releases/tag/v1.49.0-dev.2)**  •  `dev`  •  191 patches total +> **[v1.49.0-dev.3](https://github.com/MorpheApp/morphe-patches/releases/tag/v1.49.0-dev.3)**  •  `dev`  •  191 patches total

📦 YouTube  •  106 patches
diff --git a/gradle.properties b/gradle.properties index b44034b6be..4197f97651 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,4 +3,4 @@ org.gradle.jvmargs = -Xms512M -Xmx2048M org.gradle.parallel = true android.useAndroidX = true kotlin.code.style = official -version = 1.49.0-dev.2 +version = 1.49.0-dev.3 diff --git a/patches-bundle.json b/patches-bundle.json index 7f5dc0a349..c9c8b6036a 100644 --- a/patches-bundle.json +++ b/patches-bundle.json @@ -1,7 +1,7 @@ { - "created_at": "2026-10-24T08:44:29", - "description": "## [1.49.0-dev.2](https://github.com/MorpheApp/morphe-patches/compare/v1.49.0-dev.1...v1.49.0-dev.2) (2026-10-24)\n\n### 🐛 Bug Fixes\n\n* **YouTube - Disable continue watching prompt:** Prompt can still appear during playback ([#3996](https://github.com/MorpheApp/morphe-patches/issues/3996)) ([64e3e0e](https://github.com/MorpheApp/morphe-patches/commit/64e3e0ec5eb8f98e8f8f7e11ba754fc52c3af47f))\n* **YouTube - Feed card layout:** Horizontal shelves lose the card background when their header is scrolled out of the list ([60489d8](https://github.com/MorpheApp/morphe-patches/commit/60489d84d49c85c44b8007af2de39ee6b82147cb))\n* **YouTube - Feed card layout:** Videos of the horizontal shelves are cut by the left edge of the card ([c0ec5ae](https://github.com/MorpheApp/morphe-patches/commit/c0ec5ae17781305f490e589d2d53a6bc01cead46))\n* **YouTube - Hide status bar:** Status bar covers the top bar or leaves an empty bar above it ([#3997](https://github.com/MorpheApp/morphe-patches/issues/3997)) ([bf6fbe0](https://github.com/MorpheApp/morphe-patches/commit/bf6fbe0c75728741f7d51a11a20e3bf2312faba9))\n* **YouTube - Navigation bar:** Empty band below the reply bar of the comment replies ([bac5823](https://github.com/MorpheApp/morphe-patches/commit/bac5823bdd42beba2dd79f94010e2001b33017f7))\n* **YouTube - Navigation bar:** No connection and back online bars are covered by the system navigation buttons ([2576962](https://github.com/MorpheApp/morphe-patches/commit/2576962149ab22aadc16a26853c9b7154b0d9322))\n* **YouTube - Navigation bar:** System navigation buttons cannot be seen in the Shorts player with the light theme ([0115896](https://github.com/MorpheApp/morphe-patches/commit/011589657684ba34a7523f3b17af152c5962ae6e))\n* **YouTube - Navigation buttons:** Create button is not hidden with the new navigation bar layout ([47f511c](https://github.com/MorpheApp/morphe-patches/commit/47f511c7ae41de2b2ac83257d5a93c4df1216526))\n* **YouTube - Playback speed:** Channel speed whitelist does not work for Shorts ([be733a6](https://github.com/MorpheApp/morphe-patches/commit/be733a6883cdf4c5fc8176668afb1e7f5c22cb9f))\n* **YouTube - Shorts autoplay:** Overlay is out of place when going back to a Short that advanced with the comments open ([60efeb5](https://github.com/MorpheApp/morphe-patches/commit/60efeb58d08ba0e347928c7f4f903a689b474bfd))\n* **YouTube Music - Downloads:** Fix missing metadata in downloaded songs ([#3984](https://github.com/MorpheApp/morphe-patches/issues/3984)) ([7988621](https://github.com/MorpheApp/morphe-patches/commit/7988621db650eb2c32e7cab53731508ad733d5b9))\n* **YouTube Music - Downloads:** Fix wrong song downloads ([#3978](https://github.com/MorpheApp/morphe-patches/issues/3978)) ([7259e27](https://github.com/MorpheApp/morphe-patches/commit/7259e27ade3bd1f76cd89f2ecad07d86f39d0335))\n* **YouTube:** Morphe flyout menu items are missing in the new popup menu ([d2accff](https://github.com/MorpheApp/morphe-patches/commit/d2accff5135170a4288f326d89b25a81f688f063))\n\n### ✨ New Features\n\n* **YouTube Music - Third-party lyrics:** enhance sync engine, providers, AI translation, and RTL word sync ([#3877](https://github.com/MorpheApp/morphe-patches/issues/3877)) ([e092212](https://github.com/MorpheApp/morphe-patches/commit/e0922124073fe53b6ff335893b8208af512f34e7))\n\n### 🚀 Updated App Support\n\n* **YouTube:** Add experimental support for `21.43.523` ([6c413e5](https://github.com/MorpheApp/morphe-patches/commit/6c413e5b7bab279add97ac084b39b0d44c8ca059))", - "download_url": "https://github.com/MorpheApp/morphe-patches/releases/download/v1.49.0-dev.2/patches-1.49.0-dev.2.mpp", + "created_at": "2026-10-24T09:53:04", + "description": "## [1.49.0-dev.3](https://github.com/MorpheApp/morphe-patches/compare/v1.49.0-dev.2...v1.49.0-dev.3) (2026-10-24)\n\n### 🐛 Bug Fixes\n\n* **Reddit - Redgifs audio:** Error toast is shown when network request fails ([6707f0e](https://github.com/MorpheApp/morphe-patches/commit/6707f0e1215ba3620586b95e5c0f8b8576339a86))\n* **YouTube:** 21.43 navigation tabs cannot be hidden ([68b01ca](https://github.com/MorpheApp/morphe-patches/commit/68b01cad1f9419b4dc0e9141659b4def6c3660a4))", + "download_url": "https://github.com/MorpheApp/morphe-patches/releases/download/v1.49.0-dev.3/patches-1.49.0-dev.3.mpp", "signature_download_url": "", - "version": "1.49.0-dev.2" + "version": "1.49.0-dev.3" } diff --git a/patches-list.json b/patches-list.json index 5f849c6439..dd1d81ffcc 100644 --- a/patches-list.json +++ b/patches-list.json @@ -1,6 +1,6 @@ { "NOTE": "Do NOT manually edit this file. This file is automatically updated when semantic release (release.yml) runs. Manually editing this file can break your releases and break third party tools that use this file.", - "version": "1.49.0-dev.2", + "version": "1.49.0-dev.3", "patches": [ { "name": "AI channel filter", From a9e6562b2a4dbc5d57f8cf65a205206a800d192a Mon Sep 17 00:00:00 2001 From: 0xrxL <127248639+0xrxL@users.noreply.github.com> Date: Sat, 24 Oct 2026 13:12:46 +0200 Subject: [PATCH 032/189] fix(YouTube - Hide status bar): Status bar scroll color and translucent navigation bar do not follow the hidden status bar before the edge to edge flag is read --- .../youtube/patches/HideStatusBarPatch.java | 31 +++++++++++++++++-- .../youtube/patches/NavigationBarPatch.java | 2 +- .../layout/statusbar/HideStatusBarPatch.kt | 3 ++ 3 files changed, 32 insertions(+), 4 deletions(-) diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/HideStatusBarPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/HideStatusBarPatch.java index 7574cd59ea..c1091be486 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/HideStatusBarPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/HideStatusBarPatch.java @@ -133,13 +133,33 @@ public void onActivityDestroyed(@NonNull Activity a) {} @Nullable private static volatile Boolean edgeToEdgeTopBarUsed; + /** + * Changed during patching since this class is also + * used by {@link NavigationBarPatch}. + */ + private static boolean isPatchIncluded() { + return false; // Modified during patching. + } + + /** + * @return If the edge to edge top bar feature flag is turned off for the whole session + * even if it is not read yet: with the status bar layout fix and the status bar hidden. + */ + private static boolean isEdgeToEdgeTopBarTurnedOff() { + return Settings.FIX_STATUS_BAR_LAYOUT.get() && Settings.HIDE_STATUS_BAR.get(); + } + /** * @return If the window is drawn behind the system bars, or null if the app did not read - * the edge to edge top bar feature flag yet. + * the edge to edge top bar feature flag yet and its value is not known. */ @Nullable static Boolean isEdgeToEdgeTopBar() { - return edgeToEdgeTopBarUsed; + Boolean edgeToEdge = edgeToEdgeTopBarUsed; + if (edgeToEdge == null && isPatchIncluded() && isEdgeToEdgeTopBarTurnedOff()) { + return false; + } + return edgeToEdge; } /** @@ -187,6 +207,8 @@ public static boolean useEdgeToEdgeTopBar(boolean original) { * The status bar color that follows the scrolling top bar only exists with the edge to edge * top bar. Without it the status bar view is never set up, and the first scroll of the * top bar crashes the app (https://github.com/MorpheApp/morphe-patches/issues/3805). + * This flag can be read before the edge to edge top bar flag, which is then turned off later + * if the status bar is hidden. */ public static boolean useStatusBarScrollColor(boolean original) { if (!Settings.FIX_STATUS_BAR_LAYOUT.get()) { @@ -194,7 +216,10 @@ public static boolean useStatusBarScrollColor(boolean original) { } Boolean edgeToEdge = edgeToEdgeTopBar; - return original && (edgeToEdge == null || edgeToEdge); + if (edgeToEdge == null) { + edgeToEdge = !isEdgeToEdgeTopBarTurnedOff(); + } + return original && edgeToEdge; } /** diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/NavigationBarPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/NavigationBarPatch.java index e2f5b1287e..782f4885a7 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/NavigationBarPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/NavigationBarPatch.java @@ -227,7 +227,7 @@ public static boolean useTranslucentNavigationBar(boolean original) { translucent = false; } else { Boolean edgeToEdge = HideStatusBarPatch.isEdgeToEdgeTopBar(); - // Null if not read yet, or the Hide status bar patch is not included. + // Null if not known yet, or the Hide status bar patch is not included. if (edgeToEdge != null) { translucent = edgeToEdge; } diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/statusbar/HideStatusBarPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/statusbar/HideStatusBarPatch.kt index 55863012d3..f74a0fe31c 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/statusbar/HideStatusBarPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/statusbar/HideStatusBarPatch.kt @@ -18,6 +18,7 @@ import app.morphe.patches.youtube.misc.settings.settingsPatch import app.morphe.patches.youtube.shared.Constants.COMPATIBILITY_YOUTUBE import app.morphe.patches.youtube.shared.YouTubeActivityOnCreateFingerprint import app.morphe.util.insertLiteralOverride +import app.morphe.util.setExtensionIsPatchIncluded import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction @@ -65,6 +66,8 @@ val hideStatusBarPatch = bytecodePatch( ) } + setExtensionIsPatchIncluded(EXTENSION_CLASS) + YouTubeActivityOnCreateFingerprint.method.addInstruction( 0, "invoke-static/range { p0 .. p0 }, $EXTENSION_CLASS->initialize(Landroid/app/Activity;)V", From 9f0c6d359f718f7629dea7e9b55b304640bbc817 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Sat, 24 Oct 2026 11:52:25 +0000 Subject: [PATCH 033/189] chore: Update dependencies --- gradle/libs.versions.toml | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 55efe0293b..d19e200f90 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,15 +2,14 @@ [versions] morphe-patcher = "2.0.0" -morphe-patches-library = "2.0.1-dev.1" +morphe-patches-library = "2.0.2-dev.1" smali = "4220ae75f7" agp = "9.1.0" -annotation = "1.10.0" +annotation = "1.11.0" androidx-core = "1.18.0" # 1.19.0+ requires upgrading android compile sdk -androidx-javascriptengine = "1.1.0" +androidx-javascriptengine = "1.1.1" collections4 = "4.6.0" -gson = "2.14.0" -lang3 = "3.20.0" +gson = "2.15.0" guava = "33.6.0-jre" hiddenapi = "6.1" protobuf = "0.10.0" @@ -27,7 +26,6 @@ androidx-core = { module = "androidx.core:core", version.ref = "androidx-core" } androidx-javascriptengine = { module = "androidx.javascriptengine:javascriptengine", version.ref = "androidx-javascriptengine" } collections4 = { module = "org.apache.commons:commons-collections4", version.ref = "collections4" } gson = { module = "com.google.code.gson:gson", version.ref = "gson" } -lang3 = { module = "org.apache.commons:commons-lang3", version.ref = "lang3" } guava = { module = "com.google.guava:guava", version.ref = "guava" } hiddenapi = { module = "org.lsposed.hiddenapibypass:hiddenapibypass", version.ref = "hiddenapi" } protobuf-javalite = { module = "com.google.protobuf:protobuf-javalite", version.ref = "protoc" } From 52fa13489d6a97172d29cf5ff639d958506251ca Mon Sep 17 00:00:00 2001 From: 0xrxL <127248639+0xrxL@users.noreply.github.com> Date: Sat, 24 Oct 2026 14:23:22 +0200 Subject: [PATCH 034/189] fix(YouTube): Fingerprints are not found on some targets between 21.04 and 21.35 --- .../morphe/patches/youtube/interaction/ask/Fingerprints.kt | 2 +- .../patches/youtube/layout/miniplayer/Fingerprints.kt | 4 +++- .../patches/youtube/layout/miniplayer/MiniplayerPatch.kt | 2 +- .../patches/youtube/misc/spoof/SpoofVideoStreamsPatch.kt | 5 +++-- .../patches/youtube/video/information/Fingerprints.kt | 5 ++--- .../app/morphe/patches/youtube/video/series/Fingerprints.kt | 6 ++++-- .../app/morphe/patches/youtube/video/series/HistoryPatch.kt | 3 +-- 7 files changed, 15 insertions(+), 12 deletions(-) diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/ask/Fingerprints.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/ask/Fingerprints.kt index 4183cade65..0b10049318 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/ask/Fingerprints.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/ask/Fingerprints.kt @@ -79,10 +79,10 @@ internal object MenuBottomSheetFragmentFactoryFingerprint : Fingerprint( /** * Creates the row of a menu item of the menu bottom sheet. The parameter is the menu item. + * It is protected in some targets, such as 21.21 and 21.30. */ internal object MenuBottomSheetCreateRowFingerprint : Fingerprint( classFingerprint = MenuBottomSheetFragmentFactoryFingerprint, - accessFlags = listOf(AccessFlags.PUBLIC, AccessFlags.FINAL), returnType = "Lj\$/util/Optional;", parameters = listOf("L") ) diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/miniplayer/Fingerprints.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/miniplayer/Fingerprints.kt index 418f42d159..45745c628a 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/miniplayer/Fingerprints.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/miniplayer/Fingerprints.kt @@ -86,12 +86,14 @@ internal object MiniplayerDragAndDropFingerprint : Fingerprint( ) ) +/** + * 21.38+ also read the animated expand feature flag here, 21.30 to 21.35 do not. + */ internal object MiniplayerAnimatedExpandFingerprint : Fingerprint( accessFlags = listOf(AccessFlags.PUBLIC, AccessFlags.FINAL), returnType = "V", parameters = listOf("I"), filters = listOf( - literal(45838209L), opcode(opcode = Opcode.IF_NE), fieldAccess( opcode = Opcode.IGET_OBJECT, diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/miniplayer/MiniplayerPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/miniplayer/MiniplayerPatch.kt index 60cf9f48c8..691313fccc 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/miniplayer/MiniplayerPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/miniplayer/MiniplayerPatch.kt @@ -466,7 +466,7 @@ val miniplayerPatch = bytecodePatch( } else { MiniplayerAnimatedExpandFingerprint.let { it.method.apply { - val insertIndex = it.instructionMatches[1].index + val insertIndex = it.instructionMatches.first().index val labelIndex = it.instructionMatches.last().index val free = findFreeRegister(insertIndex) diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/misc/spoof/SpoofVideoStreamsPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/spoof/SpoofVideoStreamsPatch.kt index 57f38c74c4..744b995e2b 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/misc/spoof/SpoofVideoStreamsPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/spoof/SpoofVideoStreamsPatch.kt @@ -21,7 +21,7 @@ import app.morphe.patches.youtube.misc.playservice.is_20_31_or_greater import app.morphe.patches.youtube.misc.playservice.is_20_35_or_greater import app.morphe.patches.youtube.misc.playservice.is_20_39_or_greater import app.morphe.patches.youtube.misc.playservice.is_21_02_or_greater -import app.morphe.patches.youtube.misc.playservice.is_21_13_or_greater +import app.morphe.patches.youtube.misc.playservice.is_21_11_or_greater import app.morphe.patches.youtube.misc.playservice.is_21_20_or_greater import app.morphe.patches.youtube.misc.playservice.is_21_41_or_greater import app.morphe.patches.youtube.misc.playservice.versionCheckPatch @@ -56,7 +56,8 @@ val spoofVideoStreamsPatch = spoofVideoStreamsPatch( // but only recently has been causing issues. is_20_31_or_greater }, - restoreMissingCuepointMethod = { is_20_35_or_greater && !is_21_13_or_greater }, + // 21.11 and 21.12 do not have the cuepoint list class, and pass the cuepoint list as bytes. + restoreMissingCuepointMethod = { is_20_35_or_greater && !is_21_11_or_greater }, patchProtoRequest = { is_21_02_or_greater }, patchProtoRequestLegacy = { !is_21_20_or_greater }, diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/video/information/Fingerprints.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/video/information/Fingerprints.kt index 651b9616c9..c41bc4ec49 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/video/information/Fingerprints.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/video/information/Fingerprints.kt @@ -70,11 +70,10 @@ internal fun getChannelIdFingerprint(playerResponseType: String) = object : Fing definingClass = playerResponseType, returnType = "Z" ), - // Channel id. + // Channel id. 21.17 places the live stream check far before it. methodCall( definingClass = playerResponseType, - returnType = "Ljava/lang/String;", - location = MatchAfterWithin(30) + returnType = "Ljava/lang/String;" ), string( string = "com.google.android.apps.youtube.mdx.watch.LAST_MEALBAR_PROMOTED_LIVE_FEED_CHANNELS", diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/video/series/Fingerprints.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/video/series/Fingerprints.kt index e61fff06a3..f63ce6a786 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/video/series/Fingerprints.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/video/series/Fingerprints.kt @@ -84,13 +84,15 @@ internal object HistoryNavigationBuilderFingerprint : Fingerprint( returnType = "Ljava/lang/Object;", ) +/** + * Every private method of the fragment that reads the route calls the same method, + * and their parameters change between targets. + */ internal class BrowseRouteFingerprint(endpointType: String) : Fingerprint( classFingerprint = BrowseFragmentFingerprint, accessFlags = listOf(AccessFlags.PRIVATE, AccessFlags.FINAL), - parameters = listOf("L", "L"), filters = listOf( methodCall( - opcode = Opcode.INVOKE_STATIC, parameters = listOf(endpointType), returnType = "Ljava/lang/String;", diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/video/series/HistoryPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/video/series/HistoryPatch.kt index 1872947927..475da607cc 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/video/series/HistoryPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/video/series/HistoryPatch.kt @@ -139,9 +139,8 @@ internal fun BytecodePatchContext.wireHistory() { fieldAccess(definingClass = "this", type = "L"), ) val endpointField = create.getInstruction(endpointIndex).getReference()!! - // Fingerprint can match two different methods, but both are valid. val routeMethod = BrowseRouteFingerprint(endpointField.type) - .matchAll(1..2).first().instructionMatches.first().getMethodCalled() + .instructionMatches.first().getMethodCalled() fragment.methods.add( ImmutableMethod( From 006a9d492e8c79bb74715ba745ddbc4d41599d56 Mon Sep 17 00:00:00 2001 From: 0xrxL <127248639+0xrxL@users.noreply.github.com> Date: Sat, 24 Oct 2026 14:23:22 +0200 Subject: [PATCH 035/189] refactor(Extensions): Inline small methods that are called only once --- .../AndroidAutoBrowseResponseParser.java | 7 +-- .../music/patches/AndroidAutoPatch.java | 22 ++----- .../patches/ChangeMiniplayerColorPatch.java | 7 +-- .../music/patches/ChangeStartPagePatch.java | 6 +- .../music/patches/CrossfadePatch.java | 11 ++-- .../downloads/LocalDownloadsFragment.java | 6 +- .../music/patches/downloads/OfflineTrack.java | 9 +-- .../music/patches/lyrics/LyricsManager.java | 8 +-- .../patches/lyrics/derive/QuickLRCClient.java | 6 +- .../patches/lyrics/model/LyricsLine.java | 6 +- .../patches/lyrics/model/MetadataCleaner.java | 8 +-- .../patches/lyrics/parsers/LRCParser.java | 8 +-- .../lyrics/requests/CaptionsFetcher.java | 7 +-- .../lyrics/requests/DeezerProvider.java | 8 +-- .../lyrics/requests/LocalLyricsFetcher.java | 38 +++---------- .../lyrics/requests/LunaBeatProvider.java | 10 +--- .../lyrics/requests/LyricifyProvider.java | 8 +-- .../lyrics/requests/UnisonProvider.java | 9 +-- .../lyrics/romanizers/KanaRomanizer.java | 14 ++--- .../lyrics/romanizers/PinyinDictStore.java | 6 +- .../patches/lyrics/storage/LyricsCache.java | 11 +--- .../patches/lyrics/ui/LyricsLineView.java | 57 ++++++------------- .../lyrics/ui/LyricsPanelInstaller.java | 14 ++--- .../patches/lyrics/ui/LyricsPanelView.java | 20 ++----- .../patches/lyrics/ui/LyricsSpanBuilder.java | 6 +- .../ui/SponsorBlockMusicSubmitDialog.java | 15 +---- .../patches/scrobbling/lastfm/LastFM.java | 6 +- .../preference/LyricsAiPromptPreference.java | 14 ++--- .../patches/ShowFlairsInHomeFeedPatch.java | 12 ++-- .../reddit/patches/VersionCheckPatch.java | 7 +-- .../shared/patches/CustomBrandingPatch.java | 3 +- .../shared/patches/GmsCoreSupportPatch.java | 16 +----- .../shared/patches/NetworkProxyPatch.java | 12 ++-- .../shared/patches/SplashAnimationPatch.java | 5 -- .../shared/requests/CronetTransport.java | 17 ++---- .../proxy/CronetHttpURLConnection.java | 12 +--- .../ReturnYouTubeDislikeButtons.java | 16 ++---- .../requests/ReturnYouTubeDislikeAPI.java | 26 ++++----- .../ExternalDownloaderPreference.java | 7 +-- ...xternalPoTokenProviderAboutPreference.java | 32 ++++------- .../ExternalPoTokenProviderPreference.java | 20 ++----- .../FeatureFlagsManagerPreference.java | 8 +-- .../settings/preference/OAuth2Preference.java | 22 ++----- .../search/BaseSearchViewController.java | 15 ++--- .../settings/search/SearchHistoryManager.java | 19 +------ .../sponsorblock/objects/SegmentCategory.java | 19 +------ .../spoof/requests/StreamingDataRequest.java | 7 +-- .../shared/theme/ThemeColorOverlay.java | 7 +-- .../shared/theme/ThemeColorPatch.java | 35 +++--------- .../AbstractPreferenceFragment.java | 10 +--- .../preference/IconListPreference.java | 6 +- .../preference/about/AboutDialogStyle.java | 21 +------ .../preference/about/LicenseTextDialog.java | 4 +- .../preference/about/MorpheCreditsDialog.java | 14 ++--- .../shared/translation/TextTranslator.java | 14 ++--- .../shared/ui/SheetBottomDialog.java | 27 +-------- .../patches/AutoTranslateCaptionsPatch.java | 7 +-- .../youtube/patches/ChangeStartPagePatch.java | 13 +---- .../youtube/patches/ChannelSearchPatch.java | 35 ++++-------- .../youtube/patches/FeedCardLayoutPatch.java | 19 +++---- .../patches/FeedVideoActionsPatch.java | 27 +++------ .../patches/FullscreenVideoScalePatch.java | 15 ++--- .../OverrideYouTubeMusicButtonsPatch.java | 13 ++--- .../youtube/patches/PlayerVolumePatch.java | 8 +-- .../patches/ReturnYouTubeDislikePatch.java | 13 ++--- .../youtube/patches/ShortsAutoplayPatch.java | 11 +--- .../youtube/patches/VideoInformation.java | 25 ++------ .../youtube/patches/dearrow/DeArrowPatch.java | 41 ++++--------- .../RestoreOriginalTitlesPatch.java | 12 +--- .../RememberLiveStreamPositionPatch.java | 6 +- .../patches/utils/FeedVideoCardsPatch.java | 13 +---- .../youtube/patches/utils/FlyoutUtils.java | 18 ++---- .../patches/utils/LocalQueueSheet.java | 13 +---- .../utils/requests/AiSListSubmitRequest.java | 20 +++---- .../utils/requests/ChannelSearchRequest.java | 13 +---- .../youtube/series/CatalogParser.java | 11 ++-- .../youtube/series/TrackerRepository.java | 6 +- ...poofVideoStreamsSideEffectsPreference.java | 12 +--- .../SwipeVolumeStepsPreference.java | 19 +++---- .../preference/SwipeZonePreference.java | 12 +--- .../ui/SponsorBlockViewController.java | 10 ++-- .../FullscreenVideoScaleButton.java | 6 +- .../videoplayer/VideoQualityDialogButton.java | 8 +-- 83 files changed, 299 insertions(+), 867 deletions(-) diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/AndroidAutoBrowseResponseParser.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/AndroidAutoBrowseResponseParser.java index 07c86321b8..75742be5f2 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/AndroidAutoBrowseResponseParser.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/AndroidAutoBrowseResponseParser.java @@ -271,7 +271,8 @@ private static void readSpeedDial(List items, ProtoMessage speedDial } private static void readTopResult(List items, ProtoMessage topResult) { - addTopBrowseResult(items, topResult.messageField(TOP_RESULT_PRIMARY)); + addRenderer(items, topResult.messageField(TOP_RESULT_PRIMARY), RESULT_TITLE, RESULT_SUBTITLE, RESULT_IMAGE, + TOP_RESULT_TAP, TOP_RESULT_MENU, true); for (ProtoMessage row : topResult.repeatedMessageField(TOP_RESULT_RELATED)) addBrowseRow(items, row); } @@ -307,10 +308,6 @@ private static void addBrowseRow(List items, ProtoMessage renderer) addRenderer(items, renderer, RESULT_TITLE, RESULT_SUBTITLE, RESULT_IMAGE, RESULT_TAP, RESULT_MENU, false); } - private static void addTopBrowseResult(List items, ProtoMessage renderer) { - addRenderer(items, renderer, RESULT_TITLE, RESULT_SUBTITLE, RESULT_IMAGE, TOP_RESULT_TAP, TOP_RESULT_MENU, true); - } - /** Grid cards, result rows, and top results differ only in the field numbers of their contents. */ private static void addRenderer(List items, ProtoMessage renderer, int titleField, int subtitleField, int imageField, int tapField, int menuField, boolean topResultCard) { diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/AndroidAutoPatch.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/AndroidAutoPatch.java index 07184394e9..61b0fd6ead 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/AndroidAutoPatch.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/AndroidAutoPatch.java @@ -387,11 +387,13 @@ public static boolean handleBrowseRequest(BrowseRequest request) { String requestedMediaId = request.patch_getRequestedMediaId(); if (requestedMediaId == null) return false; if (requestedMediaId.equals(SPEED_DIAL_MEDIA_ID)) { - HomeLoad.forSpeedDial(request).start(); + new HomeLoad(request, HomeLoad.Mode.SPEED_DIAL, "", Collections.emptyList()).start(); return true; } if (requestedMediaId.startsWith(HOME_SHELF_PREFIX)) { - HomeLoad.forShelf(request, requestedMediaId).start(); + // An expanded shelf's media ID contains its title. Fetch phone Home again and select that shelf. + String shelfTitle = Uri.decode(requestedMediaId.substring(HOME_SHELF_PREFIX.length())); + new HomeLoad(request, HomeLoad.Mode.SHELF, shelfTitle, Collections.emptyList()).start(); return true; } if (requestedMediaId.equals(libraryMediaId)) { @@ -793,7 +795,7 @@ public static synchronized boolean loadHome(BrowseRequest request, if (phoneBrowseClient == null || homeMediaId == null || nativeItems == null || nativeItems instanceof PreparedItems || !homeMediaId.equals(request.patch_getRequestedMediaId())) return false; - HomeLoad.forHome(request, nativeItems).start(); + new HomeLoad(request, HomeLoad.Mode.HOME, "", new ArrayList<>(nativeItems)).start(); return true; } catch (RuntimeException ex) { Logger.printException(() -> "loadHome failure", ex); @@ -883,20 +885,6 @@ private HomeLoad(BrowseRequest request, Mode mode, String shelfTitle, this.podcastFolders = podcastFolders; } - static HomeLoad forHome(BrowseRequest request, List podcastFolders) { - return new HomeLoad(request, Mode.HOME, "", new ArrayList<>(podcastFolders)); - } - - static HomeLoad forShelf(BrowseRequest request, String mediaId) { - // An expanded shelf's media ID contains its title. Fetch phone Home again and select that shelf. - String shelfTitle = Uri.decode(mediaId.substring(HOME_SHELF_PREFIX.length())); - return new HomeLoad(request, Mode.SHELF, shelfTitle, Collections.emptyList()); - } - - static HomeLoad forSpeedDial(BrowseRequest request) { - return new HomeLoad(request, Mode.SPEED_DIAL, "", Collections.emptyList()); - } - @Override void load() { requestPage(""); } private void requestPage(String continuation) { diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/ChangeMiniplayerColorPatch.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/ChangeMiniplayerColorPatch.java index be3bd1e98c..28a3b416f8 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/ChangeMiniplayerColorPatch.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/ChangeMiniplayerColorPatch.java @@ -63,7 +63,7 @@ public static void setLastMiniplayerColor(int color) { } lastMiniplayerColor = color; - applyToNavigationBar(color); + if (Settings.CHANGE_NAVIGATION_BAR_COLOR.get()) postNavigationBarColor(color); } /** @@ -96,11 +96,6 @@ public static void onMiniplayerDismissed() { if (defaultColor != null) postNavigationBarColor(defaultColor); } - private static void applyToNavigationBar(int color) { - if (!Settings.CHANGE_NAVIGATION_BAR_COLOR.get()) return; - postNavigationBarColor(color); - } - private static void postNavigationBarColor(int color) { View view = navigationBarRef.get(); if (view == null) return; diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/ChangeStartPagePatch.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/ChangeStartPagePatch.java index 6c180d9c3a..0455dbee98 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/ChangeStartPagePatch.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/ChangeStartPagePatch.java @@ -47,10 +47,6 @@ public enum StartPage { this.id = id; this.isBrowseId = isBrowseId; } - - private boolean isBrowseId() { - return TRUE.equals(isBrowseId); - } } private static final String SETTINGS_CLASS = "com.google.android.apps.youtube.music.settings.SettingsCompatActivity"; @@ -102,7 +98,7 @@ public static String overrideBrowseId(@Nullable String original) { } StartPage startPage = Settings.CHANGE_START_PAGE.get(); - return processBrowseId(original, startPage.isBrowseId(), startPage.id, startPage.name()); + return processBrowseId(original, TRUE.equals(startPage.isBrowseId), startPage.id, startPage.name()); } public static void overrideIntentActionOnCreate(@NonNull Activity activity, @Nullable Bundle savedInstanceState) { diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/CrossfadePatch.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/CrossfadePatch.java index a5e42bc3cf..69fec4e8f7 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/CrossfadePatch.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/CrossfadePatch.java @@ -524,7 +524,10 @@ public static boolean onBeforeStopVideo(Object atadInstance, int reason) { // change. The old song falls silent within about a second, whatever its buffer reports (#2748). if (isSabrStream()) { logDebug(() -> "stopVideo(5): skip, SABR stream"); - showSabrToastOnce(); + if (!sabrToastShown) { + sabrToastShown = true; + Utils.showToastShort(str("morphe_music_crossfade_sabr_toast")); + } return false; } @@ -755,12 +758,6 @@ private static boolean isSabrStream() { private static volatile boolean sabrToastShown = false; - private static void showSabrToastOnce() { - if (sabrToastShown) return; - sabrToastShown = true; - Utils.showToastShort(str("morphe_music_crossfade_sabr_toast")); - } - private static ExoPlayerAccess createNewPlayer(PlayerCoordinatorAccess coordinator) { try { SessionAccess session = (SessionAccess) coordinator.patch_getSession(); diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/downloads/LocalDownloadsFragment.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/downloads/LocalDownloadsFragment.java index b25bd30b7d..1e872cf457 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/downloads/LocalDownloadsFragment.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/downloads/LocalDownloadsFragment.java @@ -733,7 +733,7 @@ private void setButtonEnabled(ImageButton button, boolean enabled) { private ImageView artworkView(@Nullable Bitmap bitmap) { ImageView view = new ImageView(getActivity()); - view.setBackgroundColor(artworkPlaceholder()); + view.setBackgroundColor(Utils.adjustColorBrightness(foreground(), 0.2f)); roundCorners(view); if (bitmap != null) { @@ -801,10 +801,6 @@ private static int background() { return ThemeUtils.getAppBackgroundColor(); } - private static int artworkPlaceholder() { - return Utils.adjustColorBrightness(foreground(), 0.2f); - } - private int dp(int value) { return (int) (value * getResources().getDisplayMetrics().density + .5f); } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/downloads/OfflineTrack.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/downloads/OfflineTrack.java index 7b8d9a2b74..1b6bb2f3a9 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/downloads/OfflineTrack.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/downloads/OfflineTrack.java @@ -22,7 +22,9 @@ public record OfflineTrack(String videoId, String title, String artist, String a int durationSeconds, File audioFile, File artworkFile) { public static OfflineTrack load(File audioFile) { - String videoId = stripExtension(audioFile.getName()); + String fileName = audioFile.getName(); + int dot = fileName.lastIndexOf('.'); + String videoId = dot > 0 ? fileName.substring(0, dot) : fileName; File metadata = new File(audioFile.getParentFile(), videoId + ".json"); File artwork = new File(audioFile.getParentFile(), videoId + ".jpg"); @@ -75,11 +77,6 @@ public String displayArtist() { return artist.isBlank() ? "YouTube Music" : artist; } - private static String stripExtension(String name) { - int dot = name.lastIndexOf('.'); - return dot > 0 ? name.substring(0, dot) : name; - } - private static String emptyFallback(String value, String fallback) { return value == null || value.isBlank() ? fallback : value; } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/LyricsManager.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/LyricsManager.java index c26fac0076..4fd866b9b0 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/LyricsManager.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/LyricsManager.java @@ -492,11 +492,6 @@ public void persistTemporaryOffsetMs() { runOnLookupThread(() -> TrackOffsetStore.put(track, ms)); } - /** Only reached on a real track change, so a loop cannot discard the offset it comes round for again. */ - private void loadTemporaryOffsetMs(TrackInfo track) { - temporaryOffsetMs = TrackOffsetStore.get(track); - } - /** Injection point; main thread. */ public void onSetMetadata(@Nullable MediaMetadata metadata) { Utils.verifyOnMainThread(); @@ -689,7 +684,8 @@ private void commitTrackChange(TrackInfo track, String rawTitle, String rawArtis currentMediaUri = mediaUri; currentTrack = track; currentVideoId = videoId; - loadTemporaryOffsetMs(track); + // Only reached on a real track change, so a loop cannot discard the offset it comes round for again. + temporaryOffsetMs = TrackOffsetStore.get(track); // A rebinding keeps the previous track's lyric, which is what makes it worth // telling apart from the video the track was committed for. if (!sameTrack) { diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/derive/QuickLRCClient.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/derive/QuickLRCClient.java index 610d85e4a8..8aa339e99b 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/derive/QuickLRCClient.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/derive/QuickLRCClient.java @@ -300,7 +300,7 @@ private static String resolve(String base, String location) { private static String dropCjkSeparators(String lrc) { final StringBuilder out = new StringBuilder(lrc.length()); for (int i = 0; i < lrc.length(); ) { - if (isWhitespaceAt(lrc, i) && isCjkGap(lrc, i)) { + if (Character.isWhitespace(lrc.charAt(i)) && isCjkGap(lrc, i)) { i = skipWhitespace(lrc, i); continue; } @@ -343,10 +343,6 @@ private static char charAfterTag(String lrc, int tagStart) { return token < lrc.length() ? lrc.charAt(token) : '\0'; } - private static boolean isWhitespaceAt(String text, int index) { - return Character.isWhitespace(text.charAt(index)); - } - private static int skipWhitespace(String text, int index) { int i = index; while (i < text.length() && Character.isWhitespace(text.charAt(i))) { diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/LyricsLine.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/LyricsLine.java index 044ed83375..a012d83e86 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/LyricsLine.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/LyricsLine.java @@ -128,7 +128,7 @@ public boolean hasWordTimings() { } if (words.size() == 1) { Word single = words.get(0); - if (isWholeLineWord(single.text(), text)) { + if (compactText(single.text()).equalsIgnoreCase(compactText(text))) { return false; } if (single.endMs() <= single.startMs()) { @@ -138,10 +138,6 @@ public boolean hasWordTimings() { return true; } - private static boolean isWholeLineWord(String wordText, String lineText) { - return compactText(wordText).equalsIgnoreCase(compactText(lineText)); - } - private static String compactText(String value) { final StringBuilder builder = new StringBuilder(value.length()); for (int i = 0; i < value.length(); i++) { diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/MetadataCleaner.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/MetadataCleaner.java index 8c63add006..6084bc53cd 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/MetadataCleaner.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/MetadataCleaner.java @@ -139,8 +139,8 @@ private static String cleanField(@Nullable String value) { if (value == null) { return ""; } - return collapseWhitespace( - applyRegex(value, resolveSetting(LyricsFilterDefaults.LYRICS_CUSTOM_REGEX.get()))); + return WHITESPACE.matcher(applyRegex(value, resolveSetting(LyricsFilterDefaults.LYRICS_CUSTOM_REGEX.get()))) + .replaceAll(" ").trim(); } static String applyRegex(String input, String regex) { @@ -308,10 +308,6 @@ public static String[] splitArtists(String artist) { private static final Pattern WHITESPACE = Pattern.compile("\\s+"); - private static String collapseWhitespace(String value) { - return WHITESPACE.matcher(value).replaceAll(" ").trim(); - } - @NonNull private static String download(String urlStr) throws Exception { HttpURLConnection conn = (HttpURLConnection) new URL(urlStr).openConnection(); diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/parsers/LRCParser.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/parsers/LRCParser.java index a96b5f19b2..57d2fe0eaf 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/parsers/LRCParser.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/parsers/LRCParser.java @@ -672,7 +672,8 @@ public static PlainLyrics parsePlainWithSlots(@Nullable String plain) { for (int i = 0; i < raw.length; i++) { String trimmed = raw[i].trim(); if (isSectionHeader(trimmed)) { - pendingPart = sectionLabel(trimmed); + // The heading text without its brackets, with runs of whitespace collapsed. + pendingPart = collapseSpaces(trimmed.substring(1, trimmed.length() - 1)); continue; } if (trimmed.isEmpty()) { @@ -704,11 +705,6 @@ private static boolean isSectionHeader(String trimmed) { && trimmed.charAt(trimmed.length() - 1) == ']'; } - /** The heading text without its brackets, with runs of whitespace collapsed. */ - private static String sectionLabel(String trimmed) { - return collapseSpaces(trimmed.substring(1, trimmed.length() - 1)); - } - private static String collapseSpaces(String text) { return HEADING_SPACES.matcher(text).replaceAll(" ").trim(); } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/CaptionsFetcher.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/CaptionsFetcher.java index 714e3b5a4d..25ad670860 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/CaptionsFetcher.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/CaptionsFetcher.java @@ -933,11 +933,6 @@ private static String computeSapisidHash(String cookies) { } } - @Nullable - private static String postInnertubePlayer(String bodyJson, @Nullable String cookies) throws Exception { - return postInnertubePlayer(bodyJson, cookies, CONNECT_TIMEOUT_MS, READ_TIMEOUT_MS); - } - @Nullable private static String postInnertubePlayer(String bodyJson, @Nullable String cookies, int connectTimeoutMs, int readTimeoutMs) throws Exception { @@ -1026,7 +1021,7 @@ public static boolean validateYouTubeCookies(String cookies) { private static String fetchInnertubePlayer(String videoId) { try { String bodyStr = buildPlayerRequestBody(videoId); - return postInnertubePlayer(bodyStr, getCookies()); + return postInnertubePlayer(bodyStr, getCookies(), CONNECT_TIMEOUT_MS, READ_TIMEOUT_MS); } catch (Exception ex) { Logger.printDebug(() -> "Could not fetch InnerTube player", ex); return null; diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/DeezerProvider.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/DeezerProvider.java index 9295cdbcc5..80ce60293e 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/DeezerProvider.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/DeezerProvider.java @@ -93,7 +93,8 @@ protected List fetchScoredSongs(TrackInfo track) throws Exception { Lyrics lyrics = fetchLyricsByTrackId(trackId, arl); if (lyrics != null) { String title = item.optString("title", ""); - String artist = artistName(item); + JSONObject artistObj = item.optJSONObject("artist"); + String artist = artistObj != null ? artistObj.optString("name", "") : ""; long durationSec = item.optInt("duration", 0); int score = LyricsRequests.scoreLyricsCandidate( title, artist, durationSec, lyrics, track); @@ -108,11 +109,6 @@ protected List fetchScoredSongs(TrackInfo track) throws Exception { return scored; } - private static String artistName(JSONObject item) { - JSONObject artistObj = item.optJSONObject("artist"); - return artistObj != null ? artistObj.optString("name", "") : ""; - } - private static int scoreCandidate(JSONObject item, TrackInfo track) { String title = item.optString("title", ""); JSONObject artistObj = item.optJSONObject("artist"); diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/LocalLyricsFetcher.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/LocalLyricsFetcher.java index cd4254bf25..6db359cf39 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/LocalLyricsFetcher.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/LocalLyricsFetcher.java @@ -306,7 +306,7 @@ private static String extractLyrics(byte[] d) { } } if (d.length >= 8 && d[4] == 'f' && d[5] == 't' && d[6] == 'y' && d[7] == 'p') { - return parseM4a(d); + return walkM4a(d, 0, d.length, 0); } return null; } @@ -722,10 +722,6 @@ private static String parseVorbisCommentFromPacket(String payload, int off) { private static final int MAX_M4A_DEPTH = 32; - private static String parseM4a(byte[] d) { - return walkM4a(d, 0, d.length, 0); - } - @Nullable private static String walkM4a(byte[] d, int start, int end, int depth) { if (depth > MAX_M4A_DEPTH) { @@ -930,28 +926,32 @@ private static String decodeAutoDetect(byte[] b, int declaredEnc) { if (isValidUtf8(b)) { return new String(b, StandardCharsets.UTF_8); } - if (isLikelyGbk(b)) { + if (isLikelyDoubleByte(b, c -> c >= 0x81 && c <= 0xFE, + c -> c >= 0x40 && c <= 0xFE && c != 0x7F)) { try { return new String(b, "GBK"); } catch (Exception ex) { Logger.printDebug(() -> "Could not decode GBK text", ex); } } - if (isLikelyBig5(b)) { + if (isLikelyDoubleByte(b, c -> c >= 0x81 && c <= 0xFE, + c -> (c >= 0x40 && c <= 0x7E) || (c >= 0xA1 && c <= 0xFE))) { try { return new String(b, "Big5"); } catch (Exception ex) { Logger.printDebug(() -> "Could not decode Big5 text", ex); } } - if (isLikelyShiftJis(b)) { + if (isLikelyDoubleByte(b, c -> (c >= 0x81 && c <= 0x9F) || (c >= 0xE0 && c <= 0xEF), + c -> (c >= 0x40 && c <= 0x7E) || (c >= 0x80 && c <= 0xFC))) { try { return new String(b, "Shift_JIS"); } catch (Exception ex) { Logger.printDebug(() -> "Could not decode Shift_JIS text", ex); } } - if (isLikelyEucKr(b)) { + if (isLikelyDoubleByte(b, c -> c >= 0x81 && c <= 0xFE, + c -> c >= 0x41 && c <= 0xFE)) { try { return new String(b, "EUC-KR"); } catch (Exception ex) { @@ -1001,26 +1001,6 @@ private static boolean isValidUtf8(byte[] b) { return true; } - private static boolean isLikelyGbk(byte[] b) { - return isLikelyDoubleByte(b, c -> c >= 0x81 && c <= 0xFE, - c -> c >= 0x40 && c <= 0xFE && c != 0x7F); - } - - private static boolean isLikelyBig5(byte[] b) { - return isLikelyDoubleByte(b, c -> c >= 0x81 && c <= 0xFE, - c -> (c >= 0x40 && c <= 0x7E) || (c >= 0xA1 && c <= 0xFE)); - } - - private static boolean isLikelyShiftJis(byte[] b) { - return isLikelyDoubleByte(b, c -> (c >= 0x81 && c <= 0x9F) || (c >= 0xE0 && c <= 0xEF), - c -> (c >= 0x40 && c <= 0x7E) || (c >= 0x80 && c <= 0xFC)); - } - - private static boolean isLikelyEucKr(byte[] b) { - return isLikelyDoubleByte(b, c -> c >= 0x81 && c <= 0xFE, - c -> c >= 0x41 && c <= 0xFE); - } - /** * Whether every non-ASCII byte belongs to a two-byte character of the encoding described by * its lead and trail byte ranges, and at least one does. A lead byte with nothing usable diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/LunaBeatProvider.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/LunaBeatProvider.java index 431fb3065b..717b1885c0 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/LunaBeatProvider.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/LunaBeatProvider.java @@ -175,8 +175,8 @@ protected List fetchScoredSongs(TrackInfo track) { } Lyrics lyrics = fetchLunabeatLyrics(song); if (lyrics != null && !lyrics.isEmpty()) { - int score = scoreLunabeatCandidate( - song.title(), artist, lyrics, track); + int score = LyricsRequests.scoreTrackCandidate(song.title(), artist, 0, track) + + LyricsRequests.syncRank(lyrics); scored.add(new ScoredSong(score, lyrics, song.title(), artist, 0L)); } } @@ -258,10 +258,4 @@ private Lyrics fetchLunabeatLyrics(Song song) { if (conn != null) conn.disconnect(); } } - - private static int scoreLunabeatCandidate(String title, String artist, - Lyrics lyrics, TrackInfo track) { - int trackScore = LyricsRequests.scoreTrackCandidate(title, artist, 0, track); - return trackScore + LyricsRequests.syncRank(lyrics); - } } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/LyricifyProvider.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/LyricifyProvider.java index 6590d2fff4..4756b2251e 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/LyricifyProvider.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/LyricifyProvider.java @@ -73,7 +73,8 @@ public FetchResult fetch(TrackInfo track) throws Exception { final String isrc = resolved.isrc(); final String username = generateUsername(); - final String isrcParam = base64NoWrap(isrc); + final String isrcParam = Base64.encodeToString( + isrc.getBytes(StandardCharsets.UTF_8), Base64.NO_WRAP); final String isrcEncoded = LyricsRequests.encode(isrcParam); final String url = API_BASE @@ -250,9 +251,4 @@ private static String generateUsername() { } return sb.toString(); } - - private static String base64NoWrap(String input) { - return Base64.encodeToString( - input.getBytes(StandardCharsets.UTF_8), Base64.NO_WRAP); - } } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/UnisonProvider.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/UnisonProvider.java index 48317b2c2f..a55eca508c 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/UnisonProvider.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/UnisonProvider.java @@ -86,7 +86,8 @@ private Lyrics fetchByVideoId(String videoId, String title, String artist, url.append("&album=").append(LyricsRequests.encode(album)); } - connection = openUnisonConnection(url.toString()); + connection = LyricsRequests.openConnection(url.toString()); + connection.setRequestProperty("X-Key-ID", getKeyId()); final int code = connection.getResponseCode(); if (code == 404) { connection.disconnect(); @@ -121,12 +122,6 @@ private Lyrics fetchByVideoId(String videoId, String title, String artist, } } - private static HttpURLConnection openUnisonConnection(String url) throws IOException { - final HttpURLConnection connection = LyricsRequests.openConnection(url); - connection.setRequestProperty("X-Key-ID", getKeyId()); - return connection; - } - private static String getKeyId() { String cached = cachedKeyId; if (cached != null) { diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/KanaRomanizer.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/KanaRomanizer.java index 30c9e6f8db..63929ba518 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/KanaRomanizer.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/KanaRomanizer.java @@ -70,13 +70,15 @@ public static String romanize(String kana) { while (i < kana.length()) { char c = katakana(kana.charAt(i)); - if (isSokuon(c)) { + // Small tsu (sokuon). + if (c == 0x3063 || c == 0x30C3) { geminate = true; i++; continue; } - if (isProlonged(c)) { + // Prolonged sound mark. + if (c == 0x30FC || c == 0x2014) { repeatVowel(out); i++; continue; @@ -189,14 +191,6 @@ private static String smallYVowel(char c) { } } - private static boolean isSokuon(char c) { - return c == 0x3063 || c == 0x30C3; - } - - private static boolean isProlonged(char c) { - return c == 0x30FC || c == 0x2014; - } - private static boolean isN(char c) { return c == 0x3093 || c == 0x30F3; } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/PinyinDictStore.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/PinyinDictStore.java index 594519d151..ee89b2fc2d 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/PinyinDictStore.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/PinyinDictStore.java @@ -158,7 +158,7 @@ private static void build(RomanizationDict.Listener listener) throws IOException } characters = CharReadingTable.load(table); - RomanizationDict.writeNotice(directory, NOTICE_NAME, noticeText()); + RomanizationDict.writeNotice(directory, NOTICE_NAME, noticeTextFor("kMandarin")); CantoneseDictStore.writeNotice(directory); WORDS.build(directory, WORDS_URL, WORDS_ARCHIVE, PinyinDictStore::addWords, listener); @@ -340,10 +340,6 @@ private static String first(String value) { return null; } - private static String noticeText() { - return noticeTextFor("kMandarin"); - } - static String noticeTextFor(String field) { return "Unicode Han Database readings\n" + "https://www.unicode.org/Public/" + VERSION + "/ucd/Unihan.zip\n\n" diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/storage/LyricsCache.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/storage/LyricsCache.java index 19214225f4..c8d28258b9 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/storage/LyricsCache.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/storage/LyricsCache.java @@ -130,7 +130,9 @@ private static boolean sameVideo(@Nullable String storedVideoId, } private static void writeEmbedded(String key, Lyrics lyrics) { - writeEmbeddedRomanization(key, lyrics.romanization()); + if (LyricsMerge.hasText(lyrics.romanization())) { + writeLyricsLineList(embeddedRomanizationFile(key), lyrics.romanization()); + } writeEmbeddedTranslations(key, lyrics.translations()); } @@ -724,13 +726,6 @@ private static List readEmbeddedRomanization(String key) { } } - private static void writeEmbeddedRomanization(String key, @Nullable List romanization) { - if (!LyricsMerge.hasText(romanization)) { - return; - } - writeLyricsLineList(embeddedRomanizationFile(key), romanization); - } - /** * Cache key that also captures the chosen lyrics source, so that switching * the source (for example to QQ or NetEase) forces a fresh fetch instead of diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsLineView.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsLineView.java index 8a3146a864..e61a11a33c 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsLineView.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsLineView.java @@ -375,40 +375,6 @@ private void ensureWordCache(Layout layout, CharSequence text, Paint paint, } } - /** - * A main run holding none of the line's words has no word to light it, so it would sit in the - * unsung colour for the whole line and then jump white at the last word. Drawn while the line - * is active instead, and kept once the words are past so it does not fall back. - * - *

Whether a run holds a word is a fact about the layout, not about how far the line has - * been sung: a seek lands before the first word opens, and reading that as an empty run - * fills it white for a moment. - */ - private static boolean shouldFillMainRun(boolean hasWords, boolean wordOnMainRun, - boolean activeLine, boolean allWordsDone) { - return hasWords && !wordOnMainRun && (activeLine || allWordsDone); - } - - /** - * Only one of the two ranges is set on a dual line, so this run is the reading's exactly when - * the reading's range is the one present. - */ - private static boolean runHoldsWords(boolean smallRunHoldsWords, boolean romaPresent, - boolean isRoma) { - return smallRunHoldsWords ? isRoma == romaPresent : isRoma; - } - - /** - * A small run takes the reading's colour so it reads as a line of its own, and a wrapped one would - * otherwise come out in two colours mid-word. What makes a run small is the main run beside it, not - * whether it holds the words: a dual line hands those to the reading wherever the swap puts it, so a - * main run without them still reads as the lyric, as does a line with nothing beside it. - */ - private static int sungColorForLine(boolean smallRun, boolean anyMainLine, - int readingColor, int lyricColor) { - return smallRun && anyMainLine ? readingColor : lyricColor; - } - /** * The end offset reports along the line's primary direction, so a word ending its line comes back * describing the next one and has to be sent to the far edge of the line it is on. Which side is @@ -610,8 +576,9 @@ private boolean drawBaseText(Canvas canvas) { if (runSecondary) { final boolean isRoma = romaFirst >= 0 && runStart >= romaFirst && runLast <= romaLast; - final boolean runHoldsWords = runHoldsWords( - smallRunHoldsWords, romaFirst >= 0, isRoma); + // Only one of the two ranges is set on a dual line, so this run is the reading's + // exactly when the reading's range is the one present. + final boolean runHoldsWords = smallRunHoldsWords ? isRoma == (romaFirst >= 0) : isRoma; final float reveal = isRoma ? romaReveal : transReveal; drawTextRun(canvas, layout, runStart, runLast, Math.round((runHoldsWords ? romaAlpha : secondaryAlpha) * reveal), @@ -819,8 +786,14 @@ protected void onDraw(Canvas canvas) { final int readingColor = (activeLine && secondaryActiveColor != 0 ? secondaryActiveColor : LyricsColors.secondaryTextColor()) | 0xFF000000; final int lyricColor = LyricsColors.lineTextColor() | 0xFF000000; - final boolean fillMainRun = shouldFillMainRun(hasWords, wordOnMainRun, - activeLine, allWordsDone && firstSungLine >= 0); + // A main run holding none of the line's words has no word to light it, so it would sit in + // the unsung colour for the whole line and then jump white at the last word. Drawn while the + // line is active instead, and kept once the words are past so it does not fall back. + // Whether a run holds a word is a fact about the layout, not about how far the line has + // been sung: a seek lands before the first word opens, and reading that as an empty run + // fills it white for a moment. + final boolean fillMainRun = hasWords && !wordOnMainRun + && (activeLine || (allWordsDone && firstSungLine >= 0)); if (fillMainRun) { for (int ln = 0; ln < lineCount; ln++) { if (!Float.isNaN(lineMaxSungX[ln]) @@ -846,8 +819,12 @@ protected void onDraw(Canvas canvas) { if (Float.isNaN(edge)) { continue; } - final int color = sungColorForLine(secondaryLine[ln], - anyMainLine, readingColor, lyricColor); + // A small run takes the reading's colour so it reads as a line of its own, and a + // wrapped one would otherwise come out in two colours mid-word. What makes a run small + // is the main run beside it, not whether it holds the words: a dual line hands those to + // the reading wherever the swap puts it, so a main run without them still reads as the + // lyric, as does a line with nothing beside it. + final int color = secondaryLine[ln] && anyMainLine ? readingColor : lyricColor; if (color != appliedColor) { tp.setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN)); appliedColor = color; diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsPanelInstaller.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsPanelInstaller.java index 0ec30687e8..b4f6f4e0ef 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsPanelInstaller.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsPanelInstaller.java @@ -600,7 +600,11 @@ private static boolean enableLyricsButtonPass(View view, String title, String ti || descLower.contains(titleLower) || descLower.contains("lyric"); if (matches) { - enableAndClick(view, desc); + view.setEnabled(true); + view.setClickable(true); + view.setAlpha(1.0f); + Logger.printDebug(() -> "Enabling lyrics button: " + view.getClass().getSimpleName() + + " content description: '" + desc + "'"); matched = true; } } @@ -613,12 +617,4 @@ private static boolean enableLyricsButtonPass(View view, String title, String ti } return matched; } - - private static void enableAndClick(View view, String desc) { - view.setEnabled(true); - view.setClickable(true); - view.setAlpha(1.0f); - Logger.printDebug(() -> "Enabling lyrics button: " + view.getClass().getSimpleName() - + " content description: '" + desc + "'"); - } } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsPanelView.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsPanelView.java index d6691ebf7e..68e3e9d542 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsPanelView.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsPanelView.java @@ -2109,14 +2109,10 @@ private boolean normalRomanizeActive() { || perWordRomaji; } - private void resetOnlyModeKaraokeState() { - karaokeState.resetContent(); - karaokeState.syncEnabled(Settings.LYRICS_WORD_SYNC.get()); - } - private void applyOnlyModeState() { captureOnlyModeScrollAnchor(); - resetOnlyModeKaraokeState(); + karaokeState.resetContent(); + karaokeState.syncEnabled(Settings.LYRICS_WORD_SYNC.get()); clearActiveLineState(); } @@ -2526,7 +2522,10 @@ private void onRefreshClicked() { } private void onRefreshLongPressed() { - showSearchDialog(); + try { + openSearchDialog(); + } catch (Throwable ignored) { + } } /** @@ -2701,13 +2700,6 @@ private void updateTranscribeAffordances() { } } - private void showSearchDialog() { - try { - openSearchDialog(); - } catch (Throwable ignored) { - } - } - /** * Lets the user type the song and artist they want lyrics for. The terms open prefilled * with what was searched before, or with the track's untouched metadata - the cleaned diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsSpanBuilder.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsSpanBuilder.java index 7ada8d06ac..c54664c936 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsSpanBuilder.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsSpanBuilder.java @@ -249,7 +249,7 @@ private static List closeReadingGaps(List spans, LyricsL static List computeWordTimings(List lines, int index, boolean splitWordlessLine) { final LyricsLine line = lines.get(index); - if (isSingleWord(line.text())) { + if (countWords(line.text()) == 1) { long rangeStart = line.startTimeMs(); if (rangeStart < 0 && line.hasWords()) { rangeStart = line.words().get(0).startMs(); @@ -411,10 +411,6 @@ private static int[] clusterBoundaries(String text) { return result; } - private static boolean isSingleWord(String text) { - return countWords(text) == 1; - } - private static int countWords(String text) { int words = 0; boolean inWord = false; diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/SponsorBlockMusicSubmitDialog.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/SponsorBlockMusicSubmitDialog.java index 105c1af460..30b28b5587 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/SponsorBlockMusicSubmitDialog.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/SponsorBlockMusicSubmitDialog.java @@ -102,12 +102,12 @@ static void show(Context context, String videoId, long startMs, long endMs, final OffsetSliderView offsetSlider = new OffsetSliderView(context); offsetSlider.setValue(manager.getTemporaryOffsetMs()); - offsetSlider.setOnOffsetChanged(ms -> { + offsetSlider.listener = ms -> { manager.setTemporaryOffsetMs(ms); // Same offset the panel drags, so it saves it the same way. manager.persistTemporaryOffsetMs(); onOffsetSync.accept(ms); - }); + }; final Runnable refreshTimes = () -> { startView.setText(formatTimeMs(times[0])); @@ -235,7 +235,7 @@ private static void submitSegment(String videoId, SegmentCategory category, if (stored) { scheduleSegmentRefresh(videoId, manager); } else { - dropDialogPreview(manager, videoId, startMs, endMs); + Utils.runOnMainThread(() -> manager.clearDialogSegment(videoId, startMs, endMs)); } } @@ -248,11 +248,6 @@ private static void scheduleSegmentRefresh(String videoId, LyricsManager manager }, REFRESH_DELAY_MS); } - private static void dropDialogPreview(LyricsManager manager, String videoId, - long startMs, long endMs) { - Utils.runOnMainThread(() -> manager.clearDialogSegment(videoId, startMs, endMs)); - } - private static void copySpan(Context context, String videoId, long[] times) { final long offsetMs = LyricsManager.getInstance().getTemporaryOffsetMs(); final String text = videoId + " [" + formatSeconds(times[0]) + ", " @@ -385,10 +380,6 @@ private static final class OffsetSliderView extends View { ThemeUtils.getCancelOrNeutralButtonBackgroundColor())); } - void setOnOffsetChanged(IntConsumer listener) { - this.listener = listener; - } - void setValue(int ms) { offsetMs = clamp(ms); invalidate(); diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/scrobbling/lastfm/LastFM.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/scrobbling/lastfm/LastFM.java index 345312b6d4..1c31b03f0e 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/scrobbling/lastfm/LastFM.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/scrobbling/lastfm/LastFM.java @@ -54,10 +54,6 @@ private static class CacheEntry { this.album = nullAlbum ? null : album; this.expiry = System.currentTimeMillis() + (nullAlbum ? CACHE_MISS_TTL : CACHE_HIT_TTL); } - - boolean isValid() { - return System.currentTimeMillis() < expiry; - } } public static class Session { @@ -162,7 +158,7 @@ public static String fetchAlbum(String artist, String track) { String key = artist.toLowerCase(Locale.ROOT) + "\u0000" + track.toLowerCase(Locale.ROOT); CacheEntry cached = albumCache.get(key); - if (cached != null && cached.isValid()) { + if (cached != null && System.currentTimeMillis() < cached.expiry) { return cached.album; } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsAiPromptPreference.java b/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsAiPromptPreference.java index 9a48c625c0..52ca63a915 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsAiPromptPreference.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsAiPromptPreference.java @@ -150,7 +150,11 @@ private void showDialog() { captionParams.topMargin = Dim.dp8; content.addView(caption, captionParams); - content.addView(createReasoningRow(context), reasoningRowParams()); + LinearLayout.LayoutParams reasoningParams = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT); + reasoningParams.topMargin = Dim.dp16; + content.addView(createReasoningRow(context), reasoningParams); Pair dialogPair = CustomDialog.create( context, @@ -302,12 +306,4 @@ private static String display(String effort) { thread.setDaemon(true); return thread; }); - - private static LinearLayout.LayoutParams reasoningRowParams() { - LinearLayout.LayoutParams params = new LinearLayout.LayoutParams( - LinearLayout.LayoutParams.MATCH_PARENT, - LinearLayout.LayoutParams.WRAP_CONTENT); - params.topMargin = Dim.dp16; - return params; - } } \ No newline at end of file diff --git a/extensions/reddit/src/main/java/app/morphe/extension/reddit/patches/ShowFlairsInHomeFeedPatch.java b/extensions/reddit/src/main/java/app/morphe/extension/reddit/patches/ShowFlairsInHomeFeedPatch.java index 20d8e2b049..a8928ed75c 100644 --- a/extensions/reddit/src/main/java/app/morphe/extension/reddit/patches/ShowFlairsInHomeFeedPatch.java +++ b/extensions/reddit/src/main/java/app/morphe/extension/reddit/patches/ShowFlairsInHomeFeedPatch.java @@ -150,19 +150,15 @@ private static void showFlair(CachedPost post, String source) { } if (WAITING_POSTS.size() > WAITING_POSTS_CLEANUP_SIZE) { - removeCollectedPosts(); + WAITING_POSTS.values().removeIf(references -> { + references.removeIf(reference -> reference.get() == null); + return references.isEmpty(); + }); } WAITING_POSTS.computeIfAbsent(fullId, id -> new CopyOnWriteArrayList<>()) .add(new WeakReference<>(post)); } - private static void removeCollectedPosts() { - WAITING_POSTS.values().removeIf(references -> { - references.removeIf(reference -> reference.get() == null); - return references.isEmpty(); - }); - } - /** * Caches the flair data, and adds the flair to the posts waiting for it. */ diff --git a/extensions/reddit/src/main/java/app/morphe/extension/reddit/patches/VersionCheckPatch.java b/extensions/reddit/src/main/java/app/morphe/extension/reddit/patches/VersionCheckPatch.java index 0dd1c4f12f..e36a781079 100644 --- a/extensions/reddit/src/main/java/app/morphe/extension/reddit/patches/VersionCheckPatch.java +++ b/extensions/reddit/src/main/java/app/morphe/extension/reddit/patches/VersionCheckPatch.java @@ -10,10 +10,5 @@ import app.morphe.extension.shared.Utils; public class VersionCheckPatch { - @SuppressWarnings("SameParameterValue") - private static boolean isVersionOrGreater(String version) { - return Utils.getAppVersionName().compareTo(version) >= 0; - } - - public static final boolean is_2026_25_or_greater = isVersionOrGreater("2026.25.0"); + public static final boolean is_2026_25_or_greater = Utils.getAppVersionName().compareTo("2026.25.0") >= 0; } diff --git a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/patches/CustomBrandingPatch.java b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/patches/CustomBrandingPatch.java index 4a1b9e6157..571bbc7975 100644 --- a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/patches/CustomBrandingPatch.java +++ b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/patches/CustomBrandingPatch.java @@ -176,7 +176,8 @@ private static int getNotificationSmallIcon() { * Injection point. */ public static View getLottieViewOrNull(View lottieStartupView) { - if (SplashAnimationPatch.isDisabled()) { + if (SharedYouTubeSettings.SPLASH_SCREEN_ANIMATION_STYLE.get() + == SplashAnimationPatch.SplashScreenAnimationStyle.DISABLED) { return null; } diff --git a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/patches/GmsCoreSupportPatch.java b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/patches/GmsCoreSupportPatch.java index e2a11368fd..bffeaf0c12 100644 --- a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/patches/GmsCoreSupportPatch.java +++ b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/patches/GmsCoreSupportPatch.java @@ -278,7 +278,7 @@ public static void checkGmsCore(Activity context) { } // Check if GmsCore is whitelisted from battery optimizations. - if (isAndroidAutomotive(context)) { + if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)) { // Ignore Android Automotive devices (Google built-in), // as there is no way to disable battery optimizations. Logger.printDebug(() -> "Device is Android Automotive"); @@ -374,7 +374,8 @@ private static void scanUser(Context context, int userId, @Nullable UserHandle h if (info == null) continue; final boolean officialMicroG = packageName.equals(GMS_CORE_PACKAGE_NAME) - && isOfficialMicroG(info); + // Signed by the official MicroG-RE signing key. + && matchesSigningCert(info); variants.add(new VariantPackage(packageName, userId, info.versionName, officialMicroG)); } @@ -504,13 +505,6 @@ private static int signatureFlags() { return PackageManager.GET_SIGNATURES; } - /** - * @return If the package is signed by the official MicroG-RE signing key. - */ - private static boolean isOfficialMicroG(PackageInfo packageInfo) { - return matchesSigningCert(packageInfo); - } - /** * @return If any certificate of the package, including the certificates of a signing key * rotation, matches the expected SHA-256 digest. @@ -856,10 +850,6 @@ private static boolean batteryOptimizationsEnabled(Context context) { return !powerManager.isIgnoringBatteryOptimizations(GMS_CORE_PACKAGE_NAME); } - private static boolean isAndroidAutomotive(Context context) { - return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE); - } - private static String getGmsCoreDownload() { //noinspection SwitchStatementWithTooFewBranches return switch (getGmsCoreVendorGroupId()) { diff --git a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/patches/NetworkProxyPatch.java b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/patches/NetworkProxyPatch.java index 50d973efd5..da57bbaca6 100644 --- a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/patches/NetworkProxyPatch.java +++ b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/patches/NetworkProxyPatch.java @@ -191,7 +191,10 @@ private static HttpURLConnection openAuthenticatedHttpProxyConnection( InetSocketAddress.createUnresolved(config.host, config.port) )) ); - setProxyAuthorizationHeader(connection); + String proxyAuthorization = getProxyAuthorizationHeader(); + if (proxyAuthorization != null) { + connection.setRequestProperty(PROXY_AUTHORIZATION_HEADER, proxyAuthorization); + } if (!config.allowDirectFallback) { return connection; @@ -238,13 +241,6 @@ private static List> getProxyHeaders() { )); } - private static void setProxyAuthorizationHeader(HttpURLConnection connection) { - String proxyAuthorization = getProxyAuthorizationHeader(); - if (proxyAuthorization != null) { - connection.setRequestProperty(PROXY_AUTHORIZATION_HEADER, proxyAuthorization); - } - } - private static String getProxyAuthorizationHeader() { if (!PROXY_AUTH_ENABLED.get()) { return null; diff --git a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/patches/SplashAnimationPatch.java b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/patches/SplashAnimationPatch.java index ba7ded6795..e0316abceb 100644 --- a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/patches/SplashAnimationPatch.java +++ b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/patches/SplashAnimationPatch.java @@ -81,11 +81,6 @@ public static SplashScreenAnimationStyle styleFromOrdinal(int style) { private static final String BRAND_SECONDARY_COLOR = "[0,0.686275,0.682353,1]"; private static final String BRAND_GRADIENT = "[0,0.117647,0.352941,0.658824,1,0,0.686275,0.682353]"; - public static boolean isDisabled() { - return SharedYouTubeSettings.SPLASH_SCREEN_ANIMATION_STYLE.get() - == SplashScreenAnimationStyle.DISABLED; - } - public static boolean isMonochrome() { SplashScreenAnimationStyle style = SharedYouTubeSettings.SPLASH_SCREEN_ANIMATION_STYLE.get(); return style == SplashScreenAnimationStyle.FPS_30_BLACK_AND_WHITE diff --git a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/requests/CronetTransport.java b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/requests/CronetTransport.java index 3aab2073af..2359ee68f4 100644 --- a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/requests/CronetTransport.java +++ b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/requests/CronetTransport.java @@ -98,10 +98,10 @@ public static HttpURLConnection openConnection( boolean allowDirectFallback ) throws IOException { boolean https = "https".equalsIgnoreCase(url.getProtocol()); - List engines = getEligibleEngines( - requireProxy, - allowDirectFallback - ); + List engines; + synchronized (ENGINE_LOCK) { + engines = getEligibleEnginesLocked(requireProxy, allowDirectFallback); + } HttpURLConnection connection = createCronetConnectionChain(url, engines); if (connection != null) { return addDirectHttpFallback(url, connection, allowDirectFallback); @@ -215,15 +215,6 @@ private static List awaitEngines( } } - private static List getEligibleEngines( - boolean requireProxy, - boolean allowDirectFallback - ) { - synchronized (ENGINE_LOCK) { - return getEligibleEnginesLocked(requireProxy, allowDirectFallback); - } - } - private static List getEligibleEnginesLocked( boolean requireProxy, boolean allowDirectFallback diff --git a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/requests/proxy/CronetHttpURLConnection.java b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/requests/proxy/CronetHttpURLConnection.java index 063b668642..46ad891291 100644 --- a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/requests/proxy/CronetHttpURLConnection.java +++ b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/requests/proxy/CronetHttpURLConnection.java @@ -83,7 +83,7 @@ private RequestDeadline responseDeadline() { // expose the boundary between connecting and reading response headers, so a finite total // deadline is only equivalent to HttpURLConnection when both phases are bounded. if (connectTimeout == 0 || readTimeout == 0) { - return RequestDeadline.none(); + return RequestDeadline.NONE; } RequestDeadline deadline = connectDeadline; @@ -109,7 +109,7 @@ private T executeWithDeadline(IoOperation operation, return operation.execute(); } - long remainingNanos = deadline.remainingNanos(); + long remainingNanos = deadline.timeoutNanos - (System.nanoTime() - deadline.startNanos); if (remainingNanos <= 0) { disconnectAfterTimeout(); throw createTimeoutException(null); @@ -348,10 +348,6 @@ private RequestDeadline(long startNanos, long timeoutNanos) { this.timeoutNanos = timeoutNanos; } - private static RequestDeadline none() { - return NONE; - } - private static RequestDeadline afterMillis(long timeoutMillis) { return timeoutMillis <= 0 ? NONE @@ -365,10 +361,6 @@ private boolean isFinite() { return timeoutNanos != 0; } - private long remainingNanos() { - return timeoutNanos - (System.nanoTime() - startNanos); - } - private RequestDeadline extendByMillis(long timeoutMillis) { return isFinite() && timeoutMillis > 0 ? new RequestDeadline( diff --git a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/returnyoutubedislike/ReturnYouTubeDislikeButtons.java b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/returnyoutubedislike/ReturnYouTubeDislikeButtons.java index f0bf0f5489..5487ef9678 100644 --- a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/returnyoutubedislike/ReturnYouTubeDislikeButtons.java +++ b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/returnyoutubedislike/ReturnYouTubeDislikeButtons.java @@ -569,17 +569,6 @@ private static long parseLabelCount(String label) { private static final int MAX_BAR_PARENTS = 5; private static final int MAX_BAR_DEPTH = 6; - /** - * The counts of the old action bar sit in the like button, a neighbor of the dislike button, - * and Litho reports the text of one host only, so the whole bar is searched. - * - * @return If anything in the bar holding this button shows text, such as a count or a label. - */ - private static boolean barShowsText(View host) { - View bar = barOf(host); - return bar == host ? hostShowsText(host) : subtreeShowsText(bar, 0); - } - /** * The height is used rather than the width, since the dislike button is no longer square. * @@ -696,7 +685,10 @@ void setButton(String label, boolean isLike) { private boolean hasOwnLabel() { Boolean cached = hasOwnLabel; if (cached == null) { - hasOwnLabel = cached = barShowsText(host); + // The counts of the old action bar sit in the like button, a neighbor of the dislike + // button, and Litho reports the text of one host only, so the whole bar is searched. + View bar = barOf(host); + hasOwnLabel = cached = bar == host ? hostShowsText(host) : subtreeShowsText(bar, 0); } return cached; } diff --git a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/returnyoutubedislike/requests/ReturnYouTubeDislikeAPI.java b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/returnyoutubedislike/requests/ReturnYouTubeDislikeAPI.java index 4076843de1..aecca3b2cb 100644 --- a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/returnyoutubedislike/requests/ReturnYouTubeDislikeAPI.java +++ b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/returnyoutubedislike/requests/ReturnYouTubeDislikeAPI.java @@ -155,21 +155,6 @@ public static int getNumberOfRateLimitRequestsEncountered() { private ReturnYouTubeDislikeAPI() { } // utility class - /** - * Simulates a slow response by doing meaningless calculations. - * Used to debug the app UI and verify UI timeout logic works - */ - private static void randomlyWaitIfLocallyDebugging() { - final boolean DEBUG_RANDOMLY_DELAY_NETWORK_CALLS = false; // set true to debug UI - - //noinspection ConstantValue - if (DEBUG_RANDOMLY_DELAY_NETWORK_CALLS) { - final long amountOfTimeToWaste = (long) (Math.random() - * (API_GET_VOTES_TCP_TIMEOUT_MILLISECONDS + API_GET_VOTES_HTTP_TIMEOUT_MILLISECONDS)); - Utils.doNothingForDuration(amountOfTimeToWaste); - } - } - /** * Clears any backoff rate limits in effect. * Should be called if RYD is turned on/off. @@ -293,7 +278,16 @@ public static RYDVoteData fetchVotes(String videoId) { connection.setConnectTimeout(API_GET_VOTES_TCP_TIMEOUT_MILLISECONDS); // timeout for TCP connection to server connection.setReadTimeout(API_GET_VOTES_HTTP_TIMEOUT_MILLISECONDS); // timeout for server response - randomlyWaitIfLocallyDebugging(); + // Simulates a slow response by doing meaningless calculations. + // Used to debug the app UI and verify UI timeout logic works + final boolean DEBUG_RANDOMLY_DELAY_NETWORK_CALLS = false; // set true to debug UI + + //noinspection ConstantValue + if (DEBUG_RANDOMLY_DELAY_NETWORK_CALLS) { + final long amountOfTimeToWaste = (long) (Math.random() + * (API_GET_VOTES_TCP_TIMEOUT_MILLISECONDS + API_GET_VOTES_HTTP_TIMEOUT_MILLISECONDS)); + Utils.doNothingForDuration(amountOfTimeToWaste); + } final int responseCode = connection.getResponseCode(); if (checkIfRateLimitWasHit(responseCode)) { diff --git a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/settings/preference/ExternalDownloaderPreference.java b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/settings/preference/ExternalDownloaderPreference.java index 862ed732b3..b5a363fef9 100644 --- a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/settings/preference/ExternalDownloaderPreference.java +++ b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/settings/preference/ExternalDownloaderPreference.java @@ -179,10 +179,6 @@ public static Downloader findByPackageName(String packageName) { this.downloadUrl = downloadUrl; this.isPreferred = isPreferred; } - - public boolean isInstalled() { - return packageName != null && isAppInstalledAndEnabled(packageName); - } } private static boolean isAppInstalledAndEnabled(String packageName) { @@ -229,7 +225,8 @@ private void updateEntries() { List entryValues = new ArrayList<>(); for (Downloader downloader : Downloader.values()) { - if (downloader.isPreferred || downloader.isInstalled()) { + if (downloader.isPreferred || (downloader.packageName != null + && isAppInstalledAndEnabled(downloader.packageName))) { String packageName = downloader.packageName; entries.add(downloader.name); diff --git a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/settings/preference/ExternalPoTokenProviderAboutPreference.java b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/settings/preference/ExternalPoTokenProviderAboutPreference.java index 3992e92bf1..e6010e8935 100644 --- a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/settings/preference/ExternalPoTokenProviderAboutPreference.java +++ b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/settings/preference/ExternalPoTokenProviderAboutPreference.java @@ -36,7 +36,10 @@ public class ExternalPoTokenProviderAboutPreference extends Preference { public void onActivityResumed(@NonNull Activity activity) { Logger.printDebug(() -> "onActivityResumed"); - updateUI(); + String summaryKey = isAvailable() + ? "morphe_external_potoken_provider_settings_summary" + : "morphe_external_potoken_provider_about_summary"; + setSummary(str(summaryKey)); } public void onActivityCreated(@NonNull Activity a, @Nullable Bundle b) {} @@ -51,35 +54,20 @@ private static boolean isAvailable() { return SharedYouTubeSettings.EXTERNAL_POTOKEN_PROVIDER.isAvailable(); } - private void registerApplicationOnResumeCallback() { - ((Application) getContext().getApplicationContext()).registerActivityLifecycleCallbacks( - ACTIVITY_LIFECYCLE_CALLBACKS - ); - } - - private void unregisterApplicationOnResumeCallback() { - ((Application) getContext().getApplicationContext()).unregisterActivityLifecycleCallbacks( - ACTIVITY_LIFECYCLE_CALLBACKS - ); - } - @Override protected void onAttachedToHierarchy(PreferenceManager preferenceManager) { super.onAttachedToHierarchy(preferenceManager); - registerApplicationOnResumeCallback(); + ((Application) getContext().getApplicationContext()).registerActivityLifecycleCallbacks( + ACTIVITY_LIFECYCLE_CALLBACKS + ); } @Override protected void onPrepareForRemoval() { super.onPrepareForRemoval(); - unregisterApplicationOnResumeCallback(); - } - - private void updateUI() { - String summaryKey = isAvailable() - ? "morphe_external_potoken_provider_settings_summary" - : "morphe_external_potoken_provider_about_summary"; - setSummary(str(summaryKey)); + ((Application) getContext().getApplicationContext()).unregisterActivityLifecycleCallbacks( + ACTIVITY_LIFECYCLE_CALLBACKS + ); } @Override diff --git a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/settings/preference/ExternalPoTokenProviderPreference.java b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/settings/preference/ExternalPoTokenProviderPreference.java index 025cfbb1e8..19549f0214 100644 --- a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/settings/preference/ExternalPoTokenProviderPreference.java +++ b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/settings/preference/ExternalPoTokenProviderPreference.java @@ -45,28 +45,20 @@ public void onActivitySaveInstanceState(@NonNull Activity a, @NonNull Bundle b) public void onActivityDestroyed(@NonNull Activity a) {} }; - private void registerApplicationOnResumeCallback() { - ((Application) getContext().getApplicationContext()).registerActivityLifecycleCallbacks( - ACTIVITY_LIFECYCLE_CALLBACKS - ); - } - - private void unregisterApplicationOnResumeCallback() { - ((Application) getContext().getApplicationContext()).unregisterActivityLifecycleCallbacks( - ACTIVITY_LIFECYCLE_CALLBACKS - ); - } - @Override protected void onAttachedToHierarchy(PreferenceManager preferenceManager) { super.onAttachedToHierarchy(preferenceManager); - registerApplicationOnResumeCallback(); + ((Application) getContext().getApplicationContext()).registerActivityLifecycleCallbacks( + ACTIVITY_LIFECYCLE_CALLBACKS + ); } @Override protected void onPrepareForRemoval() { super.onPrepareForRemoval(); - unregisterApplicationOnResumeCallback(); + ((Application) getContext().getApplicationContext()).unregisterActivityLifecycleCallbacks( + ACTIVITY_LIFECYCLE_CALLBACKS + ); } @Override diff --git a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/settings/preference/FeatureFlagsManagerPreference.java b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/settings/preference/FeatureFlagsManagerPreference.java index 00167a43c5..2151d576c0 100644 --- a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/settings/preference/FeatureFlagsManagerPreference.java +++ b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/settings/preference/FeatureFlagsManagerPreference.java @@ -331,7 +331,8 @@ private View createContentView(Context context, Dialog dialog) { private EditText createSearchBox(Context context) { EditText search = CustomDialog.createSearchBar(context, str("morphe_debug_feature_flags_manager_search_hint"), query -> { - adapter.setSearchQuery(query); + adapter.searchQuery = query == null ? "" : query.trim(); + adapter.refresh(); listView.clearChoices(); updateChips(); updateBottomBar(); @@ -1025,11 +1026,6 @@ private class FlagAdapter extends BaseAdapter { this.context = context; } - void setSearchQuery(String query) { - searchQuery = query == null ? "" : query.trim(); - refresh(); - } - void refresh() { shownFlags.clear(); for (Map.Entry entry : flagStates.entrySet()) { diff --git a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/settings/preference/OAuth2Preference.java b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/settings/preference/OAuth2Preference.java index 9564834562..a43ca0d8b2 100644 --- a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/settings/preference/OAuth2Preference.java +++ b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/settings/preference/OAuth2Preference.java @@ -116,12 +116,6 @@ public void onActivitySaveInstanceState(@NonNull Activity a, @NonNull Bundle b) public void onActivityDestroyed(@NonNull Activity a) {} }; - private void registerApplicationOnResumeCallback() { - ((Application) getContext().getApplicationContext()).registerActivityLifecycleCallbacks( - ACTIVITY_LIFECYCLE_CALLBACKS - ); - } - private void unregisterApplicationOnResumeCallback() { ((Application) getContext().getApplicationContext()).unregisterActivityLifecycleCallbacks( ACTIVITY_LIFECYCLE_CALLBACKS @@ -132,25 +126,17 @@ private void unregisterApplicationOnResumeCallback() { protected void onAttachedToHierarchy(PreferenceManager preferenceManager) { super.onAttachedToHierarchy(preferenceManager); updateUI(); - addChangeListener(); + Setting.preferences.preferences.registerOnSharedPreferenceChangeListener(listener); } @Override protected void onPrepareForRemoval() { super.onPrepareForRemoval(); - removeChangeListener(); + Setting.preferences.preferences.unregisterOnSharedPreferenceChangeListener(listener); // Remove just in case the user never finished signing in. unregisterApplicationOnResumeCallback(); } - private void addChangeListener() { - Setting.preferences.preferences.registerOnSharedPreferenceChangeListener(listener); - } - - private void removeChangeListener() { - Setting.preferences.preferences.unregisterOnSharedPreferenceChangeListener(listener); - } - protected boolean isRefreshTokenSaved() { return !SharedYouTubeSettings.OAUTH2_REFRESH_TOKEN.get().isEmpty(); } @@ -371,7 +357,9 @@ private String getDefaultCustomTabsBrowser(Context context) { private void openInBrowser(Context context, String userCode, String verificationUrl) { // Automatically fetch the auth token after the user returns. - registerApplicationOnResumeCallback(); + ((Application) getContext().getApplicationContext()).registerActivityLifecycleCallbacks( + ACTIVITY_LIFECYCLE_CALLBACKS + ); Utils.setClipboard(userCode); Intent baseIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(verificationUrl)); diff --git a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/settings/search/BaseSearchViewController.java b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/settings/search/BaseSearchViewController.java index 041cfe476a..838ed061d1 100644 --- a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/settings/search/BaseSearchViewController.java +++ b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/settings/search/BaseSearchViewController.java @@ -140,7 +140,10 @@ protected BaseSearchViewController(Activity activity, Toolbar toolbar, BasePrefe // Initialize components initializeSearchView(); initializeOverlayContainer(); - initializeSearchHistoryManager(); + searchHistoryManager = new SearchHistoryManager(activity, overlayContainer, query -> { + searchView.setQuery(query, true); + hideSearchHistory(); + }); setupToolbarMenu(); setupListeners(); } @@ -238,16 +241,6 @@ private void initializeOverlayContainer() { } } - /** - * Initializes the search history manager with the specified overlay container and listener. - */ - private void initializeSearchHistoryManager() { - searchHistoryManager = new SearchHistoryManager(activity, overlayContainer, query -> { - searchView.setQuery(query, true); - hideSearchHistory(); - }); - } - // Abstract interface for preference fragments. public interface BasePreferenceFragment { PreferenceScreen getPreferenceScreenForSearch(); diff --git a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/settings/search/SearchHistoryManager.java b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/settings/search/SearchHistoryManager.java index 474589dccb..7bf5100aca 100644 --- a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/settings/search/SearchHistoryManager.java +++ b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/settings/search/SearchHistoryManager.java @@ -177,7 +177,7 @@ public void showSearchHistory() { } // Show the search history container. - showSearchHistoryContainer(); + searchHistoryContainer.setVisibility(View.VISIBLE); } /** @@ -206,14 +206,6 @@ protected void saveSearchHistory() { SharedYouTubeSettings.SETTINGS_SEARCH_ENTRIES.save(String.join("\n", searchHistory)); } - /** - * Removes a search query from the history. - */ - public void removeSearchQuery(String query) { - searchHistory.remove(query); - saveSearchHistory(); - } - /** * Clears all search history. */ @@ -232,12 +224,6 @@ public boolean isSearchHistoryEnabled() { return showSettingsSearchHistory; } - /** - * Shows the search history container and overlay. - */ - public void showSearchHistoryContainer() { - searchHistoryContainer.setVisibility(View.VISIBLE); - } /** * Hides the search history container. @@ -360,7 +346,8 @@ public void notifyDataSetChanged() { query, str("morphe_settings_search_remove_message"), () -> { - removeSearchQuery(query); + searchHistory.remove(query); + saveSearchHistory(); remove(query); notifyDataSetChanged(); } diff --git a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/sponsorblock/objects/SegmentCategory.java b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/sponsorblock/objects/SegmentCategory.java index 8416ef25e4..7fbbe0853d 100644 --- a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/sponsorblock/objects/SegmentCategory.java +++ b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/sponsorblock/objects/SegmentCategory.java @@ -335,14 +335,6 @@ private void setColor(@ColorInt int colorWithOpacity) { paint.setColor(colorWithOpacity); } - /** - * Gets the color with opacity applied (ARGB). - */ - @ColorInt - public int getColorWithOpacity() { - return color; - } - /** * @return The default color with opacity applied. */ @@ -355,7 +347,7 @@ public int getDefaultColorWithOpacity() { * @return The color as a hex string without opacity (#RRGGBB). */ public String getColorStringWithoutOpacity() { - final int colorNoOpacity = getColorWithOpacity() & 0x00FFFFFF; + final int colorNoOpacity = color & 0x00FFFFFF; return String.format(Locale.US, "#%06X", colorNoOpacity); } @@ -379,18 +371,11 @@ private static SpannableString getCategoryColorDotSpan(String text, @ColorInt in return dotSpan; } - /** - * Returns the category title with a colored dot. - */ - public SpannableString getTitleWithColorDot(@ColorInt int categoryColor) { - return getCategoryColorDotSpan(" " + title, categoryColor); - } - /** * Returns the category title with a colored dot. */ public SpannableString getTitleWithColorDot() { - return getTitleWithColorDot(color); + return getCategoryColorDotSpan(" " + title, color); } /** diff --git a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/spoof/requests/StreamingDataRequest.java b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/spoof/requests/StreamingDataRequest.java index 235a003049..fbe2f812c0 100644 --- a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/spoof/requests/StreamingDataRequest.java +++ b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/spoof/requests/StreamingDataRequest.java @@ -542,11 +542,6 @@ private static StreamData fetch(String videoId, boolean isInline, Map "Debug: Blocking main thread"); } return future.get(MAX_MILLISECONDS_TO_WAIT_FOR_FETCH, TimeUnit.MILLISECONDS); diff --git a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/theme/ThemeColorOverlay.java b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/theme/ThemeColorOverlay.java index 96a8db5bc7..682f47f622 100644 --- a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/theme/ThemeColorOverlay.java +++ b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/theme/ThemeColorOverlay.java @@ -121,11 +121,6 @@ private static void unregister(Context context, boolean dark) { @Nullable private static ResourcesLoader lightResourcesLoader; - @Nullable - private static ResourcesLoader resourcesLoader(boolean dark) { - return dark ? darkResourcesLoader : lightResourcesLoader; - } - private static void setResourcesLoader(boolean dark, @Nullable ResourcesLoader loader) { if (dark) { darkResourcesLoader = loader; @@ -153,7 +148,7 @@ static void applyTo(Context context, boolean dark, boolean changeForeground) { } private static void applyTo(Context context, boolean dark) throws Exception { - ResourcesLoader loader = resourcesLoader(dark); + ResourcesLoader loader = dark ? darkResourcesLoader : lightResourcesLoader; if (loader == null) { OverlayInfo overlayInfo = findOverlay(context, dark); if (overlayInfo == null) { diff --git a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/theme/ThemeColorPatch.java b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/theme/ThemeColorPatch.java index 61c58b73e5..e1910ae402 100644 --- a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/theme/ThemeColorPatch.java +++ b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/theme/ThemeColorPatch.java @@ -863,9 +863,6 @@ private static float oklabDistance(float[] lab1, float[] lab2) { return (float) Math.sqrt(dl * dl + da * da + db * db); } - private static boolean useOverlay(boolean dark) { - return dark ? useDarkOverlay : useLightOverlay; - } /** * Registers, updates or removes the overlay of both themes. @@ -894,7 +891,7 @@ private static void updateOverlay(Context context, ThemeColor dark, ThemeColor l private static void updateOverlay(Context context, boolean dark, String[] resourceNames, StringSetting customColorSetting) { try { - if (!useOverlay(dark)) { + if (!(dark ? useDarkOverlay : useLightOverlay)) { ThemeColorOverlay.unregisterIfRegistered(context, dark); return; } @@ -971,7 +968,9 @@ public static int getThemeColor(Context context, boolean dark, int index) { ? (!dark ? configValue : lightConfigValue) : LIGHT_INDEX_OFFSET + APP_DEFAULT_CONFIG_VALUE); - String resourceName = themeColorResourceName(dark); + // The first name is the color the app uses for the color itself. + final String[] resourceNames = colorResourceNames(dark); + String resourceName = resourceNames.length > 0 ? resourceNames[0] : ""; if (resourceName.isEmpty()) { // The app has no theme of this kind, and no color of it to show. return ThemeUtils.getAppBackgroundColor(); @@ -1069,7 +1068,10 @@ private static void setIndicatorColor(View view, int color) { // The count is a text view, and its text must stay readable on the new color. if (view instanceof TextView count) { - final int textColor = getIndicatorTextColor(view.getContext()); + // Never black in practice, an indicator color exists only with a Material You color. + final int textColor = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S + ? view.getContext().getColor(android.R.color.system_neutral1_900) + : Color.BLACK; if (count.getCurrentTextColor() != textColor) { count.setTextColor(textColor); @@ -1122,19 +1124,6 @@ private static boolean usesMaterialYouAccent(boolean dark) { ).usesMaterialYouAccent(); } - /** - * The color of the text of the new content count, which must be readable - * on {@link #getIndicatorColor(Context)}. - */ - public static int getIndicatorTextColor(Context context) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - return context.getColor(android.R.color.system_neutral1_900); - } - - // Never reached, an indicator color exists only with a Material You color. - return Color.BLACK; - } - private static int customColor(boolean dark) { return customColor(dark ? SharedYouTubeSettings.THEME_COLOR_DARK_CUSTOM @@ -1159,14 +1148,6 @@ private static int customColor(StringSetting setting) { } } - /** - * The first name is the color the app uses for the color itself. - */ - private static String themeColorResourceName(boolean dark) { - final String[] resourceNames = colorResourceNames(dark); - return resourceNames.length > 0 ? resourceNames[0] : ""; - } - private static String[] colorResourceNames(boolean dark) { final String resourceNames = dark ? darkColorResourceNames() : lightColorResourceNames(); return resourceNames.isEmpty() ? new String[0] : resourceNames.split(","); diff --git a/extensions/shared/library/src/main/java/app/morphe/extension/shared/settings/preference/AbstractPreferenceFragment.java b/extensions/shared/library/src/main/java/app/morphe/extension/shared/settings/preference/AbstractPreferenceFragment.java index 00653b99b2..ed74b04f69 100644 --- a/extensions/shared/library/src/main/java/app/morphe/extension/shared/settings/preference/AbstractPreferenceFragment.java +++ b/extensions/shared/library/src/main/java/app/morphe/extension/shared/settings/preference/AbstractPreferenceFragment.java @@ -865,14 +865,6 @@ void clearLogs() { notifyDataSetChanged(); } - /** - * @return The list of logs matching the current search filter (used for zero-copy file streaming). - */ - @NonNull - List getFilteredLogs() { - return filteredLogs; - } - /** * @return Only the logs matching the current search filter, capped at * {@link Logger#CLIPBOARD_MAX_LENGTH} to respect Android's Binder transaction limit. @@ -1019,7 +1011,7 @@ public void showLogDialog() { Button buttonExport = CustomDialog.createButton(context, dialogPair.first, str("morphe_settings_export_file"), - () -> exportToFile(logAdapter.getFilteredLogs()), + () -> exportToFile(logAdapter.filteredLogs), false, true); LinearLayout.LayoutParams buttonExportParams = new LinearLayout.LayoutParams( diff --git a/extensions/shared/library/src/main/java/app/morphe/extension/shared/settings/preference/IconListPreference.java b/extensions/shared/library/src/main/java/app/morphe/extension/shared/settings/preference/IconListPreference.java index cd1b308db6..9fb7b6fb47 100644 --- a/extensions/shared/library/src/main/java/app/morphe/extension/shared/settings/preference/IconListPreference.java +++ b/extensions/shared/library/src/main/java/app/morphe/extension/shared/settings/preference/IconListPreference.java @@ -276,7 +276,7 @@ protected void showDialog(Bundle state) { setSummary(originalEntries[position]); } } - adapter.setSelectedValue(selected); + adapter.selectedValue = selected; adapter.notifyDataSetChanged(); } clearHighlightedEntriesForDialog(); @@ -373,9 +373,5 @@ public View getView(int position, View convertView, @NonNull ViewGroup parent) { return view; } - - public void setSelectedValue(String value) { - this.selectedValue = value; - } } } diff --git a/extensions/shared/library/src/main/java/app/morphe/extension/shared/settings/preference/about/AboutDialogStyle.java b/extensions/shared/library/src/main/java/app/morphe/extension/shared/settings/preference/about/AboutDialogStyle.java index 749e466b3e..4f4a3cf51c 100644 --- a/extensions/shared/library/src/main/java/app/morphe/extension/shared/settings/preference/about/AboutDialogStyle.java +++ b/extensions/shared/library/src/main/java/app/morphe/extension/shared/settings/preference/about/AboutDialogStyle.java @@ -74,14 +74,6 @@ static String isolateLtr(String text) { return '\u2066' + text + '\u2069'; } - /** - * @return The accent used for icons and highlights, picked from the logo gradient so it stays - * legible against the current dialog background. - */ - private static String accentColor() { - return Utils.isDarkModeEnabled() ? MORPHE_TEAL : MORPHE_BLUE; - } - private static String materialIcon(String path) { return ""; } @@ -116,15 +108,6 @@ static String escapeHtml(String text) { .replace(">", ">"); } - /** - * @return The escaped text with any urls in it turned into links, so the addresses named inside - * a license can be opened. - */ - static String linkifyHtml(String text) { - return escapeHtml(text) - .replaceAll("(https?://[^\\s<>\"]+)", "$1"); - } - /** * @return A heading for the group of rows that follows it. */ @@ -143,7 +126,9 @@ static String documentStart() { BaseSettings.MORPHE_LANGUAGE.get().getLocale()) ? "rtl" : "ltr") .replace("{bg}", Utils.getColorHexString(ThemeUtils.getDialogBackgroundColor())) .replace("{fg}", Utils.getColorHexString(ThemeUtils.getAppForegroundColor())) - .replace("{accent}", accentColor()) + // Accent of icons and highlights, picked from the logo gradient so it stays + // legible against the current dialog background. + .replace("{accent}", Utils.isDarkModeEnabled() ? MORPHE_TEAL : MORPHE_BLUE) .replace("{blue}", MORPHE_BLUE) .replace("{teal}", MORPHE_TEAL); } diff --git a/extensions/shared/library/src/main/java/app/morphe/extension/shared/settings/preference/about/LicenseTextDialog.java b/extensions/shared/library/src/main/java/app/morphe/extension/shared/settings/preference/about/LicenseTextDialog.java index 0a21ddebc0..17110d0774 100644 --- a/extensions/shared/library/src/main/java/app/morphe/extension/shared/settings/preference/about/LicenseTextDialog.java +++ b/extensions/shared/library/src/main/java/app/morphe/extension/shared/settings/preference/about/LicenseTextDialog.java @@ -107,7 +107,9 @@ private String buildHtml() { html.append("

"); for (LicenseContent item : content) { html.append("

").append(AboutDialogStyle.escapeHtml(item.title())).append("

"); - html.append("
").append(AboutDialogStyle.linkifyHtml(item.content())).append("
"); + // Urls are turned into links, so the addresses named inside a license can be opened. + html.append("
").append(AboutDialogStyle.escapeHtml(item.content())
+                    .replaceAll("(https?://[^\\s<>\"]+)", "$1")).append("
"); } html.append("
").append(AboutDialogStyle.DOCUMENT_END); diff --git a/extensions/shared/library/src/main/java/app/morphe/extension/shared/settings/preference/about/MorpheCreditsDialog.java b/extensions/shared/library/src/main/java/app/morphe/extension/shared/settings/preference/about/MorpheCreditsDialog.java index 5096b432f4..27758f0d4b 100644 --- a/extensions/shared/library/src/main/java/app/morphe/extension/shared/settings/preference/about/MorpheCreditsDialog.java +++ b/extensions/shared/library/src/main/java/app/morphe/extension/shared/settings/preference/about/MorpheCreditsDialog.java @@ -47,14 +47,6 @@ class MorpheCreditsDialog extends AboutWebViewDialog { static boolean showVancedAsPastContributor = true; - private static List getWorksLinksPrior() { - List prior = new ArrayList<>(WORKS_LINKS_PRIOR); - if (showVancedAsPastContributor) { - prior.add(WORKS_VANCED); - } - return prior; - } - private static String createDialogHtml() { StringBuilder html = new StringBuilder(AboutDialogStyle.documentStart()); @@ -63,7 +55,11 @@ private static String createDialogHtml() { .append(""); appendContributors(html, str("morphe_settings_about_contributors_current"), WORKS_LINKS_CURRENT); - appendContributors(html, str("morphe_settings_about_contributors_prior"), getWorksLinksPrior()); + List prior = new ArrayList<>(WORKS_LINKS_PRIOR); + if (showVancedAsPastContributor) { + prior.add(WORKS_VANCED); + } + appendContributors(html, str("morphe_settings_about_contributors_prior"), prior); // In-app user-facing attribution of licenses and notices (Apache 2.0 criteria). html.append("
") diff --git a/extensions/shared/library/src/main/java/app/morphe/extension/shared/translation/TextTranslator.java b/extensions/shared/library/src/main/java/app/morphe/extension/shared/translation/TextTranslator.java index 02745e2ab8..cdd78c66e5 100644 --- a/extensions/shared/library/src/main/java/app/morphe/extension/shared/translation/TextTranslator.java +++ b/extensions/shared/library/src/main/java/app/morphe/extension/shared/translation/TextTranslator.java @@ -203,7 +203,11 @@ private static List requestLines(@NonNull List lines, Exception lastFailure = null; for (int attempt = 0; attempt < MAX_ATTEMPTS; attempt++) { if (attempt > 0) { - sleepQuietly(INITIAL_BACKOFF_MILLISECONDS * attempt); + try { + Thread.sleep(INITIAL_BACKOFF_MILLISECONDS * attempt); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } } try { @@ -329,12 +333,4 @@ private static List splitRomanized(@NonNull String romanized, private static boolean isRetryable(int code) { return code == 403 || code == 404 || code == 429 || (code >= 500 && code <= 599); } - - private static void sleepQuietly(long millis) { - try { - Thread.sleep(millis); - } catch (InterruptedException ex) { - Thread.currentThread().interrupt(); - } - } } diff --git a/extensions/shared/library/src/main/java/app/morphe/extension/shared/ui/SheetBottomDialog.java b/extensions/shared/library/src/main/java/app/morphe/extension/shared/ui/SheetBottomDialog.java index 64a3a6ac19..801b2ce781 100644 --- a/extensions/shared/library/src/main/java/app/morphe/extension/shared/ui/SheetBottomDialog.java +++ b/extensions/shared/library/src/main/java/app/morphe/extension/shared/ui/SheetBottomDialog.java @@ -75,7 +75,7 @@ public static SlideDialog createSlideDialog(@NonNull Context context, @NonNull V // Drag container. DraggableLinearLayout dragContainer = new DraggableLinearLayout(context, animationDuration); dragContainer.setOrientation(LinearLayout.VERTICAL); - dragContainer.setDialog(dialog); + dragContainer.dialog = dialog; // Top spacer - drag handle and tap-to-dismiss area above the sheet content. View spacer = new View(context); @@ -98,8 +98,8 @@ public static SlideDialog createSlideDialog(@NonNull Context context, @NonNull V Utils.setDialogWindowParameters(window, Gravity.BOTTOM, 0, 100, false); } - dialog.setAnimView(dragContainer); - dialog.setAnimationDuration(animationDuration); + dialog.animView = dragContainer; + dialog.duration = animationDuration; return dialog; } @@ -237,13 +237,6 @@ public DraggableLinearLayout(@NonNull Context context, int animDuration) { postDelayed(() -> isDragEnabled = true, animationDuration + 50); } - /** - * Sets the {@link SlideDialog} associated with this layout for dismissal. - */ - public void setDialog(@NonNull SlideDialog dialog) { - this.dialog = dialog; - } - /** * Updates the dismissal threshold when the layout's size changes. */ @@ -406,20 +399,6 @@ public SlideDialog(@NonNull Context context) { screenHeight = context.getResources().getDisplayMetrics().heightPixels; } - /** - * Sets the view to animate during show and dismiss operations. - */ - public void setAnimView(@NonNull View view) { - this.animView = view; - } - - /** - * Sets the duration of the slide-in and slide-out animations. - */ - public void setAnimationDuration(int duration) { - this.duration = duration; - } - /** * Displays the dialog with a slide-up animation for the animated view. */ diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/AutoTranslateCaptionsPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/AutoTranslateCaptionsPatch.java index dd99ba656f..f162359c09 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/AutoTranslateCaptionsPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/AutoTranslateCaptionsPatch.java @@ -103,7 +103,8 @@ static List addTranslationLanguages(List videoIds = new ArrayList<>(); for (ChannelSearchResult result : searchesDescriptions ? lastResponse.results : allResults()) { - if (searchesDescriptions || (activeSort.usesViewCount() && result.viewCount < 0) - || (activeSort.usesLength() && result.lengthSeconds <= 0)) { + // The view count sorts need the exact view count of every result, + // and the length sorts the length of every result. + if (searchesDescriptions + || ((activeSort == SortOption.MOST_VIEWED || activeSort == SortOption.LEAST_VIEWED) + && result.viewCount < 0) + || ((activeSort == SortOption.SHORTEST || activeSort == SortOption.LONGEST) + && result.lengthSeconds <= 0)) { videoIds.add(result.videoId); } } @@ -735,7 +719,10 @@ public void onClick(View v) { if (controller.getActiveSort() == selectedOption) { return true; } - controller.setSort(selectedOption); + controller.activeSort = selectedOption; + controller.scrollView.scrollTo(0, 0); + controller.completeResults(); + controller.refreshResults(); updateButtonText(); return true; }); diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FeedCardLayoutPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FeedCardLayoutPatch.java index fd3ce6c48d..bbd8421bb3 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FeedCardLayoutPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FeedCardLayoutPatch.java @@ -1741,7 +1741,13 @@ private static void drawShortsShelfCard(RecyclerView list, @Nullable View header final boolean rightColumn = cellLeft + cell.getWidth() >= right; final int corners = (topRow && leftColumn ? 8 : 0) | (topRow && rightColumn ? 4 : 0) | (bottomRow && leftColumn ? 2 : 0) | (bottomRow && rightColumn ? 1 : 0); - setShortsCellOutline(cell, SHORTS_CELL_OUTLINES[corners]); + // The Shorts clip their thumbnail with the rounded outline of a view inside the Short, + // which is replaced. The views are recycled, so it's checked every time the list is drawn. + ViewOutlineProvider outline = SHORTS_CELL_OUTLINES[corners]; + setShortsCellOutline(cell, cell.getWidth(), outline, 0); + + // The screen bounds of the Short, for the inline player above it. + shortsCellOutlines.put(getScreenBounds(cell), outline); } } @@ -1849,17 +1855,6 @@ private static void findShortsCells(View view, List cells, int depth) { } } - /** - * The Shorts clip their thumbnail with the rounded outline of a view inside the Short, - * which is replaced. The views are recycled, so it's checked every time the list is drawn. - */ - private static void setShortsCellOutline(View cell, ViewOutlineProvider outline) { - setShortsCellOutline(cell, cell.getWidth(), outline, 0); - - // The screen bounds of the Short, for the inline player above it. - shortsCellOutlines.put(getScreenBounds(cell), outline); - } - private static String getScreenBounds(View view) { int[] location = new int[2]; view.getLocationOnScreen(location); diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FeedVideoActionsPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FeedVideoActionsPatch.java index aedb3e1601..5fb1b5d35b 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FeedVideoActionsPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FeedVideoActionsPatch.java @@ -211,13 +211,6 @@ void placeBadge(ComponentHost host) { } } - void removeBadge(ComponentHost host) { - if (badgeAdded) { - host.getOverlay().remove(badge); - badgeAdded = false; - } - } - boolean isBadgeHit(ComponentHost host, float x, float y) { if (!badgeAdded || FeedVideoCardsPatch.isCardPlayingInline(host) || FeedVideoCardsPatch.isPlaylistCard(host)) { @@ -527,7 +520,10 @@ private static VideoCard updateCard(ComponentHost host) { if (menuButton == null) { if (card != null) { - card.removeBadge(host); + if (card.badgeAdded) { + host.getOverlay().remove(card.badge); + card.badgeAdded = false; + } cards.remove(host); } return null; @@ -622,9 +618,12 @@ public static boolean onComponentHostTouch(ComponentHost host, MotionEvent event if (gestureEnded) { if (card != null) { card.badge.setPressed(false); + // Saves the video to Watch later. if (action == MotionEvent.ACTION_UP - && card.isBadgeHit(host, event.getX(), event.getY())) { - saveToWatchLater(host, card); + && card.isBadgeHit(host, event.getX(), event.getY()) + && card.getMenuButton() != null) { + vibrate(host, false); + openMenuItem(card, false); } } resetGesture(); @@ -800,14 +799,6 @@ private static void vibrate(View view, boolean longPress) { HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING); } - private static void saveToWatchLater(ComponentHost host, VideoCard card) { - if (card.getMenuButton() == null) { - return; - } - vibrate(host, false); - openMenuItem(card, false); - } - /** * Opens the menu of the video without showing it, and clicks its own item. * diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FullscreenVideoScalePatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FullscreenVideoScalePatch.java index 8eca75f5ca..d6ac1edeb1 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FullscreenVideoScalePatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FullscreenVideoScalePatch.java @@ -272,7 +272,10 @@ private static void mapContentToDisplay(View view) { } private static float getVideoAspectRatio(View view, int viewW, int viewH) { - View child = getVideoView(view); + // The video view is sized to the video when the renderer does not letterbox the video itself. + View child = view instanceof PlayerViewInterface player + ? player.patch_getVideoView() + : null; if (child != null && child != view && child.getWidth() > 8 && child.getHeight() > 8) { float childAR = child.getWidth() / (float) child.getHeight(); float parentAR = viewW / (float) viewH; @@ -523,16 +526,6 @@ private static View findPlayerView(View view, int depth) { return null; } - /** - * The video view is sized to the video when the renderer does not letterbox the video itself. - */ - @Nullable - private static View getVideoView(View playerView) { - return playerView instanceof PlayerViewInterface player - ? player.patch_getVideoView() - : null; - } - private static final class SavedClipState { final WeakReference viewRef; final boolean clipToOutline; diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/OverrideYouTubeMusicButtonsPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/OverrideYouTubeMusicButtonsPatch.java index 3e8eb1b954..cfa72fc8c6 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/OverrideYouTubeMusicButtonsPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/OverrideYouTubeMusicButtonsPatch.java @@ -21,16 +21,11 @@ public class OverrideYouTubeMusicButtonsPatch { private static final String MORPHE_MUSIC_PACKAGE_NAME = "app.morphe.android.apps.youtube.music"; private static final String HIJACK_FLAG = "morphe_hijacked"; - private static String getTargetPackage() { - String customTarget = Settings.CUSTOM_MUSIC_PACKAGE_NAME.get().trim(); - if (customTarget.isEmpty()) { - return MORPHE_MUSIC_PACKAGE_NAME; - } - return customTarget; - } - private static Intent hijackIntent(Intent intent) { - String target = getTargetPackage(); + String target = Settings.CUSTOM_MUSIC_PACKAGE_NAME.get().trim(); + if (target.isEmpty()) { + target = MORPHE_MUSIC_PACKAGE_NAME; + } PackageManager pm = Utils.getContext().getPackageManager(); Intent launchIntent = pm.getLaunchIntentForPackage(target); diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/PlayerVolumePatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/PlayerVolumePatch.java index 24ccb369e7..0c2047f208 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/PlayerVolumePatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/PlayerVolumePatch.java @@ -106,13 +106,9 @@ private static void applyMultiplier() { private static void scheduleEnforce() { if (enforceScheduled) return; enforceScheduled = true; - Utils.runOnMainThreadDelayed(PlayerVolumePatch::enforceTick, enforceIntervalMs()); - } - - private static long enforceIntervalMs() { - return duckMultiplier == 1.0f + Utils.runOnMainThreadDelayed(PlayerVolumePatch::enforceTick, duckMultiplier == 1.0f ? MUTE_ENFORCE_INTERVAL_MS - : DUCK_ENFORCE_INTERVAL_MS; + : DUCK_ENFORCE_INTERVAL_MS); } private static void enforceTick() { diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/ReturnYouTubeDislikePatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/ReturnYouTubeDislikePatch.java index 7acea5db5a..3ac199625b 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/ReturnYouTubeDislikePatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/ReturnYouTubeDislikePatch.java @@ -47,14 +47,6 @@ public class ReturnYouTubeDislikePatch { @Nullable private static volatile String lastPrefetchedVideoId; - private static void clearData() { - currentVideoData = null; - - // Rolling number text should not be cleared, - // as it's used if incognito Short is opened/closed - // while a regular video is on screen. - } - // // Litho player for both regular videos and Shorts. // @@ -184,7 +176,10 @@ public static void newVideoLoaded(String videoId) { PlayerType currentPlayerType = PlayerType.getCurrent(); if (currentPlayerType.isNoneHiddenOrSlidingMinimized()) { // Must clear here, otherwise the wrong data can be used for a minimized regular video. - clearData(); + // Rolling number text should not be cleared, + // as it's used if incognito Short is opened/closed + // while a regular video is on screen. + currentVideoData = null; return; } diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/ShortsAutoplayPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/ShortsAutoplayPatch.java index 971d8881e5..88840e2a55 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/ShortsAutoplayPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/ShortsAutoplayPatch.java @@ -70,14 +70,6 @@ static void setYTEnumValue(Enum ytBehavior) { private Enum ytEnumValue; } - /** - * @return If the app is currently in background PiP mode. - */ - private static boolean isAppInBackgroundPiPMode() { - Activity activity = Utils.getActivity(); - return activity != null && activity.isInPictureInPictureMode(); - } - /** * Injection point. */ @@ -96,7 +88,8 @@ public static void setYTShortsRepeatEnum(Enum ytEnum) { */ public static Enum changeShortsRepeatBehavior(Enum original) { try { - final boolean inBackgroundPiP = isAppInBackgroundPiPMode(); + Activity activity = Utils.getActivity(); + final boolean inBackgroundPiP = activity != null && activity.isInPictureInPictureMode(); boolean autoplay = inBackgroundPiP ? Settings.SHORTS_AUTOPLAY_BACKGROUND.get() : Settings.SHORTS_AUTOPLAY.get(); diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/VideoInformation.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/VideoInformation.java index b60bd611fa..112e988bb4 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/VideoInformation.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/VideoInformation.java @@ -443,18 +443,11 @@ public static void setVideoId(String newlyLoadedVideoId) { } } - /** - * @return If the player parameters are for a Short. - */ - public static boolean playerParametersAreShort(String parameters) { - return parameters.startsWith(SHORTS_PLAYER_PARAMETERS); - } - /** * Injection point. */ public static String newPlayerResponseSignature(String signature, String videoId, boolean isShortAndOpeningOrPlaying) { - final boolean isShort = playerParametersAreShort(signature); + final boolean isShort = signature.startsWith(SHORTS_PLAYER_PARAMETERS); playerResponseVideoIdIsShort = isShort; if (!isShort || isShortAndOpeningOrPlaying) { if (videoIdIsShort != isShort) { @@ -924,16 +917,6 @@ private static long getPlayerVideoTime() { : -1; } - /** - * @return The current casting player time. Value is zero if not casting. - */ - private static long getMdxVideoTime() { - PlaybackController controller = mdxPlayerDirectorRef.get(); - return controller != null - ? controller.patch_getVideoTime() - : -1; - } - /** * Playback time of the current video playing. Includes Shorts. * If casting then the time is always rounded down to the nearest whole second. @@ -947,7 +930,11 @@ public static long getVideoTime() { return playerTime; } - final long mdxTime = getMdxVideoTime(); + // The casting player time. + PlaybackController controller = mdxPlayerDirectorRef.get(); + final long mdxTime = controller != null + ? controller.patch_getVideoTime() + : -1; return mdxTime >= 0 ? mdxTime : playerTime; diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/dearrow/DeArrowPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/dearrow/DeArrowPatch.java index a0785dca8e..8ac3151984 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/dearrow/DeArrowPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/dearrow/DeArrowPatch.java @@ -341,7 +341,12 @@ public static boolean useDeArrowTitlesForCurrentNavigation() { @Nullable private static String buildYouTubeVideoStillURL(DecodedThumbnailURL decodedURL, ThumbnailQuality qualityToUse) { - ThumbnailQuality quality = UnavailableQualities.getQualityToTry(decodedURL.videoId, qualityToUse); + // The quality if it has not failed to load, a lower quality, + // or null if the original thumbnail must be used. + UnavailableQualities unavailable = UnavailableQualities.unavailableVideoIdLookup.get(decodedURL.videoId); + ThumbnailQuality quality = unavailable == null + ? qualityToUse // Unknown if it exists. Use the URL anyway and update afterward if loading fails. + : unavailable.getAvailableQuality(decodedURL.videoId, qualityToUse); if (quality == null) { return null; } @@ -584,14 +589,16 @@ public static void handleCronetSuccess(UrlRequest request, UrlResponseInfo respo Logger.printDebug(() -> "handleCronetSuccess, image not available: " + decodedURL.sanitizedURL); - ThumbnailQuality quality = ThumbnailQuality.altImageNameToQuality(decodedURL.imageQuality); + ThumbnailQuality quality = ThumbnailQuality.altNameToEnum.get(decodedURL.imageQuality); if (quality == null) { // Video is a short or a seekbar thumbnail, but somehow did not load. Should not happen. Logger.printDebug(() -> "Failed to recognize image quality of URL: " + decodedURL.sanitizedURL); return; } - UnavailableQualities.setQualityNotAvailable(decodedURL.videoId, quality); + UnavailableQualities.unavailableVideoIdLookup.computeIfAbsent( + decodedURL.videoId, key -> new UnavailableQualities() + ).setNotAvailable(decodedURL.videoId, quality); LithoRelayoutPatch.remountListViews(); } } catch (Exception ex) { @@ -662,15 +669,6 @@ private enum ThumbnailQuality { } } - /** - * Convert an alt image name to enum. - * ie: "hq720_2" returns {@link #HQ720}. - */ - @Nullable - static ThumbnailQuality altImageNameToQuality(String altImageName) { - return altNameToEnum.get(altImageName); - } - /** * Original quality to effective alt quality to use. * ie: "sddefault" returns {@link #HQ720}. @@ -726,25 +724,6 @@ private static class UnavailableQualities { private static final Map unavailableVideoIdLookup = Collections.synchronizedMap(Utils.createSizeRestrictedMap(1000)); - /** - * @return The quality to try for the video, which is the quality if it has not failed to load, - * a lower quality, or null if the original thumbnail must be used. - */ - @Nullable - static ThumbnailQuality getQualityToTry(String videoId, ThumbnailQuality quality) { - UnavailableQualities unavailable = unavailableVideoIdLookup.get(videoId); - if (unavailable == null) { - return quality; // Unknown if it exists. Use the URL anyway and update afterward if loading fails. - } - return unavailable.getAvailableQuality(videoId, quality); - } - - static void setQualityNotAvailable(String videoId, ThumbnailQuality quality) { - UnavailableQualities unavailable = unavailableVideoIdLookup.computeIfAbsent( - videoId, key -> new UnavailableQualities()); - unavailable.setNotAvailable(videoId, quality); - } - /** * Lowest quality that failed to load. Higher qualities are assumed to also not be available. */ diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/originaltitles/RestoreOriginalTitlesPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/originaltitles/RestoreOriginalTitlesPatch.java index e5ece5a088..20a928c7b9 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/originaltitles/RestoreOriginalTitlesPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/originaltitles/RestoreOriginalTitlesPatch.java @@ -1785,7 +1785,7 @@ private static boolean restoreTitle(List textNodes, Set texts return false; } final boolean layoutTitle = findLayoutTitle != null && candidate.equals(findLayoutTitle.get()); - if (requestCandidateTitle(videoId, texts, candidate) || titles == null) { + if (requestCandidateTitle(videoId, texts, candidate, 0) || titles == null) { if (titles != null) { addPendingTitleTexts(texts, videoId, component); } @@ -1979,15 +1979,9 @@ private static void requestLocalizedTitleIfNeeded(String videoId, Set te * The titles of each text found as the title of the video are fetched once, or again if they * failed to fetch because of network errors. * - * @param texts Texts of the element. - * @return If the other titles of the text are being fetched. - */ - private static boolean requestCandidateTitle(String videoId, Set texts, String candidate) { - return requestCandidateTitle(videoId, texts, candidate, 0); - } - - /** + * @param texts Texts of the element. * @param retries Number of times the titles were fetched again after failing to fetch. + * @return If the other titles of the text are being fetched. */ private static boolean requestCandidateTitle(String videoId, Set texts, String candidate, int retries) { diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/playback/livestream/RememberLiveStreamPositionPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/playback/livestream/RememberLiveStreamPositionPatch.java index ccab4c9729..356c7e10d0 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/playback/livestream/RememberLiveStreamPositionPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/playback/livestream/RememberLiveStreamPositionPatch.java @@ -207,7 +207,7 @@ private static void checkRestore(long generation) { return; } - SavedPosition saved = loadPlaybackPosition(videoId); + SavedPosition saved = getSavedPositions().get(videoId); if (saved == null) { // Nothing remembered for this video. Allow saving again. restorePending = false; @@ -354,10 +354,6 @@ private static void savePlaybackPosition(String videoId, long positionMs, long v saveSavedPositions(); } - private static SavedPosition loadPlaybackPosition(String videoId) { - return getSavedPositions().get(videoId); - } - private static void deletePlaybackPosition(String videoId) { Map map = getSavedPositions(); if (map.remove(videoId) != null) { diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/FeedVideoCardsPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/FeedVideoCardsPatch.java index 0d31fd7788..dd536df89c 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/FeedVideoCardsPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/FeedVideoCardsPatch.java @@ -130,15 +130,6 @@ public final class FeedVideoCardsPatch { } } - /** - * @return The video of a card that was laid out, or null if it is not known. - */ - @Nullable - public static String getCardVideoId(ComponentHost host) { - String videoId = cardVideoIds.get(host); - return PLAYLIST.equals(videoId) ? null : videoId; - } - /** * @return If the video plays in the feed, where the inline player shows its own buttons * over the thumbnail, such as the mute and the captions buttons. @@ -155,7 +146,9 @@ public static boolean isPlayingInline(@Nullable String videoId) { * @return If what is drawn on the thumbnail of the card is hidden now. */ public static boolean isCardPlayingInline(ComponentHost host) { - return isPlayingInline(getCardVideoId(host)); + // The video of the card that was laid out, or null if it is not known. + String videoId = cardVideoIds.get(host); + return isPlayingInline(PLAYLIST.equals(videoId) ? null : videoId); } /** diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/FlyoutUtils.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/FlyoutUtils.java index 107e7e5534..08f101e176 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/FlyoutUtils.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/FlyoutUtils.java @@ -555,7 +555,7 @@ private static void addFlyoutElements(Object flyoutPanel) { } if (nextButtonIndex > 0) { - addDivider(flyoutPanel, nextButtonIndex); + addFlyoutMenuItem(flyoutPanel, null, null, null, nextButtonIndex, true); } // Reset 'topFlyoutMenuVisible' field, once the buttons have been injected into the player's @@ -641,11 +641,6 @@ public static int addFlyoutButton( return addFlyoutMenuItem(flyoutPanel, icon, text, clickListener, index, false); } - @SuppressWarnings("UnusedReturnValue") - private static int addDivider(Object flyoutPanel, int index) { - return addFlyoutMenuItem(flyoutPanel, null, null, null, index, true); - } - private static int addFlyoutMenuItem( Object flyoutPanel, @Nullable Drawable icon, @@ -958,7 +953,9 @@ public static void extractFlyoutIdFromObject(@Nullable Object bufferObject) { return; } - byte[] flyoutBuffer = getFlyoutBuffer(bufferObject); + byte[] flyoutBuffer = bufferObject instanceof ProtocolBufferFieldInterface bufferInterface + ? bufferInterface.patch_getBuffer() + : null; if (flyoutBuffer == null) { return; } @@ -1049,13 +1046,6 @@ public static void extractFlyoutIdFromObject(@Nullable Object bufferObject) { } } - private static byte[] getFlyoutBuffer(@Nullable Object bufferObject) { - if (!(bufferObject instanceof ProtocolBufferFieldInterface bufferInterface)) { - return null; - } - return bufferInterface.patch_getBuffer(); - } - public static void setFlyoutVideoId(byte[] buffer, String rawDescription) { if (buffer == null || rawDescription == null || rawDescription.isEmpty()) { return; diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/LocalQueueSheet.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/LocalQueueSheet.java index 6a0a9e37d0..8e7169655a 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/LocalQueueSheet.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/LocalQueueSheet.java @@ -203,7 +203,7 @@ private void render() { String nowPlayingId = VideoInformation.getVideoId(); if (!PlayerType.getCurrent().isNoneOrHidden() && !nowPlayingId.isEmpty()) { - nowPlayingRow = createNowPlayingRow(nowPlayingId); + nowPlayingRow = buildRow(nowPlayingId, -1, null); list.addView(nowPlayingRow.root); } @@ -212,7 +212,8 @@ private void render() { } for (int i = 0, size = items.size(); i < size; i++) { - Row row = createRow(items.get(i), i); + LocalQueuePatch.Item item = items.get(i); + Row row = buildRow(item.videoId, i, item); rows.add(row); list.addView(row.root); } @@ -289,14 +290,6 @@ void bindText(@Nullable String title, @Nullable String channel) { } } - private Row createRow(LocalQueuePatch.Item item, int index) { - return buildRow(item.videoId, index, item); - } - - private Row createNowPlayingRow(String videoId) { - return buildRow(videoId, -1, null); - } - private Row buildRow(String videoId, int index, @Nullable LocalQueuePatch.Item item) { final boolean nowPlaying = item == null; final int rowColor = nowPlaying ? blend(backgroundColor, foregroundColor, 0.08f) : backgroundColor; diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/requests/AiSListSubmitRequest.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/requests/AiSListSubmitRequest.java index d49e1c48ee..0343862927 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/requests/AiSListSubmitRequest.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/requests/AiSListSubmitRequest.java @@ -135,7 +135,14 @@ public static String submit(String handle, String videoUrl, String username) { return "morphe_aislist_submit_success"; } - String error = readErrorResponse(connection); + // The API describes its errors in a plain text body, which is only used for logging. + String errorResponse; + try { + errorResponse = Requester.parseErrorString(connection); + } catch (Exception ex) { + errorResponse = ""; + } + final String error = errorResponse; Logger.printDebug(() -> "Submit failed with code: " + responseCode + " response: " + error); @@ -199,15 +206,4 @@ private static void writeBody(HttpURLConnection connection, String body) throws connection.setFixedLengthStreamingMode(bytes.length); connection.getOutputStream().write(bytes); } - - /** - * The API describes its errors in a plain text body, which is only used for logging. - */ - private static String readErrorResponse(HttpURLConnection connection) { - try { - return Requester.parseErrorString(connection); - } catch (Exception ex) { - return ""; - } - } } diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/requests/ChannelSearchRequest.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/requests/ChannelSearchRequest.java index 704673ee5b..ced31145f5 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/requests/ChannelSearchRequest.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/requests/ChannelSearchRequest.java @@ -205,7 +205,9 @@ private static ChannelSearchResponse fetchSingle(String channelId, String query, String channelName = parseChannelName(json); int[] indexRef = {0}; for (int page = 1; ; page++) { - String nextToken = parseResponsePage(json, allResults, seenVideoIds, indexRef, isEnglish); + String browseToken = parseBrowseContents(json, allResults, seenVideoIds, indexRef, isEnglish); + String actionToken = parseContinuationActions(json, allResults, seenVideoIds, indexRef, isEnglish); + String nextToken = browseToken != null ? browseToken : actionToken; final boolean isLast = nextToken == null || nextToken.isEmpty() || page >= MAX_CONTINUATION_PAGES || allResults.size() >= MAX_SEARCH_RESULTS; ChannelSearchResponse response = new ChannelSearchResponse(channelName, new ArrayList<>(allResults)); @@ -236,15 +238,6 @@ private static ChannelSearchResponse fetchSingle(String channelId, String query, return null; } - @Nullable - private static String parseResponsePage(JSONObject json, List results, - Set seenVideoIds, int[] indexRef, boolean isEnglish) { - String browseToken = parseBrowseContents(json, results, seenVideoIds, indexRef, isEnglish); - String actionToken = parseContinuationActions(json, results, seenVideoIds, indexRef, isEnglish); - - return browseToken != null ? browseToken : actionToken; - } - @Nullable private static String parseBrowseContents(JSONObject json, List results, Set seenVideoIds, int[] indexRef, boolean isEnglish) { diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/series/CatalogParser.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/series/CatalogParser.java index a6d9ac9880..1584f7c237 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/series/CatalogParser.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/series/CatalogParser.java @@ -210,13 +210,10 @@ static String text(JSONObject text) { StringBuilder result = new StringBuilder(); JSONArray runs = text.optJSONArray("runs"); if (runs != null) - for (int i = 0; i < runs.length(); i++) - result.append(objectAt(runs, i).optString("text")); + for (int i = 0; i < runs.length(); i++) { + JSONObject run = runs.optJSONObject(i); + result.append(run == null ? "" : run.optString("text")); + } return result.toString(); } - - private static JSONObject objectAt(JSONArray array, int index) { - JSONObject v = array.optJSONObject(index); - return v == null ? new JSONObject() : v; - } } diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/series/TrackerRepository.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/series/TrackerRepository.java index 6a03865fdd..e226fb59f7 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/series/TrackerRepository.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/series/TrackerRepository.java @@ -467,11 +467,6 @@ public Series series(String id) { } } - public void saveSeries( - String id, String name, String bookmark, CatalogClient.Catalog catalog, long now) { - saveSeries(id, name, bookmark, catalog, "", now); - } - public void saveSeries( String id, String name, @@ -939,6 +934,7 @@ int restoreMissing(LibraryBackup.Data backup, long now) { s.name(), s.bookmarkId(), s.complete() ? new CatalogClient.Catalog(s.name(), episodes) : null, + "", now); db.execSQL( "UPDATE series SET cursor_id=?,cursor_ordinal=?,cursor_revision=?," diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/preference/SpoofVideoStreamsSideEffectsPreference.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/preference/SpoofVideoStreamsSideEffectsPreference.java index 7fe9e849ae..585d79f9d2 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/preference/SpoofVideoStreamsSideEffectsPreference.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/preference/SpoofVideoStreamsSideEffectsPreference.java @@ -59,25 +59,17 @@ public SpoofVideoStreamsSideEffectsPreference(Context context) { super(context); } - private void addChangeListener() { - Setting.preferences.preferences.registerOnSharedPreferenceChangeListener(listener); - } - - private void removeChangeListener() { - Setting.preferences.preferences.unregisterOnSharedPreferenceChangeListener(listener); - } - @Override protected void onAttachedToHierarchy(PreferenceManager preferenceManager) { super.onAttachedToHierarchy(preferenceManager); updateUI(); - addChangeListener(); + Setting.preferences.preferences.registerOnSharedPreferenceChangeListener(listener); } @Override protected void onPrepareForRemoval() { super.onPrepareForRemoval(); - removeChangeListener(); + Setting.preferences.preferences.unregisterOnSharedPreferenceChangeListener(listener); } private void updateUI() { diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/preference/SwipeVolumeStepsPreference.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/preference/SwipeVolumeStepsPreference.java index 5fb0d94b14..1266ff260b 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/preference/SwipeVolumeStepsPreference.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/preference/SwipeVolumeStepsPreference.java @@ -65,7 +65,13 @@ private void init() { for (int i = 0, length = entryValues.length; i < length; i++) { String value = entryValues[i].toString(); - int steps = stepsForValue(value); + // The number of steps of the entry value, 0 for the device default, or -1 if unknown. + int steps; + try { + steps = SwipeVolumeSteps.valueOf(value).getSteps(); + } catch (IllegalArgumentException ex) { + steps = -1; + } // A step of a single volume index is what the device already does, // so such an entry would silently change nothing if it was shown. @@ -82,15 +88,4 @@ private void init() { setEntries(shownEntries.toArray(new CharSequence[0])); setEntryValues(shownValues.toArray(new CharSequence[0])); } - - /** - * @return The number of steps of an entry value, 0 for the device default, or -1 if unknown. - */ - private static int stepsForValue(String value) { - try { - return SwipeVolumeSteps.valueOf(value).getSteps(); - } catch (IllegalArgumentException ex) { - return -1; - } - } } diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/preference/SwipeZonePreference.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/preference/SwipeZonePreference.java index f407387037..f513adc987 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/preference/SwipeZonePreference.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/preference/SwipeZonePreference.java @@ -135,25 +135,17 @@ public SwipeZonePreference(Context context) { init(); } - private void addChangeListener() { - Setting.preferences.preferences.registerOnSharedPreferenceChangeListener(listener); - } - - private void removeChangeListener() { - Setting.preferences.preferences.unregisterOnSharedPreferenceChangeListener(listener); - } - @Override protected void onAttachedToHierarchy(PreferenceManager preferenceManager) { super.onAttachedToHierarchy(preferenceManager); updateUI(); - addChangeListener(); + Setting.preferences.preferences.registerOnSharedPreferenceChangeListener(listener); } @Override protected void onPrepareForRemoval() { super.onPrepareForRemoval(); - removeChangeListener(); + Setting.preferences.preferences.unregisterOnSharedPreferenceChangeListener(listener); } private void init() { diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/sponsorblock/ui/SponsorBlockViewController.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/sponsorblock/ui/SponsorBlockViewController.java index 209758c3f2..7d2eff49ab 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/sponsorblock/ui/SponsorBlockViewController.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/sponsorblock/ui/SponsorBlockViewController.java @@ -255,7 +255,10 @@ private static void playerTypeChanged(PlayerType playerType) { canShowViewElements = (isWatchFullScreen || playerType == PlayerType.WATCH_WHILE_MAXIMIZED); NewSegmentLayout newSegmentLayout = newSegmentLayoutRef.get(); - setNewSegmentLayoutMargins(newSegmentLayout, isWatchFullScreen); + if (newSegmentLayout != null) { + setLayoutMargins(newSegmentLayout, isWatchFullScreen, + newSegmentLayout.defaultBottomMargin, newSegmentLayout.ctaBottomMargin); + } setViewVisibility(newSegmentLayout, newSegmentLayoutVisible); SkipSponsorButton skipHighlightButton = skipHighlightButtonRef.get(); @@ -270,11 +273,6 @@ private static void playerTypeChanged(PlayerType playerType) { } } - private static void setNewSegmentLayoutMargins(@Nullable NewSegmentLayout layout, boolean fullScreen) { - if (layout != null) { - setLayoutMargins(layout, fullScreen, layout.defaultBottomMargin, layout.ctaBottomMargin); - } - } private static void setSkipButtonMargins(@Nullable SkipSponsorButton button, boolean fullScreen) { if (button != null) { setLayoutMargins(button, fullScreen, button.defaultBottomMargin, button.ctaBottomMargin); diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/videoplayer/FullscreenVideoScaleButton.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/videoplayer/FullscreenVideoScaleButton.java index a1536f54c0..1d8c3ad6e7 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/videoplayer/FullscreenVideoScaleButton.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/videoplayer/FullscreenVideoScaleButton.java @@ -42,7 +42,7 @@ public static void initializeButton(View controlsView) { ImageView added = PlayerOverlayButton.addButton( controlsView, - getIconName(Settings.FULLSCREEN_VIDEO_SCALE.get()), + PlayerIcons.name(Settings.FULLSCREEN_VIDEO_SCALE.get().iconBaseName), FullscreenVideoScaleButton::isOverlayButtonEnabled, view -> cycleScaleMode(), null @@ -120,8 +120,4 @@ private static void updateButtonIcon(VideoScaleMode mode) { legacy.setIcon(icon); } } - - private static String getIconName(VideoScaleMode mode) { - return PlayerIcons.name(mode.iconBaseName); - } } diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/videoplayer/VideoQualityDialogButton.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/videoplayer/VideoQualityDialogButton.java index 1c7d9a0895..a5c5d23b79 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/videoplayer/VideoQualityDialogButton.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/videoplayer/VideoQualityDialogButton.java @@ -297,7 +297,8 @@ private static void showVideoQualityDialog(Context context) { // Create ListView for quality selection. ListView listView = new ListView(context); CustomQualityAdapter adapter = new CustomQualityAdapter(context, qualityLabels); - adapter.setSelectedPosition(listViewSelectedIndex); + adapter.selectedPosition = listViewSelectedIndex; + adapter.notifyDataSetChanged(); listView.setAdapter(adapter); listView.setDivider(null); @@ -349,11 +350,6 @@ public CustomQualityAdapter(@NonNull Context context, @NonNull List obje super(context, 0, objects); } - private void setSelectedPosition(int position) { - this.selectedPosition = position; - notifyDataSetChanged(); - } - @NonNull @Override public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { From 85544128284a29522310d43ed6bfb3e99706e9d4 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Sat, 24 Oct 2026 12:47:25 +0000 Subject: [PATCH 036/189] fix(YouTube - Miniplayer): "Disable resuming miniplayer" may not work with latest experimental app target --- .../youtube/settings/YouTubeActivityHook.java | 4 +- .../youtube/layout/miniplayer/Fingerprints.kt | 38 +++++++++++++++++-- .../layout/miniplayer/MiniplayerPatch.kt | 9 +++-- 3 files changed, 43 insertions(+), 8 deletions(-) diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/YouTubeActivityHook.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/YouTubeActivityHook.java index e82ce82d10..a6b4f4f0ee 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/YouTubeActivityHook.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/YouTubeActivityHook.java @@ -36,8 +36,8 @@ @SuppressWarnings({"deprecation", "RedundantSuppression"}) public class YouTubeActivityHook extends BaseActivityHook { - public static final boolean USE_BOLD_ICONS = Settings.SETTINGS_INITIALIZED.get() - && VersionCheckPatch.IS_20_31_OR_GREATER + public static final boolean USE_BOLD_ICONS = (VersionCheckPatch.IS_21_36_OR_GREATER || + (Settings.SETTINGS_INITIALIZED.get() && VersionCheckPatch.IS_20_31_OR_GREATER)) && !SpoofAppVersionPatch.isSpoofingToLessThan("20.31.00") && !Settings.RESTORE_OLD_SETTINGS_MENUS.get(); diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/miniplayer/Fingerprints.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/miniplayer/Fingerprints.kt index 45745c628a..4c034780d9 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/miniplayer/Fingerprints.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/miniplayer/Fingerprints.kt @@ -419,11 +419,43 @@ internal object MiniplayerSetIconsLegacyFingerprint : Fingerprint( ) /** - * 21.42+ + * 21.43+ * - * Matches the resume (Continue watching) flag check of the show miniplayer command. + * Matches the resume (Continue watching) check of the show miniplayer command. */ internal object ShowMiniplayerCommandFingerprint: Fingerprint( + classFingerprint = Fingerprint( + filters = listOf( + literal(164817L), + literal(121253L) + ) + ), + accessFlags = listOf(AccessFlags.PUBLIC, AccessFlags.FINAL), + returnType = "V", + parameters = listOf("L", "L", "Ljava/util/Map;"), + filters = listOf( + methodCall( + opcode = Opcode.INVOKE_DIRECT, + definingClass = "this", + returnType = "Z" + ), + opcode( + opcode = Opcode.MOVE_RESULT, + location = MatchAfterImmediately() + ), + opcode( + opcode = Opcode.IF_EQZ, + location = MatchAfterImmediately() + ) + ) +) + +/** + * 21.42. + * + * Matches the resume (Continue watching) flag check of the show miniplayer command. + */ +internal object ShowMiniplayerCommandLegacy2142Fingerprint: Fingerprint( classFingerprint = Fingerprint( filters = listOf( literal(164817L), @@ -440,7 +472,7 @@ internal object ShowMiniplayerCommandFingerprint: Fingerprint( ), opcode( opcode = Opcode.IF_EQZ, - location = MatchAfterWithin(10) + location = MatchAfterImmediately() ) ) ) diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/miniplayer/MiniplayerPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/miniplayer/MiniplayerPatch.kt index 691313fccc..9fc77b0ccb 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/miniplayer/MiniplayerPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/miniplayer/MiniplayerPatch.kt @@ -41,6 +41,7 @@ import app.morphe.patches.youtube.misc.playservice.is_21_29_or_greater import app.morphe.patches.youtube.misc.playservice.is_21_30_or_greater import app.morphe.patches.youtube.misc.playservice.is_21_32_or_greater import app.morphe.patches.youtube.misc.playservice.is_21_42_or_greater +import app.morphe.patches.youtube.misc.playservice.is_21_43_or_greater import app.morphe.patches.youtube.misc.playservice.versionCheckPatch import app.morphe.patches.youtube.misc.settings.PreferenceScreen import app.morphe.patches.youtube.misc.settings.settingsPatch @@ -194,11 +195,13 @@ val miniplayerPatch = bytecodePatch( // Only skip the resume branch of the command. Other show miniplayer commands, // such as "Play next in queue" from the feed when no video is open, still work. - if (is_21_42_or_greater) { - ShowMiniplayerCommandFingerprint to 1 + (if (is_21_43_or_greater) { + ShowMiniplayerCommandFingerprint to 2 + } else if (is_21_42_or_greater) { + ShowMiniplayerCommandLegacy2142Fingerprint to 1 } else { ShowMiniplayerCommandLegacyFingerprint to 3 - }.let { (fingerprint, matchIndex) -> + }).let { (fingerprint, matchIndex) -> fingerprint.method.apply { val index = fingerprint.instructionMatches[matchIndex].index + 1 val free = findFreeRegister(index) From aeb3012a1d4fc42e9cb0e80e1cc139f24d445fb7 Mon Sep 17 00:00:00 2001 From: 0xrxL <127248639+0xrxL@users.noreply.github.com> Date: Sat, 24 Oct 2026 15:07:03 +0200 Subject: [PATCH 037/189] fix(YouTube - Ambient mode): Ambient mode is not disabled with 21.43 --- .../youtube/layout/hide/ambientmode/AmbientModePatch.kt | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/ambientmode/AmbientModePatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/ambientmode/AmbientModePatch.kt index b1f28d5dd2..8673acc53a 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/ambientmode/AmbientModePatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/ambientmode/AmbientModePatch.kt @@ -225,8 +225,15 @@ val ambientModePatch = bytecodePatch( const/4 v0, 0x1 invoke-static { v0 }, $EXTENSION_IS_AMBIENT_MODE_ENABLED move-result v0 - if-eqz v0, :keep + if-nez v0, :enabled + + # Ambient mode is disabled, so the renderer of the server is not used either. + # 21.43+ has no feature flags that turn ambient mode off. + invoke-static { }, $optionalType->empty()$optionalType + move-result-object p1 + return-object p1 + :enabled # No /next response yet. sget-object v0, $waitingStateField if-eq p0, v0, :keep From eb9cea95a65c222b7cc5d89a48f5836389f22df2 Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Sat, 24 Oct 2026 13:27:29 +0000 Subject: [PATCH 038/189] chore: Update translations from Crowdin --- .../values-af-rZA/music/strings.xml | 52 ++++++++++------ .../values-ar-rSA/music/strings.xml | 52 ++++++++++------ .../values-az-rAZ/music/strings.xml | 52 ++++++++++------ .../values-bg-rBG/music/strings.xml | 52 ++++++++++------ .../values-bn-rBD/music/strings.xml | 52 ++++++++++------ .../values-cs-rCZ/music/strings.xml | 52 ++++++++++------ .../values-da-rDK/music/strings.xml | 52 ++++++++++------ .../values-de-rDE/music/strings.xml | 52 ++++++++++------ .../values-el-rGR/music/strings.xml | 52 ++++++++++------ .../values-es-rES/music/strings.xml | 52 ++++++++++------ .../values-fa-rIR/music/strings.xml | 52 ++++++++++------ .../values-fi-rFI/music/strings.xml | 52 ++++++++++------ .../values-fil-rPH/music/strings.xml | 52 ++++++++++------ .../values-fr-rFR/music/strings.xml | 60 ++++++++++++------- .../values-ga-rIE/music/strings.xml | 52 ++++++++++------ .../values-hi-rIN/music/strings.xml | 52 ++++++++++------ .../values-hr-rHR/music/strings.xml | 52 ++++++++++------ .../values-hu-rHU/music/strings.xml | 52 ++++++++++------ .../values-in-rID/music/strings.xml | 52 ++++++++++------ .../values-in-rID/youtube/strings.xml | 8 +-- .../values-is-rIS/music/strings.xml | 52 ++++++++++------ .../values-it-rIT/music/strings.xml | 52 ++++++++++------ .../values-iw-rIL/music/strings.xml | 52 ++++++++++------ .../values-ja-rJP/music/strings.xml | 52 ++++++++++------ .../values-ja-rJP/youtube/strings.xml | 10 ++-- .../values-ko-rKR/music/strings.xml | 52 ++++++++++------ .../values-lt-rLT/music/strings.xml | 52 ++++++++++------ .../values-nb-rNO/music/strings.xml | 52 ++++++++++------ .../values-nl-rNL/music/strings.xml | 52 ++++++++++------ .../values-pl-rPL/music/strings.xml | 60 ++++++++++++------- .../values-pl-rPL/youtube/strings.xml | 10 ++-- .../values-pt-rBR/music/strings.xml | 52 ++++++++++------ .../values-pt-rPT/music/strings.xml | 52 ++++++++++------ .../values-ro-rRO/music/strings.xml | 52 ++++++++++------ .../values-ru-rRU/music/strings.xml | 52 ++++++++++------ .../values-sk-rSK/music/strings.xml | 52 ++++++++++------ .../values-sl-rSI/music/strings.xml | 52 ++++++++++------ .../values-sr-rCS/music/strings.xml | 52 ++++++++++------ .../values-sr-rSP/music/strings.xml | 52 ++++++++++------ .../values-sv-rSE/music/strings.xml | 52 ++++++++++------ .../values-th-rTH/music/strings.xml | 52 ++++++++++------ .../values-tr-rTR/music/strings.xml | 52 ++++++++++------ .../values-uk-rUA/music/strings.xml | 52 ++++++++++------ .../values-uk-rUA/youtube/strings.xml | 6 +- .../values-vi-rVN/music/strings.xml | 52 ++++++++++------ .../values-zh-rCN/music/strings.xml | 52 ++++++++++------ .../values-zh-rTW/music/strings.xml | 52 ++++++++++------ 47 files changed, 1487 insertions(+), 799 deletions(-) diff --git a/patches/src/main/resources/addresources/values-af-rZA/music/strings.xml b/patches/src/main/resources/addresources/values-af-rZA/music/strings.xml index 8bc127d1c7..4180aa16ef 100644 --- a/patches/src/main/resources/addresources/values-af-rZA/music/strings.xml +++ b/patches/src/main/resources/addresources/values-af-rZA/music/strings.xml @@ -462,7 +462,6 @@ Om te verseker dat scrobbling tydens agtergrondweergawe gesinkroniseer word, ver Vertoon Sinchronisasie Paneel - Tale AI-eindpunt Alle verskaffers gedeaktiveer - ingeboude lirieke en ingeslote lirieke Verskaffervolgorde terugstel? @@ -616,33 +615,50 @@ Lirieke kom van diensverskaffers van derde partye wat nie aan Morphe gelink, deu Lirieke word gesoek volgens kunstenaar en titel, […] Jy kan ook die verfris-knoppie probeer om handmatig van weergawe te wissel. […] Alle verskaffers word nie altyd in die bestelling versoek nie, maar reageer op grond van hul spoed Lirieke-bron? Lirieke-bron? - OpenAI-versoenbare API - AI-vertaling - Kies die vertaling wat die verskaffer stuur, dan jou OpenAI-geskapte model, en Google Translate laaste - KI Romanisering - Kies die romanisering wat die verskaffer stuur, dan jou OpenAI-versoenbare model, en Google Translate laaste + Vertaling + Romanisering + Vertalingsverskaffer + Google (Standaard, Gratis) + OpenAI-versoenbaar (Pasgemaak) + Romanisasie-verskaffer + Google (Standaard) + Plaaslike / Offline Woordeboek + OpenAI-versoenbaar (Pasgemaak) Kunsmatige Intelligensie-afdeling Word slegs gebruik wanneer die verskaffer geen afdelinglabels soos Koors of Vers stuur nie, wat jou OpenAI-versoenbare model dan opspoor - KI-instellings - Stel die OpenAI-kompatibele API-eindpunt in vir AI-aangedrewe liedteksvertaling, romanisering en sektor - API-eindpunt URL (bv. https://text.pollinations.ai/openai) - API-sleutel (los leeg vir anonieme toegang) - Modelnaam (bv. openai-fast, gpt-4o-mini) + KI & Wolke-eindpunte + Konfigureer API-sleutels, modelle, eindpunte en instruksie-sjablone vir KI-verskaffers. + API-eindpunt URL (bv. https://api.openai.com/v1/chat/completions) Aanwysingtemplaat Kiesers: {language}, {task}, {title}, {artist}, {count}, {lines} Aanwysing gestoor Aansporing gestoor, maar die AI-eindpunt is nog nie bruikbaar nie. Stel dit eers onder AI-instellings in Aanwysing teruggestel - Ekstra aanvraagliggaam (JSON, saamgevoeg in elke AI-aanvraag) Nie geldige JSON nie. Reg dit of maak die veld skoon voor saving - KI-instellings gestoor - Konnie nie met die AI-eindpunt verbind nie. Neem asseblief kennis van jou instellings - KI-instellings teruggestel - Model %1$s is gekonfigureer - Basis-URL kan nie leeg wees nie + Vertalings- & Romanisering-instellings gestoor + Instellings is teruggesit na verstek Basis-URL moet met http:// of https:// begin - Modelnaam kan nie leeg wees nie + Toets Konneksie + Konfigureer Eindpunte + Wys Alle Eindpunte Valideer verbinding… + Vertalingsverskaffer + Romaniseringsverskaffer + Haal Modelle op + Haal modelle op… + Kies \'n Model + Kon nie modelle ophaal nie. Gaan API-sleutel of konneksie na. + Gemini API-instellings + OpenAI-versoenbaar (Pasgemaakte Eindpunt) + DeepL API-instellings + API-sleutel + Model + Basis-URL + API-sleutel (opsioneel) + Vertalingsopdrag + Ekstra versoekliggaam (opsionele JSON) + Outentiseringssleutel (Gratis of Pro) + Konfigureer API-sleutels, modelle, eindpunte en opdragte 8.23.51 - Deaktiveer vet ikone 9.13.51 - Herstel Speler Volgende, Lirieke en Verwante oortjies diff --git a/patches/src/main/resources/addresources/values-ar-rSA/music/strings.xml b/patches/src/main/resources/addresources/values-ar-rSA/music/strings.xml index 8870499c39..1d54c36f49 100644 --- a/patches/src/main/resources/addresources/values-ar-rSA/music/strings.xml +++ b/patches/src/main/resources/addresources/values-ar-rSA/music/strings.xml @@ -462,7 +462,6 @@ tظهر المسارات التي تم تنزيلها هنا" العرض المزامنة لوحة - اللغات نقطة نهاية الذكاء الاصطناعي جميع مزودي الخدمة معطّلون - كلمات الأغاني المدمجة وكلمات الأغاني المضمنة إعادة تعيين ترتيب المزوّدين؟ @@ -606,33 +605,50 @@ tظهر المسارات التي تم تنزيلها هنا" إخفاء سطر الإسناد الظاهر أسفل الكلمات. تبديل موضع الترجمة والرومنة عند التشغيل: تكون الرومنة أسفل الكلمات والترجمة أعلاها. عند الإيقاف: تبقى الرومنة أعلى الكلمات والترجمة أسفلها. - واجهة برمجة تطبيقات متوافقة مع OpenAI - الترجمة بالذكاء الاصطناعي - يفضل الترجمة التي يوفرها المزود، ثم نموذجك المتوافق مع OpenAI، وأخيراً ترجمة Google - الرومنة بالذكاء الاصطناعي - يفضل الرومنة التي يوفرها المزود، ثم نموذجك المتوافق مع OpenAI، وأخيرًا ترجمة جوجل + الترجمة + الرومنة + مزوّد الترجمة + جوجل (افتراضي، مجاني) + متوافق مع OpenAI (مخصص) + مزوّد الرومنة + جوجل (افتراضي) + قاموس محلي / بدون اتصال + متوافق مع OpenAI (مخصص) قسم الذكاء الاصطناعي يُستخدم فقط عندما لا يوفر المزود تسميات للأقسام مثل Chorus أو Verse، والتي يقوم نموذجك المتوافق مع OpenAI باكتشافها بعد ذلك - إعدادات الذكاء الاصطناعي - تكوين نقطة نهاية واجهة برمجة التطبيقات المتوافقة مع OpenAI لترجمة النصوص المدعومة بالذكاء الاصطناعي والتعريب والقسم - عنوان URL لنقطة نهاية API (مثال: https://text.pollinations.ai/openai) - مفتاح API (اتركه فارغًا للوصول المجهول) - اسم النموذج (مثال: openai-fast, gpt-4o-mini) + الذكاء الاصطناعي & نقاط نهاية السحابة + تكوين مفاتيح API، والنماذج، ونقاط النهاية، وقوالب المطالبات لمزوّدي الذكاء الاصطناعي. + عنوان URL لنقطة نهاية API (على سبيل المثال https://api.openai.com/v1/chat/completions) قالب المطالبة العناصر النائبة: {language}, {task}, {title}, {artist}, {count}, {lines} تم حفظ المطالبة تم حفظ الموجه، لكن نقطة نهاية الذكاء الاصطناعي غير صالحة للاستخدام بعد. قم بتكوينها تحت إعدادات الذكاء الاصطناعي أولاً تم إعادة تعيين المطالبة - جسم طلب إضافي (JSON، مدمج في كل طلب ذكاء اصطناعي) ليس JSON صالحاً. أصلحه أو امسح الحقل قبل الحفظ - تم حفظ إعدادات الذكاء الاصطناعي - تعذر الاتصال بنقطة نهاية الذكاء الاصطناعي. يرجى التحقق من إعداداتك - تم إعادة تعيين إعدادات الذكاء الاصطناعي - تم تكوين النموذج %1$s - لا يمكن أن يكون عنوان URL الأساسي فارغًا + تم حفظ إعدادات الترجمة وRomanization + إعادة تعيين الإعدادات إلى الوضع الافتراضي يجب أن يبدأ عنوان URL الأساسي بـ http:// أو https:// - لا يمكن أن يكون اسم النموذج فارغًا + اختبار الاتصال + تكوين نقاط النهاية + إظهار جميع نقاط النهاية جارٍ التحقق من الاتصال… + مزوّد الترجمة + مزوّد الرومنة + جلب النماذج + جاري جلب النماذج... + اختر نموذجاً + فشل جلب النماذج. تحقق من مفتاح API أو الاتصال. + إعدادات Gemini API + متوافق مع OpenAI (نقطة نهاية مخصصة) + إعدادات DeepL API + مفتاح API + النموذج + عنوان URL الأساسي + مفتاح API (اختياري) + تلقين الترجمة + جسم طلب إضافي (JSON اختياري) + مفتاح المصادقة (مجاني أو احترافي) + تكوين مفاتيح API، والنماذج، ونقاط النهاية، والتلقينات 8.23.51 - تعطيل الأيقونات العريضة 9.13.51 - استعادة علامات تبويب المشغل: التالي وكلمات الأغاني والمرتبطة diff --git a/patches/src/main/resources/addresources/values-az-rAZ/music/strings.xml b/patches/src/main/resources/addresources/values-az-rAZ/music/strings.xml index ad57b9a321..23760479fd 100644 --- a/patches/src/main/resources/addresources/values-az-rAZ/music/strings.xml +++ b/patches/src/main/resources/addresources/values-az-rAZ/music/strings.xml @@ -462,7 +462,6 @@ Fon səsləndirmə zamanı analiz sinxronlaşdırılmasına əmin olmaq üçün Görüntü Sinxronizasiya Panel - Dillər AI son nöqtəsi Bütün təminatçılar qapadılıb - daxili mahnı sözləri və yerləşdirilən mahnı sözləri Təminatçı sırası sıfırlansın? @@ -619,33 +618,50 @@ Fon səsləndirmə zamanı analiz sinxronlaşdırılmasına əmin olmaq üçün Mahnı sözlərinin altında göstərilən müəllif sətrini gizlət Tərcümə və transkripsiyanın yerini dəyiş AÇIQ olduqda transkripsiya sözlərin altında və tərcümə yuxarıda göstərilir; QAPALI olduqda transkripsiya üstdə, tərcümə isə altda qalır - OpenAI uyğun API-si - Süni İntellekt Tərcüməsi - Təminatçının öz tərcüməsinə, sonra sizin OpenAI uyğun modelinizə, sonda isə Google Tərcüməyə üstünlük verir - Sİ Transkripsiyası - Təminatçının öz transkripsiyasına, daha sonra sizin OpenAI uyğun modelinizə, sonda isə Google Tərcüməyə üstünlük verir + Tərcümə + Transkripsiya + Tərcümə təminatçısı + Google (İlkin, Pulsuz) + OpenAI Uyğunluğu (Fərdi) + Transkripsiya təminatçısı + Google (İlkin) + Yerli / Oflayn Lüğət + OpenAI Uyğunluğu (Fərdi) Süni İntellekt Bölməsi Yalnız təminatçının Chorus və ya Verse kimi bölmə etiketləri olmadıqda istifadə olunur, bundan sonra OpenAI uyğun modeliniz onları aşkarlayır - Süni İntellekt Tənzimləmələri - OpenAI uyğun API uc nöqtəsini AI vasitəsilə mahnı sözlərinin tərcüməsi, transkripsiyası və bölmələr üçün tənzimlə - API son nöqtəsi URL-si (məsələn, https://text.pollinations.ai/openai) - API açarı (gizli giriş üçün boş qoy) - Model adı (məsələn, openai-fast, gpt-4o-mini) + Süni İntellekt & Bulud Son Nöqtələri + Süni intellekt təminatçıları üçün API açarlarını, modelləri, son nöqtələri və sorğu şablonlarını tənzimlə. + API son nöqtəsi URL-si (məsələn, https://api.openai.com/v1/chat/completions) Sorğu Şablonu Dəyişənlər: {language}, {task}, {title}, {artist}, {count}, {lines} Sorğu saxlanıldı Sorğu saxlanıldı, lakin AI son nöqtəsi hələ istifadəyə yararlı deyil. Əvvəlcə onu AI Tənzimləmələri aşağısında tənzimlə Sorğu sıfırlandı - ​Əlavə sorğu qrupu (JSON, hər AI sorğusuna birləşdirilir) Etibarsız JSON. Saxlamazdan əvvəl onu düzəlt və ya xananı təmizlə - Süni intellekt tənzimləmələri saxlanıldı - Süni intellekt son nöqtəsinə qoşulmaq olmadı. Zəhmət olmasa, tənzimləmələrinizi yoxlayın - Süni intellekt tənzimləmələri sıfırlandı - %1$s modeli tənzimlənib - Əsas URL boş ola bilməz + Tərcümə & Transkripsiya tənzimləmələri saxlanıldı + Tənzimləmələr ilkin vəziyyətə qaytarıldı Əsas URL http:// və ya https:// ilə başlamalıdır - Model adı boş ola bilməz + Bağlantını Yoxla + Son Nöqtələri Tənzimlə + Bütün Son Nöqtələri Göstər Bağlantı təsdiqlənir… + Tərcümə Təminatçısı + Transkripsiya Təminatçısı + Modelləri Əldə Et + Modelləri əldə edir… + Model Seç + Modelləri əldə etmək olmadı. API açarını və ya bağlantını yoxla. + Gemini API Tənzimləmələri + OpenAI Uyğunluğu (Fərdi Son Nöqtəsi) + DeepL API Tənzimləmələri + API açarı + Model + Əsas URL + API açarı (istəyə bağlı) + Tərcümə sorğusu + Əlavə sorğu gövdəsi (istəyə bağlı JSON) + Təsdiqləmə açarı (Pulsuz və ya Pro) + API açarlarını, modellərini, son nöqtələrini və sorğularını tənzimlə 8.23.51 - Qalın simvolları qeyri-aktiv et 9.13.51 - Oynadıcının Növbəti, Mahnı sözləri və Əlaqəli panellərini bərpa et diff --git a/patches/src/main/resources/addresources/values-bg-rBG/music/strings.xml b/patches/src/main/resources/addresources/values-bg-rBG/music/strings.xml index 57161860ad..30d03f2b2c 100644 --- a/patches/src/main/resources/addresources/values-bg-rBG/music/strings.xml +++ b/patches/src/main/resources/addresources/values-bg-rBG/music/strings.xml @@ -462,7 +462,6 @@ SABR възпроизвеждане (имитирането на видео по Показване Синхронизация Панел - Езици AI крайна точка Всички доставчици са деактивирани - вградени текстовки и вградени текстове на песни Да се нулира редът на доставчиците? @@ -607,33 +606,50 @@ SABR възпроизвеждане (имитирането на видео по Скриване на реда с кредитна информация, показан под текста. Размяна на превода и романизацията ВКЛ. поставя романизацията под текста и превода над него; ИЗКЛ. оставя романизацията над текста и превода под него. - API, съвместим с OpenAI - AI превод - Предпочита превода, предоставен от доставчика, след това вашия OpenAI-съвместим модел и накрая Google Translate - AI रोमनзиция - Предпочита романизацията, предоставена от доставчика, след това вашия OpenAI-съвместим модел и накрая Google Translate + Превод + Романизация + Доставчик на преводи + Google (по подразбиране, безплатно) + Съвместимо с OpenAI (персонализирано) + Доставчик на романизация + Google (по подразбиране) + Локален / офлайн речник + Съвместимо с OpenAI (персонализирано) AI секция Използва се само когато доставчикът не предоставя етикети за секции като Припев или Куплет, които след това се откриват от вашия OpenAI-съвместим модел - AI настройки - Конфигурирайте API края на OpenAI-съвместимия интерфейс за AI-задвижван превод, транслитерация и секциониране на лирика - URL на крайната точка на API (напр. https://text.pollinations.ai/openai) - API ключ (оставете празно за анонимен достъп) - Име на модела (напр. openai-fast, gpt-4o-mini) + AI & облачни крайни точки + Конфигурирайте API ключове, модели, крайни точки и шаблони за подкани за AI доставчици. + URL на API крайната точка (напр. https://api.openai.com/v1/chat/completions) Шаблон за подкана Запълнители: {language}, {task}, {title}, {artist}, {count}, {lines} Подканата е запазена Подканването е запазено, но AI крайният接口 все още не е годен. Конфигурирайте го първо в Настройки на AI Подканата е нулирана - Допълнително тяло на заявката (JSON, сливащо се с всяка AI заявка) Невалиден JSON. Поправете го или изчистете полето преди запазване - AI настройките са запазени - Не може да се установи връзка с AI крайната точка. Моля проверете настройките си - AI настройките са нулирани - Модел %1$s е конфигуриран - Базовият URL не може да бъде празен + Настройките за превод & романизация са запазени + Настройките са възстановени по подразбиране Базовият URL трябва да започва с http:// или https:// - Името на модела не може да бъде празно + Тест на връзката + Конфигуриране на крайни точки + Покажи всички крайни точки Проверка на връзката… + Доставчик на превод + Доставчик на романизация + Извличане на модели + Извличане на модели… + Изберете модел + Неуспешно извличане на моделите. Проверете API ключа или връзката. + Настройки на Gemini API + Съвместимо с OpenAI (собствен краен пункт) + Настройки на DeepL API + API ключ + Модел + Базов URL + API ключ (по желание) + Подкана за превод + Допълнително тяло на заявката (опционален JSON) + Ключ за автентификация (Free или Pro) + Конфигуриране на API ключове, модели, крайни точки и подкани 8.23.51 - Деактивиране на удебелени икони 9.13.51 - Възстановяване на разделите Up next, Lyrics и Related diff --git a/patches/src/main/resources/addresources/values-bn-rBD/music/strings.xml b/patches/src/main/resources/addresources/values-bn-rBD/music/strings.xml index a2cec39f55..5f12d93abd 100644 --- a/patches/src/main/resources/addresources/values-bn-rBD/music/strings.xml +++ b/patches/src/main/resources/addresources/values-bn-rBD/music/strings.xml @@ -462,7 +462,6 @@ SABR প্লেব্যাক (ভিডিও স্ট্রিম স্ প্রদর্শন সিঙ্ক্রোনাইজেশন প্যানেল - ভাষা এআই এন্ডপয়েন্ট সকল প্রদানকারী নিষ্ক্রিয় - বিল্ট-ইন লিরিক্স এবং এমবেডেড লিরিক্স প্রদানকারীর ক্রম পুনরায় সেট করবেন? @@ -616,33 +615,50 @@ SABR প্লেব্যাক (ভিডিও স্ট্রিম স্ ON রোমানাইজেশনকে লিরিক্সের নিচে এবং অনুবাদকে উপরে রাখে; OFF রোমানাইজেশনকে উপরে এবং অনুবাদকে নিচে রাখে অনুবাদ এবং রোমানাইজেশন অদলবদল করুন চালু অবস্থায় রোমানাইজেশন গানের কথার নীচে এবং অনুবাদ উপরে থাকে; বন্ধ অবস্থায় রোমানাইজেশন উপরে এবং অনুবাদ নীচে থাকে - OpenAI-সামঞ্জস্যপূর্ণ API - AI অনুবাদ - প্রথমে প্রোভাইডারের দেওয়া অনুবাদ, তারপর আপনার OpenAI-সামঞ্জস্যপূর্ণ মডেল এবং সবশেষে গুগল ট্রান্সলেট পছন্দ করে - AI রোমানাইজেশন - প্রথমে প্রোভাইডারের দেওয়া রোমানাইজেশন, তারপর আপনার OpenAI-সামঞ্জস্যপূর্ণ মডেল এবং সবশেষে Google Translate ব্যবহার করবে + অনুবাদ + রোমানাইজেশন + অনুবাদ প্রদানকারী + Google (ডিফল্ট, বিনামূল্যে) + OpenAI সামঞ্জস্যপূর্ণ (কাস্টম) + রোমানাইজেশন প্রদানকারী + Google (ডিফল্ট) + লোকাল / অফলাইন ডিকশনারি + OpenAI সামঞ্জস্যপূর্ণ (কাস্টম) AI সেকশন শুধুমাত্র তখনই ব্যবহৃত হয় যখন প্রোভাইডার Chorus বা Verse-এর মতো কোনো সেকশন লেবেল প্রদান করে না, যা আপনার OpenAI-সামঞ্জস্যপূর্ণ মডেল তখন শনাক্ত করে - AI সেটিংস - AI-চালিত লিরিক্স অনুবাদ, রোমানাইজেশন এবং বিভাগের জন্য ওপেনএআই-কম্প্যাটিবল API এন্ডপয়েন্ট কনফিগার করুন - API এন্ডপয়েন্ট URL (যেমন: https://text.pollinations.ai/openai) - API কী (বেনামী অ্যাক্সেসের জন্য খালি রাখুন) - মডেলের নাম (যেমন: openai-fast, gpt-4o-mini) + AI এবং ক্লাউড এন্ডপয়েন্ট + AI প্রদানকারীদের জন্য API কী, মডেল, এন্ডপয়েন্ট এবং প্রম্পট টেমপ্লেট কনফিগার করুন। + API এন্ডপয়েন্ট URL (যেমন: https://api.openai.com/v1/chat/completions) প্রম্পট টেমপ্লেট প্লেসহোল্ডার: {language}, {task}, {title}, {artist}, {count}, {lines} প্রম্পট সংরক্ষিত হয়েছে প্রম্পট সংরক্ষিত হয়েছে, কিন্তু AI এন্ডপয়েন্ট এখনও ব্যবহারযোগ্য নয়। প্রথমে এটি AI সেটিংস-এ কনফিগার করুন প্রম্পট রিসেট করা হয়েছে - এক্সট্রা রিকোয়েস্ট বডি (JSON, প্রতিটি এইআই রিকোয়েস্টে মার্জ করা হয়) বৈধ JSON নয়। সেভ করার আগে এটি ঠিক করুন বা ক্ষেত্রটি মুছে ফেলুন - AI সেটিংস সংরক্ষিত হয়েছে - AI endpoint-এ সংযোগ স্থাপন করা যায়নি। দয়া করে আপনার সেটিংস যাচাই করুন - AI সেটিংস রিসেট করা হয়েছে - মডেল %1$s কনফিগার করা হয়েছে - বেস URL খালি হতে পারবে না + অনুবাদ & রোমানাইজেশন সেটিংস সংরক্ষিত হয়েছে + সেটিংস ডিফল্টে রিসেট করা হয়েছে বেস URL http:// বা https:// দিয়ে শুরু হতে হবে - মডেলের নাম খালি হতে পারবে না + কানেকশন পরীক্ষা করুন + এন্ডপয়েন্ট কনফিগার করুন + সব এন্ডপয়েন্ট দেখুন সংযোগ যাচাই করা হচ্ছে… + অনুবাদ প্রদানকারী + রোমানাইজেশন প্রদানকারী + মডেলগুলি ফেচ করুন + মডেলগুলি ফেচ করা হচ্ছে… + একটি মডেল নির্বাচন করুন + মডেলগুলি ফেচ করতে ব্যর্থ হয়েছে। API কী বা সংযোগ পরীক্ষা করুন। + Gemini API সেটিংস + OpenAI সামঞ্জস্যপূর্ণ (কাস্টম এন্ডপয়েন্ট) + DeepL API সেটিংস + API কি + মডেল + বেস URL + API কি (ঐচ্ছিক) + অনুবাদ প্রম্পট + অতিরিক্ত রিকোয়েস্ট বডি (ঐচ্ছিক JSON) + অথেন্টিকেশন কি (ফ্রি বা প্রো) + API কি, মডেল, এন্ডপয়েন্ট এবং প্রম্পট কনফিগার করুন 8.23.51 - বোল্ড আইকন নিষ্ক্রিয় করুন 9.13.51 - প্লেয়ার আপ নেক্সট, লিরিক্স এবং সম্পর্কিত ট্যাব পুনরুদ্ধার করুন diff --git a/patches/src/main/resources/addresources/values-cs-rCZ/music/strings.xml b/patches/src/main/resources/addresources/values-cs-rCZ/music/strings.xml index a82fbdbc58..268fb05ac8 100644 --- a/patches/src/main/resources/addresources/values-cs-rCZ/music/strings.xml +++ b/patches/src/main/resources/addresources/values-cs-rCZ/music/strings.xml @@ -462,7 +462,6 @@ Pro zajištění synchronizace scrobblingu během přehrávání na pozadí se u Zobrazení Synchronizace Panel - Jazyky Koncový bod AI Všichni poskytovatelé vypnuti - vestavěné texty a vložené texty Obnovit výchozí pořadí poskytovatelů? @@ -616,33 +615,50 @@ Texty pocházejí ze služeb třetích stran, které nejsou přidruženy k Morph Skrýt řádek s uvedením zdroje zobrazený pod textem písně. Prohodit překlad a romanizaci Zapnuto umístí romanizaci pod texty a překlad nad texty; Vypnuto ponechá romanizaci nad texty a překlad pod texty. - API kompatibilní s OpenAI - AI překlad - Upřednostňuje překlad poskytnutý poskytovatelem, poté váš model kompatibilní s OpenAI a nakonec Google Translate - AI romanizace - Upřednostňuje romanizaci poskytnutou poskytovatelem, poté váš model kompatibilní s OpenAI a nakonec Google Translate + Překlad + Romanizace + Poskytovatel překladu + Google (výchozí, zdarma) + Kompatibilní s OpenAI (vlastní) + Poskytovatel romanizace + Google (výchozí) + Lokální / offline slovník + Kompatibilní s OpenAI (vlastní) Sekce AI Používá se pouze v případě, že poskytovatel neposkytuje žádné popisky sekcí, jako je Refrén nebo Sloka, které poté detekuje váš model kompatibilní s OpenAI - Nastavení AI - Nastavit koncový bod API kompatibilního s OpenAI pro AI-powered překlad textů písní, zromanizaci a sekci - URL koncového bodu API (např. https://text.pollinations.ai/openai) - API klíč (nechte prázdné pro anonymní přístup) - Název modelu (např. openai-fast, gpt-4o-mini) + AI & cloudové koncové body + Konfigurujte API klíče, modely, koncové body a šablony promptů pro poskytovatele AI. + URL koncového bodu API (např. https://api.openai.com/v1/chat/completions) Šablona promptu Zástupné znaky: {language}, {task}, {title}, {artist}, {count}, {lines} Prompt uložen Výzva byla uložena, ale koncový bod AI zatím není použitelný. Nejprve jej nakonfigurujte pod Nastavení AI Prompt resetován - Doplňkové tělo žádosti (JSON, sloučeno do každé žádosti AI) Neplatné JSON. Opravte to nebo vymažte pole před uložením - Nastavení AI uloženo - Nepodařilo se připojit ke koncovému bodu AI. Zkontrolujte prosím svá nastavení - Nastavení AI resetováno - Model %1$s byl konfigurován - Základní URL nemůže být prázdná + Nastavení překladu & romanizace uloženo + Nastavení obnoveno do výchozího stavu Základní URL musí začínat http:// nebo https:// - Název modelu nemůže být prázdný + Testovat připojení + Konfigurovat koncové body + Zobrazit všechny koncové body Ověřování připojení… + Poskytovatel překladu + Poskytovatel romanizace + Načíst modely + Načítání modelů… + Vyberte model + Nepodařilo se načíst modely. Zkontrolujte API klíč nebo připojení. + Nastavení Gemini API + Kompatibilní s OpenAI (vlastní koncové bod) + Nastavení DeepL API + API klíč + Model + Základní URL + API klíč (volitelně) + Prompt pro překlad + Extra tělo požadavku (volitelné JSON) + Autentizační klíč (Free nebo Pro) + Konfigurace API klíčů, modelů, koncových bodů a promptů 8.23.51 - Zakázat tučné ikony 9.13.51 - Obnovit karty Další v pořadí, Texty písní a Související diff --git a/patches/src/main/resources/addresources/values-da-rDK/music/strings.xml b/patches/src/main/resources/addresources/values-da-rDK/music/strings.xml index bbf3055c77..35ba681b34 100644 --- a/patches/src/main/resources/addresources/values-da-rDK/music/strings.xml +++ b/patches/src/main/resources/addresources/values-da-rDK/music/strings.xml @@ -461,7 +461,6 @@ For at sikre, at scrobbling synkroniseres under afspilning i baggrunden, skal du Vis Synkronisering Panel - Sprog AI-endepunkt Alle udbydere deaktiveret - indbygget tekst og indlejret tekst Nulstil udbyderrækkefølgen? @@ -615,33 +614,50 @@ Sangtekster kommer fra tredjepartstjenester, der ikke er tilknyttet, sponsoreret Skjul kreditlinjen, der vises under teksterne Byt om på oversættelse og romanisering TIL sætter romanisering under teksterne og oversættelse over; FRA beholder romanisering over og oversættelse under. - OpenAI-kompatibelt API - AI-oversættelse - Foretrækker den oversættelse, som udbyderen leverer, derefter din OpenAI-kompatible model og til sidst Google Translate - AI-romanisering - Foretrækker den romanisering, som udbyderen leverer, derefter din OpenAI-kompatible model og til sidst Google Translate + Oversættelse + Romanisering + Oversættelsesudbyder + Google (Standard, Gratis) + OpenAI-kompatibel (Brugerdefineret) + Udbyder af romanisering + Google (Standard) + Lokal / Offline ordbog + OpenAI-kompatibel (Brugerdefineret) AI-sektion Bruges kun, når udbyderen ikke leverer sektionsmærkater som Chorus eller Verse, som din OpenAI-kompatible model derefter detekterer - AI-indstillinger - Konfigurer det OpenAI-kompatible API-endpoint for AI-drevet sangtekstoversættelse, romanisering og sektion - API-slutpunkt-URL (f.eks. https://text.pollinations.ai/openai) - API-nøgle (lad den være tom for anonym adgang) - Modelnavn (f.eks. openai-fast, gpt-4o-mini) + AI & Cloud-slutpunkter + Konfigurer API-nøgler, modeller, slutpunkter og prompt-skabeloner for AI-udbydere. + API-slutpunkt-URL (f.eks. https://api.openai.com/v1/chat/completions) Prompt-skabelon Pladsholdere: {language}, {task}, {title}, {artist}, {count}, {lines} Prompt gemt Prompt gemt, men AI-endpointet er endnu ikke brugbart. Konfigurer det under AI-indstillinger først Prompt nulstillet - Ekstra requestbody (JSON, indlejret i hver AI-anmodning) Ikke gyldig JSON. Ret det eller ryd feltet før gemming - AI-indstillinger gemt - Kunne ikke oprette forbindelse til AI-endepunktet. Tjek venligst dine indstillinger - AI-indstillinger nulstillet - Model %1$s konfigureret - Base-URL må ikke være tom + Indstillinger for oversættelse & romanisering er gemt + Indstillinger nulstillet til standard Base-URL skal starte med http:// eller https:// - Modelnavn må ikke være tomt + Test forbindelse + Konfigurer endpoints + Vis alle endpoints Validerer forbindelse… + Oversættelsesudbyder + Romaniseringsudbyder + Hent modeller + Henter modeller… + Vælg en model + Kunne ikke hente modeller. Kontroller API-nøgle eller forbindelse. + Gemini API-indstillinger + OpenAI-kompatibel (brugerdefineret endpoint) + DeepL API-indstillinger + API-nøgle + Model + Base-URL + API-nøgle (valgfri) + Oversættelsesprompt + Ekstra anmodningskrop (valgfri JSON) + Autentificeringsnøgle (Free eller Pro) + Konfigurer API-nøgler, modeller, endpoints og prompts 8.23.51 - Deaktiver fede ikoner 9.13.51 - Gendan fanerne Næste, Tekster og Relaterede i Player diff --git a/patches/src/main/resources/addresources/values-de-rDE/music/strings.xml b/patches/src/main/resources/addresources/values-de-rDE/music/strings.xml index 23a0398acb..957defa4e4 100644 --- a/patches/src/main/resources/addresources/values-de-rDE/music/strings.xml +++ b/patches/src/main/resources/addresources/values-de-rDE/music/strings.xml @@ -462,7 +462,6 @@ Um sicherzustellen, dass Scrobbling während der Wiedergabe im Hintergrund synch Anzeige Synchronisation Leiste - Sprachen KI-Endpunkt Alle Anbieter deaktiviert - integrierte und eingebettete Liedtexte Anbieterreihenfolge zurücksetzen? @@ -614,33 +613,50 @@ Erforderliche Cookies: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PA Blende die unter den Songtexten angezeigte Creditszeile aus. Übersetzung und Romanisierung vertauschen. EIN setzt die Romanisierung unter die Songtexte und die Übersetzung darüber; AUS lässt die Romanisierung darüber und die Übersetzung darunter. - OpenAI-kompatible API - KI-Übersetzung - Bevorzugt die Übersetzung des Anbieters, dann Ihr OpenAI-kompatibles Modell und zuletzt Google Translate - KI-Romanisierung - Bevorzugt die vom Anbieter bereitgestellte Romanisierung, dann Ihr OpenAI-kompatibles Modell und zuletzt Google Translate + Übersetzung + Romanisierung + Übersetzungsanbieter + Google (Standard, Kostenlos) + OpenAI-kompatibel (Benutzerdefiniert) + Romanisierungs-Anbieter + Google (Standard) + Lokales / Offline-Wörterbuch + OpenAI-kompatibel (Benutzerdefiniert) KI-Abschnitt Wird nur verwendet, wenn der Anbieter keine Abschnittsbezeichnungen wie Refrain oder Strophe liefert, die dann von Ihrem OpenAI-kompatiblen Modell erkannt werden - KI-Einstellungen - Konfigurieren Sie den OpenAI-kompatiblen API-Endpunkt für KI-gestützte Lyrics-Übersetzung, Romanisierung und Sektion - API-Endpunkt-URL (z. B. https://text.pollinations.ai/openai) - API-Schlüssel (für anonymen Zugriff leer lassen) - Modellname (z. B. openai-fast, gpt-4o-mini) + KI & Cloud-Endpunkte + Konfigurieren Sie API-Schlüssel, Modelle, Endpunkte und Prompt-Vorlagen für KI-Anbieter. + API-Endpunkt-URL (z. B. https://api.openai.com/v1/chat/completions) Prompt-Vorlage Platzhalter: {language}, {task}, {title}, {artist}, {count}, {lines} Prompt gespeichert Prompt gespeichert, aber der AI-Endpunkt ist noch nicht nutzbar. Konfigurieren Sie ihn zuerst unter AI-Einstellungen Prompt zurückgesetzt - Zusätzlicher Anforderungskörper (JSON, wird in jede AI-Anfrage zusammengeführt) Ungültiges JSON. Korrigieren Sie es oder leeren Sie das Feld vor dem Speichern - KI-Einstellungen gespeichert - Keine Verbindung zum AI-Endpunkt möglich. Bitte überprüfe deine Einstellungen - KI-Einstellungen zurückgesetzt - Modell %1$s konfiguriert - Basis-URL darf nicht leer sein + Übersetzungs- & Romanisierungseinstellungen gespeichert + Einstellungen auf Standardwerte zurückgesetzt Basis-URL muss mit http:// oder https:// beginnen - Modellname darf nicht leer sein + Verbindung testen + Endpunkte konfigurieren + Alle Endpunkte anzeigen Verbindung wird überprüft… + Übersetzungsanbieter + Romanisierungsanbieter + Modelle abrufen + Modelle werden abgerufen… + Modell auswählen + Modelle konnten nicht abgerufen werden. Überprüfen Sie den API-Schlüssel oder die Verbindung. + Gemini API-Einstellungen + OpenAI-kompatibel (benutzerdefinierter Endpunkt) + DeepL API-Einstellungen + API-Schlüssel + Modell + Basis-URL + API-Schlüssel (optional) + Übersetzungsprompt + Zusätzlicher Request-Body (optionales JSON) + Authentifizierungsschlüssel (Free oder Pro) + API-Schlüssel, Modelle, Endpunkte und Prompts konfigurieren 8.23.51 - Fette Symbole deaktivieren 9.13.51 - Player-Tabs „Up next“, „Lyrics“ und „Verwandte“ wiederherstellen diff --git a/patches/src/main/resources/addresources/values-el-rGR/music/strings.xml b/patches/src/main/resources/addresources/values-el-rGR/music/strings.xml index 1a2d766541..ea598eef3e 100644 --- a/patches/src/main/resources/addresources/values-el-rGR/music/strings.xml +++ b/patches/src/main/resources/addresources/values-el-rGR/music/strings.xml @@ -462,7 +462,6 @@ Second \"item\" text" Εμφάνιση Συγχρονισμός Πίνακας - Γλώσσες Τερματικό σημείο AI Όλοι οι πάροχοι απενεργοποιήθηκαν - ενσωματωμένες και εντοιχισμένες στίχοι Επαναφορά σειράς παρόχων; @@ -616,33 +615,50 @@ Second \"item\" text" Απόκρυψη της γραμμής πιστώσεων που εμφανίζεται κάτω από τους στίχους. Εναλλαγή μετάφρασης και μεταγραφής. ON: τοποθετεί τη μεταγραφή κάτω από τους στίχους και τη μετάφραση πάνω· OFF: διατηρεί τη μεταγραφή πάνω και τη μετάφραση κάτω. - API συμβατό με OpenAI - Μετάφραση με AI - Προτιμά τη μετάφραση που παρέχει ο πάροχος, στη συνέχεια το συμβατό μοντέλο OpenAI σας και τέλος το Google Translate - Ρομαιοποίηση AI - Προτιμά τη λατινική μεταγραφή που παρέχει ο πάροχος, στη συνέχεια το μοντέλο συμβατό με το OpenAI και τελευταία τη Μετάφραση της Google + Μετάφραση + Ρωμαϊκή μεταγραφή + Πάροχος μετάφρασης + Google (Προεπιλεγμένο, Δωρεάν) + Συμβατό με OpenAI (Προlæτομιζόμενο) + Πάροχος ρωμανισμού + Google (Προεπιλεγμένο) + Τοπικό / Εκτός σύνδεσης Λεξικό + Συμβατό με OpenAI (Προlæτομιζόμενο) Ενότητα AI Χρησιμοποιείται μόνο όταν ο πάροχος δεν παρέχει ετικέτες ενοτήτων, όπως Ρεφρέν ή Στροφή, τις οποίες στη συνέχεια ανιχνεύει το μοντέλο συμβατό με το OpenAI - Ρυθμίσεις AI - Ρυθμίστε το τελικό σημείο της API συμβατής με το OpenAI για μετάφραση στίχων, αποδοχή σε λατινικούς χαρακτήρες και τμηματοποίηση με τεχνητή νοημοσύνη - URL τελικού σημείου API (π.χ. https://text.pollinations.ai/openai) - Κλειδί API (αφήστε κενό για ανώνυμη πρόσβαση) - Όνομα μοντέλου (π.χ. openai-fast, gpt-4o-mini) + AI & Cloud Endpoints + Διαμόρφωση κλειδιών API, μοντέλων, endpoints και προτύπων προτροπών για παρόχους AI. + URL του API endpoint (π.χ. https://api.openai.com/v1/chat/completions) Πρότυπο Prompt Υποκαταστάτες: {language}, {task}, {title}, {artist}, {count}, {lines} Το prompt αποθηκεύτηκε Η εκκίνηση αποθηκεύτηκε, αλλά το τελικό σημείο AI δεν είναι ακόμα διαθέσιμο. Πριν το χρησιμοποιήσετε, ρυθμίστε το στις Ρυθμίσεις AI Το prompt επαναφέρθηκε - Επιπλέον σώμα αιτήματος (JSON, συγχωνευμένο σε κάθε αίτημα AI) Μη έγκυρο JSON. Διορθώστε το ή καθαρίστε το πεδίο πριν από την αποθήκευση - Οι ρυθμίσεις AI αποθηκεύτηκαν - Αδυναμία σύνδεσης στο σημείο τελείας AI. Ελέγξτε τις ρυθμίσεις σας - Οι ρυθμίσεις AI επαναφέρθηκαν - Διαμορφώθηκε το μοντέλο %1$s - Το βασικό URL δεν μπορεί να είναι κενό + Αποθηκεύτηκαν οι ρυθμίσεις Μετάφρασης & Ρωμαΐзации + Οι ρυθμίσεις επαναφέρθηκαν στις προεπιλεγμένες Το βασικό URL πρέπει να ξεκινά με http:// ή https:// - Το όνομα μοντέλου δεν μπορεί να είναι κενό + Δοκιμή Σύνδεσης + Διαμόρφωση Τελικών Σημείων + Εμφάνιση Όλων των Τελικών Σημείων Έλεγχος σύνδεσης… + Πάροχος Μετάφρασης + Πάροχος Ρωμαΐзации + Ανάκτηση Μοντέλων + Ανάκτηση μοντέλων… + Επίλεξε ένα Μοντέλο + Αποτυχία ανάκτησης μοντέλων. Έλεγξε το API key ή τη σύνδεση. + Ρυθμίσεις Gemini API + Συμβατό με OpenAI (Προσαρμοσμένο Endpoint) + Ρυθμίσεις DeepL API + Κλειδί API + Μοντέλο + Βασική διεύθυνση URL + Κλειδί API (προαιρετικό) + Προτροπή μετάφρασης + Πρόσθετο σώμα αιτήματος (προαιρετικό JSON) + Κλειδί αυθεντικοποίησης (Free ή Pro) + Διαμόρφωση κλειδιών API, μοντέλων, endpoints και προτροπών 8.23.51 - Απενεργοποίηση έντονων εικονιδίων 9.13.51 - Επαναφορά καρτελών Up next, Στίχοι και Σχετικά του Player diff --git a/patches/src/main/resources/addresources/values-es-rES/music/strings.xml b/patches/src/main/resources/addresources/values-es-rES/music/strings.xml index 6a5d41bbd9..fa03ac5902 100644 --- a/patches/src/main/resources/addresources/values-es-rES/music/strings.xml +++ b/patches/src/main/resources/addresources/values-es-rES/music/strings.xml @@ -462,7 +462,6 @@ Para garantizar que la sincronización se realice durante la reproducción en se Pantalla Sincronización Panel - Idiomas Endpoint de IA Todos los proveedores desactivados: letras integradas y letras incrustadas ¿Restablecer orden de proveedores? @@ -621,33 +620,50 @@ Las letras provienen de servicios de terceros que no están afiliados, patrocina Oculta la línea de créditos que se muestra debajo de las letras Intercambiar traducción y romanización Activado coloca la romanización debajo de las letras y la traducción arriba; desactivado mantiene la romanización arriba y la traducción abajo - API compatible con OpenAI - Traducción con IA - Prefiere la traducción que proporciona el proveedor, luego tu modelo compatible con OpenAI y, por último, Google Translate - Romanización con IA - Prefiere la romanización que proporciona el proveedor, luego tu modelo compatible con OpenAI y, por último, Google Translate + Traducción + Romanización + Proveedor de traducción + Google (predeterminado, gratuito) + Compatible con OpenAI (personalizado) + Proveedor de romanización + Google (predeterminado) + Diccionario local / sin conexión + Compatible con OpenAI (personalizado) Sección con IA Solo se utiliza cuando el proveedor no envía etiquetas de sección, como «Estribillo» o «Verso», que tu modelo compatible con OpenAI detecta a continuación - Ajustes de IA - Configura el punto final de la API compatible con OpenAI para la traducción de letras, la romanización y las secciones mediante IA - URL del punto de conexión de la API (p. ej., https://text.pollinations.ai/openai) - Clave API (dejar en blanco para acceso anónimo) - Nombre del modelo (p. ej., openai-fast, gpt-4o-mini) + Endpoints de IA y nube + Configure claves API, modelos, endpoints y plantillas de prompts para proveedores de IA. + URL del endpoint de la API (p. ej., https://api.openai.com/v1/chat/completions) Plantilla de Prompt Marcadores de posición: {language}, {task}, {title}, {artist}, {count}, {lines} Prompt guardado Aviso guardado, pero el punto final de IA aún no es utilizable. Configúralo primero en Ajustes de IA Prompt restablecido - Cuerpo de solicitud adicional (JSON, fusionado en cada solicitud de IA) No es JSON válido. Corígelo o limpia el campo antes de guardar - Ajustes de IA guardados - No se pudo conectar con el punto de conexión de la IA. Comprueba tus ajustes - Ajustes de IA restablecidos - Modelo %1$s configurado - La URL base no puede estar vacía + Ajustes de traducción y romanización guardados + Ajustes restablecidos a predeterminados La URL base debe empezar por http:// o https:// - El nombre del modelo no puede estar vacío + Probar conexión + Configurar endpoints + Mostrar todos los endpoints Validando la conexión… + Proveedor de traducción + Proveedor de romanización + Obtener modelos + Obteniendo modelos... + Seleccionar un modelo + Error al obtener los modelos. Comprueba la clave API o la conexión. + Ajustes de la API de Gemini + Compatible con OpenAI (endpoint personalizado) + Ajustes de la API de DeepL + Clave de API + Modelo + URL base + Clave de API (opcional) + Prompt de traducción + Cuerpo de solicitud adicional (JSON opcional) + Clave de autenticación (Gratuita o Pro) + Configurar claves de API, modelos, endpoints y prompts 8.23.51 - Desactivar iconos en negrita 9.13.51 - Restaurar pestañas del reproductor \'A continuación\', Letra y Relacionado diff --git a/patches/src/main/resources/addresources/values-fa-rIR/music/strings.xml b/patches/src/main/resources/addresources/values-fa-rIR/music/strings.xml index 812d90b06c..8f2ae9c184 100644 --- a/patches/src/main/resources/addresources/values-fa-rIR/music/strings.xml +++ b/patches/src/main/resources/addresources/values-fa-rIR/music/strings.xml @@ -462,7 +462,6 @@ Second \"item\" text" نمایش همگام‌سازی پنل - زبان‌ها پایانه هوش مصنوعی تمام ارائه‌دهندگان غیرفعال شده‌اند - متن آهنگ داخلی و تعبیه‌شده بازنشانی ترتیب ارائه‌دهنده‌ها؟ @@ -621,33 +620,50 @@ Second \"item\" text" پنهان‌کردن خط اعتبار نمایش‌داده‌شده در پایین متن ترانه جابجایی ترجمه و لاتین‌نویسی روشن: لاتین‌نویسی زیر متن ترانه و ترجمه بالای آن قرار می‌گیرد؛ خاموش: لاتین‌نویسی بالا و ترجمه پایین می‌ماند - API سازگار با OpenAI - ترجمه هوش مصنوعی - ترجمه‌ای که ارائه‌کننده ارسال می‌کند را در اولویت قرار می‌دهد، سپس مدل سازگار با OpenAI شما و در نهایت گوگل ترنسلیت - رومی‌نویسی هوش مصنوعی - اولویت با رومانیزاسیونی است که ارائه‌دهنده ارسال می‌کند، سپس مدل سازگار با OpenAI شما و در نهایت گوگل ترنسلیت + ترجمه + رومی‌نویسی + ارائه‌دهندهٔ ترجمه + گوگل (پیش‌فرض، رایگان) + سازگار با OpenAI (سفارشی) + ارائه‌دهنده رومی‌سازی + گوگل (پیش‌فرض) + دیکشنری محلی / آفلاین + سازگار با OpenAI (سفارشی) بخش هوش مصنوعی تنها زمانی استفاده می‌شود که ارائه‌دهنده هیچ برچسب بخشی مانند Chorus یا Verse را ارسال نکند و مدل سازگار با OpenAI شما آن‌ها را شناس کند - تنظیمات هوش مصنوعی - نقطه پایانی API سازگار با OpenAI را برای ترجمه، رومش‌سازی و بخش‌بندی هوش مصنوعی شعرها پیکربندی کنید - URL نقطهٔ پایان API (مثلاً https://text.pollinations.ai/openai) - کلید API (برای دسترسی ناشناس خالی بگذارید) - نام مدل (مثلاً openai-fast, gpt-4o-mini) + نقاط پایانی AI & ابری + پیکربندی کلیدهای API، مدل‌ها، نقاط پایانی و الگوهای پرامپت برای ارائه‌دهندگان AI. + آدرس URL نقطه پایانی API (به عنوان مثال https://api.openai.com/v1/chat/completions) الگوی پرامپت جای‌گذارهای: {language}, {task}, {title}, {artist}, {count}, {lines} پرامپت ذخیره شد پرامپت ذخیره شد، اما نقطه پایانی AI هنوز قابل استفاده نیست. ابتدا آن را در تنظیمات AI پیکربندی کنید پرامپت بازنشانی شد - بدنه درخواست اضافی (JSON، در هر درخواست AI ادغام می‌شود) JSON معتبر نیست. آن را اصلاح کنید یا قبل از ذخیره فیلد را پاک کنید - تنظیمات هوش مصنوعی ذخیره شد - اتصال به نقطه پایانه AI امکان‌پذیر نبود. لطفا تنظیمات خود را بررسی کنید - تنظیمات هوش مصنوعی بازنشانی شد - مدل %1$s پیکربندی شد - Base URL نمی‌تواند خالی باشد + تنظیمات ترجمه & رومانیزه کردن ذخیره شد + تنظیمات به پیش‌فرض بازنشانی شد Base URL باید با http:// یا https:// شروع شود - نام مدل نمی‌تواند خالی باشد + تست اتصال + پیکربندی نقاط پایانی + نمایش همه نقاط پایانی در حال بررسی اعتبار اتصال… + ارائه‌دهندهٔ ترجمه + ارائه‌دهنده رومی‌سازی + دریافت مدل‌ها + در حال دریافت مدل‌ها… + یک مدل را انتخاب کنید + دریافت مدل‌ها ناموفق بود. کلید API یا اتصال را بررسی کنید. + تنظیمات Gemini API + سازگار با OpenAI (نقطه اتصال سفارشی) + تنظیمات DeepL API + کلید API + مدل + Base URL + کلید API (اختیاری) + پرامپت ترجمه + بدنه اضافی درخواست (JSON اختیاری) + کلید احراز هویت (رایگان یا حرفه‌ای) + پیکربندی کلیدهای API، مدل‌ها، نقاط انتهایی و پرامپت‌ها 8.23.51 - غیرفعال‌کردن آیکون‌های پررنگ 9.13.51 - بازگرداندن برگه‌های بعدی پخش‌کننده، متن آهنگ و مرتبط diff --git a/patches/src/main/resources/addresources/values-fi-rFI/music/strings.xml b/patches/src/main/resources/addresources/values-fi-rFI/music/strings.xml index b4bd6bb44a..7d90328ab8 100644 --- a/patches/src/main/resources/addresources/values-fi-rFI/music/strings.xml +++ b/patches/src/main/resources/addresources/values-fi-rFI/music/strings.xml @@ -462,7 +462,6 @@ Varmistaaksesi, että scrobbling synkronoidaan taustatoiston aikana, varmista, e Näyttö Synkronointi. Paneeli - Kielet AI-päätepiste Kaikki tarjoajat poistettu käytöstä - sisäänrakennetut sanoitukset ja upotetut sanoitukset Palautetaanko palveluntarjoajien järjestys? @@ -616,33 +615,50 @@ Sanat tulevat kolmannen osapuolen palveluista, jotka eivät liity, sponsoroi tai Piilota lyriikoiden alla näkyvä tekijätietorivi Vaihda käännöksen ja romanisoinnin paikkaa PÄÄLLÄ asettaa romanisoinnin lyriikoiden alle ja käännöksen yläpuolelle; POIS pitää romanisoinnin yläpuolella ja käännöksen alapuolella - OpenAI-yhteensopiva API - AI-käännös - Käyttää ensin palveluntarjoajan mukana tulevaa käännöstä, sitten OpenAI-yhteensopivaa mallia ja viimeisenä Google Kääntäjää - Tekoälypohjainen romanisointi - Käyttää ensisijaisesti palveluntarjoajan omaa romanisointia, sitten OpenAI-yhteensopivaa mallia ja viimeisenä Google Kääntäjää + Käännös + Romanisointi + Käännöspalvelu + Google (oletus, ilmainen) + OpenAI-yhteensopiva (mukautettu) + Romanisointipalvelu + Google (oletus) + Paikallinen / offline-sanakirja + OpenAI-yhteensopiva (mukautettu) Tekoälyosio Käytetään vain, kun palveluntarjoaja ei toimita osioiden tunnisteita, kuten Kertosäe tai Säkeistö, jolloin OpenAI-yhteensopiva mallisi tunnistaa ne - Tekoälyasetukset - Määritä OpenAI-yhteensopiva API-päätepiste tekoälyvoimaisen sanakirjan kääntämiseen, romanisointiin ja osioon - API-päätepisteen URL-osoite (esim. https://text.pollinations.ai/openai) - API-avain (jätä tyhjäksi anonyymiä käyttöä varten) - Mallin nimi (esim. openai-fast, gpt-4o-mini) + AI & Cloud Endpoints + Määritä API-avaimet, mallit, päätepisteet ja kehotepohjat tekoälypalveluille. + API-päätepisteen URL-osoite (esim. https://api.openai.com/v1/chat/completions) Kehotepohja Paikkamerkit: {language}, {task}, {title}, {artist}, {count}, {lines} Kehote tallennettu Kehoitus tallennettu, mutta tekoälypäätepiste ei ole vielä käytettävissä. Määritä se ensin Teekohtaisista asetuksista Kehote nollattu - Lisäkehys (JSON, yhdistyy jokaiseen tekoälypyyntöön) Ei kelvollista JSON-muotoa. Korjaa se tai tyhjennä kenttä ennen tallentamista - Tekoälyasetukset tallennettu - Yhteyttä AI-päätepisteeseen ei voitu muodostaa. Tarkista asetuksesi - Tekoälyasetukset nollattu - Malli %1$s määritetty - Perus-URL-osoite ei voi olla tyhjä + Translation & Romanization settings saved + Asetukset nollattiin Perus-URL-osoitteen on alettava http:// tai https:// - Mallin nimi ei voi olla tyhjä + Testaa yhteys + Määritä päätepisteet + Näytä kaikki päätepisteet Tarkistetaan yhteyttä… + Käännöspalvelun tarjoaja + Romanisointipalvelun tarjoaja + Hae mallit + Haetaan malleja… + Valitse malli + Mallien haku epäonnistui. Tarkista API-avain tai yhteys. + Gemini API -asetukset + OpenAI-yhteensopiva (oma päätepiste) + DeepL API -asetukset + API-avain + Malli + Perus-URL + API-avain (valinnainen) + Käännöskehote + Ylimääräinen pyynnön runko (valinnainen JSON) + Autentikointiavain (Free tai Pro) + Määritä API-avaimet, mallit, päätepisteet ja kehotteet 8.23.51 - Poista lihavoidut kuvakkeet 9.13.51 - Palauta Playerin Seuraavaksi-, Sanoitukset- ja Liittyvät-välilehdet diff --git a/patches/src/main/resources/addresources/values-fil-rPH/music/strings.xml b/patches/src/main/resources/addresources/values-fil-rPH/music/strings.xml index f8922727df..f513f0ae71 100644 --- a/patches/src/main/resources/addresources/values-fil-rPH/music/strings.xml +++ b/patches/src/main/resources/addresources/values-fil-rPH/music/strings.xml @@ -462,7 +462,6 @@ Para matiyak na naka-sync ang scrobbling habang nagpe-play sa background, sigura Ipakita Synchronisasyon Panel - Mga Wika Endpoint ng AI Lahat ng provider ay naka-disable - built-in lyrics at embedded lyrics I-reset ang pagkakasunud-sunod ng provider? @@ -616,33 +615,50 @@ Ang mga lyrics ay galing sa third-party services na hindi related, sponsored, o Itago ang linya ng kredito na ipinapakita sa ibaba ng lyrics. Pagpalitin ang pagsasalin at romanisasyon. Kapag ON, nasa ibaba ng lyrics ang romanisasyon at nasa itaas ang pagsasalin; kapag OFF, nananatili sa itaas ang romanisasyon at nasa ibaba ang pagsasalin. - API na katugma ng OpenAI - Pagsasalin ng AI - Mas pinipili ang translation na kasama mula sa provider, susunod ang iyong OpenAI-compatible na model, at huli ang Google Translate - AI Romanization - Mas gusto ang romanisasyon na kasama mula sa provider, pagkatapos ay ang iyong OpenAI-compatible na model, at huli ang Google Translate + Pagsasalin + Romanisasyon + Tagapagbigay ng pagsasalin + Google (Default, Libre) + Compatible sa OpenAI (Custom) + Provider ng Romanization + Google (Default) + Lokal / Offline na Diksyunaryo + Compatible sa OpenAI (Custom) Seksyon ng AI Ginagamit lamang kapag walang kasamang mga label ng seksyon ang provider tulad ng Chorus o Verse, na tutukuyin naman ng iyong OpenAI-compatible na model - Mga Pagpipilian ng AI - I-configure ang OpenAI-compatible API endpoint para sa AI-powered lyrics translation, romanization at section. - URL ng API endpoint (hal. https://text.pollinations.ai/openai) - API key (iwanang walang laman para sa anonymous na pag-access) - Pangalan ng modelo (hal. openai-fast, gpt-4o-mini) + AI & Cloud Endpoints + I-configure ang mga API key, modelo, endpoint, at prompt template para sa mga AI provider. + API endpoint URL (hal. https://api.openai.com/v1/chat/completions) Template ng Prompt Mga Placeholder: {language}, {task}, {title}, {artist}, {count}, {lines} Nai-save na ang prompt Na-save ang prompt, ngunit hindi pa gagamit ang AI endpoint. I-configure muna ito sa ilalim ng AI Settings. Na-reset na ang prompt - Dagdag na request body (JSON, ihihinto sa bawat AI request) Hindi valid JSON. Ayusin ito o linisin ang field bago save - Nai-save na ang mga pagpipilian ng AI - Hindi maka-connect sa AI endpoint. Pakiusap suriin ang iyong settings - Na-reset na ang mga pagpipilian ng AI - Nakompigura ang Model %1$s - Ang Base URL ay hindi maaaring walang laman + Na-save ang mga setting ng Translation & Romanization + Ibinalik sa default ang mga setting Ang Base URL ay dapat magsimula sa http:// o https:// - Ang pangalan ng modelo ay hindi maaaring walang laman + I-test ang Koneksyon + I-configure ang mga Endpoint + Ipakita ang Lahat ng Endpoint Tinitignan ang koneksyon… + Provider ng Pagsasalin + Provider ng Romanization + Kuhanin ang mga Modelo + Kukuha ng mga modelo… + Pumili ng Modelo + Nabigong kuhanin ang mga modelo. Suriin ang API key o koneksyon. + Mga Setting ng Gemini API + Compatible sa OpenAI (Custom Endpoint) + Mga Setting ng DeepL API + API key + Modelo + Base URL + API key (opsyonal) + Prompt sa pagsasalin + Karagdagang request body (opsyonal na JSON) + Authentication key (Libre o Pro) + I-configure ang mga API key, modelo, endpoint, at prompt 8.23.51 - I-disable ang mga bold na icon 9.13.51 - Ibalik ang Player Up next, Lyrics, at Related tabs diff --git a/patches/src/main/resources/addresources/values-fr-rFR/music/strings.xml b/patches/src/main/resources/addresources/values-fr-rFR/music/strings.xml index 3eca80c46a..7ed2d95985 100644 --- a/patches/src/main/resources/addresources/values-fr-rFR/music/strings.xml +++ b/patches/src/main/resources/addresources/values-fr-rFR/music/strings.xml @@ -462,7 +462,6 @@ Pour que le scrobbling continue à se synchroniser pendant la lecture en arrièr Affichage Synchronisation Panneau - Langues Point de terminaison de l\'IA Tous les fournisseurs sont désactivés - paroles intégrées et paroles incorporées Réinitialiser l\'ordre des fournisseurs ? @@ -522,7 +521,7 @@ Cookies nécessaires : SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PA YouTube : cookies supprimés YouTube : cookies invalides - veuillez vérifier et réessayer Appuyez longuement sur une ligne - Appuyez sur une ligne pour accéder à ce moment de la chanson. Appuyez longuement pour copier le mot situé sous votre doigt, ou toute la ligne si vous appuyez en dehors d\'un mot. Désactivé ces deux gestes ne seront plus disponibles + Appuyez sur une ligne pour accéder à ce moment de la chanson. Appuyez longuement pour copier le mot situé sous votre doigt, ou toute la ligne si vous appuyez en dehors d\'un mot. Désactivée ces deux gestes ne seront plus disponibles Afficher les étiquettes de section Identifiez chaque nouvelle section des paroles à l\'aide d\'une petite étiquette, comme \'Couplet\' ou \'Refrain\' Afficher un bouton \'Copier\' @@ -557,7 +556,7 @@ Cookies nécessaires : SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PA Afficher les paroles dans le mini-lecteur Affiche la ligne en cours dans le mini-lecteur, avec le titre déplacé vers le sous-titre Ordre d\'affichage de l\'Artiste et du Titre - Basculer l\'ordre d\'affichage de l\'Artiste et du Titre dans le mini-lecteur et MediaSession. ACTIVÉ : \"Artiste - Titre\" et DÉSACTIVÉ : \"Titre - Artiste\" N\'affecte pas la recherche de paroles + Basculer l\'ordre d\'affichage de l\'Artiste et du Titre dans le mini-lecteur et MediaSession. ACTIVÉE : \"Artiste - Titre\" et DÉSACTIVÉE : \"Titre - Artiste\" N\'affecte pas la recherche de paroles Filtres Expression régulière des métadonnées Expression régulière appliquée aux titres, artistes, et albums avant la recherche des paroles et lors de l\'évaluation de la correspondance entre deux titres. Laissez vide pour désactiver les deux fonctions. Par défaut supprime les éléments superflus courants comme \"Official Video\", \"Remaster\", \"【插曲】\", et \"- Topic\" @@ -570,7 +569,7 @@ Cookies nécessaires : SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PA Décalage temporel global Les paroles synchronisées suivent la lecture. Si elles sont décalées, ajustez le décalage de synchronisation. Décalez les paroles synchronisées vers l\'avant ou vers l\'arrière. Utilisez une valeur positive si les paroles sont en avance sur la chanson Utiliser les segments SponsorBlock pour le décalage des paroles - Faire correspondre sans les segments que SponsorBlock passe et décaler les paroles sur l\'audio publié. Désactivé ignore entièrement SponsorBlock + Faire correspondre sans les segments que SponsorBlock passe et décaler les paroles sur l\'audio publié. Désactivée ignore entièrement SponsorBlock Ce morceau n\'a pas d\'identifiant vidéo à soumettre Source: %s @@ -618,34 +617,51 @@ Cookies nécessaires : SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PA Masquer la ligne de crédit Masquer la ligne de crédit affichée sous les paroles Inverser la traduction et la romanisation - ACTIVÉ place la romanisation sous les paroles et la traduction au-dessus ; DÉSACTIVÉ conserve la romanisation au-dessus et la traduction en dessous - API compatible avec OpenAI - Traduction par IA - Privilégie la traduction fournie par le fournisseur, puis votre modèle compatible avec OpenAI, et enfin Google Traduction - Romanisation par IA - Privilégie la romanisation fournie par le fournisseur, puis votre modèle compatible avec OpenAI, et enfin Google Traduction + ACTIVÉE place la romanisation sous les paroles et la traduction au-dessus ; DÉSACTIVÉE conserve la romanisation au-dessus et la traduction en dessous + Traduction + Romanisation + Fournisseur de traduction + Google (Par défaut, Gratuit) + Compatible avec OpenAI (Personnalisé) + Fournisseur de romanisation + Google (Par défaut) + Dictionnaire Local / Hors ligne + Compatible avec OpenAI (Personnalisé) Section par IA Utilisé uniquement lorsque le fournisseur ne fournit pas d\'étiquettes de section, comme \'Refrain\' ou \'Couplet\', que votre modèle compatible avec OpenAI détecte ensuite - Paramètres de l\'IA - Configurez le point de terminaison de l\'API compatible avec OpenAI pour la traduction des paroles par IA, la romanisation et l\'identification de section - URL du point de terminaison de l\'API (par ex. https://text.pollinations.ai/openai) - Clé API (laissez vide pour un accès anonyme) - Nom du modèle (par ex. openai-fast, gpt-4o-mini) + Points de terminaison IA & Cloud + Configurez les clés API, les modèles, les points de terminaison et les modèles de prompts pour les fournisseurs d\'IA + URL du point de terminaison de l\'API (par ex. https://api.openai.com/v1/chat/completions) Modèle de prompt Variables de remplacement : {language}, {task}, {title}, {artist}, {count}, {lines} Prompt enregistré Prompt enregistré, mais le point de terminaison de l\'IA n\'est pas encore utilisable. Configurez-le d\'abord dans \'Paramètres de l\'IA\' Prompt réinitialisé - Corps de requête supplémentaire (JSON, fusionné avec chaque requête envoyée à l\'IA) Le JSON n\'est pas valide. Corrigez-le ou videz le champ avant d\'enregistrer - Paramètres de l\'IA enregistrés - Impossible de se connecter au point de terminaison de l\'IA. Veuillez vérifier vos paramètres - Paramètres de l\'IA réinitialisés - Modèle %1$s configuré - L\'URL de base ne peut pas être vide + Paramètres de traduction & de romanisation enregistrés + Paramètres réinitialisés par défaut L\'URL de base doit commencer par http:// ou https:// - Le nom du modèle ne peut pas être vide + Tester la connexion + Configurer les points de terminaison + Afficher tous les points de terminaison Validation de la connexion… + Fournisseur de traduction + Fournisseur de romanisation + Récupérer les modèles + Récupération des modèles… + Sélectionnez un modèle + Échec de la récupération des modèles. Vérifiez la clé API ou la connexion. + Paramètres de l\'API Gemini + Compatible avec OpenAI (Point de terminaison personnalisé) + Paramètres de l\'API DeepL + Clé API + Modèle + URL de base + Clé API (facultative) + Prompt de traduction + Corps de requête supplémentaire (JSON facultatif) + Clé d\'authentification (Gratuit ou Pro) + Configurer les clés API, les modèles, les points de terminaison et les prompts 8.23.51 - Désactive les icônes en gras 9.13.51 – Restaure les onglets \'À suivre\', \'Paroles\' et \'Similaires\' du lecteur diff --git a/patches/src/main/resources/addresources/values-ga-rIE/music/strings.xml b/patches/src/main/resources/addresources/values-ga-rIE/music/strings.xml index fa72d286c9..a5f7ae9dde 100644 --- a/patches/src/main/resources/addresources/values-ga-rIE/music/strings.xml +++ b/patches/src/main/resources/addresources/values-ga-rIE/music/strings.xml @@ -462,7 +462,6 @@ Chun a chinntiú go bhfuil scrobbling i gcóir le linn imirt san chúlra, déan Taispeántas Sioncrónú Painéal - Teangacha Críochphointe AI Gach soláthraí díchumasaithe — liricí ionsuite agus liricí leabaithe Athshocraigh ord an tsoláthraí? @@ -619,33 +618,50 @@ Fianáin riachtanacha: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PA Folaigh an líne aitheantais a thaispeántar faoi na liricí Malartaigh an t-aistriúchán agus an rómánú Cuireann AR an rómánú faoi na liricí agus an t-aistriúchán os a gcionn; coinníonn AS an rómánú os cionn na liricí agus an t-aistriúchán fúthu - API atá comhoiriúnach le OpenAI - Aistriúchán AI - Is fearr leis an aistriúchán a sholáthraíonn an soláthraí, ansin do mhúnla atá comhoiriúnach le OpenAI, agus Google Translate ar deireadh - Rómáinization AI - Tosaíonn sé le rómhánú an sholáthraí, ansin do mhall mhisin OpenAI-comhoiriúnach, agus Google Translate i deireadh + Aistriúchán + Rómáinization + Soláthraí aistriúcháin + Google (Réamhshocráil, Saor) + Comhoiriúnach le OpenAI (Custom) + Soláthraí románaíochta + Google (Réamhshocráil) + Foclóir Áitiúil / Aslíne + Comhoiriúnach le OpenAI (Custom) Rannóg AI Ní úsáidtear é seo ach amháin nuair nach soláthraíonn an soláthraí aon lipéid rannáin, ar nós \'Curfá\' nó \'Véarsa\'; braithfidh do mhúnla, atá comhoiriúnach le OpenAI, na lipéid sin ansin. - Socruithe AI - Cumraigh an pointe críochta API comhoiriúnach le OpenAI le haghaidh aistriú liricí faoi thiomáint AI, rómhánú agus roinn - URL pointe críochta API (m.sh. https://text.pollinations.ai/openai) - Eochair API (fág folamh le haghaidh rochtain anaithnid) - Ainm samhail (m.sh. openai-fast, gpt-4o-mini) + AI & Pointí Leoras Scamaile + Cumraigh eochracha API, samhní, endpoints, agus teimpléid spreagtha do sholáthraí AI. + URL pointe críochta API (m.sh. https://api.openai.com/v1/chat/completions) Teimpléad Leide Áitchoimeádáin: {language}, {task}, {title}, {artist}, {count}, {lines} Sábháiltear leide Sábháladh an leid, ach níl an pointe críochta AI inúsáidte go fóill. Cumraigh é faoi Socruithe AI ​​ar dtús Athshocrú an leid - Corp iarratais breise (JSON, cumaiscthe i ngach iarratas AI) Ní JSON bailí é. Ceartaigh é nó glan an réimse sula sábhálann tú - Socruithe AI ​​sábháilte - Níorbh fhéidir ceangal le pointe críochta AI. Seiceáil do shocruithe le do thoil - Socruithe AI ​​athshocraithe - Cumraithe samhail %1$s - Ní féidir URL bonn a bheith folamh + Socruithe aistriúcháin & Rómhálaíocht sábháilte + Athshocraigh socruithe go réamhshocrú Caithfidh URL bonn tosú le http:// nó https:// - Ní féidir ainm samhail a bheith folamh + Tástáil Nasc + Cumraigh Endpoints + Taispeáin Gach Endpoints Ag fíorú na nascachta… + Soláthraí aistriúcháin + Soláthraí Rómánaíochta + Fáil Modelail + Ag fáil modelail… + Roghnaigh Model + Teip ar fháil na modelail. Seiceáil an eochair API nó an nasc. + Socruithe Gemini API + Comptaible le OpenAI (Bhunphointe Custaim), + Socruithe API DeepL + Eochair API + Samhail + URL Bun + Eochair API (rodach) + banca aistriúcháin + Corp iarrata extra (JSON rodach) + Eochair authentication (Saor nó Pro) + Configure eochracha API, samhailí, endpoints, agus prompts 8.23.51 - Díchumasaigh deilbhíní troma 9.13.51 - Athchóirigh an t-imreoir Ar Aghaidh, Liricí, agus na cluaisíní Gaolmhara diff --git a/patches/src/main/resources/addresources/values-hi-rIN/music/strings.xml b/patches/src/main/resources/addresources/values-hi-rIN/music/strings.xml index 8a61a1f85d..5cda8469c3 100644 --- a/patches/src/main/resources/addresources/values-hi-rIN/music/strings.xml +++ b/patches/src/main/resources/addresources/values-hi-rIN/music/strings.xml @@ -462,7 +462,6 @@ SABR प्लेबैक (स्पूफ वीडियो स्ट्र प्रदर्शन सिंक्रनाइज़ेशन पैनल - भाषाएं AI एंडपॉइंट सभी प्रदाता अक्षम हैं - इनबिल्ट बोल और एम्बेडेड बोल प्रदाता क्रम रीसेट करें? @@ -614,33 +613,50 @@ SABR प्लेबैक (स्पूफ वीडियो स्ट्र गीतों के नीचे दिखाई जाने वाली क्रेडिट लाइन छिपाएँ अनुवाद और रोमनीकरण आपस में बदलें चालू होने पर रोमनीकरण गीतों के नीचे और अनुवाद ऊपर रखता है; बंद होने पर रोमनीकरण ऊपर और अनुवाद नीचे रहता है - OpenAI-संगत API - AI अनुवाद - पहले प्रदाता द्वारा दिए गए अनुवाद को प्राथमिकता देता है, फिर आपके OpenAI-संगत मॉडल को, और अंत में गूगल ट्रांसलेट को - AI रोमनन - पहले प्रदाता द्वारा दिए गए रोमनकरण को प्राथमिकता देता है, फिर आपके OpenAI-संगत मॉडल को, और अंत में गूगल ट्रांसलेट को + अनुवाद + रोमनाइज़ेशन + अनुवाद प्रदाता + Google (डिफ़ॉल्ट, मुफ़्त) + OpenAI संगत (कस्टम) + रोमनाइज़ेशन प्रदाता + Google (डिफ़ॉल्ट) + स्थानीय / ऑफ़लाइन डिक्शनरी + OpenAI संगत (कस्टम) AI सेक्शन केवल तब उपयोग किया जाता है जब प्रदाता कोरस या वर्स जैसे कोई सेक्शन लेबल नहीं भेजता है, जिसे आपका OpenAI-संगत मॉडल फिर डिटेक्ट करता है - AI सेटिंग्स - AI-शक्तिशाली बोल अनुवाद, रोमनाइज़ और अनुभाग के लिए ओपनएआई-संगत API एंडपॉइंट का कॉन्फ़िगरेशन करें - API एंडपॉइंट URL (उदा. https://text.pollinations.ai/openai) - API कुंजी (अनाम पहुंच के लिए खाली छोड़ें) - मॉडल नाम (उदा. openai-fast, gpt-4o-mini) + AI & क्लाउड एंडपॉइंट्स + AI प्रदाताओं के लिए API कुंजियाँ, मॉडल, एंडपॉइंट्स और प्रॉम्प्ट टेम्पलेट्स कॉन्फ़िगर करें। + API एंडपॉइंट URL (जैसे https://api.openai.com/v1/chat/completions) प्रॉम्प्ट टेम्प्लेट प्लेसहोल्डर्स: {language}, {task}, {title}, {artist}, {count}, {lines} प्रॉम्प्ट सेव किया गया प्रॉम्प्ट सहेजा गया, लेकिन AI एंडपॉइंट अभी उपयोग योग्य नहीं है। पहले AI सेटिंग्स में इसका कॉन्फ़िगरेशन करें प्रॉम्प्ट रिसेट किया गया - अतिरिक्त अनुरोध बॉडी (JSON, हर AI अनुरोध में merge किया गया) वैध JSON नहीं। इसे ठीक करें या save करने से पहले क्षेत्र को साफ करें - AI सेटिंग्स सेव की गईं - AI एंडपॉइंट से कनेक्ट नहीं किया जा सका। कृपया अपनी सेटिंग्स जांचें - AI सेटिंग्स रिसेट की गईं - मॉडल %1$s कॉन्फ़िगर किया गया - बेस URL खाली नहीं हो सकता + अनुवाद & रोमीकरण सेटिंग्स सहेज ली गईं + सेटिंग्स डिफ़ॉल्ट पर रीसेट हो गईं बेस URL http:// या https:// से शुरू होना चाहिए - मॉडल नाम खाली नहीं हो सकता + कनेक्शन टेस्ट करें + एंडपॉइंट्स कॉन्फ़िगर करें + सभी एंडपॉइंट्स दिखाएं कनेक्शन सत्यापित किया जा रहा है… + अनुवाद प्रदाता + रोमीकरण प्रदाता + मॉडल प्राप्त करें + मॉडल लाए जा रहे हैं… + एक मॉडल चुनें + मॉडल लाने में विफल। API कुंजी या कनेक्शन की जाँच करें। + Gemini API सेटिंग्स + OpenAI संगत (कस्टम एंडपॉइंट) + DeepL API सेटिंग्स + API कुंजी + मॉडल + बेस URL + API कुंजी (वैकल्पिक) + अनुवाद प्रॉम्प्ट + अतिरिक्त अनुरोध बॉडी (वैकल्पिक JSON) + प्रमाणीकरण कुंजी (Free या Pro) + API कुंजियों, मॉडल, एंडपॉइंट्स और प्रॉम्प्ट्स को कॉन्फ़िगर करें 8.23.51 - बोल्ड आइकन अक्षम करें 9.13.51 - प्लेयर के अप नेक्स्ट, गीत और संबंधित टैब को पुनर्स्थापित करें diff --git a/patches/src/main/resources/addresources/values-hr-rHR/music/strings.xml b/patches/src/main/resources/addresources/values-hr-rHR/music/strings.xml index 18a07ea64c..ea1c9b3ce8 100644 --- a/patches/src/main/resources/addresources/values-hr-rHR/music/strings.xml +++ b/patches/src/main/resources/addresources/values-hr-rHR/music/strings.xml @@ -462,7 +462,6 @@ Kako biste osigurali sinkronizaciju scrobblinga tijekom reprodukcije u pozadini, Prikaz Sinkronizacija Panel - Jezici Krajnja točka AI-ja Svi pružatelji usluga onemogućeni — ugrađeni tekstovi pjesama i tekstovi ugrađeni u datoteku Ponovno postaviti redoslijed pružatelja? @@ -619,33 +618,50 @@ Potrebni kolačići: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPI Sakrij redak sa zaslugama prikazan ispod tekstova pjesama. Zamijeni prijevod i romanizaciju ON postavlja romanizaciju ispod tekstova pjesama, a prijevod iznad; OFF ostavlja romanizaciju iznad, a prijevod ispod. - OpenAI-kompatibilni API - AI prijevod - Prvo preferira prijevod koji šalje pružatelj, zatim vaš OpenAI-kompatibilni model, a na kraju Google Translate - AI romanizacija - Pridaje prednost romanizaciji koju šalje pružatelj, zatim vašem OpenAI-kompatibilnom modelu, a na kraju Google preveditelju + Prijevod + Romanizacija + Davatelj prijevoda + Google (Zadano, besplatno) + OpenAI kompatibilno (Prilagođeno) + Davatelj romanizacije + Google (Zadano) + Lokalni / Offline rječnik + OpenAI kompatibilno (Prilagođeno) AI odjeljak Koristi se samo kada pružatelj ne šalje oznake odjeljaka kao što su Refren ili Strofa, koje zatim detektira vaš OpenAI-kompatibilni model - AI postavke - Konfigurirajte OpenAI-sklonu API točku za prijevod tekstova pjesama pomoću AI-ja, romanizaciju i odjeljke - URL API krajnje točke (npr. https://text.pollinations.ai/openai) - API ključ (ostavite prazno za anonimni pristup) - Naziv modela (npr. openai-fast, gpt-4o-mini) + AI & Cloud krajnje točke + Konfigurirajte API ključeve, modele, krajnje točke i predloške upita za AI davatelje. + URL API krajnje točke (npr. https://api.openai.com/v1/chat/completions) Predložak prompta Zamjenici: {language}, {task}, {title}, {artist}, {count}, {lines} Prompt spremljen Upit spremljen, ali AI točka još nije uporabljiva. Prvo je konfigurirajte u postavkama AI-a Prompt poništen - Dodatno tijelo zahtjeva (JSON, spaja se u svaki AI zahtjev) Nije valjani JSON. Popravite ga ili očistite polje prije spremanja - AI postavke spremljene - Nije moguće se povezati s krajnjom točkom AI-a. Molimo provjerite svoje postavke - AI postavke poništene - Model %1$s je konfiguriran - Osnovni URL ne može biti prazan + Postavke prijevoda & romanizacije spremljene + Postavke vraćene na zadane vrijednosti Osnovni URL mora započinjati s http:// ili https:// - Naziv modela ne može biti prazan + Testiraj vezu + Konfiguriraj krajnje točke + Prikaži sve krajnje točke Provjera veze… + Davatelj prijevoda + Davatelj romanizacije + Preuzmi modele + Preuzimanje modela… + Odaberite model + Preuzimanje modela nije uspjelo. Provjerite API ključ ili vezu. + Postavke Gemini API-ja + OpenAI kompatibilno (prilagođena krajnja točka) + Postavke DeepL API-ja + API ključ + Model + Osnovni URL + API ključ (opcionalno) + Upute za prijevod + Dodatno tijelo zahtjeva (opcionalni JSON) + Ključ za autentifikaciju (Free ili Pro) + Konfigurirajte API ključeve, modele, krajnje točke i upite 8.23.51 - Onemogući podebljane ikone 9.13.51 - Vrati kartice Player Zatim, Stihovi i Povezano diff --git a/patches/src/main/resources/addresources/values-hu-rHU/music/strings.xml b/patches/src/main/resources/addresources/values-hu-rHU/music/strings.xml index 5e0966bf19..0ad0ee3637 100644 --- a/patches/src/main/resources/addresources/values-hu-rHU/music/strings.xml +++ b/patches/src/main/resources/addresources/values-hu-rHU/music/strings.xml @@ -462,7 +462,6 @@ A háttérlejátszás közbeni szinkronizálás biztosításához győződjön m Megjelenítés Szinkronizálás Panel - Nyelvek AI végpont Az összes szolgáltató letiltva – beépített és beágyazott dalszövegek Visszaállítja a szolgáltatók sorrendjét? @@ -621,33 +620,50 @@ A dalszövegek harmadik féltől származó szolgáltatásokból származnak, am A dalszöveg alatt látható forrásmegjelölés elrejtése A fordítás és a romanizáció felcserélése BE: a romanizációt a dalszöveg alá, a fordítást fölé helyezi; KI: a romanizációt felül, a fordítást alul hagyja. - OpenAI-kompatibilis API - AI-fordítás - Elsंकरben a szolgáltató által küldött fordítást használja, majd az OpenAI-kompatibilis modellt, végül pedig a Google Fordítót - AI romanizáció - Elsősorban a szolgáltató által küldött romanizálást, majd az OpenAI-kompatibilis modellt, végül pedig a Google Fordítót preferálja + Fordítás + Romanizáció + Fordító szolgáltató + Google (Alapértelmezett, Ingyenes) + OpenAI kompatibilis (Egyedi) + Romanizáló szolgáltató + Google (Alapértelmezett) + Helyi / Offline szótár + OpenAI kompatibilis (Egyedi) AI szakasz Csak akkor használja, ha a szolgáltató nem küld szakaszcímkéket, mint például Korus vagy Vers, amelyeket ezután az OpenAI-kompatibilis modell érzékel - AI Beállítások - Konfigurálja az OpenAI-kompatibilis API végpontot az AI-alapú dalszövegfordításhoz, romanizáláshoz és szekciónáláshoz - API-végpont URL-címe (pl. https://text.pollinations.ai/openai) - API-kulcs (hagyja üresen anonim hozzáféréshez) - Modellnév (pl. openai-fast, gpt-4o-mini) + AI & felhő végpontok + API kulcsok, modellek, végpontok és prompt sablonok konfigurálása az AI szolgáltatóknak. + API végpont URL (pl. https://api.openai.com/v1/chat/completions) Prompt sablon Helyőrzők: {language}, {task}, {title}, {artist}, {count}, {lines} Prompt mentve Bejegyzés elmentve, de az AI végpont még nem használható. Először konfigurálja az AI beállításokban Prompt alapértékekre visszaállítva - Extra kéreli test (JSON, egyesül minden AI kérlel) Nem érvényes JSON. Javítsa ki vagy törölje a mezőt mentés előtt - AI beállítások mentve - Nem lehetett csatlakozni az AI végponthoz. Kérjük, ellenőrizze a beállításait - AI beállítások alapértékekre visszaállítva - %1$s modell beállítva - A Base URL nem lehet üres + A fordítási & romanizációs beállítások mentve + A beállítások visszaállítva az alapértelmezettre A Base URL-nek http:// vagy https:// előtaggal kell kezdődnie - A modellnév nem lehet üres + Kapcsolat tesztelése + Végpontok konfigurálása + Összes végpont megjelenítése Kapcsolat ellenőrzése… + Fordító szolgáltató + Romanizáló szolgáltató + Modellek lekérése + Modellek lekérése… + Válasszon egy modellt + Sikertetlen modellek lekérése. Ellenorizze az API-kulcsot vagy a kapcsolatot. + Gemini API beállítások + OpenAI kompatibilis (egyedi végpont) + DeepL API beállítások + API-kulcs + Modell + Alap URL + API kulcs (opcionális) + Fordítási utasítás + Extra kérés törzse (opcionális JSON) + Hitelesítési kulcs (Ingyenes vagy Pro) + API kulcsok, modellek, végpontok és utasítások konfigurálása 8.23.51 - Félkövér ikonok letiltása 9.13.51 - A Lejátszó „Következő”, „Dalszövegek” és „Kapcsolódó” lapok visszaállítása diff --git a/patches/src/main/resources/addresources/values-in-rID/music/strings.xml b/patches/src/main/resources/addresources/values-in-rID/music/strings.xml index 45818c278b..158522bf9e 100644 --- a/patches/src/main/resources/addresources/values-in-rID/music/strings.xml +++ b/patches/src/main/resources/addresources/values-in-rID/music/strings.xml @@ -462,7 +462,6 @@ Untuk memastikan scrobbling disinkronkan selama pemutaran latar belakang, pastik Tampilkan Sinkronisasi Panel - Bahasa Titik akhir AI Semua penyedia dinonaktifkan - lirik bawaan dan lirik tertanam Setel ulang urutan penyedia? @@ -621,33 +620,50 @@ Lirik berasal dari layanan pihak ketiga yang tidak berafiliasi, disponsori, atau Sembunyikan baris kredit yang ditampilkan di bawah lirik Tukar terjemahan dan romanisasi ON menempatkan romanisasi di bawah lirik dan terjemahan di atasnya; OFF membiarkan romanisasi tetap di atas dan terjemahan di bawahnya - API yang kompatibel dengan OpenAI - Terjemahan AI - Memprioritaskan terjemahan yang dikirimkan penyedia, kemudian model yang kompatibel dengan OpenAI Anda, dan terakhir Google Translate - Romanisasi AI - Memprioritaskan romanisasi yang disediakan oleh provider, kemudian model yang kompatibel dengan OpenAI, dan terakhir Google Translate + Terjemahan + Romanisasi + Penyedia terjemahan + Google (Default, Gratis) + Kompatibel OpenAI (Kustom) + Penyedia romanisasi + Google (Default) + Kamus Lokal / Offline + Kompatibel OpenAI (Kustom) Bagian AI Hanya digunakan ketika provider tidak memberikan label bagian seperti Chorus atau Verse, yang kemudian akan dideteksi oleh model yang kompatibel dengan OpenAI - Setelan AI - Konfigurasikan endpoint API yang kompatibel dengan OpenAI untuk terjemahan, romanisasi dan bagian lirik berbasis AI - URL titik akhir API (mis. https://text.pollinations.ai/openai) - Kunci API (kosongkan untuk akses anonim) - Nama model (mis. openai-fast, gpt-4o-mini) + AI & Endpoint Cloud + Konfigurasikan kunci API, model, endpoint, dan templat prompt untuk penyedia AI. + URL endpoint API (contoh: https://api.openai.com/v1/chat/completions) Templat Prompt Penampung: {language}, {task}, {title}, {artist}, {count}, {lines} Prompt disimpan Permintaan disimpan, tetapi endpoint AI belum dapat digunakan. Konfigurasikan terlebih dahulu di Pengaturan AI Prompt diatur ulang - Badan permintaan tambahan (JSON, digabungkan ke setiap permintaan AI) JSON tidak valid. Perbaiki atau kosongkan bidang sebelum menyimpan - Setelan AI disimpan - Tidak dapat terhubung ke endpoint AI. Silakan periksa pengaturan Anda - Setelan AI diatur ulang - Model %1$s telah dikonfigurasi - URL dasar tidak boleh kosong + Setelan Terjemahan & Romanisasi disimpan + Setelan diatur ke bawaan URL dasar harus diawali dengan http:// atau https:// - Nama model tidak boleh kosong + Uji Koneksi + Konfigurasi Endpoint + Tampilkan Semua Endpoint Memvalidasi koneksi… + Penyedia Terjemahan + Penyedia Romanisasi + Ambil Model + Mengambil model… + Pilih Model + Gagal mengambil model. Periksa kunci API atau koneksi. + Pengaturan API Gemini + Kompatibel OpenAI (Endpoint Kustom) + Pengaturan API DeepL + Kunci API + Model + URL Dasar + Kunci API (opsional) + Prompt terjemahan + Body permintaan tambahan (JSON opsional) + Kunci autentikasi (Gratis atau Pro) + Konfigurasikan kunci API, model, endpoint, dan prompt 8.23.51 - Nonaktifkan ikon tebal 9.13.51 - Kembalikan tab Pemutar Berikutnya, Lirik, dan Terkait diff --git a/patches/src/main/resources/addresources/values-in-rID/youtube/strings.xml b/patches/src/main/resources/addresources/values-in-rID/youtube/strings.xml index 015ee0ed7e..21e6cc2de6 100644 --- a/patches/src/main/resources/addresources/values-in-rID/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-in-rID/youtube/strings.xml @@ -169,9 +169,9 @@ Perubahan mencakup: Menyembunyikan judul video dalam mode layar penuh Bersihkan teks subtitel video Membersihkan frasa seperti \'#\', \'Penggalangan dana\', \'produk(s)\', dan \'Toko\' dari subjudul video - Sembunyikan tombol \'Ask Gemini\' - Menyembunyikan tombol \'Ask Gemini\' di bagian bawah panel \'Ask about this video\' - Keterangan video + Sembunyikan tombol \'Tanyakan Gemini\' + Menyembunyikan tombol \'Tanyakan Gemini\' di bagian bawah panel \'Tanyakan tentang video ini\' + Deskripsi video Pilih komponen mana yang akan ditampilkan dalam deskripsi video Sembunyikan \'Ringkasan video yang dibuat AI\' Sembunyikan bagian Tanya @@ -848,7 +848,7 @@ Untuk mengakses Setelan, aktifkan tombol Setelan melalui Umum → Bilah Alat → Nonaktifkan tombol navigasi tanpa label Menampilkan tombol navigasi dengan label, jika YouTube mengirimkannya tanpa label Nonaktifkan tab navigasi toolbar - Menampilkan Langganan di bilah navigasi, jika YouTube memindahkannya ke tab di bagian atas Beranda + Menampilkan Subscription di bilah navigasi, jika YouTube memindahkannya ke tab di bagian atas Beranda Aktifkan animasi bilah navigasi Menganimasikan transisi saat beralih antar tab Nonaktifkan otomatis menyembunyikan bilah navigasi diff --git a/patches/src/main/resources/addresources/values-is-rIS/music/strings.xml b/patches/src/main/resources/addresources/values-is-rIS/music/strings.xml index f3284af3dd..140f7ad59b 100644 --- a/patches/src/main/resources/addresources/values-is-rIS/music/strings.xml +++ b/patches/src/main/resources/addresources/values-is-rIS/music/strings.xml @@ -462,7 +462,6 @@ Til þess að tryggja að scrobbling sé samstilla á meðan spilað er í bakgr Sýning Samstilling Spjald - Tungumál AI endaþáttur Allir veitendur óvirkir - innbyggðar lagatextar og innfelldir lagatextar Endurstilla forgangsröð? @@ -617,33 +616,50 @@ Nauðsynlegar vafrakökur: SID, HSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPI Fela upplýsingalínuna sem sýnd er fyrir neðan textann Víxla þýðingu og rómönskun ON setur rómönskun fyrir neðan texta og þýðingu fyrir ofan; OFF heldur rómönskun fyrir ofan og þýðingu fyrir neðan - OpenAI-samhæft API - Gervigreindarþýðing - Kostar þýðingu sem veittirinn sendir með, síðan OpenAI-samþætta líkanið þitt og að síðust Google Translate - AI rómönun - Gefa forgang að rómöngrunu frá birgja, síðan OpenAI-samþættum líkani og að lokum Google Translate + Þýðing + Rómönun + Þýðingaraðili + Google (sjálfgefið, ókostnaðarlaust) + OpenAI compatible ( sérsniðið) + Veitandi rómönunar + Google (sjálfgefið) + Staðbundin / ólínu orðabók + OpenAI compatible ( sérsniðið) AI-hluti Saaðst aðeins þegar birgir gefur ekki út merki fyrir hluta eins og Chorus eða Verse, sem OpenAI-samþætt líkan þitt greinir þá - AI-stillingar - Uppsettu opnan AI-endapunkt API fyrir AI-drifina textaþýðingu, romanisering og kafla - Vefslóð API-endapunkts (t.d. https://text.pollinations.ai/openai) - API-lykill (skildu eftir auðan fyrir nafnlausan aðgang) - Heiti líkans (t.d. openai-fast, gpt-4o-mini) + AI & skýjatengiliðir + Stillingar á API lyklum, módelum, tengiliðum og fyrirsagnymyndum fyrir AI veitendur. + URL fyrir API tengilið (t.d. https://api.openai.com/v1/chat/completions) Sniðmát fyrir fyrirspurn Plasshaldar: {language}, {task}, {title}, {artist}, {count}, {lines} Fyrirspurn vistuð Prompt vistað, en AI-endipunktur er enn ekki notanlegur. Uppsettu hann undir AI-stillingar fyrst Fyrirspurn endurstillt - Aukakroppur beiðni (JSON, blandað inn í alla AI-beiðni) Ekki gilt JSON. Leiðréttu það eða hreinsaðu reitinn áður en þú vistar - AI-stillingar vistuð - Gat ekki tengst AI-endapunkti. Vinsamlegast athugaðu uppsetningarnar þínar - AI-stillingar endurstillar - Líkanið %1$s stillt - Grunnslóð má ekki vera tóm + Stillingar fyrir þýðingu & rómönun vistuðar + Stillingar afturstilltar í upphafsvirðum Grunnslóð verður að byrja á http:// eða https:// - Heiti líkans má ekki vera tómt + Prófa tengingu + Stilla endapunktana + Sýna alla endapunkta Sannreyni tengingu… + Þýðingaraðili + Rómönunaraðili + Sækja líkön + Sækist eftir líkön… + Veldu líkan + Mislaðist að sækja líkana. Athugið API lykil eða tengingu. + Stillingar fyrir Gemini API + OpenAI samhæft (sérsniðinn endapunktur) + Stillingar fyrir DeepL API + API lykill + Líkan + Grunnur-URL + API lykill (valfrjáls) + Tilvísun fyrir þýðingu + Auka beiðnisnið (valfrjálst JSON) + Aðgangslykill (Ókeypis eða Pro) + Stilla API lykla, líkan, endapunkt og leiðbeiningar 8.23.51 - Slökkva á feitletruðum táknum 9.13.51 - Endurheimta næst, texta og tengda flipa diff --git a/patches/src/main/resources/addresources/values-it-rIT/music/strings.xml b/patches/src/main/resources/addresources/values-it-rIT/music/strings.xml index c23c955052..6590be75c3 100644 --- a/patches/src/main/resources/addresources/values-it-rIT/music/strings.xml +++ b/patches/src/main/resources/addresources/values-it-rIT/music/strings.xml @@ -461,7 +461,6 @@ Per garantire che lo scrobbling sia sincronizzato durante la riproduzione in bac Visualizza Sincronizzazione Pannello - Lingue Endpoint AI Tutti i provider disabilitati - testi integrati e testi incorporati Reimpostare l\'ordine dei provider? @@ -615,33 +614,50 @@ I testi provengono da servizi di terze parti che non sono affiliati, sponsorizza Caricamento testi... Scambia traduzione e romanizzazione Riprova - API compatibile con OpenAI - Traduzione AI - Preferisce la traduzione fornita dal provider, poi il tuo modello compatibile con OpenAI e infine Google Translate - Romanizzazione AI - Preferisce la romanizzazione fornita dal provider, quindi il modello compatibile con OpenAI e infine Google Translate + Traduzione + Romanizzazione + Fornitore di traduzione + Google (Predefinito, Gratis) + Compatibile con OpenAI (Personalizzato) + Fornitore di romanizzazione + Google (Predefinito) + Dizionario locale / offline + Compatibile con OpenAI (Personalizzato) Sezione AI Utilizzato solo quando il provider non fornisce etichette di sezione come Ritornello o Strofa, che vengono quindi rilevate dal modello compatibile con OpenAI - Impostazioni AI - Configura l\'endpoint API compatibile con OpenAI per la traduzione dei testi, la romanizzazione e le sezioni basate sull\'IA - URL dell\'endpoint API (es. https://text.pollinations.ai/openai) - Chiave API (lasciare vuota per accesso anonimo) - Nome del modello (es. openai-fast, gpt-4o-mini) + AI & endpoint cloud + Configura chiavi API, modelli, endpoint e modelli di prompt per i fornitori di IA. + URL dell\'endpoint API (es. https://api.openai.com/v1/chat/completions) Modello di prompt Segnaposti: {language}, {task}, {title}, {artist}, {count}, {lines} Prompt salvato Prompt salvato, ma l\'endpoint AI non è ancora utilizzabile. Configurarlo prima nelle Impostazioni AI Prompt ripristinato - Corpo della richiesta aggiuntivo (JSON, unito a ogni richiesta AI) JSON non valido. Correggilo o svuota il campo prima di salvare - Impostazioni AI salvate - Impossibile connettersi all\'endpoint AI. Controlla le tue impostazioni - Impostazioni AI ripristinate - Modello %1$s configurato - L\'URL di base non può essere vuoto + Impostazioni di traduzione & romanizzazione salvate + Impostazioni ripristinate ai valori predefiniti L\'URL di base deve iniziare con http:// o https:// - Il nome del modello non può essere vuoto + Testa connessione + Configura endpoint + Mostra tutti gli endpoint Verifica della connessione… + Fornitore di traduzione + Fornitore di romanizzazione + Recupera modelli + Recupero modelli… + Seleziona un modello + Impossibile recuperare i modelli. Controlla la chiave API o la connessione. + Impostazioni API Gemini + Compatibile con OpenAI (Endpoint personalizzato) + Impostazioni API DeepL + Chiave API + Modello + URL di base + Chiave API (opzionale) + Prompt di traduzione + Corpo della richiesta extra (JSON opzionale) + Chiave di autenticazione (Free o Pro) + Configura chiavi API, modelli, endpoint e prompt 8.23.51 - Disabilita icone in grassetto 9.13.51 - Ripristina le schede Successivo, Testi e Correlati del lettore diff --git a/patches/src/main/resources/addresources/values-iw-rIL/music/strings.xml b/patches/src/main/resources/addresources/values-iw-rIL/music/strings.xml index 1b96b12959..7f5c1ce064 100644 --- a/patches/src/main/resources/addresources/values-iw-rIL/music/strings.xml +++ b/patches/src/main/resources/addresources/values-iw-rIL/music/strings.xml @@ -462,7 +462,6 @@ Second \"item\" text" תצוגה סנכרון פאנל - שפות נקודת קצה של בינה מלאכותית כל הספקים מבוטלים - שירים מובנים ושירים מוטבעים לאפס את סדר הספקים? @@ -621,33 +620,50 @@ Shiriot ba'am hashlshimiyim she'em lo mitkadimim, mispansim, lo menuchsim by Mor הסתר את שורת הקרדיט המוצגת מתחת למילות השיר החלף בין תרגום לרומניזציה במצב ON הרומניזציה מופיעה מתחת למילות השיר והתרגום מעל; במצב OFF הרומניזציה נשארת מעל והתרגום מתחת - API תואם OpenAI - תרגום בינה מלאכותית - מעדיף את התרגום שמספק הספק, לאחר מכן את המודל התואם ל-OpenAI שלך, ובסוף את Google Translate - רומניזציה באמצעות בינה מלאכותית - מעדיף את התעתוק הלטיני שהספק מספק, לאחר מכן את המודל התואם ל-OpenAI שלך, ובסוף את Google Translate + תרגום + רומניזציה + ספק תרגום + Google (ברירת מחדל, חינם) + תואם OpenAI (מותאם אישית) + ספק רומניזציה + Google (ברירת מחדל) + מילון מקומי / לא מקוון + תואם OpenAI (מותאם אישית) חלק AI משמש רק כאשר הספק אינו מספק תוויות חלקים כגון פזמון או בית, אותן המודל התואם ל-OpenAI שלך יזהה - הגדרות AI - הגדר את נקודת הקצה של API התואמת OpenAI לתרגום AI, רומניזציה וחלקים של מילים - כתובת URL של נקודת קצה API (לדוגמה https://text.pollinations.ai/openai) - מפתח API (השאר ריק לגישה אנונימית) - שם המודל (לדוגמה openai-fast, gpt-4o-mini) + AI & Cloud Endpoints + הגדר מפתחות API, מודלים, נקודות קצה ותבניות הנחיה עבור ספקי AI. + כתובת URL של נקודת קצה של API (לדוגמה https://api.openai.com/v1/chat/completions) תבנית הנחיה ממלאי מקום: {language}, {task}, {title}, {artist}, {count}, {lines} ההנחיה נשמרה ההנחיה נשמרה, אך נקודת הקצה של AI אינה ניתנת לשימוש עדיין. הגדר אותה תחת הגדרות AI ראשון ההנחיה אופסה - Guf basher extra (JSON, meshuk al kol begirat AI) Lo valid JSON. Takanhu o nuke ha-shida leshanor sha\'mar - הגדרות ה-AI נשמרו - לא הצלחתי להתחבר לנקודת הקצה של הבינה המלאכותית. אנא בדוק את ההגדרות שלך - הגדרות ה-AI אופסו - מודל %1$s הוגדר - כתובת ה-URL הבסיסית לא יכולה להיות ריקה + הגדרות תרגום & רומניזציה נשמרו + הגדרות אופסו לברירת מחדל כתובת ה-URL הבסיסית חייבת להתחיל ב-http:// או ב-https:// - שם המודל לא יכול להיות ריק + בדיקת חיבור + הגדרת נקודות קצה + הצג את כל נקודות הקצה מאמת חיבור… + ספק תרגום + ספק רומניזציה + הבאת מודלים + מביא מודלים... + בחר מודל + נכשל השליפה של המודלים. בדוק את מפתח ה-API או את החיבור. + הגדרות Gemini API + תואם OpenAI (נקודת קצה מותאמת אישית) + הגדרות DeepL API + מפתח API + מודל + URL בסיסי + מפתח API (אופציונלי) + הנחיית תרגום + גוף בקשה נוסף (JSON אופציונלי) + מפתח אימות (חינמי או Pro) + הגדרת מפתחות API, מודלים, נקודות קצה והנחיות 8.23.51 - השבת אייקונים מודגשים 9.13.51 - שחזור לשוניות הבא בתור, מילים, וקשורות בנגן diff --git a/patches/src/main/resources/addresources/values-ja-rJP/music/strings.xml b/patches/src/main/resources/addresources/values-ja-rJP/music/strings.xml index 73479e0a23..37b65fecfa 100644 --- a/patches/src/main/resources/addresources/values-ja-rJP/music/strings.xml +++ b/patches/src/main/resources/addresources/values-ja-rJP/music/strings.xml @@ -462,7 +462,6 @@ SABR再生(動画ストリームのスプーフがオフ)" 表示 同期 パネル - 言語 AI エンドポイント すべてのプロバイダーが無効 - 内蔵歌詞と埋め込み歌詞 プロバイダーの順序をリセットしますか? @@ -614,33 +613,50 @@ SABR再生(動画ストリームのスプーフがオフ)" 歌詞欄最下部の、作詞作曲などのクレジット情報を表示する行を非表示にします 翻訳とローマ字表記を入れ替える オン: 歌詞の下にローマ字表記を、上に翻訳を表示します。オフ: 歌詞の下に翻訳を、上にローマ字表記を表示します - OpenAI互換API - AI翻訳 - プロバイダー提供の翻訳を優先し、次にOpenAI互換モデル、最後にGoogle翻訳を使用します - AIローマ字表記 - プロバイダーが提供するローマ字表記を優先し、次に OpenAI 互換モデル、最後に Google 翻訳を使用します + 翻訳 + ローマ字表記 + 翻訳サービス + Google (デフォルト、無料) + OpenAI 互換 (カスタム) + ローマ字表記サービス + Google (デフォルト) + ローカル / オフライン辞書 + OpenAI 互換 (カスタム) AI セクション プロバイダーが「サビ」や「Aメロ」などのセクションラベルを提供せず、OpenAI 互換モデルがそれを検出する場合にのみ使用されます - AI 設定 - AI powered歌詞翻訳、ローマ字変換、セクション分割のためにOpenAI互換APIエンドポイントを構成します - API エンドポイント URL (例: https://text.pollinations.ai/openai) - API キー (認証なしの場合は空欄) - モデル名 (例: openai-fast, gpt-4o-mini) + AI & クラウドエンドポイント + AIプロバイダーのAPIキー、モデル、エンドポイント、およびプロンプトテンプレートを設定します。 + API エンドポイント URL (例: https://api.openai.com/v1/chat/completions) プロンプト テンプレート プレースホルダー: {language}, {task}, {title}, {artist}, {count}, {lines} プロンプトを保存しました プロンプトは保存されましたが、AIエンドポイントはまだ使用できません。まずAI設定で構成してください プロンプトをリセットしました - 追加リクエストボディ (JSON、すべてのAIリクエストにマージ) 無効なJSONです。保存前に修正するかフィールドをクリアしてください - AI 設定を保存しました - AIエンドポイントに接続できませんでした。設定を確認してください - AI 設定をリセットしました - モデル %1$s が設定されました - ベース URL は空欄にはできません + 翻訳 & ローマ字表記の設定を保存しました + 設定をデフォルトにリセットしました ベース URL は http:// または https:// で始まる必要があります - モデル名は空欄にはできません + 接続テスト + エンドポイントを設定 + すべてのエンドポイントを表示 接続を確認中… + 翻訳サービス + ローマ字表記プロバイダー + モデルを取得 + モデルを取得中… + モデルを選択してください + モデルの取得に失敗しました。APIキーまたは接続を確認してください。 + Gemini API設定 + OpenAI互換(カスタムエンドポイント) + DeepL API設定 + APIキー + モデル + ベースURL + APIキー(オプション) + 翻訳プロンプト + 追加のリクエストボディ(オプションのJSON) + 認証キー(FreeまたはPro) + APIキー、モデル、エンドポイント、およびプロンプトを設定します 8.23.51 - アイコンの太字表示を無効化 9.13.51 - プレーヤーの「次のコンテンツ」「歌詞」「関連コンテンツ」タブを復元 diff --git a/patches/src/main/resources/addresources/values-ja-rJP/youtube/strings.xml b/patches/src/main/resources/addresources/values-ja-rJP/youtube/strings.xml index b45c3079b2..a9364eeb53 100644 --- a/patches/src/main/resources/addresources/values-ja-rJP/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ja-rJP/youtube/strings.xml @@ -457,13 +457,13 @@ AI について扱っているだけのチャンネルや、動画の一部に A フィードのサムネイルのいいね比率を表示 コメントを検索 - コメントにある検索ボタンをタップすると、検索ワードをすべて含む読み込み済みのコメントのみが表示されます。フレーズを検索するには、単語を引用符で囲んでください + コメント欄に検索ボタンを表示します。タップすると、読み込まれているコメントのみを対象に、すべての検索ワードを含むコメントのみを表示するフィルタを適用するためのメニューを開きます。複数の単語を引用符で囲んだ場合は、フレーズとして検索します コメントを検索 - 検索する単語 + 検索ワード 検索 解除 - 並べ替え順序を変更するか、再度コメントを開いて検索してください - 検索をクリアしました。すべてのコメントを表示するには、並べ替え順序を変更するか、再度コメントを開いてください + 検索によるフィルタを適用するには、コメントの並び順 (フィルタ) を変更するかコメント欄を開き直してください + 検索によるフィルタを解除しました。コメントの並び順 (フィルタ) を変更するかコメント欄を開き直すと、すべてのコメントを表示します 「テレビでログイン」ポップアップを無効化 「デバイスの接続」ポップアップを無効化 @@ -1259,7 +1259,7 @@ Automotive レイアウト 動画はハッシュ化された動画IDの短いプレフィックスによってリクエストされるため、サーバーはあなたが見ている動画を正確に特定できません。動画にDeArrowのサムネイルがない場合、オリジナルまたは静止画が表示されます" 接続エラーの通知を表示 DeArrow サーバーが利用できない場合に通知を表示します - APIエンドポイント + API エンドポイント DeArrow がサムネイルのキャッシュを取得するエンドポイントの URL 静止画サムネイル 静止画は各動画の序盤 / 中盤 / 終盤から取得されます。これらの画像は YouTube に組み込まれており、外部 API は使用されません diff --git a/patches/src/main/resources/addresources/values-ko-rKR/music/strings.xml b/patches/src/main/resources/addresources/values-ko-rKR/music/strings.xml index c398602f49..868f4553be 100644 --- a/patches/src/main/resources/addresources/values-ko-rKR/music/strings.xml +++ b/patches/src/main/resources/addresources/values-ko-rKR/music/strings.xml @@ -462,7 +462,6 @@ SABR 프로토콜을 사용하여 재생하는 경우 (동영상 스트림 속 디스플레이 동기화 패널 - 언어 AI 엔드포인트 모든 제공자가 비활성화됨 - 내장된 가사 및 삽입된 가사 제공자 순서를 초기화하시겠습니까? @@ -621,33 +620,50 @@ LOGIN_INFO" 가사 아래에 표시되는 크레딧 라인을 숨깁니다 번역과 로마자 표기 위치 바꾸기 활성화되면 로마자 표기를 가사 아래에 표시하고, 번역은 가사 위에 표시합니다; 비활성화되면 로마자 표기를 가사 위에 표시하고, 번역은 가사 아래에 표시합니다 - OpenAI 호환 API - AI 번역 - 제공자에서 기본 제공하는 번역을 우선 사용하고, 그다음 OpenAI 호환 모델을 사용하며, Google 번역은 마지막 순위로 사용합니다 - AI 로마자 표기 - 제공자에서 기본 제공하는 로마자 표기를 우선 사용하고, 그다음 OpenAI 호환 모델을 사용하며, Google 번역은 마지막 순위로 사용합니다 + 번역 + 로마자 표기 + 번역 제공자 + Google (기본값, 무료) + OpenAI 호환 (사용자 정의) + 로마자 표기 제공자 + Google (기본값) + 로컬 / 오프라인 사전 + OpenAI 호환 (사용자 정의) AI 섹션 제공자가 후렴 또는 절 등의 섹션 라벨을 제공하지 않는 경우에만 사용되며, 이때 OpenAI 호환 모델이 가사의 섹션을 감지하여 라벨을 붙입니다 - AI 설정 - AI 기반 가사 번역, 로마자 표기 및 섹션 감지를 위해 OpenAI 호환 API 엔드포인트를 설정합니다 - API 엔드포인트 URL (예시: https://text.pollinations.ai/openai) - API 키 (익명 접속을 원하시면 비워 두세요) - 모델 이름 (예시: openai-fast, gpt-4o-mini) + AI 및 클라우드 엔드포인트 + AI 제공자를 위한 API 키, 모델, 엔드포인트 및 프롬프트 템플릿을 구성할 수 있습니다 + API 엔드포인트 URL (예시: https://api.openai.com/v1/chat/completions) 프롬프트 템플릿 자리 표시자: {language}, {task}, {title}, {artist}, {count}, {lines} 프롬프트 저장됨 프롬프트가 저장되었지만 AI 엔드포인트는 아직 사용할 수 없습니다. 먼저 AI 설정에서 구성하세요 프롬프트 초기화 - 추가 요청 본문 (JSON, 모든 AI 요청에 병합됨) 유효한 JSON이 아닙니다. 저장하기 전에 수정하거나 필드를 지우세요 - AI 설정 저장됨 - AI 엔드포인트에 연결할 수 없습니다. 설정을 확인하세요 - AI 설정 초기화 - 모델 %1$s 구성됨 - 기본 URL은 비워둘 수 없습니다 + 번역 및 로마자 표기 설정을 저장하였습니다 + 설정을 기본값으로 초기화하였습니다 기본 URL은 http:// 또는 https://로 시작해야 합니다 - 모델 이름은 비워둘 수 없습니다 + 테스트 연결 + 엔드포인트 구성 + 모든 엔드포인트 표시하기 연결 유효성을 검사하는 중… + 번역 제공자 + 로마자 표기 제공자 + 모델 가져오기 + 모델을 가져오는 중… + 모델 선택 + 모델을 가져올 수 없습니다. API 키 또는 연결을 확인하세요 + Gemini API 설정 + OpenAI 호환 (사용자 정의 엔드포인트) + DeepL API 설정 + API 키 + 모델 + 기본 URL + API 키 (선택 사항) + 번역 프롬프트 + 추가 요청 본문 (선택 사항 JSON) + 인증 키 (무료 또는 프로) + API 키, 모델, 엔드포인트 및 프롬프트를 구성할 수 있습니다 8.23.51 - 굵은 아이콘을 비활성화합니다 9.13.51 - 플레이어에서 \'다음 트랙\', \'가사\' 및 \'관련 항목\' 탭을 복원합니다 diff --git a/patches/src/main/resources/addresources/values-lt-rLT/music/strings.xml b/patches/src/main/resources/addresources/values-lt-rLT/music/strings.xml index c531f69a26..3599940439 100644 --- a/patches/src/main/resources/addresources/values-lt-rLT/music/strings.xml +++ b/patches/src/main/resources/addresources/values-lt-rLT/music/strings.xml @@ -462,7 +462,6 @@ Kad scrobbling būtų sinchronizuojamas foninio atkūrimo metu, įsitikinkite, k Rodinys Sinchronizacija Skydelis - Kalbos AI pabaiga Visi teikėjai išjungti – integruoti žodžiai ir įterpti žodžiai Atstatyti tiekėjų eilę? @@ -621,33 +620,50 @@ Tekstai pateikiami trečiųjų šalių paslaugų, kurios nėra susijusios, remia Slėpti žemiau dainų tekstų rodomą autorių eilutę Sukeisti vertimą ir romanizaciją ĮJUNGUS romanizacija rodoma žemiau dainų tekstų, o vertimas viršuje; IŠJUNGUS romanizacija lieka viršuje, o vertimas apačioje - OpenAI suderinama API - AI vertimas - Tinklamiausia yra vertimas, kurį pateikia tiekėjas, tada jūsų su OpenAI suderinamas modelis ir galiausiai „Google Translate“ - DI romanizacija - Teikia pirmenybę romanizacijai, kurią teikia paslaugų teikėjas, tada jūsų su OpenAI susavoiruota modeliu ir galiausiai „Google Translate“ + Vertimas + Romanizacija + Vertimo teikėjas + \"Google (pabažtinė, nemokama)\" + \"OpenAI suderinamas (savarankiškas)\" + \"Romanizacijos teikėjas\" + \"Google (pabažtinė)\" + \"Vietinis / neprisijungęs žodynas\" + \"OpenAI suderinamas (savarankiškas)\" AI skiltys Naudojama tik tada, kai teikėjas nepateikia jokių skilčių žymių, pavyzdžiui, Priedainio arba Strofos, kurias tada aptinka jūsų su OpenAI suderinamas modelis - AI nustatymai - Nustatyti OpenAI suderinamumo API galinę tašką AI galingoms dainų žodžių vertimams, romanizacijai ir sekcijoms - API pabaigos taško URL (pvz., https://text.pollinations.ai/openai) - API raktas (palikite tuščią, kad prisijungtumėte anonimiškai) - Modelio pavadinimas (pvz., openai-fast, gpt-4o-mini) + DI & debesijos galutiniai taškai + \"Konfigūruokite API raktus, modelius, galutinius taškus ir raginimo šablonus DI teikėjams.\" + \"API galutinio taško URL (pvz., https://api.openai.com/v1/chat/completions)\" Uždaužo šablonas Vietiniai kintamieji: {language}, {task}, {title}, {artist}, {count}, {lines} Uždauža išsaugota Užklausa išsaugota, bet AI galinė taškolis dar naudojamasi. Pirmiausia ją nustatykite AI nustatymuose Uždauža nustatyta iš naujo - Papildomas užklausos kūnas (JSON, sujungtas su kiekviena AI užklausa) Neteisingas JSON. Pataisykite jį arba išvalykite lauką prieš išsaugodami - AI nustatymai išsaugoti - Nepavyko prisijungti prie AI galinės taško punkto. Prašom patikrinti savo nustatymus - AI nustatymai nustatyti iš naujo - Modelis %1$s sukonfigūruotas - Pagrindinis URL negali būti tuščias + Vertimo & romanizacijos nustatymai išsaugoti + Nustatymai grąžinti į standartinius Pagrindinis URL turi prasidėti http:// arba https:// - Modelio pavadinimas negali būti tuščias + Patikrinti ryšį + Konfigūruoti galutinius taškus + Rodyti visus galutinius taškus Tikrinamas ryšys… + Vertimo teikėjas + Romanizacijos teikėjas + Gauti modelius + Gaunama modeliai… + Pasirinkite modelį + Nepavyko gauti modelių. Patikrinkite API raktą arba ryšį. + \"Gemini\" API nustatymai + Suderinama su \"OpenAI\" (Tinkstamas galutinis taškas) + \"DeepL\" API nustatymai + API raktas + Modelis + Pagrindinis URL + API raktas (nebūtina) + Vertimo raginimas + Papildomas užklausos kūnas (nebūtinas JSON) + Autentifikavimo raktas (nemokamas arba „Pro“) + Konfigūruoti API raktus, modelius, galutinius taškus ir raginimus 8.23.51 – išjungti paryškintas piktogramas 9.13.51 – atkurti grotuvo skirtukus „Toliau“, „Dainų tekstai“ ir „Susiję“ diff --git a/patches/src/main/resources/addresources/values-nb-rNO/music/strings.xml b/patches/src/main/resources/addresources/values-nb-rNO/music/strings.xml index 1b3d2f76c4..87f8ecdc22 100644 --- a/patches/src/main/resources/addresources/values-nb-rNO/music/strings.xml +++ b/patches/src/main/resources/addresources/values-nb-rNO/music/strings.xml @@ -460,7 +460,6 @@ For å sikre at scrobbling synkroniseres under avspilling i bakgrunnen, må du s Visning Synkronisering Panel - Språk AI-endepunkt Alle leverandører deaktivert - innebygde tekster og innebygde tekster Tilbakestill leverandørrekkefølgen? @@ -619,33 +618,50 @@ Tekstene kommer fra tredjepartstjenester som ikke er tilknyttet, sponset eller g Skjul krediteringslinjen som vises under teksten Bytt om oversettelse og romanisering PÅ setter romanisering under teksten og oversettelsen; AV holder romanisering over teksten og oversettelsen - OpenAI-kompatibelt API - AI-oversettelse - Foretrekker oversettelsen fra leverandøren først, deretter din OpenAI-kompatible modell, og til slutt Google Translate - AI-romanisering - Foretrekker romaniseringen som leverandøren sender med, deretter din OpenAI-kompatible modell, og til slutt Google Translate + Oversettelse + Romanisering + Oversettelsesleverandør + Google (Standard, gratis) + OpenAI-kompatibel (Egendefinert) + Leverandør for romanisering + Google (Standard) + Lokal / frakoblet ordbok + OpenAI-kompatibel (Egendefinert) AI-seksjon Brukes kun når leverandøren ikke sender med seksjonsmerker som Refreng eller Vers, som din OpenAI-kompatible modell deretter detekterer - AI-innstillinger - Konfigurer OpenAI-kompatibelt API-endepunkt for AI-drevet oversettelse av sangtekster, romanisering og seksjonsinndeling - API-endepunkt-URL (f.eks. https://text.pollinations.ai/openai) - API-nøkkel (la stå tom for anonym tilgang) - Modellnavn (f.eks. openai-fast, gpt-4o-mini) + AI & sky-endepunkter + Konfigurer API-nøkler, modeller, endepunkter og prompt-maler for AI-leverandører. + URL for API-endepunkt (f.eks. https://api.openai.com/v1/chat/completions) Malskjema for ledetekst Plassholdere: {language}, {task}, {title}, {artist}, {count}, {lines} Ledetekst lagret Oppfordring lagret, men AI-endepunktet er ikke brukbart ennå. Konfigurer det først under AI-innstillinger Ledetekst tilbakestilt - Ekstra forespørselsbror (JSON, sammenslått inn i hver AI-forespørsel) Ikke gyldig JSON. Rett det eller fjern feltet før lagring - AI-innstillinger lagret - Kunne ikke koble til AI-endepunktet. Vennligst sjekk innstillingene dine - AI-innstillinger tilbakestilt - Modell %1$s konfigurert - Base-URL kan ikke være tom + Innstillinger for oversettelse & romanisering lagret + Innstillinger tilbakestilt til standard Base-URL må starte med http:// eller https:// - Modellnavn kan ikke være tomt + Test tilkobling + Konfigurer endepunkter + Vis alle endepunkter Validerer tilkobling… + Oversettelsesleverandør + Romaniseringsleverandør + Hent modeller + Henter modeller… + Velg en modell + Kunne ikke hente modeller. Sjekk API-nøkkel eller tilkobling. + Gemini API-innstillinger + OpenAI-kompatibel (egendefinert endepunkt) + DeepL API-innstillinger + API-nøkkel + Modell + Base-URL + API-nøkkel (valgfritt) + Oversettelsesinstruks + Ekstra forespørselstekst (valgfri JSON) + Autentiseringsnøkkel (Gratis eller Pro) + Konfigurer API-nøkler, modeller, endepunkter og ledetekster 8.23.51 - Deaktiver fet ikoner 9.13.51 – Gjenopprett Spiller Neste, Tekster og Relaterte faner diff --git a/patches/src/main/resources/addresources/values-nl-rNL/music/strings.xml b/patches/src/main/resources/addresources/values-nl-rNL/music/strings.xml index 90356b25ef..b826f2e0c7 100644 --- a/patches/src/main/resources/addresources/values-nl-rNL/music/strings.xml +++ b/patches/src/main/resources/addresources/values-nl-rNL/music/strings.xml @@ -462,7 +462,6 @@ Om ervoor te zorgen dat scrobbling tijdens het afspelen op de achtergrond wordt Weergeven Synchronisatie Paneel - Talen AI-eindpunt Alle providers uitgeschakeld - ingebouwde songteksten en ingesloten songteksten Providerorder resetten? @@ -611,33 +610,50 @@ Songteksten komen van derde partijen die geen verbinding hebben gesponsord of go Verberg de creditregel die onder de songteksten wordt weergegeven Wissel vertaling en romanisatie om AAN zet romanisatie onder de songteksten en vertaling erboven; UIT houdt romanisatie boven en vertaling onder. - OpenAI-compatibele API - AI-vertaling - Geeft de voorkeur aan de vertaling die de provider meestuurt, daarna aan je OpenAI-compatibele model, en als laatste aan Google Translate - AI-romanisering - Geeft de voorkeur aan de romanisatie die de provider levert, daarna aan je OpenAI-compatibele model, en als laatste aan Google Translate + Vertaling + Romanisatie + Vertalingsprovider + Google (Standaard, Gratis) + OpenAI Compatibel (Aangepast) + Romanisatieprovider + Google (Standaard) + Lokaal / Offline Woordenboek + OpenAI Compatibel (Aangepast) AI-sectie Wordt alleen gebruikt wanneer de provider geen sectielabels zoals Refrein of Couplet levert, die vervolgens door je OpenAI-compatibele model worden gedetecteerd - AI-instellingen - Configureer het OpenAI-compatible API-endpoint voor AI-gestuurde vertaling, romanisatie en sectie van teksten - API-eindpunt-URL (bijv. https://text.pollinations.ai/openai) - API-sleutel (leeg laten voor anonieme toegang) - Modelnaam (bijv. openai-fast, gpt-4o-mini) + AI & Cloud Eindpunten + Configureer API-sleutels, modellen, eindpunten en prompt-sjablonen voor AI-providers. + API-eindpunt URL (bijv. https://api.openai.com/v1/chat/completions) Prompt-sjabloon Placeholders: {language}, {task}, {title}, {artist}, {count}, {lines} Prompt opgeslagen Prompt opgeslagen, maar het AI-endpoint is nog niet bruikbaar. Configureer het eerst onder AI-instellingen Prompt gereset - Extra aanvraagbody (JSON, samengevoegd in elke AI-aanvraag) Geen geldige JSON. Corrigeer het of wis het veld voordat u opslaat - AI-instellingen opgeslagen - Kon geen verbinding maken met het AI-eindpunt. Controleer je instellingen - AI-instellingen gereset - Model %1$s geconfigureerd - Basis-URL mag niet leeg zijn + Instellingen voor vertaling & romanisatie opgeslagen + Instellingen zijn teruggezet naar de standaardinstellingen Basis-URL moet beginnen met http:// of https:// - Modelnaam mag niet leeg zijn + Verbinding testen + Endpoints configureren + Alle endpoints tonen Verbinding valideren… + Vertalingsprovider + Romanisatieprovider + Modellen ophalen + Modellen ophalen… + Selecteer een model + Het ophalen van modellen is mislukt. Controleer de API-sleutel of verbinding. + Gemini API-instellingen + OpenAI-compatibel (aangepast eindpunt) + DeepL API-instellingen + API-sleutel + Model + Basis-URL + API-sleutel (optioneel) + Vertaalprompt + Extra request body (optionele JSON) + Authenticatiesleutel (Gratis of Pro) + Configureer API-sleutels, modellen, endpoints en prompts 8.23.51 - Vetgedrukte pictogrammen uitschakelen 9.13.51 - Herstel Player Up next, Lyrics en Related tabbladen diff --git a/patches/src/main/resources/addresources/values-pl-rPL/music/strings.xml b/patches/src/main/resources/addresources/values-pl-rPL/music/strings.xml index 0132cae7cb..8d5b953846 100644 --- a/patches/src/main/resources/addresources/values-pl-rPL/music/strings.xml +++ b/patches/src/main/resources/addresources/values-pl-rPL/music/strings.xml @@ -462,8 +462,7 @@ Aby zapewnić ciągłość synchronizacji podczas odtwarzania w tle, upewnij si Wyświetlanie Synchronizacja Panel - Języki - Punkt końcowy AI + Adres AI Wszyscy dostawcy wyłączeni. Teksty wbudowane i osadzone Zresetować kolejność dostawców? Przywróć domyślną kolejność priorytetów dla dostawców @@ -597,7 +596,7 @@ Wymagane ciasteczka: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPI Odświeżanie Odświeżono Klucz API QuickLRC - QuickLRC dokonuje transkrypcji lub synchronizuje czasowo tekst bieżącego utworu. Jest to osobna usługa, niezwiązana z powyższym punktem końcowym API zgodnym z OpenAI + QuickLRC dokonuje transkrypcji lub synchronizuje czasowo tekst bieżącego utworu. Jest to osobna usługa, niezwiązana z powyższym adresem API zgodnym z OpenAI Zapisano klucz QuickLRC. Użyj przycisku synchronizacji w oknie wyszukiwania tekstu lub komunikatu w panelu, gdy utwór nie ma tekstu Zapisano klucz API QuickLRC QuickLRC odrzucił ten klucz @@ -613,39 +612,56 @@ Wymagane ciasteczka: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPI Brak tekstu do synchronizacji. Rozpoczęto transkrypcję QuickLRC nie mógł przetworzyć tego utworu Wyczerpano kredyty QuickLRC dla tego utworu - "Teksty są dopasowywane według tytułu i wykonawcy, nietypowe metadane mogą skutkować brakiem wyników lub pobraniem niewłaściwej wersji. Użyj przycisku odświeżania, aby przełączać wersje. Dostawcy oznaczeni gwiazdką (*) wymagają tokenu. Włączeni dostawcy nie zawsze są odpytywani w ustalonej przez Ciebie kolejności, lecz odpowiadają zależnie od swojej szybkości.\n\nTeksty pochodzą z zewnętrznych usług, które nie są powiązane z Morphe, przez niego sponsorowane ani wspierane. Każda z nich jest używana w obecnym kształcie: same teksty, ich dostępność oraz licencje pozostają w gestii tych usług i ich licencjodawców, a korzystanie z nich może podlegać ich warunkom korzystania z usługi. Do włączonej usługi trafia wyłącznie zlecone przez Ciebie zapytanie: tytuł, wykonawca oraz, jeśli dana usługa tego wymaga, identyfikator filmu. Opcjonalny punkt końcowy AI oraz transkrypcja QuickLRC działają tylko po ich skonfigurowaniu i otrzymują tekst piosenki lub adres filmu z utworem" + "Teksty są dopasowywane według tytułu i wykonawcy, nietypowe metadane mogą skutkować brakiem wyników lub pobraniem niewłaściwej wersji. Użyj przycisku odświeżania, aby przełączać wersje. Dostawcy oznaczeni gwiazdką (*) wymagają tokenu. Włączeni dostawcy nie zawsze są odpytywani w ustalonej przez Ciebie kolejności, lecz odpowiadają zależnie od swojej szybkości.\n\nTeksty pochodzą z zewnętrznych usług, które nie są powiązane z Morphe, przez niego sponsorowane ani wspierane. Każda z nich jest używana w obecnym kształcie: same teksty, ich dostępność oraz licencje pozostają w gestii tych usług i ich licencjodawców, a korzystanie z nich może podlegać ich warunkom korzystania z usługi. Do włączonej usługi trafia wyłącznie zlecone przez Ciebie zapytanie: tytuł, wykonawca oraz, jeśli dana usługa tego wymaga, identyfikator filmu. Opcjonalny adres AI oraz transkrypcja QuickLRC działają tylko po ich skonfigurowaniu i otrzymują tekst piosenki lub adres filmu z utworem" Włącz tylko tych dostawców, z których korzystasz, lub pozostaw ustawienie domyślne. Ciągłe żądania mogą przeciążyć małe, społecznościowe strony z tekstami Ukryj informacje o twórcach Ukrywa informacje o twórcach wyświetlane pod tekstem Zamień miejscami tłumaczenie i romanizację Włączone: romanizacja znajduje się pod tekstem, a tłumaczenie nad nim. Wyłączone: romanizacja nad tekstem, a tłumaczenie pod nim - API zgodne z OpenAI - Tłumaczenie AI - W pierwszej kolejności wybiera tłumaczenie dostawcy, następnie Twój model zgodny z OpenAI, a na końcu Tłumacza Google - Romanizacja AI - W pierwszej kolejności wybiera romanizację dostawcy, następnie Twój model zgodny z OpenAI, a na końcu Tłumacza Google + Tłumaczenie + Romanizacja + Dostawca tłumaczeń + Google (domyślny, bezpłatny) + Zgodny z OpenAI (własny) + Dostawca romanizacji + Google (domyślny) + Lokalny / słownik offline + Zgodny z OpenAI (własny) Sekcje AI Używane tylko wtedy, gdy dostawca nie udostępnia etykiet sekcji, takich jak Zwrotka lub Refren. W takim przypadku wykryje je Twój model zgodny z OpenAI - Ustawienia AI - Skonfiguruj punkt końcowy API zgodny z OpenAI do tłumaczenia, romanizacji i rozpoznawania sekcji w tekście piosenki za pomocą AI - Adres punktu końcowego API (np. https://text.pollinations.ai/openai) - Klucz API (pozostaw puste dla dostępu anonimowego) - Nazwa modelu (np. openai-fast, gpt-4o-mini) + Adresy AI i chmury + Skonfiguruj klucze API, modele, adresy oraz szablony poleceń dla dostawców AI. + Adres API (np. https://api.openai.com/v1/chat/completions) Szablon polecenia Zmienne: {language}, {task}, {title}, {artist}, {count}, {lines} Zapisano polecenie - Zapisano polecenie, ale punkt końcowy AI nie jest jeszcze gotowy do użycia. Skonfiguruj go najpierw w Ustawieniach AI + Zapisano polecenie, ale adres AI nie jest jeszcze gotowy do użycia. Skonfiguruj go najpierw w Ustawieniach AI Zresetowano polecenie - Dodatkowa treść zapytania (JSON, łączona z każdym zapytaniem do AI) Nieprawidłowy format JSON. Popraw go lub wyczyść pole przed zapisaniem - Zapisano ustawienia AI - Nie udało się połączyć z punktem końcowym AI. Sprawdź swoje ustawienia - Zresetowano ustawienia AI - Skonfigurowano model %1$s - Bazowy adres nie może być pusty + Zapisano ustawienia tłumaczenia i romanizacji + Przywrócono ustawienia domyślne Bazowy adres musi zaczynać się od http:// lub https:// - Nazwa modelu nie może być pusta + Testuj połączenie + Konfiguruj adresy + Pokaż wszystkie adresy Sprawdzanie połączenia… + Dostawca tłumaczeń + Dostawca romanizacji + Pobierz modele + Pobieranie modeli… + Wybierz model + Nie udało się pobrać modeli. Sprawdź klucz API lub połączenie. + Ustawienia API Gemini + Zgodny z OpenAI (własny adres API) + Ustawienia API DeepL + Klucz API + Model + Bazowy adres URL + Klucz API (opcjonalny) + Polecenie tłumaczenia + Dodatkowa treść zapytania (opcjonalny JSON) + Klucz uwierzytelniający (Free lub Pro) + Skonfiguruj klucze API, modele, adresy API oraz polecenia 8.23.51 - Wyłącza pogrubione ikony 9.13.51 - Przywraca zakładki Następne, Tekst i Powiązane w odtwarzaczu diff --git a/patches/src/main/resources/addresources/values-pl-rPL/youtube/strings.xml b/patches/src/main/resources/addresources/values-pl-rPL/youtube/strings.xml index 5e4ae43f62..28153f7a00 100644 --- a/patches/src/main/resources/addresources/values-pl-rPL/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-pl-rPL/youtube/strings.xml @@ -456,13 +456,13 @@ Zgłoszenia są publiczne i podlegają weryfikacji przed dodaniem kanału do lis Pokaż procent polubień na miniaturkach na liście Szukaj w komentarzach - Dotknij przycisku wyszukiwania w komentarzach, aby wyświetlić tylko załadowane komentarze zawierające wszystkie wyszukiwane słowa. Użyj cudzysłowu, aby wyszukać konkretną frazę + Dotknij przycisku wyszukiwania w sekcji komentarzy, aby wyświetlić tylko te wczytane komentarze, które zawierają wszystkie szukane słowa. Ujmij słowa w cudzysłów, aby wyszukać dokładną frazę Szukaj w komentarzach - Słowa do wyszukiwania + Szukane słowa Szukaj Wyczyść Zmień kolejność sortowania lub otwórz komentarze ponownie, aby je przeszukać - Wyszukiwanie wyczyszczone. Zmień kolejność sortowania lub otwórz komentarze ponownie, aby wyświetlić wszystkie komentarze + Wyczyszczono wyszukiwanie. Zmień kolejność sortowania lub otwórz komentarze ponownie, aby wyświetlić wszystkie komentarze Wyłącz komunikat o zalogowaniu się do TV Wyłącz wyskakujące okienko \"Połącz swoje urządzenia\" @@ -1257,8 +1257,8 @@ Ograniczenie: Jeśli opcja \"Przywróć stary pasek akcji filmu\" jest wyłączo ​Filmy są pobierane za pomocą krótkiego prefiksu zakodowanego identyfikatora filmu, dzięki czemu serwer nie wie dokładnie, jaki film oglądasz. Jeśli film nie ma miniaturki DeArrow, wyświetlana jest oryginalna lub nieruchoma klatka" Pokaż powiadomienia o błędach połączenia Wyświetla powiadomienie, gdy serwer DeArrow jest niedostępny - Punkt końcowy API - Adres punktu końcowego do miniaturek DeArrow + Adres API + Adres API do miniaturek DeArrow Kadry z filmu Statyczne kadry są pobierane z początku, środka lub końca każdego filmu. Obrazy te są wbudowane w YouTube i nie korzystają z zewnętrznych API Wyodrębnij klatkę filmu z diff --git a/patches/src/main/resources/addresources/values-pt-rBR/music/strings.xml b/patches/src/main/resources/addresources/values-pt-rBR/music/strings.xml index 5270721947..fcf8ff4012 100644 --- a/patches/src/main/resources/addresources/values-pt-rBR/music/strings.xml +++ b/patches/src/main/resources/addresources/values-pt-rBR/music/strings.xml @@ -462,7 +462,6 @@ Para garantir que o scrobbling seja sincronizado durante a reprodução em segun Exibir Sincronização Painel - Idiomas Endpoint de IA Todos os provedores desativados — letras integradas e letras incorporadas Redefinir ordem dos provedores? @@ -611,33 +610,50 @@ As letras vêm de serviços de terceiros que não são afiliados, patrocinados o Ocultar a linha de crédito mostrada abaixo das letras Trocar tradução e romanização LIGADO coloca a romanização abaixo das letras e a tradução acima; DESLIGADO mantém a romanização acima e a tradução abaixo - API compatível com OpenAI - Tradução por IA - Prefere a tradução enviada pelo provedor, depois o seu modelo compatível com OpenAI e, por último, o Google Tradutor - Romanização por IA - Prefere a romanização enviada pelo provedor, depois seu modelo compatível com OpenAI e, por último, o Google Tradutor + Tradução + Romanização + Provedor de tradução + Google (Padrão, Gratuito) + Compatível com OpenAI (Personalizado) + Provedor de romanização + Google (Padrão) + Dicionário Local / Offline + Compatível com OpenAI (Personalizado) Seção de IA Usado apenas quando o provedor não envia rótulos de seção, como Refrão ou Verso, que seu modelo compatível com OpenAI então detecta - Configurações de IA - Configure o ponto de extremidade da API compatível com OpenAI para tradução, romanização e seção de letras com IA - URL do endpoint da API (ex.: https://text.pollinations.ai/openai) - Chave da API (deixe vazio para acesso anônimo) - Nome do modelo (ex.: openai-fast, gpt-4o-mini) + IA & Endpoints de Nuvem + Configure chaves de API, modelos, endpoints e modelos de prompt para provedores de IA. + URL do endpoint da API (ex: https://api.openai.com/v1/chat/completions) Template do Prompt Espaços reservados: {language}, {task}, {title}, {artist}, {count}, {lines} Prompt salvo Prompt salvo, mas o endpoint de IA ainda não está utilizável. Configure-o em Configurações de IA primeiro Prompt redefinido - Corpo de solicitação extra (JSON, mesclado em todas as solicitações de IA) Não é JSON válido. Corrija-o ou limpe o campo antes de salvar - Configurações de IA salvas - Não foi possível conectar ao endpoint da IA. Verifique suas configurações - Configurações de IA redefinidas - Modelo %1$s configurado - A URL base não pode estar vazia + Configurações de Tradução & Romanização salvas + Configurações redefinidas para o padrão A URL base deve começar com http:// ou https:// - O nome do modelo não pode estar vazio + Testar Conexão + Configurar Endpoints + Mostrar Todos os Endpoints Validando conexão… + Provedor de Tradução + Provedor de Romanização + Buscar Modelos + Buscando modelos… + Selecione um Modelo + Falha ao buscar modelos. Verifique a chave da API ou a conexão. + Configurações da API Gemini + Compatível com OpenAI (Endpoint Personalizado) + Configurações da API DeepL + Chave da API + Modelo + URL Base + Chave da API (opcional) + Prompt de tradução + Corpo de requisição extra (JSON opcional) + Chave de autenticação (Gratuita ou Pro) + Configure chaves de API, modelos, endpoints e prompts 8.23.51 - Desativar ícones em negrito 9.13.51 - Restaurar abas \"A seguir\", \"Letras\" e \"Relacionados\" do player diff --git a/patches/src/main/resources/addresources/values-pt-rPT/music/strings.xml b/patches/src/main/resources/addresources/values-pt-rPT/music/strings.xml index 4843ac736f..9d55080bd3 100644 --- a/patches/src/main/resources/addresources/values-pt-rPT/music/strings.xml +++ b/patches/src/main/resources/addresources/values-pt-rPT/music/strings.xml @@ -462,7 +462,6 @@ Para garantir que o scrobbling seja sincronizado durante a reprodução em segun Exibir Sincronização Painel - Idiomas Ponto de extremidade de IA Todos os provedores desativados - letras integradas e letras embutidas Redefinir ordem dos provedores? @@ -619,33 +618,50 @@ Cookies necessários: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAP Ocultar a linha de crédito exibida abaixo das letras Trocar tradução e romanização ON coloca a romanização abaixo das letras e a tradução acima; OFF mantém a romanização acima e a tradução abaixo - API compatível com OpenAI - Tradução por IA - Prefere a tradução enviada pelo provedor, depois o seu modelo compatível com OpenAI e, por último, o Google Tradutor - Romanização por IA - Prefere a romanização fornecida pelo provedor, depois o seu modelo compatível com OpenAI e, por último, o Google Tradutor + Tradução + Romanização + Provedor de tradução + Google (Padrão, Grátis) + Compatível com OpenAI (Personalizado) + Provedor de romanização + Google (Padrão) + Dicionário Local / Offline + Compatível com OpenAI (Personalizado) Seção de IA Usado apenas quando o provedor não fornece rótulos de seção como Refrão ou Verso, que o seu modelo compatível com OpenAI então detecta - Definições de IA - Configure o endpoint da API compatível com OpenAI para tradução de letras baseada em IA, romanização e seção - URL do endpoint da API (ex.: https://text.pollinations.ai/openai) - Chave da API (deixe em branco para acesso anônimo) - Nome do modelo (ex.: openai-fast, gpt-4o-mini) + Endpoints de IA & Nuvem + Configure chaves de API, modelos, endpoints e modelos de prompt para provedores de IA. + URL do endpoint da API (ex: https://api.openai.com/v1/chat/completions) Modelo de Prompt Espaços reservados: {language}, {task}, {title}, {artist}, {count}, {lines} Prompt salvo Prompt salvo, mas o endpoint da IA ainda não está utilizável. Configure-o primeiro nas Configurações de IA Prompt redefinido - Corpo de solicitação extra (JSON, mesclado em todas as solicitações de IA) Não é JSON válido. Corrija-o ou limpe o campo antes de salvar - Definições de IA salvas - Não foi possível conectar ao ponto de extremidade da IA. Verifique suas configurações - Definições de IA redefinidas - Modelo %1$s configurado - A URL base não pode estar vazia + Configurações de Tradução & Romanização salvas + As definições foram redefinidas para a predefinição A URL base deve começar com http:// ou https:// - O nome do modelo não pode estar vazio + Testar Conexão + Configurar Endpoints + Mostrar Todos os Endpoints Validando conexão… + Provedor de Tradução + Provedor de Romanização + Obter Modelos + Obtendo modelos… + Selecione um Modelo + Falha ao buscar modelos. Verifique a chave da API ou a conexão. + Configurações da API do Gemini + Compatível com OpenAI (Endpoint Personalizado) + Configurações da API do DeepL + Chave da API + Modelo + URL Base + Chave da API (opcional) + Prompt de tradução + Corpo de requisição extra (JSON opcional) + Chave de autenticação (Free ou Pro) + Configure chaves de API, modelos, endpoints e prompts 8.23.51 - Desativar ícones em negrito 9.13.51 - Restaurar as abas A seguir, Letras e Relacionados do Player diff --git a/patches/src/main/resources/addresources/values-ro-rRO/music/strings.xml b/patches/src/main/resources/addresources/values-ro-rRO/music/strings.xml index da87947bb5..41959f1ac1 100644 --- a/patches/src/main/resources/addresources/values-ro-rRO/music/strings.xml +++ b/patches/src/main/resources/addresources/values-ro-rRO/music/strings.xml @@ -462,7 +462,6 @@ Pentru a te asigura că scrobbling-ul este sincronizat în timpul redării în f Afișare Sincronizare Panou - Limbi Endpoint AI Toți furnizorii sunt dezactivați - versuri încorporate și versuri încapsulate Resetați ordinea furnizorilor? @@ -604,33 +603,50 @@ Versurile provin de la servicii terțe care nu sunt afiliate, sponsorizate sau a Redare în curs pe un alt dispozitiv. Redare în curs pe acest dispozitiv. Redare în curs pe un alt dispozitiv. - API compatibil OpenAI - Traducere AI - Preferă traducerea furnizată de furnizor, apoi modelul tău compatibil OpenAI și, la final, Google Translate - Romanizare AI - Preferă romanizarea furnizată de furnizor, apoi modelul tău compatibil cu OpenAI și, la final, Google Translate + Traducere + Romanizare + Furnizor de traducere + Google (Implicit, Gratuit) + Compatibil OpenAI (Personalizat) + Furnizor de romanizare + Google (Implicit) + Dicționar Local / Offline + Compatibil OpenAI (Personalizat) Secțiune AI Utilizat doar atunci când furnizorul nu trimite etichete de secțiune, cum ar fi Refren sau Strofă, pe care modelul tău compatibil cu OpenAI le detectează ulterior - Setări AI - Configurează endpoint-ul API compatibil OpenAI pentru traducerea, romanizarea și secționarea versurilor bazate pe AI - URL endpoint API (de ex. https://text.pollinations.ai/openai) - Cheie API (lasă gol pentru acces anonim) - Nume model (de ex. openai-fast, gpt-4o-mini) + AI & Puncte de terminal Cloud + Configurați cheile API, modelele, punctele de terminal și șabloanele de prompt pentru furnizorii de AI. + URL-ul punctului de terminal API (ex. https://api.openai.com/v1/chat/completions) Șablon de Prompt Placeholder-e: {language}, {task}, {title}, {artist}, {count}, {lines} Prompt salvat Prompt salvat, dar endpoint-ul AI nu este încă utilizabil. Configurează-l mai întâi la Setări AI Prompt resetat - Corp de solicitare suplimentar (JSON, fuzionat în fiecare solicitare AI) JSON nevalid. Corectează-l sau golește câmpul înainte de salvare - Setările AI salvate - Nu s-a putut stabili conexiunea cu punctul final AI. Te rugăm să verifici setările tale - Setările AI resetate - Model %1$s configurat - URL-ul de bază nu poate fi gol + Setările de traducere & romanizare au fost salvate + Setările au fost resetate la valorile implicite URL-ul de bază trebuie să înceapă cu http:// sau https:// - Numele modelului nu poate fi gol + Testare conexiune + Configurare puncte terminale + Afișare toate punctele terminale Se validează conexiunea… + Furnizor de traducere + Furnizor de romanizare + Preluare modele + Se preiau modelele… + Selectați un model + Eșec la preluarea modelelor. Verificați cheia API sau conexiunea. + Setări Gemini API + Compatibil OpenAI (Endpoint personalizat) + Setări DeepL API + Cheie API + Model + URL de bază + Cheie API (opțional) + Prompt de traducere + Corp de cerere suplimentar (JSON opțional) + Cheie de autentificare (Free sau Pro) + Configurați cheile API, modelele, punctele finale și prompturile 8.23.51 - Dezactivează pictogramele aldine 9.13.51 - Restaurează filele Up next, Versuri și Conexe din player diff --git a/patches/src/main/resources/addresources/values-ru-rRU/music/strings.xml b/patches/src/main/resources/addresources/values-ru-rRU/music/strings.xml index c4ace88d80..517cbb0b32 100644 --- a/patches/src/main/resources/addresources/values-ru-rRU/music/strings.xml +++ b/patches/src/main/resources/addresources/values-ru-rRU/music/strings.xml @@ -462,7 +462,6 @@ SABR-воспроизведение (подмена видеопотоков о Отображение Синхронизация Панель - Языки Конечная точка AI Все источники отключены - встроенные тексты песен и встроенные тексты Сбросить порядок сервисов? @@ -614,33 +613,50 @@ SABR-воспроизведение (подмена видеопотоков о Скрывать строку с указанием авторства под текстом Поменять местами перевод и романизацию ON размещает романизацию под переводом, OFF — над ним - OpenAI-совместимый API - ИИ-перевод - Приоритет отдается переводу от провайдера, затем вашей OpenAI-совместимой модели, и в последнюю очередь — Google Translate - ИИ-романизация - Приоритет отдается романизации от провайдера, затем вашей OpenAI-совместимой модели и в последнюю очередь — Google Translate + Перевод + Романизация + Поставщик перевода + Google (по умолчанию, бесплатно) + Совместимый с OpenAI (пользовательский) + Поставщик романизации + Google (по умолчанию) + Локальный / автономный словарь + Совместимый с OpenAI (пользовательский) Разделы ИИ Используется только в том случае, если провайдер не предоставляет метки разделов, таких как «Припев» или «Куплет», которые затем определяет ваша OpenAI-совместимая модель - Настройки ИИ - Настройте конечную точку API, совместимую с OpenAI, для перевода, романизации и разбиения на секции текстов песен с помощью ИИ - URL конечной точки API (например, https://text.pollinations.ai/openai) - Ключ API (оставьте пустым для анонимного доступа) - Название модели (например, openai-fast, gpt-4o-mini) + ИИ & облачные эндпоинты + Настройте API-ключи, модели, эндпоинты и шаблоны промптов для ИИ-поставщиков. + URL-адрес эндпоинта API (например, https://api.openai.com/v1/chat/completions) Шаблон промпта Плейсхолдеры: {language}, {task}, {title}, {artist}, {count}, {lines} Промпт сохранен Промпт сохранен, но конечная точка ИИ пока не готова к использованию. Сначала настройте её в разделе настроек ИИ Промпт сброшен - Дополнительный текст запроса (JSON, включаться в каждый запрос AI) Неверный JSON. Исправьте ошибку или очистите поле перед сохранением - Настройки ИИ сохранены - Не удалось подключиться к конечной точке ИИ. Проверьте ваши настройки - Настройки ИИ сброшены - Модель %1$s настроена - Базовый URL не может быть пустым + Настройки перевода & романизации сохранены + Настройки восстановлены до значений по умолчанию Базовый URL должен начинаться с http:// или https:// - Название модели не может быть пустым + Проверить соединение + Настройка эндпоинтов + Показать все эндпоинты Проверка подключения… + Провайдер перевода + Провайдер романизации + Получить модели + Загрузка моделей… + Выберите модель + Не удалось загрузить модели. Проверьте API-ключ или соединение. + Настройки Gemini API + Совместимо с OpenAI (пользовательская конечная точка) + Настройки DeepL API + API-ключ + Модель + Базовый URL + API-ключ (необязательно) + Промпт для перевода + Дополнительное тело запроса (необязательный JSON) + Ключ аутентификации (Free или Pro) + Настройка API-ключей, моделей, эндпоинтов и промптов Выключить жирные иконки 9.13.51 - Восстановить вкладки \"Далее\", \"Текст песни\" и \"Похожее\" в плеере diff --git a/patches/src/main/resources/addresources/values-sk-rSK/music/strings.xml b/patches/src/main/resources/addresources/values-sk-rSK/music/strings.xml index 723a39e446..b43a0bd2e4 100644 --- a/patches/src/main/resources/addresources/values-sk-rSK/music/strings.xml +++ b/patches/src/main/resources/addresources/values-sk-rSK/music/strings.xml @@ -462,7 +462,6 @@ Na zabezpečenie synchronizácie scrobblingu počas prehrievania na pozadí sa u Zobraziť Synchronizácia Panel - Jazyky Koncový bod AI Všetky poskytovatelia vypnutí - zabudované texty a vložené texty Obnoviť poradie poskytovateľov? @@ -616,33 +615,50 @@ Texty pochádzajú zo služieb tretích strán, ktoré nie sú pridružené, spo Nahlásiť problém s textom piesne Problém bol nahlásený Nahlásenie zlyhalo - API kompatibilné s OpenAI - AI preklad - Uprednostňuje preklad poskytnutý poskytovateľom, potom váš model kompatibilný s OpenAI a nakoniec Google Translate - AI romanizácia - Uprednostňuje romanizáciu poskytnutú poskytovateľom, potom váš model kompatibilný s OpenAI a napokon Google Translate + Preklad + Romanizácia + Poskytovateľ prekladu + Google (predvolené, zadarmo) + Kompatibilné s OpenAI (vlastné) + Poskytovateľ romanizácie + Google (predvolené) + Lokálny / offline slovník + Kompatibilné s OpenAI (vlastné) Sekcia AI Používa sa len v prípade, že poskytovateľ neposkytne žiadne označenia sekcií, ako napríklad Refrén alebo Sloka, ktoré následne deteguje váš model kompatibilný s OpenAI - Nastavenia AI - Nakonfigurujte endpoint API kompatibilného s OpenAI pre AI-prekládanie textov piesní, romanizáciu a sekciu - URL koncového bodu API (napr. https://text.pollinations.ai/openai) - API kľúč (nechajte prázdne pre anonymný prístup) - Názov modelu (napr. openai-fast, gpt-4o-mini) + AI & cloudové koncové body + Konfigurujte API kľúče, modely, koncové body a šablóny promptov pre poskytovateľov AI. + URL koncového bodu API (napr. https://api.openai.com/v1/chat/completions) Šablóna promptu Zástupníky: {language}, {task}, {title}, {artist}, {count}, {lines} Prompt uložený Výzva bola uložená, ale AI endpoint ešte nie je použiteľný. Najprv ho nakonfigurujte pod Nastaveniami AI Prompt resetovaný - Ďalšie telo žiadosti (JSON, zlúčené do každej AI žiadosti) Neplatné JSON. Opravte ho alebo vyčistite pole pred uložením - Nastavenia AI uložené - Nepodarilo sa pripojiť ku koncovému bodu AI. Skontrolujte vaše nastavenia - Nastavenia AI resetované - Model %1$s konfigurovaný - Základná URL nemôže byť prázdna + Nastavenia prekladu & romanizácie boli uložené + Nastavenia boli obnovené na predvolené Základná URL musí začínať s http:// alebo https:// - Názov modelu nemôže byť prázdny + Otestovať pripojenie + Konfigurovať koncové body + Zobraziť všetky koncové body Overuje sa pripojenie… + Poskytovateľ prekladu + Poskytovateľ romanizácie + Načítať modely + Načítavanie modelov… + Vyberte model + Nepodarilo sa načítať modely. Skontrolujte API kľúč alebo pripojenie. + Nastavenia Gemini API + Kompatibilné s OpenAI (vlastný koncový bod) + Nastavenia DeepL API + API kľúč + Model + Základná URL + API kľúč (voliteľné) + Prompt pre preklad + Dodatočné telo požiadavky (voliteľné JSON) + Kľúč na autentifikáciu (Free alebo Pro) + Konfigurujte API kľúče, modely, koncové body a prompty 8.23.51 - Zakázať tučné ikony 9.13.51 – Obnoviť karty Prehrávača: Ďalej, Texty a Súvisiace diff --git a/patches/src/main/resources/addresources/values-sl-rSI/music/strings.xml b/patches/src/main/resources/addresources/values-sl-rSI/music/strings.xml index 472a584f1b..e97e39ea28 100644 --- a/patches/src/main/resources/addresources/values-sl-rSI/music/strings.xml +++ b/patches/src/main/resources/addresources/values-sl-rSI/music/strings.xml @@ -460,7 +460,6 @@ Za zagotovitev sinhronizacije med predvajanjem v ozadju poskrbite, da je uporaba Prikaz Sinhronizacija Panel - Jeziki Končna točka AI Vsi ponudniki onemogočeni - vgrajene besedila in vdelana besedila Ponastavitev vrstnega reda ponudnikov? @@ -607,33 +606,50 @@ Besedila izhajajo iz storitev tretjih oseb, ki niso povezane s podjetjem Morphe, Skrijte vrstico z avtorstvom, prikazano pod besedilom. Zamenjajte prevod in romanizacijo. Ko je vklopljeno, se romanizacija prikaže pod besedilom, prevod pa zgoraj; ko je izklopljeno, ostane romanizacija zgoraj, prevod pa spodaj - API, združljiv z OpenAI - AI prevod - Prednost ima prevod ponudnika, nato vaš model, združljiv z OpenAI, in na koncu Google prevajalnik - AI romanizacija - Prednost da romanizacije, ki jo pošlje ponudnik, nato vaš model, združljiv z OpenAI, in na koncu Google prevajalnik + Prevod + Romanizacija + Ponudnik prevodov + Google (privzeto, brezplačno) + Združljivo z OpenAI (po meri) + Ponudnik romanizacije + Google (privzeto) + Lokalni / offline slovar + Združljivo z OpenAI (po meri) AI razdelek Uporabljeno samo, ko ponudnik ne pošlje oznak razdelkov, kot so Refren ali Kit, ki jih nato zazna vaš model, združljiv z OpenAI - AI nastavitve - Nastavite združljivo končno točko API-ja OpenAI za AI-poganjan prevod, prepis in označevanje delov besedil - URL končne točke API (npr. https://text.pollinations.ai/openai) - API ključ (pustite prazno za anonimen dostop) - Ime modela (npr. openai-fast, gpt-4o-mini) + AI & končne točke v oblaku + Konfigurirajte API ključe, modele, končne točke in predloge promptov za ponudnike umetne inteligence. + URL končne točke API (npr. https://api.openai.com/v1/chat/completions) Predloga za poziv Zastopniki: {language}, {task}, {title}, {artist}, {count}, {lines} Poziv shranjen Poziv je shranjen, vendar končna točka AI še ni uporabna. Najprejo jo konfigurirajte pod Nastavitve AI Poziv ponastavljen - Dodatno telo zahteve (JSON, združeno z vsako zahtevo AI) Ni veljavno JSON. Popravite ga ali počistite polje pred shranjevanjem - AI nastavitve shranjene - Ni mogoče vzpostaviti povezave s končno točko AI. Preverite nastavitve - AI nastavitve ponastavljene - Model %1$s konfiguriran - Osnovni URL ne sme biti prazen + Nastavitve prevoda & romanizacije so shranjene + Nastavitve so ponastavljene na privzete Osnovni URL se mora začeti z http:// ali https:// - Ime modela ne sme biti prazno + Preveri povezavo + Konfiguriraj končne točke + Prikaži vse končne točke Preverjanje povezave… + Ponudnik prevoda + Ponudnik romanizacije + Pridobi modele + Pridobivanje modelov… + Izberite model + Ni uspelo pridobiti modeljev. Preverite API ključ ali povezavo. + Nastavitve Gemini API + OpenAI kompatibilno (lastna končna točka) + Nastavitve DeepL API + API ključ + Model + Osnovni URL + API ključ (opcijonalno) + Navodilo za prevod + Dodatno telo zahteve (opcijonalni JSON) + Ključ za avtentikacijo (Brezplačni ali Pro) + Konfigurirajte API ključe, modele, končne točke in navodila 8.23.51 - Onemogoči krepke ikone 9.13.51 - Obnovi zavihke predvajalnika: Naslednje, Besedila in Povezano diff --git a/patches/src/main/resources/addresources/values-sr-rCS/music/strings.xml b/patches/src/main/resources/addresources/values-sr-rCS/music/strings.xml index e738ecdf47..8de5663a74 100644 --- a/patches/src/main/resources/addresources/values-sr-rCS/music/strings.xml +++ b/patches/src/main/resources/addresources/values-sr-rCS/music/strings.xml @@ -462,7 +462,6 @@ Da biste obezbedili sinhronizaciju scrobbling-a tokom reprodukcije u pozadini, u Prikaz Sinhronizacija Panel - Jezici Krajnja tačka veštačke inteligencije Svi izvori onemogućeni - ugrađene stihove i ugnežđene stihove Poništi redosled izvora? @@ -612,33 +611,50 @@ Tekstovi dolaze od usluga trećih strana koje nisu afilijirane, sponzorisanе il Sakrij red s informacijama prikazan ispod stihova. Promeni redosled prikaza izvođača i naslova Promeni redosled prikaza izvođača i naslova - OpenAI-kompatibilni API - AI prevod - Prioritet je prevodu koji dostavlja pružalac, zatim vašem OpenAI-kompatibilnom modelu, a na kraju Google Translate - AI romanizacija - Primat će romanizaciju koju šalje provajder, zatim vaš OpenAI-kompatibilni model, i na kraju Google Translate + Prevod + Romanizacija + Provajder prevoda + Google (podrazumevano, besplatno) + OpenAI kompatibilno (prilagođeno) + Provajder romanizacije + Google (podrazumevano) + Lokalni / oflajn rečnik + OpenAI kompatibilno (prilagođeno) AI sekcija Koristi se samo kada provajder ne šalje oznake sekcija kao što su Refren ili Strofa, koje zatim detektuje vaš OpenAI-kompatibilni model - AI podešavanja - Konfigurišite OpenAI-kompatibilnu API krajnju tačku za AI-om pokrenut prevod stihova, transliteraciju i deljenje sekcija - API endpoint URL (npr. https://text.pollinations.ai/openai) - API ključ (ostavite prazno za anonimni pristup) - Naziv modela (npr. openai-fast, gpt-4o-mini) + AI & Cloud krajnje tačke + Konfigurišite API ključeve, modele, krajnje tačke i šablone upita za AI provajdere. + URL API krajnje tačke (npr. https://api.openai.com/v1/chat/completions) Šablon uputa Rezervacije: {language}, {task}, {title}, {artist}, {count}, {lines} Uputa sačuvana Prompt je sačuvan, ali AI krajnja tačka još uvek nije upotrebljiva. Prvo je konfigurišite u podešavanjima AI-a Uputa resetovana - Dodatno telo zahteva (JSON, spaja se u svaki AI zahtev) Nije validan JSON. Popravite ga ili očistite polje pre čuvanja - AI podešavanja sačuvana - Nemoguće je povezati na tačku ulaza AI. Proverite svoja podešavanja - AI podešavanja resetovana - Model %1$s konfigurisan - Osnovni URL ne može biti prazan + Podešavanja za prevod & romanizaciju su sačuvana + Podešavanja su vraćena na podrazumevane vrednosti Osnovni URL mora početi sa http:// ili https:// - Naziv modela ne može biti prazan + Testiraj vezu + Konfiguriši krajnje tačke + Prikaži sve krajnje tačke Provera veze… + Provajder prevoda + Provajder romanizacije + Preuzmi modele + Preuzimanje modela… + Izaberite model + Preuzimanje modela nije uspelo. Proverite API ključ ili vezu. + Gemini API podešavanja + OpenAI kompatibilno (prilagođena krajnja tačka) + DeepL API podešavanja + API ključ + Model + Bazni URL + API ključ (opciono) + Prompt za prevod + Dodatno telo zahteva (opcioni JSON) + Ključ za autentifikaciju (Free ili Pro) + Konfigurišite API ključeve, modele, krajnje tačke i prompte 8.23.51 - Onemogući podebljane ikonice 9.13.51 - Vrati kartice Sledeće, Tekstovi i Povezano za plejer diff --git a/patches/src/main/resources/addresources/values-sr-rSP/music/strings.xml b/patches/src/main/resources/addresources/values-sr-rSP/music/strings.xml index 48a24c2e27..31b7ba1517 100644 --- a/patches/src/main/resources/addresources/values-sr-rSP/music/strings.xml +++ b/patches/src/main/resources/addresources/values-sr-rSP/music/strings.xml @@ -462,7 +462,6 @@ Da biste osigurali sinhronizaciju scrobbling-a tokom reprodukcije u pozadini, po Приказ Синхронизација Панел - Језици AI крајња тачка Сви пружаоци су онемогућени - уграђене речи песама и уклопљене речи песама Ресетовати редослед провајдера? @@ -614,33 +613,50 @@ Da biste osigurali sinhronizaciju scrobbling-a tokom reprodukcije u pozadini, po Сакриј линију са заслугама приказану испод текстова. Замени превод и романизацију. УКЉУЧЕНО поставља романизацију испод текста, а превод изнад; ИСКЉУЧЕНО задржава романизацију изнад и превод испод. - OpenAI kompatibilan API - AI превод - Прво даје предност преводу који испоручује провајдер, затим вашем OpenAI kompatibilном моделу, а на крају Google Translate-у - AI романизација - Предност даје романизацији коју испоручује провајдер, затим вашем OpenAI-компатибилном моделу, а на крају Google Translate-у + Превод + Романизација + Пружалац превода + Google (Подразмевано, бесплатно) + OpenAI компатибилно (Прилагођено) + Пружалац романизације + Google (Подразмевано) + Локални / Офлајн речник + OpenAI компатибилно (Прилагођено) AI секција Користи се само када провајдер не испоручује ознаке секција као што су Рефрен или Строфа, које затим детектује ваш OpenAI-компатибилни модел - AI подешавања - Подесите крајњу тачку OpenAI-компатибилног API-ја за AI вођен превод и романизацију текстова - API крајња тачка URL (нпр. https://text.pollinations.ai/openai) - API кључ (оставите празно за анониман приступ) - Назив модела (нпр. openai-fast, gpt-4o-mini) + AI & Cloud krajnje tačke + Конфигуришите API кључеве, моделе, krajnje tačke и шаблоне упита за AI пружаоце. + API endpoint URL (нпр. https://api.openai.com/v1/chat/completions) Шаблон промпта Заменичи: {language}, {task}, {title}, {artist}, {count}, {lines} Промпт је сачуван Упутство је сачувано, али AI крајња тачка још увек није доступна. Подесите је прво у подешавањима AI-ја Промпт је ресетован - Додатно тело захтева (JSON, спојено у сваки AI захтев) Није валидан JSON. Поправите га или испразните поље пре чувања - AI подешавања су сачувана - Не могу да се повезем на AI крајњу тачку. Молим вас проверите подешавања - AI подешавања су ресетована - Модел %1$s је конфигурисан - Base URL не може бити празан + Подешавања за превод & романизацију су сачувана + Подешавања су враћена на подразумеване вредности Base URL мора почети са http:// или https:// - Назив модела не може бити празан + Тестирај повезивање + Конфигуриши крајње тачке + Прикажи све крајње тачке Проверавање везе… + Пружалац превода + Пружалац романизације + Преузми моделе + Преузимање модела… + Изаберите модел + Неуспешно преузимање модела. Проверите API кључ или повезивање. + Gemini API подешавања + OpenAI компатибилно (прилагођена krajnja tačka) + DeepL API подешавања + API кључ + Модел + Базни URL + API кључ (опционо) + Упутство за превод + Додатно тело захтева (опционо JSON) + Кључ за аутентификацију (Free или Pro) + Конфигуришите API кључеве, моделе, крајње тачке и упутства 8.23.51 - Онемогући подебљане иконе 9.13.51 — Врати картице „Следеће”, „Текстови” и „Повезано” у плејеру diff --git a/patches/src/main/resources/addresources/values-sv-rSE/music/strings.xml b/patches/src/main/resources/addresources/values-sv-rSE/music/strings.xml index 630823ff36..1f0286e674 100644 --- a/patches/src/main/resources/addresources/values-sv-rSE/music/strings.xml +++ b/patches/src/main/resources/addresources/values-sv-rSE/music/strings.xml @@ -462,7 +462,6 @@ För att säkerställa att scrobbling synkroniseras under bakgrundsuppspelning, Visning Synkronisering Panel - Språk AI-slutpunkt Alla leverantörer inaktiverade - inbyggd text och inbäddad text Återställ leverantörsordningen? @@ -616,33 +615,50 @@ Texterna kommer från tredjepartstjänster som inte är anslutna till, sponsrade Dölj kreditraden som visas under texten. Växla mellan översättning och romanisering PÅ placerar romaniseringen under texten och översättningen ovanför; AV behåller romaniseringen ovanför och översättningen nedanför. - OpenAI-kompatibelt API - AI-översättning - Föredrar den översättning som leverantören skickar med, sedan din OpenAI-kompatibla modell och sist Google Translate - AI-romanisering - Föredrar den romanisering som leverantören skickar med, sedan din OpenAI-kompatibla modell och sist Google Translate + Översättning + Romanisering + Översättningsleverantör + Google (standard, gratis) + OpenAI-kompatibel (anpassad) + Leverantör för romanisering + Google (standard) + Lokal / offline-ordbok + OpenAI-kompatibel (anpassad) AI-sektion Används endast när leverantören inte skickar med sektionsetiketter som Chorus eller Verse, vilket din OpenAI-kompatibla modell sedan detekterar - AI-inställningar - Konfigurera OpenAI-kompatibla API-slutpunkten för AI-driven textöversättning, romanisering och sektion - API-slutpunktens URL (t.ex. https://text.pollinations.ai/openai) - API-nyckel (lämna tom för anonym åtkomst) - Modellnamn (t.ex. openai-fast, gpt-4o-mini) + AI & molnslutpunkter + Konfigurera API-nycklar, modeller, slutpunkter och promptmallar för AI-leverantörer. + API-slutpunkts-URL (t.ex. https://api.openai.com/v1/chat/completions) Mall för prompt Platshållare: {language}, {task}, {title}, {artist}, {count}, {lines} Prompt sparad Prompt sparad, men AI-slutpunkten är fortfarande inte användbar. Konfigurera den under AI-inställningar först Prompt återställd - Extra begäran kropp (JSON, sammanslagen i varje AI-begäran) Inte giltig JSON. Rätta det eller rensa fältet innan du sparar - AI-inställningar sparade - Kunde inte ansluta till AI-slutpunkten. Kontrollera dina inställningar - AI-inställningar återställda - Modell %1$s konfigurerad - Bas-URL kan inte vara tom + Inställningar för översättning & romanisering sparades + Inställningarna återställdes till standard Bas-URL måste börja med http:// eller https:// - Modellnamn kan inte vara tomt + Testa anslutning + Konfigurera slutpunkter + Visa alla slutpunkter Validerar anslutning… + Översättningsleverantör + Leverantör för romanisering + Hämta modeller + Hämtar modeller… + Välj en modell + Det gick inte att hämta modeller. Kontrollera API-nyckeln eller anslutningen. + Inställningar för Gemini API + OpenAI-kompatibel (egen slutpunkt) + Inställningar för DeepL API + API-nyckel + Modell + Bas-URL + API-nyckel (valfri) + Översättningsprompt + Extra anförandekropp (valfri JSON) + Autentiseringsnyckel (Free eller Pro) + Konfigurera API-nycklar, modeller, slutpunkter och prompter 8.23.51 – Inaktivera ikoner i fet stil 9.13.51 - Återställ spelarens flikar Upp nästa, Sångtext och Relaterade diff --git a/patches/src/main/resources/addresources/values-th-rTH/music/strings.xml b/patches/src/main/resources/addresources/values-th-rTH/music/strings.xml index 2aac986900..e874aed6e4 100644 --- a/patches/src/main/resources/addresources/values-th-rTH/music/strings.xml +++ b/patches/src/main/resources/addresources/values-th-rTH/music/strings.xml @@ -462,7 +462,6 @@ Second \"item\" text" แสดง การซิงโครไนซ์ แผง - ภาษา จุดปลายทาง AI ปิดใช้งานผู้ให้บริการทั้งหมด — เนื้อเพลงในตัวและเนื้อเพลงฝังตัว รีเซ็ตผู้ให้บริการทั้งหมดหรือไม่ @@ -619,33 +618,50 @@ Second \"item\" text" ซ่อนบรรทัดเครดิตที่แสดงใต้เนื้อเพลง สลับคำแปลและการโรมาไนซ์ การเปิดจะวางการโรมาไนซ์ไว้ใต้เนื้อเพลงและคำแปลไว้ด้านบน การปิดจะให้การโรมาไนซ์อยู่ด้านบนและคำแปลอยู่ด้านล่าง - API ที่รองรับ OpenAI - การแปลด้วย AI - จะเลือกใช้คำแปลที่ผู้ให้บริการจัดส่งมาให้ก่อน จากนั้นจึงใช้โมเดลที่รองรับ OpenAI ของคุณ และใช้ Google Translate เป็นลำดับสุดท้าย - การถอดอักษรโรมันด้วย AI - จะเลือกใช้การถอดอักษรโรมันที่ผู้ให้บริการส่งมาให้ก่อน จากนั้นจึงเป็นโมเดลที่เข้ากันได้กับ OpenAI ของคุณ และสุดท้ายคือ Google Translate + การแปล + การถอดเป็นอักษรโรมัน + ผู้ให้บริการการแปล + Google (ค่าเริ่มต้น, ฟรี) + OpenAI Compatible (กำหนดเอง) + ผู้ให้บริการถอดเป็นอักษรโรมัน + Google (ค่าเริ่มต้น) + พจนานุกรมในเครื่อง / ออฟไลน์ + OpenAI Compatible (กำหนดเอง) ส่วน AI ใช้เมื่อผู้ให้บริการไม่ได้ส่งป้ายกำกับส่วนต่างๆ เช่น Chorus หรือ Verse ซึ่งโมเดลที่เข้ากันได้กับ OpenAI ของคุณจะตรวจจับให้แทน - การตั้งค่า AI - กำหนดค่าจุดปลายทาง API ที่เข้ากันได้กับ OpenAI สำหรับการแปลเนื้อเพลง การถอดเป็นอักษรโรมัน และการแบ่งส่วนเนื้อเพลงด้วย AI - URL จุดปลายทาง API (เช่น https://text.pollinations.ai/openai) - คีย์ API (เว้นว่างไว้สำหรับการเข้าถึงแบบไม่ระบุตัวตน) - ชื่อโมเดล (เช่น openai-fast, gpt-4o-mini) + AI & จุดปลายทางคลาวด์ + กำหนดค่าคีย์ API, โมเดล, จุดปลายทาง และเทมเพลตคำสั่งสำหรับผู้ให้บริการ AI + URL จุดปลายทาง API (เช่น https://api.openai.com/v1/chat/completions) เทมเพลตคำสั่ง ตัวแทน: {language}, {task}, {title}, {artist}, {count}, {lines} บันทึกคำสั่งแล้ว บันทึก Prompt แล้ว แต่จุดปลายทาง AI ยังใช้งานไม่ได้ โปรดกำหนดค่าภายใต้การตั้งค่า AI ก่อน รีเซ็ตคำสั่งแล้ว - ร่างกายคำขอเพิ่มเติม (JSON ผสมเข้ากับคำขอ AI ทุกครั้ง) ไม่ใช่ JSON ที่ถูกต้อง แก้ไขหรือล้างฟิลด์ก่อนบันทึก - บันทึกการตั้งค่า AI แล้ว - ไม่สามารถเชื่อมต่อกับจุดปลายทาง AI ได้ โปรดตรวจสอบการตั้งค่าของคุณ - รีเซ็ตการตั้งค่า AI แล้ว - กำหนดค่าโมเดล %1$s แล้ว - URL ฐานต้องไม่ว่างเปล่า + บันทึกการตั้งค่าการแปล & การถอดเป็นอักษรโรมันแล้ว + รีเซ็ตการตั้งค่าเป็นค่าเริ่มต้นแล้ว URL ฐานต้องเริ่มต้นด้วย http:// หรือ https:// - ชื่อโมเดลต้องไม่ว่างเปล่า + ทดสอบการเชื่อมต่อ + กำหนดค่าจุดปลายทาง + แสดงจุดปลายทางทั้งหมด กำลังตรวจสอบการเชื่อมต่อ… + ผู้ให้บริการแปลภาษา + ผู้ให้บริการถอดเป็นอักษรโรมัน + ดึงข้อมูลโมเดล + กำลังดึงข้อมูลโมเดล… + เลือกโมเดล + ดึงข้อมูลโมเดลล้มเหลว โปรดตรวจสอบ API key หรือการเชื่อมต่อ + การตั้งค่า Gemini API + OpenAI Compatible (Custom Endpoint) + การตั้งค่า DeepL API + คีย์ API + โมเดล + Base URL + API key (ไม่บังคับ) + คำสั่งสำหรับการแปล + ส่วนขยายของ request body (JSON ไม่บังคับ) + คีย์การยืนยันตัวตน (Free หรือ Pro) + กำหนดค่า API key, โมเดล, จุดปลายทาง และคำสั่ง 8.23.51 - ปิดใช้งานไอคอนตัวหนา 9.13.51 - กู้คืนแท็บถัดไป เนื้อเพลง และที่เกี่ยวข้องของเครื่องเล่น diff --git a/patches/src/main/resources/addresources/values-tr-rTR/music/strings.xml b/patches/src/main/resources/addresources/values-tr-rTR/music/strings.xml index 9609851208..286e9852ce 100644 --- a/patches/src/main/resources/addresources/values-tr-rTR/music/strings.xml +++ b/patches/src/main/resources/addresources/values-tr-rTR/music/strings.xml @@ -462,7 +462,6 @@ Scrobbling'in arka planda çalma sırasında senkronize olduğundan emin olmak i Görünüm Senkronizasyon Panel - Diller AI uç noktası Tüm sağlayıcılar devre dışı - yerleşik sözler ve gömülü sözler Sağlayıcı sırası sıfırlansın mı? @@ -619,33 +618,50 @@ Gerekli çerezler: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPISI Şarkı sözlerinin altında gösterilen künye satırını gizle Çeviri ve okunuşun yerini değiştir AÇIK olduğunda okunuşu şarkı sözlerinin altına ve çeviriyi üstüne yerleştirir; KAPALI olduğunda okunuşu üstte ve çeviriyi altta tutar - OpenAI uyumlu API - Yapay Zeka Çevirisi - Önce sağlayıcının sunduğu çeviriyi, ardından OpenAI uyumlu modelinizi ve son olarak Google Çeviri\'yi tercih eder - Yapay Zeka Romanizasyonu - Önce sağlayıcının gönderdiği romanizasyonu, ardından OpenAI uyumlu modelinizi ve son olarak Google Çeviri\'yi tercih eder + Çeviri + Romanizasyon + Çeviri sağlayıcısı + Google (Varsayılan, Ücretsiz) + OpenAI Uyumlu (Özel) + Romanizasyon sağlayıcısı + Google (Varsayılan) + Yerel / Çevrimdışı Sözlük + OpenAI Uyumlu (Özel) Yapay Zeka Bölümü Yalnızca sağlayıcı Nakarat veya Kıta gibi bölüm etiketleri göndermediğinde ve bunları OpenAI uyumlu modeliniz tespit ettiğinde kullanılır - YZ Ayarları - AI destekli şarkı sözü çevirisi, romanizasyon ve bölümlendirme için OpenAI uyumlu API uç noktasını yapılandırın - API uç noktası URL’si (ör. https://text.pollinations.ai/openai) - API anahtarı (anonim erişim için boş bırakın) - Model adı (ör. openai-fast, gpt-4o-mini) + AI & Bulut Uç Noktaları + Yapay zeka sağlayıcıları için API anahtarlarını, modelleri, uç noktaları ve istem şablonlarını yapılandırın. + API uç nokta URL\'si (örneğin https://api.openai.com/v1/chat/completions) İstem Şablonu Yer tutucular: {language}, {task}, {title}, {artist}, {count}, {lines} İstem kaydedildi İstem kaydedildi, ancak AI uç noktası henüz kullanılabilir değil. Önce AI ayarlarından yapılandırın. İstem sıfırlandı - Ekstra istek gövdesi (JSON, her AI isteğine eklenir) Geçerli JSON değil. Düzeltin veya kaydetmeden önce alanı temizleyin - YZ ayarları kaydedildi - AI uç noktasına bağlanılamadı. Lütfen ayarlarınızı kontrol edin - YZ ayarları sıfırlandı - %1$s modeli yapılandırıldı - Temel URL boş olamaz + Çeviri & Romanizasyon ayarları kaydedildi + Ayarlar varsayılanlara sıfırlandı Temel URL http:// veya https:// ile başlamalıdır - Model adı boş olamaz + Bağlantıyı Test Et + Uç Noktaları Yapılandır + Tüm Uç Noktaları Göster Bağlantı doğrulanıyor… + Çeviri Sağlayıcısı + Romanizasyon Sağlayıcısı + Modelleri Getir + Modeller getiriliyor… + Bir Model Seçin + Modeller alınamadı. API anahtarını veya bağlantıyı kontrol edin. + Gemini API Ayarları + OpenAI Uyumlu (Özel Uç Nokta) + DeepL API Ayarları + API anahtarı + Model + Temel URL + API anahtarı (isteğe bağlı) + Çeviri istemi + Ek istek gövdesi (isteğe bağlı JSON) + Kimlik doğrulama anahtarı (Ücretsiz veya Pro) + API anahtarlarını, modelleri, uç noktaları ve istemleri yapılandırın 8.23.51 - Kalın simgeleri devre dışı bırak 9.13.51 - Çalardaki Sıradaki, Şarkı Sözleri ve İlgili sekmelerini geri yükle diff --git a/patches/src/main/resources/addresources/values-uk-rUA/music/strings.xml b/patches/src/main/resources/addresources/values-uk-rUA/music/strings.xml index ebd7326aab..46d4bd33f3 100644 --- a/patches/src/main/resources/addresources/values-uk-rUA/music/strings.xml +++ b/patches/src/main/resources/addresources/values-uk-rUA/music/strings.xml @@ -462,7 +462,6 @@ Second \"item\" text" Відображення Синхронізація Панель - Мови Кінцева точка AI Усі постачальники вимкнено, використовуються вбудовані та вбудовані в файл тексти пісень Скинути порядок постачальників? @@ -621,33 +620,50 @@ Second \"item\" text" Приховати рядок з інформацією про авторів, що показується під текстом пісні Поміняти місцями переклад і романізацію Увімкнено - романізація під текстом, переклад над текстом; вимкнено - романізація над текстом, переклад під текстом - API, сумісний з OpenAI - AI-переклад - Надає перевагу перекладу, який постачає провайдер, потім вашій моделі, сумісній з OpenAI, і Google Translate наостанок - AI-романізація - Надає перевагу романізації, яку постачає провайдер, потім вашій моделі, сумісній з OpenAI, і Google Translate наостанок + Переклад + Романізація + Постачальник перекладу + Google (типовий, безкоштовний) + Сумісний з OpenAI (власний) + Постачальник романизації + Google (типовий) + Локальний / офлайн-словник + Сумісний з OpenAI (власний) Розділ AI Використовується лише тоді, коли постачальник не надає позначок розділів, таких як \"Приспів\" або \"Куплет\", які тоді визначає ваша модель, сумісна з OpenAI - Налаштування AI - Налаштуйте кінцеву точку API, сумісну з OpenAI, для AI-перекладу текстів пісень, романізації та розділів - URL-адреса кінцевої точки API (наприклад, https://text.pollinations.ai/openai) - Ключ API (залиште порожнім для анонімного доступу) - Назва моделі (наприклад, openai-fast, gpt-4o-mini) + AI & хмарні кінцеві точки + Налаштуйте API-ключі, моделі, кінцеві точки та шаблони підказок для AI-постачальників. + URL-адреса кінцевої точки API (наприклад, https://api.openai.com/v1/chat/completions) Шаблон запиту Заповнювачі: {language}, {task}, {title}, {artist}, {count}, {lines} Запит збережено Запит збережено, але кінцева точка AI ще непридатна до використання. Спочатку налаштуйте її в розділі \"Налаштування AI\" Запит скинуто - Додаткове тіло запиту (JSON, додається до кожного запиту ШІ) Схоже, ти вставив уже готовий переклад замість англійського оригіналу. Надішли, будь ласка, англійський рядок, і я перекладу. - Налаштування AI збережено - Не вдалося підключитися до кінцевої точки AI. Будь ласка, перевірте налаштування - Налаштування AI скинуто - Модель %1$s налаштовано - Базова URL-адреса не може бути порожньою + Налаштування перекладу & романизації збережено + Налаштування скинуто до типових Базова URL-адреса має починатися з http:// або https:// - Назва моделі не може бути порожньою + Перевірити з\'єднання + Налаштувати кінцеві точки + Показати всі кінцеві точки Перевірка з\'єднання… + Постачальник перекладу + Постачальник романизації + Отримати моделі + Отримання моделей… + Виберіть модель + Не вдалося отримати моделі. Перевірте API-ключ або з’єднання. + Налаштування Gemini API + Сумісність з OpenAI (власна кінцева точка) + Налаштування DeepL API + API-ключ + Модель + Базовий URL + API-ключ (необов\'язково) + Промпт для перекладу + Додатковий тіло запиту (необов\'язковий JSON) + Ключ автентифікації (Free або Pro) + Налаштуйте API-ключі, моделі, кінцеві точки та промпти 8.23.51 - Вимкнути товсті іконки 9.13.51 - Відновити вкладки \"Далі\", \"Текст\" та \"Схожі\" в плеєрі diff --git a/patches/src/main/resources/addresources/values-uk-rUA/youtube/strings.xml b/patches/src/main/resources/addresources/values-uk-rUA/youtube/strings.xml index bb23dabe22..d73fdbdc67 100644 --- a/patches/src/main/resources/addresources/values-uk-rUA/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-uk-rUA/youtube/strings.xml @@ -456,12 +456,12 @@ Second \"item\" text" Показувати відсоток лайків на мініатюрах стрічки Пошук у коментарях - Натисніть кнопку пошуку в коментарях, щоб показати лише завантажені коментарі, які містять усі шукані слова. Щоб знайти фразу, візьміть слова в лапки + Натисніть кнопку пошуку в коментарях, щоб показати лише завантажені коментарі, які містять усі шукані слова. Візьміть слова в лапки, щоб шукати фразу Пошук у коментарях Слова для пошуку Пошук Очистити - Змініть порядок сортування або відкрийте коментарі знову, щоб здійснити пошук + Змініть порядок сортування або відкрийте коментарі знову, щоб здійснити в них пошук Пошук очищено. Змініть порядок сортування або відкрийте коментарі знову, щоб показати всі коментарі Вимкнути вікно \"Увійти на телевізорі\" @@ -848,7 +848,7 @@ Second \"item\" text" Вимкнути кнопки навігації без підписів Показує кнопки навігації з підписами, якщо YouTube надсилає їх без підписів Вимкнути вкладки навігації на панелі інструментів - Показує «Підписки» на навігаційній панелі, якщо YouTube перенесе їх у вкладку у верхній частині Головної сторінки + Показує \"Підписки\" на панелі навігації, якщо YouTube переносить їх на вкладку вгорі \"Головної\" Увімкнути анімації панелі навігації Анімує переходи між вкладками Вимкнути автоприховування панелі навігації diff --git a/patches/src/main/resources/addresources/values-vi-rVN/music/strings.xml b/patches/src/main/resources/addresources/values-vi-rVN/music/strings.xml index 45bd56b51b..db1a1642dc 100644 --- a/patches/src/main/resources/addresources/values-vi-rVN/music/strings.xml +++ b/patches/src/main/resources/addresources/values-vi-rVN/music/strings.xml @@ -462,7 +462,6 @@ Phát SABR (đã tắt giả lập luồng video)" Hiển thị Đồng bộ Bảng - Ngôn ngữ Điểm cuối AI Tất cả nhà cung cấp bị vô hiệu hóa - lời ca tích hợp và lời nhúng Đặt lại thứ tự nguồn cung cấp? @@ -612,33 +611,50 @@ Lời bài hát đến từ các dịch vụ bên thứ ba không liên kết, t Ẩn dòng ghi công hiển thị bên dưới lời bài hát Hoán đổi bản dịch và chuyển tự BẬT đặt chuyển tự bên dưới lời bài hát và bản dịch bên trên; TẮT giữ chuyển tự bên trên và bản dịch bên dưới - API tương thích OpenAI - Dịch thuật AI - Ưu tiên bản dịch đi kèm từ nhà cung cấp, sau đó là mô hình tương thích OpenAI của bạn, và cuối cùng là Google Dịch - La Mã hóa bằng AI - Ưu tiên phiên âm La-tinh từ nhà cung cấp, sau đó là mô hình tương thích OpenAI của bạn, và cuối cùng là Google Dịch + Dịch thuật + Phiên âm Latin + Dịch vụ dịch thuật + Google (Mặc định, Miễn phí) + Tương thích OpenAI (Tùy chỉnh) + Nhà cung cấp phiên âm La-tinh + Google (Mặc định) + Từ điển Cục bộ / Ngoại tuyến + Tương thích OpenAI (Tùy chỉnh) Phần AI Chỉ được sử dụng khi nhà cung cấp không gửi nhãn phần như Chorus hoặc Verse, khi đó mô hình tương thích OpenAI của bạn sẽ phát hiện các phần này - Cài đặt AI - Cấu hình điểm cuối API tương thích OpenAI để dịch, romanize và phân đoạn lời bài hát dựa trên AI - URL điểm cuối API (ví dụ: https://text.pollinations.ai/openai) - Khóa API (để trống để truy cập ẩn danh) - Tên mô hình (ví dụ: openai-fast, gpt-4o-mini) + Điểm cuối AI & Đám mây + Cấu hình khóa API, mô hình, điểm cuối và mẫu gợi ý cho các nhà cung cấp AI. + URL điểm cuối API (ví dụ: https://api.openai.com/v1/chat/completions) Mẫu Prompt Trình giữ chỗ: {language}, {task}, {title}, {artist}, {count}, {lines} Đã lưu prompt Đã lưu lời nhắc, nhưng điểm cuối AI vẫn chưa thể sử dụng. Hãy cấu hình nó trong Cài đặt AI trước Đã đặt lại prompt - Nội dung yêu cầu bổ sung (JSON, được hợp nhất vào mọi yêu cầu AI) Không phải JSON hợp lệ. Hãy sửa hoặc xóa trường này trước khi lưu - Đã lưu cài đặt AI - Không thể kết nối với điểm cuối AI. Vui lòng kiểm tra cài đặt của bạn - Đã đặt lại cài đặt AI - Mô hình %1$s đã được cấu hình - URL cơ sở không được để trống + Đã lưu cài đặt Dịch thuật & Phiên âm + Đặt lại cài đặt về mặc định URL cơ sở phải bắt đầu bằng http:// hoặc https:// - Tên mô hình không được để trống + Kiểm tra Kết nối + Cấu hình Điểm cuối + Hiển thị Tất cả Điểm cuối Đang kiểm tra kết nối… + Dịch vụ dịch thuật + Nhà cung cấp La-tinh hóa + Tải các mô hình + Đang tải các mô hình… + Chọn một mô hình + Tải các mô hình thất bại. Hãy kiểm tra khóa API hoặc kết nối. + Cài đặt Gemini API + Tương thích OpenAI (Điểm cuối tùy chỉnh) + Cài đặt DeepL API + Khóa API + Mẫu + URL cơ sở + Khóa API (tùy chọn) + Câu lệnh dịch thuật + Thân yêu cầu bổ sung (JSON tùy chọn) + Khóa xác thực (Miễn phí hoặc Pro) + Cấu hình các khóa API, mô hình, điểm cuối và câu lệnh 8.23.51 - Vô hiệu hóa biểu tượng đậm 9.13.51 - Khôi phục các tab Tiếp theo, Lời nhạc và Liên quan trong trình phát diff --git a/patches/src/main/resources/addresources/values-zh-rCN/music/strings.xml b/patches/src/main/resources/addresources/values-zh-rCN/music/strings.xml index e864cc7e55..6748b2e23a 100644 --- a/patches/src/main/resources/addresources/values-zh-rCN/music/strings.xml +++ b/patches/src/main/resources/addresources/values-zh-rCN/music/strings.xml @@ -462,7 +462,6 @@ SABR 播放(模拟视频流已关闭)" 显示 同步 面板 - 语言 AI 端点 所有提供商已禁用 - 内置歌词和嵌入歌词 重置提供程序顺序? @@ -621,33 +620,50 @@ SABR 播放(模拟视频流已关闭)" 隐藏显示在歌词下方的署名行 交换翻译和音译 开启时,音译在歌词下方、翻译在歌词上方;关闭时,音译在歌词上方、翻译在歌词下方 - OpenAI 兼容 API - AI 翻译 - 优先使用提供商自带的翻译,然后是您兼容 OpenAI 的模型,最后是 Google 翻译 - AI 罗马化 - 优先使用提供商提供的罗马化方案,其次是您的 OpenAI 兼容模型,最后是 Google 翻译 + 翻译 + 罗马化 + 翻译提供商 + Google (默认,免费) + OpenAI 兼容 (自定义) + 罗马化提供者 + Google (默认) + 本地 / 离线词典 + OpenAI 兼容 (自定义) AI 段落识别 仅在提供商未发送如副歌或主歌等段落标签,且由您的 OpenAI 兼容模型检测到时使用 - AI 设置 - 配置用于 AI 驱动的歌词翻译、罗马化和分段的兼容 OpenAI API 端点 - API 端点 URL(例如 https://text.pollinations.ai/openai) - API 密钥(留空以匿名访问) - 模型名称(例如 openai-fast、gpt-4o-mini) + AI & 云端端点 + 为 AI 提供者配置 API 密钥、模型、端点和提示词模板。 + API 端点 URL (例如 https://api.openai.com/v1/chat/completions) 提示词模板 占位符:{language}, {task}, {title}, {artist}, {count}, {lines} 提示词已保存 提示词已保存,但 AI 端点尚不可用。请先在 AI 设置中进行配置 提示词已重置 - 额外请求正文(JSON,合并到每个 AI 请求中) 无效的 JSON。保存前请修复或清空该字段 - AI 设置已保存 - 无法连接到 AI 端点。请检查您的设置 - AI 设置已重置 - 模型 %1$s 已配置 - 基础 URL 不能为空 + 翻译 & 罗马化设置已保存 + 设置已重置为默认 基础 URL 必须以 http:// 或 https:// 开头 - 模型名称不能为空 + 测试连接 + 配置端点 + 显示所有端点 正在验证连接… + 翻译提供商 + 罗马化提供商 + 获取模型 + 正在获取模型…… + 选择模型 + 获取模型失败。请检查 API 密钥或网络连接。 + Gemini API 设置 + OpenAI 兼容 (自定义端点) + DeepL API 设置 + API 密钥 + 型号 + 基础 URL + API 密钥 (可选) + 翻译提示词 + 额外请求正文(可选 JSON) + 身份验证密钥(免费版或专业版) + 配置 API 密钥、模型、端点和提示词 8.23.51 - 禁用粗体图标 9.13.51 - 恢复播放器的「接下来播放」「歌词」和「相关」标签页 diff --git a/patches/src/main/resources/addresources/values-zh-rTW/music/strings.xml b/patches/src/main/resources/addresources/values-zh-rTW/music/strings.xml index 5f4f83b606..bb6a017895 100644 --- a/patches/src/main/resources/addresources/values-zh-rTW/music/strings.xml +++ b/patches/src/main/resources/addresources/values-zh-rTW/music/strings.xml @@ -462,7 +462,6 @@ SABR 播放(影片串流模擬已關閉)" 顯示 同步 面板 - 語言 AI端點 所有提供程序已停用 - 內建歌詞和嵌入式歌詞 重設提供者順序? @@ -621,33 +620,50 @@ SABR 播放(影片串流模擬已關閉)" 在歌詞畫面中隱藏來源標示 交換翻譯和羅馬拼音 開啟時將羅馬拼音放在歌詞下面,翻譯放在上面; 關閉時維持羅馬拼音放在上面,翻譯放在下面 - OpenAI 相容 API - AI 翻譯 - 優先使用提供者隨附的翻譯,接著使用你的 OpenAI 相容模型,最後使用 Google 翻譯 - AI 羅馬拼音 - 優先使用供應商提供的羅馬拼音方案,其次是你的 OpenAI 相容模型,最後才是 Google 翻譯 + 翻譯 + 羅馬拼音 + 翻譯提供者 + Google (預設,免費) + OpenAI相容 (自訂) + 羅馬拼音來源 + Google (預設) + 本機/離線字典 + OpenAI相容 (自訂) AI 段落 僅在供應商未提供如副歌或主歌等段落標記,而由你的 OpenAI 相容模型偵測時使用 - AI 設定 - 配置 OpenAI 相容 API 端點,用於 AI 驅動的歌詞翻譯、羅馬拼音和段落標記 - API 端點 URL(例如 https://text.pollinations.ai/openai) - API 金鑰(留空以匿名存取) - 模型名稱(例如 openai-fast、gpt-4o-mini) + AI及雲端終端 + 設定API金鑰、模型、終端及AI供應商的提示詞。 + API終端網址 (例如:https://api.openai.com/v1/chat/completions) 提示詞範本 佔位符:{language}, {task}, {title}, {artist}, {count}, {lines} 已儲存提示詞 已儲存提示,但 AI 端點尚無法使用。請先在「AI 設定」中進行配置 已重設提示詞 - 額外的請求主體 (JSON,合併至每個 AI 請求中) 無效的 JSON。請修正或清空欄位後再儲存 - 已儲存 AI 設定 - 無法連線至 AI 端點。請檢查您的設定 - 已重設 AI 設定 - 已設定 %1$s 模型 - Base URL 不能為空 + 已儲存翻譯及羅馬拼音設定 + 設定已重設為預設值 Base URL 必須以 http:// 或 https:// 開頭 - 模型名稱不能為空 + 測試連線 + 設定終端 + 顯示所有終端 正在驗證連線… + 翻譯來源 + 羅馬拼音來源 + 獲取模型 + 正在獲取模型… + 選擇模型 + 無法獲取模型。請檢查API金鑰或連線狀態。 + Gemini API設定 + OpenAI相容 (自訂終端) + DeepL API設定 + API 金鑰 + 模型 + 基礎網址 + API金鑰(選填) + 翻譯提示詞 + 額外請求主體(選填JSON) + 驗證金鑰(免費版或專業版) + 設定API金鑰、模型、終端及提示詞 8.23.51 - 停用粗體圖示 9.13.51 - 恢復播放器「接下來播放」、「歌詞」和「相關」標籤 From 97f1f2df9a2ce61384df4327f6743953c1ae598e Mon Sep 17 00:00:00 2001 From: 0xrxL <127248639+0xrxL@users.noreply.github.com> Date: Sat, 24 Oct 2026 15:29:03 +0200 Subject: [PATCH 039/189] fix(YouTube - Feed card layout): Shorts shelves change width and corners when only a last row with one Short is in the list --- .../youtube/patches/FeedCardLayoutPatch.java | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FeedCardLayoutPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FeedCardLayoutPatch.java index bbd8421bb3..7a66a7072a 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FeedCardLayoutPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FeedCardLayoutPatch.java @@ -1494,7 +1494,7 @@ && getContentBounds(last, content)) { if (item.getHeight() > 0 && isShortsRow(item)) { View above = findItemAbove(current, item); if (above == null || !isShortsRow(above)) { - drawShortsShelfCard(current, getShortsShelfHeader(above), item, null); + drawShortsShelfCard(current, above, item, null); } } } @@ -1619,8 +1619,7 @@ && getContentBounds(last, content)) { if (isShortsRow(item)) { View above = findItemAbove(list, item); if (above == null || !isShortsRow(above)) { - drawShortsShelfCard(list, getShortsShelfHeader(above), - item, canvas); + drawShortsShelfCard(list, above, item, canvas); } continue; } @@ -1690,9 +1689,12 @@ private static boolean isVideoShelfHeader(View item) { /** * The Shorts shelf is one card, with the header and the rows of Shorts below it. * Only the outer corners of the card are rounded, and the Shorts fill the card. + * + * @param above The list item above the first row of Shorts, or null if the item above it is not in the list. */ - private static void drawShortsShelfCard(RecyclerView list, @Nullable View header, View firstRow, + private static void drawShortsShelfCard(RecyclerView list, @Nullable View above, View firstRow, @Nullable Canvas canvas) { + View header = getShortsShelfHeader(above); // The rows of Shorts are the list items below the header, which can be scrolled out of the list. List rows = new ArrayList<>(); View item = firstRow; @@ -1702,6 +1704,9 @@ private static void drawShortsShelfCard(RecyclerView list, @Nullable View header } // The card is as wide as the Shorts, and ends at the bottom of the last row of Shorts. + // The Shorts are laid out in columns centered in the row, and the last row can have fewer Shorts, + // such as one Short on the left. The card is centered in the row too, so it keeps its width + // while only the last row is in the list. int left = Integer.MAX_VALUE; int right = Integer.MIN_VALUE; final float top = header != null ? header.getY() : firstRow.getY(); @@ -1716,8 +1721,9 @@ private static void drawShortsShelfCard(RecyclerView list, @Nullable View header for (View cell : cells) { cell.getLocationOnScreen(cellLocation); final int cellLeft = cellLocation[0] - rowLocation[0]; - left = Math.min(left, cellLeft); - right = Math.max(right, cellLeft + cell.getWidth()); + final int cellRight = cellLeft + cell.getWidth(); + left = Math.min(left, Math.min(cellLeft, row.getWidth() - cellRight)); + right = Math.max(right, Math.max(cellRight, row.getWidth() - cellLeft)); bottom = Math.max(bottom, row.getY() + cellLocation[1] - rowLocation[1] + cell.getHeight()); } rowCells.add(cells); @@ -1730,8 +1736,11 @@ private static void drawShortsShelfCard(RecyclerView list, @Nullable View header // The outer Shorts have rounded outer corners: the top corners in the first row, // also below the header, and the bottom corners of the card in the last row. + // The first row in the list is the first row of the shelf only if an item is above it, + // or if the list is scrolled to the top. Otherwise the rows above it are scrolled out of the list. + final boolean firstRowOfShelf = above != null || !list.canScrollVertically(-1); for (int i = 0; i < rows.size(); i++) { - final boolean topRow = i == 0; + final boolean topRow = i == 0 && firstRowOfShelf; final boolean bottomRow = i == rows.size() - 1; rows.get(i).getLocationOnScreen(rowLocation); for (View cell : rowCells.get(i)) { From 4ac36138ebc93d641e6d54ad497c9ba0242d44f1 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Sat, 24 Oct 2026 13:39:27 +0000 Subject: [PATCH 040/189] chore: Release v1.49.0-dev.4 [skip ci] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## [1.49.0-dev.4](https://github.com/MorpheApp/morphe-patches/compare/v1.49.0-dev.3...v1.49.0-dev.4) (2026-10-24) ### 🐛 Bug Fixes * **YouTube - Ambient mode:** Ambient mode is not disabled with 21.43 ([aeb3012](https://github.com/MorpheApp/morphe-patches/commit/aeb3012a1d4fc42e9cb0e80e1cc139f24d445fb7)) * **YouTube - Feed card layout:** Shorts shelves change width and corners when only a last row with one Short is in the list ([97f1f2d](https://github.com/MorpheApp/morphe-patches/commit/97f1f2df9a2ce61384df4327f6743953c1ae598e)) * **YouTube - Hide status bar:** Status bar scroll color and translucent navigation bar do not follow the hidden status bar before the edge to edge flag is read ([a9e6562](https://github.com/MorpheApp/morphe-patches/commit/a9e6562b2a4dbc5d57f8cf65a205206a800d192a)) * **YouTube - Miniplayer:** "Disable resuming miniplayer" may not work with latest experimental app target ([8554412](https://github.com/MorpheApp/morphe-patches/commit/85544128284a29522310d43ed6bfb3e99706e9d4)) * **YouTube:** Fingerprints are not found on some targets between 21.04 and 21.35 ([52fa134](https://github.com/MorpheApp/morphe-patches/commit/52fa13489d6a97172d29cf5ff639d958506251ca)) --- CHANGELOG.md | 10 ++++++++++ README.md | 2 +- gradle.properties | 2 +- patches-bundle.json | 8 ++++---- patches-list.json | 2 +- 5 files changed, 17 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 811757e9ed..7be7d8ea30 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +## [1.49.0-dev.4](https://github.com/MorpheApp/morphe-patches/compare/v1.49.0-dev.3...v1.49.0-dev.4) (2026-10-24) + +### 🐛 Bug Fixes + +* **YouTube - Ambient mode:** Ambient mode is not disabled with 21.43 ([aeb3012](https://github.com/MorpheApp/morphe-patches/commit/aeb3012a1d4fc42e9cb0e80e1cc139f24d445fb7)) +* **YouTube - Feed card layout:** Shorts shelves change width and corners when only a last row with one Short is in the list ([97f1f2d](https://github.com/MorpheApp/morphe-patches/commit/97f1f2df9a2ce61384df4327f6743953c1ae598e)) +* **YouTube - Hide status bar:** Status bar scroll color and translucent navigation bar do not follow the hidden status bar before the edge to edge flag is read ([a9e6562](https://github.com/MorpheApp/morphe-patches/commit/a9e6562b2a4dbc5d57f8cf65a205206a800d192a)) +* **YouTube - Miniplayer:** "Disable resuming miniplayer" may not work with latest experimental app target ([8554412](https://github.com/MorpheApp/morphe-patches/commit/85544128284a29522310d43ed6bfb3e99706e9d4)) +* **YouTube:** Fingerprints are not found on some targets between 21.04 and 21.35 ([52fa134](https://github.com/MorpheApp/morphe-patches/commit/52fa13489d6a97172d29cf5ff639d958506251ca)) + ## [1.49.0-dev.3](https://github.com/MorpheApp/morphe-patches/compare/v1.49.0-dev.2...v1.49.0-dev.3) (2026-10-24) ### 🐛 Bug Fixes diff --git a/README.md b/README.md index 56101aa7b1..6f1ca8ef1e 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ All modifications made by Morphe, along with their dates, can be found in the Gi ## 🩹 Patches list -> **[v1.49.0-dev.3](https://github.com/MorpheApp/morphe-patches/releases/tag/v1.49.0-dev.3)**  •  `dev`  •  191 patches total +> **[v1.49.0-dev.4](https://github.com/MorpheApp/morphe-patches/releases/tag/v1.49.0-dev.4)**  •  `dev`  •  191 patches total
📦 YouTube  •  106 patches
diff --git a/gradle.properties b/gradle.properties index 4197f97651..6608ea3568 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,4 +3,4 @@ org.gradle.jvmargs = -Xms512M -Xmx2048M org.gradle.parallel = true android.useAndroidX = true kotlin.code.style = official -version = 1.49.0-dev.3 +version = 1.49.0-dev.4 diff --git a/patches-bundle.json b/patches-bundle.json index c9c8b6036a..c4e408cd80 100644 --- a/patches-bundle.json +++ b/patches-bundle.json @@ -1,7 +1,7 @@ { - "created_at": "2026-10-24T09:53:04", - "description": "## [1.49.0-dev.3](https://github.com/MorpheApp/morphe-patches/compare/v1.49.0-dev.2...v1.49.0-dev.3) (2026-10-24)\n\n### 🐛 Bug Fixes\n\n* **Reddit - Redgifs audio:** Error toast is shown when network request fails ([6707f0e](https://github.com/MorpheApp/morphe-patches/commit/6707f0e1215ba3620586b95e5c0f8b8576339a86))\n* **YouTube:** 21.43 navigation tabs cannot be hidden ([68b01ca](https://github.com/MorpheApp/morphe-patches/commit/68b01cad1f9419b4dc0e9141659b4def6c3660a4))", - "download_url": "https://github.com/MorpheApp/morphe-patches/releases/download/v1.49.0-dev.3/patches-1.49.0-dev.3.mpp", + "created_at": "2026-10-24T13:37:52", + "description": "## [1.49.0-dev.4](https://github.com/MorpheApp/morphe-patches/compare/v1.49.0-dev.3...v1.49.0-dev.4) (2026-10-24)\n\n### 🐛 Bug Fixes\n\n* **YouTube - Ambient mode:** Ambient mode is not disabled with 21.43 ([aeb3012](https://github.com/MorpheApp/morphe-patches/commit/aeb3012a1d4fc42e9cb0e80e1cc139f24d445fb7))\n* **YouTube - Feed card layout:** Shorts shelves change width and corners when only a last row with one Short is in the list ([97f1f2d](https://github.com/MorpheApp/morphe-patches/commit/97f1f2df9a2ce61384df4327f6743953c1ae598e))\n* **YouTube - Hide status bar:** Status bar scroll color and translucent navigation bar do not follow the hidden status bar before the edge to edge flag is read ([a9e6562](https://github.com/MorpheApp/morphe-patches/commit/a9e6562b2a4dbc5d57f8cf65a205206a800d192a))\n* **YouTube - Miniplayer:** \"Disable resuming miniplayer\" may not work with latest experimental app target ([8554412](https://github.com/MorpheApp/morphe-patches/commit/85544128284a29522310d43ed6bfb3e99706e9d4))\n* **YouTube:** Fingerprints are not found on some targets between 21.04 and 21.35 ([52fa134](https://github.com/MorpheApp/morphe-patches/commit/52fa13489d6a97172d29cf5ff639d958506251ca))", + "download_url": "https://github.com/MorpheApp/morphe-patches/releases/download/v1.49.0-dev.4/patches-1.49.0-dev.4.mpp", "signature_download_url": "", - "version": "1.49.0-dev.3" + "version": "1.49.0-dev.4" } diff --git a/patches-list.json b/patches-list.json index dd1d81ffcc..cc37ba4124 100644 --- a/patches-list.json +++ b/patches-list.json @@ -1,6 +1,6 @@ { "NOTE": "Do NOT manually edit this file. This file is automatically updated when semantic release (release.yml) runs. Manually editing this file can break your releases and break third party tools that use this file.", - "version": "1.49.0-dev.3", + "version": "1.49.0-dev.4", "patches": [ { "name": "AI channel filter", From 5cd2eb2f8d710e3a724d309dbf98fc845e78cb94 Mon Sep 17 00:00:00 2001 From: 0xrxL <127248639+0xrxL@users.noreply.github.com> Date: Sat, 24 Oct 2026 16:31:28 +0200 Subject: [PATCH 041/189] fix(YouTube - Miniplayer): Minimal miniplayer is covered by the keyboard before 21.29 --- .../youtube/patches/NavigationBarPatch.java | 38 ++++++++++++++++++- .../buttons/navigation/NavigationBarPatch.kt | 10 ++--- 2 files changed, 40 insertions(+), 8 deletions(-) diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/NavigationBarPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/NavigationBarPatch.java index 782f4885a7..79ceb9a366 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/NavigationBarPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/NavigationBarPatch.java @@ -70,6 +70,7 @@ import app.morphe.extension.youtube.patches.spoof.SpoofOSNamePatch; import app.morphe.extension.youtube.settings.Settings; import app.morphe.extension.youtube.shared.NavigationBar; +import app.morphe.extension.youtube.shared.PlayerType; @SuppressWarnings("unused") public final class NavigationBarPatch { @@ -309,9 +310,44 @@ public static boolean isTranslucentNavigationBarBackgroundForced() { /** * Injection point. + *

+ * The bottom margin of the layout that holds the page, the player and the navigation bar while + * the keyboard is open. With the feature flags turned on by + * {@link #useTranslucentNavigationBar(boolean)}, the keyboard height is removed from it, so + * the keyboard covers them again. + *

+ * The legacy miniplayer, such as the minimal type before 21.29, rests at the bottom of that + * layout, so while it is shown the layout ends at the top of the keyboard instead + * (https://github.com/MorpheApp/morphe-patches/issues/4003). The margin of the app leaves + * the height of the system navigation buttons for the navigation bar, which is hidden while + * the keyboard is open, so the full keyboard height is used. The other miniplayers are moved + * above the keyboard by {@link MinimalMiniplayerPatch}. + * + * @param original The keyboard height the app uses as the margin. */ - public static void setKeyboardMarginRemoved() { + public static int getKeyboardMargin(int original) { + if (!translucentNavigationBarForced) { + return original; + } + + try { + if (PlayerType.getCurrent() == PlayerType.WATCH_WHILE_MINIMIZED + && !MiniplayerPatch.getCurrentMiniplayerType().isModern() + && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + Activity activity = Utils.getActivity(); + WindowInsets insets = activity == null + ? null + : activity.getWindow().getDecorView().getRootWindowInsets(); + if (insets != null) { + return Math.max(original, insets.getInsets(WindowInsets.Type.ime()).bottom); + } + } + } catch (Exception ex) { + Logger.printException(() -> "getKeyboardMargin failure", ex); + } + keyboardMarginRemoved = true; + return 0; } static boolean isKeyboardMarginRemoved() { diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/buttons/navigation/NavigationBarPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/buttons/navigation/NavigationBarPatch.kt index fa6f936114..1fde2c8720 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/buttons/navigation/NavigationBarPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/buttons/navigation/NavigationBarPatch.kt @@ -360,7 +360,7 @@ val navigationBarPatch = bytecodePatch( // and the navigation bar to the top of the keyboard, so the navigation bar is moved above // the keyboard. Without it, the keyboard covers the bottom of the page and the navigation bar // stays at the bottom of the screen. The keyboard height is replaced with no margin, - // so the keyboard covers them again. + // so the keyboard covers them again, except while the legacy miniplayer is shown. PageBottomMarginFingerprint.let { it.method.apply { // The keyboard height and the window inset are put in the same register, @@ -372,17 +372,13 @@ val navigationBarPatch = bytecodePatch( (this as BuilderInstruction21t).target.location.index == insetIndex } val keyboardRegister = getInstruction(keyboardBranchIndex).registerA - val freeRegister = getFreeRegisterProvider(insetIndex + 1, 1).getFreeRegister4Bit() addInstructionsAtControlFlowLabel( insetIndex + 1, """ if-eqz v$keyboardRegister, :keyboard_hidden - invoke-static { }, $EXTENSION_CLASS->isTranslucentNavigationBarForced()Z - move-result v$freeRegister - if-eqz v$freeRegister, :keyboard_hidden - const/4 v$marginRegister, 0x0 - invoke-static { }, $EXTENSION_CLASS->setKeyboardMarginRemoved()V + invoke-static/range { v$marginRegister .. v$marginRegister }, $EXTENSION_CLASS->getKeyboardMargin(I)I + move-result v$marginRegister :keyboard_hidden nop """ From 9cc833f626bc456d142a6eafe2a613a3f2cc3784 Mon Sep 17 00:00:00 2001 From: 0xrxL <127248639+0xrxL@users.noreply.github.com> Date: Sat, 24 Oct 2026 16:35:41 +0200 Subject: [PATCH 042/189] fix(YouTube - Theme): Custom background colors are not applied with 21.43 (#4019) --- .../patches/youtube/layout/theme/ThemePatch.kt | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/theme/ThemePatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/theme/ThemePatch.kt index 013c667991..078e931045 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/theme/ThemePatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/theme/ThemePatch.kt @@ -88,7 +88,15 @@ private val youTubeColorNamesDark = { "yt_sys_color_baseline_dark_raised_background", "yt_sys_color_baseline_dark_base_background", "yt_sys_color_baseline_light_inverted_background", - "yt_sys_color_baseline_light_static_black" + "yt_sys_color_baseline_light_static_black", + // 21.43+ names of 'black3', 'black1' and the menu background. + "yt_ref_color_constants_default_grey5", + "yt_ref_color_constants_default_grey20", + "yt_sys_color_baseline_dark_default_menu_background", + // The mainstage color theme of 21.43+, which the app applies over the default one. + "yt_ref_color_constants_colors_mainstage_grey5", + "yt_ref_color_constants_colors_mainstage_grey20", + "yt_ref_color_constants_colors_mainstage_grey25" ) else emptySet() } @@ -106,9 +114,13 @@ private val youTubeColorNamesLight = { // "yt_ref_color_constants_default_baseline_white_white2", // "yt_ref_color_constants_default_baseline_white_white4", "yt_ref_color_constants_default_baseline_white_white3", + // 21.43+ name of 'white3'. + "yt_ref_color_constants_default_grey95", "yt_ref_color_constants_cooler_baseline_white_white3", "yt_sys_color_baseline_light_menu_background", + // 21.43+ name of the menu background. + "yt_sys_color_baseline_light_default_menu_background", // "yt_sys_color_baseline_light_static_white", // "yt_sys_color_baseline_light_static_white_background", // "yt_sys_color_baseline_dark_inverted_background", From 1669acdea4e56f6d1e812d3c0f74ee691bc2e4cd Mon Sep 17 00:00:00 2001 From: MarcaDian <152095496+MarcaDian@users.noreply.github.com> Date: Sat, 24 Oct 2026 18:03:56 +0300 Subject: [PATCH 043/189] fix: Temporarily change expired PoToken check to log only instead of a user dialog --- .../extension/shared/patches/PoTokenProviderPatch.java | 6 +++++- .../extension/shared/settings/SharedYouTubeSettings.java | 1 + .../morphe/extension/shared/spoof/PreferredClientCheck.java | 3 +-- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/patches/PoTokenProviderPatch.java b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/patches/PoTokenProviderPatch.java index c725d4995d..fb0402cede 100644 --- a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/patches/PoTokenProviderPatch.java +++ b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/patches/PoTokenProviderPatch.java @@ -197,12 +197,16 @@ public static void onMediaResponseChunk(Object callbacks, @Nullable ByteBuffer c * Playback stops once the buffered media runs out, so offer to turn on 'Spoof video streams'. */ private static synchronized void onTokenRejected(String reason) { + Logger.printDebug(() -> "YouTube no longer accepts the token made by the helper: " + reason); + // Log only until the check stops firing while the token still works. + if (!SharedYouTubeSettings.EXTERNAL_POTOKEN_PROVIDER_EXPIRED_DIALOG.get()) { + return; + } LongSetting lastShown = SharedYouTubeSettings.EXTERNAL_POTOKEN_PROVIDER_EXPIRED_DIALOG_LAST_SHOWN; Activity activity = Utils.getActivity(); if (activity == null || !ReminderDialog.isDue(lastShown)) { return; } - Logger.printDebug(() -> "YouTube no longer accepts the token made by the helper: " + reason); ReminderDialog.show( activity, lastShown, diff --git a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/settings/SharedYouTubeSettings.java b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/settings/SharedYouTubeSettings.java index 3fcc17581d..b0db5cb9de 100644 --- a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/settings/SharedYouTubeSettings.java +++ b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/settings/SharedYouTubeSettings.java @@ -103,6 +103,7 @@ public class SharedYouTubeSettings extends BaseSettings { // PoToken provider public static final BooleanSetting EXTERNAL_POTOKEN_PROVIDER = new BooleanSetting("morphe_external_potoken_provider", FALSE, true, "morphe_external_potoken_provider_user_dialog_message", new PoTokenProviderAvailability()); + public static final BooleanSetting EXTERNAL_POTOKEN_PROVIDER_EXPIRED_DIALOG = new BooleanSetting("morphe_external_potoken_provider_expired_dialog", FALSE, false, false); public static final LongSetting EXTERNAL_POTOKEN_PROVIDER_EXPIRED_DIALOG_LAST_SHOWN = new LongSetting("morphe_external_potoken_provider_expired_dialog_last_shown", 0L, false, false); // Downloads diff --git a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/spoof/PreferredClientCheck.java b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/spoof/PreferredClientCheck.java index 016cf8658c..e4c1522f93 100644 --- a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/spoof/PreferredClientCheck.java +++ b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/spoof/PreferredClientCheck.java @@ -74,6 +74,7 @@ public static synchronized void onStreamsFetched(String videoId, boolean preferr @SuppressWarnings("unchecked") private static void showDialogIfDue(ClientType workingClient) { + Logger.printDebug(() -> "Default client keeps failing, working client: " + workingClient); if (!SharedYouTubeSettings.SPOOF_VIDEO_STREAMS_CLIENT_FAILING_DIALOG.get()) { return; } @@ -91,8 +92,6 @@ private static void showDialogIfDue(ClientType workingClient) { if (names.length != values.size() || currentIndex < 0 || workingIndex < 0 || currentIndex == workingIndex) { return; } - Logger.printDebug(() -> "Default client keeps failing, suggesting: " + workingClient); - ReminderDialog.show( activity, lastShown, From a38f540c8895e90f2e96b7d3c075fd414204a333 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Sat, 24 Oct 2026 17:56:15 +0000 Subject: [PATCH 044/189] chore: Release v1.49.0-dev.5 [skip ci] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## [1.49.0-dev.5](https://github.com/MorpheApp/morphe-patches/compare/v1.49.0-dev.4...v1.49.0-dev.5) (2026-10-24) ### 🐛 Bug Fixes * Temporarily change expired PoToken check to log only instead of a user dialog ([1669acd](https://github.com/MorpheApp/morphe-patches/commit/1669acdea4e56f6d1e812d3c0f74ee691bc2e4cd)) * **YouTube - Miniplayer:** Minimal miniplayer is covered by the keyboard before 21.29 ([5cd2eb2](https://github.com/MorpheApp/morphe-patches/commit/5cd2eb2f8d710e3a724d309dbf98fc845e78cb94)) * **YouTube - Theme:** Custom background colors are not applied with 21.43 ([#4019](https://github.com/MorpheApp/morphe-patches/issues/4019)) ([9cc833f](https://github.com/MorpheApp/morphe-patches/commit/9cc833f626bc456d142a6eafe2a613a3f2cc3784)) --- CHANGELOG.md | 8 ++++++++ README.md | 2 +- gradle.properties | 2 +- patches-bundle.json | 8 ++++---- patches-list.json | 2 +- 5 files changed, 15 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7be7d8ea30..55c9750a26 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +## [1.49.0-dev.5](https://github.com/MorpheApp/morphe-patches/compare/v1.49.0-dev.4...v1.49.0-dev.5) (2026-10-24) + +### 🐛 Bug Fixes + +* Temporarily change expired PoToken check to log only instead of a user dialog ([1669acd](https://github.com/MorpheApp/morphe-patches/commit/1669acdea4e56f6d1e812d3c0f74ee691bc2e4cd)) +* **YouTube - Miniplayer:** Minimal miniplayer is covered by the keyboard before 21.29 ([5cd2eb2](https://github.com/MorpheApp/morphe-patches/commit/5cd2eb2f8d710e3a724d309dbf98fc845e78cb94)) +* **YouTube - Theme:** Custom background colors are not applied with 21.43 ([#4019](https://github.com/MorpheApp/morphe-patches/issues/4019)) ([9cc833f](https://github.com/MorpheApp/morphe-patches/commit/9cc833f626bc456d142a6eafe2a613a3f2cc3784)) + ## [1.49.0-dev.4](https://github.com/MorpheApp/morphe-patches/compare/v1.49.0-dev.3...v1.49.0-dev.4) (2026-10-24) ### 🐛 Bug Fixes diff --git a/README.md b/README.md index 6f1ca8ef1e..2a2af84e0a 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ All modifications made by Morphe, along with their dates, can be found in the Gi ## 🩹 Patches list -> **[v1.49.0-dev.4](https://github.com/MorpheApp/morphe-patches/releases/tag/v1.49.0-dev.4)**  •  `dev`  •  191 patches total +> **[v1.49.0-dev.5](https://github.com/MorpheApp/morphe-patches/releases/tag/v1.49.0-dev.5)**  •  `dev`  •  191 patches total

📦 YouTube  •  106 patches
diff --git a/gradle.properties b/gradle.properties index 6608ea3568..6e406b0944 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,4 +3,4 @@ org.gradle.jvmargs = -Xms512M -Xmx2048M org.gradle.parallel = true android.useAndroidX = true kotlin.code.style = official -version = 1.49.0-dev.4 +version = 1.49.0-dev.5 diff --git a/patches-bundle.json b/patches-bundle.json index c4e408cd80..4f2745650b 100644 --- a/patches-bundle.json +++ b/patches-bundle.json @@ -1,7 +1,7 @@ { - "created_at": "2026-10-24T13:37:52", - "description": "## [1.49.0-dev.4](https://github.com/MorpheApp/morphe-patches/compare/v1.49.0-dev.3...v1.49.0-dev.4) (2026-10-24)\n\n### 🐛 Bug Fixes\n\n* **YouTube - Ambient mode:** Ambient mode is not disabled with 21.43 ([aeb3012](https://github.com/MorpheApp/morphe-patches/commit/aeb3012a1d4fc42e9cb0e80e1cc139f24d445fb7))\n* **YouTube - Feed card layout:** Shorts shelves change width and corners when only a last row with one Short is in the list ([97f1f2d](https://github.com/MorpheApp/morphe-patches/commit/97f1f2df9a2ce61384df4327f6743953c1ae598e))\n* **YouTube - Hide status bar:** Status bar scroll color and translucent navigation bar do not follow the hidden status bar before the edge to edge flag is read ([a9e6562](https://github.com/MorpheApp/morphe-patches/commit/a9e6562b2a4dbc5d57f8cf65a205206a800d192a))\n* **YouTube - Miniplayer:** \"Disable resuming miniplayer\" may not work with latest experimental app target ([8554412](https://github.com/MorpheApp/morphe-patches/commit/85544128284a29522310d43ed6bfb3e99706e9d4))\n* **YouTube:** Fingerprints are not found on some targets between 21.04 and 21.35 ([52fa134](https://github.com/MorpheApp/morphe-patches/commit/52fa13489d6a97172d29cf5ff639d958506251ca))", - "download_url": "https://github.com/MorpheApp/morphe-patches/releases/download/v1.49.0-dev.4/patches-1.49.0-dev.4.mpp", + "created_at": "2026-10-24T17:53:42", + "description": "## [1.49.0-dev.5](https://github.com/MorpheApp/morphe-patches/compare/v1.49.0-dev.4...v1.49.0-dev.5) (2026-10-24)\n\n### 🐛 Bug Fixes\n\n* Temporarily change expired PoToken check to log only instead of a user dialog ([1669acd](https://github.com/MorpheApp/morphe-patches/commit/1669acdea4e56f6d1e812d3c0f74ee691bc2e4cd))\n* **YouTube - Miniplayer:** Minimal miniplayer is covered by the keyboard before 21.29 ([5cd2eb2](https://github.com/MorpheApp/morphe-patches/commit/5cd2eb2f8d710e3a724d309dbf98fc845e78cb94))\n* **YouTube - Theme:** Custom background colors are not applied with 21.43 ([#4019](https://github.com/MorpheApp/morphe-patches/issues/4019)) ([9cc833f](https://github.com/MorpheApp/morphe-patches/commit/9cc833f626bc456d142a6eafe2a613a3f2cc3784))", + "download_url": "https://github.com/MorpheApp/morphe-patches/releases/download/v1.49.0-dev.5/patches-1.49.0-dev.5.mpp", "signature_download_url": "", - "version": "1.49.0-dev.4" + "version": "1.49.0-dev.5" } diff --git a/patches-list.json b/patches-list.json index cc37ba4124..e4be259abc 100644 --- a/patches-list.json +++ b/patches-list.json @@ -1,6 +1,6 @@ { "NOTE": "Do NOT manually edit this file. This file is automatically updated when semantic release (release.yml) runs. Manually editing this file can break your releases and break third party tools that use this file.", - "version": "1.49.0-dev.4", + "version": "1.49.0-dev.5", "patches": [ { "name": "AI channel filter", From b2688c5eb957f3ffd142595ff4ee7b491e3b86ae Mon Sep 17 00:00:00 2001 From: Sap <165066971+sap6@users.noreply.github.com> Date: Sat, 24 Oct 2026 20:04:29 +0100 Subject: [PATCH 045/189] fix(YouTube - Captions): Captions not shown in picture-in-picture on 21.16 - 21.28 (#4024) --- .../extension/youtube/patches/CaptionsInPipPatch.java | 2 +- .../patches/youtube/layout/captions/CaptionsInPipPatch.kt | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/CaptionsInPipPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/CaptionsInPipPatch.java index 36b7963e1f..7c7cc32b9e 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/CaptionsInPipPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/CaptionsInPipPatch.java @@ -24,7 +24,7 @@ public static boolean isPipActive(String overlayName) { } /** - * Injection point. Used by versions before 21.05 that also give overlays the player type. + * Injection point. Used by versions before 21.41 that also give overlays the player type. * * @return If the overlay must be shown, even though the player type hides it. */ diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/captions/CaptionsInPipPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/captions/CaptionsInPipPatch.kt index 7981b63e28..56ac58543b 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/captions/CaptionsInPipPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/captions/CaptionsInPipPatch.kt @@ -19,7 +19,7 @@ import app.morphe.patches.shared.misc.settings.preference.SwitchPreference import app.morphe.patches.youtube.misc.extension.sharedExtensionPatch import app.morphe.patches.youtube.misc.playservice.is_20_29_or_greater import app.morphe.patches.youtube.misc.playservice.is_21_02_or_greater -import app.morphe.patches.youtube.misc.playservice.is_21_05_or_greater +import app.morphe.patches.youtube.misc.playservice.is_21_41_or_greater import app.morphe.patches.youtube.misc.playservice.versionCheckPatch import app.morphe.patches.youtube.misc.settings.settingsPatch import app.morphe.patches.youtube.shared.PlayerTypeEnumFingerprint @@ -83,9 +83,10 @@ internal val captionsInPipPatch = bytecodePatch( } } - // Before 21.05, overlays are also given the player type, which can be picture-in-picture. + // Before 21.41, overlays are also given the player type, which can be picture-in-picture. + // 21.05+ only uses it depending on a server flag. // Show the captions overlay anyway. - if (!is_21_05_or_greater) { + if (!is_21_41_or_greater) { getPlayerTypeOverlayVisibilityFingerprint(PlayerTypeEnumFingerprint.originalClassDef.type) .matchAll() .forEach { From 8b3dd7bb72367faaba210b47d2ea46be45f3bfd5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 24 Oct 2026 22:17:41 +0200 Subject: [PATCH 046/189] chore(deps): bump cycjimmy/semantic-release-action from 6 to 8 (#4030) --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b70f1c35b3..5cde5b987f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -46,7 +46,7 @@ jobs: run: npm install - name: Release - uses: cycjimmy/semantic-release-action@v6 + uses: cycjimmy/semantic-release-action@v8 id: release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 4f108b596a71adbbf223b59faaabcfbe2469d95a Mon Sep 17 00:00:00 2001 From: 0xrxL <127248639+0xrxL@users.noreply.github.com> Date: Sun, 25 Oct 2026 03:10:02 +0100 Subject: [PATCH 047/189] fix(YouTube - Playback speed): Channel speed whitelist is not applied to preloaded Shorts --- .../youtube/video/information/Fingerprints.kt | 29 +++++++++++++++++++ .../information/VideoInformationPatch.kt | 14 +++++++++ 2 files changed, 43 insertions(+) diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/video/information/Fingerprints.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/video/information/Fingerprints.kt index c41bc4ec49..d0193a8084 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/video/information/Fingerprints.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/video/information/Fingerprints.kt @@ -60,6 +60,35 @@ internal object ChannelInformationFingerprint : Fingerprint( ) ) +/** + * Makes a playback the current one, such as a preloaded Short when it's scrolled to, + * which is not loaded again with loadVideo(). + */ +internal fun currentPlaybackFingerprint(playerResponseType: String) = Fingerprint( + classFingerprint = PlayerInitFingerprint, + accessFlags = listOf(AccessFlags.PUBLIC, AccessFlags.FINAL), + returnType = "V", + parameters = listOf("L"), + filters = listOf( + fieldAccess( + opcode = Opcode.IPUT_OBJECT, + definingClass = "this" + ), + // The player response of the playback. + methodCall( + returnType = playerResponseType, + parameters = listOf() + ), + opcode(Opcode.MOVE_RESULT_OBJECT, location = MatchAfterImmediately()), + opcode(Opcode.IF_EQZ, location = MatchAfterImmediately()), + methodCall( + opcode = Opcode.INVOKE_STATIC, + parameters = listOf(playerResponseType, "L"), + location = MatchAfterImmediately() + ) + ) +) + internal fun getChannelIdFingerprint(playerResponseType: String) = object : Fingerprint( accessFlags = listOf(AccessFlags.PUBLIC, AccessFlags.FINAL), returnType = "V", diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/video/information/VideoInformationPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/video/information/VideoInformationPatch.kt index bf4587f7f5..5f0078d270 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/video/information/VideoInformationPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/video/information/VideoInformationPatch.kt @@ -19,6 +19,7 @@ import app.morphe.patcher.patch.BytecodePatchContext import app.morphe.patcher.patch.PatchException import app.morphe.patcher.patch.bytecodePatch import app.morphe.patcher.util.getReference +import app.morphe.patcher.util.matchSingle import app.morphe.patcher.util.p0Register import app.morphe.patcher.util.proxy.mutableTypes.MutableClass import app.morphe.patcher.util.proxy.mutableTypes.MutableField @@ -574,6 +575,19 @@ val videoInformationPatch = bytecodePatch( "invoke-direct { p0, p1 }, $helperMethod" ) } + + // A preloaded playback, such as the next Short, becomes the current one without loadVideo(). + currentPlaybackFingerprint(playerResponseType).matchSingle().let { currentPlayback -> + currentPlayback.method.apply { + val index = currentPlayback.instructionMatches.last().index + val playerResponseRegister = getInstruction(index).registerC + + addInstruction( + index, + "invoke-direct { p0, v$playerResponseRegister }, $helperMethod" + ) + } + } } } From db9af377093121e575fec80ba40e0025dfd66f81 Mon Sep 17 00:00:00 2001 From: 0xrxL <127248639+0xrxL@users.noreply.github.com> Date: Sun, 25 Oct 2026 03:30:57 +0100 Subject: [PATCH 048/189] feat(YouTube - Navigation bar): Add "Navigation bar style" setting --- .../youtube/patches/NavigationBarPatch.java | 60 ++++++++++++++++--- .../extension/youtube/settings/Settings.java | 10 +++- .../buttons/navigation/NavigationBarPatch.kt | 40 ++++--------- .../addresources/values/youtube/arrays.xml | 12 ++++ .../addresources/values/youtube/strings.xml | 7 ++- 5 files changed, 90 insertions(+), 39 deletions(-) diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/NavigationBarPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/NavigationBarPatch.java index 79ceb9a366..9e14b5fb4d 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/NavigationBarPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/NavigationBarPatch.java @@ -88,7 +88,24 @@ public final class NavigationBarPatch { private static final boolean SWAP_CREATE_WITH_NOTIFICATIONS_BUTTON = Settings.SWAP_CREATE_WITH_NOTIFICATIONS_BUTTON.get(); - private static final boolean DISABLE_TRANSLUCENT_NAVIGATION = Settings.DISABLE_TRANSLUCENT_NAVIGATION.get(); + public enum NavigationBarStyle { + /** + * The feature flags of the translucent navigation bar are left to the account, + * and the fixes for what they break are off. + */ + DEFAULT, + TRANSLUCENT, + /** + * Translucent, without the blurred background. + */ + TRANSPARENT, + /** + * Transparent, painted over with the app background. + */ + OPAQUE + } + + private static final NavigationBarStyle NAVIGATION_BAR_STYLE = Settings.NAVIGATION_BAR_STYLE.get(); private static final boolean NARROW_NAVIGATION_BUTTONS = Settings.NARROW_NAVIGATION_BUTTONS.get(); @@ -220,8 +237,14 @@ public static void hideNavigationBar(View view) { * The app draws the window behind the system navigation buttons only on Android 12 and later, * so on older versions the flags are always turned off, or the room is added twice * (https://github.com/MorpheApp/morphe-patches/issues/3864). + *

+ * With {@link NavigationBarStyle#DEFAULT} the flags are left to the account. */ public static boolean useTranslucentNavigationBar(boolean original) { + if (NAVIGATION_BAR_STYLE == NavigationBarStyle.DEFAULT) { + return original; + } + boolean translucent = original; try { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { @@ -272,13 +295,17 @@ public static boolean useFixedBottomBarRoom(boolean original) { *

* The blurred, translucent background of the navigation bar and the feed laid out behind it * only change how the bar looks, not the room left for the system navigation buttons, so they - * are turned on from Android 12 also when the window is not drawn edge to edge. If - * the translucent navigation bar is turned off, they follow - * {@link #useTranslucentNavigationBar(boolean)} instead. + * are turned on from Android 12 also when the window is not drawn edge to edge. With + * {@link NavigationBarStyle#OPAQUE}, they follow {@link #useTranslucentNavigationBar(boolean)} + * instead. With {@link NavigationBarStyle#DEFAULT} they are left to the account. */ public static boolean useTranslucentNavigationBarBackground(boolean original) { + if (NAVIGATION_BAR_STYLE == NavigationBarStyle.DEFAULT) { + return original; + } + final boolean translucent; - if (!DISABLE_TRANSLUCENT_NAVIGATION && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + if (NAVIGATION_BAR_STYLE != NavigationBarStyle.OPAQUE && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { translucent = true; } else { translucent = useTranslucentNavigationBar(original); @@ -302,6 +329,25 @@ public static boolean isTranslucentNavigationBarBackgroundForced() { return translucentNavigationBarBackgroundForced; } + /** + * Injection point. + *

+ * The blurred background of the navigation bar draws a copy of the watch layout, so the player + * surface is drawn twice in every frame, at two positions. Android 12 and 13 send the two + * positions to the surface from two threads at the same time, and the app crashes while + * the player moves, such as when it is slowly dragged down to the miniplayer + * (https://github.com/MorpheApp/morphe-patches/pull/3684#issuecomment-6322984709). + * With {@link NavigationBarStyle#TRANSPARENT} and {@link NavigationBarStyle#OPAQUE} the copy + * is not drawn, so the bar keeps its translucent color, without the blur. + * + * @param hardwareAccelerated If the canvas is hardware accelerated, which the blur is drawn for. + */ + public static boolean useNavigationBarBlur(boolean hardwareAccelerated) { + return hardwareAccelerated && !(translucentNavigationBarBackgroundForced + && (NAVIGATION_BAR_STYLE == NavigationBarStyle.TRANSPARENT + || NAVIGATION_BAR_STYLE == NavigationBarStyle.OPAQUE)); + } + /** * If the keyboard height was ever removed from the bottom margin of the layout that holds * the page, the player and the navigation bar, so the keyboard covers them. @@ -560,7 +606,7 @@ public static int getShortsPlayerBottomRoom(int original) { * behind the system navigation buttons or gesture handle. */ public static void setNavigationBarOpaque(View bottomBarContainer) { - if (!DISABLE_TRANSLUCENT_NAVIGATION) return; + if (NAVIGATION_BAR_STYLE != NavigationBarStyle.OPAQUE) return; try { OpaqueSystemBars.apply(bottomBarContainer); @@ -575,7 +621,7 @@ public static void setNavigationBarOpaque(View bottomBarContainer) { * @param original The color the app theme gives the system navigation buttons. */ public static int getSystemNavigationBarColor(int original) { - if (!DISABLE_TRANSLUCENT_NAVIGATION) return original; + if (NAVIGATION_BAR_STYLE != NavigationBarStyle.OPAQUE) return original; try { return OpaqueSystemBars.systemNavigationBarColor(original); diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/Settings.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/Settings.java index 55a5c3ae05..e8aa587ac9 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/Settings.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/Settings.java @@ -51,6 +51,7 @@ import app.morphe.extension.youtube.patches.MiniplayerPatch.MiniplayerMinimalBar2Availability; import app.morphe.extension.youtube.patches.MiniplayerPatch.MiniplayerOverlayOpacityAvailability; import app.morphe.extension.youtube.patches.MiniplayerPatch.MiniplayerType; +import app.morphe.extension.youtube.patches.NavigationBarPatch.NavigationBarStyle; import app.morphe.extension.youtube.patches.OpenShortsInRegularPlayerPatch.ShortsPlayerType; import app.morphe.extension.youtube.patches.OpenVideosFullscreenHookPatch.OpenFullscreenMode; import app.morphe.extension.youtube.patches.PictureinPictureButtonPatch.PipButtonPatchAvailability; @@ -572,7 +573,7 @@ public class Settings extends SharedYouTubeSettings { public static final BooleanSetting NARROW_NAVIGATION_BUTTONS = new BooleanSetting("morphe_narrow_navigation_buttons", FALSE, true, parentNot(HIDE_NAVIGATION_BAR)); public static final BooleanSetting DISABLE_AUTO_HIDE_NAVIGATION_BAR = new BooleanSetting("morphe_disable_auto_hide_navigation_bar", FALSE, true, parentNot(HIDE_NAVIGATION_BAR)); public static final BooleanSetting NAVIGATION_BAR_ANIMATIONS = new BooleanSetting("morphe_navigation_bar_animations", FALSE, parentNot(HIDE_NAVIGATION_BAR)); - public static final BooleanSetting DISABLE_TRANSLUCENT_NAVIGATION = new BooleanSetting("morphe_disable_translucent_navigation", FALSE, true, parentNot(HIDE_NAVIGATION_BAR)); + public static final EnumSetting NAVIGATION_BAR_STYLE = new EnumSetting<>("morphe_navigation_bar_style", NavigationBarStyle.DEFAULT, true, parentNot(HIDE_NAVIGATION_BAR)); // Toolbar public static final BooleanSetting HIDE_TOOLBAR_CAST_BUTTON = new BooleanSetting("morphe_hide_toolbar_cast_button", TRUE, true); @@ -869,6 +870,7 @@ public class Settings extends SharedYouTubeSettings { private static final BooleanSetting DEPRECATED_DISABLE_TRANSLUCENT_STATUS_BAR = new BooleanSetting("morphe_disable_translucent_status_bar", FALSE); private static final BooleanSetting DEPRECATED_DISABLE_TRANSLUCENT_NAVIGATION_BAR_LIGHT = new BooleanSetting("morphe_disable_translucent_navigation_bar_light", FALSE); private static final BooleanSetting DEPRECATED_DISABLE_TRANSLUCENT_NAVIGATION_BAR_DARK = new BooleanSetting("morphe_disable_translucent_navigation_bar_dark", FALSE); + private static final BooleanSetting DEPRECATED_DISABLE_TRANSLUCENT_NAVIGATION = new BooleanSetting("morphe_disable_translucent_navigation", FALSE); static { migrateOldSettingToNew(DEPRECATED_BYPASS_URL_REDIRECTS, BYPASS_LINK_REDIRECTS); @@ -966,11 +968,13 @@ public class Settings extends SharedYouTubeSettings { if (DEPRECATED_DISABLE_TRANSLUCENT_STATUS_BAR.get() || DEPRECATED_DISABLE_TRANSLUCENT_NAVIGATION_BAR_LIGHT.get() - || DEPRECATED_DISABLE_TRANSLUCENT_NAVIGATION_BAR_DARK.get()) { - DISABLE_TRANSLUCENT_NAVIGATION.save(true); + || DEPRECATED_DISABLE_TRANSLUCENT_NAVIGATION_BAR_DARK.get() + || DEPRECATED_DISABLE_TRANSLUCENT_NAVIGATION.get()) { + NAVIGATION_BAR_STYLE.save(NavigationBarStyle.OPAQUE); DEPRECATED_DISABLE_TRANSLUCENT_STATUS_BAR.resetToDefault(); DEPRECATED_DISABLE_TRANSLUCENT_NAVIGATION_BAR_LIGHT.resetToDefault(); DEPRECATED_DISABLE_TRANSLUCENT_NAVIGATION_BAR_DARK.resetToDefault(); + DEPRECATED_DISABLE_TRANSLUCENT_NAVIGATION.resetToDefault(); } // 20.37+ YT removed parts of the code for the legacy tablet miniplayer. diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/buttons/navigation/NavigationBarPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/buttons/navigation/NavigationBarPatch.kt index 1fde2c8720..087b34bd1b 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/buttons/navigation/NavigationBarPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/buttons/navigation/NavigationBarPatch.kt @@ -178,7 +178,7 @@ val navigationBarPatch = bytecodePatch( navPreferences += listOf( SwitchPreference("morphe_hide_navigation_new_content_dot"), SwitchPreference("morphe_navigation_bar_animations", summary = true), - SwitchPreference("morphe_disable_translucent_navigation", summary = true) + ListPreference("morphe_navigation_bar_style") ) if (is_20_31_or_greater) { @@ -224,11 +224,11 @@ val navigationBarPatch = bytecodePatch( // Turning on the feature flags of the translucent navigation bar, and the fixes for what // they break. Every fix checks that the flags are turned on by this patch, so the fixes do - // nothing if the flags are left to the account. + // nothing if the flags are left to the account, as with the default navigation bar style. // The translucent navigation bar comes from three feature flags that not every account // gets, so they are turned on to make the bar look the same on every account, and the opaque - // option below paints over the same translucent bar everywhere. The flag that makes the bar + // style below paints over the same translucent bar everywhere. The flag that makes the bar // cover the system navigation buttons is turned on only when the window is drawn edge to // edge. The blur and the feed behind the bar do not change the room left for the system // navigation buttons, so they are also turned on without it. @@ -248,33 +248,19 @@ val navigationBarPatch = bytecodePatch( } } - // The blurred background of the translucent navigation bar draws a copy of the watch layout, - // so the player surface is drawn twice in every frame, at two positions. Android 12 and 13 - // send the two positions to the surface from two threads at the same time, and the app - // crashes while the player moves, such as when it is slowly dragged down to the miniplayer - // (https://github.com/MorpheApp/morphe-patches/pull/3684#issuecomment-6322984709). - // Android 14 sends them one after the other. On older versions the copy is not drawn, - // so the bar keeps its translucent color, without the blur. + // The blurred background of the translucent navigation bar is drawn only if the canvas is + // hardware accelerated. The transparent and opaque styles leave it out, as it can crash + // the app on Android 12 and 13. NavigationBarBlurDrawFingerprint.let { it.method.apply { val index = it.instructionMatches[1].index val hardwareAcceleratedRegister = getInstruction(index).registerA - val freeRegisters = getFreeRegisterProvider(index + 1, 2) - val sdkRegister = freeRegisters.getFreeRegister4Bit() - val minSdkRegister = freeRegisters.getFreeRegister4Bit() - addInstructionsWithLabels( + addInstructions( index + 1, - $$""" - invoke-static { }, $$EXTENSION_CLASS->isTranslucentNavigationBarBackgroundForced()Z - move-result v$$sdkRegister - if-eqz v$$sdkRegister, :draw_blur - sget v$$sdkRegister, Landroid/os/Build$VERSION;->SDK_INT:I - const/16 v$$minSdkRegister, 0x22 - if-ge v$$sdkRegister, v$$minSdkRegister, :draw_blur - const/4 v$$hardwareAcceleratedRegister, 0x0 - :draw_blur - nop + """ + invoke-static { v$hardwareAcceleratedRegister }, $EXTENSION_CLASS->useNavigationBarBlur(Z)Z + move-result v$hardwareAcceleratedRegister """ ) } @@ -506,9 +492,9 @@ val navigationBarPatch = bytecodePatch( ) } - // Paint over the translucent status bar and navigation bar, instead of turning off the - // feature flags that drive them. Those flags also switch the app out of edge to edge, which - // moves the whole window layout and breaks everything measuring against it. + // With the opaque style, paint over the translucent status bar and navigation bar, instead + // of turning off the feature flags that drive them. Those flags also switch the app out of + // edge to edge, which moves the whole window layout and breaks everything measuring against it. addBottomBarContainerHook("$EXTENSION_CLASS->setNavigationBarOpaque(Landroid/view/View;)V") // The translucent theme hands the window a transparent color for the system navigation diff --git a/patches/src/main/resources/addresources/values/youtube/arrays.xml b/patches/src/main/resources/addresources/values/youtube/arrays.xml index 0e2bbafa02..95f35dc4bd 100644 --- a/patches/src/main/resources/addresources/values/youtube/arrays.xml +++ b/patches/src/main/resources/addresources/values/youtube/arrays.xml @@ -106,6 +106,18 @@ 5 6 + + @string/morphe_navigation_bar_style_entry_1 + @string/morphe_navigation_bar_style_entry_2 + @string/morphe_navigation_bar_style_entry_3 + @string/morphe_navigation_bar_style_entry_4 + + + DEFAULT + TRANSLUCENT + TRANSPARENT + OPAQUE + 1 2 diff --git a/patches/src/main/resources/addresources/values/youtube/strings.xml b/patches/src/main/resources/addresources/values/youtube/strings.xml index c61b2b5555..2e2b19d556 100644 --- a/patches/src/main/resources/addresources/values/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values/youtube/strings.xml @@ -923,8 +923,11 @@ To access Settings, enable the Settings button via General → Toolbar → Show Animates transitions when switching between tabs Disable auto-hiding navigation bar Keeps the navigation bar visible while scrolling - Disable translucent navigation bar - Makes the navigation and status bar fully opaque + Navigation bar style + Default + Translucent + Transparent + Opaque Toolbar Choose which components appear in the toolbar From 9a0fee9ec2846ba2db31cbfc2e4c2ac9b14895b7 Mon Sep 17 00:00:00 2001 From: pharmacomaniac Date: Sun, 25 Oct 2026 05:49:25 -0400 Subject: [PATCH 049/189] fix(YouTube - Hide layout components): Fix actions blocked by Hide Snackbar (#4038) --- .../components/LayoutComponentsFilter.java | 4 +-- .../layout/hide/general/Fingerprints.kt | 14 ++++++---- .../hide/general/HideLayoutComponentsPatch.kt | 26 +++++++++++-------- 3 files changed, 26 insertions(+), 18 deletions(-) diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/components/LayoutComponentsFilter.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/components/LayoutComponentsFilter.java index 33328d29fb..db98c10179 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/components/LayoutComponentsFilter.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/components/LayoutComponentsFilter.java @@ -1405,8 +1405,8 @@ public static int hideHelpFeedbackMenuRow(View row, int heightMeasureSpec) { /** * Injection point. */ - public static boolean hideSnackbar() { - return Settings.HIDE_SNACKBAR.get(); + public static boolean hideSnackbar(boolean skipDisplay) { + return skipDisplay || Settings.HIDE_SNACKBAR.get(); } /** diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/general/Fingerprints.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/general/Fingerprints.kt index 52d9ffaa68..3c40854adf 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/general/Fingerprints.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/general/Fingerprints.kt @@ -752,15 +752,19 @@ internal object AccountMenuLegacyFingerprint : Fingerprint( ) ) -internal object BottomUIContainerFingerprint : Fingerprint( - definingClass = "Lcom/google/android/apps/youtube/app/common/ui/bottomui/BottomUiContainer;", +internal object SnackbarDisplayFingerprint : Fingerprint( accessFlags = listOf(AccessFlags.PUBLIC, AccessFlags.FINAL), returnType = "V", - parameters = listOf("Landroid/view/View;", "L"), + parameters = listOf("L"), filters = listOf( methodCall( - definingClass = "this", - name = "removeAllViews" + parameters = emptyList(), + returnType = "Z" + ), + methodCall( + definingClass = "Lcom/google/android/apps/youtube/app/common/ui/bottomui/BottomUiContainer;", + parameters = listOf("L", "L", "L"), + returnType = "V" ) ) ) diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/general/HideLayoutComponentsPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/general/HideLayoutComponentsPatch.kt index 4b3bcb6cf7..5c25caa2c1 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/general/HideLayoutComponentsPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/general/HideLayoutComponentsPatch.kt @@ -1205,17 +1205,21 @@ val hideLayoutComponentsPatch = bytecodePatch( } } - BottomUIContainerFingerprint.method.addInstructionsWithLabels( - 0, - """ - invoke-static {}, $LAYOUT_COMPONENTS_FILTER->hideSnackbar()Z - move-result v0 - if-eqz v0, :show - return-void - :show - nop - """ - ) + SnackbarDisplayFingerprint.let { + it.method.apply { + val index = it.instructionMatches.first().index + 1 + val register = getInstruction(index).registerA + + // Let YouTube skip displaying the snackbar while still running its callbacks. + addInstructions( + index + 1, + """ + invoke-static { v$register }, $LAYOUT_COMPONENTS_FILTER->hideSnackbar(Z)Z + move-result v$register + """ + ) + } + } arrayOf( QuantumSnackbarFingerprint, From dd97d93628a3feb7bc8758139b01fbe42a532778 Mon Sep 17 00:00:00 2001 From: RAMiK <60112744+RAMiK4@users.noreply.github.com> Date: Sun, 25 Oct 2026 17:46:25 +0400 Subject: [PATCH 050/189] feat(YouTube - Downloads): Add "Quick download on tap" setting (#3883) Co-authored-by: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Co-authored-by: MarcaDian <152095496+MarcaDian@users.noreply.github.com> --- .../ExternalDownloaderPreference.java | 185 ++++++++++----- .../youtube/patches/DownloadsPatch.java | 10 +- .../utils/requests/MarkWatchedRequest.java | 172 ++++++++++++++ .../utils/requests/PlaylistRoutes.java | 26 ++- .../extension/youtube/settings/Settings.java | 3 + .../videoplayer/ExternalDownloadButton.java | 215 +++++++++++++++++- .../interaction/downloads/DownloadsPatch.kt | 19 +- .../addresources/values/youtube/strings.xml | 10 + ...morphe_external_downloader_quick_audio.xml | 29 +++ ...e_external_downloader_quick_audio_bold.xml | 29 +++ ...morphe_external_downloader_quick_video.xml | 29 +++ ...e_external_downloader_quick_video_bold.xml | 29 +++ 12 files changed, 692 insertions(+), 64 deletions(-) create mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/requests/MarkWatchedRequest.java create mode 100644 patches/src/main/resources/downloads/drawable/morphe_external_downloader_quick_audio.xml create mode 100644 patches/src/main/resources/downloads/drawable/morphe_external_downloader_quick_audio_bold.xml create mode 100644 patches/src/main/resources/downloads/drawable/morphe_external_downloader_quick_video.xml create mode 100644 patches/src/main/resources/downloads/drawable/morphe_external_downloader_quick_video_bold.xml diff --git a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/settings/preference/ExternalDownloaderPreference.java b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/settings/preference/ExternalDownloaderPreference.java index b5a363fef9..912a8f6135 100644 --- a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/settings/preference/ExternalDownloaderPreference.java +++ b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/settings/preference/ExternalDownloaderPreference.java @@ -48,59 +48,6 @@ @SuppressWarnings({"unused", "deprecation"}) public class ExternalDownloaderPreference extends CustomDialogListPreference { - /** - * The external downloader is used by the download mode, the player overlay button, or the Download menu. - */ - public static final class ExternalDownloaderAvailability implements Setting.Availability { - @Override - public boolean isAvailable() { - return SharedYouTubeSettings.DOWNLOAD_MODE.get() == DownloadMode.EXTERNAL - || SharedYouTubeSettings.EXTERNAL_DOWNLOADER_OVERLAY_BUTTON.get() - || SharedYouTubeSettings.EXTERNAL_DOWNLOADER_MENU.get(); - } - - @Override - public List> getParentSettings() { - return List.of(SharedYouTubeSettings.DOWNLOAD_MODE, - SharedYouTubeSettings.EXTERNAL_DOWNLOADER_OVERLAY_BUTTON, - SharedYouTubeSettings.EXTERNAL_DOWNLOADER_MENU); - } - } - - public static void launchExternalDownloader(String videoId, @Nullable Context context, String url) { - try { - Objects.requireNonNull(videoId); - if (context == null) { - context = Utils.getActivity(); - } - Context contextFinal = context; - Logger.printDebug(() -> "Launching external downloader: " + videoId + " context: " + contextFinal); - - // Trim string to avoid any accidental whitespace. - var downloaderPackageName = SharedYouTubeSettings.EXTERNAL_DOWNLOADER_PACKAGE_NAME.get().trim(); - - // If the package is not installed, show a dialog. - if (showDialogIfAppIsNotInstalled(context, downloaderPackageName)) { - return; - } - - Intent intent = new Intent("android.intent.action.SEND"); - intent.setType("text/plain"); - intent.setPackage(downloaderPackageName); - intent.putExtra("android.intent.extra.TEXT", url); - // Opening the downloader is not the user leaving the app. - // Without this flag a playing video enters picture-in-picture, - // and the downloader's window opens over the home screen instead of over the video. - intent.addFlags(Intent.FLAG_ACTIVITY_NO_USER_ACTION); - if (!(context instanceof Activity)) { - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - } - context.startActivity(intent); - } catch (Exception ex) { - Logger.printException(() -> "launchExternalDownloader failure", ex); - } - } - /** * Enum representing supported external downloaders with their display names, package names, and download URLs. */ @@ -108,6 +55,14 @@ private enum Downloader { YTDLNIS("YTDLnis", "com.deniscerri.ytdl", "https://ytdlnis.org", + true, + true, + false), + YTINDIR("YTindir", + "com.rl.ytindr", + "https://github.com/RAMiK4/ytindir/releases", + false, + true, true), SEAL("Seal", "com.junkfood.seal", @@ -168,19 +123,141 @@ public static Downloader findByPackageName(String packageName) { * if the app is not currently installed. */ public final boolean isPreferred; + /** + * If the downloader understands the quick download extras of YTDLnis: + * {@code TYPE} (audio or video) and {@code BACKGROUND} (queue without showing a window). + */ + public final boolean supportsQuickDownload; + /** + * If the downloader also applies the requested quality: + * {@code VIDEO_QUALITY} (maximum height) and {@code AUDIO_BITRATE} (kbps). + */ + public final boolean supportsQuickDownloadQuality; Downloader(String name, String packageName, String downloadUrl) { - this(name, packageName, downloadUrl, false); + this(name, packageName, downloadUrl, false, false, false); } Downloader(String name, @Nullable String packageName, @Nullable String downloadUrl, boolean isPreferred) { + this(name, packageName, downloadUrl, isPreferred, false, false); + } + + Downloader(String name, @Nullable String packageName, @Nullable String downloadUrl, + boolean isPreferred, boolean supportsQuickDownload, boolean supportsQuickDownloadQuality) { this.name = name; this.packageName = packageName; this.downloadUrl = downloadUrl; this.isPreferred = isPreferred; + this.supportsQuickDownload = supportsQuickDownload; + this.supportsQuickDownloadQuality = supportsQuickDownloadQuality; } } + /** Maximum video height of a quick video download. */ + public static final int QUICK_DOWNLOAD_VIDEO_HEIGHT = 1080; + /** Bitrate of a quick audio download. */ + public static final int QUICK_DOWNLOAD_AUDIO_KBPS = 192; + + /** + * @return If the chosen downloader can queue a download in the background. + */ + public static boolean supportsQuickDownload() { + Downloader downloader = getDownloader(); + return downloader != null && downloader.supportsQuickDownload; + } + + /** + * @return If the chosen downloader also applies the requested quality, + * so the quick download menu can show it. + */ + public static boolean supportsQuickDownloadQuality() { + Downloader downloader = getDownloader(); + return downloader != null && downloader.supportsQuickDownloadQuality; + } + + @Nullable + private static Downloader getDownloader() { + return Downloader.findByPackageName( + SharedYouTubeSettings.EXTERNAL_DOWNLOADER_PACKAGE_NAME.get().trim()); + } + + /** + * The external downloader is used by the download mode, the player overlay button, or the Download menu. + */ + public static final class ExternalDownloaderAvailability implements Setting.Availability { + @Override + public boolean isAvailable() { + return SharedYouTubeSettings.DOWNLOAD_MODE.get() == DownloadMode.EXTERNAL + || SharedYouTubeSettings.EXTERNAL_DOWNLOADER_OVERLAY_BUTTON.get() + || SharedYouTubeSettings.EXTERNAL_DOWNLOADER_MENU.get(); + } + + @Override + public List> getParentSettings() { + return List.of(SharedYouTubeSettings.DOWNLOAD_MODE, + SharedYouTubeSettings.EXTERNAL_DOWNLOADER_OVERLAY_BUTTON, + SharedYouTubeSettings.EXTERNAL_DOWNLOADER_MENU); + } + } + + /** + * @return If the downloader was opened. + */ + public static boolean launchExternalDownloader(String videoId, @Nullable Context context, String url) { + return launchExternalDownloader(videoId, context, url, null); + } + + /** + * @param quickDownloadType If not null, the downloader queues the download in the background + * as this type ("audio" or "video") using its default settings, + * capped at {@link #QUICK_DOWNLOAD_VIDEO_HEIGHT} / {@link #QUICK_DOWNLOAD_AUDIO_KBPS} + * if the downloader supports it. + * @return If the downloader was opened. + */ + public static boolean launchExternalDownloader(String videoId, @Nullable Context context, String url, + @Nullable String quickDownloadType) { + try { + Objects.requireNonNull(videoId); + if (context == null) { + context = Utils.getActivity(); + } + Context contextFinal = context; + Logger.printDebug(() -> "Launching external downloader: " + videoId + " context: " + contextFinal); + + // Trim string to avoid any accidental whitespace. + var downloaderPackageName = SharedYouTubeSettings.EXTERNAL_DOWNLOADER_PACKAGE_NAME.get().trim(); + + // If the package is not installed, show a dialog. + if (showDialogIfAppIsNotInstalled(context, downloaderPackageName)) { + return false; + } + + //noinspection ExtractMethodRecommender + Intent intent = new Intent("android.intent.action.SEND"); + intent.setType("text/plain"); + intent.setPackage(downloaderPackageName); + intent.putExtra("android.intent.extra.TEXT", url); + if (quickDownloadType != null) { + intent.putExtra("TYPE", quickDownloadType); + intent.putExtra("BACKGROUND", true); + intent.putExtra("VIDEO_QUALITY", QUICK_DOWNLOAD_VIDEO_HEIGHT); + intent.putExtra("AUDIO_BITRATE", QUICK_DOWNLOAD_AUDIO_KBPS); + } + // Opening the downloader is not the user leaving the app. + // Without this flag a playing video enters picture-in-picture, + // and the downloader's window opens over the home screen instead of over the video. + intent.addFlags(Intent.FLAG_ACTIVITY_NO_USER_ACTION); + if (!(context instanceof Activity)) { + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + } + context.startActivity(intent); + return true; + } catch (Exception ex) { + Logger.printException(() -> "launchExternalDownloader failure", ex); + } + return false; + } + private static boolean isAppInstalledAndEnabled(String packageName) { try { if (Utils.getContext().getPackageManager().getApplicationInfo(packageName, 0).enabled) { diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/DownloadsPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/DownloadsPatch.java index 60deba8d24..076c843283 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/DownloadsPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/DownloadsPatch.java @@ -13,8 +13,8 @@ import app.morphe.extension.shared.Logger; import app.morphe.extension.shared.Utils; import app.morphe.extension.shared.patches.DownloadMode; -import app.morphe.extension.shared.settings.preference.ExternalDownloaderPreference; import app.morphe.extension.youtube.settings.Settings; +import app.morphe.extension.youtube.videoplayer.ExternalDownloadButton; @SuppressWarnings("unused") public final class DownloadsPatch { @@ -34,8 +34,12 @@ public static boolean inAppDownloadButtonOnClick(String videoId) { return false; } - ExternalDownloaderPreference.launchExternalDownloader( - videoId, Utils.getActivity(), "https://youtu.be/" + videoId); + if (ExternalDownloadButton.quickMenuOnTap()) { + ExternalDownloadButton.showQuickDownloadMenu(Utils.getActivity(), videoId); + return true; + } + + ExternalDownloadButton.launchDownloader(Utils.getActivity(), videoId, null); return true; } catch (Exception ex) { Logger.printException(() -> "inAppDownloadButtonOnClick failure", ex); diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/requests/MarkWatchedRequest.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/requests/MarkWatchedRequest.java new file mode 100644 index 0000000000..c16331ed7c --- /dev/null +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/requests/MarkWatchedRequest.java @@ -0,0 +1,172 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/3883 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.extension.youtube.patches.utils.requests; + +import android.net.Uri; + +import androidx.annotation.Nullable; + +import org.json.JSONObject; + +import java.net.HttpURLConnection; +import java.security.SecureRandom; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; + +import app.morphe.extension.shared.Logger; +import app.morphe.extension.shared.Utils; +import app.morphe.extension.shared.innertube.utils.AuthUtils; +import app.morphe.extension.shared.requests.Requester; + +/** + * Adds a video to the watch history of the account, as if it was watched to the end, + * the same way as the {@code --mark-watched} option of yt-dlp. + */ +public final class MarkWatchedRequest { + + /** + * Characters of the client playback nonce, which identifies a playback in the reports. + */ + private static final String CPN_ALPHABET = + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_"; + + private static final int CPN_LENGTH = 16; + + private static final int CONNECTION_TIMEOUT_MILLISECONDS = 10 * 1000; + + private static final SecureRandom random = new SecureRandom(); + + private MarkWatchedRequest() { + } + + public static void markWatched(String videoId) { + Objects.requireNonNull(videoId); + if (AuthUtils.isNotLoggedIn()) { + Logger.printDebug(() -> "Not logged in, not marking as watched: " + videoId); + return; + } + // The headers of the account that is signed in now, not when the request runs. + Map authHeaders = AuthUtils.getRequestHeader(); + Utils.runOnBackgroundThread(() -> sendRequest(videoId, authHeaders)); + } + + private static void sendRequest(String videoId, Map authHeaders) { + Utils.verifyOffMainThread(); + + try { + byte[] requestBody = PlaylistRoutes.playbackTrackingBody(videoId); + HttpURLConnection connection = PlaylistRoutes.getConnection( + PlaylistRoutes.GET_PLAYBACK_TRACKING, authHeaders); + connection.setFixedLengthStreamingMode(requestBody.length); + connection.getOutputStream().write(requestBody); + + final int responseCode = connection.getResponseCode(); + if (responseCode != Requester.HTTP_STATUS_CODE_SUCCESS) { + Logger.printInfo(() -> "Playback tracking failed with code: " + responseCode); + return; + } + + JSONObject playbackTracking = Requester.parseJSONObjectAndDisconnect(connection) + .optJSONObject("playbackTracking"); + if (playbackTracking == null) { + Logger.printInfo(() -> "No playback tracking: " + videoId); + return; + } + + // The playback report starts the playback, the watch time report adds it to the watch history. + if (sendReport(getBaseUrl(playbackTracking, "videostatsPlaybackUrl"), false, authHeaders) + && sendReport(getBaseUrl(playbackTracking, "videostatsWatchtimeUrl"), true, authHeaders)) { + Logger.printDebug(() -> "Marked as watched: " + videoId); + } + } catch (Exception ex) { + Logger.printInfo(() -> "Mark as watched failed: " + videoId, ex); + } + } + + @Nullable + private static String getBaseUrl(JSONObject playbackTracking, String key) { + JSONObject url = playbackTracking.optJSONObject(key); + return url == null ? null : url.optString("baseUrl", null); + } + + /** + * @param isWatchTime If the report is the watch time report, which needs the watched range. + * @return If the report was sent. + */ + private static boolean sendReport(@Nullable String baseUrl, boolean isWatchTime, + Map authHeaders) throws Exception { + if (baseUrl == null || baseUrl.isEmpty()) { + Logger.printInfo(() -> "No report url, watch time: " + isWatchTime); + return false; + } + + Uri uri = Uri.parse(baseUrl); + Map parameters = new LinkedHashMap<>(); + for (String name : uri.getQueryParameterNames()) { + parameters.put(name, uri.getQueryParameter(name)); + } + + // Right before the end of the video. + double length = 1.5; + try { + String lengthParameter = parameters.get("len"); + if (lengthParameter != null) { + length = Double.parseDouble(lengthParameter); + } + } catch (NumberFormatException ignored) { + } + String position = String.valueOf(Math.max(0, length - 1)); + + parameters.put("ver", "2"); + parameters.put("cpn", createCpn()); + parameters.put("cmt", position); + // Otherwise the playback is reported as a Short. + parameters.put("el", "detailpage"); + if (isWatchTime) { + parameters.put("st", "0"); + parameters.put("et", position); + } + + Uri.Builder builder = uri.buildUpon().clearQuery(); + for (Map.Entry parameter : parameters.entrySet()) { + builder.appendQueryParameter(parameter.getKey(), parameter.getValue()); + } + + HttpURLConnection connection = Requester.openConnection(builder.build().toString()); + try { + connection.setRequestProperty("User-Agent", PlaylistRoutes.getUserAgent()); + for (Map.Entry header : authHeaders.entrySet()) { + String value = header.getValue(); + if (value != null && !value.isEmpty()) { + connection.setRequestProperty(header.getKey(), value); + } + } + connection.setUseCaches(false); + connection.setConnectTimeout(CONNECTION_TIMEOUT_MILLISECONDS); + connection.setReadTimeout(CONNECTION_TIMEOUT_MILLISECONDS); + + final int responseCode = connection.getResponseCode(); + if (responseCode / 100 != 2) { + Logger.printInfo(() -> "Report failed with code: " + responseCode + ", watch time: " + isWatchTime); + return false; + } + return true; + } finally { + connection.disconnect(); + } + } + + private static String createCpn() { + StringBuilder cpn = new StringBuilder(CPN_LENGTH); + for (int i = 0; i < CPN_LENGTH; i++) { + cpn.append(CPN_ALPHABET.charAt(random.nextInt(256) & 63)); + } + return cpn.toString(); + } +} diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/requests/PlaylistRoutes.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/requests/PlaylistRoutes.java index dab0bdd26d..49bc018859 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/requests/PlaylistRoutes.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/requests/PlaylistRoutes.java @@ -67,6 +67,13 @@ public final class PlaylistRoutes { "&prettyPrint=false" ).compile(); + /** + * The urls that report the playback of a video, which add the video to the watch history. + */ + public static final Route.CompiledRoute GET_PLAYBACK_TRACKING = new Route( + Route.Method.POST, "player?fields=playbackTracking&prettyPrint=false" + ).compile(); + private PlaylistRoutes() { } @@ -158,6 +165,17 @@ public static byte[] getPlaylistsBody(String playlistId) { return new byte[0]; } + public static byte[] playbackTrackingBody(String videoId) { + try { + JSONObject body = getBaseContentJson(); + body.put("videoId", videoId); + return body.toString().getBytes(StandardCharsets.UTF_8); + } catch (JSONException ex) { + Logger.printException(() -> "playbackTrackingBody failed", ex); + } + return new byte[0]; + } + public static byte[] getMixPlaylistBody(String videoId) { try { JSONObject body = getBaseContentJson(); @@ -200,15 +218,17 @@ public static byte[] savePlaylistBody(String playlistId, String libraryId) { return new byte[0]; } - public static HttpURLConnection getConnection(Route.CompiledRoute route, Map authHeaders) throws IOException { - String userAgent = String.format(Locale.US, + public static String getUserAgent() { + return String.format(Locale.US, "%s/%s (Linux; U; Android %s; %s; %s Build/%s)", PACKAGE_NAME, CLIENT_VERSION, Build.VERSION.RELEASE, Locale.getDefault(), Build.MODEL, Build.ID); + } + public static HttpURLConnection getConnection(Route.CompiledRoute route, Map authHeaders) throws IOException { HttpURLConnection connection = Requester.getConnectionFromCompiledRoute(YT_API_URL, route); connection.setRequestProperty("Content-Type", "application/json"); - connection.setRequestProperty("User-Agent", userAgent); + connection.setRequestProperty("User-Agent", getUserAgent()); connection.setRequestProperty("X-YouTube-Client-Name", String.valueOf(CLIENT_ID)); connection.setRequestProperty("X-YouTube-Client-Version", CLIENT_VERSION); connection.setRequestProperty("X-GOOG-API-FORMAT-VERSION", "2"); diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/Settings.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/Settings.java index e8aa587ac9..8ac3c9305a 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/Settings.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/Settings.java @@ -25,6 +25,7 @@ import app.morphe.extension.shared.settings.Setting; import app.morphe.extension.shared.settings.SharedYouTubeSettings; import app.morphe.extension.shared.settings.StringSetting; +import app.morphe.extension.shared.settings.preference.ExternalDownloaderPreference.ExternalDownloaderAvailability; import app.morphe.extension.shared.settings.preference.SeekBarPreference; import app.morphe.extension.shared.settings.preference.SeekBarPreference.SeekBarConfig; import app.morphe.extension.shared.sponsorblock.SegmentPlaybackController.SponsorBlockDuration; @@ -297,6 +298,8 @@ public class Settings extends SharedYouTubeSettings { public static final BooleanSetting HIDE_HANDLE = new BooleanSetting("morphe_hide_handle", FALSE); public static final EnumSetting OPEN_VIDEOS_FULLSCREEN = new EnumSetting<>("morphe_open_videos_fullscreen", OpenFullscreenMode.DISABLED); public static final BooleanSetting SANITIZE_VIDEO_SUBTITLE = new BooleanSetting("morphe_sanitize_video_subtitle", FALSE); + public static final BooleanSetting EXTERNAL_DOWNLOADER_QUICK_MENU = new BooleanSetting("morphe_external_downloader_quick_menu", FALSE, new ExternalDownloaderAvailability()); + public static final BooleanSetting EXTERNAL_DOWNLOADER_MARK_WATCHED = new BooleanSetting("morphe_external_downloader_mark_watched", FALSE, new ExternalDownloaderAvailability()); // Overlay buttons public static final BooleanSetting FULLSCREEN_VIDEO_SCALE_BUTTON = new BooleanSetting("morphe_fullscreen_video_scale_button", FALSE, true); diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/videoplayer/ExternalDownloadButton.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/videoplayer/ExternalDownloadButton.java index dd840f5646..396469cf31 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/videoplayer/ExternalDownloadButton.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/videoplayer/ExternalDownloadButton.java @@ -10,12 +10,34 @@ package app.morphe.extension.youtube.videoplayer; +import static app.morphe.extension.shared.StringRef.str; + +import android.content.Context; +import android.content.res.ColorStateList; +import android.graphics.Typeface; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.RippleDrawable; +import android.view.Gravity; import android.view.View; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import androidx.annotation.Nullable; import app.morphe.extension.shared.Logger; +import app.morphe.extension.shared.ResourceUtils; +import app.morphe.extension.shared.Utils; import app.morphe.extension.shared.settings.preference.ExternalDownloaderPreference; +import app.morphe.extension.shared.theme.ThemeUtils; +import app.morphe.extension.shared.ui.Dim; +import app.morphe.extension.shared.ui.SheetBottomDialog; import app.morphe.extension.youtube.patches.VideoInformation; +import app.morphe.extension.youtube.patches.utils.requests.MarkWatchedRequest; import app.morphe.extension.youtube.settings.Settings; +import app.morphe.extension.youtube.shared.PipDismissHelper; +import app.morphe.extension.youtube.shared.PlayerType; import app.morphe.extension.youtube.videoplayer.LegacyPlayerControlButton.ButtonVisibility; @SuppressWarnings("unused") @@ -41,7 +63,7 @@ public static void initializeLegacyButton(View controlsView) { ? ButtonVisibility.ENABLED : ButtonVisibility.DISABLED, ExternalDownloadButton::onDownloadClick, - null + ExternalDownloadButton::onDownloadLongClick ); } catch (Exception ex) { Logger.printException(() -> "initializeButton failure", ex); @@ -52,8 +74,195 @@ private static boolean isEnabled() { return Settings.EXTERNAL_DOWNLOADER_OVERLAY_BUTTON.get(); } + private static String getVideoUrl(String videoId) { + return "https://youtu.be/" + videoId; + } + + /** + * @return If a tap on a download button opens the quick download menu. + */ + public static boolean quickMenuOnTap() { + return Settings.EXTERNAL_DOWNLOADER_QUICK_MENU.get() + && ExternalDownloaderPreference.supportsQuickDownload(); + } + + /** + * Opens the external downloader, and marks the video as watched if that is on. + * The video of the open player is not marked, the app adds it to the watch history itself. + * + * @param quickDownloadType See {@link ExternalDownloaderPreference#launchExternalDownloader(String, Context, String, String)}. + */ + public static void launchDownloader(@Nullable Context context, String videoId, + @Nullable String quickDownloadType) { + if (ExternalDownloaderPreference.launchExternalDownloader( + videoId, context, getVideoUrl(videoId), quickDownloadType) + && Settings.EXTERNAL_DOWNLOADER_MARK_WATCHED.get() + && !isOpenInPlayer(videoId)) { + MarkWatchedRequest.markWatched(videoId); + } + } + + private static boolean isOpenInPlayer(String videoId) { + return !PlayerType.getCurrent().isNoneOrHidden() + && videoId.equals(VideoInformation.getVideoId()); + } + + private static void openDownloader(Context context) { + launchDownloader(context, VideoInformation.getVideoId(), null); + } + private static void onDownloadClick(View view) { - ExternalDownloaderPreference.launchExternalDownloader(VideoInformation.getVideoId(), - view.getContext(), "https://youtu.be/" + VideoInformation.getVideoId()); + if (quickMenuOnTap()) { + showQuickDownloadMenu(view.getContext(), VideoInformation.getVideoId()); + } else { + openDownloader(view.getContext()); + } + } + + /** + * Tap and hold does what a tap does not: + * the quick download menu, or all options of the downloader if the menu opens on tap. + */ + private static boolean onDownloadLongClick(View view) { + if (!ExternalDownloaderPreference.supportsQuickDownload()) { + return false; + } + if (quickMenuOnTap()) { + openDownloader(view.getContext()); + } else { + showQuickDownloadMenu(view.getContext(), VideoInformation.getVideoId()); + } + return true; + } + + /** + * Shows the quick download menu for a video. + * Also used by the YouTube download button and the feed 'Download' menu item + * in External downloader mode. + * + * @param videoId The video to download. Captured by the caller, the player may move on + * while the menu is open, and feed menus are not for the playing video. + */ + public static void showQuickDownloadMenu(Context context, String videoId) { + try { + final boolean showQuality = ExternalDownloaderPreference.supportsQuickDownloadQuality(); + final int foregroundColor = ThemeUtils.getAppForegroundColor(); + + SheetBottomDialog.DraggableLinearLayout mainLayout = + SheetBottomDialog.createMainLayout(context, LegacyPlayerControlButton.getDialogBackgroundColor()); + mainLayout.setPadding(0, 0, 0, Dim.dp8); + + TextView header = new TextView(context); + header.setText(str("morphe_external_downloader_quick_menu_header")); + header.setTextColor(foregroundColor); + header.setTextSize(16); + header.setTypeface(Typeface.create("sans-serif-medium", Typeface.NORMAL)); + header.setPadding(Dim.dp16, Dim.dp12, Dim.dp16, Dim.dp8); + mainLayout.addView(header); + + SheetBottomDialog.SlideDialog dialog = + SheetBottomDialog.createSlideDialog(context, mainLayout, LegacyPlayerControlButton.fadeInDuration); + + mainLayout.addView(createItem(context, + "morphe_external_downloader_quick_video", + str("morphe_external_downloader_quick_video"), + showQuality ? str("morphe_external_downloader_quick_video_quality") : null, + () -> { + dialog.dismiss(); + launchDownloader(context, videoId, "video"); + })); + mainLayout.addView(createItem(context, + "morphe_external_downloader_quick_audio", + str("morphe_external_downloader_quick_audio"), + showQuality ? str("morphe_external_downloader_quick_audio_quality") : null, + () -> { + dialog.dismiss(); + launchDownloader(context, videoId, "audio"); + })); + + View divider = new View(context); + divider.setBackgroundColor((foregroundColor & 0x00FFFFFF) | 0x1F000000); + LinearLayout.LayoutParams dividerParams = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, Math.max(1, Dim.dp1)); + dividerParams.setMargins(Dim.dp16, Dim.dp4, Dim.dp16, Dim.dp4); + mainLayout.addView(divider, dividerParams); + + mainLayout.addView(createItem(context, + "morphe_settings_screen_05_player", + str("morphe_external_downloader_quick_options"), + null, + () -> { + dialog.dismiss(); + launchDownloader(context, videoId, null); + })); + + PipDismissHelper.dismissOnPip(dialog); + dialog.show(); + } catch (Exception ex) { + Logger.printException(() -> "showQuickDownloadMenu failure", ex); + } + } + + private static View createItem(Context context, String iconName, String title, + @Nullable String detail, Runnable onClick) { + final int foregroundColor = ThemeUtils.getAppForegroundColor(); + + LinearLayout row = new LinearLayout(context); + row.setOrientation(LinearLayout.HORIZONTAL); + row.setGravity(Gravity.CENTER_VERTICAL); + row.setMinimumHeight(Dim.dp48 + Dim.dp4); + row.setPadding(Dim.dp16, 0, Dim.dp16, 0); + row.setClickable(true); + row.setFocusable(true); + row.setBackground(new RippleDrawable( + ColorStateList.valueOf((foregroundColor & 0x00FFFFFF) | 0x33000000), + null, + new ColorDrawable(0xFFFFFFFF))); + row.setOnClickListener(v -> onClick.run()); + + Drawable icon = getIcon(context, iconName); + if (icon != null) { + icon.setTint(foregroundColor); + ImageView iconView = new ImageView(context); + iconView.setImageDrawable(icon); + LinearLayout.LayoutParams iconParams = new LinearLayout.LayoutParams(Dim.dp24, Dim.dp24); + iconParams.setMarginEnd(Dim.dp24); + row.addView(iconView, iconParams); + } + + TextView titleView = new TextView(context); + titleView.setText(title); + titleView.setTextColor(foregroundColor); + titleView.setTextSize(16); + titleView.setSingleLine(true); + row.addView(titleView, new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1)); + + if (detail != null) { + TextView detailView = new TextView(context); + detailView.setText(detail); + detailView.setTextColor((foregroundColor & 0x00FFFFFF) | 0xA6000000); + detailView.setTextSize(14); + detailView.setSingleLine(true); + LinearLayout.LayoutParams detailParams = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT); + detailParams.setMarginStart(Dim.dp12); + row.addView(detailView, detailParams); + } + + return row; + } + + @Nullable + private static Drawable getIcon(Context context, String baseName) { + final String name = Utils.appIsUsingBoldIcons() ? baseName + "_bold" : baseName; + try { + final int id = ResourceUtils.getDrawableIdentifier(name); + if (id == 0) return null; + Drawable drawable = context.getDrawable(id); + return drawable == null ? null : drawable.mutate(); + } catch (Exception ex) { + Logger.printDebug(() -> "Icon not found: " + name); + return null; + } } } diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/downloads/DownloadsPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/downloads/DownloadsPatch.kt index 9f9f7b8d09..51aef25ada 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/downloads/DownloadsPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/downloads/DownloadsPatch.kt @@ -25,6 +25,7 @@ import app.morphe.patches.shared.misc.settings.preference.PreferenceScreenPrefer import app.morphe.patches.shared.misc.settings.preference.SwitchPreference import app.morphe.patches.shared.misc.settings.preference.TextPreference import app.morphe.patches.youtube.layout.player.icons.copyPlayerButtonIcons +import app.morphe.patches.youtube.misc.auth.authHookPatch import app.morphe.patches.youtube.misc.playercontrols.addTopControl import app.morphe.patches.youtube.misc.playercontrols.initializeTopControl import app.morphe.patches.youtube.misc.playercontrols.legacyPlayerControlsPatch @@ -33,6 +34,8 @@ import app.morphe.patches.youtube.misc.settings.settingsPatch import app.morphe.patches.youtube.misc.spoof.spoofVideoStreamsPatch import app.morphe.patches.youtube.shared.Constants.COMPATIBILITY_YOUTUBE import app.morphe.patches.youtube.video.information.videoInformationPatch +import app.morphe.util.ResourceGroup +import app.morphe.util.copyResources import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation import com.android.tools.smali.dexlib2.iface.reference.FieldReference @@ -57,12 +60,25 @@ private val downloadsResourcePatch = resourcePatch { TextPreference( "morphe_external_downloader_name", tag = "app.morphe.extension.shared.settings.preference.ExternalDownloaderPreference", - ) + ), + SwitchPreference("morphe_external_downloader_quick_menu", summary = true), + SwitchPreference("morphe_external_downloader_mark_watched", summary = true) ) ) ) copyPlayerButtonIcons("downloads", "morphe_yt_download_button") + + copyResources( + "downloads", + ResourceGroup( + "drawable", + "morphe_external_downloader_quick_video.xml", + "morphe_external_downloader_quick_video_bold.xml", + "morphe_external_downloader_quick_audio.xml", + "morphe_external_downloader_quick_audio_bold.xml" + ) + ) } finalize { @@ -92,6 +108,7 @@ val downloadsPatch = bytecodePatch( ) { dependsOn( downloadsResourcePatch, + authHookPatch, videoInformationPatch, legacyPlayerControlsPatch, fixProtoLibraryPatch, diff --git a/patches/src/main/resources/addresources/values/youtube/strings.xml b/patches/src/main/resources/addresources/values/youtube/strings.xml index 2e2b19d556..0404b8e747 100644 --- a/patches/src/main/resources/addresources/values/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values/youtube/strings.xml @@ -513,6 +513,16 @@ After a video with the Ask button is opened once, the feature works with all vid Download videos, podcasts, or songs with third-party downloading apps Show player overlay download button Shows a button in the video player that opens your external downloader + Quick download on tap + Prompts to download in the background. Tap and hold to open the downloader. Works with YTDLnis and YTindir + Mark downloads as watched + Videos downloaded with an external downloader are added to your YouTube watch history + Full HD (1080p) + Medium (192 kbps) + Quick download + Video + Audio + All options Voice over translation diff --git a/patches/src/main/resources/downloads/drawable/morphe_external_downloader_quick_audio.xml b/patches/src/main/resources/downloads/drawable/morphe_external_downloader_quick_audio.xml new file mode 100644 index 0000000000..229fcb6e0d --- /dev/null +++ b/patches/src/main/resources/downloads/drawable/morphe_external_downloader_quick_audio.xml @@ -0,0 +1,29 @@ + + + + + + diff --git a/patches/src/main/resources/downloads/drawable/morphe_external_downloader_quick_audio_bold.xml b/patches/src/main/resources/downloads/drawable/morphe_external_downloader_quick_audio_bold.xml new file mode 100644 index 0000000000..0dcb9b9d31 --- /dev/null +++ b/patches/src/main/resources/downloads/drawable/morphe_external_downloader_quick_audio_bold.xml @@ -0,0 +1,29 @@ + + + + + + diff --git a/patches/src/main/resources/downloads/drawable/morphe_external_downloader_quick_video.xml b/patches/src/main/resources/downloads/drawable/morphe_external_downloader_quick_video.xml new file mode 100644 index 0000000000..2f51dc060d --- /dev/null +++ b/patches/src/main/resources/downloads/drawable/morphe_external_downloader_quick_video.xml @@ -0,0 +1,29 @@ + + + + + + diff --git a/patches/src/main/resources/downloads/drawable/morphe_external_downloader_quick_video_bold.xml b/patches/src/main/resources/downloads/drawable/morphe_external_downloader_quick_video_bold.xml new file mode 100644 index 0000000000..38e9ef5329 --- /dev/null +++ b/patches/src/main/resources/downloads/drawable/morphe_external_downloader_quick_video_bold.xml @@ -0,0 +1,29 @@ + + + + + + From 372eea295ec4521b9ccdb93a3f188f81a6ad1a6c Mon Sep 17 00:00:00 2001 From: Kim Song Date: Sun, 25 Oct 2026 20:47:46 +0700 Subject: [PATCH 051/189] feat(YouTube - Add to queue): Add "Native queue" setting (#3953) Co-authored-by: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> --- .../lyrics/romanizers/PinyinDictStore.java | 1 + extensions/youtube/build.gradle.kts | 1 + .../youtube/patches/AddToQueuePatch.java | 108 +- .../youtube/patches/LoadVideoPatch.java | 31 +- .../youtube/patches/LocalQueuePatch.java | 5 +- .../OpenVideosFullscreenHookPatch.java | 34 +- .../RestoreOriginalTitlesPatch.java | 2 + .../patches/queue/MixPlaylistRequest.java | 101 + .../queue/NativePlaylistQueuePatch.java | 2578 +++++++++++++++++ .../queue/PlaylistMetadataRequest.java | 71 + .../youtube/patches/queue/PlaylistQueue.java | 165 ++ .../patches/queue/QueueFlyoutActions.java | 170 ++ .../patches/queue/QueueFullscreenActions.java | 93 + .../patches/queue/QueueMixActions.java | 38 + .../patches/queue/QueueRatingActions.java | 122 + .../patches/queue/QueueSaveActions.java | 223 ++ .../patches/queue/QueueSaveFeedback.java | 109 + .../patches/queue/QueueShortsSession.java | 65 + .../youtube/patches/queue/QueueStart.java | 53 + .../patches/queue/QueueUndoActions.java | 65 + .../youtube/patches/queue/VideoMetadata.java | 232 ++ .../patches/utils/FeedVideoCardsPatch.java | 68 +- .../youtube/patches/utils/FlyoutUtils.java | 4 + .../utils/requests/ChannelIdRequest.java | 13 +- .../utils/requests/ChannelIdRoutes.java | 10 +- .../extension/youtube/settings/Settings.java | 23 +- .../youtube/innertube/next_response.proto | 39 +- .../youtube/innertube/playlist_queue.proto | 368 +++ .../youtube/innertube/queue_card.proto | 77 + .../youtube/innertube/queue_fullscreen.proto | 56 + .../patches/queue/MixPlaylistRequestTest.java | 86 + .../patches/queue/PlaylistQueueTest.java | 157 + .../patches/queue/QueueFlyoutActionsTest.java | 161 + .../patches/queue/QueueRatingActionsTest.java | 111 + .../patches/queue/QueueSaveActionsTest.java | 190 ++ .../patches/queue/QueueSaveFeedbackTest.java | 85 + .../patches/queue/QueueShortsSessionTest.java | 89 + .../youtube/patches/queue/QueueStartTest.java | 60 + .../patches/queue/QueueUndoActionsTest.java | 59 + .../patches/queue/VideoMetadataTest.java | 143 + patches/build.gradle.kts | 31 + .../shared/misc/audio/silence/Fingerprints.kt | 16 +- .../misc/audio/silence/SkipSilencePatch.kt | 7 +- .../nativeplaylistqueue/BridgeMethods.kt | 65 + .../nativeplaylistqueue/Fingerprints.kt | 1346 +++++++++ .../FullscreenPlaylistHook.kt | 88 + .../NativePlaylistQueuePatch.kt | 99 + .../NativeQueueEngineHook.kt | 596 ++++ .../nativeplaylistqueue/PlaylistMenuHook.kt | 47 + .../PlaylistNavigationHook.kt | 79 + .../PlaylistNotificationControlsHook.kt | 121 + .../PlaylistNotificationHook.kt | 193 ++ .../nativeplaylistqueue/PlaylistPanelHook.kt | 280 ++ .../PlaylistPlayerControlsHook.kt | 121 + .../PlaylistRemovalHook.kt | 108 + .../PlaylistReorderHook.kt | 94 + .../nativeplaylistqueue/PlaylistRepeatHook.kt | 132 + .../nativeplaylistqueue/PlaylistRowHook.kt | 237 ++ .../nativeplaylistqueue/PlaylistTitleHook.kt | 73 + .../nativeplaylistqueue/QueueFlyoutHook.kt | 117 + .../QueuePlayerCloseHook.kt | 70 + .../QueueSaveFeedbackHook.kt | 126 + .../QueueSavePickerHook.kt | 97 + .../nativeplaylistqueue/QueueUndoHook.kt | 62 + .../nativeplaylistqueue/RatingActionsHook.kt | 30 + .../interaction/shorts/SkipLiveShortsPatch.kt | 8 +- .../youtube/layout/flyout/AddToQueuePatch.kt | 21 +- .../layout/shortsautoplay/Fingerprints.kt | 9 +- .../shortsautoplay/ShortsAutoplayPatch.kt | 10 +- .../youtube/shared/OpenVideoIntentPatch.kt | 64 + .../information/VideoInformationPatch.kt | 3 +- .../youtube/video/series/HistoryPatch.kt | 6 +- .../addresources/values/youtube/arrays.xml | 25 + .../addresources/values/youtube/strings.xml | 27 +- .../PlaylistQueueApkBuild.kt | 779 +++++ .../QueueFullscreenBoundaryTest.kt | 124 + .../QueueRuntimeBoundaryTest.kt | 1578 ++++++++++ .../QueueSaveBoundaryTest.kt | 103 + 78 files changed, 13008 insertions(+), 50 deletions(-) create mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/queue/MixPlaylistRequest.java create mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/queue/NativePlaylistQueuePatch.java create mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/queue/PlaylistMetadataRequest.java create mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/queue/PlaylistQueue.java create mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/queue/QueueFlyoutActions.java create mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/queue/QueueFullscreenActions.java create mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/queue/QueueMixActions.java create mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/queue/QueueRatingActions.java create mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/queue/QueueSaveActions.java create mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/queue/QueueSaveFeedback.java create mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/queue/QueueShortsSession.java create mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/queue/QueueStart.java create mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/queue/QueueUndoActions.java create mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/queue/VideoMetadata.java create mode 100644 extensions/youtube/src/main/proto/app/morphe/extension/youtube/innertube/playlist_queue.proto create mode 100644 extensions/youtube/src/main/proto/app/morphe/extension/youtube/innertube/queue_card.proto create mode 100644 extensions/youtube/src/main/proto/app/morphe/extension/youtube/innertube/queue_fullscreen.proto create mode 100644 extensions/youtube/src/test/java/app/morphe/extension/youtube/patches/queue/MixPlaylistRequestTest.java create mode 100644 extensions/youtube/src/test/java/app/morphe/extension/youtube/patches/queue/PlaylistQueueTest.java create mode 100644 extensions/youtube/src/test/java/app/morphe/extension/youtube/patches/queue/QueueFlyoutActionsTest.java create mode 100644 extensions/youtube/src/test/java/app/morphe/extension/youtube/patches/queue/QueueRatingActionsTest.java create mode 100644 extensions/youtube/src/test/java/app/morphe/extension/youtube/patches/queue/QueueSaveActionsTest.java create mode 100644 extensions/youtube/src/test/java/app/morphe/extension/youtube/patches/queue/QueueSaveFeedbackTest.java create mode 100644 extensions/youtube/src/test/java/app/morphe/extension/youtube/patches/queue/QueueShortsSessionTest.java create mode 100644 extensions/youtube/src/test/java/app/morphe/extension/youtube/patches/queue/QueueStartTest.java create mode 100644 extensions/youtube/src/test/java/app/morphe/extension/youtube/patches/queue/QueueUndoActionsTest.java create mode 100644 extensions/youtube/src/test/java/app/morphe/extension/youtube/patches/queue/VideoMetadataTest.java create mode 100644 patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/BridgeMethods.kt create mode 100644 patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/Fingerprints.kt create mode 100644 patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/FullscreenPlaylistHook.kt create mode 100644 patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/NativePlaylistQueuePatch.kt create mode 100644 patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/NativeQueueEngineHook.kt create mode 100644 patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/PlaylistMenuHook.kt create mode 100644 patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/PlaylistNavigationHook.kt create mode 100644 patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/PlaylistNotificationControlsHook.kt create mode 100644 patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/PlaylistNotificationHook.kt create mode 100644 patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/PlaylistPanelHook.kt create mode 100644 patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/PlaylistPlayerControlsHook.kt create mode 100644 patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/PlaylistRemovalHook.kt create mode 100644 patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/PlaylistReorderHook.kt create mode 100644 patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/PlaylistRepeatHook.kt create mode 100644 patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/PlaylistRowHook.kt create mode 100644 patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/PlaylistTitleHook.kt create mode 100644 patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/QueueFlyoutHook.kt create mode 100644 patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/QueuePlayerCloseHook.kt create mode 100644 patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/QueueSaveFeedbackHook.kt create mode 100644 patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/QueueSavePickerHook.kt create mode 100644 patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/QueueUndoHook.kt create mode 100644 patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/RatingActionsHook.kt create mode 100644 patches/src/test/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/PlaylistQueueApkBuild.kt create mode 100644 patches/src/test/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/QueueFullscreenBoundaryTest.kt create mode 100644 patches/src/test/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/QueueRuntimeBoundaryTest.kt create mode 100644 patches/src/test/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/QueueSaveBoundaryTest.kt diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/PinyinDictStore.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/PinyinDictStore.java index ee89b2fc2d..8a3a9da204 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/PinyinDictStore.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/PinyinDictStore.java @@ -335,6 +335,7 @@ private static String first(String value) { if (cut >= 0) reading = reading.substring(0, cut).trim(); cut = reading.indexOf(' '); if (cut > 0) reading = reading.substring(0, cut); + //noinspection SizeReplaceableByIsEmpty if (reading.length() > 0) return reading.toLowerCase(Locale.ROOT); } return null; diff --git a/extensions/youtube/build.gradle.kts b/extensions/youtube/build.gradle.kts index 26b9505eb5..7a2fff6a11 100644 --- a/extensions/youtube/build.gradle.kts +++ b/extensions/youtube/build.gradle.kts @@ -6,6 +6,7 @@ plugins { dependencies { testImplementation(libs.junit) + testImplementation("org.json:json:20240303") compileOnly(libs.annotation) compileOnly(libs.morphe.extensions.library) diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/AddToQueuePatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/AddToQueuePatch.java index c853077e67..dc97be88d6 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/AddToQueuePatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/AddToQueuePatch.java @@ -22,6 +22,8 @@ import app.morphe.extension.shared.ResourceType; import app.morphe.extension.shared.ResourceUtils; import app.morphe.extension.shared.Utils; +import app.morphe.extension.shared.settings.Setting; +import app.morphe.extension.youtube.patches.queue.NativePlaylistQueuePatch; import app.morphe.extension.youtube.patches.utils.FlyoutUtils; import app.morphe.extension.youtube.patches.utils.LocalQueueSheet; import app.morphe.extension.youtube.patches.utils.PlaylistPatch; @@ -42,12 +44,66 @@ public final class AddToQueuePatch { private static final int SECONDARY_CONTAINER_ID = ResourceUtils.getIdentifier(ResourceType.ID, "list_item_secondary_container"); + public enum QueueType { + /** No queue features are added. */ + DEFAULT, + /** YouTube's own playlist interface, provided by the Native playlist queue patch. */ + NATIVE, + /** The Morphe queue, stored as a playlist on the YouTube account. */ + MORPHE, + /** The Premium-style queue, stored on the device. */ + LOCAL + } + + public static final class MorpheQueueAvailability implements Setting.Availability { + @Override + public boolean isAvailable() { + return Settings.QUEUE_TYPE.get() == QueueType.MORPHE; + } + + @Override + public List> getParentSettings() { + return List.of(Settings.QUEUE_TYPE); + } + } + + public static final class NotLocalQueueAvailability implements Setting.Availability { + @Override + public boolean isAvailable() { + return Settings.QUEUE_TYPE.get() != QueueType.LOCAL; + } + + @Override + public List> getParentSettings() { + return List.of(Settings.QUEUE_TYPE); + } + } + public static boolean isPatchIncluded() { return false; // Modified during patching. } - public static int addFlyoutButton(Object flyoutPanel, int index, String videoId) { + /** + * @return If the native queue is selected. It handles the stock queue buttons. + */ + public static boolean isNativeQueueEnabled() { + return Settings.QUEUE_TYPE.get() == QueueType.NATIVE; + } + + /** + * @return If the Morphe queue or the Premium-style queue is selected. + */ + public static boolean isCustomQueueEnabled() { if (!isPatchIncluded()) { + return false; + } + + QueueType type = Settings.QUEUE_TYPE.get(); + return type == QueueType.MORPHE || type == QueueType.LOCAL; + } + + public static int addFlyoutButton(Object flyoutPanel, int index, String videoId) { + if (!isCustomQueueEnabled()) { return index; } @@ -74,7 +130,8 @@ public static int addFlyoutButton(Object flyoutPanel, int index, String videoId) } public static void onListBound(ViewGroup itemList) { - if (!isPatchIncluded() || !isFlyoutOverrideEnabled()) { + final boolean nativeQueue = isNativeQueueEnabled(); + if (!isCustomQueueEnabled() && !nativeQueue) { return; } @@ -91,6 +148,12 @@ public static void onListBound(ViewGroup itemList) { } android.view.View item = itemList.getChildAt(itemIndex); + if (nativeQueue && queueButtonOriginalNames.get(1).equals(buttonName) + && !NativePlaylistQueuePatch.canShowFlyoutLast()) { + // The queue may have been cleared since the native model was prepared. + item.setVisibility(android.view.View.GONE); + continue; + } var badge = SECONDARY_CONTAINER_ID == 0 ? null : item.findViewById(SECONDARY_CONTAINER_ID); if (badge != null && badge.getVisibility() != android.view.View.GONE) { Logger.printDebug(() -> "Hiding the menu item secondary icon"); @@ -113,10 +176,6 @@ public static void onListBound(ViewGroup itemList) { } } - private static boolean isFlyoutOverrideEnabled() { - return LocalQueuePatch.isEnabled() || Settings.QUEUE_OVERRIDE_FLYOUT_MENU.get(); - } - private static boolean hasFlyoutButton(String buttonName) { for (Pair button : FlyoutUtils.getVisibleFlyoutButtons()) { if (button.first.equals(buttonName)) { @@ -176,7 +235,12 @@ private static int addLocalQueueButtons(Object flyoutPanel, int index, String vi * Injection point. */ public static Runnable replaceButtonRunnable(Runnable original) { - if (!isFlyoutOverrideEnabled()) { + if (isNativeQueueEnabled()) { + Runnable action = getNativeQueueAction(FlyoutUtils.getCurrentButtonName(), + FlyoutUtils.getFlyoutVideoId()); + return action == null ? original : action; + } + if (!isCustomQueueEnabled()) { return original; } @@ -200,10 +264,13 @@ public static Runnable replaceButtonRunnable(Runnable original) { */ public static boolean replaceOnItemClick(Object object) { try { - if (!isFlyoutOverrideEnabled()) { + if (!isCustomQueueEnabled() && !isNativeQueueEnabled()) { return false; } + if (isNativeQueueEnabled() && object instanceof String name) { + return flyoutButtonClickLogic(name, FlyoutUtils.getFlyoutVideoId()); + } if (FlyoutUtils.getFlyoutVideoId().isEmpty()) { Logger.printDebug(() -> "Cannot replace on item click, flyoutVideoId is empty"); return false; @@ -253,7 +320,13 @@ private static Runnable getNewRunnable(@Nullable Runnable original, String butto } public static boolean flyoutButtonClickLogic(String buttonName, String videoId) { + final boolean nativeAction = isNativeQueueEnabled() + && queueButtonOriginalNames.contains(buttonName); try { + if (nativeAction) { + getNativeQueueAction(buttonName, videoId).run(); + return true; + } if (queueButtonOriginalNames.contains(buttonName)) { if (LocalQueuePatch.isEnabled()) { LocalQueuePatch.add(videoId, queueButtonOriginalNames.get(0).equals(buttonName)); @@ -277,6 +350,23 @@ public static boolean flyoutButtonClickLogic(String buttonName, String videoId) Logger.printException(() -> "flyoutButtonClickLogic failure: " + buttonName, ex); } - return false; + // An error in a Native-owned action must not dispatch the original + // Premium command through the caller's fallback path. + return nativeAction; + } + + @Nullable + private static Runnable getNativeQueueAction(String buttonName, String videoId) { + if (!queueButtonOriginalNames.contains(buttonName)) { + return null; + } + Runnable action = NativePlaylistQueuePatch.getFlyoutQueueAction(videoId, + queueButtonOriginalNames.get(1).equals(buttonName)); + // An unsupported/stale menu context cannot silently invoke a Premium or + // account-backed queue when the user explicitly selected Native queue. + return action != null ? action : () -> { + Utils.showToastShort(str("morphe_native_playlist_queue_not_ready")); + FlyoutUtils.dismissFlyout(); + }; } } diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/LoadVideoPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/LoadVideoPatch.java index f73a80978f..e3d805901c 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/LoadVideoPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/LoadVideoPatch.java @@ -17,6 +17,7 @@ import android.os.Parcelable; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import java.lang.ref.WeakReference; import java.util.Objects; @@ -24,6 +25,7 @@ import app.morphe.extension.shared.Logger; import app.morphe.extension.shared.Utils; import app.morphe.extension.youtube.patches.playback.speed.RememberPlaybackSpeedPatch; +import app.morphe.extension.youtube.patches.queue.NativePlaylistQueuePatch; import app.morphe.extension.youtube.shared.PlayerType; import kotlin.Unit; import kotlin.jvm.functions.Function1; @@ -103,12 +105,15 @@ public static void initializeInlinePlayback(@NonNull InlinePlaybackInterface inl * Once the player is dismissed, the video of the feed below it can start playing in the feed. * The player opened next then plays that video instead, with a black screen and a page that never loads. * Playback in feeds is blocked until the player opens again. + * Opens that finish minimized can release the returned token once their player is accepted. + * + * @return the block generation, or zero if feed playback has not initialized. */ - private static void blockInlinePlaybackUntilPlayerOpens() { + public static int blockInlinePlaybackUntilPlayerOpens() { InlinePlaybackInterface inlinePlaybackInterface = inlinePlaybackInterfaceRef.get(); if (inlinePlaybackInterface == null) { Logger.printDebug(() -> "Cannot block playback in feeds, interface is not set"); - return; + return 0; } if (!inlinePlaybackBlocked) { @@ -124,6 +129,14 @@ private static void blockInlinePlaybackUntilPlayerOpens() { unblockInlinePlayback(); } }, INLINE_PLAYBACK_BLOCK_TIMEOUT_MILLISECONDS); + return blockId; + } + + /** A completed or cancelled open must not release a newer operation's protection. */ + public static void releaseInlinePlaybackBlock(int blockId) { + if (blockId != 0 && blockId == inlinePlaybackBlockId) { + unblockInlinePlayback(); + } } private static void unblockInlinePlayback() { @@ -162,6 +175,13 @@ public static void initializeReloadVideo() { String videoId = VideoInformation.getStartingOrPlayingVideoId(); String playlistId = VideoInformation.getPlaylistId(); final long videoTime = VideoInformation.getVideoTime(); + Runnable queueReload = NativePlaylistQueuePatch.isPatchIncluded() + ? NativePlaylistQueuePatch.prepareVideoReload(videoId) : null; + if (queueReload != null) { + // A converted queue can retain its source playlist in the playing + // response. Reload only this video, not that replaced playlist. + playlistId = ""; + } String parameterSeparator = "&"; StringBuilder builder = new StringBuilder(videoId); if (!playlistId.isEmpty()) { @@ -178,7 +198,7 @@ public static void initializeReloadVideo() { String builderString = "https://www.youtube.com/watch?v=" + builder; Logger.printDebug(() -> "Opening: " + builderString); - openVideoIntent(builderString, true); + openVideoIntent(builderString, true, queueReload); } catch (Exception ex) { Logger.printException(() -> "Failed to reload video", ex); } @@ -194,6 +214,10 @@ public static PlayerInterface getPlayerInterface() { } public static void openVideoIntent(String url, boolean closeCurrentPlayerInstance) { + openVideoIntent(url, closeCurrentPlayerInstance, null); + } + + private static void openVideoIntent(String url, boolean closeCurrentPlayerInstance, @Nullable Runnable onDispatch) { int loadVideoDelay = 0; // Close the current player instance. @@ -219,6 +243,7 @@ public static void openVideoIntent(String url, boolean closeCurrentPlayerInstanc Intent intent = new Intent("android.intent.action.VIEW", Uri.parse(url)); intent.setPackage(context.getPackageName()); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + if (onDispatch != null) onDispatch.run(); context.startActivity(intent); }, loadVideoDelay); } diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/LocalQueuePatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/LocalQueuePatch.java index ebc3cfdae1..130c95bcd3 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/LocalQueuePatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/LocalQueuePatch.java @@ -30,6 +30,7 @@ import app.morphe.extension.shared.Logger; import app.morphe.extension.shared.Utils; import app.morphe.extension.shared.requests.Requester; +import app.morphe.extension.youtube.patches.AddToQueuePatch.QueueType; import app.morphe.extension.youtube.settings.Settings; import app.morphe.extension.youtube.shared.PlayerType; import app.morphe.extension.youtube.shared.ShortsPlayerState; @@ -80,7 +81,9 @@ public Item(String videoId, @Nullable String title, @Nullable String author) { private static volatile Consumer metadataListener; public static boolean isEnabled() { - return Settings.LOCAL_QUEUE.get(); + // Playback hooks are installed by Add to queue. A retained setting must not + // enable this queue when that patch is not included. + return AddToQueuePatch.isPatchIncluded() && Settings.QUEUE_TYPE.get() == QueueType.LOCAL; } public static void setListeners(Runnable onChange, Consumer onMetadata) { diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/OpenVideosFullscreenHookPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/OpenVideosFullscreenHookPatch.java index a022341dce..d199abebe2 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/OpenVideosFullscreenHookPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/OpenVideosFullscreenHookPatch.java @@ -40,6 +40,31 @@ public static void setFullscreenInterface(FullscreenInterface fullscreenInterfac fullscreenInterfaceRef = new WeakReference<>(fullscreenInterface); } + private static final int MINIMIZED_START_TIMEOUT_MILLISECONDS = 10_000; + + private record MinimizedStart(String videoId) {} + + @Nullable + private static MinimizedStart videoToKeepMinimized; + + /** Explicit queue starts preserve browsing even with automatic fullscreen enabled. */ + public static void keepVideoMinimized(@Nullable String videoId) { + MinimizedStart start = videoId == null ? null : new MinimizedStart(videoId); + videoToKeepMinimized = start; + + // The fullscreen patch can be absent, so its playback callback may never + // consume this request. Bound its lifetime without expiring a newer start. + if (start != null) { + Utils.runOnMainThreadDelayed(() -> { + if (videoToKeepMinimized == start) videoToKeepMinimized = null; + }, MINIMIZED_START_TIMEOUT_MILLISECONDS); + } + } + + private static boolean keepCurrentVideoMinimized() { + return videoToKeepMinimized != null && videoToKeepMinimized.videoId().equals(VideoInformation.getVideoId()); + } + public static void setOpenNextVideoFullscreen(@Nullable Boolean forceFullScreen) { openNextVideoFullscreen = forceFullScreen; } @@ -69,6 +94,7 @@ private static boolean isPatchIncluded() { * Returns negated value. */ public static boolean doNotOpenVideoFullscreenPortrait(boolean original) { + if (keepCurrentVideoMinimized()) return true; Boolean openFullscreen = openNextVideoFullscreen; if (openFullscreen != null) { openNextVideoFullscreen = null; @@ -96,8 +122,14 @@ public static void initialize(PlaybackController playerController) { */ public static void playerStatusChanged(Enum status) { try { - if (!shouldEnterFullscreen) return; if (status == null || !"VIDEO_PLAYING".equals(status.name())) return; + if (keepCurrentVideoMinimized()) { + shouldEnterFullscreen = false; + videoToKeepMinimized = null; + return; + } + videoToKeepMinimized = null; + if (!shouldEnterFullscreen) return; if (PlayerType.getCurrent() == PlayerType.WATCH_WHILE_FULLSCREEN) { shouldEnterFullscreen = false; diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/originaltitles/RestoreOriginalTitlesPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/originaltitles/RestoreOriginalTitlesPatch.java index 20a928c7b9..71adbcaf99 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/originaltitles/RestoreOriginalTitlesPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/originaltitles/RestoreOriginalTitlesPatch.java @@ -528,6 +528,8 @@ public static String getTitle(String videoId, String title) { if (!REPLACE_TITLES || videoId.isEmpty()) { return title; } + // A title reused from a parsed element can carry this patch's internal marker. + title = removeTitleMarker(title); // The title and the video id can be of different videos while the video changes. if (!isTranslatedTitle(title.trim(), videoId)) { diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/queue/MixPlaylistRequest.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/queue/MixPlaylistRequest.java new file mode 100644 index 0000000000..1434ecd677 --- /dev/null +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/queue/MixPlaylistRequest.java @@ -0,0 +1,101 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/3953 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.extension.youtube.patches.queue; + +import androidx.annotation.Nullable; + +import org.json.JSONObject; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import app.morphe.extension.shared.Logger; +import app.morphe.extension.shared.Utils; +import app.morphe.extension.shared.requests.Requester; +import app.morphe.extension.shared.requests.Route; +import app.morphe.extension.youtube.patches.utils.requests.PlaylistRoutes; + +/** Fetch the initial, finite batch of a dynamic Mix, without fetching player streams. */ +final class MixPlaylistRequest { + private static final String PLAYLIST = "contents.singleColumnWatchNextResults.playlist.playlist"; + private static final String MIX_PATH = + "next?prettyPrint=false&fields=" + PLAYLIST + "(playlistId,contents.playlistPanelVideoRenderer" + + "(videoId,title,shortBylineText,longBylineText,videoInfo,lengthText,unplayableText))"; + + private MixPlaylistRequest() {} + + @Nullable + static List fetch(String playlistId, String seedVideoId, Map headers) { + Utils.verifyOffMainThread(); + HttpURLConnection connection = null; + try { + byte[] body = PlaylistRoutes.getSetVideoIdBody(seedVideoId, playlistId); + connection = PlaylistRoutes.getConnection(new Route(Route.Method.POST, MIX_PATH).compile(), headers); + connection.setFixedLengthStreamingMode(body.length); + try (var output = connection.getOutputStream()) { + output.write(body); + } + if (connection.getResponseCode() == Requester.HTTP_STATUS_CODE_SUCCESS) { + return parse(Requester.parseJSONObject(connection), playlistId); + } + Logger.printInfo(() -> "Native queue Mix request was unavailable"); + } catch (IOException exception) { + Logger.printInfo(() -> "Could not fetch Mix for native queue", exception); + } catch (Exception exception) { + Logger.printException(() -> "Could not parse Mix for native queue", exception); + } finally { + if (connection != null) { + connection.disconnect(); + } + } + + return null; + } + + @Nullable + static List parse(JSONObject response, String playlistId) { + var contents = response.optJSONObject("contents"); + var next = contents == null ? null : contents.optJSONObject("singleColumnWatchNextResults"); + var wrapper = next == null ? null : next.optJSONObject("playlist"); + var playlist = wrapper == null ? null : wrapper.optJSONObject("playlist"); + if (playlist == null || !playlistId.equals(playlist.optString("playlistId"))) { + return null; + } + var rows = playlist.optJSONArray("contents"); + if (rows == null) { + return null; + } + List result = new ArrayList<>(); + for (int index = 0; index < rows.length(); index++) { + var item = rows.optJSONObject(index); + var video = item == null ? null : item.optJSONObject("playlistPanelVideoRenderer"); + if (video == null || video.has("unplayableText")) { + continue; + } + String id = video.optString("videoId"); + if (!PlaylistQueue.isVideoId(id)) { + return null; + } + String channel = VideoMetadata.text(video.optJSONObject("longBylineText")); + boolean fullByline = !channel.isEmpty(); + if (channel.isEmpty()) { + channel = VideoMetadata.text(video.optJSONObject("shortBylineText")); + } + result.add(new VideoMetadata(id, VideoMetadata.text(video.optJSONObject("title")), channel, + VideoMetadata.text(video.optJSONObject("videoInfo")), + VideoMetadata.text(video.optJSONObject("lengthText")), fullByline, true)); + } + + // A Mix is generated dynamically. Append its initial batch, without + // following its indefinite continuation or claiming to import a saved list. + return result.isEmpty() ? null : List.copyOf(result); + } +} diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/queue/NativePlaylistQueuePatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/queue/NativePlaylistQueuePatch.java new file mode 100644 index 0000000000..26573b0d0c --- /dev/null +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/queue/NativePlaylistQueuePatch.java @@ -0,0 +1,2578 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/3953 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.extension.youtube.patches.queue; + +import static app.morphe.extension.shared.StringRef.str; + +import android.content.Context; +import android.content.SharedPreferences; +import android.net.Uri; +import android.os.Bundle; +import android.text.Layout; +import android.util.Base64; +import android.view.View; +import android.view.ViewGroup; +import android.view.accessibility.AccessibilityManager; +import android.widget.TextView; + +import androidx.annotation.Nullable; + +import com.google.protobuf.MessageLite; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.WeakHashMap; + +import app.morphe.extension.shared.ByteTrieSearch; +import app.morphe.extension.shared.Logger; +import app.morphe.extension.shared.Utils; +import app.morphe.extension.shared.innertube.utils.AuthUtils; +import app.morphe.extension.shared.settings.Setting; +import app.morphe.extension.youtube.innertube.IconOuterClass.Icon; +import app.morphe.extension.youtube.innertube.IconOuterClass.YTIconType; +import app.morphe.extension.youtube.innertube.NextResponseOuterClass.SingleColumnWatchNextResults; +import app.morphe.extension.youtube.innertube.queue.PlaylistQueueRenderer; +import app.morphe.extension.youtube.patches.AddToQueuePatch.QueueType; +import app.morphe.extension.youtube.patches.LoadVideoPatch; +import app.morphe.extension.youtube.patches.OpenVideosFullscreenHookPatch; +import app.morphe.extension.youtube.patches.VideoInformation; +import app.morphe.extension.youtube.patches.originaltitles.RestoreOriginalTitlesPatch; +import app.morphe.extension.youtube.patches.utils.FeedVideoCardsPatch; +import app.morphe.extension.youtube.patches.utils.FlyoutUtils; +import app.morphe.extension.youtube.patches.utils.requests.ChannelIdRequest; +import app.morphe.extension.youtube.settings.Settings; +import app.morphe.extension.youtube.shared.PlayerType; +import app.morphe.extension.youtube.shared.ShortsPlayerState; + +@SuppressWarnings("unused") +public final class NativePlaylistQueuePatch { + private static final String SAVE_VIDEO_IDS_ARGUMENT = "app.morphe.native_playlist_queue.save_video_ids"; + private static final String ENDPOINT_TAG_KEY = "com.google.android.libraries.youtube.innertube.endpoint.tag"; + private static final ByteTrieSearch CARD_COMPONENTS = new ByteTrieSearch( + ByteTrieSearch.convertStringsToBytes("video_lockup_with_attachment.eml")); + private static final Map metadataCache = + Collections.synchronizedMap(Utils.createSizeRestrictedMap(1000)); + + // Accessed only on the main thread; shared lookups may outlive the queue. + private static final Set pendingMetadata = new HashSet<>(); + private static final Map ratingActions = + Collections.synchronizedMap(Utils.createSizeRestrictedMap(50)); + + /** Implemented on the native playlist controller during patching. */ + public interface PlaylistController { + String patch_getVideoId(); + String patch_getPlaylistId(); + Object patch_getModel(); + Object patch_getPanel(); + int patch_findQueueRow(Object target); + void patch_markQueueRows(); + boolean patch_isAttached(); + Object patch_getNavigator(); + Object patch_updatePanel(byte[] panel); + void patch_restorePanel(Object expectedModel, Object originalModel); + void patch_showQueueFeedback(byte[] command); + } + + /** Native playlist controls, which hold the playlist's navigator. */ + public interface NavigatorOwner { + Object patch_getNavigator(); + } + + /** Native control owner; weak registration allows eligibility refresh after local edits. */ + public interface PlayerControls { + Object patch_getNavigator(); + void patch_refreshNavigation(); + } + + /** Native notification owner; its service navigator can load without a watch screen. */ + public interface MediaControls { + ServiceNavigator patch_getQueueNavigator(); + } + + /** Native adapter publishes capability changes to both notification paths. */ + public interface NotificationControls { + MediaControls patch_getQueueMediaControls(); + void patch_refreshQueueNotification(); + } + + public interface ServiceNavigator { + /** No active player is bound while the service is starting or resetting. */ + @Nullable + String patch_getVideoId(); + NativePlayback patch_getQueuePlayback(); + void patch_navigateQueue(boolean next); + boolean patch_canNavigateQueue(boolean next); + } + + /** Bridge to YouTube's mutable playback queue, independent of the watch view. */ + public interface NativePlayback { + boolean patch_isLocalQueue(); + boolean patch_attachQueue(byte[] panel, byte[] currentWatchCommand); + void patch_updateSnapshot(byte[] panel); + int patch_getSelectedIndex(); + int patch_getRepeatMode(); + void patch_restoreQueueRepeat(int repeatMode); + boolean patch_clearQueue(); + } + + public interface RepeatHeader { + Object patch_getNavigator(); + Object patch_getPanel(); + void patch_enableRepeat(); + void patch_setShuffleAvailable(boolean available); + } + + /** Native playlist title and its currently bound panel. */ + public interface PlaylistTitle { + Object patch_getPanel(); + TextView patch_getTitle(); + } + + /** Read-only identity of a model which passed YouTube's stale-video checks. */ + public interface PlaylistModel { + String patch_getPlaylistId(); + } + + /** Native listener bridge retains the bound endpoint before logging decorates it. */ + public interface RowClick { + Object patch_getQueueEndpoint(); + } + + /** Refreshes the native swipe widget's existing foreground background cache. */ + public interface SwipeBackground { + void patch_refreshQueueSwipeBackground(); + } + + /** Nonserialized ownership bit on native rows from an accepted local panel. */ + public interface QueuePanel { + boolean patch_isQueueOwned(); + } + + public interface QueueRow { + boolean patch_isQueueOwned(); + } + + /** Native adapter bridge; its rows are the exact accepted protobuf objects. */ + public interface ReorderItems { + int patch_getSize(); + Object patch_getQueueRow(int index); + } + + private static final class DragSession { + final Object holder; + final ReorderItems items; + final Object model; + final PlaylistQueue snapshot; + final Object row; + final int from; + boolean ended; + + DragSession(Object holder, ReorderItems items, Object row, int from) { + this.holder = holder; + this.items = items; + this.row = row; + this.from = from; + model = publishedModel; + snapshot = queue; + } + } + + private static DragSession dragSession; + private static WeakReference controllerRef = new WeakReference<>(null); + private static PlaylistQueue queue; + private record MixChoice(String token, String playlistId, String videoId, + WeakReference controller, + WeakReference playback, Map headers, Locale locale) {} + private static MixChoice pendingMixChoice; + private static volatile MixChoice pendingMixAddition; + private static WeakReference nativePlaybackRef = new WeakReference<>(null); + private static Object originalModel; + private static Object publishedModel; + private record TitleWidth(int original, int applied) {} + private static final Map originalTitleWidths = new WeakHashMap<>(); + + // The running video keeps its original player-response identity when only its + // navigation is replaced. Retain this exact source/owner through Clear and Undo; + // do not rewrite shared VideoInformation or claim another playlist/player. + private record ConvertedPlaylist(String videoId, String playlistId, WeakReference playback) {} + private static volatile ConvertedPlaylist convertedPlaylist; + + // Background playback can confirm selection before the watch screen catches up. + private static boolean selectionNeedsPublishing; + private static boolean publishing; + private static boolean observingPlayer; + + // SharedPreferences holds listeners weakly. Retain ours for the installed patch's lifetime. + private static final SharedPreferences.OnSharedPreferenceChangeListener QUEUE_SETTING_LISTENER = + (preferences, key) -> { + if (Settings.QUEUE_TYPE.key.equals(key)) { + // Let the setting cache update before checking the chosen provider. + Utils.runOnMainThread(() -> { + try { + if (isNativeQueueDisabled()) { + releaseNativeQueue(); + } + } catch (Exception ex) { + Logger.printException(() -> "Queue type setting change failure", ex); + } + }); + } + }; + + private static final QueueShortsSession shortsSession = new QueueShortsSession(); + + // A return response can precede Shorts detachment. Remember its controller + // weakly, without transferring the old panel's ownership/restoration baseline. + private static WeakReference shortsControllerRef = new WeakReference<>(null); + private record RowSelection(PlaylistQueue.Entry entry) {} + private static RowSelection pendingSelection; + + // A menu click can open the first video before a watch controller exists. + // Keep this intent separate from an attached, usable native queue. + private static QueueStart pendingQueueStart; + private static int pendingQueueStartInlinePlaybackBlockId; + private static final class ClosedPlayback { + final long positionMilliseconds; + @Nullable PlaylistController feedbackOwner; + + ClosedPlayback(long positionMilliseconds, PlaylistController feedbackOwner) { + this.positionMilliseconds = positionMilliseconds; + + // Keep the dispatcher alive through teardown, then release it on show. + // The Undo expiry also bounds a close that never finishes. This owner + // is never used to restore playback. + this.feedbackOwner = feedbackOwner; + } + } + + private record PendingUndo(String token, PlaylistQueue before, WeakReference playback, + ClosedPlayback closedPlayback) {} + private static PendingUndo pendingUndo; + + // Reload opens a standalone video, which may have no playlist model. Its + // dispatched intent and completed native playback transition identify the owner. + private static final class PendingReload { + final PlaylistQueue before; + WeakReference controller = new WeakReference<>(null); + boolean opening; + boolean watchDispatched; + + PendingReload(PlaylistQueue before) { + this.before = before; + } + } + private static PendingReload pendingReload; + private static final int UNDO_DURATION_MILLISECONDS = 5_000; + private static final int ACCESSIBLE_UNDO_DURATION_MILLISECONDS = 20_000; + private static final Set repeatHeaders = Collections.newSetFromMap(new WeakHashMap<>()); + private static final Set playerControls = Collections.newSetFromMap(new WeakHashMap<>()); + private static final Set notificationControls = Collections.newSetFromMap(new WeakHashMap<>()); + + // Native navigation can be rejected (for example while signed out). A failed + // click must not authorize a much later, unrelated open of the same video. + private static final int NAVIGATION_TIMEOUT_MILLISECONDS = 10_000; + + private static final String SENDER_VIEW_KEY = "com.google.android.libraries.youtube.rendering.elements.sender_view"; + private static final String MIX_PLAYLIST_ID_PREFIX = "RD"; + + private NativePlaylistQueuePatch() {} + + public static boolean isPatchIncluded() { + return false; // Modified during patching. + } + + /** Read-only response hook; captures commands even when the action bar is hidden. */ + public static void onWatchNextResponse(MessageLite response) { + if (response == null) { + return; + } + try { + var results = SingleColumnWatchNextResults.parseFrom(response.toByteArray()); + for (var section : results.getPrimaryResults().getSecondaryResults().getSecondaryContentsList()) { + if (!section.hasSlimVideoMetadataSectionRenderer()) { + continue; + } + var metadata = section.getSlimVideoMetadataSectionRenderer(); + for (var content : metadata.getTertiaryContentsList()) { + QueueRatingActions actions = QueueRatingActions.fromElement( + content.getElementRenderer().getNewElement()); + if (actions == null || (!metadata.getVideoId().isEmpty() + && !metadata.getVideoId().equals(actions.videoId()))) { + continue; + } + + ratingActions.put(actions.videoId(), actions); + + // Finish the native response parse before updating any panel. + Utils.runOnMainThread(() -> { + if (actions.videoId().equals(VideoInformation.getVideoId())) { + refreshOwnedPanel(actions.videoId()); + } + }); + } + } + } catch (Exception ex) { + Logger.printException(() -> "onWatchNextResponse failure", ex); + } + } + + /** Read-only shared element hook; preserves bytes and every other patch's changes. */ + public static byte[] onElementLoaded(byte[] bytes) { + if (bytes == null || !CARD_COMPONENTS.matches(bytes)) { + return bytes; + } + try { + VideoMetadata metadata = VideoMetadata.fromElement(bytes); + if (metadata != null) { + metadataCache.compute(metadata.videoId(), (videoId, previous) -> + previous == null ? metadata : previous.withCapture(metadata)); + } + } catch (Exception ex) { + Logger.printException(() -> "onElementLoaded failure", ex); + } + + return bytes; + } + + private static void rememberChannel(String videoId, String title, String channelName) { + metadataCache.compute(videoId, (id, metadata) -> { + if (metadata == null) { + return new VideoMetadata(id, title, channelName, "", "", false); + } + if (metadata.title().equals(id) && !title.equals(id)) { + metadata = new VideoMetadata(id, title, metadata.channelName(), metadata.videoInfo(), metadata.duration(), metadata.fullByline()); + } + + return metadata.withChannelName(channelName); + }); + } + + /** Injection point, after a native model acceptance or completed video transition. */ + public static void onControllerUpdated(PlaylistController controller) { + onControllerUpdated(controller, false); + } + + /** A completed native descriptor transition can leave the playlist model null. */ + public static void onPlaybackTransition(PlaylistController controller) { + onControllerUpdated(controller, true); + } + + private static void onControllerUpdated(PlaylistController controller, boolean playbackTransition) { + Utils.runOnMainThreadNowOrLater(() -> { + try { + PendingReload reload = pendingReload; + if (playbackTransition && reload != null && reload.opening && !hasPlaylist(controller) + && reload.before.currentEntry().videoId().equals(controller.patch_getVideoId())) { + reload.controller = new WeakReference<>(controller); + } + + updateController(controller); + } catch (Exception ex) { + Logger.printException(() -> "onControllerUpdated failure", ex); + } + }); + } + + private static void updateController(PlaylistController controller) { + if (!observingPlayer) { + Setting.preferences.preferences.registerOnSharedPreferenceChangeListener(QUEUE_SETTING_LISTENER); + observingPlayer = true; + PlayerType.getOnChange().addObserver(type -> { + onPlaybackContextChanged(); + return kotlin.Unit.INSTANCE; + }); + + ShortsPlayerState.getOnChange().addObserver(open -> { + onPlaybackContextChanged(); + return kotlin.Unit.INSTANCE; + }); + } + + // A queued callback may arrive after our synchronous update has returned. Never + // treat our own accepted model as a new native baseline or publish it again. + if (publishing) { + return; + } + if (isNativeQueueDisabled()) { + releaseNativeQueue(); + if (queue == null) { + controllerRef = new WeakReference<>(controller); + } + return; + } + + syncNativeSelection(VideoInformation.getVideoId()); + + // A regular return response can arrive before the shared video ID changes. + // Only native controller callbacks update this candidate; lifecycle rechecks + // must not overwrite it with the old owner while waiting for that ID. + if (queue != null && isQueueSuspended() && !controller.patch_getVideoId().isEmpty()) { + shortsControllerRef = new WeakReference<>(controller); + } + + if (updateShortsSession(controller)) { + return; + } + + // The service can advance while a retained watch screen still describes the + // previous video. Only the active-ID hooks can commit background selection. + if (queue != null && !controller.patch_getVideoId().equals(VideoInformation.getVideoId())) { + return; + } + if (waitingForNativeSelection(VideoInformation.getVideoId())) { + return; + } + if (publishedModel != null && controllerRef.get() == controller + && controller.patch_getModel() == publishedModel + && controller.patch_getVideoId().equals(queue.currentEntry().videoId())) { + if (selectionNeedsPublishing && readyController() == controller) { + publish(controller, queue, false); + } + + refreshRepeatHeaders(); + return; + } + + PlaylistController previous = controllerRef.get(); + if (queue != null && previous != controller) { + if (previous != null && previous.patch_isAttached() + && previous.patch_getVideoId().equals(VideoInformation.getVideoId()) + && (!hasPlaylist(controller) || (!controller.patch_isAttached() + && !hasForeignPlaybackPlaylist()))) { + return; + } + + // Recreating a watch screen must not discard an owned background + // session. Its new controller still needs a fresh native baseline. + if (hasPlaylist(controller) || (!serviceOwnsQueue() + && controller.patch_getVideoId().equals(queue.currentEntry().videoId()))) { + releasePanel(); + } else { + if (controller.patch_getModel() != publishedModel) { + originalModel = controller.patch_getModel(); + } + publishedModel = null; + } + } + + controllerRef = new WeakReference<>(controller); + if (queue == null) { + // The native acceptance hook runs immediately after the model store, + // before its subscriptions and rendering finish. Never reenter it. + if (pendingQueueStart != null) { + Utils.runOnMainThread(NativePlaylistQueuePatch::tryStartQueue); + } + if (pendingReload != null) { + Utils.runOnMainThread(NativePlaylistQueuePatch::showPendingReloadFeedback); + } + return; + } + + // A row click authorizes only that exact entry. Wait for the authoritative + // video-ID hook before committing selection; prefetched responses cannot do it. + if (!hasPlaylist(controller) && pendingSelection != null + && controller.patch_getVideoId().equals(pendingSelection.entry().videoId())) { + return; + } + + if (hasPlaylist(controller)) { + Object navigator = controller.patch_getNavigator(); + queue = null; + pendingUndo = null; + shortsSession.reset(); + shortsControllerRef.clear(); + dragSession = null; + pendingSelection = null; + originalModel = null; + publishedModel = null; + selectionNeedsPublishing = false; + refreshPlayerControls(navigator); + QueueFullscreenActions.refresh(); + return; + } + + dragSession = null; + String videoId = controller.patch_getVideoId(); + if (!videoId.equals(queue.currentEntry().videoId())) { + // The active-ID hook and accepted controller must agree. Prefetched + // responses and Shorts cannot insert entries; account playlists win above. + PlayerType type = PlayerType.getCurrent(); + if (!PlaylistQueue.isVideoId(videoId) || type.isNoneOrHidden() + || type == PlayerType.INLINE_MINIMAL || hasForeignPlaybackPlaylist()) { + return; + } + + pendingSelection = null; + if (controller.patch_getModel() != publishedModel) { + originalModel = controller.patch_getModel(); + } + + // Keep the old snapshot until publish attaches the changed rows to the + // native backend; preassigning queue would conceal the item-list change. + if (publish(controller, queue.playNextAndSelect(videoId), false)) { + Utils.showToastShort(str("morphe_queue_manager_fetch_succeeded_add")); + completeMetadata(); + } + + return; + } + + originalModel = controller.patch_getModel(); + publish(controller, queue, false); + } + + private static void onPlaybackContextChanged() { + Utils.runOnMainThreadNowOrLater(() -> { + try { + updatePlaybackContext(); + } catch (Exception ex) { + Logger.printException(() -> "onPlaybackContextChanged failure", ex); + } + }); + } + + private static void updatePlaybackContext() { + if (isNativeQueueDisabled()) { + releaseNativeQueue(); + return; + } + + PlayerType currentType = PlayerType.getCurrent(); + if (ShortsPlayerState.isOpen() || currentType == PlayerType.HIDDEN) { + pendingReload = null; + } + if (pendingUndo != null && pendingUndo.closedPlayback() != null) { + // Until native close completes, queued notifications can still describe + // the old mini player or its dismissal animation. Once feedback is shown, + // an active player means another session and must invalidate this Undo. + boolean closing = pendingUndo.closedPlayback().feedbackOwner != null + && (currentType == PlayerType.WATCH_WHILE_MINIMIZED + || currentType == PlayerType.WATCH_WHILE_SLIDING_MINIMIZED_DISMISSED + || currentType == PlayerType.WATCH_WHILE_SLIDING_FULLSCREEN_DISMISSED); + if (ShortsPlayerState.isOpen() || currentType == PlayerType.HIDDEN + || (!currentType.isNoneOrHidden() && currentType != PlayerType.INLINE_MINIMAL && !closing)) { + pendingUndo = null; + } + } else if (ShortsPlayerState.isOpen() || currentType.isNoneOrHidden() + || currentType == PlayerType.INLINE_MINIMAL) { + pendingUndo = null; + } + + if (queue != null && (ShortsPlayerState.isOpen() || PlayerType.getCurrent() == PlayerType.HIDDEN)) { + updateShortsSession(controllerRef.get()); + } + + // The regular player can hide before Shorts attaches, and can reappear + // before its video-ID callback. Reconcile after the native event finishes. + Utils.runOnMainThread(() -> { + try { + reconcilePlaybackContext(); + } catch (Exception ex) { + Logger.printException(() -> "reconcilePlaybackContext failure", ex); + } + }); + } + + private static void reconcilePlaybackContext() { + if (queue == null) { + showClosedQueueFeedback(); + showPendingReloadFeedback(); + tryStartQueue(); + return; + } + + PlaylistController controller = returningController(); + boolean returningFromShorts = shortsSession.isSuspended(); + if (updateShortsSession(controller)) { + return; + } + PlayerType type = PlayerType.getCurrent(); + + // NONE describes the watch UI, not the playback service. Its + // controller can be cleared while audio continues in the background. + if ((type == PlayerType.INLINE_MINIMAL || (type == PlayerType.NONE + && (controller == null || controller.patch_getVideoId().isEmpty()))) + && !serviceOwnsQueue()) { + releasePanel(); + } else if (returningFromShorts && controller != null) { + updateController(controller); + } + } + + /** Returns true while the retained queue must not observe or control playback. */ + private static boolean updateShortsSession(PlaylistController controller) { + if (queue == null) { + return false; + } + PlaylistController previous = controllerRef.get(); + PlayerType type = PlayerType.getCurrent(); + boolean shortsOpen = ShortsPlayerState.isOpen() || type == PlayerType.HIDDEN; + if (shortsOpen || shortsSession.isSuspended()) { + controller = returningController(); + } + QueueShortsSession.Action action = shortsSession.update(ShortsPlayerState.isOpen(), + type == PlayerType.HIDDEN, !type.isNoneOrHidden() && type != PlayerType.INLINE_MINIMAL, + VideoInformation.getVideoId(), controller == null ? "" : controller.patch_getVideoId(), + controller != null && hasPlaylist(controller), + serviceOwnsQueue()); + switch (action) { + case SUSPEND -> { + pendingSelection = null; + dragSession = null; + refreshPlayerControls(previous == null ? null : previous.patch_getNavigator()); + return true; + } + + case WAIT -> { return true; } + case RELEASE -> { + releasePanel(); + return true; + } + + case RESUME -> { + // A recreated watch controller needs its own restoration baseline. + // Claim it only after its regular video matches active playback. + if (controller != null && controllerRef.get() != controller) { + if (controller.patch_getModel() != publishedModel) { + originalModel = controller.patch_getModel(); + publishedModel = null; + } + + controllerRef = new WeakReference<>(controller); + } + + shortsControllerRef.clear(); + selectionNeedsPublishing = true; + refreshNotificationControls(); + } + + case NONE -> { } + } + + return false; + } + + @Nullable + private static PlaylistController returningController() { + PlaylistController previous = controllerRef.get(); + PlaylistController candidate = shortsControllerRef.get(); + if (candidate == null || !candidate.patch_getVideoId().equals(VideoInformation.getVideoId())) { + return previous; + } + + // Retain an attached owner over another response for the same video. + // A confirmed account playlist must instead end the temporary queue. + if (!hasPlaylist(candidate) && previous != null && previous.patch_isAttached() + && previous.patch_getVideoId().equals(VideoInformation.getVideoId())) { + return previous; + } + + return candidate; + } + + private static boolean isQueueSuspended() { + return shortsSession.isSuspended() || ShortsPlayerState.isOpen() + || PlayerType.getCurrent() == PlayerType.HIDDEN; + } + + /** Resolve live playback, without treating a retained watch model as a service owner. */ + private static boolean serviceOwnsQueue() { + if (queue == null || hasForeignPlaybackPlaylist()) { + return false; + } + NativePlayback playback = nativePlaybackRef.get(); + if (playback != null && playback.patch_isLocalQueue()) { + return true; + } + String videoId = queue.currentEntry().videoId(); + if (!videoId.equals(VideoInformation.getVideoId())) { + return false; + } + for (NotificationControls controls : new ArrayList<>(notificationControls)) { + ServiceNavigator navigator = controls.patch_getQueueMediaControls().patch_getQueueNavigator(); + if (navigator != null && videoId.equals(navigator.patch_getVideoId())) { + return true; + } + } + + return false; + } + + @Nullable + private static NativePlayback playbackFor(PlaylistController controller) { + if (controller != null && controller.patch_getNavigator() instanceof ServiceNavigator service) { + NativePlayback playback = service.patch_getQueuePlayback(); + if (playback != null) { + return playback; + } + } + + for (NotificationControls controls : new ArrayList<>(notificationControls)) { + ServiceNavigator service = controls.patch_getQueueMediaControls().patch_getQueueNavigator(); + if (service == null || !VideoInformation.getVideoId().equals(service.patch_getVideoId())) { + continue; + } + NativePlayback playback = service.patch_getQueuePlayback(); + if (playback != null) { + return playback; + } + } + + return null; + } + + /** Rebind after native service restoration; UI visibility does not own this sequence. */ + public static void onNativeQueueResponse(NativePlayback playback) { + Utils.runOnMainThread(() -> { + try { + attachNativeQueueResponse(playback); + } catch (Exception ex) { + Logger.printException(() -> "onNativeQueueResponse failure", ex); + } + }); + } + + private static void attachNativeQueueResponse(NativePlayback playback) { + if (isNativeQueueDisabled()) { + releaseNativeQueue(); + return; + } + + if (queue == null) { + showPendingReloadFeedback(); + tryStartQueue(); + return; + } + + if (hasForeignPlaybackPlaylist()) { + return; + } + if (!queue.currentEntry().videoId().equals(VideoInformation.getVideoId())) { + if (isQueueSuspended()) { + return; + } + PlaylistController controller = readyController(); + if (controller != null && playbackFor(controller) == playback) { + updateController(controller); + } + return; + } + + if (playback.patch_isLocalQueue()) { + return; + } + boolean current = playbackFor(returningController()) == playback; + for (NotificationControls controls : new ArrayList<>(notificationControls)) { + ServiceNavigator service = controls.patch_getQueueMediaControls().patch_getQueueNavigator(); + if (service != null && VideoInformation.getVideoId().equals(service.patch_getVideoId()) + && service.patch_getQueuePlayback() == playback) { + current = true; + } + } + + if (!current) { + return; + } + if (playback.patch_attachQueue(renderer(queue), currentQueueCommand(queue))) { + nativePlaybackRef = new WeakReference<>(playback); + } + } + + public static void onNativeQueueSelection() { + Utils.runOnMainThread(() -> { + try { + if (isNativeQueueDisabled()) { + releaseNativeQueue(); + return; + } + + if (queue == null || isQueueSuspended()) { + return; + } + syncNativeSelection(VideoInformation.getVideoId()); + refreshRepeatHeaders(); + if (selectionNeedsPublishing) { + refreshOwnedPanel(VideoInformation.getVideoId()); + } + } catch (Exception ex) { + Logger.printException(() -> "onNativeQueueSelection failure", ex); + } + }); + } + + private static boolean waitingForNativeSelection(String videoId) { + NativePlayback playback = nativePlaybackRef.get(); + return queue != null && playback != null && playback.patch_isLocalQueue() + && !queue.currentEntry().videoId().equals(videoId) + && queue.entries().stream().anyMatch(entry -> entry.videoId().equals(videoId)); + } + + private static void syncNativeSelection(String videoId) { + NativePlayback playback = nativePlaybackRef.get(); + if (queue == null || playback == null || !playback.patch_isLocalQueue()) { + return; + } + int index = playback.patch_getSelectedIndex(); + if (index < 0 || index >= queue.entries().size() || !queue.entries().get(index).videoId().equals(videoId)) { + return; + } + pendingSelection = null; + if (queue.selectedIndex() != index || queue.repeatMode() != playback.patch_getRepeatMode()) { + queue = queue.select(index).withRepeatMode(playback.patch_getRepeatMode()); + selectionNeedsPublishing = true; + playback.patch_updateSnapshot(renderer(queue)); + } + } + + /** Runs after YouTube's click veto but before its logging decorates the endpoint. */ + public static void onRowClicked(RowClick click) { + try { + if (dragSession != null) { + return; + } + pendingSelection = null; + PlaylistController controller = readyController(); + if (queue == null || controller == null || controller.patch_getModel() != publishedModel) { + return; + } + int index = controller.patch_findQueueRow(click.patch_getQueueEndpoint()); + if (index < 0 || index >= queue.entries().size() || index == queue.selectedIndex()) { + return; + } + authorizeRowSelection(queue.entries().get(index)); + + // Do not publish here: the original listener still needs its bound endpoint. + } catch (Exception ex) { + Logger.printException(() -> "onRowClicked failure", ex); + } + } + + private static RowSelection authorizeRowSelection(PlaylistQueue.Entry entry) { + RowSelection selection = new RowSelection(entry); + pendingSelection = selection; + Utils.runOnMainThreadDelayed(() -> { + if (pendingSelection == selection) { + pendingSelection = null; + } + }, NAVIGATION_TIMEOUT_MILLISECONDS); + return selection; + } + + /** Keep the Queue label at its native text width instead of shrinking it in the counter chain. */ + public static void onPlaylistTitleBound(PlaylistTitle header) { + // Without the native queue, only a previously widened title needs restoring. + if (isNativeQueueDisabled() && originalTitleWidths.isEmpty()) { + return; + } + // Native bind can run inside publish, before ownership and row markers are + // committed. Recheck after it completes and after any newer rebind. + Utils.runOnMainThread(() -> { + try { + TextView title = header.patch_getTitle(); + if (title == null) { + return; + } + ViewGroup.LayoutParams params = title.getLayoutParams(); + if (params == null) { + return; + } + PlaylistController controller = readyController(); + Object panel = header.patch_getPanel(); + boolean owned = panel != null && queue != null && publishedModel != null && controller != null + && controller.patch_getModel() == publishedModel + && panel == controller.patch_getPanel(); + if (owned) { + CharSequence text = title.getText(); + if (text == null || text.length() == 0) { + return; + } + // ConstraintLayout normalizes the native 0dp/wrap constraint + // to constrained WRAP_CONTENT, which can still shrink. Use the + // actual styled font metrics and padding, without a fixed dp size. + int width = (int) Math.ceil(Layout.getDesiredWidth(text, title.getPaint())) + + title.getCompoundPaddingLeft() + title.getCompoundPaddingRight(); + if (width <= 0 || params.width == width) { + return; + } + TitleWidth previous = originalTitleWidths.get(title); + originalTitleWidths.put(title, + new TitleWidth(previous == null ? params.width : previous.original(), width)); + params.width = width; + title.setLayoutParams(params); + } else { + TitleWidth width = originalTitleWidths.remove(title); + if (width != null && params.width == width.applied()) { + params.width = width.original(); + title.setLayoutParams(params); + } + } + } catch (Exception exception) { + Logger.printException(() -> "onPlaylistTitleBound failure", exception); + } + }); + } + + public static void onRepeatHeaderBound(RepeatHeader header) { + Utils.runOnMainThreadNowOrLater(() -> { + try { + repeatHeaders.add(header); + PlaylistController controller = ownedNavigator(header.patch_getNavigator()); + if (controller != null && header.patch_getPanel() == controller.patch_getPanel()) { + refreshRepeatHeaders(); + } + } catch (Exception ex) { + Logger.printException(() -> "onRepeatHeaderBound failure", ex); + } + }); + } + + private static void refreshRepeatHeader(RepeatHeader header) { + PlaylistController controller = ownedNavigator(header.patch_getNavigator()); + if (controller != null && header.patch_getPanel() == controller.patch_getPanel()) { + header.patch_enableRepeat(); + NativePlayback playback = playbackFor(controller); + header.patch_setShuffleAvailable(playback != null + && playback.patch_isLocalQueue() && queue.entries().size() > 1); + } + } + + /** Intercept only this queue's native header; account playlist actions fall through. */ + public static boolean onQueueShuffle(RepeatHeader header) { + try { + PlaylistController controller = ownedNavigator(header.patch_getNavigator()); + if (controller == null || header.patch_getPanel() != controller.patch_getPanel()) { + return false; + } + + // Consume an owned click during a transition rather than enabling the hidden shuffler. + if (publishing || pendingSelection != null || dragSession != null) { + return true; + } + syncNativeSelection(VideoInformation.getVideoId()); + publish(controller, queue.shuffle(), true); + return true; + } catch (Exception ex) { + Logger.printException(() -> "onQueueShuffle failure", ex); + return false; + } + } + + private static void refreshRepeatHeaders() { + for (RepeatHeader header : new ArrayList<>(repeatHeaders)) { + refreshRepeatHeader(header); + } + } + + public static void onPlayerControlsCreated(PlayerControls controls) { + Utils.runOnMainThreadNowOrLater(() -> playerControls.add(controls)); + } + + private static void refreshPlayerControls(Object navigator) { + refreshNotificationControls(); + if (navigator == null) { + return; + } + for (PlayerControls controls : new ArrayList<>(playerControls)) { + if (controls.patch_getNavigator() == navigator) { + controls.patch_refreshNavigation(); + } + } + } + + private static void refreshNotificationControls() { + for (NotificationControls controls : new ArrayList<>(notificationControls)) { + controls.patch_refreshQueueNotification(); + } + } + + public static void onNotificationControlsCreated(NotificationControls controls) { + // Finish native construction before consulting its providers or publishing + // a refresh through the original observers. + Utils.runOnMainThread(() -> { + try { + notificationControls.add(controls); + if (queue != null) { + controls.patch_refreshQueueNotification(); + } + } catch (Exception ex) { + Logger.printException(() -> "onNotificationControlsCreated failure", ex); + } + }); + } + + public static void onNotificationPlaybackChanged(NotificationControls controls) { + try { + // The state handler can reset navigation after selection was published. + // Use the active service, independently of foreground video/panel callbacks. + if (ownsNativePlayback(controls.patch_getQueueMediaControls().patch_getQueueNavigator())) { + controls.patch_refreshQueueNotification(); + } + } catch (Exception ex) { + Logger.printException(() -> "onNotificationPlaybackChanged failure", ex); + } + } + + /** Bridge contract: bit 0 = Previous, bit 1 = Next; -1 keeps native flags. */ + public static int notificationNavigationAvailability(NotificationControls controls) { + try { + ServiceNavigator service = controls.patch_getQueueMediaControls().patch_getQueueNavigator(); + if (!ownsNativePlayback(service)) { + return -1; + } + + // Both native notification adapters let watch-screen delegates override + // sequencer events. Query the same service that handles our skip clicks. + return (service.patch_canNavigateQueue(false) ? 1 : 0) + | (service.patch_canNavigateQueue(true) ? 2 : 0); + } catch (Exception ex) { + Logger.printException(() -> "notificationNavigationAvailability failure", ex); + return -1; + } + } + + public static boolean playerControlAvailability(PlayerControls controls, boolean next, boolean original) { + try { + if (controls.patch_getNavigator() instanceof ServiceNavigator service && ownsNativePlayback(service)) { + return service.patch_canNavigateQueue(next); + } + + return original; + } catch (Exception ex) { + Logger.printException(() -> "playerControlAvailability failure", ex); + return original; + } + } + + public static boolean onNextPressed(PlayerControls controls) { + try { + return controls.patch_getNavigator() instanceof ServiceNavigator service && navigateNative(service, true); + } catch (Exception ex) { + Logger.printException(() -> "onNextPressed failure", ex); + return false; + } + } + + public static boolean onPreviousPressed(PlayerControls controls) { + try { + return controls.patch_getNavigator() instanceof ServiceNavigator service && navigateNative(service, false); + } catch (Exception ex) { + Logger.printException(() -> "onPreviousPressed failure", ex); + return false; + } + } + + /** Runs after native media logging, before either foreground or service skip routing. */ + public static boolean onNotificationNext(MediaControls controls) { + try { + return navigateNative(controls.patch_getQueueNavigator(), true); + } catch (Exception ex) { + Logger.printException(() -> "onNotificationNext failure", ex); + return false; + } + } + + public static boolean onNotificationPrevious(MediaControls controls) { + try { + return navigateNative(controls.patch_getQueueNavigator(), false); + } catch (Exception ex) { + Logger.printException(() -> "onNotificationPrevious failure", ex); + return false; + } + } + + private static boolean ownsNativePlayback(ServiceNavigator service) { + if (isNativeQueueDisabled() || queue == null || service == null + || ShortsPlayerState.isOpen()) { + return false; + } + + NativePlayback playback = service.patch_getQueuePlayback(); + return playback != null && playback.patch_isLocalQueue() && playback == nativePlaybackRef.get(); + } + + /** Bypass watch-history shortcuts, retaining the native queue's validation and ordering. */ + private static boolean navigateNative(ServiceNavigator service, boolean next) { + if (!ownsNativePlayback(service)) { + return false; + } + service.patch_navigateQueue(next); + return true; + } + + @Nullable + private static PlaylistController ownedNavigator(Object navigator) { + PlaylistController controller = readyController(); + return queue != null && controller != null && controller.patch_getModel() == publishedModel + && controller.patch_getNavigator() == navigator ? controller : null; + } + + /** Keep the swipe restore drawable in sync when an attached queue row changes selection. */ + public static void onSwipeBackgroundChanged(RowClick row, SwipeBackground background) { + try { + if (!Utils.isCurrentlyOnMainThread() || isQueueSuspended()) { + return; + } + PlaylistController controller = controllerRef.get(); + if (controller == null || background == null) { + return; + } + + // Binding happens synchronously inside our publication, before publishedModel + // and ownership bits are committed. Require the exact bound endpoint in the + // controller's accepted model, including during that initial publication. + if (!publishing && (queue == null || controller.patch_getModel() != publishedModel)) { + return; + } + Object endpoint = row.patch_getQueueEndpoint(); + if (endpoint != null && controller.patch_findQueueRow(endpoint) >= 0) { + background.patch_refreshQueueSwipeBackground(); + } + } catch (Exception ex) { + Logger.printException(() -> "onSwipeBackgroundChanged failure", ex); + } + } + + /** Save arguments belong to this queue-launched native fragment, including after rotation. */ + public static void onQueueSaveArguments(MessageLite endpoint, Bundle arguments) { + try { + ArrayList videoIds = QueueSaveActions.copyVideoIds(endpoint); + if (videoIds != null) { + arguments.putStringArrayList(SAVE_VIDEO_IDS_ARGUMENT, videoIds); + } + } catch (Exception ex) { + Logger.printException(() -> "onQueueSaveArguments failure", ex); + } + } + + @Nullable + public static List getQueueSaveVideoIds(@Nullable Bundle arguments) { + return arguments == null ? null : arguments.getStringArrayList(SAVE_VIDEO_IDS_ARGUMENT); + } + + /** Override only commands from an owned row or the exact published queue header. */ + @Nullable + public static MessageLite onQueueMenuCommand(MessageLite endpoint, Map context) { + if (endpoint == null || context == null) { + return endpoint; + } + Object owner = context.get(ENDPOINT_TAG_KEY); + boolean header = owner instanceof QueuePanel panel && panel.patch_isQueueOwned(); + if (!header && (!(owner instanceof QueueRow row) || !row.patch_isQueueOwned())) { + return endpoint; + } + try { + var command = PlaylistQueueRenderer.NavigationEndpoint.parseFrom(endpoint.toByteArray()); + if (header && command.hasAddToPlaylistEndpoint()) { + PlaylistController controller = readyController(); + if (queue == null || controller == null || controller.patch_getModel() != publishedModel + || owner != controller.patch_getPanel() + || !command.getAddToPlaylistEndpoint().getVideoId().equals(queue.entries().get(0).videoId())) { + return null; + } + + List videoIds = new ArrayList<>(); + for (PlaylistQueue.Entry entry : queue.entries()) { + videoIds.add(entry.videoId()); + } + + // The immutable selection is already serialized in this owned menu command. + if (!command.getQueueSaveVideoIdsList().equals(videoIds)) { + return null; + } + return endpoint; + } + + if (!command.hasHideEnclosingAction() || command.hasWatchEndpoint()) { + return endpoint; + } + PlaylistController controller = readyController(); + if (queue == null || controller == null || controller.patch_getModel() != publishedModel) { + return null; + } + if (header) { + if (owner != controller.patch_getPanel()) { + return null; + } + NativePlayback playback = playbackFor(controller); + if (pendingSelection != null || dragSession != null || playback == null) { + Utils.showToastShort(str("morphe_native_playlist_queue_unavailable")); + return null; + } + + PlaylistQueue before = queue.withRepeatMode(playback.patch_getRepeatMode()); + if (!playback.patch_clearQueue()) { + Utils.showToastShort(str("morphe_native_playlist_queue_unavailable")); + return null; + } + + releasePanel(); + return clearFeedback(endpoint, before, playback); + } + + int index = controller.patch_findQueueRow(owner); + if (index < 0 || index >= queue.entries().size()) { + return null; + } + + // Check again at activation: a previously revealed button must never + // remove the current item or race an in-flight row navigation. + if (index == queue.selectedIndex() || pendingSelection != null || dragSession != null + || queue.entries().get(index).videoId().equals(VideoInformation.getVideoId())) { + return null; + } + + if (!publish(controller, queue.remove(index), true)) { + return null; + } + + // A single-row removal is silent. Consume the local Hide rather than + // dispatching an account edit or generating an Undo snackbar. + return null; + } catch (Exception ex) { + Logger.printException(() -> "onQueueMenuCommand failure", ex); + Utils.showToastShort(str("morphe_native_playlist_queue_unavailable")); + return null; + } + } + + private static PendingUndo beginUndo(PlaylistQueue before, NativePlayback playback, ClosedPlayback closed) { + PendingUndo undo = new PendingUndo(UUID.randomUUID().toString(), before, new WeakReference<>(playback), closed); + pendingUndo = undo; + + // Bound snapshots if native feedback never reaches its snackbar factory. + expireUndo(undo, ACCESSIBLE_UNDO_DURATION_MILLISECONDS); + return undo; + } + + private static byte[] clearFeedback(PendingUndo undo) { + return QueueUndoActions.feedback(undo.token(), str("morphe_native_playlist_queue_cleared"), + str("offline_undo_snackbar_button_text")).toByteArray(); + } + + private static MessageLite clearFeedback(MessageLite endpoint, PlaylistQueue before, NativePlayback playback) { + PendingUndo undo = beginUndo(before, playback, null); + return ((QueueUndoActions.Command) endpoint).patch_parseQueueCommand( + clearFeedback(undo)); + } + + /** Returns a request-bound callback for Reload's actual intent dispatch, or null if no queue was cleared. */ + @Nullable + public static Runnable prepareVideoReload(String videoId) { + try { + if (isNativeQueueDisabled()) { + return null; + } + // Reload's dismissal/open is delayed. Cancel older capabilities now, + // before they can restore a queue into the player being replaced. + pendingUndo = null; + pendingReload = null; + clearPendingQueueStart(); + OpenVideosFullscreenHookPatch.keepVideoMinimized(null); + if (queue == null || !queue.currentEntry().videoId().equals(videoId)) { + return null; + } + PlaylistController controller = readyController(); + if (controller == null || controller.patch_getModel() != publishedModel + || pendingSelection != null || dragSession != null) { + return null; + } + + NativePlayback playback = playbackFor(controller); + if (playback == null || !playback.patch_isLocalQueue()) { + return null; + } + PlaylistQueue before = queue.withRepeatMode(playback.patch_getRepeatMode()); + if (!playback.patch_clearQueue()) { + Logger.printException(() -> "Could not clear native queue before reloading the video"); + return null; + } + + releasePanel(); + convertedPlaylist = null; + PendingReload reload = new PendingReload(before); + pendingReload = reload; + // Do not retain a queue indefinitely if Reload never opens its player. + Utils.runOnMainThreadDelayed(() -> { + if (pendingReload == reload) { + pendingReload = null; + } + }, NAVIGATION_TIMEOUT_MILLISECONDS); + return () -> { + if (pendingReload == reload) { + reload.opening = true; + } + }; + } catch (Exception exception) { + pendingReload = null; + Logger.printException(() -> "Could not prepare native queue Reload Undo", exception); + return null; + } + } + + private static void showPendingReloadFeedback() { + PendingReload reload = pendingReload; + if (reload == null || !reload.opening) { + return; + } + PlaylistController controller = readyController(); + if (controller == null || controller != reload.controller.get() + || !reload.before.currentEntry().videoId().equals(VideoInformation.getVideoId()) + || hasForeignPlaybackPlaylist()) { + return; + } + + NativePlayback playback = playbackFor(controller); + if (playback == null || playback.patch_isLocalQueue()) { + return; + } + pendingReload = null; + PendingUndo undo = beginUndo(reload.before, playback, null); + try { + controller.patch_showQueueFeedback(clearFeedback(undo)); + } catch (Exception exception) { + if (pendingUndo == undo) { + pendingUndo = null; + } + Logger.printException(() -> "Could not show native queue reload confirmation", exception); + } + } + + /** Called from accepted native mini-player close clicks or gestures before teardown. */ + public static void onQueuePlayerClosing() { + try { + if (queue == null) { + return; + } + PlaylistController controller = readyController(); + if (controller == null || controller.patch_getModel() != publishedModel + || pendingSelection != null || dragSession != null) { + return; + } + + NativePlayback playback = playbackFor(controller); + long position = VideoInformation.getVideoTime(); + if (playback == null || !playback.patch_isLocalQueue() || position < 0) { + return; + } + PlaylistQueue before = queue.withRepeatMode(playback.patch_getRepeatMode()); + + // Detach our local sequence while the original close listener still owns + // stopping playback. Never keep a disposed backend as the restore target. + if (!playback.patch_clearQueue()) { + Logger.printException(() -> "Could not detach native queue before closing the player"); + return; + } + + releasePanel(); + beginUndo(before, null, new ClosedPlayback(position, controller)); + + // The original native dismissal runs before this posted callback. + Utils.runOnMainThread(NativePlaylistQueuePatch::showClosedQueueFeedback); + } catch (Exception ex) { + Logger.printException(() -> "onQueuePlayerClosing failure", ex); + } + } + + private static void showClosedQueueFeedback() { + PendingUndo undo = pendingUndo; + if (undo == null || undo.closedPlayback() == null || queue != null) { + return; + } + if (isNativeQueueDisabled() || ShortsPlayerState.isOpen()) { + pendingUndo = null; + return; + } + + PlayerType type = PlayerType.getCurrent(); + if (type != PlayerType.NONE && type != PlayerType.INLINE_MINIMAL) { + return; + } + PlaylistController owner = undo.closedPlayback().feedbackOwner; + if (owner == null) { + return; + } + + // The activity dispatcher remains callable after its watch controller detaches. + // Clear this reference so repeated teardown notifications cannot show it twice. + undo.closedPlayback().feedbackOwner = null; + try { + owner.patch_showQueueFeedback(clearFeedback(undo)); + } catch (Exception exception) { + if (pendingUndo == undo) { + pendingUndo = null; + } + Logger.printException(() -> "Could not show native queue player-close confirmation", exception); + } + } + + /** Called by the native snackbar factory only for a matching local Undo capability. */ + static int queueUndoDuration(String token) { + PendingUndo undo = pendingUndo; + if (undo == null || !undo.token().equals(token)) { + return 0; + } + AccessibilityManager accessibility = (AccessibilityManager) + Utils.getContext().getSystemService(Context.ACCESSIBILITY_SERVICE); + + // Match YouTube's native snackbar accessibility predicate on supported versions. + boolean accessible = accessibility != null && accessibility.isEnabled() + && accessibility.isTouchExplorationEnabled(); + + // A new identity prevents an earlier timer from expiring re-created feedback. + PendingUndo presented = new PendingUndo(undo.token(), undo.before(), undo.playback(), undo.closedPlayback()); + pendingUndo = presented; + expireUndo(presented, accessible ? ACCESSIBLE_UNDO_DURATION_MILLISECONDS : UNDO_DURATION_MILLISECONDS); + + // Leave the accessible native action timeout at 20 seconds. Setting a positive + // 20-second duration would make YouTube add another 10 seconds for accessibility. + return accessible ? 0 : UNDO_DURATION_MILLISECONDS; + } + + private static void expireUndo(PendingUndo undo, int duration) { + Utils.runOnMainThreadDelayed(() -> { + if (pendingUndo == undo) { + pendingUndo = null; + } + }, duration); + } + + /** Native snackbar activation is not a row/menu action and need not retain that owner. */ + public static boolean onQueueUndoCommand(MessageLite endpoint) { + PendingUndo undo = pendingUndo; + try { + return QueueUndoActions.dispatchUndo(endpoint, undo == null ? null : undo.token(), () -> { + if (undo != null) { + undoQueueEdit(undo.token()); + } + }); + } catch (Exception ex) { + Logger.printException(() -> "onQueueUndoCommand failure", ex); + return false; + } + } + + private static void undoQueueEdit(String token) { + PendingUndo undo = pendingUndo; + if (undo == null || !undo.token().equals(token)) { + return; + } + pendingUndo = null; // A native callback can only activate this capability once. + if (undo.closedPlayback() != null) { + if (!canStartQueue() || pendingQueueStart != null) { + return; + } + startQueue(new QueueStart(undo.before(), VideoInformation.getVideoId(), + undo.closedPlayback().positionMilliseconds)); + return; + } + + PlaylistController controller = readyController(); + if (controller == null) { + return; + } + if (pendingSelection != null || dragSession != null) { + return; + } + if (hasForeignPlaybackPlaylist()) { + return; + } + NativePlayback playback = undo.playback().get(); + if (playback == null) { + return; + } + if (playbackFor(controller) != playback) { + return; + } + PlaylistQueue restored = QueueUndoActions.restore(undo.before(), queue, + VideoInformation.getVideoId()); + if (restored == null) { + return; + } + Object baseline = controller.patch_getModel(); + if (!publish(controller, restored, true)) { + return; + } + originalModel = baseline; + playback.patch_restoreQueueRepeat(restored.repeatMode()); + queue = queue.withRepeatMode(playback.patch_getRepeatMode()); + } + + /** Native row overflow creates its menu context directly, outside shared feed hooks. */ + public static void onRowMenuOpened(Map context) { + try { + if (context == null) { + return; + } + Object row = context.get(ENDPOINT_TAG_KEY); + if (row instanceof FlyoutUtils.FlyoutMenuVideoIdInterface + && (!isNativeQueueDisabled() || (row instanceof QueueRow owned && owned.patch_isQueueOwned()))) { + FlyoutUtils.extractFlyoutIdFromMap(context); + } + } catch (Exception ex) { + Logger.printException(() -> "onRowMenuOpened failure", ex); + } + } + + /** Observe native start/end notifications; never replace the gesture listeners. */ + public static void onReorderStarted(Object holder, ReorderItems items, int index) { + try { + PlaylistController controller = readyController(); + if (queue == null || controller == null || pendingSelection != null + || controller.patch_getModel() != publishedModel + || items.patch_getSize() != queue.entries().size() + || index < 0 || index >= items.patch_getSize()) { + return; + } + + Object row = items.patch_getQueueRow(index); + if (row instanceof QueueRow owned && owned.patch_isQueueOwned() + && controller.patch_findQueueRow(row) == index) { + dragSession = new DragSession(holder, items, row, index); + } + } catch (Exception ex) { + Logger.printException(() -> "onReorderStarted failure", ex); + } + } + + public static void onReorderEnded(Object holder, ReorderItems items, int from, int to) { + DragSession session = dragSession; + if (session == null || session.holder != holder || session.items != items) { + return; + } + session.ended = true; + + // Let all native observers and coordinator cleanup finish. Hold off metadata + // publication until then so it cannot rebind the adapter in mid-gesture. + Utils.runOnMainThread(() -> { + try { + if (dragSession != session) { + return; + } + dragSession = null; + PlaylistController controller = readyController(); + if (queue != session.snapshot || controller == null + || controller.patch_getModel() != session.model) { + return; + } + + boolean validMove = from == session.from && to >= 0 && to < items.patch_getSize() + && items.patch_getSize() == queue.entries().size() + && items.patch_getQueueRow(to) == session.row; + PlaylistQueue candidate = validMove ? queue.move(from, to) : queue; + if (!publish(controller, candidate, validMove)) { + publish(controller, session.snapshot, false); + } + } catch (Exception ex) { + Logger.printException(() -> "onReorderEnded failure", ex); + } + }); + } + + /** Native teardown can cancel a drag without an end notification. */ + public static void onReorderHolderUnbound(Object holder) { + DragSession session = dragSession; + if (session == null || session.holder != holder || session.ended) { + return; + } + dragSession = null; + Utils.runOnMainThread(() -> { + try { + if (dragSession == null && queue == session.snapshot) { + refreshOwnedPanel(session.snapshot.currentEntry().videoId()); + } + } catch (Exception ex) { + Logger.printException(() -> "onReorderHolderUnbound failure", ex); + } + }); + } + + /** Shared video-ID hook; prefetched player responses must not change this state. */ + public static void onVideoChanged(String videoId) { + Utils.runOnMainThread(() -> { + try { + updateVideo(videoId); + } catch (Exception ex) { + Logger.printException(() -> "onVideoChanged failure", ex); + } + }); + } + + private static void updateVideo(String videoId) { + if (!videoId.equals(VideoInformation.getVideoId())) { + return; + } + if (isNativeQueueDisabled()) { + releaseNativeQueue(); + return; + } + + if (pendingReload != null && !PlayerType.getCurrent().isNoneOrHidden() + && PlayerType.getCurrent() != PlayerType.INLINE_MINIMAL + && !pendingReload.before.currentEntry().videoId().equals(videoId)) { + pendingReload = null; + } + + showPendingReloadFeedback(); + + // A cleared queue belongs to the uninterrupted ordinary playback session. + // Returning to its video later must not revive an earlier Undo action. + if (pendingUndo != null && pendingUndo.closedPlayback() == null + && !pendingUndo.before().currentEntry().videoId().equals(videoId)) { + pendingUndo = null; + } + + tryStartQueue(); + if (queue == null) { + return; + } + + // This is a presentation snapshot of native selection, not a second navigator. + syncNativeSelection(videoId); + boolean returningFromShorts = shortsSession.isSuspended(); + PlaylistController returningController = returningController(); + if (updateShortsSession(returningController)) { + return; + } + if (returningFromShorts) { + if (returningController != null) { + updateController(returningController); + } + return; + } + + if (waitingForNativeSelection(videoId)) { + return; + } + if (pendingSelection != null && pendingSelection.entry().videoId().equals(videoId)) { + for (int index = 0; index < queue.entries().size(); index++) { + if (queue.entries().get(index) != pendingSelection.entry()) { + continue; + } + dragSession = null; + queue = queue.select(index); + selectionNeedsPublishing = true; + pendingSelection = null; + + // Let the original navigation/response callback finish first. + Utils.runOnMainThread(() -> { + try { + refreshNotificationControls(); + PlaylistController controller = readyController(); + if (controller == null) { + return; + } + if (controller.patch_getModel() == publishedModel) { + refreshOwnedPanel(videoId); + } else { + updateController(controller); + } + } catch (Exception ex) { + Logger.printException(() -> "Selection refresh failure", ex); + } + }); + + return; + } + } + + if (!queue.currentEntry().videoId().equals(videoId)) { + // The active ID can arrive before its watch controller. Retain the + // sequence until that controller confirms the new regular video. + if (returningController != null) { + updateController(returningController); + } + } + } + + /** Captures the eligible native menu action for the selected video. */ + @Nullable + public static Runnable getFlyoutQueueAction(String videoId, boolean playLast) { + if (!canQueueFlyoutVideo(videoId)) { + return null; + } + // Claim every stock Next/Last click in Native mode. Readiness is checked + // by the action; a temporarily unavailable controller must not open Premium. + return () -> addToQueue(videoId, playLast); + } + + public static boolean canQueueFlyoutVideo(String videoId) { + return !isNativeQueueDisabled() && PlaylistQueue.isVideoId(videoId); + } + + /** Only our Native queue makes Last available; account playlists and Mixes do not. */ + public static boolean canShowFlyoutLast() { + return queue != null; + } + + @Nullable + private static PlaylistController activePlaylistController() { + PlaylistController controller = controllerRef.get(); + PlayerType type = PlayerType.getCurrent(); + return controller != null && controller.patch_isAttached() && hasPlaylist(controller) + && !type.isNoneOrHidden() && type != PlayerType.INLINE_MINIMAL + && !ShortsPlayerState.isOpen() + && controller.patch_getVideoId().equals(VideoInformation.getVideoId()) ? controller : null; + } + + private static boolean isNativeQueueDisabled() { + return Settings.QUEUE_TYPE.get() != QueueType.NATIVE; + } + + /** Release only our session; never modify the Morphe queue or its settings. */ + private static void releaseNativeQueue() { + pendingMixChoice = null; + pendingMixAddition = null; + convertedPlaylist = null; + pendingReload = null; + clearPendingQueueStart(); + pendingUndo = null; + OpenVideosFullscreenHookPatch.keepVideoMinimized(null); + if (queue == null) { + return; + } + NativePlayback playback = nativePlaybackRef.get(); + try { + // Use the same native transaction as Clear: no load, seek, pause or stop. + if (playback != null && playback.patch_isLocalQueue() && !playback.patch_clearQueue()) { + Logger.printException(() -> "Could not release native queue"); + return; + } + + releasePanel(); + } catch (Exception exception) { + Logger.printException(() -> "releaseNativeQueue failure", exception); + } + } + + private static boolean canStartQueue() { + PlayerType type = PlayerType.getCurrent(); + return !isNativeQueueDisabled() && queue == null && !ShortsPlayerState.isOpen() + && (type == PlayerType.NONE || type == PlayerType.INLINE_MINIMAL); + } + + @Nullable + private static PlaylistController readyController() { + PlaylistController controller = controllerRef.get(); + if (isNativeQueueDisabled() || isQueueSuspended() || controller == null + || !controller.patch_isAttached() || hasPlaylist(controller) + || PlayerType.getCurrent().isNoneOrHidden() + || PlayerType.getCurrent() == PlayerType.INLINE_MINIMAL + || !controller.patch_getVideoId().equals(VideoInformation.getVideoId())) { + return null; + } + + return controller; + } + + private static boolean hasForeignPlaybackPlaylist() { + String playlistId = VideoInformation.getPlaylistId(); + if (playlistId.isEmpty()) { + return false; + } + ConvertedPlaylist converted = convertedPlaylist; + if (converted == null || isNativeQueueDisabled() + || !converted.videoId().equals(VideoInformation.getVideoId()) + || !converted.playlistId().equals(playlistId)) { + return true; + } + + NativePlayback playback = converted.playback().get(); + if (playback == null) { + return true; + } + // The service can retain our queue after its watch UI is collected. + if (queue != null && nativePlaybackRef.get() == playback && playback.patch_isLocalQueue()) { + return false; + } + return playback != playbackFor(returningController()); + } + + /** Keep a late response for the replaced list from reclaiming this same player. */ + public static boolean shouldIgnorePlaylistResponse(NativePlayback playback, MessageLite endpoint) { + ConvertedPlaylist converted = convertedPlaylist; + if (converted == null || isNativeQueueDisabled() || endpoint == null + || converted.playback().get() != playback + || !converted.videoId().equals(VideoInformation.getVideoId())) { + return false; + } + + try { + var command = PlaylistQueueRenderer.NavigationEndpoint.parseFrom(endpoint.toByteArray()); + return command.hasWatchEndpoint() + && converted.videoId().equals(command.getWatchEndpoint().getVideoId()) + && converted.playlistId().equals(command.getWatchEndpoint().getPlaylistId()); + } catch (Exception exception) { + Logger.printException(() -> "shouldIgnorePlaylistResponse failure", exception); + return false; + } + } + + /** Called after native stale-video validation, before storing the incoming model. */ + public static boolean shouldIgnorePlaylistModel(PlaylistController controller, PlaylistModel model) { + ConvertedPlaylist converted = convertedPlaylist; + return converted != null && !isNativeQueueDisabled() && !publishing + && converted.videoId().equals(VideoInformation.getVideoId()) + && converted.videoId().equals(controller.patch_getVideoId()) + && converted.playlistId().equals(model.patch_getPlaylistId()) + && converted.playback().get() != null + && converted.playback().get() == playbackFor(controller); + } + + private static boolean hasPlaylist(PlaylistController controller) { + String playlistId = controller.patch_getPlaylistId(); + return playlistId != null && !playlistId.isEmpty(); + } + + private static void addToQueue(String videoId, boolean playLast) { + if (isNativeQueueDisabled()) { + releaseNativeQueue(); + return; + } + + if (playLast && queue == null) { + // A menu callback can outlive the queue that made Play last available. + Utils.showToastShort(str("morphe_native_playlist_queue_not_ready")); + return; + } + + PlaylistController playlist = activePlaylistController(); + String channelName = videoId.equals(FlyoutUtils.getFlyoutVideoId()) + ? FlyoutUtils.getFlyoutChannelName() : ""; + try { + if (playlist != null) { + if (dragSession != null || queue != null) { + Utils.showToastShort(str("morphe_native_playlist_queue_not_ready")); + return; + } + + NativePlayback playback = playbackFor(playlist); + if (playback == null) { + Utils.showToastShort(str("morphe_native_playlist_queue_unavailable")); + return; + } + + String currentId = playlist.patch_getVideoId(); + String currentTitle = VideoInformation.getVideoTitle(); + String title = ChannelIdRequest.getVideoTitleIfAvailable(videoId); + rememberChannel(currentId, currentTitle, VideoInformation.getChannelName()); + rememberChannel(videoId, title.isEmpty() ? videoId : title, channelName); + PlaylistQueue candidate = new PlaylistQueue(currentId, currentTitle); + if (!currentId.equals(videoId)) { + candidate = candidate.playNext(videoId); + } + // The native accept transaction replaces navigation rows without a + // load, seek, pause or player dismissal. Reuse the mutable navigator + // when available; its factory receives only our list-free command. + String sourcePlaylistId = playlist.patch_getPlaylistId(); + if (publish(playlist, candidate, true)) { + convertedPlaylist = new ConvertedPlaylist(currentId, sourcePlaylistId, new WeakReference<>(playback)); + // Clear must hide the replaced playlist, not restore its old rows. + originalModel = null; + refreshPlayerControls(playlist.patch_getNavigator()); + QueueFullscreenActions.refresh(); + FlyoutUtils.dismissFlyout(); + Utils.showToastShort(str("morphe_queue_manager_fetch_succeeded_add")); + completeMetadata(); + } + + return; + } + + if (!playLast && canStartQueue()) { + String title = ChannelIdRequest.getVideoTitleIfAvailable(videoId); + rememberChannel(videoId, title.isEmpty() ? videoId : title, channelName); + QueueStart start = new QueueStart(new PlaylistQueue(videoId, title), VideoInformation.getVideoId()); + + // The shared helper opens YouTube's own watch intent. With false it + // does not call a native player bridge or require an existing player. + startQueue(start); + FlyoutUtils.dismissFlyout(); + return; + } + + PlaylistController controller = readyController(); + if (controller == null || dragSession != null) { + Utils.showToastShort(str("morphe_native_playlist_queue_not_ready")); + return; + } + + rememberChannel(controller.patch_getVideoId(), + VideoInformation.getVideoTitle(), VideoInformation.getChannelName()); + String title = ChannelIdRequest.getVideoTitleIfAvailable(videoId); + rememberChannel(videoId, title.isEmpty() ? videoId : title, channelName); + PlaylistQueue candidate = queue == null + ? new PlaylistQueue(controller.patch_getVideoId(), VideoInformation.getVideoTitle()) + : queue; + if (queue == null) { + originalModel = controller.patch_getModel(); + } + // The first queue already contains the playing video. An explicit + // addition to an existing queue can still create another occurrence. + PlaylistQueue updated = queue == null && controller.patch_getVideoId().equals(videoId) + ? candidate : playLast ? candidate.playLast(videoId) : candidate.playNext(videoId); + if (publish(controller, updated, true)) { + FlyoutUtils.dismissFlyout(); + Utils.showToastShort(str("morphe_queue_manager_fetch_succeeded_add")); + completeMetadata(); + } + } catch (Exception exception) { + Logger.printException(() -> "Could not add video to native playlist queue", exception); + Utils.showToastShort(str("morphe_native_playlist_queue_unavailable")); + } + } + + private static void clearPendingQueueStart() { + int blockId = pendingQueueStartInlinePlaybackBlockId; + pendingQueueStart = null; + pendingQueueStartInlinePlaybackBlockId = 0; + if (blockId != 0) { + LoadVideoPatch.releaseInlinePlaybackBlock(blockId); + } + } + + private static void startQueue(QueueStart start) { + clearPendingQueueStart(); + pendingReload = null; + pendingUndo = null; + pendingQueueStart = start; + Utils.runOnMainThreadDelayed(() -> { + if (pendingQueueStart != start) { + return; + } + clearPendingQueueStart(); + OpenVideosFullscreenHookPatch.keepVideoMinimized(null); + }, NAVIGATION_TIMEOUT_MILLISECONDS); + + // A focused feed preview must not take ownership of closed-player Undo. + if (start.restoresClosedPlayer()) { + pendingQueueStartInlinePlaybackBlockId = LoadVideoPatch.blockInlinePlaybackUntilPlayerOpens(); + } + + try { + LoadVideoPatch.openVideoIntent(start.watchUrl(), false); + } catch (RuntimeException exception) { + if (pendingQueueStart == start) { + clearPendingQueueStart(); + OpenVideosFullscreenHookPatch.keepVideoMinimized(null); + } + + throw exception; + } + } + + /** Commit a first-item queue only after native playback accepts the requested video. */ + private static void tryStartQueue() { + if (isNativeQueueDisabled()) { + releaseNativeQueue(); + return; + } + + QueueStart start = pendingQueueStart; + if (start == null || queue != null || publishing) { + return; + } + String videoId = VideoInformation.getVideoId(); + String requestedId = start.candidate().currentEntry().videoId(); + PlayerType type = PlayerType.getCurrent(); + if (ShortsPlayerState.isOpen() || type == PlayerType.HIDDEN + || (!type.isNoneOrHidden() && type != PlayerType.INLINE_MINIMAL + && start.isSupersededBy(videoId))) { + clearPendingQueueStart(); + OpenVideosFullscreenHookPatch.keepVideoMinimized(null); + return; + } + + PlaylistController controller = controllerRef.get(); + + // Controller callbacks can precede the active video ID or player type. + // Recheck when controller, video ID, player type or playback service + // becomes ready; prefetch alone cannot attach. + if (controller == null || !requestedId.equals(videoId) + || !requestedId.equals(controller.patch_getVideoId())) { + return; + } + + if (hasPlaylist(controller)) { + clearPendingQueueStart(); + OpenVideosFullscreenHookPatch.keepVideoMinimized(null); + return; + } + + if (readyController() != controller) { + return; + } + Object baseline = controller.patch_getModel(); + if (publish(controller, start.candidate(), false)) { + originalModel = baseline; + clearPendingQueueStart(); + if (start.restoresClosedPlayer()) { + NativePlayback playback = nativePlaybackRef.get(); + playback.patch_restoreQueueRepeat(start.candidate().repeatMode()); + queue = queue.withRepeatMode(playback.patch_getRepeatMode()); + refreshRepeatHeaders(); + refreshPlayerControls(controller.patch_getNavigator()); + } else { + Utils.showToastShort(str("morphe_queue_manager_fetch_succeeded_add")); + } + completeMetadata(); + } + } + + private static void completeMetadata() { + for (PlaylistQueue.Entry entry : queue.entries()) { + VideoMetadata metadata = metadataCache.get(entry.videoId()); + if (metadata != null && metadata.isComplete()) { + continue; + } + Locale locale = Locale.getDefault(); + String key = entry.videoId() + ":" + locale.toLanguageTag(); + if (!pendingMetadata.add(key)) { + continue; + } + String knownTitle = ChannelIdRequest.getVideoTitleIfAvailable(entry.videoId()); + String capturedTitle = metadata == null ? entry.title() : metadata.title(); + String searchTitle = knownTitle.isEmpty() ? capturedTitle : knownTitle; + Utils.runOnBackgroundThread(() -> { + VideoMetadata fetched = PlaylistMetadataRequest.fetch(entry.videoId(), searchTitle, locale); + Utils.runOnMainThread(() -> { + pendingMetadata.remove(key); + if (fetched == null || !Locale.getDefault().equals(locale)) { + return; + } + metadataCache.compute(entry.videoId(), (videoId, previous) -> + previous == null ? fetched : previous.withLookup(fetched)); + refreshOwnedPanel(entry.videoId()); + }); + }); + } + } + + private static void refreshOwnedPanel(String videoId) { + if (dragSession != null || queue == null + || queue.entries().stream().noneMatch(item -> item.videoId().equals(videoId))) { + return; + } + + PlaylistController controller = readyController(); + if (controller != null && controller.patch_getModel() == publishedModel) { + publish(controller, queue, false); + } + } + + /** Keep an individual music-video tap in our queue instead of starting its automatic Mix. */ + public static MessageLite prepareQueueVideoCommand(MessageLite endpoint, Map context) { + if (!Utils.isCurrentlyOnMainThread() || queue == null || publishing + || pendingSelection != null || dragSession != null || context == null + || !(endpoint instanceof QueueUndoActions.Command nativeCommand)) { + return endpoint; + } + + PlaylistController controller = readyController(); + if (controller == null || !isQueuePlayback(controller, playbackFor(controller))) { + return endpoint; + } + try { + var command = PlaylistQueueRenderer.NavigationEndpoint.parseFrom(endpoint.toByteArray()); + if (!command.hasWatchEndpoint()) { + return endpoint; + } + var watch = command.getWatchEndpoint(); + View sender = getMixTapSender(watch, context); + if (sender == null || !FeedVideoCardsPatch.isIndividualVideoTap(sender)) { + return endpoint; + } + + // Only the automatic playlist is removed. Retain the original video, + // player parameters, tracking and unknown fields, then use the native + // registry to rebuild the command before descriptor construction. + // Existing video-change handling selects/inserts it in the queue. + var prepared = command.toBuilder().setWatchEndpoint(watch.toBuilder().clearPlaylistId()).build(); + return nativeCommand.patch_parseQueueCommand(prepared.toByteArray()); + } catch (Exception ex) { + Logger.printException(() -> "prepareQueueVideoCommand failure", ex); + return endpoint; + } + } + + /** Pause an intentional Mix tap before any watch-state mutation, using the native dialog. */ + public static boolean onQueueMixTap(@Nullable MessageLite endpoint, @Nullable Map context) { + if (isNativeQueueDisabled() || !Utils.isCurrentlyOnMainThread() || endpoint == null) { + return false; + } + boolean intercepted = false; + try { + var command = PlaylistQueueRenderer.NavigationEndpoint.parseFrom(endpoint.toByteArray()); + if (!command.hasWatchEndpoint()) { + return false; + } + if (command.hasQueueMixChoiceToken()) { + intercepted = true; + MixChoice choice = pendingMixChoice; + // The native dialog re-dispatches the original watch command. + // Only this one-use capability can bypass the prompt or append. + if (choice == null || !choice.token().equals(command.getQueueMixChoiceToken())) { + return true; + } + pendingMixChoice = null; + if (!ownsMixChoice(choice, choice.controller().get(), choice.playback().get())) { + return true; + } + if (!command.getQueueMixAdd()) { + return false; + } + pendingMixAddition = choice; + Utils.showToastShort(str("morphe_native_playlist_queue_loading_mix")); + Utils.runOnBackgroundThread(() -> { + if (pendingMixAddition != choice) { + return; + } + var videos = MixPlaylistRequest.fetch(choice.playlistId(), choice.videoId(), choice.headers()); + if (pendingMixAddition != choice) { + return; + } + Utils.runOnMainThread(() -> completeMixAddition(choice, videos)); + }); + + return true; + } + + // Another explicit watch invalidates old dialog choices and fetches. + pendingMixChoice = null; + pendingMixAddition = null; + if (queue == null || publishing || pendingSelection != null + || dragSession != null || context == null) { + return false; + } + + var watch = command.getWatchEndpoint(); + View sender = getMixTapSender(watch, context); + if (sender == null || !FeedVideoCardsPatch.isPlaylistTap(sender)) { + return false; + } + PlaylistController controller = readyController(); + NativePlayback playback = controller == null ? null : playbackFor(controller); + if (controller == null || !isQueuePlayback(controller, playback)) { + return false; + } + MixChoice choice = new MixChoice(UUID.randomUUID().toString(), watch.getPlaylistId(), watch.getVideoId(), + new WeakReference<>(controller), new WeakReference<>(playback), + Map.copyOf(AuthUtils.getRequestHeader()), Locale.getDefault()); + pendingMixChoice = choice; + intercepted = true; + controller.patch_showQueueFeedback(QueueMixActions.dialog(command, choice.token(), + str("morphe_native_playlist_queue_mix_choice"), str("morphe_native_playlist_queue_watch_mix"), + str("morphe_native_playlist_queue_add_mix")).toByteArray()); + return true; + } catch (Exception exception) { + pendingMixChoice = null; + Logger.printException(() -> "onQueueMixTap failure", exception); + // A failed prompt must not silently discard the playing queue. + if (intercepted) { + Utils.showToastShort(str("morphe_native_playlist_queue_unavailable")); + } + return intercepted; + } + } + + private static boolean ownsMixChoice(MixChoice choice, @Nullable PlaylistController controller, + @Nullable NativePlayback playback) { + return !isNativeQueueDisabled() && !publishing && pendingSelection == null && dragSession == null + && queue != null && controller != null && controller == readyController() + && isQueuePlayback(controller, playback) && playbackFor(controller) == playback + && queue.currentEntry().videoId().equals(VideoInformation.getVideoId()) + && choice.headers().equals(AuthUtils.getRequestHeader()) && choice.locale().equals(Locale.getDefault()); + } + + /** If the controller shows our published model, played by our local native queue. */ + private static boolean isQueuePlayback(PlaylistController controller, @Nullable NativePlayback playback) { + return controller.patch_getModel() == publishedModel && playback != null + && playback == nativePlaybackRef.get() && playback.patch_isLocalQueue(); + } + + /** + * @return The view that dispatched a watch command which starts an automatic Mix + * from a video, rather than opening a position of a saved playlist. + */ + @Nullable + private static View getMixTapSender(PlaylistQueueRenderer.WatchEndpoint watch, Map context) { + if (!PlaylistQueue.isVideoId(watch.getVideoId()) || !watch.getPlaylistId().startsWith(MIX_PLAYLIST_ID_PREFIX) + || watch.hasIndex() || !watch.getPlaylistSetVideoId().isEmpty()) { + return null; + } + + return context.get(SENDER_VIEW_KEY) instanceof View view ? view : null; + } + + private static void completeMixAddition(MixChoice choice, @Nullable List videos) { + if (pendingMixAddition != choice) { + return; + } + pendingMixAddition = null; + PlaylistController controller = choice.controller().get(); + NativePlayback playback = choice.playback().get(); + if (!ownsMixChoice(choice, controller, playback)) { + return; + } + if (videos == null || videos.isEmpty()) { + Utils.showToastShort(str("morphe_queue_manager_fetch_failed_add")); + return; + } + + try { + var additions = new ArrayList(); + for (VideoMetadata video : videos) { + additions.add(new PlaylistQueue.Entry(video.videoId(), video.title())); + metadataCache.put(video.videoId(), video); + } + + PlaylistQueue updated = queue.append(additions) + .withRepeatMode(playback.patch_getRepeatMode()); + if (publish(controller, updated, true)) { + Utils.showToastShort(str("morphe_queue_manager_fetch_succeeded_add")); + completeMetadata(); + } + } catch (Exception exception) { + Logger.printException(() -> "Could not append Mix to native queue", exception); + Utils.showToastShort(str("morphe_queue_manager_fetch_failed_add")); + } + } + + /** Native watch dispatch reads this flag before constructing the playback descriptor. */ + public static Map prepareQueueStartContext(MessageLite endpoint, Map context) { + if (endpoint == null || isNativeQueueDisabled()) { + return context; + } + QueueStart start = pendingQueueStart; + try { + var command = PlaylistQueueRenderer.NavigationEndpoint.parseFrom(endpoint.toByteArray()); + // A deliberate new watch supersedes Clear/close/reload Undo, even for + // the same video. This hook runs on dispatch, not on prefetch. + if (command.hasWatchEndpoint()) { + pendingUndo = null; + PendingReload reload = pendingReload; + if (reload != null) { + var watch = command.getWatchEndpoint(); + if (reload.opening && !reload.watchDispatched && watch.getPlaylistId().isEmpty() + && reload.before.currentEntry().videoId().equals(watch.getVideoId())) { + reload.watchDispatched = true; + } else { + pendingReload = null; + } + } + } + + if (convertedPlaylist != null && command.hasWatchEndpoint() + && !command.getWatchEndpoint().getPlaylistId().isEmpty()) { + // This hook observes dispatch, not prefetch. An explicit playlist + // navigation, even to the same video/list, supersedes conversion. + releaseNativeQueue(); + } + + if (start == null || queue != null) { + return context; + } + Map minimized = start.minimizedContext(endpoint, context); + if (minimized != context) { + OpenVideosFullscreenHookPatch.keepVideoMinimized(start.candidate().currentEntry().videoId()); + } + + return minimized; + } catch (Exception ex) { + Logger.printException(() -> "prepareQueueStartContext failure", ex); + return context; + } + } + + private static byte[] currentQueueCommand(PlaylistQueue candidate) { + // The navigation factory must not inherit the playing video's old list ID. + // This describes navigation only; it is never loaded into the running player. + return PlaylistQueueRenderer.NavigationEndpoint.newBuilder().setWatchEndpoint( + PlaylistQueueRenderer.WatchEndpoint.newBuilder() + .setVideoId(candidate.currentEntry().videoId())).build().toByteArray(); + } + + private static boolean publish(PlaylistController controller, PlaylistQueue candidate, boolean reportFailure) { + publishing = true; + Object previousModel = controller.patch_getModel(); + Object acceptedModel = null; + boolean committed = false; + try { + NativePlayback playback = playbackFor(controller); + if (playback == null) { + if (reportFailure) { + Utils.showToastShort(str("morphe_native_playlist_queue_unavailable")); + } + return false; + } + + byte[] panel = renderer(candidate); + acceptedModel = controller.patch_updatePanel(panel); + if (acceptedModel == null) { + if (reportFailure) { + Utils.showToastShort(str("morphe_native_playlist_queue_unavailable")); + } + return false; + } + + boolean changedItems = queue == null || queue.entries().size() != candidate.entries().size(); + if (!changedItems) { + // Equal video/title values can still be different occurrences of a + // duplicated video. A shuffle must also move the native selection. + for (int index = 0; index < candidate.entries().size(); index++) { + if (queue.entries().get(index) != candidate.entries().get(index)) { + changedItems = true; + break; + } + } + } + + if (!playback.patch_isLocalQueue() || changedItems) { + if (!playback.patch_attachQueue(panel, currentQueueCommand(candidate))) { + controller.patch_restorePanel(acceptedModel, previousModel); + if (reportFailure) { + Utils.showToastShort(str("morphe_native_playlist_queue_unavailable")); + } + return false; + } + } else { + playback.patch_updateSnapshot(panel); + } + nativePlaybackRef = new WeakReference<>(playback); + + // The native sequencer, including its inherited Loop mode, is authoritative. + candidate = candidate.withRepeatMode(playback.patch_getRepeatMode()); + queue = candidate; + pendingReload = null; + if (changedItems) { + pendingUndo = null; + } + publishedModel = acceptedModel; + selectionNeedsPublishing = false; + + // From here the panel, playback backend and local snapshot agree. + // A presentation refresh cannot roll back only one of those owners. + committed = true; + controller.patch_markQueueRows(); + refreshRepeatHeaders(); + refreshPlayerControls(controller.patch_getNavigator()); + QueueFullscreenActions.refresh(); + return true; + } catch (Exception exception) { + if (committed) { + Logger.printException(() -> "Could not refresh native playlist queue controls", exception); + return true; + } + + if (acceptedModel != null && controller.patch_getModel() == acceptedModel) { + controller.patch_restorePanel(acceptedModel, previousModel); + } + + Logger.printException(() -> "Could not display native playlist queue", exception); + if (reportFailure) { + Utils.showToastShort(str("morphe_native_playlist_queue_unavailable")); + } + return false; + } finally { + publishing = false; + } + } + + private static void releasePanel() { + pendingMixChoice = null; + pendingMixAddition = null; + pendingReload = null; + pendingUndo = null; + clearPendingQueueStart(); + OpenVideosFullscreenHookPatch.keepVideoMinimized(null); + PlaylistController controller = controllerRef.get(); + Object expectedModel = publishedModel; + Object restoreModel = originalModel; + Object navigator = controller == null ? null : controller.patch_getNavigator(); + queue = null; + nativePlaybackRef.clear(); + shortsSession.reset(); + shortsControllerRef.clear(); + dragSession = null; + pendingSelection = null; + originalModel = null; + publishedModel = null; + selectionNeedsPublishing = false; + + // A null baseline is valid when YouTube had no playlist before our display. + if (controller != null && expectedModel != null) { + try { + controller.patch_restorePanel(expectedModel, restoreModel); + } catch (Exception exception) { + Logger.printException(() -> "Could not restore native playlist panel", exception); + } + } + + refreshPlayerControls(navigator); + QueueFullscreenActions.refresh(); + } + + static boolean ownsFullscreenPlaylist() { + PlaylistController controller = controllerRef.get(); + NativePlayback playback = nativePlaybackRef.get(); + + // Model/backend ownership survives a hidden watch screen. YouTube's native + // quick-actions controller, rather than this binding, decides visibility. + return !isNativeQueueDisabled() && queue != null && publishedModel != null && controller != null + && controller.patch_getModel() == publishedModel + && playback != null && playback.patch_isLocalQueue(); + } + + private static PlaylistQueueRenderer.SwipeAction removalAction() { + var label = PlaylistQueueRenderer.Accessibility.newBuilder().setAccessibilityData( + PlaylistQueueRenderer.AccessibilityLabel.newBuilder().setLabel(str("remove"))); + var endpoint = PlaylistQueueRenderer.NavigationEndpoint.newBuilder().setHideEnclosingAction( + PlaylistQueueRenderer.HideEnclosingAction.newBuilder().setHack(true)); + var button = PlaylistQueueRenderer.SwipeButton.newBuilder() + .setStyle(PlaylistQueueRenderer.SwipeButton.Style.BLACK_FILLED) + .setSize(PlaylistQueueRenderer.SwipeButton.Size.DEFAULT).setIsDisabled(false) + .setIcon(Icon.newBuilder().setYtIconType(YTIconType.DELETE)) + .setAccessibilityData(label).setServiceEndpoint(endpoint); + return PlaylistQueueRenderer.SwipeAction.newBuilder().setButtonRenderer(button).build(); + } + + private static PlaylistQueueRenderer.Menu playlistMenu(PlaylistQueue source) { + List videoIds = new ArrayList<>(); + for (PlaylistQueue.Entry entry : source.entries()) { + videoIds.add(entry.videoId()); + } + return PlaylistQueueRenderer.Menu.newBuilder().setMenuRenderer( + PlaylistQueueRenderer.MenuRenderer.newBuilder() + // Only an activated, currently owned header may attach a batch selection. + .addItems(serviceMenuItem(str("playlist_save"), YTIconType.BOOKMARK_BORDER, + QueueSaveActions.openPicker(videoIds))) + .addItems(serviceMenuItem(str("morphe_native_playlist_queue_clear"), YTIconType.DELETE, + PlaylistQueueRenderer.NavigationEndpoint.newBuilder().setHideEnclosingAction( + PlaylistQueueRenderer.HideEnclosingAction.newBuilder().setHack(true)).build())) + .setAccessibility(PlaylistQueueRenderer.Accessibility.newBuilder().setAccessibilityData( + PlaylistQueueRenderer.AccessibilityLabel.newBuilder() + .setLabel(str("accessibility_overflow_menu"))))) + .build(); + } + + private static PlaylistQueueRenderer.Menu rowMenu(PlaylistQueue source, int index) { + PlaylistQueue.Entry entry = source.entries().get(index); + String videoId = entry.videoId(); + var items = PlaylistQueueRenderer.MenuRenderer.newBuilder(); + items.addItems(PlaylistQueueRenderer.MenuItem.newBuilder().setMenuNavigationItemRenderer( + PlaylistQueueRenderer.MenuNavigationItem.newBuilder() + .setText(PlaylistQueueRenderer.Text.newBuilder().setSimpleText(str("accessibility_save_to_playlists"))) + .setIcon(Icon.newBuilder().setYtIconType(YTIconType.BOOKMARK_BORDER)) + .setNavigationEndpoint(PlaylistQueueRenderer.NavigationEndpoint.newBuilder() + .setAddToPlaylistEndpoint(PlaylistQueueRenderer.AddToPlaylistEndpoint.newBuilder() + .setVideoId(videoId))))); + + var download = PlaylistQueueRenderer.NavigationEndpoint.newBuilder().setOfflineVideoEndpoint( + PlaylistQueueRenderer.OfflineVideoEndpoint.newBuilder().setVideoId(videoId) + .setAction(PlaylistQueueRenderer.OfflineVideoEndpoint.Action.INFER_AUTOMATICALLY) + .setOnAdd(PlaylistQueueRenderer.NavigationEndpoint.newBuilder() + .setGetDownloadActionCommand(PlaylistQueueRenderer.GetDownloadActionCommand.newBuilder() + // Captured native GetDownloadActionCommand parameters for a row action. + .setVideoId(videoId).setParams("CAIQAA%3D%3D")))); + items.addItems(serviceMenuItem(str("download"), YTIconType.OFFLINE_DOWNLOAD, download.build())); + + String entity = Base64.encodeToString( + PlaylistQueueRenderer.ShareEntity.newBuilder().setVideoId(videoId).build().toByteArray(), Base64.NO_WRAP); + String escapedEntity = Uri.encode(entity); + String sheetId = "native-playlist-queue-share-" + UUID.randomUUID(); + var request = PlaylistQueueRenderer.NavigationEndpoint.newBuilder().setShareEntityServiceEndpoint( + PlaylistQueueRenderer.ShareEntityServiceEndpoint.newBuilder() + .setSerializedShareEntity(escapedEntity) + .setPanelType(PlaylistQueueRenderer.ShareEntityServiceEndpoint.PanelType.ACTION_SHEET) + .setSheetId(sheetId)); + + // Native onShow runs after the named sheet is created. Its service response + // populates that sheet; no extension layout or share-target list is needed. + var share = PlaylistQueueRenderer.NavigationEndpoint.newBuilder().setElementsCommand( + PlaylistQueueRenderer.ElementsCommand.newBuilder().setShowActionSheetCommand( + PlaylistQueueRenderer.ShowActionSheetCommand.newBuilder().setSheetId(sheetId) + .setOnShow(PlaylistQueueRenderer.ElementsCommand.newBuilder() + .setInnertubeCommand(request)))); + items.addItems(serviceMenuItem(str("accessibility_share_video"), YTIconType.SHARE, share.build())); + + if (index != source.selectedIndex() && !videoId.equals(VideoInformation.getVideoId())) { + var remove = PlaylistQueueRenderer.NavigationEndpoint.newBuilder().setHideEnclosingAction( + PlaylistQueueRenderer.HideEnclosingAction.newBuilder().setHack(true)); + items.addItems(serviceMenuItem(str("remove"), YTIconType.DELETE, remove.build())); + } + + return PlaylistQueueRenderer.Menu.newBuilder().setMenuRenderer(items + .setAccessibility(PlaylistQueueRenderer.Accessibility.newBuilder().setAccessibilityData( + PlaylistQueueRenderer.AccessibilityLabel.newBuilder() + .setLabel(str("accessibility_overflow_menu"))))) + .build(); + } + + private static PlaylistQueueRenderer.MenuItem serviceMenuItem( + String label, YTIconType icon, PlaylistQueueRenderer.NavigationEndpoint endpoint) { + return PlaylistQueueRenderer.MenuItem.newBuilder().setMenuServiceItemRenderer( + PlaylistQueueRenderer.MenuServiceItem.newBuilder() + .setText(PlaylistQueueRenderer.Text.newBuilder().setSimpleText(label)) + .setIcon(Icon.newBuilder().setYtIconType(icon)) + .setServiceEndpoint(endpoint)).build(); + } + + private static byte[] renderer(PlaylistQueue source) { + var panel = PlaylistQueueRenderer.Panel.newBuilder() + .setTitle(str("morphe_native_playlist_queue_title")) + .setSelectedIndex(source.selectedIndex()).setPlaylistId("").setVideoCount(source.entries().size()) + .setLocalSelectedIndex(source.selectedIndex()) + .setMenu(playlistMenu(source)); + var swipeToRemove = removalAction(); + for (int index = 0; index < source.entries().size(); index++) { + PlaylistQueue.Entry entry = source.entries().get(index); + VideoMetadata metadata = metadataCache.get(entry.videoId()); + String title = metadata == null ? entry.title() : metadata.title(); + var image = PlaylistQueueRenderer.Thumbnail.newBuilder() + .setUrl("https://i.ytimg.com/vi/" + entry.videoId() + "/hqdefault.jpg") + .setWidth(480).setHeight(360); + var video = PlaylistQueueRenderer.Video.newBuilder() + .setVideoId(entry.videoId()).setSelected(index == source.selectedIndex()).setCanReorder(true) + .setMenu(rowMenu(source, index)) + .setNavigationEndpoint(PlaylistQueueRenderer.NavigationEndpoint.newBuilder() + .setWatchEndpoint(PlaylistQueueRenderer.WatchEndpoint.newBuilder().setVideoId(entry.videoId()))) + .setTitle(PlaylistQueueRenderer.Text.newBuilder().setSimpleText( + RestoreOriginalTitlesPatch.getTitle(entry.videoId(), title))) + .setMainAppStyle(PlaylistQueueRenderer.MainAppStyle.newBuilder() + .setValue(PlaylistQueueRenderer.MainAppStyle.Style.PROMINENT_THUMBNAIL)) + .setThumbnails(PlaylistQueueRenderer.Thumbnails.newBuilder().addImages(image)); + if (metadata != null) { + video.setShortBylineText(PlaylistQueueRenderer.Text.newBuilder().setSimpleText(metadata.channelName())) + .setVideoInfo(PlaylistQueueRenderer.Text.newBuilder().setSimpleText(metadata.videoInfo())) + .setLengthText(PlaylistQueueRenderer.Text.newBuilder().setSimpleText(metadata.duration())); + } + + if (index != source.selectedIndex() + && !entry.videoId().equals(VideoInformation.getVideoId())) { + video.addSwipeButtons(swipeToRemove); + } + + QueueRatingActions actions = ratingActions.get(entry.videoId()); + if (index == source.selectedIndex() && actions != null && entry.videoId().equals(VideoInformation.getVideoId())) { + video.addAllActionButtons(actions.buttons( + str("offline_playlist_panel_like_button_accessibility_text"), + str("offline_playlist_panel_dislike_button_accessibility_text"))); + } + + panel.addItems(PlaylistQueueRenderer.Item.newBuilder().setVideo(video)); + } + + return panel.build().toByteArray(); + } +} diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/queue/PlaylistMetadataRequest.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/queue/PlaylistMetadataRequest.java new file mode 100644 index 0000000000..dc66631562 --- /dev/null +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/queue/PlaylistMetadataRequest.java @@ -0,0 +1,71 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/3953 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.extension.youtube.patches.queue; + +import androidx.annotation.Nullable; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.util.Locale; + +import app.morphe.extension.shared.Logger; +import app.morphe.extension.shared.Utils; +import app.morphe.extension.shared.requests.Requester; +import app.morphe.extension.shared.requests.Route; +import app.morphe.extension.youtube.patches.utils.requests.ChannelIdRoutes; + +/** Completes missing display data; never supplies playback or account actions. */ +final class PlaylistMetadataRequest { + private static final String VIDEO_FIELD = + "contents.sectionListRenderer.contents.itemSectionRenderer.contents.compactVideoRenderer"; + private static final Route.CompiledRoute SEARCH = new Route(Route.Method.POST, + "search?prettyPrint=false&fields=" + VIDEO_FIELD + ".videoId," + + VIDEO_FIELD + ".title.runs.text," + + VIDEO_FIELD + ".longBylineText.runs.text," + + VIDEO_FIELD + ".shortBylineText.runs.text," + + VIDEO_FIELD + ".shortViewCountText.runs.text," + + VIDEO_FIELD + ".publishedTimeText.runs.text," + + VIDEO_FIELD + ".lengthText.runs.text").compile(); + + private PlaylistMetadataRequest() {} + + @Nullable + static VideoMetadata fetch(String videoId, String title, Locale locale) { + Utils.verifyOffMainThread(); + HttpURLConnection connection = null; + try { + // Titles are searchable even when a newly uploaded video's ID is not indexed. + // The response parser still requires an exact video ID, never the first result. + String query = title.isEmpty() || title.equals(videoId) + ? "https://www.youtube.com/watch?v=" + videoId : title; + byte[] body = ChannelIdRoutes.createSearchBody(query); + connection = ChannelIdRoutes.getConnection(SEARCH); + connection.setFixedLengthStreamingMode(body.length); + try (var output = connection.getOutputStream()) { + output.write(body); + } + + if (connection.getResponseCode() == Requester.HTTP_STATUS_CODE_SUCCESS) { + VideoMetadata result = VideoMetadata.fromSearch(Requester.parseJSONObject(connection), videoId); + return Locale.getDefault().equals(locale) ? result : null; + } + + Logger.printInfo(() -> "Native playlist card lookup was unavailable"); + } catch (IOException exception) { + Logger.printInfo(() -> "Could not complete native playlist card metadata", exception); + } catch (Exception exception) { + Logger.printException(() -> "Could not parse native playlist card metadata", exception); + } finally { + if (connection != null) { + connection.disconnect(); + } + } + + return null; + } +} diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/queue/PlaylistQueue.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/queue/PlaylistQueue.java new file mode 100644 index 0000000000..28643a9d60 --- /dev/null +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/queue/PlaylistQueue.java @@ -0,0 +1,165 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/3953 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.extension.youtube.patches.queue; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.regex.Pattern; + +/** Immutable playlist presentation; selection and repeat mirror the native playback queue. */ +public final class PlaylistQueue { + // YouTube's native repeat observer uses off=0, all=1, one=2. + public static final int REPEAT_OFF = 0; + public static final int REPEAT_ALL = 1; + public static final int REPEAT_ONE = 2; + + private static final Pattern VIDEO_ID = Pattern.compile("[A-Za-z0-9_-]{11}"); + + public record Entry(String videoId, String title) {} + + private final List entries; + private final int selectedIndex; + private final int repeatMode; + + public PlaylistQueue(String playingVideoId, String playingTitle) { + selectedIndex = 0; + repeatMode = REPEAT_OFF; + requireVideoId(playingVideoId); + entries = List.of(new Entry(playingVideoId, + playingTitle == null || playingTitle.isEmpty() ? playingVideoId : playingTitle)); + } + + private PlaylistQueue(List entries, int selectedIndex, int repeatMode) { + this.entries = List.copyOf(entries); + this.selectedIndex = selectedIndex; + this.repeatMode = repeatMode; + } + + public List entries() { + return entries; + } + + public int selectedIndex() { + return selectedIndex; + } + + public Entry currentEntry() { + return entries.get(selectedIndex); + } + + public int repeatMode() { + return repeatMode; + } + + public PlaylistQueue withRepeatMode(int mode) { + if (mode < REPEAT_OFF || mode > REPEAT_ONE) { + throw new IllegalArgumentException("Invalid repeat mode"); + } + return mode == repeatMode ? this : new PlaylistQueue(entries, selectedIndex, mode); + } + + public PlaylistQueue select(int index) { + if (index < 0 || index >= entries.size()) { + throw new IndexOutOfBoundsException(index); + } + return new PlaylistQueue(entries, index, repeatMode); + } + + public PlaylistQueue playNext(String videoId) { + requireVideoId(videoId); + var updated = new ArrayList<>(entries); + updated.add(selectedIndex + 1, new Entry(videoId, videoId)); + return new PlaylistQueue(updated, selectedIndex, repeatMode); + } + + public PlaylistQueue playLast(String videoId) { + requireVideoId(videoId); + var updated = new ArrayList<>(entries); + updated.add(new Entry(videoId, videoId)); + return new PlaylistQueue(updated, selectedIndex, repeatMode); + } + + /** Append a fetched Mix batch atomically, retaining duplicate occurrences and selection. */ + PlaylistQueue append(List additions) { + var updated = new ArrayList<>(entries); + for (Entry entry : additions) { + requireVideoId(entry.videoId()); + updated.add(entry); + } + + return new PlaylistQueue(updated, selectedIndex, repeatMode); + } + + /** Insert after the playing entry when an outside video becomes the active watch video. */ + public PlaylistQueue playNextAndSelect(String videoId) { + return playNext(videoId).select(selectedIndex + 1); + } + + public PlaylistQueue remove(int index) { + if (index < 0 || index >= entries.size()) { + throw new IndexOutOfBoundsException(index); + } + if (index == selectedIndex) { + return this; + } + var updated = new ArrayList<>(entries); + updated.remove(index); + return new PlaylistQueue(updated, selectedIndex - (index < selectedIndex ? 1 : 0), repeatMode); + } + + /** Move an entry with the native adapter's remove-then-insert semantics. */ + public PlaylistQueue move(int from, int to) { + if (from < 0 || from >= entries.size()) { + throw new IndexOutOfBoundsException(from); + } + if (to < 0 || to >= entries.size()) { + throw new IndexOutOfBoundsException(to); + } + if (from == to) { + return this; + } + var updated = new ArrayList<>(entries); + updated.add(to, updated.remove(from)); + + // Entry identity also distinguishes duplicate videos in the same queue. + int selection = selectedIndex; + if (from == selection) { + selection = to; + } else if (from < selection && to >= selection) { + selection--; + } else if (from > selection && to <= selection) { + selection++; + } + return new PlaylistQueue(updated, selection, repeatMode); + } + + /** Shuffle is a command: the displayed order also becomes the playback order. */ + public PlaylistQueue shuffle() { + if (entries.size() < 2) { + return this; + } + var updated = new ArrayList<>(entries); + Entry playing = updated.remove(selectedIndex); + + // Use the same framework shuffler as YouTube's mutable playback queue. + Collections.shuffle(updated); + updated.add(0, playing); + return new PlaylistQueue(updated, 0, repeatMode); + } + + public static boolean isVideoId(String videoId) { + return videoId != null && VIDEO_ID.matcher(videoId).matches(); + } + + private static void requireVideoId(String videoId) { + if (!isVideoId(videoId)) { + throw new IllegalArgumentException("Invalid YouTube video ID"); + } + } +} diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/queue/QueueFlyoutActions.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/queue/QueueFlyoutActions.java new file mode 100644 index 0000000000..2fe9e45a78 --- /dev/null +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/queue/QueueFlyoutActions.java @@ -0,0 +1,170 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/3953 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.extension.youtube.patches.queue; + +import static app.morphe.extension.shared.StringRef.str; + +import androidx.annotation.Nullable; + +import com.google.protobuf.MessageLite; + +import app.morphe.extension.shared.Logger; +import app.morphe.extension.youtube.innertube.IconOuterClass.Icon; +import app.morphe.extension.youtube.innertube.IconOuterClass.YTIconType; +import app.morphe.extension.youtube.innertube.queue.PlaylistQueueRenderer; +import app.morphe.extension.youtube.innertube.queue.PlaylistQueueRenderer.MenuItem; +import app.morphe.extension.youtube.innertube.queue.PlaylistQueueRenderer.MenuRenderer; +import app.morphe.extension.youtube.patches.AddToQueuePatch; +import app.morphe.extension.youtube.patches.utils.FlyoutUtils; + +/** Adjusts native menu data before YouTube filters and binds its rows. */ +public final class QueueFlyoutActions { + private static final int MDX_CONNECTED = 1; + private static final int MDX_DISCONNECTED = 2; + + public interface Menu { + MessageLite patch_parseQueueFlyout(byte[] bytes); + } + + /** Injection point. Other queue providers and unrelated menu sources keep the original model. */ + @Nullable + public static MessageLite completeQueueMenu(@Nullable MessageLite original) { + if (original == null || !NativePlaylistQueuePatch.canQueueFlyoutVideo(FlyoutUtils.getFlyoutVideoId())) { + return original; + } + try { + MenuRenderer menu = MenuRenderer.parseFrom(original.toByteArray()); + MenuRenderer completed = completeMenu(menu, + NativePlaylistQueuePatch.canShowFlyoutLast(), + str("morphe_native_playlist_queue_next"), str("morphe_native_playlist_queue_last")); + if (completed == menu) { + return original; + } + return ((Menu) original).patch_parseQueueFlyout(completed.toByteArray()); + } catch (Exception exception) { + Logger.printException(() -> "completeQueueMenu failure", exception); + return original; + } + } + + /** Direct native sheet dispatch, which does not run the button model's Runnable. */ + public static boolean onStockQueueItemClick(Enum icon) { + return icon != null && AddToQueuePatch.isNativeQueueEnabled() + && AddToQueuePatch.flyoutButtonClickLogic(icon.name(), FlyoutUtils.getFlyoutVideoId()); + } + + static MenuRenderer completeMenu(MenuRenderer menu, boolean allowLast, String nextLabel, String lastLabel) { + MenuItem next = null; + MenuItem last = null; + for (MenuItem item : menu.getItemsList()) { + if (!isLocalItem(item)) { + continue; + } + YTIconType icon = icon(item); + if (icon == YTIconType.QUEUE_PLAY_NEXT && next == null) { + next = item; + } else if (icon == YTIconType.QUEUE_PLAY_LAST && last == null) { + last = item; + } + } + + if (next == null && last == null) { + return menu; + } + next = prepareLocalItem(next == null ? queueItem(YTIconType.QUEUE_PLAY_NEXT, nextLabel) : next); + if (allowLast) { + last = prepareLocalItem(last == null ? queueItem(YTIconType.QUEUE_PLAY_LAST, lastLabel) : last); + } + var completed = menu.toBuilder().clearItems(); + boolean added = false; + for (MenuItem item : menu.getItemsList()) { + YTIconType icon = icon(item); + if (isLocalItem(item) && (icon == YTIconType.QUEUE_PLAY_NEXT || icon == YTIconType.QUEUE_PLAY_LAST)) { + if (!added) { + completed.addItems(next); + if (allowLast) { + completed.addItems(last); + } + added = true; + } + } else { + completed.addItems(item); + } + } + + MenuRenderer result = completed.build(); + return result.equals(menu) ? menu : result; + } + + private static MenuItem queueItem(YTIconType icon, String label) { + // No copied Premium/Cast/backend endpoint. The shared stock overrides + // dispatch this row to Native; other modes cannot run an unrelated command. + return MenuItem.newBuilder().setMenuConditionalServiceItemRenderer( + PlaylistQueueRenderer.MenuConditionalServiceItem.newBuilder() + .setText(PlaylistQueueRenderer.Text.newBuilder().setSimpleText(label)) + .setIcon(Icon.newBuilder().setYtIconType(icon)) + .setVisibility(MDX_DISCONNECTED)).build(); + } + + private static boolean isLocalItem(MenuItem item) { + // Cast actions can share the queue icon, but retain their native MDX condition. + return visibility(item) != MDX_CONNECTED; + } + + private static int visibility(MenuItem item) { + if (item.hasMenuConditionalServiceItemRenderer()) { + return item.getMenuConditionalServiceItemRenderer().getVisibility(); + } + if (item.hasMenuConditionalNavigationItemRenderer()) { + return item.getMenuConditionalNavigationItemRenderer().getVisibility(); + } + return MDX_DISCONNECTED; + } + + private static MenuItem prepareLocalItem(MenuItem item) { + // Remove the secondary Premium icon in the model as well as in the shared + // list binder: legacy sheets do not record buttons through that binder. + if (item.hasMenuConditionalServiceItemRenderer()) { + return item.toBuilder().setMenuConditionalServiceItemRenderer( + item.getMenuConditionalServiceItemRenderer().toBuilder() + .setVisibility(MDX_DISCONNECTED).clearSecondaryIcon()).build(); + } + + if (item.hasMenuConditionalNavigationItemRenderer()) { + return item.toBuilder().setMenuConditionalNavigationItemRenderer( + item.getMenuConditionalNavigationItemRenderer().toBuilder().setVisibility(MDX_DISCONNECTED)).build(); + } + + if (item.hasMenuServiceItemRenderer()) { + return item.toBuilder().setMenuServiceItemRenderer( + item.getMenuServiceItemRenderer().toBuilder().clearSecondaryIcon()).build(); + } + + if (item.hasMenuNavigationItemRenderer()) { + return item.toBuilder().setMenuNavigationItemRenderer( + item.getMenuNavigationItemRenderer().toBuilder().clearSecondaryIcon()).build(); + } + + return item; + } + + private static YTIconType icon(MenuItem item) { + if (item.hasMenuConditionalServiceItemRenderer()) { + return item.getMenuConditionalServiceItemRenderer().getIcon().getYtIconType(); + } + if (item.hasMenuConditionalNavigationItemRenderer()) { + return item.getMenuConditionalNavigationItemRenderer().getIcon().getYtIconType(); + } + if (item.hasMenuServiceItemRenderer()) { + return item.getMenuServiceItemRenderer().getIcon().getYtIconType(); + } + return item.getMenuNavigationItemRenderer().getIcon().getYtIconType(); + } + + private QueueFlyoutActions() {} +} diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/queue/QueueFullscreenActions.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/queue/QueueFullscreenActions.java new file mode 100644 index 0000000000..683a2d9277 --- /dev/null +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/queue/QueueFullscreenActions.java @@ -0,0 +1,93 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/3953 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.extension.youtube.patches.queue; + +import static app.morphe.extension.shared.StringRef.str; + +import androidx.annotation.Nullable; + +import com.google.protobuf.MessageLite; + +import java.lang.ref.WeakReference; + +import app.morphe.extension.shared.Logger; +import app.morphe.extension.youtube.innertube.queue.fullscreen.QueueFullscreenRenderer; + +/** Supplies only the playlist action to YouTube's existing fullscreen renderer. */ +public final class QueueFullscreenActions { + public interface Controller { + Object patch_parseQuickActions(byte[] bytes); + + void patch_refreshQuickActions(Object model); + } + + private static WeakReference controllerRef = new WeakReference<>(null); + @Nullable + private static Object originalModel; + private static boolean renderedWithQueue; + + private QueueFullscreenActions() { + } + + /** Injection point in the native UI model setter, including its null/reset path. */ + @Nullable + public static Object onModelLoaded(Controller controller, @Nullable Object model) { + try { + controllerRef = new WeakReference<>(controller); + originalModel = model; + renderedWithQueue = NativePlaylistQueuePatch.ownsFullscreenPlaylist(); + if (model == null || !renderedWithQueue) { + return model; + } + var renderer = QueueFullscreenRenderer.Renderer.parseFrom(((MessageLite) model).toByteArray()); + var component = renderer.getNewElement().getType().getComponent(); + if (!component.getModel().hasQuickActions()) { + return model; + } + var actions = component.getModel().getQuickActions(); + + // An account playlist already supplies its own action; leave it intact. + if (actions.hasPlaylistButton()) { + return model; + } + var button = QueueFullscreenRenderer.PlaylistButton.newBuilder().setStyle(2).setButton( + QueueFullscreenRenderer.Button.newBuilder() + .setIcon("yt_outline_list_play_arrow_black_24") + .setAccessibilityLabel(str("show_playlist_videos")) + .setOnTap(QueueFullscreenRenderer.Command.newBuilder().setInnertubeCommand( + QueueFullscreenRenderer.Endpoint.newBuilder().setShowEngagementPanel( + QueueFullscreenRenderer.ShowPanel.newBuilder() + .setPanelId("engagement-panel-playlist"))))); + var updatedComponent = component.toBuilder().setModel(component.getModel().toBuilder() + .setQuickActions(actions.toBuilder().setPlaylistButton(button))); + var element = renderer.getNewElement(); + return controller.patch_parseQuickActions(renderer.toBuilder().setNewElement(element.toBuilder() + .setType(element.getType().toBuilder().setComponent(updatedComponent))).build().toByteArray()); + } catch (Exception ex) { + Logger.printException(() -> "onModelLoaded failure", ex); + return model; + } + } + + /** Called after queue ownership changes, never for metadata/order-only updates. */ + static void refresh() { + Controller controller = controllerRef.get(); + Object model = originalModel; + if (controller == null || model == null + || renderedWithQueue == NativePlaylistQueuePatch.ownsFullscreenPlaylist()) { + return; + } + + try { + controller.patch_refreshQuickActions(model); + } catch (Exception ex) { + // A presentation refresh must never roll back an attached playback queue. + Logger.printException(() -> "Could not refresh native fullscreen queue action", ex); + } + } +} diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/queue/QueueMixActions.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/queue/QueueMixActions.java new file mode 100644 index 0000000000..91edb6b761 --- /dev/null +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/queue/QueueMixActions.java @@ -0,0 +1,38 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/3953 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.extension.youtube.patches.queue; + +import app.morphe.extension.youtube.innertube.queue.PlaylistQueueRenderer.ConfirmationDialog; +import app.morphe.extension.youtube.innertube.queue.PlaylistQueueRenderer.ConfirmationDialogRenderer; +import app.morphe.extension.youtube.innertube.queue.PlaylistQueueRenderer.NavigationEndpoint; +import app.morphe.extension.youtube.innertube.queue.PlaylistQueueRenderer.ShowConfirmationDialogEndpoint; +import app.morphe.extension.youtube.innertube.queue.PlaylistQueueRenderer.SwipeAction; +import app.morphe.extension.youtube.innertube.queue.PlaylistQueueRenderer.SwipeButton; +import app.morphe.extension.youtube.innertube.queue.PlaylistQueueRenderer.Text; + +/** Native Mix confirmation; each button retains the original watch command and a local capability. */ +final class QueueMixActions { + private QueueMixActions() {} + + static NavigationEndpoint dialog(NavigationEndpoint original, String token, + String title, String watchLabel, String addLabel) { + var watch = original.toBuilder().setQueueMixChoiceToken(token).setQueueMixAdd(false); + var add = original.toBuilder().setQueueMixChoiceToken(token).setQueueMixAdd(true); + return NavigationEndpoint.newBuilder().setShowConfirmationDialogEndpoint( + ShowConfirmationDialogEndpoint.newBuilder().setDialog(ConfirmationDialog.newBuilder() + .setConfirmDialogRenderer(ConfirmationDialogRenderer.newBuilder() + .setTitle(Text.newBuilder().setSimpleText(title)) + .setCancelButton(button(watch.build(), watchLabel)) + .setConfirmButton(button(add.build(), addLabel))))).build(); + } + + private static SwipeAction button(NavigationEndpoint endpoint, String label) { + return SwipeAction.newBuilder().setButtonRenderer(SwipeButton.newBuilder() + .setText(Text.newBuilder().setSimpleText(label)).setServiceEndpoint(endpoint)).build(); + } +} diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/queue/QueueRatingActions.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/queue/QueueRatingActions.java new file mode 100644 index 0000000000..499cd2423c --- /dev/null +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/queue/QueueRatingActions.java @@ -0,0 +1,122 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/3953 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.extension.youtube.patches.queue; + +import androidx.annotation.Nullable; + +import com.google.protobuf.ByteString; +import com.google.protobuf.InvalidProtocolBufferException; + +import java.util.List; + +import app.morphe.extension.youtube.innertube.IconOuterClass.Icon; +import app.morphe.extension.youtube.innertube.IconOuterClass.YTIconType; +import app.morphe.extension.youtube.innertube.NextResponseOuterClass; +import app.morphe.extension.youtube.innertube.NextResponseOuterClass.Commands; +import app.morphe.extension.youtube.innertube.queue.PlaylistQueueRenderer; +import app.morphe.extension.youtube.innertube.queue.PlaylistQueueRenderer.LikeEndpoint.Status; + +/** Native watch-page commands and entity identity, reused by native playlist buttons. */ +record QueueRatingActions(String videoId, String entityKey, int initialStatus, + ByteString likeEndpoint, ByteString unlikeEndpoint, + ByteString dislikeEndpoint, ByteString undislikeEndpoint) { + @Nullable + static QueueRatingActions fromElement(NextResponseOuterClass.NewElement element) + throws InvalidProtocolBufferException { + var model = element.getType().getComponentType().getModel(); + var buttons = model.hasVideoActionBarModel() + ? model.getVideoActionBarModel().getVideoActionBarData().getActionButtonsList() + : model.getYoutubeModel().getViewModel().getCompactifyVideoActionBarViewModel().getActionButtonsList(); + NextResponseOuterClass.LikeButtonViewModel like = null; + NextResponseOuterClass.DislikeButtonViewModel dislike = null; + for (var action : buttons) { + var button = action.getPrimaryButtonViewModel(); + if (button.hasLikeButtonViewModel()) { + like = button.getLikeButtonViewModel(); + } + if (button.hasDislikeButtonViewModel()) { + dislike = button.getDislikeButtonViewModel(); + } + if (button.hasSegmentedLikeDislikeButtonViewModel()) { + var segmented = button.getSegmentedLikeDislikeButtonViewModel(); + like = segmented.getLikeButtonViewModel().getLikeButtonViewModel(); + dislike = segmented.getDislikeButtonViewModel().getDislikeButtonViewModel(); + } + } + + if (like == null || dislike == null || like.getLikeStatusEntityKey().isEmpty() + || !like.getLikeStatusEntityKey().equals(dislike.getDislikeEntityKey())) { + return null; + } + + var likeToggle = like.getToggleButtonViewModel().getToggleButtonViewModel(); + var dislikeToggle = dislike.getToggleButtonViewModel().getToggleButtonViewModel(); + ByteString likeEndpoint = endpoint(likeToggle.getDefaultButtonViewModel().getButtonViewModel().getOnTap()); + ByteString unlikeEndpoint = endpoint(likeToggle.getToggledButtonViewModel().getButtonViewModel().getOnTap()); + ByteString dislikeEndpoint = endpoint(dislikeToggle.getDefaultButtonViewModel().getButtonViewModel().getOnTap()); + ByteString undislikeEndpoint = endpoint(dislikeToggle.getToggledButtonViewModel().getButtonViewModel().getOnTap()); + String videoId = target(likeEndpoint, Status.LIKE); + if (!PlaylistQueue.isVideoId(videoId) || !videoId.equals(target(unlikeEndpoint, Status.INDIFFERENT)) + || !videoId.equals(target(dislikeEndpoint, Status.DISLIKE)) + || !videoId.equals(target(undislikeEndpoint, Status.INDIFFERENT))) { + return null; + } + + int initialStatus = like.hasLikeStatusEntity() + ? like.getLikeStatusEntity().getLikeStatus() : Status.INDIFFERENT_VALUE; + return new QueueRatingActions(videoId, like.getLikeStatusEntityKey(), initialStatus, + likeEndpoint, unlikeEndpoint, dislikeEndpoint, undislikeEndpoint); + } + + private static ByteString endpoint(Commands commands) { + if (!commands.getInnertubeCommand().isEmpty()) { + return commands.getInnertubeCommand(); + } + for (var command : commands.getSerialCommand().getCommandsList()) { + ByteString endpoint = endpoint(command); + if (!endpoint.isEmpty()) { + return endpoint; + } + } + + return ByteString.EMPTY; + } + + private static String target(ByteString bytes, Status expected) throws InvalidProtocolBufferException { + var endpoint = PlaylistQueueRenderer.RatingEndpoint.parseFrom(bytes); + var like = endpoint.getLikeEndpoint(); + return endpoint.hasLikeEndpoint() && like.hasStatus() && like.getStatus() == expected + && like.getTarget().getPlaylistId().isEmpty() ? like.getTarget().getVideoId() : ""; + } + + List buttons(String likeLabel, String dislikeLabel) { + return List.of(button(true, likeLabel), button(false, dislikeLabel)); + } + + private PlaylistQueueRenderer.RatingAction button(boolean isLike, String label) { + var button = PlaylistQueueRenderer.RatingToggleButton.newBuilder() + .setIsDisabled(false).setIsToggled(initialStatus == (isLike ? Status.LIKE_VALUE : Status.DISLIKE_VALUE)) + .setDefaultIcon(Icon.newBuilder().setYtIconType(isLike ? YTIconType.LIKE : YTIconType.DISLIKE)) + .setToggledIcon(Icon.newBuilder().setYtIconType(isLike ? YTIconType.LIKE_SELECTED : YTIconType.DISLIKE_SELECTED)) + .setDefaultServiceEndpoint(isLike ? likeEndpoint : dislikeEndpoint) + .setToggledServiceEndpoint(isLike ? unlikeEndpoint : undislikeEndpoint) + .setAccessibilityData(accessibility(label)) + .setToggledAccessibilityData(accessibility(label)); + return PlaylistQueueRenderer.RatingAction.newBuilder().setSlimMetadataToggleButton( + PlaylistQueueRenderer.SlimRatingButton.newBuilder().setIsLike(isLike).setIsDislike(!isLike) + .setTarget(PlaylistQueueRenderer.RatingTarget.newBuilder().setVideoId(videoId)) + .setLikeStatusEntityKey(entityKey) + .setButton(PlaylistQueueRenderer.RatingToggleContainer.newBuilder().setToggleButtonRenderer(button))) + .build(); + } + + private static PlaylistQueueRenderer.Accessibility accessibility(String label) { + return PlaylistQueueRenderer.Accessibility.newBuilder() + .setAccessibilityData(PlaylistQueueRenderer.AccessibilityLabel.newBuilder().setLabel(label)).build(); + } +} diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/queue/QueueSaveActions.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/queue/QueueSaveActions.java new file mode 100644 index 0000000000..e255d8af2d --- /dev/null +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/queue/QueueSaveActions.java @@ -0,0 +1,223 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/3953 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.extension.youtube.patches.queue; + +import static app.morphe.extension.shared.StringRef.str; + +import androidx.annotation.Nullable; + +import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protobuf.MessageLite; + +import java.util.ArrayList; +import java.util.List; + +import app.morphe.extension.shared.Logger; +import app.morphe.extension.youtube.innertube.queue.PlaylistQueueRenderer.AddToPlaylistEndpoint; +import app.morphe.extension.youtube.innertube.queue.PlaylistQueueRenderer.CreatePlaylistDialogEndpoint; +import app.morphe.extension.youtube.innertube.queue.PlaylistQueueRenderer.NavigationEndpoint; +import app.morphe.extension.youtube.innertube.queue.PlaylistQueueRenderer.PlaylistEditAction; +import app.morphe.extension.youtube.innertube.queue.PlaylistQueueRenderer.QueueSaveEditResponse; +import app.morphe.extension.youtube.innertube.queue.PlaylistQueueRenderer.QueueSaveResponse; + +/** An owned command transfers its immutable selection to the native picker's saved arguments. */ +public final class QueueSaveActions { + public interface Picker { + List patch_getQueueSaveVideoIds(); + MessageLite patch_parseQueueSaveResponse(byte[] bytes); + } + + private QueueSaveActions() { + } + + static NavigationEndpoint openPicker(List videoIds) { + if (videoIds.isEmpty()) { + throw new IllegalArgumentException("Empty queue save selection"); + } + return NavigationEndpoint.newBuilder().setAddToPlaylistEndpoint(AddToPlaylistEndpoint.newBuilder() + .setVideoId(videoIds.get(0))).addAllQueueSaveVideoIds(videoIds).build(); + } + + /** Read our local command field after native protobuf copies, without global selection state. */ + @Nullable + public static ArrayList copyVideoIds(MessageLite endpoint) throws InvalidProtocolBufferException { + var command = NavigationEndpoint.parseFrom(endpoint.toByteArray()); + return command.getQueueSaveVideoIdsCount() == 0 ? null + : new ArrayList<>(command.getQueueSaveVideoIdsList()); + } + + /** Bind the actual mutation commands, rather than assuming the query response is a batch. */ + public static MessageLite onPickerResponse(Picker picker, MessageLite response) { + try { + var ids = picker.patch_getQueueSaveVideoIds(); + if (ids == null) { + return response; + } + return picker.patch_parseQueueSaveResponse(bindResponse( + QueueSaveResponse.parseFrom(response.toByteArray()), ids).toByteArray()); + } catch (Exception ex) { + Logger.printException(() -> "onPickerResponse failure", ex); + + // An empty native response follows the original close/error path. Never + // silently fall back to saving one video after a failed batch binding. + return picker.patch_parseQueueSaveResponse(QueueSaveResponse.getDefaultInstance().toByteArray()); + } + } + + static QueueSaveResponse bindResponse(QueueSaveResponse response, List ids) { + var result = response.toBuilder(); + for (int index = 0; index < response.getContentsCount(); index++) { + var item = response.getContents(index); + if (!item.hasAddToPlaylistRenderer()) { + continue; + } + var renderer = item.getAddToPlaylistRenderer(); + var updated = renderer.toBuilder(); + for (int row = 0; row < renderer.getPlaylistsCount(); row++) { + var destination = renderer.getPlaylists(row); + if (!destination.hasPlaylistAddToOptionRenderer()) { + continue; + } + var option = destination.getPlaylistAddToOptionRenderer(); + if (!option.hasAddEndpoint()) { + continue; + } + String title = option.getTitle().getSimpleText(); + if (title.isEmpty()) { + var text = new StringBuilder(); + for (var run : option.getTitle().getRunsList()) { + text.append(run.getText()); + } + title = text.toString(); + } + + var add = bindCommand(option.getAddEndpoint(), ids).toBuilder() + .clearQueueSaveVideoIds().addAllQueueSaveVideoIds(ids).setQueueSavePlaylistTitle(title); + var updatedOption = option.toBuilder().setAddEndpoint(add); + if (ids.size() > 1) { + // The native query describes only its seed video. UNKNOWN uses + // YouTube's add-and-close row, avoiding a false whole-queue + // checkbox state and its single-video remove action. + updatedOption.setSelectionState(0); + } + + updated.setPlaylists(row, destination.toBuilder().setPlaylistAddToOptionRenderer(updatedOption)); + } + + for (int row = 0; row < renderer.getHeadersCount(); row++) { + var header = renderer.getHeaders(row); + if (!header.hasPlaylistAddToHeaderRenderer()) { + continue; + } + var content = header.getPlaylistAddToHeaderRenderer(); + if (!content.getButton().hasButtonRenderer()) { + continue; + } + var button = content.getButton().getButtonRenderer(); + + // This native picker header is its New playlist action. Its server + // command need not be a direct create endpoint. Use YouTube's own + // list-capable dialog instead, without rejecting the whole picker. + // Clear all three native click slots so creation dispatches once. + var updatedButton = button.toBuilder().clearServiceEndpoint().clearCommand() + .setNavigationEndpoint(NavigationEndpoint.newBuilder().setCreatePlaylistDialogEndpoint( + CreatePlaylistDialogEndpoint.newBuilder().addAllVideoIds(ids))); + updated.setHeaders(row, header.toBuilder().setPlaylistAddToHeaderRenderer(content.toBuilder() + .setButton(content.getButton().toBuilder().setButtonRenderer(updatedButton)))); + } + + result.setContents(index, item.toBuilder().setAddToPlaylistRenderer(updated)); + } + + return result.build(); + } + + static NavigationEndpoint bindCommand(NavigationEndpoint command, List ids) { + if (command.hasPlaylistEditEndpoint()) { + var edit = command.getPlaylistEditEndpoint(); + var additions = new ArrayList(); + for (var action : edit.getActionsList()) { + if (action.getAction() == 1) { + additions.add(action); + } + } + + if (additions.size() == 1 && additions.get(0).getAddedVideoId().equals(ids.get(0))) { + var updated = edit.toBuilder().clearActions(); + for (var action : edit.getActionsList()) { + if (action.getAction() == 1) { + for (String id : ids) { + updated.addActions(action.toBuilder().setAddedVideoId(id)); + } + } else { + updated.addActions(action); + } + } + + return command.toBuilder().setPlaylistEditEndpoint(updated).build(); + } + + var addedIds = new ArrayList(); + for (var action : additions) { + addedIds.add(action.getAddedVideoId()); + } + if (addedIds.equals(ids)) { + return command; + } + throw new IllegalStateException("Native queue save did not supply the complete add selection"); + } + + throw new IllegalStateException("Unrecognized native queue save destination command"); + } + + /** The native edit callback owns its selection; no process-wide pending save. */ + public static void onEditCreated(QueueSaveFeedback.Callback callback, MessageLite endpoint) { + try { + var command = NavigationEndpoint.parseFrom(endpoint.toByteArray()); + if (command.getQueueSaveVideoIdsCount() == 0 || !command.hasPlaylistEditEndpoint() + || !command.hasQueueSavePlaylistTitle()) { + return; + } + + var edit = command.getPlaylistEditEndpoint(); + if (edit.getActionsCount() == 0) { + return; + } + for (var action : edit.getActionsList()) { + if (action.getAction() != 1) { + return; + } + } + + callback.patch_setQueueSaveSelection(new QueueSaveFeedback.Selection(edit.getPlaylistId(), + command.getQueueSavePlaylistTitle(), command.getQueueSaveVideoIdsList())); + } catch (Exception ex) { + Logger.printException(() -> "onEditCreated failure", ex); + } + } + + /** Runs only on the native success response, before its original action dispatch. */ + public static MessageLite onEditResult(QueueSaveFeedback.Callback callback, MessageLite response) { + try { + var selection = callback.patch_getQueueSaveSelection(); + if (selection == null) { + return response; + } + var result = QueueSaveEditResponse.parseFrom(response.toByteArray()); + String message = selection.title().isEmpty() ? str("morphe_native_playlist_queue_saved") + : str("morphe_native_playlist_queue_saved_to", selection.title()); + var decorated = QueueSaveFeedback.decorate(result, selection, message, + str("offline_undo_snackbar_button_text")); + return decorated == result ? response : callback.patch_parseQueueSaveResult(decorated.toByteArray()); + } catch (Exception ex) { + // The save already completed; never replace its result with a UI failure. + Logger.printException(() -> "onEditResult failure", ex); + return response; + } + } +} diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/queue/QueueSaveFeedback.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/queue/QueueSaveFeedback.java new file mode 100644 index 0000000000..d02195b8db --- /dev/null +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/queue/QueueSaveFeedback.java @@ -0,0 +1,109 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/3953 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.extension.youtube.patches.queue; + +import com.google.protobuf.MessageLite; + +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; + +import app.morphe.extension.shared.Logger; +import app.morphe.extension.youtube.innertube.queue.PlaylistQueueRenderer.AddToToastAction; +import app.morphe.extension.youtube.innertube.queue.PlaylistQueueRenderer.NavigationEndpoint; +import app.morphe.extension.youtube.innertube.queue.PlaylistQueueRenderer.PlaylistEditAction; +import app.morphe.extension.youtube.innertube.queue.PlaylistQueueRenderer.PlaylistEditEndpoint; +import app.morphe.extension.youtube.innertube.queue.PlaylistQueueRenderer.QueueSaveEditResponse; +import app.morphe.extension.youtube.innertube.queue.PlaylistQueueRenderer.QueueToastItem; +import app.morphe.extension.youtube.innertube.queue.PlaylistQueueRenderer.QueueToastRenderer; +import app.morphe.extension.youtube.innertube.queue.PlaylistQueueRenderer.SwipeAction; +import app.morphe.extension.youtube.innertube.queue.PlaylistQueueRenderer.SwipeButton; +import app.morphe.extension.youtube.innertube.queue.PlaylistQueueRenderer.Text; + +/** Native success feedback; Undo targets only entry IDs returned by this Add request. */ +public final class QueueSaveFeedback { + private static final int CONFIRMATION_DURATION_MILLISECONDS = 5_000; + + public interface Callback { + Selection patch_getQueueSaveSelection(); + void patch_setQueueSaveSelection(Selection selection); + MessageLite patch_parseQueueSaveResult(byte[] bytes); + } + + public record Selection(String playlistId, String title, List videoIds) { + public Selection { + videoIds = List.copyOf(videoIds); + } + } + + private QueueSaveFeedback() { + } + + /** Zero retains native timing, including its accessible queue-clear timeout. */ + public static int confirmationDuration(MessageLite renderer) { + try { + var toast = QueueToastRenderer.parseFrom(renderer.toByteArray()); + if (toast.getQueueSaveConfirmation()) { + return CONFIRMATION_DURATION_MILLISECONDS; + } + String undoToken = toast.getActionButton().getButtonRenderer().getServiceEndpoint().getQueueUndoToken(); + return undoToken.isEmpty() ? 0 : NativePlaylistQueuePatch.queueUndoDuration(undoToken); + } catch (Exception ex) { + Logger.printException(() -> "confirmationDuration failure", ex); + return 0; + } + } + + static QueueSaveEditResponse decorate(QueueSaveEditResponse response, Selection selection, + String message, String undoLabel) { + // Native PlaylistEditResponse status: 0 unknown, 1 succeeded. + if (response.getStatus() != 1) { + return response; + } + for (var action : response.getActionsList()) { + if (action.hasAddToToastAction()) { + return response; + } + } + + var toast = QueueToastRenderer.newBuilder().setQueueSaveConfirmation(true) + .setResponseText(Text.newBuilder().setSimpleText(message)); + var requestedIds = new HashSet<>(selection.videoIds()); + var entryIds = new LinkedHashSet(); + boolean validUndo = !selection.playlistId().isEmpty(); + for (var result : response.getResultsList()) { + if (!result.hasAddedEntry()) { + validUndo = false; + break; + } + + var entry = result.getAddedEntry(); + if (entry.getSetVideoId().isEmpty() || !requestedIds.contains(entry.getVideoId())) { + validUndo = false; + break; + } + + entryIds.add(entry.getSetVideoId()); + } + + if (validUndo && !entryIds.isEmpty()) { + var undo = PlaylistEditEndpoint.newBuilder().setPlaylistId(selection.playlistId()); + for (String id : entryIds) { + // ACTION_REMOVE_VIDEO uses the exact playlist entry, not video ID. + undo.addActions(PlaylistEditAction.newBuilder().setAction(2).setSetVideoId(id)); + } + + toast.setActionButton(SwipeAction.newBuilder().setButtonRenderer(SwipeButton.newBuilder() + .setText(Text.newBuilder().setSimpleText(undoLabel)) + .setServiceEndpoint(NavigationEndpoint.newBuilder().setPlaylistEditEndpoint(undo)))); + } + + return response.toBuilder().addActions(NavigationEndpoint.newBuilder().setAddToToastAction( + AddToToastAction.newBuilder().setItem(QueueToastItem.newBuilder().setToastRenderer(toast)))).build(); + } +} diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/queue/QueueShortsSession.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/queue/QueueShortsSession.java new file mode 100644 index 0000000000..3e1016d02b --- /dev/null +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/queue/QueueShortsSession.java @@ -0,0 +1,65 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/3953 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.extension.youtube.patches.queue; + +/** Main-thread suspension of a regular queue while the Shorts player owns playback. */ +final class QueueShortsSession { + enum Action { NONE, SUSPEND, WAIT, RESUME, RELEASE } + + private boolean suspended; + + boolean isSuspended() { + return suspended; + } + + void reset() { + suspended = false; + } + + Action update(boolean shortsOpen, boolean playerHidden, boolean regularPlayerVisible, + String activeVideoId, String regularVideoId, boolean accountPlaylist, + boolean queueOwnedByService) { + if (shortsOpen || playerHidden) { + if (suspended) { + return Action.WAIT; + } + suspended = true; + return Action.SUSPEND; + } + + if (!suspended) { + return Action.NONE; + } + + // A regular background session can resume before its watch view/controller. + // The caller must confirm both the service's current ID and active ID; + // UI visibility alone cannot establish that playback has ended. + if (queueOwnedByService && !accountPlaylist) { + suspended = false; + return Action.RESUME; + } + + if (!regularPlayerVisible && activeVideoId.isEmpty() && regularVideoId.isEmpty()) { + suspended = false; + return Action.RELEASE; + } + + // Detaching Shorts does not itself restore the regular video ID or its + // controller. Wait for their agreement; a late Short must not end the queue. + if (!regularPlayerVisible || activeVideoId.isEmpty() || regularVideoId.isEmpty() + || !activeVideoId.equals(regularVideoId)) { + return Action.WAIT; + } + + suspended = false; + + // A newly opened regular video is inserted by the queue's controller path. + // Only an account playlist takes ownership away from the retained queue. + return accountPlaylist ? Action.RELEASE : Action.RESUME; + } +} diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/queue/QueueStart.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/queue/QueueStart.java new file mode 100644 index 0000000000..356430c494 --- /dev/null +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/queue/QueueStart.java @@ -0,0 +1,53 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/3953 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.extension.youtube.patches.queue; + +import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protobuf.MessageLite; + +import java.util.HashMap; +import java.util.Map; + +import app.morphe.extension.youtube.innertube.queue.PlaylistQueueRenderer.NavigationEndpoint; + +/** One immutable start or restore request, separate from an attached native queue. */ +record QueueStart(PlaylistQueue candidate, String previousVideoId, long resumePositionMilliseconds) { + QueueStart(PlaylistQueue candidate, String previousVideoId) { + this(candidate, previousVideoId, -1); + } + + boolean restoresClosedPlayer() { + return resumePositionMilliseconds >= 0; + } + + String watchUrl() { + // Match the native watch intent used by LoadVideoPatch's Reload action. + return "https://www.youtube.com/watch?v=" + candidate.currentEntry().videoId() + + (restoresClosedPlayer() ? "&t=" + resumePositionMilliseconds / 1000 + "s" : ""); + } + + boolean isSupersededBy(String videoId) { + return PlaylistQueue.isVideoId(videoId) && !videoId.equals(previousVideoId) + && !videoId.equals(candidate.currentEntry().videoId()); + } + + Map minimizedContext(MessageLite endpoint, Map context) throws InvalidProtocolBufferException { + var command = NavigationEndpoint.parseFrom(endpoint.toByteArray()); + if (!command.hasWatchEndpoint() + || !command.getWatchEndpoint().getVideoId().equals(candidate.currentEntry().videoId())) { + return context; + } + + Map minimized = new HashMap<>(); + if (context != null) { + minimized.putAll(context); + } + minimized.put("start_watch_minimized", true); + return minimized; + } +} diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/queue/QueueUndoActions.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/queue/QueueUndoActions.java new file mode 100644 index 0000000000..4d4a9ab8ee --- /dev/null +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/queue/QueueUndoActions.java @@ -0,0 +1,65 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/3953 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.extension.youtube.patches.queue; + +import androidx.annotation.Nullable; + +import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protobuf.MessageLite; + +import app.morphe.extension.youtube.innertube.queue.PlaylistQueueRenderer.AddToToastAction; +import app.morphe.extension.youtube.innertube.queue.PlaylistQueueRenderer.NavigationEndpoint; +import app.morphe.extension.youtube.innertube.queue.PlaylistQueueRenderer.QueueToastItem; +import app.morphe.extension.youtube.innertube.queue.PlaylistQueueRenderer.QueueToastRenderer; +import app.morphe.extension.youtube.innertube.queue.PlaylistQueueRenderer.SwipeAction; +import app.morphe.extension.youtube.innertube.queue.PlaylistQueueRenderer.SwipeButton; +import app.morphe.extension.youtube.innertube.queue.PlaylistQueueRenderer.Text; + +/** Native snackbar feedback and restoration of a cleared local queue. */ +public final class QueueUndoActions { + public interface Command { + MessageLite patch_parseQueueCommand(byte[] bytes); + } + + private QueueUndoActions() { + } + + static NavigationEndpoint feedback(String token, String message, String undoLabel) { + return NavigationEndpoint.newBuilder().setAddToToastAction(AddToToastAction.newBuilder() + .setItem(QueueToastItem.newBuilder().setToastRenderer(QueueToastRenderer.newBuilder() + .setResponseText(Text.newBuilder().setSimpleText(message)) + .setActionButton(SwipeAction.newBuilder().setButtonRenderer(SwipeButton.newBuilder() + .setText(Text.newBuilder().setSimpleText(undoLabel)) + .setServiceEndpoint(NavigationEndpoint.newBuilder().setQueueUndoToken(token))))))) + .build(); + } + + /** A snackbar capability owns its action independently of the enclosing row's context. */ + static boolean dispatchUndo(MessageLite endpoint, String expectedToken, Runnable undo) + throws InvalidProtocolBufferException { + if (endpoint == null) { + return false; + } + var command = NavigationEndpoint.parseFrom(endpoint.toByteArray()); + if (!command.hasQueueUndoToken()) { + return false; + } + if (command.getQueueUndoToken().equals(expectedToken)) { + undo.run(); + } + + // Expired/replaced local capabilities must not reach native service dispatch. + return true; + } + + /** A menu Clear leaves playback alive; never overwrite a new queue or watch session. */ + @Nullable + static PlaylistQueue restore(PlaylistQueue before, PlaylistQueue current, String activeVideoId) { + return current == null && before.currentEntry().videoId().equals(activeVideoId) ? before : null; + } +} diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/queue/VideoMetadata.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/queue/VideoMetadata.java new file mode 100644 index 0000000000..53c7dad660 --- /dev/null +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/queue/VideoMetadata.java @@ -0,0 +1,232 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/3953 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.extension.youtube.patches.queue; + +import androidx.annotation.Nullable; + +import com.google.protobuf.InvalidProtocolBufferException; + +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.List; + +import app.morphe.extension.youtube.innertube.queue.QueueCardRenderer; + +/** Server-formatted display text for a native playlist row. */ +record VideoMetadata(String videoId, String title, String channelName, String videoInfo, String duration, + boolean fullByline, boolean panelMetadata) { + VideoMetadata(String videoId, String title, String channelName, String videoInfo, String duration, boolean fullByline) { + this(videoId, title, channelName, videoInfo, duration, fullByline, false); + } + + VideoMetadata { + // Player metadata can arrive after the queue is created. Use the same + // missing-title sentinel as queue entries so a later lookup can replace it. + if (title == null || title.isBlank()) { + title = videoId; + } + } + + private static final String WATCH_URL_PREFIX = "https://www.youtube.com/watch?v="; + private static final String METADATA_SEPARATOR = " · "; + + @Nullable + static VideoMetadata fromElement(byte[] bytes) throws InvalidProtocolBufferException { + var model = QueueCardRenderer.Element.parseFrom(bytes).getType().getComponentType().getModel(); + if (!model.hasVideoWithContextModel()) { + return null; + } + var card = model.getVideoWithContextModel().getVideoWithContextData(); + var video = card.getVideoData(); + String url = video.getDragAndDropUrl(); + if (!url.startsWith(WATCH_URL_PREFIX)) { + return null; + } + + // A mix/playlist card can link its first video but show the list's title. + // Indexed entries identify an individual video; unindexed list links do not. + if (url.contains("&list=") && !url.contains("&index=")) { + return null; + } + String videoId = url.substring(WATCH_URL_PREFIX.length()).split("&", 2)[0]; + if (!PlaylistQueue.isVideoId(videoId)) { + return null; + } + + var metadata = video.getLockupMetadata().getLockupMetadataViewModel(); + String title = metadata.getTitle().getContent(); + if (title.isEmpty()) { + return null; + } + List rows = new ArrayList<>(); + for (var row : metadata.getMetadata().getContentMetadataViewModel().getMetadataRowsList()) { + List parts = new ArrayList<>(); + for (var part : row.getMetadataPartsList()) { + String content = part.getText().getContent(); + if (!content.isEmpty()) { + parts.add(content); + } + } + + if (!parts.isEmpty()) { + rows.add(String.join(METADATA_SEPARATOR, parts)); + } + } + + VideoMetadata result = new VideoMetadata(videoId, title, "", String.join("\n", rows), + video.getThumbnail().getTimestampText(), false); + return result.withChannelLabel(video.getAvatar().getAccessibilityText(), card.getAccessibilityText()); + } + + /** Native search cards provide the full byline, including collaborating channels. */ + @Nullable + static VideoMetadata fromSearch(JSONObject response, String videoId) { + JSONObject contents = response.optJSONObject("contents"); + JSONObject sections = contents == null ? null : contents.optJSONObject("sectionListRenderer"); + var sectionItems = sections == null ? null : sections.optJSONArray("contents"); + if (sectionItems == null) { + return null; + } + for (int sectionIndex = 0; sectionIndex < sectionItems.length(); sectionIndex++) { + JSONObject section = sectionItems.optJSONObject(sectionIndex); + JSONObject itemSection = section == null ? null : section.optJSONObject("itemSectionRenderer"); + var items = itemSection == null ? null : itemSection.optJSONArray("contents"); + if (items == null) { + continue; + } + for (int index = 0; index < items.length(); index++) { + JSONObject item = items.optJSONObject(index); + JSONObject video = item == null ? null : item.optJSONObject("compactVideoRenderer"); + if (video == null || !videoId.equals(video.optString("videoId"))) { + continue; + } + String title = text(video.optJSONObject("title")); + String channel = text(video.optJSONObject("longBylineText")); + boolean fullByline = !channel.isEmpty(); + if (channel.isEmpty()) { + channel = text(video.optJSONObject("shortBylineText")); + } + if (title.isEmpty() || channel.isEmpty()) { + continue; + } + List info = new ArrayList<>(); + String views = text(video.optJSONObject("shortViewCountText")); + String age = text(video.optJSONObject("publishedTimeText")); + if (!views.isEmpty()) { + info.add(views); + } + if (!age.isEmpty()) { + info.add(age); + } + return new VideoMetadata(videoId, title, channel, String.join(METADATA_SEPARATOR, info), + text(video.optJSONObject("lengthText")), fullByline); + } + } + + return null; + } + + static String text(JSONObject text) { + if (text == null) { + return ""; + } + String simple = text.optString("simpleText", ""); + if (!simple.isEmpty()) { + return simple; + } + var runs = text.optJSONArray("runs"); + StringBuilder result = new StringBuilder(); + if (runs != null) { + for (int index = 0; index < runs.length(); index++) { + JSONObject run = runs.optJSONObject(index); + if (run != null) { + result.append(run.optString("text", "")); + } + } + } + + return result.toString(); + } + + boolean isComplete() { + // Native Mix rows can intentionally omit view counts. Their server-supplied + // display data is sufficient; adding a Mix must not launch a search per row. + // Other sources need the native long byline to confirm all collaborators. + return !title.isEmpty() && !title.equals(videoId) && !channelName.isEmpty() + && (panelMetadata || fullByline && !videoInfo.isEmpty()); + } + + VideoMetadata withCapture(VideoMetadata captured) { + if (!fullByline) { + return captured.withChannelName(channelName); + } + + // Later feed/player captures must not downgrade an authoritative byline. + return new VideoMetadata(videoId, captured.title().equals(videoId) ? title : captured.title(), + channelName, videoInfo, captured.duration().isEmpty() ? duration : captured.duration(), true); + } + + VideoMetadata withLookup(VideoMetadata fetched) { + if (fullByline && !fetched.fullByline()) { + return this; + } + + // Preserve captured titles and their title-patch processing, but prefer + // the lookup's native long byline over an uploader-only fallback. + return new VideoMetadata(videoId, title.equals(videoId) ? fetched.title() : title, + fetched.channelName(), fetched.videoInfo(), + fetched.duration().isEmpty() ? duration : fetched.duration(), fetched.fullByline()); + } + + private VideoMetadata withChannelLabel(String avatarLabel, String description) { + if (avatarLabel.isEmpty() && description.isEmpty()) { + return this; + } + + // The avatar label names this card's channel. Confirm the longest metadata + // prefix at its end, or a complete segment of the card's accessibility text. + // Neither path parses localized words such as "Go to channel" or "views". + // Work backwards so separators inside a channel name remain part of the name. + String name = ""; + for (String separator : List.of(METADATA_SEPARATOR, "\n")) { + int boundary = videoInfo.lastIndexOf(separator); + while (boundary > 0) { + String candidate = videoInfo.substring(0, boundary); + if (candidate.length() > name.length() && (avatarLabel.endsWith(candidate) + || description.contains(" - " + candidate + " - "))) { + name = candidate; + } + + boundary = videoInfo.lastIndexOf(separator, boundary - 1); + } + } + + return withChannelName(name); + } + + VideoMetadata withChannelName(String name) { + if (fullByline || name.isEmpty() || name.equals(channelName)) { + return this; + } + if (videoInfo.isEmpty() || videoInfo.equals(name)) { + return new VideoMetadata(videoId, title, name, "", duration, false, panelMetadata); + } + + // Cards may combine the channel, views and age into one text. Split only an + // exact known channel prefix; channel names themselves can contain separators. + for (String separator : List.of(METADATA_SEPARATOR, "\n")) { + String prefix = name + separator; + if (videoInfo.startsWith(prefix)) { + return new VideoMetadata(videoId, title, name, videoInfo.substring(prefix.length()), duration, false, panelMetadata); + } + } + + return this; + } +} diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/FeedVideoCardsPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/FeedVideoCardsPatch.java index dd536df89c..e1e3846663 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/FeedVideoCardsPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/FeedVideoCardsPatch.java @@ -13,6 +13,7 @@ import android.view.View; import android.view.ViewGroup; import android.view.ViewParent; +import android.widget.TextView; import androidx.annotation.GuardedBy; import androidx.annotation.Nullable; @@ -364,10 +365,75 @@ private static int findThumbnailBottom(ViewGroup group, int left, int top, * @return If the text is a duration, such as 4:05, or a badge such as the live badge. */ public static boolean isVideoBadgeText(String text) { - return (text.length() <= MAX_DURATION_TEXT_LENGTH && DURATION_PATTERN.matcher(text).find()) + return isDurationText(text) || BADGE_PATTERN.matcher(text).matches(); } + private static boolean isDurationText(String text) { + return text.length() <= MAX_DURATION_TEXT_LENGTH && DURATION_PATTERN.matcher(text).find(); + } + + /** + * Classify the actual tapped card without the optional text-to-video cache. + * Music-video cards have a duration even when their watch command starts a Mix. + * An intentional Mix/playlist card has no duration. Unknown layouts are left alone. + * Must be called on the main thread while the tap's source view is still mounted. + */ + public static boolean isIndividualVideoTap(View sender) { + ComponentHost root = findTappedRootHost(sender); + return root != null && isVideoShaped(root) && !hasHorizontalList(root) && !isInDialog(root) + && hasDuration(root); + } + + /** + * Classify a mounted card whose command already identifies a Mix. Unlike an + * individual music video, a Mix card has no duration. Do not impose the full + * width feed geometry: Mix cards also appear in smaller library grids/shelves. + */ + public static boolean isPlaylistTap(View sender) { + ComponentHost root = findTappedRootHost(sender); + return root != null && root.isAttachedToWindow() && !isInDialog(root) && !hasDuration(root); + } + + /** + * @return The root host of the card that contains the tapped view. + */ + @Nullable + private static ComponentHost findTappedRootHost(View sender) { + ViewParent parent = sender instanceof ComponentHost host ? host : sender.getParent(); + while (parent instanceof View view && !(parent instanceof ComponentHost)) { + parent = view.getParent(); + } + return parent instanceof ComponentHost host ? findRootHost(host) : null; + } + + private static boolean hasDuration(View view) { + if (view instanceof ComponentHost host) { + TextContent content = host.getTextContent(); + if (content != null) { + for (CharSequence text : content.getTextItems()) { + if (text != null && isDurationText(text.toString().trim())) { + return true; + } + } + } + } else if (view instanceof TextView textView) { + CharSequence text = textView.getText(); + if (text != null && isDurationText(text.toString().trim())) { + return true; + } + } + if (view instanceof ViewGroup group) { + for (int i = 0, count = group.getChildCount(); i < count; i++) { + View child = group.getChildAt(i); + if (child.getVisibility() == View.VISIBLE && hasDuration(child)) { + return true; + } + } + } + return false; + } + /** * Videos show their duration, or a badge such as the live badge, on the thumbnail. * Mixes, playlists and Shorts do not. diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/FlyoutUtils.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/FlyoutUtils.java index 08f101e176..dec47040f8 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/FlyoutUtils.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/FlyoutUtils.java @@ -757,6 +757,7 @@ public static void setCurrentButtonInfo(@Nullable Enum buttonEnum, @Nullable currentButtonIndex++; visibleFlyoutButtons.add(new Pair<>(currentButtonName, currentButtonIndex)); + //noinspection SizeReplaceableByIsEmpty if (buttonText != null && buttonText.length() > 0) { visibleFlyoutButtonTexts.putIfAbsent(currentButtonName, buttonText.toString()); } @@ -948,7 +949,10 @@ public static void extractFlyoutIdFromObject(@Nullable Object bufferObject) { isMyTabHistoryFlyout = false; String videoId = videoIdInterface.patch_getVideoId(); if (videoId != null) { + // An empty typed ID must clear the previous menu's video as well. flyoutVideoId = videoId; + flyoutPlaylistId = ""; + isShortFlyout = false; } return; } diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/requests/ChannelIdRequest.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/requests/ChannelIdRequest.java index d530c05c49..e8f1abae07 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/requests/ChannelIdRequest.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/requests/ChannelIdRequest.java @@ -32,6 +32,7 @@ public class ChannelIdRequest { // Must be less than 5 seconds to ensure Android "App not responding" dialog does not show. private static final int MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 3 * 1000; private final Future> future; + private volatile String videoTitle = ""; private static final Map cache = Collections.synchronizedMap( Utils.createSizeRestrictedMap(50)); @@ -70,6 +71,12 @@ public static ChannelIdRequest fetchRequestIfNeeded(String videoId) { ); } + /** Reuses an existing response without starting a request or waiting for it. */ + public static String getVideoTitleIfAvailable(String videoId) { + ChannelIdRequest request = cache.get(videoId); + return request == null ? "" : request.videoTitle; + } + private static void handleConnectionError(String toastMessage, @Nullable Exception ex) { Logger.printInfo(() -> toastMessage, ex); } @@ -117,9 +124,13 @@ private static JSONObject sendRequest(String videoId) { } @Nullable - private static Pair fetch(String videoId) { + private Pair fetch(String videoId) { JSONObject json = sendRequest(videoId); if (json != null) { + JSONObject videoDetails = json.optJSONObject("videoDetails"); + if (videoDetails != null && videoId.equals(videoDetails.optString("videoId"))) { + videoTitle = videoDetails.optString("title"); + } return parseResponse(json); } return null; diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/requests/ChannelIdRoutes.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/requests/ChannelIdRoutes.java index 7481d96064..5e9a77bbb2 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/requests/ChannelIdRoutes.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/requests/ChannelIdRoutes.java @@ -63,6 +63,14 @@ private ChannelIdRoutes() { } public static byte[] createBody(String videoId) { + return createBody("videoId", videoId); + } + + public static byte[] createSearchBody(String query) { + return createBody("query", query); + } + + private static byte[] createBody(String parameter, String value) { try { JSONObject context = getJsonObject(); @@ -71,7 +79,7 @@ public static byte[] createBody(String videoId) { innerTubeBody.put("contentCheckOk", true); innerTubeBody.put("racyCheckOk", true); - innerTubeBody.put("videoId", videoId); + innerTubeBody.put(parameter, value); return innerTubeBody.toString().getBytes(StandardCharsets.UTF_8); } catch (JSONException ex) { diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/Settings.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/Settings.java index 8ac3c9305a..d6e5de4abc 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/Settings.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/Settings.java @@ -30,6 +30,9 @@ import app.morphe.extension.shared.settings.preference.SeekBarPreference.SeekBarConfig; import app.morphe.extension.shared.sponsorblock.SegmentPlaybackController.SponsorBlockDuration; import app.morphe.extension.shared.spoof.ClientType; +import app.morphe.extension.youtube.patches.AddToQueuePatch.MorpheQueueAvailability; +import app.morphe.extension.youtube.patches.AddToQueuePatch.NotLocalQueueAvailability; +import app.morphe.extension.youtube.patches.AddToQueuePatch.QueueType; import app.morphe.extension.youtube.patches.AutoCaptionsPatch.AutoCaptionsStyle; import app.morphe.extension.youtube.patches.BackgroundPlaybackPatch.AutoPauseOnLockMode; import app.morphe.extension.youtube.patches.ChangeFormFactorPatch.FormFactor; @@ -334,12 +337,11 @@ public class Settings extends SharedYouTubeSettings { public static final BooleanSetting SAVE_TO_WATCH_LATER_FLYOUT_BUTTON = new BooleanSetting("morphe_save_to_watch_later_flyout_button", FALSE); public static final BooleanSetting KIDS_SAVE_TO_WATCH_LATER_FLYOUT_BUTTON = new BooleanSetting("morphe_save_to_watch_later_kids_flyout_button", TRUE); public static final BooleanSetting SHORTS_SAVE_TO_WATCH_LATER_FLYOUT_BUTTON = new BooleanSetting("morphe_save_to_watch_later_shorts_flyout_button", TRUE); - public static final BooleanSetting LOCAL_QUEUE = new BooleanSetting("morphe_local_queue", FALSE); + public static final EnumSetting QUEUE_TYPE = new EnumSetting<>("morphe_queue_type", QueueType.MORPHE); public static final StringSetting LOCAL_QUEUE_ITEMS = new StringSetting("morphe_local_queue_items", "", false, false); - public static final BooleanSetting QUEUE_RESTORE = new BooleanSetting("morphe_queue_restore", FALSE, parentsAll(parent(SAVE_TO_WATCH_LATER_OVERLAY_BUTTON), parentNot(LOCAL_QUEUE))); + public static final BooleanSetting QUEUE_RESTORE = new BooleanSetting("morphe_queue_restore", FALSE, parentsAll(parent(SAVE_TO_WATCH_LATER_OVERLAY_BUTTON), new NotLocalQueueAvailability())); public static final BooleanSetting SWAP_SAVE_AND_QUEUE_ACTIONS = new BooleanSetting("morphe_swap_save_and_queue_actions", TRUE, true, parent(SAVE_TO_WATCH_LATER_OVERLAY_BUTTON)); - public static final BooleanSetting QUEUE_OVERRIDE_FLYOUT_MENU = new BooleanSetting("morphe_queue_override_flyout_menu", TRUE, true, parentNot(LOCAL_QUEUE)); - public static final BooleanSetting QUEUE_ADD_FLYOUT_MENU = new BooleanSetting("morphe_queue_add_flyout_menu", TRUE, parentNot(LOCAL_QUEUE)); + public static final BooleanSetting QUEUE_ADD_FLYOUT_MENU = new BooleanSetting("morphe_queue_add_flyout_menu", TRUE, new MorpheQueueAvailability()); public static final StringSetting QUEUE_PLAYLIST_ID = new StringSetting("morphe_queue_playlist_id", ""); public static final BooleanSetting OPEN_CHANNEL_OF_LIVE_AVATAR = new BooleanSetting("morphe_open_channel_of_live_avatar", FALSE); public static final BooleanSetting CHANNEL_SEARCH = new BooleanSetting("morphe_channel_search", TRUE); @@ -870,6 +872,7 @@ public class Settings extends SharedYouTubeSettings { private static final StringSetting DEPRECATED_SB_CATEGORY_MUSIC_OFFTOPIC_COLOR = new StringSetting("sb_music_offtopic_color", "#CCFF9900", false, false); private static final StringSetting DEPRECATED_SB_CATEGORY_UNSUBMITTED = new StringSetting("sb_unsubmitted", SKIP_AUTOMATICALLY.morpheKeyValue, false, false); private static final StringSetting DEPRECATED_SB_CATEGORY_UNSUBMITTED_COLOR = new StringSetting("sb_unsubmitted_color", "#FFFFFFFF", false, false); + private static final BooleanSetting DEPRECATED_LOCAL_QUEUE = new BooleanSetting("morphe_local_queue", FALSE); private static final BooleanSetting DEPRECATED_DISABLE_TRANSLUCENT_STATUS_BAR = new BooleanSetting("morphe_disable_translucent_status_bar", FALSE); private static final BooleanSetting DEPRECATED_DISABLE_TRANSLUCENT_NAVIGATION_BAR_LIGHT = new BooleanSetting("morphe_disable_translucent_navigation_bar_light", FALSE); private static final BooleanSetting DEPRECATED_DISABLE_TRANSLUCENT_NAVIGATION_BAR_DARK = new BooleanSetting("morphe_disable_translucent_navigation_bar_dark", FALSE); @@ -980,6 +983,18 @@ public class Settings extends SharedYouTubeSettings { DEPRECATED_DISABLE_TRANSLUCENT_NAVIGATION.resetToDefault(); } + // Premium-style queue was a switch under the Morphe queue. + if (DEPRECATED_LOCAL_QUEUE.get()) { + QUEUE_TYPE.save(QueueType.LOCAL); + DEPRECATED_LOCAL_QUEUE.resetToDefault(); + } + + // The native queue requires 21.16+. + if (!VersionCheckPatch.IS_21_15_OR_GREATER && QUEUE_TYPE.get() == QueueType.NATIVE) { + Logger.printInfo(() -> "Resetting native queue type"); + QUEUE_TYPE.resetToDefault(); + } + // 20.37+ YT removed parts of the code for the legacy tablet miniplayer. // This check must remain until the Tablet type is eventually removed. if (VersionCheckPatch.IS_20_37_OR_GREATER && MINIPLAYER_TYPE.get() == MiniplayerType.TABLET) { diff --git a/extensions/youtube/src/main/proto/app/morphe/extension/youtube/innertube/next_response.proto b/extensions/youtube/src/main/proto/app/morphe/extension/youtube/innertube/next_response.proto index ebfb70abc4..a0ca7bcaa8 100644 --- a/extensions/youtube/src/main/proto/app/morphe/extension/youtube/innertube/next_response.proto +++ b/extensions/youtube/src/main/proto/app/morphe/extension/youtube/innertube/next_response.proto @@ -60,9 +60,15 @@ message TertiaryContents { } message Commands { + bytes innertubeCommand = 169495254; + SerialCommands serialCommand = 170382688; ShowSheetCommand showSheetCommand = 443434441; } +message SerialCommands { + repeated Commands commands = 1; +} + message ShowSheetCommand { PanelLoadingStrategy panelLoadingStrategy = 1; } @@ -235,6 +241,7 @@ message AddToPlaylistButtonViewModel { message SecondaryButtonViewModel { string iconName = 2; + Commands onTap = 4; optional string accessibilityId = 17; } @@ -247,6 +254,7 @@ message CompactChannelBarViewModel { } message DislikeButtonViewModel { + RatingToggleContainer toggleButtonViewModel = 1; string dislikeEntityKey = 2; } @@ -255,7 +263,9 @@ message DownloadButtonViewModel { } message LikeButtonViewModel { + RatingToggleContainer toggleButtonViewModel = 1; string likeStatusEntityKey = 3; + RatingStatusEntity likeStatusEntity = 6; } message OverflowMenuButtonViewModel { @@ -263,5 +273,32 @@ message OverflowMenuButtonViewModel { } message SegmentedLikeDislikeButtonViewModel { + RatingLikeContainer likeButtonViewModel = 1; + RatingDislikeContainer dislikeButtonViewModel = 2; string teasersOrderEntityKey = 10; -} \ No newline at end of file +} + +message RatingLikeContainer { + LikeButtonViewModel likeButtonViewModel = 484585818; +} + +message RatingDislikeContainer { + DislikeButtonViewModel dislikeButtonViewModel = 484590786; +} + +message RatingToggleContainer { + RatingToggleViewModel toggleButtonViewModel = 479644279; +} + +message RatingToggleViewModel { + RatingButtonContainer defaultButtonViewModel = 1; + RatingButtonContainer toggledButtonViewModel = 2; +} + +message RatingButtonContainer { + SecondaryButtonViewModel buttonViewModel = 461054335; +} + +message RatingStatusEntity { + int32 likeStatus = 2; +} diff --git a/extensions/youtube/src/main/proto/app/morphe/extension/youtube/innertube/playlist_queue.proto b/extensions/youtube/src/main/proto/app/morphe/extension/youtube/innertube/playlist_queue.proto new file mode 100644 index 0000000000..14ba7cd4af --- /dev/null +++ b/extensions/youtube/src/main/proto/app/morphe/extension/youtube/innertube/playlist_queue.proto @@ -0,0 +1,368 @@ +syntax = "proto2"; + +package app.morphe.extension.youtube.innertube.queue; + +option java_package = "app.morphe.extension.youtube.innertube.queue"; +option java_outer_classname = "PlaylistQueueRenderer"; +option optimize_for = LITE_RUNTIME; + +import "app/morphe/extension/youtube/innertube/icon.proto"; + +// Subset of YouTube 21.20.405's native playlist renderer and actions. +message Panel { + optional string title = 1; + repeated Item items = 2; + optional int32 selected_index = 3; + optional string playlist_id = 5; + optional int32 video_count = 6; + optional Menu menu = 26; + // Native counters add selected_index - local_selected_index to the row position. + // A local queue contains the complete playlist, without a paginated prefix. + optional int32 local_selected_index = 27; +} + +// Native queue header and row overflow menus. +message Menu { + optional MenuRenderer menu_renderer = 66439850; +} + +message MenuRenderer { + repeated MenuItem items = 1; + optional Accessibility accessibility = 10; +} + +message MenuItem { + optional MenuNavigationItem menu_navigation_item_renderer = 66441108; + optional MenuServiceItem menu_service_item_renderer = 66441155; + optional MenuConditionalServiceItem menu_conditional_service_item_renderer = 77258115; + optional MenuConditionalNavigationItem menu_conditional_navigation_item_renderer = 77262503; +} + +message MenuNavigationItem { + optional Text text = 1; + optional app.morphe.extension.youtube.innertube.Icon icon = 2; + optional NavigationEndpoint navigation_endpoint = 3; + optional app.morphe.extension.youtube.innertube.Icon secondary_icon = 11; +} + +message MenuServiceItem { + optional Text text = 1; + optional app.morphe.extension.youtube.innertube.Icon icon = 2; + optional NavigationEndpoint service_endpoint = 3; + optional app.morphe.extension.youtube.innertube.Icon secondary_icon = 9; +} + +// Native feed queue actions use this conditional variant, not MenuServiceItem. +message MenuConditionalServiceItem { + optional Text text = 1; + optional app.morphe.extension.youtube.innertube.Icon icon = 2; + optional NavigationEndpoint service_endpoint = 3; + // Native MDX_CONNECTED=1 and MDX_DISCONNECTED=2. Preserve other conditions. + optional int32 visibility = 7; + optional app.morphe.extension.youtube.innertube.Icon secondary_icon = 12; +} + +message MenuConditionalNavigationItem { + optional Text text = 1; + optional app.morphe.extension.youtube.innertube.Icon icon = 2; + optional NavigationEndpoint navigation_endpoint = 3; + optional int32 visibility = 7; +} + +message Item { + optional Video video = 51779701; +} + +message Video { + optional Text title = 1; + optional Text short_byline_text = 2; + optional Thumbnails thumbnails = 3; + optional Text length_text = 4; + optional bool selected = 6; + optional NavigationEndpoint navigation_endpoint = 7; + optional string video_id = 10; + // Native per-row overflow actions are supplied only for local queue rows. + optional Menu menu = 19; + repeated SwipeAction swipe_buttons = 22; + optional MainAppStyle main_app_style = 27; + optional Text video_info = 31; + repeated RatingAction action_buttons = 32; + // Local rows deliberately omit playlistSetVideoId (26) and edit params (37). + // Native drag presentation works without authorizing an account-playlist edit. + optional bool can_reorder = 36; +} + +message RatingAction { + optional SlimRatingButton slim_metadata_toggle_button = 124608045; +} + +message SlimRatingButton { + optional RatingTarget target = 17; + optional bool is_like = 20; + optional bool is_dislike = 21; + optional RatingToggleContainer button = 26; + optional string like_status_entity_key = 27; +} + +message RatingToggleContainer { + optional RatingToggleButton toggle_button_renderer = 79971800; +} + +message RatingToggleButton { + optional bool is_toggled = 3; + optional bool is_disabled = 4; + optional app.morphe.extension.youtube.innertube.Icon default_icon = 5; + // Keep the native endpoints byte-for-byte, including opaque request parameters. + optional bytes default_service_endpoint = 7; + optional app.morphe.extension.youtube.innertube.Icon toggled_icon = 8; + optional bytes toggled_service_endpoint = 10; + optional Accessibility accessibility_data = 19; + optional Accessibility toggled_accessibility_data = 20; +} + +message Accessibility { + optional AccessibilityLabel accessibility_data = 75730170; +} + +message AccessibilityLabel { + optional string label = 2; +} + +// Validation-only view of a captured endpoint; the original bytes are rendered. +message RatingEndpoint { + optional LikeEndpoint like_endpoint = 63158558; +} + +message LikeEndpoint { + enum Status { + LIKE = 0; + DISLIKE = 1; + INDIFFERENT = 2; + } + optional Status status = 1; + optional RatingTarget target = 2; +} + +message RatingTarget { + optional string video_id = 1; + optional string playlist_id = 2; +} + +message MainAppStyle { + enum Style { + UNSPECIFIED = 0; + PROMINENT_THUMBNAIL = 2; + } + + optional Style value = 1; +} + +message Text { + repeated TextRun runs = 1; + optional string simple_text = 4; +} + +message TextRun { + optional string text = 1; +} + +message Thumbnails { + repeated Thumbnail images = 1; +} + +message Thumbnail { + optional string url = 1; + optional int32 width = 2; + optional int32 height = 3; +} + +message NavigationEndpoint { + optional WatchEndpoint watch_endpoint = 48687757; + optional HideEnclosingAction hide_enclosing_action = 77297742; + optional CreatePlaylistDialogEndpoint create_playlist_dialog_endpoint = 85650004; + optional AddToPlaylistEndpoint add_to_playlist_endpoint = 69487224; + optional OfflineVideoEndpoint offline_video_endpoint = 73080600; + optional ElementsCommand elements_command = 204158123; + optional ShareEntityServiceEndpoint share_entity_service_endpoint = 387863284; + optional GetDownloadActionCommand get_download_action_command = 382320942; + optional PlaylistEditEndpoint playlist_edit_endpoint = 60666189; + // Extension-owned local selection, never a YouTube request field. Keeping it + // in the command preserves it across native protobuf copies/serialization. + repeated string queue_save_video_ids = 500000000; + optional string queue_save_playlist_title = 500000001; + // Local snackbar capability; never dispatched to a YouTube service. + optional string queue_undo_token = 500000002; + optional AddToToastAction add_to_toast_action = 76623563; + optional ShowConfirmationDialogEndpoint show_confirmation_dialog_endpoint = 128200871; + // Local dialog capabilities, retained by native protobuf copies. + optional string queue_mix_choice_token = 500000003; + optional bool queue_mix_add = 500000004; +} + +// Native confirmation renderer consumed by YouTube's shared dialog presenter. +message ShowConfirmationDialogEndpoint { + optional ConfirmationDialog dialog = 127046477; +} + +message ConfirmationDialog { + optional ConfirmationDialogRenderer confirm_dialog_renderer = 64099105; +} + +message ConfirmationDialogRenderer { + optional Text title = 1; + optional SwipeAction confirm_button = 12; + optional SwipeAction cancel_button = 13; +} + +message PlaylistEditEndpoint { + optional string playlist_id = 1; + repeated PlaylistEditAction actions = 2; +} + +message PlaylistEditAction { + optional string set_video_id = 3; + optional string added_video_id = 2; + optional int32 action = 6; +} + +message AddToPlaylistEndpoint { + optional string video_id = 1; +} + +message OfflineVideoEndpoint { + enum Action { + INFER_AUTOMATICALLY = 11; + } + optional string video_id = 1; + optional Action action = 2; + optional NavigationEndpoint on_add = 8; +} + +// The named native sheet opens before requesting its contents. The service response +// supplies an updateActionSheetCommand for this same sheet ID. +message ElementsCommand { + optional NavigationEndpoint innertube_command = 169495254; + optional ShowActionSheetCommand show_action_sheet_command = 278463287; +} + +message ShowActionSheetCommand { + optional string sheet_id = 4; + optional ElementsCommand on_show = 5; +} + +message ShareEntityServiceEndpoint { + enum PanelType { + // Type 1 returns the legacy unified grid; type 2 updates an action sheet. + ACTION_SHEET = 2; + } + optional string serialized_share_entity = 1; + optional PanelType panel_type = 2; + optional string sheet_id = 3; +} + +message ShareEntity { + optional string video_id = 1; +} + +message GetDownloadActionCommand { + optional string video_id = 1; + optional string params = 5; +} + +// YouTube handles sign-in, name/privacy entry, creation and result feedback. +// Supplying IDs (rather than a playlist ID) saves the local order as a new playlist. +message CreatePlaylistDialogEndpoint { + repeated string video_ids = 1; + optional string source_playlist_id = 2; + optional string video_selection_entity_key = 8; +} + +message WatchEndpoint { + optional string video_id = 1; + optional string playlist_id = 2; + optional uint32 index = 3; + optional string playlist_set_video_id = 13; +} + +message HideEnclosingAction { + optional bool hack = 1; +} + +message SwipeAction { + optional SwipeButton button_renderer = 65153809; +} + +// Native Mix swipe buttons use BLACK_FILLED, DEFAULT, and VISIBILITY_OFF. +message SwipeButton { + enum Style { + BLACK_FILLED = 35; + } + enum Size { + DEFAULT = 1; + } + optional Style style = 1; + optional Size size = 2; + optional bool is_disabled = 3; + optional Text text = 5; + optional NavigationEndpoint service_endpoint = 7; + optional app.morphe.extension.youtube.innertube.Icon icon = 8; + optional NavigationEndpoint navigation_endpoint = 9; + optional Accessibility accessibility_data = 16; + optional NavigationEndpoint command = 19; +} + +// playlist/get_add_to_playlist response subsets, verified against 21.20.405. +// Preserve all unknown renderer/command fields when rebinding the owned selection. +message QueueSaveResponse { + repeated QueueSaveResponseItem contents = 2; +} +message QueueSaveResponseItem { + optional QueueSavePickerRenderer add_to_playlist_renderer = 88978004; +} +message QueueSavePickerRenderer { + repeated QueueSaveDestinationItem playlists = 1; + repeated QueueSaveHeaderItem headers = 2; +} +message QueueSaveDestinationItem { + optional QueueSaveDestination playlist_add_to_option_renderer = 74079946; +} +message QueueSaveDestination { + optional Text title = 2; + // Native UNKNOWN=0 is an add-and-close destination, with no membership checkbox. + optional int32 selection_state = 5; + optional NavigationEndpoint add_endpoint = 7; + optional NavigationEndpoint remove_endpoint = 8; +} +message QueueSaveHeaderItem { + optional QueueSaveHeader playlist_add_to_header_renderer = 82258301; +} +// Native menu title header: field 2 is the button renderer; field 3 is text. +message QueueSaveHeader { + optional SwipeAction button = 2; +} + +// Native browse/edit_playlist success response and exact added-entry results. +message QueueSaveEditResponse { + optional int32 status = 2; + repeated NavigationEndpoint actions = 7; + repeated QueueSaveEditResult results = 12; +} +message QueueSaveEditResult { + optional QueueSaveAddedEntry added_entry = 138418859; +} +message QueueSaveAddedEntry { + optional string video_id = 1; + optional string set_video_id = 2; +} +message AddToToastAction { + optional QueueToastItem item = 1; +} +message QueueToastItem { + optional QueueToastRenderer toast_renderer = 96140188; +} +message QueueToastRenderer { + optional Text response_text = 1; + optional SwipeAction action_button = 2; + // Local presentation marker; native protobuf parsing preserves it as unknown. + optional bool queue_save_confirmation = 500000000; +} diff --git a/extensions/youtube/src/main/proto/app/morphe/extension/youtube/innertube/queue_card.proto b/extensions/youtube/src/main/proto/app/morphe/extension/youtube/innertube/queue_card.proto new file mode 100644 index 0000000000..f3f9155cb9 --- /dev/null +++ b/extensions/youtube/src/main/proto/app/morphe/extension/youtube/innertube/queue_card.proto @@ -0,0 +1,77 @@ +syntax = "proto2"; + +package app.morphe.extension.youtube.innertube.queue; + +option java_package = "app.morphe.extension.youtube.innertube.queue"; +option java_outer_classname = "QueueCardRenderer"; +option optimize_for = LITE_RUNTIME; + +// Read-only subset of the existing video card model in YouTube 21.20.405. +message Element { + optional ElementType type = 1; +} + +message ElementType { + optional ComponentType component_type = 168777401; +} + +message ComponentType { + optional CardModel model = 5; +} + +message CardModel { + optional VideoWithContextModel video_with_context_model = 232954548; +} + +message VideoWithContextModel { + optional VideoWithContextData video_with_context_data = 18; +} + +message VideoWithContextData { + optional VideoData video_data = 1; + optional string accessibility_text = 2; +} + +message VideoData { + optional CardThumbnail thumbnail = 1; + optional Avatar avatar = 3; + optional string drag_and_drop_url = 6; + optional LockupMetadata lockup_metadata = 21; +} + +message CardThumbnail { + optional string timestamp_text = 2; +} + +message Avatar { + optional string accessibility_text = 3; +} + +message LockupMetadata { + optional LockupMetadataViewModel lockup_metadata_view_model = 8178; +} + +message LockupMetadataViewModel { + optional AttributedText title = 1; + optional ContentMetadata metadata = 3; +} + +message AttributedText { + optional string content = 1; +} + +message ContentMetadata { + optional ContentMetadataViewModel content_metadata_view_model = 514549827; +} + +message ContentMetadataViewModel { + repeated MetadataRow metadata_rows = 1; +} + +message MetadataRow { + repeated MetadataPart metadata_parts = 1; +} + +message MetadataPart { + optional AttributedText text = 2; +} diff --git a/extensions/youtube/src/main/proto/app/morphe/extension/youtube/innertube/queue_fullscreen.proto b/extensions/youtube/src/main/proto/app/morphe/extension/youtube/innertube/queue_fullscreen.proto new file mode 100644 index 0000000000..9e2b42f156 --- /dev/null +++ b/extensions/youtube/src/main/proto/app/morphe/extension/youtube/innertube/queue_fullscreen.proto @@ -0,0 +1,56 @@ +syntax = "proto2"; + +package app.morphe.extension.youtube.innertube.queue.fullscreen; + +option java_package = "app.morphe.extension.youtube.innertube.queue.fullscreen"; +option java_outer_classname = "QueueFullscreenRenderer"; +option optimize_for = LITE_RUNTIME; + +// Verified quick_actions.eml binding in YouTube 21.20.405. Unknown fields retain +// YouTube's template, other actions, theme, logging and experiment configuration. +message Renderer { + optional Element new_element = 172660663; +} + +message Element { + optional ElementType type = 1; +} + +message ElementType { + optional Component component = 168777401; +} + +message Component { + optional Model model = 5; +} + +message Model { + optional QuickActions quick_actions = 386137609; +} + +message QuickActions { + optional PlaylistButton playlist_button = 17; +} + +message PlaylistButton { + optional Button button = 1; + optional int32 style = 2; +} + +message Button { + optional string icon = 2; + optional Command on_tap = 4; + optional string accessibility_label = 5; +} + +message Command { + optional Endpoint innertube_command = 169495254; +} + +message Endpoint { + optional ShowPanel show_engagement_panel = 138681778; +} + +message ShowPanel { + optional string panel_id = 1; +} diff --git a/extensions/youtube/src/test/java/app/morphe/extension/youtube/patches/queue/MixPlaylistRequestTest.java b/extensions/youtube/src/test/java/app/morphe/extension/youtube/patches/queue/MixPlaylistRequestTest.java new file mode 100644 index 0000000000..f83324be1d --- /dev/null +++ b/extensions/youtube/src/test/java/app/morphe/extension/youtube/patches/queue/MixPlaylistRequestTest.java @@ -0,0 +1,86 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/3953 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.extension.youtube.patches.queue; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +import org.json.JSONArray; +import org.json.JSONObject; +import org.junit.Test; + +import java.util.List; + +import app.morphe.extension.youtube.innertube.queue.PlaylistQueueRenderer.NavigationEndpoint; +import app.morphe.extension.youtube.innertube.queue.PlaylistQueueRenderer.WatchEndpoint; + +public class MixPlaylistRequestTest { + private static JSONObject response(JSONArray rows) throws Exception { + return new JSONObject().put("contents", new JSONObject().put("singleColumnWatchNextResults", + new JSONObject().put("playlist", new JSONObject().put("playlist", new JSONObject() + .put("playlistId", "RDmix").put("contents", rows))))); + } + + private static JSONObject row(String id) throws Exception { + return new JSONObject().put("playlistPanelVideoRenderer", new JSONObject().put("videoId", id) + .put("title", new JSONObject().put("simpleText", "Title " + id)) + .put("shortBylineText", new JSONObject().put("runs", new JSONArray() + .put(new JSONObject().put("text", "Channel"))))); + } + + @Test public void batchPreservesOrderDuplicatesAndThePlayingOccurrence() throws Exception { + for (int size : new int[] {50, 100}) { + var rows = new JSONArray(); + for (int index = 0; index < size; index++) rows.put(row(index % 2 == 0 ? "abcdefghijk" : "ZYXWVUTSRQP")); + var videos = MixPlaylistRequest.parse(response(rows), "RDmix"); + assertNotNull(videos); + assertEquals(size, videos.size()); + var before = new PlaylistQueue("abcdefghijk", "Playing").playLast("0123456789_") + .select(1).withRepeatMode(PlaylistQueue.REPEAT_ONE); + var after = before.append(videos.stream().map(video -> new PlaylistQueue.Entry(video.videoId(), video.title())).toList()); + assertEquals(size + before.entries().size(), after.entries().size()); + assertEquals(List.of("abcdefghijk", "ZYXWVUTSRQP"), after.entries().subList(2, 4).stream() + .map(PlaylistQueue.Entry::videoId).toList()); + assertSame(before.currentEntry(), after.currentEntry()); + assertEquals(before.repeatMode(), after.repeatMode()); + assertEquals(2, before.entries().size()); + assertEquals("Channel", videos.get(0).channelName()); + assertTrue(videos.stream().allMatch(VideoMetadata::isComplete)); + } + } + + @Test public void wrongListOrMalformedBatchNeverReturnsAPartialImport() throws Exception { + var rows = new JSONArray().put(row("abcdefghijk")); + assertNull(MixPlaylistRequest.parse(response(rows), "RDdifferent")); + assertNull(MixPlaylistRequest.parse(response(rows.put(row("invalid"))), "RDmix")); + assertNull(MixPlaylistRequest.parse(response(new JSONArray()), "RDmix")); + assertNull(MixPlaylistRequest.parse(new JSONObject(), "RDmix")); + } + + @Test public void dialogRetainsOpaqueWatchParametersOnBothChoices() throws Exception { + var output = new java.io.ByteArrayOutputStream(); + var fields = com.google.protobuf.CodedOutputStream.newInstance(output); + fields.writeString(6, "opaque-player-params"); fields.flush(); + var original = NavigationEndpoint.newBuilder().setWatchEndpoint(WatchEndpoint.newBuilder() + .setVideoId("abcdefghijk").setPlaylistId("RDmix").mergeFrom(output.toByteArray())).build(); + var dialog = QueueMixActions.dialog(original, "capability", "Choice", "Watch", "Add") + .getShowConfirmationDialogEndpoint().getDialog().getConfirmDialogRenderer(); + var watch = dialog.getCancelButton().getButtonRenderer().getServiceEndpoint(); + var add = dialog.getConfirmButton().getButtonRenderer().getServiceEndpoint(); + for (var choice : List.of(watch, add)) { + assertEquals("capability", choice.getQueueMixChoiceToken()); + assertEquals(original, choice.toBuilder().clearQueueMixChoiceToken().clearQueueMixAdd().build()); + } + assertFalse(watch.getQueueMixAdd()); + assertTrue(add.getQueueMixAdd()); + } +} diff --git a/extensions/youtube/src/test/java/app/morphe/extension/youtube/patches/queue/PlaylistQueueTest.java b/extensions/youtube/src/test/java/app/morphe/extension/youtube/patches/queue/PlaylistQueueTest.java new file mode 100644 index 0000000000..672b99906c --- /dev/null +++ b/extensions/youtube/src/test/java/app/morphe/extension/youtube/patches/queue/PlaylistQueueTest.java @@ -0,0 +1,157 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/3953 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.extension.youtube.patches.queue; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThrows; + +import org.junit.Test; + +import java.util.List; + +public class PlaylistQueueTest { + private static final String A = "abcdefghijk"; + private static final String B = "lmnopqrstuv"; + private static final String C = "0123456789_"; + private static final String D = "ZYXWVUTSRQP"; + + private static List videos(PlaylistQueue queue) { + return queue.entries().stream().map(PlaylistQueue.Entry::videoId).toList(); + } + + private static PlaylistQueue populated() { + return new PlaylistQueue(A, "Playing").playNext(B).playNext(C); + } + + @Test public void playNextInsertsAfterPlayingAndPreservesEarlierSnapshots() { + var initial = new PlaylistQueue(A, "Playing"); + var first = initial.playNext(B); + var second = first.playNext(C); + assertEquals(List.of(A), videos(initial)); + assertEquals(List.of(A, B), videos(first)); + assertEquals(List.of(A, C, B), videos(second)); + } + + @Test public void insertionFollowsSelectedRowWithoutChangingOrder() { + var before = populated(); + var selected = before.select(2); + var updated = selected.playNext(D); + assertEquals(List.of(A, C, B), videos(selected)); + assertEquals(List.of(A, C, B, D), videos(updated)); + assertEquals(0, before.selectedIndex()); + assertEquals(2, updated.selectedIndex()); + assertThrows(IndexOutOfBoundsException.class, () -> before.select(-1)); + assertThrows(IndexOutOfBoundsException.class, () -> before.select(3)); + } + + @Test public void removalProtectsPlayingRowAndAdjustsSelection() { + var selected = populated().select(1); + assertEquals(videos(selected), videos(selected.remove(1))); + var earlierRemoved = selected.remove(0); + assertEquals(List.of(C, B), videos(earlierRemoved)); + assertEquals(0, earlierRemoved.selectedIndex()); + assertEquals(List.of(C), videos(earlierRemoved.remove(1))); + assertEquals(List.of(A, C, B), videos(selected)); + } + + @Test public void movesAcrossPlayingRowPreserveItsIdentity() { + var before = populated().select(1); + int[][] moves = {{0, 2}, {2, 0}, {1, 2}, {1, 0}, {1, 1}}; + var expected = List.of(List.of(C, B, A), List.of(B, A, C), List.of(A, B, C), + List.of(C, A, B), List.of(A, C, B)); + for (int i = 0; i < moves.length; i++) { + var moved = before.move(moves[i][0], moves[i][1]); + assertSame(before.currentEntry(), moved.currentEntry()); + assertEquals(expected.get(i), videos(moved)); + assertEquals(expected.get(i).indexOf(C), moved.selectedIndex()); + assertEquals(List.of(A, C, B), videos(before)); + } + var duplicate = new PlaylistQueue(A, "Playing").playNext(A); + assertEquals(1, duplicate.move(0, 1).selectedIndex()); + assertThrows(IndexOutOfBoundsException.class, () -> before.move(-1, 0)); + assertThrows(IndexOutOfBoundsException.class, () -> before.move(0, 3)); + } + + @Test public void outsideVideoIsInsertedAndSelectedAfterCurrentRow() { + var before = new PlaylistQueue(A, "Playing").playNext(B).withRepeatMode(PlaylistQueue.REPEAT_ALL); + var opened = before.playNextAndSelect(C); + assertEquals(List.of(A, C, B), videos(opened)); + assertEquals(1, opened.selectedIndex()); + assertEquals(PlaylistQueue.REPEAT_ALL, opened.repeatMode()); + assertEquals(List.of(A, B), videos(before)); + assertEquals(0, before.selectedIndex()); + var next = opened.playNext(D); + assertEquals(List.of(A, C, D, B), videos(next)); + var another = opened.playNextAndSelect(D); + assertEquals(videos(next), videos(another)); + assertEquals(2, another.selectedIndex()); + assertThrows(IllegalArgumentException.class, () -> opened.playNextAndSelect("invalid")); + assertEquals(3, opened.entries().size()); + } + + @Test public void playLastAppendsDuplicatesWithoutChangingSelectionOrRepeat() { + var before = populated().withRepeatMode(PlaylistQueue.REPEAT_ALL); + for (int index = 0; index < 3; index++) { + var selected = before.select(index); + var updated = selected.playLast(D); + assertEquals(List.of(A, C, B, D), videos(updated)); + assertEquals(index, updated.selectedIndex()); + assertEquals(PlaylistQueue.REPEAT_ALL, updated.repeatMode()); + assertEquals(List.of(A, C, B), videos(selected)); + assertEquals(List.of(A, C, B, D, A), videos(updated.playLast(A))); + assertThrows(IllegalArgumentException.class, () -> updated.playLast("invalid")); + assertEquals(4, updated.entries().size()); + } + } + + @Test public void invalidInsertionLeavesQueueUnchanged() { + var queue = new PlaylistQueue(A, "Playing"); + assertThrows(IllegalArgumentException.class, () -> queue.playNext("invalid")); + assertEquals(List.of(A), videos(queue)); + } + + @Test public void shufflePreservesPlayingOccurrenceRepeatAndEarlierSnapshot() { + var before = populated().playLast(A).select(3).withRepeatMode(PlaylistQueue.REPEAT_ALL); + var shuffled = before.shuffle(); + assertEquals(0, shuffled.selectedIndex()); + assertSame(before.currentEntry(), shuffled.currentEntry()); + assertEquals(PlaylistQueue.REPEAT_ALL, shuffled.repeatMode()); + assertEquals(before.entries().size(), shuffled.entries().size()); + // Compare occurrence identity, including equal-valued duplicate videos. + for (var entry : before.entries()) { + assertEquals(1, shuffled.entries().stream().filter(candidate -> candidate == entry).count()); + } + assertEquals(List.of(A, C, B, A), videos(before)); + assertEquals(3, before.selectedIndex()); + assertNotSame(before, shuffled); + var again = shuffled.shuffle(); + assertSame(before.currentEntry(), again.currentEntry()); + assertEquals(0, again.selectedIndex()); + assertEquals(shuffled.entries().size(), again.entries().size()); + } + + @Test public void shuffledOrderIsUsedBySubsequentQueueEdits() { + var shuffled = populated().shuffle(); + var next = shuffled.playNext(D); + assertSame(shuffled.currentEntry(), next.currentEntry()); + assertEquals(D, next.entries().get(1).videoId()); + assertEquals(shuffled.entries().subList(1, 3), next.entries().subList(2, 4)); + var last = shuffled.playLast(D); + assertEquals(shuffled.entries(), last.entries().subList(0, 3)); + assertEquals(D, last.entries().get(3).videoId()); + var selected = shuffled.select(1); + assertSame(shuffled.entries().get(1), selected.currentEntry()); + assertEquals(shuffled.entries(), selected.entries()); + assertSame(selected.currentEntry(), selected.move(1, 2).currentEntry()); + assertEquals(shuffled.entries().subList(0, 2), shuffled.remove(2).entries()); + var single = new PlaylistQueue(A, "Playing"); + assertSame(single, single.shuffle()); + } +} diff --git a/extensions/youtube/src/test/java/app/morphe/extension/youtube/patches/queue/QueueFlyoutActionsTest.java b/extensions/youtube/src/test/java/app/morphe/extension/youtube/patches/queue/QueueFlyoutActionsTest.java new file mode 100644 index 0000000000..d331abd4b1 --- /dev/null +++ b/extensions/youtube/src/test/java/app/morphe/extension/youtube/patches/queue/QueueFlyoutActionsTest.java @@ -0,0 +1,161 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/3953 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.extension.youtube.patches.queue; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; + +import com.google.protobuf.CodedOutputStream; + +import org.junit.Test; + +import java.io.ByteArrayOutputStream; + +import app.morphe.extension.youtube.innertube.IconOuterClass.YTIconType; +import app.morphe.extension.youtube.innertube.queue.PlaylistQueueRenderer.MenuItem; +import app.morphe.extension.youtube.innertube.queue.PlaylistQueueRenderer.MenuRenderer; + +public class QueueFlyoutActionsTest { + // Write the native conditional wire shape independently of our schema. + // Using ordinary service builders here previously missed the real feed format. + private static MenuItem conditional(int icon, int visibility) throws Exception { + var iconBytes = new ByteArrayOutputStream(); + var output = CodedOutputStream.newInstance(iconBytes); + output.writeInt32(1, icon); + output.flush(); + var renderer = new ByteArrayOutputStream(); + output = CodedOutputStream.newInstance(renderer); + output.writeByteArray(2, iconBytes.toByteArray()); + output.writeByteArray(3, new byte[] { 8, 1 }); // Opaque original endpoint. + output.writeInt32(7, visibility); + output.writeByteArray(12, iconBytes.toByteArray()); + output.writeString(90, "unknown renderer field"); + output.flush(); + var item = new ByteArrayOutputStream(); + output = CodedOutputStream.newInstance(item); + output.writeByteArray(77258115, renderer.toByteArray()); + output.flush(); + return MenuItem.parseFrom(item.toByteArray()); + } + + @Test public void realConditionalShapeAddsLastBesideLocalNextAndPreservesCastAndUnknownData() throws Exception { + var remote = conditional(251, 1); + var next = conditional(251, 2); + var share = conditional(YTIconType.SHARE.getNumber(), 2); + var menuBytes = new ByteArrayOutputStream(); + var output = CodedOutputStream.newInstance(menuBytes); + output.writeMessage(1, remote); + output.writeMessage(1, next); + output.writeMessage(1, share); + output.writeString(80, "unknown menu field"); + output.flush(); + var menu = MenuRenderer.parseFrom(menuBytes.toByteArray()); + var completed = QueueFlyoutActions.completeMenu(menu, true, "Next", "Last"); + assertEquals(4, completed.getItemsCount()); + assertArrayEquals(remote.toByteArray(), completed.getItems(0).toByteArray()); + assertArrayEquals(next.toBuilder().setMenuConditionalServiceItemRenderer( + next.getMenuConditionalServiceItemRenderer().toBuilder().clearSecondaryIcon()).build().toByteArray(), + completed.getItems(1).toByteArray()); + assertArrayEquals(share.toByteArray(), completed.getItems(3).toByteArray()); + var last = completed.getItems(2).getMenuConditionalServiceItemRenderer(); + assertEquals(YTIconType.QUEUE_PLAY_LAST, last.getIcon().getYtIconType()); + assertEquals("Last", last.getText().getSimpleText()); + assertEquals(2, last.getVisibility()); + assertFalse(last.hasServiceEndpoint()); + assertSame(completed, QueueFlyoutActions.completeMenu(completed, true, "Next", "Last")); + // Returning to no queue removes only our Last, including through serialization. + assertArrayEquals(menu.toBuilder().setItems(1, completed.getItems(1)).build().toByteArray(), QueueFlyoutActions.completeMenu( + MenuRenderer.parseFrom(completed.toByteArray()), false, "Next", "Last").toByteArray()); + } + + @Test public void suppliedLastIsGroupedAndFollowsQueueStateWithoutLosingItsEndpoint() throws Exception { + var next = conditional(251, 2); + var share = conditional(YTIconType.SHARE.getNumber(), 2); + var last = conditional(895, 7); + var menu = MenuRenderer.newBuilder().addItems(next).addItems(share).addItems(last).build(); + var completed = QueueFlyoutActions.completeMenu(menu, true, "Next", "Last"); + assertEquals(3, completed.getItemsCount()); + assertEquals(2, completed.getItems(1).getMenuConditionalServiceItemRenderer().getVisibility()); + assertEquals(last.getMenuConditionalServiceItemRenderer().getServiceEndpoint(), + completed.getItems(1).getMenuConditionalServiceItemRenderer().getServiceEndpoint()); + assertEquals(last.toBuilder().setMenuConditionalServiceItemRenderer( + last.getMenuConditionalServiceItemRenderer().toBuilder().setVisibility(2).clearSecondaryIcon()).build(), + completed.getItems(1)); + assertEquals(share, completed.getItems(2)); + assertEquals(MenuRenderer.newBuilder().addItems(completed.getItems(0)).addItems(share).build(), + QueueFlyoutActions.completeMenu(menu, false, "Next", "Last")); + } + + @Test public void castOnlyAndUnrelatedMenusStayIdentical() throws Exception { + var cast = MenuRenderer.newBuilder().addItems(conditional(251, 1)) + .addItems(conditional(895, 1)).build(); + assertSame(cast, QueueFlyoutActions.completeMenu(cast, true, "Next", "Last")); + assertSame(cast, QueueFlyoutActions.completeMenu(cast, false, "Next", "Last")); + var unrelated = MenuRenderer.newBuilder().addItems(conditional(YTIconType.SHARE.getNumber(), 2)).build(); + assertSame(unrelated, QueueFlyoutActions.completeMenu(unrelated, true, "Next", "Last")); + var next = MenuRenderer.newBuilder().addItems(conditional(251, 2)).build(); + var cleaned = QueueFlyoutActions.completeMenu(next, false, "Next", "Last"); + assertFalse(cleaned.getItems(0).getMenuConditionalServiceItemRenderer().hasSecondaryIcon()); + assertSame(cleaned, QueueFlyoutActions.completeMenu(cleaned, false, "Next", "Last")); + } + @Test public void nativeNavigationAndServiceVariantsKeepTheirCommandsAndLoseOnlyTheBadge() throws Exception { + int[][] variants = { { 66441108, 11 }, { 66441155, 9 }, { 77258115, 12 }, { 77262503, 0 } }; + for (int[] variant : variants) { + var renderer = new ByteArrayOutputStream(); + var output = CodedOutputStream.newInstance(renderer); + output.writeByteArray(2, new byte[] { 8, (byte) 251, 1 }); + output.writeByteArray(3, new byte[] { 8, 1 }); + if (variant[0] == 77258115 || variant[0] == 77262503) output.writeInt32(7, 2); + if (variant[1] != 0) output.writeByteArray(variant[1], new byte[] { 8, 1 }); + output.writeString(90, "unknown"); + output.flush(); + var bytes = new ByteArrayOutputStream(); + output = CodedOutputStream.newInstance(bytes); + output.writeByteArray(variant[0], renderer.toByteArray()); + output.flush(); + var menu = MenuRenderer.newBuilder().addItems(MenuItem.parseFrom(bytes.toByteArray())).build(); + var completed = QueueFlyoutActions.completeMenu(menu, true, "Next", "Last"); + assertEquals(2, completed.getItemsCount()); + var inactive = QueueFlyoutActions.completeMenu(completed, false, "Next", "Last"); + assertEquals(1, inactive.getItemsCount()); + assertSame(inactive, QueueFlyoutActions.completeMenu(inactive, false, "Next", "Last")); + // The only removed field is the native secondary icon. Opaque command + // and renderer fields survive even for conditional navigation items. + var expectedRenderer = new ByteArrayOutputStream(); + output = CodedOutputStream.newInstance(expectedRenderer); + output.writeByteArray(2, new byte[] { 8, (byte) 251, 1 }); + output.writeByteArray(3, new byte[] { 8, 1 }); + if (variant[0] == 77258115 || variant[0] == 77262503) output.writeInt32(7, 2); + output.writeString(90, "unknown"); + output.flush(); + var expected = new ByteArrayOutputStream(); + output = CodedOutputStream.newInstance(expected); + output.writeByteArray(variant[0], expectedRenderer.toByteArray()); + output.flush(); + assertEquals(MenuItem.parseFrom(expected.toByteArray()), inactive.getItems(0)); + } + } + + @Test public void lastOnlyMenuStillUsesNativeNextAndLastPolicy() throws Exception { + var original = MenuRenderer.newBuilder().addItems(conditional(895, 2)).build(); + var inactive = QueueFlyoutActions.completeMenu(original, false, "Next", "Last"); + assertEquals(1, inactive.getItemsCount()); + assertEquals(YTIconType.QUEUE_PLAY_NEXT, + inactive.getItems(0).getMenuConditionalServiceItemRenderer().getIcon().getYtIconType()); + assertFalse(inactive.getItems(0).getMenuConditionalServiceItemRenderer().hasServiceEndpoint()); + var active = QueueFlyoutActions.completeMenu(original, true, "Next", "Last"); + assertEquals(2, active.getItemsCount()); + assertEquals(YTIconType.QUEUE_PLAY_LAST, + active.getItems(1).getMenuConditionalServiceItemRenderer().getIcon().getYtIconType()); + assertEquals(original.getItems(0).getMenuConditionalServiceItemRenderer().getServiceEndpoint(), + active.getItems(1).getMenuConditionalServiceItemRenderer().getServiceEndpoint()); + } + +} diff --git a/extensions/youtube/src/test/java/app/morphe/extension/youtube/patches/queue/QueueRatingActionsTest.java b/extensions/youtube/src/test/java/app/morphe/extension/youtube/patches/queue/QueueRatingActionsTest.java new file mode 100644 index 0000000000..1692e54ed8 --- /dev/null +++ b/extensions/youtube/src/test/java/app/morphe/extension/youtube/patches/queue/QueueRatingActionsTest.java @@ -0,0 +1,111 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/3953 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.extension.youtube.patches.queue; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertNull; + +import com.google.protobuf.ByteString; +import com.google.protobuf.CodedOutputStream; + +import org.junit.Test; + +import java.io.ByteArrayOutputStream; + +import app.morphe.extension.youtube.innertube.NextResponseOuterClass.ActionButtons; +import app.morphe.extension.youtube.innertube.NextResponseOuterClass.Commands; +import app.morphe.extension.youtube.innertube.NextResponseOuterClass.ComponentType; +import app.morphe.extension.youtube.innertube.NextResponseOuterClass.DislikeButtonViewModel; +import app.morphe.extension.youtube.innertube.NextResponseOuterClass.LikeButtonViewModel; +import app.morphe.extension.youtube.innertube.NextResponseOuterClass.Model; +import app.morphe.extension.youtube.innertube.NextResponseOuterClass.NewElement; +import app.morphe.extension.youtube.innertube.NextResponseOuterClass.PrimaryButtonViewModel; +import app.morphe.extension.youtube.innertube.NextResponseOuterClass.RatingButtonContainer; +import app.morphe.extension.youtube.innertube.NextResponseOuterClass.RatingDislikeContainer; +import app.morphe.extension.youtube.innertube.NextResponseOuterClass.RatingLikeContainer; +import app.morphe.extension.youtube.innertube.NextResponseOuterClass.RatingStatusEntity; +import app.morphe.extension.youtube.innertube.NextResponseOuterClass.RatingToggleContainer; +import app.morphe.extension.youtube.innertube.NextResponseOuterClass.RatingToggleViewModel; +import app.morphe.extension.youtube.innertube.NextResponseOuterClass.SecondaryButtonViewModel; +import app.morphe.extension.youtube.innertube.NextResponseOuterClass.SegmentedLikeDislikeButtonViewModel; +import app.morphe.extension.youtube.innertube.NextResponseOuterClass.SerialCommands; +import app.morphe.extension.youtube.innertube.NextResponseOuterClass.Type; +import app.morphe.extension.youtube.innertube.NextResponseOuterClass.VideoActionBarData; +import app.morphe.extension.youtube.innertube.NextResponseOuterClass.VideoActionBarModel; +import app.morphe.extension.youtube.innertube.queue.PlaylistQueueRenderer; +import app.morphe.extension.youtube.innertube.queue.PlaylistQueueRenderer.LikeEndpoint.Status; + +public class QueueRatingActionsTest { + private static ByteString endpoint(String id, Status status) throws Exception { + var target = PlaylistQueueRenderer.RatingTarget.newBuilder().setVideoId(id); + var body = new ByteArrayOutputStream(); + body.write(PlaylistQueueRenderer.LikeEndpoint.newBuilder().setStatus(status).setTarget(target).build().toByteArray()); + var parameters = CodedOutputStream.newInstance(body); + parameters.writeString(6, "opaque server parameters"); + parameters.flush(); + var result = new ByteArrayOutputStream(); + var command = CodedOutputStream.newInstance(result); + command.writeBytes(63158558, ByteString.copyFrom(body.toByteArray())); + command.flush(); + return ByteString.copyFrom(result.toByteArray()); + } + private static RatingButtonContainer button(ByteString endpoint) { + return RatingButtonContainer.newBuilder().setButtonViewModel(SecondaryButtonViewModel.newBuilder() + .setOnTap(Commands.newBuilder().setSerialCommand(SerialCommands.newBuilder() + .addCommands(Commands.getDefaultInstance()) + .addCommands(Commands.newBuilder().setInnertubeCommand(endpoint))))) + .build(); + } + private static String[] parse(String dislikeId, boolean omitUndo, int initialStatus) throws Exception { + String id = "abcdefghijk"; + var likeEndpoint = endpoint(id, Status.LIKE); + var like = LikeButtonViewModel.newBuilder().setLikeStatusEntityKey("native entity key") + .setLikeStatusEntity(RatingStatusEntity.newBuilder().setLikeStatus(initialStatus)) + .setToggleButtonViewModel(RatingToggleContainer.newBuilder().setToggleButtonViewModel( + RatingToggleViewModel.newBuilder().setDefaultButtonViewModel(button(likeEndpoint)) + .setToggledButtonViewModel(button(endpoint(id, Status.INDIFFERENT))))); + var dislike = DislikeButtonViewModel.newBuilder().setDislikeEntityKey("native entity key") + .setToggleButtonViewModel(RatingToggleContainer.newBuilder().setToggleButtonViewModel( + RatingToggleViewModel.newBuilder().setDefaultButtonViewModel(button(endpoint(dislikeId, Status.DISLIKE))) + .setToggledButtonViewModel(button(omitUndo ? ByteString.EMPTY : endpoint(id, Status.INDIFFERENT))))); + var segmented = SegmentedLikeDislikeButtonViewModel.newBuilder() + .setLikeButtonViewModel(RatingLikeContainer.newBuilder().setLikeButtonViewModel(like)) + .setDislikeButtonViewModel(RatingDislikeContainer.newBuilder().setDislikeButtonViewModel(dislike)); + var element = NewElement.newBuilder().setType(Type.newBuilder().setComponentType(ComponentType.newBuilder() + .setModel(Model.newBuilder().setVideoActionBarModel(VideoActionBarModel.newBuilder() + .setVideoActionBarData(VideoActionBarData.newBuilder().addActionButtons(ActionButtons.newBuilder() + .setPrimaryButtonViewModel(PrimaryButtonViewModel.newBuilder().setSegmentedLikeDislikeButtonViewModel(segmented)))))))) + .build(); + var actions = QueueRatingActions.fromElement(NewElement.parseFrom(element.toByteArray())); + if (actions == null) return null; + var buttons = actions.buttons("Like", "Dislike"); + var nativeLike = buttons.get(0).getSlimMetadataToggleButton().getButton().getToggleButtonRenderer(); + var nativeDislike = buttons.get(1).getSlimMetadataToggleButton().getButton().getToggleButtonRenderer(); + return new String[]{actions.videoId(), actions.entityKey(), + Boolean.toString(likeEndpoint.equals(nativeLike.getDefaultServiceEndpoint())), + Boolean.toString(nativeLike.getIsToggled()), Boolean.toString(nativeDislike.getIsToggled()), + nativeLike.getAccessibilityData().getAccessibilityData().getLabel()}; + } + @Test public void ratingsPreserveOpaqueCommandsAndNativeEntityKey() throws Exception { + assertArrayEquals(new String[]{"abcdefghijk", "native entity key", "true", "false", "true", "Like"}, + parse("abcdefghijk", false, 1)); + } + + @Test public void likedStatusIsReflectedInButtons() throws Exception { + assertArrayEquals(new String[]{"abcdefghijk", "native entity key", "true", "true", "false", "Like"}, + parse("abcdefghijk", false, 0)); + } + + @Test public void mismatchedTargetsAreRejected() throws Exception { + assertNull(parse("lmnopqrstuv", false, 1)); + } + + @Test public void missingUndoCommandIsRejected() throws Exception { + assertNull(parse("abcdefghijk", true, 1)); + } +} diff --git a/extensions/youtube/src/test/java/app/morphe/extension/youtube/patches/queue/QueueSaveActionsTest.java b/extensions/youtube/src/test/java/app/morphe/extension/youtube/patches/queue/QueueSaveActionsTest.java new file mode 100644 index 0000000000..27dc604cac --- /dev/null +++ b/extensions/youtube/src/test/java/app/morphe/extension/youtube/patches/queue/QueueSaveActionsTest.java @@ -0,0 +1,190 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/3953 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.extension.youtube.patches.queue; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; + +import app.morphe.extension.youtube.innertube.queue.PlaylistQueueRenderer.AddToPlaylistEndpoint; +import app.morphe.extension.youtube.innertube.queue.PlaylistQueueRenderer.CreatePlaylistDialogEndpoint; +import app.morphe.extension.youtube.innertube.queue.PlaylistQueueRenderer.NavigationEndpoint; +import app.morphe.extension.youtube.innertube.queue.PlaylistQueueRenderer.PlaylistEditAction; +import app.morphe.extension.youtube.innertube.queue.PlaylistQueueRenderer.PlaylistEditEndpoint; +import app.morphe.extension.youtube.innertube.queue.PlaylistQueueRenderer.QueueSaveDestination; +import app.morphe.extension.youtube.innertube.queue.PlaylistQueueRenderer.QueueSaveDestinationItem; +import app.morphe.extension.youtube.innertube.queue.PlaylistQueueRenderer.QueueSaveEditResponse; +import app.morphe.extension.youtube.innertube.queue.PlaylistQueueRenderer.QueueSaveHeader; +import app.morphe.extension.youtube.innertube.queue.PlaylistQueueRenderer.QueueSaveHeaderItem; +import app.morphe.extension.youtube.innertube.queue.PlaylistQueueRenderer.QueueSavePickerRenderer; +import app.morphe.extension.youtube.innertube.queue.PlaylistQueueRenderer.QueueSaveResponse; +import app.morphe.extension.youtube.innertube.queue.PlaylistQueueRenderer.QueueSaveResponseItem; +import app.morphe.extension.youtube.innertube.queue.PlaylistQueueRenderer.RatingEndpoint; +import app.morphe.extension.youtube.innertube.queue.PlaylistQueueRenderer.SwipeAction; +import app.morphe.extension.youtube.innertube.queue.PlaylistQueueRenderer.SwipeButton; +import app.morphe.extension.youtube.innertube.queue.PlaylistQueueRenderer.Text; +import app.morphe.extension.youtube.innertube.queue.PlaylistQueueRenderer.TextRun; +import app.morphe.extension.youtube.innertube.queue.PlaylistQueueRenderer.WatchEndpoint; + +public class QueueSaveActionsTest { + private static final List IDS = List.of("abcdefghijk", "lmnopqrstuv", "ZYXWVUTSRQP"); + // Stock beoz schema: button renderer is field 2; field 3 is text. + // Write the native wire shape independently of our generated header + // builder, which previously hid an incorrect field declaration. + static QueueSaveHeader nativeHeader(SwipeAction button) throws Exception { + var bytes = new java.io.ByteArrayOutputStream(); + var output = com.google.protobuf.CodedOutputStream.newInstance(bytes); + output.writeMessage(2, button); + output.writeMessage(3, Text.newBuilder().setSimpleText("New playlist").build()); + output.flush(); + return QueueSaveHeader.parseFrom(bytes.toByteArray()); + } + static class Picker implements QueueSaveActions.Picker { + List videoIds; + Picker(List ids) { videoIds = ids; } + public List patch_getQueueSaveVideoIds() { return videoIds; } + public com.google.protobuf.MessageLite patch_parseQueueSaveResponse(byte[] bytes) { + try { return QueueSaveResponse.parseFrom(bytes); } + catch (Exception error) { throw new AssertionError(error); } + } + } + static class FeedbackCallback implements QueueSaveFeedback.Callback { + QueueSaveFeedback.Selection selection; + public QueueSaveFeedback.Selection patch_getQueueSaveSelection() { return selection; } + public void patch_setQueueSaveSelection(QueueSaveFeedback.Selection value) { selection = value; } + public com.google.protobuf.MessageLite patch_parseQueueSaveResult(byte[] bytes) { + try { return QueueSaveEditResponse.parseFrom(bytes); } + catch (Exception error) { throw new AssertionError(error); } + } + } + + private static NavigationEndpoint ordinaryAdd() { + return NavigationEndpoint.newBuilder().setPlaylistEditEndpoint( + PlaylistEditEndpoint.newBuilder().setPlaylistId("PLdestination") + .addActions(PlaylistEditAction.newBuilder().setAction(1) + .setAddedVideoId(IDS.get(0)))).build(); + } + + private static QueueSaveResponse response(SwipeButton.Builder button) throws Exception { + return QueueSaveResponse.newBuilder().addContents(QueueSaveResponseItem.newBuilder() + .setAddToPlaylistRenderer(QueueSavePickerRenderer.newBuilder().addPlaylists( + QueueSaveDestinationItem.newBuilder().setPlaylistAddToOptionRenderer( + QueueSaveDestination.newBuilder().setSelectionState(3).setAddEndpoint(ordinaryAdd()) + .setTitle(Text.newBuilder().addRuns(TextRun.newBuilder().setText("Test playlist"))))) + .addHeaders(QueueSaveHeaderItem.newBuilder().setPlaylistAddToHeaderRenderer( + nativeHeader(SwipeAction.newBuilder().setButtonRenderer(button).build()))))).build(); + } + + private static SwipeButton.Builder createButton() { + return SwipeButton.newBuilder().setNavigationEndpoint(NavigationEndpoint.newBuilder() + .setCreatePlaylistDialogEndpoint(CreatePlaylistDialogEndpoint.newBuilder() + .addVideoIds(IDS.get(0)).setVideoSelectionEntityKey("single-video-selection"))); + } + + @Test public void selectionSurvivesUnknownNativeCopyAndCallerMutations() throws Exception { + var ids = new ArrayList<>(IDS); + var open = QueueSaveActions.openPicker(ids); + assertEquals(IDS.get(0), open.getAddToPlaylistEndpoint().getVideoId()); + var copy = RatingEndpoint.parseFrom(open.toByteArray()); + var saved = QueueSaveActions.copyVideoIds(copy); + ids.clear(); + assertEquals(IDS, saved); + saved.add(IDS.get(2)); + assertEquals(IDS, QueueSaveActions.copyVideoIds(copy)); + assertEquals(List.of(IDS.get(0), IDS.get(0)), + QueueSaveActions.openPicker(List.of(IDS.get(0), IDS.get(0))).getQueueSaveVideoIdsList()); + assertThrows(IllegalArgumentException.class, () -> QueueSaveActions.openPicker(List.of())); + } + + @Test public void ordinarySavesAndResponsesPassThrough() throws Exception { + var ordinary = NavigationEndpoint.newBuilder().setAddToPlaylistEndpoint( + AddToPlaylistEndpoint.newBuilder().setVideoId(IDS.get(0))).build(); + assertNull(QueueSaveActions.copyVideoIds(ordinary)); + var response = response(createButton()); + assertSame(response, QueueSaveActions.onPickerResponse(new Picker(null), response)); + var callback = new FeedbackCallback(); + QueueSaveActions.onEditCreated(callback, ordinaryAdd()); + assertNull(callback.selection); + var result = QueueSaveEditResponse.newBuilder().setStatus(1).build(); + assertSame(result, QueueSaveActions.onEditResult(callback, result)); + } + + @Test public void existingPlaylistReceivesAllIdsAndOwnedFeedbackSelection() throws Exception { + var bound = (QueueSaveResponse) QueueSaveActions.onPickerResponse(new Picker(IDS), response(createButton())); + var destination = bound.getContents(0).getAddToPlaylistRenderer().getPlaylists(0).getPlaylistAddToOptionRenderer(); + assertEquals(0, destination.getSelectionState()); + var edit = destination.getAddEndpoint().getPlaylistEditEndpoint(); + assertEquals("PLdestination", edit.getPlaylistId()); + assertEquals(IDS, edit.getActionsList().stream().map(PlaylistEditAction::getAddedVideoId).toList()); + var callback = new FeedbackCallback(); + QueueSaveActions.onEditCreated(callback, destination.getAddEndpoint()); + assertEquals(IDS, callback.selection.videoIds()); + assertEquals("PLdestination", callback.selection.playlistId()); + assertEquals("Test playlist", callback.selection.title()); + var single = QueueSaveActions.bindResponse(response(createButton()), List.of(IDS.get(0))); + assertEquals(3, single.getContents(0).getAddToPlaylistRenderer().getPlaylists(0) + .getPlaylistAddToOptionRenderer().getSelectionState()); + } + + @Test public void newPlaylistReceivesFullSelectionWithoutSingleVideoEntityKey() throws Exception { + var bound = QueueSaveActions.bindResponse(response(createButton()), IDS); + var create = bound.getContents(0).getAddToPlaylistRenderer().getHeaders(0) + .getPlaylistAddToHeaderRenderer().getButton().getButtonRenderer() + .getNavigationEndpoint().getCreatePlaylistDialogEndpoint(); + assertEquals(IDS, create.getVideoIdsList()); + assertFalse(create.hasVideoSelectionEntityKey()); + assertEquals(bound, QueueSaveActions.bindResponse(bound, IDS)); + } + + @Test public void allNativeHeaderClickSlotsBecomeOneListCapableCreateAction() throws Exception { + var other = NavigationEndpoint.newBuilder().setWatchEndpoint( + WatchEndpoint.newBuilder().setVideoId(IDS.get(0))).build(); + var create = createButton().getNavigationEndpoint(); + for (var button : List.of( + SwipeButton.newBuilder().setServiceEndpoint(other), + SwipeButton.newBuilder().setNavigationEndpoint(other), + SwipeButton.newBuilder().setCommand(other), + SwipeButton.newBuilder().setServiceEndpoint(other).setNavigationEndpoint(create).setCommand(other))) { + button.setText(Text.newBuilder().setSimpleText("New playlist")); + var response = response(button); + var bound = (QueueSaveResponse) QueueSaveActions.onPickerResponse(new Picker(IDS), response); + assertEquals(1, bound.getContentsCount()); + var nativeButton = bound.getContents(0).getAddToPlaylistRenderer().getHeaders(0) + .getPlaylistAddToHeaderRenderer().getButton().getButtonRenderer(); + assertFalse(nativeButton.hasServiceEndpoint()); + assertFalse(nativeButton.hasCommand()); + assertEquals("New playlist", nativeButton.getText().getSimpleText()); + assertEquals(IDS, nativeButton.getNavigationEndpoint().getCreatePlaylistDialogEndpoint().getVideoIdsList()); + assertSame(response, QueueSaveActions.onPickerResponse(new Picker(null), response)); + } + } + + @Test public void bindingPreservesUnknownNativeResponseFields() throws Exception { + var wire = new java.io.ByteArrayOutputStream(); + wire.write(response(createButton()).toByteArray()); + var output = com.google.protobuf.CodedOutputStream.newInstance(wire); + output.writeInt32(127, 99); + output.flush(); + var bound = QueueSaveActions.bindResponse(QueueSaveResponse.parseFrom(wire.toByteArray()), IDS); + var input = com.google.protobuf.CodedInputStream.newInstance(bound.toByteArray()); + boolean keptUnknown = false; + for (int tag; (tag = input.readTag()) != 0;) { + if ((tag >>> 3) == 127) keptUnknown = input.readInt32() == 99; + else input.skipField(tag); + } + assertTrue("Native fields outside our schema must survive", keptUnknown); + } +} diff --git a/extensions/youtube/src/test/java/app/morphe/extension/youtube/patches/queue/QueueSaveFeedbackTest.java b/extensions/youtube/src/test/java/app/morphe/extension/youtube/patches/queue/QueueSaveFeedbackTest.java new file mode 100644 index 0000000000..c94a1b7f1e --- /dev/null +++ b/extensions/youtube/src/test/java/app/morphe/extension/youtube/patches/queue/QueueSaveFeedbackTest.java @@ -0,0 +1,85 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/3953 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.extension.youtube.patches.queue; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; + +import org.junit.Test; + +import java.util.List; + +import app.morphe.extension.youtube.innertube.queue.PlaylistQueueRenderer.QueueSaveAddedEntry; +import app.morphe.extension.youtube.innertube.queue.PlaylistQueueRenderer.QueueSaveEditResponse; +import app.morphe.extension.youtube.innertube.queue.PlaylistQueueRenderer.QueueSaveEditResult; +import app.morphe.extension.youtube.innertube.queue.PlaylistQueueRenderer.QueueToastRenderer; + +public class QueueSaveFeedbackTest { + private static final List IDS = List.of("abcdefghijk", "lmnopqrstuv", "ZYXWVUTSRQP"); + private static final QueueSaveFeedback.Selection SELECTION = + new QueueSaveFeedback.Selection("PLdestination", "Test playlist", IDS); + + private static QueueSaveEditResponse result() { + return QueueSaveEditResponse.newBuilder().setStatus(1) + .addResults(QueueSaveEditResult.newBuilder().setAddedEntry(QueueSaveAddedEntry.newBuilder() + .setVideoId(IDS.get(0)).setSetVideoId("NEW_ENTRY_A"))) + .addResults(QueueSaveEditResult.newBuilder().setAddedEntry(QueueSaveAddedEntry.newBuilder() + .setVideoId(IDS.get(1)).setSetVideoId("NEW_ENTRY_B"))).build(); + } + + private static QueueToastRenderer toast(QueueSaveEditResponse result) { + return QueueSaveFeedback.decorate(result, SELECTION, "Saved to Test playlist", "Undo") + .getActions(0).getAddToToastAction().getItem().getToastRenderer(); + } + + @Test public void undoRemovesOnlyServerReportedNewPlaylistEntries() { + var toast = toast(result()); + assertEquals("Saved to Test playlist", toast.getResponseText().getSimpleText()); + var endpoint = toast.getActionButton().getButtonRenderer().getServiceEndpoint(); + var undo = endpoint.getPlaylistEditEndpoint(); + assertEquals("PLdestination", undo.getPlaylistId()); + assertEquals(0, endpoint.getQueueSaveVideoIdsCount()); + assertEquals(2, undo.getActionsCount()); + assertEquals(2, undo.getActions(0).getAction()); + assertEquals("NEW_ENTRY_A", undo.getActions(0).getSetVideoId()); + assertEquals("NEW_ENTRY_B", undo.getActions(1).getSetVideoId()); + for (var action : undo.getActionsList()) assertFalse(action.hasAddedVideoId()); + } + + @Test public void ownedSaveConfirmationUsesFiveSecondsAfterSerialization() throws Exception { + var renderer = toast(result()); + var parsed = QueueToastRenderer.parseFrom(renderer.toByteArray()); + assertEquals(5_000, QueueSaveFeedback.confirmationDuration(parsed)); + assertEquals(2, parsed.getActionButton().getButtonRenderer().getServiceEndpoint() + .getPlaylistEditEndpoint().getActionsCount()); + } + + @Test public void ordinaryConfirmationWithTheSameTextAndUndoKeepsNativeDuration() { + var ordinary = toast(result()).toBuilder().clearQueueSaveConfirmation().build(); + assertEquals(0, QueueSaveFeedback.confirmationDuration(ordinary)); + } + + @Test public void existingFeedbackAndFailedResultsAreUnchanged() { + var decorated = QueueSaveFeedback.decorate(result(), SELECTION, "Saved", "Undo"); + assertSame(decorated, QueueSaveFeedback.decorate(decorated, SELECTION, "duplicate", "Undo")); + var failure = result().toBuilder().setStatus(0).build(); + assertSame(failure, QueueSaveFeedback.decorate(failure, SELECTION, "wrong", "Undo")); + } + + @Test public void missingEntryIdsCannotOfferDestructiveUndo() { + assertFalse(toast(result().toBuilder().clearResults().build()).hasActionButton()); + } + + @Test public void foreignVideoResultCannotOfferDestructiveUndo() { + var foreign = result().toBuilder().addResults(QueueSaveEditResult.newBuilder() + .setAddedEntry(QueueSaveAddedEntry.newBuilder().setVideoId("not-in-queue") + .setSetVideoId("OLD_ENTRY"))).build(); + assertFalse(toast(foreign).hasActionButton()); + } +} diff --git a/extensions/youtube/src/test/java/app/morphe/extension/youtube/patches/queue/QueueShortsSessionTest.java b/extensions/youtube/src/test/java/app/morphe/extension/youtube/patches/queue/QueueShortsSessionTest.java new file mode 100644 index 0000000000..d9d5f034e0 --- /dev/null +++ b/extensions/youtube/src/test/java/app/morphe/extension/youtube/patches/queue/QueueShortsSessionTest.java @@ -0,0 +1,89 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/3953 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.extension.youtube.patches.queue; + +import static org.junit.Assert.assertEquals; +import static app.morphe.extension.youtube.patches.queue.QueueShortsSession.Action.NONE; +import static app.morphe.extension.youtube.patches.queue.QueueShortsSession.Action.RELEASE; +import static app.morphe.extension.youtube.patches.queue.QueueShortsSession.Action.RESUME; +import static app.morphe.extension.youtube.patches.queue.QueueShortsSession.Action.SUSPEND; +import static app.morphe.extension.youtube.patches.queue.QueueShortsSession.Action.WAIT; + +import org.junit.Test; + +public class QueueShortsSessionTest { + private final QueueShortsSession session = new QueueShortsSession(); + private static final String A = "abcdefghijk"; + private static final String B = "lmnopqrstuv"; + + private QueueShortsSession.Action update(boolean open, boolean hidden, boolean visible, + String active, String regular, boolean playlist, boolean service) { + return session.update(open, hidden, visible, active, regular, playlist, service); + } + + @Test public void eitherShortsAttachmentOrderRetainsQueue() { + for (boolean hiddenFirst : new boolean[]{true, false}) { + session.reset(); + assertEquals(NONE, update(false, false, true, A, A, false, false)); + assertEquals(SUSPEND, update(!hiddenFirst, hiddenFirst, false, A, A, false, false)); + assertEquals(WAIT, update(true, true, false, B, A, false, false)); + assertEquals(WAIT, update(true, false, true, A, A, false, false)); + assertEquals(WAIT, update(false, false, false, B, A, false, false)); + assertEquals(WAIT, update(false, false, true, B, A, false, false)); + assertEquals(RESUME, update(false, false, true, A, A, false, false)); + assertEquals(NONE, update(false, false, true, A, A, false, false)); + } + } + + @Test public void restoredIdWaitsForShortsDetachAndVisiblePlayer() { + assertEquals(SUSPEND, update(true, false, true, A, A, false, false)); + assertEquals(WAIT, update(true, false, true, A, A, false, false)); + assertEquals(WAIT, update(false, true, false, A, A, false, false)); + assertEquals(WAIT, update(false, false, false, A, A, false, false)); + assertEquals(RESUME, update(false, false, true, A, A, false, false)); + } + + @Test public void confirmedServiceResumesWithoutWatchScreen() { + assertEquals(SUSPEND, update(true, false, true, A, A, false, false)); + assertEquals(WAIT, update(false, false, false, A, "", false, false)); + assertEquals(RESUME, update(false, false, false, A, "", false, true)); + assertEquals(NONE, update(false, false, false, A, "", false, true)); + } + + @Test public void serviceCannotOverrideShortsOrAccountPlaylist() { + assertEquals(SUSPEND, update(true, false, true, A, A, false, true)); + assertEquals(WAIT, update(true, false, true, A, A, false, true)); + assertEquals(WAIT, update(false, true, false, A, A, false, true)); + assertEquals(WAIT, update(false, false, false, A, A, true, true)); + assertEquals(RELEASE, update(false, false, true, A, A, true, false)); + } + + @Test public void controllerAgreementResumesRegularVideoOrReleasesAccountPlaylist() { + for (boolean playlist : new boolean[]{false, true}) { + session.reset(); + assertEquals(SUSPEND, update(true, false, true, A, A, false, false)); + String replacement = playlist ? A : B; + assertEquals(WAIT, update(true, false, true, replacement, replacement, playlist, false)); + assertEquals(WAIT, update(false, false, true, replacement, "", playlist, false)); + assertEquals(playlist ? RELEASE : RESUME, + update(false, false, true, replacement, replacement, playlist, false)); + } + } + + @Test public void staleCallbacksWaitAndClosingOrResetEndsInterruption() { + assertEquals(SUSPEND, update(false, true, false, A, A, false, false)); + assertEquals(WAIT, update(false, false, true, "", A, false, false)); + assertEquals(WAIT, update(false, false, true, A, "", false, false)); + assertEquals(WAIT, update(false, false, true, A, B, false, false)); + assertEquals(RELEASE, update(false, false, false, "", "", false, false)); + assertEquals(SUSPEND, update(true, false, true, A, A, false, false)); + session.reset(); + assertEquals(NONE, update(false, false, false, B, A, false, false)); + assertEquals(NONE, update(false, false, true, A, A, false, false)); + } +} diff --git a/extensions/youtube/src/test/java/app/morphe/extension/youtube/patches/queue/QueueStartTest.java b/extensions/youtube/src/test/java/app/morphe/extension/youtube/patches/queue/QueueStartTest.java new file mode 100644 index 0000000000..7d31c11a77 --- /dev/null +++ b/extensions/youtube/src/test/java/app/morphe/extension/youtube/patches/queue/QueueStartTest.java @@ -0,0 +1,60 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/3953 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.extension.youtube.patches.queue; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +import java.util.Map; + +import app.morphe.extension.youtube.innertube.queue.PlaylistQueueRenderer.NavigationEndpoint; +import app.morphe.extension.youtube.innertube.queue.PlaylistQueueRenderer.WatchEndpoint; + +public class QueueStartTest { + private final QueueStart start = new QueueStart(new PlaylistQueue("abcdefghijk", "Selected"), "lmnopqrstuv"); + + @Test public void onlyRequestedVideoGetsMinimizedContextWithoutMutatingCaller() throws Exception { + var requested = NavigationEndpoint.newBuilder().setWatchEndpoint( + WatchEndpoint.newBuilder().setVideoId("abcdefghijk")).build(); + var unrelated = NavigationEndpoint.newBuilder().setWatchEndpoint( + WatchEndpoint.newBuilder().setVideoId("ZYXWVUTSRQP")).build(); + var context = Map.of("native-context", new Object(), "start_watch_minimized", false); + var minimized = start.minimizedContext(requested, context); + assertEquals(true, minimized.get("start_watch_minimized")); + assertSame(context.get("native-context"), minimized.get("native-context")); + assertEquals(false, context.get("start_watch_minimized")); + assertEquals(true, start.minimizedContext(requested, null).get("start_watch_minimized")); + assertSame(context, start.minimizedContext(unrelated, context)); + assertSame(context, start.minimizedContext(NavigationEndpoint.getDefaultInstance(), context)); + } + + @Test public void unrelatedNavigationSupersedesRequestButPriorPlaybackDoesNot() { + assertFalse(start.isSupersededBy("")); + assertFalse(start.isSupersededBy("lmnopqrstuv")); + assertFalse(start.isSupersededBy("abcdefghijk")); + assertTrue(start.isSupersededBy("ZYXWVUTSRQP")); + } + + @Test public void closedPlayerStartPreservesSelectedDuplicateAndUsesSavedTime() throws Exception { + var queue = start.candidate().playLast("ZYXWVUTSRQP").playLast("abcdefghijk") + .select(2).withRepeatMode(PlaylistQueue.REPEAT_ONE); + var restore = new QueueStart(queue, "", 75_432); + assertSame(queue, restore.candidate()); + assertEquals(2, restore.candidate().selectedIndex()); + assertEquals("https://www.youtube.com/watch?v=abcdefghijk&t=75s", restore.watchUrl()); + assertTrue(restore.restoresClosedPlayer()); + assertFalse(start.restoresClosedPlayer()); + assertEquals("https://www.youtube.com/watch?v=abcdefghijk", start.watchUrl()); + assertEquals("https://www.youtube.com/watch?v=abcdefghijk&t=0s", + new QueueStart(queue, "", 0).watchUrl()); + } +} diff --git a/extensions/youtube/src/test/java/app/morphe/extension/youtube/patches/queue/QueueUndoActionsTest.java b/extensions/youtube/src/test/java/app/morphe/extension/youtube/patches/queue/QueueUndoActionsTest.java new file mode 100644 index 0000000000..006aef7c59 --- /dev/null +++ b/extensions/youtube/src/test/java/app/morphe/extension/youtube/patches/queue/QueueUndoActionsTest.java @@ -0,0 +1,59 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to Morphe contributions. + */ + +package app.morphe.extension.youtube.patches.queue; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +import app.morphe.extension.youtube.innertube.queue.PlaylistQueueRenderer.NavigationEndpoint; + +public class QueueUndoActionsTest { + private static final String A = "abcdefghijk"; + private static final String B = "ZYXWVUTSRQP"; + private static final String C = "12345678901"; + + private static PlaylistQueue original() { + return new PlaylistQueue(A, "A").playLast(B).playLast(A).playLast(C); + } + + @Test public void clearUndoRequiresSameVideoAndNoNewQueue() { + var before = original().select(2); + assertSame(before, QueueUndoActions.restore(before, null, A)); + assertNull(QueueUndoActions.restore(before, null, B)); + assertNull(QueueUndoActions.restore(before, new PlaylistQueue(A, "new"), A)); + } + + @Test public void feedbackCarriesIndependentCapability() throws Exception { + var feedback = NavigationEndpoint.parseFrom( + QueueUndoActions.feedback("capability", "Queue cleared", "Undo").toByteArray()); + var toast = feedback.getAddToToastAction().getItem().getToastRenderer(); + assertEquals("Queue cleared", toast.getResponseText().getSimpleText()); + var undo = toast.getActionButton().getButtonRenderer().getServiceEndpoint(); + assertEquals("capability", undo.getQueueUndoToken()); + assertFalse(undo.hasHideEnclosingAction()); + assertFalse(undo.hasPlaylistEditEndpoint()); + } + + @Test public void expiredCapabilitiesAreConsumedWithoutRunningUndo() throws Exception { + var undo = NavigationEndpoint.newBuilder().setQueueUndoToken("capability").build(); + int[] actions = {0}; + assertTrue(QueueUndoActions.dispatchUndo(undo, "capability", () -> actions[0]++)); + assertEquals(1, actions[0]); + assertTrue(QueueUndoActions.dispatchUndo(undo, null, () -> actions[0]++)); + assertTrue(QueueUndoActions.dispatchUndo(undo, "newer", () -> actions[0]++)); + assertFalse(QueueUndoActions.dispatchUndo(NavigationEndpoint.getDefaultInstance(), + "capability", () -> actions[0]++)); + assertFalse(QueueUndoActions.dispatchUndo(null, "capability", () -> actions[0]++)); + assertEquals(1, actions[0]); + } +} diff --git a/extensions/youtube/src/test/java/app/morphe/extension/youtube/patches/queue/VideoMetadataTest.java b/extensions/youtube/src/test/java/app/morphe/extension/youtube/patches/queue/VideoMetadataTest.java new file mode 100644 index 0000000000..92f3e7f461 --- /dev/null +++ b/extensions/youtube/src/test/java/app/morphe/extension/youtube/patches/queue/VideoMetadataTest.java @@ -0,0 +1,143 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/3953 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.extension.youtube.patches.queue; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertNull; + +import org.junit.Test; + +import app.morphe.extension.youtube.innertube.queue.QueueCardRenderer.AttributedText; +import app.morphe.extension.youtube.innertube.queue.QueueCardRenderer.Avatar; +import app.morphe.extension.youtube.innertube.queue.QueueCardRenderer.CardModel; +import app.morphe.extension.youtube.innertube.queue.QueueCardRenderer.ComponentType; +import app.morphe.extension.youtube.innertube.queue.QueueCardRenderer.ContentMetadata; +import app.morphe.extension.youtube.innertube.queue.QueueCardRenderer.ContentMetadataViewModel; +import app.morphe.extension.youtube.innertube.queue.QueueCardRenderer.Element; +import app.morphe.extension.youtube.innertube.queue.QueueCardRenderer.ElementType; +import app.morphe.extension.youtube.innertube.queue.QueueCardRenderer.LockupMetadata; +import app.morphe.extension.youtube.innertube.queue.QueueCardRenderer.LockupMetadataViewModel; +import app.morphe.extension.youtube.innertube.queue.QueueCardRenderer.MetadataPart; +import app.morphe.extension.youtube.innertube.queue.QueueCardRenderer.MetadataRow; +import app.morphe.extension.youtube.innertube.queue.QueueCardRenderer.VideoData; +import app.morphe.extension.youtube.innertube.queue.QueueCardRenderer.VideoWithContextData; +import app.morphe.extension.youtube.innertube.queue.QueueCardRenderer.VideoWithContextModel; + +public class VideoMetadataTest { + private static AttributedText text(String content) { + return AttributedText.newBuilder().setContent(content).build(); + } + private static String[] parse(String url, String title, String info, + String channel, String label, String description) throws Exception { + var row = MetadataRow.newBuilder().addMetadataParts(MetadataPart.newBuilder().setText(text(info))); + var metadata = LockupMetadataViewModel.newBuilder().setTitle(text(title)).setMetadata( + ContentMetadata.newBuilder().setContentMetadataViewModel( + ContentMetadataViewModel.newBuilder().addMetadataRows(row))); + var video = VideoData.newBuilder().setAvatar(Avatar.newBuilder().setAccessibilityText(label)) + .setDragAndDropUrl(url).setLockupMetadata( + LockupMetadata.newBuilder().setLockupMetadataViewModel(metadata)); + var contextual = VideoWithContextModel.newBuilder().setVideoWithContextData( + VideoWithContextData.newBuilder().setVideoData(video).setAccessibilityText(description)); + var element = Element.newBuilder().setType(ElementType.newBuilder().setComponentType( + ComponentType.newBuilder().setModel(CardModel.newBuilder().setVideoWithContextModel(contextual)))); + var parsed = VideoMetadata.fromElement(element.build().toByteArray()); + if (parsed == null) return null; + parsed = parsed.withChannelName(channel); + return new String[]{parsed.videoId(), parsed.title(), parsed.channelName(), parsed.videoInfo()}; + } + private static String[] merge(String json, String videoId, String title) throws Exception { + var card = new VideoMetadata(videoId, title, "Starbound Extra", "157 views · 46 minutes ago", "29:04", false); + var lookup = VideoMetadata.fromSearch(new org.json.JSONObject(json), videoId); + var merged = card.withLookup(lookup).withCapture(card).withChannelName("Starbound Extra"); + var reverse = lookup.withCapture(card).withChannelName("Starbound Extra"); + merged = merged.withLookup(card); + return new String[]{Boolean.toString(card.isComplete()), merged.channelName(), + reverse.channelName(), merged.title(), Boolean.toString(merged.isComplete())}; + } + private static String[] search(String json, String videoId) throws Exception { + var parsed = VideoMetadata.fromSearch(new org.json.JSONObject(json), videoId); + if (parsed == null) return null; + return new String[]{parsed.title(), parsed.channelName(), parsed.videoInfo(), parsed.duration()}; + } + private static String[] parse(String url, String title) throws Exception { + return parse(url, title, "", "", "", ""); + } + + @Test public void lookupPreservesCollaboratorsAndLocalizedDetailsForRequestedVideo() throws Exception { + String response = """ + {"contents":{"sectionListRenderer":{"contents":[{"itemSectionRenderer":{"contents":[ + {"compactVideoRenderer":{"videoId":"other_video","title":{"runs":[{"text":"Wrong result"}]}}}, + {"compactVideoRenderer":{"videoId":"waZyE4Xl6bo", + "title":{"runs":[{"text":"A native title"}]}, + "longBylineText":{"runs":[{"text":"Starbound Extra"},{"text":" and "},{"text":"Starbound HFY"}]}, + "shortBylineText":{"runs":[{"text":"Starbound Extra"}]}, + "shortViewCountText":{"runs":[{"text":"1,8 M de vues"}]}, + "publishedTimeText":{"runs":[{"text":"il y a 2 ans"}]}, + "lengthText":{"runs":[{"text":"29:04"}]}}} + ]}}]}}} + """; + assertArrayEquals(new String[]{"A native title", "Starbound Extra and Starbound HFY", + "1,8 M de vues · il y a 2 ans", "29:04"}, search(response, "waZyE4Xl6bo")); + assertNull(search(response, "abcdefghijk")); + assertNull(search("{}", "waZyE4Xl6bo")); + } + + @Test public void uploaderCaptureCannotOverwriteCollaboratorsOrLookupTitle() throws Exception { + String response = """ + {"contents":{"sectionListRenderer":{"contents":[{"itemSectionRenderer":{"contents":[ + {"compactVideoRenderer":{"videoId":"waZyE4Xl6bo", "title":{"runs":[{"text":"Search title"}]}, + "longBylineText":{"runs":[{"text":"Starbound Extra"},{"text":" and "},{"text":"Starbound HFY"}]}, + "shortViewCountText":{"runs":[{"text":"157 views"}]}, + "publishedTimeText":{"runs":[{"text":"46 minutes ago"}]}}} + ]}}]}}} + """; + assertArrayEquals(new String[]{"false", "Starbound Extra and Starbound HFY", + "Starbound Extra and Starbound HFY", "Native title", "true"}, + merge(response, "waZyE4Xl6bo", "Native title")); + for (String title : new String[]{"", " ", "waZyE4Xl6bo"}) { + assertArrayEquals(new String[]{"false", "Starbound Extra and Starbound HFY", + "Starbound Extra and Starbound HFY", "Search title", "true"}, + merge(response, "waZyE4Xl6bo", title)); + } + } + + @Test public void cardsRequireValidIdentityAndTitle() throws Exception { + String url = "https://www.youtube.com/watch?v=abcdefghijk"; + assertArrayEquals(new String[]{"abcdefghijk", "A title", "", ""}, parse(url, "A title")); + assertNull(parse("https://www.youtube.com/playlist?list=PLabcdefghijk", "A playlist")); + assertNull(parse(url + "&list=RDabcdefghijk", "A mix")); + assertArrayEquals(new String[]{"abcdefghijk", "A video in a playlist", "", ""}, + parse(url + "&list=PLabcdefghijk&index=2", "A video in a playlist")); + assertNull(parse("https://www.youtube.com/watch?v=invalid", "A title")); + assertNull(parse(url, "")); + } + + @Test public void onlyExactKnownChannelPrefixIsSplit() throws Exception { + String url = "https://www.youtube.com/watch?v=abcdefghijk"; + String channel = "La chaîne · 🎵", info = "1,8 M de vues · il y a 2 ans"; + assertArrayEquals(new String[]{"abcdefghijk", "Une vidéo 🎵", channel, info}, + parse(url, "Une vidéo 🎵", channel + " · " + info, channel, "", "")); + assertArrayEquals(new String[]{"abcdefghijk", "Une vidéo 🎵", "", channel + " · " + info}, + parse(url, "Une vidéo 🎵", channel + " · " + info, "Another channel", "", "")); + } + + @Test public void accessibilityTextIdentifiesLongChannelWithoutFlyoutMetadata() throws Exception { + String url = "https://www.youtube.com/watch?v=abcdefghijk"; + String channel = "Starbound Extra and Starbound HFY", info = "157 views · 46 minutes ago"; + assertArrayEquals(new String[]{"abcdefghijk", "We Sent Earth Our Mightiest Weapons", channel, info}, + parse(url, "We Sent Earth Our Mightiest Weapons", channel + " · " + info, + "", "Go to channel " + channel, "")); + assertArrayEquals(new String[]{"abcdefghijk", "A title", channel, info}, + parse(url, "A title", channel + " · " + info, "", "", + "A title - 10 minutes - " + channel + " - 157 views - 46 minutes ago - play video")); + assertArrayEquals(new String[]{"abcdefghijk", "A title", "Stars · HFY", info}, + parse(url, "A title", "Stars · HFY · " + info, "", "Accéder à la chaîne Stars · HFY", "")); + assertArrayEquals(new String[]{"abcdefghijk", "A title", "", channel + " · " + info}, + parse(url, "A title", channel + " · " + info, "", "Go to channel Another channel", "")); + } +} diff --git a/patches/build.gradle.kts b/patches/build.gradle.kts index e9e3f49de0..25acbb1858 100644 --- a/patches/build.gradle.kts +++ b/patches/build.gradle.kts @@ -56,13 +56,29 @@ dependencies { testImplementation(kotlin("test")) testImplementation(libs.kotlinx.coroutines.core) + testImplementation(libs.protobuf.javalite) testRuntimeOnly(libs.junit.jupiter.engine) } tasks { test { useJUnitPlatform() + dependsOn(":extensions:youtube:generateReleaseProto", ":extensions:youtube:compileReleaseJavaWithJavac") jamApk.orNull?.let { systemProperty("jamApk", it) } + // The Android-boundary tests load release bytecode in isolated class loaders. + // Declare those actual classes as inputs so runtime changes invalidate cached results. + inputs.dir(rootProject.file( + "extensions/youtube/build/intermediates/javac/release/compileReleaseJavaWithJavac/classes" + )).withPropertyName("queueRuntimeClasses").withNormalizer(ClasspathNormalizer::class) + inputs.file(rootProject.file( + "extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/OpenVideosFullscreenHookPatch.java" + )).withPropertyName("queueFullscreenBoundarySource") + inputs.files( + rootProject.file("extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/queue/QueueSaveActions.java"), + rootProject.file("extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/queue/QueueSaveFeedback.java"), + rootProject.file("extensions/youtube/src/main/proto/app/morphe/extension/youtube/innertube/playlist_queue.proto"), + rootProject.file("extensions/youtube/src/main/proto/app/morphe/extension/youtube/innertube/icon.proto") + ).withPropertyName("queueSaveBoundarySources") } register("checkStringResources") { @@ -97,6 +113,21 @@ tasks { setArgs(listOfNotNull(apk, jamOutput.orNull)) } } + + register("validatePlaylistQueue") { + description = "Applies Native playlist queue to a stock APK and verifies it with Android SDK tools" + group = "verification" + dependsOn("testClasses") + classpath = sourceSets["test"].runtimeClasspath + mainClass.set("app.morphe.patches.youtube.interaction.nativeplaylistqueue.PlaylistQueueApkBuildKt") + doFirst { + val apk = providers.gradleProperty("playlistQueueApk").orNull + ?: throw GradleException("validatePlaylistQueue requires -PplaylistQueueApk=/absolute/path/youtube.apk") + val mode = providers.gradleProperty("playlistQueueMode").getOrElse("standalone") + val output = providers.gradleProperty("playlistQueueOutput").orNull + setArgs(listOfNotNull(apk, mode, output)) + } + } // Used by gradle-semantic-release-plugin. publish { dependsOn("generatePatchesList") diff --git a/patches/src/main/kotlin/app/morphe/patches/shared/misc/audio/silence/Fingerprints.kt b/patches/src/main/kotlin/app/morphe/patches/shared/misc/audio/silence/Fingerprints.kt index e4ffc0e0ce..0475d09726 100644 --- a/patches/src/main/kotlin/app/morphe/patches/shared/misc/audio/silence/Fingerprints.kt +++ b/patches/src/main/kotlin/app/morphe/patches/shared/misc/audio/silence/Fingerprints.kt @@ -14,6 +14,7 @@ import app.morphe.patcher.opcode import app.morphe.patcher.string import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.reference.FieldReference /** * DefaultAudioSink's applyAudioProcessorPlaybackParameters, @@ -47,13 +48,20 @@ internal object ApplySkipSilenceFingerprint : Fingerprint( ) /** - * DefaultAudioSink's setSkipSilenceEnabled. + * DefaultAudioSink's setSkipSilenceEnabled, which stores the flag read by [ApplySkipSilenceFingerprint]. + * Other boolean setters of the class have the same signature. */ -internal object SetSkipSilenceEnabledFingerprint : Fingerprint( - classFingerprint = ApplySkipSilenceFingerprint, +internal fun setSkipSilenceEnabledFingerprint(skipSilenceField: FieldReference) = Fingerprint( + definingClass = skipSilenceField.definingClass, accessFlags = listOf(AccessFlags.PUBLIC, AccessFlags.FINAL), returnType = "V", - parameters = listOf("Z") + parameters = listOf("Z"), + filters = listOf( + fieldAccess( + opcode = Opcode.IPUT_BOOLEAN, + reference = skipSilenceField + ) + ) ) /** diff --git a/patches/src/main/kotlin/app/morphe/patches/shared/misc/audio/silence/SkipSilencePatch.kt b/patches/src/main/kotlin/app/morphe/patches/shared/misc/audio/silence/SkipSilencePatch.kt index da9aaf4abb..ac7d9dac1a 100644 --- a/patches/src/main/kotlin/app/morphe/patches/shared/misc/audio/silence/SkipSilencePatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/shared/misc/audio/silence/SkipSilencePatch.kt @@ -11,11 +11,13 @@ import app.morphe.patcher.extensions.InstructionExtensions.addInstruction import app.morphe.patcher.extensions.InstructionExtensions.addInstructions import app.morphe.patcher.patch.BytecodePatchBuilder import app.morphe.patcher.patch.bytecodePatch +import app.morphe.patcher.util.getReference import app.morphe.patches.shared.misc.settings.preference.BasePreferenceScreen import app.morphe.patches.shared.misc.settings.preference.ListPreference import app.morphe.patches.shared.misc.settings.preference.SwitchPreference import app.morphe.patches.shared.misc.settings.preference.noTitleUnsortedPreferenceCategory import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction +import com.android.tools.smali.dexlib2.iface.reference.FieldReference import java.util.logging.Logger private const val EXTENSION_CLASS = "Lapp/morphe/extension/shared/patches/SkipSilencePatch;" @@ -46,6 +48,9 @@ internal fun skipSilencePatch( ) ) + val skipSilenceField = ApplySkipSilenceFingerprint.instructionMatches[1] + .instruction.getReference()!! + ApplySkipSilenceFingerprint.apply { val match = instructionMatches[1] val booleanRegister = match.getInstruction().registerA @@ -59,7 +64,7 @@ internal fun skipSilencePatch( ) } - SetSkipSilenceEnabledFingerprint.method.addInstructions( + setSkipSilenceEnabledFingerprint(skipSilenceField).method.addInstructions( 0, """ invoke-static { p1 }, $EXTENSION_CLASS->isSkipSilenceEnabled(Z)Z diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/BridgeMethods.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/BridgeMethods.kt new file mode 100644 index 0000000000..481127085f --- /dev/null +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/BridgeMethods.kt @@ -0,0 +1,65 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/3953 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.patches.youtube.interaction.nativeplaylistqueue + +import app.morphe.patcher.extensions.InstructionExtensions.addInstructionsWithLabels +import app.morphe.patcher.patch.BytecodePatchContext +import app.morphe.patcher.util.getReference +import app.morphe.patcher.util.proxy.mutableTypes.MutableClass +import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod +import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable +import app.morphe.patches.shared.misc.fix.proto.ProtobufClassParseByteArrayWithRegistryFingerprint +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation +import com.android.tools.smali.dexlib2.iface.reference.FieldReference +import com.android.tools.smali.dexlib2.immutable.ImmutableMethod +import com.android.tools.smali.dexlib2.immutable.ImmutableMethodParameter + +/** + * Adds a method to an app class. [locals] is the number of v registers + * before the p registers. All parameters must be single register types. + */ +internal fun MutableClass.addBridgeMethod( + name: String, + parameters: List, + returnType: String, + locals: Int, + body: String, + accessFlags: Int = AccessFlags.PUBLIC.value or AccessFlags.FINAL.value +): MutableMethod = ImmutableMethod( + type, name, parameters.map { ImmutableMethodParameter(it, null, null) }, returnType, + accessFlags, null, null, MutableMethodImplementation(locals + parameters.size + 1) +).toMutable().apply { + addInstructionsWithLabels(0, body) +}.also { methods.add(it) } + +/** + * Smali that parses the byte array in [bytes] to a new [messageType] instance in [result], + * using the app's own protobuf runtime. [registry] is overwritten. + * All registers must be v0-v15. + */ +internal fun BytecodePatchContext.parseProtoInstructions( + messageType: String, + bytes: String, + result: String, + registry: String +): String { + val defaultInstance = protoDefaultInstanceFingerprint(messageType) + .instructionMatches.single().instruction.getReference()!! + + // Generated messages share the static parseFrom of the protobuf runtime's message class. + val parser = ProtobufClassParseByteArrayWithRegistryFingerprint.originalMethod + + return """ + sget-object $result, $defaultInstance + invoke-static {}, Lcom/google/protobuf/ExtensionRegistryLite;->getGeneratedRegistry()Lcom/google/protobuf/ExtensionRegistryLite; + move-result-object $registry + invoke-static {$result, $bytes, $registry}, $parser + move-result-object $result + """ +} diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/Fingerprints.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/Fingerprints.kt new file mode 100644 index 0000000000..43a6688b4f --- /dev/null +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/Fingerprints.kt @@ -0,0 +1,1346 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/3953 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.patches.youtube.interaction.nativeplaylistqueue + +import app.morphe.patcher.Fingerprint +import app.morphe.patcher.InstructionLocation.MatchAfterAnywhere +import app.morphe.patcher.InstructionLocation.MatchAfterImmediately +import app.morphe.patcher.InstructionLocation.MatchAfterWithin +import app.morphe.patcher.StringComparisonType +import app.morphe.patcher.anyInstruction +import app.morphe.patcher.checkCast +import app.morphe.patcher.fieldAccess +import app.morphe.patcher.instanceOf +import app.morphe.patcher.literal +import app.morphe.patcher.methodCall +import app.morphe.patcher.opcode +import app.morphe.patcher.resource.ResourceType +import app.morphe.patcher.resourceLiteral +import app.morphe.patcher.string +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.Method +import com.android.tools.smali.dexlib2.iface.reference.FieldReference +import com.android.tools.smali.dexlib2.iface.reference.MethodReference + +/** A generated protobuf message creates its default instance in its static initializer. */ +internal fun protoDefaultInstanceFingerprint(messageType: String) = Fingerprint( + definingClass = messageType, + name = "", + filters = listOf( + fieldAccess(definingClass = messageType, type = messageType, opcode = Opcode.SPUT_OBJECT) + ) +) + +/** The native batch request, not the modern panel's single-video parameter. */ +internal object QueueSaveRequestFingerprint : Fingerprint( + name = "", + filters = listOf(string("playlist/get_add_to_playlist")) +) + +/** The picker fragment reads its serialized endpoint from its arguments. */ +internal fun queueSavePickerFingerprint(requestType: String) = Fingerprint( + parameters = listOf("Landroid/os/Bundle;"), + returnType = "V", + filters = listOf( + fieldAccess(type = "Landroid/os/Bundle;", opcode = Opcode.IGET_OBJECT), + string("navigation_endpoint", location = MatchAfterImmediately()), + methodCall(definingClass = requestType, name = "") + ) +) + +/** Opens the picker with the endpoint serialized into its arguments. */ +internal fun queueSavePickerFactoryFingerprint(pickerType: String, endpointType: String) = Fingerprint( + parameters = listOf(endpointType), + returnType = "V", + filters = listOf( + methodCall(definingClass = pickerType, name = ""), + methodCall(name = "toByteArray", parameters = emptyList(), returnType = "[B"), + opcode(Opcode.MOVE_RESULT_OBJECT, location = MatchAfterImmediately()), + string("navigation_endpoint", location = MatchAfterImmediately()), + methodCall( + "Landroid/os/Bundle;->putByteArray(Ljava/lang/String;[B)V", + location = MatchAfterImmediately() + ) + ) +) + +/** Response callback owned by the picker request; the renderer discriminator is stable. */ +internal object QueueSaveResponseFingerprint : Fingerprint( + parameters = listOf("Ljava/lang/Object;"), + returnType = "V", + filters = listOf(opcode(Opcode.CHECK_CAST), literal(88978004L)) +) + +internal fun queueSaveResponsePickerFingerprint(pickerType: String) = Fingerprint( + classFingerprint = QueueSaveResponseFingerprint, + name = "", + filters = listOf(fieldAccess(definingClass = "this", type = pickerType, opcode = Opcode.IPUT_OBJECT)) +) + +/** Native edit callback error diagnostics identify the same request's success listener. */ +internal object QueueSaveEditErrorFingerprint : Fingerprint( + filters = listOf(string("Error editing the playlist")) +) + +internal fun queueSaveEditCreationFingerprint(callbackType: String, endpointType: String) = Fingerprint( + parameters = listOf(endpointType, "Ljava/lang/Object;"), + returnType = "V", + filters = listOf(methodCall(definingClass = callbackType, name = "")) +) + +internal fun queueSaveEditSuccessFingerprint(callbackType: String) = Fingerprint( + definingClass = callbackType, + parameters = listOf("L"), + returnType = "V", + filters = listOf(methodCall("Ljava/util/List;->iterator()Ljava/util/Iterator;")) +) + +/** + * The native playback queue's navigation descriptor lookup. It wraps the delegated + * queue call in the queue's own change transaction. + */ +internal object NativeQueueDescriptorFingerprint : Fingerprint( + returnType = "L", + parameters = listOf("L"), + filters = listOf( + // Begin the change transaction. + methodCall( + definingClass = "this", + opcode = Opcode.INVOKE_DIRECT, + parameters = emptyList(), + returnType = "Ljava/lang/Object;" + ), + opcode(Opcode.MOVE_RESULT_OBJECT, location = MatchAfterImmediately()), + // Queue storage. + fieldAccess(definingClass = "this", opcode = Opcode.IGET_OBJECT, location = MatchAfterImmediately()), + methodCall(opcode = Opcode.INVOKE_INTERFACE, location = MatchAfterImmediately()), + opcode(Opcode.MOVE_RESULT_OBJECT, location = MatchAfterImmediately()), + // End the change transaction. + methodCall( + definingClass = "this", + opcode = Opcode.INVOKE_DIRECT, + returnType = "V", + location = MatchAfterWithin(1) + ), + // 21.16 concatenates this diagnostic; 21.20+ formats it in one string. + string("getNavigationDescriptor for ", StringComparisonType.STARTS_WITH) + ) +) + +/** Ends a queue change: releases the change guard, then refreshes the queue state. */ +internal fun nativeQueueChangeEndFingerprint(end: MethodReference) = Fingerprint( + definingClass = end.definingClass, + name = end.name, + parameters = end.parameterTypes.map { it.toString() }, + returnType = "V", + filters = listOf( + fieldAccess(definingClass = "this", opcode = Opcode.IGET_OBJECT), + methodCall( + opcode = Opcode.INVOKE_VIRTUAL, + parameters = listOf("Ljava/lang/Object;"), + returnType = "V", + location = MatchAfterImmediately() + ), + methodCall(definingClass = "this", parameters = listOf("Z"), returnType = "V") + ) +) + +/** Refreshes the queue state and notifies its listeners. */ +internal fun nativeQueueRefreshFingerprint(refresh: MethodReference) = Fingerprint( + definingClass = refresh.definingClass, + name = refresh.name, + parameters = refresh.parameterTypes.map { it.toString() }, + returnType = refresh.returnType +) + +/** Accepts a watch response into the queue, inside a change transaction. */ +internal fun nativeQueueAcceptFingerprint(queueType: String, responseType: String, begin: MethodReference) = + Fingerprint( + definingClass = queueType, + returnType = "V", + parameters = listOf(responseType), + filters = listOf( + methodCall(reference = begin), + opcode(Opcode.MOVE_RESULT_OBJECT, location = MatchAfterImmediately()), + fieldAccess( + definingClass = "this", + type = responseType, + opcode = Opcode.IPUT_OBJECT, + location = MatchAfterImmediately() + ) + ) + ) + +/** Disposes the queue and drops its weak registration. */ +internal fun nativeQueueDisposeFingerprint(queueType: String) = Fingerprint( + definingClass = queueType, + returnType = "V", + parameters = emptyList(), + filters = listOf(methodCall("Ljava/lang/ref/WeakReference;->get()Ljava/lang/Object;")) +) + +/** The navigator wrapper remembers whether its initial navigator is the native queue. */ +internal fun nativeQueueWrapperConstructorFingerprint(queueType: String) = Fingerprint( + name = "", + filters = listOf( + instanceOf(queueType), + fieldAccess( + definingClass = "this", + type = "Z", + opcode = Opcode.IPUT_BOOLEAN, + location = MatchAfterImmediately() + ) + ) +) + +/** + * The wrapper receives a watch response. With a playlist panel it switches + * to a native queue built from the response's watch command. + */ +internal fun nativeQueueReceiveFingerprint(queueFlag: FieldReference) = Fingerprint( + definingClass = queueFlag.definingClass, + returnType = "V", + parameters = listOf("L"), + filters = listOf( + fieldAccess(reference = queueFlag, opcode = Opcode.IGET_BOOLEAN), + // Response playlist panel. + fieldAccess(opcode = Opcode.IGET_OBJECT), + opcode(Opcode.IF_EQZ, location = MatchAfterImmediately()), + // Active navigator and queue container. + fieldAccess(definingClass = "this", opcode = Opcode.IGET_OBJECT, location = MatchAfterImmediately()), + fieldAccess(definingClass = "this", opcode = Opcode.IGET_OBJECT, location = MatchAfterImmediately()), + // Descriptor builder with the response's watch command. + opcode(Opcode.NEW_INSTANCE, location = MatchAfterImmediately()), + methodCall(name = "", location = MatchAfterImmediately()), + fieldAccess(opcode = Opcode.IGET_OBJECT, location = MatchAfterImmediately()), + fieldAccess(opcode = Opcode.IPUT_OBJECT, location = MatchAfterImmediately()), + methodCall(opcode = Opcode.INVOKE_VIRTUAL, location = MatchAfterImmediately()), + opcode(Opcode.MOVE_RESULT_OBJECT, location = MatchAfterImmediately()), + // Queue container creates the native queue. + methodCall(opcode = Opcode.INVOKE_VIRTUAL, location = MatchAfterImmediately()), + opcode(Opcode.MOVE_RESULT_OBJECT, location = MatchAfterImmediately()), + // Switch navigators. + methodCall(definingClass = "this", opcode = Opcode.INVOKE_DIRECT, location = MatchAfterImmediately()), + opcode(Opcode.CONST_4, location = MatchAfterImmediately()), + fieldAccess(reference = queueFlag, opcode = Opcode.IPUT_BOOLEAN, location = MatchAfterImmediately()), + // Stored response, delivered to the active navigator. + fieldAccess(definingClass = "this", opcode = Opcode.IPUT_OBJECT, location = MatchAfterImmediately()), + fieldAccess(definingClass = "this", opcode = Opcode.IGET_OBJECT, location = MatchAfterImmediately()), + methodCall(opcode = Opcode.INVOKE_INTERFACE, location = MatchAfterImmediately()) + ) +) + +/** The wrapper delegates the repeat mode to the active navigator. */ +internal fun nativeQueueRepeatSetterFingerprint(wrapperType: String) = Fingerprint( + definingClass = wrapperType, + returnType = "V", + parameters = listOf("I"), + filters = listOf( + fieldAccess(definingClass = "this", opcode = Opcode.IGET_OBJECT), + methodCall( + opcode = Opcode.INVOKE_INTERFACE, + parameters = listOf("I"), + returnType = "V", + location = MatchAfterImmediately() + ) + ) +) + +internal fun nativeQueueRepeatGetterFingerprint(wrapperType: String) = Fingerprint( + definingClass = wrapperType, + returnType = "I", + parameters = emptyList(), + filters = listOf( + fieldAccess(definingClass = "this", opcode = Opcode.IGET_OBJECT), + methodCall( + opcode = Opcode.INVOKE_INTERFACE, + parameters = emptyList(), + returnType = "I", + location = MatchAfterImmediately() + ) + ) +) + +/** Server autoplay replaces the active navigator with an autoplay navigator. */ +internal fun nativeQueueAutoplayFingerprint(navigator: FieldReference, switchNavigator: MethodReference) = + Fingerprint( + definingClass = navigator.definingClass, + returnType = "V", + parameters = listOf("L"), + filters = listOf( + fieldAccess(reference = navigator, opcode = Opcode.IGET_OBJECT), + opcode(Opcode.INSTANCE_OF, location = MatchAfterImmediately()), + methodCall(reference = switchNavigator) + ) + ) + +internal fun nativeQueueAutoplayConstructorFingerprint(autoplayType: String) = Fingerprint( + definingClass = autoplayType, + name = "", + parameters = List(3) { "Lj\$/util/Optional;" } +) + +/** The wrapper disposes its active navigator. */ +internal fun nativeQueueWrapperDisposeFingerprint(navigator: FieldReference, disposeName: String) = Fingerprint( + definingClass = navigator.definingClass, + returnType = "V", + parameters = emptyList(), + filters = listOf( + fieldAccess(reference = navigator, opcode = Opcode.IGET_OBJECT), + methodCall( + definingClass = navigator.type, + name = disposeName, + parameters = emptyList(), + returnType = "V", + location = MatchAfterImmediately() + ) + ) +) + +/** The watch response is a parcelable envelope around the server response. */ +internal fun nativeQueueResponseConstructorFingerprint(responseType: String) = Fingerprint( + definingClass = responseType, + name = "", + parameters = listOf("L") +) + +internal fun nativeQueueResponseSourceFingerprint(responseType: String, sourceType: String) = Fingerprint( + definingClass = responseType, + name = "", + filters = listOf(fieldAccess(definingClass = "this", type = sourceType, opcode = Opcode.IPUT_OBJECT)) +) + +/** The response constructor initializes the same navigation-set type used by the UI model. */ +internal fun nativeQueueResponseNavigationFingerprint(responseType: String, navigationType: String) = Fingerprint( + definingClass = responseType, + name = "", + parameters = listOf("L"), + filters = listOf(fieldAccess(definingClass = responseType, type = navigationType, opcode = Opcode.IPUT_OBJECT)) +) + +/** + * The mutable queue builds its shuffle order with the framework shuffler, + * storing successor and predecessor index arrays. + */ +internal object NativeQueueShuffleFingerprint : Fingerprint( + returnType = "V", + parameters = listOf("Z"), + filters = listOf( + methodCall(opcode = Opcode.INVOKE_VIRTUAL, parameters = listOf("I"), returnType = "I"), + opcode(Opcode.MOVE_RESULT, location = MatchAfterImmediately()), + // Selected position. + methodCall( + opcode = Opcode.INVOKE_VIRTUAL, + parameters = emptyList(), + returnType = "I", + location = MatchAfterImmediately() + ), + methodCall("Ljava/util/Collections;->shuffle(Ljava/util/List;)V"), + opcode(Opcode.NEW_ARRAY, location = MatchAfterImmediately()), + fieldAccess(definingClass = "this", type = "[I", opcode = Opcode.IPUT_OBJECT, location = MatchAfterImmediately()), + opcode(Opcode.NEW_ARRAY, location = MatchAfterImmediately()), + fieldAccess(definingClass = "this", type = "[I", opcode = Opcode.IPUT_OBJECT, location = MatchAfterImmediately()) + ) +) + +/** The mutable queue only inserts descriptors without a playlist ID. */ +internal fun nativeQueueLocalLoadFingerprint(mutableType: String, descriptorType: String, requestType: String) = + Fingerprint( + definingClass = mutableType, + returnType = requestType, + parameters = listOf(descriptorType, "L"), + filters = listOf( + methodCall(definingClass = descriptorType, parameters = emptyList(), returnType = "Ljava/lang/String;"), + opcode(Opcode.MOVE_RESULT_OBJECT, location = MatchAfterImmediately()), + methodCall("Ljava/lang/String;->isEmpty()Z", location = MatchAfterImmediately()) + ) + ) + +/** The playback service loads a descriptor into its active sequencer. */ +internal fun nativeQueueLoaderFingerprint(serviceType: String) = Fingerprint( + definingClass = serviceType, + returnType = "V", + filters = listOf( + fieldAccess(type = "Ljava/lang/Object;", opcode = Opcode.IGET_OBJECT), + opcode(Opcode.IF_EQZ, location = MatchAfterWithin(1)), + opcode(Opcode.CHECK_CAST, location = MatchAfterWithin(1)), + string("Initializing a PlaybackSequencer in loaderNavigator, but it does not exist") + ) +) + +internal fun nativeQueueSequencerOwnerFingerprint(serviceType: String, holderType: String) = Fingerprint( + definingClass = serviceType, + returnType = "V", + filters = listOf( + fieldAccess(definingClass = "this", type = holderType, opcode = Opcode.IGET_OBJECT), + string("Initializing a PlaybackSequencer in loaderNavigator, but it does not exist") + ) +) + +internal fun nativeQueueSequencerNavigatorFingerprint(sequencerType: String, navigatorType: String) = Fingerprint( + definingClass = sequencerType, + name = "", + filters = listOf(fieldAccess(definingClass = "this", type = navigatorType, opcode = Opcode.IPUT_OBJECT)) +) + +internal object PlaylistModelDescriptionFingerprint : Fingerprint( + name = "toString", + returnType = "Ljava/lang/String;", + parameters = emptyList(), + filters = listOf(string("EngagementPanelPlaylistPanelModel{videoId=")) +) + +internal object PlaylistControllerConstructorFingerprint : Fingerprint( + name = "", + filters = listOf(string("engagement-panel-playlist")) +) + +/** Binds the fullscreen quick actions container view to its controller. */ +internal object QuickActionsContainerFingerprint : Fingerprint( + returnType = "V", + parameters = listOf("Landroid/view/View;"), + filters = listOf( + resourceLiteral(ResourceType.ID, "quick_actions_element_container"), + checkCast("Landroid/view/ViewGroup;", location = MatchAfterWithin(10)), + // Stored on the controller. Before 21.42 the controller is read from an Object field + // and cast. Quick actions margin can insert one more instruction after the view cast. + fieldAccess( + type = "Landroid/view/ViewGroup;", + opcode = Opcode.IPUT_OBJECT, + location = MatchAfterWithin(4) + ) + ) +) + +internal fun quickActionsModelCallbackFingerprint(controller: String) = Fingerprint( + definingClass = controller, + parameters = listOf("Ljava/lang/Object;", "L"), + returnType = "L", + filters = listOf(opcode(Opcode.CHECK_CAST), methodCall(definingClass = controller, + returnType = "V", location = MatchAfterImmediately())) +) + +internal fun quickActionsResetFingerprint(controller: String, setter: MethodReference) = Fingerprint( + definingClass = controller, + parameters = listOf("L"), + returnType = "V", + filters = listOf( + methodCall(reference = setter), + fieldAccess(definingClass = controller, type = "Z", opcode = Opcode.IPUT_BOOLEAN, + location = MatchAfterWithin(3)) + ) +) + +internal fun quickActionsSizeFingerprint(controller: String) = Fingerprint( + name = "onLayoutChange", + parameters = listOf("Landroid/view/View;") + List(8) { "I" }, + returnType = "V", + filters = listOf( + methodCall("Landroid/util/Size;->(II)V"), + fieldAccess(definingClass = controller, opcode = Opcode.IGET_OBJECT, + location = MatchAfterWithin(8)), + methodCall(parameters = listOf("Ljava/lang/Object;"), returnType = "V", + location = MatchAfterImmediately()) + ) +) + +internal fun acceptedPlaylistModelFingerprint( + controllerType: String, + modelType: String, + responseVideoField: FieldReference +) = Fingerprint( + definingClass = controllerType, + returnType = "V", + parameters = listOf(modelType), + filters = listOf( + fieldAccess(reference = responseVideoField, opcode = Opcode.IGET_OBJECT), + methodCall( + "Landroid/text/TextUtils;->isEmpty(Ljava/lang/CharSequence;)Z", + location = MatchAfterImmediately() + ), + // The response's optional video ID is checked against the active video's ID. + fieldAccess( + definingClass = controllerType, + type = "Ljava/lang/String;", + opcode = Opcode.IGET_OBJECT, + location = MatchAfterWithin(2) + ), + methodCall("Ljava/lang/String;->length()I", location = MatchAfterWithin(1)), + methodCall("Ljava/lang/String;->equals(Ljava/lang/Object;)Z", location = MatchAfterWithin(2)), + // The accepted model. + fieldAccess(definingClass = "this", type = modelType, opcode = Opcode.IPUT_OBJECT) + ) +) + +/** The controller compares its active playlist identity after the stale-video check. */ +internal fun playlistControllerIdentityFingerprint(accept: MethodReference) = Fingerprint( + definingClass = accept.definingClass, + name = accept.name, + parameters = accept.parameterTypes.map { it.toString() }, + returnType = "V", + filters = listOf( + fieldAccess(definingClass = "this", type = "Ljava/lang/String;", opcode = Opcode.IGET_OBJECT), + methodCall( + "Landroid/text/TextUtils;->equals(Ljava/lang/CharSequence;Ljava/lang/CharSequence;)Z", + location = MatchAfterImmediately() + ) + ) +) + +/** AutoValue builder of the playlist model; its missing-property check names queueMode. */ +internal fun playlistModelBuildFingerprint(modelType: String) = Fingerprint( + returnType = modelType, + parameters = emptyList(), + filters = listOf(string(" queueMode")) +) + +/** The model's static builder factory, which sets the default queue mode and tracking. */ +internal fun playlistModelFactoryFingerprint(modelType: String, builderType: String) = Fingerprint( + definingClass = modelType, + returnType = builderType, + parameters = emptyList() +) + +/** The AutoValue constructor stores each of its parameters in a field. */ +internal fun playlistModelConstructorFingerprint(modelType: String) = Fingerprint( + definingClass = modelType, + name = "", + parameters = listOf("Ljava/lang/String;", "Ljava/lang/String;", "L", "L", "L", "I", "L"), + filters = List(7) { index -> + fieldAccess( + definingClass = "this", + opcodes = listOf(Opcode.IPUT_OBJECT, Opcode.IPUT), + location = if (index == 0) MatchAfterAnywhere() else MatchAfterImmediately() + ) + } +) + +/** The active video transition can complete without any playlist model being accepted. */ +internal fun playlistVideoTransitionFingerprint(activeVideoField: FieldReference) = Fingerprint( + definingClass = activeVideoField.definingClass, + returnType = "V", + parameters = List(3) { "Ljava/lang/String;" }, + filters = listOf(fieldAccess(reference = activeVideoField, opcode = Opcode.IPUT_OBJECT)) +) + +/** Renders the accepted model. The literal is the native panel's logging ID. */ +internal fun playlistPanelRenderFingerprint(modelField: FieldReference) = Fingerprint( + definingClass = modelField.definingClass, + returnType = "V", + parameters = emptyList(), + filters = listOf( + fieldAccess(reference = modelField, opcode = Opcode.IGET_OBJECT), + literal(0x19b4b) + ) +) + +/** Native playlist item factory; the education label belongs to its reorder/delete controller. */ +internal object PlaylistItemsConstructorFingerprint : Fingerprint( + name = "", + filters = listOf(resourceLiteral(ResourceType.STRING, "playlist_reorder_delete_edu")) +) + +internal fun playlistItemFactoryFingerprint(videoType: String) = Fingerprint( + classFingerprint = PlaylistItemsConstructorFingerprint, + parameters = listOf("Ljava/util/List;"), + returnType = "Ljava/util/List;", + filters = listOf(fieldAccess(type = videoType, opcode = Opcode.IGET_OBJECT)) +) + +internal fun playlistContentsFingerprint(panelType: String, factory: MethodReference) = Fingerprint( + definingClass = factory.definingClass, + parameters = emptyList(), + returnType = "V", + filters = listOf( + fieldAccess(definingClass = panelType, opcode = Opcode.IGET_OBJECT), + methodCall(reference = factory, location = MatchAfterImmediately()) + ) +) + +/** + * Native playlist row click listener. A veto predicate runs first, + * then logging decorates the bound endpoint before dispatching it. + */ +internal object PlaylistRowClickFingerprint : Fingerprint( + interfaces = listOf("Landroid/view/View\$OnClickListener;"), + name = "onClick", + returnType = "V", + parameters = listOf("Landroid/view/View;"), + filters = listOf( + methodCall( + opcode = Opcode.INVOKE_INTERFACE, + parameters = listOf("Landroid/view/View;"), + returnType = "Z" + ), + opcode(Opcode.MOVE_RESULT, location = MatchAfterImmediately()), + opcode(Opcode.IF_NEZ, location = MatchAfterImmediately()), + // Interaction logger. + fieldAccess(opcode = Opcode.IGET_OBJECT, location = MatchAfterImmediately()), + // Bound endpoint. + fieldAccess(opcode = Opcode.IGET_OBJECT, location = MatchAfterImmediately()), + // Logging decoration of the endpoint. + methodCall(opcode = Opcode.INVOKE_INTERFACE, location = MatchAfterImmediately()), + string("com.google.android.libraries.youtube.logging.interaction_logger") + ) +) + +/** The rich row binder reads the video's navigation endpoint after it reads the row listener. */ +internal fun playlistRowEndpointFingerprint(listenerType: String, videoType: String, endpointType: String) = + Fingerprint( + classFingerprint = PlaylistRichRowConstructorFingerprint, + filters = listOf( + fieldAccess(type = listenerType, opcode = Opcode.IGET_OBJECT), + fieldAccess( + definingClass = videoType, + type = "I", + opcode = Opcode.IGET, + location = MatchAfterImmediately() + ), + fieldAccess( + definingClass = videoType, + type = endpointType, + opcode = Opcode.IGET_OBJECT, + location = MatchAfterWithin(4) + ) + ) + ) + +/** Binds the row listener to a video's endpoint, logger, context and callback. */ +internal fun playlistEndpointBindFingerprint(endpoint: FieldReference) = Fingerprint( + definingClass = endpoint.definingClass, + returnType = "V", + parameters = listOf("L", endpoint.type, "Ljava/util/Map;", "L"), + filters = listOf(fieldAccess(reference = endpoint, opcode = Opcode.IPUT_OBJECT)) +) + +/** Clears the row listener's endpoint when its view is recycled. */ +internal fun playlistEndpointUnbindFingerprint(endpoint: FieldReference) = Fingerprint( + definingClass = endpoint.definingClass, + returnType = "V", + parameters = emptyList(), + filters = listOf(fieldAccess(reference = endpoint, opcode = Opcode.IPUT_OBJECT)) +) + +/** The standard button click context retains the native enclosing row identity. */ +internal object NativeButtonContextFingerprint : Fingerprint( + interfaces = listOf("Landroid/view/View\$OnClickListener;"), + returnType = "Ljava/util/Map;", + parameters = listOf("L", "Z"), + filters = listOf(string("anchor_view")) +) + +internal object NativeButtonDispatchFingerprint : Fingerprint( + classFingerprint = NativeButtonContextFingerprint, + name = "onClick", + returnType = "V", + parameters = listOf("Landroid/view/View;"), + filters = listOf(methodCall(opcode = Opcode.INVOKE_INTERFACE, returnType = "V", + parameters = listOf("L", "Ljava/util/Map;"))) +) + +/** + * Native row overflow handler. It creates the endpoint context map before showing the menu. + * The second parameter is the menu's endpoint callback. + */ +internal fun playlistRowMenuOpenFingerprint(videoType: String) = Fingerprint( + returnType = "V", + parameters = listOf(videoType, "L", "L"), + filters = listOf( + string("com.google.android.libraries.youtube.logging.interaction_logger"), + string("com.google.android.libraries.youtube.innertube.endpoint.tag", location = MatchAfterImmediately()), + methodCall( + opcode = Opcode.INVOKE_STATIC, + parameters = List(4) { "Ljava/lang/Object;" }, + location = MatchAfterImmediately() + ), + opcode(Opcode.MOVE_RESULT_OBJECT, location = MatchAfterImmediately()) + ) +) + +/** The native menu sheet dispatches its endpoint callback immediately before dismissing itself. */ +internal fun playlistMenuDispatchFingerprint(callbackType: String) = Fingerprint( + interfaces = listOf("Landroid/widget/AdapterView\$OnItemClickListener;"), + returnType = "V", + parameters = listOf("L"), + filters = listOf( + methodCall("Ljava/lang/CharSequence;->length()I"), + methodCall( + definingClass = callbackType, + parameters = listOf("L", "Ljava/util/Map;"), + returnType = "V" + ), + methodCall( + opcode = Opcode.INVOKE_VIRTUAL, + parameters = emptyList(), + returnType = "V", + location = MatchAfterImmediately() + ), + opcode(Opcode.RETURN_VOID, location = MatchAfterImmediately()) + ) +) + +/** Rich playlist rows, as opposed to the compact/offline binders. */ +internal object PlaylistRichRowConstructorFingerprint : Fingerprint( + name = "", + filters = listOf(resourceLiteral(ResourceType.ID, "playlist_panel_video_item")) +) + +/** The rich row's swipe layout getter. */ +internal object PlaylistRowSwipeLayoutFingerprint : Fingerprint( + classFingerprint = PlaylistRichRowConstructorFingerprint, + returnType = "Lcom/google/android/libraries/youtube/common/ui/swipelayout/SwipeLayout;", + parameters = emptyList(), + filters = listOf( + fieldAccess( + definingClass = "this", + type = "Lcom/google/android/libraries/youtube/common/ui/swipelayout/SwipeLayout;", + opcode = Opcode.IGET_OBJECT + ) + ) +) + +/** Native selection updates replace the swipe foreground's background. */ +internal object PlaylistRowBackgroundFingerprint : Fingerprint( + classFingerprint = PlaylistRichRowConstructorFingerprint, + parameters = emptyList(), + returnType = "V", + filters = listOf( + fieldAccess(type = "Landroid/view/View;", opcode = Opcode.IGET_OBJECT), + methodCall("Landroid/view/View;->setBackground(Landroid/graphics/drawable/Drawable;)V", + location = MatchAfterImmediately()), + methodCall("Landroid/widget/TextView;->setTextColor(I)V", location = MatchAfterWithin(8)) + ) +) + +/** SwipeLayout is an unobfuscated native widget; attachment caches its foreground drawable. */ +internal object PlaylistSwipeAttachedFingerprint : Fingerprint( + definingClass = "Lcom/google/android/libraries/youtube/common/ui/swipelayout/SwipeLayout;", + name = "onAttachedToWindow", + parameters = emptyList(), + returnType = "V", + filters = listOf( + fieldAccess(type = "Landroid/view/View;", opcode = Opcode.IPUT_OBJECT), + methodCall("Landroid/view/View;->getBackground()Landroid/graphics/drawable/Drawable;", + location = MatchAfterImmediately()), + fieldAccess(type = "Landroid/graphics/drawable/Drawable;", opcode = Opcode.IPUT_OBJECT, + location = MatchAfterWithin(2)) + ) +) + +/** The native binder configures the drag handle's touch target from resources. */ +internal object PlaylistDragBinderFingerprint : Fingerprint( + classFingerprint = PlaylistRichRowConstructorFingerprint, + returnType = "V", + filters = listOf(resourceLiteral(ResourceType.DIMEN, "playlist_drag_handle_touch_padding_start")) +) + +internal fun playlistDragStartFingerprint(holderType: String) = Fingerprint( + definingClass = holderType, + returnType = "V", + parameters = listOf("L", "L", "I"), + filters = listOf( + // Holder identity guard. 21.42+ inverts the branch. + anyInstruction(opcode(Opcode.IF_EQ), opcode(Opcode.IF_NE)), + fieldAccess(type = "Landroid/view/View;", opcode = Opcode.IGET_OBJECT, + location = MatchAfterWithin(1)), + methodCall("Landroid/view/View;->setBackground(Landroid/graphics/drawable/Drawable;)V", + location = MatchAfterWithin(1)) + ) +) + +internal fun playlistDragEndFingerprint(holderType: String) = Fingerprint( + definingClass = holderType, + returnType = "V", + parameters = listOf("L", "L", "I", "I"), + filters = listOf( + anyInstruction(opcode(Opcode.IF_EQ), opcode(Opcode.IF_NE)), + methodCall(definingClass = holderType, opcode = Opcode.INVOKE_DIRECT, + parameters = emptyList(), returnType = "V", location = MatchAfterWithin(1)) + ) +) + +internal fun playlistDragUnbindFingerprint(holderType: String) = Fingerprint( + definingClass = holderType, + returnType = "V", + parameters = listOf("L"), + filters = listOf( + methodCall("Landroid/view/View;->setOnTouchListener(Landroid/view/View\$OnTouchListener;)V"), + methodCall("Landroid/view/View;->setOnLongClickListener(Landroid/view/View\$OnLongClickListener;)V") + ) +) + +/** Native edit events read the row's video through the adapter row interface. */ +internal fun playlistEditEventsFingerprint(videoType: String) = Fingerprint( + classFingerprint = PlaylistItemsConstructorFingerprint, + parameters = listOf("Ljava/lang/Class;", "Ljava/lang/Object;", "I"), + returnType = "[Ljava/lang/Class;", + filters = listOf( + methodCall(opcode = Opcode.INVOKE_INTERFACE, parameters = emptyList(), returnType = videoType) + ) +) + +internal fun playlistNavigationRequestFingerprint(enumType: String) = Fingerprint( + name = "", + accessFlags = listOf(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR), + parameters = listOf(enumType, "L", "L") +) + +internal fun playlistNavigationIntentFingerprint(enumType: String, intent: String) = Fingerprint( + definingClass = enumType, + name = "", + filters = listOf( + string(intent), + fieldAccess(type = enumType, opcode = Opcode.SPUT_OBJECT, location = MatchAfterWithin(3)) + ) +) + +/** Static navigation request for one intent. 21.41+ also passes null descriptors to the constructor. */ +internal fun playlistNavigationSingletonFingerprint(wrapperType: String, intent: FieldReference) = Fingerprint( + definingClass = wrapperType, + name = "", + filters = listOf( + fieldAccess(reference = intent, opcode = Opcode.SGET_OBJECT), + methodCall(definingClass = wrapperType, name = "", location = MatchAfterWithin(1)), + fieldAccess(type = wrapperType, opcode = Opcode.SPUT_OBJECT, location = MatchAfterImmediately()) + ) +) + +/** + * Creates the playlist controls with their navigator, after binding the collapsed header click. + * 21.28+ creates one of two implementations, chosen by a server flag. + */ +internal fun playlistControlsCreationFingerprint(controllerType: String) = Fingerprint( + definingClass = controllerType, + returnType = "V", + parameters = emptyList(), + filters = listOf( + // The collapsed panel view. + fieldAccess(definingClass = "this", type = "Landroid/view/View;", opcode = Opcode.IGET_OBJECT), + methodCall( + "Landroid/view/View;->setOnClickListener(Landroid/view/View\$OnClickListener;)V", + location = MatchAfterWithin(3) + ), + // The navigator provider. + methodCall(opcode = Opcode.INVOKE_INTERFACE, parameters = emptyList(), returnType = "Ljava/lang/Object;"), + opcode(Opcode.MOVE_RESULT_OBJECT, location = MatchAfterImmediately()), + opcode(Opcode.CHECK_CAST, location = MatchAfterWithin(1)), + methodCall(name = "", opcode = Opcode.INVOKE_DIRECT_RANGE), + // 21.43+ restores the view register before the store. + fieldAccess( + definingClass = controllerType, + opcode = Opcode.IPUT_OBJECT, + location = MatchAfterWithin(1) + ) + ) +) + +/** Before 21.28 the playlist controls are a single class. */ +internal fun playlistControlsConstructorFingerprint(navigatorType: String, controlsType: String) = Fingerprint( + definingClass = controlsType, + name = "", + filters = listOf(fieldAccess(type = navigatorType, opcode = Opcode.IPUT_OBJECT)) +) + +/** 21.28+ implementations of the playlist controls interface. */ +internal fun playlistControlsImplementationFingerprint(navigatorType: String, controlsInterface: String) = Fingerprint( + interfaces = listOf(controlsInterface), + name = "", + filters = listOf(fieldAccess(type = navigatorType, opcode = Opcode.IPUT_OBJECT)) +) + +/** Native player control eligibility, including its alternate history callbacks. */ +internal fun playlistControlAvailabilityFingerprint(navigatorType: String, request: FieldReference) = Fingerprint( + accessFlags = listOf(AccessFlags.PRIVATE, AccessFlags.FINAL), + returnType = "V", + parameters = emptyList(), + filters = listOf( + fieldAccess(opcode = Opcode.IGET_OBJECT), + opcode(Opcode.IF_EQZ, location = MatchAfterImmediately()), + methodCall(opcode = Opcode.INVOKE_INTERFACE, parameters = emptyList(), returnType = "Z", + location = MatchAfterImmediately()), + opcode(Opcode.MOVE_RESULT, location = MatchAfterImmediately()), + fieldAccess(type = navigatorType, opcode = Opcode.IGET_OBJECT), + fieldAccess(reference = request, opcode = Opcode.SGET_OBJECT, location = MatchAfterImmediately()), + methodCall(definingClass = navigatorType, parameters = listOf(request.type), returnType = "Z", + location = MatchAfterImmediately()), + opcode(Opcode.MOVE_RESULT, location = MatchAfterImmediately()), + fieldAccess(type = "Z", opcode = Opcode.IGET_BOOLEAN, location = MatchAfterImmediately()), + opcode(Opcode.IF_EQ, location = MatchAfterImmediately()) + ) +) + +/** The press callback logs first, then dispatches to its delegate or the navigator. */ +internal fun playlistControlPressFingerprint(controlsType: String, delegate: FieldReference, request: FieldReference) = + Fingerprint( + definingClass = controlsType, + returnType = "V", + parameters = emptyList(), + filters = listOf( + // Optional logging callback. + methodCall(opcode = Opcode.INVOKE_INTERFACE, parameters = emptyList(), returnType = "V"), + fieldAccess(reference = delegate, opcode = Opcode.IGET_OBJECT, location = MatchAfterImmediately()), + fieldAccess(reference = request, opcode = Opcode.SGET_OBJECT) + ) + ) + +/** The player controls keep their navigator. */ +internal fun playlistPlayerControlsConstructorFingerprint(controlsType: String, navigatorType: String) = Fingerprint( + definingClass = controlsType, + name = "", + filters = listOf(fieldAccess(definingClass = "this", type = navigatorType, opcode = Opcode.IPUT_OBJECT)) +) + +/** Dedicated playlist engagement header; ordinary engagement headers use another layout. */ +internal object PlaylistTitleFingerprint : Fingerprint( + returnType = "Landroid/view/View;", + parameters = emptyList(), + filters = listOf( + resourceLiteral(ResourceType.LAYOUT, "playlist_engagement_panel_header"), + resourceLiteral(ResourceType.ID, "title"), + methodCall("Landroid/view/View;->findViewById(I)Landroid/view/View;", location = MatchAfterImmediately()), + opcode(Opcode.MOVE_RESULT_OBJECT, location = MatchAfterImmediately()), + checkCast("Landroid/widget/TextView;", location = MatchAfterImmediately()), + fieldAccess(definingClass = "this", type = "Landroid/widget/TextView;", opcode = Opcode.IPUT_OBJECT, + location = MatchAfterImmediately()) + ) +) + +internal fun playlistTitleBindingFingerprint(headerType: String, panelType: String) = Fingerprint( + definingClass = headerType, + returnType = "V", + parameters = listOf(panelType), + filters = listOf(fieldAccess(definingClass = "this", type = panelType, opcode = Opcode.IPUT_OBJECT)) +) + +internal object PlaylistHeaderConstructorFingerprint : Fingerprint( + name = "", + filters = listOf(resourceLiteral(ResourceType.LAYOUT, "compact_playlist_panel_header")) +) + +internal object PlaylistShuffleViewFingerprint : Fingerprint( + classFingerprint = PlaylistHeaderConstructorFingerprint, + name = "", + filters = listOf( + resourceLiteral(ResourceType.ID, "shuffle"), + methodCall("Landroid/view/View;->findViewById(I)Landroid/view/View;", location = MatchAfterImmediately()), + opcode(Opcode.MOVE_RESULT_OBJECT, location = MatchAfterImmediately()), + checkCast("Landroid/widget/ImageView;", location = MatchAfterImmediately()), + fieldAccess(type = "Landroid/widget/ImageView;", opcode = Opcode.IPUT_OBJECT, + location = MatchAfterImmediately()) + ) +) + +/** The playlist header keeps the playlist's navigator. */ +internal fun playlistHeaderNavigatorFingerprint(navigatorType: String) = Fingerprint( + classFingerprint = PlaylistHeaderConstructorFingerprint, + name = "", + filters = listOf(fieldAccess(definingClass = "this", type = navigatorType, opcode = Opcode.IPUT_OBJECT)) +) + +internal object PlaylistHeaderRepeatViewFingerprint : Fingerprint( + classFingerprint = PlaylistHeaderConstructorFingerprint, + name = "", + filters = listOf( + checkCast("Lcom/google/android/apps/youtube/app/watch/panel/ui/PlaylistLoopButtonView;"), + fieldAccess( + definingClass = "this", + type = "Lcom/google/android/apps/youtube/app/watch/panel/ui/PlaylistLoopButtonView;", + opcode = Opcode.IPUT_OBJECT, + location = MatchAfterImmediately() + ) + ) +) + +/** The header binder stores the bound playlist panel. */ +internal fun playlistHeaderPanelFingerprint(binder: Method) = Fingerprint( + definingClass = binder.definingClass, + name = binder.name, + parameters = binder.parameterTypes.map { it.toString() }, + returnType = binder.returnType, + filters = listOf( + fieldAccess( + definingClass = "this", + type = binder.parameterTypes.first().toString(), + opcode = Opcode.IPUT_OBJECT + ) + ) +) + +/** The fallback header action reads its service and shuffle view before toggling. */ +internal fun playlistShufflePressFingerprint(navigator: FieldReference, view: FieldReference) = Fingerprint( + name = "onClick", + parameters = listOf("Landroid/view/View;"), + returnType = "V", + filters = listOf( + fieldAccess(reference = navigator, opcode = Opcode.IGET_OBJECT), + fieldAccess(reference = view, opcode = Opcode.IGET_OBJECT, location = MatchAfterImmediately()), + methodCall("Landroid/widget/ImageView;->isSelected()Z", location = MatchAfterImmediately()) + ) +) + +/** Both native repeat controls observe this exact mode/shuffle pair. */ +internal object PlaylistRepeatBindingFingerprint : Fingerprint( + classFingerprint = PlaylistHeaderConstructorFingerprint, + returnType = "V", + parameters = listOf("L", "L", "I"), + filters = listOf( + fieldAccess(opcode = Opcode.IGET_OBJECT), + fieldAccess(type = "I", opcode = Opcode.IGET, location = MatchAfterImmediately()), + fieldAccess(type = "Z", opcode = Opcode.IGET_BOOLEAN, location = MatchAfterImmediately()), + methodCall(parameters = listOf("I", "Z"), returnType = "V", location = MatchAfterImmediately()) + ) +) + +internal object PlaylistRepeatCapabilitiesFingerprint : Fingerprint( + classFingerprint = PlaylistHeaderConstructorFingerprint, + returnType = "V", + parameters = listOf("L", "L", "I"), + filters = listOf( + fieldAccess(type = "Z", opcode = Opcode.IGET_BOOLEAN), + opcode(Opcode.IF_NEZ, location = MatchAfterImmediately()), + fieldAccess(type = "Z", opcode = Opcode.IGET_BOOLEAN, location = MatchAfterImmediately()), + opcode(Opcode.IF_EQZ, location = MatchAfterImmediately()), + methodCall("Lcom/google/android/apps/youtube/app/watch/panel/ui/PlaylistLoopButtonView;->setEnabled(Z)V") + ) +) + +/** Observe the native notification before it compares old state or notifies either control. */ +internal fun notificationSkipCallbackFingerprint(callbackName: String) = Fingerprint( + superclass = "Landroid/media/session/MediaSession\$Callback;", + name = callbackName, + returnType = "V", + parameters = emptyList(), + filters = listOf( + fieldAccess(opcode = Opcode.IGET_OBJECT), + methodCall( + opcode = Opcode.INVOKE_VIRTUAL, + parameters = emptyList(), + returnType = "V", + location = MatchAfterImmediately() + ) + ) +) + +/** The media session callback's delegate forwards a skip to the media owner. */ +internal fun notificationDelegateFingerprint( + callback: MethodReference, + ownerType: String, + ownerMethod: String? = null, + delegateType: String? = null +) = Fingerprint( + definingClass = delegateType, + superclass = callback.definingClass, + name = callback.name, + returnType = "V", + parameters = emptyList(), + filters = listOf( + fieldAccess(opcode = Opcode.IGET_OBJECT), + methodCall( + definingClass = ownerType, + name = ownerMethod, + opcode = Opcode.INVOKE_VIRTUAL, + parameters = emptyList(), + returnType = "V", + location = MatchAfterImmediately() + ), + opcode(Opcode.RETURN_VOID, location = MatchAfterImmediately()) + ) +) + +/** Observe after media logging, before the optional foreground callback takes over. */ +internal fun notificationNextDispatchFingerprint(next: FieldReference) = Fingerprint( + returnType = "V", + parameters = emptyList(), + filters = listOf( + fieldAccess(definingClass = "this", opcode = Opcode.IGET_OBJECT), + opcode(Opcode.IF_EQZ, location = MatchAfterImmediately()), + fieldAccess(definingClass = "this", opcode = Opcode.IGET_OBJECT, location = MatchAfterWithin(5)), + methodCall(opcode = Opcode.INVOKE_INTERFACE, parameters = emptyList(), + returnType = "Ljava/lang/Object;", location = MatchAfterImmediately()), + opcode(Opcode.MOVE_RESULT_OBJECT, location = MatchAfterImmediately()), + opcode(Opcode.CHECK_CAST, location = MatchAfterImmediately()), + fieldAccess(reference = next, opcode = Opcode.SGET_OBJECT, location = MatchAfterImmediately()), + methodCall(parameters = listOf(next.type), returnType = "V", location = MatchAfterImmediately()) + ) +) + +/** The service navigator recognizes an ended same-video target before starting playback. */ +internal fun playlistServiceNavigatorFingerprint(wrapperType: String) = Fingerprint( + parameters = listOf(wrapperType), + returnType = "V", + filters = listOf( + fieldAccess(opcode = Opcode.IGET_OBJECT), + fieldAccess(opcode = Opcode.IGET_OBJECT, location = MatchAfterImmediately()), + fieldAccess(opcode = Opcode.IGET_OBJECT, location = MatchAfterImmediately()), + opcode(Opcode.IF_EQZ, location = MatchAfterImmediately()), + methodCall(opcode = Opcode.INVOKE_INTERFACE, parameters = emptyList(), returnType = "Ljava/lang/String;"), + methodCall("Landroid/text/TextUtils;->equals(Ljava/lang/CharSequence;Ljava/lang/CharSequence;)Z", + location = MatchAfterWithin(1)) + ) +) + +/** Only the ordinary watch-page callback is replaced; casting/no-op delegates keep control. */ +internal fun notificationForegroundSkipFingerprint(callbackType: String, request: FieldReference) = Fingerprint( + interfaces = listOf(callbackType), + returnType = "V", + parameters = emptyList(), + filters = listOf(fieldAccess(reference = request, opcode = Opcode.SGET_OBJECT)) +) + +/** The playback service checks whether a navigation request can be dispatched. */ +internal fun playlistServiceCanNavigateFingerprint(serviceType: String, requestType: String) = Fingerprint( + definingClass = serviceType, + returnType = "Z", + parameters = listOf(requestType) +) + +/** Load a new watch target; native JUMP only accepts targets in an existing playlist. */ +internal fun notificationPreviousDispatchFingerprint(previous: MethodReference) = Fingerprint( + definingClass = previous.definingClass, + name = previous.name, + returnType = "V", + parameters = emptyList(), + filters = listOf( + fieldAccess(definingClass = previous.definingClass, opcode = Opcode.IGET_OBJECT), + opcode(Opcode.IF_NEZ, location = MatchAfterImmediately()) + ) +) + +/** Stable broadcast actions identify the native notification's paired skip controls. */ +internal object NotificationSkipActionsFingerprint : Fingerprint( + returnType = "Ljava/lang/String;", + parameters = emptyList(), + filters = listOf( + // The notification capabilities model. + fieldAccess(definingClass = "this", opcode = Opcode.IGET_OBJECT), + string("com.google.android.libraries.youtube.player.action.controller_notification_prev"), + string("com.google.android.libraries.youtube.player.action.controller_notification_next") + ) +) + +/** Publishes the Previous and Next capabilities, returning the changed flags. */ +internal fun notificationCapabilitiesSetterFingerprint(modelType: String) = Fingerprint( + definingClass = modelType, + returnType = "I", + parameters = listOf("Z", "Z") +) + +/** Both native adapter variants publish Previous/Next through the same model setter. */ +internal fun notificationCapabilitiesFingerprint(setter: MethodReference, adapterType: String? = null) = + Fingerprint( + definingClass = adapterType, + parameters = emptyList(), + filters = listOf(methodCall(reference = setter)) + ) + +/** The adapter keeps the notification's media owner. */ +internal fun notificationAdapterConstructorFingerprint(adapterType: String, ownerType: String) = Fingerprint( + definingClass = adapterType, + name = "", + filters = listOf(fieldAccess(definingClass = "this", type = ownerType, opcode = Opcode.IPUT_OBJECT)) +) + +/** The newer adapter dispatches the setter's returned change flags through its native notifier. */ +internal fun notificationCapabilitiesDispatchFingerprint(computation: MethodReference) = Fingerprint( + definingClass = computation.definingClass, + filters = listOf( + methodCall(reference = computation), + opcode(Opcode.MOVE_RESULT, location = MatchAfterImmediately()), + methodCall(definingClass = computation.definingClass, parameters = listOf("I"), returnType = "V", + location = MatchAfterImmediately()) + ) +) + +/** PCU is the native player-controls update trace in the original notification adapter. */ +internal fun notificationPlaybackStateFingerprint(adapterType: String) = Fingerprint( + definingClass = adapterType, + returnType = "V", + parameters = listOf("L"), + filters = listOf(string("PCU")) +) + +/** The newer adapter implements the same playback-state callback without the trace. */ +internal fun notificationPlaybackStateFingerprint(adapterType: String, callback: MethodReference) = Fingerprint( + definingClass = adapterType, + name = callback.name, + returnType = callback.returnType, + parameters = callback.parameterTypes.map { it.toString() } +) + +/** The generated builder's missing-property diagnostic identifies duration finalization. */ +internal fun queueSnackbarDurationFingerprint(builderType: String) = Fingerprint( + definingClass = builderType, + parameters = emptyList(), + returnType = "L", + filters = listOf( + // Without an action, the default duration is set to zero. + methodCall("Landroid/text/TextUtils;->isEmpty(Ljava/lang/CharSequence;)Z"), + opcode(Opcode.MOVE_RESULT, location = MatchAfterImmediately()), + opcode(Opcode.IF_NEZ, location = MatchAfterImmediately()), + opcode(Opcode.CONST_4, location = MatchAfterImmediately()), + methodCall( + definingClass = "this", + parameters = listOf("I"), + returnType = "V", + location = MatchAfterImmediately() + ), + string("Property \"duration\" has not been set") + ) +) + +/** Both snackbar factories live together; the watch-screen factory is its native sibling. */ +internal object QueueSnackbarFactoryFingerprint : Fingerprint( + returnType = "L", + parameters = listOf("L", "Ljava/util/Map;"), + filters = listOf(string("feedback_undo")) +) + +/** The mini player's close click, rather than programmatic reloads using the same dismiss API. */ +internal fun queuePlayerCloseClickFingerprint(dismiss: MethodReference) = Fingerprint( + interfaces = listOf("Landroid/view/View\$OnClickListener;"), + name = "onClick", + parameters = listOf("Landroid/view/View;"), + returnType = "V", + filters = listOf(methodCall(reference = dismiss)) +) + +/** A fling past the native threshold dismisses the mini player. */ +internal fun queuePlayerCloseFlingFingerprint(dismiss: MethodReference) = Fingerprint( + name = "onFling", + parameters = listOf("Landroid/view/MotionEvent;", "Landroid/view/MotionEvent;", "F", "F"), + returnType = "Z", + filters = listOf(methodCall(reference = dismiss)) +) + +/** Before 21.32 the fling has two separate dismissal branches. */ +internal fun queuePlayerCloseFlingLegacyFingerprint(dismiss: MethodReference) = Fingerprint( + name = "onFling", + parameters = listOf("Landroid/view/MotionEvent;", "Landroid/view/MotionEvent;", "F", "F"), + returnType = "Z", + filters = listOf( + methodCall(reference = dismiss), + methodCall(reference = dismiss) + ) +) + +/** A slow drag can close on touch release without going through onFling. */ +internal fun queuePlayerCloseTouchFingerprint(dismiss: MethodReference) = Fingerprint( + parameters = listOf("Landroid/view/View;", "Landroid/view/MotionEvent;"), + returnType = "Z", + filters = listOf(methodCall(reference = dismiss)) +) + +/** The playlist controller dispatches commands through the activity's command resolver. */ +internal fun playlistControllerDispatcherFingerprint(controllerType: String, dispatcherType: String) = Fingerprint( + definingClass = controllerType, + returnType = "V", + parameters = emptyList(), + filters = listOf( + fieldAccess(definingClass = "this", type = dispatcherType, opcode = Opcode.IGET_OBJECT), + methodCall(definingClass = dispatcherType, returnType = "V") + ) +) + +/** Snackbar factory for modern buttons, whose click listener is a shared lambda class. */ +internal object QueueSnackbarModernFactoryFingerprint : Fingerprint( + classFingerprint = QueueSnackbarFactoryFingerprint, + returnType = "L", + parameters = listOf("L", "Ljava/util/Map;"), + filters = listOf( + methodCall( + name = "", + parameters = List(4) { "Ljava/lang/Object;" } + listOf("I", "[S") + ), + methodCall( + parameters = listOf("Ljava/lang/CharSequence;", "Landroid/view/View\$OnClickListener;"), + returnType = "V" + ), + opcode(Opcode.RETURN_OBJECT, location = MatchAfterImmediately()) + ) +) + +/** The snackbar factory keeps the activity's command dispatcher. */ +internal fun queueSnackbarDispatcherFingerprint(dispatcherType: String) = Fingerprint( + classFingerprint = QueueSnackbarFactoryFingerprint, + name = "", + filters = listOf(fieldAccess(definingClass = "this", type = dispatcherType, opcode = Opcode.IPUT_OBJECT)) +) + +/** The modern snackbar button dispatches its service, navigation or command endpoint. */ +internal fun queueUndoClickFingerprint( + listenerType: String, + dispatcher: FieldReference, + callback: MethodReference +) = Fingerprint( + definingClass = listenerType, + name = "onClick", + returnType = "V", + parameters = listOf("Landroid/view/View;"), + filters = listOf(string("com.google.android.libraries.youtube.innertube.endpoint.tag")) + + List(3) { + listOf( + fieldAccess(reference = dispatcher, opcode = Opcode.IGET_OBJECT), + methodCall(reference = callback, location = MatchAfterWithin(6)) + ) + }.flatten() +) + +/** Both menu sheets read their renderer before iterating its item list. */ +internal object QueueFlyoutRendererFingerprint : Fingerprint( + parameters = emptyList(), + filters = listOf( + fieldAccess(opcode = Opcode.IGET_OBJECT, definingClass = "this"), + fieldAccess(opcode = Opcode.IGET_OBJECT, location = MatchAfterWithin(3)), + methodCall("Ljava/util/List;->iterator()Ljava/util/Iterator;", location = MatchAfterImmediately()) + ), + strings = listOf("Bottom Sheet Menu is empty. No menu items were supported.") +) + +/** Native visibility filtering lives beside the popup menu list builder. */ +internal object QueueFlyoutResolverFingerprint : Fingerprint( + filters = listOf(string("Unknown menu visibility condition: %d")) +) + +/** Popup menus filter the same renderer in the native visibility resolver's class. */ +internal fun queueFlyoutPopupFingerprint(resolverType: String, menuType: String, items: FieldReference) = Fingerprint( + definingClass = resolverType, + accessFlags = listOf(AccessFlags.PUBLIC, AccessFlags.STATIC), + parameters = listOf(menuType, "L", "L", "L"), + returnType = "L", + filters = listOf( + fieldAccess(opcode = Opcode.IGET_OBJECT, reference = items), + methodCall("Ljava/util/List;->iterator()Ljava/util/Iterator;", location = MatchAfterImmediately()) + ) +) + +/** Sheet item dispatch bypasses the model's Runnable in both native sheet variants. */ +internal fun queueFlyoutClickFingerprint(sheetType: String, buttonType: String, itemType: String) = Fingerprint( + definingClass = sheetType, + returnType = "V", + filters = listOf( + fieldAccess(opcode = Opcode.IGET_OBJECT, definingClass = buttonType, type = itemType), + // Dismissal is inherited from the native fragment superclass. + methodCall(parameters = emptyList(), returnType = "V"), + opcode(Opcode.RETURN_VOID, location = MatchAfterImmediately()) + ) +) + +/** The concrete button stores its native menu item; Runnable belongs to a superclass. */ +internal fun queueFlyoutButtonConstructorFingerprint(factory: MethodReference, itemType: String) = Fingerprint( + definingClass = factory.definingClass, + name = factory.name, + parameters = factory.parameterTypes.map { it.toString() }, + returnType = factory.returnType, + filters = listOf(methodCall( + opcode = Opcode.INVOKE_DIRECT, + name = "", + parameters = listOf("Ljava/lang/String;", itemType), + returnType = "V" + )) +) diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/FullscreenPlaylistHook.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/FullscreenPlaylistHook.kt new file mode 100644 index 0000000000..8e7022474a --- /dev/null +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/FullscreenPlaylistHook.kt @@ -0,0 +1,88 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/3953 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.patches.youtube.interaction.nativeplaylistqueue + +import app.morphe.patcher.Fingerprint +import app.morphe.patcher.extensions.InstructionExtensions.addInstructionsWithLabels +import app.morphe.patcher.patch.BytecodePatchContext +import app.morphe.patcher.util.getReference +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.iface.instruction.Instruction +import com.android.tools.smali.dexlib2.iface.reference.FieldReference +import com.android.tools.smali.dexlib2.iface.reference.MethodReference + +internal fun installFullscreenPlaylist(context: BytecodePatchContext) = with(context) { + val container = QuickActionsContainerFingerprint + .instructionMatches.last().instruction.getReference()!! + val controllerType = container.definingClass + val callback = quickActionsModelCallbackFingerprint(controllerType).match() + val setter = callback.instructionMatches.last().getInstruction() + .getReference()!! + val modelType = setter.parameterTypes.single().toString() + val reset = quickActionsResetFingerprint(controllerType, setter).match() + val size = quickActionsSizeFingerprint(controllerType).match() + val dimensions = size.instructionMatches[1].getInstruction().getReference()!! + val emit = size.instructionMatches[2].getInstruction().getReference()!! + + // The native BehaviorSubject owns the last measured dimensions. Re-emitting them + // runs YouTube's original measuring/binding callback, including its lifecycle guards. + val currentDimensions = Fingerprint( + definingClass = dimensions.type, + parameters = emptyList(), + returnType = "Ljava/lang/Object;", + accessFlags = listOf(AccessFlags.PUBLIC, AccessFlags.FINAL) + ).originalMethod + val controller = mutableClassDefBy(controllerType) + controller.interfaces.add(FULLSCREEN_CONTROLLER_INTERFACE) + controller.addBridgeMethod( + "patch_parseQuickActions", + listOf("[B"), + "Ljava/lang/Object;", + 2, + """ + ${parseProtoInstructions(modelType, bytes = "p1", result = "v0", registry = "v1")} + return-object v0 + """ + ) + + controller.addBridgeMethod( + "patch_refreshQuickActions", + listOf("Ljava/lang/Object;"), + "V", + 2, + """ + const/4 v0, 0x0 + # The native reset also releases the presenter's cached component tree. + # Merely resetting the rendered flag reuses the previous button layout. + # Its lifecycle argument is unused; null is safe for this UI-only reset. + invoke-virtual {p0, v0}, ${reset.originalMethod} + check-cast p1, $modelType + invoke-virtual {p0, p1}, $setter + iget-object v0, p0, $dimensions + invoke-virtual {v0}, $currentDimensions + move-result-object v1 + if-eqz v1, :done + invoke-virtual {v0, v1}, $emit + + :done + return-void + """ + ) + + // The setter's original null handling, visibility policy and other action bindings + // all remain in place. Only our local queue can supply the additional button. + Fingerprint(definingClass = controllerType, name = setter.name, + parameters = listOf(modelType), returnType = "V").method.addInstructionsWithLabels( + 0, + """ + invoke-static/range {p0 .. p1}, $FULLSCREEN_ACTIONS_CLASS->onModelLoaded(${FULLSCREEN_CONTROLLER_INTERFACE}Ljava/lang/Object;)Ljava/lang/Object; + move-result-object p1 + check-cast p1, $modelType + """ + ) +} diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/NativePlaylistQueuePatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/NativePlaylistQueuePatch.kt new file mode 100644 index 0000000000..5ea7d3db8f --- /dev/null +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/NativePlaylistQueuePatch.kt @@ -0,0 +1,99 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/3953 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.patches.youtube.interaction.nativeplaylistqueue + +import app.morphe.patcher.patch.bytecodePatch +import app.morphe.patches.shared.misc.fix.proto.fixProtoLibraryPatch +import app.morphe.patches.shared.misc.proto.hookElement +import app.morphe.patches.youtube.layout.flyout.flyoutPatch +import app.morphe.patches.youtube.misc.auth.authHookPatch +import app.morphe.patches.youtube.misc.extension.sharedExtensionPatch +import app.morphe.patches.youtube.misc.playertype.playerTypeHookPatch +import app.morphe.patches.youtube.misc.playservice.is_21_15_or_greater +import app.morphe.patches.youtube.misc.playservice.versionCheckPatch +import app.morphe.patches.youtube.misc.settings.settingsPatch +import app.morphe.patches.youtube.shared.hookVideoIntentCommandTransform +import app.morphe.patches.youtube.shared.hookVideoIntentContext +import app.morphe.patches.youtube.shared.hookVideoIntentInterceptor +import app.morphe.patches.youtube.shared.openVideoIntentPatch +import app.morphe.patches.youtube.video.information.videoInformationPatch +import app.morphe.patches.youtube.video.videoid.hookBackgroundPlayVideoId +import app.morphe.patches.youtube.video.videoid.hookVideoId +import app.morphe.patches.youtube.video.videoid.videoIdPatch +import app.morphe.util.setExtensionIsPatchIncluded + +private const val EXTENSION_PACKAGE = "Lapp/morphe/extension/youtube/patches/queue" +internal const val EXTENSION_CLASS = "$EXTENSION_PACKAGE/NativePlaylistQueuePatch;" +internal const val CONTROLLER_INTERFACE = "$EXTENSION_PACKAGE/NativePlaylistQueuePatch\$PlaylistController;" +internal const val NAVIGATOR_OWNER_INTERFACE = "$EXTENSION_PACKAGE/NativePlaylistQueuePatch\$NavigatorOwner;" +internal const val PLAYER_CONTROLS_INTERFACE = "$EXTENSION_PACKAGE/NativePlaylistQueuePatch\$PlayerControls;" +internal const val MEDIA_CONTROLS_INTERFACE = "$EXTENSION_PACKAGE/NativePlaylistQueuePatch\$MediaControls;" +internal const val NOTIFICATION_CONTROLS_INTERFACE = "$EXTENSION_PACKAGE/NativePlaylistQueuePatch\$NotificationControls;" +internal const val SERVICE_NAVIGATOR_INTERFACE = "$EXTENSION_PACKAGE/NativePlaylistQueuePatch\$ServiceNavigator;" +internal const val NATIVE_PLAYBACK_INTERFACE = "$EXTENSION_PACKAGE/NativePlaylistQueuePatch\$NativePlayback;" +internal const val REPEAT_HEADER_INTERFACE = "$EXTENSION_PACKAGE/NativePlaylistQueuePatch\$RepeatHeader;" +internal const val ROW_CLICK_INTERFACE = "$EXTENSION_PACKAGE/NativePlaylistQueuePatch\$RowClick;" +internal const val SWIPE_BACKGROUND_INTERFACE = "$EXTENSION_PACKAGE/NativePlaylistQueuePatch\$SwipeBackground;" +internal const val QUEUE_PANEL_INTERFACE = "$EXTENSION_PACKAGE/NativePlaylistQueuePatch\$QueuePanel;" +internal const val PLAYLIST_TITLE_INTERFACE = "$EXTENSION_PACKAGE/NativePlaylistQueuePatch\$PlaylistTitle;" +internal const val PLAYLIST_MODEL_INTERFACE = "$EXTENSION_PACKAGE/NativePlaylistQueuePatch\$PlaylistModel;" +internal const val QUEUE_ROW_INTERFACE = "$EXTENSION_PACKAGE/NativePlaylistQueuePatch\$QueueRow;" +internal const val REORDER_ITEMS_INTERFACE = "$EXTENSION_PACKAGE/NativePlaylistQueuePatch\$ReorderItems;" +internal const val FULLSCREEN_ACTIONS_CLASS = "$EXTENSION_PACKAGE/QueueFullscreenActions;" +internal const val FULLSCREEN_CONTROLLER_INTERFACE = "$EXTENSION_PACKAGE/QueueFullscreenActions\$Controller;" +internal const val SAVE_ACTIONS_CLASS = "$EXTENSION_PACKAGE/QueueSaveActions;" +internal const val SAVE_PICKER_INTERFACE = "$EXTENSION_PACKAGE/QueueSaveActions\$Picker;" +internal const val SAVE_FEEDBACK_CLASS = "$EXTENSION_PACKAGE/QueueSaveFeedback;" +internal const val SAVE_CALLBACK_INTERFACE = "$EXTENSION_PACKAGE/QueueSaveFeedback\$Callback;" +internal const val SAVE_SELECTION_CLASS = "$EXTENSION_PACKAGE/QueueSaveFeedback\$Selection;" +internal const val UNDO_COMMAND_INTERFACE = "$EXTENSION_PACKAGE/QueueUndoActions\$Command;" + +/** + * Adds a temporary video queue using YouTube's playlist interface and playback controls. + * Used by Add to queue when the queue type is set to the native queue. + */ +val nativePlaylistQueuePatch = bytecodePatch( + description = "Adds a temporary video queue using YouTube's playlist interface and playback controls." +) { + dependsOn( + sharedExtensionPatch, + authHookPatch, + settingsPatch, + flyoutPatch, + videoInformationPatch, + videoIdPatch, + playerTypeHookPatch, + fixProtoLibraryPatch, + openVideoIntentPatch, + versionCheckPatch + ) + + execute { + // 20.x builds the native playlist queue differently. + // Add to queue then does not offer the native queue type. + if (!is_21_15_or_greater) { + return@execute + } + + hookVideoIntentCommandTransform(EXTENSION_CLASS, "prepareQueueVideoCommand") + hookVideoIntentInterceptor(EXTENSION_CLASS, "onQueueMixTap") + hookVideoIntentContext(EXTENSION_CLASS, "prepareQueueStartContext") + + installQueueFlyout(this) + installPlaylistPanel(this) + installPlaylistTitle(this) + installRatingCapture(this) + installPlaylistRemoval(this) + installFullscreenPlaylist(this) + + hookElement("$EXTENSION_CLASS->onElementLoaded") + hookVideoId("$EXTENSION_CLASS->onVideoChanged(Ljava/lang/String;)V") + hookBackgroundPlayVideoId("$EXTENSION_CLASS->onVideoChanged(Ljava/lang/String;)V") + setExtensionIsPatchIncluded(EXTENSION_CLASS) + } +} diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/NativeQueueEngineHook.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/NativeQueueEngineHook.kt new file mode 100644 index 0000000000..5713bd6cd9 --- /dev/null +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/NativeQueueEngineHook.kt @@ -0,0 +1,596 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/3953 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.patches.youtube.interaction.nativeplaylistqueue + +import app.morphe.patcher.extensions.InstructionExtensions.addInstruction +import app.morphe.patcher.extensions.InstructionExtensions.addInstructionsWithLabels +import app.morphe.patcher.patch.BytecodePatchContext +import app.morphe.patcher.util.cloneParameters +import app.morphe.patcher.util.findFreeRegister +import app.morphe.patcher.util.getReference +import app.morphe.patcher.util.proxy.mutableTypes.MutableField.Companion.toMutable +import app.morphe.util.addInstructionsAtControlFlowLabel +import app.morphe.util.findFieldFromToString +import app.morphe.util.findInstructionIndicesReversedOrThrow +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.reference.FieldReference +import com.android.tools.smali.dexlib2.iface.reference.MethodReference +import com.android.tools.smali.dexlib2.iface.reference.TypeReference +import com.android.tools.smali.dexlib2.immutable.ImmutableField + +/** Supply local rows to the stock mutable queue; its sequencer owns navigation. */ +internal fun installNativeQueueEngine( + context: BytecodePatchContext, + serviceType: String, + enumType: String +) = with(context) { + // The native playback queue, its navigator and the transaction around its edits. + val queueMatch = NativeQueueDescriptorFingerprint.match() + val queueType = queueMatch.originalClassDef.type + val requestType = queueMatch.originalMethod.parameterTypes.single().toString() + val descriptorType = queueMatch.originalMethod.returnType + val beginQueueChange = queueMatch.instructionMatches[0].instruction.getReference()!! + val queueOwner = queueMatch.instructionMatches[2].instruction.getReference()!! + val endQueueChange = queueMatch.instructionMatches[5].instruction.getReference()!! + + val queueChangeEnd = nativeQueueChangeEndFingerprint(endQueueChange) + val changeGuard = queueChangeEnd.instructionMatches[0].instruction.getReference()!! + val releaseChange = queueChangeEnd.instructionMatches[1].instruction.getReference()!! + val refresh = queueChangeEnd.instructionMatches[2].instruction.getReference()!! + + val queueDisposeMatch = nativeQueueDisposeFingerprint(queueType) + val queueDispose = queueDisposeMatch.originalMethod + + // The wrapper switches between the server's navigator and the local queue. + val wrapperFlag = nativeQueueWrapperConstructorFingerprint(queueType) + .instructionMatches.last().instruction.getReference()!! + val wrapperType = wrapperFlag.definingClass + + val receive = nativeQueueReceiveFingerprint(wrapperFlag) + fun receiveField(index: Int) = receive.instructionMatches[index].instruction.getReference()!! + fun receiveMethod(index: Int) = receive.instructionMatches[index].instruction.getReference()!! + val responseType = receive.originalMethod.parameterTypes.single().toString() + val responsePanel = receiveField(1) + val nativeNavigator = receiveField(3) + val container = receiveField(4) + val builderConstructor = receiveMethod(6) + val watchCommand = receiveField(7) + val commandField = receiveField(8) + val buildDescriptor = receiveMethod(9) + val containerFactory = receiveMethod(11) + val switchNavigator = receiveMethod(13) + val response = receiveField(16) + val nativeReceive = receiveMethod(18) + val navigatorType = nativeNavigator.type + val panelType = responsePanel.type + + val acceptMatch = nativeQueueAcceptFingerprint(queueType, responseType, beginQueueChange) + val accept = acceptMatch.originalMethod + val storedResponse = acceptMatch.instructionMatches.last().instruction.getReference()!! + + val repeatSetter = nativeQueueRepeatSetterFingerprint(wrapperType) + .instructionMatches.last().instruction.getReference()!! + val repeatGetter = nativeQueueRepeatGetterFingerprint(wrapperType) + .instructionMatches.last().instruction.getReference()!! + + val responseConstructor = nativeQueueResponseConstructorFingerprint(responseType).originalMethod + val sourceResponse = nativeQueueResponseSourceFingerprint( + responseType, + responseConstructor.parameterTypes.single().toString() + ).instructionMatches.single().instruction.getReference()!! + val navigationType = PlaylistModelDescriptionFingerprint.originalMethod + .findFieldFromToString(", navigationSets=").type + val responseNavigation = nativeQueueResponseNavigationFingerprint(responseType, navigationType) + .instructionMatches.single().instruction.getReference()!! + + // The mutable queue holds local rows and builds the shuffle order. + val shuffle = NativeQueueShuffleFingerprint + val mutableType = shuffle.originalClassDef.type + val shuffleSetter = shuffle.originalMethod + val position = shuffle.instructionMatches[2].instruction.getReference()!! + val shuffleLinks = listOf( + shuffle.instructionMatches[5].instruction.getReference()!!, + shuffle.instructionMatches[7].instruction.getReference()!! + ) + + val localLoad = nativeQueueLocalLoadFingerprint(mutableType, descriptorType, requestType) + val playlistId = localLoad.instructionMatches.first().instruction.getReference()!! + + val localFlag = ImmutableField( + wrapperType, "patch_localQueue", "Z", AccessFlags.PRIVATE.value, null, null, null + ).toMutable() + val localItemsFlag = ImmutableField( + mutableType, "patch_localQueue", "Z", AccessFlags.PUBLIC.value, null, null, null + ).toMutable() + mutableClassDefBy(wrapperType).fields.add(localFlag) + mutableClassDefBy(mutableType).fields.add(localItemsFlag) + mutableClassDefBy(wrapperType).interfaces.add(NATIVE_PLAYBACK_INTERFACE) + + fun bridge(type: String, name: String, parameters: List, result: String, locals: Int, body: String) = + mutableClassDefBy(type).addBridgeMethod(name, parameters, result, locals, body) + + bridge( + wrapperType, + "patch_isLocalQueue", + emptyList(), + "Z", + 1, + """ + iget-boolean v0, p0, $localFlag + return v0 + """ + ) + + // Copy the response envelope. Other native consumers retain the untouched server model. + bridge( + wrapperType, + "patch_localPanel", + listOf("[B"), + responseType, + 4, + """ + iget-object v1, p0, $response + if-eqz v1, :missing + iget-object v1, v1, $sourceResponse + new-instance v0, $responseType + invoke-direct {v0, v1}, $responseConstructor + ${parseProtoInstructions(panelType, bytes = "p1", result = "v1", registry = "v2")} + check-cast v1, $panelType + iput-object v1, v0, $responsePanel + return-object v0 + + :missing + const/4 v0, 0x0 + return-object v0 + """ + ) + + bridge( + wrapperType, + "patch_restoreQueueRepeat", + listOf("I"), + "V", + 1, + """ + iget-boolean v0, p0, $localFlag + if-eqz v0, :done + iget-object v0, p0, $nativeNavigator + invoke-interface {v0, p1}, $repeatSetter + + :done + return-void + """ + ) + + // Disable only the mutable shuffler, without publishing an intermediate mode. + // The old index links cannot be used while the accepted rows are replaced. + bridge( + mutableType, + "patch_clearShuffleOrder", + emptyList(), + "V", + 1, + """ + const/4 v0, 0x0 + invoke-virtual {p0, v0}, $shuffleSetter + iput-object v0, p0, ${shuffleLinks[0]} + iput-object v0, p0, ${shuffleLinks[1]} + return-void + """ + ) + + bridge( + wrapperType, + "patch_attachQueue", + listOf("[B", "[B"), + "Z", + 7, + """ + invoke-virtual {p0, p1}, $wrapperType->patch_localPanel([B)$responseType + move-result-object v0 + if-eqz v0, :missing + iget-object v1, p0, $nativeNavigator + invoke-interface {v1}, $repeatGetter + move-result v6 + # Reuse an existing playlist queue before constructing an INSERT request. + # Its acceptance transaction suppresses selected-item playback callbacks. + instance-of v3, v1, $queueType + if-nez v3, :existing + new-instance v2, ${builderConstructor.definingClass} + invoke-direct {v2}, $builderConstructor + ${parseProtoInstructions(watchCommand.type, bytes = "p2", result = "v3", registry = "v4")} + check-cast v3, ${watchCommand.type} + iput-object v3, v2, $commandField + invoke-virtual {v2}, $buildDescriptor + move-result-object v2 + invoke-virtual {v2}, $playlistId + move-result-object v3 + invoke-virtual {v3}, Ljava/lang/String;->isEmpty()Z + move-result v3 + if-eqz v3, :missing + iget-object v3, p0, $container + invoke-virtual {v3, v2}, $containerFactory + move-result-object v3 + goto :populate + + :existing + move-object v3, v1 + + :populate + check-cast v3, $queueType + iget-object v4, v3, $queueOwner + instance-of v5, v4, $mutableType + if-eqz v5, :missing + check-cast v4, $mutableType + invoke-virtual {v4}, $mutableType->patch_clearShuffleOrder()V + invoke-virtual {v3, v0}, $accept + const/4 v5, 0x1 + iput-boolean v5, v4, $localItemsFlag + invoke-direct {p0, v1, v3}, $switchNavigator + iput-boolean v5, p0, $wrapperFlag + iput-boolean v5, p0, $localFlag + invoke-interface {v3, v6}, $repeatSetter + invoke-virtual {v3, v5}, $refresh + return v5 + + :missing + const/4 v0, 0x0 + return v0 + """ + ) + + bridge( + wrapperType, + "patch_updateSnapshot", + listOf("[B"), + "V", + 2, + """ + iget-boolean v0, p0, $localFlag + if-eqz v0, :done + invoke-virtual {p0, p1}, $wrapperType->patch_localPanel([B)$responseType + move-result-object v0 + iget-object v1, p0, $nativeNavigator + check-cast v1, $queueType + invoke-virtual {v1, v0}, $queueType->patch_updateSnapshot($responseType)V + + :done + return-void + """ + ) + + bridge( + queueType, + "patch_updateSnapshot", + listOf(responseType), + "V", + 0, + """ + iput-object p1, p0, $storedResponse + return-void + """ + ) + + bridge( + wrapperType, + "patch_getSelectedIndex", + emptyList(), + "I", + 1, + """ + iget-object v0, p0, $nativeNavigator + check-cast v0, $queueType + iget-object v0, v0, $queueOwner + invoke-interface {v0}, ${queueOwner.type}->${position.name}()I + move-result v0 + return v0 + """ + ) + + bridge( + wrapperType, + "patch_getRepeatMode", + emptyList(), + "I", + 1, + """ + iget-object v0, p0, $nativeNavigator + check-cast v0, $queueType + invoke-interface {v0}, $repeatGetter + move-result v0 + return v0 + """ + ) + + bridge( + wrapperType, + "patch_ignorePlaylistResponse", + listOf(responseType), + "Z", + 1, + """ + if-eqz p1, :missing + iget-object p1, p1, $watchCommand + invoke-static/range {p0 .. p1}, $EXTENSION_CLASS->shouldIgnorePlaylistResponse(${NATIVE_PLAYBACK_INTERFACE}Lcom/google/protobuf/MessageLite;)Z + move-result v0 + return v0 + + :missing + const/4 v0, 0x0 + return v0 + """ + ) + + // Required native response feed, separate from the engagement-panel presentation feed. + // The native parser reuses its receiver register for the delegated navigator. + // Preserve parameter registers before adding the post-response ownership callback. + val receiveMethod = receive.method.cloneParameters() + receiveMethod.findInstructionIndicesReversedOrThrow(Opcode.RETURN_VOID).forEach { index -> + receiveMethod.addInstruction( + index, + "invoke-static/range {p0 .. p0}, $EXTENSION_CLASS->onNativeQueueResponse($NATIVE_PLAYBACK_INTERFACE)V" + ) + } + + val receiveRegister = receiveMethod.findFreeRegister(0) + receiveMethod.addInstructionsWithLabels( + 0, + """ + invoke-virtual/range {p0 .. p1}, $wrapperType->patch_ignorePlaylistResponse($responseType)Z + move-result v$receiveRegister + if-eqz v$receiveRegister, :current_response + return-void + + :current_response + iget-boolean v$receiveRegister, p0, $localFlag + if-eqz v$receiveRegister, :server_queue + iput-object p1, p0, $response + return-void + + :server_queue + nop + """ + ) + + // Server autoplay replaces the navigator. Remember its last state for Clear. + val autoplay = nativeQueueAutoplayFingerprint(nativeNavigator, switchNavigator) + val autoplayType = autoplay.originalMethod.parameterTypes.single().toString() + val autoplayConstructor = nativeQueueAutoplayConstructorFingerprint(autoplayType).originalMethod + val optionalType = autoplayConstructor.parameterTypes.first().toString() + val savedAutoplay = ImmutableField( + wrapperType, "patch_lastAutoplay", autoplayType, AccessFlags.PRIVATE.value, null, null, null + ).toMutable() + mutableClassDefBy(wrapperType).fields.add(savedAutoplay) + + var autoplayMethod = autoplay.method + if (autoplayMethod.implementation!!.registerCount <= 2) autoplayMethod = autoplayMethod.cloneParameters() + val autoplayRegister = autoplayMethod.findFreeRegister(0) + autoplayMethod.addInstructionsWithLabels( + 0, + """ + iput-object p1, p0, $savedAutoplay + iget-boolean v$autoplayRegister, p0, $localFlag + if-eqz v$autoplayRegister, :server_autoplay + return-void + + :server_autoplay + nop + """ + ) + + // Queue storage also owns the selected-item listener that loads/unloads playback. + // Reuse the native transaction guard (used by accept/commit) while disposing; + // removing the last item must not send that listener a null current video. + bridge( + queueType, + "patch_disposeWithoutPlayback", + emptyList(), + "V", + 3, + """ + invoke-direct {p0}, $beginQueueChange + move-result-object v0 + iget-object v1, p0, $changeGuard + invoke-virtual {p0}, $queueDispose + invoke-virtual {v1, v0}, $releaseChange + return-void + + move-exception v2 + invoke-virtual {v1, v0}, $releaseChange + throw v2 + """ + ).implementation!!.apply { + // Guard the dispose call (instruction 3) with the handler at instruction 6. + addCatch(newLabelForIndex(3), newLabelForIndex(4), newLabelForIndex(6)) + } + + // Reuse the ordinary autoplay navigator's initialization and response handling. + // This detaches the local sequence without loading/seeking/stopping the player. + bridge( + wrapperType, + "patch_clearQueue", + emptyList(), + "Z", + 6, + """ + iget-boolean v0, p0, $localFlag + if-eqz v0, :missing + iget-object v1, p0, $response + if-eqz v1, :missing + iget-object v2, p0, $nativeNavigator + instance-of v0, v2, $queueType + if-eqz v0, :missing + new-instance v4, ${builderConstructor.definingClass} + invoke-direct {v4}, $builderConstructor + iget-object v0, v1, $watchCommand + iput-object v0, v4, $commandField + invoke-virtual {v4}, $buildDescriptor + move-result-object v4 + invoke-virtual {v4}, $playlistId + move-result-object v4 + invoke-virtual {v4}, Ljava/lang/String;->isEmpty()Z + move-result v0 + if-nez v0, :ordinary_autoplay + # A replaced playlist must not return through its old next/previous sets. + # Copy the response, leaving the running descriptor and server data untouched. + iget-object v0, v1, $sourceResponse + new-instance v1, $responseType + invoke-direct {v1, v0}, $responseConstructor + const/4 v0, 0x0 + iput-object v0, v1, $responseNavigation + const/4 v3, 0x0 + const/4 v5, 0x0 + goto :create_autoplay + + :ordinary_autoplay + iget-object v3, p0, $savedAutoplay + move-object v5, v3 + if-nez v3, :autoplay_ready + + :create_autoplay + invoke-static {}, $optionalType->empty()$optionalType + move-result-object v4 + new-instance v3, $autoplayType + invoke-direct {v3, v4, v4, v4}, $autoplayConstructor + + :autoplay_ready + const/4 v0, 0x0 + iput-boolean v0, p0, $localFlag + invoke-virtual {p0, v3}, ${autoplay.originalMethod} + iput-object v5, p0, $savedAutoplay + iget-object v4, p0, $nativeNavigator + invoke-interface {v4, v1}, $nativeReceive + if-eqz v5, :dispose_queue + invoke-virtual {p0, v5}, ${autoplay.originalMethod} + + :dispose_queue + check-cast v2, $queueType + invoke-virtual {v2}, $queueType->patch_disposeWithoutPlayback()V + const/4 v0, 0x1 + return v0 + + :missing + const/4 v0, 0x0 + return v0 + """ + ) + + bridge( + queueType, + "patch_releaseLocalQueue", + emptyList(), + "V", + 2, + """ + iget-object v0, p0, $queueOwner + instance-of v1, v0, $mutableType + if-eqz v1, :done + check-cast v0, $mutableType + const/4 v1, 0x0 + iput-boolean v1, v0, $localItemsFlag + + :done + return-void + """ + ) + + queueDisposeMatch.method.addInstruction( + 0, + "invoke-virtual/range {p0 .. p0}, $queueType->patch_releaseLocalQueue()V" + ) + + val wrapperDispose = nativeQueueWrapperDisposeFingerprint(nativeNavigator, queueDispose.name) + val disposeRegister = wrapperDispose.method.findFreeRegister(0) + wrapperDispose.method.addInstructionsWithLabels( + 0, + """ + const/4 v$disposeRegister, 0x0 + iput-boolean v$disposeRegister, p0, $localFlag + iput-object v$disposeRegister, p0, $savedAutoplay + """ + ) + + // Local row loads must be JUMP within the native queue, rather than new ordinary playback. + val requestConstructor = playlistNavigationRequestFingerprint(enumType).originalMethod + val jump = playlistNavigationIntentFingerprint(enumType, "JUMP") + .instructionMatches.last().instruction.getReference()!! + bridge( + mutableType, + "patch_localJump", + listOf(descriptorType, requestConstructor.parameterTypes.last().toString()), + requestType, + 2, + """ + new-instance v0, $requestType + sget-object v1, $jump + invoke-direct {v0, v1, p1, p2}, $requestConstructor + return-object v0 + """ + ) + + // Only one native local is needed: the request result occupies it after the flag check. + val insertMethod = localLoad.method.cloneParameters() + val insertRegister = insertMethod.findFreeRegister(0) + val loadParameters = localLoad.originalMethod.parameterTypes.joinToString("") + insertMethod.addInstructionsWithLabels( + 0, + """ + iget-boolean v$insertRegister, p0, $localItemsFlag + if-eqz v$insertRegister, :account_insert + invoke-virtual {p0, p1, p2}, $mutableType->patch_localJump($loadParameters)$requestType + move-result-object v$insertRegister + return-object v$insertRegister + + :account_insert + nop + """ + ) + + // The playback service holds the active sequencer, which holds the wrapper. + val loader = nativeQueueLoaderFingerprint(serviceType) + val sequencer = loader.instructionMatches[0].instruction.getReference()!! + val sequencerType = loader.instructionMatches[2].instruction.getReference()!!.type + val sequencerOwner = nativeQueueSequencerOwnerFingerprint(serviceType, sequencer.definingClass) + .instructionMatches.first().instruction.getReference()!! + val sequencerField = nativeQueueSequencerNavigatorFingerprint(sequencerType, navigatorType) + .instructionMatches.single().instruction.getReference()!! + + bridge( + serviceType, + "patch_getQueuePlayback", + emptyList(), + NATIVE_PLAYBACK_INTERFACE, + 1, + """ + iget-object v0, p0, $sequencerOwner + iget-object v0, v0, $sequencer + if-eqz v0, :missing + check-cast v0, $sequencerType + iget-object v0, v0, $sequencerField + instance-of p0, v0, $wrapperType + if-eqz p0, :missing + check-cast v0, $wrapperType + return-object v0 + + :missing + const/4 v0, 0x0 + return-object v0 + """ + ) + + // Selection can commit after the shared video-ID event. Observe native completion + // without passing a receiver register that the native method may have reused. + val refreshMethod = nativeQueueRefreshFingerprint(refresh).method + refreshMethod.findInstructionIndicesReversedOrThrow(Opcode.RETURN_VOID).forEach { index -> + refreshMethod.addInstructionsAtControlFlowLabel( + index, + "invoke-static {}, $EXTENSION_CLASS->onNativeQueueSelection()V" + ) + } +} diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/PlaylistMenuHook.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/PlaylistMenuHook.kt new file mode 100644 index 0000000000..5186299355 --- /dev/null +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/PlaylistMenuHook.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/3953 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.patches.youtube.interaction.nativeplaylistqueue + +import app.morphe.patcher.extensions.InstructionExtensions.getInstruction +import app.morphe.patcher.patch.BytecodePatchContext +import app.morphe.patcher.util.getReference +import app.morphe.patcher.util.smali.ExternalLabel +import app.morphe.util.addInstructionsAtControlFlowLabel +import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction +import com.android.tools.smali.dexlib2.iface.reference.MethodReference + +/** + * Observe the shared native header/row service-menu callback. The callback already validates + * and logs the command before this point. Local queue actions then call the same sheet dismissal + * used by the native path. Unowned and other endpoints continue through the original call. + */ +internal fun installPlaylistMenuDispatch(context: BytecodePatchContext, callbackType: String) = with(context) { + val dispatch = playlistMenuDispatchFingerprint(callbackType).match() + val callback = dispatch.instructionMatches[1] + val dismissal = dispatch.instructionMatches[2] + + val endpointType = callback.instruction.getReference()!!.parameterTypes.first().toString() + + installQueueSavePicker(this, endpointType) + + val registers = callback.getInstruction() + val endpoint = registers.registerD + dispatch.method.addInstructionsAtControlFlowLabel( + callback.index, + """ + invoke-static {v$endpoint, v${registers.registerE}}, $EXTENSION_CLASS->onQueueMenuCommand(Lcom/google/protobuf/MessageLite;Ljava/util/Map;)Lcom/google/protobuf/MessageLite; + move-result-object v$endpoint + if-nez v$endpoint, :native_menu_dispatch + goto :native_menu_dismiss + + :native_menu_dispatch + check-cast v$endpoint, $endpointType + """, + ExternalLabel("native_menu_dismiss", dispatch.method.getInstruction(dismissal.index)) + ) +} diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/PlaylistNavigationHook.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/PlaylistNavigationHook.kt new file mode 100644 index 0000000000..6f43b37a06 --- /dev/null +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/PlaylistNavigationHook.kt @@ -0,0 +1,79 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/3953 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.patches.youtube.interaction.nativeplaylistqueue + +import app.morphe.patcher.patch.BytecodePatchContext +import app.morphe.patcher.util.getReference +import app.morphe.patches.youtube.layout.playlistautoplay.NavigationIntentEnumFingerprint +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.iface.reference.FieldReference +import com.android.tools.smali.dexlib2.iface.reference.TypeReference + +/** Resolve the shared native navigation owner for controls and the playback-engine bridge. */ +internal fun installPlaylistNavigation( + context: BytecodePatchContext, + controllerType: String +) = with(context) { + val enumType = NavigationIntentEnumFingerprint.originalClassDef.type + val wrapperType = playlistNavigationRequestFingerprint(enumType).originalClassDef.type + + // The playlist controls are created with the playlist's own navigator. + val creation = playlistControlsCreationFingerprint(controllerType).match() + val navigatorType = creation.instructionMatches[4].instruction.getReference()!!.type + val controlsField = creation.instructionMatches.last().instruction.getReference()!! + + // 21.28+ declares the controls as an interface with two implementations. + val controlsImplementations = if (AccessFlags.INTERFACE.isSet(classDefBy(controlsField.type).accessFlags)) { + playlistControlsImplementationFingerprint(navigatorType, controlsField.type).matchAll() + } else { + listOf(playlistControlsConstructorFingerprint(navigatorType, controlsField.type).match()) + } + + check(controlsImplementations.isNotEmpty()) { "Native playlist queue: playlist controls are missing" } + + controlsImplementations.forEach { constructor -> + val navigatorField = constructor.instructionMatches.single().instruction.getReference()!! + val controls = mutableClassDefBy(constructor.originalClassDef.type) + + controls.interfaces.add(NAVIGATOR_OWNER_INTERFACE) + controls.addBridgeMethod( + "patch_getNavigator", + emptyList(), + "Ljava/lang/Object;", + 1, + """ + iget-object v0, p0, $navigatorField + return-object v0 + """ + ) + } + + mutableClassDefBy(controllerType).addBridgeMethod( + "patch_getNavigator", + emptyList(), + "Ljava/lang/Object;", + 2, + """ + iget-object v0, p0, $controlsField + instance-of v1, v0, $NAVIGATOR_OWNER_INTERFACE + if-eqz v1, :missing + check-cast v0, $NAVIGATOR_OWNER_INTERFACE + invoke-interface {v0}, $NAVIGATOR_OWNER_INTERFACE->patch_getNavigator()Ljava/lang/Object; + move-result-object v0 + return-object v0 + + :missing + const/4 v0, 0x0 + return-object v0 + """ + ) + + installPlaylistNotification(context, wrapperType, enumType) + installPlaylistRepeat(context, navigatorType) + installPlaylistPlayerControls(context, navigatorType, enumType, wrapperType) +} diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/PlaylistNotificationControlsHook.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/PlaylistNotificationControlsHook.kt new file mode 100644 index 0000000000..567e17b98d --- /dev/null +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/PlaylistNotificationControlsHook.kt @@ -0,0 +1,121 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/3953 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.patches.youtube.interaction.nativeplaylistqueue + +import app.morphe.patcher.extensions.InstructionExtensions.getInstruction +import app.morphe.patcher.patch.BytecodePatchContext +import app.morphe.patcher.util.cloneParameters +import app.morphe.patcher.util.findFreeRegister +import app.morphe.patcher.util.getReference +import app.morphe.patcher.util.matchSingle +import app.morphe.util.addInstructionsAtControlFlowLabel +import app.morphe.util.findInstructionIndicesReversedOrThrow +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction +import com.android.tools.smali.dexlib2.iface.reference.FieldReference +import com.android.tools.smali.dexlib2.iface.reference.MethodReference + +/** Publish queue capabilities before native notification actions and playback state are built. */ +internal fun installPlaylistNotificationControls(context: BytecodePatchContext, ownerType: String) = with(context) { + val modelType = NotificationSkipActionsFingerprint + .instructionMatches.first().instruction.getReference()!!.type + val setter = notificationCapabilitiesSetterFingerprint(modelType).originalMethod + + // The original adapter returns V. The newer adapter returns its change flags. + val adapters = notificationCapabilitiesFingerprint(setter).matchAll() + check(adapters.map { it.originalMethod.returnType }.sorted() == listOf("I", "V")) { + "Native playlist queue: expected both native notification capability adapters" + } + + val playbackState = notificationPlaybackStateFingerprint( + adapters.single { it.originalMethod.returnType == "V" }.originalClassDef.type + ).originalMethod + + adapters.forEach { match -> + val adapterType = match.originalClassDef.type + val adapter = mutableClassDefBy(adapterType) + val constructor = notificationAdapterConstructorFingerprint(adapterType, ownerType).matchSingle() + val owner = constructor.instructionMatches.single().instruction.getReference()!! + val computation = match.originalMethod + val dispatch = if (computation.returnType == "I") { + notificationCapabilitiesDispatchFingerprint(computation).matchAll().map { + it.instructionMatches.last().instruction.getReference()!! + }.distinct().single() + } else null + + adapter.interfaces.add(NOTIFICATION_CONTROLS_INTERFACE) + adapter.addBridgeMethod( + "patch_getQueueMediaControls", + emptyList(), + MEDIA_CONTROLS_INTERFACE, + 1, + """ + iget-object v0, p0, $owner + return-object v0 + """ + ) + + // Recompute from the original adapter inputs on every refresh. Releasing + // the queue therefore restores native capabilities without caching flags. + adapter.addBridgeMethod( + "patch_refreshQueueNotification", + emptyList(), + "V", + 1, + """ + invoke-direct {p0}, $computation + ${if (dispatch == null) "" else "move-result v0\ninvoke-direct {p0, v0}, $dispatch"} + return-void + """ + ) + + constructor.method.findInstructionIndicesReversedOrThrow(Opcode.RETURN_VOID).forEach { index -> + constructor.method.addInstructionsAtControlFlowLabel( + index, + "invoke-static/range {p0 .. p0}, $EXTENSION_CLASS->onNotificationControlsCreated($NOTIFICATION_CONTROLS_INTERFACE)V" + ) + } + + // NEW clears the model's skip flags after the sequencer commits selection. + // In background playback there need not be another watch-screen update. + // Recompute only after the original state handler finishes clearing its + // model and delegates; preserve its receiver because native code reuses p0. + val stateMethod = notificationPlaybackStateFingerprint(adapterType, playbackState).matchSingle() + .method.cloneParameters() + stateMethod.findInstructionIndicesReversedOrThrow(Opcode.RETURN_VOID).forEach { index -> + stateMethod.addInstructionsAtControlFlowLabel( + index, + "invoke-static/range {p0 .. p0}, $EXTENSION_CLASS->onNotificationPlaybackChanged($NOTIFICATION_CONTROLS_INTERFACE)V" + ) + } + + // Both original computations reuse p0 before the setter. Keep the receiver + // and match the call again after cloning instead of retaining its index. + match.method.cloneParameters() + val update = notificationCapabilitiesFingerprint(setter, adapterType).matchSingle() + val method = update.method + val index = update.instructionMatches.single().index + val call = method.getInstruction(index) + val previous = call.registerD + val next = call.registerE + val capabilities = method.findFreeRegister(index) + method.addInstructionsAtControlFlowLabel( + index, + """ + invoke-static/range {p0 .. p0}, $EXTENSION_CLASS->notificationNavigationAvailability($NOTIFICATION_CONTROLS_INTERFACE)I + move-result v$capabilities + if-ltz v$capabilities, :native_capabilities + and-int/lit8 v$previous, v$capabilities, 0x1 + ushr-int/lit8 v$next, v$capabilities, 0x1 + + :native_capabilities + nop + """ + ) + } +} diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/PlaylistNotificationHook.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/PlaylistNotificationHook.kt new file mode 100644 index 0000000000..a56dddcd1a --- /dev/null +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/PlaylistNotificationHook.kt @@ -0,0 +1,193 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/3953 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.patches.youtube.interaction.nativeplaylistqueue + +import app.morphe.patcher.patch.BytecodePatchContext +import app.morphe.patcher.util.findFreeRegister +import app.morphe.patcher.util.getReference +import app.morphe.util.addInstructionsAtControlFlowLabel +import com.android.tools.smali.dexlib2.iface.reference.FieldReference +import com.android.tools.smali.dexlib2.iface.reference.MethodReference +import com.android.tools.smali.dexlib2.iface.reference.TypeReference + +/** Notification skips must load through the service, not a deferred watch-screen endpoint. */ +internal fun installPlaylistNotification( + context: BytecodePatchContext, + wrapperType: String, + enumType: String +) = with(context) { + val next = playlistNavigationIntentFingerprint(enumType, "NEXT") + .instructionMatches.last().instruction.getReference()!! + val nextRequest = playlistNavigationSingletonFingerprint(wrapperType, next) + .instructionMatches.last().instruction.getReference()!! + + // The media owner's Next dispatch, after logging and before the optional foreground callback. + val notification = notificationNextDispatchFingerprint(nextRequest).match() + val owner = notification.originalClassDef.type + + // The media session callback delegates its skips to the media owner. + val callback = notificationSkipCallbackFingerprint("onSkipToNext") + .instructionMatches.last().instruction.getReference()!! + val callbackDelegate = notificationDelegateFingerprint( + callback, owner, notification.originalMethod.name + ).match() + + // The foreground callback that handles Next while a watch screen is open. + val foregroundCallback = notification.instructionMatches.first().instruction + .getReference()!! + val foregroundType = notificationForegroundSkipFingerprint( + foregroundCallback.type, nextRequest + ).originalClassDef.type + + // The same paths for Previous. + val previousCallback = notificationSkipCallbackFingerprint("onSkipToPrevious") + .instructionMatches.last().instruction.getReference()!! + val previousDelegate = notificationDelegateFingerprint( + previousCallback, + owner, + delegateType = callbackDelegate.originalClassDef.type + ).instructionMatches[1].instruction.getReference()!! + val previous = notificationPreviousDispatchFingerprint(previousDelegate).match() + val previousForeground = previous.instructionMatches.first().instruction.getReference()!! + val previousIntent = playlistNavigationIntentFingerprint(enumType, "PREVIOUS") + .instructionMatches.last().instruction.getReference()!! + val previousRequest = playlistNavigationSingletonFingerprint(wrapperType, previousIntent) + .instructionMatches.last().instruction.getReference()!! + val previousForegroundType = notificationForegroundSkipFingerprint( + previousForeground.type, previousRequest + ).originalClassDef.type + + // The playback service that loads the queue's next and previous videos. + val provider = notification.instructionMatches[2].instruction.getReference()!! + val getProvider = notification.instructionMatches[3].instruction.getReference()!! + val serviceType = notification.instructionMatches[5].instruction.getReference()!!.type + + val service = playlistServiceNavigatorFingerprint(wrapperType).match() + val navigatorType = service.originalClassDef.type + val playerFields = service.instructionMatches.take(3).map { + it.instruction.getReference()!! + } + val videoId = service.instructionMatches[4].instruction.getReference()!! + check(playerFields.first().definingClass == navigatorType && + playerFields.zipWithNext().all { (parent, child) -> parent.type == child.definingClass }) { + "Native playlist queue: current player must belong to the playback service" + } + + installNativeQueueEngine(context, navigatorType, enumType) + + fun bridge(type: String, name: String, parameters: List, returnType: String, locals: Int, body: String) = + mutableClassDefBy(type).addBridgeMethod(name, parameters, returnType, locals, body) + + mutableClassDefBy(navigatorType).interfaces.add(SERVICE_NAVIGATOR_INTERFACE) + bridge( + navigatorType, + "patch_getVideoId", + emptyList(), + "Ljava/lang/String;", + 1, + """ + iget-object v0, p0, ${playerFields[0]} + iget-object v0, v0, ${playerFields[1]} + iget-object v0, v0, ${playerFields[2]} + if-eqz v0, :missing + # 21.28 declares the player field as Object. + check-cast v0, ${videoId.definingClass} + invoke-interface {v0}, $videoId + move-result-object v0 + return-object v0 + + :missing + const/4 v0, 0x0 + return-object v0 + """ + ) + + val canNavigate = playlistServiceCanNavigateFingerprint( + navigatorType, wrapperType + ).originalMethod + for ((name, resultType, target) in listOf( + Triple("patch_navigateQueue", "V", service.originalMethod), + Triple("patch_canNavigateQueue", "Z", canNavigate) + )) { + val result = if (resultType == "Z") "move-result v0\nreturn v0" else "return-void" + bridge( + navigatorType, + name, + listOf("Z"), + resultType, + 1, + """ + if-eqz p1, :previous + sget-object v0, $nextRequest + goto :dispatch + + :previous + sget-object v0, $previousRequest + + :dispatch + invoke-virtual {p0, v0}, $target + $result + """ + ) + } + + mutableClassDefBy(owner).interfaces.add(MEDIA_CONTROLS_INTERFACE) + bridge( + owner, + "patch_getQueueNavigator", + emptyList(), + SERVICE_NAVIGATOR_INTERFACE, + 2, + """ + iget-object v0, p0, $foregroundCallback + if-eqz v0, :previous + instance-of v0, v0, $foregroundType + if-eqz v0, :missing + + :previous + iget-object v0, p0, $previousForeground + if-eqz v0, :local + instance-of v0, v0, $previousForegroundType + if-eqz v0, :missing + + :local + iget-object v0, p0, $provider + invoke-interface {v0}, $getProvider + move-result-object v0 + check-cast v0, $serviceType + instance-of v1, v0, $SERVICE_NAVIGATOR_INTERFACE + if-eqz v1, :missing + check-cast v0, $SERVICE_NAVIGATOR_INTERFACE + return-object v0 + + :missing + const/4 v0, 0x0 + return-object v0 + """ + ) + + installPlaylistNotificationControls(context, owner) + + for ((match, hook) in listOf(notification to "onNotificationNext", previous to "onNotificationPrevious")) { + val method = match.method + val index = match.instructionMatches.first().index + val result = method.findFreeRegister(index) + method.addInstructionsAtControlFlowLabel( + index, + """ + invoke-static/range {p0 .. p0}, $EXTENSION_CLASS->$hook($MEDIA_CONTROLS_INTERFACE)Z + move-result v$result + if-eqz v$result, :native_skip + return-void + + :native_skip + nop + """ + ) + } +} diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/PlaylistPanelHook.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/PlaylistPanelHook.kt new file mode 100644 index 0000000000..c8853716bf --- /dev/null +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/PlaylistPanelHook.kt @@ -0,0 +1,280 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/3953 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.patches.youtube.interaction.nativeplaylistqueue + +import app.morphe.patcher.extensions.InstructionExtensions.addInstruction +import app.morphe.patcher.patch.BytecodePatchContext +import app.morphe.patcher.util.cloneParameters +import app.morphe.patcher.util.findFreeRegister +import app.morphe.patcher.util.getReference +import app.morphe.patcher.util.numberOfParameterRegistersLogical +import app.morphe.patcher.util.p0Register +import app.morphe.util.addInstructionsAtControlFlowLabel +import app.morphe.util.findFieldFromToString +import app.morphe.util.findInstructionIndicesReversedOrThrow +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction +import com.android.tools.smali.dexlib2.iface.reference.FieldReference + +// The native model factory initializes queueMode to 1 before accepting a watch response. +// Keep that neutral mode: rows use ordinary watch endpoints, without attaching +// a native playlist navigator through navigationSets. +private const val DEFAULT_QUEUE_MODE = 1 + +/** Bridge the accepted watch model; the playback engine is attached separately at runtime. */ +internal fun installPlaylistPanel(context: BytecodePatchContext) = with(context) { + val description = PlaylistModelDescriptionFingerprint.originalMethod + val modelType = description.definingClass + val responseVideoField = description.findFieldFromToString("{videoId=") + val playlistField = description.findFieldFromToString(", playlistId=") + val panelField = description.findFieldFromToString(", playlistPanel=") + val navigationField = description.findFieldFromToString(", navigationSets=") + val panelType = panelField.type + val controllerType = PlaylistControllerConstructorFingerprint.originalClassDef.type + val controller = mutableClassDefBy(controllerType) + + val accept = acceptedPlaylistModelFingerprint(controllerType, modelType, responseVideoField) + val modelField = accept.instructionMatches.last().instruction.getReference()!! + val activePlaylistField = playlistControllerIdentityFingerprint(accept.originalMethod) + .instructionMatches.first().instruction.getReference()!! + val activeVideoMatch = accept.instructionMatches[2] + val activeVideoField = activeVideoMatch.instruction.getReference()!! + check(activeVideoMatch.getInstruction().registerB == accept.originalMethod.p0Register) { + "Native playlist queue: active video must belong to the playlist controller" + } + + // The collapsed panel view, whose click opens the playlist. + val collapsedView = playlistControlsCreationFingerprint(controllerType) + .instructionMatches.first().instruction.getReference()!! + + val render = playlistPanelRenderFingerprint(modelField) + val transition = playlistVideoTransitionFingerprint(activeVideoField) + + // Ordinary videos can have no playlist model at all. Use the native builder's + // defaults, including required tracking parameters, rather than inventing them. + val buildModel = playlistModelBuildFingerprint(modelType).originalMethod + val modelFactory = playlistModelFactoryFingerprint(modelType, buildModel.definingClass) + .originalMethod + + // Constructor parameter positions are derived from the actual field stores. + val constructor = playlistModelConstructorFingerprint(modelType) + val constructorRegisters = constructor.originalMethod.p0Register + val constructorFields = constructor.instructionMatches.sortedBy { + it.getInstruction().registerA + }.map { it.instruction.getReference()!! } + check( + constructor.instructionMatches.map { + it.getInstruction().registerA - constructorRegisters - 1 + }.sorted() == constructorFields.indices.toList() + ) { "Native playlist queue: each model parameter must be stored directly" } + + fun addBridge(name: String, parameters: List, returnType: String, locals: Int, body: String) = + controller.addBridgeMethod(name, parameters, returnType, locals, body) + + controller.interfaces.add(CONTROLLER_INTERFACE) + mutableClassDefBy(modelType).apply { + interfaces.add(PLAYLIST_MODEL_INTERFACE) + addBridgeMethod( + "patch_getPlaylistId", + emptyList(), + "Ljava/lang/String;", + 1, + """ + iget-object v0, p0, $playlistField + return-object v0 + """ + ) + } + + // A watch response can omit its video ID. Use the native stale-response guard's + // active video instead of treating an optional response field as playback state. + addBridge( + "patch_getVideoId", + emptyList(), + "Ljava/lang/String;", + 1, + """ + iget-object v0, p0, $activeVideoField + if-nez v0, :done + const-string v0, "" + + :done + return-object v0 + """ + ) + + addBridge( + "patch_getPlaylistId", + emptyList(), + "Ljava/lang/String;", + 1, + """ + iget-object v0, p0, $modelField + if-eqz v0, :empty + iget-object v0, v0, $playlistField + if-nez v0, :done + + :empty + const-string v0, "" + + :done + return-object v0 + """ + ) + + addBridge( + "patch_getModel", + emptyList(), + "Ljava/lang/Object;", + 1, + """ + iget-object v0, p0, $modelField + return-object v0 + """ + ) + + addBridge( + "patch_getPanel", + emptyList(), + "Ljava/lang/Object;", + 1, + """ + iget-object v0, p0, $modelField + if-eqz v0, :done + iget-object v0, v0, $panelField + + :done + return-object v0 + """ + ) + + addBridge( + "patch_isAttached", + emptyList(), + "Z", + 1, + """ + iget-object v0, p0, $collapsedView + invoke-virtual {v0}, Landroid/view/View;->isAttachedToWindow()Z + move-result v0 + return v0 + """ + ) + + val copyArguments = constructorFields.mapIndexed { index, field -> + when { + field == responseVideoField -> "iget-object v${index + 5}, p0, $activeVideoField" + field == playlistField -> "const-string v${index + 5}, \"\"" + field == panelField -> "move-object v${index + 5}, v1" + field == navigationField -> "const/4 v${index + 5}, 0x0" + field.type == "I" -> "const/4 v${index + 5}, $DEFAULT_QUEUE_MODE" + else -> "iget-object v${index + 5}, v0, $field" + } + }.joinToString("\n") + + // The model arguments use v5-v11. With 12 locals, p0 and p1 remain 4-bit registers. + addBridge( + "patch_updatePanel", + listOf("[B"), + "Ljava/lang/Object;", + 12, + """ + iget-object v0, p0, $modelField + if-nez v0, :model_ready + invoke-static {}, $modelFactory + move-result-object v0 + invoke-virtual {v0}, $buildModel + move-result-object v0 + + :model_ready + ${parseProtoInstructions(panelType, bytes = "p1", result = "v1", registry = "v2")} + check-cast v1, $panelType + new-instance v4, $modelType + $copyArguments + invoke-direct/range {v4 .. v11}, ${constructor.originalMethod} + invoke-direct {p0, v4}, ${accept.originalMethod} + iget-object v0, p0, $modelField + if-ne v0, v4, :rejected + const-string v1, "" + iput-object v1, p0, $activePlaylistField + return-object v4 + + :rejected + const/4 v0, 0x0 + return-object v0 + """ + ) + + addBridge( + "patch_restorePanel", + List(2) { "Ljava/lang/Object;" }, + "V", + 1, + """ + iget-object v0, p0, $modelField + if-ne v0, p1, :done + check-cast p2, $modelType + const-string v0, "" + if-eqz p2, :identity_ready + iget-object v0, p2, $playlistField + + :identity_ready + iput-object v0, p0, $activePlaylistField + iput-object p2, p0, $modelField + invoke-virtual/range {p0 .. p0}, ${render.originalMethod} + + :done + return-void + """ + ) + + installPlaylistRowPlayback(context, modelField, panelField) + + // Preserve every native rejection before this assignment. In particular, a stale model + // must not claim the current controller or overwrite our local insertion state. + val acceptMethod = accept.method.cloneParameters().apply { + // Cloning adjusts match indexes. + val storeIndex = accept.instructionMatches.last().index + numberOfParameterRegistersLogical + val p0Register = p0Register + val guardRegister = findFreeRegister( + storeIndex, p0Register, p0Register + 1 + ) + addInstruction( + storeIndex + 1, + "invoke-static/range {p0 .. p0}, $EXTENSION_CLASS->onControllerUpdated($CONTROLLER_INTERFACE)V" + ) + + addInstructionsAtControlFlowLabel( + storeIndex, + """ + invoke-static/range {p0 .. p1}, $EXTENSION_CLASS->shouldIgnorePlaylistModel($CONTROLLER_INTERFACE$PLAYLIST_MODEL_INTERFACE)Z + move-result v$guardRegister + if-eqz v$guardRegister, :accept_model + return-void + + :accept_model + nop + """ + ) + } + + // Observe completed transitions, including native early returns that leave no model. + // Keep this event distinct from a model bind: Reload can reopen a standalone + // video without a playlist model, but it still has a native playback descriptor. + // Do not publish between the native assignments of its video/playlist identity fields. + // The native transition reuses p0 as a local on some exits. Preserve the + // original receiver before adding callbacks at those returns. + transition.method.cloneParameters().apply { + findInstructionIndicesReversedOrThrow(Opcode.RETURN_VOID).forEach { index -> + addInstructionsAtControlFlowLabel( + index, + "invoke-static/range {p0 .. p0}, $EXTENSION_CLASS->onPlaybackTransition($CONTROLLER_INTERFACE)V" + ) + } + } +} diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/PlaylistPlayerControlsHook.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/PlaylistPlayerControlsHook.kt new file mode 100644 index 0000000000..3e77e67aed --- /dev/null +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/PlaylistPlayerControlsHook.kt @@ -0,0 +1,121 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/3953 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.patches.youtube.interaction.nativeplaylistqueue + +import app.morphe.patcher.extensions.InstructionExtensions.addInstruction +import app.morphe.patcher.patch.BytecodePatchContext +import app.morphe.patcher.util.cloneParameters +import app.morphe.patcher.util.findFreeRegister +import app.morphe.patcher.util.getReference +import app.morphe.util.addInstructionsAtControlFlowLabel +import app.morphe.util.findInstructionIndicesReversedOrThrow +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction +import com.android.tools.smali.dexlib2.iface.reference.FieldReference + +/** Keep native listeners and visibility, including controls that bypass the navigator for history. */ +internal fun installPlaylistPlayerControls( + context: BytecodePatchContext, + navigatorType: String, + enumType: String, + wrapperType: String +) = with(context) { + val availability = listOf("NEXT", "PREVIOUS").map { intent -> + val enumField = playlistNavigationIntentFingerprint(enumType, intent) + .instructionMatches.last().instruction.getReference()!! + val request = playlistNavigationSingletonFingerprint(wrapperType, enumField) + .instructionMatches.last().instruction.getReference()!! + request to playlistControlAvailabilityFingerprint(navigatorType, request) + } + + val controlsType = availability.first().second.originalClassDef.type + check(availability.all { it.second.originalClassDef.type == controlsType }) { + "Native playlist queue: Next/Previous must share their native controls owner" + } + + val controls = mutableClassDefBy(controlsType) + val navigatorField = availability.first().second.instructionMatches[4] + .instruction.getReference()!! + + controls.interfaces.add(PLAYER_CONTROLS_INTERFACE) + controls.addBridgeMethod( + "patch_getNavigator", + emptyList(), + "Ljava/lang/Object;", + 1, + """ + iget-object v0, p0, $navigatorField + return-object v0 + """ + ) + + // Recompute both cached enablement values, even when native history delegates are present. + controls.addBridgeMethod( + "patch_refreshNavigation", + emptyList(), + "V", + 1, + availability.joinToString("\n") { (_, match) -> "invoke-direct {p0}, ${match.originalMethod}" } + + "\nreturn-void" + ) + + val constructor = playlistPlayerControlsConstructorFingerprint(controlsType, navigatorType).method + constructor.findInstructionIndicesReversedOrThrow(Opcode.RETURN_VOID).forEach { index -> + constructor.addInstruction( + index, + "invoke-static/range {p0 .. p0}, $EXTENSION_CLASS->onPlayerControlsCreated($PLAYER_CONTROLS_INTERFACE)V" + ) + } + + availability.forEachIndexed { direction, (request, match) -> + val method = match.method + val compare = match.instructionMatches.last().getInstruction() + val cachedMatch = match.instructionMatches[match.instructionMatches.lastIndex - 1] + val cached = cachedMatch.getInstruction() + check(compare.registerA == cached.registerA || compare.registerB == cached.registerA) { + "Native playlist queue: control availability must compare the cached and computed values" + } + + val originalRegister = if (compare.registerA == cached.registerA) compare.registerB else compare.registerA + val insertIndex = cachedMatch.index + val freeRegister = method.findFreeRegister(insertIndex) + method.addInstructionsAtControlFlowLabel( + insertIndex, + """ + const/4 v$freeRegister, ${if (direction == 0) 1 else 0} + invoke-static {p0, v$freeRegister, v$originalRegister}, $EXTENSION_CLASS->playerControlAvailability(${PLAYER_CONTROLS_INTERFACE}ZZ)Z + move-result v$originalRegister + """ + ) + + // The delegate read joins both paths of the optional native logging callback. + // Intercept here so logging runs, but a local queue cannot fall into history/restart. + val delegateField = match.instructionMatches.first().instruction.getReference()!! + var press = playlistControlPressFingerprint(controlsType, delegateField, request) + if (press.method.implementation!!.registerCount <= 1) { + press.method.cloneParameters() + press = playlistControlPressFingerprint(controlsType, delegateField, request) + } + + val pressIndex = press.instructionMatches[1].index + val resultRegister = press.method.findFreeRegister(pressIndex) + val callback = if (direction == 0) "onNextPressed" else "onPreviousPressed" + press.method.addInstructionsAtControlFlowLabel( + pressIndex, + """ + invoke-static/range {p0 .. p0}, $EXTENSION_CLASS->$callback($PLAYER_CONTROLS_INTERFACE)Z + move-result v$resultRegister + if-eqz v$resultRegister, :native_press + return-void + + :native_press + nop + """ + ) + } +} diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/PlaylistRemovalHook.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/PlaylistRemovalHook.kt new file mode 100644 index 0000000000..4a0316b310 --- /dev/null +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/PlaylistRemovalHook.kt @@ -0,0 +1,108 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/3953 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.patches.youtube.interaction.nativeplaylistqueue + +import app.morphe.patcher.extensions.InstructionExtensions.addInstruction +import app.morphe.patcher.patch.BytecodePatchContext +import app.morphe.patcher.util.getReference +import app.morphe.util.addInstructionsAtControlFlowLabel +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction +import com.android.tools.smali.dexlib2.iface.reference.FieldReference +import com.android.tools.smali.dexlib2.iface.reference.MethodReference + +/** Retain native button validation and dispatch every unrelated command unchanged. */ +internal fun installPlaylistRemoval(context: BytecodePatchContext) = with(context) { + val dispatch = NativeButtonDispatchFingerprint.match() + val serviceCall = dispatch.instructionMatches.single() + val registers = serviceCall.instruction as FiveRegisterInstruction + val callback = serviceCall.instruction.getReference()!! + val endpointType = callback.parameterTypes.first() + + installQueueUndo(this, callback) + + // Reuse the endpoint result register, keeping every live native local intact. + // Null consumes our local command; otherwise restore the native endpoint type. + dispatch.method.addInstructionsAtControlFlowLabel( + serviceCall.index, + """ + invoke-static {v${registers.registerD}, v${registers.registerE}}, $EXTENSION_CLASS->onQueueMenuCommand(Lcom/google/protobuf/MessageLite;Ljava/util/Map;)Lcom/google/protobuf/MessageLite; + move-result-object v${registers.registerD} + if-nez v${registers.registerD}, :native_dispatch + return-void + + :native_dispatch + check-cast v${registers.registerD}, $endpointType + """ + ) +} + +/** Preserve native swipe drawing when an attached playing row becomes removable. */ +internal fun installPlaylistSwipeBackground(context: BytecodePatchContext, listener: FieldReference) = with(context) { + val attached = PlaylistSwipeAttachedFingerprint + val foregroundStore = attached.instructionMatches.first() + val backgroundRead = attached.instructionMatches[1].instruction as FiveRegisterInstruction + val backgroundStore = attached.instructionMatches.last() + val foregroundRegisters = foregroundStore.instruction as TwoRegisterInstruction + val backgroundRegisters = backgroundStore.instruction as TwoRegisterInstruction + check(foregroundRegisters.registerA == backgroundRead.registerC && + foregroundRegisters.registerB == backgroundRegisters.registerB) { + "Native playlist queue: swipe drawable must belong to the attached foreground" + } + + val foreground = foregroundStore.instruction.getReference()!! + val cachedBackground = backgroundStore.instruction.getReference()!! + val swipe = mutableClassDefBy(attached.originalClassDef.type) + swipe.interfaces.add(SWIPE_BACKGROUND_INTERFACE) + + // Before attachment there is no foreground yet; the native attachment + // callback will initialize both fields normally. Never replace its drawable. + swipe.addBridgeMethod( + "patch_refreshQueueSwipeBackground", + emptyList(), + "V", + 1, + """ + iget-object v0, p0, $foreground + if-eqz v0, :done + invoke-virtual {v0}, Landroid/view/View;->getBackground()Landroid/graphics/drawable/Drawable; + move-result-object v0 + iput-object v0, p0, $cachedBackground + + :done + return-void + """ + ) + + val update = PlaylistRowBackgroundFingerprint + val holder = mutableClassDefBy(update.originalClassDef.type) + val swipeField = PlaylistRowSwipeLayoutFingerprint + .instructionMatches.single().instruction.getReference()!! + val bridge = holder.addBridgeMethod( + "patch_refreshQueueSwipeBackground", + emptyList(), + "V", + 2, + """ + iget-object v0, p0, $listener + iget-object v1, p0, $swipeField + invoke-static {v0, v1}, $EXTENSION_CLASS->onSwipeBackgroundChanged($ROW_CLICK_INTERFACE$SWIPE_BACKGROUND_INTERFACE)V + return-void + """, + AccessFlags.PRIVATE.value or AccessFlags.FINAL.value + ) + + // Read the receiver from the foreground load, while it is still live. The + // native method later reuses p0 for a color, so its final return is unsafe. + val receiver = (update.instructionMatches.first().instruction as TwoRegisterInstruction).registerB + update.method.addInstruction( + update.instructionMatches[1].index + 1, + "invoke-direct/range {v$receiver .. v$receiver}, $bridge" + ) +} diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/PlaylistReorderHook.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/PlaylistReorderHook.kt new file mode 100644 index 0000000000..706885a4ef --- /dev/null +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/PlaylistReorderHook.kt @@ -0,0 +1,94 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/3953 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.patches.youtube.interaction.nativeplaylistqueue + +import app.morphe.patcher.extensions.InstructionExtensions.addInstruction +import app.morphe.patcher.patch.BytecodePatchContext +import app.morphe.patcher.util.getReference +import app.morphe.patcher.util.p0Register +import app.morphe.util.addInstructionsAtControlFlowLabel +import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction +import com.android.tools.smali.dexlib2.iface.reference.MethodReference + +/** Observe the existing drag transaction; retain native animation and accessibility. */ +internal fun installPlaylistReordering(context: BytecodePatchContext, videoType: String) = with(context) { + val holderType = PlaylistDragBinderFingerprint.originalMethod.definingClass + val start = playlistDragStartFingerprint(holderType) + val end = playlistDragEndFingerprint(holderType) + check(start.originalMethod.parameterTypes.take(2) == end.originalMethod.parameterTypes.take(2)) { + "Native playlist queue: drag callbacks disagree about their holder/adapter types" + } + + for (callback in listOf(start, end)) { + val predicate = callback.instructionMatches.first().instruction as TwoRegisterInstruction + val receiver = callback.originalMethod.p0Register + check(setOf(predicate.registerA, predicate.registerB) == setOf(receiver, receiver + 1)) { + "Native playlist queue: drag notification must be guarded by holder identity" + } + } + + val adapterType = start.originalMethod.parameterTypes[1].toString() + + // Native edit events inspect the adapter's wrapper through this row interface. + // The native server-edit branch additionally requires playlistSetVideoId; our + // renderer omits it, so normal account edits need no interception or replacement. + val rowGetter = playlistEditEventsFingerprint(videoType) + .instructionMatches.single().instruction.getReference()!! + + val adapter = mutableClassDefBy(adapterType) + adapter.interfaces.add(REORDER_ITEMS_INTERFACE) + adapter.addBridgeMethod( + "patch_getSize", + emptyList(), + "I", + 1, + """ + invoke-interface {p0}, Ljava/util/List;->size()I + move-result v0 + return v0 + """ + ) + + adapter.addBridgeMethod( + "patch_getQueueRow", + listOf("I"), + "Ljava/lang/Object;", + 2, + """ + invoke-interface {p0, p1}, Ljava/util/List;->get(I)Ljava/lang/Object; + move-result-object v0 + instance-of v1, v0, ${rowGetter.definingClass} + if-eqz v1, :missing + check-cast v0, ${rowGetter.definingClass} + invoke-interface {v0}, $rowGetter + move-result-object v0 + return-object v0 + + :missing + const/4 v0, 0x0 + return-object v0 + """ + ) + + // Both callbacks first reject notifications for another holder. Enter on the + // guarded branch, before start's visual update reuses p0/p1 as drawable locals. + start.method.addInstructionsAtControlFlowLabel( + start.instructionMatches[1].index, + "invoke-static/range {p1 .. p3}, $EXTENSION_CLASS->onReorderStarted(Ljava/lang/Object;${REORDER_ITEMS_INTERFACE}I)V" + ) + + end.method.addInstructionsAtControlFlowLabel( + end.instructionMatches[1].index, + "invoke-static/range {p1 .. p4}, $EXTENSION_CLASS->onReorderEnded(Ljava/lang/Object;${REORDER_ITEMS_INTERFACE}II)V" + ) + + playlistDragUnbindFingerprint(holderType).method.addInstruction( + 0, + "invoke-static/range {p0 .. p0}, $EXTENSION_CLASS->onReorderHolderUnbound(Ljava/lang/Object;)V" + ) +} diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/PlaylistRepeatHook.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/PlaylistRepeatHook.kt new file mode 100644 index 0000000000..03466bf0b2 --- /dev/null +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/PlaylistRepeatHook.kt @@ -0,0 +1,132 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/3953 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.patches.youtube.interaction.nativeplaylistqueue + +import app.morphe.patcher.patch.BytecodePatchContext +import app.morphe.patcher.util.cloneParameters +import app.morphe.patcher.util.findFreeRegister +import app.morphe.patcher.util.getReference +import app.morphe.util.addInstructionsAtControlFlowLabel +import app.morphe.util.findInstructionIndicesReversedOrThrow +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction +import com.android.tools.smali.dexlib2.iface.reference.FieldReference + +/** Keep native repeat handling; turn only the owned queue's shuffle into a list command. */ +internal fun installPlaylistRepeat( + context: BytecodePatchContext, + navigatorType: String +) = with(context) { + val binding = PlaylistRepeatBindingFingerprint.match() + val headerType = binding.originalClassDef.type + val header = mutableClassDefBy(headerType) + val navigator = playlistHeaderNavigatorFingerprint(navigatorType) + .instructionMatches.single().instruction.getReference()!! + val panel = playlistHeaderPanelFingerprint(binding.originalMethod) + .instructionMatches.single().instruction.getReference()!! + val repeatView = PlaylistHeaderRepeatViewFingerprint + .instructionMatches.last().instruction.getReference()!! + val shuffleMatch = PlaylistShuffleViewFingerprint + val shuffleView = shuffleMatch.instructionMatches.last().instruction.getReference()!! + check(shuffleView.definingClass == headerType) { + "Native playlist queue: shuffle and repeat must share the native header" + } + + val capabilities = PlaylistRepeatCapabilitiesFingerprint + check(capabilities.originalMethod == binding.originalMethod) { + "Native playlist queue: repeat capabilities and state must share their native binder" + } + + val repeatAll = capabilities.instructionMatches[0].instruction.getReference()!! + val repeatOne = capabilities.instructionMatches[2].instruction.getReference()!! + check(repeatAll != repeatOne && repeatAll.definingClass == headerType && repeatOne.definingClass == headerType) { + "Native playlist queue: repeat capabilities must be distinct fields on the bound header" + } + + header.interfaces.add(REPEAT_HEADER_INTERFACE) + header.addBridgeMethod( + "patch_getNavigator", + emptyList(), + "Ljava/lang/Object;", + 1, + """ + iget-object v0, p0, $navigator + return-object v0 + """ + ) + + header.addBridgeMethod( + "patch_getPanel", + emptyList(), + "Ljava/lang/Object;", + 1, + """ + iget-object v0, p0, $panel + return-object v0 + """ + ) + + header.addBridgeMethod( + "patch_enableRepeat", + emptyList(), + "V", + 2, + """ + const/4 v0, 0x1 + iput-boolean v0, p0, $repeatAll + iput-boolean v0, p0, $repeatOne + iget-object v1, p0, $repeatView + invoke-virtual {v1, v0}, Lcom/google/android/apps/youtube/app/watch/panel/ui/PlaylistLoopButtonView;->setEnabled(Z)V + return-void + """ + ) + + header.addBridgeMethod( + "patch_setShuffleAvailable", + listOf("Z"), + "V", + 1, + """ + iget-object v0, p0, $shuffleView + invoke-virtual {v0, p1}, Landroid/widget/ImageView;->setEnabled(Z)V + const/4 p1, 0x0 + invoke-virtual {v0, p1}, Landroid/widget/ImageView;->setSelected(Z)V + return-void + """ + ) + + // Preserve the original listener and account-playlist command branch. Only a + // header bound to our exact local panel consumes this fallback toggle. + val shufflePress = playlistShufflePressFingerprint(navigator, shuffleView) + val pressIndex = shufflePress.instructionMatches.first().index + val headerRegister = (shufflePress.instructionMatches.first().instruction as TwoRegisterInstruction).registerB + val press = shufflePress.method + val resultRegister = press.findFreeRegister(pressIndex) + press.addInstructionsAtControlFlowLabel( + pressIndex, + """ + invoke-static/range {v$headerRegister .. v$headerRegister}, $EXTENSION_CLASS->onQueueShuffle($REPEAT_HEADER_INTERFACE)Z + move-result v$resultRegister + if-eqz v$resultRegister, :native_shuffle + return-void + + :native_shuffle + nop + """ + ) + + // The binder reuses p0 on some exits. Preserve its owner before observing all returns. + binding.method.cloneParameters().apply { + findInstructionIndicesReversedOrThrow(Opcode.RETURN_VOID).forEach { index -> + addInstructionsAtControlFlowLabel( + index, + "invoke-static/range {p0 .. p0}, $EXTENSION_CLASS->onRepeatHeaderBound($REPEAT_HEADER_INTERFACE)V" + ) + } + } +} diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/PlaylistRowHook.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/PlaylistRowHook.kt new file mode 100644 index 0000000000..e16d145dcb --- /dev/null +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/PlaylistRowHook.kt @@ -0,0 +1,237 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/3953 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.patches.youtube.interaction.nativeplaylistqueue + +import app.morphe.patcher.extensions.InstructionExtensions.addInstruction +import app.morphe.patcher.patch.BytecodePatchContext +import app.morphe.patcher.util.getReference +import app.morphe.patcher.util.proxy.mutableTypes.MutableField.Companion.toMutable +import app.morphe.patches.youtube.layout.originaltitles.PlaylistPanelVideoBindFingerprint +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction +import com.android.tools.smali.dexlib2.iface.reference.FieldReference +import com.android.tools.smali.dexlib2.immutable.ImmutableField + +/** Observe native row commands without replacing their listener or routing. */ +internal fun installPlaylistRowPlayback( + context: BytecodePatchContext, + modelField: FieldReference, + panelField: FieldReference +) = with(context) { + // Original titles also modifies these binders. Inspect fresh matches, + // including all native layouts, and require their shared video model. + PlaylistPanelVideoBindFingerprint.clearMatch() + val videoIdFields = PlaylistPanelVideoBindFingerprint.matchAll().map { + it.instructionMatches[2].instruction.getReference()!! + }.distinct().toList() + check(videoIdFields.size == 1) { + "Native playlist queue: row binders disagree on the native video ID field: $videoIdFields" + } + + val videoIdField = videoIdFields.single() + val videoType = videoIdField.definingClass + + val rowMenu = playlistRowMenuOpenFingerprint(videoType).match() + + installPlaylistMenuDispatch(context, rowMenu.originalMethod.parameterTypes[1].toString()) + + val contextCreation = rowMenu.instructionMatches.last() + val contextRegister = contextCreation.getInstruction().registerA + rowMenu.method.addInstruction( + contextCreation.index + 1, + "invoke-static {v$contextRegister}, $EXTENSION_CLASS->onRowMenuOpened(Ljava/util/Map;)V" + ) + + installPlaylistReordering(context, videoType) + + // The native veto predicate runs before logging decorates the bound endpoint. + // Observe its original identity after the veto, never the decorated copy. + val click = PlaylistRowClickFingerprint.match() + val listenerType = click.originalClassDef.type + val endpointRead = click.instructionMatches[4] + val listenerEndpoint = endpointRead.instruction.getReference()!! + val endpointType = listenerEndpoint.type + val rowEndpoint = playlistRowEndpointFingerprint(listenerType, videoType, endpointType).match() + val rowListener = rowEndpoint.instructionMatches.first().instruction.getReference()!! + val videoEndpoint = rowEndpoint.instructionMatches.last().instruction.getReference()!! + + val factory = playlistItemFactoryFingerprint(videoType).match() + val itemVideo = factory.instructionMatches.single().instruction.getReference()!! + val contents = playlistContentsFingerprint(panelField.type, factory.originalMethod).match() + .instructionMatches.first().instruction.getReference()!! + + val controller = mutableClassDefBy(modelField.definingClass) + controller.addBridgeMethod( + "patch_findQueueRow", + listOf("Ljava/lang/Object;"), + "I", + 6, + """ + iget-object v0, p0, $modelField + if-eqz v0, :missing + iget-object v0, v0, $panelField + if-eqz v0, :missing + iget-object v0, v0, $contents + invoke-interface {v0}, Ljava/util/List;->iterator()Ljava/util/Iterator; + move-result-object v0 + const/4 v1, 0x0 + + :next + invoke-interface {v0}, Ljava/util/Iterator;->hasNext()Z + move-result v2 + if-eqz v2, :missing + invoke-interface {v0}, Ljava/util/Iterator;->next()Ljava/lang/Object; + move-result-object v2 + check-cast v2, ${itemVideo.definingClass} + iget-object v2, v2, $itemVideo + if-eqz v2, :skip + if-eq v2, p1, :found + iget-object v2, v2, $videoEndpoint + if-ne v2, p1, :skip + + :found + return v1 + + :skip + add-int/lit8 v1, v1, 0x1 + goto :next + + :missing + const/4 v0, -0x1 + return v0 + """ + ) + + installPlaylistNavigation(context, controller.type) + + // A nonserialized bit belongs to the exact native object, survives a stale + // view callback, and disappears with that object. No native model is retained + // in an extension cache, and protobuf equality/serialization stay unchanged. + val video = mutableClassDefBy(videoType) + video.interfaces.add("Lapp/morphe/extension/youtube/patches/utils/FlyoutUtils\$FlyoutMenuVideoIdInterface;") + video.addBridgeMethod( + "patch_getVideoId", + emptyList(), + "Ljava/lang/String;", + 1, + """ + iget-object v0, p0, $videoIdField + return-object v0 + """ + ) + + val ownedField = ImmutableField( + videoType, "patch_queueOwned", "Z", AccessFlags.PUBLIC.value, null, null, null + ).toMutable() + video.fields.add(ownedField) + video.interfaces.add(QUEUE_ROW_INTERFACE) + video.addBridgeMethod( + "patch_isQueueOwned", + emptyList(), + "Z", + 1, + """ + iget-boolean v0, p0, $ownedField + return v0 + """ + ) + + // Retain ownership on old header models too, so a stale Clear action is consumed + // instead of falling through to YouTube's ordinary Hide command. + val panel = mutableClassDefBy(panelField.type) + val panelOwned = ImmutableField( + panel.type, "patch_queueOwned", "Z", AccessFlags.PUBLIC.value, null, null, null + ).toMutable() + panel.fields.add(panelOwned) + panel.interfaces.add(QUEUE_PANEL_INTERFACE) + panel.addBridgeMethod( + "patch_isQueueOwned", + emptyList(), + "Z", + 1, + """ + iget-boolean v0, p0, $panelOwned + return v0 + """ + ) + + controller.addBridgeMethod( + "patch_markQueueRows", + emptyList(), + "V", + 4, + """ + iget-object v0, p0, $modelField + if-eqz v0, :done + iget-object v0, v0, $panelField + if-eqz v0, :done + const/4 v1, 0x1 + iput-boolean v1, v0, $panelOwned + iget-object v0, v0, $contents + invoke-interface {v0}, Ljava/util/List;->iterator()Ljava/util/Iterator; + move-result-object v0 + + :next + invoke-interface {v0}, Ljava/util/Iterator;->hasNext()Z + move-result v3 + if-eqz v3, :done + invoke-interface {v0}, Ljava/util/Iterator;->next()Ljava/lang/Object; + move-result-object v2 + check-cast v2, ${itemVideo.definingClass} + iget-object v2, v2, $itemVideo + if-eqz v2, :next + iput-boolean v1, v2, $ownedField + goto :next + + :done + return-void + """ + ) + + // Logging can replace the listener's endpoint after a click. Retain the + // bound identity across a rejected/repeated click, and clear it on unbind. + val listener = mutableClassDefBy(listenerType) + val boundEndpoint = ImmutableField( + listenerType, "patch_queueEndpoint", "Ljava/lang/Object;", AccessFlags.PRIVATE.value, null, null, null + ).toMutable() + listener.fields.add(boundEndpoint) + listener.interfaces.add(ROW_CLICK_INTERFACE) + listener.addBridgeMethod( + "patch_getQueueEndpoint", + emptyList(), + "Ljava/lang/Object;", + 1, + """ + iget-object v0, p0, $boundEndpoint + return-object v0 + """ + ) + + listOf( + playlistEndpointBindFingerprint(listenerEndpoint), + playlistEndpointUnbindFingerprint(listenerEndpoint) + ).forEach { fingerprint -> + val match = fingerprint.match() + val store = match.instructionMatches.single() + val registers = store.getInstruction() + match.method.addInstruction( + store.index + 1, + "iput-object v${registers.registerA}, v${registers.registerB}, $boundEndpoint" + ) + } + + val listenerRegister = endpointRead.getInstruction().registerB + click.method.addInstruction( + endpointRead.index + 1, + "invoke-static/range {v$listenerRegister .. v$listenerRegister}, " + + "$EXTENSION_CLASS->onRowClicked($ROW_CLICK_INTERFACE)V" + ) + + installPlaylistSwipeBackground(context, rowListener) +} diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/PlaylistTitleHook.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/PlaylistTitleHook.kt new file mode 100644 index 0000000000..ae8423ce60 --- /dev/null +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/PlaylistTitleHook.kt @@ -0,0 +1,73 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/3953 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.patches.youtube.interaction.nativeplaylistqueue + +import app.morphe.patcher.extensions.InstructionExtensions.addInstruction +import app.morphe.patcher.patch.BytecodePatchContext +import app.morphe.patcher.util.cloneParameters +import app.morphe.patcher.util.getReference +import app.morphe.util.addInstructionsAtControlFlowLabel +import app.morphe.util.findFieldFromToString +import app.morphe.util.findInstructionIndicesReversedOrThrow +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction +import com.android.tools.smali.dexlib2.iface.reference.FieldReference + +/** Observe the native playlist title after inflation and model rebinding. */ +internal fun installPlaylistTitle(context: BytecodePatchContext) = with(context) { + val titleMatch = PlaylistTitleFingerprint.match() + val headerType = titleMatch.originalClassDef.type + val titleStore = titleMatch.instructionMatches.last() + val title = titleStore.instruction.getReference()!! + val panelType = PlaylistModelDescriptionFingerprint.match().originalMethod + .findFieldFromToString(", playlistPanel=").type + val binding = playlistTitleBindingFingerprint(headerType, panelType).match() + val panel = binding.instructionMatches.single().instruction.getReference()!! + + mutableClassDefBy(headerType).apply { + interfaces.add(PLAYLIST_TITLE_INTERFACE) + addBridgeMethod( + "patch_getPanel", + emptyList(), + "Ljava/lang/Object;", + 1, + """ + iget-object v0, p0, $panel + return-object v0 + """ + ) + + addBridgeMethod( + "patch_getTitle", + emptyList(), + "Landroid/widget/TextView;", + 1, + """ + iget-object v0, p0, $title + return-object v0 + """ + ) + } + + // Observe initial inflation once, rather than every call to the cached-view getter. + // Native model binding can happen earlier, while no TextView exists yet. + val receiver = (titleStore.instruction as TwoRegisterInstruction).registerB + titleMatch.method.addInstruction( + titleStore.index + 1, + "invoke-static/range {v$receiver .. v$receiver}, $EXTENSION_CLASS->onPlaylistTitleBound($PLAYLIST_TITLE_INTERFACE)V" + ) + + binding.method.cloneParameters().apply { + findInstructionIndicesReversedOrThrow(Opcode.RETURN_VOID).forEach { index -> + addInstructionsAtControlFlowLabel( + index, + "invoke-static/range {p0 .. p0}, $EXTENSION_CLASS->onPlaylistTitleBound($PLAYLIST_TITLE_INTERFACE)V" + ) + } + } +} diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/QueueFlyoutHook.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/QueueFlyoutHook.kt new file mode 100644 index 0000000000..7b75ea8cac --- /dev/null +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/QueueFlyoutHook.kt @@ -0,0 +1,117 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/3953 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.patches.youtube.interaction.nativeplaylistqueue + +import app.morphe.patcher.extensions.InstructionExtensions.addInstructions +import app.morphe.patcher.extensions.InstructionExtensions.getInstruction +import app.morphe.patcher.patch.BytecodePatchContext +import app.morphe.patcher.util.findFreeRegister +import app.morphe.patcher.util.getReference +import app.morphe.patcher.util.smali.ExternalLabel +import app.morphe.patches.youtube.layout.flyout.FeedFlyoutButtonsInitializerFingerprint +import app.morphe.util.addInstructionsAtControlFlowLabel +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.reference.FieldReference +import com.android.tools.smali.dexlib2.iface.reference.MethodReference + +private const val ACTIONS = "Lapp/morphe/extension/youtube/patches/queue/QueueFlyoutActions;" +private const val MESSAGE = "Lcom/google/protobuf/MessageLite;" + +/** Complete the native model, so both list and recycler adapters retain their own item positions. */ +internal fun installQueueFlyout(context: BytecodePatchContext) = with(context) { + val sheets = QueueFlyoutRendererFingerprint.matchAll(2..2) + val menuTypes = sheets.map { + it.instructionMatches[0].instruction.getReference()!!.type + }.distinct() + check(menuTypes.size == 1) { "Native queue menus must share their renderer type" } + val menuType = menuTypes.single() + val itemFields = sheets.map { + it.instructionMatches[1].instruction.getReference()!! + }.distinctBy { it.toString() } + check(itemFields.size == 1) { "Native queue menus must share their item list" } + val resolverType = QueueFlyoutResolverFingerprint.originalClassDef.type + val popup = queueFlyoutPopupFingerprint(resolverType, menuType, itemFields.single()).match() + + // The shared factory exposes native icon conversion and the button's model + // type. Direct sheet clicks can bypass its Runnable, so intercept both native + // dispatchers before their backend command, retaining their dismissal branch. + val factory = FeedFlyoutButtonsInitializerFingerprint.instructionMatches + val iconGetter = factory.first().instruction.getReference()!! + val iconField = factory[6].instruction.getReference()!! + val iconEnum = factory[7].instruction.getReference()!! + val itemType = iconGetter.parameterTypes.single().toString() + val buttonType = queueFlyoutButtonConstructorFingerprint( + FeedFlyoutButtonsInitializerFingerprint.originalMethod, itemType + ).instructionMatches.single().instruction.getReference()!!.definingClass + for (sheet in sheets) { + val click = queueFlyoutClickFingerprint( + sheet.originalClassDef.type, buttonType, itemType + ).match() + val read = click.instructionMatches.first() + val item = (read.instruction as OneRegisterInstruction).registerA + val method = click.method + val free = method.findFreeRegister(read.index + 1, item) + check(free <= 15) { "Native queue click icon conversion requires a low register" } + val dismissal = method.getInstruction(click.instructionMatches[1].index) + method.addInstructionsAtControlFlowLabel( + read.index + 1, + """ + if-eqz v$item, :native_queue_command + invoke-static/range {v$item .. v$item}, $iconGetter + move-result-object v$free + if-eqz v$free, :native_queue_command + iget v$free, v$free, $iconField + invoke-static/range {v$free .. v$free}, $iconEnum + move-result-object v$free + invoke-static/range {v$free .. v$free}, $ACTIONS->onStockQueueItemClick(Ljava/lang/Enum;)Z + move-result v$free + if-nez v$free, :native_queue_dismiss + :native_queue_command + nop + """, + ExternalLabel("native_queue_dismiss", dismissal) + ) + } + + val menu = mutableClassDefBy(menuType) + menu.interfaces.add("${ACTIONS.removeSuffix(";")}\$Menu;") + menu.addBridgeMethod( + "patch_parseQueueFlyout", + listOf("[B"), + MESSAGE, + 2, + """ + ${parseProtoInstructions(menuType, bytes = "p1", result = "v0", registry = "v1")} + return-object v0 + """ + ) + + for (sheet in sheets) { + val read = sheet.instructionMatches.first() + val register = (read.instruction as OneRegisterInstruction).registerA + // Only the local renderer is replaced. The sheet's original model and + // every unrelated item remain intact; null still follows the native branch. + sheet.method.addInstructions( + read.index + 1, + """ + invoke-static/range {v$register .. v$register}, $ACTIONS->completeQueueMenu($MESSAGE)$MESSAGE + move-result-object v$register + check-cast v$register, $menuType + """ + ) + } + + popup.method.addInstructions( + 0, + """ + invoke-static/range {p0 .. p0}, $ACTIONS->completeQueueMenu($MESSAGE)$MESSAGE + move-result-object p0 + check-cast p0, $menuType + """ + ) +} diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/QueuePlayerCloseHook.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/QueuePlayerCloseHook.kt new file mode 100644 index 0000000000..3d6c89d9fb --- /dev/null +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/QueuePlayerCloseHook.kt @@ -0,0 +1,70 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/3953 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.patches.youtube.interaction.nativeplaylistqueue + +import app.morphe.patcher.patch.BytecodePatchContext +import app.morphe.patcher.util.getReference +import app.morphe.patcher.util.matchSingle +import app.morphe.patches.youtube.misc.loadvideo.MiniAppOpenYtContentCommandEndpointFingerprint +import app.morphe.patches.youtube.misc.playservice.is_21_32_or_greater +import app.morphe.util.addInstructionsAtControlFlowLabel +import com.android.tools.smali.dexlib2.iface.reference.FieldReference +import com.android.tools.smali.dexlib2.iface.reference.MethodReference + +/** Observe the user close before teardown; keep the original native listener and dismissal. */ +internal fun installQueuePlayerClose( + context: BytecodePatchContext, + endpointType: String, + dispatch: MethodReference +) = with(context) { + val dismiss = MiniAppOpenYtContentCommandEndpointFingerprint + .instructionMatches.last().instruction.getReference()!! + + // Before 21.32 the fling has a second accepted-dismiss branch. Hook every user + // dismissal, preserving thresholds, branch targets and the native continuation, + // without hooking programmatic closes that use the same dismiss method. + val closes = listOf( + queuePlayerCloseClickFingerprint(dismiss), + if (is_21_32_or_greater) queuePlayerCloseFlingFingerprint(dismiss) + else queuePlayerCloseFlingLegacyFingerprint(dismiss), + queuePlayerCloseTouchFingerprint(dismiss) + ) + + closes.forEach { fingerprint -> + val match = fingerprint.matchSingle() + + match.instructionMatches.asReversed().forEach { call -> + match.method.addInstructionsAtControlFlowLabel( + call.index, + "invoke-static {}, $EXTENSION_CLASS->onQueuePlayerClosing()V" + ) + } + } + + // This dispatcher belongs to the activity, not the disposed playback backend. + // Reading it remains valid after the watch view detaches. No listener is replaced. + val controllerType = PlaylistControllerConstructorFingerprint.originalClassDef.type + val dispatcher = playlistControllerDispatcherFingerprint(controllerType, dispatch.definingClass) + .instructionMatches.first().instruction.getReference()!! + + mutableClassDefBy(controllerType).addBridgeMethod( + "patch_showQueueFeedback", + listOf("[B"), + "V", + 3, + """ + ${parseProtoInstructions(endpointType, bytes = "p1", result = "v0", registry = "v1")} + check-cast v0, $endpointType + iget-object v1, p0, $dispatcher + invoke-static {}, Ljava/util/Collections;->emptyMap()Ljava/util/Map; + move-result-object v2 + invoke-interface {v1, v0, v2}, $dispatch + return-void + """ + ) +} diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/QueueSaveFeedbackHook.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/QueueSaveFeedbackHook.kt new file mode 100644 index 0000000000..5664e496e7 --- /dev/null +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/QueueSaveFeedbackHook.kt @@ -0,0 +1,126 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/3953 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.patches.youtube.interaction.nativeplaylistqueue + +import app.morphe.patcher.patch.BytecodePatchContext +import app.morphe.patcher.util.cloneParameters +import app.morphe.patcher.util.findFreeRegister +import app.morphe.patcher.util.getReference +import app.morphe.patcher.util.p0Register +import app.morphe.patcher.util.proxy.mutableTypes.MutableField.Companion.toMutable +import app.morphe.util.addInstructionsAtControlFlowLabel +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.RegisterRangeInstruction +import com.android.tools.smali.dexlib2.iface.reference.MethodReference +import com.android.tools.smali.dexlib2.immutable.ImmutableField + +/** Add a native response action only for the callback created by an owned queue save. */ +internal fun installQueueSaveFeedback(context: BytecodePatchContext, endpointType: String) = with(context) { + val callbackType = QueueSaveEditErrorFingerprint.originalClassDef.type + val creation = queueSaveEditCreationFingerprint(callbackType, endpointType).match() + val success = queueSaveEditSuccessFingerprint(callbackType).match() + val responseType = success.originalMethod.parameterTypes.single().toString() + val callback = mutableClassDefBy(callbackType) + callback.interfaces.add(SAVE_CALLBACK_INTERFACE) + val selection = ImmutableField(callbackType, "patch_queueSaveSelection", SAVE_SELECTION_CLASS, + AccessFlags.PRIVATE.value, null, null, null).toMutable() + callback.fields.add(selection) + callback.addBridgeMethod( + "patch_getQueueSaveSelection", + emptyList(), + SAVE_SELECTION_CLASS, + 1, + """ + iget-object v0, p0, $selection + return-object v0 + """ + ) + + callback.addBridgeMethod( + "patch_setQueueSaveSelection", + listOf(SAVE_SELECTION_CLASS), + "V", + 0, + """ + iput-object p1, p0, $selection + return-void + """ + ) + + callback.addBridgeMethod( + "patch_parseQueueSaveResult", + listOf("[B"), + "Lcom/google/protobuf/MessageLite;", + 2, + """ + ${parseProtoInstructions(responseType, bytes = "p1", result = "v0", registry = "v1")} + return-object v0 + """ + ) + + val construct = creation.instructionMatches.single() + val reference = construct.instruction.getReference()!! + check(reference.definingClass == callbackType && reference.name == "") + val receiver = when (val instruction = construct.instruction) { + is RegisterRangeInstruction -> instruction.startRegister + is FiveRegisterInstruction -> instruction.registerC + else -> error("Native playlist queue: unsupported save callback constructor encoding") + } + + val endpoint = creation.originalMethod.p0Register + 1 + check(receiver <= 15 && endpoint <= 15) { "Native playlist queue: save context requires low registers" } + + // The constructor has completed. The original endpoint parameter is still + // live here, before the resolver proceeds to either native request branch. + creation.method.addInstructionsAtControlFlowLabel( + construct.index + 1, + """ + invoke-static {v$receiver, v$endpoint}, $SAVE_ACTIONS_CLASS->onEditCreated(${SAVE_CALLBACK_INTERFACE}Lcom/google/protobuf/MessageLite;)V + """ + ) + + success.method.addInstructionsAtControlFlowLabel( + 0, + """ + invoke-static/range {p0 .. p1}, $SAVE_ACTIONS_CLASS->onEditResult(${SAVE_CALLBACK_INTERFACE}Lcom/google/protobuf/MessageLite;)Lcom/google/protobuf/MessageLite; + move-result-object p1 + check-cast p1, $responseType + """ + ) +} + +/** Use the native builder and scheduler for owned Save and queue-clear confirmations. */ +internal fun installQueueSaveConfirmationDuration(context: BytecodePatchContext) = with(context) { + val builderType = QueueSnackbarModernFactoryFingerprint.originalMethod.returnType + val durationSetter = queueSnackbarDurationFingerprint(builderType).match() + .instructionMatches[4].instruction.getReference()!! + + // Keep the input renderer even when the native factory reuses its parameters. + QueueSnackbarModernFactoryFingerprint.method.cloneParameters() + QueueSnackbarModernFactoryFingerprint.clearMatch() + val factory = QueueSnackbarModernFactoryFingerprint.match() + + val exit = factory.instructionMatches.last() + val builder = exit.getInstruction().registerA + val duration = factory.method.findFreeRegister(exit.index) + check(builder <= 15 && duration <= 15) { "Native playlist queue: snackbar duration requires low registers" } + factory.method.addInstructionsAtControlFlowLabel( + exit.index, + """ + invoke-static/range {p1 .. p1}, $SAVE_FEEDBACK_CLASS->confirmationDuration(Lcom/google/protobuf/MessageLite;)I + move-result v$duration + if-lez v$duration, :native_duration + invoke-virtual {v$builder, v$duration}, $durationSetter + + :native_duration + nop + """ + ) +} diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/QueueSavePickerHook.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/QueueSavePickerHook.kt new file mode 100644 index 0000000000..7145c7e246 --- /dev/null +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/QueueSavePickerHook.kt @@ -0,0 +1,97 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/3953 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.patches.youtube.interaction.nativeplaylistqueue + +import app.morphe.patcher.patch.BytecodePatchContext +import app.morphe.patcher.util.findFreeRegister +import app.morphe.patcher.util.getReference +import app.morphe.patcher.util.matchSingle +import app.morphe.patcher.util.p0Register +import app.morphe.util.addInstructionsAtControlFlowLabel +import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.reference.FieldReference +import com.android.tools.smali.dexlib2.iface.reference.TypeReference + +private const val LIST = "Ljava/util/List;" + +/** Supply a real batch only to a picker launched by our queue header; keep native callbacks. */ +internal fun installQueueSavePicker(context: BytecodePatchContext, endpointType: String) = with(context) { + installQueueSaveFeedback(context, endpointType) + + val request = QueueSaveRequestFingerprint.originalClassDef + val picker = queueSavePickerFingerprint(request.type).match() + val factory = queueSavePickerFactoryFingerprint( + picker.originalClassDef.type, endpointType + ).match() + + val serialize = factory.instructionMatches[1] + val serializeCall = serialize.getInstruction() + val putArguments = factory.instructionMatches.last().getInstruction() + check(serializeCall.registerC == factory.originalMethod.p0Register + 1 && + serializeCall.registerC <= 15 && putArguments.registerC <= 15) { + "Native playlist queue: picker must serialize its original endpoint after constructing arguments" + } + + // p1 is overwritten by the serialized bytes immediately afterwards. Transfer the + // owned snapshot before that write, on both the existing/new Bundle branches. + factory.method.addInstructionsAtControlFlowLabel( + serialize.index, + """ + invoke-static {v${serializeCall.registerC}, v${putArguments.registerC}}, $EXTENSION_CLASS->onQueueSaveArguments(Lcom/google/protobuf/MessageLite;Landroid/os/Bundle;)V + """ + ) + + val argumentsField = picker.instructionMatches.first().instruction.getReference()!! + val mutablePicker = mutableClassDefBy(picker.originalClassDef.type) + mutablePicker.interfaces.add(SAVE_PICKER_INTERFACE) + mutablePicker.addBridgeMethod( + "patch_getQueueSaveVideoIds", + emptyList(), + LIST, + 1, + """ + iget-object v0, p0, $argumentsField + invoke-static {v0}, $EXTENSION_CLASS->getQueueSaveVideoIds(Landroid/os/Bundle;)$LIST + move-result-object v0 + return-object v0 + """ + ) + + // Leave the native picker request entirely unchanged: its batch variation + // returns HTTP 400 before the UI can open on the verified client. + val response = QueueSaveResponseFingerprint.matchSingle() + val callbackPicker = queueSaveResponsePickerFingerprint(mutablePicker.type).matchSingle() + .instructionMatches.single().instruction.getReference()!! + + val responseCast = response.instructionMatches.first() + val responseType = responseCast.instruction.getReference()!!.type + val responseRegister = (responseCast.instruction as OneRegisterInstruction).registerA + val responsePickerRegister = response.method.findFreeRegister(responseCast.index + 1, responseRegister, response.method.p0Register) + check(responsePickerRegister <= 15 && responseRegister <= 15 && response.method.p0Register <= 15) + response.method.addInstructionsAtControlFlowLabel( + responseCast.index + 1, + """ + iget-object v$responsePickerRegister, p0, $callbackPicker + invoke-static {v$responsePickerRegister, v$responseRegister}, $SAVE_ACTIONS_CLASS->onPickerResponse(${SAVE_PICKER_INTERFACE}Lcom/google/protobuf/MessageLite;)Lcom/google/protobuf/MessageLite; + move-result-object v$responseRegister + check-cast v$responseRegister, $responseType + """ + ) + + mutablePicker.addBridgeMethod( + "patch_parseQueueSaveResponse", + listOf("[B"), + "Lcom/google/protobuf/MessageLite;", + 2, + """ + ${parseProtoInstructions(responseType, bytes = "p1", result = "v0", registry = "v1")} + return-object v0 + """ + ) +} diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/QueueUndoHook.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/QueueUndoHook.kt new file mode 100644 index 0000000000..ce1d749056 --- /dev/null +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/QueueUndoHook.kt @@ -0,0 +1,62 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/3953 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.patches.youtube.interaction.nativeplaylistqueue + +import app.morphe.patcher.extensions.InstructionExtensions.getInstruction +import app.morphe.patcher.patch.BytecodePatchContext +import app.morphe.patcher.util.findFreeRegister +import app.morphe.patcher.util.getReference +import app.morphe.patcher.util.matchSingle +import app.morphe.patcher.util.smali.ExternalLabel +import app.morphe.util.addInstructionsAtControlFlowLabel +import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction +import com.android.tools.smali.dexlib2.iface.reference.FieldReference +import com.android.tools.smali.dexlib2.iface.reference.MethodReference + +/** Parse feedback with the native registry and chain its existing snackbar callback. */ +internal fun installQueueUndo(context: BytecodePatchContext, callback: MethodReference) = with(context) { + val endpointType = callback.parameterTypes.first().toString() + val endpoint = mutableClassDefBy(endpointType) + endpoint.interfaces.add(UNDO_COMMAND_INTERFACE) + endpoint.addBridgeMethod( + "patch_parseQueueCommand", + listOf("[B"), + "Lcom/google/protobuf/MessageLite;", + 2, + """ + ${parseProtoInstructions(endpointType, bytes = "p1", result = "v0", registry = "v1")} + return-object v0 + """ + ) + + val modern = QueueSnackbarModernFactoryFingerprint.matchSingle() + val listenerType = modern.instructionMatches.first().instruction.getReference()!!.definingClass + + installQueueSaveConfirmationDuration(context) + installQueuePlayerClose(this, endpointType, callback) + + val dispatcher = queueSnackbarDispatcherFingerprint(callback.definingClass).matchSingle() + .instructionMatches.single().instruction.getReference()!! + + // Modern buttons can carry service, navigation and command endpoints. Intercept + // only the three dispatches supplied by this snackbar factory, not other lambda cases. + val click = queueUndoClickFingerprint(listenerType, dispatcher, callback).matchSingle() + click.instructionMatches.filterIndexed { index, _ -> index > 0 && index % 2 == 0 }.asReversed().forEach { call -> + val registers = call.getInstruction() + val consumed = click.method.findFreeRegister(call.index) + click.method.addInstructionsAtControlFlowLabel( + call.index, + """ + invoke-static {v${registers.registerD}}, $EXTENSION_CLASS->onQueueUndoCommand(Lcom/google/protobuf/MessageLite;)Z + move-result v$consumed + if-nez v$consumed, :local_undo_handled + """, + ExternalLabel("local_undo_handled", click.method.getInstruction(call.index + 1)) + ) + } +} diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/RatingActionsHook.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/RatingActionsHook.kt new file mode 100644 index 0000000000..67bd136c8c --- /dev/null +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/RatingActionsHook.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/3953 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.patches.youtube.interaction.nativeplaylistqueue + +import app.morphe.patcher.extensions.InstructionExtensions.addInstruction +import app.morphe.patcher.extensions.InstructionExtensions.getInstruction +import app.morphe.patcher.patch.BytecodePatchContext +import app.morphe.patcher.util.matchSingle +import app.morphe.patches.youtube.shared.WatchNextResponseParserFingerprint +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction + +/** Observe the parsed response before view filters, including prefetched responses. */ +internal fun installRatingCapture(context: BytecodePatchContext) = with(context) { + // Hide related videos and Hide video action buttons also use this parser. + WatchNextResponseParserFingerprint.clearMatch() + val match = WatchNextResponseParserFingerprint.matchSingle() + + // The watch next response cast. + val responseCast = match.instructionMatches[5] + val responseRegister = match.method.getInstruction(responseCast.index).registerA + match.method.addInstruction( + responseCast.index + 1, + "invoke-static/range {v$responseRegister .. v$responseRegister}, $EXTENSION_CLASS->onWatchNextResponse(Lcom/google/protobuf/MessageLite;)V" + ) +} diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/shorts/SkipLiveShortsPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/shorts/SkipLiveShortsPatch.kt index 3b2990d780..eaebe4eea3 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/shorts/SkipLiveShortsPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/shorts/SkipLiveShortsPatch.kt @@ -13,10 +13,11 @@ import app.morphe.patcher.util.findFreeRegister import app.morphe.patcher.util.getReference import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable import app.morphe.patches.shared.misc.settings.preference.SwitchPreference +import app.morphe.patches.youtube.layout.shortsautoplay.ReelEnumConstructorFingerprint import app.morphe.patches.youtube.layout.shortsautoplay.ReelPlaybackFingerprint import app.morphe.patches.youtube.layout.shortsautoplay.ReelPlaybackLegacyFingerprint -import app.morphe.patches.youtube.layout.shortsautoplay.ReelPlaybackRepeatFingerprint import app.morphe.patches.youtube.layout.shortsautoplay.getInstanceFieldGetFingerprint +import app.morphe.patches.youtube.layout.shortsautoplay.reelPlaybackRepeatFingerprint import app.morphe.patches.youtube.misc.extension.sharedExtensionPatch import app.morphe.patches.youtube.misc.playservice.is_21_24_or_greater import app.morphe.patches.youtube.misc.playservice.is_21_42_or_greater @@ -112,11 +113,12 @@ val skipLiveShortsPatch = bytecodePatch( } // Hand the Shorts player to extension code. + val reelPlaybackRepeat = reelPlaybackRepeatFingerprint(ReelEnumConstructorFingerprint.originalClassDef.type) val reelPlayerField = getInstanceFieldGetFingerprint(reelPlaybackFingerprint.classDef.type) - .match(ReelPlaybackRepeatFingerprint.originalMethod) + .match(reelPlaybackRepeat.originalMethod) .instructionMatches.first().instruction.getReference()!! - ReelPlaybackRepeatFingerprint.method.apply { + reelPlaybackRepeat.method.apply { val register = findFreeRegister(0) addInstructions( diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/flyout/AddToQueuePatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/flyout/AddToQueuePatch.kt index a07e240008..dee094a8e4 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/flyout/AddToQueuePatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/flyout/AddToQueuePatch.kt @@ -11,14 +11,17 @@ import app.morphe.patcher.extensions.InstructionExtensions.addInstructions import app.morphe.patcher.extensions.InstructionExtensions.addInstructionsWithLabels import app.morphe.patcher.extensions.InstructionExtensions.getInstruction import app.morphe.patcher.patch.bytecodePatch +import app.morphe.patches.shared.misc.settings.preference.ListPreference import app.morphe.patches.shared.misc.settings.preference.SwitchPreference import app.morphe.patches.shared.misc.settings.preference.noTitleUnsortedPreferenceCategory +import app.morphe.patches.youtube.interaction.nativeplaylistqueue.nativePlaylistQueuePatch import app.morphe.patches.youtube.layout.hide.general.ContextualMenuItemBuilderOnClickFingerprint import app.morphe.patches.youtube.layout.playlistautoplay.navigationIntentHook import app.morphe.patches.youtube.misc.auth.authHookPatch import app.morphe.patches.youtube.misc.extension.sharedExtensionPatch import app.morphe.patches.youtube.misc.loadvideo.loadVideoHookPatch import app.morphe.patches.youtube.misc.playservice.is_21_05_or_greater +import app.morphe.patches.youtube.misc.playservice.is_21_15_or_greater import app.morphe.patches.youtube.misc.proto.elementProtoParserHookPatch import app.morphe.patches.youtube.misc.settings.PreferenceScreen import app.morphe.patches.youtube.misc.settings.settingsPatch @@ -38,7 +41,8 @@ private const val EXTENSION_LOCAL_QUEUE_CLASS = @Suppress("unused") val addToQueuePatch = bytecodePatch( name = "Add to queue", - description = "Overrides the feed flyout 'Play next in queue' with the Morphe video queue." + description = "Adds a video queue to the feed flyout menu, using either the Morphe queue " + + "or a native queue that uses YouTube's playlist interface." ) { dependsOn( flyoutPatch, @@ -47,7 +51,8 @@ val addToQueuePatch = bytecodePatch( elementProtoParserHookPatch, authHookPatch, loadVideoHookPatch, - videoInformationPatch + videoInformationPatch, + nativePlaylistQueuePatch ) compatibleWith(COMPATIBILITY_YOUTUBE) @@ -55,8 +60,16 @@ val addToQueuePatch = bytecodePatch( execute { PreferenceScreen.FEED.addPreferences( noTitleUnsortedPreferenceCategory( - SwitchPreference("morphe_local_queue", summary = true), - SwitchPreference("morphe_queue_override_flyout_menu", summary = true), + // The native queue requires 21.16+. + if (is_21_15_or_greater) { + ListPreference("morphe_queue_type") + } else { + ListPreference( + key = "morphe_queue_type", + entriesKey = "morphe_queue_type_legacy_entries", + entryValuesKey = "morphe_queue_type_legacy_entry_values" + ) + }, SwitchPreference("morphe_queue_add_flyout_menu", summary = true) ) ) diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/shortsautoplay/Fingerprints.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/shortsautoplay/Fingerprints.kt index ff2827afd9..cb36e69183 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/shortsautoplay/Fingerprints.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/shortsautoplay/Fingerprints.kt @@ -39,7 +39,11 @@ private object ReelPlaybackRepeatParentFingerprint : Fingerprint( ) ) -internal object ReelPlaybackRepeatFingerprint : Fingerprint( +/** + * @param reelEnumClass The loop behavior enum. 21.43+ has a sibling method that + * also uses Optional, but does not look up the loop behavior. + */ +internal fun reelPlaybackRepeatFingerprint(reelEnumClass: String) = Fingerprint( classFingerprint = ReelPlaybackRepeatParentFingerprint, returnType = "V", parameters = listOf("L"), @@ -47,7 +51,8 @@ internal object ReelPlaybackRepeatFingerprint : Fingerprint( anyInstruction( methodCall(smali = "Lcom/google/common/util/concurrent/ListenableFuture;->isDone()Z"), methodCall(smali = $$"Lj$/util/Optional;->ofNullable(Ljava/lang/Object;)Lj$/util/Optional;") // 21.17+ - ) + ), + methodCall(returnType = reelEnumClass) ) ) diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/shortsautoplay/ShortsAutoplayPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/shortsautoplay/ShortsAutoplayPatch.kt index 6f2e0032d9..e0a67c7929 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/shortsautoplay/ShortsAutoplayPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/shortsautoplay/ShortsAutoplayPatch.kt @@ -73,7 +73,9 @@ val shortsAutoplayPatch = bytecodePatch( ) } - ReelPlaybackRepeatFingerprint.method.apply { + val reelPlaybackRepeat = reelPlaybackRepeatFingerprint(reelEnumClass) + + reelPlaybackRepeat.method.apply { // The behavior enums are looked up from an ordinal value to an enum type. findInstructionIndicesReversedOrThrow( if (is_21_25_or_greater) { @@ -116,7 +118,7 @@ val shortsAutoplayPatch = bytecodePatch( val reelSequenceControllerMethodReference = ReelPlaybackLegacyFingerprint.instructionMatches[2] .getInstruction().reference as MethodReference - ReelPlaybackRepeatFingerprint.method.apply { + reelPlaybackRepeat.method.apply { // Find the first call modified by extension code above. val extensionReturnResultIndex = indexOfFirstInstructionOrThrow { opcode == Opcode.INVOKE_STATIC && @@ -125,7 +127,7 @@ val shortsAutoplayPatch = bytecodePatch( val enumRegister = getInstruction(extensionReturnResultIndex).registerA val getReelSequenceControllerReference = getInstanceFieldGetFingerprint( reelSequenceControllerMethodReference.definingClass - ).match(ReelPlaybackRepeatFingerprint.originalMethod) + ).match(reelPlaybackRepeat.originalMethod) .instructionMatches.first().instruction.getReference()!! // Add a helper method to avoid finding multiple free registers. @@ -163,7 +165,7 @@ val shortsAutoplayPatch = bytecodePatch( """ ) } - ReelPlaybackRepeatFingerprint.classDef.methods.add(helperMethod) + reelPlaybackRepeat.classDef.methods.add(helperMethod) addInstructionsWithLabels( extensionReturnResultIndex + 1, diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/shared/OpenVideoIntentPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/shared/OpenVideoIntentPatch.kt index d0ceee8ebb..0a49d6e41e 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/shared/OpenVideoIntentPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/shared/OpenVideoIntentPatch.kt @@ -10,6 +10,7 @@ package app.morphe.patches.youtube.shared import app.morphe.patcher.extensions.InstructionExtensions.addInstruction import app.morphe.patcher.extensions.InstructionExtensions.addInstructionsWithLabels import app.morphe.patcher.patch.bytecodePatch +import app.morphe.patcher.util.findFreeRegister import app.morphe.patcher.util.getFreeRegisterProvider import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod import app.morphe.patches.all.misc.resources.addResourcesPatch @@ -32,6 +33,10 @@ private var videoPlaybackIntentFreeRegister2: Int = 0 private lateinit var shortsPlaybackIntentMethod: WeakReference +private val videoIntentContextHooks = mutableListOf>() +private val videoIntentCommandTransforms = mutableListOf>() +private val videoIntentInterceptors = mutableListOf>() + @Suppress("unused") val openVideoIntentPatch = bytecodePatch( description = "Provides hooks into intent components to open videos for extension filtering." @@ -45,6 +50,9 @@ val openVideoIntentPatch = bytecodePatch( ) execute { + videoIntentContextHooks.clear() + videoIntentCommandTransforms.clear() + videoIntentInterceptors.clear() VideoPlaybackIntentFingerprint.let { it.method.apply { val match = it.instructionMatches[1] @@ -77,6 +85,62 @@ val openVideoIntentPatch = bytecodePatch( else ShortsPlaybackIntentFingerprintLegacy).method ) } + + finalize { + // Existing filtering hooks cache an instruction index during execute. + // Insert entry transforms only after all consumers have used that index. + val method = videoPlaybackIntentMethod.get()!! + videoIntentContextHooks.asReversed().forEach { (classDescriptor, methodName) -> + method.addInstructionsAtControlFlowLabel( + 0, + """ + invoke-static/range {p1 .. p2}, $classDescriptor->$methodName(Lcom/google/protobuf/MessageLite;Ljava/util/Map;)Ljava/util/Map; + move-result-object p2 + """ + ) + } + videoIntentInterceptors.asReversed().forEach { (classDescriptor, methodName) -> + val consumed = method.findFreeRegister(0) + method.addInstructionsAtControlFlowLabel( + 0, + """ + invoke-static/range {p1 .. p2}, $classDescriptor->$methodName(Lcom/google/protobuf/MessageLite;Ljava/util/Map;)Z + move-result v$consumed + if-eqz v$consumed, :continue_video_intent + return-void + :continue_video_intent + nop + """ + ) + } + // Normalize the immutable command before context hooks or the native + // descriptor can observe its original playlist. Retain its native type. + videoIntentCommandTransforms.asReversed().forEach { (classDescriptor, methodName) -> + method.addInstructionsAtControlFlowLabel( + 0, + """ + invoke-static/range {p1 .. p2}, $classDescriptor->$methodName(Lcom/google/protobuf/MessageLite;Ljava/util/Map;)Lcom/google/protobuf/MessageLite; + move-result-object p1 + check-cast p1, ${method.parameterTypes.first()} + """ + ) + } + } +} + +/** Transform the command context before native watch-descriptor construction, without consuming it. */ +fun hookVideoIntentContext(classDescriptor: String, methodName: String) { + videoIntentContextHooks += classDescriptor to methodName +} + +/** Transform the watch command before its context and native playback descriptor are read. */ +fun hookVideoIntentCommandTransform(classDescriptor: String, methodName: String) { + videoIntentCommandTransforms += classDescriptor to methodName +} + +/** Intercept a normalized watch command before context mutations or playback starts. */ +fun hookVideoIntentInterceptor(classDescriptor: String, methodName: String) { + videoIntentInterceptors += classDescriptor to methodName } /** diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/video/information/VideoInformationPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/video/information/VideoInformationPatch.kt index 5f0078d270..8592340a08 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/video/information/VideoInformationPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/video/information/VideoInformationPatch.kt @@ -19,7 +19,6 @@ import app.morphe.patcher.patch.BytecodePatchContext import app.morphe.patcher.patch.PatchException import app.morphe.patcher.patch.bytecodePatch import app.morphe.patcher.util.getReference -import app.morphe.patcher.util.matchSingle import app.morphe.patcher.util.p0Register import app.morphe.patcher.util.proxy.mutableTypes.MutableClass import app.morphe.patcher.util.proxy.mutableTypes.MutableField @@ -577,7 +576,7 @@ val videoInformationPatch = bytecodePatch( } // A preloaded playback, such as the next Short, becomes the current one without loadVideo(). - currentPlaybackFingerprint(playerResponseType).matchSingle().let { currentPlayback -> + currentPlaybackFingerprint(playerResponseType).let { currentPlayback -> currentPlayback.method.apply { val index = currentPlayback.instructionMatches.last().index val playerResponseRegister = getInstruction(index).registerC diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/video/series/HistoryPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/video/series/HistoryPatch.kt index 475da607cc..94d351b364 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/video/series/HistoryPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/video/series/HistoryPatch.kt @@ -139,8 +139,10 @@ internal fun BytecodePatchContext.wireHistory() { fieldAccess(definingClass = "this", type = "L"), ) val endpointField = create.getInstruction(endpointIndex).getReference()!! - val routeMethod = BrowseRouteFingerprint(endpointField.type) - .instructionMatches.first().getMethodCalled() + val routeMethod = BrowseRouteFingerprint(endpointField.type).matchAll() + .map { it.instructionMatches.first().getMethodCalled() } + .distinctBy { it.toString() } + .single() fragment.methods.add( ImmutableMethod( diff --git a/patches/src/main/resources/addresources/values/youtube/arrays.xml b/patches/src/main/resources/addresources/values/youtube/arrays.xml index 95f35dc4bd..e7b131b9ca 100644 --- a/patches/src/main/resources/addresources/values/youtube/arrays.xml +++ b/patches/src/main/resources/addresources/values/youtube/arrays.xml @@ -354,6 +354,31 @@ CARDS_WITH_ACTION_BUTTONS + + + @string/morphe_queue_type_entry_1 + @string/morphe_queue_type_entry_2 + @string/morphe_queue_type_entry_3 + @string/morphe_queue_type_entry_4 + + + DEFAULT + NATIVE + MORPHE + LOCAL + + + + @string/morphe_queue_type_entry_1 + @string/morphe_queue_type_entry_3 + @string/morphe_queue_type_entry_4 + + + DEFAULT + MORPHE + LOCAL + + @string/morphe_auto_captions_style_entry_1 diff --git a/patches/src/main/resources/addresources/values/youtube/strings.xml b/patches/src/main/resources/addresources/values/youtube/strings.xml index 0404b8e747..f678fc5467 100644 --- a/patches/src/main/resources/addresources/values/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values/youtube/strings.xml @@ -652,14 +652,14 @@ Info: May not work on live streams" Queue is kept after restarting the app Swap Save and Queue actions Tap for queue options, tap and hold to save to Watch later - Override feed flyout queue button - Opens the Morphe queue instead of the YouTube queue + + Play next in queue type + Default + Native queue + Morphe queue + Morphe queue client side Add feed flyout queue menu item Adds an additional flyout item. Enable this if \"Add to queue\" is not shown - Premium-style queue - "Replaces the Morphe queue with one that lives on the device, plays through on its own, and can be reordered by dragging - -Info: The next queued video only starts while YouTube is on screen" Queue Queue (%s) Clear all @@ -705,6 +705,21 @@ Info: The next queued video only starts while YouTube is on screen" Open queue Save queue to playlist + + Playlist saved + Saved to %s + Clear queue + Queue cleared + Queue + Play last in queue + Play next in queue + YouTube could not display the queue. + The queue is not ready for this video. Open the video player and try again. + Add Mix to queue or watch Mix? + Watch + Add to queue + Loading Mix… + Open channel from live avatar Prevents the live stream from opening when tapping the channel\'s avatar diff --git a/patches/src/test/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/PlaylistQueueApkBuild.kt b/patches/src/test/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/PlaylistQueueApkBuild.kt new file mode 100644 index 0000000000..e0e37cdef8 --- /dev/null +++ b/patches/src/test/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/PlaylistQueueApkBuild.kt @@ -0,0 +1,779 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to Morphe contributions. + */ + +package app.morphe.patches.youtube.interaction.nativeplaylistqueue + +import app.morphe.patcher.Patcher +import app.morphe.patcher.PatcherConfig +import app.morphe.patcher.apk.ApkUtils.applyTo +import app.morphe.patcher.dex.DexVerifier +import app.morphe.patcher.dex.SdkDexVerifier +import app.morphe.patcher.patch.Patch +import app.morphe.patches.youtube.interaction.ask.askFlyoutMenuPatch +import app.morphe.patches.youtube.interaction.downloads.downloadsPatch +import app.morphe.patches.youtube.interaction.feedactions.feedVideoActionsPatch +import app.morphe.patches.youtube.interaction.loop.loopVideoPatch +import app.morphe.patches.youtube.interaction.reload.reloadVideoButtonPatch +import app.morphe.patches.youtube.interaction.savetowatchlater.saveToWatchLaterButtonPatch +import app.morphe.patches.youtube.layout.buttons.action.hideVideoActionButtonsPatch +import app.morphe.patches.youtube.layout.buttons.overlay.hidePlayerOverlayButtonsPatch +import app.morphe.patches.youtube.layout.dearrow.deArrowPatch +import app.morphe.patches.youtube.layout.feedcards.feedCardLayoutPatch +import app.morphe.patches.youtube.layout.flyout.addToQueuePatch +import app.morphe.patches.youtube.layout.hide.general.hideLayoutComponentsPatch +import app.morphe.patches.youtube.layout.hide.relatedvideos.hideRelatedVideosPatch +import app.morphe.patches.youtube.layout.livering.openChannelOfLiveAvatarPatch +import app.morphe.patches.youtube.layout.miniplayer.miniplayerPatch +import app.morphe.patches.youtube.layout.originaltitles.restoreOriginalTitlesPatch +import app.morphe.patches.youtube.layout.player.fullscreen.exitFullscreenPatch +import app.morphe.patches.youtube.layout.player.fullscreen.openVideosFullscreenPatch +import app.morphe.patches.youtube.layout.playlistautoplay.disablePlaylistAutoplayPatch +import app.morphe.patches.youtube.layout.returnyoutubedislike.returnYouTubeDislikePatch +import app.morphe.patches.youtube.layout.sharesheet.openSystemShareSheetPatch +import app.morphe.patches.youtube.misc.backgroundplayback.backgroundPlaybackPatch +import app.morphe.patches.youtube.misc.fix.videoactionbar.restoreOldVideoActionBarPatch +import app.morphe.patches.youtube.misc.medianotification.mediaNotificationControlsPatchYouTube +import app.morphe.patches.youtube.misc.spoof.spoofVideoStreamsPatch +import app.morphe.patches.youtube.video.quality.rememberVideoQualityPatch +import app.morphe.patches.youtube.video.series.seriesTrackerPatch +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.Opcodes +import com.android.tools.smali.dexlib2.analysis.ClassPath +import com.android.tools.smali.dexlib2.analysis.ClassProvider +import com.android.tools.smali.dexlib2.analysis.DexClassProvider +import com.android.tools.smali.dexlib2.analysis.MethodAnalyzer +import com.android.tools.smali.dexlib2.analysis.RegisterType +import com.android.tools.smali.dexlib2.dexbacked.DexBackedDexFile +import com.android.tools.smali.dexlib2.iface.ClassDef +import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.Instruction +import com.android.tools.smali.dexlib2.iface.instruction.NarrowLiteralInstruction +import com.android.tools.smali.dexlib2.iface.instruction.OffsetInstruction +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction +import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction +import com.android.tools.smali.dexlib2.iface.reference.FieldReference +import com.android.tools.smali.dexlib2.iface.reference.MethodReference +import com.android.tools.smali.dexlib2.iface.reference.StringReference +import com.android.tools.smali.dexlib2.iface.reference.TypeReference +import kotlinx.coroutines.runBlocking +import java.io.File +import kotlin.io.path.createTempDirectory + +/** Applies the real patch and runs SDK DEX verification. Playback behavior still requires a phone. */ +fun main(arguments: Array) { + require(arguments.size in 1..3) { + "Usage: PlaylistQueueApkBuildKt [standalone|combined|unselected] [unsigned-output]" + } + val input = File(arguments[0]).canonicalFile + require(input.isFile) { "Stock APK does not exist: $input" } + val mode = arguments.getOrElse(1) { "standalone" } + require(mode in setOf("standalone", "combined", "unselected")) + val sdk = System.getenv("ANDROID_HOME") ?: error("ANDROID_HOME is required for SDK verification") + // Add to queue includes the native queue, so it is only selected in combined mode. + val relatedPatches = setOf( + saveToWatchLaterButtonPatch, downloadsPatch, openSystemShareSheetPatch(), loopVideoPatch, + reloadVideoButtonPatch, + disablePlaylistAutoplayPatch, hidePlayerOverlayButtonsPatch, hideVideoActionButtonsPatch, + restoreOriginalTitlesPatch, deArrowPatch, openChannelOfLiveAvatarPatch, returnYouTubeDislikePatch, + hideLayoutComponentsPatch, hideRelatedVideosPatch, exitFullscreenPatch, openVideosFullscreenPatch, + mediaNotificationControlsPatchYouTube, backgroundPlaybackPatch, + spoofVideoStreamsPatch, restoreOldVideoActionBarPatch, + feedVideoActionsPatch, feedCardLayoutPatch, seriesTrackerPatch, askFlyoutMenuPatch, miniplayerPatch, + rememberVideoQualityPatch + ) + val selected = when (mode) { + "standalone" -> setOf(nativePlaylistQueuePatch) + "combined" -> relatedPatches + addToQueuePatch + else -> relatedPatches + } + fun requiresQueue(patch: Patch<*>, visited: MutableSet> = mutableSetOf()): Boolean = + patch == nativePlaylistQueuePatch || visited.add(patch) && patch.dependencies.any { requiresQueue(it, visited) } + check(relatedPatches.none { requiresQueue(it) }) { + "Unrelated patches must not select Native playlist queue as a dependency" + } + val workspace = createTempDirectory("playlist-queue-$mode").toFile() + val sdkVerifier = SdkDexVerifier(File(sdk)) + val verifier = object : DexVerifier by sdkVerifier { + override fun verifyDexDirectory(dexDir: File) { + sdkVerifier.verifyDexDirectory(dexDir) + verifyNativePanelAssignment(dexDir, mode != "unselected") + verifyNativeRowMenuPaths(dexDir, mode != "unselected") + verifyNativeFullscreenActions(dexDir, mode != "unselected") + verifyNativeQueueUndo(dexDir, mode != "unselected") + verifyNativeMixTap(dexDir, mode != "unselected") + } + } + Patcher(PatcherConfig(input, workspace, verifier = verifier)).use { patcher -> + patcher += selected + runBlocking { + patcher().collect { result -> + result.exception?.let { throw it } + println("Applied: ${result.patch.name}") + } + } + val result = patcher.get() + arguments.getOrNull(2)?.let { result.applyTo(File(it)) } + println("$mode: APK patched and SDK DEX/resource verification completed: $workspace") + } +} + +/** The dialog must consume navigation before context hooks invalidate the playing queue. */ +private fun verifyNativeMixTap(directory: File, selected: Boolean) { + val hooks = directory.listFiles()!!.filter { it.extension == "dex" }.flatMap { file -> + file.inputStream().buffered().use { DexBackedDexFile.fromInputStream(Opcodes.getDefault(), it) } + .classes.filter { !it.type.startsWith("Lapp/morphe/") }.flatMap { it.methods } + }.mapNotNull { method -> + val code = method.implementation?.instructions?.toList() ?: return@mapNotNull null + val names = code.map { ((it as? ReferenceInstruction)?.reference as? MethodReference) + ?.takeIf { it.definingClass == EXTENSION_CLASS }?.name } + val intercept = names.indexOf("onQueueMixTap") + if (intercept < 0) return@mapNotNull null + check(names.indexOf("prepareQueueVideoCommand") in 0 until intercept && + names.indexOf("prepareQueueStartContext") > intercept) { + "Mix choice must run after music normalization and before watch context mutations" + } + check(code[intercept + 1].opcode == Opcode.MOVE_RESULT && + code[intercept + 2].opcode == Opcode.IF_EQZ && code[intercept + 3].opcode == Opcode.RETURN_VOID) { + "An owned Mix tap must return before native playback starts" + } + method + } + check(hooks.size == if (selected) 1 else 0) { "Unexpected Native queue Mix tap interception" } + println("Native Mix choice ordering and queue-unselected isolation verified") +} + +/** Verify the emitted UI bridge reaches the native setter and size subject, not playback. */ +private fun verifyNativeFullscreenActions(directory: File, selected: Boolean) { + val classes = directory.listFiles()!!.filter { it.extension == "dex" }.flatMap { file -> + file.inputStream().buffered().use { + DexBackedDexFile.fromInputStream(Opcodes.getDefault(), it).classes.toList() + } + } + val extension = "Lapp/morphe/extension/youtube/patches/queue/QueueFullscreenActions;" + val controllerInterface = "${extension.removeSuffix(";")}\$Controller;" + val controllers = classes.filter { controllerInterface in it.interfaces } + if (!selected) { + check(controllers.isEmpty()) { "Fullscreen queue UI bridge leaked into an unselected build" } + println("Native fullscreen playlist bridge absent when unselected") + return + } + val controller = controllers.single() + fun references(name: String) = controller.methods.single { it.name == name }.implementation!! + .instructions.mapNotNull { (it as? ReferenceInstruction)?.reference as? MethodReference } + val setter = controller.methods.single { method -> method.implementation?.instructions?.any { + (it as? ReferenceInstruction)?.reference?.toString()?.startsWith("$extension->onModelLoaded(") == true + } == true } + val refresh = references("patch_refreshQuickActions") + val reset = refresh.first() + check(reset.definingClass == controller.type && reset.toString() != setter.toString()) { + "Fullscreen refresh must dispose the cached native UI before setting a new model" + } + val nativeReset = controller.methods.single { it.toString() == reset.toString() } + check(nativeReset.implementation!!.instructions.any { + (it as? ReferenceInstruction)?.reference?.toString() == setter.toString() + }) { "Fullscreen refresh no longer uses the original native model cleanup" } + check(refresh.any { it.toString() == setter.toString() }) { + "Fullscreen refresh does not reuse the native model setter" + } + check(refresh.any { it.parameterTypes == listOf("Ljava/lang/Object;") && it.returnType == "V" && + it.definingClass != controller.type }) { "Fullscreen refresh lost the native size emission" } + check(references("patch_parseQuickActions").any { + it.definingClass == "Lcom/google/protobuf/ExtensionRegistryLite;" && it.name == "getGeneratedRegistry" + }) { "Fullscreen model parser does not retain native extensions" } + println("Native fullscreen playlist cleanup, setter, size refresh and registry bridges verified") +} + +/** Check the emitted native video bridge and queue-only menu dispatch paths. */ +private fun verifyNativeRowMenuPaths(directory: File, selected: Boolean) { + val dexFiles = directory.listFiles()!!.filter { it.extension == "dex" }.map { file -> + file.inputStream().buffered().use { DexBackedDexFile.fromInputStream(Opcodes.getDefault(), it) } + } + val classes = dexFiles.flatMap { it.classes } + val queueRow = "${EXTENSION_CLASS.removeSuffix(";")}\$QueueRow;" + val videoIdInterface = "Lapp/morphe/extension/youtube/patches/utils/FlyoutUtils\$FlyoutMenuVideoIdInterface;" + val extensionOwner = EXTENSION_CLASS.removePrefix("L").removeSuffix(";") + val rowTypes = classes.filter { queueRow in it.interfaces } + val menuOpenCalls = classes.flatMap { owner -> owner.methods.mapNotNull { method -> + method.implementation?.instructions?.toList()?.let { instructions -> + instructions.indexOfFirst { instruction -> + (instruction as? ReferenceInstruction)?.reference?.toString()?.startsWith( + "L$extensionOwner;->onRowMenuOpened(Ljava/util/Map;)V") == true + }.takeIf { it >= 0 }?.let { Triple(owner, method, instructions) } + } + } } + val menuDispatches = classes.flatMap { owner -> owner.methods.mapNotNull { method -> + method.implementation?.instructions?.toList()?.let { instructions -> + instructions.indexOfFirst { instruction -> + (instruction as? ReferenceInstruction)?.reference?.toString()?.startsWith( + "L$extensionOwner;->onQueueMenuCommand(Lcom/google/protobuf/MessageLite;Ljava/util/Map;)") == true + }.takeIf { index -> index >= 0 && instructions.getOrNull(index + 2)?.opcode == Opcode.IF_NEZ && + instructions.getOrNull(index + 3)?.opcode == Opcode.GOTO_16 } + ?.let { Triple(owner, method, instructions) } + } + } } + val saveEndpointInterface = "Lapp/morphe/extension/youtube/patches/queue/QueueSaveActions\$NativeEndpoint;" + val saveEndpoints = classes.filter { saveEndpointInterface in it.interfaces } + val saveDispatches = classes.flatMap { it.methods }.mapNotNull { method -> + val instructions = method.implementation?.instructions?.toList() ?: return@mapNotNull null + val index = instructions.indexOfFirst { + (it as? ReferenceInstruction)?.reference?.toString()?.startsWith( + "L$extensionOwner;->onQueueSaveCommand(") == true + } + if (index < 0) null else Triple(method, instructions, index) + } + val taggedSheetGuards = classes.flatMap { it.methods }.mapNotNull { method -> + if (method.returnType != "V" || method.parameterTypes.size != 12) return@mapNotNull null + val instructions = method.implementation?.instructions?.toList() ?: return@mapNotNull null + val index = instructions.indexOfFirst { + (it as? ReferenceInstruction)?.reference?.toString()?.startsWith( + "L$extensionOwner;->isQueueSaveSelection(") == true + } + if (index < 0) null else instructions to index + } + val scopedRowFactories = classes.flatMap { it.methods }.filter { + it.name in setOf("patch_buildQueueSaveRows", "patch_decodeQueueSaveRows") + } + check(saveEndpoints.isEmpty() && saveDispatches.isEmpty() && taggedSheetGuards.isEmpty() && + scopedRowFactories.isEmpty()) { + "Queue batch Save hooks must not modify the player's native Save paths" + } + println("Experimental batch Save hooks, endpoint bridge and shared row factories absent") + val batchEndpointInterface = "Lapp/morphe/extension/youtube/patches/queue/QueueSaveActions\$Endpoint;" + val batchPickerInterface = "Lapp/morphe/extension/youtube/patches/queue/QueueSaveActions\$Picker;" + val batchEndpoints = classes.filter { batchEndpointInterface in it.interfaces } + val batchPickers = classes.filter { batchPickerInterface in it.interfaces } + val argumentTransfers = classes.flatMap { it.methods }.mapNotNull { method -> + val instructions = method.implementation?.instructions?.toList() ?: return@mapNotNull null + val index = instructions.indexOfFirst { + (it as? ReferenceInstruction)?.reference?.toString()?.contains("->onQueueSaveArguments(") == true + } + if (index < 0) null else Triple(method, instructions, index) + } + val batchRequests = classes.flatMap { it.methods }.mapNotNull { method -> + val instructions = method.implementation?.instructions?.toList() ?: return@mapNotNull null + val index = instructions.indexOfFirst { + (it as? ReferenceInstruction)?.reference?.toString()?.contains("QueueSaveActions;->onPickerRequest(") == true + } + if (index < 0) null else Triple(method, instructions, index) + } + val responseHooks = classes.flatMap { it.methods }.filter { method -> + method.implementation?.instructions?.any { + (it as? ReferenceInstruction)?.reference?.toString()?.contains("QueueSaveActions;->onPickerResponse(") == true + } == true + } + val queueStartHooks = classes.flatMap { it.methods }.flatMap { method -> + method.implementation?.instructions?.filter { + (it as? ReferenceInstruction)?.reference?.toString() + ?.contains("NativePlaylistQueuePatch;->prepareQueueStartContext(") == true + }?.toList() ?: emptyList() + } + check(queueStartHooks.size == if (selected) 1 else 0) { + "Queue minimized-start hook must exist only when selected" + } + // Other patches use a cached insertion index in the watch resolver. A new + // entry hook must not move their filters before native descriptor construction. + val watch = classes.flatMap { it.methods }.single { method -> + !method.definingClass.startsWith("Lapp/morphe/extension/") && method.parameterTypes.size == 2 && method.parameterTypes[1] == "Ljava/util/Map;" && + method.implementation?.instructions?.any { + ((it as? ReferenceInstruction)?.reference as? StringReference)?.string == "start_watch_minimized" + } == true + }.implementation!!.instructions.toList() + val queueContextIndex = watch.indexOfFirst { + (it as? ReferenceInstruction)?.reference?.toString()?.contains("->prepareQueueStartContext(") == true + } + val queueCommandIndex = watch.indexOfFirst { + (it as? ReferenceInstruction)?.reference?.toString()?.contains("->prepareQueueVideoCommand(") == true + } + check((queueCommandIndex >= 0) == selected) { "Queue watch normalization must exist only when selected" } + if (selected) { + check(queueCommandIndex in 0 until queueContextIndex + && watch[queueCommandIndex + 1].opcode == Opcode.MOVE_RESULT_OBJECT + && watch[queueCommandIndex + 2].opcode == Opcode.CHECK_CAST) { + "Normalize and restore the native command type before context hooks or descriptor construction" + } + } + val cardObserverIndex = watch.indexOfFirst { + (it as? ReferenceInstruction)?.reference?.toString()?.startsWith( + "Lapp/morphe/extension/youtube/patches/FeedCardLayoutPatch;->onWatchCommand(") == true + } + if (selected && cardObserverIndex >= 0) { + check(queueContextIndex in 0 until cardObserverIndex) { + "Queue context must be transformed before the existing feed-card watch observer" + } + } + watch.forEachIndexed { index, instruction -> + val call = (instruction as? ReferenceInstruction)?.reference as? MethodReference + if (call?.name != "onVideoIntentLoaded" || call.definingClass !in setOf( + "Lapp/morphe/extension/youtube/patches/OpenChannelOfLiveAvatarPatch;", + "Lapp/morphe/extension/youtube/patches/components/CommentsFilter;")) return@forEachIndexed + val getter = watch[index - 2] as FiveRegisterInstruction + val receiver = getter.registerC + val lastWrite = (0 until index - 2).last { + (watch[it] as? OneRegisterInstruction)?.registerA == receiver + } + check(watch[lastWrite].opcode == Opcode.MOVE_RESULT_OBJECT) { + "Video intent filter ran before the native playback descriptor was constructed" + } + val construction = (watch[lastWrite - 1] as ReferenceInstruction).reference as MethodReference + val videoGetter = (watch[index - 2] as ReferenceInstruction).reference as MethodReference + check(construction.returnType == videoGetter.definingClass) + } + val feedbackInterface = "Lapp/morphe/extension/youtube/patches/queue/QueueSaveFeedback\$Callback;" + val feedbackCallbacks = classes.filter { feedbackInterface in it.interfaces } + val feedbackHooks = classes.flatMap { it.methods }.mapNotNull { method -> + val calls = method.implementation?.instructions?.filter { + (it as? ReferenceInstruction)?.reference?.toString()?.let { reference -> + reference.contains("QueueSaveActions;->onEditCreated(") || + reference.contains("QueueSaveActions;->onEditResult(") + } == true + }?.toList() ?: emptyList() + if (calls.isEmpty()) null else method to calls + } + if (selected) { + val callback = feedbackCallbacks.single() + check(callback.fields.count { it.name == "patch_queueSaveSelection" } == 1) + check(feedbackHooks.size == 2 && feedbackHooks.sumOf { it.second.size } == 2) + check(feedbackHooks.any { it.first.definingClass == callback.type }) + println("Queue-only save callback ownership and native success feedback bridges present") + } else { + check(feedbackCallbacks.isEmpty() && feedbackHooks.isEmpty()) { + "Queue save confirmation hooks leaked into an unselected build" + } + } + if (selected) { + check(batchEndpoints.isEmpty()) { "Transient endpoint instance markers must not own a batch selection" } + val picker = batchPickers.single() + val (factory, factoryInstructions, transferIndex) = argumentTransfers.single() + val transfer = factoryInstructions[transferIndex] as FiveRegisterInstruction + val serialization = factoryInstructions[transferIndex + 1] as ReferenceInstruction + check(factory.parameterTypes.size == 1 && + serialization.reference.toString().endsWith("->toByteArray()[B") && + (serialization as FiveRegisterInstruction).registerC == transfer.registerC) { + "Owned selection must enter native saved arguments before endpoint serialization overwrites its register" + } + check(batchRequests.isEmpty()) { "Queue Save must retain the stock single-video picker request" } + val getter = picker.methods.single { it.name == "patch_getQueueSaveVideoIds" }.implementation!!.instructions + check(getter.any { (it as? ReferenceInstruction)?.reference?.toString()?.contains("->getQueueSaveVideoIds(") == true }) + check(responseHooks.size == 1 && responseHooks.single().definingClass != picker.type) + check(picker.methods.count { it.name == "patch_parseQueueSaveResponse" } == 1) + println("Queue-owned batch Save: serialized selection, saved arguments and destination commands wired; picker request untouched") + } else { + check(batchEndpoints.isEmpty() && batchPickers.isEmpty() && argumentTransfers.isEmpty() && + batchRequests.isEmpty() && responseHooks.isEmpty()) { + "Native batch Save bridge leaked into a patch-unselected build" + } + } + if (!selected) { + check(rowTypes.isEmpty() && menuOpenCalls.isEmpty() && menuDispatches.isEmpty() && + saveEndpoints.isEmpty() && saveDispatches.isEmpty() && taggedSheetGuards.isEmpty() && + scopedRowFactories.isEmpty()) { + "Native playlist row menu bridge leaked into an unselected build" + } + println("Native playlist row menu bridge absent when unselected") + return + } + + val videoType = rowTypes.singleOrNull() + ?: error("Expected one queue row type in emitted DEX, found ${rowTypes.size}") + check(videoIdInterface in videoType.interfaces) { + "Native queue row does not implement the shared FlyoutUtils video-ID interface" + } + val videoIdGetter = videoType.methods.single { it.name == "patch_getVideoId" } + val getterInstructions = videoIdGetter.implementation!!.instructions.toList() + val getterField = getterInstructions.single { it.opcode == Opcode.IGET_OBJECT } + .let { (it as ReferenceInstruction).reference as FieldReference } + check(getterField.definingClass == videoType.type && getterField.type == "Ljava/lang/String;" && + getterInstructions.last().opcode == Opcode.RETURN_OBJECT) { + "Queue row's shared video-ID getter does not return its native String field" + } + check(classes.flatMap { it.methods }.any { method -> + method.name != "patch_getVideoId" && method.implementation?.instructions?.any { instruction -> + instruction.opcode == Opcode.IGET_OBJECT && + (instruction as? ReferenceInstruction)?.reference == getterField + } == true + }) { "Binder-derived video-ID field is not read by native code in emitted DEX" } + + val extensionClass = classes.single { it.type == EXTENSION_CLASS } + val rowMenuHelper = extensionClass.methods.single { it.name == "onRowMenuOpened" } + val rowMenuInstructions = rowMenuHelper.implementation!!.instructions.toList() + val instanceOfTypes = rowMenuInstructions.filter { it.opcode == Opcode.INSTANCE_OF } + .mapNotNull { (it as? ReferenceInstruction)?.reference?.toString() }.toSet() + check(queueRow in instanceOfTypes && videoIdInterface in instanceOfTypes) { + "Row menu helper does not guard both queue ownership and the native row's shared video-ID interface" + } + check(rowMenuInstructions.any { instruction -> + instruction.opcode == Opcode.INVOKE_STATIC && + (instruction as? ReferenceInstruction)?.reference?.toString()?.startsWith( + "Lapp/morphe/extension/youtube/patches/utils/FlyoutUtils;->extractFlyoutIdFromMap(Ljava/util/Map;)V" + ) == true + }) { "Shared row menu video context extractor is not reached by the emitted helper" } + + val menuOpen = menuOpenCalls.singleOrNull() + ?: error("Expected one row menu context extraction call, found ${menuOpenCalls.size}") + val (menuOwner, menuMethod, openInstructions) = menuOpen + val openCallIndex = openInstructions.indexOfFirst { instruction -> + (instruction as? ReferenceInstruction)?.reference?.toString()?.startsWith( + "L$extensionOwner;->onRowMenuOpened(Ljava/util/Map;)V") == true + } + val openCall = openInstructions[openCallIndex] as FiveRegisterInstruction + val mapResult = openInstructions.getOrNull(openCallIndex - 1) as? com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction + val mapFactory = openInstructions.getOrNull(openCallIndex - 2) as? ReferenceInstruction + val factoryReference = mapFactory?.reference as? MethodReference + check(mapResult != null && openCallIndex >= 2 && + openInstructions[openCallIndex - 1].opcode == Opcode.MOVE_RESULT_OBJECT && + mapResult.registerA == openCall.registerC && factoryReference != null && + factoryReference.returnType.startsWith("L") && + factoryReference.parameterTypes.map { it.toString() } == List(4) { "Ljava/lang/Object;" }) { + "Row menu context extraction is not immediately after its native four-object map factory" + } + println("Native row menu context bridge: ${menuOwner.type}->${menuMethod.name}, shared interface and native String getter verified") + + val dispatch = menuDispatches.singleOrNull() + ?: error("Expected one consumed native menu removal path, found ${menuDispatches.size}") + val dispatchInstructions = dispatch.third + val removeIndex = dispatchInstructions.indexOfFirst { instruction -> + (instruction as? ReferenceInstruction)?.reference?.toString()?.startsWith( + "L$extensionOwner;->onQueueMenuCommand(Lcom/google/protobuf/MessageLite;Ljava/util/Map;)") == true + } + check(removeIndex >= 0 && dispatchInstructions[removeIndex + 1].opcode == Opcode.MOVE_RESULT_OBJECT && + dispatchInstructions[removeIndex + 2].opcode == Opcode.IF_NEZ && + dispatchInstructions[removeIndex + 3].opcode == Opcode.GOTO_16) { + "Native removal result does not branch between the original callback and dismissal" + } + val branchAddresses = HashMap() + var address = 0 + dispatchInstructions.forEachIndexed { index, instruction -> + branchAddresses[address] = index + address += instruction.codeUnits + } + fun branchTarget(index: Int): Int { + val branch = dispatchInstructions[index] as OffsetInstruction + val instructionAddress = branchAddresses.entries.single { it.value == index }.key + return branchAddresses[instructionAddress + branch.codeOffset] + ?: error("Native row menu branch target is not an instruction boundary") + } + val callbackIndex = branchTarget(removeIndex + 2) + val dismissalIndex = branchTarget(removeIndex + 3) + check(dispatchInstructions[callbackIndex].opcode == Opcode.CHECK_CAST && + dispatchInstructions.getOrNull(callbackIndex + 1)?.opcode == Opcode.INVOKE_INTERFACE && + dispatchInstructions[dismissalIndex].opcode == Opcode.INVOKE_VIRTUAL && + (dispatchInstructions[dismissalIndex] as ReferenceInstruction).reference + .let { reference -> reference is MethodReference && reference.returnType == "V" && + reference.parameterTypes.isEmpty() } && + dispatchInstructions.getOrNull(dismissalIndex + 1)?.opcode == Opcode.RETURN_VOID) { + "Native removal branch does not preserve the original callback and terminal dismissal paths" + } + println("Native row menu dispatch preserves original callback and dismissal branches") +} + +/** SDK structural checks do not catch a base protobuf value stored in a concrete panel field. */ +private fun verifyNativePanelAssignment(directory: File, selected: Boolean) { + val dexFiles = directory.listFiles()!!.filter { it.extension == "dex" }.map { file -> + file.inputStream().buffered().use { DexBackedDexFile.fromInputStream(Opcodes.getDefault(), it) } + } + val playbackInterface = EXTENSION_CLASS.removeSuffix(";") + "\$NativePlayback;" + val owners = dexFiles.flatMap { it.classes }.filter { playbackInterface in it.interfaces } + check(owners.size == if (selected) 1 else 0) { "Unexpected native playback bridge inclusion" } + val panelInterface = EXTENSION_CLASS.removeSuffix(";") + "\$QueuePanel;" + val panels = dexFiles.flatMap { it.classes }.filter { panelInterface in it.interfaces } + check(panels.size == if (selected) 1 else 0) { "Queue header ownership must be included only with this patch" } + val shufflePresses = dexFiles.flatMap { it.classes }.filter { !it.type.startsWith("Lapp/morphe/") } + .flatMap { it.methods }.filter { method -> method.implementation?.instructions?.any { + (it as? ReferenceInstruction)?.reference?.toString()?.startsWith( + "$EXTENSION_CLASS->onQueueShuffle(") == true + } == true } + check(shufflePresses.size == if (selected) 1 else 0) { + "Queue shuffle command must intercept one native listener only when selected" + } + val transitionHook = "$EXTENSION_CLASS->onPlaybackTransition(${EXTENSION_CLASS.removeSuffix(";")}\$PlaylistController;)V" + val transitions = dexFiles.flatMap { it.classes }.filter { !it.type.startsWith("Lapp/morphe/") } + .flatMap { it.methods }.filter { method -> method.implementation?.instructions?.any { + (it as? ReferenceInstruction)?.reference?.toString() == transitionHook + } == true } + check(transitions.size == if (selected) 1 else 0) { "Unexpected native playback transition observer inclusion" } + if (!selected) return + val transition = transitions.single() + check(transition.returnType == "V" && transition.parameterTypes == List(3) { "Ljava/lang/String;" }) { + "Reload readiness must observe the playback descriptor transition, not a playlist model bind" + } + val transitionCode = transition.implementation!!.instructions.toList() + check(transitionCode.indices.filter { transitionCode[it].opcode == Opcode.RETURN_VOID }.all { index -> + index > 0 && (transitionCode[index - 1] as? ReferenceInstruction)?.reference?.toString() == transitionHook + }) { "Every completed native transition, including standalone playback, must notify queue ownership" } + println("Native playback transitions notify queue ownership even without a playlist model") + val shufflePress = shufflePresses.single().implementation!!.instructions.toList() + val shuffleIndex = shufflePress.indexOfFirst { + (it as? ReferenceInstruction)?.reference?.toString()?.startsWith( + "$EXTENSION_CLASS->onQueueShuffle(") == true + } + check(shufflePress[shuffleIndex + 1].opcode == Opcode.MOVE_RESULT && + shufflePress[shuffleIndex + 2].opcode == Opcode.IF_EQZ && + shufflePress[shuffleIndex + 3].opcode == Opcode.RETURN_VOID && + shufflePress.drop(shuffleIndex + 4).any { + (it as? ReferenceInstruction)?.reference?.toString() == + "Landroid/widget/ImageView;->isSelected()Z" + }) { "A consumed queue shuffle must return before the original fallback toggle" } + val panelFlag = panels.single().methods.single { it.name == "patch_isQueueOwned" } + .implementation!!.instructions.single { it.opcode == Opcode.IGET_BOOLEAN } + .let { (it as ReferenceInstruction).reference as FieldReference } + check(dexFiles.flatMap { it.classes }.flatMap { it.methods }.single { it.name == "patch_markQueueRows" && it.implementation != null } + .implementation!!.instructions.any { + it.opcode == Opcode.IPUT_BOOLEAN && (it as ReferenceInstruction).reference == panelFlag + }) { "Publishing local rows must also mark the exact native header model" } + verifyNativeShuffle(dexFiles.flatMap { it.classes }) + val clear = owners.single().methods.single { it.name == "patch_clearQueue" } + .implementation!!.instructions.toList() + val wrapper = owners.single() + val autoplayType = wrapper.fields.single { it.name == "patch_lastAutoplay" }.type + val clearCalls = clear.mapIndexedNotNull { index, instruction -> + ((instruction as? ReferenceInstruction)?.reference as? MethodReference)?.let { index to it } + } + val initialize = clearCalls.first { (_, reference) -> + reference.definingClass == wrapper.type && reference.parameterTypes == listOf(autoplayType) + }.first + val releaseFlag = clear.indexOfFirst { instruction -> + instruction.opcode == Opcode.IPUT_BOOLEAN && + ((instruction as ReferenceInstruction).reference as FieldReference).name == "patch_localQueue" + } + val responseType = wrapper.methods.single { it.name == "patch_localPanel" }.returnType + val hydrate = clearCalls.single { (_, reference) -> reference.parameterTypes == listOf(responseType) }.first + val dispose = clearCalls.single { (_, reference) -> + reference.name == "patch_disposeWithoutPlayback" + }.first + check(releaseFlag >= 0 && releaseFlag < initialize && initialize < hydrate && hydrate < dispose) { + "Clear queue must release local interception, hydrate ordinary navigation, then dispose the old queue" + } + val guardedDispose = dexFiles.flatMap { it.classes }.flatMap { it.methods } + .single { it.name == "patch_disposeWithoutPlayback" && it.implementation != null } + val guardedCalls = guardedDispose.implementation!!.instructions.mapNotNull { + ((it as? ReferenceInstruction)?.reference as? MethodReference) + }.toList() + val beginChange = dexFiles.flatMap { it.classes }.flatMap { it.methods } + .single { it.toString() == guardedCalls.first().toString() } + val acquireChange = beginChange.implementation!!.instructions.mapNotNull { + ((it as? ReferenceInstruction)?.reference as? MethodReference) + }.single { it.parameterTypes.isEmpty() && it.returnType == "Ljava/lang/Object;" } + check(guardedCalls.size == 4 && guardedCalls[2] == guardedCalls[3] && + guardedCalls[2].definingClass == acquireChange.definingClass && + guardedCalls[2].parameterTypes == listOf("Ljava/lang/Object;") && + guardedDispose.implementation!!.tryBlocks.single().exceptionHandlers.single().exceptionType == null) { + "Native disposal must acquire the existing queue guard and release it on success and failure" + } + println("Native clear queue restores ordinary navigation and guards selected-item callbacks during disposal") + val providers: List = dexFiles.map(::DexClassProvider) + val classPath = ClassPath(providers) + val bridge = owners.single().methods.single { it.name == "patch_localPanel" } + val analyzer = MethodAnalyzer(classPath, bridge, null, false) + analyzer.analysisException?.let { throw it } + for (instruction in analyzer.analyzedInstructions) { + val store = instruction.instruction + if (store.opcode != Opcode.IPUT_OBJECT) continue + val field = (store as ReferenceInstruction).reference as FieldReference + val value = instruction.getPreInstructionRegisterType((store as TwoRegisterInstruction).registerA) + val valueType = value.type + check(value.category == RegisterType.NULL || value.category == RegisterType.REFERENCE && + valueType != null && classPath.getClass(field.type).getCommonSuperclass(valueType).type == field.type) { + "Native playback bridge stores $value in incompatible field $field" + } + } + println("Native playback panel field assignments: compatible register types") +} + +/** A local shuffle command changes the rows; native playback must then follow their visible order. */ +private fun verifyNativeShuffle(classes: List) { + val mutableQueue = classes.single { type -> type.methods.any { it.name == "patch_clearShuffleOrder" } } + val clear = mutableQueue.methods.single { it.name == "patch_clearShuffleOrder" } + .implementation!!.instructions.toList() + val disableIndex = clear.indexOfFirst { it.opcode == Opcode.INVOKE_VIRTUAL } + val disable = (clear[disableIndex] as ReferenceInstruction).reference as MethodReference + val nativeSetter = mutableQueue.methods.single { it.toString() == disable.toString() } + check(disable.parameterTypes == listOf("Z") && disable.returnType == "V" && + (clear[disableIndex - 1] as NarrowLiteralInstruction).narrowLiteral == 0) { + "Shuffle cache reset must disable the original mutable shuffler first" + } + fun linkFields(methodInstructions: Iterable) = + methodInstructions.filter { it.opcode == Opcode.IPUT_OBJECT }.mapNotNull { + ((it as ReferenceInstruction).reference as FieldReference).takeIf { field -> field.type == "[I" } + }.toSet() + val resetLinks = linkFields(clear) + check(resetLinks.size == 2 && resetLinks == linkFields(nativeSetter.implementation!!.instructions) && + clear.indexOfFirst { it.opcode == Opcode.IPUT_OBJECT } > disableIndex) { + "Shuffle reset must invalidate both original index maps after disabling their use" + } + val playback = classes.single { type -> type.methods.any { + it.name == "patch_attachQueue" && it.implementation != null + } } + val attach = playback.methods.single { it.name == "patch_attachQueue" }.implementation!!.instructions.toList() + val responseType = playback.methods.single { it.name == "patch_localPanel" }.returnType + fun reference(index: Int) = (attach[index] as? ReferenceInstruction)?.reference as? MethodReference + val clearIndex = attach.indices.single { reference(it)?.name == "patch_clearShuffleOrder" } + val acceptIndex = attach.indices.single { index -> reference(index)?.let { + it.returnType == "V" && it.parameterTypes == listOf(responseType) + } == true } + check(clearIndex < acceptIndex && attach.indices.none { index -> reference(index)?.let { + it.name == disable.name && it.parameterTypes == listOf("Z") && it.returnType == "V" + } == true }) { + "Local rows must disable hidden shuffle before replacement and must not restore it afterward" + } + check(playback.methods.single { it.name == "patch_updateSnapshot" }.implementation!!.instructions.none { + (it as? ReferenceInstruction)?.reference?.toString()?.contains("patch_clearShuffleOrder") == true + }) { "Metadata-only publication must not reset shuffle order" } + println("Native shuffle command: original index maps disabled, visible row order controls playback") +} + +/** Undo consumes only local capabilities and preserves the modern snackbar click continuation. */ +private fun verifyNativeQueueUndo(directory: File, selected: Boolean) { + val extensionOwner = EXTENSION_CLASS.removePrefix("L").removeSuffix(";") + val classes = directory.listFiles()!!.filter { it.extension == "dex" }.flatMap { file -> + file.inputStream().buffered().use { + DexBackedDexFile.fromInputStream(Opcodes.getDefault(), it).classes.toList() + } + } + val commandInterface = "Lapp/morphe/extension/youtube/patches/queue/QueueUndoActions\$Command;" + val commandTypes = classes.filter { commandInterface in it.interfaces } + val nativeMethods = classes.filter { !it.type.startsWith("Lapp/morphe/") }.flatMap { it.methods } + val closeHook = "L$extensionOwner;->onQueuePlayerClosing()V" + val closeHandlers = nativeMethods.filter { method -> method.implementation?.instructions?.any { + (it as? ReferenceInstruction)?.reference?.toString() == closeHook + } == true } + val closeFeedback = nativeMethods.filter { it.name == "patch_showQueueFeedback" } + check(closeHandlers.size == if (selected) 3 else 0) { "Unexpected queue player-close interception" } + check(closeFeedback.size == if (selected) 1 else 0) { "Unexpected queue player-close feedback bridge" } + if (selected) { + val click = closeHandlers.single { it.name == "onClick" && + it.parameterTypes == listOf("Landroid/view/View;") } + val code = click.implementation!!.instructions.toList() + val hookIndex = code.indexOfFirst { (it as? ReferenceInstruction)?.reference?.toString() == closeHook } + val original = (code[hookIndex + 1] as ReferenceInstruction).reference as MethodReference + check(code[hookIndex].opcode == Opcode.INVOKE_STATIC && + (code[hookIndex] as FiveRegisterInstruction).registerCount == 0 && + code[hookIndex + 1].opcode == Opcode.INVOKE_VIRTUAL && + original.parameterTypes.isEmpty() && original.returnType == "V" && + code[hookIndex + 2].opcode == Opcode.RETURN_VOID) { + "Player-close capture must preserve the original dismissal and listener return" + } + val fling = closeHandlers.single { method -> + method.name == "onFling" && method.returnType == "Z" && method.parameterTypes == + listOf("Landroid/view/MotionEvent;", "Landroid/view/MotionEvent;", "F", "F") || + method.returnType == "V" && method.parameterTypes == listOf("Landroid/view/MotionEvent;", "F", "F") + } + if (fling.name != "onFling") { + check(nativeMethods.count { method -> method.name == "onFling" && + method.implementation?.instructions?.any { + (it as? ReferenceInstruction)?.reference?.toString() == fling.toString() + } == true } == 1) { "Legacy accepted-dismiss helper must belong to the native fling callback" } + } + val touch = closeHandlers.single { it.returnType == "Z" && + it.parameterTypes == listOf("Landroid/view/View;", "Landroid/view/MotionEvent;") } + for ((gesture, expectedCalls) in listOf(fling to 2, touch to 1)) { + val instructions = gesture.implementation!!.instructions.toList() + val dismissCalls = instructions.indices.filter { + (instructions[it] as? ReferenceInstruction)?.reference?.toString() == original.toString() + } + val captures = instructions.indices.filter { + (instructions[it] as? ReferenceInstruction)?.reference?.toString() == closeHook + } + check(dismissCalls.size == expectedCalls && captures == dismissCalls.map { it - 1 } && + captures.all { instructions[it].opcode == Opcode.INVOKE_STATIC && + (instructions[it] as FiveRegisterInstruction).registerCount == 0 } && + dismissCalls.all { instructions[it].opcode == Opcode.INVOKE_VIRTUAL && + instructions[it + 1].opcode in if (gesture.returnType == "V") setOf(Opcode.RETURN_VOID) + else setOf(Opcode.GOTO, Opcode.GOTO_16, Opcode.GOTO_32) }) { + "Accepted gesture dismissals must each capture Undo and keep their original continuation" + } + } + val feedback = closeFeedback.single().implementation!!.instructions.toList() + val calls = feedback.mapNotNull { (it as? ReferenceInstruction)?.reference as? MethodReference } + val commandType = commandTypes.single().type + val registryType = "Lcom/google/protobuf/ExtensionRegistryLite;" + check(feedback.any { it.opcode == Opcode.SGET_OBJECT && + ((it as? ReferenceInstruction)?.reference as? FieldReference)?.type == commandType + } && calls.any { it.definingClass == registryType && it.name == "getGeneratedRegistry" } && + calls.any { it.name == "parseFrom" && it.parameterTypes.size == 3 && + it.parameterTypes.drop(1) == listOf("[B", registryType) && + it.returnType == it.parameterTypes.first() + } && feedback.any { it.opcode == Opcode.CHECK_CAST && + ((it as? ReferenceInstruction)?.reference as? TypeReference)?.type == commandType + } && calls.any { + it.definingClass == "Ljava/util/Collections;" && it.name == "emptyMap" + } && calls.last().parameterTypes == listOf(commandType, "Ljava/util/Map;")) { + "Player-close feedback must use the native command parser and activity dispatcher" + } + println("Queue player-close: click, fling and drag keep native dismissal; feedback uses native parser and dispatcher") + } + val snackbars = classes.filter { !it.type.startsWith("Lapp/morphe/") }.flatMap { it.methods }.filter { method -> + method.implementation?.instructions?.any { + (it as? ReferenceInstruction)?.reference?.toString()?.startsWith( + "L$extensionOwner;->onQueueUndoCommand(") == true + } == true + } + val durationHook = "Lapp/morphe/extension/youtube/patches/queue/QueueSaveFeedback;->confirmationDuration(Lcom/google/protobuf/MessageLite;)I" + val durationFactories = classes.filter { !it.type.startsWith("Lapp/morphe/") }.flatMap { it.methods } + .filter { method -> method.implementation?.instructions?.any { + (it as? ReferenceInstruction)?.reference?.toString() == durationHook + } == true } + check(durationFactories.size == if (selected) 1 else 0) { "Unexpected queue save duration hook inclusion" } + if (selected) { + val factory = durationFactories.single() + val code = factory.implementation!!.instructions.toList() + val hook = code.indexOfFirst { (it as? ReferenceInstruction)?.reference?.toString() == durationHook } + val guard = (hook + 1 until code.size).first { code[it].opcode == Opcode.IF_LEZ } + val setter = (guard + 1 until code.size).first { code[it].opcode == Opcode.INVOKE_VIRTUAL } + val reference = (code[setter] as ReferenceInstruction).reference as MethodReference + val guardAddress = code.take(guard).sumOf { it.codeUnits } + val skipTarget = guardAddress + (code[guard] as OffsetInstruction).codeOffset + check(reference.definingClass == factory.returnType && reference.parameterTypes == listOf("I") && + reference.returnType == "V" && skipTarget > code.take(setter).sumOf { it.codeUnits }) { + "Ordinary feedback must bypass the native duration setter" + } + val returned = (code.last() as OneRegisterInstruction).registerA + check(code.last().opcode == Opcode.RETURN_OBJECT && + (code[setter] as FiveRegisterInstruction).registerC == returned) { + "Queue save duration must update and return the original native snackbar builder" + } + println("Queue save duration: owned confirmation guard and original native builder setter verified") + } + if (!selected) { + check(commandTypes.isEmpty() && snackbars.isEmpty()) { "Queue Undo hooks leaked into an unselected build" } + return + } + val command = commandTypes.single() + val parser = command.methods.single { it.name == "patch_parseQueueCommand" } + check(parser.implementation!!.instructions.any { + (it as? ReferenceInstruction)?.reference?.toString() == + "Lcom/google/protobuf/ExtensionRegistryLite;->getGeneratedRegistry()Lcom/google/protobuf/ExtensionRegistryLite;" + }) { "Queue feedback parser lost the native extension registry" } + val instructions = snackbars.single().implementation!!.instructions.toList() + check(instructions.none { + (it as? ReferenceInstruction)?.reference?.toString()?.contains("quantum/snackbar/Snackbar;") == true + }) { "Queue Undo still targets the unrelated legacy share-panel snackbar" } + val hooks = instructions.indices.filter { index -> + (instructions[index] as? ReferenceInstruction)?.reference?.toString()?.startsWith( + "L$extensionOwner;->onQueueUndoCommand(") == true + } + check(hooks.size == 3) { "Modern snackbar must handle all three endpoint slots" } + for (hook in hooks) { + check(instructions[hook + 1].opcode == Opcode.MOVE_RESULT && + instructions[hook + 2].opcode == Opcode.IF_NEZ && + instructions[hook + 3].opcode == Opcode.INVOKE_INTERFACE) + val original = (instructions[hook + 3] as ReferenceInstruction).reference as MethodReference + check(original.parameterTypes == listOf(command.type, "Ljava/util/Map;")) + val nativeCall = instructions[hook + 3] as FiveRegisterInstruction + val undoCall = instructions[hook] as FiveRegisterInstruction + val result = (instructions[hook + 1] as OneRegisterInstruction).registerA + check(undoCall.registerC == nativeCall.registerD && + result !in setOf(nativeCall.registerC, nativeCall.registerD, nativeCall.registerE)) { + "Modern snackbar Undo must preserve every original callback input" + } + fun address(index: Int) = instructions.take(index).sumOf { it.codeUnits } + check(address(hook + 2) + (instructions[hook + 2] as OffsetInstruction).codeOffset == address(hook + 4)) { + "Consumed Undo must skip only its native dispatch and retain remaining click behavior" + } + } + println("Queue Undo: modern snackbar's three slots, native registry, original callbacks and click continuation verified") +} diff --git a/patches/src/test/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/QueueFullscreenBoundaryTest.kt b/patches/src/test/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/QueueFullscreenBoundaryTest.kt new file mode 100644 index 0000000000..557323d5a0 --- /dev/null +++ b/patches/src/test/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/QueueFullscreenBoundaryTest.kt @@ -0,0 +1,124 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to Morphe contributions. + */ + +package app.morphe.patches.youtube.interaction.nativeplaylistqueue + +import java.io.File +import java.net.URLClassLoader +import javax.tools.ToolProvider +import kotlin.io.path.createTempDirectory +import kotlin.test.Test +import kotlin.test.assertEquals + +class QueueFullscreenBoundaryTest { + @Test + fun `minimized start expires safely and preserves ordinary fullscreen settings`() { + val root = generateSequence(File("").absoluteFile) { it.parentFile } + .first { File(it, "extensions/youtube/src/main/proto").isDirectory } + val output = createTempDirectory("playlist-queue-start").toFile() + val stubs = mapOf( + "Nullable" to "package androidx.annotation; public @interface Nullable {}", + "Logger" to """ + package app.morphe.extension.shared; + public class Logger { + public static void printDebug(java.util.function.Supplier message) {} + public static void printException(java.util.function.Supplier message, Exception error) {} + public static void printException(java.util.function.Supplier message) {} + } + """, + "Utils" to """ + package app.morphe.extension.shared; + public class Utils { + public static Runnable expiration; + public static void verifyOnMainThread() {} + public static void runOnMainThreadDelayed(Runnable action, long delay) { expiration = action; } + } + """, + "VideoInformation" to """ + package app.morphe.extension.youtube.patches; + public class VideoInformation { + public interface PlaybackController {} + public static String videoId = ""; + public static String getVideoId() { return videoId; } + } + """, + "PlayerType" to """ + package app.morphe.extension.youtube.shared; + public enum PlayerType { + WATCH_WHILE_FULLSCREEN, WATCH_WHILE_MINIMIZED; + public static PlayerType getCurrent() { return WATCH_WHILE_MINIMIZED; } + } + """, + "Settings" to """ + package app.morphe.extension.youtube.settings; + import app.morphe.extension.youtube.patches.OpenVideosFullscreenHookPatch.OpenFullscreenMode; + public class Settings { + public static java.util.function.Supplier OPEN_VIDEOS_FULLSCREEN = + () -> OpenFullscreenMode.LANDSCAPE; + } + """ + ).map { (name, source) -> File(output, "$name.java").apply { writeText(source.trimIndent()) } } + val fixture = File(output, "StartFixture.java").apply { + writeText(""" + package app.morphe.extension.youtube.patches.queue; + import java.util.*; + import app.morphe.extension.youtube.patches.OpenVideosFullscreenHookPatch; + import app.morphe.extension.youtube.patches.VideoInformation; + public class StartFixture { + static void require(boolean value) { if (!value) throw new AssertionError(); } + enum Status { VIDEO_PLAYING } + public static void verifyFullscreenCoexistence() { + int[] fullscreenCalls = {0}; + OpenVideosFullscreenHookPatch.setFullscreenInterface( + new OpenVideosFullscreenHookPatch.FullscreenInterface() { + public void patch_exitFullscreen() {} + public void patch_enterFullscreen() { fullscreenCalls[0]++; } + }); + VideoInformation.videoId = "abcdefghijk"; + OpenVideosFullscreenHookPatch.keepVideoMinimized("abcdefghijk"); + require(OpenVideosFullscreenHookPatch.doNotOpenVideoFullscreenPortrait(false)); + OpenVideosFullscreenHookPatch.initialize(null); + OpenVideosFullscreenHookPatch.playerStatusChanged(Status.VIDEO_PLAYING); + require(fullscreenCalls[0] == 0); + // The next ordinary video retains the user's automatic fullscreen mode. + VideoInformation.videoId = "ZYXWVUTSRQP"; + OpenVideosFullscreenHookPatch.initialize(null); + OpenVideosFullscreenHookPatch.playerStatusChanged(Status.VIDEO_PLAYING); + require(fullscreenCalls[0] == 1); + OpenVideosFullscreenHookPatch.keepVideoMinimized("ZYXWVUTSRQP"); + OpenVideosFullscreenHookPatch.keepVideoMinimized(null); // Cancelled queue start. + OpenVideosFullscreenHookPatch.initialize(null); + OpenVideosFullscreenHookPatch.playerStatusChanged(Status.VIDEO_PLAYING); + require(fullscreenCalls[0] == 2); + OpenVideosFullscreenHookPatch.keepVideoMinimized("abcdefghijk"); + var oldExpiration = app.morphe.extension.shared.Utils.expiration; + OpenVideosFullscreenHookPatch.keepVideoMinimized("ZYXWVUTSRQP"); + oldExpiration.run(); + require(OpenVideosFullscreenHookPatch.doNotOpenVideoFullscreenPortrait(false)); + app.morphe.extension.shared.Utils.expiration.run(); + require(!OpenVideosFullscreenHookPatch.doNotOpenVideoFullscreenPortrait(false)); + } + + } + """.trimIndent()) + } + try { + val sources = File(root, + "extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/queue") + assertEquals(0, ToolProvider.getSystemJavaCompiler().run(null, null, null, + "--release", "17", "-d", output.path, + File(sources.parentFile, "OpenVideosFullscreenHookPatch.java").path, + fixture.path, *stubs.map { it.path }.toTypedArray())) + URLClassLoader(arrayOf(output.toURI().toURL()), null).use { loader -> + loader.loadClass("app.morphe.extension.youtube.patches.queue.StartFixture") + .getMethod("verifyFullscreenCoexistence").invoke(null) + } + } finally { + output.deleteRecursively() + } + } +} diff --git a/patches/src/test/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/QueueRuntimeBoundaryTest.kt b/patches/src/test/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/QueueRuntimeBoundaryTest.kt new file mode 100644 index 0000000000..c7fd15c623 --- /dev/null +++ b/patches/src/test/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/QueueRuntimeBoundaryTest.kt @@ -0,0 +1,1578 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to Morphe contributions. + */ + +package app.morphe.patches.youtube.interaction.nativeplaylistqueue + +import com.google.protobuf.GeneratedMessageLite +import java.io.File +import java.net.URLClassLoader +import javax.tools.ToolProvider +import kotlin.io.path.createTempDirectory +import kotlin.test.Test +import kotlin.test.assertEquals + +/** Tests compiled queue ownership and feedback timing across external Android boundaries. */ +class QueueRuntimeBoundaryTest { + @Test + fun `native queue is active only for the native queue type`() = withFixture("queueTypeSelection") + + @Test + fun `handoff waits for setting cache and rejects late responses without restoring old session`() = + withFixture("acceptedHandoff") + + @Test + fun `failed detach retains ownership for retry but cancels pending work`() = withFixture("failedDetach") + + @Test + fun `handoff never clears a foreign backend`() = withFixture("foreignBackend") + + @Test + fun `other queue types cancel a pending first video start`() = withFixture("pendingStart") + + @Test + fun `local Undo follows normal confirmation timing and replacement`() = withFixture("normalUndoTiming") + + @Test + fun `local Undo follows accessible confirmation timing`() = withFixture("accessibleUndoTiming") + + @Test + fun `closing an owned player restores the queue on a new backend at its saved position`() = + withFixture("closedPlayerRestore") + + @Test + fun `closed restore releases its inline block on cancellation and timeout without releasing newer opens`() = + withFixture("closedRestoreInlineLifetime") + + @Test + fun `new playback and provider handoff invalidate closed player Undo`() = withFixture("closedPlayerSuperseded") + + @Test + fun `foreign playback and unavailable position never create close Undo`() = withFixture("closedPlayerGuards") + + @Test + fun `single row removal is silent and still protects the playing row`() = withFixture("singleRemovalSilent") + + @Test + fun `menu Clear Undo reattaches without reopening or seeking playback`() = withFixture("menuClearUndo") + + @Test + fun `Reload offers Clear Undo on the reloaded player without reopening it again`() = withFixture("reloadQueueUndo") + + @Test + fun `Reload feedback expires and cannot replace newer playback or queue ownership`() = withFixture("reloadQueueLifetime") + + @Test + fun `Reload leaves other providers and foreign or unavailable playback unchanged`() = withFixture("reloadQueueGuards") + + @Test + fun `individual music taps remove only automatic Mix navigation from an owned queue`() = withFixture("musicVideoTap") + + @Test + fun `music tap normalization preserves deliberate playlists and unavailable or foreign queue playback`() = + withFixture("musicVideoTapGuards") + + @Test + fun `stock queue actions route to Native without opening the account queue dialog`() = withFixture("stockQueueRouting") + + @Test + fun `Morphe and Local stock actions keep their existing destinations`() = withFixture("otherQueueRouting") + + @Test + fun `Native Last requires a queue and stale callbacks cannot create one`() = withFixture("staleLastAction") + + @Test + fun `stock Next replaces playlist navigation without reopening current playback`() = + withFixture("playlistNextHandoff") + + @Test + fun `stock Next replaces Mix navigation and preserves minimized presentation`() = withFixture("playlistMixHandoff") + + @Test + fun `creating a queue from current video inserts it once but later additions allow duplicates`() = + withFixture("firstQueueCurrentVideo") + + @Test + fun `failed in-place attachment preserves the old playlist and playback`() = withFixture("playlistHandoffFailed") + + @Test + fun `converted playlist rejects late source responses until deliberate new playlist navigation`() = + withFixture("convertedPlaylistLifetime") + + @Test + fun `converted playlist supports Clear Undo without restoring old playlist navigation`() = + withFixture("convertedPlaylistUndo") + + @Test + fun `queue header width follows committed ownership and restores on ordinary playlist reuse`() = + withFixture("queueTitleOwnership") + + @Test + fun `ordinary playlist row menus supply their own selected ID in Native mode`() = withFixture("playlistRowContext") + + @Test + fun `Mix choice preserves Watch and appends fetched entries without reopening playback`() = withFixture("mixChoice") + + @Test + fun `Mix choice ignores music and foreign queues and rejects stale choices and completions`() = withFixture("mixChoiceGuards") + + // NativePlaylistQueuePatch depends on Android settings, scheduling and UI classes. + // Isolate those boundaries while executing its actual compiled coordinator. + private fun withFixture(scenario: String) { + val root = generateSequence(File("").absoluteFile) { it.parentFile } + .first { File(it, "extensions/youtube/src/main/proto").isDirectory } + val classes = File(root, + "extensions/youtube/build/intermediates/javac/release/compileReleaseJavaWithJavac/classes") + val protobuf = File(GeneratedMessageLite::class.java.protectionDomain.codeSource.location.toURI()) + val output = createTempDirectory("playlist-queue-runtime").toFile() + // Only external settings/UI/scheduling boundaries are replaced. Queue state, + // preference callback, cancellation, release, feedback and timeout guards are real. + val stubs = mapOf( + "SharedPreferences" to """ + package android.content; + public interface SharedPreferences { + interface OnSharedPreferenceChangeListener { + void onSharedPreferenceChanged(SharedPreferences prefs, String key); + } + } + """, + "Bundle" to "package android.os; public class Bundle {}", + "AccessibilityManager" to """ + package android.view.accessibility; + public class AccessibilityManager { + public boolean enabled, touchExploration; + public boolean isEnabled() { return enabled; } + public boolean isTouchExplorationEnabled() { return touchExploration; } + } + """, + "Context" to """ + package android.content; + public class Context { + public static final String ACCESSIBILITY_SERVICE = "accessibility"; + public final android.view.accessibility.AccessibilityManager accessibility = + new android.view.accessibility.AccessibilityManager(); + public Object getSystemService(String service) { return accessibility; } + public android.graphics.drawable.Drawable getDrawable(int id) { return null; } + } + """, + "Activity" to """ + package android.app; + public class Activity extends android.content.Context { + public boolean isFinishing() { return false; } + public boolean isDestroyed() { return false; } + } + """, + "Drawable" to "package android.graphics.drawable; public class Drawable {}", + "View" to """ + package android.view; + public class View { + public static final int GONE = 8; + public interface OnClickListener { void onClick(View view); } + public int getVisibility() { return 0; } + public void setVisibility(int value) {} + public View findViewById(int id) { return null; } + public void setOnClickListener(OnClickListener listener) {} + } + """, + "ViewGroup" to """ + package android.view; + public class ViewGroup extends View { + public static class LayoutParams { + public static final int WRAP_CONTENT = -2; + public int width; + public LayoutParams(int width) { this.width = width; } + } + public int getChildCount() { return 0; } + public View getChildAt(int index) { throw new AssertionError("Unexpected UI binding"); } + } + """, + "TextPaint" to """ + package android.text; + public class TextPaint { public float desiredWidth = 83.25f; } + """, + "Layout" to """ + package android.text; + public class Layout { + public static float getDesiredWidth(CharSequence text, TextPaint paint) { return paint.desiredWidth; } + } + """, + "TextView" to """ + package android.widget; + public class TextView extends android.view.View { + public final android.view.ViewGroup.LayoutParams params = new android.view.ViewGroup.LayoutParams(android.view.ViewGroup.LayoutParams.WRAP_CONTENT); + public int layoutRequests; + public final android.text.TextPaint paint = new android.text.TextPaint(); + public CharSequence getText() { return "Queue"; } + public android.text.TextPaint getPaint() { return paint; } + public int getCompoundPaddingLeft() { return 2; } + public int getCompoundPaddingRight() { return 3; } + public android.view.ViewGroup.LayoutParams getLayoutParams() { return params; } + public void setLayoutParams(android.view.ViewGroup.LayoutParams value) { + if (value != params) throw new AssertionError("Keep the native layout parameters"); + layoutRequests++; + } + } + """, + "Pair" to """ + package android.util; + public class Pair { + public final F first; public final S second; + public Pair(F first, S second) { this.first = first; this.second = second; } + } + """, + "ResourceType" to "package app.morphe.extension.shared; public enum ResourceType { ID, DRAWABLE }", + "ResourceUtils" to """ + package app.morphe.extension.shared; + public class ResourceUtils { + public static int getIdentifier(ResourceType type, String name) { return 0; } + } + """, + "FeedVideoCardsPatch" to """ + package app.morphe.extension.youtube.patches.utils; + public class FeedVideoCardsPatch { + // External mounted UI boundary, observed separately on the phone. + public static boolean individual = true; + public static boolean isIndividualVideoTap(android.view.View view) { return individual; } + public static boolean isPlaylistTap(android.view.View view) { return !individual; } + } + """, + "AuthUtils" to """ + package app.morphe.extension.shared.innertube.utils; + public class AuthUtils { + public static String account = "test"; + public static java.util.Map getRequestHeader() { return java.util.Map.of("account", account); } + } + """, + "MixPlaylistRequest" to """ + package app.morphe.extension.youtube.patches.queue; + public class MixPlaylistRequest { + public static java.util.List result; + static java.util.List fetch(String playlist, String video, java.util.Map headers) { + if (app.morphe.extension.shared.Utils.mainThread) throw new AssertionError("Network on UI thread"); + return result; + } + } + """, + "FlyoutUtils" to """ + package app.morphe.extension.youtube.patches.utils; + import android.util.Pair; + import java.util.*; + public class FlyoutUtils { + public static String video = "ZYXWVUTSRQP", button = "QUEUE_PLAY_NEXT"; + public static final List> stock = new ArrayList<>(); + public static List> getVisibleFlyoutButtons() { return stock; } + public interface FlyoutMenuVideoIdInterface { String patch_getVideoId(); } + public static int contexts; + public static void extractFlyoutIdFromMap(Map context) { + contexts++; + video = ((FlyoutMenuVideoIdInterface) context.get( + "com.google.android.libraries.youtube.innertube.endpoint.tag")).patch_getVideoId(); + } + public static String getCurrentButtonName() { return button; } + public static String getFlyoutVideoId() { return video; } + public static String getFlyoutPlaylistId() { return ""; } + public static String getFlyoutChannelName() { return "Channel"; } + public static void resetCurrentButtonIndex() {} + public static void dismissFlyout() {} + public static int addFlyoutButton(Object panel, android.graphics.drawable.Drawable icon, + String text, android.view.View.OnClickListener action, int index) { + return index + 1; + } + } + """, + "PlaylistPatch" to """ + package app.morphe.extension.youtube.patches.utils; + public class PlaylistPatch { + public enum QueueManager { OPEN_QUEUE; public final int drawableId = 0; } + public static int dialogs; + public static void prepareDialogBuilder(android.content.Context context, String video) { dialogs++; } + } + """, + "LocalQueueSheet" to """ + package app.morphe.extension.youtube.patches.utils; + public class LocalQueueSheet { public static void show(android.app.Activity activity) {} } + """, + "ChannelIdRequest" to """ + package app.morphe.extension.youtube.patches.utils.requests; + public class ChannelIdRequest { public static String getVideoTitleIfAvailable(String video) { return video; } } + """, + "PlayerType" to """ + package app.morphe.extension.youtube.shared; + public enum PlayerType { + NONE, HIDDEN, INLINE_MINIMAL, WATCH_WHILE_MINIMIZED, WATCH_WHILE_MAXIMIZED, + WATCH_WHILE_SLIDING_MINIMIZED_DISMISSED, WATCH_WHILE_SLIDING_FULLSCREEN_DISMISSED; + public static PlayerType current = NONE; + public static PlayerType getCurrent() { return current; } + public boolean isNoneOrHidden() { return this == NONE || this == HIDDEN; } + } + """, + "ShortsPlayerState" to """ + package app.morphe.extension.youtube.shared; + public class ShortsPlayerState { + public static boolean open; + public static boolean isOpen() { return open; } + } + """, + "VideoInformation" to """ + package app.morphe.extension.youtube.patches; + public class VideoInformation { + public static String id = "abcdefghijk", playlist = ""; + public static long time = 75432; + public static String getVideoId() { return id; } + public static String getPlaylistId() { return playlist; } + public static String getVideoTitle() { return "Current video"; } + public static String getChannelName() { return "Channel"; } + public static long getVideoTime() { return time; } + } + """, + "LoadVideoPatch" to """ + package app.morphe.extension.youtube.patches; + public class LoadVideoPatch { + public static String url; + public interface PlayerInterface { void patch_dismissPlayer(); } + public static int opens, blocks, releases, activeBlock, dismissals; + public static PlayerInterface getPlayerInterface() { return () -> dismissals++; } + public static int blockInlinePlaybackUntilPlayerOpens() { + return activeBlock = ++blocks; + } + public static void releaseInlinePlaybackBlock(int token) { + if (token != 0 && token == activeBlock) { activeBlock = 0; releases++; } + } + public static void openVideoIntent(String value, boolean dismiss) { + if (dismiss) throw new AssertionError("Undo must not dismiss a replacement player"); + if (value.contains("&t=") && activeBlock == 0) + throw new AssertionError("Block inline playback before dispatching a closed queue restore"); + opens++; url = value; + } + } + """, + "StringRef" to """ + package app.morphe.extension.shared; + public class StringRef { public static String str(String key) { return key; } } + """, + "Base64" to """ + package android.util; + public class Base64 { + public static final int NO_WRAP = 2; + public static String encodeToString(byte[] bytes, int flags) { + return java.util.Base64.getEncoder().encodeToString(bytes); + } + } + """, + "Uri" to """ + package android.net; + public class Uri { + public static String encode(String value) { + return java.net.URLEncoder.encode(value, java.nio.charset.StandardCharsets.UTF_8); + } + } + """, + "RestoreOriginalTitlesPatch" to """ + package app.morphe.extension.youtube.patches.originaltitles; + public class RestoreOriginalTitlesPatch { + public static String getTitle(String id, String title) { return title; } + } + """, + "ChannelIdRequest" to """ + package app.morphe.extension.youtube.patches.utils.requests; + public class ChannelIdRequest { + public static String getVideoTitleIfAvailable(String id) { return ""; } + } + """, + "ByteTrieSearch" to """ + package app.morphe.extension.shared; + public class ByteTrieSearch { + public ByteTrieSearch(byte[]... values) {} + public static byte[][] convertStringsToBytes(String... values) { return new byte[0][]; } + } + """, + "Utils" to """ + package app.morphe.extension.shared; + public class Utils { + public static boolean mainThread = true; + public static boolean isCurrentlyOnMainThread() { return mainThread; } + private static final java.util.ArrayDeque pending = new java.util.ArrayDeque<>(); + public static java.util.Map createSizeRestrictedMap(int size) { + return new java.util.LinkedHashMap<>(); + } + private record Delayed(Runnable action, long when) {} + private static final java.util.List delayed = new java.util.ArrayList<>(); + private static long now; + private static final android.content.Context context = new android.content.Context(); + public static android.content.Context getContext() { return context; } + public static void runOnMainThreadDelayed(Runnable action, long delay) { + delayed.add(new Delayed(action, now + delay)); + } + public static void advanceBy(long milliseconds) { + now += milliseconds; + for (var item : new java.util.ArrayList<>(delayed)) { + if (item.when() <= now) { delayed.remove(item); item.action().run(); } + } + } + public static void runOnMainThread(Runnable action) { pending.add(action); } + public static void runOnMainThreadNowOrLater(Runnable action) { action.run(); } + private static final java.util.ArrayDeque background = new java.util.ArrayDeque<>(); + public static void runOnBackgroundThread(Runnable action) { background.add(action); } + public static void drainBackground() { + mainThread = false; + try { while (!background.isEmpty()) background.remove().run(); } + finally { mainThread = true; } + } + public static android.app.Activity getActivity() { return new android.app.Activity(); } + public static int toasts; + public static boolean failToast; + public static void showToastShort(String text) { + if (failToast) throw new IllegalStateException("Unavailable feedback surface"); + toasts++; + } + public static void drain() { while (!pending.isEmpty()) pending.remove().run(); } + } + """, + "Logger" to """ + package app.morphe.extension.shared; + public class Logger { + public interface LogMessage { String buildMessageString(); } + public static int errors; + public static void printDebug(LogMessage message) { message.buildMessageString(); } + public static void printException(LogMessage message) { message.buildMessageString(); errors++; } + public static void printException(LogMessage message, Throwable e) { printException(message); } + } + """, + "EnumSetting" to """ + package app.morphe.extension.shared.settings; + public class EnumSetting> { + public String key = "morphe_queue_type"; + public T value; + public EnumSetting(T value) { this.value = value; } + public T get() { return value; } + } + """, + "Settings" to """ + package app.morphe.extension.youtube.settings; + import app.morphe.extension.youtube.patches.AddToQueuePatch.QueueType; + import app.morphe.extension.youtube.innertube.queue.PlaylistQueueRenderer; + import app.morphe.extension.youtube.innertube.IconOuterClass; + public class Settings { + public static final app.morphe.extension.shared.settings.EnumSetting QUEUE_TYPE = + new app.morphe.extension.shared.settings.EnumSetting<>(QueueType.NATIVE); + } + """, + "LocalQueuePatch" to """ + package app.morphe.extension.youtube.patches; + public class LocalQueuePatch { + public static final java.util.List savedItems = java.util.List.of("saved-video"); + public static int additions; + public static boolean next; + public static boolean isEnabled() { return app.morphe.extension.youtube.settings.Settings.QUEUE_TYPE.get() == AddToQueuePatch.QueueType.LOCAL; } + public static void add(String video, boolean playNext) { additions++; next = playNext; } + public static int size() { return 0; } + } + """, + "OpenVideosFullscreenHookPatch" to """ + package app.morphe.extension.youtube.patches; + public class OpenVideosFullscreenHookPatch { + public static String minimized = "pending"; + public static void keepVideoMinimized(String id) { minimized = id; } + } + """, + "QueueFullscreenActions" to """ + package app.morphe.extension.youtube.patches.queue; + public class QueueFullscreenActions { + public static int refreshes; + static void refresh() { refreshes++; } + } + """ + ).map { (name, source) -> File(output, "$name.java").apply { writeText(source.trimIndent()) } } + val fixture = File(output, "RuntimeFixture.java").apply { + writeText(""" + package app.morphe.extension.youtube.patches.queue; + import android.content.SharedPreferences; + import app.morphe.extension.shared.*; + import app.morphe.extension.youtube.patches.*; + import app.morphe.extension.youtube.patches.AddToQueuePatch.QueueType; + import app.morphe.extension.youtube.innertube.queue.PlaylistQueueRenderer; + import app.morphe.extension.youtube.innertube.IconOuterClass; + import app.morphe.extension.youtube.settings.Settings; + import java.lang.ref.WeakReference; + import java.lang.reflect.*; + import java.util.*; + public class RuntimeFixture { + static Class owner = NativePlaylistQueuePatch.class; + static Field field(String name) throws Exception { + Field value = owner.getDeclaredField(name); value.setAccessible(true); return value; + } + static Object call(String name) throws Exception { + Method method = owner.getDeclaredMethod(name); method.setAccessible(true); return method.invoke(null); + } + static void require(boolean value, String message) { + if (!value) throw new AssertionError(message); + } + static int clears; + static NativePlaylistQueuePatch.NativePlayback backend(boolean local, boolean clearSucceeds) { + return (NativePlaylistQueuePatch.NativePlayback) Proxy.newProxyInstance(owner.getClassLoader(), + new Class[]{NativePlaylistQueuePatch.NativePlayback.class}, (proxy, method, args) -> { + switch (method.getName()) { + case "patch_isLocalQueue": return local; + case "patch_clearQueue": clears++; return clearSucceeds; + default: throw new AssertionError("Unexpected playback operation: " + method.getName()); + } + }); + } + static PlaylistQueue install(NativePlaylistQueuePatch.NativePlayback playback) throws Exception { + PlaylistQueue queue = new PlaylistQueue("abcdefghijk", "Playing").playNext("ZYXWVUTSRQP"); + field("queue").set(null, queue); + field("nativePlaybackRef").set(null, new WeakReference<>(playback)); + field("publishedModel").set(null, new Object()); + field("pendingQueueStart").set(null, new QueueStart(queue, "")); + Class undo = Class.forName(owner.getName() + "${'$'}PendingUndo"); + Constructor create = undo.getDeclaredConstructors()[0]; create.setAccessible(true); + field("pendingUndo").set(null, create.newInstance("token", queue, new WeakReference<>(playback), null)); + clears = 0; Logger.errors = 0; QueueFullscreenActions.refreshes = 0; + return queue; + } + public static void stockQueueRouting() throws Exception { + int[] originals = {0}; + Runnable original = () -> originals[0]++; + var action = AddToQueuePatch.replaceButtonRunnable(original); + require(action != original, "Native must replace the stock Next callback"); + app.morphe.extension.youtube.patches.utils.FlyoutUtils.video = "LMNOPQRSTUV"; + action.run(); + require(LoadVideoPatch.opens == 1 && LoadVideoPatch.url.contains("ZYXWVUTSRQP"), + "Stock Next must start the selected video through the existing queue path"); + require(field("pendingQueueStart").get(null) != null && field("queue").get(null) == null, + "First video must wait for native attachment rather than preassigning a queue"); + require(originals[0] == 0 && app.morphe.extension.youtube.patches.utils.PlaylistPatch.dialogs == 0, + "Native action must not invoke Premium or the account queue dialog"); + app.morphe.extension.youtube.patches.utils.FlyoutUtils.stock.add( + new android.util.Pair<>("QUEUE_PLAY_NEXT", 1)); + require(AddToQueuePatch.replaceOnItemClick("QUEUE_PLAY_NEXT") && LoadVideoPatch.opens == 2, + "The existing contextual click hook must route to Native as well"); + require(QueueFlyoutActions.onStockQueueItemClick(IconOuterClass.YTIconType.QUEUE_PLAY_NEXT) + && LoadVideoPatch.opens == 3, + "Direct native sheet dispatch must route to the same captured action"); + require(!QueueFlyoutActions.onStockQueueItemClick(IconOuterClass.YTIconType.SHARE), + "Direct sheet dispatch leaves unrelated actions untouched"); + app.morphe.extension.youtube.patches.utils.FlyoutUtils.button = "SAVE_TO_PLAYLIST"; + require(AddToQueuePatch.replaceButtonRunnable(original) == original, + "Unrelated stock actions must retain their exact callback"); + app.morphe.extension.youtube.patches.utils.FlyoutUtils.button = "QUEUE_PLAY_NEXT"; + app.morphe.extension.youtube.patches.utils.FlyoutUtils.video = ""; + int opens = LoadVideoPatch.opens; + var unavailable = AddToQueuePatch.replaceButtonRunnable(original); + require(unavailable != original, "Missing context cannot invoke Premium in Native mode"); + unavailable.run(); + require(AddToQueuePatch.replaceOnItemClick("QUEUE_PLAY_NEXT") && LoadVideoPatch.opens == opens + && originals[0] == 0, "Contextual clicks must also consume missing-context queue actions"); + require(AddToQueuePatch.flyoutButtonClickLogic("QUEUE_PLAY_LAST", "ZYXWVUTSRQP") + && LoadVideoPatch.opens == opens, "Unavailable Last stays Native and never creates a queue"); + Utils.failToast = true; + require(AddToQueuePatch.flyoutButtonClickLogic("QUEUE_PLAY_NEXT", ""), + "Even a Native feedback failure must not fall through to the Premium command"); + Utils.failToast = false; + Settings.QUEUE_TYPE.value = QueueType.DEFAULT; + require(AddToQueuePatch.replaceButtonRunnable(original) == original, + "Default mode must retain the original callback"); + } + public static void otherQueueRouting() { + var menu = PlaylistQueueRenderer.MenuRenderer.newBuilder(); + for (var icon : List.of(IconOuterClass.YTIconType.QUEUE_PLAY_NEXT, + IconOuterClass.YTIconType.QUEUE_PLAY_LAST)) { + menu.addItems(PlaylistQueueRenderer.MenuItem.newBuilder().setMenuServiceItemRenderer( + PlaylistQueueRenderer.MenuServiceItem.newBuilder().setIcon( + IconOuterClass.Icon.newBuilder().setYtIconType(icon)))); + } + var originalMenu = menu.build(); + Settings.QUEUE_TYPE.value = QueueType.MORPHE; + require(!QueueFlyoutActions.onStockQueueItemClick(IconOuterClass.YTIconType.QUEUE_PLAY_NEXT), + "Additional Native dispatch hooks must not intercept another queue provider"); + require(QueueFlyoutActions.completeQueueMenu(originalMenu) == originalMenu, + "Morphe must retain its exact native menu object"); + require(AddToQueuePatch.flyoutButtonClickLogic("QUEUE_PLAY_NEXT", "ZYXWVUTSRQP"), "Morphe action"); + require(app.morphe.extension.youtube.patches.utils.PlaylistPatch.dialogs == 1 + && LoadVideoPatch.opens == 0, "Morphe must retain its playlist dialog"); + Settings.QUEUE_TYPE.value = QueueType.LOCAL; + require(QueueFlyoutActions.completeQueueMenu(originalMenu) == originalMenu, + "Local must retain its exact native menu object"); + require(AddToQueuePatch.flyoutButtonClickLogic("QUEUE_PLAY_LAST", "ZYXWVUTSRQP"), "Local action"); + require(LocalQueuePatch.additions == 1 && !LocalQueuePatch.next + && app.morphe.extension.youtube.patches.utils.PlaylistPatch.dialogs == 1, + "Local Last must retain its client-side queue and append direction"); + require(!AddToQueuePatch.flyoutButtonClickLogic("SAVE_TO_PLAYLIST", "ZYXWVUTSRQP"), + "Other actions must not be consumed"); + } + public static void staleLastAction() throws Exception { + require(!NativePlaylistQueuePatch.canShowFlyoutLast(), + "Last must not be shown without a Native queue"); + var controller = new Controller(new Playback()); + installOwned(controller); + require(NativePlaylistQueuePatch.canShowFlyoutLast(), "Last is available for an owned Native queue"); + Runnable last = NativePlaylistQueuePatch.getFlyoutQueueAction("ZYXWVUTSRQP", true); + require(last != null, "Last available for an existing queue"); + field("queue").set(null, null); + last.run(); + require(LoadVideoPatch.opens == 0 && field("pendingQueueStart").get(null) == null, + "A stale Last callback must not start a new queue"); + } + static Controller playlistController(boolean mix, boolean minimized) throws Exception { + var controller = new Controller(new Playback()); + controller.playlist = mix ? "RD-mix" : "PL-account"; + controller.playback.local = false; + field("controllerRef").set(null, new WeakReference<>(controller)); + field("observingPlayer").setBoolean(null, true); + VideoInformation.playlist = controller.playlist; + app.morphe.extension.youtube.shared.PlayerType.current = minimized + ? app.morphe.extension.youtube.shared.PlayerType.WATCH_WHILE_MINIMIZED + : app.morphe.extension.youtube.shared.PlayerType.WATCH_WHILE_MAXIMIZED; + return controller; + } + static Controller verifyPlaylistHandoff(boolean mix, boolean minimized) throws Exception { + var controller = playlistController(mix, minimized); + String sourcePlaylist = controller.playlist; + var type = app.morphe.extension.youtube.shared.PlayerType.current; + require(!NativePlaylistQueuePatch.canShowFlyoutLast(), + "An ordinary playlist or Mix must not make Last visible"); + require(AddToQueuePatch.replaceOnItemClick("QUEUE_PLAY_LAST") + && QueueFlyoutActions.onStockQueueItemClick(IconOuterClass.YTIconType.QUEUE_PLAY_LAST), + "Supplied/stale Last remains consumed by Native"); + require(field("queue").get(null) == null && controller.playback.attaches == 0, + "Last cannot create a queue from an ordinary playlist"); + require(AddToQueuePatch.replaceOnItemClick("QUEUE_PLAY_NEXT"), "Native owns stock Next"); + var queue = (PlaylistQueue) field("queue").get(null); + require(queue != null && queue.entries().size() == 2 + && queue.currentEntry().videoId().equals("abcdefghijk") + && queue.entries().get(1).videoId().equals("ZYXWVUTSRQP"), + "Retain current video first and insert selected video next"); + require(controller.playback.attaches == 1 && controller.playback.clears == 0 + && field("nativePlaybackRef").get(null) instanceof WeakReference ref + && ref.get() == controller.playback, + "Rebind navigation through the SAME active native backend"); + require(LoadVideoPatch.opens == 0 && LoadVideoPatch.dismissals == 0 + && LoadVideoPatch.blocks == 0 && field("pendingQueueStart").get(null) == null, + "Never reopen, dismiss or schedule a replacement player for playlist conversion"); + Utils.advanceBy(10000); + require(LoadVideoPatch.opens == 0 && app.morphe.extension.youtube.shared.PlayerType.current == type + && VideoInformation.time == 75432 && VideoInformation.playlist.equals(sourcePlaylist), + "No delayed restart or rewrite of player type, position or shared response identity"); + require(controller.patch_getPlaylistId().isEmpty() && field("originalModel").get(null) == null, + "The local panel has no account ID and Clear cannot restore the old account panel"); + require(NativePlaylistQueuePatch.canShowFlyoutLast(), "Last becomes available after conversion"); + NativePlaylistQueuePatch.onControllerUpdated(controller); + require(field("queue").get(null) == queue, "Retained source response identity does not release ownership"); + return controller; + } + static PlaylistQueueRenderer.NavigationEndpoint playlistWatch(String video, String playlist) { + return PlaylistQueueRenderer.NavigationEndpoint.newBuilder().setWatchEndpoint( + PlaylistQueueRenderer.WatchEndpoint.newBuilder().setVideoId(video).setPlaylistId(playlist)).build(); + } + public static void playlistNextHandoff() throws Exception { verifyPlaylistHandoff(false, false); } + public static void playlistMixHandoff() throws Exception { verifyPlaylistHandoff(true, true); } + public static void firstQueueCurrentVideo() throws Exception { + for (String playlist : List.of("", "PL-account", "RD-mix")) { + var controller = new Controller(new Playback()); + controller.playlist = playlist; + controller.playback.local = false; + VideoInformation.playlist = playlist; + field("controllerRef").set(null, new WeakReference<>(controller)); + field("observingPlayer").setBoolean(null, true); + app.morphe.extension.youtube.shared.PlayerType.current = + app.morphe.extension.youtube.shared.PlayerType.WATCH_WHILE_MINIMIZED; + require(AddToQueuePatch.flyoutButtonClickLogic("QUEUE_PLAY_NEXT", "abcdefghijk"), "Create Native queue"); + var queue = (PlaylistQueue) field("queue").get(null); + require(queue != null && queue.entries().size() == 1 + && queue.currentEntry().videoId().equals("abcdefghijk") + && LoadVideoPatch.opens == 0 && LoadVideoPatch.dismissals == 0, + "Starting from the playing video must contain it once and preserve its player"); + AddToQueuePatch.flyoutButtonClickLogic("QUEUE_PLAY_NEXT", "abcdefghijk"); + queue = (PlaylistQueue) field("queue").get(null); + require(queue.entries().size() == 2 && queue.selectedIndex() == 0, + "Adding the same video again to an existing queue remains an intentional duplicate"); + call("releaseNativeQueue"); + } + } + public static void playlistHandoffFailed() throws Exception { + var controller = playlistController(false, false); + Object original = controller.model; + controller.playback.attachSucceeds = false; + AddToQueuePatch.replaceOnItemClick("QUEUE_PLAY_NEXT"); + require(field("queue").get(null) == null && field("convertedPlaylist").get(null) == null + && controller.model == original && controller.patch_getPlaylistId().equals("PL-account") + && !controller.playback.local && LoadVideoPatch.opens == 0 && LoadVideoPatch.dismissals == 0, + "A rejected attachment restores the old model and never falls back to reloading playback"); + } + public static void convertedPlaylistLifetime() throws Exception { + var controller = verifyPlaylistHandoff(false, false); + var source = playlistWatch("abcdefghijk", "PL-account"); + NativePlaylistQueuePatch.PlaylistModel oldModel = () -> "PL-account"; + require(NativePlaylistQueuePatch.shouldIgnorePlaylistModel(controller, oldModel) + && NativePlaylistQueuePatch.shouldIgnorePlaylistResponse(controller.playback, source), + "Late source models/responses must not reclaim converted navigation"); + require(!NativePlaylistQueuePatch.shouldIgnorePlaylistResponse(new Playback(), source) + && !NativePlaylistQueuePatch.shouldIgnorePlaylistModel(new Controller(new Playback()), oldModel) + && !NativePlaylistQueuePatch.shouldIgnorePlaylistResponse(controller.playback, + playlistWatch("abcdefghijk", "PL-other")) + && !NativePlaylistQueuePatch.shouldIgnorePlaylistResponse(controller.playback, + playlistWatch("ZYXWVUTSRQP", "PL-account")), + "Only the exact old source on its original player can be ignored"); + field("controllerRef").set(null, new WeakReference<>(null)); + require((boolean) call("serviceOwnsQueue") && !(boolean) call("hasForeignPlaybackPlaylist"), + "The active service retains converted ownership after its watch UI is collected"); + field("controllerRef").set(null, new WeakReference<>(controller)); + NativePlaylistQueuePatch.prepareQueueStartContext(source, Map.of()); + require(field("queue").get(null) == null + && !NativePlaylistQueuePatch.shouldIgnorePlaylistResponse(controller.playback, source) + && !NativePlaylistQueuePatch.shouldIgnorePlaylistModel(controller, oldModel), + "Explicit navigation to even the same source playlist releases the exemption"); + controller = playlistController(true, true); + AddToQueuePatch.replaceOnItemClick("QUEUE_PLAY_NEXT"); + Settings.QUEUE_TYPE.value = QueueType.DEFAULT; + call("releaseNativeQueue"); + require(field("convertedPlaylist").get(null) == null + && !NativePlaylistQueuePatch.shouldIgnorePlaylistResponse(controller.playback, + playlistWatch("abcdefghijk", "RD-mix")), + "Provider changes release converted ownership and preserve Default callbacks"); + } + public static void convertedPlaylistUndo() throws Exception { + var controller = verifyPlaylistHandoff(false, true); + var before = field("queue").get(null); + require(controller.playback.patch_clearQueue(), "Native detaches local rows without stopping playback"); + call("releasePanel"); + require(controller.model == null && controller.patch_getPlaylistId().isEmpty(), + "Clear hides the queue rather than restoring the account playlist panel"); + var source = playlistWatch("abcdefghijk", "PL-account"); + require(NativePlaylistQueuePatch.shouldIgnorePlaylistResponse(controller.playback, source) + && NativePlaylistQueuePatch.shouldIgnorePlaylistModel(controller, () -> "PL-account"), + "Late source responses stay excluded after Clear on the same player"); + Class undo = Class.forName(owner.getName() + "${'$'}PendingUndo"); + Constructor create = undo.getDeclaredConstructors()[0]; create.setAccessible(true); + field("pendingUndo").set(null, create.newInstance("converted", before, + new WeakReference<>(controller.playback), null)); + var method = owner.getDeclaredMethod("undoQueueEdit", String.class); method.setAccessible(true); + method.invoke(null, "converted"); + require(field("queue").get(null) != null && controller.playback.attaches == 2 + && LoadVideoPatch.opens == 0 && LoadVideoPatch.dismissals == 0, + "Clear Undo reattaches through the original player despite retained source response ID"); + require(controller.playback.patch_clearQueue(), "Clear after Undo"); + call("releasePanel"); + AddToQueuePatch.replaceOnItemClick("QUEUE_PLAY_NEXT"); + require(field("queue").get(null) != null && controller.playback.attaches == 3 + && LoadVideoPatch.opens == 0, + "Starting another queue after Clear must not inherit the old playlist ID or reopen playback"); + VideoInformation.playlist = "PL-other"; + require((boolean) call("hasForeignPlaybackPlaylist"), "A different playlist must retain foreign ownership"); + } + public static void queueTitleOwnership() throws Exception { + var controller = new Controller(new Playback()); installOwned(controller); + var title = new android.widget.TextView(); + var model = new Object[] { controller.panel }; + NativePlaylistQueuePatch.PlaylistTitle header = new NativePlaylistQueuePatch.PlaylistTitle() { + public Object patch_getPanel() { return model[0]; } + public android.widget.TextView patch_getTitle() { return title; } + }; + // Binding can precede publication. It must recheck ownership after the native call finishes. + field("queue").set(null, null); + NativePlaylistQueuePatch.onPlaylistTitleBound(header); + installOwned(controller); + Utils.drain(); + require(title.params.width == 89 && title.layoutRequests == 1, + "Use rounded native font measurement plus padding even when native width is already constrained WRAP_CONTENT"); + NativePlaylistQueuePatch.onPlaylistTitleBound(header); Utils.drain(); + require(title.layoutRequests == 1, "Repeated metadata/video binds must not request redundant layouts"); + model[0] = new Object(); + NativePlaylistQueuePatch.onPlaylistTitleBound(header); Utils.drain(); + require(title.params.width == android.view.ViewGroup.LayoutParams.WRAP_CONTENT && title.layoutRequests == 2, + "A header reused for an ordinary playlist restores its native width"); + model[0] = controller.panel; + NativePlaylistQueuePatch.onPlaylistTitleBound(header); + model[0] = new Object(); Utils.drain(); + require(title.params.width == android.view.ViewGroup.LayoutParams.WRAP_CONTENT && title.layoutRequests == 2, + "A stale posted queue bind cannot alter the newer ordinary playlist"); + model[0] = controller.panel; + NativePlaylistQueuePatch.onPlaylistTitleBound(header); Utils.drain(); + title.paint.desiredWidth = 96.5f; + NativePlaylistQueuePatch.onPlaylistTitleBound(header); Utils.drain(); + require(title.params.width == 102, "A native font/style change recomputes the required label width"); + call("releasePanel"); + NativePlaylistQueuePatch.onPlaylistTitleBound(header); Utils.drain(); + require(title.params.width == android.view.ViewGroup.LayoutParams.WRAP_CONTENT, "Clear restores the title width even for a retained owned model"); + } + public static void playlistRowContext() { + var row = (app.morphe.extension.youtube.patches.utils.FlyoutUtils.FlyoutMenuVideoIdInterface) + () -> "LMNOPQRSTUV"; + var context = Map.of("com.google.android.libraries.youtube.innertube.endpoint.tag", row); + NativePlaylistQueuePatch.onRowMenuOpened(context); + require(app.morphe.extension.youtube.patches.utils.FlyoutUtils.contexts == 1 + && app.morphe.extension.youtube.patches.utils.FlyoutUtils.video.equals("LMNOPQRSTUV"), + "An ordinary playlist row must replace the previous flyout video ID"); + Settings.QUEUE_TYPE.value = QueueType.MORPHE; + NativePlaylistQueuePatch.onRowMenuOpened(context); + require(app.morphe.extension.youtube.patches.utils.FlyoutUtils.contexts == 1, + "Other providers keep their existing ordinary row context path"); + } + static SharedPreferences.OnSharedPreferenceChangeListener listener() throws Exception { + return (SharedPreferences.OnSharedPreferenceChangeListener) field("QUEUE_SETTING_LISTENER").get(null); + } + static com.google.protobuf.MessageLite feedback(String token) { + return QueueUndoActions.feedback(token, "Queue cleared", "Undo") + .getAddToToastAction().getItem().getToastRenderer(); + } + // These native boundaries record calls; they do not implement a + // second playback engine. The coordinator and serialized models are real. + static class Playback implements NativePlaylistQueuePatch.NativePlayback { + boolean local = true, attachSucceeds = true, clearSucceeds = true; + int clears, attaches, repeat = PlaylistQueue.REPEAT_ONE; + byte[] panel; + public boolean patch_isLocalQueue() { return local; } + public boolean patch_attachQueue(byte[] value, byte[] command) { + attaches++; + try { + var watch = PlaylistQueueRenderer.NavigationEndpoint.parseFrom(command).getWatchEndpoint(); + require(watch.getVideoId().equals(VideoInformation.id) && watch.getPlaylistId().isEmpty(), + "Queue navigation command must describe current video without inheriting the source playlist"); + } catch (com.google.protobuf.InvalidProtocolBufferException exception) { throw new AssertionError(exception); } + if (!attachSucceeds) return false; + panel = value; local = true; return true; + } + public void patch_updateSnapshot(byte[] value) { panel = value; } + public int patch_getSelectedIndex() { return 0; } + public int patch_getRepeatMode() { return repeat; } + public void patch_restoreQueueRepeat(int value) { repeat = value; } + public boolean patch_clearQueue() { + clears++; + if (!clearSucceeds) return false; + local = false; return true; + } + } + static class Controller implements NativePlaylistQueuePatch.PlaylistController { + final Playback playback; + final Object row = (NativePlaylistQueuePatch.QueueRow) () -> true; + final Object panel = (NativePlaylistQueuePatch.QueuePanel) () -> true; + boolean attached = true; + String playlist; + Object model = new Object(), previousModel; + String previousPlaylist; + byte[] feedback; + int feedbacks; + Controller(Playback value) { playback = value; } + public String patch_getVideoId() { return VideoInformation.id; } + public String patch_getPlaylistId() { return model == null ? "" : playlist == null ? VideoInformation.playlist : playlist; } + public Object patch_getModel() { return model; } + public Object patch_getPanel() { return panel; } + public int patch_findQueueRow(Object target) { return target == row ? 1 : -1; } + public void patch_markQueueRows() {} + public boolean patch_isAttached() { return attached; } + public Object patch_getNavigator() { + return new NativePlaylistQueuePatch.ServiceNavigator() { + public String patch_getVideoId() { return VideoInformation.id; } + public NativePlaylistQueuePatch.NativePlayback patch_getQueuePlayback() { return playback; } + public void patch_navigateQueue(boolean next) { throw new AssertionError("Unexpected navigation"); } + public boolean patch_canNavigateQueue(boolean next) { return false; } + }; + } + public Object patch_updatePanel(byte[] value) { + previousModel = model; previousPlaylist = patch_getPlaylistId(); + model = new Object(); playlist = ""; return model; + } + public void patch_restorePanel(Object expected, Object original) { + require(model == expected, "Restore must only touch the accepted model"); + model = original; playlist = original == previousModel ? previousPlaylist : ""; + } + public void patch_showQueueFeedback(byte[] value) { feedbacks++; feedback = value; } + } + static PlaylistQueue installOwned(Controller controller) throws Exception { + var queue = new PlaylistQueue("abcdefghijk", "A").playLast("ZYXWVUTSRQP") + .playLast("abcdefghijk").select(2).withRepeatMode(PlaylistQueue.REPEAT_ONE); + field("queue").set(null, queue); + field("controllerRef").set(null, new WeakReference<>(controller)); + field("nativePlaybackRef").set(null, new WeakReference<>(controller.playback)); + field("publishedModel").set(null, controller.model); + field("originalModel").set(null, null); + field("pendingQueueStart").set(null, null); + field("pendingUndo").set(null, null); + field("pendingReload").set(null, null); + app.morphe.extension.youtube.shared.PlayerType.current = + app.morphe.extension.youtube.shared.PlayerType.WATCH_WHILE_MINIMIZED; + return queue; + } + static PlaylistQueue prepareReload(Controller controller) throws Exception { + var before = installOwned(controller); + field("observingPlayer").setBoolean(null, true); + Runnable dispatch = NativePlaylistQueuePatch.prepareVideoReload("abcdefghijk"); + require(dispatch != null, "Capture an owned Reload"); + require(field("queue").get(null) == null && controller.playback.clears == 1 + && field("pendingUndo").get(null) == null && controller.feedbacks == 0, + "Reload clears local rows and waits for a usable replacement before showing Undo"); + NativePlaylistQueuePatch.onPlaybackTransition(controller); Utils.drain(); + require(controller.feedbacks == 0, "The old descriptor cannot present feedback before intent dispatch"); + controller.attached = false; + app.morphe.extension.youtube.shared.PlayerType.current = + app.morphe.extension.youtube.shared.PlayerType.NONE; + call("onPlaybackContextChanged"); Utils.drain(); + VideoInformation.id = "ZYXWVUTSRQP"; + app.morphe.extension.youtube.shared.PlayerType.current = + app.morphe.extension.youtube.shared.PlayerType.INLINE_MINIMAL; + NativePlaylistQueuePatch.onVideoChanged("ZYXWVUTSRQP"); Utils.drain(); + require(field("pendingReload").get(null) != null && controller.feedbacks == 0, + "Expected teardown and inline previews neither cancel nor receive Reload Undo"); + VideoInformation.id = "abcdefghijk"; + dispatch.run(); // The delayed Reload intent is now being opened. + return before; + } + static Controller finishReload() throws Exception { + var controller = new Controller(new Playback()); controller.playback.local = false; + controller.model = null; // Ordinary standalone playback has no playlist model. + app.morphe.extension.youtube.shared.PlayerType.current = + app.morphe.extension.youtube.shared.PlayerType.WATCH_WHILE_MAXIMIZED; + NativePlaylistQueuePatch.onControllerUpdated(controller); Utils.drain(); + require(controller.feedbacks == 0, "A binding callback alone does not authorize Reload Undo"); + NativePlaylistQueuePatch.onPlaybackTransition(controller); Utils.drain(); + return controller; + } + static com.google.protobuf.MessageLite feedbackUndo(Controller controller) throws Exception { + var toast = PlaylistQueueRenderer.NavigationEndpoint.parseFrom(controller.feedback) + .getAddToToastAction().getItem().getToastRenderer(); + require(toast.getResponseText().getSimpleText().equals("morphe_native_playlist_queue_cleared") + && QueueSaveFeedback.confirmationDuration(toast) == 5000, + "Reload shares the normal Queue cleared label and five-second Undo duration"); + return toast.getActionButton().getButtonRenderer().getServiceEndpoint(); + } + public static void reloadQueueUndo() throws Exception { + var old = new Controller(new Playback()); var before = prepareReload(old); + // An old controller notification cannot offer Undo before the reload watch dispatch. + old.attached = true; + app.morphe.extension.youtube.shared.PlayerType.current = + app.morphe.extension.youtube.shared.PlayerType.WATCH_WHILE_MINIMIZED; + NativePlaylistQueuePatch.onControllerUpdated(old); Utils.drain(); + require(old.feedbacks == 0, "Retained old models must not present Reload feedback"); + var watch = playlistWatch("abcdefghijk", ""); var context = Map.of("existing", true); + require(NativePlaylistQueuePatch.prepareQueueStartContext(watch, context) == context, + "Reload does not force minimized presentation or change native watch arguments"); + NativePlaylistQueuePatch.onControllerUpdated(old); Utils.drain(); + require(old.feedbacks == 0, "Dispatch alone cannot make the old cleared model usable"); + var replacement = finishReload(); + require(replacement.feedbacks == 1 && old.feedbacks == 0 + && field("queue").get(null) == null && field("pendingReload").get(null) == null, + "Only accepted reloaded playback gets Queue cleared, with queue still cleared"); + NativePlaylistQueuePatch.onControllerUpdated(replacement); Utils.drain(); + require(replacement.feedbacks == 1, "Later binding must not present feedback twice"); + var undo = feedbackUndo(replacement); + VideoInformation.time = 81000; + NativePlaylistQueuePatch.onQueueUndoCommand(undo); + var restored = (PlaylistQueue) field("queue").get(null); + require(restored != null && restored.entries().equals(before.entries()) + && restored.selectedIndex() == 2 && replacement.playback.repeat == PlaylistQueue.REPEAT_ONE + && old.playback.attaches == 0 && replacement.playback.attaches == 1, + "Undo restores order, selected duplicate and repeat on the reloaded backend only"); + require(LoadVideoPatch.opens == 0 && LoadVideoPatch.dismissals == 0 + && VideoInformation.time == 81000 && field("pendingQueueStart").get(null) == null, + "Reload Undo never opens, dismisses or seeks the running replacement"); + NativePlaylistQueuePatch.onQueueUndoCommand(undo); + require(replacement.playback.attaches == 1, "Reload Undo is single-use"); + + call("releaseNativeQueue"); VideoInformation.time = 75432; + old = verifyPlaylistHandoff(false, false); + Runnable convertedDispatch = NativePlaylistQueuePatch.prepareVideoReload("abcdefghijk"); + require(convertedDispatch != null + && field("convertedPlaylist").get(null) == null + && VideoInformation.playlist.equals("PL-account"), + "Converted queues clear their source exemption without rewriting shared response data"); + convertedDispatch.run(); + NativePlaylistQueuePatch.prepareQueueStartContext(watch, Map.of()); + replacement = finishReload(); + require(replacement.feedbacks == 0, "Wait for the list-free reload response identity"); + VideoInformation.playlist = ""; + NativePlaylistQueuePatch.onNativeQueueResponse(replacement.playback); Utils.drain(); + require(replacement.feedbacks == 1, "Service readiness can finish a converted-playlist Reload"); + undo = feedbackUndo(replacement); + NativePlaylistQueuePatch.onQueueUndoCommand(undo); + require(field("queue").get(null) != null && LoadVideoPatch.opens == 0, + "Converted queue Reload Undo reattaches without reopening the old account playlist"); + + call("releaseNativeQueue"); + old = new Controller(new Playback()); prepareReload(old); + // Watch intents need not pass through the UI command dispatcher. + replacement = finishReload(); + require(replacement.feedbacks == 1, + "A standalone playback transition after our intent dispatch is sufficient without a menu watch command"); + } + public static void reloadQueueLifetime() throws Exception { + for (String scenario : List.of("otherWatch", "secondWatch", "shorts", "provider", + "timeout", "playlist", "activeVideo")) { + VideoInformation.id = "abcdefghijk"; + var old = new Controller(new Playback()); prepareReload(old); + var watch = playlistWatch("abcdefghijk", ""); + if (scenario.equals("otherWatch")) NativePlaylistQueuePatch.prepareQueueStartContext( + playlistWatch("ZYXWVUTSRQP", ""), Map.of()); + if (scenario.equals("secondWatch")) { + NativePlaylistQueuePatch.prepareQueueStartContext(watch, Map.of()); + NativePlaylistQueuePatch.prepareQueueStartContext(watch, Map.of()); + } + if (scenario.equals("shorts")) { + app.morphe.extension.youtube.shared.ShortsPlayerState.open = true; + call("onPlaybackContextChanged"); Utils.drain(); + app.morphe.extension.youtube.shared.ShortsPlayerState.open = false; + } + if (scenario.equals("provider")) { + Settings.QUEUE_TYPE.value = QueueType.LOCAL; + call("releaseNativeQueue"); Settings.QUEUE_TYPE.value = QueueType.NATIVE; + } + if (scenario.equals("timeout")) Utils.advanceBy(10000); + if (scenario.equals("playlist")) NativePlaylistQueuePatch.prepareQueueStartContext( + playlistWatch("abcdefghijk", "PL-account"), Map.of()); + if (scenario.equals("activeVideo")) { + app.morphe.extension.youtube.shared.PlayerType.current = + app.morphe.extension.youtube.shared.PlayerType.WATCH_WHILE_MAXIMIZED; + VideoInformation.id = "ZYXWVUTSRQP"; + NativePlaylistQueuePatch.onVideoChanged("ZYXWVUTSRQP"); Utils.drain(); + VideoInformation.id = "abcdefghijk"; + } + NativePlaylistQueuePatch.prepareQueueStartContext(watch, Map.of()); + var replacement = finishReload(); + require(replacement.feedbacks == 0 && field("pendingReload").get(null) == null + && field("pendingUndo").get(null) == null, + "Cancelled Reload must never show late feedback or retain an Undo capability: " + scenario); + } + var old = new Controller(new Playback()); prepareReload(old); + var watch = playlistWatch("abcdefghijk", ""); + NativePlaylistQueuePatch.prepareQueueStartContext(watch, Map.of()); + var replacement = finishReload(); var undo = feedbackUndo(replacement); + Utils.advanceBy(5001); NativePlaylistQueuePatch.onQueueUndoCommand(undo); + require(field("queue").get(null) == null && replacement.playback.attaches == 0, + "Expired Reload Undo keeps the queue cleared"); + old = new Controller(new Playback()); prepareReload(old); + NativePlaylistQueuePatch.prepareQueueStartContext(watch, Map.of()); + replacement = finishReload(); undo = feedbackUndo(replacement); + NativePlaylistQueuePatch.prepareQueueStartContext(watch, Map.of()); + NativePlaylistQueuePatch.onQueueUndoCommand(undo); + require(field("queue").get(null) == null && replacement.playback.attaches == 0, + "A deliberate same-video watch cancels Reload Undo even if a backend is reused"); + + old = new Controller(new Playback()); prepareReload(old); + NativePlaylistQueuePatch.prepareQueueStartContext(watch, Map.of()); + replacement = finishReload(); undo = feedbackUndo(replacement); + AddToQueuePatch.flyoutButtonClickLogic("QUEUE_PLAY_NEXT", "ZYXWVUTSRQP"); + var newer = (PlaylistQueue) field("queue").get(null); + NativePlaylistQueuePatch.onQueueUndoCommand(undo); + require(newer != null && newer.entries().size() == 2 && field("queue").get(null) == newer, + "Starting another queue invalidates Reload Undo and cannot resurrect the old sequence"); + + old = new Controller(new Playback()); prepareReload(old); + NativePlaylistQueuePatch.prepareQueueStartContext(watch, Map.of()); + replacement = finishReload(); undo = feedbackUndo(replacement); + require(NativePlaylistQueuePatch.prepareVideoReload("abcdefghijk") == null, + "A second Reload has no active queue to capture"); + NativePlaylistQueuePatch.onQueueUndoCommand(undo); + require(field("queue").get(null) == null && replacement.playback.attaches == 0, + "A second Reload invalidates earlier Undo before its delayed watch dispatch"); + beginClosedRestore(); + require(field("pendingQueueStart").get(null) != null, "Closed-player Undo is awaiting acceptance"); + NativePlaylistQueuePatch.prepareVideoReload("abcdefghijk"); + require(field("pendingQueueStart").get(null) == null && LoadVideoPatch.activeBlock == 0, + "A new Reload cancels an unfinished restore and releases only that restore's inline block"); + + VideoInformation.id = "abcdefghijk"; + old = new Controller(new Playback()); installOwned(old); + Runnable obsoleteDispatch = NativePlaylistQueuePatch.prepareVideoReload("abcdefghijk"); + var newerOwner = new Controller(new Playback()); installOwned(newerOwner); + Runnable currentDispatch = NativePlaylistQueuePatch.prepareVideoReload("abcdefghijk"); + require(obsoleteDispatch != null && currentDispatch != null, "Prepare two distinct Reload operations"); + obsoleteDispatch.run(); + replacement = finishReload(); + require(replacement.feedbacks == 0, "An old delayed intent cannot authorize the newer Reload"); + currentDispatch.run(); + NativePlaylistQueuePatch.onPlaybackTransition(replacement); Utils.drain(); + require(replacement.feedbacks == 1, "Only the matching Reload dispatch authorizes its new player"); + } + static com.google.protobuf.MessageLite nativeWatch(PlaylistQueueRenderer.WatchEndpoint watch) { + var command = PlaylistQueueRenderer.NavigationEndpoint.newBuilder().setWatchEndpoint(watch).build(); + return (com.google.protobuf.MessageLite) Proxy.newProxyInstance(owner.getClassLoader(), + new Class[]{com.google.protobuf.MessageLite.class, QueueUndoActions.Command.class}, + (proxy, method, args) -> { + if (method.getName().equals("toByteArray")) return command.toByteArray(); + if (method.getName().equals("patch_parseQueueCommand")) return + PlaylistQueueRenderer.NavigationEndpoint.parseFrom((byte[]) args[0]); + throw new AssertionError("Unexpected native command operation: " + method.getName()); + }); + } + static final String SENDER_VIEW_KEY = "com.google.android.libraries.youtube.rendering.elements.sender_view"; + static com.google.protobuf.MessageLite musicTap(com.google.protobuf.MessageLite command) { + return NativePlaylistQueuePatch.prepareQueueVideoCommand(command, Map.of(SENDER_VIEW_KEY, new android.view.View())); + } + static PlaylistQueueRenderer.WatchEndpoint.Builder musicWatch() { + return PlaylistQueueRenderer.WatchEndpoint.newBuilder().setVideoId("ZYXWVUTSRQP").setPlaylistId("RDZYXWVUTSRQP"); + } + static PlaylistQueueRenderer.ConfirmationDialogRenderer mixPrompt(Controller controller) throws Exception { + app.morphe.extension.youtube.patches.utils.FeedVideoCardsPatch.individual = false; + require(NativePlaylistQueuePatch.onQueueMixTap(nativeWatch(musicWatch().build()), + Map.of(SENDER_VIEW_KEY, new android.view.View())), "Intentional Mix tap pauses navigation"); + return PlaylistQueueRenderer.NavigationEndpoint.parseFrom(controller.feedback) + .getShowConfirmationDialogEndpoint().getDialog().getConfirmDialogRenderer(); + } + public static void mixChoice() throws Exception { + var controller = new Controller(new Playback()); var before = installOwned(controller); + var prompt = mixPrompt(controller); + require(field("queue").get(null) == before && controller.playback.attaches == 0 + && LoadVideoPatch.opens == 0, "Showing or cancelling a prompt leaves playback unchanged"); + var watch = prompt.getCancelButton().getButtonRenderer().getServiceEndpoint(); + require(!NativePlaylistQueuePatch.onQueueMixTap(watch, Map.of()), + "Watch dispatches the original Mix exactly once without a second prompt"); + require(watch.getWatchEndpoint().equals(musicWatch().build()), "Watch retains original video and Mix ID"); + require(NativePlaylistQueuePatch.onQueueMixTap(watch, Map.of()), "Replayed dialog choice cannot navigate again"); + prompt = mixPrompt(controller); + require(NativePlaylistQueuePatch.onQueueMixTap(watch, Map.of()), + "An old choice must not consume the newer dialog capability"); + var add = prompt.getConfirmButton().getButtonRenderer().getServiceEndpoint(); + MixPlaylistRequest.result = List.of(new VideoMetadata("ZYXWVUTSRQP", "First", "Channel", "", "1:00", false), + new VideoMetadata("abcdefghijk", "Duplicate", "Channel", "", "2:00", false)); + require(NativePlaylistQueuePatch.onQueueMixTap(add, Map.of()), "Add consumes the watch command"); + require(field("queue").get(null) == before, "Do not mutate before fetch completes"); + // An edit in the same playback session must survive the async append. + var edited = before.playLast("0123456789_"); + field("queue").set(null, edited); + Utils.drainBackground(); Utils.drain(); + var after = (PlaylistQueue) field("queue").get(null); + require(after.entries().size() == edited.entries().size() + 2 + && after.entries().get(edited.entries().size()).title().equals("First") + && after.entries().get(before.entries().size()).videoId().equals("0123456789_") + && after.entries().get(after.entries().size()-1).videoId().equals("abcdefghijk") + && after.currentEntry() == before.currentEntry() && after.selectedIndex() == before.selectedIndex(), + "Append the whole ordered batch, including duplicates, without changing selection"); + require(controller.playback.attaches == 1 && controller.playback.clears == 0 + && LoadVideoPatch.opens == 0 && after.repeatMode() == controller.playback.repeat, + "Publish once to the same backend, retaining repeat and continuous playback"); + } + public static void mixChoiceGuards() throws Exception { + var controller = new Controller(new Playback()); var before = installOwned(controller); + var original = nativeWatch(musicWatch().build()); + require(!NativePlaylistQueuePatch.onQueueMixTap(original, Map.of(SENDER_VIEW_KEY, new android.view.View())), + "Individual music videos never get the Mix prompt"); + app.morphe.extension.youtube.patches.utils.FeedVideoCardsPatch.individual = false; + for (var type : new QueueType[] { QueueType.DEFAULT, QueueType.MORPHE, QueueType.LOCAL }) { + Settings.QUEUE_TYPE.value = type; + require(!NativePlaylistQueuePatch.onQueueMixTap(original, Map.of(SENDER_VIEW_KEY, new android.view.View())), + "Do not intercept other queue modes"); + } + Settings.QUEUE_TYPE.value = QueueType.NATIVE; + require(!NativePlaylistQueuePatch.onQueueMixTap(original, Map.of()), "No prompt for an unproven source"); + var prompt = mixPrompt(controller); + var add = prompt.getConfirmButton().getButtonRenderer().getServiceEndpoint(); + // A later ordinary watch invalidates the dialog capability. + NativePlaylistQueuePatch.onQueueMixTap(playlistWatch("abcdefghijk", ""), Map.of()); + require(NativePlaylistQueuePatch.onQueueMixTap(add, Map.of()) && field("pendingMixAddition").get(null) == null, + "A stale dialog cannot append or start playback"); + prompt = mixPrompt(controller); + add = prompt.getConfirmButton().getButtonRenderer().getServiceEndpoint(); + MixPlaylistRequest.result = null; + NativePlaylistQueuePatch.onQueueMixTap(add, Map.of()); Utils.drainBackground(); Utils.drain(); + require(field("queue").get(null) == before && controller.playback.attaches == 0, + "Fetch failure leaves the entire existing queue intact"); + prompt = mixPrompt(controller); + add = prompt.getConfirmButton().getButtonRenderer().getServiceEndpoint(); + MixPlaylistRequest.result = List.of(new VideoMetadata("ZYXWVUTSRQP", "New", "Channel", "", "1:00", false)); + NativePlaylistQueuePatch.onQueueMixTap(add, Map.of()); Utils.drainBackground(); + app.morphe.extension.shared.innertube.utils.AuthUtils.account = "different"; + Utils.drain(); + require(field("queue").get(null) == before && controller.playback.attaches == 0, + "Account changes invalidate a completed fetch before publication"); + app.morphe.extension.shared.innertube.utils.AuthUtils.account = "test"; + prompt = mixPrompt(controller); add = prompt.getConfirmButton().getButtonRenderer().getServiceEndpoint(); + NativePlaylistQueuePatch.onQueueMixTap(add, Map.of()); Utils.drainBackground(); + call("releasePanel"); Utils.drain(); + require(field("queue").get(null) == null && controller.playback.attaches == 0, + "Clear cancels a pending fetch without resurrecting a queue"); + } + public static void musicVideoTap() throws Exception { + var controller = new Controller(new Playback()); var before = installOwned(controller); + var bytes = new java.io.ByteArrayOutputStream(); + var fields = com.google.protobuf.CodedOutputStream.newInstance(bytes); + // Player parameters and opaque fields must survive the command rewrite. + fields.writeString(6, "player-params"); fields.writeUInt32(8, 42); + fields.writeBytes(100, com.google.protobuf.ByteString.copyFromUtf8("opaque")); fields.flush(); + var watch = musicWatch().mergeFrom(bytes.toByteArray()).build(); + var original = nativeWatch(watch); + var prepared = musicTap(original); + require(prepared != original, "Confirmed single-video tap must not start an automatic Mix"); + var expected = PlaylistQueueRenderer.NavigationEndpoint.newBuilder() + .setWatchEndpoint(watch.toBuilder().clearPlaylistId()).build(); + require(java.util.Arrays.equals(expected.toByteArray(), prepared.toByteArray()), + "Only the playlist ID changes; video, player parameters and opaque fields survive"); + require(field("queue").get(null) == before && controller.playback.clears == 0 + && controller.playback.attaches == 0 && LoadVideoPatch.opens == 0, + "Normalization must leave playback and queue insertion to existing native watch handling"); + } + public static void musicVideoTapGuards() throws Exception { + var controller = new Controller(new Playback()); installOwned(controller); + var original = nativeWatch(musicWatch().build()); + app.morphe.extension.youtube.patches.utils.FeedVideoCardsPatch.individual = false; + require(musicTap(original) == original, "Explicit Mix/unknown card must retain its exact command"); + app.morphe.extension.youtube.patches.utils.FeedVideoCardsPatch.individual = true; + for (var watch : new PlaylistQueueRenderer.WatchEndpoint[] { + musicWatch().setPlaylistId("PLsaved").build(), musicWatch().clearPlaylistId().build(), + musicWatch().setIndex(0).build(), musicWatch().setPlaylistSetVideoId("entry").build() }) { + var command = nativeWatch(watch); + require(musicTap(command) == command, "Deliberate indexed/playlist and ordinary video commands must be untouched"); + } + require(NativePlaylistQueuePatch.prepareQueueVideoCommand(original, Map.of()) == original, + "Missing tapped view cannot establish a single-video intent"); + for (var type : new QueueType[] { QueueType.DEFAULT, QueueType.MORPHE, QueueType.LOCAL }) { + Settings.QUEUE_TYPE.value = type; + require(musicTap(original) == original, "Other queue providers retain their navigation"); + } + Settings.QUEUE_TYPE.value = QueueType.NATIVE; + Utils.mainThread = false; + require(musicTap(original) == original, "Background dispatch must not inspect mounted views"); + Utils.mainThread = true; + controller.playback.local = false; + require(musicTap(original) == original, "A retained panel does not authorize rewriting foreign playback"); + controller.playback.local = true; controller.attached = false; + require(musicTap(original) == original, "Detached playback is not an owned queue"); + controller.attached = true; + field("publishedModel").set(null, new Object()); + require(musicTap(original) == original, "A stale queue model cannot authorize normalization"); + field("publishedModel").set(null, controller.model); + field("queue").set(null, null); + require(musicTap(original) == original, "Without a queue the music video must retain stock navigation"); + require(controller.playback.clears == 0 && controller.playback.attaches == 0, + "Rejected taps must not modify any backend"); + } + public static void reloadQueueGuards() throws Exception { + for (var type : new QueueType[] { QueueType.DEFAULT, QueueType.MORPHE, QueueType.LOCAL }) { + var controller = new Controller(new Playback()); var before = installOwned(controller); + Settings.QUEUE_TYPE.value = type; + require(NativePlaylistQueuePatch.prepareVideoReload("abcdefghijk") == null + && controller.playback.clears == 0 && field("queue").get(null) == before, + "Other queue types must retain their own Reload behavior"); + } + Settings.QUEUE_TYPE.value = QueueType.NATIVE; + var controller = new Controller(new Playback()); var before = installOwned(controller); + require(NativePlaylistQueuePatch.prepareVideoReload("ZYXWVUTSRQP") == null + && controller.playback.clears == 0 && field("queue").get(null) == before, + "A different starting video does not authorize queue disposal"); + controller.playback.local = false; + require(NativePlaylistQueuePatch.prepareVideoReload("abcdefghijk") == null + && controller.playback.clears == 0, "Foreign playback must not be cleared"); + controller.playback.local = true; controller.playback.clearSucceeds = false; + require(NativePlaylistQueuePatch.prepareVideoReload("abcdefghijk") == null + && field("queue").get(null) == before && field("pendingReload").get(null) == null, + "A rejected clear cannot create misleading Reload Undo"); + } + static com.google.protobuf.MessageLite closePlayer(Controller controller) throws Exception { + NativePlaylistQueuePatch.onQueuePlayerClosing(); + require(controller.feedbacks == 0, "Confirmation must wait until native close runs"); + controller.attached = false; + // Pending native notifications can still describe the mini player, + // followed by its dismissal animation, before the final NONE event. + for (var type : new app.morphe.extension.youtube.shared.PlayerType[] { + app.morphe.extension.youtube.shared.PlayerType.WATCH_WHILE_MINIMIZED, + app.morphe.extension.youtube.shared.PlayerType.WATCH_WHILE_SLIDING_MINIMIZED_DISMISSED, + app.morphe.extension.youtube.shared.PlayerType.WATCH_WHILE_SLIDING_FULLSCREEN_DISMISSED }) { + app.morphe.extension.youtube.shared.PlayerType.current = type; + call("onPlaybackContextChanged"); Utils.drain(); + require(field("pendingUndo").get(null) != null && controller.feedbacks == 0, + "Dismissal must retain Undo and wait for the completed native close"); + } + app.morphe.extension.youtube.shared.PlayerType.current = + app.morphe.extension.youtube.shared.PlayerType.NONE; + // Exercise actual lifecycle invalidation as the old watch screen disappears. + call("onPlaybackContextChanged"); Utils.drain(); + call("onPlaybackContextChanged"); Utils.drain(); + require(controller.feedbacks == 1, "Close must show exactly one native confirmation"); + var action = app.morphe.extension.youtube.innertube.queue.PlaylistQueueRenderer.NavigationEndpoint + .parseFrom(controller.feedback).getAddToToastAction().getItem().getToastRenderer(); + require(action.getResponseText().getSimpleText().equals("morphe_native_playlist_queue_cleared"), + "Close must use the localized Queue cleared label"); + require(QueueSaveFeedback.confirmationDuration(action) == 5000, + "Player-close Undo uses the same five-second presentation window"); + return action.getActionButton().getButtonRenderer().getServiceEndpoint(); + } + public static void closedPlayerRestore() throws Exception { + var old = new Controller(new Playback()); var before = installOwned(old); + var undo = closePlayer(old); + var accessible = Utils.getContext().accessibility; + accessible.enabled = true; accessible.touchExploration = true; + var toast = app.morphe.extension.youtube.innertube.queue.PlaylistQueueRenderer.NavigationEndpoint + .parseFrom(old.feedback).getAddToToastAction().getItem().getToastRenderer(); + require(QueueSaveFeedback.confirmationDuration(toast) == 0, + "Accessible close Undo must retain the native twenty-second timeout"); + Utils.advanceBy(10001); + require(old.playback.clears == 1 && field("queue").get(null) == null, + "Close must release the live local sequence"); + require(((WeakReference) field("nativePlaybackRef").get(null)).get() == null, + "Disposed playback must not be retained as the restore target"); + VideoInformation.id = "ZYXWVUTSRQP"; + app.morphe.extension.youtube.shared.PlayerType.current = + app.morphe.extension.youtube.shared.PlayerType.INLINE_MINIMAL; + require(NativePlaylistQueuePatch.onQueueUndoCommand(undo), "Native button must consume local Undo"); + require(LoadVideoPatch.opens == 1 && LoadVideoPatch.url.equals( + "https://www.youtube.com/watch?v=abcdefghijk&t=75s"), "Reopen at the saved position"); + var start = (QueueStart) field("pendingQueueStart").get(null); + require(start.candidate() == before && start.restoresClosedPlayer(), + "Restore must preserve the original selected occurrence and order"); + call("tryStartQueue"); + require(old.playback.attaches == 0 && field("queue").get(null) == null, + "Do not attach through the detached old controller"); + require(LoadVideoPatch.activeBlock != 0 && LoadVideoPatch.releases == 0, + "The feed preview must stay blocked while the requested player is not accepted"); + var watch = app.morphe.extension.youtube.innertube.queue.PlaylistQueueRenderer.NavigationEndpoint + .newBuilder().setWatchEndpoint(app.morphe.extension.youtube.innertube.queue.PlaylistQueueRenderer + .WatchEndpoint.newBuilder().setVideoId("abcdefghijk")).build(); + require(Boolean.TRUE.equals(NativePlaylistQueuePatch.prepareQueueStartContext(watch, Map.of()) + .get("start_watch_minimized")), "Undo must reopen in the mini player"); + var replacement = new Controller(new Playback()); replacement.playback.repeat = 0; + field("controllerRef").set(null, new WeakReference<>(replacement)); + VideoInformation.id = "abcdefghijk"; + app.morphe.extension.youtube.shared.PlayerType.current = + app.morphe.extension.youtube.shared.PlayerType.WATCH_WHILE_MINIMIZED; + call("tryStartQueue"); + require(replacement.playback.attaches == 1 && old.playback.attaches == 0, + "Attach the full restored queue only to the newly accepted backend"); + var panel = app.morphe.extension.youtube.innertube.queue.PlaylistQueueRenderer.Panel + .parseFrom(replacement.playback.panel); + require(panel.getItemsCount() == 3 && panel.getSelectedIndex() == 2, + "Native panel must retain the selected duplicate, not just the first video"); + require(replacement.playback.repeat == PlaylistQueue.REPEAT_ONE, + "Restore repeat after the new backend attaches"); + require(field("pendingQueueStart").get(null) == null && Utils.toasts == 0, + "Successful Undo must finish without an unrelated Added toast"); + require(LoadVideoPatch.activeBlock == 0 && LoadVideoPatch.releases == 1, + "Release inline protection once the restored mini player owns its new backend"); + NativePlaylistQueuePatch.onQueueUndoCommand(undo); + require(LoadVideoPatch.opens == 1, "Undo can activate only once"); + } + static void beginClosedRestore() throws Exception { + VideoInformation.id = "abcdefghijk"; + var controller = new Controller(new Playback()); installOwned(controller); + var undo = closePlayer(controller); + VideoInformation.id = "ZYXWVUTSRQP"; + app.morphe.extension.youtube.shared.PlayerType.current = + app.morphe.extension.youtube.shared.PlayerType.INLINE_MINIMAL; + NativePlaylistQueuePatch.onQueueUndoCommand(undo); + } + public static void closedRestoreInlineLifetime() throws Exception { + beginClosedRestore(); + app.morphe.extension.youtube.shared.ShortsPlayerState.open = true; + call("tryStartQueue"); + require(field("pendingQueueStart").get(null) == null && LoadVideoPatch.activeBlock == 0, + "Shorts must cancel the restore and release its feed block"); + app.morphe.extension.youtube.shared.ShortsPlayerState.open = false; + beginClosedRestore(); + Utils.advanceBy(9999); + require(LoadVideoPatch.activeBlock != 0, "Keep protection while native restore can still complete"); + Utils.advanceBy(1); + require(field("pendingQueueStart").get(null) == null && LoadVideoPatch.activeBlock == 0, + "A failed restore must release protection at the navigation deadline"); + beginClosedRestore(); + int replacementBlock = LoadVideoPatch.blockInlinePlaybackUntilPlayerOpens(); + call("releaseNativeQueue"); + require(LoadVideoPatch.activeBlock == replacementBlock, + "Cancelling queue restore must not release a newer Reload operation's block"); + var start = owner.getDeclaredMethod("startQueue", QueueStart.class); start.setAccessible(true); + int blocks = LoadVideoPatch.blocks; + start.invoke(null, new QueueStart(new PlaylistQueue("abcdefghijk", "New queue"), "ZYXWVUTSRQP")); + call("releasePanel"); + require(LoadVideoPatch.blocks == blocks && LoadVideoPatch.activeBlock == replacementBlock, + "Ordinary first-queue navigation must not acquire or release a restore block"); + } + public static void closedPlayerSuperseded() throws Exception { + var controller = new Controller(new Playback()); installOwned(controller); + var undo = closePlayer(controller); + var watch = app.morphe.extension.youtube.innertube.queue.PlaylistQueueRenderer.NavigationEndpoint + .newBuilder().setWatchEndpoint(app.morphe.extension.youtube.innertube.queue.PlaylistQueueRenderer + .WatchEndpoint.newBuilder().setVideoId("abcdefghijk")).build(); + NativePlaylistQueuePatch.prepareQueueStartContext(watch, Map.of()); + NativePlaylistQueuePatch.onQueueUndoCommand(undo); + require(LoadVideoPatch.opens == 0, "A new watch, even the same video, cancels old close Undo"); + controller = new Controller(new Playback()); installOwned(controller); + undo = closePlayer(controller); + app.morphe.extension.youtube.shared.PlayerType.current = + app.morphe.extension.youtube.shared.PlayerType.WATCH_WHILE_MINIMIZED; + call("onPlaybackContextChanged"); Utils.drain(); + NativePlaylistQueuePatch.onQueueUndoCommand(undo); + require(LoadVideoPatch.opens == 0, "An active player after close must cancel the old Undo"); + controller = new Controller(new Playback()); installOwned(controller); + undo = closePlayer(controller); + Settings.QUEUE_TYPE.value = QueueType.LOCAL; + call("releaseNativeQueue"); NativePlaylistQueuePatch.onQueueUndoCommand(undo); + require(LoadVideoPatch.opens == 0, "Other queue type cancels closed queue Undo"); + } + public static void closedPlayerGuards() throws Exception { + var controller = new Controller(new Playback()); var before = installOwned(controller); + app.morphe.extension.youtube.shared.PlayerType.current = + app.morphe.extension.youtube.shared.PlayerType.NONE; + call("onPlaybackContextChanged"); Utils.drain(); + require(field("queue").get(null) == before && field("pendingUndo").get(null) == null + && controller.feedbacks == 0, "Backgrounding alone must not clear a live queue or show Undo"); + app.morphe.extension.youtube.shared.PlayerType.current = + app.morphe.extension.youtube.shared.PlayerType.WATCH_WHILE_MINIMIZED; + VideoInformation.playlist = "PL-account"; + NativePlaylistQueuePatch.onQueuePlayerClosing(); + require(controller.playback.clears == 0 && field("queue").get(null) == before, + "Account playlist playback must fall through unchanged"); + VideoInformation.playlist = ""; VideoInformation.time = -1; + NativePlaylistQueuePatch.onQueuePlayerClosing(); + require(controller.playback.clears == 0 && field("pendingUndo").get(null) == null, + "Unknown playback position must not create misleading restoration feedback"); + VideoInformation.time = 75432; controller.playback.local = false; + NativePlaylistQueuePatch.onQueuePlayerClosing(); + require(controller.playback.clears == 0, "Never clear a foreign playback backend"); + } + public static void singleRemovalSilent() throws Exception { + var controller = new Controller(new Playback()); installOwned(controller); + var hide = app.morphe.extension.youtube.innertube.queue.PlaylistQueueRenderer.NavigationEndpoint + .newBuilder().setHideEnclosingAction(app.morphe.extension.youtube.innertube.queue + .PlaylistQueueRenderer.HideEnclosingAction.newBuilder().setHack(true)).build(); + require(NativePlaylistQueuePatch.onQueueMenuCommand(hide, Map.of( + "com.google.android.libraries.youtube.innertube.endpoint.tag", controller.row)) == null, + "Successful owned removal must consume native Hide without feedback"); + var current = (PlaylistQueue) field("queue").get(null); + require(current.entries().size() == 2 && current.selectedIndex() == 1, + "Remove the requested row and keep the selected duplicate"); + require(field("pendingUndo").get(null) == null && controller.feedbacks == 0 && Utils.toasts == 0, + "Single removal must be silent"); + NativePlaylistQueuePatch.onQueueMenuCommand(hide, Map.of( + "com.google.android.libraries.youtube.innertube.endpoint.tag", controller.row)); + require(field("queue").get(null) == current, "The playing row must still be protected"); + require(NativePlaylistQueuePatch.onQueueMenuCommand(hide, Map.of()) == hide, + "Ordinary native removal must retain its original command"); + } + public static void menuClearUndo() throws Exception { + var controller = new Controller(new Playback()); var before = installOwned(controller); + var hide = app.morphe.extension.youtube.innertube.queue.PlaylistQueueRenderer.NavigationEndpoint + .newBuilder().setHideEnclosingAction(app.morphe.extension.youtube.innertube.queue + .PlaylistQueueRenderer.HideEnclosingAction.newBuilder().setHack(true)).build(); + var nativeCommand = (com.google.protobuf.MessageLite) Proxy.newProxyInstance(owner.getClassLoader(), + new Class[]{com.google.protobuf.MessageLite.class, QueueUndoActions.Command.class}, + (proxy, method, args) -> { + if (method.getName().equals("toByteArray")) return hide.toByteArray(); + if (method.getName().equals("patch_parseQueueCommand")) return + app.morphe.extension.youtube.innertube.queue.PlaylistQueueRenderer.NavigationEndpoint + .parseFrom((byte[]) args[0]); + throw new AssertionError("Unexpected native command operation: " + method.getName()); + }); + var feedback = NativePlaylistQueuePatch.onQueueMenuCommand(nativeCommand, Map.of( + "com.google.android.libraries.youtube.innertube.endpoint.tag", controller.panel)); + require(field("queue").get(null) == null && controller.playback.clears == 1, + "Menu Clear must detach only the local sequence"); + var undo = app.morphe.extension.youtube.innertube.queue.PlaylistQueueRenderer.NavigationEndpoint + .parseFrom(feedback.toByteArray()).getAddToToastAction().getItem().getToastRenderer() + .getActionButton().getButtonRenderer().getServiceEndpoint(); + NativePlaylistQueuePatch.onQueueUndoCommand(undo); + var restored = (PlaylistQueue) field("queue").get(null); + require(restored.entries().equals(before.entries()) && restored.selectedIndex() == 2, + "Menu Clear Undo must retain the selected duplicate and complete order"); + require(controller.playback.attaches == 1 && LoadVideoPatch.opens == 0, + "Menu Clear Undo must reuse live playback without reopening or seeking"); + } + public static void normalUndoTiming() throws Exception { + install(backend(true, true)); + var accessible = Utils.getContext().accessibility; + // Enabled accessibility without touch exploration retains native normal timing. + accessible.enabled = true; + require(QueueSaveFeedback.confirmationDuration(feedback("token")) == 5000, + "Normal Clear snackbar must last five seconds"); + var first = field("pendingUndo").get(null); + require(QueueSaveFeedback.confirmationDuration(feedback("foreign")) == 0, + "A stale/foreign token must not override a snackbar"); + require(field("pendingUndo").get(null) == first, + "Foreign feedback must not replace the active capability"); + Utils.advanceBy(1000); + QueueSaveFeedback.confirmationDuration(feedback("token")); + var renewed = field("pendingUndo").get(null); + require(renewed != first, "Re-created feedback must replace the timer identity"); + Utils.advanceBy(4000); + require(field("pendingUndo").get(null) == renewed, + "An old timer must not expire re-created feedback"); + Utils.advanceBy(999); + require(field("pendingUndo").get(null) == renewed, "Undo must remain through the visible window"); + Utils.advanceBy(1); + require(field("pendingUndo").get(null) == null, "Normal Undo must expire with the five-second confirmation"); + } + public static void accessibleUndoTiming() throws Exception { + install(backend(true, true)); + var accessible = Utils.getContext().accessibility; + accessible.enabled = true; accessible.touchExploration = true; + require(QueueSaveFeedback.confirmationDuration(feedback("token")) == 0, + "Accessible feedback must retain the verified native twenty-second action timeout"); + var current = field("pendingUndo").get(null); + Utils.advanceBy(10000); + require(field("pendingUndo").get(null) == current, + "Accessible Undo must not expire at the old ten-second deadline"); + Utils.advanceBy(9999); + require(field("pendingUndo").get(null) == current, "Accessible Undo must remain through the visible window"); + Utils.advanceBy(1); + require(field("pendingUndo").get(null) == null, "Accessible Undo must expire at twenty seconds"); + } + public static void queueTypeSelection() throws Exception { + Settings.QUEUE_TYPE.value = QueueType.NATIVE; + require(!(boolean) call("isNativeQueueDisabled"), "Native queue type must enable native queue"); + Settings.QUEUE_TYPE.value = QueueType.MORPHE; + require((boolean) call("isNativeQueueDisabled"), "Morphe queue type must disable native queue"); + Settings.QUEUE_TYPE.value = QueueType.LOCAL; + require((boolean) call("isNativeQueueDisabled"), "Premium-style queue type must disable native queue"); + Settings.QUEUE_TYPE.value = QueueType.DEFAULT; + require((boolean) call("isNativeQueueDisabled"), "Default queue type must disable native queue"); + } + public static void acceptedHandoff() throws Exception { + Settings.QUEUE_TYPE.value = QueueType.NATIVE; + var live = backend(true, true); + PlaylistQueue before = install(live); + // Settings notification can precede the setting cache write. + listener().onSharedPreferenceChanged(null, Settings.QUEUE_TYPE.key); + require(field("queue").get(null) == before && clears == 0, "Do not detach inside preference notification"); + Settings.QUEUE_TYPE.value = QueueType.LOCAL; + Utils.drain(); + require(clears == 1 && field("queue").get(null) == null, "Detach local backend once before releasing ownership"); + require(((WeakReference) field("nativePlaybackRef").get(null)).get() == null, "Release backend reference"); + require(field("publishedModel").get(null) == null, "Release accepted panel ownership"); + require(field("pendingQueueStart").get(null) == null && field("pendingUndo").get(null) == null, + "Cancel both late starts and stale Undo"); + require(OpenVideosFullscreenHookPatch.minimized == null, "Cancel pending minimized start"); + require(QueueFullscreenActions.refreshes == 1 && Logger.errors == 0, "Refresh after successful handoff"); + require(LocalQueuePatch.savedItems.equals(List.of("saved-video")), "Preserve Morphe items"); + NativePlaylistQueuePatch.onNativeQueueResponse(live); Utils.drain(); + require(clears == 1 && field("queue").get(null) == null, "Late response must not attach native queue"); + Settings.QUEUE_TYPE.value = QueueType.NATIVE; + listener().onSharedPreferenceChanged(null, Settings.QUEUE_TYPE.key); Utils.drain(); + require(field("queue").get(null) == null, "Switching back to native queue must not resurrect old queue"); + } + public static void failedDetach() throws Exception { + Settings.QUEUE_TYPE.value = QueueType.LOCAL; + var rejected = backend(true, false); var before = install(rejected); + call("releaseNativeQueue"); + require(clears == 1 && field("queue").get(null) == before, "Failed detach must retain ownership for retry"); + require(((WeakReference) field("nativePlaybackRef").get(null)).get() == rejected, + "Failed detach must retain backend reference"); + require(field("pendingQueueStart").get(null) == null && field("pendingUndo").get(null) == null, + "Failed detach must still invalidate stale work"); + require(Logger.errors == 1 && QueueFullscreenActions.refreshes == 0, "Report failure without false UI release"); + } + public static void foreignBackend() throws Exception { + Settings.QUEUE_TYPE.value = QueueType.LOCAL; + install(backend(false, true)); call("releaseNativeQueue"); + require(clears == 0 && field("queue").get(null) == null, "Never clear an unrelated backend"); + } + public static void pendingStart() throws Exception { + Settings.QUEUE_TYPE.value = QueueType.LOCAL; + field("pendingQueueStart").set(null, new QueueStart(new PlaylistQueue("abcdefghijk", "Pending"), "")); + call("tryStartQueue"); + require(field("pendingQueueStart").get(null) == null, "Cancel first-video start while another queue type is selected"); + } + } + """.trimIndent()) + } + try { + assertEquals(0, ToolProvider.getSystemJavaCompiler().run(null, null, null, + "--release", "17", "-classpath", "${classes.path}${File.pathSeparator}${protobuf.path}", + "-d", output.path, *stubs.map { it.path }.toTypedArray(), fixture.path)) + URLClassLoader(arrayOf(output.toURI().toURL(), classes.toURI().toURL()), + GeneratedMessageLite::class.java.classLoader).use { loader -> + loader.loadClass("app.morphe.extension.youtube.patches.queue.RuntimeFixture") + .getMethod(scenario).invoke(null) + } + } finally { + output.deleteRecursively() + } + } +} diff --git a/patches/src/test/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/QueueSaveBoundaryTest.kt b/patches/src/test/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/QueueSaveBoundaryTest.kt new file mode 100644 index 0000000000..1a7e31b371 --- /dev/null +++ b/patches/src/test/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/QueueSaveBoundaryTest.kt @@ -0,0 +1,103 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to Morphe contributions. + */ + +package app.morphe.patches.youtube.interaction.nativeplaylistqueue + +import com.google.protobuf.GeneratedMessageLite +import java.io.File +import java.net.URLClassLoader +import javax.tools.ToolProvider +import kotlin.io.path.createTempDirectory +import kotlin.test.Test +import kotlin.test.assertEquals + +class QueueSaveBoundaryTest { + // This error callback logs through Android's Logger. Keep only that boundary isolated; + // happy-path mutations and wire formats are direct extension tests. + @Test + fun `unsupported destination follows native error path instead of saving a partial queue`() { + val root = generateSequence(File("").absoluteFile) { it.parentFile } + .first { File(it, "extensions/youtube/src/main/proto").isDirectory } + val output = createTempDirectory("playlist-queue-save").toFile() + val generated = File(root, + "extensions/youtube/build/generated/java/generateReleaseProto/java/app/morphe/extension/youtube/innertube") + val protobuf = File(GeneratedMessageLite::class.java.protectionDomain.codeSource.location.toURI()) + val logger = File(output, "Logger.java").apply { + writeText(""" + package app.morphe.extension.shared; + public class Logger { + public static void printException(java.util.function.Supplier message, Throwable error) {} + } + """.trimIndent()) + } + val nullable = File(output, "Nullable.java").apply { + writeText("package androidx.annotation; public @interface Nullable {}") + } + val strings = File(output, "StringRef.java").apply { + writeText(""" + package app.morphe.extension.shared; + public class StringRef { + public static String str(String name, Object... args) { return name; } + } + """.trimIndent()) + } + val duration = File(output, "NativePlaylistQueuePatch.java").apply { + writeText(""" + package app.morphe.extension.youtube.patches.queue; + public class NativePlaylistQueuePatch { + static int queueUndoDuration(String token) { + throw new AssertionError("Save error handling must not create removal feedback"); + } + } + """.trimIndent()) + } + val fixture = File(output, "SaveFixture.java").apply { + writeText(""" + package app.morphe.extension.youtube.patches.queue; + import java.util.*; + import app.morphe.extension.youtube.innertube.queue.PlaylistQueueRenderer.*; + public class SaveFixture { + static class Picker implements QueueSaveActions.Picker { + List videoIds; + Picker(List ids) { videoIds = ids; } + public List patch_getQueueSaveVideoIds() { return videoIds; } + public com.google.protobuf.MessageLite patch_parseQueueSaveResponse(byte[] bytes) { + try { return QueueSaveResponse.parseFrom(bytes); } + catch (Exception error) { throw new AssertionError(error); } + } + } + public static void rejectUnsupportedDestination() { + var destination = QueueSaveDestinationItem.newBuilder().setPlaylistAddToOptionRenderer( + QueueSaveDestination.newBuilder().setAddEndpoint(NavigationEndpoint.getDefaultInstance())); + var unsupported = QueueSaveResponse.newBuilder().addContents( + QueueSaveResponseItem.newBuilder().setAddToPlaylistRenderer( + QueueSavePickerRenderer.newBuilder().addPlaylists(destination))).build(); + var result = (QueueSaveResponse) QueueSaveActions.onPickerResponse( + new Picker(List.of("abcdefghijk", "lmnopqrstuv")), unsupported); + if (result.getContentsCount() != 0) { + throw new AssertionError("Failed batch binding must follow the empty native error path"); + } + } + } + """.trimIndent()) + } + try { + assertEquals(0, ToolProvider.getSystemJavaCompiler().run(null, null, null, + "--release", "17", "-classpath", protobuf.path, "-d", output.path, + File(root, "extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/queue/QueueSaveActions.java").path, + File(generated, "IconOuterClass.java").path, + File(generated, "queue/PlaylistQueueRenderer.java").path, logger.path, nullable.path, strings.path, duration.path, + File(root, "extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/queue/QueueSaveFeedback.java").path, fixture.path)) + URLClassLoader(arrayOf(output.toURI().toURL()), GeneratedMessageLite::class.java.classLoader).use { loader -> + loader.loadClass("app.morphe.extension.youtube.patches.queue.SaveFixture") + .getMethod("rejectUnsupportedDestination").invoke(null) + } + } finally { + output.deleteRecursively() + } + } +} From fcc02f77d5a9d7a13b74d14c12b302f9d9902c61 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Sun, 25 Oct 2026 14:05:11 +0000 Subject: [PATCH 052/189] bump(Reddit): Add support for `2026.40.0` --- .../main/kotlin/app/morphe/patches/reddit/shared/Constants.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/patches/src/main/kotlin/app/morphe/patches/reddit/shared/Constants.kt b/patches/src/main/kotlin/app/morphe/patches/reddit/shared/Constants.kt index 036dbc12c0..b50c6ee5b1 100644 --- a/patches/src/main/kotlin/app/morphe/patches/reddit/shared/Constants.kt +++ b/patches/src/main/kotlin/app/morphe/patches/reddit/shared/Constants.kt @@ -26,10 +26,8 @@ internal object Constants { ), AppTarget( version = "2026.40.0", - minSdk = 29, - isExperimental = true + minSdk = 29 ), - // 2026.33.0 has issues where replying can show an empty comment box. AppTarget( version = "2026.24.0", // Last version with disable modern home. minSdk = 29 From 6407d5efa0f7d99fb54909bc39e2074e8f60ac42 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Sun, 25 Oct 2026 14:15:04 +0000 Subject: [PATCH 053/189] bump(Reddit): Add experimental support for `2026.43.0` --- .../reddit/layout/flair/Fingerprints.kt | 7 ++- .../reddit/layout/fullwidth/Fingerprints.kt | 58 ++++++++++++------- .../fullwidth/FullWidthFeedMediaPatch.kt | 4 +- .../reddit/layout/mediaviewer/Fingerprints.kt | 21 +++---- .../MediaViewerBlackBackgroundPatch.kt | 2 +- .../DisableScreenshotPopupPatch.kt | 48 +++++++++------ .../layout/screenshotpopup/Fingerprints.kt | 20 +++++++ .../reddit/misc/version/VersionCheckPatch.kt | 3 + .../morphe/patches/reddit/shared/Constants.kt | 5 ++ 9 files changed, 114 insertions(+), 54 deletions(-) diff --git a/patches/src/main/kotlin/app/morphe/patches/reddit/layout/flair/Fingerprints.kt b/patches/src/main/kotlin/app/morphe/patches/reddit/layout/flair/Fingerprints.kt index de729803c8..6034b137cd 100644 --- a/patches/src/main/kotlin/app/morphe/patches/reddit/layout/flair/Fingerprints.kt +++ b/patches/src/main/kotlin/app/morphe/patches/reddit/layout/flair/Fingerprints.kt @@ -82,13 +82,18 @@ internal object FeedPostTitleSectionToStringFingerprint : Fingerprint( ) /** - * Reddit's mapper that creates a flair element with a single flair. + * Reddit's mapper that creates a flair element with a single flair from a GraphQL fragment. + * The feed post mapper creates it the same way, but has no community placeholder strings. */ internal fun postFlairsElementMapperFingerprint( presentationModelType: String, flairsElementType: String ) = Fingerprint( returnType = flairsElementType, + strings = listOf( + "subredditName", + "subredditId" + ), filters = listOf( methodCall( definingClass = presentationModelType, diff --git a/patches/src/main/kotlin/app/morphe/patches/reddit/layout/fullwidth/Fingerprints.kt b/patches/src/main/kotlin/app/morphe/patches/reddit/layout/fullwidth/Fingerprints.kt index 588d9480a9..7a2ababe43 100644 --- a/patches/src/main/kotlin/app/morphe/patches/reddit/layout/fullwidth/Fingerprints.kt +++ b/patches/src/main/kotlin/app/morphe/patches/reddit/layout/fullwidth/Fingerprints.kt @@ -211,32 +211,46 @@ internal object PostDetailVideoFingerprint : Fingerprint( ) /** - * Gallery in the post details, since 2026.36.0. Its 14th parameter insets the gallery - * with side padding and rounded corners. + * Leading parameters of the gallery in the post details. + * Its 14th parameter insets the gallery with side padding and rounded corners. + */ +private val POST_DETAIL_GALLERY_PARAMETERS = listOf( + "L", + "Z", + "Lkotlin/jvm/functions/Function1;", + "Ljava/lang/String;", + "L", + "I", + "Z", + "Z", + "Z", + "Z", + "Z", + "Z", + "Lkotlin/jvm/functions/Function0;", + "Z", + "Ljava/lang/Float;" +) + +/** + * Gallery in the post details, since 2026.43.0. */ internal object PostDetailGalleryFingerprint : Fingerprint( accessFlags = listOf(AccessFlags.PUBLIC, AccessFlags.STATIC, AccessFlags.FINAL), returnType = "V", - parameters = listOf( - "L", - "Z", - "Lkotlin/jvm/functions/Function1;", - "Ljava/lang/String;", - "L", - "I", - "Z", - "Z", - "Z", - "Z", - "Z", - "Z", - "Lkotlin/jvm/functions/Function0;", - "Z", - "Ljava/lang/Float;", - "L", - "I", - "I" - ), + parameters = POST_DETAIL_GALLERY_PARAMETERS + listOf("Z", "L", "I", "I"), + filters = listOf( + string("post_media_gallery_content") + ) +) + +/** + * Gallery in the post details, 2026.36.0 to 2026.42.0. + */ +internal object PostDetailGalleryLegacyFingerprint : Fingerprint( + accessFlags = listOf(AccessFlags.PUBLIC, AccessFlags.STATIC, AccessFlags.FINAL), + returnType = "V", + parameters = POST_DETAIL_GALLERY_PARAMETERS + listOf("L", "I", "I"), filters = listOf( string("post_media_gallery_content") ) diff --git a/patches/src/main/kotlin/app/morphe/patches/reddit/layout/fullwidth/FullWidthFeedMediaPatch.kt b/patches/src/main/kotlin/app/morphe/patches/reddit/layout/fullwidth/FullWidthFeedMediaPatch.kt index 4f85d4b99d..fa4bf7bfb2 100644 --- a/patches/src/main/kotlin/app/morphe/patches/reddit/layout/fullwidth/FullWidthFeedMediaPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/reddit/layout/fullwidth/FullWidthFeedMediaPatch.kt @@ -20,6 +20,7 @@ import app.morphe.patches.reddit.misc.settings.settingsPatch import app.morphe.patches.reddit.misc.version.is_2026_24_0_or_greater import app.morphe.patches.reddit.misc.version.is_2026_36_0_or_greater import app.morphe.patches.reddit.misc.version.is_2026_38_0_or_greater +import app.morphe.patches.reddit.misc.version.is_2026_43_0_or_greater import app.morphe.patches.reddit.misc.version.versionCheckPatch import app.morphe.patches.reddit.shared.Constants.COMPATIBILITY_REDDIT import app.morphe.util.findInstructionIndicesReversedOrThrow @@ -296,7 +297,8 @@ val fullWidthFeedMediaPatch = bytecodePatch( if (is_2026_36_0_or_greater) { // Galleries in the post details. Same register limits as videos. - PostDetailGalleryFingerprint.method.addInstructions( + (if (is_2026_43_0_or_greater) PostDetailGalleryFingerprint + else PostDetailGalleryLegacyFingerprint).method.addInstructions( 0, """ invoke-static/range { p13 .. p13 }, $EXTENSION_CLASS->isMediaInsetEnabled(Z)Z diff --git a/patches/src/main/kotlin/app/morphe/patches/reddit/layout/mediaviewer/Fingerprints.kt b/patches/src/main/kotlin/app/morphe/patches/reddit/layout/mediaviewer/Fingerprints.kt index 6aa1bacc61..e6ba971d38 100644 --- a/patches/src/main/kotlin/app/morphe/patches/reddit/layout/mediaviewer/Fingerprints.kt +++ b/patches/src/main/kotlin/app/morphe/patches/reddit/layout/mediaviewer/Fingerprints.kt @@ -10,7 +10,6 @@ package app.morphe.patches.reddit.layout.mediaviewer import app.morphe.patcher.Fingerprint import app.morphe.patcher.InstructionLocation.MatchAfterImmediately import app.morphe.patcher.InstructionLocation.MatchAfterWithin -import app.morphe.patcher.checkCast import app.morphe.patcher.fieldAccess import app.morphe.patcher.literal import app.morphe.patcher.methodCall @@ -192,21 +191,23 @@ internal object VideoPlayerBackgroundFingerprint : Fingerprint( ) /** - * Any read of LocalContext from a composable. + * Static initializer of the Android composition locals, which creates LocalContext. */ internal object LocalContextFingerprint : Fingerprint( + definingClass = "Landroidx/compose/ui/platform/AndroidCompositionLocals_androidKt;", + name = "", filters = listOf( fieldAccess( opcode = Opcode.SGET_OBJECT, - definingClass = "Landroidx/compose/ui/platform/AndroidCompositionLocals_androidKt;" + definingClass = $$"Landroidx/compose/ui/platform/AndroidCompositionLocals_androidKt$LocalContext$1;", + name = "INSTANCE" ), - methodCall( - returnType = "Ljava/lang/Object;", - parameters = listOf("L"), - location = MatchAfterImmediately() - ), - opcode(Opcode.MOVE_RESULT_OBJECT, location = MatchAfterImmediately()), - checkCast("Landroid/content/Context;", location = MatchAfterImmediately()) + // LocalContext field. + fieldAccess( + opcode = Opcode.SPUT_OBJECT, + definingClass = "this", + location = MatchAfterWithin(2) + ) ) ) diff --git a/patches/src/main/kotlin/app/morphe/patches/reddit/layout/mediaviewer/MediaViewerBlackBackgroundPatch.kt b/patches/src/main/kotlin/app/morphe/patches/reddit/layout/mediaviewer/MediaViewerBlackBackgroundPatch.kt index daff2b7edb..97b3162ab0 100644 --- a/patches/src/main/kotlin/app/morphe/patches/reddit/layout/mediaviewer/MediaViewerBlackBackgroundPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/reddit/layout/mediaviewer/MediaViewerBlackBackgroundPatch.kt @@ -84,7 +84,7 @@ val mediaViewerBlackBackgroundPatch = bytecodePatch( if (is_2026_20_0_or_greater) { // The player is shared with the feed, // so the extension checks the composition context is the media viewer. - val localContextField = LocalContextFingerprint.instructionMatches.first() + val localContextField = LocalContextFingerprint.instructionMatches.last() .getInstruction().reference VideoPlayerBackgroundFingerprint.let { match -> match.method.apply { diff --git a/patches/src/main/kotlin/app/morphe/patches/reddit/layout/screenshotpopup/DisableScreenshotPopupPatch.kt b/patches/src/main/kotlin/app/morphe/patches/reddit/layout/screenshotpopup/DisableScreenshotPopupPatch.kt index 47a92466ab..738bb578d4 100644 --- a/patches/src/main/kotlin/app/morphe/patches/reddit/layout/screenshotpopup/DisableScreenshotPopupPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/reddit/layout/screenshotpopup/DisableScreenshotPopupPatch.kt @@ -10,7 +10,11 @@ package app.morphe.patches.reddit.layout.screenshotpopup import app.morphe.patcher.extensions.InstructionExtensions.addInstructions import app.morphe.patcher.extensions.InstructionExtensions.getInstruction import app.morphe.patcher.patch.bytecodePatch +import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod import app.morphe.patches.reddit.misc.settings.settingsPatch +import app.morphe.patches.reddit.misc.version.is_2026_33_0_or_greater +import app.morphe.patches.reddit.misc.version.is_2026_43_0_or_greater +import app.morphe.patches.reddit.misc.version.versionCheckPatch import app.morphe.patches.reddit.shared.Constants.COMPATIBILITY_REDDIT import app.morphe.util.setExtensionIsPatchIncluded import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction @@ -25,28 +29,34 @@ val disableScreenshotPopupPatch = bytecodePatch( ) { compatibleWith(COMPATIBILITY_REDDIT) - dependsOn(settingsPatch) + dependsOn(settingsPatch, versionCheckPatch) execute { + fun MutableMethod.disableScreenshotPopup(booleanIndex: Int) { + val booleanRegister = getInstruction(booleanIndex).registerA - listOf( - RedditScreenshotTriggerSharingListenerFingerprint, - ScreenshotTakenBannerFingerprint - ).forEach { fingerprint -> - fingerprint.let { - it.method.apply { - val booleanIndex = it.instructionMatches[1].index - val booleanRegister = - getInstruction(booleanIndex).registerA - - addInstructions( - booleanIndex + 1, - """ - invoke-static { v$booleanRegister }, $EXTENSION_CLASS->disableScreenshotPopup(Ljava/lang/Boolean;)Ljava/lang/Boolean; - move-result-object v$booleanRegister - """ - ) - } + addInstructions( + booleanIndex + 1, + """ + invoke-static { v$booleanRegister }, $EXTENSION_CLASS->disableScreenshotPopup(Ljava/lang/Boolean;)Ljava/lang/Boolean; + move-result-object v$booleanRegister + """ + ) + } + + if (is_2026_33_0_or_greater) { + ScreenshotShareSheetFingerprint.let { + it.method.disableScreenshotPopup(it.instructionMatches.first().index) + } + } + + // Banner was removed in 2026.43.0. + if (!is_2026_43_0_or_greater) { + listOf( + RedditScreenshotTriggerSharingListenerFingerprint, + ScreenshotTakenBannerFingerprint + ).forEach { + it.method.disableScreenshotPopup(it.instructionMatches[1].index) } } diff --git a/patches/src/main/kotlin/app/morphe/patches/reddit/layout/screenshotpopup/Fingerprints.kt b/patches/src/main/kotlin/app/morphe/patches/reddit/layout/screenshotpopup/Fingerprints.kt index 17da200330..bc4e71f7f5 100644 --- a/patches/src/main/kotlin/app/morphe/patches/reddit/layout/screenshotpopup/Fingerprints.kt +++ b/patches/src/main/kotlin/app/morphe/patches/reddit/layout/screenshotpopup/Fingerprints.kt @@ -8,7 +8,9 @@ package app.morphe.patches.reddit.layout.screenshotpopup import app.morphe.patcher.Fingerprint +import app.morphe.patcher.InstructionLocation.MatchAfterImmediately import app.morphe.patcher.InstructionLocation.MatchAfterWithin +import app.morphe.patcher.checkCast import app.morphe.patcher.fieldAccess import app.morphe.patcher.methodCall import com.android.tools.smali.dexlib2.AccessFlags @@ -49,3 +51,21 @@ internal object ScreenshotTakenBannerFingerprint : Fingerprint( parameters = listOf("Ljava/lang/Object;"), filters = SHOULD_SHOW_BANNER_FILTERS ) + +/** + * Opens the share sheet after a screenshot, if enabled by a feature flag. Since 2026.33.0. + */ +internal object ScreenshotShareSheetFingerprint : Fingerprint( + definingClass = $$"Lcom/reddit/sharing/screenshot/RedditScreenshotTriggerSharingListener$openShareSheetOnScreenshot$1;", + name = "invokeSuspend", + returnType = "Ljava/lang/Object;", + accessFlags = listOf(AccessFlags.PUBLIC, AccessFlags.FINAL), + parameters = listOf("Ljava/lang/Object;"), + filters = listOf( + checkCast("Ljava/lang/Boolean;"), + methodCall( + smali = "Ljava/lang/Boolean;->booleanValue()Z", + location = MatchAfterImmediately() + ) + ) +) diff --git a/patches/src/main/kotlin/app/morphe/patches/reddit/misc/version/VersionCheckPatch.kt b/patches/src/main/kotlin/app/morphe/patches/reddit/misc/version/VersionCheckPatch.kt index ab0885c755..a5db7bac7c 100644 --- a/patches/src/main/kotlin/app/morphe/patches/reddit/misc/version/VersionCheckPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/reddit/misc/version/VersionCheckPatch.kt @@ -41,6 +41,8 @@ var is_2026_38_0_or_greater: Boolean by Delegates.notNull() private set var is_2026_39_0_or_greater: Boolean by Delegates.notNull() private set +var is_2026_43_0_or_greater: Boolean by Delegates.notNull() + private set val versionCheckPatch = bytecodePatch { execute { @@ -66,5 +68,6 @@ val versionCheckPatch = bytecodePatch { is_2026_36_0_or_greater = isEqualsOrGreaterThan("2026.36.0") is_2026_38_0_or_greater = isEqualsOrGreaterThan("2026.38.0") is_2026_39_0_or_greater = isEqualsOrGreaterThan("2026.39.0") + is_2026_43_0_or_greater = isEqualsOrGreaterThan("2026.43.0") } } diff --git a/patches/src/main/kotlin/app/morphe/patches/reddit/shared/Constants.kt b/patches/src/main/kotlin/app/morphe/patches/reddit/shared/Constants.kt index b50c6ee5b1..11de165596 100644 --- a/patches/src/main/kotlin/app/morphe/patches/reddit/shared/Constants.kt +++ b/patches/src/main/kotlin/app/morphe/patches/reddit/shared/Constants.kt @@ -14,6 +14,11 @@ internal object Constants { "970b91143813b4c9d5f3634f672c9fcaa5621b4efaaedafd6c235cbbb869736f" ), targets = listOf( + AppTarget( + version = "2026.43.0", + minSdk = 29, + isExperimental = true + ), AppTarget( version = "2026.42.0", minSdk = 29, From 2b07abb628fec3bf625a09ab7d019f915c4dfa23 Mon Sep 17 00:00:00 2001 From: OolaaPleur <29483340+OolaaPleur@users.noreply.github.com> Date: Sun, 25 Oct 2026 16:58:33 +0200 Subject: [PATCH 054/189] feat(YouTube - Media notification controls): Add options to seek with Previous & Next buttons (#3955) --- .../MediaNotificationControlsPatch.java | 64 +++++++++++++++++- .../settings/SharedYouTubeSettings.java | 4 ++ .../MediaNotificationControlsPatch.kt | 9 ++- .../misc/medianotification/Fingerprints.kt | 66 ++++++++++++++++++- .../MediaNotificationControlsPatch.kt | 59 +++++++++++++++++ .../values/shared-youtube/strings.xml | 7 ++ 6 files changed, 205 insertions(+), 4 deletions(-) diff --git a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/patches/MediaNotificationControlsPatch.java b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/patches/MediaNotificationControlsPatch.java index c4d063182b..eacec6f9b3 100644 --- a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/patches/MediaNotificationControlsPatch.java +++ b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/patches/MediaNotificationControlsPatch.java @@ -1,17 +1,22 @@ /* * Copyright 2026 Morphe. * https://github.com/MorpheApp/morphe-patches/pull/1322 + * https://github.com/MorpheApp/morphe-patches/pull/3955 * * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. */ package app.morphe.extension.shared.patches; +import static app.morphe.extension.shared.StringRef.str; + import android.media.session.PlaybackState; import app.morphe.extension.shared.Logger; import app.morphe.extension.shared.ResourceType; import app.morphe.extension.shared.ResourceUtils; +import app.morphe.extension.shared.Utils; +import app.morphe.extension.shared.settings.IntegerSetting; import app.morphe.extension.shared.settings.SharedYouTubeSettings; @SuppressWarnings("unused") @@ -19,6 +24,13 @@ public class MediaNotificationControlsPatch { public static final Boolean HIDE_NOTIFICATIONS_MEDIA_SEEKBAR = SharedYouTubeSettings.DISABLE_NOTIFICATION_MEDIA_SEEKBAR.get(); public static final Boolean HIDE_NOTIFICATION_MEDIA_PREV_NEXT = SharedYouTubeSettings.HIDE_NOTIFICATION_MEDIA_PREV_NEXT.get(); + public static final Boolean SEEK_NOTIFICATION_MEDIA_PREV_NEXT = !HIDE_NOTIFICATION_MEDIA_PREV_NEXT + && SharedYouTubeSettings.SEEK_NOTIFICATION_MEDIA_PREV_NEXT.get(); + + /** + * Maximum seek length, in seconds. + */ + private static final int SEEK_LENGTH_MAXIMUM = 3600; private static int previousTitleId; private static int nextTitleId; @@ -52,12 +64,56 @@ public static boolean hideNotificationAction(int titleId) { return false; } + /** + * Injection point. + * + * @return If Previous and Next should rewind and fast forward the current video + * instead of changing videos. + */ + public static boolean seekInsteadOfSkip() { + return SEEK_NOTIFICATION_MEDIA_PREV_NEXT; + } + + /** + * Injection point. + * + * @param seekLength Relative seek of the media session fast forward (positive) + * or rewind (negative) action, in milliseconds. + * @return The seek length to use, in milliseconds. + */ + public static long getSeekLength(long seekLength) { + if (!SEEK_NOTIFICATION_MEDIA_PREV_NEXT) { + return seekLength; + } + + try { + final boolean fastForward = seekLength > 0; + IntegerSetting setting = fastForward + ? SharedYouTubeSettings.SEEK_NOTIFICATION_MEDIA_FAST_FORWARD_LENGTH + : SharedYouTubeSettings.SEEK_NOTIFICATION_MEDIA_REWIND_LENGTH; + + int seconds = setting.get(); + if (seconds < 1 || seconds > SEEK_LENGTH_MAXIMUM) { + Utils.showToastLong(str("morphe_seek_notification_media_length_invalid", SEEK_LENGTH_MAXIMUM)); + seconds = setting.resetToDefault(); + } + + final long millis = seconds * 1000L; + return fastForward ? millis : -millis; + } catch (Exception ex) { + Logger.printException(() -> "getSeekLength failure", ex); + } + + return seekLength; + } + /** * Injection point. */ public static PlaybackState changePlaybackState(PlaybackState state) { try { - if (HIDE_NOTIFICATIONS_MEDIA_SEEKBAR || HIDE_NOTIFICATION_MEDIA_PREV_NEXT) { + if (HIDE_NOTIFICATIONS_MEDIA_SEEKBAR || HIDE_NOTIFICATION_MEDIA_PREV_NEXT + || SEEK_NOTIFICATION_MEDIA_PREV_NEXT) { long filtered = state.getActions(); if (HIDE_NOTIFICATIONS_MEDIA_SEEKBAR) { @@ -67,6 +123,12 @@ public static PlaybackState changePlaybackState(PlaybackState state) { filtered &= ~PlaybackState.ACTION_SKIP_TO_NEXT; filtered &= ~PlaybackState.ACTION_SKIP_TO_PREVIOUS; } + if (SEEK_NOTIFICATION_MEDIA_PREV_NEXT) { + // Media buttons are only delivered if the action is available, + // and there may be no previous or next video to skip to. + filtered |= PlaybackState.ACTION_SKIP_TO_NEXT; + filtered |= PlaybackState.ACTION_SKIP_TO_PREVIOUS; + } return new PlaybackState.Builder(state).setActions(filtered).build(); } diff --git a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/settings/SharedYouTubeSettings.java b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/settings/SharedYouTubeSettings.java index b0db5cb9de..6bd2718b80 100644 --- a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/settings/SharedYouTubeSettings.java +++ b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/settings/SharedYouTubeSettings.java @@ -4,6 +4,7 @@ import static java.lang.Boolean.TRUE; import static app.morphe.extension.shared.settings.Setting.migrateOldSettingToNew; import static app.morphe.extension.shared.settings.Setting.parent; +import static app.morphe.extension.shared.settings.Setting.parentNot; import app.morphe.extension.shared.Utils; import app.morphe.extension.shared.patches.BaseAppRefreshRatePatch.AppRefreshType; @@ -61,6 +62,9 @@ public class SharedYouTubeSettings extends BaseSettings { // Media notification controls public static final BooleanSetting DISABLE_NOTIFICATION_MEDIA_SEEKBAR = new BooleanSetting("morphe_disable_notification_media_seekbar", FALSE, true); public static final BooleanSetting HIDE_NOTIFICATION_MEDIA_PREV_NEXT = new BooleanSetting("morphe_hide_notification_media_prev_next", FALSE, true); + public static final BooleanSetting SEEK_NOTIFICATION_MEDIA_PREV_NEXT = new BooleanSetting("morphe_seek_notification_media_prev_next", FALSE, true, parentNot(HIDE_NOTIFICATION_MEDIA_PREV_NEXT)); + public static final IntegerSetting SEEK_NOTIFICATION_MEDIA_REWIND_LENGTH = new IntegerSetting("morphe_seek_notification_media_rewind_length", 10, parent(SEEK_NOTIFICATION_MEDIA_PREV_NEXT)); + public static final IntegerSetting SEEK_NOTIFICATION_MEDIA_FAST_FORWARD_LENGTH = new IntegerSetting("morphe_seek_notification_media_fast_forward_length", 10, parent(SEEK_NOTIFICATION_MEDIA_PREV_NEXT)); public static final BooleanSetting CHECK_WATCH_HISTORY_DOMAIN_NAME = new BooleanSetting("morphe_check_watch_history_domain_name", TRUE, false, false); diff --git a/patches/src/main/kotlin/app/morphe/patches/shared/misc/medianotification/MediaNotificationControlsPatch.kt b/patches/src/main/kotlin/app/morphe/patches/shared/misc/medianotification/MediaNotificationControlsPatch.kt index d12aceeb6f..4103007db1 100644 --- a/patches/src/main/kotlin/app/morphe/patches/shared/misc/medianotification/MediaNotificationControlsPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/shared/misc/medianotification/MediaNotificationControlsPatch.kt @@ -1,6 +1,7 @@ /* * Copyright 2026 Morphe. * https://github.com/MorpheApp/morphe-patches/pull/1322 + * https://github.com/MorpheApp/morphe-patches/pull/3955 * * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. */ @@ -13,6 +14,7 @@ import app.morphe.patcher.patch.BytecodePatchBuilder import app.morphe.patcher.patch.BytecodePatchContext import app.morphe.patcher.patch.bytecodePatch import app.morphe.patches.shared.MediaSessionSetPlaybackStateFingerprint +import app.morphe.patches.shared.misc.settings.preference.BasePreference import app.morphe.patches.shared.misc.settings.preference.BasePreferenceScreen import app.morphe.patches.shared.misc.settings.preference.PreferenceScreenPreference import app.morphe.patches.shared.misc.settings.preference.SwitchPreference @@ -24,6 +26,7 @@ internal const val EXTENSION_CLASS = internal fun mediaNotificationControlsPatch( block: BytecodePatchBuilder.() -> Unit, preferenceScreen: BasePreferenceScreen.Screen, + additionalPreferences: Set = emptySet(), executeBlock: BytecodePatchContext.() -> Unit = {}, ) = bytecodePatch( name = "Media notification controls", @@ -36,10 +39,12 @@ internal fun mediaNotificationControlsPatch( preferenceScreen.addPreferences( PreferenceScreenPreference( key = "morphe_notification_media_screen", + // Keep the seek options right below the switch they depend on. + sorting = PreferenceScreenPreference.Sorting.UNSORTED, preferences = setOf( - SwitchPreference("morphe_hide_notification_media_prev_next", summary = true), SwitchPreference("morphe_disable_notification_media_seekbar", summary = true), - ) + SwitchPreference("morphe_hide_notification_media_prev_next", summary = true), + ) + additionalPreferences ) ) diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/misc/medianotification/Fingerprints.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/medianotification/Fingerprints.kt index a6f663b4fd..4b96cd772c 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/misc/medianotification/Fingerprints.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/medianotification/Fingerprints.kt @@ -1,6 +1,6 @@ /* * Copyright 2026 Morphe. - * https://github.com/MorpheApp/morphe-patches + * https://github.com/MorpheApp/morphe-patches/pull/3955 * * See the included NOTICE file for GPLv3 Section 7 terms that apply to Morphe contributions. */ @@ -9,6 +9,7 @@ package app.morphe.patches.youtube.misc.medianotification import app.morphe.patcher.Fingerprint import app.morphe.patcher.methodCall +import app.morphe.patcher.string import com.android.tools.smali.dexlib2.AccessFlags /** @@ -26,3 +27,66 @@ internal object PlaybackNotificationAddActionFingerprint : Fingerprint( ) ) ) + +/** + * Media session callback methods. Each one logs its own name, which is used to find it. + * The previous/next methods are in the same class as the fast forward/rewind methods. + */ +internal object MediaSessionSkipToNextFingerprint : Fingerprint( + accessFlags = listOf(AccessFlags.PUBLIC, AccessFlags.FINAL), + returnType = "V", + parameters = listOf(), + filters = listOf( + string("onSkipToNext()") + ) +) + +internal object MediaSessionSkipToPreviousFingerprint : Fingerprint( + accessFlags = listOf(AccessFlags.PUBLIC, AccessFlags.FINAL), + returnType = "V", + parameters = listOf(), + filters = listOf( + string("onSkipToPrevious()") + ) +) + +internal object MediaSessionFastForwardFingerprint : Fingerprint( + accessFlags = listOf(AccessFlags.PUBLIC, AccessFlags.FINAL), + returnType = "V", + parameters = listOf(), + filters = listOf( + string("onFastForward()") + ) +) + +internal object MediaSessionRewindFingerprint : Fingerprint( + accessFlags = listOf(AccessFlags.PUBLIC, AccessFlags.FINAL), + returnType = "V", + parameters = listOf(), + filters = listOf( + string("onRewind()") + ) +) + +/** + * The player's media session callback, which does the actual relative seek of the + * fast-forward and rewind actions. It extends the same callback class as the class of + * [MediaSessionFastForwardFingerprint], overrides the same fast forward/rewind method, + * and passes the seek length to a method that takes a long. + */ +internal class MediaSessionSeekRelativeFingerprint( + callbackClass: String, + methodName: String, +) : Fingerprint( + superclass = callbackClass, + name = methodName, + accessFlags = listOf(AccessFlags.PUBLIC, AccessFlags.FINAL), + returnType = "V", + parameters = listOf(), + filters = listOf( + methodCall( + parameters = listOf("J"), + returnType = "V" + ) + ) +) diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/misc/medianotification/MediaNotificationControlsPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/medianotification/MediaNotificationControlsPatch.kt index 7b2ae796c2..304ec166af 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/misc/medianotification/MediaNotificationControlsPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/medianotification/MediaNotificationControlsPatch.kt @@ -7,9 +7,17 @@ package app.morphe.patches.youtube.misc.medianotification +import app.morphe.patcher.Fingerprint +import app.morphe.patcher.extensions.InstructionExtensions.addInstructions import app.morphe.patcher.extensions.InstructionExtensions.addInstructionsWithLabels +import app.morphe.patcher.patch.BytecodePatchContext +import app.morphe.patcher.util.registersUsed import app.morphe.patches.shared.misc.medianotification.EXTENSION_CLASS import app.morphe.patches.shared.misc.medianotification.mediaNotificationControlsPatch +import app.morphe.patches.shared.misc.settings.preference.InputType +import app.morphe.patches.shared.misc.settings.preference.SwitchPreference +import app.morphe.patches.shared.misc.settings.preference.TextPreference +import app.morphe.patches.shared.misc.settings.preference.noTitleUnsortedPreferenceCategory import app.morphe.patches.youtube.misc.extension.sharedExtensionPatch import app.morphe.patches.youtube.misc.settings.PreferenceScreen import app.morphe.patches.youtube.misc.settings.settingsPatch @@ -26,6 +34,13 @@ val mediaNotificationControlsPatchYouTube = mediaNotificationControlsPatch( compatibleWith(COMPATIBILITY_YOUTUBE) }, preferenceScreen = PreferenceScreen.PLAYER, + additionalPreferences = setOf( + noTitleUnsortedPreferenceCategory( + SwitchPreference("morphe_seek_notification_media_prev_next", summary = true), + TextPreference("morphe_seek_notification_media_rewind_length", inputType = InputType.NUMBER), + TextPreference("morphe_seek_notification_media_fast_forward_length", inputType = InputType.NUMBER) + ) + ), executeBlock = { // Android 12 and lower show the notification actions as the media buttons. // Don't add the previous/next actions, otherwise the buttons remain visible but do nothing. @@ -42,5 +57,49 @@ val mediaNotificationControlsPatchYouTube = mediaNotificationControlsPatch( """ ) } + + // Previous and Next can rewind and fast forward instead, + // using the same seek as the media session rewind and fast forward actions. + seekInsteadOfSkip(MediaSessionSkipToNextFingerprint, MediaSessionFastForwardFingerprint) + seekInsteadOfSkip(MediaSessionSkipToPreviousFingerprint, MediaSessionRewindFingerprint) } ) + +private fun BytecodePatchContext.seekInsteadOfSkip( + skipFingerprint: Fingerprint, + seekFingerprint: Fingerprint, +) { + val seekMethod = seekFingerprint.originalMethod + + // The player callback seeks a fixed amount. Use the seek length setting instead. + MediaSessionSeekRelativeFingerprint( + seekFingerprint.originalClassDef.superclass!!, + seekMethod.name + ).let { + it.method.apply { + val match = it.instructionMatches.first() + val register = match.instruction.registersUsed[1] + + addInstructions( + match.index, + """ + invoke-static { v$register, v${register + 1} }, $EXTENSION_CLASS->getSeekLength(J)J + move-result-wide v$register + """ + ) + } + } + + skipFingerprint.method.addInstructionsWithLabels( + 0, + """ + invoke-static { }, $EXTENSION_CLASS->seekInsteadOfSkip()Z + move-result v0 + if-eqz v0, :skip + invoke-virtual { p0 }, $seekMethod + return-void + :skip + nop + """ + ) +} diff --git a/patches/src/main/resources/addresources/values/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values/shared-youtube/strings.xml index 44f8baf789..306469b451 100644 --- a/patches/src/main/resources/addresources/values/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values/shared-youtube/strings.xml @@ -161,6 +161,13 @@ To sign in, press continue below" Hides Previous and Next buttons in media notification, lock screen, and headphone controls Disable seekbar Prevents dragging the seekbar on the lock screen and notification panel + Seek with Previous & Next buttons + Changes controls for media notification, lock screen, and headphone to seek instead of changing videos + Rewind length + Seconds to rewind with the Previous button + Fast forward length + Seconds to fast-forward with the Next button + Seek length must be between 1 and %s seconds. Using default Disable QUIC protocol From a9bc8b129a5bdf14998270d0f503fb6b51713b9e Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Sun, 25 Oct 2026 15:18:07 +0000 Subject: [PATCH 055/189] chore: Update translations from Crowdin --- .../values-af-rZA/shared-youtube/strings.xml | 7 ++ .../values-af-rZA/youtube/strings.xml | 43 +++++-- .../values-am-rET/youtube/strings.xml | 2 + .../values-ar-rSA/shared-youtube/strings.xml | 7 ++ .../values-ar-rSA/youtube/strings.xml | 113 +++++++++++------- .../values-as-rIN/youtube/strings.xml | 2 + .../values-az-rAZ/youtube/strings.xml | 21 ++-- .../values-be-rBY/youtube/strings.xml | 4 + .../values-bg-rBG/shared-youtube/strings.xml | 7 ++ .../values-bg-rBG/youtube/strings.xml | 43 +++++-- .../values-bn-rBD/shared-youtube/strings.xml | 7 ++ .../values-bn-rBD/youtube/strings.xml | 43 +++++-- .../values-bs-rBA/youtube/strings.xml | 4 + .../values-ca-rES/youtube/strings.xml | 3 + .../values-ckb-rIR/youtube/strings.xml | 5 + .../values-cs-rCZ/shared-youtube/strings.xml | 7 ++ .../values-cs-rCZ/youtube/strings.xml | 43 +++++-- .../values-da-rDK/shared-youtube/strings.xml | 7 ++ .../values-da-rDK/youtube/strings.xml | 43 +++++-- .../values-de-rDE/shared-youtube/strings.xml | 7 ++ .../values-de-rDE/youtube/strings.xml | 43 +++++-- .../values-el-rGR/shared-youtube/strings.xml | 7 ++ .../values-el-rGR/youtube/strings.xml | 43 +++++-- .../values-es-rES/shared-youtube/strings.xml | 7 ++ .../values-es-rES/youtube/strings.xml | 43 +++++-- .../values-et-rEE/youtube/strings.xml | 2 + .../values-eu-rES/youtube/strings.xml | 2 + .../values-fa-rIR/shared-youtube/strings.xml | 7 ++ .../values-fa-rIR/youtube/strings.xml | 43 +++++-- .../values-fi-rFI/shared-youtube/strings.xml | 7 ++ .../values-fi-rFI/youtube/strings.xml | 43 +++++-- .../values-fil-rPH/shared-youtube/strings.xml | 7 ++ .../values-fil-rPH/youtube/strings.xml | 43 +++++-- .../values-fr-rFR/music/strings.xml | 4 +- .../values-fr-rFR/shared-youtube/strings.xml | 7 ++ .../values-fr-rFR/youtube/strings.xml | 45 +++++-- .../values-ga-rIE/music/strings.xml | 44 +++---- .../values-ga-rIE/shared-youtube/strings.xml | 7 ++ .../values-ga-rIE/youtube/strings.xml | 57 ++++++--- .../values-gl-rES/youtube/strings.xml | 5 + .../values-gu-rIN/youtube/strings.xml | 4 + .../values-hi-rIN/shared-youtube/strings.xml | 7 ++ .../values-hi-rIN/youtube/strings.xml | 43 +++++-- .../values-hr-rHR/shared-youtube/strings.xml | 7 ++ .../values-hr-rHR/youtube/strings.xml | 43 +++++-- .../values-hu-rHU/shared-youtube/strings.xml | 7 ++ .../values-hu-rHU/youtube/strings.xml | 43 +++++-- .../values-hy-rAM/youtube/strings.xml | 2 + .../values-in-rID/shared-youtube/strings.xml | 7 ++ .../values-in-rID/youtube/strings.xml | 45 +++++-- .../values-is-rIS/shared-youtube/strings.xml | 7 ++ .../values-is-rIS/youtube/strings.xml | 43 +++++-- .../values-it-rIT/shared-youtube/strings.xml | 7 ++ .../values-it-rIT/youtube/strings.xml | 43 +++++-- .../values-iw-rIL/shared-youtube/strings.xml | 7 ++ .../values-iw-rIL/youtube/strings.xml | 43 +++++-- .../values-ja-rJP/music/strings.xml | 6 +- .../values-ja-rJP/shared-youtube/strings.xml | 7 ++ .../values-ja-rJP/youtube/strings.xml | 43 +++++-- .../values-ka-rGE/youtube/strings.xml | 4 + .../values-kk-rKZ/youtube/strings.xml | 2 + .../values-km-rKH/youtube/strings.xml | 3 + .../values-kmr-rTR/youtube/strings.xml | 8 +- .../values-kn-rIN/youtube/strings.xml | 2 + .../values-ko-rKR/shared-youtube/strings.xml | 7 ++ .../values-ko-rKR/youtube/strings.xml | 43 +++++-- .../values-ky-rKG/youtube/strings.xml | 3 + .../values-lo-rLA/youtube/strings.xml | 2 + .../values-lt-rLT/shared-youtube/strings.xml | 7 ++ .../values-lt-rLT/youtube/strings.xml | 43 +++++-- .../values-lv-rLV/youtube/strings.xml | 3 + .../values-mai-rIN/youtube/strings.xml | 2 + .../values-mk-rMK/youtube/strings.xml | 4 + .../values-mn-rMN/youtube/strings.xml | 2 + .../values-mr-rIN/youtube/strings.xml | 4 +- .../values-ms-rMY/youtube/strings.xml | 5 + .../values-my-rMM/youtube/strings.xml | 2 + .../values-nb-rNO/shared-youtube/strings.xml | 7 ++ .../values-nb-rNO/youtube/strings.xml | 43 +++++-- .../values-ne-rIN/youtube/strings.xml | 2 + .../values-nl-rNL/shared-youtube/strings.xml | 7 ++ .../values-nl-rNL/youtube/strings.xml | 43 +++++-- .../values-or-rIN/youtube/strings.xml | 2 + .../values-pa-rIN/youtube/strings.xml | 2 + .../values-pl-rPL/shared-youtube/strings.xml | 7 ++ .../values-pl-rPL/youtube/strings.xml | 43 +++++-- .../values-pt-rBR/shared-youtube/strings.xml | 7 ++ .../values-pt-rBR/youtube/strings.xml | 43 +++++-- .../values-pt-rPT/shared-youtube/strings.xml | 7 ++ .../values-pt-rPT/youtube/strings.xml | 43 +++++-- .../values-ro-rRO/shared-youtube/strings.xml | 7 ++ .../values-ro-rRO/youtube/strings.xml | 43 +++++-- .../values-ru-rRU/shared-youtube/strings.xml | 7 ++ .../values-ru-rRU/youtube/strings.xml | 43 +++++-- .../values-si-rLK/youtube/strings.xml | 2 + .../values-sk-rSK/shared-youtube/strings.xml | 7 ++ .../values-sk-rSK/youtube/strings.xml | 43 +++++-- .../values-sl-rSI/shared-youtube/strings.xml | 7 ++ .../values-sl-rSI/youtube/strings.xml | 43 +++++-- .../values-sr-rCS/shared-youtube/strings.xml | 7 ++ .../values-sr-rCS/youtube/strings.xml | 43 +++++-- .../values-sr-rSP/shared-youtube/strings.xml | 7 ++ .../values-sr-rSP/youtube/strings.xml | 43 +++++-- .../values-sv-rSE/shared-youtube/strings.xml | 7 ++ .../values-sv-rSE/youtube/strings.xml | 43 +++++-- .../values-sw-rKE/youtube/strings.xml | 2 + .../values-ta-rIN/youtube/strings.xml | 3 + .../values-te-rIN/youtube/strings.xml | 2 + .../values-th-rTH/shared-youtube/strings.xml | 7 ++ .../values-th-rTH/youtube/strings.xml | 43 +++++-- .../values-tr-rTR/shared-youtube/strings.xml | 7 ++ .../values-tr-rTR/shared/strings.xml | 2 +- .../values-tr-rTR/youtube/strings.xml | 43 +++++-- .../values-uk-rUA/music/strings.xml | 32 ++--- .../values-uk-rUA/shared-youtube/strings.xml | 7 ++ .../values-uk-rUA/youtube/strings.xml | 59 ++++++--- .../values-ur-rIN/youtube/strings.xml | 2 + .../values-uz-rUZ/youtube/strings.xml | 8 +- .../values-vi-rVN/shared-youtube/strings.xml | 7 ++ .../values-vi-rVN/youtube/strings.xml | 43 +++++-- .../values-zh-rCN/shared-youtube/strings.xml | 7 ++ .../values-zh-rCN/youtube/strings.xml | 49 ++++++-- .../values-zh-rTW/shared-youtube/strings.xml | 7 ++ .../values-zh-rTW/youtube/strings.xml | 43 +++++-- .../values-zu-rZA/youtube/strings.xml | 2 + 125 files changed, 1974 insertions(+), 453 deletions(-) diff --git a/patches/src/main/resources/addresources/values-af-rZA/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-af-rZA/shared-youtube/strings.xml index bcb829b0a9..97c111c03d 100644 --- a/patches/src/main/resources/addresources/values-af-rZA/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-af-rZA/shared-youtube/strings.xml @@ -151,6 +151,13 @@ Om aan te meld, druk voort hieronder" Versteek Vorige- en Volgende-knoppies in die media-kennisgewing, op die sluitskerm en in die koptelefoonbeheer Deaktiveer soekbalk Voorkom dat die soekbalk op die sluitskerm en kennisgewingspaneel gesleep word + Soek met Vorige & Volgende knoppies + Verander beheer vir mediaberig, slotskerm, en koptelefoon om te soek in plaas van video\'s te verander + Spoel terug lengte + Sekondes om terug te spoel met die Vorige knoppie + Spoel vorentoe lengte + Sekondes om vooruit te spoel met die Volgende knoppie + Soeklengte moet tussen 1 en %s sekondes wees. Gebruik verstekwaarde Deaktiveer QUIC-protokol diff --git a/patches/src/main/resources/addresources/values-af-rZA/youtube/strings.xml b/patches/src/main/resources/addresources/values-af-rZA/youtube/strings.xml index 28b01a4f6d..e083001707 100644 --- a/patches/src/main/resources/addresources/values-af-rZA/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-af-rZA/youtube/strings.xml @@ -473,6 +473,16 @@ Nadat 'n video met die Ask-knoppie een keer oopgemaak is, werk die funksie met a Laai video\'s, podkaste of liedjies af met derdeparty-aflaai-programme Wys speler-oorlade-aflaai-knoppie Wys \'n knoppie in die video-speler wat jou eksterne aflaailaar oopmaak + Vinnige aflaai met \'n tik + Aanwysings om in die agtergrond af te laai. Tik en hou in om die aflaai-program oop te maak. Werk met YTDLnis en YTindir + Merk aflaaiings as gekyk + Video\'s wat met \'n eksterne aflaai-program afgelaai is, word by jou YouTube-kykgeskiedenis gevoeg + Volle HD (1080p) + Matig (192 kbps) + Vinnige aflaai + Video + Klank + Alle opsies Vertaalde stem over Lees video-onderskrifte hardop voor met behulp van teks-na-spraak op die toestel, gesinkroniseer met die afspeel @@ -588,14 +598,14 @@ Info: Werk moontlik nie op regstreekse uitsendings nie" Tou word behou na die herbegin van die app Ruil die Spaar en Waglys-aksies Tik vir tou-opsies, tik en hou om na Kyk later te stoor - Vervang voedingsvloei-wyser-knoppie - Oop die Morphe-ry vir die YouTube-ry + + Speel volgende in wagtelys tipe + Verstek + Inheemse wagtelys + Morphe wagtelys + Morphe wagtelys kliënt sykant Voeg voer flyout-tou-menu-item by Voeg \'n bykomende flyout-item by. Skakel dit aan as \"Voeg by tou\" nie gewys word nie - Premium-styl-tou - "Vervang die Morphe-tou met een wat op die toestel bly, vanself deurspeel, en deur te sleep herrangskik kan word - -Inligting: Die volgende video in die tou begin eers wanneer YouTube op die skerm is" Tou Waglys (%s) Wis alles @@ -640,6 +650,20 @@ Inligting: Die volgende video in die tou begin eers wanneer YouTube op die skerm Verwyder en herlaai video Maak waglys oop Stoor waglys na spyskaart + + Afspellys gestoor + Gestoor na %s + Maak wagtelys skoon + Wagtelys skoongemaak + Wagtelys + Speel laaste in wagtelys + Speel volgende in wagtelys + YouTube kon nie die wagtelys vertoon nie. + Die wagtelys is nie gereed vir hierdie video nie. Maak die videospeler oop en probeer weer. + Voeg Mengsel by wagtelys of kyk Mengsel? + Kyk + Voeg by wagtelys + Besig om Mengsel te laai... Open kanaal van regstreekse avatar Voorkom dat die regstreekse uitsending oopmaak wanneer jy op die kanaal se avatar tik @@ -853,8 +877,11 @@ Om Instellings toe te gaan, skakel die Instelling-knoppie in via Algemeen → We Makes the status bar fully opaque Deaktiveer outomatiese verberg van navigasiebalk Makes the navigation bar fully opaque in light mode - Deaktiveer semi-deursigtige navigasiebalk remercie - Maak die navigasie- en statusbalk volledig opaak + Navigasiebalkstyl + Verstek + Deurskynend + Deursigtig + Opaak Wenkbalk Kies watter komponente in die lêertooskassie vertoon moet word Hide \'Audio Track\' menu footer diff --git a/patches/src/main/resources/addresources/values-am-rET/youtube/strings.xml b/patches/src/main/resources/addresources/values-am-rET/youtube/strings.xml index 174f25cd1a..1a02f7efd7 100644 --- a/patches/src/main/resources/addresources/values-am-rET/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-am-rET/youtube/strings.xml @@ -76,7 +76,9 @@ Second \"item\" text" + + diff --git a/patches/src/main/resources/addresources/values-ar-rSA/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-ar-rSA/shared-youtube/strings.xml index 8146915226..943170dda0 100644 --- a/patches/src/main/resources/addresources/values-ar-rSA/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ar-rSA/shared-youtube/strings.xml @@ -151,6 +151,13 @@ Second \"item\" text" يخفي أزرار \'السابق\' و \'التالي\' في إشعار الوسائط، شاشة القفل، وعناصر تحكم سماعة الرأس تعطيل شريط التمرير يمنع سحب شريط التقدم على شاشة القفل ولوحة الإشعارات + التمرير بأزرار السابق & التالي + يغير عناصر التحكم لإشعار الوسائط وشاشة القفل والرأسية للتقديم بدلاً من تغيير مقاطع الفيديو + طول التراجع للخلف + عدد الثواني للترجيع باستخدام زر السابق + مدة التقديم السريع + عدد الثواني للتقديم السريع باستخدام زر التالي + يجب أن تكون مدة التمرير بين 1 و %s ثانية. سيتم استخدام الإعداد الافتراضي تعطيل بروتوكول QUIC diff --git a/patches/src/main/resources/addresources/values-ar-rSA/youtube/strings.xml b/patches/src/main/resources/addresources/values-ar-rSA/youtube/strings.xml index 5190fcf21f..6a62eae810 100644 --- a/patches/src/main/resources/addresources/values-ar-rSA/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ar-rSA/youtube/strings.xml @@ -473,6 +473,16 @@ Second \"item\" text" تنزيل الفيديوهات، البودكاست، أو الأغاني باستخدام تطبيقات تنزيل خارجية عرض زر تنزيل بواجهة المشغل يعرض زرًا في مشغل الفيديو يفتح أداة التنزيل الخارجية الخاصة بك + تنزيل سريع عند النقر + تنبيهات للتنزيل في الخلفية. اضغط مع الاستمرار لفتح أداة التنزيل. يعمل مع YTDLnis و YTindir + تحديد التنزيلات كـ تمت مشاهدتها + تتم إضافة الفيديوهات التي تم تنزيلها باستخدام أداة تنزيل خارجية إلى سجل مشاهدات YouTube الخاص بك + Full HD (1080p) + متوسط (192 kbps) + التنزيل السريع + الفيديو + الصوت + كل الخيارات الترجمة الصوتية قراءة نصوص ترجمة الفيديو بصوت عالٍ باستخدام تحويل النص إلى كلام على الجهاز، بالتزامن مع التشغيل @@ -576,7 +586,7 @@ Mistral Nemo هو الخيار الأرخص، بينما توفر Gemma 4 أعل معرف الفيديو غير متاح. يرجى إعادة تحميل الفيديو يدويًا عرض زر الحفظ في المشاهدة لاحقًا - انقر للحفظ في المشاهدة لاحقًا، واضغط مع الاستمرار لخيارات قائمة الانتظار + انقر للحفظ في المشاهدة لاحقًا، واضغط مع الاستمرار لخيارات قائمة المحتوى التالي تم حفظ الفيديو بنجاح في قائمة المشاهدة لاحقًا هذا الفيديو محفوظ بالفعل في قائمة المشاهدة لاحقًا لا يمكن حفظ هذا الفيديو في قائمة المشاهدة لاحقًا @@ -587,33 +597,33 @@ Mistral Nemo هو الخيار الأرخص، بينما توفر Gemma 4 أعل يتيح حفظ فيديوهات Shorts في قائمة \"المشاهدة لاحقًا\" من القائمة المنبثقة عرض زر \"الحفظ للمشاهدة لاحقًا\" في قائمة Shorts المنبثقة يتيح حفظ فيديوهات Shorts في قائمة \"المشاهدة لاحقًا\" من القائمة المنبثقة - إضافة الى قائمة انتظار Morphe - احتفظ بقائمة الانتظار بين الجلسات - تبقى قائمة الانتظار محفوظة بعد إعادة تشغيل التطبيق - تبديل إجراءات حفظ وإضافة إلى قائمة الانتظار - انقر لخيارات قائمة الانتظار، واضغط مع الاستمرار للحفظ في المشاهدة لاحقًا - تجاوز زر قائمة الانتظار في القائمة المنبثقة - يفتح قائمة انتظار Morphe بدلًا من قائمة انتظار YouTube - إضافة خيار قائمة الانتظار إلى قائمة الخلاصة المنبثقة - يضيف خيارًا إضافيًا إلى القائمة المنبثقة. فعّل هذا الخيار إذا لم تظهر \"إضافة إلى قائمة الانتظار\" - قائمة انتظار بأسلوب Premium - "يستبدل قائمة انتظار Morphe بقائمة موجودة على الجهاز، وتُشغَّل من تلقاء نفسها، ويمكن إعادة ترتيبها بالسحب - -ملاحظة: لا يبدأ الفيديو التالي في قائمة الانتظار إلا عندما يكون YouTube معروضًا على الشاشة" - قائمة الانتظار - قائمة الانتظار (%s) + إضافة الى قائمة المحتوى التالي الخاصة بـ Morphe + احتفظ بقائمة المحتوى التالي بين الجلسات + تبقى قائمة المحتوى التالي محفوظة بعد إعادة تشغيل التطبيق + تبديل إجراءات حفظ وإضافة إلى قائمة المحتوى التالي + انقر لخيارات قائمة المحتوى التالي، واضغط مع الاستمرار للحفظ في المشاهدة لاحقًا + + نوع تشغيل التالي في قائمة المحتوى التالي + الافتراضي + قائمة المحتوى التالي الأصلية + قائمة المحتوى التالي الخاصة بـ Morphe + قائمة المحتوى التالي الخاصة بـ Morphe جانب العميل + إضافة خيار قائمة المحتوى التالي إلى قائمة الخلاصة المنبثقة + يضيف خيارًا إضافيًا إلى القائمة المنبثقة. فعّل هذا الخيار إذا لم تظهر \"إضافة إلى قائمة المحتوى التالي\" + قائمة المحتوى التالي + قائمة المحتوى التالي (%s) مسح الكل - لا يوجد شيء في قائمة الانتظار - استخدم خيار تشغيل التالي في قائمة الانتظار على أي فيديو + لا يوجد شيء في قائمة المحتوى التالي + استخدام خيار تشغيل الفيديو التالي في قائمة المحتوى التالي على أي فيديو قيد التشغيل الآن - تشغيل التالي في قائمة الانتظار - تشغيل الأخير في القائمة + تشغيل الفيديو التالي في قائمة المحتوى التالي + تشغيل آخر فيديو في قائمة المحتوى التالي إعادة ترتيب إزالة - تمت الإضافة إلى قائمة الانتظار - تعذر إضافة هذا الفيديو إلى قائمة الانتظار - لا يمكن إضافة قوائم التشغيل إلى قائمة الانتظار - قائمة الانتظار ممتلئة + تمت الإضافة إلى قائمة المحتوى التالي + تعذر إضافة هذا الفيديو إلى قائمة المحتوى التالي + لا يمكن إضافة قوائم التشغيل إلى قائمة المحتوى التالي + قائمة المحتوى التالي ممتلئة تعذر تشغيل الفيديو القائمة المسموح بها للإعلانات في القائمة المنبثقة يضيف عنصرًا في القائمة المنبثقة يتيح تشغيل الإعلانات على قناة ذلك الفيديو @@ -623,27 +633,41 @@ Mistral Nemo هو الخيار الأرخص، بينما توفر Gemma 4 أعل يضيف عنصرًا في القائمة المنبثقة يُشغّل قناة ذلك الفيديو دائمًا بسرعة 1.0x تسجيل الدخول غير ممكن لا يوجد معرّف قائمة تشغيل - قائمة الانتظار فارغة + قائمة المحتوى التالي فارغة لا يوجد معرّف فيديو فشل: %s - فشل الإضافة إلى قائمة الانتظار - فشل إنشاء قائمة الانتظار - فشل الإزالة من قائمة الانتظار - فشل حفظ قائمة الانتظار - تمت الإضافة إلى قائمة الانتظار - تم إنشاء قائمة الانتظار - تمت الإزالة من قائمة الانتظار - تم حفظ قائمة الانتظار في %s - قائمة تشغيل Morphe - إضافة إلى قائمة الانتظار - إضافة إلى قائمة الانتظار وفتح - إضافة إلى قائمة الانتظار وتشغيل + فشل الإضافة إلى قائمة المحتوى التالي + فشل إنشاء قائمة المحتوى التالي + فشل الإزالة من قائمة المحتوى التالي + فشل حفظ قائمة المحتوى التالي + تمت الإضافة إلى قائمة المحتوى التالي + تم إنشاء قائمة المحتوى التالي + تمت الإزالة من قائمة المحتوى التالي + تم حفظ قائمة المحتوى التالي في %s + قائمة المحتوى التالي الخاصة بـ Morphe + إضافة إلى قائمة المحتوى التالي + إضافة إلى قائمة المحتوى التالي وفتح + إضافة إلى قائمة المحتوى التالي وتشغيل إضافة وإعادة تحميل الفيديو - إزالة من قائمة الانتظار - إزالة وفتح قائمة الانتظار + إزالة من قائمة المحتوى التالي + إزالة وفتح قائمة المحتوى التالي إزالة وإعادة تحميل الفيديو - فتح قائمة الانتظار - حفظ قائمة الانتظار إلى قائمة تشغيل + فتح قائمة المحتوى التالي + حفظ قائمة المحتوى التالي إلى قائمة تشغيل + + تم حفظ قائمة التشغيل + تم الحفظ إلى %s + مسح قائمة المحتوى التالي + تم مسح قائمة المحتوى التالي + قائمة المحتوى التالي + تشغيل آخر فيديو في قائمة المحتوى التالي + تشغيل الفيديو التالي في قائمة المحتوى التالي + لم يتمكن YouTube من عرض قائمة المحتوى التالي. + قائمة المحتوى التالي غير جاهزة لهذا الفيديو. افتح مشغل الفيديو وحاول مرة أخرى. + إضافة التشكيلة إلى قائمة المحتوى التالي أو مشاهدة التشكيلة؟ + مشاهدة + إضافة إلى قائمة المحتوى التالي + تحميل التشكيلة... فتح القناة عبر الصورة الرمزية المباشرة يمنع فتح البث المباشر عند النقر على صورة القناة الرمزية @@ -857,8 +881,11 @@ Mistral Nemo هو الخيار الأرخص، بينما توفر Gemma 4 أعل يُفعّل الانتقالات المتحركة عند التبديل بين علامات التبويب تعطيل إخفاء شريط التنقل تلقائيًا يبقي شريط التنقل ظاهرًا أثناء التمرير - تعطيل شريط التنقل الشفاف - يجعل شريط التنقل وشريط الحالة معتمين تمامًا + نمط شريط التنقل + الافتراضي + شبه شفاف + شفاف + معتم شريط الأدوات اختر المكونات التي تظهر في شريط الأدوات إخفاء زر البث diff --git a/patches/src/main/resources/addresources/values-as-rIN/youtube/strings.xml b/patches/src/main/resources/addresources/values-as-rIN/youtube/strings.xml index 174f25cd1a..1a02f7efd7 100644 --- a/patches/src/main/resources/addresources/values-as-rIN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-as-rIN/youtube/strings.xml @@ -76,7 +76,9 @@ Second \"item\" text" + + diff --git a/patches/src/main/resources/addresources/values-az-rAZ/youtube/strings.xml b/patches/src/main/resources/addresources/values-az-rAZ/youtube/strings.xml index 94a264d76d..2a1e6aa13e 100644 --- a/patches/src/main/resources/addresources/values-az-rAZ/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-az-rAZ/youtube/strings.xml @@ -473,6 +473,7 @@ Məhdudiyyətlər: Üçüncü tərəf yükləmə tətbiqləri ilə videoları, podkastları və ya mahnıları yüklə Oynadıcı örtüyü yükləmə düyməsini göstər Video oynadıcıda xarici yükləyicinizi açan düyməni göstərir + Video Səs üstələyici tərcümə Cihazdakı mətni nitqə çevirməni istifadə edərək video titrlərini oynatma ilə sinxronlaşdırıb ucadan oxu @@ -588,14 +589,10 @@ Məlumat: Canlı yayımlarda işləməyə bilər" Tətbiq yenidən başladıldıqdan sonra növbə qorunur Saxla və Növbə fəaliyyətlərini dəyişdir Növbə seçimləri üçün toxun, sonra Baxmağa saxlamaq üçün toxun və saxla - Axında açılan növbə düyməsini əvəzlə - YouTube növbəsi əvəzinə Morphe növbəsini açır + + Varsayılan Axına açılan növbə menyusu elementini əlavə et Əlavə açılan elementi əlavə edir. \"Növbəyə əlavə et\" görünmürsə, bunu aktivləşdir - Premium üslubu növbəsi - "Morphe növbəsini cihazda olan, ardıcıl oynadılan və sürüşdürərək sıralana bilən növbə ilə əvəzləyir -​ -Məlumat: Növbədəki növbəti video yalnız YouTube ekranda olduqda başlayır" Növbə Növbə (%s) Hamısını təmizlə @@ -640,6 +637,11 @@ Məlumat: Növbədəki növbəti video yalnız YouTube ekranda olduqda başlayı Videonu sil və yenidən yüklə Növbəni aç Növbəni pleylistdə saxla + + Növbə + Növbədə sonuncu oynat + Növbədə növbətidə oynat + Növbəyə əlavə et Canlı profildən kanalı aç Kanalın profilinə toxunduqda canlı yayımın açılmasın əngəlləyir @@ -853,8 +855,11 @@ Tənzimləmələrə daxil olmaq üçün Ümumi → Alət Cərgəsi→ Tənzimlə Tabellər arasında keçərkən keçidləri canlandırır Fəaliyyət cərgəsin avtomatik gizlətməsini qapat Sürüşdürərkən fəaliyyət zolağın görünən saxlayır - Yarımşəffaf fəaliyyət cərgəsin qapat - Fəaliyyət və status cərgəsini tamamilə qeyri-şəffaf edir + Fəaliyyət cərgəsi üslubu + İlkin + Yarımşəffaf + Şəffaf + Tutqun Alət cərgəsi Alətlər zolağında hansı elementlər görünür seç Yayımla düyməsini gizlət diff --git a/patches/src/main/resources/addresources/values-be-rBY/youtube/strings.xml b/patches/src/main/resources/addresources/values-be-rBY/youtube/strings.xml index d817afcf08..621b482897 100644 --- a/patches/src/main/resources/addresources/values-be-rBY/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-be-rBY/youtube/strings.xml @@ -79,7 +79,10 @@ Second \"item\" text" + + Звычайны + @@ -122,6 +125,7 @@ Second \"item\" text" + Звычайны diff --git a/patches/src/main/resources/addresources/values-bg-rBG/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-bg-rBG/shared-youtube/strings.xml index 462eafdba7..b5a0c56026 100644 --- a/patches/src/main/resources/addresources/values-bg-rBG/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-bg-rBG/shared-youtube/strings.xml @@ -143,6 +143,13 @@ Second \"item\" text" Скрива бутоните „Предишна“ и „Следваща“ в известията за медии, на заключения екран и в контролите на слушалките Деактивиране на лентата за превъртане Предотвратява плъзгането на лентата за превъртане на заключения екран и в панела за известия + Търсене с бутони \"Предишен\" и \"Следващ\" + Променя управлението на нотификацията за медиите, заключения екран и слушалките да търси вместо да смня видеоклипове + Дължина на превъртане назад + Секунди за превъртане назад с бутона \"Предишен\" + Дължина на превъртане напред + Секунди за превъртане напред с бутона \"Следващ\" + Дължината на търсене трябва да е между 1 и %s секунди. Използва се стойност по подразбиране Изключи протокола QUIC diff --git a/patches/src/main/resources/addresources/values-bg-rBG/youtube/strings.xml b/patches/src/main/resources/addresources/values-bg-rBG/youtube/strings.xml index 41cb359be1..09f7314584 100644 --- a/patches/src/main/resources/addresources/values-bg-rBG/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-bg-rBG/youtube/strings.xml @@ -473,6 +473,16 @@ Second \"item\" text" Изтегляйте видеа, подкасти или песни чрез външни приложения за изтегляне Показване на бутона за изтегляне в припокриването на плеъра Показва бутон във видео плеъра, който отваря външния ви изтегляч + Бързо изтегляне с докосване + Подкани за изтегляне във фонов режим. Докоснете и задръжте, за да отворите програмата за изтегляне. Работи с YTDLnis и YTindir + Маркиране на изтеглянията като гледани + Видеата, изтеглени с външен инструмент за изтегляне, се добавят към вашия историк на гледане в YouTube + Full HD (1080p) + Средно (192 kbps) + Бързо изтегляне + Видео + Аудио + Всички опции Превод на глас зад кадър Четете надписите на видеото на глас, използвайки вграден текстов говор, синхронизиран с възпроизвеждането @@ -588,14 +598,14 @@ Second \"item\" text" Опашката се запазва след рестартиране на приложението Размяна на действията Запази и Опашка Докоснете за опциите на опашката, докоснете и задръжте, за да запазите в „Гледай по-късно“ - Бутон за заместване на опашката на канала - Отваря опашката на Morphe вместо опашката на YouTube + + Пускане на следващо в опашката + По подразбиране + Родна опашка + Morphe опашка + Morphe опашка от страна на клиента Добавяне на елемент от менюто за излитащ прозорец на емисията Добавя допълнителен елемент в излитащия прозорец. Активирайте това, ако „Добавяне към опашката“ не се показва - Опашка в стил Premium - "Заменя опашката на Morphe с такава, която се съхранява на устройството, възпроизвежда се самостоятелно и може да бъде пренареждана чрез плъзгане - -Информация: Следващото видео от опашката стартира само когато YouTube е на екрана" Опашка Чакаща опашка (%s) Изчисти всичко @@ -640,6 +650,20 @@ Second \"item\" text" Премахване от опашката и презареждане на видеото Отваряне на опашката Запазване на опашката в плейлист + + Плейлистът е запазен + Запазено в %s + Изчистване на опашката + Опашката е изчистена + Опашка + Пускане на последното в опашката + Пускане на следващото в опашката + YouTube не може да покаже опашката. + Опашката не е готова за това видео. Отворете видео плейъра и опитайте отново. + Добавяне на микс към опашката или гледане на микса? + Гледане + Добавяне към опашката + Зареждане на микс… Отворете канал от аватара на живо Предотвратява отварянето на излъчването на живо при докосване на аватара на канала @@ -853,8 +877,11 @@ Second \"item\" text" Анимира преходите при превключване между табовете Забрани автоматичното скриване на навигационната лента Поддържа навигационната лента видима при превъртане - Деактивиране на полупрозрачния навигационен панел - Прави навигационния панел и лентата за статус напълно непрозрачни + Стил на навигационната лента + По подразбиране + Полупрозрачен + Прозрачен + Непрозрачен Тойбър Изберете кои компоненти да се показват в лентата с инструменти Бутон за предаване на Тв diff --git a/patches/src/main/resources/addresources/values-bn-rBD/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-bn-rBD/shared-youtube/strings.xml index 8e65d5d0a4..0443f9a4b4 100644 --- a/patches/src/main/resources/addresources/values-bn-rBD/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-bn-rBD/shared-youtube/strings.xml @@ -151,6 +151,13 @@ Second \"item\" text" মিডিয়া বিজ্ঞপ্তিতে, লক স্ক্রিনে এবং হেডফোন নিয়ন্ত্রণে পূর্ববর্তী ও পরবর্তী বোতাম লুকিয়ে রাখে সিকবার নিষ্ক্রিয় করুন লক স্ক্রিনে এবং বিজ্ঞপ্তি প্যানেলে সিকবার টেনে সরানো প্রতিরোধ করে + পূর্ববর্তী ও পরবর্তী বোতাম দিয়ে সিক করুন + ভিডিও পরিবর্তনের পরিবর্তে মিডিয়া নোটিফিকেশন, লক স্ক্রিন এবং হেডফোনে সিক করার জন্য নিয়ন্ত্রণ পরিবর্তন করুন + রিওয়ান্ড দৈর্ঘ্য + পূর্ববর্তী বোতাম দিয়ে রিওয়ান্ড করার জন্য সেকেন্ড সংখ্যা + ফাস্ট ফরওয়ার্ড দৈর্ঘ্য + পরবর্তী বোতাম দিয়ে ফাস্ট ফরওয়ার্ড করার জন্য সেকেন্ড সংখ্যা + সিক দৈর্ঘ্য 1 থেকে %s সেকেন্ডের মধ্যে হতে হবে। ডিফল্ট ব্যবহার করা হচ্ছে কিউআইসি প্রোটোকল বন্ধ করুন diff --git a/patches/src/main/resources/addresources/values-bn-rBD/youtube/strings.xml b/patches/src/main/resources/addresources/values-bn-rBD/youtube/strings.xml index 39c532313b..2ec7930c89 100644 --- a/patches/src/main/resources/addresources/values-bn-rBD/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-bn-rBD/youtube/strings.xml @@ -473,6 +473,16 @@ Second \"item\" text" তৃতীয় পক্ষের ডাউনলোড অ্যাপ্লিকেশন ব্যবহার করে ভিডিও, পডকাস্ট বা গান ডাউনলোড করুন প্লেয়ার ওভারলে ডাউনলোড বোতাম দেখান ভিডিও প্লেয়ারে একটি বোতাম দেখায় যা আপনার বাহ্যিক ডাউনলোডার খole + ট্যাপ করলে দ্রুত ডাউনলোড করুন + ব্যাকগ্রাউন্ডে ডাউনলোড করার প্রম্পট দেয়। ডাউনলোডার খুলতে ট্যাপ করে ধরে রাখুন। YTDLnis এবং YTindir-এর সাথে কাজ করে + ডাউনলোডগুলিকে দেখা হয়েছে হিসেবে চিহ্নিত করুন + এক্সটারনাল ডাউনলোডার দিয়ে ডাউনলোড করা ভিডিওগুলি আপনার YouTube ওয়াচ হিস্ট্রিতে যোগ করা হবে + ফুল এইচডি (1080p) + মাধ্যম (192 kbps) + দ্রুত ডাউনলোড + ভিডিও + অডিও + সকল বিকল্প ভয়েস ওভার অনুবাদ ডিভাইসের টেক্সট-টু-স্পিচ ব্যবহার করে প্লেব্যাকের সাথে মিলিয়ে ভিডিও ক্যাপশন পড়ে শোনান @@ -588,14 +598,14 @@ OpenRouter.ai ওপেন করতে এখানে ট্যাপ কর অ্যাপ পুনরায় চালু করার পরেও সারিটি রাখা হবে সেভ এবং কিউ অ্যাকশনগুলির স্থান পরিবর্তন করুন কিউ অপশনের জন্য ট্যাপ করুন, ওয়াচ লেটারে সংরক্ষণ করতে ট্যাপ করে ধরে রাখুন - ফিড ফ্লাইআউট সারিবদ্ধকরণ বোতামটি প্রতিস্থাপন করুন - YouTube সারির পরিবর্তে Morphe সারি খোলে + + অন্তর্ভুক্ত তালিকায় পরবর্তীটি চালান + ডিফল্ট + নেটিভ কিউ + Morphe কিউ + Morphe ক্লায়েন্ট সайд কিউ ফিড ফ্লাইআউট কিউ মেনু আইটেম যুক্ত করুন একটি অতিরিক্ত ফ্লাইআউট আইটেম যোগ করে। যদি \"কিউতে যোগ করুন\" দেখানো না হয় তবে এটি সক্ষম করুন। - প্রিমিয়াম-স্টাইলের সারি - "Morphe সারির বদলে ডিভাইসে থাকা একটি সারি ব্যবহার করে, যা নিজে নিজে চলতে থাকে এবং টেনে পুনর্বিন্যাস করা যায় - -তথ্য: পরবর্তী সারিবদ্ধ ভিডিওটি শুধুমাত্র YouTube স্ক্রিনে থাকাকালীন শুরু হয়" সারি সারি (%s) সব মুছুন @@ -640,6 +650,20 @@ OpenRouter.ai ওপেন করতে এখানে ট্যাপ কর queue থেকে সরান এবং ভিডিও রিফ্রেশ করুন queue খুলুন প্লেলিস্টে queue সংরক্ষণ করুন + + প্লেলিস্ট সংরক্ষিত হয়েছে + %s এ সংরক্ষিত হয়েছে + কিউ মুছুন + কিউ পরিষ্কার করা হয়েছে + কিউ + কিউ-তে শেষটি চালান + কিউ-তে পরবর্তীটি চালান + YouTube কিউ প্রদর্শন করতে পারেননি। + এই ভিডিওর জন্য কিউ তৈরি নয়। ভিডিও প্লেয়ার খুলুন এবং আবার চেষ্টা করুন। + মিক্সকে কিউতে যোগ করবেন নাকি মিক্স দেখবেন? + দেখুন + ভিডিও তালিকায় যুক্ত করুন + মিক্স লোড হচ্ছে… লাইভ অবতার থেকে চ্যানেল খুলুন চ্যানেলের অবতার ট্যাপ করার সময় লাইভ স্ট্রিম খুলতে বাধা দেয়। @@ -853,8 +877,11 @@ OpenRouter.ai ওপেন করতে এখানে ট্যাপ কর ট্যাবের মধ্যে পরিবর্তন করার সময় অ্যানিমেশন তৈরি করে স্বয়ংক্রিয়ভাবে নেভিগেশন বার লুকানো নিষ্ক্রিয় করুন স্ক্রোল করার সময় নেভিগেশন বারটি দৃশ্যমান রাখে - Disable translucent navigation bar - Makes the navigation and status bar fully opaque + নেভিগেশন বার স্টাইল + ডিফল্ট + অর্ধস্বচ্ছ + স্বচ্ছ + অস্বচ্ছ টুলবার টুলবারে কোন উপাদানগুলো প্রদর্শিত হবে তা নির্বাচন করুন কাস্ট বোতামটি লুকান diff --git a/patches/src/main/resources/addresources/values-bs-rBA/youtube/strings.xml b/patches/src/main/resources/addresources/values-bs-rBA/youtube/strings.xml index 6d1f52e7da..daf4d15f6b 100644 --- a/patches/src/main/resources/addresources/values-bs-rBA/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-bs-rBA/youtube/strings.xml @@ -82,7 +82,10 @@ Second \"item\" text" + + The + @@ -123,6 +126,7 @@ Second \"item\" text" + The diff --git a/patches/src/main/resources/addresources/values-ca-rES/youtube/strings.xml b/patches/src/main/resources/addresources/values-ca-rES/youtube/strings.xml index 865396451f..a748e2213a 100644 --- a/patches/src/main/resources/addresources/values-ca-rES/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ca-rES/youtube/strings.xml @@ -84,6 +84,7 @@ Second \"item\" text" + Vídeo @@ -92,8 +93,10 @@ Second \"item\" text" + Eliminar + diff --git a/patches/src/main/resources/addresources/values-ckb-rIR/youtube/strings.xml b/patches/src/main/resources/addresources/values-ckb-rIR/youtube/strings.xml index bfb444b40b..cb6a438fd2 100644 --- a/patches/src/main/resources/addresources/values-ckb-rIR/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ckb-rIR/youtube/strings.xml @@ -168,6 +168,7 @@ Second \"item\" text" + ڤیدێۆ باو @@ -184,8 +185,11 @@ Second \"item\" text" + + بنەڕەت لابردن + @@ -283,6 +287,7 @@ Second \"item\" text" شاردنەوەی پەڕەکانی گەشتکردن جوڵە پێکهاتەکانی دەستنیشانکردن بە پەیوەست بێن ئەنیمەیشنی پەڕەکانی گەشتکردن چالاک بکە + بنەڕەت بەڵتە دوگمەی پەخشکردن بشارەوە diff --git a/patches/src/main/resources/addresources/values-cs-rCZ/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-cs-rCZ/shared-youtube/strings.xml index fafcf3fed6..4c42dbae98 100644 --- a/patches/src/main/resources/addresources/values-cs-rCZ/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-cs-rCZ/shared-youtube/strings.xml @@ -151,6 +151,13 @@ Pro přihlášení stiskněte pokračovat níže" Skrývá tlačítka Předchozí a Další v oznámení o médiích, na zamykací obrazovce a v ovládacích prvcích sluchátek. Zakázat posuvník Zabrání přetahování posuvníku na zamykací obrazovce a v panelu oznámení + Hledání s předchozími a následujícími tlačítky + Změna ovládání pro notifikaci médií, zamykací obrazovku a sluchátka pro hledání místo přepínání videí + Délka rychlého posunu zpět + Sekundy pro rychlý posun zpět pomocí tlačítka Předchozí + Délka rychlého posunu vpřed + Sekundy pro rychlý posun vpřed pomocí tlačítka Další + Délka hledání musí být mezi 1 a %s sekundami. Používá se výchozí hodnota Vypnout protokol QUIC diff --git a/patches/src/main/resources/addresources/values-cs-rCZ/youtube/strings.xml b/patches/src/main/resources/addresources/values-cs-rCZ/youtube/strings.xml index a4c34fd1de..88f113fada 100644 --- a/patches/src/main/resources/addresources/values-cs-rCZ/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-cs-rCZ/youtube/strings.xml @@ -473,6 +473,16 @@ Po jednom otevření videa s tlačítkem Ask funguje funkce se všemi videi, i k Stahujte videa, podcasty nebo písně pomocí aplikací pro stahování třetích stran Zobrazí tlačítko pro stahování v přehrávači playeru Zobrazí v přehrávači videí tlačítko, které otevře váš externí downloader + Rychlé stahování klepnutím + Nabídne stahování na pozadí. Stiskněte a podržte pro otevření stahovače. Funguje s YTDLnis a YTindir + Označit stažené soubory jako zhlídnuté + Videa stažená externím stahovačem jsou přidána do vaší historie zhlídnutí na YouTube + Full HD (1080p) + Střední (192 kbps) + Rychlé stahování + Video + Zvuk + Všechny možnosti Hlasový překlad Přečte titulky videa nahlas pomocí text-to-speech v zařízení, synchronizovaně s přehráváním @@ -588,14 +598,14 @@ Upozornění: Možná nebude fungovat u živých přenosů" Fronta je zachována i po restartu aplikace Prohoďte akce Uložit a Zařadit do fronty Klepněte pro možnosti fronty, klepněte a podržte pro uložení do seznamu Přehrát později - Přepsat frontu flyout nabídky - Otevře frontu Morphe místo fronty YouTube + + Typ dalšího položky ve frontě + Výchozí + Nativní fronta + Fronta Morphe + Strana klienta fronty Morphe Přidat položku nabídky pro frontu rozbalovací nabídky feedu Přidává další položku rozbalovací nabídky. Povolte to, pokud se nezobrazuje položka \"Přidat do fronty\" - Fronta ve stylu Premium - "Nahradí frontu Morphe frontou, která je uložená v zařízení, přehrává se sama a lze ji přetahováním změnit pořadí - -Info: Další video ve frontě se spustí pouze tehdy, když je YouTube na obrazovce" Fronta Fronta (%s) Vymazat vše @@ -640,6 +650,20 @@ Info: Další video ve frontě se spustí pouze tehdy, když je YouTube na obraz Odebrat a znovu načíst video Otevřít frontu Uložit frontu do playlistu + + Playlist uložen + Uloženo do %s + Vymazat frontu + Fronta vymazána + Fronta + Přehrát jako poslední ve frontě + Přehrát další ve frontě + YouTube nemůže zobrazit frontu. + Fronta není pro toto video připravena. Otevřete přehrávač videí a zkuste to znovu. + Přidat Mix do fronty nebo sledovat Mix? + Sledovat + Přidat do fronty + Načítání Mixu… Otevření kanálu z živé avataru Zabraňuje otevření živého přenosu klepnutím na avatar kanálu @@ -851,8 +875,11 @@ Pro přístup k Nastavení povolte tlačítko Nastavení přes Obecné → Nást Animuje přechody při přepínání mezi kartami Vypnout automatické skrývání navigační lišty Ponechá navigační lištu viditelnou i při posouvání - Deaktivovat průsvitný navigační panel - Nastaví navigační panel a stavový řádek jako plně neprůhledné + Styl navigačního panelu + Výchozí + Poloprůhledný + Průhledný + Neprůhledný Nástrojová lišta Vyberte, které prvky se zobrazí v nástrojové liště Skrýt tlačítko odesílání diff --git a/patches/src/main/resources/addresources/values-da-rDK/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-da-rDK/shared-youtube/strings.xml index e21eec3848..0434c5d711 100644 --- a/patches/src/main/resources/addresources/values-da-rDK/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-da-rDK/shared-youtube/strings.xml @@ -151,6 +151,13 @@ For at logge ind, tryk på fortsæt nedenfor" Skjuler Forrige- og Næste-knapperne i medienotifikationen, på låseskærmen og i hovedtelefonkontrollerne Deaktiver søgebjælken Forhindrer, at søgebjælken kan trækkes på låseskærmen og i meddelelsespanelet + Søg med knapperne & Forrige & Næste + Ændrer kontroller for medie-meddelelse, låseskærm og hovedtelefoner til at søge i stedet for at ændre videoer + Længde for tilbagetspuling + Sekunder til at spole tilbage med knappen Forrige + Længde for fremspringning + Sekunder til at spole fremad med knappen Næste + Søgelængden skal være mellem 1 og %s sekunder. Bruger standardværdi Deaktiver QUIC-protokol diff --git a/patches/src/main/resources/addresources/values-da-rDK/youtube/strings.xml b/patches/src/main/resources/addresources/values-da-rDK/youtube/strings.xml index 5b5e0e34f2..199485e7d4 100644 --- a/patches/src/main/resources/addresources/values-da-rDK/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-da-rDK/youtube/strings.xml @@ -471,6 +471,16 @@ Efter at en video med Ask-knappen er åbnet én gang, fungerer funktionen med al Download videoer, podcasts eller sange med tredjeparts download-apps Vis knappen til download af spilleroverlejring Viser en knap i videoafspilleren, der åbner din eksterne downloadprogram + Hurtig download ved tryk + Opfordrer til download i baggrunden. Tryk og hold for at åbne downloaderen. Virker med YTDLnis og YTindir + Marker downloads som set + Videoer downloadet med en ekstern downloader tilføjes til din YouTube-historik + Full HD (1080p) + Medium (192 kbps) + Hurtig download + Video + Lyd + Alle valgmuligheder Tekstlæsning Læs videoundertitler højt ved hjælp af tekst-til-tale på enheden, synkroniseret med afspilningen @@ -586,14 +596,14 @@ Info: Virker muligvis ikke på live streams" Køen bevares efter genstart af appen Byt rundt på Gem og Kø-handlinger Tryk for kø-indstillinger, tryk og hold for at gemme til Se senere - Erstat feed flyout kø-knap - Åbner Morphe-køen i stedet for YouTube-køen + + Afspil næste i kø + Standard + Indbygget kø + Morphe-kø + Morphe-klientkø Tilføj feed-flyout-kø-menupunkt Tilføjer et ekstra flyout-element. Aktiver dette, hvis \"Tilføj til kø\" ikke vises. - Kø i premium-stil - "Erstatter Morphe-køen med en, der findes på enheden, spiller automatisk videre, og kan omarrangeres ved at trække - -Info: Den næste video i køen starter kun, mens YouTube er på skærmen" Kø Kø (%s) Ryd alt @@ -638,6 +648,20 @@ Info: Den næste video i køen starter kun, mens YouTube er på skærmen"Fjern og genindlæs video Åbn køen Gem køen til afspilningsliste + + Afsnittet er gemt + Gemt til %s + Ryd kø + Kø ryddet + Kø + Afspil sidste i kø + Afspil næste i kø + YouTube kunne ikke vise køen. + Koen er ikke klar til denne video. Åbn videospilleren, og prøv igen. + Tilføj Mix til kø eller afspil Mix? + Afspil + Tilføj til kø + Indlæser Mix… Åbn kanal fra live-avatar Forhindrer livestreamingen i at åbne, når du trykker på kanalens avatar @@ -849,8 +873,11 @@ For at få adgang til Indstillinger skal du aktivere indstillingsknappen via Gen Animerer overgange, når der skiftes mellem faner Deaktiver automatisk skjuling af navigationslinjen Holder navigationslinjen synlig under rulning - Deaktiver gennemsigtig navigationslinje - Gør navigations- og statuslinjen helt uigennemsigtig + Navigationsbjælkestil + Standard + Gennemsigtig + Transparent + Uigennemsigtig Værktøjslinje Vælg, hvilke komponenter der vises i værktøjslinjen Skjul Cast-knappen diff --git a/patches/src/main/resources/addresources/values-de-rDE/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-de-rDE/shared-youtube/strings.xml index 51cb8b965c..4d98002015 100644 --- a/patches/src/main/resources/addresources/values-de-rDE/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-de-rDE/shared-youtube/strings.xml @@ -151,6 +151,13 @@ Um sich anzumelden, drücken Sie unten auf \"Weiter\"" Blendet Zurück- und Weiter-Schaltflächen in Medienbenachrichtigung, Sperrbildschirm und Kopfhörersteuerung aus. Videofortschrittsbalken deaktivieren Verhindert das Verschieben der Fortschrittsleiste auf dem Sperrbildschirm und im Benachrichtigungsfeld. + Mit &-Tasten suchen + Steuerelemente für Medienbenachrichtigung, Sperrbildschirm und Kopfhörer ändern: Statt Videos zu wechseln, jetzt suchen + Rückspullänge + Sekunden zum Rückspulen mit der Vorherige-Taste + Vorspulänge + Sekunden zum Vorspulen mit der Nächste-Taste + Suchlänge muss zwischen 1 und %s Sekunden liegen. Verwende Standardwert QUIC-Protokoll deaktivieren diff --git a/patches/src/main/resources/addresources/values-de-rDE/youtube/strings.xml b/patches/src/main/resources/addresources/values-de-rDE/youtube/strings.xml index 8a60850d01..59c959c0d4 100644 --- a/patches/src/main/resources/addresources/values-de-rDE/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-de-rDE/youtube/strings.xml @@ -474,6 +474,16 @@ Nachdem ein Video mit der Ask-Schaltfläche einmal geöffnet wurde, funktioniert Videos, Podcasts oder Songs mit externen Download-Apps herunterladen Schaltfläche zum Herunterladen der Player-Overlay anzeigen Zeigt eine Schaltfläche im Videoplayer, die Ihren externen Downloader öffnet + Schneller Download per Fingertipp + Aufforderungen zum Download im Hintergrund. Halten Sie zum Öffnen des Downloaders gedrückt. Funktioniert mit YTDLnis und YTindir + Downloads als gesehen markieren + Mit einem externen Downloader heruntergeladene Videos werden zu Ihrem YouTube-Verlauf hinzugefügt + Full-HD (1080p) + Mittel (192 kbps) + Schnell herunterladen + Video + Audio + Alle Optionen Sprachausgabe-Übersetzung Lies die Videountertitel laut mithilfe der Text-to-Speech-Funktion des Geräts, synchronisiert mit der Wiedergabe. @@ -589,14 +599,14 @@ Info: Funktioniert möglicherweise nicht bei Livestreams" Warteschlange wird nach Neustart der App beibehalten Tausche Speichern- und Warteschlangenaktionen Tippen für Warteschlangenoptionen, tippen und halten, um zu „Später ansehen“ hinzuzufügen. - Überschreiben-Schaltfläche für Feed-Flyout-Warteschlange - Öffnet die Morphe-Warteschlange anstelle der YouTube-Warteschlange + + Als Nächstes in der Warteschlange spielen + Standard + Eingebaute Warteschlange + Morphe-Warteschlange + Clientseitige Morphe-Warteschlange Menüpunkt für Flyout-Warteschlange im Feed hinzufügen Fügt einen zusätzlichen Flyout-Eintrag hinzu. Aktivieren Sie diese Option, wenn \"Zur Warteschlange hinzufügen\" nicht angezeigt wird - Warteschlange im Premium-Stil - "Ersetzt die Morphe-Warteschlange durch eine, die auf dem Gerät gespeichert ist, automatisch weiter abspielt und durch Ziehen neu angeordnet werden kann. - -Info: Das nächste Video in der Warteschlange startet nur, solange YouTube auf dem Bildschirm angezeigt wird" Warteschlange Warteschlange (%s) Alle löschen @@ -641,6 +651,20 @@ Info: Das nächste Video in der Warteschlange startet nur, solange YouTube auf d Aus der Warteschlange entfernen und Video neu laden Warteschlange öffnen Warteschlange in Wiedergabeliste speichern + + Wiedergabeliste gespeichert + In %s gespeichert + Warteschlange leeren + Warteschlange gelöscht + Warteschlange + Als Letztes in der Warteschlange spielen + Als Nächstes in der Warteschlange spielen + YouTube kann die Warteschlange nicht anzeigen. + Die Warteschlange ist für dieses Video nicht bereit. Öffnen Sie den Videoplayer und versuchen Sie es erneut. + Mix zur Warteschlange hinzufügen oder Mix ansehen? + Ansehen + Zur Warteschlange hinzufügen + Mix wird geladen… Kanal vom Live-Avatar aus öffnen Verhindert, dass der Livestream geöffnet wird, wenn Sie auf das Avatar des Kanals tippen @@ -854,8 +878,11 @@ Um auf Einstellungen zuzugreifen, aktivieren Sie die Schaltfläche „Einstellun Audio Track-Menü-Fußzeile ausblenden Auto-Ausblenden der Navigationsleiste deaktivieren Audio Track-Menü-Header ausblenden - Transluzente Navigationsleiste deaktivieren - Macht die Navigations- und Statusleiste vollständig undurchsichtig + Stil der Navigationsleiste + Standard + Transluzent + Transparent + Undurchsichtig Leiste Wähle aus, welche Schaltflächen in der Toolbar erscheinen sollen Schaltfläche „Übertragen“ ausblenden diff --git a/patches/src/main/resources/addresources/values-el-rGR/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-el-rGR/shared-youtube/strings.xml index ba646f2620..c53274837d 100644 --- a/patches/src/main/resources/addresources/values-el-rGR/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-el-rGR/shared-youtube/strings.xml @@ -151,6 +151,13 @@ Second \"item\" text" Αποκρύπτει τα κουμπιά Προηγούμενο και Επόμενο στην ειδοποίηση πολυμέσων, στην οθόνη κλειδώματος και στα χειριστήρια ακουστικών Απενεργοποίηση γραμμής αναζήτησης Αποτρέπει τη μεταφορά της γραμμής αναζήτησης στην οθόνη κλειδώματος και στον πίνακα ειδοποιήσεων + Αναζήτηση με τα κουμπιά Προηγούμενο & Επόμενο + Οι ελέγχοι για τις ειδοποιήσεις πολυμέσων, την οθόνη κλειδώματος και τα ακουστικά αναζητούν αντί να αλλάζουν τα βίντεο + Μήκος επαναφοράς + Δευτερόλεπτα για επαναφορά με το κουμπί Προηγούμενο + Μήκος ταχείας προώθησης + Δευτερόλεπτα για ταχεία προώθηση με το κουμπί Επόμενο + Το μήκος αναζήτησης πρέπει να είναι μεταξύ 1 και %s δευτερολέπτων. Χρήση προεπιλεγμένης τιμής Απενεργοποίηση πρωτοκόλλου QUIC diff --git a/patches/src/main/resources/addresources/values-el-rGR/youtube/strings.xml b/patches/src/main/resources/addresources/values-el-rGR/youtube/strings.xml index 2a743ef856..4714015009 100644 --- a/patches/src/main/resources/addresources/values-el-rGR/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-el-rGR/youtube/strings.xml @@ -473,6 +473,16 @@ Second \"item\" text" Κάντε λήψη βίντεο, podcast ή τραγούδια με εφαρμογές λήψης τρίτων Εμφάνιση κουμπιού λήψης για το overlay του player Εμφανίζει ένα κουμπί στον video player που ανοίγει τον external downloader σας + Γρήγορη λήψη με ένα πάτημα + Ειδοποιήσεις για λήψη στο παρασκήνιο. Πατήστε και κρατήστε για να ανοίξετε το πρόγραμμα λήψης. Λειτουργεί με τα YTDLnis και YTindir + Σημάδιση λήψεων ως προβολές + Τα βίντεο που λήνθηκαν με ένα εξωτερικό πρόγραμμα λήψης προστίθενται στο ιστορικό προβολών του YouTube σας + Full HD (1080p) + Medium (192 kbps) + Γρήγορη λήψη + Βίντεο + Ήχος + Όλες οι επιλογές Μετάφραση φωνής Διαβάστε τις λεζάντες του βίντεο δυνατά χρησιμοποιώντας το text-to-speech της συσκευής, συγχρονισμένα με την αναπαραγωγή @@ -588,14 +598,14 @@ Second \"item\" text" Η ουρά διατηρείται μετά την επανεκκίνηση της εφαρμογής Αντιμετάθεση των ενεργειών Αποθήκευσης και Προσθήκης στην ουρά Πατήστε για επιλογές ουράς, πατήστε παρατεταμένα για αποθήκευση στην Παρακολούθηση αργότερα - Κουμπί αντικατάστασης σειράς αναδυόμενου μενού ροής - Ανοίγει την ουρά Morphe αντί για την ουρά YouTube + + Αναπαραγωγή στην ουρά ως \"Παίξε μετά\" + Προεπιλογή + Ενσωματωμένη ουρά + Ουρά Morphe + Κανονισμός ουράς Morphe από την πλευρά του πελάτη Προσθήκη στοιχείου μενού αναδυόμενου παραθύρου ροής Προσθέτει ένα επιπλέον στοιχείο αναδυόμενου παραθύρου. Ενεργοποιήστε αυτό εάν δεν εμφανίζεται η \"Προσθήκη στην ουρά\" - Ουρά τύπου Premium - "Αντικαθιστά την ουρά του Morphe με μία που βρίσκεται στη συσκευή, αναπαράγεται από μόνη της και μπορεί να αναδιαταχθεί με σύρσιμο - -Πληροφορίες: Το επόμενο βίντεο στην ουρά ξεκινά μόνο όταν το YouTube είναι στην οθόνη" Ουρά Ουρά (%s) Εκκαθάριση όλων @@ -640,6 +650,20 @@ Second \"item\" text" Αφαίρεση και επαναφόρτωση βίντεο Άνοιγμα ουράς Αποθήκευση ουράς σε λίστα αναπαραγωγής + + Η λίστα αναπαραγωγής αποθηκεύτηκε + Αποθηκεύτηκε στο %s + Εκκαθάριση ουράς + Η ουρά εκκαθαρίστηκε + Ουρά + Αναπαραγωγή τελευταίου στην ουρά + Αναπραγωγή επόμενου στην ουρά + Η YouTube δεν μπόρεσε να εμφανίσει την ουρά. + Η ουρά δεν είναι έτοιμη για αυτό το βίντεο. Ανοίξτε τον player και δοκιμάστε ξανά. + Προσθήκη Mix στην ουρά ή παρακολούθηση Mix; + Παρακολούθηση + Προσθήκη στην ουρά + Φόρτωση Mix… Άνοιγμα καναλιού από ζωντανό avatar Αποτρέπει το άνοιγμα της ζωντανής ροής όταν πατάτε το avatar του καναλιού @@ -853,8 +877,11 @@ Second \"item\" text" Απόκρυψη κουμπιού Πλήρης οθόνη Απενεργοποίηση αυτόματης απόκρυψης της γραμμής πλοήγησης Απόκρυψη του σκούρου shading γύρω από τα στοιχεία ελέγχου του video player - Απενεργοποίηση διαφανούς γραμμής πλοήγησης - Καθιστά τη γραμμή πλοήγησης και κατάστασης πλήρως αδιαφανή + Στυλ γραμμής πλοήγησης + Προεπιλογή + Ημιδιαφανές + Διαφανές + Αδιαφανές Γραμμή εργαλείων Επιλέξτε ποια στοιχεία εμφανίζονται στη γραμμή εργαλείων Κουμπί μετάδοσης diff --git a/patches/src/main/resources/addresources/values-es-rES/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-es-rES/shared-youtube/strings.xml index 7cc819570b..7185e9d165 100644 --- a/patches/src/main/resources/addresources/values-es-rES/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-es-rES/shared-youtube/strings.xml @@ -151,6 +151,13 @@ Para iniciar sesión, pulsa en Continuar" Oculta los botones Anterior y Siguiente en la notificación multimedia, la pantalla de bloqueo y los controles de los auriculares Desactivar barra de progreso Impide arrastrar la barra de progreso en la pantalla de bloqueo y el panel de notificaciones + Buscar con los botones Anterior y Siguiente + Cambiar los controles de la notificación multimedia, pantalla de bloqueo y auriculares para buscar en lugar de cambiar los vídeos + Longitud del retroceso + Segundos para retroceder con el botón Anterior + Longitud del adelanto + Segundos para avanzar con el botón Siguiente + La longitud de búsqueda debe estar entre 1 y %s segundos. Se usa el valor predeterminado Desactivar protocolo QUIC diff --git a/patches/src/main/resources/addresources/values-es-rES/youtube/strings.xml b/patches/src/main/resources/addresources/values-es-rES/youtube/strings.xml index 388a83bfff..ff31c3ba80 100644 --- a/patches/src/main/resources/addresources/values-es-rES/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-es-rES/youtube/strings.xml @@ -473,6 +473,16 @@ Después de abrir por primera vez un vídeo con el botón «Preguntar», la func Descarga vídeos, podcasts o canciones con aplicaciones de descarga de terceros Mostrar botón de descarga de la superposición del reproductor Muestra un botón en el reproductor de vídeo que abre tu descargador externo + Descarga rápida al pulsar + Solicita descargar en segundo plano. Mantén pulsado para abrir el descargador. Funciona con YTDLnis y YTindir + Marcar descargas como vistas + Los vídeos descargados con un gestor de descargas externo se añaden a tu historial de visualizaciones de YouTube + Full HD (1080p) + Medio (192 kbps) + Descarga rápida + Vídeo + Audio + Todas las opciones Traducción con voz Lee en voz alta los subtítulos del vídeo mediante la síntesis de texto a voz del dispositivo, sincronizada con la reproducción @@ -588,14 +598,14 @@ Información: Puede que no funcione en transmisiones en directo" La cola se mantiene después de reiniciar la aplicación Intercambiar las acciones Guardar y Añadir a la cola Pulsa para ver las opciones de cola, mantén pulsado para guardar en \'Ver más tarde\' - Reemplazar botón de cola del menú flotante del feed - Abre la cola de Morphe en lugar de la cola de YouTube + + Tipo de Reproducir el siguiente de la cola + Predeterminada + Cola nativa + Cola de Morphe + Cola de Morphe del lado del servidor Añadir Cola al menú flotante del feed Añade un elemento adicional al menú flotante. Actívalo si no se muestra \'Añadir a la cola\' - Cola de estilo premium - "Reemplaza la cola de Morphe por una que reside en el dispositivo, se reproduce sola y se puede reordenar arrastrando - -Información: el siguiente video en la cola solo se inicia mientras YouTube está en pantalla" Cola Cola (%s) Borrar todo @@ -640,6 +650,20 @@ Información: el siguiente video en la cola solo se inicia mientras YouTube est Eliminar de la cola y recargar vídeo Abrir cola Guardar cola en lista de reproducción + + Lista de reproducción guardada + Guardada en %s + Vaciar la cola + Cola vaciada + Cola + Reproducir último en cola + Reproducir el siguiente de la cola + YouTube no ha podido mostrar la cola. + La cola no está lista para este vídeo. Abre el reproductor de vídeo e inténtalo de nuevo. + ¿Añadir Mix a la cola o ver el Mix? + Ver + Añadir a la cola + Cargando Mix... Abrir canal desde el avatar del directo Evita que la transmisión en directo se abra al pulsar el avatar del canal @@ -853,8 +877,11 @@ Para acceder a los ajustes, activa el botón de ajustes a través de General → Anima las transiciones al cambiar entre pestañas Desactivar ocultación automática de la barra de navegación Mantiene la barra de navegación visible al hacer scroll - Desactivar barra de navegación translúcida - Hace que las barras de navegación y de estado sean completamente opacas + Estilo de la barra de navegación + Predeterminado + Translúcido + Transparente + Opaco Barra de herramientas Elige qué componentes aparecen en la barra de herramientas Ocultar botón Transmitir diff --git a/patches/src/main/resources/addresources/values-et-rEE/youtube/strings.xml b/patches/src/main/resources/addresources/values-et-rEE/youtube/strings.xml index 174f25cd1a..1a02f7efd7 100644 --- a/patches/src/main/resources/addresources/values-et-rEE/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-et-rEE/youtube/strings.xml @@ -76,7 +76,9 @@ Second \"item\" text" + + diff --git a/patches/src/main/resources/addresources/values-eu-rES/youtube/strings.xml b/patches/src/main/resources/addresources/values-eu-rES/youtube/strings.xml index 174f25cd1a..1a02f7efd7 100644 --- a/patches/src/main/resources/addresources/values-eu-rES/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-eu-rES/youtube/strings.xml @@ -76,7 +76,9 @@ Second \"item\" text" + + diff --git a/patches/src/main/resources/addresources/values-fa-rIR/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-fa-rIR/shared-youtube/strings.xml index 5c36f25d0d..c9eae3ef90 100644 --- a/patches/src/main/resources/addresources/values-fa-rIR/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-fa-rIR/shared-youtube/strings.xml @@ -151,6 +151,13 @@ Second \"item\" text" دکمه‌های قبلی و بعدی را در اعلان رسانه، صفحهٔ قفل و کنترل‌های هدفون پنهان می‌کند غیرفعال‌کردن نوار جست‌وجو از کشیدن نوار جست‌وجو در صفحهٔ قفل و پنل اعلان جلوگیری می‌کند + جستجو با دکمه‌های قبلی و بعدی + تغییر کنترل‌های اعلان رسانه، صفحه قفل و هدفون برای جستجو به جای تغییر ویدیوها + طول بازگشت + ثانیه‌ها برای بازگشت با دکمه قبلی + جلوزدن سریع + ثانیه‌ها برای جلو رفتن سریع با دکمه بعدی + طول جستجو باید بین ۱ تا %s ثانیه باشد. استفاده از پیش‌فرض غیرفعال‌کردن پروتکل QUIC diff --git a/patches/src/main/resources/addresources/values-fa-rIR/youtube/strings.xml b/patches/src/main/resources/addresources/values-fa-rIR/youtube/strings.xml index 9653e93052..8d7abd5ece 100644 --- a/patches/src/main/resources/addresources/values-fa-rIR/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-fa-rIR/youtube/strings.xml @@ -473,6 +473,16 @@ Second \"item\" text" دانلود موزیک‌ویدئوها، پادکست‌ها یا آهنگ‌ها با برنامه‌های دانلودکنندهٔ شخص ثالث نمایش دکمهٔ دانلود رویهم‌افتاده پخش‌کننده یک دکمه در پخش‌کنندهٔ ویدئو نمایش می‌دهد که دانلودکنندهٔ خارجی شما را باز می‌کند + دانلود سریع با یک ضربه + درخواست دانلود در پس‌زمینه. برای باز کردن دانلودر، ضربه داده و نگه دارید. با YTDLnis و YTindir سازگار است + علامت‌گذاری دانلودها به عنوان تماشا شده + ویدیوهای دانلود شده با یک دانلودر خارجی به تاریخچه تماشای YouTube شما اضافه می‌شوند + کیفیت ویدیو + میانگین (192 کیلوبیت بر ثانیه) + دانلود سریع + ویدئو + صدا + همه گزینه‌ها ترجمهٔ صوتی زیرنویس‌های ویدئو را با تبدیل متن به گفتار روی دستگاه و همگام با پخش، بلند می‌خواند @@ -588,14 +598,14 @@ Second \"item\" text" صف پس از راه‌اندازی دوبارهٔ برنامه حفظ می‌شود جابه‌جایی عملیات ذخیره و صف برای گزینه‌های صف ضربه بزنید؛ برای ذخیره در «بعداً تماشا می‌کنم» لمس طولانی کنید - جایگزینی دکمهٔ صف منوی بازشوی خوراک - به‌جای صف YouTube، صف Morphe را باز می‌کند + + پخش بعدی در صف + پیش‌فرض + صف بومی + صف Morphe + کلاینت صف Morphe افزودن مورد منوی صف به منوی بازشوی خوراک یک مورد بازشوی اضافی اضافه می‌کند. اگر «افزودن به صف» نمایش داده نمی‌شود، این گزینه را فعال کنید - صف به سبک پریمیوم - "صف Morphe را با صفی جایگزین میکند که روی دستگاه قرار دارد، خودبهخود پخش میشود و میتوان با کشیدن، ترتیب آن را تغییر داد - -اطلاعات: ویدیوی بعدی در صف فقط وقتی شروع میشود که YouTube روی صفحه باشد" صف صف (%s) پاک کردن همه @@ -640,6 +650,20 @@ Second \"item\" text" حذف و بارگذاری دوبارهٔ ویدئو بازکردن صف ذخیرهٔ صف در فهرست پخش + + لیست پخش ذخیره شد + ذخیره شده در %s + پاک کردن صف + صف پاک شد + صف + پخش آخرین مورد در صف + پخش بعدی در صف + YouTube قادر به نمایش صف نیست. + صف برای این ویدیو آماده نیست. پخش کننده ویدیو را باز کنید و دوباره تلاش کنید. + افزودن Mix به صف یا تماشای Mix؟ + تماشا + افزودن به صف + در حال بارگیری Mix… بازکردن کانال از آواتار زنده هنگام ضربه‌زدن روی آواتار کانال، از بازشدن پخش زنده جلوگیری می‌کند @@ -853,8 +877,11 @@ Second \"item\" text" هنگام جابه‌جایی بین برگه‌ها، انتقال‌ها را متحرک می‌کند غیرفعال‌کردن پنهان‌شدن خودکار نوار پیمایش هنگام پیمایش، نوار پیمایش را قابل مشاهده نگه می‌دارد - غیرفعال‌کردن نوار پیمایش نیمه‌شفاف - نوار پیمایش و نوار وضعیت را کاملاً کدر می‌کند + استایل نوار پیمایش + پیش‌فرض + نیمه‌شفاف + شفاف + کدر نوار ابزار انتخاب کنید کدام مؤلفه‌ها در نوار ابزار نمایش داده شوند پنهان‌کردن دکمهٔ ارسال diff --git a/patches/src/main/resources/addresources/values-fi-rFI/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-fi-rFI/shared-youtube/strings.xml index e818abb96b..ad135660cc 100644 --- a/patches/src/main/resources/addresources/values-fi-rFI/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-fi-rFI/shared-youtube/strings.xml @@ -151,6 +151,13 @@ Kirjaudu sisään painamalla alta Jatka-painiketta." Piilottaa Edellinen- ja seuraava-painikkeet mediailmoituksessa, lukitusnäytössä ja kuulokkeiden säätimissä. Poista kelauspalkki käytöstä Estää kelauspalkin vetämisen lukitusnäytössä ja ilmoituspaneelissa. + Haku edellisen & seuraavan painikkeilla + Muuttaa mediatilailmoituksen, lukitusnäytön ja kuulokkeiden ohjaimia hakuun videoiden vaihtamisen sijaan + Kelauksen pituus + Sekuntimäärä, jolla kelataan taaksepäin Edellinen-painikkeella + Kelauksen pituus eteenpäin + Sekuntimäärä, jolla kelataan eteenpäin Seuraava-painikkeella + Kelauksen pituuden on oltava välillä 1 ja %s sekuntia. Käytetään oletusarvoa Poista QUIC-protokolla käytöstä diff --git a/patches/src/main/resources/addresources/values-fi-rFI/youtube/strings.xml b/patches/src/main/resources/addresources/values-fi-rFI/youtube/strings.xml index 3dd0e7ecb2..eab1024a12 100644 --- a/patches/src/main/resources/addresources/values-fi-rFI/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-fi-rFI/youtube/strings.xml @@ -473,6 +473,16 @@ Kerran kun video \"Kysy\"-painikkeella avataan, ominaisuus toimii kaikkien video Lataa videoita, podcasteja tai kappaleita kolmannen osapuolen lataussovelluksilla Näytä pelaajan peitteen latauspainike Näyttää paineen videonsoittimessa, joka avaa ulkoisen lataussovelluksen + Pikalataus napautuksella + Pyytää lataamaan taustalla. Napauta ja pidä pohjassa avataksesi latausohjelman. Toimii YTDLnis- ja YTindir-ohjelmien kanssa + Merkitse lataukset katsotuiksi + Ulkoisella latausohjelmalla ladatut videot lisätään YouTube-katseluhistoriaasi + Täysi HD (1080p) + Keskitaso (192 kbps) + Pikalataus + Video + Ääni + Kaikki asetukset Ääniratateksti Lue videon tekstitykset ääneen laitteen tekstistä puheeksi -toiminnolla, synkronoitu toiston kanssa @@ -588,14 +598,14 @@ Tieto: Ei välttämättä toimi suoratoistoissa" Jono säilyy sovelluksen uudelleenkäynnistyksen jälkeen Vaihda Tallenna- ja Jonotoiminnot Napauta jonon asetuksia varten, napauta ja pidä painettuna tallentaaksesi Katso myöhemmin -listalle - Korvaa syötteen pikavalikkojonon painike - Avaa Morphe-jono YouTuben jonon sijaan + + Nouda seuraava jonossa-tyyppi + Oletus + Alkuperäinen jono + Morphen jono + Morphen jono asiakaspuolella Lisää valikkokohta syötejonon ponnahdusvalikkoon Lisää lisäponnahdusvalikkokohdan. Ota tämä käyttöön, jos \"Lisää jonoon\" ei näy. - Premium-tyylinen jono - "Korvaa Morphe-jonon laitteessa olevalla jonolla, joka toistaa itsestään ja jonka järjestystä voi muuttaa vetämällä - -Info: Seuraava jonossa oleva video käynnistyy vain silloin, kun YouTube on näytöllä" Jono Jono (%s) Tyhjennä kaikki @@ -640,6 +650,20 @@ Info: Seuraava jonossa oleva video käynnistyy vain silloin, kun YouTube on näy Poista jonosta ja lataa video uudelleen Avaa jono Tallenna jono soittolistalle + + Soittolista tallennettu + Tallennettu kohteeseen %s + Tyhjennä jono + Jono tyhjennetty + Jono + Toista viimeinen jonossa + Toista seuraava jonossa + YouTube ei voinut näyttää jonoa. + Jono ei ole valmis tähän videoon. Avaa videotoistin ja yritä uudelleen. + Lisää Mix jsoon vai katso Mixiä? + Katso + Lisää johonkin + Mixin lataus… Avaa kanava suorasta avatartista Estää suoratoiston avaamisen, kun napautat kanavan avataria @@ -853,8 +877,11 @@ Avataksesi asetukset, ota käyttöön Asetukset-painike kohdasta Yleiset → Ty Animoi siirtymät välilehtien välillä Poista automaattisen piilottamisen käytöstä navigointipalkissa Pitää navigointipalkin näkyvillä vieritettäessä - Poista läpikuultava navigointipalkki käytöstä - Tekee navigointi- ja tilapalkin täysin peittäväksi + Navigointipalkin tyyli + Oletus + Puoliläpinäkyvä + Läpinäkyvä + Peittävä Työkalupalkki Valitse, mitkä komponentit näkyvät työkalupalkissa Piilota Cast-painike diff --git a/patches/src/main/resources/addresources/values-fil-rPH/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-fil-rPH/shared-youtube/strings.xml index 6740c0497a..d3bf631728 100644 --- a/patches/src/main/resources/addresources/values-fil-rPH/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-fil-rPH/shared-youtube/strings.xml @@ -151,6 +151,13 @@ Upang mag-sign in, pindutin ang magpatuloy sa ibaba" Itinatago ang mga pindutan ng Previous at Next sa media notification, lock screen, at mga kontrol ng headphone. I-disable ang seekbar Pinipigilan ang pag-drag sa seekbar sa lock screen at notification panel. + Maghanap gamit ang mga pindutan ng Nakaraan & Susunod + Binabago ang kontrol para sa media notification, lock screen, at headphone upang maghanap sa halip na baguhin ang video + Haba ng Rewind + Segundo upang mag-rewind gamit ang pindutang Nakaraan + Haba ng Fast forward + Segundo upang mag-fast-forward gamit ang pindutang Susunod + Dapat nasa pagitan ng 1 at %s segundo ang haba ng paghahanap. Ginagamit ang default Patayin ang QUIC protocol diff --git a/patches/src/main/resources/addresources/values-fil-rPH/youtube/strings.xml b/patches/src/main/resources/addresources/values-fil-rPH/youtube/strings.xml index 319df30b4c..eef9277c7d 100644 --- a/patches/src/main/resources/addresources/values-fil-rPH/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-fil-rPH/youtube/strings.xml @@ -473,6 +473,16 @@ Pagkatapos buksan ang isang video na may Ask button, gagana ang feature sa lahat Mag-download ng mga video, podcast, o kanta gamit ang mga app na pang-download ng third-party Ipakita ang button para sa pag-download ng overlay ng player Ipinapakita ang button sa video player na nagbubukas ng iyong panlabas na downloader + Mabilis na download sa isang tap + Nag-uudyok na mag-download sa background. I-tap at i-hold para buksan ang downloader. Gumagana kasama ang YTDLnis at YTindir + Markahan ang mga download bilang napanood na + Ang mga video na na-download gamit ang external downloader ay idinaragdag sa iyong YouTube watch history + Full HD (1080p) + Katamtaman (192 kbps) + Mabilis na pag-download + Video + Audio + Lahat ng opsyon Pagsasalin ng boses Basahin nang malakas ang mga caption ng video gamit ang text-to-speech sa iyong device, na naka-synchronize sa pagpapatugtog @@ -588,14 +598,14 @@ Impormasyon: Maaaring hindi gumana sa mga live stream" Ang pila ay nananatili kahit i-restart ang app Palitan ang Save at Queue na aksyon I-tap para sa mga opsyon ng queue, i-tap at i-hold upang i-save sa Panoorin Mamaya - Palitan ang pila ng feed flyout na buton - Binubuksan ang pila ng Morphe sa halip na ang pila ng YouTube + + I-play susunod sa pila + Awtomatiko + Katutubong pila + Pila ng Morphe + Pila ng Morphe client-side Idagdag ang feed flyout queue na menu item Nagdaragdag ng karagdagang flyout item. I-enable ito kung hindi ipinapakita ang \"Idagdag sa queue\". - Pilang istilong Premium - "Pinapalitan nito ang pila ng Morphe ng isang pila na nasa device, kusang nagpe-play, at maaaring muling ayusin sa pamamagitan ng pag-drag - -Impormasyon: Magsisimula lang ang susunod na naka-pilang video habang nasa screen ang YouTube" Pila Pila (%s) I-clear lahat @@ -640,6 +650,20 @@ Impormasyon: Magsisimula lang ang susunod na naka-pilang video habang nasa scree Alisin at i-reload ang video Buksan ang queue I-save ang queue sa playlist + + Nai-save ang Playlist + Na-save sa %s + Linisin ang pila + Nalinis ang pila + Pila + I-play huling nasa pila + I-play susunod sa pila + Hindi maipakita ang pila ng YouTube. + Hindi handa ang pila para sa bidyong ito. Buksan ang manonood ng video at subukan ulit. + Magdagdag ng Mix sa pila o panoorin ang Mix? + Manood + Magdagdag sa pila + Naglo-load ng Mix… Buksan ang channel mula sa live avatar Pinipigilan ang live stream na magbukas kapag tina-tap ang avatar ng channel @@ -853,8 +877,11 @@ Para i-access ang Settings, i-enable ang Settings button sa pamamagitan ng Gener Inaanimate ang mga transisyon kapag lumilipat sa pagitan ng mga tab I-disable ang auto-hiding navigation bar Pinapanatili ang navigation bar na visible habang nag-scroll - I-disable ang translucent navigation bar - Ginagawang ganap na opaque ang navigation at status bar + Estilo ng navigation bar + Awtomatiko + Translucent + Transparent + Opaque Toolbar Pumili kung aling mga component ang lilitaw sa flyout menu Itago ang \'Audio Track\' menu diff --git a/patches/src/main/resources/addresources/values-fr-rFR/music/strings.xml b/patches/src/main/resources/addresources/values-fr-rFR/music/strings.xml index 7ed2d95985..64e49e0bb6 100644 --- a/patches/src/main/resources/addresources/values-fr-rFR/music/strings.xml +++ b/patches/src/main/resources/addresources/values-fr-rFR/music/strings.xml @@ -490,7 +490,7 @@ Pour que le scrobbling continue à se synchroniser pendant la lecture en arrièr Spotify : sp_dc enregistré Spotify : sp_dc supprimé Spotify : sp_dc invalide - veuillez vérifier et réessayer - Le jeton Apple Music est optionnel. Sans celui-ci, Lyrically sera utilisé comme solution de secours + Le jeton Apple Music est facultatif. Sans celui-ci, Lyrically sera utilisé comme solution de secours Deezer ARL "Collez le cookie arl de Deezer. @@ -612,7 +612,7 @@ Cookies nécessaires : SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PA Aucune parole à synchroniser - transcription à la place QuickLRC n\'a pas pu traiter ce morceau Les crédits QuickLRC pour ce morceau sont épuisés - "Les paroles sont associées par titre et artiste ; des métadonnées inhabituelles peuvent ne rien donner ou renvoyer une mauvaise version. Utilisez l'actualisation pour changer de version. Les fournisseurs marqués d'un * nécessitent un jeton. Tous les fournisseurs activés ne sont pas toujours sollicités dans votre ordre, mais répondent selon leur rapidité.\n\nLes paroles proviennent de services tiers qui ne sont ni affiliés, ni sponsorisés, ni approuvés par Morphe. Chacun est utilisé tel quel : les paroles, leur disponibilité et leur licence restent la propriété de ces services et de leurs concédants, et leur utilisation peut être soumise à leurs conditions d'utilisation . Seule la recherche demandée est transmise à un service activé - le titre, l'artiste et, si le service en a besoin, l'identifiant de la vidéo. Le point de terminaison de l'IA optionnel et la transcription QuickLRC ne s'exécutent qu'après leur configuration, et reçoivent le texte des paroles ou l'URL vidéo du morceau." + "Les paroles sont associées par titre et artiste ; des métadonnées inhabituelles peuvent ne rien donner ou renvoyer une mauvaise version. Utilisez l'actualisation pour changer de version. Les fournisseurs marqués d'un * nécessitent un jeton. Tous les fournisseurs activés ne sont pas toujours sollicités dans votre ordre, mais répondent selon leur rapidité.\n\nLes paroles proviennent de services tiers qui ne sont ni affiliés, ni sponsorisés, ni approuvés par Morphe. Chacun est utilisé tel quel : les paroles, leur disponibilité et leur licence restent la propriété de ces services et de leurs concédants, et leur utilisation peut être soumise à leurs conditions d'utilisation . Seule la recherche demandée est transmise à un service activé - le titre, l'artiste et, si le service en a besoin, l'identifiant de la vidéo. Le point de terminaison de l'IA facultatif et la transcription QuickLRC ne s'exécutent qu'après leur configuration, et reçoivent le texte des paroles ou l'URL vidéo du morceau." Activez uniquement les fournisseurs que vous utilisez, ou conservez les paramètres par défaut - des requêtes constantes peuvent submerger les petits sites communautaires de paroles Masquer la ligne de crédit Masquer la ligne de crédit affichée sous les paroles diff --git a/patches/src/main/resources/addresources/values-fr-rFR/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-fr-rFR/shared-youtube/strings.xml index 98d96b96b9..2a6f4df357 100644 --- a/patches/src/main/resources/addresources/values-fr-rFR/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-fr-rFR/shared-youtube/strings.xml @@ -151,6 +151,13 @@ Pour vous connecter, appuyez sur 'Continuer' ci-dessous." Masque les boutons \'Précédent\' et \'Suivant\' dans la notification multimédia, sur l\'écran de verrouillage et les commandes du casque Désactiver la barre de progression Empêche le déplacement de la barre de progression sur l\'écran de verrouillage et le panneau de notifications + Recherche avec les boutons Précédent et Suivant + Modifie les contrôles pour la notification média, l\'écran verrouillé et le casque afin de rechercher au lieu de changer les vidéos + Durée du recul + Secondes de recul avec le bouton Précédent + Durée de l\'avance rapide + Secondes d\'avance rapide avec le bouton Suivant + La durée de recherche doit être comprise entre 1 et %s secondes. Valeur par défaut utilisée Désactiver le protocole QUIC diff --git a/patches/src/main/resources/addresources/values-fr-rFR/youtube/strings.xml b/patches/src/main/resources/addresources/values-fr-rFR/youtube/strings.xml index 66d0a9e5ce..2f4125e50f 100644 --- a/patches/src/main/resources/addresources/values-fr-rFR/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-fr-rFR/youtube/strings.xml @@ -473,6 +473,16 @@ Après avoir ouvert une fois une vidéo avec le bouton 'Posez une question', la Téléchargez des vidéos, des podcasts ou des chansons avec des applications de téléchargement tierces Afficher le bouton de téléchargement du lecteur superposé Affiche un bouton dans le lecteur vidéo qui ouvre votre téléchargeur externe + Téléchargement rapide au toucher + Propose le téléchargement en arrière-plan. Appuyez longuement pour ouvrir le téléchargeur. Compatible avec YTDLnis et YTindir + Marquer les téléchargements comme regardés + Les vidéos téléchargées avec un téléchargeur externe sont ajoutées à votre historique de visionnage YouTube + Full HD (1080p) + Moyen (192 kbps) + Téléchargement rapide + Vidéo + Audio + Toutes les options Traduction en voix off Lisez les sous-titres de la vidéo à voix haute à l\'aide de la synthèse vocale sur l\'appareil, synchronisée avec la lecture @@ -489,7 +499,7 @@ Après avoir ouvert une fois une vidéo avec le bouton 'Posez une question', la Appuyez ici pour ouvrir MyMemory.translated.net" E-mail MyMemory - Optionnel. Entrez votre adresse e-mail MyMemory pour augmenter la limite quotidienne de 5,000 à 50,000 caractères. Laissez vide pour utiliser la limite anonyme + Facultatif. Entrez votre adresse e-mail MyMemory pour augmenter la limite quotidienne de 5,000 à 50,000 caractères. Laissez vide pour utiliser la limite anonyme "Traduction IA payante via OpenRouter. Rechargez une seule fois et traduisez des milliers de vidéos. Mistral Nemo est l'option la moins chère, Gemma 4 offre la meilleure qualité. Appuyez ici pour ouvrir OpenRouter.ai" @@ -588,14 +598,14 @@ Info : Peut ne pas fonctionner sur les diffusions en direct" La file d\'attente est conservée après le redémarrage de l\'application Inverser les actions \'Enregistrer\' et \'File d\'attente\' Appuyez pour afficher les options de la file d\'attente, appuyez longuement pour enregistrer dans \'À regarder plus tard - Remplacer le bouton de file d\'attente du menu déroulant - Ouvre la file d\'attente Morphe au lieu de la file d\'attente YouTube + + Lecture suivante dans la file d\'attente + Par défaut + File d\'attente native + File d\'attente Morphe + Client de file d\'attente Morphe Ajouter l\'élément file d\'attente au menu déroulant du flux Ajoute un élément supplémentaire au menu déroulant. Activez cette option si \'Ajouter à la file d’attente\' n\'est pas affiché - File d\'attente de type Premium - "Remplace la file d'attente Morphe par une file d'attente stockée sur l'appareil, qui se lit automatiquement jusqu'au bout et dont l'ordre peut être modifié par glisser-déposer - -Info : La vidéo suivante dans la file d'attente ne démarre que lorsque YouTube est à l'écran" File d\'attente File d\'attente (%s) Tout effacer @@ -640,6 +650,20 @@ Info : La vidéo suivante dans la file d'attente ne démarre que lorsque YouTube Supprimer et recharger la vidéo Ouvrir la file d\'attente Enregistrer la file d\'attente dans une playlist + + Playlist enregistrée + Enregistré dans %s + Vider la file d\'attente + File d\'attente vidée + File d\'attente + Lire le dernier de la file d\'attente + Lire ensuite dans la file d\'attente + YouTube n\'a pas pu afficher la file d\'attente. + La file d\'attente n\'est pas prête pour cette vidéo. Ouvrez le lecteur vidéo et réessayez. + Ajouter le Mix à la file d\'attente ou regarder le Mix ? + Regarder + Ajouter à la file d\'attente + Chargement du Mix…  Ouvrir la chaîne depuis l\'avatar du direct Empêche l\'ouverture du direct lorsqu\'on appuie sur l\'avatar de la chaîne @@ -853,8 +877,11 @@ Pour accéder aux paramètres, activez le bouton 'Paramètres' via Général → Anime les transitions lors du passage d\'un onglet à un autre Désactiver le masquage automatique de la barre de navigation Garde la barre de navigation visible lors du défilement - Désactiver la barre de navigation translucide - Rend les barres de navigation et d\'état complètement opaques + Style de la barre de navigation + Par défaut + Translucide + Transparent + Opaque Barre d\'outils Choisissez les composants qui apparaissent dans la barre d\'outils Masquer le bouton \'Caster\' diff --git a/patches/src/main/resources/addresources/values-ga-rIE/music/strings.xml b/patches/src/main/resources/addresources/values-ga-rIE/music/strings.xml index a5f7ae9dde..1afdd76bc8 100644 --- a/patches/src/main/resources/addresources/values-ga-rIE/music/strings.xml +++ b/patches/src/main/resources/addresources/values-ga-rIE/music/strings.xml @@ -622,16 +622,16 @@ Fianáin riachtanacha: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PA Rómáinization Soláthraí aistriúcháin Google (Réamhshocráil, Saor) - Comhoiriúnach le OpenAI (Custom) - Soláthraí románaíochta + Comhoiriúnach le OpenAI (Saincheaptha) + Soláthraí rómhánaithe Google (Réamhshocráil) - Foclóir Áitiúil / Aslíne - Comhoiriúnach le OpenAI (Custom) + Foclóir Áitiúil / As Líne + Comhoiriúnach le OpenAI (Saincheaptha) Rannóg AI Ní úsáidtear é seo ach amháin nuair nach soláthraíonn an soláthraí aon lipéid rannáin, ar nós \'Curfá\' nó \'Véarsa\'; braithfidh do mhúnla, atá comhoiriúnach le OpenAI, na lipéid sin ansin. AI & Pointí Leoras Scamaile - Cumraigh eochracha API, samhní, endpoints, agus teimpléid spreagtha do sholáthraí AI. - URL pointe críochta API (m.sh. https://api.openai.com/v1/chat/completions) + Cumraigh eochracha API, samhlacha, críochphointí, agus teimpléid leid do sholáthraithe AI. + URL críochphointe API (m.sh. https://api.openai.com/v1/chat/completions) Teimpléad Leide Áitchoimeádáin: {language}, {task}, {title}, {artist}, {count}, {lines} Sábháiltear leide @@ -639,29 +639,29 @@ Fianáin riachtanacha: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PA Athshocrú an leid Ní JSON bailí é. Ceartaigh é nó glan an réimse sula sábhálann tú Socruithe aistriúcháin & Rómhálaíocht sábháilte - Athshocraigh socruithe go réamhshocrú + Socruithe athshocraithe go dtí na réamhshocruithe Caithfidh URL bonn tosú le http:// nó https:// Tástáil Nasc - Cumraigh Endpoints - Taispeáin Gach Endpoints + Cumraigh Pointí Críche + Taispeáin Gach Críochphointe Ag fíorú na nascachta… - Soláthraí aistriúcháin - Soláthraí Rómánaíochta - Fáil Modelail - Ag fáil modelail… - Roghnaigh Model - Teip ar fháil na modelail. Seiceáil an eochair API nó an nasc. - Socruithe Gemini API - Comptaible le OpenAI (Bhunphointe Custaim), + Soláthraí Aistriúcháin + Soláthraí Rómhánaithe + Faigh Múnlaí + Ag fáil samhlacha… + Roghnaigh Múnla + Theip ar fháil na múnlaí. Seiceáil an eochair API nó an nasc. + Socruithe API Gemini + Comhoiriúnach le OpenAI (Críochphointe Saincheaptha) Socruithe API DeepL Eochair API Samhail URL Bun - Eochair API (rodach) - banca aistriúcháin - Corp iarrata extra (JSON rodach) - Eochair authentication (Saor nó Pro) - Configure eochracha API, samhailí, endpoints, agus prompts + Eochair API (roghnach) + Treoir aistriúcháin + Comhlacht iarratais breise (JSON roghnach) + Eochair fhíordheimhnithe (Saor in aisce nó Pro) + Cumraigh eochracha API, samhlacha, pointí críche, agus leideanna 8.23.51 - Díchumasaigh deilbhíní troma 9.13.51 - Athchóirigh an t-imreoir Ar Aghaidh, Liricí, agus na cluaisíní Gaolmhara diff --git a/patches/src/main/resources/addresources/values-ga-rIE/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-ga-rIE/shared-youtube/strings.xml index 6373f2e2fe..b6f62ad3d1 100644 --- a/patches/src/main/resources/addresources/values-ga-rIE/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ga-rIE/shared-youtube/strings.xml @@ -151,6 +151,13 @@ Chun síniú isteach, brúigh lean ar aghaidh thíos" Folaíonn sé cnaipí Roimhe Seo agus Ar Aghaidh i bhfógraí meán, scáileán glasála, agus rialuithe cluasán Díchumasaigh an barra cuardaigh Coscann sé an barra cuardaigh a tharraingt ar an scáileán glasála agus ar an bpainéal fógraí + Lorg le cnaipe Roimhe & Ar Aghaidh + Athraíonn riailuithe don fhógra meán, scáileán glas, agus cluasáin chun lorg in ionad videois a athrú + Fad an aisluaithe + Soicindí chun aisluaithe leis an gcnaipe Roimhe + Fad an cinnluaithe + Soicindí chun cinnluaithe leis an gcnaipe Ar Aghaidh + Ba chóir go mbeadh an fhad lorga idir 1 agus %s soicind. Ag baint úsáide as réamhshocrú Díchumasaigh prótacal QUIC diff --git a/patches/src/main/resources/addresources/values-ga-rIE/youtube/strings.xml b/patches/src/main/resources/addresources/values-ga-rIE/youtube/strings.xml index 78597d7166..b4188e4dd9 100644 --- a/patches/src/main/resources/addresources/values-ga-rIE/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ga-rIE/youtube/strings.xml @@ -455,14 +455,14 @@ Má oscailtí físeán a bhfuil cnaipe Iarrtas aige uair amháin, beidh an ghné Taispeáin líon na n-aonán ar mhionsamhail sa bhfotha Taispeáin mionsamhail beatha cosúil le céatadán - Cuardaigh tráchtanna - Tapáil ar an gcnaipe cuardaigh sna tráchtanna chun na tráchtanna luchtaithe atá ann ar féidir leo gach focal cuardaithe a bheith ann a thaispeáint. Cuir focail i gcomhrátaí chun frása a chuardach - Cuardaigh tráchtanna + Cuardaigh tuairimí + Tapáil an cnaipe cuardaigh sna tráchtanna chun gan ach na tráchtanna atá luchtaithe agus ina bhfuil na focail chuardaigh ar fad a thaispeáint. Cuir focail idir comharthaí athfhriotail chun frása a chuardach. + Cuardaigh tuairimí Focail le cuardach Cuardaigh Glan - Athraigh ord na sórtála nó oscail na tráchtanna arís chun iad a chuardach - Slánaitheadh an cuardaigh. Athraigh ord na sórtála nó oscail na tráchtanna arís chun gach trácht a thaispeáint + Athraigh an t-ord sórtála nó oscail na tuairimí arís chun cuardach a dhéanamh iontu + Cuardaigh glanta. Athraigh an t-ord sórtála nó oscail na tráchtanna arís chun na tráchtanna go léir a thaispeáint. Díchumasaigh aníos \'Sínigh isteach ar an Teilifís\' Díchumasaigh an preab ‘Ceangail do ghléasanna’ @@ -473,6 +473,16 @@ Má oscailtí físeán a bhfuil cnaipe Iarrtas aige uair amháin, beidh an ghné Íoslódáil físeáin, podchraoltaí, nó amhráin le haipeanna íoslódála tríú páirtí Taispeáin cnaipe íoslódála ar fhorleagan an imreora Taispeánann sé cnaipe sa seinnteoir físe a osclaíonn d’uirlis íoslódála sheachtrach + Íoslódáil thapa le tapáil + Cuireann sé isteach iad a íoslódáil sa chúlra. Tapáil agus coinlligh chun an íoslódálaí a oscailt. Oibríonn sé le YTDLnis agus YTindir + Marcáil íoslódálacha mar a bhí feicthe + Cuirtear físeáin a íoslódálann íoslódálaí externach leis an stair féachta YouTube agat + Full HD (1080p) + Meánach (192 kbps) + Íoslódáil tapaidh + Físeán + Fuaim + Gach rogha Aistriúchán gutha Léigh fotheidil físe os ard ag baint úsáide as téacs-go-hurlabhra ar an ngléas, sioncrónaithe le hathsheinm @@ -588,14 +598,14 @@ Eolas: Seans nach n-oibreoidh sé ar shruthanna beo" Coinnítear an scuaine tar éis an aip a atosú Malartaigh gníomhartha Sábháil agus Ciú Tapáil le haghaidh roghanna scuaine, tapáil agus coinnigh síos chun sábháil chuig Féach Níos Déanaí - Sáraigh an cnaipe scuaine fothaithe - Osclaíonn sé an scuaine Morphe in ionad scuaine YouTube + + Seinn an chéad cheann eile sa scuaine + Réamhshocrú + Scuaine tionscadail + Scuaine Morphe + Cliant scuaine Morphe Cuir mír roghchláir scuaine amach beatha leis Cuireann sé mír bhreise ar an leathanach leis. Cumasaigh é seo mura bhfuil \"Cuir leis an scuaine\" le feiceáil. - Ciú ar stíl Premium - "Cuirtear in ionad an chiú Morphe ceann a fhanann ar an ngléas, a sheinneann ar aghaidh leis féin agus is féidir é a atheagrú trí tharraingt - -Eolas: Ní thosaíonn an chéad fhíseán eile atá sa chiú ach amháin fad is atá YouTube ar an scáileán" Scuaine Scuaine (%s) Glan gach rud @@ -640,6 +650,20 @@ Eolas: Ní thosaíonn an chéad fhíseán eile atá sa chiú ach amháin fad is Bain agus athlódáil an físeán Oscail an scuaine Sábháil an scuaine chuig an seinmliosta + + Liamosta shábháilte + Sábháilte go %s + Glan scuaine + Scuaine glanta + Scuaine + Seinn an ceann deireanach sa scuaine + Seinn an chéad cheann eile sa scuaine + Níorbh fhéidir le YouTube an scuaine a thaispeáint. + Níl an scuaine réidh don físeán seo. Oscail imitheoir an físeáin agus iarr arís. + Cuir Cás leis an scuaine nó féach ar Chas? + Féach + Cuir leis an scuaine + Ag lódáil Cas… Oscail an cainéal ón avatar beo Coscann sé seo an sruth beo ó oscailt nuair a thapaítear ar avatar an chainéil. @@ -847,14 +871,17 @@ Chun rochtain a fháil ar na Socruithe, cuir an cnaipe Socruithe ar chumas trí Laghdaíonn an spásáil chothrománach idir cnaipí Díchumasaigh cnaipí nascleanúna gan lipéid Taispeánann sé na cnaipí nascleanúna le lipéid, má sheolann YouTube iad gan lipéid - Díchumasaigh tabanna nascleanúna toolbar - Taispeánann Liostraí in ionad an barra nascleanúna, má bhogann YouTube é go tab ag barr an Phoist + Díchumasaigh cluaisíní nascleanúna barra uirlisí + Taispeánann sé \'Síntiúis\' sa bharra nascleanúna, má bhogann YouTube chuig cluaisín ag barr an leathanaigh \'Baile\' é. Cumasaigh beochana barra nascleanúna Beochan aistrithe agus tú ag aistriú idir cluaisíní Díchumasaigh barra nascleanúna uathoibríoch-fholaigh Coinníonn sé an barra nascleanúna le feiceáil agus tú ag scrollú - Díchumasaigh barra nascleanúna tréshoilseach - Déanann sé an nascleanúint agus an barra stádais go hiomlán teimhneach + Stíl an bharra nascleanúna + Réamhshocrú + Tréshoilseach + Trédhearcach + Teimhneach Barra Uirlisí Roghnaigh cé na comhpháirteanna a thaispeántar sa bharra uirlisí Folaigh an cnaipe Cast diff --git a/patches/src/main/resources/addresources/values-gl-rES/youtube/strings.xml b/patches/src/main/resources/addresources/values-gl-rES/youtube/strings.xml index 401ac81534..7557b6d645 100644 --- a/patches/src/main/resources/addresources/values-gl-rES/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-gl-rES/youtube/strings.xml @@ -184,6 +184,7 @@ Os cambios inclúen: + Vídeo Personalizado @@ -194,8 +195,11 @@ Os cambios inclúen: + + Predefinido Eliminar + @@ -241,6 +245,7 @@ Os cambios inclúen: Ocultar barra de navegación + Predefinido Ocultar botón Chat diff --git a/patches/src/main/resources/addresources/values-gu-rIN/youtube/strings.xml b/patches/src/main/resources/addresources/values-gu-rIN/youtube/strings.xml index bd048ac1ca..bfc9a4cf93 100644 --- a/patches/src/main/resources/addresources/values-gu-rIN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-gu-rIN/youtube/strings.xml @@ -77,7 +77,10 @@ Second \"item\" text" + + ડિફૉલ્ટ + @@ -118,6 +121,7 @@ Second \"item\" text" + ડિફૉલ્ટ diff --git a/patches/src/main/resources/addresources/values-hi-rIN/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-hi-rIN/shared-youtube/strings.xml index 2b49b214b3..875c97e266 100644 --- a/patches/src/main/resources/addresources/values-hi-rIN/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-hi-rIN/shared-youtube/strings.xml @@ -151,6 +151,13 @@ Second \"item\" text" मीडिया नोटिफिकेशन, लॉक स्क्रीन और हेडफ़ोन नियंत्रणों में पिछले और अगले बटन छिपाता है सीकबार अक्षम करें लॉक स्क्रीन और नोटिफिकेशन पैनल पर सीकबार को खींचने से रोकता है + पिछले और अगले बटनों के साथ खोजें + मीडिया नोटिफिकेशन, लॉक स्क्रीन और हेडफ़ोन में वीडियो बदलने के बजाय खोजने के लिए नियंत्रण परिवर्तित करता है + रीवाइंड लंबाई + पिछले बटन से रीवाइंड करने के लिए सेकंड + फास्ट फॉरवर्ड लंबाई + अगले बटन से फास्ट-फॉरवर्ड करने के लिए सेकंड + खोज लंबाई 1 और %s सेकंड के बीच होनी चाहिए। डिफ़ॉल्ट का उपयोग किया जा रहा है QUIC प्रोटोकॉल को अक्षम करना diff --git a/patches/src/main/resources/addresources/values-hi-rIN/youtube/strings.xml b/patches/src/main/resources/addresources/values-hi-rIN/youtube/strings.xml index 88e2286587..76b53f8189 100644 --- a/patches/src/main/resources/addresources/values-hi-rIN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-hi-rIN/youtube/strings.xml @@ -473,6 +473,16 @@ Second \"item\" text" तृतीय-पक्ष डाउनलोडिंग ऐप्स के साथ वीडियो, पॉडकास्ट या गाने डाउनलोड करें प्लेयर ओवरले डाउनलोड बटन दिखाएं वीडियो प्लेयर में एक बटन दिखाता है जो बाहरी डाउनलोडर खोलता है + टैप करने पर त्वरित डाउनलोड + बैकग्राउंड में डाउनलोड करने के लिए संकेत देता है। डाउनलोडर खोलने के लिए टैप करके रखें। YTDLnis और YTindir के साथ काम करता है + डाउनलोड्स को देखा गया मार्क करें + बाहरी डाउनलोडर के साथ डाउनलोड किए गए वीडियो आपके YouTube वॉच हिस्ट्री में जोड़ दिए जाते हैं + फुल एचडी (1080p) + मध्यम (192 kbps) + त्वरित डाउनलोड + वीडियो + ऑडियो + सभी विकल्प वॉइस ओवर अनुवाद डिवाइस पर टेक्स्ट-टू-स्पीच का उपयोग करके वीडियो कैप्शन को ज़ोर से पढ़ें, जो प्लेबैक के साथ सिंक्रनाइज़ किया गया है। @@ -588,14 +598,14 @@ OpenRouter.ai खोलने के लिए यहां टैप करे ऐप को पुनरारंभ करने के बाद कतार बनी रहती है स्वैप सेव और क्यू क्रियाएँ कतार विकल्पों के लिए टैप करें, बाद में देखने के लिए सहेजने हेतु टैप और होल्ड करें - फ़ीड फ्लाईआउट कतार बटन को ओवरराइड करें - Morphe कतार को YouTube कतार के बजाय खोलता है + + अगला कतार में चलाएँ + डिफ़ॉल्ट + नेटिव कतार + Morphe कतार + Morphe क्लाइंट-साइड कतार फ़ीड फ़्लाईआउट कतार मेनू आइटम जोड़ें एक अतिरिक्त फ़्लाईआउट आइटम जोड़ता है। यदि \"कतार में जोड़ें\" नहीं दिखाया गया है तो इसे सक्षम करें। - प्रीमियम-शैली कतार - "Morphe कतार को एक ऐसी कतार से बदलता है जो डिवाइस पर रहती है, अपने आप चलती रहती है, और जिसे खींचकर पुनः क्रमबद्ध किया जा सकता है - -जानकारी: कतार में अगला वीडियो केवल तभी शुरू होता है जब YouTube स्क्रीन पर होता है" कतार कतार (%s) सभी साफ़ करें @@ -640,6 +650,20 @@ OpenRouter.ai खोलने के लिए यहां टैप करे क्यू से हटाएं और वीडियो रीलोड करें क्यू खोलें प्लेलिस्ट में क्यू सहेजें + + प्लेलिस्ट सहेजा गया + %s में सहेजा गया + कतार खाली करें + कतार खाली की गई + कतार + आखिरी कतार में चलाएँ + अगला कतार में चलाएँ + YouTube कतार प्रदर्शित नहीं कर सका। + यह वीडियो के लिए कतार तैयार नहीं है। वीडियो प्लेयर खोलें और पुनः प्रयास करें। + मिक्स कतार में जोड़ें या मिक्स देखें? + देखें + कतार में जोड़ें + मिक्स लोड हो रहा है… लाइव अवतार से चैनल खोलें चैनल के अवतार पर टैप करने पर लाइव स्ट्रीम को खुलने से रोकता है @@ -853,8 +877,11 @@ OpenRouter.ai खोलने के लिए यहां टैप करे स्टेटस बार को पूरी तरह से अपारदर्शी बनाता है ऑटो-हाइडिंग नेविगेशन बार को निष्क्रिय करें लाइट मोड में नेविगेशन बार को पूरी तरह से अपारदर्शी बनाता है - पारदर्शी नेविगेशन बार अक्षम करें - नेविगेशन और स्टेटस बार को पूरी तरह अपारदर्शी बनाता है + नेविगेशन बार स्टाइल + डिफ़ॉल्ट + अर्धपारदर्शी + पारदर्शी + अपारदर्शी टूलबार टूलबार में कौन से घटक दिखाई दें, यह चुनें \'ऑडियो ट्रैक\' मेनू फ़ूटर छुपाएं diff --git a/patches/src/main/resources/addresources/values-hr-rHR/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-hr-rHR/shared-youtube/strings.xml index 6f8e7ae4c9..d46cc7075a 100644 --- a/patches/src/main/resources/addresources/values-hr-rHR/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-hr-rHR/shared-youtube/strings.xml @@ -151,6 +151,13 @@ Za prijavu, pritisnite nastavak ispod" Sakriva prethodne i sljedeće gumbe u medijskoj obavijesti, na zaključanom zaslonu i na kontrolama za slušalice Onemogući kliznu traku Sprječava povlačenje klizne trake na zaključanom zaslonu i ploči obavijesti + Traži s gumbima & Prethodni i Sljedeći + Promjena kontrola za obavijest o mediju, zaključani zaslon i slušalice radi traženja umjesto promjene videa + Duljina prebacivanja unazad + Broj sekundi za prebacivanje unazad pomoću gumba Prethodni + Duljina prebacivanja unaprijed + Broj sekundi za prebacivanje unaprijed pomoću gumba Sljedeći + Duljina traženja mora biti između 1 i %s sekundi. Koristi se zadana vrijednost Onemisliti protokol QUIC diff --git a/patches/src/main/resources/addresources/values-hr-rHR/youtube/strings.xml b/patches/src/main/resources/addresources/values-hr-rHR/youtube/strings.xml index a1df1723fe..5be5a7175a 100644 --- a/patches/src/main/resources/addresources/values-hr-rHR/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-hr-rHR/youtube/strings.xml @@ -463,6 +463,16 @@ Nakon što se jednom otvori video s gumbom Ask, funkcija radi sa svim videima, Preuzmite videozapise, podcaste ili pjesme pomoću aplikacija za preuzimanje trećih strana Prikaži gumb za preuzimanje preklopa glazbenog playera Prikazuje gumb u video playeru koji otvara vaš vanjski downloader + Brzo preuzimanje na dodir + Potvrde za preuzimanje u pozadini. Dodirnite i držite za otvaranje programa za preuzimanje. Radi s YTDLnis i YTindir + Označi preuzimanja kao gledana + Videozapisi preuzeti pomoću vanjskog programa za preuzimanje dodaju se u vašu YouTube povijest gledanja + Full HD (1080p) + Srednje (192 kbps) + Brzo preuzimanje + Video + Audio + Sve opcije Prevođenje glasom Čitaj titlove videozapisa naglas koristeći uređajni text-to-speech, sinkronizirano s reprodukcijom @@ -578,14 +588,14 @@ Info: Možda neće raditi na prijenosima uživo" Red čekanja se zadržava nakon ponovnog pokretanja aplikacije Zamijeni radnje Spremi i dodaj u red Dodirnite za opcije reda čekanja, dodirnite i držite za spremanje na Gledaj kasnije - Nadjačaj gumb reda čekanja u feed izletniku - Otvara Morphe red čekanja umjesto YouTube reda čekanja + + Sljedeći u redu čekanja + Zadano + Materijalni red čekanja + Morphe red čekanja + Klijentska strana Morphe reda čekanja Dodaj stavku izbornika reda čekanja u feed izletnik Dodaje dodatnu stavku izletnika. Omogućite ovo ako \'Dodaj u red čekanja\' nije prikazano - Red u Premium stilu - "Zamjenjuje red Morphe s redom koji se nalazi na uređaju, automatski se reproducira i može se preuređivati povlačenjem - -Info: sljedeći videozapis iz reda pokreće se samo dok je YouTube na zaslonu" Red Red čekanja (%s) Očisti sve @@ -630,6 +640,20 @@ Info: sljedeći videozapis iz reda pokreće se samo dok je YouTube na zaslonu"Ukloni i ponovno učitaj video Otvori red čekanja Spremi red čekanja u playlistu + + Popis za reprodukciju spremljen + Spremljeno u %s + Očisti red čekanja + Red čekanja je očistio + Red čekanja + Zadnji u redu čekanja + Sljedeći u redu čekanja + YouTube nije mogao prikazati red čekanja. + Red čekanja nije spreman za ovaj video. Otvori player videa i pokušaj ponovo. + Dodaj Mix u red čekanja ili gledaj Mix? + Gledaj + Dodaj u red čekanja + Učitavanje Mixa… Otvori kanal iz live avatara Sprječava otvaranje live streama pri dodiru avatara kanala @@ -841,8 +865,11 @@ Da biste to popravili, uključite \"Lažiranje video streamova\" i promijenite \ Animira prijelaze pri prebacivanju između kartica Onemogući automatsko skrivanje navigacijske trake Drži navigacijsku traku vidljivom tijekom pomicanja - Onemogući prozirnu navigacijsku traku - Čini navigacijsku traku i traku stanja potpuno neprozirnima + Stil navigacijske trake + Zadano + Poluprozračno + Prozirno + Neprozirno Traka Odaberite koje komponente se pojavljuju u traci s alatima Sakrij tipku za emitiranje diff --git a/patches/src/main/resources/addresources/values-hu-rHU/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-hu-rHU/shared-youtube/strings.xml index 16efd98064..0d216e9d07 100644 --- a/patches/src/main/resources/addresources/values-hu-rHU/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-hu-rHU/shared-youtube/strings.xml @@ -151,6 +151,13 @@ Belépéshez nyomd meg a folytatást alul" Elrejti az előző és következő gombokat a médiaértesítésben, a lezárási képernyőn és a fejhallgató-vezérlőkben Seekbar letiltása Megakadályozza a seekbar húzását a lezárási képernyőn és az értesítési panelen + Keresés Előző & Következő gombokkal + A médiaesközözök vezérlése helyett a keresést végzi értesítési panelen, zárképernyőn és fülhallgatón + Visszatekerés hossza + Másodpercek visszatekerése az Előző gombbal + Előretekerés hossza + Másodpercek előretekerése a Következő gombbal + A keresési hossz 1 és %s másodperc közötti érték kell legyen. Az alapértelmezett használatával QUIC protokoll letiltása diff --git a/patches/src/main/resources/addresources/values-hu-rHU/youtube/strings.xml b/patches/src/main/resources/addresources/values-hu-rHU/youtube/strings.xml index 1b24044011..cc171afe1e 100644 --- a/patches/src/main/resources/addresources/values-hu-rHU/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-hu-rHU/youtube/strings.xml @@ -473,6 +473,16 @@ Egy alkalommal megnyitott, Ask gombbal ellátott videó esetén a funkció minde Töltsön le videókat, podcastokat vagy dalokat harmadik féles alkalmazásokkal Klejítőpanel letöltés gomb mutatása a lejátszóban Gomb megjelenítése a videolejátszón, amely megnyitja a külső letöltőt + Gyors letöltés egy érintéssel + Kérdézi, hogy letöltse a háttérben. Nyomassa hosszan a letöltő megnyitásához. Kompatibilis a YTDLnis és a YTindir alkalmazásokkal + Letöltések megjelölése megtekintettként + A külső letöltővel letöltött videók hozzá vannak adják a YouTube megtekintési előzményekhez + Full HD (1080p) + Közepes (192 kbps) + Gyors letöltés + Videó + Hang + Összes beállítás Hangfelirat hangos felolvasása Olvassa fel a videó feliratait hangosan a készüléken található szöveg-beszéddé alakító segítségével, szinkronban a lejátszással. @@ -588,14 +598,14 @@ Figyelem: Lehet, hogy az élő közvetítéseknél nem működik" A sor az alkalmazás újraindítása után is megőrizhető Csere a Mentés és a Sorba rendezés műveletekhez Koppintson a sor beállításaihoz, tartsa lenyomva a \'Megnézem később\' listára mentéshez. ,\"key\":\"morphe_swap_save_and_queue_actions_summary\",\"context\":\"morphe_swap_save_and_queue_actions_summary\",\"maxLength\":null,\"terms\":[]} - A Morphe sor felülbírálása a lejálistában - A Morphe sorbanálítást nyit meg a YouTube sor helyett. + + Lejátszás a következő sorban + Alapértelmezett + Beépített sor + Morphe sor + Morphe klienstoldali sor Sor menüelem hozzáadása a hírfolyam felugró menühöz Hozzáad egy további felugró menüelemet. Engedélyezze, ha a \"Hozzáadás a sorhoz\" nem jelenik meg. - Prémium stílusú várólista - "A Morphe-várólistát egy olyan várólistára cseréli, amely az eszközön tárolódik, magától végigjátssza a listát, és húzással átrendezhető - -Információ: A várólista következő videója csak akkor indul el, ha a YouTube éppen a képernyőn van" Várólista Sor (%s) Összes törlése @@ -640,6 +650,20 @@ Információ: A várólista következő videója csak akkor indul el, ha a YouTu Eltávolítás a sorból és videó újratöltése Sor megnyitása A sor mentése lejálistához + + Lejátszási lista mentve + Mentve ide: %s + Sor törlése + A sor törölve + Sor + Lejátszás az utolsó sorban + Lejátszás a következő sorban + A YouTube nem tudta megjeleníteni a sort. + A sor nem kész erre a videóra. Nyissa meg a videolejátszót, majd próbálja újra. + Mix hozzáadása a sornak vagy Mix lejátszása? + Lejátszás + Hozzáadás a sorhoz + Mix betöltése… Csatorna megnyitása az élő avatarból Megakadályozza, hogy az élő közvetítés megnyíljon a csatorna avatarjára koppintva @@ -853,8 +877,11 @@ A Beállítások eléréséhez engedélyezze a Beállítások gombot az Általá Animálja az átmeneteket a lapok között váltáskor Kapcsold ki az automatikus elrejtést a navigációs sávra A navigációs sáv látható marad a görgetés közben - Átlátszó navigációs sáv letiltása - Teljesen átlátszatlaná teszi a navigációs és az állapotsort + Navigációs sáv stílusa + Alapértelmezett + Félt पारátlan + Átlátszó + Átlátszatlan Eszközevek sorozata Válassza ki, mely komponensek jelenjenek meg az eszköztáron Átküldés gomb elrejtése diff --git a/patches/src/main/resources/addresources/values-hy-rAM/youtube/strings.xml b/patches/src/main/resources/addresources/values-hy-rAM/youtube/strings.xml index 174f25cd1a..1a02f7efd7 100644 --- a/patches/src/main/resources/addresources/values-hy-rAM/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-hy-rAM/youtube/strings.xml @@ -76,7 +76,9 @@ Second \"item\" text" + + diff --git a/patches/src/main/resources/addresources/values-in-rID/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-in-rID/shared-youtube/strings.xml index 133951c2ff..c14ff880e0 100644 --- a/patches/src/main/resources/addresources/values-in-rID/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-in-rID/shared-youtube/strings.xml @@ -151,6 +151,13 @@ Untuk masuk, tekan lanjutkan di bawah ini" Sembunyikan tombol Sebelumnya dan Berikutnya di notifikasi media, layar kunci, dan kontrol headphone Nonaktifkan bilah geser Mencegah penyeretan bilah pencarian di layar kunci dan panel notifikasi + Geser dengan tombol Sebelumnya & Berikutnya + Mengubah kontrol untuk notifikasi media, layar kunci, dan headphone menjadi geser alih-alih mengganti video + Durasi rewind + Detik untuk rewind dengan tombol Sebelumnya + Durasi fast forward + Detik untuk fast-forward dengan tombol Berikutnya + Durasi pencarian harus antara 1 dan %s detik. Menggunakan default Nonaktifkan protokol QUIC diff --git a/patches/src/main/resources/addresources/values-in-rID/youtube/strings.xml b/patches/src/main/resources/addresources/values-in-rID/youtube/strings.xml index 21e6cc2de6..095f7d1849 100644 --- a/patches/src/main/resources/addresources/values-in-rID/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-in-rID/youtube/strings.xml @@ -473,6 +473,16 @@ Setelah video dengan tombol Ask dibuka satu kali, fitur ini berfungsi untuk semu Unduh video, podcast, atau lagu menggunakan aplikasi pihak ketiga Tampilkan tombol unduh tumpang tindih pemutar Menampilkan tombol di pemutar video yang membuka pengunduh eksternal Anda + Unduh cepat saat diketuk + Perintah untuk mengunduh di latar belakang. Ketuk dan tahan untuk membuka pengunduh. Berfungsi dengan YTDLnis dan YTindir + Tandai unduhan sebagai sudah ditonton + Video yang diunduh menggunakan pengunduh eksternal akan ditambahkan ke riwayat tontonan YouTube Anda + Full HD (1080p) + Sedang (192 kbps) + Unduhan cepat + Video + Audio + Semua opsi Terjemahan sulih suara Baca teks keterangan video dengan suara keras menggunakan fitur text-to-speech bawaan perangkat, yang disinkronkan dengan pemutaran video @@ -588,14 +598,14 @@ Info: Mungkin tidak berfungsi pada siaran langsung" Antrean disimpan setelah memulai ulang aplikasi Tukar Tindakan Simpan dan Antrean Ketuk untuk pilihan antrean, ketuk dan tahan untuk menyimpan ke Tonton Nanti - Gantikan tombol antrean feed flyout - Membuka antrean Morphe daripada antrean YouTube + + Jenis Putar video berikutnya dalam antrean + Bawaan + Antrean Native + Antrean Morphe + Antrean Morphe sisi klien Tambahkan item menu antrean feed flyout Menambahkan item flyout tambahan. Aktifkan ini jika \"Tambahkan ke antrean\" tidak ditampilkan - Antrean gaya Premium - "Menggantikan antrean Morphe dengan antrean yang berada di perangkat, dapat diputar sendiri, dan dapat diurutkan ulang dengan menyeret - -Info: Video berikutnya dalam antrean hanya mulai diputar saat YouTube sedang berada di layar" Antrean Antrean (%s) Bersihkan semua @@ -603,7 +613,7 @@ Info: Video berikutnya dalam antrean hanya mulai diputar saat YouTube sedang ber Gunakan Putar berikutnya dalam antrean pada video apa pun Sedang diputar Putar berikutnya dalam antrean - Putar terakhir di antrean + Putar video terakhir dalam antrean Urutkan ulang Hapus Ditambahkan ke antrean @@ -640,6 +650,20 @@ Info: Video berikutnya dalam antrean hanya mulai diputar saat YouTube sedang ber Hapus dan muat ulang video Buka antrean Simpan antrean ke daftar putar + + Daftar Putar disimpan + Disimpan ke %s + Hapus antrean + Antrean dibersihkan + Antrean + Putar video terakhir dalam antrean + Putar video berikutnya dalam antrean + YouTube tidak dapat menampilkan antrean. + Antrean belum siap untuk video ini. Buka pemutar video lalu coba lagi. + Tambahkan Mix ke antrean atau tonton Mix? + Tonton + Tambahkan ke antrean + Memuat Mix… Buka saluran dari avatar siaran langsung Mencegah siaran langsung terbuka saat mengetuk avatar saluran @@ -853,8 +877,11 @@ Untuk mengakses Setelan, aktifkan tombol Setelan melalui Umum → Bilah Alat → Menganimasikan transisi saat beralih antar tab Nonaktifkan otomatis menyembunyikan bilah navigasi Menjaga bilah navigasi tetap terlihat saat menggulir - Nonaktifkan bilah navigasi transparan - Membuat bilah navigasi dan bilah status menjadi sepenuhnya buram + Gaya bilah navigasi + Bawaan + Translusen + Transparan + Buram Bilah alat Pilih komponen mana yang akan ditampilkan di bilah alat Sembunyikan tombol Transmisi diff --git a/patches/src/main/resources/addresources/values-is-rIS/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-is-rIS/shared-youtube/strings.xml index 6ab71c4f27..1b1878992c 100644 --- a/patches/src/main/resources/addresources/values-is-rIS/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-is-rIS/shared-youtube/strings.xml @@ -151,6 +151,13 @@ Til að skrá þig inn ýttu á halda áfram hér að neðan" Felur fyrri og næstu hnappa í spilunartilkynningu, á læsiskjá og í heyrnartólstýringum Slökkva á framvindustiku Kemur í veg fyrir að hægt sé að draga framvindustikuna á læsiskjánum og tilkynningaspjaldinu + Leita með takköm fyrir fyrra og næsta + Breytir stjórnun fyrir margmiðlunartilkynningu, lásaskjá og heyrnartól til að leisa frekar en að skipta um myndbönd + Tilbaka skref + Sekúndur til að baka með fyrra takkanum + Fram á við skref + Sekúndur til að fara fram á við með næsta takkanum + Leisir lengd verður að vera á milli 1 og %s sekúndna. Notar sjálfgefið Slökktu á QUIC samskiptareglum diff --git a/patches/src/main/resources/addresources/values-is-rIS/youtube/strings.xml b/patches/src/main/resources/addresources/values-is-rIS/youtube/strings.xml index 1cf07d340a..0433bdc740 100644 --- a/patches/src/main/resources/addresources/values-is-rIS/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-is-rIS/youtube/strings.xml @@ -463,6 +463,16 @@ Eftir að myndband með Ask hnappi hefur verið opnað einu sinni, virkar eiginl Niðurhalaðu myndbönd, hlaðvarpa eða lög með niðurhalssforritum frá þriðja aðila. Sýna takka fyrir niðurhald ofanborðs spilara Sýnir takka í myndbands-spilara sem opnar ytri niðurhalara + Hraðhleðsla með einu snerti + Beðir um að hlaða niður í bakgrunni. Snertu og haltuu inni til að opna hleðsluforritið. Virkar með YTDLnis og YTindir + Merkja hlaðnir myndbönd sem horft + Myndbönd sem hlaðin eru niður með utanaumhúss hleðsluforriti eru bætt við horfunsögu YouTube + Full HD (1080p) + Miðlungs (192 kbps) + Fljótleg niðurhal + Myndband + Hljóð + Allar stillingar Þýðing á raddsegulmagni Les upp undirtitla myndbandsins með texta-til-tala á tækinu, samhæmt með endursendingu @@ -576,14 +586,14 @@ API lykillinn þinn ætti aldrei að deila. Allir með aðgang að honum geta no Biðlisti er vistaður eftir endurræsingar appinu Skipta um röð á Vista og Biðlista aðgerðum Snertu fyrir biðraðarvalkosti, snertu og haltu til að vista í Horfa síðar. - Rafmagnúðu biðröð fljótfallsins - Opnar Morphe biðröðina í stað YouTube biðröðarinnar + + Spila næst í biðröð + Sjálfgefið + Innfædd biðröð + Morphe biðröð + Morphe biðröð á viðskiptavinahlið Bæta við flugvalsatriði fyrir fréttastraum Bætir við auka flugvalsatriði. Virkjaðu þetta ef \"Bæta í biðröð\" er ekki sýnt - Biðröð í Premium-stíl - "Skiptir út Morphe-biðröðinni fyrir biðröð sem er geymd á tækinu, spilast af sjálfu sér og er hægt að endurraða með því að draga - -Upplýsingar: Næsta myndband í biðröðinni byrjar aðeins meðan YouTube er á skjánum" Biðröð Biðröð (%s) Hreinsa allt @@ -628,6 +638,20 @@ Upplýsingar: Næsta myndband í biðröðinni byrjar aðeins meðan YouTube er Fjarlægja og endurhlaða myndband Opna biðlista Vista biðlista í spilalista + + Ljóðlisti vistaður + Vistað í %s + Hreinsa biðröð + Biðröð hreinsuð + Biðröð + Spila síðast í biðröð + Spila næst í biðröð + YouTube gat ekki sýnt biðröðina. + Biðröðin er ekki tilbúin fyrir þetta myndband. Opnaðu spilunarpallinn og reyndu aftur. + Bæta Mix við biðröð eða horfa á Mix? + Horfa á + Bæta við biðröð + Hleður Mix… Opnaðu rás frá lifandi hlaut Kemur í veg fyrir að lifleg streyma opni þegar þeir ýta á hlaut rásarinnar @@ -839,8 +863,11 @@ Til að laga þetta skaltu kveikja á 'Spoofa myndstreymi' og breyta 'Sjálfgefn Hreyfir umhverfi þegar skipt er á milli flipa Lokað sjálfvirkum hópum Geymir stjórnarborðið sýnilegt á meðan skellt er - Óvirkja gjálflega stýristöng - Gerir stýristöngina og stöðustönguna algjörlega ógegnsæja + Stíll vistunarlínunnar + Sjálfgefið + Hálfgagnsætt + Gagnsætt + Ógagnsætt Tólaborð Veldu hvaða þættir birtast á verkfærastikunni Felur Cast hnappinn diff --git a/patches/src/main/resources/addresources/values-it-rIT/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-it-rIT/shared-youtube/strings.xml index 4956897779..a2e5b01f8f 100644 --- a/patches/src/main/resources/addresources/values-it-rIT/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-it-rIT/shared-youtube/strings.xml @@ -151,6 +151,13 @@ Per accedere, premi su Continua di seguito" Nasconde i pulsanti Precedente e Successivo nelle notifiche multimediali, nella schermata di blocco e per i controlli delle cuffie Disattiva barra di avanzamento Impedisce di trascinare la barra di avanzamento sulla schermata di blocco e nel pannello delle notifiche + Cerca con pulsanti Precedente & Successivo + Le modifiche controllano la notifica multimediale, lo schermo di blocco e le cuffie per cercare invece di cambiare video + Durata rimandi + Secondi da retrocedere con il pulsante Precedente + Durata avanzamento rapido + Secondi da avanzare rapidamente con il pulsante Successivo + La durata della ricerca deve essere compresa tra 1 e %s secondi. Utilizzo dei valori predefiniti Disabilita il protocollo QUIC diff --git a/patches/src/main/resources/addresources/values-it-rIT/youtube/strings.xml b/patches/src/main/resources/addresources/values-it-rIT/youtube/strings.xml index 9686658474..efebb01ec0 100644 --- a/patches/src/main/resources/addresources/values-it-rIT/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-it-rIT/youtube/strings.xml @@ -473,6 +473,16 @@ Dopo aver aperto una volta un video con il pulsante Chiedi, la funzionalità fun Scarica video, podcast o canzoni con app di download di terze parti Mostra il pulsante per il download della sovrapposizione del lettore Mostra un pulsante nel lettore video che apre il tuo downloader esterno + Download rapido con un tocco + Richiede il download in background. Tieni premuto per aprire il downloader. Compatibile con YTDLnis e YTindir + Contrassegna i download come visti + I video scaricati con un downloader esterno vengono aggiunti alla cronologia di YouTube + Full HD (1080p) + Medio (192 kbps) + Download rapido + Video + Audio + Tutte le opzioni Traduzione voice over Leggi ad alta voce i sottotitoli del video utilizzando la sintesi vocale del dispositivo, sincronizzata con la riproduzione @@ -588,14 +598,14 @@ Avviso: Potrebbe non funzionare sui video in diretta" La coda viene mantenuta anche riavviando l\'app Scambia le azioni Salva e Metti in coda Tocca per le opzioni della coda, tocca e tieni premuto per salvare in \"Guarda più tardi\" - Sostituisci pulsante coda riepilogo feed - Apre la coda di Morphe invece della coda di YouTube + + Tipo riproduzione successiva nella coda + Predefinita + Coda nativa + Coda Morphe + Lato client della coda Morphe Aggiungi voce di menu a comparsa per la coda del feed Aggiunge un elemento aggiuntivo al menu a comparsa. Attiva questa opzione se \"Aggiungi alla coda\" non viene mostrato - Coda in stile Premium - "Sostituisce la coda di Morphe con una che risiede sul dispositivo, procede da sola e può essere riordinata tramite trascinamento - -Info: il video successivo in coda viene avviato solo mentre YouTube è sullo schermo" Coda Coda (%s) Cancella tutto @@ -640,6 +650,20 @@ Info: il video successivo in coda viene avviato solo mentre YouTube è sullo sch Rimuovi e ricarica video Apri coda Salva coda nella playlist + + Playlist salvata + Salvato in %s + Svuota coda + Coda svuotata + Coda + Riproduci ultimo nella coda + Riproduci successivo nella coda + YouTube non è riuscito a visualizzare la coda. + La coda non è pronta per questo video. Apri il lettore video e riprova. + Aggiungi Mix alla coda o guarda Mix? + Guarda + Aggiungi alla coda + Caricamento Mix… Apri il canale dall\'avatar in diretta Impedisce l\'apertura del video in diretta toccando l\'avatar del canale @@ -853,8 +877,11 @@ Per accedere alle Impostazioni, abilita il pulsante delle Impostazioni tramite G Anima le transizioni quando si passa tra le schede Disabilita la barra di navigazione a scomparsa automatica Mantiene la barra di navigazione visibile durante lo scorrimento - Disabilita la barra di navigazione traslucida - Rende la barra di navigazione e di stato completamente opache + Stile della barra di navigazione + Predefinita + Traslucido + Trasparente + Opaco Barra degli strumenti Scegli quali componenti mostrare nella barra degli strumenti Nascondi il pulsante Trasmetti diff --git a/patches/src/main/resources/addresources/values-iw-rIL/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-iw-rIL/shared-youtube/strings.xml index 63d0b6d2f7..2ce288d602 100644 --- a/patches/src/main/resources/addresources/values-iw-rIL/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-iw-rIL/shared-youtube/strings.xml @@ -151,6 +151,13 @@ Second \"item\" text" מסתיר את כפתורי הקודם והבא בהתראת המדיה, במסך הנעילה ובפקדי האוזניות השבת את סרגל ההתקדמות מונע גרירה של סרגל ההתקדמות במסך הנעילה ובלוח ההתראות + חיפוש באמצעות כפתורי הקודם & הבא + שינוי בקריאות של התראות מדיה, מסך נעילה ואוזניות לחיפוש במקום החלפת סרטונים + אורך הנדסה לאחור + שניות להנדסה לאחור עם כפתור הקודם + אורך קידום מהיר + שניות לקידום מהיר עם כפתור הבא + אורך החיפוש חייב להיות בין 1 ל-%s שניות. בשימוש בברירת מחדל השבת פרוטוקול QUIC diff --git a/patches/src/main/resources/addresources/values-iw-rIL/youtube/strings.xml b/patches/src/main/resources/addresources/values-iw-rIL/youtube/strings.xml index 57ccd4e134..c2eb2c9ab6 100644 --- a/patches/src/main/resources/addresources/values-iw-rIL/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-iw-rIL/youtube/strings.xml @@ -473,6 +473,16 @@ Second \"item\" text" הורד סרטונים, פודקאסטים או שירים באמצעות אפליקציות הורדה של צד שלישי הצג כפתור הורדה לשכבת הביצוען מציג כפתור בעתני הוידאו שפותח את מנהל ההורדות החיצוני שלך + הורדה מהירה בנגיעה + מציע להוריד ברקע. לחיצה ארוכה תפתח את תוכנת ההורדה. עובד עם YTDLnis ו-YTindir + סמן הורדות כנצפות + סרטונים שהורדו באמצעות תוכנת הורדה חיצונית יתווספו להיסטוריית הצפייה שלך ב-YouTube + Full HD (1080p) + בינוני (192 kbps) + הורדה מהירה + סרטון + שמע + כל האפשרויות תרגום דיבוב קריאת כתוביות הסרטון בקול רם באמצעות טקסט לדיבור מקומי, בסנכרון עם ההפעלה @@ -588,14 +598,14 @@ Second \"item\" text" התור נשמר גם לאחר הפעלה מחדש של האפליקציה החלף בין פעולות שמירה לתור הקש כדי לראות אפשרויות תור, הקש והחזק כדי לשמור לצפייה מאוחרת - כפתור החלפת תור הפיד - פותח את תור ה-Morphe במקום את התור של YouTube + + נגן הבא בתור + ברירת מחדל + תור מקורי + תור Morphe + לקוח תור Morphe בצד המשתמש הוסף הזנה לתור מוסיף פריט תפריט מוקפץ נוסף. הפעל זאת אם \'הוסף לתור\' אינו מוצג - תור בסגנון פרימיום - "מחליף את התור של Morphe בתור שמאוחסן במכשיר, מתנגן מעצמו, וניתן לסדר אותו מחדש על ידי גרירה - -מידע: הסרטון הבא בתור מתחיל להתנגן רק בזמן ש-YouTube מוצג על המסך" תור תור (%s) נקה הכל @@ -640,6 +650,20 @@ Second \"item\" text" הסר וטען מחדש וידאו פתח תור שמור תור לרשימת השמעה + + רשימת ניגונים נשמרה + נשמר ל%s + נקה תור + התור נוקה + תור + נגן את האחרון בתור + נגן הבא בתור + YouTube לא הצליח להציג את התור. + התור אינו מוכן עבור וידאו זה. פתח את נגן הוידאו ונסה שוב. + להוסיף Mix לתור או לצפות ב-Mix? + צפה + הוסף לתור + טוען Mix… פתח ערוץ מאווטאר חי מונע את פתיחת השידור החי בעת הקשה על האווטאר של הערוץ @@ -853,8 +877,11 @@ Second \"item\" text" מבצע אנימציות מעברים בעת מעבר בין כרטיסיות השבת הסתרת אוטומטית של סרגל הניווט שומר את סרגל הניווט גלוי בזמן גלילה - השבת שורת ניווט שקופה - הופך את שורת הניווט ושורת המצב לאטומות לחלוטין + סגנון סרגל ניווט + ברירת מחדל + חצי שקוף + שקוף + אטום סרגל כלים בחר אילו רכיבים יופיעו בסרגל הכלים הסתר לחצן העברה diff --git a/patches/src/main/resources/addresources/values-ja-rJP/music/strings.xml b/patches/src/main/resources/addresources/values-ja-rJP/music/strings.xml index 37b65fecfa..7e58f3f759 100644 --- a/patches/src/main/resources/addresources/values-ja-rJP/music/strings.xml +++ b/patches/src/main/resources/addresources/values-ja-rJP/music/strings.xml @@ -520,7 +520,7 @@ SABR再生(動画ストリームのスプーフがオフ)" コピーボタンを表示 タップして元の歌詞をプレーンテキストとしてコピー、長押しで歌詞ソースファイルをダウンロードディレクトリに保存 翻訳ボタンを表示 - タップして翻訳を切替、長押しで翻訳のみ使用。プロバイダーの翻訳、Google翻訳、またはOpenAI互換APIを使用 + タップすると翻訳を表示 / 非表示にします。長押しすると翻訳のみを表示するモードに切り替えます。翻訳には、歌詞配信サービスが提供する翻訳、Google 翻訳、または OpenAI 互換 API を使用します ローマ字化ボタンを表示 タップしてローマ字表記を切替、長押しでローマ字表記のみ使用。プロバイダーのローマ字表記、Google翻訳、またはOpenAI互換APIを使用 ローカルローマ字表記とアライメント @@ -555,8 +555,8 @@ SABR再生(動画ストリームのスプーフがオフ)" 曲名、アーティスト、アルバムに適用される正規表現です。歌詞の検索前と、2つの曲名が一致するかどうかの判定時に使用されます。空にすると両方が無効になります。デフォルトでは、\"Official Video\"、\"Remaster\"、\"【插曲】\"、\"- Topic\" などのよくある不要な部分を除去します。 歌詞フィルタの正規表現 元の歌詞テキストのみに適用される正規表現です(翻訳やローマ字表記には適用されません)。無効にする場合は空欄にしてください。 - 情報行のマーカー - 歌詞が誤って下部に振り分けられてしまう場合は、この設定を調整または消去してください。マーカーはカンマ区切りで指定し、マーカーの直後にコロン : が続く行を下部のクレジット情報として表示します。無効にする場合は空欄にしてください + クレジット行の判別マーカー + マーカーをカンマ区切りで入力してください。行内にマーカーを含み、その直後にコロン (:) が続く行を、クレジット情報として最下部に表示します。空欄にするとこの機能自体が無効になります。歌詞が誤って最下部に振り分けられてしまう場合は、この設定の値 (文字列) を調整するか空欄にしてください 文字サイズ 歌詞の文字サイズ 全体のタイミングオフセット diff --git a/patches/src/main/resources/addresources/values-ja-rJP/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-ja-rJP/shared-youtube/strings.xml index 5705cadc5a..1160075de3 100644 --- a/patches/src/main/resources/addresources/values-ja-rJP/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ja-rJP/shared-youtube/strings.xml @@ -151,6 +151,13 @@ YouTube Premium を利用している場合、この設定は必要ない場合 通知パネルとロック画面に表示されるプレーヤーの前の動画ボタンと次の動画ボタンを非表示にします。ヘッドホンからの操作によって前の動画 / 次の動画へ移動する機能も無効にします シークバーでのシーク操作を無効化 通知パネルとロック画面に表示されるプレーヤーのシークバーでのシーク操作を無効にします + 次 & 前ボタンでシーク + メディア通知、ロック画面、ヘッドフォンの操作を、動画切替ではなくシークに変更します + 巻き戻し時間 + 前の動画ボタンでの巻き戻し秒数 + 早送り時間 + 次の動画ボタンでの早送り秒数 + シーク時間は1〜%s秒の間で指定してください。デフォルト値を使用します QUIC プロトコルを無効化 diff --git a/patches/src/main/resources/addresources/values-ja-rJP/youtube/strings.xml b/patches/src/main/resources/addresources/values-ja-rJP/youtube/strings.xml index a9364eeb53..860079ec54 100644 --- a/patches/src/main/resources/addresources/values-ja-rJP/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ja-rJP/youtube/strings.xml @@ -474,6 +474,16 @@ AI について扱っているだけのチャンネルや、動画の一部に A サードパーティ製ダウンロード アプリで、動画、ポッドキャスト、楽曲をダウンロードします プレーヤー オーバーレイに外部ダウンローダーを呼び出すボタンを表示します ビデオ プレーヤーに、外部ダウンローダーを開くボタンを表示します + タップでクイックダウンロード + バックグラウンドでのダウンロードを促します。長押しするとダウンローダーが開きます。YTDLnisおよびYTindirで動作します + ダウンロード済みとして既読にする + 外部ダウンローダーでダウンロードされたビデオがYouTubeの視聴履歴に追加されます + フルHD(1080p) + 中(192kbps) + クイックダウンロード + 動画 + オーディオ + すべてのオプション 吹き替え翻訳 動画字幕を翻訳し、デバイス上のテキスト読み上げ機能で字幕の翻訳を字幕に同期しながら読み上げます @@ -589,14 +599,14 @@ APIキーは決して共有しないでください。アクセス権を持つ アプリを終了または再起動したときにキューを保持します 「後で見るに保存」ボタンの動作を入れ替える タップするとキュー オプションを表示します。長押しすると、動画を [後で見る] に保存します - フィードのフライアウト メニューのキューの項目の動作を上書き - YouTube のキューの代わりに Morphe のキューを開きます + + キューに次へ再生するタイプ + デフォルト + ネイティブキュー + Morphe キュー + Morpheキュー(クライアント側) フィードのフライアウト メニューにキューの項目を追加 フィードのフライアウト メニューにキューの項目を追加します。「キューに追加」が表示されない場合に、この機能を有効にしてください - Premium 風キュー - "Morphe キューを Premium 風キューに置き換えます。Premium 風キューは、デバイスに保存される、ドラッグによる並び替えが可能なキューであり、キュー内の動画を自動再生します - -制限事項: キューの次の動画が自動的に再生されるのは YouTube が画面に表示されている間に限られます" キュー キュー (%s) すべて消去 @@ -641,6 +651,20 @@ APIキーは決して共有しないでください。アクセス権を持つ 削除して動画を再読み込み キューを開く キューをプレイリストとして保存 + + プレイリストが保存されました + %s に保存済み + キューをクリア + キューをクリアしました + キュー + キューの最後に追加 + キューの最初に追加 + YouTube はキューを表示できませんでした。 + この動画にはキューが準備されていません。ビデオプレーヤーを開いて再度お試しください。 + Mix をキューに追加しますか、それとも Mix を視聴しますか? + 視聴 + キューに追加 + Mix を読み込み中… ライブ配信中のチャンネル アイコンからチャンネルを開く ライブ配信中のチャンネル アイコンをタップしたときにライブ配信ではなくチャンネル ページを開きます @@ -854,8 +878,11 @@ APIキーは決して共有しないでください。アクセス権を持つ タブの切り替えをアニメーションで表現します ナビゲーション バーの自動非表示を無効化 スクロール中にナビゲーション バーを常に表示します - ナビゲーション バーの半透明表示を無効化 - ナビゲーション バーとステータスバーが透けないようになります + ナビゲーション バーのスタイル + デフォルト + 半透明 + 透明 + 不透明 ツールバー ツールバーに表示するコンポーネントを設定します キャストボタンを非表示 diff --git a/patches/src/main/resources/addresources/values-ka-rGE/youtube/strings.xml b/patches/src/main/resources/addresources/values-ka-rGE/youtube/strings.xml index a74924e0d8..f110ddefa1 100644 --- a/patches/src/main/resources/addresources/values-ka-rGE/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ka-rGE/youtube/strings.xml @@ -76,7 +76,10 @@ Second \"item\" text" + + ნაგულისხმები + @@ -117,6 +120,7 @@ Second \"item\" text" + ნაგულისხმები diff --git a/patches/src/main/resources/addresources/values-kk-rKZ/youtube/strings.xml b/patches/src/main/resources/addresources/values-kk-rKZ/youtube/strings.xml index 174f25cd1a..1a02f7efd7 100644 --- a/patches/src/main/resources/addresources/values-kk-rKZ/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-kk-rKZ/youtube/strings.xml @@ -76,7 +76,9 @@ Second \"item\" text" + + diff --git a/patches/src/main/resources/addresources/values-km-rKH/youtube/strings.xml b/patches/src/main/resources/addresources/values-km-rKH/youtube/strings.xml index 9111351c42..236d505cea 100644 --- a/patches/src/main/resources/addresources/values-km-rKH/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-km-rKH/youtube/strings.xml @@ -81,6 +81,7 @@ Second \"item\" text" + វីដេអូ ផ្ទាល់ខ្លួន @@ -91,8 +92,10 @@ Second \"item\" text" + ដកចេញ + diff --git a/patches/src/main/resources/addresources/values-kmr-rTR/youtube/strings.xml b/patches/src/main/resources/addresources/values-kmr-rTR/youtube/strings.xml index f061257f32..86a2c93858 100644 --- a/patches/src/main/resources/addresources/values-kmr-rTR/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-kmr-rTR/youtube/strings.xml @@ -452,8 +452,8 @@ Info: May not work on live streams" Lîsteyê di nav beşan de bidomîne Lîste piştî destkirina serî lêve tê parastin Veguherîneya kirina tomarkirinê û kirina rêzê - Bi serkirîkirina pêşniyarên rêzika peydakirinê - Lihevîneya Morphe li şûna hejmara YouTube\'yê vekirin + + Bixweber Bendeleya rêza flyout-a feed-ê zêde bike Bendeleya flyout-ê ya zêde zêde dike. Heke \"Zêde bikeye rêzê\" nîşan neyê, vê çalak bike. Lêkirin @@ -481,6 +481,7 @@ Info: May not work on live streams" Derxistina ji lîsteyê kirin û dîsa bar kirin Lîsteyê vekirin Lîsteyê li lîsteyê parastin + Open channel from live avatar Prevents the live stream from opening when tapping the channel\'s avatar @@ -661,8 +662,7 @@ Divê 'kukiyên jêrnivîsan' bên peyda kirin." Tevahîyên veşêrinî dema ku di navbera taban de guherîn Kêmbara otomatîk a kêmbara navîgasyonê qeybidine. Desteyê nivîskî bi hevsengî neşêre dema ku bi hêsanî tîkê dike - Barê navîgasyonê ya nîvzelal neçalak bike - Barê navîgasyonê û barê rewşê bi temamî nezelal dike + Bixweber Rêz Hilbijêre ku kîjan pêkhateyên di menuya veşêrîn de xuya dibe Butona birêveberînê veşêrin diff --git a/patches/src/main/resources/addresources/values-kn-rIN/youtube/strings.xml b/patches/src/main/resources/addresources/values-kn-rIN/youtube/strings.xml index 22203a72b0..193b040db3 100644 --- a/patches/src/main/resources/addresources/values-kn-rIN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-kn-rIN/youtube/strings.xml @@ -78,7 +78,9 @@ Second \"item\" text" + + diff --git a/patches/src/main/resources/addresources/values-ko-rKR/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-ko-rKR/shared-youtube/strings.xml index 8df002543e..c221c1d203 100644 --- a/patches/src/main/resources/addresources/values-ko-rKR/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ko-rKR/shared-youtube/strings.xml @@ -151,6 +151,13 @@ YouTube Premium 사용자는 이 설정이 필요하지 않을 수 있습니다" 미디어 알림, 잠금 화면 그리고 헤드폰 컨트롤에서 이전 및 다음 버튼을 숨깁니다 재생바 비활성화하기 잠금 화면 및 알림 패널에서 재생바를 드래그하지 못하도록 합니다 + 이전 및 다음 버튼으로 탐색하기 + 미디어 알림, 잠금 화면 및 헤드폰에서 비디오 변경 대신 탐색하도록 제어 + 되감기 길이 + 이전 버튼으로 되감기할 초 단위 + 앞당기기 길이 + 다음 버튼으로 앞당길 초 단위 + 탐색 길이는 1~%s초여야 합니다. 기본값 사용 QUIC 프로토콜 비활성화하기 diff --git a/patches/src/main/resources/addresources/values-ko-rKR/youtube/strings.xml b/patches/src/main/resources/addresources/values-ko-rKR/youtube/strings.xml index 476e6fb507..00fc208917 100644 --- a/patches/src/main/resources/addresources/values-ko-rKR/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ko-rKR/youtube/strings.xml @@ -473,6 +473,16 @@ Second \"item\" text" 서드파티 다운로드 앱을 사용하여 동영상, 팟캐스트 또는 노래를 다운로드할 수 있습니다 플레이어 오버레이 다운로드 버튼 표시하기 동영상 플레이어에서 외부 다운로더를 실행하는 버튼을 표시합니다 + 탭 한 번으로 빠른 다운로드하기 + 백그라운드에서 다운로드할 수 있도록 프롬프트를 제공합니다. 길게 탭하면 다운로더가 열립니다. YTDLnis 및 YTindir와 호환됩니다 + 다운로드한 영상을 시청 완료로 표시하기 + 외부 다운로더를 사용하여 다운로드한 동영상을 YouTube 시청 기록에 추가합니다 + Full HD (1080p) + 중간 (192 kbps) + 빠른 다운로드 + 동영상 + 오디오 + 모든 옵션 음성 번역 기기의 텍스트 음성 변환(TTS)을 사용하여 재생에 맞춰 동기화된 동영상 자막을 읽어줍니다 @@ -590,14 +600,14 @@ API 키는 절대로 다른 사람과 공유해서는 안 됩니다. API 키에 앱을 다시 시작한 후에도 현재 재생목록이 유지됩니다 저장과 현재 재생목록 작업 교환하기 버튼을 탭하여 현재 재생목록 옵션을 실행할 수 있고, 길게 탭하면 \'나중에 볼 동영상\'에 저장됩니다 - \'현재 재생목록\' 피드 메뉴 재정의하기 - \'현재 재생목록에서 다음 순서로 재생\' 메뉴를 탭하면 YouTube 현재 재생목록 대신 Morphe 현재 재생목록이 실행됩니다 + + \'현재 재생목록에서 다음 순서로 재생\' 유형 + 기본값 + 기본 현재 재생목록 + Morohe 현재 재생목록 + Morohe 현재 재생목록 클라이언트 사이드 \'현재 재생목록\' 메뉴 항목 추가하기 추가적인 메뉴 구성요소 항목을 추가합니다. \'현재 재생목록에서 다음 순서로 재생\' 메뉴가 표시되지 않는 경우에 이 옵션을 활성화하세요 - Premium 스타일 현재 재생목록 - "Morphe 현재 재생목록을 기기에 저장되고 자동으로 재생되며, 드래그하여 순서를 변경할 수 있는 현재 재생목록으로 대체합니다 - -알림: 현재 재생목록에 있는 다음 동영상은 YouTube가 화면에 표시되어 있을 때만 재생이 시작됩니다" 현재 재생목록 현재 재생목록 (%s) 모두 지우기 @@ -642,6 +652,20 @@ API 키는 절대로 다른 사람과 공유해서는 안 됩니다. API 키에 현재 재생목록에서 제거하고 동영상 새로고침 현재 재생목록 열기 현재 재생목록을 재생목록에 저장 + + 현재 재생목록이 저장되었습니다 + %s에 저장되었습니다 + 현재 재생목록 지우기 + 현재 재생목록이 지워졌습니다 + 현재 재생목록 + 현재 재생목록의 마지막으로 재생 + 현재 재생목록에서 다음 순서로 재생 + YouTube에서 현재 재생목록을 표시할 수 없습니다 + 이 동영상에 대한 현재 재생목록이 준비되지 않았습니다. 동영상 플레이어를 열고 다시 시도하세요 + 믹스를 현재 재생목록에 추가하시겠습니까, 아니면 시청하시겠습니까? + 시청 + 패치 목록에 추가 + 믹스를 불러오는 중… 라이브 아바타 채널 열기 채널 아바타를 탭했을 경우에 실시간 스트림이 재생되지 않도록 합니다 @@ -855,8 +879,11 @@ API 키는 절대로 다른 사람과 공유해서는 안 됩니다. API 키에 하단바에서 탭 전환 시 애니메이션을 표시합니다 하단바 자동 숨기기 비활성화하기 피드에서 스크롤하는 경우에 하단바를 숨기지 않습니다 - 반투명 하단바 비활성화하기 - 하단바와 상태바를 완전히 불투명하게 만듭니다 + 하단바 스타일 + 기본값 + 반투명 + 투명 + 불투명 툴바 툴바에 표시할 구성 요소를 설정할 수 있습니다 크롬캐스트 버튼 숨기기 diff --git a/patches/src/main/resources/addresources/values-ky-rKG/youtube/strings.xml b/patches/src/main/resources/addresources/values-ky-rKG/youtube/strings.xml index be5b5a2e3d..774c80d9ff 100644 --- a/patches/src/main/resources/addresources/values-ky-rKG/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ky-rKG/youtube/strings.xml @@ -79,6 +79,7 @@ Second \"item\" text" + Видео @@ -87,7 +88,9 @@ Second \"item\" text" + + diff --git a/patches/src/main/resources/addresources/values-lo-rLA/youtube/strings.xml b/patches/src/main/resources/addresources/values-lo-rLA/youtube/strings.xml index 174f25cd1a..1a02f7efd7 100644 --- a/patches/src/main/resources/addresources/values-lo-rLA/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-lo-rLA/youtube/strings.xml @@ -76,7 +76,9 @@ Second \"item\" text" + + diff --git a/patches/src/main/resources/addresources/values-lt-rLT/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-lt-rLT/shared-youtube/strings.xml index ed7d8df2cc..8562e74094 100644 --- a/patches/src/main/resources/addresources/values-lt-rLT/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-lt-rLT/shared-youtube/strings.xml @@ -151,6 +151,13 @@ Norėdami prisijungti, žemiau bakstelėkite „Tęsti“" Slėpia „Ankstesnis“ ir „Kitas“ mygtukus medijos pranešime, užrakintame ekrane ir ausinių valdikliuose Išjungti slankiklį Neleidžia vilkti slankiklio užrakintame ekrane ir pranešimų skydelyje + Ieškoti su mygtukais „Ankstesnis“ ir „Kitas” + Perjungia medijos pranešimo, užrakto ekrano ir ausinių valdymą: vietoj vaizdo įrašų keitimo bus atliekama paieška + Gręžimo ilgis + Ankstesnio mygtuko gręžimo sekundžių skaičius + Greitėjimo ilgis + Kitu mygtuku greitėjimo sekundžių skaičius + Paieškos ilgis turi būti nuo 1 iki %s sekundžių. Naudos numatytąjį Išjungti QUIC protokolą diff --git a/patches/src/main/resources/addresources/values-lt-rLT/youtube/strings.xml b/patches/src/main/resources/addresources/values-lt-rLT/youtube/strings.xml index 6d028a0ff7..3c1d6adb77 100644 --- a/patches/src/main/resources/addresources/values-lt-rLT/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-lt-rLT/youtube/strings.xml @@ -473,6 +473,16 @@ Kartą atvėrus vaizdo įrašą su mygtuku „Ask“, funkcija veikia su visais Atsisiųskite vaizdo įrašus, tinklalaides ar dainas trečiųjų šalių atsisiuntimo programėlėmis Rodyti peržiūros valdiklį atsisiuntimo mygtuką Vaizdo grotuvėje rodomas mygtukas, kuris atverčia jūsų išorinę atsisiuntimo programą + Greitas atsisiuntimas paspaudus + Klausia, ar atsisiųsti fone. Paspauskite ir palaikykite, kad atidarytumėte atsisiuntikliai. Veikia su YTDLnis ir YTindir + Pažymėti atsisiuntimus kaip peržiūrėtus + Su išoriniu atsisiuntikliu atsisiųsti vaizdo įrašai pridedami į jūsų YouTube peržiūrų istoriją + Pilnas HD (1080p) + Vidutinė (192 kbps) + Greitas atsisiuntimas + Vaizdo įrašai + Garsas + Visi nustatymai Balso vertimas Skaito vaizdo įrašo subtitrus garsiai naudodamas įrenginio teksto į kalbą sintezatorių, sinchronizuotą su atkūrimu @@ -588,14 +598,14 @@ Informacija: gali neveikti tiesioginių transliacijų atveju" Eilė išsaugoma iš naujo paleidus programą Sukeisti „Išsaugoti“ ir „Eilė“ veiksmus Bakstelėkite, kad atidarytumėte eilės parinktis; palaikykite, kad išsaugotumėte žiūrėti vėliau - Perrašyti kanalo išskleidžiamojo meniu eilės mygtuką - Atidaro Morphe eilę, o ne „YouTube“ eilę + + Groti kitą eilėje + Numatytasis + Natūrali eilė + Morphe eilė + Morphe eilės klientinė pusė Pridėti meniu elementą „Eilė“ prie sklaidos kanalo išskleidžiamojo meniu Prideda papildomą išskleidžiamojo meniu elementą. Įgalinkite, jei „Pridėti į eilę“ nerodomas - Premium stiliaus eilė - "Pakeičia Morphe eilę tokia, kuri yra įrenginyje, groja automatiškai ir kurią galima pertvarkyti vilkiant - -Info: kitas eilėje esantis vaizdo įrašas paleidžiamas tik tada, kai YouTube yra ekrane" Eilė Eilė (%s) Išvalyti viską @@ -640,6 +650,20 @@ Info: kitas eilėje esantis vaizdo įrašas paleidžiamas tik tada, kai YouTube Pašalinti ir iš naujo įkelti vaizdo įrašą Atidaryti eilę Išsaugoti eilę grojaraštyje + + Grojaraštis išsaugotas + Išsaugota %s + Valyti eilę + Eilė išvalyta + Eilė + Groti paskutinį eilėje + Groti kitą eilėje + „YouTube“ negali rodyti eilės. + Eilė dar nepasiruošusi šiam vaizdo įrašui. Atidarykite vaizdo įrašo grotuvą ir bandykite dar kartą. + Pridėti mišinį prie eilės ar žiūrėti mišinį? + Žiūrėti + Pridėti prie eilės + Įkeliama mišinys… Atidaryti kanalą iš tiesioginės transliacijos avataro Neleidžia tiesioginei transliacijai atsidaryti palietus kanalo avatarą @@ -853,8 +877,11 @@ Norėdami gauti prieigą prie nustatymų, įgalinkite Nustatymų mygtuką: Bendr Animuoja perėjimus keičiant skirtukus Išjungti automatinį naršymo juostos slėpimą Naršymo juosta išlieka matoma slenkant - Išjungti permatomą naršymo juostą - Padaro naršymo ir būsenos juostas visiškai nepermatomas + Naršymo juostos stilius + Numatytasis + Šviesu + Skaidrus + Nepermatomas Įrankių juosta Pasirinkite, kurie komponentai rodomi įrankių juostoje Slėpti „Cast“ mygtuką diff --git a/patches/src/main/resources/addresources/values-lv-rLV/youtube/strings.xml b/patches/src/main/resources/addresources/values-lv-rLV/youtube/strings.xml index 544c51dd50..fe862fe1a9 100644 --- a/patches/src/main/resources/addresources/values-lv-rLV/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-lv-rLV/youtube/strings.xml @@ -88,6 +88,7 @@ Second \"item\" text" + Video @@ -96,8 +97,10 @@ Second \"item\" text" + Noņemt + diff --git a/patches/src/main/resources/addresources/values-mai-rIN/youtube/strings.xml b/patches/src/main/resources/addresources/values-mai-rIN/youtube/strings.xml index bc1a10121f..95ee0ab966 100644 --- a/patches/src/main/resources/addresources/values-mai-rIN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-mai-rIN/youtube/strings.xml @@ -193,7 +193,9 @@ Second \"item\" text" भिडियोलाई सफलतापूर्वक \'पछि हेर्नको लागि\' प्लेलिस्टमा बचत गरियो यो भिडियो पहिले नै \'पछि हेर्नको लागि\' प्लेलिस्टमा बचत गरिएको छ यो भिडियोलाई \'पछि हेर्नको लागि\' प्लेलिस्टमा बचत गर्न सकिएन + + सटीक अनुरोध जेस्चर अक्षम करें diff --git a/patches/src/main/resources/addresources/values-mk-rMK/youtube/strings.xml b/patches/src/main/resources/addresources/values-mk-rMK/youtube/strings.xml index 9c3a6127b0..bdf7e2650b 100644 --- a/patches/src/main/resources/addresources/values-mk-rMK/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-mk-rMK/youtube/strings.xml @@ -267,8 +267,11 @@ Second \"item\" text" Видеото успешно е зачувано на плејлиста „Гледај подоцна“ Ова видео веќе е зачувано на плејлистата „Гледај подоцна“ Не може да се зачува ова видео во плејлистата „Гледај подоцна“ + + Стандардно Отстрани + Отворете канал од аватарот во живо Спречува отворање на ливниот пренос кога ќе допрете на аватарот на каналот @@ -431,6 +434,7 @@ Second \"item\" text" Анимира преоди кога се префрлувате меѓу картичките Исклучи автоматско скривање на навигацискиот панел Ја задржува лентата за навигација видлива додека лизгате + Стандардно Toolbar Изберете кои компоненти да се појавуваат во лентата со алатки Прикријте го копчето Cast diff --git a/patches/src/main/resources/addresources/values-mn-rMN/youtube/strings.xml b/patches/src/main/resources/addresources/values-mn-rMN/youtube/strings.xml index 174f25cd1a..1a02f7efd7 100644 --- a/patches/src/main/resources/addresources/values-mn-rMN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-mn-rMN/youtube/strings.xml @@ -76,7 +76,9 @@ Second \"item\" text" + + diff --git a/patches/src/main/resources/addresources/values-mr-rIN/youtube/strings.xml b/patches/src/main/resources/addresources/values-mr-rIN/youtube/strings.xml index c41c6d574a..5209d3977f 100644 --- a/patches/src/main/resources/addresources/values-mr-rIN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-mr-rIN/youtube/strings.xml @@ -429,8 +429,7 @@ OpenRouter.ai उघडण्यासाठी येथे टॅप करा सत्रांमध्ये रांग ठेवा ॲप रीस्टार्ट केल्यानंतर रांग ठेवल्या जाईल स्वॅप सेव्ह आणि क्यू ॲक्शन - ओव्हरराइड फीड फ्लायआउट क्यू बटण - Morphe रांग YouTube रांग ऐवजी उघडते + फीड फ्लायआउट रांग मेनू आयटम जोडा एक अतिरिक्त फ्लायआउट आयटम जोडते. \'रांगेत जोडा\' दिसत नसल्यास हे सक्षम करा. @@ -457,6 +456,7 @@ OpenRouter.ai उघडण्यासाठी येथे टॅप करा व्हिडिओ काढा आणि रीलोड करा क्यू उघडा प्लेलिस्टमध्ये क्यू जतन करा + लाइव्ह अवतारमधून चॅनेल उघडा चॅनेलच्या अवतारवर टॅप केल्यावर लाइव्ह स्ट्रीम उघडण्यापासून प्रतिबंधित करते diff --git a/patches/src/main/resources/addresources/values-ms-rMY/youtube/strings.xml b/patches/src/main/resources/addresources/values-ms-rMY/youtube/strings.xml index 1ac4a6437c..5503a8a854 100644 --- a/patches/src/main/resources/addresources/values-ms-rMY/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ms-rMY/youtube/strings.xml @@ -131,6 +131,7 @@ Second \"item\" text" + Video Tersuai @@ -141,9 +142,12 @@ Second \"item\" text" + + Lalai Keluarkan Baris gilir Morphe + @@ -211,6 +215,7 @@ Second \"item\" text" Jika mengubah tetapan ini tidak berkesan, cuba tukar ke mod penyamaran Sorokkan label butang pandu arah Sorokkan bar pandu arah + Lalai diff --git a/patches/src/main/resources/addresources/values-my-rMM/youtube/strings.xml b/patches/src/main/resources/addresources/values-my-rMM/youtube/strings.xml index 174f25cd1a..1a02f7efd7 100644 --- a/patches/src/main/resources/addresources/values-my-rMM/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-my-rMM/youtube/strings.xml @@ -76,7 +76,9 @@ Second \"item\" text" + + diff --git a/patches/src/main/resources/addresources/values-nb-rNO/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-nb-rNO/shared-youtube/strings.xml index f5fd0b091a..ccb0e8531c 100644 --- a/patches/src/main/resources/addresources/values-nb-rNO/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-nb-rNO/shared-youtube/strings.xml @@ -151,6 +151,13 @@ Trykk på Fortsett nedenfor for å logge på" Skjuler forrige- og neste-knappene i medievarslingen, på låseskjermen og i hodetelefonkontrollene. Deaktiver søkebjelken Forhindrer at søkebjelken kan dras på låseskjermen og i varslingspanelet. + Søk med & forrige og neste-knapper + Endrer kontrollene for meldinger om medieavspilling, låseskjerm og hodetelefoner til å søke i stedet for å bytte videoer + Tilbakespolingslengde + Antall sekunder å tilbakespole med Forrige-knappen + Fremoverspolingslengde + Antall sekunder å spole fremover med Neste-knappen + Søkelengden må være mellom 1 og %s sekunder. Bruker standardverdi Deaktiver QUIC-protokollen diff --git a/patches/src/main/resources/addresources/values-nb-rNO/youtube/strings.xml b/patches/src/main/resources/addresources/values-nb-rNO/youtube/strings.xml index 5ae342e944..09a03dfdf2 100644 --- a/patches/src/main/resources/addresources/values-nb-rNO/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-nb-rNO/youtube/strings.xml @@ -473,6 +473,16 @@ Etter at en video med Ask-knappen er åpnet én gang, fungerer funksjonen med al Last ned videoer, podkaster eller sanger med nedlastingsapper fra tredjeparter Vis nedlastingsknapp for avspilleroverlegg Viser en knapp i videospilleren som åpner den eksterne nedlasteren din + Hurtig nedlasting ved trykk + Oppfordringer om å laste ned i bakgrunnen. Trykk og hold for å åpne nedlasteren. Fungerer med YTDLnis og YTindir + Marker nedlastinger som sett + Videoer lastet ned med en ekstern nedlaster blir lagt til i din YouTube-visningshistorikk + Full HD (1080p) + Medium (192 kbps) + Rask nedlasting + Video + Lyd + Alle alternativer Taleoversettelse Les opp tekstingen til videoen ved hjelp av tekst-til-tale på enheten, synkronisert med avspillingen @@ -588,14 +598,14 @@ Info: Fungerer kanskje ikke på direktesendinger" Køen beholdes etter omstart av appen Bytt om «Lagre» og «Kø»-handlinger Trykk for køalternativer, trykk og hold for å lagre i Se senere - Overskriv kø-flyout-knappen for avspillingsliste - Åpner Morphe-køen i stedet for YouTube-køen + + Spill neste i køtype + Standard + Innkjørt kø + Morphe-kø + Morphe-kø klientside Legg til menyelement for kø i feed-undermeny Legger til et ekstra flyout-element. Aktiver dette hvis «Legg til i kø» ikke vises - Kø i Premium-stil - "Erstatter Morphe-køen med en kø som ligger lokalt på enheten, spiller gjennom av seg selv og kan omorganiseres ved å dra - -Info: Den neste videoen i køen starter bare mens YouTube er på skjermen" Kø Kø (%s) Tøm alle @@ -640,6 +650,20 @@ Info: Den neste videoen i køen starter bare mens YouTube er på skjermen"Fjern og last inn videoen på nytt Åpne køen Lagre køen til spillelisten + + Spilleliste lagret + Lagret til %s + Tøm køen + Køen tømt + Kø + Spill sist i køen + Spill neste i køen + YouTube kunne ikke vise køen. + Køen er ikke klar for denne videoen. Åpne videospilleren og prøv igjen. + Legg Mix til i kø eller se Mix? + Se + Legg til i køen + Last inn Mix… Åpne kanal fra live avatar Forhindrer at direktesendingen åpnes når du trykker på kanalens avatar @@ -853,8 +877,11 @@ For å nå Innstillinger, aktiver Innstillingsknappen via Generelt → Verktøyl Animerer overganger når du bytter mellom faner Deaktiver automatisk skjuling av navigasjonslinje Beholder navigeringslinjen synlig mens du ruller - Disable translucent navigation bar - Makes the navigation and status bar fully opaque + Stil for navigasjonsfelt + Standard + Gjennomsiktig + Gjennomsiktig + Ugjennomsiktig Verktøylinje Velg hvilke komponenter som vises i popup-menyen Skjul Kast-knapp diff --git a/patches/src/main/resources/addresources/values-ne-rIN/youtube/strings.xml b/patches/src/main/resources/addresources/values-ne-rIN/youtube/strings.xml index 174f25cd1a..1a02f7efd7 100644 --- a/patches/src/main/resources/addresources/values-ne-rIN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ne-rIN/youtube/strings.xml @@ -76,7 +76,9 @@ Second \"item\" text" + + diff --git a/patches/src/main/resources/addresources/values-nl-rNL/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-nl-rNL/shared-youtube/strings.xml index d9e1d3c01e..3b5389afa9 100644 --- a/patches/src/main/resources/addresources/values-nl-rNL/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-nl-rNL/shared-youtube/strings.xml @@ -145,6 +145,13 @@ Om in te loggen, druk je op 'doorgaan' onderaan" Verbergt de knoppen Vorige en Volgende in mediameldingen, op het vergrendelscherm en in de hoofdtelefoonbediening Schuifregelaar uitschakelen Voorkomt dat de schuifregelaar op het vergrendelscherm en in het meldingspaneel wordt versleept + Zoeken met knoppen Vorige & Volgende + Bedieningselementen voor mediemeldingen, vergrendelscherm en koptelefoon wijzigen om te zoeken in plaats van video\'s te veranderen + Spoellengte achteruit + Seconden om terug te spoelen met de vorige knop + Spoellengte vooruit + Seconden om vooruit te spoelen met de volgende knop + De zoeklengte moet tussen 1 en %s seconden liggen. Standaardwaarde wordt gebruikt Uitschakelen van het QUIC-protocol diff --git a/patches/src/main/resources/addresources/values-nl-rNL/youtube/strings.xml b/patches/src/main/resources/addresources/values-nl-rNL/youtube/strings.xml index 130d759ebd..5af0bab71a 100644 --- a/patches/src/main/resources/addresources/values-nl-rNL/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-nl-rNL/youtube/strings.xml @@ -473,6 +473,16 @@ Nadat een video met de Ask-knop eenmaal is geopend, werkt de functie voor alle v Download video\'s, podcasts of liedjes met apps van derden Downloadknop voor speleroverlay tonen Toont een knop in de videospeler die je externe downloader opent + Snel downloaden bij tikken + Vraagt om downloaden op de achtergrond. Tik en houd vast om de downloader te openen. Werkt met YTDLnis en YTindir + Downloads markeren als bekeken + Video\'s die met een externe downloader zijn gedownload, worden toegevoegd aan je YouTube-geschiedenis + Full HD (1080p) + Gemiddeld (192 kbps) + Snel downloaden + Video + Audio + Alle opties Spraakvertaling Lees ondertitels hardop voor met behulp van tekst-naar-spraak op het apparaat, gesynchroniseerd met het afspelen @@ -588,14 +598,14 @@ Info: Werkt mogelijk niet op livestreams" Wachtrij blijft behouden na het opnieuw starten van de app Verwissel de acties Opslaan en Wachtrij Tik voor wachtrijopties, tik en houd vast om op te slaan in Later bekijken - Vervang wachtrij flyout-knop - Opent de Morphe-wachtrij in plaats van de YouTube-wachtrij + + Volgende in wachtrij afspelen + Standaard + Standaard wachtrij + Morphe-wachtrij + Wachtrijclient van Morphe Feed-flyoutmenu-item toevoegen Voegt een extra flyout-item toe. Schakel dit in als \"Toevoegen aan wachtrij\" niet wordt weergegeven - Premium-stijlwachtrij - "Vervangt de Morphe-wachtrij door een wachtrij die op het apparaat staat, vanzelf afspeelt en door te slepen opnieuw kan worden gerangschikt - -Info: De volgende video in de wachtrij start alleen terwijl YouTube op het scherm staat" Wachtrij Wachtrij (%s) Alles wissen @@ -640,6 +650,20 @@ Info: De volgende video in de wachtrij start alleen terwijl YouTube op het scher Verwijderen en video opnieuw laden Open wachtrij Wachtrij opslaan naar afspeellijst + + Afspelplaylist opgeslagen + Opgeslagen in %s + Wachtrij wissen + Wachtrij gewist + Wachtrij + Laatste in wachtrij afspelen + Volgende in wachtrij afspelen + YouTube kan de wachtrij niet tonen. + De wachtrij is nog niet klaar voor deze video. Open de videospeler en probeer het opnieuw. + Mix toevoegen aan wachtrij of Mix bekijken? + Bekijken + Toevoegen aan wachtrij + Mix laden… Open kanaal vanaf live avatar Voorkomt dat de livestream wordt geopend wanneer je op de avatar van het kanaal tikt @@ -851,8 +875,11 @@ Om Instellingen te openen, schakel je de knop Instellingen in via Algemeen → G Maakt de navigatiebalk volledig ondoorzichtig in de lichte modus Automatisch verbergen van navigatiebalk uitschakelen Maakt de navigatiebalk volledig ondoorzichtig in de donkere modus - Transparante navigatiebalk uitschakelen - Maakt de navigatie- en statusbalk volledig ondoorzichtig + Stijl van de navigatiebalk + Standaard + Translucent + Transparant + Ondoorzichtig Werkbalk Kies welke componenten in de werkbalk verschijnen Verberg Cast-knop diff --git a/patches/src/main/resources/addresources/values-or-rIN/youtube/strings.xml b/patches/src/main/resources/addresources/values-or-rIN/youtube/strings.xml index 174f25cd1a..1a02f7efd7 100644 --- a/patches/src/main/resources/addresources/values-or-rIN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-or-rIN/youtube/strings.xml @@ -76,7 +76,9 @@ Second \"item\" text" + + diff --git a/patches/src/main/resources/addresources/values-pa-rIN/youtube/strings.xml b/patches/src/main/resources/addresources/values-pa-rIN/youtube/strings.xml index 174f25cd1a..1a02f7efd7 100644 --- a/patches/src/main/resources/addresources/values-pa-rIN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-pa-rIN/youtube/strings.xml @@ -76,7 +76,9 @@ Second \"item\" text" + + diff --git a/patches/src/main/resources/addresources/values-pl-rPL/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-pl-rPL/shared-youtube/strings.xml index 2e39e94ddf..45a09a1d23 100644 --- a/patches/src/main/resources/addresources/values-pl-rPL/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-pl-rPL/shared-youtube/strings.xml @@ -150,6 +150,13 @@ Aby się zalogować, naciśnij \"Kontynuuj\" poniżej" Ukrywa przyciski Poprzedni i Następny w powiadomieniu o multimediach, na ekranie blokady i w sterowaniu słuchawkami Wyłącz pasek postępu Zapobiega przeciąganiu paska postępu na ekranie blokady i w panelu powiadomień + Szukaj za pomocą przycisków Poprzedni i Następny + Zmienia sterowanie powiadomieniem o mediach, ekranem blokady oraz słuchawkami, aby przenosić zamiast zmieniać filmy + Długość cofania + Ilość sekund do cofnięcia przyciskiem Poprzedni + Długość przewijania do przodu + Ilość sekund do przewinięcia do przodu przyciskiem Następny + Długość przesuwania musi być między 1 a %s sekundy. Użycie domyślnej Wyłącz protokół QUIC diff --git a/patches/src/main/resources/addresources/values-pl-rPL/youtube/strings.xml b/patches/src/main/resources/addresources/values-pl-rPL/youtube/strings.xml index 28153f7a00..b6c0a2f6bf 100644 --- a/patches/src/main/resources/addresources/values-pl-rPL/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-pl-rPL/youtube/strings.xml @@ -473,6 +473,16 @@ Zgłoszenia są publiczne i podlegają weryfikacji przed dodaniem kanału do lis Pobieraj filmy, podcasty lub utwory za pomocą zewnętrznych aplikacji do pobierania Pokaż przycisk pobierania na nakładce odtwarzacza Wyświetla w odtwarzaczu przycisk otwierający zewnętrzną aplikację do pobierania + Szybkie pobieranie po dotknięciu + Wyświetla prośbę o pobieranie w tle. Przytrzymaj, aby otworzyć pobieracz. Działa z YTDLnis i YTindir + Oznaczaj pobrane materiały jako obejrzane + Filmy pobrane za pomocą zewnętrznego pobieracza są dodawane do Twojej historii oglądania na YouTube + Pełna HD (1080p) + Średnia (192 kbps) + Szybkie pobieranie + Film + Audio + Wszystkie opcje Tłumaczenie głosowe Czyta napisy na głos przy użyciu wbudowanego syntezatora mowy, synchronizując je z odtwarzaniem obrazu @@ -588,14 +598,14 @@ Uwaga: Może nie działać na transmisjach na żywo" Kolejka nie zniknie po ponownym uruchomieniu aplikacji Zamień akcje Zapisz i Kolejka Dotknij, aby wyświetlić opcje kolejki. Przytrzymaj, aby zapisać do obejrzenia - Zastąp przycisk kolejki w menu strony głównej - Otwiera kolejkę Morphe zamiast kolejki YouTube + + Odtwórz jako następny w kolejce + Domyślnie + Natywna kolejka + Kolejka Morphe + Klient koleji Morphe po stronie klienta Dodaj opcję dodawania do kolejki w wysuwanym menu Dodaje dodatkową pozycję w wysuwanym menu. Włącz tę opcję, jeśli \"Dodaj do kolejki\" nie jest wyświetlane - Kolejka w stylu Premium - "Zastępuje kolejkę Morphe taką, która działa na urządzeniu, odtwarza się samodzielnie i pozwala na zmianę kolejności przez przeciąganie - -Uwaga: Następny film w kolejce uruchamia się tylko wtedy, gdy YouTube jest widoczny na ekranie" Kolejka Kolejka (%s) Wyczyść wszystko @@ -640,6 +650,20 @@ Uwaga: Następny film w kolejce uruchamia się tylko wtedy, gdy YouTube jest wid Usuń i odśwież film Otwórz kolejkę Zapisz kolejkę na playliście + + Playlista zapisana + Zapisano do %s + Wyczyść kolejkę + Kolejka wyczyszczona + Kolejka + Odtwórz jako ostatni w kolejce + Odtwórz jako następny w kolejce + YouTube nie mógł wyświetlić kolejki. + Kolejka nie jest gotowa dla tego filmu. Otwórz odtwarzacz filmów i spróbuj ponownie. + Dodać Mix do kolejki czy obejrzeć Mix? + Obejrzyj + Dodaj do kolejki + Ładowanie Mix… Otwieraj kanał awatara transmisji na żywo Zapobiega otwieraniu transmisji na żywo po dotknięciu awatara kanału @@ -853,8 +877,11 @@ Aby przejść do ustawień, włącz przycisk w menu: Ogólne → Pasek narzędzi Animuje przejścia podczas przełączania między kartami Wyłącz automatyczne ukrywanie paska nawigacji Utrzymuje widoczność paska nawigacji podczas przewijania - Wyłącz półprzezroczysty pasek nawigacji - Sprawia, że pasek nawigacji i stanu staje się całkowicie nieprzezroczysty + Styl paska nawigacji + Domyślny + Półprzezroczysty + Przezroczysty + Nieprzezroczysty Pasek narzędzi Zdecyduj, które elementy będą wyświetlane na pasku narzędzi Ukryj przycisk Przesyłaj diff --git a/patches/src/main/resources/addresources/values-pt-rBR/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-pt-rBR/shared-youtube/strings.xml index 14bfb63519..2d5deb0be7 100644 --- a/patches/src/main/resources/addresources/values-pt-rBR/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-pt-rBR/shared-youtube/strings.xml @@ -151,6 +151,13 @@ Para fazer login, pressione continuar abaixo" Oculta os botões Anterior e Próximo na notificação de mídia, na tela de bloqueio e nos controles de fone de ouvido. Desativar barra de progresso Impede o arrasto da barra de progresso na tela de bloqueio e no painel de notificação + Procurar com os botões &Anterior e Próximo + Alterna controles para notificação de mídia, tela de bloqueio e fones de ouvido para procurar em vez de alterar vídeos + Duração de retrocesso + Segundos para retroceder com o botão Anterior + Duração de avanço rápido + Segundos para avançar rápido com o botão Próximo + O comprimento da procura deve estar entre 1 e %s segundos. Usando o padrão Desativar protocolo QUIC diff --git a/patches/src/main/resources/addresources/values-pt-rBR/youtube/strings.xml b/patches/src/main/resources/addresources/values-pt-rBR/youtube/strings.xml index 915104c4ed..0289a678a7 100644 --- a/patches/src/main/resources/addresources/values-pt-rBR/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-pt-rBR/youtube/strings.xml @@ -473,6 +473,16 @@ Depois que um vídeo com o botão Ask for aberto uma vez, o recurso funciona com Faça o download de vídeos, podcasts ou músicas com apps de terceiros Exibir botão de download da sobreposição do player Mostra um botão no player de vídeo que abre seu app de download externo + Download rápido ao tocar + Solicita o download em segundo plano. Toque e segure para abrir o baixador. Funciona com YTDLnis e YTindir + Marcar downloads como assistidos + Vídeos baixados com um baixador externo são adicionados ao seu histórico de exibição do YouTube + Full HD (1080p) + Médio (192 kbps) + Download rápido + Vídeo + Áudio + Todas as opções Tradução por voz Leia as legendas do vídeo em voz alta usando a conversão de texto em fala no dispositivo, sincronizada com a reprodução @@ -588,14 +598,14 @@ Nota: pode não funcionar em transmissões ao vivo" A fila é mantida após reiniciar o aplicativo Trocar as ações de Salvar e Fila Toque para ver as opções da fila, toque e segure para salvar em \"Assistir mais tarde\" - Substituir botão da fila no feed - Abre a fila do Morphe em vez da fila do YouTube + + Reproduzir a seguir na fila + Padrão + Fila nativa + Fila do Morphe + Fila do Morphe no cliente Adicionar item de fila ao menu suspenso do feed Adiciona um item adicional ao menu suspenso. Ative isto se \"Adicionar à fila\" não for exibido - Fila estilo Premium - "Substitui a fila do Morphe por uma que fica no dispositivo, é reproduzida sozinha e pode ser reordenada arrastando - -Info: o próximo vídeo da fila só é iniciado quando o YouTube está na tela" Fila Fila (%s) Limpar tudo @@ -640,6 +650,20 @@ Info: o próximo vídeo da fila só é iniciado quando o YouTube está na tela"< Remover e recarregar vídeo Abrir a fila Salvar a fila em uma playlist + + Lista de reprodução salva + Salvo em %s + Limpar fila + Fila limpa + Fila + Adicionar à fila + Reproduzir a seguir na fila + O YouTube não pôde exibir a fila. + A fila não está pronta para este vídeo. Abra o player de vídeo e tente novamente. + Adicionar Mix à fila ou assistir ao Mix? + Assistir + Adicionar à fila + Carregando o Mix… Não abrir live ao tocar no avatar do canal Impedir a abertura de transmissões ao vivo ao tocar no avatar do canal @@ -853,8 +877,11 @@ Para acessar as Configurações, ative o botão Configurações via Geral → Ba Anima as transições ao alternar entre as abas Desativar ocultação automática da barra de navegação Mantém a barra de navegação visível durante a rolagem - Desativar barra de navegação translúcida - Deixa a barra de navegação e a barra de status totalmente opacas + Estilo da barra de navegação + Padrão + Translúcido + Transparente + Opaco Barra de ferramentas Escolha quais componentes aparecem na barra de ferramentas Ocultar botão Transmitir diff --git a/patches/src/main/resources/addresources/values-pt-rPT/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-pt-rPT/shared-youtube/strings.xml index 97e5b70e14..21fa579c25 100644 --- a/patches/src/main/resources/addresources/values-pt-rPT/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-pt-rPT/shared-youtube/strings.xml @@ -151,6 +151,13 @@ Para entrar, pressione continuar abaixo" Oculta os botões Anterior e Próximo na notificação de mídia, na tela de bloqueio e nos controles de fone de ouvido Desativar a barra de busca Impede que a barra de busca seja arrastada na tela de bloqueio e no painel de notificações + Buscar com botões Anterior & Próximo + Altera os controles de notificação de mídia, tela de bloqueio e fone de ouvido para buscar em vez de alterar vídeos + Duração do retrocesso + Segundos para retroceder com o botão Anterior + Duração do avanço rápido + Segundos para avançar com o botão Próximo + O tempo de busca deve ser entre 1 e %s segundos. Usando padrão Desativar protocolo QUIC diff --git a/patches/src/main/resources/addresources/values-pt-rPT/youtube/strings.xml b/patches/src/main/resources/addresources/values-pt-rPT/youtube/strings.xml index 13ca8236b2..98537bfae3 100644 --- a/patches/src/main/resources/addresources/values-pt-rPT/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-pt-rPT/youtube/strings.xml @@ -473,6 +473,16 @@ Depois que um vídeo com o botão Ask for aberto uma vez, o recurso funciona com Baixe vídeos, podcasts ou músicas com aplicativos de download de terceiros Mostrar botão de download da sobreposição do player Exibe um botão no player de vídeo que abre o seu downloader externo + Download rápido com um toque + Prompts para baixar em segundo plano. Toque e segure para abrir o baixador. Funciona com YTDLnis e YTindir + Marcar downloads como assistidos + Vídeos baixados com um baixador externo são adicionados ao seu histórico de exibição do YouTube + Full HD (1080p) + Médio (192 kbps) + Transferência rápida + Vídeo + Áudio + Todas as opções Tradução de narração Leia as legendas do vídeo em voz alta usando a conversão de texto em fala no dispositivo, sincronizada com a reprodução @@ -588,14 +598,14 @@ Informação: Pode não funcionar em transmissões ao vivo" A fila é mantida após reiniciar o aplicativo Trocar ações de Salvar e Fila Toque para opções de fila, toque e segure para salvar em Assistir mais tarde - Substituir fila de reprodução flutuante - Abre a fila do Morphe em vez da fila do YouTube + + Tocar seguinte na fila + Padrão + Fila nativa + Fila do Morphe + Fila do Morphe lado do cliente Adicionar item de menu de fila de feed Adiciona um item adicional de menu flutuante. Ative isso se \"Adicionar à fila\" não for exibido. - Fila estilo premium - "Substitui a fila do Morphe por uma que fica no dispositivo, reproduz sozinha e pode ser reordenada arrastando - -Informação: o próximo vídeo da fila só começa enquanto o YouTube estiver na tela" Fila Fila (%s) Limpar tudo @@ -640,6 +650,20 @@ Informação: o próximo vídeo da fila só começa enquanto o YouTube estiver n Remover da fila e recarregar vídeo Abrir fila Salvar fila para playlist + + Lista de reprodução guardada + Guardado em %s + Limpar fila + Fila limpa + Fila + Tocar último na fila + Tocar seguinte na fila + O YouTube não conseguiu exibir a fila. + A fila não está pronta para este vídeo. Abra o leitor de vídeos e tente novamente. + Adicionar Mix à fila ou assistir ao Mix? + Assistir + Adicionar à fila + A carregar Mix… Abrir canal do avatar ao vivo Impede que a transmissão ao vivo seja aberta ao tocar no avatar do canal @@ -853,8 +877,11 @@ Para acessar as Configurações, ative o botão Configurações em Geral → Bar Anima as transições ao alternar entre as guias Desativar ocultação automática da barra de navegação Mantém a barra de navegação visível durante a rolagem - Desativar a barra de navegação translúcida - Torna a barra de navegação e a barra de status totalmente opacas + Estilo da barra de navegação + Padrão + Translúcido + Transparente + Opaco Barra de ferramentas Escolha quais componentes aparecem na barra de ferramentas Ocultar botão Transmitir diff --git a/patches/src/main/resources/addresources/values-ro-rRO/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-ro-rRO/shared-youtube/strings.xml index d91326179b..fb02b192b9 100644 --- a/patches/src/main/resources/addresources/values-ro-rRO/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ro-rRO/shared-youtube/strings.xml @@ -151,6 +151,13 @@ Pentru a te apăsa, apăsă butonul Continu de mai jos" Ascunde butoanele Anterior și Următor din notificarea media, de pe ecranul de blocare și din comenzile căștilor Dezactivează bara de căutare Împiedică glisarea barei de căutare pe ecranul de blocare și în panoul de notificări + Caută cu butoanele &amp; Anterior și Următor + Modifică controlurile pentru notificarea media, ecranul blocat și căștile pentru a căuta în loc să schimbe videoclipurile + Lungimea derulării înapoi + Secunde pentru derularea înapoi cu butonul Anterior + Lungimea derulării înainte + Secunde pentru derularea înainte cu butonul Următor + Lungimea derulării trebuie să fie între 1 și %s secunde. Se utilizează valoarea implicită Dezactivați protocolul QUIC diff --git a/patches/src/main/resources/addresources/values-ro-rRO/youtube/strings.xml b/patches/src/main/resources/addresources/values-ro-rRO/youtube/strings.xml index b8b0852e90..4d39b90e73 100644 --- a/patches/src/main/resources/addresources/values-ro-rRO/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ro-rRO/youtube/strings.xml @@ -473,6 +473,16 @@ După ce un videoclip cu butonul Ask este deschis o dată, funcția funcționeaz Descărcă videoclipuri, podcasturi sau melodii folosind aplicaţii de descărcare terţe Arată butonul de descărcare al suprapunerii playerului Afișează un buton în playerul video care deschide descărcătorul extern + Descărcare rapidă la atingere + Solicită descărcarea în fundal. Apasă și menține pentru a deschide programul de descărcare. Funcționează cu YTDLnis și YTindir + Marchează descărcările ca vizionate + Videoclipurile descărcate cu un program de descărcare extern sunt adăugate în istoricul de vizionare YouTube + Full HD (1080p) + Mediu (192 kbps) + Descărcare rapidă + Video + Audio + Toate opțiunile Traducere cu voce peste Citește cu voce tare subtitrările video folosind text-to-speech pe dispozitiv, sincronizat cu redarea @@ -588,14 +598,14 @@ Info: S-ar putea să nu funcționeze pe transmisiuni live" Coada este păstrată după repornirea aplicației Schimbă acțiunile Salvare și Coadă Atingeți pentru opțiunile de coadă, atingeți și mențineți apăsat pentru a salva în Urmărește mai târziu. - Înlocuire coadă flyout feed buton - Deschide coada Morphe în loc de coada YouTube + + Redare următorul în coadă + Implicit + Coadă nativă + Coadă Morphe + Coadă Morphe client Adaugă element de meniu pentru coada flyout de feed Adaugă un element suplimentar de flyout. Activează această opțiune dacă \"Adaugă în coadă\" nu este afișat. - Coadă în stil Premium - "Înlocuiește coada Morphe cu una care este stocată pe dispozitiv, se redă singură și poate fi reordonată prin glisare - -Informații: Următorul videoclip din coadă pornește doar cât timp YouTube este afișat pe ecran" Coadă Coda (%s) Șterge tot @@ -640,6 +650,20 @@ Informații: Următorul videoclip din coadă pornește doar cât timp YouTube es Înlătură din coadă și reîncarcă video Deschide coada Salvează coada în playlist + + Playlist salvat + Salvat în %s + Golește coada + Coada a fost golită + Coadă + Redare ultimul din coadă + Redare următorul în coadă + YouTube nu a putut afișa coada. + Coada nu este pregătită pentru acest video. Deschide player-ul video și încearcă din nou. + Adaugă Mix la coadă sau privește Mix? + Privește + Adaugă la coadă + Se încarcă Mix… Deschide canalul din avatarul live Împiedică deschiderea transmisiunii live atunci când apeși pe avatarul canalului @@ -853,8 +877,11 @@ Pentru a accesa Setările, activați butonul Setări prin General → Bara de in Ascunde meniul Ascultă cu YouTube Music Dezactivează ascunderea automată a barei de navigare Ascunde meniul Urmărește în VR - Dezactivează bara de navigare translucidă - Face bara de navigare și bara de stare complet opace + Stilul barei de navigare + Implicit + Translucid + Transparent + Opac Bara de navigare Ascunde previzualizarea videoclipului următor în ecran complet înainte de începerea redării automate Ascunde butonul Distribuție diff --git a/patches/src/main/resources/addresources/values-ru-rRU/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-ru-rRU/shared-youtube/strings.xml index 558b59c313..6fd28b5983 100644 --- a/patches/src/main/resources/addresources/values-ru-rRU/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ru-rRU/shared-youtube/strings.xml @@ -151,6 +151,13 @@ Second \"item\" text" Скрывает кнопки «Предыдущий» и «Следующий» в медиауведомлении, на экране блокировки и в элементах управления гарнитуры Отключить ползунок перемотки Предотвращает перетаскивание ползунка перемотки на экране блокировки и в панели уведомлений + Поиск с помощью кнопок & «Назад» и «Вперёд» + Изменено управление уведомлениями о медиафайлах, экраном блокировки и наушниками: поиск вместо переключения видео + Длина перемотки назад + Количество секунд для перемотки назад кнопкой «Назад» + Длина перемотки вперёд + Количество секунд для перемотки вперёд кнопкой «Вперёд» + Длина поиска должна быть от 1 до %s секунд. Используется значение по умолчанию Отключить протокол QUIC diff --git a/patches/src/main/resources/addresources/values-ru-rRU/youtube/strings.xml b/patches/src/main/resources/addresources/values-ru-rRU/youtube/strings.xml index dcd1f3969f..097800599f 100644 --- a/patches/src/main/resources/addresources/values-ru-rRU/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ru-rRU/youtube/strings.xml @@ -473,6 +473,16 @@ Second \"item\" text" Скачивайте видео, подкасты или песни с помощью сторонних приложений для скачивания Показать кнопку загрузки оверлея плеера Показывает кнопку в видеоплеере, которая открывает ваше внешнее приложение для загрузки + Быстрая загрузка одним касанием + Запрос на загрузку в фоновом режиме. Нажмите и удерживайте, чтобы открыть загрузчик. Работает с YTDLnis и YTindir + Отмечать загрузки как просмотренные + Видео, загруженные с помощью внешнего загрузчика, будут добавлены в вашу историю просмотров YouTube + Full HD (1080p) + Среднее (192 кбит/с) + Быстрая загрузка + Видео + Аудио + Все параметры Перевод озвучивания Читайте субтитры видео вслух с помощью встроенного преобразования текста в речь, синхронизированного с воспроизведением @@ -590,14 +600,14 @@ Second \"item\" text" Очередь сохраняется после перезапуска приложения Поменять местами действия \"Сохранить\" и \"В очередь\" Нажмите для параметров очереди, нажмите и удерживайте для сохранения в «Смотреть позже» - Кнопка для переопределения очереди всплывающего окна каналов - Открывает очередь Morphe вместо очереди YouTube + + Воспроизвести следующим в очереди + По умолчанию + Системная очередь + Очередь Morphe + Клиентская очередь Morphe Добавить пункт меню очереди во всплывающее меню ленты Добавляет дополнительный элемент всплывающего меню. Включите это, если пункт \"Добавить в очередь\" не отображается. - Очередь в стиле Premium - "Заменяет очередь Morphe на очередь, которая хранится на устройстве, воспроизводится сама и может быть переупорядочена перетаскиванием - -Информация: следующее видео из очереди запускается только тогда, когда YouTube находится на экране" Очередь Очередь (%s) Очистить всё @@ -642,6 +652,20 @@ Second \"item\" text" Удалить из очереди и перезагрузить видео Открыть очередь Сохранить очередь в плейлист + + Плейлист сохранён + Сохранено в %s + Очистить очередь + Очередь очищена + Очередь + Воспроизвести последним в очереди + Воспроизвести следующим в очереди + YouTube не может показать очередь. + Очередь не готова для этого видео. Откройте плеер и повторите попытку. + Добавить Микс в очередь или прослушать? + Слушать + Добавить в очередь + Загрузка Микса… Открыть канал из аватара прямой трансляции Запрещает открытие прямой трансляции при нажатии на аватар канала @@ -855,8 +879,11 @@ Second \"item\" text" Анимирует переходы между вкладками Выключить авто скрытие панели навигации Не скрывать панель навигации при прокрутке - Отключить полупрозрачность панели навигации - Делает панель навигации и строку состояния полностью непрозрачными + Стиль панели навигации + По умолчанию + Полупрозрачный + Прозрачный + Непрозрачный Панель инструментов Выберите, какие компоненты отображаются на панели инструментов Скрыть кнопку \"Трансляция\" diff --git a/patches/src/main/resources/addresources/values-si-rLK/youtube/strings.xml b/patches/src/main/resources/addresources/values-si-rLK/youtube/strings.xml index 174f25cd1a..1a02f7efd7 100644 --- a/patches/src/main/resources/addresources/values-si-rLK/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-si-rLK/youtube/strings.xml @@ -76,7 +76,9 @@ Second \"item\" text" + + diff --git a/patches/src/main/resources/addresources/values-sk-rSK/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-sk-rSK/shared-youtube/strings.xml index 8702e4031f..abec3440bc 100644 --- a/patches/src/main/resources/addresources/values-sk-rSK/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-sk-rSK/shared-youtube/strings.xml @@ -151,6 +151,13 @@ Ak sa chcete prihlásiť, stlačte tlačidlo Pokračovať" Skryje predchádzajúce a nasledujúce tlačidlá v mediálnom upozornení, na uzamykacej obrazovke a v ovládacích prvkoch slúchadiel. Zakázať posuvník. Zabraňuje pretiahnutiu posuvníka na uzamykacej obrazovke a v paneli s upozorneniami. + Vyhľadávanie pomocou tlačidiel predchádzajúceho a ďalšieho + Ovládacie prvky pre upozornenie na médiá, zámku obrazovky a slúchadiel sa používajú na hľadanie namiesto zmeny videí + Dĺžka preskoku dozadu + Počet sekúnd preskoku dozadu pomocou tlačidla Predchádzajúce + Dĺžka preskoku dopredu + Počet sekúnd preskoku dopredu pomocou tlačidla Ďalšie + Dĺžka preskoku musí byť medzi 1 a %s sekundami. Používa sa predvolená hodnota Zakázať protokol QUIC diff --git a/patches/src/main/resources/addresources/values-sk-rSK/youtube/strings.xml b/patches/src/main/resources/addresources/values-sk-rSK/youtube/strings.xml index 4d978660fb..f49db73f57 100644 --- a/patches/src/main/resources/addresources/values-sk-rSK/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-sk-rSK/youtube/strings.xml @@ -473,6 +473,16 @@ Po jednom otvorení videa s tlačidlom Opýtať sa bude funkcia fungovať pre v Stiahnite si videá, podcasty alebo piesne pomocou aplikácií na sťahovanie tretích strán Zobraziť tlačidlo na stiahnutie preklapiačnej obrazovky prehrávača Zobrazí tlačidlo v videoprehrávači, ktoré otvorí vašu externú aplikáciu na sťahovanie + Rýchle sťahovanie kliknutím + Výzvy na stiahnutie na pozadí. Podržaním otvorte sťahovač. Funguje s aplikáciami YTDLnis a YTindir + Označiť sťahovania ako zhliadnuté + Videá stiahnuté externým sťahovačom sa pridávajú do histórie zhliadnutí na YouTube + Full HD (1080p) + Stredná kvalita (192 kbps) + Rýchle sťahovanie + Video + Zvuk + Všetky možnosti Preklad hlasového komentára Nahlas prečítajte titulky videa pomocou text-to-speech priamo v zariadení, synchronizovane s prehrávaním @@ -588,14 +598,14 @@ Poznámka: Možno nebude fungovať na živých prenosoch" Rad zostane zachovaný aj po reštarte aplikácie Zameniť akcie Uložiť a Zaradiť do fronty Klepnite pre možnosti frontu, klepnite a podržte pre uloženie do „Pozrieť neskôr“ - Nahradiť tlačidlo frontu rozbaľovacieho okna - Otvára rad Morphe namiesto radu YouTube + + Typ zaradenia do rady ďalšieho prehrávania + Predvolený + Natívna rada + Rada Morphe + Klientská strana rady Morphe Pridať položku ponuky do rozbaľovacej ponuky fronty informačného kanála Pridá ďalšiu položku do rozbaľovacej ponuky. Povoľte, ak sa \"Pridať do frontu\" nezobrazuje - Fronta v štýle Premium - "Nahrádza frontu Morphe frontou uloženou v zariadení, ktorá sa prehráva sama a dá sa pretiahnutím preusporiadať - -Informácia: Ďalšie video vo fronte sa spustí iba vtedy, keď je YouTube na obrazovke" Fronta Fronta (%s) Vyčistiť všetko @@ -640,6 +650,20 @@ Informácia: Ďalšie video vo fronte sa spustí iba vtedy, keď je YouTube na o Odstrániť z fronty a znovu načítať video Otvoriť frontu Uložiť frontu do zoznamu skladieb + + Playlist uložený + Uložené do %s + Vyčistiť radu + Rad vyčistený + Rad + Prehrať posledný v rade + Prehrať ďalší v rade + YouTube nemohol zobraziť rad. + Rad nie je pripravený pre toto video. Otvorte prehrávač videa a skúste to znova. + Pridať Mix do rady alebo prehrať Mix? + Prehrať + Pridať do rady + Načítava sa Mix… Otvorte kanál z avatara živého vysielania Zabráni otvoreniu živého prenosu pri ťuknutí na avatar kanála @@ -853,8 +877,11 @@ Ak chcete získať prístup k Nastaveniam, povoľte tlačidlo Nastavenia cez Vš Animuje prechody pri prechode medzi panelmi Zakázať automatické skrytie navigačného panela Udržuje navigačnú lištu viditeľnú pri posúvaní - Zakázal polopriehľadný navigačný panel - Zabezpečí úplnú nepriehľadnosť navigačného panela a panela stavu + Štýl navigačného panela + Predvolený + Polopriehľadný + Priehľadný + Nepriehľadný Panel nástrojov Vyberte, ktoré komponenty sa zobrazia v lište nástrojov Skryť tlačidlo Prenos diff --git a/patches/src/main/resources/addresources/values-sl-rSI/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-sl-rSI/shared-youtube/strings.xml index f1fc2dd057..aaa521d350 100644 --- a/patches/src/main/resources/addresources/values-sl-rSI/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-sl-rSI/shared-youtube/strings.xml @@ -151,6 +151,13 @@ Pritisnite \"nadaljujte\", da se prijavite" Skrije gumba za prejšnjo in naslednjo skladbo v medijskem obvestilu, na zaklenjenem zaslonu in v kontrolnikih za slušalke. Onemogoči drsnik. Prepreči vlečenje drsnika na zaklenjenem zaslonu in plošči z obvestili. + Išči s gumijema Prejšnji in Naslednji + Spremeni nadzore za obvestilo o medijih, zaklenjen zaslon in slušalke za iskanje namesto spreminjanja videoposnetkov + Dolžina vračanja + Sekund za vračanje s gumbom Prejšnji + Dolžina pospešenega predvajanja + Sekund za pospešeno predvajanje s gumbom Naslednji + Dolžina iskanja mora biti med 1 in %s sekundami. Uporablja se privzeto vrednost Onemogoči protokol QUIC diff --git a/patches/src/main/resources/addresources/values-sl-rSI/youtube/strings.xml b/patches/src/main/resources/addresources/values-sl-rSI/youtube/strings.xml index e1abd7cae2..9842d48292 100644 --- a/patches/src/main/resources/addresources/values-sl-rSI/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-sl-rSI/youtube/strings.xml @@ -473,6 +473,16 @@ Ko enkrat odprete video s gumbom Zastavi, bo funkcija delovala za vse videe, tud Prenesite videoposnetke, podkaste ali pesmi z aplikacijami za prenos tretje osebe Prikaži gumb za prenos predvajalnika v prepletu Prikaže gumb v video predvajalniku, ki odpre vaš zunanji program za prenos + Hitri prenos ob prijemu + Poziva za prenos v ozadju. Pritisnite in držite za odprtje prenoshnika. Deluje z YTDLnis in YTindir + Označi prenose kot ogledane + Videoposnetki, preneseni z zunanjim prenoshnikom, so dodani v zgodovino ogledov na YouTubu + Polna HD (1080p) + Srednje (192 kbps) + Hitri prenos + Video + Avdio + Vse možnosti Prevajanje z glasom Prebiranje napisov videoposnetka v glas z uporabo pretvorbe besedila v govor v napravi, sinhronizirano s predvajanjem @@ -588,14 +598,14 @@ Opozorilo: morda ne deluje na prenosih v živo" Vrsta se shrani tudi po ponovnem zagonu aplikacije Zamenjaj shranjevanje in možnosti vrste v čakalnico Tapnite za možnosti čakalne vrste, tapnite in zadržite za shranjevanje v Watch later. - Gumb za nadomestitev vrste vrstičnega menija - Odpre vrsto Morphe namesto vrste YouTube + + Način predvajanja naslednjega v čakalni vrsti + Privzeto + Sistemskaka čakalna vrsta + Morphe čakalna vrsta + Odjemalec Morphe čakalne vrste Dodaj element menija v čakalno vrsto Doda dodaten element menija. Omogočite to, če \"Dodaj v čakalno vrsto\" ni prikazano - Čakalna vrsta v slogu Premium - "Nadomesti čakalno vrsto Morphe z vrsto, ki je na napravi, se predvaja samodejno in jo je mogoče preurediti s povlekom - -Info: Naslednji videoposnetek v čakalni vrsti se začne le, ko je YouTube na zaslonu" Čakalna vrsta Vrstič (%s) Počisti vse @@ -640,6 +650,20 @@ Info: Naslednji videoposnetek v čakalni vrsti se začne le, ko je YouTube na za Odstrani in ponovno naloži videoposnetek Odpri vrsto Shrani vrsto v predvajalni seznam + + Seznam predvajanja je bil shranjen + Shranjeno v %s + Počisti čakalno vrsto + Čakalna vrsta je počišena + Čakalna vrsta + Predvajaj zadnji v čakalni vrsti + Predvajaj naslednjega v čakalni vrsti + YouTube ni mogel prikazati čakalne vrste. + Čakalna vrsta ni pripravljena za ta videoposnetek. Odprite predvajalnik videoposnetkov in poskusite znova. + Dodaj Mix v čakalno vrsto ali si oglejte Mix? + Ogled + Dodaj v čakalno vrsto + Nalaganje Meša… Odprite kanal iz avatarja v živo Preprečuje, da bi se prenos v živo odprl ob dotiku avatarja kanala @@ -853,8 +877,11 @@ Za dostop do Nastavitev omogočajte gumb Nastavitve prek Splošno → Orodna vrs Naredi vrstico za navigacijo popolnoma neprozorno v svetlem načinu Onemogoči samodejno skrivanje navigacijske vrstice Naredi vrstico za navigacijo popolnoma neprozorno v temnem načinu - Onemogoči polprozorno navigacijsko vrsto - Navigacijsko vrsto in vrsto stanja naredi popolnoma neprozorno + Slog navigacijske vrstice + Privzeto + Poltransparentno + Transparentno + Neprozorno Orodje. Skrij podnapisna stopala menija \'Zvok\' Skrij gumb za prenos diff --git a/patches/src/main/resources/addresources/values-sr-rCS/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-sr-rCS/shared-youtube/strings.xml index a8babe162f..c8e7527822 100644 --- a/patches/src/main/resources/addresources/values-sr-rCS/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-sr-rCS/shared-youtube/strings.xml @@ -151,6 +151,13 @@ Da bi se prijavili, pritisnite nastavi ispod" Sakriva dugmad za prethodni i sledeći video u medijskom obaveštenju, na zaključanom ekranu i u kontrolama na slušalicama Onemogući klizač Sprečava prevlačenje klizača na zaključanom ekranu i u panelu za obaveštenja + Traži sa dugmićima Prethodni & Sledeći + Promene kontrole za obaveštenje o medijima, zaključani ekran i slušalice za traženje umesto menjanja videa + Dužina brzanja unazad + Sekundi za brzanje unazad pomoću dugmeta Prethodni + Dužina brzog napredovanja + Sekundi za brzo napredovanje pomoću dugmeta Sledeći + Dužina traženja mora biti između %s i 1 sekundi. Koristi se podrazumevana vrednost Onemogući protokol QUIC diff --git a/patches/src/main/resources/addresources/values-sr-rCS/youtube/strings.xml b/patches/src/main/resources/addresources/values-sr-rCS/youtube/strings.xml index e644b52968..b10cb9d376 100644 --- a/patches/src/main/resources/addresources/values-sr-rCS/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-sr-rCS/youtube/strings.xml @@ -473,6 +473,16 @@ Nakon što se jednom otupi video sa dugmetom Ask, funkcija radi sa svim videima, Preuzmite video snimke, podkaste ili pesme pomoću aplikacija za preuzimanje trećih strana Prikaži dugme za preuzimanje u pregledu plejera Prikazuje dugme u video plejeru koje otvara vaš spoljni program za preuzimanje + Brzo preuzimanje dodirom + Najavljuje preuzimanje u pozadini. Dodirnite i zadržite da otvorite preuzimač. Radi sa YTDLnis i YTindir + Označi preuzete datoteke kao odgledane + Video zapisi preuzeti spoljašnjim preuzimačem dodaju se u vašu YouTube istoriju gledanja + Full HD (1080p) + Srednje (192 kbps) + Brzo preuzimanje + Video + Audio + Sve opcije Prevod sinhronizovane titlova Učita titlove iz videa i izgovara ih pomoću ugrađenog pretvarača teksta u govor, sinhronizovano sa reprodukcijom @@ -588,14 +598,14 @@ Napomena: Možda neće raditi na direktnim prenosima" Red čekanja je sačuvan nakon ponovnog pokretanja aplikacije Zameni akcije Štednje i stavljanja u red Dodirnite za opcije redosleda, dodirnite i zadržite da sačuvate u Pogledaj kasnije - Preuzmi red za reprize - Otvara Morphe red čekanja umesto YouTube reda čekanja + + Režim „Play next in queue” + Подразумеван + Sistemski red čekanja + Morphe red čekanja + Morphe klijentski deo reda čekanja Dodaj stavku menija za leteći red za feed Dodaje dodatnu stavku letećeg menija. Omogući ovo ako se \"Dodaj u red\" ne prikazuje. - Red čekanja u Premium stilu - "Zamenjuje red čekanja u Morpheu onim koji se nalazi na uređaju, sam se reprodukuje i može da se preuredi prevlačenjem - -Info: Sledeći video iz reda čekanja počinje samo dok je YouTube na ekranu" Red čekanja Red čekanja (%s) Očisti sve @@ -640,6 +650,20 @@ Info: Sledeći video iz reda čekanja počinje samo dok je YouTube na ekranu"Ukloni iz reda i ponovo učitaj video Otvori red Sačuvaj red u listu za reprodukciju + + Lista za reprodukciju je sačuvana + Sačuvano u %s + Obriši red čekanja + Red čekanja je obrisan + Red čekanja + Reprodukcija zadnjeg u redu čekanja + Reprodukcija sledećeg u redu čekanja + YouTube ne može da prikaže red čekanja. + Red čekanja nije spreman za ovaj video. Otvorite plejer videa i pokušajte ponovo. + Dodati Mešavinu u red čekanja ili gledati Mešavinu? + Gledaj + Dodaj u red čekanja + Učitavanje Mešavine… Otvorite kanal iz uživo avatara Sprečava otvaranje direktnog prenosa kada dodirnete avatar kanala @@ -853,8 +877,11 @@ Da biste pristupili Podešavanjima, omogućite dugme Podešavanja putem General Animira prelaz prilikom prelaska između kartica Onemogući automatsko skrivanje navigacione trake Drži traku za navigaciju vidljivom prilikom skrolovanja - Onemogući prozirnu navigacionu traku - Čini navigacionu i statusnu traku potpuno neprozirnim + Stil trake za navigaciju + Подразумеван + Polutransparentno + Transparentno + Neprozirno Traka za alatke Izaberite koje komponente se pojavljuju u padajućem meniju Sakrij dugme „Prebacuj” diff --git a/patches/src/main/resources/addresources/values-sr-rSP/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-sr-rSP/shared-youtube/strings.xml index 7f35953a21..477f546b33 100644 --- a/patches/src/main/resources/addresources/values-sr-rSP/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-sr-rSP/shared-youtube/strings.xml @@ -151,6 +151,13 @@ Da bi se prijavili, pritisnite nastavak ispod" Сакрива дугмад за претходну и следећу нумеру у медијском обавештењу, на закључаном екрану и у контролама слушалица Онемогући траку за премотавање Спречава превлачење траке за премотавање на закључаном екрану и панелу обавештења + Претражите са дугмадима Претходно и Следеће + Мења контроле за медијску обавештења, закључани екран и слушалице да претражује уместо мењања видео записа + Дужина враћања + Секунди за враћање са дугметом Претходно + Дужина напредовања + Секунди за напредовање са дугметом Следеће + Дужина претраге мора бити између 1 и %s секунди. Користи се подразумевана Oнемогући протокол QUIC diff --git a/patches/src/main/resources/addresources/values-sr-rSP/youtube/strings.xml b/patches/src/main/resources/addresources/values-sr-rSP/youtube/strings.xml index 3e60d32ad4..e916e2aee8 100644 --- a/patches/src/main/resources/addresources/values-sr-rSP/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-sr-rSP/youtube/strings.xml @@ -473,6 +473,16 @@ Second \"item\" text" Преузмите видео-снимке, подкасте или песме помоћу апликација за преузимање од трећих страна Прикажи дугме за преузимање прегледача играча Приказује дугме у видео плејеру које отвара ваш спољни преузимач + Брзо преузимање додиром + Има упутства за преузимање у позадини. Додirните и држите да бисте отворили програм за преузимање. Ради са YTDLnis и YTindir + Означи преузимања као гледане + Видео снимци преузети помоћу спољног програма за преузимање додају се у вашу историју прегледа на YouTube-у + Пуни ХД (1080p) + Средње (192 kbps) + Брзо преузимање + Видео + Аудио + Све опције Превод гласовног надзора Учитавајте титлове видео записа наглас користећи текст у говор на уређају, синхронизовано са репродукцијом @@ -588,14 +598,14 @@ Second \"item\" text" Ред чекања се чува након поновног покретања апликације Замените акције Чување и Додај у ред Додирните за опције реда, додирните и задржите да бисте сачували у \'Погледај касније\' - Прекид прелаза преко реда прилива - Отвара се редица са Морфе уместо YouTube редице + + Тип реде „Играј следеће у реду“ + Подразумевано + Системски ред + Morphe ред + Kлијентска страна Morphe реда Додај ставку менија за искачући ред фида Додаје додатну искачућу ставку. Омогућите ово ако се \"Додај у ред\" не приказује. - Ред у премијум стилу - "Замењује Morphe ред оним који се налази на уређају, сам се пушта и може да се преуређује превлачењем - -Инфо: Следећи видео у реду се покреће само док је YouTube на екрану" Ред Ред чекања (%s) Очисти све @@ -640,6 +650,20 @@ Second \"item\" text" Уклони и преучитај видео Отвори ред чекања Сачувај ред чекања у листу за репродукцију + + Плејлиста је сачувана + Сачувано у %s + Очисти ред + Ред је очишћен + Ред + Играј последње у реду + Играј следеће у реду + YouTube није могао да прикаже ред. + Ред није спреман за овај видео. Отворите плејер и пробајте поново. + Додај Микс у ред или гледај Микс? + Гледај + Додај у ред + Учитавање Микса… Отвори канал из аватара уживо Спречава отварање ливог стрима приликом додира аватара канала @@ -851,8 +875,11 @@ Second \"item\" text" Анимира прелазе приликом пребацивања између картица. Онемогућите аутоматско скривање траке навигације Држи навигациону траку видљивом током померања. - Онемогући прозирну导航 траку - Чини导航 и статусну траку потпуно непрозирном + Стил траке за навигацију + Подразумевано + Полупрозирно + Прозирно + Непрозирно Traka za alatke Одаберите које компоненте ће се појавити у траци са алаткама. Сакриј дугме „Пребацуј” diff --git a/patches/src/main/resources/addresources/values-sv-rSE/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-sv-rSE/shared-youtube/strings.xml index 3a3d178faf..ff84870d54 100644 --- a/patches/src/main/resources/addresources/values-sv-rSE/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-sv-rSE/shared-youtube/strings.xml @@ -151,6 +151,13 @@ Tryck på Fortsätt nedan för att logga in" Döljer knapparna Föregående och Nästa i medieaviseringen, på låsskärmen och för hörlursstyrning Inaktivera sökfältet Förhindrar att sökreglaget kan dras på låsskärmen och i aviseringspanelen + Sök med föregående & nästa-knappar + Ändrar kontrollerna för meddelandeanvisning, låsskärm och hörlurar så att de söker istället för att byta videor + Återinspelningslängd + Antal sekunder för återinspelningsåtgärd med föregående-knapp + Spola fram-längd + Antal sekunder för spola fram med nästa-knapp + Söklängden måste vara mellan 1 och %s sekunder. Använder standardvärde Inaktivera QUIC-protokoll diff --git a/patches/src/main/resources/addresources/values-sv-rSE/youtube/strings.xml b/patches/src/main/resources/addresources/values-sv-rSE/youtube/strings.xml index a2a1e8a1f1..d396160e69 100644 --- a/patches/src/main/resources/addresources/values-sv-rSE/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-sv-rSE/youtube/strings.xml @@ -473,6 +473,16 @@ När en video med Ask-knappen har öppnats en gång fungerar funktionen med alla Ladda ner videor, poddar eller låtar med nedladdningsappar från tredje part Visa nedladdningsknapp för spelaröverlag Visar en knapp i videospelaren som öppnar din externa nedladdare + Snabb nedladdning vid tryck + Frågar om att ladda ner i bakgrunden. Tryck och håll intryckt för att öppna nedladdaren. Fungerar med YTDLnis och YTindir + Markera nedladdningar som sedda + Videos som laddas ner med en extern nedladdare läggs till i din YouTube-visningshistorik + Full HD (1080p) + Medel (192 kbps) + Snabb nedladdning + Video + Ljud + Alla alternativ Voice over-översättning Läs upp video-textning med hjälp av enhetens text-till-tal, synkroniserat med uppspelning @@ -588,14 +598,14 @@ Information: Kanske inte fungerar i livestreamar" Kön behålls efter omstart av appen Byt plats på åtgärderna Spara och Kö Tryck för köalternativ, tryck och håll för att spara till Titta senare - Åsidosätt flyout-knappen för kö i flödet - Öppnar Morphe-kön istället för YouTube-kön + + Spelar nästa i kötyp + Standard + Inbyggd kön + Morphe-kö + Morphe-köklientsida Lägg till menyalternativ för kö i flödernas flyout-meny Lägger till ett extra flyout-objekt. Aktivera detta om \"Lägg till i kön\" inte visas - Kö i Premiumstil - "Ersätter Morphe-kön med en som finns på enheten, spelas upp automatiskt och kan sorteras om genom att dra - -Info: Nästa video i kön startar bara medan YouTube är på skärmen" Kö Kö (%s) Rensa alla @@ -640,6 +650,20 @@ Info: Nästa video i kön startar bara medan YouTube är på skärmen" Ta bort och ladda om video Öppna kön Spara kön till spellista + + Spellista sparad + Sparad till %s + Rensa kö + Kön rensad + Kö + Spela sista i kö + Spela nästa i kö + YouTube kunde inte visa kön. + Kön är inte redo för den här videon. Öppna videospelaren och försök igen. + Lägg till Mix i kö eller titta på Mix? + Titta + Lägg till i kö + Läser in Mix… Öppna kanal från liveprofilbild Förhindrar att livestreamen öppnas när du trycker på kanalens profilbild @@ -853,8 +877,11 @@ För att komma åt Inställningar aktiverar du Inställningsknappen via Allmänt Animerar övergångarna när du byter mellan flikar Inaktivera att navigeringsfältet döljs automatiskt Håller navigeringsfältet synligt när du skrollar - Inaktivera genomskinligt navigeringsfält - Gör navigerings- och statusfältet helt ogenomskinligt + Stil för navigeringsfält + Standard + Genomskinlig + Transparent + Opak Verktygsfält Välj vilka komponenter som visas i verktygsfältet Dölj knappen Casta diff --git a/patches/src/main/resources/addresources/values-sw-rKE/youtube/strings.xml b/patches/src/main/resources/addresources/values-sw-rKE/youtube/strings.xml index 174f25cd1a..1a02f7efd7 100644 --- a/patches/src/main/resources/addresources/values-sw-rKE/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-sw-rKE/youtube/strings.xml @@ -76,7 +76,9 @@ Second \"item\" text" + + diff --git a/patches/src/main/resources/addresources/values-ta-rIN/youtube/strings.xml b/patches/src/main/resources/addresources/values-ta-rIN/youtube/strings.xml index 8b0c1c2daa..a4923027ed 100644 --- a/patches/src/main/resources/addresources/values-ta-rIN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ta-rIN/youtube/strings.xml @@ -78,6 +78,7 @@ Second \"item\" text" + காணொளி @@ -86,7 +87,9 @@ Second \"item\" text" + + diff --git a/patches/src/main/resources/addresources/values-te-rIN/youtube/strings.xml b/patches/src/main/resources/addresources/values-te-rIN/youtube/strings.xml index 25e351a4c3..c5cf010858 100644 --- a/patches/src/main/resources/addresources/values-te-rIN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-te-rIN/youtube/strings.xml @@ -82,8 +82,10 @@ Second \"item\" text" + తొలగించు + diff --git a/patches/src/main/resources/addresources/values-th-rTH/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-th-rTH/shared-youtube/strings.xml index c385ff33bf..457f436306 100644 --- a/patches/src/main/resources/addresources/values-th-rTH/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-th-rTH/shared-youtube/strings.xml @@ -151,6 +151,13 @@ Second \"item\" text" ซ่อนปุ่มก่อนหน้าและถัดไปในการแจ้งเตือนสื่อ หน้าจอล็อก และตัวควบคุมหูฟัง ปิดใช้งานแถบเลื่อน ป้องกันการลากแถบเลื่อนบนหน้าจอล็อกและแผงการแจ้งเตือน + ค้นหาด้วยปุ่มก่อนหน้า & ถัดไป + เปลี่ยนการควบคุมสำหรับการแจ้งเตือนสื่อ หน้าจอปิดบัง และหูฟังให้เป็นการเลื่อนแทนที่จะเปลี่ยนวิดีโอ + ความยาวของการย้อนกลับ + จำนวนวินาทีเพื่อย้อนกลับเมื่อใช้ปุ่มก่อนหน้า + ความยาวของการเลื่อนไปข้างหน้า + จำนวนวินาทีเพื่อเลื่อนไปข้างหน้าเมื่อใช้ปุ่มถัดไป + ความยาวในการเลื่อนต้องอยู่ระหว่าง 1 ถึง %s วินาที ใช้ค่าเริ่มต้น ปิดใช้งานโพรโทคอล QUIC diff --git a/patches/src/main/resources/addresources/values-th-rTH/youtube/strings.xml b/patches/src/main/resources/addresources/values-th-rTH/youtube/strings.xml index 62f7956209..631f1d11ac 100644 --- a/patches/src/main/resources/addresources/values-th-rTH/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-th-rTH/youtube/strings.xml @@ -473,6 +473,16 @@ Second \"item\" text" ดาวน์โหลดวิดีโอ พอดแคสต์ หรือเพลงด้วยแอปดาวน์โหลดของบุคคลที่สาม แสดงปุ่มดาวน์โหลดซ้อนทับของเครื่องเล่น แสดงปุ่มในเครื่องเล่นวิดีโอที่จะเปิดตัวดาวน์โหลดภายนอกของคุณ + ดาวน์โหลดด่วนเพียงแตะ + แจ้งเตือนเพื่อดาวน์โหลดในพื้นหลัง แตะค้างเพื่อเปิดตัวดาวน์โหลด ใช้งานกับ YTDLnis และ YTindir + ทำเครื่องหมายการดาวน์โหลดว่าเป็นที่ดูแล้ว + วิดีโอที่ดาวน์โหลดด้วยตัวดาวน์โหลดภายนอกจะถูกเพิ่มลงในประวัติการรับชม YouTube ของคุณ + Full HD (1080p) + ปานกลาง (192 kbps) + ดาวน์โหลดด่วน + วิดีโอ + เสียง + ตัวเลือกทั้งหมด การแปลเสียงบรรยาย อ่านคำบรรยายวิดีโอออกเสียงด้วยระบบแปลงข้อความเป็นเสียงบนอุปกรณ์ โดยซิงโครไนซ์กับการเล่น @@ -588,14 +598,14 @@ Second \"item\" text" คิวจะถูกเก็บไว้หลังจากการปิดและเปิดแอปอีกครั้ง สลับการดำเนินการบันทึกและคิว แตะเพื่อดูตัวเลือกในคิว แตะค้างไว้เพื่อบันทึกไปยังดูภายหลัง - แทนที่ปุ่มคิวในเมนูแบบเลื่อนออกของฟีด - เปิดคิว Morphe แทนคิว YouTube + + ประเภทเล่นถัดไปในคิว + ค่าเริ่มต้นThe + คิวแบบเนทีฟ + คิว Morphe + ไคลเอนต์ฝั่งไคลเอนต์ของคิว Morphe เพิ่มรายการคิวในเมนูแบบเลื่อนออกของฟีด เพิ่มรายการเมนูแบบเลื่อนออกเพิ่มเติม เปิดใช้งานนี้หาก \"เพิ่มในคิว\" ไม่แสดง - คิวแบบพรีเมียม - "แทนที่คิว Morphe ด้วยคิวที่ถูกเก็บในอุปกรณ์ เล่นต่อเนื่องได้เอง และจัดลำดับใหม่ได้ด้วยการลาก - -ข้อมูล: วิดีโอถัดไปในคิวจะเริ่มเล่นเฉพาะเมื่อ YouTube อยู่บนหน้าจอเท่านั้น" คิว คิว (%s) ล้างทั้งหมด @@ -640,6 +650,20 @@ Second \"item\" text" ลบและรีโหลดวิดีโอ เปิดคิว บันทึกคิวไปยังเพลย์ลิสต์ + + บันทึกเพลย์ลิสต์แล้ว + บันทึกไปยัง %s + ล้างคิว + ล้างคิวเรียบร้อยแล้ว + คิว + เล่นล่าสุดในคิว + เล่นถัดไปในคิว + YouTube ไม่สามารถแสดงคิวได้ + คิวยังไม่พร้อมสำหรับวิดีโอนี้ เปิดเครื่องเล่นวิดีโอและลองอีกครั้ง + เพิ่ม Mix ไปยังคิวหรือดู Mix? + ดู + เพิ่มไปยังคิว + กำลังโหลด Mix… เปิดช่องจากอวาตาร์สด ป้องกันไม่ให้การถ่ายทอดสดเปิดเมื่อแตะที่อวาตาร์ของช่อง @@ -853,8 +877,11 @@ Second \"item\" text" เคลื่อนไหวการเปลี่ยนภาพเมื่อสลับระหว่างแท็บ ปิดใช้งานการซ่อนแถบนำทางอัตโนมัติ ทำให้แถบนำทางมองเห็นได้ในขณะที่เลื่อน - ปิดการใช้งานแถบนำทางแบบโปร่งแสง - ทำให้การนำทางและแถบสถานะทึบแสงโดยสมบูรณ์ + สไตล์ของแถบนำทาง + ค่าเริ่มต้น + โปร่งแสง + โปร่งใส + ทึบแสง แถบเครื่องมือ เลือกส่วนประกอบที่จะปรากฏในแถบเครื่องมือ ซ่อนปุ่มแคสต์ diff --git a/patches/src/main/resources/addresources/values-tr-rTR/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-tr-rTR/shared-youtube/strings.xml index d1f15eefc3..c9dd069b85 100644 --- a/patches/src/main/resources/addresources/values-tr-rTR/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-tr-rTR/shared-youtube/strings.xml @@ -150,6 +150,13 @@ Oturum açmak için, devam et butonuna basın." Medya bildirimi, kilit ekranı ve kulaklık kontrollerindeki Önceki ve Sonraki butonlarını gizler İlerleme çubuğunu devre dışı bırak Bildirim panelindeki ve kilit ekranındaki ilerleme çubuğunu sürüklemeyi engeller + Önceki & Sonraki düğmeleriyle arama + Medybildirimi, kilit ekranı ve kulaklık için medya kontrolünü videoları değiştirmek yerine arama yapmak üzere değiştirir + Geri sarma süresi + Önceki düğmesiyle geri sarılacak saniye sayısı + İleri sarma süresi + Sonraki düğmesiyle ileri sarılacak saniye sayısı + Arama süresi 1 ile %s saniye arasında olmalıdır. Varsayılan kullanılıyor QUIC protokolünü devre dışı bırak diff --git a/patches/src/main/resources/addresources/values-tr-rTR/shared/strings.xml b/patches/src/main/resources/addresources/values-tr-rTR/shared/strings.xml index 101711625a..e1d81a6626 100644 --- a/patches/src/main/resources/addresources/values-tr-rTR/shared/strings.xml +++ b/patches/src/main/resources/addresources/values-tr-rTR/shared/strings.xml @@ -152,7 +152,7 @@ Beklenmedik olaylar hakkında bilgilendirilmeyeceksiniz" %s bayrağın yarısı engellenir ve her yeniden başlatmanın ardından bu davranışın hâlâ devam edip etmediğini belirtirsin. Başlamadan önce bu davranışı bir kez yeniden üret; aksi takdirde uygulamanın henüz istemediği bayraklar aramanın dışında kalır." - Başlangıç + Başla "Aranacak özellik bayrağı yok. İkili aramada yalnızca uygulamanın eriştiği ve şu anda zorla açılmamış olan bayraklar kullanılabilir." diff --git a/patches/src/main/resources/addresources/values-tr-rTR/youtube/strings.xml b/patches/src/main/resources/addresources/values-tr-rTR/youtube/strings.xml index c0681673dc..f8ca8efe52 100644 --- a/patches/src/main/resources/addresources/values-tr-rTR/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-tr-rTR/youtube/strings.xml @@ -474,6 +474,16 @@ Soru düğmesine sahip olan bir video bir kez açıldıktan sonra bu özellik, m Üçüncü taraf indirme uygulamalarıyla videolar, podcast\'ler veya şarkılar indirin Oynatıcı paneli indirme düğmesini göster Video oynatıcısında harici indiricinizi açan bir düğme gösterir + Dokunulduğunda hızlı indir + İndirme işlemini arka planda başlatır. İndiriciyi açmak için dokunun ve basılı tutun. YTDLnis ve YTindir ile çalışır + İndirilenleri izlendi olarak işaretle + Harici indirici ile indirilen videolar YouTube izleme geçmişinize eklenir + Full HD (1080p) + Orta (192 kbps) + Hızlı indir + Video + Ses + Tüm seçenekler Seslendirme çevirisi Cihaz üzerindeki metin-konuşma özelliğini kullanarak video altyazılarını oynatmayla senkronize bir şekilde yüksek sesle okuyun. @@ -589,14 +599,14 @@ Bilgi: Canlı yayınlarda çalışmayabilir" Uygulama yeniden başlatıldığında sıra korunur Kaydet ve Sıra eylemlerini değiştir Sıra seçenekleri için dokunun, \'Daha Sonra İzle\'ye kaydetmek için basılı tutun - Akış açılır sıra düğmesini kullan - YouTube sırası yerine Morphe sırasını açar + + Sıranın başına ekleme tipi + Varsayılan + Yerel sıra + Morphe sırası + İstemci taraflı Morphe sırası Akış açılır menüsüne kuyruk menü öğesi ekle Ek bir açılır menü öğesi ekler. \"Kuyruğa ekle\" gösterilmiyorsa bunu etkinleştirin. - Premium tarzı kuyruk - "Morphe kuyruğunu cihazda bulunan, kendi kendine oynayan ve sürükleyerek yeniden sıralanabilen bir kuyrukla değiştirir - -Bilgi: Sıradaki video yalnızca YouTube ekrandayken başlar" Kuyruk Kuyruk (%s) Tümünü temizle @@ -641,6 +651,20 @@ Bilgi: Sıradaki video yalnızca YouTube ekrandayken başlar" Videoyu kaldır ve yeniden yükle Sırayı aç Sırayı oynatma listesine kaydet + + Oynatma listesi kaydedildi + %s\'ye kaydedildi + Sırayı temizle + Sıra temizlendi + Sıra + Sıranın sonuna ekle + Sıranın başına ekle + YouTube sırayı görüntüleyemedi. + Bu video için sıra hazır değil. Video oynatıcıyı açın ve tekrar deneyin. + Mix sıraya mı eklensin yoksa izlensin mi? + İzle + Sıraya ekle + Mix yükleniyor… Canlı avatardan kanalı aç Kanal avatarına dokunulduğunda canlı yayının açılmasını engeller @@ -854,8 +878,11 @@ Ayarlar'a erişmek için Ayarlar düğmesini Genel → Araç Çubuğu → Ayarla Sekmeler arasında animasyonlu geçişler yapar Gezinme çubuğunun otomatik gizlenmesini devre dışı bırak Kaydırırken gezinme çubuğunu görünür tutar - Yarı saydam gezinti çubuğunu devre dışı bırak - Gezinti ve durum çubuğunu tamamen opak hale getirir + Gezinme çubuğu stili + Varsayılan + Yarı saydam + Şeffaf + Opak Araç Çubuğu Araç çubuğunda hangi bileşenlerin görüneceğini seçin Yayınla düğmesini gizle diff --git a/patches/src/main/resources/addresources/values-uk-rUA/music/strings.xml b/patches/src/main/resources/addresources/values-uk-rUA/music/strings.xml index 46d4bd33f3..6159b74bbd 100644 --- a/patches/src/main/resources/addresources/values-uk-rUA/music/strings.xml +++ b/patches/src/main/resources/addresources/values-uk-rUA/music/strings.xml @@ -462,7 +462,7 @@ Second \"item\" text" Відображення Синхронізація Панель - Кінцева точка AI + Кінцева точка ШІ Усі постачальники вимкнено, використовуються вбудовані та вбудовані в файл тексти пісень Скинути порядок постачальників? Відновити типовий порядок пріоритету постачальників @@ -623,24 +623,24 @@ Second \"item\" text" Переклад Романізація Постачальник перекладу - Google (типовий, безкоштовний) + Google (типовий, безплатний) Сумісний з OpenAI (власний) - Постачальник романизації + Постачальник романізації Google (типовий) Локальний / офлайн-словник Сумісний з OpenAI (власний) - Розділ AI + Розділ ШІ Використовується лише тоді, коли постачальник не надає позначок розділів, таких як \"Приспів\" або \"Куплет\", які тоді визначає ваша модель, сумісна з OpenAI - AI & хмарні кінцеві точки - Налаштуйте API-ключі, моделі, кінцеві точки та шаблони підказок для AI-постачальників. + ШІ та хмарні кінцеві точки + Налаштуйте ключі API, моделі, кінцеві точки та шаблони запитів для постачальників ШІ URL-адреса кінцевої точки API (наприклад, https://api.openai.com/v1/chat/completions) Шаблон запиту Заповнювачі: {language}, {task}, {title}, {artist}, {count}, {lines} Запит збережено - Запит збережено, але кінцева точка AI ще непридатна до використання. Спочатку налаштуйте її в розділі \"Налаштування AI\" + Запит збережено, але кінцева точка ШІ ще непридатна до використання. Спочатку налаштуйте її в розділі \"Налаштування ШІ\" Запит скинуто Схоже, ти вставив уже готовий переклад замість англійського оригіналу. Надішли, будь ласка, англійський рядок, і я перекладу. - Налаштування перекладу & романизації збережено + Налаштування перекладу та романізації збережено Налаштування скинуто до типових Базова URL-адреса має починатися з http:// або https:// Перевірити з\'єднання @@ -652,18 +652,18 @@ Second \"item\" text" Отримати моделі Отримання моделей… Виберіть модель - Не вдалося отримати моделі. Перевірте API-ключ або з’єднання. + Не вдалося отримати моделі. Перевірте ключ API або з\'єднання Налаштування Gemini API - Сумісність з OpenAI (власна кінцева точка) + Сумісний з OpenAI (власна кінцева точка) Налаштування DeepL API - API-ключ + Ключ API Модель - Базовий URL - API-ключ (необов\'язково) - Промпт для перекладу - Додатковий тіло запиту (необов\'язковий JSON) + Базова URL-адреса + Ключ API (необов\'язково) + Запит для перекладу + Додаткове тіло запиту (необов\'язковий JSON) Ключ автентифікації (Free або Pro) - Налаштуйте API-ключі, моделі, кінцеві точки та промпти + Налаштуйте ключі API, моделі, кінцеві точки та запити 8.23.51 - Вимкнути товсті іконки 9.13.51 - Відновити вкладки \"Далі\", \"Текст\" та \"Схожі\" в плеєрі diff --git a/patches/src/main/resources/addresources/values-uk-rUA/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-uk-rUA/shared-youtube/strings.xml index 398f80d956..0ac65e1bd2 100644 --- a/patches/src/main/resources/addresources/values-uk-rUA/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-uk-rUA/shared-youtube/strings.xml @@ -151,6 +151,13 @@ Second \"item\" text" Приховує кнопки \"Попередній\" та \"Наступний\" у сповіщеннях медіа, на екрані блокування та в елементах керування навушниками Вимкнути смугу перемотування Запобігає перетягуванню смуги перемотування на екрані блокування та панелі сповіщень + Шукати за допомогою кнопок «Назад» та «Вперед» + Змінює керування сповіщенням про медіа, екраном блокування та навушниками для пошуку замість перемикання відео + Довжина перемотки назад + Кількість секунд для перемотки назад кнопкою Попереднє + Довжина перемотки вперед + Кількість секунд для перемотки вперед кнопкою Наступне + Довжина пошуку має бути від 1 до %s секунд. Використано типові значення Вимкнути протокол QUIC diff --git a/patches/src/main/resources/addresources/values-uk-rUA/youtube/strings.xml b/patches/src/main/resources/addresources/values-uk-rUA/youtube/strings.xml index d73fdbdc67..5e8e39502e 100644 --- a/patches/src/main/resources/addresources/values-uk-rUA/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-uk-rUA/youtube/strings.xml @@ -331,7 +331,7 @@ Second \"item\" text" Прихованих відео за весь час Скинути лічильник за останні 24 години? Скинути лічильник за весь час? - Фільтр AI каналів + Фільтр ШІ каналів Приховує відео з каналів, позначених спільнотою AiSList як згенеровані штучним інтелектом Список блокування: висока впевненість Список попереджень: помірна впевненість @@ -352,25 +352,25 @@ Second \"item\" text" "AiSList це список YouTube-каналів із відкритим кодом, які публікують контент, створений ШІ, і який підтримують волонтери. Користувачі повідомляють про канали, і їх перевіряють, перш ніж додати Список завантажується з GitHub, а відео фільтруються на вашому пристрої" - Повідомити про AI-канали + Повідомити про ШІ-канали Додати пункт звітування до меню відео Додає пункт до меню відео для звітування про канал відео до AiSList Ім\'я відправника Ім\'я, що показується поруч із вашими звітами в публічному рейтингу AiSList. Звітування недоступне, поки це поле порожнє - Повідомити AI-канал + Повідомити ШІ-канал "Надіслати %s до AiSList, використовуючи це відео як доказ? Повідомляйте лише про канали, які: -• Використовують AI-голоси, візуали, сценарії чи музику як основний контент -• Масово створюють контент за допомогою AI-інструментів з мінімальною людською роботою +• Використовують ШІ-голоси, візуали, сценарії чи музику як основний контент +• Масово створюють контент за допомогою ШІ-інструментів з мінімальною людською роботою -Не повідомляйте про канали, які лише обговорюють AI, або використовують AI лише для деяких частин своїх відео. +Не повідомляйте про канали, які лише обговорюють ШІ, або використовують ШІлише для деяких частин своїх відео. Звіти публічні та розглядаються перед додаванням каналу до списку. Видалення можливе лише через запит на GitHub" Цей канал вже є в списку блокування AiSList Цей канал вже є в списку попереджень AiSList Повідомити - Встановіть ім\'я відправника в налаштуваннях фільтра AI-каналів, щоб повідомляти про канали + Встановіть ім\'я відправника в налаштуваннях фільтра ШІ-каналів, щоб повідомляти про канали Пошук каналу… Не вдалося знайти канал цього відео Про канал повідомлено, очікується перевірка @@ -473,6 +473,16 @@ Second \"item\" text" Завантажувати відео, подкасти або пісні за допомогою сторонніх застосунків для завантаження Показувати кнопку завантаження в оверлеї плеєра Показує кнопку у відеоплеєрі, яка відкриває ваш зовнішній завантажувач + Швидке завантаження одним натисканням + Запитує про завантаження у фоновому режимі. Натисніть і утримуйте, щоб відкрити програму для завантажень. Працює з YTDLnis та YTindir + Позначати завантажені як переглянуті + Відео, завантажені зовнішньою програмою для завантажень, додаються до вашої історії переглядів YouTube + Full HD (1080p) + Середнє (192 кбіт/с) + Швидке завантаження + Відео + Аудіо + Усі параметри Голосовий переклад Зачитує субтитри відео вголос за допомогою локального синтезу мовлення, синхронізованого з відтворенням @@ -588,14 +598,14 @@ Second \"item\" text" Черга зберігається після перезапуску застосунку Поміняти місцями кнопки \"Зберегти\" та \"Додати в чергу\" Натисніть для параметрів черги, утримуйте, щоб зберегти до \"Переглянути пізніше\" - Замінити кнопку черги у висувному меню стрічки - Відкриває чергу Morphe замість черги YouTube + + Тип черги \'Відтворити наступним\' + Типові + Системна черга + Черга Morphe + Клієнтська черга Morphe Додати пункт меню черги у висувне меню стрічки Додає додатковий пункт у висувне меню. Увімкніть цю опцію, якщо пункт \"Додати до черги\" не показується - Черга в стилі Premium - "Замінює чергу Morphe на таку, що зберігається на пристрої, відтворюється самостійно та може бути переупорядкована перетягуванням - -Інформація: наступне відео в черзі запускається лише поки YouTube відкрито на екрані" Черга Черга (%s) Очистити все @@ -640,6 +650,20 @@ Second \"item\" text" Видалити з черги та перезавантажити відео Відкрити чергу Зберегти чергу як список відтворення + + Список відтворення збережено + Збережено в %s + Очистити чергу + Чергу очищено + Черга + Відтворити останнім у черзі + Відтворити наступним у черзі + YouTube не може відобразити чергу. + Черга не готова для цього відео. Відкрийте плеєр відео та спробуйте знову. + Додати Мікс до черги чи переглянути Мікс? + Переглянути + Додати до черги + Завантаження Міксу… Відкривати канал через аватар трансляції Запобігає відкриттю трансляції при натисканні на аватар каналу @@ -853,8 +877,11 @@ Second \"item\" text" Анімує переходи між вкладками Вимкнути автоприховування панелі навігації Залишає панель навігації видимою під час прокручування - Вимкнути напівпрозорість навігаційної панелі - Робить навігаційну панель та панель стану повністю непрозорими + Стиль панелі навігації + Типовий + Напівпрозорий + Прозорий + Непрозорий Панель інструментів Виберіть, які компоненти будуть показуватися на панелі інструментів Приховати кнопку \"Трансляція\" @@ -953,7 +980,7 @@ Second \"item\" text" Плеєр Shorts Виберіть, які компоненти будуть показуватися у плеєрі Shorts Вимкнути подвійне натискання для вподобання - Приховати кнопку \"AI\" + Приховати кнопку \"ШІ\" Приховати мітку \"Дубляж\" Приховати панель каналу Приховати кнопку \"Коментарі\" diff --git a/patches/src/main/resources/addresources/values-ur-rIN/youtube/strings.xml b/patches/src/main/resources/addresources/values-ur-rIN/youtube/strings.xml index e7a28ceb72..69d46a7cf7 100644 --- a/patches/src/main/resources/addresources/values-ur-rIN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ur-rIN/youtube/strings.xml @@ -76,7 +76,9 @@ Second \"item\" text" + + diff --git a/patches/src/main/resources/addresources/values-uz-rUZ/youtube/strings.xml b/patches/src/main/resources/addresources/values-uz-rUZ/youtube/strings.xml index 1b59e9bc90..8a6628ace2 100644 --- a/patches/src/main/resources/addresources/values-uz-rUZ/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-uz-rUZ/youtube/strings.xml @@ -444,8 +444,8 @@ Ma'lumot: Jonli efirlarda ishlamasligi mumkin" Kirish qilinmagan Saqlash uchun teging, navbatga qo\'shish uchun bosib ushlab turing Navbat opsiyalari uchun bosing, \'Keyinroq ko\'rish\'ga saqlash uchun bosib turing - Oqim menyusi navbat tugmasini bekor qilish - YouTube navbati o\'rniga Morphe navbatini ochadi + + Standart Lenta ochiladigan menyusiga navbat bandini qo\'shish Qo\'shimcha ochiladigan menyu bandini qo\'shadi. Agar \"Navbatga qo\'shish\" ko\'rsatilmasa, buni yoqing @@ -472,6 +472,7 @@ Ma'lumot: Jonli efirlarda ishlamasligi mumkin" Navbatni ochish Navbatni pleylistga saqlash Snackbar ni yashirish + Jonli efir avatarka dan kanalni ochish Kanal avatarkasini bosganda jonli efirning ochilishining oldini olish @@ -651,8 +652,7 @@ Ma'lumot: Jonli efirlarda ishlamasligi mumkin" Varaqlar oʻrtasida almashishda animatsiyali oʻtishlarni yaratadi Avtomatik yashiriladigan navigatsiya panelini oʻchirish Prokrul paytida navigatsiya panelini koʻrinishda ushlab turadi - Shaffof navigatsiya panelini o\'chirish - Navigatsiya va holat panelini to\'liq noaniq qiladi + Standart Navigatsiya panelini yashirish Pleerdagi paydo boʻladigan tugmalarni tanlang \'Audio Track\' menyusini yashirish diff --git a/patches/src/main/resources/addresources/values-vi-rVN/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-vi-rVN/shared-youtube/strings.xml index 9678961385..5ad51e4a74 100644 --- a/patches/src/main/resources/addresources/values-vi-rVN/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-vi-rVN/shared-youtube/strings.xml @@ -151,6 +151,13 @@ Bấm tiếp tục bên dưới để đăng nhập" Ẩn các nút Trước và Tiếp theo trong thông báo phương tiện, màn hình khóa và điều khiển tai nghe Tắt thanh trượt Ngăn không cho kéo thanh tiến trình trên màn hình khóa và bảng thông báo + Tìm kiếm với nút Trước & Sau + Thay đổi điều khiển cho thông báo phương tiện, màn hình khóa và tai nghe để tua thay vì chuyển video + Độ dài tua lại + Giây để tua lại bằng nút Trước + Độ dài tua nhanh + Giây để tua nhanh bằng nút Sau + Độ dài tua phải nằm trong khoảng từ 1 đến %s giây. Sử dụng mặc định Tắt giao thức QUIC diff --git a/patches/src/main/resources/addresources/values-vi-rVN/youtube/strings.xml b/patches/src/main/resources/addresources/values-vi-rVN/youtube/strings.xml index 68f389bb2d..a81d850356 100644 --- a/patches/src/main/resources/addresources/values-vi-rVN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-vi-rVN/youtube/strings.xml @@ -473,6 +473,16 @@ Sau khi mở một video có nút Ask một lần, tính năng này hoạt độ Tải video, podcast hoặc nhạc bằng ứng dụng tải xuống của bên thứ ba Hiển thị nút tải xuống lớp phủ người chơi Hiển thị một nút trong trình phát video mở ứng dụng tải xuống bên ngoài của bạn + Tải xuống nhanh chóng + Thông báo tải xuống trong nền. Chạm và giữ để mở trình tải xuống. Hoạt động với YTDLnis và YTindir. + Đánh dấu các tệp đã tải xuống là đã xem + Các video được tải xuống bằng công cụ tải xuống bên ngoài sẽ được thêm vào lịch sử xem YouTube của bạn. + Full HD (1080p) + Trung bình (192 kbps) + Tải nhanh + Video + Âm thanh + Tất cả tùy chọn Dịch giọng nói Đọc to phụ đề video bằng tính năng chuyển văn bản thành giọng nói trên thiết bị, đồng bộ với video @@ -588,14 +598,14 @@ Lưu ý: Có thể không hoạt động với video phát trực tiếp"Hàng chờ vẫn được lưu sau khi tắt đi bật lại ứng dụng Đổi chỗ Lưu và tùy chọn Hàng chờ Chạm để xem các tùy chọn hàng đợi, chạm và giữ để lưu vào Watch later - Ghi đè nút hàng chờ trong trang video - Mở hàng chờ Morphe thay vì hàng chờ YouTube + + Phát mục tiếp theo trong hàng đợi + Mặc định + Hàng chờ gốc + Hàng chờ Morphe + Máy khách hàng chờ Morphe phía client Thêm menu hàng chờ trên trang video Thêm tùy chọn vào menu bổ sung. Bật tùy chọn này nếu không thấy \"Thêm vào hàng chờ\" - Hàng đợi kiểu Premium - "Thay thế hàng đợi Morphe bằng hàng đợi được lưu trên thiết bị, tự động phát liên tục và có thể sắp xếp lại bằng cách kéo - -Thông tin: Video tiếp theo trong hàng đợi chỉ bắt đầu phát khi YouTube đang hiển thị trên màn hình" Hàng đợi Hàng đợi (%s) Xóa tất cả @@ -640,6 +650,20 @@ Thông tin: Video tiếp theo trong hàng đợi chỉ bắt đầu phát khi Yo Xóa và tải lại video Mở hàng chờ Lưu hàng chờ vào danh sách phát + + Danh sách phát đã lưu + Đã lưu vào %s + Xóa hàng đợi + Hàng đợi đã được xóa + Hàng chờ + Phát cuối trong hàng chờ + Phát mục tiếp theo trong hàng chờ + YouTube không thể hiển thị danh sách chờ. + Hàng đợi chưa sẵn sàng cho video này. Hãy mở trình phát video và thử lại. + Thêm Mix vào hàng chờ hay xem Mix? + Xem + Thêm vào hàng chờ + Đang tải bản phối… Mở kênh khi bấm vào ảnh đại diện có sự kiện trực tiếp Không mở video phát trực tiếp khi bấm vào ảnh đại diện của kênh @@ -853,8 +877,11 @@ Cần thiết lập \"Cookie phụ đề\"" Tạo hoạt ảnh khi chuyển tab Tắt tự động ẩn thanh điều hướng Giữ thanh điều hướng hiển thị khi cuộn - Vô hiệu hóa thanh điều hướng trong suốt - Làm cho thanh điều hướng và thanh trạng thái hoàn toàn không trong suốt + Kiểu thanh điều hướng + Mặc định + Bán trong suốt + Trong suốt + Đục Thanh công cụ Chọn những thành phần sẽ xuất hiện trên thanh công cụ Ẩn nút truyền diff --git a/patches/src/main/resources/addresources/values-zh-rCN/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-zh-rCN/shared-youtube/strings.xml index 8803529cef..5f3966ffff 100644 --- a/patches/src/main/resources/addresources/values-zh-rCN/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-zh-rCN/shared-youtube/strings.xml @@ -151,6 +151,13 @@ Second \"item\" text" 在媒体通知、锁屏和耳机控制中隐藏上一个和下一个按钮 禁用进度条 防止在锁屏和通知面板上拖动进度条 + 使用上一首 & 下一首按钮搜索 + 更改媒体通知、锁屏和耳机的控制以进行搜索而不是切换视频 + 倒带长度 + 使用上一首按钮倒带的秒数 + 快进长度 + 使用下一首按钮快进的秒数 + 搜索长度必须在 %s 到 1 秒之间。使用默认值 禁用QUIC协议 diff --git a/patches/src/main/resources/addresources/values-zh-rCN/youtube/strings.xml b/patches/src/main/resources/addresources/values-zh-rCN/youtube/strings.xml index 461acd8884..39712be2f3 100644 --- a/patches/src/main/resources/addresources/values-zh-rCN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-zh-rCN/youtube/strings.xml @@ -266,7 +266,7 @@ Second \"item\" text" 隐藏评论区 隐藏主页动态的评论区 隐藏社区指南 - 隐藏“创建 Short”按钮 + 隐藏“创作短视频”按钮 隐藏点踩按钮 隐藏表情符号按钮 隐藏筛选栏选项 @@ -473,6 +473,16 @@ Second \"item\" text" 使用第三方下载应用程序下载视频、播客或歌曲 显示播放器覆盖层下载按钮 在视频播放器中显示一个打开外部下载器的按钮 + 点击快速下载 + 提示在后台下载。长按以打开下载器。支持 YTDLnis 和 YTindir + 将下载内容标记为已观看 + 使用外部下载器下载的视频将添加到您的 YouTube 观看历史记录中 + 全高清(1080p) + 中(192 kbps) + 快速下载 + 视频 + 音频 + 所有选项 语音旁白翻译 使用设备上的文本转语音功能朗读视频字幕,并与播放同步 @@ -588,14 +598,14 @@ Second \"item\" text" 重启应用后队列仍会保留 交换保存和队列操作 点击可查看队列选项,长按可保存到“稍后观看” - 覆盖信息流浮动队列按钮 - 打开 Morphe 队列,而不是 YouTube 队列 + + 在队列中播放下一个 + 默认 + 原生队列 + Morphe 队列 + Morphe 客户端队列 添加信息流弹出菜单项 添加额外的弹出菜单项。如果未显示“添加到队列”,请启用此选项 - Premium 样式队列 - "将 Morphe 队列替换为设备本地队列,可自行连续播放,并可通过拖动重新排序 - -提示:仅当 YouTube 在屏幕上时,下一个队列视频才会开始播放" 队列 队列 (%s) 全部清除 @@ -640,6 +650,20 @@ Second \"item\" text" 移除并重新加载视频 打开队列 将队列保存到播放列表 + + 播放列表已保存 + 已保存至 %s + 清除队列 + 队列已清除 + 队列 + 在队列末尾播放 + 在队列中播放下一个 + YouTube 无法显示队列。 + 该视频队列尚未就绪。请打开视频播放器并再试一次。 + 将混音添加到队列或观看混音? + 观看 + 添加到队列 + 正在加载混音…… 从直播头像打开频道 防止点击频道头像时打开直播 @@ -824,7 +848,7 @@ Second \"item\" text" 隐藏 Shorts 按钮 - 隐藏创建按钮 + 隐藏创作按钮 隐藏订阅按钮 @@ -835,7 +859,7 @@ Second \"item\" text" 设置按钮位置 设置按钮操作 启用时打开 Morphe 设置,禁用时打开 YouTube 设置 - 交换“创建”按钮与“通知”按钮 + 交换“创作”按钮与“通知”按钮 如果更改此设置没有生效,请尝试切换到无痕模式。 隐藏导航栏按钮标签 隐藏新内容圆点 @@ -853,8 +877,11 @@ Second \"item\" text" 软光上子检查些“通过”软光浩通的新闻 禁用自动隐藏导航栏 隐藏“音轨”菜单页脚 - Disable translucent navigation bar - Makes the navigation and status bar fully opaque + 导航栏样式 + 默认 + 半透明 + 透明 + 不透明 工具栏 选择工具栏中显示哪些组件 隐藏“投屏”按钮 diff --git a/patches/src/main/resources/addresources/values-zh-rTW/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-zh-rTW/shared-youtube/strings.xml index 35b12212ee..f8ed7b73bc 100644 --- a/patches/src/main/resources/addresources/values-zh-rTW/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-zh-rTW/shared-youtube/strings.xml @@ -151,6 +151,13 @@ Second \"item\" text" 隱藏媒體通知、鎖定畫面和耳機控制項中的上一首與下一首按鈕 停用進度列 防止在鎖定畫面和通知面板上拖曳進度列 + 使用上一首 & 下一首按鈕搜尋 + 修改媒體通知、鎖定螢幕和耳機的 Controls,以進行搜尋而不是切換影片 + 倒帶長度 + 使用「上一首」按鈕倒帶的秒數 + 快轉長度 + 使用「下一首」按鈕快轉的秒數 + 搜尋長度必須介於 1 到 %s 秒之間。系統將使用預設值 停用 QUIC 協定 diff --git a/patches/src/main/resources/addresources/values-zh-rTW/youtube/strings.xml b/patches/src/main/resources/addresources/values-zh-rTW/youtube/strings.xml index 6fd3ba172d..271a4f1ba4 100644 --- a/patches/src/main/resources/addresources/values-zh-rTW/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-zh-rTW/youtube/strings.xml @@ -473,6 +473,16 @@ Second \"item\" text" 使用第三方下載應用程式下載影片、播客或歌曲 顯示播放器覆蓋層下載按鈕 在影片播放器中顯示一個按鈕,該按鈕可開啟您的外部下載器 + 點擊快速下載 + 提示在背景下載。長按以開啟下載器。支援 YTDLnis 與 YTindir + 將下載內容標記為已觀看 + 使用外部下載器下載的影片將被添加到您的 YouTube 觀看記錄中 + Full HD (1080p) + Medium (192 kbps) + 快速下載 + 影片 + 音訊 + 所有選項 語音轉錄翻譯 使用裝置上的文字轉語音功能朗讀影片字幕,並與播放同步 @@ -588,14 +598,14 @@ Second \"item\" text" 重新啟動應用程式後,佇列會保留 交換「儲存」和「加入佇列」動作 點擊以開啟佇列選項,長按以儲存至「稍後觀看」 - 覆蓋播放列表彈出視窗按鈕 - 開啟 Morphe 佇列,而非 YouTube 佇列 + + 佇列中下一個播放類型 + 預設 + 原生佇列 + Morphe 佇列 + Morphe 佇列客戶端 新增動態消息彈出式佇列選單項目 新增額外的彈出式項目。若未顯示「加入佇列」,請啟用此選項 - Premium 風格佇列 - "以存在於裝置上、可自行播放且可拖曳重新排序的佇列取代 Morphe 佇列 - -資訊:下一個佇列中的影片只會在 YouTube 位於畫面上時開始播放" 佇列 佇列 (%s) 全部清除 @@ -640,6 +650,20 @@ Second \"item\" text" 移除並重新載入影片 開啟佇列 將佇列儲存至播放列表 + + 已儲存播放清單 + 已儲存在 %s + 清除佇列 + 佇列已清除 + 佇列 + 佇列中最後一個播放 + 播放佇列中的下一個 + YouTube 無法顯示佇列。 + 佇列尚未為此影片就緒。開啟影片播放器,然後再試一次。 + 將混合音樂新增至佇列或觀看混合音樂? + 觀看 + 新增至佇列 + 載入混合音樂中…… 從直播形象開啟頻道 防止點擊頻道形象時開啟直播 @@ -853,8 +877,11 @@ Second \"item\" text" 在切換分頁時,讓轉換效果更生動 停用自動隱藏導航欄 在捲動時,保持導覽列可見 - 停用半透明導覽列 - 讓導覽列和狀態列完全不透明 + 導覽列樣式 + 預設 + 半透明 + 透明 + 不透明 工具列 選擇工具列中顯示的元件 隱藏「投放」按鈕 diff --git a/patches/src/main/resources/addresources/values-zu-rZA/youtube/strings.xml b/patches/src/main/resources/addresources/values-zu-rZA/youtube/strings.xml index 174f25cd1a..1a02f7efd7 100644 --- a/patches/src/main/resources/addresources/values-zu-rZA/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-zu-rZA/youtube/strings.xml @@ -76,7 +76,9 @@ Second \"item\" text" + + From 0ac8e875ba3696855356958f77b838b063245cfd Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Sun, 25 Oct 2026 15:24:51 +0000 Subject: [PATCH 056/189] chore: Release v1.49.0-dev.6 [skip ci] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## [1.49.0-dev.6](https://github.com/MorpheApp/morphe-patches/compare/v1.49.0-dev.5...v1.49.0-dev.6) (2026-10-25) ### 🐛 Bug Fixes * **YouTube - Captions:** Captions not shown in picture-in-picture on 21.16 - 21.28 ([#4024](https://github.com/MorpheApp/morphe-patches/issues/4024)) ([b2688c5](https://github.com/MorpheApp/morphe-patches/commit/b2688c5eb957f3ffd142595ff4ee7b491e3b86ae)) * **YouTube - Hide layout components:** Fix actions blocked by Hide Snackbar ([#4038](https://github.com/MorpheApp/morphe-patches/issues/4038)) ([9a0fee9](https://github.com/MorpheApp/morphe-patches/commit/9a0fee9ec2846ba2db31cbfc2e4c2ac9b14895b7)) * **YouTube - Playback speed:** Channel speed whitelist is not applied to preloaded Shorts ([4f108b5](https://github.com/MorpheApp/morphe-patches/commit/4f108b596a71adbbf223b59faaabcfbe2469d95a)) ### ✨ New Features * **YouTube - Add to queue:** Add "Native queue" setting ([#3953](https://github.com/MorpheApp/morphe-patches/issues/3953)) ([372eea2](https://github.com/MorpheApp/morphe-patches/commit/372eea295ec4521b9ccdb93a3f188f81a6ad1a6c)) * **YouTube - Downloads:** Add "Quick download on tap" setting ([#3883](https://github.com/MorpheApp/morphe-patches/issues/3883)) ([dd97d93](https://github.com/MorpheApp/morphe-patches/commit/dd97d93628a3feb7bc8758139b01fbe42a532778)) * **YouTube - Media notification controls:** Add options to seek with Previous & Next buttons ([#3955](https://github.com/MorpheApp/morphe-patches/issues/3955)) ([2b07abb](https://github.com/MorpheApp/morphe-patches/commit/2b07abb628fec3bf625a09ab7d019f915c4dfa23)) * **YouTube - Navigation bar:** Add "Navigation bar style" setting ([db9af37](https://github.com/MorpheApp/morphe-patches/commit/db9af377093121e575fec80ba40e0025dfd66f81)) ### 🚀 Updated App Support * **Reddit:** Add experimental support for `2026.43.0` ([6407d5e](https://github.com/MorpheApp/morphe-patches/commit/6407d5efa0f7d99fb54909bc39e2074e8f60ac42)) * **Reddit:** Add support for `2026.40.0` ([fcc02f7](https://github.com/MorpheApp/morphe-patches/commit/fcc02f77d5a9d7a13b74d14c12b302f9d9902c61)) --- CHANGELOG.md | 20 ++++ README.md | 8 +- gradle.properties | 2 +- patches-bundle.json | 8 +- patches-list.json | 283 ++++++++++++++++++++++++++++++++++++++------ 5 files changed, 279 insertions(+), 42 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 55c9750a26..6532cdaaf9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,23 @@ +## [1.49.0-dev.6](https://github.com/MorpheApp/morphe-patches/compare/v1.49.0-dev.5...v1.49.0-dev.6) (2026-10-25) + +### 🐛 Bug Fixes + +* **YouTube - Captions:** Captions not shown in picture-in-picture on 21.16 - 21.28 ([#4024](https://github.com/MorpheApp/morphe-patches/issues/4024)) ([b2688c5](https://github.com/MorpheApp/morphe-patches/commit/b2688c5eb957f3ffd142595ff4ee7b491e3b86ae)) +* **YouTube - Hide layout components:** Fix actions blocked by Hide Snackbar ([#4038](https://github.com/MorpheApp/morphe-patches/issues/4038)) ([9a0fee9](https://github.com/MorpheApp/morphe-patches/commit/9a0fee9ec2846ba2db31cbfc2e4c2ac9b14895b7)) +* **YouTube - Playback speed:** Channel speed whitelist is not applied to preloaded Shorts ([4f108b5](https://github.com/MorpheApp/morphe-patches/commit/4f108b596a71adbbf223b59faaabcfbe2469d95a)) + +### ✨ New Features + +* **YouTube - Add to queue:** Add "Native queue" setting ([#3953](https://github.com/MorpheApp/morphe-patches/issues/3953)) ([372eea2](https://github.com/MorpheApp/morphe-patches/commit/372eea295ec4521b9ccdb93a3f188f81a6ad1a6c)) +* **YouTube - Downloads:** Add "Quick download on tap" setting ([#3883](https://github.com/MorpheApp/morphe-patches/issues/3883)) ([dd97d93](https://github.com/MorpheApp/morphe-patches/commit/dd97d93628a3feb7bc8758139b01fbe42a532778)) +* **YouTube - Media notification controls:** Add options to seek with Previous & Next buttons ([#3955](https://github.com/MorpheApp/morphe-patches/issues/3955)) ([2b07abb](https://github.com/MorpheApp/morphe-patches/commit/2b07abb628fec3bf625a09ab7d019f915c4dfa23)) +* **YouTube - Navigation bar:** Add "Navigation bar style" setting ([db9af37](https://github.com/MorpheApp/morphe-patches/commit/db9af377093121e575fec80ba40e0025dfd66f81)) + +### 🚀 Updated App Support + +* **Reddit:** Add experimental support for `2026.43.0` ([6407d5e](https://github.com/MorpheApp/morphe-patches/commit/6407d5efa0f7d99fb54909bc39e2074e8f60ac42)) +* **Reddit:** Add support for `2026.40.0` ([fcc02f7](https://github.com/MorpheApp/morphe-patches/commit/fcc02f77d5a9d7a13b74d14c12b302f9d9902c61)) + ## [1.49.0-dev.5](https://github.com/MorpheApp/morphe-patches/compare/v1.49.0-dev.4...v1.49.0-dev.5) (2026-10-24) ### 🐛 Bug Fixes diff --git a/README.md b/README.md index 2a2af84e0a..a1503636e1 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ All modifications made by Morphe, along with their dates, can be found in the Gi ## 🩹 Patches list -> **[v1.49.0-dev.5](https://github.com/MorpheApp/morphe-patches/releases/tag/v1.49.0-dev.5)**  •  `dev`  •  191 patches total +> **[v1.49.0-dev.6](https://github.com/MorpheApp/morphe-patches/releases/tag/v1.49.0-dev.6)**  •  `dev`  •  191 patches total

📦 YouTube  •  106 patches
@@ -48,7 +48,7 @@ All modifications made by Morphe, along with their dates, can be found in the Gi | 💊 Patch | 📜 Description | ⚙️ Options | |----------|----------------|-----------| | [AI channel filter](#ai-channel-filter) | Adds options to hide videos from channels flagged as AI-generated by the AiSList community list and to report AI channels from the video menu. | | -| [Add to queue](#add-to-queue) | Overrides the feed flyout 'Play next in queue' with the Morphe video queue. | | +| [Add to queue](#add-to-queue) | Adds a video queue to the feed flyout menu, using either the Morphe queue or a native queue that uses YouTube's playlist interface. | | | [Ambient mode](#ambient-mode) | Forces Ambient mode on and adds options to bypass power saving restrictions for Ambient mode and disable it entirely or in fullscreen. | | | [App refresh rate](#app-refresh-rate) | Adds an option to change the app refresh rate. | | | [Ask flyout menu](#ask-flyout-menu) | Adds a video menu entry that opens Ask for the video and summarizes the video. This patch works with YouTube 21.16.256 and newer. | | @@ -225,8 +225,8 @@ All modifications made by Morphe, along with their dates, can be found in the Gi **🎯 Supported versions:** -| 🧪 2026.42.0 | 🧪 2026.41.0 | 🧪 2026.40.0 | 2026.24.0 | 2026.10.0 | -| :---: | :---: | :---: | :---: | :---: | +| 🧪 2026.43.0 | 🧪 2026.42.0 | 🧪 2026.41.0 | 2026.40.0 | 2026.24.0 | 2026.10.0 | +| :---: | :---: | :---: | :---: | :---: | :---: | | 💊 Patch | 📜 Description | ⚙️ Options | |----------|----------------|-----------| diff --git a/gradle.properties b/gradle.properties index 6e406b0944..262f2e3d81 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,4 +3,4 @@ org.gradle.jvmargs = -Xms512M -Xmx2048M org.gradle.parallel = true android.useAndroidX = true kotlin.code.style = official -version = 1.49.0-dev.5 +version = 1.49.0-dev.6 diff --git a/patches-bundle.json b/patches-bundle.json index 4f2745650b..2f0a9e5199 100644 --- a/patches-bundle.json +++ b/patches-bundle.json @@ -1,7 +1,7 @@ { - "created_at": "2026-10-24T17:53:42", - "description": "## [1.49.0-dev.5](https://github.com/MorpheApp/morphe-patches/compare/v1.49.0-dev.4...v1.49.0-dev.5) (2026-10-24)\n\n### 🐛 Bug Fixes\n\n* Temporarily change expired PoToken check to log only instead of a user dialog ([1669acd](https://github.com/MorpheApp/morphe-patches/commit/1669acdea4e56f6d1e812d3c0f74ee691bc2e4cd))\n* **YouTube - Miniplayer:** Minimal miniplayer is covered by the keyboard before 21.29 ([5cd2eb2](https://github.com/MorpheApp/morphe-patches/commit/5cd2eb2f8d710e3a724d309dbf98fc845e78cb94))\n* **YouTube - Theme:** Custom background colors are not applied with 21.43 ([#4019](https://github.com/MorpheApp/morphe-patches/issues/4019)) ([9cc833f](https://github.com/MorpheApp/morphe-patches/commit/9cc833f626bc456d142a6eafe2a613a3f2cc3784))", - "download_url": "https://github.com/MorpheApp/morphe-patches/releases/download/v1.49.0-dev.5/patches-1.49.0-dev.5.mpp", + "created_at": "2026-10-25T15:23:28", + "description": "## [1.49.0-dev.6](https://github.com/MorpheApp/morphe-patches/compare/v1.49.0-dev.5...v1.49.0-dev.6) (2026-10-25)\n\n### 🐛 Bug Fixes\n\n* **YouTube - Captions:** Captions not shown in picture-in-picture on 21.16 - 21.28 ([#4024](https://github.com/MorpheApp/morphe-patches/issues/4024)) ([b2688c5](https://github.com/MorpheApp/morphe-patches/commit/b2688c5eb957f3ffd142595ff4ee7b491e3b86ae))\n* **YouTube - Hide layout components:** Fix actions blocked by Hide Snackbar ([#4038](https://github.com/MorpheApp/morphe-patches/issues/4038)) ([9a0fee9](https://github.com/MorpheApp/morphe-patches/commit/9a0fee9ec2846ba2db31cbfc2e4c2ac9b14895b7))\n* **YouTube - Playback speed:** Channel speed whitelist is not applied to preloaded Shorts ([4f108b5](https://github.com/MorpheApp/morphe-patches/commit/4f108b596a71adbbf223b59faaabcfbe2469d95a))\n\n### ✨ New Features\n\n* **YouTube - Add to queue:** Add \"Native queue\" setting ([#3953](https://github.com/MorpheApp/morphe-patches/issues/3953)) ([372eea2](https://github.com/MorpheApp/morphe-patches/commit/372eea295ec4521b9ccdb93a3f188f81a6ad1a6c))\n* **YouTube - Downloads:** Add \"Quick download on tap\" setting ([#3883](https://github.com/MorpheApp/morphe-patches/issues/3883)) ([dd97d93](https://github.com/MorpheApp/morphe-patches/commit/dd97d93628a3feb7bc8758139b01fbe42a532778))\n* **YouTube - Media notification controls:** Add options to seek with Previous & Next buttons ([#3955](https://github.com/MorpheApp/morphe-patches/issues/3955)) ([2b07abb](https://github.com/MorpheApp/morphe-patches/commit/2b07abb628fec3bf625a09ab7d019f915c4dfa23))\n* **YouTube - Navigation bar:** Add \"Navigation bar style\" setting ([db9af37](https://github.com/MorpheApp/morphe-patches/commit/db9af377093121e575fec80ba40e0025dfd66f81))\n\n### 🚀 Updated App Support\n\n* **Reddit:** Add experimental support for `2026.43.0` ([6407d5e](https://github.com/MorpheApp/morphe-patches/commit/6407d5efa0f7d99fb54909bc39e2074e8f60ac42))\n* **Reddit:** Add support for `2026.40.0` ([fcc02f7](https://github.com/MorpheApp/morphe-patches/commit/fcc02f77d5a9d7a13b74d14c12b302f9d9902c61))", + "download_url": "https://github.com/MorpheApp/morphe-patches/releases/download/v1.49.0-dev.6/patches-1.49.0-dev.6.mpp", "signature_download_url": "", - "version": "1.49.0-dev.5" + "version": "1.49.0-dev.6" } diff --git a/patches-list.json b/patches-list.json index e4be259abc..3fa4de925a 100644 --- a/patches-list.json +++ b/patches-list.json @@ -1,6 +1,6 @@ { "NOTE": "Do NOT manually edit this file. This file is automatically updated when semantic release (release.yml) runs. Manually editing this file can break your releases and break third party tools that use this file.", - "version": "1.49.0-dev.5", + "version": "1.49.0-dev.6", "patches": [ { "name": "AI channel filter", @@ -83,7 +83,7 @@ }, { "name": "Add to queue", - "description": "Overrides the feed flyout 'Play next in queue' with the Morphe video queue.", + "description": "Adds a video queue to the feed flyout menu, using either the Morphe queue or a native queue that uses YouTube's playlist interface.", "default": true, "category": null, "compatiblePackages": [ @@ -314,6 +314,13 @@ ], "formFactor": "MOBILE", "targets": [ + { + "version": "2026.43.0", + "versionCodes": null, + "isExperimental": true, + "minSdk": 29, + "description": null + }, { "version": "2026.42.0", "versionCodes": null, @@ -331,7 +338,7 @@ { "version": "2026.40.0", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 29, "description": null }, @@ -1612,6 +1619,13 @@ ], "formFactor": "MOBILE", "targets": [ + { + "version": "2026.43.0", + "versionCodes": null, + "isExperimental": true, + "minSdk": 29, + "description": null + }, { "version": "2026.42.0", "versionCodes": null, @@ -1629,7 +1643,7 @@ { "version": "2026.40.0", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 29, "description": null }, @@ -2096,6 +2110,13 @@ ], "formFactor": "MOBILE", "targets": [ + { + "version": "2026.43.0", + "versionCodes": null, + "isExperimental": true, + "minSdk": 29, + "description": null + }, { "version": "2026.42.0", "versionCodes": null, @@ -2113,7 +2134,7 @@ { "version": "2026.40.0", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 29, "description": null }, @@ -2223,6 +2244,13 @@ ], "formFactor": "MOBILE", "targets": [ + { + "version": "2026.43.0", + "versionCodes": null, + "isExperimental": true, + "minSdk": 29, + "description": null + }, { "version": "2026.42.0", "versionCodes": null, @@ -2240,7 +2268,7 @@ { "version": "2026.40.0", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 29, "description": null }, @@ -2438,6 +2466,13 @@ ], "formFactor": "MOBILE", "targets": [ + { + "version": "2026.43.0", + "versionCodes": null, + "isExperimental": true, + "minSdk": 29, + "description": null + }, { "version": "2026.42.0", "versionCodes": null, @@ -2455,7 +2490,7 @@ { "version": "2026.40.0", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 29, "description": null }, @@ -3467,6 +3502,13 @@ ], "formFactor": "MOBILE", "targets": [ + { + "version": "2026.43.0", + "versionCodes": null, + "isExperimental": true, + "minSdk": 29, + "description": null + }, { "version": "2026.42.0", "versionCodes": null, @@ -3484,7 +3526,7 @@ { "version": "2026.40.0", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 29, "description": null }, @@ -3761,6 +3803,13 @@ ], "formFactor": "MOBILE", "targets": [ + { + "version": "2026.43.0", + "versionCodes": null, + "isExperimental": true, + "minSdk": 29, + "description": null + }, { "version": "2026.42.0", "versionCodes": null, @@ -3778,7 +3827,7 @@ { "version": "2026.40.0", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 29, "description": null }, @@ -4134,6 +4183,13 @@ ], "formFactor": "MOBILE", "targets": [ + { + "version": "2026.43.0", + "versionCodes": null, + "isExperimental": true, + "minSdk": 29, + "description": null + }, { "version": "2026.42.0", "versionCodes": null, @@ -4151,7 +4207,7 @@ { "version": "2026.40.0", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 29, "description": null }, @@ -4855,6 +4911,13 @@ ], "formFactor": "MOBILE", "targets": [ + { + "version": "2026.43.0", + "versionCodes": null, + "isExperimental": true, + "minSdk": 29, + "description": null + }, { "version": "2026.42.0", "versionCodes": null, @@ -4872,7 +4935,7 @@ { "version": "2026.40.0", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 29, "description": null }, @@ -5423,6 +5486,13 @@ ], "formFactor": "MOBILE", "targets": [ + { + "version": "2026.43.0", + "versionCodes": null, + "isExperimental": true, + "minSdk": 29, + "description": null + }, { "version": "2026.42.0", "versionCodes": null, @@ -5440,7 +5510,7 @@ { "version": "2026.40.0", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 29, "description": null }, @@ -5480,6 +5550,13 @@ ], "formFactor": "MOBILE", "targets": [ + { + "version": "2026.43.0", + "versionCodes": null, + "isExperimental": true, + "minSdk": 29, + "description": null + }, { "version": "2026.42.0", "versionCodes": null, @@ -5497,7 +5574,7 @@ { "version": "2026.40.0", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 29, "description": null }, @@ -5753,6 +5830,13 @@ ], "formFactor": "MOBILE", "targets": [ + { + "version": "2026.43.0", + "versionCodes": null, + "isExperimental": true, + "minSdk": 29, + "description": null + }, { "version": "2026.42.0", "versionCodes": null, @@ -5770,7 +5854,7 @@ { "version": "2026.40.0", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 29, "description": null }, @@ -5810,6 +5894,13 @@ ], "formFactor": "MOBILE", "targets": [ + { + "version": "2026.43.0", + "versionCodes": null, + "isExperimental": true, + "minSdk": 29, + "description": null + }, { "version": "2026.42.0", "versionCodes": null, @@ -5827,7 +5918,7 @@ { "version": "2026.40.0", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 29, "description": null }, @@ -5965,6 +6056,13 @@ ], "formFactor": "MOBILE", "targets": [ + { + "version": "2026.43.0", + "versionCodes": null, + "isExperimental": true, + "minSdk": 29, + "description": null + }, { "version": "2026.42.0", "versionCodes": null, @@ -5982,7 +6080,7 @@ { "version": "2026.40.0", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 29, "description": null }, @@ -6080,6 +6178,13 @@ ], "formFactor": "MOBILE", "targets": [ + { + "version": "2026.43.0", + "versionCodes": null, + "isExperimental": true, + "minSdk": 29, + "description": null + }, { "version": "2026.42.0", "versionCodes": null, @@ -6097,7 +6202,7 @@ { "version": "2026.40.0", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 29, "description": null }, @@ -6353,6 +6458,13 @@ ], "formFactor": "MOBILE", "targets": [ + { + "version": "2026.43.0", + "versionCodes": null, + "isExperimental": true, + "minSdk": 29, + "description": null + }, { "version": "2026.42.0", "versionCodes": null, @@ -6370,7 +6482,7 @@ { "version": "2026.40.0", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 29, "description": null }, @@ -6842,6 +6954,13 @@ ], "formFactor": "MOBILE", "targets": [ + { + "version": "2026.43.0", + "versionCodes": null, + "isExperimental": true, + "minSdk": 29, + "description": null + }, { "version": "2026.42.0", "versionCodes": null, @@ -6859,7 +6978,7 @@ { "version": "2026.40.0", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 29, "description": null }, @@ -6957,6 +7076,13 @@ ], "formFactor": "MOBILE", "targets": [ + { + "version": "2026.43.0", + "versionCodes": null, + "isExperimental": true, + "minSdk": 29, + "description": null + }, { "version": "2026.42.0", "versionCodes": null, @@ -6974,7 +7100,7 @@ { "version": "2026.40.0", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 29, "description": null }, @@ -7330,6 +7456,13 @@ ], "formFactor": "MOBILE", "targets": [ + { + "version": "2026.43.0", + "versionCodes": null, + "isExperimental": true, + "minSdk": 29, + "description": null + }, { "version": "2026.42.0", "versionCodes": null, @@ -7347,7 +7480,7 @@ { "version": "2026.40.0", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 29, "description": null }, @@ -7682,6 +7815,13 @@ ], "formFactor": "MOBILE", "targets": [ + { + "version": "2026.43.0", + "versionCodes": null, + "isExperimental": true, + "minSdk": 29, + "description": null + }, { "version": "2026.42.0", "versionCodes": null, @@ -7699,7 +7839,7 @@ { "version": "2026.40.0", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 29, "description": null }, @@ -7955,6 +8095,13 @@ ], "formFactor": "MOBILE", "targets": [ + { + "version": "2026.43.0", + "versionCodes": null, + "isExperimental": true, + "minSdk": 29, + "description": null + }, { "version": "2026.42.0", "versionCodes": null, @@ -7972,7 +8119,7 @@ { "version": "2026.40.0", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 29, "description": null }, @@ -8012,6 +8159,13 @@ ], "formFactor": "MOBILE", "targets": [ + { + "version": "2026.43.0", + "versionCodes": null, + "isExperimental": true, + "minSdk": 29, + "description": null + }, { "version": "2026.42.0", "versionCodes": null, @@ -8029,7 +8183,7 @@ { "version": "2026.40.0", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 29, "description": null }, @@ -8736,6 +8890,13 @@ ], "formFactor": "MOBILE", "targets": [ + { + "version": "2026.43.0", + "versionCodes": null, + "isExperimental": true, + "minSdk": 29, + "description": null + }, { "version": "2026.42.0", "versionCodes": null, @@ -8753,7 +8914,7 @@ { "version": "2026.40.0", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 29, "description": null }, @@ -8793,6 +8954,13 @@ ], "formFactor": "MOBILE", "targets": [ + { + "version": "2026.43.0", + "versionCodes": null, + "isExperimental": true, + "minSdk": 29, + "description": null + }, { "version": "2026.42.0", "versionCodes": null, @@ -8810,7 +8978,7 @@ { "version": "2026.40.0", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 29, "description": null }, @@ -9969,6 +10137,13 @@ ], "formFactor": "MOBILE", "targets": [ + { + "version": "2026.43.0", + "versionCodes": null, + "isExperimental": true, + "minSdk": 29, + "description": null + }, { "version": "2026.42.0", "versionCodes": null, @@ -9986,7 +10161,7 @@ { "version": "2026.40.0", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 29, "description": null }, @@ -10105,6 +10280,13 @@ ], "formFactor": "MOBILE", "targets": [ + { + "version": "2026.43.0", + "versionCodes": null, + "isExperimental": true, + "minSdk": 29, + "description": null + }, { "version": "2026.42.0", "versionCodes": null, @@ -10122,7 +10304,7 @@ { "version": "2026.40.0", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 29, "description": null }, @@ -10494,6 +10676,13 @@ ], "formFactor": "MOBILE", "targets": [ + { + "version": "2026.43.0", + "versionCodes": null, + "isExperimental": true, + "minSdk": 29, + "description": null + }, { "version": "2026.42.0", "versionCodes": null, @@ -10511,7 +10700,7 @@ { "version": "2026.40.0", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 29, "description": null }, @@ -10962,6 +11151,13 @@ ], "formFactor": "MOBILE", "targets": [ + { + "version": "2026.43.0", + "versionCodes": null, + "isExperimental": true, + "minSdk": 29, + "description": null + }, { "version": "2026.42.0", "versionCodes": null, @@ -10979,7 +11175,7 @@ { "version": "2026.40.0", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 29, "description": null }, @@ -11777,6 +11973,13 @@ ], "formFactor": "MOBILE", "targets": [ + { + "version": "2026.43.0", + "versionCodes": null, + "isExperimental": true, + "minSdk": 29, + "description": null + }, { "version": "2026.42.0", "versionCodes": null, @@ -11794,7 +11997,7 @@ { "version": "2026.40.0", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 29, "description": null }, @@ -12411,6 +12614,13 @@ ], "formFactor": "MOBILE", "targets": [ + { + "version": "2026.43.0", + "versionCodes": null, + "isExperimental": true, + "minSdk": 29, + "description": null + }, { "version": "2026.42.0", "versionCodes": null, @@ -12428,7 +12638,7 @@ { "version": "2026.40.0", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 29, "description": null }, @@ -12612,6 +12822,13 @@ ], "formFactor": "MOBILE", "targets": [ + { + "version": "2026.43.0", + "versionCodes": null, + "isExperimental": true, + "minSdk": 29, + "description": null + }, { "version": "2026.42.0", "versionCodes": null, @@ -12629,7 +12846,7 @@ { "version": "2026.40.0", "versionCodes": null, - "isExperimental": true, + "isExperimental": false, "minSdk": 29, "description": null }, From 445194d74ab5373334e14d31e1ceb29926ac267b Mon Sep 17 00:00:00 2001 From: 0xrxL <127248639+0xrxL@users.noreply.github.com> Date: Sun, 25 Oct 2026 16:31:08 +0100 Subject: [PATCH 057/189] feat(YouTube): Show a warning when changing 'Navigation bar style' or turning on 'Enable tablet layout in player' --- .../java/app/morphe/extension/youtube/settings/Settings.java | 2 +- .../main/resources/addresources/values/youtube/strings.xml | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/Settings.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/Settings.java index d6e5de4abc..bd414a69ad 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/Settings.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/Settings.java @@ -578,7 +578,7 @@ public class Settings extends SharedYouTubeSettings { public static final BooleanSetting NARROW_NAVIGATION_BUTTONS = new BooleanSetting("morphe_narrow_navigation_buttons", FALSE, true, parentNot(HIDE_NAVIGATION_BAR)); public static final BooleanSetting DISABLE_AUTO_HIDE_NAVIGATION_BAR = new BooleanSetting("morphe_disable_auto_hide_navigation_bar", FALSE, true, parentNot(HIDE_NAVIGATION_BAR)); public static final BooleanSetting NAVIGATION_BAR_ANIMATIONS = new BooleanSetting("morphe_navigation_bar_animations", FALSE, parentNot(HIDE_NAVIGATION_BAR)); - public static final EnumSetting NAVIGATION_BAR_STYLE = new EnumSetting<>("morphe_navigation_bar_style", NavigationBarStyle.DEFAULT, true, parentNot(HIDE_NAVIGATION_BAR)); + public static final EnumSetting NAVIGATION_BAR_STYLE = new EnumSetting<>("morphe_navigation_bar_style", NavigationBarStyle.DEFAULT, true, "morphe_navigation_bar_style_user_dialog_message", parentNot(HIDE_NAVIGATION_BAR)); // Toolbar public static final BooleanSetting HIDE_TOOLBAR_CAST_BUTTON = new BooleanSetting("morphe_hide_toolbar_cast_button", TRUE, true); diff --git a/patches/src/main/resources/addresources/values/youtube/strings.xml b/patches/src/main/resources/addresources/values/youtube/strings.xml index f678fc5467..f645470aeb 100644 --- a/patches/src/main/resources/addresources/values/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values/youtube/strings.xml @@ -953,6 +953,7 @@ To access Settings, enable the Settings button via General → Toolbar → Show Translucent Transparent Opaque + This style may not be supported by your device or YouTube version, and can cause layout issues or crashes Toolbar Choose which components appear in the toolbar @@ -1207,7 +1208,9 @@ Automotive layout "Restores the old player layout Limitation: If \'Restore old video action bar\' is turned off, \'More videos\' overlay may not be available" - Enabling this may break some player layout features, such as the comment section or \'More videos\' shown below the player + "Enabling this may break some player layout features, such as the comment section or \'More videos\' shown below the player. + +The tablet layout is no longer supported by YouTube, and is partially or fully broken on recent YouTube versions. For a working tablet layout, patch YouTube 20.21.37" 21.11.48 - Restore old captions behavior From bb6e0e38bf4216395abacf8f4afbd45418b25ed7 Mon Sep 17 00:00:00 2001 From: 0xrxL <127248639+0xrxL@users.noreply.github.com> Date: Sun, 25 Oct 2026 16:31:08 +0100 Subject: [PATCH 058/189] fix(YouTube - Minimal miniplayer): Bar is partly covered by the keyboard with the default navigation bar style --- .../youtube/patches/MinimalMiniplayerPatch.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/MinimalMiniplayerPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/MinimalMiniplayerPatch.java index 082c1086aa..c15aab820d 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/MinimalMiniplayerPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/MinimalMiniplayerPatch.java @@ -655,6 +655,10 @@ private static void updateHorizontalSpan() { * the system navigation buttons for the translucent navigation bar, and YouTube then rests * the player behind them. Measured against them directly, so nothing changes when the layout * already ends above them. + *

+ * The keyboard is always measured where the window insets have it. With the feature flags of + * the translucent navigation bar left to the account, YouTube shrinks that layout by the + * keyboard height less the system navigation buttons, so it still ends behind the keyboard. * * @param playerLayout The layout that holds the player, which the bounds are relative to. */ @@ -664,14 +668,17 @@ private static int windowBottomFor(@Nullable View playerLayout) { final boolean keyboardMarginRemoved = NavigationBarPatch.isKeyboardMarginRemoved(); final boolean translucentNavigationBar = NavigationBarPatch.isTranslucentNavigationBarForced(); - if (!keyboardMarginRemoved && !translucentNavigationBar) return Integer.MAX_VALUE; + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R + && !keyboardMarginRemoved && !translucentNavigationBar) { + return Integer.MAX_VALUE; + } WindowInsets insets = playerLayout.getRootWindowInsets(); if (insets == null) return Integer.MAX_VALUE; final int insetBottom; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - insetBottom = insets.getInsets((keyboardMarginRemoved ? WindowInsets.Type.ime() : 0) + insetBottom = insets.getInsets(WindowInsets.Type.ime() | (translucentNavigationBar ? WindowInsets.Type.navigationBars() : 0)).bottom; } else { // The translucent navigation bar is only used from Android 12, so this is the keyboard. From 5f96fbe0a90efd54b3e1233c9690aa18f4d38dda Mon Sep 17 00:00:00 2001 From: RAMiK <60112744+RAMiK4@users.noreply.github.com> Date: Sun, 25 Oct 2026 19:43:23 +0400 Subject: [PATCH 059/189] feat(YouTube): Add `Download post images` patch (#3966) Co-authored-by: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> --- .../patches/PostImageDownloadPatch.java | 1134 +++++++++++++++++ .../patches/utils/FeedVideoCardsPatch.java | 25 +- .../youtube/patches/utils/FlyoutUtils.java | 10 + .../extension/youtube/settings/Settings.java | 3 + .../support/v7/widget/RecyclerView.java | 5 + .../postimages/PostImageDownloadPatch.kt | 70 + .../addresources/values/youtube/strings.xml | 12 + 7 files changed, 1254 insertions(+), 5 deletions(-) create mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/PostImageDownloadPatch.java create mode 100644 patches/src/main/kotlin/app/morphe/patches/youtube/interaction/postimages/PostImageDownloadPatch.kt diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/PostImageDownloadPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/PostImageDownloadPatch.java new file mode 100644 index 0000000000..16ec22e997 --- /dev/null +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/PostImageDownloadPatch.java @@ -0,0 +1,1134 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/3966 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.extension.youtube.patches; + +import static app.morphe.extension.shared.StringRef.str; + +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffColorFilter; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.os.Build; +import android.os.Environment; +import android.provider.MediaStore; +import android.support.v7.widget.RecyclerView; +import android.view.HapticFeedbackConstants; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.GuardedBy; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.facebook.litho.ComponentHost; +import com.facebook.litho.TextContent; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.WeakHashMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import app.morphe.extension.shared.Logger; +import app.morphe.extension.shared.ResourceUtils; +import app.morphe.extension.shared.Utils; +import app.morphe.extension.shared.spoof.SpoofAppVersionPatch; +import app.morphe.extension.shared.theme.ThemeUtils; +import app.morphe.extension.shared.ui.Dim; +import app.morphe.extension.youtube.patches.utils.FeedVideoCardsPatch; +import app.morphe.extension.youtube.patches.utils.FlyoutUtils; +import app.morphe.extension.youtube.patches.utils.ProtoNode; +import app.morphe.extension.youtube.settings.Settings; + +/** + * Downloads the images and GIFs of posts. + *

+ * Posts are drawn by Litho, so a post has no view of its own and does not know its images. + * The element of a post includes the urls of its images and the texts it shows. When the element + * is parsed, its texts are mapped to its images. When a post is laid out, the texts it shows are + * looked up, and the images most of the texts belong to are the images of the post. + *

+ * A download button is drawn left of the Share button of the post, which downloads the image + * the post shows, and a Download images item is added to the menu of the post, which downloads + * all images of the post. Polls and posts with an embedded video are not used. + */ +@SuppressWarnings("unused") +public final class PostImageDownloadPatch { + + public static final boolean ENABLED = isPatchIncluded() && Settings.POST_IMAGE_DOWNLOAD.get(); + + private static final boolean USE_EXPERIMENTAL_ICONS = VersionCheckPatch.IS_20_31_OR_GREATER + && !SpoofAppVersionPatch.isSpoofingToLessThan("20.31.00"); + + private static final String DOWNLOAD_ICON = USE_EXPERIMENTAL_ICONS + ? "yt_outline_experimental_download_vd_theme_24" + : "yt_outline_download_black_24"; + + /** + * App texts of the Share button. The Share button of a post is described by a text + * of the server, which is the same in the language of the app. + */ + private static final String[] SHARE_LABEL_NAMES = { + "reel_footer_share", + "accessibility_send_share", + "offline_share_button_text" + }; + + /** Folder of the saved images, inside the Pictures folder. */ + private static final String FOLDER = "YouTube"; + + /** Size of the drawn icon, the same as the icons of the buttons of the post. */ + private static final int ICON_SIZE_DP = 24; + private static final int MIN_BUTTON_DP = 36; + private static final int MAX_BUTTON_DP = 48; + /** Share buttons are small, which keeps other described views of the post apart. */ + private static final int MAX_SHARE_BUTTON_DP = 160; + /** A button that ends this close to the Share button is left of it. */ + private static final int LEFT_NEIGHBOR_TOLERANCE_DP = 2; + /** Space kept after the button, when it is placed after the last button of the row. */ + private static final int ROW_END_MARGIN_DP = 4; + private static final int PRESSED_ALPHA = 0x1F; + + /** Smaller images of a post element are avatars, emojis and badges. */ + private static final int MIN_IMAGE_SIZE = 200; + /** Options of an image url that ask for the image in its original size. */ + private static final String ORIGINAL_SIZE_OPTIONS = "=s0"; + private static final int MAX_DOWNLOAD_BYTES = 50 * 1024 * 1024; + private static final int DOWNLOAD_BUFFER_SIZE = 16 * 1024; + private static final int TIMEOUT_MS = 15_000; + + private static final int MIN_TEXT_LENGTH = 4; + private static final int LONG_TEXT_LENGTH = 20; + private static final int MAX_TEXTS = 2000; + private static final int MAX_POSTS = 300; + + /** Value of a text shown by more than one post. */ + private static final String AMBIGUOUS = ""; + + /** + * Value of a text of a post that is not used, such as a poll. + * Never a key of post images, which are urls. + */ + private static final String NOT_IMAGE_POST = "not_image_post"; + + private static final byte[] POST_BYTES = "post".getBytes(StandardCharsets.US_ASCII); + private static final byte[] GGPHT_BYTES = "ggpht.com/".getBytes(StandardCharsets.US_ASCII); + private static final byte[] GOOGLE_USER_CONTENT_BYTES = + "googleusercontent.com/".getBytes(StandardCharsets.US_ASCII); + + private static final Pattern IMAGE_URL_PATTERN = Pattern.compile( + "(?:https?:)?//(?:yt\\d\\.ggpht\\.com|[a-z0-9]+\\.googleusercontent\\.com)/[^\\s\"'<>]+"); + /** Size of an image url, such as =s1080 or =w640-h480. */ + private static final Pattern IMAGE_SIZE_PATTERN = Pattern.compile("(?:^|-)[swh](\\d{1,5})(?=-|$)"); + /** Thumbnail of a video, which an embedded video shows. */ + private static final Pattern VIDEO_THUMBNAIL_PATTERN = Pattern.compile("/vi(?:_webp)?/[A-Za-z0-9_-]{11}/"); + /** The shown text can join the lines of the post text, or show invisible format characters. */ + private static final Pattern WHITESPACE_PATTERN = Pattern.compile("[\\s\\p{Cf}]+"); + + /** Text -> key of the post images, or {@link #AMBIGUOUS}. Oldest texts are removed first. */ + @GuardedBy("itself") + private static final Map textPostKeys = Utils + .createSizeRestrictedMap(MAX_TEXTS, true); + + /** Key of the post images -> images of the post. */ + @GuardedBy("itself") + private static final Map> postImages = Utils + .createSizeRestrictedMap(MAX_POSTS, true); + + /** Root host -> the post it shows. Used only on the main thread. */ + private static final Map cards = new WeakHashMap<>(); + + @Nullable + private static Drawable downloadIcon; + @Nullable + private static List shareLabels; + @Nullable + private static PostCard pressedCard; + + /** + * @param shownSize Size of the shown image, from the options of its url. + */ + private record PostImage(String originalUrl, String shownUrl, int shownSize) { + } + + private enum UrlKind { + NONE, + /** An avatar or an emoji. */ + AVATAR, + IMAGE + } + + private enum ImageType { + GIF("gif", "image/gif"), + WEBP("webp", "image/webp"), + PNG("png", "image/png"), + JPEG("jpg", "image/jpeg"); + + final String extension; + final String mimeType; + + ImageType(String extension, String mimeType) { + this.extension = extension; + this.mimeType = mimeType; + } + } + + public static boolean isPatchIncluded() { + return false; // Modified during patching. + } + + private PostImageDownloadPatch() { + } + + // region Elements + + private static boolean isPostText(String text) { + return text.length() >= MIN_TEXT_LENGTH + && !text.contains(".eml") + && !text.startsWith("http"); + } + + private static String normalize(String text) { + return WHITESPACE_PATTERN.matcher(text).replaceAll(" ").trim(); + } + + /** + * @return The start of a text cut off with an ellipsis, such as a long post text + * followed by Read more, or null if the text is not cut off. + */ + @Nullable + private static String getCutOffStart(String text) { + int index = text.indexOf("..."); + if (index < 0) { + index = text.indexOf('…'); + } + return index >= LONG_TEXT_LENGTH ? normalize(text.substring(0, index)) : null; + } + + private static String getOptions(String url) { + final int optionsIndex = url.indexOf('='); + return optionsIndex < 0 ? "" : url.substring(optionsIndex + 1); + } + + /** + * @return The size the url asks for, or {@link Integer#MAX_VALUE} if it does not ask for one. + */ + private static int getImageSize(String options) { + int size = Integer.MAX_VALUE; + Matcher matcher = IMAGE_SIZE_PATTERN.matcher(options); + while (matcher.find()) { + //noinspection ConstantConditions + final int value = Integer.parseInt(matcher.group(1)); + if (value > 0) { + size = Math.min(size, value); + } + } + return size; + } + + /** Avatars and emojis are cropped to a circle or to a square without a border. */ + private static boolean isAvatarOptions(String options) { + return options.contains("-k-") || options.contains("no-rj"); + } + + private static UrlKind getUrlKind(String text) { + UrlKind kind = UrlKind.NONE; + Matcher matcher = IMAGE_URL_PATTERN.matcher(text); + while (matcher.find()) { + String options = getOptions(matcher.group()); + if (isAvatarOptions(options)) { + kind = UrlKind.AVATAR; + } else if (getImageSize(options) >= MIN_IMAGE_SIZE) { + return UrlKind.IMAGE; + } + } + return kind; + } + + /** + * Injection point. + *

+ * Called when an element is parsed. The element is not changed. + */ + public static byte[] onElementLoaded(byte[] bytes) { + if (!ENABLED || bytes == null || !FeedVideoCardsPatch.contains(bytes, POST_BYTES) + || !(FeedVideoCardsPatch.contains(bytes, GGPHT_BYTES) + || FeedVideoCardsPatch.contains(bytes, GOOGLE_USER_CONTENT_BYTES))) { + return bytes; + } + try { + List root = ProtoNode.parse(bytes); + if (root == null) { + return bytes; + } + List textNodes = ProtoNode.textNodes(root); + + String identifier = null; + for (ProtoNode node : textNodes) { + if (node.getText().contains(".eml")) { + identifier = node.getText(); + break; + } + } + final String elementIdentifier = identifier; + if (elementIdentifier == null || !elementIdentifier.contains("post")) { + return bytes; + } + + if (!elementIdentifier.contains("shelf")) { + registerPost(textNodes, elementIdentifier, isImagePost(textNodes, elementIdentifier)); + return bytes; + } + + // A shelf of many posts. Each post starts with the avatar of its channel. + List> posts = new ArrayList<>(); + List current = new ArrayList<>(); + boolean currentHasImage = false; + for (ProtoNode node : textNodes) { + final UrlKind kind = getUrlKind(node.getText()); + if (kind == UrlKind.AVATAR && currentHasImage) { + posts.add(current); + current = new ArrayList<>(); + currentHasImage = false; + } else if (kind == UrlKind.IMAGE) { + currentHasImage = true; + } + current.add(node); + } + if (currentHasImage) { + posts.add(current); + } + Logger.printDebug(() -> "Post shelf " + elementIdentifier + " with " + posts.size() + " posts"); + for (List post : posts) { + registerPost(post, elementIdentifier, isImagePost(post, null)); + } + } catch (Exception ex) { + Logger.printException(() -> "onElementLoaded failure", ex); + } + return bytes; + } + + /** + * Only posts of images. Polls with images and posts with an embedded video are not used, + * as their images are not the images of the post. + * + * @param identifier The identifier of the element of the post, or null for a post of a shelf. + */ + private static boolean isImagePost(List textNodes, @Nullable String identifier) { + if (identifier != null && !identifier.startsWith("images_post")) { + Logger.printDebug(() -> "Not an image post: " + identifier); + return false; + } + for (ProtoNode node : textNodes) { + String text = node.getText(); + if (text.contains(".eml") && (text.contains("options_post") || text.contains("poll_post") + || text.contains("videos_post") || text.contains("shared_post"))) { + Logger.printDebug(() -> "Not an image post: " + text); + return false; + } + if (text.contains("/vi") && VIDEO_THUMBNAIL_PATTERN.matcher(text).find()) { + Logger.printDebug(() -> "Post with an embedded video: " + identifier); + return false; + } + } + return true; + } + + /** + * Maps the texts of a post to its images. The texts of a post that is not used, such as + * a poll, are mapped too, so its card is not matched to another post that shares some + * of its texts. + */ + private static void registerPost(List textNodes, String elementIdentifier, + boolean isImagePost) { + String key = NOT_IMAGE_POST; + if (isImagePost) { + Map images = findImages(textNodes); + if (images.isEmpty()) { + return; + } + key = String.join("\n", images.keySet()); + synchronized (postImages) { + postImages.put(key, new ArrayList<>(images.values())); + } + final int imageCount = images.size(); + Logger.printDebug(() -> "Post element " + elementIdentifier + " with " + imageCount + " images"); + } + + synchronized (textPostKeys) { + for (ProtoNode node : textNodes) { + String text = normalize(node.getText()); + if (!isPostText(text)) { + continue; + } + String previous = textPostKeys.get(text); + if (previous == null) { + textPostKeys.put(text, key); + } else if (!previous.equals(key)) { + textPostKeys.put(text, AMBIGUOUS); + } + } + } + } + + /** + * @return Base url -> image, in the order of the post. + */ + private static Map findImages(List textNodes) { + Map images = new LinkedHashMap<>(); + for (ProtoNode node : textNodes) { + Matcher matcher = IMAGE_URL_PATTERN.matcher(node.getText()); + while (matcher.find()) { + String url = matcher.group(); + if (url.startsWith("//")) { + url = "https:" + url; + } + final int optionsIndex = url.indexOf('='); + String base = optionsIndex < 0 ? url : url.substring(0, optionsIndex); + String options = optionsIndex < 0 ? "" : url.substring(optionsIndex + 1); + if (isAvatarOptions(options)) { + continue; + } + final int size = getImageSize(options); + if (size < MIN_IMAGE_SIZE) { + continue; + } + // The largest shown size of each image. + PostImage previous = images.get(base); + if (previous == null || size > previous.shownSize()) { + images.put(base, new PostImage(base + ORIGINAL_SIZE_OPTIONS, url, size)); + } + } + } + return images; + } + + // endregion + + // region Cards + + private static void collectTexts(ComponentHost host, List texts) { + TextContent textContent = host.getTextContent(); + if (textContent != null) { + for (CharSequence text : textContent.getTextItems()) { + if (text != null) { + addPostText(texts, text); + } + } + } + for (int i = 0, count = host.getChildCount(); i < count; i++) { + View view = host.getChildAt(i); + CharSequence description = view.getContentDescription(); + if (description != null) { + addPostText(texts, description); + } + if (view instanceof ComponentHost child) { + collectTexts(child, texts); + } + } + } + + private static void addPostText(List texts, CharSequence text) { + String item = text.toString().trim(); + if (isPostText(item) && !texts.contains(item)) { + texts.add(item); + } + } + + /** + * @return The key of the images of the post the root host shows, or null if it is not a post. + */ + @Nullable + private static String findPostKey(ComponentHost root) { + List texts = new ArrayList<>(); + collectTexts(root, texts); + if (texts.isEmpty()) { + return null; + } + + Map scores = new HashMap<>(); + synchronized (textPostKeys) { + for (String shown : texts) { + String text = normalize(shown); + String key = textPostKeys.get(text); + if (key == null) { + // A long post text is cut off and shows only its start, + // or is shown with other line breaks or links. + String start = getCutOffStart(shown); + if (start != null) { + key = findKeyByStart(start); + } else if (text.length() >= LONG_TEXT_LENGTH) { + key = findKeyByStart(text); + } + } + if (NOT_IMAGE_POST.equals(key)) { + if (text.length() >= LONG_TEXT_LENGTH) { + // The card shows a post that is not used, such as a poll. + return null; + } + continue; + } + if (key == null || key.equals(AMBIGUOUS)) { + continue; + } + final int score = text.length() >= LONG_TEXT_LENGTH ? 2 : 1; + scores.merge(key, score, Integer::sum); + } + } + return FeedVideoCardsPatch.findBestMatch(scores); + } + + /** + * @return The key of the post with a text that starts with the text, + * {@link #AMBIGUOUS} if many posts do, or null if none. + */ + @GuardedBy("textPostKeys") + @Nullable + private static String findKeyByStart(String start) { + String found = null; + for (Map.Entry entry : textPostKeys.entrySet()) { + String key = entry.getValue(); + String text = entry.getKey(); + if (key.equals(AMBIGUOUS) || !(text.startsWith(start) + || (text.length() >= LONG_TEXT_LENGTH && start.startsWith(text)))) { + continue; + } + if (found != null && !found.equals(key)) { + return AMBIGUOUS; + } + found = key; + } + return found; + } + + @Nullable + private static List getImages(@Nullable String key) { + if (key == null) { + return null; + } + synchronized (postImages) { + List images = postImages.get(key); + return images == null || images.isEmpty() ? null : new ArrayList<>(images); + } + } + + private static List getShareLabels() { + if (shareLabels == null) { + List labels = new ArrayList<>(); + for (String name : SHARE_LABEL_NAMES) { + addShareLabel(labels, ResourceUtils.getString(name)); + } + // Framework texts are given in the language of the app. + final int systemShareId = ResourceUtils.getSystemStringIdentifier("share"); + if (systemShareId != 0) { + addShareLabel(labels, Utils.getContext().getString(systemShareId)); + } + Logger.printDebug(() -> "Share button labels: " + labels); + shareLabels = labels; + } + return shareLabels; + } + + private static void addShareLabel(List labels, @Nullable String label) { + if (label == null) { + return; + } + label = label.trim(); + if (!label.isEmpty() && !labels.contains(label)) { + labels.add(label); + } + } + + private static boolean isShareDescription(CharSequence description) { + String text = description.toString().trim(); + for (String label : getShareLabels()) { + if (text.equalsIgnoreCase(label)) { + return true; + } + } + return false; + } + + /** + * @return The Share button of the post, or null if it is not shown. + */ + @Nullable + private static View findShareButton(ComponentHost root, ViewGroup group, Rect rect) { + for (int i = 0, count = group.getChildCount(); i < count; i++) { + View child = group.getChildAt(i); + if (child.getVisibility() != View.VISIBLE) { + continue; + } + CharSequence description = child.getContentDescription(); + if (description != null && isShareDescription(description)) { + rect.set(0, 0, child.getWidth(), child.getHeight()); + root.offsetDescendantRectToMyCoords(child, rect); + if (rect.width() > 0 && rect.width() <= Dim.dp(MAX_SHARE_BUTTON_DP)) { + return child; + } + } + if (child instanceof ComponentHost host) { + View found = findShareButton(root, host, rect); + if (found != null) { + return found; + } + } + } + return null; + } + + /** + * Collects the bounds of the other buttons in the row of the Share button, + * such as the like and comment buttons. + */ + private static void collectRowButtons(ComponentHost root, ViewGroup group, View share, + Rect shareRect, List row) { + for (int i = 0, count = group.getChildCount(); i < count; i++) { + View child = group.getChildAt(i); + if (child.getVisibility() != View.VISIBLE || child == share) { + continue; + } + if (child.getContentDescription() != null) { + Rect rect = new Rect(0, 0, child.getWidth(), child.getHeight()); + root.offsetDescendantRectToMyCoords(child, rect); + if (rect.width() < root.getWidth() / 2 + && rect.centerY() >= shareRect.top && rect.centerY() <= shareRect.bottom) { + row.add(rect); + } + } + if (child instanceof ComponentHost host) { + collectRowButtons(root, host, share, shareRect, row); + } + } + } + + /** + * Called by {@link FeedVideoCardsPatch} once a root host was laid out. + * Must be called on the main thread. + */ + public static void onRootLaidOut(ComponentHost root) { + if (!ENABLED) { + return; + } + try { + PostCard card = cards.get(root); + // Most root hosts are not posts and have no Share button, + // so the texts are only looked up for the ones that do. + Rect shareRect = new Rect(); + View share = FeedVideoCardsPatch.isInDialog(root) ? null : findShareButton(root, root, shareRect); + String key = share == null ? null : findPostKey(root); + if (key == null) { + if (card != null) { + card.removeButton(root); + cards.remove(root); + } + if (share != null) { + // The element of the post can be parsed after the post was laid out. + FeedVideoCardsPatch.scheduleRetry(root); + } + return; + } + if (card == null) { + card = new PostCard(); + cards.put(root, card); + } + card.key = key; + if (!card.placeButton(root, share, shareRect)) { + card.removeButton(root); + } + } catch (Exception ex) { + Logger.printException(() -> "onRootLaidOut failure", ex); + } + } + + private static final class PostCard { + String key; + final DownloadButton button = new DownloadButton(); + boolean buttonAdded; + + /** + * Places the button left of the Share button if there is room for it, + * or else after the last button of the row. + * + * @return If the button was placed. + */ + boolean placeButton(ComponentHost root, View shareView, Rect share) { + final int size = Math.max(Dim.dp(MIN_BUTTON_DP), Math.min(Dim.dp(MAX_BUTTON_DP), share.height())); + + List row = new ArrayList<>(); + collectRowButtons(root, root, shareView, share, row); + int leftNeighbor = 0; + int rowRight = share.right; + for (Rect rect : row) { + if (rect.right <= share.left + Dim.dp(LEFT_NEIGHBOR_TOLERANCE_DP)) { + leftNeighbor = Math.max(leftNeighbor, rect.right); + } + rowRight = Math.max(rowRight, rect.right); + } + + final int left; + if (share.left - leftNeighbor >= size) { + left = share.left - size; + } else if (root.getWidth() - rowRight >= size + Dim.dp(ROW_END_MARGIN_DP)) { + left = rowRight; + } else { + Logger.printDebug(() -> "No room for the download button"); + return false; + } + final int top = share.centerY() - size / 2; + button.setBounds(left, top, left + size, top + size); + if (!buttonAdded) { + root.getOverlay().add(button); + buttonAdded = true; + } + button.invalidateSelf(); + return true; + } + + void removeButton(ComponentHost root) { + if (buttonAdded) { + root.getOverlay().remove(button); + buttonAdded = false; + } + } + + boolean isHit(float x, float y) { + return buttonAdded && button.getBounds().contains(Math.round(x), Math.round(y)); + } + } + + /** + * Download icon of the app, drawn like the other buttons of the post. + */ + private static final class DownloadButton extends Drawable { + private final Paint pressedPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + private boolean pressed; + + void setPressed(boolean pressed) { + if (this.pressed != pressed) { + this.pressed = pressed; + invalidateSelf(); + } + } + + @Override + public void draw(@NonNull Canvas canvas) { + Rect bounds = getBounds(); + final int color = ThemeUtils.getAppForegroundColor(); + if (pressed) { + pressedPaint.setColor(color); + pressedPaint.setAlpha(PRESSED_ALPHA); + canvas.drawCircle(bounds.exactCenterX(), bounds.exactCenterY(), + bounds.width() / 2f, pressedPaint); + } + Drawable icon = getDownloadIcon(color); + if (icon != null) { + final int iconSize = Dim.dp(ICON_SIZE_DP); + final int left = bounds.centerX() - iconSize / 2; + final int top = bounds.centerY() - iconSize / 2; + icon.setBounds(left, top, left + iconSize, top + iconSize); + icon.draw(canvas); + } + } + + @Override + public void setAlpha(int alpha) { + } + + @Override + public void setColorFilter(@Nullable ColorFilter colorFilter) { + } + + @Override + public int getOpacity() { + return PixelFormat.TRANSLUCENT; + } + } + + private static int downloadIconColor; + + /** + * @return The download icon of the buttons drawn on posts, in the color. + */ + @Nullable + private static Drawable getDownloadIcon(int color) { + if (downloadIcon == null) { + Drawable icon = ResourceUtils.getDrawable(DOWNLOAD_ICON); + if (icon == null) { + return null; + } + downloadIcon = icon.mutate(); + downloadIconColor = ~color; + } + if (downloadIconColor != color) { + downloadIconColor = color; + downloadIcon.setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN)); + } + return downloadIcon; + } + + /** + * Injection point. + * + * @return If the touch was handled and must not be given to the children. + */ + public static boolean onComponentHostTouch(ComponentHost host, MotionEvent event) { + if (!ENABLED || host.getParent() instanceof ComponentHost) { + return false; + } + try { + final int action = event.getActionMasked(); + if (action == MotionEvent.ACTION_DOWN) { + clearPressed(); + PostCard card = cards.get(host); + if (card == null || !card.isHit(event.getX(), event.getY())) { + return false; + } + pressedCard = card; + card.button.setPressed(true); + return true; + } + + PostCard card = pressedCard; + if (card == null || cards.get(host) != card) { + return false; + } + switch (action) { + case MotionEvent.ACTION_MOVE -> card.button.setPressed(card.isHit(event.getX(), event.getY())); + case MotionEvent.ACTION_UP -> { + final boolean hit = card.isHit(event.getX(), event.getY()); + clearPressed(); + if (hit) { + host.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY); + download(getShownImages(host, getImages(card.key))); + } + } + case MotionEvent.ACTION_CANCEL -> clearPressed(); + } + return true; + } catch (Exception ex) { + Logger.printException(() -> "onComponentHostTouch failure", ex); + clearPressed(); + } + return false; + } + + private static void clearPressed() { + if (pressedCard != null) { + pressedCard.button.setPressed(false); + pressedCard = null; + } + } + + /** + * @return The image the post shows now, or all images if it is not known. + */ + @Nullable + private static List getShownImages(ComponentHost root, @Nullable List images) { + if (images == null || images.size() < 2) { + return images; + } + View carousel = findImageCarousel(root, null); + final int index = carousel instanceof RecyclerView list ? getShownIndex(list) : -1; + Logger.printDebug(() -> "Shown image " + index + " of " + images.size()); + return index >= 0 && index < images.size() ? List.of(images.get(index)) : images; + } + + /** + * @return The tallest horizontal list of the post, which is the carousel of its images + * and not the carousel of its comments. + */ + @Nullable + private static View findImageCarousel(ViewGroup group, @Nullable View best) { + for (int i = 0, count = group.getChildCount(); i < count; i++) { + View child = group.getChildAt(i); + if (child.getVisibility() != View.VISIBLE || !(child instanceof ViewGroup childGroup)) { + continue; + } + if (FeedVideoCardsPatch.isHorizontalList(child)) { + if (childGroup.getChildCount() > 0 + && (best == null || child.getHeight() > best.getHeight())) { + best = child; + } + } else { + best = findImageCarousel(childGroup, best); + } + } + return best; + } + + /** + * @return The position of the item of the carousel that is shown the most, or -1 if none. + */ + private static int getShownIndex(RecyclerView carousel) { + View shown = null; + int shownWidth = 0; + int firstLeft = Integer.MAX_VALUE; + int secondLeft = Integer.MAX_VALUE; + for (int i = 0, count = carousel.getChildCount(); i < count; i++) { + View child = carousel.getChildAt(i); + final int left = child.getLeft(); + if (left < firstLeft) { + secondLeft = firstLeft; + firstLeft = left; + } else if (left < secondLeft) { + secondLeft = left; + } + final int visible = Math.min(child.getRight(), carousel.getWidth()) - Math.max(left, 0); + if (visible > shownWidth) { + shownWidth = visible; + shown = child; + } + } + if (shown == null || shown.getWidth() <= 0) { + return -1; + } + final int pitch = secondLeft != Integer.MAX_VALUE && secondLeft > firstLeft + ? secondLeft - firstLeft + : shown.getWidth(); + + // The scroll offset is the distance of the start of the list to its first item, + // and is the same for any adapter. + final int position = carousel.computeHorizontalScrollOffset() + shown.getLeft() + - carousel.getPaddingLeft(); + return Math.max(0, Math.round(position / (float) pitch)); + } + + // endregion + + // region Menu + + /** + * Adds Download images to the menu of a post. + * + * @param sender The view that opened the menu. + */ + public static int addFlyoutButton(Object flyoutPanel, int index, @Nullable View sender) { + if (!ENABLED || sender == null) { + return index; + } + try { + ComponentHost root = FeedVideoCardsPatch.findTappedRootHost(sender); + if (root == null) { + Logger.printDebug(() -> "Menu sender has no root host: " + sender); + return index; + } + PostCard card = cards.get(root); + List images = getImages(card != null ? card.key : findPostKey(root)); + if (images == null) { + return index; + } + + final int nextIndex = FlyoutUtils.addFlyoutButton( + flyoutPanel, + ResourceUtils.getDrawable(DOWNLOAD_ICON), + str(images.size() == 1 + ? "morphe_post_image_download_flyout_single" + : "morphe_post_image_download_flyout"), + v -> { + FlyoutUtils.dismissFlyout(); + download(images); + }, + index + ); + return nextIndex < 0 ? index : nextIndex; + } catch (Exception ex) { + Logger.printException(() -> "addFlyoutButton failure", ex); + } + return index; + } + + // endregion + + // region Download + + private static void download(@Nullable List images) { + if (images == null || images.isEmpty()) { + Utils.showToastShort(str("morphe_post_image_download_not_found")); + return; + } + Utils.showToastShort(images.size() == 1 + ? str("morphe_post_image_download_started_single") + : str("morphe_post_image_download_started", images.size())); + + Utils.runOnBackgroundThread(() -> { + final long time = System.currentTimeMillis(); + int saved = 0; + for (int i = 0; i < images.size(); i++) { + PostImage image = images.get(i); + try { + byte[] data = fetch(image.originalUrl()); + if (data == null) { + data = fetch(image.shownUrl()); + } + if (data == null) { + continue; + } + String name = images.size() == 1 + ? "YouTube_post_" + time + : String.format(Locale.US, "YouTube_post_%d_%d", time, i + 1); + if (save(data, name)) { + saved++; + } + } catch (Exception ex) { + Logger.printException(() -> "Could not download post image: " + image.originalUrl(), ex); + } + } + + final int savedCount = saved; + final String folder = getSaveFolderName(); + Utils.runOnMainThread(() -> { + String toast; + if (savedCount == 0) { + toast = str("morphe_post_image_download_failed"); + } else if (savedCount == 1) { + toast = str("morphe_post_image_download_saved_single", folder); + } else { + toast = str("morphe_post_image_download_saved", savedCount, folder); + } + Utils.showToastShort(toast); + }); + }); + } + + @Nullable + private static byte[] fetch(String url) { + HttpURLConnection connection = null; + try { + connection = (HttpURLConnection) new URL(url).openConnection(); + connection.setConnectTimeout(TIMEOUT_MS); + connection.setReadTimeout(TIMEOUT_MS); + connection.setInstanceFollowRedirects(true); + final int code = connection.getResponseCode(); + if (code != HttpURLConnection.HTTP_OK) { + Logger.printDebug(() -> "Post image response " + code + ": " + url); + return null; + } + try (InputStream input = connection.getInputStream()) { + ByteArrayOutputStream output = new ByteArrayOutputStream(); + byte[] buffer = new byte[DOWNLOAD_BUFFER_SIZE]; + int read; + while ((read = input.read(buffer)) != -1) { + output.write(buffer, 0, read); + if (output.size() > MAX_DOWNLOAD_BYTES) { + Logger.printDebug(() -> "Post image is too large: " + url); + return null; + } + } + return output.size() == 0 ? null : output.toByteArray(); + } + } catch (Exception ex) { + Logger.printInfo(() -> "Could not fetch post image: " + url, ex); + return null; + } finally { + if (connection != null) { + connection.disconnect(); + } + } + } + + /** + * @return The type of the image, found from its first bytes. + */ + private static ImageType getType(byte[] data) { + if (data.length >= 6 && data[0] == 'G' && data[1] == 'I' && data[2] == 'F') { + return ImageType.GIF; + } + if (data.length >= 12 && data[0] == 'R' && data[1] == 'I' && data[2] == 'F' && data[3] == 'F' + && data[8] == 'W' && data[9] == 'E' && data[10] == 'B' && data[11] == 'P') { + return ImageType.WEBP; + } + if (data.length >= 8 && (data[0] & 0xFF) == 0x89 && data[1] == 'P' && data[2] == 'N' && data[3] == 'G') { + return ImageType.PNG; + } + return ImageType.JPEG; + } + + /** + * Before Android 10, the shared Pictures folder needs a storage permission, + * so the images are saved to the Pictures folder of the app instead. + */ + private static File getLegacySaveFolder() { + return new File(Utils.getContext().getExternalFilesDir(Environment.DIRECTORY_PICTURES), FOLDER); + } + + /** + * @return The folder the images are saved to, as shown to the user. + */ + private static String getSaveFolderName() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q + ? Environment.DIRECTORY_PICTURES + File.separator + FOLDER + : getLegacySaveFolder().getPath(); + } + + private static boolean save(byte[] data, String name) throws Exception { + ImageType type = getType(data); + String fileName = name + '.' + type.extension; + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + File folder = getLegacySaveFolder(); + if (!folder.exists() && !folder.mkdirs()) { + Logger.printInfo(() -> "Could not create folder: " + folder); + return false; + } + try (OutputStream output = new FileOutputStream(new File(folder, fileName))) { + output.write(data); + } + return true; + } + + Context context = Utils.getContext(); + ContentResolver resolver = context.getContentResolver(); + ContentValues values = new ContentValues(); + values.put(MediaStore.Images.Media.DISPLAY_NAME, fileName); + values.put(MediaStore.Images.Media.MIME_TYPE, type.mimeType); + values.put(MediaStore.Images.Media.RELATIVE_PATH, getSaveFolderName()); + values.put(MediaStore.Images.Media.IS_PENDING, 1); + Uri uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values); + if (uri == null) { + Logger.printInfo(() -> "Could not create the image: " + fileName); + return false; + } + try (OutputStream output = resolver.openOutputStream(uri)) { + if (output == null) { + throw new IllegalStateException("No output stream for: " + uri); + } + output.write(data); + } catch (Exception ex) { + resolver.delete(uri, null, null); + throw ex; + } + values.clear(); + values.put(MediaStore.Images.Media.IS_PENDING, 0); + resolver.update(uri, values, null, null); + return true; + } + + // endregion +} diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/FeedVideoCardsPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/FeedVideoCardsPatch.java index e1e3846663..97eb4ccc6e 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/FeedVideoCardsPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/FeedVideoCardsPatch.java @@ -36,6 +36,7 @@ import app.morphe.extension.shared.Utils; import app.morphe.extension.youtube.patches.FeedEngagementCountsPatch; import app.morphe.extension.youtube.patches.FeedViewCountPatch; +import app.morphe.extension.youtube.patches.PostImageDownloadPatch; import app.morphe.extension.youtube.patches.VideoInformation; import app.morphe.extension.youtube.settings.Settings; import app.morphe.extension.youtube.shared.PlayerType; @@ -59,7 +60,8 @@ public final class FeedVideoCardsPatch { private static final boolean ENABLED = FeedEngagementCountsPatch.ENABLED || FeedViewCountPatch.ENABLED - || Settings.FEED_WATCH_LATER_BUTTON.get(); + || Settings.FEED_WATCH_LATER_BUTTON.get() + || PostImageDownloadPatch.ENABLED; /** * A card must be as wide as a card of a grid of up to three columns, such as the feed in landscape. @@ -186,7 +188,10 @@ public static void onVideoLoaded(String videoId) { private FeedVideoCardsPatch() { } - private static boolean contains(byte[] bytes, byte[] search) { + /** + * @return If the bytes include the search bytes, such as an ASCII text of an element. + */ + public static boolean contains(byte[] bytes, byte[] search) { final int last = bytes.length - search.length; outer: for (int i = 0; i <= last; i++) { @@ -399,7 +404,7 @@ public static boolean isPlaylistTap(View sender) { * @return The root host of the card that contains the tapped view. */ @Nullable - private static ComponentHost findTappedRootHost(View sender) { + public static ComponentHost findTappedRootHost(View sender) { ViewParent parent = sender instanceof ComponentHost host ? host : sender.getParent(); while (parent instanceof View view && !(parent instanceof ComponentHost)) { parent = view.getParent(); @@ -576,7 +581,18 @@ public static String findVideoId(ComponentHost root) { scores.merge(videoId, score, Integer::sum); } } + // The texts of a mix or a playlist, which are never shared with a video. + // A single short text can belong to an element of another video that was not seen. + return findBestMatch(scores); + } + /** + * @param scores Match -> score of the texts that belong to it, where a long text scores 2. + * @return The match with the highest score, or null if there is a tie + * or the best match has only a single short text. + */ + @Nullable + public static String findBestMatch(Map scores) { String best = null; int bestScore = 0; boolean tie = false; @@ -590,8 +606,6 @@ public static String findVideoId(ComponentHost root) { tie = true; } } - // The texts of a mix or a playlist, which are never shared with a video. - // A single short text can belong to an element of another video that was not seen. return tie || bestScore < 2 ? null : best; } @@ -671,6 +685,7 @@ public static void scheduleRetry(ComponentHost root) { final String cardVideoId = PLAYLIST.equals(videoId) ? null : videoId; FeedEngagementCountsPatch.onCardLaidOut(root, cardVideoId); FeedViewCountPatch.onCardLaidOut(root, cardVideoId); + PostImageDownloadPatch.onRootLaidOut(root); } } catch (Exception ex) { Logger.printException(() -> "updateRoots failure", ex); diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/FlyoutUtils.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/FlyoutUtils.java index dec47040f8..0482beefc9 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/FlyoutUtils.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/FlyoutUtils.java @@ -53,6 +53,7 @@ import app.morphe.extension.youtube.patches.FeedVideoActionsPatch; import app.morphe.extension.youtube.patches.LocalQueuePatch; import app.morphe.extension.youtube.patches.PictureinPictureButtonPatch; +import app.morphe.extension.youtube.patches.PostImageDownloadPatch; import app.morphe.extension.youtube.patches.SaveToWatchLaterPatch; import app.morphe.extension.youtube.patches.VideoDelayPatch; import app.morphe.extension.youtube.patches.VideoInformation; @@ -496,6 +497,15 @@ private static void addFlyoutElements(Object flyoutPanel) { final String flyoutVideoId = getFlyoutVideoId(); + // Only added to the menus of posts with images. + if (PostImageDownloadPatch.ENABLED) { + nextButtonIndex = PostImageDownloadPatch.addFlyoutButton( + flyoutPanel, + nextButtonIndex, + senderViewRef.get() + ); + } + // region to show the following buttons only for specific flyout menus. final String allButtonsVideoId; if (!flyoutVideoId.isEmpty()) { diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/Settings.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/Settings.java index bd414a69ad..ef5704b147 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/Settings.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/Settings.java @@ -356,6 +356,9 @@ public class Settings extends SharedYouTubeSettings { public static final BooleanSetting VIDEO_QUALITY_DIALOG_BUTTON = new BooleanSetting("morphe_video_quality_dialog_button", FALSE, true); public static final BooleanSetting VIDEO_QUALITY_DIALOG_BUTTON_RESOLUTION = new BooleanSetting("morphe_video_quality_dialog_button_resolution", FALSE, parent(VIDEO_QUALITY_DIALOG_BUTTON)); + // Post image download + public static final BooleanSetting POST_IMAGE_DOWNLOAD = new BooleanSetting("morphe_post_image_download", TRUE); + // Feed view count public static final BooleanSetting FEED_VIEW_COUNT = new BooleanSetting("morphe_feed_view_count", FALSE, true); public static final StringSetting FEED_VIEW_COUNT_PATHS = new StringSetting("morphe_feed_view_count_paths", "", false, false); diff --git a/extensions/youtube/stub/src/main/java/android/support/v7/widget/RecyclerView.java b/extensions/youtube/stub/src/main/java/android/support/v7/widget/RecyclerView.java index b768b84844..c8b6d3c9fe 100644 --- a/extensions/youtube/stub/src/main/java/android/support/v7/widget/RecyclerView.java +++ b/extensions/youtube/stub/src/main/java/android/support/v7/widget/RecyclerView.java @@ -17,4 +17,9 @@ public View getChildAt(@SuppressWarnings("unused") final int index) { public int getChildCount() { return 0; } + + @Override + public int computeHorizontalScrollOffset() { + return 0; + } } diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/postimages/PostImageDownloadPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/postimages/PostImageDownloadPatch.kt new file mode 100644 index 0000000000..a41b53bf8d --- /dev/null +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/postimages/PostImageDownloadPatch.kt @@ -0,0 +1,70 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/3966 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.patches.youtube.interaction.postimages + +import app.morphe.patcher.extensions.InstructionExtensions.addInstructionsWithLabels +import app.morphe.patcher.patch.bytecodePatch +import app.morphe.patches.all.misc.resources.addResourcesPatch +import app.morphe.patches.shared.misc.proto.hookElement +import app.morphe.patches.shared.misc.settings.preference.SwitchPreference +import app.morphe.patches.youtube.interaction.feedactions.FeedComponentHostTouchFingerprint +import app.morphe.patches.youtube.layout.flyout.flyoutPatch +import app.morphe.patches.youtube.misc.extension.sharedExtensionPatch +import app.morphe.patches.youtube.misc.feedcards.feedVideoCardsHookPatch +import app.morphe.patches.youtube.misc.proto.elementProtoParserHookPatch +import app.morphe.patches.youtube.misc.settings.PreferenceScreen +import app.morphe.patches.youtube.misc.settings.settingsPatch +import app.morphe.patches.youtube.shared.Constants.COMPATIBILITY_YOUTUBE +import app.morphe.util.setExtensionIsPatchIncluded + +private const val EXTENSION_CLASS = + "Lapp/morphe/extension/youtube/patches/PostImageDownloadPatch;" + +@Suppress("unused") +val postImageDownloadPatch = bytecodePatch( + name = "Download post images", + description = "Adds a button and a menu item to download the images and GIFs of posts.", +) { + dependsOn( + sharedExtensionPatch, + settingsPatch, + addResourcesPatch, + elementProtoParserHookPatch, + // Adds the item to the menu of the post. + flyoutPatch, + // Finds the root host of each post when it is laid out. + feedVideoCardsHookPatch, + ) + + compatibleWith(COMPATIBILITY_YOUTUBE) + + execute { + PreferenceScreen.FEED.addPreferences( + SwitchPreference("morphe_post_image_download", summary = true), + ) + + // The element of a post includes the urls of its images and the texts it shows. + hookElement("$EXTENSION_CLASS->onElementLoaded") + + // The download button is drawn on the overlay of the root host of the post, + // so the touches of the root host decide if it was tapped. + FeedComponentHostTouchFingerprint.method.addInstructionsWithLabels( + 0, + """ + invoke-static { p0, p1 }, $EXTENSION_CLASS->onComponentHostTouch(Lcom/facebook/litho/ComponentHost;Landroid/view/MotionEvent;)Z + move-result v0 + if-eqz v0, :original + return v0 + :original + nop + """ + ) + + setExtensionIsPatchIncluded(EXTENSION_CLASS) + } +} diff --git a/patches/src/main/resources/addresources/values/youtube/strings.xml b/patches/src/main/resources/addresources/values/youtube/strings.xml index f645470aeb..e2948ee612 100644 --- a/patches/src/main/resources/addresources/values/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values/youtube/strings.xml @@ -468,6 +468,18 @@ Reports are public and are reviewed before a channel is added to a list. Removal Show Watch later button on thumbnails A Watch later button is shown on the thumbnails of feed videos. Tapping and holding it opens Save to playlist + + Download post images + A download button is shown next to the Share button of posts with images or GIFs, and a download item is added to their menu + Download images + Download image + Downloading %s images… + Downloading image… + %1$s images saved to %2$s + Image saved to %s + Could not download the image + No image found in this post + Add Ask item to flyout menu Adds a video summarize item to the feed flyout menu From b717a692dfc3c99bee830af548bd13f98a3a4eaa Mon Sep 17 00:00:00 2001 From: RAMiK <60112744+RAMiK4@users.noreply.github.com> Date: Sun, 25 Oct 2026 20:36:12 +0400 Subject: [PATCH 060/189] feat(YouTube - Playback speed): Add playback speed to the Shorts player menu (#3982) --- .../youtube/patches/VideoInformation.java | 36 +++++- .../speed/RememberPlaybackSpeedPatch.java | 87 +++++++++++--- .../speed/ShortsPlaybackSpeedFlyoutPatch.java | 109 ++++++++++++++++++ .../youtube/patches/utils/FlyoutUtils.java | 76 ++++++++++-- .../extension/youtube/settings/Settings.java | 2 + .../youtube/video/speed/PlaybackSpeedPatch.kt | 12 ++ .../remember/RememberPlaybackSpeedPatch.kt | 19 +-- .../shorts/ShortsPlaybackSpeedFlyoutPatch.kt | 41 +++++++ .../addresources/values/youtube/strings.xml | 5 + 9 files changed, 349 insertions(+), 38 deletions(-) create mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/playback/speed/ShortsPlaybackSpeedFlyoutPatch.java create mode 100644 patches/src/main/kotlin/app/morphe/patches/youtube/video/speed/shorts/ShortsPlaybackSpeedFlyoutPatch.kt diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/VideoInformation.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/VideoInformation.java index 112e988bb4..7fc0bc785e 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/VideoInformation.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/VideoInformation.java @@ -535,7 +535,10 @@ private static boolean updatePlaybackAudioPitchValue(float pitch) { /** * Injection point. */ - public static void videoSpeedChanged(float currentVideoSpeed) { + public static void videoSpeedChanged(float videoSpeed) { + // The Shorts player reports its own speed when a Short loads, + // but plays at the overridden Shorts speed. + final float currentVideoSpeed = RememberPlaybackSpeedPatch.getPlaybackSpeedInShorts(videoSpeed); Logger.printDebug(() -> "Video speed changed: " + currentVideoSpeed); // An exception occurs when the playback speed dialog is opened by an overlay button while 'Restore old playback speed menu' is off. // Update the formatted string value to avoid the exception. @@ -561,7 +564,7 @@ public static void setAudioPitch(float currentAudioPitch) { RememberPlaybackSpeedPatch.userSelectedPlaybackAudioPitch(playbackAudioPitch); if (!Settings.PLAYBACK_AUDIO_TIME_STRETCHING.get() && previousPlaybackSpeed != playbackSpeed) { RememberPlaybackSpeedPatch.userSelectedPlaybackSpeed(playbackSpeed); - changePlaybackSpeed(playbackSpeed); + applyUserSelectedPlaybackSpeed(playbackSpeed); } else { setPlaybackParameters(playbackSpeed, playbackAudioPitch); } @@ -1091,6 +1094,11 @@ private static void setPlaybackSpeedFormattedString( */ public static void setPlaybackSpeed(float newlyLoadedPlaybackSpeed) { if (!updatePlaybackSpeedValue(newlyLoadedPlaybackSpeed)) { + // The Shorts player can play at another speed than the one last recorded, + // such as when it reopens, so the selected speed is always applied. + if (ShortsPlayerState.isOpen()) { + applyUserSelectedPlaybackSpeed(newlyLoadedPlaybackSpeed); + } return; } @@ -1098,7 +1106,29 @@ public static void setPlaybackSpeed(float newlyLoadedPlaybackSpeed) { if (isPlaybackAudioPitchLinked()) { RememberPlaybackSpeedPatch.userSelectedPlaybackAudioPitch(playbackAudioPitch); } - changePlaybackSpeed(playbackSpeed); + applyUserSelectedPlaybackSpeed(playbackSpeed); + } + + /** + * Sets the speed of the Shorts player, which has no playback speed menu interface. + */ + public static void setShortsPlaybackSpeed(float speed) { + updatePlaybackSpeedValue(speed); + setPlaybackParameters(speed, playbackAudioPitch); + } + + /** + * Applies a speed the user selected in a Morphe speed menu. + * The Shorts player ignores {@link #changePlaybackSpeed(float)}, so the speed + * is set on the player directly and kept for the following Shorts. + */ + private static void applyUserSelectedPlaybackSpeed(float speed) { + if (ShortsPlayerState.isOpen()) { + RememberPlaybackSpeedPatch.setShortsSelectedPlaybackSpeed(speed); + setPlaybackParameters(speed, playbackAudioPitch); + return; + } + changePlaybackSpeed(speed); } private static void setCurrentQuality(@Nullable VideoQualityInterface quality) { diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/playback/speed/RememberPlaybackSpeedPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/playback/speed/RememberPlaybackSpeedPatch.java index f122399575..c8805a84b7 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/playback/speed/RememberPlaybackSpeedPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/playback/speed/RememberPlaybackSpeedPatch.java @@ -16,6 +16,7 @@ import app.morphe.extension.shared.Logger; import app.morphe.extension.shared.Utils; +import app.morphe.extension.shared.settings.BooleanSetting; import app.morphe.extension.shared.settings.FloatSetting; import app.morphe.extension.youtube.patches.VideoInformation; import app.morphe.extension.youtube.patches.utils.requests.GetMixPlaylistRequest; @@ -51,12 +52,20 @@ public final class RememberPlaybackSpeedPatch { */ private static volatile float shortsPlaybackSpeed = -2.0f; + /** + * The speed the user selected in the Shorts player, or a negative value if none. + * Used for every following Short until the Shorts player closes. + */ + private static volatile float shortsSelectedPlaybackSpeed = -2.0f; + static { // The Shorts player does not set a playback speed when a Short starts, // so the Shorts speed is set once when the Shorts player opens. ShortsPlayerState.getOnChange().addObserver((Boolean isOpen) -> { if (isOpen) { applyShortsPlaybackSpeed(true); + } else { + shortsSelectedPlaybackSpeed = -2.0f; } return Unit.INSTANCE; }); @@ -72,14 +81,33 @@ public final class RememberPlaybackSpeedPatch { }); } + /** + * Called by {@link VideoInformation} when the user selects a speed in the Shorts player. + */ + public static void setShortsSelectedPlaybackSpeed(float playbackSpeed) { + shortsSelectedPlaybackSpeed = playbackSpeed; + shortsPlaybackSpeed = playbackSpeed; + } + + /** + * @return The speed selected in the Shorts player, otherwise the Shorts default speed, + * or a negative value if neither is set. + */ + private static float getShortsSessionSpeed() { + final float selectedSpeed = shortsSelectedPlaybackSpeed; + return selectedSpeed > 0 + ? selectedSpeed + : Settings.SHORTS_PLAYBACK_SPEED_DEFAULT.get(); + } + /** * @return The speed of the current Short: 1.0x if its channel is whitelisted, - * otherwise the Shorts default speed. + * otherwise the speed of the Shorts session. */ - private static float getShortsPlaybackSpeed(float shortsDefaultSpeed) { + private static float getShortsPlaybackSpeed(float sessionSpeed) { return ChannelWhitelist.isCurrentChannelWhitelisted(WhitelistType.PLAYBACK_SPEED) ? 1.0f - : shortsDefaultSpeed; + : sessionSpeed; } /** @@ -94,23 +122,37 @@ private static float getShortsPlaybackSpeed(float shortsDefaultSpeed) { */ private static void applyShortsPlaybackSpeed(boolean playerOpened) { try { - final float shortsDefaultSpeed = Settings.SHORTS_PLAYBACK_SPEED_DEFAULT.get(); - if (shortsDefaultSpeed <= 0 || VideoInformation.isLiveStream()) { - return; + final float speed; + if (playerOpened) { + final float shortsDefaultSpeed = Settings.SHORTS_PLAYBACK_SPEED_DEFAULT.get(); + if (shortsDefaultSpeed > 0) { + speed = shortsDefaultSpeed; + } else if (shortsPlaybackSpeed > 0 && shortsPlaybackSpeed != 1.0f) { + // The player keeps the speed it last played at, such as a speed the user + // selected the last time, which is not used anymore. + speed = 1.0f; + } else { + return; + } + } else { + final float sessionSpeed = getShortsSessionSpeed(); + if (sessionSpeed <= 0) { + return; + } + speed = getShortsPlaybackSpeed(sessionSpeed); + if (speed == shortsPlaybackSpeed) { + return; + } } - final float speed = playerOpened - ? shortsDefaultSpeed - : getShortsPlaybackSpeed(shortsDefaultSpeed); - if (!playerOpened && speed == shortsPlaybackSpeed) { + if (VideoInformation.isLiveStream()) { return; } shortsPlaybackSpeed = speed; Utils.runOnMainThreadNowOrLater(() -> { Logger.printDebug(() -> "Shorts speed: applying " + speed); - VideoInformation.setPlaybackParameters( - speed, VideoInformation.getPlaybackAudioPitch()); + VideoInformation.setShortsPlaybackSpeed(speed); }); } catch (Exception ex) { Logger.printException(() -> "applyShortsPlaybackSpeed failure", ex); @@ -123,7 +165,7 @@ private static void applyShortsPlaybackSpeed(boolean playerOpened) { */ public static void onLiveStreamChanged(boolean isLive) { try { - if (!isLive || Settings.SHORTS_PLAYBACK_SPEED_DEFAULT.get() <= 0 || !ShortsPlayerState.isOpen()) { + if (!isLive || getShortsSessionSpeed() <= 0 || !ShortsPlayerState.isOpen()) { return; } @@ -161,7 +203,12 @@ public static void preservePlaybackParametersForReload() { */ public static void userSelectedPlaybackSpeed(float playbackSpeed) { try { - if (!Settings.REMEMBER_PLAYBACK_SPEED_LAST_SELECTED.get()) { + // Speed changes in the Shorts player are remembered separately. + final boolean isShorts = ShortsPlayerState.isOpen(); + final BooleanSetting rememberSetting = isShorts + ? Settings.SHORTS_REMEMBER_PLAYBACK_SPEED + : Settings.REMEMBER_PLAYBACK_SPEED_LAST_SELECTED; + if (!rememberSetting.get()) { return; } // With the 0.05x menu, if the speed is set by a patch to higher than 2.0x @@ -169,8 +216,7 @@ public static void userSelectedPlaybackSpeed(float playbackSpeed) { // still capped to 8.0x. playbackSpeed = Math.min(playbackSpeed, VideoInformation.PLAYBACK_SPEED_MAXIMUM); - // Speed changes in the Shorts player are remembered separately. - final FloatSetting defaultSpeedSetting = ShortsPlayerState.isOpen() + final FloatSetting defaultSpeedSetting = isShorts ? Settings.SHORTS_PLAYBACK_SPEED_DEFAULT : Settings.PLAYBACK_SPEED_DEFAULT; @@ -254,12 +300,15 @@ public static void userSelectedPlaybackAudioPitch(float playbackAudioPitch) { */ public static float getPlaybackSpeedInShorts(float playbackSpeed) { try { - final float shortsDefaultSpeed = Settings.SHORTS_PLAYBACK_SPEED_DEFAULT.get(); - if (shortsDefaultSpeed <= 0 || !ShortsPlayerState.isOpen() || VideoInformation.isLiveStream()) { + if (!ShortsPlayerState.isOpen() || VideoInformation.isLiveStream()) { + return playbackSpeed; + } + final float sessionSpeed = getShortsSessionSpeed(); + if (sessionSpeed <= 0) { return playbackSpeed; } - final float speed = getShortsPlaybackSpeed(shortsDefaultSpeed); + final float speed = getShortsPlaybackSpeed(sessionSpeed); shortsPlaybackSpeed = speed; Logger.printDebug(() -> "Shorts speed: overriding " + playbackSpeed + " to " + speed); diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/playback/speed/ShortsPlaybackSpeedFlyoutPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/playback/speed/ShortsPlaybackSpeedFlyoutPatch.java new file mode 100644 index 0000000000..efde37ac2c --- /dev/null +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/playback/speed/ShortsPlaybackSpeedFlyoutPatch.java @@ -0,0 +1,109 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/3982 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to Morphe contributions. + */ + +package app.morphe.extension.youtube.patches.playback.speed; + +import static app.morphe.extension.shared.StringRef.str; + +import android.content.Context; +import android.util.Pair; + +import java.util.List; + +import app.morphe.extension.shared.Logger; +import app.morphe.extension.shared.ResourceUtils; +import app.morphe.extension.shared.Utils; +import app.morphe.extension.youtube.patches.VideoInformation; +import app.morphe.extension.youtube.patches.utils.FlyoutUtils; +import app.morphe.extension.youtube.settings.Settings; +import app.morphe.extension.youtube.shared.EngagementPanel; +import app.morphe.extension.youtube.shared.ShortsPlayerState; + +/** + * Adds a playback speed item to the flyout menu of the Shorts player. + */ +@SuppressWarnings("unused") +public final class ShortsPlaybackSpeedFlyoutPatch { + + /** + * Items of the menu of the Shorts player, to tell it apart from the other + * bottom sheets opened in the Shorts player, such as the share sheet. + */ + private static final List SHORTS_MENU_BUTTONS = List.of( + "ALIGN_LEFT", // Description. + "NERD_STATS", // Stats for nerds. + "FLAG", // Report. + "FEEDBACK" // Send feedback. + ); + + /** + * Time for the flyout to close, so the speed menu is not dismissed with it. + */ + private static final long SPEED_MENU_DELAY_MILLISECONDS = 100; + + public static boolean isPatchIncluded() { + return false; // Modified during patching. + } + + /** + * Called by {@link FlyoutUtils} for every flyout menu that opens. + * + * @param isShortFlyout If the menu belongs to a Short in the feed or on a channel. + * @return If the menu is the menu of the Shorts player and gets the playback speed item. + */ + public static boolean showFlyoutButton(boolean isShortFlyout) { + return Settings.SHORTS_PLAYBACK_SPEED_FLYOUT_BUTTON.get() + && ShortsPlayerState.isOpen() + && !isShortFlyout + && !VideoInformation.isLiveStream() + // Menus opened inside a panel, such as the menu of a comment. + && EngagementPanel.getLastOpenedPanelId() == null + && isShortsPlayerMenu(); + } + + private static boolean isShortsPlayerMenu() { + for (Pair button : FlyoutUtils.getVisibleFlyoutButtons()) { + if (SHORTS_MENU_BUTTONS.contains(button.first)) { + return true; + } + } + return false; + } + + /** + * Called by {@link FlyoutUtils} if {@link #showFlyoutButton(boolean)} is true. + */ + public static int addFlyoutButton(Object flyoutPanel, int nextButtonIndex) { + try { + return FlyoutUtils.addFlyoutButton( + flyoutPanel, + ResourceUtils.getDrawable("yt_outline_play_arrow_half_circle_black_24"), + str("morphe_shorts_playback_speed_flyout_title", + VideoInformation.formatSpeedStringX(VideoInformation.getPlaybackSpeed())), + v -> { + Context context = v.getContext(); + FlyoutUtils.dismissFlyout(); + showSpeedMenu(context); + }, + nextButtonIndex + ); + } catch (Exception ex) { + Logger.printException(() -> "addFlyoutButton failure", ex); + return nextButtonIndex; + } + } + + private static void showSpeedMenu(Context context) { + Utils.runOnMainThreadDelayed(() -> { + try { + CustomPlaybackSpeedPatch.showModernCustomPlaybackSpeedDialog(context); + } catch (Exception ex) { + Logger.printException(() -> "showSpeedMenu failure", ex); + } + }, SPEED_MENU_DELAY_MILLISECONDS); + } +} diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/FlyoutUtils.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/FlyoutUtils.java index 0482beefc9..25f6a75c18 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/FlyoutUtils.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/FlyoutUtils.java @@ -17,6 +17,7 @@ import android.graphics.drawable.Drawable; import android.os.Handler; import android.os.Looper; +import android.support.v7.widget.RecyclerView; import android.text.TextUtils; import android.util.Pair; import android.view.LayoutInflater; @@ -25,6 +26,7 @@ import android.view.ViewGroup; import android.view.ViewParent; import android.view.Window; +import android.widget.AbsListView; import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.LinearLayout; @@ -58,6 +60,7 @@ import app.morphe.extension.youtube.patches.VideoDelayPatch; import app.morphe.extension.youtube.patches.VideoInformation; import app.morphe.extension.youtube.patches.components.PlayerFlyoutMenuComponentsFilter; +import app.morphe.extension.youtube.patches.playback.speed.ShortsPlaybackSpeedFlyoutPatch; import app.morphe.extension.youtube.patches.utils.requests.ChannelIdRequest; import app.morphe.extension.youtube.settings.Settings; import app.morphe.extension.youtube.videoplayer.PictureinPictureButton; @@ -323,6 +326,11 @@ public boolean dispatchTouchEvent(MotionEvent ev) { private static boolean videoMarkedAsForKids; private static boolean isMyTabHistoryFlyout; private static boolean isShortFlyout; + /** + * If the open menu is the menu of the Shorts player, which has another layout + * and only gets the playback speed item. + */ + private static boolean isShortsPlayerFlyout; private static ChannelIdRequest flyoutChannelIdRequest; public static byte[] getAsciiBytes(String string) { @@ -486,6 +494,7 @@ private static void resetFlyoutIds() { flyoutPlaylistId = ""; flyoutChannelId = ""; flyoutChannelName = ""; + isShortsPlayerFlyout = false; } private static void addFlyoutElements(Object flyoutPanel) { @@ -495,6 +504,24 @@ private static void addFlyoutElements(Object flyoutPanel) { // onto whatever this call adds instead. customItemTextRefs.clear(); + isShortsPlayerFlyout = ShortsPlaybackSpeedFlyoutPatch.isPatchIncluded() + && ShortsPlaybackSpeedFlyoutPatch.showFlyoutButton(isShortFlyout); + if (isShortsPlayerFlyout) { + nextButtonIndex = ShortsPlaybackSpeedFlyoutPatch.addFlyoutButton(flyoutPanel, nextButtonIndex); + } else { + nextButtonIndex = addVideoFlyoutButtons(flyoutPanel, nextButtonIndex); + } + + if (nextButtonIndex > 0) { + addFlyoutMenuItem(flyoutPanel, null, null, null, nextButtonIndex, true); + } + + // Reset 'topFlyoutMenuVisible' field, once the buttons have been injected into the player's + // overlay settings, to prevent them from also being added into nested menus. + PlayerFlyoutMenuComponentsFilter.resetTopFlyoutMenuVisible(); + } + + private static int addVideoFlyoutButtons(Object flyoutPanel, int nextButtonIndex) { final String flyoutVideoId = getFlyoutVideoId(); // Only added to the menus of posts with images. @@ -564,13 +591,7 @@ private static void addFlyoutElements(Object flyoutPanel) { ); } - if (nextButtonIndex > 0) { - addFlyoutMenuItem(flyoutPanel, null, null, null, nextButtonIndex, true); - } - - // Reset 'topFlyoutMenuVisible' field, once the buttons have been injected into the player's - // overlay settings, to prevent them from also being added into nested menus. - PlayerFlyoutMenuComponentsFilter.resetTopFlyoutMenuVisible(); + return nextButtonIndex; } /** @@ -827,12 +848,39 @@ private static FlyoutMenuInfo getFlyoutMenuInfo(Object flyoutPanel, int initialI } if (menuContainer == null) { - return null; + return isShortsPlayerFlyout + ? getShortsPlayerMenuInfo(flyoutPanel, popupWindow) + : null; } return new FlyoutMenuInfo(menuContainer, adjustedIndex, popupWindow); } + /** + * The menu container of the Shorts player menu is the vertical layout that holds the list of the menu. + * The items are injected above the list, so after the bottom sheet handle. + */ + @Nullable + private static FlyoutMenuInfo getShortsPlayerMenuInfo(Object flyoutPanel, @Nullable PopupWindow popupWindow) { + View root = getFlyoutRootView(flyoutPanel); + if (root instanceof ViewGroup rootGroup) { + View child = Utils.getChildView(rootGroup, true, + view -> view instanceof RecyclerView || view instanceof AbsListView); + ViewParent parent = child == null ? null : child.getParent(); + while (parent instanceof ViewGroup parentGroup && child != root) { + if (parentGroup instanceof LinearLayout linearLayout + && linearLayout.getOrientation() == LinearLayout.VERTICAL) { + return new FlyoutMenuInfo(linearLayout, linearLayout.indexOfChild(child), popupWindow); + } + child = parentGroup; + parent = parentGroup.getParent(); + } + } + + Logger.printDebug(() -> "Shorts player menu container not found"); + return null; + } + /** * The menu container of a popup menu is the first LinearLayout found by following the first child * of the content view. Depending on the app version, the device and A/B tests, the content view is: @@ -854,6 +902,18 @@ private static LinearLayout getPopupMenuContainer(@Nullable View contentView) { return null; } + @Nullable + private static View getFlyoutRootView(Object flyoutPanel) { + if (flyoutPanel instanceof PopupWindow popupWindow) { + return popupWindow.getContentView(); + } + if (flyoutPanel instanceof Dialog dialog) { + Window window = dialog.getWindow(); + return window == null ? null : window.getDecorView(); + } + return null; + } + @SuppressLint("ResourceType") private static View addFlyoutButton( Context context, diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/Settings.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/Settings.java index ef5704b147..37ed6ff2ba 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/Settings.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/Settings.java @@ -120,6 +120,8 @@ public class Settings extends SharedYouTubeSettings { public static final FloatSetting PLAYBACK_SPEED_DEFAULT = new FloatSetting("morphe_playback_speed_default", -2.0f); // -2.0f is YouTube's "auto" entry and disables the Shorts override. public static final FloatSetting SHORTS_PLAYBACK_SPEED_DEFAULT = new FloatSetting("morphe_shorts_playback_speed_default", -2.0f); + public static final BooleanSetting SHORTS_REMEMBER_PLAYBACK_SPEED = new BooleanSetting("morphe_shorts_remember_playback_speed", FALSE); + public static final BooleanSetting SHORTS_PLAYBACK_SPEED_FLYOUT_BUTTON = new BooleanSetting("morphe_shorts_playback_speed_flyout_button", TRUE); public static final FloatSetting PLAYBACK_AUDIO_PITCH_DEFAULT = new FloatSetting("morphe_playback_audio_pitch_default", -2.0f); public static final BooleanSetting CUSTOM_SPEED_MENU = new BooleanSetting("morphe_custom_speed_menu", TRUE); public static final BooleanSetting DISABLE_PLAYBACK_SPEED_MUSIC = new BooleanSetting("morphe_disable_playback_speed_music", FALSE, true); diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/video/speed/PlaybackSpeedPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/video/speed/PlaybackSpeedPatch.kt index 910e93616b..f41d71376e 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/video/speed/PlaybackSpeedPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/video/speed/PlaybackSpeedPatch.kt @@ -14,12 +14,14 @@ import app.morphe.patcher.patch.bytecodePatch import app.morphe.patches.shared.misc.settings.preference.BasePreference import app.morphe.patches.shared.misc.settings.preference.PreferenceCategory import app.morphe.patches.shared.misc.settings.preference.PreferenceScreenPreference.Sorting +import app.morphe.patches.shared.misc.settings.preference.noTitleUnsortedPreferenceCategory import app.morphe.patches.youtube.misc.settings.PreferenceScreen import app.morphe.patches.youtube.shared.Constants.COMPATIBILITY_YOUTUBE import app.morphe.patches.youtube.video.information.EXTENSION_CLASS import app.morphe.patches.youtube.video.speed.button.playbackSpeedButtonPatch import app.morphe.patches.youtube.video.speed.custom.customPlaybackSpeedPatch import app.morphe.patches.youtube.video.speed.remember.rememberPlaybackSpeedPatch +import app.morphe.patches.youtube.video.speed.shorts.shortsPlaybackSpeedFlyoutPatch import app.morphe.util.setExtensionIsPatchIncluded /** @@ -27,6 +29,11 @@ import app.morphe.util.setExtensionIsPatchIncluded */ internal val settingsMenuVideoSpeedGroup = mutableSetOf() +/** + * Shorts speed settings. Used to keep the Shorts speed settings together in the Shorts settings. + */ +internal val settingsMenuShortsSpeedGroup = mutableSetOf() + @Suppress("unused") val playbackSpeedPatch = bytecodePatch( name = "Playback speed", @@ -37,6 +44,7 @@ val playbackSpeedPatch = bytecodePatch( customPlaybackSpeedPatch, rememberPlaybackSpeedPatch, playbackSpeedButtonPatch, + shortsPlaybackSpeedFlyoutPatch, ) compatibleWith(COMPATIBILITY_YOUTUBE) @@ -53,5 +61,9 @@ val playbackSpeedPatch = bytecodePatch( preferences = settingsMenuVideoSpeedGroup ) ) + + PreferenceScreen.SHORTS.addPreferences( + noTitleUnsortedPreferenceCategory(settingsMenuShortsSpeedGroup) + ) } } diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/video/speed/remember/RememberPlaybackSpeedPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/video/speed/remember/RememberPlaybackSpeedPatch.kt index a76e74e5bc..0a16c6f27f 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/video/speed/remember/RememberPlaybackSpeedPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/video/speed/remember/RememberPlaybackSpeedPatch.kt @@ -15,7 +15,6 @@ import app.morphe.patcher.patch.bytecodePatch import app.morphe.patches.shared.misc.settings.preference.ListPreference import app.morphe.patches.shared.misc.settings.preference.SwitchPreference import app.morphe.patches.youtube.misc.extension.sharedExtensionPatch -import app.morphe.patches.youtube.misc.settings.PreferenceScreen import app.morphe.patches.youtube.misc.settings.settingsPatch import app.morphe.patches.youtube.shared.InitializePlaybackSpeedValuesFingerprint import app.morphe.patches.youtube.video.information.EXTENSION_PLAYBACK_SPEED_MENU_INTERFACE @@ -23,6 +22,7 @@ import app.morphe.patches.youtube.video.information.onCreateHook import app.morphe.patches.youtube.video.information.userSelectedPlaybackSpeedHook import app.morphe.patches.youtube.video.information.videoInformationPatch import app.morphe.patches.youtube.video.speed.custom.customPlaybackSpeedPatch +import app.morphe.patches.youtube.video.speed.settingsMenuShortsSpeedGroup import app.morphe.patches.youtube.video.speed.settingsMenuVideoSpeedGroup import app.morphe.patches.youtube.video.videoid.hookPlayerResponseVideoId import app.morphe.patches.youtube.video.videoid.videoIdPatch @@ -63,13 +63,16 @@ internal val rememberPlaybackSpeedPatch = bytecodePatch { ) // The Shorts speed belongs with the other Shorts settings. - PreferenceScreen.SHORTS.addPreferences( - ListPreference( - key = "morphe_shorts_playback_speed_default", - // Entries and values are set by the extension code based on the actual speeds available. - entriesKey = null, - entryValuesKey = null, - tag = "app.morphe.extension.youtube.settings.preference.CustomVideoSpeedListPreference" + settingsMenuShortsSpeedGroup.addAll( + listOf( + ListPreference( + key = "morphe_shorts_playback_speed_default", + // Entries and values are set by the extension code based on the actual speeds available. + entriesKey = null, + entryValuesKey = null, + tag = "app.morphe.extension.youtube.settings.preference.CustomVideoSpeedListPreference" + ), + SwitchPreference("morphe_shorts_remember_playback_speed", summary = true) ) ) diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/video/speed/shorts/ShortsPlaybackSpeedFlyoutPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/video/speed/shorts/ShortsPlaybackSpeedFlyoutPatch.kt new file mode 100644 index 0000000000..94067f95c0 --- /dev/null +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/video/speed/shorts/ShortsPlaybackSpeedFlyoutPatch.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/3982 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to Morphe contributions. + */ + +package app.morphe.patches.youtube.video.speed.shorts + +import app.morphe.patcher.patch.bytecodePatch +import app.morphe.patches.shared.misc.settings.preference.SwitchPreference +import app.morphe.patches.youtube.layout.flyout.flyoutPatch +import app.morphe.patches.youtube.misc.extension.sharedExtensionPatch +import app.morphe.patches.youtube.misc.settings.settingsPatch +import app.morphe.patches.youtube.video.information.videoInformationPatch +import app.morphe.patches.youtube.video.speed.custom.customPlaybackSpeedPatch +import app.morphe.patches.youtube.video.speed.settingsMenuShortsSpeedGroup +import app.morphe.util.setExtensionIsPatchIncluded + +private const val EXTENSION_CLASS = + "Lapp/morphe/extension/youtube/patches/playback/speed/ShortsPlaybackSpeedFlyoutPatch;" + +internal val shortsPlaybackSpeedFlyoutPatch = bytecodePatch( + description = "Adds a playback speed item to the flyout menu of the Shorts player.", +) { + dependsOn( + sharedExtensionPatch, + settingsPatch, + flyoutPatch, + videoInformationPatch, + customPlaybackSpeedPatch, + ) + + execute { + settingsMenuShortsSpeedGroup.add( + SwitchPreference("morphe_shorts_playback_speed_flyout_button", summary = true) + ) + + setExtensionIsPatchIncluded(EXTENSION_CLASS) + } +} diff --git a/patches/src/main/resources/addresources/values/youtube/strings.xml b/patches/src/main/resources/addresources/values/youtube/strings.xml index e2948ee612..2a0fcfcf7a 100644 --- a/patches/src/main/resources/addresources/values/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values/youtube/strings.xml @@ -1534,6 +1534,11 @@ Videos are requested by a short prefix of the hashed video ID, so the server can Shows a notification when a new default playback speed is saved Default playback speed Default playback speed on Shorts + Remember playback speed on Shorts + Applies your manual Shorts speed selection to all future Shorts. If off, the speed returns to default after closing the Shorts player + Show Playback speed in the Shorts menu + Adds a Playback speed item to the Shorts player menu + Playback speed · %s Changed default playback speed to: %s Use normal speed for music Always play music videos at 1.0x speed From 3e15dcd12190c4b435287080916170e82d0b4ac5 Mon Sep 17 00:00:00 2001 From: 0xrxL <127248639+0xrxL@users.noreply.github.com> Date: Sun, 25 Oct 2026 17:44:09 +0100 Subject: [PATCH 061/189] fix(YouTube - Video quality): Shorts quality menu sets another quality than the one selected --- .../quality/RememberVideoQualityPatch.java | 158 +++++++++++------- .../information/VideoInformationPatch.kt | 2 +- .../youtube/video/quality/Fingerprints.kt | 27 +++ .../quality/RememberVideoQualityPatch.kt | 52 +++++- 4 files changed, 172 insertions(+), 67 deletions(-) diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/playback/quality/RememberVideoQualityPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/playback/quality/RememberVideoQualityPatch.java index c802c3dd0d..fd22565b29 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/playback/quality/RememberVideoQualityPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/playback/quality/RememberVideoQualityPatch.java @@ -12,7 +12,6 @@ import static app.morphe.extension.shared.StringRef.str; -import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.google.protobuf.MessageLite; @@ -50,6 +49,12 @@ public class RememberVideoQualityPatch { private static volatile String userSelectedQualityVideoId = ""; private static volatile int userSelectedQualityResolution = VideoInformation.AUTOMATIC_VIDEO_QUALITY_VALUE; + /** + * The quality selected in the Shorts quality menu, until the menu sets it. + */ + @Nullable + private static volatile VideoQualityInterface shortsMenuSelectedQuality; + private static void setUserSelectedQuality(int resolution) { userSelectedQualityResolution = resolution; userSelectedQualityVideoId = VideoInformation.getVideoId(); @@ -96,25 +101,18 @@ public static void saveDefaultQuality(int qualityResolution) { qualitySetting.save(qualityResolution); if (Settings.REMEMBER_VIDEO_QUALITY_LAST_SELECTED_TOAST.get()) { - String qualityLabel = qualityResolution + "p"; - final String toastStringId = getString(shortPlayerOpen, isMobile); - Utils.showToastShort(str(toastStringId, qualityLabel)); - } - } - - @NonNull - private static String getString(boolean shortPlayerOpen, boolean isMobile) { - final String toastStringId; - if (shortPlayerOpen && isMobile) { - toastStringId = "morphe_remember_video_quality_toast_shorts_mobile"; - } else if (shortPlayerOpen) { - toastStringId = "morphe_remember_video_quality_toast_shorts_wifi"; - } else if (isMobile) { - toastStringId = "morphe_remember_video_quality_toast_mobile"; - } else { - toastStringId = "morphe_remember_video_quality_toast_wifi"; + final String toastStringId; + if (shortPlayerOpen && isMobile) { + toastStringId = "morphe_remember_video_quality_toast_shorts_mobile"; + } else if (shortPlayerOpen) { + toastStringId = "morphe_remember_video_quality_toast_shorts_wifi"; + } else if (isMobile) { + toastStringId = "morphe_remember_video_quality_toast_mobile"; + } else { + toastStringId = "morphe_remember_video_quality_toast_wifi"; + } + Utils.showToastShort(str(toastStringId, qualityResolution + "p")); } - return toastStringId; } /** @@ -187,29 +185,34 @@ public static int getLastSelectedVideoQuality() { } final int preferredQuality = getDefaultQualityResolution(); - return preferredQuality == VideoInformation.AUTOMATIC_VIDEO_QUALITY_VALUE - ? -1 - : getAvailableQualityResolution(preferredQuality); + if (preferredQuality == VideoInformation.AUTOMATIC_VIDEO_QUALITY_VALUE) { + return -1; + } + + // The quality last selected is used as the only quality allowed by the player. + // If the video does not have it, all lower qualities are allowed and + // the player chooses one of them automatically, such as 720p for the size of the player. + // So the highest quality of the current video that is not above the default quality + // is used, or the default quality if the qualities of the video are not known yet + // or all of them are above it (the player then uses the lowest quality). + VideoQualityInterface[] qualities = VideoInformation.getCurrentQualities(); + if (qualities != null) { + // Ordered from largest to smallest, with index 0 being 'Auto'. + for (VideoQualityInterface quality : qualities) { + final int resolution = quality.patch_getResolution(); + if (resolution != VideoInformation.AUTOMATIC_VIDEO_QUALITY_VALUE && resolution <= preferredQuality) { + return resolution; + } + } + } + + return preferredQuality; } catch (Exception ex) { Logger.printException(() -> "getLastSelectedVideoQuality failure", ex); return -1; } } - /** - * @return The resolution the SABR requests must keep: the quality selected by the user - * for the current video, otherwise the default quality. - * -1 to send the requests unchanged, with 'Auto' selected or no default quality. - */ - private static int getStickyVideoQuality() { - if (isQualitySelectedByUser()) { - final int selectedResolution = userSelectedQualityResolution; - return selectedResolution > 0 ? selectedResolution : -1; - } - - return getLastSelectedVideoQuality(); - } - /** * Injection point. *

@@ -228,7 +231,16 @@ public static byte[] overrideClientAbrState(@Nullable MessageLite clientAbrState return null; } - final int resolution = getStickyVideoQuality(); + // The requests keep the quality selected by the user for the current video, + // otherwise the default quality. They are sent unchanged with 'Auto' selected + // or no default quality. + final int resolution; + if (isQualitySelectedByUser()) { + final int selectedResolution = userSelectedQualityResolution; + resolution = selectedResolution > 0 ? selectedResolution : -1; + } else { + resolution = getLastSelectedVideoQuality(); + } if (resolution < 0) { return null; } @@ -266,30 +278,6 @@ public static byte[] overrideClientAbrState(@Nullable MessageLite clientAbrState } } - /** - * The quality last selected is used as the only quality allowed by the player. - * If the video does not have it, all lower qualities are allowed and - * the player chooses one of them automatically, such as 720p for the size of the player. - * - * @return The highest quality of the current video that is not above the preferred quality, - * or the preferred quality if the qualities of the video are not known yet - * or all of them are above it (the player then uses the lowest quality). - */ - private static int getAvailableQualityResolution(int preferredQuality) { - VideoQualityInterface[] qualities = VideoInformation.getCurrentQualities(); - if (qualities != null) { - // Ordered from largest to smallest, with index 0 being 'Auto'. - for (VideoQualityInterface quality : qualities) { - final int resolution = quality.patch_getResolution(); - if (resolution != VideoInformation.AUTOMATIC_VIDEO_QUALITY_VALUE && resolution <= preferredQuality) { - return resolution; - } - } - } - - return preferredQuality; - } - /** * Injection point. * @param userSelectedQualityIndex Element index of {@link VideoInformation#getCurrentQualities()}. @@ -301,10 +289,18 @@ public static void userChangedShortsQuality(int userSelectedQualityIndex) { Logger.printDebug(() -> "Cannot save default quality, qualities is null"); // The resolution is not known, so a reload uses the quality saved by YouTube. setUserSelectedQuality(VideoInformation.AUTOMATIC_VIDEO_QUALITY_VALUE); + clearShortsMenuSelectedQuality(); return; } - final int resolution = currentQualities[userSelectedQualityIndex].patch_getResolution(); + VideoQualityInterface quality = currentQualities[userSelectedQualityIndex]; + final int resolution = quality.patch_getResolution(); setUserSelectedQuality(resolution); + if (resolution == VideoInformation.AUTOMATIC_VIDEO_QUALITY_VALUE) { + // 'Auto' has no formats, so the menu sets it by its resolution. + clearShortsMenuSelectedQuality(); + } else { + shortsMenuSelectedQuality = quality; + } if (shouldRememberVideoQuality()) { saveDefaultQuality(resolution); @@ -314,6 +310,44 @@ public static void userChangedShortsQuality(int userSelectedQualityIndex) { } } + /** + * Injection point. + *

+ * The Shorts quality menu sets the selected quality by its resolution, which YouTube can get + * wrong, such as 720 for the 480p quality of some Shorts (see + * {@link VideoInformation#fixVideoQualityResolution(String, int)}). The player then plays + * another quality. The player finds a quality by its formats, so the quality selected in + * the menu is set instead, as with the quality menu of regular videos. + * + * @param menu The object the Shorts quality menu sets the quality with. + * @return If the quality selected was set, so the menu must not set it by its resolution. + */ + public static boolean setShortsMenuSelectedQuality(VideoQualityMenuInterface menu) { + VideoQualityInterface quality = shortsMenuSelectedQuality; + clearShortsMenuSelectedQuality(); + if (quality == null) { + return false; + } + + try { + menu.patch_setQuality(quality); + return true; + } catch (Exception ex) { + Logger.printException(() -> "setShortsMenuSelectedQuality failure", ex); + return false; + } + } + + /** + * Injection point. + *

+ * Called after the Shorts quality menu handled a click, so the quality selected is not set + * by another caller if the menu did not set it. + */ + public static void clearShortsMenuSelectedQuality() { + shortsMenuSelectedQuality = null; + } + /** * Injection point. Regular videos. * @param quality Quality selected by the user. diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/video/information/VideoInformationPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/video/information/VideoInformationPatch.kt index 8592340a08..29e8f2da9d 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/video/information/VideoInformationPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/video/information/VideoInformationPatch.kt @@ -76,7 +76,7 @@ internal const val EXTENSION_PLAYBACK_SPEED_MENU_INTERFACE = $$"Lapp/morphe/extension/youtube/patches/VideoInformation$PlaybackSpeedMenuInterface;" internal const val EXTENSION_EXOPLAYER_INTERFACE = "Lapp/morphe/extension/shared/patches/ExoPlayerInterface;" -private const val EXTENSION_VIDEO_QUALITY_MENU_INTERFACE = +internal const val EXTENSION_VIDEO_QUALITY_MENU_INTERFACE = $$"Lapp/morphe/extension/youtube/patches/VideoInformation$VideoQualityMenuInterface;" internal const val EXTENSION_VIDEO_QUALITY_INTERFACE = $$"Lapp/morphe/extension/youtube/patches/VideoInformation$VideoQualityInterface;" diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/video/quality/Fingerprints.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/video/quality/Fingerprints.kt index c06f36846b..ee4a9501e3 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/video/quality/Fingerprints.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/video/quality/Fingerprints.kt @@ -247,6 +247,33 @@ internal object VideoQualityItemOnClickFingerprint : Fingerprint( ) ) +/** + * The method of the quality menu that sets the quality by its resolution. + */ +internal fun getSetVideoQualityResolutionFingerprint(videoQualityMenuType: String) = Fingerprint( + definingClass = videoQualityMenuType, + returnType = "V", + parameters = listOf("I") +) + +/** + * The item click listener of the Shorts quality menu, which sets the selected quality by its resolution. + * The listener is either the menu of [VideoQualityItemOnClickFingerprint] or a class it calls. + */ +internal fun getVideoQualityItemSetResolutionFingerprint(setResolutionMethod: String) = Fingerprint( + name = "onItemClick", + returnType = "V", + parameters = listOf( + "Landroid/widget/AdapterView;", + "Landroid/view/View;", + "I", + "J" + ), + filters = listOf( + methodCall(smali = setResolutionMethod) + ) +) + internal object ShowVideoQualityQuickMenuFingerprint : Fingerprint( accessFlags = listOf(AccessFlags.PUBLIC, AccessFlags.FINAL), returnType = "V", diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/video/quality/RememberVideoQualityPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/video/quality/RememberVideoQualityPatch.kt index daf102d912..c26d55170e 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/video/quality/RememberVideoQualityPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/video/quality/RememberVideoQualityPatch.kt @@ -16,6 +16,7 @@ import app.morphe.patcher.extensions.InstructionExtensions.addInstructionsWithLa import app.morphe.patcher.extensions.InstructionExtensions.getInstruction import app.morphe.patcher.patch.bytecodePatch import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable +import app.morphe.patcher.util.smali.ExternalLabel import app.morphe.patches.shared.misc.fix.proto.fixProtoLibraryPatch import app.morphe.patches.shared.misc.fix.proto.parseByteArrayMethodRef import app.morphe.patches.shared.misc.settings.preference.ListPreference @@ -28,6 +29,8 @@ import app.morphe.patches.youtube.shared.VideoQualityChangedFingerprint import app.morphe.patches.youtube.shared.hookVideoIntent import app.morphe.patches.youtube.shared.openVideoIntentPatch import app.morphe.patches.youtube.video.information.EXTENSION_VIDEO_QUALITY_INTERFACE +import app.morphe.patches.youtube.video.information.EXTENSION_VIDEO_QUALITY_MENU_INTERFACE +import app.morphe.patches.youtube.video.information.SetVideoQualityFingerprint import app.morphe.patches.youtube.video.information.videoInformationPatch import app.morphe.util.addInstructionsAtControlFlowLabel import app.morphe.util.findFieldFromToString @@ -35,11 +38,13 @@ import app.morphe.patcher.util.findFreeRegister import app.morphe.util.findInstructionIndicesReversedOrThrow import app.morphe.patcher.util.getFreeRegisterProvider import app.morphe.patcher.util.getReference +import app.morphe.patcher.util.matchSingle import app.morphe.util.indexOfFirstInstructionReversedOrThrow import app.morphe.util.insertLiteralOverride import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation +import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction import com.android.tools.smali.dexlib2.iface.reference.FieldReference @@ -270,10 +275,49 @@ val rememberVideoQualityPatch = bytecodePatch { hookVideoIntent(EXTENSION_CLASS, detectVideo = true, detectShorts = true) // Inject a call to remember the selected quality for Shorts. - VideoQualityItemOnClickFingerprint.method.addInstruction( - 0, - "invoke-static { p3 }, $EXTENSION_CLASS->userChangedShortsQuality(I)V" - ) + VideoQualityItemOnClickFingerprint.method.apply { + addInstruction( + 0, + "invoke-static { p3 }, $EXTENSION_CLASS->userChangedShortsQuality(I)V" + ) + + findInstructionIndicesReversedOrThrow(Opcode.RETURN_VOID).forEach { index -> + addInstructionsAtControlFlowLabel( + index, + "invoke-static { }, $EXTENSION_CLASS->clearShortsMenuSelectedQuality()V" + ) + } + } + + // The Shorts quality menu sets the selected quality by its resolution, which YouTube can get + // wrong, such as 720 for the 480p quality of some Shorts, so another quality is played. + // The quality selected is set instead, as the quality menu of regular videos does. + val setResolutionMethod = getSetVideoQualityResolutionFingerprint( + SetVideoQualityFingerprint.instructionMatches[1].getFieldAccessed().type + ).matchSingle().method + + getVideoQualityItemSetResolutionFingerprint(setResolutionMethod.toString()).matchAll().forEach { + it.method.apply { + val index = it.instructionMatches.first().index + val setResolutionInstruction = getInstruction(index) + val menuRegister = setResolutionInstruction.registerC + val qualitySetRegister = findFreeRegister( + index, + menuRegister, + setResolutionInstruction.registerD + ) + + addInstructionsAtControlFlowLabel( + index, + """ + invoke-static { v$menuRegister }, $EXTENSION_CLASS->setShortsMenuSelectedQuality($EXTENSION_VIDEO_QUALITY_MENU_INTERFACE)Z + move-result v$qualitySetRegister + if-nez v$qualitySetRegister, :quality_set + """, + ExternalLabel("quality_set", getInstruction(index + 1)) + ) + } + } // Inject a call to remember the user selected quality for regular videos. // The quality object is used, as its name is needed to get the fixed resolution. From 4764483db626de5974ef2d3d3cf19d84ef4cfde1 Mon Sep 17 00:00:00 2001 From: 0xrxL <127248639+0xrxL@users.noreply.github.com> Date: Sun, 25 Oct 2026 18:07:38 +0100 Subject: [PATCH 062/189] fix(YouTube - Litho filter): Filtered elements leave empty cells in the tablet layout grids (#4035) --- .../patches/components/ContextInterface.java | 11 ++ .../patches/components/LithoFilterPatch.java | 124 +++++++++++++++++- .../youtube/patches/FeedCardLayoutPatch.java | 7 +- .../misc/litho/filter/LithoFilterPatch.kt | 3 +- .../shared/misc/litho/filter/Fingerprints.kt | 49 +++++++ .../misc/litho/filter/LithoFilterPatch.kt | 97 ++++++++++++++ .../hide/general/HideLayoutComponentsPatch.kt | 12 +- .../misc/litho/filter/LithoFilterPatch.kt | 3 +- 8 files changed, 300 insertions(+), 6 deletions(-) diff --git a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/patches/components/ContextInterface.java b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/patches/components/ContextInterface.java index 957543c42f..e7f67cd659 100644 --- a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/patches/components/ContextInterface.java +++ b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/patches/components/ContextInterface.java @@ -10,6 +10,8 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import java.util.List; + /** * Interface to use obfuscated methods. */ @@ -23,6 +25,15 @@ public interface ContextInterface { @Nullable Object patch_getHorizontalCollectionSwipeProtector(); + /** + * @return The decorators of the command event data. One of them holds the item of the list, + * such as the feed, that the element belongs to. Null if the app is not patched for it. + */ + @Nullable + default List patch_getCommandEventDataDecorators() { + return null; + } + default boolean isHomeFeedOrRelatedVideo() { return patch_getHorizontalCollectionSwipeProtector() == null; } diff --git a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/patches/components/LithoFilterPatch.java b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/patches/components/LithoFilterPatch.java index 8f26827648..b6fd8df40b 100644 --- a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/patches/components/LithoFilterPatch.java +++ b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/patches/components/LithoFilterPatch.java @@ -13,11 +13,15 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import java.lang.ref.WeakReference; import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Iterator; import java.util.List; import app.morphe.extension.shared.Logger; import app.morphe.extension.shared.StringTrieSearch; +import app.morphe.extension.shared.Utils; import app.morphe.extension.shared.settings.BaseSettings; import app.morphe.extension.shared.settings.SharedYouTubeSettings; @@ -56,6 +60,14 @@ public String toString() { } } + /** + * Interface added to the decorator that holds the item of a list, such as the feed. + */ + public interface ListItemDecoratorInterface { + @Nullable + Object patch_getListItem(); + } + /** * Placeholder for actual filters. */ @@ -90,6 +102,17 @@ private static final class DummyFilter extends Filter { } */ private static final ThreadLocal bufferThreadLocal = new ThreadLocal<>(); + /** + * Observable lists that hold the items of the lists, such as the feed. + * The lists use the equality of their content, so they are kept by reference instead of in a set. + */ + private static final List>> observableLists = new ArrayList<>(); + + /** + * Conversion context of the element that the calling thread is parsing. + */ + private static final ThreadLocal> parsedElementContext = new ThreadLocal<>(); + private static final StringTrieSearch contextSearchTree = new StringTrieSearch(); private static final StringTrieSearch pathSearchTree = new StringTrieSearch(); private static final StringTrieSearch identifierSearchTree = new StringTrieSearch(); @@ -198,8 +221,19 @@ public static boolean isFiltered(ContextInterface contextInterface, @Nullable by buffer, new BufferAsciiStrings(buffer)); Logger.printDebug(() -> "Searching " + parameter); - return identifierSearchTree.matches(identifier, parameter) - || pathSearchTree.matches(pathBuilder, parameter); + if (identifierSearchTree.matches(identifier, parameter) + || pathSearchTree.matches(pathBuilder, parameter)) { + // The root component of the element, such as 'identifier|CellType|', + // means that the whole element is hidden. + final int identifierLength = identifier.length(); + if (pathBuilder.length() > identifierLength + 1 + && pathBuilder.charAt(identifierLength) == '|' + && pathBuilder.indexOf("|", identifierLength + 1) == pathBuilder.length() - 1 + && pathBuilder.indexOf(identifier) == 0) { + removeListItem(contextInterface); + } + return true; + } } catch (Exception ex) { Logger.printException(() -> "isFiltered failure", ex); } @@ -207,6 +241,92 @@ public static boolean isFiltered(ContextInterface contextInterface, @Nullable by return false; } + /** + * Injection point. + * Called when an observable list, which can hold the items of a list such as the feed, is created. + */ + public static void addObservableList(List list) { + synchronized (observableLists) { + // Remove the lists that no longer exist. + observableLists.removeIf(reference -> reference.get() == null); + observableLists.add(new WeakReference<>(list)); + } + } + + /** + * Injection point. + * Called when the parsing of an element starts. + */ + public static void setParsedElementContext(@Nullable ContextInterface contextInterface) { + parsedElementContext.set(contextInterface == null ? null : new WeakReference<>(contextInterface)); + } + + /** + * Injection point. + * Called when the element that is being parsed is hidden. + */ + public static void removeParsedElementListItem() { + WeakReference reference = parsedElementContext.get(); + ContextInterface contextInterface = reference == null ? null : reference.get(); + if (contextInterface != null) { + removeListItem(contextInterface); + } + } + + /** + * Called when the whole element is hidden. + *

+ * Hiding an element replaces it with an empty component, but the item of the element stays + * in its list. The grids, such as the feed of the tablet layout, then show an empty cell + * in its place. To prevent this, the item is removed from the list. + */ + private static void removeListItem(ContextInterface contextInterface) { + try { + List decorators = contextInterface.patch_getCommandEventDataDecorators(); + if (decorators == null) { + return; + } + + for (Object decorator : decorators) { + if (!(decorator instanceof ListItemDecoratorInterface listItemDecorator)) { + continue; + } + + Object item = listItemDecorator.patch_getListItem(); + if (item == null) { + continue; + } + + // The lists can only be changed on the main thread. + Utils.runOnMainThread(() -> { + try { + synchronized (observableLists) { + Iterator>> iterator = observableLists.iterator(); + while (iterator.hasNext()) { + List list = iterator.next().get(); + if (list == null) { + iterator.remove(); + continue; + } + + final int index = list.indexOf(item); + if (index >= 0) { + list.remove(index); + Logger.printDebug(() -> "Removed list item of filtered element: " + item); + return; + } + } + } + } catch (Exception ex) { + Logger.printException(() -> "removeListItem failure", ex); + } + }); + } + } catch (Exception ex) { + Logger.printException(() -> "removeListItem failure", ex); + } + } + private static boolean useLegacyLithoFiltering() { return false; // Method is modified during patching. } diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FeedCardLayoutPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FeedCardLayoutPatch.java index 7a66a7072a..b4bea67390 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FeedCardLayoutPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FeedCardLayoutPatch.java @@ -127,6 +127,7 @@ import app.morphe.extension.youtube.innertube.FeedCardLayoutOuterClass.VideoThumbnail; import app.morphe.extension.youtube.innertube.FeedCardLayoutOuterClass.VideoWithContextData; import app.morphe.extension.youtube.innertube.FeedCardLayoutOuterClass.VideoWithContextModel; +import app.morphe.extension.youtube.patches.components.LayoutComponentsFilter; import app.morphe.extension.youtube.patches.theme.SeekbarColorPatch; import app.morphe.extension.youtube.patches.utils.FlyoutUtils; import app.morphe.extension.youtube.patches.utils.ProtoNode; @@ -612,11 +613,15 @@ public static byte[] setFeedCardLayout(byte[] bytes) { break toCard; } - // The filters find the hidden Shorts and live streams by the classic layout. + // The filters find the hidden Shorts, live streams and mix playlists by the classic layout. if (thumbnail.getBadgeStyle() == THUMBNAIL_BADGE_STYLE_LIVE && Settings.HIDE_LIVE_STREAMS.get()) { break toCard; } + // The mix playlists are found by the url of the mix, which the cards don't have. + if (LayoutComponentsFilter.filterMixPlaylists(bytes)) { + break toCard; + } if ((Settings.HIDE_SHORTS_HOME.get() || Settings.HIDE_SHORTS_SUBSCRIPTIONS.get() || Settings.HIDE_SHORTS_SEARCH.get() || Settings.HIDE_SHORTS_HISTORY.get()) && shortsThumbnailSearch.matches(thumbnail.getImage().toByteArray())) { diff --git a/patches/src/main/kotlin/app/morphe/patches/music/misc/litho/filter/LithoFilterPatch.kt b/patches/src/main/kotlin/app/morphe/patches/music/misc/litho/filter/LithoFilterPatch.kt index 8f9df13b29..2339b01c49 100644 --- a/patches/src/main/kotlin/app/morphe/patches/music/misc/litho/filter/LithoFilterPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/music/misc/litho/filter/LithoFilterPatch.kt @@ -18,7 +18,8 @@ val lithoFilterPatch = sharedLithoFilterPatch( hookNonNativeBuffer = { false }, // YT Music does not ship the Upb feature flag. overrideUpbFeatureFlag = { false }, - useLegacyLithoFiltering = { false } + useLegacyLithoFiltering = { false }, + removeFilteredListItems = false ) { dependsOn( sharedExtensionPatch, diff --git a/patches/src/main/kotlin/app/morphe/patches/shared/misc/litho/filter/Fingerprints.kt b/patches/src/main/kotlin/app/morphe/patches/shared/misc/litho/filter/Fingerprints.kt index fa9f17ba0b..b44f57151b 100644 --- a/patches/src/main/kotlin/app/morphe/patches/shared/misc/litho/filter/Fingerprints.kt +++ b/patches/src/main/kotlin/app/morphe/patches/shared/misc/litho/filter/Fingerprints.kt @@ -19,6 +19,7 @@ import app.morphe.patcher.methodCall import app.morphe.patcher.string import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.reference.FieldReference internal object AccessibilityIdFingerprint : Fingerprint( filters = listOf( @@ -108,3 +109,51 @@ internal object LithoConverterBufferUpbFeatureFlagFingerprint : Fingerprint( literal(45419603L) ) ) + +/** + * Creates the decorator of the command event data of an item of a list, such as the feed. + * The decorator holds the data, and the item is the first parameter of the data. + */ +internal object CommandEventDataDecoratorFingerprint : Fingerprint( + accessFlags = listOf(AccessFlags.PUBLIC, AccessFlags.STATIC), + returnType = "L", + parameters = listOf("Ljava/lang/Object;", "L", "Ljava/util/Map;"), + filters = listOf( + methodCall( + name = "", + parameters = listOf("Ljava/lang/Object;", "L", "L", "L", "Ljava/util/List;") + ), + methodCall( + opcode = Opcode.INVOKE_DIRECT, + name = "", + parameters = listOf("Ljava/util/Map;", "L"), + location = MatchAfterWithin(2) + ) + ) +) + +/** + * Gets the decorators of the command event data of the conversion context. + * They are in the elements config if there is one, otherwise in the field. + */ +internal fun commandEventDataDecoratorsFingerprint(decoratorsField: FieldReference) = Fingerprint( + definingClass = decoratorsField.definingClass, + returnType = decoratorsField.type, + parameters = listOf(), + filters = listOf( + fieldAccess( + opcode = Opcode.IGET_OBJECT, + reference = decoratorsField + ) + ) +) + +/** + * Constructor of the observable list that holds the items of the lists, such as the feed. + */ +internal object ObservableListConstructorFingerprint : Fingerprint( + superclass = "Ljava/util/AbstractList;", + name = "", + accessFlags = listOf(AccessFlags.PROTECTED, AccessFlags.CONSTRUCTOR), + parameters = listOf("Ljava/util/List;", "L") +) diff --git a/patches/src/main/kotlin/app/morphe/patches/shared/misc/litho/filter/LithoFilterPatch.kt b/patches/src/main/kotlin/app/morphe/patches/shared/misc/litho/filter/LithoFilterPatch.kt index d839719e05..7d9470e034 100644 --- a/patches/src/main/kotlin/app/morphe/patches/shared/misc/litho/filter/LithoFilterPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/shared/misc/litho/filter/LithoFilterPatch.kt @@ -17,6 +17,7 @@ import app.morphe.patcher.InstructionLocation.MatchAfterWithin import app.morphe.patcher.checkCast import app.morphe.patcher.extensions.InstructionExtensions.addInstruction import app.morphe.patcher.extensions.InstructionExtensions.addInstructions +import app.morphe.patcher.extensions.InstructionExtensions.addInstructionsWithLabels import app.morphe.patcher.extensions.InstructionExtensions.getInstruction import app.morphe.patcher.methodCall import app.morphe.patcher.opcode @@ -29,8 +30,11 @@ import app.morphe.patcher.util.getReference import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable import app.morphe.patcher.util.returnEarly +import app.morphe.patches.shared.misc.litho.context.ConversionContextToStringFingerprint import app.morphe.patches.shared.misc.litho.context.EXTENSION_CONTEXT_INTERFACE +import app.morphe.patches.shared.misc.litho.context.conversionContextClassDef import app.morphe.util.addInstructionsAtControlFlowLabel +import app.morphe.util.findFieldFromToString import app.morphe.util.insertLiteralOverride import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode @@ -44,6 +48,11 @@ internal const val EXTENSION_CLASS = "Lapp/morphe/extension/shared/patches/compo internal const val EXTENSION_FILTER = "[Lapp/morphe/extension/shared/patches/components/Filter;" +private const val EXTENSION_LIST_ITEM_DECORATOR_INTERFACE = + $$"Lapp/morphe/extension/shared/patches/components/LithoFilterPatch$ListItemDecoratorInterface;" + +private const val COMMAND_EVENT_DATA_DECORATORS_PROPERTY = ", globalCommandEventDataDecoratorsInternal=" + // Registers used in extension helperMethod. private const val REGISTER_FILTER_CLASS = 0 private const val REGISTER_FILTER_COUNT = 1 @@ -108,6 +117,9 @@ fun addLithoFilter(classDescriptor: String) { * Evaluated lazily inside execute so version flags are already set. * @param overrideUpbFeatureFlag Whether to override the A/B feature flag that enables Upb-native * protobuf parsing (only present on older YouTube; absent in Music). + * @param removeFilteredListItems Whether to remove the items of the lists (such as the feed) whose + * whole element is filtered. Otherwise the grids, such as the feed + * of the tablet layout, show an empty cell in their place. * @param block Callback to add app-specific dependencies (sharedExtensionPatch, conversionContextPatch, * versionCheckPatch, and any app-specific fixes). */ @@ -115,6 +127,7 @@ internal fun sharedLithoFilterPatch( hookNonNativeBuffer: () -> Boolean, overrideUpbFeatureFlag: () -> Boolean, useLegacyLithoFiltering: () -> Boolean, + removeFilteredListItems: Boolean, block: BytecodePatchBuilder.() -> Unit, ): BytecodePatch = bytecodePatch( description = "Hooks the method which parses the bytes into a ComponentContext to filter components." @@ -313,6 +326,90 @@ internal fun sharedLithoFilterPatch( // endregion + // region Remove the items of the lists whose whole element is filtered. + + if (removeFilteredListItems) { + // Give the extension the decorators of the conversion context. + val decoratorsMethod = commandEventDataDecoratorsFingerprint( + ConversionContextToStringFingerprint.method + .findFieldFromToString(COMMAND_EVENT_DATA_DECORATORS_PROPERTY) + ).method + + conversionContextClassDef.methods.add( + ImmutableMethod( + conversionContextClassDef.type, + "patch_getCommandEventDataDecorators", + listOf(), + "Ljava/util/List;", + AccessFlags.PUBLIC.value or AccessFlags.FINAL.value, + null, + null, + MutableMethodImplementation(2), + ).toMutable().apply { + addInstructions( + 0, + """ + invoke-virtual { p0 }, $decoratorsMethod + move-result-object v0 + return-object v0 + """ + ) + } + ) + + // Give the extension the item that the decorator holds. + CommandEventDataDecoratorFingerprint.let { + val dataType = it.instructionMatches.first() + .instruction.getReference()!!.definingClass + val itemField = mutableClassDefBy(dataType).fields.single { field -> + field.type == "Ljava/lang/Object;" + } + + mutableClassDefBy( + it.instructionMatches.last().instruction.getReference()!!.definingClass + ).apply { + val dataField = fields.single { field -> field.type == dataType } + + interfaces.add(EXTENSION_LIST_ITEM_DECORATOR_INTERFACE) + + methods.add( + ImmutableMethod( + type, + "patch_getListItem", + listOf(), + "Ljava/lang/Object;", + AccessFlags.PUBLIC.value or AccessFlags.FINAL.value, + null, + null, + MutableMethodImplementation(2), + ).toMutable().apply { + addInstructionsWithLabels( + 0, + """ + iget-object v0, p0, $dataField + if-eqz v0, :no_data + iget-object v0, v0, $itemField + :no_data + return-object v0 + """ + ) + } + ) + } + } + + // Keep the observable lists, to remove the items from them. + ObservableListConstructorFingerprint.method.apply { + addInstruction( + implementation!!.instructions.lastIndex, + "invoke-static { p0 }, $EXTENSION_CLASS->addObservableList(Ljava/util/List;)V" + ) + } + } + + // endregion + + // region Change Litho thread executor to 1 thread to fix layout issue in unpatched YouTube. LithoThreadExecutorFingerprint.method.addInstructions( diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/general/HideLayoutComponentsPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/general/HideLayoutComponentsPatch.kt index 5c25caa2c1..9684ac0d86 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/general/HideLayoutComponentsPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/general/HideLayoutComponentsPatch.kt @@ -25,6 +25,8 @@ import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMuta import app.morphe.patcher.util.registersUsed import app.morphe.patcher.util.smali.ExternalLabel import app.morphe.patches.shared.misc.fix.proto.fixProtoLibraryPatch +import app.morphe.patches.shared.misc.litho.context.EXTENSION_CONTEXT_INTERFACE +import app.morphe.patches.shared.misc.litho.filter.EXTENSION_CLASS as LITHO_FILTER_EXTENSION_CLASS import app.morphe.patches.shared.misc.litho.filter.addLithoFilter import app.morphe.patches.shared.misc.litho.node.hookTreeNodeResult import app.morphe.patches.shared.misc.proto.hookElement @@ -425,8 +427,10 @@ val hideLayoutComponentsPatch = bytecodePatch( insertIndex, """ invoke-static { $byteArrayParameter }, $LAYOUT_COMPONENTS_FILTER->filterMixPlaylists([B)Z - move-result v$freeRegister + move-result v$freeRegister if-eqz v$freeRegister, :show + # The whole element is hidden, so its item is removed from the list (such as the feed). + invoke-static { }, $LITHO_FILTER_EXTENSION_CLASS->removeParsedElementListItem()V move-object v$returnEmptyComponentRegister, p1 # Required for 19.47 goto :return_empty_component :show @@ -434,6 +438,12 @@ val hideLayoutComponentsPatch = bytecodePatch( """, ExternalLabel("return_empty_component", returnEmptyComponentInstruction), ) + + // The conversion context parameter is reused before the filter is called. + addInstruction( + 0, + "invoke-static/range { p2 .. p2 }, $LITHO_FILTER_EXTENSION_CLASS->setParsedElementContext($EXTENSION_CONTEXT_INTERFACE)V" + ) } } diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/misc/litho/filter/LithoFilterPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/litho/filter/LithoFilterPatch.kt index 08baa0a316..84766896dd 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/misc/litho/filter/LithoFilterPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/litho/filter/LithoFilterPatch.kt @@ -24,7 +24,8 @@ val lithoFilterPatch = sharedLithoFilterPatch( hookNonNativeBuffer = { !is_20_22_or_greater }, // Flag was removed in 21.15+. overrideUpbFeatureFlag = { !is_21_15_or_greater }, - useLegacyLithoFiltering = { !is_20_22_or_greater } + useLegacyLithoFiltering = { !is_20_22_or_greater }, + removeFilteredListItems = true ) { dependsOn( sharedExtensionPatch, From 3b3308b61740ab01e35aaa0e97166eeab80164e6 Mon Sep 17 00:00:00 2001 From: 0xrxL <127248639+0xrxL@users.noreply.github.com> Date: Sun, 25 Oct 2026 18:09:34 +0100 Subject: [PATCH 063/189] fix(YouTube - Search comments): Search button is not shown in the Shorts comments and is shown in other panels --- .../components/CommentsSearchFilter.java | 53 ++++++++----------- .../searchcomments/Fingerprints.kt | 1 + .../searchcomments/SearchCommentsPatch.kt | 36 +++++-------- 3 files changed, 36 insertions(+), 54 deletions(-) diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/components/CommentsSearchFilter.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/components/CommentsSearchFilter.java index 4939efdeca..cd2bd6e362 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/components/CommentsSearchFilter.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/components/CommentsSearchFilter.java @@ -81,6 +81,17 @@ public interface SectionListAccess { private static final String SEARCH_BUTTON_TAG = "morphe_search_comments_button"; + /** + * The comments panels of videos and Shorts. Newer app versions use the first id for videos. + * Other panels with "comment" in their id are not searched, such as the replies of a comment, + * which open in a panel of their own. + */ + private static final List COMMENTS_PANEL_IDS = List.of( + "comment-item-section", + "engagement-panel-comments-section", + "shorts-comments-panel" + ); + private static final int ID_CLOSE_BUTTON = ResourceUtils.getIdentifier(ResourceType.ID, "close_button"); @@ -431,25 +442,29 @@ private static SectionListAccess findPanelList(View view) { /** * Injection point. - * Called on the main thread when the header of an engagement panel is shown. + * Called on the main thread when the header of an engagement panel is set up. + * Panels reuse their header, so the search button is shown or hidden for every panel, + * also for the panels with no subheader, such as the comments of Shorts and the description. + * + * @param panel The layout of the panel, which holds its header. */ - public static void onPanelSubheaderShown(View subheader) { - if (subheader == null || !Settings.SEARCH_COMMENTS.get() || ID_CLOSE_BUTTON == 0) { + public static void onPanelHeaderSetUp(View panel) { + if (panel == null || !Settings.SEARCH_COMMENTS.get() || ID_CLOSE_BUTTON == 0) { return; } // The panel is opened after its header is set up. - subheader.post(() -> { + panel.post(() -> { try { - updateSearchButton(subheader); + updateSearchButton(panel); } catch (Exception ex) { Logger.printException(() -> "updateSearchButton failure", ex); } }); } - private static void updateSearchButton(View subheader) { - View closeButton = findCloseButton(subheader); + private static void updateSearchButton(View panel) { + View closeButton = panel.findViewById(ID_CLOSE_BUTTON); if (closeButton == null || !(closeButton.getParent() instanceof ViewGroup header)) { Logger.printDebug(() -> "Panel close button not found"); return; @@ -463,35 +478,13 @@ private static void updateSearchButton(View subheader) { } } - final boolean isCommentsPanel = isCommentsPanelOpen(); + final boolean isCommentsPanel = COMMENTS_PANEL_IDS.contains(EngagementPanel.getLastOpenedPanelId()); searchButton.setVisibility(isCommentsPanel ? View.VISIBLE : View.GONE); if (isCommentsPanel) { updateSearchButtonIcon(searchButton); } } - /** - * The header of the panel holds the close button, and the subheader is below it. - */ - @Nullable - private static View findCloseButton(View subheader) { - ViewParent parent = subheader.getParent(); - while (parent instanceof ViewGroup group) { - View closeButton = group.findViewById(ID_CLOSE_BUTTON); - if (closeButton != null) { - return closeButton; - } - parent = group.getParent(); - } - return null; - } - - private static boolean isCommentsPanelOpen() { - String panelId = EngagementPanel.getLastOpenedPanelId(); - // The comments of videos and Shorts, and the replies of a comment. - return panelId != null && panelId.contains("comment"); - } - @Nullable private static View createSearchButton(ViewGroup header, View closeButton) { Context context = header.getContext(); diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/searchcomments/Fingerprints.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/searchcomments/Fingerprints.kt index 1bb1c02c19..26483c431f 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/searchcomments/Fingerprints.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/searchcomments/Fingerprints.kt @@ -22,6 +22,7 @@ import com.android.tools.smali.dexlib2.Opcode /** * Sets up the header of an engagement panel, such as the comments panel. + * The first parameter is the layout of the panel, which holds its header. */ internal object PanelSubheaderFingerprint : Fingerprint( accessFlags = listOf(AccessFlags.PUBLIC, AccessFlags.STATIC), diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/searchcomments/SearchCommentsPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/searchcomments/SearchCommentsPatch.kt index c9109877ff..2ba1c640d7 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/searchcomments/SearchCommentsPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/searchcomments/SearchCommentsPatch.kt @@ -8,10 +8,9 @@ package app.morphe.patches.youtube.interaction.searchcomments import app.morphe.patcher.extensions.InstructionExtensions.addInstruction -import app.morphe.patcher.extensions.InstructionExtensions.getInstruction import app.morphe.patcher.patch.bytecodePatch import app.morphe.patcher.patch.resourcePatch -import app.morphe.patcher.util.getReference +import app.morphe.patcher.util.cloneParameters import app.morphe.patches.all.misc.resources.addResourcesPatch import app.morphe.patches.shared.misc.bridge.addBridge import app.morphe.patches.shared.misc.litho.filter.addLithoFilter @@ -24,12 +23,10 @@ import app.morphe.patches.youtube.misc.settings.settingsPatch import app.morphe.patches.youtube.shared.Constants.COMPATIBILITY_YOUTUBE import app.morphe.patches.youtube.video.information.videoInformationPatch import app.morphe.util.ResourceGroup +import app.morphe.util.addInstructionsAtControlFlowLabel import app.morphe.util.copyResources -import app.morphe.util.indexOfFirstInstructionOrThrow -import app.morphe.util.indexOfFirstInstructionReversedOrThrow +import app.morphe.util.findInstructionIndicesReversedOrThrow import com.android.tools.smali.dexlib2.Opcode -import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction -import com.android.tools.smali.dexlib2.iface.reference.MethodReference private const val EXTENSION_FILTER = "Lapp/morphe/extension/youtube/patches/components/CommentsSearchFilter;" @@ -68,24 +65,15 @@ val searchCommentsPatch = bytecodePatch( addLithoFilter(EXTENSION_FILTER) - // The search button is added next to the close button of the comments panel. - PanelSubheaderFingerprint.let { - it.method.apply { - val removeAllViewsIndex = indexOfFirstInstructionReversedOrThrow { - opcode == Opcode.INVOKE_VIRTUAL && - getReference()?.name == "removeAllViews" - } - - val setVisibilityIndex = indexOfFirstInstructionOrThrow(removeAllViewsIndex) { - opcode == Opcode.INVOKE_VIRTUAL && - getReference()?.name == "setVisibility" - } - - val subheaderRegister = getInstruction(setVisibilityIndex).registerC - - addInstruction( - setVisibilityIndex + 1, - "invoke-static { v$subheaderRegister }, $EXTENSION_FILTER->onPanelSubheaderShown(Landroid/view/View;)V" + // The search button is added next to the close button of the panel header. The header + // of a panel with no subheader, such as the comments of Shorts, is set up without + // reaching the end of the method, so every return is hooked. Panels reuse their header, + // so the button is shown or hidden for every panel. + PanelSubheaderFingerprint.method.cloneParameters().apply { + findInstructionIndicesReversedOrThrow(Opcode.RETURN_VOID).forEach { index -> + addInstructionsAtControlFlowLabel( + index, + "invoke-static/range { p0 .. p0 }, $EXTENSION_FILTER->onPanelHeaderSetUp(Landroid/view/View;)V" ) } } From ed3b48f98613006734c358af92a50b977024e5bc Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Sun, 25 Oct 2026 17:10:06 +0000 Subject: [PATCH 064/189] chore(YouTube): Cleanup code --- .../patches/components/LithoFilterPatch.java | 96 ++++++++----------- .../misc/litho/filter/LithoFilterPatch.kt | 2 +- .../hide/general/HideLayoutComponentsPatch.kt | 33 +++---- 3 files changed, 54 insertions(+), 77 deletions(-) diff --git a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/patches/components/LithoFilterPatch.java b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/patches/components/LithoFilterPatch.java index b6fd8df40b..9db4b22b9e 100644 --- a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/patches/components/LithoFilterPatch.java +++ b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/patches/components/LithoFilterPatch.java @@ -108,11 +108,6 @@ private static final class DummyFilter extends Filter { } */ private static final List>> observableLists = new ArrayList<>(); - /** - * Conversion context of the element that the calling thread is parsing. - */ - private static final ThreadLocal> parsedElementContext = new ThreadLocal<>(); - private static final StringTrieSearch contextSearchTree = new StringTrieSearch(); private static final StringTrieSearch pathSearchTree = new StringTrieSearch(); private static final StringTrieSearch identifierSearchTree = new StringTrieSearch(); @@ -229,8 +224,8 @@ public static boolean isFiltered(ContextInterface contextInterface, @Nullable by if (pathBuilder.length() > identifierLength + 1 && pathBuilder.charAt(identifierLength) == '|' && pathBuilder.indexOf("|", identifierLength + 1) == pathBuilder.length() - 1 - && pathBuilder.indexOf(identifier) == 0) { - removeListItem(contextInterface); + && Utils.startsWith(pathBuilder, identifier)) { + removeFilteredElementListItem(contextInterface); } return true; } @@ -255,75 +250,62 @@ public static void addObservableList(List list) { /** * Injection point. - * Called when the parsing of an element starts. - */ - public static void setParsedElementContext(@Nullable ContextInterface contextInterface) { - parsedElementContext.set(contextInterface == null ? null : new WeakReference<>(contextInterface)); - } - - /** - * Injection point. - * Called when the element that is being parsed is hidden. - */ - public static void removeParsedElementListItem() { - WeakReference reference = parsedElementContext.get(); - ContextInterface contextInterface = reference == null ? null : reference.get(); - if (contextInterface != null) { - removeListItem(contextInterface); - } - } - - /** * Called when the whole element is hidden. *

* Hiding an element replaces it with an empty component, but the item of the element stays * in its list. The grids, such as the feed of the tablet layout, then show an empty cell * in its place. To prevent this, the item is removed from the list. */ - private static void removeListItem(ContextInterface contextInterface) { + public static void removeFilteredElementListItem(@Nullable ContextInterface contextInterface) { try { + if (contextInterface == null) { + return; + } + List decorators = contextInterface.patch_getCommandEventDataDecorators(); if (decorators == null) { return; } for (Object decorator : decorators) { - if (!(decorator instanceof ListItemDecoratorInterface listItemDecorator)) { - continue; + if (decorator instanceof ListItemDecoratorInterface listItemDecorator) { + Object item = listItemDecorator.patch_getListItem(); + if (item != null) { + // The lists can only be changed on the main thread. + Utils.runOnMainThread(() -> removeObservableListItem(item)); + } } + } + } catch (Exception ex) { + Logger.printException(() -> "removeFilteredElementListItem failure", ex); + } + } - Object item = listItemDecorator.patch_getListItem(); - if (item == null) { + private static void removeObservableListItem(Object item) { + List itemList = null; + int itemIndex = -1; + + synchronized (observableLists) { + Iterator>> iterator = observableLists.iterator(); + while (iterator.hasNext()) { + List list = iterator.next().get(); + if (list == null) { + iterator.remove(); continue; } - // The lists can only be changed on the main thread. - Utils.runOnMainThread(() -> { - try { - synchronized (observableLists) { - Iterator>> iterator = observableLists.iterator(); - while (iterator.hasNext()) { - List list = iterator.next().get(); - if (list == null) { - iterator.remove(); - continue; - } - - final int index = list.indexOf(item); - if (index >= 0) { - list.remove(index); - Logger.printDebug(() -> "Removed list item of filtered element: " + item); - return; - } - } - } - } catch (Exception ex) { - Logger.printException(() -> "removeListItem failure", ex); - } - }); + itemIndex = list.indexOf(item); + if (itemIndex >= 0) { + itemList = list; + break; + } } - } catch (Exception ex) { - Logger.printException(() -> "removeListItem failure", ex); + } + + // Removing the item notifies the list observers, so it is done outside the lock. + if (itemList != null) { + itemList.remove(itemIndex); + Logger.printDebug(() -> "Removed list item of filtered element: " + item); } } diff --git a/patches/src/main/kotlin/app/morphe/patches/shared/misc/litho/filter/LithoFilterPatch.kt b/patches/src/main/kotlin/app/morphe/patches/shared/misc/litho/filter/LithoFilterPatch.kt index 7d9470e034..3829ef5c68 100644 --- a/patches/src/main/kotlin/app/morphe/patches/shared/misc/litho/filter/LithoFilterPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/shared/misc/litho/filter/LithoFilterPatch.kt @@ -361,7 +361,7 @@ internal fun sharedLithoFilterPatch( CommandEventDataDecoratorFingerprint.let { val dataType = it.instructionMatches.first() .instruction.getReference()!!.definingClass - val itemField = mutableClassDefBy(dataType).fields.single { field -> + val itemField = classDefBy(dataType).fields.single { field -> field.type == "Ljava/lang/Object;" } diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/general/HideLayoutComponentsPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/general/HideLayoutComponentsPatch.kt index 9684ac0d86..0721c5df4f 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/general/HideLayoutComponentsPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/general/HideLayoutComponentsPatch.kt @@ -19,14 +19,14 @@ import app.morphe.patcher.extensions.InstructionExtensions.removeInstruction import app.morphe.patcher.extensions.InstructionExtensions.replaceInstruction import app.morphe.patcher.methodCall import app.morphe.patcher.patch.bytecodePatch -import app.morphe.patcher.util.findFreeRegister +import app.morphe.patcher.util.cloneParameters import app.morphe.patcher.util.getReference +import app.morphe.patcher.util.numberOfParameterRegistersLogical import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable import app.morphe.patcher.util.registersUsed import app.morphe.patcher.util.smali.ExternalLabel import app.morphe.patches.shared.misc.fix.proto.fixProtoLibraryPatch import app.morphe.patches.shared.misc.litho.context.EXTENSION_CONTEXT_INTERFACE -import app.morphe.patches.shared.misc.litho.filter.EXTENSION_CLASS as LITHO_FILTER_EXTENSION_CLASS import app.morphe.patches.shared.misc.litho.filter.addLithoFilter import app.morphe.patches.shared.misc.litho.node.hookTreeNodeResult import app.morphe.patches.shared.misc.proto.hookElement @@ -84,6 +84,7 @@ import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction import com.android.tools.smali.dexlib2.iface.reference.FieldReference import com.android.tools.smali.dexlib2.immutable.ImmutableMethod import com.android.tools.smali.dexlib2.immutable.ImmutableMethodParameter +import app.morphe.patches.shared.misc.litho.filter.EXTENSION_CLASS as LITHO_FILTER_EXTENSION_CLASS private const val LAYOUT_COMPONENTS_FILTER = "Lapp/morphe/extension/youtube/patches/components/LayoutComponentsFilter;" @@ -414,36 +415,30 @@ val hideLayoutComponentsPatch = bytecodePatch( // region hide mix playlists ParseElementFromBufferFingerprint.let { - it.method.apply { - val insertIndex = it.instructionMatches.first().index + // The conversion context parameter is reused before the filter is called. + it.method.cloneParameters().apply { + // Cloning adds move instructions to the start of the method. + val insertIndex = it.instructionMatches.first().index + numberOfParameterRegistersLogical - val byteArrayParameter = "p3" - val returnEmptyComponentIndex = it.instructionMatches[4].index + val returnEmptyComponentIndex = it.instructionMatches[4].index + numberOfParameterRegistersLogical val returnEmptyComponentInstruction = getInstruction(returnEmptyComponentIndex) val returnEmptyComponentRegister = (returnEmptyComponentInstruction as FiveRegisterInstruction).registerC - val freeRegister = findFreeRegister(insertIndex, returnEmptyComponentRegister) + // p2 is the conversion context and p3 is the byte array. p4 is free after cloning. addInstructionsAtControlFlowLabel( insertIndex, """ - invoke-static { $byteArrayParameter }, $LAYOUT_COMPONENTS_FILTER->filterMixPlaylists([B)Z - move-result v$freeRegister - if-eqz v$freeRegister, :show - # The whole element is hidden, so its item is removed from the list (such as the feed). - invoke-static { }, $LITHO_FILTER_EXTENSION_CLASS->removeParsedElementListItem()V - move-object v$returnEmptyComponentRegister, p1 # Required for 19.47 + invoke-static/range { p3 .. p3 }, $LAYOUT_COMPONENTS_FILTER->filterMixPlaylists([B)Z + move-result p4 + if-eqz p4, :show + invoke-static/range { p2 .. p2 }, $LITHO_FILTER_EXTENSION_CLASS->removeFilteredElementListItem($EXTENSION_CONTEXT_INTERFACE)V + move-object/from16 v$returnEmptyComponentRegister, p1 # Required for 19.47 goto :return_empty_component :show nop """, ExternalLabel("return_empty_component", returnEmptyComponentInstruction), ) - - // The conversion context parameter is reused before the filter is called. - addInstruction( - 0, - "invoke-static/range { p2 .. p2 }, $LITHO_FILTER_EXTENSION_CLASS->setParsedElementContext($EXTENSION_CONTEXT_INTERFACE)V" - ) } } From 83c0cf004e8cafa142cb862e3d78fd7e98c9f3c3 Mon Sep 17 00:00:00 2001 From: Flarette Date: Sun, 25 Oct 2026 20:55:48 +0200 Subject: [PATCH 065/189] fix(YouTube - Force fullscreen landscape): Screen rotates before the fullscreen gesture completes (#4050) --- .../youtube/patches/ForceFullscreenLandscapePatch.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/ForceFullscreenLandscapePatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/ForceFullscreenLandscapePatch.java index 682fa526d6..026a38bf85 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/ForceFullscreenLandscapePatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/ForceFullscreenLandscapePatch.java @@ -25,7 +25,7 @@ public final class ForceFullscreenLandscapePatch { private static final String FULLSCREEN = PlayerType.WATCH_WHILE_FULLSCREEN.name(); - private static final String SLIDING_TO_FULLSCREEN = + private static final String SLIDING_MAXIMIZED_FULLSCREEN = PlayerType.WATCH_WHILE_SLIDING_MAXIMIZED_FULLSCREEN.name(); /** @@ -58,9 +58,13 @@ public static void onPlayerTypeChanged(@Nullable Enum playerType) { if (playerType == null) return; String name = playerType.name(); + // The player slides between maximized and fullscreen in both directions, + // and the gesture can still be cancelled. Wait until the player settles. + if (SLIDING_MAXIMIZED_FULLSCREEN.equals(name)) return; + // Cannot use PlayerType.getCurrent() because this hook can run // before the player type hook of the same method updates it. - boolean isFullscreen = FULLSCREEN.equals(name) || SLIDING_TO_FULLSCREEN.equals(name); + boolean isFullscreen = FULLSCREEN.equals(name); // Phones and other small screens already open fullscreen in landscape, // and forcing the orientation fights with the app and endlessly rotates the screen. From e20076530e0af6061da9029bfaa4ad985fdd2f50 Mon Sep 17 00:00:00 2001 From: Sap <165066971+sap6@users.noreply.github.com> Date: Sun, 25 Oct 2026 18:59:23 +0000 Subject: [PATCH 066/189] fix(YouTube - Captions): Old caption stays on screen after SponsorBlock skips (#3986) Co-authored-by: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> --- .../patches/CaptionsAfterSeekPatch.java | 39 +++++++ .../layout/captions/CaptionsAfterSeekPatch.kt | 88 +++++++++++++++ .../youtube/layout/captions/CaptionsPatch.kt | 1 + .../youtube/layout/captions/Fingerprints.kt | 104 ++++++++++++++++++ 4 files changed, 232 insertions(+) create mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/CaptionsAfterSeekPatch.java create mode 100644 patches/src/main/kotlin/app/morphe/patches/youtube/layout/captions/CaptionsAfterSeekPatch.kt diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/CaptionsAfterSeekPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/CaptionsAfterSeekPatch.java new file mode 100644 index 0000000000..95e210ce54 --- /dev/null +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/CaptionsAfterSeekPatch.java @@ -0,0 +1,39 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/3986 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.extension.youtube.patches; + +import java.util.Collections; +import java.util.List; + +import app.morphe.extension.shared.Logger; + +@SuppressWarnings("unused") +public class CaptionsAfterSeekPatch { + + /** + * Injection point. + * + * @param captionTimes Sorted times the captions change at. + * @return Start time of the caption at the video time, + * or the video time if it is before the first caption. + */ + public static long getCaptionStartTime(List captionTimes, long time) { + try { + int index = Collections.binarySearch(captionTimes, time); + if (index < 0) { + // Not an exact match. Use the caption time before the insertion point. + index = -index - 2; + if (index < 0) return time; + } + return captionTimes.get(index); + } catch (Exception ex) { + Logger.printException(() -> "getCaptionStartTime failure", ex); + return time; + } + } +} diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/captions/CaptionsAfterSeekPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/captions/CaptionsAfterSeekPatch.kt new file mode 100644 index 0000000000..bd664ad035 --- /dev/null +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/captions/CaptionsAfterSeekPatch.kt @@ -0,0 +1,88 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/3986 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.patches.youtube.layout.captions + +import app.morphe.patcher.extensions.InstructionExtensions.addInstructions +import app.morphe.patcher.extensions.InstructionExtensions.addInstructionsWithLabels +import app.morphe.patcher.extensions.InstructionExtensions.getInstruction +import app.morphe.patcher.patch.bytecodePatch +import app.morphe.patcher.util.findFreeRegister +import app.morphe.patcher.util.getReference +import app.morphe.patches.youtube.misc.extension.sharedExtensionPatch +import app.morphe.util.cloneMutable +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.reference.FieldReference +import com.android.tools.smali.dexlib2.iface.reference.TypeReference + +private const val EXTENSION_CLASS = + "Lapp/morphe/extension/youtube/patches/CaptionsAfterSeekPatch;" + +/** + * Captions are timed cues that replace the caption on screen when they start. + * After a long seek, the caption from before the seek stays on screen until the next cue starts: + * - Captions are loaded in chunks of the cues that start in them, so the cue at the new + * position is missing if it started in an earlier chunk. Load the chunk where it starts too. + * - The cue at the new position is loaded after the seek. YouTube shows cues added while the + * video time is inside them only with a server flag, so do that for caption cues. + */ +internal val captionsAfterSeekPatch = bytecodePatch( + description = "Shows the current caption after seeking.", +) { + dependsOn(sharedExtensionPatch) + + execute { + val captionTimesField = CaptionWindowsConstructorFingerprint.instructionMatches.last() + .instruction.getReference()!! + val loaderConstructorFingerprint = + getCaptionChunkLoaderConstructorFingerprint(captionTimesField.definingClass) + val captionWindowsField = loaderConstructorFingerprint.instructionMatches.last() + .instruction.getReference()!! + + getCaptionChunkLoaderFingerprint(loaderConstructorFingerprint.originalClassDef.type).let { + it.method.apply { + val loadChunkMethod = cloneMutable(name = "patch_loadCaptionChunk") + it.classDef.methods.add(loadChunkMethod) + val register = findFreeRegister(0) + + addInstructions( + 0, + """ + invoke-virtual { p0, p1, p2 }, $loadChunkMethod + iget-object v$register, p0, $captionWindowsField + iget-object v$register, v$register, $captionTimesField + invoke-static { v$register, p1, p2 }, $EXTENSION_CLASS->getCaptionStartTime(Ljava/util/List;J)J + move-result-wide p1 + invoke-virtual { p0, p1, p2 }, $loadChunkMethod + return-void + """ + ) + } + } + + getAddCueRangesFingerprint(EnterCueRangeOnAddFeatureFlagFingerprint.originalMethod).let { + it.method.apply { + val castInstruction = it.instructionMatches[1].getInstruction() + val cueRangeType = castInstruction.getReference()!!.type + val captionCueType = getCaptionCueConstructorFingerprint(cueRangeType).originalClassDef.type + + val flagResultIndex = it.instructionMatches.last().index + val flagRegister = getInstruction(flagResultIndex).registerA + + addInstructionsWithLabels( + flagResultIndex + 1, + """ + if-nez v$flagRegister, :enabled + instance-of v$flagRegister, v${castInstruction.registerA}, $captionCueType + :enabled + nop + """ + ) + } + } + } +} diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/captions/CaptionsPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/captions/CaptionsPatch.kt index 71139c348f..eacf128bd1 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/captions/CaptionsPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/captions/CaptionsPatch.kt @@ -63,6 +63,7 @@ val captionsPatch = bytecodePatch( transcriptPatch, translatedCaptionPensPatch, captionsInPipPatch, + captionsAfterSeekPatch, ) compatibleWith(COMPATIBILITY_YOUTUBE) diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/captions/Fingerprints.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/captions/Fingerprints.kt index ee11a31e5a..8400cca323 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/captions/Fingerprints.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/captions/Fingerprints.kt @@ -523,3 +523,107 @@ internal fun getCaptionTrackEventFingerprint(trackType: String) = object : Finge ) ) ) {} + +/** + * Cue range manager seek. Its class also adds and removes cue ranges. + */ +internal object CueRangeManagerSeekFingerprint : Fingerprint( + returnType = "J", + parameters = listOf("J", "J", "Z"), + filters = listOf( + string("CueRangeManager", StringComparisonType.CONTAINS) + ) +) + +/** + * Server flag to start cue ranges that are added while the video time is inside them. + */ +internal object EnterCueRangeOnAddFeatureFlagFingerprint : Fingerprint( + returnType = "Z", + parameters = listOf(), + filters = listOf( + literal(45415907L) + ) +) + +/** + * Adds a list of cue ranges. Matches at the cast of each cue range and the flag check. + */ +internal fun getAddCueRangesFingerprint(enterCueRangeOnAddFlagMethod: MethodReference) = object : Fingerprint( + classFingerprint = CueRangeManagerSeekFingerprint, + returnType = "V", + parameters = listOf("Ljava/util/List;"), + filters = listOf( + methodCall(smali = "Ljava/util/Iterator;->next()Ljava/lang/Object;"), + opcode(Opcode.CHECK_CAST, MatchAfterWithin(2)), + methodCall(reference = enterCueRangeOnAddFlagMethod), + opcode(Opcode.MOVE_RESULT, MatchAfterImmediately()) + ) +) {} + +/** + * Caption cue, a cue range with the caption windows it shows when it starts. + */ +internal fun getCaptionCueConstructorFingerprint(cueRangeType: String) = object : Fingerprint( + superclass = cueRangeType, + name = "", + returnType = "V", + parameters = listOf("J", "J", "Ljava/util/List;", "L"), + filters = listOf( + methodCall( + definingClass = cueRangeType, + name = "" + ) + ) +) {} + +/** + * Captions of a fully loaded caption track. + * Matches at the field of the sorted times the captions change at. + */ +internal object CaptionWindowsConstructorFingerprint : Fingerprint( + name = "", + returnType = "V", + parameters = listOf("Ljava/util/List;"), + filters = listOf( + newInstance("Ljava/util/TreeSet;"), + methodCall(smali = "Ljava/util/TreeSet;->addAll(Ljava/util/Collection;)Z"), + newInstance("Ljava/util/ArrayList;"), + methodCall(smali = "Lj$/util/DesugarCollections;->unmodifiableList(Ljava/util/List;)Ljava/util/List;"), + opcode(Opcode.MOVE_RESULT_OBJECT, MatchAfterImmediately()), + fieldAccess( + opcode = Opcode.IPUT_OBJECT, + definingClass = "this", + type = "Ljava/util/List;", + location = MatchAfterImmediately() + ) + ) +) + +/** + * Caption chunk loader. Matches at the field of the captions it loads from. + */ +internal fun getCaptionChunkLoaderConstructorFingerprint(captionWindowsType: String) = object : Fingerprint( + name = "", + returnType = "V", + filters = listOf( + newInstance("Ljava/util/TreeMap;"), + fieldAccess( + opcode = Opcode.IPUT_OBJECT, + definingClass = "this", + type = captionWindowsType + ) + ) +) {} + +/** + * Loads the caption cues that start in the chunk of a video time. + */ +internal fun getCaptionChunkLoaderFingerprint(loaderType: String) = object : Fingerprint( + definingClass = loaderType, + returnType = "V", + parameters = listOf("J"), + filters = listOf( + methodCall(smali = "Ljava/util/concurrent/Executor;->execute(Ljava/lang/Runnable;)V") + ) +) {} From f4c1f84e13f0ed9b5982adfccbdbec1529ce0091 Mon Sep 17 00:00:00 2001 From: pharmacomaniac Date: Sun, 25 Oct 2026 16:03:05 -0400 Subject: [PATCH 067/189] fix(YouTube - Disable playlist autoplay): fix looping the current video (#4064) --- .../patches/DisablePlaylistAutoplayPatch.java | 10 ++++++++++ .../DisablePlaylistAutoplayPatch.kt | 6 ++++++ .../layout/playlistautoplay/Fingerprints.kt | 15 +++++++++++++++ 3 files changed, 31 insertions(+) diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/DisablePlaylistAutoplayPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/DisablePlaylistAutoplayPatch.java index 73c9454608..4b3df1462d 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/DisablePlaylistAutoplayPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/DisablePlaylistAutoplayPatch.java @@ -12,11 +12,21 @@ @SuppressWarnings("unused") public class DisablePlaylistAutoplayPatch { + private static volatile boolean isRepeating; + + public static void setRepeatMode(int repeatMode) { + // Native state updates refresh this when playback or repeat mode changes. + // Mode 2 repeats the current video; mode 1 repeats the playlist. + isRepeating = repeatMode == 2; + } + /** * Injection point. */ public static boolean shouldSkipPlaylistAutoplay(Enum navigationIntent) { return Settings.DISABLE_PLAYLIST_AUTOPLAY.get() && "AUTOPLAY".equals(navigationIntent.name()) + // YouTube also uses AUTOPLAY to repeat the current video. + && !isRepeating // The queue plays its next video instead, and its hook may run after this one. && !LocalQueuePatch.hasQueuedVideos(); } diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/playlistautoplay/DisablePlaylistAutoplayPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/playlistautoplay/DisablePlaylistAutoplayPatch.kt index a77ab4805e..cd416c7a37 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/playlistautoplay/DisablePlaylistAutoplayPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/playlistautoplay/DisablePlaylistAutoplayPatch.kt @@ -7,6 +7,7 @@ package app.morphe.patches.youtube.layout.playlistautoplay +import app.morphe.patcher.extensions.InstructionExtensions.addInstructions import app.morphe.patcher.patch.bytecodePatch import app.morphe.patches.shared.misc.settings.preference.SwitchPreference import app.morphe.patches.youtube.misc.extension.sharedExtensionPatch @@ -34,6 +35,11 @@ val disablePlaylistAutoplayPatch = bytecodePatch( SwitchPreference("morphe_disable_playlist_autoplay", summary = true) ) + // Capture repeat mode for the autoplay check + NativeNavigationStateFingerprint.method.addInstructions( + 0, + "invoke-static { p3 }, $EXTENSION_CLASS->setRepeatMode(I)V" + ) navigationIntentHook(EXTENSION_CLASS, "shouldSkipPlaylistAutoplay") } } diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/playlistautoplay/Fingerprints.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/playlistautoplay/Fingerprints.kt index d19e07b548..f3e3d6408e 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/playlistautoplay/Fingerprints.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/playlistautoplay/Fingerprints.kt @@ -8,6 +8,8 @@ package app.morphe.patches.youtube.layout.playlistautoplay import app.morphe.patcher.Fingerprint +import app.morphe.patcher.InstructionLocation.MatchAfterImmediately +import app.morphe.patcher.fieldAccess import app.morphe.patcher.opcode import app.morphe.patcher.string import com.android.tools.smali.dexlib2.AccessFlags @@ -27,3 +29,16 @@ internal object NavigationIntentEnumFingerprint : Fingerprint( classDef.methods.any { it.name == "" && it.parameterTypes.size > 2 } }, ) + +internal object NativeNavigationStateFingerprint : Fingerprint( + accessFlags = listOf(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR), + // Previous video available, next video available, repeat mode, shuffle enabled + parameters = listOf("Z", "Z", "I", "Z"), + filters = listOf( + fieldAccess(opcode = Opcode.IPUT_BOOLEAN, definingClass = "this"), + fieldAccess(opcode = Opcode.IPUT_BOOLEAN, definingClass = "this", location = MatchAfterImmediately()), + fieldAccess(opcode = Opcode.IPUT, definingClass = "this", location = MatchAfterImmediately()), + fieldAccess(opcode = Opcode.IPUT_BOOLEAN, definingClass = "this", location = MatchAfterImmediately()), + opcode(Opcode.RETURN_VOID, location = MatchAfterImmediately()), + ), +) From bcf540ab113b02b2c4d57f3fa5570437e3c3a3db Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Sun, 25 Oct 2026 23:30:36 +0100 Subject: [PATCH 068/189] refactor(YouTube): Change extension Kotlin code to Java --- extensions/proguard-rules.pro | 4 - .../extension/youtube/addon/AddOnApi.java | 2 - .../youtube/patches/AppRefreshRatePatch.java | 3 - .../patches/BackgroundPlaybackPatch.java | 1 - .../patches/FullscreenVideoScalePatch.java | 2 - .../youtube/patches/HideStatusBarPatch.java | 2 - .../youtube/patches/LoadVideoPatch.java | 6 +- .../patches/MinimalMiniplayerPatch.java | 2 - .../patches/PlayerOverlaysHookPatch.java | 15 - .../patches/components/ShortsFilter.java | 2 - .../speed/CustomPlaybackSpeedPatch.java | 8 +- .../speed/RememberPlaybackSpeedPatch.java | 3 - .../queue/NativePlaylistQueuePatch.java | 2 - .../patches/utils/FeedVideoCardsPatch.java | 2 - .../youtube/patches/utils/PlaylistPatch.java | 10 +- .../utils/requests/CreatePlaylistRequest.java | 3 +- .../utils/requests/GetPlaylistsRequest.java | 3 +- .../VoiceOverTranslationPatch.java | 2 - .../youtube/series/SeriesTrackerPatch.java | 5 +- .../extension/youtube/shared/Event.java | 48 ++ .../morphe/extension/youtube/shared/Event.kt | 37 - .../youtube/shared/PipDismissHelper.java | 9 +- .../shared/PlayerControlsVisibility.java | 68 ++ .../shared/PlayerControlsVisibility.kt | 47 -- .../PlayerControlsVisibilityObserver.java | 100 +++ .../PlayerControlsVisibilityObserver.kt | 101 --- .../youtube/shared/PlayerOverlays.kt | 96 --- .../shared/{PlayerType.kt => PlayerType.java} | 125 +-- .../youtube/shared/ShortsPlayerState.java | 35 + .../youtube/shared/ShortsPlayerState.kt | 37 - .../extension/youtube/shared/VideoState.java | 73 ++ .../extension/youtube/shared/VideoState.kt | 53 -- .../YouTubeSponsorBlockConfig.java | 3 - .../ui/SponsorBlockViewController.java | 2 - .../SwipeControlsConfigurationProvider.java | 562 ++++++++++++++ .../SwipeControlsConfigurationProvider.kt | 440 ----------- .../SwipeControlsHostActivity.java | 386 +++++++++ .../SwipeControlsHostActivity.kt | 332 -------- .../controller/AudioVolumeController.java | 175 +++++ .../controller/AudioVolumeController.kt | 169 ---- .../ScreenBrightnessController.java | 80 ++ .../controller/ScreenBrightnessController.kt | 67 -- .../controller/SwipeZonesController.java | 184 +++++ .../controller/SwipeZonesController.kt | 168 ---- .../controller/VolumeKeysController.java | 71 ++ .../controller/VolumeKeysController.kt | 62 -- .../gesture/ClassicSwipeController.java | 126 +++ .../gesture/ClassicSwipeController.kt | 113 --- .../gesture/PressToSwipeController.java | 79 ++ .../gesture/PressToSwipeController.kt | 71 -- .../gesture/core/BaseGestureController.java | 261 +++++++ .../gesture/core/BaseGestureController.kt | 239 ------ ...reController.kt => GestureController.java} | 8 +- .../gesture/core/SwipeDetector.java | 93 +++ .../controller/gesture/core/SwipeDetector.kt | 94 --- .../core/VolumeAndBrightnessScroller.java | 152 ++++ .../core/VolumeAndBrightnessScroller.kt | 148 ---- .../youtube/swipecontrols/misc/Point.java | 26 + .../youtube/swipecontrols/misc/Point.kt | 17 - .../youtube/swipecontrols/misc/Rectangle.java | 43 + .../youtube/swipecontrols/misc/Rectangle.kt | 22 - .../misc/ScrollDistanceHelper.java | 73 ++ .../misc/ScrollDistanceHelper.kt | 56 -- .../misc/SwipeControlsOverlay.java | 44 ++ .../misc/SwipeControlsOverlay.kt | 34 - .../misc/SwipeControlsUtils.java | 31 + .../swipecontrols/misc/SwipeControlsUtils.kt | 25 - .../views/SwipeControlsOverlayLayout.java | 703 +++++++++++++++++ .../views/SwipeControlsOverlayLayout.kt | 733 ------------------ .../videoplayer/VideoQualityDialogButton.java | 2 - gradle.properties | 2 + 71 files changed, 3503 insertions(+), 3299 deletions(-) delete mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/PlayerOverlaysHookPatch.java create mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/shared/Event.java delete mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/shared/Event.kt create mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/shared/PlayerControlsVisibility.java delete mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/shared/PlayerControlsVisibility.kt create mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/shared/PlayerControlsVisibilityObserver.java delete mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/shared/PlayerControlsVisibilityObserver.kt delete mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/shared/PlayerOverlays.kt rename extensions/youtube/src/main/java/app/morphe/extension/youtube/shared/{PlayerType.kt => PlayerType.java} (56%) create mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/shared/ShortsPlayerState.java delete mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/shared/ShortsPlayerState.kt create mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/shared/VideoState.java delete mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/shared/VideoState.kt create mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/SwipeControlsConfigurationProvider.java delete mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/SwipeControlsConfigurationProvider.kt create mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/SwipeControlsHostActivity.java delete mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/SwipeControlsHostActivity.kt create mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/controller/AudioVolumeController.java delete mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/controller/AudioVolumeController.kt create mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/controller/ScreenBrightnessController.java delete mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/controller/ScreenBrightnessController.kt create mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/controller/SwipeZonesController.java delete mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/controller/SwipeZonesController.kt create mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/controller/VolumeKeysController.java delete mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/controller/VolumeKeysController.kt create mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/controller/gesture/ClassicSwipeController.java delete mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/controller/gesture/ClassicSwipeController.kt create mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/controller/gesture/PressToSwipeController.java delete mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/controller/gesture/PressToSwipeController.kt create mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/controller/gesture/core/BaseGestureController.java delete mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/controller/gesture/core/BaseGestureController.kt rename extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/controller/gesture/core/{GestureController.kt => GestureController.java} (72%) create mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/controller/gesture/core/SwipeDetector.java delete mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/controller/gesture/core/SwipeDetector.kt create mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/controller/gesture/core/VolumeAndBrightnessScroller.java delete mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/controller/gesture/core/VolumeAndBrightnessScroller.kt create mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/misc/Point.java delete mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/misc/Point.kt create mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/misc/Rectangle.java delete mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/misc/Rectangle.kt create mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/misc/ScrollDistanceHelper.java delete mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/misc/ScrollDistanceHelper.kt create mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/misc/SwipeControlsOverlay.java delete mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/misc/SwipeControlsOverlay.kt create mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/misc/SwipeControlsUtils.java delete mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/misc/SwipeControlsUtils.kt create mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/views/SwipeControlsOverlayLayout.java delete mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/views/SwipeControlsOverlayLayout.kt diff --git a/extensions/proguard-rules.pro b/extensions/proguard-rules.pro index adf93ebabf..e213fca10d 100644 --- a/extensions/proguard-rules.pro +++ b/extensions/proguard-rules.pro @@ -7,9 +7,5 @@ -keep class com.google.** { *; } -# Proguard can strip away kotlin intrinsics methods that are used by extension Kotlin code. Unclear why. --keep class kotlin.jvm.internal.Intrinsics { - public static *; -} -dontwarn java.lang.reflect.AnnotatedType -dontwarn javax.lang.model.element.Modifier diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/addon/AddOnApi.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/addon/AddOnApi.java index 2f7ef7bc88..571fe9e51f 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/addon/AddOnApi.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/addon/AddOnApi.java @@ -23,7 +23,6 @@ import app.morphe.extension.youtube.patches.VideoInformation; import app.morphe.extension.youtube.shared.VideoState; import app.morphe.extension.youtube.videoplayer.LegacyPlayerControlButton; -import kotlin.Unit; /** * Hooks that add-on patch bundles can attach to at runtime. @@ -129,7 +128,6 @@ public static void addVideoStateListener(Consumer listener) { Logger.printException(() -> "Video state listener failure", ex); } } - return Unit.INSTANCE; }); } diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/AppRefreshRatePatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/AppRefreshRatePatch.java index bec89c8b5f..ac1e8fff7e 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/AppRefreshRatePatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/AppRefreshRatePatch.java @@ -12,7 +12,6 @@ import app.morphe.extension.shared.patches.BaseAppRefreshRatePatch; import app.morphe.extension.youtube.shared.PlayerType; import app.morphe.extension.youtube.shared.VideoState; -import kotlin.Unit; @SuppressWarnings("unused") public final class AppRefreshRatePatch { @@ -27,12 +26,10 @@ public static void initialize(Activity activity) { VideoState.getOnChange().addObserver((VideoState state) -> { updatePlayerIsActive(PlayerType.getCurrent(), state); - return Unit.INSTANCE; }); PlayerType.getOnChange().addObserver((PlayerType type) -> { updatePlayerIsActive(type, VideoState.getCurrent()); - return Unit.INSTANCE; }); } diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/BackgroundPlaybackPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/BackgroundPlaybackPatch.java index bb6012c644..821561ab0b 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/BackgroundPlaybackPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/BackgroundPlaybackPatch.java @@ -72,7 +72,6 @@ public void onReceive(Context context, Intent intent) { VideoState.getOnChange().addObserver(state -> { onVideoStateChanged(state); - return kotlin.Unit.INSTANCE; }); } catch (Exception ex) { Logger.printException(() -> "initialize failure", ex); diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FullscreenVideoScalePatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FullscreenVideoScalePatch.java index d6ac1edeb1..9f346471d7 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FullscreenVideoScalePatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FullscreenVideoScalePatch.java @@ -29,7 +29,6 @@ import app.morphe.extension.youtube.settings.Settings; import app.morphe.extension.youtube.shared.PlayerType; import app.morphe.extension.youtube.shared.ShortsPlayerState; -import kotlin.Unit; /** * Maps the playing video onto the full display with a compositor transform. @@ -91,7 +90,6 @@ public enum VideoScaleMode { static { PlayerType.getOnChange().addObserver((PlayerType type) -> { applyScale(); - return Unit.INSTANCE; }); } diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/HideStatusBarPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/HideStatusBarPatch.java index c1091be486..13d306fee6 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/HideStatusBarPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/HideStatusBarPatch.java @@ -35,7 +35,6 @@ import app.morphe.extension.shared.Utils; import app.morphe.extension.youtube.settings.Settings; import app.morphe.extension.youtube.shared.PlayerType; -import kotlin.Unit; @SuppressWarnings({"unused", "deprecation"}) public final class HideStatusBarPatch { @@ -248,7 +247,6 @@ public static void initialize(Activity activity) { hiddenStatusBarActivity.getWindow().getDecorView().requestApplyInsets(); } } - return Unit.INSTANCE; }); } hideStatusBar(activity); diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/LoadVideoPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/LoadVideoPatch.java index e3d805901c..397e522fc2 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/LoadVideoPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/LoadVideoPatch.java @@ -21,14 +21,13 @@ import java.lang.ref.WeakReference; import java.util.Objects; +import java.util.function.Consumer; import app.morphe.extension.shared.Logger; import app.morphe.extension.shared.Utils; import app.morphe.extension.youtube.patches.playback.speed.RememberPlaybackSpeedPatch; import app.morphe.extension.youtube.patches.queue.NativePlaylistQueuePatch; import app.morphe.extension.youtube.shared.PlayerType; -import kotlin.Unit; -import kotlin.jvm.functions.Function1; @SuppressWarnings("unused") public final class LoadVideoPatch { @@ -80,11 +79,10 @@ public interface InlinePlaybackInterface { */ private static int inlinePlaybackBlockId; - private static final Function1 unblockInlinePlaybackOnPlayerOpen = type -> { + private static final Consumer unblockInlinePlaybackOnPlayerOpen = type -> { if (type.isMaximizedOrFullscreen()) { unblockInlinePlayback(); } - return Unit.INSTANCE; }; /** diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/MinimalMiniplayerPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/MinimalMiniplayerPatch.java index c15aab820d..3e5f079869 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/MinimalMiniplayerPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/MinimalMiniplayerPatch.java @@ -60,7 +60,6 @@ import app.morphe.extension.youtube.shared.ShortsPlayerState; import app.morphe.extension.youtube.videoplayer.PlayPauseIcons; import app.morphe.extension.youtube.videoplayer.PlayerIcons; -import kotlin.Unit; /** * Rebuilds the legacy 'Minimal' miniplayer, which YouTube dropped the code for in 21.29. @@ -406,7 +405,6 @@ public boolean onTouch(View view, MotionEvent event) { updateVideoClip(); }); - return Unit.INSTANCE; }); } diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/PlayerOverlaysHookPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/PlayerOverlaysHookPatch.java deleted file mode 100644 index 03b1de6442..0000000000 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/PlayerOverlaysHookPatch.java +++ /dev/null @@ -1,15 +0,0 @@ -package app.morphe.extension.youtube.patches; - -import android.view.ViewGroup; - -import app.morphe.extension.youtube.shared.PlayerOverlays; - -@SuppressWarnings("unused") -public class PlayerOverlaysHookPatch { - /** - * Injection point. - */ - public static void playerOverlayInflated(ViewGroup group) { - PlayerOverlays.attach(group); - } -} diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/components/ShortsFilter.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/components/ShortsFilter.java index cca40615a2..84a1c54e72 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/components/ShortsFilter.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/components/ShortsFilter.java @@ -41,7 +41,6 @@ import app.morphe.extension.youtube.shared.NavigationBar; import app.morphe.extension.youtube.shared.PlayerType; import app.morphe.extension.youtube.shared.ShortsPlayerState; -import kotlin.Unit; @SuppressWarnings({"unused", "FieldCanBeLocal"}) public final class ShortsFilter extends Filter { @@ -776,7 +775,6 @@ public static void setBottomBarContainer(View view) { } navigationBar.setLayoutParams(params); } - return Unit.INSTANCE; }); } } diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/playback/speed/CustomPlaybackSpeedPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/playback/speed/CustomPlaybackSpeedPatch.java index d7d48cc78c..36fee643e0 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/playback/speed/CustomPlaybackSpeedPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/playback/speed/CustomPlaybackSpeedPatch.java @@ -57,8 +57,6 @@ import app.morphe.extension.youtube.settings.Settings; import app.morphe.extension.youtube.shared.PipDismissHelper; import app.morphe.extension.youtube.videoplayer.LegacyPlayerControlButton; -import kotlin.Unit; -import kotlin.jvm.functions.Function1; @SuppressWarnings("unused") public class CustomPlaybackSpeedPatch { @@ -430,10 +428,9 @@ public void onStopTrackingTouch(SeekBar seekBar) {} // The first observer to keep label text and slider reactive to speed changes. // observer is removed on dialog dismissal in order. - Function1 onSpeedChanged = speed -> { + Consumer onSpeedChanged = speed -> { currentSpeedText.setText(VideoInformation.formatSpeedStringX(speed)); speedSlider.setProgress(speedToProgressValue(speed)); - return Unit.INSTANCE; }; VideoInformation.onPlaybackSpeedChange.addObserver(onSpeedChanged); @@ -529,7 +526,7 @@ public void onStopTrackingTouch(SeekBar seekBar) {} contentLayout.addView(gridLayout); // ## Audio pitch UI: link toggle and pitch controls, hidden entirely when disabled. - final Function1 onPitchChanged; + final Consumer onPitchChanged; if (Settings.ENABLE_PLAYBACK_AUDIO_PITCH.get()) { // ## Link Button LinearLayout linkLayout = new LinearLayout(context); @@ -623,7 +620,6 @@ public void onStopTrackingTouch(SeekBar seekBar) {} onPitchChanged = pitch -> { currentPitchText.setText(VideoInformation.formatAudioPitchStringX(pitch)); pitchSlider.setProgress(pitchToProgressValue(pitch)); - return Unit.INSTANCE; }; VideoInformation.onPlaybackAudioPitchChange.addObserver(onPitchChanged); diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/playback/speed/RememberPlaybackSpeedPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/playback/speed/RememberPlaybackSpeedPatch.java index c8805a84b7..c64174fad3 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/playback/speed/RememberPlaybackSpeedPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/playback/speed/RememberPlaybackSpeedPatch.java @@ -24,7 +24,6 @@ import app.morphe.extension.youtube.shared.ShortsPlayerState; import app.morphe.extension.youtube.whitelist.ChannelWhitelist; import app.morphe.extension.youtube.whitelist.WhitelistType; -import kotlin.Unit; @SuppressWarnings("unused") public final class RememberPlaybackSpeedPatch { @@ -67,7 +66,6 @@ public final class RememberPlaybackSpeedPatch { } else { shortsSelectedPlaybackSpeed = -2.0f; } - return Unit.INSTANCE; }); // The app only sets the speed of a Short if it differs from the speed it uses, @@ -77,7 +75,6 @@ public final class RememberPlaybackSpeedPatch { if (ShortsPlayerState.isOpen()) { applyShortsPlaybackSpeed(false); } - return Unit.INSTANCE; }); } diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/queue/NativePlaylistQueuePatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/queue/NativePlaylistQueuePatch.java index 26573b0d0c..cc890bce88 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/queue/NativePlaylistQueuePatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/queue/NativePlaylistQueuePatch.java @@ -391,12 +391,10 @@ private static void updateController(PlaylistController controller) { observingPlayer = true; PlayerType.getOnChange().addObserver(type -> { onPlaybackContextChanged(); - return kotlin.Unit.INSTANCE; }); ShortsPlayerState.getOnChange().addObserver(open -> { onPlaybackContextChanged(); - return kotlin.Unit.INSTANCE; }); } diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/FeedVideoCardsPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/FeedVideoCardsPatch.java index 97eb4ccc6e..a80cca516f 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/FeedVideoCardsPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/FeedVideoCardsPatch.java @@ -40,7 +40,6 @@ import app.morphe.extension.youtube.patches.VideoInformation; import app.morphe.extension.youtube.settings.Settings; import app.morphe.extension.youtube.shared.PlayerType; -import kotlin.Unit; /** * Finds the full width video cards of the feeds, and the video id of each card. @@ -128,7 +127,6 @@ public final class FeedVideoCardsPatch { // on the thumbnail is hidden while the video of the card plays in the feed. PlayerType.getOnChange().addObserver(type -> { redrawCards(); - return Unit.INSTANCE; }); } } diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/PlaylistPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/PlaylistPatch.java index ac6b521d3b..109f9af67b 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/PlaylistPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/PlaylistPatch.java @@ -12,6 +12,7 @@ import android.annotation.SuppressLint; import android.content.Context; import android.text.TextUtils; +import android.util.Pair; import android.util.TypedValue; import android.view.Gravity; import android.view.View; @@ -48,7 +49,6 @@ import app.morphe.extension.youtube.settings.Settings; import app.morphe.extension.youtube.settings.YouTubeActivityHook; import app.morphe.extension.youtube.shared.PlayerType; -import kotlin.Pair; @SuppressWarnings({"unused", "StaticFieldLeak"}) public class PlaylistPatch { @@ -210,8 +210,8 @@ private static void fetchQueue(Context context, boolean remove, boolean openPlay if (request != null) { Pair playlistIds = request.getPlaylistId(); if (playlistIds != null) { - String createdPlaylistId = playlistIds.getFirst(); - String setVideoId = playlistIds.getSecond(); + String createdPlaylistId = playlistIds.first; + String setVideoId = playlistIds.second; if (createdPlaylistId != null && setVideoId != null) { playlistId = createdPlaylistId; if (Settings.QUEUE_RESTORE.get()) { @@ -302,8 +302,8 @@ private static void saveToPlaylist(Context context) { listContainer.setOrientation(LinearLayout.VERTICAL); for (Pair playlist : playlists) { - String listId = playlist.getFirst(); - String title = playlist.getSecond(); + String listId = playlist.first; + String title = playlist.second; Runnable action = () -> saveToPlaylist(listId, title); View itemLayout = createItemLayout(context, title, libraryIconId); actionsMap.put(itemLayout, action); diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/requests/CreatePlaylistRequest.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/requests/CreatePlaylistRequest.java index 3c3bcb569c..17f5fce390 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/requests/CreatePlaylistRequest.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/requests/CreatePlaylistRequest.java @@ -9,6 +9,8 @@ import static app.morphe.extension.shared.StringRef.str; +import android.util.Pair; + import androidx.annotation.Nullable; import org.json.JSONException; @@ -29,7 +31,6 @@ import app.morphe.extension.shared.Utils; import app.morphe.extension.shared.requests.Requester; import app.morphe.extension.shared.settings.BaseSettings; -import kotlin.Pair; public class CreatePlaylistRequest { private static final int MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 20 * 1000; diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/requests/GetPlaylistsRequest.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/requests/GetPlaylistsRequest.java index 9c9ce4266f..2dee453e9d 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/requests/GetPlaylistsRequest.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/requests/GetPlaylistsRequest.java @@ -7,6 +7,8 @@ package app.morphe.extension.youtube.patches.utils.requests; +import android.util.Pair; + import androidx.annotation.Nullable; import org.json.JSONArray; @@ -30,7 +32,6 @@ import app.morphe.extension.shared.Utils; import app.morphe.extension.shared.requests.Requester; import app.morphe.extension.shared.settings.BaseSettings; -import kotlin.Pair; public class GetPlaylistsRequest { private static final int MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 20 * 1000; diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/voiceovertranslation/VoiceOverTranslationPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/voiceovertranslation/VoiceOverTranslationPatch.java index 949776cce4..3a93b5869d 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/voiceovertranslation/VoiceOverTranslationPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/voiceovertranslation/VoiceOverTranslationPatch.java @@ -188,7 +188,6 @@ static int getLastSpokenIndex() { TTSPrefetcher.clear(); } } - return kotlin.Unit.INSTANCE; }); VideoState.getOnChange().addObserver(state -> { @@ -211,7 +210,6 @@ static int getLastSpokenIndex() { PlayerVolumePatch.clearDuckMultiplier(); TTSPrefetcher.clear(); } - return kotlin.Unit.INSTANCE; }); } diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/series/SeriesTrackerPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/series/SeriesTrackerPatch.java index c50fba89af..f6413dcd24 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/series/SeriesTrackerPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/series/SeriesTrackerPatch.java @@ -39,10 +39,7 @@ public final class SeriesTrackerPatch { static { if (ENABLED) { - VideoState.getOnChange().addObserver(state -> { - videoStateChanged(state); - return kotlin.Unit.INSTANCE; - }); + VideoState.getOnChange().addObserver(SeriesTrackerPatch::videoStateChanged); } } diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/shared/Event.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/shared/Event.java new file mode 100644 index 0000000000..5e76df79b1 --- /dev/null +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/shared/Event.java @@ -0,0 +1,48 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches + * + * Original hard forked code: + * https://github.com/ReVanced/revanced-patches/commit/724e6d61b2ecd868c1a9a37d465a688e83a74799 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to Morphe contributions. + */ + +package app.morphe.extension.youtube.shared; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Consumer; + +import app.morphe.extension.shared.Logger; + +/** + * generic event provider class + */ +public final class Event { + private final Set> eventListeners = Collections.synchronizedSet(new LinkedHashSet<>()); + + public void addObserver(Consumer observer) { + Logger.printDebug(() -> "Adding observer: " + observer); + eventListeners.add(observer); + } + + public void removeObserver(Consumer observer) { + eventListeners.remove(observer); + } + + public void invoke(T value) { + // Observers can remove themselves or add other observers while they are notified, + // which would break the iteration of the set. + final List> observers; + synchronized (eventListeners) { + observers = new ArrayList<>(eventListeners); + } + for (Consumer observer : observers) { + observer.accept(value); + } + } +} diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/shared/Event.kt b/extensions/youtube/src/main/java/app/morphe/extension/youtube/shared/Event.kt deleted file mode 100644 index 2f0549a4e4..0000000000 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/shared/Event.kt +++ /dev/null @@ -1,37 +0,0 @@ -package app.morphe.extension.youtube.shared - -import app.morphe.extension.shared.Logger -import java.util.Collections - -/** - * generic event provider class - */ -class Event { - private val eventListeners = Collections.synchronizedSet(mutableSetOf<(T) -> Unit>()) - - operator fun plusAssign(observer: (T) -> Unit) { - addObserver(observer) - } - - fun addObserver(observer: (T) -> Unit) { - Logger.printDebug { "Adding observer: $observer" } - eventListeners.add(observer) - } - - operator fun minusAssign(observer: (T) -> Unit) { - removeObserver(observer) - } - - fun removeObserver(observer: (T) -> Unit) { - eventListeners.remove(observer) - } - - operator fun invoke(value: T) { - // Observers can remove themselves or add other observers while they are notified, - // which would break the iteration of the set. - val observers = synchronized(eventListeners) { eventListeners.toList() } - for (observer in observers) { - observer.invoke(value) - } - } -} diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/shared/PipDismissHelper.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/shared/PipDismissHelper.java index 1f023d363e..db27217442 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/shared/PipDismissHelper.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/shared/PipDismissHelper.java @@ -9,10 +9,10 @@ import androidx.annotation.Nullable; +import java.util.function.Consumer; + import app.morphe.extension.shared.Logger; import app.morphe.extension.shared.ui.SheetBottomDialog; -import kotlin.Unit; -import kotlin.jvm.functions.Function1; public final class PipDismissHelper { @@ -30,16 +30,15 @@ public static void dismissOnPip(SheetBottomDialog.SlideDialog dialog) { * Same as {@link #dismissOnPip(SheetBottomDialog.SlideDialog)}, and runs {@code onDismiss} when the dialog is dismissed. */ public static void dismissOnPip(SheetBottomDialog.SlideDialog dialog, @Nullable Runnable onDismiss) { - Function1 observer = new Function1<>() { + Consumer observer = new Consumer<>() { @Override - public Unit invoke(PlayerType type) { + public void accept(PlayerType type) { if (!dialog.isShowing()) { PlayerType.getOnChange().removeObserver(this); } else if (type == PlayerType.WATCH_WHILE_PICTURE_IN_PICTURE) { Logger.printDebug(() -> "Dismissing dialog due to PiP mode"); dialog.dismiss(); } - return Unit.INSTANCE; } }; PlayerType.getOnChange().addObserver(observer); diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/shared/PlayerControlsVisibility.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/shared/PlayerControlsVisibility.java new file mode 100644 index 0000000000..36b7b6edee --- /dev/null +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/shared/PlayerControlsVisibility.java @@ -0,0 +1,68 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches + * + * Original hard forked code: + * https://github.com/ReVanced/revanced-patches/commit/724e6d61b2ecd868c1a9a37d465a688e83a74799 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to Morphe contributions. + */ + +package app.morphe.extension.youtube.shared; + +import java.util.HashMap; +import java.util.Map; + +import app.morphe.extension.shared.Logger; + +/** + * PlayerControls visibility state. + */ +public enum PlayerControlsVisibility { + PLAYER_CONTROLS_VISIBILITY_UNKNOWN, + PLAYER_CONTROLS_VISIBILITY_WILL_HIDE, + PLAYER_CONTROLS_VISIBILITY_HIDDEN, + PLAYER_CONTROLS_VISIBILITY_WILL_SHOW, + PLAYER_CONTROLS_VISIBILITY_SHOWN; + + private static final Map nameToPlayerControlsVisibility; + + static { + PlayerControlsVisibility[] values = values(); + nameToPlayerControlsVisibility = new HashMap<>(2 * values.length); + for (PlayerControlsVisibility visibility : values) { + nameToPlayerControlsVisibility.put(visibility.name(), visibility); + } + } + + private static final Event onChange = new Event<>(); + + private static volatile PlayerControlsVisibility currentPlayerControlsVisibility + = PLAYER_CONTROLS_VISIBILITY_UNKNOWN; // Read/write from different threads. + + public static Event getOnChange() { + return onChange; + } + + public static void setFromString(String enumName) { + PlayerControlsVisibility newType = nameToPlayerControlsVisibility.get(enumName); + if (newType == null) { + Logger.printException(() -> "Unknown PlayerControlsVisibility encountered: " + enumName); + } else { + setCurrent(newType); + } + } + + public static PlayerControlsVisibility getCurrent() { + return currentPlayerControlsVisibility; + } + + private static void setCurrent(PlayerControlsVisibility type) { + if (currentPlayerControlsVisibility != type) { + Logger.printDebug(() -> "Changed to: " + type); + + currentPlayerControlsVisibility = type; + onChange.invoke(type); + } + } +} diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/shared/PlayerControlsVisibility.kt b/extensions/youtube/src/main/java/app/morphe/extension/youtube/shared/PlayerControlsVisibility.kt deleted file mode 100644 index 64bb05ba2e..0000000000 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/shared/PlayerControlsVisibility.kt +++ /dev/null @@ -1,47 +0,0 @@ -package app.morphe.extension.youtube.shared - -import app.morphe.extension.shared.Logger - -/** - * PlayerControls visibility state. - */ -enum class PlayerControlsVisibility { - PLAYER_CONTROLS_VISIBILITY_UNKNOWN, - PLAYER_CONTROLS_VISIBILITY_WILL_HIDE, - PLAYER_CONTROLS_VISIBILITY_HIDDEN, - PLAYER_CONTROLS_VISIBILITY_WILL_SHOW, - PLAYER_CONTROLS_VISIBILITY_SHOWN; - - companion object { - - private val nameToPlayerControlsVisibility = PlayerControlsVisibility.entries.associateBy { it.name } - - @JvmStatic - fun setFromString(enumName: String) { - val newType = nameToPlayerControlsVisibility[enumName] - if (newType == null) { - Logger.printException { "Unknown PlayerControlsVisibility encountered: $enumName" } - } else { - current = newType - } - } - - @JvmStatic - var current - get() = currentPlayerControlsVisibility - private set(type) { - if (currentPlayerControlsVisibility != type) { - Logger.printDebug { "Changed to: $type" } - - currentPlayerControlsVisibility = type - onChange(type) - } - } - - @Volatile // Read/write from different threads. - private var currentPlayerControlsVisibility = PLAYER_CONTROLS_VISIBILITY_UNKNOWN - - @JvmStatic - val onChange = Event() - } -} diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/shared/PlayerControlsVisibilityObserver.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/shared/PlayerControlsVisibilityObserver.java new file mode 100644 index 0000000000..2b582aefa7 --- /dev/null +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/shared/PlayerControlsVisibilityObserver.java @@ -0,0 +1,100 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches + * + * Original hard forked code: + * https://github.com/ReVanced/revanced-patches/commit/724e6d61b2ecd868c1a9a37d465a688e83a74799 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to Morphe contributions. + */ + +package app.morphe.extension.youtube.shared; + +import android.app.Activity; +import android.view.View; +import android.view.ViewGroup; + +import java.lang.ref.WeakReference; + +import app.morphe.extension.shared.ResourceType; +import app.morphe.extension.shared.ResourceUtils; + +/** + * Provides the visibility status of the fullscreen player controls_layout view. + * This can be used for detecting when the player controls are shown. + */ +public final class PlayerControlsVisibilityObserver { + + /** + * Activity that contains the controls_layout view. + */ + private final Activity activity; + + /** + * ID of the direct parent of player_control_play_pause_replay_button_touch_area, R.id.controls_button_group_layout + */ + private final int controlsButtonGroupId; + + /** + * id of R.id.player_control_play_pause_replay_button_touch_area + */ + private final int controlButtonId; + + /** + * reference to the control button view + */ + private WeakReference controlButtonView = new WeakReference<>(null); + + /** + * @param activity activity that contains the controls_layout view + */ + public PlayerControlsVisibilityObserver(Activity activity) { + this.activity = activity; + controlsButtonGroupId = ResourceUtils.getIdentifier( + activity, ResourceType.ID, "controls_button_group_layout"); + controlButtonId = ResourceUtils.getIdentifier( + activity, ResourceType.ID, "player_control_play_pause_replay_button_touch_area"); + } + + /** + * is the {@link #controlButtonView} set to a valid reference of a view? + */ + private boolean isAttached() { + View view = controlButtonView.get(); + return view != null && view.getParent() != null; + } + + /** + * find and attach the player_control_play_pause_replay_button_touch_area view if needed + */ + private void maybeAttach() { + if (isAttached()) return; + + // find parent, then player_control_play_pause_replay_button_touch_area view + // this is needed because there may be two views where id=R.id.player_control_play_pause_replay_button_touch_area + // because why should google confine themselves to their own guidelines... + ViewGroup parent = activity.findViewById(controlsButtonGroupId); + if (parent != null) { + View view = parent.findViewById(controlButtonId); + if (view != null) { + controlButtonView = new WeakReference<>(view); + } + } + } + + /** + * current visibility int of the controls_layout view + */ + public int getPlayerControlsVisibility() { + maybeAttach(); + View view = controlButtonView.get(); + return view == null ? View.GONE : view.getVisibility(); + } + + /** + * is the value of {@link #getPlayerControlsVisibility()} equal to {@link View#VISIBLE}? + */ + public boolean arePlayerControlsVisible() { + return getPlayerControlsVisibility() == View.VISIBLE; + } +} diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/shared/PlayerControlsVisibilityObserver.kt b/extensions/youtube/src/main/java/app/morphe/extension/youtube/shared/PlayerControlsVisibilityObserver.kt deleted file mode 100644 index 522b4aab26..0000000000 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/shared/PlayerControlsVisibilityObserver.kt +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright 2026 Morphe. - * https://github.com/MorpheApp/morphe-patches - * - * Original hard forked code: - * https://github.com/ReVanced/revanced-patches/commit/724e6d61b2ecd868c1a9a37d465a688e83a74799 - * - * See the included NOTICE file for GPLv3 Section 7 terms that apply to Morphe contributions. - */ - -package app.morphe.extension.youtube.shared - -import android.app.Activity -import android.view.View -import android.view.ViewGroup -import app.morphe.extension.shared.ResourceType -import app.morphe.extension.shared.ResourceUtils.getIdentifier -import java.lang.ref.WeakReference - -/** - * default implementation of [PlayerControlsVisibilityObserver] - * - * @param activity activity that contains the controls_layout view - */ -class PlayerControlsVisibilityObserverImpl( - private val activity: Activity, -) : PlayerControlsVisibilityObserver { - - /** - * ID of the direct parent of player_control_play_pause_replay_button_touch_area, R.id.controls_button_group_layout - */ - private val controlsButtonGroupId = getIdentifier( - activity, - ResourceType.ID, - "controls_button_group_layout" - ) - - /** - * id of R.id.player_control_play_pause_replay_button_touch_area - */ - private val controlButtonId = getIdentifier( - activity, - ResourceType.ID, - "player_control_play_pause_replay_button_touch_area" - ) - - /** - * reference to the control button view - */ - private var controlButtonView = WeakReference(null) - - /** - * is the [controlButtonView] set to a valid reference of a view? - */ - private val isAttached: Boolean - get() { - val view = controlButtonView.get() - return view != null && view.parent != null - } - - /** - * find and attach the player_control_play_pause_replay_button_touch_area view if needed - */ - private fun maybeAttach() { - if (isAttached) return - - // find parent, then player_control_play_pause_replay_button_touch_area view - // this is needed because there may be two views where id=R.id.player_control_play_pause_replay_button_touch_area - // because why should google confine themselves to their own guidelines... - activity.findViewById(controlsButtonGroupId)?.let { parent -> - parent.findViewById(controlButtonId)?.let { - controlButtonView = WeakReference(it) - } - } - } - - override val playerControlsVisibility: Int - get() { - maybeAttach() - return controlButtonView.get()?.visibility ?: View.GONE - } - - override val arePlayerControlsVisible: Boolean - get() = playerControlsVisibility == View.VISIBLE -} - -/** - * provides the visibility status of the fullscreen player controls_layout view. - * this can be used for detecting when the player controls are shown - */ -interface PlayerControlsVisibilityObserver { - /** - * current visibility int of the controls_layout view - */ - val playerControlsVisibility: Int - - /** - * is the value of [playerControlsVisibility] equal to [View.VISIBLE]? - */ - val arePlayerControlsVisible: Boolean -} diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/shared/PlayerOverlays.kt b/extensions/youtube/src/main/java/app/morphe/extension/youtube/shared/PlayerOverlays.kt deleted file mode 100644 index ada50b69f4..0000000000 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/shared/PlayerOverlays.kt +++ /dev/null @@ -1,96 +0,0 @@ -package app.morphe.extension.youtube.shared - -import android.view.View -import android.view.ViewGroup -import app.morphe.extension.youtube.swipecontrols.misc.Rectangle - -/** - * hooking class for player overlays - */ -@Suppress("MemberVisibilityCanBePrivate") -object PlayerOverlays { - - /** - * called when the overlays finished inflating - */ - val onInflate = Event() - - /** - * called when new children are added or removed from the overlay - */ - val onChildrenChange = Event() - - /** - * called when the overlay layout changes - */ - val onLayoutChange = Event() - - /** - * start listening for events on the provided view group - * - * @param overlaysLayout the overlays view group - */ - @JvmStatic - fun attach(overlaysLayout: ViewGroup) { - onInflate.invoke(overlaysLayout) - overlaysLayout.setOnHierarchyChangeListener(object : - ViewGroup.OnHierarchyChangeListener { - override fun onChildViewAdded(parent: View?, child: View?) { - if (parent is ViewGroup && child is View) { - onChildrenChange( - ChildrenChangeEventArgs( - parent, - child, - false, - ), - ) - } - } - - override fun onChildViewRemoved(parent: View?, child: View?) { - if (parent is ViewGroup && child is View) { - onChildrenChange( - ChildrenChangeEventArgs( - parent, - child, - true, - ), - ) - } - } - }) - overlaysLayout.addOnLayoutChangeListener { view, newLeft, newTop, newRight, newBottom, oldLeft, oldTop, oldRight, oldBottom -> - if (view is ViewGroup) { - onLayoutChange( - LayoutChangeEventArgs( - view, - Rectangle( - oldLeft, - oldTop, - oldRight - oldLeft, - oldBottom - oldTop, - ), - Rectangle( - newLeft, - newTop, - newRight - newLeft, - newBottom - newTop, - ), - ), - ) - } - } - } -} - -data class ChildrenChangeEventArgs( - val overlaysLayout: ViewGroup, - val childView: View, - val wasChildRemoved: Boolean, -) - -data class LayoutChangeEventArgs( - val overlaysLayout: ViewGroup, - val oldRect: Rectangle, - val newRect: Rectangle, -) diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/shared/PlayerType.kt b/extensions/youtube/src/main/java/app/morphe/extension/youtube/shared/PlayerType.java similarity index 56% rename from extensions/youtube/src/main/java/app/morphe/extension/youtube/shared/PlayerType.kt rename to extensions/youtube/src/main/java/app/morphe/extension/youtube/shared/PlayerType.java index 17cda94b9a..19641ecebb 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/shared/PlayerType.kt +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/shared/PlayerType.java @@ -8,14 +8,17 @@ * See the included NOTICE file for GPLv3 Section 7 terms that apply to Morphe contributions. */ -package app.morphe.extension.youtube.shared +package app.morphe.extension.youtube.shared; -import app.morphe.extension.shared.Logger +import java.util.HashMap; +import java.util.Map; + +import app.morphe.extension.shared.Logger; /** * Regular player type. */ -enum class PlayerType { +public enum PlayerType { /** * Either no video, or a Short is playing. */ @@ -37,7 +40,7 @@ enum class PlayerType { WATCH_WHILE_SLIDING_MINIMIZED_MAXIMIZED, /** - * Player is either sliding to [HIDDEN] state because a Short was opened while a regular video is on screen. + * Player is either sliding to {@link #HIDDEN} state because a Short was opened while a regular video is on screen. * OR * The user has swiped a minimized player away to be closed (and no Short is being opened). */ @@ -49,91 +52,95 @@ enum class PlayerType { */ INLINE_MINIMAL, VIRTUAL_REALITY_FULLSCREEN, - WATCH_WHILE_PICTURE_IN_PICTURE, - ; + WATCH_WHILE_PICTURE_IN_PICTURE; + + private static final Map nameToPlayerType = new HashMap<>(); + + static { + for (PlayerType type : values()) { + nameToPlayerType.put(type.name(), type); + } + } + + /** + * player type change listener + */ + private static final Event onChange = new Event<>(); - companion object { + private static volatile PlayerType currentPlayerType = NONE; // Read/write from different threads. - private val nameToPlayerType = PlayerType.entries.associateBy { it.name } + public static Event getOnChange() { + return onChange; + } - @JvmStatic - fun setFromString(enumName: String) { - val newType = nameToPlayerType[enumName] - if (newType == null) { - Logger.printException { "Unknown PlayerType encountered: $enumName" } - } else { - current = newType - } + public static void setFromString(String enumName) { + PlayerType newType = nameToPlayerType.get(enumName); + if (newType == null) { + Logger.printException(() -> "Unknown PlayerType encountered: " + enumName); + } else { + setCurrent(newType); } + } - /** - * The current player type. - */ - @JvmStatic - var current - get() = currentPlayerType - private set(type) { - if (currentPlayerType != type) { - Logger.printDebug { "Changed to: $type" } - - currentPlayerType = type - onChange(type) - } - } - - @Volatile // Read/write from different threads. - private var currentPlayerType = NONE - - /** - * player type change listener - */ - @JvmStatic - val onChange = Event() + /** + * The current player type. + */ + public static PlayerType getCurrent() { + return currentPlayerType; + } + + private static void setCurrent(PlayerType type) { + if (currentPlayerType != type) { + Logger.printDebug(() -> "Changed to: " + type); + + currentPlayerType = type; + onChange.invoke(type); + } } /** - * Check if the current player type is [NONE] or [HIDDEN]. + * Check if the current player type is {@link #NONE} or {@link #HIDDEN}. * Useful to check if a short is currently playing. - * + *

* Does not include the first moment after a short is opened when a regular video is minimized on screen, * or while watching a short with a regular video present on a spoofed 16.x version of YouTube. - * To include those situations instead use [isNoneHiddenOrMinimized]. + * To include those situations instead use {@link #isNoneHiddenOrMinimized()}. */ - fun isNoneOrHidden(): Boolean { - return this == NONE || this == HIDDEN + public boolean isNoneOrHidden() { + return this == NONE || this == HIDDEN; } /** * Check if the current player type is - * [NONE], [HIDDEN], [WATCH_WHILE_SLIDING_MINIMIZED_DISMISSED]. - * + * {@link #NONE}, {@link #HIDDEN}, {@link #WATCH_WHILE_SLIDING_MINIMIZED_DISMISSED}. + *

* Useful to check if a Short is being played or opened. - * + *

* Usually covers all use cases with no false positives, except if called from some hooks * when spoofing to an old version this will return false even - * though a Short is being opened or is on screen (see [isNoneHiddenOrMinimized]). - * + * though a Short is being opened or is on screen (see {@link #isNoneHiddenOrMinimized()}). + *

* Instead of this method, consider using {@link ShortsPlayerState} * which may work better for some situations. * * @return If nothing, a Short, or a regular video is sliding off-screen to a dismissed or hidden state. * @see ShortsPlayerState */ - fun isNoneHiddenOrSlidingMinimized(): Boolean { - return isNoneOrHidden() || this == WATCH_WHILE_SLIDING_MINIMIZED_DISMISSED + public boolean isNoneHiddenOrSlidingMinimized() { + return isNoneOrHidden() || this == WATCH_WHILE_SLIDING_MINIMIZED_DISMISSED; } /** * Check if the current player type is - * [NONE], [HIDDEN], [WATCH_WHILE_MINIMIZED], [WATCH_WHILE_SLIDING_MINIMIZED_DISMISSED]. - * + * {@link #NONE}, {@link #HIDDEN}, {@link #WATCH_WHILE_MINIMIZED}, {@link #WATCH_WHILE_SLIDING_MINIMIZED_DISMISSED}. + *

* Useful to check if a Short is being played, * although it will return false positive if a regular video is * opened and minimized (and a Short is not playing or being opened). - * + *

* Typically used to detect if a Short is playing when the player cannot be in a minimized state, * such as the user interacting with a button or element of the player. - * + *

* Instead of this method, consider using {@link ShortsPlayerState} * which may work better for some situations. * @@ -141,12 +148,12 @@ fun isNoneHiddenOrSlidingMinimized(): Boolean { * a regular video is minimized (and a new video is not being opened). * @see ShortsPlayerState */ - fun isNoneHiddenOrMinimized(): Boolean { - return isNoneHiddenOrSlidingMinimized() || this == WATCH_WHILE_MINIMIZED + public boolean isNoneHiddenOrMinimized() { + return isNoneHiddenOrSlidingMinimized() || this == WATCH_WHILE_MINIMIZED; } - fun isMaximizedOrFullscreen(): Boolean { + public boolean isMaximizedOrFullscreen() { return this == WATCH_WHILE_MAXIMIZED || this == WATCH_WHILE_FULLSCREEN - || this == WATCH_WHILE_SLIDING_MAXIMIZED_FULLSCREEN + || this == WATCH_WHILE_SLIDING_MAXIMIZED_FULLSCREEN; } } diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/shared/ShortsPlayerState.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/shared/ShortsPlayerState.java new file mode 100644 index 0000000000..f9ea9b55a3 --- /dev/null +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/shared/ShortsPlayerState.java @@ -0,0 +1,35 @@ +package app.morphe.extension.youtube.shared; + +import app.morphe.extension.shared.Logger; + +/** + * Shorts player state. + */ +public final class ShortsPlayerState { + + private static volatile boolean open; + + /** + * Shorts player state change listener. + */ + private static final Event onChange = new Event<>(); + + public static void setOpen(boolean isOpen) { + if (open != isOpen) { + open = isOpen; + Logger.printDebug(() -> "ShortsPlayerState open changed to: " + isOpen); + onChange.invoke(isOpen); + } + } + + public static Event getOnChange() { + return onChange; + } + + /** + * If the Shorts player is currently open. + */ + public static boolean isOpen() { + return open; + } +} diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/shared/ShortsPlayerState.kt b/extensions/youtube/src/main/java/app/morphe/extension/youtube/shared/ShortsPlayerState.kt deleted file mode 100644 index 40ab08993c..0000000000 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/shared/ShortsPlayerState.kt +++ /dev/null @@ -1,37 +0,0 @@ -package app.morphe.extension.youtube.shared - -import app.morphe.extension.shared.Logger - -/** - * Shorts player state. - */ -class ShortsPlayerState { - companion object { - - @JvmStatic - fun setOpen(open: Boolean) { - if (isOpen != open) { - isOpen = open - Logger.printDebug { "ShortsPlayerState open changed to: $isOpen" } - onChange(open) - } - } - - @Volatile - private var isOpen = false - - /** - * Shorts player state change listener. - */ - @JvmStatic - val onChange = Event() - - /** - * If the Shorts player is currently open. - */ - @JvmStatic - fun isOpen(): Boolean { - return isOpen - } - } -} diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/shared/VideoState.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/shared/VideoState.java new file mode 100644 index 0000000000..31c898080a --- /dev/null +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/shared/VideoState.java @@ -0,0 +1,73 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches + * + * Original hard forked code: + * https://github.com/ReVanced/revanced-patches/commit/724e6d61b2ecd868c1a9a37d465a688e83a74799 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to Morphe contributions. + */ + +package app.morphe.extension.youtube.shared; + +import androidx.annotation.Nullable; + +import java.util.HashMap; +import java.util.Map; + +import app.morphe.extension.shared.Logger; +import app.morphe.extension.youtube.patches.VideoInformation; + +/** + * VideoState playback state. + */ +public enum VideoState { + NEW, + PLAYING, + PAUSED, + RECOVERABLE_ERROR, + UNRECOVERABLE_ERROR, + /** + * @see VideoInformation#isAtEndOfVideo() + */ + ENDED; + + private static final Map nameToVideoState; + + static { + VideoState[] values = values(); + nameToVideoState = new HashMap<>(2 * values.length); + for (VideoState state : values) { + nameToVideoState.put(state.name(), state); + } + } + + private static final Event onChange = new Event<>(); + + @Nullable + private static volatile VideoState currentVideoState; // Read/write from different threads. + + public static Event getOnChange() { + return onChange; + } + + public static void setFromString(String enumName) { + VideoState state = nameToVideoState.get(enumName); + if (state == null) { + Logger.printException(() -> "Unknown VideoState encountered: " + enumName); + } else if (currentVideoState != state) { + Logger.printDebug(() -> "VideoState changed to: " + state); + currentVideoState = state; + onChange.invoke(state); + } + } + + /** + * Depending on which hook this is called from, + * this value may not be up to date with the actual playback state. + */ + @Nullable + public static VideoState getCurrent() { + return currentVideoState; + } +} diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/shared/VideoState.kt b/extensions/youtube/src/main/java/app/morphe/extension/youtube/shared/VideoState.kt deleted file mode 100644 index 5b71440b9f..0000000000 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/shared/VideoState.kt +++ /dev/null @@ -1,53 +0,0 @@ -package app.morphe.extension.youtube.shared - -import app.morphe.extension.shared.Logger -import app.morphe.extension.youtube.patches.VideoInformation - -/** - * VideoState playback state. - */ -enum class VideoState { - NEW, - PLAYING, - PAUSED, - RECOVERABLE_ERROR, - UNRECOVERABLE_ERROR, - /** - * @see [VideoInformation.isAtEndOfVideo] - */ - ENDED; - - companion object { - - private val nameToVideoState = VideoState.entries.associateBy { it.name } - - @JvmStatic - val onChange = Event() - - @JvmStatic - fun setFromString(enumName: String) { - val state = nameToVideoState[enumName] - if (state == null) { - Logger.printException { "Unknown VideoState encountered: $enumName" } - } else if (currentVideoState != state) { - Logger.printDebug { "VideoState changed to: $state" } - currentVideoState = state - onChange(state) - } - } - - /** - * Depending on which hook this is called from, - * this value may not be up to date with the actual playback state. - */ - @JvmStatic - var current: VideoState? - get() = currentVideoState - private set(value) { - currentVideoState = value - } - - @Volatile // Read/write from different threads. - private var currentVideoState: VideoState? = null - } -} diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/sponsorblock/YouTubeSponsorBlockConfig.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/sponsorblock/YouTubeSponsorBlockConfig.java index 2f89a127f4..a5ac1dcca1 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/sponsorblock/YouTubeSponsorBlockConfig.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/sponsorblock/YouTubeSponsorBlockConfig.java @@ -40,7 +40,6 @@ import app.morphe.extension.youtube.sponsorblock.ui.SponsorBlockViewController; import app.morphe.extension.youtube.whitelist.ChannelWhitelist; import app.morphe.extension.youtube.whitelist.WhitelistType; -import kotlin.Unit; /** * YouTube-side stub class for the SponsorBlock injection points and {@link Configuration}. @@ -64,12 +63,10 @@ public static void install() { PlayerType.getOnChange().addObserver((PlayerType type) -> { SegmentPlaybackController.onPlayerTypeChanged(); - return Unit.INSTANCE; }); VideoInformation.onChannelIdChange.addObserver((String channelId) -> { SegmentPlaybackController.onChannelIdChange(channelId); - return Unit.INSTANCE; }); // Mirrors the side effects the legacy SponsorBlockPreferenceGroup applied from its diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/sponsorblock/ui/SponsorBlockViewController.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/sponsorblock/ui/SponsorBlockViewController.java index 7d2eff49ab..bba0349f75 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/sponsorblock/ui/SponsorBlockViewController.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/sponsorblock/ui/SponsorBlockViewController.java @@ -21,7 +21,6 @@ import app.morphe.extension.youtube.shared.PlayerType; import app.morphe.extension.youtube.videoplayer.LegacyPlayerControlButton; import app.morphe.extension.youtube.videoplayer.LegacyPlayerControlButton.ButtonVisibility; -import kotlin.Unit; public class SponsorBlockViewController { public static final int ROUNDED_LAYOUT_MARGIN = 12; @@ -41,7 +40,6 @@ public class SponsorBlockViewController { static { PlayerType.getOnChange().addObserver((PlayerType type) -> { playerTypeChanged(type); - return Unit.INSTANCE; }); } diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/SwipeControlsConfigurationProvider.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/SwipeControlsConfigurationProvider.java new file mode 100644 index 0000000000..449a0d5b1f --- /dev/null +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/SwipeControlsConfigurationProvider.java @@ -0,0 +1,562 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches + * + * Original hard forked code: + * https://github.com/ReVanced/revanced-patches/commit/724e6d61b2ecd868c1a9a37d465a688e83a74799 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to Morphe contributions. + */ + +package app.morphe.extension.youtube.swipecontrols; + +import static app.morphe.extension.shared.StringRef.str; + +import android.graphics.Color; +import android.view.MotionEvent; + +import java.util.List; + +import app.morphe.extension.shared.Logger; +import app.morphe.extension.shared.Utils; +import app.morphe.extension.shared.settings.Setting; +import app.morphe.extension.shared.settings.StringSetting; +import app.morphe.extension.shared.settings.preference.SeekBarPreference; +import app.morphe.extension.youtube.settings.Settings; +import app.morphe.extension.youtube.shared.PlayerType; +import app.morphe.extension.youtube.swipecontrols.controller.gesture.ClassicSwipeController; +import app.morphe.extension.youtube.swipecontrols.controller.gesture.PressToSwipeController; +import app.morphe.extension.youtube.swipecontrols.controller.gesture.core.BaseGestureController; + +/** + * Provides configuration settings for the swipe controls in the YouTube player. + * Manages the action of each zone, overlay appearance, and behavior preferences. + */ +public final class SwipeControlsConfigurationProvider { + //region zone actions + /** + * The action performed by a swipe zone. + */ + public enum SwipeZoneAction { + OFF, + VOLUME, + BRIGHTNESS, + SPEED + } + + /** + * Availability based on any zone having an action assigned. + */ + public static class AnySwipeZoneAvailability implements Setting.Availability { + @Override + public boolean isAvailable() { + return Settings.SWIPE_LEFT_ZONE.get() != SwipeZoneAction.OFF + || Settings.SWIPE_RIGHT_ZONE.get() != SwipeZoneAction.OFF + || Settings.SWIPE_TOP_ZONE.get() != SwipeZoneAction.OFF; + } + + @Override + public List> getParentSettings() { + return List.of(Settings.SWIPE_LEFT_ZONE, Settings.SWIPE_RIGHT_ZONE, Settings.SWIPE_TOP_ZONE); + } + } + + /** + * Availability based on either side zone having an action assigned. + */ + public static class SideSwipeZonesAvailability implements Setting.Availability { + @Override + public boolean isAvailable() { + return Settings.SWIPE_LEFT_ZONE.get() != SwipeZoneAction.OFF + || Settings.SWIPE_RIGHT_ZONE.get() != SwipeZoneAction.OFF; + } + + @Override + public List> getParentSettings() { + return List.of(Settings.SWIPE_LEFT_ZONE, Settings.SWIPE_RIGHT_ZONE); + } + } + + /** + * Availability based on the top zone having an action assigned. + */ + public static class TopSwipeZoneAvailability implements Setting.Availability { + @Override + public boolean isAvailable() { + return Settings.SWIPE_TOP_ZONE.get() != SwipeZoneAction.OFF; + } + + @Override + public List> getParentSettings() { + return List.of(Settings.SWIPE_TOP_ZONE); + } + } + + /** + * Availability based on any zone being assigned the given action. + */ + public static class SwipeActionAvailability implements Setting.Availability { + private final SwipeZoneAction action; + + public SwipeActionAvailability(SwipeZoneAction action) { + this.action = action; + } + + @Override + public boolean isAvailable() { + return Settings.SWIPE_LEFT_ZONE.get() == action + || Settings.SWIPE_RIGHT_ZONE.get() == action + || Settings.SWIPE_TOP_ZONE.get() == action; + } + + @Override + public List> getParentSettings() { + return List.of(Settings.SWIPE_LEFT_ZONE, Settings.SWIPE_RIGHT_ZONE, Settings.SWIPE_TOP_ZONE); + } + } + //endregion + + //region swipe enable + /** + * Indicates whether swipe controls are enabled globally. + * Returns true if either volume or brightness controls are enabled and the video is in fullscreen or multi-window mode. + */ + public boolean isSwipeControlsEnabled() { + return (isVolumeControlEnabled() || isBrightnessControlEnabled() || isSpeedControlEnabled()) + && (isFullscreenOrMultiWindowVideo() || isVideoSliding()); + } + + public SwipeZoneAction getLeftZoneAction() { + return Settings.SWIPE_LEFT_ZONE.get(); + } + + public SwipeZoneAction getRightZoneAction() { + return Settings.SWIPE_RIGHT_ZONE.get(); + } + + public SwipeZoneAction getTopZoneAction() { + return Settings.SWIPE_TOP_ZONE.get(); + } + + /** + * Indicates whether any zone is assigned the given action. + */ + private boolean isActionAssigned(SwipeZoneAction action) { + return getLeftZoneAction() == action || getRightZoneAction() == action || getTopZoneAction() == action; + } + + /** + * Indicates whether swipe controls for adjusting volume are enabled. + */ + public boolean isVolumeControlEnabled() { + return isActionAssigned(SwipeZoneAction.VOLUME); + } + + /** + * Indicates whether swipe controls for adjusting brightness are enabled. + */ + public boolean isBrightnessControlEnabled() { + return isActionAssigned(SwipeZoneAction.BRIGHTNESS); + } + + /** + * Checks if the video player is currently in fullscreen mode. + */ + public boolean isFullscreenVideo() { + return PlayerType.getCurrent() == PlayerType.WATCH_WHILE_FULLSCREEN; + } + + /** + * Checks if the video player is currently in split screen / multi-window mode. + */ + public boolean isMultiWindowVideo() { + SwipeControlsHostActivity host = SwipeControlsHostActivity.getCurrentHost().get(); + if (host == null || !host.isInSplitScreenMode()) { + return false; + } + PlayerType current = PlayerType.getCurrent(); + return current == PlayerType.WATCH_WHILE_FULLSCREEN || current == PlayerType.WATCH_WHILE_MAXIMIZED; + } + + /** + * Checks if the video player is in fullscreen or multi-window mode. + */ + public boolean isFullscreenOrMultiWindowVideo() { + return isFullscreenVideo() || isMultiWindowVideo(); + } + + /** + * Checks if the video player is currently in sliding mode. + *

+ * The swipe control patch hooks functions of MainActivity (top-level activity) to detect {@link MotionEvent}. + * Although the player is already in the fullscreen, a {@link MotionEvent} is detected before the {@link PlayerType} is updated, + * so the current {@link PlayerType} may be {@link PlayerType#WATCH_WHILE_SLIDING_MAXIMIZED_FULLSCREEN}. + *

+ * In this case, {@link BaseGestureController#submitTouchEvent} cancels the MotionEvent, + * but sometimes the canceled {@link MotionEvent} triggers the tap and hold playback speed. + * See: https://github.com/MorpheApp/morphe-patches/issues/658. + *

+ * To resolve this concurrency issue, pass the {@link MotionEvent} even when the player type is + * {@link PlayerType#WATCH_WHILE_SLIDING_MAXIMIZED_FULLSCREEN}, and finally validate the swipe gesture + * in {@link ClassicSwipeController} and {@link PressToSwipeController} onSwipe(). + */ + public boolean isVideoSliding() { + return PlayerType.getCurrent() == PlayerType.WATCH_WHILE_SLIDING_MAXIMIZED_FULLSCREEN; + } + //endregion + + //region keys enable + /** + * Indicates whether volume key controls should be overridden by swipe controls. + * Returns true if volume controls are enabled and the video is in fullscreen or multi-window mode. + */ + public boolean shouldOverwriteVolumeKeyControls() { + return isVolumeControlEnabled() && isFullscreenOrMultiWindowVideo(); + } + //endregion + + //region gesture adjustments + /** + * Indicates whether swipe gestures should be ignored while the native lock screen is engaged. + */ + public boolean shouldIgnoreSwipesWhenLocked() { + return Settings.SWIPE_IGNORE_WHEN_LOCKED.get(); + } + + /** + * Indicates whether press-to-swipe mode is enabled, requiring a press before swiping to activate controls. + */ + private final boolean shouldEnablePressToSwipe = Settings.SWIPE_PRESS_TO_ENGAGE.get(); + + public boolean shouldEnablePressToSwipe() { + return shouldEnablePressToSwipe; + } + + /** + * The threshold for detecting swipe gestures, in pixels. + * Loaded once to ensure consistent behavior during rapid scroll events. + */ + private final int swipeMagnitudeThreshold = Settings.SWIPE_MAGNITUDE_THRESHOLD.get(); + + public int getSwipeMagnitudeThreshold() { + return swipeMagnitudeThreshold; + } + + /** + * The swipe distance of a single volume step, in dp. + * Resets to default if set to 0, as it would disable swiping. + */ + public int getVolumeSwipeDistance() { + final int distance = Settings.SWIPE_VOLUME_DISTANCE.get(); + + if (distance < 1) { + return Settings.SWIPE_VOLUME_DISTANCE.resetToDefault(); + } + + return distance; + } + + /** + * Number of steps the volume range is divided into. + * Devices that expose a fine-grained volume stream (such as 150 indices) otherwise + * adjust the volume by a tenth of what the device volume UI does. + */ + @SuppressWarnings("unused") + public enum SwipeVolumeSteps { + DEVICE_DEFAULT(0), + STEPS_5(5), + STEPS_10(10), + STEPS_15(15), + STEPS_20(20), + STEPS_30(30), + STEPS_50(50); + + private final int steps; + + SwipeVolumeSteps(int steps) { + this.steps = steps; + } + + public int getSteps() { + return steps; + } + } + + /** + * The number of volume steps to use, or 0 to use the raw steps of the device volume stream. + */ + public int getVolumeStepCount() { + return Settings.SWIPE_VOLUME_STEPS.get().getSteps(); + } + + /** + * The volume change of a single step, in stream index units. + * + * @param maxVolume The number of indices the device volume stream has. + * @param stepCount The number of steps to split the stream into, or 0 for the raw indices. + */ + public static int volumeStepSize(int maxVolume, int stepCount) { + return stepCount < 1 ? 1 : Math.max(1, maxVolume / stepCount); + } + + /** + * The sensitivity of brightness swipe gestures, determining how much brightness changes per swipe. + * Resets to default if set to 0, as it would disable swiping. + */ + public int getBrightnessSwipeSensitivity() { + final int sensitivity = Settings.SWIPE_BRIGHTNESS_SENSITIVITY.get(); + + if (sensitivity < 1) { + return Settings.SWIPE_BRIGHTNESS_SENSITIVITY.resetToDefault(); + } + + return sensitivity; + } + + /** + * Indicates whether the swipe gesture for playback speed control is enabled in any zone. + */ + public boolean isSpeedControlEnabled() { + return isActionAssigned(SwipeZoneAction.SPEED); + } + + /** + * The sensitivity of speed swipe gestures, controlling how much physical movement is needed per step. + * Resets to default if below 1 to guard against direct SharedPreferences manipulation. + */ + public int getSpeedSwipeSensitivity() { + final int sensitivity = Settings.SWIPE_SPEED_SENSITIVITY.get(); + + if (sensitivity < 1) { + return Settings.SWIPE_SPEED_SENSITIVITY.resetToDefault(); + } + + return sensitivity; + } + + /** + * Playback speed change per tick, expressed as an integer multiplied by 100 (e.g. 5 = 0.05x). + */ + public int getSpeedStepInt() { + return Settings.SWIPE_SPEED_STEP.get().getStepInt(); + } + //endregion + + //region overlay adjustments + /** + * The background color used for the filled portion of the progress bar in the overlay. + */ + public static final int OVERLAY_FILL_BACKGROUND_COLOR = 0x80D3D3D3; + + /** + * The color used for text and icons in the overlay. + */ + public static final int OVERLAY_TEXT_COLOR = Color.WHITE; + + /** + * Indicates whether haptic feedback should be enabled for swipe control interactions. + */ + private final boolean shouldEnableHapticFeedback = Settings.SWIPE_HAPTIC_FEEDBACK.get(); + + public boolean shouldEnableHapticFeedback() { + return shouldEnableHapticFeedback; + } + + /** + * The duration in milliseconds that the overlay should remain visible after a change. + */ + private final long overlayShowTimeoutMillis = Settings.SWIPE_OVERLAY_TIMEOUT.get(); + + public long getOverlayShowTimeoutMillis() { + return overlayShowTimeoutMillis; + } + + /** + * The background opacity of the overlay, converted from a percentage (0-100) to an alpha value (0-255). + */ + public int getOverlayBackgroundOpacity() { + final int opacity = SeekBarPreference.clampToRange(Settings.SWIPE_OVERLAY_OPACITY) * 255 / 100; + return Color.argb(opacity, 0, 0, 0); + } + + /** + * The color of the progress bar in the overlay for brightness. + * Resets to default and shows a toast if the color string is invalid or empty. + */ + public int getOverlayBrightnessProgressColor() { + return getSettingColor(Settings.SWIPE_OVERLAY_BRIGHTNESS_COLOR); + } + + /** + * The color of the progress bar in the overlay for volume. + * Resets to default and shows a toast if the color string is invalid or empty. + */ + public int getOverlayVolumeProgressColor() { + return getSettingColor(Settings.SWIPE_OVERLAY_VOLUME_COLOR); + } + + /** + * The color of the progress indicator in the overlay for playback speed. + */ + public int getOverlaySpeedProgressColor() { + return getSettingColor(Settings.SWIPE_OVERLAY_SPEED_COLOR); + } + + private static int getSettingColor(StringSetting setting) { + try { + return Color.parseColor(setting.get()); + } catch (IllegalArgumentException ex) { + // This code should never be reached. + // Color picker rejects and will not save bad colors to a setting. + // If a user imports bad data, the color picker preference resets the + // bad color before this method can be called. + Logger.printDebug(() -> "Could not parse color: " + setting, ex); + Utils.showToastLong(str("morphe_settings_color_invalid")); + setting.resetToDefault(); + return getSettingColor(setting); // Recursively return. + } + } + + /** + * The text size in the overlay, in density-independent pixels (dp). + */ + public int getOverlayTextSize() { + return SeekBarPreference.clampToRange(Settings.SWIPE_OVERLAY_TEXT_SIZE); + } + + /** + * Defines the style of the swipe controls overlay, determining its layout and appearance. + */ + @SuppressWarnings("unused") + public enum SwipeOverlayStyle { + /** + * A full horizontal progress bar with detailed indicators. + */ + HORIZONTAL(false, false, false, false), + + /** + * A minimal horizontal progress bar positioned at the top. + */ + HORIZONTAL_MINIMAL_TOP(true, false, false, false), + + /** + * A minimal horizontal progress bar centered vertically. + */ + HORIZONTAL_MINIMAL_CENTER(true, true, false, false), + + /** + * A full circular progress bar with detailed indicators. + */ + CIRCULAR(false, false, true, false), + + /** + * A minimal circular progress bar. + */ + CIRCULAR_MINIMAL(true, false, true, false), + + /** + * A full vertical progress bar with detailed indicators. + */ + VERTICAL(false, false, false, true), + + /** + * A minimal vertical progress bar. + */ + VERTICAL_MINIMAL(true, false, false, true); + + private final boolean isMinimal; + private final boolean isHorizontalMinimalCenter; + private final boolean isCircular; + private final boolean isVertical; + + /** + * @param isMinimal Indicates whether the style is minimalistic, omitting detailed progress indicators. + * @param isHorizontalMinimalCenter Indicates whether the style is a minimal horizontal bar centered vertically. + * @param isCircular Indicates whether the style uses a circular progress bar. + * @param isVertical Indicates whether the style uses a vertical progress bar. + */ + SwipeOverlayStyle(boolean isMinimal, boolean isHorizontalMinimalCenter, boolean isCircular, boolean isVertical) { + this.isMinimal = isMinimal; + this.isHorizontalMinimalCenter = isHorizontalMinimalCenter; + this.isCircular = isCircular; + this.isVertical = isVertical; + } + + public boolean isMinimal() { + return isMinimal; + } + + public boolean isHorizontalMinimalCenter() { + return isHorizontalMinimalCenter; + } + + public boolean isCircular() { + return isCircular; + } + + public boolean isVertical() { + return isVertical; + } + } + + /** + * The current style of the overlay, determining its layout and appearance. + */ + private final SwipeOverlayStyle overlayStyle = Settings.SWIPE_OVERLAY_STYLE.get(); + + public SwipeOverlayStyle getOverlayStyle() { + return overlayStyle; + } + + /** + * Playback speed change per tick, expressed as an integer multiplied by 100 (e.g. 5 = 0.05x). + */ + @SuppressWarnings("unused") + public enum SwipeSpeedStep { + STEP_005(5), + STEP_010(10), + STEP_025(25); + + private final int stepInt; + + SwipeSpeedStep(int stepInt) { + this.stepInt = stepInt; + } + + public int getStepInt() { + return stepInt; + } + } + //endregion + + //region behavior + /** + * Indicates whether the brightness level should be saved and restored when entering or exiting fullscreen mode. + */ + private final boolean shouldSaveAndRestoreBrightness = Settings.SWIPE_SAVE_AND_RESTORE_BRIGHTNESS.get(); + + public boolean shouldSaveAndRestoreBrightness() { + return shouldSaveAndRestoreBrightness; + } + + /** + * Indicates whether auto-brightness should be enabled when the brightness gesture reaches its lowest value. + */ + private final boolean shouldLowestValueEnableAutoBrightness = Settings.SWIPE_LOWEST_VALUE_ENABLE_AUTO_BRIGHTNESS.get(); + + public boolean shouldLowestValueEnableAutoBrightness() { + return shouldLowestValueEnableAutoBrightness; + } + + /** + * The saved brightness value for the swipe gesture, used to restore brightness in fullscreen mode. + */ + public float getSavedScreenBrightnessValue() { + return Settings.SWIPE_BRIGHTNESS_VALUE.get(); + } + + public void setSavedScreenBrightnessValue(float value) { + Settings.SWIPE_BRIGHTNESS_VALUE.save(value); + } + //endregion +} diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/SwipeControlsConfigurationProvider.kt b/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/SwipeControlsConfigurationProvider.kt deleted file mode 100644 index b93309763c..0000000000 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/SwipeControlsConfigurationProvider.kt +++ /dev/null @@ -1,440 +0,0 @@ -/* - * Copyright 2026 Morphe. - * https://github.com/MorpheApp/morphe-patches - * - * Original hard forked code: - * https://github.com/ReVanced/revanced-patches/commit/724e6d61b2ecd868c1a9a37d465a688e83a74799 - * - * See the included NOTICE file for GPLv3 Section 7 terms that apply to Morphe contributions. - */ - -package app.morphe.extension.youtube.swipecontrols - -import android.graphics.Color -import android.view.MotionEvent -import app.morphe.extension.shared.Logger -import app.morphe.extension.shared.StringRef.str -import app.morphe.extension.shared.Utils -import app.morphe.extension.shared.settings.Setting -import app.morphe.extension.shared.settings.StringSetting -import app.morphe.extension.shared.settings.preference.SeekBarPreference -import app.morphe.extension.youtube.settings.Settings -import app.morphe.extension.youtube.shared.PlayerType -import app.morphe.extension.youtube.swipecontrols.controller.gesture.ClassicSwipeController -import app.morphe.extension.youtube.swipecontrols.controller.gesture.PressToSwipeController -import app.morphe.extension.youtube.swipecontrols.controller.gesture.core.BaseGestureController - -/** - * Provides configuration settings for the swipe controls in the YouTube player. - * Manages the action of each zone, overlay appearance, and behavior preferences. - */ -class SwipeControlsConfigurationProvider { - //region zone actions - /** - * The action performed by a swipe zone. - */ - enum class SwipeZoneAction { - OFF, - VOLUME, - BRIGHTNESS, - SPEED, - } - - /** - * Availability based on any zone having an action assigned. - */ - class AnySwipeZoneAvailability : Setting.Availability { - override fun isAvailable() = - Settings.SWIPE_LEFT_ZONE.get() != SwipeZoneAction.OFF || - Settings.SWIPE_RIGHT_ZONE.get() != SwipeZoneAction.OFF || - Settings.SWIPE_TOP_ZONE.get() != SwipeZoneAction.OFF - - override fun getParentSettings() = - listOf>(Settings.SWIPE_LEFT_ZONE, Settings.SWIPE_RIGHT_ZONE, Settings.SWIPE_TOP_ZONE) - } - - /** - * Availability based on either side zone having an action assigned. - */ - class SideSwipeZonesAvailability : Setting.Availability { - override fun isAvailable() = - Settings.SWIPE_LEFT_ZONE.get() != SwipeZoneAction.OFF || - Settings.SWIPE_RIGHT_ZONE.get() != SwipeZoneAction.OFF - - override fun getParentSettings() = - listOf>(Settings.SWIPE_LEFT_ZONE, Settings.SWIPE_RIGHT_ZONE) - } - - /** - * Availability based on the top zone having an action assigned. - */ - class TopSwipeZoneAvailability : Setting.Availability { - override fun isAvailable() = Settings.SWIPE_TOP_ZONE.get() != SwipeZoneAction.OFF - - override fun getParentSettings() = listOf>(Settings.SWIPE_TOP_ZONE) - } - - /** - * Availability based on any zone being assigned the given action. - */ - class SwipeActionAvailability(private val action: SwipeZoneAction) : Setting.Availability { - override fun isAvailable() = - Settings.SWIPE_LEFT_ZONE.get() == action || - Settings.SWIPE_RIGHT_ZONE.get() == action || - Settings.SWIPE_TOP_ZONE.get() == action - - override fun getParentSettings() = - listOf>(Settings.SWIPE_LEFT_ZONE, Settings.SWIPE_RIGHT_ZONE, Settings.SWIPE_TOP_ZONE) - } - //endregion - - //region swipe enable - /** - * Indicates whether swipe controls are enabled globally. - * Returns true if either volume or brightness controls are enabled and the video is in fullscreen or multi-window mode. - */ - val enableSwipeControls: Boolean - get() = (enableVolumeControls || enableBrightnessControl || enableSpeedGestureControl) && - (isFullscreenOrMultiWindowVideo || isVideoSliding) - - val leftZoneAction: SwipeZoneAction - get() = Settings.SWIPE_LEFT_ZONE.get() - - val rightZoneAction: SwipeZoneAction - get() = Settings.SWIPE_RIGHT_ZONE.get() - - val topZoneAction: SwipeZoneAction - get() = Settings.SWIPE_TOP_ZONE.get() - - /** - * Indicates whether any zone is assigned the given action. - */ - private fun isActionAssigned(action: SwipeZoneAction) = - leftZoneAction == action || rightZoneAction == action || topZoneAction == action - - /** - * Indicates whether swipe controls for adjusting volume are enabled. - */ - val enableVolumeControls: Boolean - get() = isActionAssigned(SwipeZoneAction.VOLUME) - - /** - * Indicates whether swipe controls for adjusting brightness are enabled. - */ - val enableBrightnessControl: Boolean - get() = isActionAssigned(SwipeZoneAction.BRIGHTNESS) - - /** - * Checks if the video player is currently in fullscreen mode. - */ - val isFullscreenVideo: Boolean - get() = PlayerType.current == PlayerType.WATCH_WHILE_FULLSCREEN - - /** - * Checks if the video player is currently in split screen / multi-window mode. - */ - val isMultiWindowVideo: Boolean - get() = (SwipeControlsHostActivity.currentHost.get()?.isInSplitScreenMode == true) && - (PlayerType.current == PlayerType.WATCH_WHILE_FULLSCREEN || PlayerType.current == PlayerType.WATCH_WHILE_MAXIMIZED) - - /** - * Checks if the video player is in fullscreen or multi-window mode. - */ - val isFullscreenOrMultiWindowVideo: Boolean - get() = isFullscreenVideo || isMultiWindowVideo - - /** - * Checks if the video player is currently in sliding mode. - * - * The swipe control patch hooks functions of MainActivity (top-level activity) to detect [MotionEvent]. - * Although the player is already in the fullscreen, a [MotionEvent] is detected before the [PlayerType] is updated, - * so the current [PlayerType] may be [PlayerType.WATCH_WHILE_SLIDING_MAXIMIZED_FULLSCREEN]. - * - * In this case, [BaseGestureController.submitTouchEvent] cancels the MotionEvent, - * but sometimes the canceled [MotionEvent] triggers the tap and hold playback speed. - * See: https://github.com/MorpheApp/morphe-patches/issues/658. - * - * To resolve this concurrency issue, pass the [MotionEvent] even when the player type is [PlayerType.WATCH_WHILE_SLIDING_MAXIMIZED_FULLSCREEN], - * and finally validate the swipe gesture in [ClassicSwipeController.onSwipe] and [PressToSwipeController.onSwipe]. - */ - val isVideoSliding: Boolean - get() = PlayerType.current == PlayerType.WATCH_WHILE_SLIDING_MAXIMIZED_FULLSCREEN - //endregion - - //region keys enable - /** - * Indicates whether volume key controls should be overridden by swipe controls. - * Returns true if volume controls are enabled and the video is in fullscreen or multi-window mode. - */ - val overwriteVolumeKeyControls: Boolean - get() = enableVolumeControls && isFullscreenOrMultiWindowVideo - //endregion - - //region gesture adjustments - /** - * Indicates whether swipe gestures should be ignored while the native lock screen is engaged. - */ - val shouldIgnoreSwipesWhenLocked: Boolean - get() = Settings.SWIPE_IGNORE_WHEN_LOCKED.get() - - /** - * Indicates whether press-to-swipe mode is enabled, requiring a press before swiping to activate controls. - */ - val shouldEnablePressToSwipe = Settings.SWIPE_PRESS_TO_ENGAGE.get() - - /** - * The threshold for detecting swipe gestures, in pixels. - * Loaded once to ensure consistent behavior during rapid scroll events. - */ - val swipeMagnitudeThreshold = Settings.SWIPE_MAGNITUDE_THRESHOLD.get() - - /** - * The swipe distance of a single volume step, in dp. - * Resets to default if set to 0, as it would disable swiping. - */ - val volumeSwipeDistance: Int - get() { - val distance = Settings.SWIPE_VOLUME_DISTANCE.get() - - if (distance < 1) { - return Settings.SWIPE_VOLUME_DISTANCE.resetToDefault() - } - - return distance - } - - /** - * Number of steps the volume range is divided into. - * Devices that expose a fine-grained volume stream (such as 150 indices) otherwise - * adjust the volume by a tenth of what the device volume UI does. - */ - @Suppress("unused") - enum class SwipeVolumeSteps(val steps: Int) { - DEVICE_DEFAULT(0), - STEPS_5(5), - STEPS_10(10), - STEPS_15(15), - STEPS_20(20), - STEPS_30(30), - STEPS_50(50), - } - - /** - * The number of volume steps to use, or 0 to use the raw steps of the device volume stream. - */ - val volumeStepCount: Int - get() = Settings.SWIPE_VOLUME_STEPS.get().steps - - companion object { - /** - * The volume change of a single step, in stream index units. - * - * @param maxVolume The number of indices the device volume stream has. - * @param stepCount The number of steps to split the stream into, or 0 for the raw indices. - */ - @JvmStatic - fun volumeStepSize(maxVolume: Int, stepCount: Int) = - if (stepCount < 1) 1 else maxOf(1, maxVolume / stepCount) - } - - /** - * The sensitivity of brightness swipe gestures, determining how much brightness changes per swipe. - * Resets to default if set to 0, as it would disable swiping. - */ - val brightnessSwipeSensitivity: Int - get() { - val sensitivity = Settings.SWIPE_BRIGHTNESS_SENSITIVITY.get() - - if (sensitivity < 1) { - return Settings.SWIPE_BRIGHTNESS_SENSITIVITY.resetToDefault() - } - - return sensitivity - } - - /** - * Indicates whether the swipe gesture for playback speed control is enabled in any zone. - */ - val enableSpeedGestureControl: Boolean - get() = isActionAssigned(SwipeZoneAction.SPEED) - - /** - * The sensitivity of speed swipe gestures, controlling how much physical movement is needed per step. - * Resets to default if below 1 to guard against direct SharedPreferences manipulation. - */ - val speedSwipeSensitivity: Int - get() { - val sensitivity = Settings.SWIPE_SPEED_SENSITIVITY.get() - - if (sensitivity < 1) { - return Settings.SWIPE_SPEED_SENSITIVITY.resetToDefault() - } - - return sensitivity - } - - /** - * Playback speed change per tick, expressed as an integer multiplied by 100 (e.g. 5 = 0.05x). - */ - val speedStepInt: Int - get() = Settings.SWIPE_SPEED_STEP.get().stepInt - - //endregion - - //region overlay adjustments - /** - * Indicates whether haptic feedback should be enabled for swipe control interactions. - */ - val shouldEnableHapticFeedback = Settings.SWIPE_HAPTIC_FEEDBACK.get() - - /** - * The duration in milliseconds that the overlay should remain visible after a change. - */ - val overlayShowTimeoutMillis = Settings.SWIPE_OVERLAY_TIMEOUT.get() - - /** - * The background opacity of the overlay, converted from a percentage (0-100) to an alpha value (0-255). - */ - val overlayBackgroundOpacity: Int - get() { - val opacity = SeekBarPreference.clampToRange(Settings.SWIPE_OVERLAY_OPACITY) * 255 / 100 - return Color.argb(opacity, 0, 0, 0) - } - - /** - * The color of the progress bar in the overlay for brightness. - * Resets to default and shows a toast if the color string is invalid or empty. - */ - val overlayBrightnessProgressColor: Int - get() = getSettingColor(Settings.SWIPE_OVERLAY_BRIGHTNESS_COLOR) - - /** - * The color of the progress bar in the overlay for volume. - * Resets to default and shows a toast if the color string is invalid or empty. - */ - val overlayVolumeProgressColor: Int - get() = getSettingColor(Settings.SWIPE_OVERLAY_VOLUME_COLOR) - - private fun getSettingColor(setting: StringSetting): Int { - return try { - Color.parseColor(setting.get()) - } catch (ex: IllegalArgumentException) { - // This code should never be reached. - // Color picker rejects and will not save bad colors to a setting. - // If a user imports bad data, the color picker preference resets the - // bad color before this method can be called. - Logger.printDebug({ "Could not parse color: $setting" }, ex) - Utils.showToastLong(str("morphe_settings_color_invalid")) - setting.resetToDefault() - return getSettingColor(setting) // Recursively return. - } - } - - /** - * The color of the progress indicator in the overlay for playback speed. - */ - val overlaySpeedProgressColor: Int - get() = getSettingColor(Settings.SWIPE_OVERLAY_SPEED_COLOR) - - /** - * The background color used for the filled portion of the progress bar in the overlay. - */ - val overlayFillBackgroundPaint = 0x80D3D3D3.toInt() - - /** - * The color used for text and icons in the overlay. - */ - val overlayTextColor = Color.WHITE - - /** - * The text size in the overlay, in density-independent pixels (dp). - */ - val overlayTextSize: Int - get() = SeekBarPreference.clampToRange(Settings.SWIPE_OVERLAY_TEXT_SIZE) - - /** - * Defines the style of the swipe controls overlay, determining its layout and appearance. - * - * @property isMinimal Indicates whether the style is minimalistic, omitting detailed progress indicators. - * @property isHorizontalMinimalCenter Indicates whether the style is a minimal horizontal bar centered vertically. - * @property isCircular Indicates whether the style uses a circular progress bar. - * @property isVertical Indicates whether the style uses a vertical progress bar. - */ - @Suppress("unused") - enum class SwipeOverlayStyle( - val isMinimal: Boolean = false, - val isHorizontalMinimalCenter: Boolean = false, - val isCircular: Boolean = false, - val isVertical: Boolean = false - ) { - /** - * A full horizontal progress bar with detailed indicators. - */ - HORIZONTAL, - - /** - * A minimal horizontal progress bar positioned at the top. - */ - HORIZONTAL_MINIMAL_TOP(isMinimal = true), - - /** - * A minimal horizontal progress bar centered vertically. - */ - HORIZONTAL_MINIMAL_CENTER(isMinimal = true, isHorizontalMinimalCenter = true), - - /** - * A full circular progress bar with detailed indicators. - */ - CIRCULAR(isCircular = true), - - /** - * A minimal circular progress bar. - */ - CIRCULAR_MINIMAL(isMinimal = true, isCircular = true), - - /** - * A full vertical progress bar with detailed indicators. - */ - VERTICAL(isVertical = true), - - /** - * A minimal vertical progress bar. - */ - VERTICAL_MINIMAL(isMinimal = true, isVertical = true) - } - - /** - * The current style of the overlay, determining its layout and appearance. - */ - val overlayStyle = Settings.SWIPE_OVERLAY_STYLE.get() - - /** - * Playback speed change per tick, expressed as an integer multiplied by 100 (e.g. 5 = 0.05x). - */ - @Suppress("unused") - enum class SwipeSpeedStep(val stepInt: Int) { - STEP_005(5), - STEP_010(10), - STEP_025(25), - } - //endregion - - //region behavior - /** - * Indicates whether the brightness level should be saved and restored when entering or exiting fullscreen mode. - */ - val shouldSaveAndRestoreBrightness = Settings.SWIPE_SAVE_AND_RESTORE_BRIGHTNESS.get() - - /** - * Indicates whether auto-brightness should be enabled when the brightness gesture reaches its lowest value. - */ - val shouldLowestValueEnableAutoBrightness = Settings.SWIPE_LOWEST_VALUE_ENABLE_AUTO_BRIGHTNESS.get() - - /** - * The saved brightness value for the swipe gesture, used to restore brightness in fullscreen mode. - */ - var savedScreenBrightnessValue: Float - get() = Settings.SWIPE_BRIGHTNESS_VALUE.get() - set(value) = Settings.SWIPE_BRIGHTNESS_VALUE.save(value) - //endregion -} diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/SwipeControlsHostActivity.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/SwipeControlsHostActivity.java new file mode 100644 index 0000000000..179dda36c4 --- /dev/null +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/SwipeControlsHostActivity.java @@ -0,0 +1,386 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches + * + * Original hard forked code: + * https://github.com/ReVanced/revanced-patches/commit/724e6d61b2ecd868c1a9a37d465a688e83a74799 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to Morphe contributions. + */ + +package app.morphe.extension.youtube.swipecontrols; + +import android.app.Activity; +import android.os.Build; +import android.os.Bundle; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewParent; +import android.view.WindowInsets; + +import androidx.annotation.Nullable; + +import java.lang.ref.WeakReference; +import java.util.function.Consumer; + +import app.morphe.extension.shared.Logger; +import app.morphe.extension.youtube.patches.VersionCheckPatch; +import app.morphe.extension.youtube.settings.Settings; +import app.morphe.extension.youtube.shared.PlayerType; +import app.morphe.extension.youtube.swipecontrols.controller.AudioVolumeController; +import app.morphe.extension.youtube.swipecontrols.controller.ScreenBrightnessController; +import app.morphe.extension.youtube.swipecontrols.controller.SwipeZonesController; +import app.morphe.extension.youtube.swipecontrols.controller.VolumeKeysController; +import app.morphe.extension.youtube.swipecontrols.controller.gesture.ClassicSwipeController; +import app.morphe.extension.youtube.swipecontrols.controller.gesture.PressToSwipeController; +import app.morphe.extension.youtube.swipecontrols.controller.gesture.core.GestureController; +import app.morphe.extension.youtube.swipecontrols.misc.Rectangle; +import app.morphe.extension.youtube.swipecontrols.views.SwipeControlsOverlayLayout; + +/** + * The main controller for volume and brightness swipe controls. + * Note that the superclass is overwritten to the superclass of the MainActivity at patch time. + *

+ * The constructor of this class is never called, because the MainActivity constructor still calls + * the constructor of its original superclass. Instance fields must not have initializers, + * and are instead set in {@link #initialize()}. + */ +@SuppressWarnings("unused") +public class SwipeControlsHostActivity extends Activity { + + private static final String LOCK_MODE_OVERLAY_NAME = "player_overlay_lock_mode"; + + /** + * The currently active swipe controls host. + * The reference may be null. + */ + private static WeakReference currentHost = new WeakReference<>(null); + + /** + * Container of the native lock screen overlay. + * The reference may be null. + */ + private static WeakReference lockModeOverlay = new WeakReference<>(null); + + /** + * Current instance of {@link AudioVolumeController}. + */ + @Nullable + private AudioVolumeController audio; + + /** + * Current instance of {@link ScreenBrightnessController}. + */ + @Nullable + private ScreenBrightnessController screen; + + /** + * Current instance of {@link SwipeControlsConfigurationProvider}. + */ + private SwipeControlsConfigurationProvider config; + + /** + * Current instance of {@link SwipeControlsOverlayLayout}. + */ + private SwipeControlsOverlayLayout overlay; + + /** + * Current instance of {@link SwipeZonesController}. + */ + private SwipeZonesController zones; + + /** + * Main gesture controller. + */ + private GestureController gesture; + + /** + * Main volume keys controller. + */ + private VolumeKeysController keys; + + /** + * Listener for changes in the player type. + */ + private Consumer playerTypeObserver; + + /** + * Whether the status bar is visible on Android 15+ (edge-to-edge display). + */ + private volatile boolean statusBarVisible; + + // Flag that indicates whether the brightness has been saved and restored default brightness + private boolean isBrightnessSaved; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + initialize(); + } + + @Override + protected void onStart() { + super.onStart(); + reAttachOverlays(); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + PlayerType.getOnChange().removeObserver(playerTypeObserver); + } + + @Override + public boolean dispatchTouchEvent(MotionEvent ev) { + ensureInitialized(); + if (gesture.submitTouchEvent(ev)) { + return true; + } + return super.dispatchTouchEvent(ev); + } + + @Override + public boolean dispatchKeyEvent(KeyEvent ev) { + ensureInitialized(); + if (keys.onKeyEvent(ev)) { + return true; + } + return super.dispatchKeyEvent(ev); + } + + /** + * Dispatches a touch event to downstream views. + * + * @param event The event to dispatch. + * @return Whether the event was consumed. + */ + public boolean dispatchDownstreamTouchEvent(MotionEvent event) { + return super.dispatchTouchEvent(event); + } + + @Nullable + public AudioVolumeController getAudio() { + return audio; + } + + @Nullable + public ScreenBrightnessController getScreen() { + return screen; + } + + public SwipeControlsConfigurationProvider getConfig() { + return config; + } + + public SwipeControlsOverlayLayout getOverlay() { + return overlay; + } + + public SwipeZonesController getZones() { + return zones; + } + + public boolean isStatusBarVisible() { + return statusBarVisible; + } + + /** + * Current content view with id {@link android.R.id#content}. + */ + private ViewGroup getContentRoot() { + return getWindow().getDecorView().findViewById(android.R.id.content); + } + + /** + * Ensures that swipe controllers are initialized and attached. + * On some ROMs with SDK <= 23, {@link #onCreate} and {@link #onStart} may not be called correctly. + * See https://github.com/revanced/revanced-patches/issues/446 + */ + private void ensureInitialized() { + if (config == null) { + Logger.printException(() -> "swipe controls were not initialized in onCreate, " + + "initializing on-the-fly (SDK is " + Build.VERSION.SDK_INT + ")"); + initialize(); + reAttachOverlays(); + } + } + + /** + * Initializes controllers, only call once. + */ + private void initialize() { + // create controllers + Logger.printDebug(() -> "initializing swipe controls controllers"); + config = new SwipeControlsConfigurationProvider(); + keys = new VolumeKeysController(this); + audio = createAudioController(); + screen = createScreenController(); + + // create overlay + overlay = new SwipeControlsOverlayLayout(this, config); + getContentRoot().addView(overlay); + + // create swipe zone controller + zones = new SwipeZonesController(this, () -> { + ViewGroup contentRoot = getContentRoot(); + return new Rectangle( + (int) contentRoot.getX(), + (int) contentRoot.getY(), + contentRoot.getWidth(), + contentRoot.getHeight() + ); + }); + + // create the gesture controller + gesture = createGestureController(); + + // listen for changes in the player type + playerTypeObserver = this::onPlayerTypeChanged; + PlayerType.getOnChange().addObserver(playerTypeObserver); + + // set current instance reference + currentHost = new WeakReference<>(this); + + // fix edge-to-edge display + // see: https://github.com/MorpheApp/morphe-patches/issues/658 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) { + ViewParent rootView = getContentRoot().getParent(); + if (rootView instanceof ViewGroup viewGroup) { + viewGroup.setOnApplyWindowInsetsListener((v, insets) -> { + statusBarVisible = insets.isVisible(WindowInsets.Type.statusBars()); + return insets; + }); + } + } + } + + /** + * Re-attaches swipe overlays. + */ + private void reAttachOverlays() { + Logger.printDebug(() -> "attaching swipe controls overlay"); + ViewGroup contentRoot = getContentRoot(); + contentRoot.removeView(overlay); + contentRoot.addView(overlay); + } + + /** + * If the app shares the screen with another app, which the player of a fold device is used + * with the same way it is used in fullscreen. + *

+ * Picture in picture is a multi window mode as well and is not one of these. The window of + * the app is pinned then, and the player it returns to is the maximized one. + */ + public boolean isInSplitScreenMode() { + return isInMultiWindowMode() && !isInPictureInPictureMode(); + } + + /** + * Called when the player type changes. + * + * @param type The new player type. + */ + private void onPlayerTypeChanged(PlayerType type) { + if (config.shouldSaveAndRestoreBrightness() + && (type == PlayerType.WATCH_WHILE_FULLSCREEN + || (isInSplitScreenMode() && type == PlayerType.WATCH_WHILE_MAXIMIZED)) + && isBrightnessSaved) { + // If saving and restoring brightness is enabled, and the player type is fullscreen or multi-window, + // and brightness has already been saved, then restore the screen brightness + if (screen != null) { + screen.restore(); + } + isBrightnessSaved = false; + } else if (config.shouldSaveAndRestoreBrightness() && !isBrightnessSaved) { + // If saving and restoring brightness is enabled, and brightness has not been saved, + // then save the current screen state, restore default brightness, and mark brightness as saved + if (screen != null) { + screen.save(); + screen.restoreDefaultBrightness(); + } + isBrightnessSaved = true; + } else if (screen != null) { + // If saving and restoring brightness is disabled, simply keep the default brightness + screen.restoreDefaultBrightness(); + } + } + + /** + * Creates the audio volume controller. + */ + @Nullable + private AudioVolumeController createAudioController() { + if (!config.isVolumeControlEnabled()) { + return null; + } + AudioVolumeController controller = new AudioVolumeController(this); + return controller.isAvailable() ? controller : null; + } + + /** + * Creates the screen brightness controller instance. + */ + @Nullable + private ScreenBrightnessController createScreenController() { + return config.isBrightnessControlEnabled() + ? new ScreenBrightnessController(this) + : null; + } + + /** + * Creates the gesture controller based on settings. + */ + private GestureController createGestureController() { + return config.shouldEnablePressToSwipe() + ? new PressToSwipeController(this) + : new ClassicSwipeController(this); + } + + /** + * The currently active swipe controls host. + * The reference may be null. + */ + public static WeakReference getCurrentHost() { + return currentHost; + } + + /** + * Whether the native lock screen is currently engaged. + *

+ * The container is attached for the entire playback session, and only its Litho content + * is mounted while the screen is locked, so the mounted content is what tells both states apart. + */ + public static boolean isPlayerLocked() { + ViewGroup overlay = lockModeOverlay.get(); + if (overlay == null) return false; + + for (int i = 0, count = overlay.getChildCount(); i < count; i++) { + View child = overlay.getChildAt(i); + if (child instanceof ViewGroup group && group.getChildCount() > 0) { + return true; + } + } + + return false; + } + + /** + * Injection point. + */ + public static void setPlayerOverlay(View overlay, @Nullable String overlayName) { + // The same container class wraps every player overlay, and only the name tells them apart. + if (LOCK_MODE_OVERLAY_NAME.equals(overlayName) && overlay instanceof ViewGroup group) { + lockModeOverlay = new WeakReference<>(group); + } + } + + /** + * Injection point. + */ + public static boolean allowSwipeChangeVideo(boolean original) { + // Feature can cause crashing if forced in newer targets. + return !VersionCheckPatch.IS_20_22_OR_GREATER && Settings.SWIPE_CHANGE_VIDEO.get(); + } +} diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/SwipeControlsHostActivity.kt b/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/SwipeControlsHostActivity.kt deleted file mode 100644 index 65e3815b6b..0000000000 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/SwipeControlsHostActivity.kt +++ /dev/null @@ -1,332 +0,0 @@ -/* - * Copyright 2026 Morphe. - * https://github.com/MorpheApp/morphe-patches - * - * Original hard forked code: - * https://github.com/ReVanced/revanced-patches/commit/724e6d61b2ecd868c1a9a37d465a688e83a74799 - * - * See the included NOTICE file for GPLv3 Section 7 terms that apply to Morphe contributions. - */ - -package app.morphe.extension.youtube.swipecontrols - -import android.app.Activity -import android.os.Build -import android.os.Bundle -import android.view.KeyEvent -import android.view.MotionEvent -import android.view.View -import android.view.ViewGroup -import android.view.WindowInsets -import app.morphe.extension.shared.Logger.printDebug -import app.morphe.extension.shared.Logger.printException -import app.morphe.extension.youtube.patches.VersionCheckPatch -import app.morphe.extension.youtube.settings.Settings -import app.morphe.extension.youtube.shared.PlayerType -import app.morphe.extension.youtube.swipecontrols.controller.AudioVolumeController -import app.morphe.extension.youtube.swipecontrols.controller.ScreenBrightnessController -import app.morphe.extension.youtube.swipecontrols.controller.SwipeZonesController -import app.morphe.extension.youtube.swipecontrols.controller.VolumeKeysController -import app.morphe.extension.youtube.swipecontrols.controller.gesture.ClassicSwipeController -import app.morphe.extension.youtube.swipecontrols.controller.gesture.PressToSwipeController -import app.morphe.extension.youtube.swipecontrols.controller.gesture.core.GestureController -import app.morphe.extension.youtube.swipecontrols.misc.Rectangle -import app.morphe.extension.youtube.swipecontrols.views.SwipeControlsOverlayLayout -import java.lang.ref.WeakReference -import kotlin.concurrent.Volatile - -/** - * The main controller for volume and brightness swipe controls. - * Note that the superclass is overwritten to the superclass of the MainActivity at patch time. - */ -class SwipeControlsHostActivity : Activity() { - /** - * Current instance of [AudioVolumeController]. - */ - var audio: AudioVolumeController? = null - - /** - * Current instance of [ScreenBrightnessController]. - */ - var screen: ScreenBrightnessController? = null - - /** - * Current instance of [SwipeControlsConfigurationProvider]. - */ - lateinit var config: SwipeControlsConfigurationProvider - - /** - * Current instance of [SwipeControlsOverlayLayout]. - */ - lateinit var overlay: SwipeControlsOverlayLayout - - /** - * Current instance of [SwipeZonesController]. - */ - lateinit var zones: SwipeZonesController - - /** - * Main gesture controller. - */ - private lateinit var gesture: GestureController - - /** - * Main volume keys controller. - */ - private lateinit var keys: VolumeKeysController - - /** - * Current content view with id [android.R.id.content]. - */ - private val contentRoot - get() = window.decorView.findViewById(android.R.id.content) - - /** - * Whether the status bar is visible on Android 15+ (edge-to-edge display). - */ - @Volatile - var statusBarVisible: Boolean = false - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - initialize() - } - - override fun onStart() { - super.onStart() - reAttachOverlays() - } - - override fun onDestroy() { - super.onDestroy() - PlayerType.onChange -= this::onPlayerTypeChanged - } - - override fun dispatchTouchEvent(ev: MotionEvent?): Boolean { - ensureInitialized() - return if ((ev != null) && gesture.submitTouchEvent(ev)) { - true - } else { - super.dispatchTouchEvent(ev) - } - } - - override fun dispatchKeyEvent(ev: KeyEvent?): Boolean { - ensureInitialized() - return if ((ev != null) && keys.onKeyEvent(ev)) { - true - } else { - super.dispatchKeyEvent(ev) - } - } - - /** - * Dispatches a touch event to downstream views. - * - * @param event The event to dispatch. - * @return Whether the event was consumed. - */ - fun dispatchDownstreamTouchEvent(event: MotionEvent) = - super.dispatchTouchEvent(event) - - /** - * Ensures that swipe controllers are initialized and attached. - * On some ROMs with SDK <= 23, [onCreate] and [onStart] may not be called correctly. - * See https://github.com/revanced/revanced-patches/issues/446 - */ - private fun ensureInitialized() { - if (!this::config.isInitialized) { - printException { - "swipe controls were not initialized in onCreate, initializing on-the-fly (SDK is ${Build.VERSION.SDK_INT})" - } - initialize() - reAttachOverlays() - } - } - - /** - * Initializes controllers, only call once. - */ - private fun initialize() { - // create controllers - printDebug { "initializing swipe controls controllers" } - config = SwipeControlsConfigurationProvider() - keys = VolumeKeysController(this) - audio = createAudioController() - screen = createScreenController() - - // create overlay - SwipeControlsOverlayLayout(this, config).let { - overlay = it - contentRoot.addView(it) - } - - // create swipe zone controller - zones = SwipeZonesController(this) { - Rectangle( - contentRoot.x.toInt(), - contentRoot.y.toInt(), - contentRoot.width, - contentRoot.height, - ) - } - - // create the gesture controller - gesture = createGestureController() - - // listen for changes in the player type - PlayerType.onChange += this::onPlayerTypeChanged - - // set current instance reference - currentHost = WeakReference(this) - - // fix edge-to-edge display - // see: https://github.com/MorpheApp/morphe-patches/issues/658 - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) { - val rootView = contentRoot.parent - if (rootView is ViewGroup) { - rootView.setOnApplyWindowInsetsListener { _, insets -> - statusBarVisible = insets.isVisible(WindowInsets.Type.statusBars()) - insets - } - } - } - } - - /** - * Re-attaches swipe overlays. - */ - private fun reAttachOverlays() { - printDebug { "attaching swipe controls overlay" } - contentRoot.removeView(overlay) - contentRoot.addView(overlay) - } - - /** - * If the app shares the screen with another app, which the player of a fold device is used - * with the same way it is used in fullscreen. - * - * Picture in picture is a multi window mode as well and is not one of these. The window of - * the app is pinned then, and the player it returns to is the maximized one. - */ - val isInSplitScreenMode: Boolean - get() = isInMultiWindowMode && !isInPictureInPictureMode - - // Flag that indicates whether the brightness has been saved and restored default brightness - private var isBrightnessSaved = false - - /** - * Called when the player type changes. - * - * @param type The new player type. - */ - private fun onPlayerTypeChanged(type: PlayerType) { - when { - // If saving and restoring brightness is enabled, and the player type is fullscreen or multi-window, - // and brightness has already been saved, then restore the screen brightness - config.shouldSaveAndRestoreBrightness && (type == PlayerType.WATCH_WHILE_FULLSCREEN || - (isInSplitScreenMode && type == PlayerType.WATCH_WHILE_MAXIMIZED)) && isBrightnessSaved -> { - screen?.restore() - isBrightnessSaved = false - } - // If saving and restoring brightness is enabled, and brightness has not been saved, - // then save the current screen state, restore default brightness, and mark brightness as saved - config.shouldSaveAndRestoreBrightness && !isBrightnessSaved -> { - screen?.save() - screen?.restoreDefaultBrightness() - isBrightnessSaved = true - } - // If saving and restoring brightness is disabled, simply keep the default brightness - else -> screen?.restoreDefaultBrightness() - } - } - - /** - * Creates the audio volume controller. - */ - private fun createAudioController() = - if (config.enableVolumeControls) { - AudioVolumeController(this).takeIf { it.isAvailable } - } else { - null - } - - /** - * Creates the screen brightness controller instance. - */ - private fun createScreenController() = - if (config.enableBrightnessControl) { - ScreenBrightnessController(this) - } else { - null - } - - /** - * Creates the gesture controller based on settings. - */ - private fun createGestureController() = - if (config.shouldEnablePressToSwipe) { - PressToSwipeController(this) - } else { - ClassicSwipeController(this) - } - - companion object { - private const val LOCK_MODE_OVERLAY_NAME = "player_overlay_lock_mode" - - /** - * The currently active swipe controls host. - * The reference may be null. - */ - @JvmStatic - var currentHost: WeakReference = WeakReference(null) - private set - - /** - * Container of the native lock screen overlay. - * The reference may be null. - */ - private var lockModeOverlay: WeakReference = WeakReference(null) - - /** - * Whether the native lock screen is currently engaged. - * - * The container is attached for the entire playback session, and only its Litho content - * is mounted while the screen is locked, so the mounted content is what tells both states apart. - */ - val isPlayerLocked: Boolean - get() { - val overlay = lockModeOverlay.get() ?: return false - - for (i in 0 until overlay.childCount) { - val child = overlay.getChildAt(i) - if (child is ViewGroup && child.childCount > 0) { - return true - } - } - - return false - } - - /** - * Injection point. - */ - @Suppress("unused") - @JvmStatic - fun setPlayerOverlay(overlay: View, overlayName: String?) { - // The same container class wraps every player overlay, and only the name tells them apart. - if (LOCK_MODE_OVERLAY_NAME == overlayName && overlay is ViewGroup) { - lockModeOverlay = WeakReference(overlay) - } - } - - /** - * Injection point. - */ - @Suppress("unused") - @JvmStatic - fun allowSwipeChangeVideo(original: Boolean): Boolean = - // Feature can cause crashing if forced in newer targets. - !VersionCheckPatch.IS_20_22_OR_GREATER && Settings.SWIPE_CHANGE_VIDEO.get() - } -} diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/controller/AudioVolumeController.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/controller/AudioVolumeController.java new file mode 100644 index 0000000000..9881ea9641 --- /dev/null +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/controller/AudioVolumeController.java @@ -0,0 +1,175 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches + * + * Original hard forked code: + * https://github.com/ReVanced/revanced-patches/commit/724e6d61b2ecd868c1a9a37d465a688e83a74799 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to Morphe contributions. + */ + +package app.morphe.extension.youtube.swipecontrols.controller; + +import android.content.Context; +import android.media.AudioManager; +import android.os.Build; + +import androidx.annotation.Nullable; + +import app.morphe.extension.shared.Logger; +import app.morphe.extension.shared.Utils; +import app.morphe.extension.youtube.patches.SoundBoostPatch; +import app.morphe.extension.youtube.swipecontrols.SwipeControlsConfigurationProvider; +import app.morphe.extension.youtube.swipecontrols.SwipeControlsHostActivity; + +/** + * Controller to adjust the device volume level. + */ +public final class AudioVolumeController { + + /** + * The stream that is being controlled. + */ + private static final int TARGET_STREAM = AudioManager.STREAM_MUSIC; + + /** + * The host activity of which the volume is adjusted, the main controller instance. + */ + private final SwipeControlsHostActivity host; + + /** + * Audio service connection. + */ + @Nullable + private final AudioManager audioManager; + private final int minimumVolumeIndex; + private final int maximumVolumeIndex; + + /** + * @param host The host activity of which the volume is adjusted, the main controller instance. + */ + public AudioVolumeController(SwipeControlsHostActivity host) { + this.host = host; + + // bind audio service + audioManager = (AudioManager) host.getSystemService(Context.AUDIO_SERVICE); + if (audioManager == null) { + Logger.printException(() -> "failed to acquire AUDIO_SERVICE"); + minimumVolumeIndex = 0; + maximumVolumeIndex = 0; + } else { + maximumVolumeIndex = audioManager.getStreamMaxVolume(TARGET_STREAM); + minimumVolumeIndex = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P + ? audioManager.getStreamMinVolume(TARGET_STREAM) + : 0; + } + } + + /** + * Whether the audio service was bound and the volume can be adjusted. + * A controller that is not available must not be used, as the stream bounds are unknown. + */ + public boolean isAvailable() { + return audioManager != null; + } + + /** + * The current volume, ranging from 0.0 to {@link #getMaxVolume()}. + */ + public int getVolume() { + // check if initialized correctly + if (audioManager == null) return 0; + + // get current volume + return audioManager.getStreamVolume(TARGET_STREAM) - minimumVolumeIndex; + } + + public void setVolume(int value) { + // check if initialized correctly + if (audioManager == null) return; + + // set new volume + audioManager.setStreamVolume(TARGET_STREAM, + Utils.clamp(value + minimumVolumeIndex, minimumVolumeIndex, maximumVolumeIndex), 0); + } + + /** + * The maximum possible volume. + */ + public int getMaxVolume() { + return maximumVolumeIndex - minimumVolumeIndex; + } + + /** + * The volume change of a single step, in stream index units. + * Some devices expose a fine-grained volume stream (150 indices instead of 15), + * where a single index is a tenth of what the device volume UI adjusts by. + */ + public int getVolumeStepSize() { + if (audioManager == null) return 1; + + return SwipeControlsConfigurationProvider.volumeStepSize(getMaxVolume(), host.getConfig().getVolumeStepCount()); + } + + /** + * The current volume, counted in steps of {@link #getVolumeStepSize()}. + */ + public int getSteppedVolume() { + return getVolume() / getVolumeStepSize(); + } + + /** + * The maximum volume, counted in steps of {@link #getVolumeStepSize()}. + */ + public int getSteppedMaxVolume() { + return getMaxVolume() / getVolumeStepSize(); + } + + /** + * The current boost above the maximum volume, in steps. Always zero if volume boost is off. + */ + public int getBoostStep() { + return SoundBoostPatch.getBoostStep(); + } + + /** + * Adjusts the volume by whole steps, snapping to a multiple of {@link #getVolumeStepSize()} + * so the levels stay the same ones the device volume UI uses. + * If volume boost is allowed, steps up at the maximum volume raise the boost, + * and steps down first remove the boost before lowering the volume. + * + * @param steps The number of steps to adjust by, negative to lower the volume. + */ + public void adjustVolumeBySteps(int steps) { + if (!isAvailable()) return; + + int remaining = steps; + while (remaining != 0) { + final int direction = remaining > 0 ? 1 : -1; + remaining -= direction; + + if (direction > 0 && getVolume() >= getMaxVolume()) { + SoundBoostPatch.adjustBoostStep(1); + continue; + } + if (direction < 0 && SoundBoostPatch.adjustBoostStep(-1)) { + continue; + } + adjustSystemVolumeBySteps(direction); + } + } + + private void adjustSystemVolumeBySteps(int steps) { + final int stepSize = getVolumeStepSize(); + if (stepSize == 1) { + setVolume(getVolume() + steps); + return; + } + + // Round away from the direction of travel, so a volume set outside the app + // first snaps onto the step grid instead of jumping a full step. + final int volume = getVolume(); + final int currentStep = steps > 0 ? volume / stepSize : (volume + stepSize - 1) / stepSize; + setVolume((currentStep + steps) * stepSize); + } +} diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/controller/AudioVolumeController.kt b/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/controller/AudioVolumeController.kt deleted file mode 100644 index 8b7d3315c9..0000000000 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/controller/AudioVolumeController.kt +++ /dev/null @@ -1,169 +0,0 @@ -/* - * Copyright 2026 Morphe. - * https://github.com/MorpheApp/morphe-patches - * - * Original hard forked code: - * https://github.com/ReVanced/revanced-patches/commit/724e6d61b2ecd868c1a9a37d465a688e83a74799 - * - * See the included NOTICE file for GPLv3 Section 7 terms that apply to Morphe contributions. - */ - -package app.morphe.extension.youtube.swipecontrols.controller - -import android.content.Context -import android.media.AudioManager -import android.os.Build -import app.morphe.extension.shared.Logger.printException -import app.morphe.extension.youtube.patches.SoundBoostPatch -import app.morphe.extension.youtube.swipecontrols.SwipeControlsConfigurationProvider -import app.morphe.extension.youtube.swipecontrols.SwipeControlsHostActivity -import app.morphe.extension.youtube.swipecontrols.misc.clamp -import kotlin.properties.Delegates - -/** - * Controller to adjust the device volume level. - * - * @param host The host activity of which the volume is adjusted, the main controller instance. - * @param targetStream The stream that is being controlled. Must be one of the STREAM_* constants in [AudioManager]. - */ -class AudioVolumeController( - private val host: SwipeControlsHostActivity, - private val targetStream: Int = AudioManager.STREAM_MUSIC, -) { - - /** - * Audio service connection. - */ - private lateinit var audioManager: AudioManager - private var minimumVolumeIndex by Delegates.notNull() - private var maximumVolumeIndex by Delegates.notNull() - - init { - // bind audio service - val mgr = host.getSystemService(Context.AUDIO_SERVICE) as? AudioManager - if (mgr == null) { - printException { "failed to acquire AUDIO_SERVICE" } - } else { - audioManager = mgr - maximumVolumeIndex = audioManager.getStreamMaxVolume(targetStream) - minimumVolumeIndex = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - audioManager.getStreamMinVolume( - targetStream, - ) - } else { - 0 - } - } - } - - /** - * Whether the audio service was bound and the volume can be adjusted. - * A controller that is not available must not be used, as the stream bounds are unknown. - */ - val isAvailable: Boolean - get() = this::audioManager.isInitialized - - /** - * The current volume, ranging from 0.0 to [maxVolume]. - */ - var volume: Int - get() { - // check if initialized correctly - if (!this::audioManager.isInitialized) return 0 - - // get current volume - return currentVolumeIndex - minimumVolumeIndex - } - set(value) { - // check if initialized correctly - if (!this::audioManager.isInitialized) return - - // set new volume - currentVolumeIndex = - (value + minimumVolumeIndex).clamp(minimumVolumeIndex, maximumVolumeIndex) - } - - /** - * The maximum possible volume. - */ - val maxVolume: Int - get() = maximumVolumeIndex - minimumVolumeIndex - - /** - * The volume change of a single step, in stream index units. - * Some devices expose a fine-grained volume stream (150 indices instead of 15), - * where a single index is a tenth of what the device volume UI adjusts by. - */ - val volumeStepSize: Int - get() { - if (!this::audioManager.isInitialized) return 1 - - return SwipeControlsConfigurationProvider.volumeStepSize(maxVolume, host.config.volumeStepCount) - } - - /** - * The current volume, counted in steps of [volumeStepSize]. - */ - val steppedVolume: Int - get() = volume / volumeStepSize - - /** - * The maximum volume, counted in steps of [volumeStepSize]. - */ - val steppedMaxVolume: Int - get() = maxVolume / volumeStepSize - - /** - * The current boost above the maximum volume, in steps. Always zero if volume boost is off. - */ - val boostStep: Int - get() = SoundBoostPatch.getBoostStep() - - /** - * Adjusts the volume by whole steps, snapping to a multiple of [volumeStepSize] - * so the levels stay the same ones the device volume UI uses. - * If volume boost is allowed, steps up at the maximum volume raise the boost, - * and steps down first remove the boost before lowering the volume. - * - * @param steps The number of steps to adjust by, negative to lower the volume. - */ - fun adjustVolumeBySteps(steps: Int) { - if (!isAvailable) return - - var remaining = steps - while (remaining != 0) { - val direction = if (remaining > 0) 1 else -1 - remaining -= direction - - if (direction > 0 && volume >= maxVolume) { - SoundBoostPatch.adjustBoostStep(1) - continue - } - if (direction < 0 && SoundBoostPatch.adjustBoostStep(-1)) { - continue - } - adjustSystemVolumeBySteps(direction) - } - } - - private fun adjustSystemVolumeBySteps(steps: Int) { - val stepSize = volumeStepSize - if (stepSize == 1) { - volume += steps - return - } - - // Round away from the direction of travel, so a volume set outside the app - // first snaps onto the step grid instead of jumping a full step. - val currentStep = if (steps > 0) volume / stepSize else (volume + stepSize - 1) / stepSize - volume = (currentStep + steps) * stepSize - } - - /** - * The current volume index of the target stream. - */ - private var currentVolumeIndex: Int - get() = audioManager.getStreamVolume(targetStream) - set(value) = audioManager.setStreamVolume(targetStream, value, 0) -} diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/controller/ScreenBrightnessController.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/controller/ScreenBrightnessController.java new file mode 100644 index 0000000000..aa17bfc42a --- /dev/null +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/controller/ScreenBrightnessController.java @@ -0,0 +1,80 @@ +package app.morphe.extension.youtube.swipecontrols.controller; + +import android.view.WindowManager; + +import app.morphe.extension.shared.Utils; +import app.morphe.extension.youtube.swipecontrols.SwipeControlsHostActivity; + +/** + * Controller to adjust the screen brightness level. + */ +public final class ScreenBrightnessController { + + /** + * The host activity of which the brightness is adjusted, the main controller instance. + */ + public final SwipeControlsHostActivity host; + + // Flag that indicates whether the brightness has been restored + private boolean isBrightnessRestored; + + /** + * @param host The host activity of which the brightness is adjusted, the main controller instance. + */ + public ScreenBrightnessController(SwipeControlsHostActivity host) { + this.host = host; + } + + /** + * The current screen brightness in percent, ranging from 0.0 to 100.0. + */ + public double getScreenBrightness() { + return getRawScreenBrightness() * 100.0; + } + + public void setScreenBrightness(double value) { + setRawScreenBrightness(Utils.clamp((float) value / 100f, 0f, 1f)); + } + + /** + * Restores the screen brightness to the default device brightness. + */ + public void restoreDefaultBrightness() { + setRawScreenBrightness(WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE); + } + + /** + * Saves the current screen brightness into settings, to be brought back using {@link #restore()}. + */ + public void save() { + if (isBrightnessRestored) { + // Saves the current screen brightness value into settings + host.getConfig().setSavedScreenBrightnessValue(getRawScreenBrightness()); + // Reset the flag + isBrightnessRestored = false; + } + } + + /** + * Restores the screen brightness from settings saved using {@link #save()}. + */ + public void restore() { + // Restores the screen brightness value from the saved settings + setRawScreenBrightness(host.getConfig().getSavedScreenBrightnessValue()); + // Mark that brightness has been restored + isBrightnessRestored = true; + } + + /** + * Wrapper for the raw screen brightness in {@link WindowManager.LayoutParams#screenBrightness}. + */ + public float getRawScreenBrightness() { + return host.getWindow().getAttributes().screenBrightness; + } + + private void setRawScreenBrightness(float value) { + WindowManager.LayoutParams attr = host.getWindow().getAttributes(); + attr.screenBrightness = value; + host.getWindow().setAttributes(attr); + } +} diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/controller/ScreenBrightnessController.kt b/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/controller/ScreenBrightnessController.kt deleted file mode 100644 index ac573d33ce..0000000000 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/controller/ScreenBrightnessController.kt +++ /dev/null @@ -1,67 +0,0 @@ -package app.morphe.extension.youtube.swipecontrols.controller - -import android.view.WindowManager -import app.morphe.extension.youtube.swipecontrols.SwipeControlsHostActivity -import app.morphe.extension.youtube.swipecontrols.misc.clamp - -/** - * Controller to adjust the screen brightness level. - * - * @param host The host activity of which the brightness is adjusted, the main controller instance. - */ -class ScreenBrightnessController( - val host: SwipeControlsHostActivity, -) { - - /** - * The current screen brightness in percent, ranging from 0.0 to 100.0. - */ - var screenBrightness: Double - get() = rawScreenBrightness * 100.0 - set(value) { - rawScreenBrightness = (value.toFloat() / 100f).clamp(0f, 1f) - } - - /** - * Restores the screen brightness to the default device brightness. - */ - fun restoreDefaultBrightness() { - rawScreenBrightness = WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE - } - - // Flag that indicates whether the brightness has been restored - private var isBrightnessRestored = false - - /** - * Saves the current screen brightness into settings, to be brought back using [restore]. - */ - fun save() { - if (isBrightnessRestored) { - // Saves the current screen brightness value into settings - host.config.savedScreenBrightnessValue = rawScreenBrightness - // Reset the flag - isBrightnessRestored = false - } - } - - /** - * Restores the screen brightness from settings saved using [save]. - */ - fun restore() { - // Restores the screen brightness value from the saved settings - rawScreenBrightness = host.config.savedScreenBrightnessValue - // Mark that brightness has been restored - isBrightnessRestored = true - } - - /** - * Wrapper for the raw screen brightness in [WindowManager.LayoutParams.screenBrightness]. - */ - var rawScreenBrightness: Float - get() = host.window.attributes.screenBrightness - private set(value) { - val attr = host.window.attributes - attr.screenBrightness = value - host.window.attributes = attr - } -} diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/controller/SwipeZonesController.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/controller/SwipeZonesController.java new file mode 100644 index 0000000000..b6a37f34df --- /dev/null +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/controller/SwipeZonesController.java @@ -0,0 +1,184 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches + * + * Original hard forked code: + * https://github.com/ReVanced/revanced-patches/commit/724e6d61b2ecd868c1a9a37d465a688e83a74799 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to Morphe contributions. + */ + +package app.morphe.extension.youtube.swipecontrols.controller; + +import android.app.Activity; +import android.util.TypedValue; +import android.view.View; + +import androidx.annotation.Nullable; + +import java.util.function.Supplier; + +import app.morphe.extension.shared.ResourceType; +import app.morphe.extension.shared.ResourceUtils; +import app.morphe.extension.shared.settings.preference.SeekBarPreference; +import app.morphe.extension.youtube.settings.Settings; +import app.morphe.extension.youtube.swipecontrols.misc.Rectangle; +import app.morphe.extension.youtube.swipecontrols.misc.SwipeControlsUtils; + +/** + *

+ *  +------------------------------------------------------------------+
+ *  |                         40dp (top dead)                          |
+ *  +----------+-------------+----------------+-------------+----------+
+ *  |          |    left     |      top       |    right    |          |
+ *  |  20dp    |    (up/dn)  |   (left/right) |    (up/dn)  |  20dp    |
+ *  |          |    + top    |   speed_h%     |    + top    |          |
+ *  +- - - - - +- - - - - - -+- - - - - - - - +- - - - - - -+- - - - - +
+ *  |  dead    |    left     |     dead       |    right    |  dead    |
+ *  |          |   zone_w%   |   100-2*z%     |   zone_w%   |          |
+ *  +----------+-------------+----------------+-------------+----------+
+ *  |                        60dp (bottom dead)                        |
+ *  +------------------------------------------------------------------+
+ *
+ *  left / right - vertical swipe (up/down), span full effective height
+ *  top          - horizontal swipe (left/right), top speed_h% of effective height;
+ *                 overlaps with the top of the left and right zones
+ *
+ *  Each zone performs the action it is assigned in the settings.
+ * 
+ */ +public final class SwipeZonesController { + + private final Activity host; + + /** + * Provides the screen rectangle, used until the player bounds are known. + */ + private final Supplier fallbackScreenRect; + + /** + * 20dp, in pixels. + */ + private final int dp20; + + /** + * 40dp, in pixels. + */ + private final int dp40; + + /** + * 80dp, in pixels. + */ + private final int dp80; + + /** + * ID for R.id.inset_controls_overlay_wrapper. + */ + private final int sizeAdjustableOverlayId; + + /** + * Current bounding rectangle of the player. + */ + @Nullable + private Rectangle playerRect; + + public SwipeZonesController(Activity host, Supplier fallbackScreenRect) { + this.host = host; + this.fallbackScreenRect = fallbackScreenRect; + dp20 = SwipeControlsUtils.applyDimension(host, TypedValue.COMPLEX_UNIT_DIP, 20); + dp40 = SwipeControlsUtils.applyDimension(host, TypedValue.COMPLEX_UNIT_DIP, 40); + dp80 = SwipeControlsUtils.applyDimension(host, TypedValue.COMPLEX_UNIT_DIP, 80); + sizeAdjustableOverlayId = ResourceUtils.getIdentifier( + host, ResourceType.ID, "inset_controls_overlay_wrapper"); + } + + /** + * Rectangle of the area that is effectively usable for swipe controls. + */ + private Rectangle getEffectiveSwipeRect() { + maybeAttachPlayerBoundsListener(); + Rectangle p = playerRect != null ? playerRect : fallbackScreenRect.get(); + return new Rectangle( + p.x + dp20, + p.y + dp40, + p.width - 2 * dp20, + p.height - dp20 - dp80 + ); + } + + /** + * The rectangle of the left control zone. + */ + public Rectangle getLeft() { + Rectangle eRect = getEffectiveSwipeRect(); + final int zoneWidth = (eRect.width * SeekBarPreference.clampToRange(Settings.SWIPE_ZONE_WIDTH)) / 100; + return new Rectangle( + eRect.left, + eRect.top, + zoneWidth, + eRect.height + ); + } + + /** + * The rectangle of the right control zone. + */ + public Rectangle getRight() { + Rectangle eRect = getEffectiveSwipeRect(); + final int zoneWidth = (eRect.width * SeekBarPreference.clampToRange(Settings.SWIPE_ZONE_WIDTH)) / 100; + return new Rectangle( + eRect.right - zoneWidth, + eRect.top, + zoneWidth, + eRect.height + ); + } + + /** + * The rectangle of the top control zone. + */ + public Rectangle getTop() { + Rectangle eRect = getEffectiveSwipeRect(); + final int zoneHeight = (eRect.height * SeekBarPreference.clampToRange(Settings.SWIPE_SPEED_ZONE_HEIGHT)) / 100; + return new Rectangle( + eRect.left, + eRect.top, + eRect.width, + zoneHeight + ); + } + + /** + * Tries to attach a listener to the player_view and update the player rectangle. + * Once a listener is attached, this function does nothing. + */ + private void maybeAttachPlayerBoundsListener() { + if (playerRect != null) return; + View sizeAdjustableOverlay = host.findViewById(sizeAdjustableOverlayId); + if (sizeAdjustableOverlay != null) { + onPlayerViewLayout(sizeAdjustableOverlay); + sizeAdjustableOverlay.addOnLayoutChangeListener( + (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> + onPlayerViewLayout(sizeAdjustableOverlay)); + } + } + + /** + * Updates the player rectangle on player_view layout. + * + * @param sizeAdjustableOverlay The player view. + */ + private void onPlayerViewLayout(View sizeAdjustableOverlay) { + // the player surface is centered in the player view + // figure out the width of the surface including the padding (same on the left and right side) + // and use that width for the player rectangle size + // this automatically excludes any engagement panel from the rect + final int playerWidthWithPadding = sizeAdjustableOverlay.getWidth() + ((int) sizeAdjustableOverlay.getX() * 2); + playerRect = new Rectangle( + (int) sizeAdjustableOverlay.getX(), + (int) sizeAdjustableOverlay.getY(), + Math.min(sizeAdjustableOverlay.getWidth(), playerWidthWithPadding), + sizeAdjustableOverlay.getHeight() + ); + } +} diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/controller/SwipeZonesController.kt b/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/controller/SwipeZonesController.kt deleted file mode 100644 index c2cfbb79ae..0000000000 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/controller/SwipeZonesController.kt +++ /dev/null @@ -1,168 +0,0 @@ -/* - * Copyright 2026 Morphe. - * https://github.com/MorpheApp/morphe-patches - * - * Original hard forked code: - * https://github.com/ReVanced/revanced-patches/commit/724e6d61b2ecd868c1a9a37d465a688e83a74799 - * - * See the included NOTICE file for GPLv3 Section 7 terms that apply to Morphe contributions. - */ - -package app.morphe.extension.youtube.swipecontrols.controller - -import android.app.Activity -import android.util.TypedValue -import android.view.View -import app.morphe.extension.shared.ResourceType -import app.morphe.extension.shared.ResourceUtils.getIdentifier -import app.morphe.extension.shared.settings.preference.SeekBarPreference -import app.morphe.extension.youtube.settings.Settings -import app.morphe.extension.youtube.swipecontrols.misc.Rectangle -import app.morphe.extension.youtube.swipecontrols.misc.applyDimension -import kotlin.math.min - -/** - * +------------------------------------------------------------------+ - * | 40dp (top dead) | - * +----------+-------------+----------------+-------------+----------+ - * | | left | top | right | | - * | 20dp | (↑ ↓) | (← →) | (↑ ↓) | 20dp | - * | | + top | speed_h% | + top | | - * +- - - - - +- - - - - - -+- - - - - - - - +- - - - - - -+- - - - - + - * | dead | left | dead | right | dead | - * | | zone_w% | 100-2*z% | zone_w% | | - * +----------+-------------+----------------+-------------+----------+ - * | 60dp (bottom dead) | - * +------------------------------------------------------------------+ - * - * left / right - vertical swipe (↑ ↓), span full effective height - * top - horizontal swipe (← →), top speed_h% of effective height; - * overlaps with the top of the left and right zones - * - * Each zone performs the action it is assigned in the settings. - */ -@Suppress("PrivatePropertyName") -class SwipeZonesController( - private val host: Activity, - private val fallbackScreenRect: () -> Rectangle, -) { - /** - * 20dp, in pixels. - */ - private val _20dp = 20.applyDimension(host, TypedValue.COMPLEX_UNIT_DIP) - - /** - * 40dp, in pixels. - */ - private val _40dp = 40.applyDimension(host, TypedValue.COMPLEX_UNIT_DIP) - - /** - * 80dp, in pixels. - */ - private val _80dp = 80.applyDimension(host, TypedValue.COMPLEX_UNIT_DIP) - - /** - * ID for R.id.inset_controls_overlay_wrapper. - */ - private val sizeAdjustableOverlayId = getIdentifier( - host, ResourceType.ID, "inset_controls_overlay_wrapper" - ) - - /** - * Current bounding rectangle of the player. - */ - private var playerRect: Rectangle? = null - - /** - * Rectangle of the area that is effectively usable for swipe controls. - */ - private val effectiveSwipeRect: Rectangle - get() { - maybeAttachPlayerBoundsListener() - val p = if (playerRect != null) playerRect!! else fallbackScreenRect() - return Rectangle( - p.x + _20dp, - p.y + _40dp, - p.width - 2 * _20dp, - p.height - _20dp - _80dp, - ) - } - - /** - * The rectangle of the left control zone. - */ - val left: Rectangle - get() { - val eRect = effectiveSwipeRect - val zoneWidth = (eRect.width * SeekBarPreference.clampToRange(Settings.SWIPE_ZONE_WIDTH)) / 100 - return Rectangle( - eRect.left, - eRect.top, - zoneWidth, - eRect.height, - ) - } - - /** - * The rectangle of the right control zone. - */ - val right: Rectangle - get() { - val eRect = effectiveSwipeRect - val zoneWidth = (eRect.width * SeekBarPreference.clampToRange(Settings.SWIPE_ZONE_WIDTH)) / 100 - return Rectangle( - eRect.right - zoneWidth, - eRect.top, - zoneWidth, - eRect.height, - ) - } - - /** - * The rectangle of the top control zone. - */ - val top: Rectangle - get() { - val eRect = effectiveSwipeRect - val zoneHeight = (eRect.height * SeekBarPreference.clampToRange(Settings.SWIPE_SPEED_ZONE_HEIGHT)) / 100 - return Rectangle( - eRect.left, - eRect.top, - eRect.width, - zoneHeight, - ) - } - - /** - * Tries to attach a listener to the player_view and update the player rectangle. - * Once a listener is attached, this function does nothing. - */ - private fun maybeAttachPlayerBoundsListener() { - if (playerRect != null) return - host.findViewById(sizeAdjustableOverlayId)?.let { - onPlayerViewLayout(it) - it.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ -> - onPlayerViewLayout(it) - } - } - } - - /** - * Updates the player rectangle on player_view layout. - * - * @param sizeAdjustableOverlay The player view. - */ - private fun onPlayerViewLayout(sizeAdjustableOverlay: View) { - // the player surface is centered in the player view - // figure out the width of the surface including the padding (same on the left and right side) - // and use that width for the player rectangle size - // this automatically excludes any engagement panel from the rect - val playerWidthWithPadding = sizeAdjustableOverlay.width + (sizeAdjustableOverlay.x.toInt() * 2) - playerRect = Rectangle( - sizeAdjustableOverlay.x.toInt(), - sizeAdjustableOverlay.y.toInt(), - min(sizeAdjustableOverlay.width, playerWidthWithPadding), - sizeAdjustableOverlay.height, - ) - } -} diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/controller/VolumeKeysController.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/controller/VolumeKeysController.java new file mode 100644 index 0000000000..5f964d513a --- /dev/null +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/controller/VolumeKeysController.java @@ -0,0 +1,71 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches + * + * Original hard forked code: + * https://github.com/ReVanced/revanced-patches/commit/724e6d61b2ecd868c1a9a37d465a688e83a74799 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to Morphe contributions. + */ + +package app.morphe.extension.youtube.swipecontrols.controller; + +import android.view.KeyEvent; + +import app.morphe.extension.youtube.swipecontrols.SwipeControlsHostActivity; + +/** + * Controller for custom volume button behavior. + */ +public final class VolumeKeysController { + + /** + * Main controller instance. + */ + private final SwipeControlsHostActivity controller; + + /** + * @param controller Main controller instance. + */ + public VolumeKeysController(SwipeControlsHostActivity controller) { + this.controller = controller; + } + + /** + * Key event handler. + * + * @param event The key event. + * @return Whether to consume the event. + */ + public boolean onKeyEvent(KeyEvent event) { + // Without a volume controller the device has to handle the keys, or they would do nothing. + if (!controller.getConfig().shouldOverwriteVolumeKeyControls() || controller.getAudio() == null) { + return false; + } + + return switch (event.getKeyCode()) { + case KeyEvent.KEYCODE_VOLUME_DOWN -> handleVolumeKeyEvent(event, false); + case KeyEvent.KEYCODE_VOLUME_UP -> handleVolumeKeyEvent(event, true); + default -> false; + }; + } + + /** + * Handles a volume up/down key event. + * + * @param event The key event. + * @param volumeUp Whether the key pressed was the volume up key. + * @return Whether to consume the event. + */ + private boolean handleVolumeKeyEvent(KeyEvent event, boolean volumeUp) { + if (event.getAction() == KeyEvent.ACTION_DOWN) { + AudioVolumeController audio = controller.getAudio(); + if (audio != null) { + audio.adjustVolumeBySteps(volumeUp ? 1 : -1); + controller.getOverlay().onVolumeChanged(audio.getSteppedVolume(), audio.getSteppedMaxVolume(), audio.getBoostStep()); + } + } + + return true; + } +} diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/controller/VolumeKeysController.kt b/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/controller/VolumeKeysController.kt deleted file mode 100644 index c62f8a3dde..0000000000 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/controller/VolumeKeysController.kt +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright 2026 Morphe. - * https://github.com/MorpheApp/morphe-patches - * - * Original hard forked code: - * https://github.com/ReVanced/revanced-patches/commit/724e6d61b2ecd868c1a9a37d465a688e83a74799 - * - * See the included NOTICE file for GPLv3 Section 7 terms that apply to Morphe contributions. - */ - -package app.morphe.extension.youtube.swipecontrols.controller - -import android.view.KeyEvent -import app.morphe.extension.youtube.swipecontrols.SwipeControlsHostActivity - -/** - * Controller for custom volume button behavior. - * - * @param controller Main controller instance. - */ -class VolumeKeysController( - private val controller: SwipeControlsHostActivity, -) { - /** - * Key event handler. - * - * @param event The key event. - * @return Whether to consume the event. - */ - fun onKeyEvent(event: KeyEvent): Boolean { - // Without a volume controller the device has to handle the keys, or they would do nothing. - if (!controller.config.overwriteVolumeKeyControls || controller.audio == null) { - return false - } - - return when (event.keyCode) { - KeyEvent.KEYCODE_VOLUME_DOWN -> - handleVolumeKeyEvent(event, false) - KeyEvent.KEYCODE_VOLUME_UP -> - handleVolumeKeyEvent(event, true) - else -> false - } - } - - /** - * Handles a volume up/down key event. - * - * @param event The key event. - * @param volumeUp Whether the key pressed was the volume up key. - * @return Whether to consume the event. - */ - private fun handleVolumeKeyEvent(event: KeyEvent, volumeUp: Boolean): Boolean { - if (event.action == KeyEvent.ACTION_DOWN) { - controller.audio?.apply { - adjustVolumeBySteps(if (volumeUp) 1 else -1) - controller.overlay.onVolumeChanged(steppedVolume, steppedMaxVolume, boostStep) - } - } - - return true - } -} diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/controller/gesture/ClassicSwipeController.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/controller/gesture/ClassicSwipeController.java new file mode 100644 index 0000000000..8b278afc4d --- /dev/null +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/controller/gesture/ClassicSwipeController.java @@ -0,0 +1,126 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches + * + * Original hard forked code: + * https://github.com/ReVanced/revanced-patches/commit/724e6d61b2ecd868c1a9a37d465a688e83a74799 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to Morphe contributions. + */ + +package app.morphe.extension.youtube.swipecontrols.controller.gesture; + +import android.view.MotionEvent; + +import androidx.annotation.Nullable; + +import app.morphe.extension.youtube.shared.PlayerControlsVisibilityObserver; +import app.morphe.extension.youtube.swipecontrols.SwipeControlsConfigurationProvider.SwipeZoneAction; +import app.morphe.extension.youtube.swipecontrols.SwipeControlsHostActivity; +import app.morphe.extension.youtube.swipecontrols.controller.gesture.core.BaseGestureController; +import app.morphe.extension.youtube.swipecontrols.misc.Point; + +/** + * Provides the classic swipe controls experience, as it was with 'XFenster'. + */ +public final class ClassicSwipeController extends BaseGestureController { + + private final PlayerControlsVisibilityObserver playerControlsVisibilityObserver; + + /** + * The last event captured in {@link #onDown}. + */ + @Nullable + private MotionEvent lastOnDownEvent; + + /** + * @param controller Reference to the main swipe controller. + */ + public ClassicSwipeController(SwipeControlsHostActivity controller) { + super(controller); + playerControlsVisibilityObserver = new PlayerControlsVisibilityObserver(controller); + } + + @Override + protected boolean shouldForceInterceptEvents() { + if (lastOnDownEvent == null) return false; + Point origin = Point.fromMotionEvent(lastOnDownEvent); + return swipeActionAt(origin, getCurrentSwipe()) != SwipeZoneAction.OFF; + } + + @Override + protected boolean shouldDropMotion(MotionEvent motionEvent) { + // ignore gestures with more than one pointer + // when such a gesture is detected, dispatch the first event of the gesture to downstream + if (motionEvent.getPointerCount() > 1) { + if (lastOnDownEvent != null) { + controller.dispatchDownstreamTouchEvent(lastOnDownEvent); + lastOnDownEvent.recycle(); + } + lastOnDownEvent = null; + return true; + } + + // ignore gestures when player controls are visible + return playerControlsVisibilityObserver.arePlayerControlsVisible(); + } + + @Override + protected void onUp(MotionEvent motionEvent) { + super.onUp(motionEvent); + if (lastOnDownEvent != null) { + lastOnDownEvent.recycle(); + } + lastOnDownEvent = null; + } + + @Override + public boolean onDown(MotionEvent motionEvent) { + // save the event for later + if (lastOnDownEvent != null) { + lastOnDownEvent.recycle(); + } + lastOnDownEvent = MotionEvent.obtain(motionEvent); + + // must be inside swipe zone + return isInSwipeZone(motionEvent); + } + + @Override + public boolean onSingleTapUp(MotionEvent motionEvent) { + MotionEvent event = MotionEvent.obtain(motionEvent); + event.setAction(MotionEvent.ACTION_DOWN); + controller.dispatchDownstreamTouchEvent(event); + event.recycle(); + + return false; + } + + @Override + public boolean onDoubleTapEvent(MotionEvent motionEvent) { + MotionEvent event = MotionEvent.obtain(motionEvent); + controller.dispatchDownstreamTouchEvent(event); + event.recycle(); + + return super.onDoubleTapEvent(motionEvent); + } + + @Override + public void onLongPress(MotionEvent motionEvent) { + MotionEvent event = MotionEvent.obtain(motionEvent); + controller.dispatchDownstreamTouchEvent(event); + event.recycle(); + + super.onLongPress(motionEvent); + } + + @Override + protected boolean onSwipe(MotionEvent from, MotionEvent to, double distanceX, double distanceY) { + // cancel if not fullscreen or multi-window + if (!controller.getConfig().isFullscreenOrMultiWindowVideo()) return false; + // cancel if the swipe does not belong to any zone + if (!shouldForceInterceptEvents()) return false; + + return applySwipeAction(from, distanceX, distanceY); + } +} diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/controller/gesture/ClassicSwipeController.kt b/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/controller/gesture/ClassicSwipeController.kt deleted file mode 100644 index f5cb4744b9..0000000000 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/controller/gesture/ClassicSwipeController.kt +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright 2026 Morphe. - * https://github.com/MorpheApp/morphe-patches - * - * Original hard forked code: - * https://github.com/ReVanced/revanced-patches/commit/724e6d61b2ecd868c1a9a37d465a688e83a74799 - * - * See the included NOTICE file for GPLv3 Section 7 terms that apply to Morphe contributions. - */ - -package app.morphe.extension.youtube.swipecontrols.controller.gesture - -import android.view.MotionEvent -import app.morphe.extension.youtube.shared.PlayerControlsVisibilityObserver -import app.morphe.extension.youtube.shared.PlayerControlsVisibilityObserverImpl -import app.morphe.extension.youtube.swipecontrols.SwipeControlsConfigurationProvider.SwipeZoneAction -import app.morphe.extension.youtube.swipecontrols.SwipeControlsHostActivity -import app.morphe.extension.youtube.swipecontrols.controller.gesture.core.BaseGestureController -import app.morphe.extension.youtube.swipecontrols.misc.toPoint - -/** - * Provides the classic swipe controls experience, as it was with 'XFenster'. - * - * @param controller Reference to the main swipe controller. - */ -class ClassicSwipeController( - private val controller: SwipeControlsHostActivity, -) : BaseGestureController(controller), - PlayerControlsVisibilityObserver by PlayerControlsVisibilityObserverImpl(controller) { - /** - * The last event captured in [onDown]. - */ - private var lastOnDownEvent: MotionEvent? = null - - override val shouldForceInterceptEvents: Boolean - get() { - val origin = lastOnDownEvent?.toPoint() ?: return false - return swipeActionAt(origin, currentSwipe) != SwipeZoneAction.OFF - } - - override fun shouldDropMotion(motionEvent: MotionEvent): Boolean { - // ignore gestures with more than one pointer - // when such a gesture is detected, dispatch the first event of the gesture to downstream - if (motionEvent.pointerCount > 1) { - lastOnDownEvent?.let { - controller.dispatchDownstreamTouchEvent(it) - it.recycle() - } - lastOnDownEvent = null - return true - } - - // ignore gestures when player controls are visible - return arePlayerControlsVisible - } - - override fun onUp(motionEvent: MotionEvent) { - super.onUp(motionEvent) - lastOnDownEvent?.recycle() - lastOnDownEvent = null - } - - override fun onDown(motionEvent: MotionEvent): Boolean { - // save the event for later - lastOnDownEvent?.recycle() - lastOnDownEvent = MotionEvent.obtain(motionEvent) - - // must be inside swipe zone - return isInSwipeZone(motionEvent) - } - - override fun onSingleTapUp(motionEvent: MotionEvent): Boolean { - MotionEvent.obtain(motionEvent).let { - it.action = MotionEvent.ACTION_DOWN - controller.dispatchDownstreamTouchEvent(it) - it.recycle() - } - - return false - } - - override fun onDoubleTapEvent(motionEvent: MotionEvent): Boolean { - MotionEvent.obtain(motionEvent).let { - controller.dispatchDownstreamTouchEvent(it) - it.recycle() - } - - return super.onDoubleTapEvent(motionEvent) - } - - override fun onLongPress(motionEvent: MotionEvent) { - MotionEvent.obtain(motionEvent).let { - controller.dispatchDownstreamTouchEvent(it) - it.recycle() - } - - super.onLongPress(motionEvent) - } - - override fun onSwipe( - from: MotionEvent, - to: MotionEvent, - distanceX: Double, - distanceY: Double, - ): Boolean { - // cancel if not fullscreen or multi-window - if (!controller.config.isFullscreenOrMultiWindowVideo) return false - // cancel if the swipe does not belong to any zone - if (!shouldForceInterceptEvents) return false - - return applySwipeAction(from, distanceX, distanceY) - } -} diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/controller/gesture/PressToSwipeController.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/controller/gesture/PressToSwipeController.java new file mode 100644 index 0000000000..3cd7c0b52c --- /dev/null +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/controller/gesture/PressToSwipeController.java @@ -0,0 +1,79 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches + * + * Original hard forked code: + * https://github.com/ReVanced/revanced-patches/commit/724e6d61b2ecd868c1a9a37d465a688e83a74799 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to Morphe contributions. + */ + +package app.morphe.extension.youtube.swipecontrols.controller.gesture; + +import android.view.MotionEvent; + +import androidx.annotation.Nullable; + +import app.morphe.extension.youtube.swipecontrols.SwipeControlsConfigurationProvider.SwipeZoneAction; +import app.morphe.extension.youtube.swipecontrols.SwipeControlsHostActivity; +import app.morphe.extension.youtube.swipecontrols.controller.gesture.core.BaseGestureController; +import app.morphe.extension.youtube.swipecontrols.misc.Point; + +/** + * Provides the press-to-swipe (PtS) swipe controls experience. + */ +public final class PressToSwipeController extends BaseGestureController { + + /** + * Where the current swipe session started, or null if there is no session. + */ + @Nullable + private Point swipeSessionOrigin; + + /** + * @param controller Reference to the main swipe controller. + */ + public PressToSwipeController(SwipeControlsHostActivity controller) { + super(controller); + } + + @Override + protected boolean shouldForceInterceptEvents() { + if (swipeSessionOrigin == null) return false; + return swipeActionAt(swipeSessionOrigin, getCurrentSwipe()) != SwipeZoneAction.OFF; + } + + @Override + protected boolean shouldDropMotion(MotionEvent motionEvent) { + return false; + } + + @Override + protected void onUp(MotionEvent motionEvent) { + super.onUp(motionEvent); + swipeSessionOrigin = null; + } + + @Override + public void onLongPress(MotionEvent motionEvent) { + // enter swipe session with feedback + swipeSessionOrigin = isInSwipeZone(motionEvent) ? Point.fromMotionEvent(motionEvent) : null; + if (swipeSessionOrigin != null) { + controller.getOverlay().onEnterSwipeSession(); + } + + // send GestureDetector a ACTION_CANCEL event so it will handle further events + motionEvent.setAction(MotionEvent.ACTION_CANCEL); + detector.onTouchEvent(motionEvent); + } + + @Override + protected boolean onSwipe(MotionEvent from, MotionEvent to, double distanceX, double distanceY) { + // cancel if not fullscreen or multi-window + if (!controller.getConfig().isFullscreenOrMultiWindowVideo()) return false; + // cancel if not in swipe session + if (swipeSessionOrigin == null) return false; + + return applySwipeAction(from, distanceX, distanceY); + } +} diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/controller/gesture/PressToSwipeController.kt b/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/controller/gesture/PressToSwipeController.kt deleted file mode 100644 index 3b1597d07a..0000000000 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/controller/gesture/PressToSwipeController.kt +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright 2026 Morphe. - * https://github.com/MorpheApp/morphe-patches - * - * Original hard forked code: - * https://github.com/ReVanced/revanced-patches/commit/724e6d61b2ecd868c1a9a37d465a688e83a74799 - * - * See the included NOTICE file for GPLv3 Section 7 terms that apply to Morphe contributions. - */ - -package app.morphe.extension.youtube.swipecontrols.controller.gesture - -import android.view.MotionEvent -import app.morphe.extension.youtube.swipecontrols.SwipeControlsConfigurationProvider.SwipeZoneAction -import app.morphe.extension.youtube.swipecontrols.SwipeControlsHostActivity -import app.morphe.extension.youtube.swipecontrols.controller.gesture.core.BaseGestureController -import app.morphe.extension.youtube.swipecontrols.misc.Point -import app.morphe.extension.youtube.swipecontrols.misc.toPoint - -/** - * Provides the press-to-swipe (PtS) swipe controls experience. - * - * @param controller Reference to the main swipe controller. - */ -class PressToSwipeController( - private val controller: SwipeControlsHostActivity, -) : BaseGestureController(controller) { - /** - * Where the current swipe session started, or null if there is no session. - */ - private var swipeSessionOrigin: Point? = null - - override val shouldForceInterceptEvents: Boolean - get() { - val origin = swipeSessionOrigin ?: return false - return swipeActionAt(origin, currentSwipe) != SwipeZoneAction.OFF - } - - override fun shouldDropMotion(motionEvent: MotionEvent): Boolean = false - - override fun onUp(motionEvent: MotionEvent) { - super.onUp(motionEvent) - swipeSessionOrigin = null - } - - override fun onLongPress(motionEvent: MotionEvent) { - // enter swipe session with feedback - swipeSessionOrigin = if (isInSwipeZone(motionEvent)) motionEvent.toPoint() else null - if (swipeSessionOrigin != null) { - controller.overlay.onEnterSwipeSession() - } - - // send GestureDetector a ACTION_CANCEL event so it will handle further events - motionEvent.action = MotionEvent.ACTION_CANCEL - detector.onTouchEvent(motionEvent) - } - - override fun onSwipe( - from: MotionEvent, - to: MotionEvent, - distanceX: Double, - distanceY: Double, - ): Boolean { - // cancel if not fullscreen or multi-window - if (!controller.config.isFullscreenOrMultiWindowVideo) return false - // cancel if not in swipe session - if (swipeSessionOrigin == null) return false - - return applySwipeAction(from, distanceX, distanceY) - } -} diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/controller/gesture/core/BaseGestureController.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/controller/gesture/core/BaseGestureController.java new file mode 100644 index 0000000000..bc5f10f02b --- /dev/null +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/controller/gesture/core/BaseGestureController.java @@ -0,0 +1,261 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches + * + * Original hard forked code: + * https://github.com/ReVanced/revanced-patches/commit/724e6d61b2ecd868c1a9a37d465a688e83a74799 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to Morphe contributions. + */ + +package app.morphe.extension.youtube.swipecontrols.controller.gesture.core; + +import android.view.GestureDetector; +import android.view.MotionEvent; + +import androidx.annotation.Nullable; + +import app.morphe.extension.youtube.swipecontrols.SwipeControlsConfigurationProvider; +import app.morphe.extension.youtube.swipecontrols.SwipeControlsConfigurationProvider.SwipeZoneAction; +import app.morphe.extension.youtube.swipecontrols.SwipeControlsHostActivity; +import app.morphe.extension.youtube.swipecontrols.controller.SwipeZonesController; +import app.morphe.extension.youtube.swipecontrols.controller.gesture.core.SwipeDetector.SwipeDirection; +import app.morphe.extension.youtube.swipecontrols.misc.Point; + +/** + * The common base of all {@link GestureController} classes. + * Handles most of the boilerplate code needed for gesture detection. + */ +public abstract class BaseGestureController extends GestureDetector.SimpleOnGestureListener + implements GestureController { + + /** + * Reference to the main swipe controller. + */ + protected final SwipeControlsHostActivity controller; + + private final SwipeDetector swipeDetector; + + private final VolumeAndBrightnessScroller scroller; + + /** + * The main gesture detector that powers everything. + */ + protected final GestureDetector detector; + + /** + * Whether downstream events have been canceled; used in {@link #onScroll}. + */ + private boolean didCancelDownstream; + + /** + * @param controller Reference to the main swipe controller. + */ + protected BaseGestureController(SwipeControlsHostActivity controller) { + this.controller = controller; + SwipeControlsConfigurationProvider config = controller.getConfig(); + swipeDetector = new SwipeDetector(config.getSwipeMagnitudeThreshold()); + scroller = new VolumeAndBrightnessScroller( + controller, + controller.getAudio(), + controller.getScreen(), + controller.getOverlay(), + config.getVolumeSwipeDistance(), + config.getBrightnessSwipeSensitivity(), + config.getSpeedSwipeSensitivity(), + config.getSpeedStepInt(), + config.isSpeedControlEnabled() + ); + detector = new GestureDetector(controller, this); + } + + @Override + public boolean submitTouchEvent(MotionEvent motionEvent) { + SwipeControlsConfigurationProvider config = controller.getConfig(); + + // Ignore if swipe is disabled. + if (!config.isSwipeControlsEnabled()) { + return false; + } + + // Ignore if status bar is visible (unless the screen is shared with another app). + if (controller.isStatusBarVisible() && !controller.isInSplitScreenMode()) { + return false; + } + + // Ignore if the native lock screen is engaged. + if (config.shouldIgnoreSwipesWhenLocked() && SwipeControlsHostActivity.isPlayerLocked()) { + return false; + } + + // Create a copy of the event so we can modify it without causing any issues downstream. + MotionEvent me = MotionEvent.obtain(motionEvent); + + // Check if we should drop this motion. + final boolean dropped = shouldDropMotion(me); + if (dropped) { + me.setAction(MotionEvent.ACTION_CANCEL); + } + + // Send the event to the detector if we force intercept events, the event is always consumed. + final boolean consumed = detector.onTouchEvent(me) || shouldForceInterceptEvents(); + + // Evaluate swipe zone before recycling. + final boolean inSwipeZone = isInSwipeZone(me); + + // Invoke the custom onUp handler. + if (me.getAction() == MotionEvent.ACTION_UP || me.getAction() == MotionEvent.ACTION_CANCEL) { + onUp(me); + } + + // Recycle the copy. + me.recycle(); + + // Do not consume dropped events or events outside any swipe zone. + return !dropped && consumed && inSwipeZone; + } + + /** + * Custom handler for {@link MotionEvent#ACTION_UP} events, since GestureDetector doesn't provide one. + * + * @param motionEvent The motion event. + */ + protected void onUp(MotionEvent motionEvent) { + didCancelDownstream = false; + swipeDetector.resetSwipe(); + scroller.resetScroller(); + } + + @Override + public boolean onScroll(@Nullable MotionEvent from, MotionEvent to, float distanceX, float distanceY) { + if (from == null) { + return false; + } + + // Submit to swipe detector. + swipeDetector.submitForSwipe(from, to); + + // Call swipe callback if in a swipe. + if (getCurrentSwipe() == SwipeDirection.NONE) { + return false; + } + + final boolean consumed = onSwipe(from, to, distanceX, distanceY); + + // if the swipe was consumed, cancel downstream events once + if (consumed && !didCancelDownstream) { + didCancelDownstream = true; + MotionEvent cancelEvent = MotionEvent.obtain(from); + cancelEvent.setAction(MotionEvent.ACTION_CANCEL); + controller.dispatchDownstreamTouchEvent(cancelEvent); + cancelEvent.recycle(); + } + + return consumed; + } + + /** + * The currently detected swipe. + */ + protected SwipeDirection getCurrentSwipe() { + return swipeDetector.getCurrentSwipe(); + } + + /** + * Whether {@link #submitTouchEvent} should force-intercept all touch events. + */ + protected abstract boolean shouldForceInterceptEvents(); + + /** + * Checks if the provided motion event is in any active swipe zone. + * + * @param motionEvent The event to check. + * @return Whether the event is in any active swipe zone. + */ + protected boolean isInSwipeZone(MotionEvent motionEvent) { + SwipeControlsConfigurationProvider config = controller.getConfig(); + SwipeZonesController zones = controller.getZones(); + Point point = Point.fromMotionEvent(motionEvent); + return (config.getTopZoneAction() != SwipeZoneAction.OFF && zones.getTop().contains(point)) + || (config.getLeftZoneAction() != SwipeZoneAction.OFF && zones.getLeft().contains(point)) + || (config.getRightZoneAction() != SwipeZoneAction.OFF && zones.getRight().contains(point)); + } + + /** + * Resolves the action of the zone a swipe belongs to. + *

+ * The side zones own vertical swipes and the top zone owns horizontal ones. The zones overlap, + * so the direction is what keeps that overlap unambiguous, and it leaves every other gesture + * to the player instead of swallowing it. + * + * @param origin Where the swipe started. + * @param direction The direction of the swipe. + * @return The action to apply, or {@link SwipeZoneAction#OFF} if the swipe belongs to no zone. + */ + protected SwipeZoneAction swipeActionAt(Point origin, SwipeDirection direction) { + SwipeControlsConfigurationProvider config = controller.getConfig(); + SwipeZonesController zones = controller.getZones(); + + if (direction == SwipeDirection.HORIZONTAL) { + return zones.getTop().contains(origin) + ? config.getTopZoneAction() + : SwipeZoneAction.OFF; + } + + if (direction == SwipeDirection.VERTICAL) { + if (zones.getLeft().contains(origin)) { + return config.getLeftZoneAction(); + } + if (zones.getRight().contains(origin)) { + return config.getRightZoneAction(); + } + } + + return SwipeZoneAction.OFF; + } + + /** + * Applies the action of the zone the swipe started in. + * + * @param from Start event of the swipe. + * @param distanceX The horizontal distance of the swipe. + * @param distanceY The vertical distance of the swipe. + * @return Whether the swipe was consumed. + */ + protected boolean applySwipeAction(MotionEvent from, double distanceX, double distanceY) { + SwipeDirection direction = getCurrentSwipe(); + final double distance = direction == SwipeDirection.HORIZONTAL ? -distanceX : distanceY; + + switch (swipeActionAt(Point.fromMotionEvent(from), direction)) { + case VOLUME -> scroller.scrollVolume(distance); + case BRIGHTNESS -> scroller.scrollBrightness(distance); + case SPEED -> scroller.scrollSpeed(distance); + case OFF -> { + return false; + } + } + + return true; + } + + /** + * Checks if a touch event should be dropped. + * When an event is dropped, the gesture detector receives a {@link MotionEvent#ACTION_CANCEL} event and the event is not consumed. + * + * @param motionEvent The event to check. + * @return Whether the event should be dropped. + */ + protected abstract boolean shouldDropMotion(MotionEvent motionEvent); + + /** + * Handler for swipe events, once a swipe is detected. + * The direction of the swipe can be accessed in {@link #getCurrentSwipe()}. + * + * @param from Start event of the swipe. + * @param to End event of the swipe. + * @param distanceX The horizontal distance of the swipe. + * @param distanceY The vertical distance of the swipe. + * @return Whether the event was consumed. + */ + protected abstract boolean onSwipe(MotionEvent from, MotionEvent to, double distanceX, double distanceY); +} diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/controller/gesture/core/BaseGestureController.kt b/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/controller/gesture/core/BaseGestureController.kt deleted file mode 100644 index ecc8cbe5d9..0000000000 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/controller/gesture/core/BaseGestureController.kt +++ /dev/null @@ -1,239 +0,0 @@ -/* - * Copyright 2026 Morphe. - * https://github.com/MorpheApp/morphe-patches - * - * Original hard forked code: - * https://github.com/ReVanced/revanced-patches/commit/724e6d61b2ecd868c1a9a37d465a688e83a74799 - * - * See the included NOTICE file for GPLv3 Section 7 terms that apply to Morphe contributions. - */ - -package app.morphe.extension.youtube.swipecontrols.controller.gesture.core - -import android.view.GestureDetector -import android.view.MotionEvent -import app.morphe.extension.youtube.swipecontrols.SwipeControlsConfigurationProvider.SwipeZoneAction -import app.morphe.extension.youtube.swipecontrols.SwipeControlsHostActivity -import app.morphe.extension.youtube.swipecontrols.misc.Point -import app.morphe.extension.youtube.swipecontrols.misc.contains -import app.morphe.extension.youtube.swipecontrols.misc.toPoint - -/** - * The common base of all [GestureController] classes. - * Handles most of the boilerplate code needed for gesture detection. - * - * @param controller Reference to the main swipe controller. - */ -abstract class BaseGestureController( - private val controller: SwipeControlsHostActivity, -) : GestureController, - GestureDetector.SimpleOnGestureListener(), - SwipeDetector by SwipeDetectorImpl( - controller.config.swipeMagnitudeThreshold.toDouble(), - ), - VolumeAndBrightnessScroller by VolumeAndBrightnessScrollerImpl( - controller, - controller.audio, - controller.screen, - controller.overlay, - controller.config.volumeSwipeDistance, - controller.config.brightnessSwipeSensitivity, - controller.config.speedSwipeSensitivity, - controller.config.speedStepInt, - controller.config.enableSpeedGestureControl, - ) { - - /** - * The main gesture detector that powers everything. - */ - @Suppress("LeakingThis") - protected val detector = GestureDetector(controller, this) - - /** - * Whether downstream events have been canceled; used in [onScroll]. - */ - private var didCancelDownstream = false - - override fun submitTouchEvent(motionEvent: MotionEvent): Boolean { - // Ignore if swipe is disabled. - if (!controller.config.enableSwipeControls) { - return false - } - - // Ignore if status bar is visible (unless the screen is shared with another app). - if (controller.statusBarVisible && !controller.isInSplitScreenMode) { - return false - } - - // Ignore if the native lock screen is engaged. - if (controller.config.shouldIgnoreSwipesWhenLocked && SwipeControlsHostActivity.isPlayerLocked) { - return false - } - - // Create a copy of the event so we can modify it without causing any issues downstream. - val me = MotionEvent.obtain(motionEvent) - - // Check if we should drop this motion. - val dropped = shouldDropMotion(me) - if (dropped) { - me.action = MotionEvent.ACTION_CANCEL - } - - // Send the event to the detector if we force intercept events, the event is always consumed. - val consumed = detector.onTouchEvent(me) || shouldForceInterceptEvents - - // Evaluate swipe zone before recycling. - val inSwipeZone = isInSwipeZone(me) - - // Invoke the custom onUp handler. - if (me.action == MotionEvent.ACTION_UP || me.action == MotionEvent.ACTION_CANCEL) { - onUp(me) - } - - // Recycle the copy. - me.recycle() - - // Do not consume dropped events or events outside any swipe zone. - return !dropped && consumed && inSwipeZone - } - - /** - * Custom handler for [MotionEvent.ACTION_UP] events, since GestureDetector doesn't provide one. - * - * @param motionEvent The motion event. - */ - open fun onUp(motionEvent: MotionEvent) { - didCancelDownstream = false - resetSwipe() - resetScroller() - } - - override fun onScroll( - from: MotionEvent?, - to: MotionEvent, - distanceX: Float, - distanceY: Float, - ): Boolean { - if (from == null) { - return false - } - - // Submit to swipe detector. - submitForSwipe(from, to, distanceX, distanceY) - - // Call swipe callback if in a swipe. - return if (currentSwipe != SwipeDetector.SwipeDirection.NONE) { - val consumed = onSwipe( - from, - to, - distanceX.toDouble(), - distanceY.toDouble(), - ) - - // if the swipe was consumed, cancel downstream events once - if (consumed && !didCancelDownstream) { - didCancelDownstream = true - MotionEvent.obtain(from).let { - it.action = MotionEvent.ACTION_CANCEL - controller.dispatchDownstreamTouchEvent(it) - it.recycle() - } - } - - consumed - } else { - false - } - } - - /** - * Whether [submitTouchEvent] should force-intercept all touch events. - */ - abstract val shouldForceInterceptEvents: Boolean - - /** - * Checks if the provided motion event is in any active swipe zone. - * - * @param motionEvent The event to check. - * @return Whether the event is in any active swipe zone. - */ - fun isInSwipeZone(motionEvent: MotionEvent): Boolean { - val point = motionEvent.toPoint() - return (controller.config.topZoneAction != SwipeZoneAction.OFF && point in controller.zones.top) || - (controller.config.leftZoneAction != SwipeZoneAction.OFF && point in controller.zones.left) || - (controller.config.rightZoneAction != SwipeZoneAction.OFF && point in controller.zones.right) - } - - /** - * Resolves the action of the zone a swipe belongs to. - * - * The side zones own vertical swipes and the top zone owns horizontal ones. The zones overlap, - * so the direction is what keeps that overlap unambiguous, and it leaves every other gesture - * to the player instead of swallowing it. - * - * @param origin Where the swipe started. - * @param direction The direction of the swipe. - * @return The action to apply, or [SwipeZoneAction.OFF] if the swipe belongs to no zone. - */ - protected fun swipeActionAt(origin: Point, direction: SwipeDetector.SwipeDirection): SwipeZoneAction = - when (direction) { - SwipeDetector.SwipeDirection.HORIZONTAL -> - if (origin in controller.zones.top) controller.config.topZoneAction else SwipeZoneAction.OFF - - SwipeDetector.SwipeDirection.VERTICAL -> when (origin) { - in controller.zones.left -> controller.config.leftZoneAction - in controller.zones.right -> controller.config.rightZoneAction - else -> SwipeZoneAction.OFF - } - - else -> SwipeZoneAction.OFF - } - - /** - * Applies the action of the zone the swipe started in. - * - * @param from Start event of the swipe. - * @param distanceX The horizontal distance of the swipe. - * @param distanceY The vertical distance of the swipe. - * @return Whether the swipe was consumed. - */ - protected fun applySwipeAction(from: MotionEvent, distanceX: Double, distanceY: Double): Boolean { - val direction = currentSwipe - val distance = if (direction == SwipeDetector.SwipeDirection.HORIZONTAL) -distanceX else distanceY - - when (swipeActionAt(from.toPoint(), direction)) { - SwipeZoneAction.VOLUME -> scrollVolume(distance) - SwipeZoneAction.BRIGHTNESS -> scrollBrightness(distance) - SwipeZoneAction.SPEED -> scrollSpeed(distance) - SwipeZoneAction.OFF -> return false - } - - return true - } - - /** - * Checks if a touch event should be dropped. - * When an event is dropped, the gesture detector receives a [MotionEvent.ACTION_CANCEL] event and the event is not consumed. - * - * @param motionEvent The event to check. - * @return Whether the event should be dropped. - */ - abstract fun shouldDropMotion(motionEvent: MotionEvent): Boolean - - /** - * Handler for swipe events, once a swipe is detected. - * The direction of the swipe can be accessed in [currentSwipe]. - * - * @param from Start event of the swipe. - * @param to End event of the swipe. - * @param distanceX The horizontal distance of the swipe. - * @param distanceY The vertical distance of the swipe. - * @return Whether the event was consumed. - */ - abstract fun onSwipe( - from: MotionEvent, - to: MotionEvent, - distanceX: Double, - distanceY: Double, - ): Boolean -} diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/controller/gesture/core/GestureController.kt b/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/controller/gesture/core/GestureController.java similarity index 72% rename from extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/controller/gesture/core/GestureController.kt rename to extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/controller/gesture/core/GestureController.java index 9f3a730f06..0512c92a94 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/controller/gesture/core/GestureController.kt +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/controller/gesture/core/GestureController.java @@ -1,16 +1,16 @@ -package app.morphe.extension.youtube.swipecontrols.controller.gesture.core +package app.morphe.extension.youtube.swipecontrols.controller.gesture.core; -import android.view.MotionEvent +import android.view.MotionEvent; /** * Describes a class that accepts motion events and detects gestures. */ -interface GestureController { +public interface GestureController { /** * Accepts a touch event and tries to detect the desired gestures using it. * * @param motionEvent The motion event that was submitted. * @return Whether a gesture was detected. */ - fun submitTouchEvent(motionEvent: MotionEvent): Boolean + boolean submitTouchEvent(MotionEvent motionEvent); } diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/controller/gesture/core/SwipeDetector.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/controller/gesture/core/SwipeDetector.java new file mode 100644 index 0000000000..72131d91a4 --- /dev/null +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/controller/gesture/core/SwipeDetector.java @@ -0,0 +1,93 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches + * + * Original hard forked code: + * https://github.com/ReVanced/revanced-patches/commit/724e6d61b2ecd868c1a9a37d465a688e83a74799 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to Morphe contributions. + */ + +package app.morphe.extension.youtube.swipecontrols.controller.gesture.core; + +import android.view.MotionEvent; + +/** + * Detector that can detect swipes and their directionality. + */ +public final class SwipeDetector { + + /** + * Direction of a swipe. + */ + public enum SwipeDirection { + /** + * Swipe has no direction or no swipe detected. + */ + NONE, + + /** + * Swipe along the X axis. + */ + HORIZONTAL, + + /** + * Swipe along the Y axis. + */ + VERTICAL + } + + /** + * Minimum magnitude before a swipe is detected as such. + */ + private final double swipeMagnitudeThreshold; + + /** + * The currently detected swipe. + */ + private SwipeDirection currentSwipe = SwipeDirection.NONE; + + /** + * @param swipeMagnitudeThreshold Minimum magnitude before a swipe is detected as such. + */ + public SwipeDetector(double swipeMagnitudeThreshold) { + this.swipeMagnitudeThreshold = swipeMagnitudeThreshold; + } + + /** + * The currently detected swipe. + */ + public SwipeDirection getCurrentSwipe() { + return currentSwipe; + } + + /** + * Submits an onScroll event for swipe detection. + * + * @param from Start event. + * @param to End event. + */ + public void submitForSwipe(MotionEvent from, MotionEvent to) { + if (currentSwipe == SwipeDirection.NONE) { + // no swipe direction was detected yet, try to detect one + // if the user did not swipe far enough, we cannot detect what direction they swiped + // so we wait until a greater distance was swiped + // NOTE: sqrt() can be high- cost, so using squared magnitudes here + final float deltaX = Math.abs(to.getX() - from.getX()); + final float deltaY = Math.abs(to.getY() - from.getY()); + final float swipeMagnitudeSquared = deltaX * deltaX + deltaY * deltaY; + if (swipeMagnitudeSquared > swipeMagnitudeThreshold * swipeMagnitudeThreshold) { + currentSwipe = deltaY > deltaX + ? SwipeDirection.VERTICAL + : SwipeDirection.HORIZONTAL; + } + } + } + + /** + * Resets the swipe detection. + */ + public void resetSwipe() { + currentSwipe = SwipeDirection.NONE; + } +} diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/controller/gesture/core/SwipeDetector.kt b/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/controller/gesture/core/SwipeDetector.kt deleted file mode 100644 index ed56607471..0000000000 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/controller/gesture/core/SwipeDetector.kt +++ /dev/null @@ -1,94 +0,0 @@ -package app.morphe.extension.youtube.swipecontrols.controller.gesture.core - -import android.view.MotionEvent -import kotlin.math.abs -import kotlin.math.pow - -/** - * Describes a class that can detect swipes and their directionality. - */ -interface SwipeDetector { - /** - * The currently detected swipe. - */ - val currentSwipe: SwipeDirection - - /** - * Submits an onScroll event for swipe detection. - * - * @param from Start event. - * @param to End event. - * @param distanceX Horizontal scroll distance. - * @param distanceY Vertical scroll distance. - */ - fun submitForSwipe( - from: MotionEvent, - to: MotionEvent, - distanceX: Float, - distanceY: Float, - ) - - /** - * Resets the swipe detection. - */ - fun resetSwipe() - - /** - * Direction of a swipe. - */ - enum class SwipeDirection { - /** - * Swipe has no direction or no swipe detected. - */ - NONE, - - /** - * Swipe along the X axis. - */ - HORIZONTAL, - - /** - * Swipe along the Y axis. - */ - VERTICAL, - } -} - -/** - * Detector that can detect swipes and their directionality. - * - * @param swipeMagnitudeThreshold Minimum magnitude before a swipe is detected as such. - */ -class SwipeDetectorImpl( - private val swipeMagnitudeThreshold: Double, -) : SwipeDetector { - override var currentSwipe = SwipeDetector.SwipeDirection.NONE - - override fun submitForSwipe( - from: MotionEvent, - to: MotionEvent, - distanceX: Float, - distanceY: Float, - ) { - if (currentSwipe == SwipeDetector.SwipeDirection.NONE) { - // no swipe direction was detected yet, try to detect one - // if the user did not swipe far enough, we cannot detect what direction they swiped - // so we wait until a greater distance was swiped - // NOTE: sqrt() can be high- cost, so using squared magnitudes here - val deltaX = abs(to.x - from.x) - val deltaY = abs(to.y - from.y) - val swipeMagnitudeSquared = deltaX.pow(2) + deltaY.pow(2) - if (swipeMagnitudeSquared > swipeMagnitudeThreshold.pow(2)) { - currentSwipe = if (deltaY > deltaX) { - SwipeDetector.SwipeDirection.VERTICAL - } else { - SwipeDetector.SwipeDirection.HORIZONTAL - } - } - } - } - - override fun resetSwipe() { - currentSwipe = SwipeDetector.SwipeDirection.NONE - } -} diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/controller/gesture/core/VolumeAndBrightnessScroller.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/controller/gesture/core/VolumeAndBrightnessScroller.java new file mode 100644 index 0000000000..6c51e4d2ba --- /dev/null +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/controller/gesture/core/VolumeAndBrightnessScroller.java @@ -0,0 +1,152 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches + * + * Original hard forked code: + * https://github.com/ReVanced/revanced-patches/commit/724e6d61b2ecd868c1a9a37d465a688e83a74799 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to Morphe contributions. + */ + +package app.morphe.extension.youtube.swipecontrols.controller.gesture.core; + +import android.content.Context; +import android.util.TypedValue; + +import androidx.annotation.Nullable; + +import app.morphe.extension.youtube.patches.VideoInformation; +import app.morphe.extension.youtube.swipecontrols.controller.AudioVolumeController; +import app.morphe.extension.youtube.swipecontrols.controller.ScreenBrightnessController; +import app.morphe.extension.youtube.swipecontrols.misc.ScrollDistanceHelper; +import app.morphe.extension.youtube.swipecontrols.misc.SwipeControlsOverlay; +import app.morphe.extension.youtube.swipecontrols.misc.SwipeControlsUtils; + +/** + * Handles scrolling of volume, brightness, and playback speed, adjusts them using the provided controllers and updates the overlay. + */ +public final class VolumeAndBrightnessScroller { + + /** + * Minimum playback speed of the speed gesture, multiplied by 100. + */ + private static final int MINIMUM_SPEED_INT = 25; + + /** + * Maximum playback speed of the speed gesture, multiplied by 100. + */ + private static final int MAXIMUM_SPEED_INT = 800; + + private final ScrollDistanceHelper volumeScroller; + private final ScrollDistanceHelper brightnessScroller; + private final ScrollDistanceHelper speedScroller; + + /** + * @param context Context to create the scrollers in. + * @param volumeController Volume controller instance. If null, volume control is disabled. + * @param screenController Screen brightness controller instance. If null, brightness control is disabled. + * @param overlayController Overlay controller instance. + * @param volumeDistance Unit distance for volume scrolling, in dp. + * @param brightnessDistance Unit distance for brightness scrolling, in dp; higher = more precise. + * @param speedDistance Unit distance for speed scrolling, in dp; higher = more precise. + * @param speedStepInt Playback speed change per tick, expressed as an integer multiplied by 100 (e.g. 5 = 0.05x). + * @param enableSpeedGesture Whether the playback speed swipe gesture is enabled. + */ + public VolumeAndBrightnessScroller( + Context context, + @Nullable AudioVolumeController volumeController, + @Nullable ScreenBrightnessController screenController, + SwipeControlsOverlay overlayController, + int volumeDistance, + int brightnessDistance, + int speedDistance, + int speedStepInt, + boolean enableSpeedGesture + ) { + volumeScroller = new ScrollDistanceHelper( + SwipeControlsUtils.applyDimension(context, TypedValue.COMPLEX_UNIT_DIP, volumeDistance), + direction -> { + if (volumeController != null) { + volumeController.adjustVolumeBySteps(direction); + overlayController.onVolumeChanged( + volumeController.getSteppedVolume(), + volumeController.getSteppedMaxVolume(), + volumeController.getBoostStep() + ); + } + } + ); + + brightnessScroller = new ScrollDistanceHelper( + SwipeControlsUtils.applyDimension(context, TypedValue.COMPLEX_UNIT_DIP, brightnessDistance), + direction -> { + if (screenController != null) { + final double screenBrightness = screenController.getScreenBrightness(); + final boolean shouldAdjustBrightness; + if (screenController.host.getConfig().shouldLowestValueEnableAutoBrightness()) { + shouldAdjustBrightness = screenBrightness > 0 || direction > 0; + } else { + shouldAdjustBrightness = screenBrightness >= 0 || direction >= 0; + } + + if (shouldAdjustBrightness) { + screenController.setScreenBrightness(screenBrightness + direction); + } else { + screenController.restoreDefaultBrightness(); + } + overlayController.onBrightnessChanged(screenController.getScreenBrightness()); + } + } + ); + + speedScroller = new ScrollDistanceHelper( + SwipeControlsUtils.applyDimension(context, TypedValue.COMPLEX_UNIT_DIP, speedDistance), + direction -> { + if (!enableSpeedGesture) return; + // Integer math (x100) avoids float drift like 1.04999 instead of 1.05. + final int currentSpeedInt = Math.round(VideoInformation.getPlaybackSpeed() * 100); + final int newSpeedInt = Math.max(MINIMUM_SPEED_INT, + Math.min(MAXIMUM_SPEED_INT, currentSpeedInt + direction * speedStepInt)); + final float newSpeed = newSpeedInt / 100f; + VideoInformation.changePlaybackSpeed(newSpeed); + overlayController.onSpeedChanged(newSpeed); + } + ); + } + + /** + * Submits a scroll for volume adjustment. + * + * @param distance The scroll distance. + */ + public void scrollVolume(double distance) { + volumeScroller.add(distance); + } + + /** + * Submits a scroll for brightness adjustment. + * + * @param distance The scroll distance. + */ + public void scrollBrightness(double distance) { + brightnessScroller.add(distance); + } + + /** + * Submits a scroll for playback speed adjustment. + * + * @param distance The scroll distance. + */ + public void scrollSpeed(double distance) { + speedScroller.add(distance); + } + + /** + * Resets all scroll distances to zero. + */ + public void resetScroller() { + volumeScroller.reset(); + brightnessScroller.reset(); + speedScroller.reset(); + } +} diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/controller/gesture/core/VolumeAndBrightnessScroller.kt b/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/controller/gesture/core/VolumeAndBrightnessScroller.kt deleted file mode 100644 index e75d157377..0000000000 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/controller/gesture/core/VolumeAndBrightnessScroller.kt +++ /dev/null @@ -1,148 +0,0 @@ -/* - * Copyright 2026 Morphe. - * https://github.com/MorpheApp/morphe-patches - * - * Original hard forked code: - * https://github.com/ReVanced/revanced-patches/commit/724e6d61b2ecd868c1a9a37d465a688e83a74799 - * - * See the included NOTICE file for GPLv3 Section 7 terms that apply to Morphe contributions. - */ - -package app.morphe.extension.youtube.swipecontrols.controller.gesture.core - -import android.content.Context -import android.util.TypedValue -import app.morphe.extension.youtube.patches.VideoInformation -import app.morphe.extension.youtube.swipecontrols.controller.AudioVolumeController -import app.morphe.extension.youtube.swipecontrols.controller.ScreenBrightnessController -import app.morphe.extension.youtube.swipecontrols.misc.ScrollDistanceHelper -import app.morphe.extension.youtube.swipecontrols.misc.SwipeControlsOverlay -import app.morphe.extension.youtube.swipecontrols.misc.applyDimension -import kotlin.math.roundToInt - -/** - * Describes a class that controls volume, brightness, and playback speed based on scrolling events. - */ -interface VolumeAndBrightnessScroller { - /** - * Submits a scroll for volume adjustment. - * - * @param distance The scroll distance. - */ - fun scrollVolume(distance: Double) - - /** - * Submits a scroll for brightness adjustment. - * - * @param distance The scroll distance. - */ - fun scrollBrightness(distance: Double) - - /** - * Submits a scroll for playback speed adjustment. - * - * @param distance The scroll distance. - */ - fun scrollSpeed(distance: Double) - - /** - * Resets all scroll distances to zero. - */ - fun resetScroller() -} - -/** - * Handles scrolling of volume, brightness, and playback speed, adjusts them using the provided controllers and updates the overlay. - * - * @param context Context to create the scrollers in. - * @param volumeController Volume controller instance. If null, volume control is disabled. - * @param screenController Screen brightness controller instance. If null, brightness control is disabled. - * @param overlayController Overlay controller instance. - * @param volumeDistance Unit distance for volume scrolling, in dp. - * @param brightnessDistance Unit distance for brightness scrolling, in dp; higher = more precise. - * @param speedDistance Unit distance for speed scrolling, in dp; higher = more precise. - * @param speedStepInt Playback speed change per tick, expressed as an integer multiplied by 100 (e.g. 5 = 0.05x). - * @param enableSpeedGesture Whether the playback speed swipe gesture is enabled. - */ -class VolumeAndBrightnessScrollerImpl( - context: Context, - private val volumeController: AudioVolumeController?, - private val screenController: ScreenBrightnessController?, - private val overlayController: SwipeControlsOverlay, - volumeDistance: Int = 10, - brightnessDistance: Int = 1, - speedDistance: Int = 10, - private val speedStepInt: Int = 5, - private val enableSpeedGesture: Boolean = false, -) : VolumeAndBrightnessScroller { - - // region volume - private val volumeScroller = - ScrollDistanceHelper( - volumeDistance.applyDimension( - context, - TypedValue.COMPLEX_UNIT_DIP, - ), - ) { _, _, direction -> - volumeController?.run { - adjustVolumeBySteps(direction) - overlayController.onVolumeChanged(steppedVolume, steppedMaxVolume, boostStep) - } - } - - override fun scrollVolume(distance: Double) = volumeScroller.add(distance) - //endregion - - //region brightness - private val brightnessScroller = - ScrollDistanceHelper( - brightnessDistance.applyDimension( - context, - TypedValue.COMPLEX_UNIT_DIP, - ), - ) { _, _, direction -> - screenController?.run { - val shouldAdjustBrightness = if (host.config.shouldLowestValueEnableAutoBrightness) { - screenBrightness > 0 || direction > 0 - } else { - screenBrightness >= 0 || direction >= 0 - } - - if (shouldAdjustBrightness) { - screenBrightness += direction - } else { - restoreDefaultBrightness() - } - overlayController.onBrightnessChanged(screenBrightness) - } - } - - override fun scrollBrightness(distance: Double) = brightnessScroller.add(distance) - //endregion - - //region speed - private val speedScroller = - ScrollDistanceHelper( - speedDistance.applyDimension( - context, - TypedValue.COMPLEX_UNIT_DIP, - ), - ) { _, _, direction -> - if (!enableSpeedGesture) return@ScrollDistanceHelper - // Integer math (×100) avoids float drift like 1.04999 instead of 1.05. - val currentSpeedInt = (VideoInformation.getPlaybackSpeed() * 100).roundToInt() - val newSpeedInt = maxOf(25, minOf(800, currentSpeedInt + direction * speedStepInt)) - val newSpeed = newSpeedInt / 100f - VideoInformation.changePlaybackSpeed(newSpeed) - overlayController.onSpeedChanged(newSpeed) - } - - override fun scrollSpeed(distance: Double) = speedScroller.add(distance) - //endregion - - override fun resetScroller() { - volumeScroller.reset() - brightnessScroller.reset() - speedScroller.reset() - } -} diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/misc/Point.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/misc/Point.java new file mode 100644 index 0000000000..c9c4ab545c --- /dev/null +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/misc/Point.java @@ -0,0 +1,26 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches + * + * Original hard forked code: + * https://github.com/ReVanced/revanced-patches/commit/724e6d61b2ecd868c1a9a37d465a688e83a74799 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to Morphe contributions. + */ + +package app.morphe.extension.youtube.swipecontrols.misc; + +import android.view.MotionEvent; + +/** + * A simple 2D point class. + */ +public record Point(int x, int y) { + + /** + * Converts the motion event coordinates to a point. + */ + public static Point fromMotionEvent(MotionEvent event) { + return new Point((int) event.getX(), (int) event.getY()); + } +} diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/misc/Point.kt b/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/misc/Point.kt deleted file mode 100644 index c9cdd8e21f..0000000000 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/misc/Point.kt +++ /dev/null @@ -1,17 +0,0 @@ -package app.morphe.extension.youtube.swipecontrols.misc - -import android.view.MotionEvent - -/** - * A simple 2D point class. - */ -data class Point( - val x: Int, - val y: Int, -) - -/** - * Converts the motion event coordinates to a point. - */ -fun MotionEvent.toPoint(): Point = - Point(x.toInt(), y.toInt()) diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/misc/Rectangle.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/misc/Rectangle.java new file mode 100644 index 0000000000..b35b1a383d --- /dev/null +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/misc/Rectangle.java @@ -0,0 +1,43 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches + * + * Original hard forked code: + * https://github.com/ReVanced/revanced-patches/commit/724e6d61b2ecd868c1a9a37d465a688e83a74799 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to Morphe contributions. + */ + +package app.morphe.extension.youtube.swipecontrols.misc; + +/** + * A simple rectangle class. + */ +public final class Rectangle { + public final int x; + public final int y; + public final int width; + public final int height; + public final int left; + public final int right; + public final int top; + public final int bottom; + + public Rectangle(int x, int y, int width, int height) { + this.x = x; + this.y = y; + this.width = width; + this.height = height; + left = x; + right = x + width; + top = y; + bottom = y + height; + } + + /** + * Checks if the point is within this rectangle. + */ + public boolean contains(Point p) { + return p.x() >= left && p.x() <= right && p.y() >= top && p.y() <= bottom; + } +} diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/misc/Rectangle.kt b/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/misc/Rectangle.kt deleted file mode 100644 index 1944c967b6..0000000000 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/misc/Rectangle.kt +++ /dev/null @@ -1,22 +0,0 @@ -package app.morphe.extension.youtube.swipecontrols.misc - -/** - * A simple rectangle class. - */ -data class Rectangle( - val x: Int, - val y: Int, - val width: Int, - val height: Int, -) { - val left = x - val right = x + width - val top = y - val bottom = y + height -} - -/** - * Checks if the point is within this rectangle. - */ -operator fun Rectangle.contains(p: Point): Boolean = - p.x in left..right && p.y in top..bottom diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/misc/ScrollDistanceHelper.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/misc/ScrollDistanceHelper.java new file mode 100644 index 0000000000..d13d7a0ff6 --- /dev/null +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/misc/ScrollDistanceHelper.java @@ -0,0 +1,73 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches + * + * Original hard forked code: + * https://github.com/ReVanced/revanced-patches/commit/724e6d61b2ecd868c1a9a37d465a688e83a74799 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to Morphe contributions. + */ + +package app.morphe.extension.youtube.swipecontrols.misc; + +import java.util.function.IntConsumer; + +/** + * Helper for scaling the onScroll handler. + */ +public final class ScrollDistanceHelper { + + /** + * Absolute distance after which the callback is invoked. + */ + private final int unitDistance; + + /** + * Callback invoked with the scroll direction when the unit distance is reached. + */ + private final IntConsumer callback; + + /** + * Total distance scrolled. + */ + private double scrolledDistance; + + /** + * @param unitDistance Absolute distance after which the callback is invoked. + * @param callback Callback invoked with the scroll direction when the unit distance is reached. + */ + public ScrollDistanceHelper(int unitDistance, IntConsumer callback) { + this.unitDistance = unitDistance; + this.callback = callback; + } + + /** + * Adds a scrolled distance to the total. + * If the {@link #unitDistance} is reached, also invokes the callback. + * + * @param distance The distance to add. + */ + public void add(double distance) { + scrolledDistance += distance; + + // invoke the callback if we scrolled far enough + while (Math.abs(scrolledDistance) >= unitDistance) { + subtractUnitDistance(); + callback.accept((int) Math.signum(scrolledDistance)); + } + } + + /** + * Resets the distance scrolled to zero. + */ + public void reset() { + scrolledDistance = 0.0; + } + + /** + * Subtracts the {@link #unitDistance} from the total {@link #scrolledDistance}. + */ + private void subtractUnitDistance() { + scrolledDistance -= unitDistance * Math.signum(scrolledDistance); + } +} diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/misc/ScrollDistanceHelper.kt b/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/misc/ScrollDistanceHelper.kt deleted file mode 100644 index eab3048614..0000000000 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/misc/ScrollDistanceHelper.kt +++ /dev/null @@ -1,56 +0,0 @@ -package app.morphe.extension.youtube.swipecontrols.misc - -import kotlin.math.abs -import kotlin.math.sign - -/** - * Helper for scaling the onScroll handler. - * - * @param unitDistance Absolute distance after which the callback is invoked. - * @param callback Callback invoked when the unit distance is reached. - */ -class ScrollDistanceHelper( - private val unitDistance: Int, - private val callback: (oldDistance: Double, newDistance: Double, direction: Int) -> Unit, -) { - - /** - * Total distance scrolled. - */ - private var scrolledDistance: Double = 0.0 - - /** - * Adds a scrolled distance to the total. - * If the [unitDistance] is reached, also invokes the callback. - * - * @param distance The distance to add. - */ - fun add(distance: Double) { - scrolledDistance += distance - - // invoke the callback if we scrolled far enough - while (abs(scrolledDistance) >= unitDistance) { - val oldDistance = scrolledDistance - subtractUnitDistance() - callback.invoke( - oldDistance, - scrolledDistance, - sign(scrolledDistance).toInt(), - ) - } - } - - /** - * Resets the distance scrolled to zero. - */ - fun reset() { - scrolledDistance = 0.0 - } - - /** - * Subtracts the [unitDistance] from the total [scrolledDistance]. - */ - private fun subtractUnitDistance() { - scrolledDistance -= (unitDistance * sign(scrolledDistance)) - } -} diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/misc/SwipeControlsOverlay.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/misc/SwipeControlsOverlay.java new file mode 100644 index 0000000000..f79e7bef8f --- /dev/null +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/misc/SwipeControlsOverlay.java @@ -0,0 +1,44 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches + * + * Original hard forked code: + * https://github.com/ReVanced/revanced-patches/commit/724e6d61b2ecd868c1a9a37d465a688e83a74799 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to Morphe contributions. + */ + +package app.morphe.extension.youtube.swipecontrols.misc; + +/** + * Interface for all overlays for swipe controls. + */ +public interface SwipeControlsOverlay { + /** + * Called when the currently set volume level was changed. + * + * @param newVolume The new volume level. + * @param maximumVolume The maximum volume index. + * @param boostStep The volume boost above the maximum volume, in steps. + */ + void onVolumeChanged(int newVolume, int maximumVolume, int boostStep); + + /** + * Called when the currently set screen brightness was changed. + * + * @param brightness The new screen brightness, in percent (range 0.0-100.0). + */ + void onBrightnessChanged(double brightness); + + /** + * Called when the playback speed was changed via swipe gesture. + * + * @param speed The new playback speed multiplier. + */ + void onSpeedChanged(float speed); + + /** + * Called when a new swipe session has started. + */ + void onEnterSwipeSession(); +} diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/misc/SwipeControlsOverlay.kt b/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/misc/SwipeControlsOverlay.kt deleted file mode 100644 index 18596f0739..0000000000 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/misc/SwipeControlsOverlay.kt +++ /dev/null @@ -1,34 +0,0 @@ -package app.morphe.extension.youtube.swipecontrols.misc - -/** - * Interface for all overlays for swipe controls. - */ -interface SwipeControlsOverlay { - /** - * Called when the currently set volume level was changed. - * - * @param newVolume The new volume level. - * @param maximumVolume The maximum volume index. - * @param boostStep The volume boost above the maximum volume, in steps. - */ - fun onVolumeChanged(newVolume: Int, maximumVolume: Int, boostStep: Int = 0) - - /** - * Called when the currently set screen brightness was changed. - * - * @param brightness The new screen brightness, in percent (range 0.0–100.0). - */ - fun onBrightnessChanged(brightness: Double) - - /** - * Called when the playback speed was changed via swipe gesture. - * - * @param speed The new playback speed multiplier. - */ - fun onSpeedChanged(speed: Float) - - /** - * Called when a new swipe session has started. - */ - fun onEnterSwipeSession() -} diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/misc/SwipeControlsUtils.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/misc/SwipeControlsUtils.java new file mode 100644 index 0000000000..f2654d3822 --- /dev/null +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/misc/SwipeControlsUtils.java @@ -0,0 +1,31 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches + * + * Original hard forked code: + * https://github.com/ReVanced/revanced-patches/commit/724e6d61b2ecd868c1a9a37d465a688e83a74799 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to Morphe contributions. + */ + +package app.morphe.extension.youtube.swipecontrols.misc; + +import android.content.Context; +import android.util.TypedValue; + +public final class SwipeControlsUtils { + + /** + * Converts a value in the given unit to pixels. + * + * @param unit The unit of the value, one of the TypedValue.COMPLEX_UNIT_* constants. + * @param value The value to convert. + */ + public static int applyDimension(Context context, int unit, int value) { + return Math.round(TypedValue.applyDimension( + unit, + value, + context.getResources().getDisplayMetrics() + )); + } +} diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/misc/SwipeControlsUtils.kt b/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/misc/SwipeControlsUtils.kt deleted file mode 100644 index 3b056ac705..0000000000 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/misc/SwipeControlsUtils.kt +++ /dev/null @@ -1,25 +0,0 @@ -package app.morphe.extension.youtube.swipecontrols.misc - -import android.content.Context -import android.util.TypedValue -import kotlin.math.roundToInt - -fun Float.clamp(min: Float, max: Float): Float { - if (this < min) return min - if (this > max) return max - return this -} - -fun Int.clamp(min: Int, max: Int): Int { - if (this < min) return min - if (this > max) return max - return this -} - -fun Int.applyDimension(context: Context, unit: Int): Int { - return TypedValue.applyDimension( - unit, - this.toFloat(), - context.resources.displayMetrics, - ).roundToInt() -} diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/views/SwipeControlsOverlayLayout.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/views/SwipeControlsOverlayLayout.java new file mode 100644 index 0000000000..a0cc878e25 --- /dev/null +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/views/SwipeControlsOverlayLayout.java @@ -0,0 +1,703 @@ +package app.morphe.extension.youtube.swipecontrols.views; + +import static app.morphe.extension.shared.StringRef.str; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.drawable.Drawable; +import android.os.Handler; +import android.os.Looper; +import android.view.HapticFeedbackConstants; +import android.view.View; +import android.widget.RelativeLayout; + +import androidx.annotation.Nullable; + +import app.morphe.extension.shared.ResourceType; +import app.morphe.extension.shared.ResourceUtils; +import app.morphe.extension.shared.ui.ViewAnimations; +import app.morphe.extension.youtube.patches.SoundBoostPatch; +import app.morphe.extension.youtube.patches.VideoInformation; +import app.morphe.extension.youtube.swipecontrols.SwipeControlsConfigurationProvider; +import app.morphe.extension.youtube.swipecontrols.SwipeControlsConfigurationProvider.SwipeOverlayStyle; +import app.morphe.extension.youtube.swipecontrols.misc.SwipeControlsOverlay; +import app.morphe.extension.youtube.videoplayer.PlayerIcons; + +/** + * Main overlay layout for displaying volume, brightness, and playback speed level with circular, horizontal and vertical progress bars. + */ +@SuppressLint("ViewConstructor") +public final class SwipeControlsOverlayLayout extends RelativeLayout implements SwipeControlsOverlay { + + private static final int VOLUME_BOOST_PERCENT_PER_STEP = 25; + private static final int VOLUME_BOOST_PROGRESS_COLOR = 0xBFFF7043; + private static final long FADE_DURATION_MILLIS = 100; + + /** + * Convert dp to pixels based on system display density. + */ + private static float toDisplayPixels(float dp) { + return dp * Resources.getSystem().getDisplayMetrics().density; + } + + private final SwipeControlsConfigurationProvider config; + + // Drawable icons for brightness and volume. + private final Drawable autoBrightnessIcon; + private final Drawable lowBrightnessIcon; + private final Drawable mediumBrightnessIcon; + private final Drawable highBrightnessIcon; + private final Drawable fullBrightnessIcon; + private final Drawable mutedVolumeIcon; + private final Drawable lowVolumeIcon; + private final Drawable normalVolumeIcon; + private final Drawable fullVolumeIcon; + private final Drawable speedIcon; + + // Progress bars. + private final CircularProgressView circularProgressView; + private final HorizontalProgressView horizontalProgressView; + private final VerticalProgressView verticalBrightnessProgressView; + private final VerticalProgressView verticalVolumeProgressView; + + // Handler and callback for hiding progress bars. + private final Handler feedbackHideHandler = new Handler(Looper.getMainLooper()); + private final Runnable feedbackHideCallback; + + public SwipeControlsOverlayLayout(Context context, SwipeControlsConfigurationProvider config) { + super(context); + this.config = config; + + autoBrightnessIcon = getDrawable("morphe_ic_sc_brightness_auto"); + lowBrightnessIcon = getDrawable("morphe_ic_sc_brightness_low"); + mediumBrightnessIcon = getDrawable("morphe_ic_sc_brightness_medium"); + highBrightnessIcon = getDrawable("morphe_ic_sc_brightness_high"); + fullBrightnessIcon = getDrawable("morphe_ic_sc_brightness_full"); + mutedVolumeIcon = getDrawable("morphe_ic_sc_volume_mute"); + lowVolumeIcon = getDrawable("morphe_ic_sc_volume_low"); + normalVolumeIcon = getDrawable("morphe_ic_sc_volume_normal"); + fullVolumeIcon = getDrawable("morphe_ic_sc_volume_high"); + speedIcon = getDrawable("morphe_ic_sc_speed"); + + SwipeOverlayStyle overlayStyle = config.getOverlayStyle(); + + // Initialize circular progress bar. + circularProgressView = new CircularProgressView( + context, + config.getOverlayBackgroundOpacity(), + overlayStyle.isMinimal(), + config.getOverlayBrightnessProgressColor(), // Placeholder, updated in showFeedbackView. + SwipeControlsConfigurationProvider.OVERLAY_FILL_BACKGROUND_COLOR, + SwipeControlsConfigurationProvider.OVERLAY_TEXT_COLOR, + config.getOverlayTextSize() + ); + LayoutParams circularParams = new LayoutParams( + (int) toDisplayPixels(100f), (int) toDisplayPixels(100f)); + circularParams.addRule(CENTER_IN_PARENT, TRUE); + circularProgressView.setLayoutParams(circularParams); + circularProgressView.setVisibility(GONE); // Initially hidden. + addView(circularProgressView); + + // Initialize horizontal progress bar. + final int screenWidth = getResources().getDisplayMetrics().widthPixels; + final int layoutWidth = screenWidth * 4 / 5; // Cap at ~360dp. + horizontalProgressView = new HorizontalProgressView( + context, + config.getOverlayBackgroundOpacity(), + overlayStyle.isMinimal(), + config.getOverlayBrightnessProgressColor(), // Placeholder, updated in showFeedbackView. + SwipeControlsConfigurationProvider.OVERLAY_FILL_BACKGROUND_COLOR, + SwipeControlsConfigurationProvider.OVERLAY_TEXT_COLOR, + config.getOverlayTextSize() + ); + LayoutParams horizontalParams = new LayoutParams(layoutWidth, (int) toDisplayPixels(32f)); + horizontalParams.addRule(CENTER_HORIZONTAL); + if (overlayStyle.isHorizontalMinimalCenter()) { + horizontalParams.addRule(CENTER_VERTICAL); + } else { + horizontalParams.topMargin = (int) toDisplayPixels(20f); + } + horizontalProgressView.setLayoutParams(horizontalParams); + horizontalProgressView.setVisibility(GONE); // Initially hidden. + addView(horizontalProgressView); + + // Initialize vertical progress bar for brightness (right side). + verticalBrightnessProgressView = new VerticalProgressView( + context, + config.getOverlayBackgroundOpacity(), + overlayStyle.isMinimal(), + config.getOverlayBrightnessProgressColor(), + SwipeControlsConfigurationProvider.OVERLAY_FILL_BACKGROUND_COLOR, + SwipeControlsConfigurationProvider.OVERLAY_TEXT_COLOR, + config.getOverlayTextSize() + ); + LayoutParams verticalBrightnessParams = new LayoutParams( + (int) toDisplayPixels(40f), (int) toDisplayPixels(150f)); + verticalBrightnessParams.addRule(ALIGN_PARENT_RIGHT); + verticalBrightnessParams.rightMargin = (int) toDisplayPixels(40f); + verticalBrightnessParams.addRule(CENTER_VERTICAL); + verticalBrightnessProgressView.setLayoutParams(verticalBrightnessParams); + verticalBrightnessProgressView.setVisibility(GONE); // Initially hidden. + addView(verticalBrightnessProgressView); + + // Initialize vertical progress bar for volume (left side). + verticalVolumeProgressView = new VerticalProgressView( + context, + config.getOverlayBackgroundOpacity(), + overlayStyle.isMinimal(), + config.getOverlayVolumeProgressColor(), + SwipeControlsConfigurationProvider.OVERLAY_FILL_BACKGROUND_COLOR, + SwipeControlsConfigurationProvider.OVERLAY_TEXT_COLOR, + config.getOverlayTextSize() + ); + LayoutParams verticalVolumeParams = new LayoutParams( + (int) toDisplayPixels(40f), (int) toDisplayPixels(150f)); + verticalVolumeParams.addRule(ALIGN_PARENT_LEFT); + verticalVolumeParams.leftMargin = (int) toDisplayPixels(40f); + verticalVolumeParams.addRule(CENTER_VERTICAL); + verticalVolumeProgressView.setLayoutParams(verticalVolumeParams); + verticalVolumeProgressView.setVisibility(GONE); // Initially hidden. + addView(verticalVolumeProgressView); + + feedbackHideCallback = () -> { + ViewAnimations.fadeOut(circularProgressView, FADE_DURATION_MILLIS); + ViewAnimations.fadeOut(horizontalProgressView, FADE_DURATION_MILLIS); + ViewAnimations.fadeOut(verticalBrightnessProgressView, FADE_DURATION_MILLIS); + ViewAnimations.fadeOut(verticalVolumeProgressView, FADE_DURATION_MILLIS); + }; + } + + // Function to retrieve drawable resources by name. + private Drawable getDrawable(String name) { + Context context = getContext(); + Drawable drawable = getResources().getDrawable( + ResourceUtils.getIdentifierOrThrow(context, ResourceType.DRAWABLE, PlayerIcons.name(name)), + context.getTheme() + ); + drawable.setTint(SwipeControlsConfigurationProvider.OVERLAY_TEXT_COLOR); + return drawable; + } + + /** + * Displays the progress bar with the appropriate value, icon, and type (brightness or volume). + */ + private void showFeedbackView(String value, int progress, int max, Drawable icon, + boolean isBrightness, boolean isBoosted) { + feedbackHideHandler.removeCallbacks(feedbackHideCallback); + feedbackHideHandler.postDelayed(feedbackHideCallback, config.getOverlayShowTimeoutMillis()); + + SwipeOverlayStyle overlayStyle = config.getOverlayStyle(); + final AbstractProgressView viewToShow; + if (overlayStyle.isCircular()) { + viewToShow = circularProgressView; + } else if (overlayStyle.isVertical()) { + viewToShow = isBrightness + ? verticalBrightnessProgressView + : verticalVolumeProgressView; + } else { + viewToShow = horizontalProgressView; + } + + // Set the appropriate progress color. + final int progressColor; + if (isBrightness) { + progressColor = config.getOverlayBrightnessProgressColor(); + } else if (isBoosted) { + progressColor = VOLUME_BOOST_PROGRESS_COLOR; + } else { + progressColor = config.getOverlayVolumeProgressColor(); + } + viewToShow.setProgressColor(progressColor); + viewToShow.setProgress(progress, max, value, isBrightness); + viewToShow.icon = icon; + ViewAnimations.fadeIn(viewToShow, FADE_DURATION_MILLIS); + } + + /** + * Displays the progress bar for playback speed feedback. + * Always uses the horizontal bar (or circular for circular styles) to match the horizontal gesture. + */ + private void showSpeedFeedbackView(float speed) { + feedbackHideHandler.removeCallbacks(feedbackHideCallback); + feedbackHideHandler.postDelayed(feedbackHideCallback, config.getOverlayShowTimeoutMillis()); + + String displayText = VideoInformation.formatSpeedStringX(speed, 2, false); + final int progress = Math.max(0, Math.min(100, (int) ((speed - 0.25f) / (8.0f - 0.25f) * 100))); + + AbstractProgressView viewToShow = config.getOverlayStyle().isCircular() + ? circularProgressView + : horizontalProgressView; + viewToShow.setProgressColor(config.getOverlaySpeedProgressColor()); + viewToShow.setProgress(progress, 100, displayText, false); + viewToShow.icon = speedIcon; + ViewAnimations.fadeIn(viewToShow, FADE_DURATION_MILLIS); + } + + // Handle volume change. + @Override + public void onVolumeChanged(int newVolume, int maximumVolume, int boostStep) { + final boolean isBoosted = boostStep > 0; + final float volumePercentage = ((float) newVolume / maximumVolume) * 100; + final Drawable icon; + if (newVolume == 0) { + icon = mutedVolumeIcon; + } else if (volumePercentage < 25) { + icon = lowVolumeIcon; + } else if (volumePercentage < 50) { + icon = normalVolumeIcon; + } else { + icon = fullVolumeIcon; + } + + // Levels above 100% only exist when volume boost is allowed, so it is the only case + // where the level is shown as a percentage, and it fills the bar. + final String text; + if (isBoosted) { + text = (100 + boostStep * VOLUME_BOOST_PERCENT_PER_STEP) + "%"; + } else if (SoundBoostPatch.isBoostAllowed()) { + text = (int) Math.rint(volumePercentage) + "%"; + } else { + text = String.valueOf(newVolume); + } + showFeedbackView(text, newVolume, maximumVolume, icon, false, isBoosted); + } + + // Handle brightness change. + @Override + public void onBrightnessChanged(double brightness) { + final boolean isVertical = config.getOverlayStyle().isVertical(); + if (config.shouldLowestValueEnableAutoBrightness() && brightness <= 0) { + String displayText = isVertical + ? "А" + : str("morphe_swipe_lowest_value_enable_auto_brightness_overlay_text"); + showFeedbackView(displayText, 0, 100, autoBrightnessIcon, true, false); + } else { + final int brightnessValue = (int) Math.rint(brightness); + final int clampedProgress = Math.max(0, brightnessValue); + final Drawable icon; + if (clampedProgress < 25) { + icon = lowBrightnessIcon; + } else if (clampedProgress < 50) { + icon = mediumBrightnessIcon; + } else if (clampedProgress < 75) { + icon = highBrightnessIcon; + } else { + icon = fullBrightnessIcon; + } + String displayText = isVertical + ? String.valueOf(clampedProgress) + : clampedProgress + "%"; + showFeedbackView(displayText, clampedProgress, 100, icon, true, false); + } + } + + // Handle playback speed change. + @Override + public void onSpeedChanged(float speed) { + showSpeedFeedbackView(speed); + } + + // Begin swipe session. + @SuppressWarnings("deprecation") + @Override + public void onEnterSwipeSession() { + if (config.shouldEnableHapticFeedback()) { + performHapticFeedback( + HapticFeedbackConstants.LONG_PRESS, + HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING + ); + } + } + + /** + * Abstract base class for progress views. + */ + @SuppressLint("ViewConstructor") + private abstract static class AbstractProgressView extends View { + + protected final boolean isMinimalStyle; + + protected final Paint backgroundPaint; + protected final Paint progressPaint; + protected final Paint fillBackgroundPaint; + protected final Paint textPaint; + + // Rect for text measurement. + protected final Rect textBounds = new Rect(); + + protected int progress = 0; + protected int maxProgress = 100; + protected String displayText = "0"; + protected boolean isBrightness = true; + @Nullable + protected Drawable icon; + + AbstractProgressView(Context context, int overlayBackgroundOpacity, boolean isMinimalStyle, + int overlayProgressColor, int overlayFillBackgroundPaint, + int overlayTextColor, int overlayTextSize) { + super(context); + this.isMinimalStyle = isMinimalStyle; + + backgroundPaint = createPaint(overlayBackgroundOpacity, Paint.Style.FILL, Paint.Cap.BUTT, 0f); + progressPaint = createPaint(overlayProgressColor, Paint.Style.STROKE, Paint.Cap.ROUND, toDisplayPixels(6f)); + fillBackgroundPaint = createPaint(overlayFillBackgroundPaint, Paint.Style.FILL, Paint.Cap.BUTT, 0f); + + textPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + textPaint.setColor(overlayTextColor); + textPaint.setTextAlign(Paint.Align.CENTER); + textPaint.setTextSize(toDisplayPixels(overlayTextSize)); + } + + // Combined paint creation function for both fill and stroke styles. + private static Paint createPaint(int color, Paint.Style style, Paint.Cap strokeCap, float strokeWidth) { + Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); + paint.setStyle(style); + paint.setColor(color); + paint.setStrokeCap(strokeCap); + paint.setStrokeWidth(strokeWidth); + return paint; + } + + void setProgress(int value, int max, String text, boolean isBrightnessMode) { + progress = value; + maxProgress = max; + displayText = text; + isBrightness = isBrightnessMode; + invalidate(); + } + + void setProgressColor(int color) { + progressPaint.setColor(color); + invalidate(); + } + + protected int measureTextWidth(String text, Paint paint) { + paint.getTextBounds(text, 0, text.length(), textBounds); + return textBounds.width(); + } + } + + /** + * Custom view for rendering a circular progress indicator with icons and text. + */ + @SuppressLint("ViewConstructor") + private static final class CircularProgressView extends AbstractProgressView { + private final RectF rectF = new RectF(); + + CircularProgressView(Context context, int overlayBackgroundOpacity, boolean isMinimalStyle, + int overlayProgressColor, int overlayFillBackgroundPaint, + int overlayTextColor, int overlayTextSize) { + super(context, overlayBackgroundOpacity, isMinimalStyle, overlayProgressColor, + overlayFillBackgroundPaint, overlayTextColor, overlayTextSize); + + progressPaint.setStrokeWidth(toDisplayPixels(6f)); + fillBackgroundPaint.setStrokeWidth(toDisplayPixels(6f)); + progressPaint.setStrokeCap(Paint.Cap.ROUND); + fillBackgroundPaint.setStrokeCap(Paint.Cap.BUTT); + progressPaint.setStyle(Paint.Style.STROKE); + fillBackgroundPaint.setStyle(Paint.Style.STROKE); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + final int width = getWidth(); + final int height = getHeight(); + final float size = Math.min(width, height); + final float inset = toDisplayPixels(6f); + rectF.set(inset, inset, size - inset, size - inset); + + canvas.drawOval(rectF, fillBackgroundPaint); // Draw the outer ring. + canvas.drawCircle(width / 2f, height / 2f, size / 3, backgroundPaint); // Draw the inner circle. + + // Select the paint for drawing based on whether it's brightness or volume. + final float sweepAngle = ((float) progress / maxProgress) * 360; + canvas.drawArc(rectF, -90f, sweepAngle, false, progressPaint); // Draw the progress arc. + + // Draw the icon in the center. + if (icon != null) { + final int iconSize = (int) toDisplayPixels(isMinimalStyle ? 36f : 24f); + final int iconX = (width - iconSize) / 2; + final int iconY = isMinimalStyle + ? (height - iconSize) / 2 + : (height / 2) - (int) toDisplayPixels(24f); + icon.setBounds(iconX, iconY, iconX + iconSize, iconY + iconSize); + icon.draw(canvas); + } + + // If not a minimal style mode, draw the text inside the ring. + if (!isMinimalStyle) { + canvas.drawText(displayText, width / 2f, height / 2f + toDisplayPixels(20f), textPaint); + } + } + + @Override + void setProgress(int value, int max, String text, boolean isBrightnessMode) { + super.setProgress(value, max, text, isBrightnessMode); + requestLayout(); + } + } + + /** + * Custom view for rendering a rectangular progress bar with icons and text. + */ + @SuppressLint("ViewConstructor") + private static final class HorizontalProgressView extends AbstractProgressView { + private final float iconSize = toDisplayPixels(20f); + private final float padding = toDisplayPixels(12f); + private float textWidth = 0f; + private final float progressBarHeight = toDisplayPixels(3f); + private final float progressBarWidth; + + HorizontalProgressView(Context context, int overlayBackgroundOpacity, boolean isMinimalStyle, + int overlayProgressColor, int overlayFillBackgroundPaint, + int overlayTextColor, int overlayTextSize) { + super(context, overlayBackgroundOpacity, isMinimalStyle, overlayProgressColor, + overlayFillBackgroundPaint, overlayTextColor, overlayTextSize); + + progressBarWidth = getResources().getDisplayMetrics().widthPixels / 4f; + + progressPaint.setStrokeWidth(0f); + progressPaint.setStrokeCap(Paint.Cap.BUTT); + progressPaint.setStyle(Paint.Style.FILL); + fillBackgroundPaint.setStyle(Paint.Style.FILL); + } + + /** + * Calculate required width based on content. + * + * @return Required width to display all elements. + */ + private float calculateRequiredWidth() { + textWidth = measureTextWidth(displayText, textPaint); + + return !isMinimalStyle + ? padding + iconSize + padding + progressBarWidth + padding + textWidth + padding + : padding + iconSize + padding + textWidth + padding; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + + final int suggestedWidth = MeasureSpec.getSize(widthMeasureSpec); + final int suggestedHeight = MeasureSpec.getSize(heightMeasureSpec); + + final int requiredWidth = (int) calculateRequiredWidth(); + final int width = Math.min(Math.max(100, requiredWidth), suggestedWidth); + + setMeasuredDimension(width, suggestedHeight); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + final float viewWidth = getWidth(); + final float viewHeight = getHeight(); + final float viewHeightHalf = viewHeight / 2; + + textWidth = measureTextWidth(displayText, textPaint); + + final float cornerRadius = viewHeightHalf; + + final float startX = padding; + final float iconEndX = startX + iconSize; + + final float textStartX = viewWidth - 1.5f * padding - textWidth; + + canvas.drawRoundRect( + 0f, 0f, viewWidth, viewHeight, + cornerRadius, cornerRadius, backgroundPaint + ); + + if (icon != null) { + final float iconY = viewHeightHalf - iconSize / 2; + icon.setBounds( + (int) startX, + (int) iconY, + (int) (startX + iconSize), + (int) (iconY + iconSize) + ); + icon.draw(canvas); + } + + final float textY = viewHeightHalf + textPaint.getTextSize() / 3; + textPaint.setTextAlign(Paint.Align.LEFT); + + if (isMinimalStyle) { + canvas.drawText(displayText, textStartX, textY, textPaint); + } else { + final float progressStartX = iconEndX + padding; + final float progressEndX = textStartX - padding; + final float progressWidth = progressEndX - progressStartX; + + if (progressWidth > 50) { + final float progressBarHeightHalf = progressBarHeight / 2.0f; + final float viewHeightHalfMinusProgressBarHeightHalf = viewHeightHalf - progressBarHeightHalf; + final float viewHeightHalfPlusProgressBarHeightHalf = viewHeightHalf + progressBarHeightHalf; + + canvas.drawRoundRect( + progressStartX, + viewHeightHalfMinusProgressBarHeightHalf, + progressEndX, + viewHeightHalfPlusProgressBarHeightHalf, + progressBarHeightHalf, + progressBarHeightHalf, + fillBackgroundPaint + ); + + final float progressValue = ((float) progress / maxProgress) * progressWidth; + canvas.drawRoundRect( + progressStartX, + viewHeightHalfMinusProgressBarHeightHalf, + progressStartX + progressValue, + viewHeightHalfPlusProgressBarHeightHalf, + progressBarHeightHalf, + progressBarHeightHalf, + progressPaint + ); + } + + canvas.drawText(displayText, textStartX, textY, textPaint); + } + } + + @Override + void setProgress(int value, int max, String text, boolean isBrightnessMode) { + super.setProgress(value, max, text, isBrightnessMode); + requestLayout(); + } + } + + /** + * Custom view for rendering a vertical progress bar with icons and text. + */ + @SuppressLint("ViewConstructor") + private static final class VerticalProgressView extends AbstractProgressView { + private final float iconSize = toDisplayPixels(20f); + private final float padding = toDisplayPixels(12f); + private final float progressBarWidth = toDisplayPixels(3f); + private final float progressBarHeight; + + VerticalProgressView(Context context, int overlayBackgroundOpacity, boolean isMinimalStyle, + int overlayProgressColor, int overlayFillBackgroundPaint, + int overlayTextColor, int overlayTextSize) { + super(context, overlayBackgroundOpacity, isMinimalStyle, overlayProgressColor, + overlayFillBackgroundPaint, overlayTextColor, overlayTextSize); + + progressBarHeight = getResources().getDisplayMetrics().widthPixels / 3f; + + progressPaint.setStrokeWidth(0f); + progressPaint.setStrokeCap(Paint.Cap.BUTT); + progressPaint.setStyle(Paint.Style.FILL); + fillBackgroundPaint.setStyle(Paint.Style.FILL); + } + + /** + * Calculate required height based on content. + * + * @return Required height to display all elements. + */ + private float calculateRequiredHeight() { + return !isMinimalStyle + ? padding + iconSize + padding + progressBarHeight + padding + textPaint.getTextSize() + padding + : padding + iconSize + padding + textPaint.getTextSize() + padding; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + + final int suggestedWidth = MeasureSpec.getSize(widthMeasureSpec); + final int suggestedHeight = MeasureSpec.getSize(heightMeasureSpec); + + final int requiredHeight = (int) calculateRequiredHeight(); + final int height = Math.min(Math.max(100, requiredHeight), suggestedHeight); + + setMeasuredDimension(suggestedWidth, height); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + final float viewWidth = getWidth(); + final float viewHeight = getHeight(); + final float viewWidthHalf = viewWidth / 2; + final float cornerRadius = viewWidthHalf; + + final float startY = padding; + final float iconEndY = startY + iconSize; + + final float textStartY = viewHeight - padding - textPaint.getTextSize() / 2; + + canvas.drawRoundRect( + 0f, 0f, viewWidth, viewHeight, + cornerRadius, cornerRadius, backgroundPaint + ); + + if (icon != null) { + final float iconX = viewWidthHalf - iconSize / 2; + icon.setBounds( + (int) iconX, + (int) startY, + (int) (iconX + iconSize), + (int) (startY + iconSize) + ); + icon.draw(canvas); + } + + final float textX = viewWidthHalf; + textPaint.setTextAlign(Paint.Align.CENTER); + + if (isMinimalStyle) { + canvas.drawText(displayText, textX, textStartY, textPaint); + } else { + final float progressStartY = iconEndY + padding; + final float progressEndY = textStartY - textPaint.getTextSize() - padding; + final float progressHeight = progressEndY - progressStartY; + + if (progressHeight > 50) { + final float progressBarWidthHalf = progressBarWidth / 2; + final float viewHeightHalfMinusProgressBarHeightHalf = viewWidthHalf - progressBarWidthHalf; + final float viewHeightHalfPlusProgressBarHeightHalf = viewWidthHalf + progressBarWidthHalf; + + canvas.drawRoundRect( + viewHeightHalfMinusProgressBarHeightHalf, + progressStartY, + viewHeightHalfPlusProgressBarHeightHalf, + progressEndY, + progressBarWidthHalf, + progressBarWidthHalf, + fillBackgroundPaint + ); + + final float progressValue = ((float) progress / maxProgress) * progressHeight; + canvas.drawRoundRect( + viewHeightHalfMinusProgressBarHeightHalf, + progressEndY - progressValue, + viewHeightHalfPlusProgressBarHeightHalf, + progressEndY, + progressBarWidthHalf, + progressBarWidthHalf, + progressPaint + ); + } + canvas.drawText(displayText, textX, textStartY, textPaint); + } + } + + @Override + void setProgress(int value, int max, String text, boolean isBrightnessMode) { + super.setProgress(value, max, text, isBrightnessMode); + requestLayout(); + } + } +} diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/views/SwipeControlsOverlayLayout.kt b/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/views/SwipeControlsOverlayLayout.kt deleted file mode 100644 index fd17c889f9..0000000000 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/views/SwipeControlsOverlayLayout.kt +++ /dev/null @@ -1,733 +0,0 @@ -@file:Suppress("UnnecessaryVariable") - -package app.morphe.extension.youtube.swipecontrols.views - -import android.annotation.SuppressLint -import android.content.Context -import android.content.res.Resources -import android.graphics.Canvas -import android.graphics.Paint -import android.graphics.Rect -import android.graphics.RectF -import android.graphics.drawable.Drawable -import android.os.Handler -import android.os.Looper -import android.util.AttributeSet -import android.view.HapticFeedbackConstants -import android.view.View -import android.widget.RelativeLayout -import app.morphe.extension.shared.ResourceType -import app.morphe.extension.shared.ResourceUtils.getIdentifierOrThrow -import app.morphe.extension.shared.StringRef.str -import app.morphe.extension.shared.ui.ViewAnimations -import app.morphe.extension.youtube.patches.SoundBoostPatch -import app.morphe.extension.youtube.patches.VideoInformation -import app.morphe.extension.youtube.swipecontrols.SwipeControlsConfigurationProvider -import app.morphe.extension.youtube.swipecontrols.misc.SwipeControlsOverlay -import app.morphe.extension.youtube.videoplayer.PlayerIcons -import kotlin.math.max -import kotlin.math.min -import kotlin.math.round - -/** - * Convert dp to pixels based on system display density. - */ -fun Float.toDisplayPixels(): Float { - return this * Resources.getSystem().displayMetrics.density -} - -private const val VOLUME_BOOST_PERCENT_PER_STEP = 25 -private const val VOLUME_BOOST_PROGRESS_COLOR = 0xBFFF7043.toInt() - -/** - * Main overlay layout for displaying volume, brightness, and playback speed level with circular, horizontal and vertical progress bars. - */ -class SwipeControlsOverlayLayout( - context: Context, - private val config: SwipeControlsConfigurationProvider, -) : RelativeLayout(context), SwipeControlsOverlay { - - constructor(context: Context) : this(context, SwipeControlsConfigurationProvider()) - - // Drawable icons for brightness and volume. - private val autoBrightnessIcon: Drawable = getDrawable("morphe_ic_sc_brightness_auto") - private val lowBrightnessIcon: Drawable = getDrawable("morphe_ic_sc_brightness_low") - private val mediumBrightnessIcon: Drawable = getDrawable("morphe_ic_sc_brightness_medium") - private val highBrightnessIcon: Drawable = getDrawable("morphe_ic_sc_brightness_high") - private val fullBrightnessIcon: Drawable = getDrawable("morphe_ic_sc_brightness_full") - private val mutedVolumeIcon: Drawable = getDrawable("morphe_ic_sc_volume_mute") - private val lowVolumeIcon: Drawable = getDrawable("morphe_ic_sc_volume_low") - private val normalVolumeIcon: Drawable = getDrawable("morphe_ic_sc_volume_normal") - private val fullVolumeIcon: Drawable = getDrawable("morphe_ic_sc_volume_high") - private val speedIcon: Drawable = getDrawable("morphe_ic_sc_speed") - - // Function to retrieve drawable resources by name. - private fun getDrawable(name: String): Drawable { - val drawable = resources.getDrawable( - getIdentifierOrThrow(context, ResourceType.DRAWABLE, PlayerIcons.name(name)), - context.theme, - ) - drawable.setTint(config.overlayTextColor) - return drawable - } - - // Initialize progress bars. - @Suppress("JoinDeclarationAndAssignment") - private val circularProgressView: CircularProgressView - private val horizontalProgressView: HorizontalProgressView - private val verticalBrightnessProgressView: VerticalProgressView - private val verticalVolumeProgressView: VerticalProgressView - - init { - // Initialize circular progress bar. - circularProgressView = CircularProgressView( - context, - config.overlayBackgroundOpacity, - config.overlayStyle.isMinimal, - config.overlayBrightnessProgressColor, // Placeholder, updated in showFeedbackView. - config.overlayFillBackgroundPaint, - config.overlayTextColor, - config.overlayTextSize - ).apply { - layoutParams = LayoutParams(100f.toDisplayPixels().toInt(), 100f.toDisplayPixels().toInt()).apply { - addRule(CENTER_IN_PARENT, TRUE) - } - visibility = GONE // Initially hidden. - } - addView(circularProgressView) - - // Initialize horizontal progress bar. - val screenWidth = resources.displayMetrics.widthPixels - val layoutWidth = (screenWidth * 4 / 5) // Cap at ~360dp. - horizontalProgressView = HorizontalProgressView( - context, - config.overlayBackgroundOpacity, - config.overlayStyle.isMinimal, - config.overlayBrightnessProgressColor, // Placeholder, updated in showFeedbackView. - config.overlayFillBackgroundPaint, - config.overlayTextColor, - config.overlayTextSize - ).apply { - layoutParams = LayoutParams(layoutWidth, 32f.toDisplayPixels().toInt()).apply { - addRule(CENTER_HORIZONTAL) - if (config.overlayStyle.isHorizontalMinimalCenter) { - addRule(CENTER_VERTICAL) - } else { - topMargin = 20f.toDisplayPixels().toInt() - } - } - visibility = GONE // Initially hidden. - } - addView(horizontalProgressView) - - // Initialize vertical progress bar for brightness (right side). - verticalBrightnessProgressView = VerticalProgressView( - context, - config.overlayBackgroundOpacity, - config.overlayStyle.isMinimal, - config.overlayBrightnessProgressColor, - config.overlayFillBackgroundPaint, - config.overlayTextColor, - config.overlayTextSize - ).apply { - layoutParams = LayoutParams(40f.toDisplayPixels().toInt(), 150f.toDisplayPixels().toInt()).apply { - addRule(ALIGN_PARENT_RIGHT) - rightMargin = 40f.toDisplayPixels().toInt() - addRule(CENTER_VERTICAL) - } - visibility = GONE // Initially hidden. - } - addView(verticalBrightnessProgressView) - - // Initialize vertical progress bar for volume (left side). - verticalVolumeProgressView = VerticalProgressView( - context, - config.overlayBackgroundOpacity, - config.overlayStyle.isMinimal, - config.overlayVolumeProgressColor, - config.overlayFillBackgroundPaint, - config.overlayTextColor, - config.overlayTextSize - ).apply { - layoutParams = LayoutParams(40f.toDisplayPixels().toInt(), 150f.toDisplayPixels().toInt()).apply { - addRule(ALIGN_PARENT_LEFT) - leftMargin = 40f.toDisplayPixels().toInt() - addRule(CENTER_VERTICAL) - } - visibility = GONE // Initially hidden. - } - addView(verticalVolumeProgressView) - } - - private fun View.fadeIn() = ViewAnimations.fadeIn(this, 100) - private fun View.fadeOut() = ViewAnimations.fadeOut(this, 100) - - // Handler and callback for hiding progress bars. - private val feedbackHideHandler = Handler(Looper.getMainLooper()) - private val feedbackHideCallback = Runnable { - circularProgressView.fadeOut() - horizontalProgressView.fadeOut() - verticalBrightnessProgressView.fadeOut() - verticalVolumeProgressView.fadeOut() - } - - /** - * Displays the progress bar with the appropriate value, icon, and type (brightness or volume). - */ - private fun showFeedbackView( - value: String, - progress: Int, - max: Int, - icon: Drawable, - isBrightness: Boolean, - isBoosted: Boolean = false - ) { - feedbackHideHandler.removeCallbacks(feedbackHideCallback) - feedbackHideHandler.postDelayed(feedbackHideCallback, config.overlayShowTimeoutMillis) - - val viewToShow = when { - config.overlayStyle.isCircular -> circularProgressView - config.overlayStyle.isVertical -> - if (isBrightness) - verticalBrightnessProgressView - else - verticalVolumeProgressView - else -> horizontalProgressView - } - viewToShow.apply { - // Set the appropriate progress color. - setProgressColor( - when { - isBrightness -> config.overlayBrightnessProgressColor - isBoosted -> VOLUME_BOOST_PROGRESS_COLOR - else -> config.overlayVolumeProgressColor - } - ) - setProgress(progress, max, value, isBrightness) - this.icon = icon - fadeIn() - } - } - - /** - * Displays the progress bar for playback speed feedback. - * Always uses the horizontal bar (or circular for circular styles) to match the horizontal gesture. - */ - private fun showSpeedFeedbackView(speed: Float) { - feedbackHideHandler.removeCallbacks(feedbackHideCallback) - feedbackHideHandler.postDelayed(feedbackHideCallback, config.overlayShowTimeoutMillis) - - val displayText = VideoInformation.formatSpeedStringX(speed, 2, false) - val progress = maxOf(0, minOf(100, ((speed - 0.25f) / (8.0f - 0.25f) * 100).toInt())) - - val viewToShow = if (config.overlayStyle.isCircular) circularProgressView else horizontalProgressView - viewToShow.apply { - setProgressColor(config.overlaySpeedProgressColor) - setProgress(progress, 100, displayText, false) - this.icon = speedIcon - fadeIn() - } - } - - // Handle volume change. - override fun onVolumeChanged(newVolume: Int, maximumVolume: Int, boostStep: Int) { - val isBoosted = boostStep > 0 - val volumePercentage = (newVolume.toFloat() / maximumVolume) * 100 - val icon = when { - newVolume == 0 -> mutedVolumeIcon - volumePercentage < 25 -> lowVolumeIcon - volumePercentage < 50 -> normalVolumeIcon - else -> fullVolumeIcon - } - - // Levels above 100% only exist when volume boost is allowed, so it is the only case - // where the level is shown as a percentage, and it fills the bar. - val text = when { - isBoosted -> "${100 + boostStep * VOLUME_BOOST_PERCENT_PER_STEP}%" - SoundBoostPatch.isBoostAllowed() -> "${round(volumePercentage).toInt()}%" - else -> "$newVolume" - } - showFeedbackView( - text, - newVolume, - maximumVolume, - icon, - isBrightness = false, - isBoosted = isBoosted - ) - } - - // Handle brightness change. - override fun onBrightnessChanged(brightness: Double) { - if (config.shouldLowestValueEnableAutoBrightness && brightness <= 0) { - val displayText = if (config.overlayStyle.isVertical) "А" - else str("morphe_swipe_lowest_value_enable_auto_brightness_overlay_text") - showFeedbackView(displayText, 0, 100, autoBrightnessIcon, isBrightness = true) - } else { - val brightnessValue = round(brightness).toInt() - val clampedProgress = max(0, brightnessValue) - val icon = when { - clampedProgress < 25 -> lowBrightnessIcon - clampedProgress < 50 -> mediumBrightnessIcon - clampedProgress < 75 -> highBrightnessIcon - else -> fullBrightnessIcon - } - val displayText = if (config.overlayStyle.isVertical) "$clampedProgress" else "$clampedProgress%" - showFeedbackView(displayText, clampedProgress, 100, icon, isBrightness = true) - } - } - - // Handle playback speed change. - override fun onSpeedChanged(speed: Float) { - showSpeedFeedbackView(speed) - } - - // Begin swipe session. - override fun onEnterSwipeSession() { - if (config.shouldEnableHapticFeedback) { - @Suppress("DEPRECATION") - performHapticFeedback( - HapticFeedbackConstants.LONG_PRESS, - HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING, - ) - } - } -} - -/** - * Abstract base class for progress views. - */ -abstract class AbstractProgressView( - context: Context, - overlayBackgroundOpacity: Int, - protected val isMinimalStyle: Boolean, - overlayProgressColor: Int, - overlayFillBackgroundPaint: Int, - private val overlayTextColor: Int, - protected val overlayTextSize: Int, - attrs: AttributeSet? = null, - defStyleAttr: Int = 0 -) : View(context, attrs, defStyleAttr) { - - // Combined paint creation function for both fill and stroke styles. - private fun createPaint( - color: Int, - style: Paint.Style = Paint.Style.FILL, - strokeCap: Paint.Cap = Paint.Cap.BUTT, - strokeWidth: Float = 0f - ) = Paint(Paint.ANTI_ALIAS_FLAG).apply { - this.style = style - this.color = color - this.strokeCap = strokeCap - this.strokeWidth = strokeWidth - } - - // Initialize paints. - val backgroundPaint = createPaint( - overlayBackgroundOpacity, - style = Paint.Style.FILL - ) - val progressPaint = createPaint( - overlayProgressColor, - style = Paint.Style.STROKE, - strokeCap = Paint.Cap.ROUND, - strokeWidth = 6f.toDisplayPixels() - ) - val fillBackgroundPaint = createPaint( - overlayFillBackgroundPaint, - style = Paint.Style.FILL - ) - val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { - color = overlayTextColor - textAlign = Paint.Align.CENTER - textSize = overlayTextSize.toFloat().toDisplayPixels() - } - - // Rect for text measurement. - protected val textBounds = Rect() - - protected var progress = 0 - protected var maxProgress = 100 - protected var displayText: String = "0" - protected var isBrightness = true - var icon: Drawable? = null - - open fun setProgress(value: Int, max: Int, text: String, isBrightnessMode: Boolean) { - progress = value - maxProgress = max - displayText = text - isBrightness = isBrightnessMode - invalidate() - } - - fun setProgressColor(color: Int) { - progressPaint.color = color - invalidate() - } - - protected fun measureTextWidth(text: String, paint: Paint): Int { - paint.getTextBounds(text, 0, text.length, textBounds) - return textBounds.width() - } - - override fun onDraw(canvas: Canvas) { - // Base class implementation can be empty. - } -} - -/** - * Custom view for rendering a circular progress indicator with icons and text. - */ -@SuppressLint("ViewConstructor") -class CircularProgressView( - context: Context, - overlayBackgroundOpacity: Int, - isMinimalStyle: Boolean, - overlayProgressColor: Int, - overlayFillBackgroundPaint: Int, - overlayTextColor: Int, - overlayTextSize: Int, - attrs: AttributeSet? = null, - defStyleAttr: Int = 0 -) : AbstractProgressView( - context, - overlayBackgroundOpacity, - isMinimalStyle, - overlayProgressColor, - overlayFillBackgroundPaint, - overlayTextColor, - overlayTextSize, - attrs, - defStyleAttr -) { - private val rectF = RectF() - - init { - progressPaint.strokeWidth = 6f.toDisplayPixels() - fillBackgroundPaint.strokeWidth = 6f.toDisplayPixels() - progressPaint.strokeCap = Paint.Cap.ROUND - fillBackgroundPaint.strokeCap = Paint.Cap.BUTT - progressPaint.style = Paint.Style.STROKE - fillBackgroundPaint.style = Paint.Style.STROKE - } - - override fun onDraw(canvas: Canvas) { - super.onDraw(canvas) - - val size = min(width, height).toFloat() - val inset = 6f.toDisplayPixels() - rectF.set(inset, inset, size - inset, size - inset) - - canvas.drawOval(rectF, fillBackgroundPaint) // Draw the outer ring. - canvas.drawCircle(width / 2f, height / 2f, size / 3, backgroundPaint) // Draw the inner circle. - - // Select the paint for drawing based on whether it's brightness or volume. - val sweepAngle = (progress.toFloat() / maxProgress) * 360 - canvas.drawArc(rectF, -90f, sweepAngle, false, progressPaint) // Draw the progress arc. - - // Draw the icon in the center. - icon?.let { - val iconSize = (if (isMinimalStyle) 36f else 24f).toDisplayPixels().toInt() - val iconX = (width - iconSize) / 2 - val iconY = if (isMinimalStyle) { - (height - iconSize) / 2 - } else { - (height / 2) - 24f.toDisplayPixels().toInt() - } - it.setBounds(iconX, iconY, iconX + iconSize, iconY + iconSize) - it.draw(canvas) - } - - // If not a minimal style mode, draw the text inside the ring. - if (!isMinimalStyle) { - canvas.drawText(displayText, width / 2f, height / 2f + 20f.toDisplayPixels(), textPaint) - } - } - - override fun setProgress(value: Int, max: Int, text: String, isBrightnessMode: Boolean) { - super.setProgress(value, max, text, isBrightnessMode) - requestLayout() - } -} - -/** - * Custom view for rendering a rectangular progress bar with icons and text. - */ -@SuppressLint("ViewConstructor") -class HorizontalProgressView( - context: Context, - overlayBackgroundOpacity: Int, - isMinimalStyle: Boolean, - overlayProgressColor: Int, - overlayFillBackgroundPaint: Int, - overlayTextColor: Int, - overlayTextSize: Int, - attrs: AttributeSet? = null, - defStyleAttr: Int = 0 -) : AbstractProgressView( - context, - overlayBackgroundOpacity, - isMinimalStyle, - overlayProgressColor, - overlayFillBackgroundPaint, - overlayTextColor, - overlayTextSize, - attrs, - defStyleAttr -) { - - private val iconSize = 20f.toDisplayPixels() - private val padding = 12f.toDisplayPixels() - private var textWidth = 0f - private val progressBarHeight = 3f.toDisplayPixels() - private val progressBarWidth: Float = resources.displayMetrics.widthPixels / 4f - - init { - progressPaint.strokeWidth = 0f - progressPaint.strokeCap = Paint.Cap.BUTT - progressPaint.style = Paint.Style.FILL - fillBackgroundPaint.style = Paint.Style.FILL - } - - /** - * Calculate required width based on content. - * @return Required width to display all elements. - */ - private fun calculateRequiredWidth(): Float { - textWidth = measureTextWidth(displayText, textPaint).toFloat() - - return if (!isMinimalStyle) { - padding + iconSize + padding + progressBarWidth + padding + textWidth + padding - } else { - padding + iconSize + padding + textWidth + padding - } - } - - override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { - super.onMeasure(widthMeasureSpec, heightMeasureSpec) - - val suggestedWidth = MeasureSpec.getSize(widthMeasureSpec) - val suggestedHeight = MeasureSpec.getSize(heightMeasureSpec) - - val height = suggestedHeight - val requiredWidth = calculateRequiredWidth().toInt() - val width = min(max(100, requiredWidth), suggestedWidth) - - setMeasuredDimension(width, height) - } - - override fun onDraw(canvas: Canvas) { - super.onDraw(canvas) - - val viewWidth = width.toFloat() - val viewHeight = height.toFloat() - val viewHeightHalf = viewHeight / 2 - - textWidth = measureTextWidth(displayText, textPaint).toFloat() - - val cornerRadius = viewHeightHalf - - val startX = padding - val iconEndX = startX + iconSize - - val textStartX = (viewWidth - 1.5f * padding - textWidth) - - canvas.drawRoundRect( - 0f, 0f, viewWidth, viewHeight, - cornerRadius, cornerRadius, backgroundPaint - ) - - icon?.let { - val iconY = viewHeightHalf - iconSize / 2 - it.setBounds( - startX.toInt(), - iconY.toInt(), - (startX + iconSize).toInt(), - (iconY + iconSize).toInt() - ) - it.draw(canvas) - } - - val textY = viewHeightHalf + textPaint.textSize / 3 - textPaint.textAlign = Paint.Align.LEFT - - if (isMinimalStyle) { - canvas.drawText(displayText, textStartX, textY, textPaint) - } else { - val progressStartX = iconEndX + padding - val progressEndX = textStartX - padding - val progressWidth = progressEndX - progressStartX - - if (progressWidth > 50) { - val progressBarHeightHalf = progressBarHeight / 2.0f - val viewHeightHalfMinusProgressBarHeightHalf = viewHeightHalf - progressBarHeightHalf - val viewHeightHalfPlusProgressBarHeightHalf = viewHeightHalf + progressBarHeightHalf - - canvas.drawRoundRect( - progressStartX, - viewHeightHalfMinusProgressBarHeightHalf, - progressEndX, - viewHeightHalfPlusProgressBarHeightHalf, - progressBarHeightHalf, - progressBarHeightHalf, - fillBackgroundPaint - ) - - val progressValue = (progress.toFloat() / maxProgress) * progressWidth - canvas.drawRoundRect( - progressStartX, - viewHeightHalfMinusProgressBarHeightHalf, - progressStartX + progressValue, - viewHeightHalfPlusProgressBarHeightHalf, - progressBarHeightHalf, - progressBarHeightHalf, - progressPaint - ) - } - - canvas.drawText(displayText, textStartX, textY, textPaint) - } - } - - override fun setProgress(value: Int, max: Int, text: String, isBrightnessMode: Boolean) { - super.setProgress(value, max, text, isBrightnessMode) - requestLayout() - } -} - -/** - * Custom view for rendering a vertical progress bar with icons and text. - */ -@SuppressLint("ViewConstructor") -class VerticalProgressView( - context: Context, - overlayBackgroundOpacity: Int, - isMinimalStyle: Boolean, - overlayProgressColor: Int, - overlayFillBackgroundPaint: Int, - overlayTextColor: Int, - overlayTextSize: Int, - attrs: AttributeSet? = null, - defStyleAttr: Int = 0 -) : AbstractProgressView( - context, - overlayBackgroundOpacity, - isMinimalStyle, - overlayProgressColor, - overlayFillBackgroundPaint, - overlayTextColor, - overlayTextSize, - attrs, - defStyleAttr -) { - - private val iconSize = 20f.toDisplayPixels() - private val padding = 12f.toDisplayPixels() - private val progressBarWidth = 3f.toDisplayPixels() - private val progressBarHeight: Float = resources.displayMetrics.widthPixels / 3f - - init { - progressPaint.strokeWidth = 0f - progressPaint.strokeCap = Paint.Cap.BUTT - progressPaint.style = Paint.Style.FILL - fillBackgroundPaint.style = Paint.Style.FILL - } - - /** - * Calculate required height based on content. - * @return Required height to display all elements. - */ - private fun calculateRequiredHeight(): Float { - return if (!isMinimalStyle) { - padding + iconSize + padding + progressBarHeight + padding + textPaint.textSize + padding - } else { - padding + iconSize + padding + textPaint.textSize + padding - } - } - - override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { - super.onMeasure(widthMeasureSpec, heightMeasureSpec) - - val suggestedWidth = MeasureSpec.getSize(widthMeasureSpec) - val suggestedHeight = MeasureSpec.getSize(heightMeasureSpec) - - val requiredHeight = calculateRequiredHeight().toInt() - val height = min(max(100, requiredHeight), suggestedHeight) - - setMeasuredDimension(suggestedWidth, height) - } - - override fun onDraw(canvas: Canvas) { - super.onDraw(canvas) - - val viewWidth = width.toFloat() - val viewHeight = height.toFloat() - val viewWidthHalf = viewWidth / 2 - val cornerRadius = viewWidthHalf - - val startY = padding - val iconEndY = startY + iconSize - - val textStartY = viewHeight - padding - textPaint.textSize / 2 - - canvas.drawRoundRect( - 0f, 0f, viewWidth, viewHeight, - cornerRadius, cornerRadius, backgroundPaint - ) - - icon?.let { - val iconX = viewWidthHalf - iconSize / 2 - it.setBounds( - iconX.toInt(), - startY.toInt(), - (iconX + iconSize).toInt(), - (startY + iconSize).toInt() - ) - it.draw(canvas) - } - - val textX = viewWidthHalf - textPaint.textAlign = Paint.Align.CENTER - - if (isMinimalStyle) { - canvas.drawText(displayText, textX, textStartY, textPaint) - } else { - val progressStartY = (iconEndY + padding) - val progressEndY = textStartY - textPaint.textSize - padding - val progressHeight = progressEndY - progressStartY - - if (progressHeight > 50) { - val progressBarWidthHalf = progressBarWidth / 2 - val viewHeightHalfMinusProgressBarHeightHalf = viewWidthHalf - progressBarWidthHalf - val viewHeightHalfPlusProgressBarHeightHalf = viewWidthHalf + progressBarWidthHalf - - canvas.drawRoundRect( - viewHeightHalfMinusProgressBarHeightHalf, - progressStartY, - viewHeightHalfPlusProgressBarHeightHalf, - progressEndY, - progressBarWidthHalf, - progressBarWidthHalf, - fillBackgroundPaint - ) - - val progressValue = (progress.toFloat() / maxProgress) * progressHeight - canvas.drawRoundRect( - viewHeightHalfMinusProgressBarHeightHalf, - progressEndY - progressValue, - viewHeightHalfPlusProgressBarHeightHalf, - progressEndY, - progressBarWidthHalf, - progressBarWidthHalf, - progressPaint - ) - } - canvas.drawText(displayText, textX, textStartY, textPaint) - } - } - - override fun setProgress(value: Int, max: Int, text: String, isBrightnessMode: Boolean) { - super.setProgress(value, max, text, isBrightnessMode) - requestLayout() - } -} diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/videoplayer/VideoQualityDialogButton.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/videoplayer/VideoQualityDialogButton.java index a5c5d23b79..123ac628dd 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/videoplayer/VideoQualityDialogButton.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/videoplayer/VideoQualityDialogButton.java @@ -48,7 +48,6 @@ import app.morphe.extension.youtube.patches.playback.quality.RememberVideoQualityPatch; import app.morphe.extension.youtube.settings.Settings; import app.morphe.extension.youtube.shared.PipDismissHelper; -import kotlin.Unit; @SuppressWarnings("unused") public class VideoQualityDialogButton { @@ -64,7 +63,6 @@ public class VideoQualityDialogButton { static { VideoInformation.onQualityChange.addObserver((@Nullable VideoQualityInterface quality) -> { updateButtonText(quality); - return Unit.INSTANCE; }); } diff --git a/gradle.properties b/gradle.properties index 262f2e3d81..b87d6a245d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -2,5 +2,7 @@ org.gradle.caching = true org.gradle.jvmargs = -Xms512M -Xmx2048M org.gradle.parallel = true android.useAndroidX = true +# Extensions are Java only. Prevents AGP from adding Kotlin compilation and the Kotlin stdlib. +android.builtInKotlin = false kotlin.code.style = official version = 1.49.0-dev.6 From 82efbab5e735cfa3db7a6cc5b05a5ad00b1b0b96 Mon Sep 17 00:00:00 2001 From: 0xrxL <127248639+0xrxL@users.noreply.github.com> Date: Sun, 25 Oct 2026 23:38:17 +0100 Subject: [PATCH 069/189] fix(YouTube - Video quality): Shorts quality selected in the menu is not kept and is used for the next Shorts --- .../quality/RememberVideoQualityPatch.java | 41 ++++++++++++++++--- .../quality/RememberVideoQualityPatch.kt | 25 +++++++---- 2 files changed, 51 insertions(+), 15 deletions(-) diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/playback/quality/RememberVideoQualityPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/playback/quality/RememberVideoQualityPatch.java index fd22565b29..e5e3b3a00e 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/playback/quality/RememberVideoQualityPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/playback/quality/RememberVideoQualityPatch.java @@ -49,14 +49,20 @@ public class RememberVideoQualityPatch { private static volatile String userSelectedQualityVideoId = ""; private static volatile int userSelectedQualityResolution = VideoInformation.AUTOMATIC_VIDEO_QUALITY_VALUE; + /** + * If the quality selected by the user was selected in the Shorts quality menu. + */ + private static volatile boolean userSelectedQualityIsShorts; + /** * The quality selected in the Shorts quality menu, until the menu sets it. */ @Nullable private static volatile VideoQualityInterface shortsMenuSelectedQuality; - private static void setUserSelectedQuality(int resolution) { + private static void setUserSelectedQuality(int resolution, boolean isShorts) { userSelectedQualityResolution = resolution; + userSelectedQualityIsShorts = isShorts; userSelectedQualityVideoId = VideoInformation.getVideoId(); } @@ -147,8 +153,12 @@ public static Optional getInitialVideoQuality(Optional optional) { // The video ID of the player is updated only once the video starts, so the selected // quality is not used once the player response of another video is received, // such as the next video of the autoplay. - if (isQualitySelectedByUser() && VideoInformation.getPlayerResponseVideoId() - .equals(VideoInformation.getVideoId())) { + // The Shorts player starts the next Short before its player response is received, + // so a quality selected in the Shorts quality menu is not used, otherwise the next Short + // would start with it. The Short it was selected for keeps it with the formats saved + // with the quality last selected (see useSelectedQualityFormats). + if (isQualitySelectedByUser() && !userSelectedQualityIsShorts + && VideoInformation.getPlayerResponseVideoId().equals(VideoInformation.getVideoId())) { final int selectedResolution = userSelectedQualityResolution; Logger.printDebug(() -> "initialVideoQuality selected by the user: " + selectedResolution); // With 'Auto' selected, YouTube chooses the quality. @@ -288,13 +298,13 @@ public static void userChangedShortsQuality(int userSelectedQualityIndex) { if (currentQualities == null) { Logger.printDebug(() -> "Cannot save default quality, qualities is null"); // The resolution is not known, so a reload uses the quality saved by YouTube. - setUserSelectedQuality(VideoInformation.AUTOMATIC_VIDEO_QUALITY_VALUE); + setUserSelectedQuality(VideoInformation.AUTOMATIC_VIDEO_QUALITY_VALUE, true); clearShortsMenuSelectedQuality(); return; } VideoQualityInterface quality = currentQualities[userSelectedQualityIndex]; final int resolution = quality.patch_getResolution(); - setUserSelectedQuality(resolution); + setUserSelectedQuality(resolution, true); if (resolution == VideoInformation.AUTOMATIC_VIDEO_QUALITY_VALUE) { // 'Auto' has no formats, so the menu sets it by its resolution. clearShortsMenuSelectedQuality(); @@ -362,7 +372,7 @@ public static void userChangedQuality(VideoQualityInterface quality) { public static void userChangedQuality(int videoResolution) { Utils.verifyOnMainThread(); Logger.printDebug(() -> "User changed quality to: " + videoResolution); - setUserSelectedQuality(videoResolution); + setUserSelectedQuality(videoResolution, false); if (shouldRememberVideoQuality()) { saveDefaultQuality(videoResolution); @@ -401,4 +411,23 @@ public static boolean useInitialVideoQuality(boolean original) { public static boolean useAdvancedVideoQualitySetting() { return isDefaultQualitySet(); } + + /** + * Injection point. + *

+ * If this flag is on, the formats of the quality selected in the quality menu are saved with + * the quality last selected, and the player keeps playing them. If it is off, only the resolution + * YouTube gives the quality is saved, and the player can switch to another quality of that resolution, + * such as 720p60 after selecting a 480p quality that YouTube gives the resolution 720 + * (see {@link VideoInformation#fixVideoQualityResolution(String, int)}). + *

+ * With this flag on, Shorts played with the default quality can restart from the beginning + * when the formats change, and the formats saved for a Short are also used for the next Shorts. + * So it is on only while the Short the user selected the quality for is playing. + * + * @return If the formats of the selected quality are saved and used by the player. + */ + public static boolean useSelectedQualityFormats(boolean original) { + return ShortsPlayerState.isOpen() && isQualitySelectedByUser(); + } } diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/video/quality/RememberVideoQualityPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/video/quality/RememberVideoQualityPatch.kt index c26d55170e..24fc9388c1 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/video/quality/RememberVideoQualityPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/video/quality/RememberVideoQualityPatch.kt @@ -338,15 +338,22 @@ val rememberVideoQualityPatch = bytecodePatch { // If this flag is enabled, Shorts restart whenever the quality changes. // The restart uses the initial quality of the Short, so the quality selected by the user is lost. - listOf( - ShortsQualityChangeObserverPrimaryFeatureFlagFingerprint, - ShortsQualityChangeObserverSecondaryFeatureFlagFingerprint - ).forEach { fingerprint -> - fingerprint.matchAll().forEach { - it.method.insertLiteralOverride( - it.instructionMatches.first().index, false - ) - } + ShortsQualityChangeObserverPrimaryFeatureFlagFingerprint.matchAll().forEach { + it.method.insertLiteralOverride( + it.instructionMatches.first().index, false + ) + } + + // The secondary flag also saves the formats of the quality selected in the quality menu with the quality + // last selected, so the player keeps playing them. Without the formats only the resolution YouTube + // gives the quality is saved, and the player can switch to another quality of that resolution, + // such as 720p60 after selecting a 480p quality that YouTube gives the resolution 720. + // The flag is enabled only for the Short the user selected the quality for. + ShortsQualityChangeObserverSecondaryFeatureFlagFingerprint.matchAll().forEach { + it.method.insertLiteralOverride( + it.instructionMatches.first().index, + "$EXTENSION_CLASS->useSelectedQualityFormats(Z)Z" + ) } } } From 7e665c62dc3f88a6f9888b8bc48d1aea885761a1 Mon Sep 17 00:00:00 2001 From: 0xrxL <127248639+0xrxL@users.noreply.github.com> Date: Sun, 25 Oct 2026 23:50:16 +0100 Subject: [PATCH 070/189] fix(YouTube - Minimal miniplayer): Video title and channel are missing after going back from the Shorts player --- .../patches/MinimalMiniplayerPatch.java | 4 ++-- .../youtube/patches/VideoInformation.java | 20 +++++++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/MinimalMiniplayerPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/MinimalMiniplayerPatch.java index 3e5f079869..bc03f92976 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/MinimalMiniplayerPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/MinimalMiniplayerPatch.java @@ -1223,12 +1223,12 @@ private static void refreshContents() { } private static void updateText() { - String title = VideoInformation.getVideoTitle(); + String title = VideoInformation.getMiniplayerVideoName(); if (!title.isEmpty()) { videoTitle = title; } - String channel = VideoInformation.getChannelName(); + String channel = VideoInformation.getMiniplayerChannelName(); if (!channel.isEmpty()) { channelName = channel; } diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/VideoInformation.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/VideoInformation.java index 7fc0bc785e..1b97a09cd7 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/VideoInformation.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/VideoInformation.java @@ -424,6 +424,26 @@ public static String getVideoTitle() { return RestoreOriginalTitlesPatch.getTitle(playerVideo.videoId, playerVideo.videoTitle); } + /** + * The miniplayer only shows the video of the regular player, so the player in front + * is not used, as the Shorts player can be in front while the miniplayer is shown, + * such as when going back from the Shorts player. + * + * @return The title of the video of the regular player, + * or the title restored by {@link RestoreOriginalTitlesPatch} if enabled. + */ + public static String getMiniplayerVideoName() { + return RestoreOriginalTitlesPatch.getTitle(regularPlayerVideo.videoId, regularPlayerVideo.videoTitle); + } + + /** + * @return The channel name of the video of the regular player. + * @see #getMiniplayerVideoName() + */ + public static String getMiniplayerChannelName() { + return regularPlayerVideo.channelName; + } + /** * Injection point. * From 5545e3c87458e60c5e6ef5653053c02d343dd3f8 Mon Sep 17 00:00:00 2001 From: 0xrxL <127248639+0xrxL@users.noreply.github.com> Date: Mon, 26 Oct 2026 00:18:54 +0100 Subject: [PATCH 071/189] fix(YouTube - Add to queue): Feed below the player stutters while scrolling --- .../queue/NativePlaylistQueuePatch.java | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/queue/NativePlaylistQueuePatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/queue/NativePlaylistQueuePatch.java index cc890bce88..bade616a13 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/queue/NativePlaylistQueuePatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/queue/NativePlaylistQueuePatch.java @@ -330,18 +330,23 @@ public static void onWatchNextResponse(MessageLite response) { /** Read-only shared element hook; preserves bytes and every other patch's changes. */ public static byte[] onElementLoaded(byte[] bytes) { - if (bytes == null || !CARD_COMPONENTS.matches(bytes)) { + // Cards are parsed on the main thread while the feed scrolls, so skip them + // when the native queue is off. Missing metadata is fetched when queued. + if (bytes == null || isNativeQueueDisabled() || !CARD_COMPONENTS.matches(bytes)) { return bytes; } - try { - VideoMetadata metadata = VideoMetadata.fromElement(bytes); - if (metadata != null) { - metadataCache.compute(metadata.videoId(), (videoId, previous) -> - previous == null ? metadata : previous.withCapture(metadata)); + byte[] element = bytes.clone(); + Utils.runOnBackgroundThread(() -> { + try { + VideoMetadata metadata = VideoMetadata.fromElement(element); + if (metadata != null) { + metadataCache.compute(metadata.videoId(), (videoId, previous) -> + previous == null ? metadata : previous.withCapture(metadata)); + } + } catch (Exception ex) { + Logger.printException(() -> "onElementLoaded failure", ex); } - } catch (Exception ex) { - Logger.printException(() -> "onElementLoaded failure", ex); - } + }); return bytes; } From 66d9726f085ff4281898459fa4e6f86919b4bf1a Mon Sep 17 00:00:00 2001 From: 0xrxL <127248639+0xrxL@users.noreply.github.com> Date: Mon, 26 Oct 2026 00:18:54 +0100 Subject: [PATCH 072/189] refactor(YouTube - Add to queue): Clean up native queue extension code --- .../queue/NativePlaylistQueuePatch.java | 388 +++++++++++------- 1 file changed, 246 insertions(+), 142 deletions(-) diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/queue/NativePlaylistQueuePatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/queue/NativePlaylistQueuePatch.java index bade616a13..f13eb8c5bf 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/queue/NativePlaylistQueuePatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/queue/NativePlaylistQueuePatch.java @@ -44,7 +44,7 @@ import app.morphe.extension.youtube.innertube.IconOuterClass.YTIconType; import app.morphe.extension.youtube.innertube.NextResponseOuterClass.SingleColumnWatchNextResults; import app.morphe.extension.youtube.innertube.queue.PlaylistQueueRenderer; -import app.morphe.extension.youtube.patches.AddToQueuePatch.QueueType; +import app.morphe.extension.youtube.patches.AddToQueuePatch; import app.morphe.extension.youtube.patches.LoadVideoPatch; import app.morphe.extension.youtube.patches.OpenVideosFullscreenHookPatch; import app.morphe.extension.youtube.patches.VideoInformation; @@ -60,6 +60,15 @@ public final class NativePlaylistQueuePatch { private static final String SAVE_VIDEO_IDS_ARGUMENT = "app.morphe.native_playlist_queue.save_video_ids"; private static final String ENDPOINT_TAG_KEY = "com.google.android.libraries.youtube.innertube.endpoint.tag"; + private static final String SENDER_VIEW_KEY = "com.google.android.libraries.youtube.rendering.elements.sender_view"; + private static final String MIX_PLAYLIST_ID_PREFIX = "RD"; + private static final int UNDO_DURATION_MILLISECONDS = 5_000; + private static final int ACCESSIBLE_UNDO_DURATION_MILLISECONDS = 20_000; + + // Native navigation can be rejected (for example while signed out). A failed + // click must not authorize a much later, unrelated open of the same video. + private static final int NAVIGATION_TIMEOUT_MILLISECONDS = 10_000; + private static final ByteTrieSearch CARD_COMPONENTS = new ByteTrieSearch( ByteTrieSearch.convertStringsToBytes("video_lockup_with_attachment.eml")); private static final Map metadataCache = @@ -221,7 +230,7 @@ private record ConvertedPlaylist(String videoId, String playlistId, WeakReferenc // Let the setting cache update before checking the chosen provider. Utils.runOnMainThread(() -> { try { - if (isNativeQueueDisabled()) { + if (!AddToQueuePatch.isNativeQueueEnabled()) { releaseNativeQueue(); } } catch (Exception ex) { @@ -274,26 +283,21 @@ private static final class PendingReload { } } private static PendingReload pendingReload; - private static final int UNDO_DURATION_MILLISECONDS = 5_000; - private static final int ACCESSIBLE_UNDO_DURATION_MILLISECONDS = 20_000; private static final Set repeatHeaders = Collections.newSetFromMap(new WeakHashMap<>()); private static final Set playerControls = Collections.newSetFromMap(new WeakHashMap<>()); private static final Set notificationControls = Collections.newSetFromMap(new WeakHashMap<>()); - // Native navigation can be rejected (for example while signed out). A failed - // click must not authorize a much later, unrelated open of the same video. - private static final int NAVIGATION_TIMEOUT_MILLISECONDS = 10_000; - - private static final String SENDER_VIEW_KEY = "com.google.android.libraries.youtube.rendering.elements.sender_view"; - private static final String MIX_PLAYLIST_ID_PREFIX = "RD"; - private NativePlaylistQueuePatch() {} public static boolean isPatchIncluded() { return false; // Modified during patching. } - /** Read-only response hook; captures commands even when the action bar is hidden. */ + /** + * Injection point. + *

+ * Read-only response hook; captures commands even when the action bar is hidden. + */ public static void onWatchNextResponse(MessageLite response) { if (response == null) { return; @@ -328,11 +332,15 @@ public static void onWatchNextResponse(MessageLite response) { } } - /** Read-only shared element hook; preserves bytes and every other patch's changes. */ + /** + * Injection point. + *

+ * Read-only shared element hook; preserves bytes and every other patch's changes. + */ public static byte[] onElementLoaded(byte[] bytes) { // Cards are parsed on the main thread while the feed scrolls, so skip them // when the native queue is off. Missing metadata is fetched when queued. - if (bytes == null || isNativeQueueDisabled() || !CARD_COMPONENTS.matches(bytes)) { + if (bytes == null || !AddToQueuePatch.isNativeQueueEnabled() || !CARD_COMPONENTS.matches(bytes)) { return bytes; } byte[] element = bytes.clone(); @@ -364,12 +372,20 @@ private static void rememberChannel(String videoId, String title, String channel }); } - /** Injection point, after a native model acceptance or completed video transition. */ + /** + * Injection point. + *

+ * After a native model acceptance or completed video transition. + */ public static void onControllerUpdated(PlaylistController controller) { onControllerUpdated(controller, false); } - /** A completed native descriptor transition can leave the playlist model null. */ + /** + * Injection point. + *

+ * A completed native descriptor transition can leave the playlist model null. + */ public static void onPlaybackTransition(PlaylistController controller) { onControllerUpdated(controller, true); } @@ -408,7 +424,7 @@ private static void updateController(PlaylistController controller) { if (publishing) { return; } - if (isNativeQueueDisabled()) { + if (!AddToQueuePatch.isNativeQueueEnabled()) { releaseNativeQueue(); if (queue == null) { controllerRef = new WeakReference<>(controller); @@ -547,7 +563,7 @@ private static void onPlaybackContextChanged() { } private static void updatePlaybackContext() { - if (isNativeQueueDisabled()) { + if (!AddToQueuePatch.isNativeQueueEnabled()) { releaseNativeQueue(); return; } @@ -736,7 +752,11 @@ private static NativePlayback playbackFor(PlaylistController controller) { return null; } - /** Rebind after native service restoration; UI visibility does not own this sequence. */ + /** + * Injection point. + *

+ * Rebind after native service restoration; UI visibility does not own this sequence. + */ public static void onNativeQueueResponse(NativePlayback playback) { Utils.runOnMainThread(() -> { try { @@ -748,7 +768,7 @@ public static void onNativeQueueResponse(NativePlayback playback) { } private static void attachNativeQueueResponse(NativePlayback playback) { - if (isNativeQueueDisabled()) { + if (!AddToQueuePatch.isNativeQueueEnabled()) { releaseNativeQueue(); return; } @@ -793,10 +813,13 @@ private static void attachNativeQueueResponse(NativePlayback playback) { } } + /** + * Injection point. + */ public static void onNativeQueueSelection() { Utils.runOnMainThread(() -> { try { - if (isNativeQueueDisabled()) { + if (!AddToQueuePatch.isNativeQueueEnabled()) { releaseNativeQueue(); return; } @@ -839,7 +862,11 @@ private static void syncNativeSelection(String videoId) { } } - /** Runs after YouTube's click veto but before its logging decorates the endpoint. */ + /** + * Injection point. + *

+ * Runs after YouTube's click veto but before its logging decorates the endpoint. + */ public static void onRowClicked(RowClick click) { try { if (dragSession != null) { @@ -854,7 +881,13 @@ public static void onRowClicked(RowClick click) { if (index < 0 || index >= queue.entries().size() || index == queue.selectedIndex()) { return; } - authorizeRowSelection(queue.entries().get(index)); + RowSelection selection = new RowSelection(queue.entries().get(index)); + pendingSelection = selection; + Utils.runOnMainThreadDelayed(() -> { + if (pendingSelection == selection) { + pendingSelection = null; + } + }, NAVIGATION_TIMEOUT_MILLISECONDS); // Do not publish here: the original listener still needs its bound endpoint. } catch (Exception ex) { @@ -862,21 +895,14 @@ public static void onRowClicked(RowClick click) { } } - private static RowSelection authorizeRowSelection(PlaylistQueue.Entry entry) { - RowSelection selection = new RowSelection(entry); - pendingSelection = selection; - Utils.runOnMainThreadDelayed(() -> { - if (pendingSelection == selection) { - pendingSelection = null; - } - }, NAVIGATION_TIMEOUT_MILLISECONDS); - return selection; - } - - /** Keep the Queue label at its native text width instead of shrinking it in the counter chain. */ + /** + * Injection point. + *

+ * Keep the Queue label at its native text width instead of shrinking it in the counter chain. + */ public static void onPlaylistTitleBound(PlaylistTitle header) { // Without the native queue, only a previously widened title needs restoring. - if (isNativeQueueDisabled() && originalTitleWidths.isEmpty()) { + if (!AddToQueuePatch.isNativeQueueEnabled() && originalTitleWidths.isEmpty()) { return; } // Native bind can run inside publish, before ownership and row markers are @@ -921,12 +947,15 @@ public static void onPlaylistTitleBound(PlaylistTitle header) { title.setLayoutParams(params); } } - } catch (Exception exception) { - Logger.printException(() -> "onPlaylistTitleBound failure", exception); + } catch (Exception ex) { + Logger.printException(() -> "onPlaylistTitleBound failure", ex); } }); } + /** + * Injection point. + */ public static void onRepeatHeaderBound(RepeatHeader header) { Utils.runOnMainThreadNowOrLater(() -> { try { @@ -941,17 +970,11 @@ public static void onRepeatHeaderBound(RepeatHeader header) { }); } - private static void refreshRepeatHeader(RepeatHeader header) { - PlaylistController controller = ownedNavigator(header.patch_getNavigator()); - if (controller != null && header.patch_getPanel() == controller.patch_getPanel()) { - header.patch_enableRepeat(); - NativePlayback playback = playbackFor(controller); - header.patch_setShuffleAvailable(playback != null - && playback.patch_isLocalQueue() && queue.entries().size() > 1); - } - } - - /** Intercept only this queue's native header; account playlist actions fall through. */ + /** + * Injection point. + *

+ * Intercept only this queue's native header; account playlist actions fall through. + */ public static boolean onQueueShuffle(RepeatHeader header) { try { PlaylistController controller = ownedNavigator(header.patch_getNavigator()); @@ -974,10 +997,19 @@ public static boolean onQueueShuffle(RepeatHeader header) { private static void refreshRepeatHeaders() { for (RepeatHeader header : new ArrayList<>(repeatHeaders)) { - refreshRepeatHeader(header); + PlaylistController controller = ownedNavigator(header.patch_getNavigator()); + if (controller != null && header.patch_getPanel() == controller.patch_getPanel()) { + header.patch_enableRepeat(); + NativePlayback playback = playbackFor(controller); + header.patch_setShuffleAvailable(playback != null + && playback.patch_isLocalQueue() && queue.entries().size() > 1); + } } } + /** + * Injection point. + */ public static void onPlayerControlsCreated(PlayerControls controls) { Utils.runOnMainThreadNowOrLater(() -> playerControls.add(controls)); } @@ -1000,6 +1032,9 @@ private static void refreshNotificationControls() { } } + /** + * Injection point. + */ public static void onNotificationControlsCreated(NotificationControls controls) { // Finish native construction before consulting its providers or publishing // a refresh through the original observers. @@ -1015,6 +1050,9 @@ public static void onNotificationControlsCreated(NotificationControls controls) }); } + /** + * Injection point. + */ public static void onNotificationPlaybackChanged(NotificationControls controls) { try { // The state handler can reset navigation after selection was published. @@ -1027,7 +1065,11 @@ public static void onNotificationPlaybackChanged(NotificationControls controls) } } - /** Bridge contract: bit 0 = Previous, bit 1 = Next; -1 keeps native flags. */ + /** + * Injection point. + *

+ * Bridge contract: bit 0 = Previous, bit 1 = Next; -1 keeps native flags. + */ public static int notificationNavigationAvailability(NotificationControls controls) { try { ServiceNavigator service = controls.patch_getQueueMediaControls().patch_getQueueNavigator(); @@ -1045,6 +1087,9 @@ public static int notificationNavigationAvailability(NotificationControls contro } } + /** + * Injection point. + */ public static boolean playerControlAvailability(PlayerControls controls, boolean next, boolean original) { try { if (controls.patch_getNavigator() instanceof ServiceNavigator service && ownsNativePlayback(service)) { @@ -1058,6 +1103,9 @@ public static boolean playerControlAvailability(PlayerControls controls, boolean } } + /** + * Injection point. + */ public static boolean onNextPressed(PlayerControls controls) { try { return controls.patch_getNavigator() instanceof ServiceNavigator service && navigateNative(service, true); @@ -1067,6 +1115,9 @@ public static boolean onNextPressed(PlayerControls controls) { } } + /** + * Injection point. + */ public static boolean onPreviousPressed(PlayerControls controls) { try { return controls.patch_getNavigator() instanceof ServiceNavigator service && navigateNative(service, false); @@ -1076,7 +1127,11 @@ public static boolean onPreviousPressed(PlayerControls controls) { } } - /** Runs after native media logging, before either foreground or service skip routing. */ + /** + * Injection point. + *

+ * Runs after native media logging, before either foreground or service skip routing. + */ public static boolean onNotificationNext(MediaControls controls) { try { return navigateNative(controls.patch_getQueueNavigator(), true); @@ -1086,6 +1141,9 @@ public static boolean onNotificationNext(MediaControls controls) { } } + /** + * Injection point. + */ public static boolean onNotificationPrevious(MediaControls controls) { try { return navigateNative(controls.patch_getQueueNavigator(), false); @@ -1096,7 +1154,7 @@ public static boolean onNotificationPrevious(MediaControls controls) { } private static boolean ownsNativePlayback(ServiceNavigator service) { - if (isNativeQueueDisabled() || queue == null || service == null + if (!AddToQueuePatch.isNativeQueueEnabled() || queue == null || service == null || ShortsPlayerState.isOpen()) { return false; } @@ -1121,7 +1179,11 @@ private static PlaylistController ownedNavigator(Object navigator) { && controller.patch_getNavigator() == navigator ? controller : null; } - /** Keep the swipe restore drawable in sync when an attached queue row changes selection. */ + /** + * Injection point. + *

+ * Keep the swipe restore drawable in sync when an attached queue row changes selection. + */ public static void onSwipeBackgroundChanged(RowClick row, SwipeBackground background) { try { if (!Utils.isCurrentlyOnMainThread() || isQueueSuspended()) { @@ -1147,7 +1209,11 @@ public static void onSwipeBackgroundChanged(RowClick row, SwipeBackground backgr } } - /** Save arguments belong to this queue-launched native fragment, including after rotation. */ + /** + * Injection point. + *

+ * Save arguments belong to this queue-launched native fragment, including after rotation. + */ public static void onQueueSaveArguments(MessageLite endpoint, Bundle arguments) { try { ArrayList videoIds = QueueSaveActions.copyVideoIds(endpoint); @@ -1159,12 +1225,19 @@ public static void onQueueSaveArguments(MessageLite endpoint, Bundle arguments) } } + /** + * Injection point. + */ @Nullable public static List getQueueSaveVideoIds(@Nullable Bundle arguments) { return arguments == null ? null : arguments.getStringArrayList(SAVE_VIDEO_IDS_ARGUMENT); } - /** Override only commands from an owned row or the exact published queue header. */ + /** + * Injection point. + *

+ * Override only commands from an owned row or the exact published queue header. + */ @Nullable public static MessageLite onQueueMenuCommand(MessageLite endpoint, Map context) { if (endpoint == null || context == null) { @@ -1221,7 +1294,8 @@ public static MessageLite onQueueMenuCommand(MessageLite endpoint, Map con } releasePanel(); - return clearFeedback(endpoint, before, playback); + PendingUndo undo = beginUndo(before, playback, null); + return ((QueueUndoActions.Command) endpoint).patch_parseQueueCommand(clearFeedback(undo)); } int index = controller.patch_findQueueRow(owner); @@ -1264,17 +1338,11 @@ private static byte[] clearFeedback(PendingUndo undo) { str("offline_undo_snackbar_button_text")).toByteArray(); } - private static MessageLite clearFeedback(MessageLite endpoint, PlaylistQueue before, NativePlayback playback) { - PendingUndo undo = beginUndo(before, playback, null); - return ((QueueUndoActions.Command) endpoint).patch_parseQueueCommand( - clearFeedback(undo)); - } - /** Returns a request-bound callback for Reload's actual intent dispatch, or null if no queue was cleared. */ @Nullable public static Runnable prepareVideoReload(String videoId) { try { - if (isNativeQueueDisabled()) { + if (!AddToQueuePatch.isNativeQueueEnabled()) { return null; } // Reload's dismissal/open is delayed. Cancel older capabilities now, @@ -1317,9 +1385,9 @@ public static Runnable prepareVideoReload(String videoId) { reload.opening = true; } }; - } catch (Exception exception) { + } catch (Exception ex) { pendingReload = null; - Logger.printException(() -> "Could not prepare native queue Reload Undo", exception); + Logger.printException(() -> "Could not prepare native queue Reload Undo", ex); return null; } } @@ -1344,15 +1412,19 @@ private static void showPendingReloadFeedback() { PendingUndo undo = beginUndo(reload.before, playback, null); try { controller.patch_showQueueFeedback(clearFeedback(undo)); - } catch (Exception exception) { + } catch (Exception ex) { if (pendingUndo == undo) { pendingUndo = null; } - Logger.printException(() -> "Could not show native queue reload confirmation", exception); + Logger.printException(() -> "Could not show native queue reload confirmation", ex); } } - /** Called from accepted native mini-player close clicks or gestures before teardown. */ + /** + * Injection point. + *

+ * Called from accepted native mini-player close clicks or gestures before teardown. + */ public static void onQueuePlayerClosing() { try { if (queue == null) { @@ -1393,7 +1465,7 @@ private static void showClosedQueueFeedback() { if (undo == null || undo.closedPlayback() == null || queue != null) { return; } - if (isNativeQueueDisabled() || ShortsPlayerState.isOpen()) { + if (!AddToQueuePatch.isNativeQueueEnabled() || ShortsPlayerState.isOpen()) { pendingUndo = null; return; } @@ -1412,11 +1484,11 @@ private static void showClosedQueueFeedback() { undo.closedPlayback().feedbackOwner = null; try { owner.patch_showQueueFeedback(clearFeedback(undo)); - } catch (Exception exception) { + } catch (Exception ex) { if (pendingUndo == undo) { pendingUndo = null; } - Logger.printException(() -> "Could not show native queue player-close confirmation", exception); + Logger.printException(() -> "Could not show native queue player-close confirmation", ex); } } @@ -1451,7 +1523,11 @@ private static void expireUndo(PendingUndo undo, int duration) { }, duration); } - /** Native snackbar activation is not a row/menu action and need not retain that owner. */ + /** + * Injection point. + *

+ * Native snackbar activation is not a row/menu action and need not retain that owner. + */ public static boolean onQueueUndoCommand(MessageLite endpoint) { PendingUndo undo = pendingUndo; try { @@ -1512,7 +1588,11 @@ private static void undoQueueEdit(String token) { queue = queue.withRepeatMode(playback.patch_getRepeatMode()); } - /** Native row overflow creates its menu context directly, outside shared feed hooks. */ + /** + * Injection point. + *

+ * Native row overflow creates its menu context directly, outside shared feed hooks. + */ public static void onRowMenuOpened(Map context) { try { if (context == null) { @@ -1520,7 +1600,7 @@ public static void onRowMenuOpened(Map context) { } Object row = context.get(ENDPOINT_TAG_KEY); if (row instanceof FlyoutUtils.FlyoutMenuVideoIdInterface - && (!isNativeQueueDisabled() || (row instanceof QueueRow owned && owned.patch_isQueueOwned()))) { + && (AddToQueuePatch.isNativeQueueEnabled() || (row instanceof QueueRow owned && owned.patch_isQueueOwned()))) { FlyoutUtils.extractFlyoutIdFromMap(context); } } catch (Exception ex) { @@ -1528,7 +1608,11 @@ public static void onRowMenuOpened(Map context) { } } - /** Observe native start/end notifications; never replace the gesture listeners. */ + /** + * Injection point. + *

+ * Observe native start/end notifications; never replace the gesture listeners. + */ public static void onReorderStarted(Object holder, ReorderItems items, int index) { try { PlaylistController controller = readyController(); @@ -1549,6 +1633,9 @@ public static void onReorderStarted(Object holder, ReorderItems items, int index } } + /** + * Injection point. + */ public static void onReorderEnded(Object holder, ReorderItems items, int from, int to) { DragSession session = dragSession; if (session == null || session.holder != holder || session.items != items) { @@ -1583,7 +1670,11 @@ public static void onReorderEnded(Object holder, ReorderItems items, int from, i }); } - /** Native teardown can cancel a drag without an end notification. */ + /** + * Injection point. + *

+ * Native teardown can cancel a drag without an end notification. + */ public static void onReorderHolderUnbound(Object holder) { DragSession session = dragSession; if (session == null || session.holder != holder || session.ended) { @@ -1601,7 +1692,11 @@ public static void onReorderHolderUnbound(Object holder) { }); } - /** Shared video-ID hook; prefetched player responses must not change this state. */ + /** + * Injection point. + *

+ * Shared video-ID hook; prefetched player responses must not change this state. + */ public static void onVideoChanged(String videoId) { Utils.runOnMainThread(() -> { try { @@ -1616,7 +1711,7 @@ private static void updateVideo(String videoId) { if (!videoId.equals(VideoInformation.getVideoId())) { return; } - if (isNativeQueueDisabled()) { + if (!AddToQueuePatch.isNativeQueueEnabled()) { releaseNativeQueue(); return; } @@ -1711,7 +1806,7 @@ public static Runnable getFlyoutQueueAction(String videoId, boolean playLast) { } public static boolean canQueueFlyoutVideo(String videoId) { - return !isNativeQueueDisabled() && PlaylistQueue.isVideoId(videoId); + return AddToQueuePatch.isNativeQueueEnabled() && PlaylistQueue.isVideoId(videoId); } /** Only our Native queue makes Last available; account playlists and Mixes do not. */ @@ -1719,20 +1814,6 @@ public static boolean canShowFlyoutLast() { return queue != null; } - @Nullable - private static PlaylistController activePlaylistController() { - PlaylistController controller = controllerRef.get(); - PlayerType type = PlayerType.getCurrent(); - return controller != null && controller.patch_isAttached() && hasPlaylist(controller) - && !type.isNoneOrHidden() && type != PlayerType.INLINE_MINIMAL - && !ShortsPlayerState.isOpen() - && controller.patch_getVideoId().equals(VideoInformation.getVideoId()) ? controller : null; - } - - private static boolean isNativeQueueDisabled() { - return Settings.QUEUE_TYPE.get() != QueueType.NATIVE; - } - /** Release only our session; never modify the Morphe queue or its settings. */ private static void releaseNativeQueue() { pendingMixChoice = null; @@ -1754,21 +1835,21 @@ private static void releaseNativeQueue() { } releasePanel(); - } catch (Exception exception) { - Logger.printException(() -> "releaseNativeQueue failure", exception); + } catch (Exception ex) { + Logger.printException(() -> "releaseNativeQueue failure", ex); } } private static boolean canStartQueue() { PlayerType type = PlayerType.getCurrent(); - return !isNativeQueueDisabled() && queue == null && !ShortsPlayerState.isOpen() + return AddToQueuePatch.isNativeQueueEnabled() && queue == null && !ShortsPlayerState.isOpen() && (type == PlayerType.NONE || type == PlayerType.INLINE_MINIMAL); } @Nullable private static PlaylistController readyController() { PlaylistController controller = controllerRef.get(); - if (isNativeQueueDisabled() || isQueueSuspended() || controller == null + if (!AddToQueuePatch.isNativeQueueEnabled() || isQueueSuspended() || controller == null || !controller.patch_isAttached() || hasPlaylist(controller) || PlayerType.getCurrent().isNoneOrHidden() || PlayerType.getCurrent() == PlayerType.INLINE_MINIMAL @@ -1785,7 +1866,7 @@ private static boolean hasForeignPlaybackPlaylist() { return false; } ConvertedPlaylist converted = convertedPlaylist; - if (converted == null || isNativeQueueDisabled() + if (converted == null || !AddToQueuePatch.isNativeQueueEnabled() || !converted.videoId().equals(VideoInformation.getVideoId()) || !converted.playlistId().equals(playlistId)) { return true; @@ -1802,10 +1883,14 @@ private static boolean hasForeignPlaybackPlaylist() { return playback != playbackFor(returningController()); } - /** Keep a late response for the replaced list from reclaiming this same player. */ + /** + * Injection point. + *

+ * Keep a late response for the replaced list from reclaiming this same player. + */ public static boolean shouldIgnorePlaylistResponse(NativePlayback playback, MessageLite endpoint) { ConvertedPlaylist converted = convertedPlaylist; - if (converted == null || isNativeQueueDisabled() || endpoint == null + if (converted == null || !AddToQueuePatch.isNativeQueueEnabled() || endpoint == null || converted.playback().get() != playback || !converted.videoId().equals(VideoInformation.getVideoId())) { return false; @@ -1816,16 +1901,20 @@ public static boolean shouldIgnorePlaylistResponse(NativePlayback playback, Mess return command.hasWatchEndpoint() && converted.videoId().equals(command.getWatchEndpoint().getVideoId()) && converted.playlistId().equals(command.getWatchEndpoint().getPlaylistId()); - } catch (Exception exception) { - Logger.printException(() -> "shouldIgnorePlaylistResponse failure", exception); + } catch (Exception ex) { + Logger.printException(() -> "shouldIgnorePlaylistResponse failure", ex); return false; } } - /** Called after native stale-video validation, before storing the incoming model. */ + /** + * Injection point. + *

+ * Called after native stale-video validation, before storing the incoming model. + */ public static boolean shouldIgnorePlaylistModel(PlaylistController controller, PlaylistModel model) { ConvertedPlaylist converted = convertedPlaylist; - return converted != null && !isNativeQueueDisabled() && !publishing + return converted != null && AddToQueuePatch.isNativeQueueEnabled() && !publishing && converted.videoId().equals(VideoInformation.getVideoId()) && converted.videoId().equals(controller.patch_getVideoId()) && converted.playlistId().equals(model.patch_getPlaylistId()) @@ -1839,7 +1928,7 @@ private static boolean hasPlaylist(PlaylistController controller) { } private static void addToQueue(String videoId, boolean playLast) { - if (isNativeQueueDisabled()) { + if (!AddToQueuePatch.isNativeQueueEnabled()) { releaseNativeQueue(); return; } @@ -1850,7 +1939,14 @@ private static void addToQueue(String videoId, boolean playLast) { return; } - PlaylistController playlist = activePlaylistController(); + // An attached account playlist of the active regular video is converted to the queue. + PlaylistController playlist = controllerRef.get(); + PlayerType type = PlayerType.getCurrent(); + if (playlist == null || !playlist.patch_isAttached() || !hasPlaylist(playlist) + || type.isNoneOrHidden() || type == PlayerType.INLINE_MINIMAL || ShortsPlayerState.isOpen() + || !playlist.patch_getVideoId().equals(VideoInformation.getVideoId())) { + playlist = null; + } String channelName = videoId.equals(FlyoutUtils.getFlyoutVideoId()) ? FlyoutUtils.getFlyoutChannelName() : ""; try { @@ -1930,8 +2026,8 @@ private static void addToQueue(String videoId, boolean playLast) { Utils.showToastShort(str("morphe_queue_manager_fetch_succeeded_add")); completeMetadata(); } - } catch (Exception exception) { - Logger.printException(() -> "Could not add video to native playlist queue", exception); + } catch (Exception ex) { + Logger.printException(() -> "Could not add video to native playlist queue", ex); Utils.showToastShort(str("morphe_native_playlist_queue_unavailable")); } } @@ -1965,19 +2061,19 @@ private static void startQueue(QueueStart start) { try { LoadVideoPatch.openVideoIntent(start.watchUrl(), false); - } catch (RuntimeException exception) { + } catch (RuntimeException ex) { if (pendingQueueStart == start) { clearPendingQueueStart(); OpenVideosFullscreenHookPatch.keepVideoMinimized(null); } - throw exception; + throw ex; } } /** Commit a first-item queue only after native playback accepts the requested video. */ private static void tryStartQueue() { - if (isNativeQueueDisabled()) { + if (!AddToQueuePatch.isNativeQueueEnabled()) { releaseNativeQueue(); return; } @@ -2074,7 +2170,11 @@ private static void refreshOwnedPanel(String videoId) { } } - /** Keep an individual music-video tap in our queue instead of starting its automatic Mix. */ + /** + * Injection point. + *

+ * Keep an individual music-video tap in our queue instead of starting its automatic Mix. + */ public static MessageLite prepareQueueVideoCommand(MessageLite endpoint, Map context) { if (!Utils.isCurrentlyOnMainThread() || queue == null || publishing || pendingSelection != null || dragSession != null || context == null @@ -2109,9 +2209,13 @@ public static MessageLite prepareQueueVideoCommand(MessageLite endpoint, Map + * Pause an intentional Mix tap before any watch-state mutation, using the native dialog. + */ public static boolean onQueueMixTap(@Nullable MessageLite endpoint, @Nullable Map context) { - if (isNativeQueueDisabled() || !Utils.isCurrentlyOnMainThread() || endpoint == null) { + if (!AddToQueuePatch.isNativeQueueEnabled() || !Utils.isCurrentlyOnMainThread() || endpoint == null) { return false; } boolean intercepted = false; @@ -2178,9 +2282,9 @@ public static boolean onQueueMixTap(@Nullable MessageLite endpoint, @Nullable Ma str("morphe_native_playlist_queue_mix_choice"), str("morphe_native_playlist_queue_watch_mix"), str("morphe_native_playlist_queue_add_mix")).toByteArray()); return true; - } catch (Exception exception) { + } catch (Exception ex) { pendingMixChoice = null; - Logger.printException(() -> "onQueueMixTap failure", exception); + Logger.printException(() -> "onQueueMixTap failure", ex); // A failed prompt must not silently discard the playing queue. if (intercepted) { Utils.showToastShort(str("morphe_native_playlist_queue_unavailable")); @@ -2191,7 +2295,7 @@ public static boolean onQueueMixTap(@Nullable MessageLite endpoint, @Nullable Ma private static boolean ownsMixChoice(MixChoice choice, @Nullable PlaylistController controller, @Nullable NativePlayback playback) { - return !isNativeQueueDisabled() && !publishing && pendingSelection == null && dragSession == null + return AddToQueuePatch.isNativeQueueEnabled() && !publishing && pendingSelection == null && dragSession == null && queue != null && controller != null && controller == readyController() && isQueuePlayback(controller, playback) && playbackFor(controller) == playback && queue.currentEntry().videoId().equals(VideoInformation.getVideoId()) @@ -2246,15 +2350,19 @@ private static void completeMixAddition(MixChoice choice, @Nullable List "Could not append Mix to native queue", exception); + } catch (Exception ex) { + Logger.printException(() -> "Could not append Mix to native queue", ex); Utils.showToastShort(str("morphe_queue_manager_fetch_failed_add")); } } - /** Native watch dispatch reads this flag before constructing the playback descriptor. */ + /** + * Injection point. + *

+ * Native watch dispatch reads this flag before constructing the playback descriptor. + */ public static Map prepareQueueStartContext(MessageLite endpoint, Map context) { - if (endpoint == null || isNativeQueueDisabled()) { + if (endpoint == null || !AddToQueuePatch.isNativeQueueEnabled()) { return context; } QueueStart start = pendingQueueStart; @@ -2372,9 +2480,9 @@ private static boolean publish(PlaylistController controller, PlaylistQueue cand refreshPlayerControls(controller.patch_getNavigator()); QueueFullscreenActions.refresh(); return true; - } catch (Exception exception) { + } catch (Exception ex) { if (committed) { - Logger.printException(() -> "Could not refresh native playlist queue controls", exception); + Logger.printException(() -> "Could not refresh native playlist queue controls", ex); return true; } @@ -2382,7 +2490,7 @@ private static boolean publish(PlaylistController controller, PlaylistQueue cand controller.patch_restorePanel(acceptedModel, previousModel); } - Logger.printException(() -> "Could not display native playlist queue", exception); + Logger.printException(() -> "Could not display native playlist queue", ex); if (reportFailure) { Utils.showToastShort(str("morphe_native_playlist_queue_unavailable")); } @@ -2417,8 +2525,8 @@ private static void releasePanel() { if (controller != null && expectedModel != null) { try { controller.patch_restorePanel(expectedModel, restoreModel); - } catch (Exception exception) { - Logger.printException(() -> "Could not restore native playlist panel", exception); + } catch (Exception ex) { + Logger.printException(() -> "Could not restore native playlist panel", ex); } } @@ -2432,24 +2540,11 @@ static boolean ownsFullscreenPlaylist() { // Model/backend ownership survives a hidden watch screen. YouTube's native // quick-actions controller, rather than this binding, decides visibility. - return !isNativeQueueDisabled() && queue != null && publishedModel != null && controller != null + return AddToQueuePatch.isNativeQueueEnabled() && queue != null && publishedModel != null && controller != null && controller.patch_getModel() == publishedModel && playback != null && playback.patch_isLocalQueue(); } - private static PlaylistQueueRenderer.SwipeAction removalAction() { - var label = PlaylistQueueRenderer.Accessibility.newBuilder().setAccessibilityData( - PlaylistQueueRenderer.AccessibilityLabel.newBuilder().setLabel(str("remove"))); - var endpoint = PlaylistQueueRenderer.NavigationEndpoint.newBuilder().setHideEnclosingAction( - PlaylistQueueRenderer.HideEnclosingAction.newBuilder().setHack(true)); - var button = PlaylistQueueRenderer.SwipeButton.newBuilder() - .setStyle(PlaylistQueueRenderer.SwipeButton.Style.BLACK_FILLED) - .setSize(PlaylistQueueRenderer.SwipeButton.Size.DEFAULT).setIsDisabled(false) - .setIcon(Icon.newBuilder().setYtIconType(YTIconType.DELETE)) - .setAccessibilityData(label).setServiceEndpoint(endpoint); - return PlaylistQueueRenderer.SwipeAction.newBuilder().setButtonRenderer(button).build(); - } - private static PlaylistQueueRenderer.Menu playlistMenu(PlaylistQueue source) { List videoIds = new ArrayList<>(); for (PlaylistQueue.Entry entry : source.entries()) { @@ -2537,7 +2632,16 @@ private static byte[] renderer(PlaylistQueue source) { .setSelectedIndex(source.selectedIndex()).setPlaylistId("").setVideoCount(source.entries().size()) .setLocalSelectedIndex(source.selectedIndex()) .setMenu(playlistMenu(source)); - var swipeToRemove = removalAction(); + var removeLabel = PlaylistQueueRenderer.Accessibility.newBuilder().setAccessibilityData( + PlaylistQueueRenderer.AccessibilityLabel.newBuilder().setLabel(str("remove"))); + var removeEndpoint = PlaylistQueueRenderer.NavigationEndpoint.newBuilder().setHideEnclosingAction( + PlaylistQueueRenderer.HideEnclosingAction.newBuilder().setHack(true)); + var swipeToRemove = PlaylistQueueRenderer.SwipeAction.newBuilder().setButtonRenderer( + PlaylistQueueRenderer.SwipeButton.newBuilder() + .setStyle(PlaylistQueueRenderer.SwipeButton.Style.BLACK_FILLED) + .setSize(PlaylistQueueRenderer.SwipeButton.Size.DEFAULT).setIsDisabled(false) + .setIcon(Icon.newBuilder().setYtIconType(YTIconType.DELETE)) + .setAccessibilityData(removeLabel).setServiceEndpoint(removeEndpoint)).build(); for (int index = 0; index < source.entries().size(); index++) { PlaylistQueue.Entry entry = source.entries().get(index); VideoMetadata metadata = metadataCache.get(entry.videoId()); From c5f9f94ee71a480be00a432ee364f4e7099b9537 Mon Sep 17 00:00:00 2001 From: 0xrxL <127248639+0xrxL@users.noreply.github.com> Date: Mon, 26 Oct 2026 00:18:54 +0100 Subject: [PATCH 073/189] fix(YouTube - Add to queue): Native queue index errors crash before Android 13 --- .../extension/youtube/patches/queue/PlaylistQueue.java | 8 ++++---- .../nativeplaylistqueue/QueueRuntimeBoundaryTest.kt | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/queue/PlaylistQueue.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/queue/PlaylistQueue.java index 28643a9d60..e5a970f938 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/queue/PlaylistQueue.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/queue/PlaylistQueue.java @@ -66,7 +66,7 @@ public PlaylistQueue withRepeatMode(int mode) { public PlaylistQueue select(int index) { if (index < 0 || index >= entries.size()) { - throw new IndexOutOfBoundsException(index); + throw new IndexOutOfBoundsException("Index out of range: " + index); } return new PlaylistQueue(entries, index, repeatMode); } @@ -103,7 +103,7 @@ public PlaylistQueue playNextAndSelect(String videoId) { public PlaylistQueue remove(int index) { if (index < 0 || index >= entries.size()) { - throw new IndexOutOfBoundsException(index); + throw new IndexOutOfBoundsException("Index out of range: " + index); } if (index == selectedIndex) { return this; @@ -116,10 +116,10 @@ public PlaylistQueue remove(int index) { /** Move an entry with the native adapter's remove-then-insert semantics. */ public PlaylistQueue move(int from, int to) { if (from < 0 || from >= entries.size()) { - throw new IndexOutOfBoundsException(from); + throw new IndexOutOfBoundsException("Index out of range: " + from); } if (to < 0 || to >= entries.size()) { - throw new IndexOutOfBoundsException(to); + throw new IndexOutOfBoundsException("Index out of range: " + to); } if (from == to) { return this; diff --git a/patches/src/test/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/QueueRuntimeBoundaryTest.kt b/patches/src/test/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/QueueRuntimeBoundaryTest.kt index c7fd15c623..c284ccab27 100644 --- a/patches/src/test/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/QueueRuntimeBoundaryTest.kt +++ b/patches/src/test/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/QueueRuntimeBoundaryTest.kt @@ -1506,13 +1506,13 @@ class QueueRuntimeBoundaryTest { } public static void queueTypeSelection() throws Exception { Settings.QUEUE_TYPE.value = QueueType.NATIVE; - require(!(boolean) call("isNativeQueueDisabled"), "Native queue type must enable native queue"); + require(AddToQueuePatch.isNativeQueueEnabled(), "Native queue type must enable native queue"); Settings.QUEUE_TYPE.value = QueueType.MORPHE; - require((boolean) call("isNativeQueueDisabled"), "Morphe queue type must disable native queue"); + require(!AddToQueuePatch.isNativeQueueEnabled(), "Morphe queue type must disable native queue"); Settings.QUEUE_TYPE.value = QueueType.LOCAL; - require((boolean) call("isNativeQueueDisabled"), "Premium-style queue type must disable native queue"); + require(!AddToQueuePatch.isNativeQueueEnabled(), "Premium-style queue type must disable native queue"); Settings.QUEUE_TYPE.value = QueueType.DEFAULT; - require((boolean) call("isNativeQueueDisabled"), "Default queue type must disable native queue"); + require(!AddToQueuePatch.isNativeQueueEnabled(), "Default queue type must disable native queue"); } public static void acceptedHandoff() throws Exception { Settings.QUEUE_TYPE.value = QueueType.NATIVE; From efbc9b1ea1739b617306f19900fa398251131494 Mon Sep 17 00:00:00 2001 From: 0xrxL <127248639+0xrxL@users.noreply.github.com> Date: Mon, 26 Oct 2026 05:05:20 +0100 Subject: [PATCH 074/189] fix(YouTube - Miniplayer): Related videos lists show a black frame after exiting fullscreen and scroll down --- .../youtube/patches/MiniplayerPatch.java | 33 ++++++++++ .../youtube/layout/miniplayer/Fingerprints.kt | 48 ++++++-------- .../layout/miniplayer/MiniplayerPatch.kt | 66 +++++++++++++++---- 3 files changed, 108 insertions(+), 39 deletions(-) diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/MiniplayerPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/MiniplayerPatch.java index 95e59007f7..991123bbcd 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/MiniplayerPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/MiniplayerPatch.java @@ -567,6 +567,39 @@ public static boolean getMaximizeAnimation() { return MINIPLAYER_HORIZONTAL_DRAG_ENABLED; } + /** + * Injection point. + * + * @return The orientation the list is laid out in. + */ + public static int getListOrientation(View list) { + return list.getResources().getConfiguration().orientation; + } + + /** + * Injection point. + *

+ * After an orientation change, YouTube detaches and attaches a list again the next time its + * size changes, and the list is empty for a frame until its items are laid out again. + * Exiting fullscreen brings the list back with the size and orientation it had before, + * so nothing has to be rebuilt. Left pending, the list is rebuilt the next time its size + * changes for other reasons, such as when the player shrinks while scrolling the watch page, + * or when the home is laid out again after minimizing the player, and the black frame + * shows then. + * + * @param rebuildPending If the list is rebuilt the next time its size changes. + * @param sizeOrientation The orientation the current size of the list was laid out in, + * or 0 if not known yet. + */ + public static boolean getListRebuildPending(View list, boolean rebuildPending, int sizeOrientation) { + if (rebuildPending && sizeOrientation == getListOrientation(list)) { + Logger.printDebug(() -> "Skipping rebuild of list with unchanged size and orientation"); + return false; + } + + return rebuildPending; + } + /** * Injection point. */ diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/miniplayer/Fingerprints.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/miniplayer/Fingerprints.kt index 4c034780d9..779f7e256b 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/miniplayer/Fingerprints.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/miniplayer/Fingerprints.kt @@ -237,38 +237,32 @@ internal object NextGenWatchLayoutOnInterceptTouchEventFingerprint : Fingerprint parameters = listOf("Landroid/view/MotionEvent;") ) -private object TopBarControllerFingerprint : Fingerprint( - returnType = "Landroid/os/Parcelable;", - parameters = listOf(), - filters = listOf( - string("instance-offset") - ) -) +private const val RECYCLER_VIEW_CLASS_TYPE = "Landroid/support/v7/widget/RecyclerView;" /** - * If the top bars of the home and other pages can be laid out behind the status bar. - * Returns false while the player is maximized, in fullscreen or sliding between them. + * Called on every layout of a list. If the list has a new size, the size is kept and the list + * is detached and attached again when an orientation change is pending. */ -internal fun getTopBarBehindStatusBarFingerprint( - playerTypeEnumType: String -) = object : Fingerprint( - classFingerprint = TopBarControllerFingerprint, - returnType = "Z", - parameters = listOf(), +internal object ListLayoutChangeFingerprint : Fingerprint( + returnType = "V", + parameters = listOf(RECYCLER_VIEW_CLASS_TYPE), filters = listOf( - methodCall( // Player is sliding. - definingClass = playerTypeEnumType, - returnType = "Z" - ), - opcode(Opcode.MOVE_RESULT, location = MatchAfterImmediately()), - methodCall( // Player is maximized or in fullscreen. - definingClass = playerTypeEnumType, - returnType = "Z", - location = MatchAfterWithin(5) - ), - opcode(Opcode.MOVE_RESULT, location = MatchAfterImmediately()) + methodCall(definingClass = RECYCLER_VIEW_CLASS_TYPE, name = "getWidth"), + methodCall(definingClass = RECYCLER_VIEW_CLASS_TYPE, name = "getHeight", location = MatchAfterWithin(2)), + // Forces the size to be applied again. + fieldAccess(opcode = Opcode.IGET_BOOLEAN, type = "Z", location = MatchAfterWithin(2)), + // Last width and height. + fieldAccess(opcode = Opcode.IGET, type = "I", location = MatchAfterWithin(2)), + fieldAccess(opcode = Opcode.IGET, type = "I", location = MatchAfterWithin(2)), + // Same size, only attaches the list. + methodCall(parameters = listOf(RECYCLER_VIEW_CLASS_TYPE), returnType = "V", location = MatchAfterWithin(3)), + // New size, kept as the last width and height. + fieldAccess(opcode = Opcode.IPUT, type = "I", location = MatchAfterWithin(3)), + fieldAccess(opcode = Opcode.IPUT, type = "I", location = MatchAfterImmediately()), + // An orientation change is pending, and the list is detached and attached again. + fieldAccess(opcode = Opcode.IGET_BOOLEAN, type = "Z", location = MatchAfterImmediately()) ) -) {} +) private object MiniplayerDimensionsCalculatorParentFingerprint : Fingerprint( accessFlags = listOf(AccessFlags.PUBLIC, AccessFlags.FINAL), diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/miniplayer/MiniplayerPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/miniplayer/MiniplayerPatch.kt index 9fc77b0ccb..cdc04509fb 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/miniplayer/MiniplayerPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/miniplayer/MiniplayerPatch.kt @@ -21,6 +21,7 @@ import app.morphe.patcher.util.cloneParameters import app.morphe.patcher.util.findFreeRegister import app.morphe.patcher.util.getReference import app.morphe.patcher.util.numberOfParameterRegisters +import app.morphe.patcher.util.proxy.mutableTypes.MutableField.Companion.toMutable import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable import app.morphe.patcher.util.smali.ExternalLabel @@ -46,7 +47,6 @@ import app.morphe.patches.youtube.misc.playservice.versionCheckPatch import app.morphe.patches.youtube.misc.settings.PreferenceScreen import app.morphe.patches.youtube.misc.settings.settingsPatch import app.morphe.patches.youtube.shared.Constants.COMPATIBILITY_YOUTUBE -import app.morphe.patches.youtube.shared.PlayerTypeEnumFingerprint import app.morphe.patches.youtube.shared.openVideoIntentPatch import app.morphe.patches.youtube.shared.startVideoInformerPatch import app.morphe.patches.youtube.video.format.hookAdaptiveFormat @@ -55,6 +55,7 @@ import app.morphe.patches.youtube.video.information.videoInformationPatch import app.morphe.util.addInstructionsAtControlFlowLabel import app.morphe.util.findInstructionIndicesReversedOrThrow import app.morphe.util.indexOfFirstInstructionOrThrow +import app.morphe.util.indexOfFirstInstructionReversedOrThrow import app.morphe.util.indexOfFirstLiteralInstructionOrThrow import app.morphe.util.insertLiteralOverride import com.android.tools.smali.dexlib2.AccessFlags @@ -66,6 +67,7 @@ import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction import com.android.tools.smali.dexlib2.iface.reference.FieldReference +import com.android.tools.smali.dexlib2.immutable.ImmutableField import com.android.tools.smali.dexlib2.immutable.ImmutableMethod import com.android.tools.smali.dexlib2.immutable.ImmutableMethodParameter @@ -443,19 +445,59 @@ val miniplayerPatch = bytecodePatch( // endregion - // region Watch transition animation. + // region List rebuilt after exiting fullscreen. + + // After an orientation change a list is detached and attached again the next time its + // size changes, and it is empty for a frame. Exiting fullscreen brings the list back + // unchanged, and the rebuild otherwise happens later, such as when scrolling the watch + // page or when the home is laid out again after minimizing the player. + ListLayoutChangeFingerprint.let { + val pendingField = it.instructionMatches[8].getFieldAccessed() + + // Orientation the last size of the list was laid out in. + val sizeOrientationField = ImmutableField( + it.classDef.type, + "patch_sizeOrientation", + "I", + AccessFlags.PUBLIC.value, + null, + null, + null + ).toMutable() + it.classDef.fields.add(sizeOrientationField) - // While the player is maximized, the home is laid out below the status bar instead of - // behind it. The home is then laid out again when the minimize animation ends, and the - // feed is black until it is drawn again. Without the animation the layout is never - // changed, so the home keeps the layout of the minimized player. - getTopBarBehindStatusBarFingerprint(PlayerTypeEnumFingerprint.originalClassDef.type).let { it.method.apply { - listOf(it.instructionMatches[1].index, it.instructionMatches[3].index) - .sortedDescending().forEach { index -> - val register = getInstruction(index).registerA - addInstruction(index + 1, "const/16 v$register, 0x0") - } + // New size, kept as the last width and height. + val sizeKeptIndex = it.instructionMatches[7].index + 1 + val orientationRegister = findFreeRegister(sizeKeptIndex) + + addInstructions( + sizeKeptIndex, + """ + invoke-static { p1 }, $EXTENSION_CLASS->getListOrientation(Landroid/view/View;)I + move-result v$orientationRegister + iput v$orientationRegister, p0, $sizeOrientationField + """ + ) + + // Same size, before the list is attached. The controller register is reused there. + val sameSizeIndex = indexOfFirstInstructionReversedOrThrow( + it.instructionMatches[5].index, + Opcode.IGET_OBJECT + ) + val pendingRegister = findFreeRegister(sameSizeIndex) + val sizeOrientationRegister = findFreeRegister(sameSizeIndex, pendingRegister) + + addInstructionsAtControlFlowLabel( + sameSizeIndex, + """ + iget-boolean v$pendingRegister, p0, $pendingField + iget v$sizeOrientationRegister, p0, $sizeOrientationField + invoke-static { p1, v$pendingRegister, v$sizeOrientationRegister }, $EXTENSION_CLASS->getListRebuildPending(Landroid/view/View;ZI)Z + move-result v$pendingRegister + iput-boolean v$pendingRegister, p0, $pendingField + """ + ) } } From e32b05c663e30c23cb998e73fa23d78a58998962 Mon Sep 17 00:00:00 2001 From: 0xrxL <127248639+0xrxL@users.noreply.github.com> Date: Mon, 26 Oct 2026 05:50:10 +0100 Subject: [PATCH 075/189] fix(YouTube - Litho filter): Filtered elements leave empty cells in the related videos grid of the tablet layout --- .../patches/components/LithoFilterPatch.java | 73 +++++++++++++++++-- .../shared/misc/litho/filter/Fingerprints.kt | 27 +++++++ .../misc/litho/filter/LithoFilterPatch.kt | 67 +++++++++++++++++ 3 files changed, 162 insertions(+), 5 deletions(-) diff --git a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/patches/components/LithoFilterPatch.java b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/patches/components/LithoFilterPatch.java index 9db4b22b9e..42bb41ec02 100644 --- a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/patches/components/LithoFilterPatch.java +++ b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/patches/components/LithoFilterPatch.java @@ -68,6 +68,18 @@ public interface ListItemDecoratorInterface { Object patch_getListItem(); } + /** + * Interface added to the sections of the lists, such as the feed or the related videos. + */ + public interface ListSectionInterface { + /** + * Removes the item from the section, which lays itself out again without it. + * + * @return If the section has the item. + */ + boolean patch_removeListItem(Object item); + } + /** * Placeholder for actual filters. */ @@ -108,6 +120,11 @@ private static final class DummyFilter extends Filter { } */ private static final List>> observableLists = new ArrayList<>(); + /** + * Sections of the lists, such as the feed or the related videos, that hold the items. + */ + private static final List> listSections = new ArrayList<>(); + private static final StringTrieSearch contextSearchTree = new StringTrieSearch(); private static final StringTrieSearch pathSearchTree = new StringTrieSearch(); private static final StringTrieSearch identifierSearchTree = new StringTrieSearch(); @@ -248,13 +265,25 @@ public static void addObservableList(List list) { } } + /** + * Injection point. + * Called when a section of a list, such as the feed or the related videos, is created. + */ + public static void addListSection(ListSectionInterface section) { + synchronized (listSections) { + // Remove the sections that no longer exist. + listSections.removeIf(reference -> reference.get() == null); + listSections.add(new WeakReference<>(section)); + } + } + /** * Injection point. * Called when the whole element is hidden. *

* Hiding an element replaces it with an empty component, but the item of the element stays - * in its list. The grids, such as the feed of the tablet layout, then show an empty cell - * in its place. To prevent this, the item is removed from the list. + * in its list. The grids of the tablet layout, such as the feed or the related videos, + * then show an empty cell in its place. To prevent this, the item is removed from the list. */ public static void removeFilteredElementListItem(@Nullable ContextInterface contextInterface) { try { @@ -272,7 +301,7 @@ public static void removeFilteredElementListItem(@Nullable ContextInterface cont Object item = listItemDecorator.patch_getListItem(); if (item != null) { // The lists can only be changed on the main thread. - Utils.runOnMainThread(() -> removeObservableListItem(item)); + Utils.runOnMainThread(() -> removeListItem(item)); } } } @@ -281,7 +310,39 @@ public static void removeFilteredElementListItem(@Nullable ContextInterface cont } } - private static void removeObservableListItem(Object item) { + private static void removeListItem(Object item) { + // The sections group their items into the rows of some grids, such as the related videos, + // and lay the rows out again. Other grids, such as the feed, have a list for each item. + if (removeListSectionItem(item) || removeObservableListItem(item)) { + Logger.printDebug(() -> "Removed list item of filtered element: " + item); + } + } + + private static boolean removeListSectionItem(Object item) { + List sections = new ArrayList<>(); + synchronized (listSections) { + Iterator> iterator = listSections.iterator(); + while (iterator.hasNext()) { + ListSectionInterface section = iterator.next().get(); + if (section == null) { + iterator.remove(); + continue; + } + sections.add(section); + } + } + + // Removing the item notifies the list observers, so it is done outside the lock. + for (ListSectionInterface section : sections) { + if (section.patch_removeListItem(item)) { + return true; + } + } + + return false; + } + + private static boolean removeObservableListItem(Object item) { List itemList = null; int itemIndex = -1; @@ -305,8 +366,10 @@ private static void removeObservableListItem(Object item) { // Removing the item notifies the list observers, so it is done outside the lock. if (itemList != null) { itemList.remove(itemIndex); - Logger.printDebug(() -> "Removed list item of filtered element: " + item); + return true; } + + return false; } private static boolean useLegacyLithoFiltering() { diff --git a/patches/src/main/kotlin/app/morphe/patches/shared/misc/litho/filter/Fingerprints.kt b/patches/src/main/kotlin/app/morphe/patches/shared/misc/litho/filter/Fingerprints.kt index b44f57151b..97d44f22e9 100644 --- a/patches/src/main/kotlin/app/morphe/patches/shared/misc/litho/filter/Fingerprints.kt +++ b/patches/src/main/kotlin/app/morphe/patches/shared/misc/litho/filter/Fingerprints.kt @@ -11,11 +11,13 @@ package app.morphe.patches.shared.misc.litho.filter import app.morphe.patcher.Fingerprint +import app.morphe.patcher.InstructionLocation.MatchAfterImmediately import app.morphe.patcher.InstructionLocation.MatchAfterWithin import app.morphe.patcher.OpcodesFilter import app.morphe.patcher.fieldAccess import app.morphe.patcher.literal import app.morphe.patcher.methodCall +import app.morphe.patcher.opcode import app.morphe.patcher.string import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode @@ -157,3 +159,28 @@ internal object ObservableListConstructorFingerprint : Fingerprint( accessFlags = listOf(AccessFlags.PROTECTED, AccessFlags.CONSTRUCTOR), parameters = listOf("Ljava/util/List;", "L") ) + +/** + * Removes an item from a section of a list, such as the feed or the related videos, and lays the + * section out again without it. The grids of the tablet layout that group the items of a section + * into rows, such as the related videos, are laid out again too. + */ +internal object ListSectionRemoveItemFingerprint : Fingerprint( + returnType = "V", + parameters = listOf("Ljava/lang/Object;"), + filters = listOf( + // The header of the section, which hides the whole section instead. + fieldAccess(opcode = Opcode.IGET_OBJECT), + opcode(Opcode.IF_NE, location = MatchAfterWithin(2)), + // The items of the section. + fieldAccess(opcode = Opcode.IGET_OBJECT, type = "Ljava/util/List;", location = MatchAfterWithin(4)), + methodCall( + definingClass = "Ljava/util/List;", + name = "remove", + parameters = listOf("Ljava/lang/Object;"), + location = MatchAfterImmediately() + ), + opcode(Opcode.MOVE_RESULT, location = MatchAfterImmediately()), + opcode(Opcode.IF_EQZ, location = MatchAfterImmediately()) + ) +) diff --git a/patches/src/main/kotlin/app/morphe/patches/shared/misc/litho/filter/LithoFilterPatch.kt b/patches/src/main/kotlin/app/morphe/patches/shared/misc/litho/filter/LithoFilterPatch.kt index 3829ef5c68..d30fdce1c4 100644 --- a/patches/src/main/kotlin/app/morphe/patches/shared/misc/litho/filter/LithoFilterPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/shared/misc/litho/filter/LithoFilterPatch.kt @@ -35,13 +35,17 @@ import app.morphe.patches.shared.misc.litho.context.EXTENSION_CONTEXT_INTERFACE import app.morphe.patches.shared.misc.litho.context.conversionContextClassDef import app.morphe.util.addInstructionsAtControlFlowLabel import app.morphe.util.findFieldFromToString +import app.morphe.util.indexOfFirstInstruction import app.morphe.util.insertLiteralOverride import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation +import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction import com.android.tools.smali.dexlib2.iface.reference.MethodReference import com.android.tools.smali.dexlib2.immutable.ImmutableMethod +import com.android.tools.smali.dexlib2.immutable.ImmutableMethodParameter +import com.android.tools.smali.dexlib2.util.MethodUtil import java.lang.ref.WeakReference internal const val EXTENSION_CLASS = "Lapp/morphe/extension/shared/patches/components/LithoFilterPatch;" @@ -51,6 +55,9 @@ internal const val EXTENSION_FILTER = "[Lapp/morphe/extension/shared/patches/com private const val EXTENSION_LIST_ITEM_DECORATOR_INTERFACE = $$"Lapp/morphe/extension/shared/patches/components/LithoFilterPatch$ListItemDecoratorInterface;" +private const val EXTENSION_LIST_SECTION_INTERFACE = + $$"Lapp/morphe/extension/shared/patches/components/LithoFilterPatch$ListSectionInterface;" + private const val COMMAND_EVENT_DATA_DECORATORS_PROPERTY = ", globalCommandEventDataDecoratorsInternal=" // Registers used in extension helperMethod. @@ -399,12 +406,72 @@ internal fun sharedLithoFilterPatch( } // Keep the observable lists, to remove the items from them. + // Some grids, such as the feed, have a list for each item. ObservableListConstructorFingerprint.method.apply { addInstruction( implementation!!.instructions.lastIndex, "invoke-static { p0 }, $EXTENSION_CLASS->addObservableList(Ljava/util/List;)V" ) } + + // Keep the sections of the lists, to remove the items from them. Some grids, such as + // the related videos, group the items of a section into rows, which the section lays + // out again when an item is removed. + ListSectionRemoveItemFingerprint.let { + val itemsField = it.instructionMatches[2].getFieldAccessed() + val removeItemMethod = it.method + + it.classDef.apply { + interfaces.add(EXTENSION_LIST_SECTION_INTERFACE) + + methods.add( + ImmutableMethod( + type, + "patch_removeListItem", + listOf(ImmutableMethodParameter("Ljava/lang/Object;", null, "item")), + "Z", + AccessFlags.PUBLIC.value or AccessFlags.FINAL.value, + null, + null, + MutableMethodImplementation(3), + ).toMutable().apply { + addInstructionsWithLabels( + 0, + """ + iget-object v0, p0, $itemsField + invoke-interface { v0, p1 }, Ljava/util/List;->contains(Ljava/lang/Object;)Z + move-result v0 + if-eqz v0, :not_found + invoke-virtual { p0, p1 }, $removeItemMethod + :not_found + return v0 + """ + ) + } + ) + + // Only the constructors that call the constructor of the superclass, + // the others call those. + methods.filter { method -> MethodUtil.isConstructor(method) }.forEach { constructor -> + val superConstructorIndex = constructor.indexOfFirstInstruction { + opcode == Opcode.INVOKE_DIRECT && getReference()?.let { reference -> + reference.name == "" && reference.definingClass == superclass + } == true + } + if (superConstructorIndex < 0) return@forEach + + // The instance the superclass was initialized with. + val sectionRegister = constructor + .getInstruction(superConstructorIndex).registerC + + constructor.addInstruction( + superConstructorIndex + 1, + "invoke-static/range { v$sectionRegister .. v$sectionRegister }, $EXTENSION_CLASS->" + + "addListSection($EXTENSION_LIST_SECTION_INTERFACE)V" + ) + } + } + } } // endregion From e009d8bddb1bb94525060efc3d9b38ad97ba7d72 Mon Sep 17 00:00:00 2001 From: Kim Song Date: Mon, 26 Oct 2026 14:35:41 +0700 Subject: [PATCH 076/189] fix(YouTube - Add to queue): Fix native queue on experimental versions (#4076) --- .../queue/NativePlaylistQueuePatch.java | 70 +++++- .../nativeplaylistqueue/Fingerprints.kt | 113 ++++++++- .../NativePlaylistQueuePatch.kt | 1 + .../NativeQueueEngineHook.kt | 2 + .../NativeQueueWindowHook.kt | 130 ++++++++++ .../PlaylistNotificationHook.kt | 166 +++++++++---- .../nativeplaylistqueue/PlaylistRepeatHook.kt | 45 +++- .../PlaylistQueueApkBuild.kt | 230 +++++++++++++++++- 8 files changed, 696 insertions(+), 61 deletions(-) create mode 100644 patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/NativeQueueWindowHook.kt diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/queue/NativePlaylistQueuePatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/queue/NativePlaylistQueuePatch.java index f13eb8c5bf..2e43789682 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/queue/NativePlaylistQueuePatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/queue/NativePlaylistQueuePatch.java @@ -125,6 +125,12 @@ public interface ServiceNavigator { boolean patch_canNavigateQueue(boolean next); } + /** Newer players keep their watch-history window separately from the queue. */ + public interface DirectQueueNavigator extends ServiceNavigator { + Enum patch_getQueueNavigationIntent(Object request); + boolean patch_canNavigateNativeQueue(Object request); + } + /** Bridge to YouTube's mutable playback queue, independent of the watch view. */ public interface NativePlayback { boolean patch_isLocalQueue(); @@ -140,6 +146,7 @@ public interface RepeatHeader { Object patch_getNavigator(); Object patch_getPanel(); void patch_enableRepeat(); + void patch_setRepeatMode(int mode); void patch_setShuffleAvailable(boolean available); } @@ -245,7 +252,7 @@ private record ConvertedPlaylist(String videoId, String playlistId, WeakReferenc // A return response can precede Shorts detachment. Remember its controller // weakly, without transferring the old panel's ownership/restoration baseline. private static WeakReference shortsControllerRef = new WeakReference<>(null); - private record RowSelection(PlaylistQueue.Entry entry) {} + private record RowSelection(PlaylistQueue.Entry entry, int repeatMode) {} private static RowSelection pendingSelection; // A menu click can open the first video before a watch controller exists. @@ -809,6 +816,9 @@ private static void attachNativeQueueResponse(NativePlayback playback) { return; } if (playback.patch_attachQueue(renderer(queue), currentQueueCommand(queue))) { + // Opening an owned row can create a new native backend with repeat off. + // This is still the same local queue, so retain its pre-navigation mode. + playback.patch_restoreQueueRepeat(queue.repeatMode()); nativePlaybackRef = new WeakReference<>(playback); } } @@ -854,12 +864,20 @@ private static void syncNativeSelection(String videoId) { if (index < 0 || index >= queue.entries().size() || !queue.entries().get(index).videoId().equals(videoId)) { return; } - pendingSelection = null; - if (queue.selectedIndex() != index || queue.repeatMode() != playback.patch_getRepeatMode()) { - queue = queue.select(index).withRepeatMode(playback.patch_getRepeatMode()); + if (pendingSelection != null && pendingSelection.entry() == queue.entries().get(index)) { + playback.patch_restoreQueueRepeat(pendingSelection.repeatMode()); + pendingSelection = null; + } + if (queue.selectedIndex() != index) { + queue = queue.select(index); selectionNeedsPublishing = true; playback.patch_updateSnapshot(renderer(queue)); } + // Repeat is observed natively. It does not change the rendered row model. + // An old-video callback can run before the selected row starts. Do not + // replace its captured mode with a transient native navigation reset. + queue = queue.withRepeatMode(pendingSelection == null + ? playback.patch_getRepeatMode() : pendingSelection.repeatMode()); } /** @@ -881,7 +899,12 @@ public static void onRowClicked(RowClick click) { if (index < 0 || index >= queue.entries().size() || index == queue.selectedIndex()) { return; } - RowSelection selection = new RowSelection(queue.entries().get(index)); + NativePlayback playback = playbackFor(controller); + if (playback == null || !playback.patch_isLocalQueue()) { + return; + } + queue = queue.withRepeatMode(playback.patch_getRepeatMode()); + RowSelection selection = new RowSelection(queue.entries().get(index), queue.repeatMode()); pendingSelection = selection; Utils.runOnMainThreadDelayed(() -> { if (pendingSelection == selection) { @@ -1001,6 +1024,9 @@ private static void refreshRepeatHeaders() { if (controller != null && header.patch_getPanel() == controller.patch_getPanel()) { header.patch_enableRepeat(); NativePlayback playback = playbackFor(controller); + if (playback != null && playback.patch_isLocalQueue()) { + header.patch_setRepeatMode(playback.patch_getRepeatMode()); + } header.patch_setShuffleAvailable(playback != null && playback.patch_isLocalQueue() && queue.entries().size() > 1); } @@ -1163,6 +1189,34 @@ private static boolean ownsNativePlayback(ServiceNavigator service) { return playback != null && playback.patch_isLocalQueue() && playback == nativePlaybackRef.get(); } + /** + * Injection point. + *

+ * -1 retains the native watch-window policy for playback that we do not own. + */ + public static int nativeQueueNavigationAvailability(DirectQueueNavigator navigator, Object request) { + if (!ownsNativePlayback(navigator) || !isQueuePlaybackIntent(navigator.patch_getQueueNavigationIntent(request))) { + return -1; + } + return navigator.patch_canNavigateNativeQueue(request) ? 1 : 0; + } + + /** + * Injection point. + *

+ * Called only after the native queue has committed a non-null target descriptor. + */ + public static boolean retainCommittedQueueNavigation(DirectQueueNavigator navigator, Object request) { + return ownsNativePlayback(navigator) && isQueuePlaybackIntent(navigator.patch_getQueueNavigationIntent(request)); + } + + private static boolean isQueuePlaybackIntent(@Nullable Enum intent) { + return intent != null && switch (intent.name()) { + case "NEXT", "PREVIOUS", "AUTOPLAY", "AUTONAV" -> true; + default -> false; + }; + } + /** Bypass watch-history shortcuts, retaining the native queue's validation and ordering. */ private static boolean navigateNative(ServiceNavigator service, boolean next) { if (!ownsNativePlayback(service)) { @@ -1759,7 +1813,7 @@ private static void updateVideo(String videoId) { continue; } dragSession = null; - queue = queue.select(index); + queue = queue.select(index).withRepeatMode(pendingSelection.repeatMode()); selectionNeedsPublishing = true; pendingSelection = null; @@ -2449,6 +2503,7 @@ private static boolean publish(PlaylistController controller, PlaylistQueue cand } } + boolean restoringQueue = queue != null && !playback.patch_isLocalQueue(); if (!playback.patch_isLocalQueue() || changedItems) { if (!playback.patch_attachQueue(panel, currentQueueCommand(candidate))) { controller.patch_restorePanel(acceptedModel, previousModel); @@ -2457,6 +2512,9 @@ private static boolean publish(PlaylistController controller, PlaylistQueue cand } return false; } + if (restoringQueue) { + playback.patch_restoreQueueRepeat(candidate.repeatMode()); + } } else { playback.patch_updateSnapshot(panel); } diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/Fingerprints.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/Fingerprints.kt index 43a6688b4f..4a865ae45c 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/Fingerprints.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/Fingerprints.kt @@ -1002,11 +1002,43 @@ internal object PlaylistRepeatCapabilitiesFingerprint : Fingerprint( returnType = "V", parameters = listOf("L", "L", "I"), filters = listOf( - fieldAccess(type = "Z", opcode = Opcode.IGET_BOOLEAN), + // Tie the flags to the repeat button. Newer binders contain a similar + // boolean pair for shuffle visibility before this repeat section. + fieldAccess( + type = "Lcom/google/android/apps/youtube/app/watch/panel/ui/PlaylistLoopButtonView;", + opcode = Opcode.IGET_OBJECT + ), + fieldAccess(type = "Z", opcode = Opcode.IGET_BOOLEAN, location = MatchAfterImmediately()), opcode(Opcode.IF_NEZ, location = MatchAfterImmediately()), fieldAccess(type = "Z", opcode = Opcode.IGET_BOOLEAN, location = MatchAfterImmediately()), - opcode(Opcode.IF_EQZ, location = MatchAfterImmediately()), - methodCall("Lcom/google/android/apps/youtube/app/watch/panel/ui/PlaylistLoopButtonView;->setEnabled(Z)V") + // Both branch shapes compute the short-circuit OR of these two flags. + methodCall( + "Lcom/google/android/apps/youtube/app/watch/panel/ui/PlaylistLoopButtonView;->setEnabled(Z)V", + location = MatchAfterWithin(6) + ) + ) +) + +/** The binder restores this stored native listener when its shuffle view is shown. */ +internal fun playlistShuffleListenerFingerprint(binder: Method, shuffleView: FieldReference) = Fingerprint( + definingClass = binder.definingClass, + name = binder.name, + parameters = binder.parameterTypes.map { it.toString() }, + returnType = binder.returnType, + filters = listOf( + fieldAccess(reference = shuffleView, opcode = Opcode.IGET_OBJECT), + // Older binders first set enabled from the native shuffle capability. + methodCall("Landroid/widget/ImageView;->setVisibility(I)V", location = MatchAfterWithin(3)), + fieldAccess( + definingClass = "this", + type = "Landroid/view/View\$OnClickListener;", + opcode = Opcode.IGET_OBJECT, + location = MatchAfterImmediately() + ), + methodCall( + "Landroid/widget/ImageView;->setOnClickListener(Landroid/view/View\$OnClickListener;)V", + location = MatchAfterImmediately() + ) ) ) @@ -1085,6 +1117,80 @@ internal fun playlistServiceNavigatorFingerprint(wrapperType: String) = Fingerpr ) ) +/** The direct-backend watch navigator reports failed starts through this stable diagnostic. */ +internal object DirectQueueNavigatorFingerprint : Fingerprint( + returnType = "V", + filters = listOf(string("Watch Page unable to start playback, ")) +) + +/** The active native backend is assigned from the same factory used by the legacy sequencer. */ +internal fun directQueueBackendFingerprint(navigatorType: String, playbackType: String) = Fingerprint( + definingClass = navigatorType, + returnType = "V", + filters = listOf( + methodCall(returnType = playbackType), + opcode(Opcode.MOVE_RESULT_OBJECT, location = MatchAfterImmediately()), + fieldAccess( + definingClass = "this", + type = playbackType, + opcode = Opcode.IPUT_OBJECT, + location = MatchAfterImmediately() + ) + ) +) + +internal fun directQueuePlayerOwnerFingerprint(navigatorType: String, playerOwnerType: String) = Fingerprint( + definingClass = navigatorType, + name = "", + filters = listOf(fieldAccess(definingClass = "this", type = playerOwnerType, opcode = Opcode.IPUT_OBJECT)) +) + +internal fun directQueueNavigateFingerprint(navigatorType: String, requestType: String) = Fingerprint( + definingClass = navigatorType, + accessFlags = listOf(AccessFlags.PUBLIC, AccessFlags.FINAL), + parameters = listOf(requestType), + returnType = "V" +) + +/** The native backend's capability check, before watch-history window policy is added. */ +internal fun directQueueBackendAvailabilityFingerprint(backend: FieldReference, requestType: String) = Fingerprint( + definingClass = backend.definingClass, + accessFlags = listOf(AccessFlags.PRIVATE, AccessFlags.FINAL), + parameters = listOf(requestType), + returnType = "Z", + filters = listOf( + fieldAccess(reference = backend, opcode = Opcode.IGET_OBJECT), + methodCall( + definingClass = backend.type, + opcode = Opcode.INVOKE_VIRTUAL, + parameters = listOf(requestType), + returnType = "I", + location = MatchAfterWithin(2) + ) + ) +) + +internal fun queueNavigationIntentFieldFingerprint(requestType: String, enumType: String) = Fingerprint( + definingClass = requestType, + name = "", + parameters = listOf(enumType, "L", "L"), + filters = listOf(fieldAccess(definingClass = "this", type = enumType, opcode = Opcode.IPUT_OBJECT)) +) + +/** + * A newer watch navigator forwards queue requests to the same playback service. + * Other navigation patches can prepend guards, so match the forwarding tail. + */ +internal fun playlistServiceDelegateFingerprint(navigate: MethodReference) = Fingerprint( + parameters = navigate.parameterTypes.map { it.toString() }, + returnType = "V", + filters = listOf( + fieldAccess(definingClass = "this", type = navigate.definingClass, opcode = Opcode.IGET_OBJECT), + methodCall(reference = navigate, location = MatchAfterImmediately()), + opcode(Opcode.RETURN_VOID, location = MatchAfterImmediately()) + ) +) + /** Only the ordinary watch-page callback is replaced; casting/no-op delegates keep control. */ internal fun notificationForegroundSkipFingerprint(callbackType: String, request: FieldReference) = Fingerprint( interfaces = listOf(callbackType), @@ -1096,6 +1202,7 @@ internal fun notificationForegroundSkipFingerprint(callbackType: String, request /** The playback service checks whether a navigation request can be dispatched. */ internal fun playlistServiceCanNavigateFingerprint(serviceType: String, requestType: String) = Fingerprint( definingClass = serviceType, + accessFlags = listOf(AccessFlags.PUBLIC, AccessFlags.FINAL), returnType = "Z", parameters = listOf(requestType) ) diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/NativePlaylistQueuePatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/NativePlaylistQueuePatch.kt index 5ea7d3db8f..38239ba486 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/NativePlaylistQueuePatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/NativePlaylistQueuePatch.kt @@ -35,6 +35,7 @@ internal const val PLAYER_CONTROLS_INTERFACE = "$EXTENSION_PACKAGE/NativePlaylis internal const val MEDIA_CONTROLS_INTERFACE = "$EXTENSION_PACKAGE/NativePlaylistQueuePatch\$MediaControls;" internal const val NOTIFICATION_CONTROLS_INTERFACE = "$EXTENSION_PACKAGE/NativePlaylistQueuePatch\$NotificationControls;" internal const val SERVICE_NAVIGATOR_INTERFACE = "$EXTENSION_PACKAGE/NativePlaylistQueuePatch\$ServiceNavigator;" +internal const val DIRECT_NAVIGATOR_INTERFACE = "$EXTENSION_PACKAGE/NativePlaylistQueuePatch\$DirectQueueNavigator;" internal const val NATIVE_PLAYBACK_INTERFACE = "$EXTENSION_PACKAGE/NativePlaylistQueuePatch\$NativePlayback;" internal const val REPEAT_HEADER_INTERFACE = "$EXTENSION_PACKAGE/NativePlaylistQueuePatch\$RepeatHeader;" internal const val ROW_CLICK_INTERFACE = "$EXTENSION_PACKAGE/NativePlaylistQueuePatch\$RowClick;" diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/NativeQueueEngineHook.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/NativeQueueEngineHook.kt index 5713bd6cd9..350625ac6e 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/NativeQueueEngineHook.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/NativeQueueEngineHook.kt @@ -593,4 +593,6 @@ internal fun installNativeQueueEngine( "invoke-static {}, $EXTENSION_CLASS->onNativeQueueSelection()V" ) } + + wrapperType } diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/NativeQueueWindowHook.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/NativeQueueWindowHook.kt new file mode 100644 index 0000000000..3e3e375159 --- /dev/null +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/NativeQueueWindowHook.kt @@ -0,0 +1,130 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/4076 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.patches.youtube.interaction.nativeplaylistqueue + +import app.morphe.patcher.patch.BytecodePatchContext +import app.morphe.patcher.util.cloneParameters +import app.morphe.patcher.util.findFreeRegister +import app.morphe.patcher.util.getReference +import app.morphe.patcher.util.p0Register +import app.morphe.util.addInstructionsAtControlFlowLabel +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.OffsetInstruction +import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction +import com.android.tools.smali.dexlib2.iface.reference.FieldReference +import com.android.tools.smali.dexlib2.iface.reference.MethodReference + +/** Keep owned queue navigation independent of the newer player's watch-history window. */ +internal fun installNativeQueueWindow( + context: BytecodePatchContext, + backend: FieldReference, + enumType: String, + navigate: MethodReference, + canNavigate: MethodReference +) = with(context) { + val requestType = canNavigate.parameterTypes.single().toString() + val capability = directQueueBackendAvailabilityFingerprint(backend, requestType).originalMethod + val intent = queueNavigationIntentFieldFingerprint(requestType, enumType) + .instructionMatches.single().instruction.getReference()!! + val owner = mutableClassDefBy(backend.definingClass) + owner.interfaces.add(DIRECT_NAVIGATOR_INTERFACE) + owner.addBridgeMethod( + "patch_getQueueNavigationIntent", + listOf("Ljava/lang/Object;"), + "Ljava/lang/Enum;", + 1, + """ + check-cast p1, $requestType + iget-object v0, p1, $intent + return-object v0 + """ + ) + owner.addBridgeMethod( + "patch_canNavigateNativeQueue", + listOf("Ljava/lang/Object;"), + "Z", + 1, + """ + check-cast p1, $requestType + invoke-direct {p0, p1}, $capability + move-result v0 + return v0 + """ + ) + + // This native check retains backend restrictions (including co-watch), but + // does not require a previous/next entry in the separate history window. + val availability = playlistServiceCanNavigateFingerprint(backend.definingClass, requestType).method + val result = availability.findFreeRegister(0, availability.p0Register, availability.p0Register + 1) + availability.addInstructionsAtControlFlowLabel( + 0, + """ + invoke-static/range {p0 .. p1}, $EXTENSION_CLASS->nativeQueueNavigationAvailability(${DIRECT_NAVIGATOR_INTERFACE}Ljava/lang/Object;)I + move-result v$result + if-ltz v$result, :native_window + return v$result + + :native_window + nop + """ + ) + + // Native dispatch commits the queue target before querying availability a + // second time. That commit can move selection to the first/last item. Keep + // the already accepted descriptor on the native load path at that boundary. + // Preserve original parameters: the large dispatcher also reuses its locals. + val dispatch = directQueueNavigateFingerprint(backend.definingClass, requestType).method.cloneParameters() + check(dispatch.toString() == navigate.toString()) { + "Native playlist queue: window dispatch must be the bridged native dispatcher" + } + // Shared navigation patches can prepend guards. Inspect current instructions + // after cloning instead of reusing indexes from an earlier fingerprint match. + val code = dispatch.implementation!!.instructions.toList() + val gates = code.indices.filter { code[it].getReference()?.toString() == canNavigate.toString() } + check(gates.size == 2) { "Native playlist queue: expected pre-commit and post-commit window checks" } + val gate = gates.last() + val descriptorType = NativeQueueDescriptorFingerprint.originalMethod.returnType + val commit = (0 until gate).single { index -> + code[index].getReference()?.let { + it.definingClass == backend.type && it.parameterTypes.map { type -> type.toString() } == listOf(requestType) + && it.returnType == descriptorType + } == true + } + check(code[commit + 1].opcode == Opcode.MOVE_RESULT_OBJECT && code[gate + 1].opcode == Opcode.MOVE_RESULT && + code[gate - 3].opcode == Opcode.IF_EQZ && + code[gate - 2].getReference() == backend && + code[gate - 1].opcode == Opcode.IF_EQZ && code[gate + 2].opcode == Opcode.IF_EQZ) { + "Native playlist queue: committed target and backend must be guarded before the window recheck" + } + val descriptorGuard = code[gate - 3] as OneRegisterInstruction + val backendRead = code[gate - 2] as TwoRegisterInstruction + val backendGuard = code[gate - 1] as OneRegisterInstruction + val computed = (code[gate + 1] as OneRegisterInstruction).registerA + check((code[commit + 1] as OneRegisterInstruction).registerA == descriptorGuard.registerA && + backendRead.registerA == backendGuard.registerA && + (code[gate + 2] as OneRegisterInstruction).registerA == computed) { + "Native playlist queue: post-commit guards must inspect the native target, backend and result" + } + val addresses = code.runningFold(0) { address, instruction -> address + instruction.codeUnits } + fun target(index: Int) = addresses[index] + (code[index] as OffsetInstruction).codeOffset + check(target(gate - 3) == target(gate - 1) && target(gate - 1) == target(gate + 2)) { + "Native playlist queue: rejected targets must retain the original history fallback" + } + dispatch.addInstructionsAtControlFlowLabel( + gate + 2, + """ + if-nez v$computed, :native_target + invoke-static/range {p0 .. p1}, $EXTENSION_CLASS->retainCommittedQueueNavigation(${DIRECT_NAVIGATOR_INTERFACE}Ljava/lang/Object;)Z + move-result v$computed + + :native_target + nop + """ + ) +} diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/PlaylistNotificationHook.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/PlaylistNotificationHook.kt index a56dddcd1a..6780cf3842 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/PlaylistNotificationHook.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/PlaylistNotificationHook.kt @@ -10,6 +10,7 @@ package app.morphe.patches.youtube.interaction.nativeplaylistqueue import app.morphe.patcher.patch.BytecodePatchContext import app.morphe.patcher.util.findFreeRegister import app.morphe.patcher.util.getReference +import app.morphe.patches.youtube.misc.playservice.is_21_42_or_greater import app.morphe.util.addInstructionsAtControlFlowLabel import com.android.tools.smali.dexlib2.iface.reference.FieldReference import com.android.tools.smali.dexlib2.iface.reference.MethodReference @@ -78,62 +79,139 @@ internal fun installPlaylistNotification( "Native playlist queue: current player must belong to the playback service" } - installNativeQueueEngine(context, navigatorType, enumType) + val playbackType = installNativeQueueEngine(context, navigatorType, enumType) fun bridge(type: String, name: String, parameters: List, returnType: String, locals: Int, body: String) = mutableClassDefBy(type).addBridgeMethod(name, parameters, returnType, locals, body) + fun installVideoIdBridge(type: String, playerOwner: FieldReference) { + bridge( + type, + "patch_getVideoId", + emptyList(), + "Ljava/lang/String;", + 1, + """ + iget-object v0, p0, $playerOwner + iget-object v0, v0, ${playerFields[1]} + iget-object v0, v0, ${playerFields[2]} + if-eqz v0, :missing + # 21.28 declares the player field as Object. + check-cast v0, ${videoId.definingClass} + invoke-interface {v0}, $videoId + move-result-object v0 + return-object v0 + + :missing + const/4 v0, 0x0 + return-object v0 + """ + ) + } + mutableClassDefBy(navigatorType).interfaces.add(SERVICE_NAVIGATOR_INTERFACE) - bridge( - navigatorType, - "patch_getVideoId", - emptyList(), - "Ljava/lang/String;", - 1, - """ - iget-object v0, p0, ${playerFields[0]} - iget-object v0, v0, ${playerFields[1]} - iget-object v0, v0, ${playerFields[2]} - if-eqz v0, :missing - # 21.28 declares the player field as Object. - check-cast v0, ${videoId.definingClass} - invoke-interface {v0}, $videoId - move-result-object v0 - return-object v0 + installVideoIdBridge(navigatorType, playerFields[0]) - :missing - const/4 v0, 0x0 - return-object v0 - """ - ) + fun installNavigationBridges(type: String, navigate: MethodReference, canNavigate: MethodReference) { + for ((name, resultType, target) in listOf( + Triple("patch_navigateQueue", "V", navigate), + Triple("patch_canNavigateQueue", "Z", canNavigate) + )) { + val result = if (resultType == "Z") "move-result v0\nreturn v0" else "return-void" + bridge( + type, + name, + listOf("Z"), + resultType, + 1, + """ + if-eqz p1, :previous + sget-object v0, $nextRequest + goto :dispatch + + :previous + sget-object v0, $previousRequest + + :dispatch + invoke-virtual {p0, v0}, $target + $result + """ + ) + } + } val canNavigate = playlistServiceCanNavigateFingerprint( navigatorType, wrapperType ).originalMethod - for ((name, resultType, target) in listOf( - Triple("patch_navigateQueue", "V", service.originalMethod), - Triple("patch_canNavigateQueue", "Z", canNavigate) - )) { - val result = if (resultType == "Z") "move-result v0\nreturn v0" else "return-void" - bridge( - navigatorType, - name, - listOf("Z"), - resultType, - 1, - """ - if-eqz p1, :previous - sget-object v0, $nextRequest - goto :dispatch + installNavigationBridges(navigatorType, service.originalMethod, canNavigate) - :previous - sget-object v0, $previousRequest + // Earlier versions expose the service directly. Newer watch navigators can + // wrap it while forwarding navigation and capability checks unchanged. + // Discover it through its exact native request delegation. It is required + // on 21.42; older versions can expose the service without a wrapper. + val delegates = playlistServiceDelegateFingerprint(service.originalMethod).matchAllOrNull().orEmpty() + check(delegates.size <= 1 && (!is_21_42_or_greater || delegates.size == 1)) { + "Native playlist queue: missing or ambiguous playback service delegate" + } + delegates.forEach { match -> + val delegate = match.instructionMatches.first().instruction.getReference()!! + val delegateType = match.originalClassDef.type + mutableClassDefBy(delegateType).interfaces.add(SERVICE_NAVIGATOR_INTERFACE) + for ((name, resultType) in listOf( + "patch_getVideoId" to "Ljava/lang/String;", + "patch_getQueuePlayback" to NATIVE_PLAYBACK_INTERFACE + )) { + bridge( + delegateType, + name, + emptyList(), + resultType, + 1, + """ + iget-object v0, p0, $delegate + invoke-interface {v0}, $SERVICE_NAVIGATOR_INTERFACE->$name()$resultType + move-result-object v0 + return-object v0 + """ + ) + } + // Navigation hooks from other patches belong to the wrapper's original + // dispatch. Keep those effective instead of calling the service directly. + val delegateCanNavigate = playlistServiceCanNavigateFingerprint(delegateType, wrapperType) + .originalMethod + installNavigationBridges(delegateType, match.originalMethod, delegateCanNavigate) + } - :dispatch - invoke-virtual {p0, v0}, $target - $result - """ - ) + // This navigator owns the active backend directly, instead of reaching it + // through the legacy service's sequencer. Read its current native field; + // a global last-response cache could select an inline preview's backend. + val directType = DirectQueueNavigatorFingerprint.originalClassDef.type + val backend = directQueueBackendFingerprint(directType, playbackType) + .instructionMatches.last().instruction.getReference()!! + val playerOwner = directQueuePlayerOwnerFingerprint(directType, playerFields[0].type) + .instructionMatches.single().instruction.getReference()!! + mutableClassDefBy(directType).interfaces.add(SERVICE_NAVIGATOR_INTERFACE) + bridge( + directType, + "patch_getQueuePlayback", + emptyList(), + NATIVE_PLAYBACK_INTERFACE, + 1, + """ + iget-object v0, p0, $backend + return-object v0 + """ + ) + installVideoIdBridge(directType, playerOwner) + val directNavigate = directQueueNavigateFingerprint(directType, wrapperType).originalMethod + val directCanNavigate = playlistServiceCanNavigateFingerprint(directType, wrapperType).originalMethod + installNavigationBridges( + directType, + directNavigate, + directCanNavigate + ) + if (is_21_42_or_greater) { + installNativeQueueWindow(context, backend, enumType, directNavigate, directCanNavigate) } mutableClassDefBy(owner).interfaces.add(MEDIA_CONTROLS_INTERFACE) diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/PlaylistRepeatHook.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/PlaylistRepeatHook.kt index 03466bf0b2..0165c95198 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/PlaylistRepeatHook.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/PlaylistRepeatHook.kt @@ -11,11 +11,13 @@ import app.morphe.patcher.patch.BytecodePatchContext import app.morphe.patcher.util.cloneParameters import app.morphe.patcher.util.findFreeRegister import app.morphe.patcher.util.getReference +import app.morphe.patches.youtube.misc.playservice.is_21_20_or_greater import app.morphe.util.addInstructionsAtControlFlowLabel import app.morphe.util.findInstructionIndicesReversedOrThrow import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction import com.android.tools.smali.dexlib2.iface.reference.FieldReference +import com.android.tools.smali.dexlib2.iface.reference.MethodReference /** Keep native repeat handling; turn only the owned queue's shuffle into a list command. */ internal fun installPlaylistRepeat( @@ -25,6 +27,7 @@ internal fun installPlaylistRepeat( val binding = PlaylistRepeatBindingFingerprint.match() val headerType = binding.originalClassDef.type val header = mutableClassDefBy(headerType) + val renderState = binding.instructionMatches.last().instruction.getReference()!! val navigator = playlistHeaderNavigatorFingerprint(navigatorType) .instructionMatches.single().instruction.getReference()!! val panel = playlistHeaderPanelFingerprint(binding.originalMethod) @@ -42,11 +45,24 @@ internal fun installPlaylistRepeat( "Native playlist queue: repeat capabilities and state must share their native binder" } - val repeatAll = capabilities.instructionMatches[0].instruction.getReference()!! - val repeatOne = capabilities.instructionMatches[2].instruction.getReference()!! - check(repeatAll != repeatOne && repeatAll.definingClass == headerType && repeatOne.definingClass == headerType) { + val repeatFlags = capabilities.instructionMatches.mapNotNull { + it.instruction.getReference()?.takeIf { field -> field.type == "Z" } + } + check(repeatFlags.size == 2 && repeatFlags.distinct().size == 2 && + repeatFlags.all { it.definingClass == headerType }) { "Native playlist queue: repeat capabilities must be distinct fields on the bound header" } + val (repeatAll, repeatOne) = repeatFlags + // 21.16 installs the listener in the constructor. The supported 21.20+ + // binders instead keep a listener field so they can hide/restore the action. + val restoreShuffleListener = if (is_21_20_or_greater) { + val listener = playlistShuffleListenerFingerprint(binding.originalMethod, shuffleView) + .instructionMatches[2].instruction.getReference()!! + """ + iget-object v1, p0, $listener + invoke-virtual {v0, v1}, Landroid/widget/ImageView;->setOnClickListener(Landroid/view/View${'$'}OnClickListener;)V + """ + } else "" header.interfaces.add(REPEAT_HEADER_INTERFACE) header.addBridgeMethod( @@ -86,16 +102,33 @@ internal fun installPlaylistRepeat( """ ) + // Reuse the native selected/loop-one drawable state after an owned panel bind. + header.addBridgeMethod( + "patch_setRepeatMode", + listOf("I"), + "V", + 1, + """ + const/4 v0, 0x0 + invoke-virtual {p0, p1, v0}, $renderState + return-void + """ + ) + header.addBridgeMethod( "patch_setShuffleAvailable", listOf("Z"), "V", - 1, + 2, """ iget-object v0, p0, $shuffleView invoke-virtual {v0, p1}, Landroid/widget/ImageView;->setEnabled(Z)V - const/4 p1, 0x0 - invoke-virtual {v0, p1}, Landroid/widget/ImageView;->setSelected(Z)V + # This runs only for the owned local queue. Native binders can hide an + # unsupported shuffle action and clear its listener; restore both. + const/4 v1, 0x0 + invoke-virtual {v0, v1}, Landroid/widget/ImageView;->setVisibility(I)V + invoke-virtual {v0, v1}, Landroid/widget/ImageView;->setSelected(Z)V + $restoreShuffleListener return-void """ ) diff --git a/patches/src/test/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/PlaylistQueueApkBuild.kt b/patches/src/test/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/PlaylistQueueApkBuild.kt index e0e37cdef8..82e80338e4 100644 --- a/patches/src/test/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/PlaylistQueueApkBuild.kt +++ b/patches/src/test/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/PlaylistQueueApkBuild.kt @@ -37,9 +37,12 @@ import app.morphe.patches.youtube.layout.sharesheet.openSystemShareSheetPatch import app.morphe.patches.youtube.misc.backgroundplayback.backgroundPlaybackPatch import app.morphe.patches.youtube.misc.fix.videoactionbar.restoreOldVideoActionBarPatch import app.morphe.patches.youtube.misc.medianotification.mediaNotificationControlsPatchYouTube +import app.morphe.patches.youtube.misc.playservice.is_21_32_or_greater +import app.morphe.patches.youtube.misc.playservice.is_21_42_or_greater import app.morphe.patches.youtube.misc.spoof.spoofVideoStreamsPatch import app.morphe.patches.youtube.video.quality.rememberVideoQualityPatch import app.morphe.patches.youtube.video.series.seriesTrackerPatch +import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.Opcodes import com.android.tools.smali.dexlib2.analysis.ClassPath @@ -49,12 +52,14 @@ import com.android.tools.smali.dexlib2.analysis.MethodAnalyzer import com.android.tools.smali.dexlib2.analysis.RegisterType import com.android.tools.smali.dexlib2.dexbacked.DexBackedDexFile import com.android.tools.smali.dexlib2.iface.ClassDef +import com.android.tools.smali.dexlib2.iface.Method import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction import com.android.tools.smali.dexlib2.iface.instruction.Instruction import com.android.tools.smali.dexlib2.iface.instruction.NarrowLiteralInstruction import com.android.tools.smali.dexlib2.iface.instruction.OffsetInstruction import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction +import com.android.tools.smali.dexlib2.iface.instruction.RegisterRangeInstruction import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction import com.android.tools.smali.dexlib2.iface.reference.FieldReference import com.android.tools.smali.dexlib2.iface.reference.MethodReference @@ -108,7 +113,11 @@ fun main(arguments: Array) { verifyNativeMixTap(dexDir, mode != "unselected") } } - Patcher(PatcherConfig(input, workspace, verifier = verifier)).use { patcher -> + // Disable only when isolating a known upstream validation failure; normal + // patch application and SDK/native contract verification still run. + val validateFingerprints = System.getProperty("queueValidateFingerprints", "true").toBooleanStrict() + println("Fingerprint validation enabled: $validateFingerprints") + Patcher(PatcherConfig(input, workspace, verifier = verifier, validate = validateFingerprints)).use { patcher -> patcher += selected runBlocking { patcher().collect { result -> @@ -471,11 +480,227 @@ private fun verifyNativeRowMenuPaths(directory: File, selected: Boolean) { println("Native row menu dispatch preserves original callback and dismissal branches") } +/** A wrapper used by the watch page must expose the same backend as its native service. */ +private fun verifyNativeServiceDelegates(classes: List, selected: Boolean) { + val services = classes.filter { + SERVICE_NAVIGATOR_INTERFACE in it.interfaces && !AccessFlags.INTERFACE.isSet(it.accessFlags) + } + if (!selected) { + check(services.isEmpty()) { "Playback service bridges leaked into an unselected build" } + return + } + val service = services.single { owner -> owner.methods.single { it.name == "patch_getQueuePlayback" } + .implementation!!.instructions.any { it.opcode == Opcode.INSTANCE_OF } } + val navigate = service.methods.single { it.name == "patch_navigateQueue" } + .implementation!!.instructions.mapNotNull { (it as? ReferenceInstruction)?.reference as? MethodReference } + .single { it.definingClass == service.type } + val delegates = classes.mapNotNull { owner -> + val forwarder = owner.methods.singleOrNull { method -> + val code = method.implementation?.instructions?.toList()?.takeLast(3) ?: return@singleOrNull false + code.size == 3 && code[0].opcode == Opcode.IGET_OBJECT && + ((code[0] as ReferenceInstruction).reference as FieldReference).type == service.type && + (code[1] as? ReferenceInstruction)?.reference == navigate && code[2].opcode == Opcode.RETURN_VOID + } + forwarder?.let { owner to it } + } + for ((delegate, forwarder) in delegates) { + check(SERVICE_NAVIGATOR_INTERFACE in delegate.interfaces) { + "Native navigator wrapper cannot resolve its playback backend" + } + val fields = mutableSetOf() + for (name in listOf("patch_getVideoId", "patch_getQueuePlayback")) { + val bridge = delegate.methods.single { it.name == name } + val code = bridge.implementation!!.instructions.toList() + fields += (code.first() as ReferenceInstruction).reference as FieldReference + val call = (code[1] as ReferenceInstruction).reference as MethodReference + check(code[1].opcode == Opcode.INVOKE_INTERFACE && call.definingClass == SERVICE_NAVIGATOR_INTERFACE && + call.name == name && call.parameterTypes == bridge.parameterTypes && call.returnType == bridge.returnType) { + "Native navigator wrapper must resolve identity and backend from its service" + } + } + check(fields.size == 1 && fields.single().type == service.type) { + "Native navigator wrapper must use one playback service for identity and backend" + } + val dispatch = delegate.methods.single { it.name == "patch_navigateQueue" } + .implementation!!.instructions.mapNotNull { (it as? ReferenceInstruction)?.reference as? MethodReference } + check(dispatch.single() == forwarder) { + "Native queue navigation must retain hooks on the wrapper's original dispatch" + } + val capability = delegate.methods.single { it.name == "patch_canNavigateQueue" } + .implementation!!.instructions.mapNotNull { (it as? ReferenceInstruction)?.reference as? MethodReference } + .single() + check(capability.definingClass == delegate.type && capability.parameterTypes == forwarder.parameterTypes && + capability.returnType == "Z") { "Native queue must retain the wrapper's navigation eligibility" } + } + val playbackType = classes.single { NATIVE_PLAYBACK_INTERFACE in it.interfaces }.type + val requestType = navigate.parameterTypes.single() + val directNavigators = classes.filter { owner -> + owner.fields.any { it.type == playbackType } && owner.methods.any { + com.android.tools.smali.dexlib2.AccessFlags.PUBLIC.isSet(it.accessFlags) && + it.returnType == "V" && it.parameterTypes == listOf(requestType) + } + } + for (owner in directNavigators) { + check(SERVICE_NAVIGATOR_INTERFACE in owner.interfaces) { + "Native direct-backend navigator cannot resolve its active playback backend" + } + val getter = owner.methods.single { it.name == "patch_getQueuePlayback" } + .implementation!!.instructions.toList() + check(getter.size == 2 && getter[0].opcode == Opcode.IGET_OBJECT && + ((getter[0] as ReferenceInstruction).reference as FieldReference).type == playbackType && + getter[1].opcode == Opcode.RETURN_OBJECT) { + "Native navigator must expose its owned backend without substituting a cached response" + } + for ((name, result) in listOf("patch_navigateQueue" to "V", "patch_canNavigateQueue" to "Z")) { + val call = owner.methods.single { it.name == name }.implementation!!.instructions + .mapNotNull { (it as? ReferenceInstruction)?.reference as? MethodReference }.single() + check(call.definingClass == owner.type && call.parameterTypes == listOf(requestType) && call.returnType == result) { + "Native direct-backend navigator must retain its own navigation and eligibility paths" + } + } + val identity = owner.methods.single { it.name == "patch_getVideoId" }.implementation!!.instructions + .mapNotNull { (it as? ReferenceInstruction)?.reference as? FieldReference }.toList() + val legacyIdentity = service.methods.single { it.name == "patch_getVideoId" }.implementation!!.instructions + .mapNotNull { (it as? ReferenceInstruction)?.reference as? FieldReference }.toList() + check(identity.first().definingClass == owner.type && identity.first().type == legacyIdentity.first().type && + identity.drop(1) == legacyIdentity.drop(1)) { + "Native navigator identity must come from its own player, using the existing active-ID getter" + } + } + check(services.size == delegates.size + directNavigators.size + 1) { "Unexpected playback service bridge" } + println("Native direct-backend navigators verified: ${directNavigators.size}") + println("Native playback service and ${delegates.size} forwarding navigator(s) verified") +} + +/** A committed queue target must load even when the watch-history window cannot move. */ +private fun verifyNativeQueueWindow(classes: List, selected: Boolean) { + val owners = classes.filter { DIRECT_NAVIGATOR_INTERFACE in it.interfaces } + check(owners.size == if (selected && is_21_42_or_greater) 1 else 0) { + "Unexpected owned-queue watch-window bridge" + } + if (owners.isEmpty()) return + val owner = owners.single() + val playbackType = classes.single { NATIVE_PLAYBACK_INTERFACE in it.interfaces }.type + fun calls(method: Method) = + method.implementation!!.instructions.mapNotNull { + (it as? ReferenceInstruction)?.reference as? MethodReference + }.toList() + val capability = calls(owner.methods.single { it.name == "patch_canNavigateNativeQueue" }).single() + val nativeCapability = owner.methods.single { it.toString() == capability.toString() } + check(capability.returnType == "Z" && capability.definingClass == owner.type && + nativeCapability.implementation!!.instructions.any { + ((it as? ReferenceInstruction)?.reference as? FieldReference)?.type == playbackType + } && calls(nativeCapability).any { + it.definingClass == playbackType && it.returnType == "I" + }) { "Queue availability must retain the owned native backend's capability restrictions" } + val availability = calls(owner.methods.single { it.name == "patch_canNavigateQueue" }).single() + check(availability != capability) { "Queue capability must not recurse into the watch-window policy" } + val nativeAvailability = owner.methods.single { it.toString() == availability.toString() } + .implementation!!.instructions.toList() + val override = nativeAvailability.indexOfFirst { + ((it as? ReferenceInstruction)?.reference as? MethodReference)?.name == "nativeQueueNavigationAvailability" + } + check(override >= 0 && nativeAvailability[override + 1].opcode == Opcode.MOVE_RESULT && + nativeAvailability[override + 2].opcode == Opcode.IF_LTZ && + nativeAvailability[override + 3].opcode == Opcode.RETURN && + nativeAvailability.drop(override + 4).any { + (it as? ReferenceInstruction)?.reference?.toString() == capability.toString() + }) { "Owned capability must override history availability while other playback retains its native policy" } + val navigate = calls(owner.methods.single { it.name == "patch_navigateQueue" }).single() + val dispatch = owner.methods.single { it.toString() == navigate.toString() } + val code = dispatch.implementation!!.instructions.toList() + val gates = code.indices.filter { + (code[it] as? ReferenceInstruction)?.reference?.toString() == availability.toString() + } + check(gates.size == 2) { "Native pre-commit and post-commit checks must remain present" } + val gate = gates.last() + val retain = code.indexOfFirst { + ((it as? ReferenceInstruction)?.reference as? MethodReference)?.name == "retainCommittedQueueNavigation" + } + check(retain > gate && code[gate - 3].opcode == Opcode.IF_EQZ && + ((code[gate - 2] as? ReferenceInstruction)?.reference as? FieldReference)?.type == playbackType && + code[gate - 1].opcode == Opcode.IF_EQZ && code[gate + 1].opcode == Opcode.MOVE_RESULT && + code[retain - 1].opcode == Opcode.IF_NEZ && code[retain + 1].opcode == Opcode.MOVE_RESULT && + code[retain + 3].opcode == Opcode.IF_EQZ) { + "Only an accepted non-null queue target may bypass the post-commit history fallback" + } + val invoke = code[retain] as RegisterRangeInstruction + check(invoke.registerCount == 2 && invoke.startRegister == dispatch.implementation!!.registerCount - 2) { + "Post-commit ownership must use preserved dispatcher parameters, not reused native locals" + } + println("Native queue capabilities and accepted-target loading are independent of watch history") +} + +/** Verify repeat operands and the existing shuffle view/listener, not just an enabled flag. */ +private fun verifyNativeQueueHeader(classes: List, selected: Boolean) { + val headers = classes.filter { REPEAT_HEADER_INTERFACE in it.interfaces } + check(headers.size == if (selected) 1 else 0) { "Unexpected native queue header bridge" } + if (!selected) return + val header = headers.single() + val repeat = header.methods.single { it.name == "patch_enableRepeat" }.implementation!!.instructions.toList() + val repeatView = repeat.mapNotNull { (it as? ReferenceInstruction)?.reference as? FieldReference } + .single { it.type == "Lcom/google/android/apps/youtube/app/watch/panel/ui/PlaylistLoopButtonView;" } + val enabled = "Lcom/google/android/apps/youtube/app/watch/panel/ui/PlaylistLoopButtonView;->setEnabled(Z)V" + val binder = header.methods.single { method -> method.name != "patch_enableRepeat" && + method.implementation?.instructions?.any { (it as? ReferenceInstruction)?.reference?.toString() == enabled } == true } + .implementation!!.instructions.toList() + val call = binder.indexOfFirst { (it as? ReferenceInstruction)?.reference?.toString() == enabled } + val view = (0 until call).last { (binder[it] as? ReferenceInstruction)?.reference == repeatView } + val nativeFlags = binder.subList(view + 1, call).mapNotNull { + ((it as? ReferenceInstruction)?.reference as? FieldReference)?.takeIf { field -> field.type == "Z" } + }.toSet() + val writtenFlags = repeat.filter { it.opcode == Opcode.IPUT_BOOLEAN }.map { + (it as ReferenceInstruction).reference as FieldReference + }.toSet() + check(nativeFlags.size == 2 && writtenFlags == nativeFlags) { + "Queue repeat must update the repeat button's operands, never the shuffle/hide flags" + } + val render = header.methods.single { it.name == "patch_setRepeatMode" }.implementation!!.instructions + .mapNotNull { (it as? ReferenceInstruction)?.reference as? MethodReference }.single() + val nativeRender = header.methods.single { it.toString() == render.toString() }.implementation!!.instructions + .mapNotNull { (it as? ReferenceInstruction)?.reference as? MethodReference }.toList() + check(render.parameterTypes == listOf("I", "Z") && render.returnType == "V" && + nativeRender.any { it.definingClass == repeatView.type && it.name == "setSelected" } && + nativeRender.any { it.definingClass == repeatView.type && it.name == "refreshDrawableState" }) { + "Queue repeat rendering must retain native selected and repeat-one drawable states" + } + val shuffle = header.methods.single { it.name == "patch_setShuffleAvailable" } + .implementation!!.instructions.toList() + val visibility = shuffle.indexOfFirst { (it as? ReferenceInstruction)?.reference?.toString() == + "Landroid/widget/ImageView;->setVisibility(I)V" } + check(visibility > 0 && shuffle[visibility - 1].opcode == Opcode.CONST_4 && + (shuffle[visibility - 1] as NarrowLiteralInstruction).narrowLiteral == 0) { + "Owned queue shuffle must be visible even when the native binder hides unsupported shuffle" + } + val listenerCall = "Landroid/widget/ImageView;->setOnClickListener(Landroid/view/View\$OnClickListener;)V" + val listenerFields = header.fields.filter { it.type == "Landroid/view/View\$OnClickListener;" } + if (listenerFields.isNotEmpty()) { + val listener = shuffle.mapNotNull { (it as? ReferenceInstruction)?.reference as? FieldReference } + .single { it.type == "Landroid/view/View\$OnClickListener;" } + check(shuffle.any { (it as? ReferenceInstruction)?.reference?.toString() == listenerCall } && + binder.indices.any { index -> (binder[index] as? ReferenceInstruction)?.reference == listener && + (binder.getOrNull(index + 1) as? ReferenceInstruction)?.reference?.toString() == listenerCall }) { + "Owned queue shuffle must restore the binder's stored native listener" + } + } else { + // The oldest supported header installs its native listener once in the constructor. + check(header.methods.single { it.name == "" }.implementation!!.instructions.any { + (it as? ReferenceInstruction)?.reference?.toString() == listenerCall + } && shuffle.none { (it as? ReferenceInstruction)?.reference?.toString() == listenerCall }) { + "Owned queue shuffle must retain the constructor's native listener" + } + } + println("Native queue repeat flags and shuffle visibility/listener restoration verified") +} + /** SDK structural checks do not catch a base protobuf value stored in a concrete panel field. */ private fun verifyNativePanelAssignment(directory: File, selected: Boolean) { val dexFiles = directory.listFiles()!!.filter { it.extension == "dex" }.map { file -> file.inputStream().buffered().use { DexBackedDexFile.fromInputStream(Opcodes.getDefault(), it) } } + verifyNativeServiceDelegates(dexFiles.flatMap { it.classes }, selected) + verifyNativeQueueWindow(dexFiles.flatMap { it.classes }, selected) + verifyNativeQueueHeader(dexFiles.flatMap { it.classes }, selected) val playbackInterface = EXTENSION_CLASS.removeSuffix(";") + "\$NativePlayback;" val owners = dexFiles.flatMap { it.classes }.filter { playbackInterface in it.interfaces } check(owners.size == if (selected) 1 else 0) { "Unexpected native playback bridge inclusion" } @@ -670,7 +895,7 @@ private fun verifyNativeQueueUndo(directory: File, selected: Boolean) { } val touch = closeHandlers.single { it.returnType == "Z" && it.parameterTypes == listOf("Landroid/view/View;", "Landroid/view/MotionEvent;") } - for ((gesture, expectedCalls) in listOf(fling to 2, touch to 1)) { + for ((gesture, expectedCalls) in listOf(fling to if (is_21_32_or_greater) 1 else 2, touch to 1)) { val instructions = gesture.implementation!!.instructions.toList() val dismissCalls = instructions.indices.filter { (instructions[it] as? ReferenceInstruction)?.reference?.toString() == original.toString() @@ -683,6 +908,7 @@ private fun verifyNativeQueueUndo(directory: File, selected: Boolean) { (instructions[it] as FiveRegisterInstruction).registerCount == 0 } && dismissCalls.all { instructions[it].opcode == Opcode.INVOKE_VIRTUAL && instructions[it + 1].opcode in if (gesture.returnType == "V") setOf(Opcode.RETURN_VOID) + else if (gesture == fling && is_21_32_or_greater) setOf(Opcode.RETURN) else setOf(Opcode.GOTO, Opcode.GOTO_16, Opcode.GOTO_32) }) { "Accepted gesture dismissals must each capture Undo and keep their original continuation" } From cdaa4921eee23c99d3cbdf3f2bb1604b0e33dc83 Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Mon, 26 Oct 2026 07:42:49 +0000 Subject: [PATCH 077/189] chore: Update translations from Crowdin --- .../values-af-rZA/youtube/strings.xml | 21 +++++- .../values-am-rET/youtube/strings.xml | 1 + .../values-ar-rSA/shared-youtube/strings.xml | 6 +- .../values-ar-rSA/youtube/strings.xml | 21 +++++- .../values-as-rIN/youtube/strings.xml | 1 + .../values-az-rAZ/shared-youtube/strings.xml | 7 ++ .../values-az-rAZ/youtube/strings.xml | 45 +++++++++++- .../values-be-rBY/youtube/strings.xml | 1 + .../values-bg-rBG/youtube/strings.xml | 20 ++++- .../values-bn-rBD/youtube/strings.xml | 21 +++++- .../values-bs-rBA/youtube/strings.xml | 1 + .../values-ca-rES/youtube/strings.xml | 1 + .../values-ckb-rIR/youtube/strings.xml | 1 + .../values-cs-rCZ/youtube/strings.xml | 20 ++++- .../values-da-rDK/youtube/strings.xml | 20 ++++- .../values-de-rDE/youtube/strings.xml | 21 +++++- .../values-el-rGR/youtube/strings.xml | 21 +++++- .../values-es-rES/youtube/strings.xml | 21 +++++- .../values-et-rEE/youtube/strings.xml | 1 + .../values-eu-rES/youtube/strings.xml | 1 + .../values-fa-rIR/youtube/strings.xml | 21 +++++- .../values-fi-rFI/youtube/strings.xml | 21 +++++- .../values-fil-rPH/youtube/strings.xml | 21 +++++- .../values-fr-rFR/music/strings.xml | 4 +- .../values-fr-rFR/shared-youtube/strings.xml | 12 +-- .../values-fr-rFR/sponsorblock/strings.xml | 2 +- .../values-fr-rFR/youtube/strings.xml | 59 ++++++++++----- .../values-ga-rIE/youtube/strings.xml | 20 ++++- .../values-gl-rES/youtube/strings.xml | 1 + .../values-gu-rIN/youtube/strings.xml | 1 + .../values-hi-rIN/youtube/strings.xml | 21 +++++- .../values-hr-rHR/youtube/strings.xml | 20 ++++- .../values-hu-rHU/youtube/strings.xml | 21 +++++- .../values-hy-rAM/youtube/strings.xml | 1 + .../values-in-rID/music/strings.xml | 2 +- .../values-in-rID/youtube/strings.xml | 31 ++++++-- .../values-is-rIS/youtube/strings.xml | 21 +++++- .../values-it-rIT/youtube/strings.xml | 21 +++++- .../values-iw-rIL/youtube/strings.xml | 20 ++++- .../values-ja-rJP/youtube/strings.xml | 21 +++++- .../values-ka-rGE/youtube/strings.xml | 1 + .../values-kk-rKZ/youtube/strings.xml | 1 + .../values-km-rKH/youtube/strings.xml | 1 + .../values-kmr-rTR/youtube/strings.xml | 1 + .../values-kn-rIN/youtube/strings.xml | 1 + .../values-ko-rKR/music/strings.xml | 4 +- .../values-ko-rKR/shared-youtube/strings.xml | 10 +-- .../values-ko-rKR/youtube/strings.xml | 73 ++++++++++++------- .../values-ky-rKG/youtube/strings.xml | 1 + .../values-lo-rLA/youtube/strings.xml | 1 + .../values-lt-rLT/youtube/strings.xml | 20 ++++- .../values-lv-rLV/youtube/strings.xml | 1 + .../values-mai-rIN/youtube/strings.xml | 1 + .../values-mk-rMK/youtube/strings.xml | 1 + .../values-mn-rMN/youtube/strings.xml | 1 + .../values-mr-rIN/youtube/strings.xml | 1 + .../values-ms-rMY/youtube/strings.xml | 1 + .../values-my-rMM/youtube/strings.xml | 1 + .../values-nb-rNO/youtube/strings.xml | 21 +++++- .../values-ne-rIN/youtube/strings.xml | 1 + .../values-nl-rNL/youtube/strings.xml | 20 ++++- .../values-or-rIN/youtube/strings.xml | 1 + .../values-pa-rIN/youtube/strings.xml | 1 + .../values-pl-rPL/shared-youtube/strings.xml | 14 ++-- .../values-pl-rPL/youtube/strings.xml | 51 +++++++++---- .../values-pt-rBR/youtube/strings.xml | 21 +++++- .../values-pt-rPT/youtube/strings.xml | 20 ++++- .../values-ro-rRO/youtube/strings.xml | 21 +++++- .../values-ru-rRU/youtube/strings.xml | 23 +++++- .../values-si-rLK/music/strings.xml | 9 +++ .../values-si-rLK/youtube/strings.xml | 1 + .../values-sk-rSK/youtube/strings.xml | 20 ++++- .../values-sl-rSI/youtube/strings.xml | 20 ++++- .../values-sr-rCS/youtube/strings.xml | 20 ++++- .../values-sr-rSP/youtube/strings.xml | 21 +++++- .../values-sv-rSE/youtube/strings.xml | 20 ++++- .../values-sw-rKE/youtube/strings.xml | 1 + .../values-ta-rIN/youtube/strings.xml | 1 + .../values-te-rIN/youtube/strings.xml | 1 + .../values-th-rTH/youtube/strings.xml | 23 +++++- .../values-tr-rTR/youtube/strings.xml | 21 +++++- .../values-uk-rUA/youtube/strings.xml | 25 ++++++- .../values-ur-rIN/youtube/strings.xml | 1 + .../values-uz-rUZ/youtube/strings.xml | 1 + .../values-vi-rVN/youtube/strings.xml | 21 +++++- .../values-zh-rCN/youtube/strings.xml | 21 +++++- .../values-zh-rTW/youtube/strings.xml | 21 +++++- .../values-zu-rZA/youtube/strings.xml | 1 + 88 files changed, 1016 insertions(+), 141 deletions(-) diff --git a/patches/src/main/resources/addresources/values-af-rZA/youtube/strings.xml b/patches/src/main/resources/addresources/values-af-rZA/youtube/strings.xml index e083001707..507156c127 100644 --- a/patches/src/main/resources/addresources/values-af-rZA/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-af-rZA/youtube/strings.xml @@ -435,6 +435,17 @@ Verslae is openbaar en word nagegaan voordat 'n kanaal by 'n lys gevoeg word. Ve Tik en hou enige plek op ’n voedingsvideo open sy menu Wys Kiek later-knoop by duimborde \'n Koppeling \"Later kyk\" word op die kortprentjies van voedvideo\'s vertoon. Tik en hou dit in om \"Bewaar by afspeellys\" te open + + Laai plasingsbeelde af + \'n Aflaai-knoppie word langs die Deel-knoppie van plasinge met beelde of GIF\'s vertoon, en \'n aflaai-item word by hul spyskaart gevoeg + Laai beelde af + Laai beeld af + Laai %s beelde af… + Laai beeld af… + %1$s beelde gestoor in %2$s + Beeld gestoor in %s + Kon nie die beeld aflaai nie + Geen beeld in hierdie plasing gevind nie Voeg Ask-item by vlieëmenu Voeg \'n video-opvattingsitem by die voervaartuig-vlieëmenu @@ -882,6 +893,7 @@ Om Instellings toe te gaan, skakel die Instelling-knoppie in via Algemeen → We Deurskynend Deursigtig Opaak + Hierdie styl word moontlik nie deur jou toestel of YouTube-weergawe ondersteun nie, en kan uitlegprobleme of crashes veroorsaak Wenkbalk Kies watter komponente in die lêertooskassie vertoon moet word Hide \'Audio Track\' menu footer @@ -1109,7 +1121,9 @@ Motoruitleg "Herstel die ou speleruitleg Beperking: Indien 'Herstel ou video-aksiebalk' afgeskakel is, mag die 'Meer video's' oorvleueling nie beskikbaar wees nie" - Deaktiverings van hierdie opsie kan sommige speler-lay-outfunksies breek, soos die opmerkingafdeling of \'Meer video\'s + "Die aktivering hiervan kan sommige speler-uitlegkenmerke breek, soos die kommentaar-afdeling of 'Meer video's' wat onder die speler vertoon word. + +Die tablet-uitleg word nie meer deur YouTube ondersteun nie, en is gedeeltelik of volledig gebreek in onlangse YouTube-weergawes. Vir 'n werkende tablet-uitleg, patch YouTube 20.21.37" 21.11.48 - Herstel ou onderskrifgedrag 20.39.41 - Herstel ou stoor na snelys @@ -1377,6 +1391,11 @@ Video's word met 'n kort voorvoegsel van die gehashte video-ID aangevra, sodat d Wys \'n kennisgewing wanneer \'n nuwe verstek speel snelheid gered word Verstek speel spoed Verstek spelspoed vir Shorts + Onthou terugspeelspoed op Shorts + Pas jou handmatige Shorts-spoedkeuse toe op alle toekomstige Shorts. As dit af is, keer die spoed terug na die standaard nadat die Shorts-speler gesluit is + Wys terugspeelspoed in die Shorts-kieslys + Voeg \'n terugspeelspoed-item by die Shorts-spelerkieslys + Terugspeelspoed · %s Veranderde verstek speelkspoed na: %s Gebruik normale spoed vir musiek Speel musiekvideo\'s altyd teen 1.0x spoed diff --git a/patches/src/main/resources/addresources/values-am-rET/youtube/strings.xml b/patches/src/main/resources/addresources/values-am-rET/youtube/strings.xml index 1a02f7efd7..9079108272 100644 --- a/patches/src/main/resources/addresources/values-am-rET/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-am-rET/youtube/strings.xml @@ -57,6 +57,7 @@ Second \"item\" text" + diff --git a/patches/src/main/resources/addresources/values-ar-rSA/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-ar-rSA/shared-youtube/strings.xml index 943170dda0..e79189474e 100644 --- a/patches/src/main/resources/addresources/values-ar-rSA/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ar-rSA/shared-youtube/strings.xml @@ -151,9 +151,9 @@ Second \"item\" text" يخفي أزرار \'السابق\' و \'التالي\' في إشعار الوسائط، شاشة القفل، وعناصر تحكم سماعة الرأس تعطيل شريط التمرير يمنع سحب شريط التقدم على شاشة القفل ولوحة الإشعارات - التمرير بأزرار السابق & التالي - يغير عناصر التحكم لإشعار الوسائط وشاشة القفل والرأسية للتقديم بدلاً من تغيير مقاطع الفيديو - طول التراجع للخلف + التمرير باستخدام أزرار السابق & التالي + يغير عناصر التحكم لإشعار الوسائط، قفل الشاشة، وسماعات الرأس للتمرير بدلًا من تغيير الفيديوهات + مدة الترجيع عدد الثواني للترجيع باستخدام زر السابق مدة التقديم السريع عدد الثواني للتقديم السريع باستخدام زر التالي diff --git a/patches/src/main/resources/addresources/values-ar-rSA/youtube/strings.xml b/patches/src/main/resources/addresources/values-ar-rSA/youtube/strings.xml index 6a62eae810..a2119af3c9 100644 --- a/patches/src/main/resources/addresources/values-ar-rSA/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ar-rSA/youtube/strings.xml @@ -435,6 +435,17 @@ Second \"item\" text" يؤدي الضغط مع الاستمرار في أي مكان على فيديو في الخلاصة إلى فتح قائمته عرض زر \"المشاهدة لاحقًا\" على المصغرات يتم عرض زر المشاهدة لاحقًا على مصغرات فيديوهات الخلاصة. يؤدي الضغط مع الاستمرار عليه إلى فتح الحفظ في قوائم التشغيل + + تنزيل صور المنشورات + يظهر زر التنزيل بجانب زر المشاركة في المنشورات التي تحتوي على صور أو ملفات GIF، كما يتم إضافة عنصر تنزيل إلى القائمة الخاصة بها + تنزيل الصور + تنزيل الصورة + جارٍ تنزيل %s من الصور... + جارٍ تنزيل الصورة… + %1$s صور حُفظت في %2$s + تم حفظ الصورة في %s + تعذر تنزيل الصورة + لم يتم العثور على صورة في هذا المنشور إضافة عنصر اسأل إلى القائمة المنبثقة إضافة عنصر تلخيص الفيديو إلى قائمة الخلاصة المنبثقة @@ -886,6 +897,7 @@ Mistral Nemo هو الخيار الأرخص، بينما توفر Gemma 4 أعل شبه شفاف شفاف معتم + قد لا يكون هذا النمط مدعومًا من قبل جهازك أو إصدار YouTube، ويمكن أن يتسبب في مشكلات في التخطيط أو توقف التطبيق عن العمل شريط الأدوات اختر المكونات التي تظهر في شريط الأدوات إخفاء زر البث @@ -1113,7 +1125,9 @@ Mistral Nemo هو الخيار الأرخص، بينما توفر Gemma 4 أعل "استعادة تخطيط المشغل القديم القيود: إذا تم إيقاف تشغيل 'استعادة شريط إجراءات الفيديو القديم'، فقد لا يتوفر عنصر 'المزيد من الفيديوهات'" - قد يؤدي تمكين هذا إلى تعطيل بعض ميزات تخطيط المشغل، مثل قسم التعليقات أو \'مزيد من الفيديوهات\' المعروضة أسفل المشغل + "قد يؤدي تمكين هذا الخيار إلى تعطل بعض ميزات تخطيط المشغل، مثل قسم التعليقات أو 'المزيد من الفيديوهات' المعروضة أسفل المشغل. + +لم يعد تخطيط الجهاز اللوحي مدعومًا من قبل YouTube، وهو معطل جزئيًا أو كليًا في إصدارات YouTube الحديثة. للحصول على تخطيط جهاز لوحي يعمل، قم بتعديل الإصدار YouTube 20.21.37" 21.11.48 - استعادة سلوك الترجمة القديم 20.39.41 - استعادة الحفظ القديم إلى قائمة التشغيل @@ -1381,6 +1395,11 @@ Mistral Nemo هو الخيار الأرخص، بينما توفر Gemma 4 أعل يعرض إشعارًا عند حفظ سرعة تشغيل افتراضية جديدة سرعة التشغيل الافتراضية سرعة التشغيل الافتراضية على فيديوهات Shorts + تذكر تغييرات سرعة التشغيل على فيديوهات Shorts + يطبق اختيارك اليدوي لسرعة Shorts على جميع فيديوهات Shorts المستقبلية. إذا تم إيقاف هذا الخيار، تعود السرعة إلى الوضع الافتراضي بعد إغلاق مشغل Shorts + عرض سرعة التشغيل في قائمة Shorts + يضيف عنصر سرعة التشغيل إلى قائمة مشغل Shorts + سرعة التشغيل · %s تم تغيير سرعة التشغيل الافتراضية إلى: %s استخدام السرعة العادية للموسيقى تشغيل فيديوهات الموسيقى دائمًا بسرعة 1.0x diff --git a/patches/src/main/resources/addresources/values-as-rIN/youtube/strings.xml b/patches/src/main/resources/addresources/values-as-rIN/youtube/strings.xml index 1a02f7efd7..9079108272 100644 --- a/patches/src/main/resources/addresources/values-as-rIN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-as-rIN/youtube/strings.xml @@ -57,6 +57,7 @@ Second \"item\" text" + diff --git a/patches/src/main/resources/addresources/values-az-rAZ/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-az-rAZ/shared-youtube/strings.xml index 3b4ba30c2d..cbc0dbe000 100644 --- a/patches/src/main/resources/addresources/values-az-rAZ/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-az-rAZ/shared-youtube/strings.xml @@ -151,6 +151,13 @@ Daxil olmaq üçün aşağıda davam et-i vurun" Media bildirişlərində, kilid ekranında və qulaqlıq idarəetmələrində Əvvəlki və Növbəti düyməsini gizlədir Çevirmə cizgisin qapat Kilid ekranında və bildiriş panelində çevirmə cizgisinin sürüklənməsinə mane olur + Əvvəlki & Növbəti düymələri ilə çevirmə + Media bildirişi, kilid ekranı və qulaqlıq idarəetməsini videoları dəyişmək əvəzinə çevirmək üçün dəyişir + Geri çevirmə uzunluğu + Əvvəlki düymə ilə geri çəkmək üçün saniyə + Sürətli irəli çəkmə uzunluğu + Növbəti düyməsi ilə cəld irəliyə çəkmək üçün saniyə + Çevirmə müddəti 1 və %s saniyə arasında olmalıdır. Standartı istifadə edir QUIC protokolunu qeyri-aktiv et diff --git a/patches/src/main/resources/addresources/values-az-rAZ/youtube/strings.xml b/patches/src/main/resources/addresources/values-az-rAZ/youtube/strings.xml index 2a1e6aa13e..ece6eeb39c 100644 --- a/patches/src/main/resources/addresources/values-az-rAZ/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-az-rAZ/youtube/strings.xml @@ -435,6 +435,17 @@ Məhdudiyyətlər: Axında videonun istənilən yerinə toxunub saxlamaq onun menyusunu açır Miniatürlərdə Daha sonra izlə düyməsini göstər Daha sonra izlə düyməsi axın videolarının miniatürlərində göstərilir. Ona toxunub saxlamaq Pleylistdə saxlanı açır + + Post şəkillərini yüklə + Yükləmə düyməsi Şəkil və ya GIF olan postların Paylaş düyməsi ilə yanaşı göstərilir və yükləmə elementi menyularına əlavə edilir + Şəkilləri yüklə + Şəkli yüklə + %s şəkli yükləyir… + Şəkli yükləyir… + %1$s şəkil %2$s qovluğunda saxlanıldı + Şəkil %s qovluğunda saxlanıldı + Şəkli yükləmək olmadı + Bu postda şəkil tapılmadı Soruş elementini açılan menyuya əlavə et Axın açılan menyusuna video xülasəsi elementi əlavə edir @@ -473,7 +484,16 @@ Məhdudiyyətlər: Üçüncü tərəf yükləmə tətbiqləri ilə videoları, podkastları və ya mahnıları yüklə Oynadıcı örtüyü yükləmə düyməsini göstər Video oynadıcıda xarici yükləyicinizi açan düyməni göstərir + Toxunuşda sürətli yüklə + Fonda yükləmək üçün sorğu göstərir. Yükləyicini açmaq üçün toxunub saxlayın. YTDLnis və YTindir ilə işləyir + Yükləmələri izlənildi kimi işarələ + Xarici yükləyici ilə yüklənilən videolar YouTube baxış tarixçənizə əlavə edilir + Full HD (1080p) + Orta (192 kbps) + Sürətli yükləmə Video + Səs + Bütün seçimlər Səs üstələyici tərcümə Cihazdakı mətni nitqə çevirməni istifadə edərək video titrlərini oynatma ilə sinxronlaşdırıb ucadan oxu @@ -590,7 +610,11 @@ Məlumat: Canlı yayımlarda işləməyə bilər" Saxla və Növbə fəaliyyətlərini dəyişdir Növbə seçimləri üçün toxun, sonra Baxmağa saxlamaq üçün toxun və saxla - Varsayılan + Növbədə növbətidə oynat növü + İlkin + Yerli növbə + Morphe növbəsi + Morphe növbəsinin müştəri tərəfi Axına açılan növbə menyusu elementini əlavə et Əlavə açılan elementi əlavə edir. \"Növbəyə əlavə et\" görünmürsə, bunu aktivləşdir Növbə @@ -638,10 +662,19 @@ Məlumat: Canlı yayımlarda işləməyə bilər" Növbəni aç Növbəni pleylistdə saxla + Pleylist saxlanıldı + %s da saxlanıldı + Növbəni təmizlə + Növbə təmizləndi Növbə Növbədə sonuncu oynat Növbədə növbətidə oynat + YouTube növbəni göstərə bilmədi. + Növbə bu video üçün hazır deyil. Video oynadıcını aç və təkrar cəhd et. + Miks növbəyə əlavə edilsin, yoxsa Miksə baxılsın? + Bax Növbəyə əlavə et + Miksi Yükləyir… Canlı profildən kanalı aç Kanalın profilinə toxunduqda canlı yayımın açılmasın əngəlləyir @@ -860,6 +893,7 @@ Tənzimləmələrə daxil olmaq üçün Ümumi → Alət Cərgəsi→ Tənzimlə Yarımşəffaf Şəffaf Tutqun + Bu üslub cihazınız və ya YouTube versiyanız tərəfindən dəstəklənməyə bilər və tərtibat problemlərinə və ya çökmələrə səbəb ola bilər Alət cərgəsi Alətlər zolağında hansı elementlər görünür seç Yayımla düyməsini gizlət @@ -1087,7 +1121,9 @@ Avtomobil tərtibatı "Köhnə oynadıcı tərtibatını bərpa edir Məhdudiyyət: 'Köhnə video fəaliyyət cizgisini bərpa et' qapadılıbsa, 'Daha çox video' örtüyü mövcud olmaya bilər" - Bunun aktivləşdirilməsi oynadıcı tərtibatının bəzi xüsusiyyətlərini, məsələn, şərh bölməsini və ya oynadıcının altında göstərilən \'Daha çox video\' hissəsini sıradan çıxara bilər + "Bunu aktivləşdirmək oynadıcının aşağısında göstərilən şərhlər bölməsi və ya 'Daha çox video' kimi bəzi oynadıcı tərtibatı xüsusiyyətlərini poza bilər. + +Planşet tərtibatı artıq YouTube tərəfindən dəstəklənmir və son YouTube versiyalarında qismən və ya tamamilə pozulub. İşlək planşet tərtibatı üçün YouTube 20.21.37-ni yamaqla" 21.11.48 - Köhnə titrlərin davranışını bərpa et 20.39.41 - Köhnə pleylistdə saxlanı bərpa et @@ -1355,6 +1391,11 @@ Məhdudiyyət: 'Köhnə video fəaliyyət cizgisini bərpa et' qapadılıbsa, 'D Yeni ilkin oynatma sürəti saxlanıldıqda bildiriş göstərir İlkin oynatma sürəti Shorts-da ilkin oynatma sürəti + Shorts-da oynatma sürətini xatırla + Əllə seçdiyiniz Shorts sürətini bütün gələcək Shorts-a tətbiq edir. Qapadıldıqda, Shorts oynadıcını bağladıqdan sonra sürət standarta qayıdır + Shorts menyusunda Oynatma sürətini göstər + Shorts oynadıcı menyusuna Oynatma sürəti elementi əlavə edir + Oynatma sürəti · %s İlkin oynatma sürəti %s kimi dəyişdirildi Musiqi üçün normal sürət istifadə et Musiqi videolarını həmişə 1.0x sürətlə oynat diff --git a/patches/src/main/resources/addresources/values-be-rBY/youtube/strings.xml b/patches/src/main/resources/addresources/values-be-rBY/youtube/strings.xml index 621b482897..1dee5d4760 100644 --- a/patches/src/main/resources/addresources/values-be-rBY/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-be-rBY/youtube/strings.xml @@ -58,6 +58,7 @@ Second \"item\" text" + diff --git a/patches/src/main/resources/addresources/values-bg-rBG/youtube/strings.xml b/patches/src/main/resources/addresources/values-bg-rBG/youtube/strings.xml index 09f7314584..937215b006 100644 --- a/patches/src/main/resources/addresources/values-bg-rBG/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-bg-rBG/youtube/strings.xml @@ -435,6 +435,16 @@ Second \"item\" text" Докасването и задържането навсякъде по видео в лентата отваря неговото меню Показване на бутона \"Изглеждане по-късно\" върху миниатюрите Бутонът \"Изглеждане по-късно\" се показва върху миниатюрите на видеата в лентата. Докосването и задържането му отваря \"Записване към плейлист\" + + Изтегляне на изображения от публикации + Бутон за изтегляне се показва до бутона „Споделяне“ на публикации с изображения или GIF-ове, а елемент за изтегляне се добавя в тяхното меню + Изтегляне на изображения + Изтегляне на изображение + Изтегляне на %s изображения… + Изтегляне на изображение… + Изображението е запазено в %s + Изображението не можа да бъде изтеглено + В този пост не е намерено изображение Добавяне на елемент Ask в изплъзгащото се меню Добавяне на елемент за обобщение на видео към изплъзгащото се меню на емисията @@ -882,6 +892,7 @@ Second \"item\" text" Полупрозрачен Прозрачен Непрозрачен + Този стил може да не се поддържа от вашето устройство или версия на YouTube и може да причини проблеми с оформянето или сривове Тойбър Изберете кои компоненти да се показват в лентата с инструменти Бутон за предаване на Тв @@ -1109,7 +1120,9 @@ Second \"item\" text" "Възстановява стария вид на плейъра Ограничение: Ако „Възстановяване на старата лента за действия във видеоклипа“ е изключено, покритието „Още видеоклипове“ може да не е достъпно" - Активирането на това може да наруши някои функции на макета на плейъра, като секцията за коментари или „Още видеоклипове“, показани под плейъра + "Активирането на това може да наруши някои функции на оформянето на плейъра, като раздела с коментари или \"Още видеоклипове\", показани под плейъра. + +Таблет оформянето вече не се поддържа от YouTube и е частично или напълно счупено в последните версии на YouTube. За работещо таблет оформяне, пачнете YouTube 20.21.37" 21.11.48 - Възстановяване на старото поведение на субтитрите 20.39.41 - Възстанови старите запазвания в списъка @@ -1377,6 +1390,11 @@ Second \"item\" text" Показва известие, когато бъде запазена нова скорост на възпроизвеждане по подразбиране Надписите са показани Скорост по подразбиране за възпроизвеждане в Shorts + Запомни скоростта на възпроизвеждане в Shorts + Прилага вашия ръчен избор на скорост за Shorts към всички бъдещи Shorts. Ако е изключено, скоростта се връща към стойността по подразбиране след затваряне на плейъра на Shorts + Показвай скоростта на възпроизвеждане в менюто на Shorts + Добавя елемент за скорост на възпроизвеждане в менюто на плейъра за Shorts + Скорост на възпроизвеждане · %s Променена подразбираема скорост на възпроизвеждане на: %s Използвайте нормална скорост за музика Винаги пускайте музикални видеа на скорост 1.0x diff --git a/patches/src/main/resources/addresources/values-bn-rBD/youtube/strings.xml b/patches/src/main/resources/addresources/values-bn-rBD/youtube/strings.xml index 2ec7930c89..81444d8e5d 100644 --- a/patches/src/main/resources/addresources/values-bn-rBD/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-bn-rBD/youtube/strings.xml @@ -435,6 +435,17 @@ Second \"item\" text" ফিডের যেকোনো ভিডিওতে ট্যাপ করে ধরে রাখলে এর মেনুটি খুলে যায় থাম্বনেইলগুলিতে \'পরে দেখুন\' বাটন দেখান ফিড ভিডিওর থাম্বনেইলগুলিতে একটি \'পরে দেখুন\' বাটন দেখানো হয়। এটি ট্যাপ করে ধরে রাখলে প্লেলিস্টে সংরক্ষণ করা হয় + + পোস্টের ছবি ডাউনলোড করুন + ছবি বা GIF যুক্ত পোস্টের শেয়ার বাটনের পাশে একটি ডাউনলোড বাটন দেখানো হবে, এবং তাদের মেনুতে একটি ডাউনলোড আইটেম যোগ করা হবে + ছবিগুলো ডাউনলোড করুন + ছবি ডাউনলোড করুন + %sটি ছবি ডাউনলোড করা হচ্ছে… + ছবি ডাউনলোড করা হচ্ছে… + %1$sটি ছবি %2$s-এ সংরক্ষণ করা হয়েছে + ছবিটি %s-এ সংরক্ষণ করা হয়েছে + ছবিটি ডাউনলোড করা সম্ভব হয়নি + এই পোস্টে কোনো ছবি পাওয়া যায়নি ফ্লাইআউট মেনুতে \'Ask\' আইটেম যোগ করুন ফিড ফ্লাইআউট মেনুতে ভিডিও সামারাইজ আইটেম যোগ করা হয় @@ -882,6 +893,7 @@ OpenRouter.ai ওপেন করতে এখানে ট্যাপ কর অর্ধস্বচ্ছ স্বচ্ছ অস্বচ্ছ + এই স্টাইলটি আপনার ডিভাইস বা YouTube ভার্সন দ্বারা সমর্থিত নাও হতে পারে এবং এর ফলে লেআউটে সমস্যা হতে পারে বা ক্র্যাশ করতে পারে টুলবার টুলবারে কোন উপাদানগুলো প্রদর্শিত হবে তা নির্বাচন করুন কাস্ট বোতামটি লুকান @@ -1094,7 +1106,9 @@ OpenRouter.ai ওপেন করতে এখানে ট্যাপ কর "পুরানো প্লেয়ার লেআউট ফিরিয়ে আনুন সীমাবদ্ধতা: \"পুরানো ভিডিও অ্যাকশন বার ফিরিয়ে আনুন\" অপশনটি বন্ধ থাকলে, \"আরও ভিডিও\" ওভারলেটি পাওয়া নাও যেতে পারে" - এটি সক্রিয় করলে কিছু প্লেয়ার লেআউট বৈশিষ্ট্য যেমন কমেন্ট সেকশন বা \'More videos\' প্লেয়ারের নিচে দেখানো হয় তা ভেঙে যেতে পারে + "এটি চালু করলে প্লেয়ার লেআউটের কিছু বৈশিষ্ট্য ভেঙে যেতে পারে, যেমন কমেন্ট সেকশন বা প্লেয়ারের নিচে দেখানো 'More videos'। + +ট্যাবলেট লেআউট এখন আর YouTube দ্বারা সমর্থিত নয় এবং সাম্প্রতিক YouTube ভার্সনগুলোতে আংশিকভাবে বা সম্পূর্ণভাবে অকার্যকর। একটি কার্যকর ট্যাবলেট লেআউটের জন্য, YouTube 20.21.37 প্যাচ করুন" 21.11.48 - পুরনো ক্যাপশন আচরণ পুনরুদ্ধার করুন 20.39.41 - প্লেলিস্টে পুরোনো সেভ করার অপশন ফিরিয়ে আনুন @@ -1362,6 +1376,11 @@ OpenRouter.ai ওপেন করতে এখানে ট্যাপ কর যখন একটি নতুন ডিফল্ট প্লেব্যাক গতি সঞ্চিত হয় তখন একটি বিজ্ঞপ্তি দেখায়। প্লেব্যাকের মূল স্পিড Shorts-এ ডিফল্ট প্লেব্যাক স্পিড + Shorts-এ প্লেব্যাকের স্পিড মনে রাখুন + আপনার ম্যানুয়াল Shorts স্পিড সিলেকশন সব ভবিষ্যৎ Shorts-এর জন্য প্রয়োগ করা হবে। বন্ধ থাকলে, Shorts প্লেয়ার বন্ধ করার পর স্পিড ডিফল্ট অবস্থায় ফিরে আসবে + Shorts মেনুতে প্লেব্যাকের স্পিড দেখান + Shorts প্লেয়ার মেনুতে একটি প্লেব্যাকের স্পিড আইটেম যোগ করে + প্লেব্যাক স্পিড · %s ডিফল্ট প্লেব্যাক স্পিড পরিবর্তন করা হয়েছে: %s স্বাভাবিক গতিতে গান শুনুন মিউজিক ভিডিও সর্বদা স্বাভাবিক (১.০x) গতিতে প্লে করুন diff --git a/patches/src/main/resources/addresources/values-bs-rBA/youtube/strings.xml b/patches/src/main/resources/addresources/values-bs-rBA/youtube/strings.xml index daf4d15f6b..28b68ddfec 100644 --- a/patches/src/main/resources/addresources/values-bs-rBA/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-bs-rBA/youtube/strings.xml @@ -63,6 +63,7 @@ Second \"item\" text" + diff --git a/patches/src/main/resources/addresources/values-ca-rES/youtube/strings.xml b/patches/src/main/resources/addresources/values-ca-rES/youtube/strings.xml index a748e2213a..a4852a6954 100644 --- a/patches/src/main/resources/addresources/values-ca-rES/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ca-rES/youtube/strings.xml @@ -71,6 +71,7 @@ Second \"item\" text" + diff --git a/patches/src/main/resources/addresources/values-ckb-rIR/youtube/strings.xml b/patches/src/main/resources/addresources/values-ckb-rIR/youtube/strings.xml index cb6a438fd2..b788e62703 100644 --- a/patches/src/main/resources/addresources/values-ckb-rIR/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ckb-rIR/youtube/strings.xml @@ -156,6 +156,7 @@ Second \"item\" text" + diff --git a/patches/src/main/resources/addresources/values-cs-rCZ/youtube/strings.xml b/patches/src/main/resources/addresources/values-cs-rCZ/youtube/strings.xml index 88f113fada..5f341e0100 100644 --- a/patches/src/main/resources/addresources/values-cs-rCZ/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-cs-rCZ/youtube/strings.xml @@ -435,6 +435,16 @@ Hlášení jsou veřejná a před přidáním kanálu do seznamu jsou kontrolov Klepnutí a podržení na libovolné místo ve videu v kanálu otevře jeho nabídku Zobrazit tlačítko Později na náhledy Na náhledech videí v kanálu se zobrazuje tlačítko Později. Klepnutím a podržením se otevře Uložení do playlistu + + Stahovat obrázky z příspěvků + Vedle tlačítka Sdílet u příspěvků s obrázky nebo GIFy se zobrazí tlačítko pro stahování a do jejich menu bude přidána položka pro stahování + Stáhnout obrázky + Stáhnout obrázek + Stahování %s obrázků… + Stahování obrázku… + Obrázek byl uložen do %s + Obrázek se nepodařilo stáhnout + V tomto příspěvku nebyl nalezen žádný obrázek Přidat položku Ask do rozbalovací nabídky Přidá položku shrnutí videa do rozbalovací nabídky v kanálu @@ -880,6 +890,7 @@ Pro přístup k Nastavení povolte tlačítko Nastavení přes Obecné → Nást Poloprůhledný Průhledný Neprůhledný + Tento styl nemusí být podporován vaším zařízením nebo verzí YouTube a může způsobit problémy s rozložením nebo pády Nástrojová lišta Vyberte, které prvky se zobrazí v nástrojové liště Skrýt tlačítko odesílání @@ -1107,7 +1118,9 @@ Rozhraní pro automobily "Obnoví staré rozložení přehrávače Omezení: Pokud je vypnutá možnost 'Obnovit starou lištu akcí videa', překrytí 'Další videa' nemusí být k dispozici" - Povolení této funkce může narušit některé prvky rozložení přehrávače, jako je sekce komentářů nebo \'Více videí\' zobrazené pod přehrávačem + "Zapnutí této funkce může narušit některé prvky rozložení přehrávače, jako je sekce komentářů nebo „Další videa“ zobrazená pod přehrávačem. + +Rozložení pro tablety již YouTube nepodporuje a v novějších verzích YouTube je částečně nebo zcela nefunkční. Pro funkční rozložení pro tablety aplikujte patch na YouTube 20.21.37" 21.11.48 - Obnovit staré chování titulků 20.39.41 - Obnoví staré uložení zpět do seznamu přehrávání @@ -1375,6 +1388,11 @@ Videa jsou vyžádána krátkou předponou hashovaného ID videa, takže server Zobrazí oznámení, když je uložena nová výchozí rychlost přehrávání Výchozí rychlost přehrávání Výchozí rychlost přehrávání na Shorts + Pamatovat si rychlost přehrávání u Shorts + Aplikuje váš ruční výběr rychlosti Shorts na všechny budoucí Shorts. Pokud je tato možnost vypnutá, rychlost se po zavření přehrávače Shorts vrátí na výchozí hodnotu + Zobrazit rychlost přehrávání v menu Shorts + Přidá položku Rychlost přehrávání do menu přehrávače Shorts + Rychlost přehrávání · %s Výchozí rychlost přehrávání byla změněna na: %s Použijte normální rychlost pro hudbu Přehrávejte hudební videa vždy rychlostí 1,0x diff --git a/patches/src/main/resources/addresources/values-da-rDK/youtube/strings.xml b/patches/src/main/resources/addresources/values-da-rDK/youtube/strings.xml index 199485e7d4..6972343581 100644 --- a/patches/src/main/resources/addresources/values-da-rDK/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-da-rDK/youtube/strings.xml @@ -433,6 +433,16 @@ Rapporter er offentlige og gennemgås, før en kanal tilføjes til en liste. Fje At trykke og holde et vilkårligt sted på en feed-video åbner dens menu Vis \'Se senere\'-knap på miniaturebilleder En \"Se senere\"-knap vises på miniaturebillederne for feed-videoer. Tryk og hold for at gemme i afspilningsliste + + Download billeder fra indlæg + En download-knap vises ved siden af Del-knappen på opslag med billeder eller GIF\'er, og et download-element tilføjes til deres menu + Download billeder + Download billede + Downloader %s billeder… + Downloader billede… + Billede gemt i %s + Kunne ikke downloade billedet + Intet billede fundet i dette opslag Tilføj \"Ask\"-emne til flyout-menuen Tilføjer et emne til videofeed-sammenfatning i feedets flyout-menu @@ -878,6 +888,7 @@ For at få adgang til Indstillinger skal du aktivere indstillingsknappen via Gen Gennemsigtig Transparent Uigennemsigtig + Denne stil understøttes muligvis ikke af din enhed eller YouTube-version og kan forårsage layoutproblemer eller nedbrud Værktøjslinje Vælg, hvilke komponenter der vises i værktøjslinjen Skjul Cast-knappen @@ -1105,7 +1116,9 @@ Bil layout "Gendanner det gamle layout for afspilleren Begrænsning: Hvis 'Gendan gammel video-handlingslinje' er slået fra, er 'Flere videoer'-overlejringen muligvis ikke tilgængelig" - Aktivering af dette kan ødelægge nogle spillerlayoutfunktioner, såsom kommentarsektionen eller »More videos«, der vises nedenfor playeren + "Aktivering af dette kan ødelægge nogle af afspillerens layoutfunktioner, såsom kommentarfeltet eller \"Flere videoer\", der vises under afspilleren. + +Tablet-layout understøttes ikke længere af YouTube og er delvist eller helt ødelagt i nyere YouTube-versioner. For et fungerende tablet-layout skal YouTube 20.21.37 patches" 21.11.48 - Genopret den gamle adfærd for undertekster 20.39.41 - gendan old save til spilleliste @@ -1373,6 +1386,11 @@ Videoer anmodes ved hjælp af et kort præfiks af den hashede video-ID, så serv Viser en notifikation, når en ny standardafspilningshastighed gemmes Standard afspilningshastighed Standardafspilningshastighed på Shorts + Husk afspilningshastighed på Shorts + Anvender dit manuelle valg af Shorts-hastighed på alle fremtidige Shorts. Hvis den er slået fra, vender hastigheden tilbage til standard efter lukning af Shorts-afspilleren + Vis afspilningshastighed i Shorts-menuen + Tilføjer et element for afspilningshastighed til menuen i Shorts-afspilleren + Afspilningshastighed · %s Ændrede standard afspilningshastighed til: %s Brug normal hastighed til musik Afspil altid musikvideoer med 1,0x hastighed diff --git a/patches/src/main/resources/addresources/values-de-rDE/youtube/strings.xml b/patches/src/main/resources/addresources/values-de-rDE/youtube/strings.xml index 59c959c0d4..055cace0e6 100644 --- a/patches/src/main/resources/addresources/values-de-rDE/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-de-rDE/youtube/strings.xml @@ -436,6 +436,17 @@ Meldungen sind öffentlich und werden überprüft, bevor ein Kanal zu einer List Das Tippen und Gedrückthalten irgendwo auf einem Feed-Video öffnet dessen Menü Schaltfläche \"Später ansehen\" auf Vorschaubildern anzeigen Auf den Vorschaubildern von Feed-Videos wird eine Schaltfläche \"Später ansehen\" angezeigt. Durch Tippen und Gedrückthalten wird \"In Wiedergachelste speichern\" geöffnet + + Beitragsbilder herunterladen + Neben der Teilen-Schaltfläche von Beiträgen mit Bildern oder GIFs wird eine Download-Schaltfläche angezeigt und dem Menü wird ein Download-Punkt hinzugefügt + Bilder herunterladen + Bild herunterladen + %s Bilder werden heruntergeladen… + Bild wird heruntergeladen… + %1$s Bilder unter %2$s gespeichert + Bild unter %s gespeichert + Das Bild konnte nicht heruntergeladen werden + In diesem Beitrag wurde kein Bild gefunden Ask-Element zum Flyoutmenü hinzufügen Fügt einen Artikel zur Zusammenfassung von Videos im Feed-Flyoutmenü hinzu @@ -883,6 +894,7 @@ Um auf Einstellungen zuzugreifen, aktivieren Sie die Schaltfläche „Einstellun Transluzent Transparent Undurchsichtig + Dieser Stil wird möglicherweise von Ihrem Gerät oder Ihrer YouTube-Version nicht unterstützt und kann zu Layoutproblemen oder Abstürzen führen Leiste Wähle aus, welche Schaltflächen in der Toolbar erscheinen sollen Schaltfläche „Übertragen“ ausblenden @@ -1110,7 +1122,9 @@ Automotive-Layout "Stellt das alte Player-Layout wieder her Einschränkung: Wenn \"Alte Video-Aktionsleiste wiederherstellen\" deaktiviert ist, ist das Overlay \"Mehr Videos\" möglicherweise nicht verfügbar" - Das Aktivieren dieser Option kann einige Player-Layout-Funktionen beeinträchtigen, z. B. den Kommentarbereich oder „Weitere Videos“ unterhalb des Players + "Die Aktivierung dieser Funktion kann einige Layout-Features des Players beeinträchtigen, wie z. B. den Kommentarbereich oder die unter dem Player angezeigten \"Weiteren Videos\". + +Das Tablet-Layout wird von YouTube nicht mehr unterstützt und ist in aktuellen YouTube-Versionen teilweise oder vollständig defekt. Für ein funktionierendes Tablet-Layout patchen Sie YouTube 20.21.37" 21.11.48 - Altes Untertitelverhalten wiederherstellen 20.39.41 - Alte Shorts Spielersymbole wiederherstellen @@ -1378,6 +1392,11 @@ Videos werden über ein kurzes Präfix der gehashten Video-ID angefordert, sodas Zeigt eine Benachrichtigung an, wenn eine neue Standard-Wiedergabegeschwindigkeit gespeichert wurde Standard Wiedergabegeschwindigkeit Standard-Wiedergabegeschwindigkeit für Shorts + Wiedergabegeschwindigkeit bei Shorts merken + Wendet Ihre manuelle Auswahl der Shorts-Geschwindigkeit auf alle zukünftigen Shorts an. Wenn diese Option deaktiviert ist, kehrt die Geschwindigkeit nach dem Schließen des Shorts-Players zum Standardwert zurück + Wiedergabegeschwindigkeit im Shorts-Menü anzeigen + Fügt dem Menü des Shorts-Players einen Punkt für die Wiedergabegeschwindigkeit hinzu + Abspielgeschwindigkeit · %s Standard-Wiedergabegeschwindigkeit geändert zu: %s Verwende normale Geschwindigkeit für Musik Spiele Musikvideos immer mit 1,0-facher Geschwindigkeit ab diff --git a/patches/src/main/resources/addresources/values-el-rGR/youtube/strings.xml b/patches/src/main/resources/addresources/values-el-rGR/youtube/strings.xml index 4714015009..340469be5c 100644 --- a/patches/src/main/resources/addresources/values-el-rGR/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-el-rGR/youtube/strings.xml @@ -435,6 +435,17 @@ Second \"item\" text" Το πάτημα και το κράτημα πατημένο οπουδήποτε σε ένα βίντεο feed ανοίγει το μενού του Εμφάνιση κουμπιού προβολής αργότερα στα μικρογραφίες Ένα κουμπί προβολής αργότερα εμφανίζεται στις μικρογραφίες των βίντεο feed. Το πάτημα και το κράτημα πατημένο το ανοίγει την αποθήκευση στη λίστα αναπαραγωγής + + Λήψη εικόνων ανάρτησης + Ένα κουμπί λήψης εμφανίζεται δίπλα στο κουμπί Κοινοποίησης των αναρτήσεων με εικόνες ή GIF, και ένα στοιχείο λήψης προστίθεται στο μενού τους + Λήψη εικόνων + Λήψη εικόνας + Λήψη %s εικόνων… + Λήψη εικόνας… + %1$s εικόνες αποθηκεύτηκαν στο %2$s + Η εικόνα αποθηκεύτηκε στο %s + Δεν ήταν δυνατή η λήψη της εικόνας + Δεν βρέθηκε εικόνα σε αυτή τη δημοσίευση Προσθήκη στοιχείου Ask στο μενού flyout Προσθέτει στοιχείο σύνοψης βίντεο στο μενού flyout της ροής @@ -882,6 +893,7 @@ Second \"item\" text" Ημιδιαφανές Διαφανές Αδιαφανές + Αυτό το στυλ ενδέχεται να μην υποστηρίζεται από τη συσκευή σας ή την έκδοση του YouTube και μπορεί να προκαλέσει προβλήματα στη διάταξη ή κατάρρευση της εφαρμογής Γραμμή εργαλείων Επιλέξτε ποια στοιχεία εμφανίζονται στη γραμμή εργαλείων Κουμπί μετάδοσης @@ -1094,7 +1106,9 @@ Second \"item\" text" "Επαναφέρει την παλιά διάταξη του παίκτη Περιορισμός: Εάν η επιλογή 'Επαναφορά παλαιού γραμμής εργαλείων βίντεο' είναι απενεργοποιημένη, η επικάλυψη 'Περισσότερα βίντεο' ενδέχεται να μην είναι διαθέσιμη" - Η ενεργοποίηση αυτής της λειτουργίας ενδέχεται να διαταράξει ορισμένα χαρακτηριστικά διάταξης του player, όπως η ενότητα σχολίων ή τα «Περισσότερα βίντεο» που εμφανίζονται κάτω από τον player + "Η ενεργοποίηση αυτής της ρύθμισης ενδέχεται να καταστρέψει ορισμένα χαρακτηριστικά της διάταξης του παίκτη, όπως η ενότητα σχολίων ή τα \"Περισσότερα βίντεο\" που εμφανίζονται κάτω από τον παίκτη. + +Η διάταξη tablet δεν υποστηρίζεται πλέον από το YouTube και είναι μερικώς ή πλήρως κατεστραμμένη στις πρόσφατες εκδόσεις του YouTube. Για μια λειτουργική διάταξη tablet, εφαρμόστε το patch στο YouTube 20.21.37" 21.11.48 - Επαναφορά παλιάς συμπεριφοράς λεζάντων 20.39.41 - Επαναφορά παλιών σωζημάτων σε αναπαραγωγή @@ -1362,6 +1376,11 @@ Second \"item\" text" Εμφανίζει μια ειδοποίηση όταν αποθηκεύεται μια νέα προεπιλεγμένη ταχύτητα αναπαραγωγής Προεπιλεγμένη ταχύτητα αναπαραγωγής Ταχύτητα αναπαραγωγής ως προεπιλογή στα Shorts + Απομνημόνευση ταχύτητας αναπαραγωγής στα Shorts + Εφαρμόζει την χειροκίνητη επιλογή ταχύτητας των Shorts σε όλα τα μελλοντικά Shorts. Εάν είναι απενεργοποιημένο, η ταχύτητα επιστρέφει στην προεπιλεγμένη μετά το κλείσιμο του προγράμματος αναπαραγωγής Shorts + Εμφάνιση ταχύτητας αναπαραγωγής στο μενού των Shorts + Προσθέτει ένα στοιχείο Ταχύτητα αναπαραγωγής στο μενού του προγράμματος αναπαραγωγής Shorts + Ταχύτητα αναπαραγωγής · %s Η προεπιλεγμένη ταχύτητα αναπαραγωγής άλλαξε σε: %s Χρήση κανονικής ταχύτητας για μουσική Αναπαραγωγή μουσικών βίντεο πάντα σε ταχύτητα 1.0x diff --git a/patches/src/main/resources/addresources/values-es-rES/youtube/strings.xml b/patches/src/main/resources/addresources/values-es-rES/youtube/strings.xml index ff31c3ba80..0ee1d8877e 100644 --- a/patches/src/main/resources/addresources/values-es-rES/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-es-rES/youtube/strings.xml @@ -435,6 +435,17 @@ Los reportes son públicos y se revisan antes de añadir un canal a una lista. L Tocar y mantener pulsado en cualquier parte de un vídeo del feed abre su menú Mostrar botón \"Ver más tarde\" en las miniaturas Se muestra un botón \"Ver más tarde\" en las miniaturas de los vídeos del feed. Tocar y mantener pulsado abre Guardar en lista + + Descargar imágenes de publicaciones + Se muestra un botón de descarga junto al botón Compartir de las publicaciones con imágenes o GIFs, y se añade un elemento de descarga a su menú + Descargar imágenes + Descargar imagen + Descargando %s imágenes… + Descargando imagen… + Se han guardado %1$s imágenes en %2$s + Imagen guardada en %s + No se pudo descargar la imagen + No se encontró ninguna imagen en esta publicación Añadir elemento «Preguntar» al menú desplegable Añade un elemento de resumen de vídeo al menú desplegable del feed @@ -882,6 +893,7 @@ Para acceder a los ajustes, activa el botón de ajustes a través de General → Translúcido Transparente Opaco + Es posible que tu dispositivo o la versión de YouTube no admitan este estilo, lo que puede causar problemas de diseño o cierres inesperados Barra de herramientas Elige qué componentes aparecen en la barra de herramientas Ocultar botón Transmitir @@ -1109,7 +1121,9 @@ Diseño para automóviles "Restaura el diseño antiguo del reproductor Limitación: si la opción 'Restaurar antigua barra de acciones de vídeo' está desactivada, es posible que la superposición 'Más vídeos' no esté disponible" - Activar esto puede hacer que algunas funciones del diseño del reproductor dejen de funcionar, como la sección de comentarios o \'Más vídeos\' debajo del reproductor + "Activar esto puede afectar algunas funciones del diseño del reproductor, como la sección de comentarios o \"Más videos\" que se muestran debajo del reproductor. + +El diseño de tableta ya no es compatible con YouTube y está parcial o totalmente roto en las versiones recientes de YouTube. Para obtener un diseño de tableta funcional, aplica el parche a YouTube 20.21.37" 21.11.48 - Restaurar comportamiento antiguo de los subtítulos 20.39.41 - Restaurar guardado antiguo de lista de reproducción @@ -1377,6 +1391,11 @@ Los videos se solicitan mediante un prefijo corto del ID de video con hash, por Muestra una notificación cuando se guarda una nueva velocidad de reproducción predeterminada Velocidad predeterminada de reproducción Velocidad de reproducción predeterminada en Shorts + Recordar la velocidad de reproducción en Shorts + Aplica la velocidad de reproducción de Shorts que hayas seleccionado manualmente a todos los Shorts futuros. Si está desactivada, la velocidad vuelve al valor predeterminado tras cerrar el reproductor de Shorts. + Mostrar la velocidad de reproducción en el menú de Shorts + Añade una opción de velocidad de reproducción al menú del reproductor de Shorts + Velocidad de reproducción · %s Se ha cambiado la velocidad de reproducción predeterminada a: %s Usar velocidad normal para la música Reproduce siempre los vídeos musicales a una velocidad de 1.0x diff --git a/patches/src/main/resources/addresources/values-et-rEE/youtube/strings.xml b/patches/src/main/resources/addresources/values-et-rEE/youtube/strings.xml index 1a02f7efd7..9079108272 100644 --- a/patches/src/main/resources/addresources/values-et-rEE/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-et-rEE/youtube/strings.xml @@ -57,6 +57,7 @@ Second \"item\" text" + diff --git a/patches/src/main/resources/addresources/values-eu-rES/youtube/strings.xml b/patches/src/main/resources/addresources/values-eu-rES/youtube/strings.xml index 1a02f7efd7..9079108272 100644 --- a/patches/src/main/resources/addresources/values-eu-rES/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-eu-rES/youtube/strings.xml @@ -57,6 +57,7 @@ Second \"item\" text" + diff --git a/patches/src/main/resources/addresources/values-fa-rIR/youtube/strings.xml b/patches/src/main/resources/addresources/values-fa-rIR/youtube/strings.xml index 8d7abd5ece..04f5ca6b73 100644 --- a/patches/src/main/resources/addresources/values-fa-rIR/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-fa-rIR/youtube/strings.xml @@ -435,6 +435,17 @@ Second \"item\" text" ضربه زدن و نگه داشتن در هر جایی از یک ویدیوی فید، منوی آن را باز می‌کند نمایش دکمه \"تماشا بعداً\" روی تصاویر بندانگشتی یک دکمه \"تماشا بعداً\" روی تصاویر بندانگشتی ویدیوهای فید نمایش داده می‌شود. ضربه زدن و نگه داشتن آن گزینه ذخیره در لیست پخش را باز می‌کند + + دانلود تصاویر پست + یک دکمه دانلود در کنار دکمه اشتراک‌گذاری پست‌های دارای تصویر یا GIF نمایش داده می‌شود و یک گزینه دانلود به منوی آن‌ها اضافه می‌گردد + دانلود تصاویر + دانلود تصویر + در حال دانلود %s تصویر… + در حال دانلود تصویر… + %1$s تصویر در %2$s ذخیره شد + تصویر در %s ذخیره شد + دانلود تصویر امکان‌پذیر نبود + تصویری در این پست یافت نشد افزودن آیتم Ask به منوی پرواز افزودن آیتم خلاصه‌سازی ویدیو به منوی پرواز فید @@ -882,6 +893,7 @@ Second \"item\" text" نیمه‌شفاف شفاف کدر + این استایل ممکن است توسط دستگاه شما یا نسخه YouTube پشتیبانی نشود و می‌تواند باعث بروز مشکلاتی در چیدمان یا کرش شود نوار ابزار انتخاب کنید کدام مؤلفه‌ها در نوار ابزار نمایش داده شوند پنهان‌کردن دکمهٔ ارسال @@ -1109,7 +1121,9 @@ Second \"item\" text" "طرح‌بندی قدیمی پخش‌کننده را بازیابی می‌کند محدودیت: اگر «بازیابی نوار عملیات قدیمی ویدئو» خاموش باشد، ممکن است روی‌هم‌افتادگی «ویدئوهای بیشتر» در دسترس نباشد" - فعال‌سازی این گزینه ممکن است برخی از ویژگی‌های طرح‌بندی پخش‌کننده، مانند بخش نظرات یا \'ویدئوهای بیشتر\' که در زیر پخش‌کننده نمایش داده می‌شوند را مختل کند + "فعال کردن این گزینه ممکن است برخی از ویژگی‌های چیدمان پلیر، مانند بخش نظرات یا \"ویدیوهای بیشتر\" که در پایین پلیر نمایش داده می‌شوند را مختل کند. + +چیدمان تبلت دیگر توسط YouTube پشتیبانی نمی‌شود و در نسخه‌های اخیر YouTube به طور جزئی یا کامل خراب شده است. برای داشتن یک چیدمان تبلت کاربردی، نسخه 20.21.37 YouTube را پچ کنید" 21.11.48 — بازگردانی رفتار قبلی کپشن‌ها 20.39.41 - بازیابی ذخیره در فهرست پخش @@ -1377,6 +1391,11 @@ Second \"item\" text" وقتی سرعت پخش پیش‌فرض جدیدی ذخیره شود، اعلانی نمایش می‌دهد سرعت پخش پیش‌فرض سرعت پخش پیش‌فرض در Shorts + به‌خاطر سپردن سرعت پخش در Shorts + تنظیمات سرعت پخش دستی شما در Shorts را برای تمام Shortsهای آینده اعمال می‌کند. اگر خاموش باشد، پس از بستن پخش‌کننده Shorts، سرعت به حالت پیش‌فرض بازمی‌گردد + نمایش سرعت پخش در منوی Shorts + یک مورد سرعت پخش را به منوی پخش‌کننده Shorts اضافه می‌کند + سرعت پخش · %s سرعت پخش پیش‌فرض به این مقدار تغییر کرد: %s استفاده از سرعت معمولی برای موسیقی ویدئوهای موسیقی را همیشه با سرعت 1.0x پخش می‌کند diff --git a/patches/src/main/resources/addresources/values-fi-rFI/youtube/strings.xml b/patches/src/main/resources/addresources/values-fi-rFI/youtube/strings.xml index eab1024a12..3203cd1e04 100644 --- a/patches/src/main/resources/addresources/values-fi-rFI/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-fi-rFI/youtube/strings.xml @@ -435,6 +435,17 @@ Raportit ovat julkisia ja ne tarkistetaan ennen kuin kanava lisätään listaan. Kosketus ja pitopohjassa missä tahansa syötteiden videolla avaa sen valikon Näytä Katso myhemmin -painike pääkkeissä Katso myöhemmin -painike näkyy syötteiden videoiden pienoiskuville. Kosketus ja pitopohjassa avaa Tallenna soittoluetteloon + + Lataa julkaisun kuvat + Latauspainike näytetään kuvien tai GIF-animaatioiden sisältävien julkaisujen Jaa-painikkeen vieressä, ja latauskohta lisätään niiden valikkoon + Lataa kuvat + Lataa kuva + Ladataan %s kuvaa… + Ladataan kuvaa… + %1$s kuvaa tallennettu kohteeseen %2$s + Kuva tallennettu kohteeseen %s + Kuvaa ei voitu ladata + Tästä julkaisusta ei löydetty kuvaa Lisää \"Kysy\"-kohde ponnahdusvalikkoon Lisää videon tiivistäminen-sivunne kohteen syötteen ponnahdusvalikossa @@ -882,6 +893,7 @@ Avataksesi asetukset, ota käyttöön Asetukset-painike kohdasta Yleiset → Ty Puoliläpinäkyvä Läpinäkyvä Peittävä + Laitteesi tai YouTube-versiosi ei välttämättä tue tätä tyyliä, mikä voi aiheuttaa asetteluongelmia tai kaatumisia Työkalupalkki Valitse, mitkä komponentit näkyvät työkalupalkissa Piilota Cast-painike @@ -1109,7 +1121,9 @@ Auton asettelu "Palauttaa vanhan soittimen asettelun Rajoitus: Jos \"Palauta vanha videotoimintopalkki\" on poissa käytöstä, \"Lisää videoita\" -peittokuvio ei välttämättä ole käytettävissä" - Tämän käyttöönotto voi rikkoa joitakin toiston asettelun ominaisuuksia, kuten kommenttiosion tai ’Lisää videoita’ soittimen alapuolella + "Tämän ottaminen käyttöön voi rikkoa joitakin soittimen asettelun ominaisuuksia, kuten kommenttiosion tai soittimen alapuolella näkyvät \"Lisää videoita\" -videot. + +YouTube ei tue enää tablettiasettelua, ja se on osittain tai kokonaan rikki uusimmissa YouTube-versioissa. Toimivan tablettiasettelun saamiseksi asenna päivitys YouTube-versioon 20.21.37" 21.11.48 - Palauta vanha tekstitysten käyttäytyminen 20.39.41 - Palauta vanhat tallenteet soittimeen @@ -1377,6 +1391,11 @@ Videoita pyydetään lyhyellä etuliitteellä hajautetusta video-ID:stä, joten Näyttää ilmoituksen, kun uusi oletustoistonopeus on tallennettu Toiston oletusnopeus Shortsien oletustoistojnopeus + Muista toistonopeus Shorts-videoissa + Käyttää manuaalista Shorts-nopeuden valintaasi kaikkiin tuleviin Shorts-videoihin. Jos asetus on pois päältä, nopeus palautuu oletusarvoon Shorts-soittimen sulkemisen jälkeen + Näytä toistonopeus Shorts-valikossa + Lisää toistonopeus-valinta Shorts-soittimen valikkoon + Toistonopeus · %s Oletusarvoinen toistonopeus muutettu: %s Käytä normaalinopeutta musiikille Toista musiikkivideot aina 1.0x nopeudella diff --git a/patches/src/main/resources/addresources/values-fil-rPH/youtube/strings.xml b/patches/src/main/resources/addresources/values-fil-rPH/youtube/strings.xml index eef9277c7d..5a87eb3b2a 100644 --- a/patches/src/main/resources/addresources/values-fil-rPH/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-fil-rPH/youtube/strings.xml @@ -435,6 +435,17 @@ Pampubliko ang mga ulat at sinusuri ang mga ito bago idagdag ang isang channel s Ang pagtatakbo at paghawak ng matagal sa kahit saan sa isang feed video ay nagbubukas ng kanyang menu Ipakita ang Watch later button sa mga thumbnail Isang Watch later button ay ipinapakita sa mga thumbnail ng feed videos. Ang pag-tap at paghawak nito ay nagbubukas ng Save to playlist + + I-download ang mga imahe ng post + May lalabas na button para sa pag-download sa tabi ng button na Share ng mga post na may mga imahe o GIF, at may idaragdag na download item sa kanilang menu + I-download ang mga imahe + I-download ang imahe + Dina-download ang %s na mga imahe… + Dina-download ang imahe… + %1$s na imahe ang na-save sa %2$s + Na-save ang imahe sa %s + Hindi ma-download ang imahe + Walang nakitang imahe sa post na ito Idagdag ang Ask item sa flyout menu Idinadagdag ang video summarize item sa feed flyout menu @@ -882,6 +893,7 @@ Para i-access ang Settings, i-enable ang Settings button sa pamamagitan ng Gener Translucent Transparent Opaque + Maaaring hindi suportado ang istilong ito ng iyong device o bersyon ng YouTube, at maaaring magdulot ng mga isyu sa layout o crash Toolbar Pumili kung aling mga component ang lilitaw sa flyout menu Itago ang \'Audio Track\' menu @@ -1107,7 +1119,9 @@ Layout ng automotive "Ibinabalik ang lumang layout ng player Limitasyon: Kung ang 'Ibalik ang lumang video action bar' ay naka-off, maaaring hindi magamit ang overlay na 'Higit pang mga video'" - Ang pag-enable nito ay maaaring sirain ang ilang features ng player layout, tulad ng comment section o \'More videos\' na ipinapakita sa ibaba ng player + "Ang pag-enable nito ay maaaring makasira sa ilang feature ng layout ng player, gaya ng comment section o 'Higit pang mga video' na ipinapakita sa ibaba ng player. + +Hindi na suportado ng YouTube ang tablet layout, at bahagya o ganap na itong sira sa mga kamakailang bersyon ng YouTube. Para sa isang gumaganang tablet layout, i-patch ang YouTube 20.21.37" 21.11.48 - Ibalik ang dating pag-uugali ng mga caption 20.39.41 - Ibalik ang lumang save sa playlist @@ -1375,6 +1389,11 @@ Ang mga video ay hinihiling ng isang maikling prefix ng hashed video ID, kaya hi Nagpapakita ng notification kapag nai-save ang isang bagong default na bilis ng pag-playback Default na bilis ng pag-playback Default na bilis ng playback sa Shorts + Tandaan ang playback speed sa Shorts + Ilalapat ang iyong manwal na pagpili ng bilis ng Shorts sa lahat ng mga susunod na Shorts. Kung naka-off, babalik ang bilis sa default pagkatapos isara ang Shorts player + Ipakita ang Playback speed sa menu ng Shorts + Nagdaragdag ng item na Playback speed sa menu ng Shorts player + Bilis ng playback · %s Binago ang default playback speed sa: %s Gamitin ang normal na bilis para sa musika Laging patugtugin ang mga music video sa bilis na 1.0x diff --git a/patches/src/main/resources/addresources/values-fr-rFR/music/strings.xml b/patches/src/main/resources/addresources/values-fr-rFR/music/strings.xml index 64e49e0bb6..cf1f200a9f 100644 --- a/patches/src/main/resources/addresources/values-fr-rFR/music/strings.xml +++ b/patches/src/main/resources/addresources/values-fr-rFR/music/strings.xml @@ -75,7 +75,7 @@ Second \"item\" text" %s personne connectée %s personnes connectées Entrez ce code sur un appareil à proximité. Wi-Fi Aware fonctionne sur des réseaux locaux distincts lorsque cette fonctionnalité est prise en charge. Expire dans %s minutes - Rechercher jusqu\'à %s ? + Aller à %s ? Modifications ouvertes Modifications verrouillées Connecté via Wi-Fi Aware @@ -110,7 +110,7 @@ Second \"item\" text" Lire ce morceau du Jam Choisir où lire Déplacer l\'hôte ici - La chanson n\'est pas prête pour effectuer une recherche + La chanson n\'est pas prête pour la navigation La file d\'attente a changé ; réessayez le geste diff --git a/patches/src/main/resources/addresources/values-fr-rFR/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-fr-rFR/shared-youtube/strings.xml index 2a6f4df357..29f045b771 100644 --- a/patches/src/main/resources/addresources/values-fr-rFR/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-fr-rFR/shared-youtube/strings.xml @@ -151,13 +151,13 @@ Pour vous connecter, appuyez sur 'Continuer' ci-dessous." Masque les boutons \'Précédent\' et \'Suivant\' dans la notification multimédia, sur l\'écran de verrouillage et les commandes du casque Désactiver la barre de progression Empêche le déplacement de la barre de progression sur l\'écran de verrouillage et le panneau de notifications - Recherche avec les boutons Précédent et Suivant - Modifie les contrôles pour la notification média, l\'écran verrouillé et le casque afin de rechercher au lieu de changer les vidéos - Durée du recul - Secondes de recul avec le bouton Précédent + Naviguer avec les boutons \'Précédent\' & \'Suivant\' + Change les commandes des notifications multimédias, de l\'écran de verrouillage et du casque pour avancer ou reculer dans la vidéo au lieu de passer d\'une vidéo à l’autre + Durée du retour en arrière + Secondes de retour en arrière avec le bouton \'Précédent\' Durée de l\'avance rapide - Secondes d\'avance rapide avec le bouton Suivant - La durée de recherche doit être comprise entre 1 et %s secondes. Valeur par défaut utilisée + Secondes d\'avance rapide avec le bouton \'Suivant\' + La durée du saut doit être comprise entre 1 et %s secondes. Utilisation de la valeur par défaut Désactiver le protocole QUIC diff --git a/patches/src/main/resources/addresources/values-fr-rFR/sponsorblock/strings.xml b/patches/src/main/resources/addresources/values-fr-rFR/sponsorblock/strings.xml index 6f58be4c08..0a4b3e81ea 100644 --- a/patches/src/main/resources/addresources/values-fr-rFR/sponsorblock/strings.xml +++ b/patches/src/main/resources/addresources/values-fr-rFR/sponsorblock/strings.xml @@ -21,7 +21,7 @@ Second \"item\" text" Apparence Afficher un bouton de vote Ajoute un bouton pour voter positivement ou négativement sur les segments - Utiliser la disposition carrée + Utiliser la mise en page carrée Utilise des coins carrés pour les boutons et les commandes SponsorBlock Utiliser le bouton Passer compact Réduit la largeur du bouton \'Passer\' pour gagner de la place diff --git a/patches/src/main/resources/addresources/values-fr-rFR/youtube/strings.xml b/patches/src/main/resources/addresources/values-fr-rFR/youtube/strings.xml index 2f4125e50f..4ef9df3d65 100644 --- a/patches/src/main/resources/addresources/values-fr-rFR/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-fr-rFR/youtube/strings.xml @@ -117,7 +117,7 @@ Les changements incluent : "Limitations : • Les étagères Shorts, les pages de chaîne et les résultats de recherche peuvent toujours afficher les dates de mise en ligne -• Ne fonctionne pas avec la disposition automobile" +• Ne fonctionne pas avec la mise en page pour automobile" Masquer la miniature de la vidéo Masquer les libellés des vidéos @@ -126,7 +126,7 @@ Les changements incluent : "Limitations : • Les étagères Shorts, les pages de chaîne et les résultats de recherche peuvent toujours afficher le nombre de vues -• Ne fonctionne pas avec la disposition automobile" +• Ne fonctionne pas avec la mise en page pour automobile" Masquer les résultats web Masque la section \'Résultat du web\' des résultats de recherche Masquer la section \'Recommandations\' @@ -206,7 +206,7 @@ Les changements incluent : Masquer dans les flux Masquer dans les vidéos similaires Masquer dans les résultats de recherche - Masquer dans l\'historique + Masquer dans l\'historique de visionnage Page de chaîne Choisissez les composants qui apparaissent sur la page de la chaîne Activer le filtre de l\'onglet de chaîne @@ -435,6 +435,17 @@ Les signalements sont publics et sont examinés avant qu'une chaîne ne soit ajo Un appui long n\'importe où sur une vidéo du flux ouvre son menu Afficher le bouton \'À regarder plus tard\' sur les miniatures Un bouton \'À regarder plus tard\' est affiché sur les miniatures du flux. Un appui long dessus ouvre le menu \'Enregistrer dans une playlist\' + + Télécharger les images des posts + Un bouton de téléchargement s\'affiche à côté du bouton \'Partager\' pour les posts contenant des images ou des GIF, et un élément de téléchargement est ajoutée à leur menu + Télécharger les images + Télécharger l\'image + Téléchargement de %s images… + Téléchargement de l\'image… + %1$s images enregistrées dans %2$s + Image enregistrée dans %s + Impossible de télécharger l\'image + Aucune image trouvée dans ce post Ajouter l\'élément \'Posez une question\' au menu déroulant Ajoute un élément de résumé vidéo au menu déroulant du flux @@ -473,12 +484,12 @@ Après avoir ouvert une fois une vidéo avec le bouton 'Posez une question', la Téléchargez des vidéos, des podcasts ou des chansons avec des applications de téléchargement tierces Afficher le bouton de téléchargement du lecteur superposé Affiche un bouton dans le lecteur vidéo qui ouvre votre téléchargeur externe - Téléchargement rapide au toucher - Propose le téléchargement en arrière-plan. Appuyez longuement pour ouvrir le téléchargeur. Compatible avec YTDLnis et YTindir + Téléchargement rapide en un clic + Vous invite à télécharger en arrière-plan. Appuyez longuement pour ouvrir le téléchargeur. Fonctionne avec YTDLnis et YTindir Marquer les téléchargements comme regardés Les vidéos téléchargées avec un téléchargeur externe sont ajoutées à votre historique de visionnage YouTube Full HD (1080p) - Moyen (192 kbps) + Moyenne (192 kbps) Téléchargement rapide Vidéo Audio @@ -599,11 +610,11 @@ Info : Peut ne pas fonctionner sur les diffusions en direct" Inverser les actions \'Enregistrer\' et \'File d\'attente\' Appuyez pour afficher les options de la file d\'attente, appuyez longuement pour enregistrer dans \'À regarder plus tard - Lecture suivante dans la file d\'attente + Type de lecture suivante dans la file d\'attente Par défaut File d\'attente native File d\'attente Morphe - Client de file d\'attente Morphe + File d\'attente Morphe côté client Ajouter l\'élément file d\'attente au menu déroulant du flux Ajoute un élément supplémentaire au menu déroulant. Activez cette option si \'Ajouter à la file d’attente\' n\'est pas affiché File d\'attente @@ -652,15 +663,15 @@ Info : Peut ne pas fonctionner sur les diffusions en direct" Enregistrer la file d\'attente dans une playlist Playlist enregistrée - Enregistré dans %s + Enregistrée dans %s Vider la file d\'attente File d\'attente vidée File d\'attente - Lire le dernier de la file d\'attente + Lire en dernier dans la file d\'attente Lire ensuite dans la file d\'attente YouTube n\'a pas pu afficher la file d\'attente. La file d\'attente n\'est pas prête pour cette vidéo. Ouvrez le lecteur vidéo et réessayez. - Ajouter le Mix à la file d\'attente ou regarder le Mix ? + Ajouter le \'Mix\' à la file d\'attente ou regarder le \'Mix\' ? Regarder Ajouter à la file d\'attente Chargement du Mix…  @@ -688,10 +699,10 @@ Pour corriger cela, activez 'Falsifier les flux vidéo' et définissez 'Client p Pour corriger cela, activez 'Falsifier les flux vidéo' et définissez 'Client par défaut' en 'TV Simply' ou 'visionOS'" - Activer \'Faites glisser pour rechercher\' + Activer le déplacement par glissement Active le glissement pour rechercher au lieu de la lecture en 2× lors d\'un appui long sur le lecteur vidéo - Activer l\'appui pour rechercher + Activer la navigation par appui Appuyez simplement sur la barre de progression pour accéder directement à cette position sans la faire glisser Geste de balayage vers la gauche @@ -882,6 +893,7 @@ Pour accéder aux paramètres, activez le bouton 'Paramètres' via Général → Translucide Transparent Opaque + Ce style peut ne pas être pris en charge par votre appareil ou votre version de YouTube, et peut entraîner des problèmes de mise en page ou des plantages Barre d\'outils Choisissez les composants qui apparaissent dans la barre d\'outils Masquer le bouton \'Caster\' @@ -976,7 +988,7 @@ Pour afficher le menu 'Piste audio', changez 'Falsifier les flux vidéo' et sél Masquer dans le flux \'Abonnements\' Masquer dans la description vidéo - Masquer dans l\'historique + Masquer dans l\'historique de visionnage Lecteur Shorts Choisissez les composants qui apparaissent dans le lecteur Shorts Désactiver le double appui pour aimer @@ -1084,33 +1096,35 @@ Paramètres → Lecture (ou Lecture automatique) → Lecture automatique de la v Extra-large - Disposition de l\'appareil + Mise en page de l\'appareil Par défaut Téléphone Tablette Automobile "Les changements incluent : -Disposition tablette +Mise en page pour tablette • Les posts destinés à la communauté sont masquées • Le paramètre de lecture dans les flux n'est pas disponible • Le bouton 'Remixer' et le bouton 'Son' ne sont pas disponibles dans les Shorts • L'overlay 'Plus de vidéos' peut ne pas être disponible • L'ancienne barre d'actions vidéo non réduite est affichée -Disposition automobile +Mise en page pour automobile • Le flux est organisé par thèmes et chaînes • Le paramètre de lecture dans les flux n'est pas disponible • Le bouton 'Remixer' et le bouton 'Son' ne sont pas disponibles dans les Shorts • L'overlay 'Plus de vidéos' peut ne pas être disponible • Les Shorts s'ouvrent dans le lecteur normal • L'ancienne barre d’actions vidéo non réduite est affichée" - Activer la disposition tablette dans le lecteur + Activer la mise en page pour tablette dans le lecteur - "Restaure l'ancienne disposition du lecteur + "Restaure l'ancienne mise en page du lecteur Limitation : Si 'Restaurer l'ancienne barre d'actions vidéo' est désactivé, l'overlay 'Plus de vidéos' peut ne pas être disponible" - L\'activation de cette option peut entraîner le dysfonctionnement de certaines fonctionnalités de mise en page du lecteur, telles que la section des commentaires ou \'Plus de vidéos\' affichés sous le lecteur + "L'activation de cette option peut perturber certaines fonctionnalités de mise en page du lecteur, telles que la section des commentaires ou 'Plus de vidéos' affichée sous le lecteur. + +La mise en page pour tablette n'est plus prise en charge par YouTube et est partiellement ou totalement défectueuse dans les versions récentes de YouTube. Pour une mise en page pour tablette fonctionnelle, patchez YouTube 20.21.37" 21.11.48 – Rétabli l\'ancien comportement des sous-titres 20.39.41 - Restaure l\'ancien enregistrement dans la playlist @@ -1378,6 +1392,11 @@ Les vidéos sont recherchées à l'aide d'un court préfixe de l'identifiant vid Affiche une notification lorsqu\'une nouvelle vitesse de lecture par défaut est enregistrée Vitesse de lecture par défaut Vitesse de lecture par défaut sur Shorts + Mémoriser la vitesse de lecture des Shorts + Applique votre sélection manuelle de vitesse des Shorts à tous les futurs Shorts. Si l\'option est désactivée, la vitesse revient à la valeur par défaut après la fermeture du lecteur Shorts + Afficher \'Vitesse de lecture\' dans le menu Shorts + Ajoute un élément \'Vitesse de lecture\' au menu du lecteur Shorts + Vitesse de lecture · %s Vitesse de lecture par défaut définie sur : %s Utiliser la vitesse normale pour la musique Toujours lire les clips musicaux à une vitesse de 1,0× diff --git a/patches/src/main/resources/addresources/values-ga-rIE/youtube/strings.xml b/patches/src/main/resources/addresources/values-ga-rIE/youtube/strings.xml index b4188e4dd9..8e2665a2a5 100644 --- a/patches/src/main/resources/addresources/values-ga-rIE/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ga-rIE/youtube/strings.xml @@ -435,6 +435,16 @@ Tá tuairiscí poiblí agus déantar athbhreithniú orthu sula gcuirtear cainéa Má dhéanann tú sconadh agus coinneáil in áit ar bith ar fhíseán sa bhfotha, osclófar a roghchlár Taispeáin an cnaipe \'Féach níos déanaí\' ar mhionsamhla Tá cnaipe \'Féach níos déanaí\' le feiceáil ar mhionsamhla físeán sa fhotha. Má dhéanann tú sconna air agus é a choinneáil brúite, osclófar an rogha \'Sábháil chuig seinmliosta\' + + Íoslódáil íomhánna posta + Taispeirtear cnaipe íoslódála in aice le cnaipe Comhroinn postála a bhfuil íomhánna nó GIFa leo, agus cuirtear ítem íoslódála lenaic santáirlithe + Íomhánna a íoslódáil + Íomhá a íoslódáil + Ag íoslódáil %s íomhánna… + Ag íoslódáil íomhánna… + Sábháilte an íomhá chuig %s + Níorbh fhéidir an íomhá a íoslódáil + Níor aimsíodh aon íomhá sa phóst seo Cuir an mhír Fiafraigh leis an roghchlár aníos Cuireann sé mír achoimrithe físe leis an roghchlár aníos sa bhfotha @@ -882,6 +892,7 @@ Chun rochtain a fháil ar na Socruithe, cuir an cnaipe Socruithe ar chumas trí Tréshoilseach Trédhearcach Teimhneach + Seans go nach bhfuil an stíl seo tacaíte ag do ghlúma nó ag an leagan YouTube, agus seans go gcuirfeadh sé cúiseanna eaclaise le leआउट nó go gcuirfeadh sé an fear leisure as gnó Barra Uirlisí Roghnaigh cé na comhpháirteanna a thaispeántar sa bharra uirlisí Folaigh an cnaipe Cast @@ -1109,7 +1120,9 @@ Leagan amach gluaisteán "Athchóiríonn sé leagan amach an tsean-imreoir Teorainn: Mura bhfuil 'Barra gníomhaíochta seanfhíseáin athchóirithe' múchta, b'fhéidir nach mbeidh forleagan 'Tuilleadh físeáin' ar fáil" - Má chuirtear an ghné seo ar siúl, d\'fhéadfadh sé roinnt gnéithe den leagan amach atá ag an seinnteoir a bhriseadh, mar shampla rannóg na dtráchtanna nó \'Tuilleadh físeán\' a thaispeántar thíos faoin seinnteoir + "D'fhéadfadh an activating seo roinnt gnéithe leआउट an pleigheara a bhriseadh, mar shampla an rann commenting nó 'Tuilleadh físeán' a thaispeántar faoin pleighear. + +Níl tacaíocht agat a thuilleadh ag YouTube le leआउट tábléid, agus tá sé briste go páirteach nó go hiomlán ar leaganacha YouTube nua. Chun leआउट tábléid a fháil, patch YouTube 20.21.37" 21.11.48 - Athchóirigh iompar fotheidil d\'aois 20.39.41 - Athchóirigh an sean-shábháil chuig an seinmliosta @@ -1377,6 +1390,11 @@ Iarrtar físeáin le réimír ghearr den aitheantóir físeáin hasáilte, mar s Taispeánann sé fógra nuair a shábháiltear luas athsheinm réamhshocraithe nua Luas athsheinm réamhshocraithe Luas athsheinm réamhshocraithe ar Shorts + Cuimhnigh luas athsheinm ar Shorts + Cuireann sé do rogha luas Shorts láimhe i bhfeidhm ar gach Shorts sa todhchaí. Má tharlaíonn sé go bhfuil sé múchta, filleann an luas ar an réamhshocradh tar éis an tíosaithe Shorts a dhúnadh + Taispeáin luas athsheinm sa roghshaothar Shorts + Cuireann íomhá Luas athsheinm le menú imleactóra Shorts + Luas athsheinm · %s Athraíodh an luas athsheinm réamhshocraithe go: %s Úsáid gnáthluas don cheol Seinn físeáin cheoil i gcónaí ag luas 1.0x diff --git a/patches/src/main/resources/addresources/values-gl-rES/youtube/strings.xml b/patches/src/main/resources/addresources/values-gl-rES/youtube/strings.xml index 7557b6d645..6caf5d68af 100644 --- a/patches/src/main/resources/addresources/values-gl-rES/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-gl-rES/youtube/strings.xml @@ -171,6 +171,7 @@ Os cambios inclúen: + diff --git a/patches/src/main/resources/addresources/values-gu-rIN/youtube/strings.xml b/patches/src/main/resources/addresources/values-gu-rIN/youtube/strings.xml index bfc9a4cf93..d2e7fd509a 100644 --- a/patches/src/main/resources/addresources/values-gu-rIN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-gu-rIN/youtube/strings.xml @@ -57,6 +57,7 @@ Second \"item\" text" + diff --git a/patches/src/main/resources/addresources/values-hi-rIN/youtube/strings.xml b/patches/src/main/resources/addresources/values-hi-rIN/youtube/strings.xml index 76b53f8189..d455149235 100644 --- a/patches/src/main/resources/addresources/values-hi-rIN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-hi-rIN/youtube/strings.xml @@ -435,6 +435,17 @@ Second \"item\" text" फीड वीडियो पर कहीं भी टैप करके और दबाकर उसका मेनू खोला जा सकता है थंबनेल पर \'फिर से देखें\' बटन दिखाएं फीड वीडियो की थंबनेल पर \'फिर से देखें\' बटन दिखाया जाता है। इसे टैप करके और दबाकर प्लेलिस्ट में सहेजें खोला जाता है + + पोस्ट इमेज डाउनलोड करें + इमेज या GIF वाली पोस्ट के शेयर बटन के बगल में एक डाउनलोड बटन दिखाया जाता है, और उनके मेनू में एक डाउनलोड आइटम जोड़ा जाता है + इमेज डाउनलोड करें + इमेज डाउनलोड करें + %s इमेज डाउनलोड हो रही हैं… + छवि डाउनलोड हो रही है… + %1$s इमेज %2$s में सेव की गईं + छवि %s में सहेजी गई + छवि डाउनलोड नहीं की जा सकी + इस पोस्ट में कोई छवि नहीं मिली Ask आइटम को फ्लाउट मेनू में जोड़ें फीड फ्लाउट मेनू में वीडियो सारांश आइटम जोड़ता है @@ -882,6 +893,7 @@ OpenRouter.ai खोलने के लिए यहां टैप करे अर्धपारदर्शी पारदर्शी अपारदर्शी + हो सकता है कि यह स्टाइल आपके डिवाइस या YouTube वर्ज़न द्वारा समर्थित न हो, और इससे लेआउट संबंधी समस्याएं या क्रैश हो सकते हैं टूलबार टूलबार में कौन से घटक दिखाई दें, यह चुनें \'ऑडियो ट्रैक\' मेनू फ़ूटर छुपाएं @@ -1109,7 +1121,9 @@ OpenRouter.ai खोलने के लिए यहां टैप करे "पुराने प्लेयर लेआउट को पुनर्स्थापित करता है सीमा: यदि 'पुराने वीडियो एक्शन बार को पुनर्स्थापित करें' बंद कर दिया गया है, तो 'अधिक वीडियो' ओवरले उपलब्ध नहीं हो सकता है" - इसे सक्षम करने से कुछ प्लेयर लेआउट सुविधाएं, जैसे कि टिप्पणी अनुभाग या प्लेयर के नीचे दिखाए गए \'और वीडियो\' बिगड़ सकते हैं + "इसे सक्षम करने से प्लेयर लेआउट की कुछ विशेषताएं खराब हो सकती हैं, जैसे कि कमेंट सेक्शन या प्लेयर के नीचे दिखाए गए 'More videos'। + +टैबलेट लेआउट अब YouTube द्वारा समर्थित नहीं है, और हाल के YouTube वर्ज़न में आंशिक या पूरी तरह से खराब है। एक वर्किंग टैबलेट लेआउट के लिए, YouTube 20.21.37 को पैच करें" 21.11.48 - पुराने कैप्शन व्यवहार को पुनर्स्थापित करें 20.39.41 - पुराने सेव को प्लेलिस्ट में पुन: स्थापित करें @@ -1377,6 +1391,11 @@ OpenRouter.ai खोलने के लिए यहां टैप करे जब एक नई डिफ़ॉल्ट प्लेबैक गति सहेजी जाती है तो एक सूचना दिखाता है डिफ़ॉल्ट प्लेबैक स्पीड शॉर्ट्स पर डिफ़ॉल्ट प्लेबैक गति + Shorts पर प्लेबैक स्पीड याद रखें + यह आपके द्वारा मैन्युअल रूप से चुने गए Shorts की स्पीड को भविष्य के सभी Shorts पर लागू करता है। यदि यह बंद है, तो Shorts प्लेयर बंद करने के बाद स्पीड वापस डिफ़ॉल्ट हो जाएगी + Shorts मेनू में प्लेबैक स्पीड दिखाएं + Shorts प्लेयर मेनू में प्लेबैक स्पीड आइटम जोड़ता है + प्लेबैक गति · %s डिफ़ॉल्ट प्लेबैक गति बदलकर: %s संगीत के लिए सामान्य गति का उपयोग करें संगीत वीडियो हमेशा 1.0x की गति से चलाएं diff --git a/patches/src/main/resources/addresources/values-hr-rHR/youtube/strings.xml b/patches/src/main/resources/addresources/values-hr-rHR/youtube/strings.xml index 5be5a7175a..690a5ecd56 100644 --- a/patches/src/main/resources/addresources/values-hr-rHR/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-hr-rHR/youtube/strings.xml @@ -425,6 +425,16 @@ Prijave su javne i pregledavaju se prije nego što se kanal doda na popis. Uklan Dodirivanjem i pritiskom bilo gdje na videu u pretoku otvara se njegov izbornik Prikaži gledaj kasnije tipku na minijaturama Tipka gledaj kasnije prikazuje se na minijaturama videa u pretoku. Dodirivanje i pritiskanje otvara Spremi na popis + + Preuzmi slike objave + Gumb za preuzimanje prikazuje se pokraj gumba Podijeli na objavama s slikama ili GIF-ovima, a stavka za preuzimanje dodana je u njihov izbornik + Preuzmi slike + Preuzmi sliku + Preuzimanje %s slika… + Preuzimanje slike… + Slika sačuvana u %s + Slika nije mogla biti preuzeta + U ovoj objavi nije pronađena slika Dodaj stavku Aska u izbornik flyout Dodaje stavku za sažetak videa u izbornik flyout naloga @@ -870,6 +880,7 @@ Da biste to popravili, uključite \"Lažiranje video streamova\" i promijenite \ Poluprozračno Prozirno Neprozirno + Ovaj stil možda nije podržan na vašem uređaju ili verziji YouTubea, što može uzrokovati probleme s izgledom ili rušenje aplikacije Traka Odaberite koje komponente se pojavljuju u traci s alatima Sakrij tipku za emitiranje @@ -1097,7 +1108,9 @@ Izgled za automobil "Vraća stari izgled playera Ograničenje: Ako je 'Vrati staru traku s radnjama za videozapise' isključeno, overlay 'Više videozapisa' možda neće biti dostupan" - Omogućivanje ove značajke može uzrokovati kvarove nekih značajki rasporeda playera, poput odjeljka s komentarima ili „Više videozapisa” prikazanih ispod playera + "Omogućavanjem ovoga može doći do kvara određenih značajki izgleda playera, kao što su odjeljak s komentarima ili \"Više videa\" prikazani ispod playera. + +Tablet izgled više nije podržan od strane YouTubea te je djelomično ili potpuno neispravan u novijim verzijama YouTubea. Za funkcionalan tablet izgled, zakrpajte YouTube 20.21.37" 21.11.48 - Vrati staro ponašanje titlova 20.39.41 - Vrati stari spremi u playlistu @@ -1365,6 +1378,11 @@ Videozapisi se zahtijevaju kratkim prefiksom hashiranog ID-a videozapisa, pa pos Prikazuje obavijest kada se spremi nova zadana brzina reprodukcije Standardna brzina pregleda Zadana brzina reprodukcije na Shortsima + Zapamti brzinu reprodukcije na Shorts videima + Primjenjuje vaš ručni odabir brzine Shorts videa na sve buduće Shorts videe. Ako je isključeno, brzina se vraća na zadanu nakon zatvaranja Shorts playera + Prikaži brzinu reprodukcije u Shorts izborniku + Dodaje stavku Brzina reprodukcije u izbornik Shorts playera + Brzina reprodukcije · %s Zadana brzina reprodukcije promijenjena u: %s Koristi normalnu brzinu za glazbu Uvijek reproduciraj glazbene videozapise brzinom 1.0x diff --git a/patches/src/main/resources/addresources/values-hu-rHU/youtube/strings.xml b/patches/src/main/resources/addresources/values-hu-rHU/youtube/strings.xml index cc171afe1e..bd9b3b1a8e 100644 --- a/patches/src/main/resources/addresources/values-hu-rHU/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-hu-rHU/youtube/strings.xml @@ -435,6 +435,17 @@ A jelentések nyilvánosak, és felülvizsgálják őket, mielőtt egy csatorna A posztfolyamat videóján bárhol koppintás és hosszú nyomás nyitja meg annak menüt Mutassa a Néssebb később gombot a képkockákon A Néssebb később gomb megjelenik a posztfolyamati videók képkockáján. Koppintás és hosszú nyomása esetén Megnyitás listában lehetőséget nyújt + + Bejegyzési képek letöltése + A képekkel vagy GIF-ekkel rendelkező bejegyzések Megosztás gombja mellett megjelenik egy letöltés gomb, és a menüjükhez egy letöltési elem kerül hozzá + Képek letöltése + Kép letöltése + %s kép letöltése… + Kép letöltése… + %1$s kép mentve a %2$s mappába + Kép mentve a %s mappába + A kép letöltése nem sikerült + Ebben a bejegyzésben nem található kép Ask elem hozzáadása a kizúgó menühez Video-összefoglaló elem hozzáadása az adatfolyam kizúgó menüjéhez @@ -882,6 +893,7 @@ A Beállítások eléréséhez engedélyezze a Beállítások gombot az Általá Félt पारátlan Átlátszó Átlátszatlan + Ezt a stílustt nem támogatja az eszközöd vagy a YouTube verziójuk, ami elrendezési hibákhoz vagy összeomláshoz vezethet Eszközevek sorozata Válassza ki, mely komponensek jelenjenek meg az eszköztáron Átküldés gomb elrejtése @@ -1109,7 +1121,9 @@ Autós elrendezés "A régi lejátszó elrendezés visszaállítása Korlátozás: Ha a „Régi videós eszköztár visszaállítása” ki van kapcsolva, a „További videók” réteg nem jelenhet meg." - Az engedélyezés felbonthat néhány lejátszó-elrendezési funkciót, például a megjegyzés-szekciót vagy a \'További videók\' opciót, amelyek a lejátszó alatt jelennek meg + "Ennek engedélyezéset megsértheti a lejátszó elrendezésének bizonyos funkcióit, például a komment szekciót vagy a lejátszó alatt megjelenő „További videók” részt. + +A tabletes elrendezést a YouTube már nem támogatja, és a legújabb YouTube verziókban részben vagy teljesen hibás. A működő tabletes elrendezésért patchold a YouTube 20.21.37 verzióját" 21.11.48 - A régi felirat-viselkedés visszaállítása 20.39.41 - Visszaállítjuk az régi mentést a lejátslóba @@ -1377,6 +1391,11 @@ A videókat a hash-elt videóazonosító rövid előtagjával kérik le, így a Értesítést jelenít meg, amikor egy új alapértelmezett lejátszási sebesség kerül mentésre Alapértelmezett lejátszási sebesség Alapértelmezett lejátszási sebesség Shorts-on + Lejátszási sebesség megjegyzése a Shorts videóknál + A kézi Shorts sebességválasztást minden későbbi Shorts videóra alkalmazza. Ha ki van kapcsolva, a sebesség alapértelmezettre állítódik a Shorts lejátszó bezárása után + Lejátszási sebesség megjelenítése a Shorts menüben + Lejátszási sebesség lehetőséget ad a Shorts lejátsító menühöz + Lejátszási sebesség · %s A lejátszási sebesség alapértelmezett értékre módosítva: %s Használj normál sebességet a zenéhez A zenei videók mindig 1,0-s sebességgel legyenek lejátszva diff --git a/patches/src/main/resources/addresources/values-hy-rAM/youtube/strings.xml b/patches/src/main/resources/addresources/values-hy-rAM/youtube/strings.xml index 1a02f7efd7..9079108272 100644 --- a/patches/src/main/resources/addresources/values-hy-rAM/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-hy-rAM/youtube/strings.xml @@ -57,6 +57,7 @@ Second \"item\" text" + diff --git a/patches/src/main/resources/addresources/values-in-rID/music/strings.xml b/patches/src/main/resources/addresources/values-in-rID/music/strings.xml index 158522bf9e..f6701fc6bb 100644 --- a/patches/src/main/resources/addresources/values-in-rID/music/strings.xml +++ b/patches/src/main/resources/addresources/values-in-rID/music/strings.xml @@ -609,7 +609,7 @@ Cookie yang diperlukan: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1P Mentranskripsi Transkripsi dibatalkan Hanya trek yang diputar dari YouTube yang dapat ditranskripsi - Tidak ada lirik untuk disusun - sedang melakukan transkripsi alih-alih + 1 lirik untuk diselaraskan - melainkan ditranskripsikan QuickLRC tidak dapat memproses trek ini Kredit QuickLRC telah habis untuk trek ini "Lirik dicocokkan berdasarkan judul dan artis, metadata yang tidak biasa mungkin tidak menemukan apa-apa atau versi yang salah. Gunakan refresh untuk beralih versi. Penyedia yang ditandai * memerlukan token. Tidak semua penyedia yang diaktifkan selalu diminta sesuai urutan Anda, tetapi merespons berdasarkan kecepatan mereka. diff --git a/patches/src/main/resources/addresources/values-in-rID/youtube/strings.xml b/patches/src/main/resources/addresources/values-in-rID/youtube/strings.xml index 095f7d1849..7aaa3ef092 100644 --- a/patches/src/main/resources/addresources/values-in-rID/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-in-rID/youtube/strings.xml @@ -435,6 +435,17 @@ Laporan bersifat publik dan ditinjau sebelum saluran ditambahkan ke daftar. Peng Mengetuk dan menahan di mana saja pada video umpan membuka menunya Tampilkan tombol Tonton nanti pada thumbnail Tombol Tonton nanti ditampilkan pada thumbnail video umpan. Mengetuk dan menekannya akan membuka Simpan ke daftar putar + + Unduh gambar postingan + Tombol unduh ditampilkan di samping tombol Bagikan pada postingan dengan gambar atau GIF, dan item unduh ditambahkan ke menu mereka + Unduh gambar + Unduh gambar + Mengunduh %s gambar… + Mengunduh gambar… + %1$s gambar disimpan ke %2$s + Gambar disimpan ke %s + Tidak dapat mengunduh gambar + Tidak ditemukan gambar dalam postingan ini Tambahkan item Ask ke menu flyout Menambahkan item ringkasan video ke menu flyout umpan @@ -473,7 +484,7 @@ Setelah video dengan tombol Ask dibuka satu kali, fitur ini berfungsi untuk semu Unduh video, podcast, atau lagu menggunakan aplikasi pihak ketiga Tampilkan tombol unduh tumpang tindih pemutar Menampilkan tombol di pemutar video yang membuka pengunduh eksternal Anda - Unduh cepat saat diketuk + Unduhan cepat sekali ketuk Perintah untuk mengunduh di latar belakang. Ketuk dan tahan untuk membuka pengunduh. Berfungsi dengan YTDLnis dan YTindir Tandai unduhan sebagai sudah ditonton Video yang diunduh menggunakan pengunduh eksternal akan ditambahkan ke riwayat tontonan YouTube Anda @@ -717,7 +728,7 @@ Untuk mengatasinya, aktifkan 'Palsukan aliran video' dan ubah 'Klien bawaan' men Batas waktu hamparan usap Jumlah milidetik hamparan terlihat Opasitas latar belakang hamparan usap - Sesuaikan level transparansi (0 transparan, 100 buram) + Sesuaikan level transparansi (0 transparan, 100 pekat) Warna kecerahan lapisan usap Atur warna bilah kemajuan kontrol kecerahan Warna volume lapisan usap @@ -881,7 +892,8 @@ Untuk mengakses Setelan, aktifkan tombol Setelan melalui Umum → Bilah Alat → Bawaan Translusen Transparan - Buram + Solid + Gaya ini mungkin tidak didukung oleh perangkat atau versi YouTube Anda, dan dapat menyebabkan masalah tata letak atau crash Bilah alat Pilih komponen mana yang akan ditampilkan di bilah alat Sembunyikan tombol Transmisi @@ -1075,7 +1087,7 @@ Setelan → Pemutaran (atau Pemutaran Otomatis) → Putar video berikutnya secar Tampilkan tombol Bisu Opasitas hamparan pemutar - Sesuaikan tingkat transparansi (0 adalah transparan, 100 adalah buram) + Sesuaikan tingkat transparansi (0 adalah transparan, 100 pekat) Bilah penelusuran Dinonaktifkan @@ -1109,7 +1121,9 @@ Tata letak Otomotif "Mengembalikan tata letak pemutar lama Batasan: Jika 'Kembalikan bilah tindakan video lama' dinonaktifkan, hamparan 'Video lainnya' mungkin tidak tersedia" - Mengaktifkan ini dapat mengganggu beberapa elemen tata letak pemutar, seperti bagian komentar atau \'Video lainnya\' yang ditampilkan di bawah pemutar + "Mengaktifkan fitur ini dapat merusak beberapa fitur tata letak pemutar, seperti bagian komentar atau 'Video lainnya' yang ditampilkan di bawah pemutar. + +Tata letak tablet tidak lagi didukung oleh YouTube, dan sebagian atau seluruhnya rusak pada versi YouTube terbaru. Untuk tata letak tablet yang berfungsi, tambal YouTube 20.21.37" 21.11.48 - Kembalikan perilaku teks lama 20.39.41 - Kembalikan penyimpanan lama ke daftar putar @@ -1212,7 +1226,7 @@ Batasan: Jika 'Kembalikan bilah tindakan video lama' dinonaktifkan, hamparan 'Vi Atur ukuran awal di layar, dalam piksel Ukuran piksel harus antara %1$s dan %2$s Opasitas hamparan - Sesuaikan tingkat transparansi (0 adalah transparan, 100 adalah buram) + Sesuaikan tingkat transparansi (0 adalah transparan, 100 pekat) Tombol kembali menutup feed Menekan tombol kembali akan selalu menutup aplikasi bukannya menggulir feed ke atas @@ -1377,6 +1391,11 @@ Video diminta dengan awalan pendek dari ID video yang di-hash, sehingga server t Menampilkan notifikasi saat kecepatan pemutaran bawaan baru disimpan Kecepatan pemutaran bawaan Kecepatan pemutaran default di Shorts + Ingat kecepatan pemutaran di Shorts + Menerapkan pilihan kecepatan Shorts manual Anda ke semua Shorts berikutnya. Jika dinonaktifkan, kecepatan akan kembali ke bawaan setelah pemutar Shorts ditutup + Tampilkan kecepatan pemutaran di menu Shorts + Menambahkan item kecepatan pemutaran ke menu pemutar Shorts + Kecepatan pemutaran · %s Mengubah kecepatan bawaan menjadi: %s Gunakan kecepatan normal untuk musik Selalu putar video musik dengan kecepatan 1.0x diff --git a/patches/src/main/resources/addresources/values-is-rIS/youtube/strings.xml b/patches/src/main/resources/addresources/values-is-rIS/youtube/strings.xml index 0433bdc740..d232211c46 100644 --- a/patches/src/main/resources/addresources/values-is-rIS/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-is-rIS/youtube/strings.xml @@ -425,6 +425,17 @@ Tilkynningar eru opinberar og endurskoðaðar áður en rás er bætt við lista Þegar þú smeltir og heldur niður einhversstaðar á feed-myndbandi opnast valmynd þess Sýna hnappinn \"Skjáa síðar\" á þumabörðum Hnappurinn \"Skjáa síðar\" birtist á þumabörðum feed-mynda. Þegar þú smeltir og heldur honum niður opnarðu Vista í spilunarlista + + Sækja niður myndir úr færslum + Sækkjunarhnappur birtist við hliðina á deiluhnappi færslna með myndum eða GIF-um, og sækkjunarmatrixi er bætt við valmynd þeirra + Sækja niður myndir + Sækja niður mynd + Sæki niður %s myndir… + Hleður niður mynd… + %1$s myndir vistar í %2$s + Mynd vist í %s + Gat ekki hlaðið niður myndinni + Engin mynd fannst í þessu færslunni Bæta Ask hlut í útflytvalmynd Bætir myndbandssamantektarefni í færiborðsútflytvalynduna @@ -868,6 +879,7 @@ Til að laga þetta skaltu kveikja á 'Spoofa myndstreymi' og breyta 'Sjálfgefn Hálfgagnsætt Gagnsætt Ógagnsætt + Þessi stíll gæti ekki ráðinn af tækinu þínu eða YouTube útgáfunni og getur valdið vandamálum við uppsetningu eða hruni Tólaborð Veldu hvaða þættir birtast á verkfærastikunni Felur Cast hnappinn @@ -1092,7 +1104,9 @@ Bílaútlit "Endurskipar gamla útlit spilarans Takmörkun: Ef 'Endurskapa gamla stjórnbara myndbands' er slökkt, gæti 'Fleiri myndbönd' lagskiptarinn ekki verið tiltæk" - Virkun þessa gæti truflað suma spilunaruppsetningar eiginleika, svo sem athugasemdahluta eða „Fleiri myndbönd“ sýnd neðan við spilarann + "Með því að virkja þetta gætu sumir eiginleikar uppsetningar spilarans skemst, svo sem athugasemdadeildin eða \"Meiri myndbönd\" sem sýnd eru neðan spilarans. + +Uppsetning fyrir spjaldtölvur er ekki lengur studduð af YouTube og er að hluta eða algjörlega óvirkar í nýlegum YouTube útgáfum. Til að fá virka uppsetningu fyrir spjaldtölvur, uppfærið YouTube 20.21.37" 21.11.48 - Endurheimta gamla textahegðun 20.39.41 - Endursafnaðu gamla vistun á spilalista @@ -1360,6 +1374,11 @@ Myndbönd eru beðin um með stuttu forskeyti af dulkóðaða myndbandskenni, sv Sýnir tilkynningu þegar nýja sjálfgild hraða er vistað Sjálfgefin hríf Grunn-spilunartíðni á Shorts + Muna spilnunarhraða í Shorts + Beitur handvirka hraða Shorts til allra framtíðar Shorts. Ef þetta er slökkt snýr hraðinn aftur í sjálfgefið eftir að Shorts spilaranum er lokað + Sýna endurtilspilunarhraða í Shorts valmyndinni + Bætir endurtilspilunarhraða í valmynd Shorts spilarans + Endurtilspilunarhraði · %s Breytt sjálfvirka hraða í: %s Notaðu venjulegan hraða fyrir tónlist Spila alltaf tónlistarmyndbönd á 1,0x hraða diff --git a/patches/src/main/resources/addresources/values-it-rIT/youtube/strings.xml b/patches/src/main/resources/addresources/values-it-rIT/youtube/strings.xml index efebb01ec0..dc35b53278 100644 --- a/patches/src/main/resources/addresources/values-it-rIT/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-it-rIT/youtube/strings.xml @@ -435,6 +435,17 @@ Le segnalazioni sono pubbliche e vengono esaminate prima che un canale venga agg Toccare e tenere premuto ovunque su un video nel feed apre il relativo menu Mostra il pulsante Guarda più tardi sulle miniature Un pulsante Guarda più tardi viene visualizzato sulle miniature dei video nel feed. Toccare e tenere premuto su di esso per Salva in playlist + + Scarica immagini post + Un pulsante di download viene mostrato accanto al pulsante Condividi dei post con immagini o GIF, e una voce di download viene aggiunta al loro menu + Scarica immagini + Scarica immagine + Download di %s immagini in corso… + Download dell\'immagine in corso… + %1$s immagini salvate in %2$s + Immagine salvata in %s + Impossibile scaricare l\'immagine + Nessuna immagine trovata in questo post Aggiungi elemento Chiedi al menu a discesa Aggiunge un elemento di sintesi video al menu a discesa del feed @@ -882,6 +893,7 @@ Per accedere alle Impostazioni, abilita il pulsante delle Impostazioni tramite G Traslucido Trasparente Opaco + Questo stile potrebbe non essere supportato dal tuo dispositivo o dalla versione di YouTube, e può causare problemi di layout o crash Barra degli strumenti Scegli quali componenti mostrare nella barra degli strumenti Nascondi il pulsante Trasmetti @@ -1109,7 +1121,9 @@ Layout automotive "Ripristina il layout precedente del player Limitazione: se \"Ripristina la vecchia barra delle azioni video\" è disattivato, la sovrapposizione \"Altri video\" potrebbe non essere disponibile" - L\'attivazione potrebbe interrompere alcune funzionalità del layout del player, come la sezione commenti o \'Più video\' mostrati sotto il player + "L'abilitazione di questa opzione potrebbe compromettere alcune funzioni di layout del player, come la sezione commenti o i \"Altri video\" mostrati sotto il player. + +Il layout per tablet non è più supportato da YouTube ed è parzialmente o totalmente compromesso nelle versioni recenti di YouTube. Per un layout tablet funzionante, applica la patch a YouTube 20.21.37" 21.11.48 - Ripristina il vecchio comportamento dei sottotitoli 20.39.41 - Ripristina le vecchie icone del player Shorts @@ -1377,6 +1391,11 @@ I video vengono richiesti tramite un breve prefisso dell'ID video con hash, quin Mostra una notifica quando viene salvata una nuova velocità di riproduzione predefinita Velocità di riproduzione predefinita Velocità di riproduzione predefinita per \"Short\" + Ricorda la velocità di riproduzione negli Shorts + Applica la selezione manuale della velocità degli Shorts a tutti gli Shorts futuri. Se disattivata, la velocità torna a quella predefinita dopo la chiusura del player degli Shorts + Mostra la velocità di riproduzione nel menu degli Shorts + Aggiunge una voce Velocità di riproduzione al menu del player degli Shorts + Velocità di riproduzione · %s Velocità di riproduzione predefinita modificata in: %s Usa la velocità normale per la musica Riproduci sempre i video musicali a velocità 1.0x diff --git a/patches/src/main/resources/addresources/values-iw-rIL/youtube/strings.xml b/patches/src/main/resources/addresources/values-iw-rIL/youtube/strings.xml index c2eb2c9ab6..6e94e3d1b2 100644 --- a/patches/src/main/resources/addresources/values-iw-rIL/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-iw-rIL/youtube/strings.xml @@ -435,6 +435,16 @@ Second \"item\" text" חיפוש ועורך בכל מקום בודק המשיר פתחם את מהשיר שלו הצג את בטו׳דת \"הצג בידוע חור\" בתויערות בטו׳דת \"הצג בידוע חור\" תציע בתויערות של וידועי המשיר. חיפוש ועורך הו מזמן חסון לכריזה + + הורדת תמונות פוסט + כפתור הורדה מוצג לצד כפתור השיתוף של פוסטים עם תמונות או GIF, ופריט הורדה מתווסף לתפריט שלהם + הורדת תמונות + הורדת תמונה + הורדת %s תמונות... + מוריד תמונה… + התמונה נשמרה ב-%s + לא ניתן להוריד את התמונה + לא נמצאה תמונה בפוסט זה הוספת פריט שאל לתפריט קיבול מוסיף פריט סיכום וידאו לתפריט קיבול הזרם @@ -882,6 +892,7 @@ Second \"item\" text" חצי שקוף שקוף אטום + סגנון זה עשוי שלא להיות נתמך על ידי המכשיר שלך או גרסת ה-YouTube, ועלול לגרום לבעיות פריסה או לקריסות סרגל כלים בחר אילו רכיבים יופיעו בסרגל הכלים הסתר לחצן העברה @@ -1109,7 +1120,9 @@ Second \"item\" text" "משחזר את פריסת הנגן הישנה מגבלה: אם \"שחזר סרגל הפעולות של הווידאו הישן\" כבוי, ייתכן ש-\"עוד סרטונים\" לא יהיה זמין" - הפעלת פעולה זו עלולה לשבץ חלק מתכונות פריסת הנגן, כמו אזור התגובות או \'סרטונים נוספים\' המוצגים מתחת לנגן + "הפעלת אפשרות זו עלולה לשבש חלק מתכונות הפריסה של הנגן, כגון אזור התגובות או \"סרטונים נוספים\" המוצגים מתחת לנגן. + +פריסת הטאבלט אינה נתמכת יותר על ידי YouTube, והיא פגומה חלקית או באופן מלא בגרסאות YouTube האחרונות. לקבלת פריסת טאבלט תקינה, יש להחיל פאץ' ל-YouTube 20.21.37" 21.11.48 - שחזור התנהגות הכתוביות הישנה 20.39.41 - שחזר שפופרות ישנות לפלייליסט @@ -1377,6 +1390,11 @@ Second \"item\" text" מציג הודעה כאשר נשמרת מהירות הפעלה ברירת מחדל מהירות הפעלה ברירת מחדל מהירות הפעלה ברירת מחדל ב-Shorts + זכור מהירות הפעלה ב-Shorts + מחיל את בחירת מהירות ה-Shorts הידנית שלך על כל ה-Shorts בעתיד. אם האפשרות כבויה, המהירות תחזור לברירת המחדל לאחר סגירת נגן ה-Shorts + הצג מהירות הפעלה בתפריט ה-Shorts + מוסיף פריט של מהירות הפעלה לתפריט הנגן של Shorts + מהירות הפעלה · %s מהירות ההשמעה המוגדרת כברירת מחדל שונתה ל: %s השתמש במהירות רגילה למוזיקה נגן תמיד סרטוני מוזיקה במהירות 1.0x diff --git a/patches/src/main/resources/addresources/values-ja-rJP/youtube/strings.xml b/patches/src/main/resources/addresources/values-ja-rJP/youtube/strings.xml index 860079ec54..02076fe9b5 100644 --- a/patches/src/main/resources/addresources/values-ja-rJP/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ja-rJP/youtube/strings.xml @@ -436,6 +436,17 @@ AI について扱っているだけのチャンネルや、動画の一部に A フィードの動画をどこでもよいので長押しすると、フライアウト メニューを開きます サムネイルに「後で見る」ボタンを表示 フィード動画のサムネイルに「後で見る」ボタンが表示され、それをタップして長押しすると「プレイリストに保存」が開きます + + 投稿画像のダウンロード + 画像またはGIFを含む投稿の共有ボタンの隣にダウンロードボタンが表示され、メニューにダウンロード項目が追加されます + 画像をダウンロード + 画像をダウンロード + %s枚の画像をダウンロード中… + 画像をダウンロード中… + %1$s枚の画像を %2$s に保存しました + 画像を %s に保存しました + 画像のダウンロードに失敗しました + このポストに画像は見つかりませんでした 「動画を要約して」をフライアウト メニューに追加 「この動画について質問する」欄を、欄内の「動画を要約して」がタップされた状態で開く項目を、フィードのフライアウト メニューに追加します @@ -883,6 +894,7 @@ APIキーは決して共有しないでください。アクセス権を持つ 半透明 透明 不透明 + このスタイルはお使いのデバイスまたはYouTubeのバージョンでサポートされていない可能性があり、レイアウトの問題やクラッシュの原因となることがあります ツールバー ツールバーに表示するコンポーネントを設定します キャストボタンを非表示 @@ -1110,7 +1122,9 @@ Automotive レイアウト "動画プレーヤーのレイアウトを以前のスタイルに戻します 制限事項: [アクションバーを旧仕様に戻す] がオフのとき、「その他の動画」オーバーレイが表示されない場合があります" - これを有効にすると、コメントセクションやプレイヤー下部に表示される「他の動画」など、一部のプレイヤーレイアウト機能が壊れる可能性があります + "これを有効にすると、コメント欄やプレイヤー下に表示される「おすすめ動画」など、一部のプレイヤーレイアウト機能が正しく動作しなくなる可能性があります。 + +タブレットレイアウトはYouTubeによってサポートされなくなったため、最近のYouTubeバージョンでは部分的または完全に機能しません。タブレットレイアウトを動作させるには、YouTube 20.21.37にパッチを適用してください" 21.11.48 - 字幕の動作を旧仕様に戻す 20.39.41 -「再生リストに保存」を旧仕様に戻す @@ -1378,6 +1392,11 @@ Automotive レイアウト 設定の [デフォルトの再生速度] に新しい値が保存されたときに通知を表示します デフォルトの再生速度 ショートの再生速度のデフォルト値 + ショートの再生速度を記憶 + 手動で選択したショートの再生速度を、今後のすべてのショートに適用します。オフにすると、ショートプレーヤーを閉じた後に速度がデフォルトに戻ります + ショートのメニューに再生速度を表示 + ショートプレーヤーのメニューに再生速度項目を追加します + 再生速度 · %s デフォルトの再生速度の変更: %s 音楽動画を通常速度で再生 音楽動画を常に 1.0 倍速で再生します diff --git a/patches/src/main/resources/addresources/values-ka-rGE/youtube/strings.xml b/patches/src/main/resources/addresources/values-ka-rGE/youtube/strings.xml index f110ddefa1..5dd6a3cae5 100644 --- a/patches/src/main/resources/addresources/values-ka-rGE/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ka-rGE/youtube/strings.xml @@ -57,6 +57,7 @@ Second \"item\" text" + diff --git a/patches/src/main/resources/addresources/values-kk-rKZ/youtube/strings.xml b/patches/src/main/resources/addresources/values-kk-rKZ/youtube/strings.xml index 1a02f7efd7..9079108272 100644 --- a/patches/src/main/resources/addresources/values-kk-rKZ/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-kk-rKZ/youtube/strings.xml @@ -57,6 +57,7 @@ Second \"item\" text" + diff --git a/patches/src/main/resources/addresources/values-km-rKH/youtube/strings.xml b/patches/src/main/resources/addresources/values-km-rKH/youtube/strings.xml index 236d505cea..b4f319c9fc 100644 --- a/patches/src/main/resources/addresources/values-km-rKH/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-km-rKH/youtube/strings.xml @@ -70,6 +70,7 @@ Second \"item\" text" + diff --git a/patches/src/main/resources/addresources/values-kmr-rTR/youtube/strings.xml b/patches/src/main/resources/addresources/values-kmr-rTR/youtube/strings.xml index 86a2c93858..8edaad40b7 100644 --- a/patches/src/main/resources/addresources/values-kmr-rTR/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-kmr-rTR/youtube/strings.xml @@ -332,6 +332,7 @@ Sînorkirin: Tevgera bikirinê û bikirin û girtinê ya bişêvekirina \'Girêdana vîdyoyê kopî bikin\' bişêvekirin + diff --git a/patches/src/main/resources/addresources/values-kn-rIN/youtube/strings.xml b/patches/src/main/resources/addresources/values-kn-rIN/youtube/strings.xml index 193b040db3..a1a0baa07c 100644 --- a/patches/src/main/resources/addresources/values-kn-rIN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-kn-rIN/youtube/strings.xml @@ -57,6 +57,7 @@ Second \"item\" text" + diff --git a/patches/src/main/resources/addresources/values-ko-rKR/music/strings.xml b/patches/src/main/resources/addresources/values-ko-rKR/music/strings.xml index 868f4553be..de748a45cb 100644 --- a/patches/src/main/resources/addresources/values-ko-rKR/music/strings.xml +++ b/patches/src/main/resources/addresources/values-ko-rKR/music/strings.xml @@ -427,7 +427,7 @@ SABR 프로토콜을 사용하여 재생하는 경우 (동영상 스트림 속 토큰을 저장하였습니다 경고: 저장된 토큰이 유효하지 않습니다! 좋아요한 트랙에 좋아요 표시하기 - YouTube Music에서 트랙에 좋아요를 누르면 Last.fm에서도 자동으로 좋아요가 표시됩니다 (좋아요를 해제하거나 싫어요를 누르면 자동으로 좋아요가 해제됩니다) + YouTube Music에서 트랙에 좋아요를 탭하면 Last.fm에서도 자동으로 좋아요가 표시됩니다 (좋아요를 해제하거나 싫어요를 탭하면 자동으로 좋아요가 해제됩니다) Last.fm 계정 \'%s\'으로 로그인되어 있습니다. 계정을 관리하려면 탭하세요 Last.fm 자격 증명을 입력하여 로그인하세요 @@ -584,7 +584,7 @@ LOGIN_INFO" 번역됨 번역 언어 - 로마자 변환 언어 + 로마자 표기 언어 번역하는 중… 가사를 번역할 수 없습니다 로마자 표기됨 diff --git a/patches/src/main/resources/addresources/values-ko-rKR/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-ko-rKR/shared-youtube/strings.xml index c221c1d203..3483ff5b44 100644 --- a/patches/src/main/resources/addresources/values-ko-rKR/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ko-rKR/shared-youtube/strings.xml @@ -152,12 +152,12 @@ YouTube Premium 사용자는 이 설정이 필요하지 않을 수 있습니다" 재생바 비활성화하기 잠금 화면 및 알림 패널에서 재생바를 드래그하지 못하도록 합니다 이전 및 다음 버튼으로 탐색하기 - 미디어 알림, 잠금 화면 및 헤드폰에서 비디오 변경 대신 탐색하도록 제어 + 미디어 알림, 잠금 화면 및 헤드폰 컨트롤 버튼을 사용하여 동영상을 변경하는 대신 재생 위치를 앞뒤로 이동하도록 변경합니다 되감기 길이 - 이전 버튼으로 되감기할 초 단위 - 앞당기기 길이 - 다음 버튼으로 앞당길 초 단위 - 탐색 길이는 1~%s초여야 합니다. 기본값 사용 + 이전 버튼으로 되감기할 시간을 지정할 수 있습니다 (초) + 빨리 감기 길이 + 다음 버튼으로 빨리 감기할 시간을 지정할 수 있습니다 (초) + 탐색 길이는 1-%s 초 사이여야 합니다. 기본값을 사용합니다 QUIC 프로토콜 비활성화하기 diff --git a/patches/src/main/resources/addresources/values-ko-rKR/youtube/strings.xml b/patches/src/main/resources/addresources/values-ko-rKR/youtube/strings.xml index 00fc208917..bb35d9f704 100644 --- a/patches/src/main/resources/addresources/values-ko-rKR/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ko-rKR/youtube/strings.xml @@ -43,13 +43,13 @@ Second \"item\" text" 서버가 앱 레이아웃을 업데이트하지 못하도록 합니다 "앱 레이아웃이 처음 설치했을 때의 레이아웃으로 되돌아갑니다 -일부 서버 측 레이아웃은 원래대로 복원되지 않을 수 있습니다 +일부 서버 측 레이아웃은 원래대로 되돌아가지 않을 수 있습니다 변경 사항: • 플레이어 메뉴 구성 요소(또는 관련 설정)가 작동하지 않을 수 있습니다 • 롤링 넘버 애니메이션이 표시되지 않습니다 • 보관함 탭이 표시됩니다 -• 동영상 설명의 음악 섹션이 작동하지 않을 수 있습니다 +• 동영상 설명에서 음악 섹션이 작동하지 않을 수 있습니다 • 플레이어 메뉴 구성요소 항목을 숨길 수 없습니다 • 보관함 탭에 계정 전환 버튼이 표시되지 않을 수 있습니다" @@ -275,7 +275,7 @@ Second \"item\" text" 기프트 버튼 숨기기 정보 버튼 숨기기 실시간 채팅 후원자 섹션 숨기기 - 라이브 채팅 맨 아래로 스크롤 버튼 숨기기 + 라이브 채팅에서 맨 아래로 스크롤 버튼 숨기기 실시간 채팅 도구 설명 숨기기 구독자 전용 채팅 안내와 같이 채팅 입력창 주변에 표시되는 도구 설명 팝업을 숨깁니다 메뉴 버튼 숨기기 @@ -431,34 +431,45 @@ Second \"item\" text" 댓글을 길게 탭하여 복사할 수 있도록 합니다 댓글이 클립보드에 복사되었습니다 - 동영상을 길게 누르면 메뉴가 열립니다 - 피드 동영상의 아무 곳이나 길게 누르면 해당 동영상의 메뉴가 열립니다 - 썸네일에 \'나중에 볼 목록\' 버튼 표시 - 피드 동영상의 썸네일에 \'나중에 볼 목록\' 버튼이 표시됩니다. 버튼을 길게 누르면 플레이리스트에 저장됩니다 + 길게 탭하여 동영상 메뉴 열기 + 피드 동영상에서 아무 곳이나 길게 탭하면 해당 동영상 메뉴가 열립니다 + 썸네일에서 \'나중에 볼 동영상에 저장\' 버튼 표시하기 + 피드 동영상 썸네일에 \'나중에 볼 동영상에 저장\' 버튼을 표시합니다. 버튼을 길게 탭하면 재생목록에 저장됩니다 + + 게시물 이미지 다운로드하기 + 이미지 또는 GIF가 포함된 게시물의 공유 버튼 옆에 다운로드 버튼이 표시되며, 해당 게시물의 메뉴에도 다운로드 항목이 추가됩니다 + 이미지 다운로드 + 이미지 다운로드 + %s 개의 이미지를 다운로드하는 중… + 이미지를 다운로드하는 중… + %1$s 개의 이미지가 %2$s에 다운로드됨 + 이미지가 %s에 다운로드됨 + 이미지를 다운로드할 수 없습니다 + 이 게시물에서 이미지를 찾을 수 없습니다 메뉴 구성요소에 \'질문하기\' 항목 추가하기 - 피드 메뉴 구성요소에 동영상 요약 항목을 추가합니다 + 피드 메뉴 구성요소에 동영상을 요약할 수 있는 \'질문하기\' 항목을 추가합니다 "'질문하기' 기능을 사용하려면 그 기능을 사용할 수 있는 국가에서 로그인된 계정이 필요합니다 -'질문하기' 버튼이 있는 동영상을 한 번 열고 나면, '질문하기' 기능을 사용할 수 없는 국가에 있더라도 모든 동영상에서 그 기능을 사용할 수 있습니다" +'질문하기' 버튼이 있는 동영상을 한 번 열고 나면, '질문하기' 기능을 사용할 수 없는 국가에 있더라도 모든 동영상에서 그 기능을 사용할 수 있습니다" - 동영상 요약 + 질문하기 - 이 동영상에서 \'질문하기\'를 열 수 없습니다 + 이 동영상에서 \'질문하기\'를 열 수 없습니다 \'질문하기\'는 사용자 계정 또는 현재 국가에서 사용할 수 없습니다 - 썸네일에 조회수 표시 - 피드 동영상의 조회수는 제목 아래가 아니라 썸네일 왼쪽 하단에 표시됩니다. Shelf 등 일부 동영상에서는 조회수가 표시되지 않을 수 있습니다 + 썸네일에서 조회수 표시하기 + 피드 동영상 조회수를 제목 아래가 아닌 썸네일 왼쪽 하단에 표시합니다. 선반에 표시되는 일부 동영상은 조회수가 표시되지 않을 수 있습니다 - 피드 썸네일 좋아요 수 표시하기 - 피드 썸네일 좋아요 비율 표시하기 + 피드 썸네일에서 좋아요 수 표시하기 + 피드 썸네일에서 좋아요 비율 표시하기 댓글 검색 - 댓글의 검색 버튼을 탭하면 검색어의 모든 단어가 포함된 로드된 댓글만 표시됩니다. 구문 검색을 하려면 단어를 큰따옴표로 묶으세요 + 댓글에서 검색 버튼을 탭하면 현재 불러온 댓글 중 검색한 단어가 모두 포함된 댓글만 표시됩니다. 구문을 검색하려면 단어를 따옴표로 묶으세요 댓글 검색 - 검색할 단어 + 검색어 입력 검색 지우기 정렬 순서를 변경하거나 댓글을 다시 열어 검색하세요 @@ -884,6 +895,7 @@ API 키는 절대로 다른 사람과 공유해서는 안 됩니다. API 키에 반투명 투명 불투명 + 이 스타일은 사용자의 기기 또는 YouTube 버전에서 지원되지 않을 수 있으며, 레이아웃 문제나 충돌을 일으킬 수 있습니다 툴바 툴바에 표시할 구성 요소를 설정할 수 있습니다 크롬캐스트 버튼 숨기기 @@ -1111,7 +1123,9 @@ API 키는 절대로 다른 사람과 공유해서는 안 됩니다. API 키에 "이전 플레이어 레이아웃을 복원합니다 제한 사항: '이전 동영상 작업 버튼바 복원하기'를 비활성화하면, '동영상 더보기' 오버레이를 사용할 수 없을 수 있습니다" - 이 기능을 활성화하면 플레이어 아래에 표시되는 댓글 섹션이나 \'동영상 더보기\'와 같은 일부 플레이어 레이아웃 기능이 손상될 수 있습니다 + "이 기능을 활성화하면 댓글 섹션이나 플레이어 아래에 표시되는 '추천 동영상'과 같은 일부 플레이어 레이아웃 기능이 제대로 작동하지 않을 수 있습니다 + +태블릿 레이아웃은 더 이상 YouTube에서 지원하지 않으며, 최신 YouTube 버전에서는 레이아웃이 부분적으로 또는 완전히 깨질 수 있습니다. 태블릿 레이아웃을 정상적으로 사용하려면 YouTube 20.21.37 버전을 패치하세요" 21.11.48 - 이전 자막 동작을 복원합니다 20.39.41 - 이전 \'재생목록에 저장\'을 복원합니다 @@ -1182,7 +1196,7 @@ API 키는 절대로 다른 사람과 공유해서는 안 됩니다. API 키에 피드 카드 레이아웃 클래식 카드 - 좋아요, 댓글, 저장 버튼이 있는 카드 + 좋아요, 댓글 및 저장 버튼이 있는 카드 댓글 미니 플레이어 다시 시작 비활성화하기 @@ -1219,9 +1233,9 @@ API 키는 절대로 다른 사람과 공유해서는 안 됩니다. API 키에 뒤로가기 버튼으로 피드 나가기 뒤로가기 버튼을 탭하면 피드 상단으로 이동하지 않고 항상 앱이 종료됩니다 - 애니메이션 강제 적용 + 애니메이션 강제로 활성화하기 기본값 - 항상 사용 + 항상 켜짐 항상 꺼짐 상태바 숨기기 @@ -1331,10 +1345,10 @@ API 키는 절대로 다른 사람과 공유해서는 안 됩니다. API 키에 동영상 딜레이 블루투스 이어폰처럼 오디오가 늦게 재생되는 경우, 소리와 동영상의 싱크를 맞추기 위해 동영상에 딜레이를 줍니다 (0-5000ms) - 블루투스 오디오에서만 딜레이 + 블루투스 오디오에서만 딜레이 적용하기 블루투스 오디오가 연결된 경우에만 동영상에 딜레이를 적용하므로, 휴대폰 스피커로 재생할 때는 싱크가 유지됩니다 동영상 딜레이 버튼 표시하기 - 길게 탭하여 딜레이 시간을 0ms로 초기화합니다 + 버튼을 길게 탭하여 딜레이 시간을 0ms로 초기화할 수 있습니다 동영상 딜레이 메뉴 표시하기 딜레이 시간을 설정하세요 (밀리초) @@ -1379,6 +1393,11 @@ API 키는 절대로 다른 사람과 공유해서는 안 됩니다. API 키에 기본 동영상 재생 속도 값이 변경될 때마다 알림을 표시합니다 기본 동영상 재생 속도 기본 Shorts 재생 속도 + Shorts 재생 속도 저장 활성화하기 + 수동으로 설정한 Shorts 재생 속도를 앞으로 보는 모든 Shorts에 적용합니다. 이 기능을 비활성화하면 Shorts 플레이어를 닫은 후 재생 속도가 기본값으로 돌아갑니다 + Shorts 메뉴에 재생 속도 메뉴 표시하기 + Shorts 플레이어 메뉴에 재생 속도 항목을 추가합니다 + 재생 속도 · %s 기본 동영상 재생 속도 값을 %s로 변경하였습니다 음악 일반 동영상 재생 속도 사용하기 음악 동영상을 항상 1.0 배속으로 재생합니다 @@ -1411,7 +1430,7 @@ API 키는 절대로 다른 사람과 공유해서는 안 됩니다. API 키에 AVC를 강제로 활성화하려면, \'동영상 스트림 속이기\'에서 \'기본 클라이언트\'를 Android Studio\', \'Android VR\' 또는 \'visionOS\'로 변경하세요 "VP9 • 배터리 수명을 개선하고 동영상 재생 끊김 현상을 해결할 수 있습니다 -• AV1보다 인터넷 데이터를 더 많이 사용합니다 +• AV1보다 모바일 데이터를 더 많이 사용합니다 AVC (H.264) • 동영상 스트림 클라이언트를 Android Studio, Android VR 또는 visionOS로 속여야 합니다 @@ -1562,7 +1581,7 @@ AV1이 사용된 동영상 재생이 끊기거나 프레임이 손실될 수 있 이어보기를 확인할 수 없습니다. 플레이어를 확인한 후 다시 시도하세요 시리즈 팔로잉 중 이 동영상은 이 재생목록에 없습니다 - 플레이리스트 찾는 중… + 재생목록을 찾는 중… 재생목록 링크 붙여넣기 재생목록을 찾을 수 없습니다 사용 가능한 에피소드가 없습니다 @@ -1598,10 +1617,10 @@ AV1이 사용된 동영상 재생이 끊기거나 프레임이 손실될 수 있 백업을 저장할 수 없습니다. 다른 위치를 시도하세요 시리즈 제거됨 이제 이 에피소드가 여러 번 표시됩니다. 계속할 대상을 선택하세요 - 저장된 에피소드는 이제 이 플레이리스트에 없습니다. 저장된 위치는 유지됩니다 + 저장된 에피소드는 이제 재생목록에 없습니다. 저장된 위치는 유지됩니다 에피소드 %1$s · %2$s에서 계속하기 아래의 모든 에피소드를 선택할 수도 있습니다. 이전 에피소드는 변경되지 않습니다 - 플레이리스트가 변경되었습니다. 시리즈를 열어 계속할 위치를 선택하세요 + 재생목록이 변경되었습니다. 시리즈를 열어 계속할 위치를 선택하세요 이 방향의 에피소드가 더 이상 없습니다 %1$s 계속하기 시리즈 검색 diff --git a/patches/src/main/resources/addresources/values-ky-rKG/youtube/strings.xml b/patches/src/main/resources/addresources/values-ky-rKG/youtube/strings.xml index 774c80d9ff..ff84d4a44b 100644 --- a/patches/src/main/resources/addresources/values-ky-rKG/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ky-rKG/youtube/strings.xml @@ -68,6 +68,7 @@ Second \"item\" text" + diff --git a/patches/src/main/resources/addresources/values-lo-rLA/youtube/strings.xml b/patches/src/main/resources/addresources/values-lo-rLA/youtube/strings.xml index 1a02f7efd7..9079108272 100644 --- a/patches/src/main/resources/addresources/values-lo-rLA/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-lo-rLA/youtube/strings.xml @@ -57,6 +57,7 @@ Second \"item\" text" + diff --git a/patches/src/main/resources/addresources/values-lt-rLT/youtube/strings.xml b/patches/src/main/resources/addresources/values-lt-rLT/youtube/strings.xml index 3c1d6adb77..43ee71bf07 100644 --- a/patches/src/main/resources/addresources/values-lt-rLT/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-lt-rLT/youtube/strings.xml @@ -435,6 +435,16 @@ Pranešimai yra vieši ir peržiūrimi prieš pridedant kanalą į sąrašą. Pa Paspaudus ir palaikius bet kuria feed video vieta, atidaromas jo meniu Rodyti „Pažiūrėti vėliau“ mygtuką prie miniatiūrų Ant nuotykų srauto vaizdo įrašų miniatiūrų rodomas mygtukas „Pažiūrėti vėliau“. Paspaudus ir laikant jį, atidaroma parinktis „Įtraukti į grojaraštį“ + + Atsisiųsti įrašų nuotraukas + Šalia įrašų su nuotraukomis arba GIF failais esančio bendrinimo mygtuko rodomas atsisiuntimo mygtukas, o jų meniu pridedama atsisiuntimo parinktis + Atsisiųsti nuotraukas + Atsisiųsti nuotrauką + Atsiunčiamas %s vaizdai… + Atsiunčiamas vaizdas… + Vaizdas išsaugotas į %s + Nepavyko atsisiųsti vaizdo + Šiame įraše vaizdo nerasta Pridėti „Ask“ elementą į iškylančiąjį meniu Pridėti vaizdo įrašo apibendrinimo elementą į feed\'o iškylančiąjį meniu @@ -882,6 +892,7 @@ Norėdami gauti prieigą prie nustatymų, įgalinkite Nustatymų mygtuką: Bendr Šviesu Skaidrus Nepermatomas + Šio stiliaus jūsų įrenginys arba „YouTube“ versija gali nepalaikyti, vilketas gali sukelti išdėstymo problemų arba programos lūžimus Įrankių juosta Pasirinkite, kurie komponentai rodomi įrankių juostoje Slėpti „Cast“ mygtuką @@ -1109,7 +1120,9 @@ Automobilio maketas "Atkuria senąjį grotuvo maketą Apribojimas: jei parinktis „Atkurti senąją vaizdo įrašo veiksmų juostą“ išjungta, perdanga „Daugiau vaizdo įrašų“ gali būti nepasiekiama" - Įgalinus šią parinktį gali būti sugriauta kai kurie grojimo plano elementai, pvz., komentarų skiltis arba „Daugiau vaizdo įrašų“ rodymas po valdikliu + "Įjungus šią funkciją gali neveikti kai kurios grotuvo išdėstymo funkcijos, pavyzdžiui, komentarų skiltis arba po grotuvu rodomi „Kiti vaizdo įrašai“. + +„YouTube“ nebepalaiko planšetinio išdėstymo, todėl naujesnėse „YouTube“ versijose jis veikia dalinai arba visai neveikia. Norėdami gauti veikiančią planšetinio išdėstymo versiją, naudokite „YouTube“ 20.21.37 pataisą" 21.11.48 – Atkurti senąją subtitrų elgseną 20.39.41 – atkurti senąjį išsaugojimą į grojaraštį @@ -1377,6 +1390,11 @@ Vaizdo įrašai užklausiami pagal trumpą maišyto vaizdo įrašo ID priešdėl Rodo pranešimą, kai išsaugomas naujas numatytasis atkūrimo greitis Numatytasis atkūrimo greitis Numatytoji „Shorts“ vaizdo įrašo atkūrimo sparta + Prisiminti atkūrimo greitį Shorts + Tavo rankiniu būdu pasirinktas Shorts greitis bus taikomas visiems būsimiems Shorts. Jei išjungta, uždarius Shorts grotuvą, greitis grįžta į standartinį + Rodyti atkūrimo greitį Shorts meniu + Prideda atkūrimo greities punktą į Shorts grotuvo meniu + Atgrojimo greitis · %s Numatytasis atkūrimo greitis pakeistas į: %s Muzikai naudoti įprastą greitį Muzikos vaizdo įrašus visada leisti 1,0x greičiu diff --git a/patches/src/main/resources/addresources/values-lv-rLV/youtube/strings.xml b/patches/src/main/resources/addresources/values-lv-rLV/youtube/strings.xml index fe862fe1a9..2a8bdb44f5 100644 --- a/patches/src/main/resources/addresources/values-lv-rLV/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-lv-rLV/youtube/strings.xml @@ -76,6 +76,7 @@ Second \"item\" text" + diff --git a/patches/src/main/resources/addresources/values-mai-rIN/youtube/strings.xml b/patches/src/main/resources/addresources/values-mai-rIN/youtube/strings.xml index 95ee0ab966..88e33fa418 100644 --- a/patches/src/main/resources/addresources/values-mai-rIN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-mai-rIN/youtube/strings.xml @@ -151,6 +151,7 @@ Second \"item\" text" + diff --git a/patches/src/main/resources/addresources/values-mk-rMK/youtube/strings.xml b/patches/src/main/resources/addresources/values-mk-rMK/youtube/strings.xml index bdf7e2650b..ed9fe7fa62 100644 --- a/patches/src/main/resources/addresources/values-mk-rMK/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-mk-rMK/youtube/strings.xml @@ -205,6 +205,7 @@ Second \"item\" text" Копирајте го временскиот печат стандардно + diff --git a/patches/src/main/resources/addresources/values-mn-rMN/youtube/strings.xml b/patches/src/main/resources/addresources/values-mn-rMN/youtube/strings.xml index 1a02f7efd7..9079108272 100644 --- a/patches/src/main/resources/addresources/values-mn-rMN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-mn-rMN/youtube/strings.xml @@ -57,6 +57,7 @@ Second \"item\" text" + diff --git a/patches/src/main/resources/addresources/values-mr-rIN/youtube/strings.xml b/patches/src/main/resources/addresources/values-mr-rIN/youtube/strings.xml index 5209d3977f..4473d78ced 100644 --- a/patches/src/main/resources/addresources/values-mr-rIN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-mr-rIN/youtube/strings.xml @@ -310,6 +310,7 @@ Second \"item\" text" \'कॉपी व्हिडिओ लिंक\' बटणाच्या टॅप आणि टॅप-अँड-होल्ड क्रिया उलटवते + diff --git a/patches/src/main/resources/addresources/values-ms-rMY/youtube/strings.xml b/patches/src/main/resources/addresources/values-ms-rMY/youtube/strings.xml index 5503a8a854..47f950bf35 100644 --- a/patches/src/main/resources/addresources/values-ms-rMY/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ms-rMY/youtube/strings.xml @@ -119,6 +119,7 @@ Second \"item\" text" Membalikkan tindakan ketik serta ketik dan tahan bagi butang \'Salin pautan video\' + diff --git a/patches/src/main/resources/addresources/values-my-rMM/youtube/strings.xml b/patches/src/main/resources/addresources/values-my-rMM/youtube/strings.xml index 1a02f7efd7..9079108272 100644 --- a/patches/src/main/resources/addresources/values-my-rMM/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-my-rMM/youtube/strings.xml @@ -57,6 +57,7 @@ Second \"item\" text" + diff --git a/patches/src/main/resources/addresources/values-nb-rNO/youtube/strings.xml b/patches/src/main/resources/addresources/values-nb-rNO/youtube/strings.xml index 09a03dfdf2..16e2305ed1 100644 --- a/patches/src/main/resources/addresources/values-nb-rNO/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-nb-rNO/youtube/strings.xml @@ -435,6 +435,17 @@ Rapporter er offentlige og blir gjennomgått før en kanal legges til en liste. Å trykke og holde nede hvor som helst på en feed-video åpner menyen for den Vis \"Se senere\"-knapp på miniatyrbilder En \"Se senere\"-knapp vises på miniatyrbildene til feed-videene. Trykk og hold for å lagre i spilleliste + + Last ned bilder fra innlegg + En nedlastingsknapp vises ved siden av Del-knappen for innlegg med bilder eller GIF-er, og et nedlastingsvalg legges til i menyen deres + Last ned bilder + Last ned bilde + Laster ned %s bilder… + Laster ned bilde… + %1$s bilder lagret i %2$s + Bilde lagret i %s + Kunne ikke laste ned bildet + Ingen bilder funnet i dette innlegget Legg til Ask-objekt i uttrekksmenyen Legger til et videooppsummeringsobjekt i strømmens uttrekksmeny @@ -882,6 +893,7 @@ For å nå Innstillinger, aktiver Innstillingsknappen via Generelt → Verktøyl Gjennomsiktig Gjennomsiktig Ugjennomsiktig + Denne stilen støttes kanskje ikke av enheten din eller YouTube-versjonen, og kan forårsake layoutproblemer eller krasj Verktøylinje Velg hvilke komponenter som vises i popup-menyen Skjul Kast-knapp @@ -1109,7 +1121,9 @@ Bil-layout "Gjenoppretter det gamle spillerlayoutet Begrensning: Hvis 'Gjenopprett gammel videoaksjonslinje' er slått av, er det mulig at 'Flere videoer'-overlegg ikke er tilgjengelig" - Aktivering av denne innstillingen kan ødelegge noen spillerutformingsfunksjoner, som kommentarseksjonen eller «Flere videoer» som vises under spilleren + "Aktivering av dette kan ødelegge enkelte layoutfunksjoner i spilleren, som kommentarfeltet eller \"Flere videoer\" som vises under spilleren. + +Nettbrettlayout støttes ikke lenger av YouTube, og er delvis eller helt ødelagt i nyere YouTube-versjoner. For en fungerende nettbrettlayout, patch YouTube 20.21.37" 21.11.48 - Gjenopprett gammel tekstringsatferd 20.39.41 – Gjenopprett gammel lagring til spilleliste @@ -1377,6 +1391,11 @@ Videoer blir forespurt med et kort prefiks av den hashet video-ID-en, så server Viser et varsel når en ny standard avspillingshastighet er lagret Standard avspillingshastighet Standard avspillingshastighet for Shorts + Husk avspillingshastighet på Shorts + Bruk ditt manuelle valg av Shorts-hastighet på alle fremtidige Shorts. Hvis dette er av, går hastigheten tilbake til standard etter at Shorts-spilleren lukkes + Vis avspillingshastighet i Shorts-menyen + Legger til et element for avspillingshastighet i menyen til Shorts-spilleren + Avspillingshastighet · %s Endret standard avspillingshastighet til: %s Bruk normal hastighet for musikk Spill alltid av musikkvideoer med 1,0x hastighet diff --git a/patches/src/main/resources/addresources/values-ne-rIN/youtube/strings.xml b/patches/src/main/resources/addresources/values-ne-rIN/youtube/strings.xml index 1a02f7efd7..9079108272 100644 --- a/patches/src/main/resources/addresources/values-ne-rIN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ne-rIN/youtube/strings.xml @@ -57,6 +57,7 @@ Second \"item\" text" + diff --git a/patches/src/main/resources/addresources/values-nl-rNL/youtube/strings.xml b/patches/src/main/resources/addresources/values-nl-rNL/youtube/strings.xml index 5af0bab71a..54a3a466c2 100644 --- a/patches/src/main/resources/addresources/values-nl-rNL/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-nl-rNL/youtube/strings.xml @@ -435,6 +435,16 @@ Rapporten zijn openbaar en worden beoordeeld voordat een kanaal aan een lijst wo Korte of lange aanraking ergens op een feedvideo opent het menu Button \"Later bekijken\" weergeven op miniatuurafbeeldingen Er wordt een knop \"Later bekijken\" weergegeven op de miniatuurafbeeldingen van feedvideo\'s. Tikken en vasthouden opent Opslaan in afspeellijst + + Berafbeeldingen downloaden + Er wordt een downloadknop getoond naast de deelknop van berichten met afbeeldingen of GIF\'s, en er wordt een downloaditem toegevoegd aan hun menu + Afbeeldingen downloaden + Afbeelding downloaden + {one,1 afbeelding downloaden…|other,%s afbeeldingen downloaden…} + Afbeelding downloaden… + Afbeelding opgeslagen in %s + De afbeelding kon niet worden gedownload + Geen afbeelding gevonden in dit bericht Ask-item aan uitklapmenu toevoegen Videofatsummarisatie-item toevoegen aan het feed-uitklapmenu @@ -880,6 +890,7 @@ Om Instellingen te openen, schakel je de knop Instellingen in via Algemeen → G Translucent Transparant Ondoorzichtig + Deze stijl wordt mogelijk niet ondersteund door je apparaat of YouTube-versie, wat kan leiden tot lay-outproblemen of crashes Werkbalk Kies welke componenten in de werkbalk verschijnen Verberg Cast-knop @@ -1107,7 +1118,9 @@ Automotive-indeling "Herstelt de oude player layout Beperking: Als 'Herstel oude video acties balk' is uitgeschakeld, is de \"Meer video's\" overlay mogelijk niet beschikbaar" - Het inschaken hiervan kan enkele lay-outfuncties van de speler breken, zoals het reactiegedeelte of \'Meer video\'s\' onder de speler + "Het inschakelen hiervan kan sommige lay-outfuncties van de speler verbreken, zoals de commentaarsectie of 'Meer video's' onder de speler. + +De tabletlay-out wordt niet langer ondersteund door YouTube en is gedeeltelijk of volledig defect in recente YouTube-versies. Voor een werkende tabletlay-out, patch YouTube 20.21.37" 21.11.48 - Herstel het oude gedrag van ondertitels 20.39.41 - Herstel oude Shorts-speler pictogrammen @@ -1375,6 +1388,11 @@ Video's worden opgevraagd via een kort voorvoegsel van de gehashte video-ID, zod Toont een melding wanneer een nieuwe standaard afspeelsnelheid is opgeslagen Standaard afspeelsnelheid Standaard afspeelsnelheid op Shorts + Onthoud afspeelsnelheid bij Shorts + Past je handmatige selectie van de Shorts-snelheid toe op alle toekomstige Shorts. Als deze optie is uitgeschakeld, keert de snelheid terug naar de standaardwaarde na het sluiten van de Shorts-speler + Toon afspeelsnelheid in het Shorts-menu + Voegt een item voor afspeelsnelheid toe aan het menu van de Shorts-speler + Afspeelsnelheid · %s Standaard afspeelsnelheid gewijzigd naar: %s Gebruik normale snelheid voor muziek Speel muziekvideo\'s altijd af op 1.0x snelheid diff --git a/patches/src/main/resources/addresources/values-or-rIN/youtube/strings.xml b/patches/src/main/resources/addresources/values-or-rIN/youtube/strings.xml index 1a02f7efd7..9079108272 100644 --- a/patches/src/main/resources/addresources/values-or-rIN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-or-rIN/youtube/strings.xml @@ -57,6 +57,7 @@ Second \"item\" text" + diff --git a/patches/src/main/resources/addresources/values-pa-rIN/youtube/strings.xml b/patches/src/main/resources/addresources/values-pa-rIN/youtube/strings.xml index 1a02f7efd7..9079108272 100644 --- a/patches/src/main/resources/addresources/values-pa-rIN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-pa-rIN/youtube/strings.xml @@ -57,6 +57,7 @@ Second \"item\" text" + diff --git a/patches/src/main/resources/addresources/values-pl-rPL/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-pl-rPL/shared-youtube/strings.xml index 45a09a1d23..3d78d27a36 100644 --- a/patches/src/main/resources/addresources/values-pl-rPL/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-pl-rPL/shared-youtube/strings.xml @@ -150,13 +150,13 @@ Aby się zalogować, naciśnij \"Kontynuuj\" poniżej" Ukrywa przyciski Poprzedni i Następny w powiadomieniu o multimediach, na ekranie blokady i w sterowaniu słuchawkami Wyłącz pasek postępu Zapobiega przeciąganiu paska postępu na ekranie blokady i w panelu powiadomień - Szukaj za pomocą przycisków Poprzedni i Następny - Zmienia sterowanie powiadomieniem o mediach, ekranem blokady oraz słuchawkami, aby przenosić zamiast zmieniać filmy - Długość cofania - Ilość sekund do cofnięcia przyciskiem Poprzedni - Długość przewijania do przodu - Ilość sekund do przewinięcia do przodu przyciskiem Następny - Długość przesuwania musi być między 1 a %s sekundy. Użycie domyślnej + Przewijaj przyciskami Poprzedni i Następny + Zmienia działanie przycisków w powiadomieniu multimedialnym, na ekranie blokady oraz na słuchawkach na przewijanie zamiast przełączania filmów + Czas przewijania do tyłu + Liczba sekund przewijania do tyłu przyciskiem Poprzedni + Czas przewijania do przodu + Liczba sekund przewijania do przodu przyciskiem Następny + Czas przewijania musi wynosić od 1 do %s sekund. Użyto wartości domyślnej Wyłącz protokół QUIC diff --git a/patches/src/main/resources/addresources/values-pl-rPL/youtube/strings.xml b/patches/src/main/resources/addresources/values-pl-rPL/youtube/strings.xml index b6c0a2f6bf..39171ef1e9 100644 --- a/patches/src/main/resources/addresources/values-pl-rPL/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-pl-rPL/youtube/strings.xml @@ -435,6 +435,17 @@ Zgłoszenia są publiczne i podlegają weryfikacji przed dodaniem kanału do lis Przytrzymanie filmu w dowolnym miejscu na liście otwiera jego menu Pokaż przycisk \"Do obejrzenia\" na miniaturkach Na miniaturkach filmów na liście wyświetlany jest przycisk \"Do obejrzenia\". Przytrzymanie go otwiera opcję \"Zapisz na playliście\" + + Pobieraj obrazy z postów + Pokazuje przycisk pobierania obok przycisku \"Udostępnij\" pod postami z obrazami lub plikami GIF oraz dodaje opcję pobierania do ich menu + Pobierz obrazy + Pobierz obraz + Pobieranie %s obrazów… + Pobieranie obrazu… + Zapisano %1$s obrazów w %2$s + Zapisano obraz w %s + Nie udało się pobrać obrazu + Nie znaleziono obrazu w tym poście Dodaj opcję \"Zapytaj\" do menu Dodaje opcję podsumowania filmu do menu na liście @@ -474,14 +485,14 @@ Zgłoszenia są publiczne i podlegają weryfikacji przed dodaniem kanału do lis Pokaż przycisk pobierania na nakładce odtwarzacza Wyświetla w odtwarzaczu przycisk otwierający zewnętrzną aplikację do pobierania Szybkie pobieranie po dotknięciu - Wyświetla prośbę o pobieranie w tle. Przytrzymaj, aby otworzyć pobieracz. Działa z YTDLnis i YTindir - Oznaczaj pobrane materiały jako obejrzane - Filmy pobrane za pomocą zewnętrznego pobieracza są dodawane do Twojej historii oglądania na YouTube - Pełna HD (1080p) + Pyta o pobranie w tle. Przytrzymaj, aby otworzyć aplikację do pobierania. Działa z YTDLnis oraz YTindir + Oznaczaj pobrane filmy jako obejrzane + Filmy pobrane za pomocą zewnętrznej aplikacji do pobierania są dodawane do Twojej historii oglądania w YouTube + Full HD (1080p) Średnia (192 kbps) Szybkie pobieranie Film - Audio + Dźwięk Wszystkie opcje Tłumaczenie głosowe @@ -599,11 +610,11 @@ Uwaga: Może nie działać na transmisjach na żywo" Zamień akcje Zapisz i Kolejka Dotknij, aby wyświetlić opcje kolejki. Przytrzymaj, aby zapisać do obejrzenia - Odtwórz jako następny w kolejce - Domyślnie + Typ opcji \"Odtwórz jako następny w kolejce\" + Domyślny Natywna kolejka Kolejka Morphe - Klient koleji Morphe po stronie klienta + Kolejka Morphe (po stronie klienta) Dodaj opcję dodawania do kolejki w wysuwanym menu Dodaje dodatkową pozycję w wysuwanym menu. Włącz tę opcję, jeśli \"Dodaj do kolejki\" nie jest wyświetlane Kolejka @@ -651,19 +662,19 @@ Uwaga: Może nie działać na transmisjach na żywo" Otwórz kolejkę Zapisz kolejkę na playliście - Playlista zapisana - Zapisano do %s + Zapisano playlistę + Zapisano w %s Wyczyść kolejkę - Kolejka wyczyszczona + Wyczyszczono kolejkę Kolejka Odtwórz jako ostatni w kolejce Odtwórz jako następny w kolejce YouTube nie mógł wyświetlić kolejki. - Kolejka nie jest gotowa dla tego filmu. Otwórz odtwarzacz filmów i spróbuj ponownie. - Dodać Mix do kolejki czy obejrzeć Mix? - Obejrzyj + Kolejka nie jest gotowa dla tego filmu. Otwórz odtwarzacz i spróbuj ponownie. + Dodać składankę Mix do kolejki czy ją obejrzeć? + Oglądaj Dodaj do kolejki - Ładowanie Mix… + Wczytywanie składanki Mix… Otwieraj kanał awatara transmisji na żywo Zapobiega otwieraniu transmisji na żywo po dotknięciu awatara kanału @@ -882,6 +893,7 @@ Aby przejść do ustawień, włącz przycisk w menu: Ogólne → Pasek narzędzi Półprzezroczysty Przezroczysty Nieprzezroczysty + Ten styl może nie być obsługiwany przez Twoje urządzenie lub wersję YouTube i może powodować błędy układu lub awarie aplikacji Pasek narzędzi Zdecyduj, które elementy będą wyświetlane na pasku narzędzi Ukryj przycisk Przesyłaj @@ -1108,7 +1120,9 @@ Układ samochodowy "Przywraca stary układ odtwarzacza Ograniczenie: Jeśli opcja \"Przywróć stary pasek akcji filmu\" jest wyłączona, nakładka \"Więcej filmów\" może być niedostępna" - Włączenie tej opcji może popsuć niektóre elementy układu odtwarzacza, takie jak sekcja komentarzy lub \"Więcej filmów\" pod odtwarzaczem + "Włączenie tej opcji może zepsuć niektóre elementy układu odtwarzacza, takie jak sekcja komentarzy czy lista \"Więcej filmów\" wyświetlana pod odtwarzaczem. + +​Układ tabletu nie jest już obsługiwany przez YouTube i w nowszych wersjach aplikacji nie działa w pełni lub wcale. Aby korzystać z działającego układu tabletu, zmodyfikuj YouTube w wersji 20.21.37" 21.11.48 - Przywraca stare zachowanie napisów 20.39.41 - Przywraca stare menu dodawania do playlisty @@ -1376,6 +1390,11 @@ Ograniczenie: Jeśli opcja \"Przywróć stary pasek akcji filmu\" jest wyłączo Wyświetla powiadomienie po zapisaniu nowej domyślnej prędkości odtwarzania Domyślna prędkość odtwarzania Domyślna prędkość odtwarzania w filmach Short + Zapamiętaj prędkość odtwarzania w filmach Short + Stosuje ręcznie wybraną prędkość odtwarzania do wszystkich kolejnych filmów Short. Po wyłączeniu tej opcji prędkość wraca do wartości domyślnej po zamknięciu odtwarzacza Shorts + Pokaż prędkość odtwarzania w menu Shorts + Dodaje opcję \"Prędkość odtwarzania\" do menu odtwarzacza Shorts + Prędkość odtwarzania · %s Zmieniono domyślną prędkość na: %s Odtwarzaj muzykę z normalną prędkością Zawsze odtwarzaj teledyski z prędkością 1x diff --git a/patches/src/main/resources/addresources/values-pt-rBR/youtube/strings.xml b/patches/src/main/resources/addresources/values-pt-rBR/youtube/strings.xml index 0289a678a7..55c2a1d280 100644 --- a/patches/src/main/resources/addresources/values-pt-rBR/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-pt-rBR/youtube/strings.xml @@ -435,6 +435,17 @@ As denúncias são públicas e passam por revisão antes de um canal ser adicion Clicar e segurar em qualquer lugar em um vídeo do feed abre seu menu Mostrar botão \"Assistir mais tarde\" nas miniaturas Um botão \"Assistir mais tarde\" é exibido nas miniaturas dos vídeos do feed. Clicar e segurar nele salva na playlist + + Baixar imagens da publicação + Um botão de download é exibido ao lado do botão Compartilhar de publicações com imagens ou GIFs, e um item de download é adicionado ao menu delas + Baixar imagens + Baixar imagem + Baixando %s imagens… + Baixando imagem… + %1$s imagens salvas em %2$s + Imagem salva em %s + Não foi possível baixar a imagem + Nenhuma imagem encontrada nesta publicação Adicionar item Perguntar ao menu flutuante Adiciona um item de resumo de vídeo ao menu flutuador do feed @@ -882,6 +893,7 @@ Para acessar as Configurações, ative o botão Configurações via Geral → Ba Translúcido Transparente Opaco + Este estilo pode não ser suportado pelo seu dispositivo ou versão do YouTube, e pode causar problemas de layout ou travamentos Barra de ferramentas Escolha quais componentes aparecem na barra de ferramentas Ocultar botão Transmitir @@ -1109,7 +1121,9 @@ Layout automotivo "Restaura o layout antigo do player de vídeo Limitação: Se 'Restaurar barra de ações de vídeo antiga' estiver desativado, a sobreposição 'Mais vídeos' pode não estar disponível" - Habilitar isso pode quebrar alguns recursos de layout do player, como a seção de comentários ou \'Mais vídeos\' exibidos abaixo do player + "Ativar isso pode quebrar alguns recursos de layout do player, como a seção de comentários ou \"Mais vídeos\" exibidos abaixo do player. + +O layout de tablet não é mais suportado pelo YouTube e está parcial ou totalmente quebrado em versões recentes do YouTube. Para um layout de tablet funcional, aplique o patch no YouTube 20.21.37" 21.11.48 - Restaurar comportamento antigo de legendas 20.39.41 - Restaurar velho save para playlist @@ -1377,6 +1391,11 @@ Os vídeos são solicitados por um prefixo curto do ID do vídeo com hash, entã Mostra uma notificação quando uma nova velocidade de reprodução padrão é salva Velocidade padrão de reprodução Velocidade padrão de reprodução nos Shorts + Lembrar a velocidade de reprodução nos Shorts + Aplica a sua seleção manual de velocidade dos Shorts a todos os Shorts futuros. Se estiver desativado, a velocidade retorna ao padrão após fechar o player do Shorts + Mostrar velocidade de reprodução no menu do Shorts + Adiciona um item de Velocidade de reprodução ao menu do player do Shorts + Velocidade de reprodução · %s Velocidade de reprodução padrão alterada para: %s Usar velocidade normal para música Sempre reproduzir videoclipes de música na velocidade de 1.0x diff --git a/patches/src/main/resources/addresources/values-pt-rPT/youtube/strings.xml b/patches/src/main/resources/addresources/values-pt-rPT/youtube/strings.xml index 98537bfae3..e91566688e 100644 --- a/patches/src/main/resources/addresources/values-pt-rPT/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-pt-rPT/youtube/strings.xml @@ -435,6 +435,16 @@ As denúncias são públicas e são analisadas antes de um canal ser adicionado O toque e a segurança em qualquer parte de um vídeo do feed abrem seu menu Mostrar o botão Assistir mais tarde nas miniaturas Um botão Assistir mais tarde é mostrado nas miniaturas dos vídeos do feed. Toque e segure-o para salvar na lista de reprodução + + Descarregar imagens da publicação + Um botão de descarregar é apresentado ao lado do botão Partilhar de publicações com imagens ou GIFs, e um item de descarregar é adicionado ao seu menu + Descarregar imagens + Descarregar imagem + A descarregar %s imagens… + Baixando imagem… + Imagem salva em %s + Não foi possível baixar a imagem + Nenhuma imagem encontrada nesta publicação Adicionar item Perguntar ao menu suspenso Adiciona um item de resumo de vídeo ao menu suspenso do feed @@ -882,6 +892,7 @@ Para acessar as Configurações, ative o botão Configurações em Geral → Bar Translúcido Transparente Opaco + Este estilo pode não ser suportado pelo seu dispositivo ou versão do YouTube, e pode causar problemas de layout ou travamentos Barra de ferramentas Escolha quais componentes aparecem na barra de ferramentas Ocultar botão Transmitir @@ -1109,7 +1120,9 @@ Layout automotivo "Restaura o layout antigo do player Limitação: Se 'Restaurar a barra de ação de vídeo antiga' estiver desativado, a sobreposição de 'Mais vídeos' pode não estar disponível" - Ativar isso pode interromper alguns recursos de layout do player, como a seção de comentários ou \'More videos\' exibidos abaixo do player + "Habilitar isso pode quebrar alguns recursos de layout do player, como a seção de comentários ou \"Mais vídeos\" exibidos abaixo do player. + +O layout de tablet não é mais suportado pelo YouTube e está parcial ou totalmente quebrado em versões recentes do YouTube. Para um layout de tablet funcional, aplique o patch no YouTube 20.21.37" 21.11.48 - Restaurar comportamento antigo de legendas 20.39.41 - Restaurar a versão antiga do Morphe Spoof para a playlist @@ -1377,6 +1390,11 @@ Os vídeos são solicitados por um prefixo curto do ID de vídeo com hash, entã Mostra uma notificação quando uma nova velocidade de reprodução padrão é salva Velocidade de reprodução predefinida Velocidade de reprodução predefinida nos Shorts + Lembrar velocidade de reprodução nos Shorts + Aplica a sua seleção manual de velocidade dos Shorts a todos os Shorts futuros. Se estiver desativado, a velocidade retorna ao padrão após fechar o player de Shorts + Mostrar velocidade de reprodução no menu do Shorts + Adiciona um item de Velocidade de reprodução ao menu do player de Shorts + Velocidade de reprodução · %s Velocidade de reprodução padrão alterada para: %s Usar velocidade normal para música Sempre reproduzir videoclipes de música na velocidade de 1.0x diff --git a/patches/src/main/resources/addresources/values-ro-rRO/youtube/strings.xml b/patches/src/main/resources/addresources/values-ro-rRO/youtube/strings.xml index 4d39b90e73..cda2924a0d 100644 --- a/patches/src/main/resources/addresources/values-ro-rRO/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ro-rRO/youtube/strings.xml @@ -435,6 +435,17 @@ Rapoartele sunt publice și sunt analizate înainte ca un canal să fie adăugat Atingerea și menținerea apăsării oriunde pe un video din feed deschide meniul acestuia Arată butonul „Vizionează mai târziu” pe miniaturi Un buton Vizionează mai târziu este afișat pe miniaturile videoclipurilor din feed. Atingerea și menținerea apăsării asupra lui deschide Opțiuni salvare playlist + + Descarcă imaginile postării + Un buton de descărcare este afișat lângă butonul Share al postărilor cu imagini sau GIF-uri, iar un element de descărcare este adăugat în meniul acestora + Descarcă imaginile + Descarcă imaginea + Se descarcă %s imagini… + Se descarcă imaginea… + %1$s imagini salvate în %2$s + Imaginea a fost salvată în %s + Imaginea nu a putut fi descărcată + Nu s-a găsit nicio imagine în această postare Adăugarea elementului Ask la meniul flyout Adaugă un element de sumarizare video în meniul flyout al șirului de alimentare @@ -882,6 +893,7 @@ Pentru a accesa Setările, activați butonul Setări prin General → Bara de in Translucid Transparent Opac + Acest stil s-ar putea să nu fie suportat de dispozitivul tău sau de versiunea de YouTube și poate cauza probleme de layout sau prăbușiri Bara de navigare Ascunde previzualizarea videoclipului următor în ecran complet înainte de începerea redării automate Ascunde butonul Distribuție @@ -1109,7 +1121,9 @@ Aspect auto "Restabilește aspectul vechi al playerului Limitare: Dacă 'Restaurare bara de acțiuni video veche' este dezactivată, stratul 'Mai multe videoclipuri' ar putea să nu fie disponibil" - Activarea acestei opțiuni poate afecta unele caracteristici ale aspectului player-ului, cum ar fi secțiunea de comentarii sau „Mai multe videoclipuri” afișată sub player + "Activarea acestei opțiuni poate afecta unele funcții de layout ale playerului, cum ar fi secțiunea de comentarii sau „Mai multe videoclipuri” afișate sub player. + +Layout-ul pentru tabletă nu mai este suportat de YouTube și este parțial sau complet defect în versiunile recente de YouTube. Pentru un layout de tabletă funcțional, patch-uiește YouTube 20.21.37" 21.11.48 - Restabilește comportamentul vechi al subtitrărilor 20.39.41 - Restore old save to playlist @@ -1377,6 +1391,11 @@ Videoclipurile sunt solicitate printr-un prefix scurt al ID-ului video cu hash, Afișează o notificare când se salvează o viteză de redare implicită nouă Viteza de redare implicită Viteza de redare implicită pe Shorts + Memorează viteza de redare pentru Shorts + Aplică selecția manuală a vitezei pentru Shorts tuturor clipurilor Shorts viitoare. Dacă este dezactivată, viteza revine la cea implicită după închiderea playerului Shorts + Afișează viteza de redare în meniul Shorts + Adaugă un element de viteză de redare în meniul playerului Shorts + Viteză de redare · %s Viteza de redare implicită a fost modificată la: %s Utilizaţi viteza normală pentru muzică Reda înviteza videoclipurile muzicale în mod constant la 1.0x diff --git a/patches/src/main/resources/addresources/values-ru-rRU/youtube/strings.xml b/patches/src/main/resources/addresources/values-ru-rRU/youtube/strings.xml index 097800599f..aabafe595e 100644 --- a/patches/src/main/resources/addresources/values-ru-rRU/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ru-rRU/youtube/strings.xml @@ -121,7 +121,7 @@ Second \"item\" text" Скрыть миниатюру видео Скрыть метки видео - Скрывает метки, такие как «Автодубляж» / «Дубляж», «Получает комиссию», «Популярное», «Люди также смотрели», «товар(ы)» и «Вам может понравиться» + Скрывает метки типа \'Авто дублирование\' / \'Дублированный\', \'Получает комиссионные\', \'Нашумевший\', \'Люди также смотрели\', \'товар(ы)\' и \'Вам также может понравиться\' Скрыть количество просмотров "Ограничения: @@ -435,6 +435,17 @@ Second \"item\" text" Нажатие и удержание в любом месте видео из ленты открывает его меню Показывать кнопку «Позже» на превью видео На превью видео из ленты отображается кнопка «Позже». Нажатие и удержание открывает сохранение в плейлист + + Скачать изображения из поста + Рядом с кнопкой «Поделиться» в постах с изображениями или GIF-файлами отображается кнопка скачивания, а в их меню добавляется пункт «Скачать» + Скачать изображения + Скачать изображение + Загрузка %s изображений… + Загрузка изображения… + %1$s изображения, сохраненные в %2$s + Изображение сохранено в %s + Не удалось загрузить изображение + В этом посте изображения не найдены Добавить Ask в меню Добавляет элемент сводки видео в меню ленты @@ -884,6 +895,7 @@ Second \"item\" text" Полупрозрачный Прозрачный Непрозрачный + Этот стиль может не поддерживаться вашим устройством или версией YouTube, что может привести к проблемам с разметкой или сбоям Панель инструментов Выберите, какие компоненты отображаются на панели инструментов Скрыть кнопку \"Трансляция\" @@ -1111,7 +1123,9 @@ Second \"item\" text" "Восстанавливает старый макет плеера Ограничение: Если «Восстановить старую панель управления видео» отключена, наложение «Ещё видео» может быть недоступно" - Включение этой функции может нарушить некоторые элементы макета проигрывателя, такие как раздел комментариев или «Ещё видео», показанные ниже проигрывателя + "Включение этой функции может нарушить работу некоторых элементов интерфейса плеера, таких как раздел комментариев или блок «Другие видео» под плеером. + +Планшетный интерфейс больше не поддерживается YouTube и частично или полностью не работает в последних версиях YouTube. Для корректной работы планшетного интерфейса используйте патч для YouTube 20.21.37" 21.11.48 - Восстановить прежнее поведение субтитров 20.39.41 - Восстановить старое сохранение в список воспроизведения @@ -1379,6 +1393,11 @@ Second \"item\" text" Показывает уведомление при сохранении новой скорости воспроизведения по умолчанию Скорость воспроизведения по умолчанию Скорость воспроизведения по умолчанию для Shorts + Запоминать скорость воспроизведения в Shorts + Применяет выбранную вами скорость воспроизведения Shorts ко всем последующим Shorts. Если функция отключена, скорость возвращается к значению по умолчанию после закрытия плеера Shorts + Показывать скорость воспроизведения в меню Shorts + Добавляет пункт \"Скорость воспроизведения\" в меню плеера Shorts + Скорость воспроизведения · %s Изменена скорость воспроизведения по умолчанию на: %s Использовать нормальную скорость для музыки Всегда воспроизводить музыкальные видео на скорости 1.0x diff --git a/patches/src/main/resources/addresources/values-si-rLK/music/strings.xml b/patches/src/main/resources/addresources/values-si-rLK/music/strings.xml index ccfea4ed71..39f59fbccd 100644 --- a/patches/src/main/resources/addresources/values-si-rLK/music/strings.xml +++ b/patches/src/main/resources/addresources/values-si-rLK/music/strings.xml @@ -17,7 +17,16 @@ Second \"item\" text" --> + අවසන් ධාවනය + ග්‍රාහකත්වයන් + දෘශ්‍ය පට + වෙනත් ප්‍රතිඵල + උඩුගත කළ ගීත + උඩුගත කළ ඇල්බම් + උඩුගත කළ කලාකරුවන් + නැවත්වීමට සහ නැවත පටන් ගැනීමට host එකේ Jam patch එක යාවත්කාලින කරන්න + Jam පෝලිම් හවුල්කරණය diff --git a/patches/src/main/resources/addresources/values-si-rLK/youtube/strings.xml b/patches/src/main/resources/addresources/values-si-rLK/youtube/strings.xml index 1a02f7efd7..9079108272 100644 --- a/patches/src/main/resources/addresources/values-si-rLK/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-si-rLK/youtube/strings.xml @@ -57,6 +57,7 @@ Second \"item\" text" + diff --git a/patches/src/main/resources/addresources/values-sk-rSK/youtube/strings.xml b/patches/src/main/resources/addresources/values-sk-rSK/youtube/strings.xml index f49db73f57..fbb850780c 100644 --- a/patches/src/main/resources/addresources/values-sk-rSK/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-sk-rSK/youtube/strings.xml @@ -435,6 +435,16 @@ Hlásenia sú verejné a pred pridaním kanála do zoznamu sa posudzujú. Odstr Klepnutím a podrž kdekoľvek na videu v feedi sa otvorí jeho ponuka Zobraziť tlačidlo „Pozrieť neskôr“ na miniatúrach Na miniatúrach videí vo feedi sa zobrazuje tlačidlo Pozrieť neskôr. Klepnutím a podrž ho otvoríš Uloženie do playlistu + + Stiahnuť obrázky z príspevkov + Vedľa tlačidla zdieľať príspevkov s obrázkami alebo GIFmi sa zobrazí tlačidlo sťahovania a do ich menu sa pridá položka sťahovania + Stiahnuť obrázky + Stiahnuť obrázok + Sťahovanie %s obrázkov… + Sťahovanie obrázka… + Obrázok uložený do %s + Nebolo možné sťahovať obrázok + V tomto príspevku nebol nájdený žiadny obrázok Pridať položku Opýtať sa do rozbaľovacieho menu Pridá položku na zhrnutie videa do rozbaľovacieho menu predplatného @@ -882,6 +892,7 @@ Ak chcete získať prístup k Nastaveniam, povoľte tlačidlo Nastavenia cez Vš Polopriehľadný Priehľadný Nepriehľadný + Tento štýl nemusí byť podporovaný vaším zariadením alebo verziou YouTube, čo môže spôsobiť problémy s rozložením alebo pády Panel nástrojov Vyberte, ktoré komponenty sa zobrazia v lište nástrojov Skryť tlačidlo Prenos @@ -1108,7 +1119,9 @@ Rozloženie pre automobil "Obnoví staré rozloženie prehrávača Obmedzenie: Ak je vypnutá možnosť 'Obnoviť starú lištu akcií videa', vrstva 'Ďalšie videá' nemusí byť k dispozícii" - Povolenie tejto možnosti môže narušiť niektoré funkcie rozloženia prehrávača, ako je sekcia komentárov alebo \"Viac videí\" zobrazených pod prehrávačom + "Povolenie tejto možnosti môže narušiť niektoré funkcie rozloženia prehrávača, ako napríklad sekciu komentárov alebo „Ďalšie videá“ zobrazené pod prehrávačom. + +Rozloženie pre tablety už YouTube nepodporuje a v novších verziách YouTube je čiastočne alebo úplne nefunkčné. Pre fungujúce rozloženie pre tablety použite patch pre YouTube 20.21.37" 21.11.48 - Obnoviť staré správanie titulkov 20.39.41 - Obnoví staré ukladanie do zoznamu videí @@ -1376,6 +1389,11 @@ Videá sú vyžadované krátkym prefixom hashed video ID, takže server nemôž Zobrazí upozornenie, keď sa uloží nová predvolená rýchlosť prehrávania Predvolená rýchlosť prehrávania Predvolená rýchlosť prehrávania na Shorts + Zapamätať rýchlosť prehrávania v Shorts + Aplikuje váš manuálny výber rýchlosti Shorts na všetky budúce Shorts. Ak je vypnuté, rýchlosť sa po zatvorení prehrávača Shorts vráti na predvolenú + Zobrazovať rýchlosť prehrávania v menu Shorts + Pridáva položku Rýchlosť prehrávania do menu prehrávača Shorts + Rýchlosť prehrávania · %s Predvolená rýchlosť prehrávania bola zmenená na: %s Použite normálnu rýchlosť pre hudbu Hudobné videá vždy prehrajte rýchlosťou 1.0x diff --git a/patches/src/main/resources/addresources/values-sl-rSI/youtube/strings.xml b/patches/src/main/resources/addresources/values-sl-rSI/youtube/strings.xml index 9842d48292..1652292e59 100644 --- a/patches/src/main/resources/addresources/values-sl-rSI/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-sl-rSI/youtube/strings.xml @@ -435,6 +435,16 @@ Prijave so javne in so pregledane, preden je kanal dodan na seznam. Odstranitev Če povsod na videu v toku dotaknete se in držite, se odpre njegov meni Prikaži gumb »Ogled kasneje« na sličicah Na sličicah video posnetkov v toku je prikazan gumb Ogled kasneje. Z dotikom in držanjem odprete Shrani v seznam predvajanja + + Prenesi slike objav + Poleg gumba za deljenje objav s slikami ali GIF-i se prikaže gumb za prenos, v njihov meni pa se doda možnost prenosa + Prenesi slike + Prenesi sliko + Prenašanje %s slik… + Prenašanje slike… + Slika shranjena v %s + Slike ni bilo mogoče prenesti + V tem objavi ni bilo najdene slike Dodaj element Zastavi v meni za hitro klicanje V meni za hitro klicanje glavnega toka dodajte element za povzetek videa @@ -882,6 +892,7 @@ Za dostop do Nastavitev omogočajte gumb Nastavitve prek Splošno → Orodna vrs Poltransparentno Transparentno Neprozorno + Ta slog morda ni podprt v vaši napravi ali različici YouTube, kar lahko povzroči težave s pomizbo ali sesuve Orodje. Skrij podnapisna stopala menija \'Zvok\' Skrij gumb za prenos @@ -1109,7 +1120,9 @@ Postavitev za avtomobile "Ponastavi prejšnjo postavitev predvajalnika Omejitev: Če je možnost »Ponastavi staro vrstico za videoposnetke« izklopljena, »Več videoposnetkov« morda ne bo na voljo" - Omogočanje tega lahko moti nekatere značilnosti postavitve predvajalnika, kot je odsek za komentarje ali »Več videoposnetkov« pod predvajalnikom + "Omogočitev tega lahko povzroči težave s nekaterimi funkcijami pomizbe predvajalnika, kot so razdelek s komentarji ali \"Več videov\", ki se prikazujejo pod predvajalnikom. + +Pomizba za tablične računalnike ni več podprta v YouTube in je v novjših različicah YouTube deloma ali popolnoma neuporabna. Za delujočo pomizbo za tablične računalnike uporabite popravek za YouTube 20.21.37" 21.11.48 - Obnovi staro obnašanje podnapisov 20.39.41 - Ponastavi staro shranjevanje v seznam predvajanja @@ -1377,6 +1390,11 @@ Videoposnetki so zahtevani s kratko predpono zgoščenega ID-ja videoposnetka, t Prikaže obvestilo, ko se shrani nova privzeta hitrost predvajanja Privzeta hitrost predvajanja Privzeta hitrost predvajanja pri kratkih videoposnetkih + Zapomni si hitrost predvajanja v Shorts + Uporabi vaš ročni izbor hitrosti v Shorts za vse prihodnje Shorts. Če je ta možnost izklopljena, se hitrost po zaprtju predvajalnika Shorts vrne na privzeto. + Prikaži hitrost predvajanja v meniju Shorts + K meniju predvajalnika Shorts doda možnost hitrosti predvajanja + Hitrost predvajanja · %s Spremenjena privzeta hitrost predvajanja na: %s Uporabi normalno hitrost za glasbo Vedno predvajaj glasbene videospote s hitrostjo 1,0x diff --git a/patches/src/main/resources/addresources/values-sr-rCS/youtube/strings.xml b/patches/src/main/resources/addresources/values-sr-rCS/youtube/strings.xml index b10cb9d376..37d9398a6d 100644 --- a/patches/src/main/resources/addresources/values-sr-rCS/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-sr-rCS/youtube/strings.xml @@ -435,6 +435,16 @@ Prijave su javne i pregledaju se pre nego što kanal bude dodat na listu. Uklanj Dodirivanjem i držanjem bilo gde na videu u protoku otvara se njegov meni Prikaži dugme „Gledaj kasnije“ na sličicama Dugme „Gledaj kasnije“ se prikazuje na sličicama videa u protoku. Dodirivanjem i držanjem njega otvara se opcija Sačuvaj u listu za reprodukciju + + Preuzmi slike iz objave + Dugme za preuzimanje se prikazuje pored dugmeta za deljenje objava sa slikama ili GIF-ovima, a stavka za preuzimanje je dodata u njihov meni + Preuzmi slike + Preuzmi sliku + Preuzimanje %s slika… + Preuzimanje slike… + Slika je sačuvana u %s + Nije bilo moguće preuzeti sliku + U ovoj objavi nije pronađena slika Dodajte stavku Ask u flyout meni Dodaje stavku za brzo pregledavanje videa u feed flyout meniju @@ -882,6 +892,7 @@ Da biste pristupili Podešavanjima, omogućite dugme Podešavanja putem General Polutransparentno Transparentno Neprozirno + Ovaj stil možda nije podržan na vašem uređaju ili verziji YouTube-a, i može uzrokovati probleme sa rasporedom ili rušenje aplikacije Traka za alatke Izaberite koje komponente se pojavljuju u padajućem meniju Sakrij dugme „Prebacuj” @@ -1109,7 +1120,9 @@ Raspored za automobil "Vraća stari raspored plejera Ograničenje: Ako je \"Vrati staru traku sa akcijama za video\" isključen, prekrivanje \"Više video zapisa\" možda neće biti dostupno" - Omogućavanje ovog možda će pokvariti neke funkcije rasporeda plejera, kao što su odeljak za komentare ili \'Još videa\' prikazani ispod plejera + "Omogućavanjem ovoga mogu se pokvariti neke funkcije rasporeda plejera, kao što su odeljak sa komentarima ili \"Više video zapisa\" koji se prikazuju ispod plejera. + +Raspored za tablete više nije podržan od strane YouTube-a i delimično ili potpuno je pokvaren u novijim verzijama YouTube-a. Za funkcionalan raspored za tablete, instalirajte zakrpu za YouTube 20.21.37" 21.11.48 - Vrati staro ponašanje titlova 20.39.41 - Vraća stare ikonice Shorts plejera @@ -1377,6 +1390,11 @@ Video snimci se zahtevaju kratkim prefiksom hešovanog ID-ja, tako da server ne Prikazuje obaveštenje kada je sačuvana nova podrazumevana brzina reprodukcije Podrazumevana brzina reprodukcije Podrazumevana brzina reprodukcije na Shorts + Zapamti brzinu reprodukcije na Shorts + Primenjuje vaš ručni izbor brzine Shorts na sve buduće Shorts. Ako je isključeno, brzina se vraća na podrazumevanu nakon zatvaranja Shorts plejera + Prikaži brzinu reprodukcije u Shorts meniju + Dodaje stavku za brzinu reprodukcije u meni Shorts plejera + Brzina reprodukcije · %s Podrazumevana brzina reprodukcije je promenjena u: %s Koristite normalnu brzinu za muziku Uvek puštajte muzičke spotove brzinom 1.0x diff --git a/patches/src/main/resources/addresources/values-sr-rSP/youtube/strings.xml b/patches/src/main/resources/addresources/values-sr-rSP/youtube/strings.xml index e916e2aee8..51fcbc4f2b 100644 --- a/patches/src/main/resources/addresources/values-sr-rSP/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-sr-rSP/youtube/strings.xml @@ -435,6 +435,17 @@ Second \"item\" text" Дођување и држање било где на видео снимку у фејду отвара његово мени Прикажи дугме „Гледај касније“ на сличицима Дугме Гледај касније се приказује на сличицама видео снимака у фејду. Дођување и држање отвара опчују Чувај у плејлист + + Преузми слике објаве + Дугме за преузимање се приказује поред дугмета за дељење објава са сликама или GIF-овима, а ставка за преузимање је додата у њихов мени + Преузми слике + Преузми слику + Преузимање %s слика… + Преузимање слике… + %1$s слика сачувано у %2$s + Слика је сачувана у %s + Није било могућности да се преузме слика + У овој објави нема слика Додај ставку Питај у изборни мени Додаје ставку за сумирање видеа у изборни мени канала @@ -880,6 +891,7 @@ Second \"item\" text" Полупрозирно Прозирно Непрозирно + Овај стил можда није подржан на вашем уређају или верзији YouTube-а, и може изазвати проблеме са изгледајом или рушење апликације Traka za alatke Одаберите које компоненте ће се појавити у траци са алаткама. Сакриј дугме „Пребацуј” @@ -1092,7 +1104,9 @@ Second \"item\" text" "Враћа стари изглед плејера Ограничење: Ако је „Враћање старе траке акција видеа“ искључено, преклапање „Више видеа“ можда неће бити доступно" - Омогућавање овога може покварити неке функције распореда плејера, попут секције коментара или „More videos” испод плејера + "Омогућавање овога може покварити неке функције изгледаја плејера, као што су одељак за коментаре или „Више видео записа“ приказани испод плејера. + +YouTube више не подржава таблет изглед, и он је делимично или потпуно некоректен у новијим верзијама YouTube-а. За функционалан таблет изглед, патчујте YouTube 20.21.37" 21.11.48 - Врати старо понашање титлова 20.39.41 - Враћа старе иконице плејера @@ -1360,6 +1374,11 @@ Second \"item\" text" Приказује обавештење када се сачува нова подразумевана брзина репродукције Подразумевана брзина репродукције Подразумевана брзина репродукције на Short-овима + Запамти брзину репродукције на Shorts + Примењује ваш ручни избор брзине Shorts на све будуће Shorts. Ако је искључено, брзина се враћа на подразмештану након затварања Shorts плејера + Прикажи брзину репродукције у Shorts менију + Додаје ставку Брзина репродукције у мени Shorts плејера + Брзина репродукције · %s Брзина репродукције је промењена на: %s Koristite normalnu brzinu za muziku Uvek puštajte muzičke spotove brzinom 1.0x diff --git a/patches/src/main/resources/addresources/values-sv-rSE/youtube/strings.xml b/patches/src/main/resources/addresources/values-sv-rSE/youtube/strings.xml index d396160e69..f66d6a1952 100644 --- a/patches/src/main/resources/addresources/values-sv-rSE/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-sv-rSE/youtube/strings.xml @@ -435,6 +435,16 @@ Rapporter är offentliga och granskas innan en kanal läggs till i en lista. Bor Genom att trycka och hålla någonstans på en flödesvideo öppnas dess meny Visa knappen \"Titta senare\" på miniatyrbilder En \"Titta senare\"-knapp visas på miniatyrbilderna för flödesvideor. Genom att trycka och hålla öppnas Spara i spellista + + Ladda ned inläggsbilder + En nedladdningsknapp visas bredvid Dela-knappen i inlägg med bilder eller GIF-bilder, och ett nedladdningsalternativ läggs till i deras meny + Ladda ned bilder + Ladda ned bild + Laddar ned %s bilder … + Laddar ned bild… + Bilden sparades i %s + Det gick inte att ladda ned bilden + Ingen bild hittades i det här inlägget Lägg till Ask-objekt i flygutmatningsmenyn Lägger till ett objekt för sammanfattning av video i flödesflygutmatningsmenyn @@ -882,6 +892,7 @@ För att komma åt Inställningar aktiverar du Inställningsknappen via Allmänt Genomskinlig Transparent Opak + Den här stilen stöds eventuellt inte av din enhet eller YouTube-version, vilket kan orsaka layoutproblem eller krascher Verktygsfält Välj vilka komponenter som visas i verktygsfältet Dölj knappen Casta @@ -1109,7 +1120,9 @@ Billayout "Återställer den gamla spelarlayouten Begränsning: Om 'Återställ gammalt videoåtgärdsfält' är avstängt kan överlägget 'Fler videor' inte vara tillgängligt" - Att aktivera detta kan störa vissa funktioner för uppsättningslayouten, såsom kommentarssektionen eller \"Fler videor\" som visas under spelaren + "Om detta aktiveras kan vissa layoutfunktioner i spelaren sluta fungera, till exempel kommentarsfältet eller \"Fler videor\" som visas under spelaren. + +Surfplattelayout stöds inte längre av YouTube och är delvis eller helt trasig i nyare versioner av YouTube. För en fungerande surfplattelayout, patcha YouTube 20.21.37" 21.11.48 - Återställ det gamla undertextbeteendet 20.39.41 – Återställ gamla Spara i spellista @@ -1377,6 +1390,11 @@ Videor begärs med ett kort prefix av den hashade videons ID, så servern kan in Visar ett meddelande när en ny standarduppspelningshastighet sparas Standarduppspelningshastighet Standarduppspelningshastighet för Shorts + Kom ihåg uppspelningshastighet för Shorts + Använder ditt manuella val av hastighet för Shorts på alla framtida Shorts. Om funktionen är avstängd återgår hastigheten till standard efter att Shorts-spelaren stängts + Visa uppspelningshastighet i menyn för Shorts + Lägger till ett alternativ för uppspelningshastighet i menyn för Shorts-spelaren + Uppspelningshastighet · %s Ändrade standardhastigheten till: %s Använd normal hastighet för musik Spela alltid musikvideor i 1,0x hastighet diff --git a/patches/src/main/resources/addresources/values-sw-rKE/youtube/strings.xml b/patches/src/main/resources/addresources/values-sw-rKE/youtube/strings.xml index 1a02f7efd7..9079108272 100644 --- a/patches/src/main/resources/addresources/values-sw-rKE/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-sw-rKE/youtube/strings.xml @@ -57,6 +57,7 @@ Second \"item\" text" + diff --git a/patches/src/main/resources/addresources/values-ta-rIN/youtube/strings.xml b/patches/src/main/resources/addresources/values-ta-rIN/youtube/strings.xml index a4923027ed..b2967a39aa 100644 --- a/patches/src/main/resources/addresources/values-ta-rIN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ta-rIN/youtube/strings.xml @@ -67,6 +67,7 @@ Second \"item\" text" + diff --git a/patches/src/main/resources/addresources/values-te-rIN/youtube/strings.xml b/patches/src/main/resources/addresources/values-te-rIN/youtube/strings.xml index c5cf010858..e11c4f4775 100644 --- a/patches/src/main/resources/addresources/values-te-rIN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-te-rIN/youtube/strings.xml @@ -63,6 +63,7 @@ Second \"item\" text" + diff --git a/patches/src/main/resources/addresources/values-th-rTH/youtube/strings.xml b/patches/src/main/resources/addresources/values-th-rTH/youtube/strings.xml index 631f1d11ac..2108b78c25 100644 --- a/patches/src/main/resources/addresources/values-th-rTH/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-th-rTH/youtube/strings.xml @@ -435,6 +435,17 @@ Second \"item\" text" การแตะและค้างไว้ที่ใดก็ได้บนวิดีโอฟีดจะเปิดเมนูของวิดีโอนั้น แสดงปุ่มดูภายหลังบนภาพย่อ จะแสดงปุ่มดูภายหลังบนภาพย่อของวิดีโอในฟีด การแตะและค้างไว้จะทำให้บันทึกไปยังเพลย์ลิสต์ + + ดาวน์โหลดรูปภาพในโพสต์ + ปุ่มดาวน์โหลดจะแสดงขึ้นถัดจากปุ่มแชร์ของโพสต์ที่มีรูปภาพหรือ GIF และจะมีการเพิ่มรายการดาวน์โหลดลงในเมนูของโพสต์ดังกล่าว + ดาวน์โหลดรูปภาพ + ดาวน์โหลดรูปภาพ + กำลังดาวน์โหลดรูปภาพ %s รูป… + กำลังดาวน์โหลดรูปภาพ… + บันทึกรูปภาพ %1$s รูปไปยัง %2$s + บันทึกรูปภาพไปยัง %s แล้ว + ไม่สามารถดาวน์โหลดรูปภาพได้ + ไม่พบรูปภาพในโพสต์นี้ เพิ่มรายการ Ask ไปยังเมนูแบบเลื่อนออก เพิ่มรายการสรุปวิดีโอไปยังเมนูแบบเลื่อนออกของฟีด @@ -600,7 +611,7 @@ Second \"item\" text" แตะเพื่อดูตัวเลือกในคิว แตะค้างไว้เพื่อบันทึกไปยังดูภายหลัง ประเภทเล่นถัดไปในคิว - ค่าเริ่มต้นThe + ค่าเริ่มต้น คิวแบบเนทีฟ คิว Morphe ไคลเอนต์ฝั่งไคลเอนต์ของคิว Morphe @@ -882,6 +893,7 @@ Second \"item\" text" โปร่งแสง โปร่งใส ทึบแสง + สไตล์นี้อาจไม่รองรับโดยอุปกรณ์หรือ YouTube เวอร์ชันของคุณ และอาจทำให้เกิดปัญหาด้านเลย์เอาต์หรือแอปค้างได้ แถบเครื่องมือ เลือกส่วนประกอบที่จะปรากฏในแถบเครื่องมือ ซ่อนปุ่มแคสต์ @@ -1109,7 +1121,9 @@ Second \"item\" text" "คืนรูปแบบเครื่องเล่นแบบเก่า ข้อจำกัด: หากปิด 'คืนแถบการทำงานของวิดีโอแบบเก่า' ภาพซ้อนทับ 'วิดีโอเพิ่มเติม' อาจใช้งานไม่ได้" - การเปิดใช้นี้อาจทำให้บางฟีเจอร์เลย์เอาต์ของเครื่องเล่นเสีย เช่น ส่วนความคิดเห็นหรือ \'วิดีโอเพิ่มเติม\' ที่แสดงด้านล่างเครื่องเล่น + "การเปิดใช้งานสิ่งนี้อาจทำให้คุณสมบัติบางอย่างของเลย์เอาต์เครื่องเล่นเสียหาย เช่น ส่วนความคิดเห็นหรือ 'วิดีโอเพิ่มเติม' ที่แสดงด้านล่างเครื่องเล่น + +เลย์เอาต์สำหรับแท็บเล็ตไม่ได้รับการรองรับโดย YouTube อีกต่อไป และอาจใช้งานไม่ได้บางส่วนหรือทั้งหมดใน YouTube เวอร์ชันล่าสุด สำหรับเลย์เอาต์แท็บเล็ตที่ใช้งานได้ ให้แพตช์ YouTube 20.21.37" 21.11.48 - กู้คืนพฤติกรรมคำบรรยายแบบเดิม 20.39.41 - คืนค่าไอคอนเครื่องเล่น Shorts เก่า @@ -1377,6 +1391,11 @@ Second \"item\" text" แสดงการแจ้งเตือนเมื่อบันทึกความเร็วในการเล่นเริ่มต้นใหม่ ความเร็วในการเล่นเริ่มต้น ความเร็วในการเล่นเริ่มต้นบน Shorts + จำความเร็วในการเล่นบน Shorts + ใช้ความเร็วของ Shorts ที่คุณเลือกด้วยตนเองกับ Shorts ทั้งหมดในอนาคต หากปิดไว้ ความเร็วจะกลับเป็นค่าเริ่มต้นหลังจากปิดเครื่องเล่น Shorts + แสดงความเร็วในการเล่นในเมนู Shorts + เพิ่มรายการความเร็วในการเล่นในเมนูเครื่องเล่น Shorts + ความเร็วในการเล่น · %s เปลี่ยนความเร็วในการเล่นเริ่มต้นเป็น: %s ใช้ความเร็วปกติสำหรับเพลง เล่นวิดีโอเพลงด้วยความเร็ว 1.0x เสมอ diff --git a/patches/src/main/resources/addresources/values-tr-rTR/youtube/strings.xml b/patches/src/main/resources/addresources/values-tr-rTR/youtube/strings.xml index f8ca8efe52..44f8acd5a4 100644 --- a/patches/src/main/resources/addresources/values-tr-rTR/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-tr-rTR/youtube/strings.xml @@ -436,6 +436,17 @@ Raporlar herkese açıktır ve bir kanal listeye eklenmeden önce incelenir. Kal Akış video üzerinde herhangi bir yere dokunup basılı tutmak, menüyü açar Küçük resimlerde Daha Sonra İzle düğmesini göster Akış videolarının küçük resimlerinde bir Daha Sonra İzle düğmesi gösterilir. Dokunulup basılı tutulduğunda Çalma listesine kaydet açılır + + Gönderi görsellerini indir + Görsel veya GIF içeren gönderilerin Paylaş düğmesinin yanında bir indirme düğmesi gösterilir ve menülerine bir indirme öğesi eklenir + Görselleri indir + Görseli indir + %s görsel indiriliyor… + Görsel indiriliyor… + %1$s görsel %2$s konumuna kaydedildi + Görsel %s konumuna kaydedildi + Görsel indirilemedi + Bu gönderide görsel bulunamadı Soru ögesini açılır menüye ekle Akış açılır menüsüne bir video özetlendirme ögesi ekler @@ -883,6 +894,7 @@ Ayarlar'a erişmek için Ayarlar düğmesini Genel → Araç Çubuğu → Ayarla Yarı saydam Şeffaf Opak + Bu stil, cihazınız veya YouTube sürümünüz tarafından desteklenmiyor olabilir; bu durum yerleşim sorunlarına veya çökmelere neden olabilir Araç Çubuğu Araç çubuğunda hangi bileşenlerin görüneceğini seçin Yayınla düğmesini gizle @@ -1110,7 +1122,9 @@ Otomotiv düzeni "Eski oynatıcı düzenini geri yükler Sınırlamalar: 'Eski video eylem çubuğunu geri yükle' kapatılırsa, 'Daha fazla video' katmanı kullanılamayabilir" - Bunu etkinleştirmek, yorum bölümü veya oynatıcının altında gösterilen \"Daha fazla video\" gibi bazı oynatıcı düzeni özelliklerini bozabilir + "Bunu etkinleştirilmek, oynatıcının altında gösterilen yorumlar veya 'Daha fazla video' kısımları gibi bazı oynatıcı düzen özelliklerini bozabilir. + +Tablet düzeni artık YouTube tarafından desteklenmiyor ve güncel YouTube sürümlerinde kısmen veya tamamen bozulmuş durumda. Çalışan bir tablet düzeni için, Youtube 20.21.37'yi yamalayın" 21.11.48 - Eski altyazı davranışını geri yükle 20.39.41 - Oynatma listesine kaydetme için eski düzeni geri getir @@ -1378,6 +1392,11 @@ Videolar, karma video kimliğinin kısa bir önekiyle istenir; bu nedenle sunucu Yeni bir varsayılan oynatma hızı kaydedildiğinde bir bildirim gösterir Varsayılan oynatma hızı Shorts\'ta varsayılan oynatma hızı + Shorts\'ta oynatma hızını hatırla + Manuel Shorts hız seçimini gelecekteki tüm Shorts\'lara uygular. Kapalıysa, Shorts oynatıcısı kapatıldıktan sonra hız varsayılana döner + Oynatma hızını Shorts menüsünde göster + Shorts oynatıcı menüsüne bir Oynatma hızı öğesi ekler + Oynatma hızı · %s Varsayılan oynatma hızı %s olarak değiştirildi Müzik için normal hızı kullan Müzik videolarını her zaman 1.0x hızında oynat diff --git a/patches/src/main/resources/addresources/values-uk-rUA/youtube/strings.xml b/patches/src/main/resources/addresources/values-uk-rUA/youtube/strings.xml index 5e8e39502e..9e3b1c0608 100644 --- a/patches/src/main/resources/addresources/values-uk-rUA/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-uk-rUA/youtube/strings.xml @@ -435,6 +435,15 @@ Second \"item\" text" Натискання та утримування будь-де на відео в стрічці відкриває його меню Показувати кнопку \"Дивитися пізніше\" на мініатюрах Кнопка \"Дивитися пізніше\" показується на мініатюрах відео в стрічці. Натискання та утримування її відкриває \"Зберегти в список відтворення\" + + Завантажити зображення допису + Поруч із кнопкою «Поділитися» в допистах із зображеннями або GIF відображається кнопка завантаження, а в їхнє меню додається пункт завантаження + Завантажити зображення + Завантажити зображення + Завантаження зображення… + Зображення збережено у %s + Не вдалося завантажити зображення + У цьому дописі не знайдено зображень Додати пункт \"Запитати\" у спливне меню Додає пункт підсумовування відео до спливного меню стрічки @@ -474,9 +483,9 @@ Second \"item\" text" Показувати кнопку завантаження в оверлеї плеєра Показує кнопку у відеоплеєрі, яка відкриває ваш зовнішній завантажувач Швидке завантаження одним натисканням - Запитує про завантаження у фоновому режимі. Натисніть і утримуйте, щоб відкрити програму для завантажень. Працює з YTDLnis та YTindir - Позначати завантажені як переглянуті - Відео, завантажені зовнішньою програмою для завантажень, додаються до вашої історії переглядів YouTube + Запити на завантаження у фоновому режимі. Натисніть і утримуйте, щоб відкрити завантажувач. Працює з YTDLnis та YTindir + Позначити завантаження як переглянуті + Відео, завантажені за допомогою зовнішнього завантажувача, додаються до історії переглядів YouTube Full HD (1080p) Середнє (192 кбіт/с) Швидке завантаження @@ -882,6 +891,7 @@ Second \"item\" text" Напівпрозорий Прозорий Непрозорий + Цей стиль може не підтримуватися вашим пристроєм або версією YouTube, що може призвести до проблем із макетом або збоїв Панель інструментів Виберіть, які компоненти будуть показуватися на панелі інструментів Приховати кнопку \"Трансляція\" @@ -1108,7 +1118,9 @@ Second \"item\" text" "Відновлює старий макет плеєра Обмеження: якщо \"Відновити стару панель дій відео\" вимкнено, накладка \"Інші відео\" може бути недоступна" - Увімкнення цієї функції може порушити роботу деяких елементів компонування плеєра, як-от розділ коментарів або \"Інші відео\" під плеєром + "Увімкнення цієї функції може порушити деякі елементи макета плеєра, такі як розділ коментарів або «Більше відео», що відображаються під плеєром. + +Макет для планшетів більше не підтримується YouTube і частково або повністю не працює в останніх версіях YouTube. Для коректної роботи макета для планшетів встановіть патч для YouTube 20.21.37" 21.11.48 - Відновити стару поведінку субтитрів 20.39.41 - Відновити старе збереження до списку відтворення @@ -1376,6 +1388,11 @@ Second \"item\" text" Показує сповіщення при збереженні нової типової швидкості відтворення Типова швидкість відтворення Типова швидкість відтворення в Shorts + Запам\'ятовувати швидкість відтворення в Shorts + Застосовує ваш ручний вибір швидкості Shorts до всіх наступних Shorts. Якщо вимкнено, швидкість повертається до стандартної після закриття плеєра Shorts + Показувати швидкість відтворення в меню Shorts + Додає пункт «Швидкість відтворення» до меню плеєра Shorts + Швидкість відтворення · %s Типову швидкість відтворення змінено на %s Використовувати звичайну швидкість для музики Завжди відтворювати музичні відео зі швидкістю 1.0x diff --git a/patches/src/main/resources/addresources/values-ur-rIN/youtube/strings.xml b/patches/src/main/resources/addresources/values-ur-rIN/youtube/strings.xml index 69d46a7cf7..3bd9dd0ecc 100644 --- a/patches/src/main/resources/addresources/values-ur-rIN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ur-rIN/youtube/strings.xml @@ -57,6 +57,7 @@ Second \"item\" text" + diff --git a/patches/src/main/resources/addresources/values-uz-rUZ/youtube/strings.xml b/patches/src/main/resources/addresources/values-uz-rUZ/youtube/strings.xml index 8a6628ace2..31740c4cab 100644 --- a/patches/src/main/resources/addresources/values-uz-rUZ/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-uz-rUZ/youtube/strings.xml @@ -324,6 +324,7 @@ Cheklovlar: \'Video havolasini nuqul qilish\' tugmasining harakatlarini teskari tartibda o\'zgartiradi + diff --git a/patches/src/main/resources/addresources/values-vi-rVN/youtube/strings.xml b/patches/src/main/resources/addresources/values-vi-rVN/youtube/strings.xml index a81d850356..f3095ca641 100644 --- a/patches/src/main/resources/addresources/values-vi-rVN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-vi-rVN/youtube/strings.xml @@ -435,6 +435,17 @@ Báo cáo được công khai và được xem xét trước khi một kênh đ Chạm và giữ ở bất kỳ đâu trên video nguồn sẽ mở menu của video đó Hiển thị nút Xem sau trên hình thu nhỏ Nút Xem sau được hiển thị trên hình thu nhỏ của các video nguồn. Chạm và giữ vào nút này để lưu vào danh sách phát + + Tải hình ảnh bài đăng + Một nút tải xuống sẽ hiển thị cạnh nút Chia sẻ của các bài đăng có hình ảnh hoặc GIF, và một mục tải xuống sẽ được thêm vào menu của chúng + Tải các hình ảnh + Tải hình ảnh + Đang tải %s hình ảnh… + Đang tải hình ảnh… + %1$s hình ảnh đã được lưu vào %2$s + Hình ảnh đã được lưu vào %s + Không thể tải hình ảnh + Không tìm thấy hình ảnh nào trong bài viết này Thêm mục Hỏi vào menu bay ra Thêm mục tóm tắt video vào menu bay ra của nguồn cấp dữ liệu @@ -882,6 +893,7 @@ Cần thiết lập \"Cookie phụ đề\"" Bán trong suốt Trong suốt Đục + Kiểu này có thể không được thiết bị hoặc phiên bản YouTube của bạn hỗ trợ, và có thể gây ra sự cố bố cục hoặc treo ứng dụng Thanh công cụ Chọn những thành phần sẽ xuất hiện trên thanh công cụ Ẩn nút truyền @@ -1109,7 +1121,9 @@ Bố cục ô tô "Khôi phục bố cục trình phát cũ Hạn chế: Nếu tùy chọn \"Khôi phục thanh thao tác video cũ\" bị tắt, lớp phủ \"Thêm video\" có thể không xuất hiện" - Việc bật tính năng này có thể làm hỏng một số tính năng bố trí trình phát, chẳng hạn như phần bình luận hoặc \'Video khác\' hiển thị bên dưới trình phát + "Việc bật tính năng này có thể làm hỏng một số tính năng bố cục của trình phát, chẳng hạn như phần bình luận hoặc 'Video khác' hiển thị bên dưới trình phát. + +Bố cục máy tính bảng không còn được YouTube hỗ trợ và bị hỏng một phần hoặc toàn bộ trên các phiên bản YouTube gần đây. Để có bố cục máy tính bảng hoạt động, hãy vá YouTube 20.21.37" 21.11.48 - Khôi phục hành vi phụ đề cũ 20.39.41 - Khôi phục ‘Lưu vào danh sách phát’ kiểu cũ @@ -1377,6 +1391,11 @@ Video được yêu cầu bằng một tiền tố ngắn của ID video đã b Hiển thị thông báo khi lưu tốc độ phát mặc định mới Tốc độ phát mặc định Tốc độ phát mặc định trên Shorts + Nhớ tốc độ phát trên Shorts + Áp dụng lựa chọn tốc độ Shorts thủ công của bạn cho tất cả các Shorts trong tương lai. Nếu tắt, tốc độ sẽ trở về mặc định sau khi đóng trình phát Shorts + Hiện Tốc độ phát trong menu Shorts + Thêm mục Tốc độ phát vào menu trình phát Shorts + Tốc độ phát · %s Đã đổi tốc độ phát mặc định thành: %s Sử dụng tốc độ phát bình thường đối với nhạc Luôn phát video nhạc với tốc độ 1.0x diff --git a/patches/src/main/resources/addresources/values-zh-rCN/youtube/strings.xml b/patches/src/main/resources/addresources/values-zh-rCN/youtube/strings.xml index 39712be2f3..375a322c29 100644 --- a/patches/src/main/resources/addresources/values-zh-rCN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-zh-rCN/youtube/strings.xml @@ -435,6 +435,17 @@ Second \"item\" text" 在信息流视频中任意位置长按可打开其菜单 在缩略图上显示稍后观看按钮 信息流视频的缩略图上会显示“稍后观看”按钮,点击并长按该按钮可将其保存到播放列表 + + 下载帖子图片 + 在带有图片或 GIF 的帖子中,分享按钮旁边会显示下载按钮,并且其菜单中会添加一个下载选项 + 下载图片 + 下载图片 + 正在下载 %s 张图片…… + 正在下载图片…… + %1$s 张图片已保存至 %2$s + 图片已保存至 %s + 无法下载图片 + 此帖子中未找到图片 将Ask项目添加到弹出菜单 在动态推送弹出菜单中添加视频摘要项目 @@ -882,6 +893,7 @@ Second \"item\" text" 半透明 透明 不透明 + 您的设备或 YouTube 版本可能不支持此样式,这可能会导致布局问题或崩溃 工具栏 选择工具栏中显示哪些组件 隐藏“投屏”按钮 @@ -1109,7 +1121,9 @@ Second \"item\" text" "恢复旧版播放器布局 限制:如果关闭“恢复旧版视频操作栏”选项,则“更多视频”叠加层可能不可用" - 启用此选项可能会破坏某些播放器布局功能,例如播放器下方的评论区或“更多视频” + "启用此功能可能会破坏某些播放器布局特性,例如播放器下方显示的评论区或“更多视频”。 + +YouTube 不再支持平板电脑布局,且在较新版本的 YouTube 中该布局已部分或完全损坏。如需可用的平板电脑布局,请将 YouTube 补丁至 20.21.37 版本" 21.11.48 - 恢复旧版字幕行为 20.39.41 - 恢复旧的保存到播放列表 @@ -1377,6 +1391,11 @@ Second \"item\" text" 保存新的默认播放速度时显示通知 默认播放速度 Short 默认播放速度 + 记住 Shorts 的播放速度 + 将您手动选择的 Shorts 速度应用于以后所有的 Shorts。如果关闭,速度将在关闭 Shorts 播放器后恢复为默认值 + 在 Shorts 菜单中显示播放速度 + 在 Shorts 播放器菜单中添加“播放速度”项目 + 播放速度 · %s 已将默认播放速度更改为:%s 使用正常速度播放音乐 始终以1.0倍速播放音乐视频 diff --git a/patches/src/main/resources/addresources/values-zh-rTW/youtube/strings.xml b/patches/src/main/resources/addresources/values-zh-rTW/youtube/strings.xml index 271a4f1ba4..649d6dbe59 100644 --- a/patches/src/main/resources/addresources/values-zh-rTW/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-zh-rTW/youtube/strings.xml @@ -435,6 +435,17 @@ Second \"item\" text" 在資訊流影片的任意位置輕觸並按住,即可開啟其選單 在縮圖上顯示稍後觀看按鈕 資訊流影片的縮圖上會顯示「稍後觀看」按鈕。輕觸並按住該按鈕可進入「新增至播放清單」功能 + + 下載貼文圖片 + 在包含圖片或 GIF 的貼文中,「分享」按鈕旁會顯示下載按鈕,且其選單中會新增下載選項 + 下載圖片 + 下載圖片 + 正在下載 %s 張圖片…… + 正在下載圖片…… + %1$s 張圖片已儲存至 %2$s + 圖片已儲存至 %s + 無法下載圖片 + 此貼文中未找到圖片 在彈出式選單中加入「Ask」項目 在資訊流彈出式選單中加入影片摘要項目 @@ -882,6 +893,7 @@ Second \"item\" text" 半透明 透明 不透明 + 您的裝置或 YouTube 版本可能不支援此樣式,這可能會導致版面配置問題或崩潰 工具列 選擇工具列中顯示的元件 隱藏「投放」按鈕 @@ -1109,7 +1121,9 @@ Second \"item\" text" "還原舊版播放器版面 限制:如果關閉「還原舊版影片動作列」,則「更多影片」疊加層可能無法使用。" - 啟用此功能可能會破壞部分播放器版面功能,例如留言區或播放器下方顯示的「更多影片」 + "啟用此功能可能會損壞某些播放器版面配置功能,例如播放器下方顯示的評論區或「更多影片」。 + +YouTube 已不再支援平板電腦版面配置,且在最近的 YouTube 版本中已部分或完全損壞。如需可運行的平板電腦版面配置,請修補 YouTube 20.21.37" 21.11.48 - 恢復舊版字幕行為 20.39.41 - 復原舊版播放清單 @@ -1377,6 +1391,11 @@ Second \"item\" text" 變更預設播放速度時會顯示提示 預設播放速度 Shorts 的預設播放速度 + 記住 Shorts 的播放速度 + 將您手動選擇的 Shorts 速度應用於所有未來的 Shorts。如果關閉,則在關閉 Shorts 播放器後速度將恢復為預設值 + 在 Shorts 選單中顯示播放速度 + 在 Shorts 播放器選單中新增播放速度項目 + 播放速度 · %s 已將預設播放速度變更為:%s 使用正常速度播放音樂 總是使用 1.0 倍速播放音樂影片 diff --git a/patches/src/main/resources/addresources/values-zu-rZA/youtube/strings.xml b/patches/src/main/resources/addresources/values-zu-rZA/youtube/strings.xml index 1a02f7efd7..9079108272 100644 --- a/patches/src/main/resources/addresources/values-zu-rZA/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-zu-rZA/youtube/strings.xml @@ -57,6 +57,7 @@ Second \"item\" text" + From eadda39219f07278ca2b0f14726222338bccbe3b Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Mon, 26 Oct 2026 07:48:07 +0000 Subject: [PATCH 078/189] chore: Release v1.49.0-dev.7 [skip ci] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## [1.49.0-dev.7](https://github.com/MorpheApp/morphe-patches/compare/v1.49.0-dev.6...v1.49.0-dev.7) (2026-10-26) ### 🐛 Bug Fixes * **YouTube - Add to queue:** Feed below the player stutters while scrolling ([5545e3c](https://github.com/MorpheApp/morphe-patches/commit/5545e3c87458e60c5e6ef5653053c02d343dd3f8)) * **YouTube - Add to queue:** Fix native queue on experimental versions ([#4076](https://github.com/MorpheApp/morphe-patches/issues/4076)) ([e009d8b](https://github.com/MorpheApp/morphe-patches/commit/e009d8bddb1bb94525060efc3d9b38ad97ba7d72)) * **YouTube - Add to queue:** Native queue index errors crash before Android 13 ([c5f9f94](https://github.com/MorpheApp/morphe-patches/commit/c5f9f94ee71a480be00a432ee364f4e7099b9537)) * **YouTube - Captions:** Old caption stays on screen after SponsorBlock skips ([#3986](https://github.com/MorpheApp/morphe-patches/issues/3986)) ([e200765](https://github.com/MorpheApp/morphe-patches/commit/e20076530e0af6061da9029bfaa4ad985fdd2f50)) * **YouTube - Disable playlist autoplay:** fix looping the current video ([#4064](https://github.com/MorpheApp/morphe-patches/issues/4064)) ([f4c1f84](https://github.com/MorpheApp/morphe-patches/commit/f4c1f84e13f0ed9b5982adfccbdbec1529ce0091)) * **YouTube - Force fullscreen landscape:** Screen rotates before the fullscreen gesture completes ([#4050](https://github.com/MorpheApp/morphe-patches/issues/4050)) ([83c0cf0](https://github.com/MorpheApp/morphe-patches/commit/83c0cf004e8cafa142cb862e3d78fd7e98c9f3c3)) * **YouTube - Litho filter:** Filtered elements leave empty cells in the related videos grid of the tablet layout ([e32b05c](https://github.com/MorpheApp/morphe-patches/commit/e32b05c663e30c23cb998e73fa23d78a58998962)) * **YouTube - Litho filter:** Filtered elements leave empty cells in the tablet layout grids ([#4035](https://github.com/MorpheApp/morphe-patches/issues/4035)) ([4764483](https://github.com/MorpheApp/morphe-patches/commit/4764483db626de5974ef2d3d3cf19d84ef4cfde1)) * **YouTube - Minimal miniplayer:** Bar is partly covered by the keyboard with the default navigation bar style ([bb6e0e3](https://github.com/MorpheApp/morphe-patches/commit/bb6e0e38bf4216395abacf8f4afbd45418b25ed7)) * **YouTube - Minimal miniplayer:** Video title and channel are missing after going back from the Shorts player ([7e665c6](https://github.com/MorpheApp/morphe-patches/commit/7e665c62dc3f88a6f9888b8bc48d1aea885761a1)) * **YouTube - Miniplayer:** Related videos lists show a black frame after exiting fullscreen and scroll down ([efbc9b1](https://github.com/MorpheApp/morphe-patches/commit/efbc9b1ea1739b617306f19900fa398251131494)) * **YouTube - Search comments:** Search button is not shown in the Shorts comments and is shown in other panels ([3b3308b](https://github.com/MorpheApp/morphe-patches/commit/3b3308b61740ab01e35aaa0e97166eeab80164e6)) * **YouTube - Video quality:** Shorts quality menu sets another quality than the one selected ([3e15dcd](https://github.com/MorpheApp/morphe-patches/commit/3e15dcd12190c4b435287080916170e82d0b4ac5)) * **YouTube - Video quality:** Shorts quality selected in the menu is not kept and is used for the next Shorts ([82efbab](https://github.com/MorpheApp/morphe-patches/commit/82efbab5e735cfa3db7a6cc5b05a5ad00b1b0b96)) ### ✨ New Features * **YouTube - Playback speed:** Add playback speed to the Shorts player menu ([#3982](https://github.com/MorpheApp/morphe-patches/issues/3982)) ([b717a69](https://github.com/MorpheApp/morphe-patches/commit/b717a692dfc3c99bee830af548bd13f98a3a4eaa)) * **YouTube:** Add `Download post images` patch ([#3966](https://github.com/MorpheApp/morphe-patches/issues/3966)) ([5f96fbe](https://github.com/MorpheApp/morphe-patches/commit/5f96fbe0a90efd54b3e1233c9690aa18f4d38dda)) * **YouTube:** Show a warning when changing 'Navigation bar style' or turning on 'Enable tablet layout in player' ([445194d](https://github.com/MorpheApp/morphe-patches/commit/445194d74ab5373334e14d31e1ceb29926ac267b)) --- CHANGELOG.md | 25 ++++++++++++++ README.md | 5 +-- gradle.properties | 2 +- patches-bundle.json | 8 ++--- patches-list.json | 81 ++++++++++++++++++++++++++++++++++++++++++++- 5 files changed, 113 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6532cdaaf9..5d48c298d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,28 @@ +## [1.49.0-dev.7](https://github.com/MorpheApp/morphe-patches/compare/v1.49.0-dev.6...v1.49.0-dev.7) (2026-10-26) + +### 🐛 Bug Fixes + +* **YouTube - Add to queue:** Feed below the player stutters while scrolling ([5545e3c](https://github.com/MorpheApp/morphe-patches/commit/5545e3c87458e60c5e6ef5653053c02d343dd3f8)) +* **YouTube - Add to queue:** Fix native queue on experimental versions ([#4076](https://github.com/MorpheApp/morphe-patches/issues/4076)) ([e009d8b](https://github.com/MorpheApp/morphe-patches/commit/e009d8bddb1bb94525060efc3d9b38ad97ba7d72)) +* **YouTube - Add to queue:** Native queue index errors crash before Android 13 ([c5f9f94](https://github.com/MorpheApp/morphe-patches/commit/c5f9f94ee71a480be00a432ee364f4e7099b9537)) +* **YouTube - Captions:** Old caption stays on screen after SponsorBlock skips ([#3986](https://github.com/MorpheApp/morphe-patches/issues/3986)) ([e200765](https://github.com/MorpheApp/morphe-patches/commit/e20076530e0af6061da9029bfaa4ad985fdd2f50)) +* **YouTube - Disable playlist autoplay:** fix looping the current video ([#4064](https://github.com/MorpheApp/morphe-patches/issues/4064)) ([f4c1f84](https://github.com/MorpheApp/morphe-patches/commit/f4c1f84e13f0ed9b5982adfccbdbec1529ce0091)) +* **YouTube - Force fullscreen landscape:** Screen rotates before the fullscreen gesture completes ([#4050](https://github.com/MorpheApp/morphe-patches/issues/4050)) ([83c0cf0](https://github.com/MorpheApp/morphe-patches/commit/83c0cf004e8cafa142cb862e3d78fd7e98c9f3c3)) +* **YouTube - Litho filter:** Filtered elements leave empty cells in the related videos grid of the tablet layout ([e32b05c](https://github.com/MorpheApp/morphe-patches/commit/e32b05c663e30c23cb998e73fa23d78a58998962)) +* **YouTube - Litho filter:** Filtered elements leave empty cells in the tablet layout grids ([#4035](https://github.com/MorpheApp/morphe-patches/issues/4035)) ([4764483](https://github.com/MorpheApp/morphe-patches/commit/4764483db626de5974ef2d3d3cf19d84ef4cfde1)) +* **YouTube - Minimal miniplayer:** Bar is partly covered by the keyboard with the default navigation bar style ([bb6e0e3](https://github.com/MorpheApp/morphe-patches/commit/bb6e0e38bf4216395abacf8f4afbd45418b25ed7)) +* **YouTube - Minimal miniplayer:** Video title and channel are missing after going back from the Shorts player ([7e665c6](https://github.com/MorpheApp/morphe-patches/commit/7e665c62dc3f88a6f9888b8bc48d1aea885761a1)) +* **YouTube - Miniplayer:** Related videos lists show a black frame after exiting fullscreen and scroll down ([efbc9b1](https://github.com/MorpheApp/morphe-patches/commit/efbc9b1ea1739b617306f19900fa398251131494)) +* **YouTube - Search comments:** Search button is not shown in the Shorts comments and is shown in other panels ([3b3308b](https://github.com/MorpheApp/morphe-patches/commit/3b3308b61740ab01e35aaa0e97166eeab80164e6)) +* **YouTube - Video quality:** Shorts quality menu sets another quality than the one selected ([3e15dcd](https://github.com/MorpheApp/morphe-patches/commit/3e15dcd12190c4b435287080916170e82d0b4ac5)) +* **YouTube - Video quality:** Shorts quality selected in the menu is not kept and is used for the next Shorts ([82efbab](https://github.com/MorpheApp/morphe-patches/commit/82efbab5e735cfa3db7a6cc5b05a5ad00b1b0b96)) + +### ✨ New Features + +* **YouTube - Playback speed:** Add playback speed to the Shorts player menu ([#3982](https://github.com/MorpheApp/morphe-patches/issues/3982)) ([b717a69](https://github.com/MorpheApp/morphe-patches/commit/b717a692dfc3c99bee830af548bd13f98a3a4eaa)) +* **YouTube:** Add `Download post images` patch ([#3966](https://github.com/MorpheApp/morphe-patches/issues/3966)) ([5f96fbe](https://github.com/MorpheApp/morphe-patches/commit/5f96fbe0a90efd54b3e1233c9690aa18f4d38dda)) +* **YouTube:** Show a warning when changing 'Navigation bar style' or turning on 'Enable tablet layout in player' ([445194d](https://github.com/MorpheApp/morphe-patches/commit/445194d74ab5373334e14d31e1ceb29926ac267b)) + ## [1.49.0-dev.6](https://github.com/MorpheApp/morphe-patches/compare/v1.49.0-dev.5...v1.49.0-dev.6) (2026-10-25) ### 🐛 Bug Fixes diff --git a/README.md b/README.md index a1503636e1..d863c6b1bf 100644 --- a/README.md +++ b/README.md @@ -35,9 +35,9 @@ All modifications made by Morphe, along with their dates, can be found in the Gi ## 🩹 Patches list -> **[v1.49.0-dev.6](https://github.com/MorpheApp/morphe-patches/releases/tag/v1.49.0-dev.6)**  •  `dev`  •  191 patches total +> **[v1.49.0-dev.7](https://github.com/MorpheApp/morphe-patches/releases/tag/v1.49.0-dev.7)**  •  `dev`  •  192 patches total

-📦 YouTube  •  106 patches +📦 YouTube  •  107 patches
**🎯 Supported versions:** @@ -83,6 +83,7 @@ All modifications made by Morphe, along with their dates, can be found in the Gi | [Disable sign in to TV popup](#disable-sign-in-to-tv-popup) | Adds options to disable the popups asking to sign into or connect to a TV on the same local network. | | | [Disable video codecs](#disable-video-codecs) | Adds options to disable or force HDR, and to disable VP9 or AV1 codecs. | | | [Double tap to seek](#double-tap-to-seek) | Adds additional double-tap to seek values to the YouTube settings menu. | | +| [Download post images](#download-post-images) | Adds a button and a menu item to download the images and GIFs of posts. | | | [Downloads](#downloads) | Adds support to download videos with an external downloader app using the in-app download button or a video player action button, and adds an option to download videos in-app. | | | [Enable debugging](#enable-debugging) | Adds options for debugging and exporting Morphe logs to the clipboard. | | | [Exit fullscreen mode](#exit-fullscreen-mode) | Adds options to automatically exit fullscreen mode when a video reaches the end. | | diff --git a/gradle.properties b/gradle.properties index b87d6a245d..a53f4de739 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,4 +5,4 @@ android.useAndroidX = true # Extensions are Java only. Prevents AGP from adding Kotlin compilation and the Kotlin stdlib. android.builtInKotlin = false kotlin.code.style = official -version = 1.49.0-dev.6 +version = 1.49.0-dev.7 diff --git a/patches-bundle.json b/patches-bundle.json index 2f0a9e5199..41fc4ace5a 100644 --- a/patches-bundle.json +++ b/patches-bundle.json @@ -1,7 +1,7 @@ { - "created_at": "2026-10-25T15:23:28", - "description": "## [1.49.0-dev.6](https://github.com/MorpheApp/morphe-patches/compare/v1.49.0-dev.5...v1.49.0-dev.6) (2026-10-25)\n\n### 🐛 Bug Fixes\n\n* **YouTube - Captions:** Captions not shown in picture-in-picture on 21.16 - 21.28 ([#4024](https://github.com/MorpheApp/morphe-patches/issues/4024)) ([b2688c5](https://github.com/MorpheApp/morphe-patches/commit/b2688c5eb957f3ffd142595ff4ee7b491e3b86ae))\n* **YouTube - Hide layout components:** Fix actions blocked by Hide Snackbar ([#4038](https://github.com/MorpheApp/morphe-patches/issues/4038)) ([9a0fee9](https://github.com/MorpheApp/morphe-patches/commit/9a0fee9ec2846ba2db31cbfc2e4c2ac9b14895b7))\n* **YouTube - Playback speed:** Channel speed whitelist is not applied to preloaded Shorts ([4f108b5](https://github.com/MorpheApp/morphe-patches/commit/4f108b596a71adbbf223b59faaabcfbe2469d95a))\n\n### ✨ New Features\n\n* **YouTube - Add to queue:** Add \"Native queue\" setting ([#3953](https://github.com/MorpheApp/morphe-patches/issues/3953)) ([372eea2](https://github.com/MorpheApp/morphe-patches/commit/372eea295ec4521b9ccdb93a3f188f81a6ad1a6c))\n* **YouTube - Downloads:** Add \"Quick download on tap\" setting ([#3883](https://github.com/MorpheApp/morphe-patches/issues/3883)) ([dd97d93](https://github.com/MorpheApp/morphe-patches/commit/dd97d93628a3feb7bc8758139b01fbe42a532778))\n* **YouTube - Media notification controls:** Add options to seek with Previous & Next buttons ([#3955](https://github.com/MorpheApp/morphe-patches/issues/3955)) ([2b07abb](https://github.com/MorpheApp/morphe-patches/commit/2b07abb628fec3bf625a09ab7d019f915c4dfa23))\n* **YouTube - Navigation bar:** Add \"Navigation bar style\" setting ([db9af37](https://github.com/MorpheApp/morphe-patches/commit/db9af377093121e575fec80ba40e0025dfd66f81))\n\n### 🚀 Updated App Support\n\n* **Reddit:** Add experimental support for `2026.43.0` ([6407d5e](https://github.com/MorpheApp/morphe-patches/commit/6407d5efa0f7d99fb54909bc39e2074e8f60ac42))\n* **Reddit:** Add support for `2026.40.0` ([fcc02f7](https://github.com/MorpheApp/morphe-patches/commit/fcc02f77d5a9d7a13b74d14c12b302f9d9902c61))", - "download_url": "https://github.com/MorpheApp/morphe-patches/releases/download/v1.49.0-dev.6/patches-1.49.0-dev.6.mpp", + "created_at": "2026-10-26T07:46:31", + "description": "## [1.49.0-dev.7](https://github.com/MorpheApp/morphe-patches/compare/v1.49.0-dev.6...v1.49.0-dev.7) (2026-10-26)\n\n### 🐛 Bug Fixes\n\n* **YouTube - Add to queue:** Feed below the player stutters while scrolling ([5545e3c](https://github.com/MorpheApp/morphe-patches/commit/5545e3c87458e60c5e6ef5653053c02d343dd3f8))\n* **YouTube - Add to queue:** Fix native queue on experimental versions ([#4076](https://github.com/MorpheApp/morphe-patches/issues/4076)) ([e009d8b](https://github.com/MorpheApp/morphe-patches/commit/e009d8bddb1bb94525060efc3d9b38ad97ba7d72))\n* **YouTube - Add to queue:** Native queue index errors crash before Android 13 ([c5f9f94](https://github.com/MorpheApp/morphe-patches/commit/c5f9f94ee71a480be00a432ee364f4e7099b9537))\n* **YouTube - Captions:** Old caption stays on screen after SponsorBlock skips ([#3986](https://github.com/MorpheApp/morphe-patches/issues/3986)) ([e200765](https://github.com/MorpheApp/morphe-patches/commit/e20076530e0af6061da9029bfaa4ad985fdd2f50))\n* **YouTube - Disable playlist autoplay:** fix looping the current video ([#4064](https://github.com/MorpheApp/morphe-patches/issues/4064)) ([f4c1f84](https://github.com/MorpheApp/morphe-patches/commit/f4c1f84e13f0ed9b5982adfccbdbec1529ce0091))\n* **YouTube - Force fullscreen landscape:** Screen rotates before the fullscreen gesture completes ([#4050](https://github.com/MorpheApp/morphe-patches/issues/4050)) ([83c0cf0](https://github.com/MorpheApp/morphe-patches/commit/83c0cf004e8cafa142cb862e3d78fd7e98c9f3c3))\n* **YouTube - Litho filter:** Filtered elements leave empty cells in the related videos grid of the tablet layout ([e32b05c](https://github.com/MorpheApp/morphe-patches/commit/e32b05c663e30c23cb998e73fa23d78a58998962))\n* **YouTube - Litho filter:** Filtered elements leave empty cells in the tablet layout grids ([#4035](https://github.com/MorpheApp/morphe-patches/issues/4035)) ([4764483](https://github.com/MorpheApp/morphe-patches/commit/4764483db626de5974ef2d3d3cf19d84ef4cfde1))\n* **YouTube - Minimal miniplayer:** Bar is partly covered by the keyboard with the default navigation bar style ([bb6e0e3](https://github.com/MorpheApp/morphe-patches/commit/bb6e0e38bf4216395abacf8f4afbd45418b25ed7))\n* **YouTube - Minimal miniplayer:** Video title and channel are missing after going back from the Shorts player ([7e665c6](https://github.com/MorpheApp/morphe-patches/commit/7e665c62dc3f88a6f9888b8bc48d1aea885761a1))\n* **YouTube - Miniplayer:** Related videos lists show a black frame after exiting fullscreen and scroll down ([efbc9b1](https://github.com/MorpheApp/morphe-patches/commit/efbc9b1ea1739b617306f19900fa398251131494))\n* **YouTube - Search comments:** Search button is not shown in the Shorts comments and is shown in other panels ([3b3308b](https://github.com/MorpheApp/morphe-patches/commit/3b3308b61740ab01e35aaa0e97166eeab80164e6))\n* **YouTube - Video quality:** Shorts quality menu sets another quality than the one selected ([3e15dcd](https://github.com/MorpheApp/morphe-patches/commit/3e15dcd12190c4b435287080916170e82d0b4ac5))\n* **YouTube - Video quality:** Shorts quality selected in the menu is not kept and is used for the next Shorts ([82efbab](https://github.com/MorpheApp/morphe-patches/commit/82efbab5e735cfa3db7a6cc5b05a5ad00b1b0b96))\n\n### ✨ New Features\n\n* **YouTube - Playback speed:** Add playback speed to the Shorts player menu ([#3982](https://github.com/MorpheApp/morphe-patches/issues/3982)) ([b717a69](https://github.com/MorpheApp/morphe-patches/commit/b717a692dfc3c99bee830af548bd13f98a3a4eaa))\n* **YouTube:** Add `Download post images` patch ([#3966](https://github.com/MorpheApp/morphe-patches/issues/3966)) ([5f96fbe](https://github.com/MorpheApp/morphe-patches/commit/5f96fbe0a90efd54b3e1233c9690aa18f4d38dda))\n* **YouTube:** Show a warning when changing 'Navigation bar style' or turning on 'Enable tablet layout in player' ([445194d](https://github.com/MorpheApp/morphe-patches/commit/445194d74ab5373334e14d31e1ceb29926ac267b))", + "download_url": "https://github.com/MorpheApp/morphe-patches/releases/download/v1.49.0-dev.7/patches-1.49.0-dev.7.mpp", "signature_download_url": "", - "version": "1.49.0-dev.6" + "version": "1.49.0-dev.7" } diff --git a/patches-list.json b/patches-list.json index 3fa4de925a..324d0a00e2 100644 --- a/patches-list.json +++ b/patches-list.json @@ -1,6 +1,6 @@ { "NOTE": "Do NOT manually edit this file. This file is automatically updated when semantic release (release.yml) runs. Manually editing this file can break your releases and break third party tools that use this file.", - "version": "1.49.0-dev.6", + "version": "1.49.0-dev.7", "patches": [ { "name": "AI channel filter", @@ -4230,6 +4230,85 @@ ], "options": [] }, + { + "name": "Download post images", + "description": "Adds a button and a menu item to download the images and GIFs of posts.", + "default": true, + "category": null, + "compatiblePackages": [ + { + "packageName": "com.google.android.youtube", + "name": "YouTube", + "description": null, + "apkFileType": "APK_REQUIRED", + "appIconColor": "#FF0033", + "signatures": [ + "5aad2bee6db95d17e05a08d7d1e64c10a1511879154483916b6ae6c7fd9cb0c6", + "3d7a1223019aa39d9ea0e3436ab7c0896bfb4fb679f4de5fe7c23f326c8f994a" + ], + "formFactor": "MOBILE", + "targets": [ + { + "version": "21.43.523", + "versionCodes": null, + "isExperimental": true, + "minSdk": 29, + "description": null + }, + { + "version": "21.42.241", + "versionCodes": null, + "isExperimental": true, + "minSdk": 29, + "description": null + }, + { + "version": "21.28.208", + "versionCodes": null, + "isExperimental": true, + "minSdk": 29, + "description": null + }, + { + "version": "21.23.492", + "versionCodes": null, + "isExperimental": false, + "minSdk": 28, + "description": null + }, + { + "version": "21.20.405", + "versionCodes": null, + "isExperimental": false, + "minSdk": 28, + "description": null + }, + { + "version": "21.16.256", + "versionCodes": null, + "isExperimental": false, + "minSdk": 28, + "description": null + }, + { + "version": "20.51.39", + "versionCodes": null, + "isExperimental": false, + "minSdk": 28, + "description": null + }, + { + "version": "20.21.37", + "versionCodes": null, + "isExperimental": false, + "minSdk": 26, + "description": null + } + ] + } + ], + "options": [] + }, { "name": "Downloads", "description": "Adds support to download songs using the in-app download button, either with an external downloader app or inside YouTube Music.", From 93bd0351362da6c75eb40f47d0ea0e37c632eb1c Mon Sep 17 00:00:00 2001 From: RAMiK <60112744+RAMiK4@users.noreply.github.com> Date: Mon, 26 Oct 2026 12:25:40 +0400 Subject: [PATCH 079/189] feat(YouTube - Channel search): Search the posts of the channel (#4078) --- .../youtube/patches/ChannelSearchPatch.java | 486 ++++++++++++++-- .../requests/ChannelPostsSearchRequest.java | 544 ++++++++++++++++++ .../utils/requests/ChannelSearchRoutes.java | 25 + .../addresources/values/youtube/strings.xml | 3 + 4 files changed, 1009 insertions(+), 49 deletions(-) create mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/requests/ChannelPostsSearchRequest.java diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/ChannelSearchPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/ChannelSearchPatch.java index e276973ea5..a4da30ee3e 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/ChannelSearchPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/ChannelSearchPatch.java @@ -22,7 +22,10 @@ import android.graphics.drawable.Drawable; import android.graphics.drawable.ShapeDrawable; import android.graphics.drawable.shapes.RoundRectShape; +import android.text.Spannable; +import android.text.SpannableString; import android.text.TextUtils; +import android.text.style.StyleSpan; import android.util.TypedValue; import android.view.Gravity; import android.view.Menu; @@ -63,6 +66,8 @@ import app.morphe.extension.shared.ui.SheetBottomDialog; import app.morphe.extension.youtube.patches.dearrow.DeArrowPatch; import app.morphe.extension.youtube.patches.originaltitles.RestoreOriginalTitlesPatch; +import app.morphe.extension.youtube.patches.utils.requests.ChannelPostsSearchRequest; +import app.morphe.extension.youtube.patches.utils.requests.ChannelPostsSearchRequest.ChannelPostResult; import app.morphe.extension.youtube.patches.utils.requests.ChannelSearchRequest; import app.morphe.extension.youtube.patches.utils.requests.ChannelUploadsRequest; import app.morphe.extension.youtube.patches.utils.requests.ChannelSearchRequest.ChannelSearchResponse; @@ -159,8 +164,67 @@ private Long sortKey(ChannelSearchResult result, ChannelUploadsRequest.Uploads u } } + /** + * What the results list, which the user picks above them. + */ + private enum ResultMode { + VIDEOS("morphe_channel_search_videos"), + POSTS("morphe_channel_search_posts"); + + /** + * This class is loaded when the app starts, before strings can be read, + * so the name is read only when it is shown. + */ + private final String modeNameKey; + + ResultMode(String modeNameKey) { + this.modeNameKey = modeNameKey; + } + + String getModeName() { + return str(modeNameKey); + } + } + + /** + * The posts tab lists posts newest first, and tells their age only rounded. + */ + private enum PostSortOption { + NEWEST("morphe_channel_search_sort_newest"), + OLDEST("morphe_channel_search_sort_oldest"); + + private final String sortNameKey; + + PostSortOption(String sortNameKey) { + this.sortNameKey = sortNameKey; + } + + String getSortName() { + return str(sortNameKey); + } + + List sort(List posts) { + if (this == OLDEST) { + Collections.reverse(posts); + } + return posts; + } + } + + /** + * Kept for the next search, so that searching posts again needs no extra tap. + */ + private static ResultMode lastResultMode = ResultMode.VIDEOS; + private static final int CHANNEL_ID_LENGTH = 24; + private static final int POST_THUMBNAIL_SIZE = Dim.dp(68); + + /** + * Text of a post shown before the first match, if the match is further in the text. + */ + private static final int POST_SNIPPET_LEAD_CHARACTERS = 40; + private static final int THUMBNAIL_WIDTH = Dim.dp(120); private static final int THUMBNAIL_HEIGHT = Dim.dp(68); private static final int THUMBNAIL_CORNER_RADIUS = Dim.dp(8); @@ -365,7 +429,20 @@ private static final class SearchController implements ChannelSearchRequest.Chan private ChannelSearchResponse lastResponse; private boolean isSearchComplete; private SortOption activeSort = SortOption.RELEVANCE; + private ResultMode resultMode = lastResultMode; + private PostSortOption activePostSort = PostSortOption.NEWEST; private volatile boolean isCancelled; + private boolean isSearchFailed; + private boolean isScopeSet; + @Nullable + private TextView sortButton; + + /** + * Posts of the posts tab that have the search, newest first. + */ + private List postsTabMatches = new ArrayList<>(); + private boolean postsTabRequested; + private boolean postsTabComplete; /** * Uploads of the channel, whose matches are the results of every sort but relevance. @@ -413,6 +490,10 @@ private SearchController(Activity activity, String channelId, String query) { thumbnailCache.clear(); }); dialog.show(); + + if (resultMode == ResultMode.POSTS) { + requestPostsTab(); + } } @Override @@ -431,17 +512,20 @@ public void onResponsePage(@Nullable ChannelSearchResponse response, boolean isC return; } if (response != null && !response.results.isEmpty()) { - if (lastResponse == null && !response.channelName.isEmpty()) { - String scope = str("morphe_channel_search_results") + " • " + response.channelName; - scopeView.setText(scope); - } + setScope(response.channelName); lastResponse = response; } else if (!isComplete) { return; } else if (lastResponse == null) { - showState(str(response == null - ? "morphe_channel_search_failed" - : "morphe_channel_search_no_results")); + isSearchComplete = true; + isSearchFailed = response == null; + if (resultMode == ResultMode.POSTS) { + refreshResults(); + } else { + showState(str(isSearchFailed + ? "morphe_channel_search_failed" + : "morphe_channel_search_no_results")); + } return; } // A later page that failed keeps the results of the pages before it. @@ -456,7 +540,8 @@ public void onResponsePage(@Nullable ChannelSearchResponse response, boolean isC * their exact view count. Called on main thread. */ private void completeResults() { - if (activeSort == SortOption.RELEVANCE || lastResponse == null || isCancelled()) { + if (resultMode != ResultMode.VIDEOS || activeSort == SortOption.RELEVANCE + || lastResponse == null || isCancelled()) { return; } @@ -557,12 +642,18 @@ private boolean isLoading() { && (!uploadsComplete || pendingDetailFetches > 0)); } - public SortOption getActiveSort() { - return activeSort; - } - private void refreshResults() { + if (resultMode == ResultMode.POSTS) { + refreshPosts(); + return; + } if (lastResponse == null) { + if (isSearchComplete) { + sortLoadingView.setVisibility(View.INVISIBLE); + showState(str(isSearchFailed + ? "morphe_channel_search_failed" + : "morphe_channel_search_no_results")); + } return; } @@ -583,6 +674,120 @@ private void refreshResults() { } } + private void setScope(String channelName) { + if (isScopeSet || channelName.isEmpty()) { + return; + } + isScopeSet = true; + scopeView.setText(str("morphe_channel_search_results") + " • " + channelName); + } + + private void setResultMode(ResultMode mode) { + if (resultMode == mode) { + return; + } + resultMode = mode; + lastResultMode = mode; + updateSortButtonText(); + scrollView.scrollTo(0, 0); + listContainer.removeAllViews(); + if (mode == ResultMode.POSTS) { + requestPostsTab(); + } else { + completeResults(); + } + if (lastResponse == null && !isSearchComplete && mode == ResultMode.VIDEOS) { + showState(null); + } + refreshResults(); + } + + /** + * The posts tab is searched only once posts are shown, as it takes a request for every page. + */ + private void requestPostsTab() { + if (postsTabRequested) { + return; + } + postsTabRequested = true; + Logger.printDebug(() -> "Searching posts tab of channel: " + channelId + " for: " + query); + ChannelPostsSearchRequest.fetchPostsTab(channelId, query, this::isCancelled, + (posts, channelName, isComplete) -> Utils.runOnMainThreadNowOrLater(() -> { + if (isCancelled()) return; + setScope(channelName); + postsTabMatches = posts; + postsTabComplete = isComplete; + if (resultMode == ResultMode.POSTS) { + refreshPosts(); + } + }) + ); + } + + private void refreshPosts() { + final boolean loading = !postsTabComplete; + sortLoadingView.setVisibility(loading ? View.VISIBLE : View.INVISIBLE); + + if (postsTabMatches.isEmpty()) { + showState(loading ? null : str("morphe_channel_search_no_posts")); + return; + } + + final int scrollY = scrollView.getScrollY(); + List posts = activePostSort.sort(new ArrayList<>(postsTabMatches)); + populatePostList(activity, posts, query, listContainer, dialog); + if (scrollY > 0) { + scrollView.scrollTo(0, scrollY); + } + } + + @SuppressLint("SetTextI18n") + private void updateSortButtonText() { + if (sortButton == null) return; + String sortName = resultMode == ResultMode.POSTS + ? activePostSort.getSortName() + : activeSort.sortName; + sortButton.setText(sortName + " ▼"); + } + + /** + * Sorts of the results the active mode lists. + */ + private void showSortMenu() { + if (sortButton == null) return; + PopupMenu popupMenu = new PopupMenu(activity, sortButton); + Menu menu = popupMenu.getMenu(); + final boolean isPosts = resultMode == ResultMode.POSTS; + final int length = isPosts ? PostSortOption.values().length : SortOption.values().length; + for (int i = 0; i < length; i++) { + menu.add(0, i, i, isPosts + ? PostSortOption.values()[i].getSortName() + : SortOption.values()[i].sortName); + } + + popupMenu.setOnMenuItemClickListener(item -> { + final int index = item.getItemId(); + if (index < 0 || index >= length) { + return false; + } + if (isPosts) { + PostSortOption selected = PostSortOption.values()[index]; + if (selected == activePostSort) return true; + activePostSort = selected; + } else { + SortOption selected = SortOption.values()[index]; + if (selected == activeSort) return true; + activeSort = selected; + completeResults(); + } + scrollView.scrollTo(0, 0); + refreshResults(); + updateSortButtonText(); + return true; + }); + popupMenu.show(); + } + /** * Replaces the results with a loading animation, or with a message. * @@ -680,6 +885,27 @@ private static View createSortBar(Activity activity, SearchController controller final int fgColor = ThemeUtils.getAppForegroundColor(); final int buttonBgColor = (fgColor & 0x00FFFFFF) | 0x1A000000; + List modeChips = new ArrayList<>(); + for (ResultMode mode : ResultMode.values()) { + TextView chip = new TextView(activity); + chip.setText(mode.getModeName()); + chip.setTextSize(TypedValue.COMPLEX_UNIT_SP, 13); + chip.setSingleLine(); + chip.setGravity(Gravity.CENTER); + chip.setPadding(Dim.dp12, Dim.dp6, Dim.dp12, Dim.dp6); + chip.setTag(mode); + chip.setOnClickListener(view -> { + controller.setResultMode(mode); + styleModeChips(modeChips, controller.resultMode, fgColor, buttonBgColor); + }); + LinearLayout.LayoutParams chipParams = new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); + chipParams.setMarginEnd(Dim.dp8); + sortBarLayout.addView(chip, chipParams); + modeChips.add(chip); + } + styleModeChips(modeChips, controller.resultMode, fgColor, buttonBgColor); + TextView sortButton = new TextView(activity); sortButton.setTextSize(TypedValue.COMPLEX_UNIT_SP, 13); sortButton.setSingleLine(); @@ -691,51 +917,213 @@ private static View createSortBar(Activity activity, SearchController controller Dim.roundedCorners(16), null, null)); background.getPaint().setColor(buttonBgColor); sortButton.setBackground(background); - sortButton.setOnClickListener(new View.OnClickListener() { - { - updateButtonText(); + sortButton.setOnClickListener(view -> controller.showSortMenu()); + sortBarLayout.addView(sortButton); + controller.sortButton = sortButton; + controller.updateSortButtonText(); + + LinearLayout.LayoutParams loadingParams = new LinearLayout.LayoutParams(Dim.dp20, Dim.dp20); + loadingParams.setMarginStart(Dim.dp4); + sortBarLayout.addView(loadingView, loadingParams); + + return sortBarLayout; + } + + /** + * The picked mode is filled with the text color, as the chips of YouTube are. + */ + private static void styleModeChips(List chips, ResultMode selected, + int fgColor, int buttonBgColor) { + final int bgColor = ThemeUtils.getDialogBackgroundColor(); + for (TextView chip : chips) { + final boolean isSelected = chip.getTag() == selected; + ShapeDrawable background = new ShapeDrawable(new RoundRectShape( + Dim.roundedCorners(16), null, null)); + background.getPaint().setColor(isSelected ? fgColor : buttonBgColor); + chip.setBackground(background); + chip.setTextColor(isSelected ? bgColor : fgColor); + chip.setTypeface(isSelected ? Typeface.DEFAULT_BOLD : Typeface.DEFAULT); + chip.setSelected(isSelected); + } + } + + private static void populatePostList(Activity activity, List posts, String query, + LinearLayout listContainer, SheetBottomDialog.SlideDialog dialog) { + final int count = listContainer.getChildCount(); + Map existingViews = new HashMap<>(2 * count); + for (int i = 0; i < count; i++) { + View child = listContainer.getChildAt(i); + if (child.getTag() instanceof String tag && tag.startsWith("post:")) { + existingViews.put(tag, child); } + } - @SuppressLint("SetTextI18n") - private void updateButtonText() { - sortButton.setText(controller.getActiveSort().sortName + " ▼"); + listContainer.removeAllViews(); + + for (ChannelPostResult post : posts) { + String tag = "post:" + post.postId; + View row = existingViews.get(tag); + if (row == null) { + row = createPostRow(activity, post, query); + row.setTag(tag); + String postId = post.postId; + row.setOnClickListener(view -> { + dialog.dismiss(); + Utils.runOnMainThreadDelayed(() -> { + closeSearch(activity); + LoadVideoPatch.openVideoIntent("https://www.youtube.com/post/" + postId, false); + }, DIALOG_ANIMATION_DURATION_MILLISECONDS); + }); } + listContainer.addView(row); + } + } - @Override - public void onClick(View v) { - PopupMenu popupMenu = new PopupMenu(activity, sortButton); - SortOption[] options = SortOption.values(); - Menu menu = popupMenu.getMenu(); - for (int i = 0, length = options.length; i < length; i++) { - menu.add(0, i, i, options[i].sortName); + private static View createPostRow(Activity activity, ChannelPostResult post, String query) { + LinearLayout row = createResultRowContainer(activity); + row.setGravity(Gravity.TOP); + + if (!post.thumbnailUrl.isEmpty()) { + FrameLayout thumbnailFrame = new FrameLayout(activity); + LinearLayout.LayoutParams frameParams = + new LinearLayout.LayoutParams(POST_THUMBNAIL_SIZE, POST_THUMBNAIL_SIZE); + frameParams.setMarginEnd(Dim.dp12); + frameParams.topMargin = Dim.dp2; + thumbnailFrame.setLayoutParams(frameParams); + + ImageView thumbnail = new ImageView(activity); + thumbnail.setScaleType(ImageView.ScaleType.CENTER_CROP); + thumbnail.setClipToOutline(true); + thumbnail.setOutlineProvider(new ViewOutlineProvider() { + @Override + public void getOutline(View view, Outline outline) { + outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), + THUMBNAIL_CORNER_RADIUS); } + }); + thumbnailFrame.addView(thumbnail, new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + loadThumbnail(thumbnail, post.thumbnailUrl); + + if (post.imageCount > 1) { + TextView badge = new TextView(activity); + badge.setText(String.valueOf(post.imageCount)); + badge.setTextColor(0xFFFFFFFF); + badge.setTextSize(TypedValue.COMPLEX_UNIT_SP, 11); + badge.setTypeface(Typeface.DEFAULT_BOLD); + badge.setPadding(Dim.dp6, Dim.dp1, Dim.dp6, Dim.dp1); + ShapeDrawable badgeBackground = new ShapeDrawable(new RoundRectShape( + Dim.roundedCorners(4), null, null)); + badgeBackground.getPaint().setColor(0xA6000000); + badge.setBackground(badgeBackground); + FrameLayout.LayoutParams badgeParams = new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, + Gravity.BOTTOM | Gravity.END); + badgeParams.setMargins(0, 0, Dim.dp4, Dim.dp4); + thumbnailFrame.addView(badge, badgeParams); + } + row.addView(thumbnailFrame); + } - popupMenu.setOnMenuItemClickListener(item -> { - int index = item.getItemId(); - if (index < 0 || index >= options.length) { - return false; - } - SortOption selectedOption = options[index]; - if (controller.getActiveSort() == selectedOption) { - return true; - } - controller.activeSort = selectedOption; - controller.scrollView.scrollTo(0, 0); - controller.completeResults(); - controller.refreshResults(); - updateButtonText(); - return true; - }); - popupMenu.show(); + LinearLayout text = new LinearLayout(activity); + text.setOrientation(LinearLayout.VERTICAL); + text.setLayoutParams(new LinearLayout.LayoutParams( + 0, ViewGroup.LayoutParams.WRAP_CONTENT, 1)); + + TextView body = new TextView(activity); + body.setText(postSnippet(post.text, query)); + body.setTextColor(ThemeUtils.getAppForegroundColor()); + body.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14); + body.setMaxLines(3); + body.setEllipsize(TextUtils.TruncateAt.END); + text.addView(body); + + StringBuilder metadataText = new StringBuilder(); + if (!post.publishedTime.isEmpty()) { + metadataText.append(post.publishedTime); + } + if (!post.likes.isEmpty()) { + //noinspection SizeReplaceableByIsEmpty + if (metadataText.length() > 0) metadataText.append(" • "); + metadataText.append(post.likes); + } + //noinspection SizeReplaceableByIsEmpty + if (metadataText.length() > 0) { + TextView metadata = new TextView(activity); + metadata.setText(metadataText); + metadata.setTextColor(ThemeUtils.getAppForegroundColor()); + metadata.setTextSize(TypedValue.COMPLEX_UNIT_SP, 12); + metadata.setAlpha(0.7f); + metadata.setSingleLine(); + metadata.setPadding(0, Dim.dp2, 0, 0); + text.addView(metadata); + } + + row.addView(text); + return row; + } + + /** + * Text of the post from shortly before the first word of the search, with the words of + * the search in bold, so that a match deep in a long post is still shown. + */ + private static CharSequence postSnippet(String text, String query) { + if (text.isEmpty()) { + return text; + } + + String foldedText = foldForHighlight(text); + List words = new ArrayList<>(); + for (String word : foldForHighlight(query).split("\\s+")) { + if (!word.isEmpty()) words.add(word); + } + + int firstMatch = -1; + for (String word : words) { + final int index = foldedText.indexOf(word); + if (index >= 0 && (firstMatch < 0 || index < firstMatch)) { + firstMatch = index; } - }); - sortBarLayout.addView(sortButton); + } - LinearLayout.LayoutParams loadingParams = new LinearLayout.LayoutParams(Dim.dp20, Dim.dp20); - loadingParams.setMarginStart(Dim.dp12); - sortBarLayout.addView(loadingView, loadingParams); + int start = 0; + if (firstMatch > POST_SNIPPET_LEAD_CHARACTERS) { + start = firstMatch - POST_SNIPPET_LEAD_CHARACTERS; + // Starts at a word, or at a line. + final int lineStart = text.lastIndexOf('\n', firstMatch - 1); + if (lineStart >= start) { + start = lineStart + 1; + } else { + final int space = text.indexOf(' ', start); + if (space >= 0 && space < firstMatch) start = space + 1; + } + } - return sortBarLayout; + final String prefix = start > 0 ? "…" : ""; + SpannableString snippet = new SpannableString(prefix + text.substring(start)); + String foldedSnippet = foldedText.substring(start); + for (String word : words) { + int index = foldedSnippet.indexOf(word); + while (index >= 0) { + final int spanStart = prefix.length() + index; + snippet.setSpan(new StyleSpan(Typeface.BOLD), spanStart, spanStart + word.length(), + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + index = foldedSnippet.indexOf(word, index + word.length()); + } + } + return snippet; + } + + /** + * Lowercase with each character replaced by one character, so positions in the text stay the same. + */ + private static String foldForHighlight(String text) { + String folded = ChannelPostsSearchRequest.foldTurkishI(text); + StringBuilder builder = new StringBuilder(folded.length()); + for (int i = 0, length = folded.length(); i < length; i++) { + builder.append(Character.toLowerCase(folded.charAt(i))); + } + return builder.toString(); } private static void populateList(Activity activity, List results, @@ -745,7 +1133,7 @@ private static void populateList(Activity activity, List re for (int i = 0; i < count; i++) { View child = listContainer.getChildAt(i); Object tag = child.getTag(); - if (tag instanceof String videoId) { + if (tag instanceof String videoId && !videoId.startsWith("post:")) { existingViews.put(videoId, child); } } diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/requests/ChannelPostsSearchRequest.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/requests/ChannelPostsSearchRequest.java new file mode 100644 index 0000000000..56fc2afa2f --- /dev/null +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/requests/ChannelPostsSearchRequest.java @@ -0,0 +1,544 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/4078 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.extension.youtube.patches.utils.requests; + +import android.net.Uri; + +import androidx.annotation.Nullable; + +import org.json.JSONArray; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.function.BooleanSupplier; + +import app.morphe.extension.shared.Logger; +import app.morphe.extension.shared.Utils; +import app.morphe.extension.shared.requests.Requester; + +/** + * Posts of a channel that have the search. + *

+ * The search tab of a channel lists no posts, so the posts tab is listed page by page, + * newest first, and the text of each post is searched here. + */ +public final class ChannelPostsSearchRequest { + + public static final class ChannelPostResult { + public final String postId; + /** Text of the post, as written. */ + public final String text; + /** Image or video of the post, or empty if it has none. */ + public final String thumbnailUrl; + /** Number of images of the post, 0 if it has none. */ + public final int imageCount; + /** Localized age of the post, such as "2 days ago". */ + public final String publishedTime; + /** Localized like count, such as "1.2K likes", or empty. */ + public final String likes; + /** Text of the post and of its attachments, which the search is matched with. */ + final String searchText; + + private ChannelPostResult(String postId, String text, String thumbnailUrl, int imageCount, + String publishedTime, String likes, String searchText) { + this.postId = postId; + this.text = text; + this.thumbnailUrl = thumbnailUrl; + this.imageCount = imageCount; + this.publishedTime = publishedTime; + this.likes = likes; + this.searchText = searchText; + } + } + + public interface ChannelPostsCallback { + /** + * Called on a background thread after each page of the posts tab, + * and on the calling thread if the search is cached. + * + * @param posts Posts that have the search, so far. + * @param channelName Empty if not known. + * @param isComplete Whether this is the last page. + */ + void onPostsPage(List posts, String channelName, boolean isComplete); + } + + /** + * Each page of the posts tab lists 10 posts. The tab itself ends after about 40 pages. + */ + private static final int MAX_POSTS_TAB_PAGES = 60; + + /** + * Parts of a response that list the posts of the tab and the next page of it. + * The header lists other pages too, such as the about panel of the channel. + */ + private static final String[] POST_LIST_KEYS = { + "contents", "onResponseReceivedEndpoints", "onResponseReceivedActions"}; + + /** + * Parts of a post list that have continuations, which are not the next page of the list. + */ + private static final String[] NOT_POST_LIST_KEYS = { + "backstagePostThreadRenderer", "backstagePostRenderer", "postRenderer", + "header", "engagementPanel"}; + + private static final String[] POST_RENDERER_KEYS = {"backstagePostRenderer", "postRenderer"}; + + /** + * A post shared from another channel is not a post of this channel, + * and its own text is only a comment on it. + */ + private static final String SHARED_POST_RENDERER_KEY = "sharedPostRenderer"; + + private static final int THUMBNAIL_MIN_WIDTH = 200; + + /** + * Complete searches only. + */ + private static final Map> cache = Collections.synchronizedMap( + Utils.createSizeRestrictedMap(10)); + + private ChannelPostsSearchRequest() { + } + + /** + * @return Whether the post has every word of the search. + */ + public static boolean matches(ChannelPostResult post, String query) { + return ChannelUploadsRequest.matches(foldTurkishI(post.searchText), foldTurkishI(query)); + } + + /** + * Dotted and dotless i are the same letter for the search, so that a search typed + * with either keyboard finds both, such as "ISTANBUL" and "istanbul". + * Each character is replaced by one character, so positions in the text stay the same. + */ + public static String foldTurkishI(String text) { + StringBuilder builder = new StringBuilder(text.length()); + for (int i = 0, length = text.length(); i < length; i++) { + final char c = text.charAt(i); + builder.append(c == 'I' || c == 'İ' || c == 'ı' ? 'i' : c); + } + return builder.toString(); + } + + /** + * Adds the posts of a page of the posts tab to the list, in the order they are listed. + * + * @param seenPostIds Ids of the posts already in the list. + */ + private static void collectPagePosts(JSONObject json, List posts, Set seenPostIds) { + for (String key : POST_LIST_KEYS) { + collectPosts(json.opt(key), posts, seenPostIds); + } + } + + private static void collectPosts(@Nullable Object json, List posts, Set seenPostIds) { + if (json instanceof JSONObject object) { + Iterator keys = object.keys(); + while (keys.hasNext()) { + String key = keys.next(); + if (SHARED_POST_RENDERER_KEY.equals(key)) continue; + + Object value = object.opt(key); + if (isPostRendererKey(key) && value instanceof JSONObject post) { + ChannelPostResult result = parsePost(post); + if (result != null && seenPostIds.add(result.postId)) { + posts.add(result); + } + continue; + } + collectPosts(value, posts, seenPostIds); + } + } else if (json instanceof JSONArray array) { + for (int i = 0, length = array.length(); i < length; i++) { + collectPosts(array.opt(i), posts, seenPostIds); + } + } + } + + private static boolean isPostRendererKey(String key) { + for (String postKey : POST_RENDERER_KEYS) { + if (postKey.equals(key)) return true; + } + return false; + } + + @Nullable + private static ChannelPostResult parsePost(JSONObject post) { + String postId = post.optString("postId"); + if (postId.isEmpty()) { + return null; + } + + String text = parseText(post.optJSONObject("contentText")); + String publishedTime = parseText(post.optJSONObject("publishedTimeText")); + + JSONObject voteCount = post.optJSONObject("voteCount"); + String likes = accessibilityLabel(voteCount); + if (likes.isEmpty()) { + likes = parseText(voteCount); + } + + StringBuilder searchText = new StringBuilder(text); + String thumbnailUrl = ""; + int imageCount = 0; + + JSONObject attachment = post.optJSONObject("backstageAttachment"); + if (attachment != null) { + JSONObject image = attachment.optJSONObject("backstageImageRenderer"); + JSONObject multiImage = attachment.optJSONObject("postMultiImageRenderer"); + JSONObject video = attachment.optJSONObject("videoRenderer"); + JSONObject poll = attachment.optJSONObject("pollRenderer"); + + if (image != null) { + imageCount = 1; + thumbnailUrl = imageUrl(image); + } else if (multiImage != null) { + JSONArray images = multiImage.optJSONArray("images"); + if (images != null) { + imageCount = images.length(); + JSONObject first = images.optJSONObject(0); + if (first != null) { + JSONObject firstImage = first.optJSONObject("backstageImageRenderer"); + if (firstImage != null) thumbnailUrl = imageUrl(firstImage); + } + } + } else if (video != null) { + JSONObject thumbnail = video.optJSONObject("thumbnail"); + if (thumbnail != null) { + thumbnailUrl = ChannelSearchRequest.widestThumbnail(thumbnail.optJSONArray("thumbnails")); + } + appendSearchText(searchText, parseText(video.optJSONObject("title"))); + } else if (poll != null) { + JSONArray choices = poll.optJSONArray("choices"); + if (choices != null) { + for (int i = 0, length = choices.length(); i < length; i++) { + JSONObject choice = choices.optJSONObject(i); + if (choice != null) { + appendSearchText(searchText, parseText(choice.optJSONObject("text"))); + } + } + } + } + } + + return new ChannelPostResult(postId, text.trim(), thumbnailUrl, imageCount, + publishedTime, likes, searchText.toString()); + } + + private static void appendSearchText(StringBuilder searchText, String value) { + if (!value.isEmpty()) { + searchText.append('\n').append(value); + } + } + + /** + * The smallest image that is still sharp in a row, as images of posts are listed up to + * their original size. + */ + private static String imageUrl(JSONObject imageRenderer) { + JSONObject image = imageRenderer.optJSONObject("image"); + JSONArray thumbnails = image == null ? null : image.optJSONArray("thumbnails"); + if (thumbnails == null || thumbnails.length() == 0) { + return ""; + } + + String best = ""; + int bestWidth = Integer.MAX_VALUE; + String widest = ""; + int widestWidth = -1; + for (int i = 0, length = thumbnails.length(); i < length; i++) { + JSONObject entry = thumbnails.optJSONObject(i); + if (entry == null) continue; + String url = entry.optString("url"); + final int width = entry.optInt("width"); + if (url.isEmpty()) continue; + if (width >= THUMBNAIL_MIN_WIDTH && width < bestWidth) { + best = url; + bestWidth = width; + } + if (width > widestWidth) { + widest = url; + widestWidth = width; + } + } + String url = best.isEmpty() ? widest : best; + return url.startsWith("//") ? "https:" + url : url; + } + + /** + * Searches the posts tab of the channel. Does nothing if the search is cancelled. + */ + public static void fetchPostsTab(String channelId, String query, BooleanSupplier isCancelled, + ChannelPostsCallback callback) { + String key = channelId + "\n" + query; + List cached = cache.get(key); + if (cached != null) { + callback.onPostsPage(cached, "", true); + return; + } + + Utils.runOnBackgroundThread(() -> { + List posts = searchPostsTab(channelId, query, isCancelled, callback); + if (posts != null && !isCancelled.getAsBoolean()) { + cache.put(key, posts); + } + }); + } + + /** + * @return The posts that have the search, or null if the search did not complete. + */ + @Nullable + private static List searchPostsTab(String channelId, String query, + BooleanSupplier isCancelled, + ChannelPostsCallback callback) { + Utils.verifyOffMainThread(); + + final long startTime = System.currentTimeMillis(); + Locale locale = Requester.getAppLocale(); + List matches = new ArrayList<>(); + List pagePosts = new ArrayList<>(); + Set seenPostIds = new HashSet<>(); + int scannedPosts = 0; + int page = 0; + + try { + JSONObject json = ChannelSearchRoutes.fetch(ChannelSearchRoutes.CHANNEL_SEARCH, + ChannelSearchRoutes.createPostsTabBody(channelId, ChannelSearchRoutes.POSTS_TAB_PARAMS, locale)); + if (json == null) { + Logger.printDebug(() -> "Posts tab could not be fetched: " + channelId); + callback.onPostsPage(matches, "", true); + return null; + } + + // The tabs of the response tell the params of the posts tab, if the ones sent were not taken. + String postsTabParams = findPostsTabParams(json); + if (postsTabParams == null) { + Logger.printDebug(() -> "Channel has no posts tab: " + channelId); + callback.onPostsPage(matches, parseChannelName(json), true); + return matches; + } + if (!isPostsTabSelected(json) && !postsTabParams.isEmpty()) { + Logger.printDebug(() -> "Posts tab params of the response: " + postsTabParams); + json = ChannelSearchRoutes.fetch(ChannelSearchRoutes.CHANNEL_SEARCH, + ChannelSearchRoutes.createPostsTabBody(channelId, postsTabParams, locale)); + if (json == null) { + callback.onPostsPage(matches, "", true); + return null; + } + } + + String channelName = parseChannelName(json); + + for (page = 1; ; page++) { + pagePosts.clear(); + collectPagePosts(json, pagePosts, seenPostIds); + scannedPosts += pagePosts.size(); + for (ChannelPostResult post : pagePosts) { + if (matches(post, query)) { + matches.add(post); + } + } + + String nextToken = findNextPageToken(json); + final boolean isLast = nextToken == null || page >= MAX_POSTS_TAB_PAGES + // A page without new posts would only repeat. + || (page > 1 && pagePosts.isEmpty()); + callback.onPostsPage(new ArrayList<>(matches), channelName, isLast); + if (isLast) { + return matches; + } + if (isCancelled.getAsBoolean()) { + Logger.printDebug(() -> "Posts search cancelled"); + return null; + } + + json = ChannelSearchRoutes.fetch(ChannelSearchRoutes.CHANNEL_SEARCH, + ChannelSearchRoutes.createUploadsContinuationBody(nextToken, locale)); + if (json == null) { + // A page that could not be fetched ends the search with the posts so far. + callback.onPostsPage(new ArrayList<>(matches), channelName, true); + return null; + } + } + } catch (Exception ex) { + Logger.printException(() -> "searchPostsTab failure", ex); + callback.onPostsPage(new ArrayList<>(matches), "", true); + return null; + } finally { + final int pages = page; + final int scanned = scannedPosts; + Logger.printDebug(() -> "Searched posts tab, pages=" + pages + " posts=" + scanned + + " matches=" + matches.size() + " took: " + (System.currentTimeMillis() - startTime) + "ms"); + } + } + + /** + * @return The params of the posts tab, empty if the tab has none, + * or null if the channel has no posts tab. + */ + @Nullable + private static String findPostsTabParams(JSONObject json) { + JSONArray tabs = findTabs(json); + if (tabs == null) { + // Not a tab list, such as a page that is only the posts. + return ""; + } + for (int i = 0, length = tabs.length(); i < length; i++) { + JSONObject tab = tabs.optJSONObject(i); + JSONObject tabRenderer = tab == null ? null : tab.optJSONObject("tabRenderer"); + if (tabRenderer == null || !isPostsTab(tabRenderer)) continue; + + JSONObject endpoint = tabRenderer.optJSONObject("endpoint"); + JSONObject browse = endpoint == null ? null : endpoint.optJSONObject("browseEndpoint"); + // The params of a tab are url encoded, such as "%3D" for "=". + return browse == null ? "" : Uri.decode(browse.optString("params")); + } + return null; + } + + private static boolean isPostsTabSelected(JSONObject json) { + JSONArray tabs = findTabs(json); + if (tabs == null) { + return true; + } + for (int i = 0, length = tabs.length(); i < length; i++) { + JSONObject tab = tabs.optJSONObject(i); + JSONObject tabRenderer = tab == null ? null : tab.optJSONObject("tabRenderer"); + if (tabRenderer != null && tabRenderer.optBoolean("selected")) { + return isPostsTab(tabRenderer); + } + } + return false; + } + + private static boolean isPostsTab(JSONObject tabRenderer) { + JSONObject endpoint = tabRenderer.optJSONObject("endpoint"); + if (endpoint == null) return false; + JSONObject commandMetadata = endpoint.optJSONObject("commandMetadata"); + JSONObject web = commandMetadata == null ? null : commandMetadata.optJSONObject("webCommandMetadata"); + String url = web == null ? "" : web.optString("url"); + return url.endsWith("/posts") || url.endsWith("/community"); + } + + @Nullable + private static JSONArray findTabs(JSONObject json) { + JSONObject contents = json.optJSONObject("contents"); + if (contents == null) return null; + for (String key : new String[]{"twoColumnBrowseResultsRenderer", "singleColumnBrowseResultsRenderer"}) { + JSONObject renderer = contents.optJSONObject(key); + if (renderer != null) return renderer.optJSONArray("tabs"); + } + return null; + } + + /** + * @return Token of the next page of the posts tab, which is listed after the posts of the page. + */ + @Nullable + private static String findNextPageToken(JSONObject json) { + String token = null; + for (String key : POST_LIST_KEYS) { + String found = findContinuationToken(json.opt(key)); + if (found != null) token = found; + } + return token; + } + + @Nullable + private static String findContinuationToken(@Nullable Object json) { + String token = null; + if (json instanceof JSONObject object) { + JSONObject continuation = object.optJSONObject("continuationItemRenderer"); + if (continuation != null) { + String found = commandToken(continuation); + if (found != null) token = found; + } + Iterator keys = object.keys(); + while (keys.hasNext()) { + String key = keys.next(); + if ("continuationItemRenderer".equals(key) || isNotPostListKey(key)) continue; + String found = findContinuationToken(object.opt(key)); + if (found != null) token = found; + } + } else if (json instanceof JSONArray array) { + for (int i = 0, length = array.length(); i < length; i++) { + String found = findContinuationToken(array.opt(i)); + if (found != null) token = found; + } + } + return token; + } + + private static boolean isNotPostListKey(String key) { + for (String notPostListKey : NOT_POST_LIST_KEYS) { + if (notPostListKey.equals(key)) return true; + } + return false; + } + + @Nullable + private static String commandToken(JSONObject continuationItemRenderer) { + JSONObject endpoint = continuationItemRenderer.optJSONObject("continuationEndpoint"); + if (endpoint == null) return null; + JSONObject command = endpoint.optJSONObject("continuationCommand"); + if (command == null) return null; + String token = command.optString("token"); + return token.isEmpty() ? null : token; + } + + private static String parseChannelName(JSONObject json) { + JSONObject metadata = json.optJSONObject("metadata"); + JSONObject channel = metadata == null ? null : metadata.optJSONObject("channelMetadataRenderer"); + return channel == null ? "" : channel.optString("title"); + } + + private static String accessibilityLabel(@Nullable JSONObject text) { + if (text == null) return ""; + JSONObject accessibility = text.optJSONObject("accessibility"); + JSONObject data = accessibility == null ? null : accessibility.optJSONObject("accessibilityData"); + return data == null ? "" : data.optString("label"); + } + + /** + * Text is either a plain string or a list of runs, depending on the field. + */ + private static String parseText(@Nullable JSONObject text) { + if (text == null) { + return ""; + } + + String simpleText = text.optString("simpleText"); + if (!simpleText.isEmpty()) { + return simpleText; + } + + JSONArray runs = text.optJSONArray("runs"); + if (runs == null) { + return ""; + } + + StringBuilder builder = new StringBuilder(); + for (int i = 0, length = runs.length(); i < length; i++) { + JSONObject run = runs.optJSONObject(i); + if (run != null) { + builder.append(run.optString("text")); + } + } + return builder.toString(); + } +} diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/requests/ChannelSearchRoutes.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/requests/ChannelSearchRoutes.java index 1d34b3ab3d..1cb602da34 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/requests/ChannelSearchRoutes.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/requests/ChannelSearchRoutes.java @@ -242,6 +242,31 @@ public static byte[] createUploadsContinuationBody(String continuationToken, Loc return new byte[0]; } + /** + * Browse params of the channel "Posts" tab for the web client: {2: "community", 110: {1: {9: {}}}}. + * If the server does not take these, the params of the tab are read from the tabs of the response. + */ + public static final String POSTS_TAB_PARAMS = "Egljb21tdW5pdHnyBgQKAkoA"; + + /** + * The posts tab of a channel, as the web client, which is served it without signing in. + * + * @param params Params of the posts tab. + */ + public static byte[] createPostsTabBody(String channelId, String params, Locale locale) { + try { + JSONObject body = new JSONObject(); + body.put("context", createContext(UPLOADS_CLIENT_NAME, locale)); + body.put("browseId", channelId); + body.put("params", params); + + return body.toString().getBytes(StandardCharsets.UTF_8); + } catch (JSONException ex) { + Logger.printException(() -> "createPostsTabBody failed", ex); + } + return new byte[0]; + } + public static byte[] createBody(String channelId, String query, Locale locale) { try { JSONObject body = new JSONObject(); diff --git a/patches/src/main/resources/addresources/values/youtube/strings.xml b/patches/src/main/resources/addresources/values/youtube/strings.xml index 2a0fcfcf7a..fa083d425e 100644 --- a/patches/src/main/resources/addresources/values/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values/youtube/strings.xml @@ -438,6 +438,9 @@ Reports are public and are reviewed before a channel is added to a list. Removal Channel search results Nothing found on this channel Could not search this channel + Videos + Posts + No posts on this channel match Relevance Most viewed Least viewed From 5afde69cd222108e5bb5fab9b4e11b26e4c81f0b Mon Sep 17 00:00:00 2001 From: MarcaDian <152095496+MarcaDian@users.noreply.github.com> Date: Mon, 26 Oct 2026 10:35:16 +0200 Subject: [PATCH 080/189] chore: Sign releases with the signing key --- .github/workflows/release.yml | 1 + .gitignore | 3 +++ .releaserc | 3 ++- morphe-signing.pub | 1 + package-lock.json | 2 +- settings.gradle.kts | 2 +- 6 files changed, 9 insertions(+), 3 deletions(-) create mode 100644 morphe-signing.pub diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5cde5b987f..3957ca7cb0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -50,6 +50,7 @@ jobs: id: release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + MORPHE_SIGNING_KEY: ${{ secrets.MORPHE_SIGNING_KEY }} - name: Attest if: steps.release.outputs.new_release_published == 'true' diff --git a/.gitignore b/.gitignore index 4df04216c0..82c7d6473a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +# Private key that signs the patches file +morphe-signing.key + # Gradle files .gradle/ build/ diff --git a/.releaserc b/.releaserc index b386cb1375..332cf7089c 100644 --- a/.releaserc +++ b/.releaserc @@ -37,7 +37,8 @@ "releaseJson": { "path": "patches-bundle.json", "downloadUrlTemplate": "https://github.com/${owner}/${repo}/releases/download/v${version}/patches-${version}.mpp", - "signatureUrlTemplate": "" + "signatureUrlTemplate": "", + "publicKeyPath": "morphe-signing.pub" } } ], diff --git a/morphe-signing.pub b/morphe-signing.pub new file mode 100644 index 0000000000..2830f3b310 --- /dev/null +++ b/morphe-signing.pub @@ -0,0 +1 @@ +g8Ys9leTu2yeixojeR35HrNkK6eOSLpA6nNIizRutqI= diff --git a/package-lock.json b/package-lock.json index f28fc94e1e..9e19eadb19 100644 --- a/package-lock.json +++ b/package-lock.json @@ -121,7 +121,7 @@ }, "node_modules/@MorpheApp/changelog": { "version": "0.0.1", - "resolved": "git+ssh://git@github.com/MorpheApp/changelog.git#caa1e931730f097bb6c4dee636b01c2c24ccd72d", + "resolved": "git+ssh://git@github.com/MorpheApp/changelog.git#11c6c7257d02be913db9b3e62e559598714caaf6", "dev": true, "license": "MIT", "dependencies": { diff --git a/settings.gradle.kts b/settings.gradle.kts index 3efb7b7acf..cf2d2c3f61 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -20,7 +20,7 @@ pluginManagement { } plugins { - id("app.morphe.patches") version "2.0.0" + id("app.morphe.patches") version "2.1.0-dev.1" id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0" } From a4ced1511cdcc3388098169cf783454c09f15a07 Mon Sep 17 00:00:00 2001 From: MarcaDian <152095496+MarcaDian@users.noreply.github.com> Date: Mon, 26 Oct 2026 12:39:54 +0200 Subject: [PATCH 081/189] chore: Update the changelog plugin --- package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 9e19eadb19..d70f2e579d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -121,7 +121,7 @@ }, "node_modules/@MorpheApp/changelog": { "version": "0.0.1", - "resolved": "git+ssh://git@github.com/MorpheApp/changelog.git#11c6c7257d02be913db9b3e62e559598714caaf6", + "resolved": "git+ssh://git@github.com/MorpheApp/changelog.git#f3d88b22e0276c44e76907bed292f3cdd96873f9", "dev": true, "license": "MIT", "dependencies": { From 68b86282725f73c3a92602c792457371ddb88c8e Mon Sep 17 00:00:00 2001 From: MarcaDian <152095496+MarcaDian@users.noreply.github.com> Date: Mon, 26 Oct 2026 13:03:36 +0200 Subject: [PATCH 082/189] fix(YouTube - Player icon style): Add Ionicons picture in picture button icon --- .../drawable/morphe_pip_button_ionicons.xml | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 patches/src/main/resources/pictureinpicturebutton/drawable/morphe_pip_button_ionicons.xml diff --git a/patches/src/main/resources/pictureinpicturebutton/drawable/morphe_pip_button_ionicons.xml b/patches/src/main/resources/pictureinpicturebutton/drawable/morphe_pip_button_ionicons.xml new file mode 100644 index 0000000000..fbef021b6e --- /dev/null +++ b/patches/src/main/resources/pictureinpicturebutton/drawable/morphe_pip_button_ionicons.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + From ef8dcef6a9ef888a075fb97d963719ddf82d6b42 Mon Sep 17 00:00:00 2001 From: abemurica <2017096+abemurica@users.noreply.github.com> Date: Mon, 26 Oct 2026 05:36:01 -0700 Subject: [PATCH 083/189] fix(YouTube - Premium-style queue): Play the queue when the player Next button is pressed (#3796) --- .../youtube/layout/flyout/AddToQueuePatch.kt | 2 +- .../playlistautoplay/NavigationIntentHook.kt | 52 ++++++++++++++----- 2 files changed, 39 insertions(+), 15 deletions(-) diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/flyout/AddToQueuePatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/flyout/AddToQueuePatch.kt index dee094a8e4..9bb57d7e7b 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/flyout/AddToQueuePatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/flyout/AddToQueuePatch.kt @@ -139,7 +139,7 @@ val addToQueuePatch = bytecodePatch( } } - navigationIntentHook(EXTENSION_LOCAL_QUEUE_CLASS, "shouldCancelNavigation") + navigationIntentHook(EXTENSION_LOCAL_QUEUE_CLASS, "shouldCancelNavigation", includePlayerButtons = true) playerStatusOverrideHook(EXTENSION_LOCAL_QUEUE_CLASS, "shouldCancelEndOfVideo") diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/playlistautoplay/NavigationIntentHook.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/playlistautoplay/NavigationIntentHook.kt index f6d3b1cf70..46ca8f9f48 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/playlistautoplay/NavigationIntentHook.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/playlistautoplay/NavigationIntentHook.kt @@ -8,17 +8,28 @@ package app.morphe.patches.youtube.layout.playlistautoplay import app.morphe.patcher.Fingerprint +import app.morphe.patcher.InstructionLocation.MatchAfterWithin import app.morphe.patcher.extensions.InstructionExtensions.addInstructionsWithLabels +import app.morphe.patcher.methodCall import app.morphe.patcher.patch.BytecodePatchContext import app.morphe.patcher.util.cloneParameters +import app.morphe.patcher.util.numberOfParameterRegisters +import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod import com.android.tools.smali.dexlib2.AccessFlags /** * Hooks the method that starts navigation to another video. * The extension method receives the navigation intent enum and returns true to cancel the navigation. + * + * @param includePlayerButtons Also hook the player Next and Previous button handler, which can load a video + * from a server supplied command without reaching the navigation method. */ context(patchContext: BytecodePatchContext) -internal fun navigationIntentHook(extensionClass: String, extensionMethod: String) { +internal fun navigationIntentHook( + extensionClass: String, + extensionMethod: String, + includePlayerButtons: Boolean = false +) { val enumType = NavigationIntentEnumFingerprint.originalClassDef.type val wrapperClassDef = Fingerprint( @@ -28,19 +39,9 @@ internal fun navigationIntentHook(extensionClass: String, extensionMethod: Strin val wrapperType = wrapperClassDef.type val enumField = wrapperClassDef.fields.first { it.type == enumType } - Fingerprint( - returnType = "V", - parameters = listOf(wrapperType), - custom = { method, classDef -> - method.implementation != null && classDef.methods.any { sibling -> - sibling.implementation != null && - sibling.returnType == "I" && - sibling.parameterTypes.singleOrNull() == wrapperType - } - } - ).matchAll().forEach { match -> - var method = match.method - if (method.implementation!!.registerCount <= 2) { + fun hook(original: MutableMethod) { + var method = original + if (method.implementation!!.registerCount <= method.numberOfParameterRegisters) { method = method.cloneParameters() } @@ -58,4 +59,27 @@ internal fun navigationIntentHook(extensionClass: String, extensionMethod: Strin """ ) } + + // Matches each implementation of the navigation method. These share no common instructions + // (some are empty or only delegate), so the wrapper parameter is what identifies them. + Fingerprint( + accessFlags = listOf(AccessFlags.PUBLIC, AccessFlags.FINAL), + returnType = "V", + parameters = listOf(wrapperType) + ).matchAll().forEach { hook(it.method) } + + if (includePlayerButtons) { + hook( + Fingerprint( + accessFlags = listOf(AccessFlags.PRIVATE, AccessFlags.FINAL), + returnType = "V", + parameters = listOf(wrapperType, "Ljava/lang/Runnable;"), + filters = listOf( + methodCall(parameters = listOf(wrapperType), returnType = "Z"), + methodCall(smali = "Ljava/lang/Runnable;->run()V", location = MatchAfterWithin(3)), + methodCall(parameters = listOf(wrapperType), returnType = "V", location = MatchAfterWithin(3)) + ) + ).method + ) + } } From ef28427f63c30329b33b8d83c439c95b731a4fb6 Mon Sep 17 00:00:00 2001 From: abemurica <2017096+abemurica@users.noreply.github.com> Date: Mon, 26 Oct 2026 06:08:50 -0700 Subject: [PATCH 084/189] fix(YouTube - Premium-style queue): Fix missing titles and stale now playing row (#3795) --- .../shared/ui/SheetBottomDialog.java | 11 +++ .../youtube/patches/LocalQueuePatch.java | 79 ++++++++++++++++--- .../youtube/patches/VideoInformation.java | 1 + .../patches/utils/LocalQueueSheet.java | 28 +++++-- .../youtube/layout/flyout/AddToQueuePatch.kt | 14 ++++ 5 files changed, 118 insertions(+), 15 deletions(-) diff --git a/extensions/shared/library/src/main/java/app/morphe/extension/shared/ui/SheetBottomDialog.java b/extensions/shared/library/src/main/java/app/morphe/extension/shared/ui/SheetBottomDialog.java index 801b2ce781..1dbc1444b7 100644 --- a/extensions/shared/library/src/main/java/app/morphe/extension/shared/ui/SheetBottomDialog.java +++ b/extensions/shared/library/src/main/java/app/morphe/extension/shared/ui/SheetBottomDialog.java @@ -422,6 +422,17 @@ public void cancel() { super.cancel(); } + /** + * Dismisses the dialog at once, without the slide animation. + */ + public void dismissImmediately() { + if (animView != null) { + animView.animate().cancel(); + } + isDismissing = false; + super.dismiss(); + } + /** * Dismisses the dialog by sliding the content downward. * Ensures that dismissal is not triggered multiple times concurrently. diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/LocalQueuePatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/LocalQueuePatch.java index 130c95bcd3..6675eff325 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/LocalQueuePatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/LocalQueuePatch.java @@ -16,9 +16,11 @@ import androidx.annotation.Nullable; import org.json.JSONArray; +import org.json.JSONException; import org.json.JSONObject; import java.io.IOException; +import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.URLEncoder; import java.util.ArrayList; @@ -30,7 +32,8 @@ import app.morphe.extension.shared.Logger; import app.morphe.extension.shared.Utils; import app.morphe.extension.shared.requests.Requester; -import app.morphe.extension.youtube.patches.AddToQueuePatch.QueueType; +import app.morphe.extension.youtube.patches.utils.LocalQueueSheet; +import app.morphe.extension.youtube.patches.utils.requests.ChannelIdRoutes; import app.morphe.extension.youtube.settings.Settings; import app.morphe.extension.youtube.shared.PlayerType; import app.morphe.extension.youtube.shared.ShortsPlayerState; @@ -83,7 +86,7 @@ public Item(String videoId, @Nullable String title, @Nullable String author) { public static boolean isEnabled() { // Playback hooks are installed by Add to queue. A retained setting must not // enable this queue when that patch is not included. - return AddToQueuePatch.isPatchIncluded() && Settings.QUEUE_TYPE.get() == QueueType.LOCAL; + return AddToQueuePatch.isPatchIncluded() && Settings.QUEUE_TYPE.get() == AddToQueuePatch.QueueType.LOCAL; } public static void setListeners(Runnable onChange, Consumer onMetadata) { @@ -226,6 +229,7 @@ public static boolean shouldCancelNavigation(Enum navigationIntent) { } final String name = navigationIntent.name(); + Logger.printDebug(() -> "Navigation intent: " + name); final boolean next = "NEXT".equals(name); if (!next && !"AUTOPLAY".equals(name) && !"AUTONAV".equals(name)) { return false; @@ -366,6 +370,21 @@ private static int indexOf(String videoId) { return -1; } + public static void onVideoChanged() { + notifyChanged(); + } + + /** + * Injection point. + */ + public static void onUserLeaveHint() { + try { + LocalQueueSheet.dismissImmediately(); + } catch (Exception ex) { + Logger.printException(() -> "onUserLeaveHint failure", ex); + } + } + private static void notifyChanged() { Runnable listener = changeListener; if (listener != null) { @@ -431,14 +450,19 @@ public static void fetchMetadataIfNeeded(Item item) { Utils.runOnBackgroundThread(() -> { try { - String watchUrl = URLEncoder.encode("https://www.youtube.com/watch?v=" + item.videoId, "UTF-8"); - HttpURLConnection connection = Requester.openConnection( - "https://www.youtube.com/oembed?format=json&url=" + watchUrl); - connection.setConnectTimeout(CONNECTION_TIMEOUT_MILLISECONDS); - connection.setReadTimeout(CONNECTION_TIMEOUT_MILLISECONDS); - JSONObject json = Requester.parseJSONObjectAndDisconnect(connection); - String title = json.optString("title"); - String author = json.optString("author_name"); + JSONObject details = fetchOEmbed(item.videoId); + String title; + String author; + if (details != null) { + title = details.optString("title"); + author = details.optString("author_name"); + } else { + // oEmbed has no title for videos whose uploader disabled embedding. + details = fetchPlayerDetails(item.videoId); + if (details == null) return; + title = details.optString("title"); + author = details.optString("author"); + } if (title.isEmpty() && author.isEmpty()) return; if (!title.isEmpty()) item.title = title; @@ -462,4 +486,39 @@ public static void fetchMetadataIfNeeded(Item item) { } }); } + + @Nullable + private static JSONObject fetchOEmbed(String videoId) throws IOException, JSONException { + String watchUrl = URLEncoder.encode("https://www.youtube.com/watch?v=" + videoId, "UTF-8"); + HttpURLConnection connection = Requester.openConnection( + "https://www.youtube.com/oembed?format=json&url=" + watchUrl); + connection.setConnectTimeout(CONNECTION_TIMEOUT_MILLISECONDS); + connection.setReadTimeout(CONNECTION_TIMEOUT_MILLISECONDS); + if (connection.getResponseCode() != Requester.HTTP_STATUS_CODE_SUCCESS) { + Logger.printDebug(() -> "oEmbed request failed for: " + videoId); + connection.disconnect(); + return null; + } + return Requester.parseJSONObjectAndDisconnect(connection); + } + + @Nullable + private static JSONObject fetchPlayerDetails(String videoId) throws IOException, JSONException { + byte[] requestBody = ChannelIdRoutes.createBody(videoId); + HttpURLConnection connection = ChannelIdRoutes.getConnection(ChannelIdRoutes.GET_TITLE); + connection.setFixedLengthStreamingMode(requestBody.length); + try (OutputStream output = connection.getOutputStream()) { + output.write(requestBody); + } + if (connection.getResponseCode() != Requester.HTTP_STATUS_CODE_SUCCESS) { + Logger.printDebug(() -> "Player request failed for: " + videoId); + connection.disconnect(); + return null; + } + JSONObject videoDetails = Requester.parseJSONObjectAndDisconnect(connection).optJSONObject("videoDetails"); + if (videoDetails == null) { + Logger.printDebug(() -> "Player response has no video details for: " + videoId); + } + return videoDetails; + } } diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/VideoInformation.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/VideoInformation.java index 1b97a09cd7..ee3ea36759 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/VideoInformation.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/VideoInformation.java @@ -460,6 +460,7 @@ public static void setVideoId(String newlyLoadedVideoId) { if (!playerVideo.videoId.equals(newlyLoadedVideoId)) { Logger.printDebug(() -> "New video ID: " + newlyLoadedVideoId); playerVideo.videoId = newlyLoadedVideoId; + LocalQueuePatch.onVideoChanged(); } } diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/LocalQueueSheet.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/LocalQueueSheet.java index 8e7169655a..a58fcd36fc 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/LocalQueueSheet.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/LocalQueueSheet.java @@ -58,6 +58,19 @@ public final class LocalQueueSheet { + @Nullable + private static LocalQueueSheet current; + + /** + * Closes the sheet without the slide animation, so it is not on screen when the player enters Picture-in-Picture. + */ + public static void dismissImmediately() { + LocalQueueSheet sheet = current; + if (sheet != null && sheet.dialog.isShowing()) { + sheet.dialog.dismissImmediately(); + } + } + public static void show(Context context) { try { new LocalQueueSheet(context).show(); @@ -162,7 +175,9 @@ private void show() { render(); LocalQueuePatch.setListeners(changeListener, metadataListener); + current = this; PipDismissHelper.dismissOnPip(dialog, () -> { + if (current == this) current = null; LocalQueuePatch.clearListeners(changeListener, metadataListener); scrollView.setOnScrollChangeListener(null); drag = null; @@ -452,6 +467,14 @@ private void loadRow(Row row) { } } + // A video that starts playing was requested already as a row of the queue. + if (row == nowPlayingRow && (nowPlayingItem == null || !nowPlayingItem.videoId.equals(row.videoId))) { + nowPlayingItem = new LocalQueuePatch.Item(row.videoId, + emptyToNull(VideoInformation.getVideoTitle()), + emptyToNull(VideoInformation.getChannelName())); + LocalQueuePatch.fetchMetadataIfNeeded(nowPlayingItem); + } + if (!requestedVideoIds.add(row.videoId)) return; if (row.thumbnail.getDrawable() == null) { @@ -459,11 +482,6 @@ private void loadRow(Row row) { } if (row.item != null) { LocalQueuePatch.fetchMetadataIfNeeded(row.item); - } else if (row == nowPlayingRow) { - nowPlayingItem = new LocalQueuePatch.Item(row.videoId, - emptyToNull(VideoInformation.getVideoTitle()), - emptyToNull(VideoInformation.getChannelName())); - LocalQueuePatch.fetchMetadataIfNeeded(nowPlayingItem); } } diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/flyout/AddToQueuePatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/flyout/AddToQueuePatch.kt index 9bb57d7e7b..b7b671c8f4 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/flyout/AddToQueuePatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/flyout/AddToQueuePatch.kt @@ -7,6 +7,8 @@ package app.morphe.patches.youtube.layout.flyout +import app.morphe.patcher.Fingerprint +import app.morphe.patcher.extensions.InstructionExtensions.addInstruction import app.morphe.patcher.extensions.InstructionExtensions.addInstructions import app.morphe.patcher.extensions.InstructionExtensions.addInstructionsWithLabels import app.morphe.patcher.extensions.InstructionExtensions.getInstruction @@ -26,6 +28,7 @@ import app.morphe.patches.youtube.misc.proto.elementProtoParserHookPatch import app.morphe.patches.youtube.misc.settings.PreferenceScreen import app.morphe.patches.youtube.misc.settings.settingsPatch import app.morphe.patches.youtube.shared.Constants.COMPATIBILITY_YOUTUBE +import app.morphe.patches.youtube.shared.YOUTUBE_MAIN_ACTIVITY_CLASS_TYPE import app.morphe.patches.youtube.video.information.playerStatusOverrideHook import app.morphe.patches.youtube.video.information.videoInformationPatch import app.morphe.util.setExtensionIsPatchIncluded @@ -143,6 +146,17 @@ val addToQueuePatch = bytecodePatch( playerStatusOverrideHook(EXTENSION_LOCAL_QUEUE_CLASS, "shouldCancelEndOfVideo") + // The sheet must be gone before the player enters Picture-in-Picture, or the PiP window is zoomed. + Fingerprint( + definingClass = YOUTUBE_MAIN_ACTIVITY_CLASS_TYPE, + name = "onUserLeaveHint", + returnType = "V", + parameters = listOf() + ).method.addInstruction( + 0, + "invoke-static { }, $EXTENSION_LOCAL_QUEUE_CLASS->onUserLeaveHint()V" + ) + setExtensionIsPatchIncluded(EXTENSION_CLASS) } } From c710042f6cf3f068141e89cd833dd580d7b64b8c Mon Sep 17 00:00:00 2001 From: MarcaDian <152095496+MarcaDian@users.noreply.github.com> Date: Mon, 26 Oct 2026 16:37:06 +0200 Subject: [PATCH 085/189] fix(YouTube - Playback speed): Use the bold icon for the Shorts player menu item --- .../playback/speed/ShortsPlaybackSpeedFlyoutPatch.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/playback/speed/ShortsPlaybackSpeedFlyoutPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/playback/speed/ShortsPlaybackSpeedFlyoutPatch.java index efde37ac2c..a3bfb9097f 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/playback/speed/ShortsPlaybackSpeedFlyoutPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/playback/speed/ShortsPlaybackSpeedFlyoutPatch.java @@ -20,6 +20,7 @@ import app.morphe.extension.youtube.patches.VideoInformation; import app.morphe.extension.youtube.patches.utils.FlyoutUtils; import app.morphe.extension.youtube.settings.Settings; +import app.morphe.extension.youtube.settings.YouTubeActivityHook; import app.morphe.extension.youtube.shared.EngagementPanel; import app.morphe.extension.youtube.shared.ShortsPlayerState; @@ -45,6 +46,10 @@ public final class ShortsPlaybackSpeedFlyoutPatch { */ private static final long SPEED_MENU_DELAY_MILLISECONDS = 100; + private static final String ICON_NAME = YouTubeActivityHook.USE_BOLD_ICONS + ? "yt_outline_experimental_play_circle_half_dashed_vd_theme_24" + : "yt_outline_play_arrow_half_circle_black_24"; + public static boolean isPatchIncluded() { return false; // Modified during patching. } @@ -81,7 +86,7 @@ public static int addFlyoutButton(Object flyoutPanel, int nextButtonIndex) { try { return FlyoutUtils.addFlyoutButton( flyoutPanel, - ResourceUtils.getDrawable("yt_outline_play_arrow_half_circle_black_24"), + ResourceUtils.getDrawable(ICON_NAME), str("morphe_shorts_playback_speed_flyout_title", VideoInformation.formatSpeedStringX(VideoInformation.getPlaybackSpeed())), v -> { From 93c689374b0ef72cb97d6c73881ecdb132ce7e91 Mon Sep 17 00:00:00 2001 From: 0xrxL <127248639+0xrxL@users.noreply.github.com> Date: Mon, 26 Oct 2026 16:04:12 +0100 Subject: [PATCH 086/189] fix(YouTube Music - Downloads): Fix song menu downloads for accounts without Premium (#4086) --- .../music/patches/DownloadsPatch.java | 115 +++++++++++------- .../lyrics/derive/LyricsRomanizer.java | 1 + .../lyrics/romanizers/ScriptDetector.java | 1 + .../interaction/downloads/DownloadsPatch.kt | 15 +++ .../interaction/downloads/Fingerprints.kt | 26 ++++ 5 files changed, 112 insertions(+), 46 deletions(-) diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/DownloadsPatch.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/DownloadsPatch.java index 75511af3f8..2d692791a3 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/DownloadsPatch.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/DownloadsPatch.java @@ -19,6 +19,7 @@ import java.nio.charset.StandardCharsets; import java.util.Locale; import java.util.Map; +import java.util.regex.Pattern; import app.morphe.extension.music.patches.downloads.LocalDownloadManager; import app.morphe.extension.music.settings.Settings; @@ -48,8 +49,14 @@ public interface ProtocolBufferFieldInterface { private static volatile String downloadButtonLabel = ""; + /** Command field of the 'ypc/get_offline_upsell' endpoint. */ + private static final int OFFLINE_UPSELL_ENDPOINT = 133724106; + private static final Pattern VIDEO_ID = Pattern.compile("[A-Za-z0-9_-]{11}"); + private static volatile long lastFlyoutDownloadTime; private static volatile long lastMainPlayerDownloadTime; + /** When the player button was last left to the app, because only menus are overridden. */ + private static volatile long lastStockPlayerButtonTime; private static volatile long lastLocalDownloadsOpenTime; /** Browse id of the stock offline tab, which the local catalogue replaces. */ private static final byte[] OFFLINE_BROWSE_ID = @@ -155,23 +162,70 @@ private static boolean isViewInsideDialog(@Nullable Object viewObj) { return false; } + /** + * The player button is handled by the command resolver, so a Download command that reaches + * the app is from a menu, unless the player button was just left to the app. + */ + private static boolean onMenuDownload(String videoId) { + if (!isFlyoutMenuOverridden()) return false; + + final long now = System.currentTimeMillis(); + if (now - lastStockPlayerButtonTime < IGNORE_DOUBLE_CLICK_DURATION_MS) return false; + // The song menu stays open after Download, so repeated taps can repeat the request. + // Keep this timer separate from the player button so a menu download of another song is not blocked. + if (now - lastFlyoutDownloadTime < IGNORE_DOUBLE_CLICK_DURATION_MS) return true; + lastFlyoutDownloadTime = now; + + Logger.printDebug(() -> "Menu download of " + videoId); + startDownload(videoId, flyoutMenuUsesExternalDownloader()); + return true; + } + + /** + * The endpoint holds the video ID, or encoded parameters with the video ID in field 4. + * + * @return The video ID, or an empty string for albums and playlists. + */ + private static String getOfflineUpsellVideoId(byte[] command) { + String value = new ProtoMessage(command).messageField(OFFLINE_UPSELL_ENDPOINT).stringField(1); + if (value.isEmpty() || VIDEO_ID.matcher(value).matches()) return value; + + return new ProtoMessage(Base64.decode(Uri.decode(value), Base64.URL_SAFE)).stringField(4); + } + /** * Injection point. + * Called for accounts that are allowed to save the song. */ public static boolean offlineVideoEndpointOnClick(String videoId) { try { - if (!isActionButtonOverridden()) { + Utils.verifyOnMainThread(); + return onMenuDownload(videoId); + } catch (Exception ex) { + Logger.printException(() -> "offlineVideoEndpointOnClick failure", ex); + return false; + } + } + + /** + * Injection point. + * Called for accounts that are offered Premium instead of saving the song. + */ + public static boolean offlineUpsellOnClick(@Nullable Object command) { + try { + if (!isFlyoutMenuOverridden() + || !(command instanceof ProtocolBufferFieldInterface message)) { return false; } Utils.verifyOnMainThread(); - long now = System.currentTimeMillis(); - if (now - lastMainPlayerDownloadTime < IGNORE_DOUBLE_CLICK_DURATION_MS) return true; - lastMainPlayerDownloadTime = now; - startDownload(videoId, !inAppDownloads()); - return true; + String videoId = getOfflineUpsellVideoId(message.toByteArray()); + // Albums and playlists are left to the app. + if (videoId.isEmpty()) return false; + + return onMenuDownload(videoId); } catch (Exception ex) { - Logger.printException(() -> "offlineVideoEndpointOnClick failure", ex); + Logger.printException(() -> "offlineUpsellOnClick failure", ex); return false; } } @@ -185,6 +239,7 @@ private static boolean isPlayerDownloadButton(@Nullable Map map) return false; } CharSequence description = senderViewGroup.getContentDescription(); + //noinspection SizeReplaceableByIsEmpty if (description == null || description.length() == 0) return false; String value = description.toString().toLowerCase(Locale.ROOT); @@ -222,12 +277,13 @@ public static boolean commandResolverOnClick(ProtocolBufferFieldInterface p1, Ma } } - Object viewObj = map.get(ELEMENTS_SENDER_VIEW); - final boolean inDialog = isViewInsideDialog(viewObj); // Song menus in search results, playlists and the queue share the player's Download label. // Only the player action bar button should use the playing song's ID. - if (!inDialog && isPlayerDownloadButton(map)) { - if (!isActionButtonOverridden()) return false; + if (!isViewInsideDialog(map.get(ELEMENTS_SENDER_VIEW)) && isPlayerDownloadButton(map)) { + if (!isActionButtonOverridden()) { + lastStockPlayerButtonTime = System.currentTimeMillis(); + return false; + } String videoId = VideoInformation.getVideoId(); if (videoId == null || videoId.isEmpty()) return false; @@ -240,41 +296,8 @@ public static boolean commandResolverOnClick(ProtocolBufferFieldInterface p1, Ma startDownload(videoId, !inAppDownloads()); return true; } - - String downloadParams = new ProtoMessage(commandBytes) - .messageField(133724106).stringField(1); - if (!downloadParams.isEmpty()) { - if (viewObj == null) { - Logger.printDebug(() -> "Ignored programmatic download click (no sender_view)."); - return false; - } - - Logger.printDebug(() -> "Flyout isDownloadClick"); - // Field 4 contains the video ID selected by this Download command. - // The map's endpoint.tag entry can still refer to a previously opened player menu. - String targetId = new ProtoMessage(Base64.decode( - Uri.decode(downloadParams), Base64.URL_SAFE)).stringField(4); - - if (!targetId.isEmpty()) { - // The song menu stays open after Download, so repeated taps can repeat the request. - // Keep this timer separate from the player button so a menu download of another song is not blocked. - final long now = System.currentTimeMillis(); - if (now - lastFlyoutDownloadTime < IGNORE_DOUBLE_CLICK_DURATION_MS) { - return true; - } - lastFlyoutDownloadTime = now; - final String flyoutId = targetId; - Logger.printDebug(() -> "Flyout download of " + flyoutId - + " inDialog=" + inDialog); - startDownload(targetId, flyoutMenuUsesExternalDownloader()); - return true; - - } else { - // Album and playlist commands have no video ID in field 4. - // Let YouTube Music handle them without consuming the song download timer. - return false; - } - } + // Menu Download commands have no sender view, and are handled when they reach + // the offline video endpoint or the offline upsell. } catch (Exception ex) { Logger.printException(() -> "commandResolverOnClick failure", ex); } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/derive/LyricsRomanizer.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/derive/LyricsRomanizer.java index ef74254183..be9f31b05f 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/derive/LyricsRomanizer.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/derive/LyricsRomanizer.java @@ -774,6 +774,7 @@ private static List toLines(List texts) { } public static boolean isPurelyLatinScript(@Nullable CharSequence text) { + //noinspection SizeReplaceableByIsEmpty if (text == null || text.length() == 0) return true; for (int i = 0; i < text.length(); ) { int cp = Character.codePointAt(text, i); diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/ScriptDetector.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/ScriptDetector.java index 75a2d46f88..ec92fffb9a 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/ScriptDetector.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/ScriptDetector.java @@ -147,6 +147,7 @@ public static boolean containsRtl(@Nullable CharSequence text) { * Requires RTL characters to meet or exceed LTR characters, avoiding false positives from stray symbols. */ public static boolean isLineRtl(@Nullable CharSequence text) { + //noinspection SizeReplaceableByIsEmpty if (text == null || text.length() == 0) { return false; } diff --git a/patches/src/main/kotlin/app/morphe/patches/music/interaction/downloads/DownloadsPatch.kt b/patches/src/main/kotlin/app/morphe/patches/music/interaction/downloads/DownloadsPatch.kt index 67ae2540ee..c77b9720e2 100644 --- a/patches/src/main/kotlin/app/morphe/patches/music/interaction/downloads/DownloadsPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/music/interaction/downloads/DownloadsPatch.kt @@ -141,5 +141,20 @@ val downloadsPatch = bytecodePatch( """ ) } + + // Accounts offered Premium get an upsell command from the Download option instead. + offlineUpsellCommandFingerprint( + OfflineUpsellRequestFingerprint.classDef.type + ).method.addInstructionsWithLabels( + 0, + """ + invoke-static/range { p1 .. p1 }, $EXTENSION_CLASS->offlineUpsellOnClick(Ljava/lang/Object;)Z + move-result v0 + if-eqz v0, :show_upsell + return-void + :show_upsell + nop + """ + ) } } diff --git a/patches/src/main/kotlin/app/morphe/patches/music/interaction/downloads/Fingerprints.kt b/patches/src/main/kotlin/app/morphe/patches/music/interaction/downloads/Fingerprints.kt index 050f1945b4..2ad0301021 100644 --- a/patches/src/main/kotlin/app/morphe/patches/music/interaction/downloads/Fingerprints.kt +++ b/patches/src/main/kotlin/app/morphe/patches/music/interaction/downloads/Fingerprints.kt @@ -10,6 +10,7 @@ package app.morphe.patches.music.interaction.downloads import app.morphe.patcher.Fingerprint import app.morphe.patcher.anyInstruction import app.morphe.patcher.methodCall +import app.morphe.patcher.newInstance import app.morphe.patcher.string import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode @@ -25,6 +26,31 @@ internal object CommandResolverFingerprint : Fingerprint( ) ) +/** + * Request of the 'ypc/get_offline_upsell' endpoint. + */ +internal object OfflineUpsellRequestFingerprint : Fingerprint( + accessFlags = listOf(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR), + returnType = "V", + filters = listOf( + string("ypc/get_offline_upsell") + ) +) + +/** + * Handles the Download command of accounts that are offered Premium instead, + * by showing a progress dialog and then the Premium upsell of the endpoint. + */ +internal fun offlineUpsellCommandFingerprint(offlineUpsellRequestType: String) = Fingerprint( + accessFlags = listOf(AccessFlags.PUBLIC, AccessFlags.FINAL), + returnType = "V", + parameters = listOf("L", "Ljava/util/Map;"), + filters = listOf( + string("com.google.android.libraries.youtube.logging.interaction_logger"), + newInstance(offlineUpsellRequestType) + ) +) + internal object OfflineVideoEndpointFingerprint : Fingerprint( accessFlags = listOf(AccessFlags.PUBLIC, AccessFlags.FINAL), returnType = "V", From e85aac635e07f9171d412d8ac93467713d496706 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Mon, 26 Oct 2026 15:06:32 +0000 Subject: [PATCH 087/189] chore(Downloads): Change default setting --- .../morphe/extension/shared/settings/SharedYouTubeSettings.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/settings/SharedYouTubeSettings.java b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/settings/SharedYouTubeSettings.java index 6bd2718b80..ca877fd59c 100644 --- a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/settings/SharedYouTubeSettings.java +++ b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/settings/SharedYouTubeSettings.java @@ -111,7 +111,7 @@ public class SharedYouTubeSettings extends BaseSettings { public static final LongSetting EXTERNAL_POTOKEN_PROVIDER_EXPIRED_DIALOG_LAST_SHOWN = new LongSetting("morphe_external_potoken_provider_expired_dialog_last_shown", 0L, false, false); // Downloads - public static final EnumSetting DOWNLOAD_MODE = new EnumSetting<>("morphe_download_mode", DownloadMode.DEFAULT); + public static final EnumSetting DOWNLOAD_MODE = new EnumSetting<>("morphe_download_mode", DownloadMode.IN_APP); // YouTube only. public static final BooleanSetting EXTERNAL_DOWNLOADER_OVERLAY_BUTTON = new BooleanSetting("morphe_external_downloader_overlay_button", FALSE, true); // YouTube Music only. From 00a4bf457e8df233b21e6f73b244b0c12e91e53e Mon Sep 17 00:00:00 2001 From: urro Date: Mon, 26 Oct 2026 12:07:56 -0400 Subject: [PATCH 088/189] feat(YouTube Music - Third-party lyrics): Streamline lyrics viewing experience (#4002) --- .github/workflows/build_pull_request.yml | 2 +- extensions/music/build.gradle.kts | 1 + .../music/patches/lyrics/LyricsManager.java | 78 ++- .../patches/lyrics/LyricsPresentation.java | 162 +++++ .../music/patches/lyrics/SkipSegments.java | 10 +- .../patches/lyrics/model/ArtistLineCycle.java | 60 ++ .../lyrics/model/LyricsTextFormatter.java | 97 +++ .../lyrics/model/MiniLyricsFormatter.java | 563 ++++++++++++++++++ .../lyrics/model/MiniLyricsTimeline.java | 190 ++++++ .../lyrics/model/PairedPunctuation.java | 222 +++++++ .../music/patches/lyrics/model/TrackInfo.java | 4 - .../patches/lyrics/model/TrackVersion.java | 109 ++++ .../lyrics/requests/DeezerProvider.java | 73 +++ .../lyrics/requests/LRCLIBProvider.java | 61 +- .../lyrics/session/LockScreenLyrics.java | 54 +- .../session/LyricNotificationRefresh.java | 69 +++ .../lyrics/session/MiniplayerLyrics.java | 24 +- .../lyrics/ui/LyricsPanelInstaller.java | 27 +- .../patches/lyrics/ui/LyricsPanelView.java | 44 +- .../patches/lyrics/ui/LyricsSpanBuilder.java | 42 +- .../extension/music/settings/Settings.java | 26 +- .../LyricsEncapsulationPreference.java | 49 ++ .../preference/LyricsFieldsDialog.java | 78 +++ .../LyricsLineLimitsPreference.java | 75 +++ .../LyricsTrackFormatPreference.java | 72 +++ .../lyrics/model/ArtistLineCycleTest.java | 56 ++ .../lyrics/model/EncapsulationRepeatTest.java | 36 ++ .../lyrics/model/LyricsPresentationTest.java | 177 ++++++ .../lyrics/model/MiniLyricsGapTest.java | 135 +++++ .../lyrics/model/PairedPunctuationTest.java | 78 +++ .../patches/lyrics/model/TrackFormatTest.java | 114 ++++ .../lyrics/model/TrackVersionTest.java | 65 ++ .../requests/LRCLIBSyncedFirstTest.java | 39 ++ .../music/layout/lyrics/LyricsPatch.kt | 34 +- .../addresources/values/music/strings.xml | 29 +- 35 files changed, 2855 insertions(+), 100 deletions(-) create mode 100644 extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/LyricsPresentation.java create mode 100644 extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/ArtistLineCycle.java create mode 100644 extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/LyricsTextFormatter.java create mode 100644 extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/MiniLyricsFormatter.java create mode 100644 extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/MiniLyricsTimeline.java create mode 100644 extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/PairedPunctuation.java create mode 100644 extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/TrackVersion.java create mode 100644 extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/session/LyricNotificationRefresh.java create mode 100644 extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsEncapsulationPreference.java create mode 100644 extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsFieldsDialog.java create mode 100644 extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsLineLimitsPreference.java create mode 100644 extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsTrackFormatPreference.java create mode 100644 extensions/music/src/test/java/app/morphe/extension/music/patches/lyrics/model/ArtistLineCycleTest.java create mode 100644 extensions/music/src/test/java/app/morphe/extension/music/patches/lyrics/model/EncapsulationRepeatTest.java create mode 100644 extensions/music/src/test/java/app/morphe/extension/music/patches/lyrics/model/LyricsPresentationTest.java create mode 100644 extensions/music/src/test/java/app/morphe/extension/music/patches/lyrics/model/MiniLyricsGapTest.java create mode 100644 extensions/music/src/test/java/app/morphe/extension/music/patches/lyrics/model/PairedPunctuationTest.java create mode 100644 extensions/music/src/test/java/app/morphe/extension/music/patches/lyrics/model/TrackFormatTest.java create mode 100644 extensions/music/src/test/java/app/morphe/extension/music/patches/lyrics/model/TrackVersionTest.java create mode 100644 extensions/music/src/test/java/app/morphe/extension/music/patches/lyrics/requests/LRCLIBSyncedFirstTest.java diff --git a/.github/workflows/build_pull_request.yml b/.github/workflows/build_pull_request.yml index 3056d3ca3b..92770cf221 100644 --- a/.github/workflows/build_pull_request.yml +++ b/.github/workflows/build_pull_request.yml @@ -29,7 +29,7 @@ jobs: - name: Test env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: ./gradlew :patches:test :extensions:youtube:testDebugUnitTest --no-daemon + run: ./gradlew :patches:test :extensions:youtube:testDebugUnitTest :extensions:music:testDebugUnitTest --no-daemon # buildAndroid adds DEX to the jar in place. A later JVM task can recreate # that same archive without DEX, so Android packaging must run last. diff --git a/extensions/music/build.gradle.kts b/extensions/music/build.gradle.kts index 78af54d7b6..650028c071 100644 --- a/extensions/music/build.gradle.kts +++ b/extensions/music/build.gradle.kts @@ -1,6 +1,7 @@ import com.android.build.api.dsl.ApplicationExtension dependencies { + testImplementation(libs.junit) compileOnly(libs.morphe.extensions.library) compileOnly(project(":extensions:shared-youtube:library")) compileOnly(project(":extensions:shared-youtube:stub")) diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/LyricsManager.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/LyricsManager.java index 4fd866b9b0..fcff8787e0 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/LyricsManager.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/LyricsManager.java @@ -34,19 +34,19 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; -import app.morphe.extension.music.patches.lyrics.derive.LyricsRomanizer; import app.morphe.extension.music.patches.lyrics.derive.SectionDetector; import app.morphe.extension.music.patches.lyrics.model.Lyrics; import app.morphe.extension.music.patches.lyrics.model.LyricsBracketSplitter; import app.morphe.extension.music.patches.lyrics.model.LyricsFilter; import app.morphe.extension.music.patches.lyrics.model.LyricsFilterDefaults; import app.morphe.extension.music.patches.lyrics.model.LyricsLine; -import app.morphe.extension.music.patches.lyrics.model.LyricsMerge; import app.morphe.extension.music.patches.lyrics.model.LyricsPreference; import app.morphe.extension.music.patches.lyrics.model.MetadataCleaner; +import app.morphe.extension.music.patches.lyrics.model.MiniLyricsFormatter; import app.morphe.extension.music.patches.lyrics.model.TrackInfo; -import app.morphe.extension.music.patches.lyrics.model.Word; +import app.morphe.extension.music.patches.lyrics.model.TrackVersion; import app.morphe.extension.music.patches.lyrics.parsers.CharactersConverter; +import app.morphe.extension.music.patches.lyrics.requests.DeezerProvider; import app.morphe.extension.music.patches.lyrics.requests.LocalLyricsFetcher; import app.morphe.extension.music.patches.lyrics.requests.LunaBeatProvider; import app.morphe.extension.music.patches.lyrics.requests.LyricsProvider; @@ -157,6 +157,10 @@ private void finishFetches(List> futures) { @Nullable private volatile Uri currentMediaUri; + /** Version of the current track, for {v} of the track format. Null until it is looked up. */ + @Nullable + private volatile TrackVersion currentVersion; + /** Raw (un-cleaned) title/artist of the current track, used for the MediaStore lookup. */ @Nullable private String currentRawTitle; @@ -288,8 +292,6 @@ private void stopFrameCallback() { } } - private int lastHighlightedIndex = -1; - private int temporaryOffsetMs = 0; private LyricsManager() { @@ -303,6 +305,31 @@ private LyricsManager() { }); } + /** + * Looks up the track's version on Deezer, only when the miniplayer or the media session + * show the track format and it has {v}. If the lookup fails, the title shows without {v}. + */ + private void requestVersion(TrackInfo track) { + if (!Settings.LYRICS_ENABLED.get() + || !(Settings.LYRICS_MINIPLAYER.get() || Settings.LYRICS_MEDIASESSION.get()) + || !MiniLyricsFormatter.usesVariant(Settings.LYRICS_MINI_TRACK_FORMAT.get())) { + return; + } + final TrackInfo query = TrackVersion.query(track, currentRawTitle, currentRawArtist); + runOnLookupThread(() -> { + final TrackVersion version; + try { + version = DeezerProvider.fetchVersion(query); + } catch (Exception ex) { + Logger.printInfo(() -> "Deezer version lookup failed", ex); + return; + } + if (track.equals(currentTrack)) { + currentVersion = version; + } + }); + } + private void runOnLookupThread(Runnable body) { lookupExecutor.execute(() -> runContained(body)); } @@ -401,6 +428,11 @@ public TrackInfo getCurrentTrack() { return currentTrack; } + @Nullable + public TrackVersion getCurrentVersion() { + return currentVersion; + } + @Nullable public String getCurrentRawTitle() { return currentRawTitle; @@ -426,7 +458,6 @@ private void resetPosition() { lastFrameTimeUptimeMs = -1; estimate = 0; smoothPosition = 0; - lastHighlightedIndex = -1; } private void updateSample(long newPosition, long now) { @@ -684,6 +715,10 @@ private void commitTrackChange(TrackInfo track, String rawTitle, String rawArtis currentMediaUri = mediaUri; currentTrack = track; currentVideoId = videoId; + if (!sameTrack) { + currentVersion = null; + requestVersion(track); + } // Only reached on a real track change, so a loop cannot discard the offset it comes round for again. temporaryOffsetMs = TrackOffsetStore.get(track); // A rebinding keeps the previous track's lyric, which is what makes it worth @@ -835,7 +870,6 @@ private void load(TrackInfo track, boolean keepLyrics) { } shownFingerprints.clear(); filteredCache.clear(); - lastHighlightedIndex = -1; suppressForRequest = -1; staleLyricsDropped = false; preferredFingerprint = null; @@ -2295,25 +2329,25 @@ private void detectSections(@Nullable Lyrics lyrics) { }); } - @NonNull - public String getCurrentLineText() { - Lyrics lyrics = currentLyrics; - if (lyrics == null || !lyrics.synced() || lyrics.isEmpty()) { - return ""; - } - final int index = lyrics.indexForPosition(getPositionMs(), lastHighlightedIndex); - lastHighlightedIndex = index; - if (index < 0) { - return ""; - } - String text = lyrics.lines().get(index).text(); - return text == null ? "" : text; - } - public boolean areLyricsSynced() { Lyrics lyrics = currentLyrics; return lyrics != null && lyrics.synced() && !lyrics.isEmpty(); } + /** + * The text the miniplayer and the media session show for the current position. + * See {@link LyricsPresentation#miniLine}. + */ + @Nullable + public String getCurrentLineText() { + Lyrics lyrics = currentLyrics; + if (lyrics == null || !lyrics.synced() || lyrics.isEmpty()) { + return null; + } + TrackInfo track = currentTrack; + final long durationMs = track == null ? 0 : track.durationSeconds() * 1000L; + return LyricsPresentation.miniLine(lyrics, durationMs, getPositionMs(), + currentRawTitle, currentRawArtist); + } } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/LyricsPresentation.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/LyricsPresentation.java new file mode 100644 index 0000000000..a0cb7fad29 --- /dev/null +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/LyricsPresentation.java @@ -0,0 +1,162 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/4002 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.extension.music.patches.lyrics; + +import android.os.SystemClock; + +import androidx.annotation.Nullable; + +import java.util.List; + +import app.morphe.extension.music.patches.lyrics.model.ArtistLineCycle; +import app.morphe.extension.music.patches.lyrics.model.Lyrics; +import app.morphe.extension.music.patches.lyrics.model.LyricsLine; +import app.morphe.extension.music.patches.lyrics.model.LyricsTextFormatter; +import app.morphe.extension.music.patches.lyrics.model.MiniLyricsFormatter; +import app.morphe.extension.music.patches.lyrics.model.MiniLyricsTimeline; +import app.morphe.extension.music.patches.lyrics.model.TrackVersion; +import app.morphe.extension.music.settings.Settings; + +/** + * Display only formatting shared by the lyrics panel, the miniplayer and the media session. + * The lyrics themselves are never changed. Only used on the main thread. + */ +public final class LyricsPresentation { + + @Nullable + private static Lyrics timelineLyrics; + private static long timelineDurationMs; + @Nullable + private static MiniLyricsTimeline timeline; + + @Nullable + private static LyricsLine chunksLine; + private static long chunksEndMs; + private static int chunksLimit; + private static boolean chunksCapitalize; + private static boolean chunksStrip; + private static float chunksMultiplier = 1; + private static List chunks = List.of(); + + private static final ArtistLineCycle artistCycle = new ArtistLineCycle(); + + /** The lyric shown with a track format frame. The line tells apart repeats of the same text. */ + private record FrameKey(@Nullable LyricsLine line, @Nullable String text) { + } + + private LyricsPresentation() { + } + + /** If any of the filters that change the displayed text are on. */ + public static boolean isFormattingEnabled() { + return Settings.LYRICS_FORCE_CAPITALIZE.get() || Settings.LYRICS_STRIP_TRAILING_PUNCTUATION.get(); + } + + public static LyricsTextFormatter.Result format(String text) { + return LyricsTextFormatter.format(text, Settings.LYRICS_FORCE_CAPITALIZE.get(), + Settings.LYRICS_STRIP_TRAILING_PUNCTUATION.get()); + } + + public static String formatText(String text) { + return isFormattingEnabled() ? format(text).text() : text; + } + + /** + * The frames of the track format. A version the backend reported replaces the title with + * the backend's title without the version, which {v} then shows. + */ + private static List trackFrames(@Nullable String title, @Nullable String artist) { + TrackVersion version = LyricsManager.getInstance().getCurrentVersion(); + return MiniLyricsFormatter.trackFrames(Settings.LYRICS_MINI_TRACK_FORMAT.get(), + version == null ? title : version.title(), artist, + version == null ? null : version.variant(), + Settings.LYRICS_MINI_ARTIST_CHAR_LIMIT.get(), + Settings.LYRICS_MINI_FULL_WIDTH_MULTIPLIER.get()); + } + + /** The track format frame currently shown. */ + public static String trackLabel(@Nullable String title, @Nullable String artist) { + return artistCycle.current(trackFrames(title, artist)); + } + + /** Starts the track format frames over, as the track changed. */ + public static void resetTrackCycle() { + artistCycle.reset(); + } + + /** Advances the track format frames along with the lyric, and returns the lyric. */ + @Nullable + private static String present(@Nullable String text, @Nullable LyricsLine line, + @Nullable String title, @Nullable String artist) { + artistCycle.update(trackFrames(title, artist), new FrameKey(line, text), + SystemClock.elapsedRealtime(), Settings.LYRICS_MINI_ARTIST_MIN_MS.get(), + Settings.LYRICS_MINI_ARTIST_MAX_MS.get()); + return text; + } + + /** + * The text the miniplayer and the media session show at a playback position. + * + * @return Null if no lyric applies and the track title should be shown instead. + * An empty string is a blank instrumental frame the user configured. + */ + @Nullable + public static String miniLine(Lyrics lyrics, long durationMs, long positionMs, + @Nullable String title, @Nullable String artist) { + if (timeline == null || lyrics != timelineLyrics || durationMs != timelineDurationMs) { + timeline = new MiniLyricsTimeline(lyrics.lines(), durationMs); + timelineLyrics = lyrics; + timelineDurationMs = durationMs; + } + + List lines = lyrics.lines(); + final long position = MiniLyricsFormatter.selectionPosition(positionMs, + Settings.LYRICS_MINI_NEXT_OFFSET_MS.get()); + MiniLyricsTimeline.Selection selection = timeline.at(position); + + MiniLyricsTimeline.Vocal vocal = selection.vocal(); + if (vocal != null) { + return present(vocalText(lines, vocal, position), vocal.line(), title, artist); + } + + MiniLyricsTimeline.Gap gap = selection.gap(); + MiniLyricsTimeline.Vocal previous = gap == null ? selection.previous() : gap.previous(); + // The text the previous line ended with. + String previousText = previous == null ? null : vocalText(lines, previous, previous.endMs() - 1); + if (gap == null) { + // A pause between lines keeps the previous line. + return present(previousText, previous == null ? null : previous.line(), title, artist); + } + + return present(MiniLyricsFormatter.instrumental(Settings.LYRICS_MINI_INSTRUMENTAL.get(), + gap.startMs(), gap.endMs(), position, title, artist, + previousText == null ? title : previousText), null, title, artist); + } + + private static String vocalText(List lines, MiniLyricsTimeline.Vocal vocal, long positionMs) { + LyricsLine line = vocal.line(); + final int limit = Settings.LYRICS_MINI_CHAR_LIMIT.get(); + final boolean capitalize = Settings.LYRICS_FORCE_CAPITALIZE.get(); + final boolean strip = Settings.LYRICS_STRIP_TRAILING_PUNCTUATION.get(); + final float multiplier = Settings.LYRICS_MINI_FULL_WIDTH_MULTIPLIER.get(); + if (line != chunksLine || vocal.endMs() != chunksEndMs || limit != chunksLimit + || capitalize != chunksCapitalize || strip != chunksStrip + || multiplier != chunksMultiplier) { + chunks = MiniLyricsFormatter.chunks(line, vocal.endMs(), limit, capitalize, strip, multiplier); + chunksLine = line; + chunksEndMs = vocal.endMs(); + chunksLimit = limit; + chunksCapitalize = capitalize; + chunksStrip = strip; + chunksMultiplier = multiplier; + } + + return MiniLyricsFormatter.current(chunks, positionMs, Settings.LYRICS_MINI_ENCAPSULATION.get(), + MiniLyricsFormatter.repeatCount(lines, vocal.index())); + } +} diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/SkipSegments.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/SkipSegments.java index b2fb1cf86e..b826fd6dfe 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/SkipSegments.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/SkipSegments.java @@ -20,7 +20,6 @@ import java.util.Collections; import java.util.Comparator; import java.util.HashMap; -import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ExecutionException; @@ -31,6 +30,7 @@ import java.util.concurrent.TimeoutException; import app.morphe.extension.music.settings.Settings; +import app.morphe.extension.shared.Utils; import app.morphe.extension.shared.requests.Requester; import app.morphe.extension.shared.sponsorblock.SegmentPlaybackController; import app.morphe.extension.shared.sponsorblock.objects.SponsorSegment; @@ -78,12 +78,8 @@ public record Seg(long startMs, long endMs, @NonNull String category, private static final Object LOCK = new Object(); /** Video id to merged segments; empty when the fetch found none. */ - private static final Map> CACHE = new LinkedHashMap<>(16, 0.75f, true) { - @Override - protected boolean removeEldestEntry(Map.Entry> eldest) { - return size() > CACHE_MAX_ENTRIES; - } - }; + private static final Map> CACHE = Utils + .createSizeRestrictedMap(CACHE_MAX_ENTRIES, true); private static final Map>> TASKS = new HashMap<>(); diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/ArtistLineCycle.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/ArtistLineCycle.java new file mode 100644 index 0000000000..df62e286b4 --- /dev/null +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/ArtistLineCycle.java @@ -0,0 +1,60 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/4002 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ +package app.morphe.extension.music.patches.lyrics.model; + +import androidx.annotation.Nullable; + +import java.util.List; +import java.util.Objects; + +/** + * The frames of the track format line, shown in turn. A frame stays for at least the minimum + * time and moves on when the lyric changes, or after the maximum time when it does not. + * The miniplayer and the media session share one cycle, so polling both never advances a + * frame twice. + */ +public final class ArtistLineCycle { + private List frames = List.of(); + /** Identifies the lyric shown with the frame. */ + @Nullable + private Object token; + private int index; + private long shownAt; + private boolean pending; + + public String update(List nextFrames, @Nullable Object nextToken, long now, int minimum, int maximum) { + if (!frames.equals(nextFrames)) { + frames = List.copyOf(nextFrames); + index = 0; + shownAt = now; + pending = false; + } else if (!Objects.equals(token, nextToken)) { + pending = true; + } + token = nextToken; + long elapsed = Math.max(0, now - shownAt); + int min = Math.max(0, minimum); + int max = Math.max(Math.max(1, min), maximum); + if (frames.size() > 1 && elapsed >= min && (pending || elapsed >= max)) { + index = (index + 1) % frames.size(); + shownAt = now; + pending = false; + } + return current(nextFrames); + } + + /** The frame shown, or the first of the frames if they are not the ones being cycled. */ + public String current(List fallback) { + return frames.equals(fallback) && !frames.isEmpty() ? frames.get(index) : fallback.get(0); + } + + public void reset() { + frames = List.of(); + token = null; + pending = false; + } +} diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/LyricsTextFormatter.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/LyricsTextFormatter.java new file mode 100644 index 0000000000..ad16902d63 --- /dev/null +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/LyricsTextFormatter.java @@ -0,0 +1,97 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/4002 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.extension.music.patches.lyrics.model; + +import java.util.Locale; + +/** Final, display-only lyric cleanup, including an old-to-new UTF-16 offset map. */ +public final class LyricsTextFormatter { + public record Result(String text, int[] offsets) { + public int offset(int original) { + return original < 0 ? original : offsets[Math.min(original, offsets.length - 1)]; + } + } + + private LyricsTextFormatter() { } + + public static Result format(String input, boolean capitalize, boolean strip) { + final String text = input == null ? "" : input; + final boolean[] remove = new boolean[text.length()]; + if (strip) { + int lineStart = 0; + while (lineStart <= text.length()) { + int end = text.indexOf('\n', lineStart); + if (end < 0) end = text.length(); + for (int at = end; at > lineStart;) { + final int cp = text.codePointBefore(at); + final int start = at - Character.charCount(cp); + if (!isPunctuation(cp) && !Character.isWhitespace(cp) + && !Character.isSpaceChar(cp)) break; + if (isRedundant(cp) && !isEllipsisDot(text, start, at, cp)) { + for (int i = start; i < at; i++) remove[i] = true; + } + at = start; + } + if (end == text.length()) break; + lineStart = end + 1; + } + } + final StringBuilder out = new StringBuilder(text.length()); + final int[] offsets = new int[text.length() + 1]; + boolean firstCased = true; + for (int at = 0; at < text.length();) { + final int cp = text.codePointAt(at); + final int end = at + Character.charCount(cp); + for (int i = at; i < end; i++) offsets[i] = out.length(); + if (!remove[at]) { + final String own = text.substring(at, end); + if (capitalize && firstCased && (Character.isLowerCase(cp) + || Character.isUpperCase(cp) || Character.isTitleCase(cp))) { + out.append(own.toUpperCase(Locale.ROOT)); + firstCased = false; + } else { + out.append(own); + } + } + if (cp == '\n') firstCased = true; + offsets[end] = out.length(); + at = end; + } + return new Result(out.toString(), offsets); + } + + private static boolean isPunctuation(int cp) { + return switch (Character.getType(cp)) { + case Character.CONNECTOR_PUNCTUATION, Character.DASH_PUNCTUATION, + Character.START_PUNCTUATION, Character.END_PUNCTUATION, + Character.INITIAL_QUOTE_PUNCTUATION, Character.FINAL_QUOTE_PUNCTUATION, + Character.OTHER_PUNCTUATION -> true; + default -> false; + }; + } + + /** A conservative semantic allowlist within Unicode punctuation, not a \p{P} deletion. */ + private static boolean isRedundant(int cp) { + if (Character.getType(cp) != Character.OTHER_PUNCTUATION + || Character.isMirrored(cp) || cp == '\'' || cp == '"') return false; + final String name = Character.getName(cp); + if (name == null || name.contains("QUESTION") || name.contains("EXCLAMATION") + || name.contains("INTERROBANG") || name.contains("ELLIPSIS") + || name.contains("QUOTATION") || name.contains("APOSTROPHE")) return false; + return name.contains("FULL STOP") || name.endsWith("COMMA") + || name.endsWith("SEMICOLON") || name.endsWith("COLON") + || name.endsWith("DANDA") || cp == 0x2024; + } + + private static boolean isEllipsisDot(String text, int start, int end, int cp) { + final String name = Character.getName(cp); + if (cp != 0x2024 && (name == null || !name.contains("FULL STOP"))) return false; + return (start > 0 && text.codePointBefore(start) == cp) + || (end < text.length() && text.codePointAt(end) == cp); + } +} diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/MiniLyricsFormatter.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/MiniLyricsFormatter.java new file mode 100644 index 0000000000..d34fc81284 --- /dev/null +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/MiniLyricsFormatter.java @@ -0,0 +1,563 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/4002 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + * + * East Asian Width data: Unicode 17.0 EastAsianWidth.txt, W/F properties. + * https://www.unicode.org/Public/17.0.0/ucd/EastAsianWidth.txt + * + * UNICODE LICENSE V3 + * + * COPYRIGHT AND PERMISSION NOTICE + * + * Copyright (C) 1991-2026 Unicode, Inc. + * + * NOTICE TO USER: Carefully read the following legal agreement. BY + * DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR + * SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE + * TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT + * DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of data files and any associated documentation (the "Data Files") or + * software and any associated documentation (the "Software") to deal in the + * Data Files or Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, and/or sell + * copies of the Data Files or Software, and to permit persons to whom the + * Data Files or Software are furnished to do so, provided that either (a) + * this copyright and permission notice appear with all copies of the Data + * Files or Software, or (b) this copyright and permission notice appear in + * associated Documentation. + * + * THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY + * KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF + * THIRD PARTY RIGHTS. + * + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE + * BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, + * OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, + * WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, + * ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA + * FILES OR SOFTWARE. + * + * Except as contained in this notice, the name of a copyright holder shall + * not be used in advertising or otherwise to promote the sale, use or other + * dealings in these Data Files or Software without prior written + * authorization of the copyright holder. + */ + +package app.morphe.extension.music.patches.lyrics.model; + +import androidx.annotation.Nullable; + +import java.text.BreakIterator; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** Text of the miniplayer and the media session. The lyrics themselves are never changed. */ +public final class MiniLyricsFormatter { + public static final String DEFAULT_TRACK_FORMAT = "{t} :({v}): - {a}"; + public static final String ARTIST_FIRST_TRACK_FORMAT = "{a} - {t} :({v}):"; + public static final String DEFAULT_ENCAPSULATION = "{lrc} :(x{n}):"; + public static final String DEFAULT_INSTRUMENTAL = "♪\n♪♪\n♪♪♪"; + private static final String LYRIC_PLACEHOLDER = "{lrc}"; + private static final String VARIANT_PLACEHOLDER = "{v}"; + /** Placeholder names, written inside braces in a template. */ + private static final String[] PLACEHOLDERS = {"lrc", "t", "a", "v", "n"}; + /** Marks where a line without spaces was split. */ + public static final String SPLIT_ELLIPSIS = "…"; + /** How many times a track format line is halved at most. */ + private static final int MAX_FRAME_SPLIT_DEPTH = 6; + /** An extended grapheme cluster, so a character with combining marks or an emoji is not split. */ + private static final Pattern GRAPHEME = Pattern.compile("\\X"); + private static final Pattern LINE_BREAK = Pattern.compile("\\R"); + private static final Pattern REPEATED_SPACES = Pattern.compile("[ \\t\\u00A0]{2,}"); + + public record Chunk(String text, long startMs) { + } + + private record Unit(int start, int end, long timeMs) { + } + + private MiniLyricsFormatter() { + } + + public static boolean validEncapsulation(@Nullable String template) { + return template != null && template.contains(LYRIC_PLACEHOLDER); + } + + /** If the track format shows the version, which then has to be looked up. */ + public static boolean usesVariant(@Nullable String template) { + return (template == null ? DEFAULT_TRACK_FORMAT : template).contains(VARIANT_PLACEHOLDER); + } + + public static String trackLabel(@Nullable String template, @Nullable String title, @Nullable String artist) { + return trackLabel(template, title, artist, null); + } + + /** + * {t} is the title, {a} the artist and {v} the variant, such as Acoustic. The variant is only + * known when the backend reports it separately from the title, and is never parsed out of a + * title. Text between unescaped colons only appears when every placeholder in it has a value. + * Each line of the template is one frame of the artist line. + */ + public static String trackLabel(@Nullable String template, @Nullable String title, + @Nullable String artist, @Nullable String variant) { + final Map values = new HashMap<>(); + values.put("t", title == null ? "" : title); + values.put("a", artist == null ? "" : artist); + values.put("v", variant == null ? "" : variant.trim()); + final String rendered = render(template == null ? DEFAULT_TRACK_FORMAT : template, values, true); + final StringBuilder out = new StringBuilder(); + for (String line : LINE_BREAK.split(rendered, -1)) { + // A hidden section leaves the spaces around it behind. + final String tidy = REPEATED_SPACES.matcher(line).replaceAll(" ").trim(); + if (tidy.isEmpty()) continue; + if (out.length() > 0) out.append('\n'); + out.append(tidy); + } + return out.toString(); + } + + /** + * The frames the artist line cycles through: one per template line, and a line wider than the + * limit is split into frames the same way a long lyric is. The split is decided from the whole + * rendered line, not from any single placeholder. + */ + public static List trackFrames(@Nullable String template, @Nullable String title, + @Nullable String artist, @Nullable String variant, + int limit, double fullWidthMultiplier) { + final List frames = new ArrayList<>(); + final String label = trackLabel(template, title, artist, variant); + for (String line : label.split("\n")) { + if (line.isEmpty()) continue; + if (limit <= 0 || width(line, fullWidthMultiplier) <= limit) { + frames.add(line); + continue; + } + splitFrame(line, limit, fullWidthMultiplier, frames, 0); + } + if (frames.isEmpty()) frames.add(""); + return List.copyOf(frames); + } + + /** Halves a frame the way a long lyric is halved, again and again until every part fits. */ + private static void splitFrame(String frame, int limit, double fullWidthMultiplier, + List frames, int depth) { + if (depth >= MAX_FRAME_SPLIT_DEPTH || width(frame, fullWidthMultiplier) <= limit) { + frames.add(frame); + return; + } + final List halves = chunks(new LyricsLine(0, frame), -1, limit, false, false, fullWidthMultiplier); + if (halves.size() < 2) { + frames.add(frame); + return; + } + for (Chunk half : halves) { + final String text = half.text(); + if (text.isEmpty()) continue; + // An ellipsis added at a split does not count against the limit, or a two-character + // part would never fit a limit of two. + final String bare = text.endsWith(SPLIT_ELLIPSIS) + ? text.substring(0, text.length() - SPLIT_ELLIPSIS.length()) : text; + if (bare.equals(frame) || width(bare, fullWidthMultiplier) <= limit) { + frames.add(text); + continue; + } + final int before = frames.size(); + splitFrame(bare, limit, fullWidthMultiplier, frames, depth + 1); + if (!bare.equals(text) && frames.size() > before) { + final int last = frames.size() - 1; + final String lastFrame = frames.get(last); + if (!lastFrame.endsWith(SPLIT_ELLIPSIS)) frames.set(last, lastFrame + SPLIT_ELLIPSIS); + } + } + } + + /** + * Replaces {t} with the title, {a} with the artist and {lrc} with the lyric if one is given. + * This is done in one pass, so a placeholder in a song title is kept as is. + */ + private static String replacePlaceholders(String template, @Nullable String title, + @Nullable String artist, @Nullable String lyric) { + final StringBuilder out = new StringBuilder(); + for (int i = 0, length = template.length(); i < length; ) { + if (template.startsWith("{t}", i)) { + out.append(title == null ? "" : title); + i += 3; + } else if (template.startsWith("{a}", i)) { + out.append(artist == null ? "" : artist); + i += 3; + } else if (lyric != null && template.startsWith(LYRIC_PLACEHOLDER, i)) { + out.append(lyric); + i += LYRIC_PLACEHOLDER.length(); + } else { + out.append(template.charAt(i++)); + } + } + return out.toString(); + } + + /** + * {lrc} is the lyric and {n} its repeat count. Text between unescaped colons appears only when + * the count is above 1; a backslash before a colon keeps it literal. + */ + public static String encapsulate(@Nullable String template, String lyric, int repeat) { + final String source = validEncapsulation(template) ? template : DEFAULT_ENCAPSULATION; + final Map values = new HashMap<>(); + values.put("lrc", lyric); + values.put("n", String.valueOf(repeat)); + return render(source, values, repeat > 1); + } + + /** + * Fills in placeholders in one pass, so a placeholder inside a value is kept as is. Text between + * unescaped colons appears only when every placeholder in it is populated: a non-empty value, + * and for the count {n} a value other than 1. A section without placeholders appears when + * {@code bareShown} is set. A backslash before a colon keeps it literal. + */ + static String render(String template, Map values, boolean bareShown) { + final StringBuilder out = new StringBuilder(); + final StringBuilder segment = new StringBuilder(); + boolean inside = false; + boolean populated = true; + boolean placeholders = false; + for (int i = 0, length = template.length(); i < length; ) { + final char c = template.charAt(i); + if (c == '\\' && i + 1 < length && template.charAt(i + 1) == ':') { + segment.append(':'); + i += 2; + continue; + } + if (c == ':') { + if (!inside || (placeholders ? populated : bareShown)) out.append(segment); + segment.setLength(0); + inside = !inside; + populated = true; + placeholders = false; + i++; + continue; + } + final String name = c == '{' ? placeholderAt(template, i, values) : null; + if (name != null) { + final String value = values.get(name); + if (inside) { + placeholders = true; + if (!isPopulated(name, value)) populated = false; + } + segment.append(value == null ? "" : value); + i += name.length() + 2; + continue; + } + segment.append(c); + i++; + } + if (!inside || (placeholders ? populated : bareShown)) out.append(segment); + return out.toString(); + } + + @Nullable + private static String placeholderAt(String template, int index, Map values) { + for (String name : PLACEHOLDERS) { + if (values.containsKey(name) && template.startsWith("{" + name + "}", index)) return name; + } + return null; + } + + private static boolean isPopulated(String name, @Nullable String value) { + if (value == null || value.isEmpty()) return false; + return !"n".equals(name) || !value.equals("1"); + } + + /** How many times in a row the line at the index is sung, counting up to and including it. */ + public static int repeatCount(List lines, int index) { + final String text = lines.get(index).text(); + int count = 1; + for (int i = index - 1; i >= 0 && lines.get(i).text().equals(text); i--) { + count++; + } + return count; + } + + /** + * The width of the text against a character limit: a full-width character, such as a CJK + * ideograph or kana, counts as the multiplier and any other character as 1. + */ + public static double width(String text, double fullWidthMultiplier) { + final double wide = Double.isFinite(fullWidthMultiplier) && fullWidthMultiplier > 0 + ? fullWidthMultiplier : 1; + final Matcher matcher = GRAPHEME.matcher(text); + double total = 0; + while (matcher.find()) total += isFullWidth(text.codePointAt(matcher.start())) ? wide : 1; + return total; + } + + /** Unicode 17 EastAsianWidth.txt W/F properties, merged into inclusive ranges. + * https://www.unicode.org/Public/17.0.0/ucd/EastAsianWidth.txt + * Distributed under Unicode License V3 (see the file header). + */ + private static final int[] FULL_WIDTH_RANGES = { + 0x1100, 0x115F, 0x231A, 0x231B, 0x2329, 0x232A, 0x23E9, 0x23EC, + 0x23F0, 0x23F0, 0x23F3, 0x23F3, 0x25FD, 0x25FE, 0x2614, 0x2615, + 0x2630, 0x2637, 0x2648, 0x2653, 0x267F, 0x267F, 0x268A, 0x268F, + 0x2693, 0x2693, 0x26A1, 0x26A1, 0x26AA, 0x26AB, 0x26BD, 0x26BE, + 0x26C4, 0x26C5, 0x26CE, 0x26CE, 0x26D4, 0x26D4, 0x26EA, 0x26EA, + 0x26F2, 0x26F3, 0x26F5, 0x26F5, 0x26FA, 0x26FA, 0x26FD, 0x26FD, + 0x2705, 0x2705, 0x270A, 0x270B, 0x2728, 0x2728, 0x274C, 0x274C, + 0x274E, 0x274E, 0x2753, 0x2755, 0x2757, 0x2757, 0x2795, 0x2797, + 0x27B0, 0x27B0, 0x27BF, 0x27BF, 0x2B1B, 0x2B1C, 0x2B50, 0x2B50, + 0x2B55, 0x2B55, 0x2E80, 0x2E99, 0x2E9B, 0x2EF3, 0x2F00, 0x2FD5, + 0x2FF0, 0x303E, 0x3041, 0x3096, 0x3099, 0x30FF, 0x3105, 0x312F, + 0x3131, 0x318E, 0x3190, 0x31E5, 0x31EF, 0x321E, 0x3220, 0x3247, + 0x3250, 0xA48C, 0xA490, 0xA4C6, 0xA960, 0xA97C, 0xAC00, 0xD7A3, + 0xF900, 0xFAFF, 0xFE10, 0xFE19, 0xFE30, 0xFE52, 0xFE54, 0xFE66, + 0xFE68, 0xFE6B, 0xFF01, 0xFF60, 0xFFE0, 0xFFE6, 0x16FE0, 0x16FE4, + 0x16FF0, 0x16FF6, 0x17000, 0x18CD5, 0x18CFF, 0x18D1E, 0x18D80, 0x18DF2, + 0x1AFF0, 0x1AFF3, 0x1AFF5, 0x1AFFB, 0x1AFFD, 0x1AFFE, 0x1B000, 0x1B122, + 0x1B132, 0x1B132, 0x1B150, 0x1B152, 0x1B155, 0x1B155, 0x1B164, 0x1B167, + 0x1B170, 0x1B2FB, 0x1D300, 0x1D356, 0x1D360, 0x1D376, 0x1F004, 0x1F004, + 0x1F0CF, 0x1F0CF, 0x1F18E, 0x1F18E, 0x1F191, 0x1F19A, 0x1F200, 0x1F202, + 0x1F210, 0x1F23B, 0x1F240, 0x1F248, 0x1F250, 0x1F251, 0x1F260, 0x1F265, + 0x1F300, 0x1F320, 0x1F32D, 0x1F335, 0x1F337, 0x1F37C, 0x1F37E, 0x1F393, + 0x1F3A0, 0x1F3CA, 0x1F3CF, 0x1F3D3, 0x1F3E0, 0x1F3F0, 0x1F3F4, 0x1F3F4, + 0x1F3F8, 0x1F43E, 0x1F440, 0x1F440, 0x1F442, 0x1F4FC, 0x1F4FF, 0x1F53D, + 0x1F54B, 0x1F54E, 0x1F550, 0x1F567, 0x1F57A, 0x1F57A, 0x1F595, 0x1F596, + 0x1F5A4, 0x1F5A4, 0x1F5FB, 0x1F64F, 0x1F680, 0x1F6C5, 0x1F6CC, 0x1F6CC, + 0x1F6D0, 0x1F6D2, 0x1F6D5, 0x1F6D8, 0x1F6DC, 0x1F6DF, 0x1F6EB, 0x1F6EC, + 0x1F6F4, 0x1F6FC, 0x1F7E0, 0x1F7EB, 0x1F7F0, 0x1F7F0, 0x1F90C, 0x1F93A, + 0x1F93C, 0x1F945, 0x1F947, 0x1F9FF, 0x1FA70, 0x1FA7C, 0x1FA80, 0x1FA8A, + 0x1FA8E, 0x1FAC6, 0x1FAC8, 0x1FAC8, 0x1FACD, 0x1FADC, 0x1FADF, 0x1FAEA, + 0x1FAEF, 0x1FAF8, 0x20000, 0x2FFFD, 0x30000, 0x3FFFD, + }; + + static boolean isFullWidth(int cp) { + int lo = 0, hi = FULL_WIDTH_RANGES.length / 2 - 1; + while (lo <= hi) { + final int mid = (lo + hi) >>> 1; + if (cp < FULL_WIDTH_RANGES[mid * 2]) hi = mid - 1; + else if (cp > FULL_WIDTH_RANGES[mid * 2 + 1]) lo = mid + 1; + else return true; + } + return false; + } + + public static int characterCount(String text) { + final Matcher matcher = GRAPHEME.matcher(text); + int count = 0; + while (matcher.find()) count++; + return count; + } + + /** The chunk of a line at the position, put into the encapsulation template. */ + public static String current(List chunks, long positionMs, @Nullable String wrapper, int repeat) { + if (chunks.isEmpty()) return ""; + Chunk selected = chunks.get(0); + for (int i = 1; i < chunks.size(); i++) { + Chunk candidate = chunks.get(i); + if (candidate.startMs() < 0 || candidate.startMs() > positionMs) break; + selected = candidate; + } + return selected.text().isEmpty() ? "" : encapsulate(wrapper, selected.text(), repeat); + } + + /** + * Splits a line longer than the limit into two halves of about equal length, by words or by + * characters if the line has no spaces. A half starts at the time its first word is sung, or + * at a time in proportion to its position in the line if the words have no times. + */ + public static List chunks(LyricsLine line, long intervalEndMs, int limit, + boolean capitalize, boolean strip) { + return chunks(line, intervalEndMs, limit, capitalize, strip, 1); + } + + /** + * Splits by the word times when the provider has them, otherwise by ICU word boundaries. A line + * without spaces that is split gets an ellipsis at the split. + */ + public static List chunks(LyricsLine line, long intervalEndMs, int limit, + boolean capitalize, boolean strip, double fullWidthMultiplier) { + final LyricsTextFormatter.Result cleaned = LyricsTextFormatter.format(line.text(), false, strip); + final String text = cleaned.text(); + if (limit <= 0 || width(text, fullWidthMultiplier) <= limit) { + return List.of(new Chunk(LyricsTextFormatter.format(text, capitalize, false).text(), + line.startTimeMs())); + } + final List timed = timedUnits(line, cleaned); + final List units = timed.isEmpty() ? untimedUnits(text) : timed; + final List groups = new ArrayList<>(2); + final int middle = (units.size() + 1) / 2; + if (middle > 0) { + groups.add(new Unit(units.get(0).start(), units.get(middle - 1).end(), units.get(0).timeMs())); + } + if (middle < units.size()) { + groups.add(new Unit(units.get(middle).start(), units.get(units.size() - 1).end(), + units.get(middle).timeMs())); + } + final List result = new ArrayList<>(groups.size()); + final int total = Math.max(1, characterCount(text)); + long previousTime = line.startTimeMs(); + for (int i = 0; i < groups.size(); i++) { + final Unit group = groups.get(i); + long start = group.timeMs(); + if (i == 0) start = line.startTimeMs(); + else if (start < line.startTimeMs() || start < previousTime) { + start = intervalEndMs > line.startTimeMs() && line.startTimeMs() >= 0 + ? line.startTimeMs() + (intervalEndMs - line.startTimeMs()) + * characterCount(text.substring(0, group.start())) / total + : -1; + } + if (start >= 0) previousTime = start; + String formatted = LyricsTextFormatter.format( + text.substring(group.start(), group.end()), capitalize, strip).text(); + // The ellipsis marks the split itself, so it goes inside any quotes closed around the part. + if (i + 1 < groups.size() && !hasWhitespace(text) && !formatted.endsWith(SPLIT_ELLIPSIS)) { + formatted += SPLIT_ELLIPSIS; + } + result.add(new Chunk(PairedPunctuation.restore(text, formatted, group.start(), group.end()), start)); + } + return List.copyOf(result); + } + + /** A negative offset looks ahead, so -600 shows the next line 600 ms early. */ + public static long selectionPosition(long playbackMs, int offsetMs) { + return Math.max(0, playbackMs - (long) offsetMs); + } + + /** + * The frame of the instrumental template at the position. Each line of the template is one + * frame, and the frames share the gap duration equally. + */ + public static String instrumental(@Nullable String template, long startMs, long endMs, long positionMs, + @Nullable String title, @Nullable String artist, @Nullable String lastLyric) { + final String[] frames = LINE_BREAK.split(template == null ? DEFAULT_INSTRUMENTAL : template, -1); + final long duration = endMs - startMs; + final int index = endMs == Long.MAX_VALUE || duration <= 0 ? 0 + : (int) Math.min(frames.length - 1, + Math.max(0, positionMs - startMs) / (double) duration * frames.length); + return replacePlaceholders(frames[index], title, artist, lastLyric == null ? "" : lastLyric); + } + + private static List timedUnits(LyricsLine line, LyricsTextFormatter.Result cleaned) { + if (!line.hasWordTimings()) return List.of(); + final List units = new ArrayList<>(); + int cursor = 0; + final boolean spaced = hasWhitespace(line.text()); + for (Word word : line.words()) { + final String own = word.text().trim(); + if (own.isEmpty()) continue; + final int start = line.text().indexOf(own, cursor); + if (start < 0 || word.startMs() < 0) return List.of(); + // Unrepresented punctuation belongs to the previous unit; missing words require fallback. + if (hasLetterOrDigit(line.text().substring(cursor, start))) { + return List.of(); + } + final int end = start + own.length(); + final Unit next = new Unit(cleaned.offset(start), cleaned.offset(end), word.startMs()); + if (!units.isEmpty() && spaced && !hasWhitespace(line.text().substring(cursor, start))) { + Unit previous = units.remove(units.size() - 1); + units.add(new Unit(previous.start(), next.end(), previous.timeMs())); + } else if (next.end() > next.start()) { + units.add(next); + } + cursor = end; + } + // A partially aligned word list must not silently discard any lyric content. + if (units.isEmpty()) return List.of(); + if (hasLetterOrDigit(cleaned.text().substring(0, units.get(0).start())) + || hasLetterOrDigit(cleaned.text().substring(units.get(units.size() - 1).end()))) { + return List.of(); + } + final List covering = new ArrayList<>(units.size()); + for (int i = 0; i < units.size(); i++) { + final Unit unit = units.get(i); + int end = i + 1 < units.size() ? units.get(i + 1).start() : cleaned.text().length(); + while (end > unit.end()) { + final int cp = cleaned.text().codePointBefore(end); + if (!isWhitespace(cp)) break; + end -= Character.charCount(cp); + } + covering.add(new Unit(i == 0 ? 0 : unit.start(), end, unit.timeMs())); + } + return covering; + } + + private static boolean hasLetterOrDigit(String text) { + return text.codePoints().anyMatch(Character::isLetterOrDigit); + } + + private static boolean hasWhitespace(String text) { + return text.codePoints().anyMatch(MiniLyricsFormatter::isWhitespace); + } + + /** + * Word units from the ICU word boundaries. In text with spaces a unit runs from space to space, + * so punctuation and hyphenated words stay whole; in text without spaces, such as Japanese, the + * dictionary words are the units. Punctuation joins the word before it. + */ + private static List untimedUnits(String text) { + final List units = new ArrayList<>(); + final boolean spaced = hasWhitespace(text); + final BreakIterator words = BreakIterator.getWordInstance(Locale.ROOT); + words.setText(text); + int start = words.first(); + // Opening punctuation is held back and joins the word after it. + int opening = -1; + int worded = 0; + for (int end = words.next(); end != BreakIterator.DONE; start = end, end = words.next()) { + final String piece = text.substring(start, end); + if (!hasNonWhitespace(piece)) continue; + final boolean letters = hasLetterOrDigit(piece); + final boolean joined = start > 0 && !isWhitespace(text.codePointBefore(start)); + if (!letters && isOpening(piece) && !(spaced && joined && !units.isEmpty())) { + if (opening < 0) opening = start; + continue; + } + final int from = opening >= 0 ? opening : start; + opening = -1; + if (!units.isEmpty() && joined && from == start && (spaced || !letters)) { + final Unit previous = units.remove(units.size() - 1); + units.add(new Unit(previous.start(), end, -1)); + } else { + units.add(new Unit(from, end, -1)); + if (letters) worded++; + } + } + if (opening >= 0) { + if (units.isEmpty()) units.add(new Unit(opening, text.length(), -1)); + else { + final Unit previous = units.remove(units.size() - 1); + units.add(new Unit(previous.start(), text.length(), -1)); + } + } + if (worded < 2 && !spaced) { + // One dictionary word longer than the limit: fall back to characters. + units.clear(); + final Matcher matcher = GRAPHEME.matcher(text); + while (matcher.find()) units.add(new Unit(matcher.start(), matcher.end(), -1)); + } + return units; + } + + private static boolean isOpening(String piece) { + return piece.codePoints().allMatch(cp -> { + final int type = Character.getType(cp); + return type == Character.START_PUNCTUATION || type == Character.INITIAL_QUOTE_PUNCTUATION + || cp == '"' || cp == '\'' || cp == '„' || cp == '‚' || cp == '¿' || cp == '¡'; + }); + } + + private static boolean isWhitespace(int cp) { + return Character.isWhitespace(cp) || Character.isSpaceChar(cp); + } + + private static boolean hasNonWhitespace(String text) { + return text.codePoints().anyMatch(cp -> !isWhitespace(cp)); + } +} diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/MiniLyricsTimeline.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/MiniLyricsTimeline.java new file mode 100644 index 0000000000..ff9166683e --- /dev/null +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/MiniLyricsTimeline.java @@ -0,0 +1,190 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/4002 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.extension.music.patches.lyrics.model; + +import androidx.annotation.Nullable; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +import app.morphe.extension.music.patches.lyrics.ui.SectionDotsMotion; + +/** + * Which line the miniplayer and the media session show at a playback position. + * Instrumental gaps are the same ones the lyrics panel shows its section dots for. + */ +public final class MiniLyricsTimeline { + + public record Vocal(LyricsLine line, int index, long startMs, long endMs) { + } + + /** + * @param previous The line sung before the gap, or null if the gap is before the first line. + */ + public record Gap(long startMs, long endMs, @Nullable Vocal previous) { + } + + /** + * At most one of vocal and gap is set. + * + * @param previous If neither is set, the line that started last, or null if none did yet. + */ + public record Selection(@Nullable Vocal vocal, @Nullable Gap gap, @Nullable Vocal previous) { + } + + private static final int MUSICAL_NOTE = "🎵".codePointAt(0); + private static final int MULTIPLE_MUSICAL_NOTES = "🎶".codePointAt(0); + + private final List vocals = new ArrayList<>(); + private final List gaps = new ArrayList<>(); + /** The latest end time of the vocals up to and including each index. */ + private final long[] prefixEnds; + private final long durationMs; + + public MiniLyricsTimeline(List lines, long durationMs) { + this.durationMs = durationMs; + + for (int i = 0; i < lines.size(); i++) { + LyricsLine line = lines.get(i); + if (line.startTimeMs() < 0 || isInstrumentalMarker(line.text())) continue; + + long end = line.endTimeMs(); + if (line.hasWordTimings()) { + long wordEnd = -1; + for (Word word : line.words()) { + if (word.endMs() > word.startMs()) wordEnd = Math.max(wordEnd, word.endMs()); + } + if (wordEnd > line.startTimeMs()) end = wordEnd; + } + if (end <= line.startTimeMs()) { + // No end time, so the line lasts until the next one starts. + end = durationMs > line.startTimeMs() ? durationMs : Long.MAX_VALUE; + for (int j = i + 1; j < lines.size(); j++) { + if (!line.isBG() && lines.get(j).isBG()) continue; + long next = lines.get(j).startTimeMs(); + if (next > line.startTimeMs()) { + end = Math.min(end, next); + break; + } + } + } + if (durationMs > 0) end = Math.min(end, durationMs); + if (end > line.startTimeMs()) vocals.add(new Vocal(line, i, line.startTimeMs(), end)); + } + vocals.sort(Comparator.comparingLong(Vocal::startMs).thenComparingInt(Vocal::index)); + + // A provider line of only music notes marks an instrumental gap as well. + List classified = new ArrayList<>(SectionDotsMotion.computeGaps(lines)); + for (int i = 0, size = lines.size(); i < size; i++) { + LyricsLine line = lines.get(i); + if (line.startTimeMs() < 0 || !isInstrumentalMarker(line.text())) continue; + + long end = i + 1 < lines.size() ? lines.get(i + 1).startTimeMs() : durationMs; + if (end <= line.startTimeMs()) end = line.endTimeMs(); + if (end > line.startTimeMs()) { + classified.add(new SectionDotsMotion.Gap(i, line.startTimeMs(), end)); + } + } + + // Gaps only cover time no line is sung in. + prefixEnds = new long[vocals.size()]; + long cursor = 0; + Vocal previous = null; + for (int i = 0; i < vocals.size(); i++) { + Vocal vocal = vocals.get(i); + addGaps(classified, cursor, vocal.startMs(), previous); + if (vocal.endMs() >= cursor) { + cursor = vocal.endMs(); + previous = vocal; + } + prefixEnds[i] = cursor; + } + addGaps(classified, cursor, durationMs > 0 ? durationMs : Long.MAX_VALUE, previous); + + // A note line can overlap a gap of the panel. Merge them so only one animation plays. + gaps.sort(Comparator.comparingLong(Gap::startMs)); + for (int i = 1; i < gaps.size(); ) { + Gap before = gaps.get(i - 1); + Gap next = gaps.get(i); + if (next.startMs() <= before.endMs()) { + gaps.set(i - 1, new Gap(before.startMs(), Math.max(before.endMs(), next.endMs()), + before.previous())); + gaps.remove(i); + } else { + i++; + } + } + } + + private void addGaps(List classified, long start, long end, + @Nullable Vocal previous) { + for (SectionDotsMotion.Gap gap : classified) { + long from = Math.max(start, gap.gapStartMs()); + long to = Math.min(end, gap.gapEndMs()); + if (from < to) gaps.add(new Gap(from, to, previous)); + } + } + + /** + * If a line is empty or only has music notes, such as the ♪♪♪ some providers use. + */ + public static boolean isInstrumentalMarker(String text) { + if (text.trim().isEmpty()) return true; + + boolean note = false; + for (int at = 0; at < text.length(); ) { + final int cp = text.codePointAt(at); + at += Character.charCount(cp); + // 0xFE0F is the emoji presentation selector. + if (Character.isWhitespace(cp) || Character.isSpaceChar(cp) || cp == 0xFE0F) continue; + if ((cp >= '♩' && cp <= '♬') || cp == MUSICAL_NOTE || cp == MULTIPLE_MUSICAL_NOTES) { + note = true; + } else { + return false; + } + } + return note; + } + + public Selection at(long positionMs) { + long position = Math.max(0, positionMs); + if (durationMs > 0) position = Math.min(position, durationMs - 1); + + int low = 0; + int high = gaps.size() - 1; + while (low <= high) { + final int mid = (low + high) >>> 1; + Gap gap = gaps.get(mid); + if (position < gap.startMs()) high = mid - 1; + else if (position >= gap.endMs()) low = mid + 1; + else return new Selection(null, gap, null); + } + + // The last vocal that started at or before the position. + low = 0; + high = vocals.size() - 1; + while (low <= high) { + final int mid = (low + high) >>> 1; + if (vocals.get(mid).startMs() <= position) low = mid + 1; + else high = mid - 1; + } + + // Prefer a lead vocal over a background vocal sung at the same time. + Vocal background = null; + for (int i = high; i >= 0 && prefixEnds[i] > position; i--) { + Vocal vocal = vocals.get(i); + if (vocal.endMs() <= position) continue; + if (!vocal.line().isBG()) return new Selection(vocal, null, null); + if (background == null) background = vocal; + } + if (background != null) return new Selection(background, null, null); + + return new Selection(null, null, high >= 0 ? vocals.get(high) : null); + } +} diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/PairedPunctuation.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/PairedPunctuation.java new file mode 100644 index 0000000000..e24c1a37a7 --- /dev/null +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/PairedPunctuation.java @@ -0,0 +1,222 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/4002 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + * + * Pair data: Unicode 17.0 BidiBrackets and CLDR 48 quotation delimiters. + * https://www.unicode.org/Public/17.0.0/ucd/BidiBrackets.txt + * https://unicode.org/reports/tr35/tr35-general.html#Delimiters + * + * The pair data is distributed under the following license: + * + * UNICODE LICENSE V3 + * + * COPYRIGHT AND PERMISSION NOTICE + * + * Copyright (C) 1991-2026 Unicode, Inc. + * + * NOTICE TO USER: Carefully read the following legal agreement. BY + * DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR + * SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE + * TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT + * DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE. + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of data files and any associated documentation (the "Data Files") or + * software and any associated documentation (the "Software") to deal in the + * Data Files or Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, and/or sell + * copies of the Data Files or Software, and to permit persons to whom the + * Data Files or Software are furnished to do so, provided that either (a) + * this copyright and permission notice appear with all copies of the Data + * Files or Software, or (b) this copyright and permission notice appear in + * associated Documentation. + * + * THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY + * KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF + * THIRD PARTY RIGHTS. + * + * IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE + * BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, + * OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, + * WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, + * ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA + * FILES OR SOFTWARE. + * + * Except as contained in this notice, the name of a copyright holder shall + * not be used in advertising or otherwise to promote the sale, use or other + * dealings in these Data Files or Software without prior written + * authorization of the copyright holder. + */ +package app.morphe.extension.music.patches.lyrics.model; + +import java.util.ArrayList; +import java.util.List; + +/** + * Restores only complete enclosing punctuation shells that a split crosses. + */ +public final class PairedPunctuation { + public record Pair(String opening, String closing) { + } + + public static final List PAIRS = List.of( + new Pair("\"", "\""), + new Pair("'", "'"), + new Pair("(", ")"), + new Pair("[", "]"), + new Pair("{", "}"), + new Pair("«", "»"), + new Pair("»", "«"), + new Pair("༺", "༻"), + new Pair("༼", "༽"), + new Pair("᚛", "᚜"), + new Pair("‘", "’"), + new Pair("’", "‘"), + new Pair("’", "’"), + new Pair("‚", "‘"), + new Pair("‚", "’"), + new Pair("“", "’"), + new Pair("“", "”"), + new Pair("”", "“"), + new Pair("”", "”"), + new Pair("„", "“"), + new Pair("„", "”"), + new Pair("‹", "›"), + new Pair("›", "‹"), + new Pair("⁅", "⁆"), + new Pair("⁽", "⁾"), + new Pair("₍", "₎"), + new Pair("⌈", "⌉"), + new Pair("⌊", "⌋"), + new Pair("〈", "〉"), + new Pair("❨", "❩"), + new Pair("❪", "❫"), + new Pair("❬", "❭"), + new Pair("❮", "❯"), + new Pair("❰", "❱"), + new Pair("❲", "❳"), + new Pair("❴", "❵"), + new Pair("⟅", "⟆"), + new Pair("⟦", "⟧"), + new Pair("⟨", "⟩"), + new Pair("⟪", "⟫"), + new Pair("⟬", "⟭"), + new Pair("⟮", "⟯"), + new Pair("⦃", "⦄"), + new Pair("⦅", "⦆"), + new Pair("⦇", "⦈"), + new Pair("⦉", "⦊"), + new Pair("⦋", "⦌"), + new Pair("⦍", "⦐"), + new Pair("⦏", "⦎"), + new Pair("⦑", "⦒"), + new Pair("⦓", "⦔"), + new Pair("⦕", "⦖"), + new Pair("⦗", "⦘"), + new Pair("⧘", "⧙"), + new Pair("⧚", "⧛"), + new Pair("⧼", "⧽"), + new Pair("⸢", "⸣"), + new Pair("⸤", "⸥"), + new Pair("⸦", "⸧"), + new Pair("⸨", "⸩"), + new Pair("⹕", "⹖"), + new Pair("⹗", "⹘"), + new Pair("⹙", "⹚"), + new Pair("⹛", "⹜"), + new Pair("〈", "〉"), + new Pair("《", "》"), + new Pair("「", "」"), + new Pair("『", "』"), + new Pair("【", "】"), + new Pair("〔", "〕"), + new Pair("〖", "〗"), + new Pair("〘", "〙"), + new Pair("〚", "〛"), + new Pair("﹙", "﹚"), + new Pair("﹛", "﹜"), + new Pair("﹝", "﹞"), + new Pair(""", """), + new Pair("'", "'"), + new Pair("(", ")"), + new Pair("[", "]"), + new Pair("{", "}"), + new Pair("⦅", "⦆"), + new Pair("「", "」") + ); + + private record Shell(Pair pair, int openEnd, int closeStart) { + } + + private PairedPunctuation() { + } + + public static String restore(String whole, String chunk, int start, int end) { + List shells = new ArrayList<>(); + int left = 0, right = whole.length(); + while (left < right) { + while (left < right && space(whole.codePointAt(left))) + left += Character.charCount(whole.codePointAt(left)); + while (right > left && space(whole.codePointBefore(right))) + right -= Character.charCount(whole.codePointBefore(right)); + Shell found = null; + for (Pair pair : PAIRS) { + int openEnd = left + pair.opening().length(), closeStart = right - pair.closing().length(); + if (openEnd > closeStart || !whole.startsWith(pair.opening(), left) + || !whole.startsWith(pair.closing(), closeStart) + || !closesAtEnd(whole, pair, openEnd, closeStart)) continue; + found = new Shell(pair, openEnd, closeStart); + break; + } + if (found == null) break; + shells.add(found); + left = found.openEnd(); + right = found.closeStart(); + } + StringBuilder out = new StringBuilder(); + for (Shell shell : shells) if (start >= shell.openEnd()) out.append(shell.pair().opening()); + out.append(chunk); + for (int i = shells.size() - 1; i >= 0; i--) { + Shell shell = shells.get(i); + if (end <= shell.closeStart()) out.append(shell.pair().closing()); + } + return out.toString(); + } + + private static boolean space(int cp) { + return Character.isWhitespace(cp) || Character.isSpaceChar(cp); + } + + private static boolean escaped(String text, int at) { + int count = 0; + while (at > 0 && text.charAt(--at) == '\\') count++; + return (count & 1) != 0; + } + + private static boolean closesAtEnd(String text, Pair pair, int at, int closing) { + int depth = 1; + while (at <= closing) { + if (escaped(text, at)) { + at += Character.charCount(text.codePointAt(at)); + continue; + } + // Apostrophes inside a word are not quotation boundaries. + final boolean equals = pair.opening().equals(pair.closing()); + final boolean inWord = equals && at > 0 + && at + pair.closing().length() < text.length() + && Character.isLetterOrDigit(text.codePointBefore(at)) + && Character.isLetterOrDigit(text.codePointAt(at + pair.closing().length())); + if (!inWord && text.startsWith(pair.closing(), at)) { + if (--depth == 0) return at == closing; + at += pair.closing().length(); + } else if (!equals && text.startsWith(pair.opening(), at)) { + depth++; + at += pair.opening().length(); + } else at += Character.charCount(text.codePointAt(at)); + } + return false; + } +} diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/TrackInfo.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/TrackInfo.java index bb2e4a0116..f3b510f739 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/TrackInfo.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/TrackInfo.java @@ -25,10 +25,6 @@ public String cacheKey() { return artist.toLowerCase(Locale.ROOT) + " " + title.toLowerCase(Locale.ROOT); } - public String displayWith(boolean artistFirst) { - return artistFirst ? artist + " - " + title : title + " - " + artist; - } - /** * Identity is title and artist only: the app reports the other two late, so filling * them in must not read as a track change. diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/TrackVersion.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/TrackVersion.java new file mode 100644 index 0000000000..47f570c2be --- /dev/null +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/TrackVersion.java @@ -0,0 +1,109 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/4002 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.extension.music.patches.lyrics.model; + +import androidx.annotation.Nullable; + +import java.util.Locale; + +/** + * The title of a track without its version, and the version itself, such as "Acoustic". + * Both come from separate backend fields. The version is never parsed out of a title: + * "Rockstar (Olympia)" is a title, not a version. + * + * @param variant The version, or an empty string if the track has none. + */ +public record TrackVersion(String title, String variant) { + + /** Version matching needs untouched metadata, rather than the cleaned lyrics query. */ + public static TrackInfo query(TrackInfo track, @Nullable String rawTitle, @Nullable String rawArtist) { + return new TrackInfo(rawTitle == null || rawTitle.isBlank() ? track.title() : rawTitle, + rawArtist == null || rawArtist.isBlank() ? track.artist() : rawArtist, + track.album(), track.durationSeconds()); + } + + /** + * Strips punctuation from both ends of the version, but only punctuation with no partner left + * in the rest of the version. "(Acoustic)" becomes "Acoustic", "Acoustic (Deluxe)" keeps its + * brackets, and "Acoustic (Deluxe)," loses only the comma. + */ + public static String clean(String version) { + String s = version.strip(); + while (!s.isEmpty()) { + final String core = core(s); + final int first = s.codePointAt(0); + if (isEdgePunctuation(first) && !hasPartner(first, core)) { + s = s.substring(Character.charCount(first)).strip(); + continue; + } + final int last = s.codePointBefore(s.length()); + if (isEdgePunctuation(last) && !hasPartner(last, core)) { + s = s.substring(0, s.length() - Character.charCount(last)).strip(); + continue; + } + break; + } + return s; + } + + /** The version without the punctuation and whitespace at either end. */ + private static String core(String s) { + int start = 0; + int end = s.length(); + while (start < end && isEdge(s.codePointAt(start))) { + start += Character.charCount(s.codePointAt(start)); + } + while (end > start && isEdge(s.codePointBefore(end))) { + end -= Character.charCount(s.codePointBefore(end)); + } + return s.substring(start, end); + } + + private static boolean isEdge(int cp) { + return isEdgePunctuation(cp) || Character.isWhitespace(cp); + } + + private static boolean isEdgePunctuation(int cp) { + return switch (Character.getType(cp)) { + case Character.CONNECTOR_PUNCTUATION, Character.DASH_PUNCTUATION, + Character.START_PUNCTUATION, Character.END_PUNCTUATION, + Character.INITIAL_QUOTE_PUNCTUATION, Character.FINAL_QUOTE_PUNCTUATION, + Character.OTHER_PUNCTUATION -> true; + default -> false; + }; + } + + private static boolean hasPartner(int cp, String core) { + final String symbol = new String(Character.toChars(cp)); + for (PairedPunctuation.Pair pair : PairedPunctuation.PAIRS) { + if (pair.opening().equals(symbol) && core.contains(pair.closing()) + || pair.closing().equals(symbol) && core.contains(pair.opening())) { + return true; + } + } + return false; + } + + /** + * If the displayed title is the backend's short title and version put together. The + * displayed title itself is never parsed. + */ + public static boolean matchesStructuredTitle(String displayed, String full, String shortTitle, String version) { + if (shortTitle.isBlank()) return false; + final String wanted = normalize(displayed); + final String variant = clean(version); + if (variant.isEmpty()) { + return wanted.equals(normalize(shortTitle)) && normalize(full).equals(normalize(shortTitle)); + } + return wanted.equals(normalize(shortTitle + " (" + variant + ")")); + } + + public static String normalize(String text) { + return text.trim().toLowerCase(Locale.ROOT); + } +} diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/DeezerProvider.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/DeezerProvider.java index 80ce60293e..bfa5114954 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/DeezerProvider.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/DeezerProvider.java @@ -24,9 +24,11 @@ import app.morphe.extension.music.patches.lyrics.model.Lyrics; import app.morphe.extension.music.patches.lyrics.model.LyricsLine; import app.morphe.extension.music.patches.lyrics.model.TrackInfo; +import app.morphe.extension.music.patches.lyrics.model.TrackVersion; import app.morphe.extension.music.patches.lyrics.parsers.LRCParser; import app.morphe.extension.music.settings.Settings; import app.morphe.extension.shared.Logger; +import app.morphe.extension.shared.Utils; import app.morphe.extension.shared.requests.Requester; public final class DeezerProvider extends ScoredSongProvider { @@ -190,6 +192,77 @@ private static synchronized void dropJwt() { jwtExpiryMs = 0; } + /** Lookups that succeeded, including tracks without a version. Failed requests are retried. */ + private static final Map versions = Utils.createSizeRestrictedMap(64, true); + + /** + * The version of the track, from the title_short and title_version fields of Deezer's own + * search results. The variant is empty when Deezer has no exact match or the track has no + * version. + * + * @param track Untouched metadata, see {@link TrackVersion#query}. + * @throws IOException If a request fails. + */ + public static TrackVersion fetchVersion(TrackInfo track) throws Exception { + synchronized (versions) { + TrackVersion cached = versions.get(track); + if (cached != null) return cached; + } + TrackVersion result = lookupVersion(track); + synchronized (versions) { + versions.put(track, result); + } + return result; + } + + private static TrackVersion lookupVersion(TrackInfo track) throws Exception { + LyricsRequests.throttle(lastRequestTime, REQUEST_THROTTLE_MS); + String url = SEARCH_URL + + "?q=" + LyricsRequests.encode(track.artist() + " - " + track.title()) + + "&limit=10&output=json"; + JSONArray data = getJson(url).optJSONArray("data"); + if (data == null) throw new IOException("Deezer search has no results array"); + + final String wantedTitle = TrackVersion.normalize(track.title()); + final String wantedArtist = TrackVersion.normalize(track.artist()); + // The full title is matched first. Short title equality alone would confuse the + // original recording with a remix or acoustic version of that same song. + for (int pass = 0; pass < 2; pass++) { + for (int i = 0, length = data.length(); i < length; i++) { + JSONObject item = data.optJSONObject(i); + if (item == null) continue; + JSONObject artist = item.optJSONObject("artist"); + if (artist == null || !wantedArtist.equals(TrackVersion.normalize(artist.optString("name", "")))) { + continue; + } + final String full = item.optString("title", ""); + final String shortTitle = item.optString("title_short", ""); + final String version = item.optString("title_version", ""); + final boolean matches = pass == 0 + ? wantedTitle.equals(TrackVersion.normalize(full)) + : TrackVersion.matchesStructuredTitle(track.title(), full, shortTitle, version); + if (matches && !shortTitle.isBlank()) { + return new TrackVersion(shortTitle, TrackVersion.clean(version)); + } + } + } + return new TrackVersion(track.title(), ""); + } + + /** A Deezer error comes back as a 200 with an error object, so that counts as a failure too. */ + private static JSONObject getJson(String url) throws Exception { + HttpURLConnection connection = LyricsRequests.openConnection(url, Map.of("Accept", "application/json")); + try { + final int httpCode = connection.getResponseCode(); + if (httpCode != Requester.HTTP_STATUS_CODE_SUCCESS) throw new IOException("Deezer HTTP " + httpCode); + JSONObject json = Requester.parseJSONObject(connection); + if (json.has("error")) throw new IOException("Deezer error: " + json.optString("error")); + return json; + } finally { + connection.disconnect(); + } + } + @Nullable private JSONArray searchTracks(TrackInfo track) throws Exception { LyricsRequests.throttle(lastRequestTime, REQUEST_THROTTLE_MS); diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/LRCLIBProvider.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/LRCLIBProvider.java index 47ca769afd..bc538b3ff0 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/LRCLIBProvider.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/LRCLIBProvider.java @@ -18,6 +18,7 @@ import java.net.HttpURLConnection; import java.util.ArrayList; import java.util.List; +import java.util.Map; import app.morphe.extension.music.patches.lyrics.model.Lyrics; import app.morphe.extension.music.patches.lyrics.model.LyricsLine; @@ -36,6 +37,16 @@ public final class LRCLIBProvider implements LyricsProvider { private static final String BASE_URL = "https://lrclib.net/api/"; + /** + * Public ID of the registered client application, which gets higher rate limits. + * It is not a secret. + * + * @see API authorization + */ + private static final String APP_ID = "HLyf7QYkIJfl4wcObeD-caHjBZPnGhv0Vuvx2UiIFKk"; + + private static final Map HEADERS = Map.of("Lrclib-App-Id", APP_ID); + @Override public String name() { return "LRCLIB"; @@ -51,11 +62,22 @@ public boolean hasCandidates() { public FetchResult fetch(TrackInfo track) throws Exception { // The exact endpoint matches on duration as well, which gives the best timings, // but it fails for any track whose duration differs from the database entry. + // Synced always wins over plain, so a plain exact match only stands when no search + // result has timings either. FetchResult exact = fetchExact(track); - if (exact != null && exact.lyrics() != Lyrics.NOT_FOUND) { + final boolean exactFound = exact != null && exact.lyrics() != Lyrics.NOT_FOUND; + if (exactFound && LyricsRequests.syncRank(exact.lyrics()) > 0) { return exact; } - return fetchSearch(track); + FetchResult searched = fetchSearch(track); + if (!exactFound) { + return searched; + } + if (searched != null && searched.lyrics() != Lyrics.NOT_FOUND + && LyricsRequests.syncRank(searched.lyrics()) > LyricsRequests.syncRank(exact.lyrics())) { + return searched; + } + return exact; } @Override @@ -71,7 +93,7 @@ public List fetchCandidates(TrackInfo track) throws Excepti String url = BASE_URL + "search?track_name=" + LyricsRequests.encode(track.title()) + "&artist_name=" + LyricsRequests.encode(track.artist()); - HttpURLConnection connection = LyricsRequests.openConnection(url); + HttpURLConnection connection = LyricsRequests.openConnection(url, HEADERS); if (connection.getResponseCode() != Requester.HTTP_STATUS_CODE_SUCCESS) { return Lyrics.sortScoredByScore(scored); } @@ -114,9 +136,6 @@ public List fetchCandidates(TrackInfo track) throws Excepti } for (JSONObject candidate : gated) { - if (scored.size() >= LyricsRequests.MAX_CANDIDATES) { - break; - } Lyrics lyrics = toLyrics(candidate); if (lyrics != null && !lyrics.isEmpty()) { int score = LyricsRequests.scoreLyricsCandidate( @@ -128,7 +147,21 @@ public List fetchCandidates(TrackInfo track) throws Excepti } } - return Lyrics.sortScoredByScore(scored); + List ordered = syncedFirst(scored); + return ordered.size() > LyricsRequests.MAX_CANDIDATES + ? new ArrayList<>(ordered.subList(0, LyricsRequests.MAX_CANDIDATES)) + : ordered; + } + + /** + * Orders candidates so every synced one comes before every plain one, by score within + * each group. Word timings rank above line timings. + */ + static List syncedFirst(List scored) { + List ordered = new ArrayList<>(Lyrics.sortScoredByScore(scored)); + // List.sort is stable, so the score order survives within each timing rank. + ordered.sort((a, b) -> LyricsRequests.syncRank(b.lyrics()) - LyricsRequests.syncRank(a.lyrics())); + return ordered; } private static int scoreCandidate(JSONObject item, TrackInfo track) { @@ -169,7 +202,7 @@ private FetchResult fetchSearch(TrackInfo track) throws Exception { String url = BASE_URL + "search?track_name=" + LyricsRequests.encode(track.title()) + "&artist_name=" + LyricsRequests.encode(track.artist()); - HttpURLConnection connection = LyricsRequests.openConnection(url); + HttpURLConnection connection = LyricsRequests.openConnection(url, HEADERS); if (connection.getResponseCode() != Requester.HTTP_STATUS_CODE_SUCCESS) { LyricsRequests.logFailure(name(), connection); return null; @@ -182,6 +215,7 @@ private FetchResult fetchSearch(TrackInfo track) throws Exception { } FetchResult best = null; + int bestRank = -1; int bestCombined = -1; for (int i = 0; i < resultsLength; i++) { JSONObject candidate = results.optJSONObject(i); @@ -196,8 +230,13 @@ private FetchResult fetchSearch(TrackInfo track) throws Exception { candidate.optString("artistName", ""), candidate.optInt("duration", 0), candidateLyrics, track); - int trackScore = combined - LyricsRequests.syncRank(candidateLyrics); - if (trackScore >= LyricsRequests.SOFT_MIN && combined > bestCombined) { + final int rank = LyricsRequests.syncRank(candidateLyrics); + int trackScore = combined - rank; + // Any synced match beats any plain one; the score only breaks ties + // between results with the same kind of timings. + if (trackScore >= LyricsRequests.SOFT_MIN + && (rank > bestRank || (rank == bestRank && combined > bestCombined))) { + bestRank = rank; bestCombined = combined; best = FetchResult.of(candidateLyrics, candidate.optString("trackName", ""), @@ -256,7 +295,7 @@ private Lyrics toLyrics(JSONObject response) { @Nullable private JSONObject getJsonObject(String url) throws IOException, JSONException { - HttpURLConnection connection = LyricsRequests.openConnection(url); + HttpURLConnection connection = LyricsRequests.openConnection(url, HEADERS); final int responseCode = connection.getResponseCode(); if (responseCode == 404) { connection.disconnect(); diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/session/LockScreenLyrics.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/session/LockScreenLyrics.java index 61e4136701..65c7be86f3 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/session/LockScreenLyrics.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/session/LockScreenLyrics.java @@ -18,6 +18,7 @@ import java.util.Objects; import app.morphe.extension.music.patches.lyrics.LyricsManager; +import app.morphe.extension.music.patches.lyrics.LyricsPresentation; import app.morphe.extension.music.patches.lyrics.model.MetadataCleaner; import app.morphe.extension.music.patches.lyrics.model.TrackInfo; import app.morphe.extension.music.settings.Settings; @@ -63,9 +64,11 @@ public final class LockScreenLyrics { @Nullable private static String cachedCleanedArtist; - /** Title pushed on the last tick, to avoid redundant {@code setMetadata} calls. */ + /** Title and artist pushed on the last tick, to avoid redundant {@code setMetadata} calls. */ @Nullable private static volatile String lastPushedTitle; + @Nullable + private static String lastPushedArtist; /** Set when the app pushes fresh metadata, so the next tick pushes it again. */ private static volatile boolean needsRepush; @@ -94,6 +97,12 @@ public final class LockScreenLyrics { private LockScreenLyrics() { } + /** Pushes the current line again, as a lyrics setting changed how it is shown. */ + public static void onSettingsChanged() { + needsRepush = true; + tick(); + } + /** * Observed at the app's {@code MediaSession.setMetadata} call site. Captures the session * and original metadata and (re)starts the ticker when the feature is enabled. @@ -110,6 +119,9 @@ public static void onMediaSessionSetMetadata(MediaSession session, MediaMetadata String rt = original.getString(MediaMetadata.METADATA_KEY_TITLE); String ra = original.getString(MediaMetadata.METADATA_KEY_ARTIST); + if (!Objects.equals(rt, realTitle) || !Objects.equals(ra, realArtist)) { + LyricsPresentation.resetTrackCycle(); + } realTitle = rt; realArtist = ra; @@ -191,19 +203,23 @@ private static void push() { } String newTitle = getCurrentLine(); - if (!needsRepush && newTitle.equals(lastPushedTitle)) { + String artist = getTrackLine(); + if (!needsRepush && newTitle.equals(lastPushedTitle) && artist.equals(lastPushedArtist)) { ticker.schedule(); return; } - MediaMetadata metadata = buildMetadata(newTitle); + MediaMetadata metadata = buildMetadata(newTitle, artist); if (metadata == null) { ticker.schedule(); return; } session.setMetadata(metadata); + LyricNotificationRefresh.refresh(session, newTitle, + metadata.getString(MediaMetadata.METADATA_KEY_DISPLAY_SUBTITLE)); lastPushedTitle = newTitle; + lastPushedArtist = artist; needsRepush = false; nudgePlaybackState(session); @@ -251,6 +267,11 @@ private static void restoreIfPushed() { MediaMetadata original = originalMetadata; if (session != null && original != null) { session.setMetadata(original); + String title = original.getString(MediaMetadata.METADATA_KEY_DISPLAY_TITLE); + String text = original.getString(MediaMetadata.METADATA_KEY_DISPLAY_SUBTITLE); + LyricNotificationRefresh.refresh(session, + title != null ? title : original.getString(MediaMetadata.METADATA_KEY_TITLE), + text != null ? text : original.getString(MediaMetadata.METADATA_KEY_ARTIST)); nudgePlaybackState(session); } lastPushedTitle = null; @@ -276,33 +297,32 @@ private static boolean lyricsMatch() { private static String getCurrentLine() { String line = LyricsManager.getInstance().getCurrentLineText(); - if (line.isEmpty()) { + if (line == null) { String rt = realTitle; return rt == null ? "" : rt; } return line; } - private static MediaMetadata buildMetadata(String title) { - MediaMetadata.Builder builder = metadataBuilder; - if (builder == null) { - return null; - } - + /** The track format shown as the artist, or the plain artist if the title is unknown. */ + private static String getTrackLine() { String ra = realArtist; String artist = ra == null ? "" : ra; - String display = artist; - String rt = realTitle; - if (rt != null && !rt.isEmpty()) { - display = new TrackInfo(rt, artist, "", 0) - .displayWith(Settings.LYRICS_DISPLAY_ARTIST_FIRST.get()); + return rt == null || rt.isEmpty() ? artist : LyricsPresentation.trackLabel(rt, artist); + } + + @Nullable + private static MediaMetadata buildMetadata(String title, String artist) { + MediaMetadata.Builder builder = metadataBuilder; + if (builder == null) { + return null; } builder.putString(MediaMetadata.METADATA_KEY_TITLE, title); builder.putString(MediaMetadata.METADATA_KEY_DISPLAY_TITLE, title); - builder.putString(MediaMetadata.METADATA_KEY_ARTIST, display); - builder.putString(MediaMetadata.METADATA_KEY_DISPLAY_SUBTITLE, display); + builder.putString(MediaMetadata.METADATA_KEY_ARTIST, artist); + builder.putString(MediaMetadata.METADATA_KEY_DISPLAY_SUBTITLE, artist); return builder.build(); } } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/session/LyricNotificationRefresh.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/session/LyricNotificationRefresh.java new file mode 100644 index 0000000000..19e8291b64 --- /dev/null +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/session/LyricNotificationRefresh.java @@ -0,0 +1,69 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/4002 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.extension.music.patches.lyrics.session; + +import android.annotation.SuppressLint; +import android.app.Notification; +import android.app.NotificationManager; +import android.content.Context; +import android.media.session.MediaSession; +import android.service.notification.StatusBarNotification; +import android.text.TextUtils; + +import androidx.annotation.Nullable; + +import app.morphe.extension.shared.Logger; +import app.morphe.extension.shared.Utils; + +/** + * Updates the text of the app's media notification. Some system media controls only read the + * session metadata again when the notification is posted, so a new line would not show otherwise. + * The notification is posted again as is with only the title and the text changed, so it keeps + * its id, channel, controls and artwork. + */ +final class LyricNotificationRefresh { + + private LyricNotificationRefresh() { + } + + // Media notifications do not need the notification permission. + @SuppressLint("NotificationPermission") + @SuppressWarnings("deprecation") + static void refresh(MediaSession session, @Nullable String title, @Nullable String text) { + try { + if (title == null) { + return; + } + + NotificationManager manager = (NotificationManager) Utils.getContext() + .getSystemService(Context.NOTIFICATION_SERVICE); + for (StatusBarNotification entry : manager.getActiveNotifications()) { + Notification original = entry.getNotification(); + MediaSession.Token token = original.extras.getParcelable(Notification.EXTRA_MEDIA_SESSION); + if (!session.getSessionToken().equals(token)) { + continue; + } + if (TextUtils.equals(title, original.extras.getCharSequence(Notification.EXTRA_TITLE)) + && (text == null || TextUtils.equals(text, + original.extras.getCharSequence(Notification.EXTRA_TEXT)))) { + continue; + } + + Notification changed = original.clone(); + changed.extras.putCharSequence(Notification.EXTRA_TITLE, title); + if (text != null) { + changed.extras.putCharSequence(Notification.EXTRA_TEXT, text); + } + changed.flags |= Notification.FLAG_ONLY_ALERT_ONCE; + manager.notify(entry.getTag(), entry.getId(), changed); + } + } catch (Exception ex) { + Logger.printException(() -> "refresh failure", ex); + } + } +} diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/session/MiniplayerLyrics.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/session/MiniplayerLyrics.java index ab456f3409..4662b30993 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/session/MiniplayerLyrics.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/session/MiniplayerLyrics.java @@ -19,6 +19,7 @@ import java.util.Objects; import app.morphe.extension.music.patches.lyrics.LyricsManager; +import app.morphe.extension.music.patches.lyrics.LyricsPresentation; import app.morphe.extension.music.patches.lyrics.model.MetadataCleaner; import app.morphe.extension.music.patches.lyrics.model.TrackInfo; import app.morphe.extension.music.settings.Settings; @@ -59,9 +60,6 @@ public final class MiniplayerLyrics { @Nullable private static String rawDisplayArtist; - @Nullable - private static String cachedSubtitle; - private static boolean mirrored; @Nullable @@ -101,7 +99,6 @@ private static void disableFeature() { * without recreating the miniplayer. */ public static void onSettingsChanged() { - cachedSubtitle = null; if (!Settings.LYRICS_ENABLED.get() || !Settings.LYRICS_MINIPLAYER.get() || titleRef.get() == null || subtitleRef.get() == null) { disableFeature(); @@ -127,7 +124,6 @@ public static void onMediaSessionSetMetadata(MediaSession session, MediaMetadata displayArtist = parsed[0]; rawDisplayTitle = title; rawDisplayArtist = artist; - cachedSubtitle = null; // invalidate on track change android.net.Uri mediaUri = LyricsManager.parseMediaUri(original); LyricsManager.getInstance().onDisplayedTrackChanged(title, artist, mediaUri); @@ -216,10 +212,9 @@ private static void update() { if (synced) { String line = manager.getCurrentLineText(); - String newTitle = line.isEmpty() ? displayTitleOr(track) : line; - if (cachedSubtitle == null) { - cachedSubtitle = buildSubtitle(track); - } + String newTitle = line == null ? displayTitleOr(track) : line; + // Built on every tick, as the track format frames take turns. + String newSubtitle = buildSubtitle(track); if (!mirrored) { preMirrorTitle = title.getText(); preMirrorSubtitle = subtitle.getText(); @@ -229,10 +224,10 @@ private static void update() { title.setText(newTitle); } lastMirrorTitle = newTitle; - if (!TextUtils.equals(cachedSubtitle, subtitle.getText())) { - subtitle.setText(cachedSubtitle); + if (!TextUtils.equals(newSubtitle, subtitle.getText())) { + subtitle.setText(newSubtitle); } - lastMirrorSubtitle = cachedSubtitle; + lastMirrorSubtitle = newSubtitle; mirrored = true; } else { restoreIfMirrored(); @@ -280,10 +275,9 @@ private static String buildSubtitle(TrackInfo track) { final String rawArtist = rawDisplayArtist; if (rawTitle == null || rawTitle.trim().isEmpty() || rawArtist == null || rawArtist.trim().isEmpty()) { - return track.displayWith(Settings.LYRICS_DISPLAY_ARTIST_FIRST.get()); + return LyricsPresentation.trackLabel(track.title(), track.artist()); } - return new TrackInfo(rawTitle, rawArtist, "", 0) - .displayWith(Settings.LYRICS_DISPLAY_ARTIST_FIRST.get()); + return LyricsPresentation.trackLabel(rawTitle, rawArtist); } private static void restoreField(TextView view, @Nullable String lastMirrored, diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsPanelInstaller.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsPanelInstaller.java index b4f6f4e0ef..1d4c3a4d80 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsPanelInstaller.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsPanelInstaller.java @@ -25,6 +25,7 @@ import app.morphe.extension.music.patches.lyrics.LyricsManager; import app.morphe.extension.music.patches.lyrics.model.LyricsFilterDefaults; +import app.morphe.extension.music.patches.lyrics.session.LockScreenLyrics; import app.morphe.extension.music.patches.lyrics.session.MiniplayerLyrics; import app.morphe.extension.music.settings.Settings; import app.morphe.extension.shared.Logger; @@ -117,9 +118,25 @@ private static void onLyricsSettingChanged(String key) { updateKeepScreenOn(isLyricsPanelOpen()); return; } - if (Settings.LYRICS_MINIPLAYER.key.equals(key) - || Settings.LYRICS_DISPLAY_ARTIST_FIRST.key.equals(key)) { + final boolean formatting = Settings.LYRICS_FORCE_CAPITALIZE.key.equals(key) + || Settings.LYRICS_STRIP_TRAILING_PUNCTUATION.key.equals(key); + if (formatting) { + // Also shown in the miniplayer and the media session. MiniplayerLyrics.onSettingsChanged(); + LockScreenLyrics.onSettingsChanged(); + } else if (Settings.LYRICS_MINIPLAYER.key.equals(key) + || Settings.LYRICS_MEDIASESSION.key.equals(key) + || Settings.LYRICS_MINI_TRACK_FORMAT.key.equals(key) + || Settings.LYRICS_MINI_ENCAPSULATION.key.equals(key) + || Settings.LYRICS_MINI_CHAR_LIMIT.key.equals(key) + || Settings.LYRICS_MINI_ARTIST_CHAR_LIMIT.key.equals(key) + || Settings.LYRICS_MINI_ARTIST_MIN_MS.key.equals(key) + || Settings.LYRICS_MINI_ARTIST_MAX_MS.key.equals(key) + || Settings.LYRICS_MINI_FULL_WIDTH_MULTIPLIER.key.equals(key) + || Settings.LYRICS_MINI_INSTRUMENTAL.key.equals(key) + || Settings.LYRICS_MINI_NEXT_OFFSET_MS.key.equals(key)) { + MiniplayerLyrics.onSettingsChanged(); + LockScreenLyrics.onSettingsChanged(); return; } final LyricsPanelView panelView = panelReference.get(); @@ -133,9 +150,9 @@ private static void onLyricsSettingChanged(String key) { panelView.applyToolbarButtonSettings(); } else if (Settings.LYRICS_TEXT_SIZE.key.equals(key)) { panelView.applyTextSize(); - } else if (Settings.LYRICS_SWAP_TRANS_ROMA.key.equals(key) + } else if (formatting || Settings.LYRICS_SWAP_TRANS_ROMA.key.equals(key) || Settings.LYRICS_TAP_TO_SEEK.key.equals(key)) { - // Both are read while the line is laid out, so the lines need rebuilding. + // These are read while the line is laid out, so the lines need rebuilding. panelView.applyLyricLayoutSettings(); } else if (Settings.LYRICS_WORD_SYNC.key.equals(key)) { panelView.applyWordSyncSetting(); @@ -383,7 +400,7 @@ public static boolean isOtherPanelForeground() { @Nullable private static ViewGroup findLyricsPanelContent(View root) { TextView title = findVisibleTitle(root); - if (title != null && isLyricsTitle(title)) { + if (isLyricsTitle(title)) { // Heading and content share a panel, so the container comes from the heading's // own panel rather than a global lookup. return findPanelContent(title); diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsPanelView.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsPanelView.java index 68e3e9d542..22aca9374e 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsPanelView.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsPanelView.java @@ -67,6 +67,7 @@ import java.util.function.IntConsumer; import app.morphe.extension.music.patches.lyrics.LyricsManager; +import app.morphe.extension.music.patches.lyrics.LyricsPresentation; import app.morphe.extension.music.patches.lyrics.derive.LyricsRomanizer; import app.morphe.extension.music.patches.lyrics.derive.LyricsTranslator; import app.morphe.extension.music.patches.lyrics.derive.QuickLRCClient; @@ -1195,7 +1196,7 @@ private void buildLyrics(Lyrics newLyrics) { final LyricsLineView prevView = i < prevCount && prevViews.get(i) instanceof LyricsLineView pv ? pv : null; final boolean reusePrevious = prevView != null && !contentInBuild; - final List gapTimings = reusePrevious + List gapTimings = reusePrevious && i < prevSpans.size() ? prevSpans.get(i) : Collections.emptyList(); lineWordSpans.add(reusePrevious ? gapTimings : new ArrayList<>()); @@ -1207,17 +1208,34 @@ private void buildLyrics(Lyrics newLyrics) { // same-content rebuild reuses the previous row so toggles never blink existing text. int gapOrigStart = 0; if (preserveHidePosition) { - LyricsSpanBuilder.BuildResult result = - LyricsSpanBuilder.buildLineText(line, gapTimings, i, - perWordRomaji, romanizedLines, translatedLines, onlyMode, - baseLinePaint, isRightAligned); + gapOrigStart = computeOriginalTextStart(line, i, onlySnap, romanizedSnap, + translatedSnap, perWordRomajiSnap); + LyricsSpanBuilder.BuildResult result; + if (LyricsPresentation.isFormattingEnabled()) { + // The previous spans are offsets in the filtered text, so the line is built + // again from the raw word timings and filtered once more. + final List rawTimings = LyricsSpanBuilder + .computeWordTimings(newLyrics.lines(), i, onlySnap != OnlyMode.NONE); + final LyricsSpanBuilder.BuildResult raw = LyricsSpanBuilder.buildLineText(line, + rawTimings, i, perWordRomaji, romanizedLines, translatedLines, onlyMode, + baseLinePaint, isRightAligned); + final LyricsSpanBuilder.PresentedLine presentation = LyricsSpanBuilder.present(raw, + LyricsViewSupport.wordSpansFor(raw, onlySnap != OnlyMode.NONE, rawTimings, + gapOrigStart), gapOrigStart); + result = presentation.display(); + gapTimings = presentation.words(); + gapOrigStart = presentation.originalStart(); + lineWordSpans.set(i, gapTimings); + } else { + result = LyricsSpanBuilder.buildLineText(line, gapTimings, i, + perWordRomaji, romanizedLines, translatedLines, onlyMode, + baseLinePaint, isRightAligned); + } lineView.setText(result.text()); lineView.setTranslationBounds(result.transStart(), result.transEnd()); lineView.setRomanizationBounds(result.romaStart(), result.romaEnd(), result.readingSpans() != null && !result.readingSpans().isEmpty()); lineView.setSmallRunHoldsWords(result.smallRunHoldsWords()); - gapOrigStart = computeOriginalTextStart(line, i, onlySnap, romanizedSnap, - translatedSnap, perWordRomajiSnap); } else if (reusePrevious) { lineView.setText(prevView.getText()); lineView.setTranslationBounds(prevView.transStart, prevView.transEnd); @@ -1228,7 +1246,7 @@ private void buildLyrics(Lyrics newLyrics) { lineView.romaReveal = prevView.romaReveal; gapOrigStart = prevView.originalTextStart; } else { - String rawText = line.text().isEmpty() ? "♪" : line.text(); + String rawText = line.text().isEmpty() ? "♪" : LyricsPresentation.formatText(line.text()); boolean isRtl = ScriptDetector.isLineRtl(rawText); SpannableString initialText = new SpannableString(rawText); initialText.setSpan(new AlignmentSpan.Standard(LyricsSpanBuilder.resolveAlignment(isRtl, isRightAligned)), @@ -1438,10 +1456,14 @@ private void buildLyrics(Lyrics newLyrics) { allTimings.get(i), i, perWordRomaji, romanizedLines, translatedLines, onlyMode, baseLinePaint, lineRightAligned[i]); + final LyricsSpanBuilder.PresentedLine presentation = LyricsSpanBuilder.present(result, + LyricsViewSupport.wordSpansFor(result, only, allTimings.get(i), allOrigStarts.get(i)), + only ? 0 : allOrigStarts.get(i)); + result = presentation.display(); + lineOriginalStarts.set(i, presentation.originalStart()); tv.setText(result.text()); lineUnsungSpans.set(i, result.unsungSpan()); - lineWordSpans.set(i, LyricsViewSupport.wordSpansFor(result, only, - allTimings.get(i), allOrigStarts.get(i))); + lineWordSpans.set(i, presentation.words()); if (tv instanceof LyricsLineView lineView) { lineView.setTranslationBounds(result.transStart(), result.transEnd()); lineView.setRomanizationBounds(result.romaStart(), result.romaEnd(), @@ -2204,7 +2226,7 @@ private void onCopyClicked() { } // A copy takes what the panel is showing, so an only mode hands over the // translation or romanization the lines are rendered from. - text.append(textToCopyAt(lines.get(i), i)); + text.append(LyricsPresentation.formatText(textToCopyAt(lines.get(i), i))); } ClipboardManager clipboard = (ClipboardManager) getContext() diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsSpanBuilder.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsSpanBuilder.java index c54664c936..77097319d7 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsSpanBuilder.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsSpanBuilder.java @@ -25,7 +25,9 @@ import java.util.Collections; import java.util.List; +import app.morphe.extension.music.patches.lyrics.LyricsPresentation; import app.morphe.extension.music.patches.lyrics.model.LyricsLine; +import app.morphe.extension.music.patches.lyrics.model.LyricsTextFormatter; import app.morphe.extension.music.patches.lyrics.model.Word; import app.morphe.extension.music.patches.lyrics.romanizers.LocalRomanizer; import app.morphe.extension.music.patches.lyrics.romanizers.ScriptDetector; @@ -58,6 +60,42 @@ record BuildResult(Spannable text, @Nullable ForegroundColorSpan unsungSpan, private record Spacing(int mainClear, int smallClear, int boundaryClear) { } + record PresentedLine(BuildResult display, List words, int originalStart) { + } + + /** + * Applies the lyrics text filters to a built line, and moves its spans and word offsets to + * where they are in the filtered text. + * + * @param words Word offsets relative to originalStart. + */ + static PresentedLine present(BuildResult raw, List words, int originalStart) { + if (!LyricsPresentation.isFormattingEnabled()) { + return new PresentedLine(raw, words, originalStart); + } + final LyricsTextFormatter.Result formatted = LyricsPresentation.format(raw.text().toString()); + if (formatted.text().contentEquals(raw.text())) { + return new PresentedLine(raw, words, originalStart); + } + final SpannableString text = new SpannableString(formatted.text()); + for (Object span : raw.text().getSpans(0, raw.text().length(), Object.class)) { + final int start = formatted.offset(raw.text().getSpanStart(span)); + final int end = formatted.offset(raw.text().getSpanEnd(span)); + if (end > start) text.setSpan(span, start, end, raw.text().getSpanFlags(span)); + } + final int base = formatted.offset(originalStart); + final List mapped = new ArrayList<>(words.size()); + for (WordTiming word : words) { + final int start = formatted.offset(word.start() + originalStart) - base; + final int end = formatted.offset(word.end() + originalStart) - base; + if (end > start) mapped.add(new WordTiming(start, end, word.startMs(), word.endMs(), word.romaji())); + } + return new PresentedLine(new BuildResult(text, raw.unsungSpan(), + formatted.offset(raw.transStart()), formatted.offset(raw.transEnd()), + formatted.offset(raw.romaStart()), formatted.offset(raw.romaEnd()), + raw.readingSpans(), raw.smallRunHoldsWords()), mapped, base); + } + private static final class UniformLineSpacingSpan implements LineHeightSpan { private final Spacing spacing; private int baseDescent; @@ -130,8 +168,8 @@ record Reading(@Nullable String text, @Nullable List spans) { * whole-line reading is spread over. */ static Reading readingFor(LyricsLine line, int index, - @Nullable List romanizedLines, boolean perWordRomaji, - @Nullable List timings) { + @Nullable List romanizedLines, boolean perWordRomaji, + @Nullable List timings) { if (perWordRomaji) { final List words = readingWords(line, index, romanizedLines); // A word shown as written stands in for a reading only where the line is really read per word. diff --git a/extensions/music/src/main/java/app/morphe/extension/music/settings/Settings.java b/extensions/music/src/main/java/app/morphe/extension/music/settings/Settings.java index 10744fa27a..209db03f44 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/settings/Settings.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/settings/Settings.java @@ -18,11 +18,13 @@ import app.morphe.extension.music.patches.lyrics.derive.AiProviders; import app.morphe.extension.music.patches.lyrics.derive.OpenAIClient; import app.morphe.extension.music.patches.lyrics.model.LyricsFilterDefaults; +import app.morphe.extension.music.patches.lyrics.model.MiniLyricsFormatter; import app.morphe.extension.music.sponsorblock.MusicSponsorBlockConfig; import app.morphe.extension.shared.Logger; import app.morphe.extension.shared.patches.DownloadMode; import app.morphe.extension.shared.settings.BooleanSetting; import app.morphe.extension.shared.settings.EnumSetting; +import app.morphe.extension.shared.settings.FloatSetting; import app.morphe.extension.shared.settings.IntegerSetting; import app.morphe.extension.shared.settings.SharedYouTubeSettings; import app.morphe.extension.shared.settings.StringSetting; @@ -247,7 +249,17 @@ public class Settings extends SharedYouTubeSettings { public static final BooleanSetting LYRICS_SB_MATCHING = new BooleanSetting("morphe_music_lyrics_sb_matching", TRUE, false, parent(LYRICS_ENABLED)); public static final BooleanSetting LYRICS_MEDIASESSION = new BooleanSetting("morphe_music_lyrics_mediasession", FALSE, false, parent(LYRICS_ENABLED)); public static final BooleanSetting LYRICS_MINIPLAYER = new BooleanSetting("morphe_music_lyrics_miniplayer", FALSE, false, parent(LYRICS_ENABLED)); - public static final BooleanSetting LYRICS_DISPLAY_ARTIST_FIRST = new BooleanSetting("morphe_music_lyrics_display_artist_first", FALSE, false, parentsAll(parent(LYRICS_ENABLED), parentsAny(LYRICS_MINIPLAYER, LYRICS_MEDIASESSION))); + public static final BooleanSetting LYRICS_FORCE_CAPITALIZE = new BooleanSetting("morphe_music_lyrics_force_capitalize", FALSE, false, parent(LYRICS_ENABLED)); + public static final BooleanSetting LYRICS_STRIP_TRAILING_PUNCTUATION = new BooleanSetting("morphe_music_lyrics_strip_trailing_punctuation", FALSE, false, parent(LYRICS_ENABLED)); + public static final StringSetting LYRICS_MINI_TRACK_FORMAT = new StringSetting("morphe_music_lyrics_mini_track_format", MiniLyricsFormatter.DEFAULT_TRACK_FORMAT, false, parentsAll(parent(LYRICS_ENABLED), parentsAny(LYRICS_MINIPLAYER, LYRICS_MEDIASESSION))); + public static final StringSetting LYRICS_MINI_ENCAPSULATION = new StringSetting("morphe_music_lyrics_mini_encapsulation", MiniLyricsFormatter.DEFAULT_ENCAPSULATION, false, parentsAll(parent(LYRICS_ENABLED), parentsAny(LYRICS_MINIPLAYER, LYRICS_MEDIASESSION))); + public static final IntegerSetting LYRICS_MINI_CHAR_LIMIT = new IntegerSetting("morphe_music_lyrics_mini_char_limit", 0, false, parentsAll(parent(LYRICS_ENABLED), parentsAny(LYRICS_MINIPLAYER, LYRICS_MEDIASESSION))); + public static final IntegerSetting LYRICS_MINI_ARTIST_CHAR_LIMIT = new IntegerSetting("morphe_music_lyrics_mini_artist_char_limit", 0, false, parentsAll(parent(LYRICS_ENABLED), parentsAny(LYRICS_MINIPLAYER, LYRICS_MEDIASESSION))); + public static final IntegerSetting LYRICS_MINI_ARTIST_MIN_MS = new IntegerSetting("morphe_music_lyrics_mini_artist_min_ms", 1500, false, parentsAll(parent(LYRICS_ENABLED), parentsAny(LYRICS_MINIPLAYER, LYRICS_MEDIASESSION))); + public static final IntegerSetting LYRICS_MINI_ARTIST_MAX_MS = new IntegerSetting("morphe_music_lyrics_mini_artist_max_ms", 3000, false, parentsAll(parent(LYRICS_ENABLED), parentsAny(LYRICS_MINIPLAYER, LYRICS_MEDIASESSION))); + public static final FloatSetting LYRICS_MINI_FULL_WIDTH_MULTIPLIER = new FloatSetting("morphe_music_lyrics_mini_full_width_multiplier", 1.75f, false, parentsAll(parent(LYRICS_ENABLED), parentsAny(LYRICS_MINIPLAYER, LYRICS_MEDIASESSION))); + public static final StringSetting LYRICS_MINI_INSTRUMENTAL = new StringSetting("morphe_music_lyrics_mini_instrumental", MiniLyricsFormatter.DEFAULT_INSTRUMENTAL, false, parentsAll(parent(LYRICS_ENABLED), parentsAny(LYRICS_MINIPLAYER, LYRICS_MEDIASESSION))); + public static final IntegerSetting LYRICS_MINI_NEXT_OFFSET_MS = new IntegerSetting("morphe_music_lyrics_mini_next_offset_ms", -800, false, parentsAll(parent(LYRICS_ENABLED), parentsAny(LYRICS_MINIPLAYER, LYRICS_MEDIASESSION))); public static final BooleanSetting LYRICS_USE_EMBEDDED = new BooleanSetting("morphe_music_lyrics_use_embedded", TRUE, true, parent(LYRICS_ENABLED)); public static final StringSetting LYRICS_CAPTION_COOKIES = new StringSetting("morphe_music_lyrics_caption_cookies", "", false, parent(LYRICS_ENABLED)); @@ -293,6 +305,7 @@ public class Settings extends SharedYouTubeSettings { private static final BooleanSetting DEPRECATED_PERMANENT_REPEAT = new BooleanSetting("morphe_music_permanent_repeat", FALSE, true); private static final BooleanSetting DEPRECATED_LYRICS_USE_AI_TRANSLATION = new BooleanSetting("morphe_music_lyrics_use_ai_translation", FALSE, false); private static final BooleanSetting DEPRECATED_LYRICS_USE_AI_ROMANIZATION = new BooleanSetting("morphe_music_lyrics_use_ai_romanization", FALSE, false); + private static final BooleanSetting DEPRECATED_LYRICS_DISPLAY_ARTIST_FIRST = new BooleanSetting("morphe_music_lyrics_display_artist_first", FALSE, false); static { migrateOldSettingToNew(DEPRECATED_EXTERNAL_DOWNLOADER_FLYOUT_BUTTON, DEPRECATED_EXTERNAL_DOWNLOADER_FLYOUT_MENU); @@ -302,6 +315,7 @@ public class Settings extends SharedYouTubeSettings { migrateDownloadButtonOverride(); migrateLyricsAiSwitchToProvider(DEPRECATED_LYRICS_USE_AI_TRANSLATION, LYRICS_TRANSLATION_PROVIDER); migrateLyricsAiSwitchToProvider(DEPRECATED_LYRICS_USE_AI_ROMANIZATION, LYRICS_ROMANIZATION_PROVIDER); + migrateLyricsArtistFirstToTrackFormat(); if ("openrouter".equals(LYRICS_TRANSLATION_PROVIDER.get()) || "openai".equals(LYRICS_TRANSLATION_PROVIDER.get())) { LYRICS_TRANSLATION_PROVIDER.save(AiProviders.OPENAI_COMPATIBLE); } @@ -333,6 +347,8 @@ public class Settings extends SharedYouTubeSettings { 18, 36, 2, "sp")); SeekBarPreference.register(new SeekBarConfig(LYRICS_OFFSET_MS, -2000, 2000, 100, "ms")); + SeekBarPreference.register(new SeekBarConfig(LYRICS_MINI_NEXT_OFFSET_MS, + -2000, 2000, 100, "ms")); SeekBarPreference.register(new SeekBarConfig(PLAYBACK_SPEED, 25, 300, 5, "%")); @@ -368,6 +384,14 @@ private static void migrateLyricsAiSwitchToProvider(BooleanSetting oldSwitch, St oldSwitch.resetToDefault(); } + /** The artist first switch was replaced with the track label format. */ + private static void migrateLyricsArtistFirstToTrackFormat() { + if (DEPRECATED_LYRICS_DISPLAY_ARTIST_FIRST.get() && LYRICS_MINI_TRACK_FORMAT.isSetToDefault()) { + LYRICS_MINI_TRACK_FORMAT.save(MiniLyricsFormatter.ARTIST_FIRST_TRACK_FORMAT); + } + DEPRECATED_LYRICS_DISPLAY_ARTIST_FIRST.resetToDefault(); + } + private static void migrateDownloadButtonOverride() { if (DOWNLOAD_BUTTON_OVERRIDE_MIGRATED.get()) { return; diff --git a/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsEncapsulationPreference.java b/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsEncapsulationPreference.java new file mode 100644 index 0000000000..a03701f8e3 --- /dev/null +++ b/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsEncapsulationPreference.java @@ -0,0 +1,49 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/4002 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.extension.music.settings.preference; + +import static app.morphe.extension.shared.StringRef.str; + +import android.content.Context; +import android.util.AttributeSet; + +import app.morphe.extension.music.patches.lyrics.model.MiniLyricsFormatter; +import app.morphe.extension.shared.Utils; +import app.morphe.extension.shared.settings.preference.ResettableEditTextPreference; + +/** + * Lyrics encapsulation template, which is only saved if it has a place for the lyric. + */ +@SuppressWarnings({"unused", "deprecation"}) +public class LyricsEncapsulationPreference extends ResettableEditTextPreference { + + public LyricsEncapsulationPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + public LyricsEncapsulationPreference(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public LyricsEncapsulationPreference(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public LyricsEncapsulationPreference(Context context) { + super(context); + } + + @Override + protected boolean callChangeListener(Object newValue) { + if (!MiniLyricsFormatter.validEncapsulation(String.valueOf(newValue))) { + Utils.showToastShort(str("morphe_music_lyrics_mini_encapsulation_invalid")); + return false; + } + return super.callChangeListener(newValue); + } +} diff --git a/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsFieldsDialog.java b/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsFieldsDialog.java new file mode 100644 index 0000000000..50e30b65ff --- /dev/null +++ b/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsFieldsDialog.java @@ -0,0 +1,78 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/4002 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.extension.music.settings.preference; + +import static app.morphe.extension.shared.StringRef.str; + +import android.app.Dialog; +import android.content.Context; +import android.text.InputType; +import android.util.Pair; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.ScrollView; +import android.widget.TextView; + +import app.morphe.extension.shared.theme.ThemeUtils; +import app.morphe.extension.shared.ui.CustomDialog; +import app.morphe.extension.shared.ui.Dim; + +/** A dialog that edits several lyrics settings together, with save and reset buttons. */ +final class LyricsFieldsDialog { + + private final Context context; + private final LinearLayout content; + + LyricsFieldsDialog(Context context) { + this.context = context; + content = new LinearLayout(context); + content.setOrientation(LinearLayout.VERTICAL); + } + + void addLabel(String key) { + TextView label = new TextView(context); + label.setText(str(key)); + label.setTextColor(ThemeUtils.getAppForegroundColor()); + label.setTextSize(14); + label.setPadding(0, Dim.dp12, 0, Dim.dp8); + content.addView(label); + } + + EditText addInput(String labelKey, String value, int inputType) { + addLabel(labelKey); + EditText input = ThemedInput.create(context); + input.setInputType(inputType); + input.setText(value); + content.addView(input); + return input; + } + + EditText addNumberInput(String labelKey, Number value, boolean decimal) { + return addInput(labelKey, value.toString(), InputType.TYPE_CLASS_NUMBER + | (decimal ? InputType.TYPE_NUMBER_FLAG_DECIMAL : 0)); + } + + void show(String titleKey, Runnable onSave, Runnable onReset) { + Pair pair = CustomDialog.create(context, + str(titleKey), null, null, + str("morphe_settings_save"), onSave, + () -> {}, + str("morphe_settings_reset"), onReset, + true); + ScrollView scroll = new ScrollView(context); + scroll.addView(content); + pair.second.addView(scroll, pair.second.getChildCount() - 1, + new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT)); + pair.first.show(); + } + + static String text(EditText input) { + return input.getText().toString().trim(); + } +} diff --git a/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsLineLimitsPreference.java b/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsLineLimitsPreference.java new file mode 100644 index 0000000000..7dd48946b6 --- /dev/null +++ b/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsLineLimitsPreference.java @@ -0,0 +1,75 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/4002 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.extension.music.settings.preference; + +import static app.morphe.extension.music.settings.preference.LyricsFieldsDialog.text; +import static app.morphe.extension.shared.StringRef.str; + +import android.content.Context; +import android.preference.Preference; +import android.util.AttributeSet; +import android.widget.EditText; + +import app.morphe.extension.music.settings.Settings; +import app.morphe.extension.shared.Utils; + +/** Edits both line limits and their shared full-width character weight together. */ +@SuppressWarnings({"unused", "deprecation"}) +public class LyricsLineLimitsPreference extends Preference { + private static final int MAX_CHAR_LIMIT = 10000; + + public LyricsLineLimitsPreference(Context context, AttributeSet attrs) { + super(context, attrs); + setPersistent(false); + } + + @Override + protected void onClick() { + LyricsFieldsDialog dialog = new LyricsFieldsDialog(getContext()); + EditText lyrics = dialog.addNumberInput("morphe_music_lyrics_mini_line_limits_lyrics", + Settings.LYRICS_MINI_CHAR_LIMIT.get(), false); + EditText artist = dialog.addNumberInput("morphe_music_lyrics_mini_line_limits_artist", + Settings.LYRICS_MINI_ARTIST_CHAR_LIMIT.get(), false); + EditText weight = dialog.addNumberInput("morphe_music_lyrics_mini_line_limits_weight", + Settings.LYRICS_MINI_FULL_WIDTH_MULTIPLIER.get(), true); + + dialog.show("morphe_music_lyrics_mini_line_limits_title", () -> { + final int lyricLimit; + final int artistLimit; + final float multiplier; + try { + lyricLimit = Integer.parseInt(text(lyrics)); + artistLimit = Integer.parseInt(text(artist)); + multiplier = Float.parseFloat(text(weight)); + } catch (NumberFormatException ex) { + showInvalidToast(); + return; + } + if (!isValidLimit(lyricLimit) || !isValidLimit(artistLimit) + || !Float.isFinite(multiplier) || multiplier <= 0) { + showInvalidToast(); + return; + } + Settings.LYRICS_MINI_CHAR_LIMIT.save(lyricLimit); + Settings.LYRICS_MINI_ARTIST_CHAR_LIMIT.save(artistLimit); + Settings.LYRICS_MINI_FULL_WIDTH_MULTIPLIER.save(multiplier); + }, () -> { + Settings.LYRICS_MINI_CHAR_LIMIT.resetToDefault(); + Settings.LYRICS_MINI_ARTIST_CHAR_LIMIT.resetToDefault(); + Settings.LYRICS_MINI_FULL_WIDTH_MULTIPLIER.resetToDefault(); + }); + } + + private static boolean isValidLimit(int limit) { + return limit >= 0 && limit <= MAX_CHAR_LIMIT; + } + + private static void showInvalidToast() { + Utils.showToastShort(str("morphe_music_lyrics_mini_line_limits_invalid")); + } +} diff --git a/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsTrackFormatPreference.java b/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsTrackFormatPreference.java new file mode 100644 index 0000000000..1678bbcb46 --- /dev/null +++ b/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsTrackFormatPreference.java @@ -0,0 +1,72 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/4002 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.extension.music.settings.preference; + +import static app.morphe.extension.music.settings.preference.LyricsFieldsDialog.text; +import static app.morphe.extension.shared.StringRef.str; + +import android.content.Context; +import android.preference.Preference; +import android.text.InputType; +import android.util.AttributeSet; +import android.widget.EditText; + +import app.morphe.extension.music.settings.Settings; +import app.morphe.extension.shared.Utils; + +/** Edits the track format and how long each of its frames is shown. */ +@SuppressWarnings({"unused", "deprecation"}) +public class LyricsTrackFormatPreference extends Preference { + public LyricsTrackFormatPreference(Context context, AttributeSet attrs) { + super(context, attrs); + setPersistent(false); + } + + @Override + protected void onClick() { + LyricsFieldsDialog dialog = new LyricsFieldsDialog(getContext()); + EditText template = dialog.addInput("morphe_music_lyrics_mini_track_format_summary", + Settings.LYRICS_MINI_TRACK_FORMAT.get(), + InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE); + template.setSingleLine(false); + template.setMinLines(3); + template.setMaxLines(6); + EditText minimum = dialog.addNumberInput("morphe_music_lyrics_mini_artist_min_label", + Settings.LYRICS_MINI_ARTIST_MIN_MS.get(), false); + EditText maximum = dialog.addNumberInput("morphe_music_lyrics_mini_artist_max_label", + Settings.LYRICS_MINI_ARTIST_MAX_MS.get(), false); + dialog.addLabel("morphe_music_lyrics_mini_artist_timing_explanation"); + + dialog.show("morphe_music_lyrics_mini_track_format_title", () -> { + final int min; + final int max; + try { + min = Integer.parseInt(text(minimum)); + max = Integer.parseInt(text(maximum)); + } catch (NumberFormatException ex) { + showInvalidToast(); + return; + } + if (min < 0 || max <= 0 || max < min) { + showInvalidToast(); + return; + } + Settings.LYRICS_MINI_ARTIST_MIN_MS.save(min); + Settings.LYRICS_MINI_ARTIST_MAX_MS.save(max); + Settings.LYRICS_MINI_TRACK_FORMAT.save(template.getText().toString()); + }, () -> { + Settings.LYRICS_MINI_TRACK_FORMAT.resetToDefault(); + Settings.LYRICS_MINI_ARTIST_MIN_MS.resetToDefault(); + Settings.LYRICS_MINI_ARTIST_MAX_MS.resetToDefault(); + }); + } + + private static void showInvalidToast() { + Utils.showToastShort(str("morphe_music_lyrics_mini_artist_timing_invalid")); + } +} diff --git a/extensions/music/src/test/java/app/morphe/extension/music/patches/lyrics/model/ArtistLineCycleTest.java b/extensions/music/src/test/java/app/morphe/extension/music/patches/lyrics/model/ArtistLineCycleTest.java new file mode 100644 index 0000000000..a790b152e6 --- /dev/null +++ b/extensions/music/src/test/java/app/morphe/extension/music/patches/lyrics/model/ArtistLineCycleTest.java @@ -0,0 +1,56 @@ +package app.morphe.extension.music.patches.lyrics.model; + +import static org.junit.Assert.assertEquals; +import java.util.List; +import org.junit.Test; + +public class ArtistLineCycleTest { + private static final List FRAMES = List.of("artist", "title", "variant"); + + @Test public void minimumHoldsAnEarlyLyricChangeUntilItExpires() { + ArtistLineCycle cycle = new ArtistLineCycle(); + assertEquals("artist", cycle.update(FRAMES, 1, 0, 1500, 3000)); + assertEquals("artist", cycle.update(FRAMES, 2, 500, 1500, 3000)); + assertEquals("artist", cycle.update(FRAMES, 2, 1499, 1500, 3000)); + assertEquals("title", cycle.update(FRAMES, 2, 1500, 1500, 3000)); + } + + @Test public void maximumAdvancesWithoutAnyLyricChange() { + ArtistLineCycle cycle = new ArtistLineCycle(); + assertEquals("artist", cycle.update(FRAMES, "same lyric", 0, 1500, 3000)); + assertEquals("artist", cycle.update(FRAMES, "same lyric", 2999, 1500, 3000)); + assertEquals("title", cycle.update(FRAMES, "same lyric", 3000, 1500, 3000)); + assertEquals("variant", cycle.update(FRAMES, "same lyric", 6000, 1500, 3000)); + } + + @Test public void pollingFromBothSurfacesDoesNotDoubleAdvance() { + ArtistLineCycle cycle = new ArtistLineCycle(); + cycle.update(FRAMES, 1, 0, 0, 3000); + assertEquals("title", cycle.update(FRAMES, 2, 1500, 0, 3000)); + assertEquals("title", cycle.update(FRAMES, 2, 1500, 0, 3000)); + assertEquals("title", cycle.current(FRAMES)); + } + + @Test public void distinctTimedLinesAreNotCollapsedByTheirText() { + ArtistLineCycle cycle = new ArtistLineCycle(); + LyricsLine first = new LyricsLine(0, "same lyric"); + LyricsLine second = new LyricsLine(2000, "same lyric"); + cycle.update(FRAMES, first, 0, 1500, 3000); + assertEquals("title", cycle.update(FRAMES, second, 2000, 1500, 3000)); + } + + @Test public void singleFrameIgnoresAnimationTiming() { + ArtistLineCycle cycle = new ArtistLineCycle(); + assertEquals("artist", cycle.update(List.of("artist"), 1, 0, 1500, 3000)); + assertEquals("artist", cycle.update(List.of("artist"), 2, 9000, 1500, 3000)); + } + + @Test public void editingFramesAndChangingTracksResetTheCycle() { + ArtistLineCycle cycle = new ArtistLineCycle(); + cycle.update(FRAMES, 1, 0, 1500, 3000); + cycle.update(FRAMES, 2, 2000, 1500, 3000); + assertEquals("new", cycle.update(List.of("new", "other"), 2, 2500, 1500, 3000)); + cycle.reset(); + assertEquals("artist", cycle.update(FRAMES, 1, 3000, 1500, 3000)); + } +} diff --git a/extensions/music/src/test/java/app/morphe/extension/music/patches/lyrics/model/EncapsulationRepeatTest.java b/extensions/music/src/test/java/app/morphe/extension/music/patches/lyrics/model/EncapsulationRepeatTest.java new file mode 100644 index 0000000000..de3e3e57e8 --- /dev/null +++ b/extensions/music/src/test/java/app/morphe/extension/music/patches/lyrics/model/EncapsulationRepeatTest.java @@ -0,0 +1,36 @@ +package app.morphe.extension.music.patches.lyrics.model; + +import org.junit.Test; + +import java.util.List; + +import static org.junit.Assert.assertEquals; + +public class EncapsulationRepeatTest { + private static final String TEMPLATE = "# {lrc} # :(x{n}):"; + + @Test public void colonSegmentsAppearOnlyForRepeats() { + assertEquals("# lyric # ", MiniLyricsFormatter.encapsulate(TEMPLATE, "lyric", 1)); + assertEquals("# lyric # (x2)", MiniLyricsFormatter.encapsulate(TEMPLATE, "lyric", 2)); + assertEquals("# lyric # (x3)", MiniLyricsFormatter.encapsulate(TEMPLATE, "lyric", 3)); + } + + @Test public void bracketedCountSyntaxResolves() { + assertEquals("lyric ", MiniLyricsFormatter.encapsulate("{lrc} :[{n} times]:", "lyric", 1)); + assertEquals("lyric [2 times]", MiniLyricsFormatter.encapsulate("{lrc} :[{n} times]:", "lyric", 2)); + } + + @Test public void escapedColonsStayLiteral() { + assertEquals(":hi: lyric", MiniLyricsFormatter.encapsulate("\\:hi\\: {lrc}", "lyric", 1)); + assertEquals(":hi: lyric", MiniLyricsFormatter.encapsulate("\\:hi\\: {lrc}", "lyric", 5)); + } + + @Test public void repeatCountCountsConsecutiveRun() { + List lines = List.of(new LyricsLine(0, "a"), new LyricsLine(1000, "a"), + new LyricsLine(2000, "b"), new LyricsLine(3000, "a"), new LyricsLine(4000, "a")); + assertEquals(1, MiniLyricsFormatter.repeatCount(lines, 0)); + assertEquals(2, MiniLyricsFormatter.repeatCount(lines, 1)); + assertEquals(1, MiniLyricsFormatter.repeatCount(lines, 3)); + assertEquals(2, MiniLyricsFormatter.repeatCount(lines, 4)); + } +} diff --git a/extensions/music/src/test/java/app/morphe/extension/music/patches/lyrics/model/LyricsPresentationTest.java b/extensions/music/src/test/java/app/morphe/extension/music/patches/lyrics/model/LyricsPresentationTest.java new file mode 100644 index 0000000000..4eaa9e8971 --- /dev/null +++ b/extensions/music/src/test/java/app/morphe/extension/music/patches/lyrics/model/LyricsPresentationTest.java @@ -0,0 +1,177 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/4002 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.extension.music.patches.lyrics.model; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +import java.util.List; + +public class LyricsPresentationTest { + private static String clean(String text) { + return LyricsTextFormatter.format(text, false, true).text(); + } + + @Test public void defaultsPreserveText() { + assertEquals("hello.", LyricsTextFormatter.format("hello.", false, false).text()); + assertEquals("Title - Artist", MiniLyricsFormatter.trackLabel(null, "Title", "Artist")); + assertEquals("hello.", MiniLyricsFormatter.encapsulate("{lrc}", "hello.", 1)); + assertEquals(1, MiniLyricsFormatter.chunks(new LyricsLine(0, "a long original line."), + 5000, 0, false, false).size()); + } + + + @Test public void redundantPunctuationOnlyAtTheEnd() { + assertEquals("I'm fine", clean("I'm fine.")); + assertEquals("Hello, how are you?", clean("Hello, how are you?")); + assertEquals("I'm fin.e", clean("I'm fin.e")); + assertEquals("Hello", clean("Hello,;:.")); + assertEquals("one\ntwo", clean("one.\ntwo,")); + } + + @Test public void closingPairsStayIntact() { + assertEquals("\"I'm fine\"", clean("\"I'm fine.\"")); + assertEquals("(hello)", clean("(hello.)")); + assertEquals("‘hello’", clean("‘hello.’")); + assertEquals("「你好」", clean("「你好。」")); + assertEquals("(really?)", clean("(really?)")); + assertEquals("\"I'm fin.e\"", clean("\"I'm fin.e\"")); + } + + @Test public void expressivePunctuationIsNeverDiscarded() { + for (String line : List.of("hello!", "really?!", "wait...", "wait…", "wait..", + "حقاً؟", "שלום!", "本当?", "αλήθεια;", "wow‼", "wait—")) { + assertEquals(line, clean(line)); + } + } + + @Test public void unicodeSentenceEndings() { + assertEquals("你好", clean("你好。")); + assertEquals("你好", clean("你好,")); + assertEquals("مرحبا", clean("مرحبا،")); + assertEquals("مرحبا", clean("مرحبا؛")); + assertEquals("Բարեւ", clean("Բարեւ։")); + assertEquals("नमस्ते", clean("नमस्ते।")); + assertEquals("hello", clean("hello.")); + } + + @Test public void capitalizeFirstCasedLetterAndPreserveRest() { + assertEquals("♪ Élan is HERE\n«Привет»\n你好", LyricsTextFormatter.format( + "♪ élan is HERE\n«привет»\n你好", true, false).text()); + assertEquals("SSong", LyricsTextFormatter.format("ßong", true, false).text()); + } + + @Test public void offsetMapFollowsCaseExpansionAndDeletion() { + LyricsTextFormatter.Result result = LyricsTextFormatter.format("ßong.\nhi.", true, true); + assertEquals("SSong\nHi", result.text()); + assertEquals(2, result.offset(1)); + assertEquals(5, result.offset(4)); + assertEquals(5, result.offset(5)); + assertEquals(6, result.offset(6)); + assertEquals(8, result.offset(9)); + assertEquals(-1, result.offset(-1)); + } + + @Test public void templatesAreLiteralAndNonRecursive() { + assertEquals("{a} • X", MiniLyricsFormatter.trackLabel("{t} • {a}", "{a}", "X")); + assertEquals("A / T", MiniLyricsFormatter.trackLabel("{a} / {t}", "T", "A")); + assertEquals("{unknown} T", MiniLyricsFormatter.trackLabel("{unknown} {t}", "T", "A")); + assertEquals("♪ hello ♪", MiniLyricsFormatter.encapsulate("♪ {lrc} ♪", "hello", 1)); + assertEquals("hello ", MiniLyricsFormatter.encapsulate("invalid", "hello", 1)); + assertFalse(MiniLyricsFormatter.validEncapsulation("invalid")); + assertTrue(MiniLyricsFormatter.validEncapsulation("{lrc} / {lrc}")); + assertEquals("hello / hello", MiniLyricsFormatter.encapsulate("{lrc} / {lrc}", "hello", 1)); + } + + @Test public void limitTriggersTwoWordBalancedHalves() { + List chunks = MiniLyricsFormatter.chunks( + new LyricsLine(0, "Build me up, don't get me down"), 10000, 15, false, false); + assertEquals(List.of("Build me up, don't", "get me down"), + chunks.stream().map(MiniLyricsFormatter.Chunk::text).toList()); + assertEquals(2, chunks.size()); // Limit triggers splitting; it is not a hard cutoff. + } + + @Test public void longWordsRemainWholeButUnspacedScriptsCanSplit() { + assertEquals(List.of("one extraordinarilylong", "end"), MiniLyricsFormatter.chunks( + new LyricsLine(0, "one extraordinarilylong end"), 5000, 5, false, false) + .stream().map(MiniLyricsFormatter.Chunk::text).toList()); + assertEquals(List.of("你好世" + MiniLyricsFormatter.SPLIT_ELLIPSIS, "界再见"), MiniLyricsFormatter.chunks( + new LyricsLine(0, "你好世界再见"), 5000, 3, false, false) + .stream().map(MiniLyricsFormatter.Chunk::text).toList()); + } + + @Test public void graphemeClustersAreNotBroken() { + assertEquals(1, MiniLyricsFormatter.characterCount("👩‍👩‍👧‍👦")); + assertEquals(1, MiniLyricsFormatter.characterCount("e\u0301")); + assertEquals(1, MiniLyricsFormatter.characterCount("🇺🇸")); + assertEquals(2, MiniLyricsFormatter.chunks(new LyricsLine(0, "🇺🇸🇨🇦"), + 1000, 1, false, false).size()); + } + + @Test public void wordTimesDriveChunkChangesAndSeeking() { + LyricsLine line = new LyricsLine(1000, 7000, "one two three four", List.of( + new Word(1000, 2000, "one"), new Word(2000, 3000, "two"), + new Word(4500, 5000, "three"), new Word(5000, 6000, "four"))); + List chunks = MiniLyricsFormatter.chunks(line, 7000, 7, false, false); + assertEquals("one two", MiniLyricsFormatter.current(chunks, 4499, "{lrc}", 1)); + assertEquals("three four", MiniLyricsFormatter.current(chunks, 4500, "{lrc}", 1)); + assertEquals("three four", MiniLyricsFormatter.current(chunks, 5000, "{lrc}", 1)); + assertEquals("one two", MiniLyricsFormatter.current(chunks, 1500, "{lrc}", 1)); + } + + @Test public void wrapperDoesNotConsumeBodyBudget() { + List chunks = MiniLyricsFormatter.chunks( + new LyricsLine(0, "hello world"), 5000, 5, false, false); + assertEquals("♪ hello ♪", MiniLyricsFormatter.current(chunks, 0, "♪ {lrc} ♪", 1)); + assertEquals("♪ world ♪", MiniLyricsFormatter.current(chunks, 4999, "♪ {lrc} ♪", 1)); + } + + @Test public void unknownEndDoesNotInventTiming() { + List chunks = MiniLyricsFormatter.chunks( + new LyricsLine(0, "hello world"), -1, 5, false, false); + assertEquals("hello", MiniLyricsFormatter.current(chunks, 999999, "{lrc}", 1)); + } + + @Test public void originalModelIsNotMutated() { + LyricsLine line = new LyricsLine(1000, "hello there."); + MiniLyricsFormatter.chunks(line, 5000, 5, true, true); + assertEquals("hello there.", line.text()); + assertEquals(1000, line.startTimeMs()); + } + + @Test public void timedWordsRetainUnalignedPunctuation() { + LyricsLine line = new LyricsLine(0, 4000, "hello, world.", List.of( + new Word(0, 1000, "hello"), new Word(2000, 3000, "world."))); + assertEquals(List.of("hello,", "world."), MiniLyricsFormatter.chunks(line, 4000, 6, false, false) + .stream().map(MiniLyricsFormatter.Chunk::text).toList()); + } + + @Test public void incompleteWordListCannotDiscardMiddleContent() { + LyricsLine line = new LyricsLine(0, 4000, "one missing two", List.of( + new Word(0, 1000, "one"), new Word(2000, 3000, "two"))); + assertEquals(List.of("one missing", "two"), MiniLyricsFormatter.chunks(line, 4000, 5, false, false) + .stream().map(MiniLyricsFormatter.Chunk::text).toList()); + } + + @Test public void formattingIsAfterSplittingButBeforeDecoration() { + List chunks = MiniLyricsFormatter.chunks( + new LyricsLine(0, "hello there."), 5000, 5, true, true); + assertEquals("♪ Hello. ♪", MiniLyricsFormatter.current(chunks, 0, "♪ {lrc}. ♪", 1)); + assertEquals("♪ There. ♪", MiniLyricsFormatter.current(chunks, 4999, "♪ {lrc}. ♪", 1)); + } + + @Test public void punctuationAtANewChunkBoundaryIsAlsoTrailing() { + List chunks = MiniLyricsFormatter.chunks( + new LyricsLine(0, "one two, three four."), 10000, 10, false, true); + assertEquals("one two", chunks.get(0).text()); + assertEquals("three four", chunks.get(1).text()); + } +} diff --git a/extensions/music/src/test/java/app/morphe/extension/music/patches/lyrics/model/MiniLyricsGapTest.java b/extensions/music/src/test/java/app/morphe/extension/music/patches/lyrics/model/MiniLyricsGapTest.java new file mode 100644 index 0000000000..166ed4cd12 --- /dev/null +++ b/extensions/music/src/test/java/app/morphe/extension/music/patches/lyrics/model/MiniLyricsGapTest.java @@ -0,0 +1,135 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/4002 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.extension.music.patches.lyrics.model; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +import java.util.List; + +public class MiniLyricsGapTest { + private static LyricsLine line(long start, long end, String text) { + return new LyricsLine(start, end, text, List.of()); + } + private static MiniLyricsTimeline timeline() { + return new MiniLyricsTimeline(List.of(line(10000,15000,"first lyric"), + line(15000,45000,"♪♪♪"),line(45000,50000,"second lyric")),60000); + } + @Test public void oddWordAndCharacterCountsFavorFirstHalf() { + assertEquals(List.of("one two three","four five"),MiniLyricsFormatter.chunks( + new LyricsLine(0,"one two three four five"),10000,3,false,false) + .stream().map(MiniLyricsFormatter.Chunk::text).toList()); + assertEquals(List.of("你好世界" + MiniLyricsFormatter.SPLIT_ELLIPSIS,"再见你"),MiniLyricsFormatter.chunks( + new LyricsLine(0,"你好世界再见你"),10000,3,false,false) + .stream().map(MiniLyricsFormatter.Chunk::text).toList()); + } + @Test public void thresholdEqualityDoesNotSplit() { + assertEquals(1,MiniLyricsFormatter.chunks(new LyricsLine(0,"one two"),5000,7,false,false).size()); + assertEquals(2,MiniLyricsFormatter.chunks(new LyricsLine(0,"one two"),5000,6,false,false).size()); + } + @Test public void thirtySecondGapAllocatesTenSecondsPerFrame() { + for(long position:new long[]{15000,24999})assertEquals("♪",frame(position)); + for(long position:new long[]{25000,34999})assertEquals("♪♪",frame(position)); + for(long position:new long[]{35000,44999})assertEquals("♪♪♪",frame(position)); + } + private static String frame(long position) { + return MiniLyricsFormatter.instrumental(null,15000,45000,position,"title","artist","last"); + } + @Test public void seekingAndPausingArePositionDriven() { + assertEquals("♪♪♪",frame(40000));assertEquals("♪",frame(17000)); + assertEquals(frame(30000),frame(30000)); + } + @Test public void gapFramesAcceptAllVariablesWithoutRecursiveExpansion() { + assertEquals("T / A: ♪ Last line ♪",MiniLyricsFormatter.instrumental( + "{t} / {a}: {lrc}",0,30000,10000,"T","A","♪ Last line ♪")); + assertEquals("{t} / T",MiniLyricsFormatter.instrumental( + "{lrc} / {t}",0,30000,10000,"T","A","{t}")); + } + @Test public void lastLyricOnlyIsAnExactHoldIncludingDecoration() { + String previous="♪ Last line. ♪"; + assertEquals(previous,MiniLyricsFormatter.instrumental("{lrc}",0,30000,25000,"T","A",previous)); + } + @Test public void emptyFramesAreIntentionalAndPreserved() { + assertEquals("",MiniLyricsFormatter.instrumental("first\n\nlast",0,30000,15000,"T","A","last lyric")); + assertEquals("",MiniLyricsFormatter.instrumental("",0,30000,10000,"T","A","last lyric")); + assertEquals("",MiniLyricsFormatter.instrumental("first\n",0,30000,20000,"T","A","last lyric")); + } + @Test public void noteMarkersNeverBecomeSungLyrics() { + for(String text:List.of(""," ","♪","♪♪","♪♪♪","♫","♬","♩","🎵","🎶","♪ ♫")) + assertTrue(text,MiniLyricsTimeline.isInstrumentalMarker(text)); + assertFalse(MiniLyricsTimeline.isInstrumentalMarker("♪ actual lyric ♪")); + assertNull(timeline().at(20000).vocal()); + assertEquals(15000,timeline().at(20000).gap().startMs()); + assertEquals(45000,timeline().at(20000).gap().endMs()); + } + @Test public void introVocalGapOutroAndBoundariesAreDistinct() { + MiniLyricsTimeline t=timeline(); + assertEquals(10000,t.at(0).gap().endMs()); + assertNull(t.at(0).gap().previous()); + assertEquals("first lyric",t.at(10000).vocal().line().text()); + assertEquals("first lyric",t.at(15000).gap().previous().line().text()); + assertEquals("second lyric",t.at(45000).vocal().line().text()); + assertNull(t.at(50000).gap()); + } + @Test public void explicitVocalEndWorksWithoutAnyNoteMarker() { + MiniLyricsTimeline t=new MiniLyricsTimeline(List.of(line(0,5000,"one"),line(35000,40000,"two")),45000); + assertEquals(5000,t.at(15000).gap().startMs()); + assertEquals(35000,t.at(15000).gap().endMs()); + } + @Test public void shortWordEndsDoNotOverrideThePanelsGapDecision() { + LyricsLine vocal=new LyricsLine(0,10000,"one two",List.of( + new Word(0,1000,"one"),new Word(1000,2000,"two"))); + MiniLyricsTimeline t=new MiniLyricsTimeline(List.of(vocal,line(10000,15000,"next")),20000); + assertNotNull(t.at(1999).vocal());assertNull(t.at(2000).gap()); + } + @Test public void backgroundVocalsDoNotBecomeFalseInstrumentalGaps() { + LyricsLine background=new LyricsLine(1800,4000,"backing",List.of(),null,false,true,null); + MiniLyricsTimeline t=new MiniLyricsTimeline(List.of(line(0,2000,"lead"),background,line(10000,15000,"next")),20000); + assertEquals("lead",t.at(1900).vocal().line().text()); + assertEquals("backing",t.at(3000).vocal().line().text()); + assertEquals(4000,t.at(4000).gap().startMs()); + } + @Test public void anUnknownLineEndDoesNotInventSilence() { + MiniLyricsTimeline t=new MiniLyricsTimeline(List.of(new LyricsLine(0,"held lyric")),0); + assertNotNull(t.at(100000).vocal());assertNull(t.at(100000).gap()); + } + @Test public void unknownGapDurationHoldsItsFirstFrame() { + assertEquals("♪",MiniLyricsFormatter.instrumental(null,1000,Long.MAX_VALUE,100000,"T","A","last")); + } + @Test public void markerOnlyTrackUsesItsExplicitInterval() { + MiniLyricsTimeline t=new MiniLyricsTimeline(List.of(line(0,30000,"♪♪♪")),30000); + assertNull(t.at(10000).vocal());assertEquals(30000,t.at(10000).gap().endMs()); + } + @Test public void varyingFrameCountsUseTheWholeGapOnce() { + assertEquals("two",MiniLyricsFormatter.instrumental("one\ntwo",0,10000,5000,"T","A","last")); + assertEquals("last",MiniLyricsFormatter.instrumental("one\nmid\nlast",0,10000,10000,"T","A","previous")); + } + @Test public void shortPausesBetweenLinesNeverShowInstrumentalText() { + MiniLyricsTimeline t=new MiniLyricsTimeline(List.of(line(0,5000,"one"),line(6500,9000,"two"),line(10000,12000,"three")),20000); + assertNull(t.at(5200).gap());assertNull(t.at(9500).gap()); + assertEquals("one",t.at(5200).previous().line().text()); + assertEquals("two",t.at(9500).previous().line().text()); + } + @Test public void nothingIsPreviousBeforeTheFirstLine() { + MiniLyricsTimeline t=new MiniLyricsTimeline(List.of(line(2000,5000,"one")),20000); + assertNull(t.at(1000).vocal());assertNull(t.at(1000).gap());assertNull(t.at(1000).previous()); + } + @Test public void panelGapClassifierIsAuthoritative() { + var words = List.of(new Word(0,1000,"one"),new Word(1000,2000,"two")); + var lines = List.of(new LyricsLine(0,10000,"one two",words),line(15000,18000,"next")); + var t = new MiniLyricsTimeline(lines,20000); + assertNull("The panel doesn't show dots at 5 seconds", t.at(5000).gap()); + assertNotNull(t.at(11000).gap()); + assertEquals(10000,t.at(11000).gap().startMs()); + } +} diff --git a/extensions/music/src/test/java/app/morphe/extension/music/patches/lyrics/model/PairedPunctuationTest.java b/extensions/music/src/test/java/app/morphe/extension/music/patches/lyrics/model/PairedPunctuationTest.java new file mode 100644 index 0000000000..390ad120bf --- /dev/null +++ b/extensions/music/src/test/java/app/morphe/extension/music/patches/lyrics/model/PairedPunctuationTest.java @@ -0,0 +1,78 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/4002 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.extension.music.patches.lyrics.model; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +import java.util.List; + +public class PairedPunctuationTest { + private static List split(String input) { + return MiniLyricsFormatter.chunks(new LyricsLine(0, input), 8000, 1, false, false).stream().map(MiniLyricsFormatter.Chunk::text).toList(); + } + + @Test + public void userExamples() { + assertEquals(List.of("(Ooh yeah)", "(Ooh yeah)"), split("(Ooh yeah Ooh yeah)")); + assertEquals(List.of("\"Well, let me tell you\"", "\"She had pretty eyes\""), split("\"Well, let me tell you She had pretty eyes\"")); + } + + @Test + public void everyUnicodeAndCldrPair() { + for (var p : PairedPunctuation.PAIRS) + assertEquals(p.toString(), List.of(p.opening() + "one two" + p.closing(), p.opening() + "three four" + p.closing()), split(p.opening() + "one two three four" + p.closing())); + } + + @Test + public void nestedShellsAndDifferentScripts() { + assertEquals(List.of("(「你好…」)", "(「世界」)"), split("(「你好世界」)")); + assertEquals(List.of("«مرحبا بك»", "«يا صديقي»"), split("«مرحبا بك يا صديقي»")); + assertEquals(List.of("„eins zwei“", "„drei vier“"), split("„eins zwei drei vier“")); + } + + @Test + public void independentlyBalancedPhrasesAreNotWrapped() { + assertEquals(List.of("(one two)", "(three four)"), split("(one two) (three four)")); + assertEquals(List.of("\"one two\"", "\"three four\""), split("\"one two\" \"three four\"")); + } + + @Test + public void innerApostrophesDoNotEndOuterQuotes() { + assertEquals(List.of("'I'm going'", "'you're coming'"), split("'I'm going you're coming'")); + } + + @Test + public void noSplitDoesNotRewriteAnything() { + assertEquals("(hello)", MiniLyricsFormatter.chunks(new LyricsLine(0, "(hello)"), 1000, 100, false, false).get(0).text()); + } + + @Test + public void wordTimesAndSourceArePreserved() { + var line = new LyricsLine(0, 8000, "(one two three four)", List.of(new Word(0, 1000, "(one"), new Word(1000, 2000, "two"), new Word(4000, 5000, "three"), new Word(5000, 6000, "four)"))); + var chunks = MiniLyricsFormatter.chunks(line, 8000, 1, false, false); + assertEquals(4000, chunks.get(1).startMs()); + assertEquals("(one two)", chunks.get(0).text()); + assertEquals("(one two three four)", line.text()); + } + + @Test + public void repeatCountIsInTheNewDefault() { + assertEquals("{lrc} :(x{n}):", MiniLyricsFormatter.DEFAULT_ENCAPSULATION); + assertEquals("line ", MiniLyricsFormatter.encapsulate(null, "line", 1)); + assertEquals("line (x2)", MiniLyricsFormatter.encapsulate(null, "line", 2)); + } + + @Test + public void negativeOffsetLooksAheadAndPositiveDelays() { + assertEquals(5600, MiniLyricsFormatter.selectionPosition(5000, -600)); + assertEquals(4400, MiniLyricsFormatter.selectionPosition(5000, 600)); + assertEquals(0, MiniLyricsFormatter.selectionPosition(100, 600)); + } +} diff --git a/extensions/music/src/test/java/app/morphe/extension/music/patches/lyrics/model/TrackFormatTest.java b/extensions/music/src/test/java/app/morphe/extension/music/patches/lyrics/model/TrackFormatTest.java new file mode 100644 index 0000000000..3b61ade3da --- /dev/null +++ b/extensions/music/src/test/java/app/morphe/extension/music/patches/lyrics/model/TrackFormatTest.java @@ -0,0 +1,114 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/4002 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.extension.music.patches.lyrics.model; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +import java.util.List; + +public class TrackFormatTest { + private static final String ELLIPSIS = MiniLyricsFormatter.SPLIT_ELLIPSIS; + + @Test public void variantShownOnlyWhenTheBackendHasOne() { + assertEquals("Duvet (Acoustic) - bôa", MiniLyricsFormatter.trackLabel(null, "Duvet", "bôa", "Acoustic")); + assertEquals("Duvet - bôa", MiniLyricsFormatter.trackLabel(null, "Duvet", "bôa", null)); + assertEquals("Duvet - bôa", MiniLyricsFormatter.trackLabel(null, "Duvet", "bôa", " ")); + } + + @Test public void parenthesesInATitleAreNotAVariant() { + assertEquals("Rockstar (Olympia) - Artist", + MiniLyricsFormatter.trackLabel(null, "Rockstar (Olympia)", "Artist", null)); + } + + @Test public void sectionNeedsEveryVariableInside() { + final String template = "{t}: [{v} by {a}]:"; + assertEquals("Duvet [Live by bôa]", MiniLyricsFormatter.trackLabel(template, "Duvet", "bôa", "Live")); + assertEquals("Duvet", MiniLyricsFormatter.trackLabel(template, "Duvet", "bôa", null)); + assertEquals("Duvet", MiniLyricsFormatter.trackLabel(template, "Duvet", "", "Live")); + } + + @Test public void escapedColonIsLiteral() { + assertEquals("Duvet: bôa", MiniLyricsFormatter.trackLabel("{t}\\: {a}", "Duvet", "bôa", null)); + } + + @Test public void placeholderInsideATitleIsKept() { + assertEquals("{a} - X", MiniLyricsFormatter.trackLabel("{t} - {a}", "{a}", "X", null)); + } + + @Test public void eachTemplateLineIsAFrame() { + assertEquals(List.of("Duvet (Acoustic)", "bôa"), + MiniLyricsFormatter.trackFrames("{t} :({v}):\n{a}", "Duvet", "bôa", "Acoustic", 0, 1)); + // A line left empty by a hidden section is not a frame. + assertEquals(List.of("Duvet", "bôa"), + MiniLyricsFormatter.trackFrames("{t}\n:{v}:\n{a}", "Duvet", "bôa", null, 0, 1)); + } + + @Test public void longTrackLineIsSplitIntoFramesThatFit() { + final List frames = MiniLyricsFormatter.trackFrames("{t} - {a}", + "Smells Like Teen Spirit", "Nirvana", null, 10, 1); + assertTrue(frames.size() > 2); + for (String frame : frames) { + assertTrue(frame, MiniLyricsFormatter.width(frame, 1) <= 10); + } + assertEquals("Smells Like Teen Spirit - Nirvana", String.join(" ", frames)); + } + + @Test public void splitIsDecidedFromTheWholeLine() { + // Neither variable is over the limit, but the rendered line is. + final List frames = MiniLyricsFormatter.trackFrames("{t} - {a}", "Duvet", "bôa", null, 8, 1); + assertEquals(List.of("Duvet -", "bôa"), frames); + } + + @Test public void fullWidthCharactersCountAsTheMultiplier() { + final String six = "あいうえおか"; + assertEquals(6.0, MiniLyricsFormatter.width(six, 1), 0.001); + assertEquals(10.8, MiniLyricsFormatter.width(six, 1.8), 0.001); + assertEquals(1, MiniLyricsFormatter.chunks(new LyricsLine(0, six), 5000, 10, false, false, 1).size()); + assertEquals(2, MiniLyricsFormatter.chunks(new LyricsLine(0, six), 5000, 10, false, false, 1.8).size()); + assertEquals(4.0, MiniLyricsFormatter.width("abcd", 1.8), 0.001); + } + + @Test public void unspacedSplitGetsOneEllipsis() { + final String text = "あいうえおかきくけこ"; + final List chunks = + MiniLyricsFormatter.chunks(new LyricsLine(0, text), 5000, 5, false, false, 1); + assertEquals(2, chunks.size()); + assertTrue(chunks.get(0).text().endsWith(ELLIPSIS)); + assertFalse(chunks.get(1).text().contains(ELLIPSIS)); + assertEquals(text, (chunks.get(0).text() + chunks.get(1).text()).replace(ELLIPSIS, "")); + } + + @Test public void spacedSplitHasNoEllipsis() { + final List chunks = MiniLyricsFormatter.chunks( + new LyricsLine(0, "don't stop me now"), 5000, 10, false, false, 1); + assertEquals("don't stop", chunks.get(0).text()); + assertEquals("me now", chunks.get(1).text()); + } + + @Test public void hyphenatedAndPunctuatedWordsStayWhole() { + final List chunks = MiniLyricsFormatter.chunks( + new LyricsLine(0, "well-known words, again!"), 5000, 10, false, false, 1); + assertEquals("well-known words,", chunks.get(0).text()); + assertEquals("again!", chunks.get(1).text()); + } + + @Test public void wordTimestampsDecideTheSplit() { + final String text = "今日はいい天気"; + final LyricsLine line = new LyricsLine(0, text, List.of( + new Word(0, 500, "今日は"), new Word(500, 1000, "いい"), new Word(1000, 1500, "天気"))); + final List chunks = MiniLyricsFormatter.chunks(line, 2000, 3, false, false, 1); + assertEquals(2, chunks.size()); + assertEquals("今日はいい" + ELLIPSIS, chunks.get(0).text()); + assertEquals("天気", chunks.get(1).text()); + assertEquals(1000, chunks.get(1).startMs()); + } +} diff --git a/extensions/music/src/test/java/app/morphe/extension/music/patches/lyrics/model/TrackVersionTest.java b/extensions/music/src/test/java/app/morphe/extension/music/patches/lyrics/model/TrackVersionTest.java new file mode 100644 index 0000000000..c99168e606 --- /dev/null +++ b/extensions/music/src/test/java/app/morphe/extension/music/patches/lyrics/model/TrackVersionTest.java @@ -0,0 +1,65 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/4002 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.extension.music.patches.lyrics.model; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +public class TrackVersionTest { + @Test public void unpairedPunctuationAtTheEdgesIsStripped() { + assertEquals("Acoustic (Deluxe)", TrackVersion.clean("Acoustic (Deluxe),")); + assertEquals("Remix", TrackVersion.clean("- Remix")); + assertEquals("2026 Remaster", TrackVersion.clean("\"2026 Remaster\"")); + assertEquals("Acoustic", TrackVersion.clean("(Acoustic)")); + assertEquals("Acoustic", TrackVersion.clean("\u201CAcoustic\u201D")); + } + + @Test public void pairedPunctuationInsideTheVersionIsKept() { + assertEquals("Acoustic (Deluxe)", TrackVersion.clean("Acoustic (Deluxe)")); + assertEquals("\"Acoustic\" Version", TrackVersion.clean("\"Acoustic\" Version")); + } + + @Test public void unicodePairsUseTheSharedDatabase() { + assertEquals("🎵 Live", TrackVersion.clean("🎵 Live")); + assertEquals("ライブ", TrackVersion.clean("「ライブ」")); + assertEquals("版本(现场)", TrackVersion.clean("版本(现场),")); + assertEquals("「ライブ」 版本", TrackVersion.clean("「ライブ」 版本")); + } + + @Test public void variantQueryPreservesBackendIdentity() { + TrackInfo cleaned = new TrackInfo("Duvet (Acoustic)", "boa", "The Race", 220); + TrackInfo query = TrackVersion.query(cleaned, "Duvet (Acoustic)", "bôa"); + assertEquals("bôa", query.artist()); + assertEquals("Duvet (Acoustic)", query.title()); + assertEquals(cleaned.durationSeconds(), query.durationSeconds()); + assertEquals(cleaned.album(), query.album()); + } + + @Test public void missingRawMetadataFallsBackToTheLyricQuery() { + TrackInfo cleaned = new TrackInfo("Rockstar (Olympia)", "Hole", "", 180); + assertEquals(cleaned, TrackVersion.query(cleaned, null, "")); + } + + @Test public void titlesMatchIgnoringCaseAndSpace() { + assertEquals(TrackVersion.normalize("Duvet"), TrackVersion.normalize(" duvet ")); + } + + @Test public void combinedTitlesUseTheBackendFieldsWithoutInferringParentheses() { + assertTrue(TrackVersion.matchesStructuredTitle("Duvet (Acoustic)", "Duvet - Acoustic", + "Duvet", "(Acoustic)")); + assertFalse(TrackVersion.matchesStructuredTitle("Duvet", "Duvet (Acoustic)", + "Duvet", "(Acoustic)")); + assertFalse(TrackVersion.matchesStructuredTitle("Rockstar (Olympia)", "Rockstar", + "Rockstar", "")); + assertTrue(TrackVersion.matchesStructuredTitle("Rockstar (Olympia)", "Rockstar (Olympia)", + "Rockstar (Olympia)", "")); + } +} diff --git a/extensions/music/src/test/java/app/morphe/extension/music/patches/lyrics/requests/LRCLIBSyncedFirstTest.java b/extensions/music/src/test/java/app/morphe/extension/music/patches/lyrics/requests/LRCLIBSyncedFirstTest.java new file mode 100644 index 0000000000..048abc2a8d --- /dev/null +++ b/extensions/music/src/test/java/app/morphe/extension/music/patches/lyrics/requests/LRCLIBSyncedFirstTest.java @@ -0,0 +1,39 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/4002 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.extension.music.patches.lyrics.requests; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertFalse; + +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; + +import app.morphe.extension.music.patches.lyrics.model.Lyrics; +import app.morphe.extension.music.patches.lyrics.model.LyricsLine; + +public class LRCLIBSyncedFirstTest { + private static Lyrics lyrics(boolean synced) { + return new Lyrics(List.of(new LyricsLine(synced ? 1000 : -1, "line")), "LRCLIB", synced, + null, null, null, null, null, "lrc", null); + } + + @Test public void syncedBeatsAHigherScoredPlainResult() { + List scored = new ArrayList<>(); + scored.add(new Lyrics.ScoredLyrics(100, lyrics(false))); + scored.add(new Lyrics.ScoredLyrics(40, lyrics(true))); + scored.add(new Lyrics.ScoredLyrics(60, lyrics(true))); + List ordered = LRCLIBProvider.syncedFirst(scored); + assertTrue(ordered.get(0).lyrics().synced()); + assertEquals(60, ordered.get(0).score()); + assertEquals(40, ordered.get(1).score()); + assertFalse(ordered.get(2).lyrics().synced()); + } +} diff --git a/patches/src/main/kotlin/app/morphe/patches/music/layout/lyrics/LyricsPatch.kt b/patches/src/main/kotlin/app/morphe/patches/music/layout/lyrics/LyricsPatch.kt index 99d5b82c79..3a2ff28e78 100644 --- a/patches/src/main/kotlin/app/morphe/patches/music/layout/lyrics/LyricsPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/music/layout/lyrics/LyricsPatch.kt @@ -181,7 +181,35 @@ val lyricsPatch = bytecodePatch( SwitchPreference("morphe_music_lyrics_sb_matching", summary = true), SwitchPreference("morphe_music_lyrics_miniplayer", summary = true), SwitchPreference("morphe_music_lyrics_mediasession", summary = true), - SwitchPreference("morphe_music_lyrics_display_artist_first", summary = true) + NonInteractivePreference( + key = "morphe_music_lyrics_mini_track_format", + summaryKey = "morphe_music_lyrics_mini_track_format_menu_summary", + tag = "app.morphe.extension.music.settings.preference.LyricsTrackFormatPreference", + selectable = true, + dependency = "morphe_music_lyrics_enabled" + ), + TextPreference( + key = "morphe_music_lyrics_mini_encapsulation", + tag = "app.morphe.extension.music.settings.preference.LyricsEncapsulationPreference" + ), + NonInteractivePreference( + key = "morphe_music_lyrics_mini_line_limits", + summaryKey = "morphe_music_lyrics_mini_line_limits_summary", + tag = "app.morphe.extension.music.settings.preference.LyricsLineLimitsPreference", + selectable = true, + dependency = "morphe_music_lyrics_enabled" + ), + TextPreference( + key = "morphe_music_lyrics_mini_instrumental", + inputType = InputType.TEXT_MULTI_LINE + ), + NonInteractivePreference( + key = "morphe_music_lyrics_mini_next_offset_ms", + summaryKey = "morphe_music_lyrics_mini_next_offset_ms_summary", + tag = "app.morphe.extension.shared.settings.preference.SeekBarPreference", + selectable = true, + dependency = "morphe_music_lyrics_enabled" + ) ) ), PreferenceCategory( @@ -225,7 +253,9 @@ val lyricsPatch = bytecodePatch( TextPreference( key = "morphe_music_lyrics_credit_line_regex", inputType = InputType.TEXT_MULTI_LINE - ) + ), + SwitchPreference("morphe_music_lyrics_force_capitalize", summary = true), + SwitchPreference("morphe_music_lyrics_strip_trailing_punctuation", summary = true) ) ), screenInfoPreferenceCategory( diff --git a/patches/src/main/resources/addresources/values/music/strings.xml b/patches/src/main/resources/addresources/values/music/strings.xml index 699fc1f3e1..be02baccf7 100644 --- a/patches/src/main/resources/addresources/values/music/strings.xml +++ b/patches/src/main/resources/addresources/values/music/strings.xml @@ -605,8 +605,29 @@ Required cookies: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPISID Shows the current line on the lock screen, Android Auto and Bluetooth. Needs Android 11 or newer Show Miniplayer Lyrics Shows the current line in the miniplayer, with the track moved to the subtitle - Artist and Title display order - Toggle the display order of Artist and Title in the Miniplayer and MediaSession. ON \"Artist - Title\" and OFF \"Title - Artist\" Does not affect lyrics search + Song title/artist format + Format of the track shown with the lyrics in the Miniplayer and MediaSession. {t} is the title, {a} is the artist and {v} is the version, such as Acoustic, when the lyrics source provides one. Text between two colons is only shown when every variable inside it has a value; write \\: for a plain colon. Each line is shown in turn, moving on whenever the lyric changes + Lyric line format + Format of the lyrics line in the Miniplayer and MediaSession. {lrc} is the line and {n} is how many times in a row it is sung. Text between colons is only shown when the line repeats. Use \\: for a colon + The format must contain {lrc} + Artist-line template and animation timing + Minimum screen time (ms) + Maximum screen time (ms) + For multiline animations only. Keep each frame for at least the minimum time. At the maximum, advance the artist line even when the lyric stays the same. + Minimum must be 0 or greater. Maximum must be greater than 0 and at least the minimum. + Line length limits + Lyric and artist line limits, and full-width character weight + "Lyric line limit +Split longer lyrics into balanced halves. 0 disables splitting." + "Artist line limit +Split the populated track format into cycling frames. 0 disables splitting." + "Full-width character weight +Applies to both limits. At 1.75, four full-width characters count as 7." + Enter whole-number limits from 0 to 10000 and a character weight greater than 0 + Instrumental break + Shown in the Miniplayer and MediaSession during an instrumental break. Each line is one animation frame. {t} is the title, {a} is the artist and {lrc} is the last lyrics line + Miniplayer and MediaSession timing offset + Shifts when the next line is shown in the Miniplayer and MediaSession. Use a negative value if lines show up late, for example on the lock screen Filters Metadata regex Regular expression applied to titles, artists, and albums before looking up lyrics and when scoring whether two titles match. Leave empty to disable both. The default removes common clutter like \"Official Video\", \"Remaster\", \"【插曲】\", and \"- Topic\" @@ -614,6 +635,10 @@ Required cookies: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPISID Regular expression applied to the original lyrics text only (not translations or romanizations). Leave empty to disable Info line markers If lyrics are incorrectly filtered to the bottom, adjust or clear this. Comma-separated markers: lines containing a marker followed by a colon are shown as credit info at the bottom. Leave empty to disable + Capitalize first letter + Capitalizes the first letter of each lyrics line + Remove trailing punctuation + Removes full stops, commas, colons and semicolons at the end of lyrics lines. Question marks, exclamation marks, ellipses, quotes and brackets are kept Text size Size of the lyrics text Global timing offset From 6d81d580b8fc16af688fc619f577e6b6f5263913 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Mon, 26 Oct 2026 17:09:45 +0000 Subject: [PATCH 089/189] refactor: Prompt to restart after settings is closed User request to do this was made in the prior project years ago (better late than never) --- .../music/settings/MusicActivityHook.java | 6 ++- .../reddit/settings/RedditActivityHook.java | 48 +++++++++++++++-- .../preference/CustomFontFilePreference.java | 2 +- .../CustomFontTogglePreference.java | 4 +- .../preference/ForceSystemFontPreference.java | 2 +- .../SettingsMenuFilterPickerPreference.java | 2 +- .../shared/settings/BaseActivityHook.java | 12 ++++- .../preference/CustomFontFilePreference.java | 2 +- .../FeatureFlagsManagerPreference.java | 6 +-- .../search/BaseSearchViewController.java | 3 +- .../AbstractPreferenceFragment.java | 51 ++++++++++++++++--- .../youtube/settings/YouTubeActivityHook.java | 7 +-- .../GetCaptionCookiesPreference.java | 2 +- .../reddit/misc/settings/SettingsPatch.kt | 29 +++++++++++ .../youtube/misc/settings/SettingsPatch.kt | 8 +-- 15 files changed, 152 insertions(+), 32 deletions(-) diff --git a/extensions/music/src/main/java/app/morphe/extension/music/settings/MusicActivityHook.java b/extensions/music/src/main/java/app/morphe/extension/music/settings/MusicActivityHook.java index df070b7db8..3d2930973f 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/settings/MusicActivityHook.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/settings/MusicActivityHook.java @@ -31,6 +31,7 @@ import app.morphe.extension.shared.ResourceUtils; import app.morphe.extension.shared.Utils; import app.morphe.extension.shared.settings.BaseActivityHook; +import app.morphe.extension.shared.settings.preference.AbstractPreferenceFragment; import app.morphe.extension.shared.spoof.SpoofAppVersionPatch; import app.morphe.extension.shared.theme.ThemeUtils; @@ -171,8 +172,9 @@ protected PreferenceFragment createPreferenceFragment() { * @return if the original activity finish method should be allowed to run. */ @SuppressWarnings("unused") - public static boolean handleFinish() { - return MusicSearchViewController.handleFinish(searchViewController); + public static boolean handleFinish(Activity activity) { + return MusicSearchViewController.handleFinish(searchViewController) + || AbstractPreferenceFragment.showRestartDialogIfNeeded(activity, activity::finish); } /** diff --git a/extensions/reddit/src/main/java/app/morphe/extension/reddit/settings/RedditActivityHook.java b/extensions/reddit/src/main/java/app/morphe/extension/reddit/settings/RedditActivityHook.java index 238de55866..ecfcf44c69 100644 --- a/extensions/reddit/src/main/java/app/morphe/extension/reddit/settings/RedditActivityHook.java +++ b/extensions/reddit/src/main/java/app/morphe/extension/reddit/settings/RedditActivityHook.java @@ -34,6 +34,7 @@ import app.morphe.extension.shared.Logger; import app.morphe.extension.shared.ResourceUtils; import app.morphe.extension.shared.Utils; +import app.morphe.extension.shared.settings.preference.AbstractPreferenceFragment; import app.morphe.extension.shared.theme.ThemeUtils; import app.morphe.extension.shared.ui.Dim; import kotlin.jvm.functions.Function0; @@ -42,6 +43,7 @@ public class RedditActivityHook { private static final Drawable MORPHE_ICON = MorpheSettingsIconVectorDrawable.getIcon(); private static final String MORPHE_LABEL = "Morphe"; + private static final String EXTRA_INITIAL_URL = "com.reddit.extra.initial_url"; private static final String PRIVACY_POLICY_URI = Objects.requireNonNull( ResourceUtils.getString("privacy_policy_uri")); @@ -81,8 +83,7 @@ public Object invoke() { * Injection point. */ public static boolean hook(Activity activity) { - Intent intent = activity.getIntent(); - if (MORPHE_LABEL.equals(intent.getStringExtra("com.reddit.extra.initial_url"))) { + if (isMorpheSettingsActivity(activity)) { initialize(activity); return true; } @@ -90,13 +91,33 @@ public static boolean hook(Activity activity) { return false; } + private static boolean isMorpheSettingsActivity(Activity activity) { + return MORPHE_LABEL.equals(activity.getIntent().getStringExtra(EXTRA_INITIAL_URL)); + } + + /** + * Injection point. + * 2026.29.0 and older. + *

+ * Overrides {@link Activity#finish()} of the settings activity. + * + * @return If the restart dialog is shown, and the original finish method must not run yet. + */ + public static boolean handleFinish(Activity activity) { + if (!isMorpheSettingsActivity(activity)) { + return false; + } + + return AbstractPreferenceFragment.showRestartDialogIfNeeded(activity, activity::finish); + } + /** * Injection point. */ public static Intent initializeByIntent(Context context) { Intent intent = new Intent(); intent.setClassName(context, "com.reddit.webembed.browser.WebBrowserActivity"); - intent.putExtra("com.reddit.extra.initial_url", MORPHE_LABEL); + intent.putExtra(EXTRA_INITIAL_URL, MORPHE_LABEL); return intent; } @@ -150,6 +171,25 @@ public void onCreate(Bundle savedInstanceState) { setStyle(STYLE_NO_TITLE, android.R.style.Theme_DeviceDefault_Light_NoActionBar); } + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + return new Dialog(getActivity(), getTheme()) { + @Override + public void onBackPressed() { + closeSettings(); + } + }; + } + + /** + * Closes the settings, after first showing the restart dialog if a changed setting requires it. + */ + private void closeSettings() { + if (!AbstractPreferenceFragment.showRestartDialogIfNeeded(getActivity(), this::dismiss)) { + dismiss(); + } + } + @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { Activity activity = getActivity(); @@ -199,7 +239,7 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle sa Drawable backIcon = Objects.requireNonNull(ResourceUtils.getDrawable("icon_arrow_back")); backIcon.setTint(appForegroundColor); toolbar.setNavigationIcon(backIcon); - toolbar.setNavigationOnClickListener(v -> dismiss()); + toolbar.setNavigationOnClickListener(v -> closeSettings()); linearLayout.addView(toolbar); final int fragmentId = View.generateViewId(); diff --git a/extensions/reddit/src/main/java/app/morphe/extension/reddit/settings/preference/CustomFontFilePreference.java b/extensions/reddit/src/main/java/app/morphe/extension/reddit/settings/preference/CustomFontFilePreference.java index a09c2a4c9e..925238bcee 100644 --- a/extensions/reddit/src/main/java/app/morphe/extension/reddit/settings/preference/CustomFontFilePreference.java +++ b/extensions/reddit/src/main/java/app/morphe/extension/reddit/settings/preference/CustomFontFilePreference.java @@ -106,7 +106,7 @@ public static void handleActivityResult(Context context, int resultCode, Intent // Restart is only needed if custom font rendering is currently active. if (Settings.CUSTOM_FONT.get()) { - AbstractPreferenceFragment.showRestartDialog(context); + AbstractPreferenceFragment.setRestartNeeded(); } } diff --git a/extensions/reddit/src/main/java/app/morphe/extension/reddit/settings/preference/CustomFontTogglePreference.java b/extensions/reddit/src/main/java/app/morphe/extension/reddit/settings/preference/CustomFontTogglePreference.java index c59d414cd7..3097761bd8 100644 --- a/extensions/reddit/src/main/java/app/morphe/extension/reddit/settings/preference/CustomFontTogglePreference.java +++ b/extensions/reddit/src/main/java/app/morphe/extension/reddit/settings/preference/CustomFontTogglePreference.java @@ -25,7 +25,7 @@ public CustomFontTogglePreference(Context context) { setOnPreferenceChangeListener((preference, newValue) -> { boolean enabling = Boolean.TRUE.equals(newValue); if (!enabling) { - AbstractPreferenceFragment.showRestartDialog(context); + AbstractPreferenceFragment.setRestartNeeded(); return true; } @@ -50,7 +50,7 @@ public CustomFontTogglePreference(Context context) { } } - AbstractPreferenceFragment.showRestartDialog(context); + AbstractPreferenceFragment.setRestartNeeded(); return true; }); } diff --git a/extensions/reddit/src/main/java/app/morphe/extension/reddit/settings/preference/ForceSystemFontPreference.java b/extensions/reddit/src/main/java/app/morphe/extension/reddit/settings/preference/ForceSystemFontPreference.java index 41589a2f80..2bbede03e9 100644 --- a/extensions/reddit/src/main/java/app/morphe/extension/reddit/settings/preference/ForceSystemFontPreference.java +++ b/extensions/reddit/src/main/java/app/morphe/extension/reddit/settings/preference/ForceSystemFontPreference.java @@ -29,7 +29,7 @@ public ForceSystemFontPreference(Context context) { } } - AbstractPreferenceFragment.showRestartDialog(context); + AbstractPreferenceFragment.setRestartNeeded(); return true; }); } diff --git a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/patches/SettingsMenuFilterPickerPreference.java b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/patches/SettingsMenuFilterPickerPreference.java index 58072f8417..00141c031a 100644 --- a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/patches/SettingsMenuFilterPickerPreference.java +++ b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/patches/SettingsMenuFilterPickerPreference.java @@ -133,7 +133,7 @@ private void showPickerDialog() { null, null, () -> { - if (changesMade[0]) AbstractPreferenceFragment.showRestartDialog(context); + if (changesMade[0]) AbstractPreferenceFragment.setRestartNeeded(); }, null, str("morphe_settings_reset"), diff --git a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/settings/BaseActivityHook.java b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/settings/BaseActivityHook.java index a3d6ca162c..902a51d761 100644 --- a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/settings/BaseActivityHook.java +++ b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/settings/BaseActivityHook.java @@ -27,6 +27,7 @@ import app.morphe.extension.shared.ResourceUtils; import app.morphe.extension.shared.Utils; import app.morphe.extension.shared.patches.SettingsNamePatch; +import app.morphe.extension.shared.settings.preference.AbstractPreferenceFragment; import app.morphe.extension.shared.settings.preference.ToolbarPreferenceFragment; import app.morphe.extension.shared.theme.ThemeUtils; import app.morphe.extension.shared.ui.Dim; @@ -87,7 +88,7 @@ public static void initialize(BaseActivityHook hook, Activity activity) { && !ENABLE_PREDICTIVE_BACK_ANIMATION) { activity.getOnBackInvokedDispatcher().registerOnBackInvokedCallback( android.window.OnBackInvokedDispatcher.PRIORITY_DEFAULT, - activity::finish + () -> closeSettings(activity) ); } @@ -103,6 +104,15 @@ public static void initialize(BaseActivityHook hook, Activity activity) { } } + /** + * Closes the settings, after first showing the restart dialog if a changed setting requires it. + */ + public static void closeSettings(Activity activity) { + if (!AbstractPreferenceFragment.showRestartDialogIfNeeded(activity, activity::finish)) { + activity.finish(); + } + } + /** * Injection point. * Overrides the Morphe settings language. diff --git a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/settings/preference/CustomFontFilePreference.java b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/settings/preference/CustomFontFilePreference.java index d8cf28310a..61c6b6f2aa 100644 --- a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/settings/preference/CustomFontFilePreference.java +++ b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/settings/preference/CustomFontFilePreference.java @@ -127,7 +127,7 @@ public static void handleActivityResult(Context context, int resultCode, @Nullab preference.updateSummary(); } - AbstractPreferenceFragment.showRestartDialog(context); + AbstractPreferenceFragment.setRestartNeeded(); } catch (Exception ex) { Logger.printException(() -> "handleActivityResult failure", ex); } diff --git a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/settings/preference/FeatureFlagsManagerPreference.java b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/settings/preference/FeatureFlagsManagerPreference.java index 2151d576c0..549d89a553 100644 --- a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/settings/preference/FeatureFlagsManagerPreference.java +++ b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/settings/preference/FeatureFlagsManagerPreference.java @@ -942,13 +942,13 @@ private void persistFlagStates() { } /** - * Saves the flags and asks to restart. + * Saves the flags. The restart dialog is shown when the settings are closed. */ private void saveFlags() { persistFlagStates(); Utils.showToastShort(str("morphe_debug_feature_flags_manager_toast_saved")); - AbstractPreferenceFragment.showRestartDialog(getContext()); + AbstractPreferenceFragment.setRestartNeeded(); } /** @@ -959,7 +959,7 @@ private void resetFlags() { SharedYouTubeSettings.FORCED_FEATURE_FLAGS.resetToDefault(); Utils.showToastShort(str("morphe_debug_feature_flags_manager_toast_reset")); - AbstractPreferenceFragment.showRestartDialog(getContext()); + AbstractPreferenceFragment.setRestartNeeded(); } /** diff --git a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/settings/search/BaseSearchViewController.java b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/settings/search/BaseSearchViewController.java index 838ed061d1..d194c4376a 100644 --- a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/settings/search/BaseSearchViewController.java +++ b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/settings/search/BaseSearchViewController.java @@ -57,6 +57,7 @@ import app.morphe.extension.shared.ResourceUtils; import app.morphe.extension.shared.Utils; import app.morphe.extension.shared.settings.AppLanguage; +import app.morphe.extension.shared.settings.BaseActivityHook; import app.morphe.extension.shared.settings.BaseSettings; import app.morphe.extension.shared.settings.Setting; import app.morphe.extension.shared.settings.preference.NoTitlePreferenceCategory; @@ -323,7 +324,7 @@ public boolean onQueryTextChange(String newText) { if (isSearchActive) { closeSearch(); } else { - activity.finish(); + BaseActivityHook.closeSettings(activity); } }); } diff --git a/extensions/shared/library/src/main/java/app/morphe/extension/shared/settings/preference/AbstractPreferenceFragment.java b/extensions/shared/library/src/main/java/app/morphe/extension/shared/settings/preference/AbstractPreferenceFragment.java index ed74b04f69..662b854458 100644 --- a/extensions/shared/library/src/main/java/app/morphe/extension/shared/settings/preference/AbstractPreferenceFragment.java +++ b/extensions/shared/library/src/main/java/app/morphe/extension/shared/settings/preference/AbstractPreferenceFragment.java @@ -266,6 +266,12 @@ private static boolean searchPreferencePath(PreferenceGroup group, Preference ta */ private static boolean showingUserDialogMessage; + /** + * If a changed setting requires an app restart. The restart dialog is shown when the + * settings are closed, so multiple settings can be changed with only one restart. + */ + private static boolean restartNeeded; + /** * Confirm and restart dialog button text and title. * Set by subclasses if Strings cannot be added as a resource. @@ -313,7 +319,7 @@ private static boolean searchPreferencePath(PreferenceGroup group, Preference ta showSettingUserDialogConfirmation(pref, setting); return; } else if (setting.rebootApp) { - showRestartDialog(getContext()); + setRestartNeeded(); } } @@ -382,7 +388,7 @@ private void showSettingUserDialogConfirmation(Preference pref, Setting setti updateUIAvailability(); if (setting.rebootApp) { - showRestartDialog(context); + setRestartNeeded(); } }, () -> { @@ -537,7 +543,33 @@ protected void updateListPreferenceSummary(ListPreference listPreference, Settin } } - public static void showRestartDialog(Context context) { + /** + * Marks that a changed setting requires an app restart. + * The restart dialog is shown when the settings are closed. + */ + public static void setRestartNeeded() { + restartNeeded = true; + } + + /** + * Shows the restart dialog if a changed setting requires an app restart. + * Must be called when the user closes the settings. + * + * @param closeSettings Closes the settings, if the user does not restart. + * @return If the dialog is shown, and the settings must not close yet. + */ + public static boolean showRestartDialogIfNeeded(Context context, Runnable closeSettings) { + if (!restartNeeded) { + return false; + } + restartNeeded = false; + + Dialog dialog = showRestartDialog(context); + dialog.setOnDismissListener(d -> closeSettings.run()); + return true; + } + + public static Dialog showRestartDialog(Context context) { Utils.verifyOnMainThread(); if (restartDialogTitle == null) { restartDialogTitle = str("morphe_settings_restart_title"); @@ -564,6 +596,7 @@ public static void showRestartDialog(Context context) { // Show the dialog. dialogPair.first.show(); + return dialogPair.first; } /** @@ -593,7 +626,7 @@ public void showImportExportTextDialog() { // OK button text. str("morphe_settings_save"), // OK button action. - () -> importSettingsText(context, currentImportExportEditText.getText().toString()), + () -> importSettingsText(currentImportExportEditText.getText().toString()), // Cancel button action (dismiss only). () -> {}, // Neutral button (Copy) text. @@ -690,7 +723,7 @@ public void importActivity() { showLocalizedToast("morphe_settings_import_file_success", "Settings imported successfully, tap Save to apply"); } else { - importSettingsText(getContext(), text); + importSettingsText(text); } }); } @@ -1061,7 +1094,7 @@ protected static void showLocalizedToast(String resourceKey, String fallbackMess } } - private void importSettingsText(Context context, String replacementSettings) { + private void importSettingsText(String replacementSettings) { try { existingSettings = Setting.exportToJson(null); if (replacementSettings.equals(existingSettings)) { @@ -1070,7 +1103,7 @@ private void importSettingsText(Context context, String replacementSettings) { settingImportInProgress = true; final boolean rebootNeeded = Setting.importFromJSON(getActivity(), replacementSettings); if (rebootNeeded) { - showRestartDialog(context); + setRestartNeeded(); } } catch (Exception ex) { Logger.printException(() -> "importSettingsText failure", ex); @@ -1099,7 +1132,11 @@ public void onConfigurationChanged(@NonNull Configuration newConfig) { if (activity != null) { Intent intent = activity.getIntent(); intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION); + // Activity is recreated and not closed, so do not show the restart dialog yet. + final boolean restartWasNeeded = restartNeeded; + restartNeeded = false; activity.finish(); + restartNeeded = restartWasNeeded; activity.overridePendingTransition(0, 0); startActivity(intent); activity.overridePendingTransition(0, 0); diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/YouTubeActivityHook.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/YouTubeActivityHook.java index a6b4f4f0ee..4e1a215504 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/YouTubeActivityHook.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/YouTubeActivityHook.java @@ -24,6 +24,7 @@ import app.morphe.extension.shared.Utils; import app.morphe.extension.shared.settings.BaseActivityHook; import app.morphe.extension.shared.settings.SharedYouTubeSettings; +import app.morphe.extension.shared.settings.preference.AbstractPreferenceFragment; import app.morphe.extension.shared.spoof.SpoofAppVersionPatch; import app.morphe.extension.youtube.patches.VersionCheckPatch; import app.morphe.extension.youtube.settings.preference.YouTubePreferenceFragment; @@ -104,7 +105,7 @@ protected View.OnClickListener getNavigationClickListener(Activity activity) { if (searchViewController != null && searchViewController.isSearchActive()) { searchViewController.handleBackPress(); } else { - activity.finish(); + closeSettings(activity); } }; } @@ -182,11 +183,11 @@ public static void updateLightDarkModeStatus(Enum value) { * @return if the original activity finish method should be allowed to run. */ @SuppressWarnings("unused") - public static boolean handleBackPress() { + public static boolean handleBackPress(Activity activity) { if (searchViewController != null && searchViewController.isSearchActive()) { return searchViewController.handleBackPress(); } - return false; + return AbstractPreferenceFragment.showRestartDialogIfNeeded(activity, activity::finish); } /** diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/preference/GetCaptionCookiesPreference.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/preference/GetCaptionCookiesPreference.java index ba917e4cc5..cdc38154f9 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/preference/GetCaptionCookiesPreference.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/preference/GetCaptionCookiesPreference.java @@ -70,7 +70,7 @@ public boolean onPreferenceClick(Preference preference) { Setting.privateSetValueFromString(Settings.CAPTION_COOKIES, newValue); Settings.CAPTION_COOKIES.saveToPreferences(); AbstractPreferenceFragment.settingImportInProgress = false; - AbstractPreferenceFragment.showRestartDialog(getContext()); + AbstractPreferenceFragment.setRestartNeeded(); Utils.showToastShort(str("morphe_get_caption_cookies_success")); } else { diff --git a/patches/src/main/kotlin/app/morphe/patches/reddit/misc/settings/SettingsPatch.kt b/patches/src/main/kotlin/app/morphe/patches/reddit/misc/settings/SettingsPatch.kt index cce3de26b1..1bf0495ef7 100644 --- a/patches/src/main/kotlin/app/morphe/patches/reddit/misc/settings/SettingsPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/reddit/misc/settings/SettingsPatch.kt @@ -18,6 +18,7 @@ import app.morphe.patcher.string import app.morphe.patcher.util.cloneParameters import app.morphe.patcher.util.findFreeRegister import app.morphe.patcher.util.p0Register +import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable import app.morphe.patcher.util.registersUsed import app.morphe.patcher.util.returnEarly import app.morphe.patches.all.misc.crashlog.crashLogPatch @@ -43,7 +44,9 @@ import app.morphe.patches.util.resource.transformStringResources import app.morphe.util.ResourceGroup import app.morphe.util.copyResources import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.immutable.ImmutableMethod private const val EXTENSION_CLASS = "Lapp/morphe/extension/reddit/settings/RedditActivityHook;" @@ -248,5 +251,31 @@ val settingsPatch = bytecodePatch( ) } } + + // Override finish() to show the restart dialog when closing the settings. + WebBrowserActivityOnCreateFingerprint.classDef.let { activityClass -> + ImmutableMethod( + activityClass.type, + "finish", + emptyList(), + "V", + AccessFlags.PUBLIC.value, + null, + null, + MutableMethodImplementation(2) + ).toMutable().apply { + addInstructionsWithLabels( + 0, + """ + invoke-static { p0 }, $EXTENSION_CLASS->handleFinish(Landroid/app/Activity;)Z + move-result v0 + if-nez v0, :handled + invoke-super { p0 }, ${activityClass.superclass}->finish()V + :handled + return-void + """ + ) + }.let(activityClass.methods::add) + } } } diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/misc/settings/SettingsPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/settings/SettingsPatch.kt index 7e178f0799..3e92295cbf 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/misc/settings/SettingsPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/settings/SettingsPatch.kt @@ -408,7 +408,7 @@ internal fun modifyActivityForSettingsInjection( ) }.let(activityOnCreateClass.methods::add) - // Override finish() to intercept back gesture. + // Override finish() to intercept back gesture, and to show the restart dialog if needed. ImmutableMethod( activityOnCreateClass.type, if (isYouTubeMusic) "finish" else "onBackPressed", @@ -426,11 +426,11 @@ internal fun modifyActivityForSettingsInjection( addInstructions( 0, """ - invoke-static {}, $extensionClassType->$extensionMethodName()Z + invoke-static { p0 }, $extensionClassType->$extensionMethodName(Landroid/app/Activity;)Z move-result v0 - if-nez v0, :search_handled + if-nez v0, :handled $invokeFinishOpcode { p0 }, Landroid/app/Activity;->finish()V - :search_handled + :handled return-void """ ) From 456e0db9603f0930a4893e3f24ee3eee8bd4e5f9 Mon Sep 17 00:00:00 2001 From: 0xrxL <127248639+0xrxL@users.noreply.github.com> Date: Mon, 26 Oct 2026 20:03:28 +0100 Subject: [PATCH 090/189] feat(YouTube - Downloads): Add playlist downloads for accounts without Premium --- .../youtube/patches/DownloadsPatch.java | 31 +++ .../youtube/patches/InAppDownloadsPatch.java | 191 +++++++++++++++++- .../youtube/patches/utils/ProtoNode.java | 30 ++- .../extension/youtube/innertube/offline.proto | 96 +++++++++ .../interaction/downloads/DownloadsPatch.kt | 24 ++- .../interaction/downloads/Fingerprints.kt | 20 ++ 6 files changed, 384 insertions(+), 8 deletions(-) diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/DownloadsPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/DownloadsPatch.java index 076c843283..e3eb9b8a21 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/DownloadsPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/DownloadsPatch.java @@ -13,12 +13,18 @@ import app.morphe.extension.shared.Logger; import app.morphe.extension.shared.Utils; import app.morphe.extension.shared.patches.DownloadMode; +import app.morphe.extension.shared.settings.preference.ExternalDownloaderPreference; import app.morphe.extension.youtube.settings.Settings; import app.morphe.extension.youtube.videoplayer.ExternalDownloadButton; @SuppressWarnings("unused") public final class DownloadsPatch { + /** + * Ordinal of the action of the offline playlist endpoint that saves the playlist. + */ + private static final int OFFLINE_PLAYLIST_ACTION_ADD_ORDINAL = 1; + /** * Injection point. *

@@ -47,4 +53,29 @@ public static boolean inAppDownloadButtonOnClick(String videoId) { return false; } + /** + * Injection point. + *

+ * Called when a playlist is saved or removed, + * such as with the Download button of a playlist page header. + * Only saving opens the external downloader, so playlists saved in-app can still be removed. + * + * @param action Action of the offline playlist endpoint. + */ + public static boolean offlinePlaylistEndpointOnClick(String playlistId, Enum action) { + try { + if (Settings.DOWNLOAD_MODE.get() != DownloadMode.EXTERNAL + || action.ordinal() != OFFLINE_PLAYLIST_ACTION_ADD_ORDINAL) { + return false; + } + + ExternalDownloaderPreference.launchExternalDownloader(playlistId, Utils.getActivity(), + "https://www.youtube.com/playlist?list=" + playlistId); + return true; + } catch (Exception ex) { + Logger.printException(() -> "offlinePlaylistEndpointOnClick failure", ex); + } + return false; + } + } diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/InAppDownloadsPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/InAppDownloadsPatch.java index a142bf0649..b94a17b680 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/InAppDownloadsPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/InAppDownloadsPatch.java @@ -8,19 +8,34 @@ package app.morphe.extension.youtube.patches; +import android.net.Uri; +import android.util.Base64; + import androidx.annotation.Nullable; +import com.google.protobuf.ByteString; import com.google.protobuf.MessageLite; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; import app.morphe.extension.shared.Logger; +import app.morphe.extension.shared.ResourceUtils; import app.morphe.extension.shared.patches.DownloadMode; import app.morphe.extension.shared.spoof.ClientType; import app.morphe.extension.shared.spoof.SpoofVideoStreamsPatch; import app.morphe.extension.shared.spoof.requests.StreamingDataRequest; +import app.morphe.extension.youtube.innertube.Offline.ButtonCommand; +import app.morphe.extension.youtube.innertube.Offline.DownloadButtonColor; +import app.morphe.extension.youtube.innertube.Offline.DownloadButtonLayout; +import app.morphe.extension.youtube.innertube.Offline.DownloadButtonSize; +import app.morphe.extension.youtube.innertube.Offline.DownloadButtonStyle; +import app.morphe.extension.youtube.innertube.Offline.EntityKey; +import app.morphe.extension.youtube.innertube.Offline.FlexibleAction; +import app.morphe.extension.youtube.innertube.Offline.FlexibleActionsRow; +import app.morphe.extension.youtube.innertube.Offline.FlexibleActionsViewModel; import app.morphe.extension.youtube.innertube.Offline.OfflineCommand; import app.morphe.extension.youtube.innertube.Offline.OfflineDownloadAction; import app.morphe.extension.youtube.innertube.Offline.OfflineDownloadActionCommand; @@ -30,6 +45,8 @@ import app.morphe.extension.youtube.innertube.Offline.OfflineFormatType; import app.morphe.extension.youtube.innertube.Offline.OfflinePlayerConfig; import app.morphe.extension.youtube.innertube.Offline.OfflinePlayerResponse; +import app.morphe.extension.youtube.innertube.Offline.OfflinePlaylistAction; +import app.morphe.extension.youtube.innertube.Offline.OfflinePlaylistEndpoint; import app.morphe.extension.youtube.innertube.Offline.OfflineState; import app.morphe.extension.youtube.innertube.Offline.OfflineStateAction; import app.morphe.extension.youtube.innertube.Offline.OfflineStreamFormat; @@ -40,6 +57,10 @@ import app.morphe.extension.youtube.innertube.Offline.OfflineVideoEndpoint; import app.morphe.extension.youtube.innertube.Offline.OfflineabilityContainer; import app.morphe.extension.youtube.innertube.Offline.OfflineabilityRenderer; +import app.morphe.extension.youtube.innertube.Offline.PlaylistDownloadButtonViewModel; +import app.morphe.extension.youtube.innertube.Offline.PlaylistDownloadStatusEntityId; +import app.morphe.extension.youtube.patches.utils.FeedVideoCardsPatch; +import app.morphe.extension.youtube.patches.utils.ProtoNode; import app.morphe.extension.youtube.settings.Settings; @SuppressWarnings("unused") @@ -83,6 +104,28 @@ public final class InAppDownloadsPatch { */ private static final String UNSAVABLE_AUDIO_MIME_TYPE_PREFIX = "audio/webm"; + /** + * Name of the template of the page headers, such as the header of a playlist page. + */ + private static final byte[] PAGE_HEADER_TEMPLATE = "page_header".getBytes(StandardCharsets.US_ASCII); + + /** + * Field of the buttons of a page header, in the proto of the element. + */ + private static final int FLEXIBLE_ACTIONS_VIEW_MODEL_FIELD = 520990934; + + /** + * Entities the Download button of a playlist reads the download status from, + * with the keys the app gives them when the playlist is saved. + */ + private static final int DOWNLOAD_STATUS_ENTITY_TYPE = 141; + private static final int PLAYLIST_DOWNLOAD_STATUS_ENTITY_ID_TYPE = 306; + private static final int GENERATION_STATUS_ENTITY_TYPE = 147; + private static final String PLAYLIST_GENERATION_STATUS_ENTITY_ID = "playlist"; + private static final int ENTITY_PERSISTENCE = 1; + + private static final String PLAYLIST_DOWNLOAD_BUTTON_IDENTIFIER = "playlist-download-button"; + /** * Injection point. *

@@ -267,23 +310,159 @@ public static byte[] replaceDeclinedOfflinePlayerResponse(@Nullable Object playe } } + /** + * Injection point. + *

+ * Called for every element the app parses. + * The server only gives a Download button to the playlist page headers of Premium accounts, + * so it's added to the headers of other accounts. The button saves the playlist in-app, + * or opens the external downloader in External downloader mode. + * + * @param bytes Proto of the element. + * @return The element with a Download button, or the original element. + */ + public static byte[] addPlaylistDownloadButton(byte[] bytes) { + try { + if (Settings.DOWNLOAD_MODE.get() == DownloadMode.DEFAULT + || !FeedVideoCardsPatch.contains(bytes, PAGE_HEADER_TEMPLATE)) { + return bytes; + } + + List element = ProtoNode.parse(bytes); + if (element == null) { + return bytes; + } + + for (ProtoNode actionsNode : ProtoNode.findMessages(element, FLEXIBLE_ACTIONS_VIEW_MODEL_FIELD)) { + FlexibleActionsViewModel actions = withPlaylistDownloadButton( + FlexibleActionsViewModel.parseFrom(actionsNode.getPayload())); + if (actions != null) { + actionsNode.setPayload(actions.toByteArray()); + return ProtoNode.write(element); + } + } + } catch (Exception ex) { + Logger.printException(() -> "addPlaylistDownloadButton failure", ex); + } + + return bytes; + } + + /** + * @return The buttons with a Download button in the row of the Play all button of a playlist, + * or null if the buttons are not of a playlist or already have a Download button. + */ + @Nullable + private static FlexibleActionsViewModel withPlaylistDownloadButton(FlexibleActionsViewModel actions) { + int playlistRowIndex = -1; + String playlistId = ""; + + List rows = actions.getActionsRowsList(); + for (int i = 0; i < rows.size(); i++) { + for (FlexibleAction action : rows.get(i).getActionsList()) { + if (action.hasDownloadButtonViewModel()) { + return null; + } + String id = action.getButtonViewModel().getOnTap().getInnertubeCommand() + .getWatchEndpoint().getPlaylistId(); + if (playlistId.isEmpty() && !id.isEmpty()) { + playlistRowIndex = i; + playlistId = id; + } + } + } + if (playlistId.isEmpty()) { + return null; + } + + return actions.toBuilder() + .setActionsRows(playlistRowIndex, rows.get(playlistRowIndex).toBuilder() + .addActions(playlistDownloadButton(playlistId))) + .build(); + } + + /** + * @return The Download button the server gives to the playlist page header of Premium accounts, + * which saves the playlist or removes it if it's saved. + */ + private static FlexibleAction playlistDownloadButton(String playlistId) { + Logger.printDebug(() -> "Adding download button to playlist: " + playlistId); + + OfflinePlaylistEndpoint endpoint = OfflinePlaylistEndpoint.newBuilder() + .setPlaylistId(playlistId) + .setAction(OfflinePlaylistAction.OFFLINE_PLAYLIST_ACTION_INFER_AUTOMATICALLY) + .setOfflineability(offlineability(List.of())) + .build(); + // Texts of the app, which all supported app versions have. + String downloadableText = ResourceUtils.getString("accessibility_playlist_download"); + String inProgressText = ResourceUtils.getString("offline_button_progress_text"); + String completedText = ResourceUtils.getString("offline_button_complete_text"); + ByteString downloadStatusEntityId = PlaylistDownloadStatusEntityId.newBuilder() + .setPlaylistId(playlistId) + .setType(PLAYLIST_DOWNLOAD_STATUS_ENTITY_ID_TYPE) + .build() + .toByteString(); + + PlaylistDownloadButtonViewModel button = PlaylistDownloadButtonViewModel.newBuilder() + .setPlaylistId(playlistId) + .setDownloadableText(downloadableText) + .setInProgressText(inProgressText) + .setCompletedText(completedText) + .setDownloadableA11YText(downloadableText) + .setInProgressA11YText(inProgressText) + .setCompletedA11YText(completedText) + .setDownloadStatusKey(entityKey(DOWNLOAD_STATUS_ENTITY_TYPE, downloadStatusEntityId)) + .setGenerationStatusKey(entityKey(GENERATION_STATUS_ENTITY_TYPE, + ByteString.copyFromUtf8(PLAYLIST_GENERATION_STATUS_ENTITY_ID))) + .setLayout(DownloadButtonLayout.DOWNLOAD_BUTTON_LAYOUT_PLAYLIST_HEADER) + .setIconColor(DownloadButtonColor.DOWNLOAD_BUTTON_COLOR_PLAYLIST_HEADER) + .setTextColor(DownloadButtonColor.DOWNLOAD_BUTTON_COLOR_PLAYLIST_HEADER) + .setButtonSize(DownloadButtonSize.DOWNLOAD_BUTTON_SIZE_DEFAULT) + .setButtonStyle(DownloadButtonStyle.DOWNLOAD_BUTTON_STYLE_OVERLAY) + .setOnTapCommand(ButtonCommand.newBuilder().setInnertubeCommand( + OfflineCommand.newBuilder().setOfflinePlaylistEndpoint(endpoint))) + .setIdentifier(PLAYLIST_DOWNLOAD_BUTTON_IDENTIFIER) + .build(); + return FlexibleAction.newBuilder().setDownloadButtonViewModel(button).build(); + } + + /** + * @return The key of the entity, as the app encodes it. + */ + private static String entityKey(int entityType, ByteString entityId) { + EntityKey key = EntityKey.newBuilder() + .setEntityId(entityId) + .setEntityType(entityType) + .setPersistence(ENTITY_PERSISTENCE) + .build(); + return Uri.encode(Base64.encodeToString(key.toByteArray(), Base64.URL_SAFE | Base64.NO_WRAP)); + } + /** * @return A command of the offline video endpoint that saves the video with the formats. */ private static OfflineCommand saveVideoCommand(String videoId, List formats) { - OfflineabilityRenderer renderer = OfflineabilityRenderer.newBuilder() - .setOfflineable(true) - .addAllFormats(unlockedFormats(formats)) - .build(); OfflineVideoEndpoint endpoint = OfflineVideoEndpoint.newBuilder() .setVideoId(videoId) .setAction(OfflineVideoAction.OFFLINE_VIDEO_ACTION_ADD) - .setOfflineability(OfflineabilityContainer.newBuilder() - .setOfflineabilityRenderer(renderer)) + .setOfflineability(offlineability(formats)) .build(); return OfflineCommand.newBuilder().setOfflineVideoEndpoint(endpoint).build(); } + /** + * @return Offlineability that saves with the formats. + */ + private static OfflineabilityContainer offlineability(List formats) { + OfflineabilityRenderer renderer = OfflineabilityRenderer.newBuilder() + .setOfflineable(true) + .addAllFormats(unlockedFormats(formats)) + .build(); + return OfflineabilityContainer.newBuilder() + .setOfflineabilityRenderer(renderer) + .build(); + } + private static boolean hasLockedFormat(List formats) { for (OfflineFormat format : formats) { if (isLocked(format)) { diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/ProtoNode.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/ProtoNode.java index 78cad981de..3f511e79e4 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/ProtoNode.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/ProtoNode.java @@ -21,6 +21,7 @@ import java.nio.charset.CodingErrorAction; import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; /** @@ -28,7 +29,7 @@ *

* Length delimited fields are parsed as text if the payload is printable UTF-8, * otherwise as a message if possible, otherwise kept as bytes. - * Only text and varints can be modified, and fields can be removed. + * Only text, varints and the bytes of length delimited fields can be modified, and fields can be removed. * All other fields are written back unchanged. * Fields reference the parsed bytes instead of copying them. */ @@ -55,6 +56,11 @@ public final class ProtoNode { private String text; @Nullable private Long varint; + /** + * Replaced bytes of a length delimited field. + */ + @Nullable + private byte[] payload; /** * If the text of this field or a sub-field is modified. */ @@ -197,6 +203,26 @@ public String decodeUtf8() { public void setText(String text) { this.text = text; children = null; + payload = null; + setModified(); + } + + /** + * @return The bytes of a length delimited field, such as a message to parse with a schema. + */ + public byte[] getPayload() { + return payload != null + ? payload + : Arrays.copyOfRange(bytes, valueStart, valueEnd); + } + + /** + * Replaces the bytes of a length delimited field, such as with a modified message. + */ + public void setPayload(byte[] payload) { + this.payload = payload; + text = null; + children = null; setModified(); } @@ -286,6 +312,8 @@ private static void write(List message, CodedOutputStream output) thr List children = node.children; if (!node.modified) { output.writeRawBytes(node.bytes, node.tagStart, node.valueEnd - node.tagStart); + } else if (node.payload != null) { + output.writeByteArray(node.fieldNumber, node.payload); } else if (children != null) { output.writeByteArray(node.fieldNumber, write(children)); } else if (node.text != null) { diff --git a/extensions/youtube/src/main/proto/app/morphe/extension/youtube/innertube/offline.proto b/extensions/youtube/src/main/proto/app/morphe/extension/youtube/innertube/offline.proto index 32c3fe25f7..38bc517674 100644 --- a/extensions/youtube/src/main/proto/app/morphe/extension/youtube/innertube/offline.proto +++ b/extensions/youtube/src/main/proto/app/morphe/extension/youtube/innertube/offline.proto @@ -10,6 +10,13 @@ option java_package = "app.morphe.extension.youtube.innertube"; message OfflineCommand { optional OfflineVideoEndpoint offlineVideoEndpoint = 73080600; + optional OfflinePlaylistEndpoint offlinePlaylistEndpoint = 73082583; + // Endpoint of the Play all button of a playlist page header. + optional PlaylistWatchEndpoint watchEndpoint = 48687757; +} + +message PlaylistWatchEndpoint { + optional string playlistId = 2; } message OfflineVideoEndpoint { @@ -23,6 +30,18 @@ enum OfflineVideoAction { OFFLINE_VIDEO_ACTION_ADD = 1; } +message OfflinePlaylistEndpoint { + optional string playlistId = 1; + optional OfflinePlaylistAction action = 2; + optional OfflineabilityContainer offlineability = 4; +} + +enum OfflinePlaylistAction { + OFFLINE_PLAYLIST_ACTION_UNKNOWN = 0; + // Saves the playlist, or removes it if it's saved. + OFFLINE_PLAYLIST_ACTION_INFER_AUTOMATICALLY = 9; +} + message OfflineabilityContainer { optional OfflineabilityRenderer offlineabilityRenderer = 60572968; } @@ -137,3 +156,80 @@ enum OfflineStateAction { OFFLINE_STATE_ACTION_UNKNOWN = 0; OFFLINE_STATE_ACTION_OK = 1; } + +// Buttons of a page header, such as the Play all, Save and Share buttons of a playlist page. +message FlexibleActionsViewModel { + repeated FlexibleActionsRow actionsRows = 1; +} + +message FlexibleActionsRow { + repeated FlexibleAction actions = 1; +} + +message FlexibleAction { + optional FlexibleActionButtonViewModel buttonViewModel = 461054335; + optional PlaylistDownloadButtonViewModel downloadButtonViewModel = 33561776; +} + +message FlexibleActionButtonViewModel { + optional ButtonCommand onTap = 4; +} + +message ButtonCommand { + optional OfflineCommand innertubeCommand = 169495254; +} + +// Download button of a playlist page header, which shows the download status of the playlist. +message PlaylistDownloadButtonViewModel { + optional string playlistId = 2; + optional string completedText = 4; + optional string inProgressText = 5; + optional string downloadableText = 6; + optional string completedA11yText = 7; + optional string inProgressA11yText = 8; + // Entity keys the download status is read from. + optional string downloadStatusKey = 12; + optional string generationStatusKey = 13; + optional DownloadButtonLayout layout = 14; + optional DownloadButtonColor iconColor = 23; + optional DownloadButtonColor textColor = 24; + optional string downloadableA11yText = 26; + optional ButtonCommand onTapCommand = 27; + optional DownloadButtonSize buttonSize = 29; + optional DownloadButtonStyle buttonStyle = 30; + optional string identifier = 31; +} + +// Values of the Download button the server gives to a playlist page header. +enum DownloadButtonLayout { + DOWNLOAD_BUTTON_LAYOUT_UNKNOWN = 0; + DOWNLOAD_BUTTON_LAYOUT_PLAYLIST_HEADER = 8; +} + +enum DownloadButtonColor { + DOWNLOAD_BUTTON_COLOR_UNKNOWN = 0; + DOWNLOAD_BUTTON_COLOR_PLAYLIST_HEADER = 2; +} + +enum DownloadButtonSize { + DOWNLOAD_BUTTON_SIZE_UNKNOWN = 0; + DOWNLOAD_BUTTON_SIZE_DEFAULT = 1; +} + +enum DownloadButtonStyle { + DOWNLOAD_BUTTON_STYLE_UNKNOWN = 0; + DOWNLOAD_BUTTON_STYLE_OVERLAY = 4; +} + +// Key of an entity of the entity store. +message EntityKey { + optional bytes entityId = 2; + optional int32 entityType = 4; + optional int32 persistence = 5; +} + +// Entity id of the download status of a playlist. +message PlaylistDownloadStatusEntityId { + optional string playlistId = 1; + optional int32 type = 2; +} diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/downloads/DownloadsPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/downloads/DownloadsPatch.kt index 51aef25ada..aa50718292 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/downloads/DownloadsPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/downloads/DownloadsPatch.kt @@ -19,6 +19,7 @@ import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMuta import app.morphe.patcher.util.registersUsed import app.morphe.patches.shared.misc.fix.proto.fixProtoLibraryPatch import app.morphe.patches.shared.misc.fix.proto.parseByteArrayWithRegistryMethodRef +import app.morphe.patches.shared.misc.proto.hookElement import app.morphe.patches.shared.misc.settings.preference.ListPreference import app.morphe.patches.shared.misc.settings.preference.PreferenceScreenPreference import app.morphe.patches.shared.misc.settings.preference.PreferenceScreenPreference.Sorting @@ -29,6 +30,7 @@ import app.morphe.patches.youtube.misc.auth.authHookPatch import app.morphe.patches.youtube.misc.playercontrols.addTopControl import app.morphe.patches.youtube.misc.playercontrols.initializeTopControl import app.morphe.patches.youtube.misc.playercontrols.legacyPlayerControlsPatch +import app.morphe.patches.youtube.misc.proto.elementProtoParserHookPatch import app.morphe.patches.youtube.misc.settings.PreferenceScreen import app.morphe.patches.youtube.misc.settings.settingsPatch import app.morphe.patches.youtube.misc.spoof.spoofVideoStreamsPatch @@ -112,7 +114,8 @@ val downloadsPatch = bytecodePatch( videoInformationPatch, legacyPlayerControlsPatch, fixProtoLibraryPatch, - spoofVideoStreamsPatch + spoofVideoStreamsPatch, + elementProtoParserHookPatch ) compatibleWith(COMPATIBILITY_YOUTUBE) @@ -134,6 +137,25 @@ val downloadsPatch = bytecodePatch( ) } + // Open the external downloader with the playlist link, instead of saving the playlist. + OfflinePlaylistEndpointFingerprint.method.addInstructionsWithLabels( + 0, + """ + # The parameter registers can be higher than 15. + move-object/from16 v0, p3 + move-object/from16 v1, p4 + invoke-static { v0, v1 }, $EXTENSION_CLASS->offlinePlaylistEndpointOnClick(Ljava/lang/String;Ljava/lang/Enum;)Z + move-result v0 + if-eqz v0, :show_native_downloader + return-void + :show_native_downloader + nop + """ + ) + + // Add the Download button to the playlist page headers, which only Premium accounts get. + hookElement("$EXTENSION_IN_APP_DOWNLOADS_CLASS->addPlaylistDownloadButton") + // region In-app downloads. // Save the video instead of asking the server for the download action, which answers with the Premium upsell. diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/downloads/Fingerprints.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/downloads/Fingerprints.kt index a61b254e12..6a1a6be4ca 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/downloads/Fingerprints.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/downloads/Fingerprints.kt @@ -38,6 +38,26 @@ internal object OfflineVideoEndpointFingerprint : Fingerprint( ) ) +/** + * Saves or removes a playlist, such as from the Download button of a playlist page header. + */ +internal object OfflinePlaylistEndpointFingerprint : Fingerprint( + accessFlags = listOf(AccessFlags.PUBLIC, AccessFlags.FINAL), + returnType = "V", + parameters = listOf( + "L", + "Ljava/util/Map;", + "Ljava/lang/String;", // Playlist ID + "L", // Action + ), + filters = listOf( + anyInstruction( + string("Unsupported Offline Playlist Action: "), // 20.x + string("Unsupported Offline Playlist Action: %d") // 21.x + ) + ) +) + /** * Matches the offline video endpoint method, to find the call that saves a video with its offlineability renderer. * The quality picker and the Premium upsell of the video are both shown by the called method. From 6d4315757768d792614915d0867079437505fab9 Mon Sep 17 00:00:00 2001 From: 0xrxL <127248639+0xrxL@users.noreply.github.com> Date: Tue, 27 Oct 2026 02:38:13 +0100 Subject: [PATCH 091/189] fix(YouTube - Litho filter): Crash when scrolling a list after a filtered element is removed --- .../patches/components/LithoFilterPatch.java | 128 +++++++++--------- .../shared/misc/litho/filter/Fingerprints.kt | 51 +++++++ .../misc/litho/filter/LithoFilterPatch.kt | 35 ++++- 3 files changed, 148 insertions(+), 66 deletions(-) diff --git a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/patches/components/LithoFilterPatch.java b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/patches/components/LithoFilterPatch.java index 42bb41ec02..afa227ab29 100644 --- a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/patches/components/LithoFilterPatch.java +++ b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/patches/components/LithoFilterPatch.java @@ -74,6 +74,7 @@ public interface ListItemDecoratorInterface { public interface ListSectionInterface { /** * Removes the item from the section, which lays itself out again without it. + * The rows of the grids are laid out again too, also when the section is no longer a grid. * * @return If the section has the item. */ @@ -277,6 +278,20 @@ public static void addListSection(ListSectionInterface section) { } } + /** + * Injection point. + * Called when a section of a list is released, such as when the list is created again. + *

+ * A released section can share the list of its items with the section that replaces it. + * Removing an item through the released section would not lay out the rows of the new one + * again, and the rows that are views of the list would throw on the next access. + */ + public static void removeListSection(ListSectionInterface section) { + synchronized (listSections) { + listSections.removeIf(reference -> reference.get() == section); + } + } + /** * Injection point. * Called when the whole element is hidden. @@ -301,75 +316,62 @@ public static void removeFilteredElementListItem(@Nullable ContextInterface cont Object item = listItemDecorator.patch_getListItem(); if (item != null) { // The lists can only be changed on the main thread. - Utils.runOnMainThread(() -> removeListItem(item)); - } - } - } - } catch (Exception ex) { - Logger.printException(() -> "removeFilteredElementListItem failure", ex); - } - } - - private static void removeListItem(Object item) { - // The sections group their items into the rows of some grids, such as the related videos, - // and lay the rows out again. Other grids, such as the feed, have a list for each item. - if (removeListSectionItem(item) || removeObservableListItem(item)) { - Logger.printDebug(() -> "Removed list item of filtered element: " + item); - } - } - - private static boolean removeListSectionItem(Object item) { - List sections = new ArrayList<>(); - synchronized (listSections) { - Iterator> iterator = listSections.iterator(); - while (iterator.hasNext()) { - ListSectionInterface section = iterator.next().get(); - if (section == null) { - iterator.remove(); - continue; - } - sections.add(section); - } - } - - // Removing the item notifies the list observers, so it is done outside the lock. - for (ListSectionInterface section : sections) { - if (section.patch_removeListItem(item)) { - return true; - } - } - - return false; - } + Utils.runOnMainThread(() -> { + // The sections group their items into the rows of some grids, such as + // the related videos, and lay the rows out again. Other grids, such as + // the feed, have a list for each item. + List sections = new ArrayList<>(); + synchronized (listSections) { + Iterator> iterator = listSections.iterator(); + while (iterator.hasNext()) { + ListSectionInterface section = iterator.next().get(); + if (section == null) { + iterator.remove(); + continue; + } + sections.add(section); + } + } - private static boolean removeObservableListItem(Object item) { - List itemList = null; - int itemIndex = -1; + // Removing the item notifies the list observers, so it is done outside the lock. + for (ListSectionInterface section : sections) { + if (section.patch_removeListItem(item)) { + Logger.printDebug(() -> "Removed list item of filtered element: " + item); + return; + } + } - synchronized (observableLists) { - Iterator>> iterator = observableLists.iterator(); - while (iterator.hasNext()) { - List list = iterator.next().get(); - if (list == null) { - iterator.remove(); - continue; - } + List itemList = null; + int itemIndex = -1; + synchronized (observableLists) { + Iterator>> iterator = observableLists.iterator(); + while (iterator.hasNext()) { + List list = iterator.next().get(); + if (list == null) { + iterator.remove(); + continue; + } + + itemIndex = list.indexOf(item); + if (itemIndex >= 0) { + itemList = list; + break; + } + } + } - itemIndex = list.indexOf(item); - if (itemIndex >= 0) { - itemList = list; - break; + // Removing the item notifies the list observers, so it is done outside the lock. + if (itemList != null) { + itemList.remove(itemIndex); + Logger.printDebug(() -> "Removed list item of filtered element: " + item); + } + }); + } } } + } catch (Exception ex) { + Logger.printException(() -> "removeFilteredElementListItem failure", ex); } - - // Removing the item notifies the list observers, so it is done outside the lock. - if (itemList != null) { - itemList.remove(itemIndex); - return true; - } - - return false; } private static boolean useLegacyLithoFiltering() { diff --git a/patches/src/main/kotlin/app/morphe/patches/shared/misc/litho/filter/Fingerprints.kt b/patches/src/main/kotlin/app/morphe/patches/shared/misc/litho/filter/Fingerprints.kt index 97d44f22e9..646bae861f 100644 --- a/patches/src/main/kotlin/app/morphe/patches/shared/misc/litho/filter/Fingerprints.kt +++ b/patches/src/main/kotlin/app/morphe/patches/shared/misc/litho/filter/Fingerprints.kt @@ -184,3 +184,54 @@ internal object ListSectionRemoveItemFingerprint : Fingerprint( opcode(Opcode.IF_EQZ, location = MatchAfterImmediately()) ) ) + +/** + * Releases a section of a list, such as when the list is created again. + * The section stops listening to the events, such as the removal of its items. + */ +internal object ListSectionReleaseFingerprint : Fingerprint( + classFingerprint = ListSectionRemoveItemFingerprint, + accessFlags = listOf(AccessFlags.PUBLIC), + returnType = "V", + parameters = listOf(), + filters = listOf( + fieldAccess(opcode = Opcode.IGET_OBJECT, definingClass = "this"), + methodCall( + opcode = Opcode.INVOKE_VIRTUAL, + parameters = listOf("Ljava/lang/Object;"), + returnType = "V", + location = MatchAfterImmediately() + ) + ) +) + +/** + * Updates a section of a list after its items are removed. A grid lays out its rows again, + * otherwise the items are removed one by one. + */ +internal object ListSectionUpdateRowsFingerprint : Fingerprint( + classFingerprint = ListSectionRemoveItemFingerprint, + returnType = "V", + filters = listOf( + // If the section is a grid. + methodCall(opcode = Opcode.INVOKE_DIRECT, definingClass = "this", returnType = "Z", parameters = listOf()), + opcode(Opcode.MOVE_RESULT, location = MatchAfterImmediately()), + opcode(Opcode.IF_EQZ, location = MatchAfterImmediately()), + // Lays out the rows again. + methodCall( + opcode = Opcode.INVOKE_VIRTUAL, + definingClass = "this", + returnType = "V", + parameters = listOf(), + location = MatchAfterImmediately() + ), + // Updates the rest of the section, such as the show more button. It is right after + // laying out the rows on some versions, and after removing the items on others. + methodCall( + opcode = Opcode.INVOKE_VIRTUAL, + definingClass = "this", + returnType = "V", + parameters = listOf() + ) + ) +) diff --git a/patches/src/main/kotlin/app/morphe/patches/shared/misc/litho/filter/LithoFilterPatch.kt b/patches/src/main/kotlin/app/morphe/patches/shared/misc/litho/filter/LithoFilterPatch.kt index d30fdce1c4..a45e7b24d3 100644 --- a/patches/src/main/kotlin/app/morphe/patches/shared/misc/litho/filter/LithoFilterPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/shared/misc/litho/filter/LithoFilterPatch.kt @@ -421,6 +421,16 @@ internal fun sharedLithoFilterPatch( val itemsField = it.instructionMatches[2].getFieldAccessed() val removeItemMethod = it.method + // Removing an item does not lay out the rows again when the section is no longer + // a grid, such as when a single item is left, and the old rows that are views of + // the items would throw on the next access. + val (isGridMethod, layOutRowsMethod, updateSectionMethod) = + ListSectionUpdateRowsFingerprint.instructionMatches.let { matches -> + listOf(0, 3, 4).map { index -> + matches[index].instruction.getReference()!! + } + } + it.classDef.apply { interfaces.add(EXTENSION_LIST_SECTION_INTERFACE) @@ -433,7 +443,7 @@ internal fun sharedLithoFilterPatch( AccessFlags.PUBLIC.value or AccessFlags.FINAL.value, null, null, - MutableMethodImplementation(3), + MutableMethodImplementation(4), ).toMutable().apply { addInstructionsWithLabels( 0, @@ -441,15 +451,34 @@ internal fun sharedLithoFilterPatch( iget-object v0, p0, $itemsField invoke-interface { v0, p1 }, Ljava/util/List;->contains(Ljava/lang/Object;)Z move-result v0 - if-eqz v0, :not_found + if-eqz v0, :done + invoke-direct { p0 }, $isGridMethod + move-result v1 invoke-virtual { p0, p1 }, $removeItemMethod - :not_found + if-eqz v1, :done + invoke-direct { p0 }, $isGridMethod + move-result v1 + if-nez v1, :done + iget-object v1, p0, $itemsField + invoke-interface { v1 }, Ljava/util/List;->isEmpty()Z + move-result v1 + if-nez v1, :done + invoke-virtual { p0 }, $layOutRowsMethod + invoke-virtual { p0 }, $updateSectionMethod + :done return v0 """ ) } ) + // The released sections stop receiving the removed items. + ListSectionReleaseFingerprint.method.addInstruction( + 0, + "invoke-static/range { p0 .. p0 }, $EXTENSION_CLASS->" + + "removeListSection($EXTENSION_LIST_SECTION_INTERFACE)V" + ) + // Only the constructors that call the constructor of the superclass, // the others call those. methods.filter { method -> MethodUtil.isConstructor(method) }.forEach { constructor -> From 5fed7fc701421b701263f742a6ba38fd4cc89dd5 Mon Sep 17 00:00:00 2001 From: 0xrxL <127248639+0xrxL@users.noreply.github.com> Date: Tue, 27 Oct 2026 04:09:25 +0100 Subject: [PATCH 092/189] fix(YouTube - Channel whitelists): Flyout menu opens with a delay while the channel is fetched --- .../youtube/patches/utils/FlyoutUtils.java | 183 ++++++++++++++---- .../utils/requests/ChannelIdRequest.java | 63 ++++-- .../youtube/whitelist/ChannelWhitelist.java | 78 ++++---- .../addresources/values/youtube/strings.xml | 4 + 4 files changed, 235 insertions(+), 93 deletions(-) diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/FlyoutUtils.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/FlyoutUtils.java index 25f6a75c18..17c263f89f 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/FlyoutUtils.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/FlyoutUtils.java @@ -7,6 +7,8 @@ package app.morphe.extension.youtube.patches.utils; +import static app.morphe.extension.shared.settings.preference.ColorPickerPreference.DISABLED_ALPHA; + import android.annotation.SuppressLint; import android.app.Activity; import android.app.Dialog; @@ -18,13 +20,13 @@ import android.os.Handler; import android.os.Looper; import android.support.v7.widget.RecyclerView; -import android.text.TextUtils; import android.util.Pair; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.ViewParent; +import android.view.ViewTreeObserver; import android.view.Window; import android.widget.AbsListView; import android.widget.FrameLayout; @@ -43,10 +45,15 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.CancellationException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.function.Predicate; import app.morphe.extension.shared.Logger; import app.morphe.extension.shared.ResourceType; import app.morphe.extension.shared.ResourceUtils; +import app.morphe.extension.shared.StringRef; import app.morphe.extension.shared.Utils; import app.morphe.extension.shared.patches.components.BufferAsciiStrings; import app.morphe.extension.shared.theme.ThemeUtils; @@ -85,6 +92,19 @@ public record FlyoutMenuInfo( @Nullable PopupWindow popupWindow ) {} + /** + * Data a flyout button needs before it can be used. Until it is loaded, the button is disabled + * and shows a loading text instead of its own. + * + * @param resource Loads in the background. + * @param onLoaded Called on the main thread once the resource is loaded, with the text view of the button. + * Returns if the button can be used. + */ + public record FlyoutButtonResource( + Future resource, + Predicate onLoaded + ) {} + /** * Holds the injected items apart from the app list, and scrolls them on its own * when they do not all fit. Its height is limited by the space that the menu can use @@ -298,6 +318,7 @@ public boolean dispatchTouchEvent(MotionEvent ev) { ); private static final int ITEM_TEXT_ID = ResourceUtils.getIdentifier( ResourceType.ID, "list_item_text"); + private static final StringRef LOADING_BUTTON_TEXT = StringRef.sfc("morphe_flyout_button_loading"); private static final List> customItemTextRefs = new ArrayList<>(); @@ -320,8 +341,6 @@ public boolean dispatchTouchEvent(MotionEvent ev) { private static PopupWindow flyoutPopupWindow; private static String flyoutVideoId = ""; private static String flyoutPlaylistId = ""; - private static String flyoutChannelId = ""; - private static String flyoutChannelName = ""; private static boolean videoMarkedAsForKids; private static boolean isMyTabHistoryFlyout; @@ -331,6 +350,10 @@ public boolean dispatchTouchEvent(MotionEvent ev) { * and only gets the playback speed item. */ private static boolean isShortsPlayerFlyout; + /** + * Channel of the video of the open menu. It loads in the background, so the menu never waits for it. + */ + @Nullable private static ChannelIdRequest flyoutChannelIdRequest; public static byte[] getAsciiBytes(String string) { @@ -345,12 +368,31 @@ public static String getFlyoutPlaylistId() { return flyoutPlaylistId; } - public static String getFlyoutChannelId() { - return flyoutChannelId; + /** + * @return The channel request of the video of the open menu, or null if the menu has no video, + * such as the player settings. + */ + @Nullable + public static ChannelIdRequest getFlyoutChannelIdRequest() { + // The menus of the full history list are not litho elements, so their request starts here. + if (flyoutChannelIdRequest == null && !flyoutVideoId.isEmpty()) { + flyoutChannelIdRequest = ChannelIdRequest.fetchRequestIfNeeded(flyoutVideoId); + } + return flyoutChannelIdRequest; } + /** + * @return The channel name of the video of the open menu, or an empty string if it is not loaded yet. + */ public static String getFlyoutChannelName() { - return flyoutChannelName; + if (flyoutChannelIdRequest != null && flyoutChannelIdRequest.fetchIsDone()) { + // The request is done, so this does not wait. + Pair channelInfo = flyoutChannelIdRequest.getChannelInfo(); + if (channelInfo != null) { + return channelInfo.first; + } + } + return ""; } /** @@ -492,8 +534,11 @@ private static void resetFlyoutIds() { currentButtonIndex = 0; flyoutVideoId = ""; flyoutPlaylistId = ""; - flyoutChannelId = ""; - flyoutChannelName = ""; + if (flyoutChannelIdRequest != null) { + // The buttons waiting for the channel are gone with the menu. + flyoutChannelIdRequest.cancelIfNotDone(); + flyoutChannelIdRequest = null; + } isShortsPlayerFlyout = false; } @@ -513,7 +558,7 @@ private static void addFlyoutElements(Object flyoutPanel) { } if (nextButtonIndex > 0) { - addFlyoutMenuItem(flyoutPanel, null, null, null, nextButtonIndex, true); + addFlyoutMenuItem(flyoutPanel, null, null, null, nextButtonIndex, true, null); } // Reset 'topFlyoutMenuVisible' field, once the buttons have been injected into the player's @@ -669,7 +714,21 @@ public static int addFlyoutButton( View.OnClickListener clickListener, int index ) { - return addFlyoutMenuItem(flyoutPanel, icon, text, clickListener, index, false); + return addFlyoutMenuItem(flyoutPanel, icon, text, clickListener, index, false, null); + } + + /** + * @param buttonResource Data the button needs. The button is disabled until it is loaded. + */ + public static int addFlyoutButton( + Object flyoutPanel, + Drawable icon, + String text, + View.OnClickListener clickListener, + int index, + FlyoutButtonResource buttonResource + ) { + return addFlyoutMenuItem(flyoutPanel, icon, text, clickListener, index, false, buttonResource); } private static int addFlyoutMenuItem( @@ -678,7 +737,8 @@ private static int addFlyoutMenuItem( @Nullable String text, @Nullable View.OnClickListener clickListener, int index, - boolean isDivider + boolean isDivider, + @Nullable FlyoutButtonResource buttonResource ) { try { FlyoutMenuInfo menuInfo = getFlyoutMenuInfo(flyoutPanel, 0); @@ -732,7 +792,8 @@ private static int addFlyoutMenuItem( currentInjectedItems.itemsContainer, icon, text, - clickListener + clickListener, + buttonResource ) ); } @@ -920,7 +981,8 @@ private static View addFlyoutButton( ViewGroup parent, @Nullable Drawable icon, String text, - View.OnClickListener clickListener + View.OnClickListener clickListener, + @Nullable FlyoutButtonResource buttonResource ) { // Inflating the same layout the app uses for its own items keeps the row height, // paddings, font and icon size identical to them. @@ -971,9 +1033,72 @@ private static View addFlyoutButton( customButton.setOnClickListener(clickListener); + if (buttonResource != null) { + Future resource = buttonResource.resource(); + if (resource.isDone()) { + setFlyoutButtonLoaded(customButton, textView, text, buttonResource); + } else { + // The button is shown right away, so the menu does not change its size while the user taps it, + // and the resource is waited for in the background. + setFlyoutButtonEnabled(customButton, false); + if (textView != null) { + // The loading text can take fewer lines than the button text, so it replaces the button text + // only once the button is laid out, and keeps the height of the button text. + textView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() { + @Override + public boolean onPreDraw() { + textView.getViewTreeObserver().removeOnPreDrawListener(this); + if (resource.isDone()) { + // Loaded before the first draw, so the button is drawn ready to use. + setFlyoutButtonLoaded(customButton, textView, text, buttonResource); + } else { + textView.setMinHeight(textView.getHeight()); + textView.setText(LOADING_BUTTON_TEXT.toString()); + } + return false; // Skips the frame drawn before this change. + } + }); + } + Utils.runOnBackgroundThread(() -> { + try { + resource.get(); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + return; + } catch (CancellationException ex) { + return; // The menu was closed. + } catch (ExecutionException ex) { + // The button reads the failure from the resource itself. + } + Utils.runOnMainThread(() -> setFlyoutButtonLoaded(customButton, textView, text, buttonResource)); + }); + } + } + return customButton; } + /** + * Shows the button text again and enables the button if its resource can be used. + * Can run more than once for the same button. + */ + private static void setFlyoutButtonLoaded( + View button, + @Nullable TextView textView, + String text, + FlyoutButtonResource buttonResource + ) { + if (textView != null) { + textView.setText(text); + } + setFlyoutButtonEnabled(button, buttonResource.onLoaded().test(textView)); + } + + private static void setFlyoutButtonEnabled(View button, boolean enabled) { + button.setEnabled(enabled); + button.setAlpha(enabled ? 1.0f : DISABLED_ALPHA); + } + public static View createFlyoutDivider(Context context) { int height = ResourceUtils.getDimensionPixelSize("line_separator_height"); LinearLayout.LayoutParams dividerParams = new LinearLayout.LayoutParams( @@ -1042,7 +1167,7 @@ public static void extractFlyoutIdFromObject(@Nullable Object bufferObject) { isMyTabHistoryFlyout = !horizontalShelfHistoryBytesIndexes.isEmpty() && horizontalShelfHistoryBytesIndexes.size() == HORIZONTAL_SHELF_HISTORY_BYTES.size() && - byteIndexInStartRange(horizontalShelfHistoryBytesIndexes.get(0)); + horizontalShelfHistoryBytesIndexes.get(0) <= 30; // Check if Shorts flyout is triggered only in the feed or on channels. List shortsVideoElementsBytesIndexes = byteIndexesOf(flyoutBuffer, SHORTS_VIDEO_ELEMENT_BYTES); @@ -1091,27 +1216,9 @@ public static void extractFlyoutIdFromObject(@Nullable Object bufferObject) { // Since not every element (especially in shelf) in feed have // a channelId, we'll use YT API to safely retrieve it. + // The request is never waited for here, so the menu opens right away. if (!flyoutVideoId.isEmpty()) { - // Prevent a new request until the previous (if exists) is not done. - if (flyoutChannelIdRequest != null && !flyoutChannelIdRequest.fetchIsDone()) { - return; - } - flyoutChannelIdRequest = ChannelIdRequest.fetchRequestIfNeeded(flyoutVideoId); - - // Unfortunately must block main thread to ensure channel name is set. - Pair remoteFlyoutChannelInfo = flyoutChannelIdRequest.getChannelInfo(); - if (remoteFlyoutChannelInfo != null) { - String remoteFlyoutChannelName = remoteFlyoutChannelInfo.first; - if (!TextUtils.isEmpty(remoteFlyoutChannelName)) { - flyoutChannelName = remoteFlyoutChannelName; - } - - String remoteFlyoutChannelId = remoteFlyoutChannelInfo.second; - if (!TextUtils.isEmpty(remoteFlyoutChannelId)) { - flyoutChannelId = remoteFlyoutChannelId; - } - } } } } @@ -1215,7 +1322,7 @@ private static void setFlyoutPlaylistId(byte[] flyoutBuffer) { int playlistIdEnd = playlistIdStart; while (playlistIdEnd < flyoutBuffer.length) { byte b = flyoutBuffer[playlistIdEnd]; - if (!(isByteAlphanumeric(b) || + if (!((b >= 'A' && b <= 'Z') || (b >= 'a' && b <= 'z') || (b >= '0' && b <= '9') || b == '-' || b == '_')) { break; @@ -1307,12 +1414,4 @@ public static List byteIndexesOf(byte[] haystack, List needles, } return indices; } - - private static boolean byteIndexInStartRange(int index) { - return index >= 0 && index <= 30; - } - - private static boolean isByteAlphanumeric(byte b) { - return (b >= 'A' && b <= 'Z') || (b >= 'a' && b <= 'z') || (b >= '0' && b <= '9'); - } } diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/requests/ChannelIdRequest.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/requests/ChannelIdRequest.java index e8f1abae07..17a890f16b 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/requests/ChannelIdRequest.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/requests/ChannelIdRequest.java @@ -17,6 +17,7 @@ import java.net.SocketTimeoutException; import java.util.Collections; import java.util.Map; +import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; @@ -31,13 +32,17 @@ public class ChannelIdRequest { // Must be less than 5 seconds to ensure Android "App not responding" dialog does not show. private static final int MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 3 * 1000; + private final String videoId; private final Future> future; private volatile String videoTitle = ""; + @Nullable + private volatile HttpURLConnection openConnection; private static final Map cache = Collections.synchronizedMap( Utils.createSizeRestrictedMap(50)); private ChannelIdRequest(String videoId) { + this.videoId = videoId; this.future = Utils.submitOnBackgroundThread(() -> fetch(videoId)); } @@ -46,6 +51,31 @@ public boolean fetchIsDone() { return future.isDone(); } + /** + * @return The pending fetch, to wait for it off the main thread. + */ + public Future> getFuture() { + return future; + } + + /** + * Stops the fetch if it is not done yet, such as when nothing is waiting for it anymore. + * The next request of the video fetches it again. + */ + public void cancelIfNotDone() { + if (future.isDone()) { + return; + } + + cache.remove(videoId, this); + future.cancel(true); + HttpURLConnection connection = openConnection; + if (connection != null) { + // Closing the connection also stops a response that is still being read. + Utils.runOnBackgroundThread(connection::disconnect); + } + } + @Nullable public Pair getChannelInfo() { try { @@ -60,6 +90,8 @@ public Pair getChannelInfo() { Thread.currentThread().interrupt(); } catch (ExecutionException ex) { Logger.printException(() -> "getChannelId failure", ex); + } catch (CancellationException ex) { + Logger.printDebug(() -> "getChannelId cancelled"); } return null; } @@ -82,20 +114,7 @@ private static void handleConnectionError(String toastMessage, @Nullable Excepti } @Nullable - private static Pair parseResponse(JSONObject json) { - JSONObject videoDetails = json.optJSONObject("videoDetails"); - if (videoDetails != null) { - return new Pair<>( - videoDetails.optString("author", ""), - videoDetails.optString("channelId", "") - ); - } - - return null; - } - - @Nullable - private static JSONObject sendRequest(String videoId) { + private JSONObject sendRequest(String videoId) { Utils.verifyOffMainThread(); final long startTime = System.currentTimeMillis(); @@ -104,6 +123,11 @@ private static JSONObject sendRequest(String videoId) { try { byte[] requestBody = ChannelIdRoutes.createBody(videoId); HttpURLConnection connection = ChannelIdRoutes.getConnection(ChannelIdRoutes.GET_CHANNEL_ID); + openConnection = connection; + // The fetch can start before the constructor sets the future. + if (future != null && future.isCancelled()) { + return null; // Cancelled before the request was sent. + } connection.setFixedLengthStreamingMode(requestBody.length); connection.getOutputStream().write(requestBody); int responseCode = connection.getResponseCode(); @@ -126,12 +150,15 @@ private static JSONObject sendRequest(String videoId) { @Nullable private Pair fetch(String videoId) { JSONObject json = sendRequest(videoId); - if (json != null) { - JSONObject videoDetails = json.optJSONObject("videoDetails"); - if (videoDetails != null && videoId.equals(videoDetails.optString("videoId"))) { + JSONObject videoDetails = json == null ? null : json.optJSONObject("videoDetails"); + if (videoDetails != null) { + if (videoId.equals(videoDetails.optString("videoId"))) { videoTitle = videoDetails.optString("title"); } - return parseResponse(json); + return new Pair<>( + videoDetails.optString("author", ""), + videoDetails.optString("channelId", "") + ); } return null; } diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/whitelist/ChannelWhitelist.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/whitelist/ChannelWhitelist.java index eff1644485..ef1a5979f5 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/whitelist/ChannelWhitelist.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/whitelist/ChannelWhitelist.java @@ -10,6 +10,7 @@ import static app.morphe.extension.shared.StringRef.str; import android.graphics.drawable.Drawable; +import android.util.Pair; import androidx.annotation.Nullable; @@ -26,6 +27,7 @@ import app.morphe.extension.shared.Utils; import app.morphe.extension.youtube.patches.VideoInformation; import app.morphe.extension.youtube.patches.utils.FlyoutUtils; +import app.morphe.extension.youtube.patches.utils.requests.ChannelIdRequest; import app.morphe.extension.youtube.settings.Settings; /** @@ -70,37 +72,54 @@ private static int addWhitelistButton( Drawable icon, int index ) { - String currentChannelId = - !FlyoutUtils.getFlyoutChannelId().isEmpty() - ? FlyoutUtils.getFlyoutChannelId() - : VideoInformation.getChannelId(); - String currentChannelName = - !currentChannelId.isEmpty() - ? FlyoutUtils.getFlyoutChannelName() - : VideoInformation.getChannelName(); - - if (currentChannelId.isEmpty()) { - return index; + ChannelIdRequest request = FlyoutUtils.getFlyoutChannelIdRequest(); + if (request == null) { + // Menus without a video of their own, such as the player settings, belong to the playing video. + String currentChannelId = VideoInformation.getChannelId(); + String currentChannelName = VideoInformation.getChannelName(); + if (currentChannelId.isEmpty()) { + return index; + } + + return FlyoutUtils.addFlyoutButton( + flyoutPanel, + icon, + type.getFlyoutTitle(isChannelWhitelisted(type, currentChannelId)), + v -> { + toggleChannel(type, currentChannelId, currentChannelName); + FlyoutUtils.dismissFlyout(); + }, + index + ); } - final boolean isWhitelisted = isChannelWhitelisted( - type, - currentChannelId - ); + // The button is disabled until the channel is loaded, and is enabled right away if it already is. return FlyoutUtils.addFlyoutButton( flyoutPanel, icon, - type.getFlyoutTitle(isWhitelisted), + type.getFlyoutTitle(false), v -> { - toggleChannel( - type, - currentChannelId, - currentChannelName - ); + // The button is only enabled once the request is done, so this does not wait. + Pair channelInfo = request.getChannelInfo(); + if (channelInfo != null) { + toggleChannel(type, channelInfo.second, channelInfo.first); + } FlyoutUtils.dismissFlyout(); }, - index + index, + new FlyoutUtils.FlyoutButtonResource(request.getFuture(), text -> { + Pair channelInfo = request.getChannelInfo(); + String channelId = channelInfo == null ? "" : channelInfo.second; + if (channelId.isEmpty()) { + return false; + } + + if (text != null) { + text.setText(type.getFlyoutTitle(isChannelWhitelisted(type, channelId))); + } + return true; + }) ); } @@ -138,21 +157,14 @@ public static void toggleChannel(WhitelistType type, String channelId, @Nullable removeChannel(type, channelId); Utils.showToastShort(str("morphe_channel_whitelist_channel_removed")); } else { - addChannel(type, channelId, channelName); + Map channels = new LinkedHashMap<>(getWhitelistedChannels(type)); + if (!channelId.isEmpty() && channels.put(channelId, channelName == null ? "" : channelName) == null) { + save(type, channels); + } Utils.showToastShort(str("morphe_channel_whitelist_channel_added")); } } - public static void addChannel(WhitelistType type, String channelId, @Nullable String channelName) { - if (channelId.isEmpty()) { - return; - } - Map channels = new LinkedHashMap<>(getWhitelistedChannels(type)); - if (channels.put(channelId, channelName == null ? "" : channelName) == null) { - save(type, channels); - } - } - public static void removeChannel(WhitelistType type, String channelId) { Map channels = new LinkedHashMap<>(getWhitelistedChannels(type)); if (channels.remove(channelId) != null) { diff --git a/patches/src/main/resources/addresources/values/youtube/strings.xml b/patches/src/main/resources/addresources/values/youtube/strings.xml index fa083d425e..04f763d582 100644 --- a/patches/src/main/resources/addresources/values/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values/youtube/strings.xml @@ -410,6 +410,10 @@ Reports are public and are reviewed before a channel is added to a list. Removal Hide video ads Hide YouTube Premium promotions + + + Loading… + Current channel: Add to whitelist From 5acc298956dfc54c1d3701e3865d3905f01ee613 Mon Sep 17 00:00:00 2001 From: 0xrxL <127248639+0xrxL@users.noreply.github.com> Date: Tue, 27 Oct 2026 04:30:59 +0100 Subject: [PATCH 093/189] chore(YouTube - Change form factor): Show `Enable tablet layout in player` only on YouTube 20.21 and remove its warning --- .../extension/youtube/patches/ChangeFormFactorPatch.java | 4 ++-- .../java/app/morphe/extension/youtube/settings/Settings.java | 2 +- .../youtube/layout/formfactor/ChangeFormFactorPatch.kt | 4 ++-- .../main/resources/addresources/values/youtube/strings.xml | 3 --- 4 files changed, 5 insertions(+), 8 deletions(-) diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/ChangeFormFactorPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/ChangeFormFactorPatch.java index f200221a8b..5c114887d3 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/ChangeFormFactorPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/ChangeFormFactorPatch.java @@ -61,8 +61,8 @@ public enum FormFactor { @Nullable private static final Integer FORM_FACTOR_TYPE = FORM_FACTOR.formFactorType; private static final boolean IS_BROKEN_FORM_FACTOR = FORM_FACTOR.isBroken; - private static final boolean TABLET_LAYOUT_IN_PLAYER = VersionCheckPatch.IS_20_31_OR_GREATER - && FORM_FACTOR != FormFactor.LARGE && Settings.TABLET_LAYOUT_IN_PLAYER.get(); + private static final boolean TABLET_LAYOUT_IN_PLAYER = + FORM_FACTOR != FormFactor.LARGE && Settings.TABLET_LAYOUT_IN_PLAYER.get(); public static final class TabletLayoutInPlayerAvailability implements Setting.Availability { @Override diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/Settings.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/Settings.java index 37ed6ff2ba..f393e7f4d2 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/Settings.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/Settings.java @@ -541,7 +541,7 @@ public class Settings extends SharedYouTubeSettings { public static final BooleanSetting DISABLE_LAYOUT_UPDATES = new BooleanSetting("morphe_disable_layout_updates", FALSE, true,"morphe_disable_layout_updates_user_dialog_message"); public static final BooleanSetting RESTORE_OLD_SETTINGS_MENUS = new BooleanSetting("morphe_restore_old_settings_menus", FALSE, true); public static final EnumSetting CHANGE_FORM_FACTOR = new EnumSetting<>("morphe_change_form_factor", FormFactor.DEFAULT, true, "morphe_change_form_factor_user_dialog_message"); - public static final BooleanSetting TABLET_LAYOUT_IN_PLAYER = new BooleanSetting("morphe_tablet_layout_in_player", FALSE, true, "morphe_tablet_layout_in_player_user_dialog_message", new TabletLayoutInPlayerAvailability()); + public static final BooleanSetting TABLET_LAYOUT_IN_PLAYER = new BooleanSetting("morphe_tablet_layout_in_player", FALSE, true, new TabletLayoutInPlayerAvailability()); public static final BooleanSetting BYPASS_IMAGE_REGION_RESTRICTIONS = new BooleanSetting("morphe_bypass_image_region_restrictions", FALSE, true); public static final BooleanSetting GRADIENT_LOADING_SCREEN = new BooleanSetting("morphe_gradient_loading_screen", FALSE, true); public static final EnumSetting HEADER_LOGO = new EnumSetting<>("morphe_header_logo", HeaderLogo.DEFAULT, true); diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/formfactor/ChangeFormFactorPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/formfactor/ChangeFormFactorPatch.kt index b1427b913a..c394912340 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/formfactor/ChangeFormFactorPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/formfactor/ChangeFormFactorPatch.kt @@ -26,7 +26,7 @@ import app.morphe.patches.youtube.misc.contexthook.clientContextHookPatch import app.morphe.patches.youtube.misc.extension.sharedExtensionPatch import app.morphe.patches.youtube.misc.fix.videoactionbar.restoreOldVideoActionBarPatch import app.morphe.patches.youtube.misc.navigation.navigationBarHookPatch -import app.morphe.patches.youtube.misc.playservice.is_20_31_or_greater +import app.morphe.patches.youtube.misc.playservice.is_20_22_or_greater import app.morphe.patches.youtube.misc.playservice.versionCheckPatch import app.morphe.patches.youtube.misc.settings.PreferenceScreen import app.morphe.patches.youtube.misc.settings.settingsPatch @@ -57,7 +57,7 @@ val changeFormFactorPatch = bytecodePatch( ListPreference("morphe_change_form_factor") ) - if (is_20_31_or_greater) { + if (!is_20_22_or_greater) { preferences += SwitchPreference("morphe_tablet_layout_in_player", summary = true) } diff --git a/patches/src/main/resources/addresources/values/youtube/strings.xml b/patches/src/main/resources/addresources/values/youtube/strings.xml index 04f763d582..c4fc884136 100644 --- a/patches/src/main/resources/addresources/values/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values/youtube/strings.xml @@ -1227,9 +1227,6 @@ Automotive layout "Restores the old player layout Limitation: If \'Restore old video action bar\' is turned off, \'More videos\' overlay may not be available" - "Enabling this may break some player layout features, such as the comment section or \'More videos\' shown below the player. - -The tablet layout is no longer supported by YouTube, and is partially or fully broken on recent YouTube versions. For a working tablet layout, patch YouTube 20.21.37" 21.11.48 - Restore old captions behavior From 15a170042639ca8bc0b4d871b781c29d00926fb2 Mon Sep 17 00:00:00 2001 From: 0xrxL <127248639+0xrxL@users.noreply.github.com> Date: Tue, 27 Oct 2026 04:54:02 +0100 Subject: [PATCH 094/189] fix(YouTube - Return YouTube Dislike): Feed thumbnail like count is not shown with the Classic feed card layout --- .../patches/utils/FeedVideoCardsPatch.java | 65 +++++++++---------- 1 file changed, 30 insertions(+), 35 deletions(-) diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/FeedVideoCardsPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/FeedVideoCardsPatch.java index a80cca516f..0eecb3b75e 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/FeedVideoCardsPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/FeedVideoCardsPatch.java @@ -112,6 +112,12 @@ public final class FeedVideoCardsPatch { private static final byte[] THUMBNAIL_PATH = "/vi".getBytes(StandardCharsets.US_ASCII); + /** + * Invisible format characters, such as the ones the elements of the classic layout add + * after the title (U+2060 to U+2064), which the card does not show. + */ + private static final Pattern FORMAT_CHARACTERS_PATTERN = Pattern.compile("\\p{Cf}+"); + /** Text -> video id, or {@link #AMBIGUOUS}. Oldest texts are removed first. */ @GuardedBy("itself") private static final Map textVideoIds = Utils @@ -132,26 +138,21 @@ public final class FeedVideoCardsPatch { } /** - * @return If the video plays in the feed, where the inline player shows its own buttons - * over the thumbnail, such as the mute and the captions buttons. + * @return If what is drawn on the thumbnail of the card is hidden now, because the video plays + * in the feed, where the inline player shows its own buttons over the thumbnail, + * such as the mute and the captions buttons. */ - public static boolean isPlayingInline(@Nullable String videoId) { - if (videoId == null || PlayerType.getCurrent() != PlayerType.INLINE_MINIMAL) { + public static boolean isCardPlayingInline(ComponentHost host) { + // The video of the card that was laid out, or null if it is not known. + String videoId = cardVideoIds.get(host); + if (videoId == null || PLAYLIST.equals(videoId) + || PlayerType.getCurrent() != PlayerType.INLINE_MINIMAL) { return false; } return videoId.equals(VideoInformation.getVideoId()) || videoId.equals(VideoInformation.getPlayerResponseVideoId()); } - /** - * @return If what is drawn on the thumbnail of the card is hidden now. - */ - public static boolean isCardPlayingInline(ComponentHost host) { - // The video of the card that was laid out, or null if it is not known. - String videoId = cardVideoIds.get(host); - return isPlayingInline(PLAYLIST.equals(videoId) ? null : videoId); - } - /** * @return If the card is a playlist or a mix, which nothing is drawn on. */ @@ -203,6 +204,13 @@ public static boolean contains(byte[] bytes, byte[] search) { return false; } + /** + * @return The text as it's matched between an element and a card. + */ + private static String normalizeText(CharSequence text) { + return FORMAT_CHARACTERS_PATTERN.matcher(text).replaceAll("").trim(); + } + private static boolean isVideoText(String text) { return text.length() >= MIN_TEXT_LENGTH && !text.contains("/vi") @@ -268,7 +276,7 @@ public static byte[] onElementLoaded(byte[] bytes) { synchronized (textVideoIds) { for (ProtoNode node : textNodes) { - String text = node.getText().trim(); + String text = normalizeText(node.getText()); if (!isVideoText(text)) { continue; } @@ -465,7 +473,7 @@ private static void collectTexts(ComponentHost host, List texts) { if (textContent != null) { for (CharSequence text : textContent.getTextItems()) { if (text != null) { - String item = text.toString().trim(); + String item = normalizeText(text); if (isVideoText(item) && !texts.contains(item)) { texts.add(item); } @@ -479,37 +487,24 @@ private static void collectTexts(ComponentHost host, List texts) { } } - @Nullable - private static Activity findActivity(Context context) { - while (context instanceof ContextWrapper wrapper) { - if (context instanceof Activity activity) { - return activity; - } - context = wrapper.getBaseContext(); - } - return null; - } - /** * The share sheet and other bottom sheets are dialogs that can show the thumbnail and the texts * of the opened video, so they look like a card. Feeds are in the window of the activity. */ public static boolean isInDialog(ComponentHost host) { - Activity activity = findActivity(host.getContext()); - if (activity != null && activity.getWindow() != null) { + Context context = host.getContext(); + while (context instanceof ContextWrapper wrapper && !(context instanceof Activity)) { + context = wrapper.getBaseContext(); + } + if (context instanceof Activity activity && activity.getWindow() != null) { View decorView = activity.getWindow().peekDecorView(); if (decorView != null && host.getRootView() != decorView) { return true; } } - return isInBottomSheet(host); - } - /** - * Bottom sheets that are shown in the window of the activity. - */ - private static boolean isInBottomSheet(View view) { - ViewParent parent = view.getParent(); + // Bottom sheets that are shown in the window of the activity. + ViewParent parent = host.getParent(); while (parent instanceof View parentView) { final int id = parentView.getId(); // Generated ids, which have no package, are not resources. From e9f77d7c5932499ababf91481b883c30e96ccd1c Mon Sep 17 00:00:00 2001 From: david419kr <70783505+david419kr@users.noreply.github.com> Date: Tue, 27 Oct 2026 16:18:27 +0900 Subject: [PATCH 095/189] feat(YouTube): Add Niconico-style live/replay comments (#4080) Co-authored-by: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Co-authored-by: MarcaDian <152095496+MarcaDian@users.noreply.github.com> --- docs/custom-icons/README.md | 2 + .../morphe-player-icons-tabler-png.zip | Bin 63784 -> 66739 bytes .../morphe-player-icons-tabler.zip | Bin 117106 -> 122182 bytes .../preference/SeekBarPreference.java | 13 +- .../shared/ui/SheetBottomDialog.java | 94 ++ .../niconicocomments/CommentSchedule.java | 206 +++ .../niconicocomments/EmojiImageCache.java | 87 ++ .../niconicocomments/NiconicoComment.java | 92 ++ .../NiconicoCommentsController.java | 369 ++++++ .../NiconicoCommentsOverlayView.java | 392 ++++++ .../NiconicoCommentsToggleButton.java | 192 +++ .../niconicocomments/YouTubeChatSource.java | 1121 +++++++++++++++++ .../voiceovertranslation/VOTBottomSheet.java | 99 +- .../extension/youtube/settings/Settings.java | 20 + .../videoplayer/PlayerOverlayButton.java | 21 + .../VoiceOverTranslationButton.java | 27 +- .../niconicocomments/CommentScheduleTest.java | 120 ++ .../niconicocomments/EmojiImageCacheTest.java | 135 ++ .../NiconicoCommentsControllerTest.java | 112 ++ .../YouTubeChatSourceTest.java | 156 +++ .../niconicocomments/NiconicoCommentsPatch.kt | 130 ++ .../addresources/values/youtube/strings.xml | 14 + .../morphe_niconico_comments_button_off.xml | 62 + ...rphe_niconico_comments_button_off_bold.xml | 62 + ...he_niconico_comments_button_off_fluent.xml | 79 ++ ..._niconico_comments_button_off_ionicons.xml | 92 ++ ..._niconico_comments_button_off_phosphor.xml | 79 ++ ...o_comments_button_off_phosphor_duotone.xml | 84 ++ ...nico_comments_button_off_phosphor_fill.xml | 80 ++ ...ico_comments_button_off_phosphor_light.xml | 79 ++ ...phe_niconico_comments_button_off_sharp.xml | 62 + .../morphe_niconico_comments_button_on.xml | 62 + ...orphe_niconico_comments_button_on_bold.xml | 62 + ...phe_niconico_comments_button_on_fluent.xml | 70 + ...e_niconico_comments_button_on_ionicons.xml | 82 ++ ...e_niconico_comments_button_on_phosphor.xml | 70 + ...co_comments_button_on_phosphor_duotone.xml | 74 ++ ...onico_comments_button_on_phosphor_fill.xml | 70 + ...nico_comments_button_on_phosphor_light.xml | 70 + ...rphe_niconico_comments_button_on_sharp.xml | 62 + .../youtube_controls_bottom_ui_container.xml | 22 + 41 files changed, 4608 insertions(+), 117 deletions(-) create mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/niconicocomments/CommentSchedule.java create mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/niconicocomments/EmojiImageCache.java create mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/niconicocomments/NiconicoComment.java create mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/niconicocomments/NiconicoCommentsController.java create mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/niconicocomments/NiconicoCommentsOverlayView.java create mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/niconicocomments/NiconicoCommentsToggleButton.java create mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/niconicocomments/YouTubeChatSource.java create mode 100644 extensions/youtube/src/test/java/app/morphe/extension/youtube/niconicocomments/CommentScheduleTest.java create mode 100644 extensions/youtube/src/test/java/app/morphe/extension/youtube/niconicocomments/EmojiImageCacheTest.java create mode 100644 extensions/youtube/src/test/java/app/morphe/extension/youtube/niconicocomments/NiconicoCommentsControllerTest.java create mode 100644 extensions/youtube/src/test/java/app/morphe/extension/youtube/niconicocomments/YouTubeChatSourceTest.java create mode 100644 patches/src/main/kotlin/app/morphe/patches/youtube/layout/niconicocomments/NiconicoCommentsPatch.kt create mode 100644 patches/src/main/resources/niconicocommentsbutton/drawable/morphe_niconico_comments_button_off.xml create mode 100644 patches/src/main/resources/niconicocommentsbutton/drawable/morphe_niconico_comments_button_off_bold.xml create mode 100644 patches/src/main/resources/niconicocommentsbutton/drawable/morphe_niconico_comments_button_off_fluent.xml create mode 100644 patches/src/main/resources/niconicocommentsbutton/drawable/morphe_niconico_comments_button_off_ionicons.xml create mode 100644 patches/src/main/resources/niconicocommentsbutton/drawable/morphe_niconico_comments_button_off_phosphor.xml create mode 100644 patches/src/main/resources/niconicocommentsbutton/drawable/morphe_niconico_comments_button_off_phosphor_duotone.xml create mode 100644 patches/src/main/resources/niconicocommentsbutton/drawable/morphe_niconico_comments_button_off_phosphor_fill.xml create mode 100644 patches/src/main/resources/niconicocommentsbutton/drawable/morphe_niconico_comments_button_off_phosphor_light.xml create mode 100644 patches/src/main/resources/niconicocommentsbutton/drawable/morphe_niconico_comments_button_off_sharp.xml create mode 100644 patches/src/main/resources/niconicocommentsbutton/drawable/morphe_niconico_comments_button_on.xml create mode 100644 patches/src/main/resources/niconicocommentsbutton/drawable/morphe_niconico_comments_button_on_bold.xml create mode 100644 patches/src/main/resources/niconicocommentsbutton/drawable/morphe_niconico_comments_button_on_fluent.xml create mode 100644 patches/src/main/resources/niconicocommentsbutton/drawable/morphe_niconico_comments_button_on_ionicons.xml create mode 100644 patches/src/main/resources/niconicocommentsbutton/drawable/morphe_niconico_comments_button_on_phosphor.xml create mode 100644 patches/src/main/resources/niconicocommentsbutton/drawable/morphe_niconico_comments_button_on_phosphor_duotone.xml create mode 100644 patches/src/main/resources/niconicocommentsbutton/drawable/morphe_niconico_comments_button_on_phosphor_fill.xml create mode 100644 patches/src/main/resources/niconicocommentsbutton/drawable/morphe_niconico_comments_button_on_phosphor_light.xml create mode 100644 patches/src/main/resources/niconicocommentsbutton/drawable/morphe_niconico_comments_button_on_sharp.xml create mode 100644 patches/src/main/resources/niconicocommentsbutton/host/layout/youtube_controls_bottom_ui_container.xml diff --git a/docs/custom-icons/README.md b/docs/custom-icons/README.md index f88e7bc873..d027672a69 100644 --- a/docs/custom-icons/README.md +++ b/docs/custom-icons/README.md @@ -46,6 +46,8 @@ named after the icon it replaces. Any icon left out keeps the **Automatic** styl | `morphe_loop_video_button_range.xml` | Loop video button, loop a range | | `morphe_mute_video_button_off.xml` | Mute button, sound on | | `morphe_mute_video_button_on.xml` | Mute button, muted | +| `morphe_niconico_comments_button_off.xml` | Niconico comments button, hidden | +| `morphe_niconico_comments_button_on.xml` | Niconico comments button, shown | | `morphe_pip_button.xml` | Picture in picture button | | `morphe_play_all_button.xml` | Play all button | | `morphe_player_captions_off.xml` | Player captions button, off | diff --git a/docs/custom-icons/morphe-player-icons-tabler-png.zip b/docs/custom-icons/morphe-player-icons-tabler-png.zip index 28175952a975082837c45eb02709f6d1a7932eb8..a19ec7cd9803d85b269b4a2906c577cdac8cd572 100644 GIT binary patch delta 3084 zcmaJ@XHb(_7k=~70*I6#Ae~6>8bF#5kQTa9M2d(sStKB#w**BgN|9F}0W6di47~^# zfsGWw1p(=>2q>S@OX$-4aKExM`(y7t&zyVa&Ux;+GiRPT#p`r6iS(7CKGGh6M#^Oj z0Pqu1008*;2L{~qbjA9(`~QTiyT6~GCpHKN%{iXKGX!A_^lZa&m!DWIxm@J#=>6Eg zXG1nA%ofcy^3D6m_7bi?S6=mToLlc^WtOOT#{Ic`WIh|`EkpUXDMN)mwc6Ub{81Ad z)*t*^1Nhw3hm-9yJ29(0(f%t{grMFT=VKv(rcV|xrrq7-s2p}{Mem<5&gZK zVukQj*mb%9>`8k%fVKP9`GN0j4r6ibO)qw5+1tBgdXa-CDx)P~)o`f5lb1M_N{;wk z>`Q1RQ%vo2X=hpFxgKd5db~;~P3wfcu-$Uv91}x|Bz9_%BN}981dKny>{jWggHZd~ zSQJC!33;qOfq^P!)saHyYq}5_8>D{i0IxSYtk7bL(ps7ybfpHPhCIC@F{w%fvyQ}N z86Do&6XoQ35Fh5OV`eb9IyQGxqY~k8?EyfCoLI!dQ@dut)sqN4JKaqBOGB&#c7mB~ z#zqqdk#5OuNaED*<^n>mE%|oM?JCmfVcW$Es^kdqIU!M`0GWO&kGye&mr}=fIBOyv z6&bX#EvM_-Rd7G*xNFZMub??nHMjoowDtK3dSS9ML7l~!r$2F|gDr*yc>aJGzUrjF{&Xjz$<#4%}SM>@eTR%Fr#c@MpJWI+a0`|pwq zU-rf;*A}&P?#24LrEY z@%ntH-zNk{bGnI_@z33bn{zxfI#H+yRt>vOdIj48=(&uMT1o-!brcTR0dea0Dh_wF@=7Wu85wACj%+k&vzRnP%W( z+xVqfHx2GS!W8BD*aFX&{lz2MmVS;5$1wL9a;77m`y$0<&{+m1_Js0;v|7N0Lo6J2 zgV(Kp_i?k3i+v(F0{@%ar)l9TcVxAZ-BfxuNku1_wQqWH9{-^81DR50c?Nn{lw6dT z!#^&deB1)Qkxw~hBG8qyJ7U9#aml?c!r%x(vr0Iv8ZHysf>@oiszzXOsYIhFDT51~ z55lYMBcmutv=h<;=Ex()+E)!PW1ZeM#aI~vURgr|`C@qNo3GL>ekLnKc=@tK@R6;( z@w_s(w1DiS_@n$&mf@#y2@&q^S0VPA4@XczAR^Mt|$PFPq$;nHxQK>8s35qu@r5P1g=^}u{#_F*nf$4(xpm$j{JaBOyH8uDgsYqM z#V@h^xW6~bloJ@PeqvMtZxx|7jmWd3Q%{A?iPg>V#^*68ti zb=(}9_I^rX1wr-O&-kAPdWf4IF~b%X5yhiVw*4WRF26Ldu25nrJplZKJf87J!G87uvG@eYe45gyHYM%tPC z)h{-KlKG-ewQzZq-1#I-8pkQI@b3!`W_hiqi@Ub*Zo}Jcn8UOH_W0$Sw^jMKSMT1fV@IX`m@q?vNj))4mIE~LI$600?t=%W3IDrJKoe<0);!g^+Ns!8!c?nd`vIQ{BgIP6m)adzc(;0&i=w9mR zVE~5P`K0I5DYKR+YK2$GZzRLB6i&e5ns~mMuM;s9?u-AhbaM#DGt{ulg%P1_^rnzp zU{Tip>Ih*-+pI1;r8@4NNcto*-)^tOQVJZs;GY7V2#cx=0OJI`HbP(B`Xf^fHQA4! z7IM$h3TEZ=J4}uDq~c6+on~W$e!Nudn+#@Z#B>nDGv`qw@6;>SkuPEz^`$`}@Sx>DNSt7#%(}6d*H`^P6tzx^Wk@)^O>o z4~Iz#D%tjXT%xIXFl|5sm%92pKFBK$gh~-euj-1F6IHgXo{%aBV2bD)&V;tyoP3+< zUociC5eEI)+-mZbU!72qR{c3%+qkL2ou_d#m+=afBi;5s=r)X9Ri>`iGA7LHW=|?{ zY&kuI>>a@iG#Jy}Q}ZfUrHSVfCmED$lvJ#{Z@I2UXR5A=YkOiwyDf9Z?zS7d#6Ny| z<|7X(^5#TpN%pj_9vi9QJXHh1D|+@nl{5f}w_#i};J>8N6YRiKrsjNiefWMXFP>1M zg08-rbiI4)$R{^JVvN1gVDZq?d%0HlaMxME6bIbi26P@lXHRH^kyW38K=Q47}(LMc@Mdd!-p^R=r< zq9e7A#waMQ(R}rEOVr$_@t|*b7g+)++PU8&I7RmtvvK5nUAo#c)s=R$Q{swt)-?_t zPWvX6E4dwxTh+p?vm~hX?U(T@YVuEB%f+o-XuWP+(xZe8WL~=% zZzMvmSmA2}cVi^FtGB`XEZ&YgXvoH`ImN$QCA)RKb9fNzMq>@|lj?EB2h1!IrruQQ z#04~%HZY4VxrG_bP7S@>;8p+pt^7-wq};@TG2Bfgz(uLYbvmt`Do54zn|vrKHDUem zL_OW?&ceZu)A`MIZZ|G7TcD2#a^Yf2DH^v;zAk2&Mu=usFbxy0nj6vHJJ@%LqU4O6 zt3TXX2*yQdwEJO;b1hqOpN>wzC#@{XS z>y7^|_*o)jpGP#T0kh569p2!BZY_#QjnBFNaDLZahvJi%-TZ7LVBnnH4^~!TECm)e z-fE#w==xG`B#h+(bD6Yx^>>+fxku*<1UhaD{%Mbye}E}S#>=ce^Ku(Mb*o0yCpbav z?{nv|e*L)!FfN~em_l{dMAZJSdw-?U`~O4g!!=;rVR|bs<$43#UXk8X;jDrT&^}&@Sh6crh7@zQYH#FLFTSdmJbJdM_podVYML znGW^E^FfCjvS54$^fpNlGTf8}KN6wan+9OhGpK&k2(*3){n#`Gy(^)-U!|a^W2!q_ z{GdTS^mGdib~QrNTl!$*Z;<%59@yLld2j20>YY&0airY^EgU5bv@`uQuq#?WfpvnT2zIy>QUV}dFs);qy0Kgn`(a_Sy(AtcF S@n`X<&8@8 zF!}gjFQ(Me$-@6UnQm21j{E1t#M3Z&(LZOV#HPvdy%LlE|I=cc(>huEzcbU8_Q?hR zotTz(P2Td~jp=>whkcn#tK0C4AV AdH?_b diff --git a/docs/custom-icons/morphe-player-icons-tabler.zip b/docs/custom-icons/morphe-player-icons-tabler.zip index 1e5fd6d1123c9804caadacba828071ffa26b488f..331ee447ed910613f0cbf8d39e6e20b449bc7cf6 100644 GIT binary patch delta 807 zcmah`OK4L;6n%G|CKk1{pjcXKU!lYpc}ZR-FCVQzQ=`;hi;`unF$ACBOSCDvP|$!2 zQ6%VVxwue?;AbId30P>mXqvCp_*rG4s5`|9QgI;);!NE{MV;Yt@43vJIcM&t%jVLA z`F^-+eceBHveqC7Jk|<=peKfoYO%ri;b?-#U^JoYvG{P(SaZyCtVa4d{NnNTHg_it1@iaYm>_B`4bo%k;P}>XhuN*DJ|(#X}{yWJ|Q zRWH?Lmt9pkaJ#&ktR9h7c1xz3?5bmusU)*5CzBPs)2DJy#^bU(sfXXCehx&{<6=5y z4M){tt%WUL9xV|B9+rQ(J9Z~y{^>53eZJjc68)Nw+ad}zQVmb{y6c22SgONJ7?b)R z)ph5&4^1&518cJ3>iN&he;EOO$U#fC>=na0$ z!4~r39R_A#8)=_68ed6>)6j$uW}ubmUyc25JFqm#8jy!pF+sqHd|ZWU9&BQ08Q{S@ z1c`M8-r~7yHNMY754qBXjsgV5iS>YYTc8co1+bBEijxJ{N@5!kitIHlguO-BA*Oc& zrZ#{Dr;3d9Y8Wsbgq8T6Au@UZZ6(Hx8pcaZsbv_)N)RC4Q#jAD_bj^0EH_`o6J^*e z-WvhTq@a^?HQ`*Dc^JKh<_ZMFf!lz6BhZeq3L~D&;-w1ol4n!+vI3h(cLm!Xf~T`@ d7KB~BJ)u4OLj5}zoA_5!wl6da!p$eZ&<~VS{C)rc delta 303 zcmWlUJxD@v6vf~9KS~FKY_it}1rgB_IW!1HzBJ1(f^canXh|ZdEhmFo`oIX`tSt>e zgJ2OX;V43iEa7YS#-!h7n-ho$mUn zHlT}QfboW0cs9mj15pv0qTzBl$Po((bt?qkrZLG2OQ!a9K3iB2xitSQncm4#Z_0x| z8$51eUih~8;<9g#UR!GGfJs~Wj}CcmBPDXDv~B4NmN?{KS*>1y-aCx4WuTuWM}B&J n!?J_0@ZYoTU~YWk0ZM9RF_u}2ttNYV6gKl>CV{{%_@jXiI2 diff --git a/extensions/shared/library/src/main/java/app/morphe/extension/shared/settings/preference/SeekBarPreference.java b/extensions/shared/library/src/main/java/app/morphe/extension/shared/settings/preference/SeekBarPreference.java index b4ae14c7d5..fd637460fe 100644 --- a/extensions/shared/library/src/main/java/app/morphe/extension/shared/settings/preference/SeekBarPreference.java +++ b/extensions/shared/library/src/main/java/app/morphe/extension/shared/settings/preference/SeekBarPreference.java @@ -38,11 +38,19 @@ @SuppressWarnings({"unused", "deprecation"}) public class SeekBarPreference extends Preference { + /** + * @param maxLabel If not null, shown instead of the number when the value is the max, + * such as for a max value that means "no limit". + */ public record SeekBarConfig(IntegerSetting setting, int min, int max, int step, - String unit, int divisor) { + String unit, int divisor, @Nullable StringRef maxLabel) { public SeekBarConfig(IntegerSetting setting, int min, int max, int step, String unit) { this(setting, min, max, step, unit, 1); } + + public SeekBarConfig(IntegerSetting setting, int min, int max, int step, String unit, int divisor) { + this(setting, min, max, step, unit, divisor, null); + } } private static final Map REGISTRY = new HashMap<>(); @@ -216,6 +224,9 @@ public static String formatLabel(int value, SeekBarConfig config) { } private static String formatValue(int value, SeekBarConfig config) { + if (value == config.max && config.maxLabel != null) { + return config.maxLabel.toString(); + } if (config.divisor == 1) { return String.format(Locale.ROOT, "%d%s", value, config.unit); } diff --git a/extensions/shared/library/src/main/java/app/morphe/extension/shared/ui/SheetBottomDialog.java b/extensions/shared/library/src/main/java/app/morphe/extension/shared/ui/SheetBottomDialog.java index 1dbc1444b7..5b3ddabed0 100644 --- a/extensions/shared/library/src/main/java/app/morphe/extension/shared/ui/SheetBottomDialog.java +++ b/extensions/shared/library/src/main/java/app/morphe/extension/shared/ui/SheetBottomDialog.java @@ -23,6 +23,9 @@ import android.annotation.SuppressLint; import android.app.Dialog; import android.content.Context; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffColorFilter; +import android.graphics.Typeface; import android.graphics.drawable.ShapeDrawable; import android.graphics.drawable.shapes.RoundRectShape; import android.view.Gravity; @@ -37,11 +40,18 @@ import android.widget.ListView; import android.widget.ScrollView; import android.widget.Scroller; +import android.widget.SeekBar; +import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import java.util.function.IntConsumer; + import app.morphe.extension.shared.Utils; +import app.morphe.extension.shared.settings.IntegerSetting; +import app.morphe.extension.shared.settings.preference.SeekBarPreference; +import app.morphe.extension.shared.settings.preference.SeekBarPreference.SeekBarConfig; import app.morphe.extension.shared.theme.ThemeUtils; /** @@ -183,6 +193,90 @@ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { return scrollView; } + /** + * Creates a centered bold title for a sheet. + */ + public static TextView createTitle(Context context, String text, int fgColor) { + TextView tv = new TextView(context); + tv.setText(text); + tv.setTextSize(18); + tv.setTypeface(Typeface.DEFAULT_BOLD); + tv.setTextColor(fgColor); + tv.setGravity(Gravity.CENTER_HORIZONTAL); + LinearLayout.LayoutParams params = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT); + params.setMargins(0, Dim.dp8, 0, Dim.dp16); + tv.setLayoutParams(params); + return tv; + } + + /** + * Creates a labeled slider for a setting with a registered {@link SeekBarConfig}. + * + * @param onChanged Called with each value the user selects. It must save the setting. + */ + public static LinearLayout createSliderRow(Context context, String label, IntegerSetting setting, + int fgColor, IntConsumer onChanged) { + final SeekBarConfig config = SeekBarPreference.configFor(setting); + if (config == null) throw new IllegalStateException("No SeekBarConfig registered for " + setting.key); + final int initialValue = setting.get(); + + LinearLayout outer = new LinearLayout(context); + outer.setOrientation(LinearLayout.VERTICAL); + LinearLayout.LayoutParams outerParams = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT); + outerParams.setMargins(0, Dim.dp16, 0, 0); + outer.setLayoutParams(outerParams); + + TextView labelView = new TextView(context); + labelView.setText(label); + labelView.setTextColor(fgColor); + labelView.setTextSize(16); + labelView.setTypeface(Typeface.DEFAULT_BOLD); + outer.addView(labelView); + + LinearLayout seekRow = new LinearLayout(context); + seekRow.setOrientation(LinearLayout.HORIZONTAL); + seekRow.setGravity(Gravity.CENTER_VERTICAL); + LinearLayout.LayoutParams seekRowParams = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT); + seekRowParams.topMargin = Dim.dp16; + seekRow.setLayoutParams(seekRowParams); + + SeekBar seekBar = new SeekBar(context); + seekBar.setMax((config.max() - config.min()) / config.step()); + seekBar.setProgress(SeekBarPreference.valueToProgress(config, initialValue)); + seekBar.getProgressDrawable().setColorFilter(new PorterDuffColorFilter(fgColor, PorterDuff.Mode.SRC_IN)); + seekBar.getThumb().setColorFilter(new PorterDuffColorFilter(fgColor, PorterDuff.Mode.SRC_IN)); + seekRow.addView(seekBar, new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)); + + TextView valueView = new TextView(context); + valueView.setText(SeekBarPreference.formatLabel(initialValue, config)); + valueView.setTextColor(fgColor); + valueView.setTextSize(14); + valueView.setMinWidth(Dim.dp40); + valueView.setGravity(Gravity.END); + seekRow.addView(valueView, new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT)); + + outer.addView(seekRow); + + seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { + @Override + public void onProgressChanged(SeekBar bar, int progress, boolean fromUser) { + if (fromUser) { + final int value = SeekBarPreference.progressToValue(config, progress); + valueView.setText(SeekBarPreference.formatLabel(value, config)); + onChanged.accept(value); + } + } + @Override public void onStartTrackingTouch(SeekBar bar) { } + @Override public void onStopTrackingTouch(SeekBar bar) { } + }); + + return outer; + } + /** * A custom {@link LinearLayout} that provides drag-to-dismiss functionality for a {@link SlideDialog}. * This layout intercepts touch events to allow dragging the dialog downward to dismiss it when the diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/niconicocomments/CommentSchedule.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/niconicocomments/CommentSchedule.java new file mode 100644 index 0000000000..e97e163ff8 --- /dev/null +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/niconicocomments/CommentSchedule.java @@ -0,0 +1,206 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/4080 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + * + * Niconico-style live/replay comments implementation is adapted from + * https://github.com/david419kr/niconico-yt-morphe-patches (GPLv3) + * Feature design is based on david419kr/niconico-yt (MIT License) + */ + +package app.morphe.extension.youtube.niconicocomments; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Random; +import java.util.Set; + +/** Pure-Java comment scheduler shared by the Android view and JVM unit tests. */ +final class CommentSchedule { + static final int MAX_LIVE_COMMENTS = 512; + + private static final long LIVE_SPREAD_MIN_STEP_MS = 35; + private static final long LIVE_SPREAD_MAX_STEP_MS = 160; + private static final long LIVE_SPREAD_MIN_WINDOW_MS = 160; + private static final long LIVE_SPREAD_MAX_WINDOW_MS = 1_000; + private static final long LIVE_MAX_VISUAL_BACKLOG_MS = 750; + + private final List comments = new ArrayList<>(); + private final Random random; + + private int[] shuffledLanes = new int[0]; + private int shuffledLaneCount; + private int shuffledLaneIndex; + private int lastLane = -1; + private long nextLiveStartTimeMs = Long.MIN_VALUE; + private long commentDurationMs; + + CommentSchedule() { + this(new Random()); + } + + CommentSchedule(Random random) { + this.random = random; + } + + void addComments( + List incoming, + long videoTimeMs, + long liveSpreadWindowMs, + int laneCount + ) { + removeExpired(videoTimeMs); + + int liveCount = 0; + for (NiconicoComment comment : incoming) { + if (comment.videoOffsetMs < 0) { + liveCount++; + } + } + + long liveStepMs = calculateLiveSpreadStepMs(liveCount, liveSpreadWindowMs); + long liveCursorMs = Math.max(videoTimeMs, nextLiveStartTimeMs); + liveCursorMs = Math.min(liveCursorMs, videoTimeMs + LIVE_MAX_VISUAL_BACKLOG_MS); + + for (NiconicoComment comment : incoming) { + long startTimeMs; + if (comment.videoOffsetMs >= 0) { + startTimeMs = comment.videoOffsetMs; + } else { + startTimeMs = liveCursorMs; + liveCursorMs += liveStepMs; + } + comments.add(new ScheduledComment(comment, startTimeMs, nextLane(laneCount))); + } + + if (liveCount > 0) { + nextLiveStartTimeMs = liveCursorMs; + trimLiveOverflow(videoTimeMs); + } + recalculateLiveCursor(videoTimeMs, liveStepMs); + } + + private void trimLiveOverflow(long videoTimeMs) { + List live = new ArrayList<>(); + for (ScheduledComment scheduled : comments) { + if (scheduled.comment.videoOffsetMs < 0) live.add(scheduled); + } + if (live.size() <= MAX_LIVE_COMMENTS) return; + + // Keep recent visible comments, then the nearest upcoming live comments. + live.sort(Comparator + .comparingInt((ScheduledComment comment) -> comment.startTimeMs > videoTimeMs ? 1 : 0) + .thenComparing((left, right) -> left.startTimeMs > videoTimeMs + ? Long.compare(left.startTimeMs, right.startTimeMs) + : Long.compare(right.startTimeMs, left.startTimeMs))); + Set dropped = new HashSet<>(live.subList(MAX_LIVE_COMMENTS, live.size())); + // Replay lookahead is not disposable: its continuation has already advanced. + comments.removeIf(dropped::contains); + } + + void removeExpired(long videoTimeMs) { + comments.removeIf(scheduledComment -> videoTimeMs - scheduledComment.startTimeMs > commentDurationMs); + } + + /** How long a comment takes to cross the screen. */ + long getCommentDurationMs() { + return commentDurationMs; + } + + void setCommentDurationMs(long durationMs) { + commentDurationMs = durationMs; + } + + void clear() { + comments.clear(); + nextLiveStartTimeMs = Long.MIN_VALUE; + shuffledLanes = new int[0]; + shuffledLaneCount = 0; + shuffledLaneIndex = 0; + lastLane = -1; + } + + List comments() { + return comments; + } + + void redistributeLanes(int laneCount) { + resetLaneShuffle(); + for (ScheduledComment comment : comments) { + comment.lane = nextLane(laneCount); + } + } + + static long calculateLiveSpreadStepMs(int commentCount, long spreadWindowMs) { + if (commentCount <= 1) { + return 0; + } + long usableWindowMs = Math.min( + LIVE_SPREAD_MAX_WINDOW_MS, + Math.max(LIVE_SPREAD_MIN_WINDOW_MS, spreadWindowMs) + ); + return Math.max( + LIVE_SPREAD_MIN_STEP_MS, + Math.min(LIVE_SPREAD_MAX_STEP_MS, usableWindowMs / commentCount) + ); + } + + private int nextLane(int requestedLaneCount) { + int laneCount = Math.max(1, requestedLaneCount); + if (laneCount != shuffledLaneCount || shuffledLaneIndex >= shuffledLanes.length) { + List lanes = new ArrayList<>(laneCount); + for (int lane = 0; lane < laneCount; lane++) { + lanes.add(lane); + } + Collections.shuffle(lanes, random); + if (laneCount > 1 && lanes.get(0) == lastLane) { + Collections.swap(lanes, 0, 1); + } + shuffledLanes = new int[laneCount]; + for (int index = 0; index < laneCount; index++) { + shuffledLanes[index] = lanes.get(index); + } + shuffledLaneCount = laneCount; + shuffledLaneIndex = 0; + } + + int lane = shuffledLanes[shuffledLaneIndex++]; + lastLane = lane; + return lane; + } + + private void resetLaneShuffle() { + shuffledLanes = new int[0]; + shuffledLaneCount = 0; + shuffledLaneIndex = 0; + lastLane = -1; + } + + private void recalculateLiveCursor(long videoTimeMs, long liveStepMs) { + long latestPendingLiveMs = Long.MIN_VALUE; + for (ScheduledComment scheduled : comments) { + if (scheduled.comment.videoOffsetMs < 0 && scheduled.startTimeMs > videoTimeMs) { + latestPendingLiveMs = Math.max(latestPendingLiveMs, scheduled.startTimeMs); + } + } + nextLiveStartTimeMs = latestPendingLiveMs == Long.MIN_VALUE + ? videoTimeMs + : latestPendingLiveMs + Math.max(LIVE_SPREAD_MIN_STEP_MS, liveStepMs); + } + + static final class ScheduledComment { + final NiconicoComment comment; + final long startTimeMs; + int lane; + + ScheduledComment(NiconicoComment comment, long startTimeMs, int lane) { + this.comment = comment; + this.startTimeMs = startTimeMs; + this.lane = lane; + } + } +} diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/niconicocomments/EmojiImageCache.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/niconicocomments/EmojiImageCache.java new file mode 100644 index 0000000000..2062be324c --- /dev/null +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/niconicocomments/EmojiImageCache.java @@ -0,0 +1,87 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/4080 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + * + * Niconico-style live/replay comments implementation is adapted from + * https://github.com/david419kr/niconico-yt-morphe-patches (GPLv3) + * Feature design is based on david419kr/niconico-yt (MIT License) + */ + +package app.morphe.extension.youtube.niconicocomments; + +import androidx.annotation.Nullable; + +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +/** Main-thread image load states; worker results are delivered through View.post(). */ +final class EmojiImageCache { + static final int MAX_ENTRIES = 128; + static final int MAX_QUEUED_LOADS = 32; + + private final Map> entries = new LinkedHashMap<>(16, 0.75f, true); + + static ThreadPoolExecutor createExecutor() { + return new ThreadPoolExecutor(2, 2, 0, TimeUnit.MILLISECONDS, + new ArrayBlockingQueue<>(MAX_QUEUED_LOADS), runnable -> { + Thread thread = new Thread(runnable, "NiconicoEmoji"); + thread.setDaemon(true); + return thread; + }); + } + + @Nullable + Entry get(String url) { + return entries.get(url); + } + + /** + * @return The existing or new entry, or null if the cache is full of entries that are still needed. + */ + @Nullable + Entry beginLoad(String url, Set neededUrls) { + Entry existing = entries.get(url); + if (existing != null) return existing; + + if (entries.size() >= MAX_ENTRIES) { + Iterator>> iterator = entries.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry> candidate = iterator.next(); + if (!candidate.getValue().loading && !neededUrls.contains(candidate.getKey())) { + iterator.remove(); + break; + } + } + if (entries.size() >= MAX_ENTRIES) return null; + } + Entry entry = new Entry<>(); + entries.put(url, entry); + return entry; + } + + boolean complete(String url, Entry entry, @Nullable T value) { + if (entries.get(url) != entry) return false; + entry.value = value; + entry.failed = value == null; + entry.loading = false; + return true; + } + + void clear() { + entries.clear(); + } + + static final class Entry { + @Nullable + T value; + boolean loading = true; + boolean failed; + } +} diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/niconicocomments/NiconicoComment.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/niconicocomments/NiconicoComment.java new file mode 100644 index 0000000000..15fee0eb1d --- /dev/null +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/niconicocomments/NiconicoComment.java @@ -0,0 +1,92 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/4080 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + * + * Niconico-style live/replay comments implementation is adapted from + * https://github.com/david419kr/niconico-yt-morphe-patches (GPLv3) + * Feature design is based on david419kr/niconico-yt (MIT License) + */ + +package app.morphe.extension.youtube.niconicocomments; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** Immutable subset of a YouTube live-chat item needed by the overlay. */ +final class NiconicoComment { + final String id; + final String text; + final List segments; + final boolean membership; + final int backgroundColor; + final long videoOffsetMs; + + NiconicoComment( + String id, + String text, + boolean membership, + int backgroundColor, + long videoOffsetMs + ) { + this( + id, + text, + Collections.singletonList(Segment.text(text)), + membership, + backgroundColor, + videoOffsetMs + ); + } + + NiconicoComment( + String id, + String text, + List segments, + boolean membership, + int backgroundColor, + long videoOffsetMs + ) { + this.id = id; + this.text = text; + this.segments = Collections.unmodifiableList(new ArrayList<>(segments)); + this.membership = membership; + this.backgroundColor = backgroundColor; + this.videoOffsetMs = videoOffsetMs; + } + + /** + * Number of characters shown, counting each emoji image as one character. + */ + int length() { + int length = 0; + for (Segment segment : segments) { + length += segment.isEmoji() ? 1 : segment.text.codePointCount(0, segment.text.length()); + } + return length; + } + + static final class Segment { + final String text; + final String imageUrl; + + private Segment(String text, String imageUrl) { + this.text = text; + this.imageUrl = imageUrl; + } + + static Segment text(String text) { + return new Segment(text, ""); + } + + static Segment emoji(String fallbackText, String imageUrl) { + return new Segment(fallbackText, imageUrl); + } + + boolean isEmoji() { + return !imageUrl.isEmpty(); + } + } +} diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/niconicocomments/NiconicoCommentsController.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/niconicocomments/NiconicoCommentsController.java new file mode 100644 index 0000000000..6464e28205 --- /dev/null +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/niconicocomments/NiconicoCommentsController.java @@ -0,0 +1,369 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/4080 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + * + * Niconico-style live/replay comments implementation is adapted from + * https://github.com/david419kr/niconico-yt-morphe-patches (GPLv3) + * Feature design is based on david419kr/niconico-yt (MIT License) + */ + +package app.morphe.extension.youtube.niconicocomments; + +import android.os.SystemClock; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.Nullable; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.List; + +import app.morphe.extension.shared.Logger; +import app.morphe.extension.shared.Utils; +import app.morphe.extension.shared.settings.preference.SeekBarPreference; +import app.morphe.extension.youtube.patches.VideoInformation; +import app.morphe.extension.youtube.settings.Settings; +import app.morphe.extension.youtube.shared.PlayerType; +import app.morphe.extension.youtube.shared.VideoState; + +/** + * Connects the player hooks, the chat source and the overlay view. + */ +@SuppressWarnings("unused") +public final class NiconicoCommentsController { + private static final long SEEK_THRESHOLD_MS = 12_000; + private static final Object SOURCE_LOCK = new Object(); + private static final Object VIDEO_CLOCK_LOCK = new Object(); + + private static WeakReference overlayViewRef = new WeakReference<>(null); + + private static volatile String currentVideoId = ""; + private static volatile boolean playerVisible; + private static volatile boolean videoPlaying; + // Main thread only. + private static boolean observersRegistered; + + // Guarded by VIDEO_CLOCK_LOCK. + private static long sampledVideoTimeMs = -1; + private static long sampleElapsedRealtimeMs; + private static double playbackRate = 1.0; + + // Guarded by SOURCE_LOCK. + @Nullable + private static YouTubeChatSource activeSource; + @Nullable + private static Thread activeSourceThread; + private static String completedSourceVideoId = ""; + + private static final YouTubeChatSource.Listener CHAT_LISTENER = new YouTubeChatSource.Listener() { + @Override + public long getVideoTimeMs() { + return getVideoTime(); + } + + @Override + public void onComments(String videoId, List comments, long liveSpreadWindowMs) { + NiconicoCommentsOverlayView view = overlayViewRef.get(); + if (view != null && isAcceptingComments(videoId, liveSpreadWindowMs)) { + List shownComments = removeLongComments(comments, + SeekBarPreference.clampToRange(Settings.NICONICO_COMMENTS_MAX_LENGTH)); + view.post(() -> { + if (isAcceptingComments(videoId, liveSpreadWindowMs)) { + view.addComments(shownComments, liveSpreadWindowMs); + } + }); + } + } + + @Override + public void onReplayReset(String videoId) { + NiconicoCommentsOverlayView view = overlayViewRef.get(); + if (view != null && videoId.equals(currentVideoId)) { + view.post(view::clearComments); + } + } + }; + + private NiconicoCommentsController() { + } + + private static boolean isAcceptingComments(String videoId, long liveSpreadWindowMs) { + return Settings.NICONICO_COMMENTS_SHOWN.get() && videoId.equals(currentVideoId) + && shouldAcceptComments(liveSpreadWindowMs, videoPlaying, playerVisible); + } + + static boolean shouldAcceptComments(long liveSpreadWindowMs, boolean playing, boolean visible) { + // Replay uses zero and must keep its lookahead even while paused or minimized. + return liveSpreadWindowMs <= 0 || (playing && visible); + } + + static List removeLongComments(List comments, int maxLength) { + if (maxLength >= Settings.NICONICO_COMMENTS_MAX_LENGTH_NO_LIMIT) { + return comments; + } + List shortComments = new ArrayList<>(comments.size()); + for (NiconicoComment comment : comments) { + if (comment.length() <= maxLength) { + shortComments.add(comment); + } + } + return shortComments; + } + + /** + * Injection point. + */ + public static void initialize(ViewGroup overlayRoot) { + if (!Settings.NICONICO_COMMENTS.get()) return; + + try { + if (!observersRegistered) { + observersRegistered = true; + PlayerType.getOnChange().addObserver(NiconicoCommentsController::setPlayerType); + VideoState.getOnChange().addObserver(NiconicoCommentsController::setVideoState); + VideoInformation.onPlaybackSpeedChange.addObserver(NiconicoCommentsController::setPlaybackRate); + setPlaybackRate(VideoInformation.getPlaybackSpeed()); + } + + NiconicoCommentsOverlayView view = new NiconicoCommentsOverlayView(overlayRoot.getContext()); + view.setVideoId(currentVideoId); + view.setClickable(false); + view.setFocusable(false); + view.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); + overlayRoot.addView(view, 0, new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + overlayViewRef = new WeakReference<>(view); + + setPlayerType(PlayerType.getCurrent()); + setVideoState(VideoState.getCurrent()); + } catch (Exception ex) { + Logger.printException(() -> "initialize failure", ex); + } + } + + /** + * Injection point. + */ + public static void newVideoLoaded(String videoId) { + if (!Settings.NICONICO_COMMENTS.get()) return; + + try { + if (videoId.equals(currentVideoId)) return; + + currentVideoId = videoId; + synchronized (VIDEO_CLOCK_LOCK) { + sampledVideoTimeMs = -1; + sampleElapsedRealtimeMs = SystemClock.elapsedRealtime(); + } + synchronized (SOURCE_LOCK) { + completedSourceVideoId = ""; + stopChatSourceLocked(); + } + + NiconicoCommentsOverlayView view = overlayViewRef.get(); + if (view != null) { + view.post(() -> { + view.setVideoId(videoId); + applyViewState(view); + maybeStartChatSource(); + }); + } + } catch (Exception ex) { + Logger.printException(() -> "newVideoLoaded failure", ex); + } + } + + /** + * Injection point. + */ + public static void setVideoTime(long videoTimeMs) { + if (!Settings.NICONICO_COMMENTS.get()) return; + + try { + final long nowMs = SystemClock.elapsedRealtime(); + final boolean seeked; + synchronized (VIDEO_CLOCK_LOCK) { + final long predictedVideoTimeMs = getVideoTimeLocked(nowMs); + seeked = predictedVideoTimeMs >= 0 + && Math.abs(videoTimeMs - predictedVideoTimeMs) > SEEK_THRESHOLD_MS; + sampledVideoTimeMs = videoTimeMs; + sampleElapsedRealtimeMs = nowMs; + } + + NiconicoCommentsOverlayView view = overlayViewRef.get(); + if (view != null) { + if (seeked) { + view.post(view::clearComments); + } else { + view.postInvalidateOnAnimation(); + } + } + } catch (Exception ex) { + Logger.printException(() -> "setVideoTime failure", ex); + } + } + + private static void setPlayerType(PlayerType type) { + playerVisible = type.isMaximizedOrFullscreen(); + updateViewState(); + maybeStartChatSource(); + } + + private static void setVideoState(@Nullable VideoState state) { + final boolean nextPlaying = state == VideoState.PLAYING; + final long nowMs = SystemClock.elapsedRealtime(); + synchronized (VIDEO_CLOCK_LOCK) { + if (videoPlaying && !nextPlaying) { + sampledVideoTimeMs = getVideoTimeLocked(nowMs); + } + sampleElapsedRealtimeMs = nowMs; + videoPlaying = nextPlaying; + } + updateViewState(); + } + + private static void setPlaybackRate(float rate) { + final long nowMs = SystemClock.elapsedRealtime(); + synchronized (VIDEO_CLOCK_LOCK) { + // Keep the position reached at the old rate. + if (sampledVideoTimeMs >= 0 && videoPlaying) { + sampledVideoTimeMs = getVideoTimeLocked(nowMs); + } + sampleElapsedRealtimeMs = nowMs; + playbackRate = sanitizePlaybackRate(rate); + } + + NiconicoCommentsOverlayView view = overlayViewRef.get(); + if (view != null) { + view.postInvalidateOnAnimation(); + } + } + + /** + * @return The interpolated video time, or -1 if no time is known for the current video. + */ + static long getVideoTime() { + final long nowMs = SystemClock.elapsedRealtime(); + synchronized (VIDEO_CLOCK_LOCK) { + return getVideoTimeLocked(nowMs); + } + } + + private static long getVideoTimeLocked(long nowMs) { + if (sampledVideoTimeMs < 0 || !videoPlaying) { + return sampledVideoTimeMs; + } + return interpolateVideoTime(sampledVideoTimeMs, + Math.max(0, nowMs - sampleElapsedRealtimeMs), playbackRate); + } + + static long interpolateVideoTime(long sampledTimeMs, long elapsedRealtimeMs, double rate) { + if (elapsedRealtimeMs <= 0) { + return sampledTimeMs; + } + + final double advancedVideoTimeMs = elapsedRealtimeMs * sanitizePlaybackRate(rate); + if (advancedVideoTimeMs >= Long.MAX_VALUE - sampledTimeMs) { + return Long.MAX_VALUE; + } + return sampledTimeMs + Math.round(advancedVideoTimeMs); + } + + static double sanitizePlaybackRate(double rate) { + return rate > 0.0 && !Double.isNaN(rate) && !Double.isInfinite(rate) ? rate : 1.0; + } + + /** + * Shows or hides the comments. + */ + static void toggleCommentsShown() { + final boolean shown = !Settings.NICONICO_COMMENTS_SHOWN.get(); + Settings.NICONICO_COMMENTS_SHOWN.save(shown); + + synchronized (SOURCE_LOCK) { + if (shown) { + // Retry a video whose source ended, such as after a network error. + completedSourceVideoId = ""; + } else { + stopChatSourceLocked(); + } + } + + NiconicoCommentsOverlayView view = overlayViewRef.get(); + if (view != null && !shown) { + view.clearComments(); + } + updateViewState(); + maybeStartChatSource(); + } + + /** + * Applies the current settings and player state to the overlay. + * Can be called from any thread. + */ + static void updateViewState() { + NiconicoCommentsOverlayView view = overlayViewRef.get(); + if (view != null) { + Utils.runOnMainThread(() -> applyViewState(view)); + } + } + + private static void applyViewState(NiconicoCommentsOverlayView view) { + view.setCommentFontSizeSp(SeekBarPreference.clampToRange(Settings.NICONICO_COMMENTS_FONT_SIZE)); + view.setCommentOpacityPercent(SeekBarPreference.clampToRange(Settings.NICONICO_COMMENTS_OPACITY)); + view.setTravelDurationSeconds(SeekBarPreference.clampToRange(Settings.NICONICO_COMMENTS_TRAVEL_DURATION)); + view.setOverlayActive(Settings.NICONICO_COMMENTS_SHOWN.get() + && playerVisible && !currentVideoId.isEmpty()); + view.setVideoPlaying(videoPlaying); + } + + /** + * Starts loading the chat of the current video, if it is shown in the full size player. + */ + private static void maybeStartChatSource() { + final String videoId = currentVideoId; + if (!Settings.NICONICO_COMMENTS_SHOWN.get() || !playerVisible + || videoId.isEmpty() || overlayViewRef.get() == null) { + return; + } + + synchronized (SOURCE_LOCK) { + if (videoId.equals(completedSourceVideoId) || activeSource != null) { + return; + } + + YouTubeChatSource source = new YouTubeChatSource(videoId, CHAT_LISTENER); + Thread thread = new Thread(() -> { + try { + source.run(); + } finally { + synchronized (SOURCE_LOCK) { + if (activeSource == source) { + activeSource = null; + activeSourceThread = null; + completedSourceVideoId = videoId; + } + } + } + }, "NiconicoYouTubeChat"); + thread.setDaemon(true); + activeSource = source; + activeSourceThread = thread; + thread.start(); + } + } + + private static void stopChatSourceLocked() { + if (activeSource != null) { + activeSource.stop(); + activeSource = null; + } + if (activeSourceThread != null) { + activeSourceThread.interrupt(); + activeSourceThread = null; + } + } +} diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/niconicocomments/NiconicoCommentsOverlayView.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/niconicocomments/NiconicoCommentsOverlayView.java new file mode 100644 index 0000000000..e8d881cf87 --- /dev/null +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/niconicocomments/NiconicoCommentsOverlayView.java @@ -0,0 +1,392 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/4080 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + * + * Niconico-style live/replay comments implementation is adapted from + * https://github.com/david419kr/niconico-yt-morphe-patches (GPLv3) + * Feature design is based on david419kr/niconico-yt (MIT License) + */ + +package app.morphe.extension.youtube.niconicocomments; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.RectF; +import android.graphics.Typeface; +import android.os.Build; +import android.view.View; + +import androidx.annotation.Nullable; + +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ThreadPoolExecutor; + +import app.morphe.extension.shared.Logger; +import app.morphe.extension.shared.requests.Requester; +import app.morphe.extension.shared.settings.preference.SeekBarPreference; +import app.morphe.extension.youtube.settings.Settings; + +/** + * Draws live and replay chat comments scrolling across the video. + */ +final class NiconicoCommentsOverlayView extends View { + private static final long IDLE_FRAME_DELAY_MS = 250; + private static final int MEMBERSHIP_COLOR = Color.rgb(43, 166, 64); + private static final long EMOJI_PREFETCH_MS = 1_000; + private static final int EMOJI_CONNECT_TIMEOUT_MS = 5_000; + private static final int EMOJI_READ_TIMEOUT_MS = 10_000; + private static final String EMOJI_USER_AGENT = + "Mozilla/5.0 (Linux; Android 14) AppleWebKit/537.36 Chrome/138 Mobile"; + private static final ThreadPoolExecutor EMOJI_EXECUTOR = EmojiImageCache.createExecutor(); + + private final Paint fillPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.SUBPIXEL_TEXT_FLAG); + private final Paint strokePaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.SUBPIXEL_TEXT_FLAG); + private final Paint backgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + private final Paint.FontMetrics fontMetrics = new Paint.FontMetrics(); + private final RectF emojiDestination = new RectF(); + private final CommentSchedule schedule = new CommentSchedule(); + private final EmojiImageCache emojiImageCache = new EmojiImageCache<>(); + private final Set neededEmojiUrls = new HashSet<>(); + private final float density; + private final float scaledDensity; + private final float backgroundPadding; + private final float backgroundRadius; + + private volatile int emojiGeneration; + private String videoId = ""; + private boolean overlayActive; + private boolean videoPlaying; + private boolean highFrameRateRequested; + private int fontSizeSp = SeekBarPreference.clampToRange(Settings.NICONICO_COMMENTS_FONT_SIZE); + private int opacityPercent = SeekBarPreference.clampToRange(Settings.NICONICO_COMMENTS_OPACITY); + private float laneHeight; + private float firstBaseline; + + NiconicoCommentsOverlayView(Context context) { + super(context); + + density = context.getResources().getDisplayMetrics().density; + scaledDensity = context.getResources().getDisplayMetrics().scaledDensity; + backgroundPadding = 4 * density; + backgroundRadius = 3 * density; + + fillPaint.setColor(Color.WHITE); + fillPaint.setStyle(Paint.Style.FILL); + fillPaint.setTypeface(Typeface.DEFAULT_BOLD); + + strokePaint.setColor(Color.BLACK); + strokePaint.setStyle(Paint.Style.STROKE); + strokePaint.setStrokeWidth(2 * density); + strokePaint.setTypeface(Typeface.DEFAULT_BOLD); + + applyFontSize(); + setTravelDurationSeconds(SeekBarPreference.clampToRange(Settings.NICONICO_COMMENTS_TRAVEL_DURATION)); + setWillNotDraw(false); + setVisibility(GONE); + } + + void setVideoId(String nextVideoId) { + if (nextVideoId.equals(videoId)) { + return; + } + videoId = nextVideoId; + emojiGeneration++; + emojiImageCache.clear(); + clearComments(); + } + + void setOverlayActive(boolean active) { + if (overlayActive == active) { + return; + } + overlayActive = active; + setVisibility(active ? VISIBLE : GONE); + requestNextFrame(); + } + + void setVideoPlaying(boolean playing) { + if (videoPlaying == playing) { + return; + } + videoPlaying = playing; + requestNextFrame(); + } + + void setCommentFontSizeSp(int nextFontSizeSp) { + if (fontSizeSp == nextFontSizeSp) { + return; + } + fontSizeSp = nextFontSizeSp; + applyFontSize(); + if (getHeight() > 0) { + schedule.redistributeLanes(getLaneCount()); + } + invalidate(); + } + + void setCommentOpacityPercent(int nextOpacityPercent) { + if (opacityPercent == nextOpacityPercent) { + return; + } + opacityPercent = nextOpacityPercent; + invalidate(); + } + + void setTravelDurationSeconds(int seconds) { + final long durationMs = seconds * 1000L; + if (schedule.getCommentDurationMs() == durationMs) { + return; + } + schedule.setCommentDurationMs(durationMs); + invalidate(); + } + + void addComments(List incoming, long liveSpreadWindowMs) { + if (incoming.isEmpty()) { + return; + } + long videoTimeMs = NiconicoCommentsController.getVideoTime(); + if (videoTimeMs < 0) { + return; + } + + schedule.addComments( + incoming, + videoTimeMs, + liveSpreadWindowMs, + getLaneCount() + ); + updateEmojiDemand(videoTimeMs); + requestNextFrame(); + } + + void clearComments() { + schedule.clear(); + neededEmojiUrls.clear(); + invalidate(); + } + + @Override + protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) { + super.onSizeChanged(width, height, oldWidth, oldHeight); + if (height > 0 && height != oldHeight) { + schedule.redistributeLanes(getLaneCount()); + requestNextFrame(); + } + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + if (!overlayActive || videoId.isEmpty()) { + setHighFrameRate(false); + return; + } + + long videoTimeMs = NiconicoCommentsController.getVideoTime(); + if (videoTimeMs < 0) { + setHighFrameRate(false); + if (videoPlaying) { + postInvalidateDelayed(IDLE_FRAME_DELAY_MS); + } + return; + } + + schedule.removeExpired(videoTimeMs); + updateEmojiDemand(videoTimeMs); + int laneCount = getLaneCount(); + boolean hasActiveOrUpcomingComment = false; + boolean hasVisibleComment = false; + for (CommentSchedule.ScheduledComment scheduled : schedule.comments()) { + long elapsedMs = videoTimeMs - scheduled.startTimeMs; + if (elapsedMs < 0) { + hasActiveOrUpcomingComment = true; + continue; + } + + hasActiveOrUpcomingComment = true; + hasVisibleComment = true; + NiconicoComment comment = scheduled.comment; + float commentWidth = measureCommentWidth(comment); + float progress = (float) elapsedMs / schedule.getCommentDurationMs(); + float x = getWidth() - (progress * (getWidth() + commentWidth)); + float y = firstBaseline + (Math.floorMod(scheduled.lane, laneCount) * laneHeight); + + if (comment.backgroundColor != Color.TRANSPARENT) { + backgroundPaint.setColor(applyOpacity(comment.backgroundColor)); + canvas.drawRoundRect( + x - backgroundPadding, + y + fontMetrics.ascent - backgroundPadding, + x + commentWidth + backgroundPadding, + y + fontMetrics.descent + backgroundPadding, + backgroundRadius, + backgroundRadius, + backgroundPaint + ); + } + + fillPaint.setColor(applyOpacity( + comment.membership ? MEMBERSHIP_COLOR : Color.WHITE + )); + strokePaint.setColor(applyOpacity(Color.BLACK)); + drawCommentSegments(canvas, comment, x, y); + } + + setHighFrameRate(videoPlaying && hasVisibleComment); + if (videoPlaying && hasActiveOrUpcomingComment) { + postInvalidateOnAnimation(); + } + } + + /** + * The display can lower its refresh rate to match the video frame rate (often 30 Hz), + * which makes the scrolling comments choppy. Request a high refresh rate only while + * comments are moving across the screen. + */ + private void setHighFrameRate(boolean high) { + if (highFrameRateRequested == high || Build.VERSION.SDK_INT < Build.VERSION_CODES.VANILLA_ICE_CREAM) { + return; + } + highFrameRateRequested = high; + setRequestedFrameRate(high + ? REQUESTED_FRAME_RATE_CATEGORY_HIGH + : REQUESTED_FRAME_RATE_CATEGORY_DEFAULT); + } + + private void applyFontSize() { + float textSize = fontSizeSp * scaledDensity; + fillPaint.setTextSize(textSize); + strokePaint.setTextSize(textSize); + fillPaint.getFontMetrics(fontMetrics); + laneHeight = fontMetrics.descent - fontMetrics.ascent + (8 * density); + firstBaseline = -fontMetrics.ascent + (12 * density); + } + + private int getLaneCount() { + return Math.max(1, (int) ((getHeight() - firstBaseline) / laneHeight) + 1); + } + + private int applyOpacity(int color) { + int adjustedAlpha = Math.round(Color.alpha(color) * (opacityPercent / 100f)); + return Color.argb( + adjustedAlpha, + Color.red(color), + Color.green(color), + Color.blue(color) + ); + } + + private float measureCommentWidth(NiconicoComment comment) { + float width = 0; + for (NiconicoComment.Segment segment : comment.segments) { + width += segment.isEmoji() ? fillPaint.getTextSize() : fillPaint.measureText(segment.text); + } + return width; + } + + private void drawCommentSegments( + Canvas canvas, + NiconicoComment comment, + float startX, + float baseline + ) { + float x = startX; + for (NiconicoComment.Segment segment : comment.segments) { + if (!segment.isEmoji()) { + canvas.drawText(segment.text, x, baseline, strokePaint); + canvas.drawText(segment.text, x, baseline, fillPaint); + x += fillPaint.measureText(segment.text); + continue; + } + + float emojiSize = fillPaint.getTextSize(); + EmojiImageCache.Entry state = ensureEmojiImage(segment.imageUrl); + if (state != null && state.value != null) { + float centerY = baseline + ((fontMetrics.ascent + fontMetrics.descent) / 2); + emojiDestination.set( + x, + centerY - (emojiSize / 2), + x + emojiSize, + centerY + (emojiSize / 2) + ); + canvas.drawBitmap(state.value, null, emojiDestination, fillPaint); + } else if (state == null || state.failed) { + canvas.drawText("□", x, baseline, strokePaint); + canvas.drawText("□", x, baseline, fillPaint); + } + x += emojiSize; + } + } + + private void updateEmojiDemand(long videoTimeMs) { + neededEmojiUrls.clear(); + for (CommentSchedule.ScheduledComment scheduled : schedule.comments()) { + if (scheduled.startTimeMs > videoTimeMs + EMOJI_PREFETCH_MS) continue; + for (NiconicoComment.Segment segment : scheduled.comment.segments) { + if (segment.isEmoji()) neededEmojiUrls.add(segment.imageUrl); + } + } + for (String url : neededEmojiUrls) ensureEmojiImage(url); + } + + @Nullable + private EmojiImageCache.Entry ensureEmojiImage(String imageUrl) { + EmojiImageCache.Entry existing = emojiImageCache.get(imageUrl); + if (existing != null) return existing; + // Loads are only queued from the main thread, so the queue cannot fill up after this check. + if (!overlayActive || !videoPlaying || EMOJI_EXECUTOR.getQueue().remainingCapacity() == 0) return null; + + EmojiImageCache.Entry state = emojiImageCache.beginLoad(imageUrl, neededEmojiUrls); + if (state == null) return null; + final int generation = emojiGeneration; + EMOJI_EXECUTOR.execute(() -> loadEmojiImage(imageUrl, state, generation)); + return state; + } + + private void loadEmojiImage(String imageUrl, EmojiImageCache.Entry state, int generation) { + if (generation != emojiGeneration) return; + HttpURLConnection connection = null; + Bitmap bitmap = null; + try { + connection = Requester.openConnection(imageUrl); + connection.setConnectTimeout(EMOJI_CONNECT_TIMEOUT_MS); + connection.setReadTimeout(EMOJI_READ_TIMEOUT_MS); + connection.setInstanceFollowRedirects(true); + connection.setRequestProperty("User-Agent", EMOJI_USER_AGENT); + if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) { + try (InputStream input = connection.getInputStream()) { + bitmap = BitmapFactory.decodeStream(input); + } + } + } catch (Exception ex) { + Logger.printInfo(() -> "Could not load emoji: " + imageUrl, ex); + } finally { + if (connection != null) { + connection.disconnect(); + } + } + + Bitmap loadedBitmap = bitmap; + post(() -> { + if (generation == emojiGeneration && emojiImageCache.complete(imageUrl, state, loadedBitmap)) { + requestNextFrame(); + } + }); + } + + private void requestNextFrame() { + if (overlayActive) { + postInvalidateOnAnimation(); + } + } +} diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/niconicocomments/NiconicoCommentsToggleButton.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/niconicocomments/NiconicoCommentsToggleButton.java new file mode 100644 index 0000000000..6ee357d2e7 --- /dev/null +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/niconicocomments/NiconicoCommentsToggleButton.java @@ -0,0 +1,192 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/4080 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + * + * Niconico-style live/replay comments implementation is adapted from + * https://github.com/david419kr/niconico-yt-morphe-patches (GPLv3) + * Feature design is based on david419kr/niconico-yt (MIT License) + */ + +package app.morphe.extension.youtube.niconicocomments; + +import static app.morphe.extension.shared.StringRef.str; + +import android.content.Context; +import android.view.View; +import android.widget.ImageView; +import android.widget.LinearLayout; + +import androidx.annotation.Nullable; + +import java.lang.ref.WeakReference; + +import app.morphe.extension.shared.Logger; +import app.morphe.extension.shared.Utils; +import app.morphe.extension.shared.theme.ThemeUtils; +import app.morphe.extension.shared.ui.Dim; +import app.morphe.extension.shared.ui.SheetBottomDialog; +import app.morphe.extension.youtube.patches.LegacyPlayerControlsPatch; +import app.morphe.extension.youtube.settings.Settings; +import app.morphe.extension.youtube.shared.PipDismissHelper; +import app.morphe.extension.youtube.videoplayer.LegacyPlayerControlButton; +import app.morphe.extension.youtube.videoplayer.PlayerIcons; +import app.morphe.extension.youtube.videoplayer.PlayerOverlayButton; + +/** + * Player button that shows or hides the comments. Tap and hold opens the quick settings. + */ +@SuppressWarnings("unused") +public final class NiconicoCommentsToggleButton { + + @Nullable + private static LegacyPlayerControlButton legacy; + + private static WeakReference overlayButtonRef = new WeakReference<>(null); + + private NiconicoCommentsToggleButton() { + } + + /** + * Injection point. + */ + public static void initializeButton(View controlsView) { + try { + if (LegacyPlayerControlsPatch.RESTORE_OLD_PLAYER_BUTTONS || !Settings.NICONICO_COMMENTS.get()) { + return; + } + + ImageView button = PlayerOverlayButton.addButton( + controlsView, + PlayerIcons.name(getIconBaseName()), + getOnClickListener(), + getOnLongClickListener() + ); + if (button != null) { + button.setContentDescription(str("morphe_niconico_comments_screen_title")); + PlayerOverlayButton.setToggleAccessibilityDelegate( + button, Settings.NICONICO_COMMENTS_SHOWN::get); + } + overlayButtonRef = new WeakReference<>(button); + } catch (Exception ex) { + Logger.printException(() -> "initializeButton failure", ex); + } + } + + /** + * Injection point. + */ + public static void initializeLegacyButton(View controlsView) { + try { + if (!LegacyPlayerControlsPatch.RESTORE_OLD_PLAYER_BUTTONS) { + return; + } + + legacy = new LegacyPlayerControlButton( + controlsView, + "morphe_niconico_comments_button", + null, + PlayerIcons.name(getIconBaseName()), + Settings.NICONICO_COMMENTS, + getOnClickListener(), + getOnLongClickListener() + ); + View legacyButton = Utils.getChildViewByResourceName(controlsView, "morphe_niconico_comments_button"); + PlayerOverlayButton.setToggleAccessibilityDelegate( + legacyButton, Settings.NICONICO_COMMENTS_SHOWN::get); + } catch (Exception ex) { + Logger.printException(() -> "initializeLegacyButton failure", ex); + } + } + + private static View.OnClickListener getOnClickListener() { + return view -> { + NiconicoCommentsController.toggleCommentsShown(); + updateButtonIcon(); + }; + } + + private static View.OnLongClickListener getOnLongClickListener() { + return view -> { + showQuickSettings(view.getContext()); + return true; + }; + } + + private static void updateButtonIcon() { + Utils.verifyOnMainThread(); + + final int icon = PlayerIcons.id(getIconBaseName()); + + ImageView overlayButton = overlayButtonRef.get(); + if (overlayButton != null) { + overlayButton.setImageResource(icon); + } + + if (legacy != null) { + legacy.setIcon(icon); + } + } + + private static String getIconBaseName() { + return Settings.NICONICO_COMMENTS_SHOWN.get() + ? "morphe_niconico_comments_button_on" + : "morphe_niconico_comments_button_off"; + } + + /** + * Shows the font size, opacity, travel duration and max length sliders. Changes apply immediately. + */ + private static void showQuickSettings(Context context) { + try { + final int foregroundColor = ThemeUtils.getAppForegroundColor(); + + SheetBottomDialog.DraggableLinearLayout mainLayout = SheetBottomDialog.createMainLayout( + context, LegacyPlayerControlButton.getDialogBackgroundColor()); + + LinearLayout contentLayout = new LinearLayout(context); + contentLayout.setOrientation(LinearLayout.VERTICAL); + contentLayout.setPadding(Dim.dp16, 0, Dim.dp16, Dim.dp16); + contentLayout.addView(SheetBottomDialog.createTitle(context, + str("morphe_niconico_comments_screen_title"), foregroundColor)); + contentLayout.addView(SheetBottomDialog.createSliderRow(context, + str("morphe_niconico_comments_font_size_title"), + Settings.NICONICO_COMMENTS_FONT_SIZE, + foregroundColor, + value -> { + Settings.NICONICO_COMMENTS_FONT_SIZE.save(value); + NiconicoCommentsController.updateViewState(); + })); + contentLayout.addView(SheetBottomDialog.createSliderRow(context, + str("morphe_niconico_comments_opacity_title"), + Settings.NICONICO_COMMENTS_OPACITY, + foregroundColor, + value -> { + Settings.NICONICO_COMMENTS_OPACITY.save(value); + NiconicoCommentsController.updateViewState(); + })); + contentLayout.addView(SheetBottomDialog.createSliderRow(context, + str("morphe_niconico_comments_travel_duration_title"), + Settings.NICONICO_COMMENTS_TRAVEL_DURATION, + foregroundColor, + value -> { + Settings.NICONICO_COMMENTS_TRAVEL_DURATION.save(value); + NiconicoCommentsController.updateViewState(); + })); + contentLayout.addView(SheetBottomDialog.createSliderRow(context, + str("morphe_niconico_comments_max_length_title"), + Settings.NICONICO_COMMENTS_MAX_LENGTH, + foregroundColor, + Settings.NICONICO_COMMENTS_MAX_LENGTH::save)); + mainLayout.addView(contentLayout); + + SheetBottomDialog.SlideDialog dialog = SheetBottomDialog.createSlideDialog( + context, mainLayout, LegacyPlayerControlButton.fadeInDuration); + PipDismissHelper.dismissOnPip(dialog); + dialog.show(); + } catch (Exception ex) { + Logger.printException(() -> "showQuickSettings failure", ex); + } + } +} diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/niconicocomments/YouTubeChatSource.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/niconicocomments/YouTubeChatSource.java new file mode 100644 index 0000000000..b3ca33ec7c --- /dev/null +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/niconicocomments/YouTubeChatSource.java @@ -0,0 +1,1121 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/4080 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + * + * Niconico-style live/replay comments implementation is adapted from + * https://github.com/david419kr/niconico-yt-morphe-patches (GPLv3) + * Feature design is based on david419kr/niconico-yt (MIT License) + */ + +package app.morphe.extension.youtube.niconicocomments; + +import androidx.annotation.Nullable; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; + +import app.morphe.extension.shared.Logger; +import app.morphe.extension.shared.requests.Requester; + +/** + * Fetches the public live chat or chat replay of a video. + */ +final class YouTubeChatSource implements Runnable { + interface Listener { + long getVideoTimeMs(); + + /** Positive spread for live batches; zero for replay lookahead. */ + void onComments( + String videoId, + List comments, + long liveSpreadWindowMs + ); + + void onReplayReset(String videoId); + } + + private static final String YOUTUBE_ORIGIN = "https://www.youtube.com"; + private static final String USER_AGENT = + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " + + "AppleWebKit/537.36 (KHTML, like Gecko) " + + "Chrome/138.0.0.0 Safari/537.36"; + private static final int CONNECT_TIMEOUT_MS = 10_000; + private static final int READ_TIMEOUT_MS = 20_000; + private static final int DEFAULT_CONTINUATION_TIMEOUT_MS = 5_000; + private static final int LIVE_POLL_MIN_DELAY_MS = 100; + private static final int LIVE_POLL_MAX_DELAY_MS = 250; + private static final int REPLAY_LOOP_DELAY_MS = 1_000; + private static final int INITIAL_DISPLAY_LIMIT = 30; + private static final long REPLAY_LOOKAHEAD_MS = 45_000; + private static final long REPLAY_SEEK_THRESHOLD_MS = 12_000; + private static final long REPLAY_START_MARGIN_MS = 5_000; + private static final int REPLAY_MAX_FETCHES_PER_UPDATE = 12; + private static final int MAX_COMMENT_LENGTH = 200; + private static final int MAX_SEEN_IDS = 4_096; + + private final String videoId; + private final Listener listener; + private final Set seenIds = new LinkedHashSet<>(); + private final ArrayDeque seenIdOrder = new ArrayDeque<>(); + + private volatile boolean stopped; + @Nullable + private volatile HttpURLConnection activeConnection; + + YouTubeChatSource(String videoId, Listener listener) { + this.videoId = videoId; + this.listener = listener; + } + + void stop() { + stopped = true; + HttpURLConnection connection = activeConnection; + if (connection != null) { + connection.disconnect(); + } + } + + @Override + public void run() { + Logger.printDebug(() -> "Starting public live/replay chat source for " + videoId); + + PageState state = null; + for (int attempt = 1; attempt <= 3 && !stopped; attempt++) { + try { + state = fetchPageState(); + break; + } catch (Exception exception) { + String message = "Chat initialization attempt " + attempt + " failed"; + Logger.printInfo(() -> message, exception); + sleepBeforeRetry(attempt); + } + } + + if (stopped) { + return; + } + if (state == null || state.continuation == null) { + Logger.printDebug(() -> "No public live/replay continuation found for " + videoId); + return; + } + + if (state.replay) { + runReplay(state); + } else { + runLive(state); + } + } + + private void runLive(PageState state) { + boolean firstPage = true; + int consecutiveFailures = 0; + Continuation continuation = state.continuation; + while (!stopped && continuation != null) { + try { + ChatPage page = fetchChatPage(state, continuation.value); + consecutiveFailures = 0; + + List comments = deduplicate(page.comments); + if (firstPage && comments.size() > INITIAL_DISPLAY_LIMIT) { + comments = new ArrayList<>( + comments.subList(comments.size() - INITIAL_DISPLAY_LIMIT, comments.size()) + ); + } + firstPage = false; + + Continuation nextContinuation = page.continuation; + if (!comments.isEmpty() && !stopped) { + long spreadWindowMs = getLivePollDelayMs( + nextContinuation == null + ? DEFAULT_CONTINUATION_TIMEOUT_MS + : nextContinuation.timeoutMs + ); + listener.onComments(videoId, comments, spreadWindowMs); + int commentCount = comments.size(); + Logger.printDebug(() -> "Received " + commentCount + " live-chat comments"); + } + + continuation = nextContinuation; + if (continuation == null) { + Logger.printDebug(() -> "Live-chat continuation ended for " + videoId); + break; + } + sleep(getLivePollDelayMs(continuation.timeoutMs)); + } catch (Exception exception) { + if (stopped) { + return; + } + consecutiveFailures++; + String message = "Live-chat request failed (" + consecutiveFailures + "/3)"; + Logger.printInfo(() -> message, exception); + if (consecutiveFailures >= 3) { + break; + } + sleepBeforeRetry(consecutiveFailures); + } + } + } + + private void runReplay(PageState state) { + Logger.printDebug(() -> "Replay-chat source selected for " + videoId); + Continuation initialContinuation = state.continuation; + Continuation continuation = initialContinuation; + long loadedUntilMs = -1; + long windowStartMs = 0; + long lastVideoTimeMs = -1; + boolean hasFetched = false; + boolean done = false; + int consecutiveFailures = 0; + + while (!stopped) { + long videoTimeMs = listener.getVideoTimeMs(); + if (videoTimeMs < 0) { + sleep(REPLAY_LOOP_DELAY_MS); + continue; + } + + boolean seeked = hasFetched + && lastVideoTimeMs >= 0 + && Math.abs(videoTimeMs - lastVideoTimeMs) > REPLAY_SEEK_THRESHOLD_MS; + boolean outsideWindow = hasFetched + && (videoTimeMs < windowStartMs + || videoTimeMs > loadedUntilMs + REPLAY_SEEK_THRESHOLD_MS); + if (seeked || outsideWindow) { + continuation = initialContinuation; + windowStartMs = Math.max(0, videoTimeMs - REPLAY_START_MARGIN_MS); + loadedUntilMs = windowStartMs; + hasFetched = false; + done = false; + consecutiveFailures = 0; + clearSeenIds(); + listener.onReplayReset(videoId); + Logger.printDebug(() -> "Replay-chat window reset at " + videoTimeMs + " ms"); + } + if (loadedUntilMs < 0) { + windowStartMs = Math.max(0, videoTimeMs - REPLAY_START_MARGIN_MS); + loadedUntilMs = windowStartMs; + } + lastVideoTimeMs = videoTimeMs; + + long targetMs = videoTimeMs + REPLAY_LOOKAHEAD_MS; + int fetches = 0; + while (!stopped + && !done + && continuation != null + && loadedUntilMs < targetMs + && fetches < REPLAY_MAX_FETCHES_PER_UPDATE) { + fetches++; + long playerOffsetMs = Math.max( + 0, + hasFetched ? loadedUntilMs - REPLAY_START_MARGIN_MS : windowStartMs + ); + String previousContinuation = continuation.value; + long previousLoadedUntilMs = loadedUntilMs; + try { + ChatPage page = fetchReplayChatPage( + state, + continuation.value, + playerOffsetMs + ); + consecutiveFailures = 0; + hasFetched = true; + + List comments = deduplicate(page.comments); + if (!comments.isEmpty() && !stopped) { + listener.onComments(videoId, comments, 0); + Logger.printDebug(() -> "Received " + comments.size() + " replay-chat comments"); + } + if (page.maxVideoOffsetMs >= 0) { + loadedUntilMs = Math.max(loadedUntilMs, page.maxVideoOffsetMs); + } + + continuation = page.continuation; + if (continuation == null + || (previousContinuation.equals(continuation.value) + && loadedUntilMs <= previousLoadedUntilMs)) { + done = true; + } + if (page.comments.isEmpty() && page.maxVideoOffsetMs < 0) { + break; + } + } catch (Exception exception) { + if (stopped) { + return; + } + consecutiveFailures++; + String message = "Replay-chat request failed (" + consecutiveFailures + "/3)"; + Logger.printInfo(() -> message, exception); + if (consecutiveFailures >= 3) { + done = true; + } else { + sleepBeforeRetry(consecutiveFailures); + } + break; + } + } + sleep(REPLAY_LOOP_DELAY_MS); + } + } + + private PageState fetchPageState() throws IOException, JSONException { + String watchUrl = YOUTUBE_ORIGIN + "/watch?v=" + + URLEncoder.encode(videoId, StandardCharsets.UTF_8.name()) + + "&hl=en&bpctr=9999999999&has_verified=1"; + String html = request(watchUrl, null, null, null); + + JSONObject config = parseYoutubeConfig(html); + JSONObject context = config.optJSONObject("INNERTUBE_CONTEXT"); + String apiKey = config.optString("INNERTUBE_API_KEY", ""); + + JSONObject client = context == null ? null : context.optJSONObject("client"); + String clientVersion = client == null ? "" : client.optString("clientVersion", ""); + if (clientVersion.isEmpty()) { + clientVersion = config.optString("INNERTUBE_CLIENT_VERSION", ""); + } + if (apiKey.isEmpty() || clientVersion.isEmpty()) { + throw new JSONException("Watch page did not contain an Innertube API key and client version"); + } + + if (context == null) { + context = new JSONObject(); + client = new JSONObject(); + client.put("clientName", "WEB"); + client.put("clientVersion", clientVersion); + String visitorData = config.optString("VISITOR_DATA", ""); + if (!visitorData.isEmpty()) { + client.put("visitorData", visitorData); + } + context.put("client", client); + context.put("request", new JSONObject().put("useSsl", true)); + } + + JSONObject initialData = parseMarkedObject( + html, + "var ytInitialData =", + "ytInitialData =", + "window[\"ytInitialData\"] =" + ); + if (initialData == null) { + throw new JSONException("Watch page did not contain ytInitialData"); + } + + JSONObject playerResponse = parseMarkedObject( + html, + "var ytInitialPlayerResponse =", + "ytInitialPlayerResponse =" + ); + JSONObject liveChatRenderer = getObjectPath( + initialData, + "contents", + "twoColumnWatchNextResults", + "conversationBar", + "liveChatRenderer" + ); + Object continuations = liveChatRenderer == null + ? null + : liveChatRenderer.opt("continuations"); + Continuation replayContinuation = findContinuation( + continuations, + "playerSeekContinuationData", + "liveChatReplayContinuationData" + ); + if (replayContinuation == null && isEndedLiveBroadcast(playerResponse)) { + replayContinuation = findContinuation( + initialData, + "playerSeekContinuationData", + "liveChatReplayContinuationData", + "reloadContinuationData" + ); + } + + if (liveChatRenderer == null) { + return new PageState(apiKey, clientVersion, context, null, false); + } + if (replayContinuation != null) { + try { + Continuation allMessages = fetchAllMessagesReplayContinuation( + replayContinuation.value + ); + if (allMessages != null) { + replayContinuation = allMessages; + } + } catch (Exception exception) { + Logger.printInfo(() -> "Could not select the all-messages replay filter", exception); + } + + return new PageState( + apiKey, + clientVersion, + context, + replayContinuation, + true + ); + } + + Continuation continuation = findContinuation( + continuations, + "timedContinuationData", + "invalidationContinuationData", + "reloadContinuationData" + ); + return new PageState(apiKey, clientVersion, context, continuation, false); + } + + private Continuation fetchAllMessagesReplayContinuation(String continuation) + throws IOException, JSONException { + String url = YOUTUBE_ORIGIN + "/live_chat_replay?continuation=" + + URLEncoder.encode(continuation, StandardCharsets.UTF_8.name()) + + "&hl=en"; + String html = request(url, null, null, null); + JSONObject initialData = parseMarkedObject( + html, + "var ytInitialData =", + "ytInitialData =", + "window[\"ytInitialData\"] =" + ); + if (initialData == null) { + return null; + } + + JSONArray items = getReplayFilterItems(initialData); + if (items == null) { + return null; + } + + int selectedTopIndex = -1; + for (int index = 0; index < items.length(); index++) { + JSONObject item = items.optJSONObject(index); + if (item == null) { + continue; + } + String text = getFilterItemText(item).toLowerCase(Locale.ROOT); + boolean topChat = text.contains("top chat") || text.contains("some messages"); + boolean allMessages = !topChat + && (text.contains("live chat replay") || text.contains("all messages")); + if (allMessages) { + return item.optBoolean("selected", false) + ? null + : findContinuation( + item.opt("continuation"), + "reloadContinuationData", + "liveChatReplayContinuationData", + "playerSeekContinuationData" + ); + } + if (topChat && item.optBoolean("selected", false)) { + selectedTopIndex = index; + } + } + + if (items.length() == 2 && selectedTopIndex >= 0) { + JSONObject other = items.optJSONObject(selectedTopIndex == 0 ? 1 : 0); + return other == null + ? null + : findContinuation( + other.opt("continuation"), + "reloadContinuationData", + "liveChatReplayContinuationData", + "playerSeekContinuationData" + ); + } + return null; + } + + private ChatPage fetchChatPage(PageState state, String continuation) + throws IOException, JSONException { + String endpoint = YOUTUBE_ORIGIN + "/youtubei/v1/live_chat/get_live_chat?key=" + + URLEncoder.encode(state.apiKey, StandardCharsets.UTF_8.name()) + + "&prettyPrint=false"; + + JSONObject body = new JSONObject(); + body.put("context", state.context); + body.put("continuation", continuation); + + String responseText = request( + endpoint, + body.toString(), + state.clientVersion, + getVisitorData(state.context) + ); + JSONObject response = new JSONObject(stripJsonPrefix(responseText)); + JSONObject liveChatContinuation = getLiveChatContinuation(response); + if (liveChatContinuation == null) { + return new ChatPage(new ArrayList<>(), null, -1); + } + + List comments = parseComments(liveChatContinuation.optJSONArray("actions")); + Continuation next = findContinuation( + liveChatContinuation.opt("continuations"), + "timedContinuationData", + "invalidationContinuationData", + "reloadContinuationData" + ); + return new ChatPage(comments, next, -1); + } + + private ChatPage fetchReplayChatPage( + PageState state, + String continuation, + long playerOffsetMs + ) throws IOException, JSONException { + String endpoint = YOUTUBE_ORIGIN + "/youtubei/v1/live_chat/get_live_chat_replay?key=" + + URLEncoder.encode(state.apiKey, StandardCharsets.UTF_8.name()) + + "&prettyPrint=false"; + + JSONObject body = new JSONObject(); + body.put("context", state.context); + body.put("continuation", continuation); + body.put( + "currentPlayerState", + new JSONObject().put("playerOffsetMs", Long.toString(playerOffsetMs)) + ); + + String responseText = request( + endpoint, + body.toString(), + state.clientVersion, + getVisitorData(state.context) + ); + JSONObject response = new JSONObject(stripJsonPrefix(responseText)); + JSONObject liveChatContinuation = getLiveChatContinuation(response); + if (liveChatContinuation == null) { + return new ChatPage(new ArrayList<>(), null, -1); + } + + ReplayParseResult parsed = parseReplayComments( + liveChatContinuation.optJSONArray("actions") + ); + Continuation next = findContinuation( + liveChatContinuation.opt("continuations"), + "liveChatReplayContinuationData", + "playerSeekContinuationData", + "timedContinuationData", + "invalidationContinuationData", + "reloadContinuationData" + ); + return new ChatPage(parsed.comments, next, parsed.maxVideoOffsetMs); + } + + private String request( + String url, + @Nullable String requestBody, + @Nullable String clientVersion, + @Nullable String visitorData + ) throws IOException { + HttpURLConnection connection = Requester.openConnection(url); + activeConnection = connection; + try { + connection.setConnectTimeout(CONNECT_TIMEOUT_MS); + connection.setReadTimeout(READ_TIMEOUT_MS); + connection.setInstanceFollowRedirects(true); + connection.setUseCaches(false); + connection.setRequestProperty("User-Agent", USER_AGENT); + connection.setRequestProperty("Accept-Language", "en-US,en;q=0.9"); + + if (requestBody != null) { + connection.setRequestMethod("POST"); + connection.setDoOutput(true); + connection.setRequestProperty("Content-Type", "application/json"); + connection.setRequestProperty("Origin", YOUTUBE_ORIGIN); + connection.setRequestProperty("X-Youtube-Client-Name", "1"); + connection.setRequestProperty("X-Youtube-Client-Version", clientVersion); + if (visitorData != null && !visitorData.isEmpty()) { + connection.setRequestProperty("X-Goog-Visitor-Id", visitorData); + } + byte[] bytes = requestBody.getBytes(StandardCharsets.UTF_8); + connection.setFixedLengthStreamingMode(bytes.length); + try (OutputStream output = connection.getOutputStream()) { + output.write(bytes); + } + } + + final int status = connection.getResponseCode(); + if (status != HttpURLConnection.HTTP_OK) { + connection.disconnect(); + throw new IOException("YouTube returned HTTP " + status); + } + // Not disconnected, so the live chat polling can reuse the connection. + return Requester.parseString(connection); + } finally { + activeConnection = null; + } + } + + private List deduplicate(List comments) { + List result = new ArrayList<>(); + for (NiconicoComment comment : comments) { + if (comment.id != null && !comment.id.isEmpty()) { + if (!seenIds.add(comment.id)) { + continue; + } + seenIdOrder.addLast(comment.id); + while (seenIdOrder.size() > MAX_SEEN_IDS) { + seenIds.remove(seenIdOrder.removeFirst()); + } + } + result.add(comment); + } + return result; + } + + private void clearSeenIds() { + seenIds.clear(); + seenIdOrder.clear(); + } + + static JSONObject parseYoutubeConfig(String html) throws JSONException { + JSONObject result = new JSONObject(); + int searchIndex = 0; + while (searchIndex < html.length()) { + int markerIndex = html.indexOf("ytcfg.set(", searchIndex); + if (markerIndex < 0) { + break; + } + JSONObject value = parseObjectAt(html, markerIndex + "ytcfg.set(".length()); + if (value != null) { + Iterator keys = value.keys(); + while (keys.hasNext()) { + String key = keys.next(); + result.put(key, value.opt(key)); + } + } + searchIndex = markerIndex + "ytcfg.set(".length(); + } + return result; + } + + static JSONObject parseMarkedObject(String text, String... markers) { + for (String marker : markers) { + int markerIndex = text.indexOf(marker); + if (markerIndex >= 0) { + JSONObject value = parseObjectAt(text, markerIndex + marker.length()); + if (value != null) { + return value; + } + } + } + return null; + } + + static JSONObject parseObjectAt(String text, int startIndex) { + int start = text.indexOf('{', startIndex); + if (start < 0) { + return null; + } + + int depth = 0; + boolean inString = false; + boolean escaped = false; + for (int index = start; index < text.length(); index++) { + char value = text.charAt(index); + if (inString) { + if (escaped) { + escaped = false; + } else if (value == '\\') { + escaped = true; + } else if (value == '"') { + inString = false; + } + continue; + } + + if (value == '"') { + inString = true; + } else if (value == '{') { + depth++; + } else if (value == '}') { + depth--; + if (depth == 0) { + try { + return new JSONObject(text.substring(start, index + 1)); + } catch (JSONException ex) { + Logger.printInfo(() -> "Could not parse embedded JSON", ex); + return null; + } + } + } + } + return null; + } + + private static JSONObject getObjectPath(JSONObject root, String... path) { + JSONObject value = root; + for (String key : path) { + value = value.optJSONObject(key); + if (value == null) { + return null; + } + } + return value; + } + + private static JSONArray getReplayFilterItems(JSONObject data) { + JSONObject liveChat = getLiveChatContinuation(data); + if (liveChat == null) { + liveChat = getObjectPath( + data, + "contents", + "twoColumnWatchNextResults", + "conversationBar", + "liveChatRenderer" + ); + } + JSONObject header = liveChat == null + ? null + : liveChat.optJSONObject("header"); + JSONObject headerRenderer = header == null + ? null + : header.optJSONObject("liveChatHeaderRenderer"); + JSONObject viewSelector = headerRenderer == null + ? null + : headerRenderer.optJSONObject("viewSelector"); + JSONObject menu = viewSelector == null + ? null + : viewSelector.optJSONObject("sortFilterSubMenuRenderer"); + return menu == null ? null : menu.optJSONArray("subMenuItems"); + } + + private static String getFilterItemText(JSONObject item) { + return extractRendererText(item.opt("title")) + + " " + extractRendererText(item.opt("accessibility")) + + " " + extractRendererText(item.opt("subtitle")); + } + + private static String extractRendererText(Object value) { + if (value instanceof String) { + return (String) value; + } + if (!(value instanceof JSONObject)) { + return ""; + } + JSONObject object = (JSONObject) value; + String text = extractText(object); + if (!text.isEmpty()) { + return text; + } + JSONObject accessibilityData = object.optJSONObject("accessibilityData"); + return accessibilityData == null + ? "" + : accessibilityData.optString("label", ""); + } + + private static boolean isEndedLiveBroadcast(JSONObject playerResponse) { + JSONObject microformat = playerResponse == null + ? null + : playerResponse.optJSONObject("microformat"); + JSONObject playerMicroformat = microformat == null + ? null + : microformat.optJSONObject("playerMicroformatRenderer"); + JSONObject liveDetails = playerMicroformat == null + ? null + : playerMicroformat.optJSONObject("liveBroadcastDetails"); + return liveDetails != null && !liveDetails.optString("endTimestamp", "").isEmpty(); + } + + private static JSONObject getLiveChatContinuation(JSONObject response) { + JSONObject contents = response.optJSONObject("continuationContents"); + if (contents == null) { + JSONObject nestedResponse = response.optJSONObject("response"); + contents = nestedResponse == null + ? null + : nestedResponse.optJSONObject("continuationContents"); + } + return contents == null ? null : contents.optJSONObject("liveChatContinuation"); + } + + static Continuation findContinuation(Object value, String... keys) { + if (value == null || value == JSONObject.NULL) { + return null; + } + if (value instanceof JSONArray) { + JSONArray array = (JSONArray) value; + for (int index = 0; index < array.length(); index++) { + Continuation found = findContinuation(array.opt(index), keys); + if (found != null) { + return found; + } + } + return null; + } + if (!(value instanceof JSONObject)) { + return null; + } + + JSONObject object = (JSONObject) value; + for (String key : keys) { + JSONObject continuationData = object.optJSONObject(key); + if (continuationData == null) { + continue; + } + String continuation = continuationData.optString("continuation", ""); + if (!continuation.isEmpty()) { + return new Continuation( + continuation, + continuationData.optLong("timeoutMs", DEFAULT_CONTINUATION_TIMEOUT_MS) + ); + } + } + + Iterator childKeys = object.keys(); + while (childKeys.hasNext()) { + Continuation found = findContinuation(object.opt(childKeys.next()), keys); + if (found != null) { + return found; + } + } + return null; + } + + static List parseComments(JSONArray actions) { + List comments = new ArrayList<>(); + if (actions == null) { + return comments; + } + + for (int index = 0; index < actions.length(); index++) { + JSONObject action = actions.optJSONObject(index); + JSONObject addAction = action == null + ? null + : action.optJSONObject("addChatItemAction"); + JSONObject item = addAction == null ? null : addAction.optJSONObject("item"); + NiconicoComment comment = parseComment(item, -1); + if (comment != null) { + comments.add(comment); + } + } + return comments; + } + + static ReplayParseResult parseReplayComments(JSONArray actions) { + List comments = new ArrayList<>(); + long maxVideoOffsetMs = -1; + if (actions == null) { + return new ReplayParseResult(comments, maxVideoOffsetMs); + } + + for (int index = 0; index < actions.length(); index++) { + JSONObject action = actions.optJSONObject(index); + JSONObject replayAction = action == null + ? null + : action.optJSONObject("replayChatItemAction"); + if (replayAction == null) { + continue; + } + + // The offset is a numeric string. Missing values are -1. + final long rawOffsetMs = replayAction.optLong("videoOffsetTimeMsec", -1); + if (rawOffsetMs < 0) { + continue; + } + maxVideoOffsetMs = Math.max(maxVideoOffsetMs, rawOffsetMs); + + JSONArray innerActions = replayAction.optJSONArray("actions"); + if (innerActions == null) { + continue; + } + for (int innerIndex = 0; innerIndex < innerActions.length(); innerIndex++) { + JSONObject innerAction = innerActions.optJSONObject(innerIndex); + JSONObject addAction = innerAction == null + ? null + : innerAction.optJSONObject("addChatItemAction"); + JSONObject item = addAction == null ? null : addAction.optJSONObject("item"); + if (isBeforeReplayStart(item)) { + continue; + } + NiconicoComment comment = parseComment(item, rawOffsetMs); + if (comment != null) { + comments.add(comment); + } + } + } + return new ReplayParseResult(comments, maxVideoOffsetMs); + } + + private static boolean isBeforeReplayStart(JSONObject item) { + JSONObject renderer = getCommentRenderer(item); + if (renderer == null) { + return false; + } + String timestampText = extractText(renderer.optJSONObject("timestampText")).trim(); + return timestampText.startsWith("-") || timestampText.startsWith("−"); + } + + private static NiconicoComment parseComment(JSONObject item, long videoOffsetMs) { + if (item == null) { + return null; + } + + JSONObject paid = item.optJSONObject("liveChatPaidMessageRenderer"); + JSONObject membership = item.optJSONObject("liveChatMembershipItemRenderer"); + JSONObject renderer = getCommentRenderer(item); + if (renderer == null) { + return null; + } + + ParsedMessage message = parseMessage(renderer.optJSONObject("message")); + if (message.text.isEmpty() || message.text.length() > MAX_COMMENT_LENGTH) { + return null; + } + + boolean isMembership = membership != null || hasMembershipBadge(renderer); + int backgroundColor = getPaidMessageBackgroundColor(paid); + return new NiconicoComment( + renderer.optString("id", ""), + message.text, + message.segments, + isMembership, + backgroundColor, + videoOffsetMs + ); + } + + private static JSONObject getCommentRenderer(JSONObject item) { + if (item == null) { + return null; + } + JSONObject renderer = item.optJSONObject("liveChatTextMessageRenderer"); + if (renderer == null) { + renderer = item.optJSONObject("liveChatPaidMessageRenderer"); + } + if (renderer == null) { + renderer = item.optJSONObject("liveChatMembershipItemRenderer"); + } + if (renderer == null) { + renderer = item.optJSONObject("liveChatViewerEngagementMessageRenderer"); + } + return renderer; + } + + private static String extractText(JSONObject textRenderer) { + return parseMessage(textRenderer).text; + } + + private static ParsedMessage parseMessage(JSONObject textRenderer) { + List segments = new ArrayList<>(); + if (textRenderer == null) { + return new ParsedMessage("", segments); + } + String simpleText = textRenderer.optString("simpleText", ""); + if (!simpleText.isEmpty()) { + String normalized = normalizeMessageText(simpleText).trim(); + if (!normalized.isEmpty()) { + segments.add(NiconicoComment.Segment.text(normalized)); + } + return new ParsedMessage(normalized, segments); + } + + JSONArray runs = textRenderer.optJSONArray("runs"); + if (runs == null) { + return new ParsedMessage("", segments); + } + StringBuilder result = new StringBuilder(); + for (int index = 0; index < runs.length(); index++) { + JSONObject run = runs.optJSONObject(index); + if (run == null) { + continue; + } + String text = normalizeMessageText(run.optString("text", "")); + if (!text.isEmpty()) { + result.append(text); + segments.add(NiconicoComment.Segment.text(text)); + continue; + } + + JSONObject emoji = run.optJSONObject("emoji"); + JSONArray shortcuts = emoji == null ? null : emoji.optJSONArray("shortcuts"); + String fallback = shortcuts == null ? "" : shortcuts.optString(0, ""); + if (fallback.isEmpty() && emoji != null) { + fallback = emoji.optString("emojiId", ""); + } + if (!fallback.isEmpty()) { + result.append(fallback); + String imageUrl = getBestEmojiImageUrl(emoji); + segments.add(imageUrl.isEmpty() + ? NiconicoComment.Segment.text(fallback) + : NiconicoComment.Segment.emoji(fallback, imageUrl)); + } + } + return new ParsedMessage(result.toString().trim(), segments); + } + + private static String normalizeMessageText(String text) { + return text.replace('\n', ' ').replace('\r', ' '); + } + + private static String getBestEmojiImageUrl(JSONObject emoji) { + JSONObject image = emoji == null ? null : emoji.optJSONObject("image"); + JSONArray thumbnails = image == null ? null : image.optJSONArray("thumbnails"); + if (thumbnails == null) { + return ""; + } + + String bestUrl = ""; + long bestArea = -1; + for (int index = 0; index < thumbnails.length(); index++) { + JSONObject thumbnail = thumbnails.optJSONObject(index); + if (thumbnail == null) { + continue; + } + String url = thumbnail.optString("url", ""); + if (url.isEmpty()) { + continue; + } + long area = (long) thumbnail.optInt("width", 0) + * thumbnail.optInt("height", 0); + if (bestUrl.isEmpty() || area >= bestArea) { + bestArea = area; + bestUrl = url.startsWith("//") ? "https:" + url : url; + } + } + return bestUrl; + } + + private static boolean hasMembershipBadge(JSONObject renderer) { + JSONArray badges = renderer.optJSONArray("authorBadges"); + if (badges == null) { + return false; + } + for (int index = 0; index < badges.length(); index++) { + JSONObject badge = badges.optJSONObject(index); + JSONObject badgeRenderer = badge == null + ? null + : badge.optJSONObject("liveChatAuthorBadgeRenderer"); + if (badgeRenderer != null && badgeRenderer.has("customThumbnail")) { + return true; + } + } + return false; + } + + private static int getPaidMessageBackgroundColor(JSONObject paidRenderer) { + if (paidRenderer == null) { + return 0; + } + String key = paidRenderer.has("bodyBackgroundColor") + ? "bodyBackgroundColor" + : "headerBackgroundColor"; + if (!paidRenderer.has(key)) { + return 0; + } + long color = paidRenderer.optLong(key); + return (97 << 24) | ((int) color & 0x00ffffff); + } + + private static String getVisitorData(JSONObject context) { + JSONObject client = context.optJSONObject("client"); + return client == null ? "" : client.optString("visitorData", ""); + } + + private static String stripJsonPrefix(String value) { + int start = value.indexOf('{'); + return start > 0 ? value.substring(start) : value; + } + + static long getLivePollDelayMs(long value) { + if (value <= 0) { + return LIVE_POLL_MAX_DELAY_MS; + } + return Math.max(LIVE_POLL_MIN_DELAY_MS, Math.min(LIVE_POLL_MAX_DELAY_MS, value)); + } + + private void sleepBeforeRetry(int attempt) { + sleep(Math.min(15_000L, attempt * 3_000L)); + } + + private void sleep(long milliseconds) { + if (stopped) { + return; + } + try { + Thread.sleep(milliseconds); + } catch (InterruptedException ex) { + // Interrupted by stop(). + Thread.currentThread().interrupt(); + stopped = true; + } + } + + private static final class ParsedMessage { + final String text; + final List segments; + + ParsedMessage(String text, List segments) { + this.text = text; + this.segments = segments; + } + } + + private static final class PageState { + final String apiKey; + final String clientVersion; + final JSONObject context; + final Continuation continuation; + final boolean replay; + + PageState( + String apiKey, + String clientVersion, + JSONObject context, + Continuation continuation, + boolean replay + ) { + this.apiKey = apiKey; + this.clientVersion = clientVersion; + this.context = context; + this.continuation = continuation; + this.replay = replay; + } + } + + private static final class ChatPage { + final List comments; + final Continuation continuation; + final long maxVideoOffsetMs; + + ChatPage( + List comments, + Continuation continuation, + long maxVideoOffsetMs + ) { + this.comments = comments; + this.continuation = continuation; + this.maxVideoOffsetMs = maxVideoOffsetMs; + } + } + + static final class ReplayParseResult { + final List comments; + final long maxVideoOffsetMs; + + ReplayParseResult(List comments, long maxVideoOffsetMs) { + this.comments = comments; + this.maxVideoOffsetMs = maxVideoOffsetMs; + } + } + + static final class Continuation { + final String value; + final long timeoutMs; + + Continuation(String value, long timeoutMs) { + this.value = value; + this.timeoutMs = timeoutMs; + } + } +} diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/voiceovertranslation/VOTBottomSheet.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/voiceovertranslation/VOTBottomSheet.java index 343b5d2407..2c20809580 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/voiceovertranslation/VOTBottomSheet.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/voiceovertranslation/VOTBottomSheet.java @@ -37,7 +37,6 @@ import android.widget.LinearLayout; import android.widget.ListView; import android.widget.ScrollView; -import android.widget.SeekBar; import android.widget.TextView; import java.util.ArrayList; @@ -47,10 +46,7 @@ import app.morphe.extension.shared.ResourceType; import app.morphe.extension.shared.ResourceUtils; import app.morphe.extension.shared.Utils; -import app.morphe.extension.shared.settings.IntegerSetting; import app.morphe.extension.shared.settings.preference.CustomDialogListPreference; -import app.morphe.extension.shared.settings.preference.SeekBarPreference; -import app.morphe.extension.shared.settings.preference.SeekBarPreference.SeekBarConfig; import app.morphe.extension.shared.theme.ThemeUtils; import app.morphe.extension.shared.ui.CustomDialog; import app.morphe.extension.shared.ui.Dim; @@ -112,7 +108,7 @@ public static void show(Context context) { translationRow.setOnClickListener(v -> showTranslationServicePicker(context, mainRef[0])); refreshTranslation.run(); - TextView title = makeTitle(context, str("morphe_vot_screen_title"), fg); + TextView title = SheetBottomDialog.createTitle(context, str("morphe_vot_screen_title"), fg); ((LinearLayout.LayoutParams) title.getLayoutParams()).setMargins(Dim.dp16, Dim.dp8, Dim.dp16, Dim.dp16); root.addView(title); @@ -127,7 +123,7 @@ public static void show(Context context) { content.addView(translationRow); content.addView(engineRow); content.addView(makeDivider(context, fg)); - content.addView(makeSliderRow(context, + content.addView(SheetBottomDialog.createSliderRow(context, str("morphe_vot_original_audio_volume_title"), Settings.VOT_ORIGINAL_AUDIO_VOLUME, fg, @@ -135,7 +131,7 @@ public static void show(Context context) { Settings.VOT_ORIGINAL_AUDIO_VOLUME.save(value); VoiceOverTranslationPatch.updateOriginalAudioMultiplier(); })); - content.addView(makeSliderRow(context, + content.addView(SheetBottomDialog.createSliderRow(context, str("morphe_vot_translation_volume_title"), Settings.VOT_TRANSLATION_VOLUME, fg, @@ -143,7 +139,7 @@ public static void show(Context context) { Settings.VOT_TRANSLATION_VOLUME.save(value); VoiceOverTranslationPatch.updatePlaybackVolume(); })); - content.addView(makeSliderRow(context, + content.addView(SheetBottomDialog.createSliderRow(context, str("morphe_vot_max_speech_rate_title"), Settings.VOT_MAX_SPEECH_RATE, fg, @@ -225,7 +221,7 @@ private static void showTranslationServicePicker(Context context, SheetBottomDia SheetBottomDialog.createMainLayout(context, getDialogBackgroundColor()); pickerRoot.setPadding(Dim.dp16, 0, Dim.dp16, Dim.dp16); final int fg = ThemeUtils.getAppForegroundColor(); - pickerRoot.addView(makeTitle(context, str("morphe_vot_translation_service_title"), fg)); + pickerRoot.addView(SheetBottomDialog.createTitle(context, str("morphe_vot_translation_service_title"), fg)); String selectedService = Settings.VOT_TRANSLATION_SERVICE.get(); @@ -311,7 +307,7 @@ private static void showEnginePicker(Context context, SheetBottomDialog.SlideDia SheetBottomDialog.DraggableLinearLayout pickerRoot = SheetBottomDialog.createMainLayout(context, getDialogBackgroundColor()); pickerRoot.setPadding(Dim.dp16, 0, Dim.dp16, Dim.dp16); - pickerRoot.addView(makeTitle(context, str("morphe_vot_tts_voice_type"), fg)); + pickerRoot.addView(SheetBottomDialog.createTitle(context, str("morphe_vot_tts_voice_type"), fg)); SheetBottomDialog.SlideDialog pickerDialog = SheetBottomDialog.createSlideDialog(context, pickerRoot, fadeInDuration); ScrollView scroll = new ScrollView(context); @@ -548,7 +544,7 @@ private static void showLanguagePicker(Context context, String title, SheetBottomDialog.DraggableLinearLayout pickerRoot = SheetBottomDialog.createMainLayout(context, getDialogBackgroundColor()); pickerRoot.setPadding(Dim.dp16, 0, Dim.dp16, Dim.dp16); - pickerRoot.addView(makeTitle(context, title, ThemeUtils.getAppForegroundColor())); + pickerRoot.addView(SheetBottomDialog.createTitle(context, title, ThemeUtils.getAppForegroundColor())); ListView listView = new ListView(context); listView.setDivider(null); @@ -583,20 +579,6 @@ private static String getLangDisplayName(String code, String[] langEntries, Stri return langEntries[0]; } - private static TextView makeTitle(Context context, String text, int fgColor) { - TextView tv = new TextView(context); - tv.setText(text); - tv.setTextSize(18); - tv.setTypeface(Typeface.DEFAULT_BOLD); - tv.setTextColor(fgColor); - tv.setGravity(Gravity.CENTER_HORIZONTAL); - LinearLayout.LayoutParams params = new LinearLayout.LayoutParams( - LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT); - params.setMargins(0, Dim.dp8, 0, Dim.dp16); - tv.setLayoutParams(params); - return tv; - } - private static View makeDivider(Context context, int fgColor) { View divider = new View(context); divider.setBackgroundColor(Color.argb(30, @@ -608,74 +590,7 @@ private static View makeDivider(Context context, int fgColor) { return divider; } - private static LinearLayout makeSliderRow(Context context, String label, IntegerSetting setting, - int fgColor, IntConsumer onChanged) { - final SeekBarConfig config = SeekBarPreference.configFor(setting); - if (config == null) throw new IllegalStateException("No SeekBarConfig registered for " + setting.key); - final int initialValue = setting.get(); - - LinearLayout outer = new LinearLayout(context); - outer.setOrientation(LinearLayout.VERTICAL); - LinearLayout.LayoutParams outerParams = new LinearLayout.LayoutParams( - LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT); - outerParams.setMargins(0, Dim.dp16, 0, 0); - outer.setLayoutParams(outerParams); - - TextView labelView = new TextView(context); - labelView.setText(label); - labelView.setTextColor(fgColor); - labelView.setTextSize(16); - labelView.setTypeface(Typeface.DEFAULT_BOLD); - outer.addView(labelView); - - LinearLayout seekRow = new LinearLayout(context); - seekRow.setOrientation(LinearLayout.HORIZONTAL); - seekRow.setGravity(Gravity.CENTER_VERTICAL); - LinearLayout.LayoutParams seekRowParams = new LinearLayout.LayoutParams( - LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT); - seekRowParams.topMargin = Dim.dp16; - seekRow.setLayoutParams(seekRowParams); - - SeekBar seekBar = new SeekBar(context); - seekBar.setMax((config.max() - config.min()) / config.step()); - seekBar.setProgress(SeekBarPreference.valueToProgress(config, initialValue)); - seekBar.getProgressDrawable().setColorFilter(new PorterDuffColorFilter(fgColor, PorterDuff.Mode.SRC_IN)); - seekBar.getThumb().setColorFilter(new PorterDuffColorFilter(fgColor, PorterDuff.Mode.SRC_IN)); - seekRow.addView(seekBar, new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)); - - TextView valueView = new TextView(context); - valueView.setText(SeekBarPreference.formatLabel(initialValue, config)); - valueView.setTextColor(fgColor); - valueView.setTextSize(14); - valueView.setMinWidth(Dim.dp40); - valueView.setGravity(Gravity.END); - seekRow.addView(valueView, new LinearLayout.LayoutParams( - LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT)); - - outer.addView(seekRow); - - seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { - @Override - public void onProgressChanged(SeekBar bar, int progress, boolean fromUser) { - if (fromUser) { - final int value = SeekBarPreference.progressToValue(config, progress); - valueView.setText(SeekBarPreference.formatLabel(value, config)); - onChanged.accept(value); - } - } - @Override public void onStartTrackingTouch(SeekBar bar) { } - @Override public void onStopTrackingTouch(SeekBar bar) { } - }); - - return outer; - } - private static int secondaryColor(int fg) { return Color.argb(153, Color.red(fg), Color.green(fg), Color.blue(fg)); } - - @FunctionalInterface - private interface IntConsumer { - void accept(int value); - } } diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/Settings.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/Settings.java index f393e7f4d2..0f7fba4989 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/Settings.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/Settings.java @@ -15,6 +15,7 @@ import java.util.List; import app.morphe.extension.shared.Logger; +import app.morphe.extension.shared.StringRef; import app.morphe.extension.shared.patches.CustomFontPatch.FontType; import app.morphe.extension.shared.patches.DownloadMode; import app.morphe.extension.shared.settings.BooleanSetting; @@ -708,6 +709,16 @@ public class Settings extends SharedYouTubeSettings { public static final BooleanSetting SWIPE_LOWEST_VALUE_ENABLE_AUTO_BRIGHTNESS = new BooleanSetting("morphe_swipe_lowest_value_enable_auto_brightness", FALSE, true, new SwipeActionAvailability(SwipeZoneAction.BRIGHTNESS)); public static final IntegerSetting SWIPE_ZONE_WIDTH = new IntegerSetting("morphe_swipe_zone_width", 37, true, new SideSwipeZonesAvailability()); + // Niconico comments + public static final BooleanSetting NICONICO_COMMENTS = new BooleanSetting("morphe_niconico_comments", FALSE, true); + public static final BooleanSetting NICONICO_COMMENTS_SHOWN = new BooleanSetting("morphe_niconico_comments_shown", TRUE); + public static final IntegerSetting NICONICO_COMMENTS_FONT_SIZE = new IntegerSetting("morphe_niconico_comments_font_size", 20, parent(NICONICO_COMMENTS)); + public static final IntegerSetting NICONICO_COMMENTS_OPACITY = new IntegerSetting("morphe_niconico_comments_opacity", 100, parent(NICONICO_COMMENTS)); + public static final IntegerSetting NICONICO_COMMENTS_TRAVEL_DURATION = new IntegerSetting("morphe_niconico_comments_travel_duration", 7, parent(NICONICO_COMMENTS)); + /** Max comment length slider value that shows all comments. */ + public static final int NICONICO_COMMENTS_MAX_LENGTH_NO_LIMIT = 100; + public static final IntegerSetting NICONICO_COMMENTS_MAX_LENGTH = new IntegerSetting("morphe_niconico_comments_max_length", NICONICO_COMMENTS_MAX_LENGTH_NO_LIMIT, parent(NICONICO_COMMENTS)); + // Voice over translation public static final BooleanSetting VOT_ENABLED = new BooleanSetting("morphe_vot_enabled", FALSE, true); public static final BooleanSetting VOT_SESSION_ENABLED = new BooleanSetting("morphe_vot_session_enabled", FALSE); @@ -1177,5 +1188,14 @@ private static void migrateSwipeGestureToZone(BooleanSetting oldSetting, 10, 25, 1, "x", 10)); SeekBarPreference.register(new SeekBarConfig(VIDEO_DELAY, 0, 5000, 25, "ms")); + SeekBarPreference.register(new SeekBarConfig(NICONICO_COMMENTS_FONT_SIZE, + 12, 40, 1, "sp")); + SeekBarPreference.register(new SeekBarConfig(NICONICO_COMMENTS_OPACITY, + 10, 100, 10, "%")); + SeekBarPreference.register(new SeekBarConfig(NICONICO_COMMENTS_TRAVEL_DURATION, + 3, 15, 1, "s")); + SeekBarPreference.register(new SeekBarConfig(NICONICO_COMMENTS_MAX_LENGTH, + 10, NICONICO_COMMENTS_MAX_LENGTH_NO_LIMIT, 10, "", 1, + StringRef.sf("morphe_niconico_comments_max_length_no_limit"))); } } diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/videoplayer/PlayerOverlayButton.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/videoplayer/PlayerOverlayButton.java index 4369ac653a..0a18e8c9a0 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/videoplayer/PlayerOverlayButton.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/videoplayer/PlayerOverlayButton.java @@ -20,8 +20,10 @@ import android.view.Gravity; import android.view.View; import android.view.ViewGroup; +import android.view.accessibility.AccessibilityNodeInfo; import android.widget.ImageView; import android.widget.TextView; +import android.widget.ToggleButton; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -29,6 +31,7 @@ import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.List; +import java.util.function.BooleanSupplier; import app.morphe.extension.shared.Logger; import app.morphe.extension.shared.ResourceType; @@ -627,6 +630,24 @@ public static TextView addButtonWithTextOverlay(View sourceButton, return textOverlay; } + /** + * Exposes a button to accessibility services as a toggle, + * so screen readers announce whether its feature is on or off. + */ + public static void setToggleAccessibilityDelegate(View button, BooleanSupplier isChecked) { + button.setAccessibilityDelegate(new View.AccessibilityDelegate() { + @Override + @SuppressWarnings("deprecation") + public void onInitializeAccessibilityNodeInfo(@NonNull View host, @NonNull AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(host, info); + info.setClassName(ToggleButton.class.getName()); + info.setCheckable(true); + // setChecked(int) is API 36 only, the boolean version still works on every version. + info.setChecked(isChecked.getAsBoolean()); + } + }); + } + private static int getNumberOfEnabledButtons() { return (int) buttonControllers.stream().filter(PlayerOverlayButtonController::isEnabled).count(); } diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/videoplayer/VoiceOverTranslationButton.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/videoplayer/VoiceOverTranslationButton.java index 92e11fe104..c9fe8d0260 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/videoplayer/VoiceOverTranslationButton.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/videoplayer/VoiceOverTranslationButton.java @@ -8,11 +8,8 @@ package app.morphe.extension.youtube.videoplayer; import android.view.View; -import android.view.accessibility.AccessibilityNodeInfo; import android.widget.ImageView; -import android.widget.ToggleButton; -import androidx.annotation.NonNull; import androidx.annotation.Nullable; import java.lang.ref.WeakReference; @@ -21,8 +18,8 @@ import app.morphe.extension.shared.ResourceUtils; import app.morphe.extension.shared.Utils; import app.morphe.extension.youtube.patches.LegacyPlayerControlsPatch; -import app.morphe.extension.youtube.patches.voiceovertranslation.VoiceOverTranslationPatch; import app.morphe.extension.youtube.patches.voiceovertranslation.VOTBottomSheet; +import app.morphe.extension.youtube.patches.voiceovertranslation.VoiceOverTranslationPatch; import app.morphe.extension.youtube.settings.Settings; @SuppressWarnings("unused") @@ -56,7 +53,7 @@ public static void initializeButton(View controlsView) { overlayButtonRef = button != null ? new WeakReference<>(button) : null; if (button != null) { button.setContentDescription(ResourceUtils.getString("morphe_vot_screen_title")); - setToggleAccessibilityDelegate(button); + PlayerOverlayButton.setToggleAccessibilityDelegate(button, VoiceOverTranslationPatch::isSessionEnabled); } refreshActivatedState(); } catch (Exception ex) { @@ -88,7 +85,7 @@ public static void initializeLegacyButton(View controlsView) { }); View legacyButton = Utils.getChildViewByResourceName(controlsView, "morphe_vot_button"); if (legacyButton != null) { - setToggleAccessibilityDelegate(legacyButton); + PlayerOverlayButton.setToggleAccessibilityDelegate(legacyButton, VoiceOverTranslationPatch::isSessionEnabled); } refreshActivatedState(); } catch (Exception ex) { @@ -96,24 +93,6 @@ public static void initializeLegacyButton(View controlsView) { } } - /** - * Exposes the button to accessibility services as a toggle, - * so screen readers announce whether translation is on or off. - */ - private static void setToggleAccessibilityDelegate(View button) { - button.setAccessibilityDelegate(new View.AccessibilityDelegate() { - @Override - @SuppressWarnings("deprecation") - public void onInitializeAccessibilityNodeInfo(@NonNull View host, @NonNull AccessibilityNodeInfo info) { - super.onInitializeAccessibilityNodeInfo(host, info); - info.setClassName(ToggleButton.class.getName()); - info.setCheckable(true); - // setChecked(int) is API 36 only, the boolean version still works on every version. - info.setChecked(VoiceOverTranslationPatch.isSessionEnabled()); - } - }); - } - private static void refreshActivatedState() { Utils.verifyOnMainThread(); try { diff --git a/extensions/youtube/src/test/java/app/morphe/extension/youtube/niconicocomments/CommentScheduleTest.java b/extensions/youtube/src/test/java/app/morphe/extension/youtube/niconicocomments/CommentScheduleTest.java new file mode 100644 index 0000000000..9d166979bf --- /dev/null +++ b/extensions/youtube/src/test/java/app/morphe/extension/youtube/niconicocomments/CommentScheduleTest.java @@ -0,0 +1,120 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/4080 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + * + * Niconico-style live/replay comments implementation is adapted from + * https://github.com/david419kr/niconico-yt-morphe-patches (GPLv3) + * Feature design is based on david419kr/niconico-yt (MIT License) + */ + +package app.morphe.extension.youtube.niconicocomments; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Random; +import java.util.Set; + +public class CommentScheduleTest { + @Test + public void neverDropsACommentThatHasAlreadyStarted() { + CommentSchedule schedule = new CommentSchedule(new Random(1)); + List comments = new ArrayList<>(); + comments.add(comment("active", 1_000)); + for (int index = 0; index < 1_000; index++) { + comments.add(comment("future-" + index, 10_000 + index)); + } + + schedule.addComments(comments, 2_000, 0, 8); + + assertEquals(1_001, schedule.comments().size()); + assertTrue(schedule.comments().stream() + .anyMatch(scheduled -> "active".equals(scheduled.comment.id))); + } + + @Test + public void spreadsLiveBatchAcrossTheNextPollingWindow() { + CommentSchedule schedule = new CommentSchedule(new Random(2)); + List comments = new ArrayList<>(); + for (int index = 0; index < 10; index++) { + comments.add(comment("live-" + index, -1)); + } + + schedule.addComments(comments, 1_000, 250, 10); + + assertEquals(35, CommentSchedule.calculateLiveSpreadStepMs(10, 250)); + assertEquals(1_000, schedule.comments().get(0).startTimeMs); + assertEquals(1_035, schedule.comments().get(1).startTimeMs); + assertEquals(1_315, schedule.comments().get(9).startTimeMs); + } + + @Test + public void usesEveryLaneOnceInShuffledOrder() { + CommentSchedule schedule = new CommentSchedule(new Random(3)); + List comments = new ArrayList<>(); + for (int index = 0; index < 8; index++) { + comments.add(comment("lane-" + index, -1)); + } + + schedule.addComments(comments, 0, 5_000, 8); + + Set usedLanes = new HashSet<>(); + boolean differsFromSequentialOrder = false; + for (int index = 0; index < schedule.comments().size(); index++) { + int lane = schedule.comments().get(index).lane; + usedLanes.add(lane); + differsFromSequentialOrder |= lane != index; + } + assertEquals(8, usedLanes.size()); + assertTrue(differsFromSequentialOrder); + + schedule.redistributeLanes(2); + Set resizedLanes = new HashSet<>(); + for (CommentSchedule.ScheduledComment comment : schedule.comments()) { + assertTrue(comment.lane >= 0 && comment.lane < 2); + resizedLanes.add(comment.lane); + } + assertEquals(2, resizedLanes.size()); + } + + @Test + public void boundsLiveCommentsWhenThePlaybackClockIsFrozen() { + CommentSchedule schedule = new CommentSchedule(new Random(4)); + for (int round = 0; round < 200; round++) { + List incoming = new ArrayList<>(); + for (int index = 0; index < 50; index++) { + incoming.add(comment(round + ":" + index, -1)); + } + schedule.addComments(incoming, 10_000, 250, 12); + assertTrue(schedule.comments().size() <= CommentSchedule.MAX_LIVE_COMMENTS); + } + assertEquals(CommentSchedule.MAX_LIVE_COMMENTS, schedule.comments().size()); + } + + @Test + public void liveOverflowDoesNotDiscardFetchedReplayLookahead() { + CommentSchedule schedule = new CommentSchedule(new Random(5)); + List replay = new ArrayList<>(); + for (int index = 0; index < 1_001; index++) replay.add(comment("replay-" + index, 20_000 + index)); + schedule.addComments(replay, 10_000, 0, 12); + List live = new ArrayList<>(); + for (int index = 0; index < 2_000; index++) live.add(comment("live-" + index, -1)); + schedule.addComments(live, 10_000, 250, 12); + + assertEquals(1_001, schedule.comments().stream() + .filter(scheduled -> scheduled.comment.videoOffsetMs >= 0).count()); + assertEquals(CommentSchedule.MAX_LIVE_COMMENTS, schedule.comments().stream() + .filter(scheduled -> scheduled.comment.videoOffsetMs < 0).count()); + } + + private static NiconicoComment comment(String id, long videoOffsetMs) { + return new NiconicoComment(id, id, false, 0, videoOffsetMs); + } +} diff --git a/extensions/youtube/src/test/java/app/morphe/extension/youtube/niconicocomments/EmojiImageCacheTest.java b/extensions/youtube/src/test/java/app/morphe/extension/youtube/niconicocomments/EmojiImageCacheTest.java new file mode 100644 index 0000000000..4119f1ce8e --- /dev/null +++ b/extensions/youtube/src/test/java/app/morphe/extension/youtube/niconicocomments/EmojiImageCacheTest.java @@ -0,0 +1,135 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/4080 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + * + * Niconico-style live/replay comments implementation is adapted from + * https://github.com/david419kr/niconico-yt-morphe-patches (GPLv3) + * Feature design is based on david419kr/niconico-yt (MIT License) + */ + +package app.morphe.extension.youtube.niconicocomments; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +public class EmojiImageCacheTest { + @Test + public void repeatedLookupsShareOnePendingLoad() { + EmojiImageCache cache = new EmojiImageCache<>(); + EmojiImageCache.Entry first = cache.beginLoad("emoji", Set.of("emoji")); + for (int i = 0; i < 100; i++) assertSame(first, cache.beginLoad("emoji", Set.of("emoji"))); + assertTrue(first.loading); + assertTrue(cache.complete("emoji", first, "bitmap")); + assertFalse(first.loading); + assertEquals("bitmap", first.value); + } + + @Test + public void doesNotEvictInFlightLoadsEvenIfNotCurrentlyVisible() { + EmojiImageCache cache = new EmojiImageCache<>(); + EmojiImageCache.Entry first = null; + for (int i = 0; i < EmojiImageCache.MAX_ENTRIES; i++) { + EmojiImageCache.Entry entry = cache.beginLoad("emoji-" + i, Set.of()); + if (i == 0) first = entry; + } + assertNull(cache.beginLoad("extra", Set.of())); + assertSame(first, cache.get("emoji-0")); + } + + @Test + public void oneMoreActiveUrlThanCapacityDoesNotCauseDownloadThrashing() { + EmojiImageCache cache = new EmojiImageCache<>(); + Set needed = new HashSet<>(); + for (int i = 0; i <= EmojiImageCache.MAX_ENTRIES; i++) needed.add("emoji-" + i); + int loads = 0; + for (int pass = 0; pass < 60; pass++) { + for (int i = 0; i <= EmojiImageCache.MAX_ENTRIES; i++) { + String url = "emoji-" + i; + if (cache.get(url) != null) continue; + EmojiImageCache.Entry entry = cache.beginLoad(url, needed); + if (entry != null) { + loads++; + cache.complete(url, entry, "bitmap"); + } + } + } + assertEquals(EmojiImageCache.MAX_ENTRIES, loads); + } + + @Test + public void replacesCompletedImagesAfterTheirCommentsLeaveTheScreen() { + EmojiImageCache cache = new EmojiImageCache<>(); + Set needed = new HashSet<>(); + for (int i = 0; i < EmojiImageCache.MAX_ENTRIES; i++) { + String url = "emoji-" + i; + needed.add(url); + EmojiImageCache.Entry entry = cache.beginLoad(url, needed); + cache.complete(url, entry, "bitmap"); + } + needed.remove("emoji-0"); + needed.add("extra"); + assertNotNull(cache.beginLoad("extra", needed)); + assertNull(cache.get("emoji-0")); + } + + @Test + public void ignoresResultsFromAnOldVideo() { + EmojiImageCache cache = new EmojiImageCache<>(); + EmojiImageCache.Entry old = cache.beginLoad("emoji", Set.of()); + cache.clear(); + EmojiImageCache.Entry current = cache.beginLoad("emoji", Set.of()); + assertFalse(cache.complete("emoji", old, "old bitmap")); + assertSame(current, cache.get("emoji")); + assertNull(current.value); + } + + @Test + public void cachesFailuresInsteadOfRetryingOnEveryFrame() { + EmojiImageCache cache = new EmojiImageCache<>(); + EmojiImageCache.Entry entry = cache.beginLoad("emoji", Set.of()); + cache.complete("emoji", entry, null); + assertTrue(entry.failed); + assertSame(entry, cache.beginLoad("emoji", Set.of("emoji"))); + } + + @Test + public void workerQueueRejectsWorkPastItsBound() throws Exception { + ThreadPoolExecutor executor = EmojiImageCache.createExecutor(); + CountDownLatch started = new CountDownLatch(2); + CountDownLatch release = new CountDownLatch(1); + try { + for (int i = 0; i < 2; i++) executor.execute(() -> { + started.countDown(); + try { + release.await(); + } catch (InterruptedException interrupted) { + Thread.currentThread().interrupt(); + } + }); + assertTrue(started.await(5, TimeUnit.SECONDS)); + for (int i = 0; i < EmojiImageCache.MAX_QUEUED_LOADS; i++) executor.execute(() -> {}); + assertThrows(RejectedExecutionException.class, () -> executor.execute(() -> {})); + assertEquals(EmojiImageCache.MAX_QUEUED_LOADS, executor.getQueue().size()); + } finally { + release.countDown(); + executor.shutdownNow(); + assertTrue(executor.awaitTermination(5, TimeUnit.SECONDS)); + } + } +} diff --git a/extensions/youtube/src/test/java/app/morphe/extension/youtube/niconicocomments/NiconicoCommentsControllerTest.java b/extensions/youtube/src/test/java/app/morphe/extension/youtube/niconicocomments/NiconicoCommentsControllerTest.java new file mode 100644 index 0000000000..955cce360a --- /dev/null +++ b/extensions/youtube/src/test/java/app/morphe/extension/youtube/niconicocomments/NiconicoCommentsControllerTest.java @@ -0,0 +1,112 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/4080 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + * + * Niconico-style live/replay comments implementation is adapted from + * https://github.com/david419kr/niconico-yt-morphe-patches (GPLv3) + * Feature design is based on david419kr/niconico-yt (MIT License) + */ + +package app.morphe.extension.youtube.niconicocomments; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +import java.util.List; + +import app.morphe.extension.youtube.settings.Settings; + +public class NiconicoCommentsControllerTest { + @Test + public void dropsInactiveLiveBatchesButPreservesReplayLookahead() { + assertTrue(NiconicoCommentsController.shouldAcceptComments(250, true, true)); + assertFalse(NiconicoCommentsController.shouldAcceptComments(250, false, true)); + assertFalse(NiconicoCommentsController.shouldAcceptComments(250, true, false)); + assertFalse(NiconicoCommentsController.shouldAcceptComments(250, false, false)); + assertTrue(NiconicoCommentsController.shouldAcceptComments(0, false, true)); + assertTrue(NiconicoCommentsController.shouldAcceptComments(0, true, false)); + assertTrue(NiconicoCommentsController.shouldAcceptComments(0, false, false)); + } + + @Test + public void removesCommentsLongerThanTheMaxLength() { + NiconicoComment shortComment = new NiconicoComment("short", "12345", false, 0, 0); + NiconicoComment longComment = new NiconicoComment("long", "123456", false, 0, 0); + List comments = List.of(shortComment, longComment); + + assertEquals(List.of(shortComment), NiconicoCommentsController.removeLongComments(comments, 5)); + assertEquals(comments, NiconicoCommentsController.removeLongComments( + comments, Settings.NICONICO_COMMENTS_MAX_LENGTH_NO_LIMIT)); + } + + @Test + public void countsEachEmojiAsOneCharacter() { + NiconicoComment comment = new NiconicoComment("emoji", "hi:face-blue-smiling:", List.of( + NiconicoComment.Segment.text("hi"), + NiconicoComment.Segment.emoji(":face-blue-smiling:", "https://example.com/emoji.png") + ), false, 0, 0); + + assertEquals(3, comment.length()); + } + + @Test + public void interpolatesAtVerySlowPlaybackRate() { + assertEquals( + 10_100, + NiconicoCommentsController.interpolateVideoTime(10_000, 2_000, 0.05) + ); + } + + @Test + public void interpolatesAtEightTimesPlaybackRate() { + assertEquals( + 26_000, + NiconicoCommentsController.interpolateVideoTime(10_000, 2_000, 8.0) + ); + } + + @Test + public void preservesFractionalPlaybackRates() { + assertEquals( + 12_700, + NiconicoCommentsController.interpolateVideoTime(10_000, 2_000, 1.35) + ); + } + + @Test + public void rebasingBeforeRateChangePreservesCurrentPosition() { + long positionAtRateChange = + NiconicoCommentsController.interpolateVideoTime(10_000, 2_000, 1.0); + + assertEquals( + positionAtRateChange, + NiconicoCommentsController.interpolateVideoTime(positionAtRateChange, 0, 8.0) + ); + assertEquals( + 20_000, + NiconicoCommentsController.interpolateVideoTime(positionAtRateChange, 1_000, 8.0) + ); + } + + @Test + public void doesNotClampLargeValidPlaybackRates() { + assertEquals(32.5, NiconicoCommentsController.sanitizePlaybackRate(32.5), 0.0); + } + + @Test + public void replacesInvalidPlaybackRatesWithNormalSpeed() { + assertEquals(1.0, NiconicoCommentsController.sanitizePlaybackRate(0.0), 0.0); + assertEquals(1.0, NiconicoCommentsController.sanitizePlaybackRate(-1.0), 0.0); + assertEquals(1.0, NiconicoCommentsController.sanitizePlaybackRate(Double.NaN), 0.0); + assertEquals( + 1.0, + NiconicoCommentsController.sanitizePlaybackRate(Double.POSITIVE_INFINITY), + 0.0 + ); + } +} diff --git a/extensions/youtube/src/test/java/app/morphe/extension/youtube/niconicocomments/YouTubeChatSourceTest.java b/extensions/youtube/src/test/java/app/morphe/extension/youtube/niconicocomments/YouTubeChatSourceTest.java new file mode 100644 index 0000000000..cc6ce8a35d --- /dev/null +++ b/extensions/youtube/src/test/java/app/morphe/extension/youtube/niconicocomments/YouTubeChatSourceTest.java @@ -0,0 +1,156 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/4080 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + * + * Niconico-style live/replay comments implementation is adapted from + * https://github.com/david419kr/niconico-yt-morphe-patches (GPLv3) + * Feature design is based on david419kr/niconico-yt (MIT License) + */ + +package app.morphe.extension.youtube.niconicocomments; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import org.json.JSONArray; +import org.json.JSONObject; +import org.junit.Test; + +import java.util.List; + +public class YouTubeChatSourceTest { + @Test + public void parsesWatchPageConfigAndBalancedJson() throws Exception { + String html = "" + + ""; + + JSONObject config = YouTubeChatSource.parseYoutubeConfig(html); + JSONObject initialData = YouTubeChatSource.parseMarkedObject( + html, + "var ytInitialData =" + ); + + assertEquals("key", config.getString("INNERTUBE_API_KEY")); + assertEquals( + "1.2.3", + config.getJSONObject("INNERTUBE_CONTEXT") + .getJSONObject("client") + .getString("clientVersion") + ); + assertNotNull(initialData); + assertEquals("escaped \" } brace", initialData.getString("text")); + } + + @Test + public void findsContinuationAndTimeoutRecursively() throws Exception { + JSONObject value = new JSONObject( + "{\"outer\":[{\"invalidationContinuationData\":{" + + "\"continuation\":\"next\",\"timeoutMs\":2345}}]}" + ); + + YouTubeChatSource.Continuation continuation = YouTubeChatSource.findContinuation( + value, + "timedContinuationData", + "invalidationContinuationData" + ); + + assertNotNull(continuation); + assertEquals("next", continuation.value); + assertEquals(2345, continuation.timeoutMs); + } + + @Test + public void parsesLiveTextEmojiMembershipAndPaidColor() throws Exception { + JSONArray actions = new JSONArray("[" + + "{\"addChatItemAction\":{\"item\":{\"liveChatTextMessageRenderer\":{" + + "\"id\":\"member\",\"message\":{\"runs\":[{\"text\":\"hello \"}," + + "{\"emoji\":{\"shortcuts\":[\":wave:\"],\"image\":{\"thumbnails\":[" + + "{\"url\":\"//emoji-small\",\"width\":16,\"height\":16}," + + "{\"url\":\"https://emoji-large\",\"width\":48,\"height\":48}]}}}]}," + + "\"authorBadges\":[{\"liveChatAuthorBadgeRenderer\":{\"customThumbnail\":{}}}]}}}}," + + "{\"addChatItemAction\":{\"item\":{\"liveChatPaidMessageRenderer\":{" + + "\"id\":\"paid\",\"message\":{\"simpleText\":\"thank you\"}," + + "\"bodyBackgroundColor\":4278255360}}}}" + + "]"); + + List comments = YouTubeChatSource.parseComments(actions); + + assertEquals(2, comments.size()); + assertEquals("hello :wave:", comments.get(0).text); + assertTrue(comments.get(0).membership); + assertEquals(2, comments.get(0).segments.size()); + assertFalse(comments.get(0).segments.get(0).isEmoji()); + assertTrue(comments.get(0).segments.get(1).isEmoji()); + assertEquals("https://emoji-large", comments.get(0).segments.get(1).imageUrl); + assertEquals(-1, comments.get(0).videoOffsetMs); + assertEquals("thank you", comments.get(1).text); + assertEquals(0x6100ff00, comments.get(1).backgroundColor); + } + + @Test + public void preservesExactReplayOffsetAndNestedChatAction() throws Exception { + JSONArray actions = new JSONArray("[{\"replayChatItemAction\":{" + + "\"videoOffsetTimeMsec\":\"7449\",\"actions\":[" + + "{\"addChatItemAction\":{\"item\":{\"liveChatTextMessageRenderer\":{" + + "\"id\":\"replay\",\"timestampText\":{\"simpleText\":\"0:07\"}," + + "\"message\":{\"simpleText\":\"at seven seconds\"}}}}}]}}]"); + + YouTubeChatSource.ReplayParseResult result = + YouTubeChatSource.parseReplayComments(actions); + + assertEquals(7_449, result.maxVideoOffsetMs); + assertEquals(1, result.comments.size()); + assertEquals(7_449, result.comments.get(0).videoOffsetMs); + assertEquals("at seven seconds", result.comments.get(0).text); + } + + @Test + public void discardsNegativeReplayTimestampButKeepsRealZero() throws Exception { + JSONArray actions = new JSONArray("[" + + "{\"replayChatItemAction\":{\"videoOffsetTimeMsec\":\"0\",\"actions\":[" + + "{\"addChatItemAction\":{\"item\":{\"liveChatTextMessageRenderer\":{" + + "\"id\":\"countdown\",\"timestampText\":{\"simpleText\":\"-0:31\"}," + + "\"message\":{\"simpleText\":\"before start\"}}}}}]}}," + + "{\"replayChatItemAction\":{\"videoOffsetTimeMsec\":\"0\",\"actions\":[" + + "{\"addChatItemAction\":{\"item\":{\"liveChatTextMessageRenderer\":{" + + "\"id\":\"zero\",\"timestampText\":{\"simpleText\":\"0:00\"}," + + "\"message\":{\"simpleText\":\"at start\"}}}}}]}}]"); + + YouTubeChatSource.ReplayParseResult result = + YouTubeChatSource.parseReplayComments(actions); + + assertEquals(0, result.maxVideoOffsetMs); + assertEquals(1, result.comments.size()); + assertEquals("zero", result.comments.get(0).id); + assertEquals(0, result.comments.get(0).videoOffsetMs); + } + + @Test + public void advancesReplayWindowWhenOnlyPreStartCommentsAreFiltered() throws Exception { + JSONArray actions = new JSONArray("[{\"replayChatItemAction\":{" + + "\"videoOffsetTimeMsec\":\"0\",\"actions\":[" + + "{\"addChatItemAction\":{\"item\":{\"liveChatTextMessageRenderer\":{" + + "\"id\":\"countdown\",\"timestampText\":{\"simpleText\":\"-1:00:34\"}," + + "\"message\":{\"simpleText\":\"before start\"}}}}}]}}]"); + + YouTubeChatSource.ReplayParseResult result = + YouTubeChatSource.parseReplayComments(actions); + + assertEquals(0, result.maxVideoOffsetMs); + assertTrue(result.comments.isEmpty()); + } + + @Test + public void clampsLivePollingDelayToARealtimeWindow() { + assertEquals(250, YouTubeChatSource.getLivePollDelayMs(0)); + assertEquals(100, YouTubeChatSource.getLivePollDelayMs(20)); + assertEquals(175, YouTubeChatSource.getLivePollDelayMs(175)); + assertEquals(250, YouTubeChatSource.getLivePollDelayMs(5_000)); + } +} diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/niconicocomments/NiconicoCommentsPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/niconicocomments/NiconicoCommentsPatch.kt new file mode 100644 index 0000000000..d08cf76fbc --- /dev/null +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/niconicocomments/NiconicoCommentsPatch.kt @@ -0,0 +1,130 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/4080 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + * + * Niconico-style live/replay comments implementation is adapted from + * https://github.com/david419kr/niconico-yt-morphe-patches (GPLv3) + * Feature design is based on david419kr/niconico-yt (MIT License) + */ + +package app.morphe.patches.youtube.layout.niconicocomments + +import app.morphe.patcher.extensions.InstructionExtensions.addInstruction +import app.morphe.patcher.extensions.InstructionExtensions.getInstruction +import app.morphe.patcher.patch.bytecodePatch +import app.morphe.patcher.patch.resourcePatch +import app.morphe.patches.shared.misc.settings.preference.NonInteractivePreference +import app.morphe.patches.shared.misc.settings.preference.PreferenceScreenPreference +import app.morphe.patches.shared.misc.settings.preference.SwitchPreference +import app.morphe.patches.youtube.layout.player.buttons.addPlayerBottomButton +import app.morphe.patches.youtube.layout.player.buttons.playerOverlayButtonsHookPatch +import app.morphe.patches.youtube.layout.player.icons.copyPlayerButtonIcons +import app.morphe.patches.youtube.layout.sponsorblock.ControlsOverlayFingerprint +import app.morphe.patches.youtube.misc.extension.sharedExtensionPatch +import app.morphe.patches.youtube.misc.playercontrols.addLegacyBottomControl +import app.morphe.patches.youtube.misc.playercontrols.initializeLegacyBottomControl +import app.morphe.patches.youtube.misc.playercontrols.legacyPlayerControlsPatch +import app.morphe.patches.youtube.misc.playertype.playerTypeHookPatch +import app.morphe.patches.youtube.misc.settings.PreferenceScreen +import app.morphe.patches.youtube.misc.settings.settingsPatch +import app.morphe.patches.youtube.shared.Constants.COMPATIBILITY_YOUTUBE +import app.morphe.patches.youtube.video.information.videoInformationPatch +import app.morphe.patches.youtube.video.information.videoTimeHook +import app.morphe.patches.youtube.video.videoid.hookVideoId +import app.morphe.patches.youtube.video.videoid.videoIdPatch +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction + +private const val EXTENSION_CLASS = + "Lapp/morphe/extension/youtube/niconicocomments/NiconicoCommentsController;" + +private const val EXTENSION_BUTTON = + "Lapp/morphe/extension/youtube/niconicocomments/NiconicoCommentsToggleButton;" + +private val niconicoCommentsResourcePatch = resourcePatch { + dependsOn(legacyPlayerControlsPatch) + + execute { + copyPlayerButtonIcons( + "niconicocommentsbutton", + "morphe_niconico_comments_button_on", + "morphe_niconico_comments_button_off" + ) + addLegacyBottomControl("niconicocommentsbutton") + } +} + +@Suppress("unused") +val niconicoCommentsPatch = bytecodePatch( + name = "Niconico comments", + description = "Adds an option to show live chat and chat replay as comments that scroll across the video." +) { + dependsOn( + sharedExtensionPatch, + settingsPatch, + playerTypeHookPatch, + videoIdPatch, + videoInformationPatch, + playerOverlayButtonsHookPatch, + legacyPlayerControlsPatch, + niconicoCommentsResourcePatch, + ) + + compatibleWith(COMPATIBILITY_YOUTUBE) + + execute { + PreferenceScreen.PLAYER.addPreferences( + PreferenceScreenPreference( + key = "morphe_niconico_comments_screen", + sorting = PreferenceScreenPreference.Sorting.UNSORTED, + preferences = setOf( + SwitchPreference( + "morphe_niconico_comments", + titleKey = "morphe_niconico_comments_screen_title", + summary = true + ), + NonInteractivePreference( + "morphe_niconico_comments_font_size", + tag = "app.morphe.extension.shared.settings.preference.SeekBarPreference", + selectable = true + ), + NonInteractivePreference( + "morphe_niconico_comments_opacity", + tag = "app.morphe.extension.shared.settings.preference.SeekBarPreference", + selectable = true + ), + NonInteractivePreference( + "morphe_niconico_comments_travel_duration", + tag = "app.morphe.extension.shared.settings.preference.SeekBarPreference", + selectable = true + ), + NonInteractivePreference( + "morphe_niconico_comments_max_length", + tag = "app.morphe.extension.shared.settings.preference.SeekBarPreference", + selectable = true + ) + ) + ) + ) + + hookVideoId("$EXTENSION_CLASS->newVideoLoaded(Ljava/lang/String;)V") + videoTimeHook(EXTENSION_CLASS, "setVideoTime") + + addPlayerBottomButton(EXTENSION_BUTTON) + initializeLegacyBottomControl(EXTENSION_BUTTON) + + // Add the comments overlay to the same player layout as the SponsorBlock overlay. + ControlsOverlayFingerprint.let { + it.clearMatch() // Parent fingerprint is shared and may have changed. + it.method.apply { + val checkCastIndex = it.instructionMatches.last().index + val frameLayoutRegister = getInstruction(checkCastIndex).registerA + addInstruction( + checkCastIndex + 1, + "invoke-static { v$frameLayoutRegister }, $EXTENSION_CLASS->initialize(Landroid/view/ViewGroup;)V" + ) + } + } + } +} diff --git a/patches/src/main/resources/addresources/values/youtube/strings.xml b/patches/src/main/resources/addresources/values/youtube/strings.xml index c4fc884136..deaea95f27 100644 --- a/patches/src/main/resources/addresources/values/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values/youtube/strings.xml @@ -1791,4 +1791,18 @@ Automatic recording is optional. It stops in incognito or when the account chang No series found Untitled episode Unavailable episode + + + Niconico comments + Show live chat and chat replay as comments that scroll across the video + Shows an overlay button in the video player. Tap to show or hide the comments, tap and hold for quick settings + Comment font size + Text size of the comments (12-40 sp) + Comment opacity + Opacity of the comment text and backgrounds + Travel duration + How many seconds comments take to cross the screen. Lower values move faster + Max comment length + Hides comments with more characters than this. Each emoji counts as one character + No limit diff --git a/patches/src/main/resources/niconicocommentsbutton/drawable/morphe_niconico_comments_button_off.xml b/patches/src/main/resources/niconicocommentsbutton/drawable/morphe_niconico_comments_button_off.xml new file mode 100644 index 0000000000..db0bd08c2a --- /dev/null +++ b/patches/src/main/resources/niconicocommentsbutton/drawable/morphe_niconico_comments_button_off.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/patches/src/main/resources/niconicocommentsbutton/drawable/morphe_niconico_comments_button_off_bold.xml b/patches/src/main/resources/niconicocommentsbutton/drawable/morphe_niconico_comments_button_off_bold.xml new file mode 100644 index 0000000000..6a5355e25e --- /dev/null +++ b/patches/src/main/resources/niconicocommentsbutton/drawable/morphe_niconico_comments_button_off_bold.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/patches/src/main/resources/niconicocommentsbutton/drawable/morphe_niconico_comments_button_off_fluent.xml b/patches/src/main/resources/niconicocommentsbutton/drawable/morphe_niconico_comments_button_off_fluent.xml new file mode 100644 index 0000000000..135eedf69a --- /dev/null +++ b/patches/src/main/resources/niconicocommentsbutton/drawable/morphe_niconico_comments_button_off_fluent.xml @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/patches/src/main/resources/niconicocommentsbutton/drawable/morphe_niconico_comments_button_off_ionicons.xml b/patches/src/main/resources/niconicocommentsbutton/drawable/morphe_niconico_comments_button_off_ionicons.xml new file mode 100644 index 0000000000..5b5dbe493e --- /dev/null +++ b/patches/src/main/resources/niconicocommentsbutton/drawable/morphe_niconico_comments_button_off_ionicons.xml @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/patches/src/main/resources/niconicocommentsbutton/drawable/morphe_niconico_comments_button_off_phosphor.xml b/patches/src/main/resources/niconicocommentsbutton/drawable/morphe_niconico_comments_button_off_phosphor.xml new file mode 100644 index 0000000000..2e37ce6efa --- /dev/null +++ b/patches/src/main/resources/niconicocommentsbutton/drawable/morphe_niconico_comments_button_off_phosphor.xml @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/patches/src/main/resources/niconicocommentsbutton/drawable/morphe_niconico_comments_button_off_phosphor_duotone.xml b/patches/src/main/resources/niconicocommentsbutton/drawable/morphe_niconico_comments_button_off_phosphor_duotone.xml new file mode 100644 index 0000000000..f9daba197c --- /dev/null +++ b/patches/src/main/resources/niconicocommentsbutton/drawable/morphe_niconico_comments_button_off_phosphor_duotone.xml @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/patches/src/main/resources/niconicocommentsbutton/drawable/morphe_niconico_comments_button_off_phosphor_fill.xml b/patches/src/main/resources/niconicocommentsbutton/drawable/morphe_niconico_comments_button_off_phosphor_fill.xml new file mode 100644 index 0000000000..819d7d3a78 --- /dev/null +++ b/patches/src/main/resources/niconicocommentsbutton/drawable/morphe_niconico_comments_button_off_phosphor_fill.xml @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/patches/src/main/resources/niconicocommentsbutton/drawable/morphe_niconico_comments_button_off_phosphor_light.xml b/patches/src/main/resources/niconicocommentsbutton/drawable/morphe_niconico_comments_button_off_phosphor_light.xml new file mode 100644 index 0000000000..2a82e36f6d --- /dev/null +++ b/patches/src/main/resources/niconicocommentsbutton/drawable/morphe_niconico_comments_button_off_phosphor_light.xml @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/patches/src/main/resources/niconicocommentsbutton/drawable/morphe_niconico_comments_button_off_sharp.xml b/patches/src/main/resources/niconicocommentsbutton/drawable/morphe_niconico_comments_button_off_sharp.xml new file mode 100644 index 0000000000..21f943d7da --- /dev/null +++ b/patches/src/main/resources/niconicocommentsbutton/drawable/morphe_niconico_comments_button_off_sharp.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/patches/src/main/resources/niconicocommentsbutton/drawable/morphe_niconico_comments_button_on.xml b/patches/src/main/resources/niconicocommentsbutton/drawable/morphe_niconico_comments_button_on.xml new file mode 100644 index 0000000000..35d3550f40 --- /dev/null +++ b/patches/src/main/resources/niconicocommentsbutton/drawable/morphe_niconico_comments_button_on.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/patches/src/main/resources/niconicocommentsbutton/drawable/morphe_niconico_comments_button_on_bold.xml b/patches/src/main/resources/niconicocommentsbutton/drawable/morphe_niconico_comments_button_on_bold.xml new file mode 100644 index 0000000000..118e3e80a8 --- /dev/null +++ b/patches/src/main/resources/niconicocommentsbutton/drawable/morphe_niconico_comments_button_on_bold.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/patches/src/main/resources/niconicocommentsbutton/drawable/morphe_niconico_comments_button_on_fluent.xml b/patches/src/main/resources/niconicocommentsbutton/drawable/morphe_niconico_comments_button_on_fluent.xml new file mode 100644 index 0000000000..2292636535 --- /dev/null +++ b/patches/src/main/resources/niconicocommentsbutton/drawable/morphe_niconico_comments_button_on_fluent.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/patches/src/main/resources/niconicocommentsbutton/drawable/morphe_niconico_comments_button_on_ionicons.xml b/patches/src/main/resources/niconicocommentsbutton/drawable/morphe_niconico_comments_button_on_ionicons.xml new file mode 100644 index 0000000000..98b716f199 --- /dev/null +++ b/patches/src/main/resources/niconicocommentsbutton/drawable/morphe_niconico_comments_button_on_ionicons.xml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/patches/src/main/resources/niconicocommentsbutton/drawable/morphe_niconico_comments_button_on_phosphor.xml b/patches/src/main/resources/niconicocommentsbutton/drawable/morphe_niconico_comments_button_on_phosphor.xml new file mode 100644 index 0000000000..e08650ae23 --- /dev/null +++ b/patches/src/main/resources/niconicocommentsbutton/drawable/morphe_niconico_comments_button_on_phosphor.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/patches/src/main/resources/niconicocommentsbutton/drawable/morphe_niconico_comments_button_on_phosphor_duotone.xml b/patches/src/main/resources/niconicocommentsbutton/drawable/morphe_niconico_comments_button_on_phosphor_duotone.xml new file mode 100644 index 0000000000..9614e0f04d --- /dev/null +++ b/patches/src/main/resources/niconicocommentsbutton/drawable/morphe_niconico_comments_button_on_phosphor_duotone.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/patches/src/main/resources/niconicocommentsbutton/drawable/morphe_niconico_comments_button_on_phosphor_fill.xml b/patches/src/main/resources/niconicocommentsbutton/drawable/morphe_niconico_comments_button_on_phosphor_fill.xml new file mode 100644 index 0000000000..db01e135dc --- /dev/null +++ b/patches/src/main/resources/niconicocommentsbutton/drawable/morphe_niconico_comments_button_on_phosphor_fill.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/patches/src/main/resources/niconicocommentsbutton/drawable/morphe_niconico_comments_button_on_phosphor_light.xml b/patches/src/main/resources/niconicocommentsbutton/drawable/morphe_niconico_comments_button_on_phosphor_light.xml new file mode 100644 index 0000000000..d96fd5be0d --- /dev/null +++ b/patches/src/main/resources/niconicocommentsbutton/drawable/morphe_niconico_comments_button_on_phosphor_light.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/patches/src/main/resources/niconicocommentsbutton/drawable/morphe_niconico_comments_button_on_sharp.xml b/patches/src/main/resources/niconicocommentsbutton/drawable/morphe_niconico_comments_button_on_sharp.xml new file mode 100644 index 0000000000..412df60c28 --- /dev/null +++ b/patches/src/main/resources/niconicocommentsbutton/drawable/morphe_niconico_comments_button_on_sharp.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/patches/src/main/resources/niconicocommentsbutton/host/layout/youtube_controls_bottom_ui_container.xml b/patches/src/main/resources/niconicocommentsbutton/host/layout/youtube_controls_bottom_ui_container.xml new file mode 100644 index 0000000000..2a614c826f --- /dev/null +++ b/patches/src/main/resources/niconicocommentsbutton/host/layout/youtube_controls_bottom_ui_container.xml @@ -0,0 +1,22 @@ + + + + + + From 715982644742ca770a1c61fe292a77bb1ce11296 Mon Sep 17 00:00:00 2001 From: RAMiK <60112744+RAMiK4@users.noreply.github.com> Date: Tue, 27 Oct 2026 12:16:34 +0400 Subject: [PATCH 096/189] feat(YouTube - Downloads): Add download item to the Shorts menu (#4099) --- .../youtube/patches/DownloadsPatch.java | 21 ++++- .../speed/ShortsPlaybackSpeedFlyoutPatch.java | 41 ++------- .../youtube/patches/utils/FlyoutUtils.java | 67 +++++++++++++- .../extension/youtube/settings/Settings.java | 1 + .../videoplayer/ShortsDownloadButton.java | 89 +++++++++++++++++++ .../interaction/downloads/DownloadsPatch.kt | 13 ++- .../addresources/values/youtube/strings.xml | 3 + 7 files changed, 192 insertions(+), 43 deletions(-) create mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/videoplayer/ShortsDownloadButton.java diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/DownloadsPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/DownloadsPatch.java index e3eb9b8a21..c184629125 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/DownloadsPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/DownloadsPatch.java @@ -35,8 +35,25 @@ public final class DownloadsPatch { * Appears to always be called from the main thread. */ public static boolean inAppDownloadButtonOnClick(String videoId) { + return downloadVideo(videoId); + } + + /** + * @return If videos are downloaded by Morphe instead of the app, + * so download buttons can be added where the app has none, such as the Shorts player. + */ + public static boolean isDownloadModeHandledByMorphe() { + return Settings.DOWNLOAD_MODE.get() == DownloadMode.EXTERNAL; + } + + /** + * Downloads a video with the download mode that is set. + * + * @return If the download was handled, otherwise the app downloads the video itself. + */ + public static boolean downloadVideo(String videoId) { try { - if (Settings.DOWNLOAD_MODE.get() != DownloadMode.EXTERNAL) { + if (!isDownloadModeHandledByMorphe()) { return false; } @@ -48,7 +65,7 @@ public static boolean inAppDownloadButtonOnClick(String videoId) { ExternalDownloadButton.launchDownloader(Utils.getActivity(), videoId, null); return true; } catch (Exception ex) { - Logger.printException(() -> "inAppDownloadButtonOnClick failure", ex); + Logger.printException(() -> "downloadVideo failure", ex); } return false; } diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/playback/speed/ShortsPlaybackSpeedFlyoutPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/playback/speed/ShortsPlaybackSpeedFlyoutPatch.java index a3bfb9097f..4ce6dc7284 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/playback/speed/ShortsPlaybackSpeedFlyoutPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/playback/speed/ShortsPlaybackSpeedFlyoutPatch.java @@ -10,9 +10,6 @@ import static app.morphe.extension.shared.StringRef.str; import android.content.Context; -import android.util.Pair; - -import java.util.List; import app.morphe.extension.shared.Logger; import app.morphe.extension.shared.ResourceUtils; @@ -21,8 +18,6 @@ import app.morphe.extension.youtube.patches.utils.FlyoutUtils; import app.morphe.extension.youtube.settings.Settings; import app.morphe.extension.youtube.settings.YouTubeActivityHook; -import app.morphe.extension.youtube.shared.EngagementPanel; -import app.morphe.extension.youtube.shared.ShortsPlayerState; /** * Adds a playback speed item to the flyout menu of the Shorts player. @@ -30,17 +25,6 @@ @SuppressWarnings("unused") public final class ShortsPlaybackSpeedFlyoutPatch { - /** - * Items of the menu of the Shorts player, to tell it apart from the other - * bottom sheets opened in the Shorts player, such as the share sheet. - */ - private static final List SHORTS_MENU_BUTTONS = List.of( - "ALIGN_LEFT", // Description. - "NERD_STATS", // Stats for nerds. - "FLAG", // Report. - "FEEDBACK" // Send feedback. - ); - /** * Time for the flyout to close, so the speed menu is not dismissed with it. */ @@ -55,32 +39,17 @@ public static boolean isPatchIncluded() { } /** - * Called by {@link FlyoutUtils} for every flyout menu that opens. + * Called by {@link FlyoutUtils} for the menu of the Shorts player. * - * @param isShortFlyout If the menu belongs to a Short in the feed or on a channel. - * @return If the menu is the menu of the Shorts player and gets the playback speed item. + * @return If the menu gets the playback speed item. */ - public static boolean showFlyoutButton(boolean isShortFlyout) { + public static boolean showFlyoutButton() { return Settings.SHORTS_PLAYBACK_SPEED_FLYOUT_BUTTON.get() - && ShortsPlayerState.isOpen() - && !isShortFlyout - && !VideoInformation.isLiveStream() - // Menus opened inside a panel, such as the menu of a comment. - && EngagementPanel.getLastOpenedPanelId() == null - && isShortsPlayerMenu(); - } - - private static boolean isShortsPlayerMenu() { - for (Pair button : FlyoutUtils.getVisibleFlyoutButtons()) { - if (SHORTS_MENU_BUTTONS.contains(button.first)) { - return true; - } - } - return false; + && !VideoInformation.isLiveStream(); } /** - * Called by {@link FlyoutUtils} if {@link #showFlyoutButton(boolean)} is true. + * Called by {@link FlyoutUtils} if {@link #showFlyoutButton()} is true. */ public static int addFlyoutButton(Object flyoutPanel, int nextButtonIndex) { try { diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/FlyoutUtils.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/FlyoutUtils.java index 17c263f89f..606a86e9e7 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/FlyoutUtils.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/FlyoutUtils.java @@ -70,8 +70,11 @@ import app.morphe.extension.youtube.patches.playback.speed.ShortsPlaybackSpeedFlyoutPatch; import app.morphe.extension.youtube.patches.utils.requests.ChannelIdRequest; import app.morphe.extension.youtube.settings.Settings; +import app.morphe.extension.youtube.shared.EngagementPanel; +import app.morphe.extension.youtube.shared.ShortsPlayerState; import app.morphe.extension.youtube.videoplayer.PictureinPictureButton; import app.morphe.extension.youtube.videoplayer.SaveToWatchLaterButton; +import app.morphe.extension.youtube.videoplayer.ShortsDownloadButton; import app.morphe.extension.youtube.videoplayer.VideoDelayDialogButton; import app.morphe.extension.youtube.whitelist.ChannelWhitelist; @@ -350,6 +353,17 @@ public boolean dispatchTouchEvent(MotionEvent ev) { * and only gets the playback speed item. */ private static boolean isShortsPlayerFlyout; + + /** + * Items of the menu of the Shorts player, to tell it apart from the other + * bottom sheets opened in the Shorts player, such as the share sheet. + */ + private static final List SHORTS_PLAYER_MENU_BUTTONS = List.of( + "ALIGN_LEFT", // Description. + "NERD_STATS", // Stats for nerds. + "FLAG", // Report. + "FEEDBACK" // Send feedback. + ); /** * Channel of the video of the open menu. It loads in the background, so the menu never waits for it. */ @@ -549,10 +563,11 @@ private static void addFlyoutElements(Object flyoutPanel) { // onto whatever this call adds instead. customItemTextRefs.clear(); - isShortsPlayerFlyout = ShortsPlaybackSpeedFlyoutPatch.isPatchIncluded() - && ShortsPlaybackSpeedFlyoutPatch.showFlyoutButton(isShortFlyout); + isShortsPlayerFlyout = (ShortsPlaybackSpeedFlyoutPatch.isPatchIncluded() + || ShortsDownloadButton.isPatchIncluded()) + && isShortsPlayerMenu(); if (isShortsPlayerFlyout) { - nextButtonIndex = ShortsPlaybackSpeedFlyoutPatch.addFlyoutButton(flyoutPanel, nextButtonIndex); + nextButtonIndex = addShortsPlayerFlyoutButtons(flyoutPanel, nextButtonIndex); } else { nextButtonIndex = addVideoFlyoutButtons(flyoutPanel, nextButtonIndex); } @@ -566,6 +581,43 @@ private static void addFlyoutElements(Object flyoutPanel) { PlayerFlyoutMenuComponentsFilter.resetTopFlyoutMenuVisible(); } + /** + * @return If the menu is the menu of the Shorts player, and not another bottom sheet + * opened in the Shorts player, such as the share sheet or the menu of a comment. + */ + private static boolean isShortsPlayerMenu() { + if (!ShortsPlayerState.isOpen() + || isShortFlyout + // Menus opened inside a panel, such as the menu of a comment. + || EngagementPanel.getLastOpenedPanelId() != null) { + return false; + } + for (Pair button : visibleFlyoutButtons) { + if (SHORTS_PLAYER_MENU_BUTTONS.contains(button.first)) { + return true; + } + } + return false; + } + + private static int addShortsPlayerFlyoutButtons(Object flyoutPanel, int nextButtonIndex) { + if (ShortsPlaybackSpeedFlyoutPatch.isPatchIncluded() + && ShortsPlaybackSpeedFlyoutPatch.showFlyoutButton()) { + nextButtonIndex = ShortsPlaybackSpeedFlyoutPatch.addFlyoutButton(flyoutPanel, nextButtonIndex); + } + + if (ShortsDownloadButton.isPatchIncluded()) { + final String flyoutVideoId = getFlyoutVideoId(); + nextButtonIndex = ShortsDownloadButton.addFlyoutButton( + flyoutPanel, + nextButtonIndex, + flyoutVideoId.isEmpty() ? VideoInformation.getVideoId() : flyoutVideoId + ); + } + + return nextButtonIndex; + } + private static int addVideoFlyoutButtons(Object flyoutPanel, int nextButtonIndex) { final String flyoutVideoId = getFlyoutVideoId(); @@ -628,6 +680,15 @@ private static int addVideoFlyoutButtons(Object flyoutPanel, int nextButtonIndex ); } + // The menus of Shorts in the feed have no download option. + if (ShortsDownloadButton.isPatchIncluded() && isShortFlyout) { + nextButtonIndex = ShortsDownloadButton.addFlyoutButton( + flyoutPanel, + nextButtonIndex, + flyoutVideoId + ); + } + if (VideoDelayPatch.isPatchIncluded()) { nextButtonIndex = VideoDelayDialogButton.addFlyoutButton( flyoutPanel, diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/Settings.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/Settings.java index 0f7fba4989..bb4074a733 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/Settings.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/Settings.java @@ -306,6 +306,7 @@ public class Settings extends SharedYouTubeSettings { public static final BooleanSetting SANITIZE_VIDEO_SUBTITLE = new BooleanSetting("morphe_sanitize_video_subtitle", FALSE); public static final BooleanSetting EXTERNAL_DOWNLOADER_QUICK_MENU = new BooleanSetting("morphe_external_downloader_quick_menu", FALSE, new ExternalDownloaderAvailability()); public static final BooleanSetting EXTERNAL_DOWNLOADER_MARK_WATCHED = new BooleanSetting("morphe_external_downloader_mark_watched", FALSE, new ExternalDownloaderAvailability()); + public static final BooleanSetting SHORTS_DOWNLOAD_FLYOUT_BUTTON = new BooleanSetting("morphe_shorts_download_flyout_button", TRUE, DOWNLOAD_MODE.availability(DownloadMode.EXTERNAL)); // Overlay buttons public static final BooleanSetting FULLSCREEN_VIDEO_SCALE_BUTTON = new BooleanSetting("morphe_fullscreen_video_scale_button", FALSE, true); diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/videoplayer/ShortsDownloadButton.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/videoplayer/ShortsDownloadButton.java new file mode 100644 index 0000000000..cf8c043c25 --- /dev/null +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/videoplayer/ShortsDownloadButton.java @@ -0,0 +1,89 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/4099 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.extension.youtube.videoplayer; + +import static app.morphe.extension.shared.StringRef.str; + +import android.graphics.drawable.Drawable; + +import androidx.annotation.Nullable; + +import app.morphe.extension.shared.Logger; +import app.morphe.extension.shared.ResourceUtils; +import app.morphe.extension.shared.Utils; +import app.morphe.extension.shared.spoof.SpoofAppVersionPatch; +import app.morphe.extension.youtube.patches.DownloadsPatch; +import app.morphe.extension.youtube.patches.VersionCheckPatch; +import app.morphe.extension.youtube.patches.utils.FlyoutUtils; +import app.morphe.extension.youtube.settings.Settings; + +/** + * Adds a download item to the menus of Shorts, which the app has no download option for. + * The item downloads with the download mode that is set. + */ +@SuppressWarnings("unused") +public final class ShortsDownloadButton { + + private static final boolean USE_EXPERIMENTAL_ICONS = VersionCheckPatch.IS_20_31_OR_GREATER + && !SpoofAppVersionPatch.isSpoofingToLessThan("20.31.00"); + + private static final String DOWNLOAD_ICON = USE_EXPERIMENTAL_ICONS + ? "yt_outline_experimental_download_vd_theme_24" + : "yt_outline_download_black_24"; + + /** + * Time for the menu to close, so a menu or dialog opened by the download is not dismissed with it. + */ + private static final long DOWNLOAD_DELAY_MILLISECONDS = 100; + + @Nullable + private static Drawable downloadIcon; + + private ShortsDownloadButton() { + } + + public static boolean isPatchIncluded() { + return false; // Modified during patching. + } + + /** + * Called by {@link FlyoutUtils} for the menu of the Shorts player and the menus of Shorts in the feed. + * + * @param videoId The Short of the menu, or an empty string if unknown. + */ + public static int addFlyoutButton(Object flyoutPanel, int nextButtonIndex, String videoId) { + try { + if (videoId.isEmpty() + || !Settings.SHORTS_DOWNLOAD_FLYOUT_BUTTON.get() + || !DownloadsPatch.isDownloadModeHandledByMorphe()) { + return nextButtonIndex; + } + + if (downloadIcon == null) { + downloadIcon = ResourceUtils.getDrawable(DOWNLOAD_ICON); + } + + return FlyoutUtils.addFlyoutButton( + flyoutPanel, + downloadIcon, + str("morphe_shorts_download_flyout_title"), + v -> { + FlyoutUtils.dismissFlyout(); + Utils.runOnMainThreadDelayed( + () -> DownloadsPatch.downloadVideo(videoId), + DOWNLOAD_DELAY_MILLISECONDS + ); + }, + nextButtonIndex + ); + } catch (Exception ex) { + Logger.printException(() -> "addFlyoutButton failure", ex); + return nextButtonIndex; + } + } +} diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/downloads/DownloadsPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/downloads/DownloadsPatch.kt index aa50718292..db92680e9a 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/downloads/DownloadsPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/downloads/DownloadsPatch.kt @@ -25,6 +25,7 @@ import app.morphe.patches.shared.misc.settings.preference.PreferenceScreenPrefer import app.morphe.patches.shared.misc.settings.preference.PreferenceScreenPreference.Sorting import app.morphe.patches.shared.misc.settings.preference.SwitchPreference import app.morphe.patches.shared.misc.settings.preference.TextPreference +import app.morphe.patches.youtube.layout.flyout.flyoutPatch import app.morphe.patches.youtube.layout.player.icons.copyPlayerButtonIcons import app.morphe.patches.youtube.misc.auth.authHookPatch import app.morphe.patches.youtube.misc.playercontrols.addTopControl @@ -38,6 +39,7 @@ import app.morphe.patches.youtube.shared.Constants.COMPATIBILITY_YOUTUBE import app.morphe.patches.youtube.video.information.videoInformationPatch import app.morphe.util.ResourceGroup import app.morphe.util.copyResources +import app.morphe.util.setExtensionIsPatchIncluded import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation import com.android.tools.smali.dexlib2.iface.reference.FieldReference @@ -64,7 +66,8 @@ private val downloadsResourcePatch = resourcePatch { tag = "app.morphe.extension.shared.settings.preference.ExternalDownloaderPreference", ), SwitchPreference("morphe_external_downloader_quick_menu", summary = true), - SwitchPreference("morphe_external_downloader_mark_watched", summary = true) + SwitchPreference("morphe_external_downloader_mark_watched", summary = true), + SwitchPreference("morphe_shorts_download_flyout_button", summary = true) ) ) ) @@ -96,6 +99,9 @@ private const val EXTENSION_IN_APP_DOWNLOADS_CLASS = "Lapp/morphe/extension/yout private const val EXTENSION_SPOOF_CLASS = "Lapp/morphe/extension/shared/spoof/SpoofVideoStreamsPatch;" +private const val EXTENSION_SHORTS_DOWNLOAD_BUTTON = + "Lapp/morphe/extension/youtube/videoplayer/ShortsDownloadButton;" + private const val EXTENSION_BUTTON = "Lapp/morphe/extension/youtube/videoplayer/ExternalDownloadButton;" private const val GET_GENERATED_REGISTRY = @@ -115,7 +121,8 @@ val downloadsPatch = bytecodePatch( legacyPlayerControlsPatch, fixProtoLibraryPatch, spoofVideoStreamsPatch, - elementProtoParserHookPatch + elementProtoParserHookPatch, + flyoutPatch ) compatibleWith(COMPATIBILITY_YOUTUBE) @@ -123,6 +130,8 @@ val downloadsPatch = bytecodePatch( execute { initializeTopControl(EXTENSION_BUTTON) + setExtensionIsPatchIncluded(EXTENSION_SHORTS_DOWNLOAD_BUTTON) + OfflineVideoEndpointFingerprint.method.apply { addInstructionsWithLabels( 0, diff --git a/patches/src/main/resources/addresources/values/youtube/strings.xml b/patches/src/main/resources/addresources/values/youtube/strings.xml index deaea95f27..abefa711f6 100644 --- a/patches/src/main/resources/addresources/values/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values/youtube/strings.xml @@ -542,6 +542,9 @@ After a video with the Ask button is opened once, the feature works with all vid Video Audio All options + Show download in Shorts menu + Adds an external downloader flyout menu item to the Shorts player + Download Voice over translation From e0ea264aae286f9dfbde07156b46e892762741ca Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Tue, 27 Oct 2026 08:58:24 +0000 Subject: [PATCH 097/189] chore: Update translations from Crowdin --- .../values-af-rZA/music/strings.xml | 29 +- .../values-af-rZA/youtube/strings.xml | 25 +- .../values-am-rET/music/strings.xml | 619 +++++++ .../values-am-rET/reddit/strings.xml | 121 +- .../values-am-rET/shared-youtube/strings.xml | 250 +++ .../values-am-rET/shared/strings.xml | 198 +++ .../values-am-rET/sponsorblock/strings.xml | 212 +++ .../values-am-rET/youtube/strings.xml | 1421 +++++++++++++++++ .../values-ar-rSA/music/strings.xml | 29 +- .../values-ar-rSA/youtube/strings.xml | 25 +- .../values-as-rIN/youtube/strings.xml | 3 + .../values-az-rAZ/music/strings.xml | 29 +- .../values-az-rAZ/youtube/strings.xml | 11 +- .../values-be-rBY/music/strings.xml | 4 + .../values-be-rBY/reddit/strings.xml | 7 +- .../values-be-rBY/shared-youtube/strings.xml | 5 + .../values-be-rBY/shared/strings.xml | 2 + .../values-be-rBY/youtube/strings.xml | 12 + .../values-bg-rBG/music/strings.xml | 29 +- .../values-bg-rBG/youtube/strings.xml | 26 +- .../values-bn-rBD/music/strings.xml | 29 +- .../values-bn-rBD/youtube/strings.xml | 25 +- .../values-bs-rBA/youtube/strings.xml | 3 + .../values-ca-rES/youtube/strings.xml | 3 + .../values-ckb-rIR/youtube/strings.xml | 5 + .../values-cs-rCZ/music/strings.xml | 29 +- .../values-cs-rCZ/youtube/strings.xml | 26 +- .../values-da-rDK/music/strings.xml | 29 +- .../values-da-rDK/youtube/strings.xml | 26 +- .../values-de-rDE/music/strings.xml | 29 +- .../values-de-rDE/youtube/strings.xml | 25 +- .../values-el-rGR/music/strings.xml | 29 +- .../values-el-rGR/youtube/strings.xml | 25 +- .../values-es-rES/music/strings.xml | 29 +- .../values-es-rES/youtube/strings.xml | 25 +- .../values-et-rEE/youtube/strings.xml | 3 + .../values-eu-rES/youtube/strings.xml | 3 + .../values-fa-rIR/music/strings.xml | 29 +- .../values-fa-rIR/youtube/strings.xml | 25 +- .../values-fi-rFI/music/strings.xml | 29 +- .../values-fi-rFI/youtube/strings.xml | 25 +- .../values-fil-rPH/music/strings.xml | 29 +- .../values-fil-rPH/youtube/strings.xml | 25 +- .../values-fr-rFR/music/strings.xml | 29 +- .../values-fr-rFR/youtube/strings.xml | 25 +- .../values-ga-rIE/music/strings.xml | 29 +- .../values-ga-rIE/shared-youtube/strings.xml | 12 +- .../values-ga-rIE/youtube/strings.xml | 82 +- .../values-gl-rES/youtube/strings.xml | 5 + .../values-gu-rIN/youtube/strings.xml | 3 + .../values-hi-rIN/music/strings.xml | 29 +- .../values-hi-rIN/youtube/strings.xml | 25 +- .../values-hr-rHR/music/strings.xml | 29 +- .../values-hr-rHR/youtube/strings.xml | 26 +- .../values-hu-rHU/music/strings.xml | 29 +- .../values-hu-rHU/youtube/strings.xml | 25 +- .../values-hy-rAM/youtube/strings.xml | 3 + .../values-in-rID/music/strings.xml | 29 +- .../values-in-rID/youtube/strings.xml | 25 +- .../values-is-rIS/music/strings.xml | 29 +- .../values-is-rIS/youtube/strings.xml | 25 +- .../values-it-rIT/music/strings.xml | 29 +- .../values-it-rIT/youtube/strings.xml | 25 +- .../values-iw-rIL/music/strings.xml | 29 +- .../values-iw-rIL/youtube/strings.xml | 26 +- .../values-ja-rJP/music/strings.xml | 29 +- .../values-ja-rJP/shared-youtube/strings.xml | 2 +- .../values-ja-rJP/youtube/strings.xml | 71 +- .../values-ka-rGE/youtube/strings.xml | 3 + .../values-kk-rKZ/youtube/strings.xml | 3 + .../values-km-rKH/youtube/strings.xml | 4 + .../values-kmr-rTR/youtube/strings.xml | 3 + .../values-kn-rIN/youtube/strings.xml | 3 + .../values-ko-rKR/music/strings.xml | 29 +- .../values-ko-rKR/youtube/strings.xml | 25 +- .../values-ky-rKG/youtube/strings.xml | 3 + .../values-lo-rLA/youtube/strings.xml | 3 + .../values-lt-rLT/music/strings.xml | 29 +- .../values-lt-rLT/youtube/strings.xml | 26 +- .../values-lv-rLV/youtube/strings.xml | 3 + .../values-mai-rIN/youtube/strings.xml | 3 + .../values-mk-rMK/youtube/strings.xml | 4 + .../values-mn-rMN/youtube/strings.xml | 3 + .../values-mr-rIN/youtube/strings.xml | 3 + .../values-ms-rMY/youtube/strings.xml | 3 + .../values-my-rMM/youtube/strings.xml | 3 + .../values-nb-rNO/music/strings.xml | 29 +- .../values-nb-rNO/youtube/strings.xml | 25 +- .../values-ne-rIN/youtube/strings.xml | 3 + .../values-nl-rNL/music/strings.xml | 29 +- .../values-nl-rNL/youtube/strings.xml | 26 +- .../values-or-rIN/youtube/strings.xml | 3 + .../values-pa-rIN/youtube/strings.xml | 3 + .../values-pl-rPL/music/strings.xml | 29 +- .../values-pl-rPL/youtube/strings.xml | 25 +- .../values-pt-rBR/music/strings.xml | 29 +- .../values-pt-rBR/youtube/strings.xml | 25 +- .../values-pt-rPT/music/strings.xml | 29 +- .../values-pt-rPT/youtube/strings.xml | 26 +- .../values-ro-rRO/music/strings.xml | 29 +- .../values-ro-rRO/youtube/strings.xml | 25 +- .../values-ru-rRU/music/strings.xml | 29 +- .../values-ru-rRU/youtube/strings.xml | 25 +- .../values-si-rLK/music/strings.xml | 2 + .../values-si-rLK/shared-youtube/strings.xml | 1 + .../values-si-rLK/shared/strings.xml | 2 + .../values-si-rLK/youtube/strings.xml | 10 + .../values-sk-rSK/music/strings.xml | 29 +- .../values-sk-rSK/youtube/strings.xml | 26 +- .../values-sl-rSI/music/strings.xml | 29 +- .../values-sl-rSI/youtube/strings.xml | 26 +- .../values-sr-rCS/music/strings.xml | 29 +- .../values-sr-rCS/youtube/strings.xml | 26 +- .../values-sr-rSP/music/strings.xml | 29 +- .../values-sr-rSP/youtube/strings.xml | 25 +- .../values-sv-rSE/music/strings.xml | 29 +- .../values-sv-rSE/youtube/strings.xml | 26 +- .../values-sw-rKE/youtube/strings.xml | 3 + .../values-ta-rIN/youtube/strings.xml | 3 + .../values-te-rIN/music/strings.xml | 2 + .../values-te-rIN/reddit/strings.xml | 4 +- .../values-te-rIN/youtube/strings.xml | 5 + .../values-th-rTH/music/strings.xml | 29 +- .../values-th-rTH/youtube/strings.xml | 25 +- .../values-tr-rTR/music/strings.xml | 29 +- .../values-tr-rTR/shared-youtube/strings.xml | 6 +- .../values-tr-rTR/youtube/strings.xml | 25 +- .../values-uk-rUA/music/strings.xml | 29 +- .../values-uk-rUA/shared-youtube/strings.xml | 16 +- .../values-uk-rUA/youtube/strings.xml | 67 +- .../values-ur-rIN/youtube/strings.xml | 3 + .../values-uz-rUZ/youtube/strings.xml | 3 + .../values-vi-rVN/music/strings.xml | 29 +- .../values-vi-rVN/youtube/strings.xml | 25 +- .../values-zh-rCN/music/strings.xml | 29 +- .../values-zh-rCN/youtube/strings.xml | 25 +- .../values-zh-rTW/music/strings.xml | 29 +- .../values-zh-rTW/youtube/strings.xml | 25 +- .../values-zu-rZA/music/strings.xml | 619 +++++++ .../values-zu-rZA/reddit/strings.xml | 121 +- .../values-zu-rZA/shared-youtube/strings.xml | 250 +++ .../values-zu-rZA/shared/strings.xml | 198 +++ .../values-zu-rZA/sponsorblock/strings.xml | 212 +++ .../values-zu-rZA/youtube/strings.xml | 1405 ++++++++++++++++ 144 files changed, 7971 insertions(+), 308 deletions(-) diff --git a/patches/src/main/resources/addresources/values-af-rZA/music/strings.xml b/patches/src/main/resources/addresources/values-af-rZA/music/strings.xml index 4180aa16ef..6860b1a72e 100644 --- a/patches/src/main/resources/addresources/values-af-rZA/music/strings.xml +++ b/patches/src/main/resources/addresources/values-af-rZA/music/strings.xml @@ -550,8 +550,29 @@ Vereiste koekies: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPISID Wys die huidige lyn op die sluitingsskerm, Android Auto en Bluetooth. Benodig Android 11 of nuwer Wys romaniseer-knoppie Wys die huidige lyn in die minispeler, met die snit na die onderskrif verskuif - Wys romaniseer-knoppie - Wissel die vertoonorde van Kunstenaar en Titel in die Mini-speler en MediaSession. AAN \"Kunstenaar - Titel\" en AF \"Titel - Kunstenaar\". Dit raak nie die soektog na lirieke nie. + Liedtitel/kunstenaar-formaat + Formaat van die snit wat saam met die lirieke in die geminimaliseerde speler en MediaSession getoon word. {t} is die titel, {a} is die kunstenaar en {v} is die weergawe, soos Akoesties, wanneer die liriekebron een verskaf. Teks tussen twee dubbelpunte word slegs getoon wanneer elke veranderlike daarin \'n waarde het; skryf \\: vir \'n gewone dubbelpunt. Elke lyn word beurtelings getoon en beweeg aan wanneer die lirieke verander + Lirieke-lynformaat + Formaat van die lirieke-lyn in die geminimaliseerde speler en MediaSession. {lrc} is die lyn en {n} is hoeveel keer agtereenvolgens dit gesing word. Teks tussen dubbelpunte word slegs getoon wanneer die lyn herhaal. Gebruik \\: vir \'n dubbelpunt + Die formaat moet {lrc} bevat + Kunstenaar-lyn sjabloon en animasie-tysberekening + Minimum skermtyd (ms) + Maksimum skermtyd (ms) + Slegs vir multilyn-animasies. Hou elke raam vir ten minste die minimum tyd. By die maksimum, skuif die kunstenaarlyn selfs wanneer die lirieke dieselfde bly. + Minimum moet 0 of groter wees. Maksimum moet groter as 0 en ten minste die minimum wees. + Lynlengte-beperkings + Lirieke- en kunstenaarlyn-beperkings, en volle-breedte karaktergewig + "Lirieke lynbeperking +Verdeel langer lirieke in gebalanseerde helftes. 0 skakel verdeling af." + "Kunstenaar lynlimiet +Verdeel die bevolkte snarlestroom in sikluse frames. 0 skakel verdeeling uit." + "Volbreedte-karaktergewig +Daar word op beide limiete toegepas. By 1.75 tel vier volbreedte-karakters as 7." + Voer heelgetal-limiete van 0 tot 10000 en \'n karaktergewig groter as 0 in + Instrumentele onderbreking + Word in die Mini-speler en MediaSession tydens \'n instrumentele onderbreking vertoon. Elke lyn is een animasie-frame. {t} is die titel, {a} is die kunstenaar en {lrc} is die laaste lirieke-lyn + Mini-speler en MediaSession tydversaaing + Verschuif wanneer die volgende reël in die miniplayer en MediaSession getoon word. Gebruik \'n negatiewe waarde as reëls laat verskyn, byvoorbeeld op die slotskerm Lirieke-bron? Me […] lirieke. […] :\"Lirieke\" Reguliere uitdrukking word toegepas op titels, kunstenaars en albums voordat lirieke opgesoek word en wanneer daar beoordeel word of twee titels ooreenstem. Los leeg om albei te deaktiveer. Die verstek verwyder algemene rommel soos \"Official Video\", \"Remaster\", \"【插曲】\" en \"- Topic\". @@ -559,6 +580,10 @@ Vereiste koekies: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPISID Vertaal lirieke met verskaffervertalings of Google Translate Wys romaniseer-knoppie Romaniseer lirieke met verskaffer-romanisering of Google Translate […] knoppie + Maak eerste letter hoofletter + Maak die eerste letter van elke liriekreël \'n hoofletter + Verwyder leuensteken aan die einde + Verwyder punte, kommas, dubbelpunte en puntkommas aan die einde van liriekreëls. Vraagtekens, uitroeptekens, ellipses, aanhalingstekens en hakies word behou Teks grootte Grootte van die lirieke teks Globale tydverskuiwing diff --git a/patches/src/main/resources/addresources/values-af-rZA/youtube/strings.xml b/patches/src/main/resources/addresources/values-af-rZA/youtube/strings.xml index 507156c127..5578ceaf57 100644 --- a/patches/src/main/resources/addresources/values-af-rZA/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-af-rZA/youtube/strings.xml @@ -393,6 +393,9 @@ Verslae is openbaar en word nagegaan voordat 'n kanaal by 'n lys gevoeg word. Ve Verberg winkel skakels Verberg video advertensies Verberg YouTube Premium-promosies + + + Laai… Huidige kanaal: Voeg toe aan witlys @@ -409,6 +412,9 @@ Verslae is openbaar en word nagegaan voordat 'n kanaal by 'n lys gevoeg word. Ve Kanaalsoekresultate Niks op hierdie kanaal gevind nie Kon nie hierdie kanaal deursoek nie + Video\'s + Plasinge + Geen plasinge op hierdie kanaal stem ooreen nie Relevansie Meeste gekyk Minste gekyk @@ -494,6 +500,9 @@ Nadat 'n video met die Ask-knoppie een keer oopgemaak is, werk die funksie met a Video Klank Alle opsies + Wys aflaai in Shorts-kieslys + Voeg \'n eksterne aflaaier-uitvoukieslysitem by die Shorts-speler + Aflaai Vertaalde stem over Lees video-onderskrifte hardop voor met behulp van teks-na-spraak op die toestel, gesinkroniseer met die afspeel @@ -1121,9 +1130,6 @@ Motoruitleg "Herstel die ou speleruitleg Beperking: Indien 'Herstel ou video-aksiebalk' afgeskakel is, mag die 'Meer video's' oorvleueling nie beskikbaar wees nie" - "Die aktivering hiervan kan sommige speler-uitlegkenmerke breek, soos die kommentaar-afdeling of 'Meer video's' wat onder die speler vertoon word. - -Die tablet-uitleg word nie meer deur YouTube ondersteun nie, en is gedeeltelik of volledig gebreek in onlangse YouTube-weergawes. Vir 'n werkende tablet-uitleg, patch YouTube 20.21.37" 21.11.48 - Herstel ou onderskrifgedrag 20.39.41 - Herstel ou stoor na snelys @@ -1626,4 +1632,17 @@ Automaties opname is opsioneel. Dit stop in incognito of wanneer die rekening ve Geen reekse gevind nie Ongenommerde aflaai Onbeskikbaar aflaai + + Niconico-kommentare + Wys regstreekse kletsteks en kletsteks-herhaling as kommentare wat oor die video rol + Wys \'n oorleg-knoppie in die videospeler. Tik om kommentare te wys of weg te steek, tik en hou in vir vinnige instellings + Kommentaar-lettergrootte + Teksgrootte van die kommentare (12-40 sp) + Kommentaar-opasiteit + Opasiteit van die kommentaar-teks en agtergronde + Reisduur + Hoeveel sekondes dit neem vir kommentaar om oor die skerm te beweeg. Laer waardes beweeg vinniger + Maksimum kommentarlengte + Steek kommentaar weg met meer karakters as hierdie. Elke emoji tel as een karakter + Geen limiet diff --git a/patches/src/main/resources/addresources/values-am-rET/music/strings.xml b/patches/src/main/resources/addresources/values-am-rET/music/strings.xml index ccfea4ed71..2b85e97486 100644 --- a/patches/src/main/resources/addresources/values-am-rET/music/strings.xml +++ b/patches/src/main/resources/addresources/values-am-rET/music/strings.xml @@ -17,58 +17,677 @@ Second \"item\" text" --> + የመጨረሻ የተጫወተው + የደንበኝነት ምዝገባዎች + ቪዲዮዎች + ሌሎች ውጤቶች + የተጫኑ ዘፈኖች + የተጫኑ አልበሞች + የተጫኑ አርቲስቶች + ማቆሚያ እና መቀጥያ ለመጠቀም የአስተናጋጁን የJam ፓች ያዘምኑ + የጃም ተራ ማጋራት + በአቅራቢያዎ ያለ Jam ያስተናግዱ ወይም ይቀላቀሉ እና በJam Layer በኩል ተራውን ያርሙ + የጃም ተራ ማጋራትን ያግብሩ + YouTube Music እንደገና ሲጀመር የJam ውህደቱን ያነቃቃል + ለመስራት Jam Layerን ያውርዱ + የቅርብ ጊዜውን የJam Layer ስሪት በብሮውዘርዎ ይክፈቱ + የጃም መቆጣጠሪያዎች + በአቅራቢያዎ ያለ Jam ያስተናግዱ ወይም ይቀላቀሉ እና ክፍለ ጊዜውን ያስተዳድሩ + የጃም ሌየር ፓኬጅ + ለአቅራቢያ ተራ ማጋራት የሚያገለግለውን የተጫነ ረዳት መተግበሪያ ይምረጡ + የተጫነ አጋዥ ጥቅልን ይጠቀሙ። ጥምረት የእርስዎን ይሁንታ እና የአካባቢ ብቃት ማረጋገጫ token ይጠይቃል + Wi-Fi ይብራ? + Wi-Fi ያብሩ፣ ከዚያ Jam-ን እንደገና ለመጀመር ወይም ለመቀላቀል ይሞክሩ + የWi-Fi ቅንብሮችን ይክፈቱ + ሰርዝ + ፓኬጅን ይጠቀሙ + ትክክለኛ የአንድሮይድ ፓኬጅ ስም ያስገቡ + ጥቅሉ ተቀምጧል። ለመገናኘት Jam Layer-ን ያዋቅሩ + ጃም ተቋርጧል + መጀመሪያ Jam Layerን ያግብሩ + በቅንብሮች ውስጥ የጃም ተራ ማጋራትን ያግብሩ፣ ከዚያም YouTube Musicን እንደገና ያስጀምሩ + መጀመሪያ የተመረጠውን የጃም ሌየር ፓኬጅ ይጫኑ + በመስተናጋጁ ላይ የሚታየውን ኮድ ያስገቡ። Wi-Fi Aware የLAN አውታረ መረቦቻቸው የተለያየ ቢሆንም እንኳ በአቅራቢያ ያሉ መሳሪያዎችን ማገናኘት ይችላል + በኮድ ይቀላቀሉ + ተቀላቀል + QR ስካን አድርግ + ስምንት-ፊደላት ኮዱን ያስገቡ ወይም የግብዣ መልእክቱን ይለጥፉ + ጃምዎን ይቀላቀሉ + ኮዱን ኮፒ አድርግ + የጃም ኮድ + QR አሳይ + ተጠናቅቋል + ወደ ጃምዎ ይጋብዙ + ይህንን ተረኛ ለመቀላቀል እና ለማስተካከል ስካን ያድርጉ። ጃሙን ማቆም ግብዣውን ይሰርዘዋል + ሊንኩን ኮፒ አድርግ + የጃም ግብዣ + ጃም እንደገና እየተገናኘ ነው፤ በቅርቡ እንደገና ይሞክሩ + ወደ ጃም ተጨምሯል + ጃም አይдоступም፤ ትራኩ አልተጨመረም + የእርስዎ ጃም + አብሮ ማዳመጥ + ጃምን ይቀላቀሉ + አብረው ያዳምጡ + ጃም እየተحدث ነው… + የዋይፋይ አስተናጋጅዎን በመፈለግ ላይ… + ሰዎች ይጠበቃሉ + %s ሰው ተገናኝቷል + %s ሰዎች ተገናኝተዋል + ይህንን ኮድ በአቅራቢያዎ በሚገኝ መሳሪያ ላይ ያስገቡ። Wi-Fi Aware የሚደገፍ ከሆነ በተለያዩ LANዎች ላይ ይሰራል በ %s ደቂቃዎች ውስጥ ያበቃል + ወደ %s ይግለብጡ? + ማሻሻያዎች ክፍት ናቸው + ማስተካከያዎች ተቆልፈዋል + በWi-Fi Aware ተገናኝቷል + በWi-Fi ተገናኝቷል + በብሉቱዝ ተገናኝቷል + በጃም ወቅት የማይደገፍ ነው + መቀላቀል ተሰርዟል + ከአስተናጋጅዎ ጋር እንደገና እየተገናኘ ነው… + የመጠባበቂያ ዝርዝርን ያጋሩ። ሁሉም ሰው ዘፈኖችን እንዲጨምር ያድርጉ + ሰዎችን ጋብዙ + እንግዶች ተራውን ማስተካከል ይችላሉ + ጃም አብቃ + አስተናጋጁ ሙዚቃውን ያጫውታል። በ YouTube Music መጠባበቂያ ዝርዝር ውስጥ ዘፈኖችን በቀጥታ ይጨምሩ፣ ያንቀሳቅሱ ወይም ያስወግዱ + ሁለቱንም መሳሪያዎች አቅራቢያዎ ያድርጉ። አስተናጋጁን እስክንፈልግ ድረስ ማቋረጥ ይችላሉ + መቀላቀልን ሰርዝ + ከጃም ውጣ + ጃም ጀምር + የጃም ሌየር አዘጋጅ + አስተዳድር + ዝርዝሮች + ክፈት + የዘፈን አማራጮች እየተጫኑ ነው… + የዘፈን አማራጮች አይገኙም፤ እንደገና ይሞክሩ + የዘፈን አማራጮችን መክፈት አልተቻለም + የጃም ሌየርን ለማውረድ የሚገኝ ብሮውዘር የለም + የአስተናጋጁን ዝርዝር በመጠባበቅ ላይ + መጀመሪያ በ YouTube Music ውስጥ ዘፈን ይክፈቱ + በአስተናጋጁ ላይ ይጫወቱ + ከJam ይውጡ እና በየአካባቢው ይጫወቱ + በJam ውስጥ ቀጥሎ ይጫወቱ + ወደ Jam ተረኛነት ይጨምሩ + ይህንን የJam ትራክ ይጫወቱ + የት እንደሚጫወት ይምረጡ + አስተናጋጁን ወደዚህ ያግኙ + ዘፈኑ ለመፈለግ ዝግጁ አልሆነም + ተራው ተቀይሯል፤ የእጅ ምልክቱን እንደገና ይሞክሩ + የራስጌ ሎጎ + መደበኛ + ብጁ + የአልበሙን የድምጽ ትራኮች አጫውት + ከሙዚቃ ቪዲዮ ይልቅ የአልበሙን የድምጽ ትራኮች ብቻ አጫውት። \'ቪዲዮ ስትሪሞችን አስመሰል\' የሚለውን ይጠይቃል + የድምፅ/ቪዲዮ መቀያየሪያን አብራ + የድምፅ ትራክ ስሪት እና ይፋዊ የሙዚቃ ቪዲዮን ለመቀያየር ይሞክሩ + የሙዚቃ ቪዲዮዎችን፣ ፖድካስቶችን ወይም ዘፈኖችን በሶስተኛ ወገን ማውረጃ አፕሊኬሽኖች ያውርዱ + ለማውረጃ ማውጫ ውጫዊ ማውረጃን ይጠቀሙ + የማውጫው የማውረድ አማራጭ በምትኩ የእርስዎን ውጫዊ ማውረጃ ይከፍታል + የተወረዱ ፋይሎች + "ምንም የተወረደ የለም +የተወረዱ ትራኮች እዚህ ይታያሉ" + %1$s • %2$s • %3$s ሜጋ ባይት + %1$s ትራኮች • %2$s ሜጋ ባይት + የወረዱ ፋይሎችን ፈልግ + አደራጅ + በአርቲስት አደራጅ + በርዕስ አደራጅ + በመጠን አደራጅ + ሁሉንም የወረዱ ፋይሎች አጥፋ + ሁሉንም የወረዱ ፋይሎች ላጠፋ? + ሁሉም የወረዱ ትራኮች ከዚህ መሳሪያ ላይ ይጠፋሉ + የወረዱ ፋይሎች ተጥፍተዋል + ምንም አልተገኘም + ለ %s የሚከናወኑ ተግባራት + ጫወት + ጫወት ወይም አቁም + ቀድሞው + ቀጣዩ + አቁም + ዝጋ + ሰርዝ + የወረደውን ፋይል ሰርዝ + የወረደውን ፋይል መሰረዝ ይፈልጋሉ? + %s ከዚህ መሳሪያ ላይ ይሰረዛል + የወረደው ፋይል ተሰርዟል + የወረደው ፋይል መሰረዝ አልተቻለም + የወረደው ፋይል ማጫወት አልተቻለም + የድምፅ ማውረዶች + ያለ ኢንተርኔት ማጫወት + ለማውረድ የታሰበው ሙዚቃ ሊታወቅ አልቻለም + ማውረድ ቀድሞውኑ ሂደት ላይ ነው + ቀድሞውኑ ወርዷል + ማውረድ ተጀምሯል + ማውረድ እየተጀመረ ነው… + እየወረደ ነው %s%% + የማውረድ ሂደት ተጠናቅቋል + ማውረድ አልተሳካም + ስለ Morphe + ማስታወቂያዎች + ፊድ + ጠቅላላ + ተጫዋች + ግጥሞች + ልዩ ልዩ + የYouTube Music ቋንቋ + የስርዓት ቋንቋ + Android 13 ወይም ከዚያ በላይ ያስፈልጋል + \'Music Premium ያግኙ\' የሚለውን ስም ደብቅ + የMusic Premium ማስታወቂያዎችን ደብቅ + የቪዲዮ ማስታወቂያዎችን ደብቅ + የድምጽ / ቪዲዮ መቀያየሪያውን ደብቅ + የማውረድ ቁልፍን ደብቅ + በፖድካስት ዝግጅት ውስጥ የማውረድ ቁልፍን ይደብቃል + የፍለጋ መደርደሪያውን ደብቅ + የማጣሪያ አሞሌውን ደብቅ + የግሪድ መደርደሪያዎችን ደብቅ + አግድም መደርደሪያዎችን ደብቅ + የዝርዝር መደርደሪያዎችን ደብቅ + የግጥሞች ፓነል + የShare ቁልፍን ደብቅ + የትርጉም ቁልፍን ደብቅ + \"አዲስ ከ\" መደርደሪያውን ደብቅ + የየዘፈን ዝርዝር መደርደሪያዎችን ደብቅ + የድጋሚ ማጫወቻ ቁልፍን ደብቅ + የመቀላቀያ ቁልፍን ደብቅ + የ\'ፈጣን ጥሪ\' መደርደሪያን ደብቅ + አስተያየቶች + በአስተያየት ክፍል ውስጥ የትኞቹ ክፍሎች እንዲታዩ እንደሚፈልጉ ይምረጡ + የማህበረሰብ መመሪያዎችን ደብቅ + የዐውዳዊ መልእክቱን ደብቅ + የኢሞጂ ቁልፍን ደብቅ + የመረጃ ቁልፍን ደብቅ + የጊዜ ማህተም ቁልፍን ደብቅ + ክሮስፌድ + ትራኮችን እርስ በርስ በተቃና ሁኔታ ያቀላቅሉ + ክሮስፌድን አግብር + የመጥፋት ቅስት + እኩል ኃይል + ቀጭን አያያዝ + ቀስ ያለ ሽግግር + ለስላሳ S-curve + ወጪ + መግቢያ + የክሮስፌድ ቆይታ + 250 ሚሊሰከንድ + 500 ሚሊሰከንድ + 750 ሚሊሰከንድ + 1 ሰከንድ + 2 ሰከንዶች + 3 ሰከንዶች + 4 ሰከንዶች + 5 ሰከንዶች + 6 ሰከንድ + 7 ሰከንዶች + 8 ሰከንዶች + 9 ሰከንዶች + 10 ሰከንዶች + 11 ሰከንዶች + 12 ሰከንዶች + በእጅ በሚደረግ ዝላይ ላይ ክሮስፌድ አድርግ + በእጅዎ ወደ ቀጣዩ ትራክ በሚዘልቁበት ጊዜ የክሮስፌድ ተፅእኖን ይተገብራል + በራስ-ሰር እድገት ላይ ክሮስፌድ አድርግ + አንድ ትራክ በተፈጥሮ ሲያበቃ የክሮስፌድ ተፅእኖን ይተገብራል + የሴሽን ቁጥጥር + ክሮስፌድን ለማቆም ወይም ለመቀጠል የሻፍል ቁልፉን ለረጅም ጊዜ ተጭነው ይያዙ + ለዚህ ስብስብ ክሮስፌድ ተቋርጧል + ክሮስፌድ ቀጥሏል + ክሮስፌድ በሚበራበት ጊዜ የቪዲዮ ሁነታ አይتوفرም + ከSABR መልሶ ማጫወት ጋር Crossfade ጠፍቷል + በካስቲንግ ወቅት Crossfade ጠፍቷል + እንዴት እንደሚሰራ + "Crossfade የእርስዎን የቆይታ ጊዜ እና ኩርባ በመጠቀም የአንድን ዘፈን መጨረሻ ከሚቀጥለው ዘፈን መጀመሪያ ጋር ያጣምራል + +ይህ በራስ-ሰር ወደሚቀጥለው ሲሸጋገር እና በእጅ በሚዘለልበት ጊዜ ይሰራል። በራስ-ሰር ሽግግር ላይ ውህዱ ዘፈኑ ከመጠናቀቁ ጥቂት ሰከንዶች በፊት ይጀምራል" + ውስኖች + "Crossfade በርቶ ባለበት ወቅት የሙዚቃ ቪዲዮዎች እንደ ኦዲዮ ብቻ ይጫወታሉ። Crossfade-ን ለማቆም እና ቪዲዮውን ለማየት shuffle-ን ተጭነው ይያዙ + +መልሶ ማጫወት የጀመረ የሙዚቃ ቪዲዮ እስከ ቀጣዩ ዘፈን ድረስ በቪዲዮ ሁኔታ ይቆያል + +ከfade ቆይታ ያነሱ ዘፈኖች በcrossfade አይጫወቱም + +የመቆለፊያ ስክሪኑ ወይም ማሳወቂያው በሽግግር ወቅት ለአጭር ጊዜ የቀድሞ ሁኔታን ሊያሳይ ይችላል + +የሙዚቃ ቪዲዮ ለማየት Crossfade-ን ማቆም አንዳንድ ቪዲዮዎች ጥቁር እንዲሆኑ ሊያደርግ ይችላል። ዘፈኑን ከመምረጥዎ በፊት Crossfade-ን ማቆም ይህንን ያስቀረዋል" + የማይደገፍ + "የድምፅ ካስቲንግ ወይም ሚረሪንግ (Chromecast፣ ስክሪን ወይም የድምፅ ሚረሪንግ) + +SABR የመጫወቻ ሂደት (የቪዲዮ ስትሪሞች ማስመሰል ተጠፍቷል)" + ክሬዲት + የክሮስፌድ ባህሪ በ VazerOG የቀረበ + የድጋሚ ማጫወቻ ሁኔታን አስታውስ + የመቀላቀል ሁኔታን አስታውስ + የማጫወት ፍጥነት + ለሁሉም ትራኮች የሚያገለግል ፍጥነት። በ100% ላይ የመተግበሪያው ፍጥነት ጥቅም ላይ ይውላል፣ ለምሳሌ የፖድካስት ማጫወቻ ፍጥነት + ፒችን በፍጥነት ይቀይሩ + የማይወዱትን አቅጣጫ ማስወገድ አግድ + የማይወዱ ቁልፍ ሲጫን ወደ ቀጣዩ ሙዚቃ እንዳይዘለል ይከላከላል + የየዘፈን ዝርዝር ትራክ tapped ተግባር + የዘፈን ዝርዝሩን አጫውት + የተመረጠውን ትራክ ብቻ አጫውት + የተመረጠውን ትራክ ወደ ተረኛ ዝርዝር አክል + ወደ ተራው ተጨምሯል + የCast ቁልፍን ደብቅ + የHistory ቁልፍን ደብቅ + የNotification ቁልፍን ደብቅ + የSearch ቁልፍን ደብቅ + የድምፅ ፍለጋ ቁልፍን ደብቅ + የድምፅ ፍለጋ ቁልፍን ደብቅ + የአዲስ ቁልፍን ደብቅ + በላይብረሪ ታብ ውስጥ ያለውን \"አዲስ\" ቁልፍ ይደብቃል + የድርጊት ቁልፎች + ከተጫዋቹ በታች የትኞቹ የድርጊት ቁልፎች እንደሚታዩ ይምረጡ + የድርጊት አሞሌውን ደብቅ + የወደድኩት እና ያልወደድኩት ቁልፎችን ደብቅ + የአስተያየቶች ቁልፉን ደብቅ + \'የቀጥታ የውይይት መልሶ ማጫወቻ\' ቁልፍን ደብቅ + የግጥሞች ቁልፉን ደብቅ + የአጋራ ቁልፉን ደብቅ + የሴቭ ቁልፉን ደብቅ + የአውርድ ቁልፍን ደብቅ + \'ራዲዮ ጀምር\' ቁልፍን ደብቅ + የፍላይአውት ሜኑ + በተጫዋቹ እና በሰልፉ ፍላይአውት ሜኑዎች ውስጥ የትኞቹ ክፍሎች እንደሚታዩ ይምረጡ + የ3-አምድ ክፍለ አካልን ደብቅ + የወደድኩት እና ያልወደድኩት ቁልፎችን ደብቅ + የ\'የጣዕም ተዛማጅነት\' ሜኑን ደብቅ + \'በኋላ አዳምጥ\' ዝርዝር ውስጥ አክል ሜኑን ደብቅ + \'ወደ ተራው አክል\' ዝርዝርን ደብቅ + የርዕስ ገለጻ ሜኑን ደብቅ + \'የመጫወቻ ዝርዝርን አጥፋ\' ዝርዝርን ደብቅ + \'ሰልፉን አጥፋ\' ሜኑን ደብቅ + \'ይህን አርቲስት አትጠቁመኝ\' ሜኑን ደብቅ + የማውረጃ ሜኑን ደብቅ + \'የመጫወቻ ዝርዝር አርም\' ሜኑን ደብቅ + \'ወደ አልበሙ ሂድ\' ሜኑን ደብቅ + \'ወደ አርቲስቱ ሂድ\' ሜኑን ደብቅ + \'ወደ ክፍል ሂድ\' የሚለውን ሜኑ ደብቅ + \'ወደ ፖድካስት ሂድ\' የሚለውን ሜኑ ደብቅ + የእርዳታ ሜኑን ደብቅ + \'ክፍሉን እንደተጫወተ ምልክት አድርግ\' የሚለውን ሜኑ ደብቅ + \'ፍላጎት የለኝም\' የሚለውን ሜኑ ደብቅ + \'በፈጣን ጥሪ ላይ ፒን አድርግ\' የሚለውን ሜኑ ደብቅ + \'ቀጥሎ አጫወት\' የሚለውን ሜኑ ደብቅ + የጥራት ሜኑን ደብቅ + \'ከላይብረሪ አስወግድ\' የሚለውን ሜኑ ደብቅ + \'ከየዘፈን ዝርዝር አስወግድ\' የሚለውን ሜኑ ደብቅ + የሪፖርት ሜኑን ደብቅ + \'ክፍሉን ለበኋላ ያስቀምጡ\' / \'በላይብረሪ ውስጥ ያስቀምጡ\' የሚለውን ሜኑ ደብቅ + \'በመጫወቻ ዝርዝር ውስጥ አስቀምጥ\' ሜኑን ደብቅ + የማጋራት ሜኑን ደብቅ + \'በዘፈቀደ አጫውት\' ሜኑን ደብቅ + \'የእንቅልፍ ሰዓት\' ሜኑን ደብቅ + \'ሬዲዮ ጀምር\' የሚለውን ሜኑ ደብቅ + \'ለባለሙያዎች የሚሆን ስታትስቲክስ\' የሚለውን ሜኑ ደብቅ + የደንበኝነት ምዝገባ ሜኑን ደብቅ + \'ከፈጣን ጥሪ ዝርዝር አውጣ\' የሚለውን ሜኑ ደብቅ + \'የዘፈኑን ዝርዝር እወቅ\' የሚለውን ሜኑ ደብቅ + የመጀመሪያ ገጽ + መደበኛ + ቻርቶች + ለበኋላ የተቀመጡ ክፍሎች + ፈልግ + ታሪክ + ቤተ-መጻሕፍት + የተወደዱ ሙዚቃዎች + የሙዚቃ ዝርዝሮች + ፈልግ + የደንበኝነት ምዝገባዎች + የቁልቁለት አቀማመጥን ያስገድድ + መሣሪያው ሲሽከረከር መተግበሪያው በቁልቁለት አቀማመጥ እንዲቆይ ያደርጋል + የሚኒ ፕሌየር ቀለም ቀይር + የትንሹን ማጫወቻ ቀለም ከሙሉ ስክሪን ማጫወቻው ጋር ያስማማል + የናቪጌሽን ባር ቀለም ቀይር + የናቪጌሽን ባር ቀለምን ከሙሉ ስክሪን ማጫወቻው ጋር ያስማማል + በሚኒ ፕሌየር ውስጥ የሚቀጥለውን ቁልፍ አሳይ + በሚኒ ፕሌየር ውስጥ የቀደመውን ቁልፍ አሳይ + የተገደደ ሚኒ ፕሌየርን አግብር + አዲስ ትራክ ሲጀምር ማጫወቻው በራሱ ወደ ሙሉ ስክሪን እንዳይሰፋ ይከላከላል + ሚኒፕሌየሩን በስላይድ ለማስወገድ ያብሩ + በትንሹ የተደረገውን ተጫዋች ወደ ታች በመሳብ ለማስወገድ ያስችላል + የአሰሳ አሞሌ + በታችኛው የናቪጌሽን ባር ላይ የትኞቹ ቁልፎች እንዲታዩ እንደሚፈልጉ ይምረጡ + የመነሻ ገጽ ቁልፍን ደብቅ + የናሙናዎች ቁልፍን ደብቅ + የፍለጋ ቁልፍን ደብቅ + የላይብረሪ ቁልፍን ደብቅ + የማሻሻያ ቁልፍን ደብቅ + የአሰሳ አሞሌውን ደብቅ + የአሰሳ ቁልፍ ስሞችን ደብቅ + ወደ ውስጥ አልገቡም። ለማስተካከል ይጫኑ + ውጣ + ተወጥተዋል + scrobbling ያብቁ + \'አሁን እየተጫወተ ያለውን\' ሁኔታ ላክል + አነስተኛ የዘፈን ርዝመት + ከዚህ አጭር የሆኑ ዘፈኖች scrobble አይደረጉም + የ scrobble መዘግየት በመቶኛ + የዘፈኑን ይህንን ያህል በመቶኛ ካዳመጡ በኋላ scrobble እንዲጀምር ያድርጉ + የscrobble መዘግየት ሰከንዶች + ለዚህ ያህል ሰከንዶች ካዳመጡ በኋላ scrobble ያድርጉ + የListenBrainz አካውንት + ተ உள்ገበ። አካውንትን ለማስተዳደር ይጫኑ + scrobblingን ለማንቃት የListenBrainz የተጠቃሚ ቶክን ያስገቡ + ቶክን + አረጋግጥ + እባክዎን ቶክን ያስገቡ + እያረጋገጠ ነው… + ትክክለኛ ቶክን! ተጠቃሚ: %s + የተሳሳተ ቶክን: %s + ያልታወቀ + ማረጋገጥ አልተሳካም: %s + ቶከኑ ተሰርዟል + ቶከኑ ተቀምጧል + ማስጠንቀቂያ: የተቀመጠው ቶከን ዋጋ የለውም! + የተወደዱ ትራኮችን ውደድ + በ YouTube Music ላይ ትራኮችን ሲወዱ በLast.fm ላይ በራስ-ሰር እንዲወደዱ ያድርጉ (እና ካልወደዱ ወይም ካልተስማሙ ፍቅሩን ያስወግዱ) + የLast.fm አካውንት + እንደ %s ገብተዋል። አካውንቱን ለማስተዳደር ይጫኑ + ለመግባት የLast.fm መለያ መረጃዎን ያስገቡ + የተጠቃሚ ስም + የይለፍ ቃል + ግባ + እባክዎ የተጠቃሚ ስም እና የይለፍ ቃል ያስገቡ + እየገባ ነው… + በስኬት ተገብተዋል! + ከ Last.fm ሰሽን ማግኘት አልተቻለም + መግባት አልተሳካም: %s + የ Last.fm መለያ መረጃዎች ተቀምጠዋል! + የሜታዳታ ማጽጃ + ሜታዳታን አጽዳ + ከ scrobbling በፊት የተለመዱ አላስፈላጊ ጽሁፎችን (እንደ \"Official Video\", \"Remaster\", \"Live\") ከርዕሶች፣ ከዘፋኞች እና ከአልበሞች በራስ-ሰር ያጽዳ + ብጁ የ regex ማጽጃ + ከዘፈን ርዕሶች፣ ከዘፋኞች ወይም ከአልበሞች ተጨማሪ ጽሁፎችን ለማስወገድ ብጁ regular expression ያስገቡ። ምሳሌ፦ (?i)\\s*\\((clean|explicit)\\) + አርቲስቱን እና ትራኩን ከርዕሱ ላይ ይውሰዱ + \" - \" የሚለውን መለያ በመጠቀም የጥበብ ባለሙያውን እና የትራኩን ስም ከቪዲዮው ርዕስ ውስጥ ያውጡ። ይህ የቻናሉ ስም እንደ ጥበብ ባለሙያ ስም ጥቅም ላይ ለሚውል መልሶ ለተጫነ ይዘት ጠቃሚ ነው + የጎደለውን አልበም ሙላ + ሙዚቃው አልበምን የማይያካትት ከሆነ፣ scrobbleዎ ሙሉ እንዲሆን በ Last.fm ላይ ይፈልገው + "Scrobbling የ YouTube Music አጠቃቀምዎን ከ Last.fm ወይም ListenBrainz ፕሮፋይልዎ ጋር በራስ-ሰር ያመሳምራል + +በስተጀርባ በሚደረግ ስርጭት ወቅት scrobbling መመሳሰሉን ለማረጋገጥ፣ የ YouTube Music የባትሪ አጠቃቀም \"Unrestricted\" መሆኑን ያረጋግጡ" + የሶስተኛ ወገን ግጥሞችን ተጠቀም + ከሶስተኛ ወገን አገልግሎት ግጥሞችን ያሳያል፣ ምንም ካልተገኘ ግን ወደ ተገነባው ግጥም ይመለሳል + ግጥሞችን በሚመለከቱበት ጊዜ ስክሪኑ ክፍት ሆኖ እንዲቆይ ያድርጉ + የግጥሞች ፓነል ክፍት በሚሆንበት ጊዜ ስክሪኑ እንዳይጠፋ ያደርጋል + የግጥም አገልግሎት + ማሳያ + ቅንጅት + ፓነል + የ AI የመገናኛ ነጥብ + ሁሉም አቅራቢዎች ተዘግተዋል - የተገነቡ ግጥሞች እና የተያያዙ ግጥሞች + የአቅራቢዎችን ቅደም ተከተል እንደገና ላስተካክል? + አቅራቢዎችን ወደነበሩበት መደበኛ የቅድሚያ ቅደም ተከተል መልስ + የተቀመጡ ግጥሞችን አጥፋ + በዚህ መሳሪያ ላይ የተቀመጡ ግጥሞችን ይሰርዙ + %1$s MB ተሰርዟል + ቶክን ያግኙ + የApple Music ሚዲያ-ተጠቃሚ-ቶክን + "የmedia-user-token ኩኪን ከ music.apple.com ይቅዱ + +ደረጃዎች፦ +1. በ music.apple.com ላይ እንደ አባል ተጠቃሚ ይግቡ +2. DevTools → Application → Cookies → music.apple.com ን ይክፈቱ +3. የ \"media-user-token\" ዋጋን ይቅዱ" + Apple Music: media-user-token ተቀምጧል + Apple Music: media-user-token ተሰርዟል + Apple Music: ዋጋ የሌለው media-user-token - እባክዎ ያረጋግጡ እና እንደገና ይሞክሩ + Spotify SpDc + "የ sp_dc ኩኪን ከ Spotify ይቅዱ። + +ደረጃዎች፦ +1. በ open.spotify.com ላይ ይግቡ +2. DevTools → Application → Cookies → open.spotify.com ን ይክፈቱ +3. የ \"sp_dc\" ዋጋን ይቅዱ" + Spotify: sp_dc ተቀምጧል + Spotify: sp_dc ተሰርዟል + Spotify: ዋጋ የሌለው sp_dc - እባክዎ ያረጋግጡ እና እንደገና ይሞክሩ + የApple Music token ምርጫ ነው። ያለ него፣ Lyrically እንደ አማራጭ ጥቅም ላይ ይውላል + Deezer ARL + "የ arl ኩኪውን ከ Deezer ላይ ይለጥፉ። + +ደረጃዎች፡ +1. በ deezer.com ላይ ይግቡ +2. DevTools → Application → Cookies → deezer.com ይክፈቱ +3. የ \"arl\" እሴትን ኮፒ ያድርጉ" + Deezer: arl ተቀምጧል + Deezer: arl ተሰርዟል + Deezer: ትክክል ያልሆነ arl - እባክዎን ያረጋግጡ እና እንደገና ይሞክሩ + የMusixmatch ቶክን + የራስዎን አካውንት ለመጠቀም ብቻ ቶከን ያክሉ። ይህ ከMusixmatch አንድሮይድ መተግበሪያ መውጣት አለበት፡ ከmusixmatch.com የተቀነሱ ቶከኖች ለቀድሞው የዴስክቶፕ ክላየንት የሚሆኑ በመሆናቸው APIው አይቀበላቸውም + Musixmatch ያለ ቶከንም ይሰራል፣ ስለዚህ ይህ ቦታ ባዶ ሊቀር ይችላል + የተጠቃሚ ቶከን + Musixmatch: የተጠቃሚ ቶከን ተቀምጧል + Musixmatch: የተጠቃሚ ቶከን ተሰርዟል + Musixmatch: ትክክል ያልሆነ የተጠቃሚ ቶከን - እባክዎን ያረጋግጡ እና እንደገና ይሞክሩ + የYouTube ኩኪዎች + "ከYouTube የግርጌ ጽሁፎችን እና የጊዜ ጽሁፎችን ለመጠየቅ የYouTube ኩኪዎችዎን እዚህ ይለጥፉ። + +ደረጃዎች፦ +1. youtube.comን በብราวዘር ይክፈቱ እና ይግቡ +2. DevTools (F12) → Application → Cookies → youtube.com ይክፈቱ +3. ሙሉውን የኩኪ ስትሪንግ ይቅዱ (በሴሚኮሎን የተለዩ key=value ጥንዶች) + +የሚያስፈልጉ ኩኪዎች፦ SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPISID, LOGIN_INFO" + YouTube: ኩኪዎች ተቀምጠዋል + YouTube: ኩኪዎች ተሰርዘዋል + YouTube: ትክክል ያልሆኑ ኩኪዎች - እባክዎን ያረጋግጡ እና እንደገና ይሞክሩ + አንድ መስመርን ተጭነው ይያዙ + በዘፈኑ ውስጥ ወደዚያ ቅጽበት ለመሄድ መስመሩን ይንኩ፣ ከጣትዎ ስር ያለውን ቃል ለመቅዳት ወይም ንካቱ በቃል ላይ ካልሆነ ሙሉውን መስመር ለመቅዳት ረጅም ተጭነው ይቆዩ። ማጥፋት ሁለቱንም ተግባራት ያስወግዳል + የክፍል ታጎችን አሳይ + እያንዳንዱን አዲስ የግጥም ክፍል እንደ Verse ወይም Chorus ባሉ ትናንሽ ታጎች ምልክት ያድርጉ + የቅጂ ቁልፍን አሳይ + ኦሪጅናል ግጥሞችን እንደ ተራ ጽሑፍ ለመቅዳት ይንኩ፤ ጥሬ የግጥም ምንጭ ፋይልን ወደ ዳውንሎድስ ለማስቀመጥ ረጅም ተጭነው ይቆዩ + የትርጉም ቁልፍን አሳይ + ትርጉሙን ለማብራት/ማጥፋት ይጫኑ፤ ለትርጉም ብቻ ለረጅም ጊዜ ይጫኑ። የአቅራቢ ትርጉሞችን፣ የGoogle Translateን ወይም ከOpenAI ጋር ተኳሃኝ የሆነ API ይጠቀማል + የሮማናይዝ በተን አሳይ + ሮማናይዜሽንን ለማብራት/ማጥፋት ይጫኑ፤ ለሮማናይዜሽን ብቻ ለረጅም ጊዜ ይጫኑ። የአቅራቢ ሮማናይዜሽንን፣ የGoogle Translateን ወይም ከOpenAI ጋር ተኳሃኝ የሆነ API ይጠቀማል + የአካባቢ ሮማናይዜሽን እና አሰላለፍ + አቅራቢው፣ የእርስዎ ሞዴል እና Google Translate ሳይሰሩ ሲቀሩ በመሳሪያው ላይ ሮማናይዜሽን ያድርጉ። ንባቡ በእያንዳንዱ የጊዜ ገደብ ስንኝ ቃል ይከተላል፤ የቃል ማመሳሰያ በርቶ መሆን አለበት። ቻይንኛ፣ ካንቶኒዝ፣ ጃፓናዊ፣ ኮሪያዊ፣ ግሪክ፣ ሩሲያዊ፣ ጆርጂያዊ፣ አርመንያዊ፤ ወደ 120 MB + %1$s በመነበብ ላይ ነው: %2$d%% + ንባቦች ዝግጁ ናቸው (%1$d ዝርዝሮች) + የማውረድ ሙከራ አልተሳካም - እንደገና ለመሞከር ይህንን አጥፍተው ያብሩት + የማደሻ በተን አሳይ + ወደ ቀጣዩ የሊሪክስ እጩ ለመቀየር ይጫኑ፤ በዘፈን ርዕስ እና በድምፃዊ ስም ሊሪክስ ለመፈለግ ለረጅም ጊዜ ይጫኑ + ግጥሞችን ፈልግ + የዘፈን ርዕስ + አርቲስት + የዘፈን ርዕስ እና የዘፋኝ ስም ያስገቡ + የነቁት አቅራቢዎች አብዛኞቹ ሌሎች ዘፈኖችን መፈለግ አይችሉም + ተቀምጧል + የተቀናጁ ቃላትን ወይም የድምፅ ክፍሎችን አድምቀው ያሳዩ + ቃላት በቃላት የሚገለጹ ግጥሞች በሚኖሩበት ጊዜ ቅድሚያ ይስጡ። ግጥሞቹ የቃላት ጊዜያትን የሚይዙ ከሆነ፣ እያንዳንዱን ቃል ወይም የድምፅ ክፍል ከሙዚቃው ጋር በተናበበ ጊዜ ያደምቃሉ + የጀርባ ድምጾችን ለየብቻ አሳይ + በቅንፍ ውስጥ የተጻፉ የጀርባ ድምጾችን፣ ለምሳሌ \"(Oh) I want you\"፣ ከሚዘመርበት መስመር አውጥቶ ወደ ራሱ ደብዘዝ ያለ መስመር ያሸጋግረዋል + የተጫወቱ ግጥሞችን ደብቁ + በአሁኑ ጊዜ እየተጫወተ ካለው መስመር በላይ ያሉትን የግጥም መስመሮች ደብቁ + ያልተጫወቱ ግጥሞችን ደብቁ + በአሁኑ ጊዜ እየተጫወተ ካለው መስመር በታች ያሉትን የግጥም መስመሮች ደብቁ + የ MediaSession ግጥሞችን በቁልፍ ስክሪን ላይ ያሳዩ + የአሁኑን ስንኝ በቁልፍ ስክሪን፣ በAndroid Auto እና በብሉቱዝ ላይ ያሳያል። Android 11 ወይም ከዚያ በላይ ያስፈልገዋል + የሚኒ ፕሌየር ግጥሞችን አሳይ + የአሁኑን ስንኝ በትንሽ ተጫዋች ውስጥ ያሳያል፣ ትራኩ ወደ ንዑስ ርዕስ ተዛውሯል + የዘፈን ርዕስ/አርቲስት ቅርፅ + በትንሹ ተጫዋች እና በሚዲያ ክፍለ-ጊዜ (MediaSession) ውስጥ ከግጥሞቹ ጋር የሚታየው የሙዚቃ ትራክ ቅርፅ። {t} ርዕስ ነው፣ {a} አርቲስት ነው እና {v} ስሪት (ለምሳሌ Acoustic) ነው፣ የግጥሙ ምንጭ ሲያቀርብ። በሁለት ነጥብ ሁለት ነጥቦች (colons) መካከል ያለው ጽሑፍ የሚታየው በውስጡ ያሉት ሁሉም ተለዋዋጮች ዋጋ ሲኖራቸው ብቻ ነው፤ ለተራ ነጥብ ሁለት ነጥብ \\: ይጠቀሙ። እያንዳንዱ መስመር በየተራ ይታያል፣ ግጥሙ በተቀየረ ቁጥር ወደ ቀጣዩ ይሸጋገራል + የግጥም መስመር ቅርፅ + በትንሹ ተጫዋች እና በሚዲያ ክፍለ-ጊዜ ውስጥ የግጥም መስመር ቅርፅ። {lrc} መስመሩ ነው እና {n} ደግሞ ስንት ጊዜ በተከታታይ እንደሚዘፈን ነው። በነጥብ ሁለት ነጥቦች መካከል ያለው ጽሑፍ የሚታየው መስመሩ ሲደጋገም ብቻ ነው። ለነጥብ ሁለት ነጥብ \\: ይጠቀሙ + ቅርፁ {lrc} ማካተት አለበት + የአርቲስት መስመር ቴምፕሌት እና የአኒሜሽን ጊዜ + ዝቅተኛ የስክሪን ጊዜ (ms) + ከፍተኛ የስክሪን ጊዜ (ms) + ለብዙ መስመር አኒሜሽኖች ብቻ። እያንዳንዱን ፍሬም ቢያንስ ለዝቅተኛው ጊዜ ያቆዩት። በከፍተኛው ደረጃ፣ ግጥሙ ተመሳሳይ ሆኖ ቢቆይም የአርቲስት መስመሩን ያሽራግፉት። + ዝቅተኛው 0 ወይም ከዚያ በላይ መሆን አለበት። ከፍተኛው ከ 0 በላይ እና ቢያንስ ከዝቅተኛው ጋር እኩል መሆን አለበት። + የመስመር ርዝመት ገደቦች + የግጥም እና የአርቲስት መስመር ገደቦች፣ እና ሙሉ-leWidth የፊደል ክብደት + "የግጥም መስመር ገደብ +ረጅም ግጥሞችን ወደ ተመጣጠኑ ግማሾች ይከፋፍሉ። 0 ክፍፍልን ያጠፋል።" + "የአርቲስት መስመር ገደብ +የተሞላውን የትራክ ቅርጸት ወደ ተሽከርካሪ ፍሬሞች ይከፍሉ። 0 ክፍፍልን ያጠፋል።" + "ሙሉ ስፋት ያላቸው የፊደላት ክብደት +ለሁለቱም ገደቦች ተፈጻሚ ይሆናል። በ 1.75 ላይ አራት ሙሉ ስፋት ያላቸው ፊደላት እንደ 7 ይቆጠራሉ።" + ከ 0 እስከ 10000 ያሉ ሙሉ ቁጥሮችን እና ከ 0 የሚበልጥ የፊደል ክብደትን ያስገቡ + የሙዚቃ መሳሪያዎች እረፍት + በሙዚቃ መሳሪያዎች እረፍት ወቅት በሚኒ ፕሌየር እና በMediaSession ውስጥ ይታያል። እያንዳንዱ መስመር አንድ የአኒሜሽን ፍሬም ነው። {t} ርዕሱ፣ {a} አርቲስቱ እና {lrc} የመጨረሻው የዘፈን ግጥም መስመር ነው + የሚኒ ፕሌየር እና የMediaSession የጊዜ ልዩነት + በትንሹ ተጫዋች እና በMediaSession ውስጥ ቀጣዩ መስመር የሚታይበትን ጊዜ ያዛባል። መስመሮች ዘግይተው የሚታዩ ከሆነ፣ ለምሳሌ በቁልፍ ስክሪኑ ላይ፣ አሉታዊ ዋጋ ይጠቀሙ + ማጣሪያዎች + የሜታዳታ regex + ግጥሞችን ከመፈለጋችን በፊት እና ሁለት ርዕሶች ተመሳሳይ መሆናቸውን ስናነጻጽር በርዕሶች፣ በባለሙያዎች እና በአልበሞች ላይ የሚተገበር መደበኛ መግለጫ። ሁለቱንም ለማሰናከል ባዶ ይተዉት። ነባሩ እንደ \"Official Video\", \"Remaster\", \"【插曲】\", እና \"- Topic\" ያሉ የተለመዱ አላስፈላጊ ጽሁፎችን ያስወግዳል + የግጥም ማጣሪያ regex + በዋናው የግጥም ጽሁፍ ላይ ብቻ የሚተገበር መደበኛ ስሊል (ለትርጉሞች ወይም ለሮማናይዜሽን አይደለም)። ለማሰረዝ ባዶ ይተውት + የመረጃ መስመር ምልክቶች + የዘፈን ግጥሞች በስህተት ወደ ታች ከተሰበሰቡ፣ ይህንን ያስተካክሉ ወይም ያጽዱ። በኮማ የተለዩ ምልክቶች፡ ምልክት እና ሁለት ነጥብ (:) ያለባቸው መስመሮች ከታች እንደ ክሬዲት መረጃ ይታያሉ። ለማጥፋት ባዶ ይተዉት + የመጀመሪያውን ፊደል በካፒታል ጻፍ + የእያንዳንዱን የዘፈን ግጥም መስመር የመጀመሪያ ፊደል በካፒታል ይጽፋል + የመጨረሻዎቹን ሥርዓተ ነጥቦች አስወግድ + በዘፈን ግጥም መስመሮች መጨረሻ ላይ ያሉትን ሙሉ ነጥቦች፣ ኮማዎች፣ ኮሎኖች እና ሴሚኮሎኖች ያስወግዳል። የጥያቄ ምልክቶች፣ የቃለ አጋኖ ምልክቶች፣ የ ellipsis ምልክቶች፣ ጥቅሶች እና ቅንፎች ይቀራሉ + የጽሁፍ መጠን + የግጥሞቹ ጽሁፍ መጠን + አጠቃላይ የጊዜ ልዩነት + የተቀናጁ ግጥሞች የሙዚቃውን አጫወት ይከተላሉ። ካልተጣጣሙ፣ የጊዜ ልዩነቱን ያስተካክሉ። የተቀናጁ ግጥሞችን ወደ ፊት ወይም ወደ ኋላ ያንሸራሽሩ። ግጥሞቹ ከዘፈኑ ቀድመው የሚመጡ ከሆነ አዎንታዊ ዋጋ ይጠቀሙ + ለግጥሞች ልዩነት የSponsorBlock ክፍሎችን ይጠቀሙ + SponsorBlock በሚዘለላቸው ክፍሎች ሳይጨምር አዛምድ እና ግጥሞቹን ወደ ተለቀቀው ኦዲዮ አዛጋ። \'ጠፋ\' SponsorBlock-ን ሙሉ በሙሉ ችላ ይላል + ይህ ትራክ ለማስገባት ምንም የቪዲዮ መለያ የለውም + ምንጭ፦ %s + ይህን LRC ለማስቀመጥ የቅጂውን ቁልፍ ረዘም አድርገው ይጫኑት፤ የጊዜ ስሌቱን አስተካክለው ለማህበረሰቡ አቅራቢዎች ማጋራት ከቻሉ እናመሰግናለን + በ Google ተተረጎመ + በ Google ወደ ሮማይዝ ተቀየረ + በ %s ተተርጉሟል + በ %s ወደ ሮማይዝ ተቀይሯል + በ %s ሮማናይዝ የተደረገ + ተተረጎመ + የትርጉም ቋንቋ + የሮማናይዜሽን ቋንቋ + በመተርጎም ላይ… + ግጥሞቹን መተርጎም አልተቻለም + ሮማይዝ ተደርጓል + ራስ-ሰር + ወደ ሮማይዝ እየተቀየረ ነው… + ግጥሞቹን ወደ ላቲን ፊደላት መቀየር አልተቻለም + ተርጉመው ብቻ + ሮማናይዝ አድርገው ብቻ + ሁለገብ ሁነታ + ወደ ክሊፕቦርድ ተቀድቷል + እየታደሰ ነው + ታድሷል + QuickLRC API ቁልፍ + QuickLRC ለወቅታዊው ትራክ ግጥሞችን ይጽፋል ወይም የጊዜ ቅንጅት ያደርጋል። ይህም ከላይ ከተጠቀሰው OpenAI-ተኳሃኝ endpoint ጋር የማይገናኝ የተለየ አገልግሎት ነው + የQuickLRC ቁልፍ ተቀምጧል። በግጥም ፍለጋው ዲያሎግ ውስጥ ያለውን የቅንጅት ቁልፍ፣ ወይም ትራኩ ግጥም በሌለውበት ጊዜ የሚመጣውን የፓነል ማሳሰቢያ ይጠቀሙ + QuickLRC API ቁልፍ ተቀምጧል + QuickLRC ያንን ቁልፍ አልተቀበለውም + የQuickLRC API ቁልፍ ተሰርዟል + QuickLRC ግጥም ለሌላቸው ትራኮች በክሬዲት ክፍያ ግጥሞችን ያቀናጃል ወይም ከትራኩ ይጽፋል። ቁልፍ ከተቀመጠ፣ ግጥም የሌላቸው ትራኮች የጽሁፍ ልውውጥ ያቀርባሉ እንዲሁም የፍለጋ ዲያሎጉ የቅንጅት ቁልፍ ያገኛል + የQuickLRC ማስተካከያ ይጠይቁ + እየተስተካከለ ነው + ማስተካከያውን ተግብር + የ QuickLRC ትራንስክሪፕሽን ይጠይቁ + በመጻፍ ላይ + ትራንስክሪፕሽን ተሰርዟል + ከ YouTube የተጫወቱ ትራኮች ብቻ ነው ትራንስክሪፕሽን ሊደረላቸው የሚችለው + የሚቀናጅ ግጥም የለም - በምትኩ እየተጻፈ ነው + QuickLRC ይህንን ትራክ ማቀናበር አልቻለም + ለዚህ ትራክ የ QuickLRC ክሬዲቶች አልቀዋል + "ግጥሞች በርዕስ እና በዘፋኝ ይዛመዳሉ፣ ያልተለመዱ ሜታዳታዎች ምንም ነገር ወይም የተሳሳተ ስሪት ሊያገኙ ይችላሉ። ስሪቶችን ለመቀየር ማደሻን (refresh) ይጠቀሙ። በ * ምልክት የተደረገላቸው አቅራቢዎች ቶከን ይፈልጋሉ። ሁሉም የነቃ አቅራቢዎች ሁልጊዜ በጠየቁት ቅደም ተከተል ላይ እንዲጠየቁ አይደረግም፣ ነገር ግን እንደ ፍጥነታቸው ምላሽ ይሰጣሉ።\n\nግጥሞች ከMorphe ጋር ግንኙነት በሌላቸው፣ በስፖንሰርሺፕ ወይም በድጋፍ በማይሰሩ ሶስተኛ ወገን አገልግሎቶች ይመጣሉ። እያንዳንዳቸው እንዳሉ ጥቅም ላይ ይውላሉ፡ ግጥሞቹ፣ ተገኝነታቸው እና የፈቃድ ስምምነታቸው በእነዚያ አገልግሎቶች እና ፈቃድ ሰጪዎቻቸው ዘንድ ይቀራሉ። እነሱን መጠቀምም በአገልግሎት ውላቸው ላይ የተመሰረተ ሊሆን ይችላል። ወደ ነቃው አገልግሎት የሚደርሰው የጠየቁት ፍለጋ ብቻ ነው - ርዕሱ፣ ዘፋኙ እና አገልግሎቱ በሚፈልግበት ጊዜ የቪዲዮ መታወቂያው (video ID)። አማራጭ የ AI endpoint እና የ QuickLRC ትራንስክሪፕሽን የሚሰሩት እርስዎ ካ yapıዳቸው በኋላ ብቻ ነው፣ እና የግጥሞቹን ጽሑፍ ወይም የትራኩን የቪዲዮ URL ይቀበላሉ።" + የሚጠቀሙባቸውን አቅራቢዎች ብቻ ያብሩ፣ ወይም ነባሩን ይጠቀሙ - ተከታታይ ጥያቄዎች ትናንሽ የማህበረሰብ ግጥም ድረ-ገጾችን ሊያጨናንቁ ይችላሉ + የብድር መስመሩን ደብቅ + ከግጥሞቹ በታች የሚታየውን የብድር መስመር ደብቅ + ትርጉም እና ሮማናይዜሽንን ቀያይር + ON ሮማናይዜሽንን ከግጥሞች በታች እና ትርጉሙን በላይ ያደርጋል፤ OFF ሮማናይዜሽንን በላይ እና ትርጉሙን በታች ያደርጋል + ትርጉም + ሮማናይዜሽን + የትርጉም አቅራቢ + Google (መደበኛ፣ ነፃ) + ከOpenAI ጋር ተኳሃኝ (ብጁ) + የሮማናይዜሽን አቅራቢ + Google (መደበኛ) + የአካባቢ / ያለ መስመር ዲክሽነሪ + ከOpenAI ጋር ተኳሃኝ (ልዩ) + የ AI ክፍል + አቅራቢው እንደ Chorus ወይም Verse ያሉ የክፍል ምልክቶችን ካላቀረበ እና የ OpenAI-ተኳሃኝ ሞዴልዎ እነዚህን ሲያገኝ ብቻ ጥቅም ላይ ይውላል + AI እና የክላውድ Endpoints + ለ AI አቅራቢዎች የ API ቁልፎችን፣ ሞዴሎችን፣ ኤንድፖይንቶችን እና የ prompted ንድፎችን ያዋቅሩ። + የ API ኤንድፖይንት URL (ለምሳሌ https://api.openai.com/v1/chat/completions) + የPrompt ንድፍ + ቦታ ማስያዣዎች: {language}, {task}, {title}, {artist}, {count}, {lines} + መመሪያው ተቀምጧል + ማሳሰቢያው ተቀምጧል፣ ነገር ግን የAI endpoint ገና ጥቅም ላይ ሊውል አይችልም። በመጀመሪያ በAI Settings ውስጥ ያዋቅሩት + መመሪያው ተቀንሷል + ትክክለኛ JSON አይደለም። ከመቀመጡ በፊት ያስተካክሉት ወይም መስኩን ያጽዱት + የትርጉም & የሮማናይዜሽን ቅንብሮች ተቀምጠዋል + ቅንብሮች ወደ መነሻው ተመልሰዋል + መሰረታዊ URL በ http:// ወይም https:// መጀመር አለበት + ግንኙነቱን ይሞክሩ + ኤንድፖይንቶችን ያዋቅሩ + ሁሉንም ኤንድፖይንቶች አሳይ + ግንኙነት እየተረጋገጠ ነው… + የትርጉም አቅራቢ + የሮማናይዜሽን አቅራቢ + ሞዴሎችን አምጣ + ሞዴሎች እየተፈለጉ ነው… + ሞዴል ይምረጡ + ሞዴሎችን ማምጣት አልተቻለም። እባክዎ የAPI ቁልፉን ወይም ግንኙነቱን ያረጋግጡ። + የGemini API ቅንብሮች + OpenAI ተኳሃኝ (ልዩ Endpoint) + የDeepL API ቅንብሮች + የAPI ቁልፍ + ሞዴል + መሰረታዊ URL + የAPI ቁልፍ (አማራጭ) + የትርጉም መጠየቂያ + ተጨማሪ የጥያቄ አካል (አማራጭ JSON) + የማረጋገጫ ቁልፍ (ነፃ ወይም ፕሮ) + የAPI ቁልፎችን፣ ሞዴሎችን፣ የመጨረሻ ነጥቦችን እና መጠየቂያዎችን ያዋቅሩ + 8.23.51 - ደመቅ ያሉ ስዕላዊ ምልክቶችን አጥፋ + 9.13.51 - የቀጥሎቹ፣ የዘፈን ግጥሞች እና ተዛማጅ ታቦችን መልስ diff --git a/patches/src/main/resources/addresources/values-am-rET/reddit/strings.xml b/patches/src/main/resources/addresources/values-am-rET/reddit/strings.xml index cf7776fdaf..42e377f908 100644 --- a/patches/src/main/resources/addresources/values-am-rET/reddit/strings.xml +++ b/patches/src/main/resources/addresources/values-am-rET/reddit/strings.xml @@ -15,4 +15,123 @@ Second \"item\" text" --> - + + የReddit የதனጂነት ፖሊሲ + የGoogle መግቢያ አይቻልም። ለመግባት ኢሜል ይጠቀሙ + ማስታወቂያዎች + የፊድ ማስታወቂያዎችን ደብቅ + የአስተያየት ማስታወቂያዎችን ደብቅ + የናቪጌሽን ባር + የመልሶች ቁልፍን ደብቅ + የቻት ቁልፍን ደብቅ + የፍጠር ቁልፍን ደብቅ + የዲስከቨር ወይም የኮሙኒቲዎች ቁልፍን ደብቅ + የጨዋታዎች ቁልፍን ደብቅ + ሳይድባር + በቅርቡ የተጎበኙትን መደርደሪያ ደብቅ + በReddit ላይ ያሉ ጨዋታዎችን መደርደሪያ ደብቅ + የReddit Pro መደርደሪያን ደብቅ + ስለ እኛ መደርደሪያን ደብቅ + የሀብቶች መደርደሪያን ደብቅ + ሌአውት + ጽሑፍን ቅዳ + በጽሁፎች እና አስተያየቶች ውስጥ ጽሑፍን ለመምረጥ እና ለመቅዳት ያስችላል። አስተያየቶችን ማጠፍን ያግዳል + ጽሑፍን ቅዳ በሚነቃበት ጊዜ አስተያየቶችን ማጠፍ አይሰራም + ብጁ ፎንት + ከታች ካለው አድራሻ ብጁ TTF ወይም OTF የፎንት ፋይል ያጫኑ። ይህንን ማብራት የስርዓቱን ፎንት አስገድድ የሚለውን ያጠፋዋል + የብጁ ፎንት ፋይል + TTF ወይም OTF የፎንት ፋይል ለመምረጥ ይጫኑ + የተመረጠው ፎንት - %s + መጀመሪያ ብጁ የፎንት ፋይል ይምረጡ + እባክዎ .ttf ወይም .otf የፎንት ፋይል ይምረጡ + ብጁ ፎንቱን ለመጫን አልተቻለም። ሌላ ፋይል ይምረጡ + ዘመናዊውን የቤት ገጽ UI አግድ + የስክሪን ሾት ፖፕ-አፕ አጥፋ + ስክሪን ሾት በሚነሳበት ጊዜ የሚመጣውን ፖፕ-አፕ ያጠፋል + የስርዓቱን ፎንት አስገድድ + ከ Reddit Sans ወይም Roboto ይልቅ የመሳሪያውን የስርዓት ፎንት ይጠቀሙ። ይህንን ማብራት ብጁ ፎንትንም ያጠፋዋል + የጠይቁን ቁልፍ ደብቅ + በፍለጋ አሞሌው ውስጥ ያለውን የጠይቁን ቁልፍ ይደብቃል + የማህበረሰቦች መደርደሪያን ደብቅ + በ subreddits ውስጥ ተዛማጅ ወይም የተጠቆሙ ማህበረሰቦችን መደርደሪያ ይደብቃል + ታዋቂ መደርደሪያዎችን ደብቅ + ታዋቂ መደርደሪያዎችን ከፊድ እና ከፍለጋ ጥቆማዎች ይደብቃል + የማሳወቂያ አስተያየት对话 ሳጥኑን አስወግድ + አንድን subreddit በሚጎበኙበት ጊዜ የማሳወቂያ ጥቆማ ዲያሎጉን በራሱ ያስወግዳል + በመነሻ ፊድ ውስጥ ፍሌሮችን አሳይ + በመነሻ የይዘት ፍሰት ውስጥ የጽሁፍ ፍሌር ምልክቶችን ያሳያል + አንዳንድ የይዘት ፍሰት ንጥሎች ፍሌር ላያሳዩ ይችላሉ + በፖስቶች ላይ የተመለከቱትን ብዛት አሳይ + ገደብ፦ የሚሠራው የ API ምላሹ የተመለከቱትን ብዛት ካካተተ ብቻ ነው + የፖስት የድርጊት አሞሌን ገልብጥ + የፖስቶቹን የድምጽ አሰጣጥ እና የአስተያየት ቁልፎችን ወደ ቀኝ በኩል፣ እንዲሁም የክሮስፖስት እና የአጋርነት ቁልፎችን ወደ ግራ በኩል ያዛውራል + የድምጽ አሰጣጥ እና የአስተያየት ቁልፎችን ቀያይር + የፖስቶቹን የአስተያየት ቁልፍ ከድምጽ አሰጣጥ ቁልፎች በፊት ያሳያል + የNSFW ማስጠንቀቂያ对话 ሳጥኑን አስወግድ + አንድን subreddit በሚጎበኙበት ጊዜ የ NSFW ይዘት ማስጠንቀቂያን በራሱ ይቀበላል + ልዩ ልዩ + ስለ እኛ + የፓች ስሪት እና ይፋዊ ሊንኮች + የማጋሪያ ሊንኮችን አጽዳ + ከጋር ተደረገ ሊንኮች ላይ የክትትል መጠይቅ መለኪያዎችን ያስወግዳል + ሊንኮችን በቀጥታ ክፈት + የውጭ ሊንኮችን በሚከፍቱበት ጊዜ የድጋሚ መ yönጫጫ ዩአርኤልዎችን ይዘላል + ሙሉ ስፋት ያለው የፊድ ሚዲያ + በፊድ እና በፖስት ዝርዝሮች ውስጥ ምስሎችን እና ቪዲዮዎችን ያለ ጎን ክፍተት እና ያለ ክብ ማዕዘኖች ከጫፍ እስከ ጫፍ ያሳያል + የሚዲያ ተመልካች የደበዘዘ ጥንካሬ + ምስሎችን እና ቪዲዮዎችን በሚመለከቱበት ጊዜ ከርዕስ ጽሁፉ ጀርባ ያለውን የጥቁር ጥላ ጥንካሬ፣ ከ 0 (ያለ ጥላ) እስከ 1 (የReddit ነባሪ) + የፊድ ቦታን ጠብቅ + አንድን ጽሁፍ፣ ምስል ወይም ቪዲዮ ከዘጉ በኋላ፣ የተከፈተው ጽሁፍ ወደ ላይ ከመሸጋገሩ ይልቅ የፊድ ቦታው እንዳይቀየር ያደርጋል + የፊድ ሚዲያ ከፍተኛ ቁመት + በፊድ ውስጥ ያሉ የምስሎች፣ የቪዲዮዎች እና የጋለሪዎች ከፍተኛ ቁመት፣ ከስፋታቸው ጋር ሲነጻጸር። ረጅም ሚዲያዎች ይቆረጣሉ። 1.33 የReddit መደበኛ ቁመት ነው + Redgifsን በድምፅ አጫውት + ብዙውን ጊዜ ድምፅ የሌለውን የReddit ቅጂ ከመጫወት ይልቅ ዋናውን የRedgifs ቪዲዮ ያጫውታል + የRedgifs HD ጥራት + የHD Redgifs ቪዲዮን ያጫውታል። የHD ቪዲዮዎች መጠናቸው ትልቅ ስለሆነ ለመጫን ሊቆሙ ይችላሉ + የማውረጃ ቦታ + ሁሉም አካውንቶች + የReddit መደበኛ፡ ምስሎች በ Pictures/Reddit፣ ቪዲዮዎች በ Download ውስጥ + ፎልደር: %s + የፋይል ስም: %s + የReddit የፋይል ስሞች + \"ሁሉንም አካውንቶች\" ይጠቀማል + የራስን ቦታ ይጠቀሙ + አስቀምጥ በ + የReddit ነባር ፎልደሮች + ነባር + ንዑስ ፎልደር + በፋይሉ ስም ውስጥ / በመጠቀም ንዑስ ፎልደሮችን ያክሉ + ፈልግ… + በዚህ መሳሪያ ላይ የፎልደር መራጭ አይገኝም + የፋይሉ ስም + የReddit ፋይል ስም + ቶከንን ለማስገባት ይንኩት። / ንዑስ ፎልደር ይፈጥራል። የማይታወቁ እሴቶች ወደ \"unknown\" ይቀየራሉ + ምስሎች: %s + ቪዲዮዎች: %s + አስቀምጥ + እንደገና አዘጋጅ + ሰርዝ + የሁሉም አካውንቶች የዳውንሎድ ቦታ ወደ Reddit መደበኛ ቅንብር ይመለስ? + የ%s የዳውንሎድ ቦታ ይሰረዝ? + በዳውንሎድ ቦታው ላይ ማስቀመጥ አልተቻለም። በምትኩ በReddit መደበኛ ፎልደር ውስጥ ተቀምጧል + የዳውንሎድ ፎልደሩ ከእንግዲህ አይገኝም። በምትኩ በReddit መደበኛ ፎልደር ውስጥ ተቀምጧል + የተወረደው ቪዲዮ ሊቀመጥ አልቻለም + ይህ ፎልደር ለውርደቶች ሊያገለግል አይችልም + %s (የአሁኑ) + %s (ወደ ውስጥ አልገቡም) + ስም አልተገለጸም (ወደ ውስጥ አልገቡም ወይም በኢንኮግኒቶ ሁኔታ ላይ ናቸው) + የሚዲያ ተመልካች ኦቨርሌን ደብቅ + ምስል ወይም ቪዲዮ ሲከፈት ርዕሱን፣ ቁልፎቹን እና የቪዲዮ መቆጣጠሪያዎቹን ይደብቃል። ለማሳየት ሚዲያውን ይንኩት + \"ውይይቱን ይመልከቱ\" የሚለውን ቁልፍ ደብቅ + በሚዲያ ተመልካቹ ግርጌ ላይ የሚገኘውን \"ውይይቱን ይመልከቱ\" የሚለውን ቁልፍ ይደብቃል + የሚዲያ ተመልካች ጥቁር የጀርባ ቀለም + ከቲሙ የጀርባ ቀለም ይልቅ በሚዲያ ተመልካቹ ውስጥ ለምስሎች እና ለቪዲዮዎች ጥቁር የጀርባ ቀለምን ይጠቀማል + ሊንኮችን በውጭ ይክፈቱ + ሊንኮችን በ uygulama ውስጥ ካለው ብሮውዘር ይልቅ በብሮውዘርዎ ይከፍታል + የተዘጉ አስተያየቶችን አስታውስ + የዘጉዋቸው አስተያየቶች ፖስቱን እንደገና ሲከፍቱት እንደተዘጉ ይቆያሉ + የመተግበሪያ ምልክት + የመተግበሪያ ምልክት ይምረጡ + መደበኛ + ያልታወቀ + diff --git a/patches/src/main/resources/addresources/values-am-rET/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-am-rET/shared-youtube/strings.xml index fd007eeb78..e5cb68ab09 100644 --- a/patches/src/main/resources/addresources/values-am-rET/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-am-rET/shared-youtube/strings.xml @@ -18,46 +18,296 @@ Second \"item\" text" --> + የመጀመሪያ ማስጀመሪያ አኒሜሽን + ተዘግቷል + ቀለም + ሞኖክሮም + ቅንብሩን ለማጠናቀቅ እንደገና ያስነሱ + የመተግበሪያ ስም + የጨለማ ጭብጥ ዳራ + ጥቁር እና ጥቁር + Material You (ጥቁር እና ጥቁር) + ክላሲክ YouTube + ጥቁር ሮዝ + ጥቁር ሰማያዊ + ጥቁር አረንጓዴ + ጥቁር ቢጫ + ጥቁር ብርቱካናማ + ጥቁር ቀይ + የተቀየረ ጥቁር ዳራ + የጥቁር ጭብጥ ዳራ ቀለም ያዘጋጁ + የመተግበሪያው ነባር + Material You (ገለልተኛ) + Material You (ዋነኛው) + Material You (ሁለተኛው) + Material You (ሶስተኛው) + የተቀየረ + የሚወርዱ ፋይሎች + የማውረጃ ዘዴ + መደበኛ + በመተግበሪያው ውስጥ አውርድ + የውጭ ማውረጃ + የዳውንሎደር ፓኬጅ ስም + የተጫነው የውጪ ዳውንሎደር መተግበሪያ ፓኬጅ ስም + የጥቅል ስምን ያስገቡ + ሌላ + መተግበሪያው አልተጫነም + %s አልተጫነም። እባክዎን ይጫኑት + "በጥቅል ስም %s የተጫነ መተግበሪያ ማግኘት አልተቻለም + +የጥቅል ስሙ ትክክል መሆኑን እና መተግበሪያው መጫኑን ያረጋግጡ" + የጥቅል ስም ባዶ ሊሆን አይችልም + ብጁ + የመተግበሪያ ስርጭት ምልክት + ኦሪጅናል + Morphe ጥቁር + Morphe ጥቁር ሰማያዊ + Morphe ቀላል + Morphe play light + Morphe play black + ልዩ + "ይህ በመተግበሪያ ማስጀምሪያዎ ላይ የሚታየውን ምልክት ይቀይራል። + +ይህ ቅንብራዊ በነዚህ ቦታዎች ላይ ያለውን ምልክት አይቀይርም፦ +• የመሣሪያው ሲስተም ቅንብሮች መተግበሪያ ዝርዝር +• የተወሰኑ የፑሽ ማሳወቂያዎች +• 'በ... ክፈት' መተግበሪያ መራጭ +• የAndroid Auto ምልክት + +ምልክቱን በሁሉም ቦታዎች ለመቀየር፣ እንደገና ፓች ያድርጉ እና የ'App icon' ፓች አማራጭን ይቀይሩ" + የማሳወቂያ ምልክት + ከመተግበሪያው ምልክት ጋር አመሳስል + Morphe play + የMorphe ማሳሰቢያ + የተመለከቱት ታሪክ እየተቀመጠ አይደለም።<br><br>ይህ höchst likely በDNS ማስታወቂያ தடுላጊ ወይም በኔትወርክ ፕሮክሲ ምክንያት የሆነ ሊሆን ይችላል።<br><br>ይህንን ለማስተካከል <b>s.youtube.com</b>ን በነጭ ዝርዝር ውስጥ ያስገቡ ወይም ሁሉንም የDNS தடுላጊዎች እና ፕሮክሲዎች ያጥፉ + እንደገና አታሳይ + የቪዲዮ ስትሪሞችን አስመስል + የማጫወት ችግሮችን ለመከላከል የደንበኛ ማስመሰል ያዋቅሩ + "ተስማሚ የቪዲዮ ስትሪም በመጠየቅ ለአብዛኛዎቹ ተጠቃሚዎች የቪዲዮ ማጫወቻ ችግሮችን ይፈታል። + +የ YouTube Premium ተጠቃሚ ከሆኑ ይህ ቅንብር ላይፈለግ ይችላል" + ምንም ዓይነት የደንበኛ ስትሪሞች ማግኘት አልተቻለም + መደበኛው ክሊየንት አይሰራም + "%1$s ተከታታይነት ባለው መልኩ በተደጋጋሚ አልሰራም፣ ነገር ግን %2$s ሰርቷል። + +%2$s መደበኛ ክሊየንት እንዲሆን ይደረግ?" + ቀይር + ይህንን ቅንብር ማጥፋት የቪዲዮ ማጫወቻ ችግሮችን ሊያስከትል ይችላል + ይህንን ለማብራት ይሞክሩ፦ የቪዲዮ ስትሪሞችን አስመስል > Android VR ይግቡ + መደበኛ ደንበኛ + Android VR ይግቡ + ወደ Android VR አልገቡም። ለመግባት እዚህ ይጫኑ + በአሁኑ ጊዜ ወደ Android VR ገብተዋል + ይህንን ባህሪ ለመጠቀም፣ \'Default client\'ን ወደ Android VR ይቀይሩ + ወደ Android VR ይግቡ + "ወደ Android VR መግባት ለአንዳንድ ተጠቃሚዎች የቪዲዮ ማጫወቻ ችግሮችን ሊያስተካክል ይችላል + +ለመግባት ከታች ያለውን ይቀጥሉ የሚለውን ይጫኑ" + ውጣ + መመሪያዎች + "የማግበሪያ ኮድ፦ %s + +1. 'ድረ-ገጹን ክፈት' የሚለውን ከታች በመንካት 'መሣሪያን አገናኝ' የሚለውን ድረ-ገጽ ክፈት +2. ከላይ የሚታየውን የማግበሪያ ኮድ ከክሊፕቦርድ ኮፒ አድርገህ አጋጥመው (ወይም ጻፈው) +3. ከተጠየቅህ በGoogle ውስጥ ግባ። ብዙ አካውንቶች ካሉህ ይህንን የYouTube አካውንት ምረጥ +4. 'ለYouTube VR ፍቃድ ስጥ' የሚለውን ንካ +5. 'መሣሪያው ተገናኝቷል' የሚለው መልዕክት ሲመጣ ወደዚህ መተግበሪያ ተመለስ" + የማግበሪያ ኮድ ለመስጠት አልተቻለም + የማረጋገጫ டோክን ለመስጠት አልተቻለም + በስኬት ተገብቷል + "ማረጋገጫ ተቀምጧል + +• የMorphe ቅንብሮች የAndroid VR ማረጋገጫን ወደ ውስጥ አያስገቡም/ወደ ውጭ አይልኩም +• ይህ መተግበሪያ ለ6 ወራት ጥቅም ላይ ካልዋለ Google ይህን ማረጋገጫ ያስወግደዋል +• የGoogle የይለፍ ቃልዎን ከቀየሩ ወይም ይህን መሣሪያ ከGoogle ቅንብሮችዎ ካስወገዱ እንደገና መግባት ይኖርብዎታል" + OAuth2 ለጊዜው ዝግጁ አይደለም (የAPI ጊዜው አልፏል) + የAndroid VR መግቢያ ጊዜ አልፏል፣ እባክዎ በድጋሚ ወደ VR ለመግባት ይሞክሩ + OAuth2 ለጊዜው አይتوለበልም (ሁኔታ %s) + OAuth2 ለጊዜው ዝግጁ አይደለም + ወደ VR መግባት አይቻልም፣ የድረ-ገጽ ማረጋገጫ አልተፈቀደም + ወደ VR መግባት አይቻልም፡ %s + ከAndroid VR ተወጥተዋል + የ VR መግቢያ ማረጋገጫ ትክክል አይደለም፣ እባክዎ በድጋሚ ወደ VR ለመግባት ይሞክሩ + የማሳወቂያ ሚዲያ መቆጣጠሪያዎች + በመቆለፊያ ስክሪንዎ እና በማሳወቂያ ፓነልዎ ላይ የሚታዩትን የplayback መቆጣጠሪያዎች ያሻሽሉ + የቀደመውን እና የሚቀጥለውን බොተኖች ደብቅ + በሚዲያ ማሳወቂያ፣ በመቆለፊያ ስክሪን እና በহেድፎን መቆጣጠሪያዎች ውስጥ የቀደመ እና የሚቀጥል බොተኖችን ይደብቃል + Seekbarን አግድ + በመቆለፊያ ስክሪን እና በማሳወቂያ ፓነል ላይ seekbarን እንዳይሳብ ይከላከላል + በቀድሞ እና በሚቀጥሉት ቁልፎች ፈልግ + ቪዲዮዎችን ከመቀየር ይልቅ ፍለጋ እንዲያደርጉ የሚዲያ ማሳወቂያ፣ የቁልፍ ስክሪን እና የጆሮ ማዳመጫ መቆጣጠሪያዎችን ይቀይራል + የኋላ ፍለጋ ርዝመት + በቀድሞው ቁልፍ ወደ ኋላ የሚመለስባቸው ሰከንዶች + የፊት ፍለጋ ርዝመት + በሚቀጥለው ቁልፍ ወደ ፊት የሚዘልባቸው ሰከንዶች + የፍለጋ ርዝመቱ በ1 እና %s ሰከንዶች መካከል መሆን አለበት። መደበኛው ጥቅም ላይ እየዋለ ነው + የ QUIC ፕሮቶኮልን አጥፋ + የ DRC ኦዲዮን አጥፋ + ኦሪጅናል የድምፅ ቋንቋን አስገድድ + በተተካ የድምፅ ስሪት ፋንታ ሁልጊዜ የቪዲዮውን ዋና የድምፅ ትራክ ያጫውታል + ዝምታን ዝለል + በቪዲዮ ኦዲዮ ውስጥ ያሉ ዝምታዎችን በራስ-ሰር ይዘላል + አነስተኛ የዝምታ ርዝመት + ቢያንስ ይህ ያህል የሚረዝሙ ዝምታዎች ብቻ አጥሮ ይቀነሳሉ። በሚቀጥለው ቪዲዮ ሲጀምር ተፈጻሚ ይሆናል + የኔትወርክ ፕሮክሲ + የተደገፉ የኔትወርክ ጥያቄዎችን በፕሮክሲ በኩል አሳልፍ + ፕሮክሲን አብራ + የተደገፉ የኔትወርክ ጥያቄዎችን በተ yapıጠረ የ HTTP ወይም HTTPS ፕሮክሲ በኩል ያሳልፋል + የፕሮክሲ ሆስት + የፕሮክሲ ሰርቨሩ የሆስት ስም ወይም የ IP አድራሻ + የፕሮክሲ ፖርት + የፕሮክሲ ሰርቨሩ ፖርት + HTTPS ፕሮክሲን ተጠቀም + ከ HTTP ይልቅ ወደ ፕሮክሲ ሰርቨሩ ለመገናኘት HTTPSን ተጠቀም + የፕሮክሲ ማረጋገጫ + ከፕሮክሲ ጥያቄዎች ጋር መሠረታዊ ማረጋገጫን ይልካል + የፕሮክሲ ተጠቃሚ ስም + ለመሰረታዊ የፕሮክሲ ማረጋገጫ የሚሆን የተጠቃሚ ስም + የፕሮክሲ የይለፍ ቃል + ለመሰረታዊ የፕሮክሲ ማረጋገጫ የሚሆን የይለፍ ቃል + ቀጥታ ተመላላሽ ይፍቀዱ + ፕሮክሲው ካልሰራ በቀጥታ ይገናኙ + ሙሉ ስክሪን ማስታወቂያዎችን ደብቅ + ይህንን ባህሪ ለመጠቀም፣ \'Spoof video streams\'ን ከ \'Android Studio\' እና \'Android VR Downgraded\' ውጭ ወደማንኛውም ክላያ змінюት + የማጋሪያ ሊንኮችን አጥራ + ከሚጋሩ ሊንኮች ላይ የክትትል ጥያቄ ፓራሜትሮችን ያስወግዳል + የမျှare ሊንኮችን ወደ youtube.com ይቀይሩ + በተጋሩ ሊንኮች ውስጥ music.youtube.comን በ youtube.com ይተካል + የማጋሪያ ሊንኮችን ወደ youtu.be ቀይር + ቪዲዮዎች አጭር የሆነውን youtu.be URL ፎርማትን እንዲጠቀሙ ያስገድዳል + የዲበግ ሎግ ፕሮቶኮል በፈር + ልማቶች የ приложение አካላትን እንዲለዩ ተጨማሪ የሎግ መረጃዎችን ይጨምራል + "ይህንን ቅንብር ማብራት ለአንዳንድ የUI ክፍሎች በስክሪኑ ላይ የሚታዩ ጽሑፎችን ጨምሮ ተጨማሪ የሌይአውት መረጃዎችን ይመዘግባል። + +ይህ ልዩ ማጣሪያዎችን በሚፈጥሩበት ጊዜ ክፍሎችን ለመለየት ይረዳል። + +ነገር ግን፣ ይህንን ማብራት እንደ IP አድራሻዎ ያሉ አንዳንድ የተጠቃሚ መረጃዎችንም ይመዘግባል" + ብጁ ማጣሪያ + ማንኛውንም አካል ለመደበቅ ብጁ ማጣሪያዎችን ያዋቅሩ + ብጁ ማጣሪያን አግብር + ለማጣሪያ የሚያገለግሉ የአካል መንገድ ግንባታ ስትሪንግዎች ዝርዝር፣ በአዲስ መስመሮች የተለዩ + የተሳሳተ ብጁ ማጣሪያ: %s + የቅንብሮች ሜኑ ማጣሪያ + በመደበኛው የአፕሊኬሽን ቅንብሮች ስክሪን ላይ ያሉ ነገሮችን በሚታዩት ስም መደበቅ + ዝርዝሩን ለመሙላት መደበኛውን የአፕሊኬሽን ቅንብሮች ስክሪን አንድ ጊዜ ይክፈቱ + የተመልካች ጥንቃቄ የውይይት ሳጥንን አስወግድ + ይህ የዕድሜ ገደብን አይሻገፍም። ዝም ብሎ በራሱ ይቀበለዋል + PoToken አቅራቢ + የማጫወት ችግሮችን ለመከላከል PoToken አቅራቢን ያዋቅሩ + የውጭ PoToken አቅራቢ + "የውጭ PoToken minter በመጠቀም የቪዲዮ ማጫወቻ ችግሮችን ይቀርፋል (ሙከራ) + +የYouTube Premium ተጠቃሚ ከሆኑ ይህ ቅንብር ላይያስፈልግ ይችላል" + "የውጫዊ PoToken minter በመጠቀም የቪዲዮ ማጫወቻ ችግሮችን ያስተካክላል (ሙከራ) + +ይህንን ባህሪ ለመጠቀም 'የቪዲዮ ስትሪሞችን አስመስል' የሚለውን ያጥፉ" + ካልሰራ MicroG በግዳጅ ለመዝጋት ይሞክሩ + የPotHelper ቅንብሮችን ክፈት + "ይህ ባህሪ በአሁኑ ጊዜ በልማት ላይ ነው እና ላይሰራ ይችላል + +PotHelperን ለማውረድ እዚህ ይንኩ" + PotHelper ከአሁን በኋላ አይሰራም + "በዚህ የPotHelper ስሪት ውስጥ የተካተተው ቶከን ከአሁን በኋላ አይሰራም + +የቪዲዮ ስርጭቶችን ለማስተካከል 'ቪዲዮ ስርጭቶችን አስመሰል' የሚለውን አብሩ" + አብራ + የመተግበሪያውን ስሪት አስመስል + የቆዩ የUI ባህሪያትን ለመመለስ የድሮ የመተግበሪያ ስሪት እየተጠቀሙ እንደሆነ ሰርቨሩ እንዲያስብ ያታልለዋል + "የመተግበሪያው ስሪት ወደ አሮጌ ስሪት እንዲመስል ይደረጋል። + +ይህ የመተግበሪያውን ገጽታ እና ባህሪያት ይቀይረዋል፣ ነገር ግን ያልተጠበቁ የጎንዮሽ ጉዳቶች ሊከሰቱ ይችላሉ። + +በኋላ ቢጠፋ፣ የ UI ስህተቶችን ለመከላከል የመተግበሪያውን ዳታ ማጽዳት ይመከራል" + የመተግበሪያ ስሪት ማስመሰያ ግብ + የመተግበሪያ ማደሻ ፍጥነት + መደበኛ + %s FPS + የማደሻ ፍጥነትን ጫንፍ + ሁልጊዜ + በቁመታዊ ማጫወቻ ጊዜ + የሙሉ ስክሪን ማጫወቻ + የቁመት እና የሙሉ ስክሪን ማጫወቻ + ፎንት + መደበኛ + የመሳሪያው ሲስተም ፎንት + የተበየለ የፎንት ፋይል + የተበየለ የፎንት ፋይል + የTTF ወይም OTF ፎንት ፋይል ይምረጡ + የተመረጠው ፋይል TTF ወይም OTF የፎንት ዓይነት አይደለም + ልዩ ፎንቱን መጫን አልተቻለም እና ተዘግቷል + አልወደዱም ለጊዜው አይገኙም (API ጊዜው አልፎበታል) + አልወደዱም አይገኙም (ሁኔታ %s) + አለመውደዶች አይገኙም (የደንበኛ API ገደብ) + አለመውደዶች አይገኙም (%s) + በባለቤቱ ተደብቋል + ይህንን ቪዲዮ ከ%s ሌሎች ሰዎች ጋር አላስወደዱትም + ይህንን ቪዲዮ አላስወደዱትም፣ %s የድምጽ ምርጫዎች አልወደዱም + Return YouTube Dislikeን አግብር + አለመውደዶችን በፐርሰንት አሳይ + አለመውደዶችን ከቁጥር ይልቅ ከላይኮች አንጻር በፐርሰንት ያሳያል + የተገመተ የላይክ ብዛትን አሳይ + ፈጣሪው ላይኮችን ባጠፋባቸው ቪዲዮዎች ላይ የተገመተ የላይክ ብዛት ያሳያል + የግንኙነት ስህተት ማስጠንቀቂያዎችን አሳይ + የ Return YouTube Dislike ሰርቨር በማይገኝበት ጊዜ ማሳወቂያ ያሳያል + "Return YouTube Dislike ዩቲዩብ በ 2021 የደበቃቸውን የ싫ት ብዛቶች ይመልሳል። ብዛቶቹ ከመወገዳቸው በፊት ከተቀመጠው መረጃ የተገኙ ሲሆን ለአዳዲስ ቪዲዮዎች ደግሞ ከተጠቃሚዎች ድምፅ የተሰሉ ግምቶች ናቸው፤ ስለዚህ ግምታዊ ናቸው እና በየጥቂት ቀናቱ ይዘመናሉ + +የሚመለከቱት የቪዲዮ መለያ ወደ Return YouTube Dislike API ይላካል፣ እንዲሁም ግምቶቹን ለማሻሻል የእርስዎ ምርጫዎች በስም ሳይገለጹ ይላካሉ" + የዚህ መሳሪያ Return YouTube Dislike API ስታቲስቲክስ + የ API ምላሽ ጊዜ፣ አማካይ + የ API ምላሽ ጊዜ፣ አነስተኛ + የ API ምላሽ ጊዜ፣ ከፍተኛ + የAPI ምላሽ ጊዜ፣ የመጨረሻው ሚዲያ + አልወደዱም ለጊዜው አይገኙም - የClient API ፍጥነት ገደብ ተጥሏል + የAPI ድምጾችን ማምጫ፣ የጥሪዎች ብዛት + ምንም የኔትወርክ ጥሪዎች አልተደረጉም + %s የኔትወርክ ጥሪዎች ተደርገዋል + የAPI ድምጾችን ማምጫ፣ የጊዜ ገደብ ማለቂያዎች ብዛት + ምንም የኔትወርክ ጥሪዎች የጊዜ ገደባቸው አልላለፈም + %s የኔትወርክ ጥሪዎች ጊዜያቸው አልፏል + የAPI ክሊየንት ፍጥነት ገደቦች + ምንም የክሊየንት ፍጥነት ገደቦች አልተገኙም + የደንበኛው የፍጥነት ገደብ %s ጊዜያት ተጋጥሟል + %s ሚሊሰከንድ diff --git a/patches/src/main/resources/addresources/values-am-rET/shared/strings.xml b/patches/src/main/resources/addresources/values-am-rET/shared/strings.xml index 31cdbe4dcb..02f6f18b4f 100644 --- a/patches/src/main/resources/addresources/values-am-rET/shared/strings.xml +++ b/patches/src/main/resources/addresources/values-am-rET/shared/strings.xml @@ -17,21 +17,219 @@ Second \"item\" text" --> + ቅንብሮች + ለመቀጠል እርግጠኛ ነዎት? + አስቀምጥ + መልስ + ቀለሙን መልስ + ልክ ያልሆነ ቀለም + እንደገና ማስጀመር ያስፈልጋል + ይህ ለውጥ ተፈጻሚ እንዲሆን መተግበሪያውን እንደገና ያስጀምሩ + እንደገና አስጀምር + አስገባ + ቅዳ + የMorphe ቅንብሮች ወደ መጀመሪያው ተመልሰዋል + %s ቅንብሮች ተገብተዋል + ማስገባት አልተሳካም: %s + ወደ ላይ ተጓዝ + ቅንብሮችን ፈልግ + ስለ እኛ + ለ \'%s\' ምንም ውጤት አልተገኘም + ሌላ ቁልፍ ቃል ይሞክሩ + የቅርብ ጊዜ ፍለጋዎች + ከፍለጋ ታሪክ ውስጥ ይውጣ? + ከፍለጋ ታሪክ ውስጥ አስወግድ + የፍለጋ ታሪክን አጽዳ + ሁሉንም የፍለጋ ታሪኮች ማጽዳት እንደሚፈልጉ እርግጠኛ ነዎት? + የፍለጋ ምክሮች + "• ወደ ቦታው ለመሄድ የፍለጋ መንገዱን ይጫኑ +• ወደ ቅንብሩ ለመሄድ ቅንብሩን ተጭነው ይያዙ +• የፍለጋ ጥያቄውን በታሪክ ውስጥ ለመመዝገብ Enterን ይጫኑ +• ፍለጋው የፊደላትን ትልቅ እና ትንሽነት እንዲሁም ሥርዓተ ነጥቦችን ግምት ውስጥ አያስገባም +• ዋና ዋና ቅንብሮች ከተጠፉ ንዑስ ቅንብሮች በላይ ይታያሉ" + የፍለጋ ታሪክ ባዶ ነው + የፍለጋ ታሪክን ለማስቀመጥ፣ የፍለጋ ቃላትን ይጻፉ እና Enterን ይጫኑ + የቅንብሮች ፍለጋ ታሪክን አሳይ + የMorphe ቅንብሮች ስም + መደበኛ + ተጨማሪዎች + ማስተካከያዎች + የሞርፊንግ ጊዜ + ብጁ + ስም ያስገቡ + ስም ባዶ ሊሆን አይችልም + የMorphe የቅንብር చిహ్ቶችን አሳይ + ተቀድቷል: %s + የMorphe ቋንቋ + "ለአንዳንድ ቋንቋዎች ትርጉሞች ላይጎድሉ ወይም ያልተሟሉ ሊሆኑ ይችላሉ። + +አዳዲስ ቋንቋዎችን ለመተርጎም ወይም ያሉትን ትርጉሞች ለማሻሻል morphe.software/translate ይጎብኙ" + የመተግበሪያ ቋንቋ + አስገባ / ላክ + የMorphe ቅንብሮችን አስገባ / ላክ + ከፋይል ወደ ውስጥ አስገባ + ቅንብሮች በተሳካ ሁኔታ ወደ ውስጥ ገብተዋል፣ ለመተግበር \'Save\' የሚለውን ይጫኑ + ቅንብሮችን ወደ ውስጥ ማስገባት አልተቻለም + ወደ ፋይል ላክ + ቅንብሮች በተሳካ ሁኔታ ተልከዋል + ቅንብሮችን መላክ አልተቻለም + የቅርብ ጊዜውን የ Morphe Patches ስሪት <i>%s</i> እየተጠቀሙ ነው + የ Morphe Patches ስሪት <i>%1$s</i> እየተጠቀሙ ነው<br><br><b>የዘመነ ስሪት አለ: <i>%2$s</i></b><br><br>ፓቾችንዎን ለማዘመን፣ ይህንን መተግበሪያ እንደገና ለመፓች Morphe-ን ይጠቀሙ + ማስታወሻ + ይህ ስሪት የቅድመ-ብቃት ስሪት ነው እና ያልተጠበቁ ችግሮች ሊያጋጥሙዎት ይችላሉ + ይፋዊ ሊንኮች + ድረ-ገጽ + ልገሳ ያድርጉ + ትርጉሞች + ክሬዲቶች + ወቅታዊ ነው + ዘመናዊ ስሪት ይገኛል + የአሁኑ ልማት + የቀድሞ ልማት + ንቁ ልማት + 2022 ‑ 2025 + 2022 ‑ 2025 + 2019 ‑ 2022 + የላይሰንስ ማሳሰቢያዎች + በመጫን ላይ… + ዲባጊንግ + የላቁ የልማት አማራጮችን እና የምርመራ መዝገቦችን ያዋቅሩ + የዲባግ መዝገቦችን አግብር + የዲበግ ሎግ ስታክ ትሬስ + ስህተቶችን ለመለየት እንዲረዱ በዲባግ መዝገቦች ውስጥ ስታክ ትሬሶችን ያካትታል + በ Morphe ስህተት ላይ የቶስት ማሳወቂያ አሳይ + "የስህተት ማሳወቂያዎችን ማጥፋት ሁሉንም የ Morphe የስህተት ማሳወቂያዎች ይደብቃል። + +ማንኛውም ያልተጠበቁ ክስተቶች ሲያጋጥሙ አይነገሩዎትም" + የዲባግ ሎጎችን ላክ + የMorphe ዲባግ ሎጎችን ወደ ውጭ ላክ + የዲባግ ሎጎች በተሳካ ሁኔታ ወደ ውጭ ተላኩ + የዲባግ ሎጎችን ወደ ውጭ ለመላክ አልተቻለም + የዲባግ መዝገቦችን ይፈልጉ… + አጽዳ + የዲባግ ሎጊንግ ተዘግቷል + ምንም የዲባግ ሎጎዎች አልተገኙም + የዲባግ ሎጎች ተገለበጡ + የዲባግ ሎጎችን ወደ ውጭ ለመላክ አልተቻለም፦ %s + የዲባግ ሎጎች ተጽድተዋል + የባህሪ ምልክቶች ስራ አስኪያጅ + የቡሊያን ባህሪ ምልክቶችን ያስተዳድሩ + የባህሪ ምልክቶችን ይፈልጉ… + ንቁ አይደሉም (%s) + ንቁ ናቸው (%s) + ተዘግተዋል (%s) + የተገደደ (%s) + ራስ-ሰር + ጠፍ + 켜 + በግድ አብራ + እግድ + አስወግድ + የባህሪ ምልክቶች ተቀምጠዋል + የባህሪ ምልክቶች ተቀናሽተዋል + የባህሪ ምልክቶች ወደ ክሊፕቦርድ ተቀድበዋል + ይህንን ቅንብር ለመጠቀም መተግበሪያውን እንደገና ያስጀምሩ + ኤክስፖርት እና ኢምፖርት + %s የባህሪ ምልክቶች ገቢ ተደርገዋል + ምንም የልዩ ባህሪ ምልክቶች አልተገኙም + የልዩ ባህሪ ምልክቶች በተሳካ ሁኔታ ተላክተዋል + የልዩ ባህሪ ምልክቶችን ለመላክ አልተቻለም + የልዩ ባህሪ ምልክቶች ተጭነዋል፣ ለመተግበር Save የሚለውን ይጫኑ + ፋይሉን ለማንበብ አልተቻለም + ባይነሪ ፍለጋ + "የትኛው የልዩ ባህሪ ምልክት የ приложение ባህሪን እንደሚያስከትል ያገኛል። + +ከ %s ምልክቶች ውስጥ ግማሹ ተዘግተዋል፣ እና በእያንዳንዱ እንደገና ማስጀመር በኋላ ባህሪው አሁንም መኖሩን ይነግሩናል። + +ከመጀመርዎ በፊት ባህሪውን አንድ ጊዜ ይድገሙት፣ አለበለዚያ አፕሊኬሽኑ እስካሁን ያልጠየቃቸው ምልክቶች ከፍለጋው ውስጥ ይጎድላሉ" + ጀምር + "የሚፈለጉ የፊቸር ፍላጎች የሉም + +በባይነሪ ፍለጋ ውስጥ ሊጠቀሙባቸው የሚችሉት መተግበሪያው የደረሰባቸው እና በአሁኑ ጊዜ በግዳጅ ያልበሩ ፍላጎች ብቻ ናቸው" + "ደረጃ %1$s + +%2$s ፍላጎች ቀርተዋል፣ %3$s ለዚህ ደረጃ ታግደዋል። + +መተግበሪያውን እንደገና ያስጀምሩ፣ ባህሪው መኖሩን ያረጋግጡ፣ ከዚያም ከታች ይመልሱ" + የፊቸር ፍላጎች ፍለጋ በመካሄድ ላይ ነው + ባህሪው አለ + ባህሪው የለም + ባይነሪ ፍለጋውን ሰርዝ + ባይነሪ ፍለጋው ተሰርዟል + "የፊቸር ፍላጎት %s ለባህሪው ምክንያት ሆኗል። + +አሁን ታግዷል" + "የፊቸር ፍላጎት ማግኘት አልተቻለም + +የመተግበሪያው ባህሪ በፊቸር ፍላጎት የሚቆጣጠር አይመስልም" + "አንድም የባህሪ ምልክት ማግኘት አልተቻለም። + +ባህሪው በፍለጋው ወቅት ቀድሞ ጠፍቷል፣ ስለዚህ ከአንድ በላይ ምልክቶች ተያይዘው ሊሆን ይችላል ወይም አንዱ መልስ ስህተት ነበር" + የ MicroG ቅንብሮችን ይክፈቱ + MicroG አልተጫነም። ይጫኑት + ድርጊት ያስፈልጋል + "የእርስዎ MicroG v%1s ጊዜው ያለፈበት ነው እና ችግሮችን ሊያስከትል ይችላል። + +እባክዎን MicroGን ወደ አዲሱ ስሪት v%2s ያዘምኑ" + አዘምን + ተجاهል + "የተጫነው MicroG በ Morphe አይመከርም፦ + +%1s + +ለማንቀላቀል ይጫኑ፣ ከዚያ ይህን መተግበሪያ እንደገና ይክፈቱ እና MicroG REን ይጫኑ" + ይጫኑት ያጥፉ + "MicroG በጀርባ እንዲሰራ ፈቃድ የለውም። + +ለስልክዎ የሚስማማውን \"Don't kill my app\" መመሪያ ይከተሉ፣ እና መመሪያዎቹን ለ MicroG ጭነትዎ ይተግብሩ። + +ይህ መተግበሪያው እንዲሰራ አስፈላጊ ነው" + ድረ-ገጹን ክፈት + "ችግሮችን ለመከላከል የ MicroG የባትሪ ማመቻቻዎች መጥፋት አለባቸው። + +ለ MicroG የባትሪ ማመቻቻዎችን ማጥፋት በባትሪ አጠቃቀም ላይ አሉታዊ ተጽዕኖ አይኖረውም። + +የቀጠለውን ቁልፍ ተጫኑ እና የማመቻቻ ለውጦችን ይፍቀዱ" + ቀጥል + የMicroG የባትሪ ማመቻቻ ውይይት ሳጥንን አሳይ + "ይህንን ማጥፋት የMicroG የባትሪ ማመቻቻ ጥያቄን ይደብቀዋል። + +የባትሪ ማመቻቻዎች ካልተለወጡ የሚከተሉትን ችግሮች ሊያጋጥሙዎት ይችላሉ፡ +• መተግበሪያው በዘፈቀደ ሊዘጋ ይችላል +• ማሳወቂያዎች ላይሰሩ ይችላሉ +• የጀርባ ስርጭት ላይሰራ ይችላል" + የመተግበሪያ ሊንክ አያያዝ + ሊንኮች በዚህ መተግበሪያ ውስጥ እንዲከፈቱ ያዋቅሩ + ደረጃ 1፡ ዋናውን የመተግበሪያ ሊንኮች አግድ + የዋናውን መተግበሪያ ቅንብሮች ይክፈቱ እና ሁሉንም የዶሜይን ሊንኮች አግዱ + ደረጃ 2፡ የተስተካከሉ የመተግበሪያ ሊንኮችን አግብር + የተስተካከለውን መተግበሪያ ቅንብሮች ይክፈቱ እና ሁሉንም የዶሜይን ሊንኮች አግብሩ + ቅንብሮችን ክፈት + 🚨 የሙከራ ማስጠንቀቂያ 🚨 + "የ ⚠️ <b>%1$s</b> የሙከራ መተግበሪያ ስሪት እየተጠቀሙ ነው +<br/><br/> +🔧 ለዚህ የመተግበሪያ ስሪት ማስተካከያዎችን እያደረግን ስለሆነ ያልተለመደ የመተግበሪያ ባህሪ ወይም ያልታወቁ ስህተቶች ሊያጋጥሙ ይችላሉ +<br/><br/> +ችግር የሌለው ተሞክሮ ከፈለጉ፣ ይህንን <b>ያጥፉ</b> እና የሚመከርውን የ ✅ <b>%2$s</b> መተግበሪያ ስሪት ይጫኑ" + ⚠️ የሙከራውን እፈልጋለሁ + ✅ የተረጋጋውን እፈልጋለሁ + የመላላጥ መዝገብ በተጠቃሚ ሰነዶች ውስጥ ተቀምጧል፡ %s + የመላላጥ መዝገብ ተቀምጧል፡ %s diff --git a/patches/src/main/resources/addresources/values-am-rET/sponsorblock/strings.xml b/patches/src/main/resources/addresources/values-am-rET/sponsorblock/strings.xml index 9d2d4ab7d5..7badcf605c 100644 --- a/patches/src/main/resources/addresources/values-am-rET/sponsorblock/strings.xml +++ b/patches/src/main/resources/addresources/values-am-rET/sponsorblock/strings.xml @@ -16,7 +16,219 @@ Second \"item\" text" --> + SponsorBlockን አግብር + SponsorBlock የYouTube ቪዲዮዎች አሰልቺ ክፍሎችን ለመዝለል የሚያገለግል በብዙዎች ተሳትፎ የሚሰራ ስርዓት ነው + መልክ + የድምጽ አሰጣጥ ቁልፉን አሳይ + ክፍሎችን ለመደገፍ ወይም ለመቃወም የሚያስችል ቁልፍ ይጨምራል + የካሬ አቀማመጥን ይጠቀሙ + ለSponsorBlock ቁልፎች እና መቆጣጠሪያዎች የካሬ ማዕዘኖችን ይጠቀማል + አጭር የዝላይ ቁልፍን ይጠቀሙ + ቦታ ለመቆጠብ የዝላይ ቁልፉን ስፋት ይቀንሳል + የዝላይ බොተንን በራስ-ሰር ደብቅ + የዝላይ බොተኑን ለሙሉው ክፍል በሚታይ ሁኔታ ከማቆየት ይልቅ ከጥቂት ሰከንዶች በኋላ ይደብቀዋል + የዝላይ ቦተን የሚቆይበት ጊዜ + የዝላይ ቦተኑ ለምን ያህል ጊዜ ይታይ የሚለውን ይወስኑ + የዝላይ ማሳወቂያን አሳይ + አንድ ክፍል በራስ-ሰር ሲዘለል ማሳወቂያ ያሳያል + የዝላይ ማሳወቂያ የሚቆይበት ጊዜ + የማንሳት ማሳወቂያ ለምን ያህል ጊዜ ይታይ የሚለውን ይወስኑ + 1 ሰከንድ + 2 ሰከንዶች + 3 ሰከንድ + 4 ሰከንድ + 5 ሰከንድ + 6 ሰከንድ + 7 ሰከንድ + 8 ሰከንድ + 9 ሰከንድ + 10 ሰከንድ + የቪዲዮውን ርዝመት ያለ ክፍሎች አሳይ + በፍለጋ አጥፊው ላይ የሚታየውን አጠቃላይ የቪዲዮ ርዝመት ሁሉንም ክፍሎች ይቀንሳል + አዳዲስ ክፍሎችን መፍጠር + \'አዲስ ሴግመንት ፍጠር\' የሚለውን ቁልፍ አሳይ + አዳዲስ የSponsorBlock ሴግመንቶችን ለማስገባት የሚያስችል ቁልፍ ወደ ቪዲዮ ማጫወቻው ይጨምራል + የጊዜ ማስተካከያ እርምጃ + የጊዜ ማስተካከያ ቁልፎች የሚፈልጉት የጊዜ መጠን (በሚሊሰከንድ) + እሴቱ አዎንታዊ ቁጥር መሆን አለበት + የማስገቢያ መመሪያዎች + መመሪያዎቹ አዳዲስ ሴግመንቶችን ለመፍጠር የሚያስፈልጉ ደንቦችን እና ጠቃሚ ምክሮችን ይይዛሉ + መመሪያዎቹን ይከተሉ + አዳዲስ ክፍሎችን ከመፍጠርዎ በፊት የSponsorBlock መመሪያዎችን ያንብቡ + አነበብኩት + አሳየኝ + ጠቅላላ + የግንኙነት ስህተት ማስጠንቀቂያዎችን አሳይ + የSponsorBlock ሰርቨር በማይገኝበት ጊዜ ማሳወቂያ ያሳያል + የማለፍ ብዛት ክትትልን አግብር + ምን ያህል ጊዜ እንደተቆጠበ ለመከታተል የዝላይ ብዛትን ወደ SponsorBlock ሊደርቦርድ ይልካል + ዝቅተኛው የክፍለ-ጊዜ ርዝመት + ከዚህ ዋጋ (በሰከንድ) የሚያንሱ ክፍለ-ጊዜዎች አይታዩም ወይም አይዘለሉም + ልክ ያልሆነ የጊዜ ርዝመት + የግል የተጠቃሚ መለያ ቁጥርዎ + ይህ በምስጢር መያዝ አለበት። ይህ እንደ የይለፍ ቃል ነው እና ለማንም መጋራት የለበትም። አንድ ሰው ይህ ካለው፣ እርስዎን መስሎ ሊቀርብ ይችላል + የግል የተጠቃሚ መለያ ቁጥር ቢያንስ 30 ቁምፊዎች መሆን አለበት + የ API URL ይቀይሩ + SponsorBlock ወደ ሰርቨሩ ጥሪዎችን ለማድረግ የሚጠቀምበት አድራሻ + የቻናሎች ነጭ ዝርዝር + SponsorBlock በነጭ ዝርዝር ውስጥ ላሉ ቻናሎች ተዘግቷል + በነጭ ዝርዝር ውስጥ ባለው ቻናል። ላይ አሳውቅ + SponsorBlock በነጭ ዝርዝር ውስጥ ላለ ቻናል። ተዘግቶ በሚገኝበት ጊዜ ማሳወቂያ ያሳያል + SponsorBlock ለዚህ ቻናል። ተዘግቷል + የAPI URL ተቀይሯል + የAPI URL ስህተት ነው + የAPI URL ተለውጧል + የማስገቢያ / የማውጫ ቅንብሮች + ቅዳ + ወደ Morphe እና ሌሎች SponsorBlock ፕላትፎርሞች ማስገባት / ማውጣት የሚቻል የSponsorBlock JSON ውቅርዎ + "ወደ Morphe እና ሌሎች SponsorBlock ፕላትፎርሞች ማስገባት / ማውጣት የሚቻል የSponsorBlock JSON ውቅርዎ + +ይህ የግል ተጠቃሚ መታወቂያዎን ያካትታል። ይህንን በጥንቃቄ ያጋሩ" + ቅንብሮች በተሳካ ሁኔታ ገብተዋል + ለመጫን አልተቻለም: %s + ለመላክ አልተቻለም: %s + "የእርስዎ ቅንብሮች የግል SponsorBlock የተጠቃሚ መታወቂያ ይዘውበታል። + +የተጠቃሚ መታወቂያዎ እንደ የይለፍ ቃል ነው፣ እና በፍጹም መጋራት የለበትም" + እንደገና አታሳየኝ + የክፍል ባህሪን ይቀይሩ + ስፖንሰር + የተከፈለ ማስተዋወቂያ፣ የተከፈለ የሪፈራል እና ቀጥታ ማስታወቂያዎች። ለራስ ማስተዋወቂያ ወይም ለሚወዷቸው ጉዳዮች / ፈጣሪዎች / ድረ-ገጾች / ምርቶች ለሚደረጉ ነፃ ማስታወቂያዎች አይደለም + ያልተከፈለ / የራስ ማስተዋወቂያ + ያልተከፈለ / የራስ ማስተዋወቂያ ካልሆነ በስተቀር ከስፖንሰር ጋር ተመሳሳይ ነው። ስለ እቃዎች፣ ልገሳዎች ወይም አብረው ስለሰሩት ሰዎች መረጃ የሚሰጡ ክፍሎችን ያካትታል + የመስተጋብር ማሳሰቢያ (ሰብስክራይብ ያድርጉ) + በይዘቱ መካከል ላይክ እንዲያደርጉ፣ ሰብስክራይብ እንዲያደርጉ ወይም እንዲከተሉ የሚያሳስብ አጭር ማሳሰቢያ። ረጅም ከሆነ ወይም ስለ አንድ የተለየ ነገር ከሆነ፣ በራስ ማስተዋወቅ ስር መሆን አለበት + ልዩ ትኩረት + አብዛኛው ሰው የሚፈልገው የቪዲዮው ክፍል + የመክፈቻ አኒሜሽን / የሽግግር ጊዜ + ትክክለኛ ይዘት የሌለበት የጊዜ ልዩነት። ቆይታ፣ የማይቀየር ምስል ወይም ተደጋጋሚ አኒሜሽን ሊሆን ይችላል። መረጃ የያዙ ሽግግሮችን አያካትትም + የመዝጊያ ካርዶች / ክሬዲቶች + ክሬዲቶች ወይም የ YouTube መዝጊያ ካርዶች በሚታዩበት ጊዜ። መረጃ ያላቸውን ማጠቃለያዎች አያካትትም + ትኩረት ሳቢ / ሰላምታዎች + ለሚቀጥለው ቪዲዮ የሚቀርቡ ትረካዎች፣ ሰላምታዎች እና የስንብት ንግግሮች። ተጨማሪ ይዘት የሚጨምሩ ክፍሎችን አያካትትም + ቅድመእይታ / ማጠቃለያ + በቪዲዮው ውስጥ ወይም በሌሎች ተከታታይ ቪዲዮዎች ውስጥ ምን እንደሚመጣ ወይም ምን እንደተከሰተ የሚያሳዩ የክሊፖች ስብስብ፣ ሁሉም መረጃዎች በሌላ ቦታ ተደግመው የሚገኙበት + ተያያዥ ጉዳዮች / ቀልዶች + የቪዲዮውን ዋና ይዘት ለመረዳት የማይፈለጉ ተያያዥ ትዕይንቶች ወይም ቀልዶች። አውድ ወይም ዝርዝር መረጃዎችን የሚሰጡ ክፍሎችን አያካትትም + ሙዚቃ፡ ሙዚቃ የሌለበት ክፍል + ለሙዚቃ ቪዲዮዎች ብቻ የሚያገለግል። በሌላ ዘርፍ ያልተካተቱ ሙዚቃ የሌላቸው የሙዚቃ ቪዲዮ ክፍሎች + 飛ばす + ልዩ ትኩረት + ስፖንሰርን 飛ばስ + ማስተዋወቂያውን 飛ばስ + መስተጋብርን 飛ば + ወደ ዋናው ክፍል 飛ば + መግቢያውን 飛ば + እረፍቱን 飛ば + እረፍቱን 飛ば + መዝጊያውን 飛ば + መንጠቆውን 飛ば + ቅድመእይታውን 飛ば + ቅድመእይታውን 飛ば + ማጠቃለያውን 飛ば + ተያያዥ ጉዳዮችን 飛ば + ሙዚቃ ያልሆኑትን ዝለል + ክፍሉን ዝለል + ስፖንሰር ተዘልሏል + የራስ ማስተዋወቂያ ተዘልሏል + አሰልቺ ማሳሰቢያ ተዘልሏል + ወደ ዋናው ነጥብ ተዘልሏል + መግቢያ ተዘልሏል + የመሃል እረፍት ተዘልሏል + የመሃል እረፍት ተዘልሏል + መውጫ ተዘልሏል + መሳቢያ ተዘልሏል + ቅድመ-እይታ ተዘልቷል + ቅድመ እይታ ተዘልሏል + ማጠቃለያ ተዘልሏል + ተያያዥ ያልሆነ ንግግር ተዘልሏል + ሙዚቃ ያልሆነ ክፍል ተዘልሏል + ያልተላከ ክፍል ተዘልሏል + በርካታ ክፍሎች ተዘልለውታል + በራስ-ሰር ዝለል + አንድ ጊዜ በራስ-ሰር ዝለል + የዝለል ቁልፍ አሳይ + በseekbar ላይ አሳይ + አግድ + ክፍሉን ለመላክ አልተቻለም: %s + SponsorBlock ለጊዜው ተቋርጧል + ክፍለ-ጊዜውን ማስገባት አልተቻለም (ሁኔታ: %1$s %2$s) + ክፍለ-ጊዜውን መላክ አልተቻለም። የላኩት ብዛት ገደቡን አልፏል (ከአንድ ተጠቃሚ ወይም IP ብዙ ጥያቄዎች የቀረቡበት) + ክፍለ-ጊዜውን መላክ አይቻልም: %s + "ክፍለ-ጊዜውን መላክ አይቻልም +ቀድሞውኑ ይገኛል" + ክፍለ-ጊዜው በተሳካ ሁኔታ ተልኳል + SponsorBlock ለጊዜው ዝግ ነው (API ጊዜው አልቋል) + SponsorBlock ለጊዜው አይдоступም (ሁኔታ %s) + SponsorBlock ለጊዜው አይገኝም + ለክፍለ-ጊዜው ድምጽ ለመስጠት አልተቻለም (API ጊዜው አልቋል) + ለክፍለ-ጊዜው வாக்கላት አልተቻለም (ሁኔታ: %1$s %2$s) + ለክፍለ-ጊዜው ድምጽ መስጠት አልተቻለም: %s + ወደ ላይ ድምጽ ስጥ + ወደ ታች ድምጽ ስጥ + ምድብ ይቀይሩ + ድምጽ የሚሰጥባቸው ምንም ክፍለ-ጊዜዎች የሉም + ከ %1$s እስከ %2$s + የክፍለ-ጊዜውን ምድብ ይምረጡ + ዘርፉ በቅንብሮች ውስጥ ተዘግቷል። ለማስገባት ዘርፉን ያብሩ + አዲስ የSponsorBlock ክፍል + %sን እንደ አዲስ ክፍል መጀመሪያ ወይም መጨረሻ ላድርገው? + መጀመሪያ + መጨረሻ + አሁን + ክፍሉ የሚጀምርበት ሰዓት + ክፍሉ የሚያበቃበት ሰዓት + ሰዓቶቹ ትክክል ናቸው? + "ክፍሉ ከ + +%1$s +እስከ +%2$s + +(%3$s) + +ላ göndть ዝግጁ ነዎት?" + አስገባ + መጀመሪያው ከማብቂያው በፊት መሆን አለበት + መጀመሪያ በጊዜው ባር ላይ ቦታ ምልክት ያድርጉ + ክፍሉን አስቀድመው ይመልከቱ እና በስላሴ እንደሚዘለል ያረጋግጡ + የክፍሉን ጊዜ በእጅዎ ያርሙ + የክፍሉን መጀመሪያ ወይም መጨረሻ ጊዜ ማስተካከል ይፈልጋሉ? + የተሰጠው ጊዜ ትክክል አይደለም + ልዩ ትኩረትን አስገባ + "የልዩ ትኩረት ጊዜው ትክክል ነው? + +%s + +ልዩ ትኩረት እያላስገቡ ከሆነ፣ ሰርዝ የሚለውን ተጫን እና ሁለተኛውን ቦታ ምልክት አድርግ" + የተለየውን ክፍል ላክ + ስታቲስቲክስ + ስታቲስቲክስ ለጊዜው አይገኙም (API ተቋርጧል) + በመጫን ላይ… + SponsorBlock ተዘግቷል + የተጠቃሚ ስምዎ፦ <b>%s</b> + የተጠቃሚ ስምዎን ለመቀየር እዚህ ይጫኑ + የተጠቃሚ ስምን ለመቀየር አልተቻለም፡ ሁኔታ: %1$s %2$s + የተጠቃሚ ስም በተሳካ ሁኔታ ተቀይሯል + የታዋቂነት ደረጃዎ <b>%.2f</b> ነው + የፈጠሩት ክፍሎች ብዛት <b>%s</b> ነው + የክፍሎችን ዝርዝር ለማየት እዚህ ይንኩ + የSponsorBlock የደረጃ ሰንጠረዥ + ሰዎችን ከ<b>%s</b> ክፍሎች አድነዋል + አጠቃላይ ስታቲስቲክስን እና ከፍተኛ አስተዋፅዖ አድራጊዎችን ለማየት እዚህ ይንኩ + ይህም ከህይወታቸው <b>%s</b> ነው፡፡<br>የደረጃ ሰንጠረዡን ለማየት እዚህ ይንኩ + <b>%s</b> ክፍሎችን ዘልለው አልፈዋል + ይህም <b>%s</b> ነው + የተዘለሉ ክፍሎች ቆጣሪውን እንደገና ማስጀመር? + %1$s ሰዓት %2$s ደቂቃ + %1$s ደቂቃ %2$s ሰከንድ + %s ሰከንድ + ግልጽነት፡ + ቀለም፡ + "SponsorBlock እንደ ስፖንሰሮች፣ መግቢያዎች፣ መውጫዎች እና የደንበኝነት ምዝገባ ማስታወሻዎች ያሉ የቪዲዮ ክፍለ ክፍሎች በጋራ የሚሰበሰብ የዳታቤዝ ስብስብ ነው:: ክፍለ ክፍሎቹ በማህበረሰቡ ይላካሉ እንዲሁም ድምጽ ይሰጣቸዋል፣ እንዲሁም ዳታቤዙ ክፍት እና በነጻ የሚወርድ ነው + +የሚመለከቱት የቪዲዮ መታወቂያ የቪዲዮ ክፍለ ክፍሎቹን ለማግኘት ወደ SponsorBlock API ይላካል" diff --git a/patches/src/main/resources/addresources/values-am-rET/youtube/strings.xml b/patches/src/main/resources/addresources/values-am-rET/youtube/strings.xml index 9079108272..0ec45728de 100644 --- a/patches/src/main/resources/addresources/values-am-rET/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-am-rET/youtube/strings.xml @@ -18,210 +18,1631 @@ Second \"item\" text" + ስለ Morphe + ማስታወቂያዎች + ፊድ + አጠቃላይ + ተጫዋች + Shorts + ፈላጊ ባር + የስላይድ ቁጥጥሮች + ልዩ ልዩ + ቪዲዮ + የድሮውን የYouTube ቅንብሮች ስክሪን መልስ + የጀርባ ማጫወቻ ገደቦችን አስወግድ + የጀርባ ስርጭትን አግድ + መሣሪያው ሲ 잠ገብ በራሱ ይቆም + ጠፍቷል + ሁልጊዜ ስክሪኑ ሲቆለፍ አቁም + የገመድ አልባ ኦዲዮ ካልተገናኘ በስተቀር አቁም + የማሸብለል ፍጥነት ገደብን አጥፋ + የንድፍ ማሻሻያዎችን አቋርጥ + ሰርቨሩ የ AppName አቀማመጥን እንዳያዘምን ይከላከላል + "የመተግበሪያው አቀማመጥ መጀመሪያ ሲጫን ወደነበረው አቀማመጥ ይመለሳል። + +አንዳንድ በሰርቨር በኩል ያሉ አቀማመጦች ላይመለሱ ይችላሉ። + +ለውጦቹ የሚከተሉትን ያካትታሉ፦ +• በፕሌየር ፍላይአውት ሜኑ (ወይም ተያያዥ ቅንብሮች) ውስጥ ያሉ ክፍሎች ላይሰሩ ይችላሉ +• የሚሽከረከሩ ቁጥሮች አኒሜሽን የላቸውም +• የላይብረሪ ታብ ይታያል +• የቪዲዮ መግለጫው የሙዚቃ ክፍል ላይሰራ ይችላል +• የፕሌየር ፍላይአውት ሜኑ ንጥሎች ሊደበቁ አይችሉም +• የአካውንት መቀየሪያ ቁልፍ በላይብረሪ ታብ ላይ ላይታይ ይችላል" + የፊድ flyout ሜኑ ማጣሪያን አግብር + የፊድ flyout ሜኑ ማጣሪያ + በአዲስ መስመሮች ተለይተው የሚጻፉ፣ የሚጣሩ የፍላይአውት ሜኑ ስሞችን ያስገቡ + የላይብረሪ / የእርስዎ የታችኛው ሜኑ ማጣሪያን አብራ + የላይብረሪ / የእርስዎ የታችኛው ሜኑ ማጣሪያ + ለማጣሪያ የሚፈለጉትን የላይብረሪ / የእርስዎ የታችኛው ሜኑ ስሞች በአዲስ መስመር በመለየት ያስገቡ + የአልበም ካርዶችን ደብቅ + ከአርቲስቱ መግለጫ በታች ያሉትን የአልበም ካርዶች ይደብቃል + የአርቲስት ካርዶችን ደብቅ + በፍለጋ ውጤቶች አናት ላይ ያለውን የጥበብ ሰው ካርድ ይደብቃል + የቻናል በተኖችን ደብቅ + እንደ Subscribe፣ View Channel፣ Community እና YouTube Music ያሉ በተኖችን በፍለጋ ውጤቶች ውስጥ ከቻናል ፕሮፋይሎች ላይ ይደብቃል + የማህበረሰብ ልጥፎችን ደብቅ + አጫጭር ባነሮችን ደብቅ + ጥቃቅን ባነሮችን እና በአንዳንድ ክፍሎች መካከል ያለውን ክፍተት ይደብቃል + ከቪዲዮዎች በታች የሚገኙ የሚለጠጡ ካርዶች + ሁሉንም አሳይ + የማጠቃለያ ካርዱን ብቻ ደብቅ + ሁሉንም ደብቅ + ተንሳፋፊውን የማይክሮፎን ቁልፍ ደብቅ + በፍለጋ ጥቆማዎች ስክሪን ታችኛው የቀኝ ጥግ ላይ ያለውን ተንሳፋፊ የማይክሮፎን ቁልፍ ይደብቃል + \'Get Premium\' ቁልፉን ደብቅ + የታሪክ መደርደሪያውን ደብቅ + አግድም መደርደሪያዎችን ደብቅ + እንደ ሰበር ዜናዎች፣ ሊወዷቸው የሚችሏቸው የማህበረሰብ ልጥፎች፣ ማየት የቀጠሉባቸው፣ ተጨማሪ ቻናሎችን ያስሱ፣ ተጨማሪ ርዕሶችን ያስሱ፣ ለርስዎ፣ የቅርብ ጊዜ ቪዲዮዎች፣ በጣም ተዛማጅ የሆኑ፣ ከፈጣሪ ማህበረሰብ የተላኩ ልጥፎች፣ ግብይት እና እንደገና ይመልከቱ ያሉ አግድም መደርደሪያዎችን ይደብቃል + የምስል መደርደሪያውን ደብቅ + በፍለጋ ውጤቶች ውስጥ የምስል መደርደሪያዎችን ይደብቃል + \'ሌሎችን እንዲያናግሩ ይጋብዙ\' ካርድን ደብቅ + በማሳወቂያዎች ታብ መልእክቶች ክፍል ውስጥ የሚታየውን የግብዣ ካርድ ይደብቃል + \'የቅርብ ጊዜ ቪዲዮዎች\' የሚለውን ቁልፍ ደብቅ + በመነሻ ፊድ አናት ላይ የሚታየውን ሰማያዊ \'የቅርብ ጊዜ ቪዲዮዎች\' ባነር ይደብቃል + የቀጥታ ስርጭቶችን ደብቅ + በመነሻ ፊድ እና በፍለጋ ውጤቶች ውስጥ አሁን በቀጥታ ስርጭት ላይ ያሉ ቪዲዮዎችን ይደብቃል + የድብልቅ የሙዚቃ ዝርዝሮችን ደብቅ + የፊልሞች ክፍሉን ደብቅ + የማሳወቂያዎች ሜኑ ራስጌን ደብቅ + የደወል ምልክቱን ሲጫኑ የማሳወቂያዎች ሜኑ ራስጌን ይደብቃል + \'አሳውቀኝ\' የሚለውን ቁልፍ ደብቅ + ለታቀዱ የቀጥታ ስርጭቶች እና የመጀመሪያ ትርኢቶች የማስታወሻ ቁልፉን ይደብቃል + Playablesን ደብቅ + ጨዋታዎችን እንዲጫወቱ የሚያስችለውን ባህሪ ይደብቃል + የፍለጋ ቃላት ምስሎችን ደብቅ + በፍለጋ አውቶኮምፕሊት ዝርዝር ውስጥ የትንንሽ ምስል ቅድመእይታዎችን ይደብቃል + \'ተጨማሪ አሳይ\' የሚለውን ቁልፍ ደብቅ + በፍለጋ ውጤቶች ውስጥ ከቪዲዮዎች በታች ያለውን ማስፋፊያ ቁልፍ ይደብቃል + የተመዘገብክባቸው ቻናሎች ባርን ደብቅ + የተሰብስክራይብ የተደረጉ የቻናል ስሞችን ደብቅ + በተሰብስክራይብ የተደረጉ ቻናሎች ባር ውስጥ የቻናል ስሞችን ይደብቃል + የዳሰሳ ጥናቶችን ደብቅ + ስለ ፊድ የጥቆማ ትክክለኛነት የሚጠይቁ ጥናቶችን እና ማሳሰቢያዎችን ደብቅ + የትኬት መደርደሪያውን ደብቅ + የተጫነበትን ጊዜ ደብቅ + "ገደቦች፦ +• የ Shorts መደርደሪያዎች፣ የቻናል ገጾች እና የፍለጋ ውጤቶች አሁንም የபதிያเวลาን ሊያሳዩ ይችላሉ +• ከавтоሞቲቭ አቀማመጥ ጋር አይሰራም" + የቪዲዮ ምስልን ደብቅ + የቪዲዮ ምልክቶችን ደብቅ + እንደ \'በራስ-ሰር የተተረጎመ\' / \'የተተረጎመ\'፣ \'ኮሚሽን ያገኛል\'፣ \'የተጋነነ\'፣ \'ሰዎች እነዚህንም አይተዋል\'፣ \'ምርት(ዎች)\' እና \'ሊወዱዎት የሚችሉ\' ያሉ ምልክቶችን ይደብቃል + የዕይታ ብዛትን ደብቅ + "ገደቦች፦ +• የ Shorts መደርደሪያዎች፣ የቻናል ገጾች እና የፍለጋ ውጤቶች አሁንም የዕይታ ብዛትን ሊያሳዩ ይችላሉ +• ከавтоሞቲቭ አቀማመጥ ጋር አይሰራም" + የድረ-ገጽ ፍለጋ ውጤቶችን ደብቅ + \'ከዌብ የተገኘ ውጤት\' የሚለውን ክፍል ከፍለጋ ውጤቶች ውስጥ ይደብቃል + \'ሊወዱ ይችላሉ\' የሚለውን ክፍል ደብቅ + የYouTube Doodlesን ደብቅ + በYouTube ሎጎ ላይ ወቅታዊ እና በክስተቶች ላይ የተመሰረቱ አኒሜሽኖችን ይደብቃል + YouTube Doodles ማሳየቱ የተበጁ ርዕሶች እንዳይታዩ ሊያደርግ ይችላል + የቻናሉን ባር ደብቅ + የቻናሉን የውሃ ምልክት ደብቅ + ምዕራፎችን እና \'በዚህ ቪዲዮ ውስጥ\' የሚለውን ቁልፍ ደብቅ + የገንዘብ ማሰባሰቢያ ሳጥኑን ደብቅ + የድንገተኛ አደጋ ሳጥኖችን ደብቅ + የመረጃ ፓነሎችን ደብቅ + እንደ የሕክምና መረጃ ያሉ የመረጃ ሁኔታ ፓነሎችን ይደብቃል + የተቀላቀሉ ቁልፍን ደብቅ + \'የቀጥታ መልዕክት መልሶ ማጫወት\' የሚለውን ቁልፍ ደብቅ + በሙሉ ስክሪን ሁነታ ውስጥ የቻት መልሶ ማጫወቻ ቁልፍን ይደብቃል + የህክምና ፓነሎችን ደብቅ + የተጫዋች የእጅ ምልክት ፍንጮችን ደብቅ + እንደ \'ለጥንቃቄ ፍለጋ ወደ ላይ ይሳቡ\'፣ \'ለመሰረዝ ይልቀቁ\' እና \'2x\' ያሉ በተጫዋች የእጅ ምልክቶች ወቅት የሚታዩ ፍንጮችን ይደብቃል + ስናክባርን ደብቅ + የ مشتركዎች መመሪያዎችን ደብቅ + \'ከቪዲዮ ጋር አስተዋውቅ\' የሚለውን ቁልፍ ደብቅ + የንክኪ እና የመያዣ ግራዲየንቱን ደብቅ + ፍጥነትን ለመጨመር ተጭነው ሲያሸብሩ የቪዲዮ ማጫወቻ መቆጣጠሪያዎቹን ግራዲየንት ይደብቃል + በጊዜ የተገደቡ ምላሾችን ደብቅ + ተጠቃሚዎች በኮሜንት ክፍል እና በቀጥታ ቻት ውስጥ በኢሞጂዎች ምላሽ እንዲሰጡ የሚያስችለውን ባህሪ ይደብቃል + \'እርዳታ & አስተያየት\' ሜኑን ደብቅ + እንደ ፍለጋ ውጤቶች ባሉ ቦታዎች ላይ የሶስት ነጥብ ቁልፉን ሲጠቀሙ የ\'እርዳታ እና አስተያየት\' ሜኑን ይደብቃል + ሀንድልን ደብቅ + በ \'You\' ታብ ውስጥ የቻናልዎን ሀንድል ይደብቃል + የቪዲዮውን ርዕስ ደብቅ + በሙሉ ስክሪን ሁነታ ውስጥ የቪዲዮውን ርዕስ ይደብቃል + የቪዲዮ የግርጌ ጽሑፍን አጥራ + እንደ \'#\', \'Fundraiser\', \'product(s)\', እና \'Shop\' ያሉ ሐረጎችን ከቪዲዮ የግርጌ ጽሁፍ ያጠራል + \'Ask Gemini\' የሚለውን ቁልፍ ደብቅ + \'ስለዚህ ቪዲዮ ጠይቅ\' በሚለው ፓነል ግርጌ የሚገኘውን \'Ask Gemini\' የሚለውን ቁልፍ ይደብቃል + የቪዲዮ መግለጫ + በቪዲዮው መግለጫ ውስጥ የትኞቹ ክፍሎች እንደሚታዩ ይምረጡ + \'በAI የተፈጠረ የቪዲዮ ማጠቃለያ\' ክፍሉን ደብቅ + የጠይቁ ክፍልን ደብቅ + የባህሪያት ክፍልን ደብቅ + የተመረጡ ቦታዎችን፣ ጨዋታዎችን፣ ሙዚቃዎችን እና የተጠቀሱ ሰዎችን ክፍሎች ይደብቃል + የቻናሉ ሊንኮች ክፍልን ደብቅ + የምዕራፎች ክፍልን ደብቅ + የማስተካከያ ክፍልን ደብቅ + \'የኮርሱ ሂደት\' ክፍልን ደብቅ + የቃው ክፍልን ደብቅ + ይህን ኮርስ ቃው እና ፖድካስቱን ቃው የሚሉትን ክፍሎች ይደብቃል + \'ይህን ኮርስ ቃው\' ክፍልን ደብቅ + \'ፖድካስቱን ቃው\' ክፍልን ደብቅ + የተመረጡ ቻናሎች ክፍሉን ደብቅ + የተመረጡ ሊንኮች ክፍልን ደብቅ + ልዩ የሙዚቃ ዝርዝሮች ክፍሉን ደብቅ + የተመረጡ ቪዲዮዎች ክፍሉን ደብቅ + የሃሽታግ ክፍሉን ደብቅ + \'ይህ እንዴት ተሠራ\' የሚለውን ክፍል ደብቅ + የHype ነጥቦችን ደብቅ + የመረጃ ካርዶችን ክፍል ደብቅ + \'ቁልፍ ፅንሰ-ሀሳቦች\' ክፍልን ደብቅ + የጥያቄና መልስ ክፍልን ደብቅ + \'በዚህ ቪዲዮ ውስጥ ፈልግ\' የሚለውን ክፍል ደብቅ + የሰብስክራይብ ቁልፍን ደብቅ + የትራንስክሪፕት ክፍልን ደብቅ + \'የቪዲዮ ዝርዝሮች\' ክፍልን ደብቅ + የማጣሪያ ባር + የሚንሸራተተው የፊልተር ባር የት እንደሚደበቅ ይምረጡ + በቻናል ገጽ ላይ ደብቅ + በአስተያየቶች ውስጥ ደብቅ + በፊዶች ውስጥ ደብቅ + በተያያዥ ቪዲዮዎች ውስጥ ደብቅ + በፍለጋ ውጤቶች ውስጥ ደብቅ + በተመለከቱት ታሪክ ውስጥ ደብቅ + የቻናል ገጽ + በቻናል ገጽ ላይ የትኞቹ ክፍሎች እንደሚታዩ ይምረጡ + የቻናል ታብ ማጣሪያን አግብር + የቻናል ታብ ማጣሪያ + ለመለየት የሚፈለጉትን የቻናል ታብ ስሞችን በአዲስ መስመር በመለየት ያስገቡ + የኮሙኒቲ ቁልፍን ደብቅ + የተቀላቀሉ ቁልፍን ደብቅ + የሊንኮችን ቅድመእይታ ደብቅ + በቻናሉ ራስጌ ላይ የሚገኙትን ሊጫኑ የሚችሉ የዌብ ሊንኮችን ይደብቃል + \'ለአባላት ብቻ\' የሚለውን ምልክት ደብቅ + በቻናሉ ማጣሪያ አሞሌ ውስጥ \'ለአባላት ብቻ\' የሚለውን ምልክት ይደብቃል + የአባላትን መደርደሪያ ደብቅ + የቻናሉ አባላት ዝርዝርን ይደብቃል + የፖስቶች መደርደሪያን ደብቅ + የመደብር பொத்தானን ደብቅ + የሰብስክራይብ பொத்தானን ደብቅ + የፍለጋ ማውጫ + በፍለጋ ሜኑ ውስጥ የትኞቹ ክፍሎች እንደሚታዩ ይምረጡ + የኮርሶች ሜኑን ደብቅ + የፍለጋ ቁልፍን ደብቅ + \'ፋሽን & ውበት\' ሜኑን ደብቅ + የጌሚንግ ሜኑን ደብቅ + የሀይፕ ሜኑን ደብቅ + የመማሪያ ሜኑን ደብቅ + የቀጥታ ስርጭት ሜኑን ደብቅ + የአባልነት ሜኑን ደብቅ + \'Movies & TV\' ዝርዝርን ደብቅ + የሙዚቃ ዝርዝርን ደብቅ + የዜና ሜኑን ደብቅ + የPlayables ሜኑን ደብቅ + የፖድካስቶች ሜኑን ደብቅ + የገበያ ሜኑን ደብቅ + የስፖርት ሜኑን ደብቅ + የYouTube Premium ሜኑን ደብቅ + የYouTube Studio ሜኑን ደብቅ + የYouTube Music ሜኑን ደብቅ + የYouTube Kids ሜኑን ደብቅ + የYouTube Create ሜኑን ደብቅ + \'YouTube እንዴት እንደሚሰራ\' ሜኑን ደብቅ + የሕግ እና የግላዊነት መረጃዎችን ደብቅ + አስተያየቶች + በአስተያየቶች ክፍል ውስጥ የትኞቹ ክፍሎች እንደሚታዩ ይምረጡ + የአስተያየቶች ካሩሴል ማጣሪያን አብራ + "• የካሩሴል ንጥሉን ትክክለኛ ስም ማስገባትዎን ያረጋግጡ (ለምሳሌ፦ Ask, Hype this video, Listen on YouTube Music) +• የካሩሴል ንጥሎችን መደበቅ ያልተፈለጉ የጎንዮሽ ተፅእኖዎች ሊኖሩት ይችላል +• ሁሉም የካሩሴል ንጥሎች ከተደበቁ፣ አጠቃላይ የትያቄዎች ክፍልም አብሮ ይደበቃል" + የአስተያየቶች ካሩሴል ማጣሪያ + ለመለየት የሚፈልጓቸውን የአስተያየቶች ካሩሰል ስሞች በአዳዲስ መስመሮች በመለየት ያስገቡ + የAI የውይይት ማጠቃለያን ደብቅ + የቻናሉን መመሪያዎች ደብቅ + የሁኔታ መልእክቶችን ደብቅ + \'በአባላት የተደረጉ አስተያየቶች\' የሚለውን ርዕስ ደብቅ + የአስተያየቶች ክፍሉን ደብቅ + በመነሻ ፊድ ውስጥ የትያቄዎችን ክፍል ደብቅ + የማህበረሰብ መመሪያዎችን ደብቅ + \'አጭር ቪዲዮ ፍጠር\' የሚለውን ቁልፍ ደብቅ + የማይወዱትን ቁልፍ ደብቅ + የኢሞጂ ቁልፉን ደብቅ + የማጣሪያ አሞሌ አማራጮችን ደብቅ + \'ከፍተኛ\' እና \'አዲስ\' ካሉ በስተቀር ሁሉንም የማጣሪያ አሞሌ አማራጮች ይደብቃል + የስጦታ አኒሜሽን እና ካርዶችን ደብቅ + የስጦታ ቁልፍን ደብቅ + የመረጃ ቁልፉን ደብቅ + የቀጥታ ስርጭት የገጸ-ባህር ለጋሾች ባርን ደብቅ + የቀጥታ መልዕክት ወደ ታች ማሸብለያ ቁልፍን ደብቅ + የቀጥታ ስርጭት ውይይት መቆጣጠሪያዎችን ደብቅ + ከውይይት ማስገቢያው አጠገብ የሚታዩ የቲፕቶል ፖፕአፖችን ይደብቃል ለምሳሌ የደንበኞች ብቻ የውይይት ማሳሰቢያ + የሜኑ አዝራሩን ደብቅ + የእያንዳንዱን አስተያየት የ3-ነጥብ ሜኑ አዝራር ይደብቃል + የቅድመ እይታ አስተያየትን ደብቅ + ሙሉውን የአስተያየቶች ክፍል ከመክፈትዎ በፊት በቪዲዮው ስር ያለውን የአስተያየት ቅድመ-እይታ ይደብቃል + አስተያየቱ በ Morphe ተደብቋል… + አነስተኛ የትያቄዎች ቁልፍን አግብር + በትያቄዎች ቁልፍ ላይ የሚታዩትን ተንሸራታች ካርዶች እና የገጽ መራጫውን ይደብቃል + የምስጋና ቁልፉን ደብቅ + የጊዜ ማህተም ቁልፉን ደብቅ + \'Top fans\' በተን ደብቅ + የትርጉም አዝራሩን ደብቅ + \'ገና ምንም አስተያየት የለም\' የሚለውን መልእክት ደብቅ + የተደረጎቻቸውን የፍለጋ ሊንኮች አጽዳ + የተደረጎቻቸውን የፍለጋ ሊንኮች ያጸዳል፣ ዋናውን ተራ ጽሑፍ ብቻ ትቶ + የቁልፍ ቃል ማጣሪያ + በፊዶች እና በፍለጋ ውስጥ ቪዲዮዎችን ለመደበቅ የተወሰኑ ቃላትን ወይም ሐረጎችን ይምረጡ + አስተያየቶችን በቁልፍ ቃላት ደብቅ + የመነሻ ገጽ ቪዲዮዎችን በቁልፍ ቃላት ደብቅ + የፍለጋ ውጤቶችን በቁልፍ ቃላት ደብቅ + የደንበኝነት ምዝገባ ቪዲዮዎችን በቁልፍ ቃላት ደብቅ + ሊደበቁ የሚፈልጉ ቁልፍ ቃላት + "ሊደበቁ የሚፈልጓቸውን ቁልፍ ቃላት እና ሀረጎች በአዲስ መስመሮች በመለየት ያስገቡ + +ቁልፍ ቃላት በቪዲዮ ርዕሶች ወይም በቻናል ስሞች ውስጥ የሚታዩ ማናቸውም ጽሁፎች ሊሆኑ ይችላሉ + +በመሃላቸው የካፒታል ፊደላት ያሉባቸው ቃላት በትክክል መመሳሰል አለባቸው (ለምሳሌ iPhone፣ TikTok፣ LeBlanc)" + "መነሻ / የደንበኝነት ምዝገባዎች / ፍለጋ / አስተያየቶች ከቁልፍ ሐረጎች ጋር የሚመሳሰሉ ይዘቶችን ለመደበቅ ተጣርተው ቀርበዋል + +ገደቦች: +• Shorts በቻናል ስም ሊደበቁ አይችሉም +• አንዳንድ የUI ክፍሎች ላይደበቁ ይችላሉ +• ቁልፍ ቃላትን መፈለግ ምንም ውጤት ላይያሳይ ይችላል" + ሙሉ ቃላትን አዛምድ + ቁልፍ ቃላትን/ሐረጎችን በድርብ ጥቅስ ውስጥ ማጠር የቪዲዮ ርዕሶችን እና የቻናል ስሞችን በከፊል ማዛመድን ይከላከላል<br><br>ለምሳሌ፣<br><b>\"ai\"</b> የሚለውን ቪዲዮ ይደብቃል፡ <b>How does AI work?</b><br>ነገር ግን ይህንን አይደብቅም፡ <b>What does fair use mean?</b> + ቁልፍ ቃሉን መጠቀም አይቻልም፡ %s + ቁልፍ ቃሉን ለመጠቀም ጥቅሶችን ያክሉ፡ %s + ቁልፍ ቃሉ የሚጋጩ መግለጫዎች አሉት፡ %s + ቁልፍ ቃሉ በጣም አጭር ነው እና ጥቅሶችን ይፈልጋል፡ %s + ቁልፍ ቃሉ ሁሉንም ቪዲዮዎች ይደብቃል: %s + "• መነሻ፡ %s +• የደንበኝነት ምዝገባዎች፡ %s +• ፍለጋ፡ %s +• አስተያየቶች፡ %s +• ጠቅላላ፡ %s" + ስታቲስቲክስ + የተደበቁ ቪዲዮዎች፣ ባለፉት 24 ሰዓታት + የተደበቁ ቪዲዮዎች፣ በሁሉም ጊዜ + የመጨረሻዎቹን 24 ሰዓታት ቆጣሪ återጀምር? + የሁሉንም ጊዜ ቆጣሪ återጀምር? + የAI ቻናል ማጣሪያ + በAiSList ማህበረሰብ ዝርዝር በAI የተፈጠረ ይዘት ተብለው ምልክት የተደረገባቸውን ቻናሎች ቪዲዮዎች ደብቅ + የእገዳ ዝርዝር፡ ከፍተኛ እርግጠኝነት + የማስጠንቀቂያ ዝርዝር፡ መካከለኛ እርግጠኝነት + ቪዲዮዎችን በዋና ገጽ ላይ ደብቅ + ቪዲዮዎችን በፍለጋ ላይ ደብቅ + "• መነሻ፦ %s +• ፍለጋ፦ %s +• አጠቃላይ፦ %s" + የብሎክ ዝርዝር ቻናሎች + የማስጠንቀቂያ ዝርዝር ቻናሎች + ቻናሎችን ለመጫን ማጣሪያውን ያግብኑ + ቻናሎች እየተጫኑ ነው… + "የመነሻ እና የፍለጋ ውጤቶች በ AiSList ምልክት የተደረገባቸውን ቻናሎች ቪዲዮዎች ለመደበቅ ተለይተዋል + +ገደቦች፦ +• የተለዩ AI Shorts ባዶ ቦታ ሆኖ ሊታይ ይችላል +• አንዳንድ የ AI ቪዲዮዎች አሁንም ሊታዩ ይችላሉ" + "AiSList በፈቃደኞች የሚተዳደርና AI-የተፈጠረ ይዘትን የሚያትሙ የYouTube ቻናሎች ክፍት ምንጭ ዝርዝር ነው:: ቻናሎች በተጠቃሚዎች ሪፖርት ተደርገው ከመጨመራቸው በፊት ይገመገማሉ + +ዝርዝሩ ከGitHub የሚወርድ ሲሆን ቪዲዮዎች በየመሣሪያዎ ላይ ይ过滤 ይደረጋሉ" + የAI ቻናሎችን ሪፖርት አድርግ + የሪፖርት ማስገቢያን ወደ ቪዲዮ ሜኑ አክል + የቪዲዮውን ቻናል ወደ AiSList ሪፖርት የሚያደርግ ማስገቢያን ወደ ቪዲዮ ሜኑ አክል + የሪፖርት አቅራቢው ስም + በሕዝባዊው የAiSList መሪ ሰንጠረዥ ላይ ከሪፖርቶችዎ አጠገብ የሚታየው ስም ነው። ይህ ባዶ በሚሆንበት ጊዜ ሪፖርት ማድረግ አይቻልም + የAI ቻናል ሪፖርት ያድርጉ + "ይህንን ቪዲዮ እንደ ማስረጃ በመጠቀም %s ለAiSList ያቅርቡ? + +እነዚህን ቻናሎች ብቻ ሪፖርት ያድርጉ፦ +• የAI ድምፆችን፣ ምስሎችን፣ ስክሪፕቶችን ወይም ሙዚቃዎችን እንደ ዋና ይዘታቸው የሚጠቀሙ +• በAI መሣሪያዎች እና በትንሽ የሰው ኃይል ይዘቶችን በብዛት የሚያመርቱ + +ስለ AI ብቻ የሚወያዩ ወይም ለቪዲዮዎቻቸው አንዳንድ ክፍሎች AIን የሚጠቀሙ ቻናሎችን ሪፖርት አያድርጉ። + +ሪፖርቶች ለሕዝብ ክፍት ናቸው እና አንድ ቻናል ወደ ዝርዝሩ ከመጨመሩ በፊት ይገመገማሉ። ማስወገድ የሚቻለው በGitHub በኩል በሚቀርብ ጥያቄ ብቻ ነው" + ይህ ቻናል አስቀድሞ በAiSList የብሎክ ዝርዝር ውስጥ ይገኛል + ይህ ቻናል ቀድሞውኑ በ AiSList ማስጠንቀቂያ ዝርዝር ውስጥ ይገኛል + ሪፖርት አድርግ + ቻናሎችን ሪፖርት ለማድረግ በ AI ቻናል ማጣሪያ ቅንብሮች ውስጥ የሪፖርት አቅራቢ ስም ያስገቡ + ቻናሉን በመፈለግ ላይ… + የዚህን ቪዲዮ ቻናል ማግኘት አልተቻለም + ቻናሉ ሪፖርት ተደርጓል እና ግምገማን በመጠባበቅ ላይ ነው + AiSList ሪፖርቱን ማስቀመጥ አልቻለም፣ ቆይተው እንደገና ይሞክሩ + በጣም ብዙ ሪፖርቶች ተልከዋል፣ ቆይተው እንደገና ይሞክሩ + AiSList ሪፖርቱን ውድቅ አድርጎታል + ሪፖርቱ አልተሳካም፣ እባክዎ ግንኙነትዎን ያረጋግጡ + የቻናል ማስታወቂያ ነጭ ዝርዝር + በነጭ ዝርዝር ውስጥ ካሉ ቻናሎች የሚመጡ ቪዲዮዎች በማስታወቂያዎች ይጫወታሉ፣ ስለዚህ ፈጣሪዎቻቸው ክፍያ ያገኛሉ። በሌሎች ሁሉም ቻናሎች ላይ ማስታወቂያዎች ይታገዳሉ + ቻናሉን ወደ ማስታወቂያ ነጭ ዝርዝር ይጨምሩ + ቻናሉን ከማስታወቂያ ነጭ ዝርዝር ያስወግዱ + የፈጣሪ የሱቅ መደርደሪያን ደብቅ + የመጨረሻ ስክሪን የሱቅ ባነርን ደብቅ + ጠቅላላ ማስታወቂያዎችን ደብቅ + የሸቀጦች ባነሮችን ደብቅ + የሚከፈልባቸው የማስተዋወቂያ ሌብሎችን ደብቅ + የተጫዋች ፖፕ-አፕ ማስታወቂያዎችን ደብቅ + ራስን ስፖንሰር ያደረጉ ካርዶችን ደብቅ + የግዢ ሊንኮችን ደብቅ + የቪዲዮ ማስታወቂያዎችን ደብቅ + የYouTube Premium ማስተዋወቂያዎችን ደብቅ + + + በመጫን ላይ… + የአሁኑ ቻናል፦ + ወደ ነጭ ዝርዝር ይጨምሩ + ያስወግዱ + ነጭ ዝርዝር ውስጥ ያሉ ቻናሎች + በነጭ ዝርዝር ውስጥ ምንም ቻናሎች የሉም + ቻናሉ ወደ ነጭ ዝርዝር ተጨምሯል + ቻናሉ ከነጭ ዝርዝር ተሰርዟል + በቻናሉ ውስጥ ፈልግ + በቻናል ገጾች ላይ ያንን ቻናል ብቻ ለመፈለግ የሚያስችል ሁለተኛ የፍለጋ ቁልፍን ይጨምራል + ቻናሉን ፈልግ + የቻናሉ የፍለጋ ውጤቶች + በዚህ ቻናል ላይ ምንም አልተገኘም + ይህንን ቻናል መፈለግ አልተቻለም + ቪዲዮዎች + ፅሁፎች + በዚህ ቻናል ላይ የሚዛመዱ ምንም ፅሁፎች የሉም + ተዛማጅነት + በብዛት የታዩ + በአነስተኛ ሁኔታ የታዩ + አዳዲስ + አሮጌ + አጭር nhất + ረጅም nhất + ሊንኩ ወደ ክሊፕቦርድ ተቀድቷል + የጊዜ ምልክት ያለው ሊንክ ወደ ክሊፕቦርድ ተቀድቷል + የቪዲዮ ሊንክ ቅዳ按钮ን አሳይ + የቪዲዮ ሊንኩን ለመቅዳት አንዴ ይጫኑ፣ ከጊዜ ምልክት ጋር ለመቅዳት ደግሞ ተጭነው ይያዙ + በቀጥታ የጊዜ ማህተሙን ኮፒ ያድርግ + የ\'ቪዲዮ ሊንክ ቅዳ\' ቁልፍ የአንድ ጊዜ እና ተጭኖ የመያዝ ተግባራትን ይቀያይራል + የቪዲዮውን ርዕስ ቅዳ + የቪዲዮውን ርዕስ ለመቅዳው ተጭነው ይያዙ + የቪዲዮው ርዕስ ወደ ክሊፕቦርድ ተቅዳበታል + አስተያየቶችን ቅዳ + አንድን አስተያየት ለመቅዳው ተጭነው ይያዙ + አስተያየቱ ወደ ክሊፕቦርድ ተቅዳበታል + የቪዲዮውን ሜኑ ለመክፈት ተጭነው ይያዙ + በፊድ ቪዲዮ ላይ የትኛውም ቦታ ተጭነው በመያዝ ሜኑውን መክፈት ይቻላል + በጥቃቅን ምስሎች ላይ \"በኋላ እመለከታለሁ\" የሚለውን ቁልፍ አሳይ + በፊድ ቪዲዮዎች አሳሳል ላይ \"በኋላ እይ\" የሚል ቁልፍ ይታያል። ተጭነው በመያዝ \"ወደ የቪዲዮ ዝርዝር አስቀምጥ\" የሚለውን ይክፈቱ + የፖስት ምስሎችን አውርድ + ምስሎች ወይም GIFs ላሏቸው ፖስቶች ከShare බොተን ጎን የአውርድዳ බොተን ይታያል፣ እንዲሁም በሜኑ ውስጥ የአውርድዳ ንጥል ይጨመራል + ምስሎችን አውርድ + ምስል አውርድ + %s ምስሎች እየወረዱ ነው… + ምስል እየወረደ ነው… + %1$s ምስሎች በ %2$s ተቀምጠዋል + ምስሉ በ %s ተቀምጧል + ምስሉን ማውረድ አልተቻለም + በዚህ ፖስት ውስጥ ምንም ምስል አልተገኘም + \'ጠይቅ\' የሚለውን ንጥል ወደ ዝርዝር ሜኑ አክል + የቪዲዮ ማጠቃለያ ንጥልን ወደ ፊድ ዝርዝር ሜኑ ይጨምራል + "Ask በመጀመሪያ Ask በሚገኝበት አገር ውስጥ የገባ አካውንት ይጠይቃል። + +Ask የሚለው ቁልፍ ያለው ቪዲዮ አንዴ ከተከፈተ በኋላ፣ አሁን ያሉበት አገር የAsk ባህሪ ባይኖረውም እንኳ ይህ ባህሪ በሁሉም ቪዲዮዎች ላይ ይሰራል።" + ቪዲዮውን አጠቃልል + ለዚህ ቪዲዮ Ask መክፈት አልተቻለም + Ask ለአካውንትዎ ወይም አሁን ላሉበት አገር አይдоступም + የዕይታ ብዛትን በቲምብኔል ላይ አሳይ + የፊድ ቪዲዮዎች የዕይታ ብዛት ከርዕሱ በታች ከመሆን ይልቅ በቲምብኔላቸው ግርጌ በግራ በኩል ይታያል። እንደ ትረካዎች ያሉ አንዳንድ ቪዲዮዎች የዕይታ ብዛት ላይያሳዩ ይችላሉ + የፊድ ታምብነል የላይክ ብዛትን አሳይ + የፊድ ታምብነል የላይክ መቶኛን አሳይ + አስተያየቶችን ፈልግ + የተፈለጉትን ቃላት ሁሉ የያዙትን የተጫኑ አስተያየቶች ብቻ ለማሳየት በአስተያየቶች ውስጥ ያለውን የፍለጋ ቁልፍ ይጫኑ። ሐረጎችን ለመፈለግ ቃላቱን በጥቅስ ውስጥ ያስገቡ + አስተያየቶችን ፈልግ + የሚፈለጉ ቃላት + ፈልግ + አጥፋ + የመደርደሪያ ቅደም ተከተሉን ይቀይሩ ወይም ፍለጋውን ለመጀመር አስተያየቶቹን እንደገና ይክፈቱ + ፍለጋው ተሰርዟል። ሁሉንም አስተያየቶች ለማሳየት የመደርደሪያ ቅደም ተከተሉን ይቀይሩ ወይም አስተያየቶቹን እንደገና ይክፈቱ + \'ወደ ቲቪ ግባ\' የሚለውን የፖፕ-አፕ መልዕክት አጥፋ + \'የሚጠቀሙባቸውን መሳሪያዎች ያገናኙ\' የሚለውን የpopup መልእክት አግድ + የሁለቴ ንክኪ ምዕራፍ ዝላይን አግድ + ምዕራፎችን የሚዘልለውን የሁለት ጣት ባለ ሁለት ጊዜ መነካካት ምልክትን ያጠፋል፤ ይህም በአንድ ጣት በስህተት ሊነካ ስለሚችል ነው + ቪዲዮዎችን፣ ፖድካስቶችን ወይም ዘፈኖችን በሶስተኛ ወገን ማውረጃ መተግበሪያዎች ያውርዱ + የተጫዋች ኦቨርሌ ዳውንሎድ ቁልፍን አሳይ + የውጭ ዳውንሎደርዎን የሚከፍት ቁልፍ በቪዲዮ ተጫዋቹ ውስጥ ያሳያል + በአንድ ንካሽ ፈጣን ማውረጃ + በስተጀርባ እንዲወርድ ይጠይቃል። ማውረጃውን ለመክፈት ተጭነው ይያዙ። ከ YTDLnis እና YTindir ጋር ይሰራል + የወረዱትን እንደታዩ ምልክት አድርግ + በውጫዊ ማውረጃ የወረዱ ቪዲዮዎች ወደ YouTube የትዕይንት ታሪክዎ ይታከላሉ + ሙሉ HD (1080p) + መካከለኛ (192 kbps) + ፈጣን ማውረድ + ቪዲዮ + ድምጽ + ሁሉም አማራጮች + በ Shorts ዝርዝር ውስጥ ዳውንሎድ አሳይ + ለ Shorts ማጫወቻ የውጭ ዳውንሎደር ዝርዝር ሜኑ ይጨምራል + ዳውንሎድ + የድምፅ ትርጉም + የቪዲዮውን የግርጌ ጽሑፎች በሳቢው መሣሪያ ላይ ባለው የጽሑፍ-ወደ-ድምፅ አገልግሎት በመጠቀም ከቪዲዮው ጋር በተናበበ መልኩ በድምፅ ያንብቡ + በቪዲዮ ማጫወቻው ላይ የተደራራቢ ቁልፍ ያሳያል። ትርጉሙን ለማብራት እና ለማጥፋት ይንኩ፣ ለፈጣን ቅንብሮች ደግሞ ተጭነው ይያዙ + ቋንቋ + የትርጉም አቅራቢ + የዋናው ኦዲዮ መጠን + ትርጉሙ በሚነበብበት ጊዜ የዋናው የYouTube ኦዲዮ መጠን + የትርጉም ድምፅ መጠን + በድምፅ የተቀረጸው ትርጉም ድምፅ መጠን + ከፍተኛ የንግግር ፍጥነት + ንግግርን በንዑስ ርዕስ ቦታ ላይ ለማስማማት ጥቅም ላይ የሚውል ከፍተኛ የፍጥነት ማባዣ + "ነፃ የትርጉም API፣ ምንም አይነት የ API ቁልፍ አያስፈልግም። በነፃ ደረጃ በቀን እስከ 5,000 ቁምፊዎች፣ ወይም በኢሜል አድራሻ በቀን እስከ 50,000 ቁምፊዎች። + +MyMemory.translated.netን ለመክፈት እዚህ ይጫኑ" + የ MyMemory ኢሜል + አማራጭ። የዕለቱን ገደብ ከ 5,000 ወደ 50,000 ቁምፊዎች ለመጨመር የ MyMemory ኢሜል አድራሻዎን ያስገቡ። ስም የሌለው የገደብ መጠንን ለመጠቀም ባዶ ይተውት + "በ OpenRouter በኩል የሚከፈልበት የ AI ትርጉም። አንድ ጊዜ ክፍያ በመፈጸም በሺዎች የሚቆጠሩ ቪዲዮዎችን ይተርጉሙ። Mistral Nemo ርካሹ አማራጭ ሲሆን፣ Gemma 4 ከፍተኛ ጥራትን ይሰጣል። + +OpenRouter.aiን ለመክፈት እዚህ ይጫኑ" + የOpenRouter API ቁልፍ + የእርስዎ OpenRouter API ቁልፍ። ቁልፍን በ OpenRouter.ai > Workspace > API Keys ላይ ይፍጠሩ + የOpenRouter ሞዴል + ብጁ + OpenRouter አልተዋቀረም + ይህን አቅራቢ ከመምረጥዎ በፊት የOpenRouter API ቁልፍዎን በቅንብሮች ውስጥ ያስገቡ + ነፃ + ለ 100 የቪዲዮ ሰዓታት %s + ድምጽ + ሲስተም + ባለብዙ ቋንቋ ወንድ + ባለብዙ ቋንቋ ሴት + ወንድ + ሴት + የመቀየር ጊዜ ነው! + ትርጉም አይገኝም + "የትርጉም አገልግሎቱ የ HTTP ስህተት %s አስመላ። የእርስዎ IP አድራሻ እስከ 24 ሰዓታት ድረስ የተገደበ ሊሆን ይችላል። + +ትርጉሞችን መጠቀም ለመቀጠል፣ በቅንብሮች ውስጥ ወደ ሌላ የትርጉም አቅራቢ ይቀይሩ ወይም VPN ይጠቀሙ" + እንደገና አታሳየው + "የእርስዎ ቅንብሮች የ OpenRouter API ቁልፍን ይያዛሉ። + +የ API ቁልፍዎ በፍጹም መጋራት የለበትም። ቁልፉን የሚያገኝ ማንኛውም ሰው የእርስዎን የ OpenRouter ክሬዲቶች መጠቀም ይችላል" + የ OpenRouter የክሬዲት ገደብ ተ reached ሆነልዎ + የ OpenRouter የክሬዲት ገደብዎ ተ erreicht ሆኗል። ቀሪ ሂሳብዎን ይሙሉ ወይም በ OpenRouter.ai ላይ የቁልፍ ወጪ ገደብዎን ይጨምሩ + OpenRouter.aiን ይክፈቱ + የ OpenRouter ትርጉም አልተሳካም (HTTP %s) + የ MyMemory የዕለት ገደብ ተ erreicht ሆኗል + ቀጥሎ የሚገኘው በ %s ላይ ነው + የዛሬውን ነፃ የ MyMemory ትርጉም ኮታ (5,000 ቁምፊዎች) ተጠቅመዋል። የዕለት ገደቡን ወደ 50,000 ቁምፊዎች ለማሳደግ በቅንብሮች ውስጥ የ MyMemory ኢሜይል ያክሉ፣ ወይም ወደ ሌላ የትርጉም አገልግሎት ይቀይሩ + የዛሬውን የ MyMemory ትርጉም ኮታ (50,000 ቁምፊዎች) ተጠቅመዋል። ወደ ሌላ የትርጉም አገልግሎት ይቀይሩ ወይም ነገ እንደገና ይሞክሩ + የMyMemory ትርጉም አልተሳካም (ሁኔታ %s) + ቪዲዮን ደጋግሞ ማጫወት አብራ + ቪዲዮን መድገምን አትያዝ + የቪዲዮ መድገም ወቅታዊ ሁኔታ ለሚቀጥሉት ቪዲዮዎች እንዳይቀመጥ ያድርግ + ቪዲዮውን ደጋግሞ አጫውት ቁልፍን አሳይ + ቪዲዮን ደጋግሞ ማጫወት በርቷል + የቪዲዮ ሉፕ ተዘግቷል + የዙር ክልል + መጀመሪያ + መጨረሻ + [ሰዓት:]ደቂቃ:ሰከንድ + [ሰዓት:]ደቂቃ:ሰከንድ (ባዶ = የቪዲዮው መጨረሻ) + አስቀምጥ + አጽዳ + ከ %1$s - %2$s በመደጋገም ላይ ነው + የተሳሳተ የጊዜ ገደብ + የስዕል በስዕል ቁልፉን አሳይ + የስዕል በስዕል ሜኑን አሳይ + ወደ ስዕል በስዕል ቀይር + ሁሉንም አጫውት ቁልፍን አሳይ + "ከቻናሉ ያሉትን ሁሉንም ቪዲዮዎች የይዘት ዝርዝር ለመፍጠር ይጫኑ፣ ለመሰረዝ ደግሞ ተጭነው ይያዙ + +መረጃ፦ በቀጥታ ስርጭቶች ላይ ላይሰራ ይችላል" + የፕሌይሊስት ማመንጫ ሞድ + ሁሉም ይዘቶች (በጊዜ ተራ፣ ከትንሽ ወደ ትልቅ) + ሁሉም ይዘቶች (በጊዜ ተራ) + ሁሉም ይዘቶች (በተወዳጅነት ደራጁ) + ቪዲዮዎች ብቻ (በጊዜ ደራጁ) + ቪዲዮዎች ብቻ (በተወዳጅነት ደራጁ) + Shorts ብቻ (በጊዜ ደራጁ) + Shorts ብቻ (በተወዳጅነት ደራጁ) + የቀጥታ ስርጭት ቪዲዮዎች ብቻ (በጊዜ ደራጁ) + የቀጥታ ስርጭት ቪዲዮዎች ብቻ (በተወዳጅነት ደራጁ) + ለአባላት ብቻ የሆኑ ይዘቶች ሁሉ + ለአባላት ብቻ የሆኑ ቪዲዮዎች + ለአባላት ብቻ የሆኑ Shorts + ለአባላት ብቻ የሆኑ የቀጥታ ስርጭቶች + የቻናል መለያ አለመጣጣም ምክንያት የplaylist ዝርዝር ማመንጨት አልተቻለም + ቪዲዮውን እንደገና ጫን ቁልፍን አሳይ + የቪዲዮ መለያ አልተገኘም። እባክዎ ቪዲዮውን በእጅዎ እንደገና ይጫኑት + \'በኋላ እመለከተዋለሁ\' ላይ አስቀምጥ የሚለውን ቁልፍ አሳይ + \'በኋላ እመለከተዋለሁ\' ላይ ለማስቀመጥ ይጫኑ፣ ለተራ ምርጫዎች ተጭነው ይያዙ + ቪዲዮው በበኋላ የምመለከተው ዝርዝር ውስጥ በተሳካ ሁኔታ ተቀምጧል + ይህ ቪዲዮ አስቀድሞ በበኋላ የምመለከተው ዝርዝር ውስጥ ተቀምጧል + ይህንን ቪዲዮ በበኋላ የምመለከተው ዝርዝር ውስጥ ማስቀመጥ አይቻልም + \'በኋላ እመለከተዋለሁ\' ላይ አስቀምጥ + በተጫዋች ቅንብሮች ፍላይአውት ውስጥ \"لاحاقاً ይመልከቱ\" ውስጥ ያስቀምጡ የሚለውን ቁልፍ ያሳዩ + አሁን እየተመለከቱት ያለውን ቪዲዮ ከቪዲዮ ቅንብሮች ፍላይአውት በኩል \"لاحاقاً ይመልከቱ\" ውስጥ ለማስቀመጥ ያስችላል + በልጆች ቪዲዮዎች ውስጥ \'በኋላ እመለከተዋለሁ\' ላይ አስቀምጥ የሚለውን ቁልፍ አሳይ + ከልጆች ጋር የተያያዙ ቪዲዮዎችን ከፍላይአውት ሜኑ በኩል \"لاحاقاً ይመልከቱ\" ውስጥ ለማስቀመጥ ያስችላል + በሾርቶች ፍላይአውት ውስጥ \"لاحاقاً ይመልከቱ\" ውስጥ ያስቀምጡ የሚለውን ቁልፍ ያሳዩ + ሾርቶችን ከፍላይአውት ሜኑ በኩል \"لاحاقاً ይመልከቱ\" ውስጥ ለማስቀመጥ ያስችላል + ወደ Morphe ዝርዝር አክል + ተረኛውን በስብስቦች መካከል አቆይ + መተግበሪያው እንደገና ሲጀመር ተረኛው ይቀመጣል + የማስቀመጫ እና የተረኛ ተግባራትን ቀያይር + ለተራ ምርጫዎች ይጫኑ፣ \'በኋላ እመለከተዋለሁ\' ላይ ለማስቀመጥ ተጭነው ይያዙ + ቀጥሎ በቅደም ተከተል እንዲጫወት የሚደረግ ዓይነት + መደበኛ + የተፈጥሮ ቅደም ተከተል + የMorphe ቅደም ተከተል + የMorphe ቅደም ተከተል በደንበኛ በኩል + የፊድ ፍላይአውት የቅድሚያ ተራ ሜኑ ንጥል አክል + ተጨማሪ ፍላይአውት ንጥል ይጨምራል። \"ወደ ቅድሚያ ተራ አክል\" ካልታየ ይህንን ያግብሩት + ተረኛ + ተራ (%s) + ሁሉንም አጥፋ + ምንም የተረኛው የለም + በማንኛውም ቪዲዮ ላይ \'በተረኛው ቀጥሎ አጫውት\' የሚለውን ይጠቀሙ + አሁን እየተጫወተ ያለዉ + በተረኛው ቀጥሎ አጫውት + በተራው መጨረሻ ላይ ይጫወት + ቅደም ተከተሉን ቀይር + አስወግድ + ወደ ተረኛው ተጨምሯል + ይህንን ቪዲዮ ወደ ተራው መጨመር አልተቻለም + የቪዲዮ ዝርዝሮች ወደ ተራው ሊጨመሩ አይችሉም + ተሰላፊው ሞልቷል + ቪዲዮውን ማስጀመር አልተቻለም + የማስታወቂያ ነጭ ዝርዝር በflyout ሜኑ ውስጥ + በዚያ ቪዲዮ ቻናላ ላይ ማስታወቂያዎች እንዲጫወቱ የሚያስችል የflyout ንጥል ይጨምራል + ይህ ባህሪ PotHelper መጠቀምን ወይም \"Spoof video streams\"ን ወደ TV ወይም Android VR sign-in መቀየርን ሊጠይቅ ይችላል + የፍጥነት ነጭ ዝርዝር በflyout ሜኑ ውስጥ + የዚያን ቪዲዮ ቻናል ሁልጊዜ በ1.0x ፍጥነት የሚያጫወት የflyout ንጥል ይጨምራል + አልገቡም + የፕሌይሊስት መለያ የለም + ተራው ባዶ ነው + የቪዲዮ መታወቂያ የለም + ተበላሽቷል: %s + ወደ ተርታ ለመጨመር አልተቻለም + ተርታ ለመፍጠር አልተቻለም + ከተርታ ለማስወገድ አልተቻለም + ተርታውን ለማስቀመጥ አልተቻለም + ወደ ተርታ ተጨምሯል + ተርታ ተፈጥሯል + ከሥር列ው ተወግዷል + ተርታው በ %s ተቀምጧል + Morphe Queue + ወደ ተርታ ጨምር + ወደ ተርታ ይጨምሩ እና ይክፈቱ + ወደ ተርታ ይጨምሩ እና ያጫውቱ + ይጨምሩ እና ቪዲዮውን እንደገና ያስጫኑ + ከተርታ ያስወግዱ + ያስወግዱ እና ተርታውን ይክፈቱ + ያስወግዱ እና ቪዲዮውን እንደገና ያስጫኑ + ተርታውን ይክፈቱ + ተረኛውን ወደ ስብስብ ማስቀመጥ + የመጫኛ ዝርዝር ተቀምጧል + በ%s ተቀምጧል + ተራውን አጽዳ + ተራው ተጽድቷል + ተራ + በተራው ውስጥ ያለውን የመጨረሻውን አጫውት + በተራው ውስጥ ያለውን የሚቀጥለውን አጫውት + YouTube ተርታውን ማሳየት አልቻለም። + ተርታው ለዚህ ቪዲዮ ዝግጁ አይደለም። የቪዲዮ ማጫወቻውን ይክፈቱ እና እንደገና ይሞክሩ። + ሚክሱን ወደ ተርታው ይጨምሩ ወይስ ሚክሱን ይመልከቱ? + ይመልከቱ + ወደ ተርታው ይጨምሩ + ሚክስ እየተጫነ ነው… + ቻናሉን የቀጥታ አቫታር ተጠቅመው ይክፈቱ + የቻናሉን አቫታር ሲነኩ የቀጥታ ስርጭቱ እንዳይከፈት ይከላከላል + ትክክለኛ የፍለጋ እንቅስቃሴን አጥፋ + የፍለጋ አሞሌውን ወደ ላይ ሲጎትቱ የሚነቃውን ትክክለኛ የፍለጋ ዘዴ ያጠፋል + የቀጥታ ስርጭት DVR አግብ + በተለመደው ሁኔታ የሚገደቡትን ጨምሮ በሁሉም የቀጥታ ስርጭቶች ላይ ወደ ኋላ ወይም ወደ ፊት ለመሄድ ይፍቀዳል + የፍለጋ ባር ትምኔዎችን አግብር + የፍለጋ ባሩን ሲጎትቱ የቪዲዮ ትምኔ ያሳያል + በብዛት የተጫወተ + "'ቀጥታ ስርጭት DVR አግብር' ላይገኝ ይችላል + +ይህን ለማስተካከል፣ 'የቪዲዮ ስርጭቶችን አስመስል' የሚለውን አብራ እና 'ነባር ደንበኛ'ን ወደ 'TV Simply' ወይም 'visionOS' ቀይር" + የቀጥታ ስርጭት DVR ቆይታን አራዝም + የቀጥታ ስርጭቶችን እስከ 7 ቀናት ወደ ኋላ ለመመለስ ያስችላል + "'የቀጥታ ስርጭት DVR ቆይታን አስፋፋ' ላይገኝ ይችላል + +ይህንን ለማስተካከል 'የቪዲዮ ስርጭቶችን አስመስል' የሚለውን አብራ እና 'መደበኛ ደንበኛን' ወደ 'TV Simply' ወይም 'visionOS' ቀይር" + በማስቀየር ፍለጋን አግብር + የቪዲዮ ማጫወቻውን ተጭነው ሲይዙ በ 2x ፍጥነት ከማጫወት ይልቅ ወደ ፍለጋ እንዲሸጋገር ያደርጋል + መንካትን ለመፈለግ ያብሩ + ያለ ምንም ስحب በቀጥታ ወደዚያ ቦታ ለመሄድ ሲክባሩን ይንኩት + ወደ ግራ የመጥረግ ምልክት + ወደ ቀኝ የመጥረግ ምልክት + ወደ ላይ የመጥረግ ምልክት + የግራ/ቀኝ ዞን ስፋት + ለግራ እና ቀኝ የመጥረግ ምልክቶች የተያዘው የእያንዳንዱ ስክሪን ጠርዝ በመቶኛ (5-50) + ብሩህነት + ድምጽ + ተፈጥሯዊ + ጠፍቷል + ሲቆለፍ የመጥረግ ምልክቶችን ችላ በል + ተጫዋቹ በ\'ስክሪን ቆልፍ\' ተቆልፎ ሳለ የመጥረግ ምልክቶች ችላ ይባላሉ + ለመጥረግ የመጫን ምልክትን ያብሩ + የስላይድ ምልክት ከመታወቁ በፊት ተጭኖ መቆየትን ይጠይቃል + የሃፕቲክ ግብረ-መልስን ያብሩ + ብሩህነትን ያስቀምጡ እና ይመልሱ + ሙሉ ስክሪን ሞድ ውስጥ ሲገቡ / ሲወጡ የብሩህነት ደረጃን ያስታውሳል + ራስ-ሰር ብሩህነት ምልክትን ያብሩ + ብሩህነትን ወደ ዝቅተኛው ዋጋው ማሳለጥ የሲስተሙን አውቶ-ብሩህነት ያነቃቃል + ራስ-ሰር + የመጥረጊያ ሽፋን የጊዜ ገደብ + ሽፋኑ የሚታይበት ሚሊሰከንዶች መጠን + የመጥረጊያ ሽፋን የጀርባ ግልጽነት + የግልጽነት ደረጃን ያስተካክሉ (0 ግልጽ ነው፣ 100 ደግሞ የማይበራ ነው) + የስዊፕ ኦቨርሌ ብራይትነስ ቀለም + የብሩህነት መቆጣጠሪያዎች የሂደት ባር ቀለም ይወስኑ + የስዊፕ ኦቨርሌ የድምፅ መጠን ቀለም + የድምፅ መቆጣጠሪያዎች የሂደት ባር ቀለም ይወስኑ + የስዊፕ ኦቨርሌ ፍጥነት ቀለም + የመጫወት ፍጥነት መቆጣጠሪያዎች የሂደት አሞሌ ቀለም ያዘጋጁ + የስዊፕ ኦቨርሌ የጽሑፍ መጠን + በስክሪኑ ላይ ያለውን የጽሑፍ መጠን ያስተካክሉ (1-30) + የስዊፕ መጠን ገደብ + ማስተካከያ ከመጀመሩ በፊት የሚያስፈልገውን የማሳለጥ ርቀት ይወስኑ + የድምፅ ማሳለፊያ ስሜት + ከፍተኛ እሴቶች ለእያንዳንዱ የድምፅ ደረጃ ተጨማሪ ማሳለፊያ ይፈልጋሉ + የድምፅ ደረጃዎች + የድምፅ መጠን መጨመሪያ + ከፍተኛው የድምፅ መጠን አልፎ ወደ ላይ ስላይድ ማድረግ ድምፁን ይጨምረዋል፣ ይህም ከፍተኛ ድምፆችን ሊያበላሸው ይችላል + የመሣሪያው መደበኛ + የብሩህነት ስዊፕ ስሜት + ከፍተኛ እሴቶች በእያንዳንዱ የብሩህነት ደረጃ ላይ ተጨማሪ ስዊፕ ይፈልጋሉ + የፍጥነት ስዊፕ ስሜት + ከፍተኛ እሴቶች በእያንዳንዱ የፍጥነት ደረጃ ላይ ተጨማሪ ስዊፕ ይፈልጋሉ + የፍጥነት ደረጃ + የፍጥነት ዞን ቁመት + የፍጥነት ምልክት ዞን ቁመት ከጫዋዩ ቁመት በመቶኛ (5-75) + ፍጥነት + የስዊፕ ኦቨርሌ ስታይል + አግድም ኦቨርሌ + አግድም ኦቨርሌ (ሚኒማሊስት - የላይኛው) + አግድም ኦቨርሌ (ሚኒማሊስት - መሃል) + ክብ ኦቨርሌ + ክብ ኦቨርሌ (ሚኒማሊስት) + ቁመታዊ ኦቨርሌ + ቁመታዊ ኦቨርሌ (ሚኒማሊስት) + ቪዲዮዎችን ለመቀየር ስዊፕን ያግብሩ + ወደ ቀጣዩ ወይም የቀድሞው ቪዲዮ ለመዝለል በሙሉ ስክሪን ሞድ ውስጥ ያሳልጥዱ + የድርጊት ቁልፎች + በቁመታዊ እና በሙሉ ስክሪን ሞዶች ውስጥ የትኞቹ የድርጊት ቁልፎች እንደሚታዩ ይምረጡ + የቁመታዊ እይታ ቁልፎች + የላይክ እና ሰብስክራይብ ብርሃንን ያጥፉ + አንድ ፈጣሪ ሲጠቅሷቸው የላይክ እና የሰብስክራይብ ቁልፎች እንዳይበሩ ይከላከላል + የአክሽን ባርን ደብቅ + የላይክ እና የዲስላይክ ቁልፎችን ደብቅ + የሼር ቁልፍን ደብቅ + \'ማስታወቂያዎችን አቁም\' ቁልፍን ደብቅ + የአስተያየቶች ቁልፍን ደብቅ + የ\"ተገናኝ\" ቁልፍን ደብቅ + የሪፖርት ቁልፍን ደብቅ + የሪሚክስ ቁልፍን ደብቅ + የዳውንሎድ ቁልፍን ደብቅ + የHype ቁልፍን ደብቅ + የአስተዋወቅ ቁልፍን ደብቅ + የምስጋና ቁልፍን ደብቅ + የጠይቅ ቁልፍን ደብቅ + ቻናሉን እና የሰብስክራይብ ቁልፍን ደብቅ + በድርጊት አሞሌው መጀመሪያ ላይ ያለውን የቻናሉን አቫታር እና የሰብስክራይብ ቁልፍ ይደብቃል + የመመዝገቢያ አዝራርን ደብቅ + በድርጊት አሞሌው መጀመሪያ ላይ ከቻናሉ አቫታር አጠገብ ያሉትን የመመዝገቢያ እና የመቀላቀያ አዝራሮችን ይደብቃል + የክሊፕ ቁልፍን ደብቅ + ይህ ለአንዳንድ የተጠቃሚ አካውንቶች ላይሰራ ይችላል + የሱቅ ቁልፍን ደብቅ + የሴቭ ቁልፍን ደብቅ + የ\"ተጨማሪ\" ቁልፍን ደብቅ + የሙሉ ስክሪን ቁልፎች + የአልወደድኩትም ቁልፍን ደብቅ + የወደድኩት ቁልፍን ደብቅ + የ\'ቀጥታ ውይይት\' ቁልፍን ደብቅ + የሚክስ ቁልፍን ደብቅ + የ\'ተጨማሪ ቪዲዮዎች\' ቁልፍን ደብቅ + የplaylist ቁልፍን ደብቅ + የፈጣን ተግባራት የላይኛው ጠርዝ + የፈጣን ተግባራት አሞሌ የላይኛውን ጠርዝ ያውጡ (0-32 dp) + የግርጌ ጽሁፎች + ራስ-ሰር የጽሑፍ መግለጫዎችን እና የጽሑፍ መግለጫ ኩኪዎችን ያจัดการுங்கள் + አውቶማቲክ የግርጌ ጽሁፎች ታይነት + ሁልጊዜ አሳይ + በጭራሽ አታሳይ + ድምጽ ሲበራ አሳይ + ድምጽ ሲጠፋ አሳይ + አውቶማቲክ የግርጌ ጽሁፎችን አሳይ + ራስ-ሰር የግርጌ ጽሁፎችን ደብቅ + የሚመረጥ የልባዊ ጽሁፍ ቋንቋ + መጀመሪያ የአቅራቢውን ንዑስ ርዕስ ይመርጣል፣ ከዚያም ወደሚመረጠው ቋንቋ በራስ-ሰር ይተረጉማል + ጠፍቷል + ተጨማሪ የራስ-ትርጉም ቋንቋዎችን አክል + ወደ ራስ-ትርጉም ዝርዝር ተጨማሪ ቋንቋዎችን ያክላል + የርዕስ ማብራሪያ ኩኪዎችን ያዘጋጁ + "በኩኪዎች እጥረት ምክንያት አውቶ-ትራንስሌት ካፕሽኖች ላይገኙ የሚችሉበትን የYouTube ስቶክ ስህተት ያስተካክላል + +'የካፕሽን ኩኪዎች' መሰጠት አለባቸው" + የግርጌ ጽሁፍ ኩኪዎች + በTimed Text API (Caption Data API) ጥያቄዎች ውስጥ የሚቀመጡ ኩኪዎች + የግርጌ ጽሁፍ ኩኪዎችን ያግኙ + ኩኪዎችን ለማግኘት እዚህ ይጫኑ + ኩኪዎችን ለማግኘት አልተቻለም + ኩኪዎች ቀድሞውኑ አሉ + ኩኪዎች በተሳካ ሁኔታ ተዘምነዋል + የንግግር ጽሁፉን አስተካክል + የጽሑፍ ትርጉም እንዳይጫን ሊያደርግ የሚችልን ቅድመ ሁኔታ የፍተሻ ስህተት ይፈታዋል + በምስል ውስጥ ምስል ውስጥ የግርጌ ጽሁፎችን አሳይ + የናቪጌሽን ቁልፎች + በታችኛው የናቪጌሽን ባር ላይ የትኞቹ ቁልፎች እንደሚታዩ ይምረጡ + የመነሻ ገጽ ቁልፍን ደብቅ + የShorts ቁልፍን ደብቅ + የፍጠራ ቁልፍን ደብቅ + የደንበኝነቶች ቁልፍን ደብቅ + የማሳወቂያዎች ቁልፍን ደብቅ + የፍለጋ ቁልፍን አሳይ + የፍለጋ ቁልፍ ቦታ + የቅንብሮች ቁልፍን አሳይ + የቅንብሮች ቁልፍ ቦታ + የቅንብሮች ቁልፍ ተግባር + ሲበራ የ Morphe ቅንብሮችን፣ ሲጠፋ ደግሞ የ YouTube ቅንብሮችን ይከፍታል + የመፍጠሪያውን ቁልፍ ከማሳወቂያዎች ቁልፍ ጋር ይቀያይሩ + ይህንን ቅንብር መቀየር ውጤት ካላመጣ፣ ወደ Incognito ሁነታ ለመቀየር ይሞክሩ + የናቪጌሽን ቁልፍ ሌብሎችን ደብቁ + የአዲስ ይዘት ነጥብን ደብቅ + የናቪጌሽን ባርን ደብቅ + "ይህ 'እርስዎ' የሚለውን ታብ ጨምሮ የናቪጌሽን ባሩን ሙሉ በሙሉ ይደብቃል። + +ወደ ቅንብሮች ለመግባት፣ General → Toolbar → Show Settings በኩል የቅንብሮች ቁልፍን ያግብሩ" + ጠባብ የትራንስፖርት ቁልፎችን አግብር + በቁልፎች መካከል ያለውን አግድም ርቀት ይቀንሳል + መለያ የሌላቸውን የንቅናቄ ቁልፎች አግድ + YouTube መለያ ሳይሰጣቸው ቢልካቸውም፣ የንቅናቄ ቁልፎቹን ከመለያ ጋር ያሳያል + የቱልባር አሰሳ ታቦችን አጥፋ + YouTube ወደ Home አናት ላይ ያለ ታብ ካዛወረው፣ በናቪጌሽን ባሩ ላይ Subscriptionsን ያሳያል + የናቪጌሽን ባር አኒሜሽኖችን ያግብሩ + በታቦች መካከል ሲቀያየሩ ሽግግሮችን ያነቃቃል + የራስ-ሰር መደበቂያ የናቪጌሽን ባርን አጥፋ + ሲሽከረከሩ የናቪጌሽን ባሩ እንዲታይ ያደርጋል + የናቪጌሽን ባር ስታይል + መደበኛ + ግልጽ + ግልጽ + ደመቅ ያለ + ይህ ስታይል በኮምፒውተርዎ ወይም በYouTube ስሪት ላይ ላይደገፍ ይችላል፣ እና የንድፍ ችግሮችን ወይም መቋረጥን ሊያስከትል ይችላል + የመሳሪያ ባር + በቱልባሩ ላይ የትኞቹ ክፍሎች እንደሚታዩ ይምረጡ + የ Cast ቁልፍን ደብቅ + የቻት ቁልፍን ደብቅ + የማይክሮፎን ቁልፍን ደብቅ + የፍለጋ ቁልፍን ደብቅ + ሲበራ፣ አንድ ጊዜ በመንካት የ Morphe ቅንብሮችን እና ረጅም ጊዜ በመጫን የ YouTube ቅንብሮችን ይከፍታል፤ ለመቀያየር ያጥፉት + የፍላይአውት ሜኑ + በተንሳፋፊው ሜኑ ውስጥ የትኞቹ ክፍሎች እንዲታዩ እንደሚፈልጉ ይምረጡ + \'ተጨማሪ ቅንብሮች\' / ተጨማሪ ሜኑን ደብቅ + \'አምቢየንት ሞድ\' ሜኑን ደብቅ + "'Audio Track' ሜኑ ተደብቋል + +'Audio Track' ሜኑን ለማሳየት፣ 'Spoof video streams'ን ከ 'Android Studio' እና 'Android VR Downgraded' ውጭ ወደ ማንኛውም ክላየን ይለውጡት" + የ\'የድምፅ ትራክ\' ሜኑን ደብቅ + የ\'የድምፅ ትራክ\' ሜኑ የግርጌውን ክፍል ደብቅ + የርዕስ ገጽ ሜኑን ደብቅ + የርዕስ ገጽ ሜኑ የግርጌውን ክፍል ደብቅ + የግርጌ ጽሁፎች ሜኑ ርዕስን ደብቅ + \'እርዳታ እና አስተያየት\' ሜኑን ደብቅ + \'በYouTube Music አዳምጥ\' ሜኑን ደብቅ + \'ስክሪኑን ዝጋ\' ሜኑን ደብቅ + \'ቪዲዮውን ደጋግሞ አጫውት\' ሜኑን ደብቅ + \'በጉዞ ላይ\' ሜኑን ደብቅ + የጥራት ሜኑን ደብቅ + የጥራት ሜኑ የግርጌውን ክፍል ደብቅ + የጥራት ሜኑ የራስጌውን ክፍል ደብቅ + \'የእንቅልፍ ሰዓት\' ሜኑን ደብቅ + \'የመጫወቻ ፍጥነት\' ሜኑን ደብቅ + \'የተረጋጋ ድምፅ\' ሜኑን ደብቅ + \'በVR ተመልከት\' ሜኑን ደብቅ + ራስ-ሰር የቅድመእይታ ማሳያን ደብቅ + ራስ-ሰር ማጫወቻ ከመጀመሩ በፊት የሚመጣውን የቪዲዮ ቅድመ-እይታ በሙሉ ስክሪን ላይ ይደብቃል + የላይኛው ሽፋን ቁልፎች + በቪዲዮ ማጫወቻው ላይ የትኞቹ ቁልፎች እንዲታዩ እንደሚፈልጉ ይምረጡ + የራስ-ሰር ማጫወቻ ቁልፍን ደብቅ + የመግለጫዎች ቁልፍን ደብቅ + የካስት ቁልፍን ደብቅ + የማጠፍ ቁልፍን ደብቅ + የሙሉ ስክሪን ቁልፍን ደብቅ + የተጫዋች መቆጣጠሪያዎች የጀርባ ምስልን ደብቅ + የቪዲዮ ማጫወቻ ቁጥጥሮች አካባቢ ያለውን ጥቁር ጥላ ይደብቃል + የቁጥጥር በተኖች የጀርባ ጥርኝነት + ከተጫዋች መቆጣጠሪያ ቁልፎች እና ከጊዜ ባር በስተጀርባ ያለውን የጀርባ ቀለም ግልጽነት ያስተካክሉ (0 የማይታይ ነው፣ 100 ሙሉ ነው) + የቀድሞ እና የቀጣይ ቁልፎችን ደብቅ + የቅንብሮች ቁልፍን ደብቅ + የመጨረሻ ስክሪን ካርዶችን ደብቅ + አምቢየን ሞድ + የAmbient mode ታይነትን እና የኃይል ገደቦችን ያስተዳድር + የAmbient mode ገደቦችን ዝለል + Ambient mode በባትሪ ቆጣቢ ሁነታ ውስጥ ንቁ ሆኖ እንዲቆይ ይፈቅዳል + Ambient modeን አጥፋ + በሙሉ ስክሪን ላይ Ambient ሞድን አቋርጥ + የመረጃ ካርዶችን ደብቅ + የሚሽከረከሩ የቁጥር አኒሜሽኖችን አቋርጥ + በላይክ እና በዕይታ ብዛት ላይ ያለውን የሚቆጠር አኒሜሽን ያቆማል + የቪዲዮ ተጫዋች ሲክባርን ደብቅ + የቪዲዮ አጭር ምስሎች ሲክባርን ደብቅ + ቀደም ብለው ባዩዋቸው የቪዲዮ አጭር ምስሎች ላይ የሚታየውን የዕይታ ሂደት አሞላል ባር ይደብቃል + ትልቅ የሙሉ ስክሪን ሲክባርን አግብር + Shorts ደብቅ + በቻናል ገጽ ላይ ደብቅ + በመነሻ ገጽ ፍድ እና ተዛማጅ ቪዲዮዎች ውስጥ ደብቅ + በፍለጋ ውጤቶች ውስጥ ደብቅ + በምዝገባዎች የይዘት ስርጭት ውስጥ ደብቅ + በቪዲዮ መግለጫ ውስጥ ደብቅ + በተመለከቱት ታሪክ ውስጥ ደብቅ + የShorts ማጫወቻ + በShorts ፕሌየር ውስጥ የትኞቹ ክፍሎች እንደሚታዩ ይምረጡ + ለመውደድ የሁለት ጊዜ ንክኪን አጥፋ + የAI ቁልፍን ደብቅ + \'በራስ-ሰር የተተረጎመ\' የሚለውን ምልክት ደብቅ + የቻናል ባርን ደብቅ + የአስተያየቶች ቁልፍን ደብቅ + የተፅዕኖ ቁልፍን ደብቅ + የቪዲዮ ሊንክ ሌብልን ደብቅ + የምልክት ፍንጮችን ደብቅ + \'አረንጓዴ ስክሪን\' የሚለውን பொታን ደብቅ + የሀሽታግ ቁልፍን ደብቅ + የመረጃ ፓነልን ደብቅ + የተቀላቀሉ ቁልፍን ደብቅ + የላይክ ቁልፍን ደብቅ + የላይክ ቁልፍ የፏፏቴ አኒሜሽንን ደብቅ + ቀጥታ ቅድመእይታን ደብቅ + የቦታ ምልክቱን ደብቅ + የናቪጌሽን ባርን ደብቅ + \'አዳዲስ ፖስቶችን\' ቁልፍ ደብቅ + የቆመ የላይኛው ንብርብር ቁልፎችን ደብቅ + የቅድመ እይታ አስተያየትን ደብቅ + የሪሚክስ ቁልፍን ደብቅ + የአስቀምጥ ቁልፍን ደብቅ + \'ሙዚቃን አስቀምጥ\' ቁልፍ ደብቅ + የፍለጋ ጥቆማዎችን ደብቅ + የမျှር ቁልፍን ደብቅ + የሱቅ ቁልፍን ደብቅ + የድምፅ ቁልፍን ደብቅ + የድምፅ ሜታዳታ ሌብልን ደብቅ + የሰብስክራይብ ቁልፍን ደብቅ + \'Super Thanks ግዛ\' የሚለውን பொத்தான ደብቅ + የተያያዙ ምርቶችን ደብቅ + የሚመጡትን ቁልፍ ደብቅ + \'ይህንን ድምጽ ተጠቀም\' ቁልፍ ደብቅ + \'ይህንን ቴምፕሌት ተጠቀም\' ቁልፍ ደብቅ + የቪዲዮ ርዕስን ደብቅ + በመጨረሻው ስክሪን ላይ የሚመረከቡ ቪዲዮዎችን ደብቅ + "ቪዲዮው ካለቀ በኋላ የሚታየውን የሚመከር ቪዲዮ ኦቨርሌ ይደብቃል + +ይህ ቅንብር የሚሰራው አውቶፕሌ ጠፍቶ ከሆነ ብቻ ነው። በ YouTube ውስጥ መቀየር ይችላሉ፦ +ቅንብሮች → ተጫወች ወይም አውቶፕሌ → ቀጣዩን ቪዲዮ አውቶፕሌ አድርግ" + ተዛማጅ ቪዲዮዎችን ደብቅ + \'ተጨማሪ ቪዲዮዎች\' የሚለውን የላይኛው ሽፋን ደብቅ + በሙሉ ስክሪን ሁነーション ውስጥ ወደ ላይ ሲሰحب የሚታየውን \'ተጨማሪ ቪዲዮዎች\' የሚለውን ኦቨርሌ ይደብቃል + የቪዲዮ የጊዜ ምልክትን ደብቅ + የተጫዋች ፖፕአፕ ፓነሎችን አጥፋ + የዝርዝር እና የቀጥታ ውይይት ፓነሎች በራሳቸው እንዳይከፈቱ ያደርጋል + የ playlist autoplay አጥፋ + የቪዲዮ ዝርዝሮች በራሳቸው ወደ ቀጣዩ ቪዲዮ እንዳይሸጋገሩ ይከላከላል + \'መመልከት ይቀጥሉ?\' የሚለውን ማሳሰቢያ አግድ + ቪዲዮው ሲያልቅ ሙሉ ስክሪን ሁነታን ለቀቅ + ተዘግቷል + ቁም ምስል ብቻ + አግድም ምስል ብቻ + ቁም እና አግድም + በሙሉ ስክሪን ውስጥ ወደ አግድም ያስገድድ + ወደ ሙሉ ስክሪን ሲገቡ ተጫዋቹን ወደ አግድም ያሽከረክሩ። አስቀድመው በሙሉ ስክሪን አግድም ለሚከፈቱ ትናንሽ ስክሪኖች አይሰራም + ሙሉ ስክሪን የእጅ ምልክቶች + ሙሉ ስክሪን ሁነታ ላይ ያሉ የተጫዋች ምልክቶች + ቁመታዊ ወደ ላይ የመስحب ምልክትን አግድ + ቁመታዊ ወደ ታች የመስحب ምልክትን አግድ + የአግድም ስክሪን ወደ ታች የመሳብ ምልክትን አጥፋ + የመጭመቅ-ለማሳደግ ምልክትን አግድ + የጣት መጭመቅን አጉልቶ ማሳየት መልስ + ቪዲዮዎችን በሙሉ ስክሪን ሁነታ ክፈት + ይታገዳል + ቁልቁል + አግድም + ሙሉ ስክሪን የቪዲዮ መጠን + መደበኛ + ዘርጋ (ስክሪኑን ሙሉ አድርግ) + ዙም (ለመሙላት ቁረጥ) + የቪዲዮ መጠን ቁልፍን አሳይ + Default፣ Stretch እና Zoom መካከል ለመቀያየር ይንኩ + በሙሉ ስክሪን ብቻ አሳይ + በቁመት ማጫወቻው ውስጥ የቪዲዮ መጠን ቁልፉን ደብቅ + የድምጽ ማጥፊያ ቁልፍን አሳይ + የፕሌየር ኦቨርሌ ግልጽነት + የግልጽነት ደረጃን ያስተካክሉ (0 ግልጽ ነው፣ 100 ደግሞ የማይበራር ነው) + የፍለጋ ባር + ተዘግቷል + ሰፊ + በጣም ሰፊ + የመሣሪያ አቀማመጥ + መደበኛ + ስልክ + ታብሌት + የአውቶሞቲቭ + "ለውጦቹ የሚከተሉትን ያካትታሉ፦ + +የታብሌት አቀማመጥ +• የማህበረሰብ ልጥፎች ተደብቀዋል +• በፊዶች (feeds) ውስጥ የመጫወት ቅንብር አይдоступም +• በShorts ውስጥ የሪሚክስ (Remix) እና የድምፅ (Sound) ቁልፎች አይገኙም +• 'ተጨማሪ ቪዲዮዎች' የሚለው ንብርብር ላይገኝም ይችላል +• ያልተጨመቀው የድሮው የቪዲዮ ተግባር አሞሌ ይታያል + +የመኪና አቀማመጥ +• ፊድ (feed) በርዕሰ ጉዳዮች እና በቻናሎች ተደራጅቷል +• በፊዶች (feeds) ውስጥ የመጫወት ቅንብር አይдоступም +• በShorts ውስጥ የሪሚክስ (Remix) እና የድምፅ (Sound) ቁልፎች አይገኙም +• 'ተጨማሪ ቪዲዮዎች' የሚለው ንብርብር ላይገኝም ይችላል +• Shorts በመደበኛው ተጫዋች ውስጥ ይከፈታሉ +• ያልተጨመቀው የድሮው የቪዲዮ ተግባር አሞሌ ይታያል" + በተጫዋቹ ውስጥ የታብሌት አቀማመጥን አግብር + "የድሮውን የተጫዋች አቀማመጥ ይመልሳል + +ገደብ፦ 'የድሮውን የቪዲዮ ተግባር አሞሌ መልስ' ከተጠፋ፣ 'ተጨማሪ ቪዲዮዎች' የሚለው ሽፋን ላይኖር ይችላል" + 21.11.48 - የድሮውን የግርጌ ጽሁፍ ባህሪ መልስ + 20.39.41 - የድሮውን ወደ playlist ማስቀመጫ መልስ + 20.28.41 - ደመቅ ያሉ አዶዎችን አጥፋ + 20.13.41 - ከቪዲዮ ማጫወቻው በታች የቻናሉን ስም መልሶ ያመጣል + የስርዓቱን ማጋሪያ ስክሪን ክፈት + ይዘቶችን ሲያጋሩ ከውስጥ መተግበሪያው ፋይናል። በምትኩ የሲስተም ማጋሪያ ገጹን ይከፍታል + የYouTube Music ቁልፎችን ተካ + ከስቶክ YouTube Music መተግበሪያ ይልቅ የሚመርጡትን የሙዚቃ መተግበሪያ ይከፍታል + የሙዚቃ መተግበሪያ ፓኬጅ ስም + የተጫነው የሙዚቃ መተግበሪያዎ ፓኬጅ ስም + የመጀመሪያ ገጽ + ነባር + ሁሉም የደንበኝነት ምዝቤቦች + ቻናሎችን መመልከት + ትምህርቶች / ትምህርት + ቃኘ + ፋሽን እና ውበት + ጌሚንግ + የታሪኩ ዝርዝር + ቤተ-መጻሕፍት + የተወደዱ ቪዲዮዎች + ቀጥታ ስርጭት + ፊልሞች + ሙዚቃ + ዜናዎች + ማሳወቂያዎች + የመጫወቻ ዝርዝሮች + ፈልግ + የገበያ ቦታ + Shorts + ስፖርቶች + ምዝገባዎች + ወቅታዊ ጉዳዮች + ቨርቹዋል ሪያሊቲ + ቆይተው ይመልከቱ + የእርስዎ ክሊፖች + ሲጀመር መቀጠልን አግድ + Shortsን ይክፈቱ በ + የShorts ማጫወቻ + መደበኛ ተጫዋች + መደበኛ ተጫዋች ሙሉ ስክሪን + Shorts በራሱ እንዲጫወት አድርግ + አሁኑን ከመደጋገም ይልቅ በራሱ ወደሚቀጥለው Short ይሸጋገራል + በጀርባ በራሱ እንዲጫወት አድርግ + በጀርባ በሚጫወትበት ወቅት በራሱ ወደሚቀጥለው Short ይሸጋገራል + ቀጥታ Shortsን ዝለል + በ Shorts ፊድ ውስጥ የሚታዩ የቀጥታ ስርጭቶችን ይዘላል + በፊዶች ውስጥ መልሶ ማጫወት + ሁልጊዜ በርቷል + በWi-Fi ብቻ + ጠፍቷል + የመጀመሪያዎቹን ርዕሶች መልስ + በራስ-ትርጉም ፋንታ ዋናዎቹን የቪዲዮ ርዕሶች፣ የቪዲዮ መግለጫዎች እና የቻናል መግለጫዎችን ያሳያል + ርዕስ እየተጫነ ነው… + የፊድ አውቶማቲክ ማደስን አጥፋ + ፊዶች ጊዜያቸው ካለፈ በኋላ በራሳቸው እንዳይታደሱ ይከላከላል + የፊድ ካርድ አቀማመጥ + ክላሲክ + ካርዶች + ላይክ፣ አስተያየት እና ማስጫኛ ቁልፎች ያላቸው ካርዶች + አስተያየቶች + ሚኒ ፕሌየርን መቀጠል አግድ + በመተግበሪያው መጀመሪያ ላይ የሚታየውን የYouTube Premium ባህሪ የሆነውን \'መመልከት ይቀጥሉ\' የሚለውን ሚኒ-ፕሌየር ያጠፋል + ትንሽ ተጫዋች + የተቀነሰውን የቪዲዮ ማጫወቻ ገጽታ እና ባህሪ ያስተካክሉ + የትንሽ ተጫዋች ዓይነት + ርዕስን እና ቻናሉን ደብቅ + ተዘግቷል + መደበኛ + ሚኒማሊስት + ሚኒማል 2 + ታብሌት + ዘመናዊ 1 + ዘመናዊ 2 + ዘመናዊ 3 + ዘመናዊ 4 + ክብ ጠርዞችን አጥፋ + ጎትቶ ማ放下 ማብላሸት + የተቀነሰውን ማጫወቻ ወደ ተለያዩ የስክሪን ማዕዘኖች እንዳይሳብ ይከላከላል + አግድም የጎትቶ ማስያዝ ምልክትን ማብላሸት + ሚኒ ፕሌየሩን አግድም ወደ ስክሪኑ ውጪ በመሳብ እንዳይዘጋ ይከላከላል + በአግድም በመጎተት መልሶ ማጫወትን አቁም + ሚኒ ፕሌየሩ ከስክሪኑ ውጭ ሲጎተት መልሶ ማጫወቱ ይቆማል + የጎተታ ሁኔታ ቦታን አስታውስ + ሚኒ ፕሌየሩን ከስክሪኑ ውጭ ወደ ሌላ ቦታ ሲጎትቱ የUI ሁኔታው ይቀጥላል + የላይኛው ንብርብር ቁልፎችን ደብቅ + መደበኛ መጠን + የመጀመሪያውን በስክሪኑ ላይ ያለውን መጠን በፒክስል ያዘጋጁ + የፒክሰል መጠን በ %1$s እና %2$s መካከል መሆን አለበት + የላይኛው ንብርብር ግልጽነት + የግልጽነት ደረጃን ያስተካክሉ (0 ግልጽ ነው፣ 100 ደግሞ የማይበየን ነው) + የተመለስ ቁልፍ ከፊዱን ይዘጋል + ወደ ኋላ መጫን ወደ ፊድ አናት ከመመለስ ይልቅ ሁልጊዜ መተግበሪያውን ይዘጋዋል + አኒሜሽኖችን አስገድድ + መደበኛ + ሁልጊዜ በርቶ ይቆይ + ሁልጊዜ ጠፍቶ ይቆይ + የሁኔታ አሞሌውን ደብቅ + የስርዓቱን የሁኔታ አሞሌ ይደብቃል። ለጥቂት ጊዜ ለማሳየት ከላይኛው ጠርዝ ወደ ታች ይሳቡ + የሁኔታ ማሳወቂያ ስትሪክ የላይኛውን ባሮች እንዳይሸፍን ያስተካክላል + የሁኔታ ማሳወቂያ ስትሪክ አቀማመጥ ወጥ እንዲሆን በማድረግ የላይኞቹን ባሮች እንዳይሸፍን እና ከቪዲዮ ማጫወቻው በላይ ምንም ባዶ ቦታ እንዳይኖር ያደርጋል። መተግበሪያው ካቆመ ወይም የላይኞቹ ባሮች የተሳሳተ አቀማመጥ ካሳዩ ይህንን ያጥፉት + የቀላል ጭብጥ ዳራ + ነጭ + Material You (ነጭ) + ቀለል ያለ ሮዝ + ቀለል ያለ ሰማያዊ + ቀለል ያለ አረንጓዴ + ቀለል ያለ ቢጫ + ቀለል ያለ ብርቱካናማ + ቀለል ያለ ቀይ + ብጁ የብርሃን ዳራ + የብርሃን ጭብጥ የዳራ ቀለም ያዘጋጁ + የፊት ቀለም ይቀይሩ + ለጽሑፎች እና ለሥርዓተ-ምልክቶች የጭብጥ ቀለሙን ይጠቀሙ + ግራዲየን የጫና ስክሪንን አግብር + በመጫኛ ስክሪኑ ላይ ከአንድ ወጥ ቀለም ይልቅ የቀለም ቅልቅል ዳራ ያሳያል + ብጁ የፈላጊ አሞሌ ቀለም አግብር + የሲክባር ቀለም + የቪዲዮው ሂደት አመልካች ዋና ቀለም ይወስኑ + የSeekbar የጎላ ቀለም + የቪዲዮው ሂደት አመልካች ሁለተኛ ቀለም ይወስኑ + የተሳሳተ የseekbar ቀለም ዋጋ + የራስጌ አርማ + መደበኛ + መደበኛ + ብጁ + የምስል የክልል ገደቦችን በፓስ ማድረግ + በተገደቡ አገሮች ውስጥ የሌሉ ምስሎችን ለማስተካከል የThumbnail ጥያቄዎችን ወደ አማራጭ ሰርቨር ይልካል + DeArrow ርዕሶች + መነሻ ታብ + የደንበኝነት ምዝገባዎች ታብ + እርስዎ ታብ + የተጫዋች ዝርዝሮች & ምክሮች + የፍለጋ ውጤቶች + የDeArrow አዶን አሳይ + በሕዝብ የተሰበሰቡ የቪዲዮ ርዕሶች ላይ የDeArrow አዶን ያሳያል + ቀለል ያለ ሁኔታ + DeArrow ተጠቃሚዎች ዋናው ርዕስ እንደሚወዱት ካ hlas አሰጡ፣ ከ DeArrow ርዕስ ይልቅ ዋናውን ርዕስ ያሳያል + ዋናውን ርዕስ ለማሳየት የሚያስፈልጉ ዝቅተኛ የቀለል ያሉ ድምጾች + ዋናውን ርዕስ ለማሳየት በአንድ ምድብ ውስጥ ለምሳሌ አስቂኝ ወይም የፈጠራ የሚያስፈልጉ የቀለል ያሉ ድምጾች ብዛት + የDeArrow ታምብኔሎች + ትክክለኛ ትምኒል ምስሎች + DeArrow & ዋና ዋና ትምኒል ምስሎች + DeArrow & Still captures + የማይንቀሳቀሱ ምስሎችን ያ captures ያደርጋል + "DeArrow የክሊክባይት ርዕሶችን እና থামብናይሎችን ይበልጥ ትክክለኛ እና አነጋጋሪ ባልሆኑ፣ በህዝብ የተሰበሰቡ አማራጮች ይተካቸዋል:: ርዕሶች በማህበረሰቡ ተፅፎባቸው ድምጽ ይሰጥባቸዋል፣ থামብናይሎች ደግሞ ከቪዲዮው የተወሰዱ ፍሬሞች ናቸው + +ቪዲዮዎች የሚጠየቁት በhashed የቪዲዮ መታወቂያ አጭር ቅድመ-መያዣ (prefix) በመጠቀም ነው፣ ስለዚህ ሰርቨሩ በትክክል የትኛውን ቪዲዮ እያዩ እንደሆነ ማወቅ አይችልም:: አንድ ቪዲዮ የDeArrow থামብናይል ከሌለው፣ ዋናው ወይም ቋሚ ምስል ይታያል" + የግንኙነት ስህተት ማስጠንቀቂያዎችን አሳይ + የDeArrow ሰርቨር በማይገኝበት ጊዜ ማሳወቂያ ያሳያል + የAPI መጨረሻ ነጥብ + የ DeArrow የጥፍር ምስል ካሽ ኢንዶፖይንት URL + የቪዲዮ ቋሚ ምስል ቀረጻዎች + የማይንቀሳቀሱ ምስሎች ከእያንዳንዱ ቪዲዮ መጀመሪያ / መካከለኛ / መጨረሻ ይወሰዳሉ። እነዚህ ምስሎች በYouTube ውስጥ የተገነቡ ናቸው እና ምንም ዓይነት የውጭ API ጥቅም ላይ አልዋለም + የቪዲዮ ፍሬሙን ከዚህ ያውጡ + መጀመሪያ + መካከለኛ + መጨረሻ + DeArrow ለጊዜው አይገኝም (የሁኔታ ኮድ: %s) + DeArrow ለጊዜው አይдоступም + የድሮውን የፕሌየር ቁልፍ ስታይል መልስ + ደመቅ ያሉትን የቪዲዮ ማጫወቻ መቆጣጠሪያዎች ወደ ቀድሞው ቀጭን የእይታ ስታይል ይመልሳል + የመሣሪያ መጠንን አስመስል + ይህንን ማብራት ከፍተኛ የቪዲዮ ጥራቶችን ሊያገኙ ያስችላል + ይህንን ማብራት የቪዲዮ ንባብ መቆራረጥን፣ የባትሪ ዕድሜ መቀነስን እና ያልታወቁ የጎንዮሽ ተፅዕኖዎችን ሊያስከትል ይችላል + ሃፕቲክ ፊድባክ + የመሣሪያ ንዝረትን የሚያነቃቁትን መስተጋብሮች ይምረጡ + የምዕራፎችን ሃፕቲክ አጥፋ + ትክክለኛ ፍለጋ ሃፕቲክን አጥፋ + የፍለጋ ውድግረትን ሃፕቲክ አጥፋ + የመንካት እና የመያዝ ሃፕቲክስን አጥፋ + የዙም ሃፕቲክን አጥፋ + የአካውንት መግቢያ ዝርዝሮችን በቅርቡ ቀይረዋል፣ እንግዲያውስ MicroGን ያጥፉ እና እንደገና ይጫኑት + የሊንክ ማስተላለፊያዎችን ይዝለሉ + የክትትል ሊንኩን (youtube.com/redirect) በማለፍ ዋናውን ሊንክ በቀጥታ ይከፍታል + ሊንኮችን በብราวዘር ክፈት + ሊንኮችን በ应用程序 ውስጥ ባለው ብሮውዘር ፋንታ በውጫዊ ብሮውዘር ይከፍታል + የድግግሞሽ በፈር መጠን + መደበኛ + ዝቅተኛ + መካከለኛ + ከፍተኛ + የቪዲዮ መዘግየት + እንደ ብሉቱዝ የጆሮ ማዳመጫዎች ካሉ የድምፅ መዘግየቶች ጋር እንዲጣጣም ቪዲዮውን ያዘገያል (0-5000 ሚሊሰከንድ) + በብሉቱዝ ኦዲዮ ብቻ አዘግይ + የስልኩ ስፒከር ከድምፅ ጋር እንዲጣጣም ብሉቱዝ ኦዲዮ ሲገናኝ ብቻ ቪዲዮውን ያዘገያል + የቪዲዮ መዘግየት ቁልፍን አሳይ + መዘግየቱን ወደ 0 ሚሊሰከንድ ለመቀየር ተጫን አድርገህ ያዝ + የቪዲዮ መዘግየት ዝርዝርን አሳይ + መዘግየት በሚሊሰከንድ + ራስ-ሰር + የቪዲዮ ጥራትን አስታውስ + የመረጡትን የጥራት ምርጫ ወደፊት ለሚታዩ ሁሉም ቪዲዮዎች ይጠቀማል + የጥራት ለውጥ ማሳወቂያዎች + አዲስ መደበኛ የጥራት ደረጃ ሲቀመጥ ማሳወቂያ ያሳያል + በWi-Fi ኔትወርክ ላይ ያለ የቪዲዮ ጥራት መነሻ + በሞባይል ኔትወርክ ላይ ያለ የቪዲዮ ጥራት መነሻ + የ Shorts ጥራትን አስታውስ + የመረጡትን የጥራት ምርጫ ለወደፊት ለሁሉም Shorts ይጠቀማል + በWi-Fi ኔትወርክ ላይ ያለ የShorts ጥራት መነሻ + በሞባይል ኔትወርክ ላይ ያለ የShorts ጥራት መነሻ + የመደበኛው የሞባይል ጥራት ወደ: %s ተቀይሯል + የመደበኛው የWi-Fi ጥራት ወደ: %s ተቀይሯል + የ Shorts የሞባይል ጥራት ወደ %s ተቀይሯል + የ Shorts የWi-Fi ጥራት ወደ %s ተቀይሯል + የማጫወቻ ፍጥነት ቁልፍን አሳይ + ፍጥነትን ወደ መደበኛው ለመመለስ ተጭነው ይያዙ + የጥራት ቁልፍን አሳይ + ጥራትን ወደ መደበኛው ለመመለስ ተጭነው ይያዙ + በጥራት ቁልፍ ላይ রেজሎሽንን አሳይ + እንደ FHD ካሉ ስያሜዎች ይልቅ እንደ 1080 ያሉ የረዞሉሽን መጠኖችን ያሳያል + ብጁ የቪዲዮ ማጫወቻ ፍጥነት ሜኑ + የድሮውን የማጫወቻ ፍጥነት ሜኑ ወደ እናንተ መልሱ + ብጁ የይጫወቱ ፍጥነትዎች + የሚመርጡትን የማጫወቻ ፍጥነት አማራጭ ያስገቡ + ብጁ የቪዲዮ ማጫወቻ ፍጥነቶች ከ %s በታች መሆን አለባቸው + የተሳሳቱ ብጁ የይጫወቱ ፍጥነትዎች + ራስ-ሰር + የማጫወቻ ፍጥነትን ተጭነው ይያዙ + ስክሪኑን ተጭነው በሚይዙበት ጊዜ እንዲሠራ የሚያደርገውን የፍጥነት መጠን ይወስኑ፤ ለማሰረዝ 0 ያድርጉ + የማጫወቻ ፍጥነትን አስታውስ + የመረጡትን የፍጥነት ምርጫ ለቀጣይ ቪዲዮዎች ሁሉ ይጠቀማል + የፍጥነት ለውጥ ማሳወቂያዎች + አዲስ መደበኛ የቪዲዮ ፍጥነት ሲቀመጥ ማሳወቂያ ያሳያል + መደበኛ የይጫወቱ ፍጥነት + በ Shorts ላይ የሚኖር ነባር የቪዲዮ ማጫወቻ ፍጥነት + በ Shorts ላይ የplayback ፍጥነትን አስታውስ + የመረጡትን የ Shorts ፍጥነት ለሚቀጥሉት ሁሉም Shorts ይጠቀማል። ከተጠፋ፣ የ Shorts ተጫዋቹ ከተዘጋ በኋላ ፍጥነቱ ወደ መደበኛው ይመለሳል + በ Shorts ሜኑ ውስጥ የPlayback ፍጥነትን አሳይ + ለ Shorts ማጫወቻ ሜኑ የትጫጫ ስርጭት ፍጥነትን ይጨምራል + የትጫጫ ስርጭት ፍጥነት · %s + የመደበኛው ማጫወቻ ፍጥነት ወደ %s ተቀይሯል + ለሙዚቃ መደበኛ ፍጥነትን ይጠቀሙ + የሙዚቃ ቪዲዮዎችን ሁልጊዜ በ 1.0x ፍጥነት ያጫውቱ + የቻናል ፍጥነት ነጭ ዝርዝር + በነጭ ዝርዝር ውስጥ ካሉ ቻናሎች የሚመጡ ቪዲዮዎች ሁልጊዜ በ 1.0x ፍጥነት ይጫወታሉ + ቻናሉን ወደ ፍጥነት ነጭ ዝርዝር አክል + ቻናሉን ከፍጥነት ነጭ ዝርዝር አስወግድ + የድምፅ ፒች መቆጣጠሪያዎችን አብራ + የመደበኛ የማጫወቻ ኦዲዮ ፒች + መደበኛው የኦዲዮ ፒች ወደ %s ተቀይሯል + የኦዲዮ ጊዜ ማሳሳብ + ፍጥነትን እና ፒችን ለብቻቸው ለማስተካከል ያስችላል፣ ይህም የኦዲዮ ጥራት መዛባትን ሊያስከትል ይችላል + የቀጥታ ስርጭት ማጫወቻ ቦታን አስታውስ + ለቀጣይ የቀጥታ ስርጭቶች፣ ወደ አሁኑ የቀጥታ ቦታ ከመሄድ ይልቅ የማጫወቻ ቦታዎን ያስታውሳል እንዲሁም ይቀጥላል + ይህ ባህሪ የ 24/7 የቀጥታ ስርጭቶችን እና ሌሎች \'ሁልጊዜ ክፍት\' የሆኑ ስርጭቶችን አይሰራም + HDR ቪዲዮን አጥፋ + HDR ቪዲዮን አስገድድ + መተግበሪያው ይህንን መሳሪያ እንደ HDR ተጠቃሚ አድርጎ እንዲያውቀው ያስገድዳል + "የ HDR decoding የሚሰራ ነገር ግን YouTube HDR የማይሰጥ ከሆነ ይህንን ይጠቀሙ፣ ለምሳሌ HLG የሚደብቁ አንዳንድ የ HyperOS መሳሪያዎች። + +ውስንነቶች፦ +• ስክሪኑ HDR ማሳየት ካልቻለ ቀለማት የተሳሳቱ ሊመስሉ ይችላሉ +• ዲኮደሩ HDR መያዝ ካልቻለ የቪዲዮ ማጫወት ሊቋረጥ ይችላል +• HDR አሁንም በ spoof video streams client ላይ የተመሰረተ ነው" + የቪዲዮ ኮዴክ + መደበኛ + AVCን ለማስገደድ፣ \'የቪዲዮ ስትሪሞችን አስመስል\' የሚለውን ወደ \'Android Studio\'፣ \'Android VR\' ወይም \'visionOS\' ይቀይሩ + "VP9 +• የባትሪ ዕድሜን ሊያሻሽል እና የቪዲዮ ማጫወቻ መቆራረጥን ሊያስተካክል ይችላል +• ከAV1 ይልቅ ብዙ የኢንተርኔት ዳታ ይጠቀማል + +AVC (H.264) +• 'Android Studio'፣ 'Android VR' ወይም 'visionOS' የቪዲዮ ስትሪም ማስመስያ ክሊየንትን ይጠይቃል +• የባትሪ ዕድሜን የበለጠ ሊያሻሽል ይችላል +• በድሮ መሳሪያዎች ላይ የጠፉ የቪዲዮ ስ resolutionዎችን ሊመልስ ይችላል +• ከፍተኛው resolution 1080p ነው +• HDR ቪዲዮዎች AVCን ላይጠቀሙ ይችላሉ +• አንዳንድ መሳሪያዎች AVCን ማስገደድ አይችሉም" + የላቀ የጥራት ዝርዝርን ክፈት + የጥራት ምርጫ ዝርዝሩን ዘልሎ በቀጥታ የላቀ የጥራት ዝርዝሩን ይከፍታል + የPremium ጥራት አማራጮችን ደብቅ + ለቪዲዮ ጥራት ቅድሚያ ስጥ + ከኮዴክ ብቃት ይልቅ ከፍተኛ ለሆነው የቪዲዮ ጥራት ቅድሚያ ይሰጣል + ይህ ቅንብር ከተጠፋ፣ የቆዩ ቪዲዮዎች ምንም ዓይነት የጥራት አማራጮችን ላያሳዩ ይችላሉ + \'Android VR\' / visionOS AV1ን ይፍቀዱ + የ \'Android VR\' / visionOS ደንበኛን በሚጠቀሙበት ጊዜ የሶፍትዌር AV1 ዲኮዲንግን ይፈቅዳል + "ይህንን ቅንብር ማብራት የሶፍትዌር AV1 ዲኮዲንግን ሊጠቀም ይችላል። + +በAV1 የቪዲዮ ንባብ ሊቆራረጥ ወይም ፍሬሞች ሊጠፉ ይችላሉ" + የማታለል የጎንዮሽ ተፅእኖዎች + • በ1 ደቂቃ ላይ መልሶ ማጫወት ሊቋረጥ ይችላል + • የ \'Audio Track\' ሜኑ የለም + • ምንም AV1 ቪዲዮ ኮዴክ የለም + • 360° VR immersive mode አይдоступም + • የተረጋጋ የድምፅ መጠን አይገኝም + • መልሶ ማጫወት 1 ሰከንድ ቀድሞ ሊያበቃ ይችላል + • deobfuscation ካልተሳካ፣ ማጫወት ሊቋረጥ ይችላል + • ኦሪጅናል ኦዲዮን አስገድድ አይገኝም + • ወደ Android VR መግባት ያስፈልጋል + • ጥራቱ እስከ 720p ድረስ ሊገደብ ይችላል + የተጫዋች JavaScript ስሪት + በ \'Stats for nerds\' ውስጥ ማስመሰልን ያሳዩ + የድሮውን የቪዲዮ ተግባር አሞሌ መልስ + "የቪዲዮ የድርጊት አዝራሮች የማይደበቁበትን ችግር ያስተካክላል + +ገደብ፡ ወደ አሮጌው ስታይል 'ተጨማሪ ቪዲዮዎች' ኦቨርሌ ይመለሳል" + የፕሌየር አዶ ስታይል + ራስ-ሰር + መደበኛ + ቀጭን + ደመቅ ያለ + ብጁ + የShorts አይኮን ስታይል + የተከታታይ ፕሮግራሞች መከታተያ + የመጫወቻ ዝርዝሮችን እንደ ተከታታይ ፕሮግራም ይከተሉ እና የደረሱበትን ቦታ ያስቀምጡ + የተከታታይ ፕሮግራሞች ክትትልን ያብሩ + በታሪክ ውስጥ የተከታታይ ገጽን እና በመጫወቻ ዝርዝሮች ውስጥ ተከታታዮችን የመከተል አማራጭን ይጨምራል + ተከታታይ ርዕሶችን በናቪጌሽን ባር ላይ አሳይ + የተከታታይ ፕሮግራሞች መከታተያ ቁልፍን አሳይ + የአሁኑን ቪዲዮ ተከታታይ ፕሮግራም ለመከተል ወይም ለመክፈት የሚያስችል የጫና ቁልፍን ይጨምራል + የተከታተሏቸውን ተከታታይ ፕሮግራሞች እድገት ይመዝግቡ + ለተቀመጡ ተከታታይ ፕሮግራሞች እና ምልክት ላደረጉባቸው ቦታዎች በዚህ ስልክ ላይ መልሶ ማጫወትን ይመዝግቡ + ሰርዝ + አስቀምጥ + መለያውን አስቀምጥ + የተከታታይ ስም + የተከታታይ አማራጮች + የቤተ-መጻሕፍት አማራጮች + ክፍሎችን አድስ + ስም ቀይር + የመጫወቻ ዝርዝሩን በ YouTube ክፈት + ከቤተ-መጻሕፍት አስወግድ + የመጫወቻ ዝርዝር ሊንክ ወይም መታወቂያ + ሁሉንም የአካባቢ እድገቶች አጥፋ + ታሪክን አጥፋ + ሁሉንም የአካባቢ እድገቶች አጥፋ? + ይህ ለእያንዳንዱ ቪዲዮ ያሉትን ቦታዎች፣ የተመለከቱ የበላይነት ምርጫዎች እና የይቀጥል ምርጫዎችን ያጠፋል። የተቀመጡ ተከታታይ ቪዲዮዎች እና ካታሎጎቻቸው ይቀራሉ። መዝገብ ማድረጊያ ከተበራ አዲስ የንባብ መረጃ ይመዘገባል + እንደተመለከተ ምልክት አድርግ + እንደማልተመለከተ ምልክት አድርግ + ራስ-ሰር ሁኔታን ተጠቀም + ከ 0:00 ጀምሮ እንደገና ጀምር + ከ 0:00 ጀምሮ እንደገና አጫውት + ሁኔታው ተቀምጧል + መቀመጡን እንደገና ሞክር + የቀድሞዎቹ 100 + ቀጣዮቹ 100 + የተቀመጠውን ቪዲዮ ክፈት + የአካባቢው ሂደት ተሰርዟል + ተከታታዮችዎን ለማየት ከእርስዎ ታሪክን ይክፈቱ + የተመለከቱት ታሪክ + ተከታታይ ቪዲዮዎች + የተቀመጡ ተከታታይ ቪዲዮዎች እየተጫኑ ነው… + ክፍሎቹን ለመከታተል የቪዲዮ ዝርዝርን ይከተሉ + የብሌይሊስት ምልክት + ክፍሎች እየተحدثና ነው… + ሁሉም ተከታታይ ትዕይንቶች + ይታየዋል + ክፍሎች እየተጫኑ ነው… + ተጨማሪ አማራጮች + "ለተቀመጡ ተከታታዮች እና ምልክት የተደረገባቸው ቪዲዮዎች ብቻ የሂደት መዝገብ ይያዙ + +ይህ ላይብረሪ በዚህ መሳሪያ ላይ ይቀመጣል እንዲሁም በYouTube አካውንቶች መካከል ይጋራል። የYouTube የታሪክ ማቆሚያ እና ማጽጃ ቁጥጥሮች ይህንን አይለውጡትም። ለማጥፋት Series $\rightarrow$ More $\rightarrow$ Clear local progress የሚለውን ይጠቀሙ + +በማንነቱ የማይታወቅ incognito ሁነታ ላይ፣ የአካውንት ማንነት በማይገኝበት ጊዜ ወይም ወደ ሌላ አካውንት ሲቀይሩ አውቶማቲክ መዝገብ መያዝ ይቆማል። በዚያ አካውንት ለመጠቀም ክትትልን እንደገና ያብሩ" + በማንነቱ የማይታወቅ ሁነታ ላይ ወይም YouTube የአሁኑን አካውንት ሁኔታ ከመጫኑ በፊት ክትትልን ማብራት አይቻልም። ወደ ዋናው ገጽ ተመልሰው እንደገና ይሞክሩ። መግባት አያስፈልግም + የአካባቢ ክትትልን አግብር + የሂደት መዝገብ ጠፍቷል + በ %1$s ይቀጥሉ + %1$s / %2$s የታዩ ክፍሎች + ክፍል %1$s · %2$s + ክፍሎች %1$s-%2$s ከ %3$s + ክፍሎችን ለመጫን ለማደስ ይሞክሩ። የግል የቪዲዮ ዝርዝሮች የአካውንት ፈቃድ ያስፈልጋቸዋል፤ ምልክታቸው አሁንም በYouTube ውስጥ ይከፈታል + YouTube ይህንን ክፍል መክፈት አልቻለም + YouTube ይህንን የዘፈን ዝርዝር መክፈት አልቻለም + ክፍል ይምረጡ + እስከ ወቅታዊው ደርሷል + ሊታዩ የሚችሉ ክፍሎች የሉም + የተቀመጠ ቪዲዮ · %1$s + ተከታታሊውን ተከተል + ተከታታሊውን እይ + ቀጥል + ከዚህ ጀምር + የመጀመሪያው ነጥብ ተቀምጧል + እስከዚህ ድረስ ተመልክቻለሁ + ክፍሎች እንደተመለከቱ ተሰምረዋል + ቀይር + አግብ + {one, 1 አዲስ ክፍል, other, {count} አዳዲስ ክፍሎች} + %1$s አዳዲስ ክፍሎች + በዚህ መሳሪያ ላይ ሂደትን መዝግብ + "የተከታተሏቸው ተከታታይ ክፍሎች እና የደረሱበት ደረጃ በዚህ መሳሪያ ላይ ይቀመራሉ፣ እንዲሁም በYouTube አካውንቶች መካከል ይጋራሉ። የYouTube ታሪክ ቁጥጥሮች እነዚህን አይለውጡም። የደረሱበትን ደረጃ ከተከታታይ ክፍሎች ማውጫ ውስጥ ያጽዱ + +አውቶማቲክ ቀረጻ እንደ ምርጫዎ ነው፤ በኢንኮግኒቶ ሞድ ወይም አካውንቱ ሲቀየር ይቆማል" + ክፍሎችን መጫን አልተቻለም። የዘፈን ዝርዝሩን እንደ ምልክት ማስቀመጥ ይችላሉ + ያልተቀመጡ ለውጦች በጣም ብዙ ናቸው። በመጀመሪያ እንደገና ለማስቀመጥ ይሞክሩ + ማስቀመጥ ላይ ስራ እየተከናወነ ነው። እንደገና ይሞክሩ + ክፍሎችን መጫን ላይ ስራ እየተከናወነ ነው። እንደገና ይሞክሩ + ማከማቻው ዝግጁ አይደለም። የተቀመጠው መረጃዎ ተጠብቋል + ለውጦች ሊቀመጡ አልቻሉም። YouTubeን ከመዝጋትዎ በፊት እንደገና ለመቀመጥ ይሞክሩ + ይህ ተከታታይ ቀድሞውኑ ተቀምጧል፣ ወይም የዝርዝር ማውጫው ተለውጧል። እንደገና ይክፈቱት + ይህ ተከታታይ ተለውጧል ወይም ተሰርዟል። ላይብረሪውን እንደገና ይክፈቱ + የዝርዝር ማውጫው ተለውጧል። ክፍሉን እንደገና ይምረጡ + ይህ ክፍል ዝግጁ አይደለም። ሌላ ይምረጡ + የተከታታይ ስም ያስገቡ + የYouTube የዝርዝር ማውጫ ሊንክ ወይም የዝርዝር ማውጫ መለያ ያስገቡ + እንደገና ለማደስ እባክዎ ትንሽ ይጠብቁ + ክፍሎች ሊጫኑ አልቻሉም። ቆይተው እንደገና ይሞክሩ + ለውጡ ሊጠናቀቅ አልቻለም። እንደገና ይሞክሩ + የቀጠለበት ሁኔታ ሊረጋገጥ አልቻለም። ማጫወቻውን ያረጋግጡ እና እንደገና ይሞክሩ + ተከታታሊዎችን እየተከተሉ ነው + ይህ ቪዲዮ በዚህ የቪዲዮ ዝርዝር ውስጥ የለም + የመጫወቻ ዝርዝሮችን በመፈለግ ላይ… + የመጫወቻ ዝርዝር ሊንኩን እዚህ ይለጥፉ + የመጫወቻ ዝርዝር ማግኘት አልተቻለም + ምንም የሚገኙ ክፍሎች የሉም + የተመለከቱትን ደብቅ + የቀደሙ ክፍሎችን እንደተመለከቱ ምልክት አድርግ + በሚጀምረው፦ %1$s + የክፍሎች ቅደም ተከተል + የመጫወቻ ዝርዝር ቅደም ተከተል + የመጫወቻ ዝርዝር ቅደም ተከተልን ወደ ኋላ ቀይር + %1$s እይታዎች + %1$s እይታ + የተመለከቱትን ክፍሎች ምልክት ያድርጉ + ቢያንስ %1$s%% የተመለከተ እና ቢበዛ 90 ሰከንድ የቀረው፣ ወይም %2$s ሰከንዶች የቀሩት + ከሁለቱ ገደቦች አንዱ ሲሟላ አንድን ክፍል እንደተመለከቱ በራስ-ሰር ምልክት ያድርጉ። የፐርሰንት ደንቡ ከ 90 ሰከንድ በላይ እንዳይቀር ይፈልጋል፣ ስለዚህ የረጅም ክፍሎች መጨረሻ አይዘለሉም። የቀረው-ጊዜ ደንብ ከአንድ ደቂቃ በላይ ለሆኑ ቪዲዮዎች ተፈጻሚ ይሆናል፤ ለማሰናከያ 0 ያድርጉት። ለውጦቹ ወደፊት በሚደረጉ የሂደት ማሻሻያዎች ላይ ተፈጻሚ ይሆናሉ። በእጅ የተደረጉ የተመለከተ/ያልተመለከተ ምርጫዎች እንደቀጠሉ ይቆያሉ + የተመለከተው ፐርሰንት (1-100) + የቀሩ ሰከንዶች (0-300፤ 0 ያሰናክላል) + ከ %1$s እስከ %2$s ያለ ዋጋ ያስገቡ + የYouTube እድገትን ይጠቀሙ + በሌላ መሳሪያ ላይ ከተመለከቱ በኋላ የተከተሉትን ተከታታይ ፊልሞች ይቀጥሉ + ለሚከተሏቸው ተከታታይ ዝግጅቶች የተመለከቱበትን ሁኔታ እና የቀጠሉበትን ቦታዎች አሁን ካለው የYouTube አካውንት ያንብባል። የሚመሳሰሉ ክፍሎች ብቻ በዚህ ስልክ ላይ ይቀመጣሉ። ፍተሻዎች የሚከናወኑት የተከታታይ ዝግጅቶች ላይብረሪ ክፍት በሚሆንበት ጊዜ እና \"ቀጥል\" የሚለውን ከመጫንዎ በፊት ነው። በሌላ መሳሪያዎ ላይ የYouTube ታሪክ መብራት አለበት + የYouTube እድገት እየተረጋገጠ ነው… + የYouTube ሂደት ዝግጁ አይደለም። የተቀመጠው ሂደት ጥቅም ላይ እየዋለ ነው + አንዳንድ የYouTube ሂደቶች ዝግጁ አይደሉም። የተቀመጡ ቦታዎች ተጠብቀዋል + ቅጂ መያዝ እና መልሶ ማግኘት + የተከታታይ ፊልሞች ላይብረሪዎን ፋይል ላይ ያስቀምጡ ወይም የቅድመ ቅጂን መልሰው ያግኙ + የሚከተሏቸውን ተከታታይ ዝግጅቶች፣ የክፍል ቦታዎችን እና የተመለከቱባቸውን ምርጫዎች ያስቀምጣል። የቅጂ እና የYouTube ሂደት ፈቃዶች በዚህ ውስጥ አልተካተቱም + ቅጂውን ያስቀምጡ + ቅጂውን መልሰው ያግኙ + %1$s ተከታታይ ዝግጅቶችን ይጨምሩ። በላይብረሪዎ ውስጥ ያሉ %2$s ተከታታይ ዝግጅቶች ይቀመጣሉ። ነባር የቪዲዮ ሂደቶች ይቀመጣሉ + የተከታታይ ፊልሞች ቅጂ ተቀምጧል + %1$s ተከታታይ ዝግጅቶች ተመልሰዋል + ይህ ፋይል የሚደገፍ የሲሪስ ባክአፕ አይደለም፣ ወይም በጣም ትልቅ ነው። የእርስዎ ላይብረሪ አልተለወጠም + ባክአፑን ማስቀመጥ አልተቻለም። ሌላ ቦታ ይሞክሩ + ተከታታይ ተሰርዟል + ይህ ክፍል አሁን ከአንድ ጊዜ በላይ ይታያል። የትኛውን እንደሚቀጥሉ ይምረጡ + ያተረፉት ክፍል በዚህ ፕሌይሊስት ውስጥ የለም። የተቀመጠው ቦታዎ ተጠብቋል + በክፍል %1$s · %2$s ይቀጥሉ + ከታች ካሉት ክፍሎች ውስጥ ማንኛውንም መምረጥ ይችላሉ። የቀድሞ ክፍሎች አይለወጡም + ይህ ፕሌይሊስት ተለውጧል። የት እንደሚቀጥሉ ለመምረጥ ሲሪሱን ይክፈቱ + በዚህ አቅጣጫ ተጨማሪ ክፍሎች የሉም + %1$s ይቀጥሉ + ተከታታይ ይፈልጉ + ፍለጋውን አጽዳ + ምንም ተከታታይ አልተገኘም + ርዕስ የሌለው ክፍል + ያልተገኘ ክፍል + + የNiconico አስተያየቶች + የቀጥታ ውይይት እና የውይይት መልሶ ማጫወቻዎችን በቪዲዮው ላይ ተንሸራታች አስተያየቶች አድርጎ ያሳያል + በቪዲዮ ማጫወቻው ላይ የተደራራቢ ቁልፍ ያሳያል። አስተያየቶቹን ለማሳየት ወይም ለመደበቅ ይጫኑ፣ ለፈጣን ቅንብሮች ደግሞ ተጭነው ይያዙ + የአስተያየት የፊደል መጠን + የአስተያየቶች የጽሑፍ መጠን (12-40 sp) + የአስተያየት ግልጽነት + የአስተያየት ጽሑፎች እና የጀርባ ቀለሞች ግልጽነት + የጉዞ ቆይታ + አስተያየቶች ስክሪኑን ለማቋረጥ የሚወስዱት ሰከንዶች። አነስተኛ እሴቶች በፍጥነት ይንቀሳቀሳሉ + የአስተያየት ከፍተኛ ርዝመት + ከዚህ የበለጠ ቁምፊዎች ያላቸውን አስተያየቶች ይደብቃል። እያንዳንዱ ኢሞጂ እንደ አንድ ቁምፊ ይቆጠራል + ገደብ የለም diff --git a/patches/src/main/resources/addresources/values-ar-rSA/music/strings.xml b/patches/src/main/resources/addresources/values-ar-rSA/music/strings.xml index 1d54c36f49..e6841edb23 100644 --- a/patches/src/main/resources/addresources/values-ar-rSA/music/strings.xml +++ b/patches/src/main/resources/addresources/values-ar-rSA/music/strings.xml @@ -540,8 +540,29 @@ tظهر المسارات التي تم تنزيلها هنا" يعرض السطر الحالي على شاشة القفل وAndroid Auto والبلوتوث. يتطلب Android 11 أو أحدث إظهار الكلمات في المشغّل المصغّر يعرض السطر الحالي في المشغل المصغر، مع تحريك المسار إلى العنوان الفرعي - عند التشغيل: تكون الرومنة أسفل الكلمات والترجمة أعلاها. عند الإيقاف: تبقى الرومنة أعلى الكلمات والترجمة أسفلها. - تبديل ترتيب الفنان والعنوان في المشغل المصغر وواجهة \"MediaSession\". تشغيل يعني \"الفنان - العنوان\" وإطفاء يعني \"العنوان - الفنان\". لا يؤثر هذا الإجراء على البحث عن كلمات الأغاني + تنسيق عنوان الأغنية/الفنان + تنسيق المسار المعروض مع كلمات الأغاني في المشغل المصغر وMediaSession. {t} هو العنوان، و{a} هو الفنان و{v} هو الإصدار، مثل Acoustic، عندما يوفر مصدر كلمات الأغاني ذلك. النص الموجود بين نقطتين رأسيتين يظهر فقط عندما يكون لكل متغير بداخله قيمة؛ اكتب \\: لنقطة رأسية عادية. يتم عرض كل سطر بدوره، وينتقل السطر عند تغيير كلمات الأغاني + تنسيق سطر كلمات الأغاني + تنسيق سطر كلمات الأغاني في المشغل المصغر وMediaSession. {lrc} هو السطر و{n} هو عدد المرات التي يتم فيها غناؤه على التوالي. النص الموجود بين النقطتين الرأسيتين يظهر فقط عندما يتكرر السطر. استخدم \\: للنقطة الرأسية + يجب أن يحتوي التنسيق على {lrc} + قالب سطر الفنان وتوقيت الرسوم المتحركة + الحد الأدنى لوقت الشاشة (ملي ثانية) + الحد الأقصى لوقت الشاشة (ملي ثانية) + للرسوم المتحركة متعددة الأسطر فقط. احتفظ بكل إطار لمدة لا تقل عن الحد الأدنى من الوقت. عند الحد الأقصى، يتم تقديم سطر الفنان حتى لو ظلت كلمات الأغنية كما هي. + يجب أن يكون الحد الأدنى 0 أو أكبر. يجب أن يكون الحد الأقصى أكبر من 0 وبحد أدنى مساوٍ للحد الأدنى. + حدود طول السطر + حدود سطر الكلمات وسطر الفنان، ووزن الأحرف كاملة العرض + "حد سطر كلمات الأغاني +تقسيم الكلمات الطويلة إلى نصفين متوازنين. 0 لتعطيل التقسيم." + "حد أسطر الفنان +تقسيم تنسيق المسار المأهول إلى إطارات متناوبة. 0 لتعطيل التقسيم." + "وزن الأحرف كاملة العرض +ينطبق على كلا الحدين. عند 1.75، تُحسب أربعة أحرف كاملة العرض على أنها 7." + أدخل حدوداً بأرقام صحيحة من 0 إلى 10000 ووزناً للأحرف أكبر من 0 + فاصل موسيقي + يظهر في المشغل المصغر و MediaSession أثناء الفاصل الموسيقي. كل سطر يمثل إطار حركة واحداً. {t} هو العنوان، و {a} هو الفنان، و {lrc} هو آخر سطر من كلمات الأغنية + إزاحة التوقيت في المشغل المصغر و MediaSession + يحدد موعد ظهور السطر التالي في المشغل المصغر وMediaSession. استخدم قيمة سالبة إذا كانت الأسطر تظهر متأخرة، على سبيل المثال في شاشة القفل الفلاتر تعبير مرشّح الكلمات تعبير نمطي يُطبَّق على العناوين والفنانين والألبومات قبل البحث عن كلمات الأغاني وعند تقييم درجة تطابق عنوانين. اتركه فارغًا لتعطيل الاستخدامين معًا. الإعداد الافتراضي يزيل العبارات الزائدة الشائعة مثل \"Official Video\" و\"Remaster\" و\"【插曲】\" و\"- Topic\". @@ -549,6 +570,10 @@ tظهر المسارات التي تم تنزيلها هنا" تعبير نمطي يُطبَّق على نص الكلمات الأصلي فقط (وليس الترجمات أو الرومنة). اتركه فارغًا لتعطيله. علامات سطر المعلومات إذا تمت تصفية الكلمات بشكل غير صحيح إلى الأسفل، فاضبط هذا أو امسحه. […] اتركه فارغًا لتعطيله. + تكبير الحرف الأول + يجعل الحرف الأول من كل سطر في كلمات الأغنية حرفاً كبيراً + إزالة علامات الترقيم الختامية + يزيل النقاط والفواصل والنقطتين الرأسيتين والفاصلة المنقوطة في نهاية أسطر كلمات الأغاني. مع الإبقاء على علامات الاستفهام وعلامات التعجب وعلامات الحذف وعلامات الاقتباس والأقواس حجم النص حجم نص كلمات الأغنية إزاحة التوقيت العامة diff --git a/patches/src/main/resources/addresources/values-ar-rSA/youtube/strings.xml b/patches/src/main/resources/addresources/values-ar-rSA/youtube/strings.xml index a2119af3c9..85cc8eecab 100644 --- a/patches/src/main/resources/addresources/values-ar-rSA/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ar-rSA/youtube/strings.xml @@ -393,6 +393,9 @@ Second \"item\" text" إخفاء روابط التسوق إخفاء إعلانات الفيديو إخفاء عروض ترقية YouTube Premium + + + جارٍ التحميل… القناة الحالية: إضافة إلى القائمة المسموح بها @@ -409,6 +412,9 @@ Second \"item\" text" نتائج البحث في القناة لا توجد نتائج في هذه القناة تعذر البحث في هذه القناة + الفيديوهات + المنشورات + لا توجد منشورات في هذه القناة تطابق البحث مدى الصلة الأكثر مشاهدةً الأقل مشاهدةً @@ -494,6 +500,9 @@ Second \"item\" text" الفيديو الصوت كل الخيارات + إظهار التنزيل في قائمة Shorts + يضيف عنصر قائمة منبثقة لمُحمّل خارجي إلى مشغل Shorts + تنزيل الترجمة الصوتية قراءة نصوص ترجمة الفيديو بصوت عالٍ باستخدام تحويل النص إلى كلام على الجهاز، بالتزامن مع التشغيل @@ -1125,9 +1134,6 @@ Mistral Nemo هو الخيار الأرخص، بينما توفر Gemma 4 أعل "استعادة تخطيط المشغل القديم القيود: إذا تم إيقاف تشغيل 'استعادة شريط إجراءات الفيديو القديم'، فقد لا يتوفر عنصر 'المزيد من الفيديوهات'" - "قد يؤدي تمكين هذا الخيار إلى تعطل بعض ميزات تخطيط المشغل، مثل قسم التعليقات أو 'المزيد من الفيديوهات' المعروضة أسفل المشغل. - -لم يعد تخطيط الجهاز اللوحي مدعومًا من قبل YouTube، وهو معطل جزئيًا أو كليًا في إصدارات YouTube الحديثة. للحصول على تخطيط جهاز لوحي يعمل، قم بتعديل الإصدار YouTube 20.21.37" 21.11.48 - استعادة سلوك الترجمة القديم 20.39.41 - استعادة الحفظ القديم إلى قائمة التشغيل @@ -1630,4 +1636,17 @@ AVC (H.264) لم يتم العثور على سلسلات حلقة بدون عنوان حلقة غير متاحة + + تعليقات Niconico + عرض الدردشة المباشرة وإعادة تشغيل الدردشة كتعليقات تمر عبر الفيديو + يعرض زر متراكب في مشغل الفيديو. انقر لإظهار أو إخفاء التعليقات، واضغط مع الاستمرار للوصول إلى الإعدادات السريعة + حجم خط التعليقات + حجم نص التعليقات (12-40 sp) + شفافية التعليقات + شفافية نص التعليقات والخلفيات + مدة الانتقال + عدد الثواني التي تستغرقها التعليقات لعبور الشاشة. القيم الأقل تجعلها تتحرك بشكل أسرع + أقصى طول للتعليق + يخفي التعليقات التي تحتوي على عدد أحرف أكثر من هذا الحد. يتم احتساب كل رمز تعبيري كحرف واحد + بلا حدود diff --git a/patches/src/main/resources/addresources/values-as-rIN/youtube/strings.xml b/patches/src/main/resources/addresources/values-as-rIN/youtube/strings.xml index 9079108272..70ce03fc67 100644 --- a/patches/src/main/resources/addresources/values-as-rIN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-as-rIN/youtube/strings.xml @@ -51,6 +51,8 @@ Second \"item\" text" + + @@ -224,4 +226,5 @@ Second \"item\" text" + diff --git a/patches/src/main/resources/addresources/values-az-rAZ/music/strings.xml b/patches/src/main/resources/addresources/values-az-rAZ/music/strings.xml index 23760479fd..8a71045109 100644 --- a/patches/src/main/resources/addresources/values-az-rAZ/music/strings.xml +++ b/patches/src/main/resources/addresources/values-az-rAZ/music/strings.xml @@ -555,8 +555,29 @@ Fon səsləndirmə zamanı analiz sinxronlaşdırılmasına əmin olmaq üçün Cari sətri kilid ekranında, Android Auto-da və Bluetooth-da göstərir. Android 11 və ya daha yeni tələb olunur Kiçik Səsləndirici Sözlərini Göstər Cari sətri kiçik oynadıcıda göstərir, trek altyazıya keçirilir - İfaçı və Başlığın göstərilmə sırası - Kiçik səsləndirici və MediaSession-da İfaçı və Başlığın göstərilmə sırasını dəyişin. AÇIQ olduqda \"İfaçı - Mahnı adı\", QAPALI olduqda \"Mahnı adı - İfaçı\" göstərilir. Mahnı sözlərinin axtarışına təsir etmir + Mahnı başlığı/ifaçı formatı + Kiçik oynadıcı və MediaSession-da sözlərlə göstərilən trekin formatı. {t} başlıq, {a} ifaçı, {v} isə sözlər mənbəyi tərəfindən təqdim edildikdə məsələn, Akustik kimi versiyadır. İki qoşa nöqtə arasındakı mətn yalnız içindəki hər bir dəyişənin dəyəri olduqda göstərilir; sadə qoşa nöqtə üçün \\: yazın. Hər bir sətir növbə ilə göstərilir, sözlər dəyişdikcə növbətiyə keçir + Mahnı sözləri sətir formatı + Kiçik oynadıcı və MediaSession-da sətir sözlərinin formatı. {lrc} sətrdir, {n} isə onun ardıcıl olaraq neçə dəfə oxunduğudur. Qoşa nöqtələr arasındakı mətn yalnız sətir təkrarlandıqda göstərilir. Qoşa nöqtə üçün \\: istifadə et + Format {lrc} ehtiva etməlidir + İfaçı-sətir şablonu və animasiya müddəti + Ən az ekran vaxtı (ms) + Ən çox ekran vaxtı (ms) + Yalnız çox sətirli animasiyalar üçün. Hər bir kadrı ən azı minimum müddət saxla. Maksimumda isə sözlər eyni qalsa belə, ifaçı sətrini irəli apar. + Ən az 0 və ya daha çox olmalıdır. Ən çox 0-dan böyük və ən azı minimum olmalıdır. + Sətir uzunluğu limitləri + Söz və ifaçı sətri limitləri və tam enli simvolun ağırlığı + "Söz sətri limiti +Daha uzun sözləri balansı qorumaqla iki bərabər hissəyə böl. 0 bölünməni qapadır." + "İfaçı sətri limiti +Doldurulmuş trek formatını dövr edən kadrlara böl. 0 bölünməni qapadır." + "Tam enli simvol ağırlığı +Hər iki limitə tətbiq olunur. 1.75-də dörd tam enli simvol 7 kimi sayılır." + 0-dan 10000-dək tam ədəddən ibarət limitlər və 0-dan böyük simvol ağırlığı daxil et + İnstrumental fasilə + İnstrumental fasilə zamanı Kiçik oynadıcı və MediaSession-da göstərilir. Hər bir sətir bir animasiya kadrıdır. {t} başlıq, {a} ifaçı, {lrc} isə sonuncu sözlər sətridir + Kiçik oynadıcı və MediaSession vaxt sürüşməsi + Kiçik oynadıcı və MediaSession-da növbəti sətrin göstərilməsini sürüşdürür. Sətrin gec görünməsi halında məsələn, kilid ekranında mənfi dəyər istifadə et Filtrlər Metadata regex Mahnı sözləri axtarılmazdan əvvəl və iki başlığın uyğunluğu qiymətləndirilərkən başlıqlara, sənətçilərə və albomlara tətbiq edilən müntəzəm ifadə. Hər ikisini qapatmaq üçün boş qoy. İlkin seçim \"Official Video\", \"Remaster\", \"【插曲】\" və \"- Topic\" kimi ümumi qarışıqlıqları təmizləyir @@ -564,6 +585,10 @@ Fon səsləndirmə zamanı analiz sinxronlaşdırılmasına əmin olmaq üçün Yalnız orijinal mahnı sözləri mətninə tətbiq olunan müntəzəm ifadə (tərcümələrə və ya transkripsiyalara deyil). Qapatmaq üçün boş qoy Məlumat sətri nişanlayıcıları Mahnı sözləri yanlışlıqla aşağıya filtrlənərsə, bunu tənzimləyin və ya təmizləyin. Vergüllə ayrılmış nişanlayıcılar: tərkibində nişanlayıcı ardınca iki nöqtə olan sətirlər aşağıda müəllif məlumatı kimi göstərilir. Qapatmaq üçün boş qoy + İlk hərfi böyük hərflə yaz + Hər mahnı sözü sətrinin ilk hərfini böyük hərflə yazır + Sondakı durğu işarələrini sil + Mahnı sözləri sətirlərinin sonundakı nöqtə, vergül, qoşa nöqtə və nöqtəli vergülü silir. Sual işarələri, nida işarələri, üç nöqtə, dırnaqlar və mötərizələr saxlanılır Mətn ölçüsü Mahnı sözlərinin mətn ölçüsü Ümumi vaxt sürüşməsi diff --git a/patches/src/main/resources/addresources/values-az-rAZ/youtube/strings.xml b/patches/src/main/resources/addresources/values-az-rAZ/youtube/strings.xml index ece6eeb39c..30a6583ac6 100644 --- a/patches/src/main/resources/addresources/values-az-rAZ/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-az-rAZ/youtube/strings.xml @@ -393,6 +393,9 @@ Məhdudiyyətlər: Alış-veriş linklərini gizlət Video reklamlarını gizlət YouTube Premium reklamlarını gizlət + + + Yüklənir… Hazırkı kanal: Ağ siyahıya əlavə et @@ -409,6 +412,9 @@ Məhdudiyyətlər: Kanal axtarış nəticələri Bu kanalda heç nə tapılmadı Bu kanalda axtarış etmək olmadı + Videolar + Postlar + Bu kanalda uyğun post yoxdur Uyğunluq Ən çox baxılan Ən az baxılan @@ -494,6 +500,7 @@ Məhdudiyyətlər: Video Səs Bütün seçimlər + Yüklə Səs üstələyici tərcümə Cihazdakı mətni nitqə çevirməni istifadə edərək video titrlərini oynatma ilə sinxronlaşdırıb ucadan oxu @@ -1121,9 +1128,6 @@ Avtomobil tərtibatı "Köhnə oynadıcı tərtibatını bərpa edir Məhdudiyyət: 'Köhnə video fəaliyyət cizgisini bərpa et' qapadılıbsa, 'Daha çox video' örtüyü mövcud olmaya bilər" - "Bunu aktivləşdirmək oynadıcının aşağısında göstərilən şərhlər bölməsi və ya 'Daha çox video' kimi bəzi oynadıcı tərtibatı xüsusiyyətlərini poza bilər. - -Planşet tərtibatı artıq YouTube tərəfindən dəstəklənmir və son YouTube versiyalarında qismən və ya tamamilə pozulub. İşlək planşet tərtibatı üçün YouTube 20.21.37-ni yamaqla" 21.11.48 - Köhnə titrlərin davranışını bərpa et 20.39.41 - Köhnə pleylistdə saxlanı bərpa et @@ -1626,4 +1630,5 @@ Məhdudiyyət: Köhnə üslub üçün \"Daha çox video\" örtüyünə qayıdır Seriya tapılmadı Başlıqsız epizod Əlçatmaz epizod + diff --git a/patches/src/main/resources/addresources/values-be-rBY/music/strings.xml b/patches/src/main/resources/addresources/values-be-rBY/music/strings.xml index edeae42f1a..1da0942ff0 100644 --- a/patches/src/main/resources/addresources/values-be-rBY/music/strings.xml +++ b/patches/src/main/resources/addresources/values-be-rBY/music/strings.xml @@ -18,6 +18,7 @@ Second \"item\" text" + Адмяніць @@ -25,6 +26,8 @@ Second \"item\" text" + Закрыць + Выдаліць @@ -68,6 +71,7 @@ Second \"item\" text" + Адлюстраванне Аўтаматычна diff --git a/patches/src/main/resources/addresources/values-be-rBY/reddit/strings.xml b/patches/src/main/resources/addresources/values-be-rBY/reddit/strings.xml index cf7776fdaf..8d434d72d7 100644 --- a/patches/src/main/resources/addresources/values-be-rBY/reddit/strings.xml +++ b/patches/src/main/resources/addresources/values-be-rBY/reddit/strings.xml @@ -15,4 +15,9 @@ Second \"item\" text" --> - + + Звычайны + Выдаліць + Іконка праграмы + Звычайны + diff --git a/patches/src/main/resources/addresources/values-be-rBY/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-be-rBY/shared-youtube/strings.xml index 04e971536d..eec7826aee 100644 --- a/patches/src/main/resources/addresources/values-be-rBY/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-be-rBY/shared-youtube/strings.xml @@ -19,11 +19,14 @@ Second \"item\" text" Адключана + Колер + Манахромны Карыстальніцкае + Звычайны @@ -57,7 +60,9 @@ Second \"item\" text" + Звычайны + Звычайны diff --git a/patches/src/main/resources/addresources/values-be-rBY/shared/strings.xml b/patches/src/main/resources/addresources/values-be-rBY/shared/strings.xml index 4825bab01e..13d4744ca8 100644 --- a/patches/src/main/resources/addresources/values-be-rBY/shared/strings.xml +++ b/patches/src/main/resources/addresources/values-be-rBY/shared/strings.xml @@ -18,6 +18,7 @@ Second \"item\" text" + Звычайны Карыстальніцкае @@ -30,6 +31,7 @@ Second \"item\" text" Выкл. Укл. + Выдаліць diff --git a/patches/src/main/resources/addresources/values-be-rBY/youtube/strings.xml b/patches/src/main/resources/addresources/values-be-rBY/youtube/strings.xml index 1dee5d4760..87d55e999a 100644 --- a/patches/src/main/resources/addresources/values-be-rBY/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-be-rBY/youtube/strings.xml @@ -52,7 +52,10 @@ Second \"item\" text" + + + Выдаліць @@ -82,6 +85,7 @@ Second \"item\" text" Звычайны + Выдаліць @@ -172,6 +176,7 @@ Second \"item\" text" + Звычайны @@ -194,8 +199,10 @@ Second \"item\" text" + Звычайны + Белы @@ -214,6 +221,7 @@ Second \"item\" text" + Звычайны @@ -223,6 +231,7 @@ Second \"item\" text" + Звычайны @@ -233,6 +242,9 @@ Second \"item\" text" Аўтаматычна + Звычайны Карыстальніцкае + Адмяніць + diff --git a/patches/src/main/resources/addresources/values-bg-rBG/music/strings.xml b/patches/src/main/resources/addresources/values-bg-rBG/music/strings.xml index 30d03f2b2c..ccb69b6829 100644 --- a/patches/src/main/resources/addresources/values-bg-rBG/music/strings.xml +++ b/patches/src/main/resources/addresources/values-bg-rBG/music/strings.xml @@ -541,8 +541,29 @@ SABR възпроизвеждане (имитирането на видео по Показва текущия ред на екрана за заключване, Android Auto и Bluetooth. Изисква Android 11 или по-нова версия Показване на текстовете в миниплейъра. Показва текущия ред в мини плеъра, като парчето се премества в субтитрите - Ред на показване на изпълнител и заглавие - Превключва реда на показване на изпълнителя и заглавието в миниплейъра и MediaSession. ВКЛ. „Изпълнител - Заглавие“, а ИЗКЛ. „Заглавие - Изпълнител“. Не засяга търсенето на текстове. + Формат на заглавието/изпълнителя на песента + Формат на песента, показана с текстовете в миниплеера и MediaSession. {t} е заглавието, {a} е изпълнителят и {v} е версията, например Acoustic, когато източникът на текстовете предоставя такава. Текстът между две двоеточки се показва само когато всяка променлива в него има стойност; напишете \\: за обикновено двоеточие. Всеки ред се показва поредно, като се сменя при всяка промяна на текста + Формат на реда с текст + Формат на реда с текст в миниплеера и MediaSession. {lrc} е редът, а {n} е колко пъти подред се пее той. Текстът между двоеточките се показва само когато редът се повтаря. Използвайте \\: за двоеточие + Форматът трябва да съдържа {lrc} + Шаблон на реда с изпълнителя и времеви интервали на анимацията + Минимално време на екрана (ms) + Максимално време на екрана (ms) + Само за многоредни анимации. Запазете всеки кадър за поне минималното време. При достигане на максималното, превъртете реда на изпълнителя, дори текстът на песента да остане същият. + Минималната стойност трябва да бъде 0 или по-голяма. Максималната трябва да бъде по-голяма от 0 и поне колкото минималната. + Ограничения за дължина на реда + Ограничения за редовете на текста и изпълнителя и тегло на пълноширинни символи + "Ограничение за реда на текста +Разделете по-дългите текстове на балансирани половини. 0 изключва разделянето." + "Лимит на редовете на изпълнителя +Разделя попълнения формат на пистата на циклиращи кадри. 0 изключва разделянето." + "Тегло на знаци с пълна ширина +Важи и за двете ограничения. При 1.75, четири знака с пълна ширина се броят като 7." + Въведете цели числа за лимитите от 0 до 10000 и тегло на знака, по-голямо от 0 + Инструментална пауза + Показва се в миниплеера и MediaSession по време на инструментална пауза. Всеки ред е един анимационен кадър. {t} е заглавието, {a} е изпълнителят, а {lrc} е последният ред от текста + Времево отместване на миниплеера и MediaSession + Премества кога следващият ред се показва в минимизирания плеър и MediaSession. Използвайте отрицателна стойност, ако редовете се появяват с закъснение, например на екрана за заключване Филтри Регулярен израз за метаданни Регулярен израз, прилаган към заглавия, изпълнители и албуми преди търсене на текстове на песни и при оценяване дали две заглавия съвпадат. Оставете празно, за да деактивирате и двете. По подразбиране премахва често срещаните ненужни елементи като „Official Video“, „Remaster“, „【插曲】“ и „- Topic“. @@ -550,6 +571,10 @@ SABR възпроизвеждане (имитирането на видео по Регулярен израз, прилаган само към оригиналния текст (не към преводите или романизациите). Оставете полето празно, за да деактивирате. Маркери за информационни редове Ако редовете на текста неправилно се филтрират към долната част, коригирайте това или го изчистете. Маркери, разделени със запетая: редовете, съдържащи маркер, последван от двоеточие, се показват като информация за авторство в долната част. Оставете празно, за да деактивирате. + Заглавна първа буква + Заглавна първа буква на всеки ред от текста на песента + Премахване на крайен пунктуационен знак + Премахва точки, запетаи, двоеточия и точка и запетаи в края на редовете от текста на песента. Въпросителните знаци, удивителните знаци, многоточията, кавичките и скобите се запазват Размер на текста Размер на текста на текста на песните Глобално отместване на времето diff --git a/patches/src/main/resources/addresources/values-bg-rBG/youtube/strings.xml b/patches/src/main/resources/addresources/values-bg-rBG/youtube/strings.xml index 937215b006..3b7b633843 100644 --- a/patches/src/main/resources/addresources/values-bg-rBG/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-bg-rBG/youtube/strings.xml @@ -393,6 +393,9 @@ Second \"item\" text" Скрийте връзки Скрийте общни видеореклами Скриване на YouTube Premium предложения + + + Зарежда се… Текущ канал: Добавяне към списъка с разрешени канали @@ -409,6 +412,9 @@ Second \"item\" text" Резултати от търсенето на канали Нищо не е намерено в този канал Неуспешно търсене в този канал + Видеа + Публикации + Няма публикации в този канал, които съвпадат Уместност Най-гледани Най-малко гледани @@ -442,6 +448,7 @@ Second \"item\" text" Изтегляне на изображение Изтегляне на %s изображения… Изтегляне на изображение… + %1$s изображения са запазени в %2$s Изображението е запазено в %s Изображението не можа да бъде изтеглено В този пост не е намерено изображение @@ -493,6 +500,9 @@ Second \"item\" text" Видео Аудио Всички опции + Показване на изтегляне в менюто на Shorts + Добавя елемент за външен инструмент за изтегляне в изскачащото меню на плейъра за Shorts + Изтегляне Превод на глас зад кадър Четете надписите на видеото на глас, използвайки вграден текстов говор, синхронизиран с възпроизвеждането @@ -1120,9 +1130,6 @@ Second \"item\" text" "Възстановява стария вид на плейъра Ограничение: Ако „Възстановяване на старата лента за действия във видеоклипа“ е изключено, покритието „Още видеоклипове“ може да не е достъпно" - "Активирането на това може да наруши някои функции на оформянето на плейъра, като раздела с коментари или \"Още видеоклипове\", показани под плейъра. - -Таблет оформянето вече не се поддържа от YouTube и е частично или напълно счупено в последните версии на YouTube. За работещо таблет оформяне, пачнете YouTube 20.21.37" 21.11.48 - Възстановяване на старото поведение на субтитрите 20.39.41 - Възстанови старите запазвания в списъка @@ -1625,4 +1632,17 @@ AVC (H.264) Няма намерени поредици Епизод без заглавие Недостъпен епизод + + Коментари от Niconico + Показване на чата на живо и повтореното възпроизвеждане на чата като коментари, които се движат през видеото + Показва overlay бутон във видеоплейъра. Докоснете, за да покажете или скриете коментарите, докоснете и задръжте за бързи настройки + Размер на шрифта на коментарите + Размер на текста на коментарите (12-40 sp) + Прозрачност на коментарите + Прозрачност на текста на коментарите и фоновете + Времетраене на преминаване + Колко секунди отнема на коментарите да пресекат екрана. По-ниските стойности се движат по-бързо + Максимална дължина на коментара + Скрива коментари с повече знаци от посочения брой. Всеки емоджи се брои за един знак + Без ограничение diff --git a/patches/src/main/resources/addresources/values-bn-rBD/music/strings.xml b/patches/src/main/resources/addresources/values-bn-rBD/music/strings.xml index 5f12d93abd..96b85dd144 100644 --- a/patches/src/main/resources/addresources/values-bn-rBD/music/strings.xml +++ b/patches/src/main/resources/addresources/values-bn-rBD/music/strings.xml @@ -550,8 +550,29 @@ SABR প্লেব্যাক (ভিডিও স্ট্রিম স্ আপাত দৃশ্যমান লক স্ক্রিন, অ্যান্ড্রয়েড অটো এবং ব্লুটুথ চলমান লিরিক্স দেখায়। অ্যান্ড্রয়েড ১১ বা তার পরবর্তী সংস্করণ লাগবে মিনিপ্লেয়ার লিরিক্স দেখান মিনিপ্লেয়ারে চলমান লিরিক্স দেখায়, ট্র্যাকটি সাবটাইটেলে সরানো হয় - শিল্পী ও শিরোনাম প্রদর্শন […] ON \"Artist - Title\" এবং OFF \"Title - Artist\" লিরিক্স অনুসন্ধানকে প্রভাবিত করে না। - মিনিপ্লেয়ার এবং MediaSession-এ শিল্পী ও শিরোনামের প্রদর্শনের ক্রম টগল করুন। ON থাকলে \"শিল্পী - শিরোনাম\" এবং OFF থাকলে \"শিরোনাম - শিল্পী\"। এটি লিরিক্স অনুসন্ধানে প্রভাব ফেলে না + গানের শিরোনাম/শিল্পী ফরম্যাট + মিনিপ্লেয়ার এবং MediaSession-এ লিরিক্সের সাথে দেখানো ট্র্যাকের ফরম্যাট। {t} হলো শিরোনাম, {a} হলো শিল্পী এবং {v} হলো ভার্সন, যেমন Acoustic, যখন লিরিক্স সোর্স এটি প্রদান করে। দুটি কোলনের মধ্যবর্তী টেক্সট শুধুমাত্র তখনই দেখানো হয় যখন এর ভিতরের প্রতিটি ভেরিয়েবলের মান থাকে; সাধারণ কোলনের জন্য \\: লিখুন। প্রতিটি লাইন পর্যায়ক্রমে দেখানো হয়, লিরিক্স পরিবর্তনের সাথে সাথে পরবর্তী লাইনে চলে যায় + লিরিক্স লাইনের ফরম্যাট + মিনিপ্লেয়ার এবং MediaSession-এ লিরিক্স লাইনের ফরম্যাট। {lrc} হলো লাইন এবং {n} হলো এটি পরপর কতবার গাওয়া হয়েছে। কোলনের মধ্যবর্তী টেক্সট শুধুমাত্র তখনই দেখানো হয় যখন লাইনটি পুনরাবৃত্তি হয়। কোলনের জন্য \\: ব্যবহার করুন + ফরম্যাটে অবশ্যই {lrc} থাকতে হবে + আর্টিস্ট-লাইন টেমপ্লেট এবং অ্যানিমেশন টাইমিং + নূন্যতম স্ক্রিন টাইম (ms) + সর্বোচ্চ স্ক্রিন টাইম (ms) + শুধুমাত্র মাল্টিলাইন অ্যানিমেশনের জন্য। প্রতিটি ফ্রেম অন্তত নূন্যতম সময়ের জন্য রাখুন। সর্বোচ্চ সময়ে, লিরিক্স একই থাকলেও আর্টিস্ট লাইনটি এগিয়ে যাবে। + নূন্যতম ০ বা তার বেশি হতে হবে। সর্বোচ্চ ০-এর বেশি এবং অন্তত নূন্যতম সময়ের সমান হতে হবে। + লাইনের দৈর্ঘ্যের সীমা + লিরিক্স এবং আর্টিস্ট লাইনের সীমা, এবং ফুল-উইডথ ক্যারেক্টার ওয়েট + "লিরিক্স লাইনের সীমা +দীর্ঘ লিরিক্সগুলিকে ভারসাম্যপূর্ণ অর্ধে বিভক্ত করুন। ০ দিলে স্প্লিটিং নিষ্ক্রিয় হবে।" + "শিল্পী লাইনের সীমা +পূরণ করা ট্র্যাক ফরম্যাটটিকে সাইক্লিং ফ্রেমে বিভক্ত করুন। ০ বিভক্ত করা বন্ধ করে।" + "ফুল-উইথ ক্যারেক্টার ওয়েট +উভয় সীমার ক্ষেত্রেই প্রযোজ্য। ১.৭৫-এ, চারটি ফুল-উইথ ক্যারেক্টার ৭ হিসেবে গণ্য হবে।" + ০ থেকে ১০০০০ পর্যন্ত পূর্ণসংখ্যার সীমা এবং ০-এর বেশি ক্যারেক্টার ওয়েট লিখুন + ইনস্ট্রুমেন্টাল ব্রেক + একটি ইনস্ট্রুমেন্টাল ব্রেকের সময় মিনিপ্লেয়ার এবং মিডিয়া সেশনে প্রদর্শিত হয়। প্রতিটি লাইন একটি অ্যানিমেশন ফ্রেম। {t} হলো শিরোনাম, {a} হলো শিল্পী এবং {lrc} হলো শেষ লিরিক্স লাইন + মিনিপ্লেয়ার এবং মিডিয়া সেশন টাইমিং অফসেট + মিনিপ্লেয়ার এবং MediaSession-এ পরবর্তী লাইন কখন প্রদর্শিত হবে তা শিফট করে। যদি লাইনগুলো দেরিতে প্রদর্শিত হয়, তবে একটি নেতিবাচক মান ব্যবহার করুন, উদাহরণস্বরূপ লক স্ক্রিনে লিরিক্স ট্র্যাক শিরোনাম এবং শিল্পী অনুযায়ী খোঁজা হয়, […] ম্যানুয়ালি সংস্করণ পরিবর্তন করতে আপনি রিফ্রেশ বাটনও চেষ্টা করতে পারেন। […] সব সক্রিয় প্রদানকারী সবসময় তাদের ক্রমে অনুরোধ করা হয় না, তবে তাদের গতির ভিত্তিতে সাড়া দেয়। মেটাডেটা রেজেক্স গানের কথা খোঁজার আগে এবং দুটি শিরোনাম মেলে কিনা তা যাচাই করার সময় শিরোনাম, শিল্পী এবং অ্যালবামগুলিতে রেগুলার এক্সপ্রেশন প্রয়োগ করা হয়। উভয় নিষ্ক্রিয় করতে খালি রাখুন। ডিফল্টটি \"Official Video\", \"Remaster\", \"【插曲】\" এবং \"- Topic\" এর মতো সাধারণ অপ্রয়োজনীয় অংশ সরিয়ে দেয়। @@ -559,6 +580,10 @@ SABR প্লেব্যাক (ভিডিও স্ট্রিম স্ শুধুমাত্র মূল লিরিক্স টেক্সটে প্রযোজ্য রেগুলার এক্সপ্রেশন (অনুবাদ বা রোমানাইজেশনে নয়)। খালি রাখলে এটি নিষ্ক্রিয় থাকবে। তথ্য লাইনের মার্কার যদি লিরিক্স ভুলভাবে নিচে ফিল্টার করা হয়, তবে এটি সামঞ্জস্য করুন বা মুছে ফেলুন। কমা-বিভাজিত মার্কার: একটি মার্কারের পরে কোলন থাকে এমন লাইনগুলো নিচে ক্রেডিট তথ্য হিসেবে দেখানো হয়। খালি রাখলে এটি নিষ্ক্রিয় থাকবে। + প্রথম অক্ষরটি বড় হাতের করুন + লিরিক্সের প্রতিটি লাইনের প্রথম অক্ষরটি বড় হাতের করে + শেষের বিরামচিহ্ন সরিয়ে ফেলুন + লিরিক্স লাইনের শেষে থাকা ফুল স্টপ, কমা, কোলন এবং সেমিকোলন সরিয়ে ফেলে। প্রশ্নচিহ্ন, বিস্ময়চিহ্ন, ইলিপসিস, উদ্ধৃতি চিহ্ন এবং ব্র্যাকেট রাখা হয় টেক্সট আকার লিরিক্স টেক্সটের আকার বৈশ্বিক সময় অফসেট diff --git a/patches/src/main/resources/addresources/values-bn-rBD/youtube/strings.xml b/patches/src/main/resources/addresources/values-bn-rBD/youtube/strings.xml index 81444d8e5d..66a29cc0e2 100644 --- a/patches/src/main/resources/addresources/values-bn-rBD/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-bn-rBD/youtube/strings.xml @@ -393,6 +393,9 @@ Second \"item\" text" কেনাকাটার লিঙ্ক লুকান ভিডিও বিজ্ঞাপন লুকান YouTube প্রিমিয়াম প্রচারণা লুকান + + + লোড হচ্ছে… বর্তমান চ্যানেল: হোয়াইটলিস্টে যোগ করুন @@ -409,6 +412,9 @@ Second \"item\" text" চ্যানেল অনুসন্ধানের ফলাফল এই চ্যানেলে কিছুই পাওয়া যায়নি এই চ্যানেলে অনুসন্ধান করা যায়নি + ভিডিও + পোস্টস + এই চ্যানেলের কোনো পোস্ট মেলেনি প্রাসঙ্গিকতা সবচেয়ে বেশি দেখা সবচেয়ে কম দেখা @@ -494,6 +500,9 @@ Second \"item\" text" ভিডিও অডিও সকল বিকল্প + Shorts মেনুতে ডাউনলোড দেখান + Shorts প্লেয়ারে একটি এক্সটারনাল ডাউনলোডার ফ্লাইআউট মেনু আইটেম যোগ করে + ডাউনলোড ভয়েস ওভার অনুবাদ ডিভাইসের টেক্সট-টু-স্পিচ ব্যবহার করে প্লেব্যাকের সাথে মিলিয়ে ভিডিও ক্যাপশন পড়ে শোনান @@ -1106,9 +1115,6 @@ OpenRouter.ai ওপেন করতে এখানে ট্যাপ কর "পুরানো প্লেয়ার লেআউট ফিরিয়ে আনুন সীমাবদ্ধতা: \"পুরানো ভিডিও অ্যাকশন বার ফিরিয়ে আনুন\" অপশনটি বন্ধ থাকলে, \"আরও ভিডিও\" ওভারলেটি পাওয়া নাও যেতে পারে" - "এটি চালু করলে প্লেয়ার লেআউটের কিছু বৈশিষ্ট্য ভেঙে যেতে পারে, যেমন কমেন্ট সেকশন বা প্লেয়ারের নিচে দেখানো 'More videos'। - -ট্যাবলেট লেআউট এখন আর YouTube দ্বারা সমর্থিত নয় এবং সাম্প্রতিক YouTube ভার্সনগুলোতে আংশিকভাবে বা সম্পূর্ণভাবে অকার্যকর। একটি কার্যকর ট্যাবলেট লেআউটের জন্য, YouTube 20.21.37 প্যাচ করুন" 21.11.48 - পুরনো ক্যাপশন আচরণ পুনরুদ্ধার করুন 20.39.41 - প্লেলিস্টে পুরোনো সেভ করার অপশন ফিরিয়ে আনুন @@ -1611,4 +1617,17 @@ Incognito মোডে, অ্যাকাউন্ট পরিচয় উপ কোনো সিরিজ পাওয়া যায়নি শিরোনামহীন অধ্যায় অপ্রাপ্য অধ্যায় + + Niconico মন্তব্য + লাইভ চ্যাট এবং চ্যাট রিপ্লে-কে ভিডিওর ওপর দিয়ে স্ক্রোল করা মন্তব্য হিসেবে দেখান + ভিডিও প্লেয়ারে একটি ওভারলে বোতাম দেখায়। মন্তব্য দেখাতে বা লুকাতে ট্যাপ করুন, দ্রুত সেটিংসের জন্য ট্যাপ করে ধরে রাখুন + মন্তব্যের ফন্ট সাইজ + মন্তব্যের টেক্সটের আকার (১২-৪০ sp) + মন্তব্যের অপাসিটি + মন্তব্যের টেক্সট এবং ব্যাকগ্রাউন্ডের অপাসিটি + ট্রাভেল ডিউরেশন + মন্তব্যগুলো স্ক্রিন পার করতে কত সেকেন্ড সময় নেবে। কম মান হলে দ্রুত মুভ করবে + কমেন্টের সর্বোচ্চ দৈর্ঘ্য + এর থেকে বেশি ক্যারেক্টারের কমেন্টগুলো হাইড করে। প্রতিটি ইমোজি একটি ক্যারেক্টার হিসেবে গণ্য হবে + কোনো সীমা নেই diff --git a/patches/src/main/resources/addresources/values-bs-rBA/youtube/strings.xml b/patches/src/main/resources/addresources/values-bs-rBA/youtube/strings.xml index 28b68ddfec..68bd74274d 100644 --- a/patches/src/main/resources/addresources/values-bs-rBA/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-bs-rBA/youtube/strings.xml @@ -57,6 +57,8 @@ Second \"item\" text" + + @@ -238,4 +240,5 @@ Second \"item\" text" The + diff --git a/patches/src/main/resources/addresources/values-ca-rES/youtube/strings.xml b/patches/src/main/resources/addresources/values-ca-rES/youtube/strings.xml index a4852a6954..c721d2b6f5 100644 --- a/patches/src/main/resources/addresources/values-ca-rES/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ca-rES/youtube/strings.xml @@ -64,6 +64,8 @@ Second \"item\" text" + + Eliminar @@ -248,4 +250,5 @@ Second \"item\" text" Automàtic Cancel·lar + diff --git a/patches/src/main/resources/addresources/values-ckb-rIR/youtube/strings.xml b/patches/src/main/resources/addresources/values-ckb-rIR/youtube/strings.xml index b788e62703..62ae62fe14 100644 --- a/patches/src/main/resources/addresources/values-ckb-rIR/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ckb-rIR/youtube/strings.xml @@ -143,6 +143,9 @@ Second \"item\" text" کارتی خۆ سپۆنسەرکراو بشارەوە بەستەرەکانی بازاڕکردن بشارەوە شاردنەوەی ڕێکلامی ڤیدیۆکان + + + بارکردن… کەناڵی ئێستا: زیادکردن بۆ لیستی ڕێگەپێدراو @@ -170,6 +173,7 @@ Second \"item\" text" ڤیدێۆ + دابەزاندن باو @@ -550,4 +554,5 @@ Second \"item\" text" پاشگەز بەردەوامبوون گەڕانەوە + diff --git a/patches/src/main/resources/addresources/values-cs-rCZ/music/strings.xml b/patches/src/main/resources/addresources/values-cs-rCZ/music/strings.xml index 268fb05ac8..03da3a06db 100644 --- a/patches/src/main/resources/addresources/values-cs-rCZ/music/strings.xml +++ b/patches/src/main/resources/addresources/values-cs-rCZ/music/strings.xml @@ -550,8 +550,29 @@ Požadované cookies: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAP Zobrazí aktuální řádek na uzamčené obrazovce, Android Auto a Bluetooth. Vyžaduje Android 11 nebo novější Zobrazovat texty v minipřehrávači Zobrazí aktuální řádek v minihráči, přičemž stopa je přesunuta do titulků - Pořadí zobrazení interpreta a názvu - Přepíná pořadí zobrazení interpreta a názvu v minipřehrávači a MediaSession. Zapnuto „Interpret – Název“ a vypnuto „Název – Interpret“. Neovlivňuje vyhledávání textů. + Formát názvu skladby/umělce + Formát skladby zobrazené s textem v miniplayeru a MediaSession. {t} je název, {a} je umělec a {v} je verze, např. Acoustic, pokud ji zdroj textu poskytuje. Text mezi dvěma dvojtečkami se zobrazí pouze tehdy, pokud mají všechny proměnné uvnitř hodnotu; pro obyčejnou dvojtečku napište \\:. Každý řádek se zobrazuje postupně a mění se s textem písně + Formát řádku textu písně + Formát řádku textu v miniplayeru a MediaSession. {lrc} je řádek a {n} je počet po sobě následujících opakování. Text mezi dvojtečkami se zobrazí pouze tehdy, pokud se řádek opakuje. Pro dvojtečku použijte \\: + Formát musí obsahovat {lrc} + Šablona řádku s umělcem a načasování animace + Minimální doba zobrazení (ms) + Maximální doba zobrazení (ms) + Pouze pro víceřádkové animace. Každý snímek ponechte zobrazený alespoň minimální dobu. Při maximální době posuňte řádek s umělcem, i když zůstává text textu stejný. + Minimum musí být 0 nebo více. Maximum musí být větší než 0 a alespoň rovno minimu. + Limity délky řádku + Limity řádků textu a umělce a váha znaků s plnou šířkou + "Limit řádku textu +Delší texty rozdělte do vyvážených polovin. Hodnota 0 vypíná rozdělování." + "Limit řádků pro umělce +Rozdělí vyplněný formát stopy do cyklických snímků. Hodnota 0 vypne rozdělování." + "Váha znaků plné šířky +Platí pro oba limity. Při hodnotě 1,75 se čtyři znaky plné šířky počítají jako 7." + Zadejte celočíselné limity od 0 do 10000 a váhu znaků větší než 0 + Instrumentální pauza + Zobrazuje se v miniplayeru a MediaSession během instrumentální pauzy. Každý řádek je jeden animační snímek. {t} je název, {a} je umělec a {lrc} je poslední řádek textu + Časový posun Miniplayeru a MediaSession + Posouvá čas, kdy se v miniplayeru a MediaSession zobrazí další řádek. Pokud se řádky zobrazují pozdě, například na obrazovce zámku, použijte zápornou hodnotu Filtry Me […] texty. […] : \"Lyrics filter regex\" Regulární výraz aplikovaný na názvy, interprety a alba před vyhledáváním textů písní a při vyhodnocování, zda dva názvy odpovídají. Ponechte prázdné, chcete-li obojí zakázat. Výchozí hodnota odstraňuje běžný balast, jako jsou „Official Video“, „Remaster“, „【插曲】“ a „- Topic“. @@ -559,6 +580,10 @@ Požadované cookies: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAP Regulární výraz se použije pouze na původní text písně. Tuto funkci můžete využít pro filtrování textů. Značky informačních řádků Pokud jsou texty nesprávně odfiltrovány dolů, upravte nebo vymažte toto pole. Značky oddělené čárkami: řádky obsahující značku následovanou dvojtečkou se zobrazují dole jako informace o autorství. Nechte prázdné pro vypnutí. + Velké počáteční písmeno + Změní první písmeno každého řádku textu písně na velké + Odebrat koncovou interpunkci + Odebere tečky, čárky, dvojtečky a středníky na koncích řádků textu písně. Otazníky, vykřičníky, trojtečky, uvozovky a závorky jsou zachovány Velikost textu Velikost textu písně Globální časový posun diff --git a/patches/src/main/resources/addresources/values-cs-rCZ/youtube/strings.xml b/patches/src/main/resources/addresources/values-cs-rCZ/youtube/strings.xml index 5f341e0100..3052bdf1b4 100644 --- a/patches/src/main/resources/addresources/values-cs-rCZ/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-cs-rCZ/youtube/strings.xml @@ -393,6 +393,9 @@ Hlášení jsou veřejná a před přidáním kanálu do seznamu jsou kontrolov Skrýt nákupní odkazy Skrýt reklamy ve videu Skrýt nabídky YouTube Premium + + + Načítám… Aktuální kanál: Přidat do seznamu povolených @@ -409,6 +412,9 @@ Hlášení jsou veřejná a před přidáním kanálu do seznamu jsou kontrolov Výsledky vyhledávání kanálů Na tomto kanálu se nic nenašlo Tento kanál se nepodařilo prohledat + Videa + Príspevky + Na tomto kanálu nebyly nalezeny žádné príspevky Relevantnost Nejvíce zhlédnuté Nejméně zhlédnuté @@ -442,6 +448,7 @@ Hlášení jsou veřejná a před přidáním kanálu do seznamu jsou kontrolov Stáhnout obrázek Stahování %s obrázků… Stahování obrázku… + %1$s obrázků uloženo do %2$s Obrázek byl uložen do %s Obrázek se nepodařilo stáhnout V tomto příspěvku nebyl nalezen žádný obrázek @@ -493,6 +500,9 @@ Po jednom otevření videa s tlačítkem Ask funguje funkce se všemi videi, i k Video Zvuk Všechny možnosti + Zobrazit stahování v menu Shorts + Přidá položku externího stahovače do vysouvacího menu přehrávače Shorts + Stáhnout Hlasový překlad Přečte titulky videa nahlas pomocí text-to-speech v zařízení, synchronizovaně s přehráváním @@ -1118,9 +1128,6 @@ Rozhraní pro automobily "Obnoví staré rozložení přehrávače Omezení: Pokud je vypnutá možnost 'Obnovit starou lištu akcí videa', překrytí 'Další videa' nemusí být k dispozici" - "Zapnutí této funkce může narušit některé prvky rozložení přehrávače, jako je sekce komentářů nebo „Další videa“ zobrazená pod přehrávačem. - -Rozložení pro tablety již YouTube nepodporuje a v novějších verzích YouTube je částečně nebo zcela nefunkční. Pro funkční rozložení pro tablety aplikujte patch na YouTube 20.21.37" 21.11.48 - Obnovit staré chování titulků 20.39.41 - Obnoví staré uložení zpět do seznamu přehrávání @@ -1623,4 +1630,17 @@ Automatické nahrávání je volitelné. Zastavuje se v anonymním režimu nebo Nebyly nalezeny žádné série Díl bez názvu Nedostupný díl + + Komentáře Niconico + Zobrazit živý chat a opakování chatu jako komentáře, které přejíždějí přes video + Zobrazí překryvné tlačítko ve videopřehrávači. Klepnutím zobrazíte nebo skryjete komentáře, dlouhým stisknutím otevřete rychlé nastavení + Velikost písma komentářů + Velikost textu komentářů (12–40 sp) + Průhlednost komentářů + Průhlednost textu a pozadí komentářů + Doba průjezdu + Kolik sekund komentářům trvá překročit obrazovku. Nižší hodnoty znamenají rychlejší pohyb + Maximální délka komentáře + Skryje komentáře, které mají více znaků než tato hodnota. Každý emoji se počítá jako jeden znak + Bez limitu diff --git a/patches/src/main/resources/addresources/values-da-rDK/music/strings.xml b/patches/src/main/resources/addresources/values-da-rDK/music/strings.xml index 35ba681b34..5ea8157b42 100644 --- a/patches/src/main/resources/addresources/values-da-rDK/music/strings.xml +++ b/patches/src/main/resources/addresources/values-da-rDK/music/strings.xml @@ -549,8 +549,29 @@ Krævede cookies: SID, HSID, SSID, SAPISID, __Secure-3PAPISID" Viser den nuværende linje på låseskærmen, Android Auto og Bluetooth. Kræver Android 11 eller nyere Vis Miniplayer-tekster Viser den nuværende linje i mini-spilleren, med sporet flyttet til underteksten - Visning af kunstner og titel […] TIL \"Kunstner - Titel\" og FRA \"Titel - Kunstner\" Påvirker ikke tekstsøgning. - Skift visningsrækkefølgen for kunstner og titel i Miniplayer og MediaSession. TIL \"Kunstner - Titel\" og FRA \"Titel - Kunstner\". Det påvirker ikke søgningen efter sangtekster. + Format for sangtitel/artist + Formatet for sporet, der vises med sangteksten i miniplayeren og MediaSession. {t} er titlen, {a} er kunstneren og {v} er versionen, f.eks. akustisk, når kilden til sangteksten angiver en. Tekst mellem to kolonner vises kun, når hver variabel i den har en værdi; skriv \\: for et almindeligt kolon. Hver linje vises efter tur og skifter, hver gang sangteksten ændres + Format for tekstlinje + Formatet for sangtekstlinjen i miniplayeren og MediaSession. {lrc} er linjen, og {n} er antallet af gange, den synges i træk. Tekst mellem koloner vises kun, når linjen gentages. Brug \\: for et kolon + Formatet skal indeholde {lrc} + Skabelon for artistlinje og animationstid + Minimum skærmtid (ms) + Maksimum skærmtid (ms) + Kun for flertumsanimationer. Behold hver frame i mindst den minimumstid. Ved maksimum rykkes artistlinjen videre, selv når teksten forbliver den samme. + Minimum skal være 0 eller derover. Maksimum skal være større end 0 og mindst det samme som minimum. + Grænser for linjelængde + Grænser for tekst- og artistlinjer samt vægt for fuldbreddekarakterer + "Grænse for tekstlinje +Opdel længere tekster i balancerede halvdele. 0 deaktiverer opdeling." + "Grænse for kunstnerlinjer +Opdel det udfyldte sporformat i cykliske rammer. 0 deaktiverer opdeling." + "Vægt for fuldbredde-tegn +Gælder for begge grænser. Ved 1,75 tæller fire fuldbredde-tegn som 7." + Indtast heltalgrænser fra 0 til 10000 og en tegnvægt større end 0 + Instrumentalt stykke + Vises i Miniplayeren og MediaSession under et instrumentalt stykke. Hver linje er én animationsramme. {t} er titlen, {a} er kunstneren og {lrc} er den sidste sangtekstlinje + Tidsforskydning for Miniplayer og MediaSession + Forskubber hvornår den næste linje vises i miniplayeren og MediaSession. Brug en negativ værdi, hvis linjerne vises for sent, for eksempel på låseskærmen Filtre Regex for tekstfilter Regulært udtryk, der anvendes på titler, kunstnere og albummer, før man slår tekster op, og ved vurdering af, om to titler matcher. Lad feltet være tomt for at deaktivere begge. Standardværdien fjerner almindelig støj som \"Official Video\", \"Remaster\", \"【插曲】\" og \"- Topic\". @@ -558,6 +579,10 @@ Krævede cookies: SID, HSID, SSID, SAPISID, __Secure-3PAPISID" Et regulært udtryk anvendes kun på den oprindelige tekst (ikke oversættelser eller romaniseringer). Lad det være tomt for at deaktivere. Markører for infolinjer Hvis teksterne fejlagtigt filtreres til bunden, skal du justere eller rydde dette. […] Lad feltet være tomt for at deaktivere. + Skriv første bogstav med stort + Skriver det første bogstav i hver sangtekstlinje med stort + Fjern afsluttende tegnsætning + Fjerner punktummer, kommaer, kolon og semikolon i slutningen af sangtekstlinjer. Spørgsmålstegn, udråbstegn, ellipse, citationstegn og parenteser bevares Tekststørrelse Størrelse på sangteksten Global tidsforskydning diff --git a/patches/src/main/resources/addresources/values-da-rDK/youtube/strings.xml b/patches/src/main/resources/addresources/values-da-rDK/youtube/strings.xml index 6972343581..a011b8537c 100644 --- a/patches/src/main/resources/addresources/values-da-rDK/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-da-rDK/youtube/strings.xml @@ -391,6 +391,9 @@ Rapporter er offentlige og gennemgås, før en kanal tilføjes til en liste. Fje Skjul shoppinglinks Skjul videoannoncer Skjul YouTube Premium reklamer + + + Indlæser… Aktuel kanal: Tilføj til hvidliste @@ -407,6 +410,9 @@ Rapporter er offentlige og gennemgås, før en kanal tilføjes til en liste. Fje Kanalens søgeresultater Intet fundet på denne kanal Kunne ikke søge i denne kanal + Videoer + Opslag + Ingen opslag på denne kanal matcher Relevans Mest sete Mindst sete @@ -440,6 +446,7 @@ Rapporter er offentlige og gennemgås, før en kanal tilføjes til en liste. Fje Download billede Downloader %s billeder… Downloader billede… + %1$s billeder gemt i %2$s Billede gemt i %s Kunne ikke downloade billedet Intet billede fundet i dette opslag @@ -491,6 +498,9 @@ Efter at en video med Ask-knappen er åbnet én gang, fungerer funktionen med al Video Lyd Alle valgmuligheder + Vis download i Shorts-menuen + Tilføjer et eksternt download-menupunkt til Shorts-afspilleren + Download Tekstlæsning Læs videoundertitler højt ved hjælp af tekst-til-tale på enheden, synkroniseret med afspilningen @@ -1116,9 +1126,6 @@ Bil layout "Gendanner det gamle layout for afspilleren Begrænsning: Hvis 'Gendan gammel video-handlingslinje' er slået fra, er 'Flere videoer'-overlejringen muligvis ikke tilgængelig" - "Aktivering af dette kan ødelægge nogle af afspillerens layoutfunktioner, såsom kommentarfeltet eller \"Flere videoer\", der vises under afspilleren. - -Tablet-layout understøttes ikke længere af YouTube og er delvist eller helt ødelagt i nyere YouTube-versioner. For et fungerende tablet-layout skal YouTube 20.21.37 patches" 21.11.48 - Genopret den gamle adfærd for undertekster 20.39.41 - gendan old save til spilleliste @@ -1621,4 +1628,17 @@ Automatisk optagelse er valgfri. Den stopper i inkognito eller når kontoen ænd Ingen serier fundet Serie uden titel Utilgængelig episode + + Niconico-kommentarer + Vis live-chat og chat-genafspilning som kommentarer, der ruller hen over videoen + Viser en overlay-knap i videofafspilleren. Tryk for at vise eller skjule kommentarerne, tryk og hold for hurtige indstillinger + Kommentarernes skrifttypestørrelse + Tekststørrelse for kommentarerne (12-40 sp) + Kommentarernes gennemsigtighed + Gennemsigtighed for kommentartekst og baggrunde + Rejsetid + Hvor mange sekunder kommentarerne bruger på at krydse skærmen. Lavere værdier bevæger sig hurtigere + Maksimal kommentarlængde + Skjuler kommentarer med flere tegn end dette. Hver emoji tæller som ét tegn + Ingen grænse diff --git a/patches/src/main/resources/addresources/values-de-rDE/music/strings.xml b/patches/src/main/resources/addresources/values-de-rDE/music/strings.xml index 957defa4e4..fd9d055f20 100644 --- a/patches/src/main/resources/addresources/values-de-rDE/music/strings.xml +++ b/patches/src/main/resources/addresources/values-de-rDE/music/strings.xml @@ -550,8 +550,29 @@ Erforderliche Cookies: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PA Zeigt die aktuelle Zeile auf dem Sperrbildschirm, Android Auto und Bluetooth. Benötigt Android 11 oder neuer Miniplayer-Songtexte anzeigen. Zeigt die aktuelle Zeile im Miniplayer an, wobei der Titel in den Untertitel verschoben wird - Anzeige von Künstler und Titel […] EIN „Künstler – Titel“ und AUS „Titel – Künstler“. Das hat keinen Einfluss auf die Songtextsuche. - Schaltet die Anzeigereihenfolge von Interpret und Titel im Miniplayer und in MediaSession um. EIN „Interpret - Titel“ und AUS „Titel - Interpret“. Dies hat keinen Einfluss auf die Songtextsuche. + Format für Songtitel/Künstler + Format des Titels, der zusammen mit dem Songtext im Miniplayer und in der MediaSession angezeigt wird. {t} ist der Titel, {a} ist der Künstler und {v} ist die Version, z. B. Acoustic, sofern die Songtextquelle eine bereitstellt. Text zwischen zwei Doppelpunkten wird nur angezeigt, wenn jede darin enthaltene Variable einen Wert hat; schreiben Sie \\: für einen einfachen Doppelpunkt. Jede Zeile wird nacheinander angezeigt und wechselt, sobald sich der Songtext ändert + Format der Songtextzeile + Format der Songtextzeile im Miniplayer und in der MediaSession. {lrc} ist die Zeile und {n} ist die Anzahl der aufeinanderfolgenden Wiederholungen. Text zwischen Doppelpunkten wird nur angezeigt, wenn die Zeile wiederholt wird. Verwenden Sie \\: für einen Doppelpunkt + Das Format muss {lrc} enthalten + Vorlage für die Künstlerzeile und Timing der Animation + Minimale Bildschirmzeit (ms) + Maximale Bildschirmzeit (ms) + Nur für mehrzeilige Animationen. Behalte jeden Frame für mindestens die Mindestzeit bei. Beim Maximum rückt die Künstlerzeile weiter, selbst wenn der Songtext gleich bleibt. + Das Minimum muss 0 oder größer sein. Das Maximum muss größer als 0 und mindestens so groß wie das Minimum sein. + Zeilenlängenbeschränkungen + Beschränkungen für Songtext- und Künstlerzeilen sowie Gewichtung von Vollbreite-Zeichen + "Limit für Songtextzeile +Längere Songtexte in ausgeglichene Hälften aufteilen. 0 deaktiviert die Aufteilung." + "Zeilenlimit für Künstler +Teilt das ausgefüllte Track-Format in zyklische Frames auf. 0 deaktiviert die Aufteilung." + "Gewichtung von Vollbreite-Zeichen +Überträgt sich auf beide Limits. Bei 1,75 zählen vier Vollbreite-Zeichen als 7." + Geben Sie ganzzahlige Limits von 0 bis 10000 und eine Zeichengewichtung größer als 0 ein + Instrumentalpause + Wird im Miniplayer und in der MediaSession während einer Instrumentalpause angezeigt. Jede Zeile ist ein Animationsframe. {t} ist der Titel, {a} ist der Künstler und {lrc} ist die letzte Textzeile + Timing-Offset für Miniplayer und MediaSession + Verschiebt den Zeitpunkt, zu dem die nächste Zeile im Miniplayer und in der MediaSession angezeigt wird. Verwenden Sie einen negativen Wert, wenn Zeilen zu spät erscheinen, zum Beispiel auf dem Sperrbildschirm Filter RegEx für Songtextfilter Regulärer Ausdruck, der auf Titel, Interpreten und Alben angewendet wird, bevor Songtexte gesucht werden und beim Bewerten, ob zwei Titel übereinstimmen. Leer lassen, um beides zu deaktivieren. Die Standardeinstellung entfernt häufiges Beiwerk wie \"Official Video\", \"Remaster\", \"【插曲】\" und \"- Topic\". @@ -559,6 +580,10 @@ Erforderliche Cookies: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PA Regulärer Ausdruck, der nur auf den Originaltext der Songtexte angewendet wird (nicht auf Übersetzungen oder Romanisierungen). Leer lassen, um zu deaktivieren. Markierungen für Infozeilen. Wenn Songtexte fälschlicherweise nach unten herausgefiltert werden, passe dies an oder leere das Feld. […] Leer lassen, um zu deaktivieren. + Ersten Buchstaben großschreiben + Schreibt den ersten Buchstaben jeder Liedtextzeile groß + Abschließende Satzzeichen entfernen + Entfernt Punkte, Kommas, Doppelpunkte und Semikolons am Ende von Liedtextzeilen. Fragezeichen, Ausrufezeichen, Auslassungszeichen, Anführungszeichen und Klammern bleiben erhalten Textgröße Größe des Liedtextes Globaler Zeitversatz diff --git a/patches/src/main/resources/addresources/values-de-rDE/youtube/strings.xml b/patches/src/main/resources/addresources/values-de-rDE/youtube/strings.xml index 055cace0e6..61c3724341 100644 --- a/patches/src/main/resources/addresources/values-de-rDE/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-de-rDE/youtube/strings.xml @@ -394,6 +394,9 @@ Meldungen sind öffentlich und werden überprüft, bevor ein Kanal zu einer List Shopping-Links ausblenden Videowerbung ausblenden Werbeaktionen für YouTube Premium ausblenden + + + Lädt… Aktueller Kanal: Zur Whitelist hinzufügen @@ -410,6 +413,9 @@ Meldungen sind öffentlich und werden überprüft, bevor ein Kanal zu einer List Kanal-Suchergebnisse Auf diesem Kanal wurde nichts gefunden. Dieser Kanal konnte nicht durchsucht werden. + Videos + Beiträge + Keine Beiträge in diesem Kanal entsprechen der Suche Relevanz Am häufigsten gesehen Am wenigsten gesehen @@ -495,6 +501,9 @@ Nachdem ein Video mit der Ask-Schaltfläche einmal geöffnet wurde, funktioniert Video Audio Alle Optionen + Downloads in Shorts-Menü anzeigen + Fügt dem Shorts-Player eine externe Download-Option im Flyout-Menü hinzu + Herunterladen Sprachausgabe-Übersetzung Lies die Videountertitel laut mithilfe der Text-to-Speech-Funktion des Geräts, synchronisiert mit der Wiedergabe. @@ -1122,9 +1131,6 @@ Automotive-Layout "Stellt das alte Player-Layout wieder her Einschränkung: Wenn \"Alte Video-Aktionsleiste wiederherstellen\" deaktiviert ist, ist das Overlay \"Mehr Videos\" möglicherweise nicht verfügbar" - "Die Aktivierung dieser Funktion kann einige Layout-Features des Players beeinträchtigen, wie z. B. den Kommentarbereich oder die unter dem Player angezeigten \"Weiteren Videos\". - -Das Tablet-Layout wird von YouTube nicht mehr unterstützt und ist in aktuellen YouTube-Versionen teilweise oder vollständig defekt. Für ein funktionierendes Tablet-Layout patchen Sie YouTube 20.21.37" 21.11.48 - Altes Untertitelverhalten wiederherstellen 20.39.41 - Alte Shorts Spielersymbole wiederherstellen @@ -1627,4 +1633,17 @@ Die automatische Aufzeichnung ist optional. Sie stoppt im Inkognito-Modus oder b Keine Serien gefunden Titellose Episode Nicht verfügbare Episode + + Niconico-Kommentare + Zeigt den Live-Chat und die Chat-Wiederholung als Kommentare an, die über das Video scrollen + Zeigt eine Overlay-Schaltfläche im Videoplayer an. Tippen, um die Kommentare ein- oder auszublenden; gedrückt halten für Schnelleinstellungen + Schriftgröße der Kommentare + Textgröße der Kommentare (12-40 sp) + Deckkraft der Kommentare + Deckkraft des Kommentartexts und der Hintergründe + Laufzeit + Wie viele Sekunden die Kommentare benötigen, um den Bildschirm zu überqueren. Niedrigere Werte bewegen sich schneller + Maximale Kommentarlänge + Blendet Kommentare aus, die mehr Zeichen als diese Anzahl enthalten. Jedes Emoji zählt als ein Zeichen + Keine Begrenzung diff --git a/patches/src/main/resources/addresources/values-el-rGR/music/strings.xml b/patches/src/main/resources/addresources/values-el-rGR/music/strings.xml index ea598eef3e..d09520ab93 100644 --- a/patches/src/main/resources/addresources/values-el-rGR/music/strings.xml +++ b/patches/src/main/resources/addresources/values-el-rGR/music/strings.xml @@ -550,8 +550,29 @@ Second \"item\" text" Εμφάνιση της τρέχουσας γραμμής στην οθόνη κλειδώματος, στο Android Auto και στο Bluetooth. Απαιτείται Android 11 ή νεότερο Εμφάνιση στίχων στο Miniplayer. Εμφάνιση της τρέχουσας γραμμής στο μίνι παίκτη, με την κομμάτι να μεταφέρεται στα υπότιτλα - Εμφάνιση καλλιτέχνη και τίτλου […] MediaSession. ON: \"Καλλιτέχνης - Τίτλος\" και OFF: \"Τίτλος - Καλλιτέχνης\". Δεν επηρεάζει την αναζήτηση στίχων. - Εναλλαγή της σειράς εμφάνισης καλλιτέχνη και τίτλου στο Miniplayer και στο MediaSession. Ενεργό \"Καλλιτέχνης - Τίτλος\" και ανενεργό \"Τίτλος - Καλλιτέχνης\" Δεν επηρεάζει την αναζήτηση στίχων + Μορφή τίτλου τραγουδιού/καλλιτέχνη + Μορφή του κομματιού που εμφανίζεται με τους στίχους στο Miniplayer και στο MediaSession. Το {t} είναι ο τίτλος, το {a} είναι ο καλλιτέχνης και το {v} είναι η έκδοση, όπως η Acoustic, όταν η πηγή των στίχων παρέχει μία τέτοια. Το κείμενο μεταξύ δύο δρόμων εμφανίζεται μόνο όταν κάθε μεταβλητή μέσα σε αυτό έχει τιμή· γράψτε \\: για έναν απλό δρόμο. Κάθε γραμμή εμφανίζεται με τη σειρά, προχωρώντας κάθε φορά που αλλάζουν οι στίχοι + Μορφή γραμμής στίχου + Μορφή της γραμμής στίχων στο Miniplayer και στο MediaSession. Το {lrc} είναι η γραμμή και το {n} είναι το πόσες φορές τραγουδιέται συνεχόμενα. Το κείμενο μεταξύ των δρόμων εμφανίζεται μόνο όταν η γραμμή επαναλαμβάνεται. Χρησιμοποιήστε \\: για έναν δρόμο + Η μορφή πρέπει να περιλαμβάνει το {lrc} + Πρότυπο γραμμής καλλιτέχνη και χρονισμός animation + Ελάχιστος χρόνος οθόνης (ms) + Μέγιστος χρόνος οθόνης (ms) + Μόνο για animations πολλαπλών γραμμών. Διατήρησε κάθε frame για τουλάχιστον τον ελάχιστο χρόνο. Στο μέγιστο, προχώρα τη γραμμή του καλλιτέχνη ακόμα και όταν ο στίχος παραμένει ο ίδιος. + Το ελάχιστο πρέπει να είναι 0 ή μεγαλύτερο. Το μέγιστο πρέπει να είναι μεγαλύτερο από 0 και τουλάχιστον ίσο με το ελάχιστο. + Όρια μήκους γραμμής + Όρια γραμμής στίχου και καλλιτέχνη, και βάρος χαρακτήρων πλήρους πλάτους + "Όριο γραμμής στίχου +Διχάσε τους μακρύτερους στίχους σε ισορροπημένα μισά. Το 0 απενεργοποιεί τη διάσπαση." + "Όριο γραμμών καλλιτέχνη +Χωρίστε τη μορφή της γεμισμένης κομμάτις σε εναλλασσόμενα πλαίσια. Το 0 απενεργοποιεί τον διαχωρισμό." + "Βάρος χαρακτήρα πλήρους πλάτους +Εφαρμόζεται και στους δύο περιορισμούς. Στο 1,75, τέσσερις χαρακτήρες πλήρους πλάτους μετράνε ως 7." + Εισαγάγετε ακέραιους περιορισμούς από 0 έως 10000 και ένα βάρος χαρακτήρα μεγαλύτερο από 0 + Οργανικό διάλειμμα + Εμφανίζεται στον Miniplayer και στο MediaSession κατά τη διάρκεια ενός οργανικού διαλείμματος. Κάθε γραμμή είναι ένα πλαίσιο κίνησης. Το {t} είναι ο τίτλος, το {a} ο καλλιτέχνης και το {lrc} η τελευταία γραμμή των στίχων + Απόκλιση χρονισμού miniplayer και MediaSession + Μετοπίζει το πότε εμφανίζεται η επόμενη γραμμή στον ελαχιστοποιημένο πρόγραμμα αναπαραγωγής και στο MediaSession. Χρησιμοποιήστε μια αρνητική τιμή εάν οι γραμμές εμφανίζονται αργά, για παράδειγμα στην οθόνη κλειδώματος Φίλτρα Regex φίλτρου στίχων. Κανονική έκφραση που εφαρμόζεται σε τίτλους, καλλιτέχνες και άλμπουμ πριν από την αναζήτηση στίχων και κατά τη βαθμολόγηση του αν δύο τίτλοι ταιριάζουν. Αφήστε το κενό για να απενεργοποιήσετε και τα δύο. Η προεπιλογή αφαιρεί συνηθισμένα περιττά στοιχεία, όπως \"Official Video\", \"Remaster\", \"【插曲】\" και \"- Topic\". @@ -559,6 +580,10 @@ Second \"item\" text" Η κανονική έκφραση εφαρμόζεται μόνο στο αρχικό κείμενο στίχων (όχι σε μεταφράσεις ή μεταγραφές). Αφήστε το κενό για απενεργοποίηση. Δείκτες γραμμών πληροφοριών. Αν οι στίχοι φιλτράρονται λανθασμένα προς τα κάτω, προσαρμόστε ή καθαρίστε αυτό το πεδίο. […] Αφήστε το κενό για απενεργοποίηση. + Κεφαλαίωση του πρώτου γράμματος + Κεφαλαιοποιεί το πρώτο γράμμα κάθε γραμμής των στίχων + Αφαίρεση τελικής στίξης + Αφαιρεί τελείες, κόμματα, δύο τελείες και ερωτηματικά στο τέλος των γραμμών των στίχων. Τα ερωτηματικά, τα θαυμαστικά, οι πολύ τελείες, τα εισαγωγικά και οι παρενθέσεις διατηρούνται Μέγεθος κειμένου Μέγεθος κειμένου των στίχων Καθολική χρονική μετατόπιση diff --git a/patches/src/main/resources/addresources/values-el-rGR/youtube/strings.xml b/patches/src/main/resources/addresources/values-el-rGR/youtube/strings.xml index 340469be5c..1718cd61d0 100644 --- a/patches/src/main/resources/addresources/values-el-rGR/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-el-rGR/youtube/strings.xml @@ -393,6 +393,9 @@ Second \"item\" text" Σύνδεσμοι αγορών στην περιγραφή βίντεο Διαφημίσεις βίντεο Προωθήσεις για απόκτηση YouTube Premium + + + Φόρτωση… Τρέχον κανάλι: Προσθήκη στη λίστα επιτρεπόμενων @@ -409,6 +412,9 @@ Second \"item\" text" Αποτελέσματα αναζήτησης καναλιών Δεν βρέθηκαν αποτελέσματα σε αυτό το κανάλι Δεν ήταν δυνατή η αναζήτηση σε αυτό το κανάλι + Βίντεο + Αναρτήσεις + Δεν βρέθηκαν αναρτήσεις σε αυτό το κανάλι που να ταιριάζουν Συνάφεια Περισσότερες προβολές Λιγότερες προβολές @@ -494,6 +500,9 @@ Second \"item\" text" Βίντεο Ήχος Όλες οι επιλογές + Εμφάνιση λήψης στο μενού Shorts + Προσθέτει ένα εξωτερικό μενού λήψης στο παίκτη Shorts + Λήψη Μετάφραση φωνής Διαβάστε τις λεζάντες του βίντεο δυνατά χρησιμοποιώντας το text-to-speech της συσκευής, συγχρονισμένα με την αναπαραγωγή @@ -1106,9 +1115,6 @@ Second \"item\" text" "Επαναφέρει την παλιά διάταξη του παίκτη Περιορισμός: Εάν η επιλογή 'Επαναφορά παλαιού γραμμής εργαλείων βίντεο' είναι απενεργοποιημένη, η επικάλυψη 'Περισσότερα βίντεο' ενδέχεται να μην είναι διαθέσιμη" - "Η ενεργοποίηση αυτής της ρύθμισης ενδέχεται να καταστρέψει ορισμένα χαρακτηριστικά της διάταξης του παίκτη, όπως η ενότητα σχολίων ή τα \"Περισσότερα βίντεο\" που εμφανίζονται κάτω από τον παίκτη. - -Η διάταξη tablet δεν υποστηρίζεται πλέον από το YouTube και είναι μερικώς ή πλήρως κατεστραμμένη στις πρόσφατες εκδόσεις του YouTube. Για μια λειτουργική διάταξη tablet, εφαρμόστε το patch στο YouTube 20.21.37" 21.11.48 - Επαναφορά παλιάς συμπεριφοράς λεζάντων 20.39.41 - Επαναφορά παλιών σωζημάτων σε αναπαραγωγή @@ -1611,4 +1617,17 @@ AVC (H.264) Δεν βρέθηκαν σειρές Επεισόδιο χωρίς τίτλο Μη διαθέσιμο επεισόδιο + + Σχόλια Niconico + Εμφάνιση της ζωντανής συνομιλίας και της επανάληψης της συνομιλίας ως σχόλια που σκρολάρουν πάνω από το βίντεο + Εμφανίζει ένα κουμπί επικάλυψης στο πρόγραμμα αναπαραγωγής βίντεο. Πατήστε για να εμφανίσετε ή να 감κρύψετε τα σχόλια, πατήστε και κρατήστε για γρήγορες ρυθμίσεις + Μέγεθος γραμματοσειράς σχολίου + Μέγεθος κειμένου των σχολίων (12-40 sp) + Διαφάνεια σχολίου + Διαφάνεια του κειμένου και του φόντου του σχολίου + Διάρκεια διαδρομής + Πόσα δευτερόλεπτα χρειάζονται τα σχόλια για να διασχίσουν την οθόνη. Χαμηλότερες τιμές κινούνται ταχύτερα + Μέγιστο μήκος σχολίου + Αποκρύπτει σχόλια με περισσότερους χαρακτήρες από αυτόν. Κάθε emoji μετράται ως ένας χαρακτήρας + Χωρίς όριο diff --git a/patches/src/main/resources/addresources/values-es-rES/music/strings.xml b/patches/src/main/resources/addresources/values-es-rES/music/strings.xml index fa03ac5902..f5d3e33c5d 100644 --- a/patches/src/main/resources/addresources/values-es-rES/music/strings.xml +++ b/patches/src/main/resources/addresources/values-es-rES/music/strings.xml @@ -555,8 +555,29 @@ Cookies requeridas: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPIS Muestra la línea actual en la pantalla de bloqueo, Android Auto y Bluetooth. Requiere Android 11 o posterior Mostrar letras en el reproductor en miniatura Muestra la línea actual en el reproductor reducido, con la pista movida al subtítulo - Orden de visualización de artista y título - Alterna el orden de visualización de artista y título en el reproductor en miniatura y MediaSession. Activado: \"Artista - Título\"; desactivado: \"Título - Artista\". No afecta a la búsqueda de letras + Formato de título de canción/artista + Formato de la pista que se muestra con la letra en el miniplayer y en MediaSession. {t} es el título, {a} es el artista y {v} es la versión, como Acústico, cuando la fuente de la letra proporciona una. El texto entre dos dos puntos solo se muestra cuando cada variable en su interior tiene un valor; escriba \\: para dos puntos simples. Cada línea se muestra por turnos, avanzando cada vez que cambia la letra + Formato de línea de letra + Formato de la línea de letra en el miniplayer y en MediaSession. {lrc} es la línea y {n} es cuántas veces seguidas se canta. El texto entre dos puntos solo se muestra cuando la línea se repite. Use \\: para dos puntos + El formato debe contener {lrc} + Plantilla de línea del artista y temporización de la animación + Tiempo mínimo en pantalla (ms) + Tiempo máximo en pantalla (ms) + Solo para animaciones multilínea. Mantenga cada fotograma durante al menos el tiempo mínimo. Al llegar al máximo, avance la línea del artista incluso si la letra permanece igual. + El mínimo debe ser 0 o superior. El máximo debe ser superior a 0 y al menos el valor del mínimo. + Límites de longitud de línea + Límites de línea de la letra y del artista, y peso de caracteres de ancho completo + "Límite de línea de la letra +Divida las letras más largas en mitades equilibradas. 0 desactiva la división." + "Límite de líneas del artista +Divide el formato de pista poblado en marcos cíclicos. 0 desactiva la división." + "Peso de caracteres de ancho completo +Se aplica a ambos límites. A 1.75, cuatro caracteres de ancho completo cuentan como 7." + Introduce límites de números enteros del 0 al 10000 y un peso de caracteres mayor que 0 + Interludio instrumental + Se muestra en el miniplayer y en la MediaSession durante un interludio instrumental. Cada línea es un marco de animación. {t} es el título, {a} es el artista y {lrc} es la última línea de la letra + Desfase de tiempo del miniplayer y de la MediaSession + Desplaza el momento en que se muestra la siguiente línea en el miniplayer y en MediaSession. Usa un valor negativo si las líneas aparecen tarde, por ejemplo, en la pantalla de bloqueo Filtros Regex de metadatos Expresión regular aplicada a títulos, artistas y álbumes antes de buscar las letras y al puntuar si dos títulos coinciden. Déjala vacía para desactivar ambas. La predeterminada elimina términos comunes como \"Vídeo oficial\", \"Remaster\", \"【插曲】\" y \"- Topic\" @@ -564,6 +585,10 @@ Cookies requeridas: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPIS Expresión regular aplicada solo al texto original de las letras (no a traducciones ni romanizaciones). Déjala vacía para desactivarla Marcadores de línea de información Si las letras se filtran incorrectamente al final, ajusta o borra esto. Marcadores separados por comas: las líneas que contienen un marcador seguido de dos puntos se muestran como información de créditos al final. Déjalo vacío para desactivarlo + Mayúscula en la primera letra + Pone en mayúscula la primera letra de cada línea de la letra + Eliminar puntuación final + Elimina puntos, comas, dos puntos y puntos y coma al final de las líneas de la letra. Se mantienen los signos de interrogación, de exclamación, puntos suspensivos, comillas y paréntesis Tamaño del texto Tamaño del texto de las letras Desfase de tiempo global diff --git a/patches/src/main/resources/addresources/values-es-rES/youtube/strings.xml b/patches/src/main/resources/addresources/values-es-rES/youtube/strings.xml index 0ee1d8877e..60256fe35a 100644 --- a/patches/src/main/resources/addresources/values-es-rES/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-es-rES/youtube/strings.xml @@ -393,6 +393,9 @@ Los reportes son públicos y se revisan antes de añadir un canal a una lista. L Ocultar enlaces de compras Ocultar anuncios de vídeo Ocultar promociones de YouTube Premium + + + Cargando… Canal actual: Añadir a la lista de excepción @@ -409,6 +412,9 @@ Los reportes son públicos y se revisan antes de añadir un canal a una lista. L Resultados de búsqueda del canal No se encontró nada en este canal No se pudo buscar en este canal + Vídeos + Publicaciones + No hay publicaciones en este canal que coincidan Relevancia Más vistos Menos vistos @@ -494,6 +500,9 @@ Después de abrir por primera vez un vídeo con el botón «Preguntar», la func Vídeo Audio Todas las opciones + Mostrar descarga en el menú de Shorts + Añade un elemento de menú desplegable del descargador externo al reproductor de Shorts + Descargar Traducción con voz Lee en voz alta los subtítulos del vídeo mediante la síntesis de texto a voz del dispositivo, sincronizada con la reproducción @@ -1121,9 +1130,6 @@ Diseño para automóviles "Restaura el diseño antiguo del reproductor Limitación: si la opción 'Restaurar antigua barra de acciones de vídeo' está desactivada, es posible que la superposición 'Más vídeos' no esté disponible" - "Activar esto puede afectar algunas funciones del diseño del reproductor, como la sección de comentarios o \"Más videos\" que se muestran debajo del reproductor. - -El diseño de tableta ya no es compatible con YouTube y está parcial o totalmente roto en las versiones recientes de YouTube. Para obtener un diseño de tableta funcional, aplica el parche a YouTube 20.21.37" 21.11.48 - Restaurar comportamiento antiguo de los subtítulos 20.39.41 - Restaurar guardado antiguo de lista de reproducción @@ -1626,4 +1632,17 @@ La grabación automática es opcional. Se detiene en modo incógnito o cuando ca No se encontraron series Episodio sin título Episodio no disponible + + Comentarios de Niconico + Muestra el chat en vivo y la reproducción del chat como comentarios que se desplazan por el vídeo + Muestra un botón superpuesto en el reproductor de vídeo. Púlsalo para mostrar u ocultar los comentarios; mantenlo pulsado para acceder a los ajustes rápidos + Tamaño de fuente de los comentarios + Tamaño del texto de los comentarios (12-40 sp) + Opacidad de los comentarios + Opacidad del texto y los fondos de los comentarios + Duración del recorrido + Cuántos segundos tardan los comentarios en cruzar la pantalla. Los valores más bajos se mueven más rápido + Longitud máxima del comentario + Oculta los comentarios con más caracteres que este. Cada emoji cuenta como un carácter + Sin límite diff --git a/patches/src/main/resources/addresources/values-et-rEE/youtube/strings.xml b/patches/src/main/resources/addresources/values-et-rEE/youtube/strings.xml index 9079108272..70ce03fc67 100644 --- a/patches/src/main/resources/addresources/values-et-rEE/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-et-rEE/youtube/strings.xml @@ -51,6 +51,8 @@ Second \"item\" text" + + @@ -224,4 +226,5 @@ Second \"item\" text" + diff --git a/patches/src/main/resources/addresources/values-eu-rES/youtube/strings.xml b/patches/src/main/resources/addresources/values-eu-rES/youtube/strings.xml index 9079108272..70ce03fc67 100644 --- a/patches/src/main/resources/addresources/values-eu-rES/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-eu-rES/youtube/strings.xml @@ -51,6 +51,8 @@ Second \"item\" text" + + @@ -224,4 +226,5 @@ Second \"item\" text" + diff --git a/patches/src/main/resources/addresources/values-fa-rIR/music/strings.xml b/patches/src/main/resources/addresources/values-fa-rIR/music/strings.xml index 8f2ae9c184..a5c29499a3 100644 --- a/patches/src/main/resources/addresources/values-fa-rIR/music/strings.xml +++ b/patches/src/main/resources/addresources/values-fa-rIR/music/strings.xml @@ -555,8 +555,29 @@ Second \"item\" text" خط فعلی را در صفحه قفل، Android Auto و Bluetooth نشان می‌دهد. نیازمند Android 11 یا جدیدتر است نمایش متن ترانه در پخش‌کنندهٔ کوچک‌شده خط فعلی را در پخش‌کننده کوچک نشان می‌دهد، با حرکت ترانه به زیرنویس - ترتیب نمایش هنرمند و عنوان - ترتیب نمایش هنرمند و عنوان را در پخش‌کنندهٔ کوچک‌شده و MediaSession تغییر دهید. روشن: «هنرمند - عنوان» و خاموش: «عنوان - هنرمند». روی جستجوی متن ترانه تأثیر نمی‌گذارد + قالب عنوان آهنگ/هنرمند + قالب قطعه‌ای که همراه با متن ترانه در پخش‌کننده کوچک‌شده و MediaSession نمایش داده می‌شود. {t} عنوان، {a} هنرمند و {v} نسخه است، مانند Acoustic، در صورتی که منبع متن ترانه آن را ارائه دهد. متن بین دو دو-نقطه تنها زمانی نمایش داده می‌شود که هر متغیر داخل آن دارای مقدار باشد؛ برای دو-نقطه معمولی از \\: استفاده کنید. هر خط به نوبت نمایش داده می‌شود و با تغییر متن ترانه، به خط بعدی می‌رود + قالب خط متن ترانه + قالب خط متن ترانه در پخش‌کننده کوچک‌شده و MediaSession. {lrc} همان خط و {n} تعداد دفعاتی است که آن خط به طور متوالی خوانده می‌شود. متن بین دو-نقطه‌ها تنها زمانی نمایش داده می‌شود که خط تکرار شود. برای دو-نقطه از \\: استفاده کنید + قالب باید شامل {lrc} باشد + قالب خط هنرمند و زمان‌بندی انیمیشن + حداقل زمان نمایش (ms) + حداکثر زمان نمایش (ms) + فقط برای انیمیشن‌های چندخطی. هر فریم را حداقل به اندازه زمان مینیمم نگه دارید. در زمان حداکثر، حتی اگر متن ترانه ثابت بماند، خط هنرمند جلو می‌رود. + حداقل باید 0 یا بیشتر باشد. حداکثر باید بیشتر از 0 و حداقل برابر با مقدار مینیمم باشد. + محدودیت‌های طول خط + محدودیت‌های خط ترانه و هنرمند، و وزن نویسه‌های تمام-عرض + "محدودیت خط ترانه +ترانه‌های بلندتر را به دو نیمه متوازن تقسیم کنید. 0 تقسیم‌بندی را غیرفعال می‌کند." + "محدودیت خطوط هنرمند +قالب ترک پر شده را به فریم‌های چرخان تقسیم کنید. مقدار 0 تقسیم‌بندی را غیرفعال می‌کند." + "وزن کاراکترهای تمام-عرض +برای هر دو محدودیت اعمال می‌شود. در مقدار 1.75، چهار کاراکتر تمام-عرض به عنوان 7 محاسبه می‌شوند." + محدودیت‌ها را به صورت اعداد صحیح از 0 تا 10000 و وزن کاراکتر را بیشتر از 0 وارد کنید + وقفه سازی + در پخش‌کننده کوچک‌شده و MediaSession در طول وقفه سازی نمایش داده می‌شود. هر خط یک فریم انیمیشن است. {t} عنوان، {a} هنرمند و {lrc} آخرین خط متن ترانه است + آفست زمانی Miniplayer و MediaSession + زمان نمایش خط بعدی در پخش‌کننده کوچک‌شده و MediaSession را تغییر می‌دهد. اگر خطوط با تأخیر نمایش داده می‌شوند، برای مثال در صفحه قفل، از یک مقدار منفی استفاده کنید فیلترها عبارت باقاعدهٔ فراداده عبارت باقاعده روی عنوان‌ها، هنرمندان و آلبوم‌ها قبل از جستجوی متن ترانه و هنگام امتیازدهی به تطابق دو عنوان اعمال می‌شود. برای غیرفعال‌کردن هر دو خالی بگذارید. پیش‌فرض موارد رایج اضافی مثل «Official Video»، «Remaster»، «【插曲】» و «- Topic» را حذف می‌کند @@ -564,6 +585,10 @@ Second \"item\" text" عبارت باقاعده فقط روی متن اصلی ترانه اعمال می‌شود (نه ترجمه‌ها یا لاتین‌نویسی‌ها). برای غیرفعال‌کردن خالی بگذارید نشانگرهای خط اطلاعات اگر متن ترانه به‌اشتباه به پایین فیلتر شد، این را تنظیم یا پاک کنید. نشانگرهای جداشده با ویرگول: خطوطی که شامل نشانگری دنبال‌شده با دونقطه هستند به‌عنوان اطلاعات اعتبار در پایین نمایش داده می‌شوند. برای غیرفعال‌کردن خالی بگذارید + بزرگ کردن حرف اول + حرف اول هر خط از متن ترانه را بزرگ می‌کند + حذف علائم نگارشی انتهایی + نقطه، ویرگول، دو نقطه و نقطه-ویرگول را از انتهای خطوط ترانه حذف می‌کند. علامت‌های سؤال، تعجب، سه نقطه، نقل‌قول‌ها و پرانتزها حفظ می‌شوند اندازهٔ متن اندازهٔ متن آهنگ جبران زمان سراسری diff --git a/patches/src/main/resources/addresources/values-fa-rIR/youtube/strings.xml b/patches/src/main/resources/addresources/values-fa-rIR/youtube/strings.xml index 04f5ca6b73..6fe9a5bdef 100644 --- a/patches/src/main/resources/addresources/values-fa-rIR/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-fa-rIR/youtube/strings.xml @@ -393,6 +393,9 @@ Second \"item\" text" پنهان‌کردن پیوندهای خرید پنهان‌کردن تبلیغات ویدئویی پنهان‌کردن تبلیغ‌های YouTube Premium + + + در حال بارگیری… کانال فعلی: افزودن به فهرست مجاز @@ -409,6 +412,9 @@ Second \"item\" text" نتایج جستجو در کانال چیزی در این کانال پیدا نشد نمی‌توان در این کانال جست‌وجو کرد + ویدئوها + پست‌ها + هیچ پستی در این کانال مطابقت ندارد مرتبط‌ترین بیشترین بازدید کمترین بازدید @@ -494,6 +500,9 @@ Second \"item\" text" ویدئو صدا همه گزینه‌ها + نمایش دانلود در منوی Shorts + افزودن گزینه دانلود خارجی در منوی کشویی پخش کننده Shorts + دانلود ترجمهٔ صوتی زیرنویس‌های ویدئو را با تبدیل متن به گفتار روی دستگاه و همگام با پخش، بلند می‌خواند @@ -1121,9 +1130,6 @@ Second \"item\" text" "طرح‌بندی قدیمی پخش‌کننده را بازیابی می‌کند محدودیت: اگر «بازیابی نوار عملیات قدیمی ویدئو» خاموش باشد، ممکن است روی‌هم‌افتادگی «ویدئوهای بیشتر» در دسترس نباشد" - "فعال کردن این گزینه ممکن است برخی از ویژگی‌های چیدمان پلیر، مانند بخش نظرات یا \"ویدیوهای بیشتر\" که در پایین پلیر نمایش داده می‌شوند را مختل کند. - -چیدمان تبلت دیگر توسط YouTube پشتیبانی نمی‌شود و در نسخه‌های اخیر YouTube به طور جزئی یا کامل خراب شده است. برای داشتن یک چیدمان تبلت کاربردی، نسخه 20.21.37 YouTube را پچ کنید" 21.11.48 — بازگردانی رفتار قبلی کپشن‌ها 20.39.41 - بازیابی ذخیره در فهرست پخش @@ -1626,4 +1632,17 @@ AVC (H.264) هیچ سریالی پیدا نشد قسمت بی‌عنوان قسمت غیرموجود + + دیدگاه‌های Niconico + نمایش چت زنده و بازپخش چت به صورت دیدگاه‌هایی که روی ویدیو حرکت می‌کنند + یک دکمه روی پخش‌کننده ویدیو نمایش می‌دهد. برای نمایش یا پنهان کردن دیدگاه‌ها ضربه بزنید و برای تنظیمات سریع لمس کرده و نگه دارید + اندازه فونت دیدگاه‌ها + اندازه متن دیدگاه‌ها (12-40 sp) + شفافیت دیدگاه‌ها + شفافیت متن و پس‌زمینه‌ی نظرات + مدت زمان پیمایش + تعداد ثانیه‌هایی که نظرات برای عبور از صفحه نیاز دارند. مقادیر کمتر، سرعت را بیشتر می‌کنند + حداکثر طول نظر + نظراتی که کاراکترهای بیشتری از این مقدار دارند را مخفی می‌کند. هر ایموجی به عنوان یک کاراکتر در نظر گرفته می‌شود + بدون محدودیت diff --git a/patches/src/main/resources/addresources/values-fi-rFI/music/strings.xml b/patches/src/main/resources/addresources/values-fi-rFI/music/strings.xml index 7d90328ab8..aae94db1e2 100644 --- a/patches/src/main/resources/addresources/values-fi-rFI/music/strings.xml +++ b/patches/src/main/resources/addresources/values-fi-rFI/music/strings.xml @@ -550,8 +550,29 @@ Vaaditut evästeet: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPIS Näyttää nykyisen rivin lukitusnäytöllä, Android Autolla ja Bluetoothilla. Vaatii Android 11:n tai uudemman Näytä minisoittimen lyriikat Näyttää nykyisen rivin mini-soittimessa, jossa kappale siirretty tekstitykseen - Artistin ja kappaleen nimen näyttöjärjestys - Vaihda artistin ja kappaleen nimen näyttöjärjestystä minisoittimessa ja MediaSessionissa. PÄÄLLÄ \"Artisti - Kappale\" ja POIS \"Kappale - Artisti\" Ei vaikuta lyriikkahakuun + Kappaleen nimi/artisti-muoto + Kappaleen muoto, joka näytetään sanoitusten kanssa pienessä soittimessa ja MediaSessionissa. {t} on otsikko, {a} on artisti ja {v} on versio, kuten Acoustic, jos sanoitusten lähde antaa sellaisen. Kahden kaksoispisteen välinen teksti näytetään vain, kun jokaisella sen sisällä olevalla muuttujalla on arvo; kirjoita \\: tavallista kaksoispistettä varten. Jokainen rivi näytetään vuorotellen, ja se vaihtuu aina, kun sanoitus muuttuu + Sanoitusrivin muoto + Sanoitusrivin muoto pienessä soittimessa ja MediaSessionissa. {lrc} on rivi ja {n} on määrä, kuinka monta kertaa se lauletaan peräkkäin. Kaksoispisteiden välinen teksti näytetään vain, kun rivi toistuu. Käytä \\: kaksoispistettä varten + Muodon on sisällettävä {lrc} + Artistirivimalli ja animaation ajoitus + Miniminäyttöaika (ms) + Maksiminäyttöaika (ms) + Vain monirivisille animaatioille. Pidä kukin ruutu näkyvissä vähintään minimiajan. Maksimiajan kohdalla artistirivi vaihtuu, vaikka lyriikka pysyisi samana. + Minimin on oltava 0 tai suurempi. Maksimin on oltava suurempi kuin 0 ja vähintään minimin arvoinen. + Rivin pituuden rajoitukset + Lyriikka- ja artistirivien rajoitukset sekä täyslevymerkkien painotus + "Lyriikkarivin rajoitus +Jaa pidemmät lyriikat tasapainoiseksi puolikkaiksi. 0 poistaa jaon." + "Artistirivin rajat +Jaa täytetty raidan muoto kiertäviin kehyksiin. 0 poistaa jaon." + "Täyslevyisten merkkien paino +Koskee molempia rajoja. Arvolla 1,75 neljä täyslevyistä merkkiä lasketaan seitsemäksi (7)." + Syötä kokonaislukurajat väliltä 0–10000 ja merkkipaino, joka on suurempi kuin 0 + Instrumentaalivälisoitto + Näytetään minisoittimessa ja MediaSessionissa instrumentaalivälisoiton aikana. Jokainen rivi on yksi animaatiokehys. {t} on otsikko, {a} on artisti ja {lrc} on viimeinen sanoitusrivi + Minisoittimen ja MediaSessionin ajoitusoffset + Siirtää aikaa, jolloin seuraava rivi näytetään Miniplayerissa ja MediaSessionissa. Käytä negatiivista arvoa, jos rivit ilmestyvät myöhässä, esimerkiksi lukitusnäytöllä Suodattimet Metadatan säännöllinen lauseke Säännöllinen lauseke, jota sovelletaan kappaleiden nimiin, artisteihin ja albumeihin ennen sanoitusten hakua ja arvioitaessa, vastaavatko kaksi kappaleen nimeä toisiaan. Jätä tyhjäksi poistaaksesi molemmat käytöstä. Oletusarvo poistaa yleistä hälyä, kuten \"Official Video\", \"Remaster\", \"【插曲】\" ja \"- Topic\" @@ -559,6 +580,10 @@ Vaaditut evästeet: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPIS Säännöllinen lauseke, jota sovelletaan vain alkuperäiseen lyriikkatekstiin (ei käännöksiin tai romanisointeihin). Jätä tyhjäksi poistaaksesi käytöstä Tietorivien merkinnät Jos lyriikat suodatetaan virheellisesti alareunaan, säädä tai tyhjennä tämä. Pilkulla erotetut merkinnät: rivit, jotka sisältävät merkinnän ja sen jälkeen kaksoispisteen, näytetään tekijätietoina alareunassa. Jätä tyhjäksi poistaaksesi käytöstä + Kirjoita ensimmäinen kirjain suurella + Kirjoittaa jokaisen lyriikkarivin ensimmäisen kirjaimen suurella + Poista loppuvälimerkit + Poistaa pisteet, pilkut, kaksoispisteet ja puolipisteet lyriikkarivien lopusta. Kysymysmerkit, huutomerkit, kolme pistettä, lainausmerkit ja sulkeet säilytetään Tekstin koko Sanoitusten tekstin koko Globaali ajoitussiirtymä diff --git a/patches/src/main/resources/addresources/values-fi-rFI/youtube/strings.xml b/patches/src/main/resources/addresources/values-fi-rFI/youtube/strings.xml index 3203cd1e04..775ed782aa 100644 --- a/patches/src/main/resources/addresources/values-fi-rFI/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-fi-rFI/youtube/strings.xml @@ -393,6 +393,9 @@ Raportit ovat julkisia ja ne tarkistetaan ennen kuin kanava lisätään listaan. Piilota ostoslinkit Piilota videomainokset Piilota YouTube Premium -mainokset + + + Ladataan… Nykyinen kanava: Lisää valkolistalle @@ -409,6 +412,9 @@ Raportit ovat julkisia ja ne tarkistetaan ennen kuin kanava lisätään listaan. Kanavahaun tulokset Tältä kanavalta ei löytynyt mitään Tätä kanavaa ei voitu hakea + Videot + Postaukset + Tältä kanavalta ei löytynyt vastaavia postauksia Osuvuus Eniten katsottu Vähiten katsottu @@ -494,6 +500,9 @@ Kerran kun video \"Kysy\"-painikkeella avataan, ominaisuus toimii kaikkien video Video Ääni Kaikki asetukset + Näytä lataus Shorts-valikossa + Lisää ulkoisen lataussovelluksen ponnahdusvalikkokohdan Shorts-soittoehdotukseen + Lataa Ääniratateksti Lue videon tekstitykset ääneen laitteen tekstistä puheeksi -toiminnolla, synkronoitu toiston kanssa @@ -1121,9 +1130,6 @@ Auton asettelu "Palauttaa vanhan soittimen asettelun Rajoitus: Jos \"Palauta vanha videotoimintopalkki\" on poissa käytöstä, \"Lisää videoita\" -peittokuvio ei välttämättä ole käytettävissä" - "Tämän ottaminen käyttöön voi rikkoa joitakin soittimen asettelun ominaisuuksia, kuten kommenttiosion tai soittimen alapuolella näkyvät \"Lisää videoita\" -videot. - -YouTube ei tue enää tablettiasettelua, ja se on osittain tai kokonaan rikki uusimmissa YouTube-versioissa. Toimivan tablettiasettelun saamiseksi asenna päivitys YouTube-versioon 20.21.37" 21.11.48 - Palauta vanha tekstitysten käyttäytyminen 20.39.41 - Palauta vanhat tallenteet soittimeen @@ -1626,4 +1632,17 @@ Automaattinen tallennus on valinnaista. Se pysähtyy incognito-tilissä tai tili Sarjoja ei löytynyt Nimetön jakso Ei saatavilla oleva jakso + + Niconico-kommentit + Näytä live-chat ja chatin toisto kommenteina, jotka rullaavat videon yli + Näyttää peittopainikkeen videosoittimessa. Napauta näyttääksesi tai piilottaaksesi kommentit, napauta ja pidä painettuna pika-asetuksia varten + Kommenttien kirjasinkoko + Kommenttien tekstikoko (12–40 sp) + Kommenttien läpinäkyvyys + Kommenttitekstin ja taustojen läpinäkyvyys + Keston kesto + Kuinka monta sekuntia kommenttien kulkeminen näytön yli kestää. Pienemmät arvot liikkuvat nopeammin + Maksimit kommentin pituus + Piilottaa kommentit, jotka ovat tämän merkkimäärän pidempiä. Jokainen emojimerkki lasketaan yhdeksi merkiksi + Ei rajoitusta diff --git a/patches/src/main/resources/addresources/values-fil-rPH/music/strings.xml b/patches/src/main/resources/addresources/values-fil-rPH/music/strings.xml index f513f0ae71..1b36685887 100644 --- a/patches/src/main/resources/addresources/values-fil-rPH/music/strings.xml +++ b/patches/src/main/resources/addresources/values-fil-rPH/music/strings.xml @@ -550,8 +550,29 @@ Mga kinakailangang cookie: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure Ipakita ang current line sa lock screen, Android Auto at Bluetooth. Kailangan ng Android 11 o mas baga Ipakita ang Lyrics sa Miniplayer Ipakita ang current line sa miniplayer, with the track moved to the subtitle - Pagpapakita ng Artist at Pamagat […] ON \"Artist - Title\" at OFF \"Title - Artist\" — hindi ito nakakaapekto sa paghahanap ng lyrics. - I-toggle ang pagkakasunod-sunod ng pagpapakita ng Artist at Title sa Miniplayer at MediaSession. Kapag ON: \"Artist - Title\"; kapag OFF: \"Title - Artist\". Hindi ito nakakaapekto sa paghahanap ng lyrics. + Format ng pamagat ng kanta/artist + Format ng track na ipinapakita kasama ang lyrics sa miniplayer at MediaSession. Ang {t} ay ang pamagat, ang {a} ay ang artist at ang {v} ay ang bersyon, gaya ng Acoustic, kapag nagbigay nito ang source ng lyrics. Ang text sa pagitan ng dalawang colon ay ipinapakita lamang kapag ang bawat variable sa loob nito ay may value; isulat ang \\: para sa isang plain na colon. Ang bawat linya ay ipinapakita nang sunod-sunod, at lilipat kapag nagbago ang lyric + Format ng linya ng lyric + Format ng linya ng lyrics sa miniplayer at MediaSession. Ang {lrc} ay ang linya at ang {n} ay kung ilang beses itong kinanta nang sunod-sunod. Ang text sa pagitan ng mga colon ay ipinapakita lamang kapag nauulit ang linya. Gumamit ng \\: para sa isang colon + Ang format ay dapat maglaman ng {lrc} + Template ng artist-line at timing ng animation + Minimum na screen time (ms) + Maximum na screen time (ms) + Para sa mga multiline animation lamang. Panatilihin ang bawat frame nang hindi bababa sa minimum na oras. Sa maximum, i-advance ang artist line kahit hindi nagbabago ang liriko. + Ang minimum ay dapat 0 o higit pa. Ang maximum ay dapat higit sa 0 at hindi bababa sa minimum. + Mga limitasyon sa haba ng linya + Mga limitasyon sa linya ng liriko at artist, at full-width character weight + "Limitasyon sa linya ng liriko +Hatiin ang mas mahahabang liriko sa balanseng kalahati. I-disable ang paghahati gamit ang 0." + "Limit ng linya ng artist +Hatiin ang populated track format sa mga cycling frame. I-disable ang paghahati gamit ang 0." + "Bigat ng full-width character +Nalalapat sa parehong limitasyon. Sa 1.75, ang apat na full-width character ay mabibilang bilang 7." + Maglagay ng whole-number limit mula 0 hanggang 10000 at bigat ng character na higit sa 0 + Instrumental break + Ipinapakita sa miniplayer at MediaSession habang may instrumental break. Ang bawat linya ay isang animation frame. Ang {t} ay ang pamagat, ang {a} ay ang artist at ang {lrc} ay ang huling linya ng lyrics + Timing offset ng miniplayer at MediaSession + Inililipat kung kailan ipapakita ang susunod na linya sa Miniplayer at MediaSession. Gumamit ng negatibong halaga kung huling lumalabas ang mga linya, halimbawa sa lock screen Mga Filter Regex ng filter ng lyrics Regular expression na inilalapat sa mga pamagat, artist, at album bago maghanap ng mga liriko at kapag nagmamarka kung magkatugma ang dalawang pamagat. Iwanang blangko upang i-disable ang pareho. Ang default ay nag-aalis ng karaniwang kalat tulad ng \"Official Video\", \"Remaster\", \"【插曲】\", at \"- Topic\" @@ -559,6 +580,10 @@ Mga kinakailangang cookie: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure Ang regular expression ay inilalapat lamang sa orihinal na teksto ng lyrics (hindi sa mga pagsasalin o romanisasyon). Iwanang blangko upang i-disable. Mga marker ng linya ng impormasyon Kung ang mga lyrics ay hindi wastong na-filter sa ibaba, ayusin o i-clear ito. […] Iwanang blangko upang i-disable. + Gawing malaking titik ang unang letra + Ginagawang malaking titik ang unang letra ng bawat linya ng liriko + Alisin ang huling bantas + Inaalis ang mga tuldok, kuwit, colon at semicolon sa dulo ng mga linya ng liriko. Pinananatili ang mga question mark, exclamation mark, ellipses, panipi at panaklong Laki ng teksto Laki ng teksto ng lyrics Pandaigdigang offset ng timing diff --git a/patches/src/main/resources/addresources/values-fil-rPH/youtube/strings.xml b/patches/src/main/resources/addresources/values-fil-rPH/youtube/strings.xml index 5a87eb3b2a..8f4e4dff3e 100644 --- a/patches/src/main/resources/addresources/values-fil-rPH/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-fil-rPH/youtube/strings.xml @@ -393,6 +393,9 @@ Pampubliko ang mga ulat at sinusuri ang mga ito bago idagdag ang isang channel s Itago ang mga link ng pagbili Itago ang mga patalastas sa video Itago ang mga promosyon sa YouTube Premium + + + Naglo-load… Kasalukuyang channel: Idagdag sa whitelist @@ -409,6 +412,9 @@ Pampubliko ang mga ulat at sinusuri ang mga ito bago idagdag ang isang channel s Mga resulta ng paghahanap sa channel Walang nahanap sa channel na ito Hindi ma-search ang channel na ito + Mga Video + Mga Post + Walang mga post sa channel na ito ang tumutugma Kaugnayan Pinakamaraming napanood Pinakakaunting napanood @@ -494,6 +500,9 @@ Pagkatapos buksan ang isang video na may Ask button, gagana ang feature sa lahat Video Audio Lahat ng opsyon + Ipakita ang pag-download sa Shorts menu + Nagdadagdag ng item ng external downloader flyout menu sa Shorts player + I-download Pagsasalin ng boses Basahin nang malakas ang mga caption ng video gamit ang text-to-speech sa iyong device, na naka-synchronize sa pagpapatugtog @@ -1119,9 +1128,6 @@ Layout ng automotive "Ibinabalik ang lumang layout ng player Limitasyon: Kung ang 'Ibalik ang lumang video action bar' ay naka-off, maaaring hindi magamit ang overlay na 'Higit pang mga video'" - "Ang pag-enable nito ay maaaring makasira sa ilang feature ng layout ng player, gaya ng comment section o 'Higit pang mga video' na ipinapakita sa ibaba ng player. - -Hindi na suportado ng YouTube ang tablet layout, at bahagya o ganap na itong sira sa mga kamakailang bersyon ng YouTube. Para sa isang gumaganang tablet layout, i-patch ang YouTube 20.21.37" 21.11.48 - Ibalik ang dating pag-uugali ng mga caption 20.39.41 - Ibalik ang lumang save sa playlist @@ -1624,4 +1630,17 @@ Optional ang automatic recording. Itinatigil ito sa incognito o kapag nabagyo an Walang nahanap na serye Episodong walang pamagat Episodong hindi available + + Mga komento ng Niconico + Ipakita ang live chat at chat replay bilang mga komentong gumugulong sa video + Nagpapakita ng isang overlay button sa video player. I-tap para ipakita o itago ang mga komento, i-tap at i-hold para sa mabilis na settings + Laki ng font ng komento + Laki ng teksto ng mga komento (12-40 sp) + Opacity ng komento + Opacity ng teksto at background ng komento + Tagal ng pagdaan + Ilang segundo ang ginugugol ng mga komento para tawirin ang screen. Ang mas mababang value ay mas mabilis gumalaw + Maximum na haba ng komento + Nagtatago ng mga komento na may mas maraming karakter kaysa dito. Bawat emoji ay itinuturing na isang karakter. + Walang limitasyon diff --git a/patches/src/main/resources/addresources/values-fr-rFR/music/strings.xml b/patches/src/main/resources/addresources/values-fr-rFR/music/strings.xml index cf1f200a9f..7610721258 100644 --- a/patches/src/main/resources/addresources/values-fr-rFR/music/strings.xml +++ b/patches/src/main/resources/addresources/values-fr-rFR/music/strings.xml @@ -555,8 +555,29 @@ Cookies nécessaires : SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PA Affiche la ligne en cours sur l\'écran de verrouillage, Android Auto et Bluetooth. Nécessite Android 11 ou une version ultérieure Afficher les paroles dans le mini-lecteur Affiche la ligne en cours dans le mini-lecteur, avec le titre déplacé vers le sous-titre - Ordre d\'affichage de l\'Artiste et du Titre - Basculer l\'ordre d\'affichage de l\'Artiste et du Titre dans le mini-lecteur et MediaSession. ACTIVÉE : \"Artiste - Titre\" et DÉSACTIVÉE : \"Titre - Artiste\" N\'affecte pas la recherche de paroles + Format titre de la chanson/artiste + Format du morceau affiché avec les paroles dans le mini-lecteur et la MediaSession. {t} est le titre, {a} est l\'artiste et {v} est la version, comme \'Acoustique\', lorsque la source des paroles en fournit une. Le texte entre deux deux-points est affiché uniquement lorsque chaque variable qu\'il contient a une valeur ; écrivez \\: pour un deux-points simple. Chaque ligne est affichée à tour de rôle, en passant à la suivante à chaque changement de paroles + Format de la ligne de paroles + Format de la ligne de paroles dans le mini-lecteur et la MediaSession. {lrc} est la ligne et {n} est le nombre de fois qu\'elle est chantée à la suite. Le texte entre deux-points est affiché uniquement lorsque la ligne est répétée. Utilisez \\: pour un deux-points + Le format doit contenir {lrc} + Modèle de ligne pour l\'artiste et durée de l\'animation + Temps d\'affichage minimal à l\'écran (ms) + Temps d\'affichage maximal à l\'écran (ms) + Uniquement pour les animations multilignes. Gardez chaque image pendant au moins le temps minimum. Au maximum, passez à la ligne de l\'artiste même si les paroles restent les mêmes. + Le minimum doit être égal ou supérieur à 0. Le maximum doit être supérieur à 0 et supérieur ou égal au minimum. + Limites de longueur des lignes + Limites des lignes de paroles et d\'artiste, et pondération des caractères pleine largeur + "Limite des lignes de paroles +Divise les paroles trop longues en deux parties équilibrées. 0 désactive la division." + "Limite des lignes d'artiste +Divise le format du morceau renseigné en séquences qui défilent. 0 désactive la division." + "Pondération des caractères pleine largeur +S'applique aux deux limites. À 1,75, quatre caractères pleine largeur comptent pour 7." + Entrez des limites en nombres entiers de 0 à 10000 et un poids de caractère supérieur à 0 + Pause instrumentale + Affiché dans le mini-lecteur et la MediaSession pendant une pause instrumentale. Chaque ligne est une image de l\'animation. {t} est le titre, {a} est l\'artiste et {lrc} est la dernière ligne de paroles + Décalage temporel du mini-lecteur et de la MediaSession + Décale le moment où la ligne suivante s\'affiche dans le mini-lecteur et la MediaSession. Utilisez une valeur négative si les lignes s\'affichent en retard, par exemple sur l\'écran de verrouillage Filtres Expression régulière des métadonnées Expression régulière appliquée aux titres, artistes, et albums avant la recherche des paroles et lors de l\'évaluation de la correspondance entre deux titres. Laissez vide pour désactiver les deux fonctions. Par défaut supprime les éléments superflus courants comme \"Official Video\", \"Remaster\", \"【插曲】\", et \"- Topic\" @@ -564,6 +585,10 @@ Cookies nécessaires : SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PA Expression régulière appliquée uniquement au texte des paroles originales (pas aux traductions ni aux romanisations). Laissez vide pour désactiver Marqueurs de lignes d\'informations Si certaines paroles sont incorrectement filtrées vers le bas, ajustez ou effacez ceci. Marqueurs séparés par des virgules : les lignes contenant un marqueur suivi de deux-points sont affichées comme informations de crédit en bas. Laissez vide pour désactiver + Mettre la première lettre en majuscule + Met la première lettre de chaque ligne de paroles en majuscule + Supprimer la ponctuation finale + Supprime les points, les virgules, les deux-points et les points-virgules à la fin des lignes de paroles. Les points d\'interrogation, les points d\'exclamation, les points de suspension, les guillemets et les parenthèses sont conservés Taille du texte Taille du texte des paroles Décalage temporel global diff --git a/patches/src/main/resources/addresources/values-fr-rFR/youtube/strings.xml b/patches/src/main/resources/addresources/values-fr-rFR/youtube/strings.xml index 4ef9df3d65..d8a6237638 100644 --- a/patches/src/main/resources/addresources/values-fr-rFR/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-fr-rFR/youtube/strings.xml @@ -393,6 +393,9 @@ Les signalements sont publics et sont examinés avant qu'une chaîne ne soit ajo Masquer les liens de shopping Masquer les publicités vidéo Masquer les promotions YouTube Premium + + + Chargement… Chaîne actuelle : Ajouter à la liste blanche @@ -409,6 +412,9 @@ Les signalements sont publics et sont examinés avant qu'une chaîne ne soit ajo Résultats de recherche de chaînes Aucun résultat trouvé sur cette chaîne Impossible d\'effectuer une recherche sur cette chaîne + Vidéos + Posts + Aucun post sur cette chaîne ne correspond Pertinence Les plus vues Les moins vues @@ -494,6 +500,9 @@ Après avoir ouvert une fois une vidéo avec le bouton 'Posez une question', la Vidéo Audio Toutes les options + Afficher le téléchargement dans le menu Shorts + Ajoute un élément de menu déroulant de l\'extranet de téléchargement au lecteur Shorts + Télécharger Traduction en voix off Lisez les sous-titres de la vidéo à voix haute à l\'aide de la synthèse vocale sur l\'appareil, synchronisée avec la lecture @@ -1122,9 +1131,6 @@ Mise en page pour automobile "Restaure l'ancienne mise en page du lecteur Limitation : Si 'Restaurer l'ancienne barre d'actions vidéo' est désactivé, l'overlay 'Plus de vidéos' peut ne pas être disponible" - "L'activation de cette option peut perturber certaines fonctionnalités de mise en page du lecteur, telles que la section des commentaires ou 'Plus de vidéos' affichée sous le lecteur. - -La mise en page pour tablette n'est plus prise en charge par YouTube et est partiellement ou totalement défectueuse dans les versions récentes de YouTube. Pour une mise en page pour tablette fonctionnelle, patchez YouTube 20.21.37" 21.11.48 – Rétabli l\'ancien comportement des sous-titres 20.39.41 - Restaure l\'ancien enregistrement dans la playlist @@ -1627,4 +1633,17 @@ L'enregistrement automatique est facultatif. Il s'arrête en navigation privée Aucune série trouvée Épisode sans titre Épisode indisponible + + Commentaires Niconico + Affiche le chat en direct et le replay du chat sous forme de commentaires qui défilent sur la vidéo + Affiche un bouton overlay dans le lecteur vidéo. Appuyez pour afficher ou masquer les commentaires, appuyez longuement pour accéder aux paramètres rapides + Taille de la police des commentaires + Taille du texte des commentaires (12-40 sp) + Opacité des commentaires + Opacité du texte et des arrière-plans des commentaires + Durée du trajet + Nombre de secondes que mettent les commentaires pour traverser l\'écran. Des valeurs plus basses augmentent la vitesse + Longueur maximale du commentaire + Masque les commentaires comportant plus de caractères que cette valeur. Chaque emoji compte pour un caractère + Aucune limite diff --git a/patches/src/main/resources/addresources/values-ga-rIE/music/strings.xml b/patches/src/main/resources/addresources/values-ga-rIE/music/strings.xml index 1afdd76bc8..75d665e7e3 100644 --- a/patches/src/main/resources/addresources/values-ga-rIE/music/strings.xml +++ b/patches/src/main/resources/addresources/values-ga-rIE/music/strings.xml @@ -555,8 +555,29 @@ Fianáin riachtanacha: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PA Taispeánann sé an líne reatha ar an scáileán glasála, ar Android Auto agus trí Bluetooth. Teastaíonn Android 11 nó níos nuaí Taispeáin liricí an mhionimreora Taispeánann sé an líne reatha sa mhionsheinnteoir, agus an rian bogtha go dtí an fotheideal - Ord taispeána an ealaíontóra agus an teidil - Athraigh ord taispeána an Ealaíontóra agus an Teidil sa Mhionimreoir agus sa MediaSession. AR siúl: \"Ealaíontóir - Teideal\"; AS feidhm: \"Teideal - Ealaíontóir\". Ní dhéanann sé difear do chuardach na bhfocal + Format teideal amhráin/ealaíontóra + Format an riana a thaispeántar leis na liricí sa Miniplayer agus sa MediaSession. Is é {t} an teideal, is é {a} an t-artist agus is é {v} an leagan, mar shampla Acoustic, nuair a sholáthraíonn an t-uairceann liricí é. Ní thaispeántar téacs idir beirt gcolóin ach amháin nuair a bhíonn luach ag gach hathraí istigh ann; scríobh \\: le haghaidh colóin shimplí. Taispeántar gach líne in aice lena chéile, ag bogadh air gach uair a athraíonn an liric + Format líne liricí + Format na líne liricí sa Miniplayer agus sa MediaSession. Is é {lrc} an líne agus is é {n} an líon uair a chanfar i ndiaidh a chéile. Ní thaispeántar téacs idir colóin ach amháin nuair a athchríonnann an líne. Úsáid \\: le haghaidh colóin + Caithfidh an format {lrc} a chomhሌir + Teampláin líne ealaíontóra agus timing animationa + Minimum ama scréine (ms) + Maximum ama scréine (ms) + Do animationa il-líne amháin. Coinnigh gach fréama arís an t-am íosaithe. Ag an íomhánú, sleamhnaigh líne an ealaíontóra fiú nuair a fhanann an liricneas an cineál céanna. + Caithfidh an íosaithe a bheith 0 nó níos mó. Caithfidh an íomhánú a bheith níos mó ná 0 agus ar a laghad mar an íosaithe. + Teorainneacha fad líne + Teorainneacha líne liricneasa agus ealaíontóra, agus meáchan caractair lánleathnasaigh + "Teorainneach líne liricneasa +Rogaigh liricneasa níos faide ina leathanaí cothrom. Díallaíonn 0 an rogha rothaireachta." + "Cuirteas líne an ealaíontóra +Scail formáid rian pobuailte ina fhreamaí rothúla. Dícheistíonn 0 an scailte." + "Meáchan carachtair lánleathas +Cuirtear leis an dá chuirteas. Ag 1.75, cunann ceithre carachtar lánleathas mar 7." + Cuir isteach cuirteas uimhireán ó 0 go 10000 agus meáchan carachtair níos mó ná 0 + Cuairt uirlise + Taispeánfar sa mhinipléire agus sa Session Meán le linn cuairte uirlise. Is freama animation amháin gach líne. Is é {t} an teideal, is é {a} an ealaíontóir agus is í {lrc} an líne lirice is déanaí + Oifset timing an mhinipléire agus na Session Meán + Aistriaíonn é an am nuair a thaispeántaíonn an chéad líne eile sa Miniplayer agus sa MediaSession. Úsáid luach negativity má thaispeántaíonn líneanna go mall, mar shampla ar an scáileán glasa Scagairí Regex meiteashonraí Cuirtear gnáthshlonn i bhfeidhm ar theidil, ar ealaíontóirí, agus ar albaim roimh liricí a lorg agus nuair a dhéantar scóráil chun a chinneadh an meaitseálann dhá theideal. Fág folamh é chun an dá cheann a dhíchumasú. Baineann an réamhshocrú amach an tranglam coitianta ar nós \"Official Video\", \"Remaster\", \"【插曲】\", agus \"- Topic\". @@ -564,6 +585,10 @@ Fianáin riachtanacha: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PA Slonn rialta a chuirtear i bhfeidhm ar théacs bunaidh na liricí amháin (ní ar aistriúcháin ná ar leaganacha rómánaithe). Fág folamh é chun é a dhíchumasú Marcóirí na líne eolais Má dhéantar na liricí a scagadh go mícheart go dtí an bun, déan é seo a choigeartú nó a ghlanadh. Marcóirí scartha le camóga: taispeántar línte ina bhfuil marcóir agus ansin idirponc mar eolas creidmheasa ag bun an leathanaigh.. Fág folamh é chun an ghné seo a dhíchumasú + Cuir an chéad litir i bhmór + Cuireann sé an chéad litir de gach líne liric i bhmór + Bain puinctailleas ó thaobh thiar + Baineann sé phuinctálacha, commas, colóna agus semicolóna de scríobh ag críoch líneacha liric. Coimeádfar comhrácaí, comharthaí mianach, ellipsis, comhrais agus breisleanna Méid an téacs Méid téacs na liricí Aistriú ama domhanda diff --git a/patches/src/main/resources/addresources/values-ga-rIE/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-ga-rIE/shared-youtube/strings.xml index b6f62ad3d1..7884390e54 100644 --- a/patches/src/main/resources/addresources/values-ga-rIE/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ga-rIE/shared-youtube/strings.xml @@ -152,12 +152,12 @@ Chun síniú isteach, brúigh lean ar aghaidh thíos" Díchumasaigh an barra cuardaigh Coscann sé an barra cuardaigh a tharraingt ar an scáileán glasála agus ar an bpainéal fógraí Lorg le cnaipe Roimhe & Ar Aghaidh - Athraíonn riailuithe don fhógra meán, scáileán glas, agus cluasáin chun lorg in ionad videois a athrú - Fad an aisluaithe - Soicindí chun aisluaithe leis an gcnaipe Roimhe - Fad an cinnluaithe - Soicindí chun cinnluaithe leis an gcnaipe Ar Aghaidh - Ba chóir go mbeadh an fhad lorga idir 1 agus %s soicind. Ag baint úsáide as réamhshocrú + Athraíonn sé rialuithe le haghaidh fógraí meán, scáileán glasála agus cluasáin chun físeáin a lorg in ionad iad a athrú + Fad an athchasadh + Soicindí le dul siar leis an gcnaipe Roimhe Seo + Fad an dul chun cinn tapa + Soicindí le cur ar aghaidh go tapa leis an gcnaipe Ar Aghaidh + Ní mór fad cuardaigh a bheith idir 1 agus %s soicind. Ag baint úsáide as an réamhshocrú. Díchumasaigh prótacal QUIC diff --git a/patches/src/main/resources/addresources/values-ga-rIE/youtube/strings.xml b/patches/src/main/resources/addresources/values-ga-rIE/youtube/strings.xml index 8e2665a2a5..9204528d7f 100644 --- a/patches/src/main/resources/addresources/values-ga-rIE/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ga-rIE/youtube/strings.xml @@ -393,6 +393,9 @@ Tá tuairiscí poiblí agus déantar athbhreithniú orthu sula gcuirtear cainéa Folaigh naisc siopadóireachta Folaigh fógraí físe Folaigh ardú céime YouTube Premium + + + Ag lódáil… Cainéal reatha: Cuir leis an liosta bán @@ -409,6 +412,9 @@ Tá tuairiscí poiblí agus déantar athbhreithniú orthu sula gcuirtear cainéa Torthaí cuardaigh cainéil Níor aimsíodh aon rud ar an gcainéal seo Níorbh fhéidir an cainéal seo a chuardach + Físeáin + Postálacha + Níl aon phostáil ar an gcainéal seo a mheaitseálann Ábharthacht Is mó a breathnaíodh Is lú a breathnaíodh @@ -436,15 +442,16 @@ Tá tuairiscí poiblí agus déantar athbhreithniú orthu sula gcuirtear cainéa Taispeáin an cnaipe \'Féach níos déanaí\' ar mhionsamhla Tá cnaipe \'Féach níos déanaí\' le feiceáil ar mhionsamhla físeán sa fhotha. Má dhéanann tú sconna air agus é a choinneáil brúite, osclófar an rogha \'Sábháil chuig seinmliosta\' - Íoslódáil íomhánna posta - Taispeirtear cnaipe íoslódála in aice le cnaipe Comhroinn postála a bhfuil íomhánna nó GIFa leo, agus cuirtear ítem íoslódála lenaic santáirlithe + Íoslódáil íomhánna an phoist + Taispeántar cnaipe íoslódála in aice leis an gcnaipe Comhroinnte ar phoist a bhfuil íomhánna nó GIFanna ag gabháil leo, agus cuirtear rogha íoslódála leis an roghchlár a bhaineann leo. Íomhánna a íoslódáil - Íomhá a íoslódáil - Ag íoslódáil %s íomhánna… - Ag íoslódáil íomhánna… - Sábháilte an íomhá chuig %s + Íoslódáil íomhá + Íomhánna %s á n-íoslódáil… + Íomhá á híoslódáil… + Sábháladh %1$s íomhá chuig %2$s + Íomhá sábháilte chuig %s Níorbh fhéidir an íomhá a íoslódáil - Níor aimsíodh aon íomhá sa phóst seo + Níor aimsíodh aon íomhá sa phostáil seo Cuir an mhír Fiafraigh leis an roghchlár aníos Cuireann sé mír achoimrithe físe leis an roghchlár aníos sa bhfotha @@ -483,16 +490,19 @@ Má oscailtí físeán a bhfuil cnaipe Iarrtas aige uair amháin, beidh an ghné Íoslódáil físeáin, podchraoltaí, nó amhráin le haipeanna íoslódála tríú páirtí Taispeáin cnaipe íoslódála ar fhorleagan an imreora Taispeánann sé cnaipe sa seinnteoir físe a osclaíonn d’uirlis íoslódála sheachtrach - Íoslódáil thapa le tapáil - Cuireann sé isteach iad a íoslódáil sa chúlra. Tapáil agus coinlligh chun an íoslódálaí a oscailt. Oibríonn sé le YTDLnis agus YTindir - Marcáil íoslódálacha mar a bhí feicthe - Cuirtear físeáin a íoslódálann íoslódálaí externach leis an stair féachta YouTube agat - Full HD (1080p) + Íoslódáil thapa ar fáil láithreach + Cuireann sé in iúl duit gur féidir rudaí a íoslódáil sa chúlra. Beartaigh agus coinnigh chun an íoslódálaí a oscailt. Oibríonn sé le YTDLnis agus YTindir. + Marcáil íoslódálacha mar a breathnaíodh orthu + Cuirtear físeáin a íoslódáiltear le huirlis íoslódála sheachtrach le do stair féachana YouTube. + HD Iomlán (1080p) Meánach (192 kbps) - Íoslódáil tapaidh + Íoslódáil thapa Físeán Fuaim Gach rogha + Taispeáin íoslódáil sa chláir Shorts + Cuir earráid íoslódála seachtracha isteach i bpróiseas Shorts + Íoslódáil Aistriúchán gutha Léigh fotheidil físe os ard ag baint úsáide as téacs-go-hurlabhra ar an ngléas, sioncrónaithe le hathsheinm @@ -609,11 +619,11 @@ Eolas: Seans nach n-oibreoidh sé ar shruthanna beo" Malartaigh gníomhartha Sábháil agus Ciú Tapáil le haghaidh roghanna scuaine, tapáil agus coinnigh síos chun sábháil chuig Féach Níos Déanaí - Seinn an chéad cheann eile sa scuaine + Seinn an chéad rud eile sa scuab-chineál Réamhshocrú - Scuaine tionscadail + Scuaine dhúchasach Scuaine Morphe - Cliant scuaine Morphe + Scuaine Morphe ar thaobh an chliaint Cuir mír roghchláir scuaine amach beatha leis Cuireann sé mír bhreise ar an leathanach leis. Cumasaigh é seo mura bhfuil \"Cuir leis an scuaine\" le feiceáil. Scuaine @@ -661,19 +671,19 @@ Eolas: Seans nach n-oibreoidh sé ar shruthanna beo" Oscail an scuaine Sábháil an scuaine chuig an seinmliosta - Liamosta shábháilte - Sábháilte go %s - Glan scuaine + Seinnliosta sábháilte + Sábháilte chuig %s + Glan an scuaine Scuaine glanta Scuaine Seinn an ceann deireanach sa scuaine - Seinn an chéad cheann eile sa scuaine + Seinn an chéad rud eile sa scuaine Níorbh fhéidir le YouTube an scuaine a thaispeáint. - Níl an scuaine réidh don físeán seo. Oscail imitheoir an físeáin agus iarr arís. - Cuir Cás leis an scuaine nó féach ar Chas? + Níl an scuaine réidh don fhíseán seo. Oscail an seinnteoir físeán agus bain triail as arís. + Cuir an Meascán leis an scuaine nó féach ar an Meascán? Féach Cuir leis an scuaine - Ag lódáil Cas… + Ag luchtú an mheascáin… Oscail an cainéal ón avatar beo Coscann sé seo an sruth beo ó oscailt nuair a thapaítear ar avatar an chainéil. @@ -892,7 +902,7 @@ Chun rochtain a fháil ar na Socruithe, cuir an cnaipe Socruithe ar chumas trí Tréshoilseach Trédhearcach Teimhneach - Seans go nach bhfuil an stíl seo tacaíte ag do ghlúma nó ag an leagan YouTube, agus seans go gcuirfeadh sé cúiseanna eaclaise le leआउट nó go gcuirfeadh sé an fear leisure as gnó + D’fhéadfadh sé nach dtacaíonn do ghléas nó do leagan de YouTube leis an stíl seo, rud a d’fhéadfadh fadhbanna leagan amach nó tuairteanna a chruthú. Barra Uirlisí Roghnaigh cé na comhpháirteanna a thaispeántar sa bharra uirlisí Folaigh an cnaipe Cast @@ -1120,9 +1130,6 @@ Leagan amach gluaisteán "Athchóiríonn sé leagan amach an tsean-imreoir Teorainn: Mura bhfuil 'Barra gníomhaíochta seanfhíseáin athchóirithe' múchta, b'fhéidir nach mbeidh forleagan 'Tuilleadh físeáin' ar fáil" - "D'fhéadfadh an activating seo roinnt gnéithe leआउट an pleigheara a bhriseadh, mar shampla an rann commenting nó 'Tuilleadh físeán' a thaispeántar faoin pleighear. - -Níl tacaíocht agat a thuilleadh ag YouTube le leआउट tábléid, agus tá sé briste go páirteach nó go hiomlán ar leaganacha YouTube nua. Chun leआउट tábléid a fháil, patch YouTube 20.21.37" 21.11.48 - Athchóirigh iompar fotheidil d\'aois 20.39.41 - Athchóirigh an sean-shábháil chuig an seinmliosta @@ -1390,10 +1397,10 @@ Iarrtar físeáin le réimír ghearr den aitheantóir físeáin hasáilte, mar s Taispeánann sé fógra nuair a shábháiltear luas athsheinm réamhshocraithe nua Luas athsheinm réamhshocraithe Luas athsheinm réamhshocraithe ar Shorts - Cuimhnigh luas athsheinm ar Shorts - Cuireann sé do rogha luas Shorts láimhe i bhfeidhm ar gach Shorts sa todhchaí. Má tharlaíonn sé go bhfuil sé múchta, filleann an luas ar an réamhshocradh tar éis an tíosaithe Shorts a dhúnadh - Taispeáin luas athsheinm sa roghshaothar Shorts - Cuireann íomhá Luas athsheinm le menú imleactóra Shorts + Cuimhnigh ar luas athsheinm ar Shorts + Cuirtear an luas a roghnaigh tú de láimh le haghaidh Shorts i bhfeidhm ar gach Shorts amach anseo. Má tá sé múchta, fillfidh an luas ar an socrú réamhshocraithe tar éis duit an seinnteoir Shorts a dhúnadh. + Taispeáin luas athsheinm i roghchlár na Shorts + Cuireann sé mír le haghaidh luas athsheinm le roghchlár an imreora Shorts Luas athsheinm · %s Athraíodh an luas athsheinm réamhshocraithe go: %s Úsáid gnáthluas don cheol @@ -1625,4 +1632,17 @@ Tá taifeadadh uathoibríoch roghnach. Stopann sé i mód incognito nó nuair a Níor aimsíodh aon tsraith Eipeasóid gan teideal Eipeasóid nach bhfuil ar fáil + + Tuairimí Niconico + Taispeáin comhrá beo agus athsheallú comhrá mar thuairimí a scrolálann trasna an fhíseáin + Taispeánann sé cnaipe forleagan san imreoir físeáin. Tapáil chun na tuairimí a thaispeánadh nó a fholamh, tapáil agus coinnigh le haghaidh socruithe tapa + Méid font tuairimí + Méid téacs na tuairimí (12-40 sp) + Trédemsse tuairimí + Tránspaireacht téacs na NósCórasa agus na gcumhachraí + Fad taistealaigh + Cé mhéad seconda a thógann NósCórasa an scréin a thrasnú. Bogann luachanna íseacha níos tapúla + Fad iomlán anúllaithe + Cheanglaíonn sé údarás. Gach emoji á ríomh mar shuíomh aonair + Gan teorainn diff --git a/patches/src/main/resources/addresources/values-gl-rES/youtube/strings.xml b/patches/src/main/resources/addresources/values-gl-rES/youtube/strings.xml index 6caf5d68af..bb084a48b4 100644 --- a/patches/src/main/resources/addresources/values-gl-rES/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-gl-rES/youtube/strings.xml @@ -164,6 +164,9 @@ Os cambios inclúen: + + + Cargando… Eliminar @@ -186,6 +189,7 @@ Os cambios inclúen: Vídeo + Descargar Personalizado @@ -368,4 +372,5 @@ Os cambios inclúen: Ao día Continuar Anular + diff --git a/patches/src/main/resources/addresources/values-gu-rIN/youtube/strings.xml b/patches/src/main/resources/addresources/values-gu-rIN/youtube/strings.xml index d2e7fd509a..8e19bda11c 100644 --- a/patches/src/main/resources/addresources/values-gu-rIN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-gu-rIN/youtube/strings.xml @@ -51,6 +51,8 @@ Second \"item\" text" + + @@ -232,4 +234,5 @@ Second \"item\" text" ડિફૉલ્ટ + diff --git a/patches/src/main/resources/addresources/values-hi-rIN/music/strings.xml b/patches/src/main/resources/addresources/values-hi-rIN/music/strings.xml index 5cda8469c3..cf13d66e29 100644 --- a/patches/src/main/resources/addresources/values-hi-rIN/music/strings.xml +++ b/patches/src/main/resources/addresources/values-hi-rIN/music/strings.xml @@ -550,8 +550,29 @@ SABR प्लेबैक (स्पूफ वीडियो स्ट्र ब्लूटूथ, Android Auto और लॉक स्क्रीन पर वर्तमान पंक्ति दिखाता है। Android 11 या उससे नया चाहिए मिनीप्लेयर गीत दिखाएँ न्यूक्लेस पर ट्रैक उपशीर्षक पर स्थानांतरित करके, मिनीप्लेयर में वर्तमान पंक्ति दिखाता है - कलाकार और शीर्षक दिखाने का क्रम - मिनीप्लेयर और MediaSession में कलाकार और शीर्षक के प्रदर्शन क्रम को टॉगल करें। चालू: \"कलाकार - शीर्षक\" और बंद: \"शीर्षक - कलाकार\"। गीत खोज पर इसका कोई प्रभाव नहीं पड़ता। + गाने का शीर्षक/कलाकार प्रारूप + Miniplayer और MediaSession में लिरिक्स के साथ दिखाए गए ट्रैक का प्रारूप। {t} शीर्षक है, {a} कलाकार है और {v} वर्शन है, जैसे कि Acoustic, जब लिरिक्स स्रोत इसे प्रदान करता है। दो कोलन के बीच का टेक्स्ट केवल तभी दिखाया जाता है जब उसके अंदर के प्रत्येक वेरिएबल का मान हो; साधारण कोलन के लिए \\: लिखें। प्रत्येक पंक्ति बारी-बारी से दिखाई देती है, और लिरिक्स बदलने पर आगे बढ़ जाती है + लिरिक्स लाइन प्रारूप + Miniplayer और MediaSession में लिरिक्स लाइन का प्रारूप। {lrc} लाइन है और {n} यह है कि इसे लगातार कितनी बार गाया गया है। कोलन के बीच का टेक्स्ट केवल तभी दिखाया जाता है जब लाइन दोहराई जाती है। कोलन के लिए \\: का उपयोग करें + प्रारूप में {lrc} होना चाहिए + कलाकार-लाइन टेम्पलेट और एनिमेशन टाइमिंग + न्यूनतम स्क्रीन समय (ms) + अधिकतम स्क्रीन समय (ms) + केवल मल्टीलाइन एनिमेशन के लिए। प्रत्येक फ्रेम को कम से कम न्यूनतम समय तक रखें। अधिकतम समय पर, कलाकार लाइन को तब भी आगे बढ़ाएं जब गीत वही रहे। + न्यूनतम 0 या उससे अधिक होना चाहिए। अधिकतम 0 से अधिक और कम से कम न्यूनतम के बराबर होना चाहिए। + लाइन लंबाई सीमाएँ + गीत और कलाकार लाइन सीमाएँ, और फुल-विड्थ कैरेक्टर वेट + "गीत लाइन सीमा +लंबे गीतों को संतुलित आधे हिस्सों में विभाजित करें। 0 विभाजन को अक्षम करता है।" + "कलाकार लाइन सीमा +भरे हुए ट्रैक फॉर्मेट को साइकलिंग फ्रेम में विभाजित करें। 0 विभाजन को अक्षम करता है।" + "फुल-विड्थ कैरेक्टर वेट +दोनों सीमाओं पर लागू होता है। 1.75 पर, चार फुल-विड्थ कैरेक्टर्स को 7 गिना जाता है।" + 0 से 10000 तक पूर्ण-संख्या सीमाएं और 0 से अधिक कैरेक्टर वेट दर्ज करें + इंस्ट्रूमेंटल ब्रेक + इंस्ट्रूमेंटल ब्रेक के दौरान मिनीप्लेयर और MediaSession में दिखाया जाता है। प्रत्येक लाइन एक एनीमेशन फ्रेम है। {t} शीर्षक है, {a} कलाकार है और {lrc} अंतिम लिरिक्स लाइन है + मिनीप्लेयर और MediaSession टाइमिंग ऑफसेट + मिनीप्लेयर और MediaSession में अगली पंक्ति कब दिखाई दे, इसे शिफ्ट करता है। यदि पंक्तियाँ देर से दिखाई देती हैं, तो नकारात्मक मान का उपयोग करें, उदाहरण के लिए लॉक स्क्रीन पर फ़िल्टर मेटाडेटा regex रेगुलर एक्सप्रेशन शीर्षकों, कलाकारों और एल्बमों पर गीत खोजने से पहले और यह जाँचते समय लागू किया जाता है कि दो शीर्षक मेल खाते हैं या नहीं। दोनों को अक्षम करने के लिए खाली छोड़ें। डिफ़ॉल्ट \"Official Video\", \"Remaster\", \"【插曲】\" और \"- Topic\" जैसी सामान्य अनावश्यक चीज़ें हटा देता है। @@ -559,6 +580,10 @@ SABR प्लेबैक (स्पूफ वीडियो स्ट्र रेगुलर एक्सप्रेशन केवल मूल गीत पाठ पर लागू होता है (अनुवाद या रोमनीकरण पर नहीं)। अक्षम करने के लिए खाली छोड़ें जानकारी पंक्ति मार्कर यदि गीत गलत तरीके से नीचे फ़िल्टर हो जाते हैं, तो इसे समायोजित करें या साफ़ करें। अल्पविराम से अलग किए गए मार्कर: जिन पंक्तियों में मार्कर के बाद कोलन होता है, वे नीचे क्रेडिट जानकारी के रूप में दिखाई जाती हैं। अक्षम करने के लिए खाली छोड़ें + पहला अक्षर बड़ा करें + गीत की प्रत्येक पंक्ति के पहले अक्षर को बड़ा करता है + अंत के विराम चिह्नों को हटाएँ + गीत की पंक्तियों के अंत में पूर्ण विराम, अल्पविराम, कोलन और सेमीकोलन को हटाता है। प्रश्न चिह्न, विस्मयादिबोधक चिह्न, एलिप्सिस, उद्धरण चिह्न और ब्रैकेट रखे जाते हैं टेक्स्ट का आकार गीत टेक्स्ट का आकार वैश्विक समय ऑफ़सेट diff --git a/patches/src/main/resources/addresources/values-hi-rIN/youtube/strings.xml b/patches/src/main/resources/addresources/values-hi-rIN/youtube/strings.xml index d455149235..d5aacf2aa5 100644 --- a/patches/src/main/resources/addresources/values-hi-rIN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-hi-rIN/youtube/strings.xml @@ -393,6 +393,9 @@ Second \"item\" text" 쇼핑 링크 숨기기 छुपाएं वीडियो विज्ञापन छुपाएं YouTube प्रीमियम प्रोमोशन + + + लोड हो रहा है… वर्तमान चैनल: व्हाइटलिस्ट में जोड़ें @@ -409,6 +412,9 @@ Second \"item\" text" चैनल खोज परिणाम इस चैनल पर कुछ नहीं मिला इस चैनल में खोज नहीं की जा सकी + वीडियो + पोस्ट + इस चैनल पर कोई पोस्ट मेल नहीं खाता प्रासंगिकता सबसे अधिक देखे गए सबसे कम देखे गए @@ -494,6 +500,9 @@ Second \"item\" text" वीडियो ऑडियो सभी विकल्प + Shorts मेनू में डाउनलोड दिखाएं + Shorts प्लेयर में बाहरी डाउनलोडर फ्लाउट मेनू आइटम जोड़ें + डाउनलोड करें वॉइस ओवर अनुवाद डिवाइस पर टेक्स्ट-टू-स्पीच का उपयोग करके वीडियो कैप्शन को ज़ोर से पढ़ें, जो प्लेबैक के साथ सिंक्रनाइज़ किया गया है। @@ -1121,9 +1130,6 @@ OpenRouter.ai खोलने के लिए यहां टैप करे "पुराने प्लेयर लेआउट को पुनर्स्थापित करता है सीमा: यदि 'पुराने वीडियो एक्शन बार को पुनर्स्थापित करें' बंद कर दिया गया है, तो 'अधिक वीडियो' ओवरले उपलब्ध नहीं हो सकता है" - "इसे सक्षम करने से प्लेयर लेआउट की कुछ विशेषताएं खराब हो सकती हैं, जैसे कि कमेंट सेक्शन या प्लेयर के नीचे दिखाए गए 'More videos'। - -टैबलेट लेआउट अब YouTube द्वारा समर्थित नहीं है, और हाल के YouTube वर्ज़न में आंशिक या पूरी तरह से खराब है। एक वर्किंग टैबलेट लेआउट के लिए, YouTube 20.21.37 को पैच करें" 21.11.48 - पुराने कैप्शन व्यवहार को पुनर्स्थापित करें 20.39.41 - पुराने सेव को प्लेलिस्ट में पुन: स्थापित करें @@ -1626,4 +1632,17 @@ AV1 के साथ वीडियो प्लेबैक में झट कोई सीरीज़ नहीं मिली शीर्षकहीन एपिसोड अनुपलब्ध एपिसोड + + Niconico कमेंट्स + लाइव चैट और चैट रिप्ले को कमेंट्स के रूप में दिखाएं जो वीडियो में स्क्रॉल होते हैं + वीडियो प्लेयर में एक ओवरले बटन दिखाता है। कमेंट्स दिखाने या छिपाने के लिए टैप करें, क्विक सेटिंग्स के लिए टैप करके रखें + कमेंट फ़ॉन्ट आकार + कमेंट्स के टेक्स्ट का आकार (12-40 sp) + कमेंट ओपेसिटी + कमेंट टेक्स्ट और बैकग्राउंड की ओपेसिटी + ट्रैवल ड्यूरेशन + कमेंट्स को स्क्रीन पार करने में कितने सेकंड लगते हैं। कम वैल्यू तेज़ी से चलती हैं + कमेंट की अधिकतम लंबाई + इससे अधिक वर्णों वाले कमेंट छुपाएं। प्रत्येक इमोजी को एक वर्ण माना जाएगा + कोई सीमा नहीं diff --git a/patches/src/main/resources/addresources/values-hr-rHR/music/strings.xml b/patches/src/main/resources/addresources/values-hr-rHR/music/strings.xml index ea1c9b3ce8..ea13580b45 100644 --- a/patches/src/main/resources/addresources/values-hr-rHR/music/strings.xml +++ b/patches/src/main/resources/addresources/values-hr-rHR/music/strings.xml @@ -555,8 +555,29 @@ Potrebni kolačići: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPI Prikazuje trenutni redak na zaključanom zaslonu, Android Auto i Bluetooth-u. Zahtijeva Android 11 ili noviji Prikaži tekstove pjesama u miniplayeru. Prikazuje trenutni redak u minijaturnom playeru, s pjesmom premještenom u podnaslov - Redoslijed prikaza izvođača i naslova - Prebacuje redoslijed prikaza izvođača i naslova u miniplayeru i MediaSessionu. ULJUČENO: \"Izvođač - Naslov\", ISKLJUČENO: \"Naslov - Izvođač\". Ne utječe na pretragu tekstova pjesama. + Format naslova pjesme/izvođača + Format trake koji se prikazuje uz tekst pjesme u miniplayeru i MediaSession-u. {t} je naslov, {a} je izvođač i {v} je verzija, kao što je Acoustic, ako je izvor teksta pjesme nudi. Tekst između dva dvotočkija prikazuje se samo kada svaka varijabla unutar njega ima vrijednost; napišite \\: za običnu dvotočku. Svaki redak se prikazuje redom i mijenja se kada se promijeni tekst pjesme + Format retka teksta pjesme + Format retka teksta pjesme u miniplayeru i MediaSession-u. {lrc} je redak, a {n} je broj puta koliko se on zaredom pjeva. Tekst između dvotočkija prikazuje se samo kada se redak ponavlja. Koristite \\: za dvotočku + Format mora sadržavati {lrc} + Predložak linije izvođača i vrijeme animacije + Minimalno vrijeme prikaza na zaslonu (ms) + Maksimalno vrijeme prikaza na zaslonu (ms) + Samo za višeredne animacije. Zadržite svaki okvir barem minimalno vrijeme. Pri maksimalnom vremenu, pomaknite liniju izvođača čak i ako tekst pjesme ostaje isti. + Minimalno mora biti 0 ili više. Maksimalno mora biti veće od 0 i najmanje jednako minimalnom. + Ograničenja duljine linije + Ograničenja linija teksta pjesme i izvođača te težina znakova pune širine + "Ograničenje linije teksta pjesme +Podijelite duže stihove u uravnotežene polovice. 0 onemogućuje dijeljenje." + "Limit linija izvođača +Podijelite popunjeni format trake u ciklične okvire. 0 onemogućuje podjelu." + "Težina znakova pune širine +Primjenjuje se na oba limita. Pri 1,75, četiri znaka pune širine računaju se kao 7." + Unesite cijelobrojne limite od 0 do 10000 i težinu znaka veću od 0 + Instrumentalni predah + Prikazuje se u miniplayeru i MediaSession-u tijekom instrumentalnog predaha. Svaka linija je jedan okvir animacije. {t} je naslov, {a} je izvođač, a {lrc} je posljednja linija teksta pjesme + Vremenski pomak miniplayera i MediaSession-a + Pomiče trenutak kada se sljedeći redak prikazuje u minimiziranom playeru i MediaSession-u. Koristite negativnu vrijednost ako se redci pojavljuju prekasno, na primjer na zaključanom zaslonu Filteri Regex za metapodatke Regularni izraz primjenjuje se na naslove, izvođače i albume prije pretraživanja tekstova pjesama i pri ocjenjivanju odgovaraju li dva naslova. Ostavite prazno da biste onemogućili oboje. Zadana vrijednost uklanja uobičajene nepotrebne dijelove poput \"Official Video\", \"Remaster\", \"【插曲】\" i \"- Topic\". @@ -564,6 +585,10 @@ Potrebni kolačići: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPI Regularni izraz koji se primjenjuje samo na izvorni tekst pjesama (ne na prijevode ili romanizacije). Ostavi prazno za onemogućavanje. Oznake redaka s informacijama Ako su tekstovi pjesama pogrešno filtrirani na dno, prilagodi ili obriši ovo. Oznake odvojene zarezom: retci koji sadrže oznaku nakon koje slijedi dvotočka prikazuju se kao podaci o zaslugama na dnu. Ostavi prazno za onemogućavanje. + Veliko početno slovo + Piše prvo slovo svakog retka teksta pjesme velikim slovom + Ukloni završnu interpunkciju + Uklanja točke, zareze, dvotočke i točku-zarez na kraju retaka teksta pjesme. Upitnici, uskličnici, tri točke, navodnici i zagrade ostaju Veličina teksta Veličina teksta stihova Globalni vremenski pomak diff --git a/patches/src/main/resources/addresources/values-hr-rHR/youtube/strings.xml b/patches/src/main/resources/addresources/values-hr-rHR/youtube/strings.xml index 690a5ecd56..ee36941e6a 100644 --- a/patches/src/main/resources/addresources/values-hr-rHR/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-hr-rHR/youtube/strings.xml @@ -383,6 +383,9 @@ Prijave su javne i pregledavaju se prije nego što se kanal doda na popis. Uklan Sakrij poveznice Sakrij video oglase Sakrij YouTube Premium oglase + + + Samo trenutak… Trenutni kanal: Dodaj na popis dopuštenih @@ -399,6 +402,9 @@ Prijave su javne i pregledavaju se prije nego što se kanal doda na popis. Uklan Rezultati pretraživanja kanala Na ovom kanalu nema rezultata Pretraživanje ovog kanala nije uspjelo + Videozapisi + Objave + Nema objava na ovom kanalu koje odgovaraju pretraživanju Relevantnost Najgledaniji Najmanje gledani @@ -432,6 +438,7 @@ Prijave su javne i pregledavaju se prije nego što se kanal doda na popis. Uklan Preuzmi sliku Preuzimanje %s slika… Preuzimanje slike… + %1$s slika je spremljeno u %2$s Slika sačuvana u %s Slika nije mogla biti preuzeta U ovoj objavi nije pronađena slika @@ -483,6 +490,9 @@ Nakon što se jednom otvori video s gumbom Ask, funkcija radi sa svim videima, Video Audio Sve opcije + Prikaži preuzimanje u Shorts izborniku + Dodaje stavku izbornika za vanjski preuzimač u Shorts player + Preuzmi Prevođenje glasom Čitaj titlove videozapisa naglas koristeći uređajni text-to-speech, sinkronizirano s reprodukcijom @@ -1108,9 +1118,6 @@ Izgled za automobil "Vraća stari izgled playera Ograničenje: Ako je 'Vrati staru traku s radnjama za videozapise' isključeno, overlay 'Više videozapisa' možda neće biti dostupan" - "Omogućavanjem ovoga može doći do kvara određenih značajki izgleda playera, kao što su odjeljak s komentarima ili \"Više videa\" prikazani ispod playera. - -Tablet izgled više nije podržan od strane YouTubea te je djelomično ili potpuno neispravan u novijim verzijama YouTubea. Za funkcionalan tablet izgled, zakrpajte YouTube 20.21.37" 21.11.48 - Vrati staro ponašanje titlova 20.39.41 - Vrati stari spremi u playlistu @@ -1605,4 +1612,17 @@ Ograničenje: Vraća se na stari stil 'More videos' prekrivača" Nije pronađena serija Epizoda bez naslova Nedostupna epizoda + + Niconico komentari + Prikaži izravni chat i ponovnicu chata kao komentare koji klize preko videa + Prikazuje gumb preko video playera. Dodirnite za prikaz ili skrivanje komentara, pritišćite i držite za brze postavke + Veličina fonta komentara + Veličina teksta komentara (12-40 sp) + Neprozirnost komentara + Neprozirnost teksta i pozadina komentara + Trajanje putovanja + Koliko sekundi komentarima treba da pređu ekran. Manje vrijednosti znače brže kretanje + Maksimalna duljina komentara + Skriva komentare s više znakova od navedenog. Svaki emoji računa se kao jedan znak + Bez ograničenja diff --git a/patches/src/main/resources/addresources/values-hu-rHU/music/strings.xml b/patches/src/main/resources/addresources/values-hu-rHU/music/strings.xml index 0ad0ee3637..379a1ab749 100644 --- a/patches/src/main/resources/addresources/values-hu-rHU/music/strings.xml +++ b/patches/src/main/resources/addresources/values-hu-rHU/music/strings.xml @@ -555,8 +555,29 @@ Szükséges cookie-k: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAP Jelenlegi sor megjelenítése a zárolási képernyőn, Android Autóban és Bluetoothon. Android 11 vagy újabb szükséges Dalszöveg megjelenítése a minilejátszóban Jelenlegi sor megjelenítése a miniLejátszóban, a szám áthelyezésével az alfejlécbe - Előadó és cím megjelenítési sorrendje - Váltja az előadó és a cím sorrendjét a minilejátszóban és a MediaSessionben. BE: \"Előadó - Cím\", KI: \"Cím - Előadó\". Nem befolyásolja a dalszövegkeresést. + Dal címe/művész formátuma + A Miniplayerben és a MediaSessionben a dalszámmal együtt megjelenített szöveg formátuma. A {t} a cím, az {a} a művész, és a {v} a verzió, például Akusztikus, ha a dalszöveg forrás rendelkezik vele. A két kettőspont közötti szöveg csak akkor jelenik meg, ha minden benne lévő változónak van értéke; a sima kettősponthoz írja: \\:. Minden sor sorban jelenik meg, és váltik, amint a dalszöveg változik + Dalszöveg sor formátuma + A dalszöveg sor formátuma a Miniplayerben és a MediaSessionben. Az {lrc} a sor, az {n} pedig azt jelzi, hányszor ismétlődik egymás után. A kettőspontok közötti szöveg csak akkor jelenik meg, ha a sor ismétlődik. A kettőspont használatához használja a \\: jelet + A formátumnak tartalmaznia kell az {lrc} jelzést + Művészsor-sablon és animációs időzítés + Minimum képernyőidő (ms) + Maximum képernyőidő (ms) + Csak több soros animációkhoz. Minden képkeretet tartsa legalább a minimum időig. A maximum elérésekor a művészsor halad tovább, még akkor is, ha a dalszöveg változatlan marad. + A minimum értéke 0 vagy annál nagyobb kell hogy legyen. A maximum értéke nagyobb kell hogy legyen 0-nál, és legalább a minimum egyenlyegű. + Sorhosszúság korlátok + Dalszöveg és művészsor korlátok, valamint a teljes szélességű karakterek súlya + "Dalszöveg sorhosszúság korláta +Bontsa a hosszabb dalszövegeket egyensúlyozott felekre. A 0 érték kikapcsolja a bontást." + "Művész sorlimit +Ossza fel a kitöltött számformát ciklikus keretekre. A 0 kapcsolja ki az osztást." + "Teljes szélességű karakter súlya +Mindkét limitre vonatkozik. 1,75 esetén négy teljes szélességű karakter 7-nek számít." + Adjon meg egész számú limiteket 0 és 10000 között, valamint 0-nál nagyobb karaktersúlyt + Instrumentális szünet + A miniplayerben és a MediaSessionben megjelenítve instrumentális szünet alatt. Minden sor egy animációs keret. A {t} a cím, az {a} a művész és a {lrc} az utolsó dalszövegsor + Miniplayer és MediaSession időeltolás + Eltolás, hogy mikor jelenjen meg a következő sor a mini lejátszóban és a MediaSession-ben. Használjon negatív értéket, ha a sorok későn jelennek meg, például a zárolt képernyőn Szűrők Dalszöveg-szűrő regex A címekre, előadókra és albumokra alkalmazott reguláris kifejezés a dalszöveg keresése előtt, valamint annak kiértékelésekor, hogy két cím egyezik-e. Hagyja üresen mindkettő letiltásához. Az alapértelmezett reguláris kifejezés eltávolítja a gyakori felesleges elemeket, például \"Official Video\", \"Remaster\", \"【插曲】\" és \"- Topic\". @@ -564,6 +585,10 @@ Szükséges cookie-k: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAP Csak az eredeti dalszövegre alkalmazott reguláris kifejezés (nem a fordításokra vagy romanizációkra). A letiltáshoz hagyd üresen. Információs sorok jelölői Ha a dalszövegek hibásan az aljára szűrődnek, módosítsd vagy töröld ezt a mezőt. Vesszővel elválasztott jelölők: azok a sorok, amelyek egy, kettősponttal követett jelölőt tartalmaznak, alul forrásmegjelölésként jelennek meg. A letiltáshoz hagyd üresen. + Első betű nagybetűsre írása + Nagybetűvel kezdi minden dalszövegsor első betűjét + Végező rajzjelek eltávolítása + Eltávolítja a pontokat, vesszőket, kettőspontokat és pontosvesszőket a dalszövegsorok végéről. A kérdőjeleket, felkiáltójeleket, hárompontokat, idézőjeleket és zárójeleket megtartja Betűméret A dalszöveg betűmérete Globális időzítési eltolás diff --git a/patches/src/main/resources/addresources/values-hu-rHU/youtube/strings.xml b/patches/src/main/resources/addresources/values-hu-rHU/youtube/strings.xml index bd9b3b1a8e..f4aaaa91c9 100644 --- a/patches/src/main/resources/addresources/values-hu-rHU/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-hu-rHU/youtube/strings.xml @@ -393,6 +393,9 @@ A jelentések nyilvánosak, és felülvizsgálják őket, mielőtt egy csatorna Vásárlási hivatkozások elrejtése Videóhirdetések elrejtése YouTube Premium promóciók elrejtése + + + Betöltés… Jelenlegi csatorna: Hozzáadás a fehérlistához @@ -409,6 +412,9 @@ A jelentések nyilvánosak, és felülvizsgálják őket, mielőtt egy csatorna Csatornakeresési eredmények Nincs találat ezen a csatornán Nem sikerült keresni ezen a csatornán + Videók + Bejegyzések + Nincs egybeezlető bejegyzés ezen a csatornán Relevancia Legnézettebb Legkevésbé nézett @@ -494,6 +500,9 @@ Egy alkalommal megnyitott, Ask gombbal ellátott videó esetén a funkció minde Videó Hang Összes beállítás + Letöltés megjelenítése a Shorts menüben + Egy külső letöltő alkalmazás menüpontot ad a Shorts lejátszóhoz + Letöltés Hangfelirat hangos felolvasása Olvassa fel a videó feliratait hangosan a készüléken található szöveg-beszéddé alakító segítségével, szinkronban a lejátszással. @@ -1121,9 +1130,6 @@ Autós elrendezés "A régi lejátszó elrendezés visszaállítása Korlátozás: Ha a „Régi videós eszköztár visszaállítása” ki van kapcsolva, a „További videók” réteg nem jelenhet meg." - "Ennek engedélyezéset megsértheti a lejátszó elrendezésének bizonyos funkcióit, például a komment szekciót vagy a lejátszó alatt megjelenő „További videók” részt. - -A tabletes elrendezést a YouTube már nem támogatja, és a legújabb YouTube verziókban részben vagy teljesen hibás. A működő tabletes elrendezésért patchold a YouTube 20.21.37 verzióját" 21.11.48 - A régi felirat-viselkedés visszaállítása 20.39.41 - Visszaállítjuk az régi mentést a lejátslóba @@ -1626,4 +1632,17 @@ A automatikus rögzítés opcionális. Inkognitó módban vagy a fiókváltás s Nincs találat Cím nélküli epizód Nem elérhető epizód + + Niconico kommentek + Mutassa a élő chatet és a chat lejátszást olyan kommentekként, amelyek végiggurulnak a videón + Mutat egy felülépő gombot a videolejátszóban. Koppintson a kommentek megjelenítéséhez vagy elrejtéséhez, és tartsa nyomva a gyorsbeállításokhoz + Kommentek betűmérete + A kommentek szövegmérete (12-40 sp) + Kommentek átlátszósága + A kommentszövegek és a háttér átlátszósága + Mozgási időtartam + Hány másodperc alatt haladjanak át a kommentek a képernyőn. Az alacsonyabb értékek gyorsabb mozgást jelentenek + Maximális kommenthossz + Rejtőzi a kommenteket, amelyek több karakterből állnak, mint ez. Minden emoji egy karakternek számít + Nincs korlát diff --git a/patches/src/main/resources/addresources/values-hy-rAM/youtube/strings.xml b/patches/src/main/resources/addresources/values-hy-rAM/youtube/strings.xml index 9079108272..70ce03fc67 100644 --- a/patches/src/main/resources/addresources/values-hy-rAM/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-hy-rAM/youtube/strings.xml @@ -51,6 +51,8 @@ Second \"item\" text" + + @@ -224,4 +226,5 @@ Second \"item\" text" + diff --git a/patches/src/main/resources/addresources/values-in-rID/music/strings.xml b/patches/src/main/resources/addresources/values-in-rID/music/strings.xml index f6701fc6bb..15406b3f5e 100644 --- a/patches/src/main/resources/addresources/values-in-rID/music/strings.xml +++ b/patches/src/main/resources/addresources/values-in-rID/music/strings.xml @@ -555,8 +555,29 @@ Cookie yang diperlukan: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1P Menampilkan baris saat ini di layar kunci, Android Auto dan Bluetooth. Memerlukan Android 11 atau versi yang lebih baru Tampilkan lirik Pemutar mini Menampilkan baris saat ini di pemutar mini, dengan trek dipindahkan ke subjudul - Urutan tampilan Artis dan Judul - Ubah urutan tampilan Artis dan Judul di Pemutar mini dan MediaSession. ON \"Artis - Judul\" dan OFF \"Judul - Artis\". Tidak memengaruhi pencarian lirik + Format judul lagu/artis + Format trek yang ditampilkan bersama lirik di miniplayer dan MediaSession. {t} adalah judul, {a} adalah artis, dan {v} adalah versi, seperti Akustik, jika sumber lirik menyediakannya. Teks di antara dua titik dua hanya ditampilkan jika setiap variabel di dalamnya memiliki nilai; tulis \\: untuk titik dua biasa. Setiap baris ditampilkan secara bergantian, berpindah setiap kali lirik berubah + Format baris lirik + Format baris lirik di miniplayer dan MediaSession. {lrc} adalah barisnya dan {n} adalah berapa kali baris tersebut dinyanyikan secara berurutan. Teks di antara titik dua hanya ditampilkan saat baris berulang. Gunakan \\: untuk titik dua + Format harus mengandung {lrc} + Templat baris artis dan waktu animasi + Waktu layar minimum (ms) + Waktu layar maksimum (ms) + Hanya untuk animasi multibaris. Pertahankan setiap bingkai setidaknya selama waktu minimum. Pada batas maksimum, majukan baris artis meskipun lirik tetap sama. + Minimum harus 0 atau lebih besar. Maksimum harus lebih besar dari 0 dan setidaknya sama dengan minimum. + Batas panjang baris + Batas baris lirik dan artis, serta bobot karakter lebar penuh + "Batas baris lirik +Pecah lirik yang lebih panjang menjadi dua bagian yang seimbang. 0 menonaktifkan pemisahan." + "Batas baris Artis +Bagi format trek yang terisi menjadi bingkai siklus. 0 menonaktifkan pembagian." + "Bobot karakter lebar penuh +Berlaku untuk kedua batas. Pada 1,75, empat karakter lebar penuh dihitung sebagai 7." + Masukkan batas bilangan bulat dari 0 hingga 10000 dan bobot karakter lebih dari 0 + Jeda instrumental + Ditampilkan di miniplayer dan MediaSession selama jeda instrumental. Setiap baris adalah satu bingkai animasi. {t} adalah judul, {a} adalah artis dan {lrc} adalah baris lirik terakhir + Offset waktu miniplayer dan MediaSession + Menggeser kapan baris berikutnya ditampilkan di Miniplayer dan MediaSession. Gunakan nilai negatif jika baris muncul terlambat, misalnya pada layar kunci Filter Regex metadata Ekspresi reguler diterapkan pada judul, artis, dan album sebelum mencari lirik dan saat menilai apakah dua judul cocok. Biarkan kosong untuk menonaktifkan keduanya. Default menghapus keramaian umum seperti \"Official Video\", \"Remaster\", \"【插曲】\", dan \"- Topic\" @@ -564,6 +585,10 @@ Cookie yang diperlukan: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1P Regular expression hanya diterapkan pada teks lirik asli (bukan terjemahan atau romanisasi). Biarkan kosong untuk menonaktifkan Penanda baris info Jika lirik salah disaring ke bagian bawah, sesuaikan atau hapus ini. Penanda yang dipisahkan dengan koma: baris yang berisi penanda diikuti dengan titik dua akan ditampilkan sebagai info kredit di bagian bawah. Biarkan kosong untuk menonaktifkan + Kapitalisasi huruf pertama + Mengapitalisasi huruf pertama dari setiap baris lirik + Hapus tanda baca di akhir + Menghapus tanda titik, koma, titik dua, dan titik koma di akhir baris lirik. Tanda tanya, tanda seru, elipsis, tanda kutip, dan kurung tetap dipertahankan Ukuran teks Ukuran teks lirik Offset waktu global diff --git a/patches/src/main/resources/addresources/values-in-rID/youtube/strings.xml b/patches/src/main/resources/addresources/values-in-rID/youtube/strings.xml index 7aaa3ef092..13bd70c601 100644 --- a/patches/src/main/resources/addresources/values-in-rID/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-in-rID/youtube/strings.xml @@ -393,6 +393,9 @@ Laporan bersifat publik dan ditinjau sebelum saluran ditambahkan ke daftar. Peng Sembunyikan tautan belanja Sembunyikan iklan video Sembunyikan promosi YouTube Premium + + + Memuat… Saluran saat ini: Tambahkan ke daftar putih @@ -409,6 +412,9 @@ Laporan bersifat publik dan ditinjau sebelum saluran ditambahkan ke daftar. Peng Hasil pencarian saluran Tidak ada yang ditemukan di saluran ini Tidak dapat mencari di saluran ini + Video + Postingan + Tidak ada postingan di saluran ini yang cocok Relevansi Paling banyak ditonton Paling sedikit ditonton @@ -494,6 +500,9 @@ Setelah video dengan tombol Ask dibuka satu kali, fitur ini berfungsi untuk semu Video Audio Semua opsi + Tampilkan download di menu Shorts + Menambahkan item menu flyout pengunduh eksternal ke pemutar Shorts + Download Terjemahan sulih suara Baca teks keterangan video dengan suara keras menggunakan fitur text-to-speech bawaan perangkat, yang disinkronkan dengan pemutaran video @@ -1121,9 +1130,6 @@ Tata letak Otomotif "Mengembalikan tata letak pemutar lama Batasan: Jika 'Kembalikan bilah tindakan video lama' dinonaktifkan, hamparan 'Video lainnya' mungkin tidak tersedia" - "Mengaktifkan fitur ini dapat merusak beberapa fitur tata letak pemutar, seperti bagian komentar atau 'Video lainnya' yang ditampilkan di bawah pemutar. - -Tata letak tablet tidak lagi didukung oleh YouTube, dan sebagian atau seluruhnya rusak pada versi YouTube terbaru. Untuk tata letak tablet yang berfungsi, tambal YouTube 20.21.37" 21.11.48 - Kembalikan perilaku teks lama 20.39.41 - Kembalikan penyimpanan lama ke daftar putar @@ -1626,4 +1632,17 @@ Pencatatan otomatis bersifat opsional. Pencatatan berhenti dalam mode penyamaran Tidak ada seri ditemukan Episode tanpa judul Episode tidak tersedia + + Komentar Niconico + Tampilkan live chat dan putar ulang chat sebagai komentar yang bergulir di sepanjang video + Menampilkan tombol hamparan di pemutar video. Ketuk untuk menampilkan atau menyembunyikan komentar, ketuk dan tahan untuk setelan cepat + Ukuran font komentar + Ukuran teks komentar (12-40 sp) + Opasitas komentar + Opasitas teks dan latar belakang komentar + Durasi perjalanan + Berapa detik yang dibutuhkan komentar untuk melintasi layar. Nilai yang lebih rendah bergerak lebih cepat + Maksimal panjang komentar + Menyembunyikan komentar yang jumlah karakternya melebihi batas ini. Setiap emoji dihitung sebagai satu karakter + Tanpa batas diff --git a/patches/src/main/resources/addresources/values-is-rIS/music/strings.xml b/patches/src/main/resources/addresources/values-is-rIS/music/strings.xml index 140f7ad59b..d0b3f6dd45 100644 --- a/patches/src/main/resources/addresources/values-is-rIS/music/strings.xml +++ b/patches/src/main/resources/addresources/values-is-rIS/music/strings.xml @@ -553,8 +553,29 @@ Nauðsynlegar vafrakökur: SID, HSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPI Sýnir núverandi línu á læsiskjá, Android Auto og Bluetooth. Kræfur Android 11 eða nýrra Sýna texta í smáspilara Sýnir núverandi línu í smaspilara, með lagi flyttri í undirtexta - Birtingarröð flytjanda og titils - Notaðu lyklaborðið til að leita að texta. Stilltu leitarstillingarnar eftir þörfum. + Snið á lagatitli/listamanni + Snið á lágnumri sem sýnt er með textanum í lágnumrinum og MediaSession. {t} er titillinn, {a} er listamaðurinn og {v} er útgáfan, eins og Acoustic, ef textakerfið gefur upp slíka upplýsingar. Texti á milli tveggja skila er aðeins sýndur þegar hver breyta inni í honum hefur gildi; skrifaðu \\: fyrir venjulegan skilja. Hver lína er sýnd eftir og eftir þegar textinn breytist + Snið á textalínu + Snið á textalínu í lágnumrinum og MediaSession. {lrc} er línan og {n} er fjöldi sinna sem hún er sungin í röð. Texti á milli skila er aðeins sýndur þegar línan endurtekst. Notaðu \\: fyrir skilja + Sniðið verður að innihalda {lrc} + Snið fyrir listamannalínu og tímasetningu hreyfingar + Lágmarkstimi á skjá (ms) + Hámarkstimi á skjá (ms) + Aðeins fyrir hreyfingar með margar linnur. Halda hverju römni í að minnsta kosti lágmarkstima. Við hámarkstima er listamannalínan færð áfram jafnvel þótt textinn sé sá sami. + Lágmark verður að vera 0 eða hærra. Hámark verður að vera hærra en 0 og að minnsta kosti jafnt og lágmark. + Tökur á lengd línna + Tökur á línustærð texta og listamanns, og þyngd fullbreiddarteikna + "Taka á línustærð texta +Skipta lengri texta í jafnvægnar hluti. 0 slökkvir á skiptingu." + "Línumat fyrir listamann +Skipti fyllta lagsmynd í hringrásarramma. 0 slökkvir á skiptingu." + "Þyngd fullbreiddarteikna +Gildir fyrir bæði mörk. Við 1,75 teljast fjórar fullbreiddarteiknir sem 7." + Sláðu inn heiltölu fyrir mörk frá 0 til 10000 og teiknþyngd stærri en 0 + Tónlistarhlutur án söngs + Sýnd í lágmarkaspilaranum og MediaSession á meðan tónlistarhlutur án söngs spilar. Hver lína er einn hreyfirammi. {t} er titillinn, {a} er listamaðurinn og {lrc} er síðasta textalínan + Tímahliðrun lágmarkaspilarans og MediaSession + Breytir því hvenær næsta lína birtist í lágmörkuða spilaranum og MediaSession. Notaðu neikvætt gildi ef línurnar birtast of seint, til dæmis á læsingarskjánum Síur Regluleg segð fyrir lýsigögn Regluleg segð sem er notuð á titla, flytjendur og plötur áður en leitað er að textum og þegar lagt er mat á hvort tveir titlar passi saman. Skildu það eftir tómt til að slökkva á báðum. Sjálfgefna stillingin fjarlægir algengt rusl eins og „Official Video“, „Remaster“, „【插曲】“ og „- Topic“. @@ -562,6 +583,10 @@ Nauðsynlegar vafrakökur: SID, HSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPI Regluleg segð sem er eingöngu beitt á upprunalega textann (ekki þýðingar eða rómavæðingar). Skilið eftir tómt til að slökkva á. Upplýsingalínumerki Ef textar eru ranglega síaðir, stilltu eða hreinsaðu þetta. Skilið eftir tómt til að slökkva á + Hæfa fyrsta staf + Hæfir fyrsta staf hvers lagalínustróks + Fjarlægja punktastaðsetningu í lok + Fjarlægir punktum, kommaum, skýlu- og hálfpunktum í lok lagalínna. Spurnarmerki, utrópamerki, þrír punktar, gæslumerki og svigarar eru varðveittir Leturstærð Stærð textans. Altæk tímafærsla diff --git a/patches/src/main/resources/addresources/values-is-rIS/youtube/strings.xml b/patches/src/main/resources/addresources/values-is-rIS/youtube/strings.xml index d232211c46..26b2580055 100644 --- a/patches/src/main/resources/addresources/values-is-rIS/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-is-rIS/youtube/strings.xml @@ -383,6 +383,9 @@ Tilkynningar eru opinberar og endurskoðaðar áður en rás er bætt við lista Fela verslunarhlökki Fela vídeó auglýsingar Felaðu YouTube Premium kynningu + + + Hlaða… Núverandi rás: Bæta við á hvítlista @@ -399,6 +402,9 @@ Tilkynningar eru opinberar og endurskoðaðar áður en rás er bætt við lista Leitarniðurstöður rása Ekkert fannst á þessari rás Ekki tókst að leita í þessari rás + Myndbönd + Póstar + Engar færslur á þessum rás samsvara Mikilvægi Mest áhorft Minnst áhorft @@ -484,6 +490,9 @@ Eftir að myndband með Ask hnappi hefur verið opnað einu sinni, virkar eiginl Myndband Hljóð Allar stillingar + Sýna niðurhal í Shorts-valmynd + Bætir við útvíkkuðu niðurhalstæki í Shorts-spilaranum + Niðurhal Þýðing á raddsegulmagni Les upp undirtitla myndbandsins með texta-til-tala á tækinu, samhæmt með endursendingu @@ -1104,9 +1113,6 @@ Bílaútlit "Endurskipar gamla útlit spilarans Takmörkun: Ef 'Endurskapa gamla stjórnbara myndbands' er slökkt, gæti 'Fleiri myndbönd' lagskiptarinn ekki verið tiltæk" - "Með því að virkja þetta gætu sumir eiginleikar uppsetningar spilarans skemst, svo sem athugasemdadeildin eða \"Meiri myndbönd\" sem sýnd eru neðan spilarans. - -Uppsetning fyrir spjaldtölvur er ekki lengur studduð af YouTube og er að hluta eða algjörlega óvirkar í nýlegum YouTube útgáfum. Til að fá virka uppsetningu fyrir spjaldtölvur, uppfærið YouTube 20.21.37" 21.11.48 - Endurheimta gamla textahegðun 20.39.41 - Endursafnaðu gamla vistun á spilalista @@ -1603,4 +1609,17 @@ Takmörkun: Snýr aftur í gamla 'Fleiri myndbönd' yfirlagsstílinn" Engar raddir fundust Ritlaus episod Ótiltæk episod + + Niconico athugasemdir + Sýna beina spjall og endurtöku spjalls sem athugasemdir sem renna yfir myndbandið + Sýnir yfirlegga hnapp í myndbandaspilaranum. Ýttu til að sýna eða fela athugasemdir, ýttu og haltuu inni fyrir fljótstillingar + Leturstærð athugasemda + Textastærð athugasemda (12-40 sp) + Gegnumsýni athugasemda + Gegnumsýni athugasemdatexta og bakgrunns + Ferðatími + Hversu margar sekúndur tekur athugasemdum að fara yfir skjáinn. Lægri gildi hreyfast hraðar + Hámarks lengd athugasemdar + Fela athugasemdir sem innihalda fleiri staf en þetta. Hver tiltekni tákn taldist eins og einn stafur + Engin takmörkun diff --git a/patches/src/main/resources/addresources/values-it-rIT/music/strings.xml b/patches/src/main/resources/addresources/values-it-rIT/music/strings.xml index 6590be75c3..f3fe1cdbb0 100644 --- a/patches/src/main/resources/addresources/values-it-rIT/music/strings.xml +++ b/patches/src/main/resources/addresources/values-it-rIT/music/strings.xml @@ -549,8 +549,29 @@ Cookie richiesti: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPISID Mostra la riga corrente sullo schermo di blocco, Android Auto e Bluetooth. Richiede Android 11 o versioni successive Mostra i testi nel mini-player Mostra la riga corrente nel miniplayer, con il brano spostato nei sottotitoli - Visualizzazione di artista e titolo su MediaSession. ON \"Artista - Titolo\" e OFF \"Titolo - Artista\". Non influisce sulla ricerca dei testi. - Alterna l\'ordine di visualizzazione di Artista e Titolo nel Miniplayer e in MediaSession. ON \"Artista - Titolo\" e OFF \"Titolo - Artista\" Non influisce sulla ricerca dei testi + Formato titolo brano/artista + Formato della traccia mostrata con i testi nel miniplayer e nella MediaSession. {t} è il titolo, {a} è l\'artista e {v} è la versione, come ad esempio Acoustic, quando la sorgente dei testi ne fornisce una. Il testo tra due due punti viene mostrato solo quando ogni variabile al suo interno ha un valore; scrivi \\: per un semplice due punti. Ogni riga viene mostrata a turno, aggiornandosi ogni volta che il testo cambia + Formato riga del testo + Formato della riga dei testi nel miniplayer e nella MediaSession. {lrc} è la riga e {n} è il numero di volte in cui viene cantata consecutivamente. Il testo tra i due punti viene mostrato solo quando la riga si ripete. Usa \\: per i due punti + Il formato deve contenere {lrc} + Modello della riga dell\'artista e tempistica dell\'animazione + Tempo minimo di visualizzazione (ms) + Tempo massimo di visualizzazione (ms) + Solo per animazioni multilinea. Mantieni ogni fotogramma per almeno il tempo minimo. Al massimo, fai avanzare la riga dell\'artista anche quando il testo del brano rimane lo stesso. + Il valore minimo deve essere 0 o superiore. Il valore massimo deve essere superiore a 0 e almeno pari al minimo. + Limiti della lunghezza della riga + Limiti della riga del brano e dell\'artista, e peso dei caratteri a larghezza intera + "Limite riga brano +Dividi i testi più lunghi in metà equilibrate. 0 disabilita la divisione." + "Limite righe artista +Dividi il formato della traccia popolata in frame ciclici. 0 disabilita la divisione." + "Peso caratteri a larghezza intera +Si applica a entrambi i limiti. A 1,75, quattro caratteri a larghezza intera contano come 7." + Inserisci limiti numerici interi da 0 a 10000 e un peso dei caratteri superiore a 0 + Intermezzo strumentale + Visualizzato nel miniplayer e nella MediaSession durante un intermezzo strumentale. Ogni riga è un frame di animazione. {t} è il titolo, {a} è l\'artista e {lrc} è l\'ultima riga del testo + Offset temporale di miniplayer e MediaSession + Sposta il momento in cui la riga successiva viene mostrata nel miniplayer e in MediaSession. Usa un valore negativo se le righe appaiono in ritardo, ad esempio nella schermata di blocco Filtri Regex filtro testi Espressione regolare applicata a titoli, artisti e album prima di cercare i testi e quando si valuta se due titoli corrispondono. Lascia vuoto per disabilitare entrambe. L\'impostazione predefinita rimuove i soliti elementi superflui come \"Official Video\", \"Remaster\", \"【插曲】\" e \"- Topic\". @@ -558,6 +579,10 @@ Cookie richiesti: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPISID L\'espressione regolare viene applicata solo al testo originale dei testi (non alle traduzioni o alle romanizzazioni). Lascia vuoto per disattivare Indicatori di riga delle informazioni Sincronizza i testi con la musica + Maiuscola alla prima lettera + Rende maiuscola la prima lettera di ogni riga del testo + Rimuovi punteggiatura finale + Rimuove punti, virgole, due punti e punti e virgola alla fine delle righe del testo. I punti interrogativi, i punti esclamativi, i puntini di sospensione, le virgolette e le parentesi vengono mantenuti Dimensione del testo Dimensione del testo dei brani Offset temporale globale diff --git a/patches/src/main/resources/addresources/values-it-rIT/youtube/strings.xml b/patches/src/main/resources/addresources/values-it-rIT/youtube/strings.xml index dc35b53278..d4d1997609 100644 --- a/patches/src/main/resources/addresources/values-it-rIT/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-it-rIT/youtube/strings.xml @@ -393,6 +393,9 @@ Le segnalazioni sono pubbliche e vengono esaminate prima che un canale venga agg Nascondi link per gli acquisti Nascondi le pubblicità del video Nascondi le promozioni di YouTube Premium + + + Caricamento… Canale attuale: Aggiungi alla whitelist @@ -409,6 +412,9 @@ Le segnalazioni sono pubbliche e vengono esaminate prima che un canale venga agg Risultati della ricerca dei canali Nessun risultato trovato su questo canale Impossibile cercare in questo canale + Video + Post + Nessun post in questo canale corrisponde Rilevanza Più visti Meno visti @@ -494,6 +500,9 @@ Dopo aver aperto una volta un video con il pulsante Chiedi, la funzionalità fun Video Audio Tutte le opzioni + Mostra il download nel menu Shorts + Aggiunge un elemento di menu flyout per il downloader esterno al player Shorts + Download Traduzione voice over Leggi ad alta voce i sottotitoli del video utilizzando la sintesi vocale del dispositivo, sincronizzata con la riproduzione @@ -1121,9 +1130,6 @@ Layout automotive "Ripristina il layout precedente del player Limitazione: se \"Ripristina la vecchia barra delle azioni video\" è disattivato, la sovrapposizione \"Altri video\" potrebbe non essere disponibile" - "L'abilitazione di questa opzione potrebbe compromettere alcune funzioni di layout del player, come la sezione commenti o i \"Altri video\" mostrati sotto il player. - -Il layout per tablet non è più supportato da YouTube ed è parzialmente o totalmente compromesso nelle versioni recenti di YouTube. Per un layout tablet funzionante, applica la patch a YouTube 20.21.37" 21.11.48 - Ripristina il vecchio comportamento dei sottotitoli 20.39.41 - Ripristina le vecchie icone del player Shorts @@ -1626,4 +1632,17 @@ La registrazione automatica è facoltativa. Si arresta in modalità incognito o Nessuna serie trovata Episodio senza titolo Episodio non disponibile + + Commenti Niconico + Mostra la chat dal vivo e il replay della chat come commenti che scorrono attraverso il video + Mostra un pulsante in sovrapposizione nel lettore video. Tocca per mostrare o nascondere i commenti, tocca e tieni premuto per le impostazioni rapide + Dimensione carattere commenti + Dimensione del testo dei commenti (12-40 sp) + Opacità commenti + Opacità del testo dei commenti e degli sfondi + Durata del tragitto + Quanti secondi impiegano i commenti per attraversare lo schermo. Valori più bassi aumentano la velocità + Lunghezza massima dei commenti + Nasconde i commenti con più caratteri di questo. Ogni emoji conta come un carattere + Nessun limite diff --git a/patches/src/main/resources/addresources/values-iw-rIL/music/strings.xml b/patches/src/main/resources/addresources/values-iw-rIL/music/strings.xml index 7f5c1ce064..f578a8ed96 100644 --- a/patches/src/main/resources/addresources/values-iw-rIL/music/strings.xml +++ b/patches/src/main/resources/addresources/values-iw-rIL/music/strings.xml @@ -555,8 +555,29 @@ Second \"item\" text" מציג את הקו הנוכחי במסך הנעילה, Android Auto ובבלוטות\'. דורש אנדרואיד 11 או חדש יותר הצג מילות שיר במיני-נגן מציג את הקו הנוכחי במיניפלייר, עם הרצועה שהועברה לסאבטיילר - סדר התצוגה של אמן וכותרת - החלף את סדר התצוגה של אמן וכותרת במיני-נגן וב-MediaSession. במצב ON מוצג \"אמן - כותרת\" ובמצב OFF \"כותרת - אמן\". אינו משפיע על חיפוש מילות השיר + פורמט כותרת שיר/אמן + פורמט הרצועה המוצגת עם המילים בנגן הממוזער וב-MediaSession. {t} הוא הכותרת, {a} הוא האמן ו-{v} היא הגרסה, כגון אקוסטי, כאשר מקור המילים מספק כזו. טקסט בין שני נקודתיים יוצג רק כאשר לכל משתנה בתוכו יש ערך; כתוב \\: עבור נקודתיים רגילות. כל שורה מוצגת בתורה, ועוברת לשורה הבאה בכל פעם שהמילים משתנות + פורמט שורת מילים + פורמט שורת המילים בנגן הממוזער וב-MediaSession. {lrc} היא השורה ו-{n} הוא מספר הפעמים שהיא נשרית ברצף. טקסט בין נקודתיים מוצג רק כאשר השורה חוזרת על עצמה. השתמש ב-\\: עבור נקודתיים + הפורמט חייב להכיל {lrc} + תבנית שורת אמן ותזמון אנימציה + זמן מסך מינימלי (מילישניות) + זמן מסך מקסימלי (מילישניות) + עבור אנימציות רב-שוריות בלבד. השאר כל פריים למשך זמן מינימלי לפחות. בזמן המקסימלי, התקדם בשורת האמן גם כאשר המילים נשארות ללא שינוי. + המינימום חייב להיות 0 או יותר. המקסימום חייב להיות גדול מ-0 ולפחות בגובה המינימום. + מגבלות אורך שורה + מגבלות שורות למילים ולאמן, ומשקל תווים ברוחב מלא + "מגבלת שורת מילים +פצל מילים ארוכות לחצאים מאוזנים. 0 מבטל את הפיצול." + "מגבלת שורות לאמן +פיצול פורמט הרצועה המאוכלס למסגרות מחזוריות. 0 מבטל את הפיצול." + "משקל תווים ברוחב מלא +חל על שתי המגבלות. ב-1.75, ארבעה תווים ברוחב מלא נספרים כ-7." + הזן מגבלות כמספרים שלמים מ-0 עד 10000 ומשקל תווים הגדול מ-0 + קטע אינסטרומנטלי + מוצג בנגן הממוזער וב-MediaSession במהלך קטע אינסטרומנטלי. כל שורה היא מסגרת אנימציה אחת. {t} הוא הכותרת, {a} הוא האמן ו-{lrc} היא שורת המילים האחרונה + היסט תזמון של נגן הממוזער ו-MediaSession + מזיז את הזמן שבו השורה הבאה מוצגת בנגן הממוזער וב-MediaSession. השתמש בערך שלילי אם השורות מופיעות באיחור, לדוגמה במסך הנעילה מסננים ביטוי רגולרי למטא-דאטה ביטוי רגולרי המוחל על שמות שירים, אמנים ואלבומים לפני חיפוש מילות השיר וכאשר בודקים אם שני שמות תואמים. השאר ריק כדי להשבית את שניהם. ברירת המחדל מסירה רעש נפוץ כמו \"Official Video\", \"Remaster\", \"【插曲】\" וגם \"- Topic\". @@ -564,6 +585,10 @@ Second \"item\" text" ביטוי רגולרי המוחל רק על טקסט מילות השיר המקורי (לא תרגומים או רומניזציות). השאר ריק כדי להשבית סמני שורות מידע אם מילות שיר מסוננות בטעות לתחתית, כוונן או נקה את זה. סמנים מופרדים בפסיקים: שורות המכילות סמן ואחריו נקודתיים יוצגו כפרטי קרדיט בתחתית. השאר ריק כדי להשבית + הפוך את האות הראשונה לגדולה + הופך את האות הראשונה של כל שורת מילים לאות גדולה + הסר סימני פיסוק בסוף השורה + מסיר נקודות, פסיקים, נקודתיים ונקודות-פסיק בסוף שורות המילים. סימני שאלה, סימני קריאה, שלוש נקודות, מרכאות וסוגריים נשמרים גודל טקסט גודל טקסט המילים היסט תזמון גלובלי diff --git a/patches/src/main/resources/addresources/values-iw-rIL/youtube/strings.xml b/patches/src/main/resources/addresources/values-iw-rIL/youtube/strings.xml index 6e94e3d1b2..403c7ce3cc 100644 --- a/patches/src/main/resources/addresources/values-iw-rIL/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-iw-rIL/youtube/strings.xml @@ -393,6 +393,9 @@ Second \"item\" text" הסתר קישורי קניות הסתר מודעות סרטון הסתר קידומי YouTube Premium + + + טוען… ערוץ נוכחי: הוסף לרשימה הלבנה @@ -409,6 +412,9 @@ Second \"item\" text" תוצאות חיפוש ערוצים לא נמצא דבר בערוץ זה לא ניתן לחפש בערוץ זה + סרטונים + פוסטים + אין פוסטים בערוץ זה התואמים לחיפוש רלוונטיות הנצפים ביותר הפחות נצפים @@ -442,6 +448,7 @@ Second \"item\" text" הורדת תמונה הורדת %s תמונות... מוריד תמונה… + %1$s תמונות נשמרו ב-%2$s התמונה נשמרה ב-%s לא ניתן להוריד את התמונה לא נמצאה תמונה בפוסט זה @@ -493,6 +500,9 @@ Second \"item\" text" סרטון שמע כל האפשרויות + הצג הורדה בתפריט Shorts + מוסיף פריט תפריט קופץ של מוריד חיצוני לפייסטיינר של Shorts + הורד תרגום דיבוב קריאת כתוביות הסרטון בקול רם באמצעות טקסט לדיבור מקומי, בסנכרון עם ההפעלה @@ -1120,9 +1130,6 @@ Second \"item\" text" "משחזר את פריסת הנגן הישנה מגבלה: אם \"שחזר סרגל הפעולות של הווידאו הישן\" כבוי, ייתכן ש-\"עוד סרטונים\" לא יהיה זמין" - "הפעלת אפשרות זו עלולה לשבש חלק מתכונות הפריסה של הנגן, כגון אזור התגובות או \"סרטונים נוספים\" המוצגים מתחת לנגן. - -פריסת הטאבלט אינה נתמכת יותר על ידי YouTube, והיא פגומה חלקית או באופן מלא בגרסאות YouTube האחרונות. לקבלת פריסת טאבלט תקינה, יש להחיל פאץ' ל-YouTube 20.21.37" 21.11.48 - שחזור התנהגות הכתוביות הישנה 20.39.41 - שחזר שפופרות ישנות לפלייליסט @@ -1625,4 +1632,17 @@ AVC (H.264) לא נמצאו סדרות פרק ללא שם פרק לא זמין + + תגובות Niconico + הצגת צ\'אט חי והשמעה חוזרת של צ\'אט כתגובות שגולשות על פני הסרטון + מציג כפתור שכבה בנגן הווידאו. הקשה להצגה או הסתרה של התגובות, לחיצה ארוכה להגדרות מהירות + גודל גופן של תגובה + גודל הטקסט של התגובות (12-40 sp) + שקיפות תגובה + שקיפות של טקסט התגובה והרקעים + משך זמן מעבר + כמה שניות לוקח לתגובות לחצות את המסך. ערכים נמוכים יותר נעים מהר יותר + אורך תגובה מקסימלי + מסתיר תגובות עם יותר תווים מזה. כל אימוג\'י נחשב כתו אחד + ללא הגבלה diff --git a/patches/src/main/resources/addresources/values-ja-rJP/music/strings.xml b/patches/src/main/resources/addresources/values-ja-rJP/music/strings.xml index 7e58f3f759..c28e04a07d 100644 --- a/patches/src/main/resources/addresources/values-ja-rJP/music/strings.xml +++ b/patches/src/main/resources/addresources/values-ja-rJP/music/strings.xml @@ -548,8 +548,29 @@ SABR再生(動画ストリームのスプーフがオフ)" ロック画面、Android Auto、Bluetooth で現在の行を表示。Android 11 以降が必要 ミニプレイヤーの歌詞を表示 ミニプレイヤーで現在行を表示し、トラックをサブタイトルに移動 - メディアセッションでアーティスト名とタイトルを表示[…] ON = \"アーティスト - タイトル\"、OFF = \"タイトル - アーティスト\" この設定は歌詞検索には影響しません - Miniplayer と MediaSession で、アーティスト名とタイトルの表示順を切り替えます。ON「アーティスト - タイトル」、OFF「タイトル - アーティスト」。歌詞検索には影響しません。 + 曲名/アーティストの形式 + ミニプレイヤーおよびMediaSessionで歌詞と共に表示されるトラックの形式です。{t}は曲名、{a}はアーティスト、{v}は歌詞ソースが提供している場合のアコースティックなどのバージョンを表します。2つのコロンで囲まれたテキストは、その中のすべての変数に値がある場合にのみ表示されます。通常のコロンを表示するには\\:と入力してください。各行は歌詞が変わるたびに順番に表示されます + 歌詞行の形式 + ミニプレイヤーおよびMediaSessionにおける歌詞行の形式です。{lrc}は歌詞の行、{n}はその行が連続して歌われる回数を表します。コロンで囲まれたテキストは、行が繰り返される場合にのみ表示されます。コロンを表示するには\\:を使用してください + 形式には {lrc} を含める必要があります + アーティスト行のテンプレートとアニメーションタイミング + 最小表示時間 (ms) + 最大表示時間 (ms) + 複数行アニメーションのみに適用されます。各フレームを少なくとも最小時間分保持します。最大時間に達すると、歌詞が同じままでもアーティスト行を進めます。 + 最小値は0以上である必要があります。最大値は0より大きく、かつ最小値以上である必要があります。 + 行の長さ制限 + 歌詞とアーティスト行の制限、および全角文字のウェイト + "歌詞の行制限 +長い歌詞をバランスよく半分に分割します。0に設定すると分割を無効にします" + "アーティストの行数制限 +入力されたトラック形式をサイクリングフレームに分割します。0に設定すると分割を無効にします。" + "全角文字の重み +両方の制限に適用されます。1.75に設定した場合、全角文字4文字は7としてカウントされます。" + 0から10000までの整数値の制限と、0より大きい文字の重みを入力してください + インストゥルメンタル・ブレイク + インストゥルメンタル・ブレイク中にミニプレイヤーとMediaSessionに表示されます。各行がアニメーションフレームになります。{t}はタイトル、{a}はアーティスト、{lrc}は最後の歌詞の行です + ミニプレイヤーとMediaSessionのタイミングオフセット + ミニプレイヤーとMediaSessionで次の行が表示されるタイミングをずらします。ロック画面などで表示が遅れる場合は、負の値を指定してください フィルタ メタデータの正規表現 曲名、アーティスト、アルバムに適用される正規表現です。歌詞の検索前と、2つの曲名が一致するかどうかの判定時に使用されます。空にすると両方が無効になります。デフォルトでは、\"Official Video\"、\"Remaster\"、\"【插曲】\"、\"- Topic\" などのよくある不要な部分を除去します。 @@ -557,6 +578,10 @@ SABR再生(動画ストリームのスプーフがオフ)" 元の歌詞テキストのみに適用される正規表現です(翻訳やローマ字表記には適用されません)。無効にする場合は空欄にしてください。 クレジット行の判別マーカー マーカーをカンマ区切りで入力してください。行内にマーカーを含み、その直後にコロン (:) が続く行を、クレジット情報として最下部に表示します。空欄にするとこの機能自体が無効になります。歌詞が誤って最下部に振り分けられてしまう場合は、この設定の値 (文字列) を調整するか空欄にしてください + 先頭文字を大文字にする + 歌詞の各行の先頭文字を大文字にします + 末尾の句読点を削除 + 歌詞の行末にあるピリオド、カンマ、コロン、セミコロンを削除します。疑問符、感嘆符、省略記号、引用符、括弧は保持されます 文字サイズ 歌詞の文字サイズ 全体のタイミングオフセット diff --git a/patches/src/main/resources/addresources/values-ja-rJP/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-ja-rJP/shared-youtube/strings.xml index 1160075de3..f58f4823f7 100644 --- a/patches/src/main/resources/addresources/values-ja-rJP/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ja-rJP/shared-youtube/strings.xml @@ -164,7 +164,7 @@ YouTube Premium を利用している場合、この設定は必要ない場合 オーディオ DRC を無効化 オリジナルの音声を常に使用 - 常にオリジナルの音声トラック (言語) を再生します。オートダビングを含む吹き替えの音声トラックは再生されなくなります + オリジナルの音声トラック (言語) を常に再生します。オートダビングを含む吹き替えの音声トラックは再生されなくなります 無音の区間をスキップ 動画内の無音の部分や会話などの間を自動的にスキップします diff --git a/patches/src/main/resources/addresources/values-ja-rJP/youtube/strings.xml b/patches/src/main/resources/addresources/values-ja-rJP/youtube/strings.xml index 02076fe9b5..82c956de64 100644 --- a/patches/src/main/resources/addresources/values-ja-rJP/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ja-rJP/youtube/strings.xml @@ -394,6 +394,9 @@ AI について扱っているだけのチャンネルや、動画の一部に A 商品へのリンクを非表示 動画広告を非表示 YouTube Premium の広告を非表示 + + + 読み込み中… 現在のチャンネル: ホワイトリストに追加 @@ -408,8 +411,11 @@ AI について扱っているだけのチャンネルや、動画の一部に A チャンネルページに、そのチャンネルだけを検索する別の検索ボタンを追加します チャンネル内を検索 チャンネル内の検索結果 - このチャンネル内では見つかりませんでした + このチャンネル内にはありません このチャンネル内を検索できませんでした + 動画 + 投稿 + このチャンネル内に一致する投稿はありません 関連度順 視聴回数が多い順 視聴回数が少ない順 @@ -437,16 +443,16 @@ AI について扱っているだけのチャンネルや、動画の一部に A サムネイルに「後で見る」ボタンを表示 フィード動画のサムネイルに「後で見る」ボタンが表示され、それをタップして長押しすると「プレイリストに保存」が開きます - 投稿画像のダウンロード - 画像またはGIFを含む投稿の共有ボタンの隣にダウンロードボタンが表示され、メニューにダウンロード項目が追加されます + 投稿の画像をダウンロード + 画像または GIF 画像を含む投稿の共有ボタンの隣に、表示中の画像をダウンロードするボタンを表示します。同様の投稿のフライアウト メニューに、投稿内のすべての画像をダウンロードするための項目を追加します 画像をダウンロード 画像をダウンロード - %s枚の画像をダウンロード中… + %s 個の画像をダウンロード中… 画像をダウンロード中… - %1$s枚の画像を %2$s に保存しました - 画像を %s に保存しました + %2$s に %1$s 個の画像を保存しました + %s に画像を保存しました 画像のダウンロードに失敗しました - このポストに画像は見つかりませんでした + この投稿には画像がありません 「動画を要約して」をフライアウト メニューに追加 「この動画について質問する」欄を、欄内の「動画を要約して」がタップされた状態で開く項目を、フィードのフライアウト メニューに追加します @@ -495,6 +501,9 @@ AI について扱っているだけのチャンネルや、動画の一部に A 動画 オーディオ すべてのオプション + Shortsメニューでダウンロードを表示する + Shortsプレーヤーに外部ダウンローダーフライアウトメニュー項目を追加します + ダウンロード 吹き替え翻訳 動画字幕を翻訳し、デバイス上のテキスト読み上げ機能で字幕の翻訳を字幕に同期しながら読み上げます @@ -893,8 +902,8 @@ APIキーは決して共有しないでください。アクセス権を持つ デフォルト 半透明 透明 - 不透明 - このスタイルはお使いのデバイスまたはYouTubeのバージョンでサポートされていない可能性があり、レイアウトの問題やクラッシュの原因となることがあります + 全く透けない + このスタイルは、デバイスや YouTube のバージョンによってはサポートされていない場合があり、レイアウトの問題やクラッシュを引き起こす可能性があります ツールバー ツールバーに表示するコンポーネントを設定します キャストボタンを非表示 @@ -1122,9 +1131,6 @@ Automotive レイアウト "動画プレーヤーのレイアウトを以前のスタイルに戻します 制限事項: [アクションバーを旧仕様に戻す] がオフのとき、「その他の動画」オーバーレイが表示されない場合があります" - "これを有効にすると、コメント欄やプレイヤー下に表示される「おすすめ動画」など、一部のプレイヤーレイアウト機能が正しく動作しなくなる可能性があります。 - -タブレットレイアウトはYouTubeによってサポートされなくなったため、最近のYouTubeバージョンでは部分的または完全に機能しません。タブレットレイアウトを動作させるには、YouTube 20.21.37にパッチを適用してください" 21.11.48 - 字幕の動作を旧仕様に戻す 20.39.41 -「再生リストに保存」を旧仕様に戻す @@ -1354,19 +1360,19 @@ Automotive レイアウト 自動 画質を保存 - 画質メニューでの画質の変更を、設定の「デフォルトの画質」に反映し、すべての動画に適用します + 画質メニューでの画質の変更を、設定の [デフォルトの画質] に反映し、すべての動画に適用します 画質変更の通知 設定の [デフォルトの画質] または [ショートのデフォルトの画質] に新しい値が保存されたときに通知を表示します デフォルトの画質 (Wi-Fi) デフォルトの画質 (モバイル) ショートの画質を保存 - 画質メニューでの画質の変更を、設定の「ショートのデフォルトの画質」に反映し、すべてのショート動画に適用します - デフォルトのショートの画質 (Wi-Fi) - デフォルトのショートの画質 (モバイル) - デフォルトの画質 (モバイル) の変更: %s - デフォルトの画質 (Wi-Fi) の変更: %s - ショートの画質 (モバイル) の変更: %s - ショートの画質 (Wi-Fi) の変更: %s + 画質メニューでの画質の変更を、設定の [ショートのデフォルトの画質] に反映し、すべてのショート動画に適用します + ショートのデフォルトの画質 (Wi-Fi) + ショートのデフォルトの画質 (モバイル) + デフォルトの画質 (モバイル): %s + デフォルトの画質 (Wi-Fi): %s + ショートの画質 (モバイル): %s + ショートの画質 (Wi-Fi): %s 再生速度ボタンを表示 長押しすると、再生速度をデフォルトの値にリセットします @@ -1391,13 +1397,13 @@ Automotive レイアウト 再生速度変更の通知 設定の [デフォルトの再生速度] に新しい値が保存されたときに通知を表示します デフォルトの再生速度 - ショートの再生速度のデフォルト値 - ショートの再生速度を記憶 - 手動で選択したショートの再生速度を、今後のすべてのショートに適用します。オフにすると、ショートプレーヤーを閉じた後に速度がデフォルトに戻ります - ショートのメニューに再生速度を表示 - ショートプレーヤーのメニューに再生速度項目を追加します + ショートのデフォルトの再生速度 + ショートの再生速度を保存 + 再生速度メニューでのショート動画の再生速度の変更を、設定の [ショートのデフォルトの再生速度] に反映し、すべてのショート動画に適用します。ショート プレーヤーを閉じるとデフォルトの値が適用されるため、この機能がオフの場合、ショート プレーヤーを閉じると変更した値からデフォルトの値に戻ります + ショートのメニューに「再生速度」を表示 + ショート プレーヤーのフライアウト メニューに「再生速度」を追加します 再生速度 · %s - デフォルトの再生速度の変更: %s + デフォルトの再生速度: %s 音楽動画を通常速度で再生 音楽動画を常に 1.0 倍速で再生します チャンネル再生速度ホワイトリスト @@ -1627,4 +1633,17 @@ AV1 での動画再生はカクつくか、フレームが欠落する可能性 シリーズが見つかりません タイトルなしのエピソード 利用不可なエピソード + + ニコニコ風コメント + ライブチャットとチャットのリプレイを、ビデオ上を流れるコメントとして表示します + ビデオプレーヤーにオーバーレイボタンを表示します。タップするとコメントの表示/非表示を切り替え、長押しすると簡易設定を表示します + コメントのフォントサイズ + コメントのテキストサイズ (12-40 sp) + コメントの不透明度 + コメントのテキストと背景の不透明度 + 移動時間 + コメントが画面を横切るまでの秒数です。値を小さくすると移動速度が上がります + コメントの最大文字数 + これより長い文字数のコメントを非表示にします。絵文字は1文字としてカウントされます + 制限なし diff --git a/patches/src/main/resources/addresources/values-ka-rGE/youtube/strings.xml b/patches/src/main/resources/addresources/values-ka-rGE/youtube/strings.xml index 5dd6a3cae5..947132f120 100644 --- a/patches/src/main/resources/addresources/values-ka-rGE/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ka-rGE/youtube/strings.xml @@ -51,6 +51,8 @@ Second \"item\" text" + + @@ -231,4 +233,5 @@ Second \"item\" text" ნაგულისხმები + diff --git a/patches/src/main/resources/addresources/values-kk-rKZ/youtube/strings.xml b/patches/src/main/resources/addresources/values-kk-rKZ/youtube/strings.xml index 9079108272..70ce03fc67 100644 --- a/patches/src/main/resources/addresources/values-kk-rKZ/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-kk-rKZ/youtube/strings.xml @@ -51,6 +51,8 @@ Second \"item\" text" + + @@ -224,4 +226,5 @@ Second \"item\" text" + diff --git a/patches/src/main/resources/addresources/values-km-rKH/youtube/strings.xml b/patches/src/main/resources/addresources/values-km-rKH/youtube/strings.xml index b4f319c9fc..3b8e359e4e 100644 --- a/patches/src/main/resources/addresources/values-km-rKH/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-km-rKH/youtube/strings.xml @@ -63,6 +63,8 @@ Second \"item\" text" + + ដកចេញ @@ -83,6 +85,7 @@ Second \"item\" text" វីដេអូ + ដោនឡូដ ផ្ទាល់ខ្លួន @@ -255,4 +258,5 @@ Second \"item\" text" រក្សាទុក ប្ដូរឈ្មោះ អាន់ឌូ + diff --git a/patches/src/main/resources/addresources/values-kmr-rTR/youtube/strings.xml b/patches/src/main/resources/addresources/values-kmr-rTR/youtube/strings.xml index 8edaad40b7..34e7a9887b 100644 --- a/patches/src/main/resources/addresources/values-kmr-rTR/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-kmr-rTR/youtube/strings.xml @@ -313,6 +313,8 @@ Sînorkirin: Girêkên firotinê veşêrin Reklamên vîdyoyê veşêrin Promosyonên YouTube Premium veşêre + + Kanalê niha: Tê bikirin listeya spî @@ -1115,4 +1117,5 @@ Sînordarî: Ger 'Restore old video action bar' veqetandibe, 'More videos' dîti Otomatîk Bixweber + diff --git a/patches/src/main/resources/addresources/values-kn-rIN/youtube/strings.xml b/patches/src/main/resources/addresources/values-kn-rIN/youtube/strings.xml index a1a0baa07c..f5ee93a510 100644 --- a/patches/src/main/resources/addresources/values-kn-rIN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-kn-rIN/youtube/strings.xml @@ -51,6 +51,8 @@ Second \"item\" text" + + @@ -228,4 +230,5 @@ Second \"item\" text" Custom Continue + diff --git a/patches/src/main/resources/addresources/values-ko-rKR/music/strings.xml b/patches/src/main/resources/addresources/values-ko-rKR/music/strings.xml index de748a45cb..3f2f563b49 100644 --- a/patches/src/main/resources/addresources/values-ko-rKR/music/strings.xml +++ b/patches/src/main/resources/addresources/values-ko-rKR/music/strings.xml @@ -557,8 +557,29 @@ LOGIN_INFO" 잠금 화면, Android Auto 및 블루투스에서 현재 가사 줄을 표시합니다. Android 11 이상이 필요합니다 미니 플레이어 가사 표시하기 미니 플레이어에 현재 가사 줄을 표시하고, 노래 정보를 제목 하단으로 이동합니다 - 아티스트 및 제목 표시 순서 - 미니 플레이어와 MediaSession에서 아티스트와 제목의 표시 순서를 전환합니다. 활성화되면 \'아티스트 - 제목\', 비활성화되면 \'제목 - 아티스트\'로 표시됩니다. 가사 검색에는 영향을 주지 않습니다 + 곡 제목/아티스트 형식 + 미니플레이어 및 MediaSession에서 가사와 함께 표시되는 트랙 형식입니다. {t}는 제목, {a}는 아티스트, {v}는 가사 소스에서 제공하는 경우 Acoustic과 같은 버전입니다. 두 개의 콜론 사이의 텍스트는 내부의 모든 변수에 값이 있을 때만 표시됩니다. 일반 콜론을 입력하려면 \\:를 작성하세요. 각 줄은 가사가 변경될 때마다 차례대로 표시됩니다 + 가사 줄 형식 + 미니플레이어 및 MediaSession의 가사 줄 형식입니다. {lrc}는 해당 줄이고 {n}은 연속으로 노래하는 횟수입니다. 콜론 사이의 텍스트는 줄이 반복될 때만 표시됩니다. 콜론을 사용하려면 \\:를 사용하세요 + 형식에 {lrc}가 포함되어야 합니다 + 아티스트 라인 템플릿 및 애니메이션 타이밍 + 최소 화면 표시 시간 (ms) + 최대 화면 표시 시간 (ms) + 다중 라인 애니메이션에만 적용됩니다. 각 프레임을 최소 시간 동안 유지합니다. 최대 시간에 도달하면 가사가 동일하더라도 아티스트 라인을 다음으로 넘깁니다. + 최소값은 0보다 크거나 같아야 합니다. 최대값은 0보다 커야 하며 최소값보다 크거나 같아야 합니다. + 라인 길이 제한 + 가사 및 아티스트 라인 제한, 전각 문자 가중치 + "가사 라인 제한 +긴 가사를 균형 잡힌 두 부분으로 나눕니다. 0으로 설정하면 분할 기능이 비활성화됩니다." + "아티스트 라인 제한 +입력된 트랙 형식을 순환 프레임으로 분할합니다. 0으로 설정하면 분할되지 않습니다." + "전각 문자 가중치 +두 제한 사항 모두에 적용됩니다. 1.75로 설정하면 전각 문자 4개가 7개로 계산됩니다." + 0에서 10000 사이의 정수 제한 값과 0보다 큰 문자 가중치를 입력하세요 + 연주 간주 + 연주 간주 중에 미니플레이어 및 MediaSession에 표시됩니다. 각 라인은 하나의 애니메이션 프레임입니다. {t}는 제목, {a}는 아티스트, {lrc}는 마지막 가사 라인입니다 + 미니플레이어 및 MediaSession 타이밍 오프셋 + 미니플레이어 및 MediaSession에서 다음 가사 줄이 표시되는 시점을 조정합니다. 잠금 화면 등에서 가사가 늦게 표시되는 경우 음수 값을 사용하세요 필터 메타데이터 정규식 가사를 검색하기 전과 두 제목이 일치하는지 판단할 때 제목, 아티스트, 앨범에 적용할 정규식입니다. 비워 두면 두 기능 모두 비활성화됩니다. 기본값은 \'Official Video\', \'Remaster\', \'【插曲】\', \'- Topic\'과 같은 불필요한 문구를 제거합니다 @@ -566,6 +587,10 @@ LOGIN_INFO" 원본 가사 텍스트에만 적용되는 정규식입니다. 번역이나 로마자 표기에는 적용되지 않습니다. 비워두면 비활성화됩니다 정보 줄 마커 가사가 잘못 필터링되어 하단으로 내려가는 경우 이 설정을 조정하거나 비워 두세요. 쉼표로 구분된 마커: 마커가 포함된 줄에 콜론(:)이 뒤따르면 해당 줄은 크레딧 정보로 간주되어 하단에 표시됩니다. 비워두면 비활성화됩니다 + 첫 글자 대문자로 표시 + 각 가사 줄의 첫 글자를 대문자로 표시합니다 + 끝 문장 부호 제거 + 가사 줄 끝에 있는 마침표, 쉼표, 콜론 및 세미콜론을 제거합니다. 물음표, 느낌표, 말줄임표, 따옴표 및 괄호는 유지됩니다 텍스트 크기 가사 텍스트 크기 전역 타이밍 오프셋 diff --git a/patches/src/main/resources/addresources/values-ko-rKR/youtube/strings.xml b/patches/src/main/resources/addresources/values-ko-rKR/youtube/strings.xml index bb35d9f704..c13281cf38 100644 --- a/patches/src/main/resources/addresources/values-ko-rKR/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ko-rKR/youtube/strings.xml @@ -393,6 +393,9 @@ Second \"item\" text" 쇼핑 링크 숨기기 동영상 광고 숨기기 YouTube Premium 프로모션 숨기기 + + + 불러오는 중… 현재 채널: 화이트리스트에 추가 @@ -409,6 +412,9 @@ Second \"item\" text" 채널 검색 결과 이 채널에서 아무것도 찾을 수 없습니다 이 채널을 검색할 수 없습니다 + 동영상 + 게시물 + 이 채널에 일치하는 게시물이 없습니다 관련성순 최다 조회순 최저 조회순 @@ -494,6 +500,9 @@ Second \"item\" text" 동영상 오디오 모든 옵션 + Shorts 메뉴에서 다운로드 표시 + Shorts 플레이어에 외부 다운로더 플라이아웃 메뉴 항목을 추가합니다 + 다운로드 음성 번역 기기의 텍스트 음성 변환(TTS)을 사용하여 재생에 맞춰 동기화된 동영상 자막을 읽어줍니다 @@ -1123,9 +1132,6 @@ API 키는 절대로 다른 사람과 공유해서는 안 됩니다. API 키에 "이전 플레이어 레이아웃을 복원합니다 제한 사항: '이전 동영상 작업 버튼바 복원하기'를 비활성화하면, '동영상 더보기' 오버레이를 사용할 수 없을 수 있습니다" - "이 기능을 활성화하면 댓글 섹션이나 플레이어 아래에 표시되는 '추천 동영상'과 같은 일부 플레이어 레이아웃 기능이 제대로 작동하지 않을 수 있습니다 - -태블릿 레이아웃은 더 이상 YouTube에서 지원하지 않으며, 최신 YouTube 버전에서는 레이아웃이 부분적으로 또는 완전히 깨질 수 있습니다. 태블릿 레이아웃을 정상적으로 사용하려면 YouTube 20.21.37 버전을 패치하세요" 21.11.48 - 이전 자막 동작을 복원합니다 20.39.41 - 이전 \'재생목록에 저장\'을 복원합니다 @@ -1628,4 +1634,17 @@ AV1이 사용된 동영상 재생이 끊기거나 프레임이 손실될 수 있 시리즈를 찾을 수 없습니다 제목 없는 에피소드 사용할 수 없는 에피소드 + + Niconico 댓글 + 실시간 채팅 및 채팅 재생을 동영상 위에 흐르는 댓글로 표시합니다 + 동영상 플레이어에 오버레이 버튼을 표시합니다. 버튼을 탭하여 댓글을 표시하거나 숨길 수 있고, 길게 탭하면 빠른 설정이 실행됩니다 + 댓글 글꼴 크기 + 댓글의 텍스트 크기 (12-40 sp) + 댓글 불투명도 + 댓글 텍스트 및 배경의 불투명도 + 이동 지속 시간 + 댓글이 화면을 가로지르는 데 걸리는 초 수입니다. 값이 낮을수록 더 빠르게 이동합니다 + 댓글 최대 길이 + 이 값보다 글자 수가 많은 댓글을 숨깁니다. 각 이모지는 1글자로 계산됩니다 + 제한 없음 diff --git a/patches/src/main/resources/addresources/values-ky-rKG/youtube/strings.xml b/patches/src/main/resources/addresources/values-ky-rKG/youtube/strings.xml index ff84d4a44b..015546adb1 100644 --- a/patches/src/main/resources/addresources/values-ky-rKG/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ky-rKG/youtube/strings.xml @@ -62,6 +62,8 @@ Second \"item\" text" Видео жарнамаларын жашыруу + + @@ -253,4 +255,5 @@ Second \"item\" text" Өзүнөн өзү + diff --git a/patches/src/main/resources/addresources/values-lo-rLA/youtube/strings.xml b/patches/src/main/resources/addresources/values-lo-rLA/youtube/strings.xml index 9079108272..70ce03fc67 100644 --- a/patches/src/main/resources/addresources/values-lo-rLA/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-lo-rLA/youtube/strings.xml @@ -51,6 +51,8 @@ Second \"item\" text" + + @@ -224,4 +226,5 @@ Second \"item\" text" + diff --git a/patches/src/main/resources/addresources/values-lt-rLT/music/strings.xml b/patches/src/main/resources/addresources/values-lt-rLT/music/strings.xml index 3599940439..20430fe6f0 100644 --- a/patches/src/main/resources/addresources/values-lt-rLT/music/strings.xml +++ b/patches/src/main/resources/addresources/values-lt-rLT/music/strings.xml @@ -555,8 +555,29 @@ Reikalingi slapukai: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPI Rodo esamą eilutę atrakintame ekrane, „Android Auto“ ir Bluetooth. Reikia Android 11 ar naujesnės versijos Rodyti mini grotuvo dainų tekstus Rodo esamą eilutę mini grotuve, perkeliant takelį į subtitrą - Atlikėjo ir pavadinimo rodymo tvarka - Perjungia atlikėjo ir pavadinimo rodymo tvarką mini grotuve ir „MediaSession“. ĮJUNGUS – „Atlikėjas – Pavadinimas“, IŠJUNGUS – „Pavadinimas – Atlikėjas“. Dainų tekstų paieškai įtakos neturi + Dainos pavadinimo ir atlikėjo formatas + Takto formatas, rodomas kartu su dainos žodžiais Miniplayeryje ir MediaSession. {t} yra pavadinimas, {a} – atlikėjas, o {v} – versija (pvz., akustinė), jei žodžių šaltinis ją nurodo. Tekstas tarp dviejų dvitaškių yra rodomas tik tada, kai kiekviena jame esanti kintamoji turi reikšmę; paprastai dvitaškiai rašykite \\:. Kiekviena eilutė yra rodoma po eilutės, keičdamasi kartu su dainos žodžiais + Dainos žodžių eilutės formatas + Dainos žodžių eilutės formatas Miniplayeryje ir MediaSession. {lrc} yra eilutė, o {n} – kiek kartų iš eilgos ji yra dainuojama. Tekstas tarp dvitaškių yra rodomas tik tada, kai eilutė kartojasi. Dvitaškiai naudokite \\: + Formatas privalo содержаti {lrc} + Atlikėjo eilutės šablonas ir animacijos laikymas + Minimalus ekrano rodymo laikas (ms) + Maksimalus ekrano rodymo laikas (ms) + Tik daugiaeilinėms animacijoms. Kiekvieną kadrą rodyti bent minimalų laiką. Pasiekus maksimalų laiką, atlikėjo eilutė slenka toliau net ir jei dainos žodžiai išlieka tie patys. + Minimali reikšmė turi būti 0 arba didesnė. Maksimali reikšmė turi būti didesnė už 0 ir ne mažiau nei minimali. + Eilutės ilgio apribojimai + Dainos žodžių ir atlikėjo eilutės apribojimai bei viso pločio simbolių svoris + "Dainos žodžių eilutės riba +Ilgesni žodžiai bus padalinti į subalansuotas puses. 0 išjungia skaldymą." + "Atlikėjo eilutės riba +Suskirstykite užpildytą takelio formatą į besukančius kadrus. 0 išjungia skirstymą." + "Pilno pločio simbolio svoris +Taikoma abiems riboms. Nustatant 1,75, keturi pilno pločio simboliai counted kaip 7." + Įveskite sveikąjį skaičių riboms nuo 0 iki 10000 ir simbolio svorį, kuris yra didesnis nei 0 + Instrumentinė pertrauka + Rodoma miniplayeryje ir „MediaSession“ instrumentinės pertraukos metu. Kiekviena eilutė yra vienas animacijos kadras. {t} yra pavadinimas, {a} – atlikėjas, o {lrc} – paskutinė dainos teksto eilutė + Miniplayerio ir „MediaSession“ laiko poslinkis + Nustato, kada Miniplayeryje ir MediaSession rodoma kitka eilutė. Jei eilutės pasirodo pervėlai, pavyzdžiui, užrakinto ekrano, naudokite neigiamą reikšmę Filtrai Metaduomenų regex Reguliarioji išraiška taikoma pavadinimams, atlikėjams ir albumams prieš ieškant dainų tekstų ir vertinant, ar du pavadinimai sutampa. Palikite tuščią, kad išjungtumėte abi funkcijas. Numatytasis variantas pašalina įprastą papildomą tekstą, pvz., „Official Video“, „Remaster“, „【插曲】“ ir „- Topic“ @@ -564,6 +585,10 @@ Reikalingi slapukai: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPI Reguliarioji išraiška taikoma tik originaliam dainų tekstui (ne vertimams ar romanizacijoms). Palikite tuščią, kad išjungtumėte Informacinių eilučių žymekliai Jei dainų tekstai klaidingai nukeliami į apačią, pakoreguokite arba išvalykite šį lauką. Žymekliai, atskirti kableliais: eilutės, turinčios žymeklį, po kurio yra dvitaškis, rodomos kaip informacija apie autorius apačioje. Palikite tuščią, kad išjungtumėte + Rašyti pirmą raidę didžiuoju + Kiekvienos dainos žodžių eilutės pirmą raidę rašo didžiuoju + Pašalinti skyrybos ženklus pabaigoje + Pašalina taškus, kvertes, dvitaškius ir pusetaškius dainos žodžių eilučių pabaigoje. Klausmės ženklai, šaukeliai, daugtaškiai, citatos ir skliaustai išlieka Teksto dydis Dainų teksto dydis Bendras laiko poslinkis diff --git a/patches/src/main/resources/addresources/values-lt-rLT/youtube/strings.xml b/patches/src/main/resources/addresources/values-lt-rLT/youtube/strings.xml index 43ee71bf07..94b883d4a0 100644 --- a/patches/src/main/resources/addresources/values-lt-rLT/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-lt-rLT/youtube/strings.xml @@ -393,6 +393,9 @@ Pranešimai yra vieši ir peržiūrimi prieš pridedant kanalą į sąrašą. Pa Slėpti apsipirkimo nuorodas Slėpti vaizdo įrašų reklamas Slėpti „YouTube Premium“ reklamas + + + Įkeliama… Dabartinis kanalas: Pridėti į baltąjį sąrašą @@ -409,6 +412,9 @@ Pranešimai yra vieši ir peržiūrimi prieš pridedant kanalą į sąrašą. Pa Kanalo paieškos rezultatai Šiame kanale nieko nerasta Nepavyko atlikti paieškos šiame kanale + Vaizdo įrašai + Įrašai + Šiame kanale nėra sutinkančių įrašų Aktualumas Daugiausia peržiūrų Mažiausiai peržiūrų @@ -442,6 +448,7 @@ Pranešimai yra vieši ir peržiūrimi prieš pridedant kanalą į sąrašą. Pa Atsisiųsti nuotrauką Atsiunčiamas %s vaizdai… Atsiunčiamas vaizdas… + %1$s nuotraukos išsaugytos į %2$s Vaizdas išsaugotas į %s Nepavyko atsisiųsti vaizdo Šiame įraše vaizdo nerasta @@ -493,6 +500,9 @@ Kartą atvėrus vaizdo įrašą su mygtuku „Ask“, funkcija veikia su visais Vaizdo įrašai Garsas Visi nustatymai + Rodyti atsisiuntimą „Shorts“ meniu + Prideda išorinio atsisiuntimo įskiepio meniu punktą „Shorts“ grotuveje + Atsisiųsti Balso vertimas Skaito vaizdo įrašo subtitrus garsiai naudodamas įrenginio teksto į kalbą sintezatorių, sinchronizuotą su atkūrimu @@ -1120,9 +1130,6 @@ Automobilio maketas "Atkuria senąjį grotuvo maketą Apribojimas: jei parinktis „Atkurti senąją vaizdo įrašo veiksmų juostą“ išjungta, perdanga „Daugiau vaizdo įrašų“ gali būti nepasiekiama" - "Įjungus šią funkciją gali neveikti kai kurios grotuvo išdėstymo funkcijos, pavyzdžiui, komentarų skiltis arba po grotuvu rodomi „Kiti vaizdo įrašai“. - -„YouTube“ nebepalaiko planšetinio išdėstymo, todėl naujesnėse „YouTube“ versijose jis veikia dalinai arba visai neveikia. Norėdami gauti veikiančią planšetinio išdėstymo versiją, naudokite „YouTube“ 20.21.37 pataisą" 21.11.48 – Atkurti senąją subtitrų elgseną 20.39.41 – atkurti senąjį išsaugojimą į grojaraštį @@ -1625,4 +1632,17 @@ Automatinis įrašymas yra neprivalomas. Jis sustabdomas slaptojo režimo metu a Jokių serijų nerasta Be pavadinimo epizodas Nepasiekiamas epizodas + + Niconico komentarai + Rodyti gyvą pokalbį ir pokalbių atkūrimą kaip komentarus, kurie rieda per vaizdo įrašą + Vaizdo įrašo spusteleklio ekrane rodo mygtuką. Bakstelėkite, kad parodytumėte arba paslėptumėte komentarus, arba bakstelėkite ir laikykite greitiesiems nustatymams + Komentarų šrifto dydis + Komentarų teksto dydis (12–40 sp) + Komentarų skaidrumas + Komentarų teksto ir fono skaidrumas + Keliavimo trukmė + Kiek sekundžių komentarams reikia, kad perekryžtų ekraną. Mažesni reikšmės juda greičiau + Maksimalus komentarų ilgis + Paslepia komentarus, kuriuose yra daugiau simbolių nei nustatyta. Kiekvienas emoji skaičiuojamas kaip vienas simbolis + Be apribojimų diff --git a/patches/src/main/resources/addresources/values-lv-rLV/youtube/strings.xml b/patches/src/main/resources/addresources/values-lv-rLV/youtube/strings.xml index 2a8bdb44f5..567f102aa6 100644 --- a/patches/src/main/resources/addresources/values-lv-rLV/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-lv-rLV/youtube/strings.xml @@ -69,6 +69,8 @@ Second \"item\" text" Statistika + + Noņemt @@ -272,4 +274,5 @@ Second \"item\" text" Atcelt Atsaukt + diff --git a/patches/src/main/resources/addresources/values-mai-rIN/youtube/strings.xml b/patches/src/main/resources/addresources/values-mai-rIN/youtube/strings.xml index 88e33fa418..e2ed146263 100644 --- a/patches/src/main/resources/addresources/values-mai-rIN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-mai-rIN/youtube/strings.xml @@ -145,6 +145,8 @@ Second \"item\" text" शॉपिंग लिंक छिपाएं वीडियो विज्ञापन छिपाएं YouTube Premium प्रमोशन छिपाइ + + @@ -537,4 +539,5 @@ Second \"item\" text" + diff --git a/patches/src/main/resources/addresources/values-mk-rMK/youtube/strings.xml b/patches/src/main/resources/addresources/values-mk-rMK/youtube/strings.xml index ed9fe7fa62..38593d2eac 100644 --- a/patches/src/main/resources/addresources/values-mk-rMK/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-mk-rMK/youtube/strings.xml @@ -197,6 +197,8 @@ Second \"item\" text" Скриј линкови за купување Скриј реклами во видеа Скриј промоции за YouTube Premium + + Отстрани @@ -220,6 +222,7 @@ Second \"item\" text" Изберете ја надворешната апликација што ќе ги обработува вашите преземања Ја исклучува двојната допренска гестикулација со два прста што ги прескокнува поглавјата, која може случајно да се активира со еден прст + Преземи По нарачка @@ -806,4 +809,5 @@ Second \"item\" text" Откажи Врати + diff --git a/patches/src/main/resources/addresources/values-mn-rMN/youtube/strings.xml b/patches/src/main/resources/addresources/values-mn-rMN/youtube/strings.xml index 9079108272..70ce03fc67 100644 --- a/patches/src/main/resources/addresources/values-mn-rMN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-mn-rMN/youtube/strings.xml @@ -51,6 +51,8 @@ Second \"item\" text" + + @@ -224,4 +226,5 @@ Second \"item\" text" + diff --git a/patches/src/main/resources/addresources/values-mr-rIN/youtube/strings.xml b/patches/src/main/resources/addresources/values-mr-rIN/youtube/strings.xml index 4473d78ced..47f65cacfd 100644 --- a/patches/src/main/resources/addresources/values-mr-rIN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-mr-rIN/youtube/strings.xml @@ -298,6 +298,8 @@ Second \"item\" text" शॉपिंग लिंक्स लपवा व्हिडिओ जाहिराती लपवा YouTube Premium जाहिरात वृत्तपत्रे लपवा + + @@ -1047,4 +1049,5 @@ OpenRouter.ai उघडण्यासाठी येथे टॅप करा व्हिडिओ ॲक्शन बार पूर्वीसारखे पुनर्संचयित करा + diff --git a/patches/src/main/resources/addresources/values-ms-rMY/youtube/strings.xml b/patches/src/main/resources/addresources/values-ms-rMY/youtube/strings.xml index 47f950bf35..cae016860a 100644 --- a/patches/src/main/resources/addresources/values-ms-rMY/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ms-rMY/youtube/strings.xml @@ -101,6 +101,8 @@ Second \"item\" text" Sorokkan iklan video + + Saluran semasa: Tambahkan ke senarai putih @@ -380,4 +382,5 @@ Automotive layout Tersuai Teruskan + diff --git a/patches/src/main/resources/addresources/values-my-rMM/youtube/strings.xml b/patches/src/main/resources/addresources/values-my-rMM/youtube/strings.xml index 9079108272..70ce03fc67 100644 --- a/patches/src/main/resources/addresources/values-my-rMM/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-my-rMM/youtube/strings.xml @@ -51,6 +51,8 @@ Second \"item\" text" + + @@ -224,4 +226,5 @@ Second \"item\" text" + diff --git a/patches/src/main/resources/addresources/values-nb-rNO/music/strings.xml b/patches/src/main/resources/addresources/values-nb-rNO/music/strings.xml index 87f8ecdc22..d5f7bf0ac1 100644 --- a/patches/src/main/resources/addresources/values-nb-rNO/music/strings.xml +++ b/patches/src/main/resources/addresources/values-nb-rNO/music/strings.xml @@ -553,8 +553,29 @@ Nødvendige informasjonskapsler: SID, HSID, SSID, SAPISID, __Secure-1PAPISID, __ Viser gjeldende linje på låsskjerm, Android Auto og Bluetooth. Krever Android 11 eller nyere Vis minispillertekster Viser gjeldende linje i miniavspilleren, med spor flyttet til underteksten - Visningsrekkefølge for Artist og Tittel - Bytt visningsrekkefølgen til Artist og Tittel i Minispilleren og MediaSession. PÅ «Artist – Tittel» og AV «Tittel – Artist». Påvirker ikke tekstsøk + Format for sangtittel/artist + Formatet på sporet som vises sammen med sangteksten i minispilleren og MediaSession. {t} er tittelen, {a} er artisten og {v} er versjonen, slik som akustisk, når kilden til sangteksten oppgir dette. Tekst mellom to kolon vises bare når hver variabel inni har en verdi; skriv \\: for et vanlig kolon. Hver linje vises etter tur, og skifter når sangteksten endres + Format for sangtekstlinje + Formatet på sangtekstlinjen i minispilleren og MediaSession. {lrc} er linjen og {n} er hvor mange ganger på rad den synges. Tekst mellom kolon vises bare når linjen gjentas. Bruk \\: for et kolon + Formatet må inneholde {lrc} + Mal for artistlinje og animasjonstidspunkt + Minimum skjermtid (ms) + Maksimum skjermtid (ms) + Kun for flertallslinjeanimasjoner. Behold hver ramme i minst minimumstiden. Ved maksimum, avanser artistlinjen selv når sangteksten forblir den samme. + Minimum må være 0 eller høyere. Maksimum må være større enn 0 og minst like mye som minimum. + Grenser for linjelengde + Grenser for sangtekst- og artistlinjer, og vekt for tegn med full bredde + "Grense for sangtekstlinje +Del lengre tekster inn i balanserte halvdeler. 0 deaktiverer oppdeling." + "Linjegrense for artist +Del det utfylte sporformatet inn i sykliske rammer. 0 deaktiverer oppdeling." + "Vekt for tegn med full bredde +Gjelder for begge grenser. Ved 1,75 teller fire tegn med full bredde som 7." + Angi heltallsgrenser fra 0 til 10000 og en tegnvekt større enn 0 + Instrumental pause + Vises i minispilleren og MediaSession under en instrumental pause. Hver linje er én animasjonsramme. {t} er tittelen, {a} er artisten og {lrc} er den siste tekstlinjen + Tidsforskyvning for minispiller og MediaSession + Forskyver når neste linje vises i minispilleren og MediaSession. Bruk en negativ verdi hvis linjene vises for sent, for eksempel på låsskjermen Filtre Metadata […] \"Lyrics filter regex\" Regulært uttrykk som brukes på titler, artister og album før sangtekster slås opp, og ved vurdering av om to titler samsvarer. La det stå tomt for å deaktivere begge. Standarden fjerner vanlig støy som «Official Video», «Remaster», «【插曲】» og «- Topic». @@ -562,6 +583,10 @@ Nødvendige informasjonskapsler: SID, HSID, SSID, SAPISID, __Secure-1PAPISID, __ Regulært uttrykk brukes bare på originalteksten (ikke oversettelser eller romanisering). La feltet stå tomt for å deaktivere Markører for info-linjer Hvis tekster blir feilaktig filtrert til bunnen, justerer eller tømmer du dette. Kommaseparerte markører: linjer som inneholder en markør etterfulgt av kolon, vises som kredittinformasjon nederst. La feltet stå tomt for å deaktivere + Stor forbokstav + Gjør den første bokstaven i hver sangtekstlinje stor + Fjern tegnsetting på slutten + Fjerner punktum, komma, kolon og semikolon på slutten av sangtekstlinjer. Spørsmålstegn, utropstegn, ellipse, sitattegn og parenteser beholdes Tekststørrelse Størrelse på sangteksten Global tidsforskyvning diff --git a/patches/src/main/resources/addresources/values-nb-rNO/youtube/strings.xml b/patches/src/main/resources/addresources/values-nb-rNO/youtube/strings.xml index 16e2305ed1..b91b59f8b5 100644 --- a/patches/src/main/resources/addresources/values-nb-rNO/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-nb-rNO/youtube/strings.xml @@ -393,6 +393,9 @@ Rapporter er offentlige og blir gjennomgått før en kanal legges til en liste. Skjul shoppinglenker Skjul videoannonser Skjul YouTube Premium-kampanjer + + + Laster… Aktuell kanal: Legg til i hvitelisten @@ -409,6 +412,9 @@ Rapporter er offentlige og blir gjennomgått før en kanal legges til en liste. Søkeresultater for kanalen Fant ingenting på denne kanalen Kunne ikke søke i denne kanalen + Videoer + Innlegg + Ingen innlegg på denne kanalen samsvarer Relevans Mest sett Minst sett @@ -494,6 +500,9 @@ Etter at en video med Ask-knappen er åpnet én gang, fungerer funksjonen med al Video Lyd Alle alternativer + Vis nedlastinger i Shorts-menyen + Legger til et flytmenyalternativ for ekstern nedlasting i Shorts-spilleren + Last ned Taleoversettelse Les opp tekstingen til videoen ved hjelp av tekst-til-tale på enheten, synkronisert med avspillingen @@ -1121,9 +1130,6 @@ Bil-layout "Gjenoppretter det gamle spillerlayoutet Begrensning: Hvis 'Gjenopprett gammel videoaksjonslinje' er slått av, er det mulig at 'Flere videoer'-overlegg ikke er tilgjengelig" - "Aktivering av dette kan ødelegge enkelte layoutfunksjoner i spilleren, som kommentarfeltet eller \"Flere videoer\" som vises under spilleren. - -Nettbrettlayout støttes ikke lenger av YouTube, og er delvis eller helt ødelagt i nyere YouTube-versjoner. For en fungerende nettbrettlayout, patch YouTube 20.21.37" 21.11.48 - Gjenopprett gammel tekstringsatferd 20.39.41 – Gjenopprett gammel lagring til spilleliste @@ -1626,4 +1632,17 @@ Automatisk opptak er valgfritt. Det stoppes i inkognito-modus eller når kontoen Ingen serier funnet Episod uten tittel Utilgjengelig episode + + Niconico-kommentarer + Vis live-chat og chat-avspilling som kommentarer som ruller over videoen + Viser en overlay-knapp i videospilleren. Trykk for å vise eller skjule kommentarer, trykk og hold for hurtiginnstillinger + Skriftstørrelse for kommentarer + Tekststørrelse på kommentarene (12-40 sp) + Kommentarenes gjennomsiktighet + Gjennomsiktighet for kommentartekst og bakgrunner + Reisetid + Hvor mange sekunder kommentarene bruker på å krysse skjermen. Lavere verdier beveger seg raskere + Maksimal kommentarlengde + Skjuler kommentarer som har flere tegn enn dette. Hvert emoji teller som ett tegn + Ingen grense diff --git a/patches/src/main/resources/addresources/values-ne-rIN/youtube/strings.xml b/patches/src/main/resources/addresources/values-ne-rIN/youtube/strings.xml index 9079108272..70ce03fc67 100644 --- a/patches/src/main/resources/addresources/values-ne-rIN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ne-rIN/youtube/strings.xml @@ -51,6 +51,8 @@ Second \"item\" text" + + @@ -224,4 +226,5 @@ Second \"item\" text" + diff --git a/patches/src/main/resources/addresources/values-nl-rNL/music/strings.xml b/patches/src/main/resources/addresources/values-nl-rNL/music/strings.xml index b826f2e0c7..ee833fbc5c 100644 --- a/patches/src/main/resources/addresources/values-nl-rNL/music/strings.xml +++ b/patches/src/main/resources/addresources/values-nl-rNL/music/strings.xml @@ -545,8 +545,29 @@ Vereiste cookies: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPISID Toont de huidige regel op het vergrendelscherm, Android Auto en Bluetooth. Vereist Android 11 of nieuwer Toon miniplayer-songteksten Toont de huidige regel in de minispeler, waarbij het nummer naar de ondertiteling is verplaatst - Artiest- en titelweergave […] AAN \"Artiest - Titel\" en UIT \"Titel - Artiest\". Heeft geen invloed op het zoeken van songteksten. - Wissel de weergavevolgorde van artiest en titel in de Miniplayer en MediaSession. AAN \"Artiest - Titel\" en UIT \"Titel - Artiest\" Heeft geen invloed op het zoeken naar songteksten. + Formaat songtitel/artiest + Formaat van het nummer dat samen met de songtekst wordt getoond in de miniplayer en MediaSession. {t} is de titel, {a} is de artiest en {v} is de versie, zoals Akoestisch, wanneer de bron van de songtekst deze biedt. Tekst tussen twee dubbele punten wordt alleen getoond wanneer elke variabele daarin een waarde heeft; gebruik \\: voor een gewone dubbele punt. Elke regel wordt op beurt getoond en verandert zodra de songtekst wijzigt + Formaat songtekstregel + Formaat van de regel songtekst in de miniplayer en MediaSession. {lrc} is de regel en {n} is hoe vaak deze achter elkaar wordt gezongen. Tekst tussen dubbele punten wordt alleen getoond wanneer de regel zich herhaalt. Gebruik \\: voor een dubbele punt + Het formaat moet {lrc} bevatten + Sjabloon voor artiestenregel en timing van animatie + Minimale schermtijd (ms) + Maximale schermtijd (ms) + Alleen voor animaties met meerdere regels. Houd elk frame minstens de minimale tijd vast. Bij de maximale tijd wordt de artiestenregel doorgeschoven, zelfs als de songtekst hetzelfde blijft. + Minimum moet 0 of hoger zijn. Maximum moet groter zijn dan 0 en minstens gelijk aan het minimum. + Limieten voor regellengte + Limieten voor tekst- en artiestenregels, en gewicht van tekens met volledige breedte + "Limiet voor tekstregel +Splits langere teksten in gebalanceerde helften. 0 schakelt splitsen uit." + "Regellimiet artiest +Splits het ingevulde trackformaat in cyclische frames. 0 schakelt splitsen uit." + "Gewicht van volledige breedte tekens +Van toepassing op beide limieten. Bij 1,75 tellen vier tekens van volledige breedte als 7." + Voer gehele getal limieten in van 0 tot 10000 en een tekengewicht groter dan 0 + Instrumentaal intermezzo + Getoond in de miniplayer en MediaSession tijdens een instrumentaal intermezzo. Elke regel is één animatieframe. {t} is de titel, {a} is de artiest en {lrc} is de laatste tekstregel + Timing-offset miniplayer en MediaSession + Verschuift wanneer de volgende regel wordt getoond in de Miniplayer en MediaSession. Gebruik een negatieve waarde als regels te laat verschijnen, bijvoorbeeld op het vergrendelscherm Filters Me […] :\"Songtekstfilter-regex\" Reguliere expressie die wordt toegepast op titels, artiesten en albums voordat songteksten worden opgezocht en bij het beoordelen of twee titels overeenkomen. Laat leeg om beide uit te schakelen. De standaard verwijdert veelvoorkomende rommel zoals \"Official Video\", \"Remaster\", \"【插曲】\" en \"- Topic\". @@ -554,6 +575,10 @@ Vereiste cookies: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPISID Reguliere expressie wordt alleen toegepast op de originele songtekst (niet op vertalingen of romanisaties). Leeg laten om uit te schakelen. Info-regelmarkeringen Als songteksten onterecht naar de onderkant worden gefilterd, pas dit dan aan of wis dit. […] Leeg laten om uit te schakelen. + Eerste letter hoofdletter maken + Maakt de eerste letter van elke songtekstregel een hoofdletter + Interpunctie aan het einde verwijderen + Verwijders punten, komma\'s, dubbele punten en puntkomma\'s aan het einde van songtekstregels. Vraagtekens, uitroeptekens, beletseltekens, aanhalingstekens en haakjes blijven behouden Tekstgrootte Grootte van de songtekst Globale timing-offset diff --git a/patches/src/main/resources/addresources/values-nl-rNL/youtube/strings.xml b/patches/src/main/resources/addresources/values-nl-rNL/youtube/strings.xml index 54a3a466c2..2c647e51cf 100644 --- a/patches/src/main/resources/addresources/values-nl-rNL/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-nl-rNL/youtube/strings.xml @@ -393,6 +393,9 @@ Rapporten zijn openbaar en worden beoordeeld voordat een kanaal aan een lijst wo Winkellinks verbergen Video-advertenties verbergen YouTube Premium promoties verbergen + + + Bezig met laden… Huidig kanaal: Toevoegen aan de witte lijst @@ -409,6 +412,9 @@ Rapporten zijn openbaar en worden beoordeeld voordat een kanaal aan een lijst wo Zoekresultaten voor kanalen Niets gevonden op dit kanaal Kan dit kanaal niet doorzoeken + Video\'s + Berichten + Geen berichten op dit kanaal komen overeen Relevantie Meest bekeken Minst bekeken @@ -442,6 +448,7 @@ Rapporten zijn openbaar en worden beoordeeld voordat een kanaal aan een lijst wo Afbeelding downloaden {one,1 afbeelding downloaden…|other,%s afbeeldingen downloaden…} Afbeelding downloaden… + %1$s afbeeldingen opgeslagen in %2$s Afbeelding opgeslagen in %s De afbeelding kon niet worden gedownload Geen afbeelding gevonden in dit bericht @@ -493,6 +500,9 @@ Nadat een video met de Ask-knop eenmaal is geopend, werkt de functie voor alle v Video Audio Alle opties + \"Shorts\" weergeven in menu Downloaden + \"Shorts\"-speler krijgt item voor extern downloadmenu + Download Spraakvertaling Lees ondertitels hardop voor met behulp van tekst-naar-spraak op het apparaat, gesynchroniseerd met het afspelen @@ -1118,9 +1128,6 @@ Automotive-indeling "Herstelt de oude player layout Beperking: Als 'Herstel oude video acties balk' is uitgeschakeld, is de \"Meer video's\" overlay mogelijk niet beschikbaar" - "Het inschakelen hiervan kan sommige lay-outfuncties van de speler verbreken, zoals de commentaarsectie of 'Meer video's' onder de speler. - -De tabletlay-out wordt niet langer ondersteund door YouTube en is gedeeltelijk of volledig defect in recente YouTube-versies. Voor een werkende tabletlay-out, patch YouTube 20.21.37" 21.11.48 - Herstel het oude gedrag van ondertitels 20.39.41 - Herstel oude Shorts-speler pictogrammen @@ -1623,4 +1630,17 @@ Automatische registratie is optioneel. Deze stopt in incognito-modus of bij een Geen series gevonden Naamloze aflevering Niet beschikbare aflevering + + Niconico-reacties + Toon livechat en chat-replays als reacties die over de video scrollen + Toont een overlay-knop in de videospeler. Tik om de reacties te tonen of te verbergen, houd ingedrukt voor snelle instellingen + Lettertypegrootte reacties + Tekstgrootte van de reacties (12-40 sp) + Opaciteit reacties + Opaciteit van de reactietekst en achtergronden + Reistijd + Hoeveel seconden reacties erover doen om het scherm te kruisen. Lagere waarden bewegen sneller + Maximale lengte van opmerking + Verbergt opmerkingen met meer tekens dan dit. Elke emoji telt als één teken + Geen limiet diff --git a/patches/src/main/resources/addresources/values-or-rIN/youtube/strings.xml b/patches/src/main/resources/addresources/values-or-rIN/youtube/strings.xml index 9079108272..70ce03fc67 100644 --- a/patches/src/main/resources/addresources/values-or-rIN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-or-rIN/youtube/strings.xml @@ -51,6 +51,8 @@ Second \"item\" text" + + @@ -224,4 +226,5 @@ Second \"item\" text" + diff --git a/patches/src/main/resources/addresources/values-pa-rIN/youtube/strings.xml b/patches/src/main/resources/addresources/values-pa-rIN/youtube/strings.xml index 9079108272..70ce03fc67 100644 --- a/patches/src/main/resources/addresources/values-pa-rIN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-pa-rIN/youtube/strings.xml @@ -51,6 +51,8 @@ Second \"item\" text" + + @@ -224,4 +226,5 @@ Second \"item\" text" + diff --git a/patches/src/main/resources/addresources/values-pl-rPL/music/strings.xml b/patches/src/main/resources/addresources/values-pl-rPL/music/strings.xml index 8d5b953846..6702478320 100644 --- a/patches/src/main/resources/addresources/values-pl-rPL/music/strings.xml +++ b/patches/src/main/resources/addresources/values-pl-rPL/music/strings.xml @@ -555,8 +555,29 @@ Wymagane ciasteczka: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPI Pokazuje bieżący wers na ekranie blokady, w Android Auto i przez Bluetooth. Wymaga systemu Android 11 lub nowszego Pokaż teksty w miniodtwarzaczu Pokazuje bieżący wers w miniodtwarzaczu, przenosząc nazwę utworu do podtytułu - Kolejność wyświetlania wykonawcy i tytułu - Zmienia kolejność wyświetlania wykonawcy i tytułu w miniodtwarzaczu i MediaSession. Włączone: \"Wykonawca - Tytuł\", wyłączone: \"Tytuł - Wykonawca\". Nie wpływa to na wyszukiwanie tekstów + Format tytułu i wykonawcy utworu + Format informacji o utworze wyświetlanych wraz z tekstem w miniodtwarzaczu i MediaSession. {t} to tytuł, {a} to wykonawca, a {v} to wersja (np. akustyczna), jeśli źródło tekstu ją udostępnia. Tekst między dwoma dwukropkami jest pokazywany tylko wtedy, gdy każda zawarta w nim zmienna ma wartość. Wpisz \:, aby wstawić zwykły dwukropek. Każdy wiersz jest wyświetlany po kolei i zmienia się wraz ze zmianą wersu tekstu + Format wersu tekstu + Format wersu tekstu w miniodtwarzaczu i MediaSession. {lrc} to wers tekstu, a {n} określa, ile razy z rzędu jest śpiewany. Tekst między dwukropkami jest pokazywany tylko wtedy, gdy wers się powtarza. Użyj \:, aby wstawić dwukropek + Format musi zawierać zmienną {lrc} + Szablon wiersza wykonawcy i czas trwania animacji + Minimalny czas wyświetlania (ms) + Maksymalny czas wyświetlania (ms) + Tylko dla animacji wielowierszowych. Wyświetlaj każdą klatkę co najmniej przez czas minimalny. Po osiągnięciu czasu maksymalnego przejdź do kolejnego wiersza wykonawcy, nawet jeśli wers tekstu się nie zmienił + Wartość minimalna musi wynosić co najmniej 0. Wartość maksymalna musi być większa od 0 i nie mniejsza niż wartość minimalna + Limity długości wierszy + Limity wiersza tekstu i wykonawcy oraz waga znaków pełnej szerokości + "Limit wiersza tekstu +Dzieli dłuższe wersy na równe połowy. Wartość 0 wyłącza dzielenie" + "Limit wiersza wykonawcy +Dzieli wypełniony format utworu na zmieniające się cyklicznie klatki. Wartość 0 wyłącza dzielenie" + "Waga znaków pełnej szerokości +Dotyczy obu limitów. Przy wartości 1.75 cztery znaki pełnej szerokości liczą się jako 7" + Wprowadź limity jako liczby całkowite od 0 do 10000 oraz wagę znaków większą od 0 + Przerwa instrumentalna + Wyświetlane w miniodtwarzaczu i MediaSession podczas przerwy instrumentalnej. Każdy wiersz to jedna klatka animacji. {t} to tytuł, {a} to wykonawca, a {lrc} to ostatni wers tekstu + Przesunięcie czasu w miniodtwarzaczu i MediaSession + Przesuwa moment wyświetlenia kolejnego wersu w miniodtwarzaczu i MediaSession. Użyj wartości ujemnej, jeśli wersy pojawiają się za późno, np. na ekranie blokady Filtry Wyrażenie regularne dla metadanych Wyrażenie regularne stosowane do tytułów, wykonawców i albumów przed wyszukaniem tekstu oraz podczas oceniania zgodności dwóch tytułów. Pozostaw puste, aby wyłączyć obie funkcje. Domyślne ustawienie usuwa powszechne zbędne elementy, takie jak \"Official Video\", \"Remaster\", \"【插曲】\" i \"- Topic\" @@ -564,6 +585,10 @@ Wymagane ciasteczka: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPI Wyrażenie regularne stosowane wyłącznie do oryginalnego tekstu utworu (z pominięciem tłumaczeń i romanizacji). Pozostaw puste, aby wyłączyć Znaczniki wierszy informacyjnych Jeśli standardowy tekst utworu jest błędnie przenoszony na sam dół, dostosuj to pole lub je wyczyść. Znaczniki oddzielane przecinkami: wiersze zawierające znacznik, po którym następuje dwukropek, są traktowane jako informacje o twórcach i wyświetlane na dole. Pozostaw puste, aby wyłączyć + Wielka pierwsza litera + Zmienia pierwszą literę każdego wersu tekstu na wielką + Usuń końcową interpunkcję + Usuwa kropki, przecinki, dwukropki i średniki na końcu wersów tekstu. Znaki zapytania, wykrzykniki, wielokropki, cudzysłowy oraz nawiasy są zachowywane Rozmiar tekstu Rozmiar tekstu utworu Globalne przesunięcie czasowe diff --git a/patches/src/main/resources/addresources/values-pl-rPL/youtube/strings.xml b/patches/src/main/resources/addresources/values-pl-rPL/youtube/strings.xml index 39171ef1e9..a235d3a150 100644 --- a/patches/src/main/resources/addresources/values-pl-rPL/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-pl-rPL/youtube/strings.xml @@ -393,6 +393,9 @@ Zgłoszenia są publiczne i podlegają weryfikacji przed dodaniem kanału do lis Ukryj linki do sklepów Ukryj reklamy w filmach Ukryj promocje YouTube Premium + + + Wczytywanie… Obecny kanał: Dodaj do białej listy @@ -409,6 +412,9 @@ Zgłoszenia są publiczne i podlegają weryfikacji przed dodaniem kanału do lis Wyniki wyszukiwania kanałów Nic nie znaleziono na tym kanale Nie udało się przeszukać tego kanału + Filmy + Posty + Brak pasujących postów na tym kanale Trafność Najpopularniejsze Najrzadziej oglądane @@ -494,6 +500,9 @@ Zgłoszenia są publiczne i podlegają weryfikacji przed dodaniem kanału do lis Film Dźwięk Wszystkie opcje + Pokaż pobieranie w menu Short + Dodaje zewnętrzny element menu flyout pobierania do odtwarzacza Short + Pobierz Tłumaczenie głosowe Czyta napisy na głos przy użyciu wbudowanego syntezatora mowy, synchronizując je z odtwarzaniem obrazu @@ -1120,9 +1129,6 @@ Układ samochodowy "Przywraca stary układ odtwarzacza Ograniczenie: Jeśli opcja \"Przywróć stary pasek akcji filmu\" jest wyłączona, nakładka \"Więcej filmów\" może być niedostępna" - "Włączenie tej opcji może zepsuć niektóre elementy układu odtwarzacza, takie jak sekcja komentarzy czy lista \"Więcej filmów\" wyświetlana pod odtwarzaczem. - -​Układ tabletu nie jest już obsługiwany przez YouTube i w nowszych wersjach aplikacji nie działa w pełni lub wcale. Aby korzystać z działającego układu tabletu, zmodyfikuj YouTube w wersji 20.21.37" 21.11.48 - Przywraca stare zachowanie napisów 20.39.41 - Przywraca stare menu dodawania do playlisty @@ -1625,4 +1631,17 @@ Ograniczenie: Przywraca stary wygląd nakładki \"Więcej filmów\"" Nie znaleziono serii Odcinek bez tytułu Odcinek niedostępny + + Komentarze Niconico + Wyświetlaj czat na żywo oraz powtórkę czatu jako komentarze przesuwające się przez wideo + Wyświetla przycisk nakładki w odtwarzaczu wideo. Dotknij, aby pokazać lub ukryć komentarze, przytrzymaj, aby otworzyć szybkie ustawienia + Rozmiar czcionki komentarzy + Rozmiar tekstu komentarzy (12-40 sp) + Przezroczystość komentarzy + Przezroczystość tekstu i tła komentarzy + Czas trwania podróży + Ile sekund komentarze potrzebują, aby przemieścić się przez ekran. Niższe wartości oznaczają szybszy ruch + Maksymalna długość komentarza + Ukrywa komentarze, które mają więcej znaków niż ta wartość. Każdy emoji liczy się jako jeden znak + Brak limitu diff --git a/patches/src/main/resources/addresources/values-pt-rBR/music/strings.xml b/patches/src/main/resources/addresources/values-pt-rBR/music/strings.xml index fcf8ff4012..f23a9ca00e 100644 --- a/patches/src/main/resources/addresources/values-pt-rBR/music/strings.xml +++ b/patches/src/main/resources/addresources/values-pt-rBR/music/strings.xml @@ -545,8 +545,29 @@ Cookies necessários: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAP Mostra a linha atual na tela de bloqueio, Android Auto e Bluetooth. Precisa do Android 11 ou mais recente Mostrar letras no miniplayer Mostra a linha atual no miniplayer, com a faixa movida para a legenda - Exibição de artista e título na MediaSession. LIGADO \"Artista - Título\" e DESLIGADO \"Título - Artista\". Não afeta a busca por letras. - Alternar a ordem de exibição de Artista e Título no Miniplayer e no MediaSession. LIGADO \"Artista - Título\" e DESLIGADO \"Título - Artista\". Não afeta a busca de letras + Formato do título da música/artista + Formato da faixa exibida com as letras no miniplayer e na MediaSession. {t} é o título, {a} é o artista e {v} é a versão, como Acústico, quando a fonte da letra fornece uma. O texto entre dois dois-pontos só é exibido quando cada variável dentro dele possui um valor; escreva \\: para um dois-pontos simples. Cada linha é exibida por vez, avançando sempre que a letra muda + Formato da linha da letra + Formato da linha da letra no miniplayer e na MediaSession. {lrc} é a linha e {n} é quantas vezes seguidas ela é cantada. O texto entre dois-pontos só é exibido quando a linha se repete. Use \\: para um dois-pontos + O formato deve conter {lrc} + Modelo de linha do artista e tempo de animação + Tempo mínimo de tela (ms) + Tempo máximo de tela (ms) + Apenas para animações de várias linhas. Mantenha cada quadro por pelo menos o tempo mínimo. No máximo, avance a linha do artista mesmo quando a letra permanecer a mesma. + O mínimo deve ser 0 ou maior. O máximo deve ser maior que 0 e pelo menos o valor do mínimo. + Limites de comprimento de linha + Limites de linha da letra e do artista, e peso de caracteres de largura total + "Limite de linha da letra +Divida letras mais longas em metades equilibradas. 0 desativa a divisão." + "Limite de linhas do artista +Divide o formato de faixa preenchido em quadros cíclicos. 0 desativa a divisão." + "Peso de caracteres de largura total +Aplica-se a ambos os limites. Em 1,75, quatro caracteres de largura total contam como 7." + Insira limites de números inteiros de 0 a 10000 e um peso de caractere maior que 0 + Intervalo instrumental + Exibido no Miniplayer e na MediaSession durante um intervalo instrumental. Cada linha é um quadro de animação. {t} é o título, {a} é o artista e {lrc} é a última linha da letra + Deslocamento de tempo do Miniplayer e da MediaSession + Desloca quando a próxima linha é exibida no miniplayer e na MediaSession. Use um valor negativo se as linhas aparecerem com atraso, por exemplo, na tela de bloqueio Filtros Metadados: \"regex de filtro de letras\" Expressão regular aplicada a títulos, artistas e álbuns antes de procurar letras e ao avaliar se dois títulos correspondem. Deixe vazio para desativar ambos. O padrão remove ruído comum, como \"Official Video\", \"Remaster\", \"【插曲】\" e \"- Topic\". @@ -554,6 +575,10 @@ Cookies necessários: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAP Expressão regular aplicada apenas ao texto original da letra (não a traduções nem a romanizações). Deixe em branco para desativar. Marcadores de linha de informação Se as letras forem filtradas incorretamente para a parte inferior, ajuste ou limpe este campo. Marcadores separados por vírgula: linhas que contêm um marcador seguido de dois pontos são mostradas como informações de crédito na parte inferior. Deixe vazio para desativar + Capitalizar primeira letra + Capitaliza a primeira letra de cada linha da letra + Remover pontuação final + Remove pontos finais, vírgulas, dois-pontos e pontos e vírgulas ao final das linhas da letra. Pontos de interrogação, pontos de exclamação, reticências, aspas e colchetes são mantidos Tamanho do texto Tamanho do texto da letra Deslocamento global de tempo diff --git a/patches/src/main/resources/addresources/values-pt-rBR/youtube/strings.xml b/patches/src/main/resources/addresources/values-pt-rBR/youtube/strings.xml index 55c2a1d280..e4a447d9e9 100644 --- a/patches/src/main/resources/addresources/values-pt-rBR/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-pt-rBR/youtube/strings.xml @@ -393,6 +393,9 @@ As denúncias são públicas e passam por revisão antes de um canal ser adicion Ocultar links do Shopping Ocultar anúncios em vídeos Ocultar promoções do YouTube Premium + + + Carregando… Canal atual: Adicionar à lista branca @@ -409,6 +412,9 @@ As denúncias são públicas e passam por revisão antes de um canal ser adicion Resultados da pesquisa de canais Nada encontrado neste canal Não foi possível pesquisar neste canal + Vídeos + Postagens + Nenhum post neste canal corresponde Relevância Mais vistos Menos vistos @@ -494,6 +500,9 @@ Depois que um vídeo com o botão Ask for aberto uma vez, o recurso funciona com Vídeo Áudio Todas as opções + Mostrar download no menu Shorts + Adiciona um item de menu de flyout de baixador externo ao player Shorts + Baixar Tradução por voz Leia as legendas do vídeo em voz alta usando a conversão de texto em fala no dispositivo, sincronizada com a reprodução @@ -1121,9 +1130,6 @@ Layout automotivo "Restaura o layout antigo do player de vídeo Limitação: Se 'Restaurar barra de ações de vídeo antiga' estiver desativado, a sobreposição 'Mais vídeos' pode não estar disponível" - "Ativar isso pode quebrar alguns recursos de layout do player, como a seção de comentários ou \"Mais vídeos\" exibidos abaixo do player. - -O layout de tablet não é mais suportado pelo YouTube e está parcial ou totalmente quebrado em versões recentes do YouTube. Para um layout de tablet funcional, aplique o patch no YouTube 20.21.37" 21.11.48 - Restaurar comportamento antigo de legendas 20.39.41 - Restaurar velho save para playlist @@ -1626,4 +1632,17 @@ A gravação automática é opcional. Ela é interrompida no modo anônimo ou qu Nenhuma série encontrada Episódio sem título Episódio indisponível + + Comentários do Niconico + Exibe o chat ao vivo e a reprodução do chat como comentários que deslizam pelo vídeo + Exibe um botão de sobreposição no reprodutor de vídeo. Toque para exibir ou ocultar os comentários, toque e segure para as configurações rápidas + Tamanho da fonte do comentário + Tamanho do texto dos comentários (12-40 sp) + Opacidade do comentário + Opacidade do texto e dos fundos dos comentários + Duração do deslocamento + Quantos segundos os comentários levam para atravessar a tela. Valores menores movem mais rápido + Comprimento máximo de comentário + Oculta comentários com mais caracteres do que este. Cada emoji conta como um caractere + Sem limite diff --git a/patches/src/main/resources/addresources/values-pt-rPT/music/strings.xml b/patches/src/main/resources/addresources/values-pt-rPT/music/strings.xml index 9d55080bd3..278f944a22 100644 --- a/patches/src/main/resources/addresources/values-pt-rPT/music/strings.xml +++ b/patches/src/main/resources/addresources/values-pt-rPT/music/strings.xml @@ -555,8 +555,29 @@ Cookies necessários: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAP Mostra a linha atual na tela de bloqueio, Android Auto e Bluetooth. Precisa de Android 11 ou mais recente Mostrar letras do miniplayer Mostra a linha atual no miniplayer, com a faixa movida para a legenda - Ordem de exibição do artista e do título - Alternar a ordem de exibição de Artista e Título no Miniplayer e na MediaSession. LIGADO \"Artista - Título\" e DESLIGADO \"Título - Artista\". Não afeta a pesquisa de letras + Formato do título da música/artista + Formato da faixa exibida com a letra no miniplayer e na MediaSession. {t} é o título, {a} é o artista e {v} é a versão, como Acústico, quando a fonte da letra fornece uma. O texto entre dois dois-pontos é exibido apenas quando cada variável dentro dele possui um valor; escreva \\: para um dois-pontos simples. Cada linha é exibida por vez, avançando sempre que a letra muda + Formato da linha da letra + Formato da linha da letra no miniplayer e MediaSession. {lrc} é a linha e {n} é quantas vezes seguidas ela é cantada. O texto entre dois pontos só é exibido quando a linha se repete. Use \\: para dois pontos + O formato deve conter {lrc} + Modelo de linha do artista e temporização da animação + Tempo mínimo de tela (ms) + Tempo máximo de tela (ms) + Apenas para animações de múltiplas linhas. Mantenha cada quadro por pelo menos o tempo mínimo. No máximo, avance a linha do artista mesmo quando a letra permanecer a mesma. + O mínimo deve ser 0 ou superior. O máximo deve ser maior que 0 e pelo menos o valor do mínimo. + Limites de comprimento de linha + Limites de linha de letra e do artista, e peso de caractere de largura total + "Limite de linha da letra +Divida letras mais longas em metades equilibradas. 0 desativa a divisão." + "Limite de linha do artista +Divide o formato da faixa preenchida em quadros cíclicos. 0 desativa a divisão." + "Peso de caractere de largura total +Aplica-se a ambos os limites. Em 1,75, quatro caracteres de largura total contam como 7." + Insira limites em números inteiros de 0 a 10000 e um peso de caractere maior que 0 + Intervalo instrumental + Exibido no miniplayer e na MediaSession durante um intervalo instrumental. Cada linha é um quadro de animação. {t} é o título, {a} é o artista e {lrc} é a última linha da letra + Deslocamento de tempo do miniplayer e MediaSession + Desloca quando a próxima linha é exibida no miniplayer e no MediaSession. Use um valor negativo se as linhas aparecerem com atraso, por exemplo, na tela de bloqueio Filtros Regex do filtro de letras Expressão regular aplicada a títulos, artistas e álbuns antes de procurar letras e ao avaliar se dois títulos correspondem. Deixe em branco para desativar ambos. O padrão remove elementos comuns como \"Official Video\", \"Remaster\", \"【插曲】\" e \"- Topic\" @@ -564,6 +585,10 @@ Cookies necessários: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAP Expressão regular aplicada apenas ao texto original das letras (não a traduções ou romanizações). Deixe vazio para desativar Marcadores de linha de informações Marcadores para linhas com créditos + Capitalizar primeira letra + Capitaliza a primeira letra de cada linha da letra + Remover pontuação final + Remove pontos finais, vírgulas, dois-pontos e pontos e vírgulas ao final das linhas da letra. Pontos de interrogação, pontos de exclamação, reticências, aspas e colchetes são mantidos Tamanho do texto Tamanho do texto da letra Compensação global de sincronização diff --git a/patches/src/main/resources/addresources/values-pt-rPT/youtube/strings.xml b/patches/src/main/resources/addresources/values-pt-rPT/youtube/strings.xml index e91566688e..1464437728 100644 --- a/patches/src/main/resources/addresources/values-pt-rPT/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-pt-rPT/youtube/strings.xml @@ -393,6 +393,9 @@ As denúncias são públicas e são analisadas antes de um canal ser adicionado Ocultar links de compras Esconder anúncios do vídeo Esconder promoções Premium do YouTube + + + A carregar… Canal atual: Adicionar à lista de permissões @@ -409,6 +412,9 @@ As denúncias são públicas e são analisadas antes de um canal ser adicionado Resultados da pesquisa de canais Nada encontrado neste canal Não foi possível pesquisar neste canal + Vídeos + Postagens + Nenhuma postagem neste canal corresponde Relevância Mais vistos Menos vistos @@ -442,6 +448,7 @@ As denúncias são públicas e são analisadas antes de um canal ser adicionado Descarregar imagem A descarregar %s imagens… Baixando imagem… + %1$s imagens salvas em %2$s Imagem salva em %s Não foi possível baixar a imagem Nenhuma imagem encontrada nesta publicação @@ -493,6 +500,9 @@ Depois que um vídeo com o botão Ask for aberto uma vez, o recurso funciona com Vídeo Áudio Todas as opções + Mostrar download no menu Shorts + Adiciona um item de menu flyout de download externo ao player Shorts + Transferir Tradução de narração Leia as legendas do vídeo em voz alta usando a conversão de texto em fala no dispositivo, sincronizada com a reprodução @@ -1120,9 +1130,6 @@ Layout automotivo "Restaura o layout antigo do player Limitação: Se 'Restaurar a barra de ação de vídeo antiga' estiver desativado, a sobreposição de 'Mais vídeos' pode não estar disponível" - "Habilitar isso pode quebrar alguns recursos de layout do player, como a seção de comentários ou \"Mais vídeos\" exibidos abaixo do player. - -O layout de tablet não é mais suportado pelo YouTube e está parcial ou totalmente quebrado em versões recentes do YouTube. Para um layout de tablet funcional, aplique o patch no YouTube 20.21.37" 21.11.48 - Restaurar comportamento antigo de legendas 20.39.41 - Restaurar a versão antiga do Morphe Spoof para a playlist @@ -1625,4 +1632,17 @@ A gravação automática é opcional. Ela pára no modo anônimo ou quando a con Nenhuma série encontrada Episódio sem título Episódio indisponível + + Comentários do Niconico + Exibe o chat ao vivo e a reprodução do chat como comentários que deslizam pelo vídeo + Exibe um botão de sobreposição no player de vídeo. Toque para mostrar ou ocultar os comentários, toque e segure para configurações rápidas + Tamanho da fonte do comentário + Tamanho do texto dos comentários (12-40 sp) + Opacidade do comentário + Opacidade do texto e dos fundos dos comentários + Duração do deslocamento + Quantos segundos os comentários levam para atravessar a tela. Valores menores movem mais rápido + Comprimento máximo do comentário + Oculta comentários com mais caracteres que este. Cada emoji conta como um caractere + Sem limite diff --git a/patches/src/main/resources/addresources/values-ro-rRO/music/strings.xml b/patches/src/main/resources/addresources/values-ro-rRO/music/strings.xml index 41959f1ac1..c5af8ecc9f 100644 --- a/patches/src/main/resources/addresources/values-ro-rRO/music/strings.xml +++ b/patches/src/main/resources/addresources/values-ro-rRO/music/strings.xml @@ -538,8 +538,29 @@ Cookie-uri necesare: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPI Afișează linia curentă pe ecranul de blocare, Android Auto și Bluetooth. Necesită Android 11 sau versiuni mai noi Afișați versurile miniplayer. Afișează linia curentă în miniplayer, cu piesa mutată în subtitlu - Afișați ordinea artistului și a titlului. - Comutați vizibilitatea miniplayer-ului. + Format titlu piesă/artist + Formatul piesei afișate împreună cu versurile în miniplayer și MediaSession. {t} este titlul, {a} este artistul și {v} este versiunea, cum ar fi Acoustic, atunci când sursa versurilor furnizează una. Textul dintre două două puncte este afișat doar atunci când fiecare variabilă din interiorul acestuia are o valoare; scrie \\: pentru două puncte simple. Fiecare linie este afișată la rând, schimbându-se oricând versul se modifică + Format linie vers + Formatul liniei de versuri în miniplayer și MediaSession. {lrc} este linia și {n} este de câte ori în șir este cântată. Textul dintre două puncte este afișat doar atunci când linia se repetă. Folosește \\: pentru două puncte + Formatul trebuie să conțină {lrc} + Șablon linie artist și temporizarea animației + Timp minim pe ecran (ms) + Timp maxim pe ecran (ms) + Doar pentru animații multilinie. Mențineți fiecare cadru pentru cel puțin timpul minim. La valoarea maximă, avansați linia artistului chiar dacă versul rămâne același. + Valoarea minimă trebuie să fie 0 sau mai mare. Valoarea maximă trebuie să fie mai mare de 0 și cel puțin egală cu cea minimă. + Limite lungime linie + Limite linie vers și artist, și ponderea caracterelor cu lățime completă + "Limită linie vers +Împarte versurile mai lungi în jumătăți echilibrate. 0 dezactivează împărțirea." + "Limită linii artist +Împarte formatul pistei populate în cadre ciclice. 0 dezactivează împărțirea." + "Greutatea caracterelor cu lățime completă +Se aplică ambelor limite. La 1,75, patru caractere cu lățime completă sunt calculate ca 7." + Introduceți limite cu numere întregi de la 0 la 10000 și o greutate a caracterelor mai mare de 0 + Pauză instrumentală + Afișat în miniplayer și MediaSession în timpul unei pauze instrumentale. Fiecare linie este un cadru de animație. {t} este titlul, {a} este artistul și {lrc} este ultima linie de versuri + Decalajul temporal pentru miniplayer și MediaSession + Decalează momentul în care următoarea linie este afișată în playerul minimizat și în MediaSession. Utilizați o valoare negativă dacă liniile apar cu întârziere, de exemplu pe ecranul de blocare Afișați titlul. Afișați subtitlul. Expresie regulată aplicată titlurilor, artiștilor și albumelor înainte de a căuta versurile și atunci când se stabilește dacă două titluri se potrivesc. Lăsați gol pentru a dezactiva ambele. Valoarea implicită elimină etichetele comune precum \"Official Video\", \"Remaster\", \"【插曲】\" și \"- Topic\". @@ -547,6 +568,10 @@ Cookie-uri necesare: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPI Afișați butonul următor. Afișați butonul anterior. Afișați butonul de căutare. + Majusculă prima literă + Transformă prima literă a fiecărei linii de versuri în majusculă + Elimină punctuația de la final + Elimină punctele, virgulele, două puncte și punctele și virgulele de la sfârșitul liniilor de versuri. Semnele de întrebare, semnele exclamative, punctele de suspensie, ghilimelele și parantezele sunt păstrate Dimensiune text Dimensiunea textului versurilor Decalaj global de sincronizare diff --git a/patches/src/main/resources/addresources/values-ro-rRO/youtube/strings.xml b/patches/src/main/resources/addresources/values-ro-rRO/youtube/strings.xml index cda2924a0d..519fa4348c 100644 --- a/patches/src/main/resources/addresources/values-ro-rRO/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ro-rRO/youtube/strings.xml @@ -393,6 +393,9 @@ Rapoartele sunt publice și sunt analizate înainte ca un canal să fie adăugat Ascunde linkurile de cumpărături Ascunde reclamele video Ascunde promoțiile YouTube Premium + + + Se încarcă… Canalul curent: Adăugare la lista albă @@ -409,6 +412,9 @@ Rapoartele sunt publice și sunt analizate înainte ca un canal să fie adăugat Rezultatele căutării în canal Nu s-a găsit nimic în acest canal. Nu s-a putut căuta în acest canal. + Videoclipuri + Postări + Nicio postare de pe acest canal nu corespunde Relevanță Cele mai vizionate Cele mai puțin vizionate @@ -494,6 +500,9 @@ După ce un videoclip cu butonul Ask este deschis o dată, funcția funcționeaz Video Audio Toate opțiunile + Afișează descărcarea în meniul Shorts + Adaugă o opțiune de meniu flyout pentru descărcator extern în player-ul Shorts + Descărcare Traducere cu voce peste Citește cu voce tare subtitrările video folosind text-to-speech pe dispozitiv, sincronizat cu redarea @@ -1121,9 +1130,6 @@ Aspect auto "Restabilește aspectul vechi al playerului Limitare: Dacă 'Restaurare bara de acțiuni video veche' este dezactivată, stratul 'Mai multe videoclipuri' ar putea să nu fie disponibil" - "Activarea acestei opțiuni poate afecta unele funcții de layout ale playerului, cum ar fi secțiunea de comentarii sau „Mai multe videoclipuri” afișate sub player. - -Layout-ul pentru tabletă nu mai este suportat de YouTube și este parțial sau complet defect în versiunile recente de YouTube. Pentru un layout de tabletă funcțional, patch-uiește YouTube 20.21.37" 21.11.48 - Restabilește comportamentul vechi al subtitrărilor 20.39.41 - Restore old save to playlist @@ -1626,4 +1632,17 @@ Această bibliotecă este stocată pe acest dispozitiv și partajată între con Niciun serial găsit Episod fără titlu Episod indisponibil + + Comentarii Niconico + Afișează chat-ul live și redarea chat-ului sub formă de comentarii care defilează pe ecranul videoclipului + Afișează un buton de suprapunere în playerul video. Apasă pentru a afișa sau ascunde comentariile, apasă și menține pentru setări rapide + Dimensiunea fontului comentariilor + Dimensiunea textului comentariilor (12-40 sp) + Opacitatea comentariilor + Opacitatea textului și a fundalului comentariilor + Durata tranzitului + Câte secunde durează ca comentariile să traverseze ecranul. Valorile mai mici se deplasează mai rapid + Lungimea maximă a comentariului + Ascunde comentariile care conțin mai multe caractere decât această valoare. Fiecare emoji se numără ca un caracter + Fără limită diff --git a/patches/src/main/resources/addresources/values-ru-rRU/music/strings.xml b/patches/src/main/resources/addresources/values-ru-rRU/music/strings.xml index 517cbb0b32..0b1566bacc 100644 --- a/patches/src/main/resources/addresources/values-ru-rRU/music/strings.xml +++ b/patches/src/main/resources/addresources/values-ru-rRU/music/strings.xml @@ -550,8 +550,29 @@ SABR-воспроизведение (подмена видеопотоков о Отображает текущую строку на экране блокировки, в Android Auto и при подключении по Bluetooth. Требуется Android 11 или более поздней версии Показывать тексты в мини-плеере Отображает текущую строку в мини плеере, при этом трек перемещается к соответствующему субтитру - Порядок отображения исполнителя и названия - Переключить порядок отображения исполнителя и названия в мини-плеере и MediaSession. Включено: «Исполнитель — Название», выключено: «Название — Исполнитель» + Формат названия песни/исполнителя + Формат трека, отображаемого с текстом песни в миниплеере и MediaSession. {t} — название, {a} — исполнитель, а {v} — версия, например Acoustic, если она указана в источнике текста. Текст между двумя двоеточиями отображается только тогда, когда у каждой переменной внутри него есть значение; для обычного двоеточия используйте \\: Каждая строка отображается по очереди при смене строки текста песни + Формат строки текста песни + Формат строки текста песни в миниплеере и MediaSession. {lrc} — строка, а {n} — сколько раз подряд она поется. Текст между двоеточиями отображается только при повторе строки. Для двоеточия используйте \\: + Формат должен содержать {lrc} + Шаблон строки исполнителя и время анимации + Минимальное время отображения (мс) + Максимальное время отображения (мс) + Только для многострочных анимаций. Каждый кадр отображается не менее минимального времени. При достижении максимума строка исполнителя сменяется, даже если текст песни остается прежним. + Минимум должен быть 0 или больше. Максимум должен быть больше 0 и не меньше минимума. + Ограничения длины строки + Лимиты строк текста песни и исполнителя, а также вес полноширинных символов + "Лимит строки текста песни +Разделять длинные строки на сбалансированные части. 0 отключает разделение." + "Лимит строк исполнителя +Разделить заполненный формат трека на циклические кадры. 0 отключает разделение." + "Вес полноширинного символа +Применяется к обоим лимитам. При значении 1,75 четыре полноширинных символа считаются как 7." + Введите целые числа для лимитов от 0 до 10000 и вес символа больше 0 + Инструментальный проигрыш + Отображается в миниплеере и MediaSession во время инструментального проигрыша. Каждая строка — один кадр анимации. {t} — заголовок, {a} — исполнитель, а {lrc} — последняя строка текста песни + Смещение времени миниплеера и MediaSession + Сдвиг времени появления следующей строки в минимизированном плеере и MediaSession. Используйте отрицательное значение, если строки появляются с опозданием, например, на экране блокировки Фильтры Регулярное выражение для метаданных Регулярное выражение применяется к названиям, исполнителям и альбомам перед поиском текстов песен и при оценке совпадения двух названий. Оставьте пустым, чтобы отключить оба. По умолчанию удаляется распространённый мусор, такой как \"Official Video\", \"Remaster\", \"【插曲】\" и \"- Topic\". @@ -559,6 +580,10 @@ SABR-воспроизведение (подмена видеопотоков о Регулярное выражение применяется только к оригинальному тексту песни (не к переводам или романизациям) Маркеры информационных строк Если строки текста неправильно определяются как информационные строки, настройте эти маркеры + Начинать с заглавной буквы + Делает первую букву каждой строки текста песни заглавной + Удалять конечные знаки препинания + Удаляет точки, запятые, двоеточия и точки с запятой в конце строк текста песни. Вопросительные, восклицательные знаки, многоточия, кавычки и скобки сохраняются Размер текста Размер текста песни Глобальное смещение тайминга diff --git a/patches/src/main/resources/addresources/values-ru-rRU/youtube/strings.xml b/patches/src/main/resources/addresources/values-ru-rRU/youtube/strings.xml index aabafe595e..15534073ff 100644 --- a/patches/src/main/resources/addresources/values-ru-rRU/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ru-rRU/youtube/strings.xml @@ -393,6 +393,9 @@ Second \"item\" text" Скрыть ссылки на товары Скрыть видеорекламу Скрыть рекламу YouTube Premium + + + Загрузка… Текущий канал: Добавить в список разрешенных @@ -409,6 +412,9 @@ Second \"item\" text" Результаты поиска каналов В этом канале ничего не найдено Не удалось выполнить поиск по каналу + Видео + Посты + В этом канале не найдено подходящих постов Релевантность Самые просматриваемые Наименее просматриваемые @@ -494,6 +500,9 @@ Second \"item\" text" Видео Аудио Все параметры + Показать скачивание в меню Shorts + Добавляет пункт меню внешнего загрузчика в плеер Shorts + Скачать Перевод озвучивания Читайте субтитры видео вслух с помощью встроенного преобразования текста в речь, синхронизированного с воспроизведением @@ -1123,9 +1132,6 @@ Second \"item\" text" "Восстанавливает старый макет плеера Ограничение: Если «Восстановить старую панель управления видео» отключена, наложение «Ещё видео» может быть недоступно" - "Включение этой функции может нарушить работу некоторых элементов интерфейса плеера, таких как раздел комментариев или блок «Другие видео» под плеером. - -Планшетный интерфейс больше не поддерживается YouTube и частично или полностью не работает в последних версиях YouTube. Для корректной работы планшетного интерфейса используйте патч для YouTube 20.21.37" 21.11.48 - Восстановить прежнее поведение субтитров 20.39.41 - Восстановить старое сохранение в список воспроизведения @@ -1628,4 +1634,17 @@ AVC (H.264) Сериалы не найдены Эпизод без названия Недоступный эпизод + + Комментарии Niconico + Показывать живой чат и повтор чата в виде комментариев, проплывающих по видео + Показывает кнопку поверх видеоплеера. Нажмите, чтобы показать или скрыть комментарии, или зажмите для быстрого доступа к настройкам + Размер шрифта комментариев + Размер текста комментариев (12–40 sp) + Непрозрачность комментариев + Непрозрачность текста и фона комментариев + Длительность перемещения + Сколько секунд комментарии пересекают экран. Чем меньше значение, тем быстрее движение + Максимальная длина комментария + Скрывает комментарии, длина которых превышает это значение. Каждый эмодзи считается за один символ + Без ограничений diff --git a/patches/src/main/resources/addresources/values-si-rLK/music/strings.xml b/patches/src/main/resources/addresources/values-si-rLK/music/strings.xml index 39f59fbccd..434ef037fa 100644 --- a/patches/src/main/resources/addresources/values-si-rLK/music/strings.xml +++ b/patches/src/main/resources/addresources/values-si-rLK/music/strings.xml @@ -27,6 +27,7 @@ Second \"item\" text" නැවත්වීමට සහ නැවත පටන් ගැනීමට host එකේ Jam patch එක යාවත්කාලින කරන්න Jam පෝලිම් හවුල්කරණය + විවෘත @@ -79,5 +80,6 @@ Second \"item\" text" + ස්වයංක්‍රීය diff --git a/patches/src/main/resources/addresources/values-si-rLK/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-si-rLK/shared-youtube/strings.xml index fd007eeb78..28161f2da8 100644 --- a/patches/src/main/resources/addresources/values-si-rLK/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-si-rLK/shared-youtube/strings.xml @@ -18,6 +18,7 @@ Second \"item\" text" --> + විසන්ධි කර ඇත diff --git a/patches/src/main/resources/addresources/values-si-rLK/shared/strings.xml b/patches/src/main/resources/addresources/values-si-rLK/shared/strings.xml index 31cdbe4dcb..088ba3c656 100644 --- a/patches/src/main/resources/addresources/values-si-rLK/shared/strings.xml +++ b/patches/src/main/resources/addresources/values-si-rLK/shared/strings.xml @@ -26,7 +26,9 @@ Second \"item\" text" + අක්‍රීයයි + සක්‍රීයයි diff --git a/patches/src/main/resources/addresources/values-si-rLK/youtube/strings.xml b/patches/src/main/resources/addresources/values-si-rLK/youtube/strings.xml index 9079108272..7d16d4737d 100644 --- a/patches/src/main/resources/addresources/values-si-rLK/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-si-rLK/youtube/strings.xml @@ -19,6 +19,7 @@ Second \"item\" text" + අක්‍රීයයි @@ -51,9 +52,12 @@ Second \"item\" text" + + + දෘශ්‍ය පට @@ -89,6 +93,7 @@ Second \"item\" text" + අක්‍රීයයි @@ -111,6 +116,7 @@ Second \"item\" text" + අක්‍රීයයි @@ -168,6 +174,7 @@ Second \"item\" text" + විසන්ධි කර ඇත @@ -179,6 +186,7 @@ Second \"item\" text" + අක්‍රීයයි @@ -223,5 +231,7 @@ Second \"item\" text" + ස්වයංක්‍රීය + diff --git a/patches/src/main/resources/addresources/values-sk-rSK/music/strings.xml b/patches/src/main/resources/addresources/values-sk-rSK/music/strings.xml index b43a0bd2e4..df8dbbe23b 100644 --- a/patches/src/main/resources/addresources/values-sk-rSK/music/strings.xml +++ b/patches/src/main/resources/addresources/values-sk-rSK/music/strings.xml @@ -550,8 +550,29 @@ Vyžadované cookies: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAP Zobrazuje aktuálny riadok na zamknutej obrazovke, Android Auto a Bluetooth. Vyžaduje Android 11 alebo novší Zobraziť text piesne v mini prehrávači Zobrazuje aktuálny riadok v mini prehrávači, so stopou presunutou do titulkov - Nenašiel sa žiadny text piesne - Nenašiel sa žiadny preklad + Formát názvu skladby/umelca + Formát skladby zobrazené s textom piesne v Miniplayeri a MediaSession. {t} je názov, {a} je umelec a {v} je verzia, napríklad akustická, ak ju zdroj textu poskytuje. Text medzi dvoma dvojbodkami sa zobrazí len vtedy, keď každá premenná v ňom má hodnotu; pre obyčajnú dvojbodku napíšte \\:. Každý riadok sa zobrazuje postupne a mení sa s každou zmenou textu piesne + Formát riadku textu piesne + Formát riadka textu piesne v Miniplayeri a MediaSession. {lrc} je riadok a {n} je počet opakovaní za sebou. Text medzi dvojbodkami sa zobrazí len vtedy, keď sa riadok opakuje. Pre dvojbodku použite \\: + Formát musí obsahovať {lrc} + Šablona riadku s umelcom a časovanie animácie + Minimálny čas zobrazenia (ms) + Maximálny čas zobrazenia (ms) + Len pre viacriadkové animácie. Každý snímok ponechajte aspoň minimálny čas. Pri maximálnom čase posuňte riadok s umelcom, aj keď zostáva text piesne rovnaký. + Minimum musí byť 0 alebo viac. Maximum musí byť viac ako 0 a aspoň rovné minimu. + Limity dĺžky riadku + Limity riadkov textu piesne a umelca a váha znakov s plnou šírkou + "Limit riadku textu piesne +Rozdeľte dlhší text piesne na vyvážené polovice. Hodnota 0 vypne rozdeľovanie." + "Limit riadkov Künstly +Rozdelte vyplnený formát skladby do cyklických snímkov. Hodnota 0 vypne rozdeľovanie." + "Váha znakov plnej šírky +Platí pre oba limity. Pri hodnote 1,75 sa štyri znaky plnej šírky započítajú ako 7." + Zadajte limity ako celé čísla od 0 do 10 000 a váhu znakov väčšiu ako 0 + Instrumentálna pauza + Zobrazuje sa v Miniplayeri a MediaSession počas instrumentálnej pauzy. Každý riadok je jeden animačný snímok. {t} je názov, {a} je umelec a {lrc} je posledný riadok textu piesne + Časový posun Miniplayera a MediaSession + Posúva čas zobrazenia ďalšieho riadku v Miniplayeri a MediaSession. Ak sa riadky zobrazujú neskoro, napríklad na uzamknutej obrazovke, použite zápornú hodnotu Táto pieseň nemá text Táto pieseň nemá preklad Regulárny výraz sa aplikuje na názvy, interpretov a albumy pred vyhľadaním textov piesní a pri posudzovaní, či sa dva názvy zhodujú. Ak ho necháte prázdny, obe funkcie sa vypnú. Predvolený výraz odstraňuje bežné rušivé prvky, ako napríklad „Official Video“, „Remaster“, „【插曲】“ a „- Topic“. @@ -559,6 +580,10 @@ Vyžadované cookies: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAP Žiadne filtre Zobraziť pôvodný text piesne Skryť pôvodný text piesne + Začať veľkým písmenom + Zapíše prvé písmeno každého riadku textu piesne veľkým písmenom + Odstrániť interpunkciu na konci + Odstraňuje bodky, čiarky, dvojbodky a polobodky na konci riadkov textu piesne. Otazníky, výkričníky, trojbodkovia, úvodzovky a zátvorky zostávajú Veľkosť textu Veľkosť textu piesne Globálny časový posun diff --git a/patches/src/main/resources/addresources/values-sk-rSK/youtube/strings.xml b/patches/src/main/resources/addresources/values-sk-rSK/youtube/strings.xml index fbb850780c..928490c5fc 100644 --- a/patches/src/main/resources/addresources/values-sk-rSK/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-sk-rSK/youtube/strings.xml @@ -393,6 +393,9 @@ Hlásenia sú verejné a pred pridaním kanála do zoznamu sa posudzujú. Odstr Skryť odkazy na nákupy Skryť videoreklamy Skryť YouTube Premium promo + + + Načítava sa… Aktuálny kanál: Pridať do bielej listiny @@ -409,6 +412,9 @@ Hlásenia sú verejné a pred pridaním kanála do zoznamu sa posudzujú. Odstr Výsledky vyhľadávania kanálov Na tomto kanáli sa nič nenašlo Vyhľadávanie v tomto kanáli sa nepodarilo + Videá + Príspevky + Na tomto kanáli sa nenašli žiadne príspevky Relevantnosť Najsledovanejšie Najmenej sledované @@ -442,6 +448,7 @@ Hlásenia sú verejné a pred pridaním kanála do zoznamu sa posudzujú. Odstr Stiahnuť obrázok Sťahovanie %s obrázkov… Sťahovanie obrázka… + %1$s obrázkov uložených do %2$s Obrázok uložený do %s Nebolo možné sťahovať obrázok V tomto príspevku nebol nájdený žiadny obrázok @@ -493,6 +500,9 @@ Po jednom otvorení videa s tlačidlom Opýtať sa bude funkcia fungovať pre v Video Zvuk Všetky možnosti + Zobraziť sťahovanie v ponuke Shorts + Pridá položku externého menu na stiahnutie do prehrávača Shorts + Stiahnuť Preklad hlasového komentára Nahlas prečítajte titulky videa pomocou text-to-speech priamo v zariadení, synchronizovane s prehrávaním @@ -1119,9 +1129,6 @@ Rozloženie pre automobil "Obnoví staré rozloženie prehrávača Obmedzenie: Ak je vypnutá možnosť 'Obnoviť starú lištu akcií videa', vrstva 'Ďalšie videá' nemusí byť k dispozícii" - "Povolenie tejto možnosti môže narušiť niektoré funkcie rozloženia prehrávača, ako napríklad sekciu komentárov alebo „Ďalšie videá“ zobrazené pod prehrávačom. - -Rozloženie pre tablety už YouTube nepodporuje a v novších verziách YouTube je čiastočne alebo úplne nefunkčné. Pre fungujúce rozloženie pre tablety použite patch pre YouTube 20.21.37" 21.11.48 - Obnoviť staré správanie titulkov 20.39.41 - Obnoví staré ukladanie do zoznamu videí @@ -1624,4 +1631,17 @@ Automatické zaznamenávanie je voliteľné. Zastaví sa v režime inkognito ale Nenašli sa žiadne seriály Epizóda bez názvu Epizóda nedostupná + + Komentáre Niconico + Zobrazuje živý čet a opakovanie četu ako komentáre, ktoré pretekajú cez video + Zobrazuje tlačidlo v prehrávači videa. Klepnutím zobrazíte alebo skryjete komentáre, podržaním otvoríte rýchle nastavenia + Veľkosť písma komentárov + Veľkosť textu komentárov (12-40 sp) + Nepriehľadnosť komentárov + Nepriehľadnosť textu a pozadia komentárov + Doba prechodu + Koľko sekúnd trvá, kým komentáre prejdú cez obrazovku. Nižšie hodnoty znamenajú rýchlejší pohyb + Maximálna dĺžka komentára + Skryje komentáre s väčším počtom znakov, ako je uvedené. Každý emoji sa počíta ako jeden znak + Bez obmedzenia diff --git a/patches/src/main/resources/addresources/values-sl-rSI/music/strings.xml b/patches/src/main/resources/addresources/values-sl-rSI/music/strings.xml index e97e39ea28..18add0c473 100644 --- a/patches/src/main/resources/addresources/values-sl-rSI/music/strings.xml +++ b/patches/src/main/resources/addresources/values-sl-rSI/music/strings.xml @@ -541,8 +541,29 @@ Zahtevani piškotki: SID, HSID, SSID, SAPISID, __Secure-3APISID, __Secure-1APISI Prikazuje trenutno vrstico na zaklenjenem zaslonu, Android Auto in Bluetooth. Zahteva Android 11 ali novejši Prikažite besedilo v mini predvajalniku. Prikazuje trenutno vrstico v majhnem predvajalniku, pri čemer je skladba premaknjena v podnapis - Vrstni red prikaza izvajalca in naslova - Preklopite vrstni red prikaza izvajalca in naslova v mini predvajalniku in MediaSession. VKLOPLJENO \"Izvajalec - Naslov\" in IZKLOPLJENO \"Naslov - Izvajalec\". Ne vpliva na iskanje besedil. + Format naslova pesmi/umetnika + Format skladbe, ki se prikazuje z besedilom v mini predvajalniku in MediaSession. {t} je naslov, {a} je umetnik in {v} je različica, kot je npr. akustična, če jo vir besedila zagotavlja. Besedilo med dvema dvopisnicama se prikaže le, če ima vsaka spremenljivka znotraj nje vrednost; za običajno dvopisnico zapišite \\:. Vsaka vrstica se prikaže zaporedno in se spremeni, ko se spremeni besedilo pesmi + Format vrstice besedila + Format vrstice besedila v mini predvajalniku in MediaSession. {lrc} je vrstica in {n} je število zaporednih ponovitev, ko je vrstica zapeta. Besedilo med dvopisnicama se prikaže le, ko se vrstica ponavlja. Za dvopisnico uporabite \\: + Format mora vsebovati {lrc} + Predloga vrstice umetnika in časovni razred animacije + Minimalni čas na zaslonu (ms) + Maksimalni čas na zaslonu (ms) + Le za večvrstične animacije. Vsak okvir obdržite vsaj minimalni čas. Pri maksimalnem času napredujte vrstico umetnika, tudi če ostane besedilo pesmi isto. + Minimalna vrednost mora biti 0 ali več. Maksimalna vrednost mora biti več kot 0 in vsaj tolika kot minimalna. + Omejitve dolžine vrstice + Omejitve vrstic besedila pesme in umetnika ter težka polnosestnih znakov + "Omejitev vrstice besedila pesme +Daljša besedila razdelite na uravnoteženi polovici. Vrednost 0 izklopi razdelitev." + "Omejitev vrtic za umetnika +Razdelite zapolnjen format traku v ciklične okvirje. 0 onemogoči razdelitev." + "Teža polnoznačajnega znaka +Velja za obe omejitvi. Pri 1,75 štirje polnoznačajni znaki štejejo kot 7." + Vnesite celotište omejitve od 0 do 10000 in težo znaka, ki je večja od 0 + Instrumentalni prekid + Prikazano v mini predvoravalcem in MediaSession med instrumentalnim prekidom. Vsaka vrstica je eden animacijskega okvirja. {t} je naslov, {a} je umetnik in {lrc} je zadnja vrstica besedila + Časovni pomik mini predvoravalcem in MediaSession + Premika čas, ko se v mini predvoravatelju in MediaSession prikaže naslednja vrstica. Uporabite negativno vrednost, če se vrstice pojavijo pozno, na primer na zaklenjenem zaslonu Filtri Regex metapodatkov Regularni izraz se uporablja za naslove, izvajalce in albume pred iskanjem besedil ter pri ocenjevanju, ali se dva naslova ujemata. Pustite prazno, da onemogočite oboje. Privzeta nastavitev odstrani pogoste odvečne dele, kot so \"Official Video\", \"Remaster\", \"【插曲】\" in \"- Topic\". @@ -550,6 +571,10 @@ Zahtevani piškotki: SID, HSID, SSID, SAPISID, __Secure-3APISID, __Secure-1APISI Regularni izraz se uporabi samo za izvirno besedilo (ne za prevode ali romanizacije). Pustite prazno, da onemogočite. Oznake informacijskih vrstic Če so vrstice besedila nepravilno filtrirane na dno, prilagodite ali počistite to. […] Pustite prazno, da onemogočite. + Zapiši prvo črko z veliko + Zapiše prvo črko vsake vrstice besedila z veliko + Odstrani končna interpunkcijska znamenja + Odstrani pike, vejice, dvpike in semicolone na koncu vrstic besedila. Vprašalniki, kličaji, tripičja, narekovajniki in oklepi se ohranijo Velikost besedila Velikost besedila besedil. Globalni časovni odmik diff --git a/patches/src/main/resources/addresources/values-sl-rSI/youtube/strings.xml b/patches/src/main/resources/addresources/values-sl-rSI/youtube/strings.xml index 1652292e59..6200f293a6 100644 --- a/patches/src/main/resources/addresources/values-sl-rSI/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-sl-rSI/youtube/strings.xml @@ -393,6 +393,9 @@ Prijave so javne in so pregledane, preden je kanal dodan na seznam. Odstranitev Skrij povezanih izdelkov Skrij video oglase Skrij promocije YouTube Premium + + + Nalaganje… Trenutni kanal: Dodaj na belo listo @@ -409,6 +412,9 @@ Prijave so javne in so pregledane, preden je kanal dodan na seznam. Odstranitev Rezultati iskanja kanalov Na tem kanalu ni najdenih rezultatov. Iskanje po tem kanalu ni uspelo. + Videoposnetki + Objave + Na tem kanalu ni objav, ki bi ustrezale iskanju Ustreznost Najbolj gledani Najmanj gledani @@ -442,6 +448,7 @@ Prijave so javne in so pregledane, preden je kanal dodan na seznam. Odstranitev Prenesi sliko Prenašanje %s slik… Prenašanje slike… + %1$s slik je shranjenih v %2$s Slika shranjena v %s Slike ni bilo mogoče prenesti V tem objavi ni bilo najdene slike @@ -493,6 +500,9 @@ Ko enkrat odprete video s gumbom Zastavi, bo funkcija delovala za vse videe, tud Video Avdio Vse možnosti + Prikaži prenos v meniju Shorts + Dodaja zunanji element menija za prenos v predvajalnik Shorts + Prenesite Prevajanje z glasom Prebiranje napisov videoposnetka v glas z uporabo pretvorbe besedila v govor v napravi, sinhronizirano s predvajanjem @@ -1120,9 +1130,6 @@ Postavitev za avtomobile "Ponastavi prejšnjo postavitev predvajalnika Omejitev: Če je možnost »Ponastavi staro vrstico za videoposnetke« izklopljena, »Več videoposnetkov« morda ne bo na voljo" - "Omogočitev tega lahko povzroči težave s nekaterimi funkcijami pomizbe predvajalnika, kot so razdelek s komentarji ali \"Več videov\", ki se prikazujejo pod predvajalnikom. - -Pomizba za tablične računalnike ni več podprta v YouTube in je v novjših različicah YouTube deloma ali popolnoma neuporabna. Za delujočo pomizbo za tablične računalnike uporabite popravek za YouTube 20.21.37" 21.11.48 - Obnovi staro obnašanje podnapisov 20.39.41 - Ponastavi staro shranjevanje v seznam predvajanja @@ -1625,4 +1632,17 @@ Samodejni zapis je opcijski. Ustavi se v zasebnem načinu ali ob spremembi raču Ni najdenih serij Epizoda brez naslova Epizoda ni na voljo + + Niconico komentarji + Prikaži klepet v živo in ponovitev klepeta kot komentarje, ki se premikajo čez video + V predvajalniku videa prikaže gumb za prekrivanje. Dotaknite se za prikaz ali skrite komentarjev, pridržite pa za hitre nastavitve + Velikost pisave komentarjev + Velikost besedila komentarjev (12-40 sp) + Neprozornost komentarjev + Neprozornost besedila komentarjev in ozadij + Trajanje poti + Koliko sekund komentarji potrebujejo za preсеkanje zaslona. Nižje vrednosti pomenijo hitrejše premikanje + Največja dolžina komentarja + Skrije komentarje, ki imajo več znakov kot ta vrednost. Vsak emoji se šteje za en znak + Brez omejitev diff --git a/patches/src/main/resources/addresources/values-sr-rCS/music/strings.xml b/patches/src/main/resources/addresources/values-sr-rCS/music/strings.xml index 8de5663a74..891447f349 100644 --- a/patches/src/main/resources/addresources/values-sr-rCS/music/strings.xml +++ b/patches/src/main/resources/addresources/values-sr-rCS/music/strings.xml @@ -546,8 +546,29 @@ Potrebni kolačići: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPI Prikazuje trenutnu liniju na zaključanom ekranu, Android Auto i Bluetooth-u. Potreban je Android 11 ili noviji Prikaži stihove u mini plejeru. Prikazuje trenutnu liniju u miniplereju, sa pomeranjem tračke u titl - Redosled prikaza izvođača i naslova. - Promeni redosled prikaza izvođača i naslova u mini plejeru i […] UKLJUČENO „Artist - Title“ i ISKLJUČENO „Title - Artist“. Ne utiče na pretragu stihova. + Format naslova pesme/izvođača + Format trake koji se prikazuje uz tekst pesme u Miniplayer-u i MediaSession-u. {t} je naslov, {a} je izvođač i {v} je verzija, kao što je akustična, kada izvor teksta pesme to obezbedi. Tekst između dve dvotačke se prikazuje samo kada svaka varijabla unutar njega ima vrednost; napišite \\: za običnu dvotačku. Svaka linija se prikazuje redom, prelazeći na sledeću kad god se promeni tekst pesme + Format linije teksta pesme + Format linije teksta pesme u Miniplayer-u i MediaSession-u. {lrc} je linija, a {n} je broj puta koliko se ona zaredom peva. Tekst između dvotačaka se prikazuje samo kada se linija ponavlja. Koristite \\: za dvotačku + Format mora sadržati {lrc} + Šablon linije izvođača i tajming animacije + Minimalno vreme prikazivanja na ekranu (ms) + Maksimalno vreme prikazivanja na ekranu (ms) + Samo za animacije u više redova. Zadržite svaki okvir najmanje minimalno vreme. Na maksimalnom, pomerite liniju izvođača čak i kada tekst pesme ostaje isti. + Minimalna vrednost mora biti 0 ili veća. Maksimalna mora biti veća od 0 i najmanje jednaka minimalnoj. + Ograničenja dužine linije + Ograničenja linija teksta pesme i izvođača, i težina karaktera pune širine + "Ograničenje linije teksta pesme +Podelite duže tekstove pesama na uravnotežene polovine. 0 onemogućava podelu." + "Limit linija za umetnika +Podeli popunjen format trake u ciklične okvire. 0 onemogućava podelu." + "Težina karaktera pune širine +Primenjuje se na oba limita. Na 1.75, četiri karaktera pune širine računaju se kao 7." + Unesite celobrojne limite od 0 do 10000 i težinu karaktera veću od 0 + Instrumentalna pauza + Prikazuje se u mini-plejeru i MediaSession-u tokom instrumentalne pauze. Svaka linija je jedan animacioni okvir. {t} je naslov, {a} je umetnik, a {lrc} je poslednja linija teksta + Vremenski pomak mini-plejera i MediaSession-a + Pomera vreme kada se sledeći red prikazuje u minimizovanom plejeru i MediaSession-u. Koristite negativnu vrednost ako se redovi pojavljuju sa zakašnjenjem, na primer na zaključanom ekranu Filteri Regex metapodataka Regularni izraz koji se primenjuje na naslove, izvođače i albume pre traženja tekstova i pri ocenjivanju da li se dva naslova poklapaju. Ostavite prazno da biste onemogućili oba. Podrazumevana vrednost uklanja uobičajene nepotrebne dodatke kao što su „Official Video“, „Remaster“, „【插曲】“ i „- Topic“ @@ -555,6 +576,10 @@ Potrebni kolačići: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPI Regularni izraz se primenjuje samo na originalni tekst stihova (ne na prevode ili romanizacije). Ostavite prazno da biste onemogućili. Oznake redova s informacijama Ako se stihovi pogrešno filtriraju na dno, prilagodite ovo ili obrišite. […] Ostavite prazno da biste onemogućili. + Veliko početno slovo + Piše prvo slovo svakog reda teksta pesme velikim slovom + Ukloni znakove interpunkcije na kraju + Uklanja tačke, zareze, dvotačke i tačka-zareze na kraju redova teksta pesme. Upitnici, znaci uzvika, tri tačke, navodnici i zagrade se zadržavaju Veličina teksta Veličina teksta stihova Globalni vremenski pomak diff --git a/patches/src/main/resources/addresources/values-sr-rCS/youtube/strings.xml b/patches/src/main/resources/addresources/values-sr-rCS/youtube/strings.xml index 37d9398a6d..7b095d3f7a 100644 --- a/patches/src/main/resources/addresources/values-sr-rCS/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-sr-rCS/youtube/strings.xml @@ -393,6 +393,9 @@ Prijave su javne i pregledaju se pre nego što kanal bude dodat na listu. Uklanj Sakrij linkove za kupovinu Sakrij video oglase Sakrij promocije za YouTube Premium + + + Učitavanje… Trenutni kanal: Dodaj na listu dozvoljenih kanala @@ -409,6 +412,9 @@ Prijave su javne i pregledaju se pre nego što kanal bude dodat na listu. Uklanj Rezultati pretrage kanala Ništa nije pronađeno na ovom kanalu Pretraga ovog kanala nije uspela + Video snimci + Objave + Nijedna objava na ovom kanalu se ne poklapa Relevantnost Najgledanije Najmanje gledano @@ -442,6 +448,7 @@ Prijave su javne i pregledaju se pre nego što kanal bude dodat na listu. Uklanj Preuzmi sliku Preuzimanje %s slika… Preuzimanje slike… + %1$s slika je sačuvano u %2$s Slika je sačuvana u %s Nije bilo moguće preuzeti sliku U ovoj objavi nije pronađena slika @@ -493,6 +500,9 @@ Nakon što se jednom otupi video sa dugmetom Ask, funkcija radi sa svim videima, Video Audio Sve opcije + Prikaži preuzimanje u izborniku Shorts + Dodaje stavku menija za spoljno preuzimanje u reproduktor Shorts + Preuzmi Prevod sinhronizovane titlova Učita titlove iz videa i izgovara ih pomoću ugrađenog pretvarača teksta u govor, sinhronizovano sa reprodukcijom @@ -1120,9 +1130,6 @@ Raspored za automobil "Vraća stari raspored plejera Ograničenje: Ako je \"Vrati staru traku sa akcijama za video\" isključen, prekrivanje \"Više video zapisa\" možda neće biti dostupno" - "Omogućavanjem ovoga mogu se pokvariti neke funkcije rasporeda plejera, kao što su odeljak sa komentarima ili \"Više video zapisa\" koji se prikazuju ispod plejera. - -Raspored za tablete više nije podržan od strane YouTube-a i delimično ili potpuno je pokvaren u novijim verzijama YouTube-a. Za funkcionalan raspored za tablete, instalirajte zakrpu za YouTube 20.21.37" 21.11.48 - Vrati staro ponašanje titlova 20.39.41 - Vraća stare ikonice Shorts plejera @@ -1625,4 +1632,17 @@ Automatsko snimanje je opciono. Zaustavlja se u privatnom modu ili kada se nalog Nema pronađenih serija Epizoda bez naslova Dostupna epizoda + + Niconico komentari + Prikaži live chat i ponovnice chata kao komentare koji klize preko videa + Prikazuje overlay dugme u video plejeru. Dodirnite da prikažete ili sakrijete komentare, zadržite dodir za brza podešavanja + Veličina fonta komentara + Veličina teksta komentara (12-40 sp) + Opacitet komentara + Opacitet teksta i pozadina komentara + Trajanje putovanja + Koliko sekundi je potrebno komentarima da pređu ekran. Niže vrednosti se kreću brže + Maksimalna dužina komentara + Skriva komentare koji imaju više znakova od ovoga. Svaki emoji se računa kao jedan znak + Bez ograničenja diff --git a/patches/src/main/resources/addresources/values-sr-rSP/music/strings.xml b/patches/src/main/resources/addresources/values-sr-rSP/music/strings.xml index 31b7ba1517..69629ab7dd 100644 --- a/patches/src/main/resources/addresources/values-sr-rSP/music/strings.xml +++ b/patches/src/main/resources/addresources/values-sr-rSP/music/strings.xml @@ -550,8 +550,29 @@ Da biste osigurali sinhronizaciju scrobbling-a tokom reprodukcije u pozadini, po Приказује текућу линију на закључаном екрану, Android Auto и Bluetooth. Потребан је Android 11 или новији Прикажи текстове у миниплејеру. Приказује текућу линију у мини играчу, са стазом премештеном у потпис - Приказ извођача и наслова […] УКЉУЧЕНО: \"Извођач - Наслов\", ИСКЉУЧЕНО: \"Наслов - Извођач\". Не утиче на претрагу текстова. - Промените редослед приказа извођача и назива у мини-плејеру и MediaSession-у. УКЉУЧЕНО \"Извођач - Назив\", ИСКЉУЧЕНО \"Назив - Извођач\". Не утиче на претрагу текстова + Формат наслова песме/уметника + Формат траке који се приказује уз текст песме у миниплејеру и MediaSession-у. {t} је наслов, {a} је уметник и {v} је верзија, као што је Acoustic, када извор текста песме то обезбеди. Текст између две двотачке се приказује само када свака варијабла унутар њега има вредност; напишите \\: за обичну двотачку. Свака линија се приказује редометом, прелазећи на следећу кад год што се промени стих + Формат линије текста песме + Формат линије текста песме у миниплејеру и MediaSession-у. {lrc} је линија, а {n} је број пута колико се она заредно пева. Текст између двотачака се приказује само када се линија понавља. Користите \\: за двотачку + Формат мора да садржи {lrc} + Шаблон линије уметника и тајминг анимације + Минимално време приказивања на екрану (ms) + Максимално време приказивања на екрану (ms) + Само за вишелинијске анимације. Задржите сваки фрејм најмање минимално време. На максималном, померите линију уметника чак и када текст песме остане исти. + Минимална вредност мора бити 0 или већа. Максимална вредност мора бити већа од 0 и најмање иста као минимална. + Ограничења дужине линије + Ограничења линија текста песме и уметника, и тежина карактера пуне ширине + "Ограничење линије текста песме +Поделите дуже текстове на уравнотежене половине. 0 онемогућава делење." + "Лимит линија за извођача +Подели populated формат траке у цикличне фрејмове. 0 онемогућава делење." + "Тежина карактера пуне ширине +Примењује се на оба лимита. На 1.75, четири карактера пуне ширине рачунају се као 7." + Унесите лимите као целе бројеве од 0 до 10000 и тежину карактера већу од 0 + Инструментална пауза + Приказује се у Миниплејеру и MediaSession-у током инструменталне паузе. Свака линија је један фрејм анимације. {t} је наслов, {a} је извођач, а {lrc} је последња линија текста песме + Временски офсет Миниплејера и MediaSession-а + Помера времена када се следећа линија приказује у минимизованом плејеру и MediaSession-у. Користите негативну вредност ако се линије појављују са закашњењем, на пример на екрану за закључавање Филтери Регуларни израз за филтрирање текстова Регуларни израз примењен на наслове, извођаче и албуме пре тражења текстова и приликом оцењивања да ли се два наслова поклапају. Оставите празно да бисте онемогућили обоје. Подразумевано уклања уобичајене непотребне ознаке као што су \"Official Video\", \"Remaster\", \"【插曲】\" и \"- Topic\". @@ -559,6 +580,10 @@ Da biste osigurali sinhronizaciju scrobbling-a tokom reprodukcije u pozadini, po Регуларни израз се примењује само на оригинални текст песме (не на преводе или романизације). Оставите празно да бисте искључили. Ознаке инфо линија Ако су текстови погрешно филтрирани на дно, прилагодите или обришите ово. […] Оставите празно да бисте искључили. + Постави прво слово као велико + Поставља прво слово сваке линије текста песме као велико + Уклони интерпункцију на крају + Уклања тачке, зарезе, две тачке и тачка-зарезе на крају линија текста песме. Упитни знаци, узвици, три тачке, наводници и заграде се задржавају Величина текста Величина текста текстова песама Глобални временски померај diff --git a/patches/src/main/resources/addresources/values-sr-rSP/youtube/strings.xml b/patches/src/main/resources/addresources/values-sr-rSP/youtube/strings.xml index 51fcbc4f2b..b597636464 100644 --- a/patches/src/main/resources/addresources/values-sr-rSP/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-sr-rSP/youtube/strings.xml @@ -393,6 +393,9 @@ Second \"item\" text" Сакриј линкове за куповину Сакриј видео огласе Сакриј промоције за YouTube Premium + + + Учитавање… Тренутни канал: Додај на белу листу @@ -409,6 +412,9 @@ Second \"item\" text" Резултати претраге канала Ништа није пронађено на овом каналу Претрага овог канала није успела + Видео снимци + Објаве + Нема објава на овом каналију које одговарају претрази Релевантност Најгледанији Најмање гледани @@ -494,6 +500,9 @@ Second \"item\" text" Видео Аудио Све опције + Прикажи преузимање у менију Shorts + Додаје ставку спољног менија за преузимање на плејер Shorts + Преузми Превод гласовног надзора Учитавајте титлове видео записа наглас користећи текст у говор на уређају, синхронизовано са репродукцијом @@ -1104,9 +1113,6 @@ Second \"item\" text" "Враћа стари изглед плејера Ограничење: Ако је „Враћање старе траке акција видеа“ искључено, преклапање „Више видеа“ можда неће бити доступно" - "Омогућавање овога може покварити неке функције изгледаја плејера, као што су одељак за коментаре или „Више видео записа“ приказани испод плејера. - -YouTube више не подржава таблет изглед, и он је делимично или потпуно некоректен у новијим верзијама YouTube-а. За функционалан таблет изглед, патчујте YouTube 20.21.37" 21.11.48 - Врати старо понашање титлова 20.39.41 - Враћа старе иконице плејера @@ -1609,4 +1615,17 @@ AVC (H.264) Ne pronađene serije Bez naziva epizode Neдоступna epizoda + + Niconico коментари + Прикажи разговор уживо и снимак разговора као коментаре који се померају преко видео снимка + Приказује преклопно dugme у видео плејеру. Додирните да прикажете или сакријете коментаре, додирните и задржите за брза подешавања + Величина fonte коментара + Величина текста коментара (12-40 sp) + Прозирност коментара + Прозирност текста коментара и позадина + Трајање кретања + Колико секунда је потребно коментарима да пређу екран. Мање вредности се померају брже + Максимална дужина коментара + Сакрива коментарe са више знакова од овога. Сваки емоџи се рачуна као један знак + Без ограничења diff --git a/patches/src/main/resources/addresources/values-sv-rSE/music/strings.xml b/patches/src/main/resources/addresources/values-sv-rSE/music/strings.xml index 1f0286e674..69213f246e 100644 --- a/patches/src/main/resources/addresources/values-sv-rSE/music/strings.xml +++ b/patches/src/main/resources/addresources/values-sv-rSE/music/strings.xml @@ -550,8 +550,29 @@ Obligatoriska cookies: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PA Visar nuvarande rad på låsskärmen, Android Auto och Bluetooth. Kräver Android 11 eller nyare Visa text i minispelaren Visar nuvarande rad i minispelaren, med spåret flyttat till undertexten - Visning av artist och titel […] MediaSession. PÅ \"Artist – Titel\" och AV \"Titel – Artist\". Påverkar inte textsökning. - Växla visningsordningen för Artist och Titel i minispelaren och MediaSession. PÅ \"Artist - Titel\" och AV \"Titel - Artist\" Påverkar inte textsökningen + Format för låttitel/artist + Formatet för spåret som visas med texten i minispelaren och MediaSession. {t} är titeln, {a} är artisten och {v} är versionen, till exempel akustisk, om textkällan tillhandahåller en. Text mellan två kolon visas endast när varje variabel i den har ett värde; skriv \\: för ett vanligt kolon. Varje rad visas i tur och ordning och växlar när texten ändras + Format för textrad + Formatet för textraden i minispelaren och MediaSession. {lrc} är raden och {n} är hur många gånger i rad den sjungs. Text mellan kolon visas endast när raden upprepas. Använd \\: för ett kolon + Formatet måste innehålla {lrc} + Mall för artistrad och animationstajming + Minsta skärmtid (ms) + Maximal skärmtid (ms) + Endast för flerradiga animationer. Behåll varje bildruta i minst den minsta tiden. Vid den maximala tiden går artistraden vidare även om texten förblir densamma. + Minsta måste vara 0 eller högre. Maximala måste vara större än 0 och minst lika med det minsta värdet. + Gränser för radlängd + Gränser för text- och artistrader, samt vikt för helbreddstecken + "Gräns för textrad +Dela upp längre texter i balanserade halvor. 0 inaktiverar uppdelning." + "Artistradsgräns +Dela upp det ifyllda spårformatet i cykliska ramar. 0 inaktiverar uppdelning." + "Vikt för helbreddstecken +Gäller båda gränserna. Vid 1,75 räknas fyra helbreddstecken som 7." + Ange heltal för gränser från 0 till 10000 och en teckenvikt större än 0 + Instrumentalt parti + Visas i miniplayer och MediaSession under ett instrumentalt parti. Varje rad är en animationsram. {t} är titeln, {a} är artisten och {lrc} är den sista textraden + Tidsförskjutning för miniplayer och MediaSession + Förskjuter när nästa rad visas i minispelaren och MediaSession. Använd ett negativt värde om raderna visas för sent, till exempel på låsskärmen Filter Regex för låttextfilter Reguljärt uttryck som tillämpas på titlar, artister och album innan texter slås upp och när man bedömer om två titlar matchar. Lämna tomt för att inaktivera båda. Standardvärdet tar bort vanligt skräp som \"Official Video\", \"Remaster\", \"【插曲】\" och \"- Topic\". @@ -559,6 +580,10 @@ Obligatoriska cookies: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PA Reguljärt uttryck som endast tillämpas på originaltexten, inte översättningar eller romaniseringar. Lämna tomt för att inaktivera. Markeringar för informationsrader Om texter filtreras felaktigt till botten, justera eller rensa detta. […] Lämna tomt för att inaktivera. + Stor bokstav i början + Gör första bokstaven i varje textrad till stor bokstav + Ta bort avslutande skiljetecken + Tar bort punkter, kommatecken, kolon och semikolon i slutet av textrader. Frågetecken, utropstecken, ellips, citattecken och parenteser behålls Textstorlek Storleken på sångtexten Global tidsförskjutning diff --git a/patches/src/main/resources/addresources/values-sv-rSE/youtube/strings.xml b/patches/src/main/resources/addresources/values-sv-rSE/youtube/strings.xml index f66d6a1952..f9e4f82e3b 100644 --- a/patches/src/main/resources/addresources/values-sv-rSE/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-sv-rSE/youtube/strings.xml @@ -393,6 +393,9 @@ Rapporter är offentliga och granskas innan en kanal läggs till i en lista. Bor Dölj butikslänkar Dölj videoannonser Dölj YouTube Premium-kampanjer + + + Läser in … Nuvarande kanal: Lägg till i vitlistan @@ -409,6 +412,9 @@ Rapporter är offentliga och granskas innan en kanal läggs till i en lista. Bor Sökresultat för kanaler Inget hittades i den här kanalen Det gick inte att söka i den här kanalen + Videor + Inlägg + Inga inlägg i den här kanalen matchar Relevans Mest visade Minst visade @@ -442,6 +448,7 @@ Rapporter är offentliga och granskas innan en kanal läggs till i en lista. Bor Ladda ned bild Laddar ned %s bilder … Laddar ned bild… + %1$s bilder sparade till %2$s Bilden sparades i %s Det gick inte att ladda ned bilden Ingen bild hittades i det här inlägget @@ -493,6 +500,9 @@ När en video med Ask-knappen har öppnats en gång fungerar funktionen med alla Video Ljud Alla alternativ + Visa nedladdning i Shorts-menyn + Lägger till ett menyalternativ för extern nedladdare i Shorts-spelaren + Ladda ned Voice over-översättning Läs upp video-textning med hjälp av enhetens text-till-tal, synkroniserat med uppspelning @@ -1120,9 +1130,6 @@ Billayout "Återställer den gamla spelarlayouten Begränsning: Om 'Återställ gammalt videoåtgärdsfält' är avstängt kan överlägget 'Fler videor' inte vara tillgängligt" - "Om detta aktiveras kan vissa layoutfunktioner i spelaren sluta fungera, till exempel kommentarsfältet eller \"Fler videor\" som visas under spelaren. - -Surfplattelayout stöds inte längre av YouTube och är delvis eller helt trasig i nyare versioner av YouTube. För en fungerande surfplattelayout, patcha YouTube 20.21.37" 21.11.48 - Återställ det gamla undertextbeteendet 20.39.41 – Återställ gamla Spara i spellista @@ -1625,4 +1632,17 @@ Automatisk inspelning är valfri. Den stoppas i inkognito-läge eller när konto Inga serier hittades Avsnitt utan titel Otillgängligt avsnitt + + Niconico-kommentarer + Visa livechatt och chattuppspelning som kommentarer som rullar över videon + Visar en overlay-knapp i videospelaren. Tryck för att visa eller dölja kommentarer, tryck och håll kvar för snabbinställningar + Kommentarernas teckensnittsstorlek + Textstorlek för kommentarerna (12–40 sp) + Kommentarernas opacitet + Opacitet för kommentarstext och bakgrunder + Rörelsens varaktighet + Hur många sekunder kommentarerna tar på sig att korsa skärmen. Lägre värden rör sig snabbare + Max kommentarlängd + Döljer kommentarer som har fler tecken än detta. Varje emoji räknas som ett tecken + Ingen gräns diff --git a/patches/src/main/resources/addresources/values-sw-rKE/youtube/strings.xml b/patches/src/main/resources/addresources/values-sw-rKE/youtube/strings.xml index 9079108272..70ce03fc67 100644 --- a/patches/src/main/resources/addresources/values-sw-rKE/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-sw-rKE/youtube/strings.xml @@ -51,6 +51,8 @@ Second \"item\" text" + + @@ -224,4 +226,5 @@ Second \"item\" text" + diff --git a/patches/src/main/resources/addresources/values-ta-rIN/youtube/strings.xml b/patches/src/main/resources/addresources/values-ta-rIN/youtube/strings.xml index b2967a39aa..539b66cd1a 100644 --- a/patches/src/main/resources/addresources/values-ta-rIN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ta-rIN/youtube/strings.xml @@ -61,6 +61,8 @@ Second \"item\" text" + + @@ -237,4 +239,5 @@ Second \"item\" text" + diff --git a/patches/src/main/resources/addresources/values-te-rIN/music/strings.xml b/patches/src/main/resources/addresources/values-te-rIN/music/strings.xml index 960b06473d..25de5c2e4e 100644 --- a/patches/src/main/resources/addresources/values-te-rIN/music/strings.xml +++ b/patches/src/main/resources/addresources/values-te-rIN/music/strings.xml @@ -27,6 +27,8 @@ Second \"item\" text" + మూయండి + డిలీట్ ఫీడ్ diff --git a/patches/src/main/resources/addresources/values-te-rIN/reddit/strings.xml b/patches/src/main/resources/addresources/values-te-rIN/reddit/strings.xml index cf7776fdaf..d2b0fb5722 100644 --- a/patches/src/main/resources/addresources/values-te-rIN/reddit/strings.xml +++ b/patches/src/main/resources/addresources/values-te-rIN/reddit/strings.xml @@ -15,4 +15,6 @@ Second \"item\" text" --> - + + డిలీట్ + diff --git a/patches/src/main/resources/addresources/values-te-rIN/youtube/strings.xml b/patches/src/main/resources/addresources/values-te-rIN/youtube/strings.xml index e11c4f4775..1a0ad87c32 100644 --- a/patches/src/main/resources/addresources/values-te-rIN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-te-rIN/youtube/strings.xml @@ -56,6 +56,8 @@ Second \"item\" text" + + తొలగించు @@ -72,6 +74,8 @@ Second \"item\" text" + శోధించు + క్లియర్ చేయి @@ -238,4 +242,5 @@ Second \"item\" text" రద్దుచేయి చర్య రద్దు + diff --git a/patches/src/main/resources/addresources/values-th-rTH/music/strings.xml b/patches/src/main/resources/addresources/values-th-rTH/music/strings.xml index e874aed6e4..ded6a8a460 100644 --- a/patches/src/main/resources/addresources/values-th-rTH/music/strings.xml +++ b/patches/src/main/resources/addresources/values-th-rTH/music/strings.xml @@ -555,8 +555,29 @@ Second \"item\" text" แสดงบรรทัดปัจจุบันบนหน้าจอล็อค Android Auto และบลูทูธ ต้องการ Android 11 หรือใหม่กว่า แสดงเนื้อเพลงในมินิเพลเยอร์ แสดงบรรทัดปัจจุบันในมินิเพลเยอร์ โดยย้ายแทร็กไปยังส่วนบรรยาย - การแสดงศิลปินและชื่อเพลง […] เปิด \"ศิลปิน - ชื่อเพลง\" และปิด \"ชื่อเพลง - ศิลปิน\" ไม่มีผลต่อการค้นหาเนื้อเพลง - สลับลำดับการแสดงชื่อศิลปินและชื่อเพลงในมินิเพลเยอร์และ MediaSession เปิด \"ชื่อศิลปิน - ชื่อเพลง\" และปิด \"ชื่อเพลง - ชื่อศิลปิน\" ไม่ส่งผลต่อการค้นหาเนื้อเพลง + รูปแบบชื่อเพลง/ศิลปิน + รูปแบบของแทร็กที่แสดงพร้อมกับเนื้อเพลงใน Miniplayer และ MediaSession โดย {t} คือชื่อเพลง, {a} คือศิลปิน และ {v} คือเวอร์ชัน เช่น Acoustic เมื่อแหล่งที่มาของเนื้อเพลงมีระบุไว้ ข้อความที่อยู่ระหว่างเครื่องหมายโคลอนสองตัวจะแสดงก็ต่อเมื่อตัวแปรทุกตัวภายในนั้นมีค่า ให้ใช้ \\: สำหรับเครื่องหมายโคลอนทั่วไป แต่ละบรรทัดจะแสดงผลสลับกันไปเมื่อเนื้อเพลงเปลี่ยน + รูปแบบบรรทัดเนื้อเพลง + รูปแบบของบรรทัดเนื้อเพลงใน miniplayer และ MediaSession โดย {lrc} คือบรรทัดเนื้อเพลง และ {n} คือจำนวนครั้งที่ร้องซ้ำติดต่อกัน ข้อความที่อยู่ระหว่างเครื่องหมายโคลอนจะแสดงก็ต่อเมื่อบรรทัดนั้นมีการร้องซ้ำ ให้ใช้ \\: สำหรับเครื่องหมายโคลอน + รูปแบบต้องมี {lrc} + เทมเพลตบรรทัดศิลปินและจังหวะแอนิเมชัน + เวลาแสดงบนหน้าจอขั้นต่ำ (มิลลิวินาที) + เวลาแสดงบนหน้าจอสูงสุด (มิลลิวินาที) + สำหรับแอนิเมชันแบบหลายบรรทัดเท่านั้น ให้แสดงแต่ละเฟรมไว้อย่างน้อยตามเวลาขั้นต่ำ และเมื่อถึงเวลาสูงสุด ให้เลื่อนบรรทัดศิลปินต่อไปแม้ว่าเนื้อเพลงจะยังคงเดิม + ค่าขั้นต่ำต้องเป็น 0 หรือมากกว่า ค่าสูงสุดต้องมากกว่า 0 และไม่น้อยกว่าค่าขั้นต่ำ + ขีดจำกัดความยาวบรรทัด + ขีดจำกัดบรรทัดเนื้อเพลงและศิลปิน และน้ำหนักของตัวอักษรแบบเต็มความกว้าง + "ขีดจำกัดบรรทัดเนื้อเพลง +แบ่งเนื้อเพลงที่ยาวเกินไปให้เป็นสองส่วนที่สมดุล กำหนดเป็น 0 เพื่อปิดการแบ่งบรรทัด" + "ขีดจำกัดบรรทัดของศิลปิน +แบ่งรูปแบบแทร็กที่มีข้อมูลออกเป็นเฟรมที่วนซ้ำ การตั้งค่าเป็น 0 จะปิดการแบ่ง" + "น้ำหนักตัวอักษรความกว้างเต็ม +มีผลกับทั้งสองขีดจำกัด ที่ค่า 1.75 ตัวอักษรความกว้างเต็มสี่ตัวจะนับเป็น 7" + กรุณาป้อนขีดจำกัดเป็นจำนวนเต็มตั้งแต่ 0 ถึง 10000 และน้ำหนักตัวอักษรที่มากกว่า 0 + ช่วงบรรเลงดนตรี + แสดงในมินิเพลเยอร์และ MediaSession ระหว่างช่วงบรรเลงดนตรี แต่ละบรรทัดคือหนึ่งเฟรมแอนิเมชัน {t} คือชื่อเพลง, {a} คือศิลปิน และ {lrc} คือบรรทัดเนื้อเพลงสุดท้าย + การชดเชยเวลาของมินิเพลเยอร์และ MediaSession + ปรับเวลาแสดงบรรทัดถัดไปในมินิเพลเยอร์และ MediaSession ใช้ค่าติดลบหากบรรทัดแสดงขึ้นช้าเกินไป เช่น บนหน้าจอล็อก ตัวกรอง Metadata: \"รีเจ็กซ์ตัวกรองเนื้อเพลง\" นิพจน์ปกติที่ใช้กับชื่อเพลง ศิลปิน และอัลบั้มก่อนค้นหาเนื้อเพลง และใช้เมื่อให้คะแนนว่าชื่อสองชื่อตรงกันหรือไม่ เว้นว่างไว้เพื่อปิดใช้งานทั้งสองอย่าง ค่าเริ่มต้นจะลบคำที่ไม่จำเป็นทั่วไป เช่น \"Official Video\", \"Remaster\", \"【插曲】\" และ \"- Topic\" @@ -564,6 +585,10 @@ Second \"item\" text" นิพจน์ทั่วไปที่ใช้กับเนื้อเพลงต้นฉบับเท่านั้น (ไม่รวมคำแปลหรือข้อความแบบโรมัน) เว้นว่างเพื่อปิดใช้งาน … หากเนื้อเพลงถูกกรองไปไว้ด้านล่างอย่างไม่ถูกต้อง ให้ปรับหรือล้างค่าตรงนี้ คำกำกับที่คั่นด้วยเครื่องหมายจุลภาค: บรรทัดที่มีคำกำกับตามด้วยเครื่องหมายทวิภาคจะแสดงเป็นข้อมูลเครดิตที่ด้านล่าง เว้นว่างเพื่อปิดใช้ + ขึ้นต้นด้วยตัวพิมพ์ใหญ่ + ทำให้ตัวอักษรแรกของแต่ละบรรทัดในเนื้อเพลงเป็นตัวพิมพ์ใหญ่ + ลบเครื่องหมายวรรคตอนท้ายประโยค + ลบจุดทศนิยม จุลภาค ทวิภาค และอัฒภาคที่ท้ายบรรทัดของเนื้อเพลง โดยจะยังคงเครื่องหมายคำถาม เครื่องหมายตกใจ จุดไข่ปลา อัญประกาศ และวงเล็บไว้ ขนาดข้อความ ขนาดของข้อความเนื้อเพลง ออฟเซ็ตเวลาโดยรวม diff --git a/patches/src/main/resources/addresources/values-th-rTH/youtube/strings.xml b/patches/src/main/resources/addresources/values-th-rTH/youtube/strings.xml index 2108b78c25..3302e54af9 100644 --- a/patches/src/main/resources/addresources/values-th-rTH/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-th-rTH/youtube/strings.xml @@ -393,6 +393,9 @@ Second \"item\" text" ซ่อนลิงก์ร้านค้า ซ่อนโฆษณาวิดีโอ ซ่อนการโปรโมต YouTube Premium + + + กำลังโหลด… ช่องปัจจุบัน: เพิ่มลงในรายการที่อนุญาต @@ -409,6 +412,9 @@ Second \"item\" text" ผลการค้นหาช่อง ไม่พบผลลัพธ์ในช่องนี้ ไม่สามารถค้นหาช่องนี้ได้ + วิดีโอ + โพสต์ + ไม่มีโพสต์ในช่องนี้ที่ตรงกัน ความเกี่ยวข้อง ดูมากที่สุด ดูน้อยที่สุด @@ -494,6 +500,9 @@ Second \"item\" text" วิดีโอ เสียง ตัวเลือกทั้งหมด + แสดงการดาวน์โหลดในเมนู Shorts + เพิ่มรายการเมนู Flyout ของโปรแกรมดาวน์โหลดภายนอกในตัวเล่น Shorts + ดาวน์โหลด การแปลเสียงบรรยาย อ่านคำบรรยายวิดีโอออกเสียงด้วยระบบแปลงข้อความเป็นเสียงบนอุปกรณ์ โดยซิงโครไนซ์กับการเล่น @@ -1121,9 +1130,6 @@ Second \"item\" text" "คืนรูปแบบเครื่องเล่นแบบเก่า ข้อจำกัด: หากปิด 'คืนแถบการทำงานของวิดีโอแบบเก่า' ภาพซ้อนทับ 'วิดีโอเพิ่มเติม' อาจใช้งานไม่ได้" - "การเปิดใช้งานสิ่งนี้อาจทำให้คุณสมบัติบางอย่างของเลย์เอาต์เครื่องเล่นเสียหาย เช่น ส่วนความคิดเห็นหรือ 'วิดีโอเพิ่มเติม' ที่แสดงด้านล่างเครื่องเล่น - -เลย์เอาต์สำหรับแท็บเล็ตไม่ได้รับการรองรับโดย YouTube อีกต่อไป และอาจใช้งานไม่ได้บางส่วนหรือทั้งหมดใน YouTube เวอร์ชันล่าสุด สำหรับเลย์เอาต์แท็บเล็ตที่ใช้งานได้ ให้แพตช์ YouTube 20.21.37" 21.11.48 - กู้คืนพฤติกรรมคำบรรยายแบบเดิม 20.39.41 - คืนค่าไอคอนเครื่องเล่น Shorts เก่า @@ -1626,4 +1632,17 @@ AVC (H.264) ไม่พบซีรี่ส์ ตอนที่ไม่มีชื่อเรื่อง ตอนที่ไม่สามารถใช้งานได้ + + ความคิดเห็นแบบ Niconico + แสดงแชทสดและแชทรีเพลย์เป็นความคิดเห็นที่เลื่อนผ่านวิดีโอ + แสดงปุ่มโอเวอร์เลย์ในเครื่องเล่นวิดีโอ แตะเพื่อแสดงหรือซ่อนความคิดเห็น แตะค้างไว้เพื่อเข้าสู่การตั้งค่าด่วน + ขนาดตัวอักษรของความคิดเห็น + ขนาดข้อความของความคิดเห็น (12-40 sp) + ความโปร่งใสของความคิดเห็น + ความโปร่งใสของข้อความความคิดเห็นและพื้นหลัง + ระยะเวลาในการเคลื่อนที่ + จำนวนวินาทีที่ความคิดเห็นใช้ในการเลื่อนผ่านหน้าจอ ค่าที่น้อยกว่าจะเคลื่อนที่เร็วกว่า + ความยาวคอมเมนต์สูงสุด + ซ่อนคอมเมนต์ที่มีตัวอักษรมากกว่าจำนวนนี้ แต่ละอิโมจิจะนับเป็นหนึ่งตัวอักษร + ไม่มีขีดจำกัด diff --git a/patches/src/main/resources/addresources/values-tr-rTR/music/strings.xml b/patches/src/main/resources/addresources/values-tr-rTR/music/strings.xml index 286e9852ce..73ca971af4 100644 --- a/patches/src/main/resources/addresources/values-tr-rTR/music/strings.xml +++ b/patches/src/main/resources/addresources/values-tr-rTR/music/strings.xml @@ -555,8 +555,29 @@ Gerekli çerezler: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPISI Mevcut satırı kilit ekranı, Android Auto ve Bluetooth\'ta gösterir. Android 11 veya daha yenisini gerektirir. Mini oynatıcıda şarkı sözlerini göster Mevcut satırı mini oynatıcıda gösterir, parça ismi ise alt başlığa taşınır - Sanatçı ve Başlık görüntüleme sırası - Mini oynatıcı ve MediaSession\'daki Sanatçı ve Başlık görüntüleme sırasını değiştirir. AÇIK: \"Sanatçı - Başlık\" ve KAPALI: \"Başlık - Sanatçı\" Şarkı sözü aramasını etkilemez + Şarkı başlığı/sanatçı formatı + Mini oynatıcı ve MediaSession\'da şarkı sözleriyle birlikte gösterilen parçanın formatı. {t} başlığı, {a} sanatçıyı ve {v} (şarkı sözü kaynağı sağladığında Akustik gibi) versiyonu temsil eder. İki iki nokta üst üste arasındaki metin, yalnızca içindeki tüm değişkenlerin bir değeri olduğunda gösterilir; düz iki nokta üst üste için \\: yazın. Her satır, şarkı sözleri değiştikçe sırayla gösterilir + Şarkı sözü satırı formatı + Mini oynatıcı ve MediaSession\'daki şarkı sözü satırının formatı. {lrc} satırı, {n} ise bu satırın üst üste kaç kez söylendiğini temsil eder. İki nokta üst üste arasındaki metin yalnızca satır tekrarlandığında gösterilir. İki nokta üst üste için \\: kullanın + Format {lrc} içermelidir + Sanatçı satırı şablonu ve animasyon zamanlaması + Minimum ekran süresi (ms) + Maksimum ekran süresi (ms) + Yalnızca çok satırlı animasyonlar içindir. Her kareyi en az minimum süre boyunca tutun. Maksimum sürede, şarkı sözleri aynı kalsa bile sanatçı satırını ilerletin. + Minimum değer 0 veya daha büyük olmalıdır. Maksimum değer 0\'dan büyük ve en az minimum değer kadar olmalıdır. + Satır uzunluğu sınırları + Şarkı sözü ve sanatçı satırı sınırları ile tam genişlikteki karakter ağırlığı + "Şarkı sözü satırı sınırı +Uzun şarkı sözlerini dengeli iki yarıya bölün. 0, bölmeyi devre dışı bırakır." + "Sanatçı satır sınırı +Doldurulmuş parça formatını döngüsel karelere ayırın. 0, ayırmayı devre dışı bırakır." + "Tam genişlikteki karakter ağırlığı +Her iki sınır için de geçerlidir. 1,75 değerinde, dört tam genişlikteki karakter 7 olarak sayılır." + 0 ile 10000 arasında tam sayı sınırları ve 0\'dan büyük bir karakter ağırlığı girin + Enstrümantal ara + Enstrümantal bir ara sırasında Mini oynatıcıda ve MediaSession\'da gösterilir. Her satır bir animasyon karesidir. {t} başlık, {a} sanatçı ve {lrc} son şarkı sözü satırıdır + Mini oynatıcı ve MediaSession zamanlama ofseti + Mini oynatıcı ve MediaSession\'da sonraki satırın ne zaman gösterileceğini kaydırır. Satırlar geç görünüyorsa, örneğin kilit ekranında, negatif bir değer kullanın Filtreler Şarkı sözü filtresi normal ifadesi Düzenli ifade; şarkı sözleri aranmadan önce ve iki başlığın eşleşip eşleşmediği değerlendirilirken başlıklara, sanatçılara ve albümlere uygulanır. Her ikisini de devre dışı bırakmak için boş bırakın. Varsayılan ayar, \"Official Video\", \"Remaster\", \"【插曲】\" ve \"- Topic\" gibi yaygın gereksiz ifadeleri kaldırır. @@ -564,6 +585,10 @@ Gerekli çerezler: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPISI Düzenli ifade yalnızca orijinal şarkı sözü metnine uygulanır (çevirilere veya romanizasyonlara değil). Devre dışı bırakmak için boş bırakın Bilgi satırı belirteçleri Şarkı sözleri yanlışlıkla en alta filtreleniyorsa bu ayarı düzenleyin veya temizleyin. Virgülle ayrılmış belirteçler: Bir belirteç ve ardından iki nokta üst üste içeren satırlar, en altta künye bilgisi olarak gösterilir. Devre dışı bırakmak için boş bırakın + İlk harfi büyük yap + Her şarkı sözü satırının ilk harfini büyük yapar + Sondaki noktalama işaretlerini kaldır + Şarkı sözü satırlarının sonundaki noktaları, virgülleri, iki noktaları ve noktalı virgülleri kaldırır. Soru işaretleri, ünlem işaretleri, üç noktalar, tırnaklar ve parantezler korunur Yazı tipi boyutu Şarkı sözü metninin boyutu Genel zamanlama farkı diff --git a/patches/src/main/resources/addresources/values-tr-rTR/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-tr-rTR/shared-youtube/strings.xml index c9dd069b85..5e4eafbb1d 100644 --- a/patches/src/main/resources/addresources/values-tr-rTR/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-tr-rTR/shared-youtube/strings.xml @@ -150,13 +150,13 @@ Oturum açmak için, devam et butonuna basın." Medya bildirimi, kilit ekranı ve kulaklık kontrollerindeki Önceki ve Sonraki butonlarını gizler İlerleme çubuğunu devre dışı bırak Bildirim panelindeki ve kilit ekranındaki ilerleme çubuğunu sürüklemeyi engeller - Önceki & Sonraki düğmeleriyle arama - Medybildirimi, kilit ekranı ve kulaklık için medya kontrolünü videoları değiştirmek yerine arama yapmak üzere değiştirir + Önceki & Sonraki düğmeleriyle sardırma + Medya bildirimi, kilit ekranı ve kulaklık kontrollerinin videoları değiştirmek yerine sardırma yapmasını sağlar Geri sarma süresi Önceki düğmesiyle geri sarılacak saniye sayısı İleri sarma süresi Sonraki düğmesiyle ileri sarılacak saniye sayısı - Arama süresi 1 ile %s saniye arasında olmalıdır. Varsayılan kullanılıyor + Sardırma süresi 1 ile %s saniye arasında olmalıdır. Varsayılan kullanılıyor QUIC protokolünü devre dışı bırak diff --git a/patches/src/main/resources/addresources/values-tr-rTR/youtube/strings.xml b/patches/src/main/resources/addresources/values-tr-rTR/youtube/strings.xml index 44f8acd5a4..3e6fdd3e98 100644 --- a/patches/src/main/resources/addresources/values-tr-rTR/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-tr-rTR/youtube/strings.xml @@ -394,6 +394,9 @@ Raporlar herkese açıktır ve bir kanal listeye eklenmeden önce incelenir. Kal Alışveriş bağlantılarını gizle Video reklamlarını gizle YouTube Premium promosyonlarını gizle + + + Yükleniyor... Şu anki kanal: Beyaz listeye ekle @@ -410,6 +413,9 @@ Raporlar herkese açıktır ve bir kanal listeye eklenmeden önce incelenir. Kal Kanal arama sonuçları Bu kanalda sonuç bulunamadı Bu kanalda arama yapılamadı + Videolar + Gönderiler + Bu kanalda eşleşen gönderi bulunamadı Alaka En çok izlenen En az izlenen @@ -495,6 +501,9 @@ Soru düğmesine sahip olan bir video bir kez açıldıktan sonra bu özellik, m Video Ses Tüm seçenekler + Shorts menüsünde indiriciyi göster + Shorts oynatıcısına harici indirici açılır menü öğesi ekler + İndir Seslendirme çevirisi Cihaz üzerindeki metin-konuşma özelliğini kullanarak video altyazılarını oynatmayla senkronize bir şekilde yüksek sesle okuyun. @@ -1122,9 +1131,6 @@ Otomotiv düzeni "Eski oynatıcı düzenini geri yükler Sınırlamalar: 'Eski video eylem çubuğunu geri yükle' kapatılırsa, 'Daha fazla video' katmanı kullanılamayabilir" - "Bunu etkinleştirilmek, oynatıcının altında gösterilen yorumlar veya 'Daha fazla video' kısımları gibi bazı oynatıcı düzen özelliklerini bozabilir. - -Tablet düzeni artık YouTube tarafından desteklenmiyor ve güncel YouTube sürümlerinde kısmen veya tamamen bozulmuş durumda. Çalışan bir tablet düzeni için, Youtube 20.21.37'yi yamalayın" 21.11.48 - Eski altyazı davranışını geri yükle 20.39.41 - Oynatma listesine kaydetme için eski düzeni geri getir @@ -1628,4 +1634,17 @@ Otomatik kayıt özelliği isteğe bağlıdır; gizli modda veya hesap değişti Dizi bulunamadı İsimsiz bölüm Mevcut olmayan bölüm + + Niconico yorumları + Canlı sohbeti ve sohbet tekrarını, videonun üzerinden kayan yorumlar olarak göster + Video oynatıcıda bir ekran üstü düğmesi gösterir. Yorumları göstermek veya gizlemek için dokunun, hızlı ayarlar için basılı tutun + Yorum yazı tipi boyutu + Yorumların metin boyutu (12-40 sp) + Yorum opaklığı + Yorum metninin ve arka planlarının opaklığı + Yolculuk süresi + Yorumların ekranı geçmesi için geçen saniye sayısı. Düşük değerler daha hızlı hareket eder + Maksimum yorum uzunluğu + Bunu aşan karakter sayısına sahip yorumları gizler. Her emoji bir karakter olarak sayılır + Sınırsız diff --git a/patches/src/main/resources/addresources/values-uk-rUA/music/strings.xml b/patches/src/main/resources/addresources/values-uk-rUA/music/strings.xml index 6159b74bbd..f8a1cdc662 100644 --- a/patches/src/main/resources/addresources/values-uk-rUA/music/strings.xml +++ b/patches/src/main/resources/addresources/values-uk-rUA/music/strings.xml @@ -555,8 +555,29 @@ Second \"item\" text" Показує поточний рядок на екрані блокування, в Android Auto та через Bluetooth. Потрібен Android 11 або новіший Показувати текст пісні в мініплеєрі Показує поточний рядок у мініплеєрі, а трек переноситься в підзаголовок - Порядок відображення виконавця та назви - Перемикнути порядок показу виконавця та назви в мініплеєрі та MediaSession. Увімкнено - \"Виконавець - Назва\", вимкнено - \"Назва - Виконавець\". Не впливає на пошук текстів пісень + Формат назви пісні/виконавця + Формат треку, що показується разом із текстом пісні в міні-плеєрі та MediaSession. {t} це назва, {a} виконавець, а {v} версія, наприклад Acoustic, якщо джерело тексту її надає. Текст між двома двокрапками показується лише тоді, коли кожна змінна всередині має значення; для звичайної двокрапки пишіть \\:. Кожен рядок показується по черзі, змінюючись щоразу, коли змінюється рядок тексту + Формат рядка тексту + Формат рядка тексту пісні в мініплеєрі та MediaSession. {lrc} це рядок, а {n} скільки разів поспіль він співається. Текст між двокрапками показується лише тоді, коли рядок повторюється. Для двокрапки використовуйте \\: + Формат має містити {lrc} + Шаблон рядка виконавця та часові параметри анімації + Мінімальний час на екрані (мс) + Максимальний час на екрані (мс) + Лише для багаторядкових анімацій. Кожен кадр показується щонайменше мінімальний час. При досягненні максимуму рядок виконавця змінюється, навіть якщо рядок тексту залишається тим самим. + Мінімум має бути 0 або більше. Максимум має бути більшим за 0 і не меншим за мінімум. + Обмеження довжини рядка + Обмеження рядків тексту та виконавця й вага повноширинних символів + "Обмеження рядка тексту +Розділяти довші рядки тексту на рівні половини. 0 вимикає розділення." + "Обмеження рядка виконавця +Розділяти заповнений формат треку на кадри, що циклічно змінюються. 0 вимикає розділення." + "Вага повноширинного символу +Застосовується до обох лімітів. При 1.75 чотири повноширинні символи враховуються як 7." + Введіть цілі числа лімітів від 0 до 10000 і вагу символу більшу за 0 + Інструментальна перерва + Показується в мініплеєрі та MediaSession під час інструментальної перерви. Кожен рядок це один кадр анімації. {t} це назва, {a} виконавець, а {lrc} останній рядок тексту + Зсув синхронізації мініплеєра та MediaSession + Зсуває момент показу наступного рядка в мініплеєрі та MediaSession. Використовуйте від\'ємне значення, якщо рядки з\'являються із запізненням, наприклад на екрані блокування Фільтри Регулярний вираз для метаданих Регулярний вираз, застосований до назв, виконавців та альбомів перед пошуком тексту пісні та при оцінці відповідності двох назв. Залиште порожнім, щоб вимкнути обидва. Типове значення видаляє типове сміття, наприклад \"Official Video\", \"Remaster\", \"【插曲】\" та \"- Topic\" @@ -564,6 +585,10 @@ Second \"item\" text" Регулярний вираз, що застосовується лише до оригінального тексту пісні (не до перекладів чи романізацій). Залиште порожнім, щоб вимкнути Маркери інформаційного рядка Якщо тексти пісень неправильно відфільтровано вниз, налаштуйте або очистіть це. Маркери, розділені комами: рядки, що містять маркер, за яким іде двокрапка, показуються як інформація про авторів унизу. Залиште порожнім, щоб вимкнути + Починати з великої літери + Робить першу літеру кожного рядка тексту пісні великою + Прибирати кінцеву пунктуацію + Прибирає крапки, коми, двокрапки та крапки з комою в кінці рядків тексту пісні. Знаки питання, знаки оклику, трикрапки, лапки та дужки залишаються Розмір тексту Розмір тексту пісні Глобальний зсув часу diff --git a/patches/src/main/resources/addresources/values-uk-rUA/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-uk-rUA/shared-youtube/strings.xml index 0ac65e1bd2..99181f9b05 100644 --- a/patches/src/main/resources/addresources/values-uk-rUA/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-uk-rUA/shared-youtube/strings.xml @@ -147,17 +147,17 @@ Second \"item\" text" Керування медіа в сповіщеннях Налаштуйте елементи керування відтворенням на екрані блокування та панелі сповіщень - Приховати кнопки Попереднє та Наступне + Приховати кнопки \"Попереднє\" та \"Наступне\" Приховує кнопки \"Попередній\" та \"Наступний\" у сповіщеннях медіа, на екрані блокування та в елементах керування навушниками Вимкнути смугу перемотування Запобігає перетягуванню смуги перемотування на екрані блокування та панелі сповіщень - Шукати за допомогою кнопок «Назад» та «Вперед» - Змінює керування сповіщенням про медіа, екраном блокування та навушниками для пошуку замість перемикання відео - Довжина перемотки назад - Кількість секунд для перемотки назад кнопкою Попереднє - Довжина перемотки вперед - Кількість секунд для перемотки вперед кнопкою Наступне - Довжина пошуку має бути від 1 до %s секунд. Використано типові значення + Перемотування кнопками \"Попереднє\" та \"Наступне\" + Змінює елементи керування в медіасповіщенні, на екрані блокування та в навушниках так, щоб вони перемотували замість перемикання відео + Тривалість перемотування назад + Кількість секунд для перемотування назад кнопкою \"Попереднє\" + Тривалість перемотування вперед + Кількість секунд для перемотування вперед кнопкою \"Наступне\" + Тривалість перемотування має бути від 1 до %s секунд. Використовується типове значення Вимкнути протокол QUIC diff --git a/patches/src/main/resources/addresources/values-uk-rUA/youtube/strings.xml b/patches/src/main/resources/addresources/values-uk-rUA/youtube/strings.xml index 9e3b1c0608..8a788894fb 100644 --- a/patches/src/main/resources/addresources/values-uk-rUA/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-uk-rUA/youtube/strings.xml @@ -393,6 +393,9 @@ Second \"item\" text" Приховати посилання на покупки Приховати відеорекламу Приховати рекламу YouTube Premium + + + Завантаження… Поточний канал: Додати до білого списку @@ -409,6 +412,9 @@ Second \"item\" text" Результати пошуку з каналу Нічого не знайдено на цьому каналі Не вдалося виконати пошук на цьому каналі + Відео + Дописи + Жоден допис на цьому каналі не відповідає запиту Актуальність Найпопулярніші Найменш популярні @@ -436,14 +442,16 @@ Second \"item\" text" Показувати кнопку \"Дивитися пізніше\" на мініатюрах Кнопка \"Дивитися пізніше\" показується на мініатюрах відео в стрічці. Натискання та утримування її відкриває \"Зберегти в список відтворення\" - Завантажити зображення допису - Поруч із кнопкою «Поділитися» в допистах із зображеннями або GIF відображається кнопка завантаження, а в їхнє меню додається пункт завантаження + Завантажити зображення з допису + Кнопка завантаження показується поруч із кнопкою \"Поділитися\" для дописів із зображеннями або GIF, а до їхнього меню додається пункт завантаження Завантажити зображення Завантажити зображення + Завантаження зображень: %s… Завантаження зображення… + Зображень збережено: %1$s у %2$s Зображення збережено у %s Не вдалося завантажити зображення - У цьому дописі не знайдено зображень + У цьому дописі не знайдено зображення Додати пункт \"Запитати\" у спливне меню Додає пункт підсумовування відео до спливного меню стрічки @@ -482,16 +490,19 @@ Second \"item\" text" Завантажувати відео, подкасти або пісні за допомогою сторонніх застосунків для завантаження Показувати кнопку завантаження в оверлеї плеєра Показує кнопку у відеоплеєрі, яка відкриває ваш зовнішній завантажувач - Швидке завантаження одним натисканням - Запити на завантаження у фоновому режимі. Натисніть і утримуйте, щоб відкрити завантажувач. Працює з YTDLnis та YTindir - Позначити завантаження як переглянуті - Відео, завантажені за допомогою зовнішнього завантажувача, додаються до історії переглядів YouTube + Швидке завантаження натисканням + Пропонує завантажити у фоновому режимі. Натисніть і утримуйте, щоб відкрити завантажувач. Працює з YTDLnis і YTindir + Позначати завантаження як переглянуті + Відео, завантажені зовнішнім завантажувачем, додаються до вашої історії переглядів YouTube Full HD (1080p) - Середнє (192 кбіт/с) + Середня (192 кбіт/с) Швидке завантаження Відео Аудіо Усі параметри + Показати завантаження в меню Shorts + Додає пункт меню зовнішнього завантажувача у спливаюче вікно плеєра Shorts + Завантажити Голосовий переклад Зачитує субтитри відео вголос за допомогою локального синтезу мовлення, синхронізованого з відтворенням @@ -608,9 +619,9 @@ Second \"item\" text" Поміняти місцями кнопки \"Зберегти\" та \"Додати в чергу\" Натисніть для параметрів черги, утримуйте, щоб зберегти до \"Переглянути пізніше\" - Тип черги \'Відтворити наступним\' - Типові - Системна черга + Тип черги \"Відтворити наступним\" + Типова + Рідна черга Черга Morphe Клієнтська черга Morphe Додати пункт меню черги у висувне меню стрічки @@ -661,16 +672,16 @@ Second \"item\" text" Зберегти чергу як список відтворення Список відтворення збережено - Збережено в %s + Збережено до %s Очистити чергу Чергу очищено Черга Відтворити останнім у черзі Відтворити наступним у черзі - YouTube не може відобразити чергу. - Черга не готова для цього відео. Відкрийте плеєр відео та спробуйте знову. - Додати Мікс до черги чи переглянути Мікс? - Переглянути + YouTube не зміг показати чергу + Черга не готова для цього відео. Відкрийте відеоплеєр і спробуйте знову + Додати Мікс до черги чи дивитися Мікс? + Дивитися Додати до черги Завантаження Міксу… @@ -891,7 +902,7 @@ Second \"item\" text" Напівпрозорий Прозорий Непрозорий - Цей стиль може не підтримуватися вашим пристроєм або версією YouTube, що може призвести до проблем із макетом або збоїв + Цей стиль може не підтримуватися вашим пристроєм або версією YouTube і може спричинити проблеми з макетом або збої Панель інструментів Виберіть, які компоненти будуть показуватися на панелі інструментів Приховати кнопку \"Трансляція\" @@ -1118,9 +1129,6 @@ Second \"item\" text" "Відновлює старий макет плеєра Обмеження: якщо \"Відновити стару панель дій відео\" вимкнено, накладка \"Інші відео\" може бути недоступна" - "Увімкнення цієї функції може порушити деякі елементи макета плеєра, такі як розділ коментарів або «Більше відео», що відображаються під плеєром. - -Макет для планшетів більше не підтримується YouTube і частково або повністю не працює в останніх версіях YouTube. Для коректної роботи макета для планшетів встановіть патч для YouTube 20.21.37" 21.11.48 - Відновити стару поведінку субтитрів 20.39.41 - Відновити старе збереження до списку відтворення @@ -1389,9 +1397,9 @@ Second \"item\" text" Типова швидкість відтворення Типова швидкість відтворення в Shorts Запам\'ятовувати швидкість відтворення в Shorts - Застосовує ваш ручний вибір швидкості Shorts до всіх наступних Shorts. Якщо вимкнено, швидкість повертається до стандартної після закриття плеєра Shorts - Показувати швидкість відтворення в меню Shorts - Додає пункт «Швидкість відтворення» до меню плеєра Shorts + Застосовує вибрану вами вручну швидкість Shorts до всіх наступних Shorts. Якщо вимкнено, швидкість повертається до типової після закриття плеєра Shorts + Показувати \"Швидкість відтворення\" в меню Shorts + Додає пункт \"Швидкість відтворення\" до меню плеєра Shorts Швидкість відтворення · %s Типову швидкість відтворення змінено на %s Використовувати звичайну швидкість для музики @@ -1623,4 +1631,17 @@ AVC (H.264) Серіалів не знайдено Епізод без назви Недоступний епізод + + Коментарі Niconico + Показувати живий чат і повтор чату як коментарі, що прокручуються по відео + Показує кнопку у відеоплеєрі. Натисніть, щоб показати або приховати коментарі, утримуйте для швидких налаштувань + Розмір шрифту коментарів + Розмір тексту коментарів (12-40 sp) + Прозорість коментарів + Прозорість тексту коментарів та їхніх фонів + Тривалість проходження + За скільки секунд коментарі перетинають екран. Менші значення означають швидший рух + Максимальна довжина коментаря + Приховує коментарі з більшою кількістю символів, ніж це значення. Кожне емодзі враховується як один символ + Без обмежень diff --git a/patches/src/main/resources/addresources/values-ur-rIN/youtube/strings.xml b/patches/src/main/resources/addresources/values-ur-rIN/youtube/strings.xml index 3bd9dd0ecc..1ec5f0786e 100644 --- a/patches/src/main/resources/addresources/values-ur-rIN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ur-rIN/youtube/strings.xml @@ -51,6 +51,8 @@ Second \"item\" text" + + @@ -227,4 +229,5 @@ Second \"item\" text" محفوظ کریں تا تاریخ जारी रखें + diff --git a/patches/src/main/resources/addresources/values-uz-rUZ/youtube/strings.xml b/patches/src/main/resources/addresources/values-uz-rUZ/youtube/strings.xml index 31740c4cab..478fddfe2b 100644 --- a/patches/src/main/resources/addresources/values-uz-rUZ/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-uz-rUZ/youtube/strings.xml @@ -312,6 +312,8 @@ Cheklovlar: Savdo havolalarini yashirish Videodagi reklamalarni yashirish YouTube Premium reklamasini yashirish + + @@ -1108,4 +1110,5 @@ Cheklov: Agar 'Eski video harakatlar panelini tiklash' o'chirilgan bo'lsa, 'Ko'p Standart Maxsus + diff --git a/patches/src/main/resources/addresources/values-vi-rVN/music/strings.xml b/patches/src/main/resources/addresources/values-vi-rVN/music/strings.xml index db1a1642dc..c685ad246f 100644 --- a/patches/src/main/resources/addresources/values-vi-rVN/music/strings.xml +++ b/patches/src/main/resources/addresources/values-vi-rVN/music/strings.xml @@ -546,8 +546,29 @@ Các bước: Hiển thị dòng hiện tại trên màn hình khóa, Android Auto và Bluetooth. Yêu cầu Android 11 trở lên Hiển thị Lời bài hát trong Miniplayer Hiển thị dòng hiện tại trong trình phát nhỏ, với tiêu đề bài hát được chuyển xuống phần phụ đề - Thứ tự hiển thị Nghệ sĩ và Tên bài hát - Chuyển đổi thứ tự hiển thị của Nghệ sĩ và Tên bài hát trong Miniplayer và MediaSession. BẬT \"Nghệ sĩ - Tên bài hát\" và TẮT \"Tên bài hát - Nghệ sĩ\" Không ảnh hưởng đến tìm kiếm lời bài hát + Định dạng tiêu đề bài hát/nghệ sĩ + Định dạng của bản nhạc được hiển thị cùng với lời bài hát trong miniplayer và MediaSession. {t} là tiêu đề, {a} là nghệ sĩ và {v} là phiên bản, chẳng hạn như Acoustic, khi nguồn lời bài hát cung cấp. Văn bản giữa hai dấu hai chấm chỉ được hiển thị khi mọi biến bên trong nó có giá trị; viết \\: cho dấu hai chấm thông thường. Mỗi dòng được hiển thị lần lượt, chuyển tiếp bất cứ khi nào lời bài hát thay đổi + Định dạng dòng lời bài hát + Định dạng của dòng lời bài hát trong miniplayer và MediaSession. {lrc} là dòng lời bài hát và {n} là số lần dòng đó được hát liên tiếp. Văn bản giữa các dấu hai chấm chỉ được hiển thị khi dòng lời bài hát lặp lại. Sử dụng \\: cho dấu hai chấm + Định dạng phải bao gồm {lrc} + Mẫu dòng nghệ sĩ và thời gian hoạt ảnh + Thời gian hiển thị tối thiểu (ms) + Thời gian hiển thị tối đa (ms) + Chỉ dành cho hoạt ảnh nhiều dòng. Giữ mỗi khung hình trong ít nhất thời gian tối thiểu. Ở mức tối đa, chuyển dòng nghệ sĩ ngay cả khi lời bài hát không thay đổi. + Giá trị tối thiểu phải từ 0 trở lên. Giá trị tối đa phải lớn hơn 0 và ít nhất bằng giá trị tối thiểu. + Giới hạn độ dài dòng + Giới hạn dòng lời bài hát và nghệ sĩ, và trọng số ký tự toàn chiều rộng + "Giới hạn dòng lời bài hát +Chia lời bài hát dài thành hai nửa cân đối. 0 để tắt tính năng chia." + "Giới hạn dòng của nghệ sĩ +Chia định dạng bản nhạc đã điền thành các khung hình xoay vòng. 0 để tắt chia." + "Trọng số ký tự toàn chiều rộng +Áp dụng cho cả hai giới hạn. Tại mức 1.75, bốn ký tự toàn chiều rộng sẽ được tính là 7." + Nhập giới hạn là số nguyên từ 0 đến 10000 và trọng số ký tự lớn hơn 0 + Đoạn nhạc dạo + Hiển thị trong miniplayer và MediaSession trong một đoạn nhạc dạo. Mỗi dòng là một khung hình hoạt ảnh. {t} là tiêu đề, {a} là nghệ sĩ và {lrc} là dòng lời bài hát cuối cùng + Độ lệch thời gian của miniplayer và MediaSession + Điều chỉnh thời điểm hiển thị dòng tiếp theo trong Trình phát thu nhỏ và MediaSession. Sử dụng giá trị âm nếu các dòng hiển thị muộn, ví dụ như trên màn hình khóa Bộ lọc Regex siêu dữ liệu Biểu thức chính quy được áp dụng cho tiêu đề, nghệ sĩ và album trước khi tra cứu lời bài hát và khi đánh giá xem hai tiêu đề có khớp nhau không. Để trống để tắt cả hai. Mặc định loại bỏ những phần thừa phổ biến như \"Official Video\", \"Remaster\", \"【插曲】\" và \"- Topic\". @@ -555,6 +576,10 @@ Các bước: Biểu thức chính quy chỉ áp dụng cho văn bản lời bài hát gốc (không phải bản dịch hoặc chuyển tự). Để trống để vô hiệu hóa Đánh dấu dòng thông tin Nếu lời bài hát bị lọc sai xuống cuối, hãy điều chỉnh hoặc xóa mục này. Các dấu hiệu phân tách bằng dấu phẩy: các dòng chứa dấu hiệu theo sau bởi dấu hai chấm sẽ được hiển thị dưới dạng thông tin ghi công ở cuối. Để trống để vô hiệu hóa + Viết hoa chữ cái đầu + Viết hoa chữ cái đầu của mỗi dòng lời bài hát + Xóa dấu câu ở cuối + Xóa dấu chấm, dấu phẩy, dấu hai chấm và dấu chấm phẩy ở cuối các dòng lời bài hát. Dấu hỏi, dấu chấm than, dấu ba chấm, dấu ngoặc kép và dấu ngoặc đơn được giữ nguyên Cỡ chữ Kích thước chữ của lời bài hát Độ lệch thời gian toàn cục diff --git a/patches/src/main/resources/addresources/values-vi-rVN/youtube/strings.xml b/patches/src/main/resources/addresources/values-vi-rVN/youtube/strings.xml index f3095ca641..3f16b0bfd2 100644 --- a/patches/src/main/resources/addresources/values-vi-rVN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-vi-rVN/youtube/strings.xml @@ -393,6 +393,9 @@ Báo cáo được công khai và được xem xét trước khi một kênh đ Ẩn liên kết mua sắm Ẩn video quảng cáo Ẩn quảng cáo khuyến mãi YouTube Premium + + + Đang tải… Kênh hiện tại: Thêm vào danh sách ngoại lệ @@ -409,6 +412,9 @@ Báo cáo được công khai và được xem xét trước khi một kênh đ Kết quả tìm kiếm kênh Không tìm thấy gì trên kênh này Không thể tìm kiếm trên kênh này + Video + Bài viết + Không có bài viết nào trên kênh này khớp Mức độ liên quan Xem nhiều nhất Xem ít nhất @@ -494,6 +500,9 @@ Sau khi mở một video có nút Ask một lần, tính năng này hoạt độ Video Âm thanh Tất cả tùy chọn + Hiển thị tải xuống trong menu Shorts + Thêm mục menu bay ngoài trình tải xuống bên thứ ba vào trình phát Shorts + Tải về Dịch giọng nói Đọc to phụ đề video bằng tính năng chuyển văn bản thành giọng nói trên thiết bị, đồng bộ với video @@ -1121,9 +1130,6 @@ Bố cục ô tô "Khôi phục bố cục trình phát cũ Hạn chế: Nếu tùy chọn \"Khôi phục thanh thao tác video cũ\" bị tắt, lớp phủ \"Thêm video\" có thể không xuất hiện" - "Việc bật tính năng này có thể làm hỏng một số tính năng bố cục của trình phát, chẳng hạn như phần bình luận hoặc 'Video khác' hiển thị bên dưới trình phát. - -Bố cục máy tính bảng không còn được YouTube hỗ trợ và bị hỏng một phần hoặc toàn bộ trên các phiên bản YouTube gần đây. Để có bố cục máy tính bảng hoạt động, hãy vá YouTube 20.21.37" 21.11.48 - Khôi phục hành vi phụ đề cũ 20.39.41 - Khôi phục ‘Lưu vào danh sách phát’ kiểu cũ @@ -1626,4 +1632,17 @@ Tự động ghi là tùy chọn. Nó dừng lại khi ở chế độ ẩn danh Không tìm thấy loạt tập nào Tập không tiêu đề Tập không khả dụng + + Bình luận Niconico + Hiển thị trò chuyện trực tiếp và phát lại trò chuyện dưới dạng các bình luận chạy ngang qua video + Hiển thị một nút lớp phủ trong trình phát video. Nhấn để hiện hoặc ẩn bình luận, nhấn và giữ để mở cài đặt nhanh + Kích thước phông chữ bình luận + Kích thước văn bản của các bình luận (12-40 sp) + Độ mờ bình luận + Độ mờ của văn bản và nền bình luận + Thời gian di chuyển + Số giây mà các bình luận cần để đi hết màn hình. Giá trị càng thấp thì di chuyển càng nhanh + Độ dài bình luận tối đa + Ẩn các bình luận có nhiều ký tự hơn giá trị này. Mỗi emoji được tính là một ký tự + Không giới hạn diff --git a/patches/src/main/resources/addresources/values-zh-rCN/music/strings.xml b/patches/src/main/resources/addresources/values-zh-rCN/music/strings.xml index 6748b2e23a..9e7a97e908 100644 --- a/patches/src/main/resources/addresources/values-zh-rCN/music/strings.xml +++ b/patches/src/main/resources/addresources/values-zh-rCN/music/strings.xml @@ -555,8 +555,29 @@ SABR 播放(模拟视频流已关闭)" 在锁屏界面、Android Auto 和蓝牙中显示当前行。需要 Android 11 或更高版本 显示迷你播放器歌词 在小播放器中显示当前行,并将曲目移至字幕 - 艺术家和标题显示顺序 - 在迷你播放器和媒体通知卡片中切换艺术家和标题的显示顺序。ON“艺术家-标题”和OFF“标题-艺术家”不影响歌词搜索 + 歌曲标题/艺术家格式 + 在迷你播放器和 MediaSession 中与歌词一起显示的曲目格式。{t} 是标题,{a} 是艺术家,{v} 是版本(例如 Acoustic),当歌词源提供时显示。两个冒号之间的文本仅在其中所有变量都有值时显示;若要显示普通冒号,请写入 \\:。每行依次显示,并在歌词更改时切换 + 歌词行格式 + 在迷你播放器和 MediaSession 中的歌词行格式。{lrc} 是该行内容,{n} 是连续演唱的次数。冒号之间的文本仅在行重复时显示。使用 \\: 表示冒号 + 格式必须包含 {lrc} + 艺术家行模板和动画时间 + 最短屏幕显示时间 (ms) + 最长屏幕显示时间 (ms) + 仅适用于多行动画。每帧至少保留最短时间。达到最长时间时,即使歌词保持不变,也会推进艺术家行。 + 最小值必须为 0 或更大。最大值必须大于 0 且至少等于最小值。 + 行长度限制 + 歌词和艺术家行限制,以及全宽字符权重 + "歌词行限制 +将较长的歌词分成均衡的两半。0 为禁用拆分。" + "艺术家行限制 +将填充的轨道格式拆分为循环帧。0 表示禁用拆分。" + "全角字符权重 +适用于两个限制。当权重为 1.75 时,四个全角字符计为 7。" + 请输入 0 到 10000 之间的整数限制,以及大于 0 的字符权重 + 器乐间奏 + 在器乐间奏期间显示在迷你播放器和 MediaSession 中。每一行是一个动画帧。{t} 是标题,{a} 是艺术家,{lrc} 是最后一行歌词 + 迷你播放器和 MediaSession 时间偏移 + 偏移迷你播放器和 MediaSession 中显示下一行歌词的时间。如果歌词显示延迟,例如在锁屏界面上,请使用负值 过滤规则 元数据正则表达式 用于标题、艺术家和专辑的正则表达式,在查找歌词前以及为两个标题是否匹配评分时使用。留空可同时禁用两者。默认值会移除常见干扰项,如\"Official Video\"、\"Remaster\"、\"【插曲】\"和\"- Topic\"。 @@ -564,6 +585,10 @@ SABR 播放(模拟视频流已关闭)" 仅应用于原始歌词文本的正则表达式(不应用于翻译或音译)。留空以禁用 信息行标记 如果歌词被错误地过滤到底部,请调整或清除此项。以逗号分隔的标记:包含标记且后跟冒号的行会作为署名信息显示在底部。留空以禁用 + 首字母大写 + 将每行歌词的首字母大写 + 移除末尾标点 + 移除歌词行末尾的句号、逗号、冒号和分号。保留问号、感叹号、省略号、引号和括号 文本大小 歌词文本的大小 全局时间偏移 diff --git a/patches/src/main/resources/addresources/values-zh-rCN/youtube/strings.xml b/patches/src/main/resources/addresources/values-zh-rCN/youtube/strings.xml index 375a322c29..75e2e3d00a 100644 --- a/patches/src/main/resources/addresources/values-zh-rCN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-zh-rCN/youtube/strings.xml @@ -393,6 +393,9 @@ Second \"item\" text" 隐藏购物链接 隐藏视频广告 隐藏 YouTube Premium 促销推广 + + + 正在加载… 当前频道: 添加到白名单 @@ -409,6 +412,9 @@ Second \"item\" text" 频道搜索结果 此频道中未找到任何内容 无法搜索此频道 + 视频 + 帖子 + 此频道中没有匹配的帖子 相关性 最多观看 最少观看 @@ -494,6 +500,9 @@ Second \"item\" text" 视频 音频 所有选项 + 在 Shorts 菜单中显示下载 + 在 Shorts 播放器中添加外部下载器弹出菜单项 + 下载 语音旁白翻译 使用设备上的文本转语音功能朗读视频字幕,并与播放同步 @@ -1121,9 +1130,6 @@ Second \"item\" text" "恢复旧版播放器布局 限制:如果关闭“恢复旧版视频操作栏”选项,则“更多视频”叠加层可能不可用" - "启用此功能可能会破坏某些播放器布局特性,例如播放器下方显示的评论区或“更多视频”。 - -YouTube 不再支持平板电脑布局,且在较新版本的 YouTube 中该布局已部分或完全损坏。如需可用的平板电脑布局,请将 YouTube 补丁至 20.21.37 版本" 21.11.48 - 恢复旧版字幕行为 20.39.41 - 恢复旧的保存到播放列表 @@ -1626,4 +1632,17 @@ AVC (H.264) 未找到剧集 无名剧集 不可用剧集 + + Niconico 弹幕 + 将实时聊天和聊天回放以在视频上滚动弹幕的形式显示 + 在视频播放器中显示一个叠加按钮。点击以显示或隐藏弹幕,长按进入快速设置 + 弹幕字体大小 + 弹幕的文本大小 (12-40 sp) + 弹幕不透明度 + 弹幕文本和背景的不透明度 + 滚动时长 + 评论横跨屏幕所需的时间(秒)。值越低移动速度越快 + 最大评论长度 + 隐藏字符数超过此限制的评论。每个表情符号计为一个字符 + 无限制 diff --git a/patches/src/main/resources/addresources/values-zh-rTW/music/strings.xml b/patches/src/main/resources/addresources/values-zh-rTW/music/strings.xml index bb6a017895..43077c0a50 100644 --- a/patches/src/main/resources/addresources/values-zh-rTW/music/strings.xml +++ b/patches/src/main/resources/addresources/values-zh-rTW/music/strings.xml @@ -555,8 +555,29 @@ SABR 播放(影片串流模擬已關閉)" 在鎖定螢幕、Android Auto 和藍牙上顯示當前行。需要 Android 11 或更新版本 顯示迷你播放器的歌詞 在小播放器中顯示當前行,並將歌曲移動至字幕位置 - 藝人和標題的顯示順序 - 切換迷你播放器和媒體工作階段中藝人和標題的顯示順序。 ON「藝人 - 標題」和 OFF「標題 - 藝人」不影響歌詞搜索 + 歌曲標題/藝人格式 + 在迷你播放器和 MediaSession 中與歌詞一同顯示的曲目格式。{t} 為標題,{a} 為藝人,{v} 為版本,例如 Acoustic,若歌詞來源有提供時。兩個冒號之間的文字僅在其中所有變數均有值時顯示;若要顯示普通冒號,請輸入 \\:。每行將依次顯示,並在歌詞變更時切換 + 歌詞行格式 + 在迷你播放器和 MediaSession 中歌詞行的格式。{lrc} 為該行歌詞,{n} 為連續演唱的次數。冒號之間的文字僅在歌詞重複時顯示。使用 \\: 表示冒號 + 格式必須包含 {lrc} + 藝人行模板與動畫時間 + 最小螢幕顯示時間 (ms) + 最大螢幕顯示時間 (ms) + 僅適用於多行動畫。每幀至少保留最小時間。達到最大時間時,即使歌詞保持不變,也會推進藝人行。 + 最小值必須為 0 或更大。最大值必須大於 0 且至少等於最小值。 + 行長度限制 + 歌詞與藝人行限制,以及全形字元權重 + "歌詞行限制 +將較長的歌詞拆分為平衡的兩半。0 表示停用拆分。" + "歌手行數限制 +將已填充的軌道格式分割為循環幀。0 表示禁用分割。" + "全形字元權重 +適用於兩種限制。若設為 1.75,四個全形字元將計為 7 個。" + 請輸入 0 到 10000 之間的整數限制,以及大於 0 的字元權重 + 器樂間奏 + 在器樂間奏期間顯示在迷你播放器和 MediaSession 中。每一行即為一個動畫幀。{t} 是標題,{a} 是歌手,{lrc} 是最後一行歌詞 + 迷你播放器和 MediaSession 時間偏移 + 調整迷你播放器與 MediaSession 中顯示下一行歌詞的時間。如果歌詞顯示較慢,例如在鎖定螢幕上,請使用負值 篩選器 元資料正規表示式 套用於標題、演出者和專輯的正規表示式,在查詢歌詞之前以及判斷兩個標題是否相符時使用。留空可同時停用兩者。預設會移除常見的干擾內容,例如「Official Video」、「Remaster」、「【插曲】」和「- Topic」。 @@ -564,6 +585,10 @@ SABR 播放(影片串流模擬已關閉)" 正規表示式僅套用於原始歌詞文字(不適用於翻譯或羅馬拼音)。留空以停用 資訊列標記 如果歌詞被錯誤地過濾到底部,請調整或清除它。逗號分隔的標記:包含標記後跟冒號的行在底部顯示為製作團隊資訊。留空以停用 + 首字母大寫 + 將每行歌詞的首字母大寫 + 移除末尾標點符號 + 移除歌詞行末的句號、逗號、冒號和分號。問號、感嘆號、省略號、引號和括號將被保留 文字大小 歌詞文字的大小 全域時間偏移 diff --git a/patches/src/main/resources/addresources/values-zh-rTW/youtube/strings.xml b/patches/src/main/resources/addresources/values-zh-rTW/youtube/strings.xml index 649d6dbe59..c183b1072c 100644 --- a/patches/src/main/resources/addresources/values-zh-rTW/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-zh-rTW/youtube/strings.xml @@ -393,6 +393,9 @@ Second \"item\" text" 隱藏購物連結 隱藏影片廣告 隱藏 YouTube Premium 促銷內容 + + + 正在載入…… 目前頻道: 加入白名單 @@ -409,6 +412,9 @@ Second \"item\" text" 頻道搜尋結果 此頻道找不到任何結果 無法搜尋此頻道 + 影片 + 貼文 + 此頻道中沒有符合條件的貼文 相關性 觀看次數最多 觀看次數最少 @@ -494,6 +500,9 @@ Second \"item\" text" 影片 音訊 所有選項 + 在 Shorts 選單中顯示下載 + 在 Shorts 播放器中新增外部下載器的彈出式選單項目 + 下載 語音轉錄翻譯 使用裝置上的文字轉語音功能朗讀影片字幕,並與播放同步 @@ -1121,9 +1130,6 @@ Second \"item\" text" "還原舊版播放器版面 限制:如果關閉「還原舊版影片動作列」,則「更多影片」疊加層可能無法使用。" - "啟用此功能可能會損壞某些播放器版面配置功能,例如播放器下方顯示的評論區或「更多影片」。 - -YouTube 已不再支援平板電腦版面配置,且在最近的 YouTube 版本中已部分或完全損壞。如需可運行的平板電腦版面配置,請修補 YouTube 20.21.37" 21.11.48 - 恢復舊版字幕行為 20.39.41 - 復原舊版播放清單 @@ -1625,4 +1631,17 @@ AV1 視訊播放可能會卡頓或丟格" 找不到任何系列 未命名的集數 不可用的集數 + + Niconico 評論 + 將即時聊天和聊天回放顯示為在影片中滾動的評論 + 在影片播放器中顯示疊加按鈕。輕觸可顯示或隱藏評論,長按可開啟快速設定 + 評論字體大小 + 評論的文字大小 (12-40 sp) + 評論不透明度 + 評論文字和背景的不透明度 + 移動時長 + 評論橫跨螢幕所需秒數。數值越低,移動速度越快 + 最大評論長度 + 隱藏字數超過此限制的評論。每個表情符號計為一個字元 + 無限制 diff --git a/patches/src/main/resources/addresources/values-zu-rZA/music/strings.xml b/patches/src/main/resources/addresources/values-zu-rZA/music/strings.xml index ccfea4ed71..d1955539c0 100644 --- a/patches/src/main/resources/addresources/values-zu-rZA/music/strings.xml +++ b/patches/src/main/resources/addresources/values-zu-rZA/music/strings.xml @@ -17,58 +17,677 @@ Second \"item\" text" --> + Kudlalwe okugcina + Izisayini + Amavidiyo + Imiphumela eminye + Izingoma ezilandisiwe + Ama-album alandisiwe + Abaculi abalandisiwe + Vuselela i-Jam patch yomphathi ukuze usebenzise ukumisa nokuqhubeka + Ukuphathisana kwe-Jam queue + Hlisa noma ujoyine i-Jam eseduze bese uhlela uhlu lwayo nge-Jam Layer + Vumela ukuphathisana kwe-Jam queue + Kuvumela ukuhlanganiswa kwe-Jam ngemva kokuthi i-YouTube Music iqale kabusha + Landa i-Jam Layer ukuze isebenze + Vula i-Jam Layer release yakamuva kukuma-browser yakho + Izilawuli ze-Jam + Hlisa noma ujoyine i-Jam eseduze bese ulawula iseshini + Iphakheji ye-Jam Layer + Khetha i-app ehambisana nayo efakwe esetshenziselwa ukwabelana ohlelweni lwemigqa eseduze + Sebenzisa iphakheji elihambisana naleli elifakile. Ukuhlanganisa kudinga ukuvuma kwakho nethokheni lamandla endawo + Vula i-Wi-Fi? + Vula i-Wi-Fi, bese uzama ukuqala noma ukuhlanganyela iJam futhi + Vula izilungiselelo ze-Wi-Fi + Khansela + Sebenzisa iphakheji + Faka igama le-package lase-Android elilungile + Iphakheji igcinwe. Lungisa iJam Layer ukuze uxhumane + I-Jam idutshulwe + Vula i-Jam Layer kuqala + Vumela ukwabelana ngomugqa we-Jam kuzilungiselelo, bese uqala kabusha i-YouTube Music + Faka iphaki le-Jam Layer ekhethiwe kuqala + Faka ikhodi ebonakala kumeleli. I-Wi-Fi Aware ingaxhumanisa izamazingiza eziseduze ngisho noma ama-LAN azo ehlukile + Joyina ngekhodi + Joyina + Skena i-QR + Faka ikhodi enezinhlamvu ezisishiyagalombili noma unamathelise isimemo + Joyina i-Jam yakho + Kopisha ikhodi + Ikhodi ye-Jam + Khombisa i-QR + Kuqedile + Mema e-Jam yakho + Sikena ukuze ujoyine futhi uhlelwe lolu hlu. Ukupheza i-Jam kususa isimemo + Kopisha isixhumanisi + Isimemo se-Jam + I-Jam iyaxhumana kabusha; zama futhi maduze + Kwenyeziwe ku-Jam + I-Jam ayitholakali; iculo alizange lengezwe + I-Jam yakho + Lalelana ndawonye + Ukungena ku-Jam + Lalelani ndawonye + Kuvuselelwa iJam… + Kufunwa umeleli wakho ku-Wi-Fi… + Kulindwe abantu + %s umuntu uxhumekile + %s abantu baxhumekile + Faka le khodi kudevayisi eseduze. I-Wi-Fi Aware isebenza ezinhlelweni ze-LAN ezehlukene lapho isekelwa. Iphela ngemizuzu u-%s + Ufuna ukuya ku-%s? + Ukuhlela kuvuliwe + Ukuhlela kuvukelwe + Kuxhunywe nge-Wi-Fi Aware + Kuxhunywe nge-Wi-Fi + Kuxhunywe nge-Bluetooth + Akusekelwa ngesikhathi se-Jam + Ukuhlanganyela kukhanselwe + Kuxhunyaniswa kabusha nomnikazi wakho… + Hlanganyela ulandelelo. Vumela wonke umuntu ukuthi aneze amahubo + Mema abantu + Izivakashi zingashintsha uhlu lwezingoma + Qeda i-Jam + Umphathi udlala umculo. Neza, yisa noma suse amahubo ngokuqondile kulandelelweni lwe-YouTube Music + Gcina amadevayisi omabili eseduze. Ungaqeda ngenkathi sifuna umphathi + Khansela ukujoyina + Hamba e-Jam + Qala i-Jam + lungisa i-Jam Layer + phatha + Imininingwane + Vula + Kulayidwa izinketho zengoma… + Izinketho zengoma azitholakali; zama futhi + Kungekenzeki ukuvula izinketho zengoma + Alikho i-browser elitholakala ukudownloada i-Jam Layer + Kulindwe u-queue womphathi + Vula ingoma ku-YouTube Music kuqala + Dlala kumphathi + Phuma ku-Jam udlale ngokuhlezi + Dlala elilandelayo ku-Jam + Yengeza ku-Jam queue + Dlala le track ye-Jam + Khetha lapho udlalela khona + Yisa umphathi lapha + Iculo alizange lilungele ukufunwa + Uhlu lushintshile; zama futhi isandla + Ulogo lwe-Header + Okuhlelekele + Okwi-custom + Dlala amakhasi omculo e-album + Dlala inguqulo yomsindo kuphela ye-album yesingidizi esikhundleni sevidiyo yomculo. Kudingeka \'Spoof video streams\' + Vula ukushintsha kwe-audio/video + Zama ukushintsha phakathi kwe-Audio Track Version ne-Official Music Video + Landa amavidiyo omculo, ama-podcast, noma amahubo usebenzisa izinhlelo zokulanda zangaphandle + Sebenzisa isilandisi sangaphandle kumenyu ye-Download + Inketho ye-Download emenywini ivula isilandisi sakho sangaphandle esikhundleni salokhu + Okushuthelwa + "Alukho ukulandwa +Amakhasi alandwe azovela lapha" + %1$s • %2$s • %3$s MB + %1$s amakhamu • %2$s MB + Sesha okulandwe + Hlela + Hlela ngokomculi + Hlela ngokwesihloko + Hlela ngosayizi + Susa zonke izinto ezilandwe + Susa zonke izinto ezilandwe? + Zonke izomisebe ezilandwe zizosusa kuleli device + Okulandwe kususiwe + Akukho okutholakele + Izenzo ze %s + Dlala + Dlala noma ume + Okokuqala + Okulandelayo + Yema + Vala + Susa + Susa ukulanda + Susa ukulanda? + %s kuzosuswa kuleli device + Ukulanda kususwe + Kukwazalukanga ukususa ukulanda + Kukwazalukanga ukudlalisa ukulanda + Ukulandwa kwe-audio + Ukudlala ngaphandle kwe-internet + Kungekenzi ukhonjiswa umisebe okumele ulandwe + Ukulandwa sekuyaqala + Selulandwe + Ukulandwa kuqale + Kuqala ukulanda… + Kulandwa %s%% + Ukulanda kuphothulwe + Ukulanda kuhleleke + Mayelana neMorphe + Izikhangiso + I-feed + Jikelele + Umdlali + Amazwi ingoma + Okunye + Ulimi lwe-YouTube Music + Ulimi lwesistimu + Kudingeka i-Android 13 noma entsha + Fihla ilabeli elithi \'Thola iMusic Premium\' + Fihla izikhangiso ze-Music Premium + Fihla izikhangiso zevidiyo + Fihla ukushintsha phakathi kwe-Audio / Video + Fihla inkinobho yokulandela + ifihla inkinobho yokulandela ohlelweni lwe-podcast + Fihla ishelfu ye-Explore + Fihla ibha yokuhlunga + Fihla ama-shelf egridi + Fihla ama-shelf ahamba ngokunye + Fihla ama-shelf ohlukaniswe ngohlu + Iphandeli lamazwi omculo + Fihla inkinobho ye-Share + Fihla inkinobho ye-Translate + Fihla i-shelf ethi \'Okusha okuvela ku\' + Fihla ama-shelves ohluhwini lwezingoma + Fihla inkinobho ye-Repeat + Fihla inkinobho ye-Shuffle + Fihla i-shelf ye-\'Speed dial\' + Imibono + Khetha ukuthi yiziphi izinto ezivela esigabeni semibono + Fihla Imithetho yoMphakathi + Fihla umlayezo we-context + Fihla inkinobho ye-Emoji + Fihla inkinobho ye-Info + Fihla inkinobho ye-Timestamp + Crossfade + Hlanganisa amaculo ngokulula + Vula i-crossfade + Ikhava le-Fade + Amandla afanayo + Ukubamba okuncane + Ukulula okuthambile + I-S-curve eshelelayo + Ephumayo + Engenayo + Isikhathi se-Crossfade + ama-250 milliseconds + ama-500 milliseconds + ama-750 milliseconds + isikhiya 1 + imizuzwana eyi-2 + imizuzwana eyi-3 + imizuzwana eyi-4 + imizuzwana eyi-5 + izimizuzwana ezingu-6 + imizuzwana eyi-7 + imizuzwana eyi-8 + imizuzwana eyi-9 + imizuzwana eyi-10 + imizuzwana eyi-11 + imizuzwana eyi-12 + Crossfade lapho kuthathwa okulandelayo ngesandla + Isebenzisa umphumela we-crossfade lapho usuke uye emculweni olandelayo ngomzamo wakho + Crossfade lapho kuya okulandelayo ngokuzenzakalelayo + Isebenzisa umphumela we-crossfade lapho umculo uphela ngokwemvelo + Ukulawula iseshini + Cindezela isinkinque se-shuffle isikhathi eside ukuze umise noma uqhubeke ne-crossfade + I-Crossfade imiswe kule seshini + I-Crossfade iqhubekile + Imodi yevidiyo ayitholakali ngenkathi i-crossfade isebenza + I-Crossfade icishiwe ngokudlala kwe-SABR + I-Crossfade icishiwe ngenkathi ukastha + Ukusebenza kwayo + "I-Crossfade ihlanganisa ukuphela kwengoma eyodwa nokuqala kokulandelayo, isebenzisa isikhathi sakho nokulungiseka + +Isebenza ekunqonyeni ngokuzenzakalelayo nasekunqonyeni ngesandla, iguquguquka ngayo yona. Ekunqonyeni ngokuzenzakalelayo, ukuhlanganisa kuqala imizuzwana embalwa ngaphambi kokuba ingoma iphele" + Imingcele + "Amavidiyo omculo adlalwa njengesandi kuphela ngenkathi i-crossfade isebenza. Cindezela isikhashana u-shuffle ukuze ume i-crossfade ubuke ividiyo + +Ividiyo yomculo eqala ukudlala ihlala imodini yevidiyo kuze kube yizingoma ezilandelayo + +Izingoma ezifushane kunomzuzu wokunqunyulwa azisetshenziswa i-crossfade + +Isikrin samakhala noma isaziso singabonisa isimo esidala kancane phakathi kokushintsha + +Ukumisa i-crossfade ukuze ubuke ividiyo yomculo kungashiye amanye amavidiyo emnyama. Ukumisa i-crossfade ngaphambi kokuba ukhethe ingoma kuvimbela lokhu" + Akusekelwa + "Ukudlulisela noma ukukhomba umsindo (Chromecast, ukukhomba isikrini noma umsindo) + +Ukudlala kwe-SABR (Imifudumalo yevidiyo efakekile icishiwe)" + Ukubonga + Isici se-Crossfade sika VazerOG + Khumbula isimo sokuphinda + Khumbula isimo sokuxuba + Isivinyelo sokudlala + Isivinyelo esisetshenziswa kuzo zonke izikhala. Ku-100% isivinyelo se-app siyasetshenziswa, njengesivinyelo sokudlala i-podcast + Shintsha ipitch ngesivinywa + Khansela ukuqondiswa kokungajabuli + Kuvimbela ukuqhuba kweomculo owulandelayo lapho kuchizeka inkinobho yokungajabuli + Isenzo sokuthinta ithrekhi ohlelweni lwezingoma + Dlala uhlelo lwezingoma + Dlala ithrekhi ekhethiwe kuphela + Yengele ithrekhi ekhethiwe ohlelweni + Kwengeziwe ohlelweni + Fihla inkinobho ye-Cast + Fihla inkinobho ye-History + Fihla inkinobho ye-Notification + Fihla inkinobho ye-Search + Fihla inkinobho yokusesha ngezwi + Fihla inkinobho yokusesha ngomsindo + Fihla inkinobho entsha + ifihla inkinobho ethi New kwi-tab ye-Library + Izinkinobho zezenzo + Khetha ukuthi yiziphi izinkinobho zezenzo ezivela ngaphansi komdlali + Fihla ibha yezenzo + Fihla inkinobho yeThanda nokungathandi + Fihla inkinobho yokuCommenta + Fihla inkinobho ethi \'Phinda ubuke i-Live chat\' + Fihla inkinobho yamazwi engoma + Fihla inkinobho yokwabelana + Fihla inkinobho yokulondoloza + Fihla inkinobho ye-Download + Fihla inkinobho ethi \'Qala umsakazo\' + Imenyu ephumayo + Khetha ukuthi yiziphi izinto ezivela emenywini ephumayo lomdlali nomugqa wolandelayo + Fihla isisiseko sezikhizela ezi-3 + Fihla izinkinobho ze-Like ne-Dislike + Fihla imenyu ethi \'Taste match\' + Fihla imenyu ethi \'Yenge ku-playlist ye-Listen later\' + Fihla imenyu ethi \'Yenge ohlwini\' + Fihla imenyu ye-Captions + Fihla imenyu ethi \'Susa i-playlist\' + Fihla imenyu ethi \'Susa ohlwini\' + Fihla imenyu ethi \'Unganikumeli lo mculi\' + Fihla imenyu ye-Download + Fihla imenyu ethi \'Hlela i-playlist\' + Fihla imenyu ethi \'Ya e-album\' + Fihla imenyu ethi \'Ya kumculi\' + Fihla imenyu ethi \'Go to episode\' + Fihla imenyu ethi \'Go to podcast\' + Fihla imenyu ye-Help + Fihla imenyu ethi \'Mark episode as played\' + Fihla imenyu ethi \'Not interested\' + Fihla imenyu ethi \'Pin to speed dial\' + Fihla imenyu ethi \'Play next\' + Fihla imenyu ye-Quality + Fihla imenyu ethi \'Remove from library\' + Fihla imenyu ethi \'Remove from playlist\' + Fihla imenyu ye-Report + Fihla imenyu ethi \'Save episode for later\' / \'Save to library\' + Fihla imenyu ethi \'Save to playlist\' + Fihla imenyu ye-Share + Fihla imenyu ethi \'Shuffle play\' + Fihla imenyu ethi \'Sleep timer\' + Fihla imenyu ethi \'Start radio\' + Fihla imenyu ethi \'Stats for nerds\' + Fihla imenyu ye-Subscribe + Fihla imenyu ethi \'Unpin from speed dial\' + Fihla imenyu ethi \'View song credits\' + Ikhasi lokuqala + Okungavamile + Amashadi + Izinhlelo zokuhamba ngokuhamba + Hlola + Umlando + Umaktabu + Umculo othandwayo + Uhlu lomculo + Sesha + Izisusa + Phushela ekubonakaleni kwe-portrait + Igcina i-app isebenza ngokubonakala kwe-portrait lapho idivayisi izungeziswa + Shintsha umbala we-miniplayer + Kwenza ukuba umbala we-miniplayer ufane nombala we-player ephelele + Shintsha umbala webhala lokuqondisa + Kwenza umbala webhala lokuqondisa uhambisane nomdlali we-fullscreen + Khombisa inkinobho elandelayo ku-miniplayer + Khombisa inkinobho edlule ku-miniplayer + Vula i-miniplayer eyisheshiswayo + Kuvimbela umdlali ukuba angandlulele ngokuzenzakalelayo kuscreen ephelele lapho kuqala khona iculo elisha + Vumela ukuswepha ukuze uvalelwe i-miniplayer + Kuvumela ukuvalwa kwe-miniplayer ngokuyiswepha phansi + Ibhala lokuqondisa + Khetha izinkinobho ezizovela kubala lokunavigetha eliphansi + Fihla inkinobho yeKhaya + Fihla inkinobho yeSamples + Fihla inkinobho ye-Explore + Fihla inkinobho yeLibrary + Fihla inkinobho ye-Upgrade + Fihla ibhala lokuqondisa + Fihla amagama amaqishi okuqondisa + Awungenelele. Thinta ukuze ulungise + Phuma + Uphumile + Vumela i-scrobbling + Thumela isimo se-\'Now Playing\' + Isikhathi esincane sengoma + Izingoma ezifushane kunalokhu ngeke zi-scrobble-we + Ipesenti yokulibala kwe-scrobble + Vula i-scrobble ngemuva kokulalela le pesenti yengoma + Imizuzwana yokulinda ukuba ku-scrobble + Qala uku-scrobble ngemva kokulalela imizuzwana engikini + I-akhawunti ye-ListenBrainz + Ungenele. Thinta ukuphatha i-akhawunti + Faka i-ListenBrainz User Token yakho ukuze uvule i-scrobbling + I-Token + Hlola + Sicela ufake i-token + Kuqinisekiswa… + I-token isemthethweni! Umsebenzisi: %s + I-token ayikho emthethweni: %s + akwaziwa + Ukuqinisekiswa kwephutha: %s + I-Token isuswe + I-Token igcinwe + Isixwayiso: I-Token egcinwe ayilungile! + Thanda amaculo athandwayo + Thanda amaculo ngokuzenzakalelayo ku-Last.fm lapho uwa-like ku-YouTube Music (futhi suse i-love lapho u-unlike noma u-dislike) + I-akhawunti ye-Last.fm + Ungene njenge %s. Thinta ukuphatha i-akhawunti + Faka imininingwane yakho ye-Last.fm ukuze ungene + Igama lomsebenzisi + Iphasiwudi + Ngena + Sicela ufake igama lomsebenzisi nephasiwudi + Kungeniswa… + Ungene ngempumelelo! + Kuhleleke ukuthola iseshini ku-Last.fm + Ukungena kwephutha: %s + Imininingwane ye-Last.fm igcinwe! + Ukuhlanza i-Metadata + Hlanza i-Metadata + Hlanza ngokuzenzakalelayo imininingwane engadingeki (njenge \"Official Video\", \"Remaster\", \"Live\") ezihloko, kubaculi, nakuma-album ngaphambi kokwenza i-scrobbling + Ukuhlanza ngokusebenzisa i-regex eyenziwe + Faka i-regular expression eyenziwe ukuze ususe imibhalo eyengeziwe ezihloko zezingoma, kubaculi, noma kuma-album. Isibonelo: (?i)\\s*\\((clean|explicit)\\) + Hlaziya umculi nomculo esihloko + Khipha igama lomculi negama lomculo esihloko sevidiyo usebenzisa isihlukanisi \" - \". Kusiza ekushiyweni kwokuqukethwe lapho igama lasechannel lingase lisebenze njengomculi + Gcwalisa i-album esishodayo + Uma umculo ungenalo i-album, yilalele ku-Last.fm ukuze i-scrobble yakho iphelele + "I-scrobbling ihlanganisa ngokuzenzakalelayo ukusetshenziswa kwakho i-YouTube Music ne-profile yakho ye-Last.fm noma i-ListenBrainz + +Ukuqinisekisa ukuthi i-scrobbling ihlanganiswa ngesikhathi sokudlala ngemuva, qinisekisa ukuthi ukusetshenziswa kwebhethri kwe-app ye-YouTube Music kubekwe ku- \"Unrestricted\"" + Sebenzisa amazwi angaphandle + Ikhombisa amazwi angosizo lwangaphandle, ibuyele amazwi akhiwe phakathi lapho engenalutho olutholakele + Gcina isikrini sivuliwe ngenkathi ubuka amazwi engoma + Kuvimbela isikrini ukuba sicime ngenkathi iphenela yamazwi engoma ivuliwe + Inkonzo yamagama engoma + Khombisa + Ukuhambisana + Iphaneli + I-endpoint ye-AI + Bonke abahlinzeki bikhanselwe - amazwi akhiwe phakathi kanye namazwi afakwe + Bhala kabusha uhlelo lwezihlinzeki? + Buyisela izihlinzeki ohlelweni lokuqala lwezinto ezibalulekile + Sula amazwi agcinwe + Sula amazwi agcinwe kuleli device + Kusuliwe u-%1$s MB + Thola itokeni + I-Apple Music Media-user-token + "Namathisela i-cookie ye-media-user-token evela ku-music.apple.com + +Izinyathelo: +1. Ngena ku-music.apple.com njengomeluleko +2. Vula i-DevTools → Application → Cookies → music.apple.com +3. Kopisha inani le-\"media-user-token\"" + Apple Music: i-media-user-token igcinwe + Apple Music: i-media-user-token isusiwe + Apple Music: i-media-user-token ayilungile - sicela uhlole futhi uzame futhi + Spotify SpDc + "Namathisela i-cookie ye-sp_dc evela ku-Spotify. + +Izinyathelo: +1. Ngena ku-open.spotify.com +2. Vula i-DevTools → Application → Cookies → open.spotify.com +3. Kopisha inani le-\"sp_dc\"" + Spotify: i-sp_dc igcinwe + Spotify: i-sp_dc isusiwe + Spotify: i-sp_dc ayilungile - sicela uhlole futhi uzame futhi + I-Apple Music token ayibindikile. Ngaphandle kwayo, i-Lyrically izokusetshenziswa njengendlela yokugcina + Deezer ARL + "Namathisela i-arl cookie evela ku-Deezer. + +Izinyathelo: +1. Ngena ku-deezer.com +2. Vula i-DevTools → Application → Cookies → deezer.com +3. Kopisha inani le-\"arl\"" + Deezer: arl igcinwe + Deezer: arl isulwe + Deezer: arl ayingelona iqonda - sicela uhlole futhi uzame futhi + I-Musixmatch Token + Faka i-token kuphela ukuze usebenzise i-akhawunti yakho. Kumele isuka ohlelweni lwe-Musixmatch lwe-Android: ama-token akopishwe ku-musixmatch.com ayengowokusebenza kwe-desktop okusekhala manje futhi i-API ayawamukeli + I-Musixmatch isebenza ngaphandle kwe-token, ngakho leli bhokisi lingashiywa lingenalutho + i-user token + Musixmatch: i-user token igcinwe + Musixmatch: i-user token isulwe + Musixmatch: i-token yomsebenzisi ayilungile - sicela uhlole futhi uzame futhi + I-Cookies ze-YouTube + "Namathisela i-cookies yakho ye-YouTube ukuze ufune ama-captions ne-timetext ku-YouTube. + +Izinyathelo: +1. Vula i-youtube.com ku-browser bese ungena +2. Vula i-DevTools (F12) → Application → Cookies → youtube.com +3. Kopisha u-cookie string ophelele (ama-key=value pairs ahlukaniswe ama-semicolons) + +I-cookies ezidingekayo: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPISID, LOGIN_INFO" + YouTube: i-cookies zigcinwe + YouTube: i-cookies zisuliwe + YouTube: ama-cookies awalungile - sicela uhlole futhi uzame futhi + Thinta ubambe umugqa + Thinta umugqa ukuze uye kulelo zinga engomeni, ubambe isikhathi eside ukuze ukopishe igama elingaphansi komunwe wakho, noma umugqa wonke lapho ukucindezela kungakwenziwa egameni. Uma ukucisha, konke lokhu kuyanyamalala + Khombisa ama-tag ezahlukweni + Labela isigaba esisha ngasinye semizila ngomaki omncane, njenge-Verse noma i-Chorus + Khombisa inkinobho ye-Copy + Thinta ukuze ukopishe amazwi aqala njengombhalo ojwayelekile; cindezela isikhathi eside ukuze ugcine ifayela lezisekelo zamazwi ku-Downloads + Khombisa inkinobho ye-Translate + Chofoza ukuvula/ukuvala ukuhumusha; cindezela isikhathi eside ukuze uhumushe kuphela. Usebenzisa ukuhumusha komunikezeli, iGoogle Translate, noma i-API ehambisana ne-OpenAI + Khombisa inkinobho ye-Romanize + Chofoza ukuvula/ukuvala i-romanization; cindezela isikhathi eside ukuze uthole i-romanization kuphela. Usebenzisa i-romanization komunikezeli, iGoogle Translate, noma i-API ehambisana ne-OpenAI + I-Romanization ne-Alignment yasendaweni + Hlanganisa i-romanization kudivayisi lapho komunikezeli, imodeli yakho ne-Google Translate kungaphumeleli. Ukufundwa kulandela igama ngalinye lamazwi engoma; ukusingisa kwamagama kufanele kube kuvuliwe. IsiShayina, i-Cantonese, isiJaphanisi, isiKoreya, isiGreek, isiRussian, isiGeorgian, isiArmenian; cishe i-120 MB + Kufundwa %1$s: %2$d%% + Ukufundwa kulungile (%1$d entries) + Ukudownload kwehlulekile - cisha lokhu bese ukubuyisela ukuze uzame futhi + Khombisa inkinobho ye-Refresh + Chofoza ukushintshela kumelwe amazwi alandelayo; cindezela isikhathi eside ukuze useshe amazwi ngesihloko sengoma nomculi + Funa amagama engoma + Isihloko sengoma + Umculi + Faka isihloko sengoma nomculi + Akukho ohlinzekileyo ovunyelwe ongakwazi ukufuna ezinye izingoma + Kugcinwe + Gqamisa amagama noma iziluku ezihambisana nomculo + Nikela amagama amagama lapho etholakala. Lapho amazwi enesikhathi segama, agqamisa igama ngalinye noma isiluku ngesikhathi somculo + Khombisa amazwi asemva ngokuhlukile + Kuhambisa izwi elingemuva elibhalwe ezibakweni, njengokuthi \"(Oh) I want you\", kukhishwe emugqeni oculwayo kuye emugqeni wawo ocashile + Fihla amazwi adlalwe + Fihla imigqa yamazwi engaphezulu komugqa odlalwa njengamanje + Fihla amazwi angakadalwa + Fihla imigqa yamazwi engaphansi komugqa odlalwa njengamanje + Khombisa Amazwi eMediaSession esikrinini sokukhiya + Ikhombisa umugqa wamanje esikrinini sokukhiya, ku-Android Auto naku-Bluetooth. Kudingeka i-Android 11 noma entsha kakhulu + Khombisa amazwi eMiniplayer + Ikhombisa umugqa wamanje ku-miniplayer, kanti iculo liyiswa esikhonkweni sezincazelo + Ifomethi yesihloko sengoma/u-artist + Ifomethi yomkhondo ebonakala namagama engoma ku-Miniplayer kanye ne-MediaSession. {t} isihloko, {a} umculi futhi {v} inguinguqulo, njengokuthi Acoustic, lapho umthombo wamagama engoma unikeza eyodwa. Umbhalo phakathi kwamakholoni amabili ubonakala kuphela lapho yonke imvamva ngaphakathi kuyo inenani; bhala \\: ukuze kube kholoni elijwayelekile. Umuzila ngamunye ubonakala ngokulandelana, usuka lapho noma nini lapho amagama engoma eshintsha + Ifomethi yomugqa wamagama engoma + Ifomethi yomugqa wamagama engoma ku-Miniplayer kanye ne-MediaSession. {lrc} ngumugqa futhi {n} ukuthi zingaki izikhathi zilandelana lapho kuhlatshwa khona. Umbhalo phakathi kwamakholoni ubonakala kuphela lapho umugqa ephindaphindwa. Sebenzisa \\: ukuze kube kholoni + Ifomethi kumele ibe ne {lrc} + Ithempulethi yomculi-nomugqa nesikhathi sokuhambisa + Isikhathi esincane sesikrini (ms) + Isikhathi esiningi sesikrini (ms) + Kuzimisele nge-animations zensizakalo eziningi kuphela. Gcina ifulembi ngalinye okungani isikhathi esincane. Esikhathini esiningi, qhubekisela phambili umugqa womculi ngisho noma amazwi ehlabelelo ehlala efanayo. + Isikhathi esincane kumele kube u-0 noma ngaphezulu. Isikhathi esiningi kumele sibe ngaphezulu kuka-0 futhi okungani isikhathi esincane. + Imingcele yobude bomugqa + Imingcele yomugqa wezincwadi zomculo nomculi, kanye isisindo somlinganiswa ongaphelele + "Umngcele womugqa wezincwadi zomculo +Hlukanisa amazwi omculo amade babe izigcawu ezilinganayo. 0 kukhishwa ukuhlukanisa." + "Umngcele womugqa womculi +Hlukanisa ifomethi yomkhondo egcwele ibe amafulembi ajikayo. 0 kukhishwa ukuhlukanisa." + "Isisindo sabalingiswa ababanikezelwe ububanzi obuphelele +Sisebenza kuzo zombili izimiso. Ku-1.75, abalingiswa abane ababanikezelwe ububanzi obuphelele babalwa njenge-7." + Faka izimiso zezinombolo eziphelele kusuka ku-0 kuya ku-10000 nesisindo sabalingiswa esingaphezulu kwe-0 + Iphika lensimbi + Kuboniswa ku-miniplayer naku-MediaSession ngesikhathi sephika lensimbi. Umugqa ngamunye uwuhlobo olulodwa lokunyakaza. {t} isihloko, {a} umculi kanti {lrc} ungumugqa wokugcina wamazwi omculo + Ukuhleleka kwesikhathi se-Miniplayer ne-MediaSession + Ishifutha ukuthi umugqa olandelayo ubonakala nini ku-miniplayer kanye ne-MediaSession. Sebenzisa inani elingaphansi kwerozero uma imigqa ivela kade, isibonelo ekhasini lokukhiya + Amafilter + I-regex yemininingwane + I-Regular expression esetshenziswa kuma-titles, artists, kanye nama-albums ngaphambi kokubheka amazinga kanye lapho kubalwa ukuthi ama-titles amabili ayafana yini. Shiya kuyisempty ukuze ukhiphe zombili. I-default isusa izinto ezingadingeki ezifana ne- \"Official Video\", \"Remaster\", \"【插曲】\", kanye ne- \"- Topic\" + I-regex yesihlungi samagama engoma + I-Regular expression esetshenziswa embhalweni wokuqala wamagama engoma kuphela (hhayi izihumushi noma ama-romanization). Shiya kuyizinhlampu ukuze ukukhiphe + Izibonisezi zomugqa wolwazi + Uma amagama engoma ehlungwa ngendlela engalungile ziya phansi, lungisa noma sula lokhu. Izibonisezi ezihlukaniswe yikhoma: imigqa equkethe isibonisezi elandela i-colon ibonakala njengolwazi lwe-credit phansi. Shiya kuyizinhlampu ukuze ukukhiphe + Enza uhlamvu lokuqala lube lukukhulu + Enza uhlamvu lokuqala lwalowo mugqa wezinkondlo lube lukukhulu + Susa izimpawu zokuloba ekugcineni + Isusa amaphuzu, amakhoma, amakholoni kanye namakhoma-aphuzu ekugcineni kwemigqa yezinkondlo. Izimpawu zombuzo, izimpawu zokumangala, amaphuzu aqhubekayo, izinkobe kanye nezibakatshana ziyagcinwa + Usayizi wombhalo + Usayizi wombhalo wamazwi engoma + Isikhathi sokuhambisa somhlaba + Amazwi ahambisana nokudlalwa komculo. Uma engahambisani, lungisa isikhathi. Thuthukisa amazwi ahambisana nomculo phambili noma emuva. Sebenzisa inani elihamba phambili uma amazwi ehamba phambili kunengoma + Sebenzisa izigaba ze-SponsorBlock ukuze uthole ukuhambisa kwamazwi + Fanisa ngaphandle kwezigaba i-SponsorBlock eyishiyayo bese uhambisa amazwi ku-audio ekhishwe. Uma icishwa, i-SponsorBlock ayinandiswa neze + Le track ayina-video id yokuyithumela + Umthombo: %s + Cindezela isikhopho sokukopisha isikhathi eside ukuze ugcine le LRC; uma ukwazi ukulungisa izikhathi futhi uzihlanganyele nabahlinzeki bomphakathi, sizobonga + Huhumushiwe ngu-Google + Hlelwe ngohlamvu lwe-Roman ngu-Google + Ihunyushwe ngu %s + I-romaniziwe ngu %s + I-Romanized ngu %s + Ihhumuliwe + Ulimi lokuhumusha + Ulimi lwe-Romanization + Kuhunyushwa… + Kukwazekile ukuhumusha amazwi engoma + Hlelwe ngohlamvu lwe-Roman + Ngokuzenzakalelayo + Kuhlelwa ngohlamvu lwe-Roman… + Kungekenzeki ukuhlela amazwi ngohlamvu lwe-Roman + Huhhumulwe kuphela + I-Romanized kuphela + Imodi emibili + Kukopiwe ku-clipboard + Kuvuselelwa + Kuvuselelwe + I-API key ye-QuickLRC + I-QuickLRC ibhala phansi noma ihlanganisa izikhathi zamagama engoma yamanje. Isevisi eyodwa, engahlobani ne-endpoint ehambisana ne-OpenAI engenhla + I-key ye-QuickLRC igcinwe. Sebenzisa isikhopho sokuhlanza (align) ebhokisini lokusesha amagama engoma, noma isiboniso sepaneli lapho ingoma ingenazo amagama + I-API key ye-QuickLRC igcinwe + I-QuickLRC ayizange iyiamukele leyo key + I-API key ye-QuickLRC isuswe + I-QuickLRC ibeka izikhathi zamagama engoma angenazo, noma ibhala phansi kusuka engomeni, ngentengo ye-credits. Uma i-key isethiwe, izingoma ezingenazo amagama zinikeza ukubhalwa phansi (transcription) kanti ibhokisi lokusesha lithola isikhopho sokuhlanza (alignment) + Cela i-alignment ye-QuickLRC + Kuhleleka + Sebisa i-alignment + Cela ukukopishwa kwe-QuickLRC + Kukopishwa + Ukukopishwa kukhanselwe + Yizingoma ezidlala ku-YouTube kuphela ezingakopishwa + Ayikho amagama okuhlanza - kubhalwa phansi esikhundleni salokho + I-QuickLRC ayikwazanga ukuphrosesa le ngoma + Amakhredithi e-QuickLRC aphelile kule ngoma + "Amazwi ayahambisana nesihloko nomculi, imininingwane engajwayelekile ingase ingathole lutho noma ithole uhlobo olungalungile. Sebenzisa ukuvuselela ukuze ushintshe uhlobo. Abahlinzeki abamakiwe ngo * badinga itoken. Bonke abahlinzeki abuvuliwe abicelwa ngaso sonke isikhathi ngokulandelana kwenu, kodwa baphendula ngokusekelwe ngesivinyane sabo.\n\nAmazwi avela ezinsizakalonisweni zangaphandle ezingahlobene ne-Morphe, ezingaxikiswa, noma ezingathunyelwa yi-Morphe. Ngasinye sisetshenziswa njengoba sinjalo: amazwi, ukutholakala kwawo, kanye nokulicense kwawo kusala kulezo zinsizakaloniso kanye nabanikazi balicense, futhi ukusebenzisa kwazo kungaba ngaphansi kwemigomo yawo yosizo. Okufika ekuhlinzekweni okuvuliwe wukufuna okucelile kuphela - isihloko, umculi, nalapho leyo nsizakaloniso ikudinga, i-video ID. I-AI endpoint ekhethwekile kanye nokubhala kwe-QuickLRC kusetshenziswa kuphela ngemuva kokuba usilayile, futhi kuthola Umbhalo wamazwi noma i-URL ye-video yeculo." + Vula kuphela abahlinzeki obasebenzisayo, noma ushiye okujwayelekile - izicelo eziqhubekayo zingaba nzima emawebhasayithi amancane amazwi omphakathi + Fihla umugqa wemiklomelo + Fihla umugqa wekredithi obonakala ngaphansi kwamazwi aqinile + Shintshisana ngohumusho nokubhala ngama-roman + u-ON ubeka ukubhala ngama-roman ngaphansi kwamazwi nohumusho phezulu; u-OFF ukushiya ukubhala ngama-roman phezulu nohumusho ngaphansi + Ihuhlo + I-Romanization + Umhlinzeki wokuhumusha + Google (Okukhethiwe, Mahhala) + OpenAI Compatible (Okuyamathuba) + Umhlinzeki we-Romanization + Google (Okukhethiwe) + Isichazamazwi sasekhaya / Offline + OpenAI Compatible (Okuyamathuba) + Isigaba se-AI + Kusetshenziswa kuphela lapho umhlinzeki engithumeli izabelo zezigaba njenge-Chorus noma i-Verse, okutholwa ngumodeli wakho ohambisana ne-OpenAI + Ama-AI & Cloud Endpoints + Lungisa ama-API keys, ama-models, ama-endpoints, kanye nama-prompt templates abahlinzeki be-AI. + I-API endpoint URL (isib. https://api.openai.com/v1/chat/completions) + Ithempulethi ye-Prompt + Izindawo zokugcina: {language}, {task}, {title}, {artist}, {count}, {lines} + I-Prompt igcinwe + I-prompt igcinwe, kodwa i-AI endpoint ayikawukwazi ukusetshenziswa. Isethe ukuqala ngaphansi kwe-AI Settings + I-Prompt ibuyiselwe emva + Akusona i-JSON esemthethweni. yilungise noma sucize inkundla ngaphambi kokulonda + Izilungiselelo ze-Translation & Romanization zigcinwe + Izilungiselelo sezibuyiselwe ekusetshenzweni kwazo okwemqondo + I-Base URL kufanele iqale ngo http:// noma https:// + Hlola ukuxhumana + Lungisa ama-Endpoints + Khombisa wonke ama-Endpoints + Kuqinisekiswa ukuxhumana… + Umhlinzeki we-Translation + Umhlinzeki we-Romanization + Thola amaModel + Kutholwa amaModel… + Khetha iModel + Kuhleleke ukuthola amaModel. Hlola i-API key noma ukuxhumana. + Izilungiselelo ze-Gemini API + OpenAI Compatible (Custom Endpoint) + Izilungiselelo ze-DeepL API + I-API key + IModel + I-URL eyisisekelo + I-API key (iyakhethwa) + Isicelo sohumusho + Umzimba wesicelo owengeziwe (i-JSON eyakhethwa) + I-key yokuqinisekisa (i-Free noma i-Pro) + Lungisa ama-API key, ama-model, ama-endpoint, kanye nezicelo + 8.23.51 - Vala ama-icon agqamile + 9.13.51 - Buyisela ama-tab e-Player Up next, Lyrics, kanye ne-Related diff --git a/patches/src/main/resources/addresources/values-zu-rZA/reddit/strings.xml b/patches/src/main/resources/addresources/values-zu-rZA/reddit/strings.xml index cf7776fdaf..b1f2f387c4 100644 --- a/patches/src/main/resources/addresources/values-zu-rZA/reddit/strings.xml +++ b/patches/src/main/resources/addresources/values-zu-rZA/reddit/strings.xml @@ -15,4 +15,123 @@ Second \"item\" text" --> - + + Imigomo yobumfihlo ye-Reddit + Ukungena nge-Google akutholakali. Sebenzisa i-imeyili ukuze ungene + Izikhangiso + Fihla izikhangiso ze-feed + Fihla izikhangiso zakomemovo + Ibar yakuhamba + Fihla inkinobho ye-Answers + Fihla inkinobho ye-Chat + Fihla inkinobho ye-Create + Fihla inkinobho ye-Discover noma ye-Communities + Fihla inkinobho yeMidlalo + I-Sidebar + Fihla isheliphu le-Recently Visited + Fihla isheliphu le-Games on Reddit + Fihla isheliphu le-Reddit Pro + Fihla isheliphu le-About + Fihla isheliphu le-Resources + Uhlelo + Kopisha umbhalo + Ivumela ukukhetha nokukopisha umbhalo ezifanekisweni nasezincazelweni. Ikhansela ukufihla izincazelo + Ukufihla izincazelo kuyoba yinto engasebenzi ngenkathi iKopisha umbhalo isebenza + Ifonti eyikhethiwe + Loda ifayela lefonti le-TTF noma le-OTF eyikhethiwe endleleni engaphansi. Ukukwenza lokhu kuvala ukuPhocelela ifonti yesistimu + Ifayela lefonti eyikhethiwe + Thinta ukuze ukhethe ifayela lefonti le-TTF noma le-OTF + Ifonti ekhethiwe - %s + Khetha ifayela lefonti eyikhethiwe kuqala + Sicela ukhethe ifayela lefonti le-.ttf noma le-.otf + Kuhlulekile ukuloda ifonti eyikhethiwe. Khetha elinye ifayela + Khubisa i-UI yasekhaya yesimanje + Vala i-popup yesikrinishoti + Kuvala i-popup evele lapho uthatha isikrinishoti + Phocelela ifonti yesistimu + Sebenzisa ifonti yesistimu yedivayisi esikhundleni se-Reddit Sans noma i-Roboto. Ukukwenza lokhu kuvala Ifonti eyikhethiwe + Fihla inkinobho ye-Ask + Ifihla inkinobho ye-Ask kubha yokusesha + Fihla i-shelf yama-communities + Ifihla i-shelf yama-communities ahlobene noma aphikisiwe kuma-subreddits + Fihla ama-shelves we-Trending + Ifihla ama-shelves we-Trending ku-feed kanye neziphakamiso zokusesha + Susa i-dialog ya-notification suggestion + Iqeda ngokuzenzakalela idayalogi yesiphakamiso sesaziso lapho uvakashela i-subreddit + Khombisa ama-flair ku-feed yasekhaya + Khombisa ama-badge e-flair yethambo ku-feed yasekhaya + Izinto ezithile ze-feed zingase zingakhombisani i-flair + Khombisa isibalo sokubukwa kwezifanekiso + Umunansi: Itholakala kuphela uma impendulo ye-API ifaka isibalo sokubukwa + Shintsha ibha yezenzo yesifanekiso + Yisa izinkinobho zokuvota nezencazelo zesifanekiso ngakudla, kanye nezinkinobho zokuthumela futhi wabelane ngakwesobunxele + Shintshisana ngezinkinobho zokuvota nezencazelo + Khombisa inkinobho yencazelo yesifanekiso phambi kwezinkinobho zokuvota + Susa i-dialog yewarning ye-NSFW + Iamukela ngokuzenzakalela isixwayiso socingo lwe-NSFW lapho uvakashela i-subreddit + Okunye + Mayelana + Inguqulo ye-Patch nama-links asemthethweni + Hlanza ama-link okwabelana + Iqeda ama-query parameters okulandela kuma-link owabelene + Vula ama-link ngqo + Idlula ama-URL okudlulisela lapho kuvulwa izixhumanisi zangaphandle + Izithashamlambo ze-feed ezinewidth ephelele + Ikhombisa izithombe namavidiyo kusukela phambili kuya ekugcineni ku-feed nasemincane yezindaba, ngaphandle kwe-padding yohlangothi namakona ayigwele + Amandla okucisheka kombukeli wemidlalo + Amandla omnyama ashabalala ngemuva kocaphuna lapho ubuka izithombe namavidiyo, kusuka ku-0 (akukho ukushabalala) kuya ku-1 (okujwayelekile kwe-Reddit) + Gcina isikhundla se-feed + Kugcina i-feed esikhundleni esifanayo emva kokuvala isithombe, ividiyo noma i-post, esikhundleni sokuhambisa i-post evuliwe phezulu + Ubude obukhulu bemidlalo ye-feed + Ubude obuphezulu bezithombe, amavidiyo namagalleries ku-feed, njenge-multiple yobubanzi bwawo. Izithombe ezinde ziyasikwa. u-1.33 uyisetingi seqala se-Reddit + Dlala ama-Redgifs nomsindo + Idlala i-video yokuqala ye-Redgifs esikhundleni sekopi ye-Reddit, evamise ukungabi nomsindo + Ikhwalithi ye-Redgifs HD + Idlalisa ividiyo ye-HD yeRedgifs. Ividiyo ze-HD zinkulu kakhulu futhi zingase zimise ukuze zilayishwe + Indawo yokulanda + Zonke izakhawushi + Okujwayelekile kwe-Reddit: izithombe ku-Pictures/Reddit, ividiyo ku-Download + Ifolda: %s + Igama lefayela: %s + Amagama amafayela e-Reddit + Usebenzisa \"Zonke izakhawushi\" + Sebenzisa indawo yakho + Lungisa ku- + Amafolda ajwayelekile e-Reddit + Okungavamile + Ifolada elingaphansi kwelinye + Faka amafolada angaphansi ngokusebenzisa u / egameni lefayela + Hlabela… + Alikho ikhetho lefolada elifanele kuleli device + Igama Lefayela + Igama lefayela le-Reddit + Thinta itokeni ukuze uyifake. U / udala ifolada elingaphansi. Izixazululo ezingaziwa ziba \"unknown\" + Izithombe: %s + Ama-video: %s + Gcina + Setha kabusha + Susa + Setha kabusha indawo yokulanda yawo wonke ama-akhawunti ibe yeyona eyisisekelo yeReddit? + Susa indawo yokulanda ka %s? + Kukwazekile ukulondola endaweni yokulanda. Kulondolwe efolder eyisisekelo yeReddit esikhundleni sayo + Ifolder yokulanda ayisasebenzi. Kulondolwe efolder eyisisekelo yeReddit esikhundleni sayo + Kukwazekile ukulondola ividiyo elandwe + Le folder ayikwazi ukusetshenziswa ekulandeni + %s (yamanje) + %s (angenanga) + Okungaziwa (phumelele noma usemode we-incognito) + Fihla i-overlay yomboni wezinto zokuxhumana + Ifihla isihloko, izinkinobho nokulawula amavidiyo lapho uvula isithombe noma ividiyo. Thinta into yokuxhumana ukuze uzibone + Fihla inkinobho ethi Buka ingxoxo + Ifihla inkinobho ethi \"Buka ingxoxo\" phansi komboni wezinto zokuxhumana + Isizinda esimnyama somboni wezinto zokuxhumana + Isebenzisa isizinda esimnyama ngemuva kwezithombe namavidiyo kumboni wezinto zokuxhumana, esikhundleni sombala wesizinda we-theme + Vula ama-link ngaphandle + Ivula izixhumanisi kubrowser yakho esikhundleni se-browser engaphakathi ohlelweni + Khumbula imibono evaliwe + Izincazelo ozifihliyo zihlala zifihleke lapho uvula futhi isifanekiso + I-ayikoni yohlelo + Khetha i-ayikoni yohlelo + Okujwayelekile + Akaziwa + diff --git a/patches/src/main/resources/addresources/values-zu-rZA/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-zu-rZA/shared-youtube/strings.xml index fd007eeb78..d98199c177 100644 --- a/patches/src/main/resources/addresources/values-zu-rZA/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-zu-rZA/shared-youtube/strings.xml @@ -18,46 +18,296 @@ Second \"item\" text" --> + I-animation yokuqala + Idisayizelwe + Umbala + Imibala emnyama nemhlophe + Qala kabusha ukuze uqedele ukusetha + Igama lehapps + Isizinda setheme esimnyama + Mnyama wempela + Material You (Mnyama wempela) + YouTube yakudala + Pinku okumnyama + Blue okumnyama + Okuluhlaza okumnyama + Okuphelu okumnyama + Okubomvu okukhanyayo okumnyama + Okubomvu okumnyama + Isizindalo esimnyama esikhethiwe + Lungisa umbala wesizindalo kwe-theme esimnyama + I-default ye-App + Material You (Neutral) + Material You (Primary) + Material You (Secondary) + Material You (Tertiary) + Okukhethiwe + Ukulanda + Imode yokulanda + Okujwayelekile + Landa phakathi ohlelweni + Umlandi wezangaphandle + Igama lephakheji le-Downloader + Igama lephakheji lohlelo lwakho lwe-downloader lwangaphandle olufakwe + Faka igama le-package + Ezinye + I-App ayifakwanga + %s ayifakwanga. Ngicela uyifake + "Akukwazanga ukutholakala kwe-app efakiwe enegama lepakeji: %s + +Qinisekisa ukuthi igama lepakeji lilungile nokuthi i-app ifakiwe" + Igama lephakheji alikwazi ukuba lingena + Okuhleliwe + I-ayikoni yohlelo + Okokuqala + Morphe omnyama + Morphe omnyama kakhulu + Morphe okhanyayo + Ukudlala okukhanyayo kwe-Morphe + Morphe play black + Okwiqondiswe + "Lokhu kushintsha i-icon ebonakala ku-app launcher yakho. + +Lolu setting ngeke lushintshe i-icon kulezi zindawo: +• Uhlu lwe-app lwe-system settings lwedivayisi +• Izaziso ezithile ze-push +• I-app picker ethi 'Open with' +• I-icon ye-Android Auto + +Ukuze ushintshe i-icon yonke indawo, patch futhi ushintshe i-option ye-patch ethi 'App icon'" + Ikhona lezaziso + Fanisa i-icon yohlelo + Morphe play + Isaziso seMorphe + Umlando wakho wokubuka awugcinwa.<br><br>Lokhu kudalwa kakhulu yi-DNS ad blocker noma i-network proxy.<br><br>Ukuze ukulungise lokhu, faka i-<b>s.youtube.com</b> ohwelwini lwezinto ezivumelekile noma ucishe zonke izivimbi ze-DNS nama-proxies + Ungakukhombisi futhi + Lingisa ama-stream evidiyo + Lungisa i-client spoofing ukuze kuvikelwe izinkinga zokudlala + "Lulungisa izinkinga zokudlala kubasebenzisi abaningi ngokucela u-video stream ohambisanayo + +Uma ungumsebenzisi we-YouTube Premium, le setting kungenzeka ingadingeki" + Akukwazanga ukuthola noma yiziphi izimuerzo ze-client + I-client evamile ayisebenzi + "u-%1$s akasebenzi kaningi ngemva kwemva, kodwa u-%2$s usebenze. + +Ukwenza u-%2$s abe yi-client evamile?" + Shintsha + Ukucisha leli setting kungadala izinkinga zokudlala + Zama ukuvula: Spoof video streams > Android VR sign in + I-client eyisisekelo + Ngena ku-Android VR + Awunangena ku-Android VR. Thinta lapha ukuze ungene + Okwamanje ungenile ku-Android VR + Ukuze usebenzise lo mshini, shintsha \'Iklayenti elijwayelekile\' kube i-Android VR + Ngena ku-Android VR + "Ukungena ku-Android VR kungalungisa izinkinga zokudlala kubabukeli abathile + +Ukuze ungene, cindezela qhubeka ngezansi" + Phuma + Imiyalelo + "Ikhodi yokuvula: %s + +1. Thinta u\"Vula iwebhusayithi\" ngezansi ukuze uvule iwebhusayithi ethi \"Xhumana nendlela\" +2. Namathisela (noma bhala) ikhodi yokuvula ebonakala ngenhla +3. Ngena ku-Google uma ucelwa. Uma unama-akhawunti amaningi, khetha leli akhawunti le-YouTube +4. Thinta u\"Vumela i-YouTube VR\" ukuba ithole ukufunda +5. Buyela kulolu hlelo ngemuva kokufika umlayezo othi \"Idivayisi ixhumekile\"" + Kuhlulekile ukukhipha ikhodi yokuvula + Kuhlulekile ukukhipha ithokeni lokugqamisa + Ungene ngempumelelo + "Ukuqinisekisa kugcinwe + +• Izilungiselelo zakho zeMorphe ngeke zingenise/zikhiphe ukuqinisekisa kwe-Android VR +• Uma lolu hlelo lungasetshenziswa iminyaka eyi-6, i-Google izosusa lokuqinisekisa +• Kuzodingeka ungene futhi uma ushintsha ipasiwedi lakho loku-Google noma ususa leli kifu ezilungiselelweni zakho ze-Google" + I-OAuth2 ayitholakali okwesikhashana (i-API iphelelwe isikhathi) + Isikhathi sokungena ku-Android VR siphelile, zama ukungena ku-VR futhi + I-OAuth2 ayitholakali okwesikhashana (isimo %s) + I-OAuth2 ayitholakali okwesikhashana + Awukwazi ukungena ku-VR, ukuqinisekisa kwewebhusayithi akuzange kuvunyelwe + Awukwazi ukungena ku-VR: %s + Uphumile ku-Android VR + I-token yokungena ku-VR ayilungile, zama ukungena ku-VR futhi + Izilawuli zezimedia zezaziso + Lungisa izilawuli zokudlala ezibonakala esikrinini sakho sokukhiya nasipanelini sezaziso + Fihla izinkinobho zeNganu & Elandelayo + Ifihla izinkinobho zeNganu neLandelayo ezazisweni yezimedia, esikrinini sokukhiya, nasezilawulini zezikopho + Khulula i-seekbar + Kuvimbela ukukweba i-seekbar esikrinini sokukhiya nasipanelini sezaziso + Funa ngama-button e-Previous & Next + Ishintsha izilawuli zezaziso zezimedia, isikrini esishiwo, kanye ne-headphone ukuze kufunwe esikhundleni sokushintsha amavidiyo + Ubude bokubuyisela emuva + Imizuzwana yokubuyisela emuva nge-button e-Previous + Ubude bokunyakazisa phambili + Imizuzwana yokunyakazisa phambili nge-button e-Next + Ubude bokufuna kumele bube phakathi kwe-1 kanye ne-%s imizuzwana. Kusetshenziswe okuyisayenzi + Vala iphrothokholi ye-QUIC + Vala umsindo we-DRC + Phoqa ulimi lwe-audio lwangokuqala + Uhlale udlala umkhondo womculo wokuqala wevidiyo esikhundleni senye engihunyushwe + Gxuma ukuthula + Igxuma ngokuzenzakalelayo izikhala zokuthula ku-audio yevidiyo + Ubude obuncane bokuphetha + Kufushiswa kuphela okuphumayo okubanzi kangaka. Kuzosebenza lapho kuqala i-video elandelayo + I-Network proxy + Lalisa izicelo zenethiworki ezisekelwe ku-proxy + Vula i-proxy + Lalisa izicelo zenethiworki ezisekelwe ku-proxy ye-HTTP noma i-HTTPS ekonyiwe + I-host ye-proxy + Igama le-host noma ikheli le-IP le-proxy server + I-port ye-proxy + I-port ye-proxy server + Sebenzisa i-HTTPS proxy + Sebenzisa i-HTTPS ukuze uxhumane ne-proxy server esikhundleni se-HTTP + Ukuqinisekisa kwe-proxy + Thumela ukuqinisekisa kwe-Basic nezicelo ze-proxy + Igama lomsebenzisi we-Proxy + Igama lomsebenzisi lokuvumeleka kwe-Proxy eliyisisekelo + Iphasiwedi ye-Proxy + Iphasiwedi lokuvumeleka kwe-Proxy eliyisisekelo + Vumela ukuya ngqo lapho kuhluleka + Xhumana ngqo uma i-proxy ihluleka + Fihla izikhangiso zesikrin sahhayu + Ukuze usebenzise this feature, shintsha i\'Spoof video streams\' abe yiklayenti noma iyiphi ngaphandle kwe-\'Android Studio\' kanye ne-\'Android VR Downgraded\' + Hlanza ama-link okwabelana + Isusa ama-tracking query parameters kuma-links ahlanganyelwe + Shintsha izixhumanisi zokwabelana zibe yi-youtube.com + Shintsha i-music.youtube.com ibe yi-youtube.com ema-links ahlanganyelwe + Shintsha ama-link okwabelana abe ngu youtu.be + Yenza amavidiyo ukuba asebenzise ifomathi ye-URL efishane ye-youtu.be + I-Protocol buffer ye-Debug log + Yengeza imininingwane yokurekhoda eyengeziwe kubalusi ukuze babone izici zohlelo + "Ukuvula leli setting kuzobhala idatha eyengeziwe yohlelo, kuhlanganise nombhalo oseku-screen kwezinye izinto ze-UI. + +Lokhu kungasiza ukuhlonza izinto lapho kudalwa ama-custom filters. + +Nokho, ukuvula lokhu kuzobhala naphansi idatha yomsebenzisi efana ne-IP address yakho" + Ifiltha elilayishwe + Lungisa amafiltha alayishwe ukuze ufihle noma yisiphi isisindo + Vula ifiltha elilayishwe + Uhlu lwezingcingo zokwakha indlela yesisindo zokuhlunga, ezihlukaniswe imigqa emisha + Ifiltha elilayishwe elingelona: %s + Ihlolo lemenyu yezilungiselelo + Fihla izinto esikrinini sezilungiselelo zezohlelo ezijwayelekile ngokwegama lazo elibonakalayo + Vula isikrini sezilungiselelo zezohlelo ezijwayelekile kanye ukuze ugcwalise uhlu + Susa idiyalogi yokukhetha kombukeli + Lokhu akukwenzi ukuba udlule emingceleni yobudala. Lokhu nje kukwamukela ngokuzenzakalelayo + Umhlinzeki we-PoToken + Lungisa umhlinzeki we-PoToken ukuze uvimbe izinkinga zokudlala + Umhlinzeki we-PoToken wangaphandle + "Ilungisa izinkinga zokudlala kusetshenziswa i-PoToken minter yangaphandle (Experimental) + +Uma ungumsebenzisi we-YouTube Premium, le setting kungenzeka ingadingeki" + "Kulungisa izinkinga zokudlala kusetshenziswa i-PoToken minter yangaphandle (Experimental) + +Ukuze usebenzise le nsizakalo, cisha u-'Spoof video streams'" + Uma kungasebenzi, zama ukuvala i-MicroG ngenkani + Vula izilungiselelo ze-PotHelper + "Le feature isamanje isesethuthukiswa futhi kungenzeka ingasebenzi + +Chofoza lapha ukuze ulandele i-PotHelper" + UPotHelper ayisasebenzi + "I-token ekwakhiwe kule nguqulo yePotHelper ayisasebenzi + +Vula u'Spoof video streams' ukuze ulungise ukudlala" + Vula + Lingisa inguqulo yohlelo + Ikhohlisa iserba ukuba icabange ukuthi usebenzisa inguqulwe yakudala ye-app ukuze ubuyise izici ze-UI zakudala + "Inguqulo ye-App izocashwa ukuthi iyinguqulo endala. + +Lokhu kuzoshintsha indlela i-app ebonakala ngayo kanye nezinqubo zayo, kodwa kungase kube nemiphumela engaziwa. + +Uma icishwa kamuva, kunalulekile ukusula idatha ye-app ukuze kuvikelwe amaphutha e-UI" + Yenza i-version yohlelo lusebenze njenge-target + Izinga lokuvuselelwa kohlelo + Okujwayelekile + %s FPS + Dlula izinga lokuvuselelwa + Njalo + Ukudlala ngokuma kokuphakathi + Ukudlala ngesikrinimfunda + Ukudlala ngendlela eyimele nangesikrinimfunda + Ifonti + Okujwayelekile + Ifonti yesistimu yedivayisi + Ifayela lefonti elikhethiwe + Ifayela lefonti elikhethiwe + Khetha ifayela lefonti le-TTF noma le-OTF + Ifayela ekhethiwe aliyona ifonti ye-TTF noma i-OTF + Ifonti ekhethiwe ayikwazanga ukulodwa futhi icishiwe + Izinto ezingathandeki azitholakali okwamanje (API timed out) + Izinto ezingathandeki azitholakali (isimo %s) + Izinto ezingathandwa azitholakali (umkhawulo we-client API) + Izinto ezingathandwa azitholakali (%s) + Fifihliwe ngumnikazi + Awuyithandi le video, kanye nabanye abantu abangu %s + Awuyithandi le video, %s kwamavoti awathandi + Vula i-Return YouTube Dislike + Khombisa izinto ezingathandwa njengepesenti + Khombisa izinto ezingathandwa njengepesenti yezinto ezithandwayo esikhundleni senombolo + Khombisa izinto ezithandwayo ezilinganiselwe + Khombisa isibalo sezinto ezithandwayo ezilinganiselwe kumavidiyo lapho umdali evale khona izinto ezithandwayo + Khombisa izixwayiso zephutha lokuxhumana + Khombisa isaziso lapho iserba le-Return YouTube Dislike ingatholakali + "I-Return YouTube Dislike ibuyisa izibalo zezinto ezingathandwa u-YouTube azifihlayo ngo-2021. Izibalo zivela kudatha egcinwe ngaphambi kokususwa kwazo futhi zibalulelwe ama-video amasha ngokuvota kwabasebenzisi bayo, ngakho zingaba ngobuningi futhi zivuselelwa njalo emalangeni ambalwa + +I-ID ye-video oyibukayo ithunyelwa ku-Return YouTube Dislike API, kanti ukuthanda nokungathanda kwakho kuthunyelwa ngokungaziwa ukuze kuthuthukiswe ukubalulelwa" + Izibalo ze-API ze-Return YouTube Dislike zaleli device + Isikhathi sokuphendula se-API, isinyathelo + Isikhathi sokuphendula se-API, esiphansi kakhulu + Isikhathi sokuphendula se-API, esiphezulu kakhulu + Isikhathi sokuphendula se-API, imediya yokugcina + Okungathandeki akutholakali okwesikhashana - Umkhawulo we-Client API usebenza + Ukuthola amavoti e-API, inombolo yezingcingo + Azikho izingcingo zenetivu ezenziwe + %s izingcingo zenetwork zenziwe + Ukuthola amavoti e-API, inombolo yezikhathi zokuphela kwesikhathi + Azikho izingcingo zenetivu eziphelelwe isikhathi + %s izingcingo zenetwork ziphelile isikhathi + Imikhawulo yebhadi le-API client + Azikho imikhawulo yebhadi le-client ehlanganyelwe + Umkhawulo we-rate limit we-client uhlangana nezikhathi ezi-%s + %s milliseconds diff --git a/patches/src/main/resources/addresources/values-zu-rZA/shared/strings.xml b/patches/src/main/resources/addresources/values-zu-rZA/shared/strings.xml index 31cdbe4dcb..1ceb021c0e 100644 --- a/patches/src/main/resources/addresources/values-zu-rZA/shared/strings.xml +++ b/patches/src/main/resources/addresources/values-zu-rZA/shared/strings.xml @@ -17,21 +17,219 @@ Second \"item\" text" --> + Izilungiselelo + Uyaqiniseka yini ukuthi ufuna ukuqhubeka? + Gcina + Buyisela emuva + Buyisela umbala + Umbala ongaphumeleli + Kudingeka ukuqalisa kabusha + Qala futhi uhlelo lwe-app ukuze lolu shintsho lusebenze + Qalisa kabusha + Genisa + Kopisha + Izilungiselelo zeMorphe zibuyiselwe ekusetshenzisweni kwazo okwokuqala + Kungeniswe izilungiselelo ezi- %s + Ukungenisa kuhlulekile: %s + Hamba uye phezulu + Funa izilungiselelo + Mayelana + Akukho imiphumela etholakele ngo \'%s\' + Zama elinye igama lokufuna + Ukufunwa kwamanamuha + Suse lugu lwokufuna emlandweni? + Susa emlandweni wokusesha + Sula umlando wokufuna + Uyaqiniseka ukuthi ufuna ukusula wonke umlando wokufuna? + Amathiphu Okufuna + "• Thinta indlela ukuze uye kuyo +• Cindezela isilungiselelo isikhathi eside ukuze uye kuso +• Cindezela uEnter ukuze ugcine imibuzo yokufuna emlandweni +• Ukufuna akunaki izinhlamvu ezinkulu nezincane noma izimpawu zokuloba +• Izilungiselelo zomzali zivela ngaphezulu kwezilungiselelo zengane ezikhishwe" + Umlando wokusesha awunalutho + Ukuze ugcine umlando wokusesha, bhala imibuzo yokusesha bese ucindezela u-Enter + Khombisa umlando wokusesha wezilungiselelo + Igama lezilungiselelo ze-Morphe + Okujwayelekile + Okwezengezwe + Ukulungisa + Isikhathi sokushintsha + Okukhethiwe + Faka igama + Igama alikwazi ukuba lingenalutho + Khombisa ama-icon ezilungiselelo ze-Morphe + Kukopishwe: %s + Ulimi lwe-Morphe + "Izihumushi zezilimi ezithile zingase zingabikho noma zingaphelele. + +Ukuze uhumushe izilimi ezintsha noma uthuthukise izihumushi ezikhona, vusela i-morphe.software/translate" + Ulimi lwe-App + Ngenisa / Khipha + Ngenisa / Khipha izilungiselelo ze-Morphe + Ngena efayilini + Izilungiselelo zingeniswe ngempumelelo, thinta uSave ukuze usebenzise + Kuhleleke ukungenisa izilungiselelo + Thumela kufayela + Izilungiselelo zithunyelwe ngempumelelo + Kwahluleka ukuthumela izilungiselelo + Usebenzisa inguqulo yakamuva ye-Morphe Patches <i>%s</i> + Usebenzisa i-Morphe Patches inguqulo <i>%1$s</i><br><br><b>I-update itholakala: <i>%2$s</i></b><br><br>Ukuze uvuselele ama-patches akho, sebenzisa i-Morphe ukuze uphinde u-patch le app + Isaziso + Le nguqulo iyingxenye yokukhishwa kwesikhathi esingaphambili futhi ungahlangabezana nezinkinga ezingalindelekile + Izixhumeli ezisemthethweni + Iwebhusayithi + Nikela + Izihumushi + Amakhredithi + Isikhathi esisemuva + Kukhona ukuthuthukiswa okutholakala + Ukuthuthukiswa kwamanje + Ukuthuthukiswa kwangaphambilini + Ukuthuthukiswa okuphaphamile + 2022 ‑ 2025 + 2022 ‑ 2025 + 2019 ‑ 2022 + Izaziso zelayisense + Kulayidwa… + Ukulungisa amaphutha + Lungisa izinketho ezithuthukile zomthuthukisi namabhuku okuhlolwa + Vula ukubhala ama-log okuhlola + I-Debug log stack trace + Kufaka ama-stack traces ema-log okuhlola ukuze kusize ekubonakaliseni amaphutha + Khombisa isaziso se-toast lapho kukhona iphutha le-Morphe + "Ukucima ama-toast amaphutha kufihla zonke izaziso zamaphutha ze-Morphe. + +Angeke waziswa nganoma yiziphi izigameko ezingalindelekile" + Thumela ama-debug logs ngaphandle + Thumela ama-debug logs e-Morphe ngaphandle + Ama-debug logs athunyelwe ngaphandle ngempumelelo + Kuhlulekile ukuthumela ama-debug logs ngaphandle + Funa ema-log okuhlola… + Sula + Ukubhala ama-log okulungisa amaphutha kukhishwa + Ayitholakali ama-debug logs + Ama-debug logs akopishwe + Kuhlulekile ukuthumela ama-debug logs ngaphandle: %s + Ama-debug logs asulwe + Imenenja yama-feature flags + Phatha ama-boolean feature flags + Funa ama-feature flags… + Ayisebenzi (%s) + Iyasebenza (%s) + Ivinqiwe (%s) + Kuphezelwe (%s) + Okuzenzayo + Thwaliswa + Vula + Phushela phezulu + Vala + Susa + Ama-feature flags agcinwe + Izimpawu zezici sezibuyiselwe emuva + Izimpawu zezici sekukopiwe ku-clipboard + Qala futhi uhlelo lokusebenza ukuze usebenzise le setting + Thumela futhi ulethe + Kulethwe izinkomba zezici ezi-%s + Akukho feature flags ezitholakale + Ama-feature flags akhiyiwe ngempumelelo + Kwehlulekile ukukhipha ama-feature flags + Ama-feature flags alandisiwe, chofoza uSave ukuze usebenzise + Kwehlulekile ukufunda ifayela + Ukusesha kwe-Binary + "Thola ukuthi iyiphi i-feature flag ebangela ukuziphatha kohlelo lwe-app. + +Ingxenye eyisigamu’u-half sama-flag awe-%s ayavalwe, futhi ngemva kokuvuselelwa kwalo lonke uxelele thina ukuthi ukuziphatha kusasekhona yini. + +Phinda ukuziphatha kanye ngaphambi kokuqala, ngaphandle kwalokho ama-flag ohlelo lwe-app angakacele azobe esekhona ekusesheni" + Qala + "Azikho izinkomba zezici zokufuna. + +Kuphela izinkomba ohlelo lwe-app oluzishayile futhi ezingenyiswe ngenkani njengamanje ezingasetshenziswa ekufuneni kwebinary" + "Isinyathelo %1$s + +Kusele izinkomba %2$s, u-%3$s kuzo uvimbelwe kulesi sinyathelo. + +Qala futhi i-app, hlola ukuthi ukuziphatha kukhona yini, bese uphendula ngezansi" + Ukufunwa kwezinkomba zezici kuyaqhubeka + Ukuziphatha kukhona + Ukuziphatha akukho + Khansela ukufuna kwebinary + Ukufuna kwebinary kukhanselwe + "Inkomba yesici %s ibangela ukuziphatha. + +Isivimbelwe manje" + "Kwenzeke ukuthi akutholakali ifecha flag. + +Ukuziphatha kohlelo lwama-app kubonakala ukuthi alulawulwa ifecha flag" + "Kwenzeke ukuthi akutholakali ifecha flag eyodwa. + +Ukuziphatha kuvele kwanyamalala ekuqaleni kokusesha, ngakho kungase kube nama-flag amaningi ahunyushwe noma yinye yizimpendulo yayingelona iqiniso" + Vula izilungiselelo ze-MicroG + IMicroG ayifakile. Iyifake + Kudingeka isenzo + "IMicroG v%1s yakho isidala futhi ingadala izinkinga. + +Sicela uthade iMicroG ivulelelwe ku-version yakamuva v%2s" + Vuselela + Yigunde + "IMicroG efakile ayiphikishane ne-Morphe: + +%1s + +Thinta ukuze uyisuse, bese uvula futhi lolu hlelo ufake iMicroG RE" + Susa ukufaka + "uMicroG akanalo imvume yokusebenza ngemuva. + +Landela umhlahlandlela we-\"Don't kill my app\" wocingo lwakho, bese usebenzisa imiyalelo ekufakweni kwakho kukaMicroG. + +Lokhu kubalulekile ukuze i-app isebenze" + Vula iwebhusayithi + "Okuhleleka kwebhethri kukaMicroG kumele kukhishwe ukuze kuvikelwe izinkinga. + +Ukukhisha okuhleleka kwebhethri kukaMicroG ngeke kube nomphumela omubi ekusetshenzisweni kwebhethri. + +Chofoza inkinobho eqhubekayo bese uvumela izinguquko zokuhleleka" + Qhubeka + Khombisa i-dialog yoku-optimize ibhethari ye-MicroG + "Ukukhubaza lokhu kuzofihla isicelo sokuthuthukisa ibhathri yeMicroG. + +Uma ukuthuthukiswa kwebhathri kungashintshwa ungase uhlangabezane nalokhu: +• I-App ingavala ngokuzenzela +• Izaziso zingase zingasebenzi +• Ukudlala ngemuva kungase kungasebenzi" + Ukuphathwa kwezingxenye ze-App + Lungisa ama-link ukuba avulwe kule app + Isinyathelo 1: Khubaza ama-link ohlelo lokuqala + Vula izilungiselelo zohlelo lokuqala bese ukhubaza wonke ama-domain links + Isigaba 2: Vula ama-link ohlelo olulungisiwe + Vula izilungiselelo zohlelo olulungisiwe bese uvula wonke ama-link e-domain + Vula izilungiselelo + 🚨 Isixwayiso Sezinto Ezisayizama 🚨 + "Usebenzisa inguqulo yohlelo lokusebenza esayizama ka ⚠️ <b>%1$s</b> +<br/><br/> +🔧 Linda ukuziphatha kohlelo lokusebenza okungajwayelekile noma amaphutha angaziwa ngenkathi silungisa ama-patches ale nguqulo yohlelo lokusebenza. +<br/><br/> +Uma ufuna uhlelo olungenazo izinkinga, <b>susa</b> lolu bese ufaka inguqulo ethulekile ekhuthazwayo ka ✅ <b>%2$s</b>" + ⚠️ Ngifuna ezisayizama + ✅ Ngifuna ezizinzile + I-log yokuphuka igcinwe emaqondeni omsebenzisi: %s + I-log yokuphuka igcinwe: %s diff --git a/patches/src/main/resources/addresources/values-zu-rZA/sponsorblock/strings.xml b/patches/src/main/resources/addresources/values-zu-rZA/sponsorblock/strings.xml index 9d2d4ab7d5..2ad3459533 100644 --- a/patches/src/main/resources/addresources/values-zu-rZA/sponsorblock/strings.xml +++ b/patches/src/main/resources/addresources/values-zu-rZA/sponsorblock/strings.xml @@ -16,7 +16,219 @@ Second \"item\" text" --> + Vula iSponsorBlock + iSponsorBlock uhlelo olusebenzisana nabantu ukuze kugwenywe izingxenye eziphazamisayo zemividiyo yeYouTube + Ukubukeka + Khombisa inkinobho yokuvota + Yengeza inkinobho yokuvota ngenhla noma ngaphansi kwezingxenye + Sebenzisa uhlelo oluyisikwele + Usebenzisa amakhona ane-square kuma-button kanye nokulawula kuka-SponsorBlock + Sebenzisa inkinobho yeSkip encane + Yinciphisa ububanzi benkinobho yeSkip ukuze kongeze isikhala + Fihla inkinobho ye-Skip ngokuzenzakalelayo + Ifihla inkinobho ye-Skip emva kwezizakunde ezimbalwa esikhundleni sokuyishiya ibonakala isikhathi sonke sendlela + Isikhathi senkinobho ye-Skip + Khetha ukuthi inkinobho ye-Skip ihlale ibonakala isikhathi esingakanani + Khombisa isaziso se-skip + Ikhombisa isaziso lapho indlela isukiwe ngokuzenzakalelayo + Isikhathi sesaziso se-skip + Khetha ukuthi isaziso sokukhansela ihlale ibonakala isikhathi esingakanani + 1 second + 2 seconds + izimiseko ezingu-3 + izimiseko ezingu-4 + izimiseko ezingu-5 + izimiseko ezingu-6 + izimiseko ezingu-7 + izimiseko ezingu-8 + izimiseko ezingu-9 + izimiseko ezingu-10 + Khombisa ubude bwevidiyo ngaphandle kwezingxenye + Kukhipha zonke izingxenye ekubeni kube yisikhathi sonke sevidiyo ekhonjiswe ku-seekbar + Ukudala izingxenye ezintsha + Khombisa inkinobho ethi \'Dala isigaba esisha\' + Yenza inkinobho kumdlali wevidiyo evumela ukuthi uthumele izigaba ezintsha ze-SponsorBlock + Isinyathelo sokulungisa isikhathi + Inani lokufuna lezinkinobho zokulungisa isikhathi (ngama-milliseconds) + Inani kumele libe yinombolo eyinhle + Imigomo yokuthumela + Imigomo iqukethe imithetho namacebo okwakha izigaba ezintsha + Landela imigomo + Funda imigomo ye-SponsorBlock ngaphambi kokwakha izigaba ezintsha + Sengifundile + Ngikhombise + Okujwayelekile + Khombisa izixwayiso zephutha lokuxhumana + Ikhombisa isaziso lapho iserba le-SponsorBlock ingatholakali + Vumela ukulandelela isibalo sokugxumela + Ithumela izibalo zokugxuma ku-SponsorBlock leaderboard ukuze kulandelele ukuthi isikhathi esingakanani siyongwe + Isikhathi esiphansi se-segment + Ama-segment amafishane kunalokhu (ngamasekhondi) ngeke abonakale noma kugxumwe kuwo + Isikhathi esingalungile + I-ID yakho yomsebenzisi eyimfihlo + Lokhu kumele kugcinwe kuyimfihlo. Lokhu kufana nephasiwedi futhi akufanele kwabelanwe nanoma ubani. Uma othile enalokhu, angazenza abe nguwe + I-ID yomsebenzisi eyimfihlo kumele ibe nobude obungayi phansi kwama-character angu-30 + Shintsha i-API URL + Ikheli iSponsorBlock elisebenzisa ukwenza izingcingo ku-server + Uhlu lwamachannel avunyelwe + iSponsorBlock idisabelwe ama-channel akuhlu lwemishini evunyelwe + Yazi ngezaziso ku-channel evunyelwe + Ikhombisa isaziso lapho iSponsorBlock idisabelwe ku-channel evunyelwe + iSponsorBlock idisabelwe kule-channel + Ukubuyiselwa kwe-API URL + I-API URL ayilungile + I-API URL ishintshile + Imisebenzi yokungenisa / yokukhipha izilungiselelo + Kopisha + Ukulungiswa kwe-SponsorBlock JSON okungeniseka / okukhipheka e-Morphe nakwezinye izinkundla ze-SponsorBlock + "Ukulungiswa kwe-SponsorBlock JSON okungeniseka / okukhipheka e-Morphe nakwezinye izinkundla ze-SponsorBlock + +Lokhu kufaka i-user ID yakho eyimfihlo. Qiniseka ukuthi uyihlanganyela ngokuhlakanipha" + Izilungiselelo zenziwe ngempumelelo + Kuhleleke ukungenisa: %s + Sihlulekile ukukhipha: %s + "Izilungiselelo zakho zinye i-SponsorBlock user ID eyimfihlo. + +I-user ID yakho ifana nephasiwedi, futhi akufanele yabelane nakanye" + Ungakukhombisi futhi + Shintsha ukuziphatha kwe-segment + Isifofolo + Ukukhutshekisa okukhokhelwa, ukurefera okukhokhelwa nokukhangisa okuqondile. Akukona ukuzikhangisa noma ukukhipha mahhala izinto / abadali / amawebhusayithi / imikhiqizo abayithanda + Ukuzikhangisa okungakhokhelwa + Kufana neSifofolo ngaphandle kokuzikhangisa okungakhokhelwa. Ihlanganisa izigaba ezimayelana nemikhiqizo, iminikelo, noma ulwazi ngokuthi babashiseka nobani + Isikhumbuzo Sokuxhumana (Bhalisa) + Isikhumbuzi esifushane sokulike, ubububhayibe noma ukubalandela phakathi okuqukethwe. Uma kuyinde noma imayelana nento ethile, kufanele kube ngaphansi kokuzikhangisa + Veza + Ingxenye yevidiyo abantu abaningi abayifunayo + Isikhala / I-animation yesingeniso + Isikhala esingenalo okuqukethwe kwangempela. Kungaba ukumisa, ifreyimu engashintshi, noma i-animation ephindaphindayo. Akufaki izinguquko ezinezululwane + Amakhadi okuphela / Amacredits + Amacredits noma lapho amakhadi okuphela e-YouTube evela. Akulona isiphetho esinezululwane + Isikhangiso / Izibingelelo + Ama-trailer axoxwe eveidiyweni ezayo, izibingelelo nokuvalelisa. Akufaki izahluko ezengeza okuqukethwe okwezinhlangothi + Ukubuka / Ukubuyekeza + Iqoqo lama-clip akhombisa okuzayo noma okwenzekile kuvidiyo noma kwezinye izividiyo ze-series, lapho yonke imininingwane iphindwe kwenye indawo + Izinto ezingashayi emhlolweni / Amahlaya + Izigcawu ezingashayi emhlolweni noma amahlaya angadingeki ukuze kuqondwe okuqukethwe okuyinhloko kwevidiyo. Akufaki izigaba zinikeza ingqikithi noma imininingwane yesizinda + Umculo: Isigaba Esingenalo Umculo + Kusetshenziswa kuphela kuvidiyo zomculo. Izigaba zezividiyo zomculo ezingenalo umculo ezingakabi sezifakwe kwenye ikhathi + Skip-a + Gqamisa + Skip-a sponsor + Skip-a promo + Dlula ukuxhumana + Giya kwi-highlight + Yishaye isingeniso + Skipha isikhala saphakathi + Skipha isikhala saphakathi + Skipha isiphetho + Skipha i-hook + Skipha isiboniso + Skipha isiboniso + Skipha ukubuyekeza + Skipha okungabalulekile + Skip okungeyizimculo + Skip isigaba + Kudlulwe isponsor + Kudlulwe ukuzikhuthaza + Kudlulwe isikhumbuzo esicasulayo + Kudlulwe kuya kokubalulekile + Kudlulwe isingeniso + Kudlulwe isikhala phakathi + Kudlulwe isikhala phakathi + Kudlulwe isiphetho + Kudlulwe i-hook + Kudliwe isibonelo + Isiboniso seskiphiwe + Kudlulwe kwingxenye efingqiweyo + Kudlulwe kwingxenye engabalulekanga + Kudlulwe kwingxenye engenalo iculo + Kudlulwe kwingxenye engakafakiweyo + Kudlulwe iziqeshana eziningi + Dlula ngokuzenzakalela + Dlula ngokuzenzakalela kanye + Khombisa inkinobho ye-Skip + Khombisa ku-seekbar + Khubaza + Awukwazi ukuthumela isigaba: %s + SponsorBlock uvale kancane + Akukwazi ukuthumela isigaba (isimo: %1$s %2$s) + Awukwazi ukuthumela isigaba. Rate Limited (iminingi ivela kumsebenzisi noma ku-IP eyodwa) + Awukwazi ukuthumela isigaba: %s + "Awukwazi ukuthumela isigaba. +Sekukhona" + Isigaba sithunyelwe ngempumelelo + SponsorBlock awutholakali okwamanje (API timed out) + I-SponsorBlock ayitholakali okwesikhashana (isimo %s) + SponsorBlock awutholakali okwesikhashana + Akukwazi ukuvota isigaba (i-API iphelile isikhathi) + Akukwazi ukuvota isigaba (isimo: %1$s %2$s) + Akukwazi ukuvota isigaba: %s + Vota ngenhla + Vota phansi + Shintsha uhlobo + Azikho izigaba zokuvotela + %1$s kuya ku %2$s + Khetha uhlobo lwesigaba + Uhlobo lucishiwe ezilungiselelweni. Vula uhlobo ukuze uthumele + Isigaba esisha se-SponsorBlock + Beka u-%s njengesiqalo noma isiphetho sesigaba esisha? + Ukuqala + Isiphetho + Manje + Isikhathi isigaba esiqalayo + Isikhathi isigaba esiphelayo + Izikhathi zilungile yini? + "Isigaba isuka ku + +%1$s +uya ku +%2$s + +(%3$s) + +Ukulungele ukuthumela?" + Thumela + Isiqalo kumele sibe ngaphambi kokuphela + Kuqala markha indawo kubha yesikhathi + Buka isiqeshana, futhi uqinisekise ukuthi sihamba ngokulula + Hlela isikhathi sesiqeshana ngesandla + Uyafuna ukuhlela isikhathi sokuqala noma sokuphela kwesiqeshana? + Isikhathi esingafanele esinikeziwe + Thumela ukugqamisa + "Isikhathi sokugqamisa siyashuqa? + +%s + +Uma ungathumeli ukugqamisa, thinta ukukhansela bese umarkha indawo yesibili" + Thumela i-highlight + Izibalo + Izibalo azitholakali okwesikhashana (i-API ayisebenzi) + Kulodwa… + SponsorBlock udisayizwe + Igama lakho lomsebenzisi: <b>%s</b> + Thinta lapha ukuze ushintshe igama lakho lomsebenzisi + Akukwazi ukushintsha igama lomsebenzisi: Isimo: %1$s %2$s + Igama lomsebenzisi lishintshiwe ngempumelelo + Isithunzi sakho ngu-<b>%.2f</b> + Udale iziqeshana ezi-<b>%s</b> + Thinta lapha ukuze ubone izigaba zakho + I-leaderboard ye-SponsorBlock + Usondile abantu ezigabeni ezingu-<b>%s</b> + Thinta lapha ukuze ubone izibalo zomhlaba nabashayela abakhulu + Lokho kuwukuthi i-<b>%s</b> yezinsuku zabo.<br>Thinta lapha ukuze ubone i-leaderboard + Usigashile izigaba ezingu-<b>%s</b> + Lokho kuwukuthi i-<b>%s</b> + Ususa isibalo sezigaba ezisigashiwe? + %1$s amahora %2$s imizuzu + %1$s imizuzu %2$s izimizuzwana + %s izimizuzwana + Ukucaca: + Umbala: + "I-SponsorBlock iyidatha-bezi yama-segment evidiyo eqoqwwe ngumphakathi, efana nabaxhasi, izingeniso, iziphetho kanye nezikhumbuzo zokubhalisa. Ama-segment ahambiselwa futhi avotelwe ngumphakasti, kanti idatha-bezi ivulekile futhi imahhala ukuyilanda + +I-ID yevidiyo oyibukayo ithunyelwa ku-SponsorBlock API ukuze uthole ama-segment ayo" diff --git a/patches/src/main/resources/addresources/values-zu-rZA/youtube/strings.xml b/patches/src/main/resources/addresources/values-zu-rZA/youtube/strings.xml index 9079108272..1cfc65031c 100644 --- a/patches/src/main/resources/addresources/values-zu-rZA/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-zu-rZA/youtube/strings.xml @@ -18,210 +18,1615 @@ Second \"item\" text" + Mayelana neMorphe + Izikhangiso + I-Feed + Okujwayelekile + Umdlali + Shorts + I-Seekbar + Izilawuli zoku-swipe + Okunye + Ividiyo + Buyisela isikrini sezilungiselelo zakudala zeYouTube + Susa imingcele yokudlala ngemuva + Vala ukudlala ngemuva + Yima ngokuzenzakalela lapho idivayisi ishidiwe + Thisha + Yima ngaso sonke isikhathi lapho kushidiwe + Yima ngaphandle kokuba umsindo we-wireless uxhumekile + Khulula umkhawulo wesivinyelo sokuskrola + Vimbela ukubuyekeza kwelayauti + Kuvimbela iserver ukuthi ithuthukise indlela ehleleke ngayo i-app + "I-layout ye-app ibuyela ku-layout eyayisetshenziswa lapho ifakwa okokuqala. + +Zinye izimo ze-layout ze-server-side zingase zingabuyi. + +Izinguquko zihlanganisa: +• Izinto ku-player flyout menu (noma izilungiselelo ezihlobene) zingase zingasebenzi +• Izinombolo eziguqukayo azizisebenzi +• I-Library tab iyabonakala +• Isigaba somculo sencazelo yevidiyo singase singasebenzi +• Izinto ze-player flyout menu azikwazi ukufihlwela +• Inkinobho yokushintsha i-account ingase ingabonakali ku-Library tab" + Vumela ifilitha yemenyu ye-flyout ye-feed + Ifilitha yemenyu ye-flyout ye-feed + Faka amagama e-flyout menu ofuna ukuhlunga ngawo, ahlukaniswe imigqa emisha + Vula ifiltha lemenu yezansi yeLibrary / You + Ifiltha lemenu yezansi yeLibrary / You + Faka amagama emenu yezansi yeLibrary / You okufanele uhlungwe ngawo, uhlukanise ngemigqa emisha + Fihla amakhadi e-album + Yifihla ama-album cards ngaphansi kwencazelo yomculi + Fihla amakhadi abadlali + Ifihla ikhadi lomculi phezulu emiphumeleni yokusesha + Fihla izinkinobho ze-channel + Fihla izinkinobho ezifana ne-Subscribe, View Channel, Community, kanye ne-YouTube Music kuma-profiles e-channel emiphumeleni yokusesha + Fihla izintshumayelo zomphakathi + Fihla amabanana amancane + Ifihla ama-banner amancane nesikhala phakathi kwezinye izinto + Amakhadi avulekayo ngaphansi kwamavidiyo + Khombisa konke + Fihla ikhadi kafushane kuphela + Fihla konke + Fihla inkinobho kamayikrofoni eluphuthumayo + Ifihla inkinobho kamayikrofoni ekhasini lokuphakamisa okuseshwayo ekunye kwesokudla ngezansi + Fihla inkinobho ethi \"Thola i-Premium\" + Fihla ishelufu yeMLando + Fihla amashilofathi ahambisana + Fifa ama-shelves ahamba ngokunye njenge-Breaking news, Community posts ongase uzithande, Continue watching, Explore more channels, Explore more topics, For You, Latest videos, Most relevant, Posts from Creator\'s Community, Shopping, kanye ne-Watch it again + Fihla ishilofathi lzithombe + Ifihla ama-shelf ezithombe emiphumeleni yokusesha + Fihla ikhadi elithi \'Mema abanye ukuba bathumele imiyalezo\' + Kufihla ikhadi lemisebenzi elikhonjiswa esigabeni semiyalezo kwi-tab yeZaziso + Fihla inkinobho ethi \'Amavidiyo amasha\' + Ifihla i-banner eluhlaza okuthiwa \'Amavidiyo amasha\' evela phezulu kwe-feed yakwaHome + Fihla ama-live stream + Ifihla amavidiyo aqala manje ku-feed yasendlini kanye nemiphumela yokusesha + Fihla uhlu lwe-mix + Fihla isigaba sama-movie + Fihla ikhanda lemenyu yezaziso + Ifihla ikhanda lemenyu yezaziso lapho uchofoza inkinobho yensimbi + Fihla inkinobho ethi \'Ngazise\' + Ifihla inkinobho yokukhumbuza izinkundla zokubhala eziphathilwe kanye nezokuqala + Fihla ama-Playables + Ifihla isici esikuvumela ukuthi udlale imidlalo + Fihla imifanekiso yegama lokusesha + Ifihla izibonelo ezincane ohlisteni lokuzaliseka kwesikhenti sokusesha + Fihla inkinobho ethi \'Khombisa okwengeziwe\' + Ifihla inkinobho yokwandisa ngaphansi kwevidiyo emiphumeleni yokusesha + Fihla ibha yamazinda obubhaliselwe kuwo + Fihla amagama ama-channel obizibhalisele kuwo + Fihla amagama ama-channel e-bar yama-channel obizibhalisele kuwo + Fihla imibuzo + Fihla imibuzo nokukhuthaza mayelana nokunemba kokunikezelwa kwe-feed + Fihla ishelufu yamathikithi + Fihla isikhathi sokukhuphula + "Imikhawulo: +• Amashika e-Shorts, amakhasi ezindlela, nemiphumela yokusesha angase akhombise izikhathi zokukhulethela +• Akusebenzi nohlelo lwe-automotive" + Fihla isithombe semvidiyo + Fihla amalabeli amavidiyo + Ifihla amalabeli afana nalawa athi \'Auto-dubbed\' / \'Dubbed\', \'Earns commission\', \'Hyped\', \'People also watched\', \'product(s)\' kanye no \'You might also like\' + Fihla isibalo sokubuka + "Imikhawulo: +• Amashika e-Shorts, amakhasi ezindlela, nemiphumela yokusesha angase akhombise izibalo zokubukwa +• Akusebenzi nohlelo lwe-automotive" + Fihla imiphumela yokusesha kuwebhu + Ifihla isigaba se-\'Imiphumela evela kuwebhu\' emiphumeleni yokusesha + Fihla isigaba sika \'Mhlawumbe ungathanda\' + Fihla ama-YouTube Doodles + Ifihla ama-animation esizini kanye nemicimbi kulogo lwe-YouTube + Ukukhombisa ama-YouTube Doodles kungase kuvimbe ama-custom headers ukuba abonakale + Fihla ibha yeshanali + Fihla i-watermark yeshanali + Fihla ama-Chapters kanye nenkinobho ethi \'Kuleli video\' + Fihla ibhokisi le-crowdfunding + Fihla amabhokisi ezigameko + Fihla amapaneliwolwazi + Ifihla amapaneli wolwazi, njengolwazi lwezempilo + Fihla inkinobho ethi Joyina + Fihla inkinobho ethi \'Ukuphinda ukuxoxa okubukhoma\' + Ifihla inkinobho yokudlala kabusha ingxoxo kwimo yefullscreen + Fihla amapaneli ezempilo + Fihla iziboniso zezandla zomdlali + Kufihla iziboniso ezikhonjiswa ngesikhathi sezandla zomdlali, njenge-\'Donsa phezulu ukuze ukhuphule ngokunembile\', \'Yiyise ukuze ukhansele\' kanye no-\'2x\' + Fihla i-snackbar + Fihla imigomo yababhalisele + Fihla inkinobho ethi \'Sync to video\' + Fihla i-gradient yokuthinta nokubamba + Ifihla i-gradient yokulawula isidlali lapho usetshenzisa umunwe wakho ubambe ukuze usheshise + Fihla izimo zezikhathi + Ifihla ifuphi okuvumela abasebenzisi ukuba baphendule ngama-emoji esigabeni sokuphendula nangoxoxweni lokuqondile + Fihla imenyu ye-\'Usizo & imibono\' + Ifihla imenyu ye-\'Usizo & imibono\' lapho usebenzisa inkinobho yezichashazi ezintathu, njengakuimiphumela yokusesha + Fihla i-handle + Fihla i-handle ye-channel yakho kuthebhu elithi \'Wena\' + Fihla isihloko sevidiyo + Ifihla isihloko sevidiyo kwimo yefullscreen + Hlanza izincazelo zezithombe ze-video + Isusa amagama afana no \'#\', \'Fundraiser\', \'product(s)\', kanye no \'Shop\' ezinqubeni zezincazelo zezividiyo + Fihla inkinobho ethi \'Buza uGemini\' + Ifihla inkinobho ethi \'Buza uGemini\' phansi kwephaneli ethi \'Buza ngaleli vhidiyo\' + Incazelo yevidiyo + Khetha ukuthi yiziphi izici okufanele zivela encazelweni yevidiyo + Fihla isigaba se-\'Isifingqo sevidiyo esenziwe yi-AI\' + Fihla isigaba se-Ask + Fihla isigaba se-Attributes + Kufihla izigaba ze-Featured places, Games, Music, kanye ne-People mentioned + Fihla isigaba sezixhumelo ze-Channel + Fihla isigaba se-Chapters + Fihla isigaba se-Corrections + Fihla isigaba se-\'Course progress\' + Fihla isigaba se-Explore + Kufihla izigaba ze-Explore this course kanye ne-Explore the podcast + Fihla isigaba se-\'Explore this course\' + Fihla isigaba se-\'Explore the podcast\' + Fihla isigaba samamashaneli aveziwe + Fihla isigaba sezixhumelo eziveziwe + Fihla isigaba sezilu ze-Featured playlists + Fihla isigaba amavidiyo aveziwe + Fihla isigaba se-Hashtag + Fihla isigaba esithi \'Indlela lokhu okwenziwe ngayo\' + Fihla amaphuzu e-Hype + Fihla isigaba samakhadi wolwazi + Fihla isigaba se-\'Key Concepts\' + Fihla isigaba semibuzo + Fihla isigaba sika \'Search inside this video\' + Fihla inkinobho ye-Subscribe + Fihla isigaba sombhalo wevidiyo + Fihla isigaba se\"Imininingwane yevidiyo\" + Ibhala loku-filter-a + Khetha lapho kufanele kufihwe khona ibha yohlelo lokuhlunga esukumelayo + Fihla ekhasini leneyibala + Fihla emibhalweni + Fihla ema-feed + Fihla amavidiyo ahlobene + Fihla emiphumeleni yokusesha + Fihla emlandweni wokubuka + Ikhasi leshanneli + Khetha ukuthi yiziphi izahlkazi ezivela ekhasini lenqana + Vumela ifilitha ye-tab yeshanneli + Ifilitha ye-tab yeshanneli + Faka amagama ama-tab enquaneni ofuna ukuhlunga ngawo, ahlukaniswe imigqa emisha + Fihla inkinobho ye-Community + Fihla inkinobho ye-Join + Fihla ukubukelwa kwezixhumanisi + Fifihla izixhumelo zewebhu ezichofoza ekhanda lenqana + Fihla i-chip ethi \'Members only\' + Ifihla i-chip ethi \'Amalungu kuphela\' kubha yokuhlunga imishayelo + Fihla ishelufu yamalungu + Fihla uhlu lwamalungu enquaneni + Fihla ishelufu yezithupha + Fihla inkinobho yeIsitolo + Fihla inkinobho yeBhalisa + Imenyu ye-Explore + Khetha ukuthi yiziphi izinto ezivela ohlwini lokuhlola + Fihla imenyu yeZifundo + Fihla inkinobho ye-Explore + Fihla imenyu ye-\'Fashion & Beauty\' + Fihla imenyu yeGaming + Fihla imenyu yeHype + Fihla imenyu ye-Learning + Fihla imenyu yeLive + Fihla imenyu yeMemberships + Fihla imenyu ye\'Movies & TV\' + Fihla imenyu yeMusic + Fihla imenyu yeNews + Fihla imenyu ye-Playables + Fihla imenyu ye-Podcasts + Fihla imenyu ye-Shopping + Fihla imenyu ye-Sports + Fihla imenyu ye-YouTube Premium + Fihla imenyu ye-YouTube Studio + Fihla imenyu ye-YouTube Music + Fihla imenyu ye-YouTube Kids + Fihla imenyu ye-YouTube Create + Fihla imenyu ethi \'How YouTube works\' + Fihla izinyawo zomthetho nodabuko + Imibhalo + Khetha ukuthi yiziphi izici ezivela esigabeni semicabango + Vula i-comments carousel filter + "• Qiniseka ukuthi ungena igama eliqondile le-item ye-carousel (isib., Ask, Hype this video, Listen on YouTube Music) +• Ukufihla ama-item e-carousel kungaba nemiphumela engahleliwe +• Uma ama-item e-carousel wonke efihliwe, nesigaba sonke semicomment sizofihliwa" + I-comments carousel filter + Faka amagama e-carousel yemicabango ofuna ukuhlunga ngawo, ahlukaniswe imigqa emisha + Fihla isifinyezo se-AI chat + Fihla imigomo yeshanneli + Fihla imilayezo yengxenye + Fihla ikhanda elithi \'Imibhalo yamalungu\' + Fihla isigaba sokuphawula + Fihla isigaba sokuphawula ku-feed yaseKhaya + Fihla Imigomo yoMphakathi + Fihla inkinobho ethi \'Dala i-Short\' + Fihla inkinobho ye-Dislike + Fihla inkinobho ye-Emoji + Fihla izinketho zebha yokuhlunga + Ifihla zonke izinketho zebha yokuhlunga ngaphandle kwe-\'Top\' kanye ne-\'Newest\' + Fihla imizekeliso yezipho namakhadi + Fihla inkinobho Yesipho + Fihla inkinobho ye-Info + Fihla ibha labanikezi be-live chat + Fihla inkinobho yokuskrola phansi e-live chat + Fihla imiyalelo yengxoxo eqondile + Ifihla ama-tooltip popups akhonjiswa eduze kwenyezi lokufaka imilayezo yengxoxo, njengesaziso sengxoxo yababhalise kuphela + Fihla inkinobho yeMenu + Ifihla inkinobho ye-menu yama-dot ayi-3 yakho yonke imibono + Fihla ukubuka okufushane okuphawulayo + Ifihla ukubukela imicabango ngaphansi kwevidiyo ngaphambi kokuvula isigaba semicabango esigcwele + Ukuphawula kufihliwe ngu-Morphe… + Vumela inkinobho yokuphawula okuncane + Ifihla amakhadi ahambayo kanye nesikhethelo sekhasi saso, okubonakala enkinobhweni yokuphawula + Fihla inkinobho ye-Thanks + Fihla inkinobho yesikhathi + Fihla inkinobho ethi \'Abafani abavelele\' + Fihla inkinobho yokuhumusha + Fihla umlayezo othi \'Ayikho imibono okwamanje\' + Hlanza ama-link okusesha agundwe + Ihlanzanisa ama-link okusesha agundwe, ishiye kuphela Umbhalo ovamile wokuqala + Ihlolo lamagama angukhiye + Khetha amagama noma imisho ethile ukuze ufihle amavidiyo ema-feed nasokusesheni + Fihla imibhalo ngokwamagama okhiye + Fihla amavidiyo e-Home ngamagama angukhiye + Fihla imiphumela yokusesha ngamagama angukhiye + Fihla amavidiyo e-Subscriptions ngamagama angukhiye + Amagama angukhiye okufihla + "Faka amagama angukhiye namagama okufihla, ahlukaniswe imigqa emisha + +Amagama angukhiye angaba yizinhlobo zonke zombhalo ezikhonjiswa ezihloko zezividiyo noma emagameni emishini + +Amagama anezinhlamvu ezinkulu phakathi kumele ahambisane ngqo (isib. iPhone, TikTok, LeBlanc)" + "Ikhaya / Izinsizakalo / Ukusesha / Imibhalo ihlukaniswa ukuze kufihwe okuqukethwe okufana namagama okhiye + +Imingcele: +• Ama-Shorts aweke kufihleke ngegama leneyibala +• Ezinye izici ze-UI zingase zingafihleki +• Ukusesha igama lokhiye kungase kungakhombisazi imiphumela" + Fanela amagama aphelele + Ukuvala igama elibalulekile/isisho ngezimpawu zokuqondisa ezimbili (double-quotes) kuzovimbela ukutholakala kwezingxenye zezihloko ze-video namagama ama-channel<br><br>Isibonelo,<br><b>\"ai\"</b> kuzofihla i-video: <b>How does AI work?</b><br>kodwa ngeke ifihle: <b>What does fair use mean?</b> + Awekukwazi ukusebenzisa igama elibalulekile: %s + Faka izinto zokuqondisa ukuze usebenzise igama elibalulekile: %s + Igama elibalulekile linemimiso ephikisanayo: %s + Igama elibalulekile lifushane kakhulu futhi lidinga izinto zokuqondisa: %s + Igama eliyisihloko lizofihla zonke izividiyo: %s + "• Ikhaya: %s +• Izinsizakalo: %s +• Sesha: %s +• Imibono: %s +• Isizinda: %s" + Izibalo + Amavidiyo afihliwe, amahora angu-24 adlulile + Amavidiyo afihliwe, sonke isikhathi + Buyise kabusha isibalo samahora angu-24 adlulile? + Buyise kabusha isibalo saso sonke isikhathi? + Ihlolo lamazhelu e-AI + Fihla amavidiyo emizhelwini emakiwe njengokuquketha okwakhiwe yi-AI nguhlu lwe-AiSList community + Uhlu lokuvimba: Ukwethemba okuphezulu + Uhlu lokuxwayisa: Ukwethemba okuphakathi + Fihla amavidiyo ku-Home + Fihla amavidiyo ekusesheni + "• Ikhaya: %s +• Ukusesha: %s +• Isibalo: %s" + Imishanini evimbile + Imishanini exwayiswe + Vula uhlu lwehlungiselelo ukuze kulayishwe imishanini + Kulayishwa imishanini… + "Imiphumela yekhaya nokusesha ihlungwe ukuze kufihwe amavidiyo avele emishanini emakiwe yi-AiSList + +Imingcele: +• Ama-AI Shorts ahlungiwe angase avele njengesikhala esingenalutho +• Amanye amavidiyo e-AI angase aqhubeke nokuvela" + "iAiSList uhlu lwe-open source lwezingaelamane zeYouTube ezishicilela okuqukethwe okukhiqizwe yi-AI, olugcinwa abazisebenzi. Izingaelamane zibika abasebenzisi futhi zibuyezwa ngaphambi kokuba zengezwe + +Uhlu lulandwa ku-GitHub futhi amavidiyo ahlukaniswa kudivayisi yakho" + Bika ama-channel e-AI + Yengeza inkundla yokubika kumenyu yevidiyo + Yenge okuthile kumenyu yevidiyo okubika ishネル yevidiyo ku-AiSList + Igama lomuntu obikayo + Igama elibonakala eduze yezibiko zakho ebhodini lemiqopheliso yomphakathi lwe-AiSList. Ukubika akukho lapho lokhu kulishuthe + Bika ishネル ye-AI + "Thumela u-%s ku-AiSList, usebenzisa le vidiyo njengobufakazi? + +Bika kuphela izhネル ezi: +• Sebenzisa amazwi, imifanekiso, imibhalo noma umculo we-AI njengokuquketha kwazo okuyinhloko +• Zikhiqiza okuquketha okuningi ngamathuluzi we-AI kanye nomsebenzi omncane womuntu + +Ungabiki izhネル ezixoxa kuphela nge-AI, noma ezisebenzisa i-AI ezincinisweni ezithile zevidiyo zazo. + +Izibiko zivulekile futhi zibuyekezwa ngaphambi kokuba ishネル yengeke ohlwini. Ukususa kungenzeka kuphela ngokucela ku-GitHub" + Le channel isivele isisehlu lwokuvimbela lwe-AiSList + Le channel isivele isisehlu lwezexwayiso lwe-AiSList + Bika + Beka igama lomuntu obika ezilungelweni zokuhlunga i-AI channel ukuze ubike ama-channel + Kufunwa i-channel… + Awukwazi ukuthola i-channel yale video + I-channel ibikwe futhi ilindele ukubuyekezwa + AiSList ayikwazanga ukulondoloza umbiko, zama futhi kamuva + Imibiko eminingi kakhulu ithunyelwe, zama futhi kamuva + AiSList yenqabe umbiko + Umbiko uhleleke, hlola uxhumano lwakho + Uhlu lwezikhanyiselo zezindlela ezivunyelwe + Amavidiyo ezindlela eziku-whitelist adlala nezikhanyiselo, ukuze abenzi wazo bakhokhelwe. Izikhanyiselo zisala zivimbiwe kuzo zonke ezinye izindlela + Yengeza indlela ohlu lwezikhanyiselo ezivunyelwe + Susa ishanalini ohlelweni lwe-whitelist lwezikhangiso + Fihla ishelfu yesitolo somdali + Fihla ibhena lesitolo sekwinqaba yokugcina + Fihla izikhangiso ezijwayelekile + Fihla amabhena emikhiqizo + Fihla izikhetho zokukhangisela ekhokhelwayo + Fihla izikhangiso ze-popup ze-player + Fihla amakhadi azixikizelele + Fihla izixhumelo zokuthenga + Fihla izikhangiso zezividiyo + Fihla imikhangiso ye-YouTube Premium + + + Ishanali yamanje: + Yengeza ohlweni lwe-whitelist + Susa + Izishanali ezise-whitelist + Azikho izishanali ezise-whitelist + Ishanali yengeziwe ohlweni lwe-whitelist + Ishanali isusiwe ohlweni lwe-whitelist + Sesha phakathi kweneyibala + Yengeza inkinobho yokusesha yesibili emakhasi emishayelo ukuze useshe kuleso sizathu kuphela + Sesha ishannel + Imiphumela yokusesha kweshannel + Akukho okutholakele kule neyibala + Akukwazanga ukusesha le channel + Amavidiyo + Izithupha + Azikho izithupha kule channel ezihambisana nalokhu + Ukufaneleka + Ebukwe kakhulu + Ebukwe kancane + Ezintsha kakhulu + Ezindala kakhulu + Efushane kakhulu + Elide kakhulu + I-link ikopishwe ku-clipboard + I-link enesikhathi ikopishwe ku-clipboard + Khombisa inkinobho ye-Copy video link + Thinta ukuze ukopishe i-link yevidiyo, thinta ubambe ukuze ukopishe isikhathi + Kopisha isikhathi ngokuzenzakalelayo + Kushintsha izenzo zokuthinta nokuthinta-ubambe zenkinobho ye-\'Copy video link\' + Kopisha isihloko sevidiyo + Thinta ubambe isihloko sevidiyo ukuze usikopishe + Isihloko sevidiyo sakopishwe ku-clipboard + Kopisha imibono + Thinta ubambe umbono ukuze uwukopishe + Umbono ukopishwe ku-clipboard + Thinta ubambe ukuze uvule imenyu yevidiyo + Ukuchofoza nokubamba noma kuphi ku-feed yevidiyo kuvula imenyu yayo + Khombisa inkinobho ethi Buka kamuva kumifanekiso eyisikhombiso + Inkinobho ethi Buka kamuza ikhonywa emifanekisweni eyisikhombiso yama-feed evidiyo. Ukuchofoza nokubamba kuvula ukulonda ohlwini lwevidiyo + Landa izithombe zezithombe + Inkinobho yokulanda ibonakala eduze nenkinobho yokuabelana kwezithombe noma ama-GIFs, futhi ithemba lokulanda liyengezwa kumenyu yawo + Landa izithombe + Landa isithombe + Kulandwa izithombe ezingu %s… + Kulandwa isithombe… + Izithombe ezingu %1$s zigciniwe ku %2$s + Isithombe sigciniwe ku %s + Kukwazalekanga ukulanda isithombe + Alikho isithombe esitholakala kule post + Yenge item ye-Ask emenyu ye-flyout + Yenge item yokufingqa ividiyo emenyu ye-flyout ye-feed + "Ukuze usebenzise iBuza, kudingeka ube ne-akhawunti engenile ezweni lapho iBuza itholakala khona. + +Ngemuva kokuvulwa kwevidiyo enenkinobho ethi Buza kanye, le mpawu isebenza kuzo zonke izividiyo ngisho noma izwe lakho lanje lingenalo isici sokuBuza." + Fingqa ividiyo + Kukwazekile ukuvula iBuza kule vidiyo + IBuza ayitholakali ku-akhawunti yakho noma ezweni lakho lamanje + Khombisa isibalo sokubukwa kuma-thumbnails + Isibalo sokubukwa kwamavidiyo e-feed sikhonjiswa phansi ngesobunxelebayo kuma-thumbnails azo esikhundleni sokuba sibe ngaphansi kwesihloko. Amanye amavidiyo, njengalawo ase-shelves, angase angakhombisanga isibalo sokubukwa + Khombisa isibalo se-like se-thumbnail ye-feed + Khombisa iphesenti le-like se-thumbnail ye-feed + Sesha emibhalweni + Thinta inkinobho yokusesha emibhalweni yokuphawula ukuze ubonise kuphela imibhalo yokuphawula elandile equkethe zonke amagama afunwayo. Faka amagama ezinkomiziyweni ukuze useshe isivuselo + Sesha emibhalweni yokuphawula + Amagama okusesha + Sesha + Sula + Shintsha indlela yokuhlela noma uvule futhi imibhalo yokuphawula ukuze uyiseshe + Ukusesha kusuliwe. Shintsha indlela yokuhlela noma uvule futhi imibhalo yokuphawula ukuze ubonise yonke imibhalo yokuphawula + Vimba i-popup ethi \'Ngena ku-TV\' + Vala i-popup ethi \'Xhumana amadivayisi akho\' + Khubaza ukugxuma kwe-chapter ngokuthinta kabili + Idisable i-gesture yokuthinta kabili ngeminwe emibili edlula izahluko, okungase kwenzeke ngenyipho ngumnwe owodwa + Landa ama-video, ama-podcast, noma amahubo ngama-app okulanda aqhamuka ngaphandle + Khombisa inkinobho yokudownloada e-player overlay + Khombisa inkinobho ku-video player evula i-external downloader yakho + Dlala ngokushesha lapho uchofoza + Izikhuthazo zokulanda ngemuva. Thinta ubambe ukuvula isidlali sokulanda. Isebenza ne-YTDLnis ne-YTindir + Markha ukulanda njengokubukwayo + Amavidiyo alandwe ngesidlali sokulanda sangaphandle angezwa emlandweni wakho wokubuka we-YouTube + Full HD (1080p) + Ephakathi (192 kbps) + Ukulanda okusheshayo + Ividiyo + Umsindo + Zonke izinketho + Ukuhumusha ngezwi + Funda imibhalo yevidiyo ngokuzwakalayo usebenzisa i-text-to-speech edivaisini, ihambisana nokudlalwa kwevidiyo + Ikhombisa inkinobho engaphezu kwevidiyo kudlala-vidiyo. Thinta ukuvula/ukuvala ukuhumusha, thinta ubambe ukuze uthole izilungiselelo ezisheshayo + Ulimi + Umhlinzeki wokuhumusha + Izvoliyomu ze-audio yokuqala + Izvoliyomu ze-audio ye-YouTube yokuqala ngenkathi ukuhumusha kukhuluma + Ivoliyomu yokuhumusha + Izvoliyomu zezwi lokuhumusha elidubbiwe + Isilinganiso esiphezulu sokukhuluma + Isibalo esiphezulu sejubane lapho kufakwa inkulumo esikhundleni sezincazelo + "I-API yokuhumusha yamahhala, ayidingeki i-API key. Kuze kube ngabalingiswa abangu-5,000 ngosuku ekohlelweni lwamahhala, noma kuze kube ngabalingiswa abangu-50,000 ngosuku ngelayisense ye-imeyili. + +Thinta lapha ukuvula i-MyMemory.translated.net" + I-imeyili ye-MyMemory + Kuyinketho. Faka ikheli lakho le-imeyili le-MyMemory ukuze ukhulise umkhawulo wosuku kusuka kubalingiswa abangu-5,000 kuya kwabangu-50,000. Shiya kuyinhlanga ukuze usebenzise umkhawulo ongaziwa + "Ukuhumusha kwe-AI okukhokhwa ngayo nge-OpenRouter. Khokha kanye uhumushe izinkulungwane zamavidiyo. IMistral Nemo yindlela eshibhile kakhulu, i-Gemma 4 inikeza izinga eliphezulu kakhulu. + +Thinta lapha ukuvula i-OpenRouter.ai" + Ikey ye-OpenRouter API + Ikey yakho ye-OpenRouter API. Yakha ikey ku-OpenRouter.ai > Workspace > API Keys + I-Model ye-OpenRouter + Okukhethekile + I-OpenRouter ayikalingekiwe + Faka ikey yakho ye-OpenRouter API kuzilungiselelo ngaphambi kokukhetha lo mhlinzeki + Mahhala + %s ngamahora ayi-100 evidiyo + Izwi + Uhlelo + Indoda ekhuluma izilimi eziningi + Owesifazane okhuluma izilimi eziningi + Indoda + Owesifazane + Isikhathi sokushintsha! + Ihuhlo alitholakali + "Inkonzo yokuhumusha ibuyise iphutha le-HTTP %s. Ikheli lakho le-IP kungenzeka livalelwe imizuzu engaphezu kwehora elilingana namahora angu-24. + +Ukuze uqhubeke usebenzisa izihumushi, shintshela komunye ohlinzeka ngokuhumusha ezilungelweni noma usebenzise i-VPN" + Ungakhombisi futhi + "Izilungiselelo zakho zinokhiye we-OpenRouter API. + +Okhiye wakho we-API akufanele wabelane nakube yikuphi. Noma ubani onokufinyelela kuwo angasebenzisa ama-credits akho e-OpenRouter" + Umkhawulo wama-credit e-OpenRouter ufinyelele + Umkhawulo wakho wama-credit e-OpenRouter ufinyelele. Nyusa ibhalansi yakho noma ukhulise umkhawulo wokusebenzisa okhiye ku-OpenRouter.ai + Vula i-OpenRouter.ai + Ukuhunyushwa kwe-OpenRouter kuhlelele (HTTP %s) + Umkhawulo wansuku i-MyMemory ufinyelele + Okulandelayo kutholakala ngo %s + Usebenzise iquota yamahhala yokuhunyushwa kwe-MyMemory yanamuhla (izinhlamvu ezi-5,000). Faka i-imeyili ye-MyMemory ezilungiselelweni ukuze unyuse umkhawulo wansuku ube izinhlamvu ezi-50,000, noma shintshele enye isevisi yokuhunyusha + Usebenzise iquota yokuhunyushwa kwe-MyMemory yanamuhla (izinhlamvu ezi-50,000). Shintshele enye isevisi yokuhunyusha noma uzame futhi kusasa + Ukuhumusha kwe-MyMemory kuhleleke (isimo %s) + Vumela ukuphindaphinda kwevidiyo + Ungakhumbuli ukuphindaphinda kwevidiyo + Vimbela isimo samanje sokuphindaphinda kwevidiyo ukuthi sigcinwe kunevidiyo ezilandelayo + Khombisa inkinobho ye-Loop video + Ukuphindaphinda kwevidiyo kusebenza + Ividiyo elingapheli ayisebenzi + Ibanga lokuguqukela + Qala + Phela + [hh:]mm:ss + [hh:]mm:ss (okungabi namsindo = ukuphela kwevidiyo) + Beka + Sula + Kuphindaphinda %1$s - %2$s + Isikhathi esingafanele + Khombisa inkinobho ye-Picture-in-Picture + Khombisa imenyu ye-Picture-in-Picture + Shinthela ku-Picture-in-Picture + Khombisa inkinobho ye-Play all + "Thinta ukuze wakhe i-playlist yazo zonke izividiyo ze-channel, thinta ubambe ukuze ukhise + +Ulwazi: Kungase kungasebenzi kuma-live streams" + Imode yokukhiqiza uhlu lwezingoma + Okuqukethwe konke (Hlela ngokwesikhathi, ukunyuka) + Okuqukethwe konke (Hlela ngokwesikhathi) + Konke okuqukethwe (Hlela ngokudumile) + Amavidiyo kuphela (Hlela ngesikhathi) + Amavidiyo kuphela (Hlela ngokudumile) + Shorts kuphela (Hlela ngesikhathi) + Shorts kuphela (Hlela ngokudumile) + Amavidiyo ethunyelwe ngokubثika kuphela (Hlela ngesikhathi) + Amavidiyo ethunyelwe ngokubثika kuphela (Hlela ngokudumile) + Okuqukethwe kwamaMember kuphela + Amavidiyo amaMember kuphela + amaShorts amaMember kuphela + Ukugqiza okubukhoma kwamaMember kuphela + Awukwazi ukwakha uhlu lwezinto ezidlala ngenxa yokungahambisani kwe-ID yomhubo + Khombisa inkinobho ye-Reload video + I-ID yevidiyo ayitholakali. Sicela ulayishe evidiyo kabusha ngokuzenzela + Khombisa inkinobho yeBhala ukuze ubuke kamuva + Thinta ukuze ubhale ukuze ubuke kamuza, thinta ubambe ukuze uthole izinketho ze-queue + Ividiyo igcinwe ngempumelelo ohlwini lwevidiyo ozobuka kamuva + Le vidiyo isivele isigcinwe ohlwini lwevidiyo ozobuka kamuva + Akwazi ukugcina le vidiyo ohlwini lwevidiyo ozobuka kamuva + Bhala ukuze ubuke kamuva + Khombisa inkinobho yeSave to Watch later ekuhambelaneni nezilungiselelo zomdlali + Kuvumela ukuvimba ividiyo obukayo njengamanje ku-\'Buka kamuva\' kusuka ku-flyout yezilungiselelo ze-vidiyo + Khombisa inkinobho yeBhala ukuze ubuke kamuva kuvidiyo zezingane + Kuvumela ukuvimba ama-vidiyo ahlobene nezingane ku-\'Buka kamuva\' kusuka ku-flyout menu + Khombisa inkinobho ye-Save to Watch later ku-shorts flyout + Kuvumela ukuvimba ama-shorts ku-\'Buka kamuva\' kusuka ku-flyout menu + Yengeza ohlwini lweMorphe + Gcina uhlu lokulinda phakathi kwezikhathi zokusetshenziswa + Uhlu lokulinda lugcinwa ngemva kokuvula futhi uhlelo lokusebenza + Shintshisa izenzo ze-Save ne-Queue + Thinta ukuze uthole izinketho ze-queue, thinta ubambe ukuze ubhale ukuze ubuke kamuva + Dlala okulandelayo ohlwini + Okujwayelekile + Uhlu lwemveli + Uhlu lwe-Morphe + Uhlu lwe-Morphe ohlangeni lomsebenzisi + Yenge i-item yemenyu yohlelo lwe-queue lwe-flyout lwe-feed + Yenge i-item ye-flyout eyengeziwe. Vumela lokhu uma \"Yenge ku-queue\" ingabonakali + Ums 줄u + Umsizi (%s) + Sula konke + Ayikho into ekulandelaneni + Sebenzisa uDlala okulandelayo ekulandelaneni kunoma iyiphi ivhidiyo + Kudlalwa manje + Dlala okulandelayo ekulandelaneni + Dlala okugcina ohlelweni + Hlela kabusha + Susa + Kwenyeziwe ekulandelaneni + Kukwazekile ukwengeza le vidiyo ohlelweni + Uhlu lwezingoma alukwazi ukwengezwa ohlelweni + I-Queue igcwele + Kwenzeke ukuthi i-video ingaqali + I-whitelist yezikhangiso ku-flyout menu + Yengeza ithemba loku-flyout elivumela izikhangiso ukuthi zidlale eshanalini lalo video + Le feature kungase kudingeke ukusebenzisa iPotHelper noma ukushintsha u\"Spoof video streams\" kube i-TV noma i-Android VR sign-in + Uhlu lwezikhathi ezivunyelwe kumenyu yeflyout + Yenge item yeflyout edlala njalo ishayisheli levidiyo ngesivinyane sika 1.0x + Awunangena + Alikho i-playlist ID + Uuhlelo lokulinda alunalo lutho + Ayikho i-ID yevidiyo + Kuhleleke: %s + Kuhleleke ukwengeza ohlelweni + Kuhleleke ukwakha ohlelo + Kuhleleke ukususa ohlelweni + Kuhleleke ukugcina ohlelo + Kwengeziwe ohlelweni + Ohlelo lwadalwe + Kususwe ohlwini + Ohlelo lugcinwe ku-%s + I-Queue yeMorphe + Fake ohlelweni + Yengeza ohlelweni uvule + Yengeza ohlelweni udlale + Yengeza uvuselele ividiyo + Susa ohlelweni + Susa uvule uhlelwu + Susa uvuselele ividiyo + Vula uhlelwu + Gcina umsTextChanged ku-playlist + Uhlu lwezingoma lolondile + Kugcinwe ku-%s + Sula umsindo + Umsindo usulwe + Umsindo + Dlala okugcina emsindweni + Dlala okulandelayo emsindweni + I-YouTube ayikwazi ukukhombisa umsindo. + Umsindo awulungile kule video. Vula isidlali se-video uzame futhi. + Yengeza i-Mix emsindweni noma ubuke i-Mix? + Buka + Yengele ku-queue + Kulayidwa iMix… + Vula i-channel kusuka ku-live avatar + Ivimbela i-live stream ukuba ivulwe lapho uthinta i-avatar ye-channel + Vala isenzo sokusesha ngcqane + Idisable i-precise seeking mode esebenza lapho udonsa i-seekbar phezulu + Vumela i-DVR yohlelo lokubhala oluqondile + Ivumela ukufuna kuzo zonke izinkundla zokubhala eziqondile, ngisho nalezo ezivame ukukuvimbela + Vula ama-thumbnails e-seekbar + Ikhombisa i-thumbnail yevidiyo ngenkathi udonsa i-seekbar + Kudlalwa kakhulu + "'Vumela i-DVR yohlelo lokubhala oluqondile' kungase kungatholakali + +Ukuze ukulungise lokhu, vula 'Spoof video streams' bese ushintsha 'Default client' ibe 'TV Simply' noma 'visionOS'" + Andisa isikhathi se-DVR yohlelo lokubhala oluqondile + Ikuvumela ukuba ubuyisele ukukudlala kokuthumela okubukhoma ukuya emuva imizuzu engama-7 + "'Andisa isikhathi se-DVR yokuthumela okubukhoma' kungenzeka kungatholakali + +Ukuze ulungise lokhu, vula 'Spoof video streams' bese ushintsha 'Default client' ibe 'TV Simply' noma 'visionOS'" + Vumela ukuslayida ukuze ufune + Ivumela i-slide to seek esikhundleni sokudlala nge-2x speed lapho ucindzela ubambe i-video player + Vumela ukuthinta ukuze ufune + Chofoza nje kwi-seekbar ukuze uye kulowo mkhawulo ngaphandle kokuyikhipha + Isenzo sokuswayipha kwesobunxele + Isenzo sokuswayipha kwesobunxele + Isenzo sokuswayipha phezulu + Ububanzi bezone zesobunxele/zesobunxele + Ipesentile yezinhlangothi zesikrini ezibekelwe izenzo zokuswayipha kwesobunxele nangesobunxele (5-50) + Ukhanyiso + Umsindo + Yemvelo + Thwaliswa + Hlukumeza ukuswayipha lapho kukhiyiwe + Izenzo zokuswayipha ziyahlukumezwa ngenkathi umdlali ekhiyiwe ngo \'Lock screen\' + Vula isenzo sokucindezela ukuze uswipe + Kudingeka ukucindezela uphathe ngaphambi kokuba isenzo sokuswayiza siqondwe + Vumela i-haptic feedback + Gcina futhi ubuyise ukukhanya + Khumbula izinga lokukhanya lapho ungena / uphuma emodini yesikrin sawonke + Vumela i-gesture ye-auto-brightness + Ukuhwebha ukukhanya kuya emandleni aphansi kakhulu kuthuthukisa ukukhanya kwe-system okuzenzakalelayo + Okuzenzakalelayo + Isikhathi sokuphela kwe-swipe overlay + Inani lamilliseconds i-overlay ebonakalayo + Ukuphazima kwe-background ye-swipe overlay + Lungisa izinga lokubona (0 kuyisicwebile, 100 kuyisigubhu) + Umbala we-overlay yokukhashela ukukhanya + Beka umbala webhala lomdwebo lezilawuli zokukhanya + Umbala wevolyume we-swipe overlay + Beka umbala webhala lomdwebo lezilawuli zomnyakazo wezansi + Umbala wesivinywa se-overlay yesivinywa + Beka umbala we-progress bar yezilawulo yesivinywa sokudlala + Usayizi wombhalo we-swipe overlay + Lungisa usayizi wombhalo osikrinini (1-30) + Umngcele we-swipe magnitude + Beka ibanga lokuhwebha elidingekayo ngaphambi kokuthi ukulungisa kusebenze + Ubuzwa bokuswayipha kwevoliyamu + Amavalu aphakeme adinga ukuswayipha okuningi ngesinyathelo sevoliyamu + Izinyathelo zevoliyamu + Ukuvusa ivolume + Ukutshekisa phezulu kwivolume ephezulu kuyivusa, okungase kwonakalise umsindo omkhulu + Okujwayelekile kudivayisi + Ukuzwela kokusula ukukhanya + Izixazululo eziphezulu zidinga ukusula okuningi ngesinyathelo sokukhanya + Ukuzwela kokusula isivinywa + Izixazululo eziphezulu zidinga ukusula okuningi ngesinyathelo sesivinywa + Isinyathelo esisheshayo + Ubude bezone yesivinywa + Ubude bezone ye-gesture yesivinywa njengepesenti yobude lomdlali (5-75) + Isivinywa + Isitayela se-overlay yokuswayipha + I-overlay ehambisana nokuya ekuhayeni + I-overlay ehambayo ngokuhamba kwamasayizi (minimal - phezulu) + I-overlay ehambayo ngokuhamba kwamasayizi (minimal - phakathi) + I-overlay eyindilinga + I-overlay eyindilinga (minimal) + I-overlay ephakathi + I-overlay ephakathi (minimal) + Vumela uku-swipe ukuze ushintshe amavidiyo + Hwebha emodini yesikrin sawonke ukuze uye ku-video elandelayo noma edlule + Izinkinobho zezenzo + Khetha ukuthi yiziphi izinkinobho zezenzo ezivela emodini yokuphila naseimodini yesikrin sawonke + Izinkinobho ze-Portrait + Khise ukukhanya kwe-Like ne-Subscribe + Kuvimbela izinkinobho ze-Like ne-Subscribe ukuba zikhanye lapho umdali ekukhuluma ngazo + Fihla i-action bar + Fihla inkinobho ye-Like ne-Dislike + Fihla inkinobho ye-Share + Fihla inkinobho ethi \'Stop ads\' + Fihla inkinobho ye-Comments + Fihla inkinobho ethi Xhumana + Fihla inkinobho ye-Report + Fihla inkinobho ye-Remix + Fihla inkinobho ye-Download + Fihla inkinobho ye-Hype + Fihla inkinobho ye-Promote + Fihla inkinobho ye-Thanks + Fihla inkinobho ye-Ask + Fihla ishanneli nenkinobho ye-Subscribe + Ifihla i-avatar yeshanneli nenkinobho ye-Subscribe ekuqaleni kwe-action bar + Fihla inkinobho ye-Subscribe + Ifihla izinkinobho ze-Subscribe ne-Join eduze kwesithombe seshネルeli ekuqaleni kwe-action bar + Fihla inkinobho ye-Clip + Lokhu kungase kungasebenzi kwezinye izakhawulo zabasebenzisi + Fihla inkinobho ye-Shop + Fihla inkinobho ye-Save + Fihla inkinobho ethi Okwengeziwe + Izinkinobho ze-Fullscreen + Fihla inkinobho ye-Dislike + Fihla inkinobho ye-Like + Fihla inkinobho ye-\'Live chat\' + Fihla inkinobho ye-Mix + Fihla inkinobho ye-\'More videos\' + Fihla inkinobho ye-Playlist + Umgcauze phezulu wezenzo ezisheshayo + Beka umgcauze phezulu webha yezenzo ezisheshayo (0-32 dp) + Izincazelo + Phatha ama-captions azenzayo nama-caption cookies + Ukubonakala kwezincazelo ezizenzakalelayo + Khombisa ngaso sonke isikhathi + Ungazikhombisile nakaze + Khombisa lapho kukhishwe ukuthula + Khombisa lapho kuthulekisiwe + Khombisa izincazelo ezizenzakalelayo + Fihla imibhalo yesizulu ezenzakalayo + Ulimi lwezincazelo olukhethiwe + Khetha izincazelo zomhlinzeki kuqala, bese ihumusha ngokuzenzakalelayo olimini olukhethiwe + Thisha + Yenge izilimi zokuhumusha ngokuzenzakalelayo + Yenge izilimi eziningi kumenyu yokuhumusha ngokuzenzakalelayo + Setha ama-caption cookies + "Ilungisa i-bug ye-YouTube lapho ama-captions okuhumusha ngokuzenzakalelayo engase angatholakali ngenxa yama-cookies ashodayo + +Kumele kunikezwe ama-'Caption cookies'" + I-Caption cookies + Ama-cookies okusetha ezicelweni ze-Timed Text API (Caption Data API) + Thola i-caption cookies + Chofoza lapha ukuze uthole ama-cookies + Kuhlulekile ukuthola ama-cookies + Ama-cookies asekhona + Ama-cookies asevuselelwe ngempumelelo + Lungisa i-transcript + Ixazulula ukwehluleka kokuhlola isimo esidingekayo okungavimbela ukuthi i-transcript ilandwe + Khombisa imibhalo efanekisayo ku-picture-in-picture + Izinkinobho zokuhamba + Khetha ukuthi yiziphi izinkinobho ezivela kubala lokuqondisa laphansi + Fihla inkinobho yeKhaya + Fihla inkinobho yeShorts + Fihla inkinobho yokuDala + Fihla inkinobho yeZibhalisele + Fihla inkinobho yeZaziso + Khombisa inkinobho yeKhumbuza + Indawo yenkinobho yeKhumbuza + Khombisa inkinobho yeSettings + Indawo yenkinobho yeSettings + Isenzo senkinobha yezilungiselelo + Ivula izilungiselelo zeMorphe lapho kuvunyelwe, noma izilungiselelo zeYouTube lapho kukhishwe + Shintsha inkinobho ye-Create neyoku-Notifications + Uma ukushintsha kwalolu sethingi kungasebenzi, zama ukushintshela ku-Incognito mode + Fihla amagama ezinkinobho zokuhamba + Fihla ichashana lokuqukethwe okusha + Fihla i-navigation bar + "Lokhu kufihla ngokuphelele ibha yokusebenza, kuhlanganise neThebhunya loku-'Wena'. + +Ukuze ungene kuSettings, vula inkinobho yeSettings nge-General → Toolbar → Show Settings" + Vumela izinkinobho zokuzula ezincane + Kunciphisa isikhala esingemaphandla phakathi kwezinkinobho + Khumasha izinkinobho zokuzula ezingenazo izilabelo + Ikhombisa izinkinobho zokuzula ezinama-label, uma iYouTube izithumela ngaphandle kwezilabelo + Khishela amathebu okuzula e-toolbar + Ikhombisa izibhaliso kubala lokuzula, uma iYouTube ikuyisa ethebini eliphezulu kweKhaya + Vumela ama-animation e-navigation bar + Yenza ukuhambisa kube ne-animation lapho ushintsha phakathi kwamathabhu + Khubaza ukuzifihla kwibha yokusebenza + Gcina ibha yokuhamba ibonakale ngenkathi u-scroll-a + Isitayela sebhala lokuhamba + Okujwayelekile + Okucwebile + Okhalisile + Engabonakali + Leli stayela lingase lingasekelwa yithisizela lakho noma inguqulo ye-YouTube, futhi lingabangela izinkinga zokuhlela noma ukuphuka kohlelo + Ithuliba + Khetha ukuthi yiziphi izinto ezivela kwi-toolbar + Fihla inkinobho yeCast + Fihla inkinobho ye-Chat + Fihla inkinobho yeMicrophone + Fihla inkinobho yoKusesha + Uma kuvunyelwe, ukuphonsa kuvula izilungiselelo ze-Morphe kanti ukuphonsa isikhathi eside kuvula izilungiselelo ze-YouTube, kuvimelwe ukushintsha + Imenyu ye-Flyout + Khetha ukuthi yiziphi izici ezivela emenwini lokuphuma + Fihla imenyu ethi \'Izilungiselelo ezengeziwe\' / Okunye + Fihla imenyu ethi \'Imodi yezindawo\' + "Imenyu ye-'Audio Track' ifihliwe + +Ukuze ubonise imenyu ye-'Audio Track', shintsha 'Spoof video streams' kube yiklayenti noma iyiphi ngaphandle kwe-'Android Studio' ne-'Android VR Downgraded'" + Fihla imenyu ye-\'Umkhakha we-Audio\' + Fihla isizinda semenyu ye-\'Umkhakha we-Audio\' + Fihla imenyu ye-Captions + Fihla isizinda semenyu ye-Captions + Fihla ikhanda lemenyu ye-Captions + Fihla imenyu ethi \'Usizo & imbuyekezo\' + Fihla imenyu ethi \'Lalela nge-YouTube Music\' + Fihla imenyu ethi \'Lock screen\' + Fihla imenyu ethi \'Phinda ividiyo\' + Fihla imenyu ethi \'On-the-go\' + Fihla imenyu ye-Quality + Fihla ubuthongo bemenyu ye-Quality + Fihla ikhanda lemenyu ye-Quality + Fihla imenyu ethi \'Isikhathi sokulala\' + Fihla imenyu ethi \'Isivinyelo sokudlala\' + Fihla imenyu ethi \'Umvume ozinzile\' + Fihla imenyu ethi \'Buka nge-VR\' + Fihla ukubukiselwa kwokuqala okuzenzakalayo + Ifihla isibukiso sevidiyo ezayo ngesikrinimane siphelele ngaphambi kokuba i-autoplay iqale + Izinkinobho zokugubuzela + Khetha ukuthi yiziphi izinkinobho ezivela phezu kwesidlali sevidiyo + Fihla inkinobho ye-Autoplay + Fihla inkinobho ye-Captions + Fihla inkinobho ye-Cast + Fihla inkinobho ye-Collapse + Fihla inkinobho ye-Fullscreen + Fihla isizinda sezilawuli zomdlali + Ifihla ukumnyama okuzungeze izilawuli zesidlali sevidiyo + Ukucaca kwengemuva kwezin বোthoni zokulawula + Lungisa ukucaca kwengemuva ngemuva kwezinamba zokulawula umdlali nebhala likesikhathi (0 akubonakali, 100 lugcwele) + Fihla izinkinobho ze-Previous & Next + Fihla inkinobho ye-Settings + Fihla amakhadi esikrinini sokuqeda + Imode yezindawo + Phatha ukubonakala kwe-Ambient mode kanye nezivimbelo zamandla + Dlula imingcele ye-Ambient mode + Kuvumela i-Ambient mode ukuba ihlale isebenza ngenkathi kusebenzisa i-battery saver mode + Khise i-Ambient mode + Vimba i-Ambient mode ekukhipheni isikrini sigcwele + Fihla amakhadi wolwazi + Vimba imizwa yezinombolo ezizungezayo + Imisa ukunyakaza kokubala kunezibalo zezithandwa nezibalo zokubukwa + Fihla i-seekbar yomdlali wevidiyo + Fihla i-seekbar yezithombe ezincane zevidiyo + Fihla i-watch progress bar ebonakala kuma-thumbnails ama-video osewaqaphile + Vumela i-seekbar enkulu yesikrini esigcwele + Fihla ama-Shorts + Fihla ekhasini lasechaneli + Fihla ku-Home feed & amavidiyo ahlobene + Fihla emiphumeleni yokusesha + Fihla ku-feed lwe-Subscriptions + Fihla encazelweni yevidiyo + Fihla emlandweni wokubuka + Umdlali we-Shorts + Khetha ukuthi yiziphi izinto okumele zivela ku-Shorts player + Disable double-tap ukuze uthande + Fihla inkinobho ye-AI + Fihla irebhula ethi \'Ihhumushwe ngokuzenzakalelayo\' + Fihla ibha yeshayisheli + Fihla inkinobho yama-Comments + Fihla inkinobho ye-Effect + Fihla ilabeli lelinki levidiyo + Fihla iziboniso zezandla + Fihla inkinobho ethi \'Isikrin sahlutha\' + Fihla inkinobho ye-Hashtag + Fihla ipheneli yolwazi + Fihla inkinobho ethi Joyina + Fihla inkinobho ye-Like + Fihla i-animation ye-fountain yenkinobho ye-Like + Fihla ukubuka okushisayo + Fihla i-label yendawo + Fihla ibha yokusebenza + Fihla inkinobho ethi \'Izintshumayelo ezintsha\' + Fihla izinkinobho ze-overlay ezimile + Fihla ikhomentari yokubuka + Fihla inkinobho ye-Remix + Fihla inkinobho ye-Save + Fihla inkinobho ethi \'Londolo umculo\' + Fihla iziphakamiso zokusesha + Fihla inkinobho ye-Share + Fihla inkinobho ye-Shop + Fihla inkinobho ye-Sound + Fihla ilabeli lemininingwane yomsindo + Fihla inkinobho ye-Subscribe + Fihla inkinobho ethi \'Thenga i-Super Thanks\' + Fihla imikhiqizo ehlanganiswe + Fihla inkinobho ye-Upcoming + Fihla inkinobho ethi \'Sebenzisa umsindo lo\' + Fihla inkinobho ethi \'Sebenzisa ithamplate le\' + Fihla isihloko sevidiyo + Fihla ividiyo ephakanyiswe esikrinini sokuphela + "Ifihla i-overlay yevidiyo ephikiselwayo evela emva kokuba ividiyo iphele + +Lolu sethingi lusebenza kuphela uma i-autoplay icishwe. Ungalushintsha ku-YouTube: +Settings $\rightarrow$ Playback (noma Autoplay) $\rightarrow$ Autoplay next video" + Fihla amavidiyo ahlobene + Fihla i-overlay ethi \'More videos\' + Ifihla i-overlay ethi \'Amaviyo amaningi\' ebonakala lapho usula phezulu kwimodi yefullscreen + Fihla isikhathi sevidiyo + Vala ama-popup panels omdlali + Vikela amaphenela e-playlist ne-live chat ukuba avuleke ngokuzenzakalela + Khubezela ukuzidlala kwuhlu lwezingoma + Kuvimbela uhlu lwezingoma ukuba luyeqele ngokuzenzela kuvidiyo elandelayo + Thisha isixwayiso esithi \'Qhubeka nokubuka\' + Phuma emodini yesikrin sikhulu lapho ividiyo iphela + Kukhishwe + Portrait kuphela + Landscape kuphela + I-Portrait ne-landscape + Force landscape ku-fullscreen + Zungeza i-player ibe i-landscape lapho ungena ku-fullscreen. Ayitholakali ezikrinini ezincane, ezivulwa seziyiku-landscape ku-fullscreen + Ama-gestures e-fullscreen + Izandiso zomdlali ekuhambeni kwesikrin sokuphuthuma + Vala i-gesture yokuswepha phezulu kwe-portrait + Vala i-gesture yokuswepha phansi kwe-portrait + Disable i-gesture yokuswayisela phansi ku-landscape + Khwelisa isandiso sokuzooma ngokucindezela + Buyisela uku-pinch to zoom + Vula amavidiyo ngendlela yesikrin siphelele + Kudisayilwe + Umboniso wokuqondile + Umboniso owalawo + Isayizi ye-video yesikrin sahlelele + Okujwayelekile + Nwebula (gcwalisa isikrini) + Zoom (sika ukuze ugcwalise) + Khombisa inkinobho yesayizi ye-video + Thinta ukushintsha phakathi kwe-Default, Stretch, kanye ne-Zoom + Khombisa kuphela kukhipha isikrin sahlobo else + Fihla inkinobho yesikala sevidiyo kudlala-vidiyo okumile + Khombisa inkinobho yokuthulisa + Ukucaca kwe-overlay yomdlali + Lungisa izinga lokubona (0 kubonakala, 100 akubonakali) + Ibhala lokusesha + Idishebelwe + Banzi + Banzi kakhulu + Ukuhlelwa kwedivayisi + Okujwayelekile + Ucingo + Ithebhulethi + Ezimoto + "Izinguquko zihlanganisa: + +Ukulungiselelwa kwebulethe +• Izithamo zomphakathi zifihliwe +• Isetingu sokudlala ku-feed asitholakali +• Inkinobho ye-Remix nenkinobho ye-Sound azitholakali ku-Shorts +• I-overlay ethi 'More videos' kungenzeka ingatholakali +• I-action bar endala yevidiyo engakhiwe iyabonakala + +Ukulungiselelwa kwezimoto +• I-feed ihleleke ngokwemixhumano namachannel +• Isetingu sokudlala ku-feed asitholakali +• Inkinobho ye-Remix nenkinobho ye-Sound azitholakali ku-Shorts +• I-overlay ethi 'More videos' kungenzeka ingatholakali +• I-Shorts ivuleka kumdlali ovamile +• I-action bar endala yevidiyo engakhiwe iyabonakala" + Vumela ukulungiseka kwe-tablet kudlali + "Buyisela ukulungiselelwa kwedlali yakudala + +Ukukhawulelwa: Uma 'Buyisela ibha yezenzo yevidiyo yakudala' icishiwe, i-overlay ye-'More videos' kungenzeka ingatholakali" + 21.11.48 - Buyisela ukuziphatha kwama-captions amdala + 20.39.41 - Buyisela ukulonda kwakudala ohlwini lwezingoma + 20.28.41 - Khise ama-icon agqamile + 20.13.41 - Buyisela igama lasechaneli ngaphansi kwesidlali sevidiyo + Vula i-system share sheet + Ivula i-share sheet yesistimu esikhundleni se-in-app lapho wabelana ngokuqukethwe + Dlulela phezu kwezin ನೋಡi ze-YouTube Music + Vula i-app yomculo oyithandayo esikhundleni se-app ye-YouTube Music ejwayelekile + Igama লে-package lehlelo lomculo + Igama lephakheji lohlelo lwakho lomculo olufakiwe + Ikhasi lokuqala + Okuhleliwe + Zonke izibhaliso + Bheka amashaneli + Izifundo / Ukufunda + Hlola + Imfashini & Ubuhle + Imidlalo + Umlando + Ulayibhulali + Amavidiyo athandwayo + Okubukhoma + Amafilimu + Umculo + Izindaba + Izaziso + Uhlu lwezingoma + Sesha + Ukuthenga + Shorts + Ezemidlalo + Izibhaliso + Okudumile + I-Virtual Reality + Buka kamuva + Ama-clips akho + Vala ukuqhubeka lapho kuqalisa + Vula i-Shorts ngo- + Umdlali we-Shorts + Umdlali ojwayelekile + Umdlali ojwayelekile we-fullscreen + Dlala Shorts ngokuzenzakalela + Iya ku-Short elilandelayo ngokuzenzakalelayo esikhundleni sokuphinda lelo lamanje + Dlala ngokuzenzakalela ngemuva + Iya ku-Short elilandelayo ngokuzenzakalelayo ngesikhathi sokudlala ngemuva + Yishiye i-live Shorts + Iqeda ama-live streams avala ku-feed lama-Shorts + Ukudlala kuma-feed + Kuhlale kuvuliwe + Kwi-Wi-Fi kuphela + Kivaliwe + Buyisela izihloko zokuqala + Kukhombisa izihloko ze-video zokuqala, incazelo ye-video kanye nezincazelo zezinhlelo esikhundleni sezihunyushwe ngokuzenzakalelayo + Kulodwa isihloko… + Vimisa ukuvuselelwa kwe-feed ngokuzenzakalelayo + Kuvimbela ama-feed ukuba avuselelwe ngokuzenzakalelayo emva kokuba asendulo + Ukulungiseka kwekhadi le-feed + Okujwayelekile + Amakhadi + Amakhadi anezin ನೋಡ-like, ukuphendula kanye nezin ನೋಡ-save + Imibono + Vala ukuqhubeka kweminiplayer + Kukhishwa ukusebenza kwe-miniplayer ye-\'Qhubeka nokubuka\', okuyisici se-YouTube Premium esivela lapho kuvulwa i-app + Umdlali omncane + Lungisa ukubonakala nokusebenza komdlali wevidiyo ocishwe + Uhlobo lomdlali omncane + Fihla isihloko neshannel + Kukhishwa + Okujwayelekile + Okuncane + Okuncane 2 + Ithabulethi + Yesimanjemanje 1 + Yesimanjemanje 2 + Yesimanjemanje 3 + Yesimanjemanje 4 + Khumbanisa amakona aqwayizile + Khumbanisa ukuhudulisa nokulahlisa + Kuvimbela ukuhudulisa umdlali ocishwe emazweni ahlukahlukene esikrinini + Khumbanisa isenzo sokuhudulisa ngokuncane + Kuvimbela ukuvalwa komdlali ocishwe ngokuhudulisa ngokuhamba ngokunye esikrinini + Yema ukudlala ngokuhudulisa ngokuhamba ngohlangothi + Ukudlala kuzoma lapho i-miniplayer ihujwa iphume esikrinini + Khumbula isimo sendawo yokuhudulisa + Ukuhudulisa i-miniplayer ngaphandle kwe-screen kuzogcina isimo se-UI yayo + Fihla izinkinobho zangaphandle + Usayizi osethelwe + Setha usayizi oqala esikrinini, ngama-pixel + Usayizi we-Pixel kumele abe phakathi kuka %1$s no %2$s + Ukucaca kwe-Overlay + Lungisa izinga lokubona (0 kubonakala, 100 akubonakali) + Inkinobho yokuya emuva iphuma efidini + Ukuqhuma kwenkinobho yokuya emubuntu kuphuma ohlelweni kunokuba ukuhambise i-feed phezulu + Phusha ama-animations + Okujwayelekile + Kuhlale kuvuliwe + Kuhlale kuvaliwe + Fihla i-status bar + Ifihla i-status bar yesistimu. Sula phansi kusukela phezulu ukuze uyibone okwesikhashana + Lungisa i-status bar emboza imigqa engenhla + Gcina ukuhleleka kwe-status bar kukufana, ukuze ingabozi imigqa engenhla futhi ingashiyi isikhala esingenalutho ngaphezu kwedlala. Cima lokhu uma i-app i-crash noma imigqa engenhla ibukeka ngendlela engalungile + Isizinda sethimu ekhanyayo + Mhlophe + Material You (Mhlophe) + Opinki okukhanya + Oluhlaza okukhanya + Oluluhlaza okukhanya + Ophuzi okukhanya + Owolukhamba okukhanya + Obomvu okukhanya + Isizindalo esikhanyayo esikhethiwe + Beka umbala wesizindalo setheme ekhanyayo + Shintsha umbala ophambili + Sebenzisa umbala wetheme embhalweni nasemifanekisweni + Vula isikrini sokulodha esine-gradient + Khombisa isizinda esine-gradient esikrinini sokuloda esikhundleni sombala owodwa + Vula umbala we-seekbar olandela isifiso + Umbala we-seekbar + Setha umbala omkhulu we-video progress bar + Umbala we-Seekbar accent + Setha umbala wesibili we-video progress bar + Inani lombala lwesekibar elingelona + Ulogo we-Header + Okujwayelekile + Okujwayelekile + Okukhethiwe + Dlula izivimbelo zezindawo zezithombe + Idirekhtha izicelo zethambenali ku-server engukungezela ukuze kulungiswe izithombe ezishodayo emazweni anemikhawulo + Izihloko ze-DeArrow + Ithebhile yeKhaya + Ithebhile yeziBhaliso + Ithebhile yakho + Uhlu lwezingoma lomdlali & okuncoma + Imiphumela yokusesha + Khombisa i-icon ye-DeArrow + Khombisa i-icon ye-DeArrow ezihloko zezividiyo ezisuselelwe emsithweni + Imodi yokuziphethela + Ikhombisa isihloko sokuqala esikhundleni sesihloko seDeArrow uma abasebenzisi beDeArrow bevota ukuthi bayasithanda isihloko sokuqala + Imivoti embalwa kakhulu yokuziphethela ukuze kukhonyiswe isihloko sokuqala + Inombolo yemivoti yokuziphethela ekhasini elilodwa, efana nokuhlekisa noma ukubumba, edingekayo ukuze kukhonyiswe isihloko sokuqala + Izithombe zakwaDeArrow + Izithombe ezisungu + Izithombe ze-DeArrow & ezisungu + Izithombe ze-DeArrow & ezithunqiwe + Ukuthatha izithombe ezingashintshi + "I-DeArrow ithatha izihloko nezithombe ezikhangisa kakhulu (clickbait) izifake ezitholakala ngokubambisana komphakathi eziningi kakhulu futhi ezingaqhubezi izinto. Izihloko zibhalwa futhi zivotelwa umphakathi, kanti izithombe ziyizifريم ezithathwe kuvidiyo + +Amavidiyo aceleka ngesisekelo esifushane se-hashed video ID, ngakho iserba ayikwazi ukusho ngqo ukuthi iyiphi ividiyo oyibukayo. Uma ividiyo ingenalo isithombe se-DeArrow, isithombe sokuqala noma isithombe esithathwe kuvidiyo siyabonakala" + Khombisa izixwayiso zephutha lokuxhumana + Khombisa isaziso lapho i-DeArrow server ingatholakali + I-API endpoint + I-URL ye-endpoint ye-cache ye-thumbnail yeDeArrow + Ukuthatha izithombe zezividiyo ezingashintshi + Izithombe ezisashaywa zithathwa ekuqaleni / phakathi / ekugcineni kwayo yonke imvidiyo. Lezi zithombe zakhiwe ku-YouTube futhi ayikho i-API yangaphandle esetshenziswayo + Khipha ifreyimu yevidiyo ku- + Ukuqala + Phakathi + Ekugcineni + I-DeArrow ayitholakali okwesikhashana (i-status code: %s) + I-DeArrow ayitholakali okwesikhashana + Buyisela isitayela sakudala senkinobha yomdlali + Buyisela izilawuli zevidiyo ezigqamile endleleni yakudala, encane + Lingisa izici zesikhala sedivayisi + Ukuvula lokhu kungavula izinhlobo zezinkundla zevidiyo ezingcono + Ukuvula lokhu kungabangela ukuthi ukudlalwa kwevidiyo kuphazamiseke, ubomi bebhethri bube bububi, kanye nemiphumela engaziwa + Impendulo ye-haptic + Khetha ukuthi yiziphi izinto ezenza ukuthi idivayisi ivibe + Vala ama-haptics ezigaba + Vala ama-haptics okufuna okunembile + Vala ama-haptics okukhansela ukufuna + Vala i-haptics yokuthinta nokubamba + Vimbela i-zoom haptics + Uma usanda kushintsha imininingwane yokungena ku-akhawunti yakho, khulula bese ufaka futhi u-MicroG + Dlula ukudluliselwa kwe-link + Ivula isixhumanisi sokuqala ngqo, idlula isixhumanisi sokulandelela (youtube.com/redirect) + Vula ama-link e-browser + Ivula ama-links kubrowser yangaphandle esikhundleni se-browser yesekela yohlelo + Usayizi we-playback buffer + Okujwayelekile + Okuncane + Okuphakathi + Okukhulu kakhulu + Ukubambiseka kwevidiyo + Kubambisa ividiyo ukuze ihambisane nomsindo onokubambiseka, njenge-headphones ze-Bluetooth (0-5000 ms) + Bambisa kuphela nomsindo we-Bluetooth + Kubambisa ividiyo kuphela lapho umsindo we-Bluetooth uxhumekile, ukuze isipikha sefoni sihlale sihambisana + Khombisa inkinobho yokubambezela iVidiyo + Thinta ubambe ukuze ubuyisele ukubambezela ku-0 ms + Khombisa imenyu yokubambezela iVidiyo + Ukubambezela ngama-milliseconds + Zizenzayo + Khumbula izinga levidiyo + Kusebenzisa ukukhetha kwakho izinga levidiyo kuzo zonke izividiyo ezizayo + Izaziso zoshintsho lwezinga + Ikhombisa isaziso lapho kuvuselelwa ikhwalithi ejwayelekile entsha + Ikhwalithi yevidiyo evamile kunethenda le-Wi-Fi + Ikhwalithi yevidiyo evamile kunethenda lwemakhalekhukhwini + Khumbula izinga le-Shorts + Kusebenzisa ukukhetha kwakho kwekhwalithi kuzo zonke i-Shorts ezizayo + Ikhwalithi ye-Shorts evamile kunethenda le-Wi-Fi + Ikhwalithi ye-Shorts evamile kunethenda lwemakhalekhukhwini + Kushintshwe izinga lejalo le-mobile libe ngu: %s + Kushintshwe izinga lejalo le-Wi-Fi libe ngu: %s + Kushintshiwe ikhwalithi ye-Shorts yeselula ibe ngu: %s + Kushintshiwe ikhwalithi ye-Shorts ye-Wi-Fi ibe ngu: %s + Khombisa inkinobho yesivinyane sokudlala + Thinta ubambe ukuze ubuyisele isivinyane kwesetingi esijwayelekile + Khombisa inkinobho yeKhwalithi + Thinta ubambe ukuze ubuyisele ikhwalithi kwesetingi esijwayelekile + Khombisa isixazululo enkinobhweni ye-Quality + Khombisa isixazululo, njenge-1080, esikhundleni selabel njenge-FHD + Imenyu yesivinyelo sokudlala esikhethiwe + Buyisela imenyu yakudala yesivinyelo sokudlala + Isivinyelo sokudlala esikhethiwe + Faka inketho yesivinyane sokudlala okukhethayo + Isivinyalo sokudlala esikhethiwe kumele sibe ngaphansi kwe-%s + Isivinyelo sokudlala esikhethiwe asilungile + Okuzenzakalelayo + Thinta ubambe isivinyane sokudlala + Setha isivinyane lapho ucindezela ubambe isikrini, setha ku-0 ukuze ukukhiphe + Khumbula isivinyelo sokudlalela + Kusebenzisa ukukhetha kwakho isivinyelo kuwo wonke amavidiyo azayo + Izaziso zoshintsho lwesivinyelo + Ikhombisa isaziso lapho kuvuselelwa isivinyelo sokudlala esijwayelekile esisha + Isivinyelo sokudlala esijwayelekile + Isivinyelo sokudlala esijwayelekile kuma-Shorts + Khumbula isivinyelo sokudlala ku Shorts + Kusebenzisa ukukhetha kwakho isivinyelo se-Shorts kuwo wonke ama-Shorts azayo. Uma kucishwa, isivinyelo sibuyela kokujwayelekile ngemva kokuvala isidlali se-Shorts + Khombisa isivinyelo sokudlala kumenyu yama-Shorts + Yengeza ithamu lesivinyelo sokudlala kumenyu yesidlali sama-Shorts + Isivinyelo sokudlala · %s + Kushintshwe isivinyalo sokudlala esijwayelekile sibe ngu: %s + Sebenzisa isivinyane esijwayelekile emusicweni + Dlala njalo amavidiyo omculo ngesivinyane sika 1.0x + Uhlu lwesivinyane lwezikhanyisheli ezivunyelwe + Amanamavidiyo ezikhanyisheli ezivunyelwe adlalwa njalo ngesivinyane sika 1.0x + Yenge ishayisheli ohlwini lwesivinyane oluvunyelwe + Susa ishayisheli ohlwini lwesivinyane oluvunyelwe + Vumela izilawulo ze-audio pitch + I-audio pitch yokuvala okujwayelekile + Kushintshwe i-audio pitch evamile kuya ku: %s + Ukunwebeka kwesikhathi se-audio + Kuvumela ukusetha isivinyelo ne-pitch ngokuzimele, kungase kubangele amaphutha e-audio + Khumbula isikhundla sokudlala kokuthumela okubukhoma + Kukuthumela okubukhoma okuqhubekayo, kukhumbula futhi kuqhubeke nesikhundla sakho sokudlala esikhundleni sokufuna isikhundla sokubukhoma samanje + Le nqubo ayisebenzi kokuthumela okubukhoma okusebenza amahora angama-24/7 kanye nokunye okuthunyelwa \'okuhlale kuvuliwe\' + Khishela i-HDR video + Phosha ividiyo ye-HDR + Phosha i-app ukuba ibone leli device njenge-HDR-capable + "Sebenzisa lokhu uma i-HDR decoding isebenza kodwa i-YouTube ingeniki i-HDR, njengezinye ze-HyperOS devices ezifihla i-HLG. + +Imikhawulo: +• Imibala ingase ibukeke ingalungile uma isikrin singakwazi ukukhombisa i-HDR +• Ukudlala kungahluleka uma i-decoder ingakwazi ukuphatha i-HDR +• I-HDR isabhenze kwi-spoof video streams client" + I-Codec ye-video + Okujwayelekile + Ukuze uphoqelele i-AVC, shintsha i-\'Spoof video streams\' ibe \'Android Studio\', \'Android VR\' noma \'visionOS\' + "VP9 +• Ingathuthukisa isikhathi sebhethri futhi yilungise nokunqikiza kokudlala +• Isebenzisa idatha ye-internet eningi kun i-AV1 + +AVC (H.264) +• Idinga i-client ye-'Android Studio', 'Android VR', noma 'visionOS' Spoof video stream +• Ingathuthukisa isikhathi sebhethri kakhulu +• Ingabuyisela izincazelo ze-video ezishaywayo kumadivayisi amadala +• Isincazelo esiphezulu ngu-1080p +• Ama-video e-HDR angase angasebenzisi i-AVC +• Amanye amadivayisi aweke aphoqelele i-AVC" + Vula imenyu yezinto eziphezulu zokusebenza + Iqeda imenyu yokukhetha izinga futhi ivule imenyu yezinga ePhambili ngokuqondile + Fihla izinketho zezimiso ze-Premium quality + Nikeza imiklomelo ekhalandweni yevidiyo + Inikeza imiklomelo ekhalandweni yevidiyo ephezulu kunakubuchule be-codec + Uma le setting icishiwe, amavidiyo amadala angase akhombise okunye okungakho mayelana nekhwalithi yevidiyo + Vumela \'Android VR\' / visionOS AV1 + Kuvumela ukudekhodiwe kwe-software AV1 lapho usebenzisa i-client ye-\'Android VR\' / visionOS + "Ukuvula le setting kungasebenzisa i-software AV1 decoding. + +Ukudlalwa kwevidiyo nge-AV1 kungase kuphazamiseke noma kuwele ama-frames" + Imiphumela yoku-spoofing + • Ukudlala kungahluleka ngemva komzuzu owu-1 + • Imenyu ye-\'Audio Track\' ayikho + • Alikho i-AV1 video codec + • i-360° VR immersive mode ayitholakali + • Ivolyumi elizinzile alitholakali + • Ukudlala kungaphela imizuzwana eyi-1 ngaphambi kwesikhathi + • Uma i-deobfuscation ihluleka, ukudlala kungahluleka + • Ukunqamula umsindo wokuqala akutholakali + • Kudingeka ungene ku-Android VR + • Ikhwalithi ingase ikhawulwe ku-720p + Uhlobo lwe-Player JavaScript + Khombisa ukulingisa ku-\'Stats for nerds\' + Buyisela ibha yezenzo yevidiyo endala + "Ilungisa izinkinobho zezenzo zevidiyo ezingafihleki + +Ukukhawulelwa: Kubuyela endleleni yakudala ye-overlay ye-'Amavidiyo amaningi'" + Isitayela se-icon ye-Player + Okuzenzakalelayo + Okungajwayelekile + Emincane + Eqinile + Okukhethiwe + Isitayela se-icon ye-Shorts + Umlandisi we-Series + Landela uhlu lwezingoma njenge-series futhi ugcine lapho ufikile khona + Vula ukulandelela i-series + Yengeza ikhasi le-Series ku-History nokukhetha ukulandela i-series kuhlu lwezingoma + Khombisa i-Series kubala lokuzisusa + Khombisa inkinobho yoMlandisi we-Series + Yengeza inkinobho yomdlali yokulandela noma yokuvula i-series yevidiyo yamanje + Bhala inqubo ye-series elandelekile + Bhala ukudlala kule foni ye-series ezongwe kanye namabhukumaki + Khansela + Gcina + Gcina i-bookmark + Igama le-series + Izinketho ze-series + Izinketho zelayibrari + Vuselela iziqephu + Shintsha igama + Vula uhlu lwezinhlelo ku-YouTube + Susa kulayibrari + I-link noma i-ID yohlu lwezinhlelo + Sula konke inqubekelaphambili yasendaweni + Sula umlando + Sula wonke inqubo yasendaweni? + Lokhu kusula izikhundla, ukudlulisa okubukiwe kanye nezinketho zokuqhubeka kuyo yonke ividiyo. Izinhlelo ezongile kanye namakatalog yakho kuyahlala. Ukudlalwa okusha kuzogcinwa uma ukurekhoda kuvuliwe + Maka njengokubukwayo + Maka njengokungabukwayo + Sebenzisa isimo esizenzakalelayo + Qala kabusha kusukela ku-0:00 + Dlala futhi kusukela ku-0:00 + Isimo sigcinwe + Zama ukugcina futhi + Okungaphambili 100 + Okulandelayo 100 + Vula ividiyo egcinwe + Inqubo yasendaweni isulwe + Vula Umlando ovela kuWena ukuze ubone imidlalalo yakho + Umlando wokubuka + Imidlalalo + Kulayidwa imidlalalo egcinwe… + Landela uhlu lwevidiyo ukuze ulandelele iziqephu zalo + Isibukha se-Playlist + Kuvuselelwa iziqephu… + Zonke izinhlelo + Bukwe + Kulandwa izahluko… + Okunye okukhethwayo + "Bhala phansi inqubekelaphambili kuphela yohlelo lwakho olugcinwe kanye namavidiyo owabhukimake. + +Lolu library lugcinwe kuleli device futhi lihlanganyelwa phakathi kwe-accounts ze-YouTube. Izilawuli zoku-pause nokucoca umlando ze-YouTube azikushintshi. Sebenzisa i-Series → More → Clear local progress ukuze ukusule. + +Ukuqopshwa ngokuzenzakalelayo kuyama ku-incognito, lapho ubunikazi be-account lungatholakali, noma lapho ushintsha uye kwenye account. Vula ukulandelela futhi ukuze ukusebenzise naleyo account" + Ukulandelela akukwazi ukuvulwa ku-incognito noma ngaphambi kokuba i-YouTube ilayide isimo se-account samanje. Buyela ku-Home uzame futhi. Akudingeki ukungena + Vula ukulandelela kwendawo + Uqopho lwenqubo lucishile + Qhubeka ku-%1$s + %1$s / %2$s iziqezo zazibukelwe + Isiqenzo %1$s · %2$s + Iziqezo %1$s-%2$s ze %3$s + Refresh ukuze uzame ukulayida iziqezo. Ama-playlists ayimfihlo adinga ukufinyeleleka kwe-account; i-bookmark yawo isavulwa ku-YouTube + I-YouTube ayikwazanga ukuvula le episodes + I-YouTube ayikwazanga ukuvula le playlist + Khetha isahluko + Kusekuemnandi + Azikho izahluko ezingadlala + Ividiyo egcinwe · %1$s + Landela uchungechunge + Buka i-series + Qhubeka + Qala lapha + Iphuzu lokuqala ligcinwe + Ubukile kuze kube lapha + Izinhlelo ezimakiwe njengezibukwe + Buyisa + Vula + {count, plural, one {iqonda elisha 1} other {iziqonda ezintsha {count}}} + {%1$s} episodes ezintsha + Qopha inqubekelaphambili kuleli device + "I-series yakho nenqubekela phambili ihlala kuleli device, ihlanganyelwa phakathi kwe-accounts ze-YouTube. Izilawuli zomlando we-YouTube azizishintshi. Sula inqubekela phambili kume-nu ye-Series. + +Ukuqopha ngokuzenzakalelayo kuyinketho. Kuyama ku-incognito noma lapho i-account ishintsha" + Episodes azikwazanga ukulodwa. Ungayigcina i-playlist njenge-bookmark + Kunezinguquko eziningi ezingakagcinwa. Zama ukugcina kuqala + Ukugcina kumatasa. Zama futhi + Ukulodwa kwe-episode kumatasa. Zama futhi + Isisongamali asitholakali. Idatha yakho egcinwe igcinwe + Izinguquko azikwazanga ukugcinwa. Zama ukugcina futhi ngaphambi kokuvala iYouTube + Le series isegcinwe, noma uhlu lwayo lwe-playlist lushintshile. Ivule futhi + Le series ishintshile noma isisuswe. Vula futhi ilayibrari + Uhlu lwe-playlist lushintshile. Khetha ephizodi futhi + Le ephizodi ayitholakali. Khetha enye + Faka igama le-series + Faka isixhumanisi se-playlist yeYouTube noma i-ID ye-playlist + Sicela ulinde ngaphambi kokuvuselelana futhi + Izigaba azikwazanga ukulodwa. Zama futhi kamuva + Ushintsho alukwazanga ukuphothulwa. Zama futhi + Ukuqhubeka akukwazanga ukuqinisekiswa. Hlola isidlali uzame futhi + Imidanti elandelekayo + Le video ayikho kule playlist + Kufunwa uhlu lwezingoma… + Namathisa i-link ye-playlist + Akutholakalahle uhlu lwezingoma + Azikho iziqephu ezitholakalayo + Fihla ezibukwe + Maka iziqephu ezedlule njengezibukwe + Kuqala ngo: %1$s + Ukuhleleka kweziqephu + Ukuhleleka kohlu lwezingoma + Hlehla ukuhleleka kohlu lwezingoma + %1$s ukubuka + ukubuka kuka %1$s + Maka iziqephu njengezibukwe + Okungenani u-%1$s%% okubukiwe kusele imizuzwana engaphezu kwe-90, noma kusele imizuzwana engu-%2$s + Maka isigaba njengesibukiwe ngokuzenzakalelayo ngemuva kwelinye lala mikhawulo. Imithetho yamaphesenti idinga ukuthi kungasali ngaphezu kwemizuzwana engu-90, ukuze ukuphela kwezigaba ezinde kungashaywa. Imithetho yesikhathi esisele isetshenziswa kuvidiyo ezinde kunomzuzu owodwa; ibeke ku-0 ukuze uyikhise. Izinguquko zisebenza ezivutshweni zokuhamba ezizayo. Izinketho zokubuka/ukungabuki ngokuzenzela zigcinwe + Ipesenti ebukiwe (1-100) + Imizuzwana esele (0-300; 0 ikukhisa) + Faka inani kusuka ku-%1$s kuya ku-%2$s + Sebenzisa inqubekela phambili ye-YouTube + Qhubeka ne-series oyilandelayo ngemuva kokubuka kwenye idivayisi + Funda isimo sokubuka nezindawo zokuqhubeka ze-series ozilandela kusuka ku-akhawunti ye-YouTube yamanje. Izihloko ezihambelanayo kuphela ezigcinwe kule foni. Uhlolo lwenzeka ngenkathi ilayibrari ye-series ivuliwe nangaphambi koku-Continue. Umlando we-YouTube kumele uvuleke kudevice yakho enye + Kuhlolwa inqubekela phambili ye-YouTube… + Inqubekela phambili ye-YouTube ayitholakali. Kusetshenziswa inqubekela phambili egcinwe + Kukhona inqubekela phambili ye-YouTube engatholakali. Izindawo ezigcinwe zisagcinwe + Backup nokubuyisela + Gcina ilayibrari yakho ye-series kufayela noma ubuyisele i-backup + Gcina i-series ezilandelwayo, izindawo zezihloko kanye nezinketho zokubuka. Izimvume zokuqopha nezinqubekela phambili ze-YouTube azifakwe + Gcina i-backup + Buyisela i-backup + Yengeza i-series e-%1$s. I-series e-%2$s eselayibrari yakho zisagcinwe. Inqubekela phambili yevidiyo esekhona isagcinwe + I-backup ye-series igcinwe + Kubuyiselwe i-series e-%1$s + Leli fayela aliyona i-backup ye-series esekelwayo, noma likhulu kakhulu. I-library yakho ayizange ishintshe + Kukwazekile ukulonda i-backup. Zama enye indawo + Umsizi ususwe + Le episode manje ivele ngaphezu kakhulu kanye. Khetha ukuthi iyiphi okufanele uqhubeke ngayo + I-episode yakho elondolwe ayisasekuhleleni lwe-playlist. Isikhundla sakho esilondolwe sisasekhona + Qhubeka nge-episode %1$s · %2$s + Ungaphinde ukhethe noma iyiphi i-episode ngezansi. I-episode zasendulo azishintshwanga + Le playlist ishintshile. Vula i-series ukuze ukhethe lapho uqhubeka khona + Azisasekho iziqephi kule ndlela + Qhubeka no %1$s + Funa umsizi + Sula ukusesha + Akukho msizi otholakele + Isiqephi esingenaisihloko + Isiqephi esingatholakali + From e2ff1ea2d2c5c9663974d717f936cbf3afe5d9d3 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Tue, 27 Oct 2026 09:08:58 +0000 Subject: [PATCH 098/189] chore: Release v1.49.0-dev.8 [skip ci] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## [1.49.0-dev.8](https://github.com/MorpheApp/morphe-patches/compare/v1.49.0-dev.7...v1.49.0-dev.8) (2026-10-27) ### 🐛 Bug Fixes * **YouTube - Channel whitelists:** Flyout menu opens with a delay while the channel is fetched ([5fed7fc](https://github.com/MorpheApp/morphe-patches/commit/5fed7fc701421b701263f742a6ba38fd4cc89dd5)) * **YouTube - Litho filter:** Crash when scrolling a list after a filtered element is removed ([6d43157](https://github.com/MorpheApp/morphe-patches/commit/6d4315757768d792614915d0867079437505fab9)) * **YouTube - Playback speed:** Use the bold icon for the Shorts player menu item ([c710042](https://github.com/MorpheApp/morphe-patches/commit/c710042f6cf3f068141e89cd833dd580d7b64b8c)) * **YouTube - Player icon style:** Add Ionicons picture in picture button icon ([68b8628](https://github.com/MorpheApp/morphe-patches/commit/68b86282725f73c3a92602c792457371ddb88c8e)) * **YouTube - Premium-style queue:** Fix missing titles and stale now playing row ([#3795](https://github.com/MorpheApp/morphe-patches/issues/3795)) ([ef28427](https://github.com/MorpheApp/morphe-patches/commit/ef28427f63c30329b33b8d83c439c95b731a4fb6)) * **YouTube - Premium-style queue:** Play the queue when the player Next button is pressed ([#3796](https://github.com/MorpheApp/morphe-patches/issues/3796)) ([ef8dcef](https://github.com/MorpheApp/morphe-patches/commit/ef8dcef6a9ef888a075fb97d963719ddf82d6b42)) * **YouTube - Return YouTube Dislike:** Feed thumbnail like count is not shown with the Classic feed card layout ([15a1700](https://github.com/MorpheApp/morphe-patches/commit/15a170042639ca8bc0b4d871b781c29d00926fb2)) * **YouTube Music - Downloads:** Fix song menu downloads for accounts without Premium ([#4086](https://github.com/MorpheApp/morphe-patches/issues/4086)) ([93c6893](https://github.com/MorpheApp/morphe-patches/commit/93c689374b0ef72cb97d6c73881ecdb132ce7e91)) ### ✨ New Features * **YouTube - Channel search:** Search the posts of the channel ([#4078](https://github.com/MorpheApp/morphe-patches/issues/4078)) ([93bd035](https://github.com/MorpheApp/morphe-patches/commit/93bd0351362da6c75eb40f47d0ea0e37c632eb1c)) * **YouTube - Downloads:** Add download item to the Shorts menu ([#4099](https://github.com/MorpheApp/morphe-patches/issues/4099)) ([7159826](https://github.com/MorpheApp/morphe-patches/commit/715982644742ca770a1c61fe292a77bb1ce11296)) * **YouTube - Downloads:** Add playlist downloads for accounts without Premium ([456e0db](https://github.com/MorpheApp/morphe-patches/commit/456e0db9603f0930a4893e3f24ee3eee8bd4e5f9)) * **YouTube Music - Third-party lyrics:** Streamline lyrics viewing experience ([#4002](https://github.com/MorpheApp/morphe-patches/issues/4002)) ([00a4bf4](https://github.com/MorpheApp/morphe-patches/commit/00a4bf457e8df233b21e6f73b244b0c12e91e53e)) * **YouTube:** Add Niconico-style live/replay comments ([#4080](https://github.com/MorpheApp/morphe-patches/issues/4080)) ([e9f77d7](https://github.com/MorpheApp/morphe-patches/commit/e9f77d7c5932499ababf91481b883c30e96ccd1c)) --- CHANGELOG.md | 21 ++++++++++++ README.md | 5 +-- gradle.properties | 2 +- patches-bundle.json | 9 ++--- patches-list.json | 81 ++++++++++++++++++++++++++++++++++++++++++++- 5 files changed, 110 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d48c298d5..3da65d2388 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,24 @@ +## [1.49.0-dev.8](https://github.com/MorpheApp/morphe-patches/compare/v1.49.0-dev.7...v1.49.0-dev.8) (2026-10-27) + +### 🐛 Bug Fixes + +* **YouTube - Channel whitelists:** Flyout menu opens with a delay while the channel is fetched ([5fed7fc](https://github.com/MorpheApp/morphe-patches/commit/5fed7fc701421b701263f742a6ba38fd4cc89dd5)) +* **YouTube - Litho filter:** Crash when scrolling a list after a filtered element is removed ([6d43157](https://github.com/MorpheApp/morphe-patches/commit/6d4315757768d792614915d0867079437505fab9)) +* **YouTube - Playback speed:** Use the bold icon for the Shorts player menu item ([c710042](https://github.com/MorpheApp/morphe-patches/commit/c710042f6cf3f068141e89cd833dd580d7b64b8c)) +* **YouTube - Player icon style:** Add Ionicons picture in picture button icon ([68b8628](https://github.com/MorpheApp/morphe-patches/commit/68b86282725f73c3a92602c792457371ddb88c8e)) +* **YouTube - Premium-style queue:** Fix missing titles and stale now playing row ([#3795](https://github.com/MorpheApp/morphe-patches/issues/3795)) ([ef28427](https://github.com/MorpheApp/morphe-patches/commit/ef28427f63c30329b33b8d83c439c95b731a4fb6)) +* **YouTube - Premium-style queue:** Play the queue when the player Next button is pressed ([#3796](https://github.com/MorpheApp/morphe-patches/issues/3796)) ([ef8dcef](https://github.com/MorpheApp/morphe-patches/commit/ef8dcef6a9ef888a075fb97d963719ddf82d6b42)) +* **YouTube - Return YouTube Dislike:** Feed thumbnail like count is not shown with the Classic feed card layout ([15a1700](https://github.com/MorpheApp/morphe-patches/commit/15a170042639ca8bc0b4d871b781c29d00926fb2)) +* **YouTube Music - Downloads:** Fix song menu downloads for accounts without Premium ([#4086](https://github.com/MorpheApp/morphe-patches/issues/4086)) ([93c6893](https://github.com/MorpheApp/morphe-patches/commit/93c689374b0ef72cb97d6c73881ecdb132ce7e91)) + +### ✨ New Features + +* **YouTube - Channel search:** Search the posts of the channel ([#4078](https://github.com/MorpheApp/morphe-patches/issues/4078)) ([93bd035](https://github.com/MorpheApp/morphe-patches/commit/93bd0351362da6c75eb40f47d0ea0e37c632eb1c)) +* **YouTube - Downloads:** Add download item to the Shorts menu ([#4099](https://github.com/MorpheApp/morphe-patches/issues/4099)) ([7159826](https://github.com/MorpheApp/morphe-patches/commit/715982644742ca770a1c61fe292a77bb1ce11296)) +* **YouTube - Downloads:** Add playlist downloads for accounts without Premium ([456e0db](https://github.com/MorpheApp/morphe-patches/commit/456e0db9603f0930a4893e3f24ee3eee8bd4e5f9)) +* **YouTube Music - Third-party lyrics:** Streamline lyrics viewing experience ([#4002](https://github.com/MorpheApp/morphe-patches/issues/4002)) ([00a4bf4](https://github.com/MorpheApp/morphe-patches/commit/00a4bf457e8df233b21e6f73b244b0c12e91e53e)) +* **YouTube:** Add Niconico-style live/replay comments ([#4080](https://github.com/MorpheApp/morphe-patches/issues/4080)) ([e9f77d7](https://github.com/MorpheApp/morphe-patches/commit/e9f77d7c5932499ababf91481b883c30e96ccd1c)) + ## [1.49.0-dev.7](https://github.com/MorpheApp/morphe-patches/compare/v1.49.0-dev.6...v1.49.0-dev.7) (2026-10-26) ### 🐛 Bug Fixes diff --git a/README.md b/README.md index d863c6b1bf..45c77ae454 100644 --- a/README.md +++ b/README.md @@ -35,9 +35,9 @@ All modifications made by Morphe, along with their dates, can be found in the Gi ## 🩹 Patches list -> **[v1.49.0-dev.7](https://github.com/MorpheApp/morphe-patches/releases/tag/v1.49.0-dev.7)**  •  `dev`  •  192 patches total +> **[v1.49.0-dev.8](https://github.com/MorpheApp/morphe-patches/releases/tag/v1.49.0-dev.8)**  •  `dev`  •  193 patches total

-📦 YouTube  •  107 patches +📦 YouTube  •  108 patches
**🎯 Supported versions:** @@ -115,6 +115,7 @@ All modifications made by Morphe, along with their dates, can be found in the Gi | [Mute button](#mute-button) | Adds an option to show a player button that mutes the video audio. | | | [Navigation bar](#navigation-bar) | Adds options to hide and change the bottom navigation bar (such as the Shorts button) and the upper navigation toolbar. | • Hide Search app shortcut
• Hide Subscriptions app shortcut | | [Network proxy](#network-proxy) | Adds settings to route supported network requests through an HTTP or HTTPS proxy. Including this patch may cause connectivity problems on certain devices | | +| [Niconico comments](#niconico-comments) | Adds an option to show live chat and chat replay as comments that scroll across the video. | | | [Open Shorts in regular player](#open-shorts-in-regular-player) | Adds options to open Shorts in the regular video player. | | | [Open channel of live avatar](#open-channel-of-live-avatar) | Adds an option to prevent a channel's current live video from opening when tapping its avatar. | | | [Open links externally](#open-links-externally) | Adds an option to always open links in your browser instead of with the in-app browser. | | diff --git a/gradle.properties b/gradle.properties index a53f4de739..8a13048b1e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,4 +5,4 @@ android.useAndroidX = true # Extensions are Java only. Prevents AGP from adding Kotlin compilation and the Kotlin stdlib. android.builtInKotlin = false kotlin.code.style = official -version = 1.49.0-dev.7 +version = 1.49.0-dev.8 diff --git a/patches-bundle.json b/patches-bundle.json index 41fc4ace5a..d496261244 100644 --- a/patches-bundle.json +++ b/patches-bundle.json @@ -1,7 +1,8 @@ { - "created_at": "2026-10-26T07:46:31", - "description": "## [1.49.0-dev.7](https://github.com/MorpheApp/morphe-patches/compare/v1.49.0-dev.6...v1.49.0-dev.7) (2026-10-26)\n\n### 🐛 Bug Fixes\n\n* **YouTube - Add to queue:** Feed below the player stutters while scrolling ([5545e3c](https://github.com/MorpheApp/morphe-patches/commit/5545e3c87458e60c5e6ef5653053c02d343dd3f8))\n* **YouTube - Add to queue:** Fix native queue on experimental versions ([#4076](https://github.com/MorpheApp/morphe-patches/issues/4076)) ([e009d8b](https://github.com/MorpheApp/morphe-patches/commit/e009d8bddb1bb94525060efc3d9b38ad97ba7d72))\n* **YouTube - Add to queue:** Native queue index errors crash before Android 13 ([c5f9f94](https://github.com/MorpheApp/morphe-patches/commit/c5f9f94ee71a480be00a432ee364f4e7099b9537))\n* **YouTube - Captions:** Old caption stays on screen after SponsorBlock skips ([#3986](https://github.com/MorpheApp/morphe-patches/issues/3986)) ([e200765](https://github.com/MorpheApp/morphe-patches/commit/e20076530e0af6061da9029bfaa4ad985fdd2f50))\n* **YouTube - Disable playlist autoplay:** fix looping the current video ([#4064](https://github.com/MorpheApp/morphe-patches/issues/4064)) ([f4c1f84](https://github.com/MorpheApp/morphe-patches/commit/f4c1f84e13f0ed9b5982adfccbdbec1529ce0091))\n* **YouTube - Force fullscreen landscape:** Screen rotates before the fullscreen gesture completes ([#4050](https://github.com/MorpheApp/morphe-patches/issues/4050)) ([83c0cf0](https://github.com/MorpheApp/morphe-patches/commit/83c0cf004e8cafa142cb862e3d78fd7e98c9f3c3))\n* **YouTube - Litho filter:** Filtered elements leave empty cells in the related videos grid of the tablet layout ([e32b05c](https://github.com/MorpheApp/morphe-patches/commit/e32b05c663e30c23cb998e73fa23d78a58998962))\n* **YouTube - Litho filter:** Filtered elements leave empty cells in the tablet layout grids ([#4035](https://github.com/MorpheApp/morphe-patches/issues/4035)) ([4764483](https://github.com/MorpheApp/morphe-patches/commit/4764483db626de5974ef2d3d3cf19d84ef4cfde1))\n* **YouTube - Minimal miniplayer:** Bar is partly covered by the keyboard with the default navigation bar style ([bb6e0e3](https://github.com/MorpheApp/morphe-patches/commit/bb6e0e38bf4216395abacf8f4afbd45418b25ed7))\n* **YouTube - Minimal miniplayer:** Video title and channel are missing after going back from the Shorts player ([7e665c6](https://github.com/MorpheApp/morphe-patches/commit/7e665c62dc3f88a6f9888b8bc48d1aea885761a1))\n* **YouTube - Miniplayer:** Related videos lists show a black frame after exiting fullscreen and scroll down ([efbc9b1](https://github.com/MorpheApp/morphe-patches/commit/efbc9b1ea1739b617306f19900fa398251131494))\n* **YouTube - Search comments:** Search button is not shown in the Shorts comments and is shown in other panels ([3b3308b](https://github.com/MorpheApp/morphe-patches/commit/3b3308b61740ab01e35aaa0e97166eeab80164e6))\n* **YouTube - Video quality:** Shorts quality menu sets another quality than the one selected ([3e15dcd](https://github.com/MorpheApp/morphe-patches/commit/3e15dcd12190c4b435287080916170e82d0b4ac5))\n* **YouTube - Video quality:** Shorts quality selected in the menu is not kept and is used for the next Shorts ([82efbab](https://github.com/MorpheApp/morphe-patches/commit/82efbab5e735cfa3db7a6cc5b05a5ad00b1b0b96))\n\n### ✨ New Features\n\n* **YouTube - Playback speed:** Add playback speed to the Shorts player menu ([#3982](https://github.com/MorpheApp/morphe-patches/issues/3982)) ([b717a69](https://github.com/MorpheApp/morphe-patches/commit/b717a692dfc3c99bee830af548bd13f98a3a4eaa))\n* **YouTube:** Add `Download post images` patch ([#3966](https://github.com/MorpheApp/morphe-patches/issues/3966)) ([5f96fbe](https://github.com/MorpheApp/morphe-patches/commit/5f96fbe0a90efd54b3e1233c9690aa18f4d38dda))\n* **YouTube:** Show a warning when changing 'Navigation bar style' or turning on 'Enable tablet layout in player' ([445194d](https://github.com/MorpheApp/morphe-patches/commit/445194d74ab5373334e14d31e1ceb29926ac267b))", - "download_url": "https://github.com/MorpheApp/morphe-patches/releases/download/v1.49.0-dev.7/patches-1.49.0-dev.7.mpp", + "created_at": "2026-10-27T09:06:43", + "description": "## [1.49.0-dev.8](https://github.com/MorpheApp/morphe-patches/compare/v1.49.0-dev.7...v1.49.0-dev.8) (2026-10-27)\n\n### 🐛 Bug Fixes\n\n* **YouTube - Channel whitelists:** Flyout menu opens with a delay while the channel is fetched ([5fed7fc](https://github.com/MorpheApp/morphe-patches/commit/5fed7fc701421b701263f742a6ba38fd4cc89dd5))\n* **YouTube - Litho filter:** Crash when scrolling a list after a filtered element is removed ([6d43157](https://github.com/MorpheApp/morphe-patches/commit/6d4315757768d792614915d0867079437505fab9))\n* **YouTube - Playback speed:** Use the bold icon for the Shorts player menu item ([c710042](https://github.com/MorpheApp/morphe-patches/commit/c710042f6cf3f068141e89cd833dd580d7b64b8c))\n* **YouTube - Player icon style:** Add Ionicons picture in picture button icon ([68b8628](https://github.com/MorpheApp/morphe-patches/commit/68b86282725f73c3a92602c792457371ddb88c8e))\n* **YouTube - Premium-style queue:** Fix missing titles and stale now playing row ([#3795](https://github.com/MorpheApp/morphe-patches/issues/3795)) ([ef28427](https://github.com/MorpheApp/morphe-patches/commit/ef28427f63c30329b33b8d83c439c95b731a4fb6))\n* **YouTube - Premium-style queue:** Play the queue when the player Next button is pressed ([#3796](https://github.com/MorpheApp/morphe-patches/issues/3796)) ([ef8dcef](https://github.com/MorpheApp/morphe-patches/commit/ef8dcef6a9ef888a075fb97d963719ddf82d6b42))\n* **YouTube - Return YouTube Dislike:** Feed thumbnail like count is not shown with the Classic feed card layout ([15a1700](https://github.com/MorpheApp/morphe-patches/commit/15a170042639ca8bc0b4d871b781c29d00926fb2))\n* **YouTube Music - Downloads:** Fix song menu downloads for accounts without Premium ([#4086](https://github.com/MorpheApp/morphe-patches/issues/4086)) ([93c6893](https://github.com/MorpheApp/morphe-patches/commit/93c689374b0ef72cb97d6c73881ecdb132ce7e91))\n\n### ✨ New Features\n\n* **YouTube - Channel search:** Search the posts of the channel ([#4078](https://github.com/MorpheApp/morphe-patches/issues/4078)) ([93bd035](https://github.com/MorpheApp/morphe-patches/commit/93bd0351362da6c75eb40f47d0ea0e37c632eb1c))\n* **YouTube - Downloads:** Add download item to the Shorts menu ([#4099](https://github.com/MorpheApp/morphe-patches/issues/4099)) ([7159826](https://github.com/MorpheApp/morphe-patches/commit/715982644742ca770a1c61fe292a77bb1ce11296))\n* **YouTube - Downloads:** Add playlist downloads for accounts without Premium ([456e0db](https://github.com/MorpheApp/morphe-patches/commit/456e0db9603f0930a4893e3f24ee3eee8bd4e5f9))\n* **YouTube Music - Third-party lyrics:** Streamline lyrics viewing experience ([#4002](https://github.com/MorpheApp/morphe-patches/issues/4002)) ([00a4bf4](https://github.com/MorpheApp/morphe-patches/commit/00a4bf457e8df233b21e6f73b244b0c12e91e53e))\n* **YouTube:** Add Niconico-style live/replay comments ([#4080](https://github.com/MorpheApp/morphe-patches/issues/4080)) ([e9f77d7](https://github.com/MorpheApp/morphe-patches/commit/e9f77d7c5932499ababf91481b883c30e96ccd1c))", + "download_url": "https://github.com/MorpheApp/morphe-patches/releases/download/v1.49.0-dev.8/patches-1.49.0-dev.8.mpp", "signature_download_url": "", - "version": "1.49.0-dev.7" + "version": "1.49.0-dev.8", + "public_key": "g8Ys9leTu2yeixojeR35HrNkK6eOSLpA6nNIizRutqI=" } diff --git a/patches-list.json b/patches-list.json index 324d0a00e2..f51318f50d 100644 --- a/patches-list.json +++ b/patches-list.json @@ -1,6 +1,6 @@ { "NOTE": "Do NOT manually edit this file. This file is automatically updated when semantic release (release.yml) runs. Manually editing this file can break your releases and break third party tools that use this file.", - "version": "1.49.0-dev.7", + "version": "1.49.0-dev.8", "patches": [ { "name": "AI channel filter", @@ -8794,6 +8794,85 @@ ], "options": [] }, + { + "name": "Niconico comments", + "description": "Adds an option to show live chat and chat replay as comments that scroll across the video.", + "default": true, + "category": null, + "compatiblePackages": [ + { + "packageName": "com.google.android.youtube", + "name": "YouTube", + "description": null, + "apkFileType": "APK_REQUIRED", + "appIconColor": "#FF0033", + "signatures": [ + "5aad2bee6db95d17e05a08d7d1e64c10a1511879154483916b6ae6c7fd9cb0c6", + "3d7a1223019aa39d9ea0e3436ab7c0896bfb4fb679f4de5fe7c23f326c8f994a" + ], + "formFactor": "MOBILE", + "targets": [ + { + "version": "21.43.523", + "versionCodes": null, + "isExperimental": true, + "minSdk": 29, + "description": null + }, + { + "version": "21.42.241", + "versionCodes": null, + "isExperimental": true, + "minSdk": 29, + "description": null + }, + { + "version": "21.28.208", + "versionCodes": null, + "isExperimental": true, + "minSdk": 29, + "description": null + }, + { + "version": "21.23.492", + "versionCodes": null, + "isExperimental": false, + "minSdk": 28, + "description": null + }, + { + "version": "21.20.405", + "versionCodes": null, + "isExperimental": false, + "minSdk": 28, + "description": null + }, + { + "version": "21.16.256", + "versionCodes": null, + "isExperimental": false, + "minSdk": 28, + "description": null + }, + { + "version": "20.51.39", + "versionCodes": null, + "isExperimental": false, + "minSdk": 28, + "description": null + }, + { + "version": "20.21.37", + "versionCodes": null, + "isExperimental": false, + "minSdk": 26, + "description": null + } + ] + } + ], + "options": [] + }, { "name": "Open Shorts in regular player", "description": "Adds options to open Shorts in the regular video player.", From 04e791d3d1c9fa8c8c352c61b18a6348311a8a27 Mon Sep 17 00:00:00 2001 From: Anurag Rai <129479513+anuraggr@users.noreply.github.com> Date: Tue, 27 Oct 2026 14:40:22 +0530 Subject: [PATCH 099/189] feat(YouTube): Redirect Watch later saves to a chosen playlist (#4016) Co-authored-by: brian --- .../patches/SaveToWatchLaterPatch.java | 53 ++++++++++++++++++- .../youtube/patches/utils/PlaylistPatch.java | 17 ++++-- .../requests/SaveToWatchLaterRequest.java | 4 +- .../extension/youtube/settings/Settings.java | 5 ++ .../WatchLaterPlaylistPreference.java | 53 +++++++++++++++++++ .../videoplayer/SaveToWatchLaterButton.java | 3 +- .../youtube/innertube/playlist_edit.proto | 17 ++++++ .../savetowatchlater/Fingerprints.kt | 44 +++++++++++++++ .../SaveToWatchLaterButtonPatch.kt | 31 +++++++++-- .../addresources/values/youtube/strings.xml | 2 + 10 files changed, 218 insertions(+), 11 deletions(-) create mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/preference/WatchLaterPlaylistPreference.java create mode 100644 extensions/youtube/src/main/proto/app/morphe/extension/youtube/innertube/playlist_edit.proto create mode 100644 patches/src/main/kotlin/app/morphe/patches/youtube/interaction/savetowatchlater/Fingerprints.kt diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/SaveToWatchLaterPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/SaveToWatchLaterPatch.java index 07596d58c8..361340147d 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/SaveToWatchLaterPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/SaveToWatchLaterPatch.java @@ -1,6 +1,7 @@ /* * Copyright 2026 Morphe. - * https://github.com/MorpheApp/morphe-patches + * https://github.com/MorpheApp/morphe-patches/pull/1274 + * https://github.com/MorpheApp/morphe-patches/pull/4016 * * See the included NOTICE file for GPLv3 Section 7 terms that apply to Morphe contributions. */ @@ -11,14 +12,23 @@ import android.text.TextUtils; +import com.google.protobuf.MessageLite; + +import java.util.List; + import app.morphe.extension.shared.Logger; import app.morphe.extension.shared.Utils; import app.morphe.extension.shared.innertube.utils.AuthUtils; +import app.morphe.extension.youtube.innertube.PlaylistEdit.PlaylistEditAction; +import app.morphe.extension.youtube.innertube.PlaylistEdit.PlaylistEditActionType; import app.morphe.extension.youtube.patches.utils.requests.SaveToWatchLaterRequest; +import app.morphe.extension.youtube.settings.Settings; @SuppressWarnings("unused") public final class SaveToWatchLaterPatch { + public static final String WATCH_LATER_PLAYLIST_ID = "WL"; + /** * If the player is not active, the layout may break. * Use it only when it is guaranteed to be used in situations where the player is active. @@ -29,6 +39,47 @@ public static boolean isPatchIncluded() { return false; // Modified during patching. } + /** + * @return The playlist that Watch later saves are added to. + */ + public static String getWatchLaterPlaylistId() { + String redirectId = Settings.SAVE_TO_WATCH_LATER_REDIRECT_PLAYLIST_ID.get(); + return redirectId.isEmpty() ? WATCH_LATER_PLAYLIST_ID : redirectId; + } + + /** + * Injection point. + * + * @param actions Edit actions of the request. + * @return The playlist the edit request is sent to. + */ + public static String getEditPlaylistId(String playlistId, List actions) { + try { + if (!WATCH_LATER_PLAYLIST_ID.equals(playlistId)) { + return playlistId; + } + String redirectId = Settings.SAVE_TO_WATCH_LATER_REDIRECT_PLAYLIST_ID.get(); + if (redirectId.isEmpty()) { + return playlistId; + } + + // Only redirect video additions. Removals reference the + // video entry of the Watch later playlist and fail with any other playlist. + for (MessageLite action : actions) { + if (PlaylistEditAction.parseFrom(action.toByteArray()).getAction() + != PlaylistEditActionType.ACTION_ADD_VIDEO) { + return playlistId; + } + } + + return redirectId; + } catch (Exception ex) { + Logger.printException(() -> "getEditPlaylistId failure", ex); + } + + return playlistId; + } + public static void saveVideo(String videoId) { // Prevent a new request until the previous (if exists) is not done. if (saveToWatchLaterRequest != null && !saveToWatchLaterRequest.fetchIsDone()) { diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/PlaylistPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/PlaylistPatch.java index 109f9af67b..e598812994 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/PlaylistPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/PlaylistPatch.java @@ -29,6 +29,7 @@ import java.util.LinkedHashMap; import java.util.Map; +import java.util.function.BiConsumer; import java.util.function.Function; import app.morphe.extension.shared.Logger; @@ -283,14 +284,21 @@ private static void saveToPlaylist(Context context) { handleCheckError(checkFailedQueue); return; } + showPlaylistPicker(context, currentPlaylistId, PlaylistPatch::saveToPlaylist); + } + + public static void showPlaylistPicker(Context context, String sourcePlaylistId, + BiConsumer onSelected) { try { GetPlaylistsRequest request = GetPlaylistsRequest.fetchRequestIfNeeded( - currentPlaylistId, AuthUtils.getRequestHeader()); + sourcePlaylistId, AuthUtils.getRequestHeader()); if (request == null) { return; } - Utils.runOnBackgroundThread(() -> { + Utils.runOnBackgroundThread(() -> { Pair[] playlists = request.getPlaylists(); + // Fetch again next time, so a failed request can be retried. + GetPlaylistsRequest.clear(); if (playlists == null || playlists.length == 0) return; Utils.runOnMainThread(() -> { SheetBottomDialog.DraggableLinearLayout mainLayout = SheetBottomDialog @@ -304,7 +312,7 @@ private static void saveToPlaylist(Context context) { for (Pair playlist : playlists) { String listId = playlist.first; String title = playlist.second; - Runnable action = () -> saveToPlaylist(listId, title); + Runnable action = () -> onSelected.accept(listId, title); View itemLayout = createItemLayout(context, title, libraryIconId); actionsMap.put(itemLayout, action); listContainer.addView(itemLayout); @@ -325,11 +333,10 @@ private static void saveToPlaylist(Context context) { }); } dialog.show(); - GetPlaylistsRequest.clear(); }); }); } catch (Exception ex) { - Logger.printException(() -> "saveToPlaylist failure", ex); + Logger.printException(() -> "showPlaylistPicker failure", ex); } } diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/requests/SaveToWatchLaterRequest.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/requests/SaveToWatchLaterRequest.java index 565f2bebbb..69236266a8 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/requests/SaveToWatchLaterRequest.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/requests/SaveToWatchLaterRequest.java @@ -27,6 +27,7 @@ import app.morphe.extension.shared.Utils; import app.morphe.extension.shared.requests.Requester; import app.morphe.extension.shared.settings.BaseSettings; +import app.morphe.extension.youtube.patches.SaveToWatchLaterPatch; public class SaveToWatchLaterRequest { private static final int MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 20 * 1000; @@ -85,7 +86,8 @@ private static JSONObject sendRequest( Logger.printDebug(() -> "Fetching save to playlist request, videoId: " + videoId); try { - byte[] requestBody = PlaylistRoutes.editPlaylistBody(videoId, "WL", null); + byte[] requestBody = PlaylistRoutes.editPlaylistBody( + videoId, SaveToWatchLaterPatch.getWatchLaterPlaylistId(), null); HttpURLConnection connection = PlaylistRoutes.getConnection(PlaylistRoutes.EDIT_PLAYLIST, requestHeader); connection.setFixedLengthStreamingMode(requestBody.length); connection.getOutputStream().write(requestBody); diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/Settings.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/Settings.java index bb4074a733..8593f8582b 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/Settings.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/Settings.java @@ -341,6 +341,11 @@ public class Settings extends SharedYouTubeSettings { public static final BooleanSetting SAVE_TO_WATCH_LATER_FLYOUT_BUTTON = new BooleanSetting("morphe_save_to_watch_later_flyout_button", FALSE); public static final BooleanSetting KIDS_SAVE_TO_WATCH_LATER_FLYOUT_BUTTON = new BooleanSetting("morphe_save_to_watch_later_kids_flyout_button", TRUE); public static final BooleanSetting SHORTS_SAVE_TO_WATCH_LATER_FLYOUT_BUTTON = new BooleanSetting("morphe_save_to_watch_later_shorts_flyout_button", TRUE); + /** + * Playlist ID selected in the picker. Empty to keep the stock Watch later playlist. + */ + public static final StringSetting SAVE_TO_WATCH_LATER_REDIRECT_PLAYLIST_ID = new StringSetting("morphe_save_to_watch_later_redirect_playlist_id", "", false); + public static final StringSetting SAVE_TO_WATCH_LATER_REDIRECT_PLAYLIST_TITLE = new StringSetting("morphe_save_to_watch_later_redirect_playlist_title", "", false); public static final EnumSetting QUEUE_TYPE = new EnumSetting<>("morphe_queue_type", QueueType.MORPHE); public static final StringSetting LOCAL_QUEUE_ITEMS = new StringSetting("morphe_local_queue_items", "", false, false); public static final BooleanSetting QUEUE_RESTORE = new BooleanSetting("morphe_queue_restore", FALSE, parentsAll(parent(SAVE_TO_WATCH_LATER_OVERLAY_BUTTON), new NotLocalQueueAvailability())); diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/preference/WatchLaterPlaylistPreference.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/preference/WatchLaterPlaylistPreference.java new file mode 100644 index 0000000000..48474eb03a --- /dev/null +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/preference/WatchLaterPlaylistPreference.java @@ -0,0 +1,53 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to Morphe contributions. + */ + +package app.morphe.extension.youtube.settings.preference; + +import static app.morphe.extension.shared.StringRef.str; +import static app.morphe.extension.youtube.patches.SaveToWatchLaterPatch.WATCH_LATER_PLAYLIST_ID; + +import android.content.Context; +import android.preference.Preference; +import android.util.AttributeSet; + +import app.morphe.extension.shared.Utils; +import app.morphe.extension.shared.innertube.utils.AuthUtils; +import app.morphe.extension.youtube.patches.utils.PlaylistPatch; +import app.morphe.extension.youtube.settings.Settings; + +/** + * Selects the playlist that Watch later saves are added to. + */ +@SuppressWarnings({"unused", "deprecation"}) +public class WatchLaterPlaylistPreference extends Preference { + + public WatchLaterPlaylistPreference(Context context, AttributeSet attrs) { + super(context, attrs); + String title = Settings.SAVE_TO_WATCH_LATER_REDIRECT_PLAYLIST_TITLE.get(); + if (!title.isEmpty()) { + setSummary(title); + } + } + + @Override + protected void onClick() { + if (AuthUtils.isNotLoggedIn()) { + Utils.showToastShort(str("morphe_queue_manager_check_failed_auth")); + return; + } + + PlaylistPatch.showPlaylistPicker(getContext(), WATCH_LATER_PLAYLIST_ID, (id, title) -> { + if (WATCH_LATER_PLAYLIST_ID.equals(id)) { + Settings.SAVE_TO_WATCH_LATER_REDIRECT_PLAYLIST_ID.resetToDefault(); + } else { + Settings.SAVE_TO_WATCH_LATER_REDIRECT_PLAYLIST_ID.save(id); + } + Settings.SAVE_TO_WATCH_LATER_REDIRECT_PLAYLIST_TITLE.save(title); + setSummary(title); + }); + } +} diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/videoplayer/SaveToWatchLaterButton.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/videoplayer/SaveToWatchLaterButton.java index 6c3a332244..8739811533 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/videoplayer/SaveToWatchLaterButton.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/videoplayer/SaveToWatchLaterButton.java @@ -1,6 +1,7 @@ /* * Copyright 2026 Morphe. - * https://github.com/MorpheApp/morphe-patches + * https://github.com/MorpheApp/morphe-patches/pull/1274 + * https://github.com/MorpheApp/morphe-patches/pull/4016 * * See the included NOTICE file for GPLv3 Section 7 terms that apply to Morphe contributions. */ diff --git a/extensions/youtube/src/main/proto/app/morphe/extension/youtube/innertube/playlist_edit.proto b/extensions/youtube/src/main/proto/app/morphe/extension/youtube/innertube/playlist_edit.proto new file mode 100644 index 0000000000..b7dcf13bde --- /dev/null +++ b/extensions/youtube/src/main/proto/app/morphe/extension/youtube/innertube/playlist_edit.proto @@ -0,0 +1,17 @@ +syntax = "proto3"; + +package app.morphe.extension.youtube.innertube; + +option optimize_for = LITE_RUNTIME; +option java_package = "app.morphe.extension.youtube.innertube"; + +// Action of an edit playlist request. +// Only the fields that are read are declared. +message PlaylistEditAction { + optional PlaylistEditActionType action = 6; +} + +enum PlaylistEditActionType { + ACTION_UNKNOWN = 0; + ACTION_ADD_VIDEO = 1; +} diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/savetowatchlater/Fingerprints.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/savetowatchlater/Fingerprints.kt new file mode 100644 index 0000000000..8795eb92b5 --- /dev/null +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/savetowatchlater/Fingerprints.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.patches.youtube.interaction.savetowatchlater + +import app.morphe.patcher.Fingerprint +import app.morphe.patcher.fieldAccess +import app.morphe.patcher.string +import com.android.tools.smali.dexlib2.Opcode + +internal object EditPlaylistRequestConstructorFingerprint : Fingerprint( + name = "", + filters = listOf( + string("browse/edit_playlist"), + ), +) + +/** + * Builds the edit playlist request proto. All playlist edits of the app go through this method, + * including adding to and removing from Watch later (playlist id `WL`). + */ +internal object EditPlaylistRequestBuildFingerprint : Fingerprint( + classFingerprint = EditPlaylistRequestConstructorFingerprint, + returnType = "L", + parameters = listOf(), + filters = listOf( + // Playlist id. + fieldAccess( + opcode = Opcode.IGET_OBJECT, + definingClass = "this", + type = "Ljava/lang/String;", + ), + // Edit actions. + fieldAccess( + opcode = Opcode.IGET_OBJECT, + definingClass = "this", + type = "Ljava/util/List;", + ), + ), +) diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/savetowatchlater/SaveToWatchLaterButtonPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/savetowatchlater/SaveToWatchLaterButtonPatch.kt index 62415f7281..63edbb503d 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/savetowatchlater/SaveToWatchLaterButtonPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/savetowatchlater/SaveToWatchLaterButtonPatch.kt @@ -1,14 +1,17 @@ /* * Copyright 2026 Morphe. - * https://github.com/MorpheApp/morphe-patches + * https://github.com/MorpheApp/morphe-patches/pull/1274 + * https://github.com/MorpheApp/morphe-patches/pull/4016 * * See the included NOTICE file for GPLv3 Section 7 terms that apply to Morphe contributions. */ package app.morphe.patches.youtube.interaction.savetowatchlater +import app.morphe.patcher.extensions.InstructionExtensions.addInstructions import app.morphe.patcher.patch.bytecodePatch import app.morphe.patcher.patch.resourcePatch +import app.morphe.patches.shared.misc.settings.preference.NonInteractivePreference import app.morphe.patches.shared.misc.settings.preference.SwitchPreference import app.morphe.patches.shared.misc.settings.preference.noTitleUnsortedPreferenceCategory import app.morphe.patches.youtube.layout.buttons.overlay.addPlayerOverlayPreferences @@ -43,7 +46,8 @@ private const val EXTENSION_CLASS = @Suppress("unused") val saveToWatchLaterButtonPatch = bytecodePatch( name = "Save to Watch later", - description = "Adds an option to display save to Watch later button in the video player.", + description = "Adds an option to display save to Watch later button in the video player, " + + "and adds a setting to redirect Watch later saves to a playlist of your choice.", ) { dependsOn( saveToWatchLaterButtonResourcePatch, @@ -79,7 +83,12 @@ val saveToWatchLaterButtonPatch = bytecodePatch( PreferenceScreen.PLAYER.addPreferences( SwitchPreference("morphe_save_to_watch_later_flyout_button", summary = true), - SwitchPreference("morphe_save_to_watch_later_kids_flyout_button", summary = true) + SwitchPreference("morphe_save_to_watch_later_kids_flyout_button", summary = true), + NonInteractivePreference( + "morphe_save_to_watch_later_redirect_playlist_id", + tag = "app.morphe.extension.youtube.settings.preference.WatchLaterPlaylistPreference", + selectable = true + ) ) PreferenceScreen.SHORTS.addPreferences( @@ -89,5 +98,21 @@ val saveToWatchLaterButtonPatch = bytecodePatch( initializeTopControl(EXTENSION_BUTTON) setExtensionIsPatchIncluded(EXTENSION_CLASS) + + EditPlaylistRequestBuildFingerprint.let { + val playlistIdField = it.instructionMatches[0].getFieldAccessed() + val actionsField = it.instructionMatches[1].getFieldAccessed() + + it.method.addInstructions( + 0, + """ + iget-object v0, p0, $playlistIdField + iget-object v1, p0, $actionsField + invoke-static { v0, v1 }, $EXTENSION_CLASS->getEditPlaylistId(Ljava/lang/String;Ljava/util/List;)Ljava/lang/String; + move-result-object v0 + iput-object v0, p0, $playlistIdField + """ + ) + } } } diff --git a/patches/src/main/resources/addresources/values/youtube/strings.xml b/patches/src/main/resources/addresources/values/youtube/strings.xml index abefa711f6..c4faa33884 100644 --- a/patches/src/main/resources/addresources/values/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values/youtube/strings.xml @@ -669,6 +669,8 @@ Info: May not work on live streams" Allows saving kid related videos in the \'Watch later\' from the flyout menu Show Save to Watch later button in the shorts flyout Allows saving shorts in the \'Watch later\' from the flyout menu + Redirect Watch later to playlist + Watch later Add to Morphe queue Keep queue between sessions Queue is kept after restarting the app From 90d011dc007cf7e433196fd27f392c87d32363f6 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Tue, 27 Oct 2026 09:48:38 +0000 Subject: [PATCH 100/189] fix(YouTube - Downloads): Downloaded videos that work at first may not work later --- .../youtube/patches/InAppDownloadsPatch.java | 47 +++++++++++++++++-- .../extension/youtube/innertube/offline.proto | 4 ++ .../music/interaction/jam/Fingerprints.kt | 12 ----- .../patches/music/interaction/jam/JamABI.kt | 1 + .../shared/misc/fix/proto/Fingerprints.kt | 11 +++++ .../interaction/downloads/DownloadsPatch.kt | 42 +++++++++++++++-- .../interaction/downloads/Fingerprints.kt | 20 ++++++++ .../nativeplaylistqueue/BridgeMethods.kt | 1 + .../nativeplaylistqueue/Fingerprints.kt | 9 ---- 9 files changed, 119 insertions(+), 28 deletions(-) diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/InAppDownloadsPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/InAppDownloadsPatch.java index b94a17b680..981f38d910 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/InAppDownloadsPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/InAppDownloadsPatch.java @@ -287,10 +287,7 @@ public static byte[] replaceDeclinedOfflinePlayerResponse(@Nullable Object playe // and only the offline state token and player config are kept from the declined response. OfflinePlayerResponse.Builder builder = OfflinePlayerResponse.parseFrom(streams.streamingData()).toBuilder(); builder.setStreamingData(savableStreamingData(builder.getStreamingData())) - .setOfflineState(OfflineState.newBuilder() - .setToken(original.getOfflineState().getToken()) - .setPolicyLifetimeSeconds(OFFLINE_POLICY_LIFETIME_SECONDS) - .setAction(OfflineStateAction.OFFLINE_STATE_ACTION_OK)); + .setOfflineState(playableOfflineState(original.getOfflineState().getToken())); // Without the ExoPlayer config the app only looks at the progressive formats, which are not savable. // The player config of the app's own response fits the device, so it's kept if there is one. @@ -310,6 +307,48 @@ public static byte[] replaceDeclinedOfflinePlayerResponse(@Nullable Object playe } } + /** + * Injection point. + *

+ * Called off the main thread with the offline state the server gives a saved video when the app + * refreshes its saved videos, which the server schedules about every 12 hours. + * The server disables saved videos that need Premium to be saved, such as music videos, + * and the app then shows an error instead of playing them. + * + * @param offlineState New offline state of a saved video. + * @return An offline state that keeps the video playable, or null to keep the original. + */ + @Nullable + public static byte[] replaceDisabledOfflineState(@Nullable MessageLite offlineState) { + try { + if (Settings.DOWNLOAD_MODE.get() != DownloadMode.IN_APP || offlineState == null) { + return null; + } + + OfflineState state = OfflineState.parseFrom(offlineState.toByteArray()); + if (state.getAction() != OfflineStateAction.OFFLINE_STATE_ACTION_DISABLE) { + return null; + } + + Logger.printDebug(() -> "Replacing disabled offline state with reason: " + state.getDisabledReason()); + return playableOfflineState(state.getToken()).toByteArray(); + } catch (Exception ex) { + Logger.printException(() -> "replaceDisabledOfflineState failure", ex); + return null; + } + } + + /** + * @return Offline state that keeps a saved video playable, without asking the server again for a long time. + */ + private static OfflineState playableOfflineState(String token) { + return OfflineState.newBuilder() + .setToken(token) + .setPolicyLifetimeSeconds(OFFLINE_POLICY_LIFETIME_SECONDS) + .setAction(OfflineStateAction.OFFLINE_STATE_ACTION_OK) + .build(); + } + /** * Injection point. *

diff --git a/extensions/youtube/src/main/proto/app/morphe/extension/youtube/innertube/offline.proto b/extensions/youtube/src/main/proto/app/morphe/extension/youtube/innertube/offline.proto index 38bc517674..9429477660 100644 --- a/extensions/youtube/src/main/proto/app/morphe/extension/youtube/innertube/offline.proto +++ b/extensions/youtube/src/main/proto/app/morphe/extension/youtube/innertube/offline.proto @@ -150,11 +150,15 @@ message OfflineState { // Seconds until the saved video must be refreshed with the server. optional uint32 policyLifetimeSeconds = 3; optional OfflineStateAction action = 5; + // Message of a disabled saved video, such as "YouTube Premium required". + optional string disabledReason = 6; } enum OfflineStateAction { OFFLINE_STATE_ACTION_UNKNOWN = 0; OFFLINE_STATE_ACTION_OK = 1; + // The saved video stays in the downloads, but can't be played. + OFFLINE_STATE_ACTION_DISABLE = 3; } // Buttons of a page header, such as the Play all, Save and Share buttons of a playlist page. diff --git a/patches/src/main/kotlin/app/morphe/patches/music/interaction/jam/Fingerprints.kt b/patches/src/main/kotlin/app/morphe/patches/music/interaction/jam/Fingerprints.kt index b2249b5f74..be041bb37c 100644 --- a/patches/src/main/kotlin/app/morphe/patches/music/interaction/jam/Fingerprints.kt +++ b/patches/src/main/kotlin/app/morphe/patches/music/interaction/jam/Fingerprints.kt @@ -572,18 +572,6 @@ internal fun accountPeerDispatchFingerprint(peerType: String, forwarder: MethodR filters = listOf(methodCall(reference = forwarder)), ) -internal fun protoDefaultInstanceFingerprint(messageType: String) = Fingerprint( - definingClass = messageType, - name = "", - filters = listOf( - fieldAccess( - definingClass = messageType, - type = messageType, - opcode = Opcode.SPUT_OBJECT - ) - ) -) - /** Generic protobuf parser inherited by the already resolved native message type. */ internal fun protoParserFingerprint(messageHierarchy: Set) = Fingerprint( name = "parseFrom", diff --git a/patches/src/main/kotlin/app/morphe/patches/music/interaction/jam/JamABI.kt b/patches/src/main/kotlin/app/morphe/patches/music/interaction/jam/JamABI.kt index e6289fd5a5..e8366baada 100644 --- a/patches/src/main/kotlin/app/morphe/patches/music/interaction/jam/JamABI.kt +++ b/patches/src/main/kotlin/app/morphe/patches/music/interaction/jam/JamABI.kt @@ -17,6 +17,7 @@ import app.morphe.patcher.methodCall import app.morphe.patcher.newInstance import app.morphe.patcher.patch.BytecodePatchContext import app.morphe.patcher.util.getReference +import app.morphe.patches.shared.misc.fix.proto.protoDefaultInstanceFingerprint import app.morphe.util.findInstructionIndicesReversed import app.morphe.util.findInstructionIndicesReversedOrThrow import app.morphe.util.getMutableMethod diff --git a/patches/src/main/kotlin/app/morphe/patches/shared/misc/fix/proto/Fingerprints.kt b/patches/src/main/kotlin/app/morphe/patches/shared/misc/fix/proto/Fingerprints.kt index 8c70e70713..c7fc3e437a 100644 --- a/patches/src/main/kotlin/app/morphe/patches/shared/misc/fix/proto/Fingerprints.kt +++ b/patches/src/main/kotlin/app/morphe/patches/shared/misc/fix/proto/Fingerprints.kt @@ -14,6 +14,17 @@ import app.morphe.patcher.opcode import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode +/** + * A generated protobuf message creates its default instance in its static initializer. + */ +internal fun protoDefaultInstanceFingerprint(messageType: String) = Fingerprint( + definingClass = messageType, + name = "", + filters = listOf( + fieldAccess(definingClass = messageType, type = messageType, opcode = Opcode.SPUT_OBJECT) + ) +) + internal object EmptyRegistryFingerprint : Fingerprint( definingClass = "Lcom/google/protobuf/ExtensionRegistryLite;", name ="getGeneratedRegistry", diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/downloads/DownloadsPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/downloads/DownloadsPatch.kt index db92680e9a..f3bb31e209 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/downloads/DownloadsPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/downloads/DownloadsPatch.kt @@ -12,13 +12,16 @@ package app.morphe.patches.youtube.interaction.downloads import app.morphe.patcher.extensions.InstructionExtensions.addInstructions import app.morphe.patcher.extensions.InstructionExtensions.addInstructionsWithLabels +import app.morphe.patcher.patch.BytecodePatchContext import app.morphe.patcher.patch.bytecodePatch import app.morphe.patcher.patch.resourcePatch +import app.morphe.patcher.util.cloneParameters import app.morphe.patcher.util.getReference import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable import app.morphe.patcher.util.registersUsed import app.morphe.patches.shared.misc.fix.proto.fixProtoLibraryPatch import app.morphe.patches.shared.misc.fix.proto.parseByteArrayWithRegistryMethodRef +import app.morphe.patches.shared.misc.fix.proto.protoDefaultInstanceFingerprint import app.morphe.patches.shared.misc.proto.hookElement import app.morphe.patches.shared.misc.settings.preference.ListPreference import app.morphe.patches.shared.misc.settings.preference.PreferenceScreenPreference @@ -107,6 +110,12 @@ private const val EXTENSION_BUTTON = "Lapp/morphe/extension/youtube/videoplayer/ private const val GET_GENERATED_REGISTRY = "Lcom/google/protobuf/ExtensionRegistryLite;->getGeneratedRegistry()Lcom/google/protobuf/ExtensionRegistryLite;" +/** + * @return The default instance field of a proto message, which the message is parsed with. + */ +private fun BytecodePatchContext.protoDefaultInstance(messageType: String) = + protoDefaultInstanceFingerprint(messageType).instructionMatches.first().getFieldAccessed() + @Suppress("unused") val downloadsPatch = bytecodePatch( name = "Downloads", @@ -180,7 +189,7 @@ val downloadsPatch = bytecodePatch( if-eqz v0, :keep_command # The endpoint is an extension, which is lost if parsed without the registry. - sget-object v1, $commandType->a:$commandType + sget-object v1, ${protoDefaultInstance(commandType)} invoke-static { }, $GET_GENERATED_REGISTRY move-result-object v2 invoke-static { v1, v0, v2 }, ${parseByteArrayWithRegistryMethodRef.get()!!} @@ -205,7 +214,7 @@ val downloadsPatch = bytecodePatch( move-result-object v0 if-eqz v0, :keep_offlineability - sget-object v1, $offlineabilityType->a:$offlineabilityType + sget-object v1, ${protoDefaultInstance(offlineabilityType)} invoke-static { }, $GET_GENERATED_REGISTRY move-result-object v2 invoke-static { v1, v0, v2 }, ${parseByteArrayWithRegistryMethodRef.get()!!} @@ -235,7 +244,7 @@ val downloadsPatch = bytecodePatch( if-eqz v0, :keep_upsell_response # The endpoint of the command is an extension, which is lost if parsed without the registry. - sget-object v1, $responseType->a:$responseType + sget-object v1, ${protoDefaultInstance(responseType)} invoke-static { }, $GET_GENERATED_REGISTRY move-result-object v2 invoke-static { v1, v0, v2 }, ${parseByteArrayWithRegistryMethodRef.get()!!} @@ -314,6 +323,33 @@ val downloadsPatch = bytecodePatch( ) } + // Keep saved videos playable, if the server disables them when they are refreshed. + UpdateOfflineVideoPolicyFingerprint.matchAll().forEach { match -> + // The method has too few free registers. + match.method.cloneParameters().apply { + val offlineStateType = parameterTypes[2].toString() + + addInstructionsWithLabels( + 0, + """ + invoke-static/range { p4 .. p4 }, $EXTENSION_IN_APP_DOWNLOADS_CLASS->replaceDisabledOfflineState(Lcom/google/protobuf/MessageLite;)[B + move-result-object v0 + if-eqz v0, :keep_offline_state + + sget-object v1, ${protoDefaultInstance(offlineStateType)} + invoke-static { }, $GET_GENERATED_REGISTRY + move-result-object v2 + invoke-static { v1, v0, v2 }, ${parseByteArrayWithRegistryMethodRef.get()!!} + move-result-object v0 + check-cast v0, $offlineStateType + move-object/from16 p4, v0 + :keep_offline_state + nop + """ + ) + } + } + // endregion } } diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/downloads/Fingerprints.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/downloads/Fingerprints.kt index 6a1a6be4ca..01be39d294 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/downloads/Fingerprints.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/downloads/Fingerprints.kt @@ -133,6 +133,26 @@ internal object OfflinePlayerResponseFingerprint : Fingerprint( ) ) +/** + * Updates the offline state of a saved video with the state of the 'player/refresh' response. + * Matches two methods, one for each of the two ways the app refreshes saved videos. + */ +internal object UpdateOfflineVideoPolicyFingerprint : Fingerprint( + returnType = "V", + parameters = listOf( + "L", + "J", // Time of the refresh. + "L", // Offline state. + "L" // Video policy. + ), + filters = listOf( + anyInstruction( + string("[Offline] UpdateVideoPolicy failed for video "), // 20.x + string("[Offline] UpdateVideoPolicy failed for video %s") // 21.x + ) + ) +) + /** * Creates a player response model from the bytes of a player response. */ diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/BridgeMethods.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/BridgeMethods.kt index 481127085f..8c5968c064 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/BridgeMethods.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/BridgeMethods.kt @@ -14,6 +14,7 @@ import app.morphe.patcher.util.proxy.mutableTypes.MutableClass import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable import app.morphe.patches.shared.misc.fix.proto.ProtobufClassParseByteArrayWithRegistryFingerprint +import app.morphe.patches.shared.misc.fix.proto.protoDefaultInstanceFingerprint import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation import com.android.tools.smali.dexlib2.iface.reference.FieldReference diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/Fingerprints.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/Fingerprints.kt index 4a865ae45c..125f9c0402 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/Fingerprints.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/Fingerprints.kt @@ -28,15 +28,6 @@ import com.android.tools.smali.dexlib2.iface.Method import com.android.tools.smali.dexlib2.iface.reference.FieldReference import com.android.tools.smali.dexlib2.iface.reference.MethodReference -/** A generated protobuf message creates its default instance in its static initializer. */ -internal fun protoDefaultInstanceFingerprint(messageType: String) = Fingerprint( - definingClass = messageType, - name = "", - filters = listOf( - fieldAccess(definingClass = messageType, type = messageType, opcode = Opcode.SPUT_OBJECT) - ) -) - /** The native batch request, not the modern panel's single-video parameter. */ internal object QueueSaveRequestFingerprint : Fingerprint( name = "", From d4741b9db37261876d7f6bd9518fb272af181758 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Tue, 27 Oct 2026 10:32:50 +0000 Subject: [PATCH 101/189] chore: Add another tablet layout disclaimer https://github.com/MorpheApp/morphe-patches/issues/4089 --- .../src/main/resources/addresources/values/youtube/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/patches/src/main/resources/addresources/values/youtube/strings.xml b/patches/src/main/resources/addresources/values/youtube/strings.xml index c4faa33884..ae15f26b31 100644 --- a/patches/src/main/resources/addresources/values/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values/youtube/strings.xml @@ -1219,6 +1219,7 @@ Tablet layout • Remix button and Sound button are not available in Shorts • 'More videos' overlay may not be available • Non collapsed old video action bar is shown +• Flyout menu items may not work correctly Automotive layout • Feed is organized by topics and channels From 42697d0e47e7bad35fc6d7ea33d72cd8ae922afb Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Tue, 27 Oct 2026 10:56:40 +0000 Subject: [PATCH 102/189] chore: Update translations from Crowdin --- .../values-af-rZA/youtube/strings.xml | 31 +++++++++------- .../values-am-rET/youtube/strings.xml | 29 ++++++++------- .../values-ar-rSA/youtube/strings.xml | 37 ++++++++++--------- .../values-az-rAZ/youtube/strings.xml | 23 ++++-------- .../values-bg-rBG/youtube/strings.xml | 25 +++++++------ .../values-bn-rBD/youtube/strings.xml | 20 +++++++++- .../values-cs-rCZ/youtube/strings.xml | 29 ++++++++------- .../values-da-rDK/youtube/strings.xml | 27 ++++++++------ .../values-de-rDE/youtube/strings.xml | 26 +++++++------ .../values-el-rGR/youtube/strings.xml | 20 +++++++++- .../values-es-rES/youtube/strings.xml | 8 ++-- .../values-fa-rIR/youtube/strings.xml | 8 ++-- .../values-fi-rFI/youtube/strings.xml | 21 ++++++----- .../values-fil-rPH/youtube/strings.xml | 23 +++++++----- .../values-fr-rFR/youtube/strings.xml | 19 ++++++---- .../values-ga-rIE/youtube/strings.xml | 2 + .../values-hi-rIN/youtube/strings.xml | 15 +++++--- .../values-hr-rHR/youtube/strings.xml | 25 +++++++------ .../values-hu-rHU/youtube/strings.xml | 29 ++++++++------- .../values-in-rID/youtube/strings.xml | 2 + .../values-is-rIS/youtube/strings.xml | 18 ++------- .../values-it-rIT/youtube/strings.xml | 2 + .../values-iw-rIL/youtube/strings.xml | 29 ++++++++------- .../values-ja-rJP/youtube/strings.xml | 26 +++++++------ .../values-kmr-rTR/youtube/strings.xml | 1 + .../values-ko-rKR/youtube/strings.xml | 14 ++++--- .../values-lt-rLT/youtube/strings.xml | 27 ++++++++------ .../values-ms-rMY/youtube/strings.xml | 16 -------- .../values-nb-rNO/youtube/strings.xml | 25 +++++++------ .../values-nl-rNL/youtube/strings.xml | 37 ++++++++++--------- .../values-pl-rPL/youtube/strings.xml | 25 +++++++------ .../values-pt-rBR/youtube/strings.xml | 2 + .../values-pt-rPT/youtube/strings.xml | 23 +++++++----- .../values-ro-rRO/youtube/strings.xml | 25 +++++++------ .../values-ru-rRU/youtube/strings.xml | 22 ++++++----- .../values-sk-rSK/youtube/strings.xml | 20 ++++++---- .../values-sl-rSI/youtube/strings.xml | 27 ++++++++------ .../values-sr-rCS/youtube/strings.xml | 27 ++++++++------ .../values-sr-rSP/youtube/strings.xml | 20 +++++++++- .../values-sv-rSE/youtube/strings.xml | 27 ++++++++------ .../values-th-rTH/youtube/strings.xml | 29 ++++++++------- .../values-tr-rTR/youtube/strings.xml | 12 +++--- .../values-uk-rUA/youtube/strings.xml | 2 + .../values-vi-rVN/youtube/strings.xml | 23 +++++++----- .../values-zh-rCN/youtube/strings.xml | 27 ++++++++------ .../values-zh-rTW/youtube/strings.xml | 27 ++++++++------ .../values-zu-rZA/youtube/strings.xml | 16 -------- 47 files changed, 538 insertions(+), 430 deletions(-) diff --git a/patches/src/main/resources/addresources/values-af-rZA/youtube/strings.xml b/patches/src/main/resources/addresources/values-af-rZA/youtube/strings.xml index 5578ceaf57..5239cabc25 100644 --- a/patches/src/main/resources/addresources/values-af-rZA/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-af-rZA/youtube/strings.xml @@ -613,6 +613,8 @@ Info: Werk moontlik nie op regstreekse uitsendings nie" Dit laat jou toe om kinderverwante video\'s vanuit die kieslys na \'Kyk later\' te stoor. Wys die \'Stoor na Kyk later\'-knoppie in die kortvideokieslys Dit laat jou toe om kortvideo\'s vanuit die kieslys na \'Kyk later\' te stoor. + Stuur \"Kyk later\" na speellys toe + Kyk later Voeg by Morphe-tou Behou tou tussen sessies Tou word behou na die herbegin van die app @@ -1109,22 +1111,23 @@ Instellings → Weergawe (of outoplay) → Speel volgende video outomaties"Foon Tablet Outomaties - "Veranderinge sluit in: + "Veranderings sluit in: -Tabletuitleg -• Gemeenskapsplasings is versteek -• Die instelling 'Terugspeel in feeds' is nie beskikbaar nie -• Remix-knoppie en klankknoppie is nie in Shorts beskikbaar nie -• Die 'Meer video's'-oorleg mag dalk nie beskikbaar wees nie -• Die nie-ingeklapte ou video-aksiebalk word gewys +Tablet-uitleg +• Gemeenskappepsplasings is versteek +• 'Playback in feeds'-instelling is nie beskikbaar nie +• Remix-knoppie en Klank-knoppie is nie beskikbaar in Shorts nie +• 'Meer video's'-oorvleuning is moontlik nie beskikbaar nie +• Nie-ingeklapte ou video-aksiebalk word vertoon +• Uitklapmenu-items werk moontlik nie korrek nie -Motoruitleg -• Die feed word volgens onderwerpe en kanale georganiseer -• Die instelling 'Terugspeel in feeds' is nie beskikbaar nie -• Remix-knoppie en klankknoppie is nie in Shorts beskikbaar nie -• Die 'Meer video's'-oorleg mag dalk nie beskikbaar wees nie -• Shorts word in die gewone speler geopen -• Die nie-ingeklapte ou video-aksiebalk word gewys" +Automotive-uitleg +• Feed is georganiseer volgens onderwerpe en kanale +• 'Playback in feeds'-instelling is nie beskikbaar nie +• Remix-knoppie en Klank-knoppie is nie beskikbaar in Shorts nie +• 'Meer video's'-oorvleuning is moontlik nie beskikbaar nie +• Shorts maak oop in die gewone speler +• Nie-ingeklapte ou video-aksiebalk word vertoon" Aktiveer tablet-uitleg in die speler "Herstel die ou speleruitleg diff --git a/patches/src/main/resources/addresources/values-am-rET/youtube/strings.xml b/patches/src/main/resources/addresources/values-am-rET/youtube/strings.xml index 0ec45728de..a4d29efdd9 100644 --- a/patches/src/main/resources/addresources/values-am-rET/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-am-rET/youtube/strings.xml @@ -613,6 +613,8 @@ OpenRouter.aiን ለመክፈት እዚህ ይጫኑ" ከልጆች ጋር የተያያዙ ቪዲዮዎችን ከፍላይአውት ሜኑ በኩል \"لاحاقاً ይመልከቱ\" ውስጥ ለማስቀመጥ ያስችላል በሾርቶች ፍላይአውት ውስጥ \"لاحاقاً ይመልከቱ\" ውስጥ ያስቀምጡ የሚለውን ቁልፍ ያሳዩ ሾርቶችን ከፍላይአውት ሜኑ በኩል \"لاحاقاً ይመልከቱ\" ውስጥ ለማስቀመጥ ያስችላል + \"በኋላ እይ\"ን ወደ የሙዚቃ ዝርዝር አድጋጋ + በኋላ እይ ወደ Morphe ዝርዝር አክል ተረኛውን በስብስቦች መካከል አቆይ መተግበሪያው እንደገና ሲጀመር ተረኛው ይቀመጣል @@ -1111,20 +1113,21 @@ OpenRouter.aiን ለመክፈት እዚህ ይጫኑ" የአውቶሞቲቭ "ለውጦቹ የሚከተሉትን ያካትታሉ፦ -የታብሌት አቀማመጥ -• የማህበረሰብ ልጥፎች ተደብቀዋል -• በፊዶች (feeds) ውስጥ የመጫወት ቅንብር አይдоступም -• በShorts ውስጥ የሪሚክስ (Remix) እና የድምፅ (Sound) ቁልፎች አይገኙም -• 'ተጨማሪ ቪዲዮዎች' የሚለው ንብርብር ላይገኝም ይችላል -• ያልተጨመቀው የድሮው የቪዲዮ ተግባር አሞሌ ይታያል +የታብሌት ሌይአውት +• የኮሙኒቲ ፖስቶች ተደብቀዋል +• በፊዶች ውስጥ የመጫወት ቅንብር አይдоступም +• በShorts ውስጥ የሪሚክስ ቁልፍ እና የድምጽ ቁልፍ አይገኙም +• 'ተጨማሪ ቪዲዮዎች' የሚለው ኦቨርሌ ላይገኝ ይችላል +• ያልተጨመቀው የድሮ ቪዲዮ አክሽን ባር ይታያል +• የፍላይአውት ሜኑ ንጥሎች በትክክል ላይሰሩ ይችላሉ -የመኪና አቀማመጥ -• ፊድ (feed) በርዕሰ ጉዳዮች እና በቻናሎች ተደራጅቷል -• በፊዶች (feeds) ውስጥ የመጫወት ቅንብር አይдоступም -• በShorts ውስጥ የሪሚክስ (Remix) እና የድምፅ (Sound) ቁልፎች አይገኙም -• 'ተጨማሪ ቪዲዮዎች' የሚለው ንብርብር ላይገኝም ይችላል -• Shorts በመደበኛው ተጫዋች ውስጥ ይከፈታሉ -• ያልተጨመቀው የድሮው የቪዲዮ ተግባር አሞሌ ይታያል" +የአውቶሞቲቭ ሌይአውት +• ፊድ በርዕሰ ጉዳዮች እና በቻናሎች የተደራጀ ነው +• በፊዶች ውስጥ የመጫወት ቅንብር አይдоступም +• በShorts ውስጥ የሪሚክስ ቁልፍ እና የድምጽ ቁልፍ አይገኙም +• 'ተጨማሪ ቪዲዮዎች' የሚለው ኦቨርሌ ላይገኝ ይችላል +• Shorts በመደበኛው ፕሌየር ውስጥ ይከፈታሉ +• ያልተጨመቀው የድሮ ቪዲዮ አክሽን ባር ይታያል" በተጫዋቹ ውስጥ የታብሌት አቀማመጥን አግብር "የድሮውን የተጫዋች አቀማመጥ ይመልሳል diff --git a/patches/src/main/resources/addresources/values-ar-rSA/youtube/strings.xml b/patches/src/main/resources/addresources/values-ar-rSA/youtube/strings.xml index 85cc8eecab..674e330f6b 100644 --- a/patches/src/main/resources/addresources/values-ar-rSA/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ar-rSA/youtube/strings.xml @@ -500,8 +500,8 @@ Second \"item\" text" الفيديو الصوت كل الخيارات - إظهار التنزيل في قائمة Shorts - يضيف عنصر قائمة منبثقة لمُحمّل خارجي إلى مشغل Shorts + عرض التنزيل في قائمة Shorts + يضيف عنصر بالقائمة المنبثقة لأداة التنزيل الخارجية إلى مشغل Shorts تنزيل الترجمة الصوتية @@ -617,6 +617,8 @@ Mistral Nemo هو الخيار الأرخص، بينما توفر Gemma 4 أعل يتيح حفظ فيديوهات Shorts في قائمة \"المشاهدة لاحقًا\" من القائمة المنبثقة عرض زر \"الحفظ للمشاهدة لاحقًا\" في قائمة Shorts المنبثقة يتيح حفظ فيديوهات Shorts في قائمة \"المشاهدة لاحقًا\" من القائمة المنبثقة + إعادة توجيه المشاهدة لاحقًا إلى قائمة تشغيل + المشاهدة لاحقًا إضافة الى قائمة المحتوى التالي الخاصة بـ Morphe احتفظ بقائمة المحتوى التالي بين الجلسات تبقى قائمة المحتوى التالي محفوظة بعد إعادة تشغيل التطبيق @@ -1113,22 +1115,23 @@ Mistral Nemo هو الخيار الأرخص، بينما توفر Gemma 4 أعل الجوّال الجهاز اللوحي Automotive - "تتضمن التغييرات: + "تتضمن التغييرات ما يلي: -تصميم الجهاز اللوحي -• يتم إخفاء مشاركات المجتمع -• إعداد التشغيل في الخلاصات غير متوفر -• زر ريمكس وزر الصوت غير متوفرين في Shorts -• قد لا تتوفر واجهة 'المزيد من الفيديوهات' -• شريط إجراءات الفيديو غير مطوي +تخطيط الجهاز اللوحي +• يتم إخفاء منشورات المنتدى +• إعداد \"التشغيل في الخلاصات\" غير متوفر +• زرا \"ريمكس\" و\"الصوت\" غير متوفرين في Shorts +• قد لا تتوفر تراكبات \"مزيد من الفيديوهات\" +• يظهر شريط إجراءات الفيديو القديم غير المطوي +• قد لا تعمل عناصر القائمة المنبثقة بشكل صحيح -تصميم Automotive -• الخلاصة مُنظمة حسب المواضيع والقنوات -• إعداد التشغيل في الخلاصات غير متوفر -• زر ريمكس وزر الصوت غير متوفرين في Shorts -• قد لا تتوفر واجهة 'المزيد من الفيديوهات' -• فيديوهات Shorts تفتح في المشغل العادي -• شريط إجراءات الفيديو غير مطوي" +تخطيط السيارات +• يتم تنظيم الخلاصة حسب المواضيع والقنوات +• إعداد \"التشغيل في الخلاصات\" غير متوفر +• زرا \"ريمكس\" و\"الصوت\" غير متوفرين في Shorts +• قد لا تتوفر تراكبات \"مزيد من الفيديوهات\" +• تفتح فيديوهات Shorts في المشغل العادي +• يظهر شريط إجراءات الفيديو القديم غير المطوي" تمكين تخطيط الجهاز اللوحي في المشغل "استعادة تخطيط المشغل القديم @@ -1638,7 +1641,7 @@ AVC (H.264) حلقة غير متاحة تعليقات Niconico - عرض الدردشة المباشرة وإعادة تشغيل الدردشة كتعليقات تمر عبر الفيديو + عرض المحادثة المباشرة وإعادة تشغيل المحادثة كتعليقات تمر عبر الفيديو يعرض زر متراكب في مشغل الفيديو. انقر لإظهار أو إخفاء التعليقات، واضغط مع الاستمرار للوصول إلى الإعدادات السريعة حجم خط التعليقات حجم نص التعليقات (12-40 sp) diff --git a/patches/src/main/resources/addresources/values-az-rAZ/youtube/strings.xml b/patches/src/main/resources/addresources/values-az-rAZ/youtube/strings.xml index 30a6583ac6..0826cfdb36 100644 --- a/patches/src/main/resources/addresources/values-az-rAZ/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-az-rAZ/youtube/strings.xml @@ -500,6 +500,8 @@ Məhdudiyyətlər: Video Səs Bütün seçimlər + Shorts menyusunda yükləməni göstər + Shorts oynadıcıya xarici yükləyici açılan menyusu elementi əlavə edir Yüklə Səs üstələyici tərcümə @@ -611,6 +613,8 @@ Məlumat: Canlı yayımlarda işləməyə bilər" Uşaqlarla əlaqəli videoları açılan menyudan \"Sonra izləyin\" bölməsində saxlamağa imkan verir Shorts açılan menyusunda \"Daha sonra İzləmək üçün Saxla\" düyməsini göstər Shorts-ları açılan menyudan \"Sonra izləyin\" bölməsində saxlamağa imkan verir + Daha sonra izləni pleylistə yönləndir + Sonra izlə Morphe növbəsinə əlavə et ​Sessiyalar arasında növbəni qoru Tətbiq yenidən başladıldıqdan sonra növbə qorunur @@ -1107,22 +1111,6 @@ Ayarlar→ Oxunuş (və ya Avtomatik oxunuş) → Növbəti videonu avtomatik ox Telefon Planşet Avtomobil - "Dəyişikliklər ehtiva edir: - -Planşet tərtibatı -• İcma postları gizlədilir -• Axınlarda oynatma seçimi mövcud deyil -• Shorts-da Remiks düyməsi və Səs düyməsi mövcud deyil -• \"Daha çox video\" örtüyü mövcud olmaya bilər -• Yığılmamış köhnə video fəaliyyət cizgisi göstərilir - -Avtomobil tərtibatı -• Axın mövzular və kanallara görə qruplaşdırılır -• Axınlarda oynatma seçimi mövcud deyil -• Shorts-da Remiks düyməsi və Səs düyməsi mövcud deyil -• \"Daha çox video\" örtüyü mövcud olmaya bilər -• Shorts müntəzəm oynadıcıda açılır -• Yığılmamış köhnə video fəaliyyət cizgisi göstərilir" Oynadıcıda planşet tərtibatın aktivləşdir "Köhnə oynadıcı tərtibatını bərpa edir @@ -1631,4 +1619,7 @@ Məhdudiyyət: Köhnə üslub üçün \"Daha çox video\" örtüyünə qayıdır Başlıqsız epizod Əlçatmaz epizod + Niconico şərhləri + Canlı söhbəti və təkrar oxudulan söhbəti video üzərində axan şərhlər kimi göstər + Video oynadıcıda örtük düyməsini göstərir. Şərhləri göstərmək və ya gizlətmək üçün toxun, sürətli tənzimləmələr üçün basıb saxla diff --git a/patches/src/main/resources/addresources/values-bg-rBG/youtube/strings.xml b/patches/src/main/resources/addresources/values-bg-rBG/youtube/strings.xml index 3b7b633843..2a1bb68af9 100644 --- a/patches/src/main/resources/addresources/values-bg-rBG/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-bg-rBG/youtube/strings.xml @@ -613,6 +613,8 @@ Second \"item\" text" Позволява запазването на видеоклипове, свързани с деца, в „Гледай по-късно“ от изскачащото меню. Показване на бутона „Запазване в ‘Гледай по-късно’“ в изскачащото меню на Shorts Позволява запазване на Shorts в „Гледай по-късно“ от изскачащото меню + Пренасочване на \"Гледай по-късно\" към плейлист + Гледай по-късно Добавяне към опашката на Morphe Запазване на опашката между сесиите Опашката се запазва след рестартиране на приложението @@ -1111,20 +1113,21 @@ Second \"item\" text" Автомобилна "Промените включват: -Оформление за таблет -• Публикациите в общността са скрити -• Настройката за възпроизвеждане в feed не е налична +Таблет разположение +• Публикациите на общността са скрити +• Настройката за възпроизвеждане във фидовете не е налична • Бутонът Remix и бутонът Sound не са налични в Shorts -• Наслагването „Още видеа“ може да не е налично -• Показва се некомпактната стара лента за действия на видеото +• Оверлеят \"Още видеоклипове\" може да не е наличен +• Показва се старата лента за действия на видеоклипове, която не е сгъната +• Елементите на изскачащото меню може да не работят правилно -Оформление за автомобил -• Feed е организиран по теми и канали -• Настройката за възпроизвеждане в feed не е налична +Автомобилен интерфейс +• Фидът е организиран по теми и канали +• Настройката за възпроизвеждане във фидовете не е налична • Бутонът Remix и бутонът Sound не са налични в Shorts -• Наслагването „Още видеа“ може да не е налично -• Shorts се отварят в стандартния плейър -• Показва се некомпактната стара лента за действия на видеото" +• Оверлеят \"Още видеоклипове\" може да не е наличен +• Shorts се отварят в обичайния плейър +• Показва се старата лента за действия на видеоклипове, която не е сгъната" Активиране на оформление за таблет в плейъра "Възстановява стария вид на плейъра diff --git a/patches/src/main/resources/addresources/values-bn-rBD/youtube/strings.xml b/patches/src/main/resources/addresources/values-bn-rBD/youtube/strings.xml index 66a29cc0e2..f81e22446f 100644 --- a/patches/src/main/resources/addresources/values-bn-rBD/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-bn-rBD/youtube/strings.xml @@ -613,6 +613,8 @@ OpenRouter.ai ওপেন করতে এখানে ট্যাপ কর ফ্লাইআউট মেনু থেকে \'পরে দেখুন\'-এ শিশু-সম্পর্কিত ভিডিও সংরক্ষণ করার অনুমতি দেয় শর্টস ফ্লাইআউটে \'পরে দেখুন\'-এ সংরক্ষণ করার বোতাম দেখান ফ্লাইআউট মেনু থেকে \'পরে দেখুন\'-এ শর্টস সংরক্ষণ করার অনুমতি দেয় + \'Watch later\'-কে প্লেলিস্টে রিডাইরেক্ট করুন + পরে দেখুন Morphe ভিডিও তালিকায় যুক্ত করুন সেশনগুলির মধ্যে সারি রাখুন অ্যাপ পুনরায় চালু করার পরেও সারিটি রাখা হবে @@ -1109,7 +1111,23 @@ OpenRouter.ai ওপেন করতে এখানে ট্যাপ কর ফোন ট্যাবলেট অটোমোটিভ - "'On-the-go' মেনু লুকান" + "পরিবর্তনগুলোর মধ্যে রয়েছে: + +ট্যাবলেট লেআউট +• কমিউনিটি পোস্টগুলো লুকানো থাকে +• ফিড-এ প্লেব্যাক সেটিংটি উপলব্ধ নেই +• শর্টস-এ রিমিক্স বোতাম এবং সাউন্ড বোতাম উপলব্ধ নেই +• 'আরও ভিডিও' ওভারলে উপলব্ধ নাও হতে পারে +• নন-ক্ল্যাপসড পুরনো ভিডিও অ্যাকশন বার দেখানো হয় +• ফ্লাইআউট মেনু আইটেমগুলো সঠিকভাবে কাজ নাও করতে পারে + +অটোমোটিভ লেআউট +• ফিডটি টপিক এবং চ্যানেল অনুযায়ী সাজানো থাকে +• ফিড-এ প্লেব্যাক সেটিংটি উপলব্ধ নেই +• শর্টস-এ রিমিক্স বোতাম এবং সাউন্ড বোতাম উপলব্ধ নেই +• 'আরও ভিডিও' ওভারলে উপলব্ধ নাও হতে পারে +• শর্টসগুলো সাধারণ প্লেয়ারে খোলে +• নন-ক্ল্যাপসড পুরনো ভিডিও অ্যাকশন বার দেখানো হয়" প্লেয়ারে ট্যাবলেট লেআউট চালু করুন "পুরানো প্লেয়ার লেআউট ফিরিয়ে আনুন diff --git a/patches/src/main/resources/addresources/values-cs-rCZ/youtube/strings.xml b/patches/src/main/resources/addresources/values-cs-rCZ/youtube/strings.xml index 3052bdf1b4..3c7716916f 100644 --- a/patches/src/main/resources/addresources/values-cs-rCZ/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-cs-rCZ/youtube/strings.xml @@ -613,6 +613,8 @@ Upozornění: Možná nebude fungovat u živých přenosů" Umožňuje ukládat videa související s dětmi do seznamu „Přehrát později“ z rozbalovací nabídky Zobrazit tlačítko Uložit k přehrání později v nabídce Shorts Umožňuje ukládat Shorts do seznamu „Přehrát později“ z rozbalovací nabídky + Přesměrovat \"Sledovat později\" do seznamu přehrávání + Sledovat později Přidat do Morphe fronty Udržet frontu mezi relacemi Fronta je zachována i po restartu aplikace @@ -1109,20 +1111,21 @@ Nastavení → Přehrávání (nebo Automatické přehrávání) → Automaticky Automobilový "Změny zahrnují: -Rozhraní pro tablety -• Příspěvky komunity jsou skryté -• Nastavení „Přehrávání ve feedech“ není k dispozici -• U videí Shorts nejsou dostupná tlačítka „Remixovat“ a „Zvuk“ -• Překryvná vrstva „Další videa“ nemusí být k dispozici -• Zobrazuje se nesbalený starý panel akcí videa +Rozvržení pro tablet +• Příspěvky komunity jsou skryty +• Nastavení automatického přehrávání v kanálech není k dispozici +• Tlačítko Remix a tlačítko Zvuk nejsou v Shorts k dispozici +• Překryv „Více videí“ nemusí být k dispozici +• Zobrazuje se starý nerozbalený panel akcí videa +• Položky vysouvacího menu nemusí fungovat správně -Rozhraní pro automobily -• Feed je uspořádán podle témat a kanálů -• Nastavení „Přehrávání ve feedech“ není k dispozici -• U videí Shorts nejsou dostupná tlačítka „Remixovat“ a „Zvuk“ -• Překryvná vrstva „Další videa“ nemusí být k dispozici -• Videa Shorts se otevírají v běžném přehrávači -• Zobrazuje se nesbalený starý panel akcí videa" +Automobilní rozvržení +• Kanál je organizován podle témat a kanálů +• Nastavení automatického přehrávání v kanálech není k dispozici +• Tlačítko Remix a tlačítko Zvuk nejsou v Shorts k dispozici +• Překryv „Více videí“ nemusí být k dispozici +• Shorts se otevírají v běžném přehrávači +• Zobrazuje se starý nerozbalený panel akcí videa" Povolit rozložení tabletu v přehrávači "Obnoví staré rozložení přehrávače diff --git a/patches/src/main/resources/addresources/values-da-rDK/youtube/strings.xml b/patches/src/main/resources/addresources/values-da-rDK/youtube/strings.xml index a011b8537c..58bddea58f 100644 --- a/patches/src/main/resources/addresources/values-da-rDK/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-da-rDK/youtube/strings.xml @@ -611,6 +611,8 @@ Info: Virker muligvis ikke på live streams" Giver mulighed for at gemme børnerelaterede videoer i \'Se senere\' fra flyoutmenuen Vis knappen Gem til Se senere i Shorts-flyoutmenuen Giver mulighed for at gemme Shorts i \'Se senere\' fra flyoutmenuen + Omdiriger Se senere til afspilningsliste + Se senere Tilføj til Morphe-kø Behold køen mellem sessioner Køen bevares efter genstart af appen @@ -1105,22 +1107,23 @@ Indstillinger → Afspilning (eller Automatisk afspilning) → Afspil næste vid Telefon Tablet Bil - "Ændringer inkluderer: + "Ændringerne inkluderer: -Tablet layout -• Fællesskabsindlæg er skjult -• Afspilning i feed-indstillingen er ikke tilgængelig +Tablet-layout +• Indlæg fra fællesskabet er skjulte +• Indstillingen for afspilning i feeds er ikke tilgængelig • Remix-knappen og Lyd-knappen er ikke tilgængelige i Shorts -• 'Flere videoer'-overlayet er måske ikke tilgængeligt -• Den gamle video-handlingslinje vises, og den er ikke sammenklappet +• Overlayet \"Flere videoer\" er muligvis ikke tilgængeligt +• Den gamle videoværktøjslinje, der ikke er minimeret, vises +• Elementer i flyout-menuen fungerer muligvis ikke korrekt -Bil layout -• Feed er organiseret efter emner og kanaler -• Afspilning i feed-indstillingen er ikke tilgængelig +Automotive-layout +• Feedet er organiseret efter emner og kanaler +• Indstillingen for afspilning i feeds er ikke tilgængelig • Remix-knappen og Lyd-knappen er ikke tilgængelige i Shorts -• 'Flere videoer'-overlayet er måske ikke tilgængeligt -• Shorts åbnes i den almindelige afspiller -• Den gamle video-handlingslinje vises, og den er ikke sammenklappet" +• Overlayet \"Flere videoer\" er muligvis ikke tilgængeligt +• Shorts åbnes i den normale afspiller +• Den gamle videoværktøjslinje, der ikke er minimeret, vises" Aktivér tabletlayout i afspilleren "Gendanner det gamle layout for afspilleren diff --git a/patches/src/main/resources/addresources/values-de-rDE/youtube/strings.xml b/patches/src/main/resources/addresources/values-de-rDE/youtube/strings.xml index 61c3724341..31d2dfd6c9 100644 --- a/patches/src/main/resources/addresources/values-de-rDE/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-de-rDE/youtube/strings.xml @@ -614,6 +614,8 @@ Info: Funktioniert möglicherweise nicht bei Livestreams" Ermöglicht das Speichern von kinderbezogenen Videos in \'Später ansehen\' über das Flyout-Menü Zeige die Schaltfläche \'Speichern in Später ansehen\' im Shorts-Flyout Ermöglicht das Speichern von Shorts in \'Später ansehen\' über das Flyout-Menü + \"Später ansehen\" an Playlist weiterleiten + Später ansehen Zur Morphe-Warteschlange hinzufügen Warteschlange zwischen Sitzungen beibehalten Warteschlange wird nach Neustart der App beibehalten @@ -1110,22 +1112,22 @@ Einstellungen → Wiedergabe (oder Automatische Wiedergabe) → Nächstes Video Telefon Tablets Automobil - "Die Änderungen umfassen: + "Änderungen umfassen: Tablet-Layout -• Community-Beiträge sind ausgeblendet -• Die Einstellung 'Wiedergabe in Feeds' ist nicht verfügbar -• Remix-Button und Sound-Button sind in Shorts nicht verfügbar -• 'Weitere Videos'-Overlay ist möglicherweise nicht verfügbar -• Die alte, nicht eingeklappte Video-Aktionsleiste wird angezeigt +• Community-Beiträge werden ausgeblendet +• Wiedergabe in Feeds-Einstellung ist nicht verfügbar +• Remix-Schaltfläche und Sound-Schaltfläche sind in Shorts nicht verfügbar +• 'Mehr Videos'-Overlay möglicherweise nicht verfügbar +• Nicht zusammengeklappte alte Video-Aktionsleiste wird angezeigt Automotive-Layout -• Der Feed ist nach Themen und Kanälen organisiert -• Die Einstellung 'Wiedergabe in Feeds' ist nicht verfügbar -• Remix-Button und Sound-Button sind in Shorts nicht verfügbar -• 'Weitere Videos'-Overlay ist möglicherweise nicht verfügbar -• Shorts werden im normalen Player geöffnet -• Die alte, nicht eingeklappte Video-Aktionsleiste wird angezeigt" +• Feed ist nach Themen und Kanälen organisiert +• Wiedergabe in Feeds-Einstellung ist nicht verfügbar +• Remix-Schaltfläche und Sound-Schaltfläche sind in Shorts nicht verfügbar +• 'Mehr Videos'-Overlay möglicherweise nicht verfügbar +• Shorts werden im regulären Player geöffnet +• Nicht zusammengeklappte alte Video-Aktionsleiste wird angezeigt" Tablet-Layout im Player aktivieren "Stellt das alte Player-Layout wieder her diff --git a/patches/src/main/resources/addresources/values-el-rGR/youtube/strings.xml b/patches/src/main/resources/addresources/values-el-rGR/youtube/strings.xml index 1718cd61d0..3aaa2b83d1 100644 --- a/patches/src/main/resources/addresources/values-el-rGR/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-el-rGR/youtube/strings.xml @@ -613,6 +613,8 @@ Second \"item\" text" Επιτρέπει την αποθήκευση βίντεο που αφορούν παιδιά στη λίστα «Παρακολούθηση αργότερα» από το αναδυόμενο μενού Εμφάνιση του κουμπιού «Αποθήκευση για παρακολούθηση αργότερα» στο αναδυόμενο μενού των Shorts Επιτρέπει την αποθήκευση των Shorts στη λίστα «Παρακολούθηση αργότερα» από το αναδυόμενο μενού + Ανακατεύθυνση της λίστας \"Δείτε αργότερα\" σε λίστα αναπαραγωγής + Παρακολούθηση αργότερα Προσθήκη στην ουρά Morphe Διατήρηση ουράς μεταξύ των περιόδων σύνδεσης Η ουρά διατηρείται μετά την επανεκκίνηση της εφαρμογής @@ -1109,7 +1111,23 @@ Second \"item\" text" Τηλέφωνο Ταμπλετ Αυτοκίνητο - "Οι αλλαγές περιλαμβάνουν:\n\nΔιάταξη tablet\n• Οι αναρτήσεις κοινότητας είναι κρυφές\n• Η ρύθμιση «Αναπαραγωγή στις ροές» δεν είναι διαθέσιμη\n• Το κουμπί Remix και το κουμπί ήχου δεν είναι διαθέσιμα στα Shorts\n• Η επικάλυψη «Περισσότερα βίντεο» ενδέχεται να μην είναι διαθέσιμη\n• Εμφανίζεται η μη συμπτυγμένη παλιά γραμμή ενεργειών βίντεο\n\nΔιάταξη αυτοκινήτου\n• Η ροή είναι οργανωμένη ανά θέμα και κανάλι\n• Η ρύθμιση «Αναπαραγωγή στις ροές» δεν είναι διαθέσιμη\n• Το κουμπί Remix και το κουμπί ήχου δεν είναι διαθέσιμα στα Shorts\n• Η επικάλυψη «Περισσότερα βίντεο» ενδέχεται να μην είναι διαθέσιμη\n• Τα Shorts ανοίγουν στο κανονικό πρόγραμμα αναπαραγωγής\n• Εμφανίζεται η μη συμπτυγμένη παλιά γραμμή ενεργειών βίντεο" + "Οι αλλαγές περιλαμβάνουν: + +Διάταξη tablet +• Τα δημοσιεύματα της κοινότητας κρύβονται +• Η ρύθμιση αναπαραγωγής στα feeds δεν είναι διαθέσιμη +• Το κουμπί Remix και το κουμπί Ήχος δεν είναι διαθέσιμα στα Shorts +• Το υπέρθεμα «Περισσότερα βίντεο» μπορεί να μην είναι διαθέσιμο +• Εμφανίζεται η παλιά μπάρα εργαλείων μη συμπιεσμένων βίντεο +• Τα στοιχεία του αναδιπλούμενου μενού μπορεί να μην λειτουργούν σωστά + +Διάταξη automotive +• Το feed οργανώνεται ανά θέμα και κανάλι +• Η ρύθμιση αναπαραγωγής στα feeds δεν είναι διαθέσιμη +• Το κουμπί Remix και το κουμπί Ήχος δεν είναι διαθέσιμα στα Shorts +• Το υπέρθεμα «Περισσότερα βίντεο» μπορεί να μην είναι διαθέσιμο +• Τα Shorts ανοίγουν στον συνηθισμένο player +• Εμφανίζεται η παλιά μπάρα εργαλείων μη συμπιεσμένων βίντεο" Ενεργοποίηση διάταξης για tablet στον παίκτη "Επαναφέρει την παλιά διάταξη του παίκτη diff --git a/patches/src/main/resources/addresources/values-es-rES/youtube/strings.xml b/patches/src/main/resources/addresources/values-es-rES/youtube/strings.xml index 60256fe35a..395b5da778 100644 --- a/patches/src/main/resources/addresources/values-es-rES/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-es-rES/youtube/strings.xml @@ -613,6 +613,8 @@ Información: Puede que no funcione en transmisiones en directo" Permite guardar vídeos infantiles en \'Ver más tarde\' desde el menú flotante Mostrar botón \'Guardar para ver más tarde\' en el menú flotante de Shorts Permite guardar Shorts en \'Ver más tarde\' desde el menú flotante + Redirigir \"Ver más tarde\" a una lista de reproducción + Ver más tarde Añadir a la cola de Morphe Mantener la cola entre sesiones La cola se mantiene después de reiniciar la aplicación @@ -1115,15 +1117,15 @@ Diseño para tablet • Las publicaciones de la comunidad están ocultas • El ajuste de reproducción en feeds no está disponible • El botón Remix y el botón Sonido no están disponibles en Shorts -• La superposición 'Más vídeos' puede no estar disponible +• La superposición «Más vídeos» puede no estar disponible • Se muestra la antigua barra de acciones de vídeo sin colapsar Diseño para automóviles • El feed está organizado por temas y canales • El ajuste de reproducción en feeds no está disponible • El botón Remix y el botón Sonido no están disponibles en Shorts -• La superposición 'Más vídeos' puede no estar disponible -• Shorts se abre en el reproductor normal +• La superposición «Más vídeos» puede no estar disponible +• Los Shorts se abren en el reproductor normal • Se muestra la antigua barra de acciones de vídeo sin colapsar" Habilitar diseño para tablet en el reproductor diff --git a/patches/src/main/resources/addresources/values-fa-rIR/youtube/strings.xml b/patches/src/main/resources/addresources/values-fa-rIR/youtube/strings.xml index 6fe9a5bdef..41f0a80e38 100644 --- a/patches/src/main/resources/addresources/values-fa-rIR/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-fa-rIR/youtube/strings.xml @@ -613,6 +613,8 @@ Second \"item\" text" امکان ذخیره ویدیوهای مرتبط با کودکان در «تماشای بعداً» را از منوی بازشو فراهم می‌کند نمایش دکمهٔ ذخیره برای بعداً تماشا کردن در منوی بازشو Shorts ها امکان ذخیره Shorts در «تماشای بعداً» را از منوی بازشو فراهم می‌کند + هدایت \"بعداً می‌بینم\" به لیست پخش + بعداً تماشا می‌کنم افزودن به صف Morphe نگه‌داشتن صف بین جلسه‌ها صف پس از راه‌اندازی دوبارهٔ برنامه حفظ می‌شود @@ -1113,14 +1115,14 @@ Second \"item\" text" طرح‌بندی تبلت • پست‌های انجمن پنهان می‌شوند -• تنظیم پخش در خوراک‌ها در دسترس نیست +• تنظیم پخش در فید در دسترس نیست • دکمه‌های ریمیکس و صدا در Shorts در دسترس نیستند • ممکن است روی‌هم‌افتادگی «ویدئوهای بیشتر» در دسترس نباشد • نوار عملیات قدیمی ویدئو به‌صورت بازشده نمایش داده می‌شود طرح‌بندی خودرویی -• خوراک بر اساس موضوع‌ها و کانال‌ها سازمان‌دهی می‌شود -• تنظیم پخش در خوراک‌ها در دسترس نیست +• فید بر اساس موضوع‌ها و کانال‌ها سازمان‌دهی می‌شود +• تنظیم پخش در فید در دسترس نیست • دکمه‌های ریمیکس و صدا در Shorts در دسترس نیستند • ممکن است روی‌هم‌افتادگی «ویدئوهای بیشتر» در دسترس نباشد • Shorts در پخش‌کنندهٔ معمولی باز می‌شود diff --git a/patches/src/main/resources/addresources/values-fi-rFI/youtube/strings.xml b/patches/src/main/resources/addresources/values-fi-rFI/youtube/strings.xml index 775ed782aa..bf337bf1f4 100644 --- a/patches/src/main/resources/addresources/values-fi-rFI/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-fi-rFI/youtube/strings.xml @@ -613,6 +613,8 @@ Tieto: Ei välttämättä toimi suoratoistoissa" Sallii lapsiin liittyvien videoiden tallentamisen \'Katso myöhemmin\' -luetteloon ponnahdusvalikosta Näytä Tallenna myöhemmin -painike Shortsit-ponnahdusvalikossa Sallii Shortsien tallentamisen \'Katso myöhemmin\' -luetteloon ponnahdusvalikosta + Ohjaa Katso myöhemmin -lista soittolistalle + Katso myöhemmin Lisää Morphe-jonoon Pidä jono käytössä istuntojen välillä Jono säilyy sovelluksen uudelleenkäynnistyksen jälkeen @@ -1111,20 +1113,21 @@ Asetukset → Toisto (tai Automaattinen toisto) → Toista seuraava video"Ajoneuvo "Muutokset sisältävät: -Tabletin asettelu -• Yhteisön julkaisut ovat piilotettuja +Tablettiasettelu +• Yhteisöpostaukset on piilotettu • Toisto syötteissä -asetus ei ole käytettävissä -• Remix-painike ja äänipainike eivät ole käytettävissä Shortseissa -• 'Lisää videoita' -peittokuva ei ehkä ole käytettävissä -• Vanha, tiivistämätön video-toimintapalkki on näkyvissä +• Remix-painike ja Ääni-painike eivät ole käytettävissä Shorts-videoilla +• \"Lisää videoita\" -peitekerros ei välttämättä ole käytettävissä +• Vanha videon toimintopalkki näkyy laajennettuna +• Avautuvan valikon kohteet eivät välttämättä toimi oikein Auton asettelu • Syöte on järjestetty aiheiden ja kanavien mukaan • Toisto syötteissä -asetus ei ole käytettävissä -• Remix-painike ja äänipainike eivät ole käytettävissä Shortseissa -• 'Lisää videoita' -peittokuva ei ehkä ole käytettävissä -• Shorts avautuvat tavallisessa soittimessa -• Vanha, tiivistämätön video-toimintapalkki on näkyvissä" +• Remix-painike ja Ääni-painike eivät ole käytettävissä Shorts-videoilla +• \"Lisää videoita\" -peitekerros ei välttämättä ole käytettävissä +• Shorts-videot avautuvat tavalliseen soittimeen +• Vanha videon toimintopalkki näkyy laajennettuna" Ota tablettiasettelun käyttöön soittimessa "Palauttaa vanhan soittimen asettelun diff --git a/patches/src/main/resources/addresources/values-fil-rPH/youtube/strings.xml b/patches/src/main/resources/addresources/values-fil-rPH/youtube/strings.xml index 8f4e4dff3e..df849fa2cd 100644 --- a/patches/src/main/resources/addresources/values-fil-rPH/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-fil-rPH/youtube/strings.xml @@ -613,6 +613,8 @@ Impormasyon: Maaaring hindi gumana sa mga live stream" Nagbibigay-daan sa pag-save ng mga video na nauugnay sa mga bata sa \'Panoorin Mamaya\' mula sa flyout menu Ipakita ang button na I-save sa Panoorin Mamaya sa flyout ng shorts Nagbibigay-daan sa pag-save ng mga shorts sa \'Panoorin Mamaya\' mula sa flyout menu + I-redirect ang \"Panoorin mamaya\" sa playlist + Panoorin mamaya Idagdag sa Morphe queue Panatilihin ang pila sa pagitan ng mga sesyon Ang pila ay nananatili kahit i-restart ang app @@ -1107,22 +1109,23 @@ Mga Setting → Playback (o Autoplay) → Autoplay ng susunod na video."Telepono Tablet Automotive - "Kasama sa mga pagbabago ang sumusunod: + "Kasama sa mga pagbabago ang: -Layout ng tablet -• Nakatago ang mga post sa komunidad -• Hindi available ang setting na Playback sa mga feed +Tablet layout +• Nakatago ang mga community post +• Hindi available ang setting para sa playback sa mga feed • Hindi available ang Remix button at Sound button sa Shorts • Maaaring hindi available ang overlay na 'Higit pang mga video' -• Ipinapakita ang hindi naka-collapse na lumang video action bar +• Ipinapakita ang lumang video action bar na hindi collapsed +• Maaaring hindi gumana nang tama ang mga item sa flyout menu -Layout ng automotive -• Nakaayos ang feed ayon sa mga paksa at mga channel -• Hindi available ang setting na Playback sa mga feed +Automotive layout +• Ang feed ay naka-organisa ayon sa mga paksa at channel +• Hindi available ang setting para sa playback sa mga feed • Hindi available ang Remix button at Sound button sa Shorts • Maaaring hindi available ang overlay na 'Higit pang mga video' -• Nagbubukas ang Shorts sa regular na player -• Ipinapakita ang hindi naka-collapse na lumang video action bar" +• Ang mga Shorts ay bumubukas sa regular na player +• Ipinapakita ang lumang video action bar na hindi collapsed" Paganahin ang layout ng tablet sa player "Ibinabalik ang lumang layout ng player diff --git a/patches/src/main/resources/addresources/values-fr-rFR/youtube/strings.xml b/patches/src/main/resources/addresources/values-fr-rFR/youtube/strings.xml index d8a6237638..95e400a4bf 100644 --- a/patches/src/main/resources/addresources/values-fr-rFR/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-fr-rFR/youtube/strings.xml @@ -501,7 +501,7 @@ Après avoir ouvert une fois une vidéo avec le bouton 'Posez une question', la Audio Toutes les options Afficher le téléchargement dans le menu Shorts - Ajoute un élément de menu déroulant de l\'extranet de téléchargement au lecteur Shorts + Ajoute un élément de téléchargement externe dans le menu déroulant du lecteur Shorts Télécharger Traduction en voix off @@ -613,6 +613,8 @@ Info : Peut ne pas fonctionner sur les diffusions en direct" Permet d\'enregistrer les vidéos pour enfants dans \'À regarder plus tard\' depuis le menu déroulant Afficher le bouton \'Enregistrer dans \"À regarder plus tard\"\' dans le menu déroulant shorts Permet d\'enregistrer les shorts dans \'À regarder plus tard\' depuis le menu déroulant + Rediriger \'À regarder plus tard\' vers une playlist + À regarder plus tard Ajouter à la file d\'attente Morphe Conserver la file d\'attente entre les sessions La file d\'attente est conservée après le redémarrage de l\'application @@ -1118,9 +1120,10 @@ Mise en page pour tablette • Le bouton 'Remixer' et le bouton 'Son' ne sont pas disponibles dans les Shorts • L'overlay 'Plus de vidéos' peut ne pas être disponible • L'ancienne barre d'actions vidéo non réduite est affichée +• Les éléments du menu déroulant peuvent ne pas fonctionner correctement Mise en page pour automobile -• Le flux est organisé par thèmes et chaînes +• Le flux est organisé par topics et chaînes • Le paramètre de lecture dans les flux n'est pas disponible • Le bouton 'Remixer' et le bouton 'Son' ne sont pas disponibles dans les Shorts • L'overlay 'Plus de vidéos' peut ne pas être disponible @@ -1635,15 +1638,15 @@ L'enregistrement automatique est facultatif. Il s'arrête en navigation privée Épisode indisponible Commentaires Niconico - Affiche le chat en direct et le replay du chat sous forme de commentaires qui défilent sur la vidéo - Affiche un bouton overlay dans le lecteur vidéo. Appuyez pour afficher ou masquer les commentaires, appuyez longuement pour accéder aux paramètres rapides + Afficher le chat en direct et le replay du chat sous forme de commentaires qui défilent sur la vidéo + Affiche un bouton en superposition dans le lecteur vidéo. Appuyez pour afficher ou masquer les commentaires, ou maintenez-le enfoncé pour accéder rapidement aux paramètres. Taille de la police des commentaires Taille du texte des commentaires (12-40 sp) Opacité des commentaires - Opacité du texte et des arrière-plans des commentaires + Opacité du texte et de l\'arrière-plan des commentaires Durée du trajet - Nombre de secondes que mettent les commentaires pour traverser l\'écran. Des valeurs plus basses augmentent la vitesse - Longueur maximale du commentaire - Masque les commentaires comportant plus de caractères que cette valeur. Chaque emoji compte pour un caractère + Combien de secondes les commentaires mettent à traverser l\'écran. Des valeurs plus faibles les font défiler plus rapidement + Longueur maximale des commentaires + Masque les commentaires contenant plus de caractères que cette limite. Chaque emoji compte pour un caractère Aucune limite diff --git a/patches/src/main/resources/addresources/values-ga-rIE/youtube/strings.xml b/patches/src/main/resources/addresources/values-ga-rIE/youtube/strings.xml index 9204528d7f..a9a4147519 100644 --- a/patches/src/main/resources/addresources/values-ga-rIE/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ga-rIE/youtube/strings.xml @@ -613,6 +613,8 @@ Eolas: Seans nach n-oibreoidh sé ar shruthanna beo" Ceadaítear físeáin a bhaineann le páistí a shábháil sa liosta \'Féach níos déanaí\' ón roghchlár aníos. Taispeáin an cnaipe \'Sábháil le breathnú air níos déanaí\' sa phainéal aníos do na físeáin ghearra. Ceadaíonn sé físeáin ghearra a shábháil sa liosta \'Féach níos déanaí\' ón roghchlár aníos. + Athdhionaigh \"Féach níos déanaí\" chuig liosta pleanála + Féach níos déanaí Cuir leis an scuaine Morphe Coinnigh scuaine idir seisiúin Coinnítear an scuaine tar éis an aip a atosú diff --git a/patches/src/main/resources/addresources/values-hi-rIN/youtube/strings.xml b/patches/src/main/resources/addresources/values-hi-rIN/youtube/strings.xml index d5aacf2aa5..6913205120 100644 --- a/patches/src/main/resources/addresources/values-hi-rIN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-hi-rIN/youtube/strings.xml @@ -613,6 +613,8 @@ OpenRouter.ai खोलने के लिए यहां टैप करे फ्लाईआउट मेनू से बच्चों से जुड़े वीडियो को \'बाद में देखें\' में सेव करने की अनुमति देता है शॉर्ट्स फ्लाईआउट में बाद में देखने के लिए सेव करें बटन दिखाएँ फ्लाईआउट मेनू से शॉर्ट्स को \'बाद में देखें\' में सेव करने की अनुमति देता है + \'Watch later\' को प्लेलिस्ट पर रीडायरेक्ट करें + बाद में देखें Morphe कतार में जोड़ें सत्रों के बीच कतार बनाए रखें ऐप को पुनरारंभ करने के बाद कतार बनी रहती है @@ -1109,22 +1111,23 @@ OpenRouter.ai खोलने के लिए यहां टैप करे फ़ोन टैबलेट ऑटोमोटिव - "परिवर्तनों में शामिल हैं: + "बदलाव इस प्रकार हैं: टैबलेट लेआउट • सामुदायिक पोस्ट छिपे हुए हैं -• फ़ीड सेटिंग में प्लेबैक उपलब्ध नहीं है +• फ़ीड में प्लेबैक सेटिंग उपलब्ध नहीं है • शॉर्ट्स में रीमिक्स बटन और साउंड बटन उपलब्ध नहीं हैं • 'अधिक वीडियो' ओवरले उपलब्ध नहीं हो सकता है -• गैर-संक्षिप्त पुराना वीडियो एक्शन बार दिखाया गया है +• गैर-संक्षिप्त पुराना वीडियो एक्शन बार दिखाया जाता है +• फ्लाउआउट मेनू आइटम सही ढंग से काम न कर सकते हैं ऑटोमोटिव लेआउट -• फ़ीड विषयों और चैनलों द्वारा व्यवस्थित है -• फ़ीड सेटिंग में प्लेबैक उपलब्ध नहीं है +• फ़ीड विषयों और चैनलों द्वारा संगठित किया गया है +• फ़ीड में प्लेबैक सेटिंग उपलब्ध नहीं है • शॉर्ट्स में रीमिक्स बटन और साउंड बटन उपलब्ध नहीं हैं • 'अधिक वीडियो' ओवरले उपलब्ध नहीं हो सकता है • शॉर्ट्स नियमित प्लेयर में खुलते हैं -• गैर-संक्षिप्त पुराना वीडियो एक्शन बार दिखाया गया है" +• गैर-संक्षिप्त पुराना वीडियो एक्शन बार दिखाया जाता है" प्लेयर में टैबलेट लेआउट सक्षम करें "पुराने प्लेयर लेआउट को पुनर्स्थापित करता है diff --git a/patches/src/main/resources/addresources/values-hr-rHR/youtube/strings.xml b/patches/src/main/resources/addresources/values-hr-rHR/youtube/strings.xml index ee36941e6a..6cff344a04 100644 --- a/patches/src/main/resources/addresources/values-hr-rHR/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-hr-rHR/youtube/strings.xml @@ -603,6 +603,8 @@ Info: Možda neće raditi na prijenosima uživo" Omogućuje spremanje videozapisa za djecu u \"Gledaj kasnije\" iz skočnog izbornika Prikaži tipku Spremi za kasnije gledanje u skočnom izborniku za Shorts Omogućuje spremanje Shorts videozapisa u \"Gledaj kasnije\" iz skočnog izbornika + Preusmjeri \"Gledaj kasnije\" na popis za reprodukciju + Gledaj kasnije Dodaj u Morphe red čekanja Čuvati red čekanja između sesija Red čekanja se zadržava nakon ponovnog pokretanja aplikacije @@ -1099,20 +1101,21 @@ Postavke $\rightarrow$ Reprodukcija (ili Automatsko reproduciranje) $\rightarrow Automobil "Promjene uključuju: -Izgled za tablet +Izgled za tablete • Objave zajednice su skrivene -• Postavka za reprodukciju u feedovima nije dostupna -• Gumb za remiks i gumb za zvuk nisu dostupni u Shortsu -• Prekrivač „Više videozapisa“ možda neće biti dostupan -• Prikazuje se stara traka radnji videozapisa koja nije sažeta +• Postavka Reprodukcije u feedovima nije dostupna +• Gumb Remix i gumb Zvuk nisu dostupni u Shorts videima +• Preklopnik \"Više videa\" možda nije dostupan +• Prikazuje se stara traka s radnjama za video koji nije smanjen +• Stavke izletnog izbornika možda neće raditi ispravno -Izgled za automobil +Automobilski izgled • Feed je organiziran po temama i kanalima -• Postavka za reprodukciju u feedovima nije dostupna -• Gumb za remiks i gumb za zvuk nisu dostupni u Shortsu -• Prekrivač „Više videozapisa“ možda neće biti dostupan -• Shorts se otvaraju u standardnom playeru -• Prikazuje se stara traka radnji videozapisa koja nije sažeta" +• Postavka Reprodukcije u feedovima nije dostupna +• Gumb Remix i gumb Zvuk nisu dostupni u Shorts videima +• Preklopnik \"Više videa\" možda nije dostupan +• Shorts videi otvaraju se u običnom playeru +• Prikazuje se stara traka s radnjama za video koji nije smanjen" Omogući tablet izgled u playeru "Vraća stari izgled playera diff --git a/patches/src/main/resources/addresources/values-hu-rHU/youtube/strings.xml b/patches/src/main/resources/addresources/values-hu-rHU/youtube/strings.xml index f4aaaa91c9..7b38cd8274 100644 --- a/patches/src/main/resources/addresources/values-hu-rHU/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-hu-rHU/youtube/strings.xml @@ -613,6 +613,8 @@ Figyelem: Lehet, hogy az élő közvetítéseknél nem működik" Lehetővé teszi gyerekeknek szóló videók mentését a \'Watch later\' listába a felugró menüből A \'Watch later\' mentése gomb megjelenítése a Shorts felugró menüjében Lehetővé teszi a Shorts mentését a \'Watch later\' listába a felugró menüből + Később megnézem átirányítása lejátszási listára + Megnézem később Hozzáadás a Morphe sorhoz Tartsa fenn a sorát a munkamenetek között A sor az alkalmazás újraindítása után is megőrizhető @@ -1109,22 +1111,23 @@ Beállítások → Lejátszás (vagy Automatikus lejátszás) → Következő vi Telefon Táblagép Autóipari - "Változások: + "A módosítások a következők: -Táblagép-elrendezés +Tablet nézet • A közösségi bejegyzések el vannak rejtve -• A lejátszás beállítása a feedekben nem érhető el -• A Remix gomb és a Hang gomb nem érhetők el a Shorts-ban -• A „További videók” átfedés nem feltétlenül érhető el -• A nem összecsukott régi videó műveletsáv megjelenik +• A hírfolyamban történő lejátszás beállítás nem elérhető +• A Remix és a Hang gomb nem érhető el a Shortsokban +• A 'További videók' átfedő ablak lehet, hogy nem érhető el +• A nem összecsukott régi videóműveletsáv megjelenítése +• A kibontási menüelemek lehet, hogy nem működnek megfelelően -Autós elrendezés -• A feed témák és csatornák szerint van rendezve -• A lejátszás beállítása a feedekben nem érhető el -• A Remix gomb és a Hang gomb nem érhetők el a Shorts-ban -• A „További videók” átfedés nem feltétlenül érhető el -• A Shorts a normál lejátszóban nyílik meg -• A nem összecsukott régi videó műveletsáv megjelenik" +Autószórakoztató rendszer nézet +• A hírfolyam témákra és csatornákra van szervezve +• A hírfolyamban történő lejátszás beállítás nem elérhető +• A Remix és a Hang gomb nem érhető el a Shortsokban +• A 'További videók' átfedő ablak lehet, hogy nem érhető el +• A Shortsok a szabályos lejátszóban nyílnak meg +• A nem összecsukott régi videóműveletsáv megjelenítése" Tablet elrendezés engedélyezése a lejátszóban "A régi lejátszó elrendezés visszaállítása diff --git a/patches/src/main/resources/addresources/values-in-rID/youtube/strings.xml b/patches/src/main/resources/addresources/values-in-rID/youtube/strings.xml index 13bd70c601..010cff55ac 100644 --- a/patches/src/main/resources/addresources/values-in-rID/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-in-rID/youtube/strings.xml @@ -613,6 +613,8 @@ Info: Mungkin tidak berfungsi pada siaran langsung" Memungkinkan Anda menyimpan video terkait anak-anak ke \'Tonton Nanti\' melalui menu flyout. Tampilkan tombol Simpan ke Tonton Nanti di flyout Shorts Memungkinkan Anda menyimpan Shorts ke \'Tonton Nanti\' melalui menu flyout. + Alihkan Tonton nanti ke daftar putar + Tonton nanti Tambahkan ke antrean Morphe Simpan antrean antar sesi Antrean disimpan setelah memulai ulang aplikasi diff --git a/patches/src/main/resources/addresources/values-is-rIS/youtube/strings.xml b/patches/src/main/resources/addresources/values-is-rIS/youtube/strings.xml index 26b2580055..7e13f08816 100644 --- a/patches/src/main/resources/addresources/values-is-rIS/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-is-rIS/youtube/strings.xml @@ -601,6 +601,8 @@ API lykillinn þinn ætti aldrei að deila. Allir með aðgang að honum geta no Leyfir að vista barnatengd myndbönd í \'Horfa seinna\' úr sprettivalmyndinni Sýna hnappinn Vista í Horfa seinna í sprettivalmynd Shorts Leyfir að vista Shorts í \'Horfa seinna\' úr sprettivalmyndinni + Stefna „Horfa síðar“ á spilalista + Horfa síðar Bæta í Morphe biðröð Geymdu biðrað á milli lotna Biðlisti er vistaður eftir endurræsingar appinu @@ -1093,21 +1095,7 @@ Stillingar → Spilun (eða Sjálfvædd spilun) → Sjálfvædd næsta myndband" Sími Tafla Bíll - "Breytingar fela í sér: - -Spjaldtölvuútlit -• Samfélagsfærslur eru falnar -• Stillingin Spilun í straumum er ekki tiltæk -• Remix-hnappur og hljóðhnappur eru ekki tiltækir í Shorts -• Yfirlagið „More videos“ gæti ekki verið tiltækt -• Gömul myndbandsaðgerðarstika sem er ekki samanfallin er sýnd - -Bílaútlit -• Straumurinn er skipulagður eftir efnum og rásum -• Stillingin Spilun í straumum er ekki tiltæk -• Remix-hnappur og hljóðhnappur eru ekki tiltækir í Shorts -• Shorts opnast í venjulega spilaranum -• Gömul myndbandsaðgerðarstika sem er ekki samanfallin er sýnd" + "Breytingar fela í sér:\n\nSpjaldtölvulag\n• Færslur samfélagsins eru falin\n• Stilling fyrir spilun í flæðum er ekki tiltæk\n• Takkinn Remix og hljóðtakki eru ekki til í Shorts\n• Yfirlag „fleiri myndbönd“ kann að vera ótiltekið\n• Gamalt aðgerðastikur myrkunnar sem ekki er samanbrotið er sýnt\n• Íflýgvalmynd atriði virka kannski ekki rétt\n\nBílalag\n• Flæði er skipulegt eftir efnum og rásum\n• Stilling fyrir spilun í flæðum er ekki tiltæk\n• Takkinn Remix og hljóðtakki eru ekki til í Shorts\n• Yfirlag „fleiri myndbönd“ kann að vera ótiltekið\n• Shorts opnast í venjulega spilaranum\n• Gamalt aðgerðastikur myrkunnar sem ekki er samanbrotið er sýnt" Virkja taflalýsing í spilara "Endurskipar gamla útlit spilarans diff --git a/patches/src/main/resources/addresources/values-it-rIT/youtube/strings.xml b/patches/src/main/resources/addresources/values-it-rIT/youtube/strings.xml index d4d1997609..a8bc61acd1 100644 --- a/patches/src/main/resources/addresources/values-it-rIT/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-it-rIT/youtube/strings.xml @@ -613,6 +613,8 @@ Avviso: Potrebbe non funzionare sui video in diretta" Consente di salvare video per bambini in \'Guarda più tardi\' dal menu a comparsa Mostra il pulsante Salva in Guarda più tardi nel menu a comparsa degli Shorts Consente di salvare gli Shorts in \'Guarda più tardi\' dal menu a comparsa + Reindirizza Guarda più tardi alla playlist + Guarda più tardi Aggiungi alla coda di Morphe Mantieni la coda tra le sessioni La coda viene mantenuta anche riavviando l\'app diff --git a/patches/src/main/resources/addresources/values-iw-rIL/youtube/strings.xml b/patches/src/main/resources/addresources/values-iw-rIL/youtube/strings.xml index 403c7ce3cc..957a48e218 100644 --- a/patches/src/main/resources/addresources/values-iw-rIL/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-iw-rIL/youtube/strings.xml @@ -613,6 +613,8 @@ Second \"item\" text" מאפשר לשמור סרטונים הקשורים לילדים ברשימת \'צפה מאוחר יותר\' מהתפריט הנפתח הצג כפתור שמירה ל\'צפה מאוחר יותר\' בתפריט הנפתח של הסרטונים הקצרים מאפשר לשמור סרטונים קצרים ברשימת \'צפה מאוחר יותר\' מהתפריט הנפתח + הפנה את \"צפייה מאוחר יותר\" לפלייליסט + לצפייה בהמשך הוסף לתור Morphe שמור תור בין הפעלות התור נשמר גם לאחר הפעלה מחדש של האפליקציה @@ -1111,20 +1113,21 @@ Second \"item\" text" רכב "השינויים כוללים: -פריסת טאבלט -• פוסטים קהילתיים מוסתרים -• הגדרת השמעה בפידים אינה זמינה -• כפתור רמיקס וכפתור סאונד אינם זמינים ב-Shorts -• שכבת 'עוד סרטונים' עשויה לא להיות זמינה -• סרגל פעולות וידאו ישן שאינו מקופל מוצג +מסמך טאבלט +• פוסטים בקהילה מוסתרים +• הגדרת ניגון בריבים אינה זמינה +• כפתורי הרמיקס וצליל אינם זמינים בסרטונים קצרים +• ייתכן ששכבת העל 'סרטונים נוספים' לא תהיה זמינה +• יישור הפעולה הישן לא המקופל של הסרטון מוצג +• פריטי תפריט קופץ עשויים לא לעבוד כראוי -פריסה לרכב -• הפיד מאורגן לפי נושאים וערוצים -• הגדרת השמעה בפידים אינה זמינה -• כפתור רמיקס וכפתור סאונד אינם זמינים ב-Shorts -• שכבת 'עוד סרטונים' עשויה לא להיות זמינה -• Shorts נפתחים בנגן הרגיל -• סרגל פעולות וידאו ישן שאינו מקופל מוצג" +מסמך רכב +• הריב מסודר לפי נושאים וערוצים +• הגדרת ניגון בריבים אינה זמינה +• כפתורים של הרמיקס וצליל אינם זמינים בסרטונים קצרים +• ייתכן ששכבת העל 'סרטונים נוספים' לא תהיה זמינה +• סרטונים קצרים נפתחים בנגן הרגיל +• יישור הפעולה הישן לא המקופל של הסרטון מוצג" הפעל פריסת טאבלט בנגן "משחזר את פריסת הנגן הישנה diff --git a/patches/src/main/resources/addresources/values-ja-rJP/youtube/strings.xml b/patches/src/main/resources/addresources/values-ja-rJP/youtube/strings.xml index 82c956de64..3b2bcc2561 100644 --- a/patches/src/main/resources/addresources/values-ja-rJP/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ja-rJP/youtube/strings.xml @@ -614,6 +614,8 @@ APIキーは決して共有しないでください。アクセス権を持つ 子ども向け動画の関連動画のフライアウト メニューに「[後で見る] に保存」を表示します ショート動画のフライアウト メニューに「[後で見る] に保存」を表示 ショート動画のフライアウト メニューに「[後で見る] に保存」を表示します + 「後で見る」をプレイリストにリダイレクト + 後で見る Morphe キューに追加 セッションをまたいでキューを保持 アプリを終了または再起動したときにキューを保持します @@ -1110,22 +1112,22 @@ APIキーは決して共有しないでください。アクセス権を持つ スマートフォン タブレット Automotive - "以下の変更を伴います: + "変更点: タブレット レイアウト -• コミュニティ投稿が表示されない -• YouTube の設定項目の [フィードで再生] が表示されない -• ショート動画のリミックス ボタンとサウンドボタンが利用できない -•「その他の動画」オーバーレイが表示されない場合がある -• 動画プレーヤーのアクション ボタンにボタン名が表示される (旧仕様) +• コミュニティ投稿は非表示になります +• フィードでの再生設定は無効です +• Shortsでのリミックス ボタンとサウンド ボタンは無効です +• 「その他の動画」オーバーレイが表示されない場合があります +• 折りたたみ前の旧動画アクションバーが表示されます Automotive レイアウト -• フィードがトピックやチャンネルごとに分類された形で表示される -• YouTube の設定項目の [フィードで再生] が表示されない -• ショート動画のリミックス ボタンとサウンドボタンが利用できない -• 「その他の動画」オーバーレイが表示されない場合がある -• ショート動画が通常のプレーヤーで再生される -• 動画プレーヤーのアクション ボタンにボタン名が表示される (旧仕様)" +• フィードはトピックやチャンネルごとに整理されます +• フィードでの再生設定は無効です +• Shortsでのリミックス ボタンとサウンド ボタンは無効です +• 「その他の動画」オーバーレイが表示されない場合があります +• Shortsは通常のプレーヤーで開きます +• 折りたたみ前の旧動画アクションバーが表示されます" プレーヤーにのみタブレット レイアウトを適用 "動画プレーヤーのレイアウトを以前のスタイルに戻します diff --git a/patches/src/main/resources/addresources/values-kmr-rTR/youtube/strings.xml b/patches/src/main/resources/addresources/values-kmr-rTR/youtube/strings.xml index 34e7a9887b..1753147d86 100644 --- a/patches/src/main/resources/addresources/values-kmr-rTR/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-kmr-rTR/youtube/strings.xml @@ -451,6 +451,7 @@ Info: May not work on live streams" Nikare vî vîdyoyê di lîsteya tomar kirina ji bo paşê dîtinê de tomar bike Paşê temaşekirinê tomar bike Bişkoka \'Paşê temaşekirinê tomar bike\' di vîdyoyên zarokan de nîşan bide + Dîrûvaniya paş Zêde bikeye rêza Morphe Lîsteyê di nav beşan de bidomîne Lîste piştî destkirina serî lêve tê parastin diff --git a/patches/src/main/resources/addresources/values-ko-rKR/youtube/strings.xml b/patches/src/main/resources/addresources/values-ko-rKR/youtube/strings.xml index c13281cf38..8b7b231914 100644 --- a/patches/src/main/resources/addresources/values-ko-rKR/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ko-rKR/youtube/strings.xml @@ -615,6 +615,8 @@ API 키는 절대로 다른 사람과 공유해서는 안 됩니다. API 키에 피드 메뉴로 관련 동영상에 있는 Kids 동영상을 \'나중에 볼 동영상\'에 저장할 수 있습니다 Shorts에 \'나중에 볼 동영상에 저장\' 메뉴 표시하기 피드 메뉴로 Shorts를 \'나중에 볼 동영상\'에 저장할 수 있습니다 + 나중에 볼 동영상을 재생목록으로 리디렉션 + 나중에 볼 동영상 Morphe 현재 재생목록에 추가 세션 간에 현재 재생목록 유지하기 앱을 다시 시작한 후에도 현재 재생목록이 유지됩니다 @@ -1111,22 +1113,22 @@ API 키는 절대로 다른 사람과 공유해서는 안 됩니다. API 키에 폰 태블릿 오토모티브 - "변경 사항: + "변경 사항: 태블릿 레이아웃 • 커뮤니티 게시물이 숨겨집니다 • '피드에서 재생' 설정을 사용할 수 없습니다 • Shorts에서 리믹스 버튼과 사운드 버튼을 사용할 수 없습니다 -• '동영상 더보기' 오버레이를 사용할 수 없을 수 있습니다 -• 접히지 않는 이전 플레이어 작업 버튼바가 표시됩니다 +• '동영상 더보기' 오버레이가 표시되지 않을 수 있습니다 +• 접히지 않은 이전 동영상 작업 표시줄이 표시됩니다 오토모티브 레이아웃 • 피드가 주제 및 채널별로 구성됩니다 • '피드에서 재생' 설정을 사용할 수 없습니다 • Shorts에서 리믹스 버튼과 사운드 버튼을 사용할 수 없습니다 -• '동영상 더보기' 오버레이를 사용할 수 없을 수 있습니다 -• Shorts가 일반 플레이어에서 재생됩니다 -• 접히지 않는 이전 플레이어 작업 버튼바가 표시됩니다" +• '동영상 더보기' 오버레이가 표시되지 않을 수 있습니다 +• Shorts는 일반 플레이어에서 열립니다 +• 접히지 않은 이전 동영상 작업 표시줄이 표시됩니다" 플레이어에서 태블릿 레이아웃 활성화하기 "이전 플레이어 레이아웃을 복원합니다 diff --git a/patches/src/main/resources/addresources/values-lt-rLT/youtube/strings.xml b/patches/src/main/resources/addresources/values-lt-rLT/youtube/strings.xml index 94b883d4a0..a0a7e39e2a 100644 --- a/patches/src/main/resources/addresources/values-lt-rLT/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-lt-rLT/youtube/strings.xml @@ -613,6 +613,8 @@ Informacija: gali neveikti tiesioginių transliacijų atveju" Leidžia išsaugoti su vaikais susijusius vaizdo įrašus sąraše „Žiūrėti vėliau“ iš iškylančiojo meniu Rodyti „Išsaugoti žiūrėti vėliau“ mygtuką „Shorts“ iškylančiajame meniu Leidžia išsaugoti „Shorts“ įrašus sąraše „Žiūrėti vėliau“ iš iškylančiojo meniu + Per nukreipti „Žiūrėti vėliau“ į playlistą + Žiūrėti vėliau Pridėti į Morphe eilę Išsaugoti eilę tarp seansų Eilė išsaugoma iš naujo paleidus programą @@ -1111,20 +1113,21 @@ Nustatymai → Atkūrimas (arba Automatinis atkūrimas) → Automatiškai leisti Automobilyje "Pakeitimai apima: -Planšetės maketas +Planšetės išdėstymas • Bendruomenės įrašai paslėpti -• Nustatymas „Atkūrimas sklaidos kanaluose“ nepasiekiamas -• „Remix“ ir „Garsas“ mygtukai nepasiekiami „Shorts“ -• „Daugiau vaizdo įrašų“ perdanga gali būti nepasiekiama -• Rodoma nesutraukta senoji vaizdo įrašo veiksmų juosta +• Peržiūros šerdyje nustatymas nėra prieinamas +• „Remix“ ir „Garso“ mygtukai yra neprieinami „Shorts“ +• „Daugiau video“ persegimas gali būti neprieinamas +• Rodo nesutrauktą senosios veiksmų juostos vaizdo įrašo +„Flyout“ meniu elementai gali neveikti tinkamai -Automobilio maketas -• Srautas suskirstytas pagal temas ir kanalus -• Nustatymas „Atkūrimas sklaidos kanaluose“ nepasiekiamas -• „Remix“ ir „Garsas“ mygtukai nepasiekiami „Shorts“ -• „Daugiau vaizdo įrašų“ perdanga gali būti nepasiekiama -• „Shorts“ atidaromi įprastame grotuve -• Rodoma nesutraukta senoji vaizdo įrašo veiksmų juosta" +Automobilio išdėstymas +• Šerdis organizuojama pagal temas ir kanalus +• Peržiūros šerdyje nustatymas nėra prieinamas +• „Remix“ ir „Garso“ mygtukai yra neprieinami „Shorts“ +• „Daugiau video“ persegimas gali būti neprieinamas +• „Shorts“ atidaromi reguliariame grotuve +• Rodo nesutrauktą senosios veiksmų juostos vaizdo įrašo" Įjungti planšetės išdėstymą grotuve "Atkuria senąjį grotuvo maketą diff --git a/patches/src/main/resources/addresources/values-ms-rMY/youtube/strings.xml b/patches/src/main/resources/addresources/values-ms-rMY/youtube/strings.xml index cae016860a..1d3a966f66 100644 --- a/patches/src/main/resources/addresources/values-ms-rMY/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ms-rMY/youtube/strings.xml @@ -279,22 +279,6 @@ Second \"item\" text" Lalai - "Changes include: - -Tablet layout -• Community posts are hidden -• Playback in feeds setting is not available -• Remix button and Sound button are not available in Shorts -• 'More videos' overlay may not be available -• Non collapsed old video action bar is shown - -Automotive layout -• Feed is organized by topics and channels -• Playback in feeds setting is not available -• Remix button and Sound button are not available in Shorts -• 'More videos' overlay may not be available -• Shorts open in the regular player -• Non collapsed old video action bar is shown" Dayakan tata letak tablet di pemain diff --git a/patches/src/main/resources/addresources/values-nb-rNO/youtube/strings.xml b/patches/src/main/resources/addresources/values-nb-rNO/youtube/strings.xml index b91b59f8b5..8dd912b233 100644 --- a/patches/src/main/resources/addresources/values-nb-rNO/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-nb-rNO/youtube/strings.xml @@ -613,6 +613,8 @@ Info: Fungerer kanskje ikke på direktesendinger" Lar deg lagre barnerelaterte videoer i «Se senere» fra uttrekksmenyen. Vis «Lagre i Se senere»-knappen i uttrekksmenyen for Shorts. Lar deg lagre Shorts i «Se senere» fra uttrekksmenyen. + Omdiriger \"Se senere\" til spilleliste + Se senere Legg til i Morphe-kø Behold køen mellom øktene Køen beholdes etter omstart av appen @@ -1109,22 +1111,23 @@ Innstillinger → Avspilling (eller Autoplay) → Spill av neste video automatis Telefon Nettbrett Bil - "Endringene inkluderer: + "Endringene omfatter: -Nettbrett-layout -• Fellesskapsinnlegg er skjult +Nettbrettvisning +• Innlegg fra fellesskapet er skjult • Innstillingen for avspilling i feed er ikke tilgjengelig -• Remix-knappen og lydknappen er ikke tilgjengelige i Shorts -• 'Flere videoer'-overlegget er kanskje ikke tilgjengelig -• Den gamle videohandlingslinjen vises uten å være kollapset +• Knappene \"Gjenbruk\" og \"Lyd\" er ikke tilgjengelige i Shorts +• Legg til video-visning kan ikke være tilgjengelig +• Den ikke sammenfalte, gamle-handlingslinjen for video vises +• Flyutmenyalternativer fungerer kanskje ikke korrekt -Bil-layout +Bilvisning • Feed er organisert etter emner og kanaler • Innstillingen for avspilling i feed er ikke tilgjengelig -• Remix-knappen og lydknappen er ikke tilgjengelige i Shorts -• 'Flere videoer'-overlegget er kanskje ikke tilgjengelig -• Shorts åpnes i den vanlige spilleren -• Den gamle videohandlingslinjen vises uten å være kollapset" +• Knappene \"Gjenbruk\" og \"Lyd\" er ikke tilgjengelige i Shorts +• Legg til video-visning kan ikke være tilgjengelig +• Shorts åpnes i vanlig spiller +• Den ikke sammenfalte, gamle-handlingslinjen for video vises" Aktiver nettbrettlayout i spilleren "Gjenoppretter det gamle spillerlayoutet diff --git a/patches/src/main/resources/addresources/values-nl-rNL/youtube/strings.xml b/patches/src/main/resources/addresources/values-nl-rNL/youtube/strings.xml index 2c647e51cf..7b641d7fb0 100644 --- a/patches/src/main/resources/addresources/values-nl-rNL/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-nl-rNL/youtube/strings.xml @@ -500,8 +500,8 @@ Nadat een video met de Ask-knop eenmaal is geopend, werkt de functie voor alle v Video Audio Alle opties - \"Shorts\" weergeven in menu Downloaden - \"Shorts\"-speler krijgt item voor extern downloadmenu + Download tonen in Shorts-menu + Voegt een extern downloadmenu-item toe aan de Shorts-speler Download Spraakvertaling @@ -613,6 +613,8 @@ Info: Werkt mogelijk niet op livestreams" Hiermee kunt u kindgerelateerde video\'s opslaan in \'Later bekijken\' via het flyoutmenu Toon de knop \'Opslaan in Later bekijken\' in het flyoutmenu voor Shorts Hiermee kunt u Shorts opslaan in \'Later bekijken\' via het flyoutmenu + Stuur \'Later bekijken\' door naar afspeellijst + Later bekijken Toevoegen aan Morphe-wachtrij Wachtrij behouden tussen sessies Wachtrij blijft behouden na het opnieuw starten van de app @@ -1107,22 +1109,23 @@ Instellingen → Weergave (of automatisch afspelen) → Volgende video automatis Telefoon Tablet Auto - "Wijzigingen omvatten: + "Wijzigingen zijn onder meer: Tablet-indeling -• Community-posts zijn verborgen -• De instelling 'Afspelen in feeds' is niet beschikbaar -• Remix-knop en geluidsknop zijn niet beschikbaar in Shorts -• De overlay 'Meer video's' is mogelijk niet beschikbaar -• De niet-ingeklapte oude video-actiebalk wordt weergegeven +• Communityberichten zijn verborgen +• Instelling voor afspelen in feeds is niet beschikbaar +• Remix-knop en Geluidsknop zijn niet beschikbaar in Shorts +• Overlay 'Meer video's' is mogelijk niet beschikbaar +• Niet ingeklapte oude actiebalk voor video's wordt weergegeven +• Items in het uitklapmenu werken mogelijk niet correct -Automotive-indeling -• Feed is georganiseerd op onderwerpen en kanalen -• De instelling 'Afspelen in feeds' is niet beschikbaar -• Remix-knop en geluidsknop zijn niet beschikbaar in Shorts -• De overlay 'Meer video's' is mogelijk niet beschikbaar -• Shorts openen in de normale speler -• De niet-ingeklapte oude video-actiebalk wordt weergegeven" +Auto-indeling +• Feed is georganiseerd per onderwerp en kanaal +• Instelling voor afspelen in feeds is niet beschikbaar +• Remix-knop en Geluidsknop zijn niet beschikbaar in Shorts +• Overlay 'Meer video's' is mogelijk niet beschikbaar +• Shorts worden geopend in de reguliere speler +• Niet ingeklapte oude actiebalk voor video's wordt weergegeven" Schakel tabletindeling in de speler in "Herstelt de oude player layout @@ -1640,7 +1643,7 @@ Automatische registratie is optioneel. Deze stopt in incognito-modus of bij een Opaciteit van de reactietekst en achtergronden Reistijd Hoeveel seconden reacties erover doen om het scherm te kruisen. Lagere waarden bewegen sneller - Maximale lengte van opmerking - Verbergt opmerkingen met meer tekens dan dit. Elke emoji telt als één teken + Maximale commentlengte + Verbergt comments met meer tekens dan dit. Elke emoji telt als één teken Geen limiet diff --git a/patches/src/main/resources/addresources/values-pl-rPL/youtube/strings.xml b/patches/src/main/resources/addresources/values-pl-rPL/youtube/strings.xml index a235d3a150..ef8602c537 100644 --- a/patches/src/main/resources/addresources/values-pl-rPL/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-pl-rPL/youtube/strings.xml @@ -613,6 +613,8 @@ Uwaga: Może nie działać na transmisjach na żywo" Umożliwia zapisywanie filmów przeznaczonych dla dzieci na liście \"Do obejrzenia\" z poziomu menu Pokaż przycisk \"Zapisz do obejrzenia\" w menu Shorts Umożliwia zapisywanie filmów Short na liście \"Do obejrzenia\" z poziomu menu + Przekieruj \"Do obejrzenia później\" do playlisty + Do obejrzenia Dodaj do kolejki Morphe Zachowaj kolejkę po wyjściu Kolejka nie zniknie po ponownym uruchomieniu aplikacji @@ -1111,19 +1113,20 @@ To ustawienie działa tylko wtedy, gdy autoodtwarzanie jest wyłączone. Możesz "Zmiany obejmują: Układ tabletowy -• Posty są ukryte -• Ustawienie odtwarzania na stronie głównej nie jest dostępne -• Przyciski Remiks i Dźwięk nie są dostępne w Shortsach -• Nakładka \"Więcej filmów\" może nie być dostępna -• Stary pasek akcji filmu nie jest zwinięty +• Posty społeczności są ukryte +• Ustawienie odtwarzania w kanałach nie jest dostępne +• Przyciski Remiks i Dźwięk nie są dostępne w Shorts +• Nakładka „Więcej filmów” może nie być dostępna +• Pokazany jest nierozwinięty stary pasek akcji filmu +• Elementy menu wyskakującego mogą nie działać poprawnie Układ samochodowy -• Strona główna jest uporządkowana według tematów i kanałów -• Ustawienie odtwarzania na stronie głównej nie jest dostępne -• Przyciski Remiks i Dźwięk nie są dostępne w Shortsach -• Nakładka \"Więcej filmów\" może nie być dostępna -• Filmy Short otwierają się w zwykłym odtwarzaczu -• Stary pasek akcji filmu nie jest zwinięty" +• Kanały są uporządkowane według tematów i kanałów +• Ustawienie odtwarzania w kanałach nie jest dostępne +• Przyciski Remiks i Dźwięk nie są dostępne w Shorts +• Nakładka „Więcej filmów” może nie być dostępna +• Shorts otwierają się w standardowym odtwarzaczu +• Pokazany jest nierozwinięty stary pasek akcji filmu" Włącz układ tabletowy w odtwarzaczu "Przywraca stary układ odtwarzacza diff --git a/patches/src/main/resources/addresources/values-pt-rBR/youtube/strings.xml b/patches/src/main/resources/addresources/values-pt-rBR/youtube/strings.xml index e4a447d9e9..9da07b13d4 100644 --- a/patches/src/main/resources/addresources/values-pt-rBR/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-pt-rBR/youtube/strings.xml @@ -613,6 +613,8 @@ Nota: pode não funcionar em transmissões ao vivo" Permite salvar vídeos relacionados a crianças em \'Assistir mais tarde\' pelo menu flutuante Mostrar o botão Salvar em \'Assistir mais tarde\' no menu de Shorts Permite salvar Shorts em \'Assistir mais tarde\' pelo menu flutuante + Redirecionar \"Assistir mais tarde\" para a playlist + Assistir mais tarde Adicionar à fila do Morphe Manter a fila entre as sessões A fila é mantida após reiniciar o aplicativo diff --git a/patches/src/main/resources/addresources/values-pt-rPT/youtube/strings.xml b/patches/src/main/resources/addresources/values-pt-rPT/youtube/strings.xml index 1464437728..8b102a3785 100644 --- a/patches/src/main/resources/addresources/values-pt-rPT/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-pt-rPT/youtube/strings.xml @@ -613,6 +613,8 @@ Informação: Pode não funcionar em transmissões ao vivo" Permite salvar vídeos relacionados a crianças em \'Assistir mais tarde\' pelo menu. Mostrar o botão Salvar em \'Assistir mais tarde\' no menu de Shorts. Permite salvar Shorts em \'Assistir mais tarde\' pelo menu. + Redirecionar \"Assistir mais tarde\" para a playlist + Assistir depois Adicionar à fila do Morphe Manter fila entre as sessões A fila é mantida após reiniciar o aplicativo @@ -1109,22 +1111,23 @@ Configurações → Reprodução (ou Reprodução automática) → Reproduzir o Telemóvel Tablet Automotivo - "As mudanças incluem: + "As alterações incluem: Layout para tablet -• As publicações da comunidade ficam ocultas -• A configuração de reprodução em feeds não está disponível -• Os botões Remix e Som não estão disponíveis no Shorts +• As postagens da comunidade estão ocultas +• A configuração Reprodução nos feeds não está disponível +• O botão Remix e o botão Som não estão disponíveis no Shorts • A sobreposição 'Mais vídeos' pode não estar disponível -• A antiga barra de ações do vídeo é exibida expandida +• A barra de ação de vídeo antiga sem colapso é exibida +• Os itens do menu flutuante podem não funcionar corretamente -Layout automotivo +Layout para automóvel • O feed é organizado por tópicos e canais -• A configuração de reprodução em feeds não está disponível -• Os botões Remix e Som não estão disponíveis no Shorts +• A configuração Reprodução nos feeds não está disponível +• O botão Remix e o botão Som não estão disponíveis no Shorts • A sobreposição 'Mais vídeos' pode não estar disponível -• Os Shorts abrem no player normal -• A antiga barra de ações do vídeo é exibida expandida" +• Os Shorts abrem no player regular +• A barra de ação de vídeo antiga sem colapso é exibida" Habilitar layout de tablet no player "Restaura o layout antigo do player diff --git a/patches/src/main/resources/addresources/values-ro-rRO/youtube/strings.xml b/patches/src/main/resources/addresources/values-ro-rRO/youtube/strings.xml index 519fa4348c..8667535c8b 100644 --- a/patches/src/main/resources/addresources/values-ro-rRO/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ro-rRO/youtube/strings.xml @@ -613,6 +613,8 @@ Info: S-ar putea să nu funcționeze pe transmisiuni live" Permite salvarea videoclipurilor pentru copii în „Vizionează mai târziu” din meniul derulant Afișează butonul Salvează în Vizionează mai târziu în meniul pentru Shorts Permite salvarea shorts-urilor în „Vizionează mai târziu” din meniul derulant + Redirecționează „ulterior” către o listă de redare + Urmărește mai târziu Adaugă în coada Morphe Menține coada între sesiuni Coada este păstrată după repornirea aplicației @@ -1111,20 +1113,21 @@ Setări → Redare (sau Redare automată) → Redare automată următorul videoc Automatizări "Modificările includ: -Aspect tablet -• Postările comunității sunt ascunse -• Setarea de redare în fluxuri nu este disponibilă +Aspect tabletă +• Postările din comunitate sunt ascunse +• Setarea redare în feed nu este disponibilă • Butonul Remix și butonul Sunet nu sunt disponibile în Shorts -• Suprapunerea 'Mai multe videoclipuri' poate să nu fie disponibilă -• Bara de acțiuni veche a videoclipului este afișată necontractată +• Suprapunerea „Alte videoclipuri” poate să nu fie disponibilă +• Se afișează bara de acțiuni video vechi, nepredisată +• Elementele meniului lateral (flyout) pot să nu funcționeze corect -Aspect auto -• Fluxul este organizat pe subiecte și canale -• Setarea de redare în fluxuri nu este disponibilă +Aspect pentru automotive +• Feed-ul este organizat pe subiecte și canale +• Setarea redare în feed nu este disponibilă • Butonul Remix și butonul Sunet nu sunt disponibile în Shorts -• Suprapunerea 'Mai multe videoclipuri' poate să nu fie disponibilă -• Shorts se deschid în playerul obișnuit -• Bara de acțiuni veche a videoclipului este afișată necontractată" +• Suprapunerea „Alte videoclipuri” poate să nu fie disponibilă +• Shorts se deschide în playerul obișnuit +• Se afișează bara de acțiuni video vechi, nepredisată" Activează aspectul tabletă în player "Restabilește aspectul vechi al playerului diff --git a/patches/src/main/resources/addresources/values-ru-rRU/youtube/strings.xml b/patches/src/main/resources/addresources/values-ru-rRU/youtube/strings.xml index 15534073ff..07c7340ca0 100644 --- a/patches/src/main/resources/addresources/values-ru-rRU/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ru-rRU/youtube/strings.xml @@ -615,6 +615,8 @@ Second \"item\" text" Позволяет сохранять детские видео в «Посмотреть позже» из всплывающего меню Показывать кнопку «Сохранить в „Посмотреть позже“» во всплывающем меню шортсов Позволяет сохранять шортсы в «Посмотреть позже» из всплывающего меню + Перенаправить «Смотреть позже» в плейлист + Смотреть позже Добавить в очередь Morphe Сохранять очередь между сеансами Очередь сохраняется после перезапуска приложения @@ -1113,20 +1115,20 @@ Second \"item\" text" Автомобильный "Изменения включают: -Макет планшета +Макет для планшета • Публикации сообщества скрыты -• Настройка «Воспроизведение в лентах» недоступна -• Кнопка «Ремикс» и кнопка «Звук» недоступны в Shorts -• Оверлей «Больше видео» может быть недоступен -• Показана старая развернутая панель действий видео +• Настройка воспроизведения в лентах недоступна +• Кнопки «Ремикс» и «Звук» недоступны в Shorts +• Надпись «Больше видео» может быть недоступна +• Отображается старая неразвернутая панель действий с видео -Автомобильный макет +Макет для автомобилей • Лента организована по темам и каналам -• Настройка «Воспроизведение в лентах» недоступна -• Кнопка «Ремикс» и кнопка «Звук» недоступны в Shorts -• Оверлей «Больше видео» может быть недоступен +• Настройка воспроизведения в лентах недоступна +• Кнопки «Ремикс» и «Звук» недоступны в Shorts +• Надпись «Больше видео» может быть недоступна • Shorts открываются в обычном плеере -• Показана старая развернутая панель действий видео" +• Отображается старая неразвернутая панель действий с видео" Включить макет для планшетов в плеере "Восстанавливает старый макет плеера diff --git a/patches/src/main/resources/addresources/values-sk-rSK/youtube/strings.xml b/patches/src/main/resources/addresources/values-sk-rSK/youtube/strings.xml index 928490c5fc..798d7683a5 100644 --- a/patches/src/main/resources/addresources/values-sk-rSK/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-sk-rSK/youtube/strings.xml @@ -613,6 +613,8 @@ Poznámka: Možno nebude fungovať na živých prenosoch" Umožňuje z rozbaľovacej ponuky ukladať videá pre deti do zoznamu „Pozrieť neskôr“ Zobraziť tlačidlo „Uložiť na pozretie neskôr“ v rozbaľovacej ponuke pre Shorts Umožňuje z rozbaľovacej ponuky ukladať Shorts do zoznamu „Pozrieť neskôr“ + Presmerovať „Pozrieť neskôr“ do zoznamu prevádzania + Pozrieť neskôr Pridať do fronty Morphe Uchovajte si rad medzi reláciami Rad zostane zachovaný aj po reštarte aplikácie @@ -1113,17 +1115,19 @@ Nastavenia → Prehrávanie (alebo Automatické prehrávanie) → Automaticky pr Rozloženie pre tablet • Príspevky komunity sú skryté -• Nastavenie prehrávania vo feedoch nie je k dispozícii -• Tlačidlo Remix a tlačidlo Zvuk nie sú v službe Shorts k dispozícii -• Prekrytie „Ďalšie videá“ nemusí byť k dispozícii -• Zobrazuje sa rozbalený starý panel akcií videa +• Nastavenie prehrávania vo feedi nie je k dispozícii +• Tlačidlá Remixovať a Zvuk nie sú k dispozícii v Shorts +• Vrstva 'Ďalšie videá' nemusí byť k dispozícii +• Zobrazuje sa nezbalený starý panel akcií videa +• Položky vyskakovacieho menu nemusia fungovať správne Rozloženie pre automobil -• Feed je usporiadaný podľa tém a kanálov -• Nastavenie prehrávania vo feedoch nie je k dispozícii -• Tlačidlo Remix a tlačidlo Zvuk nie sú v službe Shorts k dispozícii +• Feed je organizovaný podľa tém a kanálov +• Nastavenie prehrávania vo feedi nie je k dispozícii +• Tlačidlá Remixovať a Zvuk nie sú k dispozícii v Shorts +• Vrstva 'Ďalšie videá' nemusí byť k dispozícii • Shorts sa otvárajú v bežnom prehrávači -• Zobrazuje sa rozbalený starý panel akcií videa" +• Zobrazuje sa nezbalený starý panel akcií videa" Povoliť rozloženie tabletu v prehrávači "Obnoví staré rozloženie prehrávača diff --git a/patches/src/main/resources/addresources/values-sl-rSI/youtube/strings.xml b/patches/src/main/resources/addresources/values-sl-rSI/youtube/strings.xml index 6200f293a6..4fdc4e44cf 100644 --- a/patches/src/main/resources/addresources/values-sl-rSI/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-sl-rSI/youtube/strings.xml @@ -613,6 +613,8 @@ Opozorilo: morda ne deluje na prenosih v živo" Omogoča shranjevanje videoposnetkov za otroke v \'Ogled pozneje\' iz pojavnega menija Prikaži gumb Shrani v Ogled pozneje v pojavnem meniju Shorts Omogoča shranjevanje kratkih videoposnetkov v \'Ogled pozneje\' iz pojavnega menija + Preusmerji \"Glej pozneje\" na seznam predvajanja + Glej pozneje Dodaj v Morphe čakalno vrsto Ohranite vrsto med sejami Vrsta se shrani tudi po ponovnem zagonu aplikacije @@ -1111,20 +1113,21 @@ Nastavitve → Predvajanje (ali Samodejno predvajanje) → Samodejno predvajanje Avto "Spremembe vključujejo: -Postavitev za tablice +Razporeditev za tablična računalnika • Objave skupnosti so skrite -• Nastavitev predvajanja v virih ni na voljo -• Gumba Remix in Zvok v Shorts nista na voljo -• Prekrivni element »Več videoposnetkov« morda ni na voljo -• Prikazana je stara vrstica z dejanji videa v razširjenem stanju +• Nastavitev predvajanja v viru ni na voljo +• Gumb za združevanje in gumb Zvok nista na voljo v prilogah Shorts +• Prekrivno okno 'Več videoposnetkov' morda ne bo na voljo +• Prikaže se nekompaktirana starejša vrstica dejanj za videoposnetke +• elementi izpadnega menija morda ne bodo pravilno delovali -Postavitev za avtomobile -• Vir je organiziran po temah in kanalih -• Nastavitev predvajanja v virih ni na voljo -• Gumba Remix in Zvok v Shorts nista na voljo -• Prekrivni element »Več videoposnetkov« morda ni na voljo -• Shorts se odprejo v običajnem predvajalniku -• Prikazana je stara vrstica z dejanji videa v razširjenem stanju" +Razporeditev za avtomobile +• Vir je organiziran po tematskih skupinah in kanalih +• Nastavitev predvajanja v viru ni na voljo +• Gumb za združevanje in gumb Zvok nista na voljo v prilogah Shorts +• Prekrivno okno 'Več videoposnetkov' morda ne bo na voljo +• Priloge Shorts se odprejo v običajnem predvajalniku +• Prikaže se nekompaktirana starejša vrstica dejanj za videoposnetke" Omogoči postavitev za tablične računalnike v predvajalniku "Ponastavi prejšnjo postavitev predvajalnika diff --git a/patches/src/main/resources/addresources/values-sr-rCS/youtube/strings.xml b/patches/src/main/resources/addresources/values-sr-rCS/youtube/strings.xml index 7b095d3f7a..f1cd1ee145 100644 --- a/patches/src/main/resources/addresources/values-sr-rCS/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-sr-rCS/youtube/strings.xml @@ -613,6 +613,8 @@ Napomena: Možda neće raditi na direktnim prenosima" Omogućava čuvanje video snimaka povezanih sa decom u listi „Pogledaj kasnije“ iz menija koji se otvara. Prikaži dugme Sačuvaj u listu „Pogledaj kasnije“ u meniju Shortsa. Omogućava čuvanje Shortsa u listi „Pogledaj kasnije“ iz menija koji se otvara. + Preusmeri \"Gledaj kasnije\" na plejlistu + Za kasnije Dodaj u Morphe red Zadrži red čekanja između sesija Red čekanja je sačuvan nakon ponovnog pokretanja aplikacije @@ -1111,20 +1113,21 @@ Postavke → Reprodukcija (ili Automatsko puštanje) → Automatski pusti slede Automobilski "Promene uključuju: -Raspored za tablet +Tablet raspored • Objave zajednice su sakrivene -• Postavka „Reprodukcija u prikazu sadržaja“ nije dostupna -• Dugme Remiks i dugme Zvuk nisu dostupni u Shorts-u -• Prekrivač „Još video snimaka“ možda neće biti dostupan -• Prikazuje se stara, nesažeta traka sa radnjama za video +• Postavka reprodukcije u feedu nije dostupna +• Dugme za remixovanje i dugme za zvuk nisu dostupni u Shorts-u +• Preklapanje 'Još videa' možda nije dostupno +• Priказuje se nerasklapana stara akcijska traka za video +• Stavke flyout menija možda neće raditi ispravno -Raspored za automobil -• Prizak sadržaja je organizovan po temama i kanalima -• Postavka „Reprodukcija u prikazu sadržaja“ nije dostupna -• Dugme Remiks i dugme Zvuk nisu dostupni u Shorts-u -• Prekrivač „Još video snimaka“ možda neće biti dostupan -• Shorts se otvaraju u standardnom video plejeru -• Prikazuje se stara, nesažeta traka sa radnjama za video" +Automotive raspored +• Feed je organizovan po temama i kanalima +• Postavka reprodukcije u feedu nije dostupna +• Dugme za remixovanje i dugme za zvuk nisu dostupni u Shorts-u +• Preklapanje 'Još videa' možda nije dostupno +• Shorts se otvara u regularnom plejeru +• Priказuje se nerasklapana stara akcijska traka za video" Omogući izgled za tablet u plejeru "Vraća stari raspored plejera diff --git a/patches/src/main/resources/addresources/values-sr-rSP/youtube/strings.xml b/patches/src/main/resources/addresources/values-sr-rSP/youtube/strings.xml index b597636464..2db56a5a90 100644 --- a/patches/src/main/resources/addresources/values-sr-rSP/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-sr-rSP/youtube/strings.xml @@ -613,6 +613,8 @@ Second \"item\" text" Омогућава чување дечијих видео снимака у листу „Погледај касније“ из искачућег менија. Прикажи дугме „Сачувај у Погледај касније“ у искачућем менију за кратке видео снимке. Омогућава чување кратких видео снимака у листу „Погледај касније“ из искачућег менија. + Преусмери „Гледај kasnije” на плејлисту + За касније Додај у Morphe ред Задржите ред чекања између сесија Ред чекања се чува након поновног покретања апликације @@ -1107,7 +1109,23 @@ Second \"item\" text" Телефон Таблет Аутомобилски - "Измене укључују:\n\nРаспоред за таблет\n• Објаве заједнице су сакривене\n• Подешавање за репродукцију у фидовима није доступно\n• Дугме за ремикс и дугме за звук нису доступна у Shorts-у\n• Прекривач „Још видео записа“ можда није доступан\n• Приказује се несажета стара трака са акцијама за видео\n\nРаспоред за аутомобил\n• Фид је организован по темама и каналима\n• Подешавање за репродукцију у фидовима није доступно\n• Дугме за ремикс и дугме за звук нису доступна у Shorts-у\n• Прекривач „Још видео записа“ можда није доступан\n• Shorts се отварају у обичном плејеру\n• Приказује се несажета стара трака са акцијама за видео" + "Измене укључују: + +Изглед таблета +• Објаве заједнице су сакривене +• Подешавање пуштања у токовима није доступно +• Дугме за ремиксовање и дугме за звук нису доступна у Shorts-у +• Прекрышће „Више видео снимака“ можда неће бити доступно +• Приказана је нерастворена старa трака радњи за видео записе +• Ставке менија са стране можда неће правилно функционисати + +Изглед за аутомобиле +• Токови су организовани по темама и каналима +• Подешавање пуштања у токовима није доступно +• Дугме за ремиксовање и дугме за звук нису доступна у Shorts-у +• Прекрышће „Више видео снимака“ можда неће бити доступно +• Shorts се отварају у редовном плејеру +• Приказана је нерастворена старa трака радњи за видео записе" Укључи таблет распоред у плејеру "Враћа стари изглед плејера diff --git a/patches/src/main/resources/addresources/values-sv-rSE/youtube/strings.xml b/patches/src/main/resources/addresources/values-sv-rSE/youtube/strings.xml index f9e4f82e3b..2db004cbc2 100644 --- a/patches/src/main/resources/addresources/values-sv-rSE/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-sv-rSE/youtube/strings.xml @@ -613,6 +613,8 @@ Information: Kanske inte fungerar i livestreamar" Gör det möjligt att spara barnrelaterade videor i \'Titta senare\' från den utfällbara menyn Visa knappen Spara till Titta senare i den utfällbara menyn för Shorts Gör det möjligt att spara Shorts i \'Titta senare\' från den utfällbara menyn + Omdirigera Se senare till spellista + Titta senare Lägg till i Morphe-kön Behåll kön mellan sessioner Kön behålls efter omstart av appen @@ -1111,20 +1113,21 @@ Inställningar → Uppspelning (eller Autouppspelning) → Autouppspela nästa v Bil "Ändringar inkluderar: -Surfplattelayout -• Community-inlägg är dolda -• Inställningen för uppspelning i flöden är inte tillgänglig -• Remix-knappen och ljudknappen är inte tillgängliga i Shorts -• Överlägget 'Fler videor' kanske inte är tillgängligt -• Det gamla, ej komprimerade videoåtgärdsfältet visas +Surfplattlayout +• Inlägg i community visas inte +• Inställningen Uppspelning i flöden är inte tillgänglig +• Knapparna \"Remixa\" och \"Ljud\" är inte tillgängliga i Shorts +• Översikten \"Fler videor\" kanske inte är tillgänglig +• Åtgärdsfältet för gamla videobeskrivningar visas inte komprimerat +• Menyalternativ i flygmenyn fungerar kanske inte korrekt Billayout -• Flödet är organiserat efter ämnen och kanaler -• Inställningen för uppspelning i flöden är inte tillgänglig -• Remix-knappen och ljudknappen är inte tillgängliga i Shorts -• Överlägget 'Fler videor' kanske inte är tillgängligt -• Shorts öppnas i den vanliga spelaren -• Det gamla, ej komprimerade videoåtgärdsfältet visas" +• Flödet är indelat efter ämnen och kanaler +• Inställningen Uppspelning i flöden är inte tillgänglig +• Knapparna \"Remixa\" och \"Ljud\" är inte tillgängliga i Shorts +• Översikten \"Fler videor\" kanske inte är tillgänglig +• Shorts öppnas i vanliga spelaren +• Åtgärdsfältet för gamla videobeskrivningar visas inte komprimerat" Aktivera surfplattelayout i spelaren "Återställer den gamla spelarlayouten diff --git a/patches/src/main/resources/addresources/values-th-rTH/youtube/strings.xml b/patches/src/main/resources/addresources/values-th-rTH/youtube/strings.xml index 3302e54af9..79ff027d35 100644 --- a/patches/src/main/resources/addresources/values-th-rTH/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-th-rTH/youtube/strings.xml @@ -613,6 +613,8 @@ Second \"item\" text" อนุญาตให้บันทึกวิดีโอที่เกี่ยวข้องกับเด็กไว้ใน \'ดูภายหลัง\' จากเมนูแบบเลื่อนออก แสดงปุ่มบันทึกไปยังดูภายหลังในเมนูแบบเลื่อนออกของ Shorts อนุญาตให้บันทึก Shorts ไว้ใน \'ดูภายหลัง\' จากเมนูแบบเลื่อนออก + เปลี่ยนเส้นทาง \"ดูภายหลัง\" ไปยังเพลย์ลิสต์ + ดูต่อภายหลัง เพิ่มลงในคิว Morphe คงคิวไว้ระหว่างการใช้งาน คิวจะถูกเก็บไว้หลังจากการปิดและเปิดแอปอีกครั้ง @@ -1109,22 +1111,23 @@ Second \"item\" text" โทรศัพท์ แท็บเล็ต ยานยนต์ - "มีการเปลี่ยนแปลงดังนี้: + "การเปลี่ยนแปลงรวมถึง: -เลย์เอาต์สำหรับแท็บเล็ต -• โพสต์จากชุมชนจะถูกซ่อนไว้ -• จะไม่มีตัวเลือกการเล่นในฟีด -• จะไม่มีปุ่มรีมิกซ์และปุ่มเสียงใน Shorts -• อาจไม่สามารถใช้งานภาพซ้อนทับ 'วิดีโอเพิ่มเติม' ได้ -• แถบการทำงานของวิดีโอแบบเดิมที่ไม่ยุบจะแสดงขึ้น +เลย์เอาต์แท็บเล็ต +• โพสต์จากชุมชนจะถูกซ่อน +• การตั้งค่าการเล่นในฟีดไม่สามารถใช้งาน +• ปุ่มรีมิกซ์และปุ่มเสียงไม่สามารถใช้งานใน Shorts +• ภาพซ้อนทับ 'วิดีโอเพิ่มเติม' อาจไม่ปรากฏ +• แถบดำเนินการวิดีโอแบบเก่าที่ไม่ยุบจะแสดง +• รายการเมนูป๊อปอัพอาจทำงานไม่ถูกต้อง -เลย์เอาต์สำหรับรถยนต์ -• ฟีดจะถูกจัดระเบียบตามหัวข้อและช่อง -• จะไม่มีตัวเลือกการเล่นในฟีด -• จะไม่มีปุ่มรีมิกซ์และปุ่มเสียงใน Shorts -• อาจไม่สามารถใช้งานภาพซ้อนทับ 'วิดีโอเพิ่มเติม' ได้ +เลย์เอาต์รถยนต์ +• ฟीडจัดเรียงตามหัวข้อและช่อง +• การตั้งค่าการเล่นในฟีดไม่สามารถใช้งาน +• ปุ่มรีมิกซ์และปุ่มเสียงไม่สามารถใช้งานใน Shorts +• ภาพซ้อนทับ 'วิดีโอเพิ่มเติม' อาจไม่ปรากฏ • Shorts จะเปิดในเครื่องเล่นปกติ -• แถบการทำงานของวิดีโอแบบเดิมที่ไม่ยุบจะแสดงขึ้น" +• แถบดำเนินการวิดีโอแบบเก่าที่ไม่ยุบจะแสดง" เปิดใช้งานรูปแบบแท็บเล็ตในเครื่องเล่น "คืนรูปแบบเครื่องเล่นแบบเก่า diff --git a/patches/src/main/resources/addresources/values-tr-rTR/youtube/strings.xml b/patches/src/main/resources/addresources/values-tr-rTR/youtube/strings.xml index 3e6fdd3e98..9ed92e5a15 100644 --- a/patches/src/main/resources/addresources/values-tr-rTR/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-tr-rTR/youtube/strings.xml @@ -501,7 +501,7 @@ Soru düğmesine sahip olan bir video bir kez açıldıktan sonra bu özellik, m Video Ses Tüm seçenekler - Shorts menüsünde indiriciyi göster + Shorts menüsünde indiri göster Shorts oynatıcısına harici indirici açılır menü öğesi ekler İndir @@ -614,6 +614,8 @@ Bilgi: Canlı yayınlarda çalışmayabilir" Açılır menüden çocuklarla ilgili videoları “Daha sonra izle” bölümüne kaydetmeye olanak tanır “Daha sonra izle” butonunu shorts açılır menüsünde göster Açılır menüden shorts videolarını “Daha sonra izle” bölümüne kaydetmeye olanak tanır + \"Daha sonra izle\"yi oynatma listesine yönlendir + Daha sonra izle Morphe sırasına ekle Sırayı oturumlar arasında koru Uygulama yeniden başlatıldığında sıra korunur @@ -1110,7 +1112,7 @@ Ayarlar → Oynatma (veya Otomatik Oynatma) → Sonraki videoyu otomatik oynat"< Telefon Tablet Otomotiv - "Değişiklikler: + "Değişiklikler şunlardır: Tablet düzeni • Topluluk gönderileri gizlenir @@ -1122,8 +1124,8 @@ Tablet düzeni Otomotiv düzeni • Akış, konulara ve kanallara göre düzenlenmiştir • Akışlarda oynatma ayarı kullanılamaz -• Shorts'ta Remix düğmesi ve Ses düğmesi kullanılamaz -• \"Daha fazla video\" katmanı kullanılamayabilir +• Shorts'ta Remix düğmesi ve Ses düğmesi bulunmaz +• 'Daha fazla video' katmanı kullanılamayabilir • Shorts, normal oynatıcıda açılır • Daraltılmamış eski video eylem çubuğu gösterilir" Oynatıcıda tablet düzenini etkinleştir @@ -1637,7 +1639,7 @@ Otomatik kayıt özelliği isteğe bağlıdır; gizli modda veya hesap değişti Niconico yorumları Canlı sohbeti ve sohbet tekrarını, videonun üzerinden kayan yorumlar olarak göster - Video oynatıcıda bir ekran üstü düğmesi gösterir. Yorumları göstermek veya gizlemek için dokunun, hızlı ayarlar için basılı tutun + Video oynatıcıda bir katman düğmesi gösterir. Yorumları göstermek veya gizlemek için dokunun, hızlı ayarlar için basılı tutun Yorum yazı tipi boyutu Yorumların metin boyutu (12-40 sp) Yorum opaklığı diff --git a/patches/src/main/resources/addresources/values-uk-rUA/youtube/strings.xml b/patches/src/main/resources/addresources/values-uk-rUA/youtube/strings.xml index 8a788894fb..8537bea044 100644 --- a/patches/src/main/resources/addresources/values-uk-rUA/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-uk-rUA/youtube/strings.xml @@ -613,6 +613,8 @@ Second \"item\" text" Дозволяє зберегти відео для дітей у \"Дивитися пізніше\" зі спливного меню Показувати кнопку \"Зберегти в \'Дивитися пізніше\'\" у спливному меню Shorts Дозволяє зберегти Shorts в \"Дивитися пізніше\" зі спливного меню + Перенаправляти «Дивитися пізніше» до плейлиста + Переглянути пізніше Додати до черги Morphe Зберігати чергу між сесіями Черга зберігається після перезапуску застосунку diff --git a/patches/src/main/resources/addresources/values-vi-rVN/youtube/strings.xml b/patches/src/main/resources/addresources/values-vi-rVN/youtube/strings.xml index 3f16b0bfd2..6dab7a6f31 100644 --- a/patches/src/main/resources/addresources/values-vi-rVN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-vi-rVN/youtube/strings.xml @@ -613,6 +613,8 @@ Lưu ý: Có thể không hoạt động với video phát trực tiếp"Cho phép lưu video liên quan đến trẻ em vào \'Xem sau\' từ menu bật lên Hiển thị nút Lưu vào Xem sau trong menu bật lên của Shorts Cho phép lưu Shorts vào \'Xem sau\' từ menu bật lên + Chuyển hướng Xem sau đến danh sách phát + Xem sau Thêm vào hàng chờ Morphe Lưu hàng chờ giữa các phiên Hàng chờ vẫn được lưu sau khi tắt đi bật lại ứng dụng @@ -1111,19 +1113,20 @@ Cài đặt → Phát (hoặc Tự động phát) → tự động phát video t Màn hình ô tô "Các thay đổi bao gồm: -Bố cục máy tính bảng +Giao diện máy tính bảng • Bài đăng cộng đồng bị ẩn -• Cài đặt phát lại trong nguồn cấp dữ liệu không khả dụng -• Nút Remix và nút Âm thanh không khả dụng trong Shorts -• Lớp phủ 'Nhiều video hơn' có thể không khả dụng +• Không có tùy chọn phát trong bảng tin +• Nút Phối lại và nút Âm thanh không khả dụng trong Shorts +• Lớp phủ 'Thêm video' có thể không khả dụng • Thanh hành động video cũ không thu gọn được hiển thị +• Các mục menu flyout có thể không hoạt động chính xác -Bố cục ô tô -• Nguồn cấp dữ liệu được tổ chức theo chủ đề và kênh -• Cài đặt phát lại trong nguồn cấp dữ liệu không khả dụng -• Nút Remix và nút Âm thanh không khả dụng trong Shorts -• Lớp phủ 'Nhiều video hơn' có thể không khả dụng -• Shorts mở trong trình phát thông thường +Giao diện ô tô +• Bảng tin được tổ chức theo chủ đề và kênh +• Không có tùy chọn phát trong bảng tin +• Nút Phối lại và nút Âm thanh không khả dụng trong Shorts +• Lớp phủ 'Thêm video' có thể không khả dụng +• Shorts mở bằng trình phát thông thường • Thanh hành động video cũ không thu gọn được hiển thị" Bật bố cục máy tính bảng trong trình phát diff --git a/patches/src/main/resources/addresources/values-zh-rCN/youtube/strings.xml b/patches/src/main/resources/addresources/values-zh-rCN/youtube/strings.xml index 75e2e3d00a..fe86978d81 100644 --- a/patches/src/main/resources/addresources/values-zh-rCN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-zh-rCN/youtube/strings.xml @@ -613,6 +613,8 @@ Second \"item\" text" 允许从弹出菜单中将与儿童相关的视频保存到“稍后观看” 在短视频弹出菜单中显示“保存到稍后观看”按钮 允许从弹出菜单中将短视频保存到“稍后观看” + 将“稍后观看”重定向至播放列表 + 稍后观看 添加到 Morphe 队列 在会话之间保留队列 重启应用后队列仍会保留 @@ -1109,22 +1111,23 @@ Second \"item\" text" 手机 平板电脑 汽车 - "更改内容包括: + "更改包括: 平板电脑布局 -• 社区帖子已隐藏 -• 信息流中的播放设置不可用 -• Remix 按钮和声音按钮在 Shorts 中不可用 -• “更多视频”叠加层可能不可用 -• 显示未折叠的旧版视频操作栏 +• 隐藏社区帖子 +• 无法使用“Feed 中播放”设置 +• Shorts 中无法使用 Remix 按钮和声音按钮 +• 可能无法使用“更多视频”叠加层 +• 显示未折叠的旧视频操作栏 +• 弹出式菜单项可能无法正常工作 -汽车布局 -• 信息流按主题和频道组织 -• 信息流中的播放设置不可用 -• Remix 按钮和声音按钮在 Shorts 中不可用 -• “更多视频”叠加层可能不可用 +车载布局 +• Feed 按主题和频道组织 +• 无法使用“Feed 中播放”设置 +• Shorts 中无法使用 Remix 按钮和声音按钮 +• 可能无法使用“更多视频”叠加层 • Shorts 在常规播放器中打开 -• 显示未折叠的旧版视频操作栏" +• 显示未折叠的旧视频操作栏" 在播放器中启用平板电脑布局 "恢复旧版播放器布局 diff --git a/patches/src/main/resources/addresources/values-zh-rTW/youtube/strings.xml b/patches/src/main/resources/addresources/values-zh-rTW/youtube/strings.xml index c183b1072c..e5150b6d31 100644 --- a/patches/src/main/resources/addresources/values-zh-rTW/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-zh-rTW/youtube/strings.xml @@ -613,6 +613,8 @@ Second \"item\" text" 允許將與兒童相關的影片從彈出式選單儲存到「稍後觀看」中 在 Shorts 彈出視窗中顯示「儲存以稍後觀看」按鈕 允許將 Shorts 儲存在彈出式選單的「稍後觀看」中 + 將「稍後觀看」重新導向至播放清單 + 稍後觀看 加入 Morphe 佇列 在不同工作階段中保留佇列 重新啟動應用程式後,佇列會保留 @@ -1112,19 +1114,20 @@ Second \"item\" text" "變更包括: 平板電腦版面配置 -• 社群貼文會隱藏 -• 動態消息播放設定無法使用 -• Shorts 中無法使用 Remix 按鈕和聲音按鈕 -• 可能無法使用「更多影片」重疊畫面 -• 會顯示未收合的舊版影片動作列 +• 社群貼文已隱藏 +• 不可用「在資訊流中播放」設定 +• Shorts 中不可用「混音」按鈕和「音效」按鈕 +• 「更多影片」疊加層可能不可用 +• 顯示未摺疊的舊版影片動作欄 +• 彈出式選單項目可能無法正常運作 -汽車版面配置 -• 動態消息會依主題和頻道整理 -• 動態消息播放設定無法使用 -• Shorts 中無法使用 Remix 按鈕和聲音按鈕 -• 可能無法使用「更多影片」重疊畫面 -• Shorts 會在一般播放器中開啟 -• 會顯示未收合的舊版影片動作列" +車載版面配置 +• 資訊流依主題和頻道排列 +• 不可用「在資訊流中播放」設定 +• Shorts 中不可用「混音」按鈕和「音效」按鈕 +• 「更多影片」疊加層可能不可用 +• Shorts 將在一般播放器中開啟 +• 顯示未摺疊的舊版影片動作欄" 在播放器中啟用平板版面配置 "還原舊版播放器版面 diff --git a/patches/src/main/resources/addresources/values-zu-rZA/youtube/strings.xml b/patches/src/main/resources/addresources/values-zu-rZA/youtube/strings.xml index 1cfc65031c..161ac457d0 100644 --- a/patches/src/main/resources/addresources/values-zu-rZA/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-zu-rZA/youtube/strings.xml @@ -1105,22 +1105,6 @@ Settings $\rightarrow$ Playback (noma Autoplay) $\rightarrow$ Autoplay next vide Ucingo Ithebhulethi Ezimoto - "Izinguquko zihlanganisa: - -Ukulungiselelwa kwebulethe -• Izithamo zomphakathi zifihliwe -• Isetingu sokudlala ku-feed asitholakali -• Inkinobho ye-Remix nenkinobho ye-Sound azitholakali ku-Shorts -• I-overlay ethi 'More videos' kungenzeka ingatholakali -• I-action bar endala yevidiyo engakhiwe iyabonakala - -Ukulungiselelwa kwezimoto -• I-feed ihleleke ngokwemixhumano namachannel -• Isetingu sokudlala ku-feed asitholakali -• Inkinobho ye-Remix nenkinobho ye-Sound azitholakali ku-Shorts -• I-overlay ethi 'More videos' kungenzeka ingatholakali -• I-Shorts ivuleka kumdlali ovamile -• I-action bar endala yevidiyo engakhiwe iyabonakala" Vumela ukulungiseka kwe-tablet kudlali "Buyisela ukulungiselelwa kwedlali yakudala From 03ef708f724c9af7f784b75ead9da58b8cede5f4 Mon Sep 17 00:00:00 2001 From: 0xrxL <127248639+0xrxL@users.noreply.github.com> Date: Tue, 27 Oct 2026 13:44:15 +0100 Subject: [PATCH 103/189] fix(YouTube): Feed thumbnail badges are missing after scrolling up, show a loading spinner until they are loaded --- .../patches/FeedEngagementCountsPatch.java | 51 +++++++-- .../youtube/patches/FeedViewCountPatch.java | 57 +++++++--- .../patches/utils/FeedVideoCardsPatch.java | 100 ++++++++++++++++-- 3 files changed, 176 insertions(+), 32 deletions(-) diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FeedEngagementCountsPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FeedEngagementCountsPatch.java index 7d470d3d84..949fa39c4b 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FeedEngagementCountsPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FeedEngagementCountsPatch.java @@ -9,7 +9,6 @@ import android.animation.ValueAnimator; import android.graphics.Canvas; -import android.graphics.Color; import android.graphics.ColorFilter; import android.graphics.Paint; import android.graphics.PixelFormat; @@ -46,8 +45,8 @@ * Part of the Return YouTube Dislike patch. *

* Feed videos do not include their like count, so it's fetched from the Return YouTube Dislike API, - * with the same cache as the player. A video is fetched when its card is laid out, and the counts - * fade in once the fetch completes. + * with the same cache as the player. A video is fetched when its card is laid out. Until the fetch + * completes, the badge shows a loading spinner, and the counts fade in once they are fetched. * The counts are not clickable, so tapping them opens the video. */ @SuppressWarnings("unused") @@ -79,6 +78,8 @@ public final class FeedEngagementCountsPatch { private static final int GAP_DP = 3; private static final int BAR_HEIGHT_DP = 3; private static final int MIN_WIDTH_DP = 34; + /** About as tall as the like count. */ + private static final int SPINNER_DP = 12; private static final float COUNT_TEXT_SP = 11f; private static final float RATIO_TEXT_SP = 10f; @@ -164,6 +165,7 @@ private static final class CountsBadge extends Drawable implements View.OnAttach private final Paint countPaint = new Paint(Paint.ANTI_ALIAS_FLAG); private final Paint ratioPaint = new Paint(Paint.ANTI_ALIAS_FLAG); private final Paint barPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + private final Paint spinnerPaint; private final RectF rect = new RectF(); private final float density; private final WeakReference hostRef; @@ -205,6 +207,8 @@ private static final class CountsBadge extends Drawable implements View.OnAttach ratioPaint.setColor(TEXT_COLOR); ratioPaint.setTextAlign(Paint.Align.CENTER); ratioPaint.setTextSize(RATIO_TEXT_SP * scaledDensity); + + spinnerPaint = FeedVideoCardsPatch.createSpinnerPaint(density, TEXT_COLOR); } private int px(float dp) { @@ -285,8 +289,9 @@ private void fetch(ComponentHost host, String fetchVideoId) { if (data != null) { setData(data); startFadeIn(); - place(host); } + // The counts replace the loading spinner, or the badge is removed if the fetch failed. + place(host); } catch (Exception ex) { Logger.printException(() -> "Feed counts update failure", ex); } @@ -335,12 +340,20 @@ private boolean hasContent() { return countText != null || ratioText != null; } + /** + * @return If the counts are being fetched, and the loading spinner is shown until then. + */ + private boolean isLoading() { + return !hasContent() && (fetching || pendingFetch != null); + } + /** * Places the badge on the right side of the thumbnail, just above the duration. */ void place(ComponentHost host) { - if (!hasContent()) { - // Nothing to show yet, or the fetch failed. A pending fetch is kept. + final boolean loading = isLoading(); + if (!hasContent() && !loading) { + // The fetch failed, or it waits for the next layout of the card. removeFromOverlay(host); return; } @@ -352,6 +365,10 @@ void place(ComponentHost host) { float contentWidth = px(ICON_DP); float contentHeight = 0; + if (loading) { + // The like icon, if the like count is shown, and the spinner below it. + contentHeight = (LIKE_COUNT ? px(ICON_DP) + gap : 0) + px(SPINNER_DP); + } if (countText != null) { contentWidth = Math.max(contentWidth, countPaint.measureText(countText)); contentHeight += px(ICON_DP) + gap + (countMetrics.descent - countMetrics.ascent); @@ -440,7 +457,8 @@ private void removeFromOverlay(ComponentHost host) { @Override public void draw(@NonNull Canvas canvas) { - if (!confirmed || !hasContent()) { + final boolean loading = isLoading(); + if (!confirmed || (!hasContent() && !loading)) { return; } ComponentHost host = hostRef.get(); @@ -450,7 +468,7 @@ public void draw(@NonNull Canvas canvas) { } Rect bounds = getBounds(); final float radius = px(CORNER_RADIUS_DP); - backgroundPaint.setAlpha(Color.alpha(BACKGROUND_COLOR) * alpha / 255); + // The background was shown with the loading spinner, so only the counts fade in. countPaint.setAlpha(alpha); ratioPaint.setAlpha(alpha); rect.set(bounds); @@ -461,17 +479,30 @@ public void draw(@NonNull Canvas canvas) { final float centerX = bounds.exactCenterX(); float y = bounds.top + padding; - if (countText != null) { + if (countText != null || (loading && LIKE_COUNT)) { final int iconSize = px(ICON_DP); Drawable icon = getLikeIcon(); if (icon != null) { final int left = Math.round(centerX - iconSize / 2f); icon.setBounds(left, Math.round(y), left + iconSize, Math.round(y) + iconSize); // Shared by all badges, so the alpha is set for each draw. - icon.setAlpha(alpha); + // Shown with the loading spinner, so it does not fade in. + icon.setAlpha(255); icon.draw(canvas); } y += iconSize + gap; + } + + if (loading) { + final float spinnerSize = px(SPINNER_DP); + rect.set(centerX - spinnerSize / 2, y, centerX + spinnerSize / 2, y + spinnerSize); + FeedVideoCardsPatch.drawSpinner(canvas, rect, spinnerPaint); + // Drawn again, so the spinner turns. + invalidateSelf(); + return; + } + + if (countText != null) { Paint.FontMetrics metrics = countPaint.getFontMetrics(); canvas.drawText(countText, centerX, y - metrics.ascent, countPaint); y += metrics.descent - metrics.ascent; diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FeedViewCountPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FeedViewCountPatch.java index e4d454d8c0..9b2bfc6645 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FeedViewCountPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FeedViewCountPatch.java @@ -61,7 +61,8 @@ * is attached to it as a span, so the card that draws the text knows its view count. * The span is copied with the text, so it is kept when the Hide view count option * of the Hide layout components patch removes the view count from below the title. - * No request is made, and the count is the same that YouTube shows. + * No request is made, and the count is the same that YouTube shows. Until the text is drawn, + * such as while only a part of the card is shown, the badge shows a loading spinner. *

* The same text is used by other lists too, such as the videos of a playlist, where no badge is * shown. So the view count is removed from below the title only for the texts of the components @@ -129,6 +130,8 @@ public final class FeedViewCountPatch { private static final float ICON_SIZE_OF_TEXT = 0.72f; /** Width of the play icon, relative to its height. */ private static final float ICON_WIDTH_OF_HEIGHT = 0.88f; + /** Size of the loading spinner, relative to the text size. */ + private static final float SPINNER_SIZE_OF_TEXT = 0.85f; private static final int BACKGROUND_COLOR = 0xA6000000; private static final int TEXT_COLOR = 0xFFFFFFFF; @@ -895,6 +898,8 @@ public static void onCardLaidOut(ComponentHost host, @Nullable String videoId) { ViewCountSpan span = null; Rect duration = null; Rect thumbnail = null; + // If the view count is not found yet, but the card is checked again. + boolean loading = false; // Videos of shelves are not known, but the view count is shown by the card itself, // so it is shown on any card shaped like a video with a duration. // Cards of a grid, such as in landscape, are narrower than the screen and their video is not known. @@ -929,7 +934,7 @@ public static void onCardLaidOut(ComponentHost host, @Nullable String videoId) { final String id = videoId == null ? "unknown" : videoId; if (viewCount == null) { // The texts can be drawn after the layout of the card. - FeedVideoCardsPatch.scheduleRetry(host); + loading = FeedVideoCardsPatch.scheduleRetry(host); logTexts(id, info.texts); } else if (duration == null) { final Rect found = thumbnail; @@ -939,7 +944,7 @@ public static void onCardLaidOut(ComponentHost host, @Nullable String videoId) { } ViewCountBadge badge = badges.get(host); - if (viewCount == null) { + if (viewCount == null && !loading) { if (badge != null) { badge.detach(host); badges.remove(host); @@ -968,16 +973,20 @@ private static final class ViewCountBadge extends Drawable { private final Paint backgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG); private final Paint textPaint = new Paint(Paint.ANTI_ALIAS_FLAG); private final Paint iconPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + private final Paint spinnerPaint; private final Path iconPath = new Path(); private final RectF rect = new RectF(); private final float density; private final float maxTextSize; private final WeakReference hostRef; + /** The view count, or null while it's loading. */ @Nullable private String viewCount; private boolean added; private int alpha = 255; + /** If the background fades in too, as it was not shown with the loading spinner. */ + private boolean backgroundFades; @Nullable private ValueAnimator fadeIn; @@ -997,6 +1006,8 @@ private static final class ViewCountBadge extends Drawable { iconPaint.setStrokeJoin(Paint.Join.ROUND); iconPaint.setStrokeCap(Paint.Cap.ROUND); iconPaint.setStrokeWidth(ICON_STROKE_DP * density); + + spinnerPaint = FeedVideoCardsPatch.createSpinnerPaint(density, TEXT_COLOR); } private int px(float dp) { @@ -1004,13 +1015,16 @@ private int px(float dp) { } /** - * @param thumbnail The thumbnail, or null if not found. - * Then a 16:9 thumbnail as wide as the card is used. + * @param newViewCount The view count, or null to show the loading spinner. + * @param thumbnail The thumbnail, or null if not found. + * Then a 16:9 thumbnail as wide as the card is used. */ - void bind(ComponentHost host, String newViewCount, @Nullable Rect duration, @Nullable Rect thumbnail) { + void bind(ComponentHost host, @Nullable String newViewCount, @Nullable Rect duration, @Nullable Rect thumbnail) { // Fades in when shown, or when the card shows another video. - // Not when the card is only laid out again. - final boolean changed = !newViewCount.equals(viewCount); + // Not when the card is only laid out again, and not the loading spinner. + final boolean changed = newViewCount != null && !newViewCount.equals(viewCount); + // The background of the loading spinner stays. + backgroundFades = !(added && viewCount == null); viewCount = newViewCount; // Cards of some layouts show the thumbnail inset from their edges, @@ -1037,8 +1051,10 @@ void bind(ComponentHost host, String newViewCount, @Nullable Rect duration, @Nul textPaint.setTextSize(Math.min(maxTextSize, height * TEXT_SIZE_OF_HEIGHT)); final float iconSize = textPaint.getTextSize() * ICON_SIZE_OF_TEXT; - final int width = Math.round(px(PADDING_DP) * 2 + iconSize + px(GAP_DP) - + textPaint.measureText(viewCount)); + final float contentWidth = viewCount == null + ? textPaint.getTextSize() * SPINNER_SIZE_OF_TEXT + : textPaint.measureText(viewCount); + final int width = Math.round(px(PADDING_DP) * 2 + iconSize + px(GAP_DP) + contentWidth); setBounds(left, top, left + width, bottom); if (!added) { @@ -1048,6 +1064,9 @@ void bind(ComponentHost host, String newViewCount, @Nullable Rect duration, @Nul if (changed) { startFadeIn(); } else { + if (viewCount == null) { + endFadeIn(); + } invalidateSelf(); } } @@ -1083,17 +1102,15 @@ void detach(ComponentHost host) { @Override public void draw(@NonNull Canvas canvas) { - if (viewCount == null) { - return; - } ComponentHost host = hostRef.get(); if (host != null && FeedVideoCardsPatch.isCardPlayingInline(host)) { // The inline player shows its own controls over the thumbnail. return; } - backgroundPaint.setAlpha(Color.alpha(BACKGROUND_COLOR) * alpha / 255); + backgroundPaint.setAlpha(Color.alpha(BACKGROUND_COLOR) * (backgroundFades ? alpha : 255) / 255); textPaint.setAlpha(alpha); - iconPaint.setAlpha(alpha); + // The icon is shown with the loading spinner too, so it fades in only with the background. + iconPaint.setAlpha(backgroundFades ? alpha : 255); Rect bounds = getBounds(); final float radius = px(RADIUS_DP); @@ -1112,8 +1129,16 @@ public void draw(@NonNull Canvas canvas) { iconPath.close(); canvas.drawPath(iconPath, iconPaint); - Paint.FontMetrics metrics = textPaint.getFontMetrics(); final float textX = bounds.left + px(PADDING_DP) + iconSize + px(GAP_DP); + if (viewCount == null) { + final float spinnerSize = textPaint.getTextSize() * SPINNER_SIZE_OF_TEXT; + rect.set(textX, centerY - spinnerSize / 2, textX + spinnerSize, centerY + spinnerSize / 2); + FeedVideoCardsPatch.drawSpinner(canvas, rect, spinnerPaint); + // Drawn again, so the spinner turns. + invalidateSelf(); + return; + } + Paint.FontMetrics metrics = textPaint.getFontMetrics(); final float baseline = centerY - (metrics.ascent + metrics.descent) / 2; canvas.drawText(viewCount, textX, baseline, textPaint); } diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/FeedVideoCardsPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/FeedVideoCardsPatch.java index 0eecb3b75e..33319b05bb 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/FeedVideoCardsPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/FeedVideoCardsPatch.java @@ -10,9 +10,15 @@ import android.app.Activity; import android.content.Context; import android.content.ContextWrapper; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.RectF; +import android.os.SystemClock; import android.view.View; import android.view.ViewGroup; import android.view.ViewParent; +import android.view.ViewTreeObserver; import android.widget.TextView; import androidx.annotation.GuardedBy; @@ -28,6 +34,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.WeakHashMap; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -88,6 +95,13 @@ public final class FeedVideoCardsPatch { */ private static final int MAX_RETRIES = 3; private static final long RETRY_DELAY_MS = 500; + /** Time between checks of the cards that are partly shown, while the list scrolls. */ + private static final long SCROLL_CHECK_DELAY_MS = 50; + + /** Duration of a turn of the loading spinner of the badges. */ + private static final long SPINNER_TURN_MS = 1000; + private static final float SPINNER_SWEEP_DEGREES = 270; + private static final float SPINNER_STROKE_DP = 1.6f; /** * Result of {@link #findVideoId(ComponentHost)} for a video card whose texts are not drawn yet, @@ -160,6 +174,29 @@ public static boolean isPlaylistCard(ComponentHost host) { return PLAYLIST.equals(cardVideoIds.get(host)); } + /** + * @return The paint of the loading spinner of a badge, shown until what the badge shows is found. + */ + public static Paint createSpinnerPaint(float density, int color) { + Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); + paint.setColor(color); + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeCap(Paint.Cap.ROUND); + paint.setStrokeWidth(SPINNER_STROKE_DP * density); + return paint; + } + + /** + * Draws the loading spinner of a badge, an arc that turns with the time. + * The badge draws itself again after it, so the arc keeps turning while it's shown. + */ + public static void drawSpinner(Canvas canvas, RectF oval, Paint paint) { + final float inset = paint.getStrokeWidth() / 2; + oval.inset(inset, inset); + final float start = (SystemClock.uptimeMillis() % SPINNER_TURN_MS) * 360f / SPINNER_TURN_MS; + canvas.drawArc(oval, start, SPINNER_SWEEP_DEGREES, false, paint); + } + private static void redrawCards() { Utils.runOnMainThread(() -> { try { @@ -625,6 +662,33 @@ private static ComponentHost findRootHost(ComponentHost host) { /** Root host -> number of checks since its last layout. Used only on the main thread. */ private static final Map retries = new WeakHashMap<>(); + /** + * Root hosts that are checked again when the list scrolls. Litho mounts only the part of a card + * that is shown, so what is drawn on it can be found once more of it is shown. + * Used only on the main thread. + */ + private static final Set scrollChecks = Collections.newSetFromMap(new WeakHashMap<>()); + /** Tree observers of the windows the scroll listener was added to. Used only on the main thread. */ + private static final Map scrollObservers = new WeakHashMap<>(); + private static boolean scrollCheckScheduled; + private static final Rect visibleRect = new Rect(); + + private static final Runnable scrollCheckRunnable = () -> { + scrollCheckScheduled = false; + for (ComponentHost root : scrollChecks.toArray(new ComponentHost[0])) { + if (root != null) { + scheduleUpdate(root); + } + } + }; + + private static final ViewTreeObserver.OnScrollChangedListener scrollListener = () -> { + if (!scrollCheckScheduled && !scrollChecks.isEmpty()) { + scrollCheckScheduled = true; + Utils.runOnMainThreadDelayed(scrollCheckRunnable, SCROLL_CHECK_DELAY_MS); + } + }; + private static void scheduleUpdate(ComponentHost root) { rootsToUpdate.put(root, Boolean.TRUE); if (!rootsUpdateScheduled) { @@ -634,13 +698,28 @@ private static void scheduleUpdate(ComponentHost root) { } /** - * Checks the card again a little later, such as when what is drawn on it was not found yet. + * Checks the card again a little later, such as when what is drawn on it was not found yet, + * and when the list scrolls while the card is partly shown. + * Must be called on the main thread. + * + * @return If the card is checked again. */ - public static void scheduleRetry(ComponentHost root) { + public static boolean scheduleRetry(ComponentHost root) { + // Only the part of the card on the screen is mounted. + final boolean partlyShown = root.isAttachedToWindow() + && (!root.getLocalVisibleRect(visibleRect) || visibleRect.height() < root.getHeight()); + if (partlyShown) { + scrollChecks.add(root); + ViewTreeObserver observer = root.getViewTreeObserver(); + if (observer.isAlive() && scrollObservers.put(observer, Boolean.TRUE) == null) { + observer.addOnScrollChangedListener(scrollListener); + } + } + Integer count = retries.get(root); final int attempt = count == null ? 0 : count; if (attempt >= MAX_RETRIES) { - return; + return partlyShown; } retries.put(root, attempt + 1); root.postDelayed(() -> { @@ -648,6 +727,7 @@ public static void scheduleRetry(ComponentHost root) { scheduleUpdate(root); } }, RETRY_DELAY_MS); + return true; } private static final Runnable updateRootsRunnable = () -> { @@ -656,13 +736,21 @@ public static void scheduleRetry(ComponentHost root) { ComponentHost[] roots = rootsToUpdate.keySet().toArray(new ComponentHost[0]); rootsToUpdate.clear(); for (ComponentHost root : roots) { - if (root == null || !root.isAttachedToWindow() - || root.getParent() instanceof ComponentHost) { + if (root == null) { + continue; + } + // Added again below if the card is still checked again. + scrollChecks.remove(root); + if (!root.isAttachedToWindow() || root.getParent() instanceof ComponentHost) { continue; } String videoId = findVideoId(root); if (NOT_READY.equals(videoId) - || (videoId == null && isVideoShaped(root) && !hasHorizontalList(root))) { + || (videoId == null && isVideoShaped(root) && !hasHorizontalList(root)) + // A card that enters from the top of the list shows its texts before the + // duration of its thumbnail, so it looks like a mix until its top is shown. + || (PLAYLIST.equals(videoId) + && (!root.getLocalVisibleRect(visibleRect) || visibleRect.top > 0))) { scheduleRetry(root); } else { retries.remove(root); From fbb095dbd6b9bf7410e4cff38b664d2fb3048aeb Mon Sep 17 00:00:00 2001 From: MarcaDian <152095496+MarcaDian@users.noreply.github.com> Date: Tue, 27 Oct 2026 15:31:29 +0200 Subject: [PATCH 104/189] feat: Validate custom header and icon folders when the option is set --- .../branding/BaseCustomBrandingPatch.kt | 118 ++++-------------- .../branding/header/BaseChangeHeaderPatch.kt | 54 ++++---- 2 files changed, 50 insertions(+), 122 deletions(-) diff --git a/patches/src/main/kotlin/app/morphe/patches/shared/layout/branding/BaseCustomBrandingPatch.kt b/patches/src/main/kotlin/app/morphe/patches/shared/layout/branding/BaseCustomBrandingPatch.kt index 41f749849b..bb83987bb1 100644 --- a/patches/src/main/kotlin/app/morphe/patches/shared/layout/branding/BaseCustomBrandingPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/shared/layout/branding/BaseCustomBrandingPatch.kt @@ -109,6 +109,27 @@ private val notificationIconPngDirectories = mapOf( "drawable-xxxhdpi" to "96x96 px", ) +/** + * The icon files of the custom icon folder at [path], each in the resource folder it goes to, or + * null when the folder has none or holds only part of the adaptive icon in a density. + */ +private fun customIconFiles(path: String): List? { + val folder = File(path.trim()) + if (!folder.isDirectory) return null + + val adaptiveIcons = mipmapDirectories.keys.flatMap { density -> + val layers = USER_CUSTOM_ADAPTIVE_FILE_NAMES.map { folder.resolve("$density/$it") }.filter(File::isFile) + if (layers.isNotEmpty() && layers.size != USER_CUSTOM_ADAPTIVE_FILE_NAMES.size) return null + layers + } + val optionalIcons = listOf( + "drawable/$USER_CUSTOM_MONOCHROME_FILE_NAME", + "drawable/$USER_CUSTOM_NOTIFICATION_ICON_XML_FILE_NAME" + ) + notificationIconPngDirectories.keys.map { "$it/$USER_CUSTOM_NOTIFICATION_ICON_PNG_FILE_NAME" } + + return (adaptiveIcons + optionalIcons.map(folder::resolve).filter(File::isFile)).ifEmpty { null } +} + internal const val EXTENSION_CLASS = "Lapp/morphe/extension/shared/patches/CustomBrandingPatch;" // Must be named in the 'app.morphe' namespace, otherwise the log is not published. @@ -174,7 +195,9 @@ internal fun baseCustomBrandingPatch( - PNG raster images placed in the matching 'drawable-' folders: ${notificationIconPngDirectories.map { (dpi, dim) -> "- $dpi/$USER_CUSTOM_NOTIFICATION_ICON_PNG_FILE_NAME ($dim)" }.joinToString("\n")} """ - ) + ) { + it == null || customIconFiles(it) != null + } val appIconStyle by stringOption( key = "appIcon", @@ -568,97 +591,10 @@ internal fun baseCustomBrandingPatch( // Copy the user provided icon files here and not in the finalize block, because a mounted // installation uses them to replace the icon of the app and that is done while finalizing. if (useCustomIcon) { - // Copy user provided files - val iconPathFile = File(customIcon!!.trim()) - - if (!iconPathFile.exists()) { - throw PatchException( - "The custom icon path cannot be found: " + iconPathFile.absolutePath - ) - } - - if (!iconPathFile.isDirectory) { - throw PatchException( - "The custom icon path must be a folder: " + iconPathFile.absolutePath - ) - } - + // Reading the option already checked the folder, see customIconFiles val resourceDirectory = get("res") - var copiedFiles = false - - // For each source folder, copy the files to the target resource directories. - iconPathFile.listFiles { - file -> file.isDirectory && file.name in mipmapDirectories - }!!.forEach { dpiSourceFolder -> - val targetDpiFolder = resourceDirectory.resolve(dpiSourceFolder.name) - if (!targetDpiFolder.exists()) { - // Should never happen. - throw IllegalStateException("Resource not found: $dpiSourceFolder") - } - - val customFiles = dpiSourceFolder.listFiles { file -> - file.isFile && file.name in USER_CUSTOM_ADAPTIVE_FILE_NAMES - }!! - - if (customFiles.isNotEmpty() && customFiles.size != USER_CUSTOM_ADAPTIVE_FILE_NAMES.size) { - throw PatchException("Must include all required icon files " + - "but only found " + customFiles.map { it.name }) - } - - customFiles.forEach { imgSourceFile -> - val imgTargetFile = targetDpiFolder.resolve(imgSourceFile.name) - imgSourceFile.copyTo(target = imgTargetFile, overwrite = true) - - copiedFiles = true - } - } - - // Copy monochrome icon if provided. - val drawableSourceFolder = iconPathFile.resolve("drawable") - if (drawableSourceFolder.exists()) { - val monochromeFile = drawableSourceFolder.resolve(USER_CUSTOM_MONOCHROME_FILE_NAME) - if (monochromeFile.exists()) { - monochromeFile.copyTo( - target = resourceDirectory.resolve("drawable/$USER_CUSTOM_MONOCHROME_FILE_NAME"), - overwrite = true - ) - copiedFiles = true - } - - // XML vector notification icon. - val notificationXml = drawableSourceFolder.resolve(USER_CUSTOM_NOTIFICATION_ICON_XML_FILE_NAME) - if (notificationXml.exists()) { - notificationXml.copyTo( - target = resourceDirectory.resolve("drawable/$USER_CUSTOM_NOTIFICATION_ICON_XML_FILE_NAME"), - overwrite = true - ) - copiedFiles = true - } - } - - // PNG notification icons. - // If any DPI folder is present, copy what's available. - iconPathFile.listFiles { file -> - file.isDirectory && file.name in notificationIconPngDirectories - }!!.forEach { dpiSourceFolder -> - val pngFile = dpiSourceFolder.resolve(USER_CUSTOM_NOTIFICATION_ICON_PNG_FILE_NAME) - if (pngFile.exists()) { - val targetFolder = resourceDirectory.resolve(dpiSourceFolder.name) - if (!targetFolder.exists()) { - throw IllegalStateException("Resource directory not found: ${dpiSourceFolder.name}") - } - pngFile.copyTo( - target = targetFolder.resolve(USER_CUSTOM_NOTIFICATION_ICON_PNG_FILE_NAME), - overwrite = true - ) - copiedFiles = true - } - } - - if (!copiedFiles) { - throw PatchException("Expected to find directories and files: " - + USER_CUSTOM_ADAPTIVE_FILE_NAMES.contentToString() - + "\nBut none were found in the provided option file path: " + iconPathFile.absolutePath) + customIconFiles(customIcon!!)!!.forEach { file -> + file.copyTo(resourceDirectory.resolve("${file.parentFile.name}/${file.name}"), overwrite = true) } } diff --git a/patches/src/main/kotlin/app/morphe/patches/shared/layout/branding/header/BaseChangeHeaderPatch.kt b/patches/src/main/kotlin/app/morphe/patches/shared/layout/branding/header/BaseChangeHeaderPatch.kt index ff73b4c0b2..e1cfa62a53 100644 --- a/patches/src/main/kotlin/app/morphe/patches/shared/layout/branding/header/BaseChangeHeaderPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/shared/layout/branding/header/BaseChangeHeaderPatch.kt @@ -10,7 +10,6 @@ package app.morphe.patches.shared.layout.branding.header -import app.morphe.patcher.patch.PatchException import app.morphe.patcher.patch.ResourcePatch import app.morphe.patcher.patch.ResourcePatchBuilder import app.morphe.patcher.patch.ResourcePatchContext @@ -55,7 +54,9 @@ internal fun baseChangeHeaderPatch( Required dimensions: ${targetResourceDirectoryNames.map { (dpi, dim) -> "- $dpi: $dim" }.joinToString("\n")} """ - ) + ) { + it == null || customHeaderImages(it, targetResourceDirectoryNames.keys, customHeaderResourceFileNames) != null + } block() @@ -86,37 +87,28 @@ internal fun baseChangeHeaderPatch( copyResources("change-header", ResourceGroup(dpi, *customHeaderResourceFileNames)) } - custom?.trim()?.let { customPath -> - val customDir = File(customPath) - if (!customDir.exists()) - throw PatchException("Custom header path not found: ${customDir.absolutePath}") - - if (!customDir.isDirectory) - throw PatchException("Custom header path must be a directory.") - - var copied = false - customDir.listFiles { file -> file.isDirectory && file.name in targetResourceDirectoryNames } - ?.forEach { dpiFolder -> - val targetFolder = get("res").resolve(dpiFolder.name) - targetFolder.mkdirs() - - val files = dpiFolder.listFiles { file -> - file.isFile && file.name in customHeaderResourceFileNames - } ?: emptyArray() - - if (files.size != customHeaderResourceFileNames.size) - throw PatchException("Missing required images in ${dpiFolder.name}. Expected: ${customHeaderResourceFileNames.joinToString(", ")}") - - files.forEach { source -> - source.copyTo(targetFolder.resolve(source.name), overwrite = true) - copied = true - } - } - - if (!copied) - throw PatchException("No valid DPI folders found in: ${customDir.absolutePath}") + // Reading the option already checked the folder, see customHeaderImages + custom?.let { path -> + val resourceDirectory = get("res") + customHeaderImages(path, targetResourceDirectoryNames.keys, customHeaderResourceFileNames)!!.forEach { image -> + image.copyTo(resourceDirectory.resolve("${image.parentFile.name}/${image.name}"), overwrite = true) + } } executeBlock(customHeaderResourceFileNames) } +} + +/** + * The images of the custom header folder at [path], each in the folder of its density, or null + * when the folder has none of [densities] or leaves out one of [fileNames] in a density it has. + */ +private fun customHeaderImages(path: String, densities: Set, fileNames: Array): List? { + val densityFolders = File(path.trim()).listFiles { file -> file.isDirectory && file.name in densities } + ?.takeIf { it.isNotEmpty() } + ?: return null + + return densityFolders.flatMap { folder -> + fileNames.map(folder::resolve).takeIf { images -> images.all(File::isFile) } ?: return null + } } \ No newline at end of file From 35e412375a1de62b500e2c9ca70c0ca5a18fb793 Mon Sep 17 00:00:00 2001 From: 0xrxL <127248639+0xrxL@users.noreply.github.com> Date: Tue, 27 Oct 2026 15:01:30 +0100 Subject: [PATCH 105/189] fix(YouTube): Watch page feed lags when scrolling --- .../misc/playercontrols/Fingerprints.kt | 9 ++++-- .../LegacyPlayerControlsPatch.kt | 29 +++++++++++-------- 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/misc/playercontrols/Fingerprints.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/playercontrols/Fingerprints.kt index 438199f5e6..6f98f7c8b0 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/misc/playercontrols/Fingerprints.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/playercontrols/Fingerprints.kt @@ -136,10 +136,13 @@ internal object PlayerCommentTeaserFeatureFlagFingerprint : Fingerprint( ) ) - -internal object RecycleViewScrollingFlagFingerprint : Fingerprint( +/** + * Watch feed resize: scrolling the watch next feed resizes the player, + * and every scroll event relayouts the whole watch page. + */ +internal object WatchFeedResizeFeatureFlagFingerprint : Fingerprint( filters = listOf( - literal(45763727) + literal(45763727L) // 21.11+ ) ) diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/misc/playercontrols/LegacyPlayerControlsPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/playercontrols/LegacyPlayerControlsPatch.kt index 7f61fb28c2..4ed9c4fcda 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/misc/playercontrols/LegacyPlayerControlsPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/playercontrols/LegacyPlayerControlsPatch.kt @@ -44,6 +44,7 @@ import app.morphe.patches.youtube.misc.playservice.is_20_40_or_greater import app.morphe.patches.youtube.misc.playservice.is_21_04_or_greater import app.morphe.patches.youtube.misc.playservice.is_21_05_or_greater import app.morphe.patches.youtube.misc.playservice.is_21_08_or_greater +import app.morphe.patches.youtube.misc.playservice.is_21_11_or_greater import app.morphe.patches.youtube.misc.playservice.is_21_15_or_greater import app.morphe.patches.youtube.misc.playservice.is_21_29_or_greater import app.morphe.patches.youtube.misc.playservice.is_21_30_or_greater @@ -423,18 +424,22 @@ val legacyPlayerControlsPatch = bytecodePatch( } // Override flags that interfere with old player icons override. - arrayOf( - PlayerCommentTeaserFeatureFlagFingerprint to is_21_15_or_greater, - RecycleViewScrollingFlagFingerprint to is_21_15_or_greater - ).forEach { (fingerprint, applyChanges) -> - if (applyChanges) { - fingerprint.matchAll().forEach { - it.method.insertLiteralOverride( - it.instructionMatches.first().index, - "$EXTENSION_CLASS->" + - "allowModernPlayerLayoutFlags(Z)Z" - ) - } + if (is_21_15_or_greater) { + PlayerCommentTeaserFeatureFlagFingerprint.matchAll().forEach { + it.method.insertLiteralOverride( + it.instructionMatches.first().index, + "$EXTENSION_CLASS->" + + "allowModernPlayerLayoutFlags(Z)Z" + ) + } + } + + // Watch feed resize A/B test: every scroll event of the watch next feed + // resizes the player and relayouts the watch page, and scrolling lags. + // It also interferes with the old player icons, so always turn it off. + if (is_21_11_or_greater) { + WatchFeedResizeFeatureFlagFingerprint.matchAllMethodIndicesForEach { index -> + insertLiteralOverride(index, false) } } From 0e058e8d184650326d81a243d083ae9bd6f3f942 Mon Sep 17 00:00:00 2001 From: 0xrxL <127248639+0xrxL@users.noreply.github.com> Date: Tue, 27 Oct 2026 15:45:27 +0100 Subject: [PATCH 106/189] fix(YouTube - Feed video actions): Watch later button opens the channel in right to left languages --- .../patches/FeedVideoActionsPatch.java | 41 ++++++++++++++++--- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FeedVideoActionsPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FeedVideoActionsPatch.java index 5fb1b5d35b..ff99f410fa 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FeedVideoActionsPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FeedVideoActionsPatch.java @@ -44,6 +44,7 @@ import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.WeakHashMap; import java.util.regex.Pattern; @@ -61,7 +62,8 @@ *

* Feed videos are drawn by Litho, so a video has no view of its own. The root * {@link ComponentHost} of a full width video is recognized by its shape: - * a 16:9 thumbnail at the top, and the menu button below it on the right. + * a 16:9 thumbnail at the top, and the menu button below it at the end side + * (on the right, or on the left in right to left languages). *

    *
  • Tap and hold anywhere on the video: clicks the menu button of the video.
  • *
  • Watch later button: drawn on the overlay of the root host. Tapping it opens the menu @@ -97,7 +99,10 @@ public final class FeedVideoActionsPatch { private static final int THUMBNAIL_OVERLAP_DP = 8; private static final int MENU_BUTTON_MIN_DP = 20; private static final int MENU_BUTTON_MAX_DP = 72; - /** The menu button is in the right part of the video. */ + /** + * The menu button is in the end part of the video: the right part, or the left part + * in right to left languages, where the channel avatar is in the right part. + */ private static final float MENU_BUTTON_MIN_CENTER_X = 0.8f; private static final int BADGE_SIZE_DP = 34; @@ -124,6 +129,7 @@ public final class FeedVideoActionsPatch { /** A short badge in capital letters, such as the live badge. */ private static final Pattern BADGE_PATTERN = Pattern.compile("^\\p{Lu}{2,12}$"); private static final Pattern NOT_LETTERS_PATTERN = Pattern.compile("[^\\p{L}]+"); + private static final Pattern VIDEO_ID_PATTERN = Pattern.compile("^[\\w-]{11}$"); /** Time the menu click has to open the menu, before a Watch later tap is forgotten. */ private static final long PENDING_WATCH_LATER_TIMEOUT_MS = 2000; @@ -176,6 +182,8 @@ public final class FeedVideoActionsPatch { /** If the pending menu item is Save to playlist instead of Watch later. */ private static boolean pendingSaveToPlaylist; private static String pendingVideoId = ""; + /** Video of the tapped card, used if the menu does not give the video id. */ + private static String pendingCardVideoId = ""; /** Menu that is hidden until its Watch later item is clicked. */ private static WeakReference pendingFlyoutRef = new WeakReference<>(null); private static boolean pendingItemClicked; @@ -323,7 +331,7 @@ private static boolean isVideoShaped(ComponentHost host) { /** * @return If the view is where the menu button of a video is: - * small, below the thumbnail and at the right side. + * small, below the thumbnail and at the end side. */ private static boolean isMenuButtonShaped(ComponentHost root, View view, Rect rect) { rect.set(0, 0, view.getWidth(), view.getHeight()); @@ -332,9 +340,13 @@ private static boolean isMenuButtonShaped(ComponentHost root, View view, Rect re final int min = dp(root, MENU_BUTTON_MIN_DP); final int max = dp(root, MENU_BUTTON_MAX_DP); final int width = root.getWidth(); + // The channel avatar has the same size, at the start side. + final boolean atEndSide = root.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL + ? rect.centerX() <= width * (1 - MENU_BUTTON_MIN_CENTER_X) + : rect.centerX() >= width * MENU_BUTTON_MIN_CENTER_X; return rect.width() >= min && rect.width() <= max && rect.height() >= min && rect.height() <= max - && rect.centerX() >= width * MENU_BUTTON_MIN_CENTER_X + && atEndSide && rect.top >= width * THUMBNAIL_RATIO - dp(root, THUMBNAIL_OVERLAP_DP); } @@ -407,11 +419,16 @@ private static String getDescriptionPattern(CharSequence description, List(menuButton); pendingVideoId = ""; + ComponentHost root = findRootHost(menuButton); + String cardVideoId = root == null ? null : FeedVideoCardsPatch.findVideoId(root); + pendingCardVideoId = cardVideoId != null && VIDEO_ID_PATTERN.matcher(cardVideoId).matches() + ? cardVideoId + : ""; pendingWatchLaterUptime = SystemClock.uptimeMillis(); pendingSaveToPlaylist = saveToPlaylist; pendingItemClicked = false; @@ -836,6 +858,7 @@ private static void clearPendingWatchLater() { pendingWatchLaterUptime = 0; pendingSaveToPlaylist = false; pendingVideoId = ""; + pendingCardVideoId = ""; pendingMenuButtonRef = new WeakReference<>(null); pendingFlyoutRef = new WeakReference<>(null); pendingItemClicked = false; @@ -1032,6 +1055,14 @@ public static void onFlyoutListBound(ViewGroup itemList) { // The items may not be bound yet. The fallback runs if they never are. return; } + // The index is only right if every button is an item, and no other item was added, + // otherwise another item is clicked, such as the share item. The fallback runs instead. + final int buttonCount = FlyoutUtils.getVisibleFlyoutButtons().size(); + if (items.getChildCount() != buttonCount) { + Logger.printDebug(() -> "Menu items do not match the buttons: " + items.getChildCount() + + " items, " + buttonCount + " buttons"); + return; + } item = items.getChildAt(itemIndex); } @@ -1204,7 +1235,7 @@ private static void onPendingFallback() { return; } Object flyout = pendingFlyoutRef.get(); - String videoId = pendingVideoId; + String videoId = pendingVideoId.isEmpty() ? pendingCardVideoId : pendingVideoId; final boolean saveToPlaylist = pendingSaveToPlaylist; clearPendingWatchLater(); if (saveToPlaylist) { From b01253535eb65761f896042d46ba1fed002232c2 Mon Sep 17 00:00:00 2001 From: 0xrxL <127248639+0xrxL@users.noreply.github.com> Date: Tue, 27 Oct 2026 15:59:45 +0100 Subject: [PATCH 107/189] fix(YouTube - Feed view count): View count badge covers the duration in right to left languages --- .../patches/FeedEngagementCountsPatch.java | 31 ++++++++++++----- .../youtube/patches/FeedViewCountPatch.java | 34 ++++++++++++++----- 2 files changed, 47 insertions(+), 18 deletions(-) diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FeedEngagementCountsPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FeedEngagementCountsPatch.java index 949fa39c4b..2971f28c29 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FeedEngagementCountsPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FeedEngagementCountsPatch.java @@ -66,7 +66,10 @@ public final class FeedEngagementCountsPatch { /** Top margin of a thumbnail that is too small to fit the badge above the duration. */ private static final int MARGIN_DP = 10; - /** Same as the duration badge and the Watch later button, so their right edges line up. */ + /** + * Same as the duration badge and the Watch later button, so their right edges line up. + * In right to left languages it's the left margin, the same as the duration badge. + */ private static final int RIGHT_MARGIN_DP = 8; /** Space between the badge and the duration badge below it. */ private static final int DURATION_GAP_DP = 4; @@ -348,7 +351,7 @@ private boolean isLoading() { } /** - * Places the badge on the right side of the thumbnail, just above the duration. + * Places the badge on the side of the duration of the thumbnail, just above it. */ void place(ComponentHost host) { final boolean loading = isLoading(); @@ -390,33 +393,43 @@ void place(ComponentHost host) { FeedViewCountPatch.ThumbnailFrame frame = FeedViewCountPatch.findThumbnailFrame(host); final Rect duration = frame.duration; final Rect thumbnail = frame.thumbnail; - final int right; + // In right to left languages the duration badge is at the left, so the badge is too. + final boolean rtl = FeedViewCountPatch.isRtl(host); + // The right edge of the badge, or its left edge in right to left languages. + final int edge; final int bottom; final int thumbnailTop; if (duration != null) { - right = duration.right; + edge = rtl ? duration.left : duration.right; bottom = duration.top - px(DURATION_GAP_DP); if (thumbnail != null && thumbnail.top < duration.top) { thumbnailTop = thumbnail.top; } else { // Thumbnails are centered on the card, and at least 16:9. - final int thumbnailRight = duration.right + px(RIGHT_MARGIN_DP); - final int thumbnailWidth = thumbnailRight - Math.max(0, host.getWidth() - thumbnailRight); + final int thumbnailWidth; + if (rtl) { + final int thumbnailLeft = duration.left - px(RIGHT_MARGIN_DP); + thumbnailWidth = host.getWidth() - thumbnailLeft - Math.max(0, thumbnailLeft); + } else { + final int thumbnailRight = duration.right + px(RIGHT_MARGIN_DP); + thumbnailWidth = thumbnailRight - Math.max(0, host.getWidth() - thumbnailRight); + } thumbnailTop = duration.bottom + px(RIGHT_MARGIN_DP) - Math.round(thumbnailWidth * FeedVideoCardsPatch.THUMBNAIL_RATIO); } } else if (thumbnail != null) { - right = thumbnail.right - px(RIGHT_MARGIN_DP); + edge = rtl ? thumbnail.left + px(RIGHT_MARGIN_DP) : thumbnail.right - px(RIGHT_MARGIN_DP); bottom = thumbnail.bottom - px(ABOVE_DURATION_DP); thumbnailTop = thumbnail.top; } else { - right = host.getWidth() - px(RIGHT_MARGIN_DP); + edge = rtl ? px(RIGHT_MARGIN_DP) : host.getWidth() - px(RIGHT_MARGIN_DP); bottom = FeedVideoCardsPatch.getThumbnailBottom(host) - px(ABOVE_DURATION_DP); thumbnailTop = 0; } final int top = Math.max(thumbnailTop + px(MARGIN_DP), bottom - height); - setBounds(right - width, top, right, top + height); + final int left = rtl ? edge : edge - width; + setBounds(left, top, left + width, top + height); if (!added) { host.getOverlay().add(this); added = true; diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FeedViewCountPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FeedViewCountPatch.java index 9b2bfc6645..215c63d7a0 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FeedViewCountPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FeedViewCountPatch.java @@ -55,6 +55,7 @@ /** * Shows the view count of feed videos at the bottom left of their thumbnails, * lined up with the duration badge at the bottom right. + * In right to left languages, both are on the other side. *

    * The view count is the text YouTube shows below the title, such as * "Channel - 4.7M views - 2 days ago". When that text is created, the shortened view count @@ -105,7 +106,7 @@ public final class FeedViewCountPatch { /** Space between the duration badge and the edges of the thumbnail, if the badge is not found. */ private static final int FALLBACK_MARGIN_DP = 8; /** A little more space than the duration badge has, so the count is not at the very edge. */ - private static final int EXTRA_LEFT_MARGIN_DP = 2; + private static final int EXTRA_MARGIN_DP = 2; /** A square thumbnail is this much taller than 16:9, at least. */ private static final int SQUARE_MIN_EXTRA_DP = 40; /** Space between the thumbnail and the title. */ @@ -480,8 +481,10 @@ private static ViewCountMatch findViewCount(String text, Locale locale) { index = 1; } } else if (parsed[0] != null && segments.get(0).startsWithNumber() - && segments.get(1).startsWithNumber()) { - // The view count and the upload time, such as on the channel page. + && segments.get(1).hasNumber()) { + // The view count and the upload time, such as on the channel page and on the cards. + // Some languages show a word before the number of the upload time, + // such as "vor 2 Tagen" in German. index = 0; } } @@ -834,7 +837,8 @@ private static boolean isThumbnailShaped(ComponentHost root, Rect rect) { } /** - * A host only as large as the badge, at the bottom right of the thumbnail. + * A host only as large as the badge, at the bottom right of the thumbnail, + * or at the bottom left in right to left languages. */ private static boolean isDurationBadgeShaped(ComponentHost root, Rect rect) { final float density = root.getResources().getDisplayMetrics().density; @@ -843,10 +847,18 @@ private static boolean isDurationBadgeShaped(ComponentHost root, Rect rect) { && rect.width() <= MAX_DURATION_WIDTH_DP * density && rect.height() >= MIN_DURATION_HEIGHT_DP * density && rect.height() <= MAX_DURATION_HEIGHT_DP * density - && rect.centerX() > root.getWidth() / 2 + && (isRtl(root) ? rect.centerX() < root.getWidth() / 2 : rect.centerX() > root.getWidth() / 2) && rect.bottom <= root.getWidth() + TOLERANCE_DP * density; } + /** + * @return If the card is laid out right to left, where the duration badge is at the bottom left + * of the thumbnail, so the badges are placed on the other side. + */ + public static boolean isRtl(View view) { + return view.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; + } + /** * Where the duration badge and the thumbnail of a card are, relative to the card. */ @@ -1034,18 +1046,21 @@ void bind(ComponentHost host, @Nullable String newViewCount, @Nullable Rect dura : new Rect(0, 0, host.getWidth(), Math.round(host.getWidth() * FeedVideoCardsPatch.THUMBNAIL_RATIO)); - // Same height, bottom and margin as the duration badge. + // Same height, bottom and margin as the duration badge, on the other side of the thumbnail. + final boolean rtl = isRtl(host); final int top; final int bottom; - final int left; + // Distance of the badge from the edge of the thumbnail on its side. + final int margin; if (duration != null) { top = duration.top; bottom = duration.bottom; - left = frame.left + Math.max(0, frame.right - duration.right) + px(EXTRA_LEFT_MARGIN_DP); + margin = Math.max(0, rtl ? duration.left - frame.left : frame.right - duration.right) + + px(EXTRA_MARGIN_DP); } else { bottom = frame.bottom - px(FALLBACK_MARGIN_DP); top = bottom - px(FALLBACK_HEIGHT_DP); - left = frame.left + px(FALLBACK_MARGIN_DP + EXTRA_LEFT_MARGIN_DP); + margin = px(FALLBACK_MARGIN_DP + EXTRA_MARGIN_DP); } final int height = bottom - top; @@ -1056,6 +1071,7 @@ void bind(ComponentHost host, @Nullable String newViewCount, @Nullable Rect dura : textPaint.measureText(viewCount); final int width = Math.round(px(PADDING_DP) * 2 + iconSize + px(GAP_DP) + contentWidth); + final int left = rtl ? frame.right - margin - width : frame.left + margin; setBounds(left, top, left + width, bottom); if (!added) { host.getOverlay().add(this); From 141aa8fedea935664df73154cdc1823e893c5b88 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Tue, 27 Oct 2026 15:51:16 +0000 Subject: [PATCH 108/189] bump(YouTube Music): Add experimental support for `9.43.51` --- .../music/interaction/jam/Fingerprints.kt | 24 +++++++- .../patches/music/interaction/jam/JamUIABI.kt | 57 +++++++++---------- .../music/interaction/jam/JamUIInstaller.kt | 29 +++++++++- .../EnableSwipeToDismissMiniplayerPatch.kt | 51 +++++++++++++---- .../music/layout/miniplayer/Fingerprints.kt | 42 +++++++++++++- .../misc/playservice/VersionCheckPatch.kt | 3 + .../morphe/patches/music/shared/Constants.kt | 6 +- 7 files changed, 161 insertions(+), 51 deletions(-) diff --git a/patches/src/main/kotlin/app/morphe/patches/music/interaction/jam/Fingerprints.kt b/patches/src/main/kotlin/app/morphe/patches/music/interaction/jam/Fingerprints.kt index be041bb37c..ca7501eab8 100644 --- a/patches/src/main/kotlin/app/morphe/patches/music/interaction/jam/Fingerprints.kt +++ b/patches/src/main/kotlin/app/morphe/patches/music/interaction/jam/Fingerprints.kt @@ -190,8 +190,18 @@ internal fun seekForwarderFingerprint() = Fingerprint( } ) +/** Finds the player palette source's local apply method, which stores the palette entity. */ +internal object PaletteEntityFingerprint : Fingerprint( + returnType = "V", + parameters = listOf("L"), + filters = listOf( + string("YTM_PLAYER_COLOR_SAMPLE_PALETTE_ENTITY_KEY") + ) +) + /** Finds the palette source's publish-then-apply entry point. */ internal object PalettePublicationFingerprint : Fingerprint( + classFingerprint = PaletteEntityFingerprint, returnType = "V", parameters = emptyList(), filters = listOf( @@ -585,11 +595,19 @@ internal fun protoParserFingerprint(messageHierarchy: Set) = Fingerprint } ) -/** A palette source owns an extractor producing the same type its local apply method consumes. */ -internal fun paletteExtractorFingerprint(paletteType: String, ownedTypes: Set) = Fingerprint( +/** The extractor produces the same type the palette source's local apply method consumes. */ +internal fun paletteExtractorFingerprint(paletteType: String) = Fingerprint( parameters = listOf("Landroid/graphics/Bitmap;"), returnType = paletteType, - custom = { _, owner -> owner.type in ownedTypes }, +) + +/** Finds the end of the palette extractor constructor. */ +internal fun paletteExtractorConstructorFingerprint(extractorType: String) = Fingerprint( + definingClass = extractorType, + name = "", + filters = listOf( + opcode(Opcode.RETURN_VOID) + ) ) internal fun queueItemEndpointFingerprint(ownerType: String, commandType: String) = Fingerprint( diff --git a/patches/src/main/kotlin/app/morphe/patches/music/interaction/jam/JamUIABI.kt b/patches/src/main/kotlin/app/morphe/patches/music/interaction/jam/JamUIABI.kt index 5729dfb5cf..a0a645b58d 100644 --- a/patches/src/main/kotlin/app/morphe/patches/music/interaction/jam/JamUIABI.kt +++ b/patches/src/main/kotlin/app/morphe/patches/music/interaction/jam/JamUIABI.kt @@ -15,6 +15,7 @@ import app.morphe.patcher.methodCall import app.morphe.patcher.patch.BytecodePatchContext import app.morphe.patcher.util.getReference import app.morphe.patcher.util.p0Register +import app.morphe.patches.music.misc.playservice.is_9_43_or_greater import app.morphe.util.findInstructionIndicesReversed import app.morphe.util.findInstructionIndicesReversedOrThrow import com.android.tools.smali.dexlib2.AccessFlags @@ -73,7 +74,8 @@ internal data class ClockAbi( internal data class PaletteAbi( val type: String, val entry: MethodReference, - val extractor: FieldReference, + /** Null with 9.43+, where the palette source no longer keeps an extractor. */ + val extractor: FieldReference?, val extract: MethodReference, val publisher: FieldReference, val publish: MethodReference, @@ -247,37 +249,30 @@ private fun BytecodePatchContext.resolveSeek(timeBar: ClassDef): MethodReference } private fun BytecodePatchContext.resolvePalette(): PaletteAbi { - val candidates = - PalettePublicationFingerprint.matchAll().mapNotNull { match -> - val source = match.originalClassDef - val publish = match.instructionMatches[0].instruction.getReference()!! - val local = match.instructionMatches[1].instruction.getReference()!! - val extractors = - paletteExtractorFingerprint( - local.parameterTypes[0].toString(), - source.fields.map { it.type }.toSet(), - ) - .matchAll(0..1) - if (extractors.isEmpty()) return@mapNotNull null - val extract = extractors.single().originalMethod - val extractor = - source.fields.singleOrNull { it.type == extract.definingClass } - ?: error("Missing or ambiguous – Jam palette extractor field") - val publisher = - source.fields.singleOrNull { it.type == publish.definingClass } - ?: error("Missing or ambiguous – Jam palette publisher field") - PaletteAbi( - source.type, - match.originalMethod, - extractor, - extract, - publisher, - publish, - local, - ) + val match = PalettePublicationFingerprint.match() + val source = match.originalClassDef + val publish = match.instructionMatches[0].instruction.getReference()!! + val local = match.instructionMatches[1].instruction.getReference()!! + val extract = paletteExtractorFingerprint(local.parameterTypes[0].toString()).originalMethod + val extractor = + if (is_9_43_or_greater) { + null + } else { + source.fields.singleOrNull { it.type == extract.definingClass } + ?: error("Missing or ambiguous – Jam palette extractor field") } - return candidates.singleOrNull() - ?: error("Missing or ambiguous – Jam player palette pipeline") + val publisher = + source.fields.singleOrNull { it.type == publish.definingClass } + ?: error("Missing or ambiguous – Jam palette publisher field") + return PaletteAbi( + source.type, + match.originalMethod, + extractor, + extract, + publisher, + publish, + local, + ) } private fun BytecodePatchContext.resolvePlayback(): PlaybackAbi { diff --git a/patches/src/main/kotlin/app/morphe/patches/music/interaction/jam/JamUIInstaller.kt b/patches/src/main/kotlin/app/morphe/patches/music/interaction/jam/JamUIInstaller.kt index 06f0562cda..2c715186f3 100644 --- a/patches/src/main/kotlin/app/morphe/patches/music/interaction/jam/JamUIInstaller.kt +++ b/patches/src/main/kotlin/app/morphe/patches/music/interaction/jam/JamUIInstaller.kt @@ -7,6 +7,7 @@ package app.morphe.patches.music.interaction.jam +import app.morphe.patcher.extensions.InstructionExtensions.addInstruction import app.morphe.patcher.extensions.InstructionExtensions.addInstructions import app.morphe.patcher.extensions.InstructionExtensions.addInstructionsWithLabels import app.morphe.patcher.extensions.InstructionExtensions.getInstruction @@ -15,6 +16,7 @@ import app.morphe.patcher.extensions.InstructionExtensions.replaceInstruction import app.morphe.patcher.methodCall import app.morphe.patcher.patch.BytecodePatchContext import app.morphe.patcher.util.matchAllMethodIndicesForEach +import app.morphe.patcher.util.proxy.mutableTypes.MutableField.Companion.toMutable import app.morphe.patcher.util.smali.ExternalLabel import app.morphe.util.findInstructionIndicesReversedOrThrow import app.morphe.util.getMutableMethod @@ -27,6 +29,7 @@ import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction import com.android.tools.smali.dexlib2.iface.instruction.RegisterRangeInstruction import com.android.tools.smali.dexlib2.iface.reference.MethodReference +import com.android.tools.smali.dexlib2.immutable.ImmutableField private const val CLOCK = "Lapp/morphe/extension/music/patches/jam/JamClock;" private const val OBJECT = "Ljava/lang/Object;" @@ -230,13 +233,16 @@ private fun BytecodePatchContext.installPalette(palette: PaletteAbi) { return-void """ ) + // 9.43+ palette source no longer keeps an extractor, so use the last one the app created. + val loadExtractor = palette.extractor?.let { "iget-object v0, p0, $it" } + ?: "sget-object v0, ${addExtractorInstanceField(palette.extract.definingClass)}" source.addBridge( "patch_jamExtract", listOf("Landroid/graphics/Bitmap;"), "Ljava/lang/Object;", 3, body = """ - iget-object v0, p0, ${palette.extractor} + $loadExtractor ${invokeKind(palette.extract)} {v0, p1}, ${palette.extract} move-result-object v0 return-object v0 @@ -257,6 +263,27 @@ private fun BytecodePatchContext.installPalette(palette: PaletteAbi) { ) } +/** Adds a static field to the extractor class that holds the last constructed instance. */ +private fun BytecodePatchContext.addExtractorInstanceField(extractorType: String) = + paletteExtractorConstructorFingerprint(extractorType).let { + val instanceField = ImmutableField( + extractorType, + "patch_jamInstance", + extractorType, + AccessFlags.PUBLIC.value or AccessFlags.STATIC.value, + null, + null, + null, + ).toMutable() + + it.classDef.staticFields.add(instanceField) + it.method.addInstruction( + it.instructionMatches.first().index, + "sput-object p0, $instanceField" + ) + instanceField + } + private fun BytecodePatchContext.installPlayback(playback: PlaybackAbi) { playback.routers.forEach { router -> val owner = mutableClassDefBy(router.type) diff --git a/patches/src/main/kotlin/app/morphe/patches/music/layout/miniplayer/EnableSwipeToDismissMiniplayerPatch.kt b/patches/src/main/kotlin/app/morphe/patches/music/layout/miniplayer/EnableSwipeToDismissMiniplayerPatch.kt index c82fad5ca0..28d5febc53 100644 --- a/patches/src/main/kotlin/app/morphe/patches/music/layout/miniplayer/EnableSwipeToDismissMiniplayerPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/music/layout/miniplayer/EnableSwipeToDismissMiniplayerPatch.kt @@ -22,6 +22,7 @@ import app.morphe.patcher.util.getReference import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable import app.morphe.patcher.util.smali.ExternalLabel import app.morphe.patches.music.misc.extension.sharedExtensionPatch +import app.morphe.patches.music.misc.playservice.is_9_43_or_greater import app.morphe.patches.music.misc.playservice.versionCheckPatch import app.morphe.patches.music.misc.settings.PreferenceScreen import app.morphe.patches.music.misc.settings.settingsPatch @@ -219,17 +220,45 @@ val enableSwipeToDismissMiniplayerPatch = bytecodePatch( // region Disable player page motion event - PlayerPageBehaviorFingerprint.method.addInstructionsWithLabels( - 0, - """ - invoke-static { }, $EXTENSION_CLASS->enableSwipeToDismissMiniplayer()Z - move-result v0 - if-eqz v0, :ignore - return-void - :ignore - nop - """ - ) + if (is_9_43_or_greater) { + // The motion field is now set directly from several places, + // but it is only read by the touch methods. Clear it before they run. + val motionField = PlayerPageBehaviorInterceptTouchEventFingerprint + .instructionMatches + .first() + .instruction + .getReference()!! + + listOf( + PlayerPageBehaviorInterceptTouchEventFingerprint, + PlayerPageBehaviorTouchEventFingerprint + ).forEach { + it.method.addInstructionsWithLabels( + 0, + """ + invoke-static { }, $EXTENSION_CLASS->enableSwipeToDismissMiniplayer()Z + move-result v0 + if-eqz v0, :ignore + const/4 v0, 0x0 + iput-boolean v0, p0, $motionField + :ignore + nop + """ + ) + } + } else { + PlayerPageBehaviorFingerprint.method.addInstructionsWithLabels( + 0, + """ + invoke-static { }, $EXTENSION_CLASS->enableSwipeToDismissMiniplayer()Z + move-result v0 + if-eqz v0, :ignore + return-void + :ignore + nop + """ + ) + } // endregion diff --git a/patches/src/main/kotlin/app/morphe/patches/music/layout/miniplayer/Fingerprints.kt b/patches/src/main/kotlin/app/morphe/patches/music/layout/miniplayer/Fingerprints.kt index 706925fceb..06f58652a9 100644 --- a/patches/src/main/kotlin/app/morphe/patches/music/layout/miniplayer/Fingerprints.kt +++ b/patches/src/main/kotlin/app/morphe/patches/music/layout/miniplayer/Fingerprints.kt @@ -24,6 +24,9 @@ import com.android.tools.smali.dexlib2.Opcode internal const val WATCH_WHILE_LAYOUT_CLASS = "Lcom/google/android/apps/youtube/music/watchpage/ui/WatchWhileLayout;" +private const val PLAYER_PAGE_BEHAVIOR_CLASS = + "Lcom/google/android/apps/youtube/music/watchpage/ui/PlayerPageBehavior;" + /** * Matches the miniplayer constructor. * Identified by the play/pause button resource literal and a unique string in the method body. @@ -193,10 +196,10 @@ internal object MiniPlayerDefaultTextFingerprint : Fingerprint( ) /** - * 9.02+ + * 9.02 - 9.42 */ internal object PlayerPageBehaviorFingerprint : Fingerprint( - definingClass = "Lcom/google/android/apps/youtube/music/watchpage/ui/PlayerPageBehavior;", + definingClass = PLAYER_PAGE_BEHAVIOR_CLASS, accessFlags = listOf(AccessFlags.FINAL), parameters = listOf(), returnType = "V", @@ -207,6 +210,41 @@ internal object PlayerPageBehaviorFingerprint : Fingerprint( ) ) +/** + * 9.43+ + * The first instruction match reads the player page motion field. + */ +internal object PlayerPageBehaviorInterceptTouchEventFingerprint : Fingerprint( + definingClass = PLAYER_PAGE_BEHAVIOR_CLASS, + name = "onInterceptTouchEvent", + filters = listOf( + fieldAccess( + opcode = Opcode.IGET_BOOLEAN, + definingClass = "this" + ), + opcode(Opcode.IF_EQZ, location = MatchAfterImmediately()), + fieldAccess( + opcode = Opcode.IGET_OBJECT, + definingClass = "this", + location = MatchAfterImmediately() + ), + methodCall( + opcode = Opcode.INVOKE_VIRTUAL, + parameters = listOf("Landroid/view/MotionEvent;"), + returnType = "V", + location = MatchAfterImmediately() + ) + ) +) + +/** + * 9.43+ + */ +internal object PlayerPageBehaviorTouchEventFingerprint : Fingerprint( + definingClass = PLAYER_PAGE_BEHAVIOR_CLASS, + name = "onTouchEvent" +) + /** * Matches the watch-while layout's onFinishInflate() method. * definingClass uses a contains match, covering class renames across builds: diff --git a/patches/src/main/kotlin/app/morphe/patches/music/misc/playservice/VersionCheckPatch.kt b/patches/src/main/kotlin/app/morphe/patches/music/misc/playservice/VersionCheckPatch.kt index 05e2c3824c..75964dec3f 100644 --- a/patches/src/main/kotlin/app/morphe/patches/music/misc/playservice/VersionCheckPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/music/misc/playservice/VersionCheckPatch.kt @@ -29,6 +29,8 @@ var is_9_35_or_greater: Boolean by Delegates.notNull() private set var is_9_41_or_greater: Boolean by Delegates.notNull() private set +var is_9_43_or_greater: Boolean by Delegates.notNull() + private set val versionCheckPatch = bytecodePatch { execute { @@ -48,5 +50,6 @@ val versionCheckPatch = bytecodePatch { is_9_33_or_greater = isEqualsOrGreaterThan("9.33.00") is_9_35_or_greater = isEqualsOrGreaterThan("9.35.00") is_9_41_or_greater = isEqualsOrGreaterThan("9.41.00") + is_9_43_or_greater = isEqualsOrGreaterThan("9.43.00") } } diff --git a/patches/src/main/kotlin/app/morphe/patches/music/shared/Constants.kt b/patches/src/main/kotlin/app/morphe/patches/music/shared/Constants.kt index 5017b4955e..13222321ca 100644 --- a/patches/src/main/kotlin/app/morphe/patches/music/shared/Constants.kt +++ b/patches/src/main/kotlin/app/morphe/patches/music/shared/Constants.kt @@ -18,17 +18,17 @@ internal object Constants { ), targets = listOf( AppTarget( - version = "9.42.52", + version = "9.43.51", minSdk = 26, isExperimental = true, ), AppTarget( - version = "9.41.52", + version = "9.42.52", minSdk = 26, isExperimental = true, ), AppTarget( - version = "9.40.51", + version = "9.41.52", minSdk = 26, isExperimental = true, ), From 1f24e8433b26aed7a0e8cd21bf6c719526e7ecc4 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Tue, 27 Oct 2026 16:53:28 +0100 Subject: [PATCH 109/189] feat(YouTube - Theme): Add experimental YT splash screen animation (#4109) --- .../music/patches/StartupAnimationPatch.java | 3 +- .../shared/patches/CustomBrandingPatch.java | 63 ++--- .../shared/patches/SplashAnimationPatch.java | 94 +++++--- .../settings/SharedYouTubeSettings.java | 19 +- .../shared/theme/ThemeColorPatch.java | 30 ++- .../youtube/patches/VersionCheckPatch.java | 2 + .../youtube/patches/theme/ThemePatch.java | 221 +++++++++++++++++- .../extension/youtube/settings/Settings.java | 8 + .../layout/branding/CustomBrandingPatch.kt | 9 +- .../branding/BaseCustomBrandingPatch.kt | 15 +- .../branding/StartupAnimationResourcePatch.kt | 29 +++ .../youtube/layout/theme/Fingerprints.kt | 135 +++++++++++ .../layout/theme/SplashAnimationPatch.kt | 161 ++++++++++++- .../youtube/layout/theme/ThemePatch.kt | 44 +--- .../values/shared-youtube/arrays.xml | 19 +- .../values/shared-youtube/strings.xml | 10 +- .../addresources/values/youtube/arrays.xml | 17 ++ .../addresources/values/youtube/strings.xml | 1 + 18 files changed, 746 insertions(+), 134 deletions(-) create mode 100644 patches/src/main/kotlin/app/morphe/patches/shared/layout/branding/StartupAnimationResourcePatch.kt diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/StartupAnimationPatch.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/StartupAnimationPatch.java index 9f4189d8b7..1c9386346d 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/StartupAnimationPatch.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/StartupAnimationPatch.java @@ -1,6 +1,7 @@ /* * Copyright 2026 Morphe. * https://github.com/MorpheApp/morphe-patches/pull/3178 + * https://github.com/MorpheApp/morphe-patches/pull/4109 * * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. */ @@ -43,7 +44,7 @@ public static void setSplashAnimationLottie(LottieAnimationView view, int resour private static void setSplashAnimation(LottieAnimationView view, int resourceId) { try { - // A custom branding icon replaces the YT Music logo animation with its own. + // The Morphe styles replace the YT Music logo animation with the Morphe one. if (SplashAnimationPatch.setBrandedSplashAnimation(view)) { return; } diff --git a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/patches/CustomBrandingPatch.java b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/patches/CustomBrandingPatch.java index 571bbc7975..00e2fe5666 100644 --- a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/patches/CustomBrandingPatch.java +++ b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/patches/CustomBrandingPatch.java @@ -88,8 +88,7 @@ String notificationIconResourceName() { * Returns the raw resource name of the startup animation for this theme. *

    * Themes that share a launcher foreground share an animation. Returns {@code null} for - * {@link #ORIGINAL} and {@link #CUSTOM}, which keep the original animation. - * + * {@link #ORIGINAL} and {@link #CUSTOM}, which have no animation of their own. */ @Nullable String startupAnimationResourceName() { @@ -176,51 +175,59 @@ private static int getNotificationSmallIcon() { * Injection point. */ public static View getLottieViewOrNull(View lottieStartupView) { - if (SharedYouTubeSettings.SPLASH_SCREEN_ANIMATION_STYLE.get() + if (SharedYouTubeSettings.SPLASH_SCREEN_STYLE.get() == SplashAnimationPatch.SplashScreenAnimationStyle.DISABLED) { return null; } - if (getStartupAnimation() != 0) { - // The icon has its own animation, which replaces the original one. - return lottieStartupView; + return lottieStartupView; + } + + /** + * If custom branding is included in the app. + */ + public static boolean isPatchIncluded() { + return false; // Modified during patching. + } + + private static BrandingTheme getLauncherIconBranding() { + if (!isPatchIncluded()) { + // The app keeps its original icon. + return BrandingTheme.ORIGINAL; } if (GmsCoreSupportPatch.isPackageNameOriginal()) { - // A mounted install cannot change the icon at runtime, so the icon chosen - // while patching decides if the original startup animation is kept. - return mountedIconApplied() ? null : lottieStartupView; + // A mounted install cannot change the icon at runtime. + return mountedIconApplied() + ? getDefaultIconStyle() + : BrandingTheme.ORIGINAL; } - if (SharedYouTubeSettings.CUSTOM_BRANDING_ICON.get() == BrandingTheme.ORIGINAL) { - return lottieStartupView; - } + return SharedYouTubeSettings.CUSTOM_BRANDING_ICON.get(); + } - return null; + /** + * If the launcher icon has a Morphe startup animation of its own. + */ + public static boolean iconHasStartupAnimation() { + return getLauncherIconBranding().startupAnimationResourceName() != null; } /** - * The startup animation that matches the launcher icon. + * The branded startup animation that matches the launcher icon. The original icon and a user + * provided icon have no animation of their own, and use the animation of the default icon. * - * @return The raw resource id, or 0 to keep the original animation. + * @return The raw resource id, or 0 if the animations are excluded. */ public static int getStartupAnimation() { try { - BrandingTheme branding; - if (GmsCoreSupportPatch.isPackageNameOriginal()) { - // A mounted install cannot change the icon at runtime. - branding = mountedIconApplied() - ? getDefaultIconStyle() - : BrandingTheme.ORIGINAL; - } else { - branding = SharedYouTubeSettings.CUSTOM_BRANDING_ICON.get(); + String animationName = getLauncherIconBranding().startupAnimationResourceName(); + if (animationName == null) { + animationName = BrandingTheme.BLACK.startupAnimationResourceName(); } - String animationName = branding.startupAnimationResourceName(); - if (animationName != null) { - // Not found if custom branding is excluded. - return ResourceUtils.getIdentifier(ResourceType.RAW, animationName); - } + // Not found if the patch that adds the startup animations is excluded. + return ResourceUtils.getIdentifier(ResourceType.RAW, animationName); } catch (Exception ex) { Logger.printException(() -> "getStartupAnimation failure", ex); } diff --git a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/patches/SplashAnimationPatch.java b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/patches/SplashAnimationPatch.java index e0316abceb..dfff6acf16 100644 --- a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/patches/SplashAnimationPatch.java +++ b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/patches/SplashAnimationPatch.java @@ -1,6 +1,7 @@ /* * Copyright 2026 Morphe. * https://github.com/MorpheApp/morphe-patches/pull/3178 + * https://github.com/MorpheApp/morphe-patches/pull/4109 * * Original hard forked code: * https://github.com/ReVanced/revanced-patches/commit/724e6d61b2ecd868c1a9a37d465a688e83a74799 @@ -39,20 +40,33 @@ public class SplashAnimationPatch { public enum SplashScreenAnimationStyle { // 0 int style exists in target app as a fall through default, but its value is repurposed to be disabled. - DISABLED(0), - FPS_60_ONE_SECOND(1), - FPS_60_TWO_SECOND(2), - FPS_60_FIVE_SECOND(3), - FPS_60_BLACK_AND_WHITE(4), - FPS_30_ONE_SECOND(5), - FPS_30_TWO_SECOND(6), - FPS_30_FIVE_SECOND(7), - FPS_30_BLACK_AND_WHITE(8); + DISABLED(0, false), + FPS_60_ONE_SECOND(1, false), + FPS_60_TWO_SECOND(2, false), + FPS_60_FIVE_SECOND(3, false), + FPS_60_BLACK_AND_WHITE(4, true), + FPS_30_ONE_SECOND(5, false), + FPS_30_TWO_SECOND(6, false), + FPS_30_FIVE_SECOND(7, false), + FPS_30_BLACK_AND_WHITE(8, true), + /** + * YouTube 21.39+ Rive animation of a logo that comes into focus out of a colored glow. + * Low memory devices cannot play it and get the Lottie animation of this style instead. + */ + RIVE_GLOW(1, false), + /** + * Morphe animation of the launcher icon, which replaces the Lottie animation of the app. + */ + MORPHE_COLOR(1, false), + /** + * Morphe animation of the launcher icon in the foreground color of the theme. + */ + MORPHE_MONOCHROME(1, true); // There exists a 10th JSON style used as the switch statement default, // but visually it is identical to 60fps one second. // - // YT Music has a single animation and no styles, so only the three styles - // shown in the settings apply to it: disabled, color and black and white. + // YT Music has a single animation and no styles, so only the styles shown in the + // settings apply to it: disabled, color, black and white, and the Morphe styles. @Nullable public static SplashScreenAnimationStyle styleFromOrdinal(int style) { @@ -68,8 +82,14 @@ public static SplashScreenAnimationStyle styleFromOrdinal(int style) { public final int style; - SplashScreenAnimationStyle(int style) { + /** + * If the animation is drawn in the foreground color of the theme, or in black and white. + */ + public final boolean isMonochrome; + + SplashScreenAnimationStyle(int style, boolean isMonochrome) { this.style = style; + this.isMonochrome = isMonochrome; } } @@ -82,24 +102,27 @@ public static SplashScreenAnimationStyle styleFromOrdinal(int style) { private static final String BRAND_GRADIENT = "[0,0.117647,0.352941,0.658824,1,0,0.686275,0.682353]"; public static boolean isMonochrome() { - SplashScreenAnimationStyle style = SharedYouTubeSettings.SPLASH_SCREEN_ANIMATION_STYLE.get(); - return style == SplashScreenAnimationStyle.FPS_30_BLACK_AND_WHITE - || style == SplashScreenAnimationStyle.FPS_60_BLACK_AND_WHITE; + return SharedYouTubeSettings.SPLASH_SCREEN_STYLE.get().isMonochrome; } /** - * Plays the animation of the branding icon, in the color of the theme if the style asks - * for a monochrome animation. + * Plays the Morphe animation if the style asks for it, + * in the color of the theme for the monochrome style. * - * @return If a branded animation was played, and the original one should not be. + * @return If a Morphe animation was played, and the original one should not be. */ public static boolean setBrandedSplashAnimation(LottieAnimationView view) { + SplashScreenAnimationStyle style = SharedYouTubeSettings.SPLASH_SCREEN_STYLE.get(); + if (style != SplashScreenAnimationStyle.MORPHE_COLOR && style != SplashScreenAnimationStyle.MORPHE_MONOCHROME) { + return false; + } + final int animation = CustomBrandingPatch.getStartupAnimation(); if (animation == 0) { return false; } - if (isMonochrome()) { + if (style.isMonochrome) { // A gradient of a single color is a flat fill, so the same file // also serves as the monochrome animation. final int color = ThemeUtils.getAppForegroundColor(); @@ -109,7 +132,7 @@ public static boolean setBrandedSplashAnimation(LottieAnimationView view) { BRAND_PRIMARY_COLOR, flatColor, BRAND_SECONDARY_COLOR, flatColor, BRAND_GRADIENT, getGradientStringArray(color) - )); + ), COLOR_KEYS); } else { view.patch_setAnimation(animation); } @@ -118,8 +141,12 @@ BRAND_GRADIENT, getGradientStringArray(color) } /** - * A fixed color is written as the value of a property, and a color that animates as the - * value of one of its keyframes. + * A fixed color is written as the value of a property. + */ + private static final String[] FIXED_COLOR_KEYS = { "\"k\":" }; + + /** + * A fixed color, and a color that animates, which is written as the value of one of its keyframes. */ private static final String[] COLOR_KEYS = { "\"k\":", "\"s\":" }; @@ -130,14 +157,29 @@ BRAND_GRADIENT, getGradientStringArray(color) */ public static void setSplashAnimation(LottieAnimationView view, int resourceId, Map replacements) { + replaceColorsAndPlay(view, resourceId, getColorStringArrays(replacements), COLOR_KEYS); + } + + /** + * Plays a Lottie animation with some of the fixed colors of the file replaced. + * The colors that animate are kept, so a shape that fades out of a replaced color still + * starts with its original color. + * + * @param replacements The color of the file, as it is written in the JSON, and its replacement. + */ + public static void setSplashAnimationFixedColors(LottieAnimationView view, int resourceId, + Map replacements) { + replaceColorsAndPlay(view, resourceId, getColorStringArrays(replacements), FIXED_COLOR_KEYS); + } + + private static Map getColorStringArrays(Map replacements) { Map colors = new LinkedHashMap<>(); replacements.forEach((original, color) -> colors.put(original, getColorStringArray(color))); - - replaceColorsAndPlay(view, resourceId, colors); + return colors; } private static void replaceColorsAndPlay(LottieAnimationView view, int resourceId, - Map replacements) { + Map replacements, String[] colorKeys) { String json = loadRawResourceAsString(resourceId); String replacement = json; @@ -148,7 +190,7 @@ private static void replaceColorsAndPlay(LottieAnimationView view, int resourceI Logger.printException(() -> "Could not replace splash animation color: " + original); } - for (String key : COLOR_KEYS) { + for (String key : colorKeys) { replacement = replacement.replace(key + original, key + entry.getValue()); } } diff --git a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/settings/SharedYouTubeSettings.java b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/settings/SharedYouTubeSettings.java index ca877fd59c..43d2b0673a 100644 --- a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/settings/SharedYouTubeSettings.java +++ b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/settings/SharedYouTubeSettings.java @@ -6,6 +6,8 @@ import static app.morphe.extension.shared.settings.Setting.parent; import static app.morphe.extension.shared.settings.Setting.parentNot; +import java.util.EnumSet; + import app.morphe.extension.shared.Utils; import app.morphe.extension.shared.patches.BaseAppRefreshRatePatch.AppRefreshType; import app.morphe.extension.shared.patches.BaseAppRefreshRatePatch.RefreshRateType; @@ -75,9 +77,10 @@ public class SharedYouTubeSettings extends BaseSettings { public static final StringSetting THEME_COLOR_LIGHT_CUSTOM = new StringSetting("morphe_theme_color_light_custom", "#FFFFFF", true, new ThemeColorCustomAvailability(THEME_COLOR_LIGHT)); public static final BooleanSetting THEME_COLOR_CHANGE_FOREGROUND = new BooleanSetting("morphe_theme_color_change_foreground", false, true, new ThemeColorChangeForegroundAvailability()); public static final BooleanSetting THEME_LAST_USED_DARK_MODE = new BooleanSetting("morphe_theme_last_used_dark_mode", Utils.isDarkModeEnabled(), false, false); + public static final IntegerSetting THEME_LAST_SPLASH_SCREEN_INDEX = new IntegerSetting("morphe_theme_last_splash_screen_index", -1, false, false); // Custom branding - public static final EnumSetting SPLASH_SCREEN_ANIMATION_STYLE = new EnumSetting<>("morphe_splash_screen_animation_style", SplashScreenAnimationStyle.FPS_60_ONE_SECOND, true); + public static final EnumSetting SPLASH_SCREEN_STYLE = new EnumSetting<>("morphe_splash_screen_style", CustomBrandingPatch.isPatchIncluded() ? SplashScreenAnimationStyle.MORPHE_COLOR : SplashScreenAnimationStyle.FPS_60_ONE_SECOND, true); public static final EnumSetting CUSTOM_BRANDING_ICON = new EnumSetting<>("morphe_custom_branding_icon", CustomBrandingPatch.getDefaultIconStyle(), true, "morphe_custom_branding_icon_user_dialog_message"); public static final EnumSetting CUSTOM_BRANDING_NOTIFICATION_ICON = new EnumSetting<>("morphe_custom_branding_notification_icon", NotificationIconTheme.FOLLOW, true); public static final IntegerSetting CUSTOM_BRANDING_NAME = new IntegerSetting("morphe_custom_branding_name", CustomBrandingPatch.getDefaultAppNameIndex(), true); @@ -140,10 +143,24 @@ public class SharedYouTubeSettings extends BaseSettings { // Migration private static final BooleanSetting DEPRECATED_SANITIZE_URL_QUERY = new BooleanSetting("morphe_sanitize_url_query", TRUE); + private static final EnumSetting DEPRECATED_SPLASH_SCREEN_ANIMATION_STYLE = new EnumSetting<>("morphe_splash_screen_animation_style", SplashScreenAnimationStyle.FPS_60_ONE_SECOND); static { // TODO: Eventually remove these migrations migrateOldSettingToNew(DEPRECATED_SANITIZE_URL_QUERY, SANITIZE_SHARING_LINKS); + + // The styles of the original animation used to play the Morphe animation of an icon that has + // one, and now always play the original animation. The Morphe styles play the Morphe animation. + SplashScreenAnimationStyle splashStyle = DEPRECATED_SPLASH_SCREEN_ANIMATION_STYLE.get(); + if (!DEPRECATED_SPLASH_SCREEN_ANIMATION_STYLE.isSetToDefault() + && EnumSet.range(SplashScreenAnimationStyle.FPS_60_ONE_SECOND, + SplashScreenAnimationStyle.FPS_30_BLACK_AND_WHITE).contains(splashStyle) + && CustomBrandingPatch.iconHasStartupAnimation()) { + DEPRECATED_SPLASH_SCREEN_ANIMATION_STYLE.save(splashStyle.isMonochrome + ? SplashScreenAnimationStyle.MORPHE_MONOCHROME + : SplashScreenAnimationStyle.MORPHE_COLOR); + } + migrateOldSettingToNew(DEPRECATED_SPLASH_SCREEN_ANIMATION_STYLE, SPLASH_SCREEN_STYLE); } private static String getDefaultSpoofAppVersionTarget() { diff --git a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/theme/ThemeColorPatch.java b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/theme/ThemeColorPatch.java index e1910ae402..a3b1928fb0 100644 --- a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/theme/ThemeColorPatch.java +++ b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/theme/ThemeColorPatch.java @@ -277,6 +277,12 @@ public List> getParentSettings() { private static boolean splashScreenThemeApplied; + /** + * Index of the theme the system drew the splash screen of this launch with, + * or -1 if it was given none and drew it with the theme of the launched activity. + */ + private static int launchSplashScreenThemeIndex = -1; + /** * If the system gives the app the overlay manager it needs. Resolved with the first context * of the app, because the API being there is not a promise that the manager is. @@ -483,7 +489,15 @@ private static void resolveThemeColors(Context context) { */ public static void setSplashScreenTheme(Activity activity) { try { - if (splashScreenThemeApplied || Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { + return; + } + + // A theme handed over is used from the next launch on, + // so this launch was drawn with the one handed over before. + launchSplashScreenThemeIndex = SharedYouTubeSettings.THEME_LAST_SPLASH_SCREEN_INDEX.get(); + + if (splashScreenThemeApplied) { return; } splashScreenThemeApplied = true; @@ -495,11 +509,25 @@ public static void setSplashScreenTheme(Activity activity) { Logger.printDebug(() -> "Splash screen theme: " + SPLASH_THEME_NAME + index + " id: " + themeId); activity.getSplashScreen().setSplashScreenTheme(themeId); + SharedYouTubeSettings.THEME_LAST_SPLASH_SCREEN_INDEX.save(index); } catch (Exception ex) { Logger.printException(() -> "setSplashScreenTheme failure", ex); } } + /** + * The theme the system drew the splash screen of this launch with. + * + * @return The theme, or 0 if the system was given none and drew it with the theme + * of the launched activity. + */ + public static int getLaunchSplashScreenTheme() { + final int index = launchSplashScreenThemeIndex; + return index < 0 + ? 0 + : ResourceUtils.getIdentifier(ResourceType.STYLE, SPLASH_THEME_NAME + index); + } + /** * The index of the theme that draws the splash screen of the selected color. *

    diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/VersionCheckPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/VersionCheckPatch.java index 5a68809d05..449744ec43 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/VersionCheckPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/VersionCheckPatch.java @@ -28,4 +28,6 @@ private static boolean isVersionOrGreater(String version) { public static final boolean IS_21_30_OR_GREATER = isVersionOrGreater("21.30.00"); public static final boolean IS_21_36_OR_GREATER = isVersionOrGreater("21.36.00"); + + public static final boolean IS_21_39_OR_GREATER = isVersionOrGreater("21.39.00"); } diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/theme/ThemePatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/theme/ThemePatch.java index 1e1ea1a8de..613590c49f 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/theme/ThemePatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/theme/ThemePatch.java @@ -10,21 +10,36 @@ package app.morphe.extension.youtube.patches.theme; +import android.app.Activity; +import android.content.Context; +import android.content.ContextWrapper; +import android.content.pm.PackageManager; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Color; import android.graphics.drawable.ColorDrawable; +import android.os.Build; +import android.util.TypedValue; import android.view.View; import android.widget.Spinner; import androidx.annotation.ColorInt; +import androidx.annotation.Nullable; import com.airbnb.lottie.LottieAnimationView; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; import java.util.Map; import app.morphe.extension.shared.Logger; +import app.morphe.extension.shared.ResourceType; +import app.morphe.extension.shared.ResourceUtils; import app.morphe.extension.shared.patches.SplashAnimationPatch; import app.morphe.extension.shared.patches.SplashAnimationPatch.SplashScreenAnimationStyle; import app.morphe.extension.shared.settings.SharedYouTubeSettings; import app.morphe.extension.shared.theme.BaseThemePatch; +import app.morphe.extension.shared.theme.ThemeColorPatch; import app.morphe.extension.shared.theme.ThemeUtils; import app.morphe.extension.youtube.settings.Settings; @@ -77,13 +92,206 @@ public static boolean useLotteLaunchSplashScreen(boolean original) { return true; // Force lottie animation view. } + /** + * The glow animation is drawn on a dark square in the middle of the screen, + * and the rest of the screen is given the same color. + */ + @ColorInt + private static final int RIVE_GLOW_ORIGINAL_BACKGROUND_COLOR = 0xFF09090A; + + /** + * The dark square of the glow animation, as it is written in the Rive file: + * a color keyframe (property key 88) followed by the ARGB color in little endian order. + */ + private static final byte[] RIVE_GLOW_BACKGROUND_KEYFRAME = { 0x58, 0x0A, 0x09, 0x09, (byte) 0xFF }; + + /** + * The key the app keeps the Rive file of a raw resource in memory with, followed by the resource id. + */ + private static final String RIVE_RAW_RESOURCE_KEY_PREFIX = "raw_res_"; + + /** + * The color the glow animation is drawn on, or 0 if the animation is not shown. + */ + @ColorInt + private static volatile int riveGlowBackgroundColor; + + private static boolean isRiveSplashScreenEnabled() { + return SharedYouTubeSettings.SPLASH_SCREEN_STYLE.get() == SplashScreenAnimationStyle.RIVE_GLOW; + } + + private static boolean isRiveGlowFile(int resourceId) { + return resourceId == ResourceUtils.getIdentifier(ResourceType.RAW, "rive_phantom"); + } + + /** + * Injection point. + */ + public static boolean useRiveSplashScreen(boolean original) { + return isRiveSplashScreenEnabled(); + } + + /** + * The background color the system drew the splash screen of this launch with. + * + * @return The color, or 0 if it is not a color. + */ + @ColorInt + private static int getSplashScreenBackgroundColor(Activity activity) throws PackageManager.NameNotFoundException { + PackageManager packageManager = activity.getPackageManager(); + + int themeId = ThemeColorPatch.getLaunchSplashScreenTheme(); + if (themeId == 0) { + themeId = packageManager.getActivityInfo(activity.getComponentName(), 0).getThemeResource(); + } + + // The system resolves the theme with the configuration of the device, + // and not with the theme color variant that the contexts of the app use. + Resources.Theme theme = packageManager + .getResourcesForApplication(activity.getApplicationInfo()) + .newTheme(); + theme.applyStyle(themeId, true); + + // Android 12+ uses the splash screen background unless it is transparent, + // and otherwise the window background. + final int[] attributes = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S + ? new int[]{android.R.attr.windowBackground, android.R.attr.windowSplashScreenBackground} + : new int[]{android.R.attr.windowBackground}; + + TypedArray array = theme.obtainStyledAttributes(attributes); + try { + for (int i = attributes.length - 1; i >= 0; i--) { + final int type = array.getType(i); + if (type >= TypedValue.TYPE_FIRST_COLOR_INT && type <= TypedValue.TYPE_LAST_COLOR_INT) { + final int color = array.getColor(i, 0); + if (Color.alpha(color) != 0) { + return color; + } + } + } + return 0; + } finally { + array.recycle(); + } + } + + /** + * Injection point. + * + * @param animationView The view that the Rive animation is drawn in. + */ + public static void setRiveSplashScreenBackground(View animationView) { + try { + if (!isRiveSplashScreenEnabled() || !(animationView.getParent() instanceof View parent)) { + return; + } + + Context context = animationView.getContext(); + while (context instanceof ContextWrapper wrapper && !(context instanceof Activity)) { + context = wrapper.getBaseContext(); + } + + // The glow animation is drawn on the color of the splash screen, so it continues + // it. The Rive file is changed to use the color when it is read. + int color = context instanceof Activity activity + ? getSplashScreenBackgroundColor(activity) + : 0; + if (Color.alpha(color) != 0xFF) { + Logger.printDebug(() -> "Splash screen background is not an opaque color"); + color = RIVE_GLOW_ORIGINAL_BACKGROUND_COLOR; + } + + riveGlowBackgroundColor = color; + parent.setBackgroundColor(color); + } catch (Exception ex) { + Logger.printException(() -> "setRiveSplashScreenBackground failure", ex); + } + } + + /** + * Injection point. + *

    + * The app keeps the Rive file of a raw resource in memory, and the key of the glow animation + * is changed for each background color. Otherwise a file with the background color of + * an earlier launch would be used. + * + * @param key The key of a raw resource, which is the prefix followed by the resource id. + * @return The key to keep the Rive file in memory with. + */ + public static String getRiveRawResourceKey(String key) { + try { + final int backgroundColor = riveGlowBackgroundColor; + if (backgroundColor != 0 + && backgroundColor != RIVE_GLOW_ORIGINAL_BACKGROUND_COLOR + && key.startsWith(RIVE_RAW_RESOURCE_KEY_PREFIX) + && isRiveGlowFile(Integer.parseInt(key.substring(RIVE_RAW_RESOURCE_KEY_PREFIX.length())))) { + return key + "_" + Integer.toHexString(backgroundColor); + } + } catch (Exception ex) { + Logger.printException(() -> "getRiveRawResourceKey failure", ex); + } + + return key; + } + + /** + * Injection point. + *

    + * Called when the app reads the Rive file of a raw resource it has not cached yet. + * The splash screen reads its file after the background is set. + * + * @param fileBytes The Rive file of a raw resource, or null if it could not be read. + * @param resourceId The raw resource the file was read from. + * @return The Rive file to play. + */ + @Nullable + public static byte[] getRiveFile(@Nullable byte[] fileBytes, int resourceId) { + try { + final int backgroundColor = riveGlowBackgroundColor; + if (fileBytes == null + || backgroundColor == 0 + || backgroundColor == RIVE_GLOW_ORIGINAL_BACKGROUND_COLOR + || !isRiveGlowFile(resourceId)) { + return fileBytes; + } + + final int index = indexOf(fileBytes, RIVE_GLOW_BACKGROUND_KEYFRAME); + if (index < 0) { + Logger.printException(() -> "Could not find the background color of the glow animation"); + return fileBytes; + } + + // Replace the color after the property key. + ByteBuffer.wrap(fileBytes, index + 1, Integer.BYTES) + .order(ByteOrder.LITTLE_ENDIAN) + .putInt(backgroundColor); + } catch (Exception ex) { + Logger.printException(() -> "getRiveFile failure", ex); + } + + return fileBytes; + } + + private static int indexOf(byte[] array, byte[] target) { + outer: + for (int i = 0, last = array.length - target.length; i <= last; i++) { + for (int j = 0; j < target.length; j++) { + if (array[i + j] != target[j]) { + continue outer; + } + } + return i; + } + return -1; + } + /** * Injection point. * Modern Lottie style animation. */ public static void setSplashAnimationLottie(LottieAnimationView view, int resourceId) { try { - // A custom branding icon replaces the YouTube logo animation with its own. + // The Morphe styles replace the YouTube logo animation with the Morphe one. if (SplashAnimationPatch.setBrandedSplashAnimation(view)) { return; } @@ -95,8 +303,9 @@ public static void setSplashAnimationLottie(LottieAnimationView view, int resour return; } - // Must specify primary key name otherwise the morphing YT logo color is also changed. - SplashAnimationPatch.setSplashAnimation(view, resourceId, Map.of( + // The logo animates from the same red the progress bar is drawn with, + // so only the fixed colors of the progress bar are replaced. + SplashAnimationPatch.setSplashAnimationFixedColors(view, resourceId, Map.of( "[1,0,0.2,1]", SeekbarColorPatch.getSeekbarColor(), "[1,0.152941176471,0.56862745098,1]", SeekbarColorPatch.getSeekbarAccentColor() )); @@ -109,14 +318,14 @@ public static void setSplashAnimationLottie(LottieAnimationView view, int resour * Injection point. */ public static boolean showSplashScreen(boolean original) { - return SharedYouTubeSettings.SPLASH_SCREEN_ANIMATION_STYLE.get() != SplashScreenAnimationStyle.DISABLED && original; + return SharedYouTubeSettings.SPLASH_SCREEN_STYLE.get() != SplashScreenAnimationStyle.DISABLED && original; } /** * Injection point. */ public static int showSplashScreen(int i, int i2) { - if (SharedYouTubeSettings.SPLASH_SCREEN_ANIMATION_STYLE.get() != SplashScreenAnimationStyle.DISABLED || i != i2) { + if (SharedYouTubeSettings.SPLASH_SCREEN_STYLE.get() != SplashScreenAnimationStyle.DISABLED || i != i2) { return i; } return i - 1; @@ -126,7 +335,7 @@ public static int showSplashScreen(int i, int i2) { * Injection point. */ public static int getLoadingScreenType(int original) { - SplashScreenAnimationStyle style = SharedYouTubeSettings.SPLASH_SCREEN_ANIMATION_STYLE.get(); + SplashScreenAnimationStyle style = SharedYouTubeSettings.SPLASH_SCREEN_STYLE.get(); if (style == SplashScreenAnimationStyle.DISABLED) { return original; diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/Settings.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/Settings.java index 8593f8582b..6265647deb 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/Settings.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/Settings.java @@ -18,6 +18,7 @@ import app.morphe.extension.shared.StringRef; import app.morphe.extension.shared.patches.CustomFontPatch.FontType; import app.morphe.extension.shared.patches.DownloadMode; +import app.morphe.extension.shared.patches.SplashAnimationPatch.SplashScreenAnimationStyle; import app.morphe.extension.shared.settings.BooleanSetting; import app.morphe.extension.shared.settings.EnumSetting; import app.morphe.extension.shared.settings.FloatSetting; @@ -1042,6 +1043,13 @@ public class Settings extends SharedYouTubeSettings { RESTORE_OLD_PLAYER_BUTTONS.resetToDefault(); } + // The Rive startup animation requires 21.39+. + if (!VersionCheckPatch.IS_21_39_OR_GREATER + && SharedYouTubeSettings.SPLASH_SCREEN_STYLE.get() == SplashScreenAnimationStyle.RIVE_GLOW) { + Logger.printInfo(() -> "Resetting Rive splash screen animation style"); + SharedYouTubeSettings.SPLASH_SCREEN_STYLE.resetToDefault(); + } + // Android XR, Android XR Downgraded, and visionOS 1.03 are not selectable in the settings and are selected by spoof stream patch if needed. ClientType client = SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get(); if (client == ClientType.ANDROID_XR_SABR || client == ClientType.ANDROID_XR_DASH || client == ClientType.VISIONOS_1_03) { diff --git a/patches/src/main/kotlin/app/morphe/patches/music/layout/branding/CustomBrandingPatch.kt b/patches/src/main/kotlin/app/morphe/patches/music/layout/branding/CustomBrandingPatch.kt index 866c39685a..2ac9593ce1 100644 --- a/patches/src/main/kotlin/app/morphe/patches/music/layout/branding/CustomBrandingPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/music/layout/branding/CustomBrandingPatch.kt @@ -25,6 +25,7 @@ import app.morphe.patches.music.shared.Constants.COMPATIBILITY_YOUTUBE_MUSIC import app.morphe.patches.music.shared.MusicActivityOnCreateFingerprint import app.morphe.patches.shared.layout.branding.EXTENSION_CLASS import app.morphe.patches.shared.layout.branding.baseCustomBrandingPatch +import app.morphe.patches.shared.layout.branding.startupAnimationResourcePatch import app.morphe.patches.shared.misc.lottie.LOTTIE_ANIMATION_VIEW_CLASS_TYPE import app.morphe.patches.shared.misc.lottie.lottieAnimationPatch import app.morphe.patches.shared.misc.settings.preference.ListPreference @@ -38,16 +39,14 @@ private const val STARTUP_ANIMATION_EXTENSION_CLASS = "Lapp/morphe/extension/music/patches/StartupAnimationPatch;" private val startupAnimationPatch = bytecodePatch { - dependsOn(settingsPatch, lottieAnimationPatch) + dependsOn(settingsPatch, lottieAnimationPatch, startupAnimationResourcePatch) execute { PreferenceScreen.GENERAL.addPreferences( - ListPreference("morphe_splash_screen_animation_style") + ListPreference("morphe_splash_screen_style") ) - // The original animation starts with the original logo, which would flash on screen - // before a branded app. An icon with its own animation plays that instead, and the - // animation is turned off for a user provided icon. + // The style decides if the original animation, the Morphe animation or none is played. CairoSplashAnimationConfigFingerprint.let { it.method.apply { // The extension decides which animation is played and with which colors. diff --git a/patches/src/main/kotlin/app/morphe/patches/shared/layout/branding/BaseCustomBrandingPatch.kt b/patches/src/main/kotlin/app/morphe/patches/shared/layout/branding/BaseCustomBrandingPatch.kt index bb83987bb1..42d502e1a4 100644 --- a/patches/src/main/kotlin/app/morphe/patches/shared/layout/branding/BaseCustomBrandingPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/shared/layout/branding/BaseCustomBrandingPatch.kt @@ -41,6 +41,7 @@ import app.morphe.util.copyResources import app.morphe.util.findElementByAttributeValueOrThrow import app.morphe.util.inputStreamFromBundledResource import app.morphe.util.removeFromParent +import app.morphe.util.setExtensionIsPatchIncluded import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction import org.w3c.dom.Element @@ -224,6 +225,9 @@ internal fun baseCustomBrandingPatch( fixRecycledBitmapPatch, bytecodePatch { execute { + // Custom branding changes the default splash screen style to the Morphe animation. + setExtensionIsPatchIncluded(EXTENSION_CLASS) + mainActivityOnCreateFingerprint.method.addInstruction( 0, "invoke-static { }, $EXTENSION_CLASS->setBranding()V" @@ -565,17 +569,6 @@ internal fun baseCustomBrandingPatch( ) ) - // Startup animation of each launcher foreground, played by the app hooks - // in place of the original one. - copyResources( - "custom-branding", - ResourceGroup( - "raw", - "morphe_startup_animation.json", - "morphe_startup_animation_play.json" - ) - ) - // Copy template icon files. mipmapDirectories.keys.forEach { dpi -> copyResources( diff --git a/patches/src/main/kotlin/app/morphe/patches/shared/layout/branding/StartupAnimationResourcePatch.kt b/patches/src/main/kotlin/app/morphe/patches/shared/layout/branding/StartupAnimationResourcePatch.kt new file mode 100644 index 0000000000..d7ba02fb0e --- /dev/null +++ b/patches/src/main/kotlin/app/morphe/patches/shared/layout/branding/StartupAnimationResourcePatch.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/4109 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.patches.shared.layout.branding + +import app.morphe.patcher.patch.resourcePatch +import app.morphe.util.ResourceGroup +import app.morphe.util.copyResources + +/** + * Adds the Morphe startup animation of each launcher foreground, played by the + * Morphe splash screen styles in place of the original animation. + */ +val startupAnimationResourcePatch = resourcePatch { + execute { + copyResources( + "custom-branding", + ResourceGroup( + "raw", + "morphe_startup_animation.json", + "morphe_startup_animation_play.json" + ) + ) + } +} diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/theme/Fingerprints.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/theme/Fingerprints.kt index dfcd21dc0d..520576a26a 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/theme/Fingerprints.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/theme/Fingerprints.kt @@ -14,12 +14,16 @@ import app.morphe.patcher.Fingerprint import app.morphe.patcher.InstructionLocation.MatchAfterImmediately import app.morphe.patcher.InstructionLocation.MatchAfterWithin import app.morphe.patcher.anyInstruction +import app.morphe.patcher.checkCast +import app.morphe.patcher.fieldAccess import app.morphe.patcher.literal import app.morphe.patcher.methodCall import app.morphe.patcher.opcode import app.morphe.patcher.resource.ResourceType import app.morphe.patcher.resourceLiteral +import app.morphe.patcher.string import app.morphe.patches.youtube.shared.YOUTUBE_MAIN_ACTIVITY_CLASS_TYPE +import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode internal object UseGradientLoadingScreenFingerprint : Fingerprint( @@ -47,6 +51,137 @@ internal object LithoImageColorFilterFingerprint : Fingerprint( returnType = "V" ) +/** + * Rive startup animation, which is played instead of the Lottie animation. + */ +internal object RiveSplashScreenFeatureFlagFingerprint : Fingerprint( + filters = listOf( + literal(1073830295L) + ) +) + +internal const val RIVE_THEMED_SPLASH_SCREEN_FEATURE_FLAG = 1073830411L + +/** + * Rive animation 'rive_themed_startup_animation', of a logo that turns into a play button. + */ +internal object RiveThemedSplashScreenFeatureFlagFingerprint : Fingerprint( + filters = listOf( + literal(RIVE_THEMED_SPLASH_SCREEN_FEATURE_FLAG) + ) +) + +/** + * 21.39+ Rive animation 'rive_phantom', of a logo that comes into focus out of a glow. + * In earlier versions this flag selects a different Rive view. + */ +internal object RiveGlowSplashScreenFeatureFlagFingerprint : Fingerprint( + filters = listOf( + literal(1073840124L) + ) +) + +/** + * Creates the startup animation view, and sets the field that the Rive animation is played. + */ +internal object RiveSplashScreenViewFingerprint : Fingerprint( + returnType = "Landroid/view/View;", + filters = listOf( + literal(1073830295L), + fieldAccess( + opcode = Opcode.IPUT_BOOLEAN, + type = "Z" + ), + fieldAccess( + opcode = Opcode.IPUT_BOOLEAN, + type = "Z", + location = MatchAfterImmediately() + ), + resourceLiteral( + ResourceType.LAYOUT, + "main_activity_modern_rive_startup_animation", + location = MatchAfterWithin(5) + ) + ) +) + +/** + * 21.39+ Sets the view that the Rive animation is drawn in. + */ +internal object RiveSplashScreenAnimationViewFingerprint : Fingerprint( + definingClass = YOUTUBE_MAIN_ACTIVITY_CLASS_TYPE, + name = "onCreate", + returnType = "V", + parameters = listOf("Landroid/os/Bundle;"), + filters = listOf( + resourceLiteral(ResourceType.ID, "rive_animation"), + methodCall( + name = "findViewById", + location = MatchAfterImmediately() + ), + opcode( + opcode = Opcode.MOVE_RESULT_OBJECT, + location = MatchAfterImmediately() + ), + checkCast( + "Landroid/widget/ImageView;", + location = MatchAfterImmediately() + ), + fieldAccess( + opcode = Opcode.IPUT_OBJECT, + type = "Landroid/widget/ImageView;", + location = MatchAfterImmediately() + ) + ) +) + +/** + * Closes the startup animation as soon as the home feed is shown. + */ +internal object SplashScreenOnStartFingerprint : Fingerprint( + classFingerprint = RiveSplashScreenViewFingerprint, + name = "onStart", + returnType = "V", + filters = listOf( + literal(1073815019L), + literal(1073828809L) + ) +) + +/** + * Creates the source of a Rive file that is a raw resource, + * with the key the app keeps the file in memory with. + */ +internal object RiveRawResourceSourceFingerprint : Fingerprint( + accessFlags = listOf(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR), + parameters = listOf("I"), + filters = listOf( + string("raw_res_"), + fieldAccess( + opcode = Opcode.IPUT_OBJECT, + type = "Ljava/lang/String;", + location = MatchAfterWithin(3) + ), + string("resId must not be 0") + ) +) + +/** + * Reads the Rive file of a raw resource. + */ +internal object RiveReadRawResourceFingerprint : Fingerprint( + returnType = "[B", + parameters = listOf("I"), + filters = listOf( + string("readRawResource"), + methodCall(smali = "Landroid/content/res/Resources;->openRawResource(I)Ljava/io/InputStream;"), + opcode( + opcode = Opcode.RETURN_OBJECT, + location = MatchAfterWithin(5) + ) + ) +) + internal object LottieLayerColorFingerprint : Fingerprint( parameters = listOf( "Landroid/graphics/Canvas;", diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/theme/SplashAnimationPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/theme/SplashAnimationPatch.kt index ca0ce0af81..b6846183b3 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/theme/SplashAnimationPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/theme/SplashAnimationPatch.kt @@ -1,27 +1,43 @@ /* * Copyright 2026 Morphe. * https://github.com/MorpheApp/morphe-patches/pull/3178 + * https://github.com/MorpheApp/morphe-patches/pull/4109 * * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. */ package app.morphe.patches.youtube.layout.theme +import app.morphe.patcher.extensions.InstructionExtensions.addInstruction +import app.morphe.patcher.extensions.InstructionExtensions.addInstructions +import app.morphe.patcher.extensions.InstructionExtensions.addInstructionsWithLabels +import app.morphe.patcher.extensions.InstructionExtensions.getInstruction import app.morphe.patcher.extensions.InstructionExtensions.replaceInstruction +import app.morphe.patcher.literal import app.morphe.patcher.methodCall import app.morphe.patcher.patch.bytecodePatch +import app.morphe.patcher.util.getReference +import app.morphe.patcher.util.smali.ExternalLabel +import app.morphe.patches.shared.layout.branding.startupAnimationResourcePatch import app.morphe.patches.shared.misc.lottie.LOTTIE_ANIMATION_VIEW_CLASS_TYPE import app.morphe.patches.shared.misc.lottie.LottieAnimationViewSetAnimationIntFingerprint import app.morphe.patches.shared.misc.lottie.lottieAnimationPatch +import app.morphe.patches.shared.misc.settings.preference.ListPreference import app.morphe.patches.youtube.layout.seekbar.LottieSplashScreenFeatureFlagFingerprint import app.morphe.patches.youtube.layout.seekbar.LottieSplashScreenFeatureFlagLegacyFingerprint import app.morphe.patches.youtube.misc.extension.sharedExtensionPatch import app.morphe.patches.youtube.misc.playservice.is_20_31_or_greater +import app.morphe.patches.youtube.misc.playservice.is_21_39_or_greater import app.morphe.patches.youtube.misc.playservice.versionCheckPatch +import app.morphe.patches.youtube.misc.settings.PreferenceScreen +import app.morphe.patches.youtube.misc.settings.settingsPatch import app.morphe.patches.youtube.shared.YouTubeActivityOnCreateFingerprint import app.morphe.util.findInstructionIndicesReversedOrThrow import app.morphe.util.fiveRegisters import app.morphe.util.insertLiteralOverride +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction +import com.android.tools.smali.dexlib2.iface.reference.FieldReference private const val EXTENSION_CLASS = "Lapp/morphe/extension/youtube/patches/theme/ThemePatch;" @@ -32,9 +48,64 @@ private const val EXTENSION_CLASS = */ val splashAnimationPatch = bytecodePatch { - dependsOn(sharedExtensionPatch, lottieAnimationPatch, versionCheckPatch) + dependsOn( + sharedExtensionPatch, + settingsPatch, + lottieAnimationPatch, + versionCheckPatch, + startupAnimationResourcePatch + ) execute { + // 21.39+ also has the glow Rive animation. + val arrayKeySuffix = if (is_21_39_or_greater) "_21_39" else "" + + PreferenceScreen.GENERAL.addPreferences( + ListPreference( + key = "morphe_splash_screen_style", + entriesKey = "morphe_splash_screen_style${arrayKeySuffix}_entries", + entryValuesKey = "morphe_splash_screen_style${arrayKeySuffix}_entry_values" + ) + ) + + // Lottie splash screen exists in earlier versions, but it may not be always on. + SplashScreenStyleFingerprint.let { + it.method.insertLiteralOverride( + it.instructionMatches.first().index, + "$EXTENSION_CLASS->getLoadingScreenType(I)I" + ) + } + + ShowSplashScreenFingerprint.let { + it.method.apply { + val lastIndex = it.instructionMatches.last().index + val lastInstruction = getInstruction(lastIndex) + val lastRegisterA = lastInstruction.registerA + val lastRegisterB = lastInstruction.registerB + + addInstructions( + lastIndex, + """ + invoke-static { v$lastRegisterA, v$lastRegisterB }, $EXTENSION_CLASS->showSplashScreen(II)I + move-result v$lastRegisterA + """ + ) + + val firstIndex = it.instructionMatches[1].index + val firstRegister = getInstruction( + firstIndex + ).registerA + + addInstructions( + firstIndex + 1, + """ + invoke-static { v$firstRegister }, $EXTENSION_CLASS->showSplashScreen(Z)Z + move-result v$firstRegister + """ + ) + } + } + // Force newer Lottie animation. (if (is_20_31_or_greater) { LottieSplashScreenFeatureFlagFingerprint @@ -47,6 +118,94 @@ val splashAnimationPatch = bytecodePatch { ) } + if (is_21_39_or_greater) { + arrayOf( + RiveSplashScreenFeatureFlagFingerprint, + RiveGlowSplashScreenFeatureFlagFingerprint + ).forEach { fingerprint -> + fingerprint.matchAll().forEach { + it.method.insertLiteralOverride( + it.instructionMatches.first().index, + "$EXTENSION_CLASS->useRiveSplashScreen(Z)Z" + ) + } + } + + // The themed Rive animation is used instead of the glow animation if it is on. + RiveThemedSplashScreenFeatureFlagFingerprint.matchAll().forEach { + it.method.apply { + findInstructionIndicesReversedOrThrow( + literal(RIVE_THEMED_SPLASH_SCREEN_FEATURE_FLAG) + ).forEach { index -> + insertLiteralOverride(index, false) + } + } + } + + RiveSplashScreenAnimationViewFingerprint.let { + it.method.apply { + val index = it.instructionMatches.last().index + val viewRegister = getInstruction(index).registerA + + addInstruction( + index + 1, + "invoke-static/range { v$viewRegister .. v$viewRegister }, " + + "$EXTENSION_CLASS->setRiveSplashScreenBackground(Landroid/view/View;)V" + ) + } + } + + // The backdrop of the glow animation is changed to the color of the splash screen + // when the file is read, and the app keeps the file in memory with a key of that color. + RiveRawResourceSourceFingerprint.let { + it.method.apply { + val index = it.instructionMatches[1].index + val register = getInstruction(index).registerA + + addInstructions( + index, + """ + invoke-static { v$register }, $EXTENSION_CLASS->getRiveRawResourceKey(Ljava/lang/String;)Ljava/lang/String; + move-result-object v$register + """ + ) + } + } + + RiveReadRawResourceFingerprint.let { + it.method.apply { + val index = it.instructionMatches.last().index + val register = getInstruction(index).registerA + + addInstructions( + index, + """ + invoke-static { v$register, p1 }, $EXTENSION_CLASS->getRiveFile([BI)[B + move-result-object v$register + """ + ) + } + } + + // The Rive animation is loaded in the background, and a home feed that is + // shown quickly closes the animation before it has drawn anything. The animation + // is instead closed when it ends, or by the timeout of the app if it never does. + val riveAnimationShownField = RiveSplashScreenViewFingerprint.instructionMatches[1] + .instruction.getReference()!! + + SplashScreenOnStartFingerprint.method.apply { + addInstructionsWithLabels( + 0, + """ + iget-boolean v0, p0, $riveAnimationShownField + if-eqz v0, :close_on_home_feed + return-void + """, + ExternalLabel("close_on_home_feed", getInstruction(0)) + ) + } + } + YouTubeActivityOnCreateFingerprint.method.apply { val setAnimationIntMethodName = LottieAnimationViewSetAnimationIntFingerprint.originalMethod.name diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/theme/ThemePatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/theme/ThemePatch.kt index 078e931045..2a3bc04a61 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/theme/ThemePatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/theme/ThemePatch.kt @@ -2,6 +2,7 @@ * Copyright 2026 Morphe. * https://github.com/MorpheApp/morphe-patches/pull/2524 * https://github.com/MorpheApp/morphe-patches/pull/3684 + * https://github.com/MorpheApp/morphe-patches/pull/4109 * * Original hard forked code: * https://github.com/ReVanced/revanced-patches/commit/724e6d61b2ecd868c1a9a37d465a688e83a74799 @@ -58,7 +59,6 @@ import app.morphe.util.findInstructionIndicesReversedOrThrow import app.morphe.util.forEachChildElement import app.morphe.util.insertLiteralOverride import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction -import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction private const val EXTENSION_CLASS = "Lapp/morphe/extension/youtube/patches/theme/ThemePatch;" @@ -461,10 +461,6 @@ val themePatch = baseThemePatch( noTitleUnsortedPreferenceCategory(preferences) ) - PreferenceScreen.GENERAL.addPreferences( - ListPreference("morphe_splash_screen_animation_style") - ) - // Splash screen is drawn by the system with the theme of the launcher activity, so // the activity is handed over as soon as it exists. A patched theme color is // already a part of that theme, and no theme is generated to hand over. @@ -614,44 +610,6 @@ val themePatch = baseThemePatch( } } - // Lottie splash screen exists in earlier versions, but it may not be always on. - SplashScreenStyleFingerprint.let { - it.method.insertLiteralOverride( - it.instructionMatches.first().index, - "$EXTENSION_CLASS->getLoadingScreenType(I)I" - ) - } - - ShowSplashScreenFingerprint.let { - it.method.apply { - val lastIndex = it.instructionMatches.last().index - val lastInstruction = getInstruction(lastIndex) - val lastRegisterA = lastInstruction.registerA - val lastRegisterB = lastInstruction.registerB - - addInstructions( - lastIndex, - """ - invoke-static { v$lastRegisterA, v$lastRegisterB }, $EXTENSION_CLASS->showSplashScreen(II)I - move-result v$lastRegisterA - """ - ) - - val firstIndex = it.instructionMatches[1].index - val firstRegister = getInstruction( - firstIndex - ).registerA - - addInstructions( - firstIndex + 1, - """ - invoke-static { v$firstRegister }, $EXTENSION_CLASS->showSplashScreen(Z)Z - move-result v$firstRegister - """ - ) - } - } - // Popup background of drop-down menus (Spinners) falls back to the Android OS // default, which remains gray and does not go with a custom theme color. SpinnerThemeHookFingerprint.matchAll().forEach { match -> diff --git a/patches/src/main/resources/addresources/values/shared-youtube/arrays.xml b/patches/src/main/resources/addresources/values/shared-youtube/arrays.xml index 5634a9999c..342388cc56 100644 --- a/patches/src/main/resources/addresources/values/shared-youtube/arrays.xml +++ b/patches/src/main/resources/addresources/values/shared-youtube/arrays.xml @@ -15,17 +15,22 @@ - - @string/morphe_splash_screen_animation_style_entry_1 - @string/morphe_splash_screen_animation_style_entry_2 - @string/morphe_splash_screen_animation_style_entry_3 - - + + @string/morphe_splash_screen_style_entry_1 + @string/morphe_splash_screen_style_entry_2 + @string/morphe_splash_screen_style_entry_3 + @string/morphe_splash_screen_style_entry_4 + @string/morphe_splash_screen_style_entry_5 + + DISABLED FPS_60_ONE_SECOND + MORPHE_COLOR FPS_60_BLACK_AND_WHITE + MORPHE_MONOCHROME diff --git a/patches/src/main/resources/addresources/values/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values/shared-youtube/strings.xml index 306469b451..0f1e025ff6 100644 --- a/patches/src/main/resources/addresources/values/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values/shared-youtube/strings.xml @@ -17,10 +17,12 @@ Second \"item\" text" --> - Startup animation - Disabled - Color - Monochrome + Startup animation + Disabled + Color + Color Morphe + Monochrome + Monochrome Morphe Restart to finish setting up diff --git a/patches/src/main/resources/addresources/values/youtube/arrays.xml b/patches/src/main/resources/addresources/values/youtube/arrays.xml index e7b131b9ca..66a4ff6f64 100644 --- a/patches/src/main/resources/addresources/values/youtube/arrays.xml +++ b/patches/src/main/resources/addresources/values/youtube/arrays.xml @@ -287,6 +287,23 @@ + + + @string/morphe_splash_screen_style_entry_1 + @string/morphe_splash_screen_style_entry_2 + @string/morphe_splash_screen_style_entry_3 + @string/morphe_splash_screen_style_entry_4 + @string/morphe_splash_screen_style_entry_5 + @string/morphe_splash_screen_style_entry_6 + + + DISABLED + FPS_60_ONE_SECOND + MORPHE_COLOR + FPS_60_BLACK_AND_WHITE + MORPHE_MONOCHROME + RIVE_GLOW + @string/morphe_theme_color_entry_1 @string/morphe_theme_color_light_entry_2 diff --git a/patches/src/main/resources/addresources/values/youtube/strings.xml b/patches/src/main/resources/addresources/values/youtube/strings.xml index ae15f26b31..47f4234365 100644 --- a/patches/src/main/resources/addresources/values/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values/youtube/strings.xml @@ -1366,6 +1366,7 @@ Limitation: If \'Restore old video action bar\' is turned off, \'More videos\' o Keeps the layout of the status bar consistent, so it does not cover the top bars and leaves no empty space above the player. Turn this off if the app crashes or the top bars look wrong + Glow Light theme background White Material You (White) From de9e6e4e7953f6192991e35025ed644806948ecd Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Tue, 27 Oct 2026 18:26:06 +0100 Subject: [PATCH 110/189] feat(YouTube - Custom branding): Add experimental YouTube launcher icons (#4114) --- .../shared/patches/CustomBrandingPatch.java | 54 +++++++++++++++++-- .../preference/IconListPreference.java | 48 +++++++++-------- .../NotificationIconListPreference.java | 8 ++- .../branding/BaseCustomBrandingPatch.kt | 31 +++++++++-- .../layout/branding/CustomBrandingPatch.kt | 21 +++++++- .../addresources/values/youtube/arrays.xml | 47 ++++++++++++++++ .../addresources/values/youtube/strings.xml | 3 ++ 7 files changed, 179 insertions(+), 33 deletions(-) diff --git a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/patches/CustomBrandingPatch.java b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/patches/CustomBrandingPatch.java index 00e2fe5666..e2ddb0c4f6 100644 --- a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/patches/CustomBrandingPatch.java +++ b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/patches/CustomBrandingPatch.java @@ -1,6 +1,7 @@ /* * Copyright 2026 Morphe. * https://github.com/MorpheApp/morphe-patches/pull/2518 + * https://github.com/MorpheApp/morphe-patches/pull/4114 * * Original hard forked code: * https://github.com/ReVanced/revanced-patches/commit/724e6d61b2ecd868c1a9a37d465a688e83a74799 @@ -55,7 +56,15 @@ public enum BrandingTheme { PLAY, PLAY_BLACK, /** User provided custom icon. */ - CUSTOM; + CUSTOM, + /** + * Launcher icons of YouTube 21.41 and newer. The launch aliases only exist if the app + * has these icons. If YouTube removes an icon, the alias must stay with a dummy icon. + * Numbered like the icons of the app, so a theme can be reused if YouTube replaces an icon. + */ + YOUTUBE_ICON1, // Neon + YOUTUBE_ICON2, // Cake + YOUTUBE_ICON3; // Metal private String packageAndNameIndexToClassAlias(String packageName, int appIndex) { if (appIndex <= 0) { @@ -72,12 +81,13 @@ private String packageAndNameIndexToClassAlias(String packageName, int appIndex) * that can be overridden at patch time with either an XML or a PNG supplied by * the user. *

    - * Returns {@code null} for {@link #ORIGINAL} (no override desired). + * Returns {@code null} for {@link #ORIGINAL} and the launcher icons of the app, + * which keep the original notification icon. */ @Nullable String notificationIconResourceName() { return switch (this) { - case ORIGINAL -> null; + case ORIGINAL, YOUTUBE_ICON1, YOUTUBE_ICON2, YOUTUBE_ICON3 -> null; case CUSTOM -> "morphe_notification_icon_custom"; // LIGHT, DARK, BLACK, PLAY – each has its own bundled XML. default ->"morphe_notification_icon_" + name().toLowerCase(Locale.US); @@ -88,7 +98,8 @@ String notificationIconResourceName() { * Returns the raw resource name of the startup animation for this theme. *

    * Themes that share a launcher foreground share an animation. Returns {@code null} for - * {@link #ORIGINAL} and {@link #CUSTOM}, which have no animation of their own. + * {@link #ORIGINAL}, {@link #CUSTOM} and the launcher icons of the app, which have no + * animation of their own. */ @Nullable String startupAnimationResourceName() { @@ -98,6 +109,30 @@ String startupAnimationResourceName() { default -> null; }; } + + /** + * Returns the mipmap resource name of the launcher icon of the app this theme uses, + * or {@code null} if the theme uses an icon bundled with the patches. + */ + @Nullable + String appLauncherIconResourceName() { + return switch (this) { + case ORIGINAL -> originalLauncherIconName(); + case YOUTUBE_ICON1 -> "ic_launcher_icon1"; + case YOUTUBE_ICON2 -> "ic_launcher_icon2"; + case YOUTUBE_ICON3 -> "ic_launcher_icon3"; + default -> null; + }; + } + + /** + * If the app has the launch aliases of this theme. The launcher icons of the app only + * exist in some apps and app versions, and patching adds their aliases only if they do. + */ + boolean isAvailable() { + String iconName = appLauncherIconResourceName(); + return iconName == null || ResourceUtils.getIdentifier(ResourceType.MIPMAP, iconName) != 0; + } } /** @@ -449,6 +484,16 @@ public static void setBranding() { List componentsToDisable = new ArrayList<>(); for (BrandingTheme theme : BrandingTheme.values()) { + // An unavailable theme has no aliases, and if it was selected the branding is reset. + if (!theme.isAvailable()) { + continue; + } + + String launcherIconName = theme.appLauncherIconResourceName(); + if (launcherIconName != null) { + IconListPreference.setAppLauncherIconName(theme.name(), launcherIconName); + } + // Must always update all aliases including custom alias (last index). final int numberOfPresetAppNames = numberOfPresetAppNames(); @@ -481,7 +526,6 @@ public static void setBranding() { componentsToDisable.remove(defaultComponent); } - IconListPreference.setOriginalLauncherIconName(originalLauncherIconName()); IconListPreference.setOriginalNotificationIconName(originalNotificationIconName()); // Reset cached notification icon so it is re-resolved with the current theme diff --git a/extensions/shared/library/src/main/java/app/morphe/extension/shared/settings/preference/IconListPreference.java b/extensions/shared/library/src/main/java/app/morphe/extension/shared/settings/preference/IconListPreference.java index 9fb7b6fb47..ddddd0bb2c 100644 --- a/extensions/shared/library/src/main/java/app/morphe/extension/shared/settings/preference/IconListPreference.java +++ b/extensions/shared/library/src/main/java/app/morphe/extension/shared/settings/preference/IconListPreference.java @@ -1,6 +1,6 @@ /* * Copyright 2026 Morphe. - * https://github.com/MorpheApp/morphe-patches + * https://github.com/MorpheApp/morphe-patches/pull/4114 * * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. */ @@ -11,9 +11,9 @@ import android.content.Context; import android.content.res.Resources; import android.graphics.Bitmap; -import android.graphics.drawable.BitmapDrawable; import android.graphics.Canvas; import android.graphics.Path; +import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.LayerDrawable; import android.os.Bundle; @@ -32,9 +32,12 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import java.util.HashMap; import java.util.Locale; +import java.util.Map; import java.util.Objects; +import app.morphe.extension.shared.Logger; import app.morphe.extension.shared.ResourceType; import app.morphe.extension.shared.ResourceUtils; import app.morphe.extension.shared.theme.ThemeUtils; @@ -48,7 +51,7 @@ * Icons are resolved by convention: * {@code morphe_adaptive_background_{value}} and {@code morphe_adaptive_foreground_{value}} * are layered and rendered to a rounded bitmap. Entries with no matching resources - * fall back to the app's launcher icon. + * use a launcher icon of the app, such as the original icon. */ @SuppressWarnings({"unused", "deprecation"}) public class IconListPreference extends CustomDialogListPreference { @@ -58,15 +61,18 @@ public class IconListPreference extends CustomDialogListPreference { static final float ICON_SIZE_DP = 48f; static final float ICON_CORNER_RADIUS_FRACTION = 0.22f; - @Nullable - private static String originalLauncherIconName; + /** + * Mipmap resource names of the launcher icons of the app, such as the original unpatched + * icon, by the lowercase entry value that uses them. + */ + private static final Map appLauncherIconNames = new HashMap<>(); @Nullable private static String originalNotificationIconName; /** Called from CustomBrandingPatch.setBranding() at app startup. */ - public static void setOriginalLauncherIconName(@Nullable String name) { - originalLauncherIconName = name; + public static void setAppLauncherIconName(String entryValue, String name) { + appLauncherIconNames.put(entryValue.toLowerCase(Locale.US), name); } /** Called from CustomBrandingPatch.setBranding() at app startup. */ @@ -80,19 +86,18 @@ static String getOriginalNotificationIconName() { } /** - * Resolves the original unpatched launcher icon drawable. - * Uses the resource name injected at patch time; falls back to the current app icon. + * Resolves the launcher icon of the app that an entry uses, such as the original unpatched icon. + * Uses the resource name injected at app startup; falls back to the current app icon. */ - static Drawable resolveOriginalIconDrawable(Context context) { - try { - if (originalLauncherIconName != null && !originalLauncherIconName.isEmpty()) { - int resId = ResourceUtils.getIdentifier(ResourceType.MIPMAP, originalLauncherIconName); - if (resId != 0) { - Drawable drawable = context.getDrawable(resId); - if (drawable != null) return drawable; - } + static Drawable resolveAppLauncherIconDrawable(Context context, String suffix) { + String iconName = appLauncherIconNames.get(suffix); + if (iconName != null && !iconName.isEmpty()) { + int resId = ResourceUtils.getIdentifier(ResourceType.MIPMAP, iconName); + if (resId != 0) { + Drawable drawable = context.getDrawable(resId); + if (drawable != null) return drawable; } - } catch (Exception ignored) {} + } return context.getPackageManager().getApplicationIcon(context.getApplicationInfo()); } @@ -167,14 +172,15 @@ private static Drawable buildIconDrawable( if (bg != null && fg != null) { source = new LayerDrawable(new Drawable[]{bg, fg}); } else { - // No morphe assets (e.g. ORIGINAL) - use the original unpatched launcher icon. + // No morphe assets (e.g. ORIGINAL) - use the launcher icon of the app. source = Objects.requireNonNullElseGet(fg, () -> Objects.requireNonNullElseGet(bg, - () -> resolveOriginalIconDrawable(context))); + () -> resolveAppLauncherIconDrawable(context, suffix))); } return renderToRounded(context.getResources(), source, sizePx, cornerRadius, isAdaptive); - } catch (Exception e) { + } catch (Exception ex) { + Logger.printInfo(() -> "buildIconDrawable failure: " + suffix, ex); return null; } } diff --git a/extensions/shared/library/src/main/java/app/morphe/extension/shared/settings/preference/NotificationIconListPreference.java b/extensions/shared/library/src/main/java/app/morphe/extension/shared/settings/preference/NotificationIconListPreference.java index c61b40ebf6..204ae756c6 100644 --- a/extensions/shared/library/src/main/java/app/morphe/extension/shared/settings/preference/NotificationIconListPreference.java +++ b/extensions/shared/library/src/main/java/app/morphe/extension/shared/settings/preference/NotificationIconListPreference.java @@ -1,6 +1,6 @@ /* * Copyright 2026 Morphe. - * https://github.com/MorpheApp/morphe-patches + * https://github.com/MorpheApp/morphe-patches/pull/4114 * * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. */ @@ -84,7 +84,11 @@ protected Drawable[] resolveIconDrawables() { for (int i = 0; i < values.length; i++) { String suffix = values[i].toString().toLowerCase(Locale.US); if ("follow".equals(suffix) && currentAppIconSuffix != null) { - suffix = currentAppIconSuffix; + // An app icon without a notification icon of its own, such as a launcher icon + // of the app, keeps the original notification icon. + suffix = resolveResId("morphe_notification_icon_" + currentAppIconSuffix) != 0 + ? currentAppIconSuffix + : "original"; } drawables[i] = buildNotificationIconDrawable(context, suffix, sizePx, cornerRadius, fgColor, bgColor); } diff --git a/patches/src/main/kotlin/app/morphe/patches/shared/layout/branding/BaseCustomBrandingPatch.kt b/patches/src/main/kotlin/app/morphe/patches/shared/layout/branding/BaseCustomBrandingPatch.kt index 42d502e1a4..a9b7edc810 100644 --- a/patches/src/main/kotlin/app/morphe/patches/shared/layout/branding/BaseCustomBrandingPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/shared/layout/branding/BaseCustomBrandingPatch.kt @@ -138,6 +138,10 @@ private val logger = Logger.getLogger(ResourcePatchContext::class.java.name) /** * Shared custom branding patch for YouTube and YT Music. + * + * @param appLauncherIconNames Launcher icons of the app that can be selected in the app settings, + * as the icon style name and the mipmap resource name. They are not patch option values, + * because older app versions do not have them. */ internal fun baseCustomBrandingPatch( originalLauncherIconName: String, @@ -150,6 +154,7 @@ internal fun baseCustomBrandingPatch( mainActivityName: String, activityAliasNameWithIntents: String, preferenceScreen: BasePreferenceScreen.Screen, + appLauncherIconNames: () -> Map = { emptyMap() }, block: ResourcePatchBuilder.() -> Unit, executeBlock: ResourcePatchContext.() -> Unit = {} ): ResourcePatch = resourcePatch( @@ -434,6 +439,21 @@ internal fun baseCustomBrandingPatch( ) } + // Launcher icons of the app. Not enabled here, because they can only be + // selected in the app settings. + appLauncherIconNames().forEach { (style, iconMipmapName) -> + application.appendChild( + createAlias( + aliasName = aliasName(style), + iconMipmapName = iconMipmapName, + appNameIndex = appNameIndex, + useCustomName = useCustomNameLabel, + enabled = false, + intentFilters + ) + ) + } + // User provided custom icon. // // Must add all aliases even if the user did not provide a custom icon of their own. @@ -512,12 +532,15 @@ internal fun baseCustomBrandingPatch( ListPreference("morphe_custom_branding_name") } + // YouTube 21.41+ also has launcher icons of its own. + val arrayKeySuffix = if (appLauncherIconNames().isEmpty()) "" else "_21_41" + if (useCustomIcon) { preferences += ListPreference( key = "morphe_custom_branding_icon", tag = "app.morphe.extension.shared.settings.preference.IconListPreference", - entriesKey = "morphe_custom_branding_icon_custom_entries", - entryValuesKey = "morphe_custom_branding_icon_custom_entry_values" + entriesKey = "morphe_custom_branding_icon${arrayKeySuffix}_custom_entries", + entryValuesKey = "morphe_custom_branding_icon${arrayKeySuffix}_custom_entry_values" ) preferences += ListPreference( key = "morphe_custom_branding_notification_icon", @@ -528,7 +551,9 @@ internal fun baseCustomBrandingPatch( } else { preferences += ListPreference( key = "morphe_custom_branding_icon", - tag = "app.morphe.extension.shared.settings.preference.IconListPreference" + tag = "app.morphe.extension.shared.settings.preference.IconListPreference", + entriesKey = "morphe_custom_branding_icon${arrayKeySuffix}_entries", + entryValuesKey = "morphe_custom_branding_icon${arrayKeySuffix}_entry_values" ) preferences += ListPreference( key = "morphe_custom_branding_notification_icon", diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/branding/CustomBrandingPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/branding/CustomBrandingPatch.kt index 85be6e0164..3b178c1320 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/branding/CustomBrandingPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/branding/CustomBrandingPatch.kt @@ -1,6 +1,6 @@ /* * Copyright 2026 Morphe. - * https://github.com/MorpheApp/morphe-patches + * https://github.com/MorpheApp/morphe-patches/pull/4114 * * Original hard forked code: * https://github.com/ReVanced/revanced-patches/commit/724e6d61b2ecd868c1a9a37d465a688e83a74799 @@ -15,6 +15,8 @@ import app.morphe.patches.youtube.layout.theme.splashAnimationPatch import app.morphe.patches.youtube.misc.extension.sharedExtensionPatch import app.morphe.patches.youtube.misc.gms.Constants.YOUTUBE_MAIN_ACTIVITY_NAME import app.morphe.patches.youtube.misc.gms.Constants.YOUTUBE_PACKAGE_NAME +import app.morphe.patches.youtube.misc.playservice.is_21_41_or_greater +import app.morphe.patches.youtube.misc.playservice.versionCheckPatch import app.morphe.patches.youtube.misc.settings.PreferenceScreen import app.morphe.patches.youtube.shared.Constants.COMPATIBILITY_YOUTUBE import app.morphe.patches.youtube.shared.YouTubeActivityOnCreateFingerprint @@ -33,12 +35,27 @@ val customBrandingPatch = baseCustomBrandingPatch( mainActivityName = YOUTUBE_MAIN_ACTIVITY_NAME, activityAliasNameWithIntents = $$"com.google.android.youtube.app.honeycomb.Shell$HomeActivity", preferenceScreen = PreferenceScreen.GENERAL, + // Alternative launcher icons that YouTube has but does not show in its settings. + // The aliases are numbered like the icons, so an alias can be reused if YouTube + // replaces an icon. + appLauncherIconNames = { + if (is_21_41_or_greater) { + mapOf( + "youtube_icon1" to "ic_launcher_icon1", + "youtube_icon2" to "ic_launcher_icon2", + "youtube_icon3" to "ic_launcher_icon3" + ) + } else { + emptyMap() + } + }, block = { dependsOn( sharedExtensionPatch, // Owns the hook that loads the startup animation. - splashAnimationPatch + splashAnimationPatch, + versionCheckPatch ) compatibleWith(COMPATIBILITY_YOUTUBE) diff --git a/patches/src/main/resources/addresources/values/youtube/arrays.xml b/patches/src/main/resources/addresources/values/youtube/arrays.xml index 66a4ff6f64..d652839df4 100644 --- a/patches/src/main/resources/addresources/values/youtube/arrays.xml +++ b/patches/src/main/resources/addresources/values/youtube/arrays.xml @@ -40,6 +40,53 @@ 4 5 + + + @string/morphe_custom_branding_icon_entry_1 + @string/morphe_custom_branding_icon_entry_8 + @string/morphe_custom_branding_icon_entry_9 + @string/morphe_custom_branding_icon_entry_10 + @string/morphe_custom_branding_icon_entry_2 + @string/morphe_custom_branding_icon_entry_3 + @string/morphe_custom_branding_icon_entry_4 + @string/morphe_custom_branding_icon_entry_5 + @string/morphe_custom_branding_icon_entry_6 + + + ORIGINAL + YOUTUBE_ICON1 + YOUTUBE_ICON2 + YOUTUBE_ICON3 + BLACK + DARK + LIGHT + PLAY + PLAY_BLACK + + + @string/morphe_custom_branding_icon_entry_1 + @string/morphe_custom_branding_icon_entry_8 + @string/morphe_custom_branding_icon_entry_9 + @string/morphe_custom_branding_icon_entry_10 + @string/morphe_custom_branding_icon_entry_2 + @string/morphe_custom_branding_icon_entry_3 + @string/morphe_custom_branding_icon_entry_4 + @string/morphe_custom_branding_icon_entry_5 + @string/morphe_custom_branding_icon_entry_6 + @string/morphe_custom_branding_icon_entry_7 + + + ORIGINAL + YOUTUBE_ICON1 + YOUTUBE_ICON2 + YOUTUBE_ICON3 + BLACK + DARK + LIGHT + PLAY + PLAY_BLACK + CUSTOM + diff --git a/patches/src/main/resources/addresources/values/youtube/strings.xml b/patches/src/main/resources/addresources/values/youtube/strings.xml index 47f4234365..97470769a3 100644 --- a/patches/src/main/resources/addresources/values/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values/youtube/strings.xml @@ -1394,6 +1394,9 @@ Limitation: If \'Restore old video action bar\' is turned off, \'More videos\' o YouTube Morphe YT Morphe YT + Original neon + Original cake + Original metal Header logo From b54aaecaa26e8e6e4491b0c54193cd2b2b6e0f2c Mon Sep 17 00:00:00 2001 From: Anurag Rai <129479513+anuraggr@users.noreply.github.com> Date: Tue, 27 Oct 2026 23:01:15 +0530 Subject: [PATCH 111/189] fix(YouTube): Channel page community posts do not load when feed "Hide community posts" is enabled (#4110) --- .../patches/components/LayoutComponentsFilter.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/components/LayoutComponentsFilter.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/components/LayoutComponentsFilter.java index db98c10179..6408283250 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/components/LayoutComponentsFilter.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/components/LayoutComponentsFilter.java @@ -585,6 +585,15 @@ public boolean isFiltered(ContextInterface contextInterface, } if (matchedGroup == communityPosts) { + // YouTube now also shows community posts on a dedicated page for channels. + // This page is navigated with a back button, and hiding its posts + // prevents any posts from loading (the page shows a loading spinner forever). + // Only hide the posts when no back button is visible (home and subscription feeds). + // https://github.com/MorpheApp/morphe-patches/issues/4101 + if (NavigationBar.isBackButtonVisible()) { + return false; + } + return contextInterface.isHomeFeedOrRelatedVideo() || contextInterface.isSubscriptionOrLibrary(); } From 0ce43f7b8bf0119f295f269b939186ff264b5d48 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Tue, 27 Oct 2026 17:40:26 +0000 Subject: [PATCH 112/189] fix(YouTube): Remove legacy litho buffer filtering code required for app versions less than 20.22, change legacy support to `20.26.46` --- .../patches/components/LithoFilterPatch.java | 40 +------- .../youtube/patches/VersionCheckPatch.java | 2 +- .../SwipeControlsHostActivity.java | 2 +- .../misc/litho/filter/LithoFilterPatch.kt | 3 - .../shared/misc/litho/filter/Fingerprints.kt | 19 ---- .../misc/litho/filter/LithoFilterPatch.kt | 34 +------ .../buttons/navigation/NavigationBarPatch.kt | 91 +++++++++---------- .../overlay/HidePlayerOverlayButtonsPatch.kt | 21 ++--- .../layout/captions/AutoCaptionsPatch.kt | 27 ++---- .../formfactor/ChangeFormFactorPatch.kt | 4 +- .../hide/general/HideLayoutComponentsPatch.kt | 11 +-- .../updatescreen/HideUpdateScreenPatch.kt | 4 +- .../misc/litho/filter/LithoFilterPatch.kt | 4 - .../misc/playservice/VersionCheckPatch.kt | 7 +- .../patches/youtube/shared/Constants.kt | 2 +- .../video/playerresponse/Fingerprints.kt | 28 ------ .../PlayerResponseMethodHookPatch.kt | 6 +- .../addresources/values/youtube/arrays.xml | 16 ---- .../addresources/values/youtube/strings.xml | 2 - 19 files changed, 75 insertions(+), 248 deletions(-) diff --git a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/patches/components/LithoFilterPatch.java b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/patches/components/LithoFilterPatch.java index afa227ab29..37e19faa45 100644 --- a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/patches/components/LithoFilterPatch.java +++ b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/patches/components/LithoFilterPatch.java @@ -14,7 +14,6 @@ import androidx.annotation.Nullable; import java.lang.ref.WeakReference; -import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Iterator; import java.util.List; @@ -108,13 +107,6 @@ private static final class DummyFilter extends Filter { } private static final byte[] EMPTY_BYTE_ARRAY = new byte[0]; - /** - * Because litho filtering is multithreaded and the buffer is passed in from a different injection point, - * the buffer is saved to a ThreadLocal so each calling thread does not interfere with other threads. - * Used for 20.21 and lower. - */ - private static final ThreadLocal bufferThreadLocal = new ThreadLocal<>(); - /** * Observable lists that hold the items of the lists, such as the feed. * The lists use the equality of their content, so they are kept by reference instead of in a set. @@ -180,24 +172,6 @@ private static void filterUsingCallbacks(StringTrieSearch pathSearchTree, } } - /** - * Injection point. Called off the main thread. - * Targets 20.21 and lower. - */ - public static void setProtoBuffer(@Nullable ByteBuffer buffer) { - if (buffer == null || !buffer.hasArray()) { - // It appears the buffer can be cleared out just before the call to #filter() - // Ignore this null value and retain the last buffer that was set. - Logger.printDebug(() -> "Ignoring null or empty buffer: " + buffer); - } else { - // Set the buffer to a thread local. The buffer will remain in memory, even after the call to #filter completes. - // This is intentional, as it appears the buffer can be set once and then filtered multiple times. - // The buffer will be cleared from memory after a new buffer is set by the same thread, - // or when the calling thread eventually dies. - bufferThreadLocal.set(buffer.array()); - } - } - /** * Injection point. */ @@ -220,14 +194,8 @@ public static boolean isFiltered(ContextInterface contextInterface, @Nullable by accessibility = ""; } - byte[] buffer = useLegacyLithoFiltering() - ? bufferThreadLocal.get() - : bytes; - // Potentially the buffer may have been null or never set up until now. - // Use an empty buffer so the litho id/path filters that do not use a buffer still work. - if (buffer == null) { - buffer = EMPTY_BYTE_ARRAY; - } + // Use an empty buffer if there is none, so the litho id/path filters that do not use a buffer still work. + byte[] buffer = bytes == null ? EMPTY_BYTE_ARRAY : bytes; LithoFilterParameters parameter = new LithoFilterParameters( contextInterface, identifier, pathBuilder, accessibility, @@ -374,10 +342,6 @@ public static void removeFilteredElementListItem(@Nullable ContextInterface cont } } - private static boolean useLegacyLithoFiltering() { - return false; // Method is modified during patching. - } - /** * Injection point. */ diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/VersionCheckPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/VersionCheckPatch.java index 449744ec43..3786989e35 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/VersionCheckPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/VersionCheckPatch.java @@ -7,7 +7,7 @@ private static boolean isVersionOrGreater(String version) { return Utils.getAppVersionName().compareTo(version) >= 0; } - public static final boolean IS_20_22_OR_GREATER = isVersionOrGreater("20.22.00"); + public static final boolean IS_20_27_OR_GREATER = isVersionOrGreater("20.27.00"); public static final boolean IS_20_31_OR_GREATER = isVersionOrGreater("20.31.00"); diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/SwipeControlsHostActivity.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/SwipeControlsHostActivity.java index 179dda36c4..8298462c1f 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/SwipeControlsHostActivity.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/SwipeControlsHostActivity.java @@ -381,6 +381,6 @@ public static void setPlayerOverlay(View overlay, @Nullable String overlayName) */ public static boolean allowSwipeChangeVideo(boolean original) { // Feature can cause crashing if forced in newer targets. - return !VersionCheckPatch.IS_20_22_OR_GREATER && Settings.SWIPE_CHANGE_VIDEO.get(); + return !VersionCheckPatch.IS_20_27_OR_GREATER && Settings.SWIPE_CHANGE_VIDEO.get(); } } diff --git a/patches/src/main/kotlin/app/morphe/patches/music/misc/litho/filter/LithoFilterPatch.kt b/patches/src/main/kotlin/app/morphe/patches/music/misc/litho/filter/LithoFilterPatch.kt index 2339b01c49..fe6ecadcf4 100644 --- a/patches/src/main/kotlin/app/morphe/patches/music/misc/litho/filter/LithoFilterPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/music/misc/litho/filter/LithoFilterPatch.kt @@ -14,11 +14,8 @@ import app.morphe.patches.shared.misc.litho.context.conversionContextPatch import app.morphe.patches.shared.misc.litho.filter.sharedLithoFilterPatch val lithoFilterPatch = sharedLithoFilterPatch( - // Supported YT Music versions always use the native Upb encode path. - hookNonNativeBuffer = { false }, // YT Music does not ship the Upb feature flag. overrideUpbFeatureFlag = { false }, - useLegacyLithoFiltering = { false }, removeFilteredListItems = false ) { dependsOn( diff --git a/patches/src/main/kotlin/app/morphe/patches/shared/misc/litho/filter/Fingerprints.kt b/patches/src/main/kotlin/app/morphe/patches/shared/misc/litho/filter/Fingerprints.kt index 646bae861f..c1c520cada 100644 --- a/patches/src/main/kotlin/app/morphe/patches/shared/misc/litho/filter/Fingerprints.kt +++ b/patches/src/main/kotlin/app/morphe/patches/shared/misc/litho/filter/Fingerprints.kt @@ -13,7 +13,6 @@ package app.morphe.patches.shared.misc.litho.filter import app.morphe.patcher.Fingerprint import app.morphe.patcher.InstructionLocation.MatchAfterImmediately import app.morphe.patcher.InstructionLocation.MatchAfterWithin -import app.morphe.patcher.OpcodesFilter import app.morphe.patcher.fieldAccess import app.morphe.patcher.literal import app.morphe.patcher.methodCall @@ -61,12 +60,6 @@ internal object LithoFilterFingerprint : Fingerprint( ) ) -internal object LithoFilterLegacyMethodFingerprint : Fingerprint( - definingClass = EXTENSION_CLASS, - name = "useLegacyLithoFiltering", - returnType = "Z" -) - internal object ProtobufBufferEncodeFingerprint : Fingerprint( accessFlags = listOf(AccessFlags.PUBLIC, AccessFlags.FINAL), returnType = "[B", @@ -84,18 +77,6 @@ internal object ProtobufBufferEncodeFingerprint : Fingerprint( ) ) -internal object ProtobufBufferReferenceFingerprint : Fingerprint( - accessFlags = listOf(AccessFlags.PUBLIC, AccessFlags.FINAL), - returnType = "V", - parameters = listOf("I", "Ljava/nio/ByteBuffer;"), - filters = OpcodesFilter.opcodesToFilters( - Opcode.IPUT, - Opcode.INVOKE_VIRTUAL, - Opcode.MOVE_RESULT, - Opcode.SUB_INT_2ADDR, - ) -) - internal object LithoThreadExecutorFingerprint : Fingerprint( superclass = "Ljava/util/concurrent/ThreadPoolExecutor;", accessFlags = listOf(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR), diff --git a/patches/src/main/kotlin/app/morphe/patches/shared/misc/litho/filter/LithoFilterPatch.kt b/patches/src/main/kotlin/app/morphe/patches/shared/misc/litho/filter/LithoFilterPatch.kt index a45e7b24d3..a8fff1c7f7 100644 --- a/patches/src/main/kotlin/app/morphe/patches/shared/misc/litho/filter/LithoFilterPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/shared/misc/litho/filter/LithoFilterPatch.kt @@ -29,7 +29,6 @@ import app.morphe.patcher.util.getFreeRegisterProvider import app.morphe.patcher.util.getReference import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable -import app.morphe.patcher.util.returnEarly import app.morphe.patches.shared.misc.litho.context.ConversionContextToStringFingerprint import app.morphe.patches.shared.misc.litho.context.EXTENSION_CONTEXT_INTERFACE import app.morphe.patches.shared.misc.litho.context.conversionContextClassDef @@ -99,14 +98,6 @@ fun addLithoFilter(classDescriptor: String) { * * The following pseudocode shows how this patch works: * - * class SomeOtherClass { - * // Called before ComponentContextParser.parseComponent() method. - * public void someOtherMethod(ByteBuffer byteBuffer) { - * ExtensionClass.setProtoBuffer(byteBuffer); // Inserted by this patch. - * ... - * } - * } - * * class ComponentContextParser { * public Component parseComponent() { * ... @@ -118,10 +109,6 @@ fun addLithoFilter(classDescriptor: String) { * } * } * - * @param hookNonNativeBuffer Whether to also insert the non-native ByteBuffer hook. - * Older client versions push data through a non-native path; newer ones - * (YouTube 20.22+, YT Music 9.x) always use the native Upb encode path. - * Evaluated lazily inside execute so version flags are already set. * @param overrideUpbFeatureFlag Whether to override the A/B feature flag that enables Upb-native * protobuf parsing (only present on older YouTube; absent in Music). * @param removeFilteredListItems Whether to remove the items of the lists (such as the feed) whose @@ -131,9 +118,7 @@ fun addLithoFilter(classDescriptor: String) { * versionCheckPatch, and any app-specific fixes). */ internal fun sharedLithoFilterPatch( - hookNonNativeBuffer: () -> Boolean, overrideUpbFeatureFlag: () -> Boolean, - useLegacyLithoFiltering: () -> Boolean, removeFilteredListItems: Boolean, block: BytecodePatchBuilder.() -> Unit, ): BytecodePatch = bytecodePatch( @@ -142,8 +127,6 @@ internal fun sharedLithoFilterPatch( block() execute { - LithoFilterLegacyMethodFingerprint.method.returnEarly(useLegacyLithoFiltering()) - // Remove dummy filter from extenion static field // and add the filters included during patching. LithoFilterFingerprint.let { @@ -180,24 +163,11 @@ internal fun sharedLithoFilterPatch( } } - // region Pass the buffer into extension. - - if (hookNonNativeBuffer()) { - // Non-native buffer. - ProtobufBufferReferenceFingerprint.method.addInstruction( - 0, - "invoke-static { p2 }, $EXTENSION_CLASS->setProtoBuffer(Ljava/nio/ByteBuffer;)V", - ) - } - - val protoBufferEncodeMethod = ProtobufBufferEncodeFingerprint.method - - // endregion - - // region Modify the create component method and // if the component is filtered then return an empty component. + val protoBufferEncodeMethod = ProtobufBufferEncodeFingerprint.method + // Find class and methods to create an empty component. val builderMethodDescriptor = EmptyComponentFingerprint.method diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/buttons/navigation/NavigationBarPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/buttons/navigation/NavigationBarPatch.kt index 087b34bd1b..e397fde8f4 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/buttons/navigation/NavigationBarPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/buttons/navigation/NavigationBarPatch.kt @@ -43,7 +43,6 @@ import app.morphe.patches.youtube.misc.extension.sharedExtensionPatch import app.morphe.patches.youtube.misc.navigation.addBottomBarContainerHook import app.morphe.patches.youtube.misc.navigation.hookNavigationButtonCreated import app.morphe.patches.youtube.misc.navigation.navigationBarHookPatch -import app.morphe.patches.youtube.misc.playservice.is_20_26_or_greater import app.morphe.patches.youtube.misc.playservice.is_20_31_or_greater import app.morphe.patches.youtube.misc.playservice.is_21_07_or_greater import app.morphe.patches.youtube.misc.playservice.is_21_35_or_greater @@ -379,53 +378,51 @@ val navigationBarPatch = bytecodePatch( // does not scroll and its items only get the height left above the padding. With a tall // keyboard nothing is left, and the items are hidden. The keyboard is removed from // the padding, as for the navigation bar above. - if (is_20_26_or_greater) { - FeedListPaddingFingerprint.let { - it.method.apply { - val index = it.instructionMatches.last().index - val paddingInstruction = getInstruction(index) - val listRegister = paddingInstruction.registerC - val bottomPaddingRegister = paddingInstruction.registerG - val freeRegisters = getFreeRegisterProvider(index, 3) - val insetsRegister = freeRegisters.getFreeRegister4Bit() - val imeRegister = freeRegisters.getFreeRegister4Bit() - val systemBarsRegister = freeRegisters.getFreeRegister4Bit() + FeedListPaddingFingerprint.let { + it.method.apply { + val index = it.instructionMatches.last().index + val paddingInstruction = getInstruction(index) + val listRegister = paddingInstruction.registerC + val bottomPaddingRegister = paddingInstruction.registerG + val freeRegisters = getFreeRegisterProvider(index, 3) + val insetsRegister = freeRegisters.getFreeRegister4Bit() + val imeRegister = freeRegisters.getFreeRegister4Bit() + val systemBarsRegister = freeRegisters.getFreeRegister4Bit() - addInstructionsWithLabels( - index, - $$""" - invoke-static { }, $$EXTENSION_CLASS->isTranslucentNavigationBarBackgroundForced()Z - move-result v$$imeRegister - if-eqz v$$imeRegister, :keep_padding - # The inset types are only on Android 11 and later. - sget v$$imeRegister, Landroid/os/Build$VERSION;->SDK_INT:I - const/16 v$$systemBarsRegister, 0x1e - if-lt v$$imeRegister, v$$systemBarsRegister, :keep_padding - invoke-virtual { v$$listRegister }, Landroid/view/View;->getRootWindowInsets()Landroid/view/WindowInsets; - move-result-object v$$insetsRegister - if-eqz v$$insetsRegister, :keep_padding - # The keyboard inset is empty when the keyboard is hidden. - invoke-static { }, Landroid/view/WindowInsets$Type;->ime()I - move-result v$$imeRegister - invoke-virtual { v$$insetsRegister, v$$imeRegister }, Landroid/view/WindowInsets;->getInsets(I)Landroid/graphics/Insets; - move-result-object v$$imeRegister - iget v$$imeRegister, v$$imeRegister, Landroid/graphics/Insets;->bottom:I - # The padding does not hold the bottom inset, as when the feed is not behind the navigation bar. - if-lt v$$bottomPaddingRegister, v$$imeRegister, :keep_padding - invoke-static { }, Landroid/view/WindowInsets$Type;->systemBars()I - move-result v$$systemBarsRegister - invoke-virtual { v$$insetsRegister, v$$systemBarsRegister }, Landroid/view/WindowInsets;->getInsets(I)Landroid/graphics/Insets; - move-result-object v$$systemBarsRegister - iget v$$systemBarsRegister, v$$systemBarsRegister, Landroid/graphics/Insets;->bottom:I - # Keyboard height above the system bars. - sub-int/2addr v$$imeRegister, v$$systemBarsRegister - if-lez v$$imeRegister, :keep_padding - sub-int/2addr v$$bottomPaddingRegister, v$$imeRegister - :keep_padding - nop - """ - ) - } + addInstructionsWithLabels( + index, + $$""" + invoke-static { }, $$EXTENSION_CLASS->isTranslucentNavigationBarBackgroundForced()Z + move-result v$$imeRegister + if-eqz v$$imeRegister, :keep_padding + # The inset types are only on Android 11 and later. + sget v$$imeRegister, Landroid/os/Build$VERSION;->SDK_INT:I + const/16 v$$systemBarsRegister, 0x1e + if-lt v$$imeRegister, v$$systemBarsRegister, :keep_padding + invoke-virtual { v$$listRegister }, Landroid/view/View;->getRootWindowInsets()Landroid/view/WindowInsets; + move-result-object v$$insetsRegister + if-eqz v$$insetsRegister, :keep_padding + # The keyboard inset is empty when the keyboard is hidden. + invoke-static { }, Landroid/view/WindowInsets$Type;->ime()I + move-result v$$imeRegister + invoke-virtual { v$$insetsRegister, v$$imeRegister }, Landroid/view/WindowInsets;->getInsets(I)Landroid/graphics/Insets; + move-result-object v$$imeRegister + iget v$$imeRegister, v$$imeRegister, Landroid/graphics/Insets;->bottom:I + # The padding does not hold the bottom inset, as when the feed is not behind the navigation bar. + if-lt v$$bottomPaddingRegister, v$$imeRegister, :keep_padding + invoke-static { }, Landroid/view/WindowInsets$Type;->systemBars()I + move-result v$$systemBarsRegister + invoke-virtual { v$$insetsRegister, v$$systemBarsRegister }, Landroid/view/WindowInsets;->getInsets(I)Landroid/graphics/Insets; + move-result-object v$$systemBarsRegister + iget v$$systemBarsRegister, v$$systemBarsRegister, Landroid/graphics/Insets;->bottom:I + # Keyboard height above the system bars. + sub-int/2addr v$$imeRegister, v$$systemBarsRegister + if-lez v$$imeRegister, :keep_padding + sub-int/2addr v$$bottomPaddingRegister, v$$imeRegister + :keep_padding + nop + """ + ) } } diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/buttons/overlay/HidePlayerOverlayButtonsPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/buttons/overlay/HidePlayerOverlayButtonsPatch.kt index ee81ff6b90..08d891ccf8 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/buttons/overlay/HidePlayerOverlayButtonsPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/buttons/overlay/HidePlayerOverlayButtonsPatch.kt @@ -24,7 +24,6 @@ import app.morphe.patches.shared.misc.settings.preference.NonInteractivePreferen import app.morphe.patches.shared.misc.settings.preference.SwitchPreference import app.morphe.patches.shared.misc.settings.preference.noTitleUnsortedPreferenceCategory import app.morphe.patches.youtube.misc.extension.sharedExtensionPatch -import app.morphe.patches.youtube.misc.playservice.is_20_28_or_greater import app.morphe.patches.youtube.misc.playservice.is_21_35_or_greater import app.morphe.patches.youtube.misc.playservice.is_21_42_or_greater import app.morphe.patches.youtube.misc.playservice.versionCheckPatch @@ -161,17 +160,15 @@ val hidePlayerOverlayButtonsPatch = bytecodePatch( } } - if (is_20_28_or_greater) { - arrayOf( - CastButtonPlayerFeatureFlagFingerprint, - CastButtonActionFeatureFlagFingerprint // Cast button in the feed. - ).forEach { fingerprint -> - fingerprint.matchAll().forEach { - it.method.insertLiteralOverride( - it.instructionMatches.first().index, - "$EXTENSION_CLASS->hideCastButton(Z)Z" - ) - } + arrayOf( + CastButtonPlayerFeatureFlagFingerprint, + CastButtonActionFeatureFlagFingerprint // Cast button in the feed. + ).forEach { fingerprint -> + fingerprint.matchAll().forEach { + it.method.insertLiteralOverride( + it.instructionMatches.first().index, + "$EXTENSION_CLASS->hideCastButton(Z)Z" + ) } } diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/captions/AutoCaptionsPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/captions/AutoCaptionsPatch.kt index 5f8a50413a..9cf38ecec7 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/captions/AutoCaptionsPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/captions/AutoCaptionsPatch.kt @@ -15,8 +15,6 @@ import app.morphe.patcher.extensions.InstructionExtensions.getInstruction import app.morphe.patcher.patch.bytecodePatch import app.morphe.patches.shared.misc.settings.preference.ListPreference import app.morphe.patches.youtube.misc.extension.sharedExtensionPatch -import app.morphe.patches.youtube.misc.playservice.is_20_26_or_greater -import app.morphe.patches.youtube.misc.playservice.versionCheckPatch import app.morphe.patches.youtube.misc.settings.settingsPatch import app.morphe.patches.youtube.shared.hookVideoStarted import app.morphe.patches.youtube.shared.startVideoInformerPatch @@ -35,21 +33,12 @@ internal val autoCaptionsPatch = bytecodePatch( sharedExtensionPatch, settingsPatch, startVideoInformerPatch, - versionCheckPatch, videoInformationPatch ) execute { settingsMenuCaptionGroup.add( - if (is_20_26_or_greater) { - ListPreference("morphe_auto_captions_style") - } else { - ListPreference( - key = "morphe_auto_captions_style", - entriesKey = "morphe_auto_captions_style_legacy_entries", - entryValuesKey = "morphe_auto_captions_style_legacy_entry_values" - ) - } + ListPreference("morphe_auto_captions_style") ) SubtitleManagerFingerprint.let { @@ -71,14 +60,12 @@ internal val autoCaptionsPatch = bytecodePatch( hookVideoStarted("$EXTENSION_CLASS->videoInformationLoaded()V") - if (is_20_26_or_greater) { - NoVolumeCaptionsFeatureFlagFingerprint.matchAll().forEach { - // 21.30+ inlines the flag lookup and must patch ~4 places. - it.method.insertLiteralOverride( - it.instructionMatches.first().index, - "$EXTENSION_CLASS->disableMuteAutoCaptions(Z)Z" - ) - } + NoVolumeCaptionsFeatureFlagFingerprint.matchAll().forEach { + // 21.30+ inlines the flag lookup and must patch ~4 places. + it.method.insertLiteralOverride( + it.instructionMatches.first().index, + "$EXTENSION_CLASS->disableMuteAutoCaptions(Z)Z" + ) } } } diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/formfactor/ChangeFormFactorPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/formfactor/ChangeFormFactorPatch.kt index c394912340..3e125ce7e2 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/formfactor/ChangeFormFactorPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/formfactor/ChangeFormFactorPatch.kt @@ -26,7 +26,7 @@ import app.morphe.patches.youtube.misc.contexthook.clientContextHookPatch import app.morphe.patches.youtube.misc.extension.sharedExtensionPatch import app.morphe.patches.youtube.misc.fix.videoactionbar.restoreOldVideoActionBarPatch import app.morphe.patches.youtube.misc.navigation.navigationBarHookPatch -import app.morphe.patches.youtube.misc.playservice.is_20_22_or_greater +import app.morphe.patches.youtube.misc.playservice.is_20_27_or_greater import app.morphe.patches.youtube.misc.playservice.versionCheckPatch import app.morphe.patches.youtube.misc.settings.PreferenceScreen import app.morphe.patches.youtube.misc.settings.settingsPatch @@ -57,7 +57,7 @@ val changeFormFactorPatch = bytecodePatch( ListPreference("morphe_change_form_factor") ) - if (!is_20_22_or_greater) { + if (!is_20_27_or_greater) { preferences += SwitchPreference("morphe_tablet_layout_in_player", summary = true) } diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/general/HideLayoutComponentsPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/general/HideLayoutComponentsPatch.kt index 0721c5df4f..8bf0f85a97 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/general/HideLayoutComponentsPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/general/HideLayoutComponentsPatch.kt @@ -52,7 +52,6 @@ import app.morphe.patches.youtube.misc.engagement.engagementPanelHookPatch import app.morphe.patches.youtube.misc.litho.filter.lithoFilterPatch import app.morphe.patches.youtube.misc.litho.node.treeNodeElementHookPatch import app.morphe.patches.youtube.misc.navigation.navigationBarHookPatch -import app.morphe.patches.youtube.misc.playservice.is_20_26_or_greater import app.morphe.patches.youtube.misc.playservice.is_20_31_or_greater import app.morphe.patches.youtube.misc.playservice.is_21_11_or_greater import app.morphe.patches.youtube.misc.playservice.is_21_20_or_greater @@ -321,15 +320,7 @@ val hideLayoutComponentsPatch = bytecodePatch( SwitchPreference("morphe_hide_channel_buttons", summary = true), SwitchPreference("morphe_hide_community_posts"), SwitchPreference("morphe_hide_compact_banner", summary = true), - if (is_20_26_or_greater) { - ListPreference("morphe_hide_expandable_card") - } else { - ListPreference( - key = "morphe_hide_expandable_card", - entriesKey = "morphe_hide_expandable_card_legacy_entries", - entryValuesKey = "morphe_hide_expandable_card_legacy_entry_values" - ) - }, + ListPreference("morphe_hide_expandable_card"), noTitleUnsortedPreferenceCategory( SwitchPreference("morphe_hide_feed_flyout_menu"), TextPreference( diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/updatescreen/HideUpdateScreenPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/updatescreen/HideUpdateScreenPatch.kt index 6fb21eea72..b01846f5c5 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/updatescreen/HideUpdateScreenPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/updatescreen/HideUpdateScreenPatch.kt @@ -10,7 +10,7 @@ package app.morphe.patches.youtube.layout.hide.updatescreen import app.morphe.patcher.Fingerprint import app.morphe.patcher.extensions.InstructionExtensions.addInstructions import app.morphe.patcher.patch.bytecodePatch -import app.morphe.patches.youtube.misc.playservice.is_20_22_or_greater +import app.morphe.patches.youtube.misc.playservice.is_20_27_or_greater import app.morphe.patches.youtube.misc.playservice.versionCheckPatch import app.morphe.patches.youtube.shared.Constants.COMPATIBILITY_YOUTUBE import com.android.tools.smali.dexlib2.AccessFlags @@ -25,7 +25,7 @@ val hideUpdateScreenPatch = bytecodePatch( execute { // Only disable update screen for 20.21 and lower. This can be later adjusted if desired. - if (!is_20_22_or_greater) { + if (!is_20_27_or_greater) { Fingerprint( classFingerprint = AppBlockingCheckResultToStringFingerprint, accessFlags = listOf(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR), diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/misc/litho/filter/LithoFilterPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/litho/filter/LithoFilterPatch.kt index 84766896dd..8887459f3c 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/misc/litho/filter/LithoFilterPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/litho/filter/LithoFilterPatch.kt @@ -15,16 +15,12 @@ import app.morphe.patches.youtube.misc.extension.sharedExtensionPatch import app.morphe.patches.youtube.misc.fix.backtoexitgesture.fixBackToExitGesturePatch import app.morphe.patches.youtube.misc.fix.verticalscroll.fixVerticalScrollPatch import app.morphe.patches.youtube.misc.litho.rendernext.disableRenderNextPatch -import app.morphe.patches.youtube.misc.playservice.is_20_22_or_greater import app.morphe.patches.youtube.misc.playservice.is_21_15_or_greater import app.morphe.patches.youtube.misc.playservice.versionCheckPatch val lithoFilterPatch = sharedLithoFilterPatch( - // YouTube 20.22+ always uses the native Upb encode path. - hookNonNativeBuffer = { !is_20_22_or_greater }, // Flag was removed in 21.15+. overrideUpbFeatureFlag = { !is_21_15_or_greater }, - useLegacyLithoFiltering = { !is_20_22_or_greater }, removeFilteredListItems = true ) { dependsOn( diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/misc/playservice/VersionCheckPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/playservice/VersionCheckPatch.kt index 87107969f1..7f407586e6 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/misc/playservice/VersionCheckPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/playservice/VersionCheckPatch.kt @@ -7,9 +7,7 @@ import kotlin.properties.Delegates // Use notNull delegate so an exception is thrown if these fields are accessed before they are set. -var is_20_22_or_greater : Boolean by Delegates.notNull() - private set -var is_20_26_or_greater : Boolean by Delegates.notNull() +var is_20_27_or_greater : Boolean by Delegates.notNull() private set var is_20_28_or_greater : Boolean by Delegates.notNull() private set @@ -123,8 +121,7 @@ val versionCheckPatch = bytecodePatch { return versionName >= version } - is_20_22_or_greater = isEqualsOrGreaterThan("20.22.00") - is_20_26_or_greater = isEqualsOrGreaterThan("20.26.00") + is_20_27_or_greater = isEqualsOrGreaterThan("20.27.00") is_20_28_or_greater = isEqualsOrGreaterThan("20.28.00") is_20_29_or_greater = isEqualsOrGreaterThan("20.29.00") is_20_30_or_greater = isEqualsOrGreaterThan("20.30.00") diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/shared/Constants.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/shared/Constants.kt index 9a739172f5..17ca15fc4a 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/shared/Constants.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/shared/Constants.kt @@ -49,7 +49,7 @@ internal object Constants { minSdk = 28 ), AppTarget( - version = "20.21.37", + version = "20.26.46", // Last version for Android 8.0 minSdk = 26 ) ) diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/video/playerresponse/Fingerprints.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/video/playerresponse/Fingerprints.kt index 87ec18d0bc..b13e466dad 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/video/playerresponse/Fingerprints.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/video/playerresponse/Fingerprints.kt @@ -58,31 +58,3 @@ internal object PlayerParameterBuilder2026Fingerprint : Fingerprint( string("psps") ) ) - -/** - * For targets 20.15 to 20.25 - */ -internal object PlayerParameterBuilder2015Fingerprint : Fingerprint( - accessFlags = listOf(AccessFlags.PUBLIC, AccessFlags.FINAL), - returnType = "L", - parameters = listOf( - "Ljava/lang/String;", // VideoId. - "[B", - "Ljava/lang/String;", // Player parameters = listOf( proto buffer.), - "Ljava/lang/String;", - "I", - "Z", - "I", - "L", - "Ljava/util/Set;", - "Ljava/lang/String;", - "Ljava/lang/String;", - "L", - "Z", // Appears to indicate if the video ID is being opened or is currently playing. - "Z", - "Z", - ), - filters = listOf( - string("psps") - ) -) diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/video/playerresponse/PlayerResponseMethodHookPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/video/playerresponse/PlayerResponseMethodHookPatch.kt index 6cda90df2c..b01fd80059 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/video/playerresponse/PlayerResponseMethodHookPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/video/playerresponse/PlayerResponseMethodHookPatch.kt @@ -17,7 +17,6 @@ import app.morphe.patcher.patch.bytecodePatch import app.morphe.patcher.util.p0Register import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod import app.morphe.patches.youtube.misc.extension.sharedExtensionPatch -import app.morphe.patches.youtube.misc.playservice.is_20_26_or_greater import app.morphe.patches.youtube.misc.playservice.is_20_46_or_greater import app.morphe.patches.youtube.misc.playservice.versionCheckPatch import java.lang.ref.WeakReference @@ -55,12 +54,9 @@ val playerResponseMethodHookPatch = bytecodePatch { if (is_20_46_or_greater) { parameterIsShortAndOpeningOrPlaying = 13 fingerprint = PlayerParameterBuilderFingerprint - } else if (is_20_26_or_greater) { - parameterIsShortAndOpeningOrPlaying = 13 - fingerprint = PlayerParameterBuilder2026Fingerprint } else { parameterIsShortAndOpeningOrPlaying = 13 - fingerprint = PlayerParameterBuilder2015Fingerprint + fingerprint = PlayerParameterBuilder2026Fingerprint } val playerResponseMethod = fingerprint.method diff --git a/patches/src/main/resources/addresources/values/youtube/arrays.xml b/patches/src/main/resources/addresources/values/youtube/arrays.xml index d652839df4..f4614c00d0 100644 --- a/patches/src/main/resources/addresources/values/youtube/arrays.xml +++ b/patches/src/main/resources/addresources/values/youtube/arrays.xml @@ -397,14 +397,6 @@ HIDE_SUMMARY_ONLY HIDE_ALL - - @string/morphe_hide_expandable_card_entry_1 - @string/morphe_hide_expandable_card_entry_3 - - - SHOW_ALL - HIDE_ALL - @@ -456,14 +448,6 @@ WITH_VOLUME_ONLY WITHOUT_VOLUME_ONLY - - @string/morphe_auto_captions_style_legacy_entry_1 - @string/morphe_auto_captions_style_legacy_entry_2 - - - BOTH_ENABLED - BOTH_DISABLED - diff --git a/patches/src/main/resources/addresources/values/youtube/strings.xml b/patches/src/main/resources/addresources/values/youtube/strings.xml index 97470769a3..2315dc3f2a 100644 --- a/patches/src/main/resources/addresources/values/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values/youtube/strings.xml @@ -905,8 +905,6 @@ To fix this, turn on \'Spoof video streams\' and change the \'Default client\' t Never show Show when unmuted Show when muted - Show auto captions - Hide auto captions Preferred caption language From d145e147a685a6475f0530505bda4d3ef54f349b Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Tue, 27 Oct 2026 18:05:30 +0000 Subject: [PATCH 113/189] chore(YouTube - Custom branding): Rename 'neon' to 'analog' --- .../morphe/extension/shared/patches/CustomBrandingPatch.java | 2 +- .../src/main/resources/addresources/values/youtube/strings.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/patches/CustomBrandingPatch.java b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/patches/CustomBrandingPatch.java index e2ddb0c4f6..d26da91f9f 100644 --- a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/patches/CustomBrandingPatch.java +++ b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/patches/CustomBrandingPatch.java @@ -62,7 +62,7 @@ public enum BrandingTheme { * has these icons. If YouTube removes an icon, the alias must stay with a dummy icon. * Numbered like the icons of the app, so a theme can be reused if YouTube replaces an icon. */ - YOUTUBE_ICON1, // Neon + YOUTUBE_ICON1, // Analog YOUTUBE_ICON2, // Cake YOUTUBE_ICON3; // Metal diff --git a/patches/src/main/resources/addresources/values/youtube/strings.xml b/patches/src/main/resources/addresources/values/youtube/strings.xml index 2315dc3f2a..ac22ab20ff 100644 --- a/patches/src/main/resources/addresources/values/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values/youtube/strings.xml @@ -1392,7 +1392,7 @@ Limitation: If \'Restore old video action bar\' is turned off, \'More videos\' o YouTube Morphe YT Morphe YT - Original neon + Original analog Original cake Original metal From fb51b722e05df50e98302f87996525b349d0474b Mon Sep 17 00:00:00 2001 From: 0xrxL <127248639+0xrxL@users.noreply.github.com> Date: Tue, 27 Oct 2026 19:24:39 +0100 Subject: [PATCH 114/189] feat(YouTube): Hidden a new type of community post --- .../youtube/patches/components/LayoutComponentsFilter.java | 1 + 1 file changed, 1 insertion(+) diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/components/LayoutComponentsFilter.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/components/LayoutComponentsFilter.java index 6408283250..a269dff6cd 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/components/LayoutComponentsFilter.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/components/LayoutComponentsFilter.java @@ -285,6 +285,7 @@ public LayoutComponentsFilter() { communityPosts = new StringFilterGroup( Settings.HIDE_COMMUNITY_POSTS, + "communities_entrypoint.e", "images_post_responsive_root.e", "images_post_root.e", "images_post_root_slim.e", From 51dfd7565426be389f61dc5f34196cc762a103fe Mon Sep 17 00:00:00 2001 From: 0xrxL <127248639+0xrxL@users.noreply.github.com> Date: Tue, 27 Oct 2026 20:24:00 +0100 Subject: [PATCH 115/189] fix(YouTube - Feed video actions): Watch later button is missing after scrolling up --- .../patches/FeedVideoActionsPatch.java | 21 ++++++++++++++++ .../patches/utils/FeedVideoCardsPatch.java | 25 ++++++++++++++----- 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FeedVideoActionsPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FeedVideoActionsPatch.java index ff99f410fa..bbd46f5105 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FeedVideoActionsPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FeedVideoActionsPatch.java @@ -572,6 +572,27 @@ private static void updateRoots() { } } + /** + * Called when {@link FeedVideoCardsPatch} checks a card, which it also does again while + * the list scrolls and the card is partly shown. Litho mounts only the part of a card that + * is shown, so the menu button of a card that enters from the top of the list may not be + * found when it is laid out, and the card may not be laid out again once it is shown. + * + * @param videoId The video of the card, or null if the card is not a known video. + */ + public static void onCardLaidOut(ComponentHost host, @Nullable String videoId) { + if (!ENABLED) { + return; + } + try { + if (updateCard(host) == null && videoId != null) { + FeedVideoCardsPatch.scheduleScrollCheck(host); + } + } catch (Exception ex) { + Logger.printException(() -> "onCardLaidOut failure", ex); + } + } + /** * Injection point. * Called after a host has laid out its children. diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/FeedVideoCardsPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/FeedVideoCardsPatch.java index 33319b05bb..6ba0c9a16a 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/FeedVideoCardsPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/FeedVideoCardsPatch.java @@ -42,6 +42,7 @@ import app.morphe.extension.shared.Logger; import app.morphe.extension.shared.Utils; import app.morphe.extension.youtube.patches.FeedEngagementCountsPatch; +import app.morphe.extension.youtube.patches.FeedVideoActionsPatch; import app.morphe.extension.youtube.patches.FeedViewCountPatch; import app.morphe.extension.youtube.patches.PostImageDownloadPatch; import app.morphe.extension.youtube.patches.VideoInformation; @@ -698,14 +699,13 @@ private static void scheduleUpdate(ComponentHost root) { } /** - * Checks the card again a little later, such as when what is drawn on it was not found yet, - * and when the list scrolls while the card is partly shown. - * Must be called on the main thread. + * Checks the card again when the list scrolls, if the card is partly shown. + * Litho mounts only the part of a card that is shown, so what is drawn on it can be found + * once more of it is shown. Must be called on the main thread. * - * @return If the card is checked again. + * @return If the card is partly shown, so it is checked again. */ - public static boolean scheduleRetry(ComponentHost root) { - // Only the part of the card on the screen is mounted. + public static boolean scheduleScrollCheck(ComponentHost root) { final boolean partlyShown = root.isAttachedToWindow() && (!root.getLocalVisibleRect(visibleRect) || visibleRect.height() < root.getHeight()); if (partlyShown) { @@ -715,6 +715,18 @@ public static boolean scheduleRetry(ComponentHost root) { observer.addOnScrollChangedListener(scrollListener); } } + return partlyShown; + } + + /** + * Checks the card again a little later, such as when what is drawn on it was not found yet, + * and when the list scrolls while the card is partly shown. + * Must be called on the main thread. + * + * @return If the card is checked again. + */ + public static boolean scheduleRetry(ComponentHost root) { + final boolean partlyShown = scheduleScrollCheck(root); Integer count = retries.get(root); final int attempt = count == null ? 0 : count; @@ -766,6 +778,7 @@ public static boolean scheduleRetry(ComponentHost root) { final String cardVideoId = PLAYLIST.equals(videoId) ? null : videoId; FeedEngagementCountsPatch.onCardLaidOut(root, cardVideoId); FeedViewCountPatch.onCardLaidOut(root, cardVideoId); + FeedVideoActionsPatch.onCardLaidOut(root, cardVideoId); PostImageDownloadPatch.onRootLaidOut(root); } } catch (Exception ex) { From 74ae9ddb3f27f8d15cfe24de36eca9f88c4ecccf Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Tue, 27 Oct 2026 19:24:47 +0000 Subject: [PATCH 116/189] chore: Update translations from Crowdin --- .../values-af-rZA/shared-youtube/strings.xml | 10 +- .../values-af-rZA/youtube/strings.xml | 6 +- .../values-am-rET/shared-youtube/strings.xml | 10 +- .../values-am-rET/youtube/strings.xml | 6 +- .../values-ar-rSA/shared-youtube/strings.xml | 10 +- .../values-ar-rSA/youtube/strings.xml | 22 +- .../values-as-rIN/music/strings.xml | 619 +++++++ .../values-as-rIN/reddit/strings.xml | 121 +- .../values-as-rIN/shared-youtube/strings.xml | 249 +++ .../values-as-rIN/shared/strings.xml | 198 +++ .../values-as-rIN/sponsorblock/strings.xml | 213 +++ .../values-as-rIN/youtube/strings.xml | 1418 +++++++++++++++++ .../values-az-rAZ/shared-youtube/strings.xml | 10 +- .../values-az-rAZ/youtube/strings.xml | 32 +- .../values-be-rBY/music/strings.xml | 614 +++++++ .../values-be-rBY/reddit/strings.xml | 114 ++ .../values-be-rBY/shared-youtube/strings.xml | 225 ++- .../values-be-rBY/shared/strings.xml | 193 +++ .../values-be-rBY/sponsorblock/strings.xml | 212 +++ .../values-be-rBY/youtube/strings.xml | 1347 ++++++++++++++++ .../values-bg-rBG/shared-youtube/strings.xml | 10 +- .../values-bg-rBG/youtube/strings.xml | 8 +- .../values-bn-rBD/shared-youtube/strings.xml | 10 +- .../values-bn-rBD/youtube/strings.xml | 6 +- .../values-ca-rES/shared-youtube/strings.xml | 2 +- .../values-ckb-rIR/shared-youtube/strings.xml | 6 +- .../values-ckb-rIR/youtube/strings.xml | 2 - .../values-cs-rCZ/shared-youtube/strings.xml | 10 +- .../values-cs-rCZ/youtube/strings.xml | 6 +- .../values-da-rDK/shared-youtube/strings.xml | 10 +- .../values-da-rDK/youtube/strings.xml | 6 +- .../values-de-rDE/shared-youtube/strings.xml | 10 +- .../values-de-rDE/youtube/strings.xml | 21 +- .../values-el-rGR/shared-youtube/strings.xml | 10 +- .../values-el-rGR/youtube/strings.xml | 6 +- .../values-es-rES/shared-youtube/strings.xml | 10 +- .../values-es-rES/youtube/strings.xml | 6 +- .../values-eu-rES/music/strings.xml | 617 +++++++ .../values-eu-rES/reddit/strings.xml | 121 +- .../values-eu-rES/shared-youtube/strings.xml | 239 +++ .../values-eu-rES/shared/strings.xml | 183 +++ .../values-eu-rES/sponsorblock/strings.xml | 211 +++ .../values-eu-rES/youtube/strings.xml | 1391 ++++++++++++++++ .../values-fa-rIR/shared-youtube/strings.xml | 10 +- .../values-fa-rIR/youtube/strings.xml | 6 +- .../values-fi-rFI/shared-youtube/strings.xml | 10 +- .../values-fi-rFI/youtube/strings.xml | 6 +- .../values-fil-rPH/shared-youtube/strings.xml | 10 +- .../values-fil-rPH/youtube/strings.xml | 6 +- .../values-fr-rFR/shared-youtube/strings.xml | 10 +- .../values-fr-rFR/youtube/strings.xml | 6 +- .../values-ga-rIE/shared-youtube/strings.xml | 10 +- .../values-ga-rIE/youtube/strings.xml | 6 +- .../values-gl-rES/shared-youtube/strings.xml | 2 +- .../values-hi-rIN/shared-youtube/strings.xml | 10 +- .../values-hi-rIN/youtube/strings.xml | 6 +- .../values-hr-rHR/shared-youtube/strings.xml | 10 +- .../values-hr-rHR/youtube/strings.xml | 6 +- .../values-hu-rHU/shared-youtube/strings.xml | 10 +- .../values-hu-rHU/youtube/strings.xml | 6 +- .../values-hy-rAM/music/strings.xml | 617 +++++++ .../values-hy-rAM/reddit/strings.xml | 121 +- .../values-hy-rAM/shared-youtube/strings.xml | 237 +++ .../values-hy-rAM/shared/strings.xml | 198 +++ .../values-hy-rAM/sponsorblock/strings.xml | 207 +++ .../values-hy-rAM/youtube/strings.xml | 1407 ++++++++++++++++ .../values-in-rID/shared-youtube/strings.xml | 10 +- .../values-in-rID/youtube/strings.xml | 6 +- .../values-is-rIS/shared-youtube/strings.xml | 10 +- .../values-is-rIS/youtube/strings.xml | 6 +- .../values-it-rIT/shared-youtube/strings.xml | 10 +- .../values-it-rIT/youtube/strings.xml | 6 +- .../values-iw-rIL/shared-youtube/strings.xml | 10 +- .../values-iw-rIL/youtube/strings.xml | 6 +- .../values-ja-rJP/music/strings.xml | 2 +- .../values-ja-rJP/shared-youtube/strings.xml | 10 +- .../values-ja-rJP/youtube/strings.xml | 62 +- .../values-km-rKH/shared-youtube/strings.xml | 2 +- .../values-kmr-rTR/shared-youtube/strings.xml | 4 +- .../values-kmr-rTR/youtube/strings.xml | 2 - .../values-ko-rKR/music/strings.xml | 16 +- .../values-ko-rKR/shared-youtube/strings.xml | 10 +- .../values-ko-rKR/youtube/strings.xml | 10 +- .../values-ky-rKG/shared-youtube/strings.xml | 2 +- .../values-lt-rLT/shared-youtube/strings.xml | 10 +- .../values-lt-rLT/youtube/strings.xml | 6 +- .../values-lv-rLV/shared-youtube/strings.xml | 2 +- .../values-mai-rIN/shared-youtube/strings.xml | 2 +- .../values-mai-rIN/youtube/strings.xml | 2 - .../values-mk-rMK/shared-youtube/strings.xml | 4 +- .../values-mk-rMK/youtube/strings.xml | 2 - .../values-mr-rIN/youtube/strings.xml | 2 - .../values-ms-rMY/shared-youtube/strings.xml | 4 +- .../values-nb-rNO/shared-youtube/strings.xml | 10 +- .../values-nb-rNO/youtube/strings.xml | 6 +- .../values-nl-rNL/shared-youtube/strings.xml | 10 +- .../values-nl-rNL/youtube/strings.xml | 6 +- .../values-pl-rPL/shared-youtube/strings.xml | 10 +- .../values-pl-rPL/youtube/strings.xml | 8 +- .../values-pt-rBR/shared-youtube/strings.xml | 10 +- .../values-pt-rBR/youtube/strings.xml | 6 +- .../values-pt-rPT/shared-youtube/strings.xml | 10 +- .../values-pt-rPT/youtube/strings.xml | 6 +- .../values-ro-rRO/shared-youtube/strings.xml | 10 +- .../values-ro-rRO/youtube/strings.xml | 6 +- .../values-ru-rRU/shared-youtube/strings.xml | 10 +- .../values-ru-rRU/youtube/strings.xml | 6 +- .../values-si-rLK/shared-youtube/strings.xml | 2 +- .../values-sk-rSK/shared-youtube/strings.xml | 10 +- .../values-sk-rSK/youtube/strings.xml | 6 +- .../values-sl-rSI/shared-youtube/strings.xml | 10 +- .../values-sl-rSI/youtube/strings.xml | 6 +- .../values-sr-rCS/shared-youtube/strings.xml | 10 +- .../values-sr-rCS/youtube/strings.xml | 6 +- .../values-sr-rSP/shared-youtube/strings.xml | 10 +- .../values-sr-rSP/youtube/strings.xml | 6 +- .../values-sv-rSE/shared-youtube/strings.xml | 10 +- .../values-sv-rSE/youtube/strings.xml | 6 +- .../values-ta-rIN/shared-youtube/strings.xml | 2 +- .../values-te-rIN/shared-youtube/strings.xml | 2 +- .../values-th-rTH/shared-youtube/strings.xml | 10 +- .../values-th-rTH/youtube/strings.xml | 6 +- .../values-tr-rTR/music/strings.xml | 2 +- .../values-tr-rTR/shared-youtube/strings.xml | 10 +- .../values-tr-rTR/youtube/strings.xml | 17 +- .../values-uk-rUA/shared-youtube/strings.xml | 10 +- .../values-uk-rUA/youtube/strings.xml | 13 +- .../values-uz-rUZ/youtube/strings.xml | 2 - .../values-vi-rVN/shared-youtube/strings.xml | 10 +- .../values-vi-rVN/youtube/strings.xml | 6 +- .../values-zh-rCN/shared-youtube/strings.xml | 10 +- .../values-zh-rCN/youtube/strings.xml | 6 +- .../values-zh-rTW/shared-youtube/strings.xml | 10 +- .../values-zh-rTW/youtube/strings.xml | 6 +- .../values-zu-rZA/shared-youtube/strings.xml | 7 +- .../values-zu-rZA/youtube/strings.xml | 2 - 136 files changed, 11621 insertions(+), 371 deletions(-) diff --git a/patches/src/main/resources/addresources/values-af-rZA/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-af-rZA/shared-youtube/strings.xml index 97c111c03d..4d1f963860 100644 --- a/patches/src/main/resources/addresources/values-af-rZA/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-af-rZA/shared-youtube/strings.xml @@ -18,10 +18,12 @@ Second \"item\" text" --> - Opstart-animasie - Deaktiveer - Kleur - Monochroom + Opstart-animasie + Deaktiveer + Kleur + Kleur Morphe + Monochroom + Monochroom Morphe Herbegin om die opstelling te voltooi diff --git a/patches/src/main/resources/addresources/values-af-rZA/youtube/strings.xml b/patches/src/main/resources/addresources/values-af-rZA/youtube/strings.xml index 5239cabc25..59042218cb 100644 --- a/patches/src/main/resources/addresources/values-af-rZA/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-af-rZA/youtube/strings.xml @@ -837,8 +837,6 @@ Om dit reg te stel, skakel 'Vervals videostrome' aan en verander die 'Verstekkli Nooit wys nie Show when unmuted Wys wanneer gedemp - Wys outo-onderskrifte - Verberg outo-onderskrifte Voorkeur-onderskriftaal Kies eers verskaffer-onderskrifte, en vertaal hulle dan outomaties na die voorkeurtaal @@ -1250,6 +1248,7 @@ Beperking: Indien 'Herstel ou video-aksiebalk' afgeskakel is, mag die 'Meer vide Verander die statusbalk sodat dit nie die boonste barkies bedek nie Hou die uitleg van die statusbalk konsekwent, sodat dit nie die boonste barkies bedek en geen spasie bo die speler los nie. Skakel dit af as die app ineenstort of die boonste barkies verkeerd lyk + Gloei Ligte tema-agtergrond Wit Jou Materiaal (Wit) @@ -1272,6 +1271,9 @@ Beperking: Indien 'Herstel ou video-aksiebalk' afgeskakel is, mag die 'Meer vide Stel die sekondêre kleur van die videovorderingsbalk Ongeldige soekbalk kleurwaarde + Oorspronklike analoog + Oorspronklike koek + Oorspronklike metaal Header logo Verstek diff --git a/patches/src/main/resources/addresources/values-am-rET/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-am-rET/shared-youtube/strings.xml index e5cb68ab09..a851c60154 100644 --- a/patches/src/main/resources/addresources/values-am-rET/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-am-rET/shared-youtube/strings.xml @@ -18,10 +18,12 @@ Second \"item\" text" --> - የመጀመሪያ ማስጀመሪያ አኒሜሽን - ተዘግቷል - ቀለም - ሞኖክሮም + የመጀመሪያ ማስጀመሪያ አኒሜሽን + ተዘግቷል + ቀለም + የቀለም Morphe + ሞኖክሮም + ሞኖክሮም Morphe ቅንብሩን ለማጠናቀቅ እንደገና ያስነሱ diff --git a/patches/src/main/resources/addresources/values-am-rET/youtube/strings.xml b/patches/src/main/resources/addresources/values-am-rET/youtube/strings.xml index a4d29efdd9..e2e5093c08 100644 --- a/patches/src/main/resources/addresources/values-am-rET/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-am-rET/youtube/strings.xml @@ -837,8 +837,6 @@ OpenRouter.aiን ለመክፈት እዚህ ይጫኑ" በጭራሽ አታሳይ ድምጽ ሲበራ አሳይ ድምጽ ሲጠፋ አሳይ - አውቶማቲክ የግርጌ ጽሁፎችን አሳይ - ራስ-ሰር የግርጌ ጽሁፎችን ደብቅ የሚመረጥ የልባዊ ጽሁፍ ቋንቋ መጀመሪያ የአቅራቢውን ንዑስ ርዕስ ይመርጣል፣ ከዚያም ወደሚመረጠው ቋንቋ በራስ-ሰር ይተረጉማል @@ -1250,6 +1248,7 @@ OpenRouter.aiን ለመክፈት እዚህ ይጫኑ" የሁኔታ ማሳወቂያ ስትሪክ የላይኛውን ባሮች እንዳይሸፍን ያስተካክላል የሁኔታ ማሳወቂያ ስትሪክ አቀማመጥ ወጥ እንዲሆን በማድረግ የላይኞቹን ባሮች እንዳይሸፍን እና ከቪዲዮ ማጫወቻው በላይ ምንም ባዶ ቦታ እንዳይኖር ያደርጋል። መተግበሪያው ካቆመ ወይም የላይኞቹ ባሮች የተሳሳተ አቀማመጥ ካሳዩ ይህንን ያጥፉት + ብልጭታ የቀላል ጭብጥ ዳራ ነጭ Material You (ነጭ) @@ -1272,6 +1271,9 @@ OpenRouter.aiን ለመክፈት እዚህ ይጫኑ" የቪዲዮው ሂደት አመልካች ሁለተኛ ቀለም ይወስኑ የተሳሳተ የseekbar ቀለም ዋጋ + ኦሪጅናል አናሎግ + ኦሪጅናል ኬክ + ኦሪጅናል ሜታል የራስጌ አርማ መደበኛ diff --git a/patches/src/main/resources/addresources/values-ar-rSA/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-ar-rSA/shared-youtube/strings.xml index e79189474e..e18564bec8 100644 --- a/patches/src/main/resources/addresources/values-ar-rSA/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ar-rSA/shared-youtube/strings.xml @@ -18,10 +18,12 @@ Second \"item\" text" --> - تأثير حركة بدء التشغيل - معطّل - اللون - أحادية اللون + تأثير حركة بدء التشغيل + معطّل + اللون + تلوين Morphe + أحادي اللون + Morphe أحادي اللون إعادة التشغيل لإنهاء الإعداد diff --git a/patches/src/main/resources/addresources/values-ar-rSA/youtube/strings.xml b/patches/src/main/resources/addresources/values-ar-rSA/youtube/strings.xml index 674e330f6b..e7dfbb45d3 100644 --- a/patches/src/main/resources/addresources/values-ar-rSA/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ar-rSA/youtube/strings.xml @@ -841,8 +841,6 @@ Mistral Nemo هو الخيار الأرخص، بينما توفر Gemma 4 أعل عدم عرضها أبدًا عرضها عندما يكون الصوت غير مكتوم عرضها عند كتم الصوت - عرض الترجمة التلقائية - إخفاء الترجمة التلقائية لغة الترجمة المفضلة يقوم باختيار ترجمات المزود أولًا، ثم يترجم تلقائيًا إلى اللغة المفضلة @@ -1117,19 +1115,19 @@ Mistral Nemo هو الخيار الأرخص، بينما توفر Gemma 4 أعل Automotive "تتضمن التغييرات ما يلي: -تخطيط الجهاز اللوحي +تصميم الجهاز اللوحي • يتم إخفاء منشورات المنتدى -• إعداد \"التشغيل في الخلاصات\" غير متوفر -• زرا \"ريمكس\" و\"الصوت\" غير متوفرين في Shorts -• قد لا تتوفر تراكبات \"مزيد من الفيديوهات\" +• إعداد التشغيل في الخلاصات غير متوفر +• زر ريمكس وزر الصوت غير متوفرين في Shorts +• قد لا تتوفر واجهات 'مزيد من الفيديوهات' • يظهر شريط إجراءات الفيديو القديم غير المطوي • قد لا تعمل عناصر القائمة المنبثقة بشكل صحيح -تخطيط السيارات +تصميم Automotive • يتم تنظيم الخلاصة حسب المواضيع والقنوات -• إعداد \"التشغيل في الخلاصات\" غير متوفر -• زرا \"ريمكس\" و\"الصوت\" غير متوفرين في Shorts -• قد لا تتوفر تراكبات \"مزيد من الفيديوهات\" +• إعداد التشغيل في الخلاصات غير متوفر +• زر ريمكس وزر الصوت غير متوفرين في Shorts +• قد لا تتوفر واجهات 'مزيد من الفيديوهات' • تفتح فيديوهات Shorts في المشغل العادي • يظهر شريط إجراءات الفيديو القديم غير المطوي" تمكين تخطيط الجهاز اللوحي في المشغل @@ -1254,6 +1252,7 @@ Mistral Nemo هو الخيار الأرخص، بينما توفر Gemma 4 أعل إصلاح شريط الحالة بحيث لا يغطي الشريط العلوي يحافظ على تخطيط شريط الحالة بشكل متسق، بحيث لا يغطي الأشرطة العلوية ولا يترك مساحة فارغة فوق المشغل. قم بإيقاف تشغيل هذا إذا تعطل التطبيق أو ظهرت الأشرطة العلوية بشكل خاطئ + توهج خلفية المظهر الفاتح أبيض Material You (أبيض) @@ -1276,6 +1275,9 @@ Mistral Nemo هو الخيار الأرخص، بينما توفر Gemma 4 أعل تعيين اللون الثانوي لشريط تقدم الفيديو لون شريط التقدم غير صالح + الأصلية تماثلية + الأصلية كعكة + الأصلية معدن علامة الشعار الافتراضي diff --git a/patches/src/main/resources/addresources/values-as-rIN/music/strings.xml b/patches/src/main/resources/addresources/values-as-rIN/music/strings.xml index ccfea4ed71..6021bb398e 100644 --- a/patches/src/main/resources/addresources/values-as-rIN/music/strings.xml +++ b/patches/src/main/resources/addresources/values-as-rIN/music/strings.xml @@ -17,58 +17,677 @@ Second \"item\" text" --> + শেষবাৰৰ বাবে চালিত + চাবস্ক্ৰিপচন + ভিডিঅ\' + অন্যান্য ফলাফল + আপলোড কৰা গানবোৰ + আপলোড কৰা এলবামবোৰ + আপলোড কৰা শিল্পীসকল + পজ আৰু ৰিজুম ব্যৱহাৰ কৰিবলৈ হোস্টৰ Jam পেচটো আপডেট কৰক + Jam queue শ্বেয়াৰিং + কাষৰ কোনো এটা Jam হোস্ট কৰক বা যোগদান কৰক আৰু Jam Layer-ৰ জৰিয়তে ইয়াৰ কিউ সম্পাদনা কৰক + Jam queue শ্বেয়াৰিং সক্ৰিয় কৰক + YouTube Music পুনৰ আৰম্ভ হোৱাৰ পিছত Jam সংৰচনাটো সক্ৰিয় কৰে + কাম কৰোৱাবলৈ Jam Layer ডাউনলোড কৰক + আপোনাৰ ব্ৰাউজাৰত শেহতীয়া Jam Layer ৰিলিজটো খোলক + Jam নিয়ন্ত্ৰক + কাষৰ কোনো এটা Jam হোস্ট কৰক বা যোগদান কৰক আৰু ছেছনটো পৰিচালনা কৰক + Jam Layer প্যাকেজ + কাষৰ কিউ শেয়াৰ কৰিবলৈ ব্যৱহাৰ কৰা সংস্থাপিত কম্প্যানিয়ন অ্যাপটো বাছনি কৰক + এটা সংস্থাপিত কম্প্যানিয়ন পেকেজ ব্যৱহাৰ কৰক। পেয়ৰিং কৰিবলৈ আপোনাৰ অনুমোদন আৰু এটা লোকাল কেপাবিলিটি টোকেনৰ প্ৰয়োজন + Wi-Fi অন কৰিবনে? + Wi-Fi অন কৰক, তাৰ পিছত Jam আৰম্ভ কৰিবলৈ বা যোগদান কৰিবলৈ পুনৰ চেষ্টা কৰক + Wi-Fi ছেটিংছ খোলক + বাতিল কৰক + প্যাকেজটো ব্যৱহাৰ কৰক + এটা বৈধ Android package নাম লিখক + পেকেজ সংৰক্ষিত হৈছে। সংযোগ কৰিবলৈ Jam Layer ছেট আপ কৰক + Jam বিচ্ছিন্ন হৈছে + প্ৰথমে Jam Layer সক্ৰিয় কৰক + Settings-ত Jam queue sharing সক্ৰিয় কৰক, তাৰ পিছত YouTube Music পুনৰ আৰম্ভ কৰক + প্ৰথমে নিৰ্বাচিত Jam Layer package-টো সংস্থাপন কৰক + হোষ্টত দেখুওৱা ক’ডটো লিখক। Wi-Fi Aware-এ ওচৰৰ ডিভাইচবোৰক সংযোগ কৰিব পাৰে যদিও সিহঁতৰ LAN পৃথক থাকে + এটা কোডৰ সৈতে যোগ দিয়ক + যোগ দিয়ক + QR স্কেন কৰক + আঠ-অক্ষৰৰ কোডটো লিখক অথবা আমন্ত্ৰণটো পেষ্ট কৰক + আপোনাৰ Jam-ত যোগ দিয়ক + কোড কপি কৰক + Jam কোড + QR দেখুৱাওক + সম্পন্ন + আপোনাৰ Jam-লৈ আমন্ত্ৰণ জনাওক + এই কিউটোত যোগদান কৰিবলৈ আৰু সম্পাদনা কৰিবলৈ স্কেন কৰক। Jam শেষ কৰিলে আমন্ত্ৰণ বাতিল হ’ব + লিংক কপি কৰক + Jam আমন্ত্ৰণ + Jam পুনৰ সংযোগ হৈ আছে; কিছু সময় পিছত পুনৰ চেষ্টা কৰক + Jam-ত যোগ কৰা হৈছে + Jam উপলব্ধ নহয়; ট্ৰেকটো যোগ কৰা হোৱা নাছিল + আপোনাৰ Jam + একেলগে শুনিছে + Jam-ত যোগদান কৰিছে + একেলগে শুনক + Jam আপডেট কৰা হৈছে… + Wi-Fi-ত আপোনাৰ হোষ্টক বিচাৰিছে… + মানুহৰ বাবে অপেক্ষা কৰিছে + %s জন ব্যক্তি সংযোগ হৈছে + %s জন ব্যক্তি সংযোগ হৈছে + কাষৰ এটা ডিভাইচত এই ক’ডটো লিখক। Wi-Fi Aware সমৰ্থিত হ’লে পৃথক LAN-ৰ মাজতো কাম কৰে। %s মিনিটত শেষ হ’ব + %s লৈ যাব নেকি? + সম্পাদনা খোলা আছে + সম্পাদনা লক কৰা হৈছে + Wi-Fi Aware-ৰ জৰিয়তে সংযুক্ত হৈছে + Wi-Fi-ৰ জৰিয়তে সংযুক্ত হৈছে + Bluetooth-ৰ জৰিয়তে সংযুক্ত হৈছে + Jam-ৰ সময়ত সমৰ্থন নকৰা হয় + যোগদান বাতিল কৰা হৈছে + আপোনাৰ হোষ্টৰ সৈতে পুনৰ সংযোগ কৰা হৈছে… + এটা কিউ শ্বেয়াৰ কৰক। সকলোকে গান যোগ কৰিবলৈ দিয়ক + মানুহক আমন্ত্ৰণ কৰক + অতিথিসকলে কিউটো সম্পাদনা কৰিব পাৰে + Jam শেষ কৰক + হোষ্টই সংগীত বজাব। YouTube Music কিউ-ত পোনপটীয়াকৈ গান যোগ কৰক, স্থান সলনি কৰক বা আঁতৰ কৰক + দুয়োটা ডিভাইচ কাষতে ৰাখক। আমি হোষ্টক বিচাৰি থকাৰ সময়ত আপুনি বাতিল কৰিব পাৰে + যোগদান বাতিল কৰক + Jam ত্যাগ কৰক + এটা Jam আৰম্ভ কৰক + Jam Layer ছেট আপ কৰক + পৰিচালনা কৰক + বিৱৰণ + খোলক + গানৰ বিকল্পসমূহ লোড হৈছে… + গানৰ বিকল্পসমূহ উপলব্ধ নহয়; পুনৰ চেষ্টা কৰক + গানৰ বিকল্পসমূহ খুলিব নোৱাৰিল + Jam Layer ডাউনলোড কৰিবলৈ কোনো ব্ৰাউজাৰ উপলব্ধ নহয় + হোষ্ট কিউৰ বাবে অপেক্ষা কৰিছে + প্ৰথমে YouTube Music-ত এটা গান খোলক + হোষ্টত প্লে কৰক + Jam ত্যাগ কৰক আৰু স্থানীয়ভাৱে প্লে কৰক + Jam-ত পিছৰটো প্লে কৰক + Jam কিউত যোগ কৰক + এই Jam ট্ৰেকটো প্লে কৰক + ক’ত প্লে কৰিব বিচাৰে বাছনি কৰক + হোষ্টক ইয়ালৈ স্থানান্তৰ কৰক + গানটো seek কৰিবলৈ সাজু হোৱা নাই + কিউ সলনি হৈছে; জেচচাৰটো পুনৰ চেষ্টা কৰক + হেডাৰ ল’গ’ + ডিফল্ট + কাষ্টম + এলবাম অডিঅ\' ট্ৰেকসমূহ প্লে কৰক + মিউজিক ভিডিঅ\'ৰ পৰিৱৰ্তে গানবোৰৰ এলবাম অডিঅ\' সংস্কৰণটো প্লে কৰক। ইয়াৰ বাবে \'Spoof video streams\' প্ৰয়োজনীয় + অডিঅ\'ভিডিঅ\'ৰ সুইচ সক্ৰিয় কৰক + অডিঅ’ ট্ৰেক সংস্কৰণ আৰু অফিচিয়েল মিউজিক ভিডিঅ’ৰ মাজত পৰিৱৰ্তন কৰিবলৈ চেষ্টা কৰক + তৃতীয়-পক্ষৰ ডাউনলোড এপৰ সহায়ত সংগীত ভিডিঅ’, পডকাষ্ট বা গান ডাউনলোড কৰক + Download মেনুৰ বাবে বাহ্যিক ডাউনলোডাৰ ব্যৱহাৰ কৰক + মেনুৰ Download বিকল্পটোৱে তাৰ পৰিৱৰ্তে আপোনাৰ বাহ্যিক ডাউনলোডাৰটো খুলিব + ডাউনলোডসমূহ + "কোনো ডাউনলোড নাই +ডাউনলোড কৰা ট্ৰেকসমূহ ইয়াত দেখুৱাব" + %1$s • %2$s • %3$s MB + %1$s ট্ৰেক • %2$s MB + ডাউনলোডসমূহৰ বাবে সন্ধান কৰক + ক্ৰমবিন্যাস + শিল্পী অনুসৰি সৰ্ট কৰক + শিৰোনাম অনুসৰি সৰ্ট কৰক + আকাৰ অনুসৰি সৰ্ট কৰক + সকলো ডাউনলোড মচি পেলাওক + সকলো ডাউনলোড মচি পেলাব নেকি? + এই ডিভাইচৰ পৰা সকলো ডাউনলোড কৰা ট্ৰেক মচি পেলোৱা হ\'ব + ডাউনলোডবোৰ মচি পেলোৱা হৈছে + একো পোৱা নগ\'ল + %s ৰ বাবে কাৰ্যসমূহ + চালক + চলাওক বা ৰখক + পূৰ্বৰ + পিছৰটো + ৰখা + বন্ধ কৰক + মচি পেলাও + ডাউনলোড মচি পেলাওক + ডাউনলোডটো মচি পেলাব নেকি? + %s এই ডিভাইচৰ পৰা মচি পেলোৱা হ\'ব + ডাউনলোডটো মচি পেলোৱা হ\'ল + ডাউনলোডটো মচিব নোৱাৰিল + ডাউনলোডটো বজাব নোৱাৰিল + অডিঅ ডাউনলোড + অফলাইন প্লেবেক + ডাউনলোড কৰিবলগীয়া ট্ৰেকটো চিনাক্ত কৰিব নোৱাৰিল + ডাউনলোড ইতিমধ্যে চলি আছে + ইতিমধ্যে ডাউনলোড কৰা হৈছে + ডাউনলোড আৰম্ভ হৈছে + ডাউনলোড আৰম্ভ কৰা হৈছে… + ডাউনলোড হৈছে %s%% + ডাউনলোড সম্পূৰ্ণ হৈছে + ডাউনলোড বিফল হৈছে + Morpheৰ বিষয়ে + বিজ্ঞাপন + ফিড + সাধাৰণ + প্লেয়াৰ + গীতৰ কথা + বিবিধ + YouTube Music ভাষা + চিস্টেমৰ ভাষা + Android 13 বা তাৰ পিছৰ সংস্কৰণৰ প্ৰয়োজন + \'Get Music Premium\' লেবেলটো লুকুওক + Music Premium প্ৰচাৰসমূহ ঢাকি থোৱক + ভিডিঅ\' বিজ্ঞাপনবোৰ লুকুওক + অডিও / ভিডিঅ\' টগেলটো লুকুৱাই ৰাখক + ডাউনলড বুটনটো লুকুৱাওক + পডকাস্ট শ্ব’ত ডাউনলোড বুটনটো লুকুৱায় + Explore শ্বেল্ফটো লুকুৱাই ৰাখক + ফিল্টাৰ বাৰটো লুকুৱাওক + গ্ৰিড শ্বেল্ফবোৰ লুকুৱাই ৰাখক + অনুভূমিক শ্বেল্ফসমূহ লুকুৱাওক + লিস্ট শ্বেল্ফসমূহ লুকুৱাওক + লিৰিকছ পেনেল + শেয়াৰ বুটামটো লুকুৱাওক + অনুবাদ বুটামটো লুকুৱাওক + \'New from\' শ্বেল্ফটো লুকুৱাওক + প্লেলিষ্ট শ্বেল্ফসমূহ লুকুৱাওক + ৰিপিট বুটামটো লুকুৱাওক + চাফেল বুটামটো লুকুৱাওক + \'স্পীড ডায়েল\' শ্বেল্ফটো লুকুৱাওক + মন্তব্য + মন্তব্য অংশত কোনবোৰ উপাদান দেখা যাব তাৰ বাবে বাছনি কৰক + কমিউনিটি গাইডলাইনসমূহ লুকুৱাওক + প্ৰসংগ বাৰ্তাটো লুকুৱাওক + ইমজি বুটামটো লুকুৱাওক + ইনফো বুটামটো লুকুৱাওক + টাইমষ্টেম্প বুটামটো লুকুৱাওক + ক্ৰচফেড + ট্ৰেকবোৰক ইটোৰ পিছত সিটো মসৃণভাৱে মিলাই দিয়ক + ক্ৰচফেড সক্ৰিয় কৰক + ফেড কাৰ্ভ + সমান শক্তি + সুত্ৰীয় ধৰণ + মৃদু ইজ + স্মুদ S-কাৰ্ভ + বাহিৰ হোৱা + আহা + ক্ৰছফেড স্থায়িত্ব + ২৫০ মিলিসেকণ্ড + ৫০০ মিলিসেকণ্ড + ৭৫০ মিলিসেকণ্ড + ১ ছেকেণ্ড + ২ ছেকেণ্ড + ৩ ছেকেণ্ড + ৪ ছেকেণ্ড + ৫ ছেকেণ্ড + ৬ ছেকেণ্ড + ৭ চেকেণ্ড + ৮ ছেকেণ্ড + ৯ ছেকেণ্ড + ১০ ছেকেণ্ড + ১১ ছেকেণ্ড + ১২ ছেকেণ্ড + ম্যানুৱেল স্কিপত ক্ৰছফেইড + যেতিয়া আপুনি ম্যানুৱেলকৈ পৰৱৰ্তী ট্ৰেকলৈ স্কিপ কৰে তেতিয়া ক্ৰছফেইড ইফেক্ট প্ৰয়োগ কৰে + অটো-এডভান্সত ক্ৰছফেইড + এটা ট্ৰেক স্বাভাৱিকভাৱে শেষ হোৱাৰ পিছত ক্ৰছফেইড এফেক্ট প্ৰয়োগ কৰে + ছেচন নিয়ন্ত্ৰণ + ক্ৰছফেইড সাময়িকভাৱে বন্ধ কৰিবলৈ বা পুনৰ আৰম্ভ কৰিবলৈ শ্বাফেল বুটামটোত দীৰ্ঘসময়ৰ বাবে টিপি ৰাখক + এই ছেচনৰ বাবে ক্ৰছফেইড বন্ধ কৰা হৈছে + ক্ৰছফেইড পুনৰ আৰম্ভ কৰা হৈছে + ক্ৰচফেইড সক্ৰিয় থাকোতে ভিডিঅ\' মোড উপলব্ধ নহয় + SABR প্লেবেৰ সৈতে ক্ৰচফেইড বন্ধ হৈ আছে + কাস্টিং কৰাৰ সময়ত ক্ৰচফেইড বন্ধ হৈ থাকে + ই কেনেকৈ কাম কৰে + "Crossfade-এ আপোনাৰ স্থায়িত্ব আৰু কৰ্ভ ব্যৱহাৰ কৰি এটা গানৰ শেষৰ অংশটোক আন এটা গানৰ আৰম্ভণিৰ সৈতে মিলাই দিয়ে + +ই অটম-এডভান্স আৰু মেনুৱেল স্কিপত চলে, যাৰ প্ৰত্যেকৰে নিজস্ব টগল আছে। অটম-এডভান্সৰ ক্ষেত্ৰত গানটি শেষ হোৱাৰ কেইছেকেণ্ডমান আগতে ব্লেন্ডিং আৰম্ভ হয়" + সীমাবদ্ধতাসমূহ + "ক্ৰচফেইড অন হৈ থকাৰ সময়ত মিউজিক ভিডিঅ’সমূহ কেৱল অডিঅ’ হিচাপে চলে। ক্ৰচফেইড পজ কৰি ভিডিঅ’টো চাবলৈ শ্বাফেলত লং-প্ৰেছ কৰক + +প্লেবেক আৰম্ভ হোৱা এটা মিউজিক ভিডিঅ’ পৰৱৰ্তী গানটোত পৰ্যন্ত ভিডিঅ’ মোডত থাকে + +ফেইড ডিউৰেচনতকৈ চুটি গানবোৰ ক্ৰচফেইড নহয় + +লক স্ক্ৰীন বা নটিফিকেচনত সংক্ৰমণৰ মাজত ক্ষন্তেকৰ বাবে পুৰণি অৱস্থা দেখুৱাব পাৰে + +মিউজিক ভিডিঅ’ এটা চাবলৈ ক্ৰচফেইড পজ কৰিলে কিছুমান ভিডিঅ’ ক’লা হৈ থাকিব পাৰে। গানটো বাছনি কৰাৰ আগতে ক্ৰচফেইড পজ কৰিলে ইয়াৰ পৰিহাৰ কৰিব পাৰি" + সমৰ্থন কৰা হোৱা নাই + "অডিঅ’ কাস্টিং বা মিৰৰিং (Chromecast, স্ক্ৰীন বা অডিঅ’ মিৰৰিং) + +SABR প্লেবেক (স্পুফ ভিডিঅ’ ষ্ট্ৰিমবোৰ বন্ধ কৰা হৈছে)" + কৃতজ্ঞতা + VazerOG ৰ দ্বাৰা ক্ৰচফেড বৈশিষ্ট্য + ৰিপিট অৱস্থাটো মনত ৰাখক + চাফল অৱস্থাটো মনত ৰাখক + প্লেবেকেৰ গতি + সকলো ট্ৰেকৰ বাবে ব্যৱহাৰ কৰা গতি। ১০০% ত এপৰ গতি ব্যৱহাৰ কৰা হয়, যেনে পডকাস্ট প্লেবেকেৰ গতি + গতিৰ সৈতে পিচ পৰিৱৰ্তন কৰক + ডিসলাইক ৰিডাইৰেকচন নিষ্ক্ৰিয় কৰক + ডিসলাইক বুটৰটো টিপালে পৰৱৰ্তী ট্ৰেকলৈ যোৱাটো প্ৰতিৰোধ কৰে + প্লেলিষ্ট ট্ৰেক টেপ একচন + প্লেলিষ্টটো বজাওক + কেৱল নিৰ্বাচিত ট্ৰেকটো বজাওক + নিৰ্বাচিত ট্ৰেকটো কিউত যোগ কৰক + কিউত যোগ কৰা হৈছে + Cast বুটামটো লুকুৱাওক + History বুটামটো লুকুৱাওক + Notification বুটামটো লুকুৱাওক + চৰ্চা বুটামটো লুকুৱাওক + ভইচ চাৰ্চ বুটামটো লুকুওক + চাউণ্ড চাৰ্চ বুটামটো লুকুওক + New বুটামটো লুকুওক + Library টেবৰত New বুটামটো লুকুৱায় + কাৰ্য বুটামসমূহ + প্লেয়াৰৰ তলত কোনটো কাৰ্য বুটাম দেখাব সেয়া বাছনি কৰক + কাৰ্য বাৰটো লুকুৱাওক + পচন্দ আৰু অপচন্দ বুটামটো লুকুৱাওক + মন্তব্য বুটামটো লুকুৱাওক + \'লাইভ চেট ৰিপ্লে\' বুটামটো লুকুৱাওক + গীতৰ কথা বুটামটো লুকুৱাওক + শেয়াৰ বুটামটো লুকুৱাই ৰাখক + সংৰক্ষণ বুটামটো লুকুৱাই ৰাখক + ডাউনলোড বুটৰৰ ঢাকি থোৱক + \'ৰাডিয়ো আৰম্ভ কৰক\' বুটামটো লুকুৱাওক + ফ্লাইআউট মেনু + প্লেয়াৰ আৰু কিউ ফ্লাইআউট মেনুসমূহত কোনবোৰ কম্পোনেন্ট দেখা যাব তাৰ বাবে বাছনি কৰক + 3-column component লুকুৱাওক + Like আৰু Dislike বুটামসমূহ লুকুৱাওক + \'Taste match\' মেনুটো লুকুৱাওক + \'Add to \'Listen later\' playlist\' মেনুটো লুকুৱাও + \'Add to queue\' মেনুটো লুকুৱাও + Captions মেনু লুকুৱাওক + \'Delete playlist\' মেনুটো লুকুৱাও + \'Dismiss queue\' মেনুটো লুকুৱাও + \'Don\'t recommend this artist\' মেনুটো লুকুৱাও + Download মেনু লুকুৱাওক + \'Edit playlist\' মেনুটো লুকুৱাও + \'Go to album\' মেনুটো লুকুৱাও + \'Go to artist\' মেনুটো লুকুৱাও + \'এপিসোডলৈ যাওক\' মেনুটো লুকুৱাওক + \'পডকাষ্টলৈ যাওক\' মেনুটো লুকুৱাওক + Help মেনু লুকুৱাওক + \'এপিসোডটো প্লে কৰা বুলি চিন্ চিহ্নিত কৰক\' মেনুটো লুকুৱাওক + \'আগ্রহ নাই\' মেনুটো লুকুৱাওক + \'স্পীড ডায়াললৈ পিন কৰক\' মেনুটো লুকুৱাওক + \'পিছৰটো প্লে কৰক\' মেনুটো লুকুৱাওক + Quality মেনু লুকুৱাওক + \'লাইব্ৰেৰীৰ পৰা আঁতৰ কৰক\' মেনুটো লুকুৱাওক + \'প্লেলিষ্টৰ পৰা আঁতৰ কৰক\' মেনুটো লুকুৱাওক + Report মেনু লুকুৱাওক + \'এপিসোডটো পিছৰ বাবে সংৰক্ষণ কৰক\' / \'লাইব্ৰেৰীত সংৰক্ষণ কৰক\' মেনুটো লুকুৱাওক + \'প্লেলিষ্টত সংৰক্ষণ কৰক\' মেনুটো লুকুৱাওক + Share মেনু লুকুৱাওক + \'শ্বাফেল প্লে\' মেনুটো লুকুৱাওক + \'স্লীপ টাইমাৰ\' মেনুটো লুকুৱাওক + \'ৰেডিয়ো আৰম্ভ কৰক\' মেনুটো লুকুৱাওক + \'ষ্ট্যাটচ ফৰ নাৰ্ডছ\' মেনুটো লুকুৱাওক + Subscribe মেনু লুকুৱাওক + \'Unpin from speed dial\' মেনুটো লুকুৱাওক + \'View song credits\' মেনুটো লুকুৱাওক + আৰম্ভণিৰ পৃষ্ঠা + ডিফল্ট + চাৰ্টসমূহ + পিছৰ বাবে এপিসোডসমূহ + অন্বেষণ কৰক + ইতিহাস + লাইব্ৰেৰী + পছন্দ কৰা সংগীত + প্লেলিষ্টসমূহ + সন্ধান কৰক + চাবস্ক্ৰিপশ্বনসমূহ + পৰ্ট্ৰেইট অৰিয়েণ্টেচন বলবৎ কৰক + ডিভাইচটো ঘূৰোৱাৰ সময়ত এপটো পৰ্ট্ৰেইট অৰিয়েণ্টেচনত ৰাখে + Miniplayer ৰ ৰং সলনি কৰক + মিনিপ্লেয়াৰৰ ৰংটো ফুলস্ক্ৰীন প্লেয়াৰৰ সৈতে মিলাই দিয়ে + নেভিগেচন বাৰৰ ৰং সলনি কৰক + নেভিগেচন বাৰৰ ৰংটো ফুলস্ক্ৰীন প্লেয়াৰৰ সৈতে মিল কৰায় + মিনিপ্লেয়াৰত নেক্সট বুটনে দেখুৱাওক + মিনিপ্লেয়াৰত প্ৰিভিয়াছ বুটনে দেখুৱাওক + বাধ্যতামূলক মিনিপ্লেয়াৰ সক্ৰিয় কৰক + নতুন ট্ৰেক আৰম্ভ হোৱাৰ সময়ত প্লেয়াৰটো স্বয়ংক্ৰিয়ভাৱে ফুলস্ক্ৰীনলৈ বিস্তাৰিত হোৱাত বাধা দিয়ে + মিনিপ্লেয়াৰটো আঁতৰাবলৈ সোয়াইপ সক্ৰিয় কৰক + মিনিপ্লেয়াৰটোৰ ওপৰত তললৈ সোয়াইপ কৰি ইয়াক আঁতৰাবৰ পৰমৰ্শ দিয়ে + নেভিগেচন বাৰ + তলৰ নেভিগেচন বাৰত কোনবোৰ বুটাম দেখা যাব তাৰ বাছনি কৰক + মুখ্য পৃষ্ঠা বুটামত লুকুৱাই থোৱক + নমুনা বুটামত লুকুৱাই থোৱক + অন্বেষণ বুটামত লুকুৱাই থোৱক + লাইব্ৰেৰী বুটামত লুকুৱাই থোৱক + আপগ্ৰেড বুটামত লুকুৱাই থোৱক + নেভিগেচন বাৰ লুকুওক + নেভিগেচন বুটামৰ লেবেলবোৰ লুকুওক + লগ ইন কৰা নাই। কনফিগাৰ কৰিবলৈ টেপ কৰক + লগ আউট কৰক + লগ আউট কৰা হ\'ল + Scrobbling সক্ৰিয় কৰক + \'Now Playing\' স্থিতি জমা কৰক + গীতৰ নূন্যতম সময়সীমা + ইয়াৰতকৈ চুটি গানসমূহ scrobble কৰা নহ\'ব + Scrobble প্ৰতিশত পলম + গানটোৰ এইটো প্ৰতিশত শুনাৰ পিছত scrobble ট্ৰিগাৰ কৰক + Scrobble পলমৰ ছেকেণ্ড + এইমান ছেকেণ্ড শুনাৰ পিছত scrobble ট্ৰিগাৰ কৰক + ListenBrainz একাউণ্ট + লগ-ইন কৰা হৈছে। একাউণ্ট পৰিচালনা কৰিবলৈ ট্যাপ কৰক + scrobbling সক্ৰিয় কৰিবলৈ আপোনাৰ ListenBrainz User Token প্ৰদান কৰক + টোকেন + যাচালয় কৰক + অনুগ্ৰহ কৰি এটা টোকেন লিখক + বৈধতা পৰীক্ষা কৰি থকা হৈছে… + বৈধ টোকেন! ব্যৱহাৰকাৰী: %s + অবৈধ টোকেন: %s + অচিনাকী + পৰীক্ষণ বিফল হৈছে: %s + টোকেন মচি পেলোৱা হৈছে + টোকেন সংৰক্ষণ কৰা হৈছে + সতৰ্কবাণী: সংৰক্ষিত টোকেনটো অবৈধ! + পছন্দ কৰা গানবোৰক Love কৰক + আপুনি যেতিয়া YouTube Music-ত গানবোৰ পছন্দ কৰিব তেতিয়া Last.fm-ত স্বয়ংক্ৰিয়ভাৱে Love কৰক (আৰু অপছন্দ কৰিলে Love আঁতৰ কৰক) + Last.fm একাউণ্ট + %s হিচাপে লগ-ইন কৰা হৈছে। একাউণ্টটো পৰিচালনা কৰিবলৈ টেপ কৰক + লগ-ইন কৰিবলৈ আপোনাৰ Last.fm credentials প্ৰদান কৰক + ইউজাৰনেম + পাছৱৰ্ড + লগ ইন কৰক + অনুগ্ৰহ কৰি ইউজাৰনেম আৰু পাছৱৰ্ড লিখক + লগ-ইন কৰি থকা হৈছে… + সফলভাৱে লগ ইন কৰা হ\'ল! + Last.fm ৰ পৰা চেছনটো লাভ কৰাত ব্যৰ্থ হ\'ল + লগইন বিফল হৈছে: %s + Last.fm ক্রেডেন্সিয়েলসমূহ সংৰক্ষিত হৈছে! + মেটাডেটা পৰিষ্কাৰকৰণ + মেটাডেটা পৰিষ্কাৰ কৰক + scrobbling কৰাৰ আগতে শিৰোনাম, শিল্পী আৰু এলবামৰ পৰা সাধাৰণ জৰুৰী নোহোৱা শব্দসমূহ (যেনে \"Official Video\", \"Remaster\", \"Live\") স্বয়ংক্ৰিয়ভাৱে পৰিষ্কাৰ কৰক + কষ্টম regex পৰিষ্কাৰকৰণ + গানৰ শিৰোনাম, শিল্পী বা এলবামৰ পৰা অতিৰিক্ত পাঠ আঁতৰ কৰিবলৈ এটা কাস্টম ৰেগুলৰ এক্সপ্ৰেছন প্ৰৱেশ কৰক। উদাহৰণ: (?i)\\s*\\((clean|explicit)\\) + শিৰোনামৰ পৰা শিল্পী আৰু ট্ৰেক পাৰ্স কৰক + \" - \" পৃথককাৰক ব্যৱহাৰ কৰি ভিডিঅ’ৰ শিৰোনামৰ পৰা শিল্পী আৰু ট্ৰেকৰ নাম সংগ্ৰহ কৰক। পুনৰ আপলোড কৰা সামগ্ৰীৰ বাবে উপযোগী য’ত চেনেলৰ নাম শিল্পী হিচাপে ব্যৱহাৰ হোৱা থাকিব পাৰে + মিচিং এলবাম পূৰণ কৰক + যেতিয়া সংগীতত কোনো এলবাম নাথাকে, তেতিয়া Last.fm-ত ইয়াক খৰুজি চাওক যাতে আপোনাৰ scrobble সম্পূৰ্ণ হয় + "scrobbling-এ আপোনাৰ YouTube Music ব্যৱহাৰক আপোনাৰ Last.fm বা ListenBrainz প্ৰফাইলৰ সৈতে স্বয়ংক্ৰিয়ভাৱে সংলগ্ন কৰে + +বেকগ্ৰাউণ্ড প্লেবেকৰ সময়ত scrobbling সংলগ্ন হৈ আছে বুলি নিশ্চিত কৰিবলৈ, YouTube Music এপৰ বেটাৰী ব্যৱহাৰ \"Unrestricted\"ত ছেট কৰা আছে নে নাই সেয়া নিশ্চিত কৰক" + তৃতীয়-পক্ষৰ গীতৰ কথা ব্যৱহাৰ কৰক + তৃতীয়-পক্ষৰ সেৱাৰ পৰা গীতৰ কথা দেখুৱায়, কোনোটো নোহোৱা হ\'লে বিল্ট-ইন গীতৰ কথালৈ উভতি যায় + গীতৰ কথা চাই থকাৰ সময়ত স্ক্ৰীনটো অন কৰি ৰাখক + গীতৰ কথাৰ পেনেলটো খোলা থকাৰ সময়ত ডিচপ্লে অফ হোৱাত বাধা দিয়ে + লিরিক সেৱা + প্ৰদৰ্শন + চিঙ্ক্ৰোনাইজেচন + পেনেল + AI এন্ডপইণ্ট + সকলো প্ৰদানকাৰী নিষ্ক্ৰিয় কৰা হৈছে - বিল্ট-ইন লিৰিক্স আৰু এমবেডড লিৰিক্স + প্ৰোভাইডাৰৰ ক্ৰম ৰিছেট কৰিবনে? + প্ৰোভাইডাৰসমূহক ডিফল্ট অগ্ৰাধিকাৰ ক্ৰমত পুনৰুদ্ধাৰ কৰক + সংৰক্ষিত কথাৰ স্তৰবোৰ মুছক + এই ডিভাইচত ৰখা গীতৰ কথাখিনি মচি পেলাওক + %1$s MB মুছা হ\'ল + টোকেন লাভ কৰক + Apple Music Media-user-token + "music.apple.com ৰ পৰা media-user-token কুকিটো পেষ্ট কৰক + +পৰ্যায়সমূহ: +১. সদস্য হিচাপে music.apple.com ত লগ-ইন কৰক +২. DevTools → Application → Cookies → music.apple.com খোলক +৩. \"media-user-token\" ৰ মানটো কপি কৰক" + Apple Music: media-user-token সংৰক্ষণ কৰা হ\'ল + Apple Music: media-user-token মচি পেলোৱা হ\'ল + Apple Music: অবৈধ media-user-token - অনুগ্ৰহ কৰি পৰীক্ষা কৰক আৰু পুনৰ চেষ্টা কৰক + Spotify SpDc + "Spotify ৰ পৰা sp_dc কুকিটো পেষ্ট কৰক। + +পৰ্যায়সমূহ: +১. open.spotify.com ত লগ-ইন কৰক +২. DevTools → Application → Cookies → open.spotify.com খোলক +৩. \"sp_dc\" ৰ মানটো কপি কৰক" + Spotify: sp_dc সংৰক্ষিত হৈছে + Spotify: sp_dc মুছা হৈছে + Spotify: অবৈধ sp_dc - অনুগ্ৰহ কৰি পৰীক্ষা কৰক আৰু পুনৰ চেষ্টা কৰক + Apple Music টোকেনটো ঐচ্ছিক। ইয়াৰ অবিহনে, Lyrically-ক বিকল্প হিচাপে ব্যৱহাৰ কৰা হ\'ব + Deezer ARL + "Deezer ৰ পৰা arl কুকিটো পেষ্ট কৰক। + +পৰ্যায়সমূহ: +১. deezer.com ত ছাইন ইন কৰক +২. DevTools → Application → Cookies → deezer.com খোলক +৩. \"arl\" ৰ মানটো কপি কৰক" + Deezer: arl সংৰক্ষিত হৈছে + Deezer: arl মুছা হৈছে + Deezer: অবৈধ arl - অনুগ্ৰহ কৰি পৰীক্ষা কৰক আৰু পুনৰ চেষ্টা কৰক + Musixmatch টোকেন + আপোনাৰ নিজৰ একাউণ্টটো ব্যৱহাৰ কৰিবলৈ কেৱল এটা টোকেন যোগ কৰক। ই Musixmatch Android এপৰ পৰা আহিব লাগিব: musixmatch.com ৰ পৰা কপি কৰা টোকেনবোৰ অৱসৰপ্ৰাপ্ত ডেস্কটপ ক্লায়েন্টৰ আৰু API-য়ে সেইবোৰ গ্ৰহণ নকৰে + Musixmatch টোকেন অবিহনেই কাম কৰে, গতিকে এই ফিল্ডটো খালী থাকিব পাৰে + ব্যৱহাৰকাৰী টোকেন + Musixmatch: ব্যৱহাৰকাৰী টোকেন সংৰক্ষিত হৈছে + Musixmatch: ব্যৱহাৰকাৰী টোকেন মুছা কৰা হৈছে + Musixmatch: অবৈধ ইউজাৰ টোকেন - অনুগ্ৰহ কৰি পৰীক্ষা কৰক আৰু পুনৰ চেষ্টা কৰক + YouTube Cookies + "YouTubeৰ পৰা কেপচন আৰু টাইমটেক্স্ট অনুসন্ধান কৰিবলৈ আপোনাৰ YouTube cookies পেষ্ট কৰক। + + পদক্ষেপসমূহ: +১. ব্ৰাউজাৰত youtube.com খোলক আৰু ছাইন-ইন কৰক +২. DevTools (F12) → Application → Cookies → youtube.com খোলক +৩. সম্পূৰ্ণ কুকি ষ্ট্ৰিংটো কপি কৰক (ছেমিকোলনৰ দ্বাৰা পৃথক কৰা key=value পেয়ৰসমূহ) + +প্ৰয়োজনীয় কুকিসমূহ: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPISID, LOGIN_INFO" + YouTube: কুকিসমূহ সংৰক্ষিত কৰা হৈছে + YouTube: কুকিসমূহ মুছা হৈছে + YouTube: অবৈধ কুকিজ - অনুগ্ৰহ কৰি পৰীক্ষা কৰক আৰু পুনৰ চেষ্টা কৰক + এটা শাৰীত টিপি ধৰক + গীতৰ সেই মুহূৰ্তলৈ যাবলৈ এটা শাৰীত টিপক, আপোনাৰ আঙুলিৰ তলৰ শব্দটো কপি কৰিবলৈ ইয়াক দীঘলীয়া সময়লৈ টিপি ধৰক, অথবা যেতিয়া টিপটো কোনো শব্দৰ ওপৰত নাথাকে তেতিয়া গোটেই শাৰীটো কপি কৰক। অফ কৰিলে এই দুয়োটা কাৰ্য আঁতৰ হয় + ছেকচন টেগসমূহ দেখুৱাওক + গীতৰ প্ৰতিটো নতুন ছেকচনক এটা সৰু টেগৰ দ্বাৰা চিহ্নিত কৰক, যেনে Verse বা Chorus + কপি বুটাম দেখুওক + মূল গীতৰ কথাখিনি প্লেইন টেক্সট হিচাপে কপি কৰিবলৈ টিপক; ৰ’ ফাইলটো ডাউনলোডচত সংৰক্ষণ কৰিবলৈ দীঘলীয়া সময়লৈ টিপি ধৰক + অনুবাদ বুটাম দেখুওক + অনুবাদটো সলনি কৰিবলৈ টিপক; কেৱল অনুবাদৰ বাবে দীঘলীয়া সময়লৈ টিপি ধৰক। ই প্ৰোভাইডৰ অনুবাদ, গুগল ট্ৰেন্সলেট, বা এটা OpenAI-সংগত API ব্যৱহাৰ কৰে + Romanize বুটামটো দেখুৱাওক + ৰোমানাইজেশ্যন সলনি কৰিবলৈ টিপক; কেৱল ৰোমানাইজেশ্যনৰ বাবে দীঘলীয়া সময়লৈ টিপি ধৰক। ই প্ৰোভাইডৰ ৰোমানাইজেশ্যন, গুগল ট্ৰেন্সলেট, বা এটা OpenAI-সংগত API ব্যৱহাৰ কৰে + স্থানীয় ৰোমানাইজেশ্যন আৰু এলাইনমেণ্ট + প্ৰদানকাৰী, আপোনাৰ মডেল আৰু গুগল অনুবাদ ব্যৰ্থ হ\'লে ডিভাইচত ৰোমানাইজ কৰক। পঢ়াটো এটা টাইমড লিৰিকৰ প্ৰতিটো শব্দৰ পিছত থাকে; শব্দ সিংক অন থাকিব লাগিব। চীনা, কেন্টনিজ, জাপানী, কোৰীয়ান, গ্ৰীক, ৰুছিয়ান, জৰ্জিয়ান, আৰমেনিয়ান; প্ৰায় ১২০ MB + %1$s পঢ়ি থকা হৈছে: %2$d%% + পঢ়াবোৰ সাজু আছে (%1$d খন প্ৰৱেশ) + ডাউনলোড বিফল হৈছে - পুনৰ চেষ্টা কৰিবলৈ ইয়াক বন্ধ কৰি আকৌ অন কৰক + Refresh বুটামটো দেখুৱাওক + পিছৰ লিৰিক কেণ্ডিডেটলৈ যাবলৈ টেপ কৰক; গানৰ শিৰোনাম আৰু শিল্পীৰ দ্বাৰা লিৰিক বিচাৰিবলৈ লং-প্ৰেছ কৰক + গীতৰ কথা খোজো + গীতৰ শিৰোনাম + শিল্পী + গীতৰ এটা শিৰোনাম আৰু এজন শিল্পীৰ নাম লিখক + সক্ৰিয় কৰা কোনো প্ৰদানকাৰীয়েই আন গানৰ বাবে সন্ধান কৰিব নোৱাৰে + সংৰক্ষিত + চিঙ্ৰোনাইজ কৰা শব্দ বা শব্দাংশসমূহ হাইলাইট কৰক + উপলব্ধ থাকিলে শব্দ-অনুসৰি লৰিক্সক অগ্ৰাধিকাৰ দিয়ক। যেতিয়া লৰিক্সত শব্দৰ সময় থাকে, ই সংগীতৰ সৈতে মিল খাই প্ৰতিটো শব্দ বা শব্দাংশ হাইলাইট কৰে + বেকগ্ৰাউণ্ড ভ’কালবোৰ পৃথককৈ দেখুৱাওক + ব্ৰেকেটত লিখা বেকগ্ৰাউণ্ড ভ’কাল, যেনে \"(Oh) I want you\", ইয়াক গানৰ লাইনটোৰ পৰা আঁতৰাই এটা পৃথক ম্লান লাইনলৈ স্থানান্তৰ কৰে + বাজি উঠা লৰিক্সসমূহ লুকুৱাওক + বৰ্তমান বাজি থকা লাইনটোৰ ওপৰৰ লৰিক্সৰ লাইনসমূহ লুকুৱাওক + নাবাজি উঠা লৰিক্সসমূহ লুকুৱাওক + বৰ্তমান বাজি থকা লাইনটোৰ তলৰ লৰিক্সৰ লাইনসমূহ লুকুৱাওক + লক স্ক্ৰীনত MediaSession লৰিক্স দেখুৱাওক + লক স্ক্ৰীন, এণ্ড্ৰইড অটো আৰু ব্লুটুথত বৰ্তমানৰ শাৰীটো দেখুৱায়। এণ্ড্ৰইড ১১ বা তাতকৈ নতুন সংস্কৰণৰ প্ৰয়োজন + মিনিপ্লেয়াৰৰ লৰিকছ দেখুৱাওক + মিনিপ্লেয়াৰত বৰ্তমানৰ শাৰীটো দেখুৱায়, য\'ত ট্ৰেকটো চাবটাইটললৈ স্থানান্তৰ কৰা হয় + গানৰ শিৰোনাম/শিল্পীৰ ফৰ্মেট + Miniplayer আৰু MediaSession-ত লৰিকচৰ সৈতে দেখুওৱা ট্ৰেকৰ ফৰ্মেট। {t} হৈছে শিৰোনাম, {a} হৈছে শিল্পী আৰু {v} হৈছে সংস্কৰণ, যেনে Acoustic, যেতিয়া লৰিকচৰ উৎসই এটা প্ৰদান কৰে। দুটা কলোনৰ মাজৰ পাঠ কেৱল তেতিয়াই দেখুওৱা হয় যেতিয়া ইয়াৰ ভিতৰত থকা প্ৰতিটো চলকৰ এটা মান থাকে; এক সাধারণ কলোনৰ বাবে \\: লিখক। লৰিকচ পৰিবৰ্তন হোৱাৰ লগে লগে প্ৰতিটো শাৰী পাৰ্থক্যক্ৰমে দেখুওৱা হয় + লৰিকচ শাৰীৰ ফৰ্মেট + Miniplayer আৰু MediaSession-ত লৰিকচ শাৰীৰ ফৰ্মেট। {lrc} হৈছে শাৰী আৰু {n} হৈছে ই কেইবাৰ ক্ৰমিকভাৱে গোৱা হৈছে। কলোনৰ মাজৰ পাঠ কেৱল শাৰীটো পুনৰাবৃত্তি হোলে দেখুওৱা হয়। কলোনৰ বাবে \\: ব্যৱহাৰ কৰক + ফৰ্মেটটোত {lrc} থাকিব লাগিব + শিল্পী-শাৰী টেমপ্লেট আৰু এনিমেচন টাইমিং + নূন্যতম স্ক্ৰীন সময় (ms) + সৰ্বোচ্চ স্ক্ৰীন সময় (ms) + কেৱল বহুমুৰীয়া এনিমেচনৰ বাবে। প্ৰতিটো ফ্ৰেমক অন্ততঃ নূন্যতম সময়ৰ বাবে ৰাখক। সৰ্বোচ্চ সময়ত, লৰিকৰ কথা একে থাকিলেও শিল্পীৰ শাৰীটো আগবঢ়াওক। + নূন্যতম ০ বা তাতকৈ অধিক হ\'ব লাগে। সৰ্বোচ্চ ০ তকৈ অধিক আৰু অন্ততঃ নূন্যতমৰ সমান হ\'ব লাগে। + লাইনৰ দৈৰ্ঘ্যৰ সীমা + গীতৰ কথা আৰু শিল্পীৰ লাইনৰ সীমা, আৰু সম্পূৰ্ণ-প্ৰস্থ চৰিত্ৰৰ ওজন + "গীতৰ কথাৰ লাইনৰ সীমা +দীঘল গীতৰ কথাবোৰক সন্তুলিত অৰ্ধত বিভক্ত কৰক। 0-এ বিভাজন নিষ্ক্ৰিয় কৰে।" + "শিল্পীৰ লাইনৰ সীমা +পূৰিত ট্ৰেক ফৰ্মেটটোচক্ৰীয় ফ্ৰেমত বিভক্ত কৰক। 0-এ বিভাজন নিষ্ক্ৰিয় কৰে।" + "সম্পূৰ্ণ-প্ৰস্থ চৰিত্ৰৰ ওজন +দুয়োটা সীমায়ৰ ক্ষেত্ৰত প্ৰয়োগ হয়। ১.৭৫ ত, চাৰিটা সম্পূৰ্ণ-প্ৰস্থ চৰিত্ৰক ৭ বুলি গণ্য কৰা হয়।" + 0 ৰ পৰা 10000 লৈ অখণ্ড সংখ্যাৰ সীমা আৰু 0 তকৈ অধিক চৰিত্ৰৰ ওজন লিখক + বাদ্যযন্ত্ৰৰ বিৰতি + এটি ইনষ্ট্ৰুমেণ্টেল ব্ৰেকৰ সময়ত মিনিপ্লেয়াৰ আৰু মিডিয়া চেছনত দেখুওৱা হয়। প্ৰতিটো শাৰী এটা এনিমেচন ফ্ৰেম। {t} হৈছে শিৰোনাম, {a} হৈছে শিল্পী আৰু {lrc} হৈছে শেষৰ লৰিক শাৰীটো + মিনিপ্লেয়াৰ আৰু মিডিয়া চেছনৰ টাইমিং অফছেট + Miniplayer আৰু MediaSession-ত পিছৰ লাইনটো কেতিয়া দেখুৱাব লাগে তাৰ পৰিৱৰ্তন কৰে। যদি লাইনবোৰ পলমকৈ দেখা দিয়ে, উদাহৰণস্বৰূপে লক স্ক্ৰীনত, তেন্তে ঋণাত্মক মান ব্যৱহাৰ কৰক + ফিল্টাৰসমূহ + মেটাডেটা regex + গীতৰ কথা বিচাৰোঁতে আৰু দুটা শিৰোনামৰ মিল আছে নে নাই তাৰ স্কৰিং কৰোঁতে শিৰোনাম, শিল্পী আৰু এলবামত প্ৰয়োগ কৰা Regular expression। দুয়োটাকে নিষ্ক্ৰিয় কৰিবলৈ খালী এৰি দিয়ক। ডিফল্টৰ দ্বাৰা \"Official Video\", \"Remaster\", \"【插曲】\", আৰু \"- Topic\" ৰ দৰে সাধাৰণ আৱৰ্জনা আঁতৰ কৰা হয় + লৰিকছ ফিল্টাৰ regex + কেৱল মূল লৰিকছ পাঠত প্ৰয়োগ কৰা ৰেগুলৰ এক্সপ্ৰেচন (অনুবাদ বা ৰোমানাইজেশ্যনত নহয়)। নিষ্ক্ৰিয় কৰিবলৈ খালী কৰি থৈ দিয়ক + তথ্যৰ লাইনৰ মাৰ্কাৰসমূহ + যদি লৰিকছবোৰ ভুলকৈ তললৈ ফিল্টাৰ হৈছে, তেন্তে ইয়াক সামজস্টিং কৰক বা মুছক। কমাৰে পৃথক কৰা মাৰ্কাৰসমূহ: এটা মাৰ্কাৰৰ পিছত কোলন থকা লাইনবোৰ তলত ক্ৰেডিট তথ্য হিচাপে দেখুওৱা হয়। নিষ্ক্ৰিয় কৰিবলৈ খালী কৰি থৈ দিয়ক + প্ৰথম আখৰটো ডাঙৰ কৰক + গীতৰ প্ৰতিটো লাইনৰ প্ৰথম আখৰটো ডাঙৰ কৰে + পিছৰ বিৰাম চিনসমূহ আঁতৰ কৰক + গীতৰ শাৰীৰ শেষত থকা পূৰ্ণ বিৰাম চিহ্ন, কমা, কোলন আৰু চেমিকোলন আঁতৰ কৰে। প্ৰশ্নবোধক চিহ্ন, বিস্ময়সূচক চিহ্ন, এলিপসিস, উদ্ধৃতি চিহ্ন আৰু ব্ৰেকেটসমূহ ৰখা হয় + টেক্সটৰ আকাৰ + গীতৰ কথাৰ টেক্সটৰ আকাৰ + গ্লোবেল টাইমিং অফছেট + চিঙ্কেট গীতিকাবলি প্লেবেকৰ সৈতে চলে। যদি সেইবোৰ বিচ্যুত হয়, তেন্তে টাইমিং অফছেট ঠিক কৰক। চিঙ্কেট গীতিকাবলি আগত বা পিছত স্থানান্তৰ কৰক। যদি গীতিকাবলি গানটোতকৈ আগত চলে তেন্তে ধনাত্মক মান ব্যৱহাৰ কৰক + লিরিক্স অফছেটৰ বাবে SponsorBlock ছেগমেণ্টসমূহ ব্যৱহাৰ কৰক + SponsorBlock-এ স্কিপ কৰা ছেগমেণ্টসমূহৰ অবিহনে মেচ কৰক আৰু লিরিক্সবোৰ মুক্তিপ্ৰাপ্ত অডিঅ’ৰ সৈতে স্থানান্তৰ কৰক। অফ কৰিলে SponsorBlock-ক সম্পূৰ্ণৰূপে আওনীয়া কৰা হ\'ব + এই ট্ৰেকটোত জমা দিবলৈ কোনো ভিডিঅ’ আইডি নাই + উৎস: %s + এই LRC টো সংৰক্ষণ কৰিবলৈ কপি বুটনটোত লং-প্ৰেছ কৰক; যদি আপুনি টাইমিংবোৰ মিলাই কমিউনিটি প্ৰদানকাৰীসকলৰ সৈতে শ্বেয়াৰ কৰিব পাৰে, তেন্তে আমি কৃতজ্ঞ হম + Google দ্বাৰা অনুবাদ কৰা হৈছে + Google দ্বাৰা ৰোমানাইজ কৰা হৈছে + %s দ্বাৰা অনুবাদ কৰা হৈছে + %s দ্বাৰা ৰোমানাইজ কৰা হৈছে + %s ৰ দ্বাৰা ৰোমানাইজ কৰা হৈছে + অনুবাদ কৰা হৈছে + অনুবাদৰ ভাষা + ৰোমানাইজেশ্যন ভাষা + অনুবাদ কৰা হৈছে… + গীতৰ কথাৰ অনুবাদ কৰিব নোৱাৰিল + ৰোমানাইজ কৰা হৈছে + স্বয়ংক্ৰিয় + ৰোমানাইজ কৰা হৈছে… + গীতিকাবলি ৰোমানাইজ কৰিব পৰা নগ\'ল + অনুবাদ কৰাখিনিহে + ৰোমানাইজড কেৱল + ডুৱেল মোড + ক্লিপবৰ্ডলৈ কপি কৰা হ\'ল + ৰিফ্ৰেচ কৰা হৈছে + ৰিফ্ৰেচ কৰা হ\'ল + QuickLRC API কী + QuickLRC-এ বৰ্তমানৰ ট্ৰেকটোৰ বাবে লিৰিক্সসমূহ লিপিবদ্ধ কৰে বা টাইম-এলাইন কৰে। ই ওপৰৰ OpenAI-সংগত ইণ্ডপইণ্টৰ সৈতে সম্পৰ্কহীন এক পৃথক সেৱা + QuickLRC কি সংৰক্ষণ কৰা হৈছে। লিৰিক্স চাৰ্চ ডায়ালগৰ এলাইন বুটামটো, অথবা ট্ৰেকটোত লিৰিক্স নাথাকিলে পেনেল প্ৰম্পটটো ব্যৱহাৰ কৰক + QuickLRC API কী সংৰক্ষিত হৈছে + QuickLRC-এ সেইটো কী গ্ৰহণ কৰা নাই + QuickLRC API কী মুছা হৈছে + QuickLRC-এ ক্ৰেডিটৰ খৰচত কথাৰ স্তৰ নথকা গানবোৰৰ বাবে সময় নিৰ্ধাৰণ কৰে, অথবা ট্ৰেকটোৰ পৰা সেইবোৰ লিপিবদ্ধ কৰে। এটা কি ছেট কৰিলে, কথাৰ স্তৰ নথকা ট্ৰেকবোৰত লিপিবদ্ধকৰণৰ সুবিধা উপলব্ধ হয় আৰু সন্ধান ডায়ালগত এটা alignment বুটাম যোগ হয় + QuickLRC alignment-ৰ বাবে অনুৰোধ কৰক + Align কৰি থকা হৈছে + Alignment প্ৰয়োগ কৰক + QuickLRC transcription-ৰ বাবে অনুৰোধ কৰক + Transcribe কৰি থকা হৈছে + Transcription বাতিল কৰা হৈছে + কেৱল YouTube ৰ পৰা প্লে কৰা গানবোৰহে ট্রান্সক্ৰাইব কৰিব পৰা যাব + সংগত কৰিবলৈ কোনো কথাৰ স্তৰ নাই - তাৰ পৰিৱৰ্তে লিপিবদ্ধ কৰা হৈছে + QuickLRC-এ এই ট্ৰেকটো প্ৰচেছ কৰিব নোৱাৰিলে + এই ট্ৰেকটোৰ বাবে QuickLRC ক্ৰেডিট শেষ হৈছে + "গীতৰ কথাখিনি শিৰোনাম আৰু শিল্পীৰ সৈতে মিলাৰ হয়, অস্বাভাবিক মেটাডাটাত একো নপাব পাৰে বা ভুল সংস্কৰণ পাব পাৰে। সংস্কৰণ সলনি কৰিবলৈ refresh ব্যৱহাৰ কৰক। * চিহ্নিত প্ৰদানকাৰীৰ বাবে এটা টোকেনৰ প্ৰয়োজন। সকলো সক্ৰিয় প্ৰদানকাৰীক সদায় আপোনাৰ ক্ৰম অনুসৰি অনুৰোধ কৰা নহয়, বৰঞ্চ তেওঁলোকৰ গতিৰ ওপৰত ভিত্তি কৰি সঁহাৰি দিয়া হয়।\n\nগীতৰ কথাখিনি তৃতীয় পক্ষৰ সেৱাৰ পৰা আহে যিবোৰ Morphe ৰ সৈতে সম্পৰ্কিত, স্পনচৰ কৰা বা অনুমোদিত নহয়। প্ৰতিটো সেৱাক ইয়াৰ বৰ্তমানৰ অৱস্থাতে ব্যৱহাৰ কৰা হয়: গীতৰ কথা, ইয়াৰ উপলব্ধতা আৰু ইয়াৰ লাইচেঞ্চিং সেই সেৱাসমূহ আৰু ইয়াৰ লাইচেনছদাতাসমূহৰ হাতত থাকে, আৰু সেইবোৰ ব্যৱহাৰ কৰিলে তেওঁলোকৰ সেৱাৰ চৰ্তৰ অধীনত হ'ব পাৰে। এটা সক্ৰিয় সেৱালৈ কেৱল আপুনি বিচৰা তথ্যখিনিহে পঠোৱা হয় - শিৰোনাম, শিল্পী, আৰু য'ত সেই সেৱাৰ প্ৰয়োজন হয়, তাত ভিডিঅ' আইডি। বিকল্প AI এনডপইণ্ট আৰু QuickLRC ট্ৰান্সক্ৰিপচন কেৱল আপুনি সেইবোৰ কনফিগার কৰাৰ পিছতহে চলে, আৰু গীতৰ কথা বা ট্ৰেকৰ ভিডিঅ' URL লাভ কৰে।" + আপুনি ব্যৱহাৰ কৰা প্ৰদানকাৰকহে সক্ৰিয় কৰক, অথবা ডিফল্ট ৰাখক - ধাৰাবাহিক অনুৰোধে সৰু সম্প্ৰদায়ৰ লৰিক্স సైটবোৰক অতিৰিক্ত বোজা দিব পাৰে + ক্ৰেডিট লাইন লুকুৱাওক + গীতৰ তলত দেখুৱা ক্ৰেডিট লাইনটো লুকুৱাওক + অনুবাদ আৰু ৰোমানাইজেশ্যন সলনি কৰক + ON কৰিলে ৰোমানাইজেশ্যন গীতৰ তলত আৰু অনুবাদ ওপৰত থাকে; OFF কৰিলে ৰোমানাইজেশ্যন ওপৰত আৰু অনুবাদ তলত থাকে + অনুবাদ + ৰোমানাইজেশ্যন + অনুবাদ প্ৰদানকাৰী + Google (ডিফল্ট, বিনামূলীয়া) + OpenAI সংগত (কষ্টম) + ৰোমানাইজেশান প্ৰদানকাৰী + Google (ডিফল্ট) + স্থানীয় / অফলাইন অভিধান + OpenAI সংগত (কষ্টম) + AI ছেকচন + কেৱল তেতিয়াই ব্যৱহাৰ কৰা হয় যেতিয়া প্ৰদানকাৰীয়ে Chorus বা Verse-ৰ দৰে কোনো ছেকচন লেবেল পঠিয়ায়, যাক আপোনাৰ OpenAI-সংগত মডেলটোৱে ধৰা পেলায় + AI & Cloud Endpoints + AI প্ৰদানকাৰীৰ বাবে API কিচ, মডেল, এন্ডপইণ্ট আৰু প্ৰম্ট টেম্পলেটসমূহ কনফিগাৰ কৰক। + API এন্ডপইণ্ট URL (যেনে https://api.openai.com/v1/chat/completions) + প্ৰমপ্ট টেমপ্লেট + প্লেচহোল্ডাৰসমূহ: {language}, {task}, {title}, {artist}, {count}, {lines} + প্ৰমপ্ট সংৰক্ষিত হৈছে + প্ৰম্পট সংৰক্ষণ কৰা হৈছে, কিন্তু AI ইণ্ডপইণ্টটো এতিয়াও ব্যৱহাৰযোগ্য নহয়। প্ৰথমে ইয়াক AI Settings-ৰ অধীনত কনফিগাৰ কৰক + প্ৰমপ্ট ৰিছেট কৰা হৈছে + সঠিক JSON নহয়। সংৰক্ষণ কৰাৰ আগতে ইয়াক ঠিক কৰক বা ফিল্ডটো খালী কৰক + অনুবাদ আৰু ৰোমানাইজেশান ছেটিংছ সংৰক্ষণ কৰা হৈছে + ছেটিংছবোৰ ডিফল্টত ৰিছেট কৰা হৈছে + Base URL টো http:// বা https:// ৰে আৰম্ভ হ\'ব লাগিব + সংযোগ পৰীক্ষা কৰক + এণ্ডপইণ্টসমূহ কনফিগাৰ কৰক + সকলো এণ্ডপইণ্ট দেখুৱাওক + সংযোগ বৈধতা পৰীক্ষা কৰা হৈছে… + অনুবাদ প্ৰদানকাৰী + ৰোমানাইজেশ্যন প্ৰদানকাৰী + মডেলসমূহ আহৰণ কৰক + মডেলসমূহ আহৰণ কৰা হৈছে… + এটা মডেল বাছনি কৰক + মডেলসমূহ আহৰণ কৰাত বিফল হৈছে। API কী বা সংযোগ পৰীক্ষা কৰক। + Gemini API ছেটিংছ + OpenAI সংগতিপূৰ্ণ (কষ্টম এণ্ডপইণ্ট) + DeepL API ছেটিংছ + API কী + মডেল + বেছ URL + API কি (ঐচ্ছিক) + অনুবাদ প্ৰম্পট + অতিৰিক্ত অনুৰোধ বডী (ঐচ্ছিক JSON) + প্ৰমাণীকৰণ কি (Free বা Pro) + API কি, মডেল, এন্ডপইণ্ট আৰু প্ৰম্পটসমূহ কনফিগাৰ কৰক + 8.23.51 - বোল্ড আইকনসমূহ নিষ্ক্ৰিয় কৰক + 9.13.51 - প্লেয়াৰৰ Up next, Lyrics, আৰু Related টেবসমূহ পুনৰুদ্ধাৰ কৰক diff --git a/patches/src/main/resources/addresources/values-as-rIN/reddit/strings.xml b/patches/src/main/resources/addresources/values-as-rIN/reddit/strings.xml index cf7776fdaf..0f0f355764 100644 --- a/patches/src/main/resources/addresources/values-as-rIN/reddit/strings.xml +++ b/patches/src/main/resources/addresources/values-as-rIN/reddit/strings.xml @@ -15,4 +15,123 @@ Second \"item\" text" --> - + + Reddit গোপনীয়তা নীতি + Google লগইন উপলব্ধ নহয়। লগইন কৰিবলৈ ইমেইল ব্যৱহাৰ কৰক + বিজ্ঞাপন + ফীডৰ বিজ্ঞাপনবোৰ লুকুৱাই ৰাখক + মন্তব্যৰ বিজ্ঞাপনবোৰ লুকুৱাই ৰাখক + নেভিগেচন বাৰ + Answers বুটনটো লুকুৱাই ৰাখক + Chat বুটনটো লুকুৱাই ৰাখক + Create বুটনটো লুকুৱাই ৰাখক + Discover বা Communities বুটনটো লুকুৱাই ৰাখক + Games বুটামটো লুকুৱাওক + চাইডবাৰ + সদ্য ভিজিট কৰা শ্বেলফটো লুকুৱাওক + Reddit-ত গেমছ শ্বেলফটো লুকুৱাওক + Reddit Pro শ্বেলফটো লুকুৱাওক + About শ্বেলফটো লুকুৱাওক + সম্পদ শ্বেলফটো লুকুৱাওক + লেআউট + পাঠ কপি কৰক + পোষ্ট আৰু মন্তব্যত পাঠ নিৰ্বাচন আৰু কপি কৰাৰ অনুমতি দিয়ে। মন্তব্য সংকুচিত কৰা সুবিধাটো নিষ্ক্ৰিয় কৰে + \'পাঠ কপি কৰক\' সক্ৰিয় হৈ থকাৰ সময়ত মন্তব্য সংকুচিত কৰা সুবিধাই কাম নকৰিব + কাষ্টম ফন্ট + তলত দিয়া পথৰ পৰা এটা কাষ্টম TTF বা OTF ফন্ট ফাইল লোড কৰক। ইয়াক সক্ৰিয় কৰিলে চিষ্টেম ফন্ট জোৰ কৰা বিকল্পটো বন্ধ হৈ যাব + কাষ্টম ফন্ট ফাইল + TTF বা OTF ফন্ট ফাইল বাছন কৰিবলৈ টিপ কৰক + নিৰ্বাচিত ফন্ট - %s + প্ৰথমে এটা কাষ্টম ফন্ট ফাইল বাছন কৰক + অনুগ্ৰহ কৰি এটা .ttf বা .otf ফন্ট ফাইল নিৰ্বাচন কৰক + কাষ্টম ফন্ট লোড কৰাত বিফল হৈছে। আন এটা ফাইল বাছন কৰক + আধুনিক হোম UI নিষ্ক্ৰিয় কৰক + স্ক্ৰীনশ্বট পপ-আপটো নিষ্ক্ৰিয় কৰক + স্ক্ৰীনশ্বট লওঁতে প্ৰদৰ্শিত হোৱা পপ-আপটো নিষ্ক্ৰিয় কৰে + চিষ্টেম ফন্ট জোৰ কৰক + Reddit Sans বা Roboto ৰ পৰিৱৰ্তে ডিভাইচৰ চিষ্টেম ফন্ট ব্যৱহাৰ কৰক। ইয়াক সক্ৰিয় কৰিলে কাষ্টম ফন্টটোও বন্ধ হৈ যাব + Ask বুটনটো লুকুৱাওক + চাৰ্চ বাৰত থকা Ask বুটনটো লুকুৱায় + কমিউনিটিজ শ্বেল্ফ লুকুৱাওক + ছাবৰেডিটবোৰত সম্পৰ্কিত বা পৰামৰ্শ দিয়া সম্প্ৰদায়ৰ শ্বেলটো লুকুৱায় + ট্ৰেণ্ডিং শ্বেল্ফসমূহ লুকুৱাওক + ফিড আৰু সন্ধান পৰামৰ্শৰ পৰা ট্ৰেণ্ডিং শ্বেল্ফসমূহ লুকুৱায় + নটিফিকেশ্যন পৰামৰ্শ ডায়েলগটো আঁতৰাওক + এটা subreddit ভিজিট কৰোঁতে নোটিফিকেচন পৰামৰ্শ ডায়ালগটো স্বয়ংক্ৰিয়ভাৱে আঁতৰায় + হোম ফিডত ফ্লেয়াৰসমূহ দেখুৱাওক + হোম ফিডত পোষ্ট ফ্লেয়াৰ বেজসমূহ দেখুৱায় + কিছুমান ফিড আইটেমত ফ্লেয়াৰ দেখা নাযাব পাৰে + পোষ্টত ভিউ কাউণ্ট দেখুৱাওক + সীমাবদ্ধতা: কেৱল API ৰেসপনচত ভিউ কাউণ্ট থাকিলেহে উপলব্ধ + পোষ্ট একচন বাৰটো সলনি কৰক + পোষ্টৰ ভোট আৰু মন্তব্যৰ বুটামবোৰ সোঁফালে আৰু ক্ৰচপোষ্ট আৰু শেয়াৰ বুটামবোৰ বাওঁফালে স্থানান্তৰ কৰে + ভোট আৰু মন্তব্যৰ বুটামবোৰৰ স্থান সলনি কৰক + ভোটৰ বুটামবোৰৰ আগতে পোষ্টৰ মন্তব্যৰ বুটামটো দেখুৱায় + NSFW সতৰ্কবাণী ডায়েলগটো আঁতৰাওক + এটা subreddit ভিজিট কৰোঁতে NSFW সমলৰ সতৰ্কবাণী স্বয়ংক্ৰিয়ভাৱে গ্ৰহণ কৰে + বিবিধ + жөнүндө + পাচ সংস্কৰণ আৰু অফিচিয়েল লিংকসমূহ + শেয়াৰ কৰা লিংকসমূহ স্যানিটাইজ কৰক + শেয়াৰ কৰা লিংকৰ পৰা ট্ৰেকিং কুৱেৰী পাৰামিটাৰসমূহ আঁতৰায় + লিংকসমূহ পোনপটীয়াকৈ খোলক + বাহ্যিক লিংকসমূহ খোলাৰ সময়ত ৰিডাইৰেক্ট URL বোৰ বাইপাস কৰে + পূৰ্ণ প্ৰস্থৰ ফিড মিডিয়া + ফিড আৰু পোষ্টৰ সবিশেষত ছবি আৰু ভিডিঅ\'সমূহ কাষৰ পেডিং আৰু গোলাকৃতি কোণ অবিহনে কাৰুকৰ পৰা কাৰুকৈ দেখুৱায় + মিডিয়া ভিউয়াৰ ফেড শক্তি + ছবি আৰু ভিডিঅ\' ভিউ কৰোঁতে কেপচনৰ পিছৰ ডাৰ্ক ফেডৰ শক্তি, 0 (কোনো ফেড নাই) ৰ পৰা 1 (Reddit ৰ ডিফল্ট) লৈ + ফিডৰ স্থান বজাই ৰাখক + এটা পোষ্ট, ছবি বা ভিডিঅ\' বন্ধ কৰাৰ পিছত, খোলা পোষ্টটো ওপৰলৈ লৈ যোৱাৰ পৰিৱৰ্তে ফিডটো একেই স্থানত ৰাখে + ফিড মিডিয়াৰ সৰ্বাধিক উচ্চতা + ফিডত ছবি, ভিডিঅ\' আৰু গেলাৰীৰ সৰ্বাধিক উচ্চতা, ইয়াৰ প্ৰস্থৰ গুণক হিচাপে। অধিক ওখ মিডিয়া কাটি দিয়া হয়। ১.৩৩ হৈছে Reddit-ৰ ডিফল্ট + অডিঅৰ সৈতে Redgifs প্লে কৰক + ৰেডিটৰ কপিৰ পৰিৱৰ্তে মূল Redgifs ভিডিঅ’টো প্লে কৰে, যাৰ প্ৰায়ে কোনো অডিঅ নাথাকে + Redgifs HD কোৱালিটি + HD Redgifs ভিডিঅ’টো প্লে কৰে। HD ভিডিঅ’বোৰ ডাঙৰ হয় আৰু লোড হ’বলৈ কিছু সময়ত ৰৈ যাব পাৰে + ডাউনলোড স্থান + সকলো একাউণ্ট + Reddit-ৰ ডিফল্ট: ছবিবোৰ Pictures/Reddit-ত, ভিডিঅ’বোৰ Download-ত + ফ’ল্ডাৰ: %s + ফাইলৰ নাম: %s + Reddit-ৰ ফাইলৰ নামসমূহ + \"সকলো একাউণ্ট\" ব্যৱহাৰ কৰে + নিজৰ স্থান ব্যৱহাৰ কৰক + ত্ৰেত সংৰক্ষণ কৰক + Reddit-ৰ ডিফল্ট ফ’ল্ডাৰসমূহ + ডিফল্ট + উপ-ফ’ল্ডাৰ + ফাইলৰ নামত / ব্যৱহাৰ কৰি উপ-ফ’ল্ডাৰ যোগ কৰক + ব্ৰাউজ কৰক… + এই ডিভাইচত কোনো ফ’ল্ডাৰ পিকাৰ উপলব্ধ নহয় + ফাইলৰ নাম + Reddit-ৰ ফাইলৰ নাম + টোকেনটো ইনচাৰ্ট কৰিবলৈ তাৰ ওপৰত টেপ কৰক। এটা / এ উপ-ফ’ল্ডাৰ সৃষ্টি কৰে। অজ্ঞাত মানসমূহ \"unknown\" হৈ পৰে + ছবিসমূহ: %s + ভিডিঅ’সমূহ: %s + সংৰক্ষণ কৰক + ৰীছেট কৰক + মচি পেলাওক + সকলো একাউণ্টৰ ডাউনলোড স্থান Reddit-ৰ ডিফল্টত ৰিছেট কৰিব নেকি? + %s ৰ ডাউনলোড স্থানটো মচি পেলাব নেকি? + ডাউনলোড স্থানত সংৰক্ষণ কৰিব পৰা নগ’ল। তাৰ পৰিৱৰ্তে Reddit-ৰ ডিফল্ট ফোল্ডাৰত সংৰক্ষণ কৰা হৈছে + ডাউনলোড ফোল্ডাৰটো আৰু উপলব্ধ নহয়। তাৰ পৰিৱৰ্তে Reddit-ৰ ডিফল্ট ফোল্ডাৰত সংৰক্ষণ কৰা হৈছে + ডাউনলোড কৰা ভিডিঅ’টো সংৰক্ষণ কৰিব পৰা নগ’ল + এই ফোল্ডাৰটো ডাউনলোডৰ বাবে ব্যৱহাৰ কৰিব নোৱাৰি + %s (বৰ্তমান) + %s (লগ-ইন কৰা নাই) + অনামিক (লগ আউট কৰা বা ইনকগনিটো) + মিডিয়া ভিউয়াৰ অভাৰলেটি লুকাই ৰাখক + এটা ছবি বা ভিডিঅ\' খোলাৰ সময়ত শিৰোনাম, বুটাম আৰু ভিডিঅ\' নিয়ন্ত্ৰকবোৰ লুকাই থাকে। সেইবোৰ দেখুৱাবলৈ মিডিয়াত টেপ কৰক + \"কথোপকথনটো চাওক\" বুটামটো লুকাই ৰাখক + মিডিয়া ভিউয়াৰৰ তলত থকা \"কথোপকথনটো চাওক\" বুটামটো লুকাই ৰাখে + মিডিয়া ভিউয়াৰৰ ক’লা পটভূমি + থিমৰ পটভূমিৰ ৰঙৰ পৰিৱৰ্তে মিডিয়া ভিউয়াৰৰ ছবি আৰু ভিডিঅ\'ৰ পিছত ক’লা পটভূমি ব্যৱহাৰ কৰে + লিংকসমূহ বাহ্যিকভাৱে খোলক + ইন-এপ ব্ৰাউজাৰৰ পৰিৱৰ্তে আপোনাৰ ব্ৰাউজাৰত লিংকসমূহ খোলে + সংকুচিত কমেন্টবোৰ মনত ৰাখক + আপুনি যিবোৰ মন্তব্য সংকুচিত কৰে, সেইবোৰ পোষ্টটো পুনৰ খোলাৰ পিছতো সংকুচিত হৈ থাকে + এপ আইকন + এপ আইকন বাছনি কৰক + ডিফল্ট + অজান + diff --git a/patches/src/main/resources/addresources/values-as-rIN/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-as-rIN/shared-youtube/strings.xml index fd007eeb78..f44d966cb0 100644 --- a/patches/src/main/resources/addresources/values-as-rIN/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-as-rIN/shared-youtube/strings.xml @@ -18,46 +18,295 @@ Second \"item\" text" --> + ষ্টাৰ্টআপ এনিমেশ্বন + নিষ্ক্ৰীয় + ৰং + ছেটিং আপ শেষ কৰিবলৈ পুনৰ আৰম্ভ কৰক + এপৰ নাম + ডাৰ্ক থিম বেকগ্ৰাউণ্ড + বিশুদ্ধ ক\'লা + Material You (বিশুদ্ধ ক\'লা) + ক্লাচিক YouTube + গাঢ় গোলাপি + গাঢ় নীলা + গাঢ় সেউজীয়া + গাঢ় হালধীয়া + গাঢ় কমলা + গাঢ় ৰঙা + কষ্টম গাঢ় পটভূমি + গাঢ় থিমৰ পটভূমিৰ ৰং নিৰ্ধাৰণ কৰক + এপৰ ডিফল্ট + Material You (নিৰপেক্ষ) + Material You (প্ৰাথমিক) + Material You (গৌণ) + Material You (তৃতীয়া) + কষ্টম + ডাউনলোডসমূহ + ডাউনলোড মোড + ডিফল্ট + ইন-এপ ডাউনলোড + বাহ্যিক ডাউনলোডকাৰ + ডাউনলোডাৰৰ পেকেজ নাম + আপোনাৰ সংস্থাপন কৰা এক্সটেৰ্ণাল ডাউনলোডাৰ এপৰ পেকেজ নাম + পেকেজৰ নাম লিখক + অন্যান্য + এপ্লিকেচনটো সংস্থাপন কৰা হোৱা নাই + %s সংস্থাপন কৰা হোৱা নাই। অনুগ্ৰহ কৰি ইয়াক সংস্থাপন কৰক + "পেকেজ নামৰ সৈতে সংস্থাপন কৰা এপ্লিকেচনটো বিচাৰি পোৱা নগ’ল: %s + +পেকেজ নামটো সঠিক আৰু এপ্লিকেচনটো সংস্থাপন কৰা হৈছে নে নাই পৰীক্ষা কৰক" + পেকেজৰ নাম খালী হ\'ব নোৱাৰে + কষ্টম + এপ আইকন + মুল + Morphe ক\'লা + Morphe গাঢ় + Morphe পাতল + Morphe play light + Morphe play black + কষ্টম + "ইয়াৰ দ্বাৰা আপোনাৰ এপ লঞ্চাৰত দেখুওৱা আইকনটো সলনি হয়। + +এই ছেটিংটোৱে তলৰ কোনো ঠাইত আইকনটো সলনি নকৰিব: +• ডিভাইচ চিষ্টেম ছেটিংছ এপ তালিকা +• কিছুমান পুছ নটিফিকেচন +• 'Open with' এপ পিকাৰ +• Android Auto আইকন + +সকলো ঠাইতে আইকনটো সলনি কৰিবলৈ, পুনৰ পেচ কৰক আৰু 'App icon' পেচ বিকল্পটো সলনি কৰক" + নটিফিকেচন আইকন + এপ আইকনৰ সৈতে মিল কৰক + Morphe play + Morphe জাননী + আপোনাৰ ঘৰুৱা ইতিহাস সংৰক্ষণ কৰা হোৱা নাই।<br><br>এইটো সম্ভৱতঃ DNS বিজ্ঞাপনৰোধক বা নেটৱৰ্ক প্ৰক্সিৰ কাৰণে হৈছে।<br><br>এইটো সমাধান কৰিবলৈ, <b>s.youtube.com</b> হোয়াইটলিষ্ট কৰক বা সকলো DNS ব্লকাৰ আৰু প্ৰক্সি বন্ধ কৰক + পুনৰ দেখুৱাব নালাগে + ভিডিঅ\' ষ্ট্ৰীমস অনুকৰণ কৰক + প্লেবেক সমস্যা প্ৰতিৰোধ কৰিবলৈ ক্লায়েন্ট স্পুফিং কনফিগার কৰক + "এটা সংগতিপূৰ্ণ ভিডিঅ' ষ্ট্ৰিমৰ বাবে অনুৰোধ কৰি অধিকাংশ ব্যৱহাৰকাৰীৰ বাবে প্লেবেকৰ সমস্যাবোৰ সমাধান কৰে + +যদি আপুনি এজন YouTube Premium ব্যৱহাৰকাৰী হয়, তেন্তে এই ছেটিংটোৰ প্ৰয়োজন নহ'ব পাৰে" + কোনো ক্লায়েন্ট ষ্ট্ৰিম লাভ কৰিব পৰা নগ\'ল + ডিফল্ট ক্লায়েন্টটোৱে কাম কৰা নাই + "%1$s-এ ক্ৰমান্বয়ে কেইবাবাৰো কাম নকৰিলে, কিন্তু %2$s-এ কৰিলে। + +%2$s-ক ডিফল্ট ক্লায়েন্ট হিচাপে নিৰ্ধাৰণ কৰিবনে?" + সুইচ কৰক + এই ছেটিংটো বন্ধ কৰিলে প্লেবেকে সমস্যা হ\'ব পাৰে + এনেদৰে চেষ্টা কৰক: Spoof video streams > Android VR sign in + ডিফল্ট ক্লায়েন্ট + Android VR ছাইন ইন + Android VR-ত ছাইন ইন কৰা হোৱা নাই। ছাইন ইন কৰিবলৈ ইয়াত টেপ কৰক + বৰ্তমান Android VR-ত ছাইন ইন কৰা হৈছে + এই বৈশিষ্ট্যটো ব্যৱহাৰ কৰিবলৈ, \'ডিফাল্ট ক্লায়েণ্ট\'ক Android VR-লৈ সলনি কৰক + Android VR-ত ছাইন ইন কৰক + "Android VR ত ছাইন ইন কৰিলে কিছুমান ব্যৱহাৰকাৰীৰ বাবে প্লেবেকৰ সমস্যা সমাধান হ'ব পাৰে + +ছাইন ইন কৰিবলৈ, তলত কন্টিনিউত ক্লিক কৰক" + ছাইন আউট + নিৰ্দেশনাবলি + "সক্ৰিয়কৰণ ক’ড: %s + +১. 'Open website' তলত ক্লিক কৰি 'Connect a device' ৱেবছাইটটো খোলক +২. ওপৰত দেখুওৱা সক্ৰিয়কৰণ ক’ডটো ক্লিপবৰ্ডৰ পৰা পেষ্ট কৰক (বা টাইপ কৰক) +৩. প্ৰয়োজন হ’লে Google-ত ছাইন ইন কৰক। যদি আপোনাৰ একাধিক একাউণ্ট আছে তেন্তে এই YouTube একাউণ্টটো বাছনি কৰক +৪. 'Allow YouTube VR' এক্সেচত ক্লিক কৰক +৫. 'Device connected' বাৰ্তাটো আহাৰ পিছত এই এপটোত উভতি আহক" + সক্ৰিয়কৰণ ক’ড প্ৰদান কৰাত ব্যৰ্থ হৈছে + প্ৰমাণীকৰণ টোকেন প্ৰদান কৰাত ব্যৰ্থ হৈছে + সফলভাৱে ছাইন ইন কৰা হৈছে + "প্ৰমাণীকৰণ সংৰক্ষিত হৈছে + +• আপোনাৰ Morphe ছেটিংসবোৰে Android VR প্ৰমাণীকৰণ আমদানি/ৰপ্তানি নকৰিব +• যদি এই এপটো ৬ মাহৰ বাবে ব্যৱহাৰ নকৰা হয় তেন্তে Google-এ এই প্ৰমাণীকৰণটো আঁতৰাই দিব +• যদি আপুনি আপোনাৰ Google পাছৱৰ্ড সলনি কৰে বা আপোনাৰ Google ছেটিংসৰ পৰা এই ডিভাইচটো ডি-ৰেজিষ্টৰ কৰে তেন্তে আপুনি পুনৰ ছাইন ইন কৰিব লাগিব" + OAuth2 সাময়িকভাৱে উপলব্ধ নহয় (API টাইম আউট হৈছে) + Android VR ছাইন ইনৰ মেয়াদ শেষ হৈছে, অনুগ্ৰহ কৰি পুনৰ VR-ত ছাইন ইন কৰক + OAuth2 সাময়িকভাৱে উপলব্ধ নহয় (স্থিতি %s) + OAuth2 সাময়িকভাৱে উপলব্ধ নহয় + VR-ত ছাইন ইন কৰিব পৰা নাই, ৱেবছাইট প্ৰমাণীকৰণ অনুমোদিত হোৱা নাই + VR-ত ছাইন ইন কৰিব পৰা নাই: %s + Android VR ৰ পৰা লগ আউট কৰা হৈছে + অবিধ VR ছাইন ইন টোকেন, অনুগ্ৰহ কৰি পুনৰ VR-ত ছাইন ইন কৰক + নটিফিকেচন মিডিয়া নিয়ন্ত্ৰক + আপোনাৰ লক স্ক্ৰীন আৰু নটিফিকেচন পেনেলত দেখুওৱা প্লেবেক নিয়ন্ত্ৰকসমূহ কাস্টমাইজ কৰক + পূৰ্বৰ & পৰৱৰ্তী বুটামসমূহ লুকুওক + মিডিয়া নটিফিকেচন, লক স্ক্ৰীন আৰু হেডফোন নিয়ন্ত্ৰকত পূৰ্বৰ আৰু পৰৱৰ্তী বুটামসমূহ লুকুৱাই ৰাখে + চীকবাৰটো নিষ্ক্ৰিয় কৰক + লক স্ক্ৰীন আৰু নটিফিকেচন পেনেলত চীকবাৰটো টানি নিয়াৰ পৰা বাধা দিয়ে + Previous আৰু Next বুটামৰ সৈতে Seek কৰক + ভিডিঅ\' সলনি কৰাৰ পৰিৱৰ্তে seek কৰিবলৈ মিডিয়া নোটিফিকেচন, লক স্ক্ৰীণ আৰু হেডফোনৰ কণ্ট্ৰলসমূহ সলনি কৰে + Rewind দৈর্ঘ্য + Previous বুটামৰ সৈতে rewind কৰিবলগীয়া ছেকেণ্ড + Fast forward দৈর্ঘ্য + Next বুটামৰ সৈতে fast-forward কৰিবলগীয়া ছেকেণ্ড + Seek length ১ আৰু %s ছেকেণ্ডৰ ভিতৰত হ\'ব লাগে। ডিফল্ট ব্যৱহাৰ কৰা হৈছে + QUIC প্ৰটোকল নিষ্ক্ৰীয় কৰক + DRC অডিঅ\' নিষ্ক্ৰীয় কৰক + মূল অডিঅ\' ভাষা প্ৰয়োগ কৰক + ডাব কৰা সংস্কৰণৰ পৰিৱৰ্তে সদায় ভিডিঅ\'ৰ মূল অডিঅ\' ট্ৰেকটো প্লে কৰে + নিৰৱতা ছেকটো বাদ দিয়ক + ভিডিঅ’ অডিঅ’ত থকা নিৰৱ স্তব্ধতাবোৰ স্বয়ংক্ৰিয়ভাৱে বাদ দিয়ে + নূন্যতম বিৰতিৰ দৈৰ্ঘ্য + কেৱল এইমান সময়ৰ বাবে হোৱা বিৰতিবোৰহে চুটি কৰা হয়। পৰৱৰ্তী ভিডিঅ\'টো আৰম্ভ হোৱাৰ পিছত ই কাৰ্যকৰী হ\'ব + নেটৱৰ্ক প্ৰক্সি + প্ৰক্সিৰ জৰিয়তে সমৰ্থিত নেটৱৰ্ক অনুরোধসমূহ পৰিচালনা কৰক + প্ৰক্সি সক্ৰিয় কৰক + কনফিগাৰ কৰা HTTP বা HTTPS প্ৰক্সিৰ জৰিয়তে সমৰ্থিত নেটৱৰ্ক অনুরোধসমূহ পৰিচালনা কৰে + প্ৰক্সি হোষ্ট + প্ৰক্সি চাৰ্ভাৰৰ হোষ্টনেম বা IP ঠিকনা + প্ৰক্সি পৰ্ট + প্ৰক্সি চাৰ্ভাৰৰ পৰ্ট + HTTPS প্ৰক্সি ব্যৱহাৰ কৰক + HTTP-ৰ পৰিৱৰ্তে প্ৰক্সি চাৰ্ভাৰৰ সৈতে সংযোগ কৰিবলৈ HTTPS ব্যৱহাৰ কৰক + প্ৰক্সি প্ৰমাণীকৰণ + প্ৰক্সি অনুৰোধৰ সৈতে Basic authentication পঠিয়ায় + প্ৰক্সিৰ ইউজাৰনেম + বেছিক প্ৰক্সি প্ৰমাণীকৰণৰ বাবে ইউজাৰনেম + প্ৰক্সিৰ পাছৱৰ্ড + বেছিক প্ৰক্সি প্ৰমাণীকৰণৰ বাবে পাছৱৰ্ড + প্ৰত্যক্ষ ফ\'লবেক অনুমতি দিয়ক + প্ৰক্সি বিফল হ\'লে প্ৰত্যক্ষভাৱে সংযোগ কৰক + ফুলস্ক্ৰীন বিজ্ঞাপনসমূহ লুকুৱাওক + এই বৈশিষ্ট্যটো ব্যৱহাৰ কৰিবলৈ, \'Spoof video streams\'ক \'Android Studio\' আৰু \'Android VR Downgraded\'ৰ বাহিৰে যিকোনো ক্লায়েণ্টলৈ সলনি কৰক + শ্বেয়াৰিং লিংকসমূহ পৰিষ্কাৰ কৰক + শেয়াৰ কৰা লিংকসমূহৰ পৰা ট্ৰেকিং কোৱেৰী পাৰামিটাৰসমূহ আঁতৰ কৰে + শেয়াৰ লিংকবোৰ youtube.com লৈ সলনি কৰক + শেয়াৰ কৰা লিংকবোৰত music.youtube.com ৰ ঠাইত youtube.com প্ৰতিস্থাপন কৰে + শেয়াৰ লিংকসমূহ youtu.be লৈ সলনি কৰক + ভিডিঅ\'সমূহক চুটি youtu.be URL ফৰ্মেট ব্যৱহাৰ কৰিবলৈ বাধ্য কৰে + ডিবাগ লগ প্ৰটোকল বাফৰ + ডেভেলাপাৰসকলক এপৰ উপাদানসমূহ চিনাক্ত কৰিবলৈ অতিৰিক্ত লগিং তথ্য যোগ কৰে + "এই ছেটিংটো সক্ৰিয় কৰিলে কিছুমান UI উপাদানৰ বাবে স্ক্ৰীনৰ টেক্সটৰ সৈতে অতিৰিক্ত লেআউট ডাটা লগ কৰিব। + +কাষ্টম ফিল্টাৰ সৃষ্টি কৰোতে এইটোৱে উপাদানসমূহ চিনাক্ত কৰাত সহায় কৰিব পাৰে। + +তদুপৰি, এইটো সক্ৰিয় কৰিলে আপোনাৰ IP ঠিকনাৰ দৰে কিছুমান ব্যৱহাৰকাৰী ডাটাও লগ কৰিব" + কষ্টম ফিল্টাৰ + যিকোনো উপাদান লুকুৱাবলৈ কাস্টম ফিল্টাৰ কনফিগাৰ কৰক + কষ্টম ফিল্টাৰ সক্ৰিয় কৰক + ফিল্টাৰ কৰিবলগীয়া কম্পোনেন্ট পাথ বিল্ডাৰ ষ্ট্ৰিংৰ তালিকা, যিবোৰ নতুন লাইনৰ দ্বাৰা পৃথক কৰা হৈছে + অবৈধ কাস্টম ফিল্টাৰ: %s + ছেটিংছ মেনু ফিল্টাৰ + দৃশ্যমান নামৰ দ্বাৰা মানক এপ ছেটিংছ স্ক্ৰীণত আইটেমবোৰ লুকুৱাওক + তালিকাক পূৰণ কৰিবলৈ এবাৰ মানক এপ ছেটিংছ স্ক্ৰীণখন খোলক + ভিউয়াৰ ডিসক্ৰেচেন ডায়ালগ আঁতৰ কৰক + ই বয়সৰ বাধা অতিক্ৰম নকৰে। ই কেৱল ইয়াক স্বয়ংক্ৰিয়ভাৱে গ্ৰহণ কৰে + PoToken প্ৰোভাইডৰ + প্লেবেকে সমস্যা প্ৰতিৰোধ কৰিবলৈ PoToken প্ৰোভাইডৰ কনফিগাৰ কৰক + বাহ্যিক PoToken প্ৰোভাইডাৰ + "বাহ্যিক PoToken minter ব্যৱহাৰ কৰি প্লেবেকেৰ সমস্যাবোৰ সমাধান কৰে (প্ৰায়োগিক) + +যদি আপুনি এজন YouTube Premium ব্যৱহাৰকাৰী হয়, তেন্তে এই ছেটিংটোৰ প্ৰয়োজন নহ'ব পাৰে" + "এটা বহিৰ্ভূত PoToken minter ব্যৱহাৰ কৰি প্লেবেকেৰ সমস্যাবোৰ সমাধান কৰে (পৰীক্ষামূলক) + +এই বৈশিষ্ট্যটো ব্যৱহাৰ কৰিবলৈ, 'Spoof video streams' বন্ধ কৰক" + যদি ই কাম নকৰে, তেন্তে MicroG ফৰ্চ-ক্লজ কৰি চেষ্টা কৰক + PotHelper ছেটিংছ খোলক + "এই বৈশিষ্ট্যটো বৰ্তমান উন্নয়নৰ অধীনত আছে আৰু ই কাম নকৰিব পাৰে + +PotHelper ডাউনলোড কৰিবলৈ ইয়াত টিপক" + PotHelper এ এতিয়া আৰু কাম নকৰে + "PotHelper ৰ এই সংস্কৰণত বিল্ট-ইন থকা টকেনটোৱে এতিয়া আৰু কাম নকৰে + +প্লেবেকে ঠিক কৰিবলৈ 'Spoof video streams' অন কৰক" + অন কৰক + এপৰ সংস্কৰণৰ নকল কৰক + পুৰণি UI বৈশিষ্ট্যসমূহ পুনৰুদ্ধাৰ কৰিবলৈ আপুনি এটা পুৰণি এপ সংস্কৰণ ব্যৱহাৰ কৰিছে বুলি চাৰ্ভাৰক ঠগাত হয় + "এপ্লিকেচনৰ সংস্কৰণটো এটা পুৰণি সংস্কৰণৰ দৰে নকল কৰা হ'ব + +ইয়াৰ ফলত এপ্লিকেচনটোৰ ৰূপ আৰু বৈশিষ্ট্যসমূহ সলনি হ'ব, কিন্তু কিছু অজ্ঞাত কাৰ্যকাৰিতাৰ সমস্যা হ'ব পাৰে + +যদি পিছত ইয়াৰ ব্যৱহাৰ বন্ধ কৰা হয়, তেন্তে UI বুগ প্ৰতিৰোধ কৰিবলৈ এপ্লিকেচনৰ ডাটা মচি পেলোৱাৰ পৰামৰ্শ দিয়া হৈছে" + এপ সংস্কৰণৰ নকলৰ লক্ষ্য + এপ ৰিফ্ৰেছ ৰেট + ডিফল্ট + %s FPS + ৰিফ্ৰেছ ৰেট অভাৰৰাইড কৰক + সদায় + প্লেবেকেৰ পৰ্ট্ৰেইট + প্লেব্যাক ফুলস্ক্ৰীন + প্লেব্যাক প’ট্ৰেইট আৰু ফুলস্ক্ৰীন + ফণ্ট + ডিফল্ট + ডিভাইচ চিষ্টেম ফণ্ট + কাষ্টম ফণ্ট ফাইল + কাষ্টম ফণ্ট ফাইল + এটা TTF বা OTF ফণ্ট ফাইল বাছনি কৰক + পছন্দ কৰা ফাইলটো TTF বা OTF ফন্ট নহয় + কাষ্টম ফন্টটো লোড কৰিব পৰা নগ’ল আৰু ই বন্ধ হৈ আছে + ডিসলাইকসমূহ সাময়িকভাৱে উপলব্ধ নহয় (API টাইম আউট হৈছে) + ডিসলাইক উপলব্ধ নহয় (স্থিতি %s) + অনিষ্টকৰণবোৰ উপলব্ধ নহয় (client API limit) + অনিষ্টকৰণবোৰ উপলব্ধ নহয় (%s) + মালিকৰ দ্বাৰা লুকাই ৰখা হৈছে + এই ভিডিঅ\'টো অপছন্দ কৰক, আন %s জন লোকে কৰাৰ লগতে + এই ভিডিঅ\'টো অপছন্দ কৰক, ভোটৰ %s শতাংশ অপছন্দ + Return YouTube Dislike সক্ৰিয় কৰক + অনিষ্টকৰণবোৰ শতাংশ হিচাপে দেখুওক + সংখ্যাৰ পৰিৱৰ্তে ইষ্টকৰণবোৰক পছন্দৰ শতাংশ হিচাপে দেখুৱায় + অনুমানিত পছন্দবোৰ দেখুওক + যিবোৰ ভিডিঅ\'ত সৃষ্টিকৰ্তাই পছন্দবোৰ নিষ্ক্ৰিয় কৰিছে তাত এটা অনুমানিত পছন্দৰ সংখ্যা দেখুৱায় + সংযোগৰ ত্ৰুটিৰ সতৰ্কবাৰ্তাসমূহ দেখুৱাওক + Return YouTube Dislike চাৰ্ভাৰটো উপলব্ধ নহ\'লে এটা জাননী দেখুৱায় + "Return YouTube Dislike-এ ২০২১ চনত YouTube-এ লুকাই থোৱা dislike গণনা স্তৰবোৰ পুনৰুদ্ধাৰ কৰে। এই গণনাৰ তথ্যবোৰ আঁতৰ কৰাৰ আগতে সংৰক্ষিত কৰা ডাটাৰ পৰা লোৱা হৈছে আৰু নতুন ভিডিঅ'বোৰৰ বাবে ইয়াৰ ব্যৱহাৰকাৰীৰ ভোটৰ পৰা অনুমান কৰা হৈছে, সেয়েহে এইবোৰ আনুমানিক আৰু প্ৰতি কেইদিনো এবাৰ আপডেট হয় + +আপুনি চোৱা ভিডিঅ'টোৰ ID Return YouTube Dislike API-লৈ পঠোৱা হয়, আৰু অনুমানসমূহ উন্নত কৰিবলৈ আপোনাৰ likes আৰু dislikes নামহীনভাৱে পঠোৱা হয়" + এই ডিভাইচৰ Return YouTube Dislike API পৰিসংখ্যা + API সঁহাৰি সময়, গড় + API সঁহাৰি সময়, নূন্যতম + API সঁহাৰি সময়, সৰ্বোচ্চ + API ৰেষ্টপনচ টাইম, শেষ মিডিয়া + ডিসলাইকসমূহ সাময়িকভাৱে উপলব্ধ নহয় - ক্লায়েণ্ট API ৰেট লিমিট কাৰ্যকৰী হৈছে + API ফেচ ভোটচ, কলৰ সংখ্যা + কোনো নেটৱৰ্ক কল কৰা হোৱা নাই + %s টা নেটৱৰ্ক কল কৰা হৈছে + API ফেচ ভোটচ, টাইমআউটৰ সংখ্যা + কোনো নেটৱৰ্ক কল টাইমআউট হোৱা নাই + %s টা নেটৱৰ্ক কল টাইম আউট হৈছে + API ক্লায়েণ্ট ৰেট লিমিট + কোনো ক্লায়েণ্ট ৰেট লিমিট পোৱা নগ\'ল + ক্লায়েন্ট ৰেট লিমিট %s বাৰত سامنا কৰা হৈছে + %s মিলিছেকেণ্ড diff --git a/patches/src/main/resources/addresources/values-as-rIN/shared/strings.xml b/patches/src/main/resources/addresources/values-as-rIN/shared/strings.xml index 31cdbe4dcb..4900d2ae24 100644 --- a/patches/src/main/resources/addresources/values-as-rIN/shared/strings.xml +++ b/patches/src/main/resources/addresources/values-as-rIN/shared/strings.xml @@ -17,21 +17,219 @@ Second \"item\" text" --> + ছেটিংছ + আপুনি নিশ্চিতনে যে আপুনি আগবাঢ়িব বিচাৰে? + সংৰক্ষণ কৰক + ৰিছেট + ৰং ৰিছেট কৰক + অ বৈধ ৰং + পুনৰ আৰম্ভ কৰা প্ৰয়োজনীয় + এই পৰিৱৰ্তনটো কাৰ্যকৰী কৰিবলৈ এপটো পুনৰ আৰম্ভ কৰক + পুনৰ আৰম্ভ কৰক + আমদানি কৰক + কপি কৰক + Morphe ছেটিংছসমূহ ডিফল্টত ৰিছেট কৰা হৈছে + %s ছেটিংছ ইম্পৰ্ট কৰা হৈছে + আমদানি বিফল হৈছে: %s + ওপৰলৈ নেভিগেট কৰক + ছেটিংছ সন্ধান কৰক + ৰবাৰত + \'%s\' ৰ বাবে কোনো ফলাফল পোৱা নাযিল + অন্য এটা কীৱৰ্ড চেষ্টা কৰক + শেহতীয়া সন্ধানসমূহ + সন্ধানৰ ইতিহাসৰ পৰা আঁতৰ কৰিব নেকি? + অনুসন্ধান ইতিহাসৰ পৰা আঁতৰ কৰক + সন্ধানৰ ইতিহাস পৰিষ্কাৰ কৰক + আপুনি নিশ্চিতনে যে আপুনি সকলো সন্ধানৰ ইতিহাস পৰিষ্কাৰ কৰিব বিচাৰে? + সন্ধানৰ পৰামৰ্শ + "• তাত যাবলৈ এটা পথত টেপ কৰক +• তাত যাবলৈ এটা ছেটিংছত লং-প্ৰেছ কৰক +• সন্ধানৰ অনুসন্ধানটো ইতিহাসত সংৰক্ষণ কৰিবলৈ Enter চাপক +• সন্ধান কৰোঁতে কেছিং আৰু বিৰাম চিহ্নক আওকাণ কৰা হয় +• নিষ্ক্ৰীয় চাইল্ড ছেটিংছৰ ওপৰত পেৰেন্ট ছেটিংছ দেখা যায়" + সন্ধানৰ ইতিহাস খালী আছে + সন্ধানৰ ইতিহাস সংৰক্ষণ কৰিবলৈ, এটা সন্ধান কুৱেৰী টাইপ কৰক আৰু Enter চাপক + ছেটিংছৰ সন্ধান ইতিহাস দেখুৱাওক + Morphe ছেটিংছৰ নাম + ডিফল্ট + অতিৰিক্ত + টুইকছ + মৰ্ফিং সময় + কষ্টম + এটা নাম লিখক + নাম খালী হ\'ব নোৱাৰে + Morphe ছেটিংছ আইকনবোৰ দেখুৱাওক + কপি কৰা হৈছে: %s + Morphe ভাষা + "কিছু ভাষাৰ অনুবাদ অনুপস্থিত বা অপূৰ্ণ হ'ব পাৰে। + +নতুন ভাষা অনুবাদ কৰিবলৈ বা বিদ্যমান অনুবাদসমূহ উন্নত কৰিবলৈ, morphe.software/translate ভ্ৰমণ কৰক" + এপ্পৰ ভাষা + আমদানি / ৰপ্তানি + Morphe ছেটিংছ আমদানি / ৰপ্তানি কৰক + ফাইলৰ পৰা ইমপৰ্ট কৰক + ছেটিংছবোৰ সফলতাৰে ইমপৰ্ট কৰা হৈছে, প্ৰয়োগ কৰিবলৈ Save ত টেপ কৰক + ছেটিংছ ইমপৰ্ট কৰাত ব্যৰ্থ হৈছে + ফাইললৈ এক্সপৰ্ট কৰক + ছেটিংছবোৰ সফলতাৰে এক্সপৰ্ট কৰা হৈছে + ছেটিংছ এক্সপৰ্ট কৰাত ব্যৰ্থ হৈছে + আপুনি Morphe Patches ৰ শেৰুৱা সংস্কৰণ <i>%s</i> ব্যৱহাৰ কৰিছে + আপুনি Morphe Patches ৰ সংস্কৰণ <i>%1$s</i> ব্যৱহাৰ কৰিছে<br><br><b>আপডেট উপলব্ধ: <i>%2$s</i></b><br><br>আপোনাৰ পেচবোৰ আপডেট কৰিবলৈ, এই এপটো পুনৰ পেচ কৰিবলৈ Morphe ব্যৱহাৰ কৰক + টোকা + এই সংস্কৰণটো এটা প্ৰি-ৰিলিজ আৰু আপুনি অপ্ৰত্যাশিত সমস্যাৰ সন্মুখীন হ\'ব পাৰে + চৰকাৰী লিংকসমূহ + ৱেবছাইট + দান কৰক + অনুবাদ + ক্ৰেডিটছ + নৱতম সংস্কৰণ + আপডেট উপলব্ধ + বৰ্তমানৰ উন্নয়ন + পূৰ্বৰ উন্নয়ন + সক্ৰিয় উন্নয়ন + 2022 ‑ 2025 + 2022 ‑ 2025 + 2019 ‑ 2022 + লাইচেন্স জাননী + লোড হৈছে… + ডিবাগিং + উন্নত ডেভেলপাৰ বিকল্প আৰু ডায়াগনষ্টিক লগসমূহ কনফিগাৰ কৰক + ডিবাগ লগিং সক্ৰিয় কৰক + ডিবাগ লগ ষ্টেক ট্ৰেচ + ত্ৰুটিসমূহ চিনাক্ত কৰাত সহায় কৰিবলৈ ডিবাগ লগত ষ্টেক ট্ৰেচ অন্তৰ্ভুক্ত কৰক + Morphe ত্ৰুটিৰ সময়ত toast দেখুৱাওক + "Error toasts বন্ধ কৰিলে সকলো Morphe ত্ৰুটি জাননী লুকুৱাই ৰখা হয়। + +কোনো আশানুপেক্ষিত ঘটনাৰ বিষয়ে আপোনাক জনোৱা নহ'ব" + ডিবাগ লগ এক্সপোর্ট কৰক + Morphe ডিবাগ লগ এক্সপোর্ট কৰক + ডিবাগ লগ সফলভাৱে এক্সপোর্ট কৰা হৈছে + ডিবাগ লগ এক্সপোর্ট কৰাত ব্যৰ্থ হৈছে + ডিবাগ লগৰ বাবে সন্ধান কৰক… + মচি পেলাওক + ডিবাগ লগিং নিষ্ক্ৰিয় কৰা হৈছে + কোনো ডিবাগ লগ পোৱা নগ’ল + ডিবাগ লগ কপি কৰা হৈছে + ডিবাগ লগ এক্সপোর্ট কৰাত ব্যৰ্থ হৈছে: %s + ডিবাগ লগ মুছা হৈছে + ফিচাৰ ফ্ল্যাগ মেনেজাৰ + বুলিয়ান ফিচাৰ ফ্ল্যাগসমূহ পৰিচালনা কৰক + ফিচাৰ ফ্ল্যাগৰ বাবে সন্ধান কৰক… + সক্ৰিয় নহয় (%s) + সক্ৰিয় (%s) + বাৰত দিয়া হৈছে (%s) + বাধ্য কৰা হৈছে (%s) + অটো + বন্ধ + চালু + বাধ্যতৰকৈ চালু কৰক + বাৰত কৰক + হতাওক + ফিচাৰ ফ্লেগসমূহ সংৰক্ষণ কৰা হৈছে + ফিচা ফ্লেগসমূহ ৰিছেট কৰা হৈছে + ফিচা ফ্লেগসমূহ ক্লিপবৰ্ডলৈ কপি কৰা হৈছে + এই ছেটিংটো ব্যৱহাৰ কৰিবলৈ এপটো পুনৰ আৰম্ভ কৰক + ৰপ্তানি আৰু আমদানি + %s টা বৈশিষ্ট্যৰ পৰিচায়ক আমদানি কৰা হৈছে + কোনো ফিচাৰ ফ্ল্যাগ পোৱা নগ\'ল + ফিচাৰ ফ্ল্যাগসমূহ সফলতাৰে এক্সপোর্ট কৰা হৈছে + ফিচাৰ ফ্ল্যাগসমূহ এক্সপোর্ট কৰাত ব্যৰ্থ হৈছে + ফিচাৰ ফ্ল্যাগসমূহ লোড কৰা হৈছে, প্ৰয়োগ কৰিবলৈ Save টেপ কৰক + ফাইলটো পঢ়িবলৈ ব্যৰ্থ হৈছে + বাইনাৰী চাৰ্চ + "কোনটো ফিচাৰ ফ্ল্যাগে এপৰ আচৰণৰ কাৰণ হৈছে সেয়া বিচাৰি উলিয়ায়। + +%s ফ্ল্যাগৰ আধা অংশ ব্লক কৰা হৈছে, আৰু প্ৰতিবাৰ ৰিষ্টাৰ্টৰ পিছত আচৰণটো এতিয়াও আছে নে নাই সেইটো আপুনি জানাব। + +আৰম্ভ কৰাৰ আগতে এবাৰ আচৰণটো পুনৰ সৃষ্টি কৰক, নহ'লে এপটোৱে এতিয়ালৈকে বিচৰা নাই এনে ফ্ল্যাগসমূহ চাৰ্চৰ পৰা অনুপস্থিত থাকিব" + আৰম্ভ কৰক + "সন্ধান কৰিবলৈ কোনো ফিচাৰ ফ্লেগ নাই। + +কেৱল সেইবোৰ ফ্লেগহে বাইনাৰী চাৰ্চত ব্যৱহাৰ কৰিব পাৰি যিবোৰ এপটোৱে হিট কৰিছে আৰু বৰ্তমান জোৰকৈ সক্ৰিয় কৰা হোৱা নাই" + "ধাপ %1$s + +%2$s টাৰ ফ্ল্যাগ বাকী আছে, যাৰ ভিতৰত %3$s টা এই পদক্ষেপৰ বাবে ব্লক কৰা হৈছে। + +আৰু এবাৰ এপটো ৰিষ্টাৰ্ট কৰক, আচৰণটো উপস্থিত আছে নে নাই পৰীক্ষা কৰক, তাৰ পিছত তলত উত্তৰ দিয়ক" + ফিচাৰ ফ্ল্যাগৰ সন্ধান চলি আছে + আচৰণটো উপস্থিত আছে + আচৰণটো উপস্থিত নাই + বাইনাৰী চাৰ্চ বাতিল কৰক + বাইনাৰী চাৰ্চ বাতিল কৰা হৈছে + "ফিচাৰ ফ্ল্যাগ %s-এ আচৰণটোৰ সৃষ্টি কৰিছে। + +ই এতিয়া ব্লক কৰা হৈছে" + "এটা ফিচাৰ ফ্ল্যাগ বিচাৰি পোৱা নগ'ল + +এপ্লিকেচনটোৰ আচৰণ এটা ফিচাৰ ফ্ল্যাগৰ দ্বাৰা নিয়ন্ত্ৰিত হোৱা যেন দেখা নাই" + "এটাও ফিচাৰ ফ্ল্যাগ বিচাৰি পোৱা নগ'ল + +আচৰণটো সন্ধানৰ আগতেই নাইকিয়া হৈ গৈছে, গতিকে একাধিক ফ্ল্যাগ জড়িত হ'ব পাৰে বা উত্তৰবোৰৰ কোনো এটা ভুল আছিল" + MicroG ছেটিংস খোলাক + MicroG সংস্থাপন কৰা হোৱা নাই। ইয়াক সংস্থাপন কৰক + কাৰ্যৰ প্ৰয়োজন + "আপোনাৰ MicroG v%1s পুৰণি আৰু ই সমস্যাৰ সৃষ্টি কৰিব পাৰে। + +অনুগ্ৰহ কৰি MicroG-ক শেহতীয়া সংস্কৰণ v%2s লৈ আপডেট কৰক" + আপডেট কৰক + অৱহেলা কৰক + "সংস্থাপন কৰা MicroG-টো Morphe-ৰ দ্বাৰা পৰামৰ্শ দিয়া হোৱা নাই: + +%1s + +ইয়াক আন-ইনষ্টল কৰিবলৈ ট্যাপ কৰক, তাৰপিছত এই এপটো পুনৰ খোলক আৰু MicroG RE সংস্থাপন কৰক" + আন-ইনষ্টল কৰক + "MicroG-ক পটভূমিত চলিবলৈ অনুমতি নাই। + +আপোনাৰ ফোনৰ বাবে \"Don't kill my app\" নিৰ্দেশিকাটো অনুসৰণ কৰক, আৰু আপোনাৰ MicroG সংস্থাপনত সেই নিৰ্দেশনাবোৰ প্ৰয়োগ কৰক। + +এপ্লিকেচনটো কাম কৰিবলৈ ইয়াৰ প্ৰয়োজন" + ৱেবছাইট খোলক + "সমস্যা প্ৰতিৰোধ কৰিবলৈ MicroG ব্যাটাৰী অপ্টিমাইজেশ্যনসমূহ নিষ্ক্ৰিয় কৰিব লাগিব। + +MicroG-ৰ বাবে ব্যাটাৰী অপ্টিমাইজেশ্যন নিষ্ক্ৰিয় কৰিলে ব্যাটাৰীৰ ব্যৱহাৰত কোনো নেতিবাচক প্ৰভাৱ পৰিব নালাগে। + +কন্টিনিউ বুটামত ক্লিক কৰক আৰু অপ্টিমাইজেশ্যন পৰিৱৰ্তনৰ অনুমতি দিয়ক" + চালু ৰাখক + MicroG বেটাৰী অপ্টিমাইজেশ্যন ডায়লগ দেখুৱাওক + "এইটো নিষ্ক্ৰীয় কৰিলে MicroG বেটাৰী অপটিমাইজেশ্যন প্ৰম্পটটো লুকুৱাই ৰখা হ'ব। + +যদি বেটাৰী অপটিমাইজেশ্যন সলনি কৰা নহয় তেন্তে আপুনি নিম্নলিখিত সমস্যাৰ সন্মুখীন হ'ব পাৰে: +• এপটো হঠাতে বন্ধ হ'ব পাৰে +• নটিফিকেচনবোৰে কাম নকৰিব পাৰে +• বেকগ্ৰাউণ্ড প্লেবেকে কাম নকৰিব পাৰে" + এপ লিংক হেণ্ডলিং + এই এপ্লিকেচনত লিংকসমূহ খুলিবলৈ কনফিগাৰ কৰক + ধাপ ১: মূল এপ লিংকসমূহ নিষ্ক্ৰিয় কৰক + মূল এপটোৰ ছেটিংছ খোলক আৰু সকলো ডোমেইন লিংক নিষ্ক্ৰিয় কৰক + ধাপ ২: পেচড এপ লিংকসমূহ সক্ৰিয় কৰক + পেচড এপটোৰ ছেটিংছ খোলক আৰু সকলো ডোমেইন লিংক সক্ৰিয় কৰক + ছেটিংছ খোলাক + 🚨 এক্সপৰিমেণ্টেল এলাৰ্ট 🚨 + "আপুনি ⚠️ <b>%1$s</b> ৰ এটা এক্সপৰিমেণ্টেল এপ সংস্কৰণ ব্যৱহাৰ কৰি আছে +<br/><br/> +🔧 আমি এই এপ সংস্কৰণটোৰ বাবে পেচসমূহ নিখুঁত কৰাৰ সময়ত কিছু অদ্ভুত আচৰণ বা চিনাক্ত নোহোৱা বাগৰ সম্ভাৱনা থাকিব পাৰে। +<br/><br/> +যদি আপুনি সমস্যাৰাহিত অভিজ্ঞতা বিচাৰে, তেন্তে ইয়াক <b>আনইনষ্টল</b> কৰক আৰু ✅ <b>%2$s</b> ৰ পৰিচিত এপ সংস্কৰণটো পেচ কৰক" + ⚠️ মই এক্সপৰিমেণ্টেল বিচাৰো + ✅ মই ষ্টেবল বিচাৰো + ক্ৰাশ লগ ব্যৱহাৰকাৰীৰ নথি পত্ৰত সংৰক্ষণ কৰা হৈছে: %s + ক্ৰাশ লগ সংৰক্ষণ কৰা হৈছে: %s diff --git a/patches/src/main/resources/addresources/values-as-rIN/sponsorblock/strings.xml b/patches/src/main/resources/addresources/values-as-rIN/sponsorblock/strings.xml index 9d2d4ab7d5..59110fdf6d 100644 --- a/patches/src/main/resources/addresources/values-as-rIN/sponsorblock/strings.xml +++ b/patches/src/main/resources/addresources/values-as-rIN/sponsorblock/strings.xml @@ -16,7 +16,220 @@ Second \"item\" text" --> + SponsorBlock সক্ৰিয় কৰক + SponsorBlock হৈছে YouTube ভিডিঅ\'ৰ বিরক্তিকৰ অংশসমূহ স্কিপ কৰিবলৈ এটা ক্ৰাউডচৰ্ছড প্ৰণালী + প্ৰকাশ + ভোটদান বুটাম দেখুৱাওক + খণ্ডসমূহক আপভোট বা ডাউনভোট দিবলৈ এটা বুটাম যোগ কৰে + বৰ্গাকৃতি লেআউট ব্যৱহাৰ কৰক + SponsorBlock বুটাম আৰু কন্ট্ৰলৰ বাবে বৰ্গাকাৰ কোণ ব্যৱহাৰ কৰে + কম্পেক্ট Skip বুটাম ব্যৱহাৰ কৰক + জায়গা ৰক্ষা কৰিবলৈ Skip বুটামৰ বহলতা হ্ৰাস কৰে + স্বয়ংক্ৰিয়ভাৱে Skip বুটামটো লুকুৱাই ৰাখক + সম্পূৰ্ণ ছেগমেণ্টটোৰ বাবে দৃশ্যমান কৰি ৰখাৰ পৰিৱৰ্তে কেইছেকেণ্ডমানৰ পিছত Skip বুটামটো লুকুৱাই দিয়ে + Skip বুটামৰ সময়সীমা + Skip বুটামটো কিমান সময়লৈ দৃশ্যমান হৈ থাকিব সেইটো নিৰ্ধাৰণ কৰক + স্কিপ নটিফিকেচন দেখুৱাওক + যেতিয়া এটা ছেগমেণ্ট স্বয়ংক্ৰিয়ভাৱে স্কিপ কৰা হয় তেতিয়া এটা নটিফিকেচন দেখুৱায় + স্কিপ নটিফিকেচনৰ সময়সীমা + আনডু নটিফিকেচনটো কিমান সময়লৈ দৃশ্যমান হৈ থাকিব সেইটো নিৰ্ধাৰণ কৰক + ১ ছেকেণ্ড + ২ ছেকেণ্ড + ৩ ছেকেণ্ড + ৪ ছেকেনড + ৫ ছেকেনড + ৬ ছেকেনড + ৭ ছেকেনড + ৮ ছেকেনড + ৯ ছেকেনড + ১০ ছেকেনড + ছেগমেন্ট অবিহনে ভিডিঅ\'ৰ দৈৰ্ঘ্য দেখুৱাওক + চীকবাৰত দেখুওৱা মুঠ ভিডিঅ\'ৰ সময়ৰ পৰা সকলো ছেগমেন্ট বিয়োগ কৰে + নতুন ছেগমেন্ট সৃষ্টি কৰা + \'নতুন ছেগমেণ্ট সৃষ্টি কৰক\' বুটামটো দেখুৱাওক + ভিডিঅ\' প্লেয়াৰত এটা বুটাম যোগ কৰে যাৰ জৰিয়তে আপুনি নতুন SponsorBlock ছেগমেণ্ট জমা দিব পাৰিব + সময়ৰ সামঞ্জস্যৰ পদক্ষেপ + সময়ৰ সামঞ্জস্যৰ বুটামবোৰৰ সন্ধানৰ পৰিমাণ (মিলিসেকণ্ডত) + মানটো এটা ধনাত্মক সংখ্যা হ\'ব লাগিব + জমা দিয়াৰ নিৰ্দেশাৱলী + নিৰ্দেশাৱলীসমূহত নতুন ছেগমেণ্ট সৃষ্টি কৰাৰ বাবে নিয়ম আৰু পৰামৰ্শ থাকে + নিৰ্দেশাৱলীসমূহ অনুসৰণ কৰক + নতুন ছেগমেন্ট সৃষ্টি কৰাৰ আগতে SponsorBlock নিৰ্দেশিকাটো পঢ়ক + আগতেই পঢ়া হৈছে + মোক দেখুৱাওক + সাধাৰণ + সংযোগৰ ত্ৰুটিৰ সতৰ্কবাৰ্তাসমূহ দেখুৱাওক + SponsorBlock চাৰ্ভাৰটো উপলব্ধ নোহোৱাৰ সময়ত এটা জাননী দেখুৱায় + স্কিপ কাউণ্ট ট্ৰেকিং সক্ষম কৰক + কিমান সময় ৰৈছে তাৰ ট্ৰেক কৰিবলৈ SponsorBlock লিডাৰবৰ্ডলৈ স্কিপ কাউণ্টবোৰ পঠিয়াই + খণ্ডৰ ন্যূনতম সময়সীমা + এই মানতকৈ (ছেকেণ্ডত) চুটি খণ্ডবোৰ দেখুওৱা নহ\'ব বা স্কিপ কৰা নহ\'ব + অ वैध সময়সীমা + আপোনাৰ ব্যক্তিগত ব্যৱহাৰকাৰী ID + এয়া গোপন ৰখা উচিত। এয়া এটা পাছৱৰ্ডৰ দৰে আৰু কাৰোবাৰ সৈতে ভাগ কৰিব নালাগে। যদি কাৰোবাৰ হাতত এইটো থাকে, তেওঁ আপোনাৰ নামত ভুৱা পৰিচয় দিব পাৰে + ব্যক্তিগত ব্যৱহাৰকাৰী ID অন্ততঃ ৩০ টা আখৰৰ হ\'ব লাগে + API URL পৰিৱৰ্তন কৰক + ছাৰ্ভাৰলৈ কল কৰিবলৈ SponsorBlock-এ ব্যৱহাৰ কৰা ঠিকনাটো + চেনেল হোৱাইটলিষ্ট + হোৱাইটলিষ্ট কৰা চেনেলবোৰৰ বাবে SponsorBlock নিষ্ক্ৰিয় কৰা হৈছে + হোৱাইটলিষ্ট কৰা চেনেলৰ বাবে জাননী দিয়ক + হোৱাইটলিষ্ট কৰা কোনো চেনেলৰ বাবে SponsorBlock নিষ্ক্ৰিয় হৈ থাকিলে এটা জাননী দেখুৱাব + এই চেনেলটোৰ বাবে SponsorBlock নিষ্ক্ৰিয় কৰা হৈছে + API URL ৰীছেট কৰা হৈছে + API URL টো অবৈধ + API URL সলনি কৰা হৈছে + Import / Export ছেটিংছ + কপি কৰক + আপোনাৰ SponsorBlock JSON কনফিগাৰেশ্যন যিটো Morphe আৰু অন্যান্য SponsorBlock প্লেটফৰ্মলৈ আমদানি / ৰপ্তানি কৰিব পাৰি + "আপোনাৰ SponsorBlock JSON কনফিগাৰেশ্যন যিটো Morphe আৰু অন্যান্য SponsorBlock প্লেটফৰ্মলৈ আমদানি / ৰপ্তানি কৰিব পাৰি + +ইয়াত আপোনাৰ ব্যক্তিগত ইউজাৰ ID অন্তৰ্ভুক্ত থাকে। ইয়াক সাৱধানে শ্বেয়াৰ কৰক" + ছেটিংছসমূহ সফলতাৰে ইম্পৰ্ট কৰা হৈছে + ইম্পৰ্ট কৰাত ব্যৰ্থ হৈছে: %s + এক্সপৰ্ট কৰাত ব্যৰ্থ হৈছে: %s + "আপোনাৰ ছেটিংছত এটা ব্যক্তিগত SponsorBlock ব্যৱহাৰকাৰী ID আছে। + +আপোনাৰ ব্যৱহাৰকাৰী ID টো এটা পাছৱৰ্ডৰ দৰে, আৰু ইয়াক কেতিয়াও আনৰ সৈতে ভাগ কৰিব নালাগে" + পুনৰ দেখুৱাব নালাগে + ছেগমেণ্টৰ আচৰণ সলনি কৰক + স্পনছৰ + পেইড প্ৰমোদচন, পেইড ৰিফাৰেল আৰু পোনপটীয়া বিজ্ঞাপন। আত্ম-প্ৰমোচন বা তেওঁলোকে পছন্দ কৰা কাৰ্য/সৃষ্টিকৰ্তা/ৱেবচাইট/প্ৰডাক্টৰ বিনামূলীয়া শ্বাউট-আউটৰ বাবে নহয় + বিনামূলীয়া / আত্ম-প্ৰমোচন + বিনামূলীয়া / আত্ম-প্ৰমোচনৰ বাদে স্পনছৰৰ দৰেই। ইয়াত মাৰ্চেনডাইজ, অনুদান, বা তেওঁলোকে কাৰ সৈতে সহযোগিতা কৰিছে তাৰ তথ্য সংলগ্ন আছে + আন্তঃক্ৰিয়াৰ সোঁৰণ কাৰক (চাবস্ক্ৰাইব কৰক) + চামগৰ মাজতে লাইক, চাবস্ক্ৰাইব বা ফলো কৰিবলৈ এটা চুটি সোঁৱৰণি। যদি ই দীঘলীয়া বা কোনো বিশেষ কথাৰ ওপৰত হয়, তেন্তে ইয়াক স্ব-প্ৰচাৰৰ অধীনত ৰখা উচিত + হাইলাইট + ভিডিঅ\'ৰ সেই অংশটো যিটো বেছিভাগ মানুহে বিচাৰে + অন্তৰাল / ইন্ট্রো এনিমেশ্বন + আচল বিষয়বস্তু নথকা এটা অন্তৰাল। ই এটা বিৰতি, স্থিৰ ফ্ৰেম, বা পুনৰাবৃত্তি হোৱা এনিমেশ্বন হ\'ব পাৰে। ইয়াত তথ্যযুক্ত পৰিৱৰ্তনসমূহ অন্তৰ্ভুক্ত নহয় + এণ্ডকাৰ্ডছ / ক্ৰেডিটছ + ক্ৰেডিটছ বা যেতিয়া YouTube এণ্ডকাৰ্ডসমূহ দেখা যায়। তথ্যসহ সমাপ্তিসমূহৰ বাবে নহয় + হুক / সম্ভাষণ + আহাৰ মানি আগন্তুক ভিডিঅ\'ৰ বাবে বৰ্ণনা কৰা ট্ৰেলাৰ, সম্ভাষণ আৰু বিদায়। ইয়াত অতিৰিক্ত বিষয়বস্তু যোগ কৰা বিভাগসমূহ অন্তৰ্ভুক্ত নহয় + পিৰুভিউ / ৰিক্যাপ + ক্লিপৰ সংগ্ৰহ যিয়ে দেখুৱায় যে ভিডিঅ\'টোত বা শৃংখলৰ অন্যান্য ভিডিঅ\'ত কি অহাৰ আছে বা কি ঘটিছে, য\'ত সকলো তথ্য আন ঠাইতে পুনৰাবৃত্তি কৰা হৈছে + টেনজেন্ট / জকছ + পৰোক্ষ দৃশ্য বা জকছ যিবোৰ ভিডিঅ\'টোৰ মূল বিষয়বস্তু বুজিবলৈ প্ৰয়োজন নাই। ইয়াৰ ভিতৰত প্ৰসংগ বা পটভূমিৰ সবিশেষ প্ৰদান কৰা অংশসমূহ অন্তৰ্ভুক্ত নহয় + সংগীত: সংগীতহীন অংশ + কেৱল সংগীত ভিডিঅ\'ৰ ক্ষেত্ৰত ব্যৱহাৰ কৰিব। সংগীত ভিডিঅ\'ৰ সংগীতহীন অংশ যিবোৰ ইতিমধ্যে অন্য কোনো শ্ৰেণীৰ অন্তৰ্গত হোৱা নাই + স্কিপ কৰক + হাইলাইট + স্পনছৰ স্কিপ কৰক + প্ৰোমো স্কিপ কৰক + আন্তঃক্ৰিয়া বাদ দিয়ক + হাইলাইটলৈ যাওক + আৰম্ভণিৰ অংশটো বাদ দিয়ক + অন্তৰা ত্বৰান্বিত কৰক + অন্তৰা ত্বৰান্বিত কৰক + আউট্ৰো ত্বৰান্বিত কৰক + হুক ত্বৰান্বিত কৰক + প্ৰিভিউ ত্বৰান্বিত কৰক + প্ৰিভিউ ত্বৰান্বিত কৰক + ৰিক্যাপ ত্বৰান্বিত কৰক + অপ্রাসঙ্গিক কথা ত্বৰান্বিত কৰক + সংগীত নোহোৱা অংশ ছেক কৰক + খণ্ডটো ছেক কৰক + স্পনচৰ অংশ ছেক কৰা হ\'ল + নিজৰ প্ৰচাৰৰ অংশ ছেক কৰা হ\'ল + নিৰুদ্দেশ কৰা ৰিমেণ্ডাৰ ছেক কৰা হ\'ল + হাইলাইটৰ ফালে ছেক কৰা হ\'ল + আৰম্ভণিৰ অংশ ছেক কৰা হ\'ল + মধ্যভৰতি ছেক কৰা হ\'ল + মধ্যভৰতি ছেক কৰা হ\'ল + সামৰণিৰ অংশ ছেক কৰা হ\'ল + হুক অংশ ছেক কৰা হ\'ল + প্ৰিভিউ স্কিপ কৰা হৈছে + প্ৰিভিউ ত্বৰান্বিত কৰা হৈছে + ৰিক্যাপ ত্বৰান্বিত কৰা হৈছে + বৰ্ণনাক্ৰম বাদ দিয়া হৈছে + সংগীত নোহোৱা বিভাগ এটা বাদ দিয়া হৈছে + নজমা লোৱা খণ্ড এটা বাদ দিয়া হৈছে + বহুতো খণ্ড বাদ দিয়া হৈছে + স্বয়ংক্ৰিয়ভাৱে বাদ দিয়ক + একবাৰ স্বয়ংক্ৰিয়ভাৱে বাদ দিয়ক + এটা Skip বুটাম দেখুৱাওক + Seekbar-ত দেখুৱাওক + অক্ষম কৰক + ছেগমেন্টটো জমা দিব নোৱাৰিল: %s + SponsorBlock সাময়িকভাৱে বন্ধ হৈ আছে + ছেগমেন্টটো জমা কৰিব নোৱাৰিল (স্থিতি: %1$s %2$s) + ছেগমেন্টটো জমা দিব নোৱাৰিল। ৰেট লিমিটেড (একেজন ব্যৱহাৰকাৰী বা IP পৰা অতিৰিক্ত সংখ্যক অনুৰোধ) + ছেগমেন্টটো জমা দিব নোৱাৰি: %s + "ছেগমেন্টটো জমা দিব নোৱাৰি। +আগতেই বিদ্যমান" + ছেগমেন্টটো সফলভাৱে জমা দিয়া হৈছে + SponsorBlock সাময়িকভাৱে উপলব্ধ নহয় (API টাইম আউট হৈছে) + SponsorBlock সাময়িকভাৱে উপলব্ধ নহয় (স্থিতি %s) + SponsorBlock সাময়িকভাৱে উপলব্ধ নহয় + ছেগমেণ্টটোৰ বাবে ভোট দিবলৈ অক্ষম (API টাইম আউট হৈছে) + ছেগমেন্টটোৰ বাবে ভোট দিব নোৱাৰিল (স্থিতি: %1$s %2$s) + ছেগমেণ্টটোৰ বাবে ভোট দিবলৈ অক্ষম: %s + আপভোট + ডাউনভোট + শ্ৰেণী সলনি কৰক + ভোট দিবলৈ কোনো ছেগমেণ্ট নাই + %1$s ৰ পৰা %2$s লৈ + ছেগমেণ্ট শ্ৰেণীটো বাছনি কৰক + ছেটিংছত কেটেগৰীটো নিষ্ক্ৰীয় কৰা হৈছে। জমা কৰিবলৈ কেটেগৰীটো সক্ৰীয় কৰক + নতুন SponsorBlock খণ্ড + %s-ক এটা নতুন খণ্ডৰ আৰম্ভণি বা শেষ হিচাপে নিৰ্ধাৰণ কৰিমনে? + আৰম্ভণি + শেষ + এতিয়াই + খণ্ডটো আৰম্ভ হোৱা সময় + খণ্ডটো শেষ হোৱা সময় + সময়বোৰ শুদ্ধ নে? + "খণ্ডটো + +%1$s +ৰ পৰা +%2$s +লৈ + +(%3$s) + +দাখিল কৰিবলৈ সাজু নে?" + জমা দিয়ক + আৰম্ভটো শেষৰ আগতে হ\'ব লাগিব + প্ৰথমে টাইম বাৰত এটা স্থান চিহ্নিত কৰক + চিগমেন্টটোৰ প্ৰিভিউ কৰক, আৰু ই মসৃণভাৱে স্কিপ কৰা নিশ্চিত কৰক + চিগমেন্টৰ টাইমিংটো হাতেৰে সম্পাদনা কৰক + আপুনি চিগমেন্টটোৰ আৰম্ভণি বা শেষৰ টাইমিংটো সম্পাদনা কৰিব বিচাৰে নেকি? + অহা সময়টো অবৈধ + হাইলাইট জমা দিয়ক + "হাইলাইটৰ সময়টো শুদ্ধ নে? + +%s + +যদি আপুনি হাইলাইট জমা নিদিয়ে, তেন্তে Cancel টিপক আৰু দ্বিতীয়টো স্থান চিহ্নিত কৰক" + হাইলাইট জমা কৰক + প্ৰসাংখ্য + প্ৰসাংখ্য সাময়িকভাবে উপলব্ধ নহয় (API ডাউন হৈছে) + লোডিং হৈছে… + SponsorBlock নিষ্ক্ৰিয় কৰা হৈছে + আপোনাৰ ইউজাৰনেম: <b>%s</b> + আপোনাৰ ইউজাৰনেম সলনি কৰিবলৈ ইয়াত টেপ কৰক + ব্যৱহাৰকাৰীৰ নাম সলনি কৰিব নোৱাৰিল: স্থিতি: %1$s %2$s + ইউজাৰনেম সফলতাৰে সলনি কৰা হৈছে + আপোনাৰ খ্যাতি হ\'ল <b>%.2f</b> + আপুনি <b>%s</b> টা চেগমেন্ট সৃষ্টি কৰিছে + আপোনাৰ চেগমেণ্টসমূহ চাবলৈ ইয়াত টেপ কৰক + SponsorBlock লিডাৰবৰ্ড + আপুনি মানুহক <b>%s</b> চেগমেণ্টৰ পৰা ৰক্ষা কৰিছে + গোলকীয় পৰিসংখ্যা আৰু শীৰ্ষ দাতা সকলক চাবলৈ ইয়াত টেপ কৰক + সেয়া তেওঁলোকৰ জীৱনৰ <b>%s</b> অংশ।<br>লিডাৰবৰ্ডটো চাবলৈ ইয়াত টেপ কৰক + আপুনি <b>%s</b> টা চেগমেণ্ট স্কিপ কৰিছে + সেয়া হ\'ল <b>%s</b> + স্কিপ কৰা চেগমেণ্টৰ কাউণ্টাৰটো ৰিছেট কৰিবনে? + %1$s ঘণ্টা %2$s মিনিট + %1$s মিনিট %2$s ছেকেণ্ড + %s ছেকেণ্ড + অপাসিটি: + ৰং: + "SponsorBlock হৈছে ভিডিঅ'ৰ বিভিন্ন খণ্ড যেনে স্পনছৰ, ইন্ট্রো, আউট্ৰো আৰু চাবস্ক্ৰাইব কৰাৰ ৰিমাইণ্ডাৰৰ এটা ক্লাউডচৰ্ছড ডেটাবেচ। এই খণ্ডসমূহ সম্প্ৰদায়ৰ দ্বাৰা জমা দিয়া হয় আৰু ভোট দিয়া হয়, আৰু ডেটাবেচটো মুকলি আৰু বিনামূল্যে ডাউনলোড কৰিব পৰা হয় + +আপুনি যিটো ভিডিঅ' চাইছে তাৰ ID SponsorBlock API-লৈ পঠিয়াই ইয়াৰ খণ্ডসমূহ সংগ্ৰহ কৰা হয়" diff --git a/patches/src/main/resources/addresources/values-as-rIN/youtube/strings.xml b/patches/src/main/resources/addresources/values-as-rIN/youtube/strings.xml index 70ce03fc67..16d628a4a5 100644 --- a/patches/src/main/resources/addresources/values-as-rIN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-as-rIN/youtube/strings.xml @@ -18,213 +18,1631 @@ Second \"item\" text" + Morphe ৰ বিষয়ে + বিজ্ঞাপন + ফীড + সাধাৰণ + প্লেয়াৰ + Shorts + ছিকবাৰ (Seekbar) + চুইপ নিয়ন্ত্ৰক + বিবিধ + ভিডিঅ’ + পুৰণি YouTube ছেটিংছ স্ক্ৰীনটো পুনৰুদ্ধাৰ কৰক + বেকগ্ৰাউণ্ড প্লে ৰৈ restrictions বো আঁতৰ কৰক + বেকগ্ৰাউণ্ড প্লে নিষ্ক্ৰিয় কৰক + ডিভাইচ লক হোৱাৰ সময়ত স্বয়ংক্ৰিয়ভাৱে পজ কৰক + বন্ধ + লক হোৱাৰ সময়ত সদায় পজ কৰক + ৱায়াৰলেছ অডিঅ\' সংযোগ হৈ নাথাকিলে পজ কৰক + স্ক্ৰলিং গতিৰ সীমাটো নিষ্ক্ৰিয় কৰক + লেআউট আপডেটসমূহ নিষ্ক্ৰীয় কৰক + ছাৰ্ভাৰক এপ লেআউট আপডেট কৰাত বাধা দিয়ে + "এপৰ লেআউটে প্ৰথমবাৰত সংস্থাপন কৰা সময়ত ব্যৱহাৰ কৰা লেআউটলৈ উভতি যাব। + +কিছুমান চাৰ্ভাৰ-চাইড লেআউট উভতি নাযাব পাৰে। + +পৰিবৰ্তনবোৰৰ ভিতৰত আছে: +• প্লেয়াৰ ফ্লাইআউট মেনুৰ উপাদানবোৰ (বা সম্পৰ্কীয় ছেটিংছ) কাম নকৰিব পাৰে +• ৰোলিং নম্বৰবোৰ এনিমেটেড নহয় +• লাইব্ৰেৰী টেবটো দেখুৱাব +• ভিডিঅ' বৰ্ণনাৰ সংগীত বিভাগটো কাম নকৰিব পাৰে +• প্লেয়াৰ ফ্লাইআউট মেনুৰ আইটেমবোৰ লুকুৱাব নোৱাৰি +• লাইব্ৰেৰী টেবত একাউণ্ট সলনি কৰা বুটনে দেখা নাযাব পাৰে" + ফীড ফ্লাইআউট মেনু ফিল্টাৰ সক্ৰীয় কৰক + ফীড ফ্লাইআউট মেনু ফিল্টাৰ + ফিল্টাৰ কৰিবলৈ ফ্লাইআউট মেনুৰ নামসমূহ লিখক, যিবোৰ নতুন লাইনৰ দ্বাৰা পৃথক কৰা থাকিব + লাইব্ৰেৰী / আপুনি তলৰ মেনু ফিল্টাৰ সক্ৰিয় কৰক + লাইব্ৰেৰী / আপুনি তলৰ মেনু ফিল্টাৰ + ফিল্টাৰ কৰিবলগীয়া লাইব্ৰেৰী / আপুনি তলৰ মেনুৰ নামবোৰ লিখক, নতুন লাইনৰ দ্বাৰা পৃথক কৰক + এলবাম কাৰ্ডবোৰ লুকুৱাওক + শিল্পীৰ বৰ্ণনাৰ তলত থকা এলবাম কাৰ্ডবোৰ লুকুৱাই ৰাখে + শিল্পী কাৰ্ডবোৰ লুকুৱাওক + অনুসন্ধানৰ ফলাফলৰ ওপৰত থকা শিল্পীৰ কাৰ্ডখন লুকুৱাই ৰাখে + চেনেলৰ বুটൺবোৰ লুকুওক + সন্ধানৰ ফলাফলত চেনেল প্ৰফাইলৰ পৰা Subscribe, View Channel, Community, আৰু YouTube Music ৰ দৰে বুটൺবোৰ লুকুৱাই দিয়ে + কমিউনিটি পোষ্টবোৰ লুকুৱাওক + কম্পেক্ট বেনাৰবোৰ লুকুৱাওক + কম্পেক্ট বেনাৰ আৰু কিছুমান উপাদানৰ মাজৰ খালী ঠাই লুকুৱাই ৰাখে + ভিডিঅ’ৰ তলত থকা সম্প্ৰসাৰণযোগ্য কাৰ্ডসমূহ + সকলো দেখুৱাওক + কেৱল সামৰিপাটৰ কাৰ্ডটো লুকুৱাওক + সকলো লুকুৱাওক + ফ্লোটিং মাইক্ৰোফোন বুটামটো লুকুৱাওক + অনুসন্ধান পৰামৰ্শ স্ক্ৰীণৰ তলৰ সোঁ কাষত থকা ফ্লোটিং মাইক্ৰ’ফোন বুটামটো লুকুৱাই ৰাখে + \'Get Premium\' বুটামটো লুকুৱাওক + ইতিহাস শ্বেল লুকুৱাওক + অনুভূমিক শ্বেল্ফবোৰ লুকুৱাওক + Breaking news, Community posts you may like, Continue watching, Explore more channels, Explore more topics, For You, Latest videos, Most relevant, Posts from Creator\'s Community, Shopping, আৰু Watch it again ৰ দৰে অনুভূমিক শ্বেল্ফবোৰ লুকুৱাই পেলাব + ছবিৰ শ্বেল্ফটো লুকুৱাওক + অনুসন্ধানৰ ফলাফলত ছবিৰ শ্বেল্ফসমূহ লুকুৱাই ৰাখে + \'আনক বাৰ্তা পঠিয়াবলৈ আমন্ত্ৰণ কৰক\' কাৰ্ডটো লুকুৱাওক + নটিফিকেচন টেবৰ বাৰ্তা বিভাগত দেখুওৱা আমন্ত্ৰণ কাৰ্ডটো লুকুৱাই পেলায় + \'নৱীনতম ভিডিঅ\'বোৰ\' বুটামটো লুকুৱাওক + হোম ফিডৰ ওপৰত দেখা দিয়া নীলা ৰঙৰ \'শেহতীয়া ভিডিঅ\' বেনাৰটো লুকুৱাই ৰাখে + লাইভ ষ্ট্ৰীমসমূহ লুকুৱাই ৰাখক + হোম ফিড আৰু সন্ধানৰ ফলাফলত বৰ্তমান লাইভ থকা ভিডিঅ\'সমূহ লুকুৱাই ৰাখে + মিক প্লেলিষ্টবোৰ লুকুৱাওক + চিনেমাৰ বিভাগটো লুকুৱাওক + জাননী মেনুৰ হেডাৰটো লুকুৱাওক + বেল বুটামত ক্লিক কৰিলে জাননী মেনুৰ হেডাৰটো লুকুৱাই ৰাখে + \'মোক জনাওক\' বুটামটো লুকুৱাওক + তনিৰ্দিষ্ট সময়ৰ লাইভ ষ্ট্ৰীম আৰু প্ৰিমিয়াৰৰ বাবে ৰিমাইণ্ডাৰ বুটামটো লুকুৱায় + Playables লুকুৱাওক + আপোনাক গেম খেলিবলৈ অনুমতি দিয়া বৈশিষ্ট্যটো লুকুৱাই ৰাখে + আৰ্চৰ শব্দৰ থাম্বনেইলসমূহ লুকুওক + অনুসন্ধান autocomplete তালিকাত thumbnail previews বোৰ লুকুৱাই ৰাখে + \'আৰু দেখুৱাওক\' বুটামটো লুকুৱাওক + অনুসন্ধানৰ ফলাফলত ভিডিঅ’ৰ তলত থকা expand বুটনে লুকুৱাই ৰাখে + চাবস্ক্ৰাইব কৰা চেনেলৰ বাৰটো লুকুৱাওক + সাবস্ক্ৰাইব কৰা চেনেলৰ নামবোৰ লুকুওক + সাবস্ক্ৰাইব কৰা চেনেলৰ বাৰত চেনেলৰ নামবোৰ লুকুৱাই দিয়ে + জৰীপসমূহ লুকুৱাওক + ফীডৰ পৰামৰ্শৰ সঠিকতাৰ বিষয়ে সমীক্ষা আৰু নজৰ কাঢ়ি লোৱা বাৰ্তাসমূহ লুকুৱাওক + টিকট শ্বেল্ফটো লুকুৱাওক + আপলোড কৰা সময় লুকুৱাওক + "সীমাবদ্ধতা: +• Shorts শ্বেলফ, চেনেলৰ পৃষ্ঠা আৰু সন্ধানৰ ফলাফলত আপলোডৰ সময় এতিয়াও দেখুৱাব পাৰে +• অটোমোটিভ লেআউটৰ সৈতে কাম নকৰে" + ভিডিঅ’ থাম্বনেইল লুকুৱাওক + ভিডিঅ’ লেবেলসমূহ লুকুৱাওক + \'Auto-dubbed\' / \'Dubbed\', \'Earns commission\', \'Hyped\', \'People also watched\', \'product(s)\' আৰু \'You might also like\' ৰ দৰে লেবেলসমূহ লুকুৱায় + ভিউ কাউন্ট লুকুৱাওক + "সীমাবদ্ধতা: +• Shorts শ্বেলফ, চেনেলৰ পৃষ্ঠা আৰু সন্ধানৰ ফলাফলত ভিউৰ সংখ্যা এতিয়াও দেখুৱাব পাৰে +• অটোমোটিভ লেআউটৰ সৈতে কাম নকৰে" + ৱেব চাৰ্চৰ ফলাফলসমূহ লুকুৱাওক + অনুসন্ধানৰ ফলাফলৰ পৰা \'ৱেবৰ পৰা ফলাফল\' অংশটো লুকুৱাই ৰাখে + \'আপুনি ভাল পাব পাৰে\' অংশটো লুকুৱাই থওক + YouTube Doodles লুকুৱাওক + YouTube ল\'গ\'টোত ঋতুভিত্তিক আৰু অনুষ্ঠান-ভিত্তিক animation বোৰ লুকুৱাই ৰাখে + YouTube Doodles দেখুৱালে কাস্টম হেডাৰসমূহ প্ৰদৰ্শিত হোৱাত বাধা দিব পাৰে + চেনেল বাৰ লুকুৱাওক + চেনেল ৱাটাৰমাৰ্ক লুকুৱাওক + অধ্যায় আৰু \'এই ভিডিঅ\'টোত\' বুটামটো লুকুৱাওক + ক্ৰাউডফাণ্ডিং বক্সটো লুকুৱাওক + জৰুৰী বক্সসমূহ লুকুৱাওক + তথ্য প্য়ানেলসমূহ লুকুৱাওক + তথ্যগত context panel বোৰ লুকুৱাই ৰাখে, যেনে চিকিৎসাৰ তথ্য + যোগদান কৰক বুটনটো লুকুৱাওক + \'লাইভ চ্যাট ৰিপ্লে\' বুটামটো লুকুৱাওক + ফুলস্ক্রীন মোডত চেট ৰিপ্লে বুটামটো লুকুৱাই ৰাখে + চিকিৎসা প্য়ানেলসমূহ লুকুৱাওক + প্লেয়াৰ জেষ্টাৰৰ ইংগিতসমূহ লুকুৱাওক + প্লেয়াৰ জেষ্টাৰৰ সময়ত দেখুওৱা ইংগিতসমূহ লুকুৱাই পেলায়, যেনে \'সঠিকভাৱে খোজ কাঢ়িবলৈ ওপৰলৈ টানি লওক\', \'বাতিল কৰিবলৈ এৰি দিয়ক\' আৰু \'2x\' + চ্নাকবাৰটো লুকুৱাই থওক + চাবস্ক্ৰাইবাৰৰ নিৰ্দেশাবলী লুকুৱাওক + \'Sync to video\' বুটামটো লুকুৱাওক + ট্যাপ কৰি ধৰি থকাৰ গ্ৰেডিয়েণ্টটো লুকুৱাওক + গতি বৃদ্ধি কৰিবলৈ ধৰি থকা অৱস্থাত আঙুলি সৰুৱালে প্লেয়াৰ কন্ট্ৰলৰ গ্ৰেডিয়েণ্টটো লুকুৱাই ৰাখে + সময়সীমা নিৰ্ধাৰিত প্ৰতিক্ৰিয়াবোৰ লুকুৱাওক + মন্তব্য অংশ আৰু live chat-ত ইমোজীৰ সৈতে প্ৰতিক্ৰিয়া জনোৱাৰ সুবিধাটো লুকুৱাই ৰাখে + \'সহায়তা আৰু মতামত\' মেনুটো লুকুৱাওক + ত্ৰি-বিন্দু বুটনটো ব্যৱহাৰ কৰোঁতে \'সহায়তা আৰু মতামত\' মেনুটো লুকুৱাই ৰাখে, যেনে সন্ধানৰ ফলাফলত + হ্যান্ডেলটো লুকুওক + \'You\' টেবত আপোনাৰ চেনেলৰ হ্যান্ডেলটো লুকুৱাই দিয়ে + ভিডিঅ\'ৰ শিৰোনাম লুকুৱাওক + ফুলস্ক্রীন মোডত ভিডিঅ’ৰ শিৰোনামটো লুকুৱাই ৰাখে + ভিডিঅ\'ৰ চাবটাইটেল চাফ কৰক + ভিডিঅ\'ৰ চাবটাইটেলৰ পৰা \'#\', \'Fundraiser\', \'product(s)\', আৰু \'Shop\'ৰ দৰে শব্দসমূহ পৰিষ্কাৰ কৰে + \'Ask Gemini\' বুটামটো লুকুৱাওক + \'Ask about this video\' পেনেলটোৰ তলত থকা \'Ask Gemini\' বুটামটো লুকুৱায় + ভিডিঅ\'ৰ বৰ্ণনা + ভিডিঅ’ৰ বৰ্ণনাৰ কোনবোৰ উপাদান দেখা যাব তাৰ বাবে বাছনি কৰক + \'AI-generated video summary\' অংশটো লুকুৱাওক + Ask বিভাগ লুকুৱাওক + Attributes বিভাগ লুকুৱাওক + Featured places, Games, Music, আৰু People mentioned বিভাগসমূহ লুকুৱায় + চেনেল লিংক সেকচনটো লুকুৱাওক + Chapters বিভাগ লুকুৱাওক + Corrections বিভাগ লুকুৱাওক + \'Course progress\' বিভাগ লুকুৱাওক + Explore বিভাগ লুকুৱাওক + Explore this course আৰু Explore the podcast বিভাগসমূহ লুকুৱায় + \'Explore this course\' বিভাগ লুকুৱাওক + \'Explore the podcast\' বিভাগ লুকুৱাওক + Featured channels অংশটো লুকুৱাই ৰাখক + Featured links সেকচনটো লুকুৱাওক + Featured playlists অংশটো লুকুৱাওক + Featured videos সেকচনটো লুকুৱাওক + Hashtag অংশটো লুকুৱাওক + \'How this was made\' সেকচনটো লুকুৱাওক + Hype পইণ্টবোৰ লুকুৱাওক + Info cards সেকচনটো লুকুৱাওক + \'মূল ধাৰণা\' বিভাগটো লুকুৱাওক + Quizzes সেকচনটো লুকুৱাওক + \'এই ভিডিঅ’টোৰ ভিতৰত সন্ধান কৰক\' অংশটো লুকুৱাওক + Subscribe বুটামত লুকাই ৰাখক + Transcript সেকচনটো লুকুৱাওক + \'Video details\' সেকচনটো লুকুৱাওক + ফিল্টাৰ বাৰ + স্ক্ৰলিং ফিল্টাৰ বাৰটো ক’ত লুকাব সেইটো বাছনি কৰক + চেনেল পেজত লুকুৱাওক + মন্তব্যত লুকাব + ফিডত লুকাব + সম্পৰ্কিত ভিডিঅ’ত লুকাব + সন্ধানৰ ফলাফলত লুকুৱাওক + ৱাচ ഹിষ্টৰীত লুকুৱাওক + চেনেলৰ পৃষ্ঠা + চেনেলৰ পৃষ্ঠাৰ কোনবোৰ উপাদান দেখা যাব তাৰ বাবে বাছনি কৰক + চেনেল টেব ফিল্টাৰ সক্ৰীয় কৰক + চেনেল টেব ফিল্টাৰ + ফিল্টাৰ কৰিবলৈ চেনেল টেবৰ নামবোৰ লিখক, নতুন লাইনৰ দ্বাৰা পৃথক কৰক + Community বুটামত লুকাই ৰাখক + Join বুটামত লুকাই ৰাখক + লিঙ্কৰ প্ৰিভিউ লুকুৱাওক + চেনেলৰ হেডাৰত থকা ক্লিক কৰিব পৰা ৱেব লিংকবোৰ লুকুৱাই ৰাখে + \'কেৱল সদস্যসকলৰ বাবে\' চিপটো লুকুৱাওক + চেনেল ফিল্টাৰ বাৰত \'Members only\' চিপটো লুকুৱায় + মেম্বাৰ শ্বেল্ফটো লুকুৱাওক + চেনেলৰ সদস্যৰ তালিকাখন লুকুৱাই ৰাখে + পোষ্ট শ্বেল্ফটো লুকুওক + ষ্টোৰ বুটামটো লুকুৱাওক + চাবস্ক্ৰাইব বুটামটো লুকুৱাওক + Explore মেনু + এক্সপ্লোৰ মেনুৰ কোনবোৰ উপাদান দেখা যাব বাচনি কৰক + কৰ্ছ মেনু লুকুৱাই ৰাখক + এক্সপ্লোৰ বুটনটো লুকুৱাই ৰাখক + \'Fashion & Beauty\' মেনুটো লুকুৱাই ৰাখক + গেমিং মেনু লুকুৱাই ৰাখক + হাইপ মেনু লুকুৱাই ৰাখক + শিকন মেনু লুকাই থওক + লাইভ মেনু লুকুৱাই ৰাখক + মেম্বাৰশ্বিপ মেনু লুকুৱাই ৰাখক + \'Movies & TV\' মেনু লুকুৱাওক + সংগীত মেনু লুকুৱাওক + সংবাদ মেনু লুকুৱাই ৰাখক + Playables মেনু লুকুৱাওক + পডকাস্ট মেনু লুকুৱাওক + চপিং মেনু লুকুৱাওক + ক্ৰীড়া মেনু লুকুৱাওক + YouTube Premium মেনু লুকুৱাওক + YouTube Studio মেনু লুকুৱাওক + YouTube Music মেনু লুকুৱাওক + YouTube Kids মেনু লুকুৱাওক + YouTube Create মেনু লুকুৱাওক + \'YouTube কেনেকৈ কাম কৰে\' মেনু লুকাই থওক + আইনী আৰু গোপনীয়তাৰ ফুটাৰবোৰ লুকাই থওক + মন্তব্য + মন্তব্যৰ বিভাগত কোনবোৰ উপাদান দেখা যাব সেইটো বাছনি কৰক + মন্তব্য কাৰৌচেল ফিল্টাৰ সক্ৰীয় কৰক + "• কেৰোজেল আইটেমটোৰ সঠিক নামটো প্ৰৱেশ কৰাটো নিশ্চিত কৰক (যেনে, Ask, Hype this video, Listen on YouTube Music) +• কেৰোজেল আইটেমবোৰ লুকুৱালে অনভিপ্রেত কাৰ্যৰ ফল হ'ব পাৰে +• যদি সকলো কেৰোজেল আইটেম লুকুৱাই দিয়া হয়, তেন্তে সমগ্ৰ কমেন্ট অংশটোও লুকুৱাই দিয়া হ'ব" + মন্তব্য কাৰৌচেল ফিল্টাৰ + ফিল্টাৰ কৰিবলৈ মন্তব্য কেৰ’চেলৰ নামবোৰ লিখক, যিবোৰ নতুন লাইনৰ দ্বাৰা পৃথক কৰা থাকিব + AI চেট সাৰাংশ লুকুৱাওক + চেনেলৰ নিৰ্দেশাৱলী লুকুৱাওক + প্ৰেক্ষাপটৰ বাৰ্তাসমূহ লুকুৱাওক + \'মেম্বাৰৰ দ্বাৰা কৰা মন্তব্য\' হেডাৰটো লুকুৱাওক + মন্তব্য বিভাগ লুকুৱাওক + হোম ফিডত কমেণ্ট সেকচনটো লুকুৱাই থোৱক + কমিউনিটি গাইডলাইনসমূহ লুকুৱাওক + \"Create a Short\" বুটামটো লুকুৱাওক + Dislike বুটনটো লুকুৱাই ৰাখক + Emoji বুটামটো লুকুৱাওক + ফিল্টাৰ বাৰৰ বিকল্পসমূহ লুকুৱাওক + \'Top\' আৰু \'Newest\'ৰ বাহিৰে সকলো ফিল্টাৰ বাৰৰ বিকল্পসমূহ লুকুৱায় + উপহাৰৰ এনিমেশ্বন আৰু কাৰ্ডবোৰ লুকুৱাওক + উপহাৰ বুটামটো লুকুৱাওক + তথ্য বুটামটো লুকুৱাওক + লাইভ চেট দানকাৰীৰ বাৰটো লুকুৱাই ৰাখক + লাইভ চেটৰ তললৈ স্ক্ৰোল কৰা বুটামটো লুকুওক + লাইভ চেট টুলটিপসমূহ লুকুৱাওক + চ্যাট ইনপুট বাৰৰ কাষত দেখুওৱা টুলটিপ পপআপবোৰ লুকুৱাই ৰাখে, যেনে কেৱল সাবস্ক্ৰাইবাৰৰ বাবে চ্যাটৰ জাননী + মেনু বুটম লুকুৱাওক + প্ৰতিটো মন্তব্যৰ ৩-ডট মেনু বুটমটো লুকুৱায় + প্ৰিভিউ কমেন্ট লুকুৱাওক + সম্পূৰ্ণ মন্তব্যৰ বিভাগটো খোলাৰ আগতে ভিডিঅ\'ৰ তলত থকা মন্তব্যৰ প্ৰিভিউটো লুকুৱাই ৰাখে + Morphe ৰ দ্বাৰা মন্তব্যটো লুকাই ৰখা হৈছে… + মিনিমেল কমেন্ট বুটনে সক্ৰিয় কৰক + স্লাইডিং কাৰ্ড আৰু ইয়াৰ পেজ চেলেক্টৰক লুকুৱাই ৰাখে, যিবোৰ কমেন্ট বুটামত প্ৰদৰ্শিত হয় + ধন্যবাদ বাটনটো লুকুৱাওক + Timestamp বুটামটো লুকুৱাওক + \'শীৰ্ষ অনুচৰী\' বুটৰটো লুকুৱাওক + অনুবাদ বুটম লুকুৱাওক + \'এতিয়াও কোনো মন্তব্য নাই\' বাৰ্তাটি লুকুৱাওক + হাইলাইট কৰা চাৰ্চ লিংকসমূহ স্যানিটাইজ কৰক + হাইলাইট কৰা চাৰ্চ লিংকসমূহ স্যানিটাইজ কৰে, কেৱল মূল প্লেইন টেক্সটটো ৰাখি + কি-ৱৰ্ড ফিল্টাৰ + ফীড আৰু সন্ধানত ভিডিঅ\'বোৰ লুকুৱাবলৈ নিৰ্দিষ্ট শব্দ বা বাক্যাংশ বাছনি কৰক + কিৱৰ্ডৰ দ্বাৰা মন্তব্যবোৰ লুকুৱাই থওক + কিৱৰ্ডৰ দ্বাৰা হোম ভিডিঅ\'সমূহ লুকুৱাওক + কিৱৰ্ডৰ দ্বাৰা সন্ধানৰ ফলাফলসমূহ লুকুৱাওক + কিৱৰ্ডৰ দ্বাৰা চাবস্ক্ৰিপশ্বন ভিডিঅ\'সমূহ লুকুৱাওক + লুকুৱাব লগা কিৱৰ্ডসমূহ + "লুকাবলৈ কি-ৱৰ্ড আৰু ফ্ৰেজবোৰ লিখক, যিবোৰ নতুন লাইনৰ দ্বাৰা পৃথক কৰা হৈছে + +কি-ৱৰ্ডবোৰ ভিডিঅ’ৰ শিৰোনাম বা চেনেলৰ নামত দেখুৱাব পৰা যিকোনো পাঠ হ'ব পাৰে + +মাজত ডাঙৰ আখৰ থকা শব্দবোৰ সঠিকভাৱে মিল থাকিব লাগিব (যেনে, iPhone, TikTok, LeBlanc)" + "হোম / চাবস্ক্ৰিপচন / চাৰ্চ / মন্তব্যবোৰত কিৱৰ্ড ফ্ৰেজৰ সৈতে মিল থকা সমলবোৰ লুকুৱাবলৈ ফিল্টাৰ কৰা হৈছে + +সীমাবদ্ধতাসমূহ: +• Shorts চেনেলৰ নামৰ দ্বাৰা লুকুৱাব নোৱাৰি +• কিছুমান UI কম্পোনেন্ট লুকুৱাব নোৱাৰিব পাৰে +• কোনো এটা কিৱৰ্ডৰ বাবে চাৰ্চ কৰিলে কোনো ফলাফল নোমাব পাৰে" + সম্পূৰ্ণ শব্দৰ সৈতে মিল খাওক + এটা কীৱৰ্ড/বাক্যক ডাবল-ক’টৰ মাজত ৰাখিলে ভিডিঅ’ৰ শিৰোনাম আৰু চেনেলৰ নামৰ আংশিক মিলবোৰ ৰোধ কৰিব<br><br>উদাহৰণস্বৰূপে,<br><b>\"ai\"</b> এই ভিডিঅ’টো লুকাব: <b>How does AI work?</b><br>কিন্তু এইটো লুকুৱাব নোৱাৰে: <b>What does fair use mean?</b> + কীৱৰ্ডটো ব্যৱহাৰ কৰিব নোৱাৰি: %s + কীৱৰ্ডটো ব্যৱহাৰ কৰিবলৈ ক’ট যোগ কৰক: %s + কীৱৰ্ডটোৰ সংঘাতময় ঘোষণা আছে: %s + কীৱৰ্ডটো অতি চুটি আৰু ইয়াত ক’টৰ প্ৰয়োজন: %s + কিৱৰ্ডটোৱে সকলো ভিডিঅ\' লুকুৱাই পেলাব: %s + "• হোম: %s +• চাবস্ক্ৰিপচন: %s +• সন্ধান: %s +• মন্তব্য: %s +• মুঠ: %s" + ষ্টাটছ + ভিডিঅ’সমূহ লুকুৱাইছে, শেষ ২৪ ঘণ্টা + ভিডিঅ’সমূহ লুকুৱাইছে, সকলো সময়ৰ + ৰূপনৈ যোৱা ২৪ ঘণ্টাৰ কাউণ্টাৰটো ৰিছেট কৰিবনে? + সকলো সময়ৰ কাউণ্টাৰটো ৰিছেট কৰিবনে? + AI চেনেল ফিল্টাৰ + AiSList কমিউনিটি তালিকাৰ দ্বাৰা AI-ত উৎপন্ন বিষয়বস্তু হিচাপে চিহ্নিত কৰা চেনেলবোৰৰ ভিডিঅ\'বোৰ লুকুৱাওক + ব্লকLIST: উচ্চ আত্মবিশ্বাস + ৱাৰ্নLIST: মধ্যম আত্মবিশ্বাস + Home-ত ভিডিঅ\'বোৰ লুকুৱাওক + সন্ধানত ভিডিঅ\'বোৰ লুকুৱাওক + "• হোম: %s +• সন্ধান: %s +• মুঠত: %s" + ব্লক লিষ্ট চেনেলসমূহ + ৱাৰ্ন লিষ্ট চেনেলসমূহ + চেনেলসমূহ লোড কৰিবলৈ এটা ফিল্টাৰ সক্ৰিয় কৰক + চেনেলসমূহ লোড হৈছে… + "AiSList ৰ দ্বাৰা চিহ্নিত চেনেলৰ ভিডিঅ’সমূহ লুকাবৰ বাবে হোম আৰু সন্ধানৰ ফলাফলসমূহ ফিল্টাৰ কৰা হৈছে + +সীমাবদ্ধতা: +• ফিল্টাৰ কৰা AI Shorts ৰ ঠাইত এটা খালী ঠাই দেখা যাব পাৰে +• কিছুমান AI ভিডিঅ’ তথাপিও দেখা যাব পাৰে" + "AiSList হৈছে AI-দ্বাৰা সৃষ্টি কৰা বিষয়বস্তু প্ৰকাশ কৰা YouTube চেনেলসমূহৰ এটা মুকলি উৎসৰ তালিকা, যিটো স্বেচ্ছাসেৱকৰ দ্বাৰা পৰিচালিত। ব্যৱহাৰকাৰীৰ দ্বাৰা চেনেলসমূহৰ প্ৰতিবেদন কৰা হয় আৰু যোগ কৰাৰ আগতে সেইবোৰ পৰীক্ষা কৰা হয় + +তালিকাতো GitHub ৰ পৰা ডাউনলোড কৰা হয় আৰু আপোনাৰ ডিভাইচত ভিডিঅ’সমূহ ফিল্টাৰ কৰা হয়" + AI চেনেলৰ ৰিপৰ্ট কৰক + ভিডিঅ’ মেনুৰ সৈতে ৰিপৰ্ট এণ্ট্ৰি যোগ কৰক + ভিডিঅ’ মেনুৰ সৈতে এটি এণ্ট্ৰি যোগ কৰক যাৰ দ্বাৰা ভিডিঅ’টোৰ চেনেলটো AiSList-লৈ ৰিপৰ্ট কৰা হয় + দাখিল কৰোঁতাৰ নাম + ৰাজহুৱা AiSList লিডাৰবৰ্ডত আপোনাৰ ৰিপৰ্টৰ কাষত দেখুওৱা নাম। এইটো খালী থাকিলে ৰিপৰ্টিং উপলব্ধ নহয় + AI চেনেল ৰিপৰ্ট কৰক + "এই ভিডিঅ'টোক প্ৰমাণ হিচাপে ব্যৱহাৰ কৰি %s AiSList-লৈ পঠাবনে? + +কেৱল সেইবোৰ চেনেলৰ ৰিপৰ্ট কৰক যিবোৰে: +• মূল সমল হিচাপে AI কণ্ঠস্বৰ, দৃশ্য, স্ক্ৰিপ্ট বা সংগীত ব্যৱহাৰ কৰে +• AI সঁজুলিৰে আৰু অতি কম মানৱীয় পৰিশ্ৰমেৰে ব্যাপকভাৱে সমল উৎপাদন কৰে + +যিবোৰ চেনেলে কেৱল AI আলোচনা কৰে, বা তেওঁলোকৰ ভিডিঅ'ৰ কিছু অংশত AI ব্যৱহাৰ কৰে, সেইবোৰৰ ৰিপৰ্ট নকৰিব + +ৰিপৰ্টসমূহ ৰাজহুৱা আৰু এটা চেনেল তালিকাভুক্ত কৰাৰ আগতে পৰীক্ষা কৰা হয়। GitHub-ত অনুৰোধ জনাইহে ইয়াতকৈ আঁতৰাব পৰা যায়" + এই চেনেলটো ইতিমধ্যে AiSList ব্লকলিষ্টত আছে + এই চেনেলটো ইতিমধ্যে AiSList ৱাৰ্ণলিষ্টত আছে + ৰিপৰ্ট কৰক + চেনেলৰ ৰিপৰ্ট কৰিবলৈ AI চেনেল ফিল্টাৰ ছেটিংছত এজন জমা কৰোঁতাৰ নাম নিৰ্ধাৰণ কৰক + চেনেলটো বিচৰা হৈছে… + এই ভিডিঅ\'টোৰ চেনেলটো বিচাৰি পোৱা নগ\'ল + চেনেলটোৰ ৰিপৰ্ট কৰা হৈছে আৰু পৰীক্ষাৰ বাবে অপেক্ষা কৰিছে + AiSList-এ ৰিপৰ্টটো সংৰক্ষণ কৰিব নোৱাৰিলে, পিছত পুনৰ চেষ্টা কৰক + অত্যধিক ৰিপৰ্ট পঠোৱা হৈছে, পিছত পুনৰ চেষ্টা কৰক + AiSList-এ ৰিপৰ্টটো নাকৰ কৰিলে + ৰিপৰ্ট বিফল হৈছে, আপোনাৰ সংযোগটো পৰীক্ষা কৰক + চেনেল বিজ্ঞাপন হোৱাইটলিষ্ট + হোৱাইটলিষ্ট কৰা চেনেলৰ ভিডিঅ\'বোৰ বিজ্ঞাপনৰ সৈতে চলে, যাতে সিহঁতৰ সৃষ্টিকৰ্তাসকলে পৰিশোধ পায়। অন্য সকলো চেনেলত বিজ্ঞাপনসমূহ বাধা দি ৰখা হয় + বিজ্ঞাপনৰ হোৱাইটলিষ্টত চেনেলটো যোগ কৰক + বিজ্ঞাপনৰ হোৱাইটলিষ্টৰ পৰা চেনেলটো আঁতৰ কৰক + ক্ৰিয়েটৰ ষ্টোৰ শ্বেলফ লুকুৱাওক + এণ্ড স্ক্ৰীন ষ্টোৰ বেনাৰ লুকুৱাওক + সাধাৰণ বিজ্ঞাপনসমূহ লুকুৱাওক + মাৰ্চেনডাইজ বেনাৰসমূহ লুকুৱাওক + পেইড প্ৰম’চন লেবেলসমূহ লুকুৱাওক + প্লেয়াৰ পপ-আপ বিজ্ঞাপনসমূহ লুকুৱাওক + স্ব-প্ৰায়োজিত কাৰ্ডসমূহ লুকুৱাওক + চপিং লিংকসমূহ লুকুৱাওক + ভিডিঅ\' বিজ্ঞাপনসমূহ লুকুৱাওক + YouTube Premium প্ৰচাৰসমূহ লুকাই ৰাখক + লোড হৈ আছে… + বৰ্তমানৰ চেনেল: + হোৱাইটলিষ্টত যোগ কৰক + আঁতৰ কৰক + হোৱাইটলিষ্ট কৰা চেনেলসমূহ + কোনো চেনেল হোৱাইটলিষ্ট কৰা হোৱা নাই + চেনেলটো হোৱাইটলিষ্টত যোগ কৰা হৈছে + চেনেলটো হোৱাইটলিষ্টৰ পৰা আঁতৰ কৰা হৈছে + চেনেলটোৰ ভিতৰত সন্ধান কৰক + চেনেলৰ পৃষ্ঠাৰ ওপৰত কেৱল সেই চেনেলটোতহে অনুসন্ধান কৰিবলৈ দ্বিতীয়টো অনুসন্ধান বুটাম যোগ কৰে + চেনেল সন্ধান কৰক + চেনেল সন্ধানৰ ফলাফলসমূহ + এই চেনেলত একো পোৱা নগ\'ল + এই চেনেলটোত সন্ধান কৰিব নোৱাৰিল + ভিডিঅ\' + পোষ্ট + এই চেনেলৰ কোনো পষ্ট মিলা নাই + প্ৰাসংগিকতা + সৰ্বাধিক দৰ্শিত + সৰ্বনিম্ন দৰ্শিত + নতুনতম + পুৰণি + আটাইতকৈ চুটি + আটাইতকৈ দীঘল + লিংকটো ক্লিপবৰ্ডলৈ কপি কৰা হ\'ল + টাইমষ্টেম্পৰ সৈতে লিংকটো ক্লিপবৰ্ডলৈ কপি কৰা হ\'ল + Copy video link বুটামত দেখুৱাওক + ভিডিঅ\' লিংকটো কপি কৰিবলৈ টেপ কৰক, টাইমষ্টেম্পৰ সৈতে কপি কৰিবলৈ টেপ কৰি ধৰি ৰাখক + ডিফল্টভাৱে টাইমষ্টাম্প কপি কৰক + \'Copy video link\' বুটামৰ টেপ আৰু টেপ-এণ্ড-হোল্ড কাৰ্যবোৰ সলনি কৰে + ভিডিঅ\'ৰ শিৰোনাম কপি কৰক + ভিডিঅ\'ৰ শিৰোনামটো কপি কৰিবলৈ ইয়াৰ ওপৰত টিপি ধৰক + ভিডিঅ\'ৰ শিৰোনামটো ক্লিপবৰ্ডলৈ কপি কৰা হৈছে + মন্তব্যসমূহ কপি কৰক + মন্তব্য এটা কপি কৰিবলৈ ইয়াৰ ওপৰত টিপি ধৰক + মন্তব্যটো ক্লিপবৰ্ডলৈ কপি কৰা হৈছে + ভিডিঅ’ মেনু খুলিবলৈ টেপ কৰি ধৰি ৰাখক + ফিড ভিডিঅ’ৰ যিকোনো ঠাইত টেপ কৰি ধৰি ৰাখিলে ইয়াৰ মেনু খোল + থাম্বনেইলত \'পাছত চাম\' বুটামটো দেখুৱাওক + ফিড ভিডিঅ’ৰ থাম্বনেইলত এটা \'পাছত চাম\' বুটাম দেখুওৱা হয়। ইয়াক টেপ কৰি ধৰি ৰাখিলে \'প্লেলিষ্টত সংৰক্ষণ কৰক\' খোল + পোষ্টৰ ছবিসমূহ ডাউনলোড কৰক + ছবি বা GIF থকা পোষ্টবোৰৰ Share বুটামত এটা download বুটাম দেখুৱা হয়, আৰু তেওঁলোকৰ মেনুত এটা download আইটেম যোগ কৰা হয় + ছবিসমূহ ডাউনলোড কৰক + ছবিখন ডাউনলোড কৰক + %s খন ছবি ডাউনলোড কৰা হৈছে… + ছবি ডাউনলোড কৰা হৈছে… + %1$s খন ছবি %2$s ত সংৰক্ষণ কৰা হৈছে + ছবিখন %s ত সংৰক্ষণ কৰা হৈছে + ছবিখন ডাউনলোড কৰিব নোৱাৰিল + এই পোষ্টটোত কোনো ছবি পোৱা নগ\'ল + ফ্লাইআউট মেনুলৈ Ask আইটেম যোগ কৰক + ফিড ফ্লাইআউট মেনুলৈ এটা ভিডিঅ\' সামৰিপণ আইটেম যোগ কৰে + "Ask ব্যৱহাৰ কৰিবলৈ প্ৰথমে Ask উপলব্ধ থকা এখন দেশৰ চাইন-ইন কৰা একাউণ্টৰ প্ৰয়োজন। + +Ask বুটাম থকা এটা ভিডিঅ' এবাৰ খোলাৰ পিছত, এই ফিচাৰটো সকলো ভিডিঅ'ত কাম কৰিব, যদিও বৰ্তমানৰ দেশত Ask ফিচাৰটো উপলব্ধ নাই।" + ভিডিঅ\'টোৰ সাৰাংশ লিখক + এই ভিডিঅ\'টোৰ বাবে Ask খুলিব পৰা নগ\'ল + আপোনাৰ একাউণ্ট বা বৰ্তমানৰ দেশৰ বাবে Ask উপলব্ধ নহয় + থাম্বনেইলত ভিউ গণনা দেখুওক + ফীড ভিডিঅ’ৰ ভিউ গণনাটো শীৰ্ষকৰ তলত থকাৰ পৰিৱৰ্তে সিহঁতৰ থাম্বনেইলৰ তলৰ বাওঁফালে দেখুওৱা হয়। কিছুমান ভিডিঅ’, যেনে যে শ্বেল্ফৰ ভিতৰত থকা, তাত ভিউ গণনা দেখা নাযাবও পাৰে + ফিড থাম্বনেইলৰ লাইকৰ সংখ্যা দেখুৱাওক + ফিড থাম্বনেইলৰ লাইকৰ শতাংশ দেখুৱাওক + মন্তব্যৰ বাবে সন্ধান কৰক + কেৱল লোড হোৱা মন্তব্যবোৰৰ ভিতৰত খোজোৱা শব্দসমূহ থকা মন্তব্যবোৰ দেখুৱাবলৈ মন্তব্যৰ ছেৰ্চ বুটামত টিপক। কোনো এটা বাক্য খোজোৱাৰ বাবে শব্দবোৰক উদ্ধাৰ চিনৰ (quotes) ভিতৰত ৰাখক + মন্তব্য খোজোৱা + খোজোৱাৰ বাবে শব্দবোৰ + খোজোৱা + মচি পেলাওক + ছাঁটাইৰ ক্ৰম সলনি কৰক বা মন্তব্যবোৰ বিচাৰিবলৈ পুনৰ খোলক + চৰ্চা খালী কৰা হৈছে। সকলো মন্তব্য দেখুৱাবলৈ সৰ্ট অৰ্ডাৰ সলনি কৰক বা মন্তব্যসমূহ পুনৰ খোলক + \'টিভিত ছাইন ইন কৰক\' পপ-আপটো নিষ্ক্ৰিয় কৰক + \'আপোনাৰ ডিভাইচসমূহ সংযোগ কৰক\' পপ-আপটো নিষ্ক্ৰীয় কৰক + ডাবল-ট্যাপ চেপটাৰ স্কিপ নিষ্ক্ৰীয় কৰক + দুটি আঙুলিৰে ডাবল-টেপ গেচ্চাৰটো নিষ্ক্ৰীয় কৰে যিয়ে অধ্যায়সমূহ স্কিপ কৰে, যিটো ভুলবশতঃ এটা আঙুলিৰ দ্বাৰা ট্রিগাৰ হ\'ব পাৰে + তৃতীয় পক্ষৰ ডাউনলোড এপৰ সহায়ত ভিডিঅ\', পডকাস্ট বা গান ডাউনলোড কৰক + প্লেয়াৰ অভাৰলে ডাউনলোড বুটাম দেখুওক + ভিডিঅ’ প্লেয়াৰত এটা বুটাম দেখুৱায় যিয়ে আপোনাৰ এক্সটার্ণেল ডাউনলোডৰ খুলি দিয়ে + টেপ কৰিলে দ্ৰুত ডাউনলোড + বেকগ্ৰাউণ্ডত ডাউনলোড কৰিবলৈ প্ৰম্পট কৰে। ডাউনলোডার খুলিবলৈ টেপ কৰি ধৰি ৰাখক। YTDLnis আৰু YTindir-ৰ সৈতে কাম কৰে + ডাউনলোডসমূহ চাইছে বুলি চিহ্নিত কৰক + বাহ্যিক ডাউনলোডারৰ দ্বাৰা ডাউনলোড কৰা ভিডিঅ\'সমূহ আপোনাৰ YouTube ৱাচ হিষ্টৰীত যোগ কৰা হয় + ফুল HD (1080p) + মধ্যম (192 kbps) + দ্ৰুত ডাউনলোড + ভিডিঅ\' + অডিঅ\' + সকলো বিকল্প + Shorts মেনুত ডাউনলোড দেখুৱাওক + Shorts প্লেয়াৰলৈ এটা বাহ্যিক ডাউনলোডৰ ফ্লাইআউট মেনু আইটেম যোগ কৰে + ডাউনলোড + ভইচ অভাৰ অনুবাদ + ডিভাইচৰ টেক্সট-টু-স্পীচ ব্যৱহাৰ কৰি ভিডিঅ\'ৰ কেপচনসমূহ উচ্চস্বৰে পঢ়ক, যিবোৰ প্লেবেকৰ সৈতে সংsynchronize কৰা হৈছে + ভিডিঅ\' প্লেয়াৰত এটা অভাৰলে বটൺ দেখুৱায়। অনুবাদটো সলনি কৰিবলৈ টেপ কৰক, দ্ৰুত ছেটিংছৰ বাবে টেপ কৰি ধৰি ৰাখক + ভাষা + অনুবাদ প্ৰদানকাৰী + মূল অডিঅ\' ভলিউম + অনুবাদটো চলি থকাৰ সময়ত মূল YouTube অডিঅ\'ৰ ভলিউম + অনুবাদৰ শব্দৰ মাত্ৰা + ডাব কৰা অনুবাদৰ কণ্ঠৰ শব্দৰ মাত্ৰা + সৰ্বোচ্চ কথা কোৱাৰ দৰ + উপশিৰোনামৰ খালী ঠাইত কথাখিনি খাপ খুৱাবলৈ ব্যৱহাৰ কৰা সৰ্বোচ্চ গতি গুণক + "বিনামূলীয়া অনুবাদ API, কোনো API কীৰ প্ৰয়োজন নাই। বিনামূলীয়া স্তৰত প্ৰতিদিনত ৫,০০০ চৰিত্ৰলৈকে, অথবা ইমেইল ঠিকনাৰ সৈতে প্ৰতিদিনত ৫০,০০০ চৰিত্ৰলৈকে অনুবাদ কৰিব পাৰিব। + +MyMemory.translated.net খুলিবলৈ ইয়াত টিপক" + MyMemory ইমেইল + ঐচ্ছিক। দৈনিক সীমা ৫,০০০ৰ পৰা ৫০,০০০ চৰিত্ৰলৈ বৃদ্ধি কৰিবলৈ আপোনাৰ MyMemory ইমেইল ঠিকনাটো লিখক। বেনামী সীমা ব্যৱহাৰ কৰিবলৈ খালী ৰাখক + "OpenRouter-ৰ জৰিয়তে পেইড AI অনুবাদ। এবাৰ টপ-আপ কৰক আৰু হাজাৰ হাজাৰ ভিডিঅ' অনুবাদ কৰক। Mistral Nemo হৈছে আটাইতকৈ সস্তা বিকল্প, Gemma 4-এ সৰ্বোত্তম মানদণ্ড প্ৰদান কৰে। + +OpenRouter.ai খুলিবলৈ ইয়াত টিপক" + OpenRouter API কী + আপোনাৰ OpenRouter API কী। OpenRouter.ai > Workspace > API Keys ত এটা কী সৃষ্টি কৰক + OpenRouter মডেল + কষ্টম + OpenRouter কনফিগাৰ কৰা হোৱা নাই + এই প্ৰোভাইডাৰটো বাছনি কৰাৰ আগতে ছেটিংছত আপোনাৰ OpenRouter API কীটো লিখক + বিনামূলীয়া + প্ৰতি ১০০ ভিডিঅ\' ঘণ্টাৰ বাবে %s + কণ্ঠ + চিষ্টেম + বহুভাষী পুৰুষ + বহুভাষী মহিলা + পুৰুষ + মহিলা + এয়া মৰ্ফিং সময়! + অনুবাদ উপলব্ধ নহয় + "অনুবাদ সেৱাই HTTP ত্ৰুটি %s প্ৰদান কৰিছে। আপোনাৰ IP ঠিকনটো সম্ভৱত ২৪ ঘণ্টাৰ বাবে ৰেট-লিমিটেড কৰা হৈছে। + +অনুবাদ ব্যৱহাৰ কৰি থাকিবলৈ, ছেটিংছত আন এখন অনুবাদ প্ৰদানকাৰী বাচনি কৰক অথবা VPN ব্যৱহাৰ কৰক" + পুনৰ দেখুৱাব নালাগে + "আপোনাৰ ছেটিংছত এটা OpenRouter API কী আছে। + +আপোনাৰ API কী কেতিয়াও শ্বেয়াৰ নকৰিব। ইয়াৰ এক্সেছ থকা যিকোনো ব্যক্তিয়ে আপোনাৰ OpenRouter ক্ৰেডিট ব্যৱহাৰ কৰিব পাৰিব" + OpenRouter ক্ৰেডিট সীমা সম্পূৰ্ণ হৈছে + আপোনাৰ OpenRouter ক্ৰেডিট সীমা সম্পূৰ্ণ হৈছে। আপোনাৰ বেলেঞ্চ টপ-আপ কৰক অথবা OpenRouter.ai ত আপোনাৰ কী খৰচৰ সীমা বৃদ্ধি কৰক + OpenRouter.ai খোলক + OpenRouter অনুবাদ বিফল হৈছে (HTTP %s) + MyMemory দৈনিক সীমা সম্পূৰ্ণ হৈছে + পিছৰটো %s ত উপলব্ধ হ\'ব + আপুনি আজিৰ বিনামূলীয়া MyMemory অনুবাদৰ কোটা (৫,০০০ আখৰ) ব্যৱহাৰ কৰিছে। দৈনিক সীমা ৫০,০০০ আখৰলৈ বৃদ্ধি কৰিবলৈ ছেটিংছত এটা MyMemory ইমেইল যোগ কৰক, অথবা অন্য এটা অনুবাদ সেৱালৈ পৰিৱৰ্তন কৰক + আপুনি আজিৰ MyMemory অনুবাদৰ কোটা (৫০,০০০ আখৰ) ব্যৱহাৰ কৰিছে। অন্য এটা অনুবাদ সেৱালৈ পৰিৱৰ্তন কৰক অথবা কাইলৈ পুনৰ চেষ্টা কৰক + MyMemory অনুবাদ বিফল হৈছে (স্থিতি %s) + লুপ ভিডিঅ\' সক্ৰিয় কৰক + লুপ ভিডিঅ\' মনত নাৰাখিব + পিছৰ ভিডিঅ\'সমূহৰ বাবে লুপ ভিডিঅ\'ৰ বৰ্তমানৰ অৱস্থা সংৰক্ষণ হোৱাত বাধা দিয়ক + লুপ ভিডিঅ\' বুটামটো দেখুৱাওক + লুপ ভিডিঅ\' সক্ৰিয় হৈ আছে + লুপ ভিডিয়ো বন্ধ হৈছে + লুপ ৰেঞ্জ + আৰম্ভ + শেষ + [hh:]mm:ss + [hh:]mm:ss (খালী = ভিডিঅ\'ৰ শেষ) + নিৰ্ধাৰণ কৰক + মচি পেলাওক + %1$s - %2$s লুপিং হৈছে + অবৈধ সময়ৰ সীমা + Picture-in-Picture বুটনটো দেখুৱাওক + Picture-in-Picture মেনুটো দেখুৱাওক + Picture-in-Picture লৈ পৰিৱৰ্তন কৰক + Play all বুটামটো দেখুৱাওক + "চেনেলৰ সকলো ভিডিঅ'ৰ এখন প্লেলিষ্ট সৃষ্টি কৰিবলৈ টেপ কৰক, বাতিল কৰিবলৈ টেপ কৰি ধৰি ৰাখক + +তথ্য: লাইভ ষ্ট্ৰিমৰ ক্ষেত্ৰত কাম নকৰিব পাৰে" + প্লেলিষ্ট জেনেৰেট মুড + সকলো বিষয়বস্তু (সময় অনুসৰি সৰ্ত কৰক, আৰোহী) + সকলো বিষয়বস্তু (সময় অনুসৰি সৰ্ত কৰক) + সকলো বিষয়বস্তু (জনপ্রিয়তা অনুসৰি সৰ্ট কৰক) + কেৱল ভিডিঅ’ (সময় অনুসৰি সৰ্ট কৰক) + কেৱল ভিডিঅ’ (জনপ্রিয়তা অনুসৰি সৰ্ট কৰক) + কেৱল Shorts (সময় অনুসৰি সৰ্ট কৰক) + কেৱল Shorts (জনপ্রিয়তা অনুসৰি সৰ্ট কৰক) + কেৱল ষ্ট্ৰীমিং ভিডিঅ’ (সময় অনুসৰি সৰ্ট কৰক) + কেৱল ষ্ট্ৰীমিং ভিডিঅ’ (জনপ্রিয়তা অনুসৰি সৰ্ট কৰক) + কেৱল সকলো সদস্যৰ বাবে বিষয়বস্তু + কেৱল সদস্যৰ বাবে ভিডিঅ’ + কেৱল সদস্যৰ বাবে Shorts + কেৱল সদস্যৰ বাবে লাইভ ষ্ট্ৰীম + চেনেল ID অমিল হোৱাৰ বাবে প্লেলিষ্ট সৃষ্টি কৰিব পৰা নাই + ৰিলোড ভিডিঅ\' বুটামটো দেখুৱাওক + ভিডিঅ\' ID উপলব্ধ নহয়। অনুগ্ৰহ কৰি হাতেৰে ভিডিঅ\'টো পুনৰ লোড কৰক + Watch later-ত সংৰক্ষণ কৰক বুটামটো দেখুৱাওক + Watch later-ত সংৰক্ষণ কৰিবলৈ ট্যাপ কৰক, কিউ বিকল্পবোৰৰ বাবে ট্যাপ কৰি ধৰি ৰাখক + ভিডিঅ’টো সফলভাৱে \'পাছত চাম\' প্লেলিষ্টত সংৰক্ষণ কৰা হৈছে + এই ভিডিঅ’টো ইতিমধ্যে \'পাছত চাম\' প্লেলিষ্টত সংৰক্ষণ কৰা হৈছে + এই ভিডিঅ’টো \'পাছত চাম\' প্লেলিষ্টত সংৰক্ষণ কৰিব পৰা নাই + Watch later-ত সংৰক্ষণ কৰক + প্লেয়াৰ চেটিংছ ফ্লাইআউটত \'বেছি পাছত চাম\'লৈ সংৰক্ষণ কৰক বুটামটো দেখুৱাওক + ভিডিঅ\' চেটিংছ ফ্লাইআউটৰ পৰা বৰ্তমান চাই থকা ভিডিঅ\'টো \'বেছি পাছত চাম\'ত সংৰক্ষণ কৰিবলৈ অনুমতি দিয়ে + বাৰুৰ ভিডিঅ’বোৰত Watch later-ত সংৰক্ষণ কৰক বুটামটো দেখুৱাওক + ফ্লাইআউট মেনুৰ পৰা শিশুৰ সৈতে সম্পৰ্কিত ভিডিঅ\'সমূহ \'বেছি পাছত চাম\'ত সংৰক্ষণ কৰিবলৈ অনুমতি দিয়ে + শ্বৰ্টছ ফ্লাইআউটত \'বেছি পাছত চাম\'লৈ সংৰক্ষণ কৰক বুটামটো দেখুৱাওক + ফ্লাইআউট মেনুৰ পৰা শ্বৰ্টছসমূহ \'বেছি পাছত চাম\'ত সংৰক্ষণ কৰিবলৈ অনুমতি দিয়ে + Watch later-ক প্লেলিষ্টলৈ ৰিডাইৰেক্ট কৰক + পিছৰত চাম + Morphe কিউত সংযোগ কৰক + ছেছনৰ মাজত কিউ ৰাখক + এপটো পুনৰ আৰম্ভ কৰাৰ পিছতো কিউ ৰখা হয় + সংৰক্ষণ আৰু কিউ কাৰ্যসমূহৰ স্থান সলনি কৰক + কিউ বিকল্পবোৰৰ বাবে ট্যাপ কৰক, Watch later-ত সংৰক্ষণ কৰিবলৈ ট্যাপ কৰি ধৰি ৰাখক + কিউত পৰৱৰ্তীটো বজাওক প্ৰকাৰ + ডিফল্ট + নেটিভ কিউ + Morphe কিউ + Morphe কিউ ক্লায়েন্ট সাইড + ফীড ফ্লাইআউট কিউ মেনু আইটেম যোগ কৰক + এটা অতিৰিক্ত ফ্লাইআউট আইটেম যোগ কৰে। যদি \"কিউত যোগ কৰক\" দেখুৱাই নাথাকে তেন্তে ইয়াক সক্ৰিয় কৰক + কিউ + কিউ (%s) + সকলোবোৰ মুছক + কিউত একো নাই + যিকোনো ভিডিঅ\'ত কিউৰ পৰা \'Play next in queue\' ব্যৱহাৰ কৰক + এতিয়া চলি আছে + কিউত পৰৱৰ্তীটো প্লে কৰক + কিউৰ শেষত বজাওক + পুনৰ বিন্যাস কৰক + আপুৱাওক + কিউত যোগ কৰা হৈছে + এই ভিডিঅ\'টো কিউট যোগদান কৰিব নোৱাৰিল + প্লেলিষ্টবোৰ কিউট যোগদান কৰিব নোৱাৰি + কিউটো সম্পূৰ্ণ হৈছে + ভিডিঅ’টো আৰম্ভ কৰিব নোৱাৰিল + ফ্লাইআউট মেনুত বিজ্ঞাপন হোৱাইটলিষ্ট + এটা ফ্লাইআউট আইটেম যোগ কৰে যাৰ দ্বাৰা সেই ভিডিঅ’ৰ চেনেলত বিজ্ঞাপনবোৰ চলিব পাৰে + এই ফিচাৰটোৰ বাবে PotHelper ব্যৱহাৰ কৰিবলগীয়া হ\'ব পাৰে অথবা \"Spoof video streams\" ক TV বা Android VR ছাইন-ইনলৈ সলনি কৰিবলগীয়া হ\'ব পাৰে + ফ্লাইআউট মেনুত গতি হোৱাইটলিষ্ট + এটা ফ্লাইআউট আইটেম যোগ কৰে যাৰ দ্বাৰা সেই ভিডিঅ’ৰ চেনেলটো সদায় ১.০x গতিত চলে + লগ ইন কৰা নাই + কোনো প্লেলিষ্ট ID নাই + কিউ খালী আছে + কোনো ভিডিঅ\' ID নাই + বিফল: %s + কিউত যোগ কৰিবলৈ বিফল + কিউ সৃষ্টি কৰিবলৈ বিফল + কিউৰ পৰা আঁতৰ কৰিবলৈ বিফল + কিউ সংৰক্ষণ কৰাত ব্যৰ্থ হৈছে + কিউত যোগ কৰা হৈছে + কিউ সৃষ্টি কৰা হৈছে + কিউৰ পৰা আঁতৰ কৰা হৈছে + কিউটো %s ত সংৰক্ষণ কৰা হৈছে + Morphe কিউ + কিউত যোগ কৰক + কিউত যোগ কৰক আৰু খোলক + কিউত যোগ কৰক আৰু প্লে কৰক + যোগ কৰক আৰু ভিডিঅ\'টো ৰিলোড কৰক + কিউৰ পৰা আঁতৰ কৰক + তৰাব আৰু কিউ খোলক + তৰাব আৰু ভিডিঅ\'টো পুনৰ লোড কৰক + কিউ খোলক + কিউটো প্লেলিষ্টত সংৰক্ষণ কৰক + প্লেলিষ্ট সংৰক্ষণ কৰা হৈছে + %s ত সংৰক্ষণ কৰা হৈছে + কিউ মুছাব + কিউ মুছাব পৰা হৈছে + কিউ + কিউৰ শেষৰটো বজাওক + কিউৰ পিছৰটো বজাওক + YouTube-এ কিউটো প্ৰদৰ্শন কৰিব নোৱাৰিলে। + এই ভিডিঅ’টোৰ বাবে কিউটো সাজু নহয়। ভিডিঅ’ প্লেয়াৰটো খোলক আৰু পুনৰ চেষ্টা কৰক। + মিকটো কিউত যোগ কৰিব নে মিকটো চাব? + চাওক + কিউত যোগ কৰক + মিক লোড হৈছে… + লাইভ এভাটাৰৰ পৰা চেনেলটো খোলক + চেনেলৰ এভাটাৰত টেপ কৰিলে লাইভ ষ্ট্ৰিমটো খোলাৰ পৰা বাধা দিয়ে + প্ৰিসাইজ ছীকিং জেষ্টাৰ নিষ্ক্ৰিয় কৰক + চীকবাৰখন ওপৰলৈ টানিলে সক্ৰীয় হোৱা প্ৰেচাইচ চীকিং মোডটো নিষ্ক্ৰীয় কৰে + লাইভ ষ্ট্ৰীম DVR সক্ৰিয় কৰক + সকলো লাইভ ষ্ট্ৰীমত চিচিং কৰিবলৈ অনুমতি দিয়ে, আনকি সেইবোৰতো যিবোৰত সাধাৰণতে এইটো সীমাবদ্ধ কৰা থাকে + Seekbar থাম্বনেইল সক্ৰিয় কৰক + Seekbar-টো টানি থকাৰ সময়ত এটা ভিডিঅ’ থাম্বনেইল দেখুৱায় + আটাইতকৈ বেছিবাৰ চালিত + "'লাইভ ষ্ট্ৰীম DVR সক্ৰিয় কৰক' উপলব্ধ নহ'ব পাৰে + +এয়া ঠিক কৰিবলৈ, 'ভিডিঅ' ষ্ট্ৰীম স্পুফ' অন কৰক আৰু 'ডিফল্ট ক্লায়েন্ট'টো 'TV Simply' বা 'visionOS' লৈ সলনি কৰক" + লাইভ ষ্ট্ৰিম DVR ৰ সময়সীমা বৃদ্ধি কৰক + আপোনাক ৭ দিন আগলৈকে লাইভ প্ৰসাৰণ ৰিৱাইণ্ড কৰিবলৈ অনুমতি দিয়ে + "'লাইভ ষ্ট্ৰিম DVR ৰ সময়সীমা বৃদ্ধি কৰক' উপলব্ধ নহ'ব পাৰে + +এয়া ঠিক কৰিবলৈ, 'Spoof video streams' অন কৰক আৰু 'Default client' টি 'TV Simply' বা 'visionOS' লৈ সলনি কৰক" + ছ্লাইড টু ছিক সক্ৰিয় কৰক + ভিডিঅ\' প্লেয়াৰটো চেপ কৰি ধৰিলে ২x স্পিডত প্লে কৰাৰ পৰিৱৰ্তে চীক কৰিবলৈ স্লাইড কৰা সুবিধাটো সক্ৰীয় কৰে + ট্যাপ কৰি বিচাৰক সক্ৰিয় কৰক + টানি নকৰাকৈ সেই স্থানলৈ যাবলৈ কেৱল seekbar-ত টেপ কৰক + বাওঁফালৰ সোৱাইপ জেসচাৰ + সোঁফালৰ সোৱাইপ জেসচাৰ + ওপৰৰ সোৱাইপ জেসচাৰ + বাওঁ/সোঁ জ’নৰ প্ৰস্থ + বাওঁ আৰু সোঁ সোৱাইপ জেসচাৰৰ বাবে সংৰক্ষিত প্ৰতিটো স্ক্ৰীনৰ কাষৰ শতাংশ (৫-৫০) + উজ্জ্বলতা + ভলিউম + নেটিভ + বন্ধ + লক থাকাৰ সময়ত সোৱাইপসমূহ অৱজ্ঞা কৰক + প্লেয়াৰটো \'স্ক্ৰীন লক\'ৰ দ্বাৰা লক হৈ থকাৰ সময়ত সোৱাইপ জেসচাৰসমূহ অৱজ্ঞা কৰা হয় + প্ৰেছ কৰি সোয়াইপ কৰা জেচচাৰ সক্ৰিয় কৰক + সুইপ জেসচাৰটো চিনি পোৱাৰ আগতে টিপি ধৰিবলৈ প্ৰয়োজন + হেপটিক ফিডবেক সক্ৰিয় কৰক + উজ্জ্বলতা সংৰক্ষণ আৰু পুনৰুদ্ধাৰ কৰক + ফুলস্ক্ৰীন মোডত প্ৰৱেশ কৰোঁতে / ওলাই আহোঁতে উজ্জ্বলতাৰ স্তৰটো মনত ৰাখে + অটো-ব্ৰাইটনেছ জেচচাৰ সক্ৰিয় কৰক + উজ্জ্বলতাক ইয়াৰ সৰ্বনিম্ন মানলৈ সোৱাই নিলে চিষ্টেমৰ অটো-ব্ৰাইটনেছ সক্ৰিয় হয় + অটো + সোয়াইপ অভাৰলে টাইমআউট + অভাৰলেটো কিমান মিলিছেকণ্ডৰ বাবে দৃশ্যমান হ\'ব + সোয়াইপ অভাৰলেৰ পটভূমিৰ অপাচিটি + স্বচ্ছতাৰ স্তৰটো এডজাস্ট কৰক (০ মানে স্বচ্ছ, ১০০ মানে অপেক) + Swipe overlay brightness color-ৰ ৰং + উজ্জ্বলতা নিয়ন্ত্ৰণৰ প্ৰোগ্ৰেছ বাৰৰ ৰং নিৰ্ধাৰণ কৰক + Swipe overlay volume color-ৰ ৰং + ভলিউম নিয়ন্ত্ৰণৰ প্ৰোগ্ৰেছ বাৰৰ ৰং নিৰ্ধাৰণ কৰক + সোৱাইপ অভাৰলে স্পীড ৰং + প্লেবে এক্স স্পীড কণ্ট্ৰোলৰ প্ৰোগ্ৰেছ বাৰৰ ৰং নিৰ্ধাৰণ কৰক + Swipe overlay-ৰ পাঠৰ আকাৰ + স্ক্ৰীনৰ পাঠৰ আকাৰ এডজাস্ট কৰক (১-৩০) + সোৱাইপ মেগনিটিউড থ্ৰেশহোল্ড + এডজাস্টমেণ্ট ট্ৰিগাৰ হোৱাৰ আগতে প্ৰয়োজনীয় সোৱাই নিয়াৰ দূৰত্ব নিৰ্ধাৰণ কৰক + ভলিউম সোৱাইপ সংবেদনশীলতা + বেছি মানৰ বাবে প্ৰতিটো ভলিউম ষ্টেপৰ বাবে অধিক সোৱাইপৰ প্ৰয়োজন হয় + ভলিউম ষ্টেপসমূহ + ভলিউম বুষ্ট + সৰ্বোচ্চ ভলিউমৰ ওপৰলৈ সোয়াইপ কৰিলে ইয়াক বুষ্ট কৰে, যাৰ ফলত উচ্চ অডিঅ\' বিকৃত হ\'ব পাৰে + ডিভাইচৰ ডিফল্ট + ব্ৰাইটনেছ সোৱাইপ চেণ্চিটিভিটি + বেছি মানৰ বাবে প্ৰতিটো ব্ৰাইটনেছ ষ্টেপৰ বাবে অধিক সোৱাইপৰ প্ৰয়োজন + স্পীড সোৱাইপ চেণ্চিটিভিটি + বেছি মানৰ বাবে প্ৰতিটো স্পীড ষ্টেপৰ বাবে অধিক সোৱাইপৰ প্ৰয়োজন + গতিৰ পদক্ষেপ + স্পীড জ’নৰ উচ্চতা + প্লেয়াৰৰ উচ্চতাৰ শতাংশ হিচাপে স্পীড জেষ্টাৰ জ’নৰ উচ্চতা (৫-৭৫) + স্পীড + Swipe overlay শৈলী + অনুভূমিক অভাৰলে + অনুভূমিক অভাৰলে (minimal - ওপৰত) + অনুভূমিক অভাৰলে (minimal - কেন্দ্ৰত) + বৃত্তাকাৰ অভাৰলে + বৃত্তাকাৰ অভাৰলে (মিনিমেলিষ্টিক) + উলম্ব অভাৰলে + উলম্ব অভাৰলে (মিনিমেলিষ্টিক) + ভিডিঅ’ সলনি কৰিবলৈ সোয়াইপ সক্ৰিয় কৰক + পিছৰ বা আগৰ ভিডিঅ’লৈ যাবলৈ ফুলস্ক্ৰীন মোডত সোৱাই নিওক + একচন বাটনবোৰ + পৰ্ট্ৰেট আৰু ফুলস্ক্ৰীন মোডত কোনবোৰ একচন বুটাম দেখা দিব তাৰ বাছনি কৰক + পৰ্ট্ৰেট বাটনবোৰ + Like আৰু Subscribe গ্লো নিষ্ক্ৰিয় কৰক + সৃষ্টিকাৰে উল্লেখ কৰিলে Like আৰু Subscribe বুটামবোৰ উজ্জ্বল হোৱাত বাধা দিয়ে + একচন বাৰ লুকুৱাই থওক + Like আৰু Dislike বুটাম লুকুৱাওক + Share বুটাম লুকুৱাওক + \'Stop ads\' বুটাম লুকুৱাওক + Comments বুটাম লুকুৱাওক + কানেক্ট বুটচটো লুকুৱাওক + Report বুটাম লুকুৱাওক + Remix বুটাম লুকুৱাওক + Download বুটাম লুকুৱাওক + Hype বুটামটো লুকুৱাওক + Promote বুটামটো লুকুৱাওক + Thanks বুটামটো লুকুৱাওক + Ask বুটামটো লুকুৱাওক + চেনেল আৰু Subscribe বুটাম লুকুৱাওক + একচন বাৰৰ আৰম্ভণিত চেনেল অৱতাৰ আৰু Subscribe বুটাম লুকুৱায় + Subscribe বুটনটো লুকুৱাই ৰাখক + একচন বাৰৰ আৰম্ভণিত চেনেল অৱতাৰৰ কাষত থকা Subscribe আৰু Join বুটনবোৰ লুকুৱাই ৰাখে + Clip বুটামটো লুকুৱাওক + কিছুমান ব্যৱহাৰকাৰী একাউণ্টৰ বাবে ই কাম নকৰিব পাৰে + Shop বুটামটো লুকুৱাওক + Save বুটামটো লুকুৱাওক + অধিক বুটচটো লুকুৱাওক + ফুলস্ক্ৰীন বাটনবোৰ + অনিচ্ছাসূচক বুটামটো লুকুৱাওক + Like বুটামটো লুকুৱাওক + \'Live chat\' বুটামটো লুকুৱাওক + Mix বুটামটো লুকুৱাওক + \'More videos\' বুটামটো লুকুৱাওক + Playlist বুটামটো লুকুৱাওক + Quick actions ৰ ওপৰৰ মার্জিন + Quick actions বাৰৰ ওপৰৰ মার্জিন নিৰ্ধাৰণ কৰক (0-32 dp) + ক্যাপচন + স্বয়ংক্ৰিয় কেপচন আৰু কেপচন কুকিজসমূহ পৰিচালনা কৰক + অটো কেপচনৰ দৃশ্যতা + সদায় দেখুৱাওক + কেতিয়াও দেখুৱাব নালাগে + আনমিউট কৰিলে দেখুৱাওক + মিউট কৰিলে দেখুৱাওক + পছন্দৰ কেপশ্বন ভাষা + প্ৰথমে প্ৰদানকাৰীৰ সাবটাইটেলসমূহ নিৰ্বাচন কৰে, তাৰ পিছত স্বয়ংক্ৰিয়ভাৱে পছন্দৰ ভাষালৈ অনুবাদ কৰে + বন্ধ + অতিৰিক্ত অটো-ট্ৰেন্সলেট ভাষা যোগ কৰক + অটো-ট্ৰেন্সলেট মেনুলৈ অধিক ভাষা যোগ কৰে + ক্যাপচন কুকিজ নিৰ্ধাৰণ কৰক + "YouTube-ৰ এটা ষ্টক বাগ ঠিক কৰে য'ত কুকিজৰ অভাৱৰ বাবে অটো-ট্ৰেন্সলেট কেপশ্বন উপলব্ধ নহ'ব পাৰে + +'কেপশ্বন কুকিজ' প্ৰদান কৰিব লাগিব" + ক্যাপচন কুকিজ + Timed Text API (Caption Data API) অনুৰোধবোৰত নিৰ্ধাৰণ কৰিবলগীয়া কুকিজ + ক্যাপচন কুকিজ লাভ কৰক + কুকিজবোৰ লাভ কৰিবলৈ ইয়াত ক্লিক কৰক + কুকিজসমূহ আহৰণ কৰাত ব্যৰ্থ হৈছে + কুকিজ ইতিমধ্যে বিদ্যমান + কুকিজসমূহ সফলতাৰে আপডেট কৰা হৈছে + ট্ৰান্সক্ৰিপ্ট ঠিক কৰক + এটা প্ৰাক-চৰ্ত পৰীক্ষাৰ বিফলতা সমাধান কৰে যাৰ বাবে ট্ৰান্সক্ৰিপ্ট লোড হোৱাত বাধা আহিব পাৰে + পিকচাৰ-ইন-পিকচাৰত কেপচন দেখুৱাওক + নেভিগেশ্বন বুটামসমূহ + তলৰ নেভিগেছন বাৰত কোনবোৰ বুটাম দেখুৱাব সেইটো বাছনি কৰক + হোম বুটামটো লুকুৱাওক + Shorts বুটামটো লুকুৱাওক + তৈৰী কৰক বুটামটো লুকুৱাওক + চাবস্ক্ৰিপশ্বন বুটামটো লুকুৱাওক + নটিফিকেচন বুটামটো লুকুৱাওক + আৰ্হি বুটামটো দেখুৱাওক + আৰ্হি বুটামটোৰ স্থান + Settings বুটাম দেখুৱাওক + Settings বুটামৰ স্থান + Settings বুটামৰ কাৰ্য + সক্ৰিয় কৰিলে Morphe settings খোলৰ, অথবা নিষ্ক্ৰিয় কৰিলে YouTube settings খোলৰ + Create বুটামটো Notifications বুটামটোৰ সৈতে সলনি কৰক + যদি এই ছেটিংটো সলনি কৰিও কোনো প্ৰভাৱ নপৰে, তেন্তে ইনকগ্নিটো মোডলৈ পৰিৱৰ্তন কৰি চেষ্টা কৰক + নেভিগেশ্বন বুটামৰ লেবেলসমূহ লুকুৱাই ৰাখক + নতুন সমল্ড ডট লুকুৱাওক + নেভিগেচন বাৰ লুকুৱাই থওক + "ইয়াৰ দ্বাৰা 'You' টেবৰো অন্তৰ্ভুক্ত কৰি নেভিগেচন বাৰটো সম্পূৰ্ণৰূপে লুকুৱাই ৰখা হয়। + +Settings-লৈ যাবলৈ, General → Toolbar → Show Settings-ৰ জৰিয়তে Settings বুটামটো সক্ৰিয় কৰক" + সঙ্কুচিত নেভিগেচন বুটামবোৰ সক্ৰিয় কৰক + বুটামবোৰৰ মাজৰ অনুভূমিক খালী ঠাই কম কৰে + লেবেল নথকা নেভিগেশ্যন বুটামসমূহ নিষ্ক্ৰীয় কৰক + যদি YouTube-এ লেবেল অবিহনে নেভিগেশ্যন বুটাম পঠিয়ায়, তেন্তে এইটোৱে লেবেলৰ সৈতে নেভিগেশ্যন বুটামসমূহ দেখুৱাব + টুলবাৰ নেভিগেচন টেবসমূহ নিষ্ক্ৰিয় কৰক + যদি YouTube-এ ইয়াক Home-ৰ ওপৰৰ এটা টেবলৈ স্থানান্তৰ কৰে, তেন্তে নেভিগেচন বাৰত Subscriptions দেখুৱাব + নেভিগেশ্বন বাৰ এনিমেশ্বনসমূহ সক্ৰিয় কৰক + ট্যাববোৰৰ মাজত পৰিৱৰ্তন কৰোঁতে এনিমেশ্যন কৰে + নেভিগেশ্যন বাৰৰ অটো-হাইড নিষ্ক্ৰীয় কৰক + স্ক্ৰল কৰাৰ সময়ত নেভিগেচন বাৰটো দৃশ্যমান কৰি ৰাখে + নেভিগেচন বাৰৰ শৈলী + ডিফল্ট + আৰ্ধ-স্বচ্ছ + স্বচ্ছ + অস্বচ্ছ + এই শৈলীটো আপোনাৰ ডিভাইচ বা YouTube সংস্কৰণৰ দ্বাৰা সমৰ্থিত নহ\'ব পাৰে, আৰু ই লেআউটৰ সমস্যা বা ক্ৰেচৰ কাৰণ হ\'ব পাৰে + টুলবাৰ + টুলবাৰত কোনবোৰ উপাদান দেখা যাব তাৰ বাবে বাছনি কৰক + Cast বুটাম লুকুৱাওক + চ্যাট বুটামটো লুকুৱাই থওক + Microphone বুটাম লুকুৱাওক + অনুসন্ধান বুটামটো লুকুৱাওক + সক্ৰিয় কৰিলে, টেপ কৰিলে Morphe ছেটিংছ আৰু লং প্ৰেছ কৰিলে YouTube ছেটিংছ খুলিব, সলাই পেলাবলৈ নিষ্ক্ৰিয় কৰক + ফ্লাইআউট মেনু + ফ্লাইআউট মেনুত কোনবোৰ উপাদান দেখা যাব তাৰ বাবে বাছনি কৰক + \'অতিৰিক্ত ছেটিংছ\' / অধিক মেনু লুকুৱাওক + \'এম্বিয়েণ্ট মোড\' মেনু লুকুৱাওক + "'Audio Track' মেনুটো লুকাই আছে + +'Audio Track' মেনুটো দেখুৱাবলৈ, 'Spoof video streams' টো 'Android Studio' আৰু 'Android VR Downgraded' ৰ বাহিৰে যিকোনো ক্লায়েণ্টলৈ সলনি কৰক" + \'অডিঅ\' ট্ৰেক\' মেনুটো লুকুৱাওক + \'অডিঅ\' ট্ৰেক\' মেনুৰ ফুটাৰটো লুকুৱাওক + ক্যাপচন মেনুটো লুকুৱাওক + ক্যাপচন মেনুৰ ফুটাৰটো লুকুৱাওক + ক্যাপচন মেনু হেডাৰ লুকুৱাওক + \'সহায় & মতামত\' মেনু লুকুৱাওক + \'YouTube Music ৰ সৈতে শুনক\' মেনু লুকাই ৰাখক + \'লক স্ক্ৰীন\' মেনু লুকাই ৰাখক + \'ভিডিঅ\' লুপ কৰক\' মেনু লুকুৱাওক + \'অন-দ্য-গো\' মেনু লুকুৱাওক + কোৱালিটি মেনু লুকাই ৰাখক + কোৱালিটি মেনুৰ ফুটাৰ লুকাই ৰাখক + কোৱালিটি মেনুৰ হেৰ্ডাৰ লুকাই ৰাখক + \'স্লীপ টাইমাৰ\' মেনু লুকুৱাওক + \'প্লে\'বেক গতি\' মেনু লুকাই ৰাখক + \'ষ্টেবল ভলিউম\' মেনু লুকুৱাওক + \'VR-ত চাওক\' মেনু লুকাই ৰাখক + অটোপ্লে প্ৰিভিউ লুকুৱাওক + অটোপ্লে আৰম্ভ হোৱাৰ আগতে ফুলস্ক্ৰীণত অহা ভিডিঅ\'ৰ প্ৰিভিউটো লুকুৱায় + অভাৰলে বাটনবোৰ + ভিডিঅ\' প্লেয়াৰৰ ওপৰত কোনবোৰ বুটাম দেখা যাব তাৰ বাবে বাছনি কৰক + অটোপ্লে বুটামটো লুকুওক + ক্যাপচন বুটামটো লুকুওক + কাস্ট বুটামটো লুকুওক + কোল্যাপচ বুটামটো লুকুওক + ফুলস্ক্ৰীণ বুটামটো লুকুওক + প্লেয়াৰ কন্ট্ৰলৰ পৃষ্ঠভূমি লুকুৱাওক + ভিডিঅ\' প্লেয়াৰ কণ্ট্ৰলবোৰৰ চাৰিওফালৰ ক\'লা ছাডিংটো লুকুৱায় + নিয়ন্ত্ৰক বুটামৰ পটভূমিৰ অপাসিটি + প্লেয়াৰ কন্ট্ৰল বুটাম আৰু টাইম বাৰৰ পিছৰ পটভূমিৰ অপাচিটি ঠিক কৰক (০ মানে অদৃশ্য, ১০০ মানে ঘন) + পূৰ্বৰ আৰু পৰৱৰ্তী বুটামবোৰ লুকুওক + ছেটিংছ বুটামটো লুকুওক + এণ্ড স্ক্ৰীন কাৰ্ডসমূহ লুকুৱাওক + এম্বিয়েন্ট মোড + Ambient মোডৰ দৃশ্যমানতা আৰু শক্তিৰ বাধাৰ ব্যৱস্থাপনা কৰক + Ambient mode সীমাবদ্ধতা বাইপাস কৰক + বেটাৰী চেভাৰ মোডত Ambient মোড সক্ৰিয় হৈ থাকিবলৈ অনুমতি দিয়ে + Ambient mode নিষ্ক্ৰিয় কৰক + ফুলস্ক্ৰীণত এম্বিয়ান্ট মোড নিষ্ক্ৰীয় কৰক + তথ্য কাৰ্ডসমূহ লুকুৱাওক + ৰোলিং নম্বৰ এনিমেচনসমূহ নিষ্ক্ৰিয় কৰক + লাইকৰ সংখ্যা আৰু ভিউৰ সংখ্যাৰ গণনা এনিমেশ্যন বন্ধ কৰে + ভিডিঅ’ প্লেয়াৰৰ seekbar লুকুৱাওক + ভিডিঅ’ থাম্বনেইল seekbar লুকুৱাওক + আপুনি ইতিমধ্যে চাই থকা ভিডিঅ\'ৰ থাম্বনেইলত দেখুওৱা watch progress bar-টো লুকুৱাই ৰাখে + ফুলস্ক্ৰীন ডাঙৰ seekbar সক্ৰিয় কৰক + Shorts লুকুৱাওক + চ্যানেল পেজত লুকুৱাই ৰাখক + হোম ফিড আৰু সংলগ্ন ভিডিঅ’ত লুকুৱাওক + সন্ধানৰ ফলাফলত লুকুৱাই ৰাখক + Subscription ফিডত লুকুৱাওক + ভিডিঅ\'ৰ বিৱৰণত লুকুৱাওক + ৱাচ হিষ্টৰীত লুকুৱাওক + Shorts প্লেয়াৰ + Shorts প্লেয়াৰত কোনবোৰ উপাদান দেখা যাব সেইটো বাছনি কৰক + লাইক কৰিবলৈ ডবল-ট্যাপ নিষ্ক্ৰিয় কৰক + AI বুটামটো লুকুৱাই ৰাখক + \'অটো-ডাবড\' লেবেলটো লুকুৱাওক + চেনেল বাৰ লুকুৱাওক + Comments বুটামটো লুকুৱাওক + ইফেক্ট বুটামটো লুকুৱাওক + ভিডিঅ’ লিংক লেবেল লুকুৱাওক + জেষ্টৰ ইঙ্গিতবোৰ লুকুৱাওক + \'গ্ৰীন স্ক্ৰীন\' বুটামটো লুকুৱাওক + হেচটেগ বুটামটো লুকুৱাওক + তথ্য প্যানেল লুকুৱাওক + যোগদান কৰক বুটামটো লুকুৱাওক + Like বুটামটো লুকুৱাওক + Like বুটামৰ ফাউন্টেন এনিমেচনটো লুকুৱাওক + লাইভ প্ৰিভিউ লুকুৱাওক + স্থান লেবেলটো লুকুৱাওক + নেভিগেচন বাৰ লুকুৱাওক + \'New posts\' বুটামটো লুকুৱাওক + পজ কৰা অভাৰলে বুটামবোৰ লুকুৱাওক + প্ৰিভিউ মন্তব্যটো লুকুৱাওক + Remix বুটামটো লুকুৱাওক + সংৰক্ষণ বুটামটো লুকুৱাওক + \'Save music\' বুটামটো লুকুৱাওক + অনুসন্ধানৰ পৰামৰ্শবোৰ লুকুৱাওক + Share বুটামটো লুকুৱাওক + চপ বুটামটো লুকুৱাওক + Sound বুটামটো লুকুৱাওক + শব্দৰ মেটাডেটা লেবেল লুকুৱাওক + Subscribe বুটামটো লুকুৱাওক + \'ছুপাৰ থ্যাংকচ কিনিবলৈ\' বুটামটো লুকুৱাওক + ট্যাগ কৰা সামগ্ৰীসমূহ লুকুৱাওক + Upcoming বুটামটো লুকুৱাওক + \'Use this sound\' বুটামটো লুকুৱাওক + \'Use this template\' বুটামটো লুকুৱাওক + ভিডিঅ’ৰ শিৰোনাম লুকুৱাওক + এণ্ড স্ক্ৰীনৰ পৰামৰ্শ কৰা ভিডিঅ’ লুকুৱাওক + "ভিডিঅ'টো শেষ হোৱাৰ পিছত দেখা দিয়া পৰামৰ্শ কৰা ভিডিঅ' অভাৰলেটো লুকুৱাই থয় + +এই ছেটিংটো কেৱল অটোপ্লে বন্ধ থাকিলেহে কাম কৰে। আপুনি ইয়াক YouTube ত সলনি কৰিব পাৰে: +Settings → Playback (বা Autoplay) → Autoplay next video" + সম্পৰ্কিত ভিডিঅ\'সমূহ লুকুৱাই থওক + \'অধিক ভিডিঅ\' অভাৰলে লুকুৱাওক + ফুলস্ক্ৰীন মোডত ওপৰলৈ সোয়াইপ কৰোতে দেখুওৱা \'More videos\' অভাৰলেটো লুকুৱায় + ভিডিঅ’ টাইমষ্টেম্প লুকুৱাওক + প্লেয়াৰ পপ-আপ পেনেলসমূহ নিষ্ক্ৰীয় কৰক + প্লেলিষ্ট আৰু লাইভ চ্যাট পেনেলবোৰ স্বয়ংক্রিয়ভাৱে খোলা হোৱাটো নিষ্ক্ৰিয় কৰে + প্লেলিষ্ট অটোপ্লে নিষ্ক্ৰিয় কৰক + প্লেলিষ্টবোৰ স্বয়ংক্ৰিয়ভাৱে পৰৱৰ্তী ভিডিঅ\'লৈ আগুৱাই যোৱাত বাধা দিয়ে + \'চাব নেকি?\' প্ৰম্পটটো নিষ্ক্ৰীয় কৰক + ভিডিঅ\' শেষ হোৱাৰ পিছত ফুলস্ক্ৰীণ মোডৰ পৰা ওলাই আহক + অক্ষম কৰা হৈছে + কেৱল প’ৰ্ট্ৰেইট + কেৱল ল্যান্ডস্কেপ + পৰ্ট্ৰেট আৰু লেণ্ডস্কেপ + ফুলস্ক্ৰীনত লেণ্ডস্কেপ জোৰ কৰক + ফুলস্ক্ৰীনত প্ৰৱেশ কৰাৰ সময়ত প্লেয়াৰটো লেণ্ডস্কেপলৈ ঘূৰাওক। সৰু স্ক্ৰীনৰ ক্ষেত্ৰত এইটো উপলব্ধ নহয়, য\'ত ইতিমধ্যে লেণ্ডস্কেপত ফুলস্ক্ৰীন খোলা থাকে + ফুলস্ক্ৰীন জেষ্টাৰ্ছ + ফুলস্ক্ৰীন মোডত প্লেয়াৰ জেচচাৰ্ছ + পৰ্ট্ৰেইট সোৱাইপ-আপ জেষ্টাৰ্ছ নিষ্ক্ৰিয় কৰক + পৰ্ট্ৰেইট সোৱাইপ-ডাউন জেষ্টাৰ্ছ নিষ্ক্ৰিয় কৰক + লেণ্ডস্কেপ সোয়াইপ-ডাউন জেচচাৰ বন্ধ কৰক + পিঞ্চ-টু-জুম জেচচাৰ্ছটো নিষ্ক্ৰীয় কৰক + পিঞ্চ টু জুম পুনৰুদ্ধাৰ কৰক + ফুলস্ক্ৰীন মোডত ভিডিঅ\' খোলক + বন্ধ কৰা হৈছে + পৰ্ট্ৰেট + লেণ্ডস্কেপ + ফুলস্ক্ৰীন ভিডিঅ\'ৰ স্কেল + ডিফল্ট + ষ্ট্ৰেচ (স্ক্ৰীন ভৰাই তোলা) + জুম (ভৰাবলৈ ক্ৰপ কৰা) + ভিডিঅ\' স্কেল বুটনে দেখুৱাওক + ডিফল্ট, ষ্ট্ৰেচ আৰু জুমৰ মাজত পৰিৱৰ্তন কৰিবলৈ টেপ কৰক + কেৱল ফুলস্ক্ৰীণত দেখুৱাওক + পৰ্ট্ৰেইট প্লেয়াৰত ভিডিঅ\' স্কেল বটൺটো লুকুৱাই থৈ দিয়ক + Mute বুটাম দেখুৱাওক + প্লেয়াৰ অভাৰলে অপাছিটি + পাৰ্ شفافতাৰ স্তৰ ঠিক কৰক (0 মানে স্বচ্ছ, 100 মানে অস্বচ্ছ) + চৰ্চৰ বাৰ + নিষ্ক্ৰিয় + চওৰ + অতি চওৰ + ডিভাইচ লেআউট + ডিফল্ট + ফোন + টেবলেট + অট’ম’টিভ + "পৰিৱৰ্তনবোৰৰ ভিতৰত আছে: + +টেবলেট লেআউট +• কমিউনিটি পোষ্টবোৰ লুকাই থাকে +• ফিডত প্লেবেক ছেটিংটো উপলব্ধ নহয় +• Shorts-ত ৰিমিক্স বুটাম আৰু ছাউণ্ড বুটাম উপলব্ধ নহয় +• 'অধিক ভিডিঅ' অভাৰলে উপলব্ধ নহ’ব পাৰে +• ন-কোল্যাপচড পুৰণি ভিডিঅ এক্সন বাৰটো দেখুৱাইছে +• ফ্লাইআউট মেনু আইটেমবোৰ হয়তো সঠিকভাৱে কাম নকৰে + +অটোমোটিভ লেআউট +• ফিডটো বিষয় আৰু চেনেলৰ দ্বাৰা সংগঠিত কৰা হৈছে +• ফিডত প্লেবেক ছেটিংটো উপলব্ধ নহয় +• Shorts-ত ৰিমিক্স বুটাম আৰু ছাউণ্ড বুটাম উপলব্ধ নহয় +• 'অধিক ভিডিঅ' অভাৰলে উপলব্ধ নহ’ব পাৰে +• Shortsবোৰ নিয়মীয়া প্লেয়াৰত খোল খাই থাকে +• ন-কোল্যাপচড পুৰণি ভিডিঅ এক্সন বাৰটো দেখুৱাইছে" + প্লেয়াৰত টেবলেট লেআউট সক্ৰিয় কৰক + "পুৰণি প্লেয়াৰ লেআউট পুনৰুদ্ধাৰ কৰে + +সীমাবদ্ধতা: যদি 'Restore old video action bar' বন্ধ কৰি থোৱা হয়, তেন্তে 'More videos' অভাৰলেটো উপলব্ধ নহ'ব পাৰে" + ২১.১১.৪৮ - পুৰণি কেপচনৰ আচৰণ পুনৰুদ্ধাৰ কৰক + 20.39.41 - প্লেলিষ্টলৈ পুৰণি ছেভ পুনৰুদ্ধাৰ কৰক + 20.28.41 - বোল্ড আইকনসমূহ নিষ্ক্ৰিয় কৰক + 20.13.41 - ভিডিঅ\' প্লেয়াৰৰ তলত চেনেলৰ নাম পুনৰুদ্ধাৰ কৰক + চিষ্টেম শ্বেয়াৰ শ্বীট খোলক + সামগ্ৰী শ্বেয়াৰ কৰাৰ সময়ত ইন-এপৰ পৰিৱৰ্তে চিষ্টেম শ্বেয়াৰ শ্বিট খোলক + YouTube Music বুটমসমূহ অভাৰৰাইড কৰক + ষ্টক YouTube Music এপ্পৰ পৰিৱৰ্তে আপোনাৰ পছন্দৰ সংগীত এপ্পটো খোলক + সংগীত এপৰ পেকেজ নাম + আপোনাৰ সংস্থাপিত মিউজিক এপৰ পেকেজ নাম + আৰম্ভণিৰ পৃষ্ঠা + ডিফল্ট + সকলো চাবস্ক্ৰিপচন + চেনেলসমূহ ব্ৰাউজ কৰক + কৰ্ছ / শিক্ষণ + এক্সপ্লোৰ কৰক + ফেশ্বন & সৌন্দৰ্য + গেমিং + ইতিহাস + লাইব্ৰেৰী + পছন্দ কৰা ভিডিঅ’সমূহ + লাইভ + চিনেমা + সংগীত + সংবাদ + জাননী + প্লেলিষ্ট + সন্ধান + চৰ্চা-বজাৰ + Shorts + ক্ৰীড়া + চাবস্ক্ৰিপচন + ট্ৰেণ্ডিং + ভাৰ্চুৱেল ৰিয়েলিটি + পিছত চাব + আপোনাৰ ক্লিপসমূহ + ষ্টাৰ্টআপত ৰিজিউম কৰাটো নিষ্ক্ৰীয় কৰক + Shorts খোলক ইয়াৰ সৈতে + Shorts প্লেয়াৰ + নিয়মিত প্লেয়াৰ + নিয়মিত প্লেয়াৰ ফুলস্ক্ৰীন + Shorts স্বয়ংক্ৰিয়ভাৱে চলাওক + বৰ্তমানৰটো পুনৰাবৃত্তি কৰাৰ পৰিৱৰ্তে স্বয়ংক্রিয়ভাৱে পৰৱৰ্তী Short-লৈ আগবাঢ়ি যায় + বেকগ্ৰাউণ্ডত অটোপ্লে কৰক + বেকগ্ৰাউণ্ড প্লেবেকৰ সময়ত স্বয়ংক্রিয়ভাৱে পৰৱৰ্তী Short-লৈ আগবাঢ়ি যায় + লাইভ Shorts স্কিপ কৰক + Shorts ফিডত দেখা দিয়া লাইভ ষ্ট্ৰিমসমূহ স্কিপ কৰে + ফীডত প্লেবেকে + সদায় অন + কেৱল Wi-Fi + বন্ধ + মূল শিৰোনামবোৰ পুনৰুদ্ধাৰ কৰক + অটো-ট্ৰেন্সলেটেড বা স্বয়ংক্ৰিয়ভাৱে অনুবাদ কৰা শিৰোনামৰ পৰিৱৰ্তে মূল ভিডিঅ\'ৰ শিৰোনাম, ভিডিঅ\'ৰ বৰ্ণনা আৰু চেনেলৰ বৰ্ণনা দেখুৱায় + শিৰোনাম লোড হৈছে… + অটো ফিড ৰিফ্ৰেছ নিষ্ক্ৰীয় কৰক + ফিডবোৰ পুৰণি হোৱাৰ পিছত স্বয়ংক্ৰিয়ভাৱে ৰিফ্ৰেছ হোৱাত বাধা দিয়ে + ফিড কাৰ্ড লেআউট + ক্লাছিক + কাৰ্ড + লাইক, কমেন্ট আৰু সেভ বুটাম থকা কাৰ্ড + মন্তব্য + Miniplayer ৰিজিউমিং নিষ্ক্ৰিয় কৰক + \'Continue watching\' মিনিপ্লেয়াৰটো নিষ্ক্ৰীয় কৰে, যিটো এটা YouTube Premium বৈশিষ্ট্য আৰু এপটো আৰম্ভ হোৱাৰ সময়ত দেখা যায় + মিনিপ্লেয়াৰ + মিনিমাইজ কৰা ভিডিঅ\' প্লেয়াৰৰ ৰূপ আৰু আচৰণ পছন্দ অনুযায়ী সজাই লওক + মিনিপ্লেয়াৰৰ প্ৰকাৰ + শিৰোনাম আৰু চেনেল লুকুৱাওক + অক্ষম কৰা হৈছে + ডিফল্ট + মিনিমেল + মিনিমেল ২ + টেবলেট + মডৰ্ণ ১ + মডৰ্ণ ২ + মডৰ্ণ ৩ + মডৰ্ণ ৪ + গোলাকৰ কোণবোৰ নিষ্ক্ৰীয় কৰক + ড্ৰেগ আৰু ড্ৰপ নিষ্ক্ৰীয় কৰক + মিনিমাইজ কৰা প্লেয়াৰটো স্ক্ৰীনৰ বিভিন্ন কোণলৈ টানি নিবলৈ বাধা দিয়ে + অনুভূমিক ড্ৰেগ জেসচাৰ নিষ্ক্ৰীয় কৰক + মিনিপ্লেয়াৰটো অনুভূমিকভাৱে স্ক্ৰীনৰ বাহিৰলৈ সোয়াই বন্ধ কৰাত বাধা দিয়ে + অনুভূমিকভাৱে টানি প্লেবেক বন্ধ কৰক + মিনিপ্লেয়াৰটো স্ক্ৰীনৰ পৰা আঁতৰাই নিলে প্লেব্যাক সাময়িকভাৱে বন্ধ হ\'ব + টানি লৈ যোৱা অৱস্থাৰ স্থানটো মনত ৰাখক + মিনিপ্লেয়াৰটো স্ক্ৰীণৰ বাহিৰলৈ টানি নিলে ইয়াৰ UI অৱস্থাটো সংৰক্ষিত হ\'ব + অভীষ্ট বুটামবোৰ লুকুৱাওক + ডিফল্ট আকাৰ + পিক্সেলত, স্ক্ৰীনত আৰম্ভণিৰ আকাৰ নিৰ্ধাৰণ কৰক + পিক্সেলৰ আকাৰ %1$s আৰু %2$s ৰ ভিতৰত হ\'ব লাগে + অভাৰলে অপাছিটি + পৰদৰ্শিতাৰ স্তৰটো সামজৰুৱাওক (০ মানে স্বচ্ছ, ১০০ মানে অস্বচ্ছ) + বৰ্তমানৰ বুটামটোৱে ফিডৰ পৰা বাহিৰ কৰি আনে + বৰ্তমানৰ বুটামটো টিপিলে ফিডৰ ওপৰলৈ স্ক্ৰল কৰাৰ পৰিৱৰ্তে সদায় এপৰ পৰা বাহিৰ হয় + এনিমেশ্যন বল বাধ্য কৰক + ডিফল্ট + সদায় অন + সদায় অফ + ষ্টেটাচ বাৰ লুকুৱাই ৰাখক + চিস্টেম ষ্টেটাচ বাৰটো লুকুৱাই ৰাখে। ইয়াৰ এটা মুহূৰ্তৰ বাবে দেখুৱাবলৈ ওপৰৰ কাষৰ পৰা তললৈ সোয়াইপ কৰক + ষ্টেটাছ বাৰে টপ বাৰসমূহ ঢাকি থকা সমস্যাটো সমাধান কৰক + ষ্টেটাছ বাৰৰ লেআউটো সংগত কৰি ৰাখে, যাতে ই টপ বাৰসমূহক ঢাকি নেৰাখে আৰু প্লেয়াৰৰ ওপৰত কোনো খালী ঠাই নেৰে। যদি এপটো ক্ৰাশ কৰে বা টপ বাৰবোৰ ভুলকৈ দেখায় তেন্তে ইয়াক বন্ধ কৰক + লাইট থিমৰ পৃষ্ঠভূমি + বগা + Material You (বগা) + হালুকা গোলাপি + হালুকা নীলা + হালুকা সেউজী + হালুকা হালধীয়া + হালুকা কমলা + হালুকা ৰঙা + কাস্টম লাইট বেকগ্ৰাউণ্ড + লাইট থিমৰ বেকগ্ৰাউণ্ড ৰং নিৰ্ধাৰণ কৰক + ফৰগ্ৰাউণ্ডৰ ৰং সলনি কৰক + পাঠ আৰু আইকনৰ বাবে থিমৰ ৰং ব্যৱহাৰ কৰক + গ্ৰেডিয়েন্ট লোডিং স্ক্ৰীণ সক্ৰিয় কৰক + একেকটা নিৰ্দিষ্ট ৰঙৰ পৰিৱৰ্তে লোডিং স্ক্ৰীনত এটা গ্ৰেডিয়েণ্ট বেকগ্ৰাউণ্ড দেখুৱায় + কষ্টম ছিকবাৰ ৰং সক্ৰিয় কৰক + Seekbar ৰ ৰং + ভিডীঅ\' প্ৰগতি বাৰৰ প্ৰাথমিক ৰং নিৰ্ধাৰণ কৰক + Seekbar accent ৰং + ভিডীঅ\' প্ৰগতি বাৰৰ গৌণ ৰং নিৰ্ধাৰণ কৰক + অবৈধ seekbar ৰঙৰ মান + হেডাৰ ল’গ’ + ডিফল্ট + ৰিগুলৰ + কষ্টম + ছবিৰ আঞ্চলিক বাধা অতিক্ৰম কৰক + প্ৰতিবন্ধক দেশসমূহত অনুপস্থিত ছবিসমূহ ঠিক কৰিবলৈ থাম্বনেইল অনুৰোধসমূহক এটা বিকল্প চাৰ্ভাৰলৈ পুনৰ নিৰ্দেশিত কৰে + DeArrow শিৰোনাম + মুখ্য পৃষ্ঠা টেব + চাবদানসমূহ টেব + আপুনি টেব + প্লেয়াৰ প্লেলিষ্ট আৰু পৰামৰ্শসমূহ + অনুসন্ধানৰ ফলাফল + DeArrow আইকন দেখুওক + Crowdsourced ভিডিঅ’ৰ শিৰোনামত DeArrow আইকনটো দেখুৱায় + কেজুৱেল মোড + যদি DeArrow ব্যৱহাৰকাৰীসকলে মূল শিৰোনামটো পছন্দ কৰিছে বুলি ভোট দিয়ে, তেন্তে DeArrow শিৰোনামৰ পৰিৱৰ্তে মূল শিৰোনামটো দেখুৱাব + মূল শিৰোনাম দেখুৱাবলৈ নূন্যতম কেজুৱেল ভোট + মূল শিৰোনাম দেখুৱাবলৈ এটা নিৰ্দিষ্ট বিভাগত, যেনে ধেমেলীয়া বা সৃজনীমূলক, প্ৰয়োজনীয় কেজুৱেল ভোটৰ সংখ্যা + DeArrow থাম্বনেইলসমূহ + মুল থাম্বনেইলসমূহ + DeArrow & মূল থাম্বনেইলসমূহ + DeArrow & Still captures + স্থিৰ কেপচাৰ + "DeArrow-এ ক্লিকবেট শিৰোনাম আৰু থাম্বনেইলসমূহক ভিৰৰ দ্বাৰা সংগ্ৰহ কৰা অধিক সঠিক আৰু কম উত্তেজনাৰ শিৰোনাম আৰু থাম্বনেইলৰ সৈতে সলাই দিয়ে। শিৰোনামসমূহ সম্প্ৰদায়ৰ দ্বাৰা লিখা আৰু ভোট দিয়া হয়, আৰু থাম্বনেইলসমূহ ভিডিঅ’ৰ পৰা লোৱা ফ্ৰেম হয় + +ভিডিঅ’সমূহৰ বাবে হেচড ভিডিঅ’ ID-ৰ এটা চুটি প্ৰিফিক্সৰ দ্বাৰা অনুৰোধ কৰা হয়, যাতে চাৰ্ভাৰে ঠিককৈ ক’ব নোৱাৰে যে আপুনি কোনটো ভিডিঅ’ চাই আছে। যদি কোনো ভিডিঅ’ৰ DeArrow থাম্বনেইল নাথাকে, তেন্তে মূল থাম্বনেইল বা এটা স্টিল কেপচাৰ দেখুওৱা হয়" + সংযোগৰ ত্ৰুটিৰ সতৰ্কবাণী দেখুৱাওক + DeArrow চাৰ্ভাৰ উপলব্ধ নহ\'লে এটা জাননী দেখুৱায় + API এনডপইণ্ট + DeArrow থাম্বনেইল কেছ এণ্ডপইণ্টৰ URL + স্থিৰ ভিডিঅ\' কেপচাৰ + ষ্টিল কেপচাৰসমূহ প্ৰতিটো ভিডিঅ’ৰ আৰম্ভণি / মাজৰ পৰা / শেষৰ পৰা লোৱা হয়। এই ছবিসমূহ YouTube-ত বিল্ট-ইন কৰা হৈছে আৰু কোনো বহিঃস্থ API ব্যৱহাৰ কৰা হোৱা নাই + পৰা ভিডিঅ\' ফ্ৰেম নিষ্কাশন কৰক + আৰম্ভণি + মাজত + শেষত + DeArrow সাময়িকভাৱে উপলব্ধ নহয় (ষ্টেটাচ ক\'ড: %s) + DeArrow সাময়িকভাৱে উপলব্ধ নহয় + পুৰণি প্লেয়াৰ বুটামৰ শৈলী পুনৰুদ্ধাৰ কৰক + ভিডিঅ\' প্লেয়াৰৰ বোল্ড নিয়ন্ত্ৰকবোৰক পূৰ্বৰ পাতল দৃশ্যমান শৈলীলৈ ঘূৰাই আনে + ডিভাইচৰ ডাইমেনচন নকল কৰক + এইটো সক্ৰিয় কৰিলে অধিক উচ্চমানৰ ভিডিঅ\' কোৱালিটি আনলক কৰিব পাৰি + এইটো সক্ৰিয় কৰিলে ভিডিঅ\' প্লেবেকেত্ বাধা আহিব পাৰে, বেটাৰীৰ আয়ু হ্ৰাস পাব পাৰে আৰু অন্যান্য অজ্ঞাত কাৰণৰ ফলত সমস্যা হ\'ব পাৰে + হেপটিক ফিডবেক + কোনবোৰ ইণ্টেৰেকচনে ডিভাইচৰ ভাইব্ৰেচন ট্ৰিগাৰ কৰিব সেয়া বাছনি কৰক + অধ্যায়ৰ হেপটিক বন্ধ কৰক + প্ৰিসাইজ চিচিং হেপটিক বন্ধ কৰক + চিচ আনডু হেপটিক বন্ধ কৰক + ট্যাপ আৰু হোল্ড হেপটিক্ছ নিষ্ক্ৰীয় কৰক + জুম হেপটিক বন্ধ কৰক + যদি আপুনি শেহতীয়াকৈ আপোনাৰ একাউণ্টৰ লগ-ইন সবিশেষ পৰিৱৰ্তন কৰিছে, তেন্তে MicroG আন-ইনষ্টল কৰি পুনৰ ইনষ্টল কৰক + লিংক ৰিডাইৰেক্টসমূহ বাইপাচ কৰক + ট্ৰেকিং লিংক (youtube.com/redirect) উপৰিও পোনপটীয়াকৈ মূল লিংকটো মুকলি কৰে + ব্ৰাউজাৰত লিংক খোলাক + ইন-এপ ব্ৰাউজাৰৰ পৰিৱৰ্তে বাহ্যিক ব্ৰাউজাৰত লিংকসমূহ খোলা হয় + প্লেবেক বাফৰৰ আকাৰ + ডিফল্ট + কম + মধ্যম + সৰ্বাধিক + ভিডিয়ো ঢিলাকৰণ + ব্লুটুথ হেডফোনৰ দৰে লেগ থকা অডিঅৰ সৈতে সিঙ্ক কৰি ৰাখিবলৈ ভিডিঅ’টো ঢিলা কৰে (০-৫০০০ মি.ছে.) + কেৱল ব্লুটুথ অডিঅৰ সৈতে ঢিলা কৰক + কেৱল ব্লুটুথ অডিঅ সংযুক্ত হৈ থকাৰ সময়ত ভিডিঅ’টো ঢিলা কৰে, যাতে ফোনৰ স্পীকাৰটো সিঙ্ক হৈ থাকে + ভিডিঅ\' ডিলে বাটন দেখুৱাওক + ডিলিয়েটটো 0 ms কৰিবলৈ টেপ কৰি ধৰি ৰাখক + ভিডিঅ\' ডিলে মেনু দেখুৱাওক + মিলিছেকণ্ডত ডিলে + অটো + ভিডিঅ\'ৰ মানদণ্ড মনত ৰাখক + আপুনি হাতেৰে বাছনি কৰা মানদণ্ডটো ভৱিষ্যতৰ সকলো ভিডিঅ\'ত প্ৰয়োগ কৰে + মানদণ্ড পৰিৱৰ্তনৰ জাননী + এটা নতুন ডিফল্ট কোৱালিটি সংৰক্ষণ কৰিলে এটা জাননী দেখুৱায় + Wi-Fi নেটৱৰ্কত ডিফল্ট ভিডিঅ\'ৰ গুণগতমান + মবাইল নেটৱৰ্কত ডিফল্ট ভিডিঅ\'ৰ গুণগতমান + Shorts মান মনত ৰাখক + আপোনাৰ হাতেৰে নিৰ্বাচিত মানদণ্ডটো ভৱিষ্যতৰ সকলো Shorts-ৰ ক্ষেত্ৰত প্ৰয়োগ কৰক + Wi-Fi নেটৱৰ্কত ডিফল্ট Shorts ৰ গুণগতমান + মবাইল নেটৱৰ্কত ডিফল্ট Shorts ৰ গুণগতমান + ডিফল্ট মবাইল কোৱালিটি সলনি কৰি %s কৰা হৈছে + ডিফল্ট Wi-Fi কোৱালিটি সলনি কৰি %s কৰা হৈছে + Shorts মোবাইলৰ কোৱালিটি সলনি কৰি %s কৰা হৈছে + Shorts Wi-Fi কোৱালিটি সলনি কৰি %s কৰা হৈছে + প্লে’বেক স্পীড বুটন দেখুৱাওক + গতিটো ডিফল্টত ৰাখিবলৈ টিপি ধৰি ৰাখক + মানদণ্ড বুটন দেখুৱাওক + মানদণ্ডটো ডিফল্টত ৰাখিবলৈ টিপি ধৰি ৰাখক + Quality বুটামত ৰিজল্যুচন দেখুৱাওক + FHD ৰ দৰে লেবেলৰ পৰিৱৰ্তে 1080 ৰ দৰে ৰিজল্যুচন দেখুৱাওক + কাষ্টম প্লেবেক স্পীড মেনু + পুৰণি প্লেবেক স্পীড মেনু পুনৰুদ্ধাৰ কৰক + কষ্টম প্লেবেক স্পীড + আপোনাৰ পছন্দৰ প্লে’বেক স্পীড বিকল্পটো প্ৰৱেশ কৰক + কষ্টম প্লেবেজ স্পীড %s তকৈ কম হ\'ব লাগিব + অবৈধ কষ্টম প্লেবেচ স্পীড + অটো + প্লে’বেক স্পীড টিপি ধৰি ৰাখক + স্ক্ৰীৰনটি টিপি ধৰি ৰখাৰ সময়ত স্পীড মাল্টিপ্লায়াৰটো নিৰ্ধাৰণ কৰক, নিষ্ক্ৰিয় কৰিবলৈ 0 কৰক + প্লেব্যাক স্পীড মনত ৰাখক + আপোনাৰ হাতেৰে নিৰ্বাচিত স্পীডটো ভৱিষ্যতৰ সকলো ভিডিঅ\'ৰ বাবে প্ৰয়োগ কৰক + স্পীড পৰিৱৰ্তনৰ জাননী + এটা নতুন ডিফল্ট প্লেব্যাক স্পীড সংৰক্ষণ কৰিলে এটা জাননী দেখুৱায় + ডিফল্ট প্লেবেচ স্পীড + Shorts-ত ডিফল্ট প্লেব্যাক স্পীড + Shorts-ত প্লেবেকেৰ গতি মনত ৰাখক + আপোনাৰ হাতেৰে নিৰ্বাচিত Shorts-ৰ গতি ভৱিষ্যতৰ সকলো Shorts-ৰ বাবে প্ৰয়োগ কৰিব। যদি এইটো বন্ধ থাকে, তেন্তে Shorts প্লেয়াৰটো বন্ধ কৰাৰ পিছত গতি আকৌ ডিফল্টত উভতি আহিব + Shorts মেনুত Playback speed দেখুৱাওক + Shorts প্লেয়াৰ মেনুতো এটা Playback speed আইটেম যোগ কৰে + প্লে’বেছ স্পীড · %s + ডিফল্ট প্লেবেজ স্পীডলৈ সলনি কৰা হৈছে: %s + সংগীতৰ বাবে স্বাভাৱিক গতি ব্যৱহাৰ কৰক + সংগীতৰ ভিডিঅ\'বোৰ সদায় 1.0x গতিত চলাওক + চেনেলৰ গতি হোৱাইটলিষ্ট + হোৱাইটলিষ্ট কৰা চেনেলৰ ভিডিঅ’বোৰ সদায় ১.০x গতিত চলে + গতি হোৱাইটলিষ্টলৈ চেনেল যোগ কৰক + গতি হোৱাইটলিষ্টৰ পৰা চেনেল আঁতৰাওক + অডিঅ’ পিচ নিয়ন্ত্ৰকসমূহ সক্ৰিয় কৰক + ডিফল্ট প্লেবেত অডিঅ’ পিচ + ডিফল্ট অডিঅ’ পিচলৈ সলনি কৰা হৈছে: %s + অডিঅ’ টাইম ষ্ট্ৰেচিং + গতি আৰু পিচ স্বতন্ত্ৰকৈ নিৰ্ধাৰণ কৰিবলৈ অনুমতি দিয়ে, ই অডিঅ’ আৰ্টিফেক্টৰ সৃষ্টি কৰিব পাৰে + লাইভ ষ্ট্ৰিম প্লেবেকেৰ স্থান মনত ৰাখক + চলমান লাইভ ষ্ট্ৰিমৰ বাবে, বৰ্তমানৰ লাইভ স্থানলৈ যোৱাৰ পৰিৱৰ্তে আপোনাৰ প্লেবেকেৰ স্থান মনত ৰাখে আৰু তাৰ পৰাই আৰম্ভ কৰে + এই বৈশিষ্ট্যটো ২৪/৭ লাইভ ষ্ট্ৰীম আৰু আন \'সদায় সক্ৰিয়\' হৈ থকা ষ্ট্ৰীমবোৰৰ ক্ষেত্ৰত কাম নকৰে + HDR ভিডিঅ\' নিষ্ক্ৰিয় কৰক + Force HDR ভিডিঅ\' + এই ডিভাইচটো HDR-সক্ষম বুলি চিনি পাবলৈ এপটো বুজাব + "যদি HDR ডিক’ডিং কাম কৰে কিন্তু YouTube-এ HDR প্ৰদান নকৰে, তেন্তে ইয়াক ব্যৱহাৰ কৰক, যেনে কিছুমান HyperOS ডিভাইচ যিয়ে HLG লুকুৱায়। + +সীমাবদ্ধতাসমূহ: +• যদি ডিচপ্লেয়ে আচলতে HDR দেখুৱাব নোৱাৰে তেন্তে ৰংবোৰ ভুল যেন লাগিব পাৰে +• যদি ডিক’ডৰে HDR পৰিচালনা কৰিব নোৱাৰে তেন্তে প্লেব্যাক বিফল হ'ব পাৰে +• HDR এতিয়াও স্পুফ ভিডিঅ' ষ্ট্ৰিম ক্লায়েন্টৰ ওপৰত নিৰ্ভৰ কৰে" + ভিডিঅ’ কোডেক + ডিফল্ট + AVC বল বাধ্য কৰিবলৈ, \'Spoof video streams\'ক \'Android Studio\', \'Android VR\' বা \'visionOS\'লৈ সলনি কৰক + "VP9 +• বেটাৰীৰ জীৱনকাল উন্নত কৰিব পাৰে আৰু প্লেবেৰ্ৰোৰ ষ্টাটৰিং ঠিক কৰিব পাৰে +• AV1তকৈ অধিক ইণ্টাৰনেট ডাটা ব্যৱহাৰ কৰে + +AVC (H.264) +• 'Android Studio', 'Android VR', বা 'visionOS' Spoof video stream ক্লায়েন্টৰ প্ৰয়োজন +• বেটাৰীৰ জীৱনকাল আৰু অধিক উন্নত কৰিব পাৰে +• পুৰণি ডিভাইচত হেৰাই যোৱা ভিডিঅ’ ৰিজলিউচন পুনৰুদ্ধাৰ কৰিব পাৰে +• সৰ্বোচ্চ ৰিজলিউচন হ’ল 1080p +• HDR ভিডিঅ’সমূহে AVC ব্যৱহাৰ নকৰিব পাৰে +• কিছুমান ডিভাইচে AVC বল বাধ্য কৰিব নোৱাৰে" + উন্নত মানদণ্ডৰ মেনু খোলক + কোৱালিটি নিৰ্বাচন মেনু স্কিপ কৰি পোনপটীয়াকৈ এডভান্সড কোৱালিটি মেনু খোলা হ\'ব + Premium মানৰ বিকল্পসমূহ লুকুৱাই ৰাখক + ভিডিঅ’ৰ মানদণ্ডক অগ্ৰাধিকাৰ দিয়ক + কোডেক কাৰ্যক্ষমতাৰ পৰিবৰ্তে উপলব্ধ উচ্চতম ভিডিঅ’ মানদণ্ডক অগ্ৰাধিকাৰ দিয়ে + যদি এই ছেটিংটো বন্ধ কৰা হয়, তেন্তে পুৰণি ভিডিঅ’বোৰত কোনো ভিডিঅ’ মানদণ্ডৰ বিকল্প দেখা নাযাব পাৰে + \'Android VR\' / visionOS AV1 অনুমতি দিয়ক + \'Android VR\' / visionOS ক্লায়েন্ট ব্যৱহাৰ কৰোঁতে ছফ্টৱেৰ AV1 ডিকডিং কৰিবলৈ অনুমতি দিয়ে + "এই ছেটিংটো সক্ৰিয় কৰিলে ছফ্টৱেৰ AV1 ডিকোডিং ব্যৱহাৰ হ'ব পাৰে। + +AV1 ৰ সৈতে ভিডিঅ' প্লেবেকেত্ বাধা আহিব পাৰে বা ফ্ৰেম ড্ৰপ হ'ব পাৰে" + Spoofing-ৰ কাৰকসমূহ + • ১ মিনিটত প্লেবেকে বিফল হ\'ব পাৰে + • \'অডিঅ’ ট্ৰেক\' মেনুটো নাই + • কোনো AV1 ভিডিঅ codec নাই + • 360° VR ইম্মাৰ্সিভ মোড উপলব্ধ নহয় + • Stable volume উপলব্ধ নহয় + • প্লেব্যাক ১ ছেকেণ্ড আগতেই শেষ হ\'ব পাৰে + • যদি ডি-অ’বফিউকেচন ব্যৰ্থ হয়, তেন্তে প্লেবেকে ব্যৰ্থ হ’ব পাৰে + • Force original audio উপলব্ধ নহয় + • Android VR-ত ছাইন ইন কৰাটো প্ৰয়োজনীয় + • মানদণ্ড 720p লৈ সীমিত হ\'ব পাৰে + প্লেয়াৰ JavaScript ভাৰিয়াণ্ট + \'Stats for nerds\'ত স্পুফিং দেখুৱাওক + পুৰণি ভিডিঅ\' এক্সন বাৰ পুনৰুদ্ধাৰ কৰক + "ভিডিঅ' একচন বুটমসমূহ লুকুৱাব নোৱাৰা সমস্যাটো সমাধান কৰে + +সীমাবদ্ধতা: পুৰণি শৈলীৰ 'More videos' অভাৰলেীলৈ উভতি যায়" + প্লেয়াৰ আইকন শৈলী + স্বয়ংক্ৰিয় + ডিফল্ট + সৰু + গাঢ় + কাষ্টম + Shorts আইকন শৈলী + চিৰিজ ট্ৰেককাৰ + প্লেতলিষ্টবোৰক চিৰিজ হিচাপে অনুসৰণ কৰক আৰু আপোনাৰ স্থানটো ৰাখক + চিৰিজ ট্ৰেকিং সক্ৰিয় কৰক + ইতিহাসলৈ এটা চিৰিজ পৃষ্ঠা আৰু প্লেতলিষ্টলৈ এটা চিৰিজ অনুসৰণ কৰাৰ বিকল্প যোগ কৰে + নেভিগেচন বাৰত চিৰিজ দেখুৱাওক + চিৰিজ ট্ৰেককাৰ বুটাম দেখুৱাওক + বৰ্তমানৰ ভিডিঅ\'টোৰ চিৰিজ অনুসৰণ কৰিবলৈ বা খুলিবলৈ এটা প্লেয়াৰ বুটাম যোগ কৰে + অনুসৰণ কৰা চিৰিজৰ প্ৰগতি ৰেকৰ্ড কৰক + সংৰক্ষিত চিৰিজ আৰু বুকমাৰ্কৰ বাবে এই ফোনত প্লেবেক ৰেকৰ্ড কৰক + বাতিল কৰক + সংৰক্ষণ কৰক + বুকমাৰ্ক সংৰক্ষণ কৰক + ছিৰিজৰ নাম + চিৰিজৰ বিকল্পসমূহ + লাইব্ৰেৰীৰ বিকল্পসমূহ + এপিসোডসমূহ ৰিফ্ৰেচ কৰক + নাম সলনি কৰক + YouTube-ত প্লেলিষ্টটো খোলক + লাইব্ৰেৰীৰ পৰা আঁতৰ কৰক + প্লেলিষ্ট লিংক বা ID + সকলো স্থানীয় অগ্ৰগতি মুচি পেলাওক + ইতিহাস মুচি পেলাওক + সকলো স্থানীয় অগ্ৰগতি মুচি পেলাব নেকি? + ইয়াৰ দ্বাৰা প্ৰতিটো ভিডিঅ\'ৰ পজিশন, পটি যোৱা অভাৰৰাইড আৰু ৰিজুম সিলেকচনসমূহ মুছকৈ দিয়া হয়। সংৰক্ষিত ছিৰিজ আৰু সেইবোৰৰ ক্যাটালগসমূহ থাকিব। ৰেকৰ্ডিং সক্ৰিয় থাকিলে নতুন প্লেবেক ৰেকৰ্ড কৰা হ\'ব + চাব পৰা বুলি চিহ্নিত কৰক + নচাব পৰা বুলি চিহ্নিত কৰক + স্বয়ংক্ৰিয় স্থিতি ব্যৱহাৰ কৰক + ০:০০ ৰ পৰা পুনৰ আৰম্ভ কৰক + ০:০০ ৰ পৰা পুনৰ প্লে কৰক + স্থিতি সংৰক্ষণ কৰা হৈছে + সংৰক্ষণ কৰিবলৈ পুনৰ চেষ্টা কৰক + আগৰৰ ১০০টি + পিছৰ ১০০টি + সংৰক্ষিত ভিডিঅ\'টো খোলক + স্থানীয় অগ্ৰগতি মচি পেলোৱা হ\'ল + আপোনাৰ চিৰিজসমূহ চাবলৈ \'You\'ৰ পৰা History খোলক + চোৱাৰ ইতিহাস + চিৰিজ + সংৰক্ষিত চিৰিজসমূহ লোড হৈ আছে… + এপিসোডসমূহৰ ট্ৰেক কৰিবলৈ এটা প্লেলিষ্ট ফল\' কৰক + প্লেলিষ্ট বুকমাৰ্ক + এপিসোডসমূহ ৰিফ্ৰেচ কৰা হৈছে… + সকলো ছিৰিজ + চিলুৱা + এপিচোডবোৰ লোড হৈছে… + অধিক বিকল্প + "আপোনাৰ সংৰক্ষিত ছিৰিজ আৰু বুকমাৰ্ক কৰা ভিডিঅ’ৰ বাবেহে প্ৰগতি ৰেকৰ্ড কৰক + +এই লাইব্ৰেৰীটো এই ডিভাইচত সংৰক্ষিত হয় আৰু বিভিন্ন YouTube একাউণ্টৰ মাজত ভাগ কৰা হয়। YouTube-ৰ pause আৰু clear history নিয়ন্ত্ৰকবোৰৰ দ্বাৰা ইয়াক সলনি কৰিব নোৱাৰি। ইয়াক মচি পেলাবলৈ Series → More → Clear local progress ব্যৱহাৰ কৰক + +ইনকগনিটোত, যেতিয়া একাউণ্টৰ পৰিচয় উপলব্ধ নহয়, অথবা যেতিয়া আপুনি আন এখন একাউণ্টলৈ সলনি কৰে, তেতিয়া স্বয়ংক্ৰিয় ৰেকৰ্ডিং বন্ধ হৈ যায়। সেই একাউণ্টৰ সৈতে ইয়াৰ ব্যৱহাৰৰ বাবে ট্ৰেকিং পুনৰ সক্ৰিয় কৰক" + ইনকগনিটোত অথবা YouTube-এ বৰ্তমানৰ একাউণ্টৰ অৱস্থা লোড কৰাৰ আগতে ট্ৰেকিং সক্ৰিয় কৰিব নোৱাৰি। হোম লৈ উভতি আহক আৰু পুনৰ চেষ্টা কৰক। কোনো ছাইন-ইনৰ প্ৰয়োজন নাই + স্থানীয় ট্ৰেকিং সক্ৰিয় কৰক + প্ৰগতি ৰেকৰ্ডিং বন্ধ হৈ আছে + %1$s ত পুনৰ আৰম্ভ কৰক + %1$s / %2$s খন এপিসোড চোৱা হৈছে + এপিসোড %1$s · %2$s + %3$s খনৰ ভিতৰত এপিসোড %1$s-%2$s + এপিসোডবোৰ লোড কৰিবলৈ ৰিফ্ৰেচ কৰক। ব্যক্তিগত প্লেলিস্টৰ বাবে একাউণ্টৰ এক্সেছৰ প্ৰয়োজন; তাৰ বুকমাৰ্কটো এতিয়াও YouTube-ত খোল খাব + YouTube-এ এই এপিসোডটো খুলিব নোৱাৰিলে + YouTube-এ এই প্লেলিষ্টটো খুলিব নোৱাৰিলে + এটা এপিসোড বাছনি কৰক + সৰ্বশেষ আপডেট + কোনো playable এপিসোড নাই + সংৰক্ষিত ভিডিঅ\' · %1$s + চিৰিজটো অনুসৰণ কৰক + চিৰিজটো চাওক + আৰু আগবাঢ়ক + ইয়াতকৈ আৰম্ভ কৰক + আৰম্ভণিৰ বিন্দু সংৰক্ষিত হৈছে + ইয়ালৈকে চাইছোঁ + এপিসোডসমূহ চাইছোঁ বুলি চিহ্নিত কৰা হৈছে + বাতিল কৰক + সক্ৰিয় কৰক + %1$s টা নতুন এপিসোড + এই ডিভাইচত প্ৰগতি ৰেকৰ্ড কৰক + "আপোনাৰ ছিৰিজ আৰু প্ৰগতি এই ডিভাইচটোত থাকিব, যিবোৰ বিভিন্ন YouTube একাউণ্টৰ মাজত ভাগ কৰি লোৱা হয়। YouTube ইতিহাস নিয়ন্ত্ৰকবোৰে ইয়াক সলনি নকৰে। ছিৰিজ মেনুৰ পৰা প্ৰগতি মুছক। + +স্বয়ংক্ৰিয় ৰেকৰ্ডিং ঐচ্ছিক। ইনকগনিটোত বা একাউণ্ট সলনি হ’লে ই বন্ধ হৈ যায়" + এপিসোডবোৰ লোড কৰিব নোৱাৰিলে। আপুনি প্লেলিষ্টটো বুকমাৰ্ক হিচাপে সংৰক্ষণ কৰিব পাৰে + অসংৰক্ষিত পৰিৱৰ্তনৰ সংখ্যা অতি বেছি। প্ৰথমে সংৰক্ষণ কৰিবলৈ পুনৰ চেষ্টা কৰক + সংৰক্ষণ কৰাৰ প্ৰক্ৰিয়া চলি আছে। পুনৰ চেষ্টা কৰক + এপিসোড লোড কৰাৰ প্ৰক্ৰিয়া চলি আছে। পুনৰ চেষ্টা কৰক + ষ্টোরেজ উপলব্ধ নহয়। আপোনাৰ সংৰক্ষিত ডাটা ৰখা হৈছে + পৰিবৰ্তনসমূহ সংৰক্ষণ কৰিব পৰা নগ\'ল। YouTube বন্ধ কৰাৰ আগতে পুনৰ সংৰক্ষণ কৰিবলৈ চেষ্টা কৰক + এই ছিৰিজটো ইতিমধ্যে সংৰক্ষণ কৰা হৈছে, অথবা ইয়াৰ প্লেলিষ্ট সলনি হৈছে। ইয়াক পুনৰ খোলক + এই ছিৰিজটো সলনি হৈছে বা আঁতৰাই দিয়া হৈছে। লাইব্ৰেৰীটো পুনৰ খোলক + প্লেলিষ্টটো সলনি হৈছে। এপিসোডটো পুনৰ বাছনি কৰক + এই এপিসোডটো উপলব্ধ নহয়। আন এটা বাছনি কৰক + এটা ছিৰিজৰ নাম লিখক + এটা YouTube প্লেলিষ্ট লিংক বা প্লেলিষ্ট ID লিখক + পুনৰ ৰিফ্ৰেছ কৰাৰ আগতে অনুগ্ৰহ কৰি অপেক্ষা কৰক + এপিসোডবোৰ লোড কৰিব পৰা নগ\'ল। পিছত পুনৰ চেষ্টা কৰক + পৰিৱৰ্তনটো সম্পূৰ্ণ কৰিব পৰা নগ\'ল। পুনৰ চেষ্টা কৰক + ৰিজুম নিশ্চিত কৰিব পৰা নগ\'ল। প্লেয়াৰটো পৰীক্ষা কৰক আৰু পুনৰ চেষ্টা কৰক + ফলো কৰা ছিৰিজ + এই ভিডিঅ\'টো এই প্লেলিষ্টত নাই + প্লেলিষ্টবোৰৰ সন্ধান চলিছে… + প্লেলিষ্ট লিংকটো পেষ্ট কৰক + প্লেৰিলিষ্ট এটা বিচাৰি পোৱা নগ\'ল + কোনো এপিসোড উপলব্ধ নাই + চাবুৰকৈ দেখাটো লুকুৱাই থওক + পূৰ্বৰ এপিসোডবোৰ দেখা বুলি চিনাক্ত কৰক + আৰম্ভ হয়: %1$s + এপিসোডৰ ক্ৰম + প্লেলিষ্টৰ ক্ৰম + প্লেলিষ্টৰ ক্ৰম সলনি কৰক + %1$s ভিউ + %1$s ভিউ + এপিসোডসমূহ দেখা বুলি চিহ্নিত কৰক + পৰিসীমাৰ অন্তৰালত অন্ততঃ %1$s%% চাইছে আৰু সৰ্বোচ্চ ৯০ ছেকেণ্ড বাকী আছে, অথবা %2$s ছেকেণ্ড বাকী আছে + যিকোনো এটা সীমাৰ পিছত এপিসোডটো স্বয়ংক্ৰিয়ভাৱে চাইছে বুলি চিহ্নিত কৰক। শতাংশৰ নিয়মটোৰ বাবে ৯০ ছেকেণ্ডতকৈ অধিক বাকী থাকিব নালাগে, যাতে দীঘল এপিসোডৰ শেষৰ অংশটো বাদ নিযায়। বাকী থকা সময়ৰ নিয়মটো এক মিনিটতকৈ অধিক দীঘল ভিডিঅ\'ৰ ক্ষেত্ৰত প্ৰযোজ্য হয়; ইয়াক নিষ্ক্ৰীয় কৰিবলৈ ০ কৰক। পৰিৱৰ্তনবোৰ ভৱিষ্যতৰ প্ৰগতিৰ আপডেটৰ ক্ষেত্ৰত প্ৰযোজ্য হ\'ব। ম্যানুৱেলভাৱে চাইছে/চৈ নাথাকে বুলি লোৱা পছন্দবোৰ ৰখা হ\'ব + চৈ থকা শতাংশ (১-১০০) + বাকী থকা ছেকেণ্ড (০-৩০০; ০ ই নিষ্ক্ৰীয় কৰে) + %1$s ৰ পৰা %2$s লৈ এটা মান অন্তৰ্ভুক্ত কৰক + YouTube প্ৰগতি ব্যৱহাৰ কৰক + আনটো ডিভাইচত চোৱাৰ পিছত অনুসৰণ কৰা ছিৰিজটো পুনৰ আৰম্ভ কৰক + বৰ্তমানৰ YouTube একাউণ্টৰ পৰা আপুনি অনুসৰণ কৰা ছিৰিজৰ পচিল ষ্টেটাচ আৰু ৰিজুম পজিশন পঢ়ক। কেৱল মিল থকা এপিসোডবোৰ এই ফোনত সংৰক্ষণ কৰা হয়। ছিৰিজ লাইব্ৰেৰী খোলা থাকোতে আৰু Continue কৰাৰ আগতে চেকবোৰ চলি থাকে। আপোনাৰ আন ডিভাইচত YouTube ইতিহাস সক্ৰিয় হ\'ব লাগিব + YouTube প্ৰগতি পৰীক্ষা কৰা হৈছে… + YouTube প্ৰগতি উপলব্ধ নহয়। সংৰক্ষিত প্ৰগতি ব্যৱহাৰ কৰা হৈছে + কিছুমান YouTube প্ৰগতি উপলব্ধ নহয়। সংৰক্ষিত পজিশনবোৰ ৰখা হৈছে + বেকআপ আৰু ৰিস্টোৰ + আপোনাৰ ছিৰিজ লাইব্ৰেৰীটো এটা ফাইলত সংৰক্ষণ কৰক বা বেকআপ ৰিস্টোৰ কৰক + অনুসৰণ কৰা ছিৰিজ, এপিসোড পজিশন আৰু পচিল পছন্দবোৰ সংৰক্ষণ কৰে। ৰেকৰ্ডিং আৰু YouTube প্ৰগতিৰ অনুমতিসমূহ ইয়াত অন্তৰ্ভুক্ত নহয় + বেকআপ সংৰক্ষণ কৰক + ব keepsake বৰ্তাই আনক + %1$s ছিৰিজ যোগ কৰক। আপোনাৰ লাইব্ৰেৰীত ইতিমধ্যে থকা %2$s ছিৰিজ ৰখা হৈছে। বিদ্যমান ভিডিঅ\' প্ৰগতি ৰখা হৈছে + চিৰিজ বেকআপ সংৰক্ষিত হৈছে + %1$s ছিৰিজ পুনৰুদ্ধাৰ কৰা হৈছে + এই ফাইলটো এটা সমৰ্থিত ছিৰিজ বেকআপ নহয়, অথবা ই অতি ডাঙৰ। আপোনাৰ লাইব্ৰেৰীটো সলনি কৰা হোৱা নাই + বেকআপটো সংৰক্ষণ কৰিব নোৱাৰিল। অন্য স্থান চেষ্টা কৰক + চিৰিজটো আঁতৰাই দিয়া হৈছে + এই এপিসোডটো এতিয়া একাধিকবাৰ দেখা গৈছে। কোনটোৰ পৰা আগবাঢ়িব বিচাৰে সেইটো বাছনি কৰক + আপোনাৰ সংৰক্ষণ কৰা এপিসোডটো এই প্লেলিষ্টত আৰু নাই। আপোনাৰ সংৰক্ষিত স্থানটো ৰখা হৈছে + এপিসোড %1$s · %2$s ৰ সৈতে আগবাঢ়ক + আপুনি তলত যিকোনো এপিসোডো বাছনি কৰিব পাৰে। আগৰ এপিসোডবোৰ সলনি কৰা হোৱা নাই + এই প্লেলিষ্টটো সলনি হৈছে। ক’ৰ পৰা আগবাঢ়িব বিচাৰে সেইটো বাছনি কৰিবলৈ ছিৰিজটো খোলক + এই দিশত আৰু কোনো এপিসোড নাই + %1$s অব্যাহত ৰাখক + ছিৰিজ খোজোৱা + খোজোৱা মচি পেলাওক + কোনো ছিৰিজ পোৱা নাগিল + শিৰোনামবিহীন এপিসোড + অপ্রাপ্য এপিসোড + Niconico মন্তব্যসমূহ + লাইভ চেট আৰু চেট ৰিপ্লেকক ভিডিঅ\'ৰ ওপৰেৰে গৈ থকা মন্তব্য হিচাপে দেখুৱাওক + ভিডিঅ\' প্লেয়াৰত এটা অভাৰলে বটൺ দেখুৱায়। মন্তব্যসমূহ দেখুৱাবলৈ বা লুকুৱাবলৈ ট্যাপ কৰক, দ্ৰুত ছেটিংছৰ বাবে ট্যাপ কৰি ধৰি ৰাখক + মন্তব্যৰ ফণ্টৰ আকাৰ + মন্তব্যৰ পাঠৰ আকাৰ (১২-৪০ sp) + মন্তব্যৰ অপাচিটি + মন্তব্যৰ পাঠ আৰু পৃষ্ঠভুমীৰ অপাচিটি + ভ্ৰমণৰ সময়সীমা + মন্তব্যসমূহে স্ক্ৰীনখন পাৰ হ’বলৈ কিমান ছেকেণ্ড সময় লয়। কম মানৰ বাবে অধিক দ্ৰুতগতিত চলে + মন্তব্যৰ সৰ্বোচ্চ দৈৰ্ঘ্য + ইয়াতকৈ অধিক আখৰ থকা মন্তব্যবোৰ লুকুৱাই ৰাখে। প্ৰতিটো ইমোজিক এটা আখৰ হিচাপে গণ্য কৰা হয় + কোনো সীমা নাই diff --git a/patches/src/main/resources/addresources/values-az-rAZ/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-az-rAZ/shared-youtube/strings.xml index cbc0dbe000..6b9047cf75 100644 --- a/patches/src/main/resources/addresources/values-az-rAZ/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-az-rAZ/shared-youtube/strings.xml @@ -18,10 +18,12 @@ Second \"item\" text" --> - Başlanğıc animasiyası - Qapalıdı - Rəng - Saya + Başlanğıc animasiyası + Qapalıdı + Rəng + Morphe-ni Rənglə + Saya + Saya Morphe Tənzimləməni bitirmək üçün yenidən başlat diff --git a/patches/src/main/resources/addresources/values-az-rAZ/youtube/strings.xml b/patches/src/main/resources/addresources/values-az-rAZ/youtube/strings.xml index 0826cfdb36..3e88733268 100644 --- a/patches/src/main/resources/addresources/values-az-rAZ/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-az-rAZ/youtube/strings.xml @@ -837,8 +837,6 @@ Bunu düzəltmək üçün 'Video yayımlarını saxtalaşdır'-ı aktivləşdir Heç vaxt göstərmə Səsli olduqda göstər Səssiz olduqda göstər - Avtomatik titrləri göstər - Avtomatik titrləri gizlət Üstünlük verilmiş titr dili Əvvəlcə təminatçının altyazılarını seçir, sonra avtomatik üstünlük verilən dilə tərcümə edir @@ -1111,6 +1109,23 @@ Ayarlar→ Oxunuş (və ya Avtomatik oxunuş) → Növbəti videonu avtomatik ox Telefon Planşet Avtomobil + "Dəyişikliklər ehtiva edir: + +Planşet tərtibatı +• İcma postları gizlədilir +• Axınlarda oynatma seçimi mövcud deyil +• Shorts-da Remiks düyməsi və Səs düyməsi mövcud deyil +• \"Daha çox video\" örtüyü mövcud olmaya bilər +• Yığılmamış köhnə video fəaliyyət cizgisi göstərilir +• Açılan menyu elementləri düzgün işləməyə bilər + +Avtomobil tərtibatı +• Axın mövzular və kanallara görə qruplaşdırılır +• Axınlarda oynatma seçimi mövcud deyil +• Shorts-da Remiks düyməsi və Səs düyməsi mövcud deyil +• \"Daha çox video\" örtüyü mövcud olmaya bilər +• Shorts müntəzəm oynadıcıda açılır +• Yığılmamış köhnə video fəaliyyət cizgisi göstərilir" Oynadıcıda planşet tərtibatın aktivləşdir "Köhnə oynadıcı tərtibatını bərpa edir @@ -1233,6 +1248,7 @@ Məhdudiyyət: 'Köhnə video fəaliyyət cizgisini bərpa et' qapadılıbsa, 'D Üst cərgələri örtən status panelini düzəlt Status zolağının tərtibatını sabit saxlayır, beləliklə o, yuxarı zolaqları örtmür və oynadıcı yuxarısında boş yer saxlamır. Tətbiq çöksə və ya yuxarı zolaqlar yanlış görünsə, bunu qapat + Parlama İşıqlı kolorit fonu Ağ Material You (Ağ) @@ -1255,6 +1271,9 @@ Məhdudiyyət: 'Köhnə video fəaliyyət cizgisini bərpa et' qapadılıbsa, 'D Videonun çəkmə cizgisinin ikinci dərəcəli rəngini təyin et Etibarsız çevirmə cizgisi rəng dəyəri + Orijinal analoq + Orijinal tort + Orijinal metal Sərlövhə simvolu İlkin @@ -1622,4 +1641,13 @@ Məhdudiyyət: Köhnə üslub üçün \"Daha çox video\" örtüyünə qayıdır Niconico şərhləri Canlı söhbəti və təkrar oxudulan söhbəti video üzərində axan şərhlər kimi göstər Video oynadıcıda örtük düyməsini göstərir. Şərhləri göstərmək və ya gizlətmək üçün toxun, sürətli tənzimləmələr üçün basıb saxla + Şərh şriftinin ölçüsü + Şərhlərin mətn ölçüsü (12-40 sp) + Şərhin qeyri-şəffaflığı + Şərh mətninin və arxa planların qeyri-şəffaflığı + Səyahət müddəti + Şərhlərin ekrandan keçməsi üçün tələb olunan saniyə dəyəri. Kiçik dəyərlər daha sürətli hərəkət edir + Maksimum şərh uzunluğu + Bundan çox simvolu olan şərhləri gizlədir. Hər bir emoji bir simvol kimi sayılır + Limitsiz diff --git a/patches/src/main/resources/addresources/values-be-rBY/music/strings.xml b/patches/src/main/resources/addresources/values-be-rBY/music/strings.xml index 1da0942ff0..a14d354f4a 100644 --- a/patches/src/main/resources/addresources/values-be-rBY/music/strings.xml +++ b/patches/src/main/resources/addresources/values-be-rBY/music/strings.xml @@ -17,63 +17,677 @@ Second \"item\" text" --> + Апошні прайграны + Падпіскі + Відэа + Іншыя вынікі + Загружаныя песні + Загружаныя альбомы + Загружаныя выканаўцы + Абнавіце патч Jam хоста, каб выкарыстоўваць паўзу і ўзнаўленне + Супольнае выкарыстанне чаргі Jam + Станьце хостам або далучайцеся да бліжэйшага Jam і рэдагуйце яго чаргу праз Jam Layer + Уключыць супольнае выкарыстанне чаргі Jam + Уключае інтэграцыю Jam пасля перазапуску YouTube Music + Загрузіце Jam Layer для працы + Адкрыйце апошні рэліз Jam Layer у вашым браўзеры + Кіраванне Jam + Станьце хостам або далучайцеся да бліжэйшага Jam і кіруйце сесіяй + Пакет Jam Layer + Выберыце ўсталяваную дапаможную праграму, якая выкарыстоўваецца для размеркавання чаргі паблізу + Выкарыстоўвайце ўсталяваны дапаможны пакет. Спарынг патрабуе вашага пацверджання і лакальнага токена магчымасцяў + Уключыць Wi-Fi? + Уключыце Wi-Fi, а затым паспрабуйце зноў запусціць Jam або далучыцца да яго + Адкрыць налады Wi-Fi Адмяніць + Выкарыстаць пакет + Увядзіце правільнае імя пакета Android + Пакет захаваны. Настройце Jam Layer для падключэння + Jam адключаны + Спачатку ўключыце Jam Layer + Уявіце размер чаргі Jam у наладках, пасля чаго перазапусціце YouTube Music + Спачатку ўсталюйце выбраны пакет Jam Layer + Увядзіце код, паказаны на хосце. Wi-Fi Aware можа падключаць прылады побач, нават калі іх сеткі LAN розныя + Далучыцца з кодам + Далучыцца + Сканаваць QR + Увядзіце васьмізначны код або ўстаўце запрашэнне + Далучыцеся да вашага Jam + Капіяваць код + Код джэма + Паказаць QR + Гатова + Запрасіце ў свой джэм + Сканавайце, каб далучыцца і рэдагаваць гэтую чаргу. Завяршэнне Jam адклікае запрашэнне + Капіяваць спасылку + Запрашэнне ў джэм + Злучэнне з джэмам аднаўляецца; паспрабуйце яшчэ раз праз хвілінку + Дададзена ў джэм + Джэм недаступны; трэк не быў дададзены + Ваш джэм + Слухаем разам + Прыяднанне да джэма + Слухаць разам + Абнаўленне джэма… + Пошук вашага хоста ў Wi-Fi… + Ачаківанне людзей + падключана чалавек: %s + падключана чалавек: %s + Увядзіце гэты код на прыладнасці побач. Wi-Fi Aware працуе ў розных сетках LAN, калі гэта падтрымліваецца. Тэрмін дзеяння скончыцца праз %s хвілін + Пераматыць да %s? + Рэдагаванне адкрыта + Рэдагаванне заблакавана + Падключана праз Wi-Fi Aware + Падключана праз Wi-Fi + Падключана праз Bluetooth + Не падтрымліваецца падчас Jam + Далучэнне скасавана + Перападключэнне да вашага хоста… + Падзяліцеся чаргой. Дазвольце кожнаму дадаваць песні + Запрасіць людзей + Госці могуць рэдагаваць чаргу + Завяршыць Jam + Гасць прайгравае музыку. Дадавайце, перамящайце або выдаляйце песні прама ў чарзе YouTube Music + Трымайце абодва прылады побач. Вы можаце скасаваць працэс, пакуль мы шукаем гасця + Скасаваць далучэнне + Пакінуць джэм + Пачаць джэм + Наладзіць слой джэма + Кіраваць + Падрабязнасці + Адкрыць + Загрузка параметров песні… + Параметры песні недаступныя; паспрабуйце яшчэ раз + Не ўдалося адкрыць параметры песні + Браўзер для спампоўкі слоя джэма недаступны + Очакаванне ў чарзе гаспадара + Спачатку адкрыйце песню ў YouTube Music + Праграваць на хосце + Выйсці з джэма і праграваць лакальна + Праграваць наступнае ў джэме + Дадаць у чаргу джэма + Праграваць гэты трэк джэма + Выбраць, дзе праграваць + Перанесці хоста сюды + Песня яшчэ не гатова да пераматывання + Чарга змянілася; паспрабуйце жэст зноў + Логотып загалоўка + Звычайны + Карыстальніцкае + Праграваць аўдыятрэкі альбома + Прагравайце толькі аўдыёверсію песень з альбома замест музычнага відэа. Патрабуецца «Падаробіць відэапатокі» + Уключаць пераключальнік аўдыё/відэа + Спрабуйце пераключыць версію аўдыятрэка і афіцыйны музычны кліп + Спампоўвайце музыкальныя кліпы, падкасты або песні з дапамогай старонніх праграм для спампоўкі + Выкарыстоўваць знешні загрузчык для меню \"Спампаваць\" + Пункт \"Спампаваць\" у меню будзе адкрываць ваш знешні загрузчык + Загрузкі + "Няма загрузкі +Загружаныя трэкі з'яўляюцца тут" + %1$s • %2$s • %3$s МБ + %1$s трэкаў • %2$s МБ + Пошук у загрузках + Сартаваць + Сартаваць па выканаўцы + Сартаваць па назве + Сартаваць па памеры + Выдаліць усе загрузкі + Выдаліць усе загрузкі? + Усе загружаныя трэкі будуць выдалены з гэтага прылады + Загрузкі выдалены + Нічога не знойдзена + Дзеянні для %s + Праграваць + Відіўціць або прыпыніць + Папярэдні + Наступны + Прыпыніць Закрыць Выдаліць + Выдаліць спампавацьны файл + Выдаліць спампавацьны файл? + %s будзе выдалены з гэтага прылады + Загрузка выдалена + Не ўдалося выдаліць загрузку + Не ўдалося прайграць загрузку + Загрузка аўдыя + Афлайн-прайграванне + Не ўдалося ідэнтыфікаваць трэк для загрузкі + Загрузка ўжо выконваецца + Ужо загружана + Загрузка пачата + Пачатак загрузкі… + Загрузка %s%% + Загрузка завершана + Загрузка памылковая + Пра Morphe + Рэклама + Струя + Агульныя + Плеер + Тэксты песень + Разнастайное + Мова YouTube Music + Сістэмная мова + Патрабуецца Android 13 або больш новая версія + Схаваць надпіс \"Набуць Music Premium\" + Схаваць прапановы Music Premium + Схаваць відэарэкламу + Схаваць пераключальнік \"Аудыё / Відэа\" + Схаваць кнопку «Спампаваць» + Схаваць кнопку «Спампаваць» у падкасце + Схаваць паліцу \"Explore\" + Схаваць панэль фільтраў + Схаваць сеткавыя паліцы + Схаваць гарызантальныя паліцы + Схаваць спісавыя паліцы + Панэль тэкстаў + Схаваць кнопку \"Паділіцца\" + Схаваць кнопку \"Перакласці\" + Схаваць паліцу \"Новае ад\" + Схаваць паліцы плейлістаў + Схаваць кнопку \"Паўтор\" + Схаваць кнопку \"Пемеш\" + Схаваць паліцу \"Хуткі набор\" + Каментары + Выберыце, якія кампаненты будуць адлюстроўвацца ў раздзеле каментараў + Схаваць правілы супольнасці + Схаваць кантэкстнае паведамленне + Схаваць кнопку эмодзі + Схаваць кнопку інфармацыі + Схаваць кнопку часавай меткі + Кросфейд + Плаўна змешвайце трэкі адзін з адным + Уключаць кросфейд + Крывая затухання + Рівнамерная магутнасць + Мяккая затрымка + Плаўны пераход + Плаўная S-крывая + Выхадны + Уваходны + Працягласць кросфейду + 250 мілісекундаў + 500 мілісекундаў + 750 мілісекунд + 1 секунда + 2 секунды + 3 секунды + 4 секунды + 5 секунд + 6 секунд + 7 секунд + 8 секунд + 9 секунд + 10 секунд + 11 секунд + 12 секунд + Кросфейд пры ручным пераскоку + Уключае эфект кросфейду, калі вы пераскакуеце на наступны трэк уручную + Кросфейд пры аўтаматычным пераходзе + Прымяняе эфект кросфейду, калі трэк заканчваецца натуральным чынам + Кіраванне сесіяй + Націсніце і ўтрымайце кнопку перамешавання, каб прыпыніць або ўзнавіць кросфейд + Кросфейд прыпынены для гэтай сесіі + Кросфейд Regardless resumed + Відэорэжым недаступны, пакуль уключаны кросфейд + Кросфейд выключаны пры працягненні SABR + Кросфейд выключаны падчас трансляцыі + Як гэта працуе + "Кросфейд накладае канец адной песні на пачатак наступнай, выкарыстоўваючы вашу працягласць і крывую + +Ён працуе пры аўтаматычным пераходзе і пры ручным прапуканні, кожны з якіх мае ўласны пераключальнік. Пры аўтаматычным пераходзе змешванне пачынаецца за некалькі секунд да заканчэння песні" + Абмежаванні + "Музыкальныя відэа працуюць толькі як аўдыё, пакуль уключаны кросфейд. Зацісніце кнопку перамешавання, каб паўстаноўіць кросфейд і паглядзець відэа + +Музыкальнае відэа, якое пачынае працягненне, застаецца ў рэжыме відэа да наступнай песні + +Песні, працягласць якіх меншая за працягласць заціхання, не праходзяць праз кросфейд + +Блакаванне экрана або паведамленне можа на кароткі час паказваць састарэлы стан пасярэдзін пераходу + +Паўза кросфейду для прагляду музыкальнага відэа можа прывесці да таго, што некаторыя відэа будуць чорнымі. Каб пазбегнуць гэтага, пастаўце кросфейд на паўзу перад выбарам песні" + Не падтрымліваецца + "Трансляцыя або дубляванне аўдыё (Chromecast, дубляванне экрана або аўдыё) + +Працягненне SABR (імітацыя відэапатокаў выключана)" + Аўтарства + Функцыя кросфейду ад VazerOG + Запамятаць стан паўтору + Запамятаць стан перамешавання + Хуткасць прайгравання + Хуткасць, якая выкарыстоўваецца для ўсіх трэкаў. Пры 100% выкарыстоўваецца хуткасць праграмы, напрыклад, хуткасць прайгравання падкастаў + Змяняць вышыню тону разам з хуткасцю + Адключаць перанакіранне пры адмове\" + Запабігае пераскаку на наступны трэк пры націсканні кнопкі \"не падабаецца\" + Дзеянне пры націсканні на трэк у плэйлісце + Праіграць плэйліст + Праіграць толькі выбраны трэк + Дадаць выбраны трэк у чаргу + Дададзена ў чаргу + Схаваць кнопку Cast + Схаваць кнопку \"Гісторыя\" + Схаваць кнопку \"Апавешчэнні\" + Схаваць кнопку \"Пошук\" + Схаваць кнопку галаснага пошуку + Схаваць кнопку пошуку па гуку + Схаваць кнопку \"Новае\" + Схавае кнопку \"Новае\" ў ўкладцы \"Бібліятэка\" + Кнопкі дзеянняў + Выберыце, якія кнопкі дзеянняў будуць паміжаць пад прагравальнікам + Схаваць панэль дзеянняў + Схаваць кнопкі \"Падабаецца\" і \"Не падабаецца\" + Кнопка «Схаваць каментары» + Схаваць кнопку «Паўтор чата ў прамымэфіры» + Схаваць кнопку \"Тэкст песні\" + Схаваць кнопку \"Паділіцца\" + Схаваць кнопку \"Захаваць\" + Схаваць кнопку \"Спампаваць\" + Схаваць кнопку «Запусціць радыё» + Выпаднае меню + Выберыце, якія кампаненты будуць адлюстроўвацца ў выпадаючых меню прайгравача і чаргі + Схаваць 3-калонкавы кампанент + Схаваць кнопкі \"Падабаецца\" і \"Не падабаецца\" + Схаваць меню \"Супаданне смакаў\" + Схаваць пункт меню \"Дадаць у плэйліст \'Паслухаць пазней\'\" + Схаваць пункт меню \"Дадаць у чаргу\" + Схаваць меню \"Субтытры\" + Схаваць пункт меню \"Выдаліць плэйліст\" + Схаваць пункт меню \"Ачысціць чаргу\" + Схаваць меню \"Не рэкамендаваць гэтага выканаўцу\" + Схаваць меню \"Спампаваць\" + Схаваць меню \"Рэдагаваць плэйліст\" + Схаваць меню \"Перайсці да альбома\" + Схаваць меню \"Перайсці да выканаўцы\" + Схаваць меню \"Перайсці да эпізода\" + Схаваць меню \"Перайсці да падкаста\" + Схаваць меню \"Дапамога\" + Схаваць меню \"Пазначыць эпізод як праслуханы\" + Схаваць меню \"Не цікава\" + Схаваць пункт меню \"Закрэпіць у хуткім наборы\" + Схаваць пункт меню \"Праіграць наступным\" + Схаваць меню якасці + Схаваць пункт меню \"Выдаліць з бібліятэкі\" + Схаваць пункт меню \"Выдаліць з плэйліста\" + Схаваць меню скаргаў + Схаваць меню \"Захаваць эпізод на пазней / Захаваць у бібліятэку\" + Схаваць меню \"Дадаць у плэйліст\" + Схаваць меню паділу + Схаваць меню \"Выпадковае прайграванне\" + Схаваць меню \"Таймер сну\" + Схаваць меню \"Запусціць радыё\" + Схаваць меню \"Статыстыка для гікоў\" + Схаваць меню падпіскі + Схаваць меню \"Адпанаваць ад хуткага набору\" + Схаваць меню \"Паказаць аўтараў песні\" + Стартэрная старонка + Звычайны + Дыяграмы + Эпізоды на пазней + Даследаванне + Гісторыя + Бібліятэка + Пастаўленая музыка + Плэйлісты + Пошук + Падпіскі + Прымусовая партатная арыентацыя + Захоўвае праграму ў партатнай арыентацыі пры павароце прылады + Змяніць колер мініплэера + Падагнаць колер мініплэера да колеру плэера на ўвесь экран + Змяніць колер панэлі навігацыі + Падагнаць колер панэлі навігацыі пад паўнаэкранны прайгравач + Паказваць кнопку \"Далей\" ў мініплэеры + Паказваць кнопку \"Папярэдні\" ў мініплэеры + Уключаць прымусовы мініплэер + Забараніць плэеру аўтаматычна разгортвацца на ўвесь экран пры пачатку новага трэка + Уключаць свайп для закрыцця мініплэера + Дазваляе закрыць мініплэер, свайпаючы яго ўніз + Панэль навігацыі + Выберыце, якія кнопкі будуць адлюстроўвацца ў ніжняй панэлі навігацыі + Схаваць кнопку \"Галоўная\" + Схаваць кнопку \"Урыўкі\" + Схаваць кнопку \"Даследаванне\" + Схаваць кнопку \"Бібліятэка\" + Схаваць кнопку \"Палепшыць\" + Схаваць панэль навігацыі + Схаваць подпісы кнопак навігацыі + Не аўтарызаваны. Націсніце, каб наладзіць + Выйсці + Выйшли з уліковага запісу + Уключыць scrobbling + Перадаваць статус \"Зараз гучыць\" + Мінімальная працягаванне песні + Песні карацей за гэты час не будуць scrobble + Працэнт затрымкі scrobble + Запусціць scrobble пасля праслухоўвання гэтага працэнта песні + Затрымка scrobble ў секундах + Запусціць scrobble пасля праслухоўвання ў indicated колькасці секунд + Уліковы запіс ListenBrainz + Уваход здзейснены. Націсніце, каб кіраваць уліковым запісам + Увядзіце ваш токен карыстальніка ListenBrainz, каб уключыць scrobbling + Токен + Праверыць + Калі ласка, увядзіце токен + Праверка… + Сапраўдны токен! Карыстальнік: %s + Няправільны токен: %s + невядомы + Праверка не ўдалася: %s + Токен ачышчаны + Токен захаваны + Папярэджанне: Захаваны токен няправільны! + Дадаваць у \"Любімае\" пастаўленыя лайкі + Аўтаматычна дадаваць трэкі ў \"Любімае\" на Last.fm, калі вы ставіце ім лайк у YouTube Music (і выдаляць з \"Любімага\", калі лайк прыбраны або пастаўлены дызлайк) + Уліковы запіс Last.fm + Увайшли як %s. Націсніце, каб кіраваць уліковым запісам + Увядзіце свае даныя Last.fm, каб увайсці + Імя карыстальніка + Пароль + Увайсці + Калі ласка, увядзіце імя карыстальніка і пароль + Уваход… + Паспяховы ўваход! + Не ўдалося атрымаць сесію ад Last.fm + Памылка ўваходу: %s + Даныя Last.fm захаваны! + Ачыстка метаданых + Ачысціць метаданыя + Аўтаматычна ачысціць назвы, выканаўцоў і альбомы ад звычайнага лішняга тэксту (напрыклад, \"Official Video\", \"Remaster\", \"Live\") перад scrobbling + Спецыяльная ачыстка праз regex + Увядзіце спецыяльны рэгулярны выраз, каб выдаліць дадатковы тэкст з назваў песень, выканаўцоў або альбомаў. Прыклад: (?i)\\s*\\((clean|explicit)\\) + Вызначыць выканаўцу і трэк з назвы + Вылучаць імя выканаўцы і назву трэка з загалоўка відэа, выкарыстоўваючы раздзяльнік \" - \". Карысна для перазалітага кантэнту, дзе назва канала можа выкарыстоўвацца як імя выканаўцы + Запоўніць прапушчаны альбом + Калі музыка не ўключае альбом, шукайце яго на Last.fm, каб ваш scrobble быў поўным + "Scrobbling аўтаматычна сінхранізуе выкарыстанне YouTube Music з вашым профілем Last.fm або ListenBrainz + +Каб scrobbling сінхранізаваўся падчас працыўкавання ў фоне, пераканайцеся, што выкарыстанне зарада батарэі праграмай YouTube Music усталявана ў значэнне \"Без абмежаванняў\"" + Выкарыстоўваць тэксты іншых пастаўшчыкаў + Паказвае тэксты песень з паслугі трэціх асоб, выкарыстоўваючы ўбудаваная тэксты, калі іншыя не знойдзеныя + Трымаць экран уключаным пры праглядзе тэкстаў + Забягае выключаэння дысплея, пакуль адкрыта панэль з тэкстамі + Сэрвіс тэкстаў песень Адлюстраванне + Сінхранізацыя + Панэль + AI-конечная кропка + Усе пастаўшчыкі адключаны — убуяваны і ўбудаваны тэксты + Скінуць парадак правайдэраў? + Аднавіць правайдэраў да пачатковага парадка прыярытэтаў + Ачысціць захаваныя тэксты + Выдаліць тэксты, захаваныя на гэтай прыладнасці + Ачышчана %1$s МБ + Атрымаць токен + Apple Music Media-user-token + "Устаўце кукі media-user-token з music.apple.com + +Крокі: +1. Увайдзіце ў сістэму на music.apple.com як карыстальнік-член +2. Адкрыйце DevTools → Application → Cookies → music.apple.com +3. Скапіруйце значэнне \"media-user-token\"" + Apple Music: media-user-token захаваны + Apple Music: media-user-token ачышчаны + Apple Music: няправільны media-user-token - праверце і паспрабуйце зноў + Spotify SpDc + "Устаўце кукі sp_dc з Spotify. + +Крокі: +1. Увайдзіце ў сістэму на open.spotify.com +2. Адкрыйце DevTools $\rightarrow$ Application $\rightarrow$ Cookies $\rightarrow$ open.spotify.com +3. Скапіруйце значэнне \"sp_dc\"" + Spotify: sp_dc захавано + Spotify: sp_dc ачышчана + Spotify: няправільны sp_dc - праверце і паспрабуйце зноў + Токен Apple Music неабавязковы. Без яго будзе выкарыстоўвацца Lyrically як рэзервны варыянт + Deezer ARL + "Устаўце кукі arl з Deezer. + +Крокі: +1. Увайдзіце ў сістэму на deezer.com +2. Адкрыйце DevTools $\rightarrow$ Application $\rightarrow$ Cookies $\rightarrow$ deezer.com +3. Скапіруйце значэнне \"arl\"" + Deezer: arl захавано + Deezer: arl ачышчана + Deezer: няправільны arl - праверце і паспрабуйце зноў + Токен Musixmatch + Дадайце токен толькі для таго, каб выкарыстоўваць уласны рахунак. Ён павінен быць атрыманы з праграмы Musixmatch для Android: токены, скапіяваныя з musixmatch.com, належаць устарэламу дэсктопнаму кліенту, і API адхіляе іх + Musixmatch працуе без токена, таму гэтае поле можа застацца пустым + токен карыстальніка + Musixmatch: токен карыстальніка захаваны + Musixmatch: токен карыстальніка ачышчаны + Musixmatch: няправільны токен карыстальніка - праверце і паспрабуйце зноў + Cookies YouTube + "Устаўце вашы cookies YouTube, каб запытаць субтытры і часовы тэкст з YouTube. + +Крокі: +1. Адкрыйце youtube.com у браўзеры і ўвайдзіце ў сістэму +2. Адкрыйце DevTools (F12) → Application → Cookies → youtube.com +3. Скапіруйце ўвесь радок cookie (пары ключ=значэнне, аддзяленыя кропкай з коскай) + +Патрэбныя cookies: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPISID, LOGIN_INFO" + YouTube: кукі захаваны + YouTube: cookies ачышчаны + YouTube: няправільныя cookies - калі ласка, праверце і паспрабуйце зноў + Націсніце і ўтрымайце радок + Націсніце на радок, каб пераскочыць да гэтага моманту ў песні, зацісніце яго, каб скапіяваць слова пад пальцам або ўвесь радок, калі націсканне адбылося не на слове. Выключэнне прыбірае абодва жэсты + Паказваць тэгі секцый + Пазначайце кожную новую секцыю тэксту песні невялікім тэгам, напрыклад, «Куплет» або «Прыспеў» + Паказваць кнопку «Капіяваць» + Націсніце, каб скапіяваць арыгінальны тэкст як звычайны тэкст; зацісніце, каб захаваць выходны файл тэксту ў Загрузкі + Паказваць кнопку «Перакласці» + Націсніце, каб пераключаць пераклад; зацісніце для толькі перакладу. Выкарыстоўваюць пераклады пастаўшчыка, Google Translate або API, сумяшчальнае з OpenAI + Паказваць кнопку раманізацыі + Націсніце, каб пераключаць раманізацыю; зацісніце для толькі раманізацыі. Выкарыстоўваюць раманізацыю пастаўшчыка, Google Translate або API, сумяшчальнае з OpenAI + Локальная раманізацыя і выраўніванне + Раманізацыя на прыладзе, калі пастаўшчык, ваша мадэль і Google Translate не спрацавалі. Чытанне слідуе за кожным словам часовых тэкстаў песень; сінхранізацыя слоў павінна быць уключана. Кітайская, кантонская, японская, карейская, грэцкая, руская, грузінская, армянская мовы; прыкладна 120 МБ + Чытанне %1$s: %2$d%% + Чытанні гатовыя (%1$d запісаў) + Загрузка недакладна — выключыце і зноў уключыце гэтую функцыю, каб паўтарыць спробу + Паказваць кнопку абнаўлення + Націсніце, каб пераключыцца на наступны варыянт тэксту; доўгае націсканне — каб пашукаць тэкст па назве песні і выканаўцы + Пошук тэкстаў песень + Назва песні + Выканаўца + Увядзіце назву песні і выканаўцу + Ні адзін з уключаных пастаўшчыкаў не можа шукаць іншыя песні + Захавана + Выдзяляць сінхранізаваныя словы або склады + Аддаваць прыярытэт тэксту па словах, калі ён даступны. Калі тэкст мае часовыя меткі для слоў, кожнае слова або склад будзе выдзяляцца ў такт з музыкай + Паказваць бэк-вакал асобна + Пераносіць бэк-вакал, запісаны ў дужках, напрыклад, \"(Oh) I want you\", з асноўнага радка ў асобны радком з прыглушаным тэкстам + Схаваць праграныя радкі тэксту + Схаваць радкі тэксту вышэй за той, які праграваецца ў дадзены момант + Схаваць непраграныя радкі тэксту + Схаваць радкі тэксту ніжэй за бягучы + Паказваць тэксты MediaSession на экране блакіроўкі + Паказвае бягучы радок на экране блакіроўкі, у Android Auto і Bluetooth. Патрабуецца Android 11 або новее + Паказваць тэксты ў мініплэеры + Паказвае бягучы радок у мініплэеры, пры гэтым трэк пераносіцца ў субтытры + Фармат назвы песні/выканаўцы + Фармат трэка, які паказваецца разам з тэкстам у мініплэеры і MediaSession. {t} — гэта назва, {a} — выканаўца, а {v} — версія, напрыклад, Acoustic, калі крыніца тэксту яе прадастаўляе. Тэкст паміж дзвюма двукроп\'ямі паказваецца толькі тады, калі кожная пераменная ўнутры яго мае значэнне; пішыце \\: для звычайнага двукроп\'я. Кожны радок паказваецца па чарзе, змяняючыся разам з тэкстам песні + Фармат радка тэксту + Фармат радка тэксту песні ў мініплэеры і MediaSession. {lrc} — гэта радок, а {n} — колькі разоў паступу яго спяваюць. Тэкст паміж двукроп\'ямі паказваецца толькі пры паўторы радка. Выкарыстоўвайце \\: для двукроп\'я + Фармат павінен утрымліваць {lrc} + Шаблон радка выканаўцы і таймінг анімацыі + Мінімальны час паказу на экране (мс) + Максимальны час паказу на экране (мс) + Толькі для швельг-анімацый. Захоўвайце кожны кадр як мінімум мінімальны час. Пры дасягненні максімума радок выканаўцы прасуваецца, нават калі тэкст песні застаецца нязменным. + Мінімум павінен быць 0 або больш. Максімум павінен быць больш за 0 і не менш за мінімум. + Ліміты даўжыні радка + Ліміты радкоў тэксту песні і выканаўцы, а таксама вага паўнашырынных сімвалаў + "Ліміт радкоў тэксту +Разбівайце доўгія тэксты на збалансаваныя паловы. 0 адключае разбіўку." + "Ліміт радкоў выканаўцы +Разбівайце запаўнены фармат трэка на цыклічныя кадры. 0 адключае разбіўку." + "Вага шырокіх сімвалаў +Стыкаецца да абодвух лімітаў. Пры значэнні 1.75 чатыры шырокія сімвалы лічацца як 7." + Увядзіце цэлалікавыя ліміты ад 0 да 10000 і вагу сімвалаў больш за 0 + Інструментальная паўза + Паказваецца ў Мініплэеры і MediaSession падчас інструментальнага працягу. Кожны радок — гэта адзін кадр анімацыі. {t} — гэта назва, {a} — выканаўца, а {lrc} — апошні радок тэксту + Часовы зсуў Мініплэера і MediaSession + Змяняе час паказу наступнага радка ў Мініплэеры і MediaSession. Выкарыстоўвайце адмоўнае значэнне, калі радкі з\'яўляюцца з затрыманнем, напрыклад, на экране блакіроўкі + Фільтры + Regex метаданых + Рэгулярны выраз, які прымяняецца да назваў, выканаўцаў і альбомаў перад пошукам тэкстаў песень і пры ацэнцы супадзення двух назваў. Зазіўце пустым, каб адключаць абодва. Налады па gâyме выдаляюць звычайны шум, такі як \"Official Video\", \"Remaster\", \"【插曲】\" і \"- Topic\" + Regex фільтра тэкстаў + Рэгулярны выраз, які прымяняецца толькі да арыгінальнага тэксту песні (не да перакладаў або раманізацыі). Пакіньце пустым, каб адключыць + Маркеры інфармацыйных радкоў + Калі тэксты песень няправільна адфільтраваны ўніз, скарыгуйце або ачысціце гэта. Маркеры праз коску: радкі, якія ўтрымліваюць маркер, пасля якога ідзе двоўкрапка, паказваюцца як інфармацыя аб аўтарстве ўнізе. Застаўце поле пустым, каб адключыць + Пісаць першую літару з вялікай + Піша першую літару кожнага радка тэксту з вялікай + Выдаліць прыканцавую пунктуацыю + Выдаляе кропкі, коскі, двукропкі і паўкропкі ў канцы радкоў тэксту. Знакі пытання, клічнікі, троекрап\'я, цытаты і дужкі застаюцца + Памер тэксту + Памер тэксту песень + Глабальны зсуў часу + Сінхранізаваныя тэксты ідуць за прайграваннем. Калі яны расінхранізуюцца, скарыгуйце часовы зсуў. Перамясціце сінхранізаваныя тэксты наперад або назад. Выкарыстоўвайце станоўчае значэнне, калі тэксты ідуць выпераджваючы песню + Выкарыстоўваць сегменты SponsorBlock для зсуву тэксту + Сумесціць без сегментаў, якія прапускае SponsorBlock, і зсунуць тэкст па выпушчаным аўдыя. Выключаны рэжым цалкам ігнаруе SponsorBlock + Гэты трэк не мае id відэа для адпраўкі + Крыніца: %s + Зацісніце кнопку капіявання, каб захаваць гэты LRC; мы будзем ўдзячны, калі вы зможаце дакладна наладзіць таймінгі і паділіцца імі з грамадскімі пастаўшчыкамі + Перакладзена Google + Раманізавана Google + Перакладзена %s + Раманізавана %s + Раманізавана %s + Перакладзена + Мова перакладу + Мова раманізацыі + Пераклад... + Не ўдалося перакласці тэкст песні + Раманізавана Аўтаматычна + Раманізацыя… + Не удалось раманізаваць тэкст песні + Толькі пераклад + Толькі раманізацыя + Двойны рэжым + Скапіравана ў буфер абмену + Абнаўленне + Абнаўлена + API-ключ QuickLRC + QuickLRC транскрыбуе або выраўняе па часе тэксты для бягучага трэка. Асобны сэрвіс, які не звязаны з OpenAI-сумесным эндпоінтам вышэй + Ключ QuickLRC захаваны. Выкарыстоўвайце кнопку выраўняння ў дыялогу пошуку тэкстаў або паведамленне ў панэлі, калі ў трэка няма тэксту + API-ключ QuickLRC захаваны + QuickLRC не прыняў гэты ключ + API-ключ QuickLRC выдалены + QuickLRC вызначае час для тэкстаў, якіх няма, або транскрыбуе іх з трэка за кошт крэдытаў. Пры наладжаным ключы трэкі без тэкстаў прапануюць транскрыпцыю, а ў дыялогу пошуку з\'яўляецца кнопка выраўняння + Запытаць выраўнянне QuickLRC + Выраўнянне + Прымяніць выраўнянне + Запытаць транскрыпцыю QuickLRC + Транскрыбацыя + Транскрыбацыя скасавана + Транскрыбаваць можна толькі трэкі, якія прайграваюць з YouTube + Няма тэкстаў для выраўняння — замест гэтага выконваецца транскрыпцыя + QuickLRC не змог апрацаваць гэты трэк + Крэдыты QuickLRC для гэтага трэка вычарпаны + "Тэксты песень падабираюцца па назве і выканаўцы, незвычайныя метаданыя могуць прывесці да адсутнасці вынікаў або выбару няправільнай версіі. Выкарыстоўвайце абнаўленне, каб змяніць версію. Провайдэры, пазначаныя *, патрабуюць токена. Усе ўключаныя провайдэры не заўсёды запытваюцца ў вашай паслядоўнасці, а адказыўць залежнаць ад іх хуткасці.\n\nТэксты песень паступаюць з іншых сэрвісаў, якія не звязаны з Morphe, не спонсаруюцца і не падтрымліваюцца ім. Кожны з іх выкарыстоўваецца «як ёсць»: тэксты, іх даступнасць і ліцэнзаванне застаюцца за гэтымі сэрвісамі і іх ліцэнзіярами, а іх выкарыстанне можа рэгулявацца іх умовамі абслугоўвання. У ўключаны сэрвіс перадаецца толькі ваш запыт на пошук — назва, выканаўца і, калі гэта неабходна сэрвісу, ID відэа. Дапаможны AI-эндпоінт і транскрыпцыя QuickLRC працуюць толькі пасля таго, як вы іх наладзіце, і атрымліваюць тэкст песні або URL-адрес відэа трэка." + Уключайце толькі тыя крыніцы, якімі карыстаецеся, альбо пакіньце налады па змаўчанні - пастаянныя запыты могуць перагрузіць невялікія супольнасці сайтаў з тэкстамі песень + Схаваць інфармацыю аб аўтарах + Схаваць інфармацыю аб аўтарах, якая паказваецца пад тэкстам песні + Паменять месцамі пераклад і раманізацыю + ON ставіць раманізацыю пад тэкстам, а пераклад — над ім; OFF пакідае раманізацыю над тэкстам, а пераклад — пад ім + Пераклад + Раманізацыя + Пастаўшчык перакладу + Google (па змаўчанні, бясплатна) + Сумяшчальны з OpenAI (свой) + Пастаўшчык раманізацыі + Google (па deployment) + Локальны / па-афлайн слоўнік + Сумяшчальны з OpenAI (свой) + Секцыя ШІ + Выкарыстоўваецца толькі ў тым выпадку, калі правайдэр не перадае пазначэнняў секцый, такіх як прыспеў або куплет, якія затым вызначае ваша OpenAI-сумяшчальная мадэль + AI & хмарныя эндпоінтнты + Настройце API-ключы, мадэлі, эндпоінтнты і шаблоны прамптаў для пастаўшчыкаў ШІ. + URL-адрас эндпоінтнта API (напрыклад, https://api.openai.com/v1/chat/completions) + Шаблон прампта + Плейсхолдэры: {language}, {task}, {title}, {artist}, {count}, {lines} + Прампт захаваны + Запыт захаваны, але эндпоінт AI пакуль недаступны. Спачатку наладзьце яго ў Наладках AI + Прампт скінуты + Некарэктны JSON. Выправіце яго або ачысціце поле перад захаваннем + Налады перакладу & раманізацыі захаваны + Налады скінуты да пачатковых + Базавы URL павінен пачынацца з http:// або https:// + Праверыць злучэнне + Наладзіцьы канектныя пункты + Паказаць усе эндпоінты + Праверка злучэння… + Правіднік перакладу + Правіднік раманізацыі + Атрымаць мадэлі + Атрыманне мадэляў… + Выберыце мадэль + Не удалось атрымаць спіс мадэляў. Праверце API-ключ або падключэнне. + Налады Gemini API + Сумяшчальнасць з OpenAI (карыстальніцкі endpoint) + Налады DeepL API + API-ключ + Мадэль + Базавы URL + API-ключ (неабавязкова) + Промпт для перакладу + Дадатковае цела запыту (неабавязковы JSON) + Ключ аўтэнтыфікацыі (Free або Pro) + Настройце API-ключы, мадэлі, эндпоінты і прампты + 8.23.51 - Адключаць тлустыя іконкі + 9.13.51 - Васстанавіць укладкі «Далей», «Тэксты песень» і «Падобнае» diff --git a/patches/src/main/resources/addresources/values-be-rBY/reddit/strings.xml b/patches/src/main/resources/addresources/values-be-rBY/reddit/strings.xml index 8d434d72d7..30f6190034 100644 --- a/patches/src/main/resources/addresources/values-be-rBY/reddit/strings.xml +++ b/patches/src/main/resources/addresources/values-be-rBY/reddit/strings.xml @@ -16,8 +16,122 @@ Second \"item\" text" --> + Палітыка прыватнасці Reddit + Уваход праз Google недаступны. Выкарыстайце электронную пошту для ўваходу + Рэклама + Схаваць рэкламу ў стужцы + Схаваць рэкламу ў каментарыях + Панэль навігацыі + Схаваць кнопку \"Адказы\" + Схаваць кнопку \"Чат\" + Схаваць кнопку \"Стварыць\" + Схаваць кнопку \"Discover\" або \"Супольнасці\" + Схаваць кнопку \"Гульні\" + Бакавая панэль + Схаваць паліцу «Нядаўна наведаныя» + Схаваць гульні на паліцы Reddit + Схаваць паліцу Reddit Pro + Схаваць паліцу \"Пра нас\" + Схаваць паліцу рэсурсаў + Макет + Капіяваць тэкст + Дазваляе выбіраць і капіяваць тэкст у публікацыях і каментарыях. Адключае згортванне каментарыяў + Згортванне каментарыяў перастане працаваць, пакуль уключана функцыя \"Капіяваць тэкст\" + Уласны шрыфт + Загрузіць уласны файл шрыфта TTF або OTF па шлейху ніжэй. Уключанне гэтай функцыі таксама адключае «Сістэмны шрыфт» + Файл уласнага шрыфта + Націсніце, каб выбраць файл шрыфта TTF або OTF + Выбраны шрыфт - %s + Спачатку выберыце файл уласнага шрыфта + Калі ласка, выберыце файл шрыфта .ttf або .otf + Не ўдалося загрузіць карыстальніцкі шрыфт. Выберыце іншы файл + Адключаць сучасны інтэрфейс галоўнай старонкі + Адключыць ўсплывальнае акно скрыншота + Адключае ўсплывальнае акно, якое з\'яўляецца пры стварэнні скрыншота + Прымусіць сістэмны шрыфт + Выкарыстоўваць сістэмны шрыфт прылады замест Reddit Sans або Roboto. Уключанне гэтай функцыі таксама адключае «Уласны шрыфт» + Схаваць кнопку «Спытаць» + Схавае кнопку «Спытаць» у палі пошуку + Схаваць паліцу супольнасцей + Схаваць паліцу з павязанымі або рэкамендаванымі супольнасцямі ў сабрэдзітах + Схаваць паліцы \"Папулярнае\" + Хавае паліцы \"Папулярнае\" з стрычкі і прапаноў пошуку + Выдаліць дыялогавае акно з прапановай паведамленняў + Аўтаматычна закрывае дыялогавае акно з прапановай паведамленняў пры наведванні ў сабрэдыт + Паказваць флейры ў галоўнай стужцы + Паказвае значкі флейраў пастоў у галоўнай стужцы + Некаторыя элементы стужкі могуць не паказваць флейр + Паказваць колькасць праглядаў пастоў + Абмежаванне: даступна толькі ў выпадку, калі адказ API ўключае колькасць праглядаў + Перавярнуць панэль дзеянняў публікацыі + Пераносіць кнопкі галасавання і каментарыяў публікацый на правую сторону, а кнопкі кроспостанга і пашырэння — на левую + Памяняць месцамі кнопкі галасавання і каментарыяў + Паказваць кнопку каментарыяў публікацыі перад кнопкамі галасавання + Выдаліць дыялогавае акно з папярэджаннем NSFW + Аўтаматычна прымае папярэджанне аб кантэнце NSFW пры наведванні ў сабрэдыт + Разнастайнае + Пра нас + Версія патча і афіцыйныя спасылкі + Ачысціць спасылкі для падзелу + Выдаляе параметры адсочвання з падзеленых спасылак + Адкрываць спасылкі напрамую + Абход URL-адрасаў перанакіравання пры адкрыцці знешніх спасылак + Медыя ў стужцы на ўсю шырыню + Паказвае выявы і відэа ад краю да краю ў стужцы і дэталях паста, без бакавых адступаў і закругленых углоў + Інтэнсіўнасць зацірэння праглядальніка медыя + Інтэнсіўнасць цёмнага зацірэння за тэкстам пры праглядзе выяваў і відэа, ад 0 (без зацірэння) да 1 (па змаўчанні ў Reddit) + Захаваць пазіцыю стужкі + Захоўвае стужку ў той жа пазіцыі пасля закрыцця паста, выявы або відэа, замест таго, каб перамясціць адкрыты паст уверх + Макс. вышыня медыя ў стужцы + Максимальная вышыня выяваў, відэа і галерэй у стужцы, як множнік іх шырыні. Вышэйшыя матэрыялы абрэзаюцца. 1.33 — значэнне па adapting Reddit + Праіграваць Redgifs з гукам + Праігравае арыгінальнае відэа Redgifs замест копіі Reddit, у якой часта няма гуку + Якасць Redgifs HD + Праграе HD-відэа Redgifs. HD-відэа маюць большы памер і могуць спыняцца для загрузкі + Месца загрузкі + Усе ўліхоўкі + Па adaption Reddit: выявы ў Pictures/Reddit, відэа ў Download + Папка: %s + Назва файла: %s + Назвы файлаў Reddit + Выкарыстоўваецца \"Усе ўліхоўкі\" + Выкарыстаць уласнае месца + Захаваць у + Папкі па adaption Reddit Звычайны + Падпапка + Дадайце падпапкі з сімвалам / у назве файла + Прагляд… + На гэтай прыладнасці няма даступнага выбару папак + Назва файла + Назва файла Reddit + Націсніце на токен, каб уставіць яго. Сімвал / стварае падпапку. Невядомыя значэнні заменяюцца на \"unknown\" + Выявы: %s + Відэа: %s + Захаваць + Скінуць Выдаліць + Скінуць месца загрузкі для ўсіх уліхавак да значаэння па adapting Reddit? + Выдаліць месца загрузкі %s? + Не ўдалося захаваць у месца загрузкі. Захавано ў папку Reddit па змаўчанні + Папка загрузкі больш не даступная. Захавана ў палепку Reddit па змаўчанні + Не ўдалося захаваць загружанае відэа + Гэтую папку немагчыма выкарыстоўваць для загрузкі + %s (поTypeText) + %s (не аўтарызаваны) + Анонім (выхаджэнне зі ўліковага запісу або рэжым інкагніта) + Схаваць панэль прагляду медыя + Хавае загаловак, кнопкі і элементы кіравання відэа пры адкрыцці выявы або відэа. Націсніце на медыяфайл, каб паказаць іх + Схаваць кнопку \"Глядзець размову\" + Хавае кнопку \"Глядзець размову\" ў ніжняй частцы праглядальніка медыя + Чорны фон праглядальніка медыя + Выкарыстоўвае чорны фон за выявамі і відэа ў праглядальніку медыя замест колеру фона тэмы + Адкрываць спасылкі знаextérieur + Адкрывае спасылкі ў вашым браўзеры замест убудаванага браўзера праграмы + Запамятоўваць згорнутыя каментары + Каментары, якія вы згортнулі, застануць згорнутымі пры паўторным адкрыцці публікацыі Іконка праграмы + Выбраць іконку праграмы Звычайны + Невядома diff --git a/patches/src/main/resources/addresources/values-be-rBY/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-be-rBY/shared-youtube/strings.xml index eec7826aee..f87c67b8dc 100644 --- a/patches/src/main/resources/addresources/values-be-rBY/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-be-rBY/shared-youtube/strings.xml @@ -18,53 +18,272 @@ Second \"item\" text" --> - Адключана - Колер - Манахромны + Анімацыя запуску + Адключана + Колер + Манахромны + Перазагрузіце прыладу, каб завяршыць налады + Назва праграмы + Фон цёмнай тэмы + Чыста чорны + Material You (чыста чорны) + Класічны YouTube + Цёмна-рожавы + Цёмна-сіні + Цёмна-зялёны + Цёмна-жоўты + Цёмна-аранжавы + Цёмна-чырвоны + Уласны цёмны фон + Усталяваць колер фона для цёмнай тэмы + Па râмiнне праграмы + Material You (нейтральны) + Material You (асноўны) + Material You (другалепшае) + Material You (трэцялепшае) Карыстальніцкае + Загрузкі + Рэжым спампоўкі Звычайны + Спампаваць у праграме + Зональны загрузчык + Увядзіце назву пакета + %s не ўсталяваны. Калі ласка, ўсталюйце яго + Назва пакета не можа быць пустай + Карыстальніцкае + Іконка праграмы + Арыгінальная + Morphe black + Morphe dark + Morphe light + Morphe play светлы + Morphe play чорны + Карыстальніцкае + "Гэта змяняе значок, які wyświetляецца ў запускунгу праграм. + +Гэтая настройка не змяніць значок у наступных месцах: +• Спіс праграм у сістэмных наладках прылады +• Пэўныя push-апавешчэнні +• Выбар праграмы ў меню \"Адкрыць з дапамогой\" +• Значок Android Auto + +Каб змяніць значок усюды, выканайце патч паўторна і змяніце параметр патча \"Значок праграмы\"" + Morphe play + Паведамленне Morphe + Ваша гісторыя прагляду не захраняецца.<br><br>Штоўжней, гэта выклікана DNS-блакіроўшчыкам рэкламы або сеткавым проксі.<br><br>Каб выправіць гэта, дадайце <b>s.youtube.com</b> у белы спіс або выключайце ўсе DNS-блакіроўшчыкі і проксі + Больш не паказваць + Падарабіць відэапатокі + "Выпраўляе праблемы з відэапрайміўкасцю для więksнасці карыстальнікаў праз запыт сумяшчальнага відэапатока + +Калі вы карыстальнік YouTube Premium, гэтая настройка можа быць не патрэбнай" + Не ўдалося атрымаць кліенцкія патокі + Кліент па змаўчанні не працуе + "%1$s некалькі разоў запаляку не спрацаваў, але %2$s спрацаваў. + +Зрабіць %2$s кліентам па змаўчанні?" + Пераключыць + Выключение гэтай налады можа выклікаць праблемы з прайграваннем + Паспрабуйце ўłączyć: Імітацыя відэапатокаў > Уваход у Android VR + Кліент па змаўчанні + Уваход у Android VR + Вы не ўвайшли ў Android VR. Націсніце тут, каб увайсці + У дадзены момант вы ўвайшли ў Android VR + Каб скарыстацца гэтай функцыяй, змяніце \"Кліент па змаўчанні\" на Android VR + Увайсці ў Android VR + "Уваход у Android VR можа выправіць праблемы з відтворам для некаторых карыстальнікаў + +Каб увайти, націсніце «Працягаць» ніжэй" + Выйсці + Інструкцыі + "Код актывацыі: %s + +1. Націсніце «Адкрыць вебсайт» ніжэй, каб адкрыць вебсайт «Падключыць прыладу» +2. Устаўце з буфера абменью (або ўвядзіце ўручную) код актывацыі, паказаны вышэй +3. Увайсці ў Google, калі з'явіцца запыт. Калі ў вас некалькі ўлічаных запісаў, выберыце гэты ўлічаны запіс YouTube +4. Націсніце «Даць доступ YouTube VR» +5. Верніцеся ў гэтую праграму пасля з'яўлення паведамлення «Прыладу падключана»" + Не ўдалося выдаць код актывацыі + Не удалось выдаць токен аўтэнтыфікацыі + Успяхова ўвайшли ў сістэму + "Аўтэнтыфікацыя захавана + +• Налады Morphe не будуць імпартаваць/экспартаваць аўтэнтыфікацыю Android VR +• Калі гэтая праграма не будзе выкарыстоўвацца на працягу 6 месяцаў, Google выдаліць гэтую аўтэнтыфікацыю +• Вам спатрэбіцца ўвайсці ў сістэму зноў, калі вы змяніце пароль Google або адключаце гэтую прыладу ў наладках Google" + OAuth2 часова недаступны (таймаўт API) + Тэрмін дзеяння сесіі Android VR скончыўся, паспрабуйце ўвайсці ў VR зноў + OAuth2 часова недаступны (статус %s) + OAuth2 часова недаступны + Не ўдалося ўвайсці ў VR, аўтэнтыфікацыя на веб-сайце не была пацверджана + Не ўдалося ўвайсці ў VR: %s + Выйшлі з улікову запису Android VR + Нячынны токен уваходу ў VR, паспрабуйце ўвайсці ў VR зноў + Скіроўка медыя ў паведамленнях + Настройце элементы кіравання прайграваннем, якія адлюстроўваюцца на экране блакіроўкі і ў панэлі паведамленняў + Схаваць кнопкі \"Папярэдняя\" & \"Наступная\" + Хавае кнопкі \"Папярэдняя\" і \"Наступная\" ў паведамленні аб медыя, на экране блакіроўкі і ў скіроўцы навушнікоў + Адключаць палосу пракруткі + Забараняе перацягванне палосы пракруткі на экране блакіроўкі і ў панэлі паведамленняў + Пераматыванне кнопкамі \"Назад\" і \"Далей\" + Змяняе органы кіравання для паведамленняў пра медыя, экрана блакіроўкі і навушнікаў, каб выканаўваць пераматыванне замест змены відэа + Даўжыня пераматывання назад + Колькасць секунд для пераматывання назад кнопкай \"Назад\" + Даўжыня пераматывання ўперёд + Колькасць секунд для пераматывання ўперёд кнопкай \"Далей\" + Даўжыня пераматывання павінна быць ад 1 да %s секунд. Выкарыстоўваецца значэнне па adapting + Адключаць пратакол QUIC + Адключаць DRC-аудыё + Прымусіць арыгінальную мову аўдыя + Прапускаць цішыню + Аўтаматычна прапускае ціхія паўзы ў аўдыядарожцы відэа + Мінімальная працялосць паўзы + Скарачаюцца толькі тыя паўзы, якія доўжацца не менш за гэты час. Запрацуе пры запуску наступнага відэа + Сятэвы проксі + Накіроўваць падтрыманыя сеткавыя запыты праз проксі + Уключаць проксі + Накіроўвае падтрыманыя сеткавыя запыты праз наладжаны HTTP або HTTPS проксі + Хост проксі + Імя хоста або IP-адрас проксі-сервера + Порт проксі + Порт праксі-сервера + Выкарыстоўваць HTTPS-праксі + Выкарыстоўваць HTTPS для падключэння да праксі-сервера замест HTTP + Аўтэнтыфікацыя праксі + Адпраўляць Basic-аўтэнтыфікацыю з запытамі праксі + Імя карыстальніка праксі + Імя карыстальніка для Basic-аўтэнтыфікацыі праксі + Пароль праксі + Пароль для Basic-аўтэнтыфікацыі праксі + Дазволіць прамое падключэнне ў выпадку збою + Падключацца прама, калі праксі не працуе + Каб скарыстацца гэтай функцыяй, змяніце \"Spoof video streams\" на любога кліента, акрамя \"Android Studio\" і \"Android VR Downgraded\" + Ачысціць спасылкі для падзелу + Выдаляе параметры адсочвання з размерных спасылак + Змяніць спасылкі для падзелу на youtube.com + Замяняе music.youtube.com на youtube.com у паділеных спасылках + Прымушае відэа выкарыстоўваць скарочаны фармат URL youtu.be + "Уключанне гэтай налады дазволіць запіс дадатковых даных размяшчання, уключаючы тэкст на экране для некаторых кампанентаў інтэрфейсу. + +Гэта можа дапамагчы ідэнтыфікаваць кампаненты пры стварэнні ўласных фільтраў. + +Аднак уключанне гэтай функцыі таксама прывядзе да запісу некаторых карыстальніцкіх даных, такіх як ваш IP-адрас" + Спіс радкоў пабудоўніка шляху кампанента для фільтрацыі, аддзяленых новымі радкамі + Няправільны карыстальніцкі фільтр: %s + Фільтр меню налад + Схаваць элементы на стандартным экране налад праграмы па іх бачнай назве + Адкрыйце стандартны экран налад праграмы адзін раз, каб запоўніць спіс + Выдаліць дыялогавае акно папярэджання для глядачоў + Гэта не абыходзіць абмежаванне па ўзросце. Гэта проста прымае яго аўтаматычна + Постаўшчык PoToken + Наладзіце пастаўшчыка PoToken, каб пазбегнуць праблем з відэаўтварэннем + Зональны пастаўшчык PoToken + "Выпраўляе праблемы з відэаўтварэннем з дапамогай знешняга мінтера PoToken (Эксперыментальна) + +Калі вы карыстальнік YouTube Premium, гэтая налада можа быць не патрэбнай" + "Выпраўляе праблемы з прайграваннем з выкарыстаннем знешняга мінтера PoToken (Эксперыментальна) + +Каб скарыстацца гэтай функцыяй, выключайце \"Падмена відэапатокаў\"" + Калі гэта не працуе, паспрабуйце прымусова закрыць MicroG + Адкрыць налады PotHelper + "Гэтая функцыя цяпер знаходзіцца ў распрацоўцы і можа не працаваць + +Націсніце тут, каб спампаваць PotHelper" + PotHelper больш не працуе + "Токен, убудаваны ў гэтую версію PotHelper, больш не працуе + +Уключыце \"Імітаваць відэапатокі\", каб выправіць працэс прайгравання" + Уключыць + Імітацыя версіі праграмы + Абманоўвае сервер, прымушаючы яго думаць, што вы выкарыстоўваеце старую версію праграмы, каб аднавіць састарэлыя функцыі інтэрфейсу + "Версія праграмы будзе заменена на больш старую. + +Гэта змяніць выгляд і функцыі праграмы, але могуць узнікнуць невядомыя пабачныя эфекты. + +Калі пазней выміць гэтую функцыю, рэкамендуецца ачысціць даныя праграмы, каб пазбегнуць памылак інтэрфейсу" + Цэль імітацыі версіі праграмы + Частата абнаўлення праграмы Звычайны + %s FPS + Перакрыць частату абнаўлення + Працяўканьне на ўвесь экран + Працяўканьне ў партрэтным рэжыме і на ўвесь экран + Шрыфт Звычайны + Сістэмны шрыфт прылады + Свой файл шрыфта + Свой файл шрыфта + Выберыце файл шрыфта TTF або OTF + Выбраны файл не з\'яўляецца шрыфтом TTF або OTF + Не ўдалося загрузіць спецыяльны шрыфт, яго было выключана + Дызлайкі часова недаступныя (таймаут API) + Дізлайкі недаступныя (статус %s) + Дызлайкі недаступныя (ліміт кліенцкага API) + Дызлайкі недаступныя (%s) + Схавана ўладальнікам + Не падабаецца гэтае відэа, разам з %s іншымі людзьмі + Не падабаецца гэтае відэа, %s галасаў — гэта негатыўныя ацэнкі + Уключыць Return YouTube Dislike + Паказваць дызлайкі ў працэнтах + Паказвае дызлайкі як працэнт ад лайкаў замест колькасці + Паказваць прыкладную колькасць лайкаў + Паказвае прыблізную колькасць лайкаў у відэа, дзе стваральнік адхіліў лайкі + Паказваць папярэджанні пра памылку злучэння + Паказвае паведамленне, калі сервер Return YouTube Dislike недаступны + "Return YouTube Dislike аднаўляе лічыльнікі дызлайкаў, якія YouTube схаваў у 2021 годзе. Лічыльнікі бяруцься з даных, архіваваных да выдалення, а для больш новых відэа яны разлічваюцца на аснове галасаванняў карыстальнікаў, таму яны прыбліжаныя і абнаўляюцца кожныя некалькі дзён + +ID відэа, якое вы глядзіце, адпраўляецца ў API Return YouTube Dislike, а вашы лайкі і дызлайкі адпраўляюцца ананімна для палепшання разлікаў" + Статыстыка API Return YouTube Dislike гэтага прылады + Час адказу API, сярэдні + Час адказу API, мінімальны + Час адказу API, максімальны + Час адказу API, апошні медыяфайл + Дыслайкі часова недаступныя — дзейнічае абмежаванне хуткасці Client API + API атрымання галасаў, колькасць выклікаў + Сеткавых выклікаў не было + API атрымання галасаў, колькасць таймаўтаў + Сеткавых выклікаў з таймаўтамі не было + Ліміты частаты запытаў API-кліента + Ліміты частаты запытаў кліента не сустрэкаліся diff --git a/patches/src/main/resources/addresources/values-be-rBY/shared/strings.xml b/patches/src/main/resources/addresources/values-be-rBY/shared/strings.xml index 13d4744ca8..474dc5b877 100644 --- a/patches/src/main/resources/addresources/values-be-rBY/shared/strings.xml +++ b/patches/src/main/resources/addresources/values-be-rBY/shared/strings.xml @@ -17,26 +17,219 @@ Second \"item\" text" --> + Налады + Вы ўпэўнены, што хочаце працягнуць? + Захаваць + Скінуць + Скінуць колер + Несапраўдны колер + Патрабуецца перазагрузка + Перазапусціце праграму, каб гэтыя змены ўступілі ў сілу + Перазагрузіць + Імпартаваць + Капіяваць + Налады Morphe скінуты да пачатку + Імпартавана %s налад + Імпарт недаўлічыўся: %s + Перайсці ўверх + Пошук па наладах + Пра праграму + Па \'%s\' нічога не знойдзена + Спрабуйце іншае ключавое слова + Нядаўнія запыты + Выдаліць з гісторыі пошуку? + Выдаліць з гісторыі пошуку + Ачысціць гісторыю пошуку + Вы ўпэўнены, што хочаце ачысціць усю гісторыю пошуку? + Падказкі па пошуку + "• Націсніце на шлях, каб перайсці па ім +• Зацісніце настройку, каб перайсці да яе +• Націсніце Enter, каб захаваць пошуковы запыт у гісторыі +• Пры пошуку рэгістр і пунктуацыя ігнаруюцца +• Бацькоўскія настуроўкі відацейся вышэй за адключаныя дачернія настуроўкі" + Гісторыя пошуку пустая + Каб захаваць гісторыю пошуку, увядзіце пошуковы запыт і націсніце Enter + Паказваць гісторыю пошуку па настуроўках + Назва налад Morphe Звычайны + Дадатковае + Твікі + Час марфінгу Карыстальніцкае + Увядзіце імя + Імя не можа быць пустым + Паказваць іконкі настуроўкаў Morphe + Скопіявана: %s + Мова Morphe + "Пераклады для некаторых моў могуць адсутнічаць або быць няпоўнымі. + +Каб перакласці новыя мовы або палепшыць існуючыя пераклады, наведзіцеся па адрасе morphe.software/translate" + Мова праграмы + Імпарт / Экспарт + Імпартаваць / Экспартаваць налады Morphe + Імпартаваць з файла + Налады паспяхова імпартаваны, націсніце \"Захаваць\", каб прымяніць іх + Не ўдалося імпартаваць налады + Экспартаваць у файл + Налады паспяхова экспартаваны + Не удалось экспартаваць налады + Вы карыстаецеся найсвежай версіяй Morphe Patches <i>%s</i> + Вы выкарыстоўваеце версію Morphe Patches <i>%1$s</i><br><br><b>Даступна абнаўленне: <i>%2$s</i></b><br><br>Каб абнавіць патчы, выкарыстайце Morphe для паўторнага патчавання гэтай праграмы + Заўвага + Гэтая версія з\'яўляецца папярэднім выпускам, і вы можаце сунуцца з нечаканымі праблемамі + Афіцыйныя спасылкі + Вебсайт + Данатаць + Пераклады + Уклад + Актуальна + Даступна абнаўленне + Бягучая распрацоўка + Папярэдняя распрацоўка + Актыўная распрацоўка + 2022 ‑ 2025 + 2022 ‑ 2025 + 2019 ‑ 2022 + Заўвагі аб ліцэнзіі + Загрузка… + Адладка + Налада пашырэных опцый распрацоўшчыка і дыягнастычных сінхронаў + Уключыць адлагоджаныя сінхроны + Стэк выклікаў лага адладкі + Уключае трасы стэка ў адлагоджаных сінхронах, каб дапамагчы вызначыць памылкі + Паказваць паведамленне пры памылцы Morphe + "Выключаючы паведамленні аб памылках, вы схаваеце ўсе паведамленні аб памылках Morphe. + +Вы не будзеце атрымліваць паведамленні аб любых нечаканых падзеях" + Экспартаваць лагі адладкі + Экспартаваць налагодзванням-логі Morphe + Налагодзванням-логі паспяхова экспартаваны + Не ўдалося экспартаваць налагодзванням-логі + Пошук у логах адладкі… + Ачысціць + Адладчывы лагаванне адключана + Налагодзванням-логі не знойдзены + Налагодзванням-логі скапіяваны + Не ўдалося экспартаваць налагодзванням-логі: %s + Налагодзванням-логі ачышчаны + Менеджэр функцыянальных сцягаў + Кіраванне булевымі сцягамі функцый + Пошук сцягоў функцый… + Неактыўныя (%s) + Актыўныя (%s) + Заблакавана (%s) + Прымусова (%s) + Аўта Выкл. Укл. + Уłączyć прымусова + Заблакаваць Выдаліць + Сцяги функцый захаваны + Скінута сцягі функцый + Сцягі функцый скопіяваны ў буфер абмену + Перазапусціце праграму, каб выкарыстаць гэтую настройку + Імпорт і экспорт + Імпартавана %s прагрэсіўных сцягаў + Сцягі функцый не знайдзены + Сцягі функцый паспяхова экспартаваны + Не удалось экспартаваць сцягі функцый + Сцягі функцый загружаны, націсніце \"Захаваць\", каб прымяніць + Не удалось прачытаць файл + Бінарны пошук + "Вызначае, які працяк функцыянальнасці выклікае паводзіны праграмы. + +Палова з %s працякоў заблакавана, і пасля кожнага перазапуску вы паведамляеце, ці застаецца паводзіна. + +Адна разім адтворыце паводзіну перад пачатком, інакш працякі, якія праграма яшчэ не запытала, будуць адсутнічаць у пошуку" + Пачаць + "Няма сيزةвых сцягаў для пошуку. + +У бінарным пошуку можна выкарыстоўваць толькі тыя сцягі, да якіх праграма звярталася і якія ў дадзены момант не прымусава ўключаны" + "Крок %1$s + +Засталося сцягаў: %2$s, з іх %3$s заблакаваны для гэтага кроку. + +Перазапусціце праграму, праверце, ці заўважаецца паводзіна, затым адкажыце ніжэй" + Ідзе пошук сيزةвага сцяга + Паводзіна заўважаецца + Паводзіна не заўважаецца + Скасаваць бінарны пошук + Бінарны пошук скасаваны + "Сيزةвы сцяг %s выклікае гэтую паводзіну. + +Цяпер ён заблакаваны" + "Не ўдалося знайсці працяк функцыянальнасці. + +Здаецца, што паводзіны праграмы не кіруюцца працяком функцыянальнасці" + "Не ўдалося знайсці адзіны працяк функцыянальнасці. + +Паводзіна знікла раней у пошуку, таму можа быць задейна больш за адзін працяк або адзін з адказаў быў няправільным" + Адкрыць налады MicroG + MicroG не ўсталяваны. Усталюйце яго + Патрабуюцца дзеянні + "Ваша версія MicroG v%1s састарэла і можа выклікаць праблемы. + +Калі ласка, абновіце MicroG да найпрыхраснейшай версіі v%2s" + Абнавіць + Ігнараваць + "Усталяваны MicroG не рэкамендуецца Morphe: + +%1s + +Націсніце, каб выдаліць яго, затым зноў адкрыйце гэтую праграму і ўсталюйце MicroG RE" + Выдаліць + "MicroG не мае дазволу працаваць у фоне. + +Пазнаёмся з інструкцыяй \"Don't kill my app\" для вашага тэлефона і прымяніце яе да ўсталёўкі MicroG. + +Гэта неабходна для працы праграмы" + Адкрыць вебсайт + "Аптымізацыю батарэі для MicroG трэба адключаць, каб пазбегнуць праблем. + +Адключэнне аптымізацыі батарэі для MicroG не паўплывае негатыўна на выкарыстанне зараду. + +Націсніце кнопку працягу і дазвольце змены аптымізацыі" + Працягнуць + Паказаць дыялогавае акно аптымізацыі батарэі MicroG + "Адключэнне гэтай функцыі скрое запыт на аптымізацыю батарэі MicroG. + +Калі налады аптымізацыі батарэі не будуць змененыя, вы можаце сунуцься з наступным: +• Праграма можа выпадкова закрывацца +• Апавешчэнні могуць не працаваць +• Відтварванне ў фоне можа не працаваць" + Апрацоўка спасылак праграмы + Наладзіць адкрыццё спасылак у гэтай праграме + Крок 1: Адключыце арыгінальныя спасылкі праграмы + Адкрыйце налады арыгінальнай праграмы і адключайце ўсе спасылкі даменаў + Крок 2: Уключыце спасылкі патчанай праграмы + Адкрыйце налады патчанай праграмы і ўключыце ўсе спасылкі даменаў + Адкрыць налады + 🚨 Эксперыментальнае папярэджанне 🚨 + "Вы выкарыстоўваеце eksperыментальную версію праграмы ⚠️ <b>%1$s</b> +<br/><br/> +🔧 Очакуйце дзіўнай паводзіны праграмы або невядомых памылак, пакуль мы дапрацоўваем патчы для гэтай версіі праграмы. +<br/><br/> +Калі вы жадаеце максімальна стабільнай працы, тады <b>выдаліце</b> гэтую версію і ўсталюйце рэкамендаваную версію праграмы ✅ <b>%2$s</b>" + ⚠️ Я хачу эксперыментальную версію + ✅ Я хачу стабільную версію + Журнал паломкі захаваны ў дакументы карыстальніка: %s + Журнал паломкі захаваны: %s diff --git a/patches/src/main/resources/addresources/values-be-rBY/sponsorblock/strings.xml b/patches/src/main/resources/addresources/values-be-rBY/sponsorblock/strings.xml index 9d2d4ab7d5..724045390b 100644 --- a/patches/src/main/resources/addresources/values-be-rBY/sponsorblock/strings.xml +++ b/patches/src/main/resources/addresources/values-be-rBY/sponsorblock/strings.xml @@ -16,7 +16,219 @@ Second \"item\" text" --> + Уключыць SponsorBlock + SponsorBlock — гэта сістэма краудсорсінгу для прапускання раздражальных частак відэа на YouTube + Выгляд + Паказваць кнопку галасавання + Дадае кнопку для пазітыўнага або негатыўнага галасавання за сегменты + Выкарыстоўваць квадратны макет + Выкарыстоўваць квадратныя куты для кнопак і элементаў кіравання SponsorBlock + Выкарыстоўваць кампактную кнопку «Прапусціць» + Зменшвае шырыню кнопкі «Прапусціць», каб заостатаць месца + Аўтаматычна схаваць кнопку «Прапусціць» + Хавае кнопку «Прапусціць» праз некалькі секунд замест таго, каб яна заставалася бачнай на працягу ўсяго сегмента + Час паказва кнопкі «Прапусціць» + Усталюйце, як доўга кнопка «Прапусціць» застаецца бачнай + Паказваць паведамленне пра прапуск + Паказвае паведамленне, калі сегмент прапушчаны аўтаматычна + Час паказа паведамлення пра прапуск + Усталюйце, як доўга паведамленне аб скасаванні застаецца бачным + 1 секунда + 2 секунды + 3 секунды + 4 секунды + 5 секунд + 6 секунд + 7 секунд + 8 секунд + 9 секунд + 10 секунд + Паказваць працягласць відэа без сегментаў + Аднімае ўсе сегменты з агульнага часу працягласці відэа, паказанага на панэлі пракруткі + Стварэнне новых сегментаў + Паказаць кнопку \"Стварыць новы сегмент\" + Дадае кнопку ў відэаплеер, якая дазваляе адпраўляць новыя сегменты SponsorBlock + Крок карэкціроўкі часу + Значэнне перамоткі для кнопак карэкціроўкі часу (у мілісекундах) + Значэнне павінна быць станоўчым лікам + Рэкамендацыі па адпраўцы + Рэкамендацыі ўключаюць правілы і парады па стварэнні новых сегментаў + Цільбуйцеся на кіраўніцтва + Прачытайце кіраўніцтва SponsorBlock перад стварэннем новых сегментаў + Ужо чытаў + Паказаць + Агульнае + Паказваць паведамленні пра памылкі падключэння + Паказвае паведамленне, калі сервер SponsorBlock недаступны + Уключаць адследжванне колькасці прапушчаных фрагментаў + Адпраўляе лічальнікі прапускаў у табліцу лідэраў SponsorBlock, каб адслічыць, колькі часу было заэканомілі + Мінімальная працягласць сегмента + Сегменты карацейшыя за гэтае значэнне (у секундах) не будуць паказвацца або прапускацца + Няправільная працягласць часу + Ваш прыватны ID карыстальніка + Гэта павінна заставацца ў сакрэце. Гэта падобна да пароля і не павінна перадавацца нікаму. Калі хтосьці атрымае яго, ён зможа выдаць сябе за вас + Прыватны ID карыстальніка павінен być не менш за 30 сімвалаў + Змяніць URL API + Адрас, які SponsorBlock выкарыстоўвае для зваротаў да сервера + Белы спіс каналаў + SponsorBlock адключаны для каналаў з белага спіса + Паведамляць пра канал з белага спіса + Паказваць паведамленне, калі SponsorBlock адключаны для канала з белага спіса + SponsorBlock адключаны для гэтага канала + Скідаванне API URL + API URL няправільны + API URL зменены + Імпарт / Экспарт налад + Капіяваць + Ваша JSON-канфігурацыя SponsorBlock, якую можна імпартаваць / экспартаваць у Morphe і іншыя платформы SponsorBlock + "Ваша JSON-канфігурацыя SponsorBlock, якую можна імпартаваць / экспартаваць у Morphe і іншыя платформы SponsorBlock + +Гэта ўключае ваш прыватны ідэнтыфікатар карыстальніка. Будзьце ўважлівыя пры яго перадачы" + Налады паспяхова імпартаваны + Не ўдалося імпартаваць: %s + Не ўдалося экспартаваць: %s + "Вашы налады змяшчаюць прыватны ідэнтыфікатар карыстальніка SponsorBlock. + +Ваш ідэнтыфікатар карыстальніка падобны да пароля, і яго ніколі не варта перадаваць іншым" + Больш не паказваць + Змяніць паводзіны сегмента + Спансар + Платная прамоцыя, платныя рэфералы і прамая рэклама. Не для самапрамоцыі або бясплатнага рэкамендавання spraw / творцаў / сайтаў / прадуктаў, якія ім падабаюцца + Бясплатная / самапрамоцыя + Падобна да Спансара, але для бясплатнай / самапрамоцыі. Уключае раздзелы пра мерч, данаты або інфармацыю аб тых, з кім яны супрацоўнічалі + Нагаданне пра ўзаемадзеянне (падпіска) + Кароткае напамінне папостаць лайк, падпісацца або саледзіць за аўтарам пасярэдзіне кантэнту. Калі яно доўгае або тычыцца чагосьці канкрэтнага, яно павінна быць у раздзеле \"самапрасоўванне\" + Выяўленне + Частка відэа, якую шукаеБольшасць людзей + Перапынак / Уступная анімацыя + Інтэрвал без рэальнага кантэнту. Гэта можа быць паўза, статычны кадр або паўторная анімацыя. Не ўключае пераходы, якія змяшчаюць інфармацыю + Канцоўкі / Тітры + Тітры або момант, калі з\'яўляюцца канцоўкі YouTube. Не для заключэнняў з інфармацыяй + Зацяжка / Прывітанне + Агучаныя трэйлеры будучых відэа, прывітаньні і развітанні. Не ўключае раздзелы, якія дадаюць дадатковы кантэнт + Прагляд / Кароткі змест + Калекцыя кліпаў, якія паказваюць, што будзе далей або што адбылося ў відэа ці ў іншых відэа серыі, дзе ўся інфармацыя паўтараецца ў іншым месцы + Адступленні / Жарты + Прыбічныя сцэны або жарты, якія не з\'яўляюцца неабходнымі для разумення асноўнага зместу відэа. Не ўключае раздзелы, якія даюць кантэкст або дапаможныя дэталі + Музыка: Раздзел без музыкі + Толькі для выкарыстання ў музыкальных відэа. Часці музыкальных відэа без музыкі, якія яшчэ не ўваходзяць у іншую катэгорыю + Прапусціць + Высвеціць галоўнае + Прапусціць спонсара + Прапусціць прамоцыю + Прапусціць узаемадзеянне + Прапусціць да выдзялення + Прапусціць уступ + Прапусціць перарву + Прапусціць перарву + Прапусціць завяршэнне + Прапусціць завязку + Прапусціць папярэдні прагляд + Прапусціць папярэдні прагляд + Прапусціць кароткі змест + Прапусціць адступленне + Прапусціць немузіку + Прапусціць сегмент + Прапушчаны спонсар + Прапушчана самарэклама + Прапушчана раздраляльнае напамін + Прапушчана да галоўнага + Прапушчана ўступ + Прапушчана перапынак + Прапушчана перапынак + Прапушчана застаўка ў канцы + Прапушчана ўступная частка + Прапушчана папярэдні прагляд + Прапушчана папярэдні прагляд + Прапушчана кароткае паўтор + Прапушчана адступленне + Прапушчана немузікальны раздзел + Прапушчаны непаданы сегмент + Прапушчана некалькі сегментаў + Прапускаць аўтаматычна + Прапусціць аўтаматычна адзін раз + Паказваць кнопку \"Прапусціць\" + Паказваць у палосе пракруткі + Адключана + Не ўдалося падаць сегмент: %s + SponsorBlock часова не працуе + Не ўдалося адправіць сегмент (статус: %1$s %2$s) + Не удалося адправіць сегмент. Абмежаванне па хуткасці (занадта шмат запытаў ад аднаго карыстальніка або IP) + Не удаецца адправіць сегмент: %s + "Не удаецца адправіць сегмент. +Ужо існуе" + Сегмент паспяхова адпраўлены + SponsorBlock часова недаступны (тайм-аут API) + SponsorBlock часова недаступны (статус %s) + SponsorBlock часова недаступны + Не удаецца прагаласаваць за сегмент (тайм-аут API) + Не ўдалося прагаласаваць за сегмент (статус: %1$s %2$s) + Не ўдалося прагаласаваць за сегмент: %s + Голосаваць \"за\" + Голосаваць \"праціў\" + Змяніць катэгорыю + Няма сегментаў для галасавання + з %1$s па %2$s + Выберыце катэгорыю сегмента + Катэгорыя адключана ў наладках. Уключыце катэгорыю, каб адправіць сегмент + Новы сегмент SponsorBlock + Усталяваць %s як пачатак або канец новага сегмента? + Пачатак + Канiec + Цяпер + Час пачатку сегмента + Час заканчэння сегмента + Ці правільны час? + "Сегмент з + +%1$s +па +%2$s + +(%3$s) + +Гатовы адправіць?" + Адправіць + Пачатак павінен быць перад канцом + Спачатку пазначце месца на часовой панэлі + Прагледзіце сегмент і пераканайцеся, што ён прапускаецца плаўна + Рэдагаваць час сегмента ўручную + Вы хочаце зрэдагаваць час пачатку ці канца сегмента? + Пададзены час няправільны + Адправіць выдзяленне + "Ці правільны час выдзялення? + +%s + +Калі вы не адпраўляеце выдзяленне, націсніце \"Скасаваць\" і пазначце другое месца" + Адправіць выдзяленне + Статыстыка + Статыстыка часова недаступная (API не працуе) + Загрузка… + SponsorBlock выключаны + Ваш імя карыстальніка: <b>%s</b> + Націсніце тут, каб змяніць імя карыстальніка + Не ўдалося змяніць імя карыстальніка: Статус: %1$s %2$s + Імя карыстальніка паспяхова зменена + Ваша рэпутацыя: <b>%.2f</b> + Вы стварылі <b>%s</b> сегментаў + Націсніце тут, каб паглядзець свае сегменты + Табліца лідараў SponsorBlock + Вы выратавалі людзей ад <b>%s</b> сегментаў + Націсніце тут, каб паглядзець глабальную статыстыку і лепшых аўтараў + Гэта <b>%s</b> іх жыцця.<br>Націсніце тут, каб паглядзець табліцу лідараў + Вы прапусцілі <b>%s</b> сегментаў + Гэта <b>%s</b> + Скінуць лічыльнік прапушчаных сегментаў? + %1$s гадзін %2$s хвілін + %1$s хвілін %2$s секунд + %s секунд + Прадпразочнасць: + Колер: + "SponsorBlock — гэта краудсорсінговая база даных відэасегментаў, такіх як спонсары, уступы, застаўкі і напаміны падпісацца. Сегменты дадаюцца і галосуюцца супольнасцю, а база даных з'яўляецца адкрытай і бясплатнай для спампоўкі + +ID відэа, якое вы глядзіце, адпраўляецца ў API SponsorBlock для атрымання яго сегментаў" diff --git a/patches/src/main/resources/addresources/values-be-rBY/youtube/strings.xml b/patches/src/main/resources/addresources/values-be-rBY/youtube/strings.xml index 87d55e999a..9ce9b26822 100644 --- a/patches/src/main/resources/addresources/values-be-rBY/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-be-rBY/youtube/strings.xml @@ -18,233 +18,1580 @@ Second \"item\" text" + Пра Morphe + Рэклама + Струмень + Агульныя + Плеер + Shorts + Палоска пракруткі + Кіраванне свайпамі + Разнастайнае + Відэа + Аднавіць стары экран налад YouTube + Выдаліць абмежаванні прайгравання ў фоне + Выключаць фоновы працу + Аўтаматычная паўза пры блакіроўцы прылады Выкл. + Заўсёды ставіць на паўзу пры блакіроўцы + Ставіць на паўзу, калі не падключана бесправадное аўдыя + Адключаць абмежаванне хуткасці пракруткі + Адключаць абнаўленні макета + Забараняе серверу абнаўляць макет праграмы + "Размяшчэнне праграмы вяртаецца да размяшчэння, якое выкарыстоўвалася пры першай ўсталёўцы. + +Некаторыя размяшчэнні на баку сервера могуць не вярнуцца. + +Змены ўключаюць: +• Кампаненты ў выпадаючым меню плэера (або ў адпаведных наладках) могуць не працаваць +• Бегучыя лічбы не анімаваныя +• Укладка «Бібліятэка» паказваецца +• Музічны раздзел апісання відэа можа не працаваць +• Пункты выпадаючага меню плэера нельга схаваць +• Кнопка пераключэння ўліковых запісаў можа не з'явіцца на ўкладцы «Бібліятэка»" + Уключыць фільтр выпадаючага меню стужкі + Фільтр выпадаючага меню стужкі + Увядзіце назвы выпадаючага меню для фільтрацыі, аддзеленыя новымі радкамі + Уключаць фільтр ніжняга меню \"Бібліятэка / Вы\" + Фільтр ніжняга меню \"Бібліятэка / Вы\" + Увядзіце назвы меню Бібліятэкі / ніжняга меню для фільтрацыі, аддзеленыя новымі радкамі + Схаваць карткі альбомаў + Схаваць карткі альбомаў пад апісаннем выканаўцы + Схаваць карткі артыстаў + Схаваць картку выканаўцы ў верхняй частцы вынікаў пошуку + Схаваць кнопкі канала + Схаваць такія кнопкі, як \"Падпісацца\", \"Прагледзіць канал\", \"Супольнасць\" і \"YouTube Music\", з профіляў каналаў у выніках пошуку + Схаваць пасты супольнасці + Схаваць кампактныя банары + Схаваць кампактныя банеры і адступы паміж некаторымі кампанентамі + Разгартальныя карткі пад відэа + Убачыць усе + Схаваць толькі картку з підсумі + Схаваць усё + Схаваць плаваючую кнопку мікрафона + Схаваць плаваючую кнопку мікрафона ў ніжнім правым куце экрана з прапановамі пошуку + Схаваць кнопку \"Атрымаць Premium\" + Схаваць паліцу гісторыі + Схаваць гарызантальныя паліцы + Схаваць гарызантальныя паліцы, такія як «Галоўныя навіны», «Публікацыі супольнасці, якія вам могуць спадабацца», «Працягнуць прагляд», «Даследаваць больш каналаў», «Даследаваць больш тэм», «Для вас», «Найсвежае відэа», «Найбольш рэлевантныя», «Публікацыі супольнасці twórcы», «Пакупкі» і «Глядзець зноў» + Схаваць паліцу з выявамі + Схаваць паліцы з выявамі ў выніках пошуку + Схаваць картку «Запрасіць іншых у паведамленні» + Схавае картку з запрашаण्यास, якая паказваецца ў раздзеле «Паведамленні» ўкладкі «Апавяшчэнні» + Схаваць кнопку \"Новыя відэа\" + Схаваць сіні банер \"Найсвежаее відэа\", які з\'яўляецца ў верхняй частцы галоўнай стужкі + Хаваць прамыя трансляцыі + Хавае відэа, якія ідуць у прамым эфіры зараз, у стужцы галоўнай старонкі і выніках пошуку + Схаваць мікс-плэйлісты + Схаваць раздзел з фільмамі + Схаваць загаловак меню \"Апавяшчэнні\" + Схаваць загаловак меню \"Апавяшчэнні\" пры націсканні на кнопку-званік + Схаваць кнопку \"Паведаміць мяне\" + Хавае кнопку напамінання для запланаваных прамых трансляцый і прэм\'ер + Схаваць Playables + Схаваць функцыю, якая дазваляе гуляць у гульні + Схаваць мініятуры пошукавых запытаў + Схаваць мініятюры ў спісе аўтазапаўнення пошуку + Схаваць кнопку \"Паказаць больш\" + Схаваць кнопку разгарнуць пад відэа ў выніках пошуку + Схаваць панэль падпісаных каналаў + Схаваць назвы каналаў, на якія вы падпісаны + Схаваць назвы каналаў у панэлі падпісак + Схаваць апытанні + Схаваць апытанні і падказкі аб дакладнасці рэкамендацый стужкі + Схаваць паліцу з білетамі + Схаваць час загрузкі + "Абмежаванні: +• Паліцы Shorts, старонкі каналаў і вынікі пошуку ўсё яшчэ могуць паказваць час загрузкі +• Не працуе з аўтамабільным размяшчэннем" + Схаваць мініяцюру відэа + Схаваць пазнакі відэа + Схаваць такія пазнакі, як \"Аўтаматычны пераклад\" / \"Дубляваны\", \"Зарабляе камісію\", \"Хайпавы\", \"Глядзелі таксама\", \"тавар(ы)\" і \"Вам таксама можа спадабацца\" + Схаваць колькасць праглядаў + "Абмежаванні: +• У паліцах Shorts, на старонках каналаў і ў выніках пошуку ўсё яшчэ могуць паказвацца колькасці праглядаў +• Не працуе з аўтамабільным інтэрфейсам" + Схаваць вынікі пошуку ў вебе + Схаваць раздзел \"Вынікі з інтэрнэту\" з вынікаў пошуку + Схаваць раздзел \"Вам можа спадабацца\" + Схаваць YouTube Doodles + Хавае сезонныя і падзеявыя анімацыі на лагатыпе YouTube + Паказ паказванне YouTube Doodles можа перашкодзіць адлюстраванні кастомных загалоўкаў + Схаваць панэль канала + Схаваць вадзяны знак канала + Схаваць раздзелы і кнопку «У гэтым відэа» + Схаваць блок краўдфандынгу + Схаваць паведамленні аб надзвычайных сітуацыях + Схаваць інфармацыйныя панэлі + Хавае інфармацыйныя кантэкстныя панэлі, такія як медыцынская інфармацыя + Схаваць кнопку «Уступіць» + Схаваць кнопку \"Паўтор чата ў прамым эфіры\" + Хавае кнопку паўтору чата ў паўнаэкранным рэжыме + Схаваць медыцынскія панэлі + Схаваць падказкі па жэстах прагравальніка + Схаваць падказкі, якія паказваюцца пры жастах прайгравача, такія як \"Працягніце ўверх для дакладнага пошуку\", \"Адпусціце, каб скасаваць\" і \"2x\" + Схаваць snackbar + Схаваць правілы супольнасці для падпісчыкаў + Схаваць кнопку \"Сінхранізаваць з відэа\" + Схаваць градыянт націску і ўтрымання + Схаўвае градыянт элементаў кіравання плэерам пры праміканні пальцам падчас утрымання для паскарэння + Схаваць часовыя рэакцыі + Хавае функцыю, якая дазваляе карыстальнікам рэагаваць эмодзі ў раздзеле каментарыяў і ў жывым чаце + Схаваць меню \"Дапамога & водгукі\" + Хавае меню \"Дапамога & водгукі\" пры выкарыстанні кнопкі з трыма кропкамі, напрыклад, у выніках пошуку + Схаваць псаўдонім + Схаваць псаўдонім вашага канала ў ўкладцы \"Вы\" + Схаваць назву відэа + Хавае назву відэа ў паўнаэкранным рэжыме + Ачысціць субтытры відэа + Ачышчае такія фразы, як \"#\", \"Збор сродкаў\", \"тавар(ы)\" і \"Крама\" з субтытраў відэа + Схаваць кнопку «Спытаць Gemini» + Хавае кнопку «Спытаць Gemini» ў ніжняй частцы панэлі «Спытаць пра гэтае відэа» + Апісанне відэа + Выбірайце, якія кампаненты будуць адлюстроўвацца ў апісанні відэа + Схаваць раздзел \"Рэзюме відэа, створанае ШІ\" + Схаваць раздзел \"Спытайце\" + Схаваць раздзел \"Атрыбуты\" + Схаваць раздзелы «Рэкамендаваныя месцы», «Гульні», «Музыка» і «Згадкі аб людзях» + Схаваць раздзел спасылак на канал + Схаваць раздзел «Главы» + Схаваць раздзел «Выпраўленні» + Схаваць раздзел «Прагрэс курсу» + Схаваць раздзел «Даследаванне» + Схаваць раздзелы «Даследуйце гэты курс» і «Даследуйце падкаст» + Схаваць раздзел «Даследуйце гэты курс» + Схаваць раздзел «Даследуйце падкаст» + Схаваць раздзел «Рэкамендаваныя каналы» + Схаваць раздзел «Рэкамендаваныя спасылкі» + Схаваць раздзел \"Рэкамендаваныя плэйлісты\" + Схаваць раздзел «Рэкамендаваныя відэа» + Схаваць раздзел хэштагаў + Схаваць раздзел «Як гэта было зроблена» + Схаваць бал Hype + Схаваць раздзел інфармацыйных карткас + Схаваць раздзел \"Ключавыя паняцці\" + Схаваць раздзел віктарын + Схаваць раздзел \"Пошук у гэтым відэа\" + Схаваць кнопку «Падпісацца» + Схаваць раздзел транскрыпцыі + Схаваць раздзел \"Падрабязнасці відэа\" + Панэль фільтраў + Выберыце, дзе схаваць пракручвальную панэль фільтраў + Схаваць на старонцы канала + Схаваць у каментарах + Схаваць у стужках + Схаваць у паalikeных відэа + Схаваць у выніках пошуку + Схаваць у гісторыі праглядў + Старонка канала + Выбірайце, якія кампаненты будуць паказвацца на старонцы канала + Уключыць фільтр ўкладкі канала + Фільтр ўкладкі канала + Увядзіце назвы ўкладак канала для фільтрацыі, аддзеленыя новымі радкамі + Схаваць кнопку «Супольнасць» + Схаваць кнопку «Далучыцца» + Схаваць папярэдны прагляд спасылак + Схаваць клікальныя вэб-спасылкі ў загалоўку канала + Схаваць плашку \"Толькі для ўдзельнікаў\" + Схаваць плашку \"Толькі для ўдзельнікаў\" у панэлі фільтраў канала + Схаваць паліцу ўдзельнікаў + Схаваць спіс удзельнікаў канала + Схаваць паліцу пастоў + Схаваць кнопку «Крама» + Схаваць кнопку «Падпісацца» + Меню \"Даследаванне\" + Выберыце, якія кампаненты будуць адлюстроўвацца ў меню даследавання + Схаваць меню курсаў + Схаваць кнопку \"Даследаваць\" + Схаваць меню «Мода і прыгажосць» + Схаваць меню \"Гульні\" + Схаваць меню \"Hype\" + Схаваць меню «Навучанне» + Схаваць меню \"Прамыя эфіры\" + Схаваць меню \"Членства\" + Схаваць меню «Кіно і ТБ» + Схаваць меню \'Музыка\' + Схаваць меню \"Навіны\" + Схаваць меню \"Playables\" + Схаваць меню «Падкасты» + Схаваць меню «Пакупкі» + Схаваць меню «Спорт» + Схаваць меню YouTube Premium + Схаваць меню YouTube Studio + Схаваць меню YouTube Music + Схаваць меню YouTube Kids + Схаваць меню стварэння YouTube + Схаваць меню «Як працуе YouTube» + Схаваць ніжніягалоўкі з юрыдычнай інфармацыяй і прыватнасцю + Каментары + Выбірайце, якія кампаненты будуць паказвацца ў раздзеле каментарыяў + Уключаць фільтр каруселі каментарыяў + "• Пераканайцеся, што ўвялі дакладнае імя элемента каруселі (напрыклад, Ask, Hype this video, Listen on YouTube Music) +• Схаванне элементаў каруселі можа прывесці да непрадбачальных пабачных эфектаў +• Калі ўсе элементы каруселі схаваны, увесь раздзел каментарыяў таксама будзе схаваны" + Фільтр каруселі каментарыяў + Увядзіце назвы каруселяў каментарыяў для фільтрацыі, аддзеленыя новымі радкамі + Схаваць AI-рэзюмэ чата + Схаваць правілы канала + Схаваць кантэкстныя паведамленні + Схаваць загаловак \"Каментары ўдзельнікаў\" + Схаваць раздзел каментараў + Схаваць раздзел каментарыяў у стужцы галоўнай старонкі + Схаваць Правілы супольнасці + Схаваць кнопку \"Стварыць Short\" + Схаваць кнопку \"Не падабаецца\" + Схаваць кнопку эмодзі + Схаваць опцыі панэлі фільтраў + Схаваць усе опцыі панэлі фільтраў, за исключением \"Папулярныя\" і \"Новыя\" + Схаваць анімацыю падарункаў і карткі + Схаваць кнопку \"Падараваць\" + Схаваць кнопку Інфа + Схаваць панэль данатараў жывога чата + Схаваць кнопку пракруткі жывога чата ўніз + Схаваць падказкі жывога чата + Схаваць ўсплываючыя падказкі, якія паказваюцца каля паля ўводу чата, напрыклад, паведомленне аб чаце толькі для падпісчыкаў + Схаваць кнопку меню + Схаваць кнопку меню з 3 кропкамі для кожнага каментарыя + Схаваць папярэдні прагляд каментара + Хавае папярэдні прагляд каментарыяў пад відэа перад адкрыццём поўнага раздзела каментарыяў + Каментар схаваны Morphe… + Уключаць мінімалістычную кнопку каментарыяў + Схаваць карткі, што высуваюцца, і селектар старонак, якія адлюстроўваюцца на кнопцы каментарыяў + Схаваць кнопку \"Дзякуй\" + Схаваць кнопку часовай меткі + Схаваць кнопку \"Найлепшыя фанаты\" + Схаваць кнопку «Перакласці» + Схаваць паведамленне «Каментарыяў пакуль няма» + Ачысціць выдзяленыя спасылкі пошуку + Ачышчае выдзяленыя спасылкі пошуку, пакідаючы толькі арыгінальны звычайны тэкст + Фільтр ключавых слоў + Выбірайце канкрэтныя словы або фразы, каб схаваць відэа ў стужках і пошуку + Схаваць каментары па ключавых словах + Схаваць відэа на Галоўнай па ключавых словах + Схаваць вынікі пошуку па ключавых словах + Схаваць відэа ў Падпісках па ключавых словах + Ключавыя словы для схавання + "Увядзіце ключавыя словы і фразы для схавання, аддзеленыя новымі радкамі + +Ключавыя словы могуць быць любым тэкстам, які паказваецца ў назвах відэа або каналаў + +Словы з вялікімі літарамі ў сярэдзіне павінны супадаць дакладна (напрыклад, iPhone, TikTok, LeBlanc)" + "Галоўная / Падпіскі / Пошук / Каментары фільтруюцца, каб схаваць кантэнт, які адпавядае ключавым фразам + +Абмежаванні: +• Shorts нельга схаваць па назве канала +• некаторыя кампаненты інтэрфейса могуць застацца бачнымі +• пошук па ключавым слове можа не даць вынікаў" + Супадаць з цэлымі словамі + Выніканне ключавога слова/фразы ў двойных лапках прадухіліць частковыя супаданні ў назвах відэа і каналаў<br><br>Напрыклад,<br><b>\"ai\"</b> схавае відэа: <b>How does AI work?</b><br>але не схавае: <b>What does fair use mean?</b> + Нельга выкарыстаць ключавое слова: %s + Дадайце лапкі, каб выкарыстаць ключавое слова: %s + Ключавое слова мае супярэчлівыя дэкларацыі: %s + Ключавое слова занадта кароткае і патрабуе лапак: %s + Ключавое слова скрое ўсе відэа: %s + "• Галоўная: %s +• Падпіскі: %s +• Пошук: %s +• Каментары: %s +• Усяго: %s" + Статыстыка + Схаваныя відэа, за апошнія 24 гадзіны + Схаваныя відэа, за ўвесь час + Скінуць лічыльнік за апошнія 24 гадзіны? + Скінуць лічыльнік за ўвесь час? + Фільтр AI-каналаў + Схаваць відэа з каналаў, пазначаных як AI-генераваны кантэнт спісочнам супольнасці AiSList + Чорны спіс: Высокая давернасць + Спіс папярэджанняў: Срэдняя давернасць + Схаваць відэа на Галоўнай + Схаваць відэа ў пошуку + "• Галоўная: %s +• Пошук: %s +• Усяго: %s" + Каналы з чорнага спіса + Каналы са спіса папярэджанняў + Уключыце фільтр, каб загрузіць каналы + Загрузка каналаў… + "Галоўная старонка і вынікі пошуку фільтруюцца, каб схаваць відэа з каналаў, пазначаных AiSList + +Абмежаванні: +• Відфільтраваныя AI Shorts могуць выглядаць як пустая прастора +• Некаторыя AI-відэа ўсё яшчэ могуць адлюстроўвацца" + "AiSList — гэта спіс YouTube-каналаў з кантэнтам, створаным штучным інтэлектам, з адкрытым кодам, які падтрымліваецца пазіянерамі. Каналы рэкамендуюцца карыстальнікамі і праходзяць праверку перад даданнем + +Спіс спампоўваецца з GitHub, а відэа фільтруюцца на вашай прыладнасці" + Паведаміць пра AI-каналы + Дадаць пункт справазвесткі ў меню відэа + Дадаць пункт у відэаменю для паведамлення пра канал відэа ў AiSList + Імя падавальніка + Імя, якое паказваецца побач з вашымі паведамленнямі ў публічнай табліцы лідараў AiSList. Паведамленні недаступныя, пакуль гэтае поле пустое + Паведаміць пра AI-канал + "Адправіць %s у AiSList, выкарыстоўваючы гэтае відэа як доказательство? + +Паведамляйце толькі пра тыя каналы, якія: +• Выкарыстоўваюць AI-галасы, візуальныя эфекты, скрыпты або музыку як асноўны кантэнт +• Масава ствараюць кантэнт з дапамогай AI-інструментаў пры мінімальнай удзеле чалавека + +Не паведамляйце пра каналы, якія толькі абмяркоўваюць AI або выкарыстоўваюць AI для некаторых частак сваіх відэа. + +Паведамленні з'яўляюцца публічнымі і праверяюцца перад даданнем канала ў спіс. Выдаленне магчыма толькі праз запыт на GitHub" + Гэты канал ужо знаходзіцца ў чорным спісе AiSList + Гэты канал ужо знаходзіцца ў спісе папярэджанняў AiSList + Паведаміць + Каб паведаміць пра каналы, усталюйце імя пасыльніка ў наладах фільтра AI-каналаў + Пошук канала… + Не удалося знайсці канал гэтага відэа + Пра канал паведамілі, чакаем праверкі + AiSList не змагў захаваць справаздачу, паспрабуйце пазней + Адпраўлено занадта шмат справаздач, паспрабуйце пазней + AiSList адхіліў справаздачу + Памылка адпраўкі справаздачы, праверце падключэнне + Белы спіс рэкламных каналаў + Відэа з каналаў з белага спіса прайграваюць з рэкламай, каб іх стваральнікі атрымлівалі аплату. Рэклама застаецца заблакаванай на ўсіх іншых каналах + Дадаць канал у белы спіс рэкламы + Выдаліць канал з белага спіса рэкламы + Схаваць паліцу крамы аўтара + Схаваць банэр крамы ў канцы відэа + Схаваць агульныя рэкламы + Схаваць банэры з таварамі + Схаваць пазнакі платнага прасоўвання + Схаваць ўсплывальную рэкламу ў прагравацелі + Схаваць карткі з уласнай спонсарайskiej рэкламай + Схаваць спасылкі на пакупкі + Схаваць відэарэкламу + Схаваць прапановы YouTube Premium + Загрузка… + Потэчны канал: + Дадаць у белы спіс Выдаліць + Белыя спісы каналаў + Няма каналаў у белым спісе + Канал дададзены ў белы спіс + Канал выдалены з белага спіса + Пошук у канале + Дадае другую кнопку пошуку на старонках каналаў для пошуку толькі па канкрэтным канале + Пошук канала + Вынікі пошуку канала + У гэтым канале нічога не знайдзена + Не ўдалося выканаць пошук у гэтым канале + Відэа + Посты + Ні адзін пост на гэтым канале не адпавядае запыту + Рэлевантнасць + Самапраглядальныя + Найменш прагледжаныя + Новейшыя + Найстарэйшыя + Самыя кароткія Short + Самыя доўгія + Спасылка скопіявана ў буфер абмена + Спасылка з частавай знакамі скопіявана ў буфер абмена + Паказаць кнопку \"Капіяваць спасылку на відэа\" + Націсніце, каб скапіяваць спасылку на відэа, або націсніце і ўтрымайце, каб скапіяваць яе з частавай знакамі + Капіяваць метку часу па змаўчанні + Змяняе дзеянні пры націсканні і зацісканні кнопкі \"Капіяваць спасылку на відэа\" + Капіяваць загаловак відэа + Націсніце і ўтрымайце загаловак відэа, каб скапіяваць яго + Загаловак відэа скапіяваны ў буфер абмену + Скапіяваць каментары + Націсніце і ўтрымайце каментар, каб скапіяваць яго + Каментар скопіраваны ў буфер абмену + Націсніце і ўтрымайце, каб адкрыць меню відэа + Доўгае націсканне ў любым месці відэа ў стужцы адкрывае яго меню + Паказваць кнопку \"Глядзець пазней\" на мініятурах + Кнопка \"Глядзець пазней\" паказваецца на мініятурах відэа ў стужцы. Доўгае націсканне на яе адкрывае пункт \"Захаваць у плэйліст\" + Спампаваць выявы з паста + Кнопка спампаваць паказваецца побач з кнопкай «Паділіцца» ў пастах з выявамі або GIF-анімацыямі, а ў іх меню дадаецца пункт спампаваць + Спампаваць выявы + Спампаваць выяву + Спампаваць %s выяў… + Спампаваць выяву… + %1$s выяў захавана ў %2$s + Выява захавана ў %s + Не ўдалося спампаваць выяву + У гэтым пасе выявы не знойдзены + Дадаць пункт «Спытаць» у выпадаючае меню + Дадае пункт «Сціснуць відэа» у выпадаюе меню стужкі + "Для работы Ask спачатку патрабуецца ўваход у ўліковы запіс у краіне, дзе Ask даступны. + +Пасля таго як відэа з кнопкай Ask будзе адкрыта адзін раз, функцыя будзе працаваць з усімі відэа, нават калі ў бягучай краіне функцыя Ask недаступная." + Сціснуць відэа + Не ўдалося адкрыць функцыю \"Задаць пытанне\" для гэтага відэа + Функцыя \"Задаць пытанне\" недаступная для вашага ўліковага запісу або бягучай краіны + Паказваць колькасць праглядаў на мініятурах + Колькасць праглядў відэа ў стужцы паказваецца ў ніжнім левым куце іх мініятураў замест таго, каб быць пад загалоўкам. У некаторых відэа, напрыклад, у паліцах, колькасць праглядў можа не паказвацца + Паказваць колькасць лайкаў на мініятурах стужкі + Паказваць працэнт лайкаў на мініятурах стужкі + Пошук па каментарах + Націсніце кнопку пошуку ў каментарах, каб паказаць толькі загружаныя каментары, якія ўтрымліваюць усе шуканыя словы. Пастаўце словы ў лапкі, каб шукаць фразу + Пошук па каментарах + Словы для пошуку Пошук Ачысціць + Змяніце парадак сартавання або адкрыйце каментары зноў, каб пашукаць у іх + Пошук ачышчаны. Змяніце парадак сартавання або адкрыйце каментары зноў, каб паказаць усе каментары + Адключаць ўсплывальнае акно \"Увайсці ў TV\" + Адключаць ўсплывальнае акно \"Падключыце сваі прылады\" + Адключаць прапуск главы па двайным націсканні + Адключае жэст падвойнага пастукання двума пальцамі для пераскоку па раздзелах, які можа быць выпадкова актываваны адным пальцам + Спампаваць відэа, падкасты або песні з дапамогай старонніх праграм для спампоўкі + Паказаць кнопку спампоўкі ў накладання плеяра + Паказвае кнопку ў відэаплееры, якая адкрывае ваш знешні загрузчык + Хуткая загрузка панацісканні + Прапановы загрузку ў фонавым рэжыме. Націсніце і ўтрымайце, каб адкрыць загрузчык. Працуе з YTDLnis і YTindir + Адзначаць загрузкі як праглянутыя + Відэа, загружаныя з дапамогай знешняга загрузчыка, дадаюцца ў гісторыю прагляду YouTube + Full HD (1080p) + Срэдняя (192 kbps) + Хуткая загрузка + Відэа + Аудыё + Усе параметры + Паказваць спампаваць у меню Shorts + Дадае пункт выпадаючага меню з вонкавым загрузчыкам у плеер Shorts + Загрузіць + Галасовы пераклад + Чытайце субтытры відэа ўгульс з дапамогой сінтэзаўчы мовы на прыладзе, сінхранізаванага з відэазапісам + Паказвае кнопку паверх відэаплеяра. Націсніце, каб уключыць/выключыць пераклад, або націсніце і ўтрымайце для хуткага доступу да налад + Мова + Пастаўшчык перакладу + Гучнасць арыгінальнага аўдыё + Гучнасць арыгінальнага аўдыё YouTube, пакуль ідзе пераклад + "Бясплатны API перакладу, ключ API не патрабуецца. Да 5 000 сімвалаў у дзень на бясплатным тарыфе або да 50 000 сімвалаў у дзень пры наяўнасці адраса электроннай пошты. + +Націсніце тут, каб адкрыць MyMemory.translated.net" + Электронная пошта MyMemory + Неабавязкова. Увядзіце ваш адрас электроннай пошты MyMemory, каб павялічыць штодзённы ліміт з 5 000 да 50 000 сімвалаў. Застаўце поле пустым, каб выкарыстоўваць анонімны ліміт + "Платны пераклад зІ праз OpenRouter. Папаўніце рахунак адзін раз і перакладзіце тысячы відэа. Mistral Nemo — самы танны варыянт, Gemma 4 прапануе найвышэйшую якасць. + +Націсніце тут, каб адкрыць OpenRouter.ai" + Ключ API OpenRouter + Ваш ключ API OpenRouter. Стварыце ключ на OpenRouter.ai > Workspace > API Keys + Мадэль OpenRouter + Карыстальніцкае + OpenRouter не настроены + Перад выбарам гэтага правайдэра ўвядзіце ваш API-ключ OpenRouter у наладках + Бясплатна + %s за 100 відэагадзін + Голас + Сістэма + Пераклад недаступны + "Служба перакладу вернула памылку HTTP %s. Ваша IP-адрас, верагодна, быў абмежаваны па колькасці запытаў на тэрмін да 24 гадзін. + +Каб працягнуць карыстацца перакладамі, пераключыцеся на іншага пастаўшчыка перакладу ў наладках або выкарыстайце VPN" + Больш не паказваць + "Вашы налады ўтрымліваюць API-ключ OpenRouter. + +Ваш API-ключ ніколі не павінен перадавацца іншым. Любы, хто мае даступ да яго, можа выкарыстаць вашы крэдыты OpenRouter" + Дасягнута ліміт kredітоў OpenRouter + Ваш ліміт крэдытаў OpenRouter вычарпаны. Папоўніце баланс або павялічыце ліміт выдаткаў па ключы на OpenRouter.ai + Адкрыць OpenRouter.ai + Пераклад OpenRouter не ўдался (HTTP %s) + Штодзённы ліміт MyMemory вычарпаны + Наступны даступны %s + Вы выкарысталі сённяшнюю бясплатную квоту перакладу MyMemory (5 000 сімвалаў). Дадайце адрас электроннай пошты MyMemory ў наладках, каб павялічыць штодзённы ліміт да 50 000 сімвалаў, або пераключыцеся на іншы сэрвіс перакладу + Вы выкарысталі сённяшнюю квоту перакладу MyMemory (50 000 сімвалаў). Пераключыцеся на іншы сэрвіс перакладу або паспрабуйце зноў заўтра + Пераклад MyMemory не ўдаўся (статус %s) + Уключыць зацыкліванне відэа + Не запам’яць цыклаванне відэа + Забароніць захаванне бягучага стану цыклавання відэа для наступных відэа + Паказваць кнопку \"Цыклаваць відэа\" + Зацыкліванне відэа ўключана + Зацыкліванне відэа выключана + Дыяпазон цыкла + Пачатак + Канiec + [гг:]хх:сс + [гг:]хх:сс (пуста = канец відэа) + Усталяваць + Ачысціць + Цыклаванне %1$s - %2$s + Няправільны часовы дыяпазон + Паказваць кнопку \"Карцінка ў карцінцы\" + Паказваць меню \"Карцінка ў карцінцы\" + Пераключыцца на режим \"Карцінка ў карцінцы\" + Паказваць кнопку \"Праіграць усё\" + "Націсніце, каб стварыць плэйліст з усіх відэа канала, націсніце і ўтрымайце, каб скасаваць + +Інфа: можа не працаваць для прамых трансляцый" + Рэжым генерацыі плэйліста + Увесь кантэнт (сартаваць па часе, па нарастучай) + Увесь кантэнт (сартаваць па часе) + Увесь кантэнт (сартаваць па папулярнасці) + Толькі відэа (сартаваць па часе) + Толькі відэа (сартаваць па папулярнасці) + Толькі Shorts (сартаваць па часе) + Толькі Shorts (сартаваць па папулярнасці) + Толькі трансляцыі (сартаваць па часе) + Толькі трансляцыі (сартаваць па папулярнасці) + Толькі ўвесь кантэнт для спонсараў + Відэа толькі для спонсараў + Shorts толькі для ўдзельнікаў + Прамыя трансляцыі толькі для ўдзельнікаў + Не удалось стварыць плэйліст з-за нязгоднасці ID канала + Паказваць кнопку \"Перазагрузіць відэа\" + ID відэа недаступны. Калі ласка, перазагрузіце відэа ўручную + Паказваць кнопку \"Захаваць у \"Глядзець пазней\" + Націсніце, каб захаваць у «Глядзець пазней», націсніце і ўтрымайце для выбару варыянтаў чаргі + Відэа паспяхова захавана ў спіс «Глядзець пазней» + Гэта відэа ўжо захавана ў спісе «Глядзець пазней» + Не удаецца захаваць гэтае відэа ў спіс «Глядзець пазней» + Захаваць у «Глядзець пазней» + Паказать кнопки \"Зглядеть пазней\" у выпадаусим настройки наградки + Дазвляе зарахить текущий видеавио у список \"Зглядеть пазней\" з настройки наградки видеавио + Паказваць кнопку «Захаваць у «Глядзець пазней»» у дзіцячых відэа + Дазвляе зарахить дети для детей видеавио у список \"Зглядеть пазней\" з выпадаусим меню + Паказать кнопки \"Зглядеть пазней\" у выпадаусим шортсой + Дазваляе захаваць Shorts у спіс \"Паглядзець пазней\" праз выпадаючае меню + Перанакіраваць \"Глядзець пазней\" на плейліст + Глядзець пазней + Дадаць у чаргу Morphe + Захоўваць чаргу паміж сесіямі + Чарга захоўваецца пасля перазапуску прыкладання + Паменять месцамі дзеянні «Захаваць» і «Чарга» + Націсніце для выбару варыянтаў чаргі, націсніце і ўтрымайце, каб захаваць у «Глядзець пазней» + Тып «Сярэдняе ў чарзе» Звычайны + Натыўная чарга + Чарга Morphe + Чарга Morphe на баку кліента + Дадаць пункт меню чаргі ў ўсплывальнае акно стужкі + Дадае дадатковы пункт у ўсплывальнае акно. Уключыце гэта, калі \"Дадаць у чаргу\" не паказваецца + Чэрга + Чэрга (%s) + Ачысціць усё + Чэрга пустая + Выкарыстоўвайце «Праграваць наступным у чэрзе» для любога відэа + Праграецца зараз + Праграць наступнае ў чарзе + Праграць апошні ў чарзе + Змяніць парадак Выдаліць + Дададзена ў чаргу + Не ўдалося дадаць гэтае відэа ў чаргу + Плэйлісты нельга дадаваць у чаргу + Чарга запоўнена + Не ўдалося запусціць відэа + Белы спіс рэкламы ў выпадаючым меню + Дадае пункт у выпадаючае меню, які дазваляе праграваць рэкламу на канале гэтага відэа + Гэтая функцыя можа патрабаваць выкарыстання PotHelper або змены \"Імітацыя відэапатокаў\" на ўваход праз TV або Android VR + Белы спіс хуткасці ў выпадаючым меню + Дадае пункт у выпадаючае меню, які заўсёды прагравае канал гэтага відэа са хуткасцю 1.0x + Не аўтарызаваны + Няма ID плэйліста + Чарга пустая + Няма ID відэа + Памылка: %s + Не ўдалося дадаць у чаргу + Не ўдалося стварыць чаргу + Не ўдалося выдаліць з чаргі + Не ўдалося захаваць чаргу + Дададзена ў чаргу + Чарга створана + Выдалена з чаргі + Чарга захавана ў %s + Чэрга Morphe + Дадаць у чаргу + Дадаць у чаргу і адкрыць + Дадаць у чаргу і праіграць + Дадаць і перазагрузіць відэа + Выдаліць з чаргі + Выдаліць і адкрыць чаргу + Выдаліць і перазагрузіць відэа + Адкрыць чаргу + Захаваць чаргу ў плейліст + Плэйліст захаваны + Захавана ў %s + Ачысціць чаргу + Чарга ачышчана + Чарга + Праіграць апошнім у чарзе + Праіграць наступнае ў чардзе + YouTube не змог паказаць чаргу. + Чарга не гатова для гэтага відэа. Адкрыйце відэаплеер і паспрабуйце зноў. + Дадаць мікс у чаргу ці праглядзець мікс? + Праглядзець + Дадаць у чаргу + Загрузка мікса… + Адкрыць канал праз аватар прамой трансляцыі + Забірае адкрыццё прамой трансляцыі пры націсканні на аватар канала + Адключыць жэст дакладнай перамоткі + Адключае рэжым дакладнага перамоткі, які актывуецца пры пацягванні папоўскі перамоткі ўверх + Уключаць DVR прамых трансляцый + Дазваляе пераматываць усе прамыя трансляцыі, нават тыя, дзе гэта звычайна абмежавана + Уключаць мініятуры ў паўзунку пракруткі + Паказвае мініятуру відэа пры перамішчэнні паўзунка пракруткі + Найбольш пракручаны + "\"Уключаць DVR прамых трансляцый\" можа быць недаступна + +Каб выправіць гэта, уключыце \"Падараць відэапатокі\" і змяніце \"Кліента па змаўчанні\" на \"TV Simply\" або \"visionOS\"" + Павялічыць працягласць DVR прамых трансляцый + Дазваляе пераматываць прамыя трансляцыі на 7 дзён назад + "Функцыя \"Пашырыць працягласць DVR прамой трансляцыі\" можа быць недаступнай + +Каб выправіць гэта, уключыце \"Імітаваць відэапатокі\" і змяніце \"Кліента па змаўчанні\" на \"TV Simply\" або \"visionOS\"" + Уключаць пракрутку для перамотвы + Уключае працягванне для перамоткі замест прайгравання з хуткасцю 2x пры націсканні і ўтрыманні відэаплэера + Уключаць партак для перамоткі + Проста націсніце на папоўску перамоткі, каб скакаць на гэтую пазіцыю без яе перацягвання + Жэст працягвання ўлева + Жэст працягнення ўправа + Жэст працягнення ўверх + Шырыня левай/правай зоны + Працэнт кожнага краю экрана, зарэзаваны для жэстаў прагартвання ўлева і ўправа (5-50) + Яркосць + Гучнасць + Натыўны Выкл. + Ігнараваць прагартванні пры блакіроўцы + Жэсты прагартвання ігнаруюцца, пакуль плеер заблакіраваны з дапамогой функцыі \"Блакіроўка экрана\" + Уключаць жэст «націсніць, каб праціркнуць» + Патрабуецца націсканне і ўтрыманне перад тым, як будзе распазнаны жэст працягвання + Уключаць тактыльны водгук + Запамінае ўзровень яскравасці пры ўваходзе / выхадзе з паўнаэкраннага рэжыму + Перамяшчэнне паўзунка яскравасці да мінімальнага значэння актывуе сістэмную аўтаяскравасць + Наладзіце ўзровень празрыстасці (0 — празрысты, 100 — непрозрачны) + Колер пакрыцця прагарткі яскравасці + Усталюйце колер палоскі прагрэсу рэгулятара яскрасці + Усталюйце колер палоскі прагрэсу рэгулятара гучнасці + Колер паказчыка хуткасці свайпа + Усталюйце колер палоскі прагрэсу элементаў кіравання хуткасцю прайгравання + Памер тэксту ў накладання свайпа + Наладзіце памер тэксту на экране (1–30) + Усталюйце неабходную дыстанцыю свайпа для актывацыі рэгуляцыі + Частривливасть свайпа граммисты гусникки + Вышейшыйся значенниусть паребвавниутся большй свайп на кожни ступень гусникки + Ступень гусникки + Павелічэнне гучнасці + Свайп уверх за максімальны узровень гучнасці павышае яго, што можа выклікаць іскажэнне гучнага аўдыё + Параметр па заводкам + Чулавасць свайпа яркосці + Большыя значэнні патрабуюць больш свайпаў для кожнага кроку яркосці + Чулавасць свайпа хуткасці + Большыя значэнні патрабуюць больш свайпаў для кожнага кроку хуткасці + Крок хуткасці + Вышыня зоны хуткасці + Вышыня зоны жэсту хуткасці як працэнт ад вышыні плеера (5-75) + Хуткасць + Гарызантальны слой + Круглае пакрыццё (мінімалістычнае) + Вертыкальнае пакрыццё + Вертыкальнае пакрыццё (мінімалістычнае) + Уключаць прагартванне для змены відэа + Свайпайце ў паўнаэкранным рэжыме, каб перайсці да наступнага або папярэдняга відэа + Кнопкі дзеянняў + Выберыце, якія кнопкі дзеянняў будуць адлюстроўвацца ў партатрэтным і паўнаэкранным рэжымах + Кнопкі ў партатным рэжыме + Адключацьсвечэнне кнопак \"Падабаецца\" і \"Падпісацца\" + Забараняе кнопкам \"Падабаецца\" і \"Падпісацца\" свяціцца, калі творца згадвае іх + Схаваць панэль дзеянняў + Схаваць кнопку \"Падабаецца\" і \"Не падабаецца\" + Схаваць кнопку \"Паділіцца\" + Схаваць кнопку \"Спыніць рэкламу\" + Схаваць кнопку каментарыяў + Схаваць кнопку \"Падключачыся\" + Схаваць кнопку \"Паведаміць\" + Схаваць кнопку \"Рэмікс\" + Схаваць кнопку \"Спампаваць\" + Схаваць кнопку \"Хайп\" + Схаваць кнопку \"Прасоўваць\" + Схаваць кнопку \"Дзякуй\" + Схаваць кнопку \"Спытаць\" + Схаваць канал і кнопку \"Падпісацца\" + Схаваць аватар канала і кнопку \"Падпісацца\" ў пачатку панэлі дзеянняў + Схаваць кнопку «Падпісацца» + Схаваць кнопкі «Падпісацца» і «Далучыцца» побач з аватарам канала ў пачатку панэлі дзеянняў + Схаваць кнопку \"Кліп\" + Гэта можа працаваць не для ўсіх уліковых запісаў карыстальнікаў + Схаваць кнопку \"Крама\" + Схаваць кнопку \"Захаваць\" + Схаваць кнопку \"Яшчэ\" + Кнопкі ў паўнаэкранным рэжыме + Схаваць кнопку \"Не падабаецца\" + Схаваць кнопку \"Падабаецца\" + Схаваць кнопку \"Жывы чат\" + Схаваць кнопку Mix + Схаваць кнопку \"Больш відэа\" + Схаваць кнопку \"Плэйліст\" + Верхні адступ хуткіх дзеянняў + Усталюйце верхні адступ панэлі хуткіх дзеянняў (0-32 dp) + Субтытры + Кіраваць аўтаматычнымі субтытрамі і файлы cookie субтытраў + Відзімасць аўтаматычных субтытраў + Заўсёды паказваць + Ніколі не паказваць + Паказваць, калі гук уключаны + Паказваць пры выключаным гуку + Прымежаная мова субтытраў + Сперша выбіраюцца субтытры пастаўшчыка, затым яны аўтаматычна перакладаюцца на прымежаную мову Выкл. + Дадаць дадатковыя мовы аўтаперакладу + Дадае больш моў у меню аўтаперакладу + Усталяваць cookies для субтытраў + "Выпраўляе памылку стандартнага YouTube, пры якой аўтапераклад субтытраў можа быць недаступны з-за адсутнасці cookies + +Павінны быць прадстаўлены «cookies субтытраў»" + Cookies субтытраў + Cookies, якія будуць усталяваны ў запытах Timed Text API (Caption Data API) + Атрымаць cookies субтытраў + Націсніце тут, каб атрымаць cookies + Не удалось атрымаць файлы cookie + Файлы cookie ўжо існуюць + Файлы cookie паспяхова абноўлены + Выправіць транскрыпцыю + Выпраўляе памылку праверкі папярэдніх умоў, якая можа перашкодзіць загрузцы тэксту відэа + Паказваць субтытры ў рэжыме „карціна ў карціне“ + Кнопкі навігацыі + Выберыце, якія кнопкі будуць адлюстроўвацца ў ніжняй панэлі навігацыі + Схаваць кнопку \"Галоўная\" + Схаваць кнопку Shorts + Схаваць кнопку стварэння + Схаваць кнопку \"Падпіскі\" + Схаваць кнопку \"Апавяшчэнні\" + Паказаць кнопку пошуку + Пазіцыя кнопкі пошуку + Паказаць кнопку налад + Пазіцыя кнопкі налад + Дзеянне кнопкі Налад + Адкрывае налады Morphe, калі ўключана, або налады YouTube, калі выключана + Замяніць кнопку \"Стварыць\" кнопкай \"Апавешчэнні\" + Калі змена гэтай налады не дае выніку, паспрабуйце пераключыцца ў рэжым інкагніта + Схаваць подпісы кнопак навігацыі + Схаваць кропку новага кантэнту + Схаваць панэль навігацыі + "Гэта цалкам хавае панэль навігацыі, уключаючы ўкладку \"Вы\". + +Каб атрымаць доступ да Налад, уключыце кнопку Налад праз Агульныя → Панэль інструментаў → Паказваць налады" + Уключаць вузкія кнопкі навігацыі + Зменшвае гарызантальны інтэрвал паміж кнопкамі + Адключаць кнопкі навігацыі без подпісаў + Паказваць кнопкі навігацыі з подпісамі, калі YouTube адпраўляе іх без подпісаў + Адключаць навігацыйныя ўкладкі панэлі інструментаў + Паказвае \"Падпіскі\" ў панэлі навігацыі, калі YouTube перанясе іх у ўкладку ў верхняй частцы Галоўнай старонкі + Уключаць анімацыі панэлі навігацыі + Анімуе пераходы пры пераключэнні паміж ўкладкамі + Адхіліць аўтаматычнае схавань панэлі навігацыі + Залишае панэль навігацыі видимай пры пракрутцы + Стыль панэлі навігацыі Звычайны + Паўпразрачны + Празрысты + Непразрысты + Гэты стыль можа не падтрымлівацца вашай прыладай або версіяй YouTube, што можа прывесці да праблем з макетам або збояў + Панэль інструментаў + Выберыце, якія кампаненты будуць адлюстроўвацца ў панэлі інструментаў + Схаваць кнопку Cast + Схаваць кнопку чата + Схаваць кнопку мікрафона + Схаваць кнопку пошуку + Калі ўключана, націсканне адкрывае налады Morphe, а доўгае націсканне — налады YouTube; выключайце, каб памяняць іх месцамі + Выпадаючыя меню + Выберыце, якія кампаненты будуць адлюстроўвацца ў выпадаючым меню + Схаваць «Дадатковыя налады» / меню «Яшчэ» + Схаваць меню «Атмасферны рэжым» + "Меню \"Аўдыёдарожка\" схавана + +Каб паказаць меню \"Аўдыёдарожка\", змяніце \"Імітацыя відэапатокаў\" на любы кліент, акрамя \"Android Studio\" і \"Android VR Downgraded\"" + Схаваць меню \"Аўдыядарожка\" + Схаваць ніжнюю частку меню \"Аўдыядарожка\" + Схаваць меню \"Субтытры\" + Схаваць ніжнюю частку меню \"Субтытры\" + Схаваць верхнюю частку меню \"Субтытры\" + Схаваць меню «Дапамога і водгукі» + Схаваць меню \"Слухаць у YouTube Music\" + Схаваць меню \"Блакіроўка экрана\" + Схаваць меню «Паўтор відэа» + Схаваць меню \"На ходзе\" + Схаваць меню \"Якасць\" + Схаваць ніжнюю частку меню \"Якасць\" + Схаваць верхнюю частку меню \"Якасць\" + Схаваць меню «Таймер сну» + Схаваць меню \"Шумясць прайгравання\" + Схаваць меню «Стабільная гучнасць» + Схаваць меню \"Глядзець у VR\" + Схаваць папярэдні прагляд аўтазапуску + Хавае папярэдні прагляд наступнага відэа ў паўны экран перад пачатком аўтазапуску + Кнопкі паверхні + Выберыце, якія кнопкі будуць адлюстроўвацца поверх відэаплэера + Схаваць кнопку \"Аўтазапуск\" + Схаваць кнопку \"Субтытры\" + Схаваць кнопку Cast + Схаваць кнопку \"Згарнуць\" + Схаваць кнопку \"Паўны экран\" + Хавае цёмную зацянелую вобласць вакол элементаў кіравання відэаплэерам + Празрачнасць фона кнопак кіравання + Наладаць празрыстасць фона за кнопкамі кіравання плеярам і палосай часу (0 — нябачны, 100 — сплошны) + Схаваць кнопкі «Папярэдні» і «Наступны» + Схаваць кнопку \"Налады\" + Атмасферны рэжым + Кіраванне бачнасцю рэжыму Ambient і абмежаваннямі па харчаванні + Абісці абмежаванні атмасфернага рэжыму + Дазваляе рэжыму Ambient заставацца актыўным у рэжыме эканоміі зарада + Адключаць атмасферны рэжым + Адключаць рэжым Ambient у паўнаэкранным рэжыме + Спыняе анімацыю лічыльніка лайкаў і праглядаў + Схаваць палоску прагрэсу прагляду, якая паказваецца на мініятурах відэа, якія вы ўжо паглядзелі + Уключаць паўнаэкранны вялікі паўзунак пракруткі + Схаваць Shorts + Схаваць на старонцы канала + Схаваць у стужцы \"Галоўная\" & павязаных відэа + Схаваць у выніках пошуку + Схаваць у стужцы \"Падпіскі\" + Схаваць у апісанні відэа + Схаваць у гісторыі праглядаў + Плэер Shorts + Выбірайце, якія кампаненты будуць адлюстроўвацца ў плееры Shorts + Адключаць пастаўленне лайка па двайным націсканні + Схаваць кнопку AI + Схаваць пазначку \"Аўтаматычна дубляваны\" + Схаваць кнопку \"Каментарыі\" + Схаваць кнопку \"Эфект\" + Схаваць надпіс спасылкі на відэа + Схаваць падказкі па жэстах + Схаваць кнопку \"Зялёны экран\" + Схаваць кнопку \"Хэштэг\" + Схаваць кнопку \"Далучыцца\" + Схаваць кнопку \"Падабаецца\" + Схаваць анімацыю fonte-эфекту кнопкі \"Падабаецца\" + Схаваць жывы прагляд + Схаваць пазнаку месцазнаходжання + Схаваць панэль навігацыі + Схаваць кнопку \"Новыя пасты\" + Схаваць кнопкі паўзы ў накладанні + Схаваць папярэдні прагляд каментарыя + Схаваць кнопку \"Рэмікс\" + Схаваць кнопку \'Захаваць\' + Схаваць кнопку \"Захаваць музыку\" + Схаваць прапановы пошуку + Схаваць кнопку \"Паділіцца\" + Схаваць кнопку \"Крама\" + Схаваць кнопку гуку + Схаваць кнопку \"Падпісацца\" + Схаваць кнопку \"Купіць Super Thanks\" + Схаваць пазначаныя тавары + Схаваць кнопку \"Далейшае\" + Схаваць кнопку \"Выкарыстаць гэты гук\" + Схаваць кнопку \"Выкарыстаць гэты шаблон\" + Схаваць рэкамендаванае відэа ў канцы + "Хавае ўкладку з рэкамендацыйнымі відэа, якая з'яўляецца пасля заканчэння відэа + +Гэтая настройка працуе толькі пры выключаным аўтазапуску. Вы можаце змяніць яе ў YouTube: +Налады → Відтвораванне (або Аўтазапуск) → Аўтазапуск наступнага відэа" + Схаваць падобныя відэа + Схаваць панэль \"Яшчэ відэа\" + Схаваць панэль \"Яшчэ відэа\", якая з\'яўляецца пры свайпінгу ўверх у паўнаэкранным рэжыме + Схаваць часовую метку відэа + Адключыць ўсплывальныя панэлі прагравацеля + Адключае аўтаматычнае адкрыццё панэляў плэйлістаў і жывога чата + Выключаць аўтазапуск плэйлістаў + Забараняе плэйлістам аўтаматычна пераходзіць да наступнага відэа + Адключаць запыт \"Працягнуць прагляд\" + Выходзіць з паўнаэкраннага рэжыма па заканчэнні відэа + Адключана + Толькі ў партрэтным рэжыме + Толькі ў альбомным рэжыме + Портратны і альбомны рэжымы + Прымусовы альбомны рэжым у паўнаэкранным рэжыме + Павароціць плеер у альбомны рэжым пры пераходзе ў паўнаэкранны рэжым. Недаступна на малых экранах, дзе паўнаэкранны рэжым ужо адкрываецца ў альбомным + Жэсты паўнаэкраннага рэжыма + Жэсты плэерам у паўнаэкранным рэжыме + Адключаць жэст свайпа ўверх у партатрынным рэжыме + Адключаць жэст свайпа ўніз у партатрынным рэжыме + Адключаць жэст свайпа ўніз у альбомам рэжыме + Выключаць жэст маштабавання пацягвай + Виввирациммастымми маӁштабанним применим для масштабанним + Адкрываць відэа ў паўнаэкранным рэжыме + Адключана + Портрат + Альбам + Маштаб відэа на ўвесь экран Звычайны + Растягнуць (запоўніць экран) + Маштабаваць (абрэзаць для запаўнення) + Паказваць кнопку маштабавання відэа + Націсніце, каб пераключацца паміж \"Па praising\", \"Растянуць\" і \"Маштабаваць\" + Паказваць толькі ў паўнаэкранным рэжыме + Схаваць кнопку маштабавання відэа ў плеяры ў партачырным рэжыме + Паказваць кнопку выключавання гуку + Празрачнасць паверхні плеера + Наладзіце ўзровень празрыстасці (0 — празрысты, 100 — непразрысты) + Панель пошуку Адключана + Шырокая + Вельмі шырокі + Раскладка прылады + Звычайны + Тэлефон + Планшэт + Аўтамабільны + "Змены ўключаюць: + +Планшэтны макет +• Пасты супольнасці схаваны +• Настройка «Відпраўканне ў стужках» недаступная +• Кнопка Remix і кнопка Sound недаступныя ў Shorts +• Накладка «Больш відэа» можа быць недаступная +• Паказваецца старая панэль дзеянняў відэа, якая не згарнута +• Пункты выпадаючага меню могуць працаваць няправільна + +Аўтамабільны макет +• Стужка арганізавана па тэмах і каналах +• Настройка «Відпраўканне ў стужках» недаступная +• Кнопка Remix і кнопка Sound недаступныя ў Shorts +• Накладка «Больш відэа» можа быць недаступная +• Shorts адкрываюцца ў звычайным прагравач +• Паказваецца старая панэль дзеянняў відэа, якая не згарнута" + Уключаць планшэтны размяшчэнне ў плееры + "Аднаўяе стары размяшчэнне плеера + +Абмежаванне: калі функцыя \"Аднавіць старую панэль дзеянняў відэа\" выключана, панэль \"Больш відэа\" можа быць недаступнай" + 21.11.48 - Вярнуць старую паводзіну субтытраў + 20.39.41 - Віднаўленне старога захавання ў плейліст + 20.28.41 - Адключаць тлустыя значкі + 20.13.41 - Віднавіць назву канала пад відэаплэерам + Адкрыць сістэмнае акно паділу + Адкрываць сістэмнае акно паділу замест унутрапрыкладнага пры паділе кантэнту + Перакрыць кнопкі YouTube Music + Адкрывае вашу прыляганую музыку замест стандартнай праграмы YouTube Music + Стартовая старонка + Звычайны + Усе падпіскі + Прагляд каналаў + Курсы / Навучанне + Даследаваць + Мода & прыгажосць + Геймінг + Гісторыя + Бібліятэка + Пастаўленыя лайкі + Прыма + Фільмы + Музыка + Навіны + Апавяшчэнні + Плэйлісты + Пошук + Пакупкі + Shorts + Спорт + Падпіскі + У трэндзе + Віртуальная рэальнасць + Глядзець пазней + Вашы кліпы + Адхіліць аднаўленне пры запуску + Адкрыць Shorts праз + Плэер Shorts + Звычайны плэер + Звычайны плэер на ўвесь экран + Аўтазапуск Shorts + Аўтаматычна пераходзіць да наступнага Short замест паўтораў current-ага + Аўтаматычнае праўтрыманне ў фонавым рэжыме + Аўтаматычна пераходзіць да наступнага Short падчас відпрацоўкі ў фоне + Прапусціць жывыя Shorts + Прапускаць прамыя трансляцыі, якія з\'яўляюцца ў стужцы Shorts + Відэапрацяг у стужках + Заўсёды ўключана + Толькі Wi-Fi Выкл. + Аднавіць арыгінальныя загалоўкі + Паказвае арыгінальныя назвы відэа, апісанні відэа і апісанні каналаў замест аўтаперакладанных + Загрузка загалоўка… + Адхіліць аўтаматычнае абнаўленне стужкі + Запабігае аўтаматычнаму абнаўленню стужак пасля таго, як яны састарэюць + Раскладка картэчак стужкі + Класічная + Картэчкі + Картэчкі з кнопкамі \"Падабаецца\", \"Каментар\" і \"Захаваць\" + Каментары + Адключыць паўзновленне мініплэера + Выключае мініплэер \"Працягнуць прагляд\", функцыю YouTube Premium, якая з\'яўляецца пры запуску праграмы + Мініплэер + Настройце выгляд і паводзіны зведзенага відэаплэера + Тып мініплэера + Схаваць загаловак і канал + Звычайны + Мінімалістычны + Мінімалістычны 2 + Планшэт + Сучасны 1 + Сучасны 2 + Сучасны 3 + Сучасны 4 + Адключаць закругленыя куты + Адключаць перацягванне + Забараняе перацягванне зведзенага плэера па розных кутах экрана + Забараняе закрываць мініплэер шляхам гарызантальнага працягвання за межы экрана + Паўза ў працьоўванні пры гарызантальным перацягванні + Відпраўка спыніцца, калі мініплэер будзе перацягнуты за межы экрана + Запамятаць пазіцыю стану перацягвання + Перацягванне мініплэера за межы экрана захавае стан яго інтэрфейсу + Схаваць кнопкі паверхні + Памер па змаўчанні + Усталюйце пачатковы памер на экране ў пікселях + Памер пікселя павінен быць ад %1$s да %2$s + Прадпрацязнасць паверхні + Наладуйце ўзровень празрыстасці (0 — празрысты, 100 — непрозрачны) + Кнопка \"Назад\" выходзіць зを選стрычкі + Націсканне кнопкі \"Назад\" заўсёды закрывае праграму замест пракруткі ўверху па стужцы + Прымусавыя анімацыі Звычайны + Заўсёды ўключана + Заўсёды выключана + Схаваць панэль стану + Хавае панэль стану сістэмы. Прачніце ўніз ад верхняга краю, каб паказаць яе на момант + Выправіць перакрыццё верхніх панэляў панэллю стану + Забяспечвае стабільнасць размяшчаўкі панэлі стану, каб яна не перакрывала верхнія панэлі і не пакідала пустага месца над плэерам. Выключайце гэтую функцыю, калі праграма вылетае або верхнія панэлі выглядаюць няправільна + Фон светлай тэмы Белы + Material You (белы) + Святла-рожавы + Святла-аранжавы + Усталяваць колер фона для светлай тэмы + Выкарыстоўваць колер тэмы для тэксту і іконак + Уключыць градыянтны экран загрузкі + Паказвае градыентны фон на экране загрузкі замест аднатоннага колары + Уключыць кастомны колер палоскі пракруткі + Колер палосы пракруткі + Усталюйце асноўны колер палоскі прагрэсу відэа + Акцэнтны колер Seekbar + Усталюйце дапаможны колер палоскі прагрэсу відэа + Непрыемнае значэнне колеру палоскі пракруткі + Логотып загалоўка + Звычайны + Рэгулярны + Карыстальніцкае + Абіходзіць рэгіянальныя абмежаванні для выяваў + Перанакіроўвае запыты на мініятуры ў альтернатыўны сервер, каб выправіць праблему з адсутнасцю выяваў у абмежаваных краінах + Загалоўкі DeArrow + Галоўная ўкладка + Укладка \"Падпіскі\" + Вы + Плэйлісты & рэкамендацыі плэера + Вынікі пошуку + Паказваць іконку DeArrow + Паказвае іконку DeArrow ў загалоўках відэа з калектыўнага рэдагавання + Пазільны рэжым + Паказвае арыгінальны загаловак замест загалоўка DeArrow, калі карыстальнікі DeArrow прагаласавалі, што ім падабаецца арыгінальны загаловак + Мінімальная колькасць \"пазільных\" галасаў для паказу арыгінальнага загалоўка + Колькасць пабужных галасаў у адной катэгорыі, напрыклад, «смешная» або «крэатыўная», неабходная для паказвання арыгінальнага загалоўка + Мінатуравы DeArrow + Арыгінальныя мініятуры + DeArrow & арыгінальныя мінацюры + DeArrow & статычныя кадры + Статычныя кадры + "DeArrow замяняе клікбэйтныя загалоўкі і мініятуры на краудсорсингавыя, якія з'яўляюцца больш дакладнымі і менш сенсацыйнымі. Загалоўкі пішуцца і галосуюцца супольнасцю, а мініятуры ўяўляюць сабой кадры з відэа + +Відэа запытываюцца па кароткім префіксе хешаванага ID відэа, таму сервер не можа дакладна вызначыць, якое відэа вы праглядаеце. Калі для відэа няма мініятуры DeArrow, паказваецца арыгінальная або статычны кадру" + Паказваць паведамленні пра памылку падключэння + Паказвае паведамленне, калі сервер DeArrow недаступны + Канцавая кропка API + URL-адрес эндпоінта кэша мінацюр DeArrow + Статычныя відэакадры + Статычныя кадры ўсё яшчэ бяруцца з пачатку / сярэдзіны / канца кожнага відэа. Гэтыя выявы ўбудаваны ў YouTube, і жодны знешні API не выкарыстоўваецца + Изцягнуць кадр відэа з + Пачатак + Сярэдзіна + Канiec + DeArrow часова недаступны (код стану: %s) + DeArrow часова недаступны + Павярнуць стары стыль кнопак прайгравача + Павяртае тлустыя элементы кіравання відэаплэерам да старэйшага, больш тонкага візуальнага стылю + Падмена памераў прылады + Уключанне гэтай функцыі можа адкрыць доступ да больш высокай якасці відэа + Уключанне гэтага можа прывесці да заікання відэа, пагаршэння часу працы акумулятара і невядомых пабочных эфектаў + Тактыльны водгук + Выбірайце, якія ўзаемадзеянні выклікаюць вібрацыю прылады + Выключаць тактыльны водгук раздзелаў + Выключаць тактыльны водгук дакладнага пераматывання + Адключаць тактыльны водгук пры скасоўванні перамоткі + Адключыць тактыльны водгук пры націсканні і ўтрыманні + Адключаць тактыльны водгук пры маштабаванні + Калі вы нядаўна змянілі даныя для ўваходу ў свой уліковы запіс, выдаліце і ўсталюйце MicroG паноў + Абходзіць перанакіраванні спасылак + Адкрывае арыгінальную спасылку напрамую, абходзячы спасылку адсочвання (youtube.com/redirect) + Адкрываць спасылкі ў браўзеры + Адкрываць спасылкі ў знешнім браўзеры замест убудаванага браўзера праграмы + Памер буфера прайгравання Звычайны + Нізкі + Срэдні + Максімальны + Затрымка відэа + Затрымлівае відэа, каб сінхранізаваць яго з гукам, які мае затрымку, напрыклад, пры выкарыстанні Bluetooth-навушнікаў (0-5000 мс) + Толькі затрымка з Bluetooth-аудыё + Затрымлівае відэа толькі пры падключэнні Bluetooth-аудыё, каб дынамік тэлефона заставаўся ў сінхранізацыі + Паказваць кнопку затрымкі відэа + Націсніце і ўтрымайце, каб скінуць затрымку да 0 мс + Паказваць меню затрымкі відэа + Затрымка ў мілісекундах + Аўта + Запамятаць якасць відэа + Прымяняе ваш выбар якасці да ўсіх будучых відэа + Апавяшчэнні аб змене якасці + Паказваць паведамленне пры захаванні новай якасці па змаўчанні + Якасць відэа па змаўчанні ў сетцы Wi-Fi + Якасць відэа па змаўчанні ў мабільнай сетцы + Запамятаць якасць Shorts + Прымяняе ваш выбар якасці да ўсіх будучых Shorts + Якасць Shorts па змаўчанні ў сетцы Wi-Fi + Якасць Shorts па змаўчанні ў мабільнай сетцы + Зменена якасць па змаўчанні для мабільнай сувязі на: %s + Зменена якасць па змаўчанні для Wi-Fi на: %s + Зменена мабільная якасць Shorts на: %s + Якасць Shorts па Wi-Fi зменена на: %s + Паказваць кнопку хуткасці пракрывання + Націсніце і ўтрымайце, каб скінуць хуткасць да пачатковай + Паказваць кнопку якасці + Націсніце і ўтрымайце, каб скінуць якасць да пачатковай + Паказваць развязанне на кнопцы \"Якасць\" + Паказваць развязанне, напрыклад 1080, замест пазначэння, напрыклад FHD + Меню карыстальніцкай хуткасці прайгравання + Павярнуць старое меню хуткасці прайгравання + Карыстальніцкія хуткасці прайгравання + Увядзіце пажаданы варыянт хуткасці пракрывання + Пальзовацкаскія хуткасці прайгравання павінны быць менш за %s + Няправільныя карыстальніцкія хуткасці прайгравання + Аўтаматычна + Націскаць і ўтрымліваць хуткасць пракрывання + Усталюйце множнік хуткасці пры націсканні і ўтрымліванні экрана; усталюйце 0, каб адключыць + Запамятаць хуткасць відпраўлення + Прымяняе ваш выбар хуткасці ўручную да ўсіх будучых відэа + Апавяшчэнні аб змене хуткасці + Паказваць апавешчэнне пры захаванні новай хуткасці прайгравання па змаўчанні + Хуткасць прайгравання па змаўчанні + Шпарыгасць прайгравання па змаўчанні ў Shorts + Запамінаць хуткасць прайгравання ў Shorts + Прымяняе ваш ручны выбар хуткасці Shorts да ўсіх будучых Shorts. Калі гэтая функцыя выключана, хуткасць вяртаецца да пачатковай пасля закрыцця плеера Shorts + Паказваць хуткасць прайгравання ў меню Shorts + Дадае пункт «Хуткасць прайгравання» ў меню плеера Shorts + Хуткасць прайгравання · %s + Хуткасць прайгравання па змаўчанні зменена на: %s + Выкарыстоўваць звычайную хуткасць для музыкі + Заўсёды праіграваць музыкальныя кліпы з хуткасцю 1.0x + Белы спіс хуткасці каналаў + Відэа з каналаў з белага спіса заўсёды праграваюцца са хуткасцю 1.0x + Дадаць канал у белы спіс хуткасці + Выдаліць канал з белага спіса хуткасці + Уключаць кіраванне вышынёй гуку + Вышыня гуку па змаўчанні + Вышыня гуку па змаўчанні зменена на: %s + Часавое расцягненне аўдыё + Дазваляе незалежна ўсталёўваць хуткасць і вышыню, што можа выклікаць артэфакты гуку + Запамятоўваць пазіцыю прайгравання прамой трансляцыі + Для бягучых прамых трансляцый запомніць і адновіць вашу пазіцыю прайгравання замест пераходу да бягучай пазіцыі ў прамым эфіры + Гэтая функцыя не працуе з прамымі трансляцыямі 24/7 і іншымі трансляцыямі, якія працуюць «пастаянна» + Адключаць HDR-відэа + Прымусіць HDR-відэа + Прымушае праграму разглядаць гэтую прыладу як такую, што падтрымлівае HDR + "Выкарыстоўвайце гэта, калі дэкадаванне HDR працуе, але YouTube не прапануе HDR, як у некаторых прылад HyperOS, якія схавалі HLG. + +Абмежаванні: +• Колеры могуць выглядаць няправільна, калі дысплей насамрэч не можа паказваць HDR +• Відэа можа не прайгравацца, калі дэкодар не можа апрацаваць HDR +• HDR усё яшчэ залежыць ад кліента, які імітуе відэапатокі" + Відэакадэк Звычайны + Каб прымусіць выкарыстоўваць AVC, змяніце \"Імітацыя відэапатокаў\" на \"Android Studio\", \"Android VR\" або \"visionOS\" + "VP9 +• Можа павялічыць час працы акумулятара і выправіць заіканні пры прайграванні +• Выкарыстоўвае больш інтэрнэт-трафіку, чым AV1 + +AVC (H.264) +• Патрабуе кліента спафувікацыі відэапатоку 'Android Studio', 'Android VR' або 'visionOS' +• Можа яшчэ больш павялічыць час працы акумулятара +• Можа аднавіць адсутныя развялані відэа на старых прыладах +• Максімальная развялан — 1080p +• HDR-відэа могуць не падтрымліваць AVC +• Некаторыя прылады не могуць прымусіць выкарыстанне AVC" + Адкрыць меню пашыранай якасці + Прапускае меню выбару якасці і адкрывае меню пашырнай якасці напрамую + Схаваць параметры якасці Premium + Прыярытэт якасці відэа + Надае прыярытэт найвышэйшай даступнай якасці відэа над эфектыўнасцю кадэка + Калі гэтую настройку выключаць, для старых відэа могуць быць недаступныя параметры якасці + Дазволіць \'Android VR\' / visionOS AV1 + Дазваляе праграмнае дэкадаванне AV1 пры выкарыстанні кліента \'Android VR\' / visionOS + "Уключанне гэтай налады можа выкарыстоўваць праграмнае дэкадаванне AV1. + +Відэапрацяг з AV1 можа запінвацца або прапускаць кадры" + Пабочныя эфекты спафінга + • Прайграванне можа спыніцца праз 1 хвіліну + • Мена \'Аўдыядарожка\' адсутнічае + • Няма відэакадэка AV1 + • Іммерсіўны рэжым VR 360° недаступны + • Стабільная гучнасць недаступная + • Відпрацоўка можа заканчыцца на 1 секунду раней + • Калі дэабфускацыя пацьме, прайграванне можа працягнуць працаваць з памылкамі + • Выкарыстанне арыгінальнага аўдыё недаступна + • Патрабуецца ўваход у Android VR + • Якасць можа быць абмежавана да 720p + Варыянт JavaScript плеера + Паказваць імітацыю ў \'Статыстыцы для гікаў\' + Варыць старую панэль дзеянняў відэа + "Выпраўлене праблема, пры якой кнопкі дзеянняў відэа не можна было схаваць + +Абмежаванне: вяртаецца стары стыль панэлі «Яшчэ відэа»" + Стыль іконкі плеера Аўтаматычна Звычайны + Цінькая + Тоўстая Карыстальніцкае + Стыль іконкі Shorts + Трэкер серыялаў + Следзіце за плэйлістамі як за серыяламі і захавайце сваё месца + Уключаць трэкінг серыялаў + Дадае старонку «Серыялы» ў гісторыю і опцыю «Следзіць за серыялам» у плэйлісты + Паказваць \"Серыялы\" ў панэлі навігацыі + Паказаць кнопку трэкера серыялаў + Дадае кнопку ў прагравач, каб сачыць за серыяламі або адкрыць серію бягучага відэа + Запіс прагрэсу адсачываных серыялаў + Запіс пракрывання на гэтым тэлефоне для захаваных серыялаў і закладак Адмяніць + Захаваць + Захаваць закладку + Назва серыяла + Налады серыяла + Налады бібліятэкі + Абнавіць эпізоды + Перайменаваць + Адкрыць плэйліст у YouTube + Выдаліць з бібліятэкі + Спасылка або ID плэйліста + Ачысціць увесь лакальны прагрэс + Ачысціць гісторыю + Ачысціць увесь лакальны прагрэс? + Гэта ачысціць пазіцыі, перакрыцці прагледжанага і выбары ўзнаўлення для кожнага відэа. Захаваныя серыялы і іх каталогі застаюцца. Новае працяг будзе запісаны, калі запіс уключаны + Пазначыць як прагледжанае + Пазначыць як непраглянутае + Выкарыстоўваць аўтаматычны статус + Перазапусціць з 0:00 + Локальны прагрэс ачышчаны + Адкрыйце «Гісторыю», каб пабачыць свае серыялы + Гісторыя прагляду + Серыялы + Загрузка захаваных серыялаў… + Падпішыцеся на плэйліст, каб адсачыць яго эпізоды + Закладка плэйліста + Абнаўленне эпізодаў… + Усе серыялы + Праглянута + Загрузка эпізодаў… + Больш увальгаў + "Запісвайце прагрэс толькі для захаваных серыялаў і відэа ў закладках. + +Гэтая бібліятэка захоўваецца на гэтай прыладнасці і выкарыстоўваецца для ўсіх уліхавак YouTube. Функцыі паўзы і ачысткі гісторыі YouTube не ўплываюць на яе. Карыстайцеся раздзелам Серыялы → Больш → Ачысціць лакальны прагрэс, каб выдаліць яго. + +Аўтаматычны запіс спыняецца ў рэжыме інкагніта, калі ідэнтыфікацыя ўліхоўкі недаступная або калі вы пераключаецеся на іншую ўліхоўку. Уключыце адследжванне зноў, каб выкарыстоўваць яго з гэтай уліхоўкай" + Адсочванне немагчыма ўключаць у рэжыме інкагніто або да таго, як YouTube загрузіць бягучы стан уліхоўкага запісу. Вярніцеся на Гаўную старонку і паспрабуйце зноў. Уваход у сістэму не патрабуецца + Уключыць лакальнае адсочванне + Запіс прагрэсу выключаны + Рэзумаваць на %1$s + %1$s / %2$s прагледнах серый + Эпізод %1$s · %2$s + Эпізоды %1$s-%2$s з %3$s + Абнавіце старонку, каб паспрабаваць загрузіць эпізоды. プрыватныя плэйлісты патрабуюць доступу да ўліхоўкага запісу; іх закладка ўсё роўна адкрываецца ў YouTube + YouTube не змог адкрыць гэты эпізод + YouTube не змог адкрыць гэты плэйліст + Выберыце эпізод + Усё праглядзена + Няма даступных эпізодаў + Захаванае відэа · %1$s + Следзіць за серыялам + Праглядзець серыялы + Працягнуць + Пачніце тут + Скасаваць + Уключыць + %1$s новых серый + Запісвайце прагрэс на гэтай прыладзе + "Вашы серыялы і прагрэс застаюцца на гэтай прыладзе і будуць сумеснымі для розных улікоў YouTube. Спрыміяныя налады гісторыі YouTube не ўплываюць на іх. Ачысціце прагрэс у меню «Серыялы». + +Аўтаматычны запіс з'яўляецца пазіцыяном. Ён спыняецца ў рэжыме інкогніта або пры змене ўліковага запісу" + Не ўдалося загрузіць эпізоды. Вы можаце захаваць плэйліст як закладку + Занадта шмат незахаваных змен. Спачатку паспрабуйце захаваць іх зноў + Ідзе працэс захавання. Паспрабуйце яшчэ раз + Ідзе загрузка эпізодаў. Паспрабуйце яшчэ раз + Сховішча недаступна. Вашы захаваныя даныя былі захаваныя + Не ўдалося захаваць змены. Паспрабуйце захаваць іх зноў перад закрыццём YouTube + Гэты серыял ужо захаваны або яго плэйліст змяніўся. Адкрыйце яго зноў + Гэты серыял змяніўся або быў выдалены. Адкрыйце бібліятэку зноў + Плэйліст змяніўся. Выберыце эпізод зноў + Гэты эпізод недаступны. Выберыце іншы + Увядзіце назву серыяла + Увядзіце спасылку на плэйліст YouTube або ID плэйліста + Калі ласка, пачакайце перад наступным абнаўленнем + Не ўдалося загрузіць эпізоды. Паспрабуйце пазней + Не ўдалося завяршыць змену. Паспрабуйце яшчэ раз + Не ўдалося пацвердзіць працяг. Праверце плеер і паспрабуйце яшчэ раз + Следаючыя серыялы + Гэта відэа няма ў гэтым плейлісце + Пошук плэйлістаў… + Уставіць спасылку на плэйліст + Не ўдалося знайсці плэйліст + Няма даступных эпізодаў + Схаваць праглядзенае + Пазначыць папярэднія эпізоды як праглядзеныя + Пачынаецца з: %1$s + Порадак эпізодаў + Порадак плэйліста + Змяніць парадак плэйліста на адваротны + %1$s праглядаў + %1$s прагляд + Пазначыць эпізоды як праглядзеныя + Праглядзена прынаймені %1$s%% пры ўмове, што засталося не больш за 90 секунд, альбо засталося %2$s секунд + Аўтаматычна пазначаць эпізод як праглядзены пасля дасягнення любога з парогаў. Правіла працэнта таксама патрабуе, каб засталося не больш за 90 секунд, каб завяршэнне доўгіх эпізодаў не прапускалася. Правіла застатку часу прымяняецца да відэа даўжэйшых за адну хвіліну; усталюйце 0, каб выключаць яе. Змены прымяняюцца да будучых абнаўленняў прагрэсу. Ручны выбар \"праглядзена/не праглядзена\" застаецца + Працэнт праглядзенага (1-100) + Застацца секунд (0-300; 0 выключае) + Увядзіце значэнне ад %1$s да %2$s + Выкарыстоўваць прагрэс YouTube + Чытаць статус прагляду і пазіцыі ўзоўлення для вашых падпісаных серыялаў з бягучага ўліковага запісу YouTube. На гэтым тэлефоне захраняюцца толькі адпаведныя эпізоды. Праверкі выконваюцца, пакуль адкрыта бібліятэка серыялаў і перад «Працягнуць». Гісторыя YouTube павінна быць уключана на іншай прыладзе + Праверка прагрэсу ў YouTube… + Прагрэс YouTube недаступны. Выкарыстоўваецца захаваны прагрэс + Частка прагрэсу YouTube недаступная. Захаваныя пазіцыі засталіся + Рэзервоўнае капіяванне і аднаўленне + Захавайце вашу бібліятэку серыялаў у файл або аднавіце рэзервовую копію + ЗахаLikewise серыялы, на якія вы падпісаны, пазіцыіэпізодаў і выбары прагледу. Дазволы на запіс і прагрэс YouTube не ўключаюцца + Захаваць рэзервовую копію + Аднавіць рэзервовую копію + Дадаць %1$s серыялаў. %2$s серыялаў, якія ўжо ёсць у вашай бібліятэцы, застануцца. Існуючы прагрэс відэа захаваны + Рэзэрвовая копія серыялаў захавана + Відновлена серыйялаў: %1$s + Гэты файл не з\'яўляецца падтрымаваным рэзервовым копіяй серыялаў або з\'яўляецца занадта вялікім. Ваша бібліятэка не была зменена + Не ўдалося захаваць рэзервовую копію. Паспрабуйце іншае месца + Серыял выдалены + Гэты эпізод цяпер з\'яўляецца больш за адзін раз. Выберыце, з якога працягнуць + Ваш захаваны эпізод больш няма ў гэтым плэйлісце. Ваша захаваная пазіцыя захавана + Працягнуць з эпізодам %1$s · %2$s + Вы таксама можаце выбраць любы эпізод ніжэй. Ранейшыя эпізоды не змяняюцца + Гэты плэйліст змяніўся. Адкрыйце серыял, каб выбраць, дзе працягнуць + Больш няма серый у гэтым кірунку + Працягнуць %1$s + Пошук серыялаў + Ачысціць пошук + Серыялаў не знойдзена + Эпізод без назвы + Недаступны эпізод + Каментары Niconico + Паказваць жывы чат і запіс чата ў выглядзе каментароў, якія пракручваюцца па відэа + Паказвае кнопку паверх відэаплеера. Націсніце, каб паказаць або схаваць каментары, ці націсніце і ўтрымайце для хуткіх налад + Памер шрыфта каментарыяў + Памер тэксту каментарыяў (12-40 sp) + Прадпразорнасць каментараў + Прадпразорнасць тэксту і фонаў каментараў + Час руху + Колькі секунды патрабуюцца каментарам, каб перасячціць экран. Меншыя значэнні робяць рух хутчэйшым + Максимальная даўжыня каментара + Схаваць каментары, у якіх больш сімвалаў, чым гэтая колькасць. Кожны эмодзі лічыцца за адзін сімвал + Без абмежавання diff --git a/patches/src/main/resources/addresources/values-bg-rBG/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-bg-rBG/shared-youtube/strings.xml index b5a0c56026..390535e0a0 100644 --- a/patches/src/main/resources/addresources/values-bg-rBG/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-bg-rBG/shared-youtube/strings.xml @@ -18,10 +18,12 @@ Second \"item\" text" --> - Анимация при стартиране - Изключено - Цвят - Монохромен + Анимация при стартиране + Изключено + Цвят + Цветен Morphe + Монохром + Монохромно Morphe Рестартирайте, за да завършите настройката diff --git a/patches/src/main/resources/addresources/values-bg-rBG/youtube/strings.xml b/patches/src/main/resources/addresources/values-bg-rBG/youtube/strings.xml index 2a1bb68af9..13309cd987 100644 --- a/patches/src/main/resources/addresources/values-bg-rBG/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-bg-rBG/youtube/strings.xml @@ -414,7 +414,7 @@ Second \"item\" text" Неуспешно търсене в този канал Видеа Публикации - Няма публикации в този канал, които съвпадат + Няма публикации в този канал, които да съвпадат Уместност Най-гледани Най-малко гледани @@ -837,8 +837,6 @@ Second \"item\" text" Никога не показвай Прави лентата на състоянието напълно непрозрачна Показване при мълчание - Показване на автоматични надписи - Скриване на автоматични надписи Предпочитан език на субтитрите Първо избира субтитрите от доставчика, след което автоматично превежда на предпочитания език @@ -1250,6 +1248,7 @@ Second \"item\" text" Оправя статус бара да не покрива горните ленти Поддържа последователен макет на статус бара, така че той да не покрива горните ленти и да не оставя празно пространство над плеъра. Изключете тази опция, ако приложението се счупи или горните ленти изглеждат неправилно + Сяяне Фон на светлата тема Бяло Material You (Бяло)‎ @@ -1272,6 +1271,9 @@ Second \"item\" text" Задайте вторичния цвят на лентата за напредък на видеото Редовно + Оригинален аналог + Оригинална торта + Оригинален метал Табло за Home Табло за Abonations diff --git a/patches/src/main/resources/addresources/values-bn-rBD/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-bn-rBD/shared-youtube/strings.xml index 0443f9a4b4..323762f45e 100644 --- a/patches/src/main/resources/addresources/values-bn-rBD/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-bn-rBD/shared-youtube/strings.xml @@ -18,10 +18,12 @@ Second \"item\" text" --> - স্টার্টআপ অ্যানিমেশন - বন্ধ রাখুন - রং - একরঙা + স্টার্টআপ অ্যানিমেশন + বন্ধ রাখুন + রং + কালার Morphe + মনোক্রোম + মনোক্রোম Morphe সেটআপ সম্পন্ন করতে রিস্টার্ট করুন diff --git a/patches/src/main/resources/addresources/values-bn-rBD/youtube/strings.xml b/patches/src/main/resources/addresources/values-bn-rBD/youtube/strings.xml index f81e22446f..518f279e71 100644 --- a/patches/src/main/resources/addresources/values-bn-rBD/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-bn-rBD/youtube/strings.xml @@ -837,8 +837,6 @@ OpenRouter.ai ওপেন করতে এখানে ট্যাপ কর কখনো দেখান না যখন আনমিউট করা হবে তখন দেখান মুটি অবস্থায় দেখান - স্বয়ংক্রিয় ক্যাপশন দেখান - স্বয়ংক্রিয় ক্যাপশন লুকান পছন্দের ক্যাপশন ভাষা প্রথমে প্রদানকারীর সাবটাইটেল নির্বাচন করে, তারপর স্বয়ংক্রিয়ভাবে পছন্দের ভাষায় অনুবাদ করে @@ -1250,6 +1248,7 @@ OpenRouter.ai ওপেন করতে এখানে ট্যাপ কর স্ট্যাটাস বারটি উপরের বারগুলি ঢেকে যাওয়া ঠিক করুন স্ট্যাটাস বারের লেআউট ধ্রুবক রাখে, তাই এটি উপরের বারগুলি ঢাকবে না এবং প্লেয়ারের উপরে কোনো খালি জায়গা থাকবে না। অ্যাপটি ক্র্যাশ হলে বা উপরের বারগুলো ভুল দেখালে এই সেটিংটি বন্ধ করুন + গ্লো হালকা থিমের পটভূমি সাদা Material You (সাদা) @@ -1272,6 +1271,9 @@ OpenRouter.ai ওপেন করতে এখানে ট্যাপ কর ভিডিও অগ্রগতি বারের মাধ্যমিক রঙ সেট করুন অবৈধ সিকবার রঙ মান + মূল অ্যানালগ + অরিজিনাল কেক + অরিজিনাল মেটাল হেডার লোগো ডিফল্ট diff --git a/patches/src/main/resources/addresources/values-ca-rES/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-ca-rES/shared-youtube/strings.xml index c0dc76b69f..e86ad344e1 100644 --- a/patches/src/main/resources/addresources/values-ca-rES/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ca-rES/shared-youtube/strings.xml @@ -18,7 +18,7 @@ Second \"item\" text" --> - Deshabilitat + Deshabilitat diff --git a/patches/src/main/resources/addresources/values-ckb-rIR/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-ckb-rIR/shared-youtube/strings.xml index 910acde8db..917f5facd8 100644 --- a/patches/src/main/resources/addresources/values-ckb-rIR/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ckb-rIR/shared-youtube/strings.xml @@ -18,9 +18,9 @@ Second \"item\" text" --> - لەکارخراو - ڕەنگ - Monochrome + لەکارخراو + ڕەنگ + منۆكرۆم دەستپێکردنەوە بۆ تەواوکردنی ئامادەکردن diff --git a/patches/src/main/resources/addresources/values-ckb-rIR/youtube/strings.xml b/patches/src/main/resources/addresources/values-ckb-rIR/youtube/strings.xml index 62ae62fe14..376d0857eb 100644 --- a/patches/src/main/resources/addresources/values-ckb-rIR/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ckb-rIR/youtube/strings.xml @@ -262,8 +262,6 @@ Second \"item\" text" دیارە ڕەنگەکانی زیرەکان تۆ نەربێنە هنگێو سڕکرێتەوە نیشانە - نیشاندانی ژێرنووسی خۆکار - به‌کارھێنانی زیروکان قایم بگرە کوژاوە diff --git a/patches/src/main/resources/addresources/values-cs-rCZ/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-cs-rCZ/shared-youtube/strings.xml index 4c42dbae98..afd8699a58 100644 --- a/patches/src/main/resources/addresources/values-cs-rCZ/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-cs-rCZ/shared-youtube/strings.xml @@ -18,10 +18,12 @@ Second \"item\" text" --> - Animace při spuštění - Vypnuto - Barva - Monochromatický + Animace při spuštění + Vypnuto + Barva + Barevné Morphe + Monochrom + Monochromatické Morphe Restartovat pro dokončení nastavení diff --git a/patches/src/main/resources/addresources/values-cs-rCZ/youtube/strings.xml b/patches/src/main/resources/addresources/values-cs-rCZ/youtube/strings.xml index 3c7716916f..d48ee0d2de 100644 --- a/patches/src/main/resources/addresources/values-cs-rCZ/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-cs-rCZ/youtube/strings.xml @@ -837,8 +837,6 @@ Chcete-li to opravit, zapněte 'Spoof video streams' a změňte 'Default client' Nikdy neukázat Zobrazovat, když není ztlumené Zobrazit při tichém - Zobrazit automatické titulky - Skrýt automatické titulky Preferovaný jazyk titulků Nejprve vybere titulky poskytovatele a poté je automaticky přeloží do preferovaného jazyka @@ -1248,6 +1246,7 @@ Omezení: Pokud je vypnutá možnost 'Obnovit starou lištu akcí videa', překr Oprava stavového řádku zakrývajícího horní lišty Udržuje konzistentní rozvržení stavového řádku, takže nezakrývá horní lišty a nezanechává prázdný prostor nad přehrávačem. Toto vypněte, pokud aplikace havaruje nebo pokud horní lišty vypadají špatně + Záře Pozadí světlého motivu Bílá Material You (bílá) @@ -1270,6 +1269,9 @@ Omezení: Pokud je vypnutá možnost 'Obnovit starou lištu akcí videa', překr Nastavte sekundární barvu indikátoru průběhu videa Neplatná hodnota barvy posuvníku + Originální analog + Původní dort + Původní kov Logo záhlaví Výchozí diff --git a/patches/src/main/resources/addresources/values-da-rDK/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-da-rDK/shared-youtube/strings.xml index 0434c5d711..b7b00009dd 100644 --- a/patches/src/main/resources/addresources/values-da-rDK/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-da-rDK/shared-youtube/strings.xml @@ -18,10 +18,12 @@ Second \"item\" text" --> - Startanimation - Deaktiveret - Farve - Monokrom + Startanimation + Deaktiveret + Farve + Farve Morphe + Monokrom + Monokrom Morphe Genstart for at færdiggøre opsætningen diff --git a/patches/src/main/resources/addresources/values-da-rDK/youtube/strings.xml b/patches/src/main/resources/addresources/values-da-rDK/youtube/strings.xml index 58bddea58f..ea8ddaa4f9 100644 --- a/patches/src/main/resources/addresources/values-da-rDK/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-da-rDK/youtube/strings.xml @@ -835,8 +835,6 @@ For at løse dette skal du slå 'Spoof video streams' til og ændre 'Standardkli Vis aldrig Gør statuslinjen fuldstændig uigennemsigtig Vis når dæmpet - Vis automatiske undertekster - Skjul automatiske undertekster Foretrukket undertekstsprog Vælger udbyderens undertekster først og oversætter derefter automatisk til det foretrukne sprog @@ -1246,6 +1244,7 @@ Begrænsning: Hvis 'Gendan gammel video-handlingslinje' er slået fra, er 'Flere Fixér statuslinjen, så den ikke dækker de øverste felter Gør layoutet af statuslinjen konsistent, så det ikke dækker de øverste felter og ikke efterlader tom plads over afspilleren. Slå fra, hvis appen krascher, eller de øverste felter ser forkerte ud + Glød Lyst tema baggrund Hvid Material You (Hvid)! @@ -1268,6 +1267,9 @@ Begrænsning: Hvis 'Gendan gammel video-handlingslinje' er slået fra, er 'Flere Indstil andenfarven på videofremdriftlinjen Ugyldig søgelinje farveværdi + Original analog + Original kage + Originalt metal Sidehovedlogo Standard diff --git a/patches/src/main/resources/addresources/values-de-rDE/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-de-rDE/shared-youtube/strings.xml index 4d98002015..653f29dbf9 100644 --- a/patches/src/main/resources/addresources/values-de-rDE/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-de-rDE/shared-youtube/strings.xml @@ -18,10 +18,12 @@ Second \"item\" text" --> - Startanimation - Deaktiviert - Farbe - Einfarbig + Startanimation + Deaktiviert + Farbe + Farbiges Morphe + Einfarbig + Monochromes Morphe Starte die App neu, um die Einrichtung abzuschließen diff --git a/patches/src/main/resources/addresources/values-de-rDE/youtube/strings.xml b/patches/src/main/resources/addresources/values-de-rDE/youtube/strings.xml index 31d2dfd6c9..01684ed801 100644 --- a/patches/src/main/resources/addresources/values-de-rDE/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-de-rDE/youtube/strings.xml @@ -838,8 +838,6 @@ Um das zu beheben, aktiviere 'Video-Streams vortäuschen' und ändere den 'Stand Nie anzeigen Bei Stummschaltung anzeigen Zeige bei stummgeschaltet - Zeige automatische Untertitel - Verberge automatische Untertitel Bevorzugte Untertitelsprache Wählt zuerst die Untertitel des Anbieters aus und übersetzt sie dann automatisch in die bevorzugte Sprache @@ -1116,17 +1114,18 @@ Einstellungen → Wiedergabe (oder Automatische Wiedergabe) → Nächstes Video Tablet-Layout • Community-Beiträge werden ausgeblendet -• Wiedergabe in Feeds-Einstellung ist nicht verfügbar +• Die Einstellung \"Wiedergabe in Feeds\" ist nicht verfügbar • Remix-Schaltfläche und Sound-Schaltfläche sind in Shorts nicht verfügbar -• 'Mehr Videos'-Overlay möglicherweise nicht verfügbar +• Das Overlay \"Weitere Videos\" ist möglicherweise nicht verfügbar • Nicht zusammengeklappte alte Video-Aktionsleiste wird angezeigt +• Flyout-Menüelemente funktionieren möglicherweise nicht korrekt -Automotive-Layout -• Feed ist nach Themen und Kanälen organisiert -• Wiedergabe in Feeds-Einstellung ist nicht verfügbar +Automobil-Layout +• Der Feed ist nach Themen und Kanälen organisiert +• Die Einstellung \"Wiedergabe in Feeds\" ist nicht verfügbar • Remix-Schaltfläche und Sound-Schaltfläche sind in Shorts nicht verfügbar -• 'Mehr Videos'-Overlay möglicherweise nicht verfügbar -• Shorts werden im regulären Player geöffnet +• Das Overlay \"Weitere Videos\" ist möglicherweise nicht verfügbar +• Shorts öffnen sich im regulären Player • Nicht zusammengeklappte alte Video-Aktionsleiste wird angezeigt" Tablet-Layout im Player aktivieren @@ -1250,6 +1249,7 @@ Einschränkung: Wenn \"Alte Video-Aktionsleiste wiederherstellen\" deaktiviert i Statusleiste verhindert nicht das Überdecken der oberen Leisten Behält das Layout der Statusleiste bei, sodass sie die oberen Leisten nicht überdeckt und über dem Player kein leerer Raum entsteht. Schalten Sie diese Option aus, wenn die App abstürzt oder die oberen Leisten falsch aussehen + Leuchten Hintergrund des hellen Designs Weiß Material You (Weiß) @@ -1272,6 +1272,9 @@ Einschränkung: Wenn \"Alte Video-Aktionsleiste wiederherstellen\" deaktiviert i Die Sekundärfarbe des Videofortschrittsbalkens Ungültiger Farbwert + Original analog + Original-Kuchen + Original-Metall Header-Logo Standard diff --git a/patches/src/main/resources/addresources/values-el-rGR/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-el-rGR/shared-youtube/strings.xml index c53274837d..09f1e211df 100644 --- a/patches/src/main/resources/addresources/values-el-rGR/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-el-rGR/shared-youtube/strings.xml @@ -18,10 +18,12 @@ Second \"item\" text" --> - Κινούμενο σχέδιο εκκίνησης - Απενεργοποιημένο - Χρωματιστή - Μονόχρωμο + Κινούμενο σχέδιο εκκίνησης + Απενεργοποιημένο + Χρωματιστή + Έγχρωμο Morphe + Μονόχρωμο + Μονόχρωμο Morphe Επανεκκινήστε για να ολοκληρωθεί η διαδικασία diff --git a/patches/src/main/resources/addresources/values-el-rGR/youtube/strings.xml b/patches/src/main/resources/addresources/values-el-rGR/youtube/strings.xml index 3aaa2b83d1..9b8fcad100 100644 --- a/patches/src/main/resources/addresources/values-el-rGR/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-el-rGR/youtube/strings.xml @@ -837,8 +837,6 @@ Second \"item\" text" Δεν εμφανίζεται ποτέ Απόκρυψη τμήματος Κουίζ Εμφάνιση όταν σιωπηλός - Εμφάνιση αυτόματων υπότιτλων - Απόκρυψη αυτόματων υπότιτλων Προτιμώμενη γλώσσα υποτίτλων Επιλέγει πρώτα τους υποτίτλους του παρόχου και στη συνέχεια μεταφράζει αυτόματα στην προτιμώμενη γλώσσα @@ -1250,6 +1248,7 @@ Second \"item\" text" Διόρθωση κατάστασης μπάρας να καλύπτει τις πάνω μπάρες Διατηρεί τη διάταξη της μπάρας κατάστασης συνεπή, ώστε να μην καλύπτει τις πάνω μπάρες και δεν αφήνει κενό χώρο πάνω από τον παίκτη. Απενεργοποιήστε αυτό αν η εφαρμογή καταρρεύσει ή οι πάνω μπάρες φαίνονται λάθος + Λάμψη Φόντο φωτεινού θέματος Λευκό Material You (Λευκό) @@ -1272,6 +1271,9 @@ Second \"item\" text" Ορίστε το δευτερεύον χρώμα της γραμμής προόδου του βίντεο Μη έγκυρη τιμή χρώματος γραμμής προόδου + Αρχικό αναλογικό + Πρωτότυπη τούρτα + Πρωτότυπο μέταλλο Λογότυπο επικεφαλίδας Προεπιλογή diff --git a/patches/src/main/resources/addresources/values-es-rES/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-es-rES/shared-youtube/strings.xml index 7185e9d165..2af080645d 100644 --- a/patches/src/main/resources/addresources/values-es-rES/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-es-rES/shared-youtube/strings.xml @@ -18,10 +18,12 @@ Second \"item\" text" --> - Animación de inicio - Desactivado - Color - Monocromático + Animación de inicio + Desactivado + Color + Color Morphe + Monocromático + Morphe monocromo Reiniciar para finalizar la configuración diff --git a/patches/src/main/resources/addresources/values-es-rES/youtube/strings.xml b/patches/src/main/resources/addresources/values-es-rES/youtube/strings.xml index 395b5da778..ed620d1fd2 100644 --- a/patches/src/main/resources/addresources/values-es-rES/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-es-rES/youtube/strings.xml @@ -837,8 +837,6 @@ Para solucionar esto, activa 'Falsificar transmisiones de vídeo' y cambia 'Clie No mostrar nunca Mostrar cuando no está silenciado Mostrar cuando está silenciado - Mostrar subtítulos automáticos - Ocultar subtítulos automáticos Idioma preferido para subtítulos Selecciona primero los subtítulos del proveedor y luego los traduce automáticamente al idioma preferido @@ -1249,6 +1247,7 @@ Limitación: si la opción 'Restaurar antigua barra de acciones de vídeo' está Corregir barra de estado que cubre barras superiores Mantiene la uniformidad del diseño de la barra de estado, de modo que no cubra las barras superiores y no deje ningún espacio vacío encima del reproductor. Desactiva esta opción si la aplicación se bloquea o las barras superiores no se ven bien. + Resplandor Fondo del tema claro Blanco Material You (Blanco) @@ -1271,6 +1270,9 @@ Limitación: si la opción 'Restaurar antigua barra de acciones de vídeo' está Establece el color secundario de la barra de progreso del vídeo Valor de color de la barra de progreso no válido + Analógico original + Tarta original + Metal original Logo del encabezado Predeterminado diff --git a/patches/src/main/resources/addresources/values-eu-rES/music/strings.xml b/patches/src/main/resources/addresources/values-eu-rES/music/strings.xml index ccfea4ed71..3b5f8a8955 100644 --- a/patches/src/main/resources/addresources/values-eu-rES/music/strings.xml +++ b/patches/src/main/resources/addresources/values-eu-rES/music/strings.xml @@ -17,58 +17,675 @@ Second \"item\" text" --> + Azkenbehin joa + Izakبتzioak + Bideok + Emaitza besteak + Igoitako abestak + Igoitako albumak + Igoitako artistak + Eguneratu hostgaineko Jam patch-a pause eta resume erabiltzeko + Jam-erenQUEUEa partekatzea + Hostatu edo join egin hurbilko Jam batean eta editatu haren herren Jam Layer bidez + Gaitu JamQUEUE partea sailatzea + YouTube Music berrifiziatu ondoren Jam integrazioa aktibatzen du + Deskargatu Jam Layer funtzionatzeko + Ireki Jam Layer-en azken bertsioa zure nabigatzailean + Jam kontrolak + Hostatu edo batzu hurbilko Jam batean eta kudeatu saioa + Jam Layer paketea + Aukeratu hurbilko hirutik bat partekatzeko erabiltzen den instalatutako laguntzaile aplikazioa + Erabili instalatutako laguntzaile pakete bat. Parekatzeak zure onarpena eta tokiko gaitasun token bat behar ditu + Aktibatu Wi-Fi? + Aktibatu Wi-Fi-a, eta gero saiatu Jam-a hasten edo batzen berriro + Ireki Wi-Fi konfigurazioa + Batalde + Erabili paketea + Sartu Android pakete-izen baliagarri bat + Paketea gorde da. Konfiguratu Jam Layer konektatzeko + Jam deskonektatuta dago + Aktiba lehen Jam Layer-a + Aktiba Jam-eren herren partekatzea konfigurazioan, eta ondoren berrifizi YouTube Music + Instalatu lehen hautatutako Jam Layer paketea + Sartu hostSoilan agertzen den kodea. Wi-Fi Aware-k hurbilko gailerak konektatu ditzake haien LANak desberdinak izan arren + Sartu kode batekin + Sartu + Escaneatu QR + Sartu zirebi karaktereko kodea edo itsasi gonbidapena + Sartu zure Jam-ean + Kopiatu kodea + Jam kodea + Erakutsi QR + Done + Zure Jam-era gonbidatu + Escaneatu herrendu eta editatu diru-errendu hau. Jam-a amaitzeak gonbidapena bertatzen du + Kopia esteka + Jam-eko gonbidapena + Jam berriz konektatzen ari da; saiatu berriro lashiko + Jam-era gehituta + Jam ez dago eskuragarri; pista ez da gehitu + Zure Jam-a + Elkarrekin entzuten + Jam-era sartzen + Entzun elkarrekin + Jam-a eguneratzen... + Zure hosta Wi-Fi-n bilatzen... + Jendea zain + %s pertsona konektatuta + %s pertsona konektatuta + Sartu kode hau gertu dagoen gailu batean. Wi-Fi Aware-k LAN bereizituen artean funtzionatzen du onartzen bada. %s minututan agintzen da + %s-ra jauzi? + Editaketa irekia + Editaketa blokeatua + Wi-Fi Aware bidez konektatuta + Wi-Fi bidez konektatuta + Bluetooth bidez konektatuta + Ez da onartzen Jam-ean + Batetzea bertatu da + Hostekin berkonektatzen... + Partekatu diru-errendua. Utzi denorek abestuak gehitu + Gonbidatu jendea + Gonbidatuekmala erremakada editatu dezakete + Jam amaitu + Hostek musika joku. Gehitu, mugitu edo kendu abestiak zuzen YouTube Music queue-n + Mantendu bi gailuak hurbil. Bertxea bilatzen ari garen bitartean zuk bertatu dezakezu + Bertatu batetzea + Jam utzi + Hasi Jam bat + Konfiguratu Jam Layer-a + Kudeatu + Xehetasunak + Iraki + Abestien aukerak kargatzen... + Abestien aukerak eskuragarri ez dira; saiatu berriz + Abestien aukerak ezin izan dira ireki + Ez dago nabigatzaireik Jam Layer deskargatzeko + Hosten queuingat zain + Ireki lehen abestia YouTube Music-en + Host-ean erreproduzi + Irten Jam-etik eta tokian erreproduzi + Hurrengoa playback egin Jam-ean + Gehitu Jam zerrendara + Joan Jam kantu hau + Aukeratu non joan + Mugitu hostnagasia hona + Kantuak ez da prest bilatzen + Zerrenda aldatu da; saiatu berriro jestuarekin + Goiburuko logotipoa + Lehenagoa + Kustomizatua + Albumezko audio-pistantzak erreproduzi + Erreproduzi abestuen albumezko audio bertsioa musikavideoaren ordez. \'Spoof video streams\' behar du + Gaitu audio/bideo aldauntza + Saiatu Audio Track bertsioa eta Musikako Bideo Ofiziala aldatzen + Deskarga tu musika-bideoak, podcast-ak edo abestak hirugarren partiduen deskarga-aplikazioekin + Erabili deskargatzaile kanpora Download menurako + Menuen Download aukerak zure deskargatzaile kanpoa irekiko du ordez + Deskargak + "Ez da deskargaik +Deskargatutako pisteak hemen agertuko dira" + %1$s • %2$s • %3$s MB + Bilatu deskargak + Ordenatu + Artista arabera ordenatu + Izenaren arabera ordena tu + Kupuruen arabera ordena tu + Deskargapen guztiak ezabatu + Deskargapen guztiak ezabatu? + Deskargatutako pista guztiak ezabatuko dira gailu honetatik + Deskargapenak ezabatuta + Ezer ez da aurkitu + Ekintzak %s ratzat + Joystick + Joystick edo pause + Aurrekoa + Hurrekoa + Pause + Itxi + Ezabitu + Ezabitu deskarga + Deskargatua ezabatu? + %s gailu honetatik ezabatuko da + Deskargatua ezabatua + Ezin izan da deskargatua ezabatu + Ezin izan da deskargatua erreproduzitu + Audio deskargak + Offline erreprodukzioa + Ezin da deskargatu beharreko pista identifikatu + Deskarga martxan dago + Deskargatuta dago + Deskarga hasita + Deskarga hasten ari da… + Deskargatzen %s%% + Deskarga osatua + Deskarga huts egin du + Morphe לגain + Iragarkiak + Feed-a + Orokorra + Se reproducing + Letrak + besteak + YouTube Music-en hizkuntza + Sistemaren hizkuntza + Android 13 edo berriagoa behar du + Ezkutatu \"Lortu Music Premium\" etiketa + Ezkutatu Music Premium promozioak + Ezkutatu bideo-iragarkiak + Ezkutatu Audio / Bideo aldatzailea + Ezkutatu Deskargatu botoia + Ezkutatzen du Deskargatu botoia podcast saioan + Ezkutatu Explore estantea + Ezkutatu filterbarra + Ezkutatu sare estanteak + Ezkutatu estante horizontalak + Ezkutatu zerrenda estanteak + Letren panela + Ezkutatu Partekatu botoia + Ezkutatu Itzulpen botoia + Ezkutatu \'Berriak\' estantea + Ezkutatu zerrenden estanteak + Ezkutatu Errepikatu botoia + Ezkutatu nahasteko botoia + Ezkutatu \'Speed dial\' estantea + Komentarioak + Aukeratu zein komponentek agertu behar duten komentarioen sekzioan + Ezikusi Komunitatearen Gidalerak + Ezkutatu testuinguru-mezua + Ezikusi Emoji botoia + Ezikusi Info botoia + Ezikusi Denbora-zigikada botoia + Crossfade-a + Nahasi pista batek bestekin modu leunetan + Aktiba crossfade-a + Itzal-kurba + Potentzia berdin + Eutsa sutila + Leun easing + S-kurba leuna + Irteera + Sarrera + Crossfade-aren iraupena + 250 milisegundo + 500 milisegundo + 750 milisegundo + 1 segundoa + 2 segundokoak + 3 segundoa + 4 segundu + 5 segundu + 6 segundu + 7 segundu + 8 segundu + 9 segundu + 10 segundu + 11 segundu + 12 segundu + Crossfade eskuz saltzean + Hurrengo track-era eskuz skipsatzean crossfade efektua aplikatzen du + Crossfade automatikoan + Track bat modu naturalean amaitzean crossfade efektua aplikatzen du + Saio kontrola + Shuffle botoia luze sakatu crossfade pausedu edo berraktibatzeko + Crossfade paused saio honetan + Crossfade berraktivatu da + Bideo modua ez dago eskuragarri crossfade aktibatuta dagoen bitartean + Crossfade-a txutfatuta dago SABR playback-arekin + Crossfade-a txutfatuta dago casting egitean + Nola funtzionatzen duen + "Crossfade-ak kantu baten amaiera eta hurrengokoaren hasiera gaineztu egiten ditu, zure iraupena eta kurba erabiliz + +Automatikoan eta eskualdeko saltzailean funtzionatzen du, bakoitzak bere aktibatzailea du. Automatikoan, nahasketa kantuak amaitu baino segundu gutxiz aurrerago hasten da" + Mugaak + "Musika bideoak audio gisa bakarrik txalgatzen dira crossfade aktibatuta dagoen bitartean. Shuffle botoia luze sakatu crossfade pausatzeko eta bideoa ikusteko + +Errejenta hasten den musika bideoa bideo moduan geratzen da hurrengo abestia arte + +Fade iraupena baino laburragoak diren abestiak ez dira crossfaded + +Lock screen-ek edo jakinarazpenak egoera zaharkitua erakutsi dezake trantsizio erdian + +Crossfade pausatzeak musika bideo bat ikusteko batzuak beltz utzi ditzake. Abestia aukeratu aurretik crossfade pausatzeak saihesten du hau" + Ez dago supportatua + "Audioa transmititzea edo mirroring egitea (Chromecast, pantaila edo audio mirroring) + +SABR errejentzea (Bideo stream-en imitazioa desaktibatuta)" + Kredituak + Crossfade funtzionalitatea VazerOG-k egina + Gogoratu errepikazio-egoera + Gogoratu nahasketa-egoera + Erreprodukzio abiadura + Pista guztietan erabiltzen den abiadura. %100ean, aplikazioaren abiadura erabiltzen da, podcasten erreprodukzio abiadura bezalakoa + Aldatu tonua abiadurarekin + Desaktibu ez-gustu bideratzea + Ez-gustu botoia sakatu ethanez hurrengo track-era salto egitea ekiditen du + Playlisteko trackean sakatzeko ekintza + Playlista erreproduzi + Hautatutako tracka soilik erreproduzi + Hautatutako tracka hurrengoen zerrendara gehitu + Hurrengoen zerrendara gehituta + Ezkutatu Cast botoia + Ezkutatu Historia botoia + Ezkutatu Jakinarazpen botoia + Ezkutatu Bilaketa botoia + Ezagutze bozko bilaketa botoia ezkutatu + Ezagutze soinuzko bilaketa botoia ezkutatu + Berria botoia ezkutatu + Liburutegi fitxako Berria botoia ezkutatzen du + Ekintza botoiak + Aukeratu zein ekintza botoiak agertu nahi ditzunatzunatzun playback-aren azpian + Ezkutatu ekintza barra + Ezkutatu Gustuko dut eta Ez dut gustuko botoiak + Ezkutatu Iruzkinak botoia + \'Zuzeneko txateko erreproduzketa\' botoia ezkutatu + Ezkutatu Letrak botoia + Ezkutatu Partekatu botoia + Ezkutatu Gorde botoia + Deskarga botoia ezkutatu + \'Hasi irradyoa\' botoia ezkutatu + Flyout menua + Aukeratu zein komponentei eman nahi diezu leku playback-eko eta txutuneko flyout menuetan + Ezkutatu 3-zutabeko komponentia + Ezagutarazi Gustuko eta Gustuko ez duten botoiak + Ezkutatu \'Goguen batzuera\' menua + Ezagututxo \'Gero entzun\' zerrendara gehitu\' menua + Ezkutatu \'Txutunera gehitu\' menua + Ezkutatu Azpitituluak menua + Ezkutatu \'Playlist-a ezabatu\' menua + Ezkutatu \'Txutuna kendu\' menua + Ezkutatu \'Ez gomendatu artista hau\' menua + Ezkutatu Deskarga menua + Ezkutatu \'Playlista editatu\' menua + Ezkutatu \'Albumera joan\' menua + Ezkutatu \'Artistara joan\' menua + Ezkutatu \'Episodioari joan\' menua + Ezkutatu \'Podcast-era joan\' menua + Ezkutatu Laguntza menua + Ezkutatu \'Markatu episodioa 재생 gisa\' menua + Ezkutatu \'Ez dut interesa\' menua + Ezkutatu \'Marcatu markagarri gisa\' menua + Ezkutatu \'Playback hurgoa\' menua + Ezkutatu Kalitate menua + Ezkutatu \'Kendu liburutegitik\' menua + Ezkutatu \'Kendu zerrendatik\' menua + Ezkutatu Salbidea menua + Ezkutatu \'Gorde episodioa geroago\' / \'Gorde liburutegian\' menua + Ezkutatu \'Gorde zerrendan\' menua + Ezagutze-menua ezkutatu + Ezkutatu \'Shuffle play\' menua + Ezkutatu \'Lo-timerra\' menua + Ezkutatu \'Hasi irratia\' menua + Ezkutatu \'Stats for nerds\' menua + Izapiltze-menua ezkutatu + Ezkutatu \'Kendu accesses azkarretik\' menua + Ezkutatu \'Ikusi abestien kredituak\' menua + Hasi-orria + Lehenagoa + Taulak + Episodioak geroago + Arakastu + Historia + Liburutegia + Atsegin musikak + Zerrendak + Bilatu + Izakscriptions + Zuberrengoko orientazioa behartu + Gailua biratu ethane app-a zutik mantentzen du + Aldatu mini-playeraren kolorea + Miniplayeren kolorea pantaila osoko sekoerera moldatzen du + Navigation barraren kolorea alda + Navigation barraren kolorea pantaila osoko segaearekin bat egiten du + Erakutsi botoi hurrengoa miniplayer-en + Erakutsi botoi aurrekoa miniplayer-en + Aktiba miniplayer behartua + Kantu berri bat hasten denean sekoerea pantaila osoan automatikoki zabaltzea ekiditen du + Gaitu mini-erreproduzitorea irrasan itxalteko aukera + Miniplayer-a behera irrastea bidez itxiteko aukera ematen du + Nabigazio-barra + Aukeratu zein botoiak agertzen diren behera navigazio barraan + Ezagutua botoia ezkutatu + Lagunak botoia ezkutatu + Arakatu botoia ezkutatu + Liburutegia botoia ezkutatu + Melioratu botoia ezkutatu + Ezkutatu nabigazio-barra + Ezkutatu nabigazio-botoien etiketak + Ez dago saioa hasita. Konfiguratzeko sakatu + Saioa amaitu + Saioa amaituta + Gaitu scrobbling-a + Bidali \'Orain dozena\' egoera + Abestu iraupen minimoa + Honek baino laburragoak ez dira scrobbled-koak + Scrobble gutxitu percentages + Abestuaren ehuneko hau entzun ondoren aktibatu scrobble + Scrobble gutxitu segunduak + Segunduen kopuru hau entzun ondoren aktibatu scrobble + ListenBrainz kontua + Sesi empezó. Toka egin kontua kudeatzeko + Sartu zure ListenBrainz Token erabiltzailea scrobbling aktibatzeko + Tokena + Berritsatu + Sartu token bat, mesedez + Balidatzen... + Tokena valide! Erabiltzailea: %s + Tokena ez da balidea: %s + ezaguna ez + Berifikatzea huts egin du: %s + Tokena ezabatu da + Tokena gorde da + Oharra: Gorde duguen tokena ez da baliagarria! + Maitetuta utzi gustuko trackak + Maitetu automatikoki trackak Last.fm-en YouTube Music-en gustukoak marka wrinkles (eta kendu maitasunik markatuen edo ez gustukoak marka wrinkles) + Last.fm Kontua + Sartuta %s bezala. Tokatu kontua kudeatzeko + Sartzeko, sartu zure Last.fm datuak + Erabiltzaile izena + Pasworda + Sartu + Sartu erabiltzaile-izena eta pasworda, mesedez + Sartzen ari zara… + Sarrera arrakastatsua! + Sesioa lortzea huts egin du Last.fm-en + Sarrera huts egin du: %s + Last.fm datuak gorde dira! + Metadatulen garbiketa + Garbitu metadatuak + Automatiki garbitu ohiko kaskarra (adibidez \"Official Video\", \"Remaster\", \"Live\") izenlardan, artistak eta albumeak scrobbling egin aurretik + Regex garbitu pertsonalizatua + Sartu erregulazio-adierazpen pertsonalizatu bat abestien izenlardan, artistak edo albumeak testu gehigarriak kendzeko. Adibidea: (?i)\\s*\\((clean|explicit)\\) + Parseatu artista eta track-a izenetik + Atera artista eta kantuaren izena bideoaren izendapenetik \" - \" separatzailea erabiliz. Berriz igotako edukizupekozu erabilgarria, non kanalaren izena artista gisa erabiltzen den + Album falta dena betetu + Musika batek albumik ez duenean, bilatu Last.fm-en zure scrobble-a osatu dadin + "Scrobbling-ak automatikoki sinkronizatzen du zure YouTube Music erabilerak zure Last.fm edo ListenBrainz profilarekin + +Atzealdean erreproduktu duten bitartean scrobbling-a sinkronizatua dagoela ziurtatzeko, ziurtatu YouTube Music aplikazioaren bateria-erabilera \"Muga gabe\" konfiguratuta dagoela" + Hirugarren partyren letrak erabili + Hirugarren partyren zerbitzuko letrak erakusten ditu; bat ere ez bada aurkitzen, integratutako letrak erabiltzen ditu + Pantaila txartu utzi letra ikusten ari zaren bitartean + Letren panela irekita dagoen bitartean pantailak ez itzaltzea ziurtatzen du + Leten zerbitzua + Ikusiera + Sinkronizazioa + Panela + AI endpoint + Bidalklari guztiak desaktibatuta daude - letra integratuak eta barneko letrak + Hornitzaileen ordena berreskuratu? + Hornitzaileen lehenetsuatze-ordena lehenagatik restoreatu + Garbitu gorde diren letrak + Ezabatu gailu honetan gordetako letrak + %1$s MB garbituta + Tokena lortu + Apple Music-en Media-user-tokena + "Itsartu music.apple.com-en dagoen media-user-token cookie-a + +Urratsak: +1. Hasi saioa music.apple.com-en kide gisa +2.Ireki DevTools $\rightarrow$ Application $\rightarrow$ Cookies $\rightarrow$ music.apple.com +3. Kopiatu \"media-user-token\"-en balioa" + Apple Music: media-user-token gorde da + Apple Music: media-user-token ezabatu da + Apple Music: media-user-token baligabea - egitea eta berriz saiatzea eskatu + Spotify SpDc + "Itsartu Spotify-en sp_dc cookie-a. + +Urratsak: +1. Hasi saioa open.spotify.com-en +2. Ireki DevTools $\rightarrow$ Application $\rightarrow$ Cookies $\rightarrow$ open.spotify.com +3. Kopiatu \"sp_dc\"-ren balioa" + Spotify: sp_dc gorde da + Spotify: sp_dc ezabatu da + Spotify: sp_dc baligabea - egitea eta berriz saiatzea eskatu + Apple Music-en tokena aukeratua da. Gabe, Lyrically erabiliko da alternatiba gisa + Deezer ARL + "Itsasi Deezer-en arl cookie-a. + +Urratsak: +1. Sasi deezer.com-en +2. Ireki DevTools $\rightarrow$ Application $\rightarrow$ Cookies $\rightarrow$ deezer.com +3. Kopiatu \"arl\"-en balioa" + Deezer: arl gordeされています + Deezer: arl ezabatuされています + Deezer: arl baligabea - egitea eta berriz saiatzea eskatu + Musixmatch Tokena + Gehitu token bat zure kontua soilik erabiltzeko. Musixmatch Android aplikaziotik etorri behar da: musixmatch.com-etik kopiatutako tokenak behinbehineko desktop klientiarenak dira eta APIak ez ditu onartzen + Musixmatch-ek tokenik gabe funtzionatzen du, beraz, eremu hau hutsik utzi daiteke + hiztegi tokena + Musixmatch: erabiltzaile tokena gordeされています + Musixmatch: erabiltzaile tokena ezabatuされています + Musixmatch: erabiltzaile-token baligabea - egitea eta berriz saiatzea eskatu + YouTube Cookie-ak + "Gaussian YouTube cookie-ak pastesatu subtituluak eta denbora-testuak YouTube-tik eska depletedkoan. + +Urratsak: +1. Ireki youtube.com nabigatzaile batean eta saioa hasi +2. Ireki DevTools (F12) → Application → Cookies → youtube.com +3. Kopiatu cookie-kode osoa (gako=balio pareak puntu eta komaz bereztuta) + +Cookie beharrezkoak: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPISID, LOGIN_INFO" + YouTube: cookie-ak gorde dira + YouTube: cookie-ak garbituta + YouTube: cookie-ak baligabeak - egitea eta berriz saiatzea eskatu + Sakatu eta eutsi lerro batean + Sakatu lerro bat abestuko une horretara salto egiteko, sakatu luze hitzak kopiatzeko, edo lerro osoa hitzik ez den tokian sakatu zuenean. Txektatu egiteak bi kezkatu hauek kenduko ditu + Atal-etiketak erakutsi + Letren atal berri bakoitza etikatu etiketa txiki batekin, Verse edo Chorus bezalakoak + Erakutsi Kopiaren botoia + Sakatu letra originalak testu soil gisa kopiatzeko; sakatu luze letra iturriko fitxategia Deskargak folderrakoa gordetzeko + Erakutsi Itzulpenaren botoia + Sakatu itzulpena aktibatzeko/desaktibatzeko; sakatu luze itzulpen bakarra ikusteko. Itzultzaileak, Google Translate edo OpenAI-rekin kompatiborena erabiltzen ditu + Erromanizatze botoia erakutsi + Sakatu romanizazioa aktibatzeko/desaktibatzeko; sakatu luze romanizazio bakarra ikusteko. Romanizazio iturriak, Google Translate edo OpenAI-rekin kompatiborena erabiltzen ditu + Romanizazio eta Azeratze Lokalizatuak + Romanizatu gailuaren barruan itzultzaileak, zure modeloa eta Google Translate fallatzen direnean. Irakurkizuna letra sinkronizatuaren hitz bakoitzaren ondoren dator; hitz sinkronizazioa aktibo egon behar da. Txinoa, Kantonesa, Japoniera, Koreera, Grekera, Errusiera, Georgiana, Armeniera; 120 MB inguru + %1$s irakurtzen: %2$d%% + Deskarga huts egin du - desaktibatu eta aktibatu berriro saiatzeko + Aktualizatzeko botoia erakutsi + Sakatu hurrengo letra kandidatora aldatzeko; sakatu luze abestuaren izenaren eta artistaren arabera letra bilatzeko + Bilatu letraak + Abestuko izenburua + Artista + Sartu abestuko izenburua eta artista bat + Gaitzitutako hornitzaileetatik batek ere ezin ditu beste abestuak bilatu + Gordea da + Markatu sinkronizatutako hitzak edo silabak + Lehenetsi hitz-by-hitz letak eskuragarriak direnean. Letak hitzen timinga dutenean, musikaurreko hitz edo silaba bakoitza nabementzen ditu + Erakutsi atzeko ahizpak separat Updateren + Parantezian idatzitako ahozko fondoak, \"(Oh) I want you\" bezalakoak, abestu egin den lerrotik atera eta lerro propioago eta diseinuzko batean jartzen ditu + Ezikusi abestutako hitzak + Ezikusi gaur egun dozen linearen gaindiko hitz-lerroak + Ezagutu gabeko letra ezkutatu + Ezkutatu gaur egun erreproduzitzen ari den lerroaren beheko letra lerroak + MediaSession letrak erakutsi blokeatutako pantailan + Lock screen-ean, Android Auto-n eta Bluetooth-en erakusten du egungo lerroa. Android 11 edo bertsio berriagoa behar du + Erakutsi mini-sekoialdiko letraak + Egungo lerroa miniplayer-en erakusten du, pista subtituluan mugituta + Abestuko izenaren/artistaren formatua + Miniplayer-en eta MediaSession-en letraekin birlikte erakutsitako pistaren formatua. {t} izena da, {a} artista eta {v} bertsioa, adibidez, Akustikoa, letra-iturriak bertsioa ematen badu. Bi puntuako zezeichenen arteko testua soilik erakutsiko da barruan dagoen bakoitzak balioa baduenean; \\: idatzi puntuazio soil bat lortzeko. Linea bakoitza txandataka erakutsiko da, letra aldatzen den bakoitzean + Letra-lerren formatua + Letakren lerren formatua Miniplayerrean eta MediaSession-en. {lrc} lerra da eta {n} zenbatetan jeduki den jarraian. Koloneen arteko testua lerrak errepikatzen denean soilik agertzen da. \\: erabili koloneka + Formatuak {lrc} izan behar du + Artistaren-lerra txamplea eta animazio-denbora + Pantaila-denbora minimoa (ms) + Pantailako denbora maximoa (ms) + Animazio multiliniekentzat soilik. Mantendu frame bakoitza gutxienez denbora minimoan. Maximoan, artistaren lerroa aurreratu lyric-ak berdin egonez ere. + Minimoa 0 edo handiagoa izan behar da. Maximoa 0 baino handiagoa eta gutxienez minimoa izan behar da. + Lerroen luzerako mugaak + Lyric eta artist lerroen mugaak, eta zabalera osoak karakteren pisua + "Lyric lerroen muga +Lyric luzeagoak zatiak orekatuetan banatu. 0-ak banaketa desaktivatzen du." + "Artist lerroen muga +Betetako track formatua frame zirkulazioetan banatu. 0-ak banaketa desaktivatzen du." + "Zabalera osoak karakteren pisua +Muga bikoitzera aplikatzen da. 1,75-an, lau zabalera osoak karakterek 7 kontatzen dute." + Sartu 0 eta 10000 arteko zenbaki osoen muga bat eta 0 baino handiagoa den karakteren pisua + Atalak instrumentalak + Miniplayeren eta MediaSessionen erakutsia instrumentalak atalean. Linea bakoitza animazio-frame bat da. {t} izenburua da, {a} artista eta {lrc} azken letra-linea + Miniplayeren eta MediaSessionen denbora-desphasea + Aldatu hurrengo linea Miniplayeren eta MediaSessionen erakutsia den unea. Balio negatiboa erabili lineak berandu agertzen badira, lock screen-ean adibidez + Irudagailuak + Metadatuen regex + Erregulazio erregresioa izenetara, artistei eta albumei aplikatzen zaie letra bilatzen aurretik eta bi izenak bat egiten duten puntuazioa kalkulatzen denean. Biak desaktibatzeko, utzi hutsik. Lehenestapenak \"Official Video\", \"Remaster\", \"【插曲】\" eta \"- Topic\" bezalako elementu ohikoak kenden + Letren irudagailu regex + Erregulazio-adierazpena letra originalei soilik aplikatzen da (ez itzulpenei edo romanizazioei). Desaktibatzeko, utzi hutsik + Informazio-lerren markatzaileak + Letrak modu okerrean irudagailu egin badira beheko aldetik, egokitu edo garbitu hau. Markatzaileak komaz banatuta: markatzaile bat eta ondoren kolonak dituzten lerroak kreditu-informazio gisa erakutsiko dira behean. Desaktibatzeko, utzi hutsik + Lehen letra mayuskulatu + Letaren lerro guztien lehen letra mayuskulatzen du + Kendu amaierako puntuazioa + Letaren lerroen amaierako puntuak, komak, kolonak eta puntu eta komak kenden ditu. Galderragokiak, exclamation markak, tripuntoak, aipatoak eta parentesiekon ez dute ukitzen + Testu-taman + Letren testu-tamaina + Denbora globaleko offseta + Sinkronizatutako letraek erreprodukzioari jarraitzen dio. Desberdintasuna badago, egokitu denbora-desphasea. Sinkronizatutako letrak aurreratu edo atzeratu egin. Balio positiboa erabili letraek kantuaren aurrekoan badabilzat + SponsorBlock segmentuak erabili letra offset-arako + Match egin SponsorBlock-ek saltzen dituen segmentuak gabe eta lekuko letrak askatu audioan. Off-ek SponsorBlock guztiz ezaitzen du + Pista honek ez du bidaltzeko video id-ik + Iturria: %s + LRC hau gordetzeko sakatu luze kopiatzeko botoia; denborizazioak doitu eta komunitateko bidalklariekin parteka ditzakezuen면, eskertuko zaitugu + Googlek itzuli du + Googlek romanizatu du + %s-ek itzulia + %s-ek romanizatua + %s-ek romanizatua + Itzulitakoa + Itzulpen hizkuntza + Romanizazio hizkuntza + Itzulizuten... + Ezin izan dira letrak itzuli + Romanizatua + Automatikoa + Romanizatzen... + Letrak ezin izan dira romanizatu + Itzulituta soilik + Romanizatua soilik + Modu bikoitza + Kopiatu da paperrean + Aktualizatzen + Aktualizatua + QuickLRC API giltza + QuickLRC-k gai current-eko letrak transkribitzen ditu edo denborizatzen ditu. Zerbitzu berezia da, goikohako OpenAI-rekin kompatible endpoint-arekin zerik ez duen + QuickLRC gakoa gordetuta. Erabili letrak bilatzeko dialogue-ko lerrokatze botoia, edo panelko gomendioa gaiak letrik ez baditu + QuickLRC API giltza gorde da + QuickLRC-k ez du giltza hori onartu + QuickLRC API giltza ezabatu da + QuickLRC-k letrik ez duten letrak denborizatzen ditu, edo gaietatik transkribitzen ditu, kredituen kostu batean. Gako bat konfiguratuta badago, letrik ez duten gaiak transkripzioa eskaintzen dute eta bilaketa dialogue-ak lerrokatze botoi bat lortzen du + Eskatu QuickLRC lerroketzeak + Lerroketzen + Aplikatu lerroketzea + Eskatu QuickLRC transkripzioak + Transkribitzen + Transkripzioa bertatu da + YouTube-en erreproduzitzen diren pistak soilik transkribatu daitezke + Ez dau lyricsik lerrokatzeko - transkribatzen ari da ordez + QuickLRC-k ezin du pista hau prozesatu + QuickLRC kredituak agotuta daude pista honentzat + "Letak izenburua eta artista arabera bat egiten dira; metadatu ezohikoek ezer edo bertsio 잘못a aurkitu ditzakete. Bertsioak aldatzeko, freskatu botoia erabili. * markatuta dauden hornitzaileek token bat behar dute. Gaitzitutako hornitzaile guztiak ez dira beti zure ordenean eskatzen, baina haien abiaduraren arabera erantzuten dute.\n\nLetak Morphe-rekin lotura, patroinatzaile edo onarpenik ez duten hirugarren party zerbitzuek ematen dituzte. Bakoitza modu berean erabiltzen da: letak, haien eskuragarritasuna eta baimenak zerbitzu hauen eta haien baimen-emaileen esestu geratzen dira, eta haien erabilerak haien zerbitzu-baldintzak aplikatu ditzake. Gaitzitutako zerbitzu batera iristen dena eskatu zuen bilaketa soilik da: izenburua, artista eta, zerbitzu horrek behar badu, bideo IDa. Aukerako AI endpoint-a eta QuickLRC transkripzioa konfituratu ondoren bakarrikra dabilen, eta letakren testua edo pistaren bideo URL-a jasotzen dute." + Gaitu soilik erabiltzen dituzun hornitzaileak, edo utzi lehenagoa - eskaera etengabeek gogor egin diezakiak komunitateko letra-gune txikiak + Ezagutze-lerra ezkutatu + Ezkututu letraen azpian agertzen den kreditu-lerroa + Truka itzulpena eta romanizazioa + ON-ak romanizazioa letraen azpian eta itzulpena gainean jartzen ditu; OFF-ak romanizazioa gainean eta itzulpena azpian mantentzen ditu + Itzulpena + Romanizazioa + Itzulpen hornitzailea + Google (Lehenagoa, Doakoa) + OpenAIrekin kompatibila (Kustomizatua) + Romanizazio hornitzailea + Google (Lehenagoa) + Hiztegi lokala / Offline + OpenAIekin kompatibila (Personalizatua) + AI Atala + Hornitzaileak Chorus edo Verse bezalako atal-etiketarik ez bidaltzen dituenean soilik erabiltzen da, eta zure OpenAI-rekin bateragarria den modeluak detektatzen ditu orduan + AI & Hodeiko endpointak + Konfiguratu API gakoak, modeloak, endpointak eta prompt txantiloiak AI hornitzaileentzat. + API endpoint URL-a (adib. https://api.openai.com/v1/chat/completions) + Prompt-txantilokia + Lehenkideak: {language}, {task}, {title}, {artist}, {count}, {lines} + Prompta gorde da + Prompta gorde da, baina AI endpoint-a ez da erabili nahi. Konfiguratu lehen AI E設定etan + Prompta berreskuratuta + JSON baligabea. Konponduzu edo garbitu eroi hau gorde aurretik + Itzulpen eta romanizazio konfigurazioak gordetuta + Konfigurazioak lehenagokoetara itzulitakoak + Oinarrizko URLak http:// edo https://-z eta hasi behar du + Konexioa probatu + Endpointak konfiguratu + Endpoint guztiak erakutsi + Konexioa egiaztatzen... + Itzulpen Hornitzailea + Romanizazio Hornitzailea + Modelak ekidatu + Modeluak eskatzen ari zahiz... + Aukeratu Modela bat + Modeluak eskatzen huts egin du. Egituta API gakoa edo konexioa. + Gemini API-ren konfigurazioa + OpenAI-rekin kompatibila (endpoint pertsonalizatua) + DeepL API-ren konfigurazioa + API-gako giltza + Modelua + Oinarrizko URLa + API gakoa (aukerikora) + Itzulpen-prompta + Eskaeraren gorputz gehigarria (JSON aukerikora) + Autentifikazio-gakoa (Doakoa edo Pro) + Konfiguratu API-gakoak, modeluak, endpoint-ak eta prompt-ak + 8.23.51 - Ezgaitu ikonok bold egitea + 9.13.51 - Berakutsi Playeren Hurrengoak, Letrak eta Related fitxak diff --git a/patches/src/main/resources/addresources/values-eu-rES/reddit/strings.xml b/patches/src/main/resources/addresources/values-eu-rES/reddit/strings.xml index cf7776fdaf..947a5c3104 100644 --- a/patches/src/main/resources/addresources/values-eu-rES/reddit/strings.xml +++ b/patches/src/main/resources/addresources/values-eu-rES/reddit/strings.xml @@ -15,4 +15,123 @@ Second \"item\" text" --> - + + Reddit-en pribatutasun politika + Google-rekin saioa egiteko aukera ez dago. Erabili posta elektronikoa saioa egiteko + Iragarkiak + Ezkutatu feedeko iragarkiak + Ezkutatu komentarioen iragarkiak + Nabigazio barraa + Ezkutatu Erantzunak botoia + Ezkutatu Txata botoia + Ezkutatu Sortu botoia + Ezkutatu Deskubritu edo Komunitateak botoia + Ezagututu Jokoen botoia + Aldeko barraa + Ezkutatu Berriki Bisitatutako estantea + Ezkutatu Reddit-eko Jolasak estantea + Ezkutatu Reddit Pro estantea + Ezkutatu About estantea + Ezkutatu Baliabideak estantea + Kutxaren txeredura + Testua kopiatu + Postuetan eta iruzkinetan testua hautatzea eta kopiatzea ahalbidetzen du. Iruzkinak itxitea desaktibatzen du + Iruzkinak itxiteak funtzionatzea utziko du \"Testua kopiatu\" aktibatuta dagoen bitartean + Letra personnaliseria + Karga TTF edo OTF letra-tipo fitxegu pertsonalizatu bat beheko bidetik. Hau aktibatzeak Sistemako letra-tipoa behartzea ere desaktibatuko du + Letra-tipo fitxegu pertsonalizatua + Sakatu TTF edo OTF letra-tipo fitxegu bat hautatzeko + Hautatutako letra-tipoa - %s + Hautatu lehenago letra-tipo fitxegu pertsonalizatu bat + Hautatu .ttf edo .otf letra-tipo fitxegu bat, mesedez + Letra-tipo pertsonalizatua kargatzen ezin izan da. Hautatu fitxegu beste bat + Desaktibu etorkizuneko etxeko UIa + Desaktibu pantaila-argazkiaren popup-a + Pantaila-argazkia hartzean agertzen den popup-a desaktivatzen du + Sistema-letra behin forcedu + Erabili gailuaren sistemako letra-tipoa Reddit Sans edo Robotoren ordez. Hau aktibatzeak Letra-tipo pertsonalizatua ere desaktibatuko du + Ezagutu botoia ezkutatu + Bilaketa barrako Ezagutu botoia ezkutatzen du + Ezkutatu komunitateen棚a + Komunitate erlazionatutako edo suggested shelfa ezkutatzen du subredditen + Ezkutatu Joan eta Nora棚ak + Joan eta Nora棚ak ezkutatzen ditu feed-etik eta bilaketa iradokizunetatik + Kendu jakinarazpen iradokizuneko elkarrizuna + Jakinarazpen suggested dialoga automatikoki bazten du subreddit bat bisitatzean + Erakutsi flair-ak etxeko feed-ean + Postu-flair-en badgearak erakutsi etxeko feed-ean + Feed-eko elementu batzuk agian ez dute flair-ik erakutsiko + Ikusimenduen kopurua erakutsi postetan + Muga: API erantzunak ikusimenduen kopurua bada besterik gabe eskuragarria + Postuaren ekintza-barra alderaka + Postuen boto eta iruzkin botoiak eskuineko aldera lekuko, eta crosspost eta partekatze botoiak eskerreko aldera + Boto eta iruzkin botoiak trukatu + Postuen iruzkin botoia boto botoien aurrean erakusten du + Kendu NSFW ohartutze elkarrizuna + NSFW edukizuen ohartupenak automatikoki onartzen ditu subreddit bat bisitatzean + Besteak + Gutera + Patch bertsioa eta esteka ofizialak + Partekatu eta garbitu estekak + Atrakatu query parametroak kentzen ditu partekatutako esteketatik + Ireki estekak zuzenean + Kanpoko estekak irekitzean, bideberaimentzu URLak saltzen ditu + Feed-eko multimedia zabalera osoa + Irudiak eta bideoak ertzetik ertzera erakusten ditu feed-en eta postuen xehetasunetan, kanporako espazioik eta gainetak birrindutu gabe + Multimedien ikusleitzailearen itzaldura-indarra + Irudiak eta bideoak ikusten ari zarena duten azaleko itzaldura ilunaren indarra, 0 (itzalduraik gabe) eta 1 (Reddit-en lehenestekoa) artean + Feed-eko posizioa mantendu + Postu bat, irudi bat edo bideo bat itxi ondoren, feed-a leku berean mantentzen du, irekitako postua goialdera lekuko behin + Feed-eko multimedien gehienezko altitudea + Feed-eko irudien, bideoen eta galerien height maximoa, zabaleraren anizkide gisa. Multimedia altuagoak moztuko dira. 1,33 da Reddit-en lehenestekoa + Redgifs audioarekin erreproduzi + Redgifs bideo originala erreproduziko du Reddit-en kopiaren ordez, eta honek askotan ez du audiorik + Redgifs kalitate HD + Redgifs bideo HDa jaugatzen du. HD bideoak handiagoak dira eta kargatzen ari diren bitartean pausedu egin daitezke + Deskarga kokapena + Kontu guztiak + Reddit-en lehenetsua: irudiak Pictures/Reddit-en, bideoak Download-en + Karpetak: %s + Fitxategi izena: %s + Reddit-en fitxategi izenak + \"Kontu guztiak\" erabiltzen du + Kokapen propioa erabili + Gorde hemen + Reddit-en karpeta lehenetsuak + Lehenagoa + Azpikarpeta + Gehitu azkarutegiak fitxategiaren izeneko / zehaztzailearekin + Arakatu… + Gailu honetan ez dago karpeta-hautagailurik eskuragarri + Fitxategiaren izena + Reddit-en fitxategiaren izena + Token bat ukitu txertatzeko. / batek azkarutegia sortzen du. Balio ezagunak \"ezaguna\" bihurtzen dira + Irudiak: %s + Bideoak: %s + Gorde + Berreskuratu + Ezabatu + Kontu guztien deskarga-kokapena Reddit-en lehenestezkoan itzuli? + %s-en deskarga-kokapena ezabatu? + Ezin izan da deskarga-kokapenean gorde. Reddit-en lehenestezko karpetan gorde da ordez + Deskarga-karpeta ez dago eskuragarri. Reddit-en lehenestezko karpetan gorde da ordez + Ezin izan da deskargatutako bideoa gorde + Karpeta honek ezin du deskargak egindu + %s (gaurora) + %s (sai egon gabe) + Anonimoa (sesioa itxita edo inkognito) + Ezkutatu multimedia ikuslegailearen overlay-a + Irudi edo bideo bat irekitzean izenburua, botoiak eta bideo-kontrolak ezkutatzen ditu. Multimediaan sakatu itzulitzeko + Ezkutatu \"Ikusi elkarrizuna\" botoia + Multimedia ikuslegailearen beheko \"Ikusi elkarrizuna\" botoia ezkutatzen du + Multimedia ikuslegailearen fondo beltza + Tema-ren fondo-kolorearen ordez, fondo beltza erabiltzen du multimedia ikuslegaileko irudien eta bideoen atzean + Estekak kanpoan ireki + Estekak zure nabigatzailean irekitzen ditu aplikazioaren barneko nabigatzailearen ordez + Gogoratu itxiarazitako komentarioak + Itxi diten iruzkinak itxita geratuko dira postua berriz irekitzean + App-en ikonoa + Aukeratu app-en ikonoa + Lehenagoa + Ezaguna ez + diff --git a/patches/src/main/resources/addresources/values-eu-rES/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-eu-rES/shared-youtube/strings.xml index fd007eeb78..94b6645916 100644 --- a/patches/src/main/resources/addresources/values-eu-rES/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-eu-rES/shared-youtube/strings.xml @@ -18,46 +18,285 @@ Second \"item\" text" --> + Hasi-animazioa + Desaktibatuta + Kolorea + Berrifiziatu konfiguratzea amaitzeko + App izena + Tema iluneko fondoa + Beltz garbia + Material You (Beltz garbia) + YouTube Klasikoa + Ardoi iluna + Urdin iluna + Berde iluna + HoriDARK horia + Urare iluna + Gorri iluna + Aplikazioaren lehenestekoa + Material You (Neutroa) + Material You (Primaria) + Material You (Sekundaria) + Material You (Terziaria) + Deskargak + Deskarga modu + Lehenagoa + Deskargatu aplikazio barruan + Kanpoko deskargatzailea + Deskargatzaile paketearen izena + Instalatu duzun kanpoko deskargatzaile aplikazioaren pakete-izena + Sartu paketearen izena + Besteak + Aplikazioa ez dago instalatuta + "Ezin izan da pakete-izen honek instalatutako aplikazioik aurkitu: %s + +Egiaztatu pakete-izena zuzena dela eta aplikazioa instalatuta dagoela" + Paketearen izena ez da huts egin maaaring + Kustomizatua + App ikona + Jatorrizkoa + Morphe beltza + Morphe iluna + Morphe argia + Morphe play light + Morphe play beltza + Kustomizatua + "Honek zure aplikazio-abiaragalean agertzen den ikonoa aldatzen du. + +Konfigurazio honek ez du ikonoa hauen ingeniruetatik batzuetan aldatuko: +• Gailuaren sistema-konfigurazio aplikazioen zerrenda +• Push jakinarazpen batzuk +• 'Ireki honekin' aplikazio-hautatzailea +• Android Auto ikonoa + +Ikonoa guzten guzten aldatzeko, patch-atu berriro eta aldatu 'Aplikazio-ikonoa' patch- aukera" + Jakinarazpen ikonoa + App-eko ikonoarekin bat egin + Morphe play + Morphe Ohartupena + Ez erakutsi berriz + Bideo-fluxuak imitatu + Konfiguratu bezeroen imitazioa erreprodukzio arazoak saihesteko + "Erabiltzaile gehienentzat playback arazoak konpondu egiten ditu bideo stream kompatigarria eskछोटेz + +YouTube Premium erabiltzailea bazara, agian ez da konfigurazio hau behar" + Ez izan da ahalbidebe clienten streamik lortzeko + Default bezeroak ez dabil + "%1$s-k ez du funtzionatu behin eta berriz, baina %2$s-k bai. + +%2$s default bezeroa izatea nahi duzu?" + Aldatu + Ayarpen hau desaktuatzeak erreprodukzio arazoak sortu ditzake + Saiatu aktibatuz: Bideo streamak imitatu > Android VR saioa hasi + Kleriente lehenagikoa + Android VR saioa hasi + Android VR-en saioa ez dago hasita. Sakatu hemen saioa hasteko + Android VR-en saioa hasita dago gaur egun + Ezaugarri hau erabiltzeko, alda \'bezero lehenetsua\' Android VR-ra + Android VR-en saioa hasi + "Android VR saioa hasteak playback arazoak konpondu ditzake erabiltzaile batzuentzat + +Saioa hasteko, sakatu jarraitu behean" + Saioa amaitu + Instrukzioak + "Aktibazio-kodea: %s + +1. Sakatu beheko \"Webgunea ireki\" botoia \"Gailu bat konekta\" webgunea irekitzeko +2. Pastatu apalabotik (edo idatzi) goian agertzen den aktibazio-kodea +3. Sartu Google-en eskatzen badizuk. Kontu asko badituzu, aukeratu YouTube kontu hau +4. Sakatu \"YouTube VR-i baimetu\" sarbidea +5. \"Gailua konektatuta\" mezuak agertu ondoren, itzuli aplikazio honetara" + Aktibazio-kodea emititea huts egin du + Autentifikazio-tokena emititea huts egin du + Sartu zara arrakastaz + "Autentifikazioa gorde da + +• Zure Morphe konfigurazioek ez dute Android VR autentifikazioa inportatuko/esportatuko +• Aplikazio hau 6 hilabet own ez bada erabiltzen, Googlek autentifikazio hau kenduko du +• Google paswordua aldatzen baduzu edo gailu hau zure Google konfigurazioetatik ezerabat kenden baduzu, berriz saioa hasi beharko zara" + OAuth2nahi dagoi batzuetan erabilgarri ez da (API-ak denbora gehiegi behar du) + Android VR saioa agotan da, saiatu berriro VR-n saioa hasten + OAuth2nahi dagoi erabilgarri ez da一時ki (estatu %s) + OAuth2nahi dagoi batzuetan erabilgarri ez da + Ezin da VR-n saioa hasi, webguneko autentizazioa ez da onartu + Ezin da VR-n saioa hasi: %s + Android VR saioaixteko da + VR saio-tokena ez da baliudarria, saiatu berriro VR-n saioa hasten + Jakinarazpenen multimedia kontrolak + Kustomizatu blokatzeko pantaila eta jakinarazpen panelen erakutsi hazendako erreprodukzio kontrolak + Ezkutatu Aurrekoa & Hurrengoa botoiak + Multimedia jakinarazpenean, blokatzeko pantailan eta🎧🎧🎧aurikularen kontroletan ezkutatzen ditu Aurrekoa eta Hurrengoa botoiak + Desaktibu bilatzailea + Bilatzailea arrastatzeko aukera ez du blokatzeko pantailan eta jakinarazpen panelean + Bilatu Aurreko & Hurrengo botoekin + Media jakinarazpenaren, blokeatutako pantailaren eta entzunagailuen kontrolak aldatzen ditu, bideoak aldavegetarian ordez bilatzeko + Atrezko luzera + Aurreko botoiaren bidez atrez egiteko segunduak + Aurrerako luzera + Hurrengo botoiaren bidez aurreratzeko segunduak + Bilaketa-luzeak 1 eta %s segundoren artean izan behar du. Lehenestekoa erabiltzen da + Desaktibu QUIC protokoloa + Desaktibu DRC audia + Audioren jatorrizko hizkuntza behartu + Bideoaren jatorrizko audio-pistaa joko du beti bertsio doblatua erabili beharrean + Saltatu isiltasuna + Automatikoki saltatzen ditu bideo-audiorako isiltasun pausedak + Pausaren gutxieneko luzera + Gutxienez luzera honeko pausedak soilik laburtzen dira. Hustu izango da hurrengo bideoa hasi ethane + Sare proxya + Bideratu onartutako sare-eskariak proxy baten bidez + Gaitu proxya + Onartutako sare-eskariak konfiguratutako HTTP edo HTTPS proxy baten bidez bideratzen ditu + Proxy ostegadia + Proxy zerbitzariaren hostizena edo IP helbidea + Proxy porta + Proxy zerbitzariaren porta + Erabili HTTPS proxya + Erabili HTTPS proxy zerbitzariarekin konektatzeko HTTP-ren ordez + Proxy autentifikazioa + Basic autentifikazioa bidaltzen du proxy eskaerekin + Proxy erabiltzailea + Proxy autentikazio oinarrizkoerako erabiltzailea + Proxy pasahitza + Proxy autentikazio oinarrizkoerako pasahitza + Baimenu zuzeneko atzak + Konektatu zuzen proxya falla egitean + Ezkutatu pantaila osoko iragakiak + Ezaugarri hau erabiltzeko, alda \'Spoof video streams\' \'Android Studio\' eta \'Android VR Downgraded\' sailen gaindiko edozein klientera + Partekatzeko estekak garbitu + Partekatutako estekeetatik kendu jarraipen-galdegaiak + Aldatu partekatze-estekak youtube.com-etara + Partekatutako esteketan music.youtube.com youtube.com-z ordezkatzen du + Aldatu partekatze estekak youtu.be-ra + Beharra dio bideuei youtu.be URL formatu laburtua erabiltzea + Debug log-en protokolo bufferra + Informazio gehigarria gehitzen du garapidek aplikaziozko komponentak identifikatu ditzaten + "Ayarpen hau aktibatzeak layout datu gehigarriak erregistratuko ditu, UI komponentu batzuen pantailako testuak barne. + +Horrek lagun dezake komponentuak identifikatzen iragazki pertsonalizatuak sortzean. + +Hala ere, hau aktibatzeak erabiltzailearen datu batzuk ere erregistratuko ditu, zure IP helbidea adibidez" + Kustomizazio-iragazkia + Konfiguratu kustomizazio-iragazkiak edozein komponenteari ezkutatzeko + Aktibatu kustomizazio-iragazkia + Iragaztuko komponentuen bide-eraikitzaile-testuen zerrenda, berrizkidetaz txandapenak erabiliz + Kustomizazio-iragazki ezaguna: %s + Konfigurazio menuaren iragazkia + Ezkutatu aplikazioaren konfigurazio-pantaila standardeko elementuak hauen izen ikusgarriaren arabera + Ireki aplikazioaren konfigurazio pantaila standartua behin zerrenda betetzeko + Ikustzailearen diskrezio-dialogoa kendu + Honek ez du adinkako gehikarria gainditu. Automatiki onartzen du soilik + PoToken hornitzaile kanpokoa + "PoToken minter kanpoko bat erabiliz erreprodukzio arazoak konpondu egiten ditu (Experimentala) + +YouTube Premium erabiltzailea bazara, agian ez da konfigurazio hau beharrezkoa" + "Kanpoko PoToken minter bat erabiliz erreprodukzio arazoak konpondu egiten ditu (Experimentala) + +Ezaugarri hau erabiltzeko, 'Bideo fluxuak spoof egin' aukera desaktibu egin ezazu" + Ez funtzionatzen badu, saiatu MicroG behin forceden kutxatzen + Ireki PotHelper konfigurazioa + "Ezaugarri hau garapenean dago gaur egun eta agian ez funtzionatzen duen + +Sakatu hemen PotHelper deskargatzeko" + PotHelper-ek ez du gehiago funtzionatzen + "PotHelper bertsio honetako tokena ez du gehiago funtzionatzen + +Aktiba 'Bideo-fluxuak imitatu' playback-a konponduz" + Aktiba + Aplikazio bertsioa imitatzea + Zerbitzariengatik aplikazio bertsio zahar bat erabiltzen ari zarela pentsatuz, interfaze zaharkituen ezaugarriak berreskuratzen ditu + "Aplikazioen bertsioa bertsio zahago batez imitatu egingo da. + +Honek aplikazioaren itxura eta ezaugarriak aldatuko ditu, baina efektu lateral ezagunak gertatu daitezke. + +Gero desaktibatzen bada, aplikazioaren datuak garbitzea gomendatzen da interfazearen akatsak saihesteko" + Imitatzeko aplikazio bertsio helburua + Aplikazioaren freskagarritasun tasa + Lehenagoa + %s FPS + Freskagarritasun tasa gainditu + Beti + Erreprodukzioa zutik + Erregengatzea pantaila osoan + Erregengatzea zubert eta pantaila osoan + letra + Lehenetsua + Gailuaren sistemako letra + Letra-fitxegia pertsonalizatua + Letra-fitxegia pertsonalizatua + Aukeratu TTF edo OTF letra-fitxegi bat + Hautatutako fitxategia ez da TTF edo OTF letra bat + Letra kustomizatua ezin izan da kargatu eta desaktibatu da + Ez-gustuak guildsatuta daude batzuletan (API-ak denbora gehiegiz utzi du) + Ezagutzeak ez dausableak dira (estatusa %s) + Ez dira ez-gustukoak eskuragarriak (bezeroaren API muga) + Ez dira ez-gustukoak eskuragarriak (%s) + Jabeak ezkutatu ditu + Ez izan gustuko bideo hau, beste %s pertsona baterekin + Ez izan gustuko bideo hau, botoen %s dira ez-gustukoak + Gaitasi Return YouTube Dislike + Erakutsi ez-gustukoak ehuneko gisa + Erakutsi ez-gustukoak gustukoen ehuneko gisa, zenbaki baten ordez + Erakutsi gustuko estimatuak + Erakutsi gustukoen kopuru estimatua sortzaileak gustukoak desaktibatutako bideoetan + Konexio-erroren alertak erakutsi + Jakinarazupen bat erakutsi Return YouTube Dislike zerbitzaria eskitarazi ezin denean + "Return YouTube Dislike-k 2021ean YouTube-k ezkutatu zituen dislike kopuruak berakupatzen ditu. Kopuruak ezketu ziren ezabatzearen aurreko datuetan datorren, eta bideo berriagoen kasuan erabiltzaileen botoetatik estimatuak dira, beraz, h aproximatuak dira eta egun gutxiko behin eguneratzen dira + +Ikusten ari zaren bideoaren IDa Return YouTube Dislike API-ra bidaltzen da, eta zure like-ak eta dislike-ak anonimoki bidaltzen dira estimatuak hobetzeko" + Gailu honen Return YouTube Dislike API estatistikeak + API erantzun-denbora, batezkoa + API erantzun-denbora, gutxienezkoa + API erantzun-denbora, gehienezkoa + API erantzun-denbora, azken mediate + Ezonukak batzuetan eskuragarri ez dira - Bezeroaren API tasa muga aplikatuta dago + API botoen eskaerak, deiekin kopurua + Ez da sare-deiik egin + API botoen eskaerak, timeout-en kopurua + Ezik sare-deiik timeout-etan amaitu + API bezeroaren tasa mugaak + Ez da bezeroaren tasa mugarik aurkitu + Klienteren tasa-muga %s aldiz aurkitu da diff --git a/patches/src/main/resources/addresources/values-eu-rES/shared/strings.xml b/patches/src/main/resources/addresources/values-eu-rES/shared/strings.xml index 31cdbe4dcb..9143e44840 100644 --- a/patches/src/main/resources/addresources/values-eu-rES/shared/strings.xml +++ b/patches/src/main/resources/addresources/values-eu-rES/shared/strings.xml @@ -17,21 +17,204 @@ Second \"item\" text" --> + ⚙️ Konfigurazioa + Ziur yakin duzu jarraitu nahi duzula? + Gorde + Berreseta + Kolorea berreseta + Kolorea baligabe + Berrifuntzatzea beharrezkoa da + Berr------+ aplikazioa aldaketa honek eragingo izan owndo + Berrifuntzatzea + Importatu + Kopiatu + Morphe konfigurazioa lehenestekoa + %s konfigurazioak inportatu dira + Inportatzea huts egin du: %s + Goizu + Bilatu araukadetan + About + Emaitzarik ez da aurkitu \'%s\'erako + Saiatu beste gako bat + Azken bilaketak + Kendu bilaketa historiatik? + Kendu bilaketa historiatik + Garbitu bilaketa historia + Ziur baza bilaketa historia guztia garbitu nahi duzula? + Bilaketa Aideak + "• Toka batean sakatu hara joateko +• Araukada batean sakatu luze hara joateko +• Enter sakatu bilaketa query-a historian gordeko +• Bilaketak ez ditu maiuskulak eta puntuazioak kontuan hartzen +• Araukada gurasoak desaktibatutako araukada semearen gainean agertzen dira" + Bilaketa historia hutsik dago + Bilaketa historia gordetzeko, idatzi bilaketa-galdera bat eta sakatu Enter + Erakutsi konfigurazioen bilaketa historia + Morphe konfigurazioen izena + Lehenagoa + Osagarriak + Doiketak + Morphing denbora + Kustomizatua + Sartu izen bat + Izena ez da utzi hutsik + Erakutsi Morphe konfigurazio-ikonoak + Kopiatuta: %s + Morphe hizkuntza + "Batzu eta hizkuntzen itzulpenak faltan egon daitezke edo incomplete + +Hizkuntza berriak itzuli edo itzulpen existenteak hobetzeko, bisitatu morphe.software/translate" + Aplikazioaren hizkuntza + Inportatu / Esportatu + Inportatu / Esportatu Morphe konfigurazioak + Fitxutxutik importatu + ⚙️ Konfigurazioa arrakastaz inportatu da, 保存 sakatu aplikatzeko + Konfigurazioak inportatzea huts egin du + Morphe Patches-en azken bertsioa erabiltzen ari zara: <i>%s</i> + Morphe Patches-en <i>%1$s</i> bertsioa erabiltzen ari zara<br><br><b>Eguneratzea eskuragarri: <i>%2$s</i></b><br><br>Patch-ak eguneratzeko, erabili Morphe aplikazio honek berriz patchatzeko + Oharra + Bertsio honek pre-lanzamendu bat da eta arazo ustekabeak izan ditzakezu + Esteka ofizialak + Webgunea + Donatu + Itzulpenak + Kredituak + Eguneratuta + Eguneratzea eskuragarri + Garapen egunteko + Garapen aurrekoa + Garapen aktiboa + 2022 ‑ 2025 + 2022 ‑ 2025 + 2019 ‑ 2022 + Kargatzen… + Debugatzea + Konfiguratu garapide aurreratuko aukerak eta diagnostiko-logak + Aktibu debug logak + Sartzen ditu stack trace-ak debug logetan erroreak identifikatzen laguntzeko + Erakutsi jakinarazupen laburra Morphe errorean + "Erroen toastak txukituak egiteak Morphe-ren erroen jakinarazpen guztiak ezkutatzen ditu. + +Ez zaizkizu ezusteko gertakari batzeik jakinaraziko" + Esportatu debug log-ak + Esportatu Morphe-ren debug log-ak + Debug log-ak arrakastetsunez esportatu dira + Debug log-ak esportatzea huts egin du + Bilatu debug logetan… + Garbitu + Debug logak desaktibatuta daude + Ez da debug logikarik aurkitu + Debug log-ak kopiatuta + Debug log-ak esportatzea huts egin du: %s + Debug log-ak garbituta + Ezaugarri-bandera kudeatzailea + Kudeatu boolean ezaugarri-banderak + Bilatu ezaugarri-banderak… + Aktibo ez (%s) + Aktibo (%s) + Blokatuta (%s) + Beharrezkoa (%s) + Automatikoa + Txoelectrico + Aktiboa + Beharbehara aktibatu + Blokeatu + Kendu + Ezaugarri-banderak gorde dira + Ezaugarrien flagak berreseta dira + Ezaugarrien flagak pantailako kopiatzailean kopiatuta + Berrifiziatu aplikazioa konfigurazio hau erabiltzeko + Esportatu eta importatu + Ez da ezaugarri-banderaik aurkitu + Ezaugarri-banderak ar വിജയtsunez esportatuak dira + Ezaugarri-banderak esportatzea huts egin du + Ezaugarri-banderak kargatu dira, sakatu Gorde aplikatzeko + Fitxategia irakurtzea huts egin du + Bilaketa binarioa + "Zein ezaugarri-banderak eragiten duen aplikazioaren jokabide bat aurkitzen du. + +%s banderen halfa blokeatuta daude, eta berrariro bakoitzaren ondoren, jokabidea oraindik present dagoen edo ez esaten duzu. + +Hasi aurretik erreproduz jokabidea behin, bestela aplikazioak oraindik ez ditu eskatu banderak bilaketan falta izango dira" + Hasi + "Ez dago bilatu beharreko ezaugarri-banderarik. + +Binary bilaketan, aplikazioak erabili dituen eta gaur egun behartuta ez daudenen banderak soilik lagun daitezke" + "Urratsa %1$s + +%2$s bandera geratzen dira, %3$s blokeatuta daude urrats honetan. + +Berrifiziatu aplikazioa, egiaztatu jokabidea present dagoen ala ez, eta erantzun behekoalean" + Ezaugarri-banderen bilaketa martxan dago + Jokabidea present dago + Jokabidea ez dago present + Binary bilaketa bertatu + Binary bilaketa bertatuta + "%s ezaugarri-banderak eragiten du jokabidea. + +Orain blokeatuta dago" + "Ez da feature flag bat aurkitu + +Aplikazioaren jokamoduak ez dirudi feature flag batek kontrolatzen" + "Ez da feature flag bakar bat aurkitu + +Jokamodua bilaketan aurrago desagertu zen, beraz, flag bat baino gehiago egon da behaz eta edo erantzunen bat okerra zen" + Ireki MicroG-ren konfigurazioa + MicroG ez dago instalatuta. Instalatu + Ekintza behar da + "Zure MicroG v%1s bertsioa zaharkituta dago eta arazoak sor ditzake. + +Eguneratu MicroG v%2s azken bertsiora" + "Instaluratutako MicroG bertsioa ez da Morphe-k gomendatzen duena: + +%1s + +Sakatu uninstall egiteko, ondoren ireki berri aplikazio hau eta instalatu MicroG RE" + Desinstalatu + "MicroG-k ez du atzealdeanra egiteko baimenik. + +Jarraitu zure telefonorako \"Don't kill my app\" gida eta aplikatu instrukzioak zure MicroG instalazioan. + +Horrek aplikazioa funtzionatzeko behar du" + Ireki webgunea + "MicroG-ren bateria optimizazioak desaktibatu behar dira arazoak saihesteko. + +MicroG-ren bateria optimizazioak desaktibatzeak ez du eragina negatiboa izango bateriaren erabileraan. + +Sakatu jarraitu botoia eta onartu optimizazio aldaketak" + Jarraitu + Erakutsi MicroG bateria optimizazioaren elkarrizuna + Aplikazio-estek들의 kudeaketa + Konfiguratu estekak aplikazio honetan irekitzeko + 1. urratsa: Desaktibu aplikazio-esteka originalak + Ireki aplikazio originalaren konfigurazioa eta desaktibu domeinu-esteka guztiak + 2. urratsa: Aktibu patched aplikazioaren estekak + Ireki patched aplikazioaren konfigurazioa eta aktibu domeinu-esteka guztiak + 🚨 Ohartupen Esperimentala 🚨 + "⚠️ <b>%1$s</b> aplikazioaren bertsio esperimental bat erabiltzen ari zara +<br/><br/> +🔧 Aplikazioaren portaera arraroa edo akats ezagun gabekoak espero ditugu bertsio honetako patchingak doitu while ditugun heinean. +<br/><br/> +Esperientzia arazorik gabekoa nahi baduzu, <b>desinstalatu</b> hau eta ✅ <b>%2$s</b> bertsio gomendatua patchatu" + ⚠️ Esperimentala nahi dut + ✅ Estabila nahi dut + Erroreen egiztea erabiltzailearen dokumentuetan gorde da: %s + Erroreen egiztea gorde da: %s diff --git a/patches/src/main/resources/addresources/values-eu-rES/sponsorblock/strings.xml b/patches/src/main/resources/addresources/values-eu-rES/sponsorblock/strings.xml index 9d2d4ab7d5..8a05bdc4a4 100644 --- a/patches/src/main/resources/addresources/values-eu-rES/sponsorblock/strings.xml +++ b/patches/src/main/resources/addresources/values-eu-rES/sponsorblock/strings.xml @@ -16,7 +16,218 @@ Second \"item\" text" --> + Gaitzitu SponsorBlock + SponsorBlock YouTube bideoko zati gogaikarriak saltatzeko sistema kolektiboa da + Itxura + Erakutsi botoi botoia + Zatiak ondo edo gaizki bostatzeko botoi bat gehitzen du + Kuadradozko layouta erabili + SponsorBlock botoi eta kontrolak angleak karratuak dituztenak erabiltzen ditu + Saltzeko botoi konpaktoa erabili + Saltzeko botoiaren zabalera murrizten du espazioa aurrezteko + Ezkutatu automatikoki Saltzeko botoia + Saltzeko botoia sekundu gutxiren ondoren ezkutatzen du, segmentu osoan ikusgarria mantentzean beharrean + Saltzeko botoiaren iraupena + Zarbatzen du Saltzeko botoia zenbat denboran geratzen den ikusgarria + Erakutsi salto jakinarazpena + Jakinarazpen bat erakutzen du segmentu bat automatikoki saltzean + Salto jakinarazpenaren iraupena + Zarbatzen du undo jakinarazpena zenbat denboran geratzen den ikusgarria + 1 seconded + 2 segundotzat + 3 segundu + 4 seconded + 5 segundu + 6 segundu + 7 segundu + 8 segundu + 9 segundu + 10 segundu + Erakutsi bideoaren luzera segmenturik gabe + Kendu segmentu guztiak bilaketa-barran erakutsi den bideoaren guztizko iraupenetik + Segmentu berriak sortzea + Erakutsi \'Segmentu berria sortu\' botoia + Botoi bat gehitzen dio bideo jfaithfulari SponsorBlock segmentu berriak bidaltzeko + Denbora egokitzapen urratsa + Denbora egokitzapen botoien bilaketa kantitatea (milisegundotan) + Balioa zenbaki positibo bat izan behar du + Bidalketa gidaak + Gideek segmentu berriak sortzeko arauak eta aholkuak dituzte + Jarraitu gidaak + Irakurri SponsorBlock gidalerenak segmentu berriak sortu aurretik + Irkurria + Erakutsi + Orokorra + Erakutsi konexio-erroreen alertak + Jakinarazpen bat erakutsi SponsorBlock zerbitzaria eskuragarri ez denean + Gaitu salto-zenbakizuna jarraitzeko aukera + Bidaiatzen ditu salto-kontuak SponsorBlock leaderboardera, aurreztu den denboraren kopurua jarraitzeko + Segmentuaren gutxieneko iraupena + Balio honek (segundotan) baino laburagoak diren segmentuak ez dira erakutsiko edo saltuko + Denbora iraupena ez da baliagarria + Zure erabiltzaile-ID pribatua + Pribatuan mantendu behar da. Pasword bat bezalakoa da eta ez da inork partekatu behar. Norbaitek badu, zure izenean jardun ahal izaten da + Erabiltzaile-ID pribatuak gutxienez 30 karaktereko luzera izan behar du + Aldatu API URLa + SponsorBlock-ek zerbitzari deiak egiteko erabiltzen duen helbidea + Kanalen zutabe zuria + SponsorBlock desaktatuta dago zutabe zurian dauden kanalezat + Zutabe zurian dauden kanaletan jakinarazi + Jakinarazpen bat erakutsiko du SponsorBlock zutabe zuriko kanal batez desaktatuta dagoenean + SponsorBlock desaktatuta dago kanal honetan + API URL berristatu + API URL ez da baliagarria + API URL aldatu da + Konfigurazioa inportatu / esportatu + Kopia + Zure SponsorBlock JSON konfigurazioa, Morphera eta beste SponsorBlock plataformetara inportatu / esportatu daiteke + "Zure SponsorBlock JSON konfigurazioa, Morphera eta beste SponsorBlock plataformetara inportatu / esportatu daiteke + +Honek zure erabiltzaile-ID pribatua aukeratzen du. Ziurtatu kontuarekin partekatzen duzula" + ⚙️ Konfigurazioa arrakastaz inportatu da + Inportatzea huts egin du: %s + Esportatzea huts egin du: %s + "Zure konfigurazioak SponsorBlock erabiltzaile-ID pribatu bat dauka. + +Zure erabiltzaile-IDa pasword bat bezala da, eta inoiz ez da partekatzeべき" + Ez erakutsi berriz + Segmentuen portaera alda + Sponsorizalea + Promozio ordaindua, erreferentzia ordainduak eta iragarkiak zuzenak. Ez da auto-promozioetarako edo kauza / sortzaile / webgune / produktu gustukoen doako aipamenetarako + Auto-promozioa / Doakoa + Sponsortxeara antza, baina auto-promozioetarako edo doakoetarako. Merchandising, donazioak edo kinen kolaboratu deni buruzko atalak sartzen dira + Interakzio gogorarazlea (izapiltu) + Gogorarazpen labur bat atsegin, 구독 egin edo jarraitu gaituzten edukiaren erdian. Luzeagoa bada edo zerbait zehatzaz hitz egiten bada, autopromozioan egon beharko litzateke + Ardura + Bideoaren zati hau da jende gehienek bilatzen dutena + Atseguera / Sarrera-animazioa + Eduki erreala gabeko tartea. Pausa bat, irudi estatiko bat edo animazio errepikatua izan daiteke. Informazioa duten tranzizioak ez ditu barne + Amaiera-txartelak / Kredituak + Kredituak edo YouTube amaiera-txartelak agertzen direneko momentua. Informazioa duten ondorioetarako ez da + Guztia / Agurrak + Datorren bideoaren trailer narratuak, agurrak eta agurrak. Eduki gehigarria gehitzen duten atalak ez ditu barne + Aurrebista / Laburpena + Bideoan edo serie bateko beste bideoetan zer gertatuko den edo zer gertatu den erakusten duten clipen bilduma, non informazio guztia beste leku batean errepikatzen den + Tangentea / txantxak + Bideoaren eduki nagusa ulertzeko ez diren beharrezko eszena tangentziak edo txantxak. Ez ditu barne kontekstua edo aurreplaneko xehetasunak eskaintzen duten atalak + Musika: Musika gabeko atala + Musika bideoetan erabiltzeko soilik. Musika bideoetako musika gabeko atalak, beste kategoriarik ez dituen jada jasota + Saltatu + Nabarmendu + Sponsorra saltatu + Promoa saltatu + Saltatu elkarrekintza + Saltatu nabarmendutakoera + Saltatu sarrera + Saltatu etenak + Saltatu etenak + Saltatu amaiera + Saltatu гаu + Saltatu aurbizkundea + Saltatu aurbizkundea + Saltatu laburpena + Saltatu digresioa + Musikarik gabekoak salto egin + Segmentua salto egin + Patrocinatzailea salto egin da + Autopromozioa salto egin da + Gogorarazpen gogaikarria salto egin da + Ardura-puntura salto egin da + Sarrera salto egin da + Atzean salto egin da + Atzean salto egin da + Irteera salto egin da + Hook-a salto egin da + Aurreikuspena saltoz pasatu da + Aurreikuspena saltoz pasatu da + Laburpena saltoz pasatu da + Tangentia saltoz pasatu da + Musika gabeko atala saltoz pasatu da + Bidali gabeko segmentua saltoz pasatu da + Segmentu asko saltoz pasatu dira + Automatikoki salto egin + Behin automatikoki salto egin + Saltsatu botoia erakutsi + Bilatzailean erakutsi + Desaktibatu + Segmentua bidaltu ezin da: %s + SponsorBlocki一時ki erori da + Segmentua ezin da bidali (estatu: %1$s %2$s) + Segmentua bidaltu ezin da. Abiadura mugatua (beheko erabiltzaile edo IP batetik bidalitakoak gehiegi dira) + Segmentua bidaltu ezin da: %s + "Segmentua bidaltu ezin da. +Jada existitzen da" + Segmentua arrakastaz bidalitakoa + SponsorBlocki一時ki eskuragarri ez da (API-ak denbora gehiegi behar du) + SponsorBlocki batzuetan ez zaio espremiu egin daiteke (estatu %s) + SponsorBlocki ez dago erabilgarri txatorrez + Segmentuari bozka egin ezin da (API-ak denbora gehiegi behar du) + Segmentuagatik bozkatu ezin da (estatu: %1$s %2$s) + Segmentuari bozka egin ezin da: %s + Bozka positiboa + Bozka negatiboa + Kategoria alda + Ez da bozka eginik daitezkeen segmenturik + %1$s batetik %2$s ra + Aukeratu segmentu-kategoria + Kategoria desaktibatuta dago konfigurazioan. Kategoria aktibatu bidaltzeko + SponsorBlock segmentu berria + %s markatu segmentu berri baten hasiera edo amaiera gisa? + Hasi + Bukaera + Orain + Segmentua hasten denunea + Segmentua amaitzen denunea + Orduak zuzenak al dira? + "Segmentua honetatik + +%1$s +horretara +%2$s + +(%3$s) + +Prest zara bidaltzeko?" + Bidali + Hasiera amaieraren aurrekoa izan behar da + Lehen markatu kokapen bat denbora-barraren gainean + Ikusi segmentuaren aurbikusia eta ziurtatu sailkatzea leun egiten dela + Editatu segmentuaren denbora eskuz + Segmentuaren hasierako edo amaierako denbora editatu nahi duzu? + Denbora baligabea emanda + Bidali nabarmena + "Nabarmenaren denbora zuzena al da? + +%s + +Nabarmenik bidaltzen ez baduzu, sakatu Utzi eta markatu bigarren kokapena" + Bidai highlight-a + Estatistikak + Estatistikak duneen bat ez da доступgarri (APIa behera dago) + Kargatzen… + SponsorBlock desaktizatuta dago + Zure erabiltzaile izena: <b>%s</b> + Sakatu hemen zure erabiltzaile izena aldatzeko + Erabiltzaile-izena ezin da alda tu: Egoera: %1$s %2$s + Erabiltzaile izena arrakastetsunez aldatua + Zure ospea <b>%.2f</b> da + Sakatu hemen zure segmentuak ikusteko + SponsorBlock-en sailburua + Jendeari <b>%s</b> segmentu saltsi dizkiokizkie + Sakatu hemen estatistika globalak eta kontribuitzaile nagusiak ikusteko + Hori beren bizitzaren <b>%s</b> da.<br>Sakatu hemen sailburua ikusteko + <b>%s</b> segmentu saltsi dituzu + Hori <b>%s</b> da + Saltsitako segmentuen kontatzailea berreztatu? + %1$s ora eta %2$s minutu + %1$s minutu eta %2$s segundu + %s segundu + Opazitatea: + Kolorea: + "SponsorBlock bideo-segmentuen datu-base crowdsourceatua da, sponsorak, sarrera-zatiak, amaiera-zatiak eta izapiltzeko gogorarazpenak bezalakoak jasota. Komunitateak bidaltzen eta bozatzen ditu segmentuak, eta datu-basea irekia eta doan dago deskargatzeko + +Ikusten ari zaren bideoaren IDa SponsorBlock APIra bidaltzen da bere segmentuak lortzeko" diff --git a/patches/src/main/resources/addresources/values-eu-rES/youtube/strings.xml b/patches/src/main/resources/addresources/values-eu-rES/youtube/strings.xml index 70ce03fc67..e63cc902de 100644 --- a/patches/src/main/resources/addresources/values-eu-rES/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-eu-rES/youtube/strings.xml @@ -18,213 +18,1604 @@ Second \"item\" text" + Morphe לגarri + Iragarkiak + Feed-a + Orokorra + Atarijoka + Shorts + Bilatzailea + Irraspen kontrolak + besteak + Bideoa + YouTube konfigurazio-pantaila zaharra berreskuratu + Kendu fondoan erreproduziteko gehikizunak + Desgaitu atzeko planeko erreprodukzioa + Automatiko paused gailua blokeatuta dagoen + Off + Pause beti blokeatuta dagoenean + Pause wireless audio konektatuta ez badago + Desaktibu eskrola-abiaduzako limitea + Desaktibu egin layout eguneratzeak + Serverrak aplikazioaren layouta eguneratzea ekiditen du + "Aplikazioaren disegua lehen instalatu zenean erabilitako diseguera itzultzen da. + +Zerbitzari-aldezko disegu batzuk agian ez dira itzultuko. + +Aldaketen artean daude: +• Atzalki-menuaren komponentek (edo konfigurazio lotuak) agian ez dute funtzionatzen +• Zenbakien mugimendua ez da animatua + +Liburutegi fitxa erakusten da +• Bideo deskribapeneko musika atalak agian ez du funtzionatzen +• Atzalki-menuaren elementuak ezin dira ezkutatu +• Kontu-aldaketa botoia agian ez da Liburutegi fitxan agertuko" + Aktibu egin feed-en flyout menu-ko iragazkia + Feed-en flyout menu-ko iragazkia + Sartu flyout-menuen izenak filtratzeko, berri lerriek bereizita + Aktibatu Liburutegia / Zu beheko menu-iragazkia + Liburutegia / Zu beheko menu-iragazkia + Sartu Library / You beheko menuen izenak filtratzeko, berriztatze-lerrekintzatituta + Ezkutatu album-txartelak + Album-txartelak ezkutatzen ditu artista-deskribapenaren azpian + Ezkutatu artista-txartelak + Bilaketa-emaitzen goiko artista-txartela ezkutatzen du + Kanalen botoiak ezkutu + Bilaketa emaitzetako kanalen profiluekin lotutako botoiak ezkutatzen ditu, adibidez: Haraidutu, Kanala Ikusi, Komunitatea eta YouTube Music + Ezkutatu komunitate-postak + Ezkutatu banner konpaktuak + Banner konprimatuak eta komponentziaren arteko espazioa ezkutatzen ditu + Bideoen azpian dauden txartel zabagarriak + Denak erakutsi + Ezkutatu laburpen txartela soilik + Denak ezkutatu + Ezkutatu mikrofono botoi flotantea + Bilaketa-iradokizunen pantailaren beheko eskuineko mikrofono botoi flotantea ezkutatzen du + Ezkutatu Historia estantea + Ezkutatu estante horizontalak + Estante horizontalak ezkutatzen ditu, adibidez: Azken berriak, Gustuko izan neckline dirudien komunitate-postak, Jarraitu ikusten, Arakatu kanal gehiago, Arakatu gai gehiago, Zuretzat, Azken bideoak, Relevantenak, Sortzailearen komunitateko postak, Shopping eta Berriro ikusi + Ezkutatu irudien estantea + Bilaketa-emaitzekoa irudien estanteak ezkutatzen ditu + Ezkutatu \'Ganatu beste lagun batzuk mezuak bidaltzeko\' txartela + Jakinarazu tabernako Mezuak sekzioan agertzen den gonbidapen-txartela ezkutatzen du + Ezkutatu \"Azken bideoak\" botoia + Home feed-en goian agertzen den \'Azken bideoak\' banner urdina ezkutatzen du + Ezkutatu zuzeneko emitaisunak + Home feed-en eta bilaketa emaitzetan orain zuzenekoak diren bideoak ezkutatzen ditu + Ezkutatu mix zerrendak + Ezkutatu pelikulen sekzioa + Ezkutatu Jakinarazpenen menuaren gainera + Jakinarazpenen menuaren gainera ezkutatzen du kanpaiak botoia sakatu ethanean + Ezagutu \'Jakinarazi niri\' botoia + Programatutako zuzenekarretako eta estreko gogorarazpen botoia ezkutatzen du + Ezkutatu Playables + Jokoak jorateko aukera ematen duen funtzionalitatea ezkutatzen du + Ezkutatu bilaketa-termiko miniatureak + Bilaketa automatikoaren zerrendako aurbizitzak ezkutatzen ditu + Ezkutatu \"Gehiago ikusi\" botoia + Bilaketa emaitzenetako bideoz gaindiko zabaltze botoia ezkutatzen du + Ezkutatu izumezko kanalen barra + Haraidutuen kanalen izenak ezkutu + Ezaugarri honek izpidatzen ditu izumeatutako kanalen barreko kanal-izenak + Ezkutatu inkestaak + Ezkutu inkestak eta feed-eko gomendapenen zehaztasunari buruzko ohartupenak + Ezkutatu txitiketen estantea + Ezkutatu igotze-denbora + "Mugaak: +• Shorts estanteek, kanalen orrialdeek eta bilaketa emaizek igotze-denborak erakutsi ditzakete oraindik +• Ez dabil automotive layout-ekin" + Ezkutu bideoaren mini-irudia + Ezkutatu bideo etiketak + \'Auto-dubbed\' / \'Dubbed\', \'Earns commission\', \'Hyped\', \'People also watched\', \'product(s)\' eta \'You might also like\' etiketak ezkutatzen ditu + Ezkutatu ikusimenduen kopurua + "Mugaak: +• Shorts estanteek, kanalen orrialdeek eta bilaketa emaizek ikusimenduen kopuruak erakutsi ditzakete oraindik +• Ez dabil automotive layout-ekin" + Ezkutatu web bilaketen emaitzak + Ezkutatzen du bilaketa emaitzetako \'Webeko emaitza\' atala + Ezagutu behalkidea ezkutatu + Ezkutatu YouTube Doodles + YouTube logotipoko animazio seasonalak eta ekitalunetan oinarritutakoak ezkutatzen ditu + YouTube Doodles erakutzeak header pertsonalizatuak ezagutzea eragotzi dezake + Ezkutatu kanal barra + Ezkutatu kanalaren urmarka + Ezkutatu Kapituluak eta \'Bideo honetan\' botoia + Ezkutatu crowdfundinga kaxa + Ezkutatu larrialdi kaxa + Ezkutatu informazio panelak + Informazio-testuinguru panelak ezkutatzen ditu, adibidez, informazio medikoa + Ezkutatu Elkartu botoia + Ezkutatu \"Zuzeneko txataren erreproduzkapena\" botoia + Chat-eko erreproduzkuntza botoia ezkutatzen du pantaila osoko moduan + Ezkutatu panel medikoak + Ezkutuatuzatzeen seinaleak ezkutu + Ezkutatzen ditu seinaleak playera gestures egitean agertzen direnak, adibidez, \'Gora irrastu bilaketa zehatzagoa egiteko\', \'Utzi bertan utzteko\' eta \'2x\' + Ezkutatu snackbarra + Ezkutatu iznatzaileen gida + Ezkututsi \'Bideoarekin sinkronizatu\' botoia + Sakatu eta eutsi gradientea ezkutatu + Atzeleratzeko eutzinduan hatza irairtzen denean, sekoikagarriaren kontrolak gradientea ezkutatzen du + Ezkutatu denbora mugatuko erreakzioak + Komentaruen sekzioan eta zuzeneko chat-ean emojiak erabiltzeko aukera ezkutatzen du + Ezkutatu \'Laguntza & feedback\' menua + Hiru-puntu botokia erabiltzean \'Laguntza & feedback\' menua ezkutatzen du, bilaketa emaitzetan bezalakoetan + Ezkutatu erabiltzaile-izena + Zure kanalak kanalak erabiltzaile-izena ezkutatzen du \'Zu\' fitxan + Ezkutatu bideoaren izenburua + Bideoaren izenburua ezkutatzen du pantaila osoko moduan + Garbitu bideoaren subtituluak + Bideo subtituluetan \'#\', \'Fundraiser\', \'product(s)\' eta \'Shop\' bezalako esaldiak garbitzen ditu + Ezkutatu \'Galdetu Gemini\' botoia + \'Bideo honi tentang galdetu\' panelaren beheko \'Galdetu Gemini\' botoia ezkutatzen du + Bideoaren deskribapena + Hautatu bideoaren deskribapenean zein komponentek agertzen den + \'AI-ak sortutako bideo laburpena\' sekzioa ezkutatu + Ezkutatu Galdetu atala + Ezkutatu Atributuak atala + Ezkutatzen ditu Lefu Gartzatutak, Jokoak, Musika eta Aipatutako Pertsonak atalak + Ezkutatu Kanalen estekak atala + Ezkutatu Kapituluak atala + Ezkutatu Zuzenkizunlar atala + Ezkutatu \"Ikastrolako progresioa\" atala + Ezkutatu Arakastu atala + Ezkutatzen ditu Ikastrola hau arakastu eta Podcasta arakastu atalak + Ezkutatu \"Ikastrola hau arakastu\" atala + Ezkutatu \"Podcasta arakastu\" atala + Ezkutatu kanalak nabarmenduen atala + Ezikusi Irekizun nabarmenduen atala + Ezkutatu Playlist nabarmenduen sekzioa + Ezikusi Bideo nabarmenduen atala + Ezkutatu Hashtag sekzioa + Ezkututsi \"Nola egin zen hau\" atala + Ezkutatu Hype puntuak + Ezikusi Informazio-txartelen atala + Ezkutatu \'Gako Konzeptuak\' atala + Ezikusi Galdegaien atala + Ezagutu \'Bideo honen barruan bilatu\' atala ezkutatu + Ezkutatu Elizatu botoia + Ezikusi Transkripzioaren atala + Ezikusi \"Bideoaren xehetasunak\" atala + Iragazien barra + Aukeratu non ezkutzi irrosi filter bar-ra + Ezakutsi kanaleko orrialdean + Ezkutzi komentarioetan + Ezkutzi feed-etan + Ezkutzi bideo erlazionatuetan + Ezkutatu bilaketa emaitzetan + Ezkutatu ikusi-historiadan + Kanalen orrialdea + Aukeratu zein komponentak agertu nahi dituzun kanalen orrialdean + Aktibu egin kanalen fitxategiaren iragazkia + Kanal-estalkunaren filtroa + Sartu kanalen fitxen izenak filtratzeko, berrizkideen lerroka bereizita + Ezkutatu Komunitatea botoia + Ezkutatu Batu botoia + Estekien aurbizura ezkutatu + Ezkutatzen ditu kanalen goiko aldeko web-esteka klik egitekoak + Ezakutsi \'Kideentzat soilik\' txipia + Ezkutatzen du kanalen filtro-barrako \'Kideentzatik bakarrik\' txipia + Kideen estantea ezkutatu + Ezkutatzen du kanalen kideen zerrenda + Ezkutatu postuen estantea + Ezkutatu Denda botoia + Ezkutatu Harrapatu botoia + Arakatu menua + Aukeratu zein komponentak agertu nahi dituzun arakastu menuan + Ezkutatu Ikastropen menua + Ezkutatu Arakastu botoia + Ezkutatu \'Moda & Edertasuna\' menua + Ezkutatu Bideojokoen menua + Ezkutatu Hype menua + Ikasteko menua ezkutatu + Ezkutatu Zuzenekoen menua + Ezkutatu Kidegoen menua + Ezagutzi \'Pelikulak & TV\' menua + Ezagutzi Musika menua + Ezkutatu Berrien menua + Ezkutatu Playables menua + Ezagutu Podcast-en menua ezkutatu + Erosketen menua ezkutatu + Kirolen menua ezkutatu + YouTube Premium menua ezkutatu + Ezkutatu YouTube Studio menua + Ezagutu YouTube Music menua + Ezkutatu YouTube Kids menua + Ezkutatu YouTube Create menua + \"YouTube nola funtzionatzen duen\" menua ezkutatu + Lege- eta pribatutasun-oinak ezkutatu + Komentarioak + Aukeratu zein komponentek agertu behar duten iruzkinen sekzioan + Aktibu iruzkinen karusel-iragazkia + "• Beharrezkoa da carousel-eko elementuaren izen zehatza sartzea (adib. Galdetu, Hype bideo hau, Entzun YouTube Music-en) +• Carousel-eko elementuak ezkutatzeak side-effect ez-nahikoak izan ditzake +• Carousel-eko elementu guztiak ezkutatzen badira, iruzkinak atal osoa ere ezkutuko da" + Iruzkinen karusel-iragazkia + Sartu iruzkinen karrusel izenak filtratzeko, berrizhasiak erabiliz bereizita + AI txat laburpena ezkutatu + Kanalaren gida ezkutatu + Ezkutatu testuinguru mezuak + \"Kideen komentarioak\" nagusiak ezkutatu + Ezkutatu iruzkinen atala + Ezkutatu iruzkinen atala Home feed-ean + Ezkutatu \'Short bat sortu\' botoia + Ezukitu Ez-gustatzen botoia + Ezkutatu iragazki-barrako aukerak + \'Goikoak\' eta \'Berrienak\' ezukez iragazki-barrako gainerako aukerak guztiak ezkutatzen ditu + Ezkutatu opari-animazioak eta txartelak + Ezkutatu Opari botoia + Ezkutatu Info botoia + Ezkutatu txat zuzeneko donatzaileen barra + Ezkutatu txat zuzenekoaren beheko aldea ikusteko botoia + Ezkutatu txat zuzenekoaren tooltippak + Txat-sarrerako barraren ondoan agertzen diren tooltip-popuperak ezkutatzen ditu, adizentzat soilik dagoen txat-ohartupena adibidez + Ezkutatu Menu botoia + Komentari bakoitzaren 3-puntu botoko menua ezkutatzen du + Ezkutatu iruzkinen aurbekista + Ezkutatzen du bideoaren peonies iruzkinen aurbizkizuna iruzkinen sekzio osoa irekiz gero + Morphe-k ezkututa utzitako komentarioa… + Gaitu iruzkin minimalistikoen botoia + Ezkutatzen ditu iruzkin-botoian agertzen diren tx thẻ iraleak eta haien orrialde-hautagailua + Ezkutatu Eskerrak botoia + Ezkutatu \'Fan top\' botoia + Ezkutatu Itzuli botoia + Ezkututu \"Irten ez da commentsik\" mezua + Garbitu bilaketa-estekak nabrementzatuta + Bilaketa-estekak nabarmendutakoak garbitzen ditu, testu soil originala bakarrik utziz + Gako-hitz filtroa + Aukeratu hitz edo esaldi zehatzak feed-etan eta bilaketan bideoak ezkutatzeko + Ezkutatu iruzkinak gako-hitzymendiaren arabera + Ezkutatu Home bideoak gako-hitzen arabera + Ezkutatu bilaketa-emaitzak gako-hitzen arabera + Ezkutatu Harpidetzekoe bideoak gako-hitzen arabera + Ezkutatzeko gako-hitzak + "Sartu ezkutuko gako-hitzak eta esaldiak, berriz linensiatuta + +Gako-hitzak bideo-izenetan edo kanalen izenetan agertzen den edozein testu izan daitezke + +Erdian letra mayuskulak dituzten hitzek zehatzak izan behar dute (adib. iPhone, TikTok, LeBlanc)" + "Home / Subscriptions / Search / Komentarioak filtratzen dira gako-hitzen phrase-ekin bat egiten duten edukia ezagutzeko + +Mugaak: +• Shorts ezin dira kanalaren izenaren arabera ezkutatu +• Batzuk UI komponentak agian ezkutatu nahi izan +• Gako-hitza bilatzeak emaitzarik agertu ez dakar" + Hitz osoak batitu + Gako-hitz edo esalbatzun bat aipatoz betetzeak bideo-izen eta kanal-izenen z khớp zatiak saihestuko ditu<br><br>Adibidez,<br><b>\"ai\"</b>k bideo honek ezkutuko du: <b>How does AI work?</b><br>baina honek ez: <b>What does fair use mean?</b> + Ezin da gako-hitz hau erabili: %s + Aipatzen erabili behar duzu gako-hitz hau: %s + Gako-hitzak deklarazio kontradoak ditu: %s + Gako-hitza laburregia da eta aipatzea behar du: %s + Gako-hitzak bideo guztiak ezkutuko ditu: %s + "• Hasiera: %s +• Iz Harpidedunak: %s +• Bilaketa: %s +• Komentarioak: %s +• Guztira: %s" + Estatistikak + Bideo ezkutatuak, azken 24 orduetan + Bideo ezkutatuak, guztira + Azken 24 orduko kontadorea berreskuratu? + Denbora guztiko kontadorea berreskuratu? + AI kanalen iruzkia + AiSList komunitate-zerrendak AI-ak sortutako edukia bezala markatutako kanalen bideoak ezkutatu + Blokeatzeko zerrenda: Konfidantzia alta + Ohartuteko zerrenda: Konfidantzia moderatua + Bideoak ezkutatu Hasieran + Bideoak ezkutatu bilaketan + "• Homea: %s +• Bilaketa: %s +• Guztira: %s" + Blokeatze-zerrendako kanalak + Ohartutze-zerrendako kanalak + Gaitu iragazki bat kanalak kargatzeko + Kanalak kargatzen... + "Homea eta bilaketa emaitzak filtratu dira AiSList-ek markatutako kanaletako bideoak ezkutatzeko + +Mugak: +• Filtratuz dauden AI Shorts-ak espazio huts gisa agertu daitezke +• Balo AI bideo batzuk oraindik agertu daitezke" + "AiSList boluntariuek mantentzen duten, AI-k sortutako edukia publikatzen duten YouTube kanalen zerrenda irekia da. Erabiltzaileek kanalak saldarazten dituzte eta berriztasuna egiaztatzen da gehitu aurretik + +Zerrenda GitHub-etik deskargatzen da eta bideoak zure gailuan filtratzen dira" + AI kanalak salatu + Salaketa sarrera gehitu bideo menura + Bideo menura sarrera bat gehitu bideoko kanala AiSList-era salatzeko + Aurkezlearen izena + AiSList-eko sailkatze publikoan zure salaketen ondoan agertzen den izena. Aukera honek hutsik egon bitartean, salatzea ez da eskuragarri + AI kanala salatu + "%s AiSList-era bidali, bideo hau froga gisa erabiliz? + + Kanal hauek bakarrik saldutu: +• AI ahotsak, irudiak, gidoiak edo musika nagusi kontzeptu gisa erabiltzen duten +• AI tresnekin edukia masiboki ekoizten duten eta lan giza txiki egiten duten + +Ez saldutu AI-a soilik eztabaida duten edo bideoen zati batzuetan AI-a erabiltzen duten kanaletan. + +Txostenak publikoak dira eta kanal bat zerrendara gehitu aurretik berrikusi egiten dira. Ezabatzeak GitHub-en eskaera baten bidez bakarrik da posiblea" + Kanal hau AiSList blokeo-zerrendan dagoleba + Kanal hau AiSList ohartu-zerrendan dagoleba + Saldutu + Kanalak saldutu nahi badizue, AI kanal-iragazkiaren konfigurazioan bidaltzaile-izen bat definitu + Kanala bilatzen... + Ezin da bideo honen kanala aurkitu + Kanala saldaraztuta dago eta berrikuntza zain + AiSList-ek ezin du saldarazpena gorde, geroago saiatu berriro + Saldarazpen gehiegi bidali dira, geroago saiatu berriro + AiSList-ek saldarazpena bazatu du + Saldarazpena huts egin du, egiaztatu konexioa + Kanalen iragarki-zscalerra + Whitelistatutako kanalen bideoak iragarkiekin joko dira, sortzaileak ordaindu ditzaten. Beste 모든 kanaletan iragarkiak blokeatuta jarraituko dira + Kanala iragarkien whitelistara gehitu + Kanala iragarkien whitelistatik kendu + Ezkutatu sortzailearen denda-estantea + Ezkutatu amaiera-pantailako denda-bannerra + Ezkutatu iragarki orokorrak + Ezkutatu merchandise-bannerrak + Ezagutu seinaleak ezkutatu + Ezkutatu auto-patrozinatutako txartelak + Ezkutatu inkaupazio-estekak + Ezkutatu bideo-iragarkiak + Ezkutatu YouTube Premium promozioak + Kargatzen… + Kanal current: + Whitelistara gehitu + Kendu + Whitelistatutako kanalak + Ez dago whitelistatutako kanalik + Kanala whitelistara gehituta + Kanala whitelistatik kenduta + Bilatu kanalean + Bilaketa botoi bigarra gehitzen du kanalen orrialdeetan, kanal horretan bakarrik bilatzeko + Bilan kanala + Kanalen bilaketaren emaitzak + Ezer ez da aurkitu kanalean + Ezin izan da kanala bilatu + Bideoak + Postak + Kanal honetako postuik ez dira bat + Errelazioa + Gehien ikusitakoak + Gutxiago ikusitakoak + Berrienak + Zaharrenak + Short motelko laburrenak + Luzeenak + EstekaClipboard-era kopiatuta + Denbora-zigiluarekin esteka Clipboard-era kopiatuta + Erakutsi \'Bideo-estekaa kopiatu\' botoia + Sakatu bideo-estekaa kopiatzeko, sakatu eta eutsi denbora marka batekin kopiatzeko + Kopiatu denbora-zigilatza lehenengoa + Inbertsitu \'Bideo-estekaa kopiatu\' botoiaren sakatu eta sakatu-eta-eutsi ekintzak + Kopioatu bideoaren izenburua + Sakatu eta eutsi bideoaren izenburuan kopiatzeko + Bideoaren izenburua kopiatu da klima-papan + Kopioatu komentarioak + Sakatu eta eutsi komentario batean kopiatzeko + Komentarioa kopiatu da klima-papan + Sakatu eta mantendu bideo-menua irekitzeko + Feed-eko bideo baten edozein lekuan sakatu eta mantentzeak bere menua irekiko du + Ezagutun geroago botoia erakutsi miniaturetan + Ezagutun geroago botoia erakutsi feed-eko bideoen miniaturetan. Sakatu eta mantentzeak \"Playlistean gorde\" aukera irekiko du + Deskargatu postuen irudiak + Deskarga-botoi bat agertzen da irudiak edo GIFak duten postuen Partekitu botoiaren ondoan, eta deskarga-elementu bat gehitzen da haien menuera + Deskargatu irudiak + Deskargatu irudia + %s irudi deskargatzen ari da… + Irudia deskargatzen ari da… + Irudia %snon gorde da + Ezin izan da irudia deskargatu + Ez da irurik aurkitu post honetan + Galdetu elementua gehitu menu zabagarria + Bideoaren laburpen elementu bat gehitzen du feed-eko menu zabagarrian + "Galdetzeko hasieran, Ask gai dagoen herrialdeko kontu bat izatea eta saioa hasita izatea behar da. + +Ask botoia duen bideo bat behin ireki ondoren, funtzioak bideo guztiekin funtzionatzen du, nahiz eta egungo herrialdean ez egon Ask funtzioa." + Laburtu bideoa + Ezin izan da Ask ireki bideo honentzat + Ask ez dago eskuragarri zure konturako edo egungo herrialdeko + Ikusi bisturatzeen kopurua miniaturak + Feedeko bideoen bisturatze kopurua miniaturakren beheko aldean eskerian agertzen da, izenaren beheko aldean agertzean behin. Bideo batzuek, estanteetan daudenak adibidez, agian ez dute bisturatze kopurua erakutsiko + Erakutsi feed-eko miniaturaren gustu kopurua + Erakutsi feed-eko miniaturaren gustu ehunekoa + Ibilasi komentarioak + Sakatu iruzkinen bilaketa botoia bilatutako hitz guztiak dituzten kargatutako iruzkinak soilik ikusteko. Esaldia bat bilatzeko, jarri hitzak aipatetan + Iruzkinak bilatu + Bilatu nahi dituzun hitzak + Bilatu + Garbitu + Aldatu ordena edo ireki berriro iruzkinak bilatzeko + Bilaketa garbituta. Aldatu ordena edo ireki berriro iruzkinak iruzkin guztiak ikusteko + Desaktibu \'Sartu TV-n\' popup-a + \'Konektatu zure gailak\' popup-a desaktibatu + Desaktibu kapituluak salto egiteko bi-toque-a + Desaktibatzen du kapituluak saltzen dituen bi hatseko odolku gesturea, ausauntza batzuekin hatse bakantiarekin aktiba daitekeen + Deskargatu bideoak, podcastak edo abestuak hirugarrenDownloading aplikazioekin + Ikusi sehailu-deskarga botoia + Botoi bat erakusten du bideo-atarian zure deskargatzaile kanpora aukeratzeko + Deskarga azkarra ukitzean + Atzealdean deskargarako gogorarazpenak. Ukitu eta eutsi deskargatzailea irekitzeko. YTDLnis eta YTindirrekin funtzionatzen du + Markatu deskargak ikusitako gisa + Deskargatzaile kanpora deskargatutako bideoak zure YouTube ikusimenduen historiana gehitzen dira + Full HD (1080p) + Erdi (192 kbps) + Deskarga azkarra + Bideoa + Audioa + Aukera guztiak + Erakutsi deskarga Shorts menuan + Deskargatzaile kanpora egon menu-elementu bat gehitzen dio Shorts atariari + Deskargatu + Ahots-gain translationa + Bideoaren azpitituluak ozen irakurri gailuaren testu-ahots sistemaren bidez, erreprodukzioarekin sinkronizatuta + Botoi gainjartua erakusten du bideo jedoian. Tokatu itzulpena aktibatzeko edo desaktibatzeko, eta sakatu heldu konfigurazio azkarra lortzeko + Hizkuntza + Itzulpen-estaltzailea + Jatorrizko audiorako bolumena + YouTube audiorako bolumena itzulpena hitz egiten duen bitartean + Itzulpen bolumena + Itzulpen-ahots dublatuaren bolumena + Hitzketa-abiadura maximoa + Abiadura-multiplikatore maximoa hitzketa subtituluen slot batean egokitzeko + "Itzulpen API doakoa, API giltzorik ez da behar. Eguneko 5.000 karakterera arte doako mailan, edo eguneko 50.000 karakterera arte helbide elektroniko batekin. + +Sakatu hemen MyMemory.translated.net irekitzeko" + MyMemory helbide elektronikoa + Aukeratua. Sartu zure MyMemory helbide elektronikoa eguneroko limitea 5.000tik 50.000 karakteretara igotzeko. Utzi hutsik anonimo limitea erabiltzeko + "OpenRouter bidezko AI itzulpen ordainia. Karga egin behin eta itzuli bideo milaka. Mistral Nemo da aukerik merkeena, Gemma 4-ek kalitatez goiena eskaintzen du. + +Sakatu hemen OpenRouter.ai irekitzeko" + OpenRouter API gakoa + Zure OpenRouter API gakoa. Sortu gako bat OpenRouter.ai > Workspace > API Keys bidean + OpenRouter Modelua + Kustomizatua + OpenRouter ez dago konfiguratuta + Sartu zure OpenRouter API gakoa konfigurazioan hornitzaile hau hautatu aurretik + Gratis + %s 100 bideo orduko + Ahotsa + Sistema + Gizonezko multilingue + Emeginezko multilingue + Gizonezkoa + Emegizkoa + Morphing ordua da! + Itzulpena ez dago eskuragarri + "Itzulpen zerbitzariak HTTP %s errorea itzuli du. Zure IP helbidea rate-limitatuta egon da ziurren 24 ordu arte. + +Itzulpenak erabiltzen jarraitzeko, aldatu itzulpen hornitzailea araukadetan edo erabili VPN bat" + Ez erakutsi berriz + "Zure konfigurazioetan OpenRouter API gako bat dago. + +Zure API gakoa inoiz ez dela partekat knockout. Hura duen edozeinek zure OpenRouter kredituak erabili ditzake" + OpenRouter krediten limitea lortua da + Zure OpenRouter krediten limitea lortua da. Saldoa kargatu edo handitu zure gakoaren gastu limitea OpenRouter.ai-en + Ireki OpenRouter.ai + OpenRouter itzulpena huts egin du (HTTP %s) + MyMemory-ren eguneroko limitea lortua da + Hurrengoa %s-en eskuragarri + Gaurko MyMemory-ren doako itzulpen kuotaa (5.000 carattere) erabili duzu. Ezugarapenetan MyMemory-ren posta elektroniko bat gehitu eguneroko limitea 50.000 karakterera igotzeko, edo itzulpen zerbitzu beste bat choose egin + Gaurko MyMemory-ren itzulpen kuotaa (50.000 carattere) erabili duzu. Itzulpen zerbitzu beste batean aldatu edo bihar saiatu berriz + MyMemory itzulpena huts egin du (egoera %s) + Gaitu bideo-buclesa + Ez gogoratu bideo-bucles + Bideo-buclesen egoera currenta ez gordetzea hurrengo bideoez + Erakutsi Bideoa errepika botoia + Bideo-buclesa aktibatu egin da + Bideo-loopa txututa dago + Beheko tartea + Hasi + Amaiera + [hh:]mm:ss + [hh:]mm:ss (hutsik = bideoaren amaiera) + Konfiguratu + Garbitu + %1$s - %2$s tartean errepikatzen + Denbora-tarte okerra + Erdi-irudia botoia erakutsi + Erdi-irudia menua erakutsi + Erdi-irudiari aldatu + Erakutsi Denak jarrai botoia + "Sakatu kanaleko bideo guztien zerrenda sortzeko, sakatu eta mantendu atzean uzteko + +Informazioa: Agian ez funtzionatzen duen zuzeneko emanbisteetan" + Playlist-a sortzeko modua + Eduki guztia (Denboraren arabera ordenatu, Igoraki) + Eduki guztia (Denbora arabeko ordena) + Eduki guztia (Herensika herensuka) + Bideoak soilik (Denboraen arabera ordena) + Bideoak soilik (Herensika herensuka) + Shorts soilik (Denboraen arabera ordena) + Shorts soilik (Herensika herensuka) + Streaming bideoak soilik (Denboraen arabera ordena) + Streaming bideoak soilik (Herensika herensuka) + Kideentzako eduki guztia + Kideentzako bideoak + Kideentzako Shorts + Kideentzako zuzeneko emisioak + Playlist-a ezin da sortu kanalen ID-ak ez batzaten + Erakutsi bideoa berkargatzeko botoia + Bideo-IDa ez dago eskuragarri. Mesedez, kargatu berri bideoa eskuz + Erakutsi \"Gero ikusi\"n gorde botoia + Sakatu \"Gero ikusi\"n gordetzeko, sakatu eta eutsi herren aukerak ikusteko + Bideoa arrakastetsunez gordetako \"gero ikusi\" zerrendan + Bideo hau dagoすでに \"gero ikusi\" zerrendan gorde + Bideo hau ezin da \"gero ikusi\" zerrendan gorde + Erakutsi \'Gero ikusteko gorde\' botoia seiatzailearen konfigurazio-menuan + Gaur egungo bideoa \'Gero ikusteko\' gorde sailkatzailearen konfigurazio-menuatik + Erakutsi \"Gero ikusi\"n gorde botoia haurren bideoetan + Umeen bideoak \'Gero ikusteko\' gorde menu-txikiatik + Erakutsi \'Gero ikusteko gorde\' botoia shorts-en menu-txikian + Shortsak \'Gero ikusi\'n gordetzea ahalbidetzen du menu txikiari bidez + Bideratu \"Gero ikusi\" zerrendara + Gero ikusi + Gehitu Morphe zerrendara + Mantendu herren saila saioen artean + Herren saila mantentzen da aplikazioa berrifiziatu ondoren + Gorde eta Herrenatuko ekintzak trukatu + Sakatu herren aukerak ikusteko, sakatu eta eutsi \"Gero ikusi\"n gordetzeko + Hurrengoa errekuan play egiteko mota + Lehenetsua + Erroda natiboa + Morphe erroda + Morphe erroda bezeroaren aldetik + Gehitu feed-eko flyout-en herren menuko elementua + Flyout-en elementu gehigarri bat gehitzen du. Aktibatu hau \"Herrenean gehitu\" ez bada ikusten + Herrena + Hirutegia (%s) + Garbitu dena + Ez dago ezer herrenetan + Erabili \'Juekin jarraitu herrenetan\' edozein bideoan + Orain jueltan + Juekin jarraitu herrenetan + Joko azkena herren egutekoan + Orderinga aldatu + Kendu + Herrenetan gehituta + Ez izan da ahal izan bideo hau herren egutekoan gehitu + Jokozerrak ezin dira herren egutekoan gehitu + Herrena beteta dago + Ezin izan da bideoa hasi + Iragarkien zurendapen zerrenda flyout menuan + Flyout elementu bat gehitzen du, bideo horren kanalean iragarkiak erreproduzitzeko + Ezaugarri honek PotHelper erabiltzea edo \"Spoof video streams\" TV-ra edo Android VR saio-hasierara aldatzea eska diezaki + Abiaduraren zuri-zerrenda flyout menuan + Flyout elementu bat gehitzen du, bideo horren kanala beti 1.0x abiaduran erreproduzitzeko + Ez dago saioa hasita + Ez dago zerrenda-ID-ik + Herrenak hutsik dago + Ez dago bideo-ID-ik + Huts egin du: %s + Herrenan gehitu ezin da + Herrena sortu ezin da + Herrenetik kendu ezin da + Herrena gorde ezin da + Herrenan gehituta + Herrena sortuta + Herren sailatik kenduta + Herren saila %s-en gorde da + Morphe Araua + Gehitu herren sailara + Gehitu herren sailara eta ireki + Gehitu herren sailara eta jarraitu + Gehitu eta kargatu berri bideoa + Kendu herrenetik + Kendu eta ireki herrena + Kendu eta kargatu berriro bideoa + Ireki herrena + Gorde herrena zerrendan + Playlista gorde da + %s-n gorde da + Hutsi herrenidea + Mixa kargatzen ari da… + Ireki kanala zuzeneko avatarretik + Kanalaren avatara sakatzean zuzeneko emanbistea irekitzea ekiditen du + Desaktibatu bilaketa zehatzaren keze gestua + Desaktibatzen du bilaketa-barra gora kéo egitean aktibatzen den bilaketa zehatz modu + Gaitu zuzenekarren DVR-a + Zuzenekar guztietan aurrera edo atzera nabigatzeko aukera ematen du, normalean bera gailikatzen dutenetan ere + Gaitu seekbar-ren miniaturak + Bideo baten miniatura erakusten du seekbar-ra arrastatzean + Gehien erreproduzita + "'Gaitu zuzenekarren DVR-a' agian eskuragarri ez da + +Horretarako, gaituren 'Bideo-fluxuak imitatu' eta aldatu 'Klerente lehenetsua' 'TV Simply' edo 'visionOS'-ra" + Luzeitu zuzeneko emisioen DVR iraupena + Zuzeneko emisioak 7 egun arte atzera itzuliko dizuk + "'Luzeitu zuzeneko emisioen DVR iraupena' agian ez dago eskuragarri + +Hona konponduz, aktibu 'Bideo-emisioak imitatzea' eta alda 'Beherako bezeroa' 'TV Simply' edo 'visionOS'ra" + Aktibu irrasiraz bilatzeko aukera + Aktibatzen du iritsateko irrasiro, bideo jarlatzaia sakatu eta mantendu underminean 2x abiadurean jokoan jartzearen ordez + Gaitu ukitu eta bilatu aukera + Sakatu soilik bilatzailean posizio hura lortzeko, arrastatu gabe + Ezkerreko irrasteko gesturea + Eskuineko irrasteko gesturea + Goizko irraspen gesturea + Ezkerreko/Eskumeko zonaren zabalera + Pantailaren ertz bakoitzaren ehunekoa ezkerreko eta eskuineko irraspen gestureentzat erreserbatua (5-50) + Argizkia + Bolumena + Aberrizkoa + Txoelectrico + Ezagotatu irraspenak blokeatuta dagoen + Irraspen gestureak ezagoten dira sekoerea \'Pantaila blokeatu\' batekin blokeatuta dagoen bitartean + Gaitu sakatu eta irrastu jesnutorea + Irraspen-kezkua ezagutzeden aurretik, sakatu eta mantendu behar da + Gaitu haptiko erantzuna + Gorde eta berakututsi argizkia + Gogoratu argizune maila pantaila oso moduan sartzean / ateratzean + Gaitu argizki automatikoaren jesnutorea + Argizunea bere gutxieneko balorera irairtzeak sistemaren auto-argizunea aktibatzen du + Automatikoa + Irrastu-estalkiaren timeout-a + Estalkia ikusgarria den milisegundoen kopurua + Irrastu-estalkiaren fondoaren opazitatea + Doitu transparenzia maila (0en garduna da, 100en opakoa) + Mugitze-estalkiaren argizune-kolorea + Sagaratu argizune kontrolak progress bar-raren kolorea + Irraspen overlay bolumen kolorea + Sagaratu bolumen kontrolak progress bar-raren kolorea + Irraspen-overlayko abiadura-kolorea + Sagaratu playback-abiadura-kontrolen progresbarraren kolorea + Irraspen overlay testu neurria + Doitu pantailako testuaren tamaina (1-30) + Irrasteko magnitude-oreka + Sagaratu egokitzapena aktibatzea aurretik behar den irairtze distantzia + Bolumenaren irrasgarritasuna + Balore altuagoek irraspen gehiago eskatzen dute bolumen urrats bakoitzeko + Bolumen urratsak + Bolumen handitzea + Bolumen maximoa gaindituz gora irabazteak bolumena handitzen du, eta horrek audio okerrak distorsionatu ditzake + Gailuaren lehenestapena + Argizuneen irraspen sensitibitatea + Balio altuagoek irraspen gehiago eskatzen dute argizuneen urrats bakoitzeko + Abiadura-irraspen sensitibitatea + Balio altuagoek irraspen gehiago eskatzen dute abiadura-urrats bakoitzeko + Abiadura urratsa + Abiadura-eremunaren altura + Abiadura-kezkatu-eremunaren altura, playback-er reproduciblearen altueren ehuneko gisa (5-75) + Abiadura + Overlay zirkularra (minimalista) + Overlay bertikalak + Overlay bertikalak (minimalista) + Gaitu swipe bideoak aldatzeko + Irairtzi pantaila oso moduan hurrengo edo aurreko bideora saltzeko + Ekintza botoiak + Aukeratu zein ekintza-botoiak agertzen diren zutik eta pantaila osoko moduan + Zubileko botoiak + Desgaitu Atsegin eta Harpidetu distira + Saiaturzaileak aipatzen dituztenean, Gustuko duten eta Harrapatu botoiak distira egitea ekiditen du + Ezkutatu ekintza-barra + Ezkutatu Atsegin eta Atsegin ez botoiak + Ezkutatu Partekatu botoia + Ezkutatu \"Stop ads\" botoia + Ezkutatu Iruzkinak botoia + Ezagutu botoia ezkutatu + Ezkutatu Salatu botoia + Ezkutatu Remix botoia + Ezkutatu Deskargatu botoia + Ezagutzia botoia ezkutatu + Promotu botoia ezkutatu + Eskerrak botoia ezkutatu + Galdetu botoia ezkutatu + Ezkutu kanala eta Izan subscribed botoia + Ezkutatzen ditu kanalaren avatarra eta Izan subscribed botoia ekzio-barrara hasieran + Ezagutze botoia ezkutatu + Ekintza-barrari hasieran, kanalen avatarraren ondoan dauden Ezagutze eta Elkartu botoiak ezkutatzen ditu + Klipa botoia ezkutatu + Kuentu batzuetan agian ez dabilen + Denda botoia ezkutatu + Gorde botoia ezkutatu + Gehiago botoia ezkutatu + Pantaila osoko botoiak + Ezagutarazi \'Ez dut gustatzen\' botoia + Ezagutarazi \'Gustatzen zait\' botoia + Ezagutarazi \'Zirbilak zuzenekoak\' botoia + Ezagutarazi Mix botoia + Ezagutarazi \'Bideo gehiago\' botoia + Ezagutarazi Playlist botoia + Ekintza azkarren goiko margina + Ekintza azkarren barraren goiko margina ezarri (0-32 dp) + Kaptuak + Kudeatu azpititulu automatikoak eta azpitituluen cookie-ak + Kaptu automatikoen ikusgarritasuna + Erakutsi beti + Ez erakutsi inoiz + Erakutsi soinua aktibatuta dagoenean + Erakutsi bakitik egon uenean + Lehenagoa den azpitituluen hizkuntza + Lehenikly eskaintzailearen azpitituluak hautatzen ditu, eta ondoren automatikoki itzultzen ditu lehenagoa den hizkuntzara + Txerto + Gehitu auto-itzulpen hizkuntza gehigiz + Auto-itzulpen menura hizkuntza gehiago gehitzen ditu + Kaptuen cookie-ak konfiguratu + "YouTube-ren bug stock bat konpondu egiten du, non automatikoak diren subtituluak ez dazdan erabilgarriak cookie-ak falta direlako + +'Subtitulu-cookie-ak' emanda izan behar dira" + Kaptu-cookieak + Timed Text API (Caption Data API) eskaeretan ezartuko diren cookie-ak + Lortu kaptu-cookieak + Klik egin hemen cookie-ak lortzeko + Cookieak lortzea huts egin du + Cookieak dagoenbe + Cookieak arrakastaz eguneratuta daude + Konpondu transkripzioa + Transkripzioa kargatzea eragoindu ditzakeen baldintza aurreko egiaztapen hutsbaten arazoa konpondu + Erakutsi subtituluak irudi-irudian + Nabigazio botoiak + Aukeratu zein botoiak agertu behar diren behera navigazio barra + Ezagutazi Home botoia + Ezagutazi Shorts botoia + Ezagutazi Sortu botoia + Ezagutazi Izapidetzak botoia + Ezagutazi Jakinarazpenak botoia + Erakutsi Bilaketa botoia + Bilaketa botoiaren posizioa + Konfigurazio botoia erakutsi + Konfigurazio botoiaren posizioa + ⚙️ botoiaren ekintza + Aktibatuta dagoenean Morphe konfigurazioak irekitzen ditu, edo desaktibatuta dagoenean YouTube konfigurazioak + Eskutatu Sortu botoia Jakinarazpen botoiarekin + Aukera hau aldatzeak ez badu eragina duenean, saiatu Inkognito modura aldatzen + Ezkutatu nabigazio botoien etiketak + Ezkutu eduki berriaren puntua + Ezkutatu nabigazio-barra + "Honek nabigazio barra guztiz ezkutatzen du, \"Zu\" fitxa barne. + +Konfiguraziora sartzeko, aktibu Konfigurazio botoia Oroharra → Barrako tresnak → Konfigurazioa erakutsi bidean" + Gaitu nabigazio botoi estuagoak + Botoien artekodistantzia horizontala murrizten du + Labelik gabeko nabigazio botoiak desaktibatu + YouTube-k labelik gabe bidaltzen baditu, nabigazio botoiak labelekin erakusten ditu + Desaktibu barrako nabigazio-txarkeak + YouTube-k Home-ren goiko txarke batean mugituta badu, Izakiztunak erakutsiko ditu nabigazio-barran + Gaitu nabigazio barraren animazioak + Tabuladorenen artean aldatzen denean trantsizioen animazioak egiten ditu + Desaktibu nabigazio-barra automatikoan ezkutatzeko aukera + Nabigazio barraa ikusgarri mantentzen du scroll egitean + Nabigazio-barraren estiloa + Lehenagoa + Transluszentia + Klaratua + Opakoa + Estilo honek zure gailua edo YouTube bertsioa ez duten supportatu aukera, eta layout-eko arazteak edo crash-ak eragin ditzake + Barra tresnak + Aukeratu toolbarran zein komponentak agerti nahi diren + Cast botoia ezkutatu + Ezkutatu Txat botoia + Mikrofono botoia ezkutatu + Ezkutatu Bilaketa botoia + Gaitzituta dagoenean, ukitzeak Morphe konfigurazioa irekiko du eta sakadura luzeak YouTube konfigurazioa irekiko du; txaldatumoldatzeko desaktibatu + Flyout menua + Aukeratu zein komponentei eman diezaizkien flywheel menuan + Ezkutatu \"⚙️ Gehiago\" menua + Ezkutatu \"Ingurune modua\" menua + "'Audio Track' menua ezkututa dago + +'Audio Track' menua erakutsetzeko, alda 'Spoof video streams' 'Android Studio' eta 'Android VR Downgraded' ez den edozein klientetara" + Ezkutatu \'Audio pista\' menua + Ezkutatu \'Audio pista\' menuaren oina + Ezkutatu Azpitituluak menua + Ezkutatu Azpitituluak menuaren oina + Ezkutatu Azpitituluen menuaren gainera + Ezkutatu \"Laguntza & iritzi\" menua + Ezkutatu \'Entzun YouTube Musikan\' menua + Ezkutatu \'Pantalla blokeatzea\' menua + Ezkutatu \"Bideoa errepika\" menua + Ezkutatu \'Bidean\' menua + Ezkutatu Kalitate menua + Ezkutatu Kalitate menuko oina + Ezkutatu Kalitate menuko goiburua + Ezkutatu \"Lo sailatzailea\" menua + Ezagutzi \'Erregengatze-abiaduz\' menua + Ezkutatu \"Bolumen egonkorra\" menua + Ezkutatu \'Ikusi VR-tan\' menua + Ezkutatu automatikoki erreproduziteko aurbikusia + Autoplay hasieran, datorren bideoaren aurbizitza ezkutatzen du pantaila osoan + Overlayer botoiak + Aukeratu zein botoiak agertu behar diren bideo-atarijainaren gainean + Ezkutatu Autoplay botoia + Ezkutatu Azpianagusiak botoia + Ezkutatu Cast botoia + Ezkutatu Itxi botoia + Ezkutatu Pantaila osoa botoia + Bideo-atarijainaren kontrolak inguratzen dituen itzalilaguna ezkutatzen du + Kontrol botoien fondoaren opazitatea + Doituatuzatzeen kontrol botoien eta denbora-barrari atzean dagoen fondoaren opazitatea (0 ikusgarri gabe, 100 solido) + Ezkutatu Aurrekoa & Hurrengoko botoiak + Ezkutatu Konfigurazio botoia + Ezkutatu amaiera-pantailako txartelak + Giro-modua + Kudeatu Ambient moduko ikusgarritasuna eta energia-mugaak + Ambient moduko gehikerak saltatu + Ambient moduak bateria-aurrekitzaile moduan aktibo mantentzea ahalbidetzen du + Desaktibu Ambient moda + Desaktibu Ambient modua pantaila osoan + Ezkutatu informazio-txartelak + Desaktibu zenbakien mugimendu-animazioak + Like-en eta bistaratzeen zenbakiak countatzearen animazioa gelditzea + Ezkutatu bideo-izatzaileko bilatzailea + Ezkutatu bideo-miniaturak bilatzailean + Ezohitu jada ikusi dituzun bideoen miniaturatan agertzen den progresbarra + Aktibatu pantaila osoko bilatzaile handia + Ezohitu Shorts-ak + Ezkutatu kanalen orrian + Ezkutatu Hasierako feedean eta bideo erlazionatuetan + Ezkutatu bilaketa emaitzetan + Ezkututu Iz Harpidedunen feed-ean + Ezkututu bideoaren deskribapenean + Ezkututu ikusiHizteginan + Shortsatako seiogarria + Zein komponentak agertu nahi den Shorts playerean aukeratu + Desaktibu bi-tap eta atsegin + Ezkutatu AI botoia + Ezkutatu \'Automatiki doblatua\' etiketa + Ezkutatu kanalak barra + Iruzkinak botoia ezkutatu + Epektu botoia ezagutu + Ezkutatu bideoko estekaren etiketa + Ezagutapen-keinuak ezkutatu + Ezkutatu \'Pantaila berdea\' botoia + Hashtag botoia ezagutu + Ezkutatu informazio panela + Elkartu botoia ezagutu + Atsegin botoia ezkutatu + Atsegin botoiaren iturri-animazioa ezkutatu + Ezkutatu zuzeneko aurbizitza + Kokapen etiketa ezagutu + Ezkutatu nabigazio barra + Ezkutatu \'Postu berriak\' botoia + Pause egindako gainjarekin datozen botoiak ezagutu + Iruzkin aurbizitza ezagutu + Remix botoia ezkutatu + Ezagutzi Gorde botoia + Ezkutatu \'Musika gordetu\' botoia + Bilaketa iradokizunak ezagutu + Partekatu botoia ezkutatu + Denda botoia ezagutu + Sondoa botoia ezkutatu + Ezkutatu soinuen metadatuen etiketa + Ezagutزی izen eman botoia ezkutatu + Ezkutatu \'Erosi Super Eskututsa\' botoia + Produktu markatuak ezkutatu + Datorren botoia ezkutatu + Ezkutatu \'Sondo hau erabili\' botoia + Ezkutatu \'Xablon hau erabili\' botoia + Ezkutatu bideoaren izenburua + Ezkutatu amaiera-pantailako bideo suggested + "Ezkutatzen du bideoa amaitzean agertzen den bideo suggested overlay-a + +Konfigurazio hau automatikoki jarraitzeko aukera desaktatuta badago soilik funtzionatzen du. YouTube-en alda dabilke: +Konfigurazioa → Erregengatzea (edo Autoplay) → Hurrengo bideoa automatikoki jarraitu" + Ezkutatu bideo lotutakoak + Ezohitu \"Bideo gehiago\" overlay-a + Pantaila osoko moduan gora irrastea eginean agertzen den \'Bideo gehiago\'tako kapa ezaugarria ezkutatzen du + Ezkutatu bideoko denbora-zigilu + Desaktibu playback-eko popup-panelak + Desaktibatzen ditu playlist-eko eta txat zuzeneko panelak automatikoki irekitzea + Desaktibu playlisten automatiko jokoa + Playlisten hurrengo bideoari automatikoki pasatzea saihesten du + Desaktibu \'Jarraitu ikusten?\' galderaren gomendua + Irten pantaila oso modu-tik bideoa amaitzean + Desaktibatu + Zubildun bakarrik + Kutsu bakarrik + Zubildun eta landscape + Pantaila osoko keze-gestuak + Atzalkada-keiztuak pantaila osoko moduan + Desaktibu bertikalko goizondo keze-gestua + Desaktibu bertikalko behera keze-gestua + Desaktibu horizontalko behera keze-gestua + Desaktibu pinch-to-zoom keiztuen + Itzuli pizka-zoom funtzioa + Ireki bideoak pantaila osoko moduan + Desaktibatuta + Zubildun + Horizontala + Pantaila osoko bideoaren eskala + Lehenagoa + Luzea (pantaila betetzea) + Zoom (moztu betetzeko) + Erakutsi bideoaren eskala botoia + Sakatu Lehenagoa, Luzea eta Zoom artean altxatzeko + Pantaila osoan bakarrik erakutsi + Ezeztu bideoaren eskala botoia zutik jedoian + Ikusi Bakuteko botoia + Atari-estaldiaren opazitatea + Egokitu transparenzia maila (0 garduna da, 100 opakoa) + Bilaketa-barra + Desaktibatuta + Zabal + Oso zabal + Gailuaren layout-a + Lehenxekoa + Telefonoa + Tableta + Automobilismoa + "Aldaketen artean honak daude: + +Tabletaren diseinua +• Komunitatekoa postak ezkututa daude +• Feed-etan erreproduziteko aukera ez dago eskuragarri +• Remix botoia eta Sotua botoia ez daude eskuragarri Shorts-etan +• 'Bideo gehiago' overlay-a agian ez dago eskuragarri +• Bideo zaharren ekintza-barra ez dagoたたtuta +• Menu flotantearen elementuek agian ez dute behar bezala funtzionatzen + +Automobilen diseinua +• Feed-a gaiak eta kanalak arabera antolatuta dago +• Feed-etan erreproduziteko aukera ez dago eskuragarri +• Remix botoia eta Sotua botoia ez daude eskuragarri Shorts-etan +• 'Bideo gehiago' overlay-a agian ez dago eskuragarri +• Shorts-ak erreproduzitzaile ohikoan irekitzen dira +• Bideo zaharren ekintza-barra ez dagoたたtuta" + Aktibu tabletaren disenuat playback-ean + "Elkarrailtzalearen layout zaharra berreskuratzen du + +Limitazioa: 'Bideo ekintza barra zaharra berreskuratu' tắt dago bada, 'Bideo gehiago' overlay-a agian ez dago eskuragarri" + 21.11.48 - Lehengo kaptuen jokapenari itzuli + 20.39.41 - Lorekin gorde zen save zaharra playlistera itzuli + 20.28.41 - Desaktibu ikono lutsak + 20.13.41 - Berreskuratu kanalak bideo playera azpian + Ireki sistemaren partekatze-panela + Edukia partekatzean, aplikazioaren ordez sistemaren partekatze-panelak irekiko ditu + YouTube Music botoiak gainditu + Zure musika-aplikazio lehenetsua irekitzen du stock YouTube Music aplikazioaren ordez + Musika aplikazioaren pakete-izena + Instalatu duzun musika aplikazioaren pakete-izena + Hasierako orria + Lehenagoa + Izakinta guztiak + Kanalak arakatu + Ikastintzak / Ikaskuntza + Arakatu + Moda & Edertasuna + Bideojokoak + Historia + Liburutegia + Gustuko bideoak + Zuzenean + Pelikulak + Musika + Berriak + Jakinarazpenak + Zerrendak + Bilatu + Erosketak + Shorts + Kirolak + Izakintzak + Joerak + Errealitate Birtuala + Gero ikusi + Desaktibu hasieran berreskuratzea + Ireki Shorts honi + Shortsatzailea + Jokalari ohiko + Jokalari ohiko pantaila osoan + Shorts automatikoak + Hurrengo Short-era pasatzen da automatikoki, egungokoa errepikatu beharrean + Autojokoa atzealdean + Hurrengo Short-era pasatzen da automatikoki atzealdean erreproduzitzen ari den bitartean + Saltatu Shorts zuzenekoak + Shorts feeden agertzen diren zuzeneko emisioak salto egiten ditu + Erregenetan erreproduztea + Beti aktibatu + Wi-Fi soilik + Desaktibatu + Berakututu izenburuko originalak + Bideo-izen originalak, bideo-deskribapenak eta kanal-deskribapenak erakusten ditu automatikoki itzulituek ordez + Izenburua kargatzen... + Desaktibu feed-aren automatiko refresgatzea + Feed-ak zaharkatu ondoren automatikoki refresgatzea ekiditen du + Feed-eko txartelen diseinua + Klasikoa + Txartelak + Atsegin, iruzkin eta gorde botoiek dituzten txartelak + Komentarioak + Desgaitu miniplayerra berraktzatzea + Desaktibatzen du \'Jarraitu ikusten\' miniplayer-a, app-a hasterean agertzen den YouTube Premium ezaugarri bat + Mini-jokalaria + Personalizatu bideo pantaila txikiagoaren itxura eta jokapena + Mini-jokalariaren mota + Ezkutatu izenburua eta kanala + Desaktibatuta + Lehenesteko + Minimalista + Minimalista 2 + Tablet + Modernoa 1 + Modernoa 2 + Modernoa 3 + Moderno 4 + Desaktibu eskinak birbilduz + Desaktibu arrastatu eta utzi + Pantailaren eskinetan pantaila txikiagoa arrastatzeko aukera desaktivatzen du + Desaktibu arrastatu horizontalaren jesチュア + Miniplayerra pantailatik kanpo horizontalki arrastatuz itxiすることa saihesten du + Gelditu erreprodukzioa horizontalki arrastatuz + Erregengatzea geldi egongo da mini-playera pantailatik kanpora arrastatu zuenean + Gogoratu arrastatu egoerako posizioa + Mini-playera pantailatik kanpo arrastatzeak bere UI egoera gordetuko du + Ezestu gainjarianeko botoiak + Lehenetsatutako neurria + Sagaratu pantailako hasierako neurria, pixeletan + Pixel neurriak %1$s eta %2$s artean egon behar dute + Gainjariko opazitatea + Doitu transparenzia maila (0en garduna, 100en opakea) + Atrezari botoiak feed-etik ateratzen du + Atrezari sakatzeak beti aplikazioa itxitzen du feed-eko goialdera scroll egiteko ordez + Animazioak behartu + Lehenxekoa + Beti aktiboa + Betia desaktiboa + Itsasbarra ezkutatu + Ezkutatzen du sistemaren egoera-barra. Goiko ertzatik behera irrastun egin füru batzuek agertzeko + Status-barrak goiko barrak estaltzen dituen arazoa konpondu + Status-barrak layout koherentea mantentzen du, goiko barrak estaltu ez ditzan eta playera gaindik hutsik ez utzi. Desaktibu aukera honek aplikazioa crashes-era eramaten badu edo goiko barrak gaizki agertzen badira + Tema argiaren atzeondoa + Zuria + Material You (Zuria) + Hurre pink argia + Urdin argia + Berde argia + Hori argia + Urrutxul argia + Gorri argia + Atzeondo argia pertsonalizatua + Ezarri gai argiaren atzeondo kolorea + Aldatu lehenplaneko kolorea + ErabiliGaia kolorea testu eta ikonoentzat + Aktibu gradientuko karga-pantaila + Karga-pantailan gradientu fondo bat erakusten du kolore solidobatzaren ordez + Aktibu bilatzaile-barra pertsonalizatu kolorea + Seekbarren kolorea + Katu bideoaren progresbarrako kolorat primaxoa + Seekbar-ren kolore accent-a + Katu bideoaren progresbarrako kolore sekundarioa + Seekbar-ren kolore-balioa ez da baliagarria + Header-ren logotipoa + Lehenetsua + Ohiko + Kustomizatua + Irudien eskualde-mugak gainditu + Miniaturaren eskaerak zerbitzari alternatibora bideratzen ditu herrialde erretrictuetan faltan dauden irudiak konpontzeko + DeArrow izenburuak + Home fitxa + Izakبتsak fitxa + Zu fitxa + Sekoereko zerrendak & gomendapenak + Bilaketa emaitzak + DeArrow ikona erakutsi + DeArrow ikona erakusten du crowdsourced bideo tituluetan + Modu kasuala + DeArrow erabiltzaileek jatorrizko izenburua gustatzen ziela botoatseば, DeArrow izenburutik behintz aurrez jatorrizko izenburua erakusten du + Jatorrizko izenburua erakutzeko gutxieneko boto kasualak + Kategoria bakar bateko, adibidez barregarriak edo sormenari buruzkoak, jatorrizko izenburua erakutzeko behar diren boto kasualen kopurua + DeArrow miniaturak + Miniatura jatorrizkoak + DeArrow & Miniatura jatorrizkoak + DeArrow & Irudi estatikoak + Irudi finkoak + "DeArrow-ek clickbait izenburuak eta miniatureak crowdsourcing bidez lortutako beste batzuek ordezkatzen ditu, zeзуak zehatzagoak eta sensazionalistagoak ez diren. Komunitateak idazten eta bozkatzen ditu izenburuak, eta miniatureak bideoa khudatik atera diren frameak dira + +Bideoak bideo-ID hashed-ko prefixo labur baten bidez eskatzen dira, beraz, zerbitzarrak ez daki zehazki zein bideo ari zaren ikusten. Bideo batek DeArrow miniaturea ez badu, jatorrizkoa edo irudi estatikoa erakutsiko da" + Ikusi konexio-erroen alertak + Jakinarazpena erakusten du DeArrow zerbitzaria eskuragarri ez denean + API endpoint-a + DeArrow-ren thumbnail cache endpoint-en URLa + Bideo-irudi finkoak + Irudi estatikoak bakoitzaren hasieratik / erdian / amaieran hartzen dira. Irudi hauek YouTube-n integratuta daude eta ez da API kanpora bat erabiltzen + Atera bideo-frame-a honetatik + Hasi + Erdian + Amaiera + DeArrow batzuetan ez dago eskuragarri (%s kodea) + DeArrow batzuetan ez dago eskuragarri + Loreztatu seinalearen botoi estilo zaharra + Bideo-seiatzailearen kontrolak boldoak lehenagoko estilo bisual meheragoara itzultzen ditu + Gailuaren dimentsioak imitatzea + Hau aktibatzeak bideo-kalitate altuagoak askatu ditzake + Hau aktibatzeak bideoen erreprodukzioan jolten egitea, bateriaren iraupena txikutzea eta efektu lateral ezagunak eragin ditzake + Haptic feedback-a + Zein elkarrekintzak aktibatu behar duten gailuaren bibrazioa aukeratu + Kapituluen haptikoak desaktibatu + Bilaketa zehatzaren haptikoak desaktibatu + Bilaketa undo egiteko haptikoak desaktibatu + Desaktibu sakatu eta eutseko haptika + Zoom-haptikoak desaktibatu + Zure kontuaren saioa hasten duten datuak berriki aldatu badituzu, uninstall eta instalatu berri MicroG + Saltatu esteka-birredirekzioak + Ireki jatorrizko esteka zuzenean, jarraipen-esteka (youtube.com/redirect) saltatuz + Estekak aialditzailean ireki + Estekak aplikazioaren barruko nabigatzailearen ordez kanpoko nabigatzaile batean irekitzen ditu + Erregengapen-bufferren neurria + Lehenagoa + Txikia + Erdibidea + Gehiagoa + Bideo atzerapena + Bideoa atzeratzen du audio lagunetsuarekin sinkronizatuta mantentzeko, Bluetooth headphones bezalako kasuetan (0-5000 ms) + Bluetooth audioarekin soilik atzera + Bideoa Bluetooth audioa konektatuta dagoen bitean soilik atzeratzen du, telefonoaren hautspaildia sinkronizatuta geratzaiteko + Erakutsi bideoatzeko berotze botoia + Sakatu eta eutsi berotzea 0 ms-ra berresetatzeko + Erakutsi bideoatzeko berotze menua + Berotzea milisegundotan + Automatikoki + Gogoratu bideo-kalitatea + Zure kalitate-aukerapen manuala etorkizuneko bideo guztiei aplikatzen die + Kalitate-aldaketak jakinaraztea + Kalitate default berri bat gorde duten bitartean jakinarazpen bat erakusten du + Bideo-kalitate lehenetsua Wi-Fi sarean + Bideo-kalitate lehenetsua mugikorraren sarean + Gogoratu Shorts-en kalitatea + Zure kalitate eskortze manuela etorkizuneko Shorts guztiei aplikatzen dizkio + Shorts-en kalitate lehenetsua Wi-Fi sarean + Shorts-en kalitate lehenetsua sare mugikorrean + Kalanitate lehenetsua aldatu da mugikorrarentzat: %s + Kalitate lehenetsua aldatu da Wi-Firarentzat: %s + Shorts-en mugikorreko kalitatea aldatu da: %s + Shorts-en Wi-Fi kalitatea aldatu da: %s + Erakutsi Erregengatze-abiaduzko botoia + Sakatu eta eutsi abiadia lehenageroa izaten jartzeko + Erakutsi Kalitateko botoia + Sakatu eta eutsi kalitatea lehenageroa izaten jartzeko + Erakutsi erresoluzioa Kalitate botoian + Erakutsi erresoluzioa, adibidez 1080, FHD bezalako etiketa baten ordez + Erregengatze-abiadura pertsonalizatuaren menua + Berakutsi erregengatze-abiadura menua zaharra + Erregengatze-abiaduren konfigurazio pertsonalizatua + Sartu zure preferred erregengatze-abiaduzko aukera + Kupatutako erreprodukzio-abiadak %s baino txikiagoak izan behar dira + Erregengatze-abiaduren konfigurazio pertsonalizatua ez da baliagarria + Automatikoa + Sakatu eta eutsi erregengatze-abiadua + K مقررatu abiadura-multiplikatzaia pantaila sakatu eta eutsi zuenean; 0-koa jartu desaktibatzeko + Gogoratu erreprodukzio-abiadura + Zure abiadura-hautaketa eskualizatuak etorkizuneko bideo guztiei aplikatzen ditu + Abiadura-aldaketa jakinarazpenak + Erregengatze-abiadura default berri bat gorde duten bitartean jakinarazpen bat erakusten du + Erregengatze-abiadura lehenetsua + Shorts-en playback abiadura lehenetsua + Gogoratu Shorts-en erreprodukzio-abiadura + Zure Shorts-en abiadura eskualda selectiona etorkizuneko Shorts guztiei aplikatzen dizkie. Desaktibatuta badago, abiadura lehenagertzen da Shorts jedatzailea itxilcutean + Erakutsi erreprodukzio-abiadusa Shorts menuan + Erreprodukzio-abiadusa item bat gehitzen du Shorts player menura + Erreprodukzio-abiadusa · %s + Lehenetsuatze okreko erreprodukzio-abiada aldatuta: %s + Erabili abiadura normala musikarentzat + Musika-bideoak beti 1.0x abiaduraan erreproduzi + Kanalen abiaduraren zuri-zerrenda + Zure zerrendan dauden kanalen bideoak beti 1.0x abiaduraan joko dira + Kanala abiadura zerrendara gehitu + Kanala abiadura zerrendatik kendu + Gaitu audio-tonu kontrolak + Lehentasun audio-tonu erreprodukzioa + Lehentasun audio-tonua %s-ra aldatu da + Audio-denboraren luzapena + Abiadura eta tonua modu independentean ezartzea ahalbidetzen du, audio-artefaktoak sortu ditzake + Gogoratu zuzeneko emisioen erreprodukzio posizioa + Zuzeneko emisio martxan daudenetan, zure erreprodukzio posizioa gogoratzen du eta berreskuratzen du, gaur egungo zuzeneko posizioara joateko ordez + Ezaugarri honek ez dion funtzionatzen 24/7 zuzeneko emisioei eta \'beti aktibo\' dauden beste emisioei + Desaktibu HDR bideoa + Force HDR bideoa + Aplikazioari derrigorrez ere gailu hau HDR-gaiarria dela aitertarazten dio + "Erabili hau HDR dekodatzea funtzionatzen badu baina YouTube-k ez badu HDR eskaintzen, HLG ezkutatzen duten HyperOS gailu batzuen kasen bezalakoan. + +Mugaak: +• Koloreak gaizki agertu daitezke pantailak HDR erakutsi ezin badu +• Erregengatzea huts egin dezake dekodatzaileak HDR kudeatu ezin badu +• HDR-ak oraindik bideo-fluxuen spoof-egiteko klientetik depende du" + Bideo kodeka + Lehenagikoa + AVC behartzeko, alda \'Spoof video streams\' \'Android Studio\', \'Android VR\' edo \'visionOS\' izan + "VP9 +• Bateriako bizitza hobetu eta erreprodukzioaren stutteringa konpondu sakiona +• AV1 baino datu internetzatarako gehiago erabiltzen ditu + +AVC (H.264) +• 'Android Studio', 'Android VR', edo 'visionOS' bideo-fluxu klienatza imitatzea behar du +• Bateriako bizitza nahiko hobetu sakiona +• Gailu zaharkituetan faltan dauden bideo-rezoluzioak berreskuratu sakiona +• Gehieneko rezoluzioa 1080p da +• HDR bideoek AVC erabili behaz ez dute +• Gailu batzuek AVC behartu nahi dute" + Ireki kalitate aurreratuko menua + Kalitate hautaketa menua saltatzen du eta Kalitate Aurreratua menua zuzeneanIrekitzen du + Ezkutatu Premium kalitate aukerak + Lehenetsi bideoaren kalitatea + Esaugarri den bideo-kalitatezat korektuen kodekaren eraginkortasunaren gainetik lehentasuna ematen dio + Hau desaktibatuta badago, bideo zaharrek agian ez dute kalitate-auzak erakutsiko + \'Android VR\' / visionOS AV1 gaitзиtu + \'Android VR\' / visionOS bezeroa erabiltzean, software AV1 dekodaketa baimentzen du + "Kovigurazio hau aktibatzeak software AV1 dekodakizapena erabili dezake. + +AV1-ekin bideoen erreprodukzioa jolten badau edo frame-ak galdu ditzake" + Spoofing-en efektu segundarioak + • Erreprodukzioa huts egin dezake 1 minutuan + • \'Audio Track\' menua falta da + • Ez dago AV1 bideo-kodekik + • 360° VR murgu modua ez dago eskuragarri + • Ez dago bolumen egonkorra eskuragarri + • Erreginea 1 segundo lehenago amaitu daiteke + • Deobfuskazioa huts egiten badu, erreprodukzioa huts egin dezake + • Ez dago audio originala behartzea eskuragarri + • Android VR-en saioa hastea beharrezkoa da + • Kalitatea 720p-ra mugitu daiteke + Playeren JavaScript bertsioa + Erakutsi imitazioa \'Stats for nerds\'-en + Bideo-ekintza barra zaharra berreskuratu + "Bideo-ekintza botoiak ezkutatu ezin izateko konponkortza + +Muga: 'Bideo gehiago' overlay zaharkundean itzultzen da" + Atarijoko ikonoen estiloa + Automatikoa + Kustomizatua + Shorts ikonoen estiloa + Sailen Jarraipena + Jarraitu playlistak sail gisa eta gordetu zure kokapenik + Gaitu sailen jarraipena + Sailen orrialdea gehitzen du Historiara eta sailak jarraitzeko aukera bat playlistei + Erakutsi Serieak nabigazio barrahian + Erakutsi Sailen Jarraipeneko botoia + Botoi bat gehitzen du play-atzean seriea jarraitzeko edo irekitzeko + Grabatu jarraitu dituen serieen progresoa + Grabatu erreprodukzioa telefono honetan gordetako serieen eta markadorenen kasuan + Batalde + Gorde + Gorde markadorea + Seriearen izena + Seriearen aukerak + Liburutegiko aukerak + Episodioak eguneratu + Izena aldatu + Ireki zerrenda YouTube-en + Kendu liburutegitik + Zerrendaren esteka edo IDa + Garbitu guzten progreso lokala + Garbitu historia + Guzten progreso lokala garbitu? + Honek posizioak, ikusitako overriding-ak eta jarraipen aukerak garbitzen ditu bideo bakoitzeko. Gorde eta katalogoak mantentzen dira. Erreprodukzio berriak grabatuko dira grabaketa aktibatuta badago + Markatu ikusita + Markatu ikusigabe + Erabili automatiko egoera + Berrarizi 0:00tik + Erreginak playback 0:00tik + Egoera gordea da + Saiatu berri gorde + Aurreko 100 + Hurrengo 100 + Ireki gordeko bideoa + Tokiko progresa garbituta + Ireki Zureko Historia zure sailak ikusteko + Ikusteko historia + Sailak + Gorde diren sailak kargatzen... + Jarraitu zerrenda bat bere episodiek kontrolatzeko + Playlist-eko marka + Episodioak eguneratzen... + Serie guztiak + Ikusia + Episodioak kargatzen ari dira… + Ezaugarri gehiago + "Grabatu progresua gorde dituzun sailetan eta markatutako bideoetan soilik + +Liburutegia hau gailu honetan gordetzen da eta YouTube kontuen artean partekatzen da. YouTube’ren historia pausedu edo garbitu kontrolak ez dute hori aldatzen. Ezabatzeko, erabili Sailak → Gehiago → Garbitu progresu lokala + +Grabaketa automatikoa gelditu egiten da inkognito moduan, kontuen identitatea eskuragarri ez denean edo kontu ezberdin batean aldatzen zuenean. Aktibatu berriro jarraipena kontu horrekin erabiltzeko" + Jarraipena ezin da aktibatu inkognito moduan edo YouTube-ek kontu egungo egoera kargatu aurretik. Itzali Home-ra eta saiatu berriro. Ez da saioa hasi beharko + Gaitu jarraipen lokala + Aurrerapen grabaketa tx tắt dago + Jarraitu %1$s-en + %1$s / %2$s episodioak ikusia + %1$s. Episodioa · %2$s + %3$s-ko %1$s-%2$s episodioak + Aktualizatu episodioak kargatzen saiatzeko. Zerrenda pribatuek kontuaren sarbidea behar dute; hauen marka YouTube-en irekitzen jarraitzen da + YouTube-ek ezin du episodic hau ireki + YouTube-ek ezin du playlist hau ireki + Aukeratu episodio bat + Eguneratuta + Ez da playable episodorik + Gorde duten bideoa · %1$s + Jarraitu saila + Ikusi saila + Jarraitu + Hemen hasi + Hasi puntua gorde da + Hemen arte ikusia + Episodioak ikusita markatu dira + Utzi + Gaitu + {count, plural, one {1 episodio berri} other {# episodio berri}} + Grabatu progresua gailu honetan + "Zure sailak eta progresua gaildu honetan geratzen dira, YouTube kontu guztien artean partekatuz. YouTube historiako kontrolak ez dituzte aldatzen. Series menutik garbitu progresua. + +Automatiko grabaketa aukeratua da. Inkognito moduan edo kontua aldatzean gelditu egiten da" + Episodioak ezin dira kargatu. Playlista markatze gisa gordezkazu + Gorde gabeko aldaketa gehiegi. Saiatu berriro gordetzen lehen + Gorde prozesuan dago. Saiatu berriro + Episodioen karga prozesuan dago. Saiatu berriro + Biltegiria ez dago eskuragarri. Salbatutako datuak gordetuta utzi dira + Aldaketak ezin izan dira gordetuta. Saiatu berriro gordetzen YouTube itxi aurretik + Sail hau jada gordetuta dago, edo haren zerrenda aldatu da. Ireki berriro + Sail hau aldatu da edo kendu dute. Ireki berriro liburutegia + Zerrenda aldatu da. Hautatu berriro episodioa + Episodio hau ez dago eskuragarri. Aukeratu beste bat + Sartu sail baten izena + Sartu YouTube zerrenda baten esteka edo ID bat + Itxaron mesedez berriz eguneratzea lehen + Episodioak ezin izan dira kargatu. Saiatu berriro geroago + Aldaketa ezin izan da osatu. Saiatu berriro + Jarraipena ezin izan da berretsi. Egiaztatuatzailea eta saiatu berriro + Sailak jarraitzen + Bideo hau ez dago zerrenda honetan + Playlistak bilatzen... + Itsasi playlistaren esteka + Ezin izan da playlistik aurkitu + Ez da episodorik eskuragarri + Ezagutuenak ezkutatu + Markatu aurreko episodak ikusitako gisa + Hasi: %1$s + Episoduen ordena + Playlistaren ordena + Playlistaren ordena alderaka + %1$s bisita + %1$s ikusitzea + Markatu ikusi diren episodoak + Gutxienez %1$s%% ikusia, gehienez 90 segundu bakarrik geratzen direnean, edo %2$s segundu geratzen direnean + Markatu automatikoki episodio bat ikusia tauparen bati iristean. Percentajearen arauak 90 segundutik gutxiago geratzea eskatzen du, episodic luzeen amaiera ez saltzeko. Geratzen den denboraren araua minutu bat baino luzagoak diren bideoei aplikatzen zaie; 0-an jarri desaktibatzeko. Aldaketak etorkizuneko progresu eguneratzeei aplikatzen zaizakizkie. Eskuz markatutako ikusi/ikusi gabe aukerak mantentzen dira + Ikusi den ehunekoa (1-100) + Geratzen diren segunduak (0-300; 0-ak desaktibatzen du) + Sartu %1$s eta %2$s arteko balio bat + Erabili YouTube-ko progresioa + Jarraitu serieak beste gailu batean ikusi ondoren + Irakurri jarraitu dituzun sailen ikusitako egoera eta jarraitze-posizioak egungo YouTube kontuaren bidez. Episode batzuak soilik gordatzen dira telefono honetan. Egiaztapenak serie-biblioteka zabaltzean eta Jarraitu sakatu aurretik egiten dira. Beste gailu batean YouTube historiala aktibatuta izan behar da + YouTube-ko progresioa egiaztatzen... + YouTube progresua eskuragarri ez da. Gordetako progresua erabiltzen da + YouTube progresu batzuk eskuragarri ez dira. Gordetako posizioak mantentzen dira + Bakup eta berakutapen + Gorde zure serieen liburutegia fitxagune batean edo berakutu bakup bat + Jarraitutako sailak, episode-posizioak eta ikusitako aukerak gordetzen ditu. Grabaketa eta YouTube progresuaren baimenak ez daude barne + Gorde bakupa + Berakutu bakupa + %1$s saile gehitu. Zure bibliotekan dagoen %2$s saile mantentzen dira. Bideoen progresu existentea mantentzen da + Serieen bakupa gorde da + %1$s saile berreskuratu dira + Fitxamen hau ez da onartzen den sailen kopia seguru bat, edo handia egon. Zure liburutegia ez da aldatu + Kopia segurua ezin da gordetakoa izan. Saiatu kokapen batean + Saila kenduta + Episodio hau orain behin baino gehiago agertzen da. Zein batetik jarraitu nahi duen aukeratu + Zure gordetako episodioa ez dago jada zerrenda honetan. Zure gordetako posizioa mantentzen da + Jarraitu %1$s · %2$s episodioarekin + Beheko edozein episodio ere aukera dezakezu. Aurreko episodioak ez dira aldatu + Zerrenda hau aldatu da. Ireki saila non jarraitu duen aukeratzeko + Ez dau gehiago episodiorik norabide honetan + Jarraitu %1$s + Bilatu sailak + Garbitu bilaketa + Ez da sailik aurkitu + Izenik gabeko episodioa + Diskurgarri ez den episodioa + Niconico komentarioak + Zirbilak zuzeneko txat eta txat erreprodukzioa bideo zehar mugitzen diren komentario gisa erakutsi + Bideoatzean botoi gainikusi bat erakutsi. Sakatu komentarioak erakusteko edo ezaguteko, eta sakatu eta eutsi konfigurazio azkarra lortzeko + Komentarioen letra-tamana + Komentarioen testu-tamana (12-40 sp) + Komentarioen opazitatea + Komentarioen testuaren eta fondoen opazitatea + Bidaia-iraupena + Segundu zenbatz behar duten komentarioek pantaila zehar mugitzeko. Balio txikiagoek azkarrago mugitzen dute + Iruzkinen luzera maximoa + Honek baino karakterak gehiago dituzten iruzkinak ezkutatzen ditu. Emoji bakoitzak karaktera bat kontatzen du + Mugaik gabe diff --git a/patches/src/main/resources/addresources/values-fa-rIR/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-fa-rIR/shared-youtube/strings.xml index c9eae3ef90..fcf33fc67d 100644 --- a/patches/src/main/resources/addresources/values-fa-rIR/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-fa-rIR/shared-youtube/strings.xml @@ -18,10 +18,12 @@ Second \"item\" text" --> - انیمیشن راه‌اندازی - غیرفعال - رنگی - تک‌رنگ + انیمیشن راه‌اندازی + غیرفعال + رنگی + Morphe رنگی + تک‌رنگ + Morphe تک‌رنگ برای پایان راه‌اندازی، مجدداً راه‌اندازی کنید diff --git a/patches/src/main/resources/addresources/values-fa-rIR/youtube/strings.xml b/patches/src/main/resources/addresources/values-fa-rIR/youtube/strings.xml index 41f0a80e38..8f7741db8a 100644 --- a/patches/src/main/resources/addresources/values-fa-rIR/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-fa-rIR/youtube/strings.xml @@ -837,8 +837,6 @@ Second \"item\" text" هرگز نمایش نده هنگام بی‌صدا نبودن نمایش بده هنگام بی‌صدا بودن نمایش بده - نمایش زیرنویس خودکار - پنهان‌کردن زیرنویس خودکار زبان ترجیحی زیرنویس‌ها ابتدا زیرنویس‌های ارائه‌شده را انتخاب می‌کند، سپس به‌طور خودکار به زبان ترجیحی ترجمه می‌کند @@ -1249,6 +1247,7 @@ Second \"item\" text" رفع مشکل پوشیدن نوار وضعیت بر روی نوارهای بالایی چیدمان نوار وضعیت را یکپارچه نگه می‌دارد، بنابراین بر نوارهای بالایی پوشانده نمی‌شود و فضای خالی در بالای پخش‌کننده باقی نمی‌گذارد. اگر برنامه کرش کرد یا نوارهای بالایی به درستی نمایش داده نشدند، این گزینه را غیرفعال کنید + درخشش پس‌زمینهٔ طرح روشن سفید Material You (سفید) @@ -1271,6 +1270,9 @@ Second \"item\" text" رنگ ثانویهٔ نوار پیشرفت ویدئو را تنظیم کنید مقدار رنگ نوار جست‌وجو نامعتبر است + آنالوگ اصلی + کیک اصلی + فلز اصلی لوگوی سربرگ پیش‌فرض diff --git a/patches/src/main/resources/addresources/values-fi-rFI/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-fi-rFI/shared-youtube/strings.xml index ad135660cc..649ff1a5f0 100644 --- a/patches/src/main/resources/addresources/values-fi-rFI/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-fi-rFI/shared-youtube/strings.xml @@ -18,10 +18,12 @@ Second \"item\" text" --> - Käynnistysanimaatio - Pois käytöstä - Väri - Yksivärinen + Käynnistysanimaatio + Pois käytöstä + Väri + Värillinen Morphe + Monokromi + Monokromi Morphe Käynnistä uudelleen valituksen suorittamiseksi diff --git a/patches/src/main/resources/addresources/values-fi-rFI/youtube/strings.xml b/patches/src/main/resources/addresources/values-fi-rFI/youtube/strings.xml index bf337bf1f4..4605c585f0 100644 --- a/patches/src/main/resources/addresources/values-fi-rFI/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-fi-rFI/youtube/strings.xml @@ -837,8 +837,6 @@ Korjaa tämä ottamalla käyttöön 'Väärennä videovirrat' ja vaihtamalla 'Ol Älä näytä koskaan Ota tekstityskeksit käyttöön Näytä, kun mykistetty - Näytä automaattiset tekstitykset - Piilota automaattiset tekstitykset Ensisijainen tekstityskieli Valitsee ensin tarjoajan tekstitykset ja kääntää ne sitten automaattisesti ensisijaiselle kielelle @@ -1250,6 +1248,7 @@ Rajoitus: Jos \"Palauta vanha videotoimintopalkki\" on poissa käytöstä, \"Lis Korjaa tilapalkin peittämistä yläpalkkien kanssa Pysäyttää tilapalkin asettelun johdonmukaisuuden, joten se ei peitä yläpalkkeja ja jättää tyhjää tilaa soittimen yläpuolelle. Poista tämä käytöstä, jos sovellus kaatuu tai yläpalkit näyttävät vääriltä + Hehku Vaalean teeman tausta Valkoinen Material You (Valkoinen)() @@ -1272,6 +1271,9 @@ Rajoitus: Jos \"Palauta vanha videotoimintopalkki\" on poissa käytöstä, \"Lis Aseta videon etenemispalkin toissijainen väri Virheellinen etenemispalkin väriarvo + Alkuperäinen analogi + Alkuperäinen kakku + Alkuperäinen metalli Ylätunnisteen logo Oletus diff --git a/patches/src/main/resources/addresources/values-fil-rPH/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-fil-rPH/shared-youtube/strings.xml index d3bf631728..f123208d8c 100644 --- a/patches/src/main/resources/addresources/values-fil-rPH/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-fil-rPH/shared-youtube/strings.xml @@ -18,10 +18,12 @@ Second \"item\" text" --> - Animasyon sa pagsisimula - Nakapatay - Kulay - Monokromo + Animasyon sa pagsisimula + Nakapatay + Kulay + Color Morphe + Monokromo + Monochrome Morphe I-restart upang tapusin ang pag-setup diff --git a/patches/src/main/resources/addresources/values-fil-rPH/youtube/strings.xml b/patches/src/main/resources/addresources/values-fil-rPH/youtube/strings.xml index df849fa2cd..7293c1bc3c 100644 --- a/patches/src/main/resources/addresources/values-fil-rPH/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-fil-rPH/youtube/strings.xml @@ -837,8 +837,6 @@ Para ayusin ito, i-on ang 'I-spoof ang mga video stream' at baguhin ang 'Default Huwag ipakita Ipakita kapag hindi naka-mute Ipakita kapag naka-mute - Ipakita ang auto captions - Itago ang auto captions Ginustong wika ng caption Pinipili muna ang mga subtitle mula sa provider, pagkatapos ay awtomatikong isinasalin ang mga ito sa ginustong wika @@ -1248,6 +1246,7 @@ Limitasyon: Kung ang 'Ibalik ang lumang video action bar' ay naka-off, maaaring Ayusin ang layout ng status bar na sumasakop sa mga itaas na bar Pinapanatiling consistent ang layout ng status bar upang hindi ito sumakop sa mga itaas na bar at walang walang laman na espasyo sa itaas ng player. I-off ito kung crash ang app o magmukha nang mali ang mga itaas na bar + Kislap Likuran ng maliwanag na tema Puti Material You (Puti) @@ -1270,6 +1269,9 @@ Limitasyon: Kung ang 'Ibalik ang lumang video action bar' ay naka-off, maaaring Itakda ang pangalawang kulay ng video progress bar Hindi wastong halaga ng seekbar color + Orihinal na analog + Orihinal na cake + Orihinal na metal Header logo Panimula diff --git a/patches/src/main/resources/addresources/values-fr-rFR/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-fr-rFR/shared-youtube/strings.xml index 29f045b771..8af88523e4 100644 --- a/patches/src/main/resources/addresources/values-fr-rFR/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-fr-rFR/shared-youtube/strings.xml @@ -18,10 +18,12 @@ Second \"item\" text" --> - Animation de démarrage - Désactivé - Coloré - Monochrome + Animation de démarrage + Désactivé + Coloré + Couleur de Morphe + Monochrome + Morphe monochrome Redémarrez pour terminer la configuration diff --git a/patches/src/main/resources/addresources/values-fr-rFR/youtube/strings.xml b/patches/src/main/resources/addresources/values-fr-rFR/youtube/strings.xml index 95e400a4bf..13aadc07f1 100644 --- a/patches/src/main/resources/addresources/values-fr-rFR/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-fr-rFR/youtube/strings.xml @@ -837,8 +837,6 @@ Pour corriger cela, activez 'Falsifier les flux vidéo' et définissez 'Client p Ne jamais afficher Afficher quand non coupé Afficher quand coupé - Afficher les sous-titres automatiques - Masquer les sous-titres automatiques Langue préférée des sous-titres Sélectionne d\'abord les sous-titres fournis, puis les traduit automatiquement dans la langue préférée @@ -1251,6 +1249,7 @@ Limitation : Si 'Restaurer l'ancienne barre d'actions vidéo' est désactivé, l Corriger la barre d\'état qui recouvre les barres supérieures Maintient une mise en page cohérente de la barre d\'état, afin qu\'elle ne masque pas les barres supérieures et ne laisse aucun espace vide au-dessus du lecteur. Désactivez cette option si l\'application plante ou si les barres supérieures s\'affichent incorrectement + Lueur Arrière-plan du thème clair Blanc Material You (Blanc) @@ -1273,6 +1272,9 @@ Limitation : Si 'Restaurer l'ancienne barre d'actions vidéo' est désactivé, l Définir la couleur secondaire de la barre de progression de la vidéo Valeur invalide pour la couleur de la barre de progression + Originale analogique + Originale gâteau + Originale métal Logo d\'en-tête Par défaut diff --git a/patches/src/main/resources/addresources/values-ga-rIE/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-ga-rIE/shared-youtube/strings.xml index 7884390e54..584a49168d 100644 --- a/patches/src/main/resources/addresources/values-ga-rIE/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ga-rIE/shared-youtube/strings.xml @@ -18,10 +18,12 @@ Second \"item\" text" --> - Beochan tosaithe - Díchumasaithe - Dath - Monacrómach + Beochan tosaithe + Díchumasaithe + Dath + Dath Morphe + Monacrómach + Morphe Monacrómach Atosaigh chun an socrú a chríochnú diff --git a/patches/src/main/resources/addresources/values-ga-rIE/youtube/strings.xml b/patches/src/main/resources/addresources/values-ga-rIE/youtube/strings.xml index a9a4147519..d2bfdbb5a8 100644 --- a/patches/src/main/resources/addresources/values-ga-rIE/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ga-rIE/youtube/strings.xml @@ -837,8 +837,6 @@ Chun é seo a shocrú, cuir 'Spoof video streams' ar siúl agus athraigh an 'Cli Ná taispeáin riamh Taispeáin nuair nach bhfuil sé díbhalbhaigh Taispeáin nuair a bhíonn sé balbhaithe - Taispeáin fotheidil uathoibríocha - Folaigh fotheidil uathoibríocha Teanga na bhfo-theideal is fearr leat Roghnaíonn sé fotheidil an tsoláthraí ar dtús, agus ansin aistríonn sé go huathoibríoch iad go dtí an teanga is fearr leat. @@ -1249,6 +1247,7 @@ Teorainn: Mura bhfuil 'Barra gníomhaíochta seanfhíseáin athchóirithe' múch Ceartaigh an barra stádais a bhíonn ag clúdach na mbarraí uachtaracha Coinníonn sé leagan amach an bharra stádais comhsheasmhach, ionas nach gclúdaíonn sé na barraí uachtaracha agus nach bhfágann sé spás folamh os cionn an imreora. Múch an rogha seo má thuairteálann an aip nó má bhíonn cuma mhícheart ar na barraí uachtaracha. + Breo Cúlra téama éadrom Bán Material You (Bán) @@ -1271,6 +1270,9 @@ Teorainn: Mura bhfuil 'Barra gníomhaíochta seanfhíseáin athchóirithe' múch Socraigh an dath tánaisteach den bharra dul chun cinn físe Luach datha barra cuardaigh neamhbhailí + Anallóg Bunaidh + Cáca bunaidh + Miatal bunaidh Lógó ceanntásca Réamhshocrú diff --git a/patches/src/main/resources/addresources/values-gl-rES/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-gl-rES/shared-youtube/strings.xml index e3d041945a..5f957ff04d 100644 --- a/patches/src/main/resources/addresources/values-gl-rES/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-gl-rES/shared-youtube/strings.xml @@ -18,7 +18,7 @@ Second \"item\" text" --> - Desactivado + Desactivado diff --git a/patches/src/main/resources/addresources/values-hi-rIN/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-hi-rIN/shared-youtube/strings.xml index 875c97e266..2bf11fc6ec 100644 --- a/patches/src/main/resources/addresources/values-hi-rIN/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-hi-rIN/shared-youtube/strings.xml @@ -18,10 +18,12 @@ Second \"item\" text" --> - स्टार्टअप एनीमेशन - बंद किया गया - रंगीन - एक रंग का + स्टार्टअप एनीमेशन + बंद किया गया + रंगीन + कलर Morphe + एक रंग का + मोनोक्रोम Morphe सेटिंग को पूरा करने के लिए रीस्टार्ट करें diff --git a/patches/src/main/resources/addresources/values-hi-rIN/youtube/strings.xml b/patches/src/main/resources/addresources/values-hi-rIN/youtube/strings.xml index 6913205120..1f6b727c1a 100644 --- a/patches/src/main/resources/addresources/values-hi-rIN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-hi-rIN/youtube/strings.xml @@ -837,8 +837,6 @@ OpenRouter.ai खोलने के लिए यहां टैप करे कभी भी नहीं दिखाएं जब अनम्यूट पर दिखाएं म्यूट किए हुए दिखाएं - ऑटो कैप्शंस दिखाएं - ऑटो कैप्शंस छुपाएं पसंदीदा कैप्शन भाषा पहले प्रदाता उपशीर्षक चुनता है, फिर स्वतः पसंदीदा भाषा में अनुवाद करता है @@ -1250,6 +1248,7 @@ OpenRouter.ai खोलने के लिए यहां टैप करे स्टेटस बार को टॉप बार्स को कवर करना ठीक करें स्टेटस बार की लेआउट को स्थिर रखता है, ताकि यह टॉप बार्स को न कवर करे और प्लेयर के ऊपर खाली जगह न छोड़े। अगर ऐप क्रैश हो जाए या टॉप बार्स गलत दिखें तो इसे बंद करें + ग्लो लाइट थीम की पृष्ठभूमि सफेद मटेरियल यू (सफेद) @@ -1272,6 +1271,9 @@ OpenRouter.ai खोलने के लिए यहां टैप करे वीडियो प्रोग्रेस बार का माध्यमिक रंग सेट करें अमान्य सर्च बार रंग मूल्य + ओरिजनल एनालॉग + ओरिजिनल केक + ओरिजिनल मेटल हेडर लोगो डिफ़ॉल्ट diff --git a/patches/src/main/resources/addresources/values-hr-rHR/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-hr-rHR/shared-youtube/strings.xml index d46cc7075a..82dff48f8f 100644 --- a/patches/src/main/resources/addresources/values-hr-rHR/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-hr-rHR/shared-youtube/strings.xml @@ -18,10 +18,12 @@ Second \"item\" text" --> - Animacija pokretanja - Onemogućeno - Boja - Monokromatski + Animacija pokretanja + Onemogućeno + Boja + Kolorna Morphe + Monokromatski + Monokromatska Morphe Ponovno pokrenite da biste dovršili postavljanje diff --git a/patches/src/main/resources/addresources/values-hr-rHR/youtube/strings.xml b/patches/src/main/resources/addresources/values-hr-rHR/youtube/strings.xml index 6cff344a04..75989c399a 100644 --- a/patches/src/main/resources/addresources/values-hr-rHR/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-hr-rHR/youtube/strings.xml @@ -827,8 +827,6 @@ Da biste to popravili, uključite \"Lažiranje video streamova\" i promijenite \ Nikada ne prikazuj Prikaži kada zvuk nije utišan Prikaži kada je mutirano - Prikaži automatske podnaslove - Sakrij automatske podnaslove Preferirani jezik titlova Prvo odabire titlove pružatelja, a zatim ih automatski prevodi na preferirani jezik @@ -1238,6 +1236,7 @@ Ograničenje: Ako je 'Vrati staru traku s radnjama za videozapise' isključeno, Ispravi statusnu traku koja prekriva gornje trake Održava dosljednost rasporeda statusne trake, tako da ne prekriva gornje trake i ne ostavlja prazan prostor iznad playera. Isključite ovo ako aplikacija zapne ili gornje trake izgledaju pogrešno + Sjaj Boja pozadine u svijetloj temi Bijela Material You (Bijela) @@ -1260,6 +1259,9 @@ Ograničenje: Ako je 'Vrati staru traku s radnjama za videozapise' isključeno, Postavite sekundarnu boju trake napretka videozapisa Nevažeća vrijednost obojskog seekbara + Originalni analog + Originalna torta + Originalni metal Zastavica Zadano diff --git a/patches/src/main/resources/addresources/values-hu-rHU/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-hu-rHU/shared-youtube/strings.xml index 0d216e9d07..80249e17e1 100644 --- a/patches/src/main/resources/addresources/values-hu-rHU/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-hu-rHU/shared-youtube/strings.xml @@ -18,10 +18,12 @@ Second \"item\" text" --> - Indítási animáció - Kikapcsolva - Színes - Monokróm + Indítási animáció + Kikapcsolva + Színes + Színes Morphe + Monokróm + Monokróm Morphe Indíts újra a beállítások befejezéséhez diff --git a/patches/src/main/resources/addresources/values-hu-rHU/youtube/strings.xml b/patches/src/main/resources/addresources/values-hu-rHU/youtube/strings.xml index 7b38cd8274..88e5465935 100644 --- a/patches/src/main/resources/addresources/values-hu-rHU/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-hu-rHU/youtube/strings.xml @@ -837,8 +837,6 @@ A javításhoz kapcsolja be a „Videostreamek meghamisítása” beállítást, Soha ne mutassa Megjelenítés, ha a hang ki van némítva Mutoltáskor mutat - Mutatja az automatikus feliratokat - Rejtje az automatikus feliratokat Feliratok előnyben részesített nyelve Először a szolgáltató feliratait választja ki, majd automatikusan lefordítja az előnyben részesített nyelvre @@ -1250,6 +1248,7 @@ Korlátozás: Ha a „Régi videós eszköztár visszaállítása” ki van kapc Állítsa be, hogy az állapotsáv ne takarja ki a felső sávokat A status sáv elrendezése konzisztens marad, így nem takarja le a felső sávokat, és nincs üres hely a lejátszó felett. Kapcsolja ki ezt, ha az alkalmazás összeomlik vagy a felső sávok rossz megjelenésűek + Ragyogás Világos téma háttér Fehér Material You (Fehér) @@ -1272,6 +1271,9 @@ Korlátozás: Ha a „Régi videós eszköztár visszaállítása” ki van kapc Állítsa be a videó lejátszás jelzője másodlagos színét Érvénytelen keresősáv színértéke + Eredeti analóg + Eredeti sütemény + Eredeti fém Fejléclogó Alapértelmezett diff --git a/patches/src/main/resources/addresources/values-hy-rAM/music/strings.xml b/patches/src/main/resources/addresources/values-hy-rAM/music/strings.xml index ccfea4ed71..37297b6f2c 100644 --- a/patches/src/main/resources/addresources/values-hy-rAM/music/strings.xml +++ b/patches/src/main/resources/addresources/values-hy-rAM/music/strings.xml @@ -17,58 +17,675 @@ Second \"item\" text" --> + Վերջին նվագվածը + Բաժանորդագրություններ + Տեսանյութեր + Այլ արդյունքներ + Վերբեռնված երգեր + Վերբեռնված ալբոմներ + Վերբեռնված կատարողներ + Թարմացրեք հոսթի Jam փաչը՝ դադարեցման և վերսկսման ֆունկցիաներն օգտագործելու համար + Jam հերթի կիսում + Ստեղծեք կամ միացեք մոտակա Jam-ին և խմբագրեք դրա հերթը Jam Layer-ի միջոցով + Միացրեք Jam հերթի կիսումը + Միացնում է Jam ինտեգրումը YouTube Music-ի վերագործարկումից հետո + Ներբեռնեք Jam Layer-ը աշխատանքի սկսելու համար + Բացել Jam Layer-ի վերջին թողարկումը ձեր բրաուզերում + Jam-ի կառավարման գործիքները + Հյուրընկալել կամ միանալ մոտակա Jam-ին և կառավարել սեսիան + Jam Layer փաթեթ + Ընտրեք տեղադրված ուղեկից հավելվածը, որն օգտագործվում է մոտակա հերթի බෙදාժխտման համար + Օգտագործել տեղադրված ուղեկից փաթեթը: Զուգակցումը պահանջում է ձեր հաստատումը և տեղային հնարավորությունների տոկեն + Միացնե՞լ Wi-Fi-ն։ + Միացրեք Wi-Fi-ն, ապա փորձեք նորից սկսել կամ միանալ Jam-ին + Բացել Wi-Fi կարգավորումները + Չեղարկել + Օգտագործել փաթեթը + Մուտքագրեք Android փաթեթի գործող անվանում + Փաթեթը պահպանված է: Միացնելու համար կարգավորեք Jam Layer-ը + Jam-ի կապը խզված է + Նախ միացրեք Jam Layer-ը + Միացրեք Jam հերթի կիսումը կարգավորումներում, ապա վերագործարկեք YouTube Music-ը + Նախ տեղադրեք ընտրված Jam Layer փաթեթը + Մուտքագրեք հոստի վրա ցուցադրված կոդը: Wi-Fi Aware-ը կարող է միացնել մոտակա սարքերը, նույնիսկ եթե նրանց LAN-երը տարբեր են + Միացեք կոդով + Միանալ + Սկանավորել QR-ը + Մուտքագրեք ութնիշանոց կոդը կամ տեղադրեք հրավերը + Միացեք ձեր Jam-ին + Պատճենել կոդը + Jam-ի կոդ + Ցույց տալ QR-ը + Պատրաստ է + Հրավիրեք ձեր Jam-ին + Սկանավորեք՝ միանալու և խմբագրելու այս հերթը: Jam-ի ավարտը չեղարկում է հրավերը + Պատճենել հղումը + Jam-ի հրավեր + Jam-ը վերակցվում է. փորձեք նորից մի փոքր ուշ + Ավելացված է Jam-ին + Jam-ը հասանելի չէ. տրեկը չավելացվեց + Ձեր Jam-ը + Լսում ենք միասին + Միանում ենք Jam-ին + Լսել միասին + Թարմացում Jam-ը… + Փնտրում ենք ձեր հոսթը Wi-Fi-ում… + Սպասում ենք մարդկանց + %s մարդ միացավ + %s մարդ միացան + Մուտքագրեք այս կոդը մոտակայ սարքի վրա: Wi-Fi Aware-ը աշխատում է առանձին տեղային ցանցերի միջև, եթե այն աջակցվում է: Ժամկետը լրանում է %s րոպեի ընթացքում + Տեղափոխվել %s-ի? + Խմբագրումները բաց են + Խմբագրումները արգելափակված են + Միացված է Wi-Fi Aware-ի միջոցով + Միացված է Wi-Fi-ի միջոցով + Միացված է Bluetooth-ի միջոցով + Չի աջակցվում Jam-ի ժամանակ + Միացումը չեղարկվեց + Վերամիացում ձեր հոսթին… + Կիսվեք հերթով: Թույլ տվեք բոլորին ավելացնել երգեր + Հրավիրել մարդկանց + Հյուրերը կարող են խմբագրել հերթը + Ավարտել Jam-ը + Հոսթը միացնում է երաժշտությունը: Ավելացրեք, տեղափոխեք կամ հեռացրեք երգերը անմիջապես YouTube Music-ի հերթում + Պահեք երկու սարքերն էլ մոտ: Դուք կարող եք չեղարկել, մինչ մենք փնտրում ենք հոսթին + Չեղարկել միացումը + Լքել Jam-ը + Սկսել Jam + Կարգավորել Jam Layer-ը + Կառավարել + Մանրամասներ + Բացել + Բեռնվում են երգի տարբերակները... + Երգի տարբերակները հասանելի չեն. փորձեք նորից + Չհաջողվեց բացել երգի տարբերակները + Jam Layer-ը ներբեռնելու համար բրաուզեր հասանելի չէ + Սպասում ենք հոսթի հերթին + Նախ բացեք երգը YouTube Music-ում + Նվագարկել հոսթի վրա + Դուրս եկեք Jam-ից և նվագեք տեղում + Նվագել հաջորդը Jam-ում + Ավելացնել Jam-ի հերթի մեջ + Նվագել Jam-ի այս թրեքը + Ընտրեք, թե որտեղ նվագել + Տեղափոխել հոսթին այստեղ + Երգը պատրաստ չէ փոխադրման համար + Հերթը փոփոխվել է, փորձեք ժեստը նորից + Վերնագրի լոգոն + Լռելյայն + Անհատական + Նվագարկել ալբոմի աուդիո թրեքները + Նվագարկել երգերի ալբոմի միայն աուդիո տարբերակը՝ երաժշտական տեսահոլովակի փոխարեն: Պահանջվում է \'Spoof video streams\' ֆունկցիան + Միացնել աուդիո/վիդեո փոխարկիչը + Փորձեք փոխել աուդիո տրեկի տարբերակը և պաշտոնական երաժշտական տեսահոլովակը + Ներբեռնեք երաժշտական տեսահոլովակներ, փոդքասթներ կամ երգեր երրորդ կողմի ներբեռնման հավելվածների միջոցով + Օգտագործել արտաքին ներբեռնիչ «Ներբեռնում» մենյուի համար + Մենյուների «Ներբեռնում» տարբերակը փոխարենը կբացի ձեր արտաքին ներբեռնիչը + Ներբեռնումներ + "Ներբեռնումներ չկան +Ներբեռնված թրեկները կհայտնվեն այստեղ" + %1$s • %2$s • %3$s ՄԲ + Որոնել ներբեռնումների մեջ + Տեսակավորել + Տեսակավորել ըստ կատարողի + Տեսակավորել ըստ վերնագրի + Տեսակավորել ըստ չափսի + Ջնջել բոլոր ներբեռնումները + Ջնջե՞լ բոլոր ներբեռնումները + Բոլոր ներբեռնված թրեքները կջնջվեն այս սարքից + Ներբեռնումները ջնջված են + Ոչինչ չի գտնվել + Գործողություններ %s-ի համար + Նվագարկել + Նվագարկել կամ դադարեցնել + Նախորդ + Հաջորդ + Դադարեցնել + Փակել + Ջնջել + Ջնջել ներբեռնումը + Ջնջե՞լ ներբեռնումը։ + %s-ը կջնջվի այս սարքից + Ներբեռնումը ջնջված է + Չհաջողվեց ջնջել ներբեռնումը + Չհաջողվեց նվագարկել ներբեռնումը + Աուդիո ներբեռնումներ + Օֆլայն վերարտադրում + Չհաջողվեց նույնականացնել ներբեռնվող թրեքը + Ներբեռնումն արդեն ընթացքի մեջ է + Արդեն ներբեռնված է + Ներբեռնումը սկսված է + Ներբեռնումը սկսվում է… + Ներբեռնվում է %s%% + Ներբեռնումն ավարտված է + Ներբեռնումը ձախողվեց + Morphe-ի մասին + Գովազդներ + Լրահոս + Ընդհանուր + Նվագարկիչ + Բառեր + Տարբեր + YouTube Music-ի լեզուն + Համակարգի լեզուն + Պահանջվում է Android 13 կամ ավելի նոր տարբերակ + Թաքցնել \"Ձեռքբերեք Music Premium\" պիտակը + Թաքցնել Music Premium գովազդները + Թաքցնել տեսանյութերի գովազդները + Թաքցնել Աուդիո / Վիդեո փոխանցիչը + Թաքցնել «Ներբեռնել» կոճակը + Թաքցնում է «Ներբեռնել» կոճակը պոդկաստ ծրագրում + Թաքցնել «Բացահայտել» դարակը + Թաքցնել զտիչների տողը + Թաքցնել ցանցային դարակները + Թաքցնել հորիզոնական դարակները + Թաքցնել ցուցակային դարակները + Տեքստերի վահանակ + Թաքցնել Կիսվելու կոճակը + Թաքցնել Թարգմանելու կոճակը + Թաքցնել «Նորություններ» դարակը + Թաքցնել плейլիստերի դարակները + Թաքցնել \"Կրկնել\" կոճակը + Թաքցնել \"Խառնել\" կոճակը + Թաքցնել \"Արագ հավաքման\" դարակը + Մեկնաբանություններ + Ընտրեք, թե որ բաղադրիչները հայտնվեն մեկնաբանությունների բաժնում + Թաքցնել համայնքի ուղեցույցները + Թաքցնել համատեքստի հաղորդագրությունը + Թաքցնել էմոջիների կոճակը + Թաքցնել տեղեկատվության կոճակը + Թաքցնել ժամանակային նշման կոճակը + Crossfade + Հարթ կերպով միացրեք տրեկները միմյանց + Միացնել crossfade-ը + Մարման կորը + Հավասար հզորություն + Զգուշացված պահում + Մեղմ անցում + Հարթ S-կոր + Ելքային + Մուտքային + Անցումային տևողություն + 250 միլիվայրկյան + 500 միլիվայրկյան + 750 միլիվայրկյան + {one, 1 վայրկյան, other, {count} վայրկյան} + 2 վայրկյան + 3 վայրկյան + 4 վայրկյան + 5 վայրկյան + 6 վայրկյան + 7 վայրկյան + 8 վայրկյան + 9 վայրկյան + 10 վայրկյան + 11 վայրկյան + 12 վայրկյան + Crossfade-ի միացում ձեռքով բաց թողնելիս + Կիրառում է crossfade էֆեկտը, երբ դուք ձեռքով անցնում եք հաջորդ տրեկին + Crossfade-ի միացում ավտոմատ անցման ժամանակ + Կիրառում է crossfade էֆեկտը, երբ տրեկն ավարտվում է բնական ճանապարհով + Սեսիայի կառավարում + Սեղմեք shuffle կոճակը երկար՝ crossfade-ը դադարեցնելու կամ վերսկսելու համար + Crossfade-ը դադարեցված է այս սեսիայի համար + Crossfade-ը վերսկսված է + Տեսանյութի ռեժիմը հասանելի չէ, երբ crossfade-ը միացված է + Crossfade-ը անջատված է SABR-ի վերարտադրման ժամանակ + Crossfade-ը անջատված է casting-ի ժամանակ + Ինչպես է այն աշխատում + "Crossfade-ը համադրում է մի երգի ավարտը հաջորդի սկզբի հետ՝ օգտագործելով ձեր նշած տևողությունը և կորը + +Այն աշխատում է ավտոմատ անցման և ձեռքով բաց թողման ժամանակ, որոնցից յուրաքանչյուրն ունի իր անջատիչը: Ավտոմատ անցման դեպքում համադրումը սկսվում է մի քանի վայրկյան առաջ, նախքան երգի ավարտը" + Սահմանափակումներ + "Երաժշտական տեսահոլովակները հնչում են միայն որպես աուդիո, երբ crossfade-ը միացված է: Երկար սեղմեք shuffle-ի վրա՝ crossfade-ը դադարեցնելու և տեսահոլովակը դիտելու համար + +Երաժշտական տեսահոլովակը, որը սկսում է վերարտադրվել, մնում է տեսանյութի ռեժիմում մինչև հաջորդ երգը + +Fade-ի տևողությունից կարճ երգերը չեն crossfade-վում + +Կողպեքի էկրանը կամ ծանուցումը կարող են կարճ ժամանակով ցույց տալ հնացած վիճակ անցման ընթացքում + +Crossfade-ի դադարեցումը տեսահոլովակը դիտելու համար կարող է որոշ տեսանյութեր դարձնել սև: Crossfade-ի դադարեցումը նախքան երգն ընտրելը կխուսափեցնի դրանից" + Չի աջակցվի + "Աուդիոյի casting կամ mirroring (Chromecast, էկրանի կամ աուդիոյի mirroring) + +SABR վերարտադրում (Spoof տեսանյութերի հոսքերը անջատված են)" + Հեղինակ + Crossfade ֆունկցիան՝ VazerOG-ի կողմից + Հիշել կրկնության վիճակը + Հիշել խառնման վիճակը + Վերարտադրման արագություն + Արագությունը, որն օգտագործվում է բոլոր թրեկների համար: 100%-ի դեպքում օգտագործվում է հավելվածի արագությունը, օրինակ՝ փոդքասթի վերարտադրման արագությունը + Փոխել տոնայնությունը արագության հետ + Անջատել չհավանելու վերահասցեավորումը + Կանխում է հաջորդ տրեկին անցումը, երբ սեղմվում է «Չհավանել» կոճակը + Ցուցակի տրեկի հպման գործողություն + Նվագարկել ցուցակը + Նվագարկել միայն ընտրված տրեկը + Ավելացնել ընտրված տրեկը հերթի մեջ + Ավելացված է հերթի մեջ + Թաքցնել Cast կոճակը + Թաքցնել Պատմության կոճակը + Թաքցնել Ծանուցումների կոճակը + Թաքցնել Որոնման կոճակը + Թաքցնել ձայնային որոնման կոճակը + Թաքցնել ձայնի որոնման կոճակը + Թաքցնել \"Նոր\" կոճակը + Թաքցնում է \"Նոր\" կոճակը «Գրադարան» ներդիրում + Գործողությունների կոճակներ + Ընտրեք, թե որ գործողությունների կոճակները հայտնվեն նվագարկիչի տակ + Թաքցնել գործողությունների տողը + Թաքցնել «Հավանել» և «Չհավանել» կոճակները + Թաքցնել «Մեկնաբանություններ» կոճակը + Թաքցնել «Ուղիղ եթերչատի կրկնություն» կոճակը + Թաքցնել «Տեքստը» կոճակը + Թաքցնել «Կիսվել» կոճակը + Թաքցնել «Պահպանել» կոճակը + Թաքցնել «Ներբեռնել» կոճակը + Թաքցնել «Միացնել ռադիոն» կոճակը + Ցուցակիչ մենյու + Ընտրեք, թե որ բաղադրիչները հայտնվեն նվագարկիչի և հերթի ցուցակիչ մենյուներում + Թաքցնել 3-սյունականի բաղադրիչը + Թաքցնել \"Հավանել» և \"Չհավանել» կոճակները + Թաքցնել \"Taste match\" մենյուն + Թաքցնել «Ավելացնել \'Լսել ավելի ուշ\' ցուցակում» մենյուն + Թաքցնել «Ավելացնել հերթի մեջ» մենյուն + Թաքցնել Ենթագրերի մենյուն + Թաքցնել \"Ջնջել and playlist\" մենյուն + Թաքցնել \"Հեռացնել հերթը\" մենյուն + Թաքցնել \"Մի՛ առաջարկեք այս արտիստին\" մենյուն + Թաքցնել Ներբեռնման մենյուն + Թաքցնել \"Խմբագրել playlist-ը\" մենյուն + Թաքցնել \"Անցնել ալբոմին\" մենյուն + Թաքցնել \"Անցնել արտիստին\" մենյուն + Թաքցնել \"Անցնել էպիզոդին\" մենյուն + Թաքցնել \"Անցնել փոդքասթին\" մենյուն + Թաքցնել օգնության մենյուն + Թաքցնել \"Նշել էպիզոդը որպես լսված\" մենյուն + Թաքցնել \"Հետաքրքիր չէ\" մենյուն + Թաքցնել \"Ամրացնել արագ հավաքման համար\" մենյուն + Թաքցնել \"Նվագարկել հաջորդը\" մենյուն + Թաքցնել որակի մենյուն + Թաքցնել \"Հեռացնել գրադարանից\" մենյուն + Թաքցնել \"Հեռացնել пլեյլիստից\" մենյուն + Թաքցնել հաշվետվության մենյուն + Թաքցնել \"Պահպանել էպիզոդը ավելի ուշ համար\" / \"Պահպանել գրադարանում\" մենյուն + Թաքցնել \"Պահպանել пլեյլիստում\" մենյուն + Թաքցնել կիսման մենյուն + Թաքցնել \"Պատահական նվագարկում\" մենյուն + Թաքցնել \"Քնի ժամանակաչափի\" մենյուն + Թաքցնել \"Միացնել ռադիոն\" մենյուն + Թաքցնել \"Վիճակագրություն գիտնականների համար\" մենյուն + Թաքցնել բաժանորդագրության մենյուն + Թաքցնել \"Հեռացնել արագ հավաքման ցուցակից\" մենյուն + Թաքցնել \"Տեսնել երգի հեղինակներին\" մենյուն + Մեկնարկային էջ + Լռելյայն + Աղբյուրներ + Էպիզոդներ ավելի ուշ լսելու համար + Բացահայտել + Պատմություն + Գրադարան + Հավանած երաժշտություն + Պլեյլիստեր + Որոնում + Բաժանորդագրություններ + Հարկադիր դիվերտիզիոն կողմնորոշում + Պահպանում է հավելվածը դիվերտիզիայի կողմնորոշմամբ, երբ սարքը պտտվում է + Փոխել մինիպլայերի գույնը + Համապատասխանեցնում է մինի-նվագիչի գույնը լիարժեք էկրանի նվագիչին + Փոխել նավիգացիոն վահանակի գույնը + Համապատասխանեցնում է նավիգացիոն վահանակի գույնը լրամասն էկրանի նվագարկիչին + Ցույց տալ \"Հաջորդ» կոճակը մինիպլեյերում + Ցույց տալ «Նախորդ» կոճակը մինիպլեյերում + Միացնել հարկադիր մինիպլեյերը + Խոչընդոտում է նվագիչի ավտոմատ լիարժեք էկրանի անցումը նոր թրեկի սկզբում + Ակտիվացնել մինի-նվագիչի փոխանցմամբ փակումը + Թույլ է տալիս փակել մինիպլեյերը՝ այն ներքև doğru սլայսելով + Նավիգացիայի վահանակ + Ընտրեք, թե որ կոճակները հայտնվեն ներքևի նավիգացիոն վահանակում + Թաքցնել Գլխավոր էջ կոճակը + Թաքցնել Հնչյունանմուշներ կոճակը + Թաքցնել Բացահայտել կոճակը + Թաքցնել Հավաքածու կոճակը + Թաքցնել Թարմացնել կոճակը + Թաքցնել նավիգացիայի վահանակը + Թաքցնել նավիգացիայի կոճակների պիտակները + Չեք մուտք գործել: Պատրաստելու համար հպեք + Ելք + Ելք եք գործել + Միացնել scrobbling-ը + Ուղարկել \"Հիմա հնչում է\" կարգավիճակը + Երգի նվազագույն տևողությունը + Այս տևողությունից կարճ երգերը չեն scrobble լինի + Scrobble հապաղման տոկոսը + Ակտիվացնել scrobble-ը երգի այս տոկոսը լսելուց հետո + Scrobble հապաղման վայրկյաններ + Ակտիվացնել scrobble-ը լսելուց այսքան վայրկյան հետո + ListenBrainz հաշիվ + Մուտք եք կատարել: Հպեք հաշվի կառավարման համար + Մուտքագրեք ձեր ListenBrainz-ի օգտատիրոջ տոկենը՝ scrobbling-ը միացնելու համար + Տոկեն + Ստուգել + Խնդրում ենք մուտքագրել տոկենը + Ստուգվում է… + Վավեր տոկեն: Օգտատեր՝ %s + Անվավեր տոկեն՝ %s + անհայտ + Ստուգումը ձախողվեց՝ %s + Թոքենը ջնջված է + Թոքենը պահպանված է + Զգուշացում. Պահպանված թոքենը անվավեր է: + Սիրել հավանած թրեկները + Ավտոմատ կերպով սիրեք թրեկները Last.fm-ում, երբ դրանք հավանում եք YouTube Music-ում (և հեռացրեք սերը, երբ հավանանքը հեռացնում եք կամ նշում եք որպես «չհավանած»), + Last.fm հաշիվ + Մուտք եք գործել որպես %s: Հպվեք հաշիվը կառավարելու համար + Մուտք գործելու համար մուտքագրեք ձեր Last.fm տվյալները + Օգտանուն + Գաղտնաբառ + Մուտք գործել + Խնդրում ենք մուտքագրել օգտանունը և գաղտնաբառը + Մուտք է գործվում… + Մուտքը հաջողությամբ կատարվեց: + Չհաջողվեց սեսիա ստանալ Last.fm-ից + Մուտքը ձախողվեց. %s + Last.fm-ի տվյալները պահպանվեցին: + Մետատվյալների մաքրում + Մաքրել մետատվյալները + Ավտոմատ մաքրել տարածված ավելորդ տեքստերը (օրինակ՝ \"Official Video\", \"Remaster\", \"Live\") վերնագրերից, կատարողներից և ալբոմներից scrobbling-ից առաջ + Custom regex մաքրում + Մուտքագրեք հատուկ կանոնավոր արտահայտություն՝ երգերի վերնագրերից, կատարողներից կամ ալբոմներից լրացուցիչ տեքստերը հեռացնելու համար: Օրինակ՝ (?i)\\s*\\((clean|explicit)\\) + Վերլուծել կատարողը և թրեկը վերնագրից + Հեռացրեք արտիստի և թրեկի անվանումը տեսանյութի վերնագրից՝ օգտագործելով \" - \" բախչողը: Օգտակար է վերաբեռնված բովանդակության համար, որտեղ ալիքի անվանումը կարող է օգտագործվել որպես արտիստ + Լրացնել բաց թողնված ալբոմը + Երբ երաժշտությունը չի ներառում ալբոմ, փնտրել այն Last.fm-ում, որպեսզի ձեր scrobble-ը լինի ամբողջական + "Scrobbling-ն ավտոմատ կերպով սինխրոնացնում է ձեր YouTube Music-ի օգտագործումը ձեր Last.fm կամ ListenBrainz պրոֆիլի հետ + +Որպեսզի scrobbling-ը սինխրոնացվի ֆոնային ընտացքի ժամանակ, համոզվեք, որ YouTube Music հավելվածի մարտկոցի օգտագործումը սահմանված է որպես \"Սահմանափակում չկա\"" + Օգտագործել երրորդ կողմի բառերը + Ցուցադրում է բառերը երրորդ կողմի ծառայությունից, անցում է ներկառուցված բառերին, եթե ոչինչ չի գտնվել + Պահել էկրանը միացված տեքստերը դիտելիս + Կանխում է էկրանի անջատումը, քանի դեռ տեքստերի վահանակը բաց է + Տեքստերի ծառայություն + Ցուցադրում + Սինխրոնիզացիա + Պանել + AI վերջնակետ + Բոլոր մատակարարները անջատված են՝ ներկառուցված տեքստերը և ամրացված տեքստերը + Վերակնդրե՞լ տրամադրողների հերթականությունը։ + Վերականգնել տրամադրողները լռելյայն առաջնահերթության հերթականությամբ + Մաքրել պահպանված տեքստերը + Ջնջել այս սարքի վրա պահպանված տեքստերը + Մաքրված է %1$s ՄԲ + Ստանալ թոքեն + Apple Music-ի մեդիա օգտատիրոջ տոկեն + "Տեղադրեք media-user-token քուքին music.apple.com կայքից + +Քայլերը +1. Մուտք գործեք music.apple.com կայք՝ որպես անդամ +2. Բացեք DevTools → Application → Cookies → music.apple.com +3. Պատճենեք \"media-user-token\"-ի արժեքը" + Apple Music. media-user-token-ը պահպանված է + Apple Music. media-user-token-ը ջնջված է + Apple Music. անվավեր media-user-token. խնդրում ենք ստուգել և փորձել նորից + Spotify SpDc + "Տեղադրեք sp_dc քուքին Spotify-ից: + +Քայլերը +1. Մուտք գործեք open.spotify.com +2. Բացեք DevTools → Application → Cookies → open.spotify.com +3. Պատճենեք \"sp_dc\"-ի արժեքը" + Spotify. sp_dc-ն պահպանված է + Spotify. sp_dc-ն ջնջված է + Spotify. անվավեր sp_dc. խնդրում ենք ստուգել և փորձել նորից + Apple Music-ի տոկենը կամընտիր է: Առանց դրա կօգտագործվի Lyrically-ն որպես պահուստային տարբերակ + Deezer ARL + "Տեղադրեք arl cookie-ն Deezer-ից + +Քայլերը +1. Մուտք գործեք deezer.com կայքում +2. Բացեք DevTools → Application → Cookies → deezer.com +3. Պատճենեք \"arl\"-ի արժեքը" + Deezer. arl-ը պահվեց + Deezer. arl-ը ջնջվեց + Deezer. անվավեր arl. խնդրում ենք ստուգել և փորձել նորից + Musixmatch-ի տոկեն + Ավելացրեք թոքեն միայն ձեր սեփական հաշվից օգտվելու համար: Այն պետք է լինի Musixmatch Android հավելվածից. musixmatch.com-ից պատճենված թոքենները պատկանում են հնացած desktop կլիենտին, և API-ն մերժում է դրանք + Musixmatch-ն աշխատում է առանց թոքենի, ուստի այս դաշտը կարող է մնալ դատարկ + օգտատիրոջ տոկեն + Musixmatch. օգտատիրոջ տոկենը պահվեց + Musixmatch. օգտատիրոջ տոկենը ջնջվեց + Musixmatch. անվավեր օգտատիրոջ թոքեն. խնդրում ենք ստուգել և փորձել նորից + YouTube-ի ኩկիները + "Տեղադրեք ձեր YouTube-ի ኩկիները՝ YouTube-ից սուբտիտրերը և ժամանակային տեքստերը հարցնելու համար: + +Քայլերը +1. Բացեք youtube.com-ը բրաուզերում և մուտք գործեք +2. Բացեք DevTools (F12) → Application → Cookies → youtube.com +3. Պատճենեք ኩկիների ամբողջ տողը (key=value զույգեր՝ բաժանված կետ-ստորակետներով) + +Պահանջվող ኩկիները՝ SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPISID, LOGIN_INFO" + YouTube. ኩկիները պահպանված են + YouTube. cookies-ները մաքրվեցին + YouTube. անվավեր cookies. խնդրում ենք ստուգել և փորձել նորից + Սեղմեք և պահեք տողը + Սեղմեք տողի վրա՝ երգի այդ հատվածին անցնելու համար, երկար սեղմեք՝ ձեր մատի տակ գտնվող բառը պատճենելու համար, կամ ամբողջ տողը, եթե սեղմումը բառի վրա չէ: Անջատումը հեռացնում է երկու ժեստերն էլ + Ցուցադրել բաժնի պիտակները + Նշել լիրիկայի յուրաքանչյուր նոր բաժին փոքր պիտակով, օրինակ՝ Տուն կամ Կրկներ + Ցույց տալ պատճենման կոճակը + Սեղմեք՝ բնօրինակ տեքստը որպես սովորական տեքստ պատճենելու համար, երկար սեղմեք՝ տեքստի աղբյուր ֆայլը Ներբեռնումներ պապկայում պահպանելու համար + Ցույց տալ թարգմանության կոճակը + Սեղմեք՝ թարգմանությունը միացնելու/անջատելու համար, երկար սեղմեք՝ միայն թարգմանության համար: Օգտագործում է տրամադրողի թարգմանությունները, Google Translate-ը կամ OpenAI-ի հետ համատեղելի API-ն + Ցույց տալ Romanize կոճակը + Սեղմեք՝ տրանսլիտերացիան միացնելու/անջատելու համար, երկար սեղմեք՝ միայն տրանսլիտերացիայի համար: Օգտագործում է տրամադրողի տրանսլիտերացիան, Google Translate-ը կամ OpenAI-ի հետ համատեղելի API-ն + Տեղային տառադարձում և համապատասխանեցում + Կատարել տառադարձում սարքի վրա, երբ մատակարարը, ձեր մոդելը և Google Translate-ը չեն աշխատում: Կարդոմությունը հաջորդում է ժամանակային տեքստի յուրաքանչյուր բառին. բառերի սինխրոնացումը պետք է միացված լինի: Չինարեն, կանտոներեն, ճապոներեն, կորեերեն, հունարեն, ռուսերեն, վրացերեն, հայերեն; մոտ 120 ՄԲ + Կարդացվում է %1$s-ը՝ %2$d%% + Ներբեռնումը ձախողվեց. անջատեք և նորից միացրեք սա՝ կրկնելու համար + Ցույց տալ Refresh կոճակը + Հպեք՝ հաջորդ տեքստի տարբերակին անցնելու համար. սեղմեք երկար՝ ըստ երգի վերնագրի և կատարողի տեքստեր փնտրելու համար + Փնտրել տեքստերը + Երգի վերնագիր + Արտիստ + Մուտքագրեք երգի վերնագիրը և արտիստին + Ակտիվացված մատակարարներից ոչ մեկը չի կարող փնտրել այլ երգեր + Պահպանված է + Ընդգծել սինխրոնացված բառերը կամ վանկերը + Առաջնահերթություն տալ բառ առ բառ տեքստերին, երբ դրանք հասանելի են: Երբ տեքստերը ունեն բառերի ժամանակային տվյալներ, ընդգծվում է յուրաքանչյուր բառ կամ վանկ՝ համահունչ երաժշտությանը + Ցուցադրել հետնապլանային ձայները առանձին + Փակագծերով գրված հետնափոնային վոկալը, օրինակ՝ \"(Oh) I want you\", տեղափոխում է երգվող տողից դեպի իր սեփական ավելի մթնագույն տողը + Թաքցնել հնչեցված տեքստերը + Թաքցնել տեքստի տողերը, որոնք գտնվում են ընթացիկ տողից վերև + Թաքցնել չհնչեցված տեքստերը + Թաքցնել տեքստի տողերը, որոնք գտնվում են ընթացիկ տողից ներքև + Ցուցադրել MediaSession-ի տեքստերը կողպված էկրանին + Ցուցադրում է ընթացիկ տողը կողպելու էկրանին, Android Auto-ում և Bluetooth-ի միջոցով: Պահանջվում է Android 11 կամ ավելի նոր տարբերակ + Ցուցադրել մինի-նվագարկիչի տեքստերը + Ցուցադրում է ընթացիկ տողը մինիպլայերում, իսկ թրեքը տեղափոխվում է ենթավերնագրի տող + Երգի վերնագիր/կատարողի ձևաչափ + Մինիպլեյերում և MediaSession-ում բառերի հետ ցուցադրվող թրեկի ձևաչափը: {t}-ն վերնագիրն է, {a}-ն կատարողը, իսկ {v}-ն տարբերակը, օրինակ՝ Acoustic, եթե բառերի աղբյուրը տրամադրում է այդպիսին: Երկու կետերի միջև գրված տեքստը ցուցադրվում է միայն այն դեպքում, երբ դրա ներսի բոլոր փոփոխականները ունեն արժեք. սովորական կետի համար գրեք \\: Յուրաքանչյուր տող ցուցադրվում է հերթով՝ փոխվելով բառերի փոփոխության հետ մեկտեղ + Լիրիկայի տողի ձևաչափ + Լիրիկայի տողի ձևաչափը մինի-նվագարկիչում և MediaSession-ում: {lrc}-ն տողն է, իսկ {n}-ը՝ թե քանի անգամ է այն երգվում հաջորդաբար: Վերջակետների միջև գտնվող տեքստը ցուցադրվում է միայն, երբ տողը կրկնվում է: Վերջակետի համար օգտագործեք \\: + Ձևաչափը պետք է պարունակի {lrc} + Արտիստ-տողի կաղապար և անիմացիայի ժամանակացույց + Էկրանի նվազագույն ժամանակը (մվ) + Էկրանի առավելագույն ժամանակը (մվ) + Միայն բազմատող անիմացիաների համար: Պահպանեք յուրաքանչյուր կադրը առնվազն նվազագույն ժամանակի ընթացքում: Առավելագույնի դեպքում, առաջացնեք արտիստի տողը, նույնիսկ եթե լիրիկան մնում է նույնը: + Նվազագույնը պետք է լինի 0 կամ ավելի: Առավելագույնը պետք է լինի ավելի մեծ, քան 0-ն, և առնվազն հավասար նվազագույնին: + Տողի երկարության սահմանափակումներ + Բառերի և արտիստի տողի սահմանափակումներ, և լրիվ լայնությամբ նիշի կշիռը + "Բառերի տողի սահմանափակում +Բաժանել ավելի երկար բառերը հավասար կեսերի: 0-ն անջատում է բաժանումը:" + "Արտիստի տողի սահմանափակում +Բաժանել լրացված թրեքի ձևաչափը փոխ替վող շրջանակների: 0-ն անջատում է բաժանումը:" + "Լրիվ լայնությամբ նիշի կշիռը +Կիրառվում է երկու սահմանափակումների համար էլ: 1.75-ի դեպքում չորս լրիվ լայնությամբ նիշերը հաշվարկվում են որպես 7:" + Մուտքագրեք ամբողջ թվային սահմանափակումներ 0-ից 10000-ի միջակայքում և նիշի կշիռ, որը մեծ է 0-ից + Ինստրումենտալ դադար + Ցուցադրվում է փոքրացված նվագարկիչում և MediaSession-ում գործիքային դադարի ժամանակ: Յուրաքանչյուր տողը մեկ անիմացիոն կադր է: {t}-ն վերնագիրն է, {a}-ն՝ արտիստը, իսկ {lrc}-ն՝ լիրիկայի վերջին տողը + Փոքրացված նվագարկիչի և MediaSession-ի ժամանակային շեղում + Տեղաշարժում է այն պահը, երբ հաջորդ տողը ցուցադրվում է փոքրացված նվագարկիչում և MediaSession-ում: Օգտագործեք բացասական արժեք, եթե տողերը ուշ են հայտնվում, օրինակ՝ կողպեքի էկրանին + Զտիչներ + Մետատվյալների regex + Կանոնավոր արտահայտություն, որը կիրառվում է վերնագրերի, կատարողների և ալբոմների վրա՝ նախքան տեքստերի որոնումը և երկու վերնագրերի համապատասխանությունը գնահատելիս: Թողեք դատարկ՝ երկուսն էլ անջատելու համար: Լռելյայն տարբերակը հեռացնում է տարածված ավելորդությունները, ինչպիսիք են \"Official Video\"-ն, \"Remaster\"-ը, \"【插曲】\"-ը և \"- Topic\"-ը + Տեքստերի զտիչի regex + Կանոնավոր արտահայտություն, որը կիրառվում է միայն տեքստերի բնօրինակ տեքստի վրա (ոչ թե թարգմանությունների կամ ռոմանիզացիայի): Թողեք դատարկ՝ անջատելու համար + Տեղեկատվական տողերի նշաններ + Եթե տեքստերը սխալ կերպով են զտվել դեպի ներքև, կարգավորեք կամ մաքրեք սա: Ստորակետով բաժանված նշաններ. նշան պարունակող տողերը, որոնց հաջորդում է վերջակետ, ցուցադրվում են որպես հեղինակային տեղեկություն ներքևում: Թողեք դատարկ՝ անջատելու համար + Մեծատառով սկսել + Մեծատառով է դարձնում լիրիկայի յուրաքանչյուր տողի առաջին տառը + Հեռացնել վերջնակետային նշանները + Հեռացնում է վերջակետները, ստորակետները, երկչնակները և կիսավերջակետները երգի տողերի վերջից: Հարցականները, բացականչական նշանները, ellipsis-ները, չակերտները և փակագծերը պահպանվում են: + Տեքստի չափը + Բառերի տեքստի չափը + Ժամանակային գլոբալ շեղում + Սինխրոնացված տեքստերը հետևում են վերարտադրմանը: Եթե դրանք շեղվում են, կարգավորեք ժամանակային շեղումը: Տեղափոխեք սինխրոնացված տեքստերը առաջ կամ հետ: Օգտագործեք դրական արժեք, եթե տեքստերը գնում են երգից առաջ + Օգտագործել SponsorBlock հատվածները լիրիկայի շեղման համար + Համադրել՝ առանց SponsorBlock-ի բաց թողնած հատվածների, և տեղափոխել լիրիկան թողարկված աուդիոյի վրա: «Անջատված» տարբերակը ամբողջությամբ անտեսում է SponsorBlock-ը + Այս թրեքը չունի տեսանյութի ID՝ ներկայացնելու համար + Աղբյուր՝ %s + Սեղմեք պատճենման կոճակը երկար՝ այս LRC-ը պահպանելու համար. եթե կարողանաք կատարելագործել ժամանակացույցերը և կիսվել դրանցով համայնքային մատակարարների հետ, մենք շատ երախտապարտ կլինենք + Թարգմանված է Google-ի կողմից + Ռոմանիզացված է Google-ի կողմից + Թարգմանված է %s-ի կողմից + Տրանսլիտերացված է %s-ի կողմից + Ռոմանիզացված է %s-ի կողմից + Թարգմանված + Թարգմանության լեզուն + Ռոմանիզացման լեզուն + Թարգմանվում է… + Չհաջողվեց թարգմանել բառերը + Ռոմանիզացված + Ավտոմատ + Ռոմանիզացվում է... + Չհաջողվեց ռոմանիզացնել տեքստերը + Միայն թարգմանված + Միայն տառադարձում + Երկակի ռեժիմ + Պատճենված է հիշողության մեջ + Թարմացվում է + Թարմացված է + QuickLRC API բանալին + QuickLRC-ը տառադարձում է կամ ժամանակային համապատասխանեցնում է ընթացիկ թրեկի տեքստերը: Սա առանձին ծառայություն է, որը կապ չունի վերոհի OpenAI-ի հետ համատեղելի վերջնակետի հետ: + QuickLRC բանալին պահպանված է: Օգտագործեք համապատասխանեցման կոճակը երգերի տեքստերի որոնման պատուհանում կամ վահանակի հուշումը, երբ տրեկը չունի տեքստ + QuickLRC API բանալին պահպանված է + QuickLRC-ը չընդունեց այդ բանալին + QuickLRC API բանալին ջնջված է + QuickLRC-ն ժամանակային նշումներ է ավելացնում այն տեքստերին, որոնք չունեն դրանք, կամ տրանսկրիպցիայի է ենթարկում տրեկը՝ կրեդիտների հաշվին: Բանալիի առկայության դեպքում, առանց տեքստի տրեկերը կառաջարկեն տրանսկրիպցիա, իսկ որոնման պատուհանում կհայտնվի համապատասխանեցման կոճակ + Հարցումել QuickLRC համապատասխանեցում + Համապատասխանեցում + Կիրառել համապատասխանեցումը + Հարցումել QuickLRC տրանսկրիպցիա + Տրանսկրիպցիա + Տրանսկրիպցիան չեղարկվեց + Միայն YouTube-ից նվագվող տրեկները կարող են տրանսկրիբ될 + Համապատասխանեցնելու տեքստ չկա. փոխարենը կատարվում է տրանսկրիպցիա + QuickLRC-ն չկարողացավ մշակել այս տրեկը + QuickLRC-ի կրեդիտները սպառված են այս տրեկի համար + "Բառերը համադրվում են ըստ վերնագրի և կատարողի, անսովոր մետատվյալները կարող են ոչինչ չգտնել կամ գտնել սխալ տարբերակը: Օգտագործեք թարմացումը տարբերակները փոխելու համար: * նշված մատակարարները պահանջում են տոկեն: Միացված բոլոր մատակարարներին միշտ չէ, որ հարցում է ուղարկվում ձեր նշած հերթականությամբ, այլ պատասխանը ստացվում է՝ կախված նրանց արագությունից:\n\nԲառերը տրամադրվում են երրորդ կողմի ծառայությունների կողմից, որոնք կապված չեն Morphe-ի հետ, չեն հովանավորվի կամ հավաստվում նրա կողմից: Յուրաքանչյուրն օգտագործվում է այնպես, ինչպես կա. բառերը, դրանց հասանելիությունը և լիցենզավորումը մնում են այդ ծառայությունների և նրանց լիցենզարուների մոտ, իսկ դրանց օգտագործումը կարող է ենթակայ լինել նրանց օգտագործման պայմաններին: Այն, ինչ հասնում է միացված ծառայությանը, միայն ձեր խնդրած որոնումն է՝ վերնագիրը, կատարողը և, այնտեղ, որտեղ այդ ծառայությանը դա անհրաժեշտ է, տեսանյութի ID-ն: Լրացուցիչ AI վերջնակետը և QuickLRC տրանսկրիպցիան աշխատում են միայն ձեր կողմից կայ alright-ացվելուց հետո և ստանում են բառերի տեքստը կամ թրեքի տեսանյութի URL-ը:" + Ակտիվացրեք միայն ձեր օգտագործած մատակարարները կամ պահպանեք լռելյայն կարգավորումները. հաճախակի հարցումները կարող են przecանցել լիրիկայի փոքր համայնքային կայքերը + Թաքցնել հեղինակության տողը + Թաքցնել տեքստի ներքևում ցուցադրվող հեղինակության տողը + Փոխել թարգմանության և տառադարձման տեղերը + ՄԻԱՑՎԱԾ՝ տառադարձումը դրվում է տեքստի ներքևում, իսկ թարգմանությունը՝ վերևում. ԱՆջԱՏՎԱԾ՝ տառադարձումը մնում է վերևում, իսկ թարգմանությունը՝ ներքևում + Թարգմանություն + Ռոմանիզացիա + Թարգմանչի տրամադրող + Google (Լռումնյա, Անվճար) + OpenAI համատեղելի (Անհատականացված) + Ռոմանիզացման մատակարար + Google (Լռելյայն) + Տեղային / អնցանց բառարան + OpenAI-ի հետ համատեղելի (Անհատական) + AI բաժին + Օգտագործվում է միայն այն դեպքում, երբ տրամադրողը չի ներառում բաժնի պիտակներ, ինչպիսիք են Կրկները կամ Տունը, որոնք այնուհետև հայտնաբերվում են ձեր OpenAI-ի հետ համատեղելի մոդելի կողմից + AI & ամպային վերջնակետեր + Կարգավորեք API բանալիները, մոդելները, վերջնակետերը և հուշումների կաղապարները AI մատակարարների համար: + API endpoint-ի URL-ը (օրինակ՝ https://api.openai.com/v1/chat/completions) + Հուշման կաղապար + Տեղադրնշաններ՝ {language}, {task}, {title}, {artist}, {count}, {lines} + Հուշումը պահպանված է + Հուշումը պահվեց, սակայն AI վերջնակետը դեռ հասանելի չէ: Նախ կայքերգավորեք այն AI կարգավորումներում + Հուշումը վերակայացված է + Անվալիդ JSON: Ուղղեք այն կամ մաքրեք դաշտը նախքան պահպանելը + Թարգմանության և ռոմանիզացիայի կարգավորումները պահպանված են + Կարգավորումները վերականգնվեցին լռելյայն արժեքներին + Հիմնական URL-ը պետք է սկսվի http:// կամ https://-ով + Ստուգել կապը + Կարգավորել endpoint-ները + Ցույց տալ բոլոր endpoint-ները + Կապի ստուգում... + Թարգմանության տրամադրող + Ռոմանիզացիայի տրամադրող + Ստանալ մոդելները + Մոդելների բեռնում… + Ընտրեք մոդելը + Չհաջողվեց բեռնել մոդելները: Ստուգեք API բանալին կամ կապը: + Gemini API կարգավորումներ + OpenAI համատեղելի (Custom Endpoint) + DeepL API կարգավորումներ + API բանալի + Մոդել + Հիմնական URL + API բանալի (ըստ ցանկության) + Թարգմանության հուշում + Հարցման լրացուցիչ մարմին (ըստ ցանկության JSON) + Ավտորիզացիայի բանալի (Free կամ Pro) + Կարգավորեք API բանալիները, մոդելները, վերջնակետերը և հուշումները + 8.23.51 - Անջատել թավ տառերով սիմվոլները + 9.13.51 - Վերականգնել նվագարկիչի \"Հաջորդը\", \"Լիրիկան» և \"Կապված» ներդիրները diff --git a/patches/src/main/resources/addresources/values-hy-rAM/reddit/strings.xml b/patches/src/main/resources/addresources/values-hy-rAM/reddit/strings.xml index cf7776fdaf..e6d216cfd6 100644 --- a/patches/src/main/resources/addresources/values-hy-rAM/reddit/strings.xml +++ b/patches/src/main/resources/addresources/values-hy-rAM/reddit/strings.xml @@ -15,4 +15,123 @@ Second \"item\" text" --> - + + Reddit-ի գաղտնիության քաղաքականությունը + Google-ով մուտքը հասանելի չէ: Մուտք գործելու համար օգտագործեք էլ. հասցեն + Գովազդներ + Թաքցնել լրահոսի գովազդները + Թաքցնել մեկնաբանությունների գովազդները + Նավիգացիայի վահանակ + Թաքցնել \"Պատասխաններ\" կոճակը + Թաքցնել \"Չատ\" կոճակը + Թաքցնել \"Ստեղծել\" կոճակը + Թաքցնել \"Բացահայտել\" կամ \"Համայնքներ\" կոճակը + Թաքցնել Խաղերի կոճակը + Կողմային մենյու + Թաքցնել «Վերջերս այցելած» դարակը + Թաքցնել Խաղերը Reddit-ի դարակից + Թաքցնել Reddit Pro-ի դարակը + Թաքցնել Մասին դարակը + Թաքցնել Ռեսուրսների դարակը + Դասավորություն + Պատճենել տեքստը + Թույլ է տալիս ընտրել և պատճենել տեքստը հրապարակումներում և մեկնաբանություններում: Անջատում է մեկնաբանությունների փակումը + Մեկնաբանությունների փակումը կդադարի աշխատել, քանի դեռ «Պատճենել տեքստը» ֆունկցիան միացված է + Անհատականացված տառատիպ + Բեռնեք անհատականացված TTF կամ OTF տառատիպի ֆայլ ստորև նշված հասցեից: Սա միացնելով՝ կանջատվի նաև «Համարել համակարգային տառատիպը» տարբերակը + Անհատականացված տառատիպի ֆայլ + Հպրեք՝ TTF կամ OTF տառատիպի ֆայլ ընտրելու համար + Ընտրված տառատիպը՝ %s + Նախ ընտրեք անհատականացված տառատիպի ֆայլ + Խնդրում ենք ընտրել .ttf կամ .otf տառատիպի ֆայլ + Չհաջողվեց բեռնել անհատականացված տառատիպը: Ընտրեք այլ ֆայլ + Անջատել գլխավոր էջի ժամանակակից UI-ը + Անջատել սքրինշոթի պատուհանը + Անջատում է պատուհանը, որը հայտնվում է սքրինշոթ անելիս + Համարել համակարգային տառատիպը + Օգտագործեք սարքի համակարգային տառատիպը Reddit Sans-ի կամ Roboto-ի փոխարեն: Սա միացնելով՝ կանջատվի նաև «Անհատականացված տառատիպ» տարբերակը + Թաքցնել «Հարցնել» կոճակը + Թաքցնում է «Հարցնել» կոճակը որոնման տողում + Թաքցնել համայնքների բաժինը + Թաքցնում է կապված կամ առաջարկվող համայնքների բաժինը ենթահամայնքներում + Թաքցնել թրենդային բաժինները + Թաքցնում է թրենդային բաժինները ленտայից և որոնման առաջարկներից + Հեռացնել ծանուցումների առաջարկի երկխոսությունը + Ավտոմատ կերպով փակում է ծանուցման առաջարկի պատուհանը subreddit այցելելիս + Ցուցադրել թեգերը գլխավոր ленտայում + Ցուցադրում է գրառումների թեգերը գլխավոր ленտայում + Լենտայի որոշ տարրեր կարող են չունենալ թեգ + Ցույց տալ դիտումների քանակը հրապարակումներում + Սահմանափակում. հասանելի է միայն եթե API պատասխանը ներառում է դիտումների քանակը + Շրջել հրապարակման գործողությունների տողը + Տեղափոխում է հրապարակումների քվեարկության և մեկնաբանության կոճ button-ները աջ կողմ, իսկ վերակայացման և կիսման կոճ button-ները՝ ձախ կողմ + Փոխել քվեարկության և մեկնաբանության կոճ button-ների տեղերը + Ցուցադրում է հրապարակումների մեկնաբանության կոճ button-ը քվեարկության կոճ button-ներից առաջ + Հեռացնել NSFW զգուշացման երկխոսությունը + Ավտոմատ կերպով ընդունում է NSFW բովանդակության զգուշացումը subreddit այցելելիս + Տարբեր + Մասին + Պատչի տարբերակը և պաշտոնական հղումները + Մաքրել կիսման հղումները + Հեռացնում է հետևման հարցումների պարամետրերը կիսված հղումներից + Բացել հղումները ուղղակիորեն + Շրջանցում է վերահասցեավորման URL-ները արտաքին հղումներ բացելիս + Լիարժեք լայնությամբ ленտայի մեդիան + Ցուցադրում է պատկերները և տեսանյութերը ленտայում և հրապարակման մանրամասներում եզրից եզր, առանց կողային արհեստական հեռացման և կլորացված անկյունների + Մեդիա դիտիչի մգլման ուժը + Պատկերներ և տեսանյութեր դիտելիս վերնագրի հետևում գտնվող մգլման ուժը՝ 0-ից (առանց մգլման) մինչև 1 (Reddit-ի լռելյայն արժեքը) + Պահպանել լիգի դիրքը + Պահպանում է լիգը նույն դիրքում հրապարակումը, պատկերը կամ տեսանյութը փակելուց հետո, փոխարենը բացված հրապարակումը վերև տեղափոխելու + Լիգի մեդիայի առավելագույն բարձրությունը + Լ 당연այլ նկարների, տեսանյութերի և պատկերասրահների առավելագույն բարձրությունը ленտայում՝ որպես նրանց լայնության բազմապատիկ: Ավելի բարձր մեդիան կտրվում է: 1.33-ը Reddit-ի լռելյայն արժեքն է + Նվագարկել Redgifs-ը ձայնով + Նվագարկում է Redgifs-ի բնօրինակ տեսանյութը Reddit-ի պատճենի փոխարեն, որը հաճախ չունի ձայն + Redgifs HD որակ + Նվագարկում է HD Redgifs տեսանյութը: HD տեսանյութերն ավելի մեծ են և կարող են դադարել բեռնման համար + Ներբեռնման վայրը + Բոլոր հաշիվները + Reddit-ի լռելյայն կարգավորումները. պատկերները՝ Pictures/Reddit-ում, տեսանյութերը՝ Download-ում + Թղթապանակ՝ %s + Ֆայլի անվանում՝ %s + Reddit-ի ֆայլերի անվանումները + Օգտագործում է \"Բոլոր հաշիվները\" տարբերակը + Օգտագործել սեփական վայրը + Պահպանել + Reddit-ի լռելյայն թղթապանակները + Լռելյայն + Ենթաթղթապանակ + Ավելացրեք ենթաթղթապանակներ՝ ֆայլի անվան մեջ / նշանով + Փնտրել… + Այս սարքի վրա թղթապանակների ընտրիչ հասանելի չէ + Ֆայլի անվանումը + Reddit-ի ֆայլի անվանումը + Հպեք տոկենի վրա՝ այն տեղադրելու համար: / նշանը ստեղծում է ենթաթղթապանակ: Անհայտ արժեքները դառնում են \"unknown\" + Պատկերներ՝ %s + Տեսանյութեր՝ %s + Պահպանել + Վերակնդիր + Ջնջել + Վերականգնե՞լ բոլոր հաշիվների ներբեռնման վայրը Reddit-ի լռելյայն արժեքին։ + Ջնջե՞լ %s-ի ներբեռնման վայրը։ + Չհաջողվեց պահպանել ներբեռնման վայրում: Փոխարենը պահպանվեց Reddit-ի լռելյայն թղթապանակում + Ներբեռնման թղթապանակն այլևս հասանելի չէ: Փոխարենը պահպանվեց Reddit-ի լռելյայն թղթապանակում + Չհաջողվեց պահպանել ներբեռնված տեսանյութը + Այս թղթապանակը չի կարող օգտագործվել ներբեռնումների համար + %s (տվյալ) + %s (չեն մուտք գործել) + Անանուն (ելք է կատարել կամ-ինկոգնիտո) + Թաքցնել մեդիա դիտիչի շերտը + Թաքցնում է վերնագիրը, կոճirները և տեսանյութերի կառավարման գործիքները պատկերը կամ տեսանյութը բացելիս: Հպեք մեդիա ֆայլին դրանք ցուցադրելու համար + Թաքցնել «Տես conversations» կոճակը + Թաքցնում է \"Տեսնել զրույցը\" կոճակը մեդիա դիտիչի ներքևի մասում + Մեդիա դիտիչի սև ֆոն + Օգտագործում է սև ֆոն պատկերների և տեսանյութերի հետևում մեդիա դիտիչում՝ թեմայի ֆոնի գույնի փոխարեն + Բացել հղումները արտաքին բրաուզերով + Բացում է հղումները ձեր բրաուզերում՝ հավելվածի ներքին բրաուզերի փոխարեն + Հիշել փակված մեկնաբանությունները + Ձեր փակած մեկնաբանությունները կմնան փակ, երբ նորից բացեք հրապարակումը + Հավելվածի սիկոնը + Ընտրել հավելվածի սիկոնը + Լռելյայն + Անհայտ + diff --git a/patches/src/main/resources/addresources/values-hy-rAM/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-hy-rAM/shared-youtube/strings.xml index fd007eeb78..2768210b7e 100644 --- a/patches/src/main/resources/addresources/values-hy-rAM/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-hy-rAM/shared-youtube/strings.xml @@ -18,46 +18,283 @@ Second \"item\" text" --> + Մեկնարկային անիմացիա + Անջատված + Գույն + Վերագործարկեք կարգավորումներն ավարտելու համար + Հավելվածի անվանումը + Մութ թեմայի ֆոն + Մաքուր սև + Material You (Մաքուր սև) + Դասական YouTube + Մուգ վարդագույն + Մուգ կապույտ + Մուգ կանաչ + Մուգ դեղին + Մուգ նարնջագույն + Մուգ կարմիր + Պատ춤ային մուգ ֆոն + Սահմանեք մուգ թեմայի ֆոնի գույնը + Հավելվածի լռելյայն տարբերակը + Material You (Չեզոք) + Material You (Հիմնական) + Material You (Երկրորդական) + Material You (Երրորդական) + Պատ춤ային + Ներբեռնումներ + Ներբեռնման ռեժիմ + Ներբեռնում հավելվածի միջոցով + Արտաքին ներբեռնող + Ներբեռնողի փաթեթի անվանում + Ձեր տեղադրված արտաքին ներբեռնող հավելվածի փաթեթի անվանումը + Մուտքագրեք փաթեթի անվանումը + Այլ + Հավելվածը տեղադրված չէ + %s-ը տեղադրված չէ: Խնդրում ենք տեղադրել այն + "Չհաջողվեց գտնել տեղադրված հավելվածը հետևյալ փաթեթի անվանով՝ %s + +Համոզվեք, որ փաթեթի անվանումը ճիշտ է և հավելվածը տեղադրված է" + Փաթեթի անվանումը չի կարող լինել դատարկ + Պատ춤ային + Morphe play light + Morphe play black + Պատ춤ային + "Սա փոխում է ձեր հավելվածի գործարկիչում ցուցադրվող սիմվոլը: + +Այս կարգավորումը չի փոխի սիմվոլը հետևյալ վայրերից ոչ մեկում. +• Սարքի համակարգային կարգավորումների հավելվածների ցուցակ +• Որոշ push ծանուցումներ +• \"Բացել հետևյալով\" հավելվածի ընտրիչ +• Android Auto-ի սիմվոլ + +Սիմվոլը ամենուր փոխելու համար կրկին կատարեք patch-ը և փոխեք \"App icon\" patch տարբերակը" + Ծանուցումների սիկոն + Համապատասխանեցնել հավելվածի պատկերին + Morphe play + Morphe-ի ծանուցում + Ձեր դիտումների պատմությունը չի պահպանվում:<br><br>Սա ամենայն հավանականությամբ spowodված է DNS գովազդի արգելափակիչով կամ ցանցային պրոքսիով:<br><br>Սա ուղղելու համար սպիտակ ցուցակում ավելացրեք <b>s.youtube.com</b>-ը կամ անջատեք բոլոր DNS արգելափակիչները և պրոքսիները + Այլևս չցուցադրել + Կեղծել տեսանյութերի հոսքերը + Կարգավորեք հաճախորդի կեղծումը՝ վերարտադրման խնդիրները կանխելու համար + "Ուղղում է վերարտադրման խնդիրները օգտատերերի մեծամասնության համար՝ հայցելով համատեղելի տեսանյութի հոսք + +Եթե դուք YouTube Premium-ի օգտատեր եք, այս կարգավորումը կարող է անհրաժեշտ չլինել" + Չհաջողվեց ստանալ հաճախորդների հոսքերը + Լռելյայն հաճախորդը չի աշխատում + "%1$s-ը մի քանի անգամ հաջորդៗ չաշխատեց, բայց %2$s-ը աշխատեց: + +Դարձնե՞նք %2$s-ը լռելյայն հաճախորդը:" + Փոխել + Այս կարգավորման անջատումը կարող է առաջացնել վերարտադրման խնդիրներ + Փորձեք միացնել. Տեսանյութերի հոսքերի կեղծում > Մուտք Android VR + Լռելյայն հաճախորդ + Մուտք Android VR + Չեք մուտք գործել Android VR: Հպեք այստեղ՝ մուտք գործելու համար + Այս պահին մուտք եք գործել Android VR + Այս ֆունկցիան օգտագործելու համար փոխեք «Լռելյայն հաճախորդը» Android VR-ի + Մուտք գործեք Android VR + "Android VR մուտք գործելը կարող է լուծել վերարտադրման խնդիրները որոշ օգտատերերի համար + +Մուտք գործելու համար սեղմեք ներքևում գտնվող «Շարունակել» կոճակը" + Ելք + Հրահանգներ + "Ակտիվացման կոդ՝ %s + +1. Սեղմեք ստորև գտնվող «Բացել կայքը» կոճակը՝ «Սարքի միացում» կայքը բացելու համար +2. Տեղադրեք (կամ մուտքագրեք) վերևում ցուցադրված ակտիվացման կոդը +3. Մուտք գործեք Google հաշիվ, եթե պահանջվի։ Եթե ունեք մի քանի հաշիվ, ընտրեք YouTube-ի այս հաշիվը +4. Սեղմեք «Թույլ տվեք YouTube VR-ին» +5. Վերադարձեք այս հավելվածին «Սարքը միացված է» հաղորդագրությունը ստանալուց հետո" + Ակտիվացման կոդը տրամադրելը ձախողվեց + Հավաստագրման տոկենը տրամադրելը ձախողվեց + Մուտքը հաջողությամբ կատարվեց + "Հավաստիացումը պահպանված է + +• Ձեր Morphe կարգավորումները չեն ներմուծի/արտահանի Android VR հավաստիացումը +• Եթե այս հավելվածը չօգտագործվի 6 ամիս, Google-ը կհեռացնի այս հավաստիացումը +• Ձեզនឹង պետք լինի նորից մուտք գործել, եթե փոխեք Google-ի ձեր գաղտնաբառը կամ հեռացնեք այս սարքը Google-ի ձեր կարգավորումներից" + OAuth2-ը ժամանակավորապես հասանելի չէ (API-ի ժամանակը սպառվել է) + Android VR-ի մուտքի ժամանակը լիքն է, փորձեք նորից մուտք գործել VR + OAuth2-ը ժամանակավորապես հասանելի չէ (կարգավիճակ %s) + OAuth2-ը ժամանակավորապես հասանելի չէ + Հնարավոր չէ մուտք գործել VR. %s + Ելք է կատարվել Android VR-ից + VR մուտքի տոկենը անվավեր է, փորձեք կրկին մուտք գործել VR + Ծանուցումների մեդիայի կառավարման միջոցները + Անհատականացրեք կողպված էկրանին և ծանուցումների վահանակին ցուցադրվող վերարտադրման կառավարման միջոցները + Թաքցնել «Նախորդ» & «Հաջորդ» կոճակները + Թաքցնում է «Նախորդ» և «Հաջորդ» կոճակները մեդիայի ծանուցումներում, կողպված էկրանին և ականջակալների կառավարման միջոցներում + Անջատել առաջդիմության սահիչը + Արգելում է առաջդիմության սահիչի քաշումը կողպված էկրանին և ծանուցումների վահանակին + Փնտրում «Նախորդ» և «Հաջորդ» կոճակներով + Փոխում է մեդիայի ծանուցման, կողպեքի էկրանի և ականջակալների կառավարման տարրերը, որպեսզի դրանք փնտրեն տեսանյութի մեջ, այլ ոչ թե փոխեն տեսանյութերը + Հետդարձի տևողությունը + Վայրկյանները, որոնք հետ են դարձվում «Նախորդ» կոճակով + Առաջդարձի տևողությունը + Վայրկյանները, որոնք առաջ են դարձվում «Հաջորդ» կոճակով + Փնտրման տևողությունը պետք է լինի 1-ից %s վայրկյանի միջակայքում: Օգտագործվում է լռելյայն արժեքը + Անջատել QUIC արտայայտությունը + Անջատել DRC աուդիոն + Հարկադիր օրիգինալ աուդիո լեզու + Միշտ միացնում է տեսանյութի բնօրինակ ձայնային գիծը՝ կրկնօրինակ տարբերակի փոխարեն + Բաց թողնել լռությունը + Ավտոմատ կերպով բաց է թողնում լռելյության դադարները տեսանյութի ձայնագրության մեջ + Դադարի նվազագույն տևողությունը + Կկարճացվեն միայն առնվազն այսքան տևողությամբ դադարները: Կկիրառվի հաջորդ տեսանյութի մեկնարկից հետո + Ցանցային պրոքսի + Ուղղեք աջակցվող ցանցային հարցումները պրոքսիի միջոցով + Միացնել պրոքսին + Ուղղում է աջակցվող ցանցային հարցումները կոնֆիգուրացված HTTP կամ HTTPS պրոքսիի միջոցով + Պրոքսի հոսթ + Պրոքսի սերվերի հոսթի անունը կամ IP հասցեն + Պրոքսի պորտ + Պրոքսի սերվերի պորտը + Օգտագործել HTTPS պրոքսի + Օգտագործեք HTTPS՝ պրոքսի սերվերին միանալու համար HTTP-ի փոխարեն + Պրոքսիի նույնականացում + Ուղարկում է Basic նույնականացում պրոքսի հարցումների հետ + Պրոքսիի օգտանուն + Օգտանուն Basic պրոքսի-ավտորիզացիայի համար + Պրոքսիի գաղտնաբառ + Գաղտնաբառ Basic պրոքսի-ավտորիզացիայի համար + Թույլատրել ուղիղ կապի անցում + Միացնել ուղղակիորեն, եթե պրոքսին չի աշխատում + Թաքցնել լիաէկրան գովազդները + Այս գործառույթն օգտագործելու համար փոխեք \'Spoof video streams\'-ը ցանկացած հաճախորդի, բացի \'Android Studio\'-ից և \'Android VR Downgraded\'-ից + Մաքրել կիսման հղումները + Հեռացնում է հետևման հարցումների պարամետրերը կիսված հղումներից + Փոխել կիսման հղումները youtube.com-ի + Կիսվող հղումներում music.youtube.com-ը փոխարինում է youtube.com-ով + Փոխել կիսման հղումները youtu.be-ի + Ստիպում է տեսանյութերին օգտագործել youtu.be կարճ URL ձևաչափը + Debug log protocol buffer + Ավելացնում է լրացուցիչ լոգավորման տեղեկություններ մշակողների համար՝ հավելվածի բաղադրիչները նույնականացնելու համար + "Այս կարգավորման միացումը կգրանցի լրացուցիչ դասավորության տվյալներ, ներառյալ էկրանի տեքստերը որոշ UI բաղադրիչների համար: + +Սա կարող է օգնել բաղադրիչները նույնականացնել պատվորյալ զտիչներ ստեղծելիս: + +Սակայն, սրա միացումը կգրանցի նաև օգտատիրոջ որոշ տվյալներ, օրինակ՝ Ձեր IP հասցեն" + Պատ춤ային զտիչ + Կարգավորեք հատուկ զտիչներ ցանկացած բաղադրիչ թաքցնելու համար + Միացնել հատուկ զտիչը + Զտվելիք բաղադրիչի ուղու կառուցող տողերի ցուցակ՝ բաժանված նոր տողերով + Անվավեր հատուկ զտիչ. %s + Կարգավորումների մենյուի զտիչ + Թաքցնել տարրերը հավելվածի ստանդարտ կարգավորումների էջում՝ ըստ նրանց տեսանելի անվան + Մեկ անգամ բացեք հավելվածի ստանդարտ կարգավորումների էջը՝ ցուցակը լրացնելու համար + Հեռացնել դիտողի զգուշացման երկխոսությունը + Սա չի շրջանցում տարիքային սահմանափակումը: Այն պարզապես ավտոմատ կերպով ընդունում է այն + PoToken մատակարար + Կարգավորեք PoToken մատակարարը՝ վերարտադրման խնդիրները կանխելու համար + Արտաքին PoToken մատակարար + "Ուղղում է վերարտադրման խնդիրները՝ օգտագործելով արտաքին PoToken minter (Փորձարարական) + +Եթե դուք YouTube Premium-ի օգտատեր եք, այս կարգավորումը կարող է անհրաժեշտ չլինել" + "Ուղղում է վերարտադրման խնդիրները՝ օգտագործելով արտաքին PoToken minter (Փորձարարական) + +Այս գործառույթն օգտագործելու համար անջատեք \"Կեղծել տեսանյութերի հոսքերը\"" + Եթե այն չի աշխատում, փորձեք നിർബന്ധապես փակել MicroG-ն + Բացել PotHelper-ի կարգավորումները + "Այս գործառույթը ներկայումս մշակվում է և կարող է չաշխատել + +Հպեք այստեղ PotHelper-ը ներբեռնելու համար" + PotHelper-ն այլևս չի աշխատում + "PotHelper-ի այս տարբերակում ներկառուցված տոկենն այլևս չի աշխատում + +Միացրեք «Կեղծել տեսանյութերի հոսքերը»՝ ընկարումը շտկելու համար" + Միացնել + Կեղծել հավելվածի տարբերակը + Խաբում է սերվերին՝ ստիպելով կարծել, թե դուք օգտագործում եք հավելվածի հին տարբերակ, որպեսզի վերականգնեք UI-ի հին հնարավորությունները + "Հավելվածի տարբերակը կփոխարինվի ավելի հին տարբերակով + +Սա կփոխի հավելվածի տեսքն ու հնարավորությունները, սակայն կարող են առաջանալ անհայտ կողմնակի էֆեկտներ + +Եթե հետագայում անջատեք այս ֆունկցիան, խորհուրդ է տրվում մաքրել հավելվածի տվյալները՝ UI սխալներից խուսափելու համար" + Կեղծել հավելվածի տարբերակի թիրախը + Հավելվածի թարմացման հաճախականությունը + Լռելյայն + %s FPS + Գերազանցել թարմացման հաճախականությունը + Միշտ + Վերարտադրում դիվերտիսմենտով + Վերարտադրում լիքը էկրանով + Վերարտադրում դիզայնով և լիքը էկրանով + Տառատեսակ + Լռելյայն + Սարքի համակարգային տառատեսակ + Տառատեսակի հատուկ ֆայլ + Տառատեսակի հատուկ ֆայլ + Ընտրեք TTF կամ OTF տառատեսակի ֆայլ + Ընտրված ֆայլը TTF կամ OTF տառատեսակ չէ + Պատվորյալ տառատեսակը չհաջողվեց բեռնել և այն անջատվել է + Բացասական գնահատականները ժամանակավորապես հասանելի չեն (API-ի ժամանակը սպառվել է) + «Չհավանել» կոճակը հասանելի չէ (կարգավիճակ %s) + Բացասական գնահատականները հասանելի չեն (client API-ի սահմանափակում) + Բացասական գնահատականները հասանելի չեն (%s) + Թաքցված է սեփականատիրոջ կողմից + Չհավանել այս տեսանյութը, ձայների %s-ը չհավանություններ են + Միացնել Return YouTube Dislike-ը + Ցուցադրել անհավանմանները տոկոսով + Ցուցադրում է անհավանմանները որպես հավանմանների տոկոս, այլ ոչ թե թվով + Ցուցադրել հավանմանների մոտ approximate քանակը + Ցուցադրում է հավանմանների մոտ approximate քանակը այն տեսանյութերի համար, որտեղ δημιουργողը անջատել է հավանմանները + Ցուցադրել կապի սխալների մասին ծանուցումներ + Ցուցադրում է ծանուցում, երբ Return YouTube Dislike սերվերը հասանելի չէ + "Return YouTube Dislike-ը վերականգնում է YouTube-ի կողմից 2021-ին թաքցված բացասական ձայների քանակը: Քանակները վերցված են հեռացումից առաջ արխիվացված տվյալներից, իսկ ավելի նոր տեսանյութերի համար դրանք գնահատվում են օգտատերերի ձայների հիման վրա, ուստի դրանք մոտ approximate են և թարմացվում են մի քանի օրը մեկ + +Ձեր դիտած տեսանյութի ID-ն ուղարկվում է Return YouTube Dislike API-ին, իսկ ձեր հավանությունները և բացասական ձայները ուղարկվում են անանուն՝ գնահատումները բարելավելու համար" + Վերադարձնել այս սարքի Return YouTube Dislike API վիճակագրությունը + API պատասխանի ժամանակը, միջին + API պատասխանի ժամանակը, նվազագույն + API պատասխանի ժամանակը, առավելագույն + API-ի արձագանքման ժամանակը, վերջին մեդիան + Անհավանությունները ժամանակավորապես հասանելի չեն. գործում է Client API-ի հարցումների սահմանափակումը rate limit + API-ով ձայների ստացում, կանչերի քանակը + Ցանցային կանչեր չեն կատարվել + API-ով ձայների ստացում, ժամանակաժամկետի լիքացման timeout քանակը + Ցանցային կանչերից ոչ մեկի ժամանակաժամկետը չի լիքացել + API client-ի հարցումների սահմանափակումներ + Client-ի հարցումների սահմանափակումներ չեն հանդիպել diff --git a/patches/src/main/resources/addresources/values-hy-rAM/shared/strings.xml b/patches/src/main/resources/addresources/values-hy-rAM/shared/strings.xml index 31cdbe4dcb..5d1c19dd23 100644 --- a/patches/src/main/resources/addresources/values-hy-rAM/shared/strings.xml +++ b/patches/src/main/resources/addresources/values-hy-rAM/shared/strings.xml @@ -17,21 +17,219 @@ Second \"item\" text" --> + Կարգավորումներ + Համոզվա՞ծ եք, որ ցանկանում եք շարունակել: + Պահպանել + Վերակնդրել + Վերակնդրել գույնը + Անվավեր գույն + Պահանջվում է վերագործարկում + Վերագործարկեք հավելվածը, որպեսզի այս փոփոխությունն ուժի մեջ մտնի + Վերագործարկել + Ներմուծել + Պատճենել + Morphe-ի կարգավորումները վերակնդրվեցին լռելյայն արժեքներին + Ներմուծվել է %s կարգավորում + Ներմուծումը ձախողվեց՝ %s + Նավարկել վերև + Որոնել կարգավորումները + Մասին + «%s»-ի համար արդյունքներ չեն գտնվել + Փորձեք մեկ այլ բանալի բառ + Վերջին որոնումները + Հեռացնել որոնման պատմությունից: + Հեռացնել որոնման պատմությունից + Մաքրել որոնման պատմությունը + Վստա՞հ եք, որ ցանկանում եք մաքրել որոնման ամբողջ պատմությունը: + Որոնման հուշումներ + "• Հպեք ուղուն՝ դրանց տեղափոխվելու համար +• Սեղմած պահեք կարգավորումը՝ դրանց տեղափոխվելու համար +• Սեղմեք Enter՝ որոնման հարցումը պատմության մեջ պահպանելու համար +• Որոնումը չի հաշվի առնում մեծատառ/փոքրատառ տարբերակումը և կետադրությունը +• Ծնող կարգավորումները հայտնվում են անջատված զավակ կարգավորումներից վերև" + Որոնման պատմությունը դատարկ է + Որոնման պատմությունը պահպանելու համար մուտքագրեք որոնման հարցումը և սեղմեք Enter + Ցույց տալ կարգավորումների որոնման պատմությունը + Morphe կարգավորումների անվանումը + Լռելյայն + Լրացուցիչ + Փոփոխություններ + Մորֆինգի ժամանակ + Անհատական + Մուտքագրեք անվանումը + Անունը չի կարող լինել դատարկ + Ցույց տալ Morphe կարգավորումների պատկերակները + Պատճենված է՝ %s + Morphe լեզուն + "Որոշ լեզուների թարգմանությունները կարող են բացակայել կամ լինել թերի: + +Թարգմանել նոր լեզուներ կամ բարելավել գոյություն ունեցող թարգմանությունները, այցելեք morphe.software/translate" + Հավելվածի լեզուն + Ներմուծում / Արտահանում + Ներմուծել / Արտահանել Morphe կարգավորումները + Ներմուծել ֆայլից + Կարգավորումները հաջողությամբ ներմուծվեցին, սեղմեք «Պահպանել» կիրառելու համար + Կարգավորումների ներմուծումը ձախողվեց + Արտահանել ֆայլի մեջ + Կարգավորումները հաջողությամբ արտահանվեցին + Կարգավորումների արտահանումը ձախողվեց + Դուք օգտագործում եք Morphe Patches-ի վերջին տարբերակը՝ <i>%s</i> + Դուք օգտագործում եք Morphe Patches-ի <i>%1$s</i> տարբերակը<br><br><b>Հասանելի է թարմացում՝ <i>%2$s</i></b><br><br>Թարմացնելու համար օգտագործեք Morphe-ը՝ այս հավելվածը նորից փաթեթավորելու համար + Նշում + Այս տարբերակը նախնական թողարկում է, և դուք կարող եք հանդիպել անսպասելի խնդիրների + Պաշտոնական հղումներ + Կայք + Նվիրաբերել + Թարգմանություններ + Հեղինակներ + Թարմացված է + Թարմացում հասանելի է + Ընթացիկ մշակում + Նախկին մշակում + Ակտիվ մշակում + 2022 ‑ 2025 + 2022 ‑ 2025 + 2019 ‑ 2022 + Լիցենզիոն ծանուցումներ + Բեռնում է… + Սխալների ուղղում + Կարգավորեք մշակողի 고급 տարբերակները և ախտորոշման մատյանները + Միացնել 디բագ մատյանների գրանցումը + Debug լոգի stack trace-ը + Ներառում է ստեկի հետքերը 디բագ մատյաններում՝ սխալները հայտնաբերելու համար + Ցույց տալ ծանուցում Morphe-ի սխալի դեպքում + "Սխալների toast ծանուցումների անջատումը թաքցնում է Morphe-ի բոլոր սխալների ծանուցումները: + +Դուք չեք ծանուցվի ցանկացած անսպասելի իրադարձության մասին" + Արտահանել 디բագ լոգերը + Արտահանել Morphe-ի 디բագ լոգերը + Debug-ի լոգերը հաջողությամբ արտահանվեցին + Debug-ի լոգերի արտահանումը ձախողվեց + Փնտրել 디բագ լոգերում… + Մաքրել + Debug լոգավորումն անջատված է + Debug-ի լոգեր չեն գտնվել + Debug-ի լոգերը պատճենվեցին + Debug-ի լոգերի արտահանումը ձախողվեց. %s + Debug-ի լոգերը մաքրվեցին + Ֆունկցիանալության դրոշակների կառավարիչ + Կառավարել բուլյան ֆունկցիանալության դրոշակները + Փնտրել ֆունկցիոնալության դրոշակներում… + Ոչ ակտիվ (%s) + Ակտիվ (%s) + Արգելված (%s) + Պարտադիր (%s) + Ավտոմատ + Անջատված + Միացված + Միացնել հարկադիր + Արգելել + Հեռացնել + Ֆունկցիայի դրոշակները պահպանվեցին + Ֆունկցիաների դրոշակները վերակնդրվեցին + Ֆունկցիաների դրոշակները պատճենվեցին échanges-ի մեջ + Վերագործարկեք հավելվածը՝ այս կարգավորումն օգտագործելու համար + Արտահանել և ներմուծել + Ներմուծվեց %s ֆունկցիոնալության դրոշակ + Ֆունկցիոնալության ցուցիչներ չեն գտնվել + Ֆունկցիոնալության ցուցիչները հաջողությամբ արտահանվեցին + Ֆունկցիոնալության ցուցիչների արտահանումը ձախողվեց + Ֆունկցիոնալության ցուցիչները բեռնվեցին, սեղմեք «Պահպանել» կիրառելու համար + Ֆայլի ընթերցումը ձախողվեց + Բինար որոնում + "Գտնում է, թե որ ֆունկցիոնալության ցուցիչն է առաջացնում հավելվածի վարքագիծը + +%s ցուցիչների կեսը արգելափակված են, և յուրաքանչյուր վերագործարկումից հետո դուք նշում եք՝ արդյոք վարքագիծը դեռ առկա է + +Նախքան սկսելը մեկ անգամ վերարտադրեք վարքագիծը, հակառակ դեպքում որոնումից բացակայելու են այն ցուցիչները, որոնք հավելվածը դեռ չի հայցել" + Սկսել + "Որոնելու համար ֆունկցիոնալության դրոշակներ չկան + +Բինար որոնման մեջ կարող են օգտագործվել միայն այն դրոշակները, որոնց հավելվածը հասել է և որոնք ներկայումս հարկադիր միացված չեն" + "Քայլ %1$s + +Մնացել է %2$s դրոշակ, որոնցից %3$s-ը արգելափակված են այս քայլի համար + +Վերագործարկեք հավելվածը, ստուգեք՝ արդյոք վարքագիծը առկա է, ապա պատասխանեք ստորև" + Ֆունկցիոնալ դրոշակների որոնումը ընթացքի մեջ է + Վարքագիծը առկա է + Վարքագիծը առկա չէ + Չեղարկել երկուական որոնումը + Երկուական որոնումը չեղարկվեց + "%s ֆունկցիոնալ դրոշակը առաջացնում է այդ վարքագիծը + +Այն այժմ արգելափակված է" + "Չհաջողվեց գտնել ֆունկցիոնալ դրոշակ + +Թվում է, թե հավելվածի վարքագիծը չի կառավարվի ֆունկցիոնալ դրոշակի միջոցով" + "Չհաջողվեց գտնել որևէ առանձին feature flag + +Պատկերագծված վարքագիծն անհետացել է որոնման ավելի վաղ փուլում, ուստի հնարավոր է, որ ներգրավված լինեն մեկից ավելի flag-եր, կամ պատասխաններից մեկը սխալ էր" + Բացել MicroG-ի կարգավորումները + MicroG-ն տեղադրված չէ: Տեղադրեք այն + Պահանջվում է գործողություն + "Ձեր MicroG v%1s-ը հնացած է և կարող է խնդիրներ առաջացնել: + +Խնդրում ենք թարմացնել MicroG-ն վերջին v%2s տարբերակին" + Թարմացնել + Անտեսել + "Տեղադրված MicroG-ն չի ಶಿջեստվում Morphe-ի կողմից + +%1s + +Հպեք այն հեռացնելու համար, այնուհետև նորից բացեք այս հավելվածը և տեղադրեք MicroG RE-ն" + Հեռացնել + "MicroG-ն չունի հետին պլանում աշխատելու թույլտվություն: + +Հետևեք ձեր հեռախոսի համար նախատեսված \"Don't kill my app\" ուղեցույցին և կիրառեք հրահանգները ձեր MicroG-ի տեղադրման համար: + +Սա անհրաժեշտ է հավելվածի աշխատանքի համար" + Բացել կայքը + "Խնդիրներից խուսափելու համար MicroG-ի մարտկոցի օպտիմիզացիան պետք է անջատված լինի: + +MicroG-ի համար մարտկոցի օպտիմիզացիայի անջատումը բացասաբար չի անդրադառնա մարտկոցի օգտագործման վրա: + +Սեղմեք շարունակել կոճակը և թույլ տվեք օպտիմիզացիայի փոփոխությունները" + Շարունակել + Ցույց տալ MicroG-ի մարտկոցի օպտիմիզացման երկխոսությունը + "Սա անջատելը կթաքցնի MicroG-ի մարտկոցի օպտիմալացման հրդիരը + +Եթե մարտկոցի օպտիմալացումները չփոխվեն, ապա դուք կարող եք հանդիպել հետևյալին. +• Հավելվածը կարող է պատահականորեն փակվել +• Ծանուցումները կարող են չաշխատել +• Հետնապլանի վերարտադրությունը կարող է չաշխատել" + Հավելվածի հղման մշակում + Կարգավորեք հղումների բացումն այս հավելվածում + Քայլ 1. Անջատեք բնօրինակ հավելվածի հղումները + Բացեք բնօրինակ հավելվածի կարգավորումները և անջատեք տիրույթի բոլոր հղումները + Քայլ 2. Միացրեք փոփոխված հավելվածի հղումները + Բացեք փոփոխված հավելվածի կարգավորումները և միացրեք տիրույթի բոլոր հղումները + Բացել կարգավորումները + 🚨 Փորձարարական զգուշացում 🚨 + "Դուք օգտագործում եք ⚠️ <b>%1$s</b> հավելվածի փորձարարական տարբերակը +<br/><br/> +🔧 Ակնկալեք հավելվածի տարօրինակ վարքագիծ կամ անհայտ սխալներ, քանի դեռ մենք հղկում ենք այս տարբերակի ուղղումները: +<br/><br/> +Եթե ցանկանում եք առավել անխափան փորձառություն, ապա <b>ջնջեք</b> սա և տեղադրեք ✅ <b>%2$s</b> հավելվածի երաշխավորված տարբերակը" + ⚠️ Ես ուզում եմ փորձարարականը + ✅ Ես ուզում եմ կայունը + Ավարհման մատյանը պահպանվել է օգտատիրոջ փաստաթղթերում՝ %s + Ավարհման մատյանը պահպանված է՝ %s diff --git a/patches/src/main/resources/addresources/values-hy-rAM/sponsorblock/strings.xml b/patches/src/main/resources/addresources/values-hy-rAM/sponsorblock/strings.xml index 9d2d4ab7d5..133986071b 100644 --- a/patches/src/main/resources/addresources/values-hy-rAM/sponsorblock/strings.xml +++ b/patches/src/main/resources/addresources/values-hy-rAM/sponsorblock/strings.xml @@ -16,7 +16,214 @@ Second \"item\" text" --> + Միացնել SponsorBlock + SponsorBlock-ը համայնքային համակարգ է YouTube տեսանյութերի ձանձրալի հատվածները բաց թողնելու համար + Արտաքին տեսք + Ցույց տալ քվեարկության կոճակը + Ավելացնում է կոճակ՝ հատվածները դրական կամ բացասական գնահատելու համար + Օգտագործել քառակուսի դասավորություն + Օգտագործում է ուղղանկյուն անկյուններ SponsorBlock-ի կոճակների և կառավարման տարրերի համար + Օգտագործել կրճատված «Բաց թողնել» կոճակ + Նվազեցնում է «Բաց թողնել» կոճակի լայնությունը՝ տեղ խնայելու համար + Ավտոմատ թաքցնել «Բաց թողնել» կոճակը + Թաքցնում է «Բաց թողնել» կոճակը մի քանի վայրկյան անց, փոխանակ այն տեսանելի պահելու ամբողջ հատվածի ընթացքում + «Բաց թողնել» կոճակի տևողությունը + Սահմանեք, թե ինչքան ժամանակ պետք է տեսանելի մնա «Բաց թողնել» կոճակը + Ցուցադրել բաց թողման ծանուցում + Ցուցադրում է ծանուցում, երբ հատվածն ավտոմատ կերպով բաց է թողնվում + Բաց թողման ծանուցման տևողությունը + Սահմանեք, թե ինչքան ժամանակ պետք է տեսանելի մնա չեղարկման ծանուցումը + 1 վայրկյան + 2 վայրկյան + 3 վայրկյան + 4 վայրկյան + 5 վայրկյան + 6 վայրկյան + 7 վայրկյան + 8 վայրկյան + 9 վայրկյան + 10 վայրկյան + Ցույց տալ տեսանյութի տևողությունը՝ առանց հատվածների + Հանում է բոլոր հատվածները փնտրման սլայդերի վրա ցուցադրվող տեսանյութի ընդհանուր տևողությունից + Նոր հատվածների ստեղծում + Ցույց տալ \"Ստեղծել նոր հատված\" կոճակը + Ավելացնում է կոճակ տեսանյութերի նվագարկիչին, որը թույլ է տալիս ուղարկել նոր SponsorBlock հատվածներ + Ժամանակի ճշգրտման քայլը + Ժամանակի ճշգրտման կոճակների փոխ displacement-ը (միլիվայրկյաններով) + Արժեքը պետք է լինի դրական թիվ + Ուղարկման ուղեցույցներ + Ուղեցույցները պարունակում են կանոններ և հուշումներ նոր հատվածներ ստեղծելու համար + Հետևեք ուղեցույցներին + Նոր հատվածներ ստեղծելուց առաջ կարդացեք SponsorBlock-ի ուղեցույցները + Արդեն կարդացել եմ + Ցույց տրվի + Ընդհանուր + Ցույց տալ կապի սխալների մասին ծանուցումները + Ցույց է տալ ծանուցում, երբ SponsorBlock սերվերը հասանելի չէ + Միացնել բաց թողնումների քանակի հետևումը + Ուղարկում է բաց արժեքները SponsorBlock-ի առաջատարների ցուցակին՝ հետևելու համար, թե որքան ժամանակ է խնայվել + Սեգմենտի նվազագույն տևողությունը + Այս արժեքից (վայրկյաններով) կարճ սեգմենտները չեն ցուցադրվի և չեն բացվի + Ժամանակի անվավեր տևողություն + Ձեր անձնական օգտատիրոջ ID-ն + Սա պետք է մնա գաղտնի: Սա նման է գաղտնաբառի և չպետք է հաղորդվի ոչ մեկին: Եթե ինչ-որ մեկը ունենա սա, նա կարող է ներկայանալ որպես դուք + Անձնական օգտատիրոջ ID-ն պետք է լինի առնվազն 30 նիշ + Փոխել API URL-ը + Հասցեն, որն օգտագործում է SponsorBlock-ը սերվերին հարցումներ ուղարկելու համար + Ալիքների սպիտակ ցուցակ + SponsorBlock-ը անջատված է սպիտակ ցուցակում ներառված ալիքների համար + Ծանուցել սպիտակ ցուցակում ներառված ալիքի դեպքում + Ցուցադրում է ծանուցում, երբ SponsorBlock-ը անջատված է սպիտակ ցուցակում ներառված ալիքի համար + SponsorBlock-ը անջատված է այս ալիքի համար + API URL-ի վերակայում + API URL-ը անվավեր է + API URL-ը փոխվել է + Կարգավորումների ներմուծում / արտահանում + Պատճենել + Ձեր SponsorBlock JSON կոնֆիգուրացիան, որը կարելի է ներմուծել / արտահանել Morphe և SponsorBlock-ի այլ հարթակներ + "Ձեր SponsorBlock JSON կոնֆիգուրացիան, որը կարելի է ներմուծել / արտահանել Morphe և SponsorBlock-ի այլ հարթակներ + +Սա ներառում է ձեր անձնական օգտատիրոջ ID-ն: Համոզվեք, որ կիսվում եք դրանով խոհեմությամբ" + Կարգավորումները հաջողությամբ ներմուծվեցին + Ներմուծումը ձախողվեց՝ %s + Արտահանումը ձախողվեց՝ %s + "Ձեր կարգավորումները պարունակում են SponsorBlock-ի մասնավոր օգտատիրոջ ID-ն + +Ձեր օգտատիրոջ ID-ն նման է գաղտնաբառի և երբեք չպետք է փոխանցվի ուրիշներին" + Այլևս չցուցադրել + Փոխել սեգմենտի վարքագիծը + Հովանավոր + Վճարովի and promotion, վճարովի հղումներ և ուղիղ գովազդներ: Ոչ ինքնագովազդման կամ անվճար հիշատակումների համար՝ նպատակների / ստեղծողների / կայքերի / նախնականապես հավանած ապրանքների մասին + Անվճար / Ինքնագովազդում + Նման է Հովանավորին, բացի անվճար / ինքնագովազդումից: Ներառում է բաժիններ ապրանքների, նվիրատվությունների կամ այն մասին տեղեկություն, թե ում հետ են համագործակցել + Փոխազդեցության հիշեցում (Բաժանորդագրություն) + Կարճ հիշեցում՝ հավանելու, բաժանորդագրվելու կամ հետևելու նրանց համար բովանդակության մեջ: Եթե այն երկար է կամ վերաբերում է որևէ կոնկրետ բանի, ապա այն պետք է լինի ինքնագովազդման բաժնում + Հիգլայթ + Տեսանյութի այն հատվածը, որն ամենաշատն են փնտրում մարդիկ + Դադար / Ներածական անիմացիա + Միջակայք առանց իրական բովանդակության: Կարող է լինել դադար, ստատիկ կադր կամ կրկնվող անիմացիա: Չի ներառում տեղեկատվություն պարունակող անցումները + Վերջնական կադրեր / Հեղինակներ + Հեղինակները կամ այն ժամանակը, երբ հայտնվում են YouTube-ի վերջնական կադրերը: Ոչ տեղեկատվություն պարունակող եզրակացությունների համար + Գրավիչ մաս / Ողջույններ + Պատմված թրեյլերներ հաջորդող տեսանյութի համար, ողջույններ և հրաժեշտներ: Չի ներառում լրացուցիչ բովանդակություն ավելացնող բաժինները + Նախադիտում / Ամփոփում + Հոլովակների հավաքածու, որը ցույց է տալիս, թե ինչ է սպասվում կամ ինչ է տեղի ունեցել տվյալ տեսանյութում կամ շարքի այլ տեսանյութերում, որտեղ ողջ տեղեկատվությունը կրկնվում է այլ տեղ + Շեղումներ / Կատակներ + Շեղող տեսարաններ կամ կատակներ, որոնք անհրաժեշտ չեն տեսանյութի հիմնական բովանդակությունը հասկանալու համար: չի ներառում համատեքստ կամ հիմնական մանրամասներ տրամադրող հատվածները + Երաժշկություն. Առանց երաժշկության հատված + Միայն երաժշտական տեսահոլովակների համար: Երաժշտական տեսահոլովակների այն հատվածները, որոնք առանց երաժշկության են և արդեն չեն ընդգրկված մեկ այլ կատեգորիայի մեջ + Բաց թողնել + Շեշտադրում + Բաց թողնել հովնավորին + Բաց թողնել գովազդը + Բաց թողնել փոխազդեցությունը + Անցնել ընդգծմանը + Բաց թողնել ներածությունը + Բաց թողնել միջնաբեկումը + Բաց թողնել միջնաբեկումը + Բաց թողնել եզրափակումը + Բաց թողնել հիմնական մասի սկիզբը + Բաց թողնել նախադիտումը + Բաց թողնել նախադիտումը + Բաց թողնել ամփոփումը + Բաց թողնել շեղումը + Բաց թողնել ոչ երաժշտականը + Բաց թողնել հատվածը + Բաց է թողնվել հովնավորը + Բաց է թողնվել ինքնագովազդը + Բաց է թողնվել ձանձրալի հիշեցումը + Բաց է թողնվել մինչև կարևոր հատվածը + Բաց է թողնվել ներածությունը + Բաց է թողնվել ընդմիջումը + Բաց է թողնվել ընդմիջումը + Բաց է թողնվել եզրափակումը + Բաց է թողնվել հիմնական հատվածը + Բաց է թողնվել նախնական դիտումը + Բաց է թողնվել նախադիտումը + Բաց է թողնվել ամփոփումը + Բաց է թողնվել շեղումը + Բաց է թողնվել ոչ երաժշտական հատվածը + Բաց է թողնվել չուղարկված հատվածը + Բաց են թողնվել մի քանի հատվածներ + Բաց թողնել ավտոմատ կերպով + Բաց թողնել ավտոմատ կերպով մեկ անգամ + Ցույց տալ \"Բաց թողնել\" կոճակը + Ցույց տալ միայն փոխադարձման սլայդերում + Անջատել + Հնարավոր չէ ուղարկել հատվածը՝ %s + SponsorBlock-ը ժամանակավորապես ան అందు throughout է + Հնարավոր չէ ուղարկել հատվածը (կարգավիճակ՝ %1$s %2$s) + Հնարավոր չէ ուղարկել հատվածը: Սահմանափակված է հարցումների քանակը (չափազանց շատ հարցումներ նույն օգտատիրոջ կամ IP-ի կողմից) + Հնարավոր չէ ներկայացնել հատվածը՝ %s + "Հնարավոր չէ ներկայացնել հատվածը +Այն արդեն գոյություն ունի" + Հատվածը հաջողությամբ ներկայացվեց + SponsorBlock-ը ժամանակավորապես հասանելի չէ (API-ի ժամանակը սպառվել է) + SponsorBlock-ը ժամանակավորապես հասանելի չէ (կարգավիճակ՝ %s) + SponsorBlock-ը ժամանակավորապես հասանելի չէ + Հնարավոր չé քվեարկել հատվածի համար (API-ի ժամանակը սպառվել է) + Հնարավոր չէ քվեարկել հատվածի համար (կարգավիճակ՝ %1$s %2$s) + Հնարավոր չé քվեարկել հատվածի համար՝ %s + Դրական ձայն + Բացասական ձայն + Փոխել կատեգորիան + Չկան հատվածներ քվեարկելու համար + %1$s-ից %2$s + Ընտրեք հատվածի կատեգորիան + Կատեգորիան անջատված է կարգավորումներում: Ուղարկելու համար միացրեք կատեգորիան + SponsorBlock-ի նոր հատված + Սահմանել %s-ը որպես նոր հատվածի սկիզբ կամ ավարտ: + Սկիզբ + Ավարտ + Հիմա + Ժամանակը, որից սկսվում է հատվածը + Ժամանակը, երբ ավարտվում է հատվածը + Ժամանակները ճիշտ են՞ + "Հատվածը սկսվում է + +%1$s-ից +և ավարտվում +%2$s-ին + +(%3$s) + +Պատրա՞ստ եք ուղարկել։" + Ուղարկել + Սկիզբը պետք է լինի ավարտից առաջ + Նախ նշեք վայրը ժամանակային զողովի վրա + Նայեք հատվածի նախադիտմանը և համոզվեք, որ այն սահուն է բաց թողնվում + Խմբագրել հատվածի ժամանակավորումը ձեռքով + Ցանկանու՞մ եք խմբագրել հատվածի սկզբի կամ ավարտի ժամանակը։ + Տրված ժամանակը սխալ է + Ուղարկել ընդգծումը + "Արդյո՞ք ընդգծման ժամանակը ճիշտ է + +%s + +Եթե չեք ուղարկում ընդգծումը, սեղմեք «Չեղարկել» և նշեք երկրորդ տեղը" + Ուղարկել ընդգծումը + Վիճակագրություն + Վիճակագրությունը ժամանակավորապես հասանելի չէ (API-ն անջատված է) + Բեռնվում է… + SponsorBlock-ը անջատված է + Ձեր օգտանունը՝ <b>%s</b> + Հպեք այստեղ՝ օգտանունը փոխելու համար + Հնարավոր չէ փոխել օգտանունը. Կարգավիճակ՝ %1$s %2$s + Օգտանունը հաջողությամբ փոխվեց + Ձեր հեղինակությունը <b>%.2f</b> է + Դուք ստեղծել եք <b>%s</b> սեգմենտներ + Հպեք այստեղ՝ ձեր հատվածները տեսնելու համար + SponsorBlock-ի առաջատարների ցուցակը + Հպեք այստեղ՝ գլոբալ վիճակագրությունը և լավագույն ներդ contribu-տորները տեսնելու համար + Դա նրանց կյանքի <b>%s</b>-ն է։<br>Հպեք այստեղ՝ առաջատարների ցուցակը տեսնելու համար + Դա <b>%s</b> է + Վերակայել բաց թողնված հատվածների հաշվիչը՞ + Թափանցիկություն՝ + Գույն՝ + "SponsorBlock-ը տեսանյութերի հատվածների հանրային տվյալների բազա է, ինչպիսիք են հովናരերը, ներածությունները, եզրափակությունները և բաժանորդագրվելու հիշեցումները: Հատվածները ներկայացվում և քվեարկվում են համայնքի կողմից, իսկ տվյալների բազան բաց է և անվճար ներբեռնման համար + +Ձեր դիտած տեսանյութի ID-ն ուղարկվում է SponsorBlock API-ին՝ դրա հատվածները ստանալու համար" diff --git a/patches/src/main/resources/addresources/values-hy-rAM/youtube/strings.xml b/patches/src/main/resources/addresources/values-hy-rAM/youtube/strings.xml index 70ce03fc67..d63dd8a3ca 100644 --- a/patches/src/main/resources/addresources/values-hy-rAM/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-hy-rAM/youtube/strings.xml @@ -18,213 +18,1620 @@ Second \"item\" text" + Morphe-ի մասին + Գովազդներ + Լիգայ + Ընդհանուր + Պլեյեր + Shorts + Դիզայնի սահիչ + Սահմանման կառավարում + Տարբեր + Տեսանյութ + Վերականգնել YouTube-ի կարգավորումների հին էջը + Հեռացնել հետին պլանի ընկնող վերարտադրման սահմանափակումները + Անջատել ֆոնային նվագումը + Ավտոմատ դադարեցում սարքը 잠գված լինելիս + Անջատված + Միշտ դադարեցնել 잠գված լինելիս + Դադարեցնել, եթե անլար աուդիո չի միացված + Անջատել թողարկման արագության սահմանափակումը + Անջատել դասավորության թարմացումները + Արգելում է սերվերին թարմացնել հավելվածի դասավորությունը + "Հավելվածի դասավորությունը վերադարձնում է առաջին անգամ տեղադրվելիս օգտագործված դասավորությանը: + +Սերվերային որոշ դասավորություններ կարող են չվերադարձնել: + +Փոփոխությունները ներառում են. +• Նվագարկիչի ցուցակի բաղադրիչները (կամ համապատասխան կարգավորումները) կարող են չաշխատել +• Գլխող թվերը չեն անիմացիա +• Գրադարանի ներդիրը ցուցադրվում է +• Տեսանյութի նկարագրության երաժշտական բաժինը կարող է չաշխատել +• Նվագարկիչի ցուցակի կետերը հնարավոր չէ թաքցնել +• Հաշվի փոփոխման կոճակը կարող է չհայտնվել Գրադարանի ներդիրում" + Միացնել ленտայի թռիչքային մենյուի զտիչը + Лենտայի թռիչքային մենյուի զտիչ + Մուտքագրեք զտման համար նախատեսված թռիչքային մենյուի անվանումները՝ բաժանված նոր տողերով + Միացնել Գրադարան / Դուք ներքևի մենյուի զտիչը + Գրադարան / Դուք ներքևի մենյուի զտիչ + Մուտքագրեք Գրադարան / Դուք ներքևի մենյուի անվանումները զտելու համար՝ բաժանելով նոր տողերով + Թաքցնել ալբոմների քարտերը + Թաքցնում է ալբոմի քարտերը արտիստի նկարագրության տակ + Թաքցնել արտիստների քարտերը + Թաքցնում է արտիստի քարտը որոնման արդյունքների վերևում + Թաքցնել ալիքի կոճակները + Թաքցնում է այնպիսի կոճակներ, ինչպիսիք են՝ «Բաժանորդագրվել», «Տեսնել ալիքը», «Համայնք» և «YouTube Music» որոնման արդյունքների ալիքի պրոֆիլներից + Թաքցնել համայնքի գրառումները + Թաքցնել կոմպակտ բաներները + Թաքցնում է կոմպակտ բաներները և հեռավորությունը որոշ բաղադրիչների միջև + Ընդլայնվող քարտեր տեսանյութերի տակ + Ցուցադրել բոլորը + Թաքցնել միայն ամփոփիչ քարտը + Թաքցնել բոլորը + Թաքցնել լողացող միկրոֆոնի կոճակը + Թաքցնում է լողացող միկրոֆոնի կոճակը որոնման առաջարկների էկրանի ներքևի աջ անկյունում + Թաքցնել \"Ձեռքբերել Premium\" կոճակը + Թաքցնել պատմության դարակը + Թաքցնել հորիզոնական դարակները + Թաքցնում է հորիզոնական դարակները, ինչպիսիք են՝ Վերջին նորությունները, Համայնքային գրառումները, որոնք կարող են ձեզ դուր գալ, Շարունակել դիտումը, Բացահայտել ավելի շատ ալիքներ, Բացահայտել ավելի շատ թեմաներ, Ձեզ համար, Վերջին տեսանյութերը, Ամենահամապատասխանները, Գրառումներ ստեղծողի համայնքից, Գնումներ և Դիտել նորից + Թաքցնել պատկերների դարակը + Թաքցնում է պատկերների դարակները որոնման արդյունքներում + Թաքցնել «Հրավիրել ուրիշներին հաղորդագրություն ուղարկելու» քարտը + Թաքցնում է հրավիրման քարտը, որը ցուցադրվում է «Ծանուցումներ» ներդիրի «Հաղորդագրություններ» բաժնում + Թաքցնել «Վերջին տեսանյութերը» կոճակը + Թաքցնում է կապույտ \"Վերջին տեսանյութերը\" բանเนրը, որը հայտնվում է Գլխավոր ленտայի վերևում + Թաքցնել ուղիղ հեռարձակումները + Թաքցնում է այն տեսանյութերը, որոնք այժմ ուղիղ հեռարձակվում են գլխավոր ленտայում և որոնման արդյունքներում + Թաքցնել միքս ռեպլայլիստները + Թաքցնել ֆիլմերի բաժինը + Թաքցնել Ծանուցումների մենյուի վերնագիրը + Թաքցնում է Ծանուցումների մենյուի վերնագիրը զանգակնի կոճակը սեղմելիս + Թաքցնել «Ծանուցել ինձ» կոճակը + Թաքցնում է հիշեցման կոճակը նախապլանավորված ուղիղ եթերների և պրեմիերաների համար + Թաքցնել Playables-ը + Թաքցնում է այն հնարավորությունը, որը թույլ է տալիս խաղալ խաղեր + Թաքցնել որոնման տերմինի նկարները + Թաքցնում է նախնական դիտման պատկերները որոնման ավտոլրացման ցուցակում + Թաքցնել «Ցուցադրել ավելին» կոճակը + Թաքցնում է ընդլայնման կոճակը որոնման արդյունքների տեսանյութերի տակ + Թաքցնել բաժանորդագրված andալների զողը + Թաքցնել բաժանորդագրված ալիքների անվանումները + Թաքցնում է ալիքների անվանումները բաժանորդագրված ալիքների վահանակում + Թաքցնել հարցումները + Թաքցնել հարցումները և հուշումները ленտայի առաջարկությունների ճշգրտության վերաբերյալ + Թաքցնել տոմսերի բաժինը + Թաքցնել վերբեռնման ժամանակը + "Սահմանափակումներ. +• Shorts-ի դարակները, ալիքի էջերը և որոնման արդյունքները կարող են դեռ ցուցադրել վերբեռնման ժամանակը +• Չի աշխատում ավտոմൊբային դասավորության հետ" + Թաքցնել տեսանյութի նկարը + Թաքցնել տեսանյութերի պիտակները + Թաքցնում է այնպիսի պիտակներ, ինչպիսիք են՝ «Ավտոմատ թարգմանված» / «Թարգմանված», «Ստանում է միջնորդավճար», «Հայտարգություն», «Մարդիկ դիտել են նաև», «ապրանք(ներ)» և «Ձեզ դուր կարող է գալ» + Թաքցնել դիտումների քանակը + "Սահմանափակումներ. +• Shorts-ի դարակները, ալիքի էջերը և որոնման արդյունքները կարող են դեռ ցուցադրել դիտումների քանակը +• Չի աշխատում ավտոմոբային դասավորության հետ" + Թաքցնել վեբ որոնման արդյունքները + Թաքցնում է «Արդյունքներ վեբից» բաժինը որոնման արդյունքներից + Թաքցնել \"Ձեզ գուցե դուր գա\" բաժինը + Թաքցնել YouTube Doodles-ները + Թաքցնում է սեզոնային և իրադարձությունների վերաբերող անիմացիաները YouTube լոգոյի վրա + YouTube Doodles-ի ցուցադրումը կարող է խոչընդոտել հատուկ վերնագրերի ցուցադրմանը + Թաքցնել ալիքի վահանակը + Թաքցնել ալիքի ջրային նշանը + Թաքցնել գլուխները և \'Այս տեսանյութում\' կոճակը + Թաքցնել կրա crowd-ֆնդինգի բլոկը + Թաքցնել արտակարգ իրավիճակների վանդակները + Թաքցնել տեղեկատվական վահանակները + Թաքցնում է տեղեկատվական համատեքստի վահանակները, օրինակ՝ բժշկական տեղեկությունները + Թաքցնել «Անդամակցել» կոճակը + Թաքցնել «Ուղիղ եթերային չաթի կրկնություն» կոճակը + Թաքցնում է չաթի կրկնօրնման կոճակը լրիվ էկրանի ռեժիմում + Թաքցնել բժշկական վահանակները + Թաքցնել նվագարկիչի ժեստերի հուշումները + Թաքցնում է նվագարկիչի ժեստերի ժամանակ ցուցադրվող հուշումները, օրինակ՝ «Քաշեք վերև ճշգրիտ փնտրտուքի համար», «Բաց թողեք՝ չեղարկելու համար» և «2x» + Թաքցնել snackbar-ը + Թաքցնել բաժանորդների ուղեցույցները + Թաքցնել \"Սինխրոնացնել տեսանյութի հետ\" կոճակը + Թաքցնել սեղմելու և պահելու գրադիենտը + Թաքցնում է նվագարկիչի կառավարման գրադիենտը, երբ մատով սահեցնում եք՝ պահելով արագացնելու համար + Թաքցնել ժամանակավոր արձագանքները + Թաքցնում է այն հնարավորությունը, որը թույլ է տալիս օգտատերերին արձագանքել էմոջիներով մեկնաբանությունների բաժնում և ուղիղ չաթում + Թաքցնել «Օգնություն և հետադարձ կապ» մենյուն + Թաքցնում է «Օգնություն և հետադարձ կապ» մենյուն երբ օգտագործվում է երեք կետերով կոճակը, օրինակ՝ որոնման արդյունքների էջում + Թաքցնել հասցեն + Թաքցնում է ձեր ալիքի հասցեն «Դուք» ներդիրում + Թաքցնել տեսանյութի վերնագիրը + Թաքցնում է տեսանյութի վերնագիրը լրիվ էկրանի ռեժիմում + Մաքրել տեսանյութի ենթագրերը + Մաքրում է տեսանյութի ենթագրերից այնպիսի արտահայտություններ, ինչպիսիք են՝ \'#\', \'Fundraiser\', \'product(s)\' և \'Shop\' + Թաքցնել \"Հարցրու Gemini\" կոճակը + Թաքցնում է \"Հարցրու Gemini\" կոճակը \"Հարցրու այս տեսանյութի մասին\" վահանակի ներքևում + Ընտրեք, թե որ բաղադրիչները հայտնվեն տեսանյութի նկարագրության մեջ + Թաքցնել «AI-ով գեներացված տեսանյութի ամփոփում» բաժինը + Թաքցնել Հարցման բաժինը + Թաքցնել Ատրիբուտների բաժինը + Թաքցնում է Առաջատար վայրերը, Խաղերը, Երաժշտությունը և Հիշատակված անձինք բաժինները + Թաքցնել ալիքի հղումների բաժինը + Թաքցնել Գլուխների բաժինը + Թաքցնել Ուղղումների բաժինը + Թաքցնել \"Դասընթացի առաջընթացը\" բաժինը + Թաքցնել Բացահայտման բաժինը + Թաքցնում է \"Բացահայտեք այս դասընթացը\" և \"Բացահայտեք փոդքասթը\" բաժինները + Թաքցնել \"Բացահայտեք այս դասընթացը\" բաժինը + Թաքցնել \"Բացահայտեք փոդքասթը\" բաժինը + Թաքցնել «Առաջարկվող ալիքներ» բաժինը + Թաքցնել «Առաջացած հղումներ» բաժինը + Թաքցնել «Առաջացած միացումներ» բաժինը + Թաքցնել «Առաջացած տեսանյութեր» բաժինը + Թաքցնել հեշթեգերի բաժինը + Թաքցնել «Ինչպես է սա պատրաստվել» բաժինը + Թաքցնել Hype միավորները + Թաքցնել «Տեղեկատվական քարտեր» բաժինը + Թաքցնել \"Հիմնական հասկացություններ\" բաժինը + Թաքցնել «Վիկտորինաներ» բաժինը + Թաքցնել «Որոնել այս տեսանյութի ներսում» բաժինը + Թաքցնել «Բաժանորդագրվել» կոճակը + Թաքցնել «Տրանսկրիպցիա» բաժինը + Թաքցնել «Տեսանյութի մանրամասները» բաժինը + Ընտրեք, թե որտեղ թաքցնել թողնմամբ ֆիլտրի զուգแถվը + Թաքցնել ալիքի էջում + Թաքցնել մեկնաբանություններում + Թաքցնել լիգաներում + Թաքցնել կապված տեսանյութերում + Թաքցնել որոնման արդյունքներում + Թաքցնել դիտումների պատմության մեջ + Ընտրեք, թե որ բաղադրիչները հայտնվեն ալիքի էջում + Միացնել ալիքի ներ পাতাի զտիչը + Ալիքի ներ পাতাի զտիչ + Մուտքագրեք ալիքի ներ পাতাկների անվանումները զտելու համար՝ բաժանելով նոր տողերով + Թաքցնել «Համայնք» կոճակը + Թաքցնել «Միանալ» կոճակը + Թաքցնում է սեղմելի վեբ հղումները ալիքի վերնագրում + Թաքցնել \"Միայն անդամների համար\" չիպը + Թաքցնում է «Միայն անդամների համար» չիպը ալիքի զտիչների տողում + Թաքցնում է ալիքի անդամների ցուցակը + Թաքցնել գրառումների դարակը + Թաքցնել «Խանութ» կոճակը + Թաքցնել «Բաժանորդագրվել» կոճակը + Բացահայտման մենյու + Ընտրեք, թե որ բաղադրիչները հայտնվեն բացահայտման մենյուում + Թաքցնել դասընթացների մենյուն + Թաքցնել բացահայտման կոճակը + Թաքցնել \"Fashion & Beauty\" մենյուն + Թաքցնել գեյմինգի մենյուն + Թաքցնել Hype մենյուն + Թաքցնել Ուսումնառության մենյուն + Թաքցնել Live մենյուն + Թաքցնել անդամակցությունների մենյուն + Թաքցնել «Ֆիլմեր և հեռուստանայտիք» մենյուն + Թաքցնել Երաժշկության մենյուն + Թաքցնել նորությունների մենյուն + Թաքցնել Playables մենյուն + Թաքցնել Փոդքաստների մենյուն + Թաքցնել Գնումների մենյուն + Թաքցնել Սպորտի մենյուն + Թաքցնել YouTube Premium մենյուն + Թաքցնել YouTube Studio մենյուն + Թաքցնել YouTube Music-ի մենյուն + Թաքցնել YouTube Kids-ի մենյուն + Թաքցնել YouTube Create-ի մենյուն + Թաքցնել \"Ինչպես է աշխատում YouTube-ը\" մենյուն + Թաքցնել իրավական և գաղտնիության մասին տեղեկությունների ստորին հատվածները + Ընտրեք, թե որ բաղադրիչները հայտնվեն մեկնաբանությունների բաժնում + Միացնել մեկնաբանությունների կարուսելի զտիչը + "• Համոզվեք, որ մուտքագրում եք կարուսելի տարրի ճիշտ անվանումը (օր.՝ Ask, Hype this video, Listen on YouTube Music) +• Կարուսելի տարրերի թաքցումը կարող է ունենալ անակնկալ կողմնակի ազդեցություններ +• Եթե բոլոր կարուսելի տարրերը թաքցված են, ապա մեկնաբանությունների ողջ բաժինը նույնպես կթաքցվի" + Մեկնաբանությունների կարուսելի զտիչ + Մուտքագրեք մեկնաբանությունների կարուսելի անվանումները զտման համար՝ բաժանելով նոր տողերով + Թաքցնել համատեքստային հաղորդագրությունները + Թաքցնել մեկնաբանությունների բաժինը + Թաքցնել մեկնաբանությունների բաժինը Գլխավոր ленտայում + Թաքցնել համայնքի կանոնները + Թաքցնել \"Ստեղծել Short\" կոճակը + Թաքցնել «Չհավանելու» կոճակը + Թաքցնել Emoji կոճակը + Թաքցնել զտիչների տողի տարբերակները + Թաքցնում է զտիչների տողի բոլոր տարբերակները, բացի \"Հայտնի\" և \"Ամենանորից\" տարբերակներից + Թաքցնել նվերների անիմացիան և քարտերը + Թաքցնել «Նվեր» կոճակը + Թաքցնել Տեղեկություն կոճակը + Թաքցնել ուղիղ եթերային չաթի նվիրատուների շերտը + Թաքցնել ուղիղ չատի էջի ներքևի մասին գնալու կոճակը + Թաքցնել ուղիղ եթեր եկտեսության հուշումները + Թաքցնում է չատի մուտքագրման տողի մոտ ցուցադրվող հուշումները, օրինակ՝ միայն բաժանորդների համար նախատեսված չատի ծանուցումը + Թաքցնել մենյուի կոճակը + Թաքցնում է յուրաքանչյուր մեկնաբանության 3-կետանի մենյուի կոճակը + Թաքցնել մեկնաբանության նախադիտումը + Թաքցնում է մեկնաբանության նախադիտումը տեսանյութի տակ՝ նախքան մեկնաբանությունների ամբողջական բաժնի բացումը + Մեկնաբանությունը թաքցված է Morphe-ի կողմից… + Միացնել մինիմալիստական մեկնաբանությունների կոճակը + Թաքցնում է սահող քարտերը և դրանց էջերի ընտրիչը, որոնք ցուցադրվում են մեկնաբանությունների կոճակի վրա + Թաքցնել \"Շնորհակալություն\" կոճակը + Թաքցնել ժամանակային նշման կոճակը + Թաքցնել \"Լավագույն երկրպագուները\" կոճակը + Թաքցնել թարգմանության կոճակը + Թաքցնել \"Մեկնաբանություններ դեռ չկան\" հաղորդագրությունը + Մաքրել ընդգծված որոնման հղումները + Մաքրում է ընդգծված որոնման հղումները՝ թողնելով միայն բնօրինակ սովորական տեքստը + Բանալի բառերի զտիչ + Ընտրեք կոնկրետ բառեր կամ արտահայտություններ՝ թեյլերում և որոնման մեջ տեսանյութերը թաքցնելու համար + Թաքցնել մեկնաբանությունները ըստ բանալի բառերի + Թաքցնել Գլխավոր էջի տեսանյութերը ըստ բանալի բառերի + Թաքցնել որոնման արդյունքները ըստ բանալի բառերի + Թաքցնել Բաժանորդագրությունների տեսանյութերը ըստ բանալի բառերի + Թաքցնելու բանալի բառերը + "Մուտքագրեք թաքցնելու համար նախատեսված բանալի բառեր և արտահայտություններ՝ բաժանված նոր տողերով + +Բանալի բառերը կարող են լինել ցանկացած տեքստ, որը ցուցադրվում է տեսանյութերի վերնագրերում կամ ալիքների անվանումներում + +Միջինում մեծատառեր ունեցող բառերը պետք է համընկնեն ճշգրիտ (օրինակ՝ iPhone, TikTok, LeBlanc)" + "Գլխավոր / Բաժանորդագրություններ / Որոնում / Մեկնաբանությունները զտվում են՝ թաքցնելու համար բանալի արտահայտություններին համապատասխանող բովանդակությունը + +Սահմանափակումներ. +• Shorts-երը հնարավոր չէ թաքցնել ալիքի անվանամբ +• Որոշ UI բաղադրիչներ կարող են չթաքնվել +• Բանալի բառով որոնումը կարող է չտալ արդյունքներ" + Համընկնություն ամբողջական բառերի հետ + Բանալի բառը/արտահայտությունը չակերտներով շրջելը կկանխի տեսանյութերի վերնագրերի և ալիքների անվանումների մասնակի համընկնումները<br><br>Օրինակ՝<br><b>\"ai\"</b>-ը կթաքցնի տեսանյութը՝ <b>How does AI work?</b><br>բայց չի թաքցնի՝ <b>What does fair use mean?</b> + Չի կարելի օգտագործել բանալի բառը՝ %s + Ավելացրեք չակերտներ՝ %s բանալի բառն օգտագործելու համար + Բանալի բառն ունի հակասող հայտարարություններ՝ %s + Բանալի բառը չափազանց կարճ է և պահանջում է չակերտներ՝ %s + Բանալի բառը կթաքցնի բոլոր տեսանյութերը՝ %s + "• Գլխավոր էջ՝ %s +• Բաժանորդագրություններ՝ %s +• Որոնում՝ %s +• Մեկնաբանություններ՝ %s +• Ընդհանուր՝ %s" + Վիճակագրություն + Թաքցված տեսանյութեր, վերջին 24 ժամ + Թաքցված տեսանյութեր, ամբողջ ժամանակահատվածում + Վերակայմանել վերջին 24 ժամվա հաշվիչը: + Վերակազմել ժամանակի բոլոր հաշվիչները՞ + Արհեստական բանականության ալիքների զտիչ + Թաքցնել տեսանյութերը այն ալիքներից, որոնք AiSList համայնքի ցուցակի կողմից նշվել են որպես AI-ով ստեղծված բովանդակություն + Արգելափակման ցուցակ. բարձր վստահություն + Զգուշացման ցուցակ. միջին վստահություն + Թաքցնել տեսանյութերը «Գլխավոր» էջում + Թաքցնել տեսանյութերը որոնման մեջ + "• Գլխավոր էջ՝ %s +• Որոնում՝ %s +• Ընդհանուր՝ %s" + Արգելափակման ցուցակի ալիքները + Զգուշացման ցուցակի ալիքները + Միացրեք զտիչը՝ ալիքները բեռնելու համար + Բեռնում է ալիքները... + "Գլխավոր էջի և որոնման արդյունքները զտվում են՝ թաքցնելու AiSList-ի կողմից նշված ալիքների տեսանյութերը + +Սահմանափակումներ. +• Զտված AI Shorts-երը կարող են երևալ որպես դատարկ տարածություն +• Որոշ AI տեսանյութեր կարող են դեռևս երևալ" + "AiSList-ը YouTube ալիքների բաց կոդով ցուցակ է, որոնք հրապարակում են AI-ով գեներացված բովանդակություն և սպասարկվում են կամավորների կողմից: Ալիքները հայտնում են օգտատերերը և վերանայվում են նախքան ավելացվելը + +Ցուցակը ներբեռնվում է GitHub-ից, իսկ տեսանյութերը զտվում են ձեր սարքի վրա" + Հայտնել AI ալիքների մասին + Ավելացնել հայտի կետ տեսահոլովակի մենյուում + Ավելացնել կետ տեսահոլովակի մենյուում, որը հայտում է տեսահոլովակի ալիքի մասին AiSList-ին + Հայտողի անունը + Անունը, որը ցուցադրվում է հանրային AiSList լիդերբորդում ձեր հայտերի կողքին: Հայտումը անհասանելի է, քանի դեռ սա դատարկ է + Հայտնել AI ալիքի մասին + "Ուղարկե՞լ %s-ը AiSList, օգտագործելով այս տեսանյութը որպես ապացույց: + +Հայտնեք միայն այն ալիքների մասին, որոնք +• Օգտագործում են AI ձայներ, տեսողական էլեմենտներ, սցենարներ կամ երաժշտություն որպես իրենց հիմնական բովանդակություն +• Զանգվածաբար արտադրում են բովանդակություն AI գործիքներով և նվազագույն մարդկային աշխատանքով + +Չհայտնեք այն ալիքների մասին, որոնք միայն քննարկում են AI-ն կամ օգտագործում են AI-ն իրենց տեսանյութերի որոշ մասերում: + +Հայտերը հրապարակային են և վերանայվում են նախքան ալիքը ցուցակում ավելացնելը: Հեռացումը հնարավոր է միայն GitHub-ի միջոցով հարցում ուղարկելով" + Այս ալիքն արդեն կա AiSList-ի արգելափակման ցուցակում + Այս ալիքն արդեն կա AiSList-ի զգուշացման ցուցակում + Հայտնել + Սահմանեք ուղարկողի անունը AI ալիքների զտիչի կարգավորումներում՝ ալիքների մասին հայտնելու համար + Փնտրելով ալիքը… + Չհաջողվեց գտնել այս տեսանյութի ալիքը + Ալիքի մասին հաղորդումը ուղարկված է և սպասում է վերանայման + AiSList-ը չկարողացավ պահպանել հաղորդումը, փորձեք ավելի ուշ + Ուղարկվել է չափազանց շատ հաղորդումներ, փորձեք ավելի ուշ + AiSList-ը մերժել է հաղորդումը + Հաղորդումը չուղարկվեց, ստուգեք ձեր կապը + Ալիքի գովազդային սպիտակ ցուցակ + Սպիտակ ցուցակում ներառված ալիքների տեսանյութերը ցուցադրվում են գովազդներով, որպեսզի նրանց δημιουργիչները վարձատրվեն: Մնացած բոլոր ալիքներում գովազդները մնում են արգելափակված + Ավելացնել ալիքը գովազդների սպիտակ ցուցակում + Հեռացնել ալիքը գովազդների սպիտակ ցուցակից + Թաքցնել ստեղծողի խնութի դարակը + Թաքցնել վերջնական էկրանի խնութի բաները + Թաքցնել ընդհանուր գովազդները + Թաքցնել ապրանքային բաները + Թաքցնել վճարովի գովազդի պիտակները + Թաքցնել player-ի ցատկող գովազդները + Թաքցնել ինքնահովանավորվող քարտերը + Թաքցնել գնումների հղումները + Թաքցնել տեսանյութերի գովազդները + Թաքցնել YouTube Premium-ի գովազդները + Բեռնում է… + Ընթացիկ ալիքը՝ + Ավելացնել սպիտակ ցուցակում + Հեռացնել + Սպիտակ ցուցակում ներառված ալիքներ + Սպիտակ ցուցակում ալիքներ չկան + Ալիքն ավելացվել է սպիտակ ցուցակում + Ալիքը հեռացվել է սպիտակ ցուցակից + Որոնում ալիքի ներսում + Ավելացնում է որոնման երկրորդ կոճակ ալիքի էջերում՝ միայն այդ ալիքի մեջ որոնելու համար + Որոնել ալիքը + Աղբյուրի որոնման արդյունքները + Այս ալիքում ոչինչ չի գտնվել + Չհաջողվեց որոնել այս ալիքում + Տեսանյութեր + Գրառումներ + Այս ալիքում համընկնող գրառումներ չկան + Համապատասխանություն + Ամենաշատ դիտված + Ամենաքիչ դիտված + Ամենանորը + Ամենահինը + Ամենակարճը + Ամենաերկարը + Հղումը պատճենվեց zտաբլիչնային + Ժամանակային նշումով հղումը պատճենվեց zտաբլիչնային + Ցուցադրել «Պատճենել տեսանյութի հղումը» կոճակը + Հպեք՝ տեսանյութի հղումը պատճենելու համար, հպեք և սեղմեք՝ ժամանակային նշումով պատճենելու համար + Լռելյայն պատճենել ժամանակային նշումը + Փոխում է «Պատճենել տեսանյութի հղումը» կոճակի հպման և հպել-և-սեղմել գործողությունները + Պատճենել տեսանյութի վերնագիրը + Սեղմեք և պահեք տեսանյութի վերնագիրը՝ այն պատճենելու համար + Տեսանյութի վերնագիրը պատճենվեց հիշողության մեջ + Պատճենել մեկնաբանությունները + Սեղմեք և պահեք մեկնաբանությունը՝ այն պատճենելու համար + Մեկնաբանությունը պատճենվեց հիշողության մեջ + Սեղմեք և պահեք՝ տեսանյութի մենյուն բացելու համար + Լիգայի տեսանյութի ցանկացած հատվածի վրա սեղմելը և պահելը բացում է դրա մենյուն + Ցույց տալ «Դիտել ավելի ուշ» կոճակը նկարահատվածների վրա + «Դիտել ավելի ուշ» կոճակը ցուցադրվում է լիգայի տեսանյութերի նկարահատվածների վրա: Դրա վրա սեղմելը և պահելը բացում է «Պահպանել ցուցակում» բաժինը + Ներբեռնել գրառումների պատկերները + Ներբեռնման կոճակը ցուցադրվում է պատկերներով կամ GIF-երով գրառումների «Կիսվել» կոճակի կողքին, իսկ նրանց մենյուին ավելացվում է ներբեռնման տարբերակ + Ներբեռնել պատկերները + Ներբեռնել պատկերը + Ներբեռնվում է %s պատկեր… + Ներբեռնվում է պատկերը… + %1$s պատկեր պահպանվեց %2$s-ում + Պատկերը պահպանվեց %s-ում + Չհաջողվեց ներբեռնել պատկերը + Այս հրապարակման մեջ պատկերներ չեն գտնվել + Ավելացնել «Հարցրու» տարրը լողացող մենյուում + Ավելացնում է տեսանյութի ամփոփման տարրը ленտայի լողացող մենյուում + "«Հարցրու»-ն սկզբնապես պահանջում է մուտք գործած հաշիվ այն երկրում, որտեղ հասանելի է «Հարցրու»-ն + +«Հարցրու» կոճակով տեսանյութը մեկ անգամ բացելուց հետո, ֆունկցիան աշխատում է բոլոր տեսանյութերի հետ, նույնիսկ եթե ընթացիկ երկրում «Հարցրու» ֆունկցիան հասանելի չէ:" + Ամփոփել տեսանյութը + Չհաջողվեց բացել «Հարցրու»-ն այս տեսանյութի համար + Հարցումը հասանելի չէ ձեր հաշվի կամ ներկայիս երկրի համար + Ցույց տալ դիտումների քանակը նկարների վրա + Լրահոսի տեսանյութերի դիտումների քանակը ցույց է տրվում նկարների ներքևի ձախ մասում՝ վերնագրի տակը ոչ + Ցուցադրել ленտայի նկարահատի հավանելությունների քանակը + Ցուցադրել ленտայի նկարահատի հավանելությունների տոկոսը + Փնտրել մեկնաբանություններում + Սեղմեք մեկնաբանությունների որոնման կոճակը՝ ցուցադրելու միայն բեռնված այն մեկնաբանությունները, որոնք պարունակում են որոնվող բոլոր բառերը: Փրազի որոնման համար բառերը դրեք չակերտների մեջ + Որոնել մեկնաբանություններում + Որոնվող բառեր + Որոնել + Մաքրել + Փոխեք տես sorting-ի հերթականությունը կամ նորից բացեք մեկնաբանությունները՝ դրանք որոնելու համար + Որոնումը մաքրված է: Փոխեք տես sorting-ի հերթականությունը կամ նորից բացեք մեկնաբանությունները՝ բոլոր մեկնաբանությունները ցուցադրելու համար + Անջատել «Մուտք գործեք հեռուստացույց» պատուհանը + Անջատել «Միացրեք ձեր սարքերը» պատուհանը + Անջատել գլուխների բաց թողման կրկնակի հպումը + Անջատում է երկու մատով կրկնակի հպման ժեստը, որը բաց է թողնում գլուխները, ինչը կարող է պատահաբար ակտիվանալ մեկ մատով + Ներբեռնեք տեսանյութեր, փոդքաստեր կամ երգեր՝ օգտագործելով երրորդ կողմի ներբեռնման հավելվածներ + Ցուցադրել նվագարկիչի վրա ներբեռնման կոճակը + Ցուցադրում է կոճակ տեսանվագարկիչում, որը բացում է ձեր արտաքին ներբեռնիչը + Արագ ներբեռնում հպմանពេល + Հուշում է՝ ներբեռնել ֆոնային ռեժիմում: Հպեք և պահեք՝ ներբեռնիչը բացելու համար: Աշխատում է YTDLnis-ի և YTindir-ի հետ + Նշել ներբեռնումները որպես դիտված + Արտաքին ներբեռնիչով ներբեռնված տեսանյութերը ավելացվում են ձեր YouTube-ի դիտման պատմության մեջ + Full HD (1080p) + Միջին (192 kbps) + Արագ ներբեռնում + Տեսանյութ + Աուդիո + Բոլոր տարբերակները + Ցուցադրել ներբեռնումը Shorts-երի մենյուում + Ավելացնում է արտաքին ներբեռնողի կոճակ Shorts-երի նվագարկիչի լողացող մենյուում + Ներբեռնել + Ձայնային թարգմանություն + Բարձրաձայն կարդալ տեսանյութի ենթագրերը՝ օգտագործելով սարքի տեքստ-to-speech-ը, համաժամանակեցնելով ընկացման հետ + Ցույց է տալիս վերնադրույտ կոճակ տեսահոլովակի նվագարկիչում: Հպեք՝ թարգմանությունը միացնելու/անջատելու համար, սեղմեք և պահեք՝ արագ կարգավորումների համար + Լեզու + Թարգմանչական ծառայություն + Օրիգինալ ձայնի բարձրություն + YouTube-ի օրիգինալ ձայնի բարձրությունը, երբ հնչում է թարգմանությունը + Թարգմանության ձայնի ուժը + Դուբլյաժային թարգմանության ձայնի ուժը + Խոսքի առավելագույն արագությունը + Արագության առավելագույն բազմապատկիչը, երբ խոսքը տեղավորվում է ենթագրի բացատեղում + "Անվճար թարգմանության API, API բանալի չի պահանջվում: Մինչև 5,000 նիշ օրական անվճար սակագնով, կամ մինչև 50,000 նիշ օրական՝ էլեկտրոնային հասցեով: + +Սեղմեք այստեղ՝ MyMemory.translated.net կայքը բացելու համար" + MyMemory-ի էլ. հասցեն + Կամընտիր: Մուտքագրեք ձեր MyMemory էլեկտրոնային հասցեն՝ օրական սահմանաչափը 5,000-ից 50,000 նիշի բարձրացնելու համար: Թողեք դատարկ՝ անանուն սահմանաչափն օգտագործելու համար + "Վճարովի AI թարգմանություն OpenRouter-ի միջոցով: Լիցքավորեք մեկ անգամ և թարգմանեք հազարավոր տեսանյութեր: Mistral Nemo-ն ամենամատչելի տարբերակն է, իսկ Gemma 4-ն ապահովում է ամենաբարձր որակը: + +Սեղմեք այստեղ՝ OpenRouter.ai կայքը բացելու համար" + OpenRouter API բանալի + Ձեր OpenRouter API բանալին: Ստեղծեք բանալի OpenRouter.ai > Workspace > API Keys բաժնում + OpenRouter մոդել + Պատ춤ային + OpenRouter-ը կոնֆիգուրացված չէ + Նախքան այս պրովայդերը ընտրելը, կարգավորումներում մուտքագրեք ձեր OpenRouter API բանալին + Անվճար + %s յուրաքանչյուր 100 տեսանյութի ժամի համար + Ձայն + Համակարգ + Բազմալեզու արական + Բազմալեզու իգական + Արական + Իգական + Մորֆինգի ժամանակն է: + Թարգմանությունը հասանելի չէ + "Թարգմանության ծառայությունը վերադարձրել է HTTP %s սխալը: Հավանաբար ձեր IP հասցեն սահմանափակվել է մինչև 24 ժամով: + +Շարունակել թարգմանություններն օգտագործելու համար փոխեք թարգմանության մատակարարը կարգավորումներում կամ օգտագործեք VPN:" + Այլևս չցուցադրել + "Ձեր կարգավորումները պարունակում են OpenRouter API բանալի: + +Ձեր API բանալին երբեք չպետք է փոխանցվի ուրիշներին: Ով էլ որ հասանելիություն ունենա դրան, կարող է օգտագործել ձեր OpenRouter կրեդիտները" + OpenRouter կրեդիտների սահմանը սպառված է + Ձեր OpenRouter կրեդիտների սահմանը սպառված է: Լրացրեք ձեր հաշվեկշիռը կամ բարձրացրեք բանալիի ծախսերի սահմանը OpenRouter.ai կայքում + Բացել OpenRouter.ai-ն + OpenRouter թարգմանությունը ձախցավ (HTTP %s) + MyMemory-ի օրական սահմանը սպառված է + Հաջորդը հասանելի կլինի %s-ին + Դուք օգտագործել եք MyMemory-ի թարգմանության այսօրվա անվճար քվոտան (5,000 նիշ): Ավելացրեք MyMemory-ի էլ. հասցեն կարգավորումներում՝ օրական սահմանը բարձրացնելու համար մինչև 50,000 նիշ, կամ անցեք այլ թարգմանչական ծառայության + Դուք օգտագործել եք MyMemory-ի թարգմանության այսօրվա քվոտան (50,000 նիշ): Անցեք այլ թարգմանչական ծառայության կամ փորձեք նորից վաղը + MyMemory-ի թարգմանությունը ձախացավ (կարգավիճակ %s) + Միացնել տեսանյութի կրկնությունը + Չհիշել տեսանյութի կրկնությունը + Արգելել տեսանյութի կրկնության ընթացիկ վիճակի պահպանումը հետագա տեսանյութերի համար + Ցուցադրել տեսանյութի հնկցման կոճակը + Տեսանյութի կրկնությունը միացված է + Տեսանյութի կրկնությունը անջատված է + Կրկնության միջակայք + Սկիզբ + Ավարտ + [ժժ:]րր:վվ + [ժժ:]րր:վվ (դատարկ = տեսանյութի ավարտ) + Սահմանել + Մաքրել + Կրկնվում է %1$s - %2$s + Անվավեր ժամանակային միջակայք + Ցուցադրել «Պատկերը պատկերի մեջ» կոճակը + Ցուցադրել «Պատկերը պատկերի մեջ» մենյուն + Անցնել «Պատկերը պատկերի մեջ» ռեժիմին + Ցուցադրել «Նվագարկել բոլորը» կոճակը + "Հպեք՝ ալիքի բոլոր տեսանյութերի վերցուցակ ստեղծելու համար, սեղմեք և պահեք՝ չեղարկելու համար + +Տեղեկություն. Հնարավոր է չաշխատի ուղիղ հեռարձակումների դեպքում" + Պլայլիստի գեներացման ռեժիմ + Բոլոր բովանդակությունները (Տեսակավորում ըստ ժամանակի, աճման կարգով) + Բոլոր բովանդակությունները (Տեսակավորում ըստ ժամանակի) + Բոլոր բովանդակությունները (Տես sorting ը ըստ հանրաճանաչության) + Միայն տեսանյութեր (Տես sorting ը ըստ ժամանակի) + Միայն տեսանյութեր (Տես sorting ը ըստ հանրաճանաչության) + Միայն Shorts (Տես sorting ը ըստ ժամանակի) + Միայն Shorts (Տես sorting ը ըստ հանրաճանաչության) + Միայն ուղիղ եթերների տեսանյութեր (Տես sorting ը ըստ ժամանակի) + Միայն ուղիղ եթերների տեսանյութեր (Տես sorting ը ըստ հանրաճանաչության) + Միայն անդամների համար նախատեսված ողջ բովանդակությունը + Միայն անդամների համար նախատեսված տեսանյութեր + Միայն անդամների համար նախատեսված Shorts + Միայն անդամների համար նախատեսված ուղիղ եթերներ + Հնարավոր չէ ստեղծել նվագացանկը՝ ալիքի ID-ի անհամապատասխանության պատճառով + Ցուցադրել տեսանյութի վերաբեռնման կոճակը + Տեսանյութի ID-ն հասանելի չէ: Խնդրում ենք տեսանյութը վերբեռնել ձեռքով + Ցուցադրել «Պահպանել ավելի ուշ դիտելու համար» կոճակը + Հպրեք՝ «Ավելի ուշ դիտելու համար» պահպանելու, սեղմեք և պահեք՝ հերթի տարբերակների համար + Տեսանյութը հաջողությամբ պահպանվեց «Դիտել ավելի ուշ» ցուցակում + Այս տեսանյութն արդեն պահպանված է «Դիտել ավելի ուշ» ցուցակում + Չի հաջողվում այս տեսանյութը պահպանել «Դիտել ավելի ուշ» ցուցակում + Պահպանել ավելի ուշ դիտելու համար + Ցուցադրել \"Պահպանել ավելի ուշ դիտելու համար\" կոճակը նվագարկիչի կարգավորումների ընտացող մենյուում + Թույլ է տալիս պահպանել ընթացիկ դիտվող տեսանյութը \"Ավելի ուշ դիտելու\" ցանկում՝ տեսանյութի կարգավորումների ընտացող մենյուից + Ցուցադրել «Պահպանել ավելի ուշ դիտելու համար» կոճակը երեխաների համար նախատեսված տեսանյութերում + Թույլ է տալիս պահպանել երեխաներին ուղղված տեսանյութերը \"Ավելի ուշ դիտելու\" ցանկում՝ ընտացող մենյուից + Ցուցադրել \"Պահպանել ավելի ուշ դիտելու համար\" կոճակը shorts-երի ընտացող մենյուում + Թույլ է տալիս պահպանել shorts-երը \"Ավելի ուշ դիտելու\" ցանկում՝ ընտացող մենյուից + Перенаправить «Смотреть позже» в плейлист + Դիտել ավելի ուշ + Ավելացնել Morphe հերթի մեջ + Պահպանել հերթը սեស្សիաների միջև + Հերթը պահպանվում է հավելվածը վերագործարկելուց հետո + Փոխել «Պահպանել» և «Հերթի մեջ» գործողությունների տեղերը + Հպրեք՝ հերթի տարբերակների համար, սեղմեք և պահեք՝ «Ավելի ուշ դիտելու համար» պահպանելու + Նայել հաջորդը հերթում տիպը + Լռելյայն + Բնիկ հերթ + Morphe հերթ + Morphe հերթ՝ հաճախորդի կողմից + Ավելացնել feed-ի flyout հերթի մենյուի տարրը + Ավելացնում է լրացուցիչ flyout տարր: Միացրեք սա, եթե \"Ավելացնել հերթի մեջ\"-ը չի ցուցադրվում + Հերթ + Հերթ (%s) + Մաքրել բոլորը + Հերթում ոչինչ չկա + Ցանկացած տեսանյութի համար օգտագործեք \"Նվագարկել հաջորդը հերթում\" տարբերակը + Այժմ նվագարկվում է + Նվագարկել հաջորդը հերթում + Նվագարկել հերթի վերջինը + Վերադասավորել + Հեռացնել + Ավելացված է հերթում + Չհաջողվեց այս տեսանյութը ավելացնել հերթի մեջ + Плейլիստերը հնարավոր չէ ավելացնել հերթի մեջ + Հերթը լցված է + Չհաջողվեց միացնել տեսանյութը + Գովազդային սպիտակ ցուցակը ցատկող մենյուում + Ավելացնում է ցատկող տարր, որը թույլ է տալիս գովազդները ցուցադրվեն այդ տեսանյութի ալիքում + Այս գործառույթը կարող է պահանջել PotHelper-ի օգտագործում կամ \"Spoof video streams\"-ի փոփոխումը TV-ի կամ Android VR մուտքի տարբերակին + Արագության սպիտակ ցուցակը ցատկող մենյուում + Ավելացնում է ցատկող տարր, որը միշտ նվագում է այդ տեսանյութի ալիքը 1.0x արագությամբ + Մուտք չէր կատարված + Ցուցակի ID-ն բացակայում է + Հերթը դատարկ է + Տեսանյութի ID-ն բացակայում է + Ձախողվեց՝ %s + Չհաջողվեց ավելացնել հերթի մեջ + Չհաջողվեց ստեղծել հերթը + Չհաջողվեց հեռացնել հերթից + Չհաջողվեց պահպանել հերթը + Ավելացվեց հերթի մեջ + Հերթը ստեղծվեց + Հեռացվեց հերթից + Հերթը պահպանվեց %s-ում + Morphe-ի հերթը + Ավելացնել հերթի մեջ + Ավելացնել հերթի մեջ և բացել + Ավելացնել հերթի մեջ և նվագարկել + Ավելացնել և վերաբեռնել տեսանյութը + Հեռացնել հերթից + Հեռացնել և բացել հերթը + Հեռացնել և վերաբեռնել տեսանյութը + Բացել հերթը + Պահպանել հերթը նվագացուցակում + Պլեյլիստը պահպանված է + Պահպանվեց %s-ում + Մաքրել հերթը + Հերթը մաքրվեց + Հերթ + Նվագարկել հերթի վերջինը + Նվագարկել հերթի հաջորդը + YouTube-ը չկարողացավ ցուցադրել հերթը: + Հերթը պատրաստ չէ այս տեսանյութի համար: Բացեք տեսանյութի նվագարկիչը և փորձեք նորից: + Ավելացնել Mix-ը հերթի մեջ, թե՞ դիտել Mix-ը: + Դիտել + Ավելացնել հերթի մեջ + Բեռնվում է Mix-ը… + Բացել ալիքը ուղիղ եթերային ավատարից + Խոչընդոտում է ուղիղ հեռարձակման բացումը ալիքի ավատարին հպվելիս + Անջատել ճշգրիտ փնտրտուքի ժեստը + Անջատում է ճշգրիտ փնտրման ռեժիմը, որն ակտիվանում է փնտրման սլայդերը վերև քաշելիս + Միացնել ուղիղ եթերի DVR-ը + Թույլ է տալիս հետ կամ առաջ թռիչքներ կատարել բոլոր ուղիղ եթերներում, նույնիսկ նրանց, որոնք սովորաբար սահմանափակում են դա + Միացնել փոխառիվի նկարակները + Ցուցադրում է տեսանյութի նկարակը seekbar-ը քաշելիս + Ամենաշատ դիտված հատվածը + "\"Միացնել ուղիղ եթերի DVR-ը\"-ն կարող է հասանելի չլինել + +Սա ուղղելու համար միացրեք \"Կեղծել տեսանյութերի հոսքերը\"-ն և փոխեք \"Լռելյայն հաճախորդը\"-ն \"TV Simply\"-ի կամ \"visionOS\"-ի" + Ընդլայնել ուղիղ հեռարձակման DVR տևողությունը + Թույլ է տալիս հետសារել ուղիղ հեռարձակումները մինչև 7 օր անցյալն + "'Ընդլայնել ուղիղ հեռարձակման DVR տևողությունը' կարող է unavailable լինել + +Սա ուղղելու համար միացրեք 'Կեղծել տեսանյութերի հոսքերը' և փոխեք 'Լռելյայն հաճախորդը' 'TV Simply'-ի կամ 'visionOS'-ի" + Միացնել սահեցումով փնտրումը + Միացնում է սլայդով փնտրումը՝ տեսանյութի նվագարկիչը սեղմած պահելիս 2x արագությամբ նվագարկելու փոխարեն + Միացնել հպումով փնտրելը + Պարզապես հպեք փնտրման սլայդերին՝ տվյալ դիրքին անցնելու համար առանց այն քաշելու + Ձախ սվայփ ժեստ + Աջ սվայփ ժեստ + Վերևի սվայփ ժեստ + Ձախ/Աջ գոտու լայնությունը + Էկրանի յուրաքանչյուր եզրի տոկոսը, որը ամրագրված է ձախ և աջ սվայփ ժեստերի համար (5-50) + Պայծառություն + Ձայնի and հզորություն + Բնիկ + Անջատված + Անտեսել սվայփերը արգելափակման ժամանակ + Սվայփ ժեստերը անտեսվում են, երբ նվագարկիչը արգելափակված է «Արգելափակել էկրանը» տարբերակով + Միացնել սեղմելով սահեցնելու ժեստը + Պահանջում է սեղմել և պահել, նախքան սվայպի ժեստի ճանաչումը + Միացնել հապտիկ հետադարձ կապը + Պահպանել և վերականգնել լուսավորությունը + Հիշում է հիշողության մակարդակը լիអេկրան ռեժիմ մուտք գործելիս / դուրս գալիս + Միացնել լուսավորության ավտոմատ կարգավորման ժեստը + Պայծառությանը նվազագույն արժեքիまで սահելը ակտիվացնում է համակարգի ավտոպայծառությունը + Ավտոմատ + Սահեցման վերադրույքի ժամանակնիշ + Միլիվայրկյանների քանակը, որի ընթացքում վերադրույքը տեսանելի է + Սահեցման վերադրույքի ֆոնի թափանցիկությունը + Կարգավորեք թափանցիկության մակարդակը (0-ն թափանցիկ է, 100-ը՝ անթափանցիկ) + Սահեցման շերտի պայծառության գույնը + Սահմանեք պայծառության կառավարման առաջընթացի ստիպի գույնը + Սահեցման շերտի ձայնի գույնը + Սահմանեք ձայնի կառավարման առաջընթացի ստիպի գույնը + Սահի շերտի արագության գույնը + Սահմանեք վերարտադրման արագության կառավարման առաջընթացի սահմանի գույնը + Սահեցման շերտի տեքստի չափսը + Կարգավորեք էկրանի տեքստի չափսը (1-30) + Սահեցման աստիճանի շեմը + Սահմանեք սահելու անհրաժեշտ հեռավորությունը, նախքան կարգավորումը կգործի + Ձայնի սահեցման զգայունությունը + Ավելի բարձր արժեքները պահանջում են ավելի շատ սահեցում յուրաքանչյուր ձայնի քայլի համար + Ձայնի քայլերը + Ձայնի ուժեղացում + Առավելագույն ձայնի մակարդակից վերև սահելը ուժեղացնում է այն, ինչը կարող է խեղթացնել բարձր ձայնը + Սարքի լռելյայն արժեքը + Պայծառության սահեցման զգայունություն + Ավելի բարձր արժեքները պահանջում են ավելի շատ սահեցում պայծառության յուրաքանչյուր քայլի համար + Արագության սահեցման զգայունություն + Ավելի բարձր արժեքները պահանջում են ավելի շատ սահեցում արագության յուրաքանչյուր քայլի համար + Արագության քայլ + Արագության գոտու բարձրություն + Արագության ժեստի գոտու բարձրությունը որպես նվագարկիչի բարձրության տոկոս (5-75) + Արագություն + Սահեցման շերտի ոճը + Հորիզոնական շերտ + Հորիզոնական շերտ (մինիմալիստական - վերևում) + Հորիզոնական շերտ (մինիմալիստական - կենտրոնում) + Շրջանաձև շերտ + Շրջանաձև overlay (մինիմալիստական) + Ուղղահայաց overlay + Ուղղահայաց overlay (մինիմալիստական) + Միացնել սվայպը տեսանյութերը փոխելու համար + Սահsetեք լրամասն էկրանի ռեժիմում՝ անցնելու հաջորդ կամ նախորդ տեսանյութին + Գործողությունների կոճակներ + Դիզaynի կոճակներ + Անջատել «Հավանել» և «Բաժանորդագրվել» կոճակների փայլը + Կանխում է «Հավանել» և «Բաժանորդագրվել» կոճակների փայլեցումը, երբ ստեղծողը հիշատակում է դրանք + Թաքցնել գործողությունների վահանակը + Թաքցնել «Հավանել» և «Չհավանել» կոճակները + Թաքցնել «Կիսվել» կոճակը + Թաքցնել «Կանգնեցնել գովազդները» կոճակը + Թաքցնել «Մեկնաբանություններ» կոճակը + Թաքցնել \"Միացնել\" կոճակը + Թաքցնել «Բողոքել» կոճակը + Թաքցնել «Ռեմիքս» կոճակը + Թաքցնել «Ներբեռնել» կոճակը + Թաքցնել «Hype» կոճակը + Թաքցնել «Խթանել» կոճակը + Թաքցնել «Շնորհակալություն» կոճակը + Թաքցնել «Հարցնել» կոճակը + Թաքցնել ալիքը և «Բաժանորդագրվել» կոճակը + Թաքցնում է ալիքի ավատարը և «Բաժանորդագրվել» կոճակը գործողությունների տողի սկզբում + Թաքցնել բաժանորդագրվելու կոճակը + Թաքցնում է «Բաժանորդագրվել» և «Միանալ» կոճակները՝ ալիքի ավատարի կողքին, գործողությունների տողի սկզբում + Թաքցնել «Հատված» կոճակը + Սա կարող է չաշխատել որոշ օգտատերերի հաշիվների համար + Թաքցնել «Խանութ» կոճակը + Թաքցնել «Պահպանել» կոճակը + Թաքցնել \"Էլի\" կոճակը + Լրիվ էկրանի կոճակներ + Թաքցնել \"Չհավանալ\" կոճակը + Թաքցնել \"Հավանել\" կոճակը + Թաքցնել \"Ուղիղ չատի\" կոճակը + Թաքցնել Mix կոճակը + Թաքցնել \"Ավելի շատ տեսանյութեր\" կոճակը + Թաքցնել Ցուցակների կոճակը + Արագ գործողությունների վերին լուսանցք + Սահմանեք արագ գործողությունների զուգแถվի վերին լուսանցքը (0-32 dp) + Ենթագրեր + Կառավարել ավտոմատ ենթագրերը և ենթագրերի cookie-ները + Ավտոմատ ենթագրերի տեսանելիությունը + Միշտ ցուցադրել + Երբեք չցուցադրել + Ցուցադրել, երբ ձայնը միացված է + Ցուցադրել, երբ ձայնը անջատված է + Ենթագրերի նախընտրելի լեզուն + Նախ ստուգում է տրամադրողի ենթագրերը, ապա ավտոմատ կերպով թարգմանում է դրանք նախընտրելի լեզվով + Անջատված + Ավելացնել ավտոմատ թարգմանության լրացուցիչ լեզուներ + Ավելացնում է ավելի շատ լեզուներ ավտոմատ թարգմանության մենյուին + Սահմանել ենթագրերի cookies-ները + "Ուղղում է YouTube-ի ստանդարտ սխալը, երբ ավտոմատ թարգմանության ենթարկվող ենթագրերը կարող են հասանելի չլինել cookie-ների բացակայության պատճառով + +Պետք է տրամադրվեն 'Caption cookies'-ները" + Ենթագրերի cookies-ներ + Cookies-ներ, որոնք պետք է սահմանվեն Timed Text API-ի (Caption Data API) հարցումներում + Ստանալ ենթագրերի cookies-ները + Սեղմեք այստեղ cookies-ները ստանալու համար + Cookies-ների ստացումը ձախողվեց + Cookies-ները արդեն գոյություն ունեն + Cookies-ները հաջողությամբ թարմացվեցին + Ուղղել տառադարձումը + Լուծում է նախնական պայմանի ստուգման սխալը, որը կարող է խոչընդոտել տեքստի բեռնմանը + Ցուցադրել ենթագրերը պատկերը-պատկերի մեջ + Նավիգացիայի կոճակներ + Ընտրեք, թե որ կոճirները հայտնվեն ներքևի նավիգացիոն վահանակում + Թաքցնել «Գլխավոր» կոճակը + Թաքցնել Shorts կոճակը + Թաքցնել «Ստեղծել» կոճակը + Թաքցնել «Բաժանորդագրություններ» կոճակը + Թաքցնել «Ծանուցումներ» կոճակը + Ցուցադրել «Որոնում» կոճակը + Որոնման կոճակի դիրքը + Ցուցադրել կարգավորումների կոճակը + Կարգավորումների կոճակի դիրքը + Կարգավորումների կոճակի գործողությունը + Միացված լինելու դեպքում բացում է Morphe-ի կարգավորումները, իսկ անջատված լինելու դեպքում՝ YouTube-ի կարգավորումները + Փոխել «Ստեղծել» կոճակը «Ծանուցումների» կոճակի հետ + Եթե այս կարգավորման փոփոխությունը չի ազդում, փորձեք անցնել Incognito ռեժիմի + Թաքցնել նավիգացիայի կոճակների պիտակները + Թաքցնել նոր բովանդակության կետը + Թաքցնել նավիգացիայի վահանակը + "Սա ամբողջությամբ թաքցնում է նավիգացիայի վահանակը, ներառյալ \"Դուք\" ներ পাতাն + +Կարգավորումների բաժին մուտք գործելու համար ակտիվացրեք Կարգավորումների կոճակը՝ Ընդհանուր → Գործիքների վահանակ → Ցույց տալ կարգավորումները" + Միացնել նավիգացիայի նեղ կոճակները + Նվազեցնում է կոճակների միջև հորիզոնական հեռավորությունը + Անջատել առանց պիտակների նավիգացիոն կոճակները + Ցույց է տալիս նավիգացիոն կոճակները պիտակներով, եթե YouTube-ը դրանք ուղարկում է առանց պիտակների + Անջատել գործիքների andպանակի նավիգացիոն ներդիրները + Ցուցադրում է «Բաժանորդագրությունները» նավիգացիոն վահանակում, եթե YouTube-ը այն տեղափոխի Գլխավոր էջի վերևի ներդիրի մեջ + Միացնել նավիգացիայի վահանակի անիմացիաները + Անիմացնում է անցումները ներդիրների միջև անցում կատարելիս + Անջատել նավիգացիայի վահանակի ավտոմատ թաքցումը + Պահպանում է նավիգացիոն վահանակի տեսանելիությունը թողարկման ժամանակ + Նավիգացիայի վահանակի ոճը + Լռելյայն + Կիսաթափանցիկ + Թափանցիկ + Անթափանց + Այս ոճը հնարավոր է չի աջակցվի ձեր սարքի կամ YouTube-ի տարբերակի կողմից, ինչը կարող էបերել դասավորվածության խնդիրների կամ անսպասելի անջատումների + Գործիքների վահանակ + Ընտրեք, թե որ բաղադրիչները հայտնվեն գործիքների վահանակում + Թաքցնել Cast կոճակը + Թաքցնել չաթի կոճակը + Թաքցնել խոսափողի կոճակը + Թաքցնել որոնման կոճակը + Երբ միացված է, հպումը բացում է Morphe-ի կարգավորումները, իսկ երկար սեղմումը՝ YouTube-ի կարգավորումները. անջատեք՝ փոխելու համար + Ցուցակիչ մենյու + Ընտրեք, թե որ բաղադրիչները հայտնվեն թռիչքային մենյուում + Թաքցնել \"Լրացուցիչ կարգավորումներ\" / Ելս մենյուն + Թաքցնել \"Շրջապատային ռեժիմ\" մենյուն + "'Audio Track' մենյուն թաքցված է + +'Audio Track' մենյուն ցուցադրելու համար փոխեք 'Spoof video streams'-ը ցանկացած կլիենտի, բացի 'Android Studio'-ից և 'Android VR Downgraded'-ից" + Թաքցնել \"Աուդիո գիժի\" մենյուն + Թաքցնել \"Աուդիո գիժի\" մենյուի ստորին մասը + Թաքցնել Ենթագրերի մենյուն + Թաքցնել Ենթագրերի մենյուի ստորին մասը + Թաքցնել Սուբտիտրերի մենյուի վերնագիրը + Թաքցնել \"Օգնություն & հետադարձ կապ\" մենյուն + Թաքցնել \"Լսել YouTube Music-ով\" մենյուն + Թաքցնել \"Կողպել էկրանը\" մենյուն + Թաքցնել \"Կրկնել տեսանյութը\" մենյուն + Թաքցնել \"Ճանապարհին\" մենյուն + Թաքցնել որակի մենյուն + Թաքցնել որակի մենյուի ստորին մասը + Թաքցնել որակի մենյուի վերնագիրը + Թաքցնել \"Քնի ժամանակացույց\" մենյուն + Թաքցնել «Վերարտադրման արագությունը» մենյուն + Թաքցնել \"Կայուն ձայնի մակարդակ\" մենյուն + Թաքցնել \"Դիտել VR-ով\" մենյուն + Թաքցնել ավտոմատ միացման նախադիտումը + Թաքցնում է հաջորդող տեսանյութի նախադիտումը լրիվ էկրանով նախքան ավտոմատ միացումը սկսվելը + Վերադիր կոճակներ + Ընտրեք, թե որ կոճակները հայտնվեն տեսանյութի նվագարկիչի վրա + Թաքցնել «Ավտոմատ միացում» կոճակը + Թաքցնել «Ենթագրեր» կոճակը + Թաքցնել «Cast» կոճակը + Թաքցնել «Փոքրացնել» կոճակը + Թաքցնել «Լրիվ էկրան» կոճակը + Թաքցնել նվագարկիչի կառավարման կոճակների հետնամասը + Թաքցնում է մուգ အရանձնացումը, որը շրջապատում է տեսանյութի նվագարկիչի կառավարման կոճակները + Կառավարման կոճակների ֆոնի թափանցիկությունը + Կարգավորել նվագարկիչի կառավարման կոճակների և ժամանակի սլայդերի հետևի ֆոնի թափանցիկությունը (0-ն անտեսանելի է, 100-ը՝ լիարժեք) + Թաքցնել «Նախորդ» և «Հաջորդ» կոճակները + Թաքցնել «Կարգավորումներ» կոճակը + Թաքցնել ավարտական էկրանի քարտերը + Շրջապատի ռեժիմ + Կառավարեք Ambient ռեժիմի տեսանելիությունը և էներգիայի սահմանափակումները + Շրջանցել Ambient ռեժիմի սահմանափակումները + Թույլ է տալիս Ambient ռեժիմին մնալ ակտիվ մարտկոցի խնայողության ռեժիմում + Անջատել Ambient ռեժիմը + Անջատել Ambient ռեժիմը լրիվ էկրանի ժամանակ + Թաքցնել տեղեկատվական քարտերը + Անջատել թվերի փոփոխման անիմացիաները + Դադարեցնում է հավանումների և դիտումների քանակի հաշվարկի անիմացիան + Թաքցնել տեսանվագարկիչի առաջընթացի սահիկը + Թաքցնել տեսանյութերի նկարների առաջընթացի սահիկը + Թաքցնում է դիտման առաջընթացի սլայդերը, որը ցուցադրվում է արդեն դիտված տեսանյութերի նկարազարդերին + Միացնել լրիվ էկրանի մեծ սահիկը + Թաքցնել Shorts-ը + Թաքցնել ալիքի էջում + Թաքցնել «Գլխավոր» լիգայում և հարակից տեսանյութերում + Թաքցնել որոնման արդյունքներում + Թաքցնել «Բաժանորդագրություններ» լրացուցիչում + Թաքցնել տեսանյութի նկարագրության մեջ + Թաքցնել դիտման պատմության մեջ + Shorts-ի ընտվացիչ + Ընտրեք, թե որ բաղադրիչները հայտնվեն Shorts-ի նվագարկիչում + Անջատել կրկնակի հպումը՝ հավանելու համար + Թաքցնել AI կոճակը + Թաքցնել «Ավտոմատ թարգմանված» պիտակը + Թաքցնել ալիքի զուկը + Թաքցնել «Մեկնաբանություններ» կոճակը + Թաքցնել էֆեկտի կոճակը + Թաքցնել տեսանյութի հղման պիտակը + Թաքցնել ժեստերի հուշումները + Թաքցնել «Կանաչ էկրան» կոճակը + Թաքցնել հեշթեգի կոճակը + Թաքցնել տեղեկատվական վահանակը + Թաքցնել Միանալ կոճակը + Թաքցնել «Հավանել» կոճակը + Թաքցնել «Հավանել» կոճակի շռայլության անիմացիան + Թաքցնել ուղիղ եթերների նախադիտումը + Թաքցնել տեղադրության պիտակը + Թաքցնել navigացիայի զուկը + Թաքցնել \"Նոր գրառումներ\" կոճակը + Թաքցնել դադարեցման վերապատկերման կոճակները + Թաքցնել նախադիտման մեկնաբանությունը + Թաքցնել «Ռեմիքս» կոճակը + Թաքցնել «Պահպանել» կոճակը + Թաքցնել \"Պահպանել երաժշտությունը\" կոճակը + Թաքցնել որոնման առաջարկությունները + Թաքցնել «Կիսվել» կոճակը + Թաքցնել Խանութի կոճակը + Թաքցնել «Ձայն» կոճակը + Թաքցնել ձայնի մետատվյալների պիտակը + Թաքցնել «Բաժանորդագրվել» կոճակը + Թաքցնել «Գնել Super Thanks» կոճակը + Թաքցնել թեգված ապրանքները + Թաքցնել «Առաջիկա» կոճակը + Թաքցնել \"Օգտագործել այս ձայնը\" կոճակը + Թաքցնել \"Օգտագործել այս կաղապարը\" կոճակը + Թաքցնել տեսանյութի վերնագիրը + Թաքցնել վերջնական էկրանի առաջարկվող տեսանյութը + "Թաքցնում է առաջարկվող տեսանյութի շերտը, որը հայտնվում է տեսանյութի ավարտից հետո + +Այս կարգավորումն աշխատում է միայն, եթե ավտոմատ միացումն անջատված է: Դուք կարող եք փոխել այն YouTube-ում. +Կարգավորումներ → Վերարտադրում (կամ Ավտոմատ միացում) → Ավտոմատ միացնել հաջորդ տեսանյութը" + Թաքցնել կապված տեսանյութերը + Թաքցնել «Ավելի շատ տեսանյութեր» շերտը + Թաքցնում է «Ավելի շատ տեսանյութեր» շերտը, որը ցուցադրվում է լիអេկրան ռեժիմում վերև սահսկելիս + Թաքցնել տեսանյութի ժամանակային նշումը + Անջատել նվագարկիչի ցատկող վահանակները + Անջատում է and ಪլեյլիստի և ուղիղ չատի վահանակների ավտոմատ բացումը + Անջատել and הפլեյլիստի ավտոմատ միացումը + Կանխում է պլեյլիստերի ավտոմատ անցումը հաջորդ տեսանյութին + Անջատել «Շարունակե՞լ դիտումը» հարցումը + Դուրս գալ լիք էկրանի ռեժիմից տեսանյութի ավարտին + Անջատված է + Միայն ուղղահայաց + Միայն հորիզոնական + Դիվերտիզիոն և լանդշաֆտ + Հարկադիր լանդշաֆտ ռեժիմ լիអេկրանով + Պտտել նվագարկիչը լանդշաֆտ ռեժիմի, երբ անցնում եք լիអេկրանի ռեժիմի: Անհասանելի է փոքր էկրանների համար, որոնք արդեն բացվում են լիអេկրանով լանդշաֆտ ռեժիմում + Լրամասշտաբ ժեստեր + Նվագարկի ժեստերը լիաէկրան ռեժիմում + Անջատել դիվերտիվ վերև սահեցման ժեստը + Անջատել դիվերտիվ ներքև սահեցման ժեստը + Անջատել հորիզոնական էկրանի վայրս սահեցման ժեստը + Անջատել մատներով խยายման ժեստը + Վերականգնել մատներով մեծացումը + Բացել տեսանյութերը լիաէկրան ռեժիմով + Անջատված + Դիվերտիզիա + Հորիզոնական + Լրամասն էկրանի տեսանյութի մասշտաբ + Լռելյայն + Ձգել (լրացնել էկրանը) + Մեծացնել (կտրել լրացնելու համար) + Ցուցադրել տեսանյութի մասշտաբի կոճակը + Հպեք՝ փոխելու Լռելյայն, Ձգել և Մեծացնել տարբերակների միջև + Ցույց տալ միայն լրամասշտաբային ռեժիմում + Թաքցնել տեսահոլովակի մասշտաբի կոճակը դիվերտիզացիոն նվագարկիչում + Ցույց տալ «Մուտքի անջատում» կոճակը + Պլեյերի overlay-ի թափանցիկությունը + Կարգավորեք թափանցիկության մակարդակը (0-ն թափանցիկ է, 100-ը՝ անթափանցիկ) + Որոնման տող + Անջատված + Լայն + Չափազանց լայն + Սարքի դասավորություն + Լռելյայն + Հեռախոս + Պլանշետ + Ավտոմൊբային + "Փոփոխությունները ներառում են + +Պլանշետի դասավորություն +• Համայնքի գրառումները թաքցված են +• Հոսքերում միացնելու կարգավորումը հասանելի չէ +• Remix կոճակը և Ձայնի կոճակը հասանելի չեն Shorts-ում +• «Ավելի շատ տեսանյութեր» շերտը կարող է հասանելի չլինել +• Ցուցադրվում է տեսանյութի հին գործողությունների չփոքրացված պատուհանը +• Ցանկի տարբերակները կարող են ճիշտ չաշխատել + +Ավտոմոբի դասավորություն +• Հոսքը կազմակերպված է ըստ թեմաների և ալիքների +• Հոսքերում միացնելու կարգավորումը հասանելի չէ +• Remix կոճակը և Ձայնի կոճակը հասանելի չեն Shorts-ում +• «Ավելի շատ տեսանյութեր» շերտը կարող է հասանելի չլինել +• Shorts-երը բացվում են սովորական նվագարկիչով +• Ցուցադրվում է տեսանյութի հին գործողությունների չփոքրացված պատուհանը" + Միացնել պլանշետի դասավորությունը նվագարկիչում + "Վերականգնում է նվագարկիչի հին դասավորությունը + +Սահմանափակում. Եթե \"Վերականգնել տեսանյութի գործողությունների հին տողը\" տարբերակն անջատված է, \"Ավելի շատ տեսանյութեր\" շերտը կարող է հասանելի չլինել" + 21.11.48 - Վերականգնել ենթագրերի հին վարքագիծը + 20.39.41 - Վերականգնել հին պահպանումը ցուցակում + 20.28.41 - Անջատել թարթի սիկոնները + 20.13.41 - Վերականգնել ալիքի անվանումը տեսանյութի նվագարկիչի տակ + Բացել համակարգի կիսման պատուհանը + Բովանդակությունը կիսելիս բացում է համակարգի կիսման պատուհանը՝ հավելվածի պատուհանի փոխարեն + Գերակայել YouTube Music-ի կոճակները + Բացում է ձեր նախընտրած երաժշտական հավելվածը YouTube Music-ի ստանդարտ հավելվածի փոխարեն + Երաժշտական հավելվածի փաթեթի անվանումը + Ձեր տեղադրված երաժշտական հավելվածի փաթեթի անվանումը + Մեկնարկային էջ + Լռելյայն + Բոլոր բաժանորդագրությունները + Դիտել ալիքները + Դասընթացներ / Ուսում + Ուսումնասիրել + Նորաձևություն և գեղեցկություն + Գեյմինգ + Պատմություն + Գրադարան + Հավանած տեսանյութեր + Ուղիղ եթեր + Ֆիլմեր + Երաժշտություն + Լուրեր + Ծանուցումներ + Ցուցակներ + Որոնում + Գնումներ + Shorts + Սպորտ + Բաժանորդագրություններ + Թրենդներ + Վիրտուալ իրականություն + Դիտել ավելի ուշ + Ձեր հատվածները + Անջատել միացման ժամանակ շարունակումը + Բացել Shorts-ը հետևյալով՝ + Shorts-ի նվագիչ + Սովորական ընտռայց + Սովորական ընտռայց լրիվ էկրանով + Shorts-երի ավտոմատ միացում + Ավտոմատ կերպով անցնում է հաջորդ Short-ին՝ ընթացիկը կրկնելու փոխարեն + Ավտոմատ միացում հետին պլանում + Հետին պլանում նվագարկման ժամանակ ավտոմատ կերպով անցնում է հաջորդ Short-ին + Բաց թողնել ուղիղ Shorts-երը + Բաց է թողնում ուղիղ հեռարձակումները, որոնք հայտնվում են Shorts-երի ленտայում + Վերարտադրումը ленտաներում + Միշտ միացված + Միայն Wi-Fi-ով + Անջատված + Վերականգնել բնօրինակ վերնագրերը + Ցուցադրում է տեսանյութերի բնօրինակ վերնագրերը, տեսանյութերի նկարագրությունները և ալիքի նկարագրությունները՝ ավտոմատ թարգմանվածների փոխարեն + Վերնագրի բեռնում… + Անջատել լիարժեք ավտոմատ թարմացումը + Կանխում է լիարժեքերի ավտոմատ թարմացումը, երբ դրանք հնացած են դառնում + Լրահոսի քարտի դասավորություն + Դասական + Քարտեր + Քարտեր՝ հավանելու, մեկնաբանելու և պահպանելու կոճակներով + Մեկնաբանություններ + Անջատել մինի-նվագիչի վերսկսումը + Անջատում է \"Շարունակել դիտումը\" մինիպլեյերը՝ YouTube Premium-ի հնարավորությունը, որը հայտնվում է հավելվածի միացման ժամանակ + Մինի-ընտռայց + Անհատականացրեք փոքրացված տեսանյութերի նվագարկիչի տեսքն ու վարքը + Մինի-ընտռայցի տիպը + Թաքցնել վերնագիրը և ալիքը + Անջատված + Լռելյայն + Մինիմալ + Մինիմալիստական 2 + Պլանշետ + Ժամանակակից 1 + Ժամանակակից 2 + Ժամանակակից 3 + Ժամանակակից 4 + Անջատել կլորացված անկյունները + Անջատել քաշել և գցել գործողությունը + Արգելում է փոքրացված նվագարկիչի քաշումը էկրանի տարբեր անկյուններ + Անջատել հորիզոնական քաշման ժեստը + Արգելում է մինի-նվագարկիչի փակումը՝ այն հորիզոնական ដែលն ուղղությամբ էկրանից դուրս քաշելով + Դադարեցրեք ընկացումը հորիզոնական քաշելով + Վերարտադրումը կդադարի, երբ մինիպլեյերը էկրանից դուրս կքաշվի + Հիշել քաշելու վիճակի դիրքը + Մինիպլեյերը էկրանից դուրս քաշելը կպահպանի դրա UI վիճակը + Թաքցնել վերնադիր կոճակները + Լռելյայն չափս + Սահմանեք էկրանի սկզբնական չափսը՝ պիքսելներով + Պիքսելների չափը պետք է լինի %1$s-ի և %2$s-ի միջև + Վերնադրի թափանցիկությունը + Կարգավորեք թափանցիկության մակարդակը (0-ն թափանցիկ է, 100-ը՝ անթափանցիկ) + «Հետ» կոճակը դուրս է բերում լողացող թղթապատիչից + «Հետ» կոճակը սեղմելը միշտ դուրս է բերում հավելվածից՝ լողացող թղթապատիչի վերև գնալու փոխարեն + Հարկադիր անիմացիաներ + Լռելյայն + Միշտ միացված + Միշտ անջատված + Թաքցնել կարգավիճակի տողը + Թաքցնում է համակարգի կարգավիճակի տողը: Քողարկեք վերևից ներքև՝ այն մի պահ ցուցադրելու համար + Ուղղել կարգավիճակի տողի ծածկելը վերին տողերի + Պահպանում է կարգավիճակի տողի դասավորությունը հետևողական, որպեսզի այն չծածկի վերին տողերը և չթողնի դատարկ տարածք նվագարկիչի վերևում: Անջատեք սա, եթե հավելվածը փակվում է կամ վերին տողերը սխալ են երևում + Լուսավոր թեմայի ֆոն + Սպիտակ + Material You (Սպիտակ) + Բաց վարդագույն + Բաց կապույտ + Բաց կանաչ + Բաց դեղին + Բաց նարնջագույն + Բաց կարմիր + Պատ춤ային լուսավոր ֆոն + Սահմանել լուսավոր թեմայի ֆոնի գույնը + Փոխել առաջին պլանի գույնը + Օգտագործել թեմայի գույնը տեքստի և սիկոնների համար + Միացնել գրադիենտային բեռնման էկրանը + Բեռնման էկրանին ցուցադրում է գրադիենտային ֆոն՝ միագույնի փոխարեն + Միացնել seekbar-ի հատուկ գույնը + Դիպայողի գույնը + Սահմանեք տեսանյութի առաջդիմության ստորի հիմնական գույնը + Seekbar-ի շեշտադրվող գույնը + Սահմանեք տեսանյութի առաջդիմության ստորի երկրորդական գույնը + Սլայդերի գույնի անվավեր արժեք + Վերնագրի լոգո + Լռելյայն + Սովորական + Անհատական + Շրջանցել պատկերների տարածաշրջանային սահմանափակումները + Վերահասցեագրում է նկարների հարցումները այլընտրանքային սերվեր սահմանափակում ունեցող երկրներում բացակայող պատկերները ուղղելու համար + DeArrow վերնագրեր + Գլխավոր էջ + Բաժանորդագրություններ + Դուք + Նվագարկիցի պլայլիստներ & հանդնարարություններ + Որոնման արդյունքները + Ցուցադրել DeArrow-ի սիմվոլը + Ցուցադրում է DeArrow-ի սիմվոլը հանրային ներդրումներով տեսանյութերի վերնագրերին + Casual ռեժիմ + Ցույց է տալիս բնօրինակ վերնագիրը DeArrow-ի վերնագրի փոխարեն, եթե DeArrow-ի օգտատերերը քվեարկել են, որ նախընտրում են բնօրինակ վերնագիրը + Բնօրինակ վերնագիրը ցույց տալու համար անհրաժեշտ minimal casual քվեների քանակը + Մեկ կատեգորիայում, օրինակ՝ զվարճալի կամ ստեղծագործական, անհրաժեշտ casual քվեների քանակը՝ բնօրինակ վերնագիրը ցույց տալու համար + DeArrow նկարահատվածները + Բնօրինակ նախապատկերներ + DeArrow և բնօրինակ նախապատկերներ + DeArrow և կադրեր + Անշարժ կադրեր + "DeArrow-ն փոխարինում է կլիկբեյթ վերնագրերը և նկարները համայնքի կողմից ստեղծված այնպիսիներով, որոնք ավելի ճշգրիտ են և պակաս զգայական: Վերնագրերը գրվում և քվեարկվում են համայնքի կողմից, իսկ նկարները կադրեր են տեսանյութից + +Տեսանյութերը հայցվում են հեշավորված տեսանյութի ID-ի կարճ նախդիրով, ուստի սերվերը չի կարող հստակ ասել, թե որ տեսանյութն եք դուք դիտում: Եթե տեսանյութը չունի DeArrow նկար, ցուցադրվում է բնօրինակը կամ կադրի լուսանկարը" + Ցուցադրել կապի սխալների մասին ծանուցումներ + Ցուցադրում է ծանուցում, երբ DeArrow սերվերը հասանելի չէ + API վերջնակետ + DeArrow-ի նկարների քեշի վերջնակետի URL-ը + Տեսանյութի անշարժ կադրեր + Կադրերը դեռևս վերցվում են յուրաքանչյուր տեսանյութի սկզբից / միջնամասից / վերջից: Այս պատկերները ներկառուցված են YouTube-ում, և արտաքին API-ներ չեն օգտագործվում + Հանել տեսանյութի կադրը + Սկիզբ + Միջին + Ավարտ + DeArrow-ը ժամանակավորապես հասանելի չէ (կարգավիճակի կոդը՝ %s) + DeArrow-ը ժամանակավորապես հասանելի չէ + Վերականգնել նվագարկիչի կոճակների հին ոճը + Վերադարձնում է տեսավիդեո նվագարկիչի հաստ կառավարման կոճակները հին, ավելի բարակ տեսողական ոճին + Կեղծել սարքի չափսերը + Սրա միացումը կարող է բացել տեսանյութերի ավելի բարձր որակներ + Սրա ակտիվացումը կարող է առաջացնել տեսանյութի վերարտադրման կտրումներ, մարտկոցի կյանքի կրճատում և անհայտ կողմնակի հետևանքներ + Հապտիկ հետադարձ կապ + Ընտրեք, թե որ փոխազդեցությունները պետք է գործադրեն սարքի թրթռում + Անջատել գլուխների հապտիկ արձագանքը + Անջատել ճշգրիտ փնտրման հապտիկ արձագանքը + Անջատել փնտրման չեղարկման հապտիկ արձագանքը + Անջատել սեղմելու և պահելու հապտիկ արձագանքը + Անջատել զոմի հապտիկ արձագանքը + Եթե դուք վերջերս փոխել եք ձեր հաշվի մուտքի տվյալները, ապա հեռացրեք և նորից տեղադրեք MicroG + Շրջանցել հղումների վերահասցեավորումները + Բացում է բնօրինակ հղումը ուղղակիորեն՝ շրջանցելով հետևողության հղումը (youtube.com/redirect) + Բացել հղումները բրաուզերում + Բացում է հղումները արտաքին բրաուզերում՝ հավելվածի ներսի բրաուզերի փոխարեն + Վերարտադրման բուֆերի չափը + Լռելյայն + Ցածր + Միջին + Առավելագույն + Տեսանյութի ուշացում + Հապաղում է տեսանյութը՝ այն համաժամանակյա պահելու համար ուշացած ձայնի հետ, օրինակ՝ Bluetooth ականջակալների դեպքում (0-5000 մվ) + Հապաղել միայն Bluetooth ձայնի դեպքում + Հետաձգում է տեսանյութը միայն Bluetooth աուդիոն միացված ժամանակ, որպեսզի հեռախոսի բարձրախոսը մնա համաժամանակյացված + Ցուցադրել տեսանյութի հետաձգման կոճակը + Սեղմեք և պահեք՝ հետաձգումը վերակայացնելու 0 մվ-ի + Ցուցադրել տեսանյութի հետաձգման մենյուն + Հետաձգումը միլիվայրկյաններով + Ավտոմատ + Հիշել տեսանյութի որակը + Կիրառում է որակի ձեր ձեռքով ընտրած արժեքը բոլոր հետագա տեսանյութերի համար + Որակի փոփոխության ծանուցումներ + Ցուցադրում է ծանուցում, երբ նոր լռելյայն որակը պահպանվում է + Տեսանյութի լռելյայն որակը Wi-Fi ցանցում + Տեսանյութի լռելյայն որակը բջջային ցանցում + Հիշել Shorts-ի որակը + Կիրառում է ձեր ընտրած որակը բոլոր հետագա Shorts-երի համար + Shorts-երի լռելյայն որակը Wi-Fi ցանցում + Shorts-երի լռելյայն որակը բջջային ցանցում + Բջջային լռելյայն որակը փոխվեց՝ %s + Wi-Fi լռելյայն որակը փոխվեց՝ %s + Shorts-ի բջջային որակը փոխվեց՝ %s + Shorts-ի Wi-Fi որակը փոխվեց՝ %s + Ցույց տալ վերարտադրման արագության կոճակը + Սեղմեք և պահեք արագությունը լռելյայն արժեքին վերադարձնելու համար + Ցույց տալ որակի կոճակը + Սեղմեք և պահեք որակը լռելյայն արժեքին վերադարձնելու համար + Ցուցադրել լուծաչափը Որակի կոճակի վրա + Ցուցադրում է լուծաչափը, օրինակ՝ 1080, փոխարեն պիտակի, օրինակ՝ FHD + Վերարտադրման արագության անհատականացված մենյու + Վերականգնել վերարտադրման արագության հին մենյուն + Վերարտադրման անհատական արագություններ + Մուտքագրեք վերարտադրման արագության ձեր նախընտրած տարբերակը + Վերարտադրման անհատական արագությունները պետք է լինեն %s-ից ցածր + Վերարտադրման անհատական արագությունների անվավեր արժեքներ + Ավտոմատ + Սեղմեք և պահեք վերարտադրման արագությունը + Սահմանեք արագության բազմապատկիչը էկրանին սեղմելիս և պահելիս, սահմանեք 0՝ անջատելու համար + Հիշել վերարտադրման արագությունը + Կիրառում է ձեր ընտրած արագությունը բոլոր հետագա տեսանյութերի համար + Արագության փոփոխության ծանուցումներ + Ցուցադրում է ծանուցում, երբ նոր լռելյայն նվագարկման արագությունը պահպանվում է + Վերարտադրման լռելյայն արագություն + Վերարտադրման լռելյայն արագությունը Shorts-երի համար + Հիշել վերարտադրման արագությունը Shorts-ում + Կիրառում է Shorts-երի արագության ձեր ձեռքով ընտրած արժեքը բոլոր հետագա Shorts-երի համար: Եթե անջատված է, արագությունը վերադառնում է լռելյայն արժեքին Shorts-երի նվագարկիչը փակելուց հետո + Ցուցադրել վերարտադրման արագությունը Shorts-ի մենյուում + Ավելացնում է վերարտադրման արագության կետ Shorts-ի նվագարկիչի մենյուում + Վերարտադրման արագություն · %s + Լռելյայն վերարտադրման արագությունը փոխվեց՝ %s + Օգտագործել սովորական արագությունը երաժշտության համար + Միշտ նվագարկել երաժշտական տեսանյութերը 1.0x արագությամբ + Ալիքների արագության սպիտակ ցուցակ + Սպիտակ ցուցակում ներառված ալիքների տեսանյութերը միշտ նվագում են 1.0x արագությամբ + Ավելացնել ալիքը արագության սպիտակ ցուցակում + Հեռացնել ալիքը արագության սպիտակ ցուցակից + Միացնել ձայնի տոնայնության կառավարումը + Վերարտադրման ձայնի լռելյայն տոնայնությունը + Ձայնի լռելյայն տոնայնությունը փոխվեց հետևյալի. %s + Ձայնի ժամանակային ձգում + Թույլ է տալիս առանձնահատուկ սահմանել արագությունը և տոնայնությունը, ինչը կարող է առաջացնել ձայնային արտեֆակտներ + Հիշել ուղիղ հեռարձակման նվագարկման դիրքը + Ընթացիկ ուղիղ հեռարձակումների համար հիշում և վերսկսում է ձեր նվագարկման դիրքը՝ փոխարենը ներկայիս ուղիղ դիրքին տեղափոխվելու + Այս ֆունկցիան չի աշխատում 24/7 ուղիղ հեռարձակումների և այլ «միշտ միացված» հեռարձակումների հետ + Անջատել HDR տեսանյութերը + Հարկադիր HDR տեսանյութ + Ստիպում է հավելվածին ճանաչել այս սարքը որպես HDR-ի հնարավորություն ունեցող + "Օգտագործեք սա, եթե HDR-ի վերծանումն աշխատում է, բայց YouTube-ը չի առաջարկում HDR, օրինակ՝ որոշ HyperOS սարքերի դեպքում, որոնք թաքցնում են HLG-ն: + +Սահմանափակումներ. +• Գույները կարող են սխալ երևալ, եթե էկրանն իրականում չի կարող ցուցադրել HDR +• Նվագարկումը կարող է ձախողվել, եթե վերծանողը չի կարող մշակել HDR-ը +• HDR-ը դեռևս կախված է տեսանյութերի հոսքերի կեղծող հաճախորդից" + Տեսանյութի կոդեկ + Լռելյայն + AVC-ն պարտադրելու համար փոխեք «Տեսանյութի հոսքերի կեղծումը»՝ «Android Studio», «Android VR» կամ «visionOS» տարբերակների + "VP9 +• Կարող է բարելավել մարտկոցի կյանքի տևողությունը և շտկել տեսանյութի կտտոցները +• Օգտագործում է ավելի շատ ինտերնետ տվյալներ, քան AV1-ը + +AVC (H.264) +• Պահանջում է «Android Studio», «Android VR» կամ «visionOS» տեսանյութի հոսքերի կեղծման հաճախորդ +• Կարող է էլ ավելի բարելավել մարտկոցի կյանքի տևողությունը +• Կարող է վերականգնել բացակայող տեսանյութի կետ resolution-ները հին սարքերի վրա +• Առավելագույն կետ resolution-ն է 1080p +• HDR տեսանյութերը կարող են չօգտագործել AVC +• Որոշ սարքեր չեն կարող պարտադրել AVC" + Բացել որակի ընդլայնված մենյուն + Բաց է թողնում որակի ընտրության մենյուն և անմիջապես բացում որակի առաջացած մենյուն + Թաքցնել Premium որակի տարբերակները + Առաջնահերթություն տալ տեսանյութի որակին + Տեսանյութի առկայ բարձրագույն որակն ավելի կարևոր է համարում, քան կոդեկի արդյունավետությունը + Եթե այս կարգավորումը անջատված է, ապա հին տեսանյութերի համար որակի ընտրության հնարավորությունները կարող են չլինել + Թույլատրել \'Android VR\' / visionOS AV1 + Թույլ է տալիս ծրագրային AV1 դեկոդավորում \'Android VR\' / visionOS հաճախորդն օգտագործելիս + "Այս կարգավորման ակտիվացումը կարող է օգտագործել AV1-ի ծրագրային դեկոդավորումը + +AV1-ով տեսանյութի վերարտադրությունը կարող է կտրվել կամ կադրեր բաց թողնել" + Կեղծման կողմնակի ազդեցությունները + • Վերարտադրումը կարող է ընդհատվել 1 րոպեի դեպքում + • \'Աուդիո գիծ\' մենյուն բացակայում է + • AV1 տեսանյութերի կոդեկի բացակայություն + • 360° VR ընկղմումային ռեժիմը հասանելի չէ + • Կայուն ձայնայնությունը հասանելի չէ + • Վերարտադրությունը կարող է ավարտվել 1 վայրկյան շուտ + • Եթե deobfuscation-ը ձախողվի, միացումը կարող է չաշխատել + • Բնօրինակ ձայնի 강제 කිරීමը հասանելի չէ + • Պահանջվում է մուտք գործել Android VR + • Որակը կարող է սահմանափակվել մինչև 720p + Player-ի JavaScript տարբերակը + Ցուցադրել կեղծումը \'Stats for nerds\'-ում + Վերականգնել տեսանյութի հին գործողությունների տողը + "Ուղղում է տեսանյութի գործողությունների կոճակների չթաքցվելու խնդիրը + +Սահմանափակում. վերադառնում է \"Ավելի շատ տեսանյութեր\" հին ոճի շերտին" + Նվագարկիկի ստվարակների ոճը + Ավտոմատ + Լռելյայն + Բարակ + Հաստ + Անհատական + Shorts-ի սիկոնի ոճը + Սերիալների հետևող + Հետևեք նվագացուցակներին որպես սերիալների և պահպանեք ձեր տեղը + Միացնել սերիալների հետևումը + Ավելացնում է Սերիալների էջ Պատմության բաժնում և «Հետևել սերիալին» տարբերակ նվագացուցակների համար + Ցուցադրել սերիաները նավիգացիայի վահանակում + Ցուցադրել Սերիալների հետևողի կոճակը + Ավելացնում է նվագարկիչի կոճակ՝ հետևելու կամ բացելու ընթացիկ տեսանյութի սերիալը + Գրանցել հետևվող սերիալների առաջընթացը + Գրանցել վերարտադրումը այս հեռախոսի վրա պահպանված սերիալների և նշումների համար + Չեղարկել + Պահպանել + Պահպանել նշումը + Սերիայի անվանումը + Սերիայի կարգավորումներ + Գրադարանի կարգավորումներ + Թարմացնել էպիզոդները + Վերանվանել + Բացել նվագացուցակը YouTube-ում + Հեռացնել գրադարանից + Նվագացուցակի հղումը կամ ID-ն + Մաքրել ողջ տեղական առաջընթացը + Մաքրել պատմությունը + Մաքրե՞լ ողջ տեղական առաջընթացը։ + Սա մաքրում է դիրքերը, դիտումների փոփոխությունները և վերսկսման ընտրությունները յուրաքանչյուր տեսանյութի համար: Պահպանված սերիաները և դրանց կատալոգները մնում են: Նոր վերարտադրությունը կձայնագրվի, եթե ձայնագրումն ակտիվ է + Նշել որպես դիտված + Հեռացնել դիտված նշումը + Օգտագործել ավտոմատ կարգավիճակը + Վերագործարկել 0:00-ից + Նորից նվագարկել 0:00-ից + Կարգավիճակը պահպանված է + Կրկնել պահպանումը + Նախորդ 100-ը + Հաջորդ 100-ը + Բացել պահպանված տեսանյութը + Տեղական առաջընթացը մաքրվել է + Բացեք «Դուք» բաժնի պատմությունը՝ ձեր սերիաները տեսնելու համար + Դիտման պատմություն + Սերիաներ + Բեռնվում են պահպանված սերիաները… + Հետևեք փլեյլիստին՝ դրա էպիզոդները հետևելու համար + Պլեյլիստի նշում + Թարմացվում են դրվագները… + Բոլոր սերիալները + Դիտված է + Բեռնվում են էպիզոդները… + Լրացուցիչ տարբերակներ + "Գրանցեք առաջդիմությունը միայն ձեր պահպանված սերիաների և նշումների մեջ ավելացված տեսանյութերի համար: + +Այս գրադարանը պահպանվում է այս սարքի վրա և հասանելի է բոլոր YouTube հաշիվների համար: YouTube-ի պատմության դադարեցման և մաքրման հսկիչները չեն փոխում այն: Ջնջելու համար օգտագործեք Սերիաներ → Ավելին → Մաքրել տեղական առաջդիմությունը: + +Ավտոմատ գրանցումը դադարում է գաղտնի ռեժիմում, երբ հաշվի տվյալները հասանելի չեն կամ երբ դուք անցնում եք մեկ այլ հաշվի: Միացրեք հետևումը նորից՝ այդ հաշվի հետ օգտագործելու համար:" + Հետևումը հնարավոր չէ միացնել գաղտնի ռեժիմում կամ նախքան YouTube-ի կողմից ընթացիկ հաշվի վիճակի բեռնումը: Վերադարձեք Գլխավոր էջ և փորձեք նորից: Մուտք գործելու անհրաժեշտություն չկա: + Միացնել տեղային հետևումը + Առաջընթացի գրանցումն անջատված է + Շարունակել %1$s-ից + Էպիզոդ %1$s · %2$s + Էպիզոդներ %1$s-%2$s %3$s-ից + Թարմացրեք՝ էպիզոդների բեռնումը փորձելու համար: Մասնավոր ցուցակների համար անհրաժեշտ է հաշվի հասանելիություն. դրանց նշումները դեռևս բացվում են YouTube-ում: + YouTube-ը չկարողացավ բացել այս էպիզոդը + YouTube-ը չկարողացավ բացել այսվարցանկը + Ընտրեք էպիզոդ + Թարմացված է + Չկան դիտելի էպիզոդներ + Պահպանված տեսանյութ · %1$s + Հետևել սերիալին + Տեսնել սերիալը + Շարունակել + Սկսել այստեղից + Մեկնարկային կետը պահպանված է + Դիտել եմ մինչև այստեղ + Էպիզոդները նշված են որպես դիտված + Չեղարկել + Միացնել + {\"one\":\"1 նոր էպիզոդ\",\"other\":\"{} նոր էպիզոդ\"} + Գրանցել առաջընթացը այս սարքի վրա + "Ձեր սերիալները և առաջընթացը մնում են այս սարքի վրա՝ հասանելի լինելով YouTube-ի հաշիվների համար: YouTube-ի պատմության կառավարման գործիքները չեն փոխում դրանք: Մաքրեք առաջընթացը «Սերիալներ» մենյուից: + +Ավտոմատ գրանցումը կամընտիր է: Այն դադարեցվում է գաղտնի ռեժիմում կամ հաշվի փոփոխության ժամանակ" + Էպիզոդները հնարավոր չէր բեռնել: Դուք կարող եք պահպանել վարցանկը որպես նշում + Չպահպանված փոփոխությունների քանակը չափազանց 많 է: Նախ փորձեք կրկին պահպանել + Պահպանումը ընթացքի մեջ է: Փորձեք կրկին + Էպիզոդների բեռնումը ընթացքի մեջ է: Փորձեք կրկին + Պահոցը հասանելի չէ: Ձեր պահպանված տվյալները պահպանվել են + Փոփոխությունները չհաջողվեցին պահպանել: Փորձեք կրկին պահպանել նախքան YouTube-ը փակելը + Այս սերիան արդեն պահպանված է, կամ դրաացուցակը փոխվել է: Բացեք այն նորից + Այս սերիան փոխվել է կամ հեռացվել: Բացեք գրադարանը նորից + Ցուցակը փոխվել է: Ընտրեք էպիզոդը նորից + Այս էպիզոդը հասանելի չէ: Ընտրեք մեկ այլ էպիզոդ + Մուտքագրեք սերիայի անվանումը + Մուտքագրեք YouTube-ի ցուցակի հղումը կամ ցուցակի ID-ն + Խնդրում ենք սպասել նախքան կրկին թարմացնելը + Էպիզոդները չհաջողվեց բեռնել: Փորձեք ավելի ուշ + Փոփոխությունը չհաջողվեց կատարել: Փորձեք նորից + Շարունակությունը չհաջողվեց հաստատել: Ստուգեք նվագարկիչը և փորձեք նորից + Հետևում եմ սերիալին + Այս տեսանյութը չկա այսបញ្ակցում + Փնտրվում են пլեյլիստները… + Տեղադրեք пլեյլիստի հղումը + Չհաջողվեց գտնել пլեյլիստը + Հասանելի էպիզոդներ չկան + Թաքցնել դիտվածները + Նշել նախորդ էպիզոդները որպես դիտված + Սկսվում է՝ %1$s + Էպիզոդների հերթականություն + Պլեյլիստի հերթականություն + Հակառակ պլեյլիստի հերթականություն + %1$s դիտում + %1$s դիտում + Նշել դիտված էպիզոդները + Դիտված է առնվազն %1$s%%, כאשר մնացել է առավելագույնը 90 վայրկյան, կամ մնացել է %2$s վայրկյան + Ավտոմատ կերպով նշեք էպիզոդը որպես դիտված ցանկացած շեմին հասնելուց հետո: Տոկոսային կանոնը նաև պահանջում է, որ մնացած ժամանակը չգերազանցի 90 վայրկյանը, որպեսզի երկար էպիզոդների վերջը բաց չթողնվի: Մնացած ժամանակի կանոնը կիրառվում է մեկ րոպեից ավելի երկար տեսանյութերի համար. սահմանեք այն 0-ի, որպեսզի անջատեք այն: Փոփոխությունները կիրառվում են առաջիկա առաջընթացի թարմացումների համար: Ձեռքով կատարված դիտված/չդիտված ընտրությունները պահպանվում են + Դիտված տոկոսը (1-100) + Մնացած վայրկյանները (0-300; 0-ն անջատում է) + Մուտքագրեք արժեք %1$s-ից %2$s-ի միջակայքում + Օգտագործել YouTube-ի առաջընթացը + Շարունակել հետևված սերիալները այլ սարքով դիտելուց հետո + Կարդալ դիտված կարգավիճակը և վերսկսման դիրքերը ձեր հետևած սերիալների համար ընթացիկ YouTube հաշվից: Այս հեռախոսում պահվում են միայն համապատասխանող էպիզոդները: Ստուգումները կատարվում են, երբ սերիալների գրադարանը բաց է և նախքան «Շարունակել»-ը: YouTube-ի պատմությունը պետք է միացված լինի ձեր մյուս սարքի վրա + Ստուգվում է YouTube-ի առաջընթացը… + YouTube-ի առաջընթացը հասանելի չէ: Օգտագործվում է պահպանված առաջընթացը + YouTube-ի որոշ առաջընթացներ հասանելի չեն: Պահպանված դիրքերը մնում են: + Պահուստային պատճենում և վերականգնում + Պահպանեք ձեր սերիալների գրադարանը ֆայլի մեջ կամ վերականգնեք պահուստային պատճենը + Պահպանում է հետևած սերիալները, էպիզոդների դիրքերը և դիտված ընտրությունները: Ձայնագրման և YouTube-ի առաջընթացի թույլտվությունները ներառված չեն: + Պահպանել պահուստային պատճենը + Վերականգնել պահուստային պատճենը + Ավելացնել %1$s սերիալ: Ձեր գրադարանում արդեն եղած %2$s սերիալները պահպանվում են: Վիդեոների առկա առաջընթացը պահպանվում է: + Սերիալների պահուստային պատճենը պահպանված է + Վերականգնված է %1$s սերիալ + Այս ֆայլը սերիալների պահուստային պատճենի աջակցվող ձևաչափ չէ կամ չափազանց մեծ է: Ձեր գրադարանը չի փոփոխվել + Չհաջողվեց պահպանել պահուստային պատճենը: Փորձեք այլ տեղ + Սերիան հեռացված է + Այս էպիզոդն այժմ հայտնվում է մեկից ավելի անգամ: Ընտրեք, թե որից շարունակել + Ձեր պահպանված էպիզոդն այլևս չկա այս плейլիստում: Ձեր պահպանված դիրքը մնացել է + Շարունակել %1$s էպիզոդից · %2$s + Դուք կարող եք նաև ընտրել ցանկացած էպիզոդ ստորև: Նախորդ էպիզոդները չեն փոփոխվել + Այս плейլիստը փոփոխվել է: Բացեք սերիալը՝ ընտրելու համար, թե որտեղից շարունակել + Այս ուղղությամբ էպիզոդներ այլևս չկան + Շարունակել %1$s + Փնտրել սերիալներ + Մաքրել որոնումը + Սերիալներ չեն գտնվել + Անվանշի էպիզոդ + Անհասանելի էպիզոդ + Niconico-ի մեկնաբանություններ + Ցուցադրել ուղիղ չատը և չատի կրկնությունը որպես մեկնաբանություններ, որոնք սահում են տեսանյութի վրայով + Ցուցադրում է վերապատկերված կոճակ տեսանյութի նվագարկիչում: Հպեք՝ մեկնաբանությունները ցուցադրելու կամ թաքցնելու համար, սեղմեք և պահեք՝ արագ կարգավորումների համար + Մեկնաբանության տառատեսակի չափսը + Մեկնաբանությունների տեքստի չափսը (12-40 sp) + Մեկնաբանության թափանցիկությունը + Մեկնաբանության տեքստի և ֆոնների թափանցիկությունը + Շարժման տևողությունը + Քանի վայրկյան է պահանջվում մեկնաբանություններին անցնել էկրանով: Ավելի ցածր արժեքները շարժվում են ավելի արագ + Մեկնաբանության առավելագույն երկարությունը + Թաքցնում է այն մեկնաբանությունները, որոնք ունեն այսքանից ավելի նիշեր: Յուրաքանչյուր էմոջին հաշվարկվում է որպես մեկ նիշ + Առանց սահմանափակման diff --git a/patches/src/main/resources/addresources/values-in-rID/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-in-rID/shared-youtube/strings.xml index c14ff880e0..64c1a0f440 100644 --- a/patches/src/main/resources/addresources/values-in-rID/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-in-rID/shared-youtube/strings.xml @@ -18,10 +18,12 @@ Second \"item\" text" --> - Animasi startup - Dinonaktifkan - Berwarna - Monokrom + Animasi startup + Dinonaktifkan + Berwarna + Warna Morphe + Monokrom + Monokrom Morphe Mulai ulang untuk menyelesaikan penyiapan diff --git a/patches/src/main/resources/addresources/values-in-rID/youtube/strings.xml b/patches/src/main/resources/addresources/values-in-rID/youtube/strings.xml index 010cff55ac..c13c7204f8 100644 --- a/patches/src/main/resources/addresources/values-in-rID/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-in-rID/youtube/strings.xml @@ -837,8 +837,6 @@ Untuk mengatasinya, aktifkan 'Palsukan aliran video' dan ubah 'Klien bawaan' men Jangan pernah tampilkan Tampilkan saat audio tidak dibisukan Tampilkan saat audio dibisukan - Tampilkan teks otomatis - Sembunyikan teks otomatis Bahasa teks pilihan Memilih subtitle dari penyedia terlebih dahulu, lalu menerjemahkan secara otomatis ke bahasa pilihan @@ -1249,6 +1247,7 @@ Batasan: Jika 'Kembalikan bilah tindakan video lama' dinonaktifkan, hamparan 'Vi Perbaiki status bar yang menutupi bar atas Menjaga tata letak status bar konsisten, sehingga tidak menutupi bar atas dan tidak meninggalkan ruang kosong di atas pemutar. Matikan ini jika aplikasi mengalami crash atau bar atas terlihat salah + Bercahaya Latar belakang tema terang Putih Material You (Putih) @@ -1271,6 +1270,9 @@ Batasan: Jika 'Kembalikan bilah tindakan video lama' dinonaktifkan, hamparan 'Vi Atur warna sekunder bilah kemajuan video Nilai warna bilah geser tidak sah + Analog asli + Kue asli + Logam asli Logo Header Bawaan diff --git a/patches/src/main/resources/addresources/values-is-rIS/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-is-rIS/shared-youtube/strings.xml index 1b1878992c..c192a5339a 100644 --- a/patches/src/main/resources/addresources/values-is-rIS/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-is-rIS/shared-youtube/strings.xml @@ -18,10 +18,12 @@ Second \"item\" text" --> - Ræsingarhreyfimynd - Óvirkt - Litur - Einlit + Ræsingarhreyfimynd + Óvirkt + Litur + Litur Morphe + Einstafengur + Einstafengur Morphe Endurræstu til að lokar á uppsetninguna diff --git a/patches/src/main/resources/addresources/values-is-rIS/youtube/strings.xml b/patches/src/main/resources/addresources/values-is-rIS/youtube/strings.xml index 7e13f08816..72a143f6e2 100644 --- a/patches/src/main/resources/addresources/values-is-rIS/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-is-rIS/youtube/strings.xml @@ -825,8 +825,6 @@ Til að laga þetta skaltu kveikja á 'Spoofa myndstreymi' og breyta 'Sjálfgefn Sýna aldrei Sýna þegar hljóðlaust Sýna þegar hljóðlát - Sýna sjálfvirka undirtitla - Fela sjálfvirka undirtitla Æskilegt skjátextamál Velur skjátexta frá veitanda fyrst, þýðir síðan sjálfkrafa á æskilegt tungumál @@ -1218,6 +1216,7 @@ Takmörkun: Ef 'Endurskapa gamla stjórnbara myndbands' er slökkt, gæti 'Fleir Fixa stöðustiku sem þekur efri tækin Heldir útliti stöðustikunnar í samræmi, svo hún þeki ekki efri tækin og skilur eftir sig enginn tómarúm ofan við spilarann. Slökktu á þessu ef forritið brotnar eða efri tækin líta ranglega út + Glóð Ljósþemabakgrunnur Hvítur Material You (hvítur) @@ -1240,6 +1239,9 @@ Takmörkun: Ef 'Endurskapa gamla stjórnbara myndbands' er slökkt, gæti 'Fleir Stilltu aukalitinn á vídeóleiðarstikunni Ógild litagildi litastiku + Upprunaleg analog + Upprunaleg kaka + Upprunaleg málm Logo haus Sjálfgefið diff --git a/patches/src/main/resources/addresources/values-it-rIT/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-it-rIT/shared-youtube/strings.xml index a2e5b01f8f..0a0640aaa8 100644 --- a/patches/src/main/resources/addresources/values-it-rIT/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-it-rIT/shared-youtube/strings.xml @@ -18,10 +18,12 @@ Second \"item\" text" --> - Animazione di avvio - Disattivato - Colore - Monocromatico + Animazione di avvio + Disattivato + Colore + Color Morphe + Monocromatico + Morphe monocromatico Riavvia per completare la configurazione diff --git a/patches/src/main/resources/addresources/values-it-rIT/youtube/strings.xml b/patches/src/main/resources/addresources/values-it-rIT/youtube/strings.xml index a8bc61acd1..3f749e2a7a 100644 --- a/patches/src/main/resources/addresources/values-it-rIT/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-it-rIT/youtube/strings.xml @@ -837,8 +837,6 @@ Per risolvere, attiva 'Spoof dei flussi video' e cambia il 'Client predefinito' Mai mostrare Mostra con audio Mostra quando silenzioso - Mostra i sottotitoli automatici - Nascondi i sottotitoli automatici Lingua preferita per i sottotitoli Seleziona prima i sottotitoli del provider, poi li traduce automaticamente nella lingua preferita @@ -1249,6 +1247,7 @@ Limitazione: se \"Ripristina la vecchia barra delle azioni video\" è disattivat Correggi la barra di stato che copre le barre superiori Mantiene coerente il layout della barra di stato, in modo che non copra le barre superiori e non lasci spazio vuoto sopra il player. Disattiva questa opzione se l\'app si arresta in modo anomalo o se le barre superiori appaiono errate + Bagliore Sfondo del tema chiaro Bianco Material You (Bianco) @@ -1271,6 +1270,9 @@ Limitazione: se \"Ripristina la vecchia barra delle azioni video\" è disattivat Imposta il colore secondario della barra di avanzamento del video Valore del colore della barra di avanzamento non valido + Analogico originale + Torta originale + Metallo originale Logo dell\'intestazione Predefinito diff --git a/patches/src/main/resources/addresources/values-iw-rIL/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-iw-rIL/shared-youtube/strings.xml index 2ce288d602..128d3c2bd3 100644 --- a/patches/src/main/resources/addresources/values-iw-rIL/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-iw-rIL/shared-youtube/strings.xml @@ -18,10 +18,12 @@ Second \"item\" text" --> - אנימציית אתחול - מושבת - צבע - מונוכרום + אנימציית אתחול + מושבת + צבע + Color Morphe + מונוכרום + Monochrome Morphe הפעל מחדש כדי לסיים את ההגדרות diff --git a/patches/src/main/resources/addresources/values-iw-rIL/youtube/strings.xml b/patches/src/main/resources/addresources/values-iw-rIL/youtube/strings.xml index 957a48e218..1108d764cb 100644 --- a/patches/src/main/resources/addresources/values-iw-rIL/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-iw-rIL/youtube/strings.xml @@ -837,8 +837,6 @@ Second \"item\" text" אל תהצג הצג כאשר הושתיק הצג בעת שתק - הצג כתוביות אוטומטיות - הסתר כתוביות אוטומטיות שפת כתוביות מועדפת בוחר תחילה בכתוביות של הספק, ולאחר מכן מתרגם אותן אוטומטית לשפה המועדפת @@ -1250,6 +1248,7 @@ Second \"item\" text" תיקון מסך החיווי שמכסה את הפסים העליונים שומר על עקביות תצורת מסך החיווי, כך שהוא לא יכסה את הפסים העליונים ולא יישאר רווח ריק מעל הנגן. כבה אפשרות זו אם האפליקציה קורסת או שהפסים העליונים נראים לא תקין + זוהר רקע של ערכת נושא בהירה לבן Material You (לבן) @@ -1272,6 +1271,9 @@ Second \"item\" text" הגדר את הצבע המשני של סרגל התקדמות הסרטון ערך צבע סרגל דילוג לא חוקי + אנלוג מקורי + עוגה מקורית + מתכת מקורית לוגו כותרת ברירת מחדל diff --git a/patches/src/main/resources/addresources/values-ja-rJP/music/strings.xml b/patches/src/main/resources/addresources/values-ja-rJP/music/strings.xml index c28e04a07d..a31113aba2 100644 --- a/patches/src/main/resources/addresources/values-ja-rJP/music/strings.xml +++ b/patches/src/main/resources/addresources/values-ja-rJP/music/strings.xml @@ -548,7 +548,7 @@ SABR再生(動画ストリームのスプーフがオフ)" ロック画面、Android Auto、Bluetooth で現在の行を表示。Android 11 以降が必要 ミニプレイヤーの歌詞を表示 ミニプレイヤーで現在行を表示し、トラックをサブタイトルに移動 - 曲名/アーティストの形式 + 曲名 / アーティスト名のフォーマット ミニプレイヤーおよびMediaSessionで歌詞と共に表示されるトラックの形式です。{t}は曲名、{a}はアーティスト、{v}は歌詞ソースが提供している場合のアコースティックなどのバージョンを表します。2つのコロンで囲まれたテキストは、その中のすべての変数に値がある場合にのみ表示されます。通常のコロンを表示するには\\:と入力してください。各行は歌詞が変わるたびに順番に表示されます 歌詞行の形式 ミニプレイヤーおよびMediaSessionにおける歌詞行の形式です。{lrc}は歌詞の行、{n}はその行が連続して歌われる回数を表します。コロンで囲まれたテキストは、行が繰り返される場合にのみ表示されます。コロンを表示するには\\:を使用してください diff --git a/patches/src/main/resources/addresources/values-ja-rJP/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-ja-rJP/shared-youtube/strings.xml index f58f4823f7..ac745e1b19 100644 --- a/patches/src/main/resources/addresources/values-ja-rJP/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ja-rJP/shared-youtube/strings.xml @@ -18,10 +18,12 @@ Second \"item\" text" --> - 起動時のアニメーション - 無効 - カラー - モノクロ + 起動時のアニメーション + 無効 + カラー + カラー Morphe + モノクロ + モノクロ Morphe 設定を完了するには再起動してください diff --git a/patches/src/main/resources/addresources/values-ja-rJP/youtube/strings.xml b/patches/src/main/resources/addresources/values-ja-rJP/youtube/strings.xml index 3b2bcc2561..3626cff102 100644 --- a/patches/src/main/resources/addresources/values-ja-rJP/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ja-rJP/youtube/strings.xml @@ -64,7 +64,7 @@ Second \"item\" text" アーティスト カードを非表示 検索結果の最上部に表示されるアーティストカードを非表示にします チャンネルボタンを非表示 - 検索結果のチャンネルプロフィールから「登録」「チャンネルを見る」「コミュニティ」「YouTube Music」などのボタンを非表示にします + 検索結果のチャンネル プロフィールから「登録」「チャンネルを見る」「コミュニティ」「YouTube Music」などのボタンを非表示にします コミュニティ投稿を非表示 コンパクトなバナーを非表示 コンパクトなバナーおよび一部のコンポーネント間のスペースを非表示にします @@ -349,9 +349,9 @@ Second \"item\" text" 制限事項: • AI 生成のショート動画を除外したときに空白が残る場合がある • 一部の AI 生成動画が除外されずに残ってしまう場合がある" - "AiSListは、AI生成コンテンツを公開するYouTubeチャンネルのオープンソースリストで、ボランティアによって維持されています。チャンネルはユーザーが報告し、追加前にレビューされます + "AiSList は、AI 生成コンテンツを公開する YouTube チャンネルのオープン ソース リストで、ボランティアによって維持されています。チャンネルはユーザーからの報告に基づいて追加され、審査を経て掲載されます -このリストはGitHubからダウンロードされ、動画はお使いのデバイス上でフィルタリングされます" +リストは GitHub からダウンロードされ、動画はお使いのデバイス上でフィルタリングされます" AI チャンネルを報告 動画のメニューに報告項目を追加 動画を投稿したチャンネルを AiSList に報告する項目を、動画のフライアウト メニューに追加します @@ -489,20 +489,20 @@ AI について扱っているだけのチャンネルや、動画の一部に A チャプターをスキップする、二本指でのダブルタップ ジェスチャーを無効にします。このジェスチャーが有効の場合、一本指でのダブルタップ ジェスチャーが二本指だと誤認識され、意図せずチャプターをスキップしてしまう場合があります サードパーティ製ダウンロード アプリで、動画、ポッドキャスト、楽曲をダウンロードします - プレーヤー オーバーレイに外部ダウンローダーを呼び出すボタンを表示します + プレーヤー オーバーレイにダウンロード ボタンを表示 ビデオ プレーヤーに、外部ダウンローダーを開くボタンを表示します - タップでクイックダウンロード - バックグラウンドでのダウンロードを促します。長押しするとダウンローダーが開きます。YTDLnisおよびYTindirで動作します - ダウンロード済みとして既読にする - 外部ダウンローダーでダウンロードされたビデオがYouTubeの視聴履歴に追加されます - フルHD(1080p) - 中(192kbps) - クイックダウンロード + タップでクイック ダウンロード + バックグラウンドでのダウンロードを促します。タップして長押しするとダウンローダーが開きます。YTDLnis と YTindir に対応しています + ダウンロード済みの動画を視聴済みとする + 外部ダウンローダーでダウンロードした動画が、YouTube の視聴履歴に追加されます + フル HD(1080p) + 中(192 kbps) + クイック ダウンロード 動画 オーディオ すべてのオプション - Shortsメニューでダウンロードを表示する - Shortsプレーヤーに外部ダウンローダーフライアウトメニュー項目を追加します + ショート動画メニューにダウンロードを表示 + ショート プレーヤーに外部ダウンローダーのフライアウト メニューの項目を追加します ダウンロード 吹き替え翻訳 @@ -649,7 +649,7 @@ APIキーは決して共有しないでください。アクセス権を持つ この機能には PotHelper の使用または「動画ストリームを偽装」を TV または Android VR に変更する必要がある場合があります フライアウトメニュー内の再生速度ホワイトリスト - その動画のチャンネルの動画を常に1.0倍速で再生するフライアウト項目を追加します + このチャンネルの動画を常に 1.0 倍速で再生するフライアウト項目を追加します ログインしていません プレイリスト ID がありません キューが空です @@ -838,8 +838,6 @@ APIキーは決して共有しないでください。アクセス権を持つ 常に非表示 ミュート解除中に表示 ミュート中に表示 - 自動字幕を表示 - 自動字幕を非表示 字幕の優先言語 提供元の字幕を最初に選択し、その後、優先言語に自動翻訳します @@ -1250,6 +1248,7 @@ Automotive レイアウト ステータスバーが上部のバーを覆っている問題を修正 ステータスバーのレイアウトを統一し、上部のバーが覆われないようにして、プレイヤーの上に空白ができないようにします。アプリがクラッシュしたり、上部のバーの表示がおかしい場合は、これをオフにしてください + グロウ ライトモードのテーマ ホワイト Material You(ホワイト) @@ -1272,6 +1271,9 @@ Automotive レイアウト 動画の現在の再生位置を示すプログレスバーのセカンドカラーを設定します シークバーの色の値が無効です + オリジナルアナログ + オリジナルケーキ + オリジナルメタル ヘッダーロゴ デフォルト @@ -1351,9 +1353,9 @@ Automotive レイアウト 最大 ビデオ遅延 - Bluetoothヘッドセットなど、ラグのある音声に合わせて映像を同期させるために映像を遅延させます(0〜5000 ms) - Bluetoothオーディオでのみ遅延させる - Bluetoothオーディオが接続されている間のみ映像を遅延させるため、電話のスピーカーは同期されたままになります + Bluetooth ヘッドホンなどを接続した際に、音声の遅延がある場合に、映像を遅らせて音声と同期させます(0〜5000 ms) + Bluetooth オーディオ接続時のみ遅延させる + Bluetooth オーディオが接続されている間のみ映像を遅延させます。そのため、スマートフォンのスピーカーの音は同期したままになります ビデオ遅延ボタンを表示 長押しして遅延を0 msにリセット ビデオ遅延メニューを表示 @@ -1382,7 +1384,7 @@ Automotive レイアウト 画質ボタンを表示 長押しすると、画質をデフォルトの値にリセットします 画質ボタンに解像度を表示 - FHDなどのラベルの代わりに、1080などの解像度を表示する + FHD などのラベルの代わりに、1080 などの解像度を表示します カスタム再生速度メニュー 以前の再生速度メニューに戻す @@ -1408,8 +1410,8 @@ Automotive レイアウト デフォルトの再生速度: %s 音楽動画を通常速度で再生 音楽動画を常に 1.0 倍速で再生します - チャンネル再生速度ホワイトリスト - ホワイトリスト登録されたチャンネルの動画は常に1.0倍速で再生されます + チャンネル単位での再生速度ホワイトリスト + ホワイトリストに登録されたチャンネルの動画は常に 1.0 倍速で再生されます チャンネルを再生速度ホワイトリストに追加 チャンネルを再生速度ホワイトリストから削除 音声ピッチ コントロールを有効化 @@ -1490,13 +1492,13 @@ AV1 での動画再生はカクつくか、フレームが欠落する可能性 カスタム ショート アイコンのスタイル - シリーズトラッカー - プレイリストをシリーズとして追跡し、現在地を維持します + シリーズ トラッカー + プレイリストをシリーズとしてフォローし、再生位置を保存します シリーズ追跡を有効化 - 履歴にシリーズページを追加し、プレイリストにシリーズフォローオプションを追加します - ナビゲーションバーでシリーズを表示 - シリーズトラッカーボタンを表示 - 現在の動画のシリーズを追跡または開くためのプレイヤーボタンを追加します + 履歴にシリーズページを追加し、プレイリストにシリーズをフォローするオプションを追加します + ナビゲーション バーにシリーズを表示 + シリーズ トラッカー ボタンを表示 + 現在の動画のシリーズをフォローまたは開くためのプレーヤーボタンを追加します フォロー中のシリーズの進捗を記録する 保存されたシリーズとブックマークに対して、この電話での再生を記録します キャンセル @@ -1601,7 +1603,7 @@ AV1 での動画再生はカクつくか、フレームが欠落する可能性 %1$s 回視聴 %1$s 回視聴 エピソードを視聴済みとしてマーク - 少なくとも %1$s%% 視聴し、最大 90 秒が残っているか、または %2$s 秒残り + 最低 %1$s%% 視聴かつ残り 90 秒以下、または残り %2$s 秒の時点 いずれかの基準を満たすと、エピソードを自動的に視聴済みとしてマークします。パーセント基準では残り時間が 90 秒以内であることも必要となるため、長編エピソードの最後部分はスキップされません。残り時間の基準は 1 分以上の動画に適用されます。無効にするには 0 に設定します。変更は今後の進捗状況更新に適用されます。手動での視聴済み/未視聴の選択は保持されます 視聴済み割合 (1-100) 残り秒数 (0-300; 0 で無効化) @@ -1636,7 +1638,7 @@ AV1 での動画再生はカクつくか、フレームが欠落する可能性 タイトルなしのエピソード 利用不可なエピソード - ニコニコ風コメント + ニコニコ動画風のコメント表示 ライブチャットとチャットのリプレイを、ビデオ上を流れるコメントとして表示します ビデオプレーヤーにオーバーレイボタンを表示します。タップするとコメントの表示/非表示を切り替え、長押しすると簡易設定を表示します コメントのフォントサイズ diff --git a/patches/src/main/resources/addresources/values-km-rKH/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-km-rKH/shared-youtube/strings.xml index dff62f8338..d88f93dad4 100644 --- a/patches/src/main/resources/addresources/values-km-rKH/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-km-rKH/shared-youtube/strings.xml @@ -18,7 +18,7 @@ Second \"item\" text" --> - បានផ្ដាច់ + បានផ្ដាច់ ឈ្មោះកម្មវិធី diff --git a/patches/src/main/resources/addresources/values-kmr-rTR/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-kmr-rTR/shared-youtube/strings.xml index dce17ab88b..51a0178559 100644 --- a/patches/src/main/resources/addresources/values-kmr-rTR/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-kmr-rTR/shared-youtube/strings.xml @@ -18,8 +18,8 @@ Second \"item\" text" --> - Vemirandî - Reng + Vemirandî + Reng Dest pê bike ku sazkirinê temam bike diff --git a/patches/src/main/resources/addresources/values-kmr-rTR/youtube/strings.xml b/patches/src/main/resources/addresources/values-kmr-rTR/youtube/strings.xml index 1753147d86..d84369e152 100644 --- a/patches/src/main/resources/addresources/values-kmr-rTR/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-kmr-rTR/youtube/strings.xml @@ -617,8 +617,6 @@ Info: May not work on live streams" Hîşka nîşan nedê Dema ku bêdeng dibe nîşan bide Dibe ku darvekirin - Nîşanê otomatîk - Qefalê otomatîk Dabe diff --git a/patches/src/main/resources/addresources/values-ko-rKR/music/strings.xml b/patches/src/main/resources/addresources/values-ko-rKR/music/strings.xml index 3f2f563b49..78e9ad9441 100644 --- a/patches/src/main/resources/addresources/values-ko-rKR/music/strings.xml +++ b/patches/src/main/resources/addresources/values-ko-rKR/music/strings.xml @@ -557,16 +557,16 @@ LOGIN_INFO" 잠금 화면, Android Auto 및 블루투스에서 현재 가사 줄을 표시합니다. Android 11 이상이 필요합니다 미니 플레이어 가사 표시하기 미니 플레이어에 현재 가사 줄을 표시하고, 노래 정보를 제목 하단으로 이동합니다 - 곡 제목/아티스트 형식 - 미니플레이어 및 MediaSession에서 가사와 함께 표시되는 트랙 형식입니다. {t}는 제목, {a}는 아티스트, {v}는 가사 소스에서 제공하는 경우 Acoustic과 같은 버전입니다. 두 개의 콜론 사이의 텍스트는 내부의 모든 변수에 값이 있을 때만 표시됩니다. 일반 콜론을 입력하려면 \\:를 작성하세요. 각 줄은 가사가 변경될 때마다 차례대로 표시됩니다 + 노래 제목/아티스트 형식 + 미니 플레이어와 MediaSession에서 가사와 함께 표시되는 트랙의 형식입니다. {t}는 노래 제목, {a}는 아티스트, {v}는 가사 소스에서 정보를 제공하는 경우 Acoustic과 같은 버전을 의미합니다. 두 개의 콜론 사이의 텍스트는 그 안에 있는 모든 변수가 값을 가지고 있을 때만 표시됩니다. 일반 콜론을 표시하려면 \\:를 입력하세요. 각 줄은 차례대로 표시되며, 가사가 바뀔 때마다 다음 줄로 넘어갑니다 가사 줄 형식 - 미니플레이어 및 MediaSession의 가사 줄 형식입니다. {lrc}는 해당 줄이고 {n}은 연속으로 노래하는 횟수입니다. 콜론 사이의 텍스트는 줄이 반복될 때만 표시됩니다. 콜론을 사용하려면 \\:를 사용하세요 + 미니 플레이어와 MediaSession에서 가사 한 줄이 표시되는 형식입니다. {lrc}는 가사 한 줄을, {n}은 해당 가사가 연속으로 반복되는 횟수를 의미합니다. 콜론 사이의 텍스트는 가사가 반복될 때만 표시됩니다. 콜론을 표시하려면 \\:를 사용하세요 형식에 {lrc}가 포함되어야 합니다 아티스트 라인 템플릿 및 애니메이션 타이밍 최소 화면 표시 시간 (ms) 최대 화면 표시 시간 (ms) - 다중 라인 애니메이션에만 적용됩니다. 각 프레임을 최소 시간 동안 유지합니다. 최대 시간에 도달하면 가사가 동일하더라도 아티스트 라인을 다음으로 넘깁니다. - 최소값은 0보다 크거나 같아야 합니다. 최대값은 0보다 커야 하며 최소값보다 크거나 같아야 합니다. + 여러 줄로 구성된 애니메이션에만 적용됩니다. 각 프레임은 최소 표시 시간 동안 유지됩니다. 최대 표시 시간에 도달하면 가사가 바뀌지 않더라도 아티스트 표시 줄로 넘어갑니다 + 최소값은 0 이상이어야 합니다. 최대값은 0보다 커야 하며, 최소값 이상이어야 합니다 라인 길이 제한 가사 및 아티스트 라인 제한, 전각 문자 가중치 "가사 라인 제한 @@ -577,9 +577,9 @@ LOGIN_INFO" 두 제한 사항 모두에 적용됩니다. 1.75로 설정하면 전각 문자 4개가 7개로 계산됩니다." 0에서 10000 사이의 정수 제한 값과 0보다 큰 문자 가중치를 입력하세요 연주 간주 - 연주 간주 중에 미니플레이어 및 MediaSession에 표시됩니다. 각 라인은 하나의 애니메이션 프레임입니다. {t}는 제목, {a}는 아티스트, {lrc}는 마지막 가사 라인입니다 - 미니플레이어 및 MediaSession 타이밍 오프셋 - 미니플레이어 및 MediaSession에서 다음 가사 줄이 표시되는 시점을 조정합니다. 잠금 화면 등에서 가사가 늦게 표시되는 경우 음수 값을 사용하세요 + 연주 구간에서 미니 플레이어와 MediaSession에 표시됩니다. 각 줄은 애니메이션의 한 프레임입니다. {t}는 노래 제목, {a}는 아티스트, {lrc}는 마지막 가사 줄을 의미합니다 + 미니 플레이어 및 MediaSession 타이밍 오프셋 + 미니 플레이어와 MediaSession에서 다음 가사 줄이 표시되는 시점을 조정합니다. 예를 들어 잠금 화면에서 가사가 늦게 표시된다면 음수 값을 사용하세요 필터 메타데이터 정규식 가사를 검색하기 전과 두 제목이 일치하는지 판단할 때 제목, 아티스트, 앨범에 적용할 정규식입니다. 비워 두면 두 기능 모두 비활성화됩니다. 기본값은 \'Official Video\', \'Remaster\', \'【插曲】\', \'- Topic\'과 같은 불필요한 문구를 제거합니다 diff --git a/patches/src/main/resources/addresources/values-ko-rKR/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-ko-rKR/shared-youtube/strings.xml index 3483ff5b44..25d68baed0 100644 --- a/patches/src/main/resources/addresources/values-ko-rKR/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ko-rKR/shared-youtube/strings.xml @@ -18,10 +18,12 @@ Second \"item\" text" --> - 앱 시작 애니메이션 - 사용 안함 - 컬러 - 흑백 + 앱 시작 애니메이션 + 사용 안함 + 컬러 + 컬러 Morphe + 흑백 + 모노크롬 Morphe 설정을 완료하려면 다시 시작하세요 diff --git a/patches/src/main/resources/addresources/values-ko-rKR/youtube/strings.xml b/patches/src/main/resources/addresources/values-ko-rKR/youtube/strings.xml index 8b7b231914..3442bef0db 100644 --- a/patches/src/main/resources/addresources/values-ko-rKR/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ko-rKR/youtube/strings.xml @@ -839,8 +839,6 @@ API 키는 절대로 다른 사람과 공유해서는 안 됩니다. API 키에 표시 안함 음소거 해제 시 표시 음소거 시 표시 - 자동 자막을 표시 - 자동 자막을 표시 안함 선호하는 자막 언어 설정 제공자가 제공하는 자막을 먼저 선택한 다음, 선호하는 언어로 자동 번역합니다 @@ -1120,7 +1118,7 @@ API 키는 절대로 다른 사람과 공유해서는 안 됩니다. API 키에 • '피드에서 재생' 설정을 사용할 수 없습니다 • Shorts에서 리믹스 버튼과 사운드 버튼을 사용할 수 없습니다 • '동영상 더보기' 오버레이가 표시되지 않을 수 있습니다 -• 접히지 않은 이전 동영상 작업 표시줄이 표시됩니다 +• 접히지 않은 이전 동영상 작업 버튼바가 표시됩니다 오토모티브 레이아웃 • 피드가 주제 및 채널별로 구성됩니다 @@ -1128,7 +1126,7 @@ API 키는 절대로 다른 사람과 공유해서는 안 됩니다. API 키에 • Shorts에서 리믹스 버튼과 사운드 버튼을 사용할 수 없습니다 • '동영상 더보기' 오버레이가 표시되지 않을 수 있습니다 • Shorts는 일반 플레이어에서 열립니다 -• 접히지 않은 이전 동영상 작업 표시줄이 표시됩니다" +• 접히지 않은 이전 동영상 작업 버튼바가 표시됩니다" 플레이어에서 태블릿 레이아웃 활성화하기 "이전 플레이어 레이아웃을 복원합니다 @@ -1251,6 +1249,7 @@ API 키는 절대로 다른 사람과 공유해서는 안 됩니다. API 키에 상단 막대가 상태 표시줄에 가려지는 것을 해결 상태 표시줄 레이아웃을 일정하게 유지하여 상단 바가 덮이지 않도록 하고 플레이어 위에 빈 공간이 생기지 않습니다. 앱이 강제 종료되거나 상단 바가 잘못 보이는 경우 이 기능을 끄세요 + 글로우 밝은 테마 배경 색상 하얀색 Material You (하얀색) @@ -1273,6 +1272,9 @@ API 키는 절대로 다른 사람과 공유해서는 안 됩니다. API 키에 동영상 진행률의 보조 색상을 설정할 수 있습니다 잘못된 재생바 색상입니다 + 원본 아날로그 + 오리지널 케이크 + 오리지널 메탈 헤더 로고 기본값 diff --git a/patches/src/main/resources/addresources/values-ky-rKG/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-ky-rKG/shared-youtube/strings.xml index 52a4b1f4ef..054a3d967c 100644 --- a/patches/src/main/resources/addresources/values-ky-rKG/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ky-rKG/shared-youtube/strings.xml @@ -18,7 +18,7 @@ Second \"item\" text" --> - Иштен чыгарылган + Иштен чыгарылган diff --git a/patches/src/main/resources/addresources/values-lt-rLT/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-lt-rLT/shared-youtube/strings.xml index 8562e74094..60f93a1943 100644 --- a/patches/src/main/resources/addresources/values-lt-rLT/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-lt-rLT/shared-youtube/strings.xml @@ -18,10 +18,12 @@ Second \"item\" text" --> - Paleidimo animacija - Išjungta - Spalva - Vienspalvė + Paleidimo animacija + Išjungta + Spalva + Spalvota Morphe + Vienspalvė + Monochromatinė Morphe Paleiskite iš naujo, kad užbaigtumėte sąranką diff --git a/patches/src/main/resources/addresources/values-lt-rLT/youtube/strings.xml b/patches/src/main/resources/addresources/values-lt-rLT/youtube/strings.xml index a0a7e39e2a..9973cb2560 100644 --- a/patches/src/main/resources/addresources/values-lt-rLT/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-lt-rLT/youtube/strings.xml @@ -837,8 +837,6 @@ Norėdami tai ištaisyti, įjunkite „Maskuoti vaizdo įrašų srautus“ ir pa Niekada nerodyti Rodyti, kai garsas įjungtas Rodyti, kai garsas išjungtas - Rodyti automatinius subtitrus - Slėpti automatinius subtitrus Pageidaujama subtitrų kalba Pirmiausia parenka teikėjo subtitrus, tada automatiškai išverčia į pageidaujamą kalbą @@ -1250,6 +1248,7 @@ Apribojimas: jei parinktis „Atkurti senąją vaizdo įrašo veiksmų juostą Ištaisyti būsenos juostą, dengiančią viršutines juostas Laiko būsenos juostos išdėstymą nuoseklų, kad ji nengeltų viršutinių juostų ir nepaliktų tuščios vietos virš grojimo įrenginio. Išjunkite šią parinktį, jei programėlė žlunga arba viršutinės juostos atrodo netinkamai + Švytėjimas Šviesios temos fonas Balta Material You (balta) @@ -1272,6 +1271,9 @@ Apribojimas: jei parinktis „Atkurti senąją vaizdo įrašo veiksmų juostą Nustatykite antrinę vaizdo įrašo eigos juostos spalvą Neteisinga slankiklio spalvos reikšmė + Originalus analogas + Originalus tortas + Originalus metalas Antraštės logotipas Numatyta diff --git a/patches/src/main/resources/addresources/values-lv-rLV/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-lv-rLV/shared-youtube/strings.xml index c849f8256a..45e6b0aefc 100644 --- a/patches/src/main/resources/addresources/values-lv-rLV/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-lv-rLV/shared-youtube/strings.xml @@ -18,7 +18,7 @@ Second \"item\" text" --> - Atspējots + Atspējots diff --git a/patches/src/main/resources/addresources/values-mai-rIN/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-mai-rIN/shared-youtube/strings.xml index 0589fed54e..bd6bb03297 100644 --- a/patches/src/main/resources/addresources/values-mai-rIN/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-mai-rIN/shared-youtube/strings.xml @@ -18,7 +18,7 @@ Second \"item\" text" --> - रंग + रंग सेटअप पूरा करबाक लेल पुनः आरंभ करू diff --git a/patches/src/main/resources/addresources/values-mai-rIN/youtube/strings.xml b/patches/src/main/resources/addresources/values-mai-rIN/youtube/strings.xml index e2ed146263..7574feda5a 100644 --- a/patches/src/main/resources/addresources/values-mai-rIN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-mai-rIN/youtube/strings.xml @@ -262,8 +262,6 @@ Second \"item\" text" ऑटो कैप्शन दृश्यता कविकैर नहि देखाइत म्यूटित कब स देखाइत अछि - ऑटो कैप्शन दिखाओ - ऑटो कैप्शन छिपाइ diff --git a/patches/src/main/resources/addresources/values-mk-rMK/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-mk-rMK/shared-youtube/strings.xml index e9e4b69bb0..9f4fa20d1f 100644 --- a/patches/src/main/resources/addresources/values-mk-rMK/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-mk-rMK/shared-youtube/strings.xml @@ -18,8 +18,8 @@ Second \"item\" text" --> - Оневозможено - Боја + Оневозможено + Боја Рестартирај за да завршиш со поставување diff --git a/patches/src/main/resources/addresources/values-mk-rMK/youtube/strings.xml b/patches/src/main/resources/addresources/values-mk-rMK/youtube/strings.xml index 38593d2eac..c0bc012951 100644 --- a/patches/src/main/resources/addresources/values-mk-rMK/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-mk-rMK/youtube/strings.xml @@ -393,8 +393,6 @@ Second \"item\" text" Никогаш не покажувај Прикажи кога е исклучен звук Покажи кога е исклучен звукот - Покажи автоматски титри - Скриј автоматски титри Исклучено diff --git a/patches/src/main/resources/addresources/values-mr-rIN/youtube/strings.xml b/patches/src/main/resources/addresources/values-mr-rIN/youtube/strings.xml index 47f65cacfd..3979ab1aeb 100644 --- a/patches/src/main/resources/addresources/values-mr-rIN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-mr-rIN/youtube/strings.xml @@ -583,8 +583,6 @@ OpenRouter.ai उघडण्यासाठी येथे टॅप करा कधीही दाखवू नका अनम्यूट केल्यावर दर्शवा शांत असताना दाखवा - ऑटो कॅप्शन दाखवा - ऑटो कॅप्शन लपवा diff --git a/patches/src/main/resources/addresources/values-ms-rMY/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-ms-rMY/shared-youtube/strings.xml index 0246379c09..d915a2ec76 100644 --- a/patches/src/main/resources/addresources/values-ms-rMY/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ms-rMY/shared-youtube/strings.xml @@ -18,8 +18,8 @@ Second \"item\" text" --> - Dilumpuhkan - Monokrom + Dilumpuhkan + Monokrom diff --git a/patches/src/main/resources/addresources/values-nb-rNO/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-nb-rNO/shared-youtube/strings.xml index ccb0e8531c..d353fabb63 100644 --- a/patches/src/main/resources/addresources/values-nb-rNO/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-nb-rNO/shared-youtube/strings.xml @@ -18,10 +18,12 @@ Second \"item\" text" --> - Oppstartsanimasjon - Deaktivert - Farge - Monokrom + Oppstartsanimasjon + Deaktivert + Farge + Farget Morphe + Monokrom + Monokrom Morphe Start på nytt for å fullføre oppsettet diff --git a/patches/src/main/resources/addresources/values-nb-rNO/youtube/strings.xml b/patches/src/main/resources/addresources/values-nb-rNO/youtube/strings.xml index 8dd912b233..ac1ef27c3c 100644 --- a/patches/src/main/resources/addresources/values-nb-rNO/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-nb-rNO/youtube/strings.xml @@ -837,8 +837,6 @@ For å fikse dette, slå på 'Forfalsk videostrømmer' og endre 'Standardklient' Aldri vis Vis når den er dempet Vises når dempet - Auto - Viser automatisk undertekst Foretrukket undertittelsspråk Velger leverandørens undertekster først, og oversetter deretter automatisk til det foretrukne språket @@ -1250,6 +1248,7 @@ Begrensning: Hvis 'Gjenopprett gammel videoaksjonslinje' er slått av, er det mu Fiks at statuslinjen dekker toppbaren Holder utseendet til statuslinjen konsistent, slik at den ikke dekker topparene og etterlater ingen tom plass over avspilleren. Skru dette av hvis appen krasjer eller topparene ser feil ut + Glød Bakgrunn for lyst tema Hvit Material You (Hvit) @@ -1272,6 +1271,9 @@ Begrensning: Hvis 'Gjenopprett gammel videoaksjonslinje' er slått av, er det mu Angi sekundærfargen på videofremdriftslinjen Invalid seekbar color value + Original analog + Original kake + Original metall Header logo Standard diff --git a/patches/src/main/resources/addresources/values-nl-rNL/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-nl-rNL/shared-youtube/strings.xml index 3b5389afa9..aecad96aef 100644 --- a/patches/src/main/resources/addresources/values-nl-rNL/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-nl-rNL/shared-youtube/strings.xml @@ -18,10 +18,12 @@ Second \"item\" text" --> - Opstartanimatie - Uitgeschakeld - Kleur - Monochroom + Opstartanimatie + Uitgeschakeld + Kleur + Kleur Morphe + Monochroom + Monochroom Morphe Herstarten om de installatie te voltooien diff --git a/patches/src/main/resources/addresources/values-nl-rNL/youtube/strings.xml b/patches/src/main/resources/addresources/values-nl-rNL/youtube/strings.xml index 7b641d7fb0..a703ffac4f 100644 --- a/patches/src/main/resources/addresources/values-nl-rNL/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-nl-rNL/youtube/strings.xml @@ -837,8 +837,6 @@ Om dit op te lossen, schakel 'Videostreams vervalsen' in en wijzig de 'Standaard Nooit tonen Weergeven wanneer gedempt Toon als gemuteerd - Toon automatisch ondertitels - Verberg automatisch ondertitels Voorkeurstaal voor ondertiteling Selecteert eerst de ondertiteling van de provider en vertaalt deze vervolgens automatisch naar de voorkeurstaal @@ -1248,6 +1246,7 @@ Beperking: Als 'Herstel oude video acties balk' is uitgeschakeld, is de \"Meer v Statusbalk oplossen die bovenste balken bedekt Houdt de lay-out van de statusbalk consistent, zodat deze de bovenste balken niet bedekt en er geen lege ruimte boven de speler overblijft. Schakel dit uit als de app crasht of als de bovenste balken verkeerd eruitzien + Gloed Achtergrond van het lichtthema Wit Material You (Wit) @@ -1270,6 +1269,9 @@ Beperking: Als 'Herstel oude video acties balk' is uitgeschakeld, is de \"Meer v Stel de secundaire kleur van de video voortgangsbalk in Ongeldige waarde voor de kleur van de schuifregelaar + Originele analoog + Originele cake + Origineel metaal Headerlogo Standaard diff --git a/patches/src/main/resources/addresources/values-pl-rPL/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-pl-rPL/shared-youtube/strings.xml index 3d78d27a36..d598291b3e 100644 --- a/patches/src/main/resources/addresources/values-pl-rPL/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-pl-rPL/shared-youtube/strings.xml @@ -18,10 +18,12 @@ Second \"item\" text" --> - Animacja startowa - Wyłączona - Kolorowa - Monochromatyczna + Animacja startowa + Wyłączona + Kolorowa + Kolorowy Morphe + Monochromatyczna + Monochromatyczny Morphe Uruchom ponownie, aby dokończyć konfigurację diff --git a/patches/src/main/resources/addresources/values-pl-rPL/youtube/strings.xml b/patches/src/main/resources/addresources/values-pl-rPL/youtube/strings.xml index ef8602c537..3bba86d9ec 100644 --- a/patches/src/main/resources/addresources/values-pl-rPL/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-pl-rPL/youtube/strings.xml @@ -837,8 +837,6 @@ Uwaga: Może nie działać na transmisjach na żywo" Nigdy nie pokazuj Pokazuj przy włączonym dźwięku Pokazuj przy wyciszonym dźwięku - Pokaż automatyczne napisy - Ukryj automatyczne napisy Preferowany język napisów Najpierw wybiera napisy dostawcy, a następnie automatycznie tłumaczy je na preferowany język @@ -1249,6 +1247,7 @@ Ograniczenie: Jeśli opcja \"Przywróć stary pasek akcji filmu\" jest wyłączo Napraw pasek stanu zasłaniający górne paski Utrzymuje spójny układ paska stanu, dzięki czemu nie zasłania on górnych pasków ani nie zostawia pustego miejsca nad odtwarzaczem. Wyłącz tę opcję, jeśli aplikacja ulega awarii lub górne paski wyświetlają się nieprawidłowo + Poświata Tło jasnego motywu Białe Material You (Biały) @@ -1271,6 +1270,9 @@ Ograniczenie: Jeśli opcja \"Przywróć stary pasek akcji filmu\" jest wyłączo Ustaw dodatkowy kolor paska postępu Nieprawidłowa wartość koloru + Oryginał analogowy + Oryginalny tort + Oryginalny metal Logo w nagłówku Domyślne @@ -1646,5 +1648,5 @@ Ograniczenie: Przywraca stary wygląd nakładki \"Więcej filmów\"" Ile sekund komentarze potrzebują, aby przemieścić się przez ekran. Niższe wartości oznaczają szybszy ruch Maksymalna długość komentarza Ukrywa komentarze, które mają więcej znaków niż ta wartość. Każdy emoji liczy się jako jeden znak - Brak limitu + Bez limitu diff --git a/patches/src/main/resources/addresources/values-pt-rBR/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-pt-rBR/shared-youtube/strings.xml index 2d5deb0be7..e551998867 100644 --- a/patches/src/main/resources/addresources/values-pt-rBR/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-pt-rBR/shared-youtube/strings.xml @@ -18,10 +18,12 @@ Second \"item\" text" --> - Animação de inicialização - Desativado - Cor - Monocromático + Animação de inicialização + Desativado + Cor + Morphe Colorido + Monocromático + Morphe Monocromático Reinicie para finalizar a configuração diff --git a/patches/src/main/resources/addresources/values-pt-rBR/youtube/strings.xml b/patches/src/main/resources/addresources/values-pt-rBR/youtube/strings.xml index 9da07b13d4..71a485a460 100644 --- a/patches/src/main/resources/addresources/values-pt-rBR/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-pt-rBR/youtube/strings.xml @@ -837,8 +837,6 @@ Para corrigir isso, ative 'Falsificar streams de vídeo' e altere o 'Cliente pad Não mostrar nunca Mostrar quando não estiver mudo Mostrar quando estiver mudo - Mostrar legendas automáticas - Esconder legendas automáticas Idioma preferido das legendas Seleciona primeiro as legendas do provedor e depois traduz automaticamente para o idioma preferido @@ -1249,6 +1247,7 @@ Limitação: Se 'Restaurar barra de ações de vídeo antiga' estiver desativado Corrige a barra de status cobrindo as barras superiores Mantém o layout da barra de status consistente, para que não cubra as barras superiores e não deixe espaço vazio acima do player. Desative esta opção se o aplicativo travar ou se as barras superiores parecerem incorretas + Brilho Fundo do tema claro Branco Material You (Branco) @@ -1271,6 +1270,9 @@ Limitação: Se 'Restaurar barra de ações de vídeo antiga' estiver desativado Defina a cor secundária da barra de progresso do vídeo Valor de cor da barra de progresso inválido + Analógico original + Bolo original + Metal original Logo do cabeçalho Padrão diff --git a/patches/src/main/resources/addresources/values-pt-rPT/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-pt-rPT/shared-youtube/strings.xml index 21fa579c25..5fe5f53ade 100644 --- a/patches/src/main/resources/addresources/values-pt-rPT/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-pt-rPT/shared-youtube/strings.xml @@ -18,10 +18,12 @@ Second \"item\" text" --> - Animação de inicialização - Desativado - Cor - Monocromático + Animação de inicialização + Desativado + Cor + Morphe Colorido + Monocromático + Morphe Monocromático Reiniciar para finalizar a configuração diff --git a/patches/src/main/resources/addresources/values-pt-rPT/youtube/strings.xml b/patches/src/main/resources/addresources/values-pt-rPT/youtube/strings.xml index 8b102a3785..cc27ce33e2 100644 --- a/patches/src/main/resources/addresources/values-pt-rPT/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-pt-rPT/youtube/strings.xml @@ -837,8 +837,6 @@ Para corrigir isso, ative 'Falsificar streams de vídeo' e altere o 'Cliente pad Não mostrar nunca Mostrar quando não está mudo Exibir quando silenciado - Exibir legendas automáticas - Ocultar legendas automáticas Idioma preferido das legendas Seleciona primeiro as legendas do provedor e depois traduz automaticamente para o idioma preferido @@ -1250,6 +1248,7 @@ Limitação: Se 'Restaurar a barra de ação de vídeo antiga' estiver desativad Corrigir barra de status cobrindo as barras superiores Mantém o layout da barra de status consistente, para que não cubra as barras superiores e não deixe espaço vazio acima do player. Desative isso se o aplicativo travar ou se as barras superiores estiverem incorretas + Brilho Cor de fundo do tema claro Branco Material You (Branco) @@ -1272,6 +1271,9 @@ Limitação: Se 'Restaurar a barra de ação de vídeo antiga' estiver desativad Defina a cor secundária da barra de progresso do vídeo Valor de cor de seekbar inválido + Análogo original + Bolo original + Metal original Logotipo do cabeçalho Padrão diff --git a/patches/src/main/resources/addresources/values-ro-rRO/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-ro-rRO/shared-youtube/strings.xml index fb02b192b9..64364fcb66 100644 --- a/patches/src/main/resources/addresources/values-ro-rRO/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ro-rRO/shared-youtube/strings.xml @@ -18,10 +18,12 @@ Second \"item\" text" --> - Animație de pornire - Dezactivat - Culoare - Monocrom + Animație de pornire + Dezactivat + Culoare + Morphe Color + Monocrom + Morphe Monocrom Reporniți pentru a finaliza configurarea diff --git a/patches/src/main/resources/addresources/values-ro-rRO/youtube/strings.xml b/patches/src/main/resources/addresources/values-ro-rRO/youtube/strings.xml index 8667535c8b..803c76a7b9 100644 --- a/patches/src/main/resources/addresources/values-ro-rRO/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ro-rRO/youtube/strings.xml @@ -837,8 +837,6 @@ Pentru a remedia, activează 'Falsifică fluxurile video' și schimbă 'clientul Nu arată niciodată Face bara de stare complet opacă Arată când mut - Arată subtitrări automate - Ascunde subtitrări automate Limba preferată pentru subtitrări Selectează mai întâi subtitrările furnizorului, apoi traduce automat în limba preferată @@ -1250,6 +1248,7 @@ Limitare: Dacă 'Restaurare bara de acțiuni video veche' este dezactivată, str Corectează bara de stare pentru a nu acoperi barele superioare Păstrează aspectul fermei de stare constant, astfel încât să nu acopere barele superioare și să lase spațiu liber deasupra player-ului. Dezactivează această opțiune dacă aplicația se blochează sau dacă barele superioare arată incorect + Strălucire Fundalul temei luminoase Alb Material You (Alb) @@ -1272,6 +1271,9 @@ Limitare: Dacă 'Restaurare bara de acțiuni video veche' este dezactivată, str Stabilește culoarea secundară a barei de progres video Valoare culoare bară căutare nevalidă + Analog original + Prăjitură originală + Metal original Siglă antet Implicit diff --git a/patches/src/main/resources/addresources/values-ru-rRU/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-ru-rRU/shared-youtube/strings.xml index 6fd28b5983..5223fc4e04 100644 --- a/patches/src/main/resources/addresources/values-ru-rRU/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ru-rRU/shared-youtube/strings.xml @@ -18,10 +18,12 @@ Second \"item\" text" --> - Анимация запуска - Выключено - Цветной - Монохромная + Анимация запуска + Выключено + Цветной + Цветной Morphe + Монохромный + Монохромный Morphe Перезагрузите для завершения настройки diff --git a/patches/src/main/resources/addresources/values-ru-rRU/youtube/strings.xml b/patches/src/main/resources/addresources/values-ru-rRU/youtube/strings.xml index 07c7340ca0..311bd56b6c 100644 --- a/patches/src/main/resources/addresources/values-ru-rRU/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ru-rRU/youtube/strings.xml @@ -839,8 +839,6 @@ Second \"item\" text" Никогда не показывать Показывать, когда звук отключен Показать при заглушенном звуке - Показать автоматические субтитры - Скрыть автоматические субтитры Предпочитаемый язык субтитров Сначала выбирает субтитры поставщика, затем автоматически переводит их на предпочитаемый язык @@ -1251,6 +1249,7 @@ Second \"item\" text" Исправлена строка состояния, закрывающая верхние панели Сохраняет неизменным расположение строки состояния, поэтому она не закрывает верхние панели и не оставляет пустого пространства над проигрывателем. Отключите, если приложение выходит из строя или верхние панели выглядят неправильно. + Свечение Фон светлой темы Белый Material You (белый) @@ -1273,6 +1272,9 @@ Second \"item\" text" Установите вторичный цвет полосы прогресса видео Неверное значение цвета полосы прогресса + Оригинальный аналог + Оригинальный торт + Оригинальный металл Логотип заголовка По умолчанию diff --git a/patches/src/main/resources/addresources/values-si-rLK/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-si-rLK/shared-youtube/strings.xml index 28161f2da8..e6cb21c4aa 100644 --- a/patches/src/main/resources/addresources/values-si-rLK/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-si-rLK/shared-youtube/strings.xml @@ -18,7 +18,7 @@ Second \"item\" text" --> - විසන්ධි කර ඇත + විසන්ධි කර ඇත diff --git a/patches/src/main/resources/addresources/values-sk-rSK/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-sk-rSK/shared-youtube/strings.xml index abec3440bc..89120f44c1 100644 --- a/patches/src/main/resources/addresources/values-sk-rSK/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-sk-rSK/shared-youtube/strings.xml @@ -18,10 +18,12 @@ Second \"item\" text" --> - Animácia spustenia - Vypnutý - Farebná - Monochrómny + Animácia spustenia + Vypnutý + Farebná + Farebné Morphe + Monochromatické + Monochromatické Morphe Reštartujte aplikáciu, aby sa dokončila konfigurácia diff --git a/patches/src/main/resources/addresources/values-sk-rSK/youtube/strings.xml b/patches/src/main/resources/addresources/values-sk-rSK/youtube/strings.xml index 798d7683a5..4d6e5bcaf0 100644 --- a/patches/src/main/resources/addresources/values-sk-rSK/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-sk-rSK/youtube/strings.xml @@ -837,8 +837,6 @@ Ak to chcete vyriešiť, zapnite 'Spoofovať video streamy' a zmeňte 'Predvolen Nikdy nezobrazovať Zobrazovať, keď je zvuk vypnutý Zobraziť, keď je zvuk stlmený - Zobraziť automatické titulky - Skryť automatické titulky Preferovaný jazyk titulkov Najprv vyberie titulky poskytovateľa a potom ich automaticky preloží do preferovaného jazyka @@ -1250,6 +1248,7 @@ Obmedzenie: Ak je vypnutá možnosť 'Obnoviť starú lištu akcií videa', vrst Opraviť stavový riadok, ktorý zakrýva horné panely Udržuje konzistentné rozloženie stavového riadka, takže nezakrýva horné panely a nenecháva prázdny priestor nad prehrávačom. Toto vypnite, ak sa aplikácia zrúti alebo horné panely vyzerajú nesprávne + Žiarenie Pozadie svetlého motívu Biela Material You (biely) @@ -1272,6 +1271,9 @@ Obmedzenie: Ak je vypnutá možnosť 'Obnoviť starú lištu akcií videa', vrst Nastavte sekundárnu farbu pruhu priebehu videa Neplatná farba ukazovateľa priebehu + Pôvodný analóg + Pôvodná torta + Pôvodný kov Logo hlavičky Predvolené diff --git a/patches/src/main/resources/addresources/values-sl-rSI/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-sl-rSI/shared-youtube/strings.xml index aaa521d350..761fcb443f 100644 --- a/patches/src/main/resources/addresources/values-sl-rSI/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-sl-rSI/shared-youtube/strings.xml @@ -18,10 +18,12 @@ Second \"item\" text" --> - Zagonska animacija - Onemogočeno - Barva - Enobarvno + Zagonska animacija + Onemogočeno + Barva + Barvni Morphe + Enobarvno + Enobarvni Morphe Za dokončanje nastavitve znova zaženite aplikacijo diff --git a/patches/src/main/resources/addresources/values-sl-rSI/youtube/strings.xml b/patches/src/main/resources/addresources/values-sl-rSI/youtube/strings.xml index 4fdc4e44cf..25c80f00a1 100644 --- a/patches/src/main/resources/addresources/values-sl-rSI/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-sl-rSI/youtube/strings.xml @@ -837,8 +837,6 @@ Opozorilo: morda ne deluje na prenosih v živo" Nikoli ne prikazuj. Prikaži, ko je izključen zvok Pokaži, ko je izklopljeno. - Pokaži samodejne podnapise. - Skrij samodejne podnapise. Prednostni jezik podnapisov Najprej izbere podnapise ponudnika, nato samodejno prevede v želeni jezik @@ -1250,6 +1248,7 @@ Omejitev: Če je možnost »Ponastavi staro vrstico za videoposnetke« izkloplje Odpravi pokritje zgornjih vrstic s strani vrstice stanja Ohranja dosledno zasnovo vrstice stanja, tako da ne prekriva zgornjih vrstic in ne pušča praznega prostora nad predvajalnikom. To funkcijo izklopite, če se aplikacija sesuje ali zgornje vrstice ne izgledajo pravilno + Sijaj Ozadje svetle teme Bela Material You (bela) @@ -1272,6 +1271,9 @@ Omejitev: Če je možnost »Ponastavi staro vrstico za videoposnetke« izkloplje Nastavite sekundarno barvo vrstice za napredovanje videa Neveljavna vrednost barvnega drsnika + Izvirni analog + Izvirna torta + Izvirni kovin Logotip naslova Privzeto diff --git a/patches/src/main/resources/addresources/values-sr-rCS/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-sr-rCS/shared-youtube/strings.xml index c8e7527822..d33449ff7f 100644 --- a/patches/src/main/resources/addresources/values-sr-rCS/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-sr-rCS/shared-youtube/strings.xml @@ -18,10 +18,12 @@ Second \"item\" text" --> - Animacija pri pokretanju - Onemogućen - Boja - Monohromni + Animacija pri pokretanju + Onemogućen + Boja + Kolorni Morphe + Monohromski + Monohromni Morphe Ponovo pokrenite da biste završili postavljanje diff --git a/patches/src/main/resources/addresources/values-sr-rCS/youtube/strings.xml b/patches/src/main/resources/addresources/values-sr-rCS/youtube/strings.xml index f1cd1ee145..dcec0e2ee3 100644 --- a/patches/src/main/resources/addresources/values-sr-rCS/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-sr-rCS/youtube/strings.xml @@ -837,8 +837,6 @@ Da biste to popravili, uključite 'Spoof video strimovi' i promenite 'Podrazumev Nikada ne prikaži Pokaži kada je zvuk isključen Prikaži kada mutiran - Prikaži automatske titlove - Sakrij automatske titlove Željeni jezik titlova Prvo bira titlove provajdera, a zatim ih automatski prevodi na željeni jezik @@ -1250,6 +1248,7 @@ Ograničenje: Ako je \"Vrati staru traku sa akcijama za video\" isključen, prek Popravi statusnu traku koja prekriva gornje trake Održava dosledan izgled statusne trake, tako da ne prekrije gornje trake i ostavlja prazan prostor iznad plejera. Isključite ovo ako aplikacija padne ili gornje trake izgledaju pogrešno + Sjaj Pozadina svetle teme Bela Material You (Bela) @@ -1272,6 +1271,9 @@ Ograničenje: Ako je \"Vrati staru traku sa akcijama za video\" isključen, prek Postavi sekundarnu boju trake za napredak videa Nevažeća vrednost boje trake za premotavanje + Originalni analog + Originalna torta + Originalni metal Logotip u zaglavlju Podrazumevani diff --git a/patches/src/main/resources/addresources/values-sr-rSP/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-sr-rSP/shared-youtube/strings.xml index 477f546b33..8c631f1752 100644 --- a/patches/src/main/resources/addresources/values-sr-rSP/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-sr-rSP/shared-youtube/strings.xml @@ -18,10 +18,12 @@ Second \"item\" text" --> - Анимација покретања - Онемогућен - Боја - Монохромно + Анимација покретања + Онемогућен + Боја + Боја Morphe + Монохромски + Монохромни Morphe Поново покрените да бисте завршили постављање diff --git a/patches/src/main/resources/addresources/values-sr-rSP/youtube/strings.xml b/patches/src/main/resources/addresources/values-sr-rSP/youtube/strings.xml index 2db56a5a90..f5d3131f3e 100644 --- a/patches/src/main/resources/addresources/values-sr-rSP/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-sr-rSP/youtube/strings.xml @@ -837,8 +837,6 @@ Second \"item\" text" Никада не приказуј Прикажи када је звук искључен Прикажи када је утишано - Прикажи аутоматске титлове - Сакриј аутоматске титлове Омиљени језик титлова Прво бира провајдерске титлове, затим аутоматски преводи на омиљени језик @@ -1248,6 +1246,7 @@ Second \"item\" text" Поправи статусну траку која прекрива горње траке Одржава конзистентан распоред статусне траке, тако да не прекрива горње траке и не оставља празан простор изнад плејера. Исключи ово ако се апликација уруши или горње траке изгледају лоше + Сјај Позадина светле теме Бела Material You (бела) @@ -1270,6 +1269,9 @@ Second \"item\" text" Подесите секундарну боју траке за напредак видеа Неважећа вредност боје траке за премотавање + Оригинални аналог + Оригинална торта + Оригинални метал Логотип у заглављу Подразумевани diff --git a/patches/src/main/resources/addresources/values-sv-rSE/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-sv-rSE/shared-youtube/strings.xml index ff84870d54..06ca483623 100644 --- a/patches/src/main/resources/addresources/values-sv-rSE/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-sv-rSE/shared-youtube/strings.xml @@ -18,10 +18,12 @@ Second \"item\" text" --> - Startanimation - Inaktiverad - Färg - Monokrom + Startanimation + Inaktiverad + Färg + Färg Morphe + Monokrom + Monokrom Morphe Starta om för att slutföra konfigureringen diff --git a/patches/src/main/resources/addresources/values-sv-rSE/youtube/strings.xml b/patches/src/main/resources/addresources/values-sv-rSE/youtube/strings.xml index 2db004cbc2..13800a48b1 100644 --- a/patches/src/main/resources/addresources/values-sv-rSE/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-sv-rSE/youtube/strings.xml @@ -837,8 +837,6 @@ För att åtgärda detta, aktivera 'Spoofa videoströmmar' och ändra 'Standardk Visa aldrig Visa när ljudet inte är avstängt Visa när ljudet är avstängt - Visa automatisk textning - Dölj automatisk textning Föredraget undertextspråk Väljer leverantörens undertexter först och översätter sedan automatiskt till det föredragna språket @@ -1250,6 +1248,7 @@ Begränsning: Om 'Återställ gammalt videoåtgärdsfält' är avstängt kan öv Fixa statusraden så att den inte täcker de övre fältens Behåller statusradens layout konsekvent, så att den inte täcker de övre fälten och lämnar inget tomt utrymme ovanför spelaren. Stäng av detta om appen kraschar eller om de övre fälten ser fel ut + Glöd Bakgrundsfärg för ljust tema Vit Material You (Vit) @@ -1272,6 +1271,9 @@ Begränsning: Om 'Återställ gammalt videoåtgärdsfält' är avstängt kan öv Ange den sekundära färgen för videons förloppsindikator Ogiltigt färgvärde för sökreglaget + Original analog + Original tårta + Original metall Logotyp i sidhuvudet Standard diff --git a/patches/src/main/resources/addresources/values-ta-rIN/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-ta-rIN/shared-youtube/strings.xml index df4972fa69..32465cf89a 100644 --- a/patches/src/main/resources/addresources/values-ta-rIN/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ta-rIN/shared-youtube/strings.xml @@ -18,7 +18,7 @@ Second \"item\" text" --> - முடக்கப்பட்டது + முடக்கப்பட்டது diff --git a/patches/src/main/resources/addresources/values-te-rIN/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-te-rIN/shared-youtube/strings.xml index d66d3ca855..234ba91e74 100644 --- a/patches/src/main/resources/addresources/values-te-rIN/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-te-rIN/shared-youtube/strings.xml @@ -18,7 +18,7 @@ Second \"item\" text" --> - నిలిపివే + నిలిపివే diff --git a/patches/src/main/resources/addresources/values-th-rTH/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-th-rTH/shared-youtube/strings.xml index 457f436306..4838b0fcaf 100644 --- a/patches/src/main/resources/addresources/values-th-rTH/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-th-rTH/shared-youtube/strings.xml @@ -18,10 +18,12 @@ Second \"item\" text" --> - แอนิเมชันเริ่มต้น - ปิดการใช้งาน - สี - โมโนโครม + แอนิเมชันเริ่มต้น + ปิดการใช้งาน + สี + สี Morphe + โมโนโครม + Morphe แบบโมโนโครม รีสตาร์ทเพื่อดำเนินการตั้งค่าให้เสร็จสิ้น diff --git a/patches/src/main/resources/addresources/values-th-rTH/youtube/strings.xml b/patches/src/main/resources/addresources/values-th-rTH/youtube/strings.xml index 79ff027d35..3b21b425f5 100644 --- a/patches/src/main/resources/addresources/values-th-rTH/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-th-rTH/youtube/strings.xml @@ -837,8 +837,6 @@ Second \"item\" text" ไม่แสดง แสดงเมื่อไม่ได้ปิดเสียง แสดงเมื่อเงียบเสียง - แสดงคำบรรยายอัตโนมัติ - ซ่อนคำบรรยายอัตโนมัติ ภาษาคำบรรยายที่ต้องการ เลือกคำบรรยายที่ผู้ให้บริการจัดเตรียมไว้ก่อน จากนั้นจึงแปลเป็นภาษาที่ต้องการโดยอัตโนมัติ @@ -1250,6 +1248,7 @@ Second \"item\" text" แก้ไขแถบสถานะไม่ให้บังแถบด้านบน คงรูปแบบของแถบสถานะให้สอดคล้องกัน เพื่อไม่ให้เกิดการบังแถบด้านบนและเหลือพื้นที่ว่างเปล่าเหนือเครื่องเล่น เปิดการใช้งานนี้หากแอปค้างหรือแถบด้านบนแสดงผลผิดปกติ + เรืองแสง พื้นหลังธีมสว่าง สีขาว Material You (สีขาว) @@ -1272,6 +1271,9 @@ Second \"item\" text" ตั้งค่าสีรองของแถบความคืบหน้าของวิดีโอ ค่าสีแถบเลื่อนไม่ถูกต้อง + อนาล็อกต้นฉบับ + เค้กต้นฉบับ + โลหะต้นฉบับ โลโก้ส่วนหัว ค่าเริ่มต้น diff --git a/patches/src/main/resources/addresources/values-tr-rTR/music/strings.xml b/patches/src/main/resources/addresources/values-tr-rTR/music/strings.xml index 73ca971af4..d3fd62f699 100644 --- a/patches/src/main/resources/addresources/values-tr-rTR/music/strings.xml +++ b/patches/src/main/resources/addresources/values-tr-rTR/music/strings.xml @@ -17,7 +17,7 @@ Second \"item\" text" --> - Son çalındı + Son çalınan Abonelikler Videolar Diğer sonuçlar diff --git a/patches/src/main/resources/addresources/values-tr-rTR/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-tr-rTR/shared-youtube/strings.xml index 5e4eafbb1d..27a87cdc8c 100644 --- a/patches/src/main/resources/addresources/values-tr-rTR/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-tr-rTR/shared-youtube/strings.xml @@ -18,10 +18,12 @@ Second \"item\" text" --> - Başlangıç animasyonu - Devre dışı - Renkli - Tek renk + Başlangıç animasyonu + Devre dışı + Renkli + Renkli Morphe + Tek renk + Tek renkli Morphe Kurulumu tamamlamak için yeniden başlatın diff --git a/patches/src/main/resources/addresources/values-tr-rTR/youtube/strings.xml b/patches/src/main/resources/addresources/values-tr-rTR/youtube/strings.xml index 9ed92e5a15..f83f2b4578 100644 --- a/patches/src/main/resources/addresources/values-tr-rTR/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-tr-rTR/youtube/strings.xml @@ -228,7 +228,7 @@ Değişiklikler şunları içerir: Mağaza düğmesini gizle Abone ol düğmesini gizle - Menüyü keşfet + Keşfet menüsü Keşfet menüsünde hangi bileşenlerin görüneceğini seçin Dersler menüsünü gizle Keşfet düğmesini gizle @@ -838,8 +838,6 @@ Bunu düzeltmek için 'Video akışlarını taklit et' seçeneğini etkinleştir Asla gösterme Ses kapatılmadığında göster Ses kapatıldığında göster - Otomatik altyazıları göster - Otomatik altyazıları gizle Tercih edilen altyazı dili Önce sağlayıcı altyazılarını seçer, ardından otomatik olarak tercih edilen dile çevirir @@ -1112,20 +1110,21 @@ Ayarlar → Oynatma (veya Otomatik Oynatma) → Sonraki videoyu otomatik oynat"< Telefon Tablet Otomotiv - "Değişiklikler şunlardır: + "Değişiklikler şunları içerir: Tablet düzeni -• Topluluk gönderileri gizlenir +• Topluluk gönderileri gizlidir • Akışlarda oynatma ayarı kullanılamaz • Shorts'ta Remix düğmesi ve Ses düğmesi bulunmaz -• 'Daha fazla video' katmanı kullanılamayabilir +• 'Daha fazla video' paneli kullanılamayabilir • Daraltılmamış eski video eylem çubuğu gösterilir +• Açılır menü öğeleri düzgün çalışmayabilir Otomotiv düzeni • Akış, konulara ve kanallara göre düzenlenmiştir • Akışlarda oynatma ayarı kullanılamaz • Shorts'ta Remix düğmesi ve Ses düğmesi bulunmaz -• 'Daha fazla video' katmanı kullanılamayabilir +• 'Daha fazla video' paneli kullanılamayabilir • Shorts, normal oynatıcıda açılır • Daraltılmamış eski video eylem çubuğu gösterilir" Oynatıcıda tablet düzenini etkinleştir @@ -1250,6 +1249,7 @@ Sınırlamalar: 'Eski video eylem çubuğunu geri yükle' kapatılırsa, 'Daha f Durum çubuğunun üst barları kapatmasını düzelt Üst barları kapatmaması ve oynatıcının üzerinde boş alan bırakmaması için durum çubuğu düzenini tutarlı hale getirir. Uygulama çöktüğünde veya üst barlar yanlış göründüğünde bu özelliği kapatın + Işıltı Açık tema arka plan Beyaz Material You (Beyaz) @@ -1272,6 +1272,9 @@ Sınırlamalar: 'Eski video eylem çubuğunu geri yükle' kapatılırsa, 'Daha f Video ilerleme çubuğunun ikincil rengini ayarlayın İlerleme çubuğu renk değeri geçersiz + Orijinal analog + Orijinal pasta + Orijinal metal Başlık logosu Varsayılan diff --git a/patches/src/main/resources/addresources/values-uk-rUA/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-uk-rUA/shared-youtube/strings.xml index 99181f9b05..23943250af 100644 --- a/patches/src/main/resources/addresources/values-uk-rUA/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-uk-rUA/shared-youtube/strings.xml @@ -18,10 +18,12 @@ Second \"item\" text" --> - Анімація запуску - Вимкнено - Кольорова - Монохромна + Анімація запуску + Вимкнено + Кольорова + Кольоровий Morphe + Монохромна + Монохромний Morphe Перезавантажте, щоб завершити налаштування diff --git a/patches/src/main/resources/addresources/values-uk-rUA/youtube/strings.xml b/patches/src/main/resources/addresources/values-uk-rUA/youtube/strings.xml index 8537bea044..707ea26819 100644 --- a/patches/src/main/resources/addresources/values-uk-rUA/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-uk-rUA/youtube/strings.xml @@ -500,8 +500,8 @@ Second \"item\" text" Відео Аудіо Усі параметри - Показати завантаження в меню Shorts - Додає пункт меню зовнішнього завантажувача у спливаюче вікно плеєра Shorts + Показувати \"Завантаження\" в меню Shorts + Додає до плеєра Shorts пункт спливного меню зовнішнього завантажувача Завантажити Голосовий переклад @@ -613,7 +613,7 @@ Second \"item\" text" Дозволяє зберегти відео для дітей у \"Дивитися пізніше\" зі спливного меню Показувати кнопку \"Зберегти в \'Дивитися пізніше\'\" у спливному меню Shorts Дозволяє зберегти Shorts в \"Дивитися пізніше\" зі спливного меню - Перенаправляти «Дивитися пізніше» до плейлиста + Перенаправляти \"Дивитися пізніше\" до списку відтворення Переглянути пізніше Додати до черги Morphe Зберігати чергу між сесіями @@ -837,8 +837,6 @@ Second \"item\" text" Ніколи не показувати Показувати при увімкненому звуці Показувати при вимкненому звуці - Показувати автоматичні субтитри - Приховати автоматичні субтитри Бажана мова субтитрів Спочатку вибирає субтитри постачальника, потім автоматично перекладає їх на бажану мову @@ -1118,6 +1116,7 @@ Second \"item\" text" • Кнопки \"Ремікс\" та \"Звук\" недоступні в Shorts • Накладка \"Більше відео\" може бути недоступна • Відображається нерозгорнута стара панель дій відео +• Пункти спливного меню можуть працювати неправильно Автомобільний макет • Стрічка організована за темами та каналами @@ -1248,6 +1247,7 @@ Second \"item\" text" Виправити перекриття верхніх панелей рядком стану Підтримує сталий макет рядка стану, щоб він не перекривав верхні панелі та не залишав порожнього місця над плеєром. Вимкніть це, якщо застосунок аварійно завершує роботу або верхні панелі виглядають неправильно + Сяйво Фон світлої теми Білий Material You (білий) @@ -1270,6 +1270,9 @@ Second \"item\" text" Встановлює додатковий колір смуги прогресу відтворення відео Недійсне значення кольору смуги відтворення + Оригінальний аналог + Оригінальний торт + Оригінальний метал Логотип заголовка Стандартний diff --git a/patches/src/main/resources/addresources/values-uz-rUZ/youtube/strings.xml b/patches/src/main/resources/addresources/values-uz-rUZ/youtube/strings.xml index 478fddfe2b..c1a94bcdbf 100644 --- a/patches/src/main/resources/addresources/values-uz-rUZ/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-uz-rUZ/youtube/strings.xml @@ -607,8 +607,6 @@ Ma'lumot: Jonli efirlarda ishlamasligi mumkin" Hech qachon ko\'rsatma \'Hype tugmasini\' yashirish Ovozsiz paytda ko\'rsatish - Avtomatik yozuvlarni ko\'rsatish - Avtomatik yozuvlarni yashirish diff --git a/patches/src/main/resources/addresources/values-vi-rVN/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-vi-rVN/shared-youtube/strings.xml index 5ad51e4a74..2732f93ab9 100644 --- a/patches/src/main/resources/addresources/values-vi-rVN/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-vi-rVN/shared-youtube/strings.xml @@ -18,10 +18,12 @@ Second \"item\" text" --> - Hoạt ảnh khởi động - Tắt - Màu - Đơn sắc + Hoạt ảnh khởi động + Tắt + Màu + Morphe Có Màu + Đơn sắc + Morphe Đơn sắc Khởi động lại để hoàn tất thiết lập diff --git a/patches/src/main/resources/addresources/values-vi-rVN/youtube/strings.xml b/patches/src/main/resources/addresources/values-vi-rVN/youtube/strings.xml index 6dab7a6f31..2dd1947c3a 100644 --- a/patches/src/main/resources/addresources/values-vi-rVN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-vi-rVN/youtube/strings.xml @@ -837,8 +837,6 @@ Lưu ý: Có thể không hoạt động với video phát trực tiếp"Không bao giờ hiện Hiển thị khi bật tiếng Hiển thị khi tắt tiếng - Hiển thị phụ đề tự động - Ẩn phụ đề tự động Ngôn ngữ phụ đề ưa thích Chọn phụ đề từ nhà cung cấp trước, sau đó tự động dịch sang ngôn ngữ ưa thích @@ -1250,6 +1248,7 @@ Hạn chế: Nếu tùy chọn \"Khôi phục thanh thao tác video cũ\" bị t Sửa thanh trạng thái che các thanh trên cùng Giữ cho bố cục của thanh trạng nhất quán, để nó không che các thanh trên cùng và không để khoảng trống phía trên trình phát. Tắt tùy chọn này nếu ứng dụng bị đóng băng hoặc các thanh trên cùng trông sai + Phát sáng Nền chủ đề sáng Trắng Material You (Trắng) @@ -1272,6 +1271,9 @@ Hạn chế: Nếu tùy chọn \"Khôi phục thanh thao tác video cũ\" bị t Đặt màu phụ cho thanh tiến trình video Giá trị màu của thanh tiến trình không hợp lệ + Analog nguyên bản + Bánh gốc + Kim loại gốc Logo đầu trang Mặc định diff --git a/patches/src/main/resources/addresources/values-zh-rCN/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-zh-rCN/shared-youtube/strings.xml index 5f3966ffff..e97860a1e8 100644 --- a/patches/src/main/resources/addresources/values-zh-rCN/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-zh-rCN/shared-youtube/strings.xml @@ -18,10 +18,12 @@ Second \"item\" text" --> - 启动动画 - 已禁用 - 颜色 - 单色 + 启动动画 + 已禁用 + 颜色 + 彩色 Morphe + 单色 + 单色 Morphe 重启以完成设置 diff --git a/patches/src/main/resources/addresources/values-zh-rCN/youtube/strings.xml b/patches/src/main/resources/addresources/values-zh-rCN/youtube/strings.xml index fe86978d81..5cf3f7a153 100644 --- a/patches/src/main/resources/addresources/values-zh-rCN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-zh-rCN/youtube/strings.xml @@ -837,8 +837,6 @@ Second \"item\" text" 从不显示 显示设置按钮 显示静音时的字幕 - 显示自动字幕 - 隐藏自动字幕 首选字幕语言 先选择提供方字幕,然后自动翻译为首选语言 @@ -1250,6 +1248,7 @@ Second \"item\" text" 修复状态栏覆盖顶部栏的问题 保持状态栏布局一致,使其不覆盖顶部栏且播放器上方无空白区域。如果应用崩溃或顶部栏显示异常,请关闭此选项 + 发光 浅色主题背景 白色 Material You(白色) @@ -1272,6 +1271,9 @@ Second \"item\" text" 设置视频进度条的第二颜色 无效的进度条颜色值 + 原始模拟 + 原始蛋糕 + 原始金属 标题logo 默认 diff --git a/patches/src/main/resources/addresources/values-zh-rTW/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-zh-rTW/shared-youtube/strings.xml index f8ed7b73bc..a0da7129fe 100644 --- a/patches/src/main/resources/addresources/values-zh-rTW/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-zh-rTW/shared-youtube/strings.xml @@ -18,10 +18,12 @@ Second \"item\" text" --> - 啟動動畫 - 已停用 - 顏色 - 單色 + 啟動動畫 + 已停用 + 顏色 + 彩色 Morphe + 單色 + 單色 Morphe 重新啟動以完成設定 diff --git a/patches/src/main/resources/addresources/values-zh-rTW/youtube/strings.xml b/patches/src/main/resources/addresources/values-zh-rTW/youtube/strings.xml index e5150b6d31..6a3a8c9248 100644 --- a/patches/src/main/resources/addresources/values-zh-rTW/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-zh-rTW/youtube/strings.xml @@ -837,8 +837,6 @@ Second \"item\" text" 永不顯示 在取消靜音時顯示 靜音時顯示 - 顯示自動字幕 - 隱藏自動字幕 偏好字幕語言 先選取提供者提供的字幕,再自動翻譯成偏好語言 @@ -1250,6 +1248,7 @@ Second \"item\" text" 修復狀態列遮蓋頂部工具列的問題 保持狀態列的版面配置一致,使其不會遮蓋頂部工具列,且在播放器上方不會留有空白。如果應用程式發生當機或頂部工具列顯示異常,請停用此選項 + 發光 淺色主題背景 白色 動態顏色(白色) @@ -1272,6 +1271,9 @@ Second \"item\" text" 設定影片進度條的次要顏色 跳轉列色彩值無效 + 原始類比 + 原始蛋糕 + 原版金屬 標題標誌 預設 diff --git a/patches/src/main/resources/addresources/values-zu-rZA/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-zu-rZA/shared-youtube/strings.xml index d98199c177..324777b962 100644 --- a/patches/src/main/resources/addresources/values-zu-rZA/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-zu-rZA/shared-youtube/strings.xml @@ -18,10 +18,9 @@ Second \"item\" text" --> - I-animation yokuqala - Idisayizelwe - Umbala - Imibala emnyama nemhlophe + I-animation yokuqala + Idisayizelwe + Umbala Qala kabusha ukuze uqedele ukusetha diff --git a/patches/src/main/resources/addresources/values-zu-rZA/youtube/strings.xml b/patches/src/main/resources/addresources/values-zu-rZA/youtube/strings.xml index 161ac457d0..3fcced6949 100644 --- a/patches/src/main/resources/addresources/values-zu-rZA/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-zu-rZA/youtube/strings.xml @@ -831,8 +831,6 @@ Ukuze ulungise lokhu, vula 'Spoof video streams' bese ushintsha 'Default client' Ungazikhombisile nakaze Khombisa lapho kukhishwe ukuthula Khombisa lapho kuthulekisiwe - Khombisa izincazelo ezizenzakalelayo - Fihla imibhalo yesizulu ezenzakalayo Ulimi lwezincazelo olukhethiwe Khetha izincazelo zomhlinzeki kuqala, bese ihumusha ngokuzenzakalelayo olimini olukhethiwe From 71c8c7c9b98b97ba97dfb25d71b50628b7de05e3 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Tue, 27 Oct 2026 19:52:05 +0000 Subject: [PATCH 117/189] chore: Release v1.49.0-dev.9 [skip ci] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## [1.49.0-dev.9](https://github.com/MorpheApp/morphe-patches/compare/v1.49.0-dev.8...v1.49.0-dev.9) (2026-10-27) ### 🐛 Bug Fixes * **YouTube - Downloads:** Downloaded videos that work at first may not work later ([90d011d](https://github.com/MorpheApp/morphe-patches/commit/90d011dc007cf7e433196fd27f392c87d32363f6)) * **YouTube - Feed video actions:** Watch later button is missing after scrolling up ([51dfd75](https://github.com/MorpheApp/morphe-patches/commit/51dfd7565426be389f61dc5f34196cc762a103fe)) * **YouTube - Feed video actions:** Watch later button opens the channel in right to left languages ([0e058e8](https://github.com/MorpheApp/morphe-patches/commit/0e058e8d184650326d81a243d083ae9bd6f3f942)) * **YouTube - Feed view count:** View count badge covers the duration in right to left languages ([b012535](https://github.com/MorpheApp/morphe-patches/commit/b01253535eb65761f896042d46ba1fed002232c2)) * **YouTube:** Channel page community posts do not load when feed "Hide community posts" is enabled ([#4110](https://github.com/MorpheApp/morphe-patches/issues/4110)) ([b54aaec](https://github.com/MorpheApp/morphe-patches/commit/b54aaecaa26e8e6e4491b0c54193cd2b2b6e0f2c)) * **YouTube:** Feed thumbnail badges are missing after scrolling up, show a loading spinner until they are loaded ([03ef708](https://github.com/MorpheApp/morphe-patches/commit/03ef708f724c9af7f784b75ead9da58b8cede5f4)) * **YouTube:** Remove legacy litho buffer filtering code required for app versions less than 20.22, change legacy support to `20.26.46` ([0ce43f7](https://github.com/MorpheApp/morphe-patches/commit/0ce43f7b8bf0119f295f269b939186ff264b5d48)) * **YouTube:** Watch page feed lags when scrolling ([35e4123](https://github.com/MorpheApp/morphe-patches/commit/35e412375a1de62b500e2c9ca70c0ca5a18fb793)) ### ✨ New Features * Validate custom header and icon folders when the option is set ([fbb095d](https://github.com/MorpheApp/morphe-patches/commit/fbb095dbd6b9bf7410e4cff38b664d2fb3048aeb)) * **YouTube - Custom branding:** Add experimental YouTube launcher icons ([#4114](https://github.com/MorpheApp/morphe-patches/issues/4114)) ([de9e6e4](https://github.com/MorpheApp/morphe-patches/commit/de9e6e4e7953f6192991e35025ed644806948ecd)) * **YouTube - Theme:** Add experimental YT splash screen animation ([#4109](https://github.com/MorpheApp/morphe-patches/issues/4109)) ([1f24e84](https://github.com/MorpheApp/morphe-patches/commit/1f24e8433b26aed7a0e8cd21bf6c719526e7ecc4)) * **YouTube:** Hidden a new type of community post ([fb51b72](https://github.com/MorpheApp/morphe-patches/commit/fb51b722e05df50e98302f87996525b349d0474b)) * **YouTube:** Redirect Watch later saves to a chosen playlist ([#4016](https://github.com/MorpheApp/morphe-patches/issues/4016)) ([04e791d](https://github.com/MorpheApp/morphe-patches/commit/04e791d3d1c9fa8c8c352c61b18a6348311a8a27)) ### 🚀 Updated App Support * **YouTube Music:** Add experimental support for `9.43.51` ([141aa8f](https://github.com/MorpheApp/morphe-patches/commit/141aa8fedea935664df73154cdc1823e893c5b88)) --- CHANGELOG.md | 25 +++ README.md | 8 +- gradle.properties | 2 +- patches-bundle.json | 8 +- patches-list.json | 514 ++++++++++++++++++++++---------------------- 5 files changed, 291 insertions(+), 266 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3da65d2388..d8fb576d0e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,28 @@ +## [1.49.0-dev.9](https://github.com/MorpheApp/morphe-patches/compare/v1.49.0-dev.8...v1.49.0-dev.9) (2026-10-27) + +### 🐛 Bug Fixes + +* **YouTube - Downloads:** Downloaded videos that work at first may not work later ([90d011d](https://github.com/MorpheApp/morphe-patches/commit/90d011dc007cf7e433196fd27f392c87d32363f6)) +* **YouTube - Feed video actions:** Watch later button is missing after scrolling up ([51dfd75](https://github.com/MorpheApp/morphe-patches/commit/51dfd7565426be389f61dc5f34196cc762a103fe)) +* **YouTube - Feed video actions:** Watch later button opens the channel in right to left languages ([0e058e8](https://github.com/MorpheApp/morphe-patches/commit/0e058e8d184650326d81a243d083ae9bd6f3f942)) +* **YouTube - Feed view count:** View count badge covers the duration in right to left languages ([b012535](https://github.com/MorpheApp/morphe-patches/commit/b01253535eb65761f896042d46ba1fed002232c2)) +* **YouTube:** Channel page community posts do not load when feed "Hide community posts" is enabled ([#4110](https://github.com/MorpheApp/morphe-patches/issues/4110)) ([b54aaec](https://github.com/MorpheApp/morphe-patches/commit/b54aaecaa26e8e6e4491b0c54193cd2b2b6e0f2c)) +* **YouTube:** Feed thumbnail badges are missing after scrolling up, show a loading spinner until they are loaded ([03ef708](https://github.com/MorpheApp/morphe-patches/commit/03ef708f724c9af7f784b75ead9da58b8cede5f4)) +* **YouTube:** Remove legacy litho buffer filtering code required for app versions less than 20.22, change legacy support to `20.26.46` ([0ce43f7](https://github.com/MorpheApp/morphe-patches/commit/0ce43f7b8bf0119f295f269b939186ff264b5d48)) +* **YouTube:** Watch page feed lags when scrolling ([35e4123](https://github.com/MorpheApp/morphe-patches/commit/35e412375a1de62b500e2c9ca70c0ca5a18fb793)) + +### ✨ New Features + +* Validate custom header and icon folders when the option is set ([fbb095d](https://github.com/MorpheApp/morphe-patches/commit/fbb095dbd6b9bf7410e4cff38b664d2fb3048aeb)) +* **YouTube - Custom branding:** Add experimental YouTube launcher icons ([#4114](https://github.com/MorpheApp/morphe-patches/issues/4114)) ([de9e6e4](https://github.com/MorpheApp/morphe-patches/commit/de9e6e4e7953f6192991e35025ed644806948ecd)) +* **YouTube - Theme:** Add experimental YT splash screen animation ([#4109](https://github.com/MorpheApp/morphe-patches/issues/4109)) ([1f24e84](https://github.com/MorpheApp/morphe-patches/commit/1f24e8433b26aed7a0e8cd21bf6c719526e7ecc4)) +* **YouTube:** Hidden a new type of community post ([fb51b72](https://github.com/MorpheApp/morphe-patches/commit/fb51b722e05df50e98302f87996525b349d0474b)) +* **YouTube:** Redirect Watch later saves to a chosen playlist ([#4016](https://github.com/MorpheApp/morphe-patches/issues/4016)) ([04e791d](https://github.com/MorpheApp/morphe-patches/commit/04e791d3d1c9fa8c8c352c61b18a6348311a8a27)) + +### 🚀 Updated App Support + +* **YouTube Music:** Add experimental support for `9.43.51` ([141aa8f](https://github.com/MorpheApp/morphe-patches/commit/141aa8fedea935664df73154cdc1823e893c5b88)) + ## [1.49.0-dev.8](https://github.com/MorpheApp/morphe-patches/compare/v1.49.0-dev.7...v1.49.0-dev.8) (2026-10-27) ### 🐛 Bug Fixes diff --git a/README.md b/README.md index 45c77ae454..5c7d9861aa 100644 --- a/README.md +++ b/README.md @@ -35,14 +35,14 @@ All modifications made by Morphe, along with their dates, can be found in the Gi ## 🩹 Patches list -> **[v1.49.0-dev.8](https://github.com/MorpheApp/morphe-patches/releases/tag/v1.49.0-dev.8)**  •  `dev`  •  193 patches total +> **[v1.49.0-dev.9](https://github.com/MorpheApp/morphe-patches/releases/tag/v1.49.0-dev.9)**  •  `dev`  •  193 patches total

    📦 YouTube  •  108 patches
    **🎯 Supported versions:** -| 🧪 21.43.523 | 🧪 21.42.241 | 🧪 21.28.208 | 21.23.492 | 21.20.405 | 21.16.256 | 20.51.39 | 20.21.37 | +| 🧪 21.43.523 | 🧪 21.42.241 | 🧪 21.28.208 | 21.23.492 | 21.20.405 | 21.16.256 | 20.51.39 | 20.26.46 | | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | | 💊 Patch | 📜 Description | ⚙️ Options | @@ -136,7 +136,7 @@ All modifications made by Morphe, along with their dates, can be found in the Gi | [Restore original titles](#restore-original-titles) | Adds an option to show the original video titles, video descriptions and channel descriptions instead of the auto-translated ones. | | | [Return YouTube Dislike](#return-youtube-dislike) | Adds an option to show the dislike count of videos with Return YouTube Dislike, and the like count and like ratio on the thumbnails of feed videos. | | | [Sanitize sharing links](#sanitize-sharing-links) | Removes the tracking query parameters from shared links. | | -| [Save to Watch later](#save-to-watch-later) | Adds an option to display save to Watch later button in the video player. | | +| [Save to Watch later](#save-to-watch-later) | Adds an option to display save to Watch later button in the video player, and adds a setting to redirect Watch later saves to a playlist of your choice. | | | [Search comments](#search-comments) | Adds a button to the comments to show only the loaded comments that contain the searched words. | | | [Seekbar](#seekbar) | Adds options to show old seekbar thumbnails, disable precise seeking when swiping up on the seekbar, slide to seek instead of playing at 2x speed when pressing and holding, tapping the player seekbar to seek, hiding the video player seekbar, enabling seeking in live streams, and expanding the live stream DVR duration. | | | [Series tracking](#series-tracking) | Adds a series library to History with local progress and episode continuation. This patch works with YouTube 21.02 and newer. | | @@ -164,7 +164,7 @@ All modifications made by Morphe, along with their dates, can be found in the Gi **🎯 Supported versions:** -| 🧪 9.42.52 | 🧪 9.41.52 | 🧪 9.40.51 | 9.20.53 | 9.15.51 | +| 🧪 9.43.51 | 🧪 9.42.52 | 🧪 9.41.52 | 9.20.53 | 9.15.51 | | :---: | :---: | :---: | :---: | :---: | | 💊 Patch | 📜 Description | ⚙️ Options | diff --git a/gradle.properties b/gradle.properties index 8a13048b1e..08c5cccde1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,4 +5,4 @@ android.useAndroidX = true # Extensions are Java only. Prevents AGP from adding Kotlin compilation and the Kotlin stdlib. android.builtInKotlin = false kotlin.code.style = official -version = 1.49.0-dev.8 +version = 1.49.0-dev.9 diff --git a/patches-bundle.json b/patches-bundle.json index d496261244..26a0c5a34b 100644 --- a/patches-bundle.json +++ b/patches-bundle.json @@ -1,8 +1,8 @@ { - "created_at": "2026-10-27T09:06:43", - "description": "## [1.49.0-dev.8](https://github.com/MorpheApp/morphe-patches/compare/v1.49.0-dev.7...v1.49.0-dev.8) (2026-10-27)\n\n### 🐛 Bug Fixes\n\n* **YouTube - Channel whitelists:** Flyout menu opens with a delay while the channel is fetched ([5fed7fc](https://github.com/MorpheApp/morphe-patches/commit/5fed7fc701421b701263f742a6ba38fd4cc89dd5))\n* **YouTube - Litho filter:** Crash when scrolling a list after a filtered element is removed ([6d43157](https://github.com/MorpheApp/morphe-patches/commit/6d4315757768d792614915d0867079437505fab9))\n* **YouTube - Playback speed:** Use the bold icon for the Shorts player menu item ([c710042](https://github.com/MorpheApp/morphe-patches/commit/c710042f6cf3f068141e89cd833dd580d7b64b8c))\n* **YouTube - Player icon style:** Add Ionicons picture in picture button icon ([68b8628](https://github.com/MorpheApp/morphe-patches/commit/68b86282725f73c3a92602c792457371ddb88c8e))\n* **YouTube - Premium-style queue:** Fix missing titles and stale now playing row ([#3795](https://github.com/MorpheApp/morphe-patches/issues/3795)) ([ef28427](https://github.com/MorpheApp/morphe-patches/commit/ef28427f63c30329b33b8d83c439c95b731a4fb6))\n* **YouTube - Premium-style queue:** Play the queue when the player Next button is pressed ([#3796](https://github.com/MorpheApp/morphe-patches/issues/3796)) ([ef8dcef](https://github.com/MorpheApp/morphe-patches/commit/ef8dcef6a9ef888a075fb97d963719ddf82d6b42))\n* **YouTube - Return YouTube Dislike:** Feed thumbnail like count is not shown with the Classic feed card layout ([15a1700](https://github.com/MorpheApp/morphe-patches/commit/15a170042639ca8bc0b4d871b781c29d00926fb2))\n* **YouTube Music - Downloads:** Fix song menu downloads for accounts without Premium ([#4086](https://github.com/MorpheApp/morphe-patches/issues/4086)) ([93c6893](https://github.com/MorpheApp/morphe-patches/commit/93c689374b0ef72cb97d6c73881ecdb132ce7e91))\n\n### ✨ New Features\n\n* **YouTube - Channel search:** Search the posts of the channel ([#4078](https://github.com/MorpheApp/morphe-patches/issues/4078)) ([93bd035](https://github.com/MorpheApp/morphe-patches/commit/93bd0351362da6c75eb40f47d0ea0e37c632eb1c))\n* **YouTube - Downloads:** Add download item to the Shorts menu ([#4099](https://github.com/MorpheApp/morphe-patches/issues/4099)) ([7159826](https://github.com/MorpheApp/morphe-patches/commit/715982644742ca770a1c61fe292a77bb1ce11296))\n* **YouTube - Downloads:** Add playlist downloads for accounts without Premium ([456e0db](https://github.com/MorpheApp/morphe-patches/commit/456e0db9603f0930a4893e3f24ee3eee8bd4e5f9))\n* **YouTube Music - Third-party lyrics:** Streamline lyrics viewing experience ([#4002](https://github.com/MorpheApp/morphe-patches/issues/4002)) ([00a4bf4](https://github.com/MorpheApp/morphe-patches/commit/00a4bf457e8df233b21e6f73b244b0c12e91e53e))\n* **YouTube:** Add Niconico-style live/replay comments ([#4080](https://github.com/MorpheApp/morphe-patches/issues/4080)) ([e9f77d7](https://github.com/MorpheApp/morphe-patches/commit/e9f77d7c5932499ababf91481b883c30e96ccd1c))", - "download_url": "https://github.com/MorpheApp/morphe-patches/releases/download/v1.49.0-dev.8/patches-1.49.0-dev.8.mpp", + "created_at": "2026-10-27T19:49:48", + "description": "## [1.49.0-dev.9](https://github.com/MorpheApp/morphe-patches/compare/v1.49.0-dev.8...v1.49.0-dev.9) (2026-10-27)\n\n### 🐛 Bug Fixes\n\n* **YouTube - Downloads:** Downloaded videos that work at first may not work later ([90d011d](https://github.com/MorpheApp/morphe-patches/commit/90d011dc007cf7e433196fd27f392c87d32363f6))\n* **YouTube - Feed video actions:** Watch later button is missing after scrolling up ([51dfd75](https://github.com/MorpheApp/morphe-patches/commit/51dfd7565426be389f61dc5f34196cc762a103fe))\n* **YouTube - Feed video actions:** Watch later button opens the channel in right to left languages ([0e058e8](https://github.com/MorpheApp/morphe-patches/commit/0e058e8d184650326d81a243d083ae9bd6f3f942))\n* **YouTube - Feed view count:** View count badge covers the duration in right to left languages ([b012535](https://github.com/MorpheApp/morphe-patches/commit/b01253535eb65761f896042d46ba1fed002232c2))\n* **YouTube:** Channel page community posts do not load when feed \"Hide community posts\" is enabled ([#4110](https://github.com/MorpheApp/morphe-patches/issues/4110)) ([b54aaec](https://github.com/MorpheApp/morphe-patches/commit/b54aaecaa26e8e6e4491b0c54193cd2b2b6e0f2c))\n* **YouTube:** Feed thumbnail badges are missing after scrolling up, show a loading spinner until they are loaded ([03ef708](https://github.com/MorpheApp/morphe-patches/commit/03ef708f724c9af7f784b75ead9da58b8cede5f4))\n* **YouTube:** Remove legacy litho buffer filtering code required for app versions less than 20.22, change legacy support to `20.26.46` ([0ce43f7](https://github.com/MorpheApp/morphe-patches/commit/0ce43f7b8bf0119f295f269b939186ff264b5d48))\n* **YouTube:** Watch page feed lags when scrolling ([35e4123](https://github.com/MorpheApp/morphe-patches/commit/35e412375a1de62b500e2c9ca70c0ca5a18fb793))\n\n### ✨ New Features\n\n* Validate custom header and icon folders when the option is set ([fbb095d](https://github.com/MorpheApp/morphe-patches/commit/fbb095dbd6b9bf7410e4cff38b664d2fb3048aeb))\n* **YouTube - Custom branding:** Add experimental YouTube launcher icons ([#4114](https://github.com/MorpheApp/morphe-patches/issues/4114)) ([de9e6e4](https://github.com/MorpheApp/morphe-patches/commit/de9e6e4e7953f6192991e35025ed644806948ecd))\n* **YouTube - Theme:** Add experimental YT splash screen animation ([#4109](https://github.com/MorpheApp/morphe-patches/issues/4109)) ([1f24e84](https://github.com/MorpheApp/morphe-patches/commit/1f24e8433b26aed7a0e8cd21bf6c719526e7ecc4))\n* **YouTube:** Hidden a new type of community post ([fb51b72](https://github.com/MorpheApp/morphe-patches/commit/fb51b722e05df50e98302f87996525b349d0474b))\n* **YouTube:** Redirect Watch later saves to a chosen playlist ([#4016](https://github.com/MorpheApp/morphe-patches/issues/4016)) ([04e791d](https://github.com/MorpheApp/morphe-patches/commit/04e791d3d1c9fa8c8c352c61b18a6348311a8a27))\n\n### 🚀 Updated App Support\n\n* **YouTube Music:** Add experimental support for `9.43.51` ([141aa8f](https://github.com/MorpheApp/morphe-patches/commit/141aa8fedea935664df73154cdc1823e893c5b88))", + "download_url": "https://github.com/MorpheApp/morphe-patches/releases/download/v1.49.0-dev.9/patches-1.49.0-dev.9.mpp", "signature_download_url": "", - "version": "1.49.0-dev.8", + "version": "1.49.0-dev.9", "public_key": "g8Ys9leTu2yeixojeR35HrNkK6eOSLpA6nNIizRutqI=" } diff --git a/patches-list.json b/patches-list.json index f51318f50d..50f4994287 100644 --- a/patches-list.json +++ b/patches-list.json @@ -1,6 +1,6 @@ { "NOTE": "Do NOT manually edit this file. This file is automatically updated when semantic release (release.yml) runs. Manually editing this file can break your releases and break third party tools that use this file.", - "version": "1.49.0-dev.8", + "version": "1.49.0-dev.9", "patches": [ { "name": "AI channel filter", @@ -70,7 +70,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -149,7 +149,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -228,7 +228,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -258,21 +258,21 @@ "formFactor": "MOBILE", "targets": [ { - "version": "9.42.52", + "version": "9.43.51", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.41.52", + "version": "9.42.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.40.51", + "version": "9.41.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, @@ -380,21 +380,21 @@ "formFactor": "MOBILE", "targets": [ { - "version": "9.42.52", + "version": "9.43.51", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.41.52", + "version": "9.42.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.40.51", + "version": "9.41.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, @@ -487,7 +487,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -566,7 +566,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -645,7 +645,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -724,7 +724,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -803,7 +803,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -882,7 +882,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -912,21 +912,21 @@ "formFactor": "MOBILE", "targets": [ { - "version": "9.42.52", + "version": "9.43.51", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.41.52", + "version": "9.42.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.40.51", + "version": "9.41.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, @@ -1029,7 +1029,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -1093,21 +1093,21 @@ "formFactor": "MOBILE", "targets": [ { - "version": "9.42.52", + "version": "9.43.51", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.41.52", + "version": "9.42.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.40.51", + "version": "9.41.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, @@ -1151,21 +1151,21 @@ "formFactor": "MOBILE", "targets": [ { - "version": "9.42.52", + "version": "9.43.51", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.41.52", + "version": "9.42.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.40.51", + "version": "9.41.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, @@ -1258,7 +1258,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -1337,7 +1337,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -1416,7 +1416,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -1446,21 +1446,21 @@ "formFactor": "MOBILE", "targets": [ { - "version": "9.42.52", + "version": "9.43.51", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.41.52", + "version": "9.42.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.40.51", + "version": "9.41.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, @@ -1553,7 +1553,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -1734,7 +1734,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -1813,7 +1813,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -1843,21 +1843,21 @@ "formFactor": "MOBILE", "targets": [ { - "version": "9.42.52", + "version": "9.43.51", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.41.52", + "version": "9.42.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.40.51", + "version": "9.41.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, @@ -1901,21 +1901,21 @@ "formFactor": "MOBILE", "targets": [ { - "version": "9.42.52", + "version": "9.43.51", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.41.52", + "version": "9.42.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.40.51", + "version": "9.41.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, @@ -2045,7 +2045,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -2188,21 +2188,21 @@ "formFactor": "MOBILE", "targets": [ { - "version": "9.42.52", + "version": "9.43.51", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.41.52", + "version": "9.42.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.40.51", + "version": "9.41.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, @@ -2359,7 +2359,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -2438,7 +2438,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -2581,7 +2581,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -2611,21 +2611,21 @@ "formFactor": "MOBILE", "targets": [ { - "version": "9.42.52", + "version": "9.43.51", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.41.52", + "version": "9.42.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.40.51", + "version": "9.41.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, @@ -2718,7 +2718,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -2756,21 +2756,21 @@ "formFactor": "MOBILE", "targets": [ { - "version": "9.42.52", + "version": "9.43.51", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.41.52", + "version": "9.42.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.40.51", + "version": "9.41.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, @@ -2863,7 +2863,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -2942,7 +2942,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -3021,7 +3021,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -3100,7 +3100,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -3130,21 +3130,21 @@ "formFactor": "MOBILE", "targets": [ { - "version": "9.42.52", + "version": "9.43.51", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.41.52", + "version": "9.42.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.40.51", + "version": "9.41.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, @@ -3237,7 +3237,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -3316,7 +3316,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -3395,7 +3395,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -3474,7 +3474,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -3617,7 +3617,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -3696,7 +3696,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -3775,7 +3775,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -3918,7 +3918,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -3997,7 +3997,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -4076,7 +4076,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -4155,7 +4155,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -4298,7 +4298,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -4328,21 +4328,21 @@ "formFactor": "MOBILE", "targets": [ { - "version": "9.42.52", + "version": "9.43.51", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.41.52", + "version": "9.42.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.40.51", + "version": "9.41.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, @@ -4435,7 +4435,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -4465,21 +4465,21 @@ "formFactor": "MOBILE", "targets": [ { - "version": "9.42.52", + "version": "9.43.51", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.41.52", + "version": "9.42.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.40.51", + "version": "9.41.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, @@ -4523,21 +4523,21 @@ "formFactor": "MOBILE", "targets": [ { - "version": "9.42.52", + "version": "9.43.51", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.41.52", + "version": "9.42.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.40.51", + "version": "9.41.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, @@ -4630,7 +4630,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -4660,21 +4660,21 @@ "formFactor": "MOBILE", "targets": [ { - "version": "9.42.52", + "version": "9.43.51", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.41.52", + "version": "9.42.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.40.51", + "version": "9.41.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, @@ -4718,21 +4718,21 @@ "formFactor": "MOBILE", "targets": [ { - "version": "9.42.52", + "version": "9.43.51", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.41.52", + "version": "9.42.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.40.51", + "version": "9.41.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, @@ -4776,21 +4776,21 @@ "formFactor": "MOBILE", "targets": [ { - "version": "9.42.52", + "version": "9.43.51", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.41.52", + "version": "9.42.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.40.51", + "version": "9.41.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, @@ -4883,7 +4883,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -4962,7 +4962,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -5105,7 +5105,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -5184,7 +5184,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -5263,7 +5263,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -5342,7 +5342,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -5372,21 +5372,21 @@ "formFactor": "MOBILE", "targets": [ { - "version": "9.42.52", + "version": "9.43.51", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.41.52", + "version": "9.42.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.40.51", + "version": "9.41.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, @@ -5479,7 +5479,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -5509,21 +5509,21 @@ "formFactor": "MOBILE", "targets": [ { - "version": "9.42.52", + "version": "9.43.51", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.41.52", + "version": "9.42.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.40.51", + "version": "9.41.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, @@ -5744,7 +5744,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -5774,21 +5774,21 @@ "formFactor": "MOBILE", "targets": [ { - "version": "9.42.52", + "version": "9.43.51", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.41.52", + "version": "9.42.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.40.51", + "version": "9.41.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, @@ -5881,7 +5881,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -6088,7 +6088,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -6201,21 +6201,21 @@ "formFactor": "MOBILE", "targets": [ { - "version": "9.42.52", + "version": "9.43.51", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.41.52", + "version": "9.42.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.40.51", + "version": "9.41.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, @@ -6372,7 +6372,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -6451,7 +6451,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -6481,21 +6481,21 @@ "formFactor": "MOBILE", "targets": [ { - "version": "9.42.52", + "version": "9.43.51", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.41.52", + "version": "9.42.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.40.51", + "version": "9.41.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, @@ -6652,7 +6652,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -6731,7 +6731,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -6761,21 +6761,21 @@ "formFactor": "MOBILE", "targets": [ { - "version": "9.42.52", + "version": "9.43.51", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.41.52", + "version": "9.42.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.40.51", + "version": "9.41.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, @@ -6868,7 +6868,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -6898,21 +6898,21 @@ "formFactor": "MOBILE", "targets": [ { - "version": "9.42.52", + "version": "9.43.51", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.41.52", + "version": "9.42.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.40.51", + "version": "9.41.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, @@ -7005,7 +7005,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -7099,21 +7099,21 @@ "formFactor": "MOBILE", "targets": [ { - "version": "9.42.52", + "version": "9.43.51", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.41.52", + "version": "9.42.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.40.51", + "version": "9.41.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, @@ -7270,7 +7270,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -7349,7 +7349,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -7428,7 +7428,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -7507,7 +7507,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -7650,7 +7650,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -7729,7 +7729,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -7808,7 +7808,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -7838,21 +7838,21 @@ "formFactor": "MOBILE", "targets": [ { - "version": "9.42.52", + "version": "9.43.51", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.41.52", + "version": "9.42.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.40.51", + "version": "9.41.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, @@ -8009,7 +8009,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -8039,21 +8039,21 @@ "formFactor": "MOBILE", "targets": [ { - "version": "9.42.52", + "version": "9.43.51", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.41.52", + "version": "9.42.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.40.51", + "version": "9.41.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, @@ -8146,7 +8146,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -8353,7 +8353,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -8383,21 +8383,21 @@ "formFactor": "MOBILE", "targets": [ { - "version": "9.42.52", + "version": "9.43.51", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.41.52", + "version": "9.42.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.40.51", + "version": "9.41.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, @@ -8490,7 +8490,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -8520,21 +8520,21 @@ "formFactor": "MOBILE", "targets": [ { - "version": "9.42.52", + "version": "9.43.51", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.41.52", + "version": "9.42.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.40.51", + "version": "9.41.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, @@ -8627,7 +8627,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -8676,21 +8676,21 @@ "formFactor": "MOBILE", "targets": [ { - "version": "9.42.52", + "version": "9.43.51", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.41.52", + "version": "9.42.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.40.51", + "version": "9.41.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, @@ -8783,7 +8783,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -8862,7 +8862,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -8941,7 +8941,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -9020,7 +9020,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -9227,7 +9227,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -9306,7 +9306,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -9385,7 +9385,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -9464,7 +9464,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -9551,7 +9551,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -9581,21 +9581,21 @@ "formFactor": "MOBILE", "targets": [ { - "version": "9.42.52", + "version": "9.43.51", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.41.52", + "version": "9.42.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.40.51", + "version": "9.41.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, @@ -9688,7 +9688,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -9767,7 +9767,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -9846,7 +9846,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -9876,21 +9876,21 @@ "formFactor": "MOBILE", "targets": [ { - "version": "9.42.52", + "version": "9.43.51", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.41.52", + "version": "9.42.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.40.51", + "version": "9.41.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, @@ -9983,7 +9983,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -10062,7 +10062,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -10102,21 +10102,21 @@ "formFactor": "MOBILE", "targets": [ { - "version": "9.42.52", + "version": "9.43.51", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.41.52", + "version": "9.42.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.40.51", + "version": "9.41.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, @@ -10160,21 +10160,21 @@ "formFactor": "MOBILE", "targets": [ { - "version": "9.42.52", + "version": "9.43.51", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.41.52", + "version": "9.42.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.40.51", + "version": "9.41.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, @@ -10267,7 +10267,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -10410,7 +10410,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -10553,7 +10553,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -10583,21 +10583,21 @@ "formFactor": "MOBILE", "targets": [ { - "version": "9.42.52", + "version": "9.43.51", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.41.52", + "version": "9.42.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.40.51", + "version": "9.41.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, @@ -10641,21 +10641,21 @@ "formFactor": "MOBILE", "targets": [ { - "version": "9.42.52", + "version": "9.43.51", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.41.52", + "version": "9.42.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.40.51", + "version": "9.41.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, @@ -10699,21 +10699,21 @@ "formFactor": "MOBILE", "targets": [ { - "version": "9.42.52", + "version": "9.43.51", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.41.52", + "version": "9.42.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.40.51", + "version": "9.41.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, @@ -10806,7 +10806,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -10900,21 +10900,21 @@ "formFactor": "MOBILE", "targets": [ { - "version": "9.42.52", + "version": "9.43.51", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.41.52", + "version": "9.42.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.40.51", + "version": "9.41.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, @@ -11007,7 +11007,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -11086,7 +11086,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -11116,21 +11116,21 @@ "formFactor": "MOBILE", "targets": [ { - "version": "9.42.52", + "version": "9.43.51", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.41.52", + "version": "9.42.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.40.51", + "version": "9.41.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, @@ -11223,7 +11223,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -11253,21 +11253,21 @@ "formFactor": "MOBILE", "targets": [ { - "version": "9.42.52", + "version": "9.43.51", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.41.52", + "version": "9.42.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.40.51", + "version": "9.41.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, @@ -11424,7 +11424,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -11437,7 +11437,7 @@ }, { "name": "Save to Watch later", - "description": "Adds an option to display save to Watch later button in the video player.", + "description": "Adds an option to display save to Watch later button in the video player, and adds a setting to redirect Watch later saves to a playlist of your choice.", "default": true, "category": null, "compatiblePackages": [ @@ -11503,7 +11503,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -11533,21 +11533,21 @@ "formFactor": "MOBILE", "targets": [ { - "version": "9.42.52", + "version": "9.43.51", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.41.52", + "version": "9.42.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.40.51", + "version": "9.41.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, @@ -11640,7 +11640,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -11719,7 +11719,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -11798,7 +11798,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -11828,21 +11828,21 @@ "formFactor": "MOBILE", "targets": [ { - "version": "9.42.52", + "version": "9.43.51", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.41.52", + "version": "9.42.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.40.51", + "version": "9.41.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, @@ -11935,7 +11935,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -12014,7 +12014,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -12093,7 +12093,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -12246,7 +12246,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -12276,21 +12276,21 @@ "formFactor": "MOBILE", "targets": [ { - "version": "9.42.52", + "version": "9.43.51", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.41.52", + "version": "9.42.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.40.51", + "version": "9.41.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, @@ -12383,7 +12383,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -12413,21 +12413,21 @@ "formFactor": "MOBILE", "targets": [ { - "version": "9.42.52", + "version": "9.43.51", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.41.52", + "version": "9.42.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.40.51", + "version": "9.41.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, @@ -12520,7 +12520,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -12550,21 +12550,21 @@ "formFactor": "MOBILE", "targets": [ { - "version": "9.42.52", + "version": "9.43.51", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.41.52", + "version": "9.42.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.40.51", + "version": "9.41.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, @@ -12657,7 +12657,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -12736,7 +12736,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -12845,21 +12845,21 @@ "formFactor": "MOBILE", "targets": [ { - "version": "9.42.52", + "version": "9.43.51", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.41.52", + "version": "9.42.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.40.51", + "version": "9.41.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, @@ -12952,7 +12952,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -13095,7 +13095,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -13125,21 +13125,21 @@ "formFactor": "MOBILE", "targets": [ { - "version": "9.42.52", + "version": "9.43.51", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.41.52", + "version": "9.42.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.40.51", + "version": "9.41.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, @@ -13258,7 +13258,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -13337,21 +13337,21 @@ "formFactor": "MOBILE", "targets": [ { - "version": "9.42.52", + "version": "9.43.51", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.41.52", + "version": "9.42.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, "description": null }, { - "version": "9.40.51", + "version": "9.41.52", "versionCodes": null, "isExperimental": true, "minSdk": 26, @@ -13444,7 +13444,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -13523,7 +13523,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -13602,7 +13602,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, @@ -13681,7 +13681,7 @@ "description": null }, { - "version": "20.21.37", + "version": "20.26.46", "versionCodes": null, "isExperimental": false, "minSdk": 26, From 3edf03b8efc2f5ea0458456002938c7dbd4bf8d8 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Tue, 27 Oct 2026 20:04:37 +0000 Subject: [PATCH 118/189] fix(YouTube - Spoof video streams): Patch can be included for root installs --- .../spoof/requests/StreamingDataRequest.java | 16 +++++++++++++--- .../interaction/downloads/DownloadsPatch.kt | 2 -- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/spoof/requests/StreamingDataRequest.java b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/spoof/requests/StreamingDataRequest.java index fbe2f812c0..6b52423bef 100644 --- a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/spoof/requests/StreamingDataRequest.java +++ b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/spoof/requests/StreamingDataRequest.java @@ -33,9 +33,15 @@ import app.morphe.extension.shared.Logger; import app.morphe.extension.shared.Utils; -import app.morphe.extension.shared.innertube.ResponseContextOuterClass.*; -import app.morphe.extension.shared.innertube.PlayerResponseOuterClass.*; +import app.morphe.extension.shared.innertube.PlayerResponseOuterClass.AudioConfig; +import app.morphe.extension.shared.innertube.PlayerResponseOuterClass.InlinePlaybackConfig; +import app.morphe.extension.shared.innertube.PlayerResponseOuterClass.PlayabilityStatus; +import app.morphe.extension.shared.innertube.PlayerResponseOuterClass.PlayerConfig; +import app.morphe.extension.shared.innertube.PlayerResponseOuterClass.PlayerGestureConfig; +import app.morphe.extension.shared.innertube.PlayerResponseOuterClass.PlayerResponse; +import app.morphe.extension.shared.innertube.PlayerResponseOuterClass.StreamingData; import app.morphe.extension.shared.innertube.ReelItemWatchResponseOuterClass.ReelItemWatchResponse; +import app.morphe.extension.shared.innertube.utils.AuthUtils; import app.morphe.extension.shared.oauth2.requests.OAuth2Requester; import app.morphe.extension.shared.settings.BaseSettings; import app.morphe.extension.shared.settings.SharedYouTubeSettings; @@ -219,7 +225,11 @@ public static StreamingDataRequest fetchRequestForDownload(String videoId, ClientType[] clientOrder = buildClientOrder(downloadClients, preferredClient) .toArray(new ClientType[0]); // The video details name the saved file, so the download asks for them as well. - return new StreamingDataRequest(videoId, false, lastPlayerHeaders, true, clientOrder, true); + // The auth hook keeps the headers even if spoofing is off, but it is not used by all apps. + Map headers = AuthUtils.getAuthorization().isEmpty() + ? lastPlayerHeaders + : AuthUtils.getRequestHeader(); + return new StreamingDataRequest(videoId, false, headers, true, clientOrder, true); } @Nullable diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/downloads/DownloadsPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/downloads/DownloadsPatch.kt index f3bb31e209..8914610847 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/downloads/DownloadsPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/downloads/DownloadsPatch.kt @@ -37,7 +37,6 @@ import app.morphe.patches.youtube.misc.playercontrols.legacyPlayerControlsPatch import app.morphe.patches.youtube.misc.proto.elementProtoParserHookPatch import app.morphe.patches.youtube.misc.settings.PreferenceScreen import app.morphe.patches.youtube.misc.settings.settingsPatch -import app.morphe.patches.youtube.misc.spoof.spoofVideoStreamsPatch import app.morphe.patches.youtube.shared.Constants.COMPATIBILITY_YOUTUBE import app.morphe.patches.youtube.video.information.videoInformationPatch import app.morphe.util.ResourceGroup @@ -129,7 +128,6 @@ val downloadsPatch = bytecodePatch( videoInformationPatch, legacyPlayerControlsPatch, fixProtoLibraryPatch, - spoofVideoStreamsPatch, elementProtoParserHookPatch, flyoutPatch ) From 7532430e096f5730ba9321b32d8b4bd2c54c77f6 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Tue, 27 Oct 2026 20:13:40 +0000 Subject: [PATCH 119/189] fix(YouTube - Ask flyout menu): Ask chat window can be hidden by onscreen keyboard --- .../patches/ask/AskFlyoutMenuPatch.java | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/ask/AskFlyoutMenuPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/ask/AskFlyoutMenuPatch.java index 18e6be5136..6541079d7e 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/ask/AskFlyoutMenuPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/ask/AskFlyoutMenuPatch.java @@ -9,15 +9,21 @@ import static app.morphe.extension.shared.StringRef.str; +import android.app.Activity; +import android.os.Build; import android.os.SystemClock; +import android.view.View; +import android.view.WindowInsets; import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; import java.lang.ref.WeakReference; import java.util.List; import java.util.Set; import app.morphe.extension.shared.Logger; +import app.morphe.extension.shared.ResourceUtils; import app.morphe.extension.shared.Utils; import app.morphe.extension.youtube.patches.VideoInformation; import app.morphe.extension.youtube.patches.utils.FlyoutUtils; @@ -653,9 +659,79 @@ private static void showAskPanel(String videoId) { } finally { isExecutingOwnCommand = false; } + + AskPanelKeyboard.handleKeyboardInsets(Utils.getActivity()); } catch (Exception ex) { Logger.printException(() -> "showAskPanel failure", ex); Utils.showToastLong(str("morphe_ask_summarize_failed")); } } } + +/** + * Keeps the text field of the Ask panel above the keyboard. + *

    + * The Ask panel opened from a video menu is shown in the panel container of the app, not of the watch page. + * The app is drawn behind the keyboard and this container does not handle the keyboard insets, + * so the keyboard covers the text field at the bottom of the panel. While the keyboard shows, + * the bottom padding of the container is raised by the keyboard height, which moves the text field up. + */ +final class AskPanelKeyboard { + + private static WeakReference panelRef = new WeakReference<>(null); + + /** + * The bottom padding of the panel container without the keyboard. + */ + private static int panelPaddingBottom; + + private AskPanelKeyboard() { + } + + /** + * Handles the keyboard insets of the panel container of the activity, if not already handled. + */ + static void handleKeyboardInsets(@Nullable Activity activity) { + // The keyboard insets are only known with Android 11 and newer. + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { + return; + } + + View panel = activity == null + ? null + : activity.findViewById(ResourceUtils.getIdIdentifier("app_engagement_panel")); + if (panel == null) { + Logger.printDebug(() -> "No engagement panel container"); + return; + } + if (panel == panelRef.get()) { + return; + } + + panelRef = new WeakReference<>(panel); + panelPaddingBottom = panel.getPaddingBottom(); + // A layout pass follows every change of the window insets, such as the keyboard showing or hiding. + panel.getViewTreeObserver().addOnGlobalLayoutListener(() -> updatePadding(panel)); + } + + @RequiresApi(Build.VERSION_CODES.R) + private static void updatePadding(View panel) { + try { + WindowInsets insets = panel.getRootWindowInsets(); + if (insets == null) { + return; + } + + // The panel already keeps its content above the navigation bar, which the keyboard covers. + final int keyboardHeight = Math.max(0, insets.getInsets(WindowInsets.Type.ime()).bottom + - insets.getInsets(WindowInsets.Type.navigationBars()).bottom); + final int paddingBottom = panelPaddingBottom + keyboardHeight; + if (panel.getPaddingBottom() != paddingBottom) { + panel.setPadding(panel.getPaddingLeft(), panel.getPaddingTop(), + panel.getPaddingRight(), paddingBottom); + } + } catch (Exception ex) { + Logger.printException(() -> "updatePadding failure", ex); + } + } +} From dae6bb78c843c92ce92c8c6ba934f868d7e15206 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Tue, 27 Oct 2026 21:59:42 +0000 Subject: [PATCH 120/189] chore(YouTube - Custom branding): Log why a custom header or icon folder fails validation A failed option validation only reports that the value is invalid, so the reason (missing density folder or image) is logged before it. --- .../branding/BaseCustomBrandingPatch.kt | 17 +++++++++--- .../branding/header/BaseChangeHeaderPatch.kt | 26 ++++++++++++++----- 2 files changed, 34 insertions(+), 9 deletions(-) diff --git a/patches/src/main/kotlin/app/morphe/patches/shared/layout/branding/BaseCustomBrandingPatch.kt b/patches/src/main/kotlin/app/morphe/patches/shared/layout/branding/BaseCustomBrandingPatch.kt index a9b7edc810..4da2997c42 100644 --- a/patches/src/main/kotlin/app/morphe/patches/shared/layout/branding/BaseCustomBrandingPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/shared/layout/branding/BaseCustomBrandingPatch.kt @@ -113,14 +113,22 @@ private val notificationIconPngDirectories = mapOf( /** * The icon files of the custom icon folder at [path], each in the resource folder it goes to, or * null when the folder has none or holds only part of the adaptive icon in a density. + * The reason is logged, as a failed option validation does not say what is wrong. */ private fun customIconFiles(path: String): List? { val folder = File(path.trim()) - if (!folder.isDirectory) return null + if (!folder.isDirectory) { + logger.warning("Custom icon path is not a folder: ${folder.absolutePath}") + return null + } val adaptiveIcons = mipmapDirectories.keys.flatMap { density -> val layers = USER_CUSTOM_ADAPTIVE_FILE_NAMES.map { folder.resolve("$density/$it") }.filter(File::isFile) - if (layers.isNotEmpty() && layers.size != USER_CUSTOM_ADAPTIVE_FILE_NAMES.size) return null + if (layers.isNotEmpty() && layers.size != USER_CUSTOM_ADAPTIVE_FILE_NAMES.size) { + logger.warning("Custom icon folder $density must contain all of " + + "${USER_CUSTOM_ADAPTIVE_FILE_NAMES.joinToString()} but only has ${layers.joinToString { it.name }}") + return null + } layers } val optionalIcons = listOf( @@ -128,7 +136,10 @@ private fun customIconFiles(path: String): List? { "drawable/$USER_CUSTOM_NOTIFICATION_ICON_XML_FILE_NAME" ) + notificationIconPngDirectories.keys.map { "$it/$USER_CUSTOM_NOTIFICATION_ICON_PNG_FILE_NAME" } - return (adaptiveIcons + optionalIcons.map(folder::resolve).filter(File::isFile)).ifEmpty { null } + return (adaptiveIcons + optionalIcons.map(folder::resolve).filter(File::isFile)).ifEmpty { + logger.warning("Custom icon folder has none of the expected icon files: ${folder.absolutePath}") + null + } } internal const val EXTENSION_CLASS = "Lapp/morphe/extension/shared/patches/CustomBrandingPatch;" diff --git a/patches/src/main/kotlin/app/morphe/patches/shared/layout/branding/header/BaseChangeHeaderPatch.kt b/patches/src/main/kotlin/app/morphe/patches/shared/layout/branding/header/BaseChangeHeaderPatch.kt index e1cfa62a53..692c944b24 100644 --- a/patches/src/main/kotlin/app/morphe/patches/shared/layout/branding/header/BaseChangeHeaderPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/shared/layout/branding/header/BaseChangeHeaderPatch.kt @@ -20,9 +20,13 @@ import app.morphe.patches.shared.misc.settings.preference.ListPreference import app.morphe.util.ResourceGroup import app.morphe.util.copyResources import java.io.File +import java.util.logging.Logger const val CUSTOM_HEADER_RESOURCE_NAME = "morphe_header_custom" +// Must be named in the 'app.morphe' namespace, otherwise the log is not published. +private val logger = Logger.getLogger(ResourcePatchContext::class.java.name) + internal fun baseChangeHeaderPatch( targetResourceDirectoryNames: Map, variants: Array, @@ -102,13 +106,23 @@ internal fun baseChangeHeaderPatch( /** * The images of the custom header folder at [path], each in the folder of its density, or null * when the folder has none of [densities] or leaves out one of [fileNames] in a density it has. + * The reason is logged, as a failed option validation does not say what is wrong. */ private fun customHeaderImages(path: String, densities: Set, fileNames: Array): List? { - val densityFolders = File(path.trim()).listFiles { file -> file.isDirectory && file.name in densities } - ?.takeIf { it.isNotEmpty() } - ?: return null + val folder = File(path.trim()) + val densityFolders = folder.listFiles { file -> file.isDirectory && file.name in densities } + if (densityFolders.isNullOrEmpty()) { + logger.warning("Custom header folder must contain one of ${densities.joinToString()}: ${folder.absolutePath}") + return null + } - return densityFolders.flatMap { folder -> - fileNames.map(folder::resolve).takeIf { images -> images.all(File::isFile) } ?: return null + return densityFolders.flatMap { densityFolder -> + val images = fileNames.map(densityFolder::resolve) + val missing = images.filterNot(File::isFile) + if (missing.isNotEmpty()) { + logger.warning("Custom header folder ${densityFolder.name} is missing: ${missing.joinToString { it.name }}") + return null + } + images } -} \ No newline at end of file +} From 7faa48834b1f78f8ca572c773392ed9513de8ea3 Mon Sep 17 00:00:00 2001 From: 0xrxL <127248639+0xrxL@users.noreply.github.com> Date: Wed, 28 Oct 2026 06:25:19 +0100 Subject: [PATCH 121/189] fix(YouTube - Video quality): Selected quality is not applied to the already buffered video (#4117) --- .../quality/RememberVideoQualityPatch.java | 29 ++++++++++++ .../youtube/video/quality/Fingerprints.kt | 47 +++++++++++++++++++ .../quality/RememberVideoQualityPatch.kt | 31 ++++++++++++ 3 files changed, 107 insertions(+) diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/playback/quality/RememberVideoQualityPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/playback/quality/RememberVideoQualityPatch.java index e5e3b3a00e..dfd0338fe1 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/playback/quality/RememberVideoQualityPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/playback/quality/RememberVideoQualityPatch.java @@ -54,6 +54,12 @@ public class RememberVideoQualityPatch { */ private static volatile boolean userSelectedQualityIsShorts; + /** + * ID of the video whose buffered video is cleared once the player sets the quality + * selected by the user, or an empty string. + */ + private static volatile String clearVideoBufferVideoId = ""; + /** * The quality selected in the Shorts quality menu, until the menu sets it. */ @@ -64,6 +70,7 @@ private static void setUserSelectedQuality(int resolution, boolean isShorts) { userSelectedQualityResolution = resolution; userSelectedQualityIsShorts = isShorts; userSelectedQualityVideoId = VideoInformation.getVideoId(); + clearVideoBufferVideoId = userSelectedQualityVideoId; } /** @@ -430,4 +437,26 @@ public static boolean useAdvancedVideoQualitySetting() { public static boolean useSelectedQualityFormats(boolean original) { return ShortsPlayerState.isOpen() && isQualitySelectedByUser(); } + + /** + * Injection point. + *

    + * When the quality changes, the player keeps playing the video already buffered and + * plays the new quality only after it. Shorts are often buffered entirely when they open, + * so the quality selected for a Short is not played, not even when the Short loops. + * So the buffered video is cleared the first time the player sets the video formats + * after the user selected the quality of the current video. + * + * @return If the buffered video is cleared. + */ + public static boolean clearVideoBufferOnQualityChange(boolean original) { + final String videoId = clearVideoBufferVideoId; + if (videoId.isEmpty()) { + return original; + } + + clearVideoBufferVideoId = ""; + // Not cleared if another video started before the player set the selected quality. + return original || videoId.equals(VideoInformation.getVideoId()); + } } diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/video/quality/Fingerprints.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/video/quality/Fingerprints.kt index ee4a9501e3..eec94e64d3 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/video/quality/Fingerprints.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/video/quality/Fingerprints.kt @@ -11,6 +11,7 @@ package app.morphe.patches.youtube.video.quality import app.morphe.patcher.Fingerprint +import app.morphe.patcher.InstructionLocation.MatchAfterImmediately import app.morphe.patcher.InstructionLocation.MatchAfterWithin import app.morphe.patcher.checkCast import app.morphe.patcher.fieldAccess @@ -162,6 +163,52 @@ internal object ShortsQualityChangeObserverSecondaryFeatureFlagFingerprint : Fin ) ) +/** + * If this flag is off, the qualities of the quality menu have no formats, and the player ignores + * the formats of the quality last selected and finds the quality only by its resolution. + */ +internal object QualityFormatsFeatureFlagFingerprint : Fingerprint( + filters = listOf( + literal(45400072) + ) +) + +/** + * The method of the player that sets the video formats to play, and clears the buffered video + * if the parameter is true. + */ +internal object PlaybackControllerSetVideoFormatsFingerprint : Fingerprint( + definingClass = "Lcom/google/android/libraries/youtube/media/player/exo2/platypus/PlaybackControllerWrapper;", + accessFlags = listOf(AccessFlags.FINAL), + returnType = "Z", + parameters = listOf("Z"), + filters = listOf( + fieldAccess( + opcode = Opcode.IGET_OBJECT, + type = "Lcom/google/android/libraries/youtube/media/interfaces/PlaybackController;" + ), + opcode(Opcode.IF_NEZ, location = MatchAfterImmediately()), + // The parameter, if the buffered video is cleared. + opcode(Opcode.IF_EQZ, location = MatchAfterWithin(3)), + opcode(Opcode.SGET_OBJECT, location = MatchAfterImmediately()) // Video track type. + ) +) + +/** + * The method of the async media fetch controller that sets the video formats to play. + * If the flag is on, the method sets the formats itself instead of calling + * [PlaybackControllerSetVideoFormatsFingerprint], and clears the buffered video + * if the boolean parameter is true. + */ +internal object AsyncMediaFetchControllerSetVideoFormatsFingerprint : Fingerprint( + definingClass = "Lcom/google/android/libraries/youtube/media/player/exo2/platypus/AsyncMediaFetchControllerWrapper;", + returnType = "V", + parameters = listOf("L", "Z"), + filters = listOf( + literal(45779716) + ) +) + internal fun getCurrentVideoFormatConstructorFingerprint( videoQualityArray: String ) = object : Fingerprint( diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/video/quality/RememberVideoQualityPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/video/quality/RememberVideoQualityPatch.kt index 24fc9388c1..1dad4c546a 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/video/quality/RememberVideoQualityPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/video/quality/RememberVideoQualityPatch.kt @@ -23,6 +23,7 @@ import app.morphe.patches.shared.misc.settings.preference.ListPreference import app.morphe.patches.shared.misc.settings.preference.SwitchPreference import app.morphe.patches.youtube.misc.extension.sharedExtensionPatch import app.morphe.patches.youtube.misc.playertype.playerTypeHookPatch +import app.morphe.patches.youtube.misc.playservice.is_21_17_or_greater import app.morphe.patches.youtube.misc.playservice.versionCheckPatch import app.morphe.patches.youtube.misc.settings.settingsPatch import app.morphe.patches.youtube.shared.VideoQualityChangedFingerprint @@ -355,5 +356,35 @@ val rememberVideoQualityPatch = bytecodePatch { "$EXTENSION_CLASS->useSelectedQualityFormats(Z)Z" ) } + + // The player plays another quality of the same resolution if this flag is off, + // such as 720p after selecting a 480p quality that YouTube gives the resolution 720. + // Without a quality selected in the Shorts quality menu, the player has no formats to use. + QualityFormatsFeatureFlagFingerprint.let { + it.method.insertLiteralOverride( + it.instructionMatches.first().index, + true + ) + } + + // When the quality changes, the player keeps playing the video already buffered, + // so the buffered video is cleared when the quality selected by the user is set. + if (is_21_17_or_greater) { + PlaybackControllerSetVideoFormatsFingerprint.method.addInstructions( + 0, + """ + invoke-static { p1 }, $EXTENSION_CLASS->clearVideoBufferOnQualityChange(Z)Z + move-result p1 + """ + ) + + AsyncMediaFetchControllerSetVideoFormatsFingerprint.method.addInstructions( + 0, + """ + invoke-static { p2 }, $EXTENSION_CLASS->clearVideoBufferOnQualityChange(Z)Z + move-result p2 + """ + ) + } } } From 9d3b53c92cb877f309b4463b7d5067aaa1a996a3 Mon Sep 17 00:00:00 2001 From: 0xrxL <127248639+0xrxL@users.noreply.github.com> Date: Wed, 28 Oct 2026 06:33:16 +0100 Subject: [PATCH 122/189] fix(YouTube - Hide status bar): Top bars are covered by a second status bar when the app is kept out of the camera cutout (#4112) --- .../youtube/patches/HideStatusBarPatch.java | 178 +++++++----------- .../extension/youtube/settings/Settings.java | 1 + .../layout/statusbar/HideStatusBarPatch.kt | 3 +- 3 files changed, 75 insertions(+), 107 deletions(-) diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/HideStatusBarPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/HideStatusBarPatch.java index 13d306fee6..0c13204fbc 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/HideStatusBarPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/HideStatusBarPatch.java @@ -10,29 +10,26 @@ import android.app.Activity; import android.app.Application; import android.app.Dialog; +import android.content.res.Configuration; import android.graphics.Insets; import android.graphics.Rect; import android.os.Build; import android.os.Bundle; import android.view.DisplayCutout; import android.view.View; -import android.view.ViewGroup; -import android.view.ViewParent; import android.view.Window; import android.view.WindowInsets; import android.view.WindowInsetsController; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; import java.util.Collections; import java.util.Set; import java.util.WeakHashMap; import app.morphe.extension.shared.Logger; -import app.morphe.extension.shared.ResourceType; -import app.morphe.extension.shared.ResourceUtils; -import app.morphe.extension.shared.Utils; import app.morphe.extension.youtube.settings.Settings; import app.morphe.extension.youtube.shared.PlayerType; @@ -72,31 +69,25 @@ public void onActivityDestroyed(@NonNull Activity a) {} private static boolean activityLifecycleCallbacksRegistered; /** - * The layout that holds the toolbar of the pages. With the edge to edge top bar, its top padding - * is the room left for the status bar. + * Main activities whose window position is watched once they are started. */ - private static final int TOOLBAR_CONTAINER = ResourceUtils.getIdentifier( - ResourceType.ID, "toolbar_container"); - - /** - * Main activities whose top bars are kept below the status bar once they are started. - */ - private static final Set topBarActivities = Collections.newSetFromMap(new WeakHashMap<>()); + private static final Set windowPositionActivities = Collections.newSetFromMap(new WeakHashMap<>()); /** * Callback when an activity is started. The window of the main activity is not created - * when it is initialized, so the top bars are watched once it is started. + * when it is initialized, so its position is watched once it is started. */ - private static final Application.ActivityLifecycleCallbacks TOP_BAR_LIFECYCLE_CALLBACKS + private static final Application.ActivityLifecycleCallbacks WINDOW_POSITION_LIFECYCLE_CALLBACKS = new Application.ActivityLifecycleCallbacks() { public void onActivityStarted(@NonNull Activity activity) { try { - if (topBarActivities.remove(activity)) { - keepTopBarsBelowStatusBar(activity.getWindow().getDecorView()); + if (windowPositionActivities.remove(activity) + && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + watchWindowPosition(activity); } } catch (Exception ex) { - Logger.printException(() -> "onActivityStarted top bar failure", ex); + Logger.printException(() -> "onActivityStarted window position failure", ex); } } @@ -108,7 +99,7 @@ public void onActivitySaveInstanceState(@NonNull Activity a, @NonNull Bundle b) public void onActivityDestroyed(@NonNull Activity a) {} }; - private static boolean topBarLifecycleCallbacksRegistered; + private static boolean windowPositionLifecycleCallbacksRegistered; /** * If a video is watched in the player that fills the screen in portrait, not in fullscreen. @@ -142,10 +133,12 @@ private static boolean isPatchIncluded() { /** * @return If the edge to edge top bar feature flag is turned off for the whole session - * even if it is not read yet: with the status bar layout fix and the status bar hidden. + * even if it is not read yet: with the status bar layout fix, and the status bar hidden + * or the window laid out below the status bar. */ private static boolean isEdgeToEdgeTopBarTurnedOff() { - return Settings.FIX_STATUS_BAR_LAYOUT.get() && Settings.HIDE_STATUS_BAR.get(); + return Settings.FIX_STATUS_BAR_LAYOUT.get() + && (Settings.HIDE_STATUS_BAR.get() || Settings.WINDOW_BELOW_STATUS_BAR.get()); } /** @@ -173,6 +166,9 @@ static Boolean isEdgeToEdgeTopBar() { * With the status bar hidden, the edge to edge layout leaves an empty bar of the height of * the status bar above the player, so the flag is turned off and the top bars are laid out * with {@link #setSystemWindowInsetTop(Rect, View)}. + *

    + * The flag is also turned off if the window was laid out below the status bar, + * see {@link #watchWindowPosition(Activity)}. */ public static boolean useEdgeToEdgeTopBar(boolean original) { if (!Settings.FIX_STATUS_BAR_LAYOUT.get()) { @@ -187,10 +183,12 @@ public static boolean useEdgeToEdgeTopBar(boolean original) { synchronized (HideStatusBarPatch.class) { value = edgeToEdgeTopBar; if (value == null) { - final boolean used = original && !Settings.HIDE_STATUS_BAR.get(); + final boolean used = original && !Settings.HIDE_STATUS_BAR.get() + && !Settings.WINDOW_BELOW_STATUS_BAR.get(); value = used; edgeToEdgeTopBar = value; - Logger.printDebug(() -> "Edge to edge top bar: " + used + " (original: " + original + ")"); + Logger.printDebug(() -> "Edge to edge top bar: " + used + " (original: " + original + + ", window below status bar: " + Settings.WINDOW_BELOW_STATUS_BAR.get() + ")"); } } } @@ -227,8 +225,9 @@ public static boolean useStatusBarScrollColor(boolean original) { * Called when the main activity is created. */ public static void initialize(Activity activity) { + registerWindowPositionWatch(activity); + if (!Settings.HIDE_STATUS_BAR.get()) { - watchTopBars(activity); return; } @@ -255,112 +254,79 @@ public static void initialize(Activity activity) { } } - /** - * Keeps the top bars of the main activity below the status bar, with the status bar layout fix - * and the status bar not hidden. With the status bar hidden, the top bars are laid out - * by {@link #setSystemWindowInsetTop(Rect, View)} instead. - */ - private static void watchTopBars(Activity activity) { - if (!Settings.FIX_STATUS_BAR_LAYOUT.get() || TOOLBAR_CONTAINER == 0) { + private static void registerWindowPositionWatch(Activity activity) { + if (!Settings.FIX_STATUS_BAR_LAYOUT.get() || Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { return; } try { - if (!topBarLifecycleCallbacksRegistered) { - topBarLifecycleCallbacksRegistered = true; - activity.getApplication().registerActivityLifecycleCallbacks(TOP_BAR_LIFECYCLE_CALLBACKS); + if (!windowPositionLifecycleCallbacksRegistered) { + windowPositionLifecycleCallbacksRegistered = true; + activity.getApplication().registerActivityLifecycleCallbacks(WINDOW_POSITION_LIFECYCLE_CALLBACKS); } - topBarActivities.add(activity); + windowPositionActivities.add(activity); } catch (Exception ex) { - Logger.printException(() -> "watchTopBars failure", ex); + Logger.printException(() -> "registerWindowPositionWatch failure", ex); } } /** - * The room for the status bar above the toolbar comes from many classes, and on some devices - * they do not agree on the edge to edge top bar (https://github.com/MorpheApp/morphe-patches/issues/2969). - * The room is then left out, so the status bar view covers the toolbar and its buttons, - * or it is added twice, leaving an empty bar above the toolbar. - * Whenever the window is laid out with the status bar visible, the top padding of the toolbar - * container is set so that the room above the toolbar is the height of the status bar once. + * Some devices keep the window out of the status bar area, for example Samsung devices with + * the app not allowed to use the camera cutout area. The edge to edge top bar still makes room + * for the status bar above the top bars, which is then drawn as a second status bar below + * the real one and covers the top bars (https://github.com/MorpheApp/morphe-patches/issues/2969). + *

    + * The flag is read before the window is laid out, so the position is saved and the flag is + * turned off from the next app start. Only the full screen window in portrait with the status bar + * shown is checked, where the status bar is above the window. */ - private static void keepTopBarsBelowStatusBar(View decorView) { + @RequiresApi(api = Build.VERSION_CODES.R) + private static void watchWindowPosition(Activity activity) { + View decorView = activity.getWindow().getDecorView(); + // Width and height of the window last checked, so the position is only read when they change. + final int[] lastSize = {-1, -1}; + final int[] location = new int[2]; + decorView.getViewTreeObserver().addOnGlobalLayoutListener(() -> { try { - // Only with the edge to edge top bar flag handed to the app by this patch. - if (!Settings.FIX_STATUS_BAR_LAYOUT.get() || Settings.HIDE_STATUS_BAR.get() - || edgeToEdgeTopBarUsed == null) { - return; - } - - WindowInsets insets = decorView.getRootWindowInsets(); - if (insets == null || !isStatusBarVisible(decorView)) { + // With the status bar hidden, the window can be laid out below the display cutout instead. + if (activity.isInMultiWindowMode() || activity.isInPictureInPictureMode() + || activity.getResources().getConfiguration().orientation + != Configuration.ORIENTATION_PORTRAIT + || !isStatusBarVisible(decorView)) { return; } - final int statusBarHeight = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R - ? insets.getInsets(WindowInsets.Type.statusBars()).top - : insets.getSystemWindowInsetTop(); - if (statusBarHeight <= 0) { - return; - } - - // With the edge to edge top bar, the app lays out its whole layout behind the status bar. - // If the layout still makes room for the status bar, it is drawn as a dark bar over - // the top bars, so the layout is drawn behind the status bar the same as the app does. - View content = decorView.findViewById(android.R.id.content); - View layout = content instanceof ViewGroup group && group.getChildCount() > 0 - ? group.getChildAt(0) - : null; - if (layout != null && layout.getPaddingTop() > 0 && edgeToEdgeTopBarUsed) { - Logger.printDebug(() -> "Layout room for the status bar: " + layout.getPaddingTop()); - layout.setFitsSystemWindows(false); - layout.setPadding(layout.getPaddingLeft(), 0, layout.getPaddingRight(), layout.getPaddingBottom()); - layout.requestApplyInsets(); + final int width = decorView.getWidth(); + final int height = decorView.getHeight(); + if ((width == lastSize[0] && height == lastSize[1]) || width == 0 || height == 0) { return; } + lastSize[0] = width; + lastSize[1] = height; + + decorView.getLocationOnScreen(location); + final int windowTop = location[1]; + final boolean belowStatusBar = windowTop > 0; + Logger.printDebug(() -> { + WindowInsets insets = decorView.getRootWindowInsets(); + return "Window top: " + windowTop + " height: " + height + (insets == null ? "" : + " status bar inset: " + insets.getInsets(WindowInsets.Type.statusBars()).top + + " stable: " + insets.getInsetsIgnoringVisibility(WindowInsets.Type.statusBars()).top + + " cutout: " + insets.getInsets(WindowInsets.Type.displayCutout()).top); + }); - View toolbarContainer = decorView instanceof ViewGroup decorGroup - ? Utils.getChildView(decorGroup, true, - view -> view.getId() == TOOLBAR_CONTAINER && view.isShown()) - : null; - if (toolbarContainer != null) { - setStatusBarRoom(decorView, toolbarContainer, statusBarHeight); + if (belowStatusBar != Settings.WINDOW_BELOW_STATUS_BAR.get()) { + Logger.printDebug(() -> "Window below status bar: " + belowStatusBar + + ", applied from the next app start"); + Settings.WINDOW_BELOW_STATUS_BAR.save(belowStatusBar); } } catch (Exception ex) { - Logger.printException(() -> "keepTopBarsBelowStatusBar failure", ex); + Logger.printException(() -> "watchWindowPosition failure", ex); } }); } - private static void setStatusBarRoom(View decorView, View toolbarContainer, int statusBarHeight) { - // The room above the toolbar: the top padding and margin of the toolbar container - // and of every layout that holds it. Scrolling the top bars away only moves them. - int room = toolbarContainer.getPaddingTop(); - for (View view = toolbarContainer; view != decorView; ) { - if (view.getLayoutParams() instanceof ViewGroup.MarginLayoutParams params) { - room += params.topMargin; - } - ViewParent parent = view.getParent(); - if (!(parent instanceof View)) { - break; - } - view = (View) parent; - room += view.getPaddingTop(); - } - - final int roomAbove = room; - final int difference = statusBarHeight - roomAbove; - final int paddingTop = toolbarContainer.getPaddingTop() + difference; - if (difference == 0 || paddingTop < 0) { - return; - } - - Logger.printDebug(() -> "Toolbar room above: " + roomAbove + " status bar: " + statusBarHeight); - toolbarContainer.setPadding(toolbarContainer.getPaddingLeft(), paddingTop, - toolbarContainer.getPaddingRight(), toolbarContainer.getPaddingBottom()); - } - private static void hideStatusBar(Activity activity) { if (!activities.add(activity)) { return; diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/Settings.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/Settings.java index 6265647deb..f03e2beb6f 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/Settings.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/Settings.java @@ -561,6 +561,7 @@ public class Settings extends SharedYouTubeSettings { public static final EnumSetting CHANGE_START_PAGE = new EnumSetting<>("morphe_change_start_page", StartPage.DEFAULT, true); public static final BooleanSetting HIDE_STATUS_BAR = new BooleanSetting("morphe_hide_status_bar", FALSE, true); public static final BooleanSetting FIX_STATUS_BAR_LAYOUT = new BooleanSetting("morphe_fix_status_bar_layout", TRUE, true); + public static final BooleanSetting WINDOW_BELOW_STATUS_BAR = new BooleanSetting("morphe_window_below_status_bar", FALSE, false, false); public static final EnumSetting FORCE_ANIMATIONS = new EnumSetting<>("morphe_force_animations", AnimationsMode.DEFAULT); // Custom filter diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/statusbar/HideStatusBarPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/statusbar/HideStatusBarPatch.kt index f74a0fe31c..f3f202c205 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/statusbar/HideStatusBarPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/statusbar/HideStatusBarPatch.kt @@ -49,7 +49,8 @@ val hideStatusBarPatch = bytecodePatch( // so the status bar and its background view cover them. Every class uses the first value read, // instead of always turning the flag off: the edge to edge layout is the stock layout of most accounts // (transparent status bar over Shorts and the player). The flag is turned off only when the - // status bar is hidden, as it then leaves an empty bar above the player. + // status bar is hidden, as it then leaves an empty bar above the player, and when the window + // is laid out below the status bar, as the room left for the status bar then covers the top bars. EdgeToEdgeTopBarFeatureFlagFingerprint.matchAll().forEach { // Overrides every read of the flag in the method. it.method.insertLiteralOverride( From 9542d24adf3b5ec2c25fce931418160dadbde93c Mon Sep 17 00:00:00 2001 From: pharmacomaniac Date: Wed, 28 Oct 2026 03:50:31 -0400 Subject: [PATCH 123/189] fix(YouTube): Restore TalkBack access to Shorts comments and descriptions (#4128) --- .../misc/fix/accessibility/Fingerprints.kt | 39 +++++++++++++++++ .../FixShortsAccessibilityPatch.kt | 43 +++++++++++++++++++ .../misc/playservice/VersionCheckPatch.kt | 3 ++ .../youtube/misc/settings/SettingsPatch.kt | 2 + 4 files changed, 87 insertions(+) create mode 100644 patches/src/main/kotlin/app/morphe/patches/youtube/misc/fix/accessibility/Fingerprints.kt create mode 100644 patches/src/main/kotlin/app/morphe/patches/youtube/misc/fix/accessibility/FixShortsAccessibilityPatch.kt diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/misc/fix/accessibility/Fingerprints.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/fix/accessibility/Fingerprints.kt new file mode 100644 index 0000000000..54bd09460c --- /dev/null +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/fix/accessibility/Fingerprints.kt @@ -0,0 +1,39 @@ +package app.morphe.patches.youtube.misc.fix.accessibility + +import app.morphe.patcher.Fingerprint +import app.morphe.patcher.InstructionLocation.MatchAfterImmediately +import app.morphe.patcher.InstructionLocation.MatchAfterWithin +import app.morphe.patcher.literal +import app.morphe.patcher.methodCall +import app.morphe.patcher.opcode +import com.android.tools.smali.dexlib2.Opcode + +private const val BOTTOM_UI_CONTAINER = + "Lcom/google/android/apps/youtube/app/common/ui/bottomui/BottomUiContainer;" + +internal object BottomUiKeyboardVisibilityFingerprint : Fingerprint( + returnType = "V", + parameters = listOf("L"), + filters = listOf( + literal(45731499L), + opcode(Opcode.IGET_BOOLEAN, MatchAfterWithin(10)), + opcode(Opcode.IF_EQ, MatchAfterWithin(2)), + opcode(Opcode.MOVE, MatchAfterImmediately()), + opcode(Opcode.GOTO, MatchAfterImmediately()), + literal(8, opcodes = listOf(Opcode.CONST_16), MatchAfterImmediately()), + methodCall( + opcode = Opcode.INVOKE_VIRTUAL, + smali = "$BOTTOM_UI_CONTAINER->setVisibility(I)V", + location = MatchAfterImmediately() + ) + ) +) + +internal object BottomUiVisibilityGateFingerprint : Fingerprint( + definingClass = BOTTOM_UI_CONTAINER, + returnType = "V", + parameters = listOf("Z"), + filters = listOf( + methodCall(smali = "$BOTTOM_UI_CONTAINER->setVisibility(I)V") + ) +) diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/misc/fix/accessibility/FixShortsAccessibilityPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/fix/accessibility/FixShortsAccessibilityPatch.kt new file mode 100644 index 0000000000..95171d3510 --- /dev/null +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/fix/accessibility/FixShortsAccessibilityPatch.kt @@ -0,0 +1,43 @@ +package app.morphe.patches.youtube.misc.fix.accessibility + +import app.morphe.patcher.extensions.InstructionExtensions.addInstructions +import app.morphe.patcher.extensions.InstructionExtensions.getInstruction +import app.morphe.patcher.extensions.InstructionExtensions.removeInstructions +import app.morphe.patcher.patch.bytecodePatch +import app.morphe.patches.youtube.misc.playservice.is_20_51_or_greater +import app.morphe.patches.youtube.misc.playservice.is_21_28_or_greater +import app.morphe.patches.youtube.misc.playservice.versionCheckPatch +import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction + +internal val fixShortsAccessibilityPatch = bytecodePatch( + description = "Fixes Shorts comments and descriptions being inaccessible to screen readers." +) { + dependsOn(versionCheckPatch) + + execute { + if (!is_20_51_or_greater || is_21_28_or_greater) return@execute + + val visibilityGate = BottomUiVisibilityGateFingerprint.originalMethod + BottomUiKeyboardVisibilityFingerprint.let { + val keyboardIndex = it.instructionMatches[1].index + val visibilityIndex = it.instructionMatches.last().index + val method = it.method + val keyboardRegister = method.getInstruction(keyboardIndex).registerA + val containerRegister = method.getInstruction(visibilityIndex).registerC + + // An empty BottomUiContainer can hide Shorts panels from TalkBack (#3417). + // Backport YouTube 21.28.208's keyboard visibility gate to preserve the requested visibility. + method.apply { + removeInstructions(keyboardIndex + 1, visibilityIndex - keyboardIndex) + addInstructions( + keyboardIndex + 1, + """ + xor-int/lit8 v$keyboardRegister, v$keyboardRegister, 0x1 + invoke-virtual { v$containerRegister, v$keyboardRegister }, $visibilityGate + """ + ) + } + } + } +} diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/misc/playservice/VersionCheckPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/playservice/VersionCheckPatch.kt index 7f407586e6..8d0ce2bd33 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/misc/playservice/VersionCheckPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/playservice/VersionCheckPatch.kt @@ -41,6 +41,8 @@ var is_20_47_or_greater : Boolean by Delegates.notNull() private set var is_20_49_or_greater : Boolean by Delegates.notNull() private set +var is_20_51_or_greater : Boolean by Delegates.notNull() + private set var is_21_02_or_greater : Boolean by Delegates.notNull() private set var is_21_03_or_greater : Boolean by Delegates.notNull() @@ -138,6 +140,7 @@ val versionCheckPatch = bytecodePatch { is_20_46_or_greater = isEqualsOrGreaterThan("20.46.00") is_20_47_or_greater = isEqualsOrGreaterThan("20.47.00") is_20_49_or_greater = isEqualsOrGreaterThan("20.49.00") + is_20_51_or_greater = isEqualsOrGreaterThan("20.51.00") is_21_02_or_greater = isEqualsOrGreaterThan("21.02.00") is_21_03_or_greater = isEqualsOrGreaterThan("21.03.000") is_21_04_or_greater = isEqualsOrGreaterThan("21.04.000") diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/misc/settings/SettingsPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/settings/SettingsPatch.kt index 3e92295cbf..cb6e791ac0 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/misc/settings/SettingsPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/settings/SettingsPatch.kt @@ -48,6 +48,7 @@ import app.morphe.patches.shared.misc.settings.preference.SwitchPreference import app.morphe.patches.shared.misc.settings.preference.TextPreference import app.morphe.patches.shared.misc.settings.settingsPatch import app.morphe.patches.youtube.misc.extension.sharedExtensionPatch +import app.morphe.patches.youtube.misc.fix.accessibility.fixShortsAccessibilityPatch import app.morphe.patches.youtube.misc.fix.contentprovider.fixContentProviderPatch import app.morphe.patches.youtube.misc.fix.likebutton.fixLikeButtonPatch import app.morphe.patches.youtube.misc.fix.pipchatbar.fixPipChatBarPatch @@ -219,6 +220,7 @@ val settingsPatch = bytecodePatch( versionCheckPatch, // Currently there is no easy way to make patches mandatory, // so for now these are all dependents of this patch. + fixShortsAccessibilityPatch, fixPlaybackSpeedWhilePlayingPatch, fixPreferenceIconPatch, fixLikeButtonPatch, From 7f1a6b35c8a3989cd2b06a1e81d817015f2ca433 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Wed, 28 Oct 2026 07:48:10 +0000 Subject: [PATCH 124/189] fix(YouTube - Theme): Glow startup animation does not extend to fullscreen --- .../youtube/patches/theme/ThemePatch.java | 277 ++++++++++++++++-- .../extension/youtube/settings/Settings.java | 1 + .../youtube/layout/theme/Fingerprints.kt | 10 + .../layout/theme/SplashAnimationPatch.kt | 7 + .../addresources/values/youtube/strings.xml | 1 + 5 files changed, 278 insertions(+), 18 deletions(-) diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/theme/ThemePatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/theme/ThemePatch.java index 613590c49f..e49913d228 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/theme/ThemePatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/theme/ThemePatch.java @@ -10,6 +10,8 @@ package app.morphe.extension.youtube.patches.theme; +import static app.morphe.extension.shared.StringRef.str; + import android.app.Activity; import android.content.Context; import android.content.ContextWrapper; @@ -17,14 +19,19 @@ import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.Color; +import android.graphics.Rect; import android.graphics.drawable.ColorDrawable; import android.os.Build; +import android.util.DisplayMetrics; import android.util.TypedValue; import android.view.View; +import android.view.ViewGroup; import android.widget.Spinner; import androidx.annotation.ColorInt; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; import com.airbnb.lottie.LottieAnimationView; @@ -35,6 +42,7 @@ import app.morphe.extension.shared.Logger; import app.morphe.extension.shared.ResourceType; import app.morphe.extension.shared.ResourceUtils; +import app.morphe.extension.shared.Utils; import app.morphe.extension.shared.patches.SplashAnimationPatch; import app.morphe.extension.shared.patches.SplashAnimationPatch.SplashScreenAnimationStyle; import app.morphe.extension.shared.settings.SharedYouTubeSettings; @@ -93,18 +101,60 @@ public static boolean useLotteLaunchSplashScreen(boolean original) { } /** - * The glow animation is drawn on a dark square in the middle of the screen, - * and the rest of the screen is given the same color. + * The dark backdrop the glow animation is drawn on. */ @ColorInt private static final int RIVE_GLOW_ORIGINAL_BACKGROUND_COLOR = 0xFF09090A; /** - * The dark square of the glow animation, as it is written in the Rive file: + * The backdrop color of the glow animation, as it is written in the Rive file: * a color keyframe (property key 88) followed by the ARGB color in little endian order. */ private static final byte[] RIVE_GLOW_BACKGROUND_KEYFRAME = { 0x58, 0x0A, 0x09, 0x09, (byte) 0xFF }; + /** + * The glow animation is made for a 440 x 956 phone screen, but the app shows only + * a square in the middle of it, and the glow is cut off at the edges of the square. + */ + private static final float RIVE_GLOW_ARTBOARD_WIDTH = 440; + private static final float RIVE_GLOW_ARTBOARD_HEIGHT = 956; + + /** + * The artboard size of the glow animation, as it is written in the Rive file: + * the width (property key 7) and height (property key 8), each followed by a little endian float. + */ + private static final byte[] RIVE_GLOW_ARTBOARD_SIZE = { + 0x07, 0x00, 0x00, (byte) 0xDC, 0x43, + 0x08, 0x00, 0x00, 0x6F, 0x44 + }; + + private static final byte RIVE_PROPERTY_KEY_WIDTH = 7; + private static final byte RIVE_PROPERTY_KEY_HEIGHT = 8; + private static final byte RIVE_PROPERTY_KEY_ORIGIN_X = 11; + private static final byte RIVE_PROPERTY_KEY_ORIGIN_Y = 12; + + /** + * The timeline of the glow animation, as it is written in the Rive file: a linear animation + * (type 31) named "Timeline 1", 150 frames long, that loops. Its keyed objects follow it. + */ + private static final byte[] RIVE_GLOW_TIMELINE = { + 0x1F, 0x37, 0x0A, 'T', 'i', 'm', 'e', 'l', 'i', 'n', 'e', ' ', '1', + 0x39, (byte) 0x96, 0x01, 0x3B, 0x01, 0x00 + }; + + /** + * Keyframes added to the timeline of the glow animation. The animation starts with a glow + * that covers the whole screen, and it fades in from the backdrop instead of appearing at once: + * the artboard (keyed object type 25, object id 0) has its opacity (keyed property type 26, + * property key 18) go from 0 at frame 0 to 1 at frame 20 (linear keyframes of type 30). + */ + private static final byte[] RIVE_GLOW_FADE_IN_KEYFRAMES = { + 0x19, 0x33, 0x00, 0x00, + 0x1A, 0x35, 0x12, 0x00, + 0x1E, 0x44, 0x01, 0x46, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x1E, 0x43, 0x14, 0x44, 0x01, 0x46, 0x00, 0x00, (byte) 0x80, 0x3F, 0x00 + }; + /** * The key the app keeps the Rive file of a raw resource in memory with, followed by the resource id. */ @@ -116,10 +166,56 @@ public static boolean useLotteLaunchSplashScreen(boolean original) { @ColorInt private static volatile int riveGlowBackgroundColor; + /** + * The artboard size that fills the screen at the scale the app draws the glow animation with, + * or 0 to keep the original size. + */ + private static volatile float riveGlowArtboardWidth; + private static volatile float riveGlowArtboardHeight; + + /** + * How long the system splash screen is kept at most while the glow animation loads. + */ + private static final long RIVE_SPLASH_SCREEN_TIMEOUT_MILLISECONDS = 1500; + + /** + * How long the system splash screen fades out when the glow animation has drawn. + */ + private static final long RIVE_SPLASH_SCREEN_FADE_OUT_MILLISECONDS = 300; + + /** + * Removes the system splash screen that is kept while the glow animation loads, + * or null if it is not kept. Used on the main thread. + */ + @Nullable + private static volatile Runnable riveSplashScreenRemover; + + private static boolean riveSplashScreenCrashChecked; + private static boolean isRiveSplashScreenEnabled() { return SharedYouTubeSettings.SPLASH_SCREEN_STYLE.get() == SplashScreenAnimationStyle.RIVE_GLOW; } + /** + * Some devices crash when drawing the glow animation, because the Rive renderer + * uses GPU features they do not have. If the app closed while the animation was shown, + * the animation is turned off. Checked once when the app starts, before this launch + * shows the animation. + */ + private static synchronized void checkRiveSplashScreenCrash() { + if (riveSplashScreenCrashChecked) { + return; + } + riveSplashScreenCrashChecked = true; + + if (Settings.RIVE_SPLASH_SCREEN_SHOWING.get()) { + Logger.printInfo(() -> "App closed while the glow splash screen was shown, resetting splash screen style"); + Settings.RIVE_SPLASH_SCREEN_SHOWING.save(false); + SharedYouTubeSettings.SPLASH_SCREEN_STYLE.resetToDefault(); + Utils.showToastLong(str("morphe_splash_screen_style_glow_not_supported")); + } + } + private static boolean isRiveGlowFile(int resourceId) { return resourceId == ResourceUtils.getIdentifier(ResourceType.RAW, "rive_phantom"); } @@ -128,6 +224,14 @@ private static boolean isRiveGlowFile(int resourceId) { * Injection point. */ public static boolean useRiveSplashScreen(boolean original) { + try { + if (isRiveSplashScreenEnabled()) { + checkRiveSplashScreenCrash(); + } + } catch (Exception ex) { + Logger.printException(() -> "useRiveSplashScreen failure", ex); + } + return isRiveSplashScreenEnabled(); } @@ -201,18 +305,104 @@ public static void setRiveSplashScreenBackground(View animationView) { color = RIVE_GLOW_ORIGINAL_BACKGROUND_COLOR; } + // The app draws the artboard to cover a square view, so the width of the artboard + // fits the square. The view is changed to fill the screen, and the artboard is + // given the shape of the screen at that same scale. + float artboardWidth = 0; + float artboardHeight = 0; + ViewGroup.LayoutParams params = animationView.getLayoutParams(); + if (context instanceof Activity activity && params.width > 0) { + final float artboardUnitsPerPixel = RIVE_GLOW_ARTBOARD_WIDTH / params.width; + Rect windowBounds = getWindowBounds(activity); + artboardWidth = windowBounds.width() * artboardUnitsPerPixel; + artboardHeight = windowBounds.height() * artboardUnitsPerPixel; + + params.width = ViewGroup.LayoutParams.MATCH_PARENT; + params.height = ViewGroup.LayoutParams.MATCH_PARENT; + animationView.setLayoutParams(params); + } else { + Logger.printDebug(() -> "Glow animation view is not a fixed size"); + } + + riveGlowArtboardWidth = artboardWidth; + riveGlowArtboardHeight = artboardHeight; riveGlowBackgroundColor = color; parent.setBackgroundColor(color); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && context instanceof Activity activity) { + keepSplashScreenUntilRiveFrame(activity); + } + + // The app removes the view when the animation is closed. + Settings.RIVE_SPLASH_SCREEN_SHOWING.save(true); + animationView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() { + @Override + public void onViewAttachedToWindow(@NonNull View view) { + } + + @Override + public void onViewDetachedFromWindow(@NonNull View view) { + view.removeOnAttachStateChangeListener(this); + Settings.RIVE_SPLASH_SCREEN_SHOWING.save(false); + } + }); } catch (Exception ex) { Logger.printException(() -> "setRiveSplashScreenBackground failure", ex); } } + /** + * The system removes its splash screen when the app has drawn, which is before + * the glow animation has loaded. The splash screen is instead kept until the + * animation has drawn, and then it fades out while the animation fades in. + */ + @RequiresApi(Build.VERSION_CODES.S) + private static void keepSplashScreenUntilRiveFrame(Activity activity) { + activity.getSplashScreen().setOnExitAnimationListener(splashScreenView -> { + riveSplashScreenRemover = () -> splashScreenView.animate() + .alpha(0) + .setDuration(RIVE_SPLASH_SCREEN_FADE_OUT_MILLISECONDS) + .withEndAction(splashScreenView::remove); + + // In case the animation never draws. + Utils.runOnMainThreadDelayed(ThemePatch::removeRiveSplashScreen, + RIVE_SPLASH_SCREEN_TIMEOUT_MILLISECONDS); + }); + } + + private static void removeRiveSplashScreen() { + Runnable remover = riveSplashScreenRemover; + if (remover != null) { + riveSplashScreenRemover = null; + remover.run(); + } + } + + /** + * Injection point. + *

    + * Called for each frame the Rive renderer draws. + */ + public static void onRiveFrameAvailable() { + if (riveSplashScreenRemover != null) { + Utils.runOnMainThreadNowOrLater(ThemePatch::removeRiveSplashScreen); + } + } + + private static Rect getWindowBounds(Activity activity) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + return activity.getWindowManager().getCurrentWindowMetrics().getBounds(); + } + + DisplayMetrics metrics = activity.getResources().getDisplayMetrics(); + return new Rect(0, 0, metrics.widthPixels, metrics.heightPixels); + } + /** * Injection point. *

    * The app keeps the Rive file of a raw resource in memory, and the key of the glow animation - * is changed for each background color. Otherwise a file with the background color of + * is changed for each background color and artboard size. Otherwise a file changed for * an earlier launch would be used. * * @param key The key of a raw resource, which is the prefix followed by the resource id. @@ -222,10 +412,10 @@ public static String getRiveRawResourceKey(String key) { try { final int backgroundColor = riveGlowBackgroundColor; if (backgroundColor != 0 - && backgroundColor != RIVE_GLOW_ORIGINAL_BACKGROUND_COLOR && key.startsWith(RIVE_RAW_RESOURCE_KEY_PREFIX) && isRiveGlowFile(Integer.parseInt(key.substring(RIVE_RAW_RESOURCE_KEY_PREFIX.length())))) { - return key + "_" + Integer.toHexString(backgroundColor); + return key + "_" + Integer.toHexString(backgroundColor) + + "_" + riveGlowArtboardWidth + "x" + riveGlowArtboardHeight; } } catch (Exception ex) { Logger.printException(() -> "getRiveRawResourceKey failure", ex); @@ -238,7 +428,7 @@ && isRiveGlowFile(Integer.parseInt(key.substring(RIVE_RAW_RESOURCE_KEY_PREFIX.le * Injection point. *

    * Called when the app reads the Rive file of a raw resource it has not cached yet. - * The splash screen reads its file after the background is set. + * The splash screen reads its file after the background and size are set. * * @param fileBytes The Rive file of a raw resource, or null if it could not be read. * @param resourceId The raw resource the file was read from. @@ -248,23 +438,28 @@ && isRiveGlowFile(Integer.parseInt(key.substring(RIVE_RAW_RESOURCE_KEY_PREFIX.le public static byte[] getRiveFile(@Nullable byte[] fileBytes, int resourceId) { try { final int backgroundColor = riveGlowBackgroundColor; - if (fileBytes == null - || backgroundColor == 0 - || backgroundColor == RIVE_GLOW_ORIGINAL_BACKGROUND_COLOR - || !isRiveGlowFile(resourceId)) { + if (fileBytes == null || backgroundColor == 0 || !isRiveGlowFile(resourceId)) { return fileBytes; } - final int index = indexOf(fileBytes, RIVE_GLOW_BACKGROUND_KEYFRAME); - if (index < 0) { - Logger.printException(() -> "Could not find the background color of the glow animation"); + if (backgroundColor != RIVE_GLOW_ORIGINAL_BACKGROUND_COLOR) { + setRiveGlowBackgroundColor(fileBytes, backgroundColor); + } + + final float artboardWidth = riveGlowArtboardWidth; + final float artboardHeight = riveGlowArtboardHeight; + if (artboardWidth > 0 && artboardHeight > 0) { + fileBytes = setRiveGlowArtboardSize(fileBytes, artboardWidth, artboardHeight); + } + + final int timelineIndex = indexOf(fileBytes, RIVE_GLOW_TIMELINE); + if (timelineIndex < 0) { + Logger.printException(() -> "Could not find the timeline of the glow animation"); return fileBytes; } - // Replace the color after the property key. - ByteBuffer.wrap(fileBytes, index + 1, Integer.BYTES) - .order(ByteOrder.LITTLE_ENDIAN) - .putInt(backgroundColor); + return replaceBytes(fileBytes, timelineIndex + RIVE_GLOW_TIMELINE.length, 0, + RIVE_GLOW_FADE_IN_KEYFRAMES); } catch (Exception ex) { Logger.printException(() -> "getRiveFile failure", ex); } @@ -272,6 +467,52 @@ public static byte[] getRiveFile(@Nullable byte[] fileBytes, int resourceId) { return fileBytes; } + private static void setRiveGlowBackgroundColor(byte[] fileBytes, @ColorInt int color) { + final int index = indexOf(fileBytes, RIVE_GLOW_BACKGROUND_KEYFRAME); + if (index < 0) { + Logger.printException(() -> "Could not find the background color of the glow animation"); + return; + } + + // Replace the color after the property key. + ByteBuffer.wrap(fileBytes, index + 1, Integer.BYTES) + .order(ByteOrder.LITTLE_ENDIAN) + .putInt(color); + } + + private static byte[] setRiveGlowArtboardSize(byte[] fileBytes, float width, float height) { + final int index = indexOf(fileBytes, RIVE_GLOW_ARTBOARD_SIZE); + if (index < 0) { + Logger.printException(() -> "Could not find the artboard size of the glow animation"); + return fileBytes; + } + + // The content is drawn from the origin of the artboard, which is a fraction of its size. + // The origin keeps the content in the middle of the resized artboard. + final float originX = (width - RIVE_GLOW_ARTBOARD_WIDTH) / 2 / width; + final float originY = (height - RIVE_GLOW_ARTBOARD_HEIGHT) / 2 / height; + + // Each property is a one byte property key followed by a float. + byte[] artboardProperties = ByteBuffer.allocate(4 * (1 + Float.BYTES)) + .order(ByteOrder.LITTLE_ENDIAN) + .put(RIVE_PROPERTY_KEY_WIDTH).putFloat(width) + .put(RIVE_PROPERTY_KEY_HEIGHT).putFloat(height) + .put(RIVE_PROPERTY_KEY_ORIGIN_X).putFloat(originX) + .put(RIVE_PROPERTY_KEY_ORIGIN_Y).putFloat(originY) + .array(); + + return replaceBytes(fileBytes, index, RIVE_GLOW_ARTBOARD_SIZE.length, artboardProperties); + } + + private static byte[] replaceBytes(byte[] array, int index, int length, byte[] replacement) { + final int end = index + length; + return ByteBuffer.allocate(array.length - length + replacement.length) + .put(array, 0, index) + .put(replacement) + .put(array, end, array.length - end) + .array(); + } + private static int indexOf(byte[] array, byte[] target) { outer: for (int i = 0, last = array.length - target.length; i <= last; i++) { diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/Settings.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/Settings.java index f03e2beb6f..acf90660d3 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/Settings.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/Settings.java @@ -552,6 +552,7 @@ public class Settings extends SharedYouTubeSettings { public static final BooleanSetting TABLET_LAYOUT_IN_PLAYER = new BooleanSetting("morphe_tablet_layout_in_player", FALSE, true, new TabletLayoutInPlayerAvailability()); public static final BooleanSetting BYPASS_IMAGE_REGION_RESTRICTIONS = new BooleanSetting("morphe_bypass_image_region_restrictions", FALSE, true); public static final BooleanSetting GRADIENT_LOADING_SCREEN = new BooleanSetting("morphe_gradient_loading_screen", FALSE, true); + public static final BooleanSetting RIVE_SPLASH_SCREEN_SHOWING = new BooleanSetting("morphe_rive_splash_screen_showing", FALSE, false, false); public static final EnumSetting HEADER_LOGO = new EnumSetting<>("morphe_header_logo", HeaderLogo.DEFAULT, true); public static final BooleanSetting DISABLE_SIGN_IN_TO_TV_POPUP = new BooleanSetting("morphe_disable_sign_in_to_tv_popup", FALSE); public static final BooleanSetting DISABLE_CONNECT_YOUR_DEVICES_POPUP = new BooleanSetting("morphe_disable_connect_your_devices_popup", FALSE); diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/theme/Fingerprints.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/theme/Fingerprints.kt index 520576a26a..a677eed3d7 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/theme/Fingerprints.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/theme/Fingerprints.kt @@ -182,6 +182,16 @@ internal object RiveReadRawResourceFingerprint : Fingerprint( ) ) +/** + * Stores each frame the Rive renderer draws. + */ +internal object RiveCanvasSessionSetLatestBitmapFingerprint : Fingerprint( + definingClass = "Lapp/rive/RiveCanvasSession;", + name = "access\$setLatestBitmap\$p", + returnType = "V", + parameters = listOf("Lapp/rive/RiveCanvasSession;", "Landroid/graphics/Bitmap;") +) + internal object LottieLayerColorFingerprint : Fingerprint( parameters = listOf( "Landroid/graphics/Canvas;", diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/theme/SplashAnimationPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/theme/SplashAnimationPatch.kt index b6846183b3..d5f838297f 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/theme/SplashAnimationPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/theme/SplashAnimationPatch.kt @@ -187,6 +187,13 @@ val splashAnimationPatch = bytecodePatch { } } + // The system splash screen is kept until the glow animation has drawn, + // otherwise a blank screen is shown while the animation loads. + RiveCanvasSessionSetLatestBitmapFingerprint.method.addInstruction( + 0, + "invoke-static { }, $EXTENSION_CLASS->onRiveFrameAvailable()V" + ) + // The Rive animation is loaded in the background, and a home feed that is // shown quickly closes the animation before it has drawn anything. The animation // is instead closed when it ends, or by the timeout of the app if it never does. diff --git a/patches/src/main/resources/addresources/values/youtube/strings.xml b/patches/src/main/resources/addresources/values/youtube/strings.xml index ac22ab20ff..34c88d0a35 100644 --- a/patches/src/main/resources/addresources/values/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values/youtube/strings.xml @@ -1365,6 +1365,7 @@ Limitation: If \'Restore old video action bar\' is turned off, \'More videos\' o Glow + Glow splash screen is not supported by this device Light theme background White Material You (White) From 1fc1475541f8db4ed44aa480d1990e3dff87d07f Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Wed, 28 Oct 2026 08:24:43 +0000 Subject: [PATCH 125/189] chore: Release v1.49.0-dev.10 [skip ci] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## [1.49.0-dev.10](https://github.com/MorpheApp/morphe-patches/compare/v1.49.0-dev.9...v1.49.0-dev.10) (2026-10-28) ### 🐛 Bug Fixes * **YouTube - Ask flyout menu:** Ask chat window can be hidden by onscreen keyboard ([7532430](https://github.com/MorpheApp/morphe-patches/commit/7532430e096f5730ba9321b32d8b4bd2c54c77f6)) * **YouTube - Hide status bar:** Top bars are covered by a second status bar when the app is kept out of the camera cutout ([#4112](https://github.com/MorpheApp/morphe-patches/issues/4112)) ([9d3b53c](https://github.com/MorpheApp/morphe-patches/commit/9d3b53c92cb877f309b4463b7d5067aaa1a996a3)) * **YouTube - Spoof video streams:** Patch can be included for root installs ([3edf03b](https://github.com/MorpheApp/morphe-patches/commit/3edf03b8efc2f5ea0458456002938c7dbd4bf8d8)) * **YouTube - Theme:** Glow startup animation does not extend to fullscreen ([7f1a6b3](https://github.com/MorpheApp/morphe-patches/commit/7f1a6b35c8a3989cd2b06a1e81d817015f2ca433)) * **YouTube - Video quality:** Selected quality is not applied to the already buffered video ([#4117](https://github.com/MorpheApp/morphe-patches/issues/4117)) ([7faa488](https://github.com/MorpheApp/morphe-patches/commit/7faa48834b1f78f8ca572c773392ed9513de8ea3)) * **YouTube:** Restore TalkBack access to Shorts comments and descriptions ([#4128](https://github.com/MorpheApp/morphe-patches/issues/4128)) ([9542d24](https://github.com/MorpheApp/morphe-patches/commit/9542d24adf3b5ec2c25fce931418160dadbde93c)) --- CHANGELOG.md | 11 +++++++++++ README.md | 2 +- gradle.properties | 2 +- patches-bundle.json | 8 ++++---- patches-list.json | 2 +- 5 files changed, 18 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d8fb576d0e..3561b1910a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +## [1.49.0-dev.10](https://github.com/MorpheApp/morphe-patches/compare/v1.49.0-dev.9...v1.49.0-dev.10) (2026-10-28) + +### 🐛 Bug Fixes + +* **YouTube - Ask flyout menu:** Ask chat window can be hidden by onscreen keyboard ([7532430](https://github.com/MorpheApp/morphe-patches/commit/7532430e096f5730ba9321b32d8b4bd2c54c77f6)) +* **YouTube - Hide status bar:** Top bars are covered by a second status bar when the app is kept out of the camera cutout ([#4112](https://github.com/MorpheApp/morphe-patches/issues/4112)) ([9d3b53c](https://github.com/MorpheApp/morphe-patches/commit/9d3b53c92cb877f309b4463b7d5067aaa1a996a3)) +* **YouTube - Spoof video streams:** Patch can be included for root installs ([3edf03b](https://github.com/MorpheApp/morphe-patches/commit/3edf03b8efc2f5ea0458456002938c7dbd4bf8d8)) +* **YouTube - Theme:** Glow startup animation does not extend to fullscreen ([7f1a6b3](https://github.com/MorpheApp/morphe-patches/commit/7f1a6b35c8a3989cd2b06a1e81d817015f2ca433)) +* **YouTube - Video quality:** Selected quality is not applied to the already buffered video ([#4117](https://github.com/MorpheApp/morphe-patches/issues/4117)) ([7faa488](https://github.com/MorpheApp/morphe-patches/commit/7faa48834b1f78f8ca572c773392ed9513de8ea3)) +* **YouTube:** Restore TalkBack access to Shorts comments and descriptions ([#4128](https://github.com/MorpheApp/morphe-patches/issues/4128)) ([9542d24](https://github.com/MorpheApp/morphe-patches/commit/9542d24adf3b5ec2c25fce931418160dadbde93c)) + ## [1.49.0-dev.9](https://github.com/MorpheApp/morphe-patches/compare/v1.49.0-dev.8...v1.49.0-dev.9) (2026-10-27) ### 🐛 Bug Fixes diff --git a/README.md b/README.md index 5c7d9861aa..bf1cc0c853 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ All modifications made by Morphe, along with their dates, can be found in the Gi ## 🩹 Patches list -> **[v1.49.0-dev.9](https://github.com/MorpheApp/morphe-patches/releases/tag/v1.49.0-dev.9)**  •  `dev`  •  193 patches total +> **[v1.49.0-dev.10](https://github.com/MorpheApp/morphe-patches/releases/tag/v1.49.0-dev.10)**  •  `dev`  •  193 patches total

    📦 YouTube  •  108 patches
    diff --git a/gradle.properties b/gradle.properties index 08c5cccde1..62a696316f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,4 +5,4 @@ android.useAndroidX = true # Extensions are Java only. Prevents AGP from adding Kotlin compilation and the Kotlin stdlib. android.builtInKotlin = false kotlin.code.style = official -version = 1.49.0-dev.9 +version = 1.49.0-dev.10 diff --git a/patches-bundle.json b/patches-bundle.json index 26a0c5a34b..0a3300ecaa 100644 --- a/patches-bundle.json +++ b/patches-bundle.json @@ -1,8 +1,8 @@ { - "created_at": "2026-10-27T19:49:48", - "description": "## [1.49.0-dev.9](https://github.com/MorpheApp/morphe-patches/compare/v1.49.0-dev.8...v1.49.0-dev.9) (2026-10-27)\n\n### 🐛 Bug Fixes\n\n* **YouTube - Downloads:** Downloaded videos that work at first may not work later ([90d011d](https://github.com/MorpheApp/morphe-patches/commit/90d011dc007cf7e433196fd27f392c87d32363f6))\n* **YouTube - Feed video actions:** Watch later button is missing after scrolling up ([51dfd75](https://github.com/MorpheApp/morphe-patches/commit/51dfd7565426be389f61dc5f34196cc762a103fe))\n* **YouTube - Feed video actions:** Watch later button opens the channel in right to left languages ([0e058e8](https://github.com/MorpheApp/morphe-patches/commit/0e058e8d184650326d81a243d083ae9bd6f3f942))\n* **YouTube - Feed view count:** View count badge covers the duration in right to left languages ([b012535](https://github.com/MorpheApp/morphe-patches/commit/b01253535eb65761f896042d46ba1fed002232c2))\n* **YouTube:** Channel page community posts do not load when feed \"Hide community posts\" is enabled ([#4110](https://github.com/MorpheApp/morphe-patches/issues/4110)) ([b54aaec](https://github.com/MorpheApp/morphe-patches/commit/b54aaecaa26e8e6e4491b0c54193cd2b2b6e0f2c))\n* **YouTube:** Feed thumbnail badges are missing after scrolling up, show a loading spinner until they are loaded ([03ef708](https://github.com/MorpheApp/morphe-patches/commit/03ef708f724c9af7f784b75ead9da58b8cede5f4))\n* **YouTube:** Remove legacy litho buffer filtering code required for app versions less than 20.22, change legacy support to `20.26.46` ([0ce43f7](https://github.com/MorpheApp/morphe-patches/commit/0ce43f7b8bf0119f295f269b939186ff264b5d48))\n* **YouTube:** Watch page feed lags when scrolling ([35e4123](https://github.com/MorpheApp/morphe-patches/commit/35e412375a1de62b500e2c9ca70c0ca5a18fb793))\n\n### ✨ New Features\n\n* Validate custom header and icon folders when the option is set ([fbb095d](https://github.com/MorpheApp/morphe-patches/commit/fbb095dbd6b9bf7410e4cff38b664d2fb3048aeb))\n* **YouTube - Custom branding:** Add experimental YouTube launcher icons ([#4114](https://github.com/MorpheApp/morphe-patches/issues/4114)) ([de9e6e4](https://github.com/MorpheApp/morphe-patches/commit/de9e6e4e7953f6192991e35025ed644806948ecd))\n* **YouTube - Theme:** Add experimental YT splash screen animation ([#4109](https://github.com/MorpheApp/morphe-patches/issues/4109)) ([1f24e84](https://github.com/MorpheApp/morphe-patches/commit/1f24e8433b26aed7a0e8cd21bf6c719526e7ecc4))\n* **YouTube:** Hidden a new type of community post ([fb51b72](https://github.com/MorpheApp/morphe-patches/commit/fb51b722e05df50e98302f87996525b349d0474b))\n* **YouTube:** Redirect Watch later saves to a chosen playlist ([#4016](https://github.com/MorpheApp/morphe-patches/issues/4016)) ([04e791d](https://github.com/MorpheApp/morphe-patches/commit/04e791d3d1c9fa8c8c352c61b18a6348311a8a27))\n\n### 🚀 Updated App Support\n\n* **YouTube Music:** Add experimental support for `9.43.51` ([141aa8f](https://github.com/MorpheApp/morphe-patches/commit/141aa8fedea935664df73154cdc1823e893c5b88))", - "download_url": "https://github.com/MorpheApp/morphe-patches/releases/download/v1.49.0-dev.9/patches-1.49.0-dev.9.mpp", + "created_at": "2026-10-28T08:22:03", + "description": "## [1.49.0-dev.10](https://github.com/MorpheApp/morphe-patches/compare/v1.49.0-dev.9...v1.49.0-dev.10) (2026-10-28)\n\n### 🐛 Bug Fixes\n\n* **YouTube - Ask flyout menu:** Ask chat window can be hidden by onscreen keyboard ([7532430](https://github.com/MorpheApp/morphe-patches/commit/7532430e096f5730ba9321b32d8b4bd2c54c77f6))\n* **YouTube - Hide status bar:** Top bars are covered by a second status bar when the app is kept out of the camera cutout ([#4112](https://github.com/MorpheApp/morphe-patches/issues/4112)) ([9d3b53c](https://github.com/MorpheApp/morphe-patches/commit/9d3b53c92cb877f309b4463b7d5067aaa1a996a3))\n* **YouTube - Spoof video streams:** Patch can be included for root installs ([3edf03b](https://github.com/MorpheApp/morphe-patches/commit/3edf03b8efc2f5ea0458456002938c7dbd4bf8d8))\n* **YouTube - Theme:** Glow startup animation does not extend to fullscreen ([7f1a6b3](https://github.com/MorpheApp/morphe-patches/commit/7f1a6b35c8a3989cd2b06a1e81d817015f2ca433))\n* **YouTube - Video quality:** Selected quality is not applied to the already buffered video ([#4117](https://github.com/MorpheApp/morphe-patches/issues/4117)) ([7faa488](https://github.com/MorpheApp/morphe-patches/commit/7faa48834b1f78f8ca572c773392ed9513de8ea3))\n* **YouTube:** Restore TalkBack access to Shorts comments and descriptions ([#4128](https://github.com/MorpheApp/morphe-patches/issues/4128)) ([9542d24](https://github.com/MorpheApp/morphe-patches/commit/9542d24adf3b5ec2c25fce931418160dadbde93c))", + "download_url": "https://github.com/MorpheApp/morphe-patches/releases/download/v1.49.0-dev.10/patches-1.49.0-dev.10.mpp", "signature_download_url": "", - "version": "1.49.0-dev.9", + "version": "1.49.0-dev.10", "public_key": "g8Ys9leTu2yeixojeR35HrNkK6eOSLpA6nNIizRutqI=" } diff --git a/patches-list.json b/patches-list.json index 50f4994287..e60c5550d5 100644 --- a/patches-list.json +++ b/patches-list.json @@ -1,6 +1,6 @@ { "NOTE": "Do NOT manually edit this file. This file is automatically updated when semantic release (release.yml) runs. Manually editing this file can break your releases and break third party tools that use this file.", - "version": "1.49.0-dev.9", + "version": "1.49.0-dev.10", "patches": [ { "name": "AI channel filter", From 16012c9ef4e4aa0631917d910ba66ebb23dc3d27 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Wed, 28 Oct 2026 08:45:44 +0000 Subject: [PATCH 126/189] chore(YouTube - Theme): Glow startup animation can loop if app is sent to background immediately after launch --- .../youtube/patches/theme/ThemePatch.java | 95 +++++++++++++++---- .../extension/youtube/settings/Settings.java | 2 +- .../youtube/layout/theme/Fingerprints.kt | 6 ++ .../layout/theme/SplashAnimationPatch.kt | 33 ++++--- 4 files changed, 101 insertions(+), 35 deletions(-) diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/theme/ThemePatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/theme/ThemePatch.java index e49913d228..4137d53164 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/theme/ThemePatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/theme/ThemePatch.java @@ -29,7 +29,6 @@ import android.widget.Spinner; import androidx.annotation.ColorInt; -import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; @@ -190,6 +189,22 @@ public static boolean useLotteLaunchSplashScreen(boolean original) { @Nullable private static volatile Runnable riveSplashScreenRemover; + /** + * How many frames the glow animation draws before the device is known to support it. + */ + private static final int RIVE_SPLASH_SCREEN_SUPPORTED_FRAMES = 30; + + /** + * Frames the glow animation still has to draw before the device is known to support it, + * or 0 if the animation is not starting. + */ + private static volatile int riveSplashScreenFramesUntilSupported; + + /** + * If the activity has not started since the glow animation was set up. + */ + private static volatile boolean riveSplashScreenFirstStart; + private static boolean riveSplashScreenCrashChecked; private static boolean isRiveSplashScreenEnabled() { @@ -198,7 +213,7 @@ private static boolean isRiveSplashScreenEnabled() { /** * Some devices crash when drawing the glow animation, because the Rive renderer - * uses GPU features they do not have. If the app closed while the animation was shown, + * uses GPU features they do not have. If the app closed before the animation could draw, * the animation is turned off. Checked once when the app starts, before this launch * shows the animation. */ @@ -208,9 +223,9 @@ private static synchronized void checkRiveSplashScreenCrash() { } riveSplashScreenCrashChecked = true; - if (Settings.RIVE_SPLASH_SCREEN_SHOWING.get()) { - Logger.printInfo(() -> "App closed while the glow splash screen was shown, resetting splash screen style"); - Settings.RIVE_SPLASH_SCREEN_SHOWING.save(false); + if (Settings.RIVE_SPLASH_SCREEN_STARTING.get()) { + Logger.printInfo(() -> "App closed before the glow splash screen could draw, resetting splash screen style"); + Settings.RIVE_SPLASH_SCREEN_STARTING.save(false); SharedYouTubeSettings.SPLASH_SCREEN_STYLE.resetToDefault(); Utils.showToastLong(str("morphe_splash_screen_style_glow_not_supported")); } @@ -333,19 +348,10 @@ public static void setRiveSplashScreenBackground(View animationView) { keepSplashScreenUntilRiveFrame(activity); } - // The app removes the view when the animation is closed. - Settings.RIVE_SPLASH_SCREEN_SHOWING.save(true); - animationView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() { - @Override - public void onViewAttachedToWindow(@NonNull View view) { - } - - @Override - public void onViewDetachedFromWindow(@NonNull View view) { - view.removeOnAttachStateChangeListener(this); - Settings.RIVE_SPLASH_SCREEN_SHOWING.save(false); - } - }); + // Cleared when the animation has drawn, or when the app is stopped. + Settings.RIVE_SPLASH_SCREEN_STARTING.save(true); + riveSplashScreenFramesUntilSupported = RIVE_SPLASH_SCREEN_SUPPORTED_FRAMES; + riveSplashScreenFirstStart = true; } catch (Exception ex) { Logger.printException(() -> "setRiveSplashScreenBackground failure", ex); } @@ -384,9 +390,58 @@ private static void removeRiveSplashScreen() { * Called for each frame the Rive renderer draws. */ public static void onRiveFrameAvailable() { - if (riveSplashScreenRemover != null) { - Utils.runOnMainThreadNowOrLater(ThemePatch::removeRiveSplashScreen); + try { + if (riveSplashScreenRemover != null) { + Utils.runOnMainThreadNowOrLater(ThemePatch::removeRiveSplashScreen); + } + + final int framesUntilSupported = riveSplashScreenFramesUntilSupported; + if (framesUntilSupported == 1) { + clearRiveSplashScreenStarting(); + } else if (framesUntilSupported > 1) { + riveSplashScreenFramesUntilSupported = framesUntilSupported - 1; + } + } catch (Exception ex) { + Logger.printException(() -> "onRiveFrameAvailable failure", ex); + } + } + + /** + * Injection point. + *

    + * The glow animation is not drawn while the app is stopped, so it cannot crash the app. + */ + public static void onSplashScreenStop() { + try { + if (riveSplashScreenFramesUntilSupported > 0) { + clearRiveSplashScreenStarting(); + } + } catch (Exception ex) { + Logger.printException(() -> "onSplashScreenStop failure", ex); + } + } + + private static void clearRiveSplashScreenStarting() { + riveSplashScreenFramesUntilSupported = 0; + Settings.RIVE_SPLASH_SCREEN_STARTING.save(false); + } + + /** + * Injection point. + *

    + * A home feed that is shown quickly closes the glow animation before it has drawn + * anything, so on the first start the timeout of the app closes it instead. + * Stopping the app cancels the timeout, and then the home feed closes it as usual. + * + * @param riveAnimationShown If the app shows a Rive animation. + * @return If the home feed does not close the animation. + */ + public static boolean skipSplashScreenCloseOnHomeFeed(boolean riveAnimationShown) { + if (riveAnimationShown && riveSplashScreenFirstStart) { + riveSplashScreenFirstStart = false; + return true; } + return false; } private static Rect getWindowBounds(Activity activity) { diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/Settings.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/Settings.java index acf90660d3..c58722e8a6 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/Settings.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/Settings.java @@ -552,7 +552,7 @@ public class Settings extends SharedYouTubeSettings { public static final BooleanSetting TABLET_LAYOUT_IN_PLAYER = new BooleanSetting("morphe_tablet_layout_in_player", FALSE, true, new TabletLayoutInPlayerAvailability()); public static final BooleanSetting BYPASS_IMAGE_REGION_RESTRICTIONS = new BooleanSetting("morphe_bypass_image_region_restrictions", FALSE, true); public static final BooleanSetting GRADIENT_LOADING_SCREEN = new BooleanSetting("morphe_gradient_loading_screen", FALSE, true); - public static final BooleanSetting RIVE_SPLASH_SCREEN_SHOWING = new BooleanSetting("morphe_rive_splash_screen_showing", FALSE, false, false); + public static final BooleanSetting RIVE_SPLASH_SCREEN_STARTING = new BooleanSetting("morphe_rive_splash_screen_starting", FALSE, false, false); public static final EnumSetting HEADER_LOGO = new EnumSetting<>("morphe_header_logo", HeaderLogo.DEFAULT, true); public static final BooleanSetting DISABLE_SIGN_IN_TO_TV_POPUP = new BooleanSetting("morphe_disable_sign_in_to_tv_popup", FALSE); public static final BooleanSetting DISABLE_CONNECT_YOUR_DEVICES_POPUP = new BooleanSetting("morphe_disable_connect_your_devices_popup", FALSE); diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/theme/Fingerprints.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/theme/Fingerprints.kt index a677eed3d7..d7513c6bb8 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/theme/Fingerprints.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/theme/Fingerprints.kt @@ -148,6 +148,12 @@ internal object SplashScreenOnStartFingerprint : Fingerprint( ) ) +internal object SplashScreenOnStopFingerprint : Fingerprint( + classFingerprint = RiveSplashScreenViewFingerprint, + name = "onStop", + returnType = "V" +) + /** * Creates the source of a Rive file that is a raw resource, * with the key the app keeps the file in memory with. diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/theme/SplashAnimationPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/theme/SplashAnimationPatch.kt index d5f838297f..207e74445f 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/theme/SplashAnimationPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/theme/SplashAnimationPatch.kt @@ -17,7 +17,6 @@ import app.morphe.patcher.literal import app.morphe.patcher.methodCall import app.morphe.patcher.patch.bytecodePatch import app.morphe.patcher.util.getReference -import app.morphe.patcher.util.smali.ExternalLabel import app.morphe.patches.shared.layout.branding.startupAnimationResourcePatch import app.morphe.patches.shared.misc.lottie.LOTTIE_ANIMATION_VIEW_CLASS_TYPE import app.morphe.patches.shared.misc.lottie.LottieAnimationViewSetAnimationIntFingerprint @@ -194,23 +193,29 @@ val splashAnimationPatch = bytecodePatch { "invoke-static { }, $EXTENSION_CLASS->onRiveFrameAvailable()V" ) + SplashScreenOnStopFingerprint.method.addInstruction( + 0, + "invoke-static { }, $EXTENSION_CLASS->onSplashScreenStop()V" + ) + // The Rive animation is loaded in the background, and a home feed that is - // shown quickly closes the animation before it has drawn anything. The animation - // is instead closed when it ends, or by the timeout of the app if it never does. + // shown quickly closes the animation before it has drawn anything. On the first + // start the animation is instead closed by the timeout of the app. val riveAnimationShownField = RiveSplashScreenViewFingerprint.instructionMatches[1] .instruction.getReference()!! - SplashScreenOnStartFingerprint.method.apply { - addInstructionsWithLabels( - 0, - """ - iget-boolean v0, p0, $riveAnimationShownField - if-eqz v0, :close_on_home_feed - return-void - """, - ExternalLabel("close_on_home_feed", getInstruction(0)) - ) - } + SplashScreenOnStartFingerprint.method.addInstructionsWithLabels( + 0, + """ + iget-boolean v0, p0, $riveAnimationShownField + invoke-static { v0 }, $EXTENSION_CLASS->skipSplashScreenCloseOnHomeFeed(Z)Z + move-result v0 + if-eqz v0, :close_on_home_feed + return-void + :close_on_home_feed + nop + """ + ) } YouTubeActivityOnCreateFingerprint.method.apply { From c000b53eeae02941f82a37e1c66c12a06c3d1059 Mon Sep 17 00:00:00 2001 From: 0xrxL <127248639+0xrxL@users.noreply.github.com> Date: Wed, 28 Oct 2026 10:18:38 +0100 Subject: [PATCH 127/189] fix(YouTube - Video quality): Video restarts or plays faster after selecting a quality --- .../youtube/patches/VideoFormat.java | 1 + .../youtube/patches/VideoInformation.java | 5 + .../quality/RememberVideoQualityPatch.java | 182 +++++++++++++++++- .../videoplayer/VideoQualityDialogButton.java | 7 +- .../youtube/video/format/VideoFormatPatch.kt | 3 +- .../information/VideoInformationPatch.kt | 26 +++ .../youtube/video/quality/Fingerprints.kt | 35 ++++ .../quality/RememberVideoQualityPatch.kt | 92 ++++++++- 8 files changed, 331 insertions(+), 20 deletions(-) diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/VideoFormat.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/VideoFormat.java index 880264d2e4..79dfce505d 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/VideoFormat.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/VideoFormat.java @@ -25,6 +25,7 @@ public interface FormatInterface { String patch_getMimeType(); int patch_getWidth(); int patch_getHeight(); + int patch_getItag(); } /** diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/VideoInformation.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/VideoInformation.java index ee3ea36759..22d2be5af6 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/VideoInformation.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/VideoInformation.java @@ -17,6 +17,7 @@ import java.lang.ref.WeakReference; import java.util.Arrays; +import java.util.Collection; import java.util.Locale; import java.util.Objects; import java.util.regex.Matcher; @@ -71,6 +72,10 @@ public interface VideoQualityInterface { // Methods are added during patching. String patch_getQualityName(); int patch_getResolution(); + /** + * @return The IDs (itags) of the formats of the quality, empty if the quality has no formats. + */ + Collection patch_getFormatIds(); } /** diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/playback/quality/RememberVideoQualityPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/playback/quality/RememberVideoQualityPatch.java index dfd0338fe1..b09574b858 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/playback/quality/RememberVideoQualityPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/playback/quality/RememberVideoQualityPatch.java @@ -12,16 +12,24 @@ import static app.morphe.extension.shared.StringRef.str; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.google.protobuf.MessageLite; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import app.morphe.extension.shared.Logger; import app.morphe.extension.shared.Utils; import app.morphe.extension.shared.settings.BooleanSetting; import app.morphe.extension.shared.settings.IntegerSetting; +import app.morphe.extension.youtube.patches.VideoFormat.FormatInterface; import app.morphe.extension.youtube.patches.VideoInformation; import app.morphe.extension.youtube.innertube.ClientAbrStateOuterClass.ClientAbrState; import app.morphe.extension.youtube.patches.VideoInformation.*; @@ -60,17 +68,143 @@ public class RememberVideoQualityPatch { */ private static volatile String clearVideoBufferVideoId = ""; + /** + * Playback time in microseconds when the user selected the quality, after which + * the buffered video is cleared, or -1. + */ + private static volatile long clearVideoBufferTimeUs = -1; + /** * The quality selected in the Shorts quality menu, until the menu sets it. */ @Nullable private static volatile VideoQualityInterface shortsMenuSelectedQuality; + /** + * Number of videos whose video formats are kept. The next Shorts are loaded before they play. + */ + private static final int MAX_VIDEO_FORMAT_CODECS = 20; + + /** + * The codec of each video format by format ID (itag), by video ID. + */ + private static final Map> videoFormatCodecs = Collections.synchronizedMap( + new LinkedHashMap<>(MAX_VIDEO_FORMAT_CODECS, 0.75f, true) { + @Override + protected boolean removeEldestEntry(Map.Entry> eldest) { + return size() > MAX_VIDEO_FORMAT_CODECS; + } + }); + + /** + * Format ID (itag) of the video format playing. + */ + private static volatile int currentVideoFormatId; + private static void setUserSelectedQuality(int resolution, boolean isShorts) { userSelectedQualityResolution = resolution; userSelectedQualityIsShorts = isShorts; userSelectedQualityVideoId = VideoInformation.getVideoId(); clearVideoBufferVideoId = userSelectedQualityVideoId; + // The playback time is read on the main thread, + // as the player may set the video formats on another thread. + final long videoTime = VideoInformation.getVideoTime(); + clearVideoBufferTimeUs = videoTime >= 0 ? videoTime * 1000 : -1; + } + + /** + * The quality menu can have qualities the player cannot play, such as a 144p quality + * that has only AVC formats while a VP9 format plays. The player then has no video to play + * and reloads the video. A quality without formats is found by the player by its resolution. + * + * @return If the quality has a format with the codec of the video format playing, + * or if it is not known. + */ + private static boolean isQualityPlayable(VideoQualityInterface quality) { + final Map codecs = videoFormatCodecs.get(VideoInformation.getVideoId()); + if (codecs == null) { + return true; + } + final String currentCodec = codecs.get(currentVideoFormatId); + final Collection formatIds = quality.patch_getFormatIds(); + if (currentCodec == null || formatIds == null || formatIds.isEmpty()) { + return true; + } + for (Object formatId : formatIds) { + if (currentCodec.equals(codecs.get(formatId))) { + return true; + } + } + return false; + } + + /** + * @return The quality selected, or if the player cannot play it, the closest quality + * the player can play between the quality selected and the quality playing. + */ + public static VideoQualityInterface getPlayableQuality(VideoQualityInterface selectedQuality) { + try { + final VideoQualityInterface[] qualities = VideoInformation.getCurrentQualities(); + final VideoQualityInterface currentQuality = VideoInformation.getCurrentQuality(); + final int selectedResolution = selectedQuality.patch_getResolution(); + if (qualities == null || currentQuality == null || selectedResolution <= 0 + || isQualityPlayable(selectedQuality)) { + return selectedQuality; + } + + final boolean selectedHigherQuality = selectedResolution > currentQuality.patch_getResolution(); + VideoQualityInterface playableQuality = selectedQuality; + // The qualities are ordered from largest to smallest. + for (VideoQualityInterface quality : qualities) { + final int resolution = quality.patch_getResolution(); + if (resolution <= 0 || !isQualityPlayable(quality)) { + continue; + } + if (selectedHigherQuality + ? playableQuality == selectedQuality && resolution < selectedResolution + : resolution > selectedResolution) { + playableQuality = quality; + } + } + + final VideoQualityInterface replacement = playableQuality; + Logger.printDebug(() -> "Selected quality " + selectedQuality + + " cannot be played, using: " + replacement); + return playableQuality; + } catch (Exception ex) { + Logger.printException(() -> "getPlayableQuality failure", ex); + return selectedQuality; + } + } + + /** + * Injection point. + */ + public static List setVideoFormats(@NonNull String videoId, @NonNull List adaptiveFormats) { + try { + final Map codecs = new HashMap<>(); + for (FormatInterface format : adaptiveFormats) { + final String mimeType = format.patch_getMimeType(); + if (mimeType != null && mimeType.startsWith("video")) { + // The codec without its profile, such as 'video/mp4; codecs="avc1' for 'video/mp4; codecs="avc1.4d401e"'. + final int profileIndex = mimeType.indexOf('.'); + codecs.put(format.patch_getItag(), profileIndex > 0 ? mimeType.substring(0, profileIndex) : mimeType); + } + } + videoFormatCodecs.put(videoId, codecs); + } catch (Exception ex) { + Logger.printException(() -> "setVideoFormats failure", ex); + } + return adaptiveFormats; + } + + /** + * Injection point. + * + * @param formatId Format ID (itag) of the video format playing. + */ + public static void setCurrentVideoFormat(int formatId) { + currentVideoFormatId = formatId; } /** @@ -141,6 +275,7 @@ public static boolean onVideoIntentLoaded(Map playbackStartDescr // the video starts, so the quality selected for the current video // would otherwise be used when the other video starts. userSelectedQualityVideoId = ""; + clearVideoBufferTimeUs = -1; } return false; } @@ -298,8 +433,9 @@ public static byte[] overrideClientAbrState(@Nullable MessageLite clientAbrState /** * Injection point. * @param userSelectedQualityIndex Element index of {@link VideoInformation#getCurrentQualities()}. + * @return The element index of the quality to set (see {@link #getPlayableQuality(VideoQualityInterface)}). */ - public static void userChangedShortsQuality(int userSelectedQualityIndex) { + public static int userChangedShortsQuality(int userSelectedQualityIndex) { try { VideoQualityInterface[] currentQualities = VideoInformation.getCurrentQualities(); if (currentQualities == null) { @@ -307,9 +443,16 @@ public static void userChangedShortsQuality(int userSelectedQualityIndex) { // The resolution is not known, so a reload uses the quality saved by YouTube. setUserSelectedQuality(VideoInformation.AUTOMATIC_VIDEO_QUALITY_VALUE, true); clearShortsMenuSelectedQuality(); - return; + return userSelectedQualityIndex; + } + final VideoQualityInterface selectedQuality = currentQualities[userSelectedQualityIndex]; + VideoQualityInterface quality = getPlayableQuality(selectedQuality); + final int qualityIndex = quality == selectedQuality + ? userSelectedQualityIndex + : Arrays.asList(currentQualities).indexOf(quality); + if (qualityIndex < 0) { + quality = selectedQuality; } - VideoQualityInterface quality = currentQualities[userSelectedQualityIndex]; final int resolution = quality.patch_getResolution(); setUserSelectedQuality(resolution, true); if (resolution == VideoInformation.AUTOMATIC_VIDEO_QUALITY_VALUE) { @@ -322,8 +465,10 @@ public static void userChangedShortsQuality(int userSelectedQualityIndex) { if (shouldRememberVideoQuality()) { saveDefaultQuality(resolution); } + return qualityIndex < 0 ? userSelectedQualityIndex : qualityIndex; } catch (Exception ex) { Logger.printException(() -> "userChangedShortsQuality failure", ex); + return userSelectedQualityIndex; } } @@ -367,10 +512,13 @@ public static void clearShortsMenuSelectedQuality() { /** * Injection point. Regular videos. - * @param quality Quality selected by the user. + * @param selectedQuality Quality selected by the user. + * @return The quality to set (see {@link #getPlayableQuality(VideoQualityInterface)}). */ - public static void userChangedQuality(VideoQualityInterface quality) { + public static VideoQualityInterface userChangedQuality(VideoQualityInterface selectedQuality) { + final VideoQualityInterface quality = getPlayableQuality(selectedQuality); userChangedQuality(quality.patch_getResolution()); + return quality; } /** @@ -457,6 +605,28 @@ public static boolean clearVideoBufferOnQualityChange(boolean original) { clearVideoBufferVideoId = ""; // Not cleared if another video started before the player set the selected quality. - return original || videoId.equals(VideoInformation.getVideoId()); + if (original || clearVideoBufferTimeUs < 0 || !videoId.equals(VideoInformation.getVideoId())) { + clearVideoBufferTimeUs = -1; + return original; + } + return true; + } + + /** + * Injection point. + *

    + * The player clears the whole buffered video and buffers it again from the time the video + * started or was last seeked to, such as the start of a Short. The video then plays + * from that time and is played faster until it reaches the audio. + * The buffered video is cleared only after the segment that is playing, as YouTube does + * when the audio track changes, so the new quality is played after it without a gap. + * + * @return The playback time in microseconds after which the buffered video is cleared, + * or -1 to clear the whole buffered video. + */ + public static long getClearVideoBufferTime() { + final long timeUs = clearVideoBufferTimeUs; + clearVideoBufferTimeUs = -1; + return timeUs; } } diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/videoplayer/VideoQualityDialogButton.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/videoplayer/VideoQualityDialogButton.java index 123ac628dd..5cc3940d4d 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/videoplayer/VideoQualityDialogButton.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/videoplayer/VideoQualityDialogButton.java @@ -143,7 +143,7 @@ private static View.OnLongClickListener getOnLongClickListener() { if (resolution != VideoInformation.AUTOMATIC_VIDEO_QUALITY_VALUE && resolution <= defaultResolution) { Logger.printDebug(() -> "Resetting quality to: " + quality); - VideoInformation.changeQuality(quality); + VideoInformation.changeQuality(RememberVideoQualityPatch.getPlayableQuality(quality)); return true; } } @@ -307,9 +307,8 @@ private static void showVideoQualityDialog(Context context) { listView.setOnItemClickListener((parent, view, which, id) -> { try { final int originalIndex = which + 1; // Adjust for automatic. - VideoQualityInterface selectedQuality = currentQualities[originalIndex]; - RememberVideoQualityPatch.userChangedQuality(selectedQuality.patch_getResolution()); - VideoInformation.changeQuality(selectedQuality); + VideoInformation.changeQuality( + RememberVideoQualityPatch.userChangedQuality(currentQualities[originalIndex])); dialog.dismiss(); } catch (Exception ex) { diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/video/format/VideoFormatPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/video/format/VideoFormatPatch.kt index a525ea6773..b836abb6aa 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/video/format/VideoFormatPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/video/format/VideoFormatPatch.kt @@ -52,7 +52,8 @@ val videoFormatPatch = bytecodePatch( mapOf( "width" to "patch_getWidth", "height" to "patch_getHeight", - "mimeType" to "patch_getMimeType" + "mimeType" to "patch_getMimeType", + "itag" to "patch_getItag" ).forEach { (protoFieldName, patchInterfaceMethodName) -> val targetMethod = FormatStreamModelToStringFingerprint.originalMethod.findMethodFromToString(protoFieldName) diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/video/information/VideoInformationPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/video/information/VideoInformationPatch.kt index 29e8f2da9d..8f88b1caa9 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/video/information/VideoInformationPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/video/information/VideoInformationPatch.kt @@ -384,6 +384,11 @@ val videoInformationPatch = bytecodePatch( field.type == "I" } + // The IDs of the formats of the quality, the last constructor parameter. + val formatIdsField = fields.single { field -> + field.type == it.method.parameterTypes.last() + } + // Add interface and helper methods to allow extension code to call obfuscated methods. interfaces.add(EXTENSION_VIDEO_QUALITY_INTERFACE) methods.add( @@ -430,6 +435,27 @@ val videoInformationPatch = bytecodePatch( ) } ) + + methods.add( + ImmutableMethod( + type, + "patch_getFormatIds", + listOf(), + "Ljava/util/Collection;", + AccessFlags.PUBLIC.value or AccessFlags.FINAL.value, + null, + null, + MutableMethodImplementation(2), + ).toMutable().apply { + addInstructions( + 0, + """ + iget-object v0, p0, $formatIdsField + return-object v0 + """ + ) + } + ) } } diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/video/quality/Fingerprints.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/video/quality/Fingerprints.kt index eec94e64d3..eb04c523a5 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/video/quality/Fingerprints.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/video/quality/Fingerprints.kt @@ -209,6 +209,41 @@ internal object AsyncMediaFetchControllerSetVideoFormatsFingerprint : Fingerprin ) ) +internal const val BUFFER_MANAGER_CLASS = + "Lcom/google/android/libraries/youtube/media/player/exo2/platypus/mediapush/BufferManagerImpl;" + +/** + * The method of the player that clears the buffered media of a track, and buffers it again + * from the time the video started or was last seeked to. + */ +internal object PlaybackControllerClearBufferFingerprint : Fingerprint( + definingClass = "Lcom/google/android/libraries/youtube/media/player/exo2/platypus/PlaybackControllerWrapper;", + returnType = "V", + filters = listOf( + // Stops the media requests. + methodCall(definingClass = "this", returnType = "V", parameters = listOf()), + fieldAccess( + opcode = Opcode.IGET_OBJECT, + definingClass = "this", + type = BUFFER_MANAGER_CLASS + ), + string("setStartTimeUs with disposed BufferManager") + ) +) + +/** + * The method of the buffer manager that clears the buffered media of a track + * after the segment that contains a time. + */ +internal object BufferManagerDiscardBufferAfterSegmentFingerprint : Fingerprint( + definingClass = BUFFER_MANAGER_CLASS, + returnType = "V", + parameters = listOf("L", "J"), + filters = listOf( + string("discardBufferAfterCurrentSegment with disposed BufferManager") + ) +) + internal fun getCurrentVideoFormatConstructorFingerprint( videoQualityArray: String ) = object : Fingerprint( diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/video/quality/RememberVideoQualityPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/video/quality/RememberVideoQualityPatch.kt index 1dad4c546a..620e8ebab2 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/video/quality/RememberVideoQualityPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/video/quality/RememberVideoQualityPatch.kt @@ -10,7 +10,6 @@ package app.morphe.patches.youtube.video.quality -import app.morphe.patcher.extensions.InstructionExtensions.addInstruction import app.morphe.patcher.extensions.InstructionExtensions.addInstructions import app.morphe.patcher.extensions.InstructionExtensions.addInstructionsWithLabels import app.morphe.patcher.extensions.InstructionExtensions.getInstruction @@ -32,7 +31,13 @@ import app.morphe.patches.youtube.shared.openVideoIntentPatch import app.morphe.patches.youtube.video.information.EXTENSION_VIDEO_QUALITY_INTERFACE import app.morphe.patches.youtube.video.information.EXTENSION_VIDEO_QUALITY_MENU_INTERFACE import app.morphe.patches.youtube.video.information.SetVideoQualityFingerprint +import app.morphe.patches.youtube.video.information.VideoQualityFingerprint import app.morphe.patches.youtube.video.information.videoInformationPatch +import app.morphe.patches.youtube.video.format.hookAdaptiveFormat +import app.morphe.patches.youtube.video.format.videoFormatPatch +import app.morphe.patches.shared.FormatStreamModelToStringFingerprint +import app.morphe.util.findMethodFromToString +import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction import app.morphe.util.addInstructionsAtControlFlowLabel import app.morphe.util.findFieldFromToString import app.morphe.patcher.util.findFreeRegister @@ -40,6 +45,7 @@ import app.morphe.util.findInstructionIndicesReversedOrThrow import app.morphe.patcher.util.getFreeRegisterProvider import app.morphe.patcher.util.getReference import app.morphe.patcher.util.matchSingle +import app.morphe.util.indexOfFirstInstructionOrThrow import app.morphe.util.indexOfFirstInstructionReversedOrThrow import app.morphe.util.insertLiteralOverride import com.android.tools.smali.dexlib2.AccessFlags @@ -49,6 +55,7 @@ import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction import com.android.tools.smali.dexlib2.iface.reference.FieldReference +import com.android.tools.smali.dexlib2.iface.reference.MethodReference import com.android.tools.smali.dexlib2.immutable.ImmutableMethod import com.android.tools.smali.dexlib2.immutable.ImmutableMethodParameter @@ -64,6 +71,7 @@ val rememberVideoQualityPatch = bytecodePatch { versionCheckPatch, openVideoIntentPatch, fixProtoLibraryPatch, + videoFormatPatch, ) execute { @@ -277,9 +285,13 @@ val rememberVideoQualityPatch = bytecodePatch { // Inject a call to remember the selected quality for Shorts. VideoQualityItemOnClickFingerprint.method.apply { - addInstruction( + // A quality the player can play is set instead of a quality it cannot play. + addInstructions( 0, - "invoke-static { p3 }, $EXTENSION_CLASS->userChangedShortsQuality(I)V" + """ + invoke-static { p3 }, $EXTENSION_CLASS->userChangedShortsQuality(I)I + move-result p3 + """ ) findInstructionIndicesReversedOrThrow(Opcode.RETURN_VOID).forEach { index -> @@ -322,21 +334,52 @@ val rememberVideoQualityPatch = bytecodePatch { // Inject a call to remember the user selected quality for regular videos. // The quality object is used, as its name is needed to get the fixed resolution. + // A quality the player can play is set instead of a quality it cannot play. VideoQualityChangedFingerprint.let { it.method.apply { - val qualityRegister = getInstruction( - it.instructionMatches[3].index - ).registerA + val qualityIndex = it.instructionMatches[3].index + val qualityRegister = getInstruction(qualityIndex).registerA + val qualityType = getInstruction(qualityIndex).reference val constructorIndex = it.instructionMatches.last().index - addInstruction( + addInstructions( constructorIndex + 1, - "invoke-static/range { v$qualityRegister .. v$qualityRegister }, " + - "$EXTENSION_CLASS->userChangedQuality($EXTENSION_VIDEO_QUALITY_INTERFACE)V", + """ + invoke-static/range { v$qualityRegister .. v$qualityRegister }, $EXTENSION_CLASS->userChangedQuality($EXTENSION_VIDEO_QUALITY_INTERFACE)$EXTENSION_VIDEO_QUALITY_INTERFACE + move-result-object v$qualityRegister + check-cast v$qualityRegister, $qualityType + """ ) } } + // The codecs of the video formats and the video format playing, + // to know the qualities the player can play. + hookAdaptiveFormat("$EXTENSION_CLASS->setVideoFormats") + + val formatIdMethod = FormatStreamModelToStringFingerprint.originalMethod.findMethodFromToString("itag") + getCurrentVideoFormatConstructorFingerprint("[${VideoQualityFingerprint.classDef.type}").method.apply { + // The first video format field set is the video format playing. The constructor + // has no free register, so the register of the video format is used once it is set. + val videoFormatIndex = indexOfFirstInstructionOrThrow { + opcode == Opcode.IPUT_OBJECT && + getReference()?.type == formatIdMethod.definingClass + } + val videoFormatRegister = getInstruction(videoFormatIndex).registerA + + addInstructionsWithLabels( + videoFormatIndex + 1, + """ + if-eqz v$videoFormatRegister, :no_video_format + invoke-virtual/range { v$videoFormatRegister .. v$videoFormatRegister }, $formatIdMethod + move-result v$videoFormatRegister + invoke-static/range { v$videoFormatRegister .. v$videoFormatRegister }, $EXTENSION_CLASS->setCurrentVideoFormat(I)V + :no_video_format + nop + """ + ) + } + // If this flag is enabled, Shorts restart whenever the quality changes. // The restart uses the initial quality of the Short, so the quality selected by the user is lost. ShortsQualityChangeObserverPrimaryFeatureFlagFingerprint.matchAll().forEach { @@ -385,6 +428,37 @@ val rememberVideoQualityPatch = bytecodePatch { move-result p2 """ ) + + // The player buffers the video again from the time the video started, such as the + // start of a Short, so the video plays from there faster until it reaches the audio. + // The buffered video is cleared only after the segment that is playing. + val videoTrackTypeField = PlaybackControllerSetVideoFormatsFingerprint.instructionMatches + .last().instruction.getReference()!! + val discardBufferAfterSegmentMethod = BufferManagerDiscardBufferAfterSegmentFingerprint.method + + PlaybackControllerClearBufferFingerprint.let { + val stopRequestsMethod = it.instructionMatches[0].instruction.getReference()!! + val bufferManagerField = it.instructionMatches[1].instruction.getReference()!! + + it.method.addInstructionsWithLabels( + 0, + """ + sget-object v0, $videoTrackTypeField + if-ne p1, v0, :clear_buffer + invoke-static { }, $EXTENSION_CLASS->getClearVideoBufferTime()J + move-result-wide v0 + const-wide/16 v2, 0x0 + cmp-long v2, v0, v2 + if-ltz v2, :clear_buffer + invoke-virtual { p0 }, $stopRequestsMethod + iget-object v2, p0, $bufferManagerField + invoke-virtual { v2, p1, v0, v1 }, $discardBufferAfterSegmentMethod + return-void + :clear_buffer + nop + """ + ) + } } } } From 15628806e269895f55ba56460b2ebd1662074655 Mon Sep 17 00:00:00 2001 From: ILoveOpenSourceApplications <117499019+ILoveOpenSourceApplications@users.noreply.github.com> Date: Wed, 28 Oct 2026 14:54:48 +0530 Subject: [PATCH 128/189] chore(Reddit): Suppress Lint Error --- .../morphe/extension/reddit/settings/RedditActivityHook.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/extensions/reddit/src/main/java/app/morphe/extension/reddit/settings/RedditActivityHook.java b/extensions/reddit/src/main/java/app/morphe/extension/reddit/settings/RedditActivityHook.java index ecfcf44c69..9b87041bf5 100644 --- a/extensions/reddit/src/main/java/app/morphe/extension/reddit/settings/RedditActivityHook.java +++ b/extensions/reddit/src/main/java/app/morphe/extension/reddit/settings/RedditActivityHook.java @@ -9,6 +9,7 @@ import static app.morphe.extension.shared.StringRef.str; +import android.annotation.SuppressLint; import android.app.Activity; import android.app.Dialog; import android.app.DialogFragment; @@ -175,6 +176,7 @@ public void onCreate(Bundle savedInstanceState) { public Dialog onCreateDialog(Bundle savedInstanceState) { return new Dialog(getActivity(), getTheme()) { @Override + @SuppressLint("GestureBackNavigation") public void onBackPressed() { closeSettings(); } From 4344fab6ef2e8aa6694072f38478238b84e0f42a Mon Sep 17 00:00:00 2001 From: 0xrxL <127248639+0xrxL@users.noreply.github.com> Date: Wed, 28 Oct 2026 10:57:12 +0100 Subject: [PATCH 129/189] fix(YouTube): Feed thumbnail badges disappear while scrolling, are missing on cards with replaced titles and are shown on live videos --- .../patches/FeedEngagementCountsPatch.java | 2 +- .../patches/FeedVideoActionsPatch.java | 7 ++ .../youtube/patches/FeedViewCountPatch.java | 2 +- .../originaltitles/OriginalTitleRequest.java | 12 ++- .../patches/utils/FeedVideoCardsPatch.java | 80 ++++++++++++++++--- 5 files changed, 87 insertions(+), 16 deletions(-) diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FeedEngagementCountsPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FeedEngagementCountsPatch.java index 2971f28c29..20fbffd9f1 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FeedEngagementCountsPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FeedEngagementCountsPatch.java @@ -142,7 +142,7 @@ public static void onCardLaidOut(ComponentHost host, @Nullable String videoId) { if (!ENABLED) { return; } - if (videoId != null && FeedVideoCardsPatch.isInDialog(host)) { + if (videoId != null && (FeedVideoCardsPatch.isInDialog(host) || FeedVideoCardsPatch.isLiveCard(host))) { videoId = null; } CountsBadge badge = badges.get(host); diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FeedVideoActionsPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FeedVideoActionsPatch.java index bbd46f5105..88897130d7 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FeedVideoActionsPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FeedVideoActionsPatch.java @@ -537,6 +537,13 @@ private static VideoCard updateCard(ComponentHost host) { if (menuButton == null) { if (card != null) { + // Litho mounts only the part of a card that is shown, so the menu button below + // the thumbnail, such as of the classic layout, is not mounted while only the + // thumbnail is shown, as when the card leaves the screen. The button is kept + // until the card is fully shown and still has no menu button. + if (card.badgeAdded && isVideoShaped(host) && FeedVideoCardsPatch.scheduleScrollCheck(host)) { + return card; + } if (card.badgeAdded) { host.getOverlay().remove(card.badge); card.badgeAdded = false; diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FeedViewCountPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FeedViewCountPatch.java index 215c63d7a0..5fafb6f836 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FeedViewCountPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FeedViewCountPatch.java @@ -919,7 +919,7 @@ public static void onCardLaidOut(ComponentHost host, @Nullable String videoId) { || (FeedVideoCardsPatch.isVideoShaped(host, MIN_GRID_CARD_WIDTH_OF_SCREEN) && !FeedVideoCardsPatch.isPlaylistCard(host) && !FeedVideoCardsPatch.hasHorizontalList(host)); - if (checkCard && !FeedVideoCardsPatch.isInDialog(host)) { + if (checkCard && !FeedVideoCardsPatch.isInDialog(host) && !FeedVideoCardsPatch.isLiveCard(host)) { CardInfo info = new CardInfo(getTextsTop(host)); findCardInfo(host, host, info, new Rect()); if (videoId != null || info.hasDuration) { diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/originaltitles/OriginalTitleRequest.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/originaltitles/OriginalTitleRequest.java index 115282146f..4ad96d8328 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/originaltitles/OriginalTitleRequest.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/originaltitles/OriginalTitleRequest.java @@ -30,6 +30,7 @@ import app.morphe.extension.youtube.patches.dearrow.DeArrowBrandingRequest.DeArrowTitle; import app.morphe.extension.youtube.patches.dearrow.DeArrowPatch; import app.morphe.extension.youtube.patches.dearrow.DeArrowPatch.DeArrowTitlesAvailability; +import app.morphe.extension.youtube.patches.utils.FeedVideoCardsPatch; import app.morphe.extension.youtube.patches.utils.requests.ChannelIdRoutes; /** @@ -149,9 +150,10 @@ static CompletableFuture fetch(String videoId) { if (originalTitle == null && deArrow.needsOriginalTitle()) { originalTitle = fetchOriginalTitle(videoId); } + Titles titles = new Titles(deArrow.titleFor(originalTitle), fetchedTitles.originalTitle()); + FeedVideoCardsPatch.addVideoText(videoId, titles.deArrowTitle()); // Not replaced if the titles were fetched again meanwhile. - cache.replace(videoId, fetchedFuture, CompletableFuture.completedFuture( - new Titles(deArrow.titleFor(originalTitle), fetchedTitles.originalTitle()))); + cache.replace(videoId, fetchedFuture, CompletableFuture.completedFuture(titles)); } catch (DeArrowBrandingRequest.DeArrowException ex) { // The DeArrow title is fetched again later, // and the titles fetched before are used meanwhile. @@ -185,6 +187,12 @@ static CompletableFuture fetch(String videoId) { } return new Titles(deArrowTitle, fetchOriginal ? originalTitle : null); }, Utils::runOnBackgroundThread); + // The cards of the video show the title that replaces its title, + // which the elements of the video do not include. + future.thenAccept(titles -> { + FeedVideoCardsPatch.addVideoText(videoId, titles.deArrowTitle()); + FeedVideoCardsPatch.addVideoText(videoId, titles.originalTitle()); + }); cache.put(videoId, future); } return future; diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/FeedVideoCardsPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/FeedVideoCardsPatch.java index 6ba0c9a16a..2e61cf3403 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/FeedVideoCardsPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/FeedVideoCardsPatch.java @@ -138,6 +138,21 @@ public final class FeedVideoCardsPatch { private static final Map textVideoIds = Utils .createSizeRestrictedMap(MAX_TEXTS, true); + /** + * Icon of the badge of a live video on its thumbnail, such as 'yt_outline_experimental_live_black_24'. + * The live badge text depends on the language, and the avatar of a channel that is live + * shows the same text on its other videos. + */ + private static final Pattern LIVE_ICON_PATTERN = Pattern.compile("^yt_\\w+_live_\\w+$"); + + /** Number of live videos that are kept. */ + private static final int MAX_LIVE_VIDEOS = 200; + + /** Live videos, by video id. Oldest videos are removed first. */ + @GuardedBy("itself") + private static final Map liveVideoIds = Utils + .createSizeRestrictedMap(MAX_LIVE_VIDEOS, true); + /** Root host -> video of the card, of the cards laid out last. Used only on the main thread. */ private static final Map cardVideoIds = Collections.synchronizedMap( new WeakHashMap<>()); @@ -175,6 +190,19 @@ public static boolean isPlaylistCard(ComponentHost host) { return PLAYLIST.equals(cardVideoIds.get(host)); } + /** + * @return If the card is a live video, which has no like count yet and shows its number of viewers. + */ + public static boolean isLiveCard(ComponentHost host) { + String videoId = cardVideoIds.get(host); + if (videoId == null) { + return false; + } + synchronized (liveVideoIds) { + return liveVideoIds.containsKey(videoId); + } + } + /** * @return The paint of the loading spinner of a badge, shown until what the badge shows is found. */ @@ -312,26 +340,54 @@ public static byte[] onElementLoaded(byte[] bytes) { return bytes; } - synchronized (textVideoIds) { - for (ProtoNode node : textNodes) { - String text = normalizeText(node.getText()); - if (!isVideoText(text)) { - continue; - } - String previous = textVideoIds.get(text); - if (previous == null) { - textVideoIds.put(text, videoId); - } else if (!previous.equals(videoId)) { - textVideoIds.put(text, AMBIGUOUS); - } + boolean live = false; + for (ProtoNode node : textNodes) { + if (LIVE_ICON_PATTERN.matcher(node.getText()).matches()) { + live = true; + break; } } + synchronized (liveVideoIds) { + if (live) { + liveVideoIds.put(videoId, Boolean.TRUE); + } else { + // The live stream ended. + liveVideoIds.remove(videoId); + } + } + + for (ProtoNode node : textNodes) { + addVideoText(videoId, node.getText()); + } } catch (Exception ex) { Logger.printException(() -> "onElementLoaded failure", ex); } return bytes; } + /** + * Adds a text shown by the cards of a video. Also called for the titles that replace the title + * of a video on its cards, such as original titles and DeArrow titles, which the elements + * of the video do not include. Can be called on any thread. + */ + public static void addVideoText(String videoId, @Nullable String text) { + if (!ENABLED || text == null) { + return; + } + String normalized = normalizeText(text); + if (!isVideoText(normalized)) { + return; + } + synchronized (textVideoIds) { + String previous = textVideoIds.get(normalized); + if (previous == null) { + textVideoIds.put(normalized, videoId); + } else if (!previous.equals(videoId)) { + textVideoIds.put(normalized, AMBIGUOUS); + } + } + } + public static boolean isVideoShaped(ComponentHost host) { return isVideoShaped(host, MIN_WIDTH_OF_SCREEN); } From a4ecfcfc3ebe99ba452e5e87c9767a83c4760c25 Mon Sep 17 00:00:00 2001 From: ILoveOpenSourceApplications <117499019+ILoveOpenSourceApplications@users.noreply.github.com> Date: Wed, 28 Oct 2026 15:29:41 +0530 Subject: [PATCH 130/189] fix(YouTube): Watch history search bar from getting hidden when selected (#4130) --- .../layout/buttons/navigation/Fingerprints.kt | 14 +------ .../buttons/navigation/NavigationBarPatch.kt | 18 ++------- .../youtube/misc/fix/padding/Fingerprints.kt | 37 +++++++++++++++++++ .../misc/fix/padding/FixPagePaddingPatch.kt | 34 +++++++++++++++++ .../youtube/misc/settings/SettingsPatch.kt | 2 + 5 files changed, 77 insertions(+), 28 deletions(-) create mode 100644 patches/src/main/kotlin/app/morphe/patches/youtube/misc/fix/padding/Fingerprints.kt create mode 100644 patches/src/main/kotlin/app/morphe/patches/youtube/misc/fix/padding/FixPagePaddingPatch.kt diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/buttons/navigation/Fingerprints.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/buttons/navigation/Fingerprints.kt index f60e269aae..25531be44a 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/buttons/navigation/Fingerprints.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/buttons/navigation/Fingerprints.kt @@ -141,17 +141,6 @@ internal object CommentReplyPaddingFeatureFlagFingerprint : Fingerprint( ) ) -/** - * Changes how the height of the navigation bar and the miniplayer is given to the pages, - * which leaves wrong padding in some pages, such as the incognito search. - * 21.29 and older read it with a single accessor method. - */ -internal object IncognitoSearchPaddingFeatureFlagFingerprint : Fingerprint( - filters = listOf( - literal(45724388L) - ) -) - /** * Feature flags of the translucent navigation bar. 21.29 and older read each of them with a single * accessor method, newer targets read them inline in many classes, sometimes more than once in @@ -159,8 +148,7 @@ internal object IncognitoSearchPaddingFeatureFlagFingerprint : Fingerprint( */ internal val TRANSLUCENT_NAVIGATION_BAR_FEATURE_FLAGS = listOf( 45630927L, // Blurred, translucent background of the navigation bar. - NAVIGATION_BAR_COVERS_SYSTEM_BUTTONS_FLAG, - 45631257L // Feed laid out behind the navigation bar instead of above it. + NAVIGATION_BAR_COVERS_SYSTEM_BUTTONS_FLAG ) /** diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/buttons/navigation/NavigationBarPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/buttons/navigation/NavigationBarPatch.kt index e397fde8f4..b602bb24c6 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/buttons/navigation/NavigationBarPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/buttons/navigation/NavigationBarPatch.kt @@ -44,7 +44,6 @@ import app.morphe.patches.youtube.misc.navigation.addBottomBarContainerHook import app.morphe.patches.youtube.misc.navigation.hookNavigationButtonCreated import app.morphe.patches.youtube.misc.navigation.navigationBarHookPatch import app.morphe.patches.youtube.misc.playservice.is_20_31_or_greater -import app.morphe.patches.youtube.misc.playservice.is_21_07_or_greater import app.morphe.patches.youtube.misc.playservice.is_21_35_or_greater import app.morphe.patches.youtube.misc.playservice.versionCheckPatch import app.morphe.patches.youtube.misc.settings.PreferenceScreen @@ -225,12 +224,12 @@ val navigationBarPatch = bytecodePatch( // they break. Every fix checks that the flags are turned on by this patch, so the fixes do // nothing if the flags are left to the account, as with the default navigation bar style. - // The translucent navigation bar comes from three feature flags that not every account + // The translucent navigation bar comes from two feature flags that not every account // gets, so they are turned on to make the bar look the same on every account, and the opaque // style below paints over the same translucent bar everywhere. The flag that makes the bar // cover the system navigation buttons is turned on only when the window is drawn edge to - // edge. The blur and the feed behind the bar do not change the room left for the system - // navigation buttons, so they are also turned on without it. + // edge. The blur does not change the room left for the system + // navigation buttons, so it is also turned on without it. TranslucentNavigationBarFeatureFlagsFingerprint.matchAll().forEach { match -> match.method.apply { TRANSLUCENT_NAVIGATION_BAR_FEATURE_FLAGS.forEach { flag -> @@ -325,17 +324,6 @@ val navigationBarPatch = bytecodePatch( ) } - // A flag gives the pages the height of the navigation bar and the miniplayer in another way, - // which leaves wrong padding in some pages, such as the incognito search. It is turned off. - if (is_21_07_or_greater) { - IncognitoSearchPaddingFeatureFlagFingerprint.matchAllMethodIndicesForEach { index -> - val moveResultIndex = indexOfFirstInstructionOrThrow(index, Opcode.MOVE_RESULT) - val register = getInstruction(moveResultIndex).registerA - - addInstruction(moveResultIndex + 1, "const/4 v$register, 0x0") - } - } - // The slim status bar ("No connection", "Back online") is laid out below the bar covering // the system navigation buttons, so the buttons cover it. The room for the buttons is moved // below it while it is shown. diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/misc/fix/padding/Fingerprints.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/fix/padding/Fingerprints.kt new file mode 100644 index 0000000000..6eb5f832de --- /dev/null +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/fix/padding/Fingerprints.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to Morphe contributions. + */ + +package app.morphe.patches.youtube.misc.fix.padding + +import app.morphe.patcher.Fingerprint +import app.morphe.patcher.anyInstruction +import app.morphe.patcher.literal + +/** + * Changes how the height of the navigation bar and the miniplayer is given to the pages, + * which leaves wrong padding in some pages, such as the incognito search. + * 21.29 and older read it with a single accessor method. + */ +internal object IncognitoSearchPaddingFeatureFlagFingerprint : Fingerprint( + filters = listOf( + literal(45724388L) + ) +) + +/** + * Lays out the feed behind the navigation bar instead of above it, which triggers + * dynamic runtime insets/padding recalculations on the RecyclerView that cause watch history + * items (and other top horizontal carousels) to briefly detach and vanish (#3135). + */ +internal object FeedBehindNavigationBarFeatureFlagsFingerprint : Fingerprint( + filters = listOf( + anyInstruction( + literal(45631257L), + literal(45708229L) + ) + ) +) diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/misc/fix/padding/FixPagePaddingPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/fix/padding/FixPagePaddingPatch.kt new file mode 100644 index 0000000000..1865ca03c7 --- /dev/null +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/fix/padding/FixPagePaddingPatch.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to Morphe contributions. + */ + +package app.morphe.patches.youtube.misc.fix.padding + +import app.morphe.patcher.patch.bytecodePatch +import app.morphe.patcher.util.matchAllMethodIndicesForEach +import app.morphe.patches.youtube.misc.playservice.is_21_07_or_greater +import app.morphe.patches.youtube.misc.playservice.versionCheckPatch +import app.morphe.patches.youtube.shared.Constants.COMPATIBILITY_YOUTUBE +import app.morphe.util.insertLiteralOverride + +internal val fixPagePaddingPatch = bytecodePatch( + description = "Fixes layout padding glitches in incognito search and watch history." +) { + dependsOn(versionCheckPatch) + compatibleWith(COMPATIBILITY_YOUTUBE) + + execute { + if (is_21_07_or_greater) { + IncognitoSearchPaddingFeatureFlagFingerprint.matchAllMethodIndicesForEach(requireMatches = false) { index -> + insertLiteralOverride(index, false) + } + } + + FeedBehindNavigationBarFeatureFlagsFingerprint.matchAllMethodIndicesForEach(requireMatches = false) { index -> + insertLiteralOverride(index, false) + } + } +} diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/misc/settings/SettingsPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/settings/SettingsPatch.kt index cb6e791ac0..1ffae4dbf1 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/misc/settings/SettingsPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/settings/SettingsPatch.kt @@ -51,6 +51,7 @@ import app.morphe.patches.youtube.misc.extension.sharedExtensionPatch import app.morphe.patches.youtube.misc.fix.accessibility.fixShortsAccessibilityPatch import app.morphe.patches.youtube.misc.fix.contentprovider.fixContentProviderPatch import app.morphe.patches.youtube.misc.fix.likebutton.fixLikeButtonPatch +import app.morphe.patches.youtube.misc.fix.padding.fixPagePaddingPatch import app.morphe.patches.youtube.misc.fix.pipchatbar.fixPipChatBarPatch import app.morphe.patches.youtube.misc.fix.playbackspeed.fixPlaybackSpeedWhilePlayingPatch import app.morphe.patches.youtube.misc.fix.preference.fixPreferenceIconPatch @@ -226,6 +227,7 @@ val settingsPatch = bytecodePatch( fixLikeButtonPatch, fixContentProviderPatch, fixPipChatBarPatch, + fixPagePaddingPatch, addLicensePatch, removeLinkVerification, crashLogPatch( From aeac8c9c15f93127a41326a3e5ae913a7c75dbae Mon Sep 17 00:00:00 2001 From: 0xrxL <127248639+0xrxL@users.noreply.github.com> Date: Wed, 28 Oct 2026 11:25:33 +0100 Subject: [PATCH 131/189] fix(YouTube - Ambient mode): Bypass Ambient mode restrictions does not work with 21.43 --- .../hide/ambientmode/AmbientModePatch.kt | 8 ++++ .../layout/hide/ambientmode/Fingerprints.kt | 42 +++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/ambientmode/AmbientModePatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/ambientmode/AmbientModePatch.kt index 8673acc53a..72d04678a2 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/ambientmode/AmbientModePatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/ambientmode/AmbientModePatch.kt @@ -140,12 +140,20 @@ val ambientModePatch = bytecodePatch( PowerSaveModeSyntheticFingerprint.matchOrNull( mutableClassDefBy(reference.definingClass) )?.method?.hook() + + // 21.43+ inlines the lambda into the receiver. + if (is_21_43_or_greater) hook() } } } if (is_21_43_or_greater) { IntentActionSyntheticAlternativeFingerprint.method.hook() + + // 21.43+ inlines the lambdas that read the first value for the Ambient mode receiver. + // The receiver is registered on the main thread, or asynchronously if a server flag is on. + powerSaveModeInitialValueFingerprint(PowerSaveModeReceiverRegisterFingerprint.method).method.hook() + PowerSaveModeInitialValueAsyncFingerprint.method.hook() } else if (is_21_03_or_greater) { IntentActionSyntheticAlternativeLegacyFingerprint.method.hook() } diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/ambientmode/Fingerprints.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/ambientmode/Fingerprints.kt index 1a7db88da7..b7652e3b90 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/ambientmode/Fingerprints.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/ambientmode/Fingerprints.kt @@ -12,6 +12,7 @@ import app.morphe.patcher.opcode import app.morphe.patcher.string import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.reference.MethodReference private const val GET_ACTION_METHOD_CALL = "Landroid/content/Intent;->getAction()Ljava/lang/String;" internal const val IS_POWER_SAVE_MODE_METHOD_CALL = "Landroid/os/PowerManager;->isPowerSaveMode()Z" @@ -223,6 +224,47 @@ internal object IntentActionSyntheticAlternativeLegacyFingerprint : Fingerprint( ) ) +/** + * 21.43+ Registers the receiver of [IntentActionBroadcastReceiverFingerprint]. + */ +internal object PowerSaveModeReceiverRegisterFingerprint : Fingerprint( + accessFlags = listOf(AccessFlags.PUBLIC, AccessFlags.STATIC), + returnType = "V", + parameters = listOf("Landroid/content/Context;", "L"), + filters = listOf( + string(POWER_SAVE_MODE_CHANGED), + methodCall(definingClass = "Landroid/content/Context;", name = "registerReceiver"), + ) +) + +/** + * 21.43+ Reads the first power save mode value, then calls [PowerSaveModeReceiverRegisterFingerprint]. + */ +internal fun powerSaveModeInitialValueFingerprint(registerMethod: MethodReference) = Fingerprint( + accessFlags = listOf(AccessFlags.PUBLIC, AccessFlags.FINAL), + returnType = "V", + parameters = listOf("Ljava/lang/Object;"), + filters = listOf( + checkCast("Landroid/os/PowerManager;"), + methodCall(smali = IS_POWER_SAVE_MODE_METHOD_CALL), + methodCall(reference = registerMethod), + ) +) + +/** + * 21.43+ Reads the first power save mode value, if the receiver is registered asynchronously. + */ +internal object PowerSaveModeInitialValueAsyncFingerprint : Fingerprint( + classFingerprint = PowerSaveModeReceiverRegisterFingerprint, + accessFlags = listOf(AccessFlags.PUBLIC, AccessFlags.STATIC), + returnType = "V", + parameters = listOf("L", "Landroid/content/Context;"), + filters = listOf( + checkCast("Landroid/os/PowerManager;"), + methodCall(smali = IS_POWER_SAVE_MODE_METHOD_CALL), + ) +) + internal object PowerSaveModeSyntheticFingerprint : Fingerprint( accessFlags = listOf(AccessFlags.PUBLIC, AccessFlags.FINAL), returnType = "V", From a7ce357d2b654aed2a241c901bf2bc3df2ce7274 Mon Sep 17 00:00:00 2001 From: ILoveOpenSourceApplications <117499019+ILoveOpenSourceApplications@users.noreply.github.com> Date: Wed, 28 Oct 2026 16:27:22 +0530 Subject: [PATCH 132/189] feat(YouTube - Hide layout components): Add "Hide 'Unavailable videos are hidden' banner" setting (#4134) --- .../components/LayoutComponentsFilter.java | 33 ++++++++++++------- .../extension/youtube/settings/Settings.java | 1 + .../hide/general/HideLayoutComponentsPatch.kt | 1 + .../addresources/values/youtube/strings.xml | 3 ++ 4 files changed, 27 insertions(+), 11 deletions(-) diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/components/LayoutComponentsFilter.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/components/LayoutComponentsFilter.java index a269dff6cd..d8480f080e 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/components/LayoutComponentsFilter.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/components/LayoutComponentsFilter.java @@ -90,8 +90,8 @@ public final class LayoutComponentsFilter extends Filter { private final StringFilterGroup expandableMetadata; private final ByteArrayFilterGroup summaryCardBuffer; private final StringFilterGroup exploreTopicsShelf; - private final StringFilterGroup inviteToMessageCard; - private final ByteArrayFilterGroup inviteToMessageCardBuffer; + private final StringFilterGroup linearLayout; + private final ByteArrayFilterGroupList linearLayoutGroupList = new ByteArrayFilterGroupList(); private final StringFilterGroup liveStream; private final ByteArrayFilterGroup liveStreamBuffer; private final StringFilterGroup notificationsMenuHeader; @@ -147,14 +147,20 @@ public LayoutComponentsFilter() { // 'This week', 'Older') uses the exact same identifier and an otherwise byte // identical buffer, and the title is localized by the server without an app string // resource, so there is no language independent way to match it. - inviteToMessageCard = new StringFilterGroup( - Settings.HIDE_INVITE_TO_MESSAGE_CARD, + linearLayout = new StringFilterGroup( + null, "linear_layout.e" ); - inviteToMessageCardBuffer = new ByteArrayFilterGroup( - null, - "connections_inbox_zero_state" + linearLayoutGroupList.addAll( + new ByteArrayFilterGroup( + Settings.HIDE_INVITE_TO_MESSAGE_CARD, + "connections_inbox_zero_state" + ), + new ByteArrayFilterGroup( + Settings.HIDE_UNAVAILABLE_VIDEOS_HIDDEN_BANNER, + "basic_content" + ) ); // Feed and search result items. @@ -180,8 +186,8 @@ public LayoutComponentsFilter() { addIdentifierCallbacks( cellDivider, exploreTopicsShelf, + linearLayout, liveChatReplay, - inviteToMessageCard, liveStream, seekEduOverlay ); @@ -624,18 +630,23 @@ public boolean isFiltered(ContextInterface contextInterface, } } - if (matchedGroup == inviteToMessageCard) { + if (matchedGroup == linearLayout) { // The identifier is generic and used all over the app. if (contentIndex != 0) { return false; } - if (!inviteToMessageCardBuffer.check(buffer).isFiltered()) { + FilterGroupResult result = linearLayoutGroupList.check(buffer); + if (!result.isFiltered()) { return false; } // Check the navigation button last and only after all buffer checks pass. - return NavigationButton.getSelectedNavigationButton() == NavigationButton.NOTIFICATIONS; + if (result.getSetting() == Settings.HIDE_INVITE_TO_MESSAGE_CARD) { + return NavigationButton.getSelectedNavigationButton() == NavigationButton.NOTIFICATIONS; + } + + return true; } if (matchedGroup == liveStream) { diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/Settings.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/Settings.java index c58722e8a6..b2794c1ee7 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/Settings.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/Settings.java @@ -178,6 +178,7 @@ public class Settings extends SharedYouTubeSettings { public static final BooleanSetting HIDE_SUBSCRIBED_CHANNELS_BAR_NAMES = new BooleanSetting("morphe_hide_subscribed_channels_bar_names", FALSE, parentNot(HIDE_SUBSCRIBED_CHANNELS_BAR)); public static final BooleanSetting HIDE_SURVEYS = new BooleanSetting("morphe_hide_surveys", TRUE); public static final BooleanSetting HIDE_TICKET_SHELF = new BooleanSetting("morphe_hide_ticket_shelf", FALSE); + public static final BooleanSetting HIDE_UNAVAILABLE_VIDEOS_HIDDEN_BANNER = new BooleanSetting("morphe_hide_unavailable_videos_hidden_banner", FALSE); public static final BooleanSetting HIDE_UPLOAD_TIME = new BooleanSetting("morphe_hide_upload_time", FALSE); public static final BooleanSetting HIDE_VIDEO_LABELS = new BooleanSetting("morphe_hide_video_labels", TRUE); public static final BooleanSetting HIDE_VIDEO_THUMBNAIL = new BooleanSetting("morphe_hide_video_thumbnail", FALSE); diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/general/HideLayoutComponentsPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/general/HideLayoutComponentsPatch.kt index 8bf0f85a97..70c3d75f42 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/general/HideLayoutComponentsPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/general/HideLayoutComponentsPatch.kt @@ -356,6 +356,7 @@ val hideLayoutComponentsPatch = bytecodePatch( SwitchPreference("morphe_hide_subscribed_channels_bar_names", summary = true), SwitchPreference("morphe_hide_surveys", summary = true), SwitchPreference("morphe_hide_ticket_shelf"), + SwitchPreference("morphe_hide_unavailable_videos_hidden_banner", summary = true), SwitchPreference( "morphe_hide_upload_time", summary = true, diff --git a/patches/src/main/resources/addresources/values/youtube/strings.xml b/patches/src/main/resources/addresources/values/youtube/strings.xml index 34c88d0a35..5b2ef3e2a4 100644 --- a/patches/src/main/resources/addresources/values/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values/youtube/strings.xml @@ -119,6 +119,9 @@ Changes include: Hide surveys Hide surveys and nudges about feed recommendation accuracy Hide ticket shelf + + Hide \'Unavailable videos are hidden\' banner + Hides the \'Unavailable videos are hidden\' banner in playlists Hide upload time "Limitations: From e6fd2c09f88c5164fbc6415fc1d2641d9d3f2ef8 Mon Sep 17 00:00:00 2001 From: 0xrxL <127248639+0xrxL@users.noreply.github.com> Date: Wed, 28 Oct 2026 12:00:09 +0100 Subject: [PATCH 133/189] refactor(YouTube - Video quality): Use Morphe matching helpers and shared utils in the Shorts quality code --- .../quality/RememberVideoQualityPatch.java | 8 +--- .../youtube/video/quality/Fingerprints.kt | 5 +- .../quality/RememberVideoQualityPatch.kt | 46 +++++++++---------- 3 files changed, 27 insertions(+), 32 deletions(-) diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/playback/quality/RememberVideoQualityPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/playback/quality/RememberVideoQualityPatch.java index b09574b858..9645fc40ae 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/playback/quality/RememberVideoQualityPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/playback/quality/RememberVideoQualityPatch.java @@ -21,7 +21,6 @@ import java.util.Collection; import java.util.Collections; import java.util.HashMap; -import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -89,12 +88,7 @@ public class RememberVideoQualityPatch { * The codec of each video format by format ID (itag), by video ID. */ private static final Map> videoFormatCodecs = Collections.synchronizedMap( - new LinkedHashMap<>(MAX_VIDEO_FORMAT_CODECS, 0.75f, true) { - @Override - protected boolean removeEldestEntry(Map.Entry> eldest) { - return size() > MAX_VIDEO_FORMAT_CODECS; - } - }); + Utils.createSizeRestrictedMap(MAX_VIDEO_FORMAT_CODECS, true)); /** * Format ID (itag) of the video format playing. diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/video/quality/Fingerprints.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/video/quality/Fingerprints.kt index eb04c523a5..46dd0ff85a 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/video/quality/Fingerprints.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/video/quality/Fingerprints.kt @@ -25,6 +25,7 @@ import app.morphe.patches.shared.CurrentAudioVideoFormatToStringFingerprint import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.iface.reference.FieldReference +import com.android.tools.smali.dexlib2.iface.reference.MethodReference internal const val FIXED_RESOLUTION_STRING = ", initialPlaybackVideoQualityFixedResolution=" @@ -342,7 +343,7 @@ internal fun getSetVideoQualityResolutionFingerprint(videoQualityMenuType: Strin * The item click listener of the Shorts quality menu, which sets the selected quality by its resolution. * The listener is either the menu of [VideoQualityItemOnClickFingerprint] or a class it calls. */ -internal fun getVideoQualityItemSetResolutionFingerprint(setResolutionMethod: String) = Fingerprint( +internal fun getVideoQualityItemSetResolutionFingerprint(setResolutionMethod: MethodReference) = Fingerprint( name = "onItemClick", returnType = "V", parameters = listOf( @@ -352,7 +353,7 @@ internal fun getVideoQualityItemSetResolutionFingerprint(setResolutionMethod: St "J" ), filters = listOf( - methodCall(smali = setResolutionMethod) + methodCall(reference = setResolutionMethod) ) ) diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/video/quality/RememberVideoQualityPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/video/quality/RememberVideoQualityPatch.kt index 620e8ebab2..1384b7e304 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/video/quality/RememberVideoQualityPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/video/quality/RememberVideoQualityPatch.kt @@ -13,9 +13,15 @@ package app.morphe.patches.youtube.video.quality import app.morphe.patcher.extensions.InstructionExtensions.addInstructions import app.morphe.patcher.extensions.InstructionExtensions.addInstructionsWithLabels import app.morphe.patcher.extensions.InstructionExtensions.getInstruction +import app.morphe.patcher.fieldAccess import app.morphe.patcher.patch.bytecodePatch +import app.morphe.patcher.util.findFreeRegister +import app.morphe.patcher.util.getFreeRegisterProvider +import app.morphe.patcher.util.getReference +import app.morphe.patcher.util.matchSingle import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable import app.morphe.patcher.util.smali.ExternalLabel +import app.morphe.patches.shared.FormatStreamModelToStringFingerprint import app.morphe.patches.shared.misc.fix.proto.fixProtoLibraryPatch import app.morphe.patches.shared.misc.fix.proto.parseByteArrayMethodRef import app.morphe.patches.shared.misc.settings.preference.ListPreference @@ -28,23 +34,17 @@ import app.morphe.patches.youtube.misc.settings.settingsPatch import app.morphe.patches.youtube.shared.VideoQualityChangedFingerprint import app.morphe.patches.youtube.shared.hookVideoIntent import app.morphe.patches.youtube.shared.openVideoIntentPatch +import app.morphe.patches.youtube.video.format.hookAdaptiveFormat +import app.morphe.patches.youtube.video.format.videoFormatPatch import app.morphe.patches.youtube.video.information.EXTENSION_VIDEO_QUALITY_INTERFACE import app.morphe.patches.youtube.video.information.EXTENSION_VIDEO_QUALITY_MENU_INTERFACE import app.morphe.patches.youtube.video.information.SetVideoQualityFingerprint import app.morphe.patches.youtube.video.information.VideoQualityFingerprint import app.morphe.patches.youtube.video.information.videoInformationPatch -import app.morphe.patches.youtube.video.format.hookAdaptiveFormat -import app.morphe.patches.youtube.video.format.videoFormatPatch -import app.morphe.patches.shared.FormatStreamModelToStringFingerprint -import app.morphe.util.findMethodFromToString -import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction import app.morphe.util.addInstructionsAtControlFlowLabel import app.morphe.util.findFieldFromToString -import app.morphe.patcher.util.findFreeRegister import app.morphe.util.findInstructionIndicesReversedOrThrow -import app.morphe.patcher.util.getFreeRegisterProvider -import app.morphe.patcher.util.getReference -import app.morphe.patcher.util.matchSingle +import app.morphe.util.findMethodFromToString import app.morphe.util.indexOfFirstInstructionOrThrow import app.morphe.util.indexOfFirstInstructionReversedOrThrow import app.morphe.util.insertLiteralOverride @@ -53,9 +53,9 @@ import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction import com.android.tools.smali.dexlib2.iface.reference.FieldReference -import com.android.tools.smali.dexlib2.iface.reference.MethodReference import com.android.tools.smali.dexlib2.immutable.ImmutableMethod import com.android.tools.smali.dexlib2.immutable.ImmutableMethodParameter @@ -309,10 +309,11 @@ val rememberVideoQualityPatch = bytecodePatch { SetVideoQualityFingerprint.instructionMatches[1].getFieldAccessed().type ).matchSingle().method - getVideoQualityItemSetResolutionFingerprint(setResolutionMethod.toString()).matchAll().forEach { + getVideoQualityItemSetResolutionFingerprint(setResolutionMethod).matchAll().forEach { it.method.apply { - val index = it.instructionMatches.first().index - val setResolutionInstruction = getInstruction(index) + val setResolutionMatch = it.instructionMatches.first() + val index = setResolutionMatch.index + val setResolutionInstruction = setResolutionMatch.getInstruction() val menuRegister = setResolutionInstruction.registerC val qualitySetRegister = findFreeRegister( index, @@ -337,9 +338,9 @@ val rememberVideoQualityPatch = bytecodePatch { // A quality the player can play is set instead of a quality it cannot play. VideoQualityChangedFingerprint.let { it.method.apply { - val qualityIndex = it.instructionMatches[3].index - val qualityRegister = getInstruction(qualityIndex).registerA - val qualityType = getInstruction(qualityIndex).reference + val qualityMatch = it.instructionMatches[3] + val qualityRegister = qualityMatch.getInstruction().registerA + val qualityType = qualityMatch.getInstruction().reference val constructorIndex = it.instructionMatches.last().index addInstructions( @@ -361,10 +362,9 @@ val rememberVideoQualityPatch = bytecodePatch { getCurrentVideoFormatConstructorFingerprint("[${VideoQualityFingerprint.classDef.type}").method.apply { // The first video format field set is the video format playing. The constructor // has no free register, so the register of the video format is used once it is set. - val videoFormatIndex = indexOfFirstInstructionOrThrow { - opcode == Opcode.IPUT_OBJECT && - getReference()?.type == formatIdMethod.definingClass - } + val videoFormatIndex = indexOfFirstInstructionOrThrow( + fieldAccess(opcode = Opcode.IPUT_OBJECT, type = formatIdMethod.definingClass) + ) val videoFormatRegister = getInstruction(videoFormatIndex).registerA addInstructionsWithLabels( @@ -433,12 +433,12 @@ val rememberVideoQualityPatch = bytecodePatch { // start of a Short, so the video plays from there faster until it reaches the audio. // The buffered video is cleared only after the segment that is playing. val videoTrackTypeField = PlaybackControllerSetVideoFormatsFingerprint.instructionMatches - .last().instruction.getReference()!! + .last().getFieldAccessed() val discardBufferAfterSegmentMethod = BufferManagerDiscardBufferAfterSegmentFingerprint.method PlaybackControllerClearBufferFingerprint.let { - val stopRequestsMethod = it.instructionMatches[0].instruction.getReference()!! - val bufferManagerField = it.instructionMatches[1].instruction.getReference()!! + val stopRequestsMethod = it.instructionMatches[0].getMethodCalled() + val bufferManagerField = it.instructionMatches[1].getFieldAccessed() it.method.addInstructionsWithLabels( 0, From 328b85acb6fe32b7722dda7fd41ff2a8988e9c7d Mon Sep 17 00:00:00 2001 From: 0xrxL <127248639+0xrxL@users.noreply.github.com> Date: Wed, 28 Oct 2026 12:00:23 +0100 Subject: [PATCH 134/189] fix(YouTube - Video quality): Short plays the previous quality segments again when it loops --- .../quality/RememberVideoQualityPatch.java | 6 ++- .../youtube/video/quality/Fingerprints.kt | 46 +++++++++++++++++ .../quality/RememberVideoQualityPatch.kt | 51 ++++++++++++++++++- 3 files changed, 100 insertions(+), 3 deletions(-) diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/playback/quality/RememberVideoQualityPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/playback/quality/RememberVideoQualityPatch.java index 9645fc40ae..9b6c7fd1d6 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/playback/quality/RememberVideoQualityPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/playback/quality/RememberVideoQualityPatch.java @@ -614,9 +614,11 @@ public static boolean clearVideoBufferOnQualityChange(boolean original) { * from that time and is played faster until it reaches the audio. * The buffered video is cleared only after the segment that is playing, as YouTube does * when the audio track changes, so the new quality is played after it without a gap. + * The video buffered before that segment is cleared too, so a Short that loops + * does not play the previous quality from its start. * - * @return The playback time in microseconds after which the buffered video is cleared, - * or -1 to clear the whole buffered video. + * @return The playback time in microseconds after and before whose segment the buffered video + * is cleared, or -1 to clear the whole buffered video. */ public static long getClearVideoBufferTime() { final long timeUs = clearVideoBufferTimeUs; diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/video/quality/Fingerprints.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/video/quality/Fingerprints.kt index 46dd0ff85a..0c376f1d40 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/video/quality/Fingerprints.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/video/quality/Fingerprints.kt @@ -245,6 +245,52 @@ internal object BufferManagerDiscardBufferAfterSegmentFingerprint : Fingerprint( ) ) +internal const val SAMPLE_HOLDER_CLASS = + "Lcom/google/android/libraries/youtube/media/player/exo2/platypus/mediapush/SampleHolder;" + +/** + * The method of the buffer manager that returns the buffered media of a track. + */ +internal object BufferManagerGetMediaSegmentHolderFingerprint : Fingerprint( + definingClass = BUFFER_MANAGER_CLASS, + accessFlags = listOf(AccessFlags.PUBLIC, AccessFlags.FINAL), + returnType = "Lcom/google/android/libraries/youtube/media/player/exo2/platypus/mediapush/MediaSegmentHolder;", + parameters = listOf("L") +) + +/** + * 21.17 - 21.42 + * + * The method of the buffered media of an audio or video track that clears + * the media before the segment that contains a time. + */ +internal object SampleHolderDiscardBufferBeforeSegmentLegacyFingerprint : Fingerprint( + definingClass = SAMPLE_HOLDER_CLASS, + returnType = "V", + parameters = listOf("J"), + filters = listOf( + methodCall(smali = "Ljava/lang/Math;->min(JJ)J"), + methodCall(smali = "Ljava/util/List;->subList(II)Ljava/util/List;"), + methodCall(smali = "Ljava/util/List;->clear()V") + ) +) + +/** + * 21.43+ + * + * Same as [SampleHolderDiscardBufferBeforeSegmentLegacyFingerprint], + * with the parameters: time, playback time and size of the media kept before the playback time. + */ +internal object SampleHolderDiscardBufferBeforeSegmentFingerprint : Fingerprint( + definingClass = SAMPLE_HOLDER_CLASS, + returnType = "V", + parameters = listOf("J", "J", "J"), + filters = listOf( + methodCall(definingClass = "this", returnType = "Z", parameters = listOf("J", "J", "J")), + methodCall(definingClass = "this", returnType = "V", parameters = listOf("J", "J", "J")) + ) +) + internal fun getCurrentVideoFormatConstructorFingerprint( videoQualityArray: String ) = object : Fingerprint( diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/video/quality/RememberVideoQualityPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/video/quality/RememberVideoQualityPatch.kt index 1384b7e304..f1fdcf70f5 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/video/quality/RememberVideoQualityPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/video/quality/RememberVideoQualityPatch.kt @@ -29,6 +29,7 @@ import app.morphe.patches.shared.misc.settings.preference.SwitchPreference import app.morphe.patches.youtube.misc.extension.sharedExtensionPatch import app.morphe.patches.youtube.misc.playertype.playerTypeHookPatch import app.morphe.patches.youtube.misc.playservice.is_21_17_or_greater +import app.morphe.patches.youtube.misc.playservice.is_21_43_or_greater import app.morphe.patches.youtube.misc.playservice.versionCheckPatch import app.morphe.patches.youtube.misc.settings.settingsPatch import app.morphe.patches.youtube.shared.VideoQualityChangedFingerprint @@ -431,10 +432,54 @@ val rememberVideoQualityPatch = bytecodePatch { // The player buffers the video again from the time the video started, such as the // start of a Short, so the video plays from there faster until it reaches the audio. - // The buffered video is cleared only after the segment that is playing. + // The buffered video is cleared after and before the segment that is playing. val videoTrackTypeField = PlaybackControllerSetVideoFormatsFingerprint.instructionMatches .last().getFieldAccessed() val discardBufferAfterSegmentMethod = BufferManagerDiscardBufferAfterSegmentFingerprint.method + val getMediaSegmentHolderMethod = BufferManagerGetMediaSegmentHolderFingerprint.method + + // The video buffered before the segment that is playing is cleared too, + // otherwise a Short plays the previous quality from its start when it loops. + // The method of the player is not public before 21.26, so a public method calls it. + val sampleHolderDiscardBufferMethod = if (is_21_43_or_greater) { + SampleHolderDiscardBufferBeforeSegmentFingerprint + } else { + SampleHolderDiscardBufferBeforeSegmentLegacyFingerprint + }.let { + val discardBufferBeforeSegmentMethod = ImmutableMethod( + it.classDef.type, + "patch_discardBufferBeforeSegment", + listOf(ImmutableMethodParameter("J", null, null)), + "V", + AccessFlags.PUBLIC.value or AccessFlags.FINAL.value, + null, + null, + MutableMethodImplementation(10), + ).toMutable().apply { + addInstructions( + 0, + if (is_21_43_or_greater) { + // No media is kept before the playback time. + """ + move-object v0, p0 + move-wide v1, p1 + move-wide v3, p1 + const-wide/16 v5, 0x0 + invoke-virtual/range { v0 .. v6 }, ${it.method} + return-void + """ + } else { + """ + invoke-virtual { p0, p1, p2 }, ${it.method} + return-void + """ + } + ) + } + it.classDef.methods.add(discardBufferBeforeSegmentMethod) + + discardBufferBeforeSegmentMethod + } PlaybackControllerClearBufferFingerprint.let { val stopRequestsMethod = it.instructionMatches[0].getMethodCalled() @@ -453,6 +498,10 @@ val rememberVideoQualityPatch = bytecodePatch { invoke-virtual { p0 }, $stopRequestsMethod iget-object v2, p0, $bufferManagerField invoke-virtual { v2, p1, v0, v1 }, $discardBufferAfterSegmentMethod + invoke-virtual { v2, p1 }, $getMediaSegmentHolderMethod + move-result-object v2 + check-cast v2, $SAMPLE_HOLDER_CLASS + invoke-virtual { v2, v0, v1 }, $sampleHolderDiscardBufferMethod return-void :clear_buffer nop From 4c002101e3472e9700acb8f92a63291f69b61116 Mon Sep 17 00:00:00 2001 From: pharmacomaniac Date: Wed, 28 Oct 2026 07:05:08 -0400 Subject: [PATCH 135/189] fix(YouTube): Prevent TalkBack crashes caused by Litho hover recursion (#4129) --- .../misc/fix/accessibility/Fingerprints.kt | 16 +++++ .../accessibility/FixTouchExplorationPatch.kt | 58 +++++++++++++++++++ .../youtube/misc/settings/SettingsPatch.kt | 2 + 3 files changed, 76 insertions(+) create mode 100644 patches/src/main/kotlin/app/morphe/patches/youtube/misc/fix/accessibility/FixTouchExplorationPatch.kt diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/misc/fix/accessibility/Fingerprints.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/fix/accessibility/Fingerprints.kt index 54bd09460c..c489ca7bc8 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/misc/fix/accessibility/Fingerprints.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/fix/accessibility/Fingerprints.kt @@ -11,6 +11,22 @@ import com.android.tools.smali.dexlib2.Opcode private const val BOTTOM_UI_CONTAINER = "Lcom/google/android/apps/youtube/app/common/ui/bottomui/BottomUiContainer;" +internal object TouchDelegateConstructorFingerprint : Fingerprint( + superclass = "Landroid/view/TouchDelegate;", + name = "", + parameters = listOf("Lcom/facebook/litho/ComponentHost;") +) + +/** + * 21.30+ + */ +internal object TouchExplorationHoverEventFingerprint : Fingerprint( + classFingerprint = TouchDelegateConstructorFingerprint, + name = "onTouchExplorationHoverEvent", + returnType = "Z", + parameters = listOf("Landroid/view/MotionEvent;") +) + internal object BottomUiKeyboardVisibilityFingerprint : Fingerprint( returnType = "V", parameters = listOf("L"), diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/misc/fix/accessibility/FixTouchExplorationPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/fix/accessibility/FixTouchExplorationPatch.kt new file mode 100644 index 0000000000..773b08ffeb --- /dev/null +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/fix/accessibility/FixTouchExplorationPatch.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/4129 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.patches.youtube.misc.fix.accessibility + +import app.morphe.patcher.patch.bytecodePatch +import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable +import app.morphe.patcher.util.returnEarly +import app.morphe.patches.youtube.misc.playservice.is_20_51_or_greater +import app.morphe.patches.youtube.misc.playservice.is_21_30_or_greater +import app.morphe.patches.youtube.misc.playservice.is_21_42_or_greater +import app.morphe.patches.youtube.misc.playservice.versionCheckPatch +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation +import com.android.tools.smali.dexlib2.immutable.ImmutableMethod +import com.android.tools.smali.dexlib2.immutable.ImmutableMethodParameter + +/** + * Touch exploration events can loop between Litho's TouchDelegate and ComponentHost, causing StackOverflowError. + * Fixes https://github.com/MorpheApp/morphe-patches/issues/4045. + */ +internal val fixTouchExplorationPatch = bytecodePatch( + description = "Fixes stack overflow crashes caused by recursive TalkBack event forwarding in Litho." +) { + dependsOn(versionCheckPatch) + + execute { + if (!is_20_51_or_greater || is_21_42_or_greater) return@execute + + // The inherited TouchDelegate implementation forwards the event back to the ComponentHost. + // Return false instead, the same as the flag gated workaround YouTube added in 21.30. + // Ordinary touch events use the class's own implementation. + if (is_21_30_or_greater) { + TouchExplorationHoverEventFingerprint.method.returnEarly(false) + } else { + val classDef = TouchDelegateConstructorFingerprint.classDef + + classDef.methods.add( + ImmutableMethod( + classDef.type, + "onTouchExplorationHoverEvent", + listOf(ImmutableMethodParameter("Landroid/view/MotionEvent;", null, null)), + "Z", + AccessFlags.PUBLIC.value or AccessFlags.FINAL.value, + null, + null, + MutableMethodImplementation(2), + ).toMutable().apply { + returnEarly(false) + } + ) + } + } +} diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/misc/settings/SettingsPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/settings/SettingsPatch.kt index 1ffae4dbf1..4d7d2b334a 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/misc/settings/SettingsPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/settings/SettingsPatch.kt @@ -49,6 +49,7 @@ import app.morphe.patches.shared.misc.settings.preference.TextPreference import app.morphe.patches.shared.misc.settings.settingsPatch import app.morphe.patches.youtube.misc.extension.sharedExtensionPatch import app.morphe.patches.youtube.misc.fix.accessibility.fixShortsAccessibilityPatch +import app.morphe.patches.youtube.misc.fix.accessibility.fixTouchExplorationPatch import app.morphe.patches.youtube.misc.fix.contentprovider.fixContentProviderPatch import app.morphe.patches.youtube.misc.fix.likebutton.fixLikeButtonPatch import app.morphe.patches.youtube.misc.fix.padding.fixPagePaddingPatch @@ -222,6 +223,7 @@ val settingsPatch = bytecodePatch( // Currently there is no easy way to make patches mandatory, // so for now these are all dependents of this patch. fixShortsAccessibilityPatch, + fixTouchExplorationPatch, fixPlaybackSpeedWhilePlayingPatch, fixPreferenceIconPatch, fixLikeButtonPatch, From 16d4e602c7c8961b35bd737b5e5770312ef1781c Mon Sep 17 00:00:00 2001 From: 0xrxL <127248639+0xrxL@users.noreply.github.com> Date: Wed, 28 Oct 2026 12:13:38 +0100 Subject: [PATCH 136/189] fix(YouTube - Video quality): Quality selected for a regular video is lost when the video reloads --- .../playback/quality/RememberVideoQualityPatch.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/playback/quality/RememberVideoQualityPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/playback/quality/RememberVideoQualityPatch.java index 9b6c7fd1d6..32101775e4 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/playback/quality/RememberVideoQualityPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/playback/quality/RememberVideoQualityPatch.java @@ -57,7 +57,7 @@ public class RememberVideoQualityPatch { private static volatile int userSelectedQualityResolution = VideoInformation.AUTOMATIC_VIDEO_QUALITY_VALUE; /** - * If the quality selected by the user was selected in the Shorts quality menu. + * If the quality selected by the user was selected in the Shorts player. */ private static volatile boolean userSelectedQualityIsShorts; @@ -426,6 +426,9 @@ public static byte[] overrideClientAbrState(@Nullable MessageLite clientAbrState /** * Injection point. + *

    + * The quality menu of Shorts is also the quality menu of regular videos. + * * @param userSelectedQualityIndex Element index of {@link VideoInformation#getCurrentQualities()}. * @return The element index of the quality to set (see {@link #getPlayableQuality(VideoQualityInterface)}). */ @@ -435,7 +438,7 @@ public static int userChangedShortsQuality(int userSelectedQualityIndex) { if (currentQualities == null) { Logger.printDebug(() -> "Cannot save default quality, qualities is null"); // The resolution is not known, so a reload uses the quality saved by YouTube. - setUserSelectedQuality(VideoInformation.AUTOMATIC_VIDEO_QUALITY_VALUE, true); + setUserSelectedQuality(VideoInformation.AUTOMATIC_VIDEO_QUALITY_VALUE, ShortsPlayerState.isOpen()); clearShortsMenuSelectedQuality(); return userSelectedQualityIndex; } @@ -448,7 +451,7 @@ public static int userChangedShortsQuality(int userSelectedQualityIndex) { quality = selectedQuality; } final int resolution = quality.patch_getResolution(); - setUserSelectedQuality(resolution, true); + setUserSelectedQuality(resolution, ShortsPlayerState.isOpen()); if (resolution == VideoInformation.AUTOMATIC_VIDEO_QUALITY_VALUE) { // 'Auto' has no formats, so the menu sets it by its resolution. clearShortsMenuSelectedQuality(); From e1410e3189531c1fb4ab41c576c4fdd20aa89e36 Mon Sep 17 00:00:00 2001 From: ILoveOpenSourceApplications <117499019+ILoveOpenSourceApplications@users.noreply.github.com> Date: Wed, 28 Oct 2026 17:17:11 +0530 Subject: [PATCH 137/189] fix(YouTube Music - Change miniplayer color): Miniplayer color does not update after track changes --- .../patches/ChangeMiniplayerColorPatch.java | 21 ---------------- .../miniplayer/ChangeMiniplayerColorPatch.kt | 25 +++++++------------ .../music/layout/miniplayer/Fingerprints.kt | 7 ++++++ 3 files changed, 16 insertions(+), 37 deletions(-) diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/ChangeMiniplayerColorPatch.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/ChangeMiniplayerColorPatch.java index 28a3b416f8..f585005d2d 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/ChangeMiniplayerColorPatch.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/ChangeMiniplayerColorPatch.java @@ -20,13 +20,6 @@ public class ChangeMiniplayerColorPatch { @Nullable private static volatile Integer lastMiniplayerColor; - /** - * Baseline color from YTM's empty-state callback at app start; every later - * call carrying this value is treated as "back to empty". - */ - @Nullable - private static volatile Integer initialCapturedColor; - private static volatile WeakReference navigationBarRef = new WeakReference<>(null); @Nullable @@ -48,20 +41,6 @@ public static boolean changeMiniplayerColor() { public static void setLastMiniplayerColor(int color) { if (SystemClock.uptimeMillis() < dismissWindowUntilMs) return; - final Integer initial = initialCapturedColor; - if (initial == null) { - initialCapturedColor = color; - return; - } - - if (color == initial) { - if (lastMiniplayerColor == null) return; - lastMiniplayerColor = null; - final Integer defaultColor = defaultNavigationBarColor; - if (defaultColor != null) postNavigationBarColor(defaultColor); - return; - } - lastMiniplayerColor = color; if (Settings.CHANGE_NAVIGATION_BAR_COLOR.get()) postNavigationBarColor(color); } diff --git a/patches/src/main/kotlin/app/morphe/patches/music/layout/miniplayer/ChangeMiniplayerColorPatch.kt b/patches/src/main/kotlin/app/morphe/patches/music/layout/miniplayer/ChangeMiniplayerColorPatch.kt index cabfe0db7d..da9f58c24a 100644 --- a/patches/src/main/kotlin/app/morphe/patches/music/layout/miniplayer/ChangeMiniplayerColorPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/music/layout/miniplayer/ChangeMiniplayerColorPatch.kt @@ -10,6 +10,7 @@ package app.morphe.patches.music.layout.miniplayer import app.morphe.patcher.extensions.InstructionExtensions.addInstructions +import app.morphe.patcher.extensions.InstructionExtensions.addInstructionsWithLabels import app.morphe.patcher.extensions.InstructionExtensions.getInstruction import app.morphe.patcher.patch.bytecodePatch import app.morphe.patcher.util.findFreeRegister @@ -20,10 +21,8 @@ import app.morphe.patches.music.misc.settings.PreferenceScreen import app.morphe.patches.music.misc.settings.settingsPatch import app.morphe.patches.music.shared.Constants.COMPATIBILITY_YOUTUBE_MUSIC import app.morphe.patches.shared.misc.settings.preference.SwitchPreference -import app.morphe.util.addInstructionsAtControlFlowLabel import app.morphe.util.indexOfFirstInstructionOrThrow import app.morphe.util.indexOfFirstInstructionReversedOrThrow -import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction @@ -66,21 +65,18 @@ val changeMiniplayerColorPatch = bytecodePatch( val colorMathPlayerIPutReference = MiniPlayerConstructorFingerprint.method .getInstruction(iPutIndex).reference - MiniPlayerConstructorFingerprint.classDef.methods.single { method -> - method.accessFlags == AccessFlags.PUBLIC.value or AccessFlags.FINAL.value && - method.returnType == "V" && - method.parameters == it.originalMethod.parameters - }.apply { - val insertIndex = indexOfFirstInstructionReversedOrThrow(Opcode.INVOKE_DIRECT) + SwitchToggleColorParentFingerprint.method.apply { + val insertIndex = 0 val freeRegister = findFreeRegister(insertIndex) // Publish the resolved color so the navigation bar can reuse it. - addInstructionsAtControlFlowLabel( + addInstructionsWithLabels( insertIndex, """ invoke-static {}, $EXTENSION_CLASS->changeMiniplayerColor()Z move-result v$freeRegister if-eqz v$freeRegister, :off + if-eqz p1, :off iget-object v$freeRegister, p1, $colorMathPlayerFieldReference check-cast v$freeRegister, ${colorMathPlayerIGetReference.definingClass} iget v$freeRegister, v$freeRegister, $colorMathPlayerIGetReference @@ -109,21 +105,18 @@ val changeMiniplayerColorPatch = bytecodePatch( val colorMathPlayerIPutReference = MiniPlayerConstructorFingerprint.method .getInstruction(iPutIndex).reference - MiniPlayerConstructorFingerprint.classDef.methods.single { method -> - method.accessFlags == AccessFlags.PUBLIC.value or AccessFlags.FINAL.value && - method.returnType == "V" && - method.parameters == it.originalMethod.parameters - }.apply { - val insertIndex = indexOfFirstInstructionReversedOrThrow(Opcode.INVOKE_DIRECT) + SwitchToggleColorParentFingerprint.method.apply { + val insertIndex = 0 val freeRegister = findFreeRegister(insertIndex) // Publish the resolved color so the navigation bar can reuse it. - addInstructionsAtControlFlowLabel( + addInstructionsWithLabels( insertIndex, """ invoke-static {}, $EXTENSION_CLASS->changeMiniplayerColor()Z move-result v$freeRegister if-eqz v$freeRegister, :off + if-eqz p1, :off invoke-virtual { p1 }, $colorMathPlayerInvokeVirtualReference move-result-object v$freeRegister check-cast v$freeRegister, ${colorMathPlayerIGetReference.definingClass} diff --git a/patches/src/main/kotlin/app/morphe/patches/music/layout/miniplayer/Fingerprints.kt b/patches/src/main/kotlin/app/morphe/patches/music/layout/miniplayer/Fingerprints.kt index 06f58652a9..e3210f6f6d 100644 --- a/patches/src/main/kotlin/app/morphe/patches/music/layout/miniplayer/Fingerprints.kt +++ b/patches/src/main/kotlin/app/morphe/patches/music/layout/miniplayer/Fingerprints.kt @@ -58,6 +58,13 @@ internal object NavigationBarTabLayoutFingerprint : Fingerprint( ) ) +internal object SwitchToggleColorParentFingerprint : Fingerprint( + classFingerprint = MiniPlayerConstructorFingerprint, + accessFlags = listOf(AccessFlags.PUBLIC, AccessFlags.FINAL), + returnType = "V", + parameters = listOf("L", "J") +) + internal object SwitchToggleColorFingerprint : Fingerprint( classFingerprint = MiniPlayerConstructorFingerprint, accessFlags = listOf(AccessFlags.PRIVATE, AccessFlags.FINAL), From b22841c5a6ddf6ff38b49f262ca5aa893348a600 Mon Sep 17 00:00:00 2001 From: pharmacomaniac Date: Wed, 28 Oct 2026 11:10:28 -0400 Subject: [PATCH 138/189] fix(YouTube): Fix TalkBack not reading captions (#4136) --- .../misc/fix/accessibility/Fingerprints.kt | 12 +++++++ .../FixCaptionsAccessibilityPatch.kt | 35 +++++++++++++++++++ .../youtube/misc/settings/SettingsPatch.kt | 2 ++ 3 files changed, 49 insertions(+) create mode 100644 patches/src/main/kotlin/app/morphe/patches/youtube/misc/fix/accessibility/FixCaptionsAccessibilityPatch.kt diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/misc/fix/accessibility/Fingerprints.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/fix/accessibility/Fingerprints.kt index c489ca7bc8..c279a0dcc1 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/misc/fix/accessibility/Fingerprints.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/fix/accessibility/Fingerprints.kt @@ -6,11 +6,23 @@ import app.morphe.patcher.InstructionLocation.MatchAfterWithin import app.morphe.patcher.literal import app.morphe.patcher.methodCall import app.morphe.patcher.opcode +import app.morphe.patcher.resource.ResourceType +import app.morphe.patcher.resourceLiteral import com.android.tools.smali.dexlib2.Opcode private const val BOTTOM_UI_CONTAINER = "Lcom/google/android/apps/youtube/app/common/ui/bottomui/BottomUiContainer;" +internal object CaptionTextViewConstructorFingerprint : Fingerprint( + superclass = "Landroid/view/View;", + name = "", + parameters = listOf("Landroid/content/Context;"), + filters = listOf( + resourceLiteral(ResourceType.ID, "subtitle_window_identifier"), + opcode(Opcode.RETURN_VOID) + ) +) + internal object TouchDelegateConstructorFingerprint : Fingerprint( superclass = "Landroid/view/TouchDelegate;", name = "", diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/misc/fix/accessibility/FixCaptionsAccessibilityPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/fix/accessibility/FixCaptionsAccessibilityPatch.kt new file mode 100644 index 0000000000..07a9ebcf2a --- /dev/null +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/fix/accessibility/FixCaptionsAccessibilityPatch.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/4136 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.patches.youtube.misc.fix.accessibility + +import app.morphe.patcher.extensions.InstructionExtensions.addInstructions +import app.morphe.patcher.patch.bytecodePatch +import app.morphe.patches.youtube.misc.playservice.is_21_23_or_greater +import app.morphe.patches.youtube.misc.playservice.versionCheckPatch + +internal val fixCaptionsAccessibilityPatch = bytecodePatch( + description = "Fixes captions not being announced by screen readers." +) { + dependsOn(versionCheckPatch) + + execute { + if (is_21_23_or_greater) return@execute + + // YouTube 21.23.492 uses ACCESSIBILITY_LIVE_REGION_POLITE to announce caption updates. + // Backport it so TalkBack can read changing captions without accessibility focus (#3095). + CaptionTextViewConstructorFingerprint.let { + it.method.addInstructions( + it.instructionMatches.last().index, + """ + const/4 v0, 0x1 + invoke-virtual { p0, v0 }, Landroid/view/View;->setAccessibilityLiveRegion(I)V + """ + ) + } + } +} diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/misc/settings/SettingsPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/settings/SettingsPatch.kt index 4d7d2b334a..b33c760520 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/misc/settings/SettingsPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/settings/SettingsPatch.kt @@ -48,6 +48,7 @@ import app.morphe.patches.shared.misc.settings.preference.SwitchPreference import app.morphe.patches.shared.misc.settings.preference.TextPreference import app.morphe.patches.shared.misc.settings.settingsPatch import app.morphe.patches.youtube.misc.extension.sharedExtensionPatch +import app.morphe.patches.youtube.misc.fix.accessibility.fixCaptionsAccessibilityPatch import app.morphe.patches.youtube.misc.fix.accessibility.fixShortsAccessibilityPatch import app.morphe.patches.youtube.misc.fix.accessibility.fixTouchExplorationPatch import app.morphe.patches.youtube.misc.fix.contentprovider.fixContentProviderPatch @@ -222,6 +223,7 @@ val settingsPatch = bytecodePatch( versionCheckPatch, // Currently there is no easy way to make patches mandatory, // so for now these are all dependents of this patch. + fixCaptionsAccessibilityPatch, fixShortsAccessibilityPatch, fixTouchExplorationPatch, fixPlaybackSpeedWhilePlayingPatch, From 89defd5d039e9dbcc139866cb5941cb3b592559d Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Wed, 28 Oct 2026 15:13:53 +0000 Subject: [PATCH 139/189] bump(YouTube): Add support for `21.30.216` --- .../youtube/layout/miniplayer/MiniplayerPatch.kt | 2 +- .../morphe/patches/youtube/shared/Constants.kt | 15 +++++---------- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/miniplayer/MiniplayerPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/miniplayer/MiniplayerPatch.kt index cdc04509fb..92abc34cb3 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/miniplayer/MiniplayerPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/miniplayer/MiniplayerPatch.kt @@ -80,7 +80,7 @@ internal const val MINIMAL_BOUNDS_INTERFACE = val miniplayerPatch = bytecodePatch( name = "Miniplayer", description = "Adds options to change the in-app minimized player. " + - "Patching 21.28.206 and lower has more miniplayer types to choose from." + "Patching 21.28.208 and lower has more miniplayer types to choose from." ) { dependsOn( sharedExtensionPatch, diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/shared/Constants.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/shared/Constants.kt index 17ca15fc4a..41cd9a3fa8 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/shared/Constants.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/shared/Constants.kt @@ -28,20 +28,15 @@ internal object Constants { isExperimental = true ), AppTarget( - version = "21.28.208", // Last version with different miniplayers. - minSdk = 29, - isExperimental = true - ), - AppTarget( - version = "21.23.492", // Last version for Android 9.0 - minSdk = 28 + version = "21.30.216", + minSdk = 29 ), AppTarget( - version = "21.20.405", - minSdk = 28 + version = "21.28.208", // Last version with different miniplayers. + minSdk = 29 ), AppTarget( - version = "21.16.256", + version = "21.23.492", // Last version for Android 9.0 minSdk = 28 ), AppTarget( From a325744ef3434bf0fe40525d4e2da9b696a2f2cb Mon Sep 17 00:00:00 2001 From: 0xrxL <127248639+0xrxL@users.noreply.github.com> Date: Wed, 28 Oct 2026 22:18:19 +0100 Subject: [PATCH 140/189] fix(YouTube - Video quality): Short loop restarts with the previous quality when the quality is selected in its first segment --- .../quality/RememberVideoQualityPatch.java | 44 ++++++++++++++++ .../youtube/video/quality/Fingerprints.kt | 37 +++++++++++++ .../quality/RememberVideoQualityPatch.kt | 52 ++++++++++++++++--- 3 files changed, 127 insertions(+), 6 deletions(-) diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/playback/quality/RememberVideoQualityPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/playback/quality/RememberVideoQualityPatch.java index 32101775e4..f843e8bb82 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/playback/quality/RememberVideoQualityPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/playback/quality/RememberVideoQualityPatch.java @@ -73,6 +73,12 @@ public class RememberVideoQualityPatch { */ private static volatile long clearVideoBufferTimeUs = -1; + /** + * ID of the video whose buffered segment that was playing when the user selected the quality + * is cleared once it was played, or an empty string. + */ + private static volatile String clearPlayedVideoSegmentVideoId = ""; + /** * The quality selected in the Shorts quality menu, until the menu sets it. */ @@ -619,6 +625,7 @@ public static boolean clearVideoBufferOnQualityChange(boolean original) { * when the audio track changes, so the new quality is played after it without a gap. * The video buffered before that segment is cleared too, so a Short that loops * does not play the previous quality from its start. + * The segment itself is cleared once it was played (see {@link #shouldClearPlayedVideoSegment()}). * * @return The playback time in microseconds after and before whose segment the buffered video * is cleared, or -1 to clear the whole buffered video. @@ -626,6 +633,43 @@ public static boolean clearVideoBufferOnQualityChange(boolean original) { public static long getClearVideoBufferTime() { final long timeUs = clearVideoBufferTimeUs; clearVideoBufferTimeUs = -1; + if (timeUs >= 0) { + clearPlayedVideoSegmentVideoId = VideoInformation.getVideoId(); + } return timeUs; } + + /** + * Injection point. + *

    + * The segment that was playing when the user selected the quality keeps the previous quality. + * A Short is often buffered entirely, so it plays that segment again when it loops, + * such as its first segment if the quality was selected right after the Short started. + * While this returns true, the player clears the video buffered before the playback time, + * which it does only once the playback time is after the first segment buffered. + * + * @return If the video buffered before the playback time is cleared. + */ + public static boolean shouldClearPlayedVideoSegment() { + final String videoId = clearPlayedVideoSegmentVideoId; + if (videoId.isEmpty()) { + return false; + } + if (!videoId.equals(VideoInformation.getVideoId())) { + // Another video started before the segment was played. + clearPlayedVideoSegmentVideoId = ""; + return false; + } + return true; + } + + /** + * Injection point. + *

    + * Called when the segment that was playing when the user selected the quality was cleared. + */ + public static void onPlayedVideoSegmentCleared() { + Logger.printDebug(() -> "Cleared the video segment buffered with the previous quality"); + clearPlayedVideoSegmentVideoId = ""; + } } diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/video/quality/Fingerprints.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/video/quality/Fingerprints.kt index 0c376f1d40..383993c5e9 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/video/quality/Fingerprints.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/video/quality/Fingerprints.kt @@ -291,6 +291,43 @@ internal object SampleHolderDiscardBufferBeforeSegmentFingerprint : Fingerprint( ) ) +/** + * The method of the buffered media of a track that saves the end time of its first segment. + * The media before a time is cleared only if the time is after it. + */ +internal object MediaSegmentHolderSetFirstSegmentEndTimeFingerprint : Fingerprint( + definingClass = "Lcom/google/android/libraries/youtube/media/player/exo2/platypus/mediapush/MediaSegmentHolder;", + returnType = "V", + parameters = listOf(), + filters = listOf( + methodCall(smali = "Ljava/util/List;->isEmpty()Z"), + methodCall( + definingClass = "Lcom/google/android/libraries/youtube/media/player/exo2/platypus/mediapush/BufferedMediaSegment;", + returnType = "J", + parameters = listOf() + ), + fieldAccess(opcode = Opcode.IPUT_WIDE, definingClass = "this", type = "J") + ) +) + +/** + * The method of the media source that clears the media buffered before the playback time, + * called by the player while the video is loaded. + */ +internal object MediaPushMediaSourceDiscardBufferFingerprint : Fingerprint( + definingClass = "Lcom/google/android/libraries/youtube/media/player/exo2/platypus/mediapush/MediaPushMediaSource;", + returnType = "V", + parameters = listOf("J"), + filters = listOf( + fieldAccess( + opcode = Opcode.SGET_WIDE, + definingClass = "Lcom/google/android/libraries/youtube/media/player/exo2/platypus/live/LiveTimeline;" + ), + fieldAccess(opcode = Opcode.IGET_OBJECT, definingClass = "this", type = BUFFER_MANAGER_CLASS), + methodCall(smali = "Ljava/lang/Math;->max(JJ)J") + ) +) + internal fun getCurrentVideoFormatConstructorFingerprint( videoQualityArray: String ) = object : Fingerprint( diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/video/quality/RememberVideoQualityPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/video/quality/RememberVideoQualityPatch.kt index f1fdcf70f5..da2199a732 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/video/quality/RememberVideoQualityPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/video/quality/RememberVideoQualityPatch.kt @@ -441,6 +441,9 @@ val rememberVideoQualityPatch = bytecodePatch { // The video buffered before the segment that is playing is cleared too, // otherwise a Short plays the previous quality from its start when it loops. // The method of the player is not public before 21.26, so a public method calls it. + // It returns if media was cleared: the player changes the end time of the first segment. + val firstSegmentEndTimeField = MediaSegmentHolderSetFirstSegmentEndTimeFingerprint + .instructionMatches.last().getFieldAccessed() val sampleHolderDiscardBufferMethod = if (is_21_43_or_greater) { SampleHolderDiscardBufferBeforeSegmentFingerprint } else { @@ -450,30 +453,39 @@ val rememberVideoQualityPatch = bytecodePatch { it.classDef.type, "patch_discardBufferBeforeSegment", listOf(ImmutableMethodParameter("J", null, null)), - "V", + "Z", AccessFlags.PUBLIC.value or AccessFlags.FINAL.value, null, null, - MutableMethodImplementation(10), + MutableMethodImplementation(12), ).toMutable().apply { - addInstructions( + addInstructionsWithLabels( 0, if (is_21_43_or_greater) { // No media is kept before the playback time. """ + iget-wide v7, p0, $firstSegmentEndTimeField move-object v0, p0 move-wide v1, p1 move-wide v3, p1 const-wide/16 v5, 0x0 invoke-virtual/range { v0 .. v6 }, ${it.method} - return-void """ } else { """ + iget-wide v7, p0, $firstSegmentEndTimeField invoke-virtual { p0, p1, p2 }, ${it.method} - return-void """ - } + } + """ + iget-wide v0, p0, $firstSegmentEndTimeField + cmp-long v0, v0, v7 + if-eqz v0, :not_cleared + const/4 v0, 0x1 + return v0 + :not_cleared + const/4 v0, 0x0 + return v0 + """ ) } it.classDef.methods.add(discardBufferBeforeSegmentMethod) @@ -508,6 +520,34 @@ val rememberVideoQualityPatch = bytecodePatch { """ ) } + + // The segment that was playing when the quality was selected keeps the previous quality, + // and a Short plays it again when it loops, such as its first segment. + // It is cleared once it was played: the player then clears the video buffered + // before the playback time, as when the quality was selected. + MediaPushMediaSourceDiscardBufferFingerprint.let { + val bufferManagerField = it.instructionMatches[1].getFieldAccessed() + + it.method.addInstructionsWithLabels( + 0, + """ + invoke-static { }, $EXTENSION_CLASS->shouldClearPlayedVideoSegment()Z + move-result v0 + if-eqz v0, :keep_buffer + iget-object v0, p0, $bufferManagerField + sget-object v1, $videoTrackTypeField + invoke-virtual { v0, v1 }, $getMediaSegmentHolderMethod + move-result-object v0 + check-cast v0, $SAMPLE_HOLDER_CLASS + invoke-virtual { v0, p1, p2 }, $sampleHolderDiscardBufferMethod + move-result v0 + if-eqz v0, :keep_buffer + invoke-static { }, $EXTENSION_CLASS->onPlayedVideoSegmentCleared()V + :keep_buffer + nop + """ + ) + } } } } From 36e3612c0bfa7435e47fa9529b12a819b11ac843 Mon Sep 17 00:00:00 2001 From: 0xrxL <127248639+0xrxL@users.noreply.github.com> Date: Wed, 28 Oct 2026 22:28:08 +0100 Subject: [PATCH 141/189] fix(YouTube - Video quality): Buffered video is cleared with the placeholder playback times of live streams after selecting a quality --- .../quality/RememberVideoQualityPatch.kt | 50 +++++++++++-------- 1 file changed, 30 insertions(+), 20 deletions(-) diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/video/quality/RememberVideoQualityPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/video/quality/RememberVideoQualityPatch.kt index da2199a732..cdb7ee4c15 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/video/quality/RememberVideoQualityPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/video/quality/RememberVideoQualityPatch.kt @@ -525,28 +525,38 @@ val rememberVideoQualityPatch = bytecodePatch { // and a Short plays it again when it loops, such as its first segment. // It is cleared once it was played: the player then clears the video buffered // before the playback time, as when the quality was selected. + // The code is added after the checks of live streams, so the buffered video is not + // cleared with the playback times the player does not clear the buffered media with. MediaPushMediaSourceDiscardBufferFingerprint.let { - val bufferManagerField = it.instructionMatches[1].getFieldAccessed() + val bufferManagerMatch = it.instructionMatches[1] + val bufferManagerField = bufferManagerMatch.getFieldAccessed() + val insertIndex = bufferManagerMatch.index - it.method.addInstructionsWithLabels( - 0, - """ - invoke-static { }, $EXTENSION_CLASS->shouldClearPlayedVideoSegment()Z - move-result v0 - if-eqz v0, :keep_buffer - iget-object v0, p0, $bufferManagerField - sget-object v1, $videoTrackTypeField - invoke-virtual { v0, v1 }, $getMediaSegmentHolderMethod - move-result-object v0 - check-cast v0, $SAMPLE_HOLDER_CLASS - invoke-virtual { v0, p1, p2 }, $sampleHolderDiscardBufferMethod - move-result v0 - if-eqz v0, :keep_buffer - invoke-static { }, $EXTENSION_CLASS->onPlayedVideoSegmentCleared()V - :keep_buffer - nop - """ - ) + it.method.apply { + val registerProvider = getFreeRegisterProvider(insertIndex, 2) + val sampleHolderRegister = registerProvider.getFreeRegister4Bit() + val trackTypeRegister = registerProvider.getFreeRegister4Bit() + + addInstructionsAtControlFlowLabel( + insertIndex, + """ + invoke-static { }, $EXTENSION_CLASS->shouldClearPlayedVideoSegment()Z + move-result v$sampleHolderRegister + if-eqz v$sampleHolderRegister, :keep_buffer + iget-object v$sampleHolderRegister, p0, $bufferManagerField + sget-object v$trackTypeRegister, $videoTrackTypeField + invoke-virtual { v$sampleHolderRegister, v$trackTypeRegister }, $getMediaSegmentHolderMethod + move-result-object v$sampleHolderRegister + check-cast v$sampleHolderRegister, $SAMPLE_HOLDER_CLASS + invoke-virtual { v$sampleHolderRegister, p1, p2 }, $sampleHolderDiscardBufferMethod + move-result v$sampleHolderRegister + if-eqz v$sampleHolderRegister, :keep_buffer + invoke-static { }, $EXTENSION_CLASS->onPlayedVideoSegmentCleared()V + :keep_buffer + nop + """ + ) + } } } } From e5cd7523ba2963b29fff6c3169cc71e1a58f405d Mon Sep 17 00:00:00 2001 From: 0xrxL <127248639+0xrxL@users.noreply.github.com> Date: Wed, 28 Oct 2026 22:44:22 +0100 Subject: [PATCH 142/189] fix(YouTube - Video quality): Regular video returns to the default quality when it resumes from the background --- .../quality/RememberVideoQualityPatch.java | 52 +++++++++++++++---- .../youtube/video/quality/Fingerprints.kt | 19 +++++++ .../quality/RememberVideoQualityPatch.kt | 17 ++++++ 3 files changed, 79 insertions(+), 9 deletions(-) diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/playback/quality/RememberVideoQualityPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/playback/quality/RememberVideoQualityPatch.java index f843e8bb82..43f1615550 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/playback/quality/RememberVideoQualityPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/playback/quality/RememberVideoQualityPatch.java @@ -215,6 +215,24 @@ private static boolean isQualitySelectedByUser() { return !videoId.isEmpty() && videoId.equals(userSelectedQualityVideoId); } + /** + * YouTube can load the current video again, such as after the user selects a quality + * that needs another codec, or when the app returns from the background. + * The video ID of the player is updated only once the video starts, so the selected + * quality is not used once the player response of another video is received, + * such as the next video of the autoplay. + * The Shorts player starts the next Short before its player response is received, + * so a quality selected in the Shorts quality menu is not used, otherwise the next Short + * would start with it. The Short it was selected for keeps it with the formats saved + * with the quality last selected (see {@link #useSelectedQualityFormats(boolean)}). + * + * @return If the quality selected by the user is used when the video loads again. + */ + private static boolean useUserSelectedQualityOnReload() { + return isQualitySelectedByUser() && !userSelectedQualityIsShorts + && VideoInformation.getPlayerResponseVideoId().equals(VideoInformation.getVideoId()); + } + public static boolean shouldRememberVideoQuality() { BooleanSetting preference = ShortsPlayerState.isOpen() ? Settings.REMEMBER_SHORTS_QUALITY_LAST_SELECTED @@ -292,15 +310,7 @@ public static Optional getInitialVideoQuality(Optional optional) { // YouTube can reload the video after the user selects a quality, such as when // the selected quality needs another codec. The reload has no initial quality, // or a copy of the default quality used when the video started. - // The video ID of the player is updated only once the video starts, so the selected - // quality is not used once the player response of another video is received, - // such as the next video of the autoplay. - // The Shorts player starts the next Short before its player response is received, - // so a quality selected in the Shorts quality menu is not used, otherwise the next Short - // would start with it. The Short it was selected for keeps it with the formats saved - // with the quality last selected (see useSelectedQualityFormats). - if (isQualitySelectedByUser() && !userSelectedQualityIsShorts - && VideoInformation.getPlayerResponseVideoId().equals(VideoInformation.getVideoId())) { + if (useUserSelectedQualityOnReload()) { final int selectedResolution = userSelectedQualityResolution; Logger.printDebug(() -> "initialVideoQuality selected by the user: " + selectedResolution); // With 'Auto' selected, YouTube chooses the quality. @@ -317,6 +327,30 @@ public static Optional getInitialVideoQuality(Optional optional) { return optional; } + /** + * Injection point. + *

    + * The player loads the video again with the playback start parameters it was started with, + * such as when the app returns from the background with background playback off. + * Their initial video quality is the one used when the video started + * (see {@link #getInitialVideoQuality(Optional)}), so the quality selected by the user + * for the video is used instead. Nothing is changed in the Shorts player. + * + * @param resolution Initial video quality of the playback start parameters, or null. + * @return The initial video quality to use, or null if YouTube chooses the quality. + */ + @Nullable + public static Integer getPlaybackStartVideoQuality(@Nullable Integer resolution) { + if (ShortsPlayerState.isOpen() || !useUserSelectedQualityOnReload()) { + return resolution; + } + + final int selectedResolution = userSelectedQualityResolution; + Logger.printDebug(() -> "Playback start video quality selected by the user: " + selectedResolution); + // With 'Auto' selected, YouTube chooses the quality. + return selectedResolution > 0 ? selectedResolution : null; + } + /** * Injection point. *

    diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/video/quality/Fingerprints.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/video/quality/Fingerprints.kt index 383993c5e9..9831a505e2 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/video/quality/Fingerprints.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/video/quality/Fingerprints.kt @@ -394,6 +394,25 @@ internal fun getPlaybackStartParametersConstructorFingerprint( ) ) {} +/** + * The method that returns the initial video quality of the playback start parameters, + * used by the player each time it loads the video, such as when the app returns from the background. + */ +internal fun getPlaybackStartParametersInitialResolutionFingerprint( + initialResolutionField: FieldReference +) = object : Fingerprint( + returnType = "Ljava/lang/Integer;", + parameters = listOf(), + filters = listOf( + fieldAccess( + opcode = Opcode.IGET_OBJECT, + reference = initialResolutionField + ), + methodCall(smali = "Lj\$/util/Optional;->orElse(Ljava/lang/Object;)Ljava/lang/Object;"), + checkCast("Ljava/lang/Integer;") + ) +) {} + private object VideoQualityItemOnClickParentFingerprint : Fingerprint( returnType = "V", filters = listOf( diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/video/quality/RememberVideoQualityPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/video/quality/RememberVideoQualityPatch.kt index cdb7ee4c15..b2ad284820 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/video/quality/RememberVideoQualityPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/video/quality/RememberVideoQualityPatch.kt @@ -122,6 +122,23 @@ val rememberVideoQualityPatch = bytecodePatch { } } + // The player loads the video again with the playback start parameters it was started with, + // such as when the app returns from the background with background playback off, + // so the quality selected by the user for the video is used instead of the initial quality. + getPlaybackStartParametersInitialResolutionFingerprint(initialResolutionField).matchSingle().method.apply { + findInstructionIndicesReversedOrThrow(Opcode.RETURN_OBJECT).forEach { index -> + val register = getInstruction(index).registerA + + addInstructionsAtControlFlowLabel( + index, + """ + invoke-static/range { v$register .. v$register }, $EXTENSION_CLASS->getPlaybackStartVideoQuality(Ljava/lang/Integer;)Ljava/lang/Integer; + move-result-object v$register + """ + ) + } + } + // Turn off the YouTube automatic quality selection when the video starts, // so the initial video quality is used as a fixed quality. InitialVideoQualityFeatureFlagFingerprint.matchAll().forEach { From 26715b1723f5755dc13a64c8b41be4a499f4f2cb Mon Sep 17 00:00:00 2001 From: 0xrxL <127248639+0xrxL@users.noreply.github.com> Date: Wed, 28 Oct 2026 23:14:17 +0100 Subject: [PATCH 143/189] fix(YouTube - Feed view count): View count badge of a card that enters from the bottom loads only when its title row is shown --- .../youtube/patches/FeedViewCountPatch.java | 113 +++++++++++++++++- .../patches/utils/FeedVideoCardsPatch.java | 38 ++++++ 2 files changed, 150 insertions(+), 1 deletion(-) diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FeedViewCountPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FeedViewCountPatch.java index 5fafb6f836..40c884f4c3 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FeedViewCountPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FeedViewCountPatch.java @@ -27,6 +27,7 @@ import android.widget.ImageView; import android.widget.TextView; +import androidx.annotation.GuardedBy; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -35,6 +36,7 @@ import java.lang.ref.WeakReference; import java.util.ArrayList; +import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; @@ -47,9 +49,11 @@ import java.util.regex.Pattern; import app.morphe.extension.shared.Logger; +import app.morphe.extension.shared.Utils; import app.morphe.extension.shared.patches.components.ContextInterface; import app.morphe.extension.youtube.patches.components.LayoutComponentsFilter; import app.morphe.extension.youtube.patches.utils.FeedVideoCardsPatch; +import app.morphe.extension.youtube.patches.utils.ProtoNode; import app.morphe.extension.youtube.settings.Settings; /** @@ -149,6 +153,28 @@ public final class FeedViewCountPatch { /** Root hosts that show a view count. Used only on the main thread. */ private static final Map badges = new WeakHashMap<>(); + /** Spaces, which texts of the same view count can show differently, such as "4.7 M" and "4.7M". */ + private static final Pattern SPACES_PATTERN = Pattern.compile("[\\s  ]+"); + + /** Number of videos whose view count is kept. */ + private static final int MAX_VIDEO_VIEW_COUNTS = 200; + + /** + * View count of the element of a video, and the text of the duration badge on its thumbnail, + * such as "12:34". The badge of a card must show the same text, so the view count of + * another video is not shown if the card is matched to the wrong video. + */ + private record ElementViewCount(String viewCount, String badgeText) { + } + + /** + * Video id -> view count of the element of the video, used while the texts below the title + * of its card are not drawn yet. Oldest videos are removed first. + */ + @GuardedBy("itself") + private static final Map videoViewCounts = Utils + .createSizeRestrictedMap(MAX_VIDEO_VIEW_COUNTS, true); + private FeedViewCountPatch() { } @@ -600,6 +626,68 @@ public static SpannableString onFeedSubtitleCreated(SpannableString text) { return text; } + /** + * Called by {@link FeedVideoCardsPatch} when the element of a video that is not live is loaded, + * before its cards are drawn. Litho draws only the part of a card that is shown, so a card that + * enters from the bottom of the list shows its thumbnail before the texts below its title. + * The view count of the element is kept, so the badge shows it as soon as the thumbnail is shown. + *

    + * Only the text shown below the title is used, such as "Channel 4.7M 2 days ago": + * the text with a part that the element also has as a text of its own, such as "4.7M". + * Usually called off the main thread. + */ + public static void onVideoElementLoaded(String videoId, List textNodes) { + if (!ENABLED) { + return; + } + try { + Set texts = new HashSet<>(); + String badgeText = null; + for (ProtoNode node : textNodes) { + String text = node.getText(); + texts.add(SPACES_PATTERN.matcher(text).replaceAll("")); + if (badgeText == null && FeedVideoCardsPatch.isVideoBadgeText(text.trim())) { + badgeText = text.trim(); + } + } + if (badgeText == null) { + return; + } + Locale locale = Locale.getDefault(); + for (ProtoNode node : textNodes) { + String text = node.getText(); + if (text.length() > MAX_SUBTITLE_LENGTH) { + continue; + } + List segments = splitSegments(text); + if (segments.size() < 2) { + continue; + } + boolean hasCountText = false; + for (Segment segment : segments) { + if (segment.hasNumber() + && texts.contains(SPACES_PATTERN.matcher(segment.text).replaceAll(""))) { + hasCountText = true; + break; + } + } + if (!hasCountText) { + continue; + } + ViewCountMatch match = findViewCount(text, locale); + if (match != null + && texts.contains(SPACES_PATTERN.matcher(match.parsed.count).replaceAll(""))) { + synchronized (videoViewCounts) { + videoViewCounts.put(videoId, new ElementViewCount(match.parsed.count, badgeText)); + } + return; + } + } + } catch (Exception ex) { + Logger.printException(() -> "onVideoElementLoaded failure", ex); + } + } + /** * Injection point. *

    @@ -655,6 +743,9 @@ private static final class CardInfo { Rect duration; /** If the card shows a duration or a live badge, which videos do. */ boolean hasDuration; + /** Text of the first duration or live badge found, such as "12:34". */ + @Nullable + String badgeText; /** * The thumbnail, if a host of its size shows the duration, or else its image. * Cards of some layouts show it inset from their edges. @@ -728,10 +819,14 @@ private static void findCardInfo(ComponentHost root, View view, CardInfo info, R root.offsetDescendantRectToMyCoords(host, rect); } info.addText(text, rect); - if (!FeedVideoCardsPatch.isVideoBadgeText(text.toString().trim())) { + String badgeText = text.toString().trim(); + if (!FeedVideoCardsPatch.isVideoBadgeText(badgeText)) { continue; } info.hasDuration = true; + if (info.badgeText == null) { + info.badgeText = badgeText; + } if (info.duration == null) { if (isDurationBadgeShaped(root, rect)) { info.duration = new Rect(rect); @@ -943,6 +1038,22 @@ public static void onCardLaidOut(ComponentHost host, @Nullable String videoId) { } viewCount = findViewCountInTexts(info.texts); } + if (viewCount == null) { + // The texts below the title are not drawn yet, + // such as while only the thumbnail of a card that enters the list is shown. + String cardVideoId = videoId != null + ? videoId + : FeedVideoCardsPatch.findVideoIdByDescription(host); + if (cardVideoId != null && info.badgeText != null) { + ElementViewCount element; + synchronized (videoViewCounts) { + element = videoViewCounts.get(cardVideoId); + } + if (element != null && element.badgeText.equals(info.badgeText)) { + viewCount = element.viewCount; + } + } + } final String id = videoId == null ? "unknown" : videoId; if (viewCount == null) { // The texts can be drawn after the layout of the card. diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/FeedVideoCardsPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/FeedVideoCardsPatch.java index 2e61cf3403..2b59d4d394 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/FeedVideoCardsPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/FeedVideoCardsPatch.java @@ -114,6 +114,12 @@ public final class FeedVideoCardsPatch { /** Value of a text shown by more than one video. */ private static final String AMBIGUOUS = ""; + /** + * Delimiter of the parts of the accessibility description of a video card, + * such as "Title - 12 minutes - Channel - 4.7M views - 2 days ago - play video". + */ + private static final String DESCRIPTION_DELIMITER = " - "; + /** * Result for a card without a duration on its thumbnail, such as a mix or a playlist, * which shows the thumbnail of its first video. Nothing is drawn on these cards. @@ -359,6 +365,9 @@ public static byte[] onElementLoaded(byte[] bytes) { for (ProtoNode node : textNodes) { addVideoText(videoId, node.getText()); } + if (!live) { + FeedViewCountPatch.onVideoElementLoaded(videoId, textNodes); + } } catch (Exception ex) { Logger.printException(() -> "onElementLoaded failure", ex); } @@ -673,6 +682,35 @@ public static String findVideoId(ComponentHost root) { return findBestMatch(scores); } + /** + * Litho draws only the part of a card that is shown, so a card that enters from the bottom + * of the list shows its thumbnail before its texts, and its video is not known by its texts. + * The accessibility description of the card is set already, and starts with the title, + * such as "Title - 12 minutes - Channel - 4.7M views - 2 days ago - play video". + * + * @return The video of the card found by the start of its description, or null if not known. + */ + @Nullable + public static String findVideoIdByDescription(ComponentHost root) { + CharSequence description = root.getContentDescription(); + if (description == null) { + return null; + } + String text = description.toString(); + synchronized (textVideoIds) { + // A title can contain the delimiter too, so the longest start of the description + // that is a text of a video is used. + for (int end = text.lastIndexOf(DESCRIPTION_DELIMITER); end > 0; + end = text.lastIndexOf(DESCRIPTION_DELIMITER, end - 1)) { + String videoId = textVideoIds.get(normalizeText(text.substring(0, end))); + if (videoId != null) { + return videoId.equals(AMBIGUOUS) ? null : videoId; + } + } + } + return null; + } + /** * @param scores Match -> score of the texts that belong to it, where a long text scores 2. * @return The match with the highest score, or null if there is a tie From 43a9e4c8935627af8f1748071438c25b653deefb Mon Sep 17 00:00:00 2001 From: 0xrxL <127248639+0xrxL@users.noreply.github.com> Date: Wed, 28 Oct 2026 23:39:17 +0100 Subject: [PATCH 144/189] fix(YouTube - Ambient mode): Ambient mode is replaced by a black background with the new watch page experiment --- .../youtube/layout/hide/ambientmode/AmbientModePatch.kt | 9 +++++++++ .../youtube/layout/hide/ambientmode/Fingerprints.kt | 9 +++++++++ 2 files changed, 18 insertions(+) diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/ambientmode/AmbientModePatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/ambientmode/AmbientModePatch.kt index 72d04678a2..05c0862941 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/ambientmode/AmbientModePatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/ambientmode/AmbientModePatch.kt @@ -28,6 +28,7 @@ import app.morphe.patches.shared.misc.settings.preference.SwitchPreference import app.morphe.patches.youtube.misc.extension.sharedExtensionPatch import app.morphe.patches.youtube.misc.playservice.is_21_02_or_greater import app.morphe.patches.youtube.misc.playservice.is_21_03_or_greater +import app.morphe.patches.youtube.misc.playservice.is_21_12_or_greater import app.morphe.patches.youtube.misc.playservice.is_21_43_or_greater import app.morphe.patches.youtube.misc.playservice.versionCheckPatch import app.morphe.patches.youtube.misc.settings.PreferenceScreen @@ -36,6 +37,7 @@ import app.morphe.patches.youtube.shared.Constants.COMPATIBILITY_YOUTUBE import app.morphe.util.findFieldFromToString import app.morphe.util.findInstructionIndicesReversedOrThrow import app.morphe.util.indexOfFirstInstructionOrThrow +import app.morphe.util.insertLiteralOverride import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation @@ -179,6 +181,13 @@ val ambientModePatch = bytecodePatch( ) } + // Black background experiment: ambient mode is always inactive, whatever the + // CinematicContainerRenderer, power save mode or flyout menu toggle say. + // Turn it off, so the watch page uses the same ambient mode and layout as outside the experiment. + if (is_21_12_or_greater) AmbientModeBlackBackgroundFeatureFlagFingerprint.matchAllMethodIndicesForEach { index -> + insertLiteralOverride(index, false) + } + // The /next response can have no CinematicContainerRenderer, a static color style // (the background does not change with the video, issue #3783) // or ambient mode off by default. Override what the server decides. diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/ambientmode/Fingerprints.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/ambientmode/Fingerprints.kt index b7652e3b90..07bcd8ffbc 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/ambientmode/Fingerprints.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/ambientmode/Fingerprints.kt @@ -27,6 +27,15 @@ internal object AmbientModeFeatureFlagFingerprint : Fingerprint( ) ) +/** + * 21.12+ Server experiment of the new watch page that replaces ambient mode with a black background. + */ +internal object AmbientModeBlackBackgroundFeatureFlagFingerprint : Fingerprint( + filters = listOf( + literal(45767512L) + ) +) + /** * CinematicContainerRenderer, the ambient mode proto of the /next response. * The literal is its extension field number. From f8180da70c848560c4fe17106afb49ff6852e564 Mon Sep 17 00:00:00 2001 From: 0xrxL <127248639+0xrxL@users.noreply.github.com> Date: Thu, 29 Oct 2026 00:21:08 +0100 Subject: [PATCH 145/189] feat(YouTube - Minimal miniplayer): Swipe the bar up to move it to the top of the screen instead of expanding the player --- .../patches/MinimalMiniplayerPatch.java | 100 +++++++++++++----- 1 file changed, 72 insertions(+), 28 deletions(-) diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/MinimalMiniplayerPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/MinimalMiniplayerPatch.java index bc03f92976..f7def4ad65 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/MinimalMiniplayerPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/MinimalMiniplayerPatch.java @@ -153,7 +153,7 @@ private static int videoWidthFor(int height) { private static final Rect clipBounds = new Rect(); private static final Rect dockedBounds = new Rect(); - private static final Rect horizontalSpan = new Rect(); + private static final Rect barSpan = new Rect(); private static final int[] windowLocation = new int[2]; private static final Rect morphFrom = new Rect(); @@ -201,6 +201,12 @@ private static int videoWidthFor(int height) { */ private static boolean barShapeApplied; + /** + * Whether the bar was dragged to the top of the screen. Kept while the player stays + * minimized, so a change of the window keeps it there, and dropped once it is expanded. + */ + private static boolean barAtTop; + /** * Do not set it to 1.00f, otherwise it will cause artifacts (e.g: with the controls fading). */ @@ -304,7 +310,8 @@ public static void setLegacyControls(ViewGroup controlsLayout) { } // YouTube reads a gesture against the shape of its own miniplayer, which the bar is not, - // so the bar answers for itself: a tap or a drag up expands, a drag down closes. + // so the bar answers for itself: a tap expands, a drag moves the bar between the bottom + // and the top of the screen, and a drag down from the bottom closes it. final int slop = Dim.dp20; controlsLayout.setClickable(true); @@ -327,10 +334,22 @@ public boolean onTouch(View view, MotionEvent event) { case MotionEvent.ACTION_UP: final float draggedUp = downY - event.getRawY(); - if (draggedUp > slop) { - expandPlayer(); - } else if (-draggedUp > slop) { + if (-draggedUp > slop && !barAtTop) { closeBar(); + } else if (Math.abs(draggedUp) > slop) { + // Already at the top, a drag up has nowhere to go. Nor while the bar + // is animating, which can be the bar leaving before it is closed. + final boolean toTop = draggedUp > 0; + if (toTop == barAtTop || morphing || !barShapeApplied + || boundsControllerRef.get() == null || lastBounds.isEmpty()) { + return true; + } + + barAtTop = toTop; + barBoundsFor(lastBounds, toTop); + morphFrom.set(currentBounds); + morphTo.set(barBounds); + runMorph(false, () -> {}); } else if (Math.abs(event.getRawX() - downX) < slop) { // Goes through the click, so accessibility services see it. view.performClick(); @@ -370,7 +389,7 @@ public boolean onTouch(View view, MotionEvent event) { Logger.printDebug(() -> "No miniplayer bounds controller"); } else { morphFrom.set(currentBounds.isEmpty() ? lastBounds : currentBounds); - barBoundsFor(lastBounds); + barBoundsFor(lastBounds, barAtTop); morphTo.set(barBounds); barShapeApplied = true; showControls(true); @@ -390,6 +409,10 @@ public boolean onTouch(View view, MotionEvent event) { } } else { barShapeApplied = false; + // Still needed while the player grows from the top. + if (type != PlayerType.WATCH_WHILE_SLIDING_MINIMIZED_MAXIMIZED) { + barAtTop = false; + } // A dismissal is the bar sliding itself away, anything else grows the // player back and has to be uncovered, or it stays clipped. @@ -503,18 +526,24 @@ public static Rect getMinimalBarBounds(Rect original) { // Prevents the video from anchoring into one of the display corners by spanning the // bounds to full display width, making the transition to miniplayer smoother. - updateHorizontalSpan(); - dockedBounds.set(horizontalSpan.left, original.top, horizontalSpan.right, original.bottom); + updateBarSpan(); + dockedBounds.set(barSpan.left, original.top, barSpan.right, original.bottom); lastBounds.set(dockedBounds); PlayerType getCurrent = PlayerType.getCurrent(); if (getCurrent == PlayerType.WATCH_WHILE_SLIDING_MINIMIZED_MAXIMIZED) { - barBoundsFor(dockedBounds); + barBoundsFor(dockedBounds, false); final int maxDragTop = barBounds.top > 0 ? barBounds.top : dockedBounds.top; final float progress = maxDragTop > 0 ? Math.min(maxDragProgress, Math.max(0.0f, (float) original.top / maxDragTop)) : maxDragProgress; + // YouTube slides from where it rests at the bottom, which is what the progress + // is measured against, but a bar moved to the top grows from there. + if (barAtTop) { + barBoundsFor(dockedBounds, true); + } + final int currentLeft = interpolate(dockedBounds.left, barBounds.left, progress); final int currentTop = interpolate(dockedBounds.top, barBounds.top, progress); final int currentRight = interpolate(dockedBounds.right, barBounds.right, progress); @@ -530,7 +559,7 @@ public static Rect getMinimalBarBounds(Rect original) { // ends, so it is the bar for the new window, as it would be had nothing changed. final boolean minimized = getCurrent == PlayerType.WATCH_WHILE_MINIMIZED; if (minimized || getCurrent.isMaximizedOrFullscreen()) { - barBoundsFor(dockedBounds); + barBoundsFor(dockedBounds, barAtTop); currentBounds.set(barBounds); if (minimized) { barShapeApplied = true; @@ -547,7 +576,10 @@ public static Rect getMinimalBarBounds(Rect original) { return original; } - private static void barBoundsFor(Rect resting) { + /** + * @param atTop Whether the bar rests at the top of the layout instead of the bottom. + */ + private static void barBoundsFor(Rect resting, boolean atTop) { View playerLayout = barPlayerLayout(); // A floating miniplayer keeps a margin to the navigation bar, a docked bar sits against it. @@ -589,21 +621,26 @@ private static void barBoundsFor(Rect resting) { final int bottom = Math.min(dockedBottom, windowBottomFor(playerLayout)) - BAR_MARGIN; - updateHorizontalSpan(); - barBounds.set(horizontalSpan.left + BAR_MARGIN, bottom - BAR_HEIGHT, - horizontalSpan.right - BAR_MARGIN, bottom); + updateBarSpan(); + barBounds.set(barSpan.left + BAR_MARGIN, bottom - BAR_HEIGHT, + barSpan.right - BAR_MARGIN, bottom); + + if (atTop) { + // Below the status bar and the display cutout. + barBounds.offsetTo(barBounds.left, barSpan.top + BAR_MARGIN); + } } /** - * Where a bar can span, relative to the layout that holds the player. Not the display metrics, - * which measure the whole display. In landscape the window leaves out the display cutout, - * such as a front camera hole, and the system navigation buttons can sit beside the content, - * so a bar as wide as the display is cut off there. The window also differs from the display - * in split screen and on foldables. + * Where a bar can span, relative to the layout that holds the player: the sides, and the top + * edge a bar moved up rests below. Not the display metrics, which measure the whole display. + * In landscape the window leaves out the display cutout, such as a front camera hole, and the + * system navigation buttons can sit beside the content, so a bar as wide as the display is cut + * off there. The window also differs from the display in split screen and on foldables. */ @SuppressWarnings("deprecation") - private static void updateHorizontalSpan() { - horizontalSpan.set(0, 0, Dim.getMetrics().widthPixels, 0); + private static void updateBarSpan() { + barSpan.set(0, 0, Dim.getMetrics().widthPixels, 0); try { View watchPlayer = watchPlayer(); @@ -615,19 +652,23 @@ private static void updateHorizontalSpan() { if (parentWidth <= 0 || root == null || insets == null) return; int insetLeft; + int insetTop; int insetRight; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - Insets sideInsets = insets.getInsets( + Insets systemInsets = insets.getInsets( WindowInsets.Type.systemBars() | WindowInsets.Type.displayCutout()); - insetLeft = sideInsets.left; - insetRight = sideInsets.right; + insetLeft = systemInsets.left; + insetTop = systemInsets.top; + insetRight = systemInsets.right; } else { insetLeft = insets.getSystemWindowInsetLeft(); + insetTop = insets.getSystemWindowInsetTop(); insetRight = insets.getSystemWindowInsetRight(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { DisplayCutout cutout = insets.getDisplayCutout(); if (cutout != null) { insetLeft = Math.max(insetLeft, cutout.getSafeInsetLeft()); + insetTop = Math.max(insetTop, cutout.getSafeInsetTop()); insetRight = Math.max(insetRight, cutout.getSafeInsetRight()); } } @@ -639,10 +680,10 @@ private static void updateHorizontalSpan() { final int right = Math.min(parentWidth, root.getWidth() - insetRight - windowLocation[0]); if (right - left > 2 * BAR_MARGIN) { - horizontalSpan.set(left, 0, right, 0); + barSpan.set(left, Math.max(0, insetTop - windowLocation[1]), right, 0); } } catch (Exception ex) { - Logger.printException(() -> "updateHorizontalSpan failure", ex); + Logger.printException(() -> "updateBarSpan failure", ex); } } @@ -871,7 +912,10 @@ private static void closeBar() { morphTo.set(currentBounds); // Clear of the screen, not one bar height down. That only reaches the navigation bar, // which the bar then sits behind until YouTube is done closing after the click below. - morphTo.offset(0, Dim.getScreenHeight() - currentBounds.top); + // A bar moved to the top leaves through the top, the nearest edge. + morphTo.offset(0, barAtTop + ? -currentBounds.bottom + : Dim.getScreenHeight() - currentBounds.top); runMorph(false, () -> clickModernButton(modernCloseButtonRef, "close")); } @@ -1043,7 +1087,7 @@ private static void reapplyBarBounds() { MiniplayerBoundsController controller = boundsControllerRef.get(); if (controller == null) return; - barBoundsFor(lastBounds); + barBoundsFor(lastBounds, barAtTop); if (barBounds.equals(currentBounds)) return; setBounds(controller, barBounds); From e11da94ca64632956222074390f67f7f13bd82f9 Mon Sep 17 00:00:00 2001 From: 0xrxL <127248639+0xrxL@users.noreply.github.com> Date: Thu, 29 Oct 2026 03:20:28 +0100 Subject: [PATCH 146/189] fix(YouTube - Hide layout components): "Visit the community" entry point is not hidden in the comments of a video opened from a playlist --- .../components/LayoutComponentsFilter.java | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/components/LayoutComponentsFilter.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/components/LayoutComponentsFilter.java index d8480f080e..b26b4c241d 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/components/LayoutComponentsFilter.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/components/LayoutComponentsFilter.java @@ -46,8 +46,10 @@ import app.morphe.extension.youtube.patches.VersionCheckPatch; import app.morphe.extension.youtube.patches.utils.ProtoNode; import app.morphe.extension.youtube.settings.Settings; +import app.morphe.extension.youtube.shared.EngagementPanel; import app.morphe.extension.youtube.shared.NavigationBar; import app.morphe.extension.youtube.shared.NavigationBar.NavigationButton; +import app.morphe.extension.youtube.shared.PlayerType; @SuppressWarnings("unused") public final class LayoutComponentsFilter extends Filter { @@ -65,6 +67,14 @@ public final class LayoutComponentsFilter extends Filter { "channel_profile" ); + /** + * The comments panel of videos. Newer app versions use the first id. + */ + private static final List COMMENTS_PANEL_IDS = List.of( + "comment-item-section", + "engagement-panel-comments-section" + ); + private static final List channelTabFilterStrings = Utils.getFilterStrings(Settings.HIDE_CHANNEL_TAB_FILTER_STRINGS); private static final List flyoutMenuFilterStrings = Utils.getFilterStrings(Settings.HIDE_FEED_FLYOUT_MENU_FILTER_STRINGS); @@ -597,7 +607,11 @@ public boolean isFiltered(ContextInterface contextInterface, // prevents any posts from loading (the page shows a loading spinner forever). // Only hide the posts when no back button is visible (home and subscription feeds). // https://github.com/MorpheApp/morphe-patches/issues/4101 - if (NavigationBar.isBackButtonVisible()) { + // The comments of the watch page cover the page below them, whose back button + // stays visible when the video is opened from a page such as a playlist. + final boolean isCommentsPanel = PlayerType.getCurrent().isMaximizedOrFullscreen() + && COMMENTS_PANEL_IDS.contains(EngagementPanel.getLastOpenedPanelId()); + if (NavigationBar.isBackButtonVisible() && !isCommentsPanel) { return false; } From 5c6357fed1f52c5ed5d0236a393392b860e8d5ae Mon Sep 17 00:00:00 2001 From: 0xrxL <127248639+0xrxL@users.noreply.github.com> Date: Thu, 29 Oct 2026 03:32:57 +0100 Subject: [PATCH 147/189] fix(YouTube - Feed card layout): Movies converted to cards are not hidden when "Hide movies section" is enabled --- .../youtube/patches/FeedCardLayoutPatch.java | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FeedCardLayoutPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FeedCardLayoutPatch.java index b4bea67390..207d9e8fd3 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FeedCardLayoutPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FeedCardLayoutPatch.java @@ -324,6 +324,12 @@ public interface ElementItemInterface { private static final ByteTrieSearch shelfHeaderLabelSearch = new ByteTrieSearch( ByteTrieSearch.convertStringsToBytes(String.valueOf(SHELF_HEADER_LABEL_MARKER)) ); + /** + * Link to the movies page, in the bytes of the movies. + */ + private static final ByteTrieSearch moviesPageSearch = new ByteTrieSearch( + ByteTrieSearch.convertStringsToBytes("FEstorefront") + ); /** * Thumbnails of the Shorts, the same that the Shorts filter looks for. */ @@ -613,7 +619,7 @@ public static byte[] setFeedCardLayout(byte[] bytes) { break toCard; } - // The filters find the hidden Shorts, live streams and mix playlists by the classic layout. + // The filters find the hidden Shorts, live streams, mix playlists and movies by the classic layout. if (thumbnail.getBadgeStyle() == THUMBNAIL_BADGE_STYLE_LIVE && Settings.HIDE_LIVE_STREAMS.get()) { break toCard; @@ -622,6 +628,10 @@ public static byte[] setFeedCardLayout(byte[] bytes) { if (LayoutComponentsFilter.filterMixPlaylists(bytes)) { break toCard; } + // The movies are found by the link to the movies page, as in the ads filter. + if (Settings.HIDE_MOVIES_SECTION.get() && moviesPageSearch.matches(bytes)) { + break toCard; + } if ((Settings.HIDE_SHORTS_HOME.get() || Settings.HIDE_SHORTS_SUBSCRIPTIONS.get() || Settings.HIDE_SHORTS_SEARCH.get() || Settings.HIDE_SHORTS_HISTORY.get()) && shortsThumbnailSearch.matches(thumbnail.getImage().toByteArray())) { From 394ab97af76d788d6b13fc4c47f8e2ba4d892cfc Mon Sep 17 00:00:00 2001 From: 0xrxL <127248639+0xrxL@users.noreply.github.com> Date: Thu, 29 Oct 2026 05:04:32 +0100 Subject: [PATCH 148/189] fix(YouTube): Feed thumbnail badges are shown on the page header of a channel that is live --- .../youtube/patches/FeedViewCountPatch.java | 18 +++----- .../patches/utils/FeedVideoCardsPatch.java | 42 ++++++++++++++++--- 2 files changed, 42 insertions(+), 18 deletions(-) diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FeedViewCountPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FeedViewCountPatch.java index 40c884f4c3..8117f7ce5d 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FeedViewCountPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FeedViewCountPatch.java @@ -101,12 +101,6 @@ public final class FeedViewCountPatch { /** Allowed difference of sizes and positions that are compared. */ private static final int TOLERANCE_DP = 4; - /** - * Largest space between the thumbnail and the left, top and right edges of the card. - * Cards of some layouts show the thumbnail inset from their edges, such as by 12 dp. - */ - private static final int MAX_THUMBNAIL_INSET_DP = 16; - /** Space between the duration badge and the edges of the thumbnail, if the badge is not found. */ private static final int FALLBACK_MARGIN_DP = 8; /** A little more space than the duration badge has, so the count is not at the very edge. */ @@ -118,9 +112,6 @@ public final class FeedViewCountPatch { private static final int FALLBACK_HEIGHT_DP = 20; /** Cards of a grid of up to three columns, such as the feed in landscape. */ private static final float MIN_GRID_CARD_WIDTH_OF_SCREEN = 0.3f; - /** A duration badge larger than this is not the badge itself, but a host around it. */ - private static final int MAX_DURATION_WIDTH_DP = 120; - private static final int MAX_DURATION_HEIGHT_DP = 32; private static final int MIN_DURATION_HEIGHT_DP = 12; private static final int PADDING_DP = 5; private static final int GAP_DP = 3; @@ -820,7 +811,8 @@ private static void findCardInfo(ComponentHost root, View view, CardInfo info, R } info.addText(text, rect); String badgeText = text.toString().trim(); - if (!FeedVideoCardsPatch.isVideoBadgeText(badgeText)) { + if (!FeedVideoCardsPatch.isVideoBadgeText(badgeText) + || !FeedVideoCardsPatch.isThumbnailBadge(root, rect)) { continue; } info.hasDuration = true; @@ -921,7 +913,7 @@ private static int findSquareThumbnailBottom(ComponentHost root, List 0 && rect.left <= maxInset @@ -939,9 +931,9 @@ private static boolean isDurationBadgeShaped(ComponentHost root, Rect rect) { final float density = root.getResources().getDisplayMetrics().density; // Thumbnails are 16:9 or square, such as of music videos. return rect.width() > 0 - && rect.width() <= MAX_DURATION_WIDTH_DP * density + && rect.width() <= FeedVideoCardsPatch.MAX_DURATION_WIDTH_DP * density && rect.height() >= MIN_DURATION_HEIGHT_DP * density - && rect.height() <= MAX_DURATION_HEIGHT_DP * density + && rect.height() <= FeedVideoCardsPatch.MAX_DURATION_HEIGHT_DP * density && (isRtl(root) ? rect.centerX() < root.getWidth() / 2 : rect.centerX() > root.getWidth() / 2) && rect.bottom <= root.getWidth() + TOLERANCE_DP * density; } diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/FeedVideoCardsPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/FeedVideoCardsPatch.java index 2b59d4d394..5cb2e7b600 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/FeedVideoCardsPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/FeedVideoCardsPatch.java @@ -548,23 +548,55 @@ private static boolean hasDuration(View view) { return false; } + /** + * Largest space between the thumbnail and the left, top and right edges of the card. + * Cards of some layouts show the thumbnail inset from their edges, such as by 12 dp. + */ + public static final int MAX_THUMBNAIL_INSET_DP = 16; + /** A duration badge larger than this is not the badge itself, but a host around it. */ + public static final int MAX_DURATION_WIDTH_DP = 120; + public static final int MAX_DURATION_HEIGHT_DP = 32; + + /** + * @param rect The host that shows a badge text, such as a duration or 'LIVE', relative to the root host. + * @return If the text is a badge of the thumbnail: the host is as wide as the thumbnail and starts in it, + * such as the host of the whole thumbnail, or it's as large as a badge, on the side of the duration + * (the right, or the left in right to left languages). Not a badge text elsewhere on the card, + * such as the live badge on the avatar of a channel that is live, at the top of its page. + */ + public static boolean isThumbnailBadge(ComponentHost root, Rect rect) { + final float density = root.getResources().getDisplayMetrics().density; + final int width = root.getWidth(); + if (rect.width() >= width - 2 * MAX_THUMBNAIL_INSET_DP * density + && rect.top < width * THUMBNAIL_RATIO) { + return true; + } + return rect.width() <= MAX_DURATION_WIDTH_DP * density + && rect.height() <= MAX_DURATION_HEIGHT_DP * density + && (FeedViewCountPatch.isRtl(root) ? rect.centerX() < width / 2 : rect.centerX() > width / 2); + } + /** * Videos show their duration, or a badge such as the live badge, on the thumbnail. * Mixes, playlists and Shorts do not. */ - private static boolean hasVideoBadge(ComponentHost host) { + private static boolean hasVideoBadge(ComponentHost root, ComponentHost host, Rect rect) { TextContent textContent = host.getTextContent(); if (textContent != null) { for (CharSequence text : textContent.getTextItems()) { - if (text != null) { - if (isVideoBadgeText(text.toString().trim())) { + if (text != null && isVideoBadgeText(text.toString().trim())) { + rect.set(0, 0, host.getWidth(), host.getHeight()); + if (host != root) { + root.offsetDescendantRectToMyCoords(host, rect); + } + if (isThumbnailBadge(root, rect)) { return true; } } } } for (int i = 0, count = host.getChildCount(); i < count; i++) { - if (host.getChildAt(i) instanceof ComponentHost child && hasVideoBadge(child)) { + if (host.getChildAt(i) instanceof ComponentHost child && hasVideoBadge(root, child, rect)) { return true; } } @@ -661,7 +693,7 @@ public static String findVideoId(ComponentHost root) { if (texts.isEmpty()) { return NOT_READY; } - if (!hasVideoBadge(root)) { + if (!hasVideoBadge(root, root, new Rect())) { // A mix or a playlist, which shows a badge without a duration. return PLAYLIST; } From 958aa55c56ccef1e36ea1003f0f869203cc828b7 Mon Sep 17 00:00:00 2001 From: 0xrxL <127248639+0xrxL@users.noreply.github.com> Date: Thu, 29 Oct 2026 06:32:13 +0100 Subject: [PATCH 149/189] fix(YouTube - Video quality): Extend the background resume fix of regular videos to Shorts --- .../youtube/patches/VideoInformation.java | 7 + .../quality/RememberVideoQualityPatch.java | 84 +++++++++--- .../youtube/video/quality/Fingerprints.kt | 43 ++++++ .../quality/RememberVideoQualityPatch.kt | 125 +++++++++++++++--- 4 files changed, 222 insertions(+), 37 deletions(-) diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/VideoInformation.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/VideoInformation.java index 22d2be5af6..a18961b8d8 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/VideoInformation.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/VideoInformation.java @@ -279,6 +279,12 @@ private static boolean isPlaybackAudioPitchLinked() { */ public static final Event onChannelIdChange = new Event<>(); + /** + * Fires whenever the video of a player changes, such as when the next Short starts. + * Not when the same video is loaded again, such as after the app returns from the background. + */ + public static final Event onVideoIdChange = new Event<>(); + @Nullable public static VideoQualityInterface[] getCurrentQualities() { return currentQualities; @@ -466,6 +472,7 @@ public static void setVideoId(String newlyLoadedVideoId) { Logger.printDebug(() -> "New video ID: " + newlyLoadedVideoId); playerVideo.videoId = newlyLoadedVideoId; LocalQueuePatch.onVideoChanged(); + onVideoIdChange.invoke(newlyLoadedVideoId); } } diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/playback/quality/RememberVideoQualityPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/playback/quality/RememberVideoQualityPatch.java index 43f1615550..09f36dc298 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/playback/quality/RememberVideoQualityPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/playback/quality/RememberVideoQualityPatch.java @@ -17,7 +17,6 @@ import com.google.protobuf.MessageLite; -import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; @@ -33,6 +32,7 @@ import app.morphe.extension.youtube.innertube.ClientAbrStateOuterClass.ClientAbrState; import app.morphe.extension.youtube.patches.VideoInformation.*; import app.morphe.extension.youtube.settings.Settings; +import app.morphe.extension.youtube.shared.PlayerType; import app.morphe.extension.youtube.shared.ShortsPlayerState; import j$.util.Optional; @@ -101,6 +101,35 @@ public class RememberVideoQualityPatch { */ private static volatile int currentVideoFormatId; + static { + // A quality selected by the user is used for its video until another video plays or its player closes, + // such as when the video is loaded again after the app returns from the background. + // A video shown again starts with the default quality. + ShortsPlayerState.getOnChange().addObserver((Boolean isOpen) -> { + if (!isOpen && userSelectedQualityIsShorts) { + clearUserSelectedQuality(); + } + }); + // Another Short plays, such as after a swipe. The video of the regular player can change + // for a moment while its video plays, such as for an ad, so only the Shorts player is checked. + VideoInformation.onVideoIdChange.addObserver((String videoId) -> { + if (userSelectedQualityIsShorts && ShortsPlayerState.isOpen() + && !videoId.equals(userSelectedQualityVideoId)) { + clearUserSelectedQuality(); + } + }); + PlayerType.getOnChange().addObserver((PlayerType type) -> { + if (type == PlayerType.NONE && !userSelectedQualityIsShorts) { + clearUserSelectedQuality(); + } + }); + } + + private static void clearUserSelectedQuality() { + userSelectedQualityVideoId = ""; + clearVideoBufferTimeUs = -1; + } + private static void setUserSelectedQuality(int resolution, boolean isShorts) { userSelectedQualityResolution = resolution; userSelectedQualityIsShorts = isShorts; @@ -292,8 +321,7 @@ public static boolean onVideoIntentLoaded(Map playbackStartDescr // Another video is opened. The video ID of the player is updated only once // the video starts, so the quality selected for the current video // would otherwise be used when the other video starts. - userSelectedQualityVideoId = ""; - clearVideoBufferTimeUs = -1; + clearUserSelectedQuality(); } return false; } @@ -334,14 +362,22 @@ public static Optional getInitialVideoQuality(Optional optional) { * such as when the app returns from the background with background playback off. * Their initial video quality is the one used when the video started * (see {@link #getInitialVideoQuality(Optional)}), so the quality selected by the user - * for the video is used instead. Nothing is changed in the Shorts player. + * for the video is used instead. + *

    + * The Shorts player also loads the next Shorts before they play, while the current Short + * is still the video of the player, so a quality selected in the Shorts quality menu + * is used only by the player that loads the Short it was selected for. * * @param resolution Initial video quality of the playback start parameters, or null. + * @param videoId Video the player loads, or null if not known. * @return The initial video quality to use, or null if YouTube chooses the quality. */ @Nullable - public static Integer getPlaybackStartVideoQuality(@Nullable Integer resolution) { - if (ShortsPlayerState.isOpen() || !useUserSelectedQualityOnReload()) { + public static Integer getPlaybackStartVideoQuality(@Nullable Integer resolution, @Nullable String videoId) { + final boolean useSelectedQuality = userSelectedQualityIsShorts + ? videoId != null && !videoId.isEmpty() && videoId.equals(userSelectedQualityVideoId) + : !ShortsPlayerState.isOpen() && useUserSelectedQualityOnReload(); + if (!useSelectedQuality) { return resolution; } @@ -469,26 +505,40 @@ public static byte[] overrideClientAbrState(@Nullable MessageLite clientAbrState *

    * The quality menu of Shorts is also the quality menu of regular videos. * - * @param userSelectedQualityIndex Element index of {@link VideoInformation#getCurrentQualities()}. + * @param menuQualities Qualities of the quality menu, with index 0 being 'Auto', or null if not known. + * The menu keeps the qualities it was opened with, which can differ from + * {@link VideoInformation#getCurrentQualities()}, such as when YouTube removes + * a quality the player cannot play after the menu was opened. + * @param userSelectedQualityIndex Element index of the quality selected in the menu qualities. * @return The element index of the quality to set (see {@link #getPlayableQuality(VideoQualityInterface)}). */ - public static int userChangedShortsQuality(int userSelectedQualityIndex) { + public static int userChangedShortsQuality(@Nullable VideoQualityInterface[] menuQualities, + int userSelectedQualityIndex) { try { - VideoQualityInterface[] currentQualities = VideoInformation.getCurrentQualities(); - if (currentQualities == null) { - Logger.printDebug(() -> "Cannot save default quality, qualities is null"); + if (menuQualities == null || userSelectedQualityIndex < 0 + || userSelectedQualityIndex >= menuQualities.length) { + Logger.printDebug(() -> "Cannot save default quality, the quality selected is not known"); // The resolution is not known, so a reload uses the quality saved by YouTube. setUserSelectedQuality(VideoInformation.AUTOMATIC_VIDEO_QUALITY_VALUE, ShortsPlayerState.isOpen()); clearShortsMenuSelectedQuality(); return userSelectedQualityIndex; } - final VideoQualityInterface selectedQuality = currentQualities[userSelectedQualityIndex]; + final VideoQualityInterface selectedQuality = menuQualities[userSelectedQualityIndex]; VideoQualityInterface quality = getPlayableQuality(selectedQuality); - final int qualityIndex = quality == selectedQuality - ? userSelectedQualityIndex - : Arrays.asList(currentQualities).indexOf(quality); - if (qualityIndex < 0) { - quality = selectedQuality; + int qualityIndex = userSelectedQualityIndex; + if (quality != selectedQuality) { + // The quality found is one of the current qualities, so it is found in the menu by its name. + qualityIndex = -1; + final String qualityName = quality.patch_getQualityName(); + for (int i = 0; i < menuQualities.length; i++) { + if (qualityName.equals(menuQualities[i].patch_getQualityName())) { + qualityIndex = i; + break; + } + } + if (qualityIndex < 0) { + quality = selectedQuality; + } } final int resolution = quality.patch_getResolution(); setUserSelectedQuality(resolution, ShortsPlayerState.isOpen()); diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/video/quality/Fingerprints.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/video/quality/Fingerprints.kt index 9831a505e2..ac1b643340 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/video/quality/Fingerprints.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/video/quality/Fingerprints.kt @@ -22,6 +22,7 @@ import app.morphe.patcher.string import app.morphe.patcher.resource.ResourceType import app.morphe.patcher.resourceLiteral import app.morphe.patches.shared.CurrentAudioVideoFormatToStringFingerprint +import app.morphe.patches.youtube.video.information.VideoQualitySetterFingerprint import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.iface.reference.FieldReference @@ -413,6 +414,48 @@ internal fun getPlaybackStartParametersInitialResolutionFingerprint( ) ) {} +/** + * A method of the class of [getPlaybackStartParametersInitialResolutionFingerprint] that gets + * the playback start descriptor of its player, which has the video the player loads. + */ +internal fun getPlayerPlaybackStartDescriptorFingerprint( + playbackStartParametersGetterClass: String, + playbackStartDescriptorClass: String +) = Fingerprint( + definingClass = playbackStartParametersGetterClass, + filters = listOf( + fieldAccess( + opcode = Opcode.IGET_OBJECT, + definingClass = "this" + ), + methodCall( + returnType = playbackStartDescriptorClass, + parameters = listOf(), + location = MatchAfterImmediately() + ) + ) +) + +/** + * The method of [VideoQualitySetterFingerprint] that sets the qualities the quality menu shows. + */ +internal fun getVideoQualityMenuQualitiesFingerprint( + videoQualityMenuClass: String, + videoQualityArrayType: String +) = Fingerprint( + classFingerprint = VideoQualitySetterFingerprint, + returnType = "V", + parameters = listOf("[L", "I", "Z"), + strings = listOf("menu_item_video_quality"), + filters = listOf( + fieldAccess( + opcode = Opcode.IPUT_OBJECT, + definingClass = videoQualityMenuClass, + type = videoQualityArrayType + ) + ) +) + private object VideoQualityItemOnClickParentFingerprint : Fingerprint( returnType = "V", filters = listOf( diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/video/quality/RememberVideoQualityPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/video/quality/RememberVideoQualityPatch.kt index b2ad284820..e75fb29980 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/video/quality/RememberVideoQualityPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/video/quality/RememberVideoQualityPatch.kt @@ -39,10 +39,12 @@ import app.morphe.patches.youtube.video.format.hookAdaptiveFormat import app.morphe.patches.youtube.video.format.videoFormatPatch import app.morphe.patches.youtube.video.information.EXTENSION_VIDEO_QUALITY_INTERFACE import app.morphe.patches.youtube.video.information.EXTENSION_VIDEO_QUALITY_MENU_INTERFACE +import app.morphe.patches.youtube.video.information.PlaybackStartDescriptorToStringFingerprint import app.morphe.patches.youtube.video.information.SetVideoQualityFingerprint import app.morphe.patches.youtube.video.information.VideoQualityFingerprint import app.morphe.patches.youtube.video.information.videoInformationPatch import app.morphe.util.addInstructionsAtControlFlowLabel +import app.morphe.util.cloneMutable import app.morphe.util.findFieldFromToString import app.morphe.util.findInstructionIndicesReversedOrThrow import app.morphe.util.findMethodFromToString @@ -125,18 +127,70 @@ val rememberVideoQualityPatch = bytecodePatch { // The player loads the video again with the playback start parameters it was started with, // such as when the app returns from the background with background playback off, // so the quality selected by the user for the video is used instead of the initial quality. + // The Shorts player also loads the next Shorts before they play, so the video ID of the player + // that loads the video is passed, from the playback start descriptor of its player. getPlaybackStartParametersInitialResolutionFingerprint(initialResolutionField).matchSingle().method.apply { - findInstructionIndicesReversedOrThrow(Opcode.RETURN_OBJECT).forEach { index -> - val register = getInstruction(index).registerA - - addInstructionsAtControlFlowLabel( - index, + val descriptorMatch = getPlayerPlaybackStartDescriptorFingerprint( + definingClass, + PlaybackStartDescriptorToStringFingerprint.classDef.type + ) + val playerField = descriptorMatch.instructionMatches[0].getFieldAccessed() + val descriptorMethod = descriptorMatch.instructionMatches[1].getMethodCalled() + val invokeOpcode = if (AccessFlags.INTERFACE.isSet(classDefBy(playerField.type).accessFlags)) + "invoke-interface" else "invoke-virtual" + val videoIdMethod = PlaybackStartDescriptorToStringFingerprint.instructionMatches[1].getMethodCalled() + val playbackStartParametersGetterClass = mutableClassDefBy(definingClass) + + // The register of 'this' can be reused before the method returns, + // so the original method is called by a method that also gets the video ID. + val originalMethod = cloneMutable( + name = "patch_getPlaybackStartInitialResolution", + accessFlags = AccessFlags.PUBLIC.value or AccessFlags.FINAL.value + ) + playbackStartParametersGetterClass.methods.add(originalMethod) + + val getPlaybackStartVideoQualityMethod = ImmutableMethod( + definingClass, + "patch_getPlaybackStartVideoQuality", + listOf(), + "Ljava/lang/Integer;", + AccessFlags.PUBLIC.value or AccessFlags.FINAL.value, + null, + null, + MutableMethodImplementation(3), + ).toMutable().apply { + addInstructionsWithLabels( + 0, """ - invoke-static/range { v$register .. v$register }, $EXTENSION_CLASS->getPlaybackStartVideoQuality(Ljava/lang/Integer;)Ljava/lang/Integer; - move-result-object v$register + invoke-virtual { p0 }, $originalMethod + move-result-object v1 + iget-object v0, p0, $playerField + if-eqz v0, :no_video_id + $invokeOpcode { v0 }, $descriptorMethod + move-result-object v0 + if-eqz v0, :no_video_id + invoke-virtual { v0 }, $videoIdMethod + move-result-object v0 + :get_quality + invoke-static { v1, v0 }, $EXTENSION_CLASS->getPlaybackStartVideoQuality(Ljava/lang/Integer;Ljava/lang/String;)Ljava/lang/Integer; + move-result-object v0 + return-object v0 + :no_video_id + const/4 v0, 0x0 + goto :get_quality """ ) } + playbackStartParametersGetterClass.methods.add(getPlaybackStartVideoQualityMethod) + + addInstructions( + 0, + """ + invoke-virtual { p0 }, $getPlaybackStartVideoQualityMethod + move-result-object p0 + return-object p0 + """ + ) } // Turn off the YouTube automatic quality selection when the video starts, @@ -302,22 +356,53 @@ val rememberVideoQualityPatch = bytecodePatch { hookVideoIntent(EXTENSION_CLASS, detectVideo = true, detectShorts = true) // Inject a call to remember the selected quality for Shorts. - VideoQualityItemOnClickFingerprint.method.apply { - // A quality the player can play is set instead of a quality it cannot play. - addInstructions( - 0, - """ - invoke-static { p3 }, $EXTENSION_CLASS->userChangedShortsQuality(I)I - move-result p3 - """ - ) + VideoQualityItemOnClickFingerprint.let { + // The menu shows the qualities it was opened with, which YouTube can change after that, + // so the quality selected is found in the qualities of the menu. + val menuQualitiesField = getVideoQualityMenuQualitiesFingerprint( + it.classDef.type, + "[${VideoQualityFingerprint.classDef.type}" + ).instructionMatches.first().getFieldAccessed() - findInstructionIndicesReversedOrThrow(Opcode.RETURN_VOID).forEach { index -> - addInstructionsAtControlFlowLabel( - index, - "invoke-static { }, $EXTENSION_CLASS->clearShortsMenuSelectedQuality()V" + val userChangedShortsQualityMethod = ImmutableMethod( + it.classDef.type, + "patch_userChangedShortsQuality", + listOf(ImmutableMethodParameter("I", null, null)), + "I", + AccessFlags.PUBLIC.value or AccessFlags.FINAL.value, + null, + null, + MutableMethodImplementation(3), + ).toMutable().apply { + addInstructions( + 0, + """ + iget-object v0, p0, $menuQualitiesField + invoke-static { v0, p1 }, $EXTENSION_CLASS->userChangedShortsQuality([${EXTENSION_VIDEO_QUALITY_INTERFACE}I)I + move-result v0 + return v0 + """ ) } + it.classDef.methods.add(userChangedShortsQualityMethod) + + it.method.apply { + // A quality the player can play is set instead of a quality it cannot play. + addInstructions( + 0, + """ + invoke-virtual { p0, p3 }, $userChangedShortsQualityMethod + move-result p3 + """ + ) + + findInstructionIndicesReversedOrThrow(Opcode.RETURN_VOID).forEach { index -> + addInstructionsAtControlFlowLabel( + index, + "invoke-static { }, $EXTENSION_CLASS->clearShortsMenuSelectedQuality()V" + ) + } + } } // The Shorts quality menu sets the selected quality by its resolution, which YouTube can get From a5d943b40369e1a6050db6b5a51a5104a2190ab9 Mon Sep 17 00:00:00 2001 From: Zahran Ansaria Date: Thu, 29 Oct 2026 14:11:38 +0700 Subject: [PATCH 150/189] feat(YouTube Music - Third-party lyrics): Per-syllable romanization layout, dynamic secondary vocals, and typography refinements (#4090) --- .../music/patches/lyrics/LyricsManager.java | 135 +- .../music/patches/lyrics/LyricsMatch.java | 2 +- .../patches/lyrics/LyricsPrefetcher.java | 8 +- .../music/patches/lyrics/SkipSegments.java | 2 +- .../patches/lyrics/derive/AiProviders.java | 4 +- .../lyrics/derive/LyricsRomanizer.java | 14 +- .../lyrics/derive/LyricsTranslator.java | 4 +- .../patches/lyrics/derive/OpenAIClient.java | 44 +- .../patches/lyrics/derive/QuickLRCClient.java | 48 +- .../lyrics/derive/SectionDetector.java | 8 +- .../music/patches/lyrics/model/Lyrics.java | 2 +- .../lyrics/model/LyricsBracketSplitter.java | 739 +++++++-- .../patches/lyrics/model/LyricsLine.java | 24 +- .../patches/lyrics/model/LyricsMerge.java | 3 +- .../lyrics/model/LyricsTextFormatter.java | 14 +- .../patches/lyrics/model/MetadataCleaner.java | 6 +- .../lyrics/model/MiniLyricsFormatter.java | 78 +- .../patches/lyrics/model/TrackVersion.java | 8 +- .../patches/lyrics/parsers/LRCParser.java | 20 +- .../lyrics/parsers/LyricifyParser.java | 38 +- .../patches/lyrics/parsers/TTMLParser.java | 372 ++--- .../patches/lyrics/requests/AMLLProvider.java | 4 +- .../lyrics/requests/AppleMusicProvider.java | 6 +- .../lyrics/requests/BinimumProvider.java | 2 +- .../lyrics/requests/CaptionsFetcher.java | 4 +- .../lyrics/requests/DeezerProvider.java | 16 +- .../lyrics/requests/LRCLIBProvider.java | 2 +- .../lyrics/requests/LyricifyProvider.java | 60 +- .../lyrics/requests/LyricsProvider.java | 2 +- .../lyrics/requests/LyricsRequests.java | 62 +- .../lyrics/requests/MusixmatchProvider.java | 100 +- .../patches/lyrics/requests/QQProvider.java | 2 +- .../patches/lyrics/requests/RMMProvider.java | 4 +- .../lyrics/requests/SimpMusicProvider.java | 26 +- .../lyrics/requests/SpotifyProvider.java | 124 +- .../lyrics/requests/UnisonProvider.java | 42 +- .../lyrics/requests/YouTubeMusicProvider.java | 60 +- .../lyrics/romanizers/AlphabetRomanizer.java | 7 +- .../lyrics/romanizers/JyutpingRomanizer.java | 2 +- .../lyrics/romanizers/LocalRomanizer.java | 40 +- .../lyrics/romanizers/PinyinRomanizer.java | 2 +- .../lyrics/romanizers/ScriptDetector.java | 2 +- .../lyrics/session/MiniplayerLyrics.java | 12 +- .../patches/lyrics/storage/LyricsCache.java | 18 +- .../lyrics/storage/TrackOffsetStore.java | 10 +- .../music/patches/lyrics/ui/LyricsColors.java | 14 +- .../music/patches/lyrics/ui/LyricsFit.java | 16 +- .../patches/lyrics/ui/LyricsLineView.java | 1448 ++++++++++++++++- .../lyrics/ui/LyricsPanelInstaller.java | 9 +- .../patches/lyrics/ui/LyricsPanelView.java | 736 ++++++--- .../patches/lyrics/ui/LyricsSpanBuilder.java | 1094 +++++++++++-- .../patches/lyrics/ui/LyricsViewSupport.java | 136 +- .../patches/lyrics/ui/SectionDotsMotion.java | 14 +- .../ui/SponsorBlockMusicSubmitDialog.java | 66 +- .../extension/music/settings/Settings.java | 1 - .../preference/LyricsAiConfigPreference.java | 2 +- .../preference/LyricsAiPromptPreference.java | 12 +- .../LyricsClearCachePreference.java | 2 +- .../LyricsLocalRomanizationPreference.java | 4 +- .../LyricsOrderedListPreference.java | 2 +- .../LyricsTokenDialogPreference.java | 2 +- .../LyricsTranscribeKeyPreference.java | 28 +- .../model/LyricsBracketSplitterTest.java | 78 + .../patches/lyrics/model/TrackFormatTest.java | 22 +- .../music/layout/lyrics/LyricsPatch.kt | 1 - .../values-af-rZA/music/strings.xml | 2 - .../values-ar-rSA/music/strings.xml | 2 - .../values-az-rAZ/music/strings.xml | 2 - .../values-bg-rBG/music/strings.xml | 2 - .../values-bn-rBD/music/strings.xml | 2 - .../values-cs-rCZ/music/strings.xml | 2 - .../values-da-rDK/music/strings.xml | 2 - .../values-de-rDE/music/strings.xml | 2 - .../values-el-rGR/music/strings.xml | 2 - .../values-es-rES/music/strings.xml | 2 - .../values-fa-rIR/music/strings.xml | 2 - .../values-fi-rFI/music/strings.xml | 2 - .../values-fil-rPH/music/strings.xml | 2 - .../values-fr-rFR/music/strings.xml | 2 - .../values-ga-rIE/music/strings.xml | 2 - .../values-hi-rIN/music/strings.xml | 2 - .../values-hr-rHR/music/strings.xml | 2 - .../values-hu-rHU/music/strings.xml | 2 - .../values-in-rID/music/strings.xml | 2 - .../values-is-rIS/music/strings.xml | 2 - .../values-it-rIT/music/strings.xml | 2 - .../values-iw-rIL/music/strings.xml | 2 - .../values-ja-rJP/music/strings.xml | 2 - .../values-ko-rKR/music/strings.xml | 2 - .../values-lt-rLT/music/strings.xml | 2 - .../values-nb-rNO/music/strings.xml | 2 - .../values-nl-rNL/music/strings.xml | 2 - .../values-pl-rPL/music/strings.xml | 2 - .../values-pt-rBR/music/strings.xml | 2 - .../values-pt-rPT/music/strings.xml | 2 - .../values-ro-rRO/music/strings.xml | 2 - .../values-ru-rRU/music/strings.xml | 2 - .../values-sk-rSK/music/strings.xml | 2 - .../values-sl-rSI/music/strings.xml | 2 - .../values-sr-rCS/music/strings.xml | 2 - .../values-sr-rSP/music/strings.xml | 2 - .../values-sv-rSE/music/strings.xml | 2 - .../values-th-rTH/music/strings.xml | 2 - .../values-tr-rTR/music/strings.xml | 2 - .../values-uk-rUA/music/strings.xml | 2 - .../values-vi-rVN/music/strings.xml | 2 - .../values-zh-rCN/music/strings.xml | 2 - .../values-zh-rTW/music/strings.xml | 2 - .../addresources/values/music/strings.xml | 2 - 109 files changed, 4427 insertions(+), 1530 deletions(-) create mode 100644 extensions/music/src/test/java/app/morphe/extension/music/patches/lyrics/model/LyricsBracketSplitterTest.java diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/LyricsManager.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/LyricsManager.java index fcff8787e0..b399e525be 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/LyricsManager.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/LyricsManager.java @@ -36,7 +36,6 @@ import app.morphe.extension.music.patches.lyrics.derive.SectionDetector; import app.morphe.extension.music.patches.lyrics.model.Lyrics; -import app.morphe.extension.music.patches.lyrics.model.LyricsBracketSplitter; import app.morphe.extension.music.patches.lyrics.model.LyricsFilter; import app.morphe.extension.music.patches.lyrics.model.LyricsFilterDefaults; import app.morphe.extension.music.patches.lyrics.model.LyricsLine; @@ -315,9 +314,9 @@ private void requestVersion(TrackInfo track) { || !MiniLyricsFormatter.usesVariant(Settings.LYRICS_MINI_TRACK_FORMAT.get())) { return; } - final TrackInfo query = TrackVersion.query(track, currentRawTitle, currentRawArtist); + TrackInfo query = TrackVersion.query(track, currentRawTitle, currentRawArtist); runOnLookupThread(() -> { - final TrackVersion version; + TrackVersion version; try { version = DeezerProvider.fetchVersion(query); } catch (Exception ex) { @@ -515,7 +514,7 @@ public void setTemporaryOffsetMs(int ms) { /** Called from the panel when the track is about to change or to go away, not per drag frame. */ public void persistTemporaryOffsetMs() { - final TrackInfo track = currentTrack; + TrackInfo track = currentTrack; if (track == null) { return; } @@ -636,7 +635,7 @@ private void ensureLoadedForCurrentVideo() { || unconfirmedVideoId != null) { return; } - final String videoId = VideoInformation.getVideoId(); + String videoId = VideoInformation.getVideoId(); if (videoId.isEmpty() || videoId.equals(loadedForVideoId)) { return; } @@ -669,12 +668,12 @@ private void confirmTrackRebinding(String videoId, int generation, String rawTit if (generation != rebindingGeneration || unconfirmedVideoId == null) { return; } - final String target = unconfirmedVideoId; + String target = unconfirmedVideoId; unconfirmedVideoId = null; if (!target.equals(VideoInformation.getVideoId())) { return; } - final TrackInfo track = currentTrack; + TrackInfo track = currentTrack; if (track == null) { return; } @@ -682,7 +681,7 @@ private void confirmTrackRebinding(String videoId, int generation, String rawTit } private void dropStaleLyrics() { - final String videoId = VideoInformation.getVideoId(); + String videoId = VideoInformation.getVideoId(); if (videoId.isEmpty()) { return; } @@ -699,7 +698,7 @@ private void dropStaleLyrics() { suppressForRequest = -1; currentVideoId = videoId; loadedForVideoId = videoId; - final Lyrics shown = currentLyrics; + Lyrics shown = currentLyrics; if (shown != null && !shown.isEmpty()) { staleLyricsDropped = true; } @@ -828,7 +827,7 @@ public void onDisplayedTrackChanged(@Nullable String title, @Nullable String art return; } - final TrackInfo previous = currentTrack; + TrackInfo previous = currentTrack; TrackInfo track = new TrackInfo(cleanedTitle, cleanedArtist, previous == null ? "" : previous.album(), previous == null ? 0 : previous.durationSeconds()); @@ -909,14 +908,14 @@ public void fetchNextCandidate() { setState(State.LOADING, current); runOnLookupThread(() -> { - final String sbVideoId = skipSegmentsVideoId(); - final List segs = SkipSegments.await(sbVideoId, + String sbVideoId = skipSegmentsVideoId(); + List segs = SkipSegments.await(sbVideoId, track.durationSeconds() * 1000L); if (id == requestId.get()) { displaySegsVideoId = sbVideoId; displaySegs = segs; } - final TrackInfo searchTrack = LyricsMatch.withEffectiveDuration(track, segs); + TrackInfo searchTrack = LyricsMatch.withEffectiveDuration(track, segs); phase2Done = false; if (pollAndPublishNext(id, track)) { return; @@ -981,7 +980,7 @@ private void collectRemainingCandidates(int id, TrackInfo track, if (id != requestId.get()) { return; } - final TrackInfo candidateTrack = LyricsRequests.isCustomMatchMode() + TrackInfo candidateTrack = LyricsRequests.isCustomMatchMode() ? new TrackInfo(track.title(), track.artist(), "", 0) : track; Set existing = new HashSet<>(shownFingerprints); synchronized (candidateQueue) { @@ -1074,15 +1073,15 @@ private void processCandidateFuture(Future> f, int id, * lookup that called it has to treat everything it finds as queue material. */ private boolean applyRememberedPreference(int id, TrackInfo track) { - final LyricsPreference preference = LyricsCache.getPreference(preferenceVideoId, track); + LyricsPreference preference = LyricsCache.getPreference(preferenceVideoId, track); if (preference == null || preference.preferred() == null) { return false; } - final Lyrics raw = preference.preferred(); - final List queue = preference.queue(); - final String queryTitle = preference.queryTitle(); - final String queryArtist = preference.queryArtist(); - final String storedFingerprint = preference.fingerprint(); + Lyrics raw = preference.preferred(); + List queue = preference.queue(); + String queryTitle = preference.queryTitle(); + String queryArtist = preference.queryArtist(); + String storedFingerprint = preference.fingerprint(); if (!LyricsMatch.isValidLyrics(raw, track)) { Logger.printInfo(() -> "LyricsPref skipped: filters keep out the remembered lyric"); return false; @@ -1109,7 +1108,7 @@ private boolean applyRememberedPreference(int id, TrackInfo track) { rememberPreferenceDisarmed(); return false; } - final Lyrics display = preferred; + Lyrics display = preferred; Utils.runOnMainThread(() -> { if (id != requestId.get()) { return; @@ -1169,7 +1168,7 @@ public void publishPreferred(@Nullable Lyrics injected) { return; } - final Lyrics display; + Lyrics display; try { display = prepareForDisplay(injected); } catch (Throwable ignored) { @@ -1183,8 +1182,8 @@ public void publishPreferred(@Nullable Lyrics injected) { final int id = requestId.incrementAndGet(); cancelPendingFetches(); - final List pushedBack = new ArrayList<>(); - final Lyrics previous = currentLyrics; + List pushedBack = new ArrayList<>(); + Lyrics previous = currentLyrics; if (previous != null && !previous.isEmpty() && previous != Lyrics.NOT_FOUND) { pushedBack.add(previous); } @@ -1195,7 +1194,7 @@ public void publishPreferred(@Nullable Lyrics injected) { candidateQueue.clear(); } - final String chosen = LyricsMatch.fingerprint(display); + String chosen = LyricsMatch.fingerprint(display); shownFingerprints.clear(); shownFingerprints.add(chosen); rememberedQueue = null; @@ -1209,7 +1208,7 @@ public void publishPreferred(@Nullable Lyrics injected) { if (candidate == null || candidate.isEmpty() || candidate == Lyrics.NOT_FOUND) { continue; } - final String fp = LyricsMatch.fingerprint(candidate); + String fp = LyricsMatch.fingerprint(candidate); if (fp.equals(chosen)) { continue; } @@ -1280,15 +1279,15 @@ private List orderedQueueFingerprints() { } private void rememberPreference(Lyrics shown) { - final TrackInfo track = currentTrack; + TrackInfo track = currentTrack; if (track == null || shown == null || shown == Lyrics.NOT_FOUND || shown.isEmpty()) { return; } - final String videoId = preferenceVideoId; - final String queryTitle = searchQueryTitle; - final String queryArtist = searchQueryArtist; - final String fingerprint = LyricsMatch.fingerprint(shown); - final List queue = orderedQueueFingerprints(); + String videoId = preferenceVideoId; + String queryTitle = searchQueryTitle; + String queryArtist = searchQueryArtist; + String fingerprint = LyricsMatch.fingerprint(shown); + List queue = orderedQueueFingerprints(); preferredFingerprint = fingerprint; runOnLookupThread(() -> LyricsCache.putPreference(videoId, track, queryTitle, queryArtist, shown, queue, fingerprint)); @@ -1363,13 +1362,13 @@ public String[] rememberedSearchTerms() { */ public void searchWithCustomQuery(String title, String artist, boolean defaultTerms) { Utils.verifyOnMainThread(); - final TrackInfo track = currentTrack; + TrackInfo track = currentTrack; if (track == null) { return; } cancelPendingTrack(); - final String queryTitle = title != null ? title.trim() : ""; - final String queryArtist = artist != null ? artist.trim() : ""; + String queryTitle = title != null ? title.trim() : ""; + String queryArtist = artist != null ? artist.trim() : ""; if (queryTitle.isEmpty() || queryArtist.isEmpty()) { return; } @@ -1392,9 +1391,9 @@ public void searchWithCustomQuery(String title, String artist, boolean defaultTe // Video id providers only know the playing track, so they are asked only when the // terms are that track's own metadata. - final List enabledProviders = + List enabledProviders = LyricsProviders.providersInOrder(Settings.LYRICS_SOURCE.get()); - final List providers = defaultTerms + List providers = defaultTerms ? enabledProviders : LyricsProviders.searchableProviders(enabledProviders); if (providers.isEmpty()) { @@ -1403,15 +1402,15 @@ public void searchWithCustomQuery(String title, String artist, boolean defaultTe } runOnLookupThread(() -> { - final String sbVideoId = skipSegmentsVideoId(); - final List segs = SkipSegments.await(sbVideoId, + String sbVideoId = skipSegmentsVideoId(); + List segs = SkipSegments.await(sbVideoId, track.durationSeconds() * 1000L); if (id == requestId.get()) { displaySegsVideoId = sbVideoId; displaySegs = segs; } - final TrackInfo searchTrack = LyricsMatch.withEffectiveDuration(track, segs); - final TrackInfo queryTrack = defaultTerms + TrackInfo searchTrack = LyricsMatch.withEffectiveDuration(track, segs); + TrackInfo queryTrack = defaultTerms ? searchTrack : new TrackInfo(queryTitle, queryArtist, searchTrack.album(), searchTrack.durationSeconds()); @@ -1458,14 +1457,14 @@ public void searchWithCustomQuery(String title, String artist, boolean defaultTe private void runProviderLookup(int id, TrackInfo trackParam) { if (id != requestId.get()) return; - final String sbVideoId = skipSegmentsVideoId(); - final List segs = SkipSegments.await(sbVideoId, + String sbVideoId = skipSegmentsVideoId(); + List segs = SkipSegments.await(sbVideoId, trackParam.durationSeconds() * 1000L); if (id == requestId.get()) { displaySegsVideoId = sbVideoId; displaySegs = segs; } - final TrackInfo track = LyricsMatch.withEffectiveDuration(trackParam, segs); + TrackInfo track = LyricsMatch.withEffectiveDuration(trackParam, segs); final boolean suppressed = applyRememberedPreference(id, track); // Local files take priority: read embedded LYRICS/LYRIC tags before hitting providers. @@ -1486,7 +1485,7 @@ private void runProviderLookup(int id, TrackInfo trackParam) { String order = Settings.LYRICS_SOURCE.get(); List providers = LyricsProviders.providersInOrder(order); - final TrackInfo queryTrack = customQueryTrack(track); + TrackInfo queryTrack = customQueryTrack(track); if (queryTrack != null) { // The queue is being rebuilt from custom terms, which the providers keyed by // video id cannot answer. The cache is keyed by the playing track and holds what @@ -1784,8 +1783,8 @@ private LookupResult fetchFromProviders(int id, List originalTier, } } final int stage1Count = Math.min(4, nonBlind.size()); - final List stage1Providers = nonBlind.subList(0, stage1Count); - final List stage2Providers = nonBlind.subList( + List stage1Providers = nonBlind.subList(0, stage1Count); + List stage2Providers = nonBlind.subList( stage1Count, nonBlind.size()); CompletionService cs = new ExecutorCompletionService<>(fetchExecutor); @@ -1796,7 +1795,7 @@ private LookupResult fetchFromProviders(int id, List originalTier, AtomicBoolean threadFailed = new AtomicBoolean(false); List scoreCandidates = new ArrayList<>(); - final DisplayState display = new DisplayState(wordSync, exhaustive); + DisplayState display = new DisplayState(wordSync, exhaustive); for (VariantQuery vq : originalTier) { for (LyricsProvider provider : stage1Providers) { @@ -1963,7 +1962,7 @@ private int pollStage(int id, CompletionService cs, * rather than render the song twice. */ private void publishBest(int id, DisplayState display) { - final Lyrics available = display.bestResult != null + Lyrics available = display.bestResult != null ? display.bestResult : display.bestFallback; if (available == null || available == display.published) { return; @@ -2023,8 +2022,8 @@ private void publishFromLookup(int id, Lyrics lyrics) { } catch (Throwable ignored) { display = lyrics; } - final Lyrics raw = lyrics; - final Lyrics prepared = display; + Lyrics raw = lyrics; + Lyrics prepared = display; Utils.runOnMainThread(() -> { Lyrics current = currentLyrics; if (id == requestId.get() && state == State.LOADED @@ -2118,7 +2117,7 @@ public void clearDialogSegment() { } public void clearDialogSegment(@NonNull String videoId, long startMs, long endMs) { - final SkipSegments.Seg seg = dialogPreviewSeg; + SkipSegments.Seg seg = dialogPreviewSeg; if (seg != null && videoId.equals(dialogPreviewVideoId) && seg.startMs() == startMs && seg.endMs() == endMs) { clearDialogSegment(); @@ -2143,24 +2142,24 @@ private List syncSegs() { if (LyricsMatch.isVideoIdMatchedLyrics(currentLyrics)) { return null; } - final String videoId = skipSegmentsVideoId(); + String videoId = skipSegmentsVideoId(); if (videoId == null) { return null; } - final TrackInfo track = currentTrack; + TrackInfo track = currentTrack; List base = SkipSegments.peek(videoId, track == null ? 0 : track.durationSeconds() * 1000L); if (base != null) { displaySegsVideoId = videoId; displaySegs = base; } else { - final List latched = displaySegs; + List latched = displaySegs; base = latched != null && videoId.equals(displaySegsVideoId) ? latched : null; if (base == null) { SkipSegments.prefetch(videoId); } } - final SkipSegments.Seg preview = dialogPreviewSeg; + SkipSegments.Seg preview = dialogPreviewSeg; if (preview == null || !videoId.equals(dialogPreviewVideoId)) { return base; } @@ -2171,7 +2170,7 @@ private List syncSegs() { * the subtraction {@link #getPositionMs()} applies before it reads a lyric time. */ public long toVideoTimeMs(long lyricTimeMs) { - final List segs = syncSegs(); + List segs = syncSegs(); if (segs == null || segs.isEmpty()) { return lyricTimeMs; } @@ -2182,13 +2181,9 @@ public long toVideoTimeMs(long lyricTimeMs) { * of synced lyrics, consulting the short-lived cache of already filtered results. */ private Lyrics prepareForDisplay(Lyrics lyrics) { - final boolean splitBackground = Settings.LYRICS_SPLIT_BACKGROUND.get(); String cacheKey = null; if (lyrics != null && lyrics != Lyrics.NOT_FOUND && !lyrics.isEmpty()) { - // The split changes the lines, so it belongs in the key or toggling it hands back - // the other result. - StringBuilder sb = new StringBuilder(lyrics.providerName()) - .append(splitBackground ? "|bg" : "|"); + StringBuilder sb = new StringBuilder(lyrics.providerName()).append('|'); List lines = lyrics.lines(); int limit = Math.min(3, lines.size()); for (int i = 0; i < limit; i++) { @@ -2204,10 +2199,6 @@ private Lyrics prepareForDisplay(Lyrics lyrics) { if (cacheKey == null || lyrics != filteredCache.get(cacheKey)) { lyrics = LyricsFilter.filterCreditLines(lyrics, currentTrack); lyrics = LyricsFilter.filterLyricsText(lyrics); - // After both filters, so a pattern the user wrote sees the lines as they arrived. - if (splitBackground) { - lyrics = LyricsBracketSplitter.apply(lyrics); - } if (cacheKey != null) { filteredCache.put(cacheKey, lyrics); } @@ -2237,9 +2228,9 @@ private void scheduleQueueFill(int id, boolean pending, List origi if (id != requestId.get() || !pending) { return; } - final List fillOriginal = List.copyOf(original); - final List fillDerived = List.copyOf(derived); - final List fillProviders = List.copyOf(providers); + List fillOriginal = List.copyOf(original); + List fillDerived = List.copyOf(derived); + List fillProviders = List.copyOf(providers); runOnLookupThread( () -> fillQueueInBackground(id, fillOriginal, fillDerived, fillProviders)); } @@ -2304,12 +2295,12 @@ private void detectSections(@Nullable Lyrics lyrics) { if (!SectionDetector.needsDetection(lyrics)) { return; } - final TrackInfo track = currentTrack; - final Lyrics target = lyrics; + TrackInfo track = currentTrack; + Lyrics target = lyrics; if (track == null || target == null) { return; } - final String fingerprint = LyricsMatch.fingerprint(target); + String fingerprint = LyricsMatch.fingerprint(target); if (fingerprint.equals(sectionsPendingFingerprint)) { return; } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/LyricsMatch.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/LyricsMatch.java index 4ca5f589e4..5eec60ee8a 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/LyricsMatch.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/LyricsMatch.java @@ -40,7 +40,7 @@ public static String cacheSourceFor(@Nullable Lyrics lyrics) { if (lyrics == null) { return ""; } - final String providerName = lyrics.providerName(); + String providerName = lyrics.providerName(); for (String id : LyricsProviders.PROVIDER_ORDER) { if (providerName.equals(id)) { return id; diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/LyricsPrefetcher.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/LyricsPrefetcher.java index 580aae029f..6eb5f961ea 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/LyricsPrefetcher.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/LyricsPrefetcher.java @@ -69,7 +69,7 @@ static void prefetchSuccessorOf(@Nullable String currentVideoId) { if (!Settings.LYRICS_ENABLED.get()) return; if (!Utils.isNetworkConnected()) return; - final Future previous; + Future previous; synchronized (LyricsPrefetcher.class) { if (currentVideoId.equals(prefetchingFor)) return; prefetchingFor = currentVideoId; @@ -79,7 +79,7 @@ static void prefetchSuccessorOf(@Nullable String currentVideoId) { // Frees the single thread for the track that is playing now. previous.cancel(true); } - final Future task = executor.submit(() -> runPrefetch(currentVideoId)); + Future task = executor.submit(() -> runPrefetch(currentVideoId)); synchronized (LyricsPrefetcher.class) { if (currentVideoId.equals(prefetchingFor)) { running = task; @@ -105,7 +105,7 @@ private static void runPrefetch(String currentVideoId) { private static void prefetch(String currentVideoId) { if (superseded(currentVideoId)) return; - final String[] nextVideoId = new String[1]; + String[] nextVideoId = new String[1]; TrackInfo track = SessionQueue.nextTrackOf(currentVideoId, nextVideoId); if (superseded(currentVideoId)) return; if (track == null) return; @@ -120,7 +120,7 @@ private static void prefetch(String currentVideoId) { // The providers keyed by the video of the playing track only know that track, // so the successor is asked with the ones that search by title and artist. - final List providers = LyricsProviders.searchableProviders( + List providers = LyricsProviders.searchableProviders( LyricsProviders.providersInOrder(Settings.LYRICS_SOURCE.get())); if (providers.isEmpty()) return; if (allKnown(track, providers)) return; diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/SkipSegments.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/SkipSegments.java index b826fd6dfe..2f6285672e 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/SkipSegments.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/SkipSegments.java @@ -272,7 +272,7 @@ private static List applicable(@Nullable List segs, long durationMs) { if (!stale) { return segs; } - final List kept = new ArrayList<>(segs.size()); + List kept = new ArrayList<>(segs.size()); for (Seg seg : segs) { final long reported = seg.reportedDurationMs(); if (reported > 0 && Math.abs(reported - durationMs) > REPORTED_DURATION_TOLERANCE_MS) { diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/derive/AiProviders.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/derive/AiProviders.java index 152553d47c..d65290a71f 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/derive/AiProviders.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/derive/AiProviders.java @@ -42,8 +42,8 @@ public static boolean isAi(@Nullable String provider) { */ @Nullable static Result mapLines(String provider, String prompt, List lines) { - final String model; - final List mapped; + String model; + List mapped; switch (provider) { case GEMINI -> { String apiKey = Settings.LYRICS_GEMINI_API_KEY.get(); diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/derive/LyricsRomanizer.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/derive/LyricsRomanizer.java index be9f31b05f..d42b569233 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/derive/LyricsRomanizer.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/derive/LyricsRomanizer.java @@ -131,7 +131,7 @@ public static void romanize(TrackInfo track, Lyrics lyrics, String source, Callb executor.execute(() -> { try { - final String provider = Settings.LYRICS_ROMANIZATION_PROVIDER.get(); + String provider = Settings.LYRICS_ROMANIZATION_PROVIDER.get(); if (AiProviders.LOCAL.equals(provider)) { if (!JapaneseDictStore.isReady()) JapaneseDictStore.ensure(null); @@ -145,8 +145,8 @@ public static void romanize(TrackInfo track, Lyrics lyrics, String source, Callb dictionary = result != null ? RULE_BASED_DICTIONARY : null; } if (LyricsMerge.hasText(result)) { - final List finalResult = alignIfWordSynced(lyrics, result, isWordSynced); - final String finalDictionary = dictionary; + List finalResult = alignIfWordSynced(lyrics, result, isWordSynced); + String finalDictionary = dictionary; final boolean hasWordRomaji = isWordSynced && finalResult != null && LyricsMerge.anyWordHasRomaji(finalResult); Utils.runOnMainThread(() -> callback.onRomanized( finalResult, false, false, null, hasWordRomaji, finalDictionary)); @@ -158,7 +158,7 @@ public static void romanize(TrackInfo track, Lyrics lyrics, String source, Callb if (AiProviders.isAi(provider)) { List aiCached = LyricsCache.getRomanizationAI(track, source, lines); if (aiCached != null) { - final List aligned = alignIfWordSynced(lyrics, aiCached, isWordSynced); + List aligned = alignIfWordSynced(lyrics, aiCached, isWordSynced); final boolean hasWordRomaji = isWordSynced && aligned != null && LyricsMerge.anyWordHasRomaji(aligned); Utils.runOnMainThread(() -> callback.onRomanized(aligned, false, true, provider, hasWordRomaji, null)); return; @@ -213,7 +213,7 @@ public static void romanize(TrackInfo track, Lyrics lyrics, String source, Callb } } - final List finalResult = result; + List finalResult = result; final boolean fromGoogle = LyricsMerge.hasText(result) && online && local == null; final boolean hasWordRomaji = isWordSynced && finalResult != null && LyricsMerge.anyWordHasRomaji(finalResult); Utils.runOnMainThread(() -> callback.onRomanized( @@ -375,7 +375,7 @@ private static List romanizeTexts(List texts) { return result; } - final String language = Settings.LYRICS_ROMANIZE_LANGUAGE.get(); + String language = Settings.LYRICS_ROMANIZE_LANGUAGE.get(); List fetched = LyricsMerge.mapLinesOnline(toFetch, batch -> { try { return TextTranslator.romanize(batch, language); @@ -853,7 +853,7 @@ private static List alignRomanizationAnchors( String fullText, List romanizedGuides, List originalSyllables) { - final String target = fullText; + String target = fullText; final int N = target.length(); final int M = romanizedGuides.size(); diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/derive/LyricsTranslator.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/derive/LyricsTranslator.java index 159f9040aa..6b00bc1689 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/derive/LyricsTranslator.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/derive/LyricsTranslator.java @@ -45,7 +45,7 @@ private static List embeddedTranslation(Lyrics lyrics, String target, in return null; } String targetLang = LyricsRequests.primarySubtag(target); - final String targetScript = LyricsRequests.resolvedScript(target); + String targetScript = LyricsRequests.resolvedScript(target); for (Map.Entry> entry : byLang.entrySet()) { String key = entry.getKey(); if (key.startsWith(LyricsMerge.BACKGROUND_TRACK_PREFIX)) { @@ -81,7 +81,7 @@ private static List embeddedTranslationLines(Lyrics lyrics, List fetchModels(String baseUrl, @Nullable String apiToken) { List ids = new ArrayList<>(); - final String models = listModels(baseUrl, apiToken); + String models = listModels(baseUrl, apiToken); if (models == null) { return ids; } @@ -119,7 +119,7 @@ public static List fetchModels(String baseUrl, @Nullable String apiToken /** The raw response of the endpoint's model list, or null if it has none. */ @Nullable private static String listModels(String baseUrl, @Nullable String apiToken) { - final String url = modelsUrl(baseUrl); + String url = modelsUrl(baseUrl); if (url == null) { return null; } @@ -158,7 +158,7 @@ private static String modelsUrl(String baseUrl) { if (slash < 0) { return null; } - final String host = base.substring(0, slash); + String host = base.substring(0, slash); String path = base.substring(slash); for (String chatPath : new String[]{"/chat/completions", "/completions"}) { if (path.endsWith(chatPath)) { @@ -176,12 +176,12 @@ private static String modelsUrl(String baseUrl) { } private static String[] parseSupportedEfforts(String json, String model) throws JSONException { - final String trimmed = json.trim(); + String trimmed = json.trim(); JSONArray entries; if (trimmed.startsWith("[")) { entries = new JSONArray(trimmed); } else { - final JSONObject root = new JSONObject(trimmed); + JSONObject root = new JSONObject(trimmed); entries = root.optJSONArray("data"); if (entries == null) { entries = root.optJSONArray("models"); @@ -191,23 +191,23 @@ private static String[] parseSupportedEfforts(String json, String model) throws return new String[0]; } for (int i = 0; i < entries.length(); i++) { - final JSONObject entry = entries.optJSONObject(i); + JSONObject entry = entries.optJSONObject(i); if (entry == null || !describesSameModel(entry, model)) { continue; } - final Object reasoning = entry.opt("reasoning"); + Object reasoning = entry.opt("reasoning"); if (!(reasoning instanceof JSONObject detail)) { // A plain true/false says the model reasons but not how hard, so there is nothing // to choose from and the parameter is not worth offering. return new String[0]; } - final JSONArray efforts = detail.optJSONArray("supported_efforts"); + JSONArray efforts = detail.optJSONArray("supported_efforts"); if (efforts == null || efforts.length() == 0) { return new String[0]; } - final List values = new ArrayList<>(efforts.length()); + List values = new ArrayList<>(efforts.length()); for (int j = 0; j < efforts.length(); j++) { - final String value = efforts.optString(j, "").trim(); + String value = efforts.optString(j, "").trim(); if (!value.isEmpty() && !values.contains(value)) { values.add(value); } @@ -221,9 +221,9 @@ private static boolean describesSameModel(JSONObject entry, String model) { if (model == null) { return false; } - final String wanted = model.trim(); + String wanted = model.trim(); for (String key : new String[]{"id", "name", "canonical_slug"}) { - final String candidate = entry.optString(key, "").trim(); + String candidate = entry.optString(key, "").trim(); if (candidate.isEmpty()) { continue; } @@ -232,7 +232,7 @@ private static boolean describesSameModel(JSONObject entry, String model) { return true; } } - final JSONArray aliases = entry.optJSONArray("aliases"); + JSONArray aliases = entry.optJSONArray("aliases"); if (aliases != null) { for (int i = 0; i < aliases.length(); i++) { if (aliases.optString(i, "").equalsIgnoreCase(wanted)) { @@ -251,7 +251,7 @@ private static String stripVariant(String id) { /** The stored effort, sent as typed, or empty when nothing is stored. */ @SuppressWarnings("ConstantValue") public static String reasoningEffort() { - final String configured = Settings.LYRICS_AI_REASONING_EFFORT.get(); + String configured = Settings.LYRICS_AI_REASONING_EFFORT.get(); if (configured != null && !configured.trim().isEmpty()) { return configured.trim(); } @@ -261,7 +261,7 @@ public static String reasoningEffort() { /** An array or a bare value has no keys to lay over a body, so neither counts as one. */ public static boolean isValidExtraBody(@Nullable String body) { if (body == null) return true; - final String trimmed = body.trim(); + String trimmed = body.trim(); if (trimmed.isEmpty()) return true; try { new JSONObject(trimmed); @@ -276,16 +276,16 @@ public static boolean isValidExtraBody(@Nullable String body) { * messages, whose length decides where the answer is cut into lines. */ private static void applyExtraBody(JSONObject body) { - final String extra = Settings.LYRICS_AI_EXTRA_BODY.get(); + String extra = Settings.LYRICS_AI_EXTRA_BODY.get(); if (extra == null) return; - final String trimmed = extra.trim(); + String trimmed = extra.trim(); if (trimmed.isEmpty()) return; try { - final JSONObject overlay = new JSONObject(trimmed); - final Iterator keys = overlay.keys(); + JSONObject overlay = new JSONObject(trimmed); + Iterator keys = overlay.keys(); while (keys.hasNext()) { - final String key = keys.next(); + String key = keys.next(); if (key.equals("model") || key.equals("messages")) continue; // get rather than opt, so an explicit null is sent as the null it was written as. body.put(key, overlay.get(key)); @@ -305,7 +305,7 @@ static String request(String baseUrl, String apiToken, String model, try { JSONObject body = new JSONObject(); body.put("model", model); - final String effort = reasoningEffort(); + String effort = reasoningEffort(); if (!effort.isEmpty()) { body.put("reasoning_effort", effort); } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/derive/QuickLRCClient.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/derive/QuickLRCClient.java index 8aa339e99b..b20cbfac41 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/derive/QuickLRCClient.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/derive/QuickLRCClient.java @@ -71,12 +71,12 @@ public interface KeyCheckCallback { } public static void checkKey(String apiKey, KeyCheckCallback callback) { - final String key = apiKey == null ? "" : apiKey.trim(); + String key = apiKey == null ? "" : apiKey.trim(); if (key.isEmpty()) { callback.onChecked(KeyCheck.REJECTED); return; } - final Thread worker = new Thread(() -> { + Thread worker = new Thread(() -> { KeyCheck result; HttpURLConnection conn = null; try { @@ -99,7 +99,7 @@ public static void checkKey(String apiKey, KeyCheckCallback callback) { conn.disconnect(); } } - final KeyCheck answer = result; + KeyCheck answer = result; Utils.runOnMainThread(() -> callback.onChecked(answer)); }, "QuickLRC key check"); worker.setDaemon(true); @@ -115,17 +115,17 @@ private QuickLRCClient() { @SuppressWarnings("ConstantValue") public static boolean isConfigured() { - final String key = Settings.LYRICS_TRANSCRIBE_KEY.get(); + String key = Settings.LYRICS_TRANSCRIBE_KEY.get(); return key != null && !key.trim().isEmpty(); } public static boolean isRunning() { - final Future current = running; + Future current = running; return current != null && !current.isDone(); } public static void cancel() { - final Future current = running; + Future current = running; running = null; if (current != null) { current.cancel(true); @@ -135,17 +135,17 @@ public static void cancel() { @SuppressWarnings("ConstantValue") public static void transcribe(String videoId, @Nullable String lyrics, BooleanSupplier stillWanted, Callback callback) { - final String key = Settings.LYRICS_TRANSCRIBE_KEY.get(); + String key = Settings.LYRICS_TRANSCRIBE_KEY.get(); if (key == null || key.trim().isEmpty()) { Utils.runOnMainThread(() -> callback.onTranscribed(null, Failure.NO_KEY)); return; } - final String source = videoId == null ? "" : videoId.trim(); + String source = videoId == null ? "" : videoId.trim(); if (source.isEmpty()) { Utils.runOnMainThread(() -> callback.onTranscribed(null, Failure.UNSUPPORTED_SOURCE)); return; } - final String text = lyrics == null ? "" : lyrics.trim(); + String text = lyrics == null ? "" : lyrics.trim(); cancel(); running = executor.submit(() -> { @@ -153,7 +153,7 @@ public static void transcribe(String videoId, @Nullable String lyrics, Lyrics result = null; try { if (stillWanted.getAsBoolean()) { - final String ttml = post(source, text, key.trim()); + String ttml = post(source, text, key.trim()); if (ttml == null) { failure = Failure.FAILED; } else { @@ -176,8 +176,8 @@ public static void transcribe(String videoId, @Nullable String lyrics, } } - final Lyrics deliver = result; - final Failure reason = failure; + Lyrics deliver = result; + Failure reason = failure; Utils.runOnMainThread(() -> callback.onTranscribed(deliver, reason)); }); } @@ -190,7 +190,7 @@ private static final class OutOfCreditsException extends IOException { private static String post(String videoId, String lyrics, String apiKey) throws IOException, JSONException { - final JSONObject body = new JSONObject(); + JSONObject body = new JSONObject(); body.put("fileUrl", "https://www.youtube.com/watch?v=" + videoId); body.put("format", "lrc"); body.put("isWordLevel", true); @@ -198,19 +198,19 @@ private static String post(String videoId, String lyrics, String apiKey) body.put("lyrics", lyrics); } - final String json = body.toString(); + String json = body.toString(); String target = ENDPOINT; for (int hop = 0; hop <= MAX_REDIRECTS; hop++) { - final Attempt attempt = sendOnce(target, json, apiKey); + Attempt attempt = sendOnce(target, json, apiKey); final int code = attempt.code(); if (code == 307 || code == 308 || code == 301 || code == 302) { - final String location = attempt.location(); + String location = attempt.location(); if (location == null || location.isEmpty()) { return null; } - final String next = resolve(target, location); + String next = resolve(target, location); if (next == null) { return null; } @@ -258,15 +258,15 @@ private static Attempt sendOnce(String url, String json, String apiKey) throws I conn.setDoOutput(true); conn.setInstanceFollowRedirects(false); - final byte[] payload = json.getBytes(StandardCharsets.UTF_8); + byte[] payload = json.getBytes(StandardCharsets.UTF_8); try (java.io.OutputStream out = conn.getOutputStream()) { out.write(payload); } final int code = conn.getResponseCode(); - final String location = conn.getHeaderField("Location"); + String location = conn.getHeaderField("Location"); if (code == 200) { - final String body = Requester.parseString(conn); + String body = Requester.parseString(conn); return new Attempt(code, conn.getResponseMessage(), conn.getContentType(), conn.getContentLength(), location, body, null); } @@ -282,7 +282,7 @@ private static Attempt sendOnce(String url, String json, String apiKey) throws I @Nullable private static String resolve(String base, String location) { try { - final java.net.URL resolved = new URL(new URL(base), location); + java.net.URL resolved = new URL(new URL(base), location); return resolved.getProtocol() + "://" + resolved.getAuthority() + resolved.getPath() + (resolved.getQuery() == null ? "" : "?" + resolved.getQuery()); } catch (java.net.MalformedURLException ex) { @@ -298,7 +298,7 @@ private static String resolve(String base, String location) { * that far past the tag matches. */ private static String dropCjkSeparators(String lrc) { - final StringBuilder out = new StringBuilder(lrc.length()); + StringBuilder out = new StringBuilder(lrc.length()); for (int i = 0; i < lrc.length(); ) { if (Character.isWhitespace(lrc.charAt(i)) && isCjkGap(lrc, i)) { i = skipWhitespace(lrc, i); @@ -380,8 +380,8 @@ private static boolean isCjk(char c) { private static Lyrics toLyrics(String lrc) { try { // Cleaned first, so the raw format and everything reading it are the clean one. - final String body = dropCjkSeparators(lrc); - final List lines = LRCParser.parseSynced(body); + String body = dropCjkSeparators(lrc); + List lines = LRCParser.parseSynced(body); if (lines == null || lines.isEmpty()) { return null; } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/derive/SectionDetector.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/derive/SectionDetector.java index c6668ae1ed..6b4fdd59e8 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/derive/SectionDetector.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/derive/SectionDetector.java @@ -62,11 +62,11 @@ public static boolean needsDetection(@Nullable Lyrics lyrics) { public static void detect(TrackInfo track, Lyrics lyrics, BooleanSupplier stillWanted, Callback callback) { List lines = lyrics.lines(); - final List texts = new ArrayList<>(lines.size()); + List texts = new ArrayList<>(lines.size()); for (LyricsLine line : lines) { texts.add(line.text()); } - final String source = lyrics.providerName(); + String source = lyrics.providerName(); executor.execute(() -> { try { @@ -76,7 +76,7 @@ public static void detect(TrackInfo track, Lyrics lyrics, BooleanSupplier stillW } List cached = LyricsCache.getSectionAI(track, source, texts); if (cached != null) { - final List result = cached; + List result = cached; Utils.runOnMainThread(() -> callback.onSections(result)); return; } @@ -89,7 +89,7 @@ public static void detect(TrackInfo track, Lyrics lyrics, BooleanSupplier stillW if (detected != null) { LyricsCache.putSectionAI(track, source, texts, detected); } - final List result = detected; + List result = detected; Utils.runOnMainThread(() -> callback.onSections(result)); } catch (Exception ex) { Logger.printException(() -> "detect failure", ex); diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/Lyrics.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/Lyrics.java index cfd2568a45..d7e6904c12 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/Lyrics.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/Lyrics.java @@ -242,7 +242,7 @@ public static List clampLastWordEnds(List lines) { long effectiveEnd = lastWord.endMs(); long nextStart = LyricsLine.NO_TIME; for (int j = i + 1; j < size; j++) { - final LyricsLine candidate = out.get(j); + LyricsLine candidate = out.get(j); if (candidate.isBG()) { continue; } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/LyricsBracketSplitter.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/LyricsBracketSplitter.java index 91bde3e79b..d37abf607c 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/LyricsBracketSplitter.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/LyricsBracketSplitter.java @@ -22,7 +22,7 @@ public final class LyricsBracketSplitter { private static final LyricsLine BLANK = new LyricsLine(LyricsLine.NO_TIME, ""); private static final Pattern NOT_SUNG_RUN = Pattern.compile( - "(?i)\\s*(?:x\\s*\\d+|\\d+\\s*x|repeat|chorus|verse|pre\\s*chorus|post\\s*chorus)\\s*"); + "(?i)^[\\s()()\\[\\]]*(?:x\\s*\\d+|\\d+\\s*x|repeat(?:\\s*x?\\d+)?|chorus|verse(?:\\s*\\d+)?|pre[-\\s]*chorus|post[-\\s]*chorus|intro|outro|instrumental|bridge|hook|interlude|solo|music|guitar\\s*solo)[\\s()()\\[\\]]*$"); private record Run(int open, int close) { } @@ -38,159 +38,351 @@ public static Lyrics apply(Lyrics lyrics) { return lyrics; } - final List lines = lyrics.lines(); + List lines = lyrics.lines(); final int count = lines.size(); - final int[] room = new int[count]; + Cut[] cuts = new Cut[count]; + LyricsLine[] normalizedBg = new LyricsLine[count]; + int[] room = new int[count]; int extra = 0; + boolean anyNormalized = false; + for (int i = 0; i < count; i++) { - room[i] = cut(lines, i) == null ? 1 : 2; - extra += room[i] - 1; + Cut c = cut(lines, i); + if (c != null) { + cuts[i] = c; + room[i] = 2; + extra++; + } else { + room[i] = 1; + LyricsLine norm = normalizeStandaloneBg(lines, i); + if (norm != null) { + normalizedBg[i] = norm; + anyNormalized = true; + } + } } - if (extra == 0) { + + if (extra == 0 && !anyNormalized) { return lyrics; } - final List split = new ArrayList<>(count + extra); + List split = new ArrayList<>(count + extra); for (int i = 0; i < count; i++) { - final Cut cut = cut(lines, i); - if (cut == null) { - split.add(lines.get(i)); + Cut c = cuts[i]; + if (c != null) { + split.add(c.sung()); + split.add(c.background()); + } else if (normalizedBg[i] != null) { + split.add(normalizedBg[i]); } else { - split.add(cut.sung()); - split.add(cut.background()); + split.add(lines.get(i)); } } - return new Lyrics(split, lyrics.providerName(), lyrics.synced(), - widen(lyrics.romanization(), count, room), - widen(lyrics.translations(), count, room), - widen(lyrics.romanizations(), count, room), - lyrics.songwriters(), lyrics.rawFormat(), lyrics.formatType(), - lyrics.sourceUrl(), lyrics.about()); + List finalLines = split; + if (lyrics.synced() && !finalLines.isEmpty()) { + finalLines = Lyrics.clampLastWordEnds(Lyrics.fixAnomalousWordTimestamps(finalLines)); + } + + return new Lyrics( + finalLines, + lyrics.providerName(), + lyrics.synced(), + widen(lyrics.romanization(), count, room, cuts, normalizedBg, true), + widenMap(lyrics.translations(), count, room, cuts, normalizedBg, false), + widenMap(lyrics.romanizations(), count, room, cuts, normalizedBg, true), + lyrics.songwriters(), + lyrics.rawFormat(), + lyrics.formatType(), + lyrics.sourceUrl(), + lyrics.about()); } - /** Reading brackets on top of a stated split would put two background lines under one sung line. */ + /** + * Splits a line sung partly in round brackets, half or full width, into the lead outside + * them and the background vocal inside. + */ @Nullable private static Cut cut(List lines, int index) { - final LyricsLine line = lines.get(index); - if (line.isBG() || line.text().trim().isEmpty()) { + LyricsLine line = lines.get(index); + String text = line.text(); + // A background line stays one: cutting it would turn the part outside the brackets into a lead. + if (line.isBG() || text.trim().isEmpty() || hasTagBrackets(text)) { return null; } - if (index + 1 < lines.size() && lines.get(index + 1).isBG()) { + + List runs = findSungBracketRuns(text); + if (runs.isEmpty()) { return null; } - return line.hasWords() ? cutWordTimed(line) : cutLineTimed(line); + + String outsideText = extractOutsideText(text, runs); + String insideText = extractInsideText(text, runs); + if (!sung(outsideText) || !sung(insideText)) { + return null; + } + + if (line.hasWords()) { + Cut wordCut = cutWordTimed(line, runs, outsideText, insideText); + if (wordCut != null) { + return wordCut; + } + } + return cutLineTimed(line, runs, outsideText, insideText); } + /** + * A line written whole in brackets, such as "(Ooh, yeah)", becomes a background line of the lead + * before it, and a background line loses the brackets it still carries in its text or words. + */ @Nullable - private static Cut cutWordTimed(LyricsLine line) { - final String text = line.text(); - final List words = line.words(); - // The renderer reads the words back out of the line text, so the two have to cover the - // same characters for the offsets below to mean anything. - if (lengthOf(words) != text.length()) { + private static LyricsLine normalizeStandaloneBg(List lines, int index) { + LyricsLine line = lines.get(index); + String raw = line.text(); + if (raw.trim().isEmpty() || hasTagBrackets(raw)) { + return null; + } + String trimmed = raw.trim(); + final boolean outerParens = hasBalancedOuterBrackets(trimmed); + if (!line.isBG() && (!outerParens || index == 0)) { return null; } + String strippedText = stripOuterBrackets(trimmed); + if (!sung(strippedText)) { + return null; + } + // A bracketed line with no lead above it is the lead itself. + if (!line.isBG()) { + boolean hasLeadBefore = false; + for (int p = index - 1; p >= 0; p--) { + LyricsLine prev = lines.get(p); + if (!prev.isBG() && !hasBalancedOuterBrackets(prev.text().trim())) { + hasLeadBefore = true; + break; + } + } + if (!hasLeadBefore) { + return null; + } + } - final Run head = headRun(text); - final Run tail = tailRun(text); - if (head == null && tail == null) { + List origWords = line.words(); + List cleanedWords = origWords.isEmpty() ? origWords : trimBrackets(origWords); + final boolean textChanged = !strippedText.equals(raw); + final boolean bgChanged = !line.isBG(); + boolean wordsChanged = cleanedWords.size() != origWords.size(); + if (!wordsChanged) { + for (int i = 0; i < cleanedWords.size(); i++) { + if (!cleanedWords.get(i).text().equals(origWords.get(i).text())) { + wordsChanged = true; + break; + } + } + } + if (!textChanged && !bgChanged && !wordsChanged) { return null; } - final int sungFrom = head == null ? 0 : head.close() + 1; - final int sungTo = tail == null ? text.length() : tail.open(); - if (sungFrom >= sungTo || !sung(text.substring(sungFrom, sungTo)) - || (head != null && !sung(text.substring(head.open(), head.close() + 1))) - || (tail != null && !sung(text.substring(tail.open(), tail.close() + 1)))) { + return new LyricsLine( + line.startTimeMs(), + line.endTimeMs(), + strippedText, + cleanedWords, + line.agentId(), + line.isDuet(), + true, + line.songPart()); + } + + @Nullable + private static Cut cutWordTimed(LyricsLine line, List runs, + String fallbackSungText, String fallbackBgText) { + String text = line.text(); + List words = line.words(); + if (words.isEmpty()) { return null; } - final List sung = new ArrayList<>(words.size()); - final List background = new ArrayList<>(words.size()); - int offset = 0; - for (Word word : words) { - final int from = offset; - final int to = offset + word.text().length(); - offset = to; + List sung = new ArrayList<>(words.size()); + List background = new ArrayList<>(words.size()); + int searchFrom = 0; - final int inSung = Math.max(0, Math.min(to, sungTo) - Math.max(from, sungFrom)); - int inBackground = 0; - if (head != null) { - inBackground += Math.max(0, Math.min(to, head.close() + 1) - from); + for (int wi = 0; wi < words.size(); wi++) { + Word word = words.get(wi); + String wText = word.text(); + if (wText == null || wText.isEmpty()) { + continue; + } + + while (searchFrom < text.length() && Character.isWhitespace(text.charAt(searchFrom))) { + searchFrom++; + } + + int found = text.indexOf(wText, searchFrom); + int matchLen = wText.length(); + if (found < 0) { + String trimmedW = wText.trim(); + if (!trimmedW.isEmpty()) { + found = text.indexOf(trimmedW, searchFrom); + matchLen = trimmedW.length(); + } } - if (tail != null) { - inBackground += Math.max(0, to - Math.max(from, tail.open())); + + final int from; + final int to; + if (found >= 0) { + from = found; + to = Math.min(text.length(), found + matchLen); + searchFrom = to; + } else { + from = Math.min(searchFrom, text.length()); + to = Math.min(text.length(), from + wText.trim().length()); + searchFrom = to; } + final boolean spaceAfterInText = to < text.length() && Character.isWhitespace(text.charAt(to)); + final boolean hasSpace = word.endsWithSpace() || spaceAfterInText; + + int inBackground = 0; + for (int ri = 0; ri < runs.size(); ri++) { + Run r = runs.get(ri); + inBackground += Math.max(0, Math.min(to, r.close() + 1) - Math.max(from, r.open())); + } + final int spanLen = Math.max(0, to - from); + final int inSung = Math.max(0, spanLen - inBackground); + if (inSung <= 0 && inBackground <= 0) { + // Fallback: if the word itself starts and ends with brackets, it belongs to background. + String wt = wText.trim(); + if (!wt.isEmpty() && (isOpen(wt.charAt(0)) || isClose(wt.charAt(wt.length() - 1)))) { + background.add(new Word(word.startMs(), word.endMs(), wText, word.romaji(), hasSpace)); + } else { + sung.add(new Word(word.startMs(), word.endMs(), wText, word.romaji(), hasSpace)); + } continue; } - // Halving a straddling word would cost it its timing, so it goes whole to the bigger side. - if (inSung > inBackground || (inSung == inBackground && inSung > 0)) { - sung.add(word); + + if (inBackground == 0) { + sung.add(new Word(word.startMs(), word.endMs(), wText, word.romaji(), hasSpace)); + } else if (inSung == 0) { + background.add(new Word(word.startMs(), word.endMs(), wText, word.romaji(), hasSpace)); } else { - background.add(word); + // Single Word token straddles a bracket boundary (e.g. "you(yeah)" or "(ooh)baby"). + StringBuilder sungPart = new StringBuilder(); + StringBuilder bgPart = new StringBuilder(); + boolean startsInBg = false; + for (int ci = from; ci < to && ci < text.length(); ci++) { + final boolean charInBg = isIndexInsideRuns(ci, runs); + if (ci == from) { + startsInBg = charInBg; + } + if (charInBg) { + bgPart.append(text.charAt(ci)); + } else { + sungPart.append(text.charAt(ci)); + } + } + String sStr = stripAllBrackets(sungPart.toString()).trim(); + String bStr = stripAllBrackets(bgPart.toString()).trim(); + final long wStart = word.startMs(); + final long wEnd = word.endMs(); + final long dur = (wStart != LyricsLine.NO_TIME && wEnd > wStart) ? (wEnd - wStart) : 0L; + final long splitTime = (dur > 0 && spanLen > 0) + ? wStart + (dur * (startsInBg ? inBackground : inSung)) / spanLen + : wStart; + + if (!sStr.isEmpty()) { + final long sStart = startsInBg ? splitTime : wStart; + final long sEnd = startsInBg ? wEnd : splitTime; + sung.add(new Word(sStart, sEnd, sStr, word.romaji(), !startsInBg || hasSpace)); + } + if (!bStr.isEmpty()) { + final long bStart = startsInBg ? wStart : splitTime; + final long bEnd = startsInBg ? splitTime : wEnd; + background.add(new Word(bStart, bEnd, bStr, null, startsInBg || hasSpace)); + } } } - return finish(line, sung, background); - } - - /** Both halves keep the line's one timing, there being none to divide. */ - @Nullable - private static Cut cutLineTimed(LyricsLine line) { - final String text = line.text(); + if (sung.isEmpty() || background.isEmpty()) { + return null; + } - final Run head = headRun(text); - final Run tail = tailRun(text); - if (head == null && tail == null) { + List trimmedSung = trimBrackets(sung); + List trimmedBackground = trimBrackets(background); + if (trimmedSung.isEmpty() || trimmedBackground.isEmpty()) { return null; } - final int sungFrom = head == null ? 0 : head.close() + 1; - final int sungTo = tail == null ? text.length() : tail.open(); - if (sungFrom >= sungTo) { + String sungText = textOf(trimmedSung); + String backgroundText = textOf(trimmedBackground); + if (sungText.trim().isEmpty()) { + sungText = fallbackSungText; + } + if (backgroundText.trim().isEmpty()) { + backgroundText = fallbackBgText; + } + if (!sung(sungText) || !sung(backgroundText)) { return null; } - final StringBuilder backgroundText = new StringBuilder(); - if (head != null) { - backgroundText.append(text, head.open() + 1, head.close()); + final long bgStart = firstValidStart(trimmedBackground, line.startTimeMs()); + final long bgEnd = lastValidEnd(trimmedBackground, line.endTimeMs()); + final long sungWordStart = firstValidStart(trimmedSung, line.startTimeMs()); + final long sungWordEnd = lastValidEnd(trimmedSung, line.endTimeMs()); + + // Parent sungLine's line-level [startTimeMs, endTimeMs] spans the full cluster so + // Lyrics.indexForPosition activates the parent line even when bgBeforeMain starts early, + // while sungLine.words() and bgLine.words() keep their exact individual word timings. + long parentStart = sungWordStart; + if (line.startTimeMs() != LyricsLine.NO_TIME && parentStart != LyricsLine.NO_TIME) { + parentStart = Math.min(line.startTimeMs(), parentStart); } - if (tail != null) { - if (backgroundText.length() > 0) { - backgroundText.append(' '); - } - backgroundText.append(text, tail.open() + 1, tail.close()); + if (bgStart != LyricsLine.NO_TIME && parentStart != LyricsLine.NO_TIME) { + parentStart = Math.min(parentStart, bgStart); + } + long parentEnd = sungWordEnd; + if (line.endTimeMs() != LyricsLine.NO_TIME && parentEnd != LyricsLine.NO_TIME) { + parentEnd = Math.max(line.endTimeMs(), parentEnd); + } + if (bgEnd != LyricsLine.NO_TIME && parentEnd != LyricsLine.NO_TIME) { + parentEnd = Math.max(parentEnd, bgEnd); } - return finishText(line, text.substring(sungFrom, sungTo), backgroundText.toString()); + LyricsLine sungLine = new LyricsLine( + parentStart, parentEnd, sungText, trimmedSung, + line.agentId(), line.isDuet(), false, line.songPart()); + LyricsLine bgLine = new LyricsLine( + bgStart, bgEnd, backgroundText, trimmedBackground, + line.agentId(), line.isDuet(), true, line.songPart()); + return new Cut(sungLine, bgLine); } @Nullable - private static Cut finish(LyricsLine line, List sung, List background) { - if (sung.isEmpty() || background.isEmpty()) { - return null; - } - final List trimmed = trimBrackets(sung); - final List trimmedBackground = trimBrackets(background); - final String sungText = textOf(trimmed); - final String backgroundText = textOf(trimmedBackground); - if (sungText.trim().isEmpty() || backgroundText.trim().isEmpty()) { - return null; - } - return new Cut(plain(line, sungText, trimmed, false), - plain(line, backgroundText, trimmedBackground, true)); + private static Cut cutLineTimed(LyricsLine line, List runs, + String outsideText, String insideText) { + String text = line.text(); + int firstNonSpace = 0; + while (firstNonSpace < text.length() && Character.isWhitespace(text.charAt(firstNonSpace))) { + firstNonSpace++; + } + final boolean headBeforeMain = runs.size() == 1 && runs.get(0).open() == firstNonSpace; + return finishText(line, outsideText, insideText, headBeforeMain); } @Nullable - private static Cut finishText(LyricsLine line, String sungText, String backgroundText) { - final String kept = sungText.trim(); - if (kept.isEmpty() || !sung(backgroundText)) { + private static Cut finishText(LyricsLine line, String sungText, String backgroundText, + boolean headBeforeMain) { + String kept = stripAllBrackets(sungText).trim(); + String bgKept = stripAllBrackets(backgroundText).trim(); + if (kept.isEmpty() || bgKept.isEmpty() || !sung(kept) || !sung(bgKept)) { return null; } - return new Cut(plain(line, kept, List.of(), false), - plain(line, backgroundText.trim(), List.of(), true)); + LyricsLine bgLine = headBeforeMain && line.startTimeMs() > 0 + ? new LyricsLine(line.startTimeMs() - 1L, line.endTimeMs(), + bgKept, List.of(), line.agentId(), + line.isDuet(), true, line.songPart()) + : plain(line, bgKept, List.of(), true); + return new Cut(plain(line, kept, List.of(), false), bgLine); } private static LyricsLine plain(LyricsLine line, String text, List words, boolean isBG) { @@ -198,73 +390,195 @@ private static LyricsLine plain(LyricsLine line, String text, List words, line.agentId(), line.isDuet(), isBG, line.songPart()); } - /** Whitespace goes first and last too, or a bracket touching the text is never reached. */ + private static long firstValidStart(List words, long fallback) { + for (int i = 0; i < words.size(); i++) { + final long s = words.get(i).startMs(); + if (s != LyricsLine.NO_TIME && s >= 0) { + return s; + } + } + return fallback; + } + + private static long lastValidEnd(List words, long fallback) { + for (int i = words.size() - 1; i >= 0; i--) { + final long e = words.get(i).endMs(); + if (e != LyricsLine.NO_TIME && e > 0) { + return e; + } + } + return fallback; + } + + /** + * Strips leading/trailing round brackets from every word while preserving {@code endsWithSpace()}. + */ private static List trimBrackets(List words) { - final List out = new ArrayList<>(words.size()); + List out = new ArrayList<>(words.size()); for (int i = 0; i < words.size(); i++) { - String body = words.get(i).text(); - if (i == 0) { - body = TextTrim.stripLeading(body); - while (isOpen(body.charAt(0))) { - body = TextTrim.stripLeading(body.substring(1)); - } + Word word = words.get(i); + String body = word.text(); + if (body == null) { + continue; } - if (i == words.size() - 1) { - body = TextTrim.stripTrailing(body); - while (!body.isEmpty() && isClose(body.charAt(body.length() - 1))) { - body = TextTrim.stripTrailing(body.substring(0, body.length() - 1)); - } + final boolean hadTrailingWhitespace = !body.isEmpty() + && Character.isWhitespace(body.charAt(body.length() - 1)); + body = TextTrim.stripLeading(body); + while (!body.isEmpty() && isOpen(body.charAt(0))) { + body = TextTrim.stripLeading(body.substring(1)); } + body = TextTrim.stripTrailing(body); + while (!body.isEmpty() && isClose(body.charAt(body.length() - 1))) { + body = TextTrim.stripTrailing(body.substring(0, body.length() - 1)); + } + // Also strip any stray internal round brackets left in a token. + body = stripAllBrackets(body).trim(); if (body.isEmpty()) { continue; } - final Word word = words.get(i); - out.add(new Word(word.startMs(), word.endMs(), body, - word.romaji(), body.endsWith(" "))); + String roma = word.romaji(); + if (roma != null && !roma.isEmpty()) { + roma = stripAllBrackets(roma).trim(); + if (roma.isEmpty()) { + roma = null; + } + } + final boolean endsWithSpace = word.endsWithSpace() || hadTrailingWhitespace; + out.add(new Word(word.startMs(), word.endMs(), body, roma, endsWithSpace)); + } + if (!out.isEmpty()) { + final int lastIdx = out.size() - 1; + Word last = out.get(lastIdx); + if (last.endsWithSpace()) { + out.set(lastIdx, new Word(last.startMs(), last.endMs(), + last.text(), last.romaji(), false)); + } } return out; } - /** A run running straight into a word is not a vocal of its own. */ - @Nullable - private static Run headRun(String text) { - int open = 0; - while (open < text.length() && Character.isWhitespace(text.charAt(open))) { - open++; + private static List findSungBracketRuns(String text) { + List runs = new ArrayList<>(2); + int i = 0; + final int len = text.length(); + while (i < len) { + final char c = text.charAt(i); + if (isOpen(c)) { + Run run = match(text, i, len, true); + if (run != null) { + String inner = text.substring(run.open() + 1, run.close()); + if (sung(inner)) { + runs.add(run); + i = run.close() + 1; + continue; + } + } + } + i++; } - if (open >= text.length() || !isOpen(text.charAt(open))) { - return null; + return runs; + } + + private static boolean isIndexInsideRuns(int charIndex, List runs) { + for (int i = 0; i < runs.size(); i++) { + Run r = runs.get(i); + if (charIndex >= r.open() && charIndex <= r.close()) { + return true; + } } - final Run run = match(text, open, text.length(), true); - if (run == null) { - return null; + return false; + } + + private static String extractOutsideText(String text, List runs) { + if (runs.isEmpty()) { + return text.trim(); } - return isWide(text.charAt(run.open())) - || run.close() + 1 == text.length() - || Character.isWhitespace(text.charAt(run.close() + 1)) ? run : null; + StringBuilder sb = new StringBuilder(text.length()); + int last = 0; + for (int i = 0; i < runs.size(); i++) { + Run r = runs.get(i); + if (r.open() > last) { + sb.append(text, last, r.open()); + } + last = r.close() + 1; + } + if (last < text.length()) { + sb.append(text, last, text.length()); + } + return sb.toString() + .replaceAll("\\s+([!?,.:;])", "$1") + .replaceAll("\\s{2,}", " ") + .trim(); } - @Nullable - private static Run tailRun(String text) { - int close = text.length(); - while (close > 0 && Character.isWhitespace(text.charAt(close - 1))) { - close--; + private static String extractInsideText(String text, List runs) { + if (runs.isEmpty()) { + return ""; + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < runs.size(); i++) { + Run r = runs.get(i); + String part = stripAllBrackets(text.substring(r.open() + 1, r.close())).trim(); + if (!part.isEmpty()) { + //noinspection SizeReplaceableByIsEmpty + if (sb.length() > 0) { + sb.append(' '); + } + sb.append(part); + } } - if (close == 0 || !isClose(text.charAt(close - 1))) { - return null; + return sb.toString().replaceAll("\\s{2,}", " ").trim(); + } + + /** Lenticular brackets hold a title or credit tag, never a vocal. */ + public static boolean hasTagBrackets(String text) { + return text.indexOf('【') >= 0 || text.indexOf('】') >= 0; + } + + /** A whole line in round brackets, which is how a background vocal is usually written. */ + public static boolean isBracketedVocal(@Nullable String text) { + return text != null && !hasTagBrackets(text) && hasBalancedOuterBrackets(text); + } + + public static boolean hasBalancedOuterBrackets(@Nullable String text) { + if (text == null) { + return false; } - final Run run = match(text, close - 1, 0, false); - if (run == null) { - return null; + String trimmed = text.trim(); + if (trimmed.length() < 2) { + return false; + } + final char first = trimmed.charAt(0); + if (!isOpen(first)) { + return false; + } + Run matched = match(trimmed, 0, trimmed.length(), true); + return matched != null && matched.close() == trimmed.length() - 1; + } + + public static String stripOuterBrackets(@Nullable String text) { + if (text == null || text.isEmpty()) { + return ""; + } + String trimmed = text.trim(); + while (trimmed.length() >= 2 && hasBalancedOuterBrackets(trimmed)) { + trimmed = trimmed.substring(1, trimmed.length() - 1).trim(); } - return isWide(text.charAt(run.open())) - || run.open() == 0 - || Character.isWhitespace(text.charAt(run.open() - 1)) ? run : null; + return trimmed; } - /** A full-width bracket stands on a cell of its own, so it needs no space around it. */ - private static boolean isWide(char c) { - return c == '(' || c == ')'; + public static String stripAllBrackets(@Nullable String text) { + if (text == null || text.isEmpty()) { + return ""; + } + StringBuilder sb = new StringBuilder(text.length()); + for (int i = 0; i < text.length(); i++) { + final char c = text.charAt(i); + if (!isOpen(c) && !isClose(c)) { + sb.append(c); + } + } + return sb.toString(); } /** Nesting is counted so an inner bracket does not close the run early; "(oh]" is no run at all. */ @@ -273,6 +587,9 @@ private static Run match(String text, int from, int toward, boolean forward) { final char first = text.charAt(from); final char opener = forward ? first : openerOf(first); final char closer = forward ? closerOf(first) : first; + if (opener == 0 || closer == 0) { + return null; + } // Scanning back starts on the closing bracket, so that one is the deeper side. final char deeper = forward ? opener : closer; final char shallower = forward ? closer : opener; @@ -302,19 +619,19 @@ private static boolean isClose(char c) { } private static char closerOf(char open) { - switch (open) { - case '(': return ')'; - case '(': return ')'; - default: return 0; - } + return switch (open) { + case '(' -> ')'; + case '(' -> ')'; + default -> 0; + }; } private static char openerOf(char close) { - switch (close) { - case ')': return '('; - case ')': return '('; - default: return 0; - } + return switch (close) { + case ')' -> '('; + case ')' -> '('; + default -> 0; + }; } /** A repeat count or section name is bracketed too, but it is not sung. */ @@ -322,7 +639,7 @@ private static boolean sung(String text) { if (text == null) { return false; } - final String body = text.trim(); + String body = text.trim(); if (body.isEmpty() || NOT_SUNG_RUN.matcher(body).matches()) { return false; } @@ -334,49 +651,133 @@ private static boolean sung(String text) { return false; } + private static boolean isContinuousCjk(@Nullable String s) { + if (s == null || s.isEmpty()) { + return false; + } + final int cp = s.codePointBefore(s.length()); + Character.UnicodeBlock b = Character.UnicodeBlock.of(cp); + return b == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS + || b == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_A + || b == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_B + || b == Character.UnicodeBlock.CJK_COMPATIBILITY_IDEOGRAPHS + || b == Character.UnicodeBlock.CJK_SYMBOLS_AND_PUNCTUATION + || b == Character.UnicodeBlock.HIRAGANA + || b == Character.UnicodeBlock.KATAKANA; + } + private static String textOf(List words) { - final StringBuilder text = new StringBuilder(); - for (Word word : words) { - text.append(word.text()); + StringBuilder text = new StringBuilder(); + for (int i = 0; i < words.size(); i++) { + Word word = words.get(i); + String wText = word.text(); + if (wText == null || wText.isEmpty()) { + continue; + } + text.append(wText); + if (i < words.size() - 1 && !wText.endsWith(" ")) { + String nextText = words.get(i + 1).text(); + final boolean cjkPair = isContinuousCjk(wText) && isContinuousCjk(nextText); + if (word.endsWithSpace() || !cjkPair) { + text.append(' '); + } + } } - return text.toString(); + return text.toString().trim(); } - private static int lengthOf(List words) { - int total = 0; - for (Word word : words) { - total += word.text().length(); + @Nullable + private static String synthesizeRomajiFromWords(List words) { + if (words == null || words.isEmpty()) { + return null; + } + StringBuilder sb = new StringBuilder(); + boolean any = false; + for (int i = 0; i < words.size(); i++) { + Word w = words.get(i); + String r = w.romaji(); + if (r != null && !r.trim().isEmpty()) { + any = true; + sb.append(r.trim()); + if (i < words.size() - 1 && !r.endsWith(" ")) { + sb.append(' '); + } + } } - return total; + return any ? sb.toString().trim() : null; } /** A track written against other lines stays as it is: shorter than the lyrics is the mismatch * the readers already check for before trusting one. */ @Nullable - private static List widen(@Nullable List aligned, int count, - int[] room) { + private static List widen( + @Nullable List aligned, + int count, + int[] room, + Cut[] cuts, + LyricsLine[] normalizedBg, + boolean isRomanization) { if (aligned == null || aligned.isEmpty() || aligned.size() != count) { return aligned; } - final List out = new ArrayList<>(count); + List out = new ArrayList<>(count + 8); for (int i = 0; i < count; i++) { - out.add(aligned.get(i)); - for (int slot = 1; slot < room[i]; slot++) { + LyricsLine item = aligned.get(i); + Cut mainCut = cuts[i]; + if (room[i] == 2 && mainCut != null) { + String itemText = item != null && item.text() != null ? item.text() : ""; + List itemRuns = findSungBracketRuns(itemText); + if (!itemRuns.isEmpty()) { + String outPart = extractOutsideText(itemText, itemRuns); + String inPart = extractInsideText(itemText, itemRuns); + if (!outPart.isEmpty() && !inPart.isEmpty()) { + out.add(new LyricsLine(item.startTimeMs(), outPart)); + out.add(new LyricsLine(item.startTimeMs(), inPart)); + continue; + } + } + if (isRomanization) { + String sungRoma = synthesizeRomajiFromWords(mainCut.sung().words()); + String bgRoma = synthesizeRomajiFromWords(mainCut.background().words()); + if (sungRoma != null || bgRoma != null) { + out.add(new LyricsLine(LyricsLine.NO_TIME, + sungRoma != null ? sungRoma : stripAllBrackets(itemText).trim())); + out.add(new LyricsLine(LyricsLine.NO_TIME, + bgRoma != null ? bgRoma : "")); + continue; + } + } + out.add(item); out.add(BLANK); + } else if (normalizedBg[i] != null && item != null && item.text() != null) { + String cleaned = stripOuterBrackets(item.text()); + out.add(cleaned.equals(item.text()) + ? item + : new LyricsLine(item.startTimeMs(), item.endTimeMs(), cleaned, + item.words() != null ? trimBrackets(item.words()) : List.of(), + item.agentId(), item.isDuet(), true, item.songPart())); + } else { + out.add(item); } } return out; } @Nullable - private static Map> widen(@Nullable Map> tracks, - int count, int[] room) { + private static Map> widenMap( + @Nullable Map> tracks, + int count, + int[] room, + Cut[] cuts, + LyricsLine[] normalizedBg, + boolean isRomanization) { if (tracks == null || tracks.isEmpty()) { return tracks; } - final Map> out = new HashMap<>(2 * tracks.size()); + Map> out = new HashMap<>(2 * tracks.size()); for (Map.Entry> entry : tracks.entrySet()) { - out.put(entry.getKey(), widen(entry.getValue(), count, room)); + out.put(entry.getKey(), + widen(entry.getValue(), count, room, cuts, normalizedBg, isRomanization)); } return out; } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/LyricsLine.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/LyricsLine.java index a012d83e86..b9a442a0ac 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/LyricsLine.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/LyricsLine.java @@ -128,10 +128,10 @@ public boolean hasWordTimings() { } if (words.size() == 1) { Word single = words.get(0); - if (compactText(single.text()).equalsIgnoreCase(compactText(text))) { + if (single.endMs() <= single.startMs()) { return false; } - if (single.endMs() <= single.startMs()) { + if (hasMultipleWords(text) && compactText(single.text()).equalsIgnoreCase(compactText(text))) { return false; } } @@ -139,7 +139,7 @@ public boolean hasWordTimings() { } private static String compactText(String value) { - final StringBuilder builder = new StringBuilder(value.length()); + StringBuilder builder = new StringBuilder(value.length()); for (int i = 0; i < value.length(); i++) { final char c = value.charAt(i); if (!Character.isWhitespace(c)) { @@ -149,6 +149,24 @@ private static String compactText(String value) { return builder.toString(); } + // ponytail: whitespace word counting; upgrade to BreakIterator if space-less scripts mix dummy spans. + private static boolean hasMultipleWords(String value) { + boolean inWord = false; + int wordCount = 0; + for (int i = 0; i < value.length(); i++) { + if (Character.isWhitespace(value.charAt(i))) { + inWord = false; + } else if (!inWord) { + inWord = true; + wordCount++; + if (wordCount > 1) { + return true; + } + } + } + return false; + } + @NonNull @Override public String toString() { diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/LyricsMerge.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/LyricsMerge.java index d08f8b2fb3..d016a495b0 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/LyricsMerge.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/LyricsMerge.java @@ -16,7 +16,6 @@ import java.util.Map; import java.util.function.Function; -import app.morphe.extension.music.patches.lyrics.model.Lyrics; import app.morphe.extension.shared.Logger; import app.morphe.extension.shared.Utils; import app.morphe.extension.shared.translation.TextTranslator; @@ -65,7 +64,7 @@ public static String textAt(@Nullable List track, int index) { if (track == null || index < 0 || index >= track.size()) { return ""; } - final String text = track.get(index).text(); + String text = track.get(index).text(); return text == null ? "" : text.trim(); } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/LyricsTextFormatter.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/LyricsTextFormatter.java index ad16902d63..78c64699b6 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/LyricsTextFormatter.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/LyricsTextFormatter.java @@ -20,8 +20,8 @@ public int offset(int original) { private LyricsTextFormatter() { } public static Result format(String input, boolean capitalize, boolean strip) { - final String text = input == null ? "" : input; - final boolean[] remove = new boolean[text.length()]; + String text = input == null ? "" : input; + boolean[] remove = new boolean[text.length()]; if (strip) { int lineStart = 0; while (lineStart <= text.length()) { @@ -41,15 +41,15 @@ public static Result format(String input, boolean capitalize, boolean strip) { lineStart = end + 1; } } - final StringBuilder out = new StringBuilder(text.length()); - final int[] offsets = new int[text.length() + 1]; + StringBuilder out = new StringBuilder(text.length()); + int[] offsets = new int[text.length() + 1]; boolean firstCased = true; for (int at = 0; at < text.length();) { final int cp = text.codePointAt(at); final int end = at + Character.charCount(cp); for (int i = at; i < end; i++) offsets[i] = out.length(); if (!remove[at]) { - final String own = text.substring(at, end); + String own = text.substring(at, end); if (capitalize && firstCased && (Character.isLowerCase(cp) || Character.isUpperCase(cp) || Character.isTitleCase(cp))) { out.append(own.toUpperCase(Locale.ROOT)); @@ -79,7 +79,7 @@ private static boolean isPunctuation(int cp) { private static boolean isRedundant(int cp) { if (Character.getType(cp) != Character.OTHER_PUNCTUATION || Character.isMirrored(cp) || cp == '\'' || cp == '"') return false; - final String name = Character.getName(cp); + String name = Character.getName(cp); if (name == null || name.contains("QUESTION") || name.contains("EXCLAMATION") || name.contains("INTERROBANG") || name.contains("ELLIPSIS") || name.contains("QUOTATION") || name.contains("APOSTROPHE")) return false; @@ -89,7 +89,7 @@ private static boolean isRedundant(int cp) { } private static boolean isEllipsisDot(String text, int start, int end, int cp) { - final String name = Character.getName(cp); + String name = Character.getName(cp); if (cp != 0x2024 && (name == null || !name.contains("FULL STOP"))) return false; return (start > 0 && text.codePointBefore(start) == cp) || (end < text.length() && text.codePointAt(end) == cp); diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/MetadataCleaner.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/MetadataCleaner.java index 6084bc53cd..222b98bbb1 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/MetadataCleaner.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/MetadataCleaner.java @@ -147,7 +147,7 @@ static String applyRegex(String input, String regex) { if (regex == null || regex.trim().isEmpty()) { return input; } - final Pattern pattern = compiled(regex); + Pattern pattern = compiled(regex); if (pattern == null) { return input; } @@ -170,12 +170,12 @@ public static String applyRegexPreserveOriginal(String input, String regex) { if (regex == null || regex.trim().isEmpty()) { return input; } - final Pattern pattern = compiled(regex); + Pattern pattern = compiled(regex); if (pattern == null) { return input; } try { - final String normalized = CharactersConverter.normalizePreserveCase(input); + String normalized = CharactersConverter.normalizePreserveCase(input); if (!pattern.matcher(normalized).find()) { return input; } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/MiniLyricsFormatter.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/MiniLyricsFormatter.java index d34fc81284..432b3399ab 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/MiniLyricsFormatter.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/MiniLyricsFormatter.java @@ -110,15 +110,15 @@ public static String trackLabel(@Nullable String template, @Nullable String titl */ public static String trackLabel(@Nullable String template, @Nullable String title, @Nullable String artist, @Nullable String variant) { - final Map values = new HashMap<>(); + Map values = new HashMap<>(); values.put("t", title == null ? "" : title); values.put("a", artist == null ? "" : artist); values.put("v", variant == null ? "" : variant.trim()); - final String rendered = render(template == null ? DEFAULT_TRACK_FORMAT : template, values, true); - final StringBuilder out = new StringBuilder(); + String rendered = render(template == null ? DEFAULT_TRACK_FORMAT : template, values, true); + StringBuilder out = new StringBuilder(); for (String line : LINE_BREAK.split(rendered, -1)) { // A hidden section leaves the spaces around it behind. - final String tidy = REPEATED_SPACES.matcher(line).replaceAll(" ").trim(); + String tidy = REPEATED_SPACES.matcher(line).replaceAll(" ").trim(); if (tidy.isEmpty()) continue; if (out.length() > 0) out.append('\n'); out.append(tidy); @@ -134,8 +134,8 @@ public static String trackLabel(@Nullable String template, @Nullable String titl public static List trackFrames(@Nullable String template, @Nullable String title, @Nullable String artist, @Nullable String variant, int limit, double fullWidthMultiplier) { - final List frames = new ArrayList<>(); - final String label = trackLabel(template, title, artist, variant); + List frames = new ArrayList<>(); + String label = trackLabel(template, title, artist, variant); for (String line : label.split("\n")) { if (line.isEmpty()) continue; if (limit <= 0 || width(line, fullWidthMultiplier) <= limit) { @@ -155,17 +155,17 @@ private static void splitFrame(String frame, int limit, double fullWidthMultipli frames.add(frame); return; } - final List halves = chunks(new LyricsLine(0, frame), -1, limit, false, false, fullWidthMultiplier); + List halves = chunks(new LyricsLine(0, frame), -1, limit, false, false, fullWidthMultiplier); if (halves.size() < 2) { frames.add(frame); return; } for (Chunk half : halves) { - final String text = half.text(); + String text = half.text(); if (text.isEmpty()) continue; // An ellipsis added at a split does not count against the limit, or a two-character // part would never fit a limit of two. - final String bare = text.endsWith(SPLIT_ELLIPSIS) + String bare = text.endsWith(SPLIT_ELLIPSIS) ? text.substring(0, text.length() - SPLIT_ELLIPSIS.length()) : text; if (bare.equals(frame) || width(bare, fullWidthMultiplier) <= limit) { frames.add(text); @@ -175,7 +175,7 @@ private static void splitFrame(String frame, int limit, double fullWidthMultipli splitFrame(bare, limit, fullWidthMultiplier, frames, depth + 1); if (!bare.equals(text) && frames.size() > before) { final int last = frames.size() - 1; - final String lastFrame = frames.get(last); + String lastFrame = frames.get(last); if (!lastFrame.endsWith(SPLIT_ELLIPSIS)) frames.set(last, lastFrame + SPLIT_ELLIPSIS); } } @@ -187,7 +187,7 @@ private static void splitFrame(String frame, int limit, double fullWidthMultipli */ private static String replacePlaceholders(String template, @Nullable String title, @Nullable String artist, @Nullable String lyric) { - final StringBuilder out = new StringBuilder(); + StringBuilder out = new StringBuilder(); for (int i = 0, length = template.length(); i < length; ) { if (template.startsWith("{t}", i)) { out.append(title == null ? "" : title); @@ -210,8 +210,8 @@ private static String replacePlaceholders(String template, @Nullable String titl * the count is above 1; a backslash before a colon keeps it literal. */ public static String encapsulate(@Nullable String template, String lyric, int repeat) { - final String source = validEncapsulation(template) ? template : DEFAULT_ENCAPSULATION; - final Map values = new HashMap<>(); + String source = validEncapsulation(template) ? template : DEFAULT_ENCAPSULATION; + Map values = new HashMap<>(); values.put("lrc", lyric); values.put("n", String.valueOf(repeat)); return render(source, values, repeat > 1); @@ -224,8 +224,8 @@ public static String encapsulate(@Nullable String template, String lyric, int re * {@code bareShown} is set. A backslash before a colon keeps it literal. */ static String render(String template, Map values, boolean bareShown) { - final StringBuilder out = new StringBuilder(); - final StringBuilder segment = new StringBuilder(); + StringBuilder out = new StringBuilder(); + StringBuilder segment = new StringBuilder(); boolean inside = false; boolean populated = true; boolean placeholders = false; @@ -245,9 +245,9 @@ static String render(String template, Map values, boolean bareSh i++; continue; } - final String name = c == '{' ? placeholderAt(template, i, values) : null; + String name = c == '{' ? placeholderAt(template, i, values) : null; if (name != null) { - final String value = values.get(name); + String value = values.get(name); if (inside) { placeholders = true; if (!isPopulated(name, value)) populated = false; @@ -278,7 +278,7 @@ private static boolean isPopulated(String name, @Nullable String value) { /** How many times in a row the line at the index is sung, counting up to and including it. */ public static int repeatCount(List lines, int index) { - final String text = lines.get(index).text(); + String text = lines.get(index).text(); int count = 1; for (int i = index - 1; i >= 0 && lines.get(i).text().equals(text); i--) { count++; @@ -293,7 +293,7 @@ public static int repeatCount(List lines, int index) { public static double width(String text, double fullWidthMultiplier) { final double wide = Double.isFinite(fullWidthMultiplier) && fullWidthMultiplier > 0 ? fullWidthMultiplier : 1; - final Matcher matcher = GRAPHEME.matcher(text); + Matcher matcher = GRAPHEME.matcher(text); double total = 0; while (matcher.find()) total += isFullWidth(text.codePointAt(matcher.start())) ? wide : 1; return total; @@ -349,7 +349,7 @@ static boolean isFullWidth(int cp) { } public static int characterCount(String text) { - final Matcher matcher = GRAPHEME.matcher(text); + Matcher matcher = GRAPHEME.matcher(text); int count = 0; while (matcher.find()) count++; return count; @@ -383,15 +383,15 @@ public static List chunks(LyricsLine line, long intervalEndMs, int limit, */ public static List chunks(LyricsLine line, long intervalEndMs, int limit, boolean capitalize, boolean strip, double fullWidthMultiplier) { - final LyricsTextFormatter.Result cleaned = LyricsTextFormatter.format(line.text(), false, strip); - final String text = cleaned.text(); + LyricsTextFormatter.Result cleaned = LyricsTextFormatter.format(line.text(), false, strip); + String text = cleaned.text(); if (limit <= 0 || width(text, fullWidthMultiplier) <= limit) { return List.of(new Chunk(LyricsTextFormatter.format(text, capitalize, false).text(), line.startTimeMs())); } - final List timed = timedUnits(line, cleaned); - final List units = timed.isEmpty() ? untimedUnits(text) : timed; - final List groups = new ArrayList<>(2); + List timed = timedUnits(line, cleaned); + List units = timed.isEmpty() ? untimedUnits(text) : timed; + List groups = new ArrayList<>(2); final int middle = (units.size() + 1) / 2; if (middle > 0) { groups.add(new Unit(units.get(0).start(), units.get(middle - 1).end(), units.get(0).timeMs())); @@ -400,11 +400,11 @@ public static List chunks(LyricsLine line, long intervalEndMs, int limit, groups.add(new Unit(units.get(middle).start(), units.get(units.size() - 1).end(), units.get(middle).timeMs())); } - final List result = new ArrayList<>(groups.size()); + List result = new ArrayList<>(groups.size()); final int total = Math.max(1, characterCount(text)); long previousTime = line.startTimeMs(); for (int i = 0; i < groups.size(); i++) { - final Unit group = groups.get(i); + Unit group = groups.get(i); long start = group.timeMs(); if (i == 0) start = line.startTimeMs(); else if (start < line.startTimeMs() || start < previousTime) { @@ -436,7 +436,7 @@ public static long selectionPosition(long playbackMs, int offsetMs) { */ public static String instrumental(@Nullable String template, long startMs, long endMs, long positionMs, @Nullable String title, @Nullable String artist, @Nullable String lastLyric) { - final String[] frames = LINE_BREAK.split(template == null ? DEFAULT_INSTRUMENTAL : template, -1); + String[] frames = LINE_BREAK.split(template == null ? DEFAULT_INSTRUMENTAL : template, -1); final long duration = endMs - startMs; final int index = endMs == Long.MAX_VALUE || duration <= 0 ? 0 : (int) Math.min(frames.length - 1, @@ -446,11 +446,11 @@ public static String instrumental(@Nullable String template, long startMs, long private static List timedUnits(LyricsLine line, LyricsTextFormatter.Result cleaned) { if (!line.hasWordTimings()) return List.of(); - final List units = new ArrayList<>(); + List units = new ArrayList<>(); int cursor = 0; final boolean spaced = hasWhitespace(line.text()); for (Word word : line.words()) { - final String own = word.text().trim(); + String own = word.text().trim(); if (own.isEmpty()) continue; final int start = line.text().indexOf(own, cursor); if (start < 0 || word.startMs() < 0) return List.of(); @@ -459,7 +459,7 @@ private static List timedUnits(LyricsLine line, LyricsTextFormatter.Result return List.of(); } final int end = start + own.length(); - final Unit next = new Unit(cleaned.offset(start), cleaned.offset(end), word.startMs()); + Unit next = new Unit(cleaned.offset(start), cleaned.offset(end), word.startMs()); if (!units.isEmpty() && spaced && !hasWhitespace(line.text().substring(cursor, start))) { Unit previous = units.remove(units.size() - 1); units.add(new Unit(previous.start(), next.end(), previous.timeMs())); @@ -474,9 +474,9 @@ private static List timedUnits(LyricsLine line, LyricsTextFormatter.Result || hasLetterOrDigit(cleaned.text().substring(units.get(units.size() - 1).end()))) { return List.of(); } - final List covering = new ArrayList<>(units.size()); + List covering = new ArrayList<>(units.size()); for (int i = 0; i < units.size(); i++) { - final Unit unit = units.get(i); + Unit unit = units.get(i); int end = i + 1 < units.size() ? units.get(i + 1).start() : cleaned.text().length(); while (end > unit.end()) { final int cp = cleaned.text().codePointBefore(end); @@ -502,16 +502,16 @@ private static boolean hasWhitespace(String text) { * dictionary words are the units. Punctuation joins the word before it. */ private static List untimedUnits(String text) { - final List units = new ArrayList<>(); + List units = new ArrayList<>(); final boolean spaced = hasWhitespace(text); - final BreakIterator words = BreakIterator.getWordInstance(Locale.ROOT); + BreakIterator words = BreakIterator.getWordInstance(Locale.ROOT); words.setText(text); int start = words.first(); // Opening punctuation is held back and joins the word after it. int opening = -1; int worded = 0; for (int end = words.next(); end != BreakIterator.DONE; start = end, end = words.next()) { - final String piece = text.substring(start, end); + String piece = text.substring(start, end); if (!hasNonWhitespace(piece)) continue; final boolean letters = hasLetterOrDigit(piece); final boolean joined = start > 0 && !isWhitespace(text.codePointBefore(start)); @@ -522,7 +522,7 @@ private static List untimedUnits(String text) { final int from = opening >= 0 ? opening : start; opening = -1; if (!units.isEmpty() && joined && from == start && (spaced || !letters)) { - final Unit previous = units.remove(units.size() - 1); + Unit previous = units.remove(units.size() - 1); units.add(new Unit(previous.start(), end, -1)); } else { units.add(new Unit(from, end, -1)); @@ -532,14 +532,14 @@ private static List untimedUnits(String text) { if (opening >= 0) { if (units.isEmpty()) units.add(new Unit(opening, text.length(), -1)); else { - final Unit previous = units.remove(units.size() - 1); + Unit previous = units.remove(units.size() - 1); units.add(new Unit(previous.start(), text.length(), -1)); } } if (worded < 2 && !spaced) { // One dictionary word longer than the limit: fall back to characters. units.clear(); - final Matcher matcher = GRAPHEME.matcher(text); + Matcher matcher = GRAPHEME.matcher(text); while (matcher.find()) units.add(new Unit(matcher.start(), matcher.end(), -1)); } return units; diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/TrackVersion.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/TrackVersion.java index 47f570c2be..b65a581955 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/TrackVersion.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/TrackVersion.java @@ -35,7 +35,7 @@ public static TrackInfo query(TrackInfo track, @Nullable String rawTitle, @Nulla public static String clean(String version) { String s = version.strip(); while (!s.isEmpty()) { - final String core = core(s); + String core = core(s); final int first = s.codePointAt(0); if (isEdgePunctuation(first) && !hasPartner(first, core)) { s = s.substring(Character.charCount(first)).strip(); @@ -79,7 +79,7 @@ private static boolean isEdgePunctuation(int cp) { } private static boolean hasPartner(int cp, String core) { - final String symbol = new String(Character.toChars(cp)); + String symbol = new String(Character.toChars(cp)); for (PairedPunctuation.Pair pair : PairedPunctuation.PAIRS) { if (pair.opening().equals(symbol) && core.contains(pair.closing()) || pair.closing().equals(symbol) && core.contains(pair.opening())) { @@ -95,8 +95,8 @@ private static boolean hasPartner(int cp, String core) { */ public static boolean matchesStructuredTitle(String displayed, String full, String shortTitle, String version) { if (shortTitle.isBlank()) return false; - final String wanted = normalize(displayed); - final String variant = clean(version); + String wanted = normalize(displayed); + String variant = clean(version); if (variant.isEmpty()) { return wanted.equals(normalize(shortTitle)) && normalize(full).equals(normalize(shortTitle)); } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/parsers/LRCParser.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/parsers/LRCParser.java index 57d2fe0eaf..7dc6516521 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/parsers/LRCParser.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/parsers/LRCParser.java @@ -477,7 +477,7 @@ private static BodyParse parseBody(String body, long lineStartMs) { * between two of them. Dropped, it would be gone from the next write. */ private static void addWord(List words, long startMs, String written) { - final String body = written.trim(); + String body = written.trim(); if (body.isEmpty()) { markSpaceAfter(words); return; @@ -493,7 +493,7 @@ private static void markSpaceAfter(List words) { if (words.isEmpty()) { return; } - final Word previous = words.get(words.size() - 1); + Word previous = words.get(words.size() - 1); if (!previous.endsWithSpace()) { words.set(words.size() - 1, new Word(previous.startMs(), previous.endMs(), previous.text(), previous.romaji(), true)); @@ -557,11 +557,11 @@ public static String formatLine(LyricsLine line) { builder.append("[duet]"); } if (line.hasWords()) { - final List words = line.words(); - final String marks = spacesAfter(line.text(), words); + List words = line.words(); + String marks = spacesAfter(line.text(), words); Word lastWord = null; for (int i = 0; i < words.size(); i++) { - final Word word = words.get(i); + Word word = words.get(i); builder.append('<') .append(formatWordTimestamp(word.startMs())) .append('>') @@ -591,11 +591,11 @@ public static String formatLine(LyricsLine line) { * where it can be placed; a word it does not hold keeps the flag it came in with. */ private static String spacesAfter(String text, List words) { - final StringBuilder marks = new StringBuilder(words.size()); + StringBuilder marks = new StringBuilder(words.size()); int at = 0; for (int i = 0; i < words.size(); i++) { - final Word word = words.get(i); - final String body = word.text(); + Word word = words.get(i); + String body = word.text(); if (body.isEmpty()) { marks.append(word.endsWithSpace() ? SEPARATED : '0'); continue; @@ -619,14 +619,14 @@ private static String spacesAfter(String text, List words) { * writing the space again would double it. */ private static boolean carriesOwnSpace(List words, int index) { - final String body = words.get(index).text(); + String body = words.get(index).text(); if (Character.isWhitespace(body.charAt(body.length() - 1))) { return true; } if (index + 1 >= words.size()) { return false; } - final String next = words.get(index + 1).text(); + String next = words.get(index + 1).text(); return !next.isEmpty() && Character.isWhitespace(next.charAt(0)); } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/parsers/LyricifyParser.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/parsers/LyricifyParser.java index f0bdfd43b3..bc32c0ad9f 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/parsers/LyricifyParser.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/parsers/LyricifyParser.java @@ -38,22 +38,22 @@ public static List parseLines(String text, int offsetMs) { return Collections.emptyList(); } - final List lines = new ArrayList<>(); + List lines = new ArrayList<>(); for (String raw : text.split("\\r?\\n")) { - final String line = raw.trim(); + String line = raw.trim(); if (line.isEmpty()) { continue; } - final Matcher m = LINE_TIMING.matcher(line); + Matcher m = LINE_TIMING.matcher(line); if (!m.matches()) { continue; } final long startMs = Long.parseLong(Objects.requireNonNull(m.group(1))) + offsetMs; final long endMs = Long.parseLong(Objects.requireNonNull(m.group(2))) + offsetMs; - final String body = Objects.requireNonNull(m.group(3)).trim(); + String body = Objects.requireNonNull(m.group(3)).trim(); if (body.isEmpty()) { continue; } @@ -74,15 +74,15 @@ public static List parseSyllable(String text, int offsetMs) { return Collections.emptyList(); } - final List lines = new ArrayList<>(); + List lines = new ArrayList<>(); for (String raw : text.split("\\r?\\n")) { - final String line = raw.trim(); + String line = raw.trim(); if (line.isEmpty()) { continue; } - final String body; + String body; if (line.startsWith("[")) { final int close = line.indexOf(']'); body = (close > 0) ? line.substring(close + 1) : line; @@ -90,12 +90,12 @@ public static List parseSyllable(String text, int offsetMs) { body = line; } - final List words = parseSyllableWords(body, offsetMs); + List words = parseSyllableWords(body, offsetMs); if (words.isEmpty()) { continue; } - final String fullText = joinWordTexts(words); + String fullText = joinWordTexts(words); final long startMs = words.get(0).startMs(); final long endMs = words.get(words.size() - 1).endMs(); lines.add(new LyricsLine(startMs, endMs, fullText, words)); @@ -110,11 +110,11 @@ public static List parseSyllable(String text, int offsetMs) { } private static List parseSyllableWords(String body, int offsetMs) { - final List words = new ArrayList<>(); - final Matcher m = WORD_TIMING.matcher(body); + List words = new ArrayList<>(); + Matcher m = WORD_TIMING.matcher(body); while (m.find()) { - final String wordText = Objects.requireNonNull(m.group(1)); + String wordText = Objects.requireNonNull(m.group(1)); final long startMs = Long.parseLong(Objects.requireNonNull(m.group(2))) + offsetMs; final long durMs = Long.parseLong(Objects.requireNonNull(m.group(3))); final long endMs = startMs + durMs; @@ -132,7 +132,7 @@ private static List parseSyllableWords(String body, int offsetMs) { } private static String joinWordTexts(List words) { - final StringBuilder sb = new StringBuilder(); + StringBuilder sb = new StringBuilder(); for (Word w : words) { sb.append(w.text()); } @@ -144,22 +144,22 @@ public static List parseTranslation(String trans, int offsetMs) { return Collections.emptyList(); } - final List entries = new ArrayList<>(); + List entries = new ArrayList<>(); for (String raw : trans.split("\\r?\\n")) { - final String line = raw.trim(); + String line = raw.trim(); if (line.isEmpty()) { continue; } - final Matcher m = LRC_TIMESTAMP.matcher(line); + Matcher m = LRC_TIMESTAMP.matcher(line); if (!m.matches()) { continue; } final long min = Long.parseLong(Objects.requireNonNull(m.group(1))); - final String secPart = Objects.requireNonNull(m.group(2)); - final String text = Objects.requireNonNull(m.group(3)).trim(); + String secPart = Objects.requireNonNull(m.group(2)); + String text = Objects.requireNonNull(m.group(3)).trim(); if (text.isEmpty()) { continue; } @@ -174,7 +174,7 @@ public static List parseTranslation(String trans, int offsetMs) { entries.sort(Comparator.comparingLong(TranslatedEntry::timeMs)); - final List result = new ArrayList<>(entries.size()); + List result = new ArrayList<>(entries.size()); for (TranslatedEntry e : entries) { result.add(e.text()); } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/parsers/TTMLParser.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/parsers/TTMLParser.java index c974bf576a..fe1b6cf9af 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/parsers/TTMLParser.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/parsers/TTMLParser.java @@ -80,9 +80,9 @@ private static HeadMetadata parseHeadMetadata(String ttml) { Map sidecarTrans = new HashMap<>(); try { - final XmlPullParserFactory factory = XmlPullParserFactory.newInstance(); + XmlPullParserFactory factory = XmlPullParserFactory.newInstance(); factory.setNamespaceAware(true); - final XmlPullParser p = factory.newPullParser(); + XmlPullParser p = factory.newPullParser(); p.setInput(new StringReader(ttml)); boolean inHead = false; @@ -96,7 +96,7 @@ private static HeadMetadata parseHeadMetadata(String ttml) { int event = p.getEventType(); while (event != XmlPullParser.END_DOCUMENT) { if (event == XmlPullParser.START_TAG) { - final String local = localName(p.getName()); + String local = localName(p.getName()); if ("head".equals(local)) { inHead = true; } else if (inHead) { @@ -109,7 +109,7 @@ private static HeadMetadata parseHeadMetadata(String ttml) { // one would leave songwritersDepth above zero at // and latch inSongwriters, which swallows // the transliterations below. - final String name = readTextContent(p); + String name = readTextContent(p); if (!name.trim().isEmpty()) { songwriters.add(name.trim()); } @@ -117,8 +117,8 @@ private static HeadMetadata parseHeadMetadata(String ttml) { songwritersDepth++; } } else if ("meta".equals(local) && NS_AMLL.equals(p.getNamespace())) { - final String key = getAttr(p, NS_AMLL, "key", "amll:key"); - final String value = getAttr(p, NS_AMLL, "value", "amll:value"); + String key = getAttr(p, NS_AMLL, "key", "amll:key"); + String value = getAttr(p, NS_AMLL, "value", "amll:value"); if (key != null && value != null && !key.isEmpty() && !value.isEmpty() && !AMLL_EXCLUDED_KEYS.contains(key)) { amllCredits.add(key + ": " + value.trim()); @@ -132,7 +132,7 @@ private static HeadMetadata parseHeadMetadata(String ttml) { agentName = null; inAgent = true; } else if (inAgent && "name".equals(local)) { - final String name = readTextContent(p); + String name = readTextContent(p); if (!name.trim().isEmpty()) { agentName = name.trim(); } @@ -143,7 +143,7 @@ private static HeadMetadata parseHeadMetadata(String ttml) { } } } else if (event == XmlPullParser.END_TAG) { - final String local = localName(p.getName()); + String local = localName(p.getName()); if ("head".equals(local)) { inHead = false; } else if (inSongwriters && "songwriters".equals(local)) { @@ -195,9 +195,9 @@ public static TtmlResult parse(String ttml) { return null; } try { - final HeadMetadata hm = parseHeadMetadata(ttml); + HeadMetadata hm = parseHeadMetadata(ttml); - final List songwriters; + List songwriters; if (!hm.songwriters().isEmpty()) { StringBuilder sb = new StringBuilder("Written by: "); for (int i = 0; i < hm.songwriters().size(); i++) { @@ -208,20 +208,20 @@ public static TtmlResult parse(String ttml) { } else { songwriters = null; } - final List amllCreditLines = hm.amllCredits(); - final Map agentTypes = hm.agentTypes(); - final Map> sidecarRoman = hm.sidecarRoman(); - final Map sidecarTrans = hm.sidecarTrans(); + List amllCreditLines = hm.amllCredits(); + Map agentTypes = hm.agentTypes(); + Map> sidecarRoman = hm.sidecarRoman(); + Map sidecarTrans = hm.sidecarTrans(); - final XmlPullParserFactory factory = XmlPullParserFactory.newInstance(); + XmlPullParserFactory factory = XmlPullParserFactory.newInstance(); factory.setNamespaceAware(true); - final XmlPullParser p = factory.newPullParser(); + XmlPullParser p = factory.newPullParser(); p.setInput(new StringReader(ttml)); - final List lines = new ArrayList<>(); - final List romanization = new ArrayList<>(); - final Map> translations = new HashMap<>(); - final Map> romanizations = new HashMap<>(); + List lines = new ArrayList<>(); + List romanization = new ArrayList<>(); + Map> translations = new HashMap<>(); + Map> romanizations = new HashMap<>(); int mainLineCount; boolean noTiming = false; @@ -232,7 +232,7 @@ public static TtmlResult parse(String ttml) { int event = p.getEventType(); while (event != XmlPullParser.END_DOCUMENT) { if (event == XmlPullParser.START_TAG) { - final String local = localName(p.getName()); + String local = localName(p.getName()); if ("tt".equals(local)) { noTiming = "none".equals( @@ -247,35 +247,35 @@ public static TtmlResult parse(String ttml) { divSongPart = getAttr(p, NS_ITUNES, "songPart", "itunes:song-part"); } } else if ("p".equals(local) && inBody && !inHead) { - final String lineId = getAttr(p, NS_ITUNES, "key", "itunes:key"); - final String agentId = getAttr(p, NS_TTM, "agent", "ttm:agent"); + String lineId = getAttr(p, NS_ITUNES, "key", "itunes:key"); + String agentId = getAttr(p, NS_TTM, "agent", "ttm:agent"); final long pBegin = noTiming ? 0 : parseTime(getAttr(p, null, "begin", "begin")); final long pEnd = noTiming ? 0 : parseTime(getAttr(p, null, "end", "end")); - final ParsedLine pl = processPElement(p, pBegin, pEnd); + ParsedLine pl = processPElement(p, pBegin, pEnd); if (pl != null && (!pl.text().trim().isEmpty() || !pl.bgLines().isEmpty())) { - final List lineSidecar = + List lineSidecar = findSidecarRoman(lineId, sidecarRoman); // Read off the sidecar before the words carry a reading, or the // line loses the parts no word matched. - final String romaText = buildLineRomaji(pl.words(), lineSidecar); + String romaText = buildLineRomaji(pl.words(), lineSidecar); // The panel draws a reading over the words, not beside them. if (lineSidecar != null && !lineSidecar.isEmpty()) { alignRomajiToWords(pl.words(), lineSidecar); } if (!pl.text().trim().isEmpty()) { - final LyricsLine line = new LyricsLine( + LyricsLine line = new LyricsLine( pl.begin(), pl.end(), pl.text(), pl.words(), agentId, false, false, divSongPart); lines.add(line); } mainLineCount = lines.size(); - final String bgAgent = pl.bgAgentId() != null ? pl.bgAgentId() : agentId; + String bgAgent = pl.bgAgentId() != null ? pl.bgAgentId() : agentId; for (LyricsLine bgSrc : pl.bgLines()) { - final LyricsLine bgLine = new LyricsLine( + LyricsLine bgLine = new LyricsLine( bgSrc.startTimeMs(), bgSrc.endTimeMs(), bgSrc.text(), bgSrc.words(), bgAgent, false, true, divSongPart); @@ -298,10 +298,10 @@ public static TtmlResult parse(String ttml) { if (pl.inlineTranslations() != null) { for (Map.Entry e : pl.inlineTranslations().entrySet()) { - final String lang = e.getKey(); - final String text = e.getValue(); + String lang = e.getKey(); + String text = e.getValue(); if (text.isEmpty()) continue; - final List langLines = translations.computeIfAbsent( + List langLines = translations.computeIfAbsent( lang, k -> new ArrayList<>()); while (langLines.size() < mainLineCount - 1) { langLines.add(new LyricsLine(LyricsLine.NO_TIME, "")); @@ -317,10 +317,10 @@ public static TtmlResult parse(String ttml) { if (pl.inlineRomanizations() != null) { for (Map.Entry e : pl.inlineRomanizations().entrySet()) { - final String lang = e.getKey(); - final String text = e.getValue(); + String lang = e.getKey(); + String text = e.getValue(); if (text.isEmpty()) continue; - final List langLines = romanizations.computeIfAbsent( + List langLines = romanizations.computeIfAbsent( lang, k -> new ArrayList<>()); while (langLines.size() < mainLineCount - 1) { langLines.add(new LyricsLine(LyricsLine.NO_TIME, "")); @@ -331,10 +331,10 @@ public static TtmlResult parse(String ttml) { if (pl.bgInlineTranslations() != null) { for (Map.Entry e : pl.bgInlineTranslations().entrySet()) { - final String lang = "bg:" + e.getKey(); - final String text = e.getValue(); + String lang = "bg:" + e.getKey(); + String text = e.getValue(); if (text.isEmpty()) continue; - final List langLines = translations.computeIfAbsent( + List langLines = translations.computeIfAbsent( lang, k -> new ArrayList<>()); while (langLines.size() < mainLineCount) { langLines.add(new LyricsLine(LyricsLine.NO_TIME, "")); @@ -345,10 +345,10 @@ public static TtmlResult parse(String ttml) { if (pl.bgInlineRomanizations() != null) { for (Map.Entry e : pl.bgInlineRomanizations().entrySet()) { - final String lang = "bg:" + e.getKey(); - final String text = e.getValue(); + String lang = "bg:" + e.getKey(); + String text = e.getValue(); if (text.isEmpty()) continue; - final List langLines = romanizations.computeIfAbsent( + List langLines = romanizations.computeIfAbsent( lang, k -> new ArrayList<>()); while (langLines.size() < mainLineCount) { langLines.add(new LyricsLine(LyricsLine.NO_TIME, "")); @@ -377,16 +377,16 @@ public static TtmlResult parse(String ttml) { padDerivedTo(translations, lines.size()); padDerivedTo(romanizations, lines.size()); - final Map> transOut = + Map> transOut = translations.isEmpty() ? null : translations; - final Map> romaOut = + Map> romaOut = romanizations.isEmpty() ? null : romanizations; - final Map agentNames; + Map agentNames; if (agentTypes.isEmpty()) { agentNames = null; } else { - final Map names = new HashMap<>(); + Map names = new HashMap<>(); for (Map.Entry e : agentTypes.entrySet()) { if (e.getValue().name() != null) { names.put(e.getKey(), e.getValue().name()); @@ -412,8 +412,8 @@ private static void resolveDuet(List lines, Map a boolean lastPersonIsDuet = false; for (int i = 0; i < lines.size(); i++) { - final LyricsLine original = lines.get(i); - final String agentId = original.agentId(); + LyricsLine original = lines.get(i); + String agentId = original.agentId(); if (agentId == null) { continue; } @@ -424,8 +424,8 @@ private static void resolveDuet(List lines, Map a if (agentNum > 0) { isDuet = (agentNum % 2 == 0); } else { - final AgentInfo info = agentTypes.get(agentId); - final String type = info != null ? info.type() : AGENT_TYPE_PERSON; + AgentInfo info = agentTypes.get(agentId); + String type = info != null ? info.type() : AGENT_TYPE_PERSON; if (AGENT_TYPE_GROUP.equals(type)) { isDuet = false; @@ -475,9 +475,9 @@ private static void collectSidecarTransliterations(XmlPullParser p, break; } if (event == XmlPullParser.START_TAG) { - final String local = localName(p.getName()); + String local = localName(p.getName()); if ("transliteration".equals(local)) { - final String lang = getAttr(p, NS_XML, "lang", "xml:lang"); + String lang = getAttr(p, NS_XML, "lang", "xml:lang"); collectOneTransliteration(p, lang, map); continue; } @@ -499,13 +499,13 @@ private static void collectOneTransliteration(XmlPullParser p, break; } if (event == XmlPullParser.START_TAG) { - final String local = localName(p.getName()); + String local = localName(p.getName()); if ("text".equals(local)) { - final String forKey = getAttr(p, null, "for", "for"); + String forKey = getAttr(p, null, "for", "for"); if (forKey != null && !forKey.isEmpty()) { - final List syllables = parseTransliterationText(p); + List syllables = parseTransliterationText(p); if (!syllables.isEmpty()) { - final String key = (lang != null && !lang.isEmpty()) + String key = (lang != null && !lang.isEmpty()) ? lang + ":" + forKey : forKey; map.put(key, syllables); continue; @@ -523,7 +523,7 @@ private static void collectOneTransliteration(XmlPullParser p, private static List parseTransliterationText(XmlPullParser p) throws XmlPullParserException, IOException { - final List syllables = new ArrayList<>(); + List syllables = new ArrayList<>(); int depth = 1; while (depth > 0) { final int event = p.next(); @@ -531,12 +531,12 @@ private static List parseTransliterationText(XmlPullParser p) break; } if (event == XmlPullParser.START_TAG) { - final String local = localName(p.getName()); + String local = localName(p.getName()); if ("span".equals(local)) { - final String begin = getAttr(p, null, "begin", "begin"); - final String end = getAttr(p, null, "end", "end"); + String begin = getAttr(p, null, "begin", "begin"); + String end = getAttr(p, null, "end", "end"); if (begin != null && end != null) { - final String text = normalizeText(readTextContent(p)); + String text = normalizeText(readTextContent(p)); if (!text.isEmpty()) { syllables.add(new RomajiSyllable( parseTime(begin), parseTime(end), text)); @@ -550,9 +550,9 @@ private static List parseTransliterationText(XmlPullParser p) } else if (event == XmlPullParser.END_TAG) { depth--; } else if (event == XmlPullParser.TEXT) { - final String text = p.getText(); + String text = p.getText(); if (text != null) { - final String trimmed = normalizeText(text); + String trimmed = normalizeText(text); if (!trimmed.isEmpty()) { syllables.add(new RomajiSyllable(0, 0, trimmed)); } @@ -575,9 +575,9 @@ private static void collectSidecarTranslations(XmlPullParser p, } if (event == XmlPullParser.START_TAG) { - final String local = localName(p.getName()); + String local = localName(p.getName()); if ("translation".equals(local)) { - final String lang = getAttr(p, NS_XML, "lang", "xml:lang"); + String lang = getAttr(p, NS_XML, "lang", "xml:lang"); if (lang != null && !lang.isEmpty()) { collectOneTranslation(p, lang, map); continue; @@ -602,11 +602,11 @@ private static void collectOneTranslation(XmlPullParser p, String lang, break; } if (event == XmlPullParser.START_TAG) { - final String local = localName(p.getName()); + String local = localName(p.getName()); if ("text".equals(local)) { - final String forKey = getAttr(p, null, "for", "for"); + String forKey = getAttr(p, null, "for", "for"); if (forKey != null && !forKey.isEmpty()) { - final SidecarTranslation st = parseTranslationText(p); + SidecarTranslation st = parseTranslationText(p); if (!st.text().isEmpty()) { map.put(lang + ":" + forKey, st); } @@ -624,19 +624,19 @@ private static void collectOneTranslation(XmlPullParser p, String lang, private static SidecarTranslation parseTranslationText(XmlPullParser p) throws XmlPullParserException, IOException { - final StringBuilder mainFullText = new StringBuilder(); - final List mainWords = new ArrayList<>(); - final StringBuilder bgFullText = new StringBuilder(); - final List bgWords = new ArrayList<>(); + StringBuilder mainFullText = new StringBuilder(); + List mainWords = new ArrayList<>(); + StringBuilder bgFullText = new StringBuilder(); + List bgWords = new ArrayList<>(); boolean inBg = false; int bgWrapperDepth = 0; boolean inMainWord = false; - final StringBuilder mainWordBuf = new StringBuilder(); + StringBuilder mainWordBuf = new StringBuilder(); long mainWordBegin = 0, mainWordEnd = 0; boolean inBgWord = false; - final StringBuilder bgWordBuf = new StringBuilder(); + StringBuilder bgWordBuf = new StringBuilder(); long bgWordBegin = 0, bgWordEnd = 0; int depth = 1; @@ -648,14 +648,14 @@ private static SidecarTranslation parseTranslationText(XmlPullParser p) } if (event == XmlPullParser.START_TAG) { depth++; - final String local = localName(p.getName()); - final String role = getAttr(p, NS_TTM, "role", "ttm:role"); + String local = localName(p.getName()); + String role = getAttr(p, NS_TTM, "role", "ttm:role"); if ("x-bg".equals(role)) { inBg = true; } else if ("span".equals(local)) { - final String beginAttr = getAttr(p, null, "begin", "begin"); - final String endAttr = getAttr(p, null, "end", "end"); + String beginAttr = getAttr(p, null, "begin", "begin"); + String endAttr = getAttr(p, null, "end", "end"); if (beginAttr != null && endAttr != null) { if (inBg) { inBgWord = true; @@ -681,15 +681,15 @@ private static SidecarTranslation parseTranslationText(XmlPullParser p) if (depth <= 1) { if (inBgWord) { inBgWord = false; - final String normalized = normalizeTextRaw(bgWordBuf.toString()); + String normalized = normalizeTextRaw(bgWordBuf.toString()); final boolean startsWithSpace = !normalized.isEmpty() && Character.isWhitespace(normalized.charAt(0)); final boolean endsWithSpace = !normalized.isEmpty() && Character.isWhitespace(normalized.charAt(normalized.length() - 1)); - final String text = normalized.trim(); + String text = normalized.trim(); bgFullText.append(normalized); if (startsWithSpace && !bgWords.isEmpty()) { - final Word prev = bgWords.get(bgWords.size() - 1); + Word prev = bgWords.get(bgWords.size() - 1); bgWords.set(bgWords.size() - 1, new Word(prev.startMs(), prev.endMs(), prev.text(), prev.romaji(), true)); @@ -700,15 +700,15 @@ private static SidecarTranslation parseTranslationText(XmlPullParser p) } } else if (inMainWord) { inMainWord = false; - final String normalized = normalizeTextRaw(mainWordBuf.toString()); + String normalized = normalizeTextRaw(mainWordBuf.toString()); final boolean startsWithSpace = !normalized.isEmpty() && Character.isWhitespace(normalized.charAt(0)); final boolean endsWithSpace = !normalized.isEmpty() && Character.isWhitespace(normalized.charAt(normalized.length() - 1)); - final String text = normalized.trim(); + String text = normalized.trim(); mainFullText.append(normalized); if (startsWithSpace && !mainWords.isEmpty()) { - final Word prev = mainWords.get(mainWords.size() - 1); + Word prev = mainWords.get(mainWords.size() - 1); mainWords.set(mainWords.size() - 1, new Word(prev.startMs(), prev.endMs(), prev.text(), prev.romaji(), true)); @@ -724,7 +724,7 @@ private static SidecarTranslation parseTranslationText(XmlPullParser p) } } } else if (event == XmlPullParser.TEXT) { - final String raw = p.getText(); + String raw = p.getText(); if (raw == null || raw.isEmpty()) continue; if (inBgWord) { bgWordBuf.append(raw); @@ -734,7 +734,7 @@ private static SidecarTranslation parseTranslationText(XmlPullParser p) bgFullText.append(raw); // Whitespace between bg word spans if (!raw.contains("\n") && raw.trim().isEmpty() && !bgWords.isEmpty()) { - final Word prev = bgWords.get(bgWords.size() - 1); + Word prev = bgWords.get(bgWords.size() - 1); if (!prev.endsWithSpace()) { bgWords.set(bgWords.size() - 1, new Word( prev.startMs(), prev.endMs(), @@ -745,7 +745,7 @@ private static SidecarTranslation parseTranslationText(XmlPullParser p) mainFullText.append(raw); // Whitespace between main word spans if (!raw.contains("\n") && raw.trim().isEmpty() && !mainWords.isEmpty()) { - final Word prev = mainWords.get(mainWords.size() - 1); + Word prev = mainWords.get(mainWords.size() - 1); if (!prev.endsWithSpace()) { mainWords.set(mainWords.size() - 1, new Word( prev.startMs(), prev.endMs(), @@ -756,17 +756,17 @@ private static SidecarTranslation parseTranslationText(XmlPullParser p) } } - final String main = normalizeText(mainFullText.toString()); - final String bg = normalizeText(bgFullText.toString()); + String main = normalizeText(mainFullText.toString()); + String bg = normalizeText(bgFullText.toString()); if (!mainWords.isEmpty()) { - final Word first = mainWords.get(0); + Word first = mainWords.get(0); if (first.text().startsWith(" ")) { mainWords.set(0, new Word(first.startMs(), first.endMs(), first.text().substring(1).trim(), first.romaji(), first.endsWithSpace())); } final int lastIdx = mainWords.size() - 1; - final Word last = mainWords.get(lastIdx); + Word last = mainWords.get(lastIdx); if (last.text().endsWith(" ") || last.endsWithSpace()) { mainWords.set(lastIdx, new Word(last.startMs(), last.endMs(), last.text().trim(), last.romaji(), false)); @@ -774,23 +774,23 @@ private static SidecarTranslation parseTranslationText(XmlPullParser p) } if (!bgWords.isEmpty()) { - final Word first = bgWords.get(0); + Word first = bgWords.get(0); if (first.text().startsWith(" ")) { bgWords.set(0, new Word(first.startMs(), first.endMs(), first.text().substring(1).trim(), first.romaji(), first.endsWithSpace())); } final int lastIdx = bgWords.size() - 1; - final Word last = bgWords.get(lastIdx); + Word last = bgWords.get(lastIdx); if (last.text().endsWith(" ") || last.endsWithSpace()) { bgWords.set(lastIdx, new Word(last.startMs(), last.endMs(), last.text().trim(), last.romaji(), false)); } } - final List finalMainWords = (mainWords.size() == 1 + List finalMainWords = (mainWords.size() == 1 && mainWords.get(0).startMs() == 0 && mainWords.get(0).endMs() == 0) ? null : (mainWords.isEmpty() ? null : mainWords); - final List finalBgWords = (bgWords.size() == 1 + List finalBgWords = (bgWords.size() == 1 && bgWords.get(0).startMs() == 0 && bgWords.get(0).endMs() == 0) ? null : (bgWords.isEmpty() ? null : bgWords); @@ -809,49 +809,49 @@ private record ParsedLine(long begin, long end, String text, List words, private static ParsedLine processPElement(XmlPullParser p, long pBegin, long pEnd) throws XmlPullParserException, IOException { - final List words = new ArrayList<>(); - final StringBuilder fullText = new StringBuilder(); + List words = new ArrayList<>(); + StringBuilder fullText = new StringBuilder(); boolean inTranslation = false; - final StringBuilder transBuf = new StringBuilder(); + StringBuilder transBuf = new StringBuilder(); String transLang = null; boolean inRoman = false; - final StringBuilder romanBuf = new StringBuilder(); + StringBuilder romanBuf = new StringBuilder(); String romanLang = null; - final Map inlineTrans = new HashMap<>(); - final Map inlineRoma = new HashMap<>(); - final Map bgInlineTrans = new HashMap<>(); - final Map bgInlineRoma = new HashMap<>(); + Map inlineTrans = new HashMap<>(); + Map inlineRoma = new HashMap<>(); + Map bgInlineTrans = new HashMap<>(); + Map bgInlineRoma = new HashMap<>(); boolean inBg = false; - final List bgWords = new ArrayList<>(); - final StringBuilder bgFullText = new StringBuilder(); - final StringBuilder bgWordBuf = new StringBuilder(); + List bgWords = new ArrayList<>(); + StringBuilder bgFullText = new StringBuilder(); + StringBuilder bgWordBuf = new StringBuilder(); long bgWordBegin = 0, bgWordEnd = 0; int bgWrapperDepth = 0; String bgAgentId = null; long bgBeginMs = 0, bgEndMs = 0; - final StringBuilder bgTransBuf = new StringBuilder(); - final StringBuilder bgRomanBuf = new StringBuilder(); + StringBuilder bgTransBuf = new StringBuilder(); + StringBuilder bgRomanBuf = new StringBuilder(); boolean inBgTranslation = false; boolean inBgRoman = false; boolean inBgWord = false; - final List bgLines = new ArrayList<>(); + List bgLines = new ArrayList<>(); boolean inWord = false; - final StringBuilder wordBuf = new StringBuilder(); + StringBuilder wordBuf = new StringBuilder(); long wordBegin = 0, wordEnd = 0; int wrapperDepth = 0; boolean inRubyContainer = false; boolean inRubyBase = false; - final StringBuilder rubyBaseBuf = new StringBuilder(); + StringBuilder rubyBaseBuf = new StringBuilder(); boolean inRubyTextContainer = false; boolean inRubyText = false; - final StringBuilder rubyTextBuf = new StringBuilder(); + StringBuilder rubyTextBuf = new StringBuilder(); long rubyTextBegin = 0, rubyTextEnd = 0; - final List rubyTags = new ArrayList<>(); + List rubyTags = new ArrayList<>(); int depth = 1; @@ -862,15 +862,15 @@ private static ParsedLine processPElement(XmlPullParser p, long pBegin, long pEn } if (event == XmlPullParser.START_TAG) { depth++; - final String local = localName(p.getName()); + String local = localName(p.getName()); if (!"span".equals(local)) { continue; } - final String role = getAttr(p, NS_TTM, "role", "ttm:role"); - final String ruby = getAttr(p, NS_TTS, "ruby", "tts:ruby"); - final String beginAttr = getAttr(p, null, "begin", "begin"); - final String endAttr = getAttr(p, null, "end", "end"); + String role = getAttr(p, NS_TTM, "role", "ttm:role"); + String ruby = getAttr(p, NS_TTS, "ruby", "tts:ruby"); + String beginAttr = getAttr(p, null, "begin", "begin"); + String endAttr = getAttr(p, null, "end", "end"); // Ruby is settled first: an annotation sits inside the vocal it reads, // so the background branch below would drop its base text. @@ -928,34 +928,34 @@ private static ParsedLine processPElement(XmlPullParser p, long pBegin, long pEn wrapperDepth++; } } else if (event == XmlPullParser.END_TAG) { - final String local = localName(p.getName()); + String local = localName(p.getName()); if ("span".equals(local)) { if (inBgTranslation) { inBgTranslation = false; - final String btText = bgTransBuf.toString().trim(); + String btText = bgTransBuf.toString().trim(); if (!btText.isEmpty()) { bgInlineTrans.merge("bg", btText, (a, b) -> a + " " + b); } } else if (inBgRoman) { inBgRoman = false; - final String brText = bgRomanBuf.toString().trim(); + String brText = bgRomanBuf.toString().trim(); if (!brText.isEmpty()) { bgInlineRoma.merge("bg", brText, (a, b) -> a + " " + b); } } else if (inBgWord) { inBgWord = false; - final String rawBgWord = bgWordBuf.toString(); - final String normalized = normalizeTextRaw(rawBgWord); + String rawBgWord = bgWordBuf.toString(); + String normalized = normalizeTextRaw(rawBgWord); final boolean isFormatting = rawBgWord.contains("\n"); final boolean startsWithSpace = !isFormatting && !normalized.isEmpty() && Character.isWhitespace(normalized.charAt(0)); final boolean endsWithSpace = !isFormatting && !normalized.isEmpty() && Character.isWhitespace(normalized.charAt(normalized.length() - 1)); - final String text = normalized.trim(); + String text = normalized.trim(); bgFullText.append(normalized); if (startsWithSpace && !bgWords.isEmpty()) { - final Word prev = bgWords.get(bgWords.size() - 1); + Word prev = bgWords.get(bgWords.size() - 1); bgWords.set(bgWords.size() - 1, new Word(prev.startMs(), prev.endMs(), prev.text(), prev.romaji(), true)); @@ -971,7 +971,7 @@ private static ParsedLine processPElement(XmlPullParser p, long pBegin, long pEn inBg = false; } else if (inRubyText && inRubyContainer) { inRubyText = false; - final String text = normalizeText(rubyTextBuf.toString()); + String text = normalizeText(rubyTextBuf.toString()); if (!text.isEmpty()) { rubyTags.add(new RomajiSyllable( rubyTextBegin, rubyTextEnd, text)); @@ -982,7 +982,7 @@ private static ParsedLine processPElement(XmlPullParser p, long pBegin, long pEn inRubyBase = false; } else if (inRubyContainer) { inRubyContainer = false; - final String baseText = normalizeText(rubyBaseBuf.toString()); + String baseText = normalizeText(rubyBaseBuf.toString()); if (!baseText.isEmpty()) { long rBegin = 0, rEnd = 0; if (!rubyTags.isEmpty()) { @@ -996,7 +996,7 @@ private static ParsedLine processPElement(XmlPullParser p, long pBegin, long pEn rRoma.append(rs.text()); } //noinspection SizeReplaceableByIsEmpty - final String romaji = rRoma.length() > 0 ? rRoma.toString() : null; + String romaji = rRoma.length() > 0 ? rRoma.toString() : null; // The base is the sung text either way, only the line differs. if (inBg) { @@ -1017,29 +1017,29 @@ private static ParsedLine processPElement(XmlPullParser p, long pBegin, long pEn inBg = false; } else if (inTranslation) { inTranslation = false; - final String tText = transBuf.toString().trim(); + String tText = transBuf.toString().trim(); if (!tText.isEmpty() && transLang != null) { inlineTrans.merge(transLang, tText, (a, b) -> a + " " + b); } } else if (inRoman) { inRoman = false; - final String rText = romanBuf.toString().trim(); + String rText = romanBuf.toString().trim(); if (!rText.isEmpty() && romanLang != null) { inlineRoma.merge(romanLang, rText, (a, b) -> a + " " + b); } } else if (inWord) { inWord = false; - final String rawWord = wordBuf.toString(); - final String normalized = normalizeTextRaw(rawWord); + String rawWord = wordBuf.toString(); + String normalized = normalizeTextRaw(rawWord); final boolean isFormatting = rawWord.contains("\n"); final boolean startsWithSpace = !isFormatting && !normalized.isEmpty() && Character.isWhitespace(normalized.charAt(0)); final boolean endsWithSpace = !isFormatting && !normalized.isEmpty() && Character.isWhitespace(normalized.charAt(normalized.length() - 1)); - final String text = normalized.trim(); + String text = normalized.trim(); fullText.append(normalized); if (startsWithSpace && !words.isEmpty()) { - final Word prev = words.get(words.size() - 1); + Word prev = words.get(words.size() - 1); words.set(words.size() - 1, new Word(prev.startMs(), prev.endMs(), prev.text(), prev.romaji(), true)); @@ -1054,7 +1054,7 @@ private static ParsedLine processPElement(XmlPullParser p, long pBegin, long pEn } depth--; } else if (event == XmlPullParser.TEXT) { - final String raw = p.getText(); + String raw = p.getText(); if (raw == null || raw.isEmpty()) continue; final boolean isWhitespace = raw.trim().isEmpty(); final boolean hasNewline = raw.contains("\n"); @@ -1072,7 +1072,7 @@ private static ParsedLine processPElement(XmlPullParser p, long pBegin, long pEn } else if (inBg && bgWrapperDepth == 0) { if (isWhitespace) { if (!bgWords.isEmpty()) { - final Word prev = bgWords.get(bgWords.size() - 1); + Word prev = bgWords.get(bgWords.size() - 1); final boolean cjk = isContinuousCjk(prev.text()); if (!hasNewline || !cjk) { if (bgFullText.length() > 0 && bgFullText.charAt(bgFullText.length() - 1) != ' ') { @@ -1097,7 +1097,7 @@ private static ParsedLine processPElement(XmlPullParser p, long pBegin, long pEn } else if (!inBg && !inRubyContainer) { if (isWhitespace) { if (!words.isEmpty()) { - final Word prev = words.get(words.size() - 1); + Word prev = words.get(words.size() - 1); final boolean cjk = isContinuousCjk(prev.text()); if (!hasNewline || !cjk) { if (fullText.length() > 0 && fullText.charAt(fullText.length() - 1) != ' ') { @@ -1117,12 +1117,12 @@ private static ParsedLine processPElement(XmlPullParser p, long pBegin, long pEn } } - final String rawLineText = normalizeText(fullText.toString()); + String rawLineText = normalizeText(fullText.toString()); String lineText = rawLineText; if (lineText.indexOf(' ') < 0 && words.size() > 1 && !isContinuousCjk(words.get(0).text())) { StringBuilder reconstructed = new StringBuilder(); for (int i = 0; i < words.size(); i++) { - final Word w = words.get(i); + Word w = words.get(i); if (i > 0) reconstructed.append(' '); reconstructed.append(w.text()); final boolean shouldEndWithSpace = (i < words.size() - 1); @@ -1172,13 +1172,13 @@ private static ParsedLine processPElement(XmlPullParser p, long pBegin, long pEn } if (!words.isEmpty()) { - final Word first = words.get(0); + Word first = words.get(0); if (first.text().startsWith(" ")) { words.set(0, new Word(first.startMs(), first.endMs(), first.text().substring(1).trim(), first.romaji(), first.endsWithSpace())); } final int lastIdx = words.size() - 1; - final Word last = words.get(lastIdx); + Word last = words.get(lastIdx); if (last.text().endsWith(" ") || last.endsWithSpace()) { words.set(lastIdx, new Word(last.startMs(), last.endMs(), last.text().trim(), last.romaji(), false)); @@ -1221,7 +1221,7 @@ private static void flushBgLine( if (cleanBgText.indexOf(' ') < 0 && bgWords.size() > 1 && !isContinuousCjk(bgWords.get(0).text())) { StringBuilder reconstructed = new StringBuilder(); for (int i = 0; i < bgWords.size(); i++) { - final Word w = bgWords.get(i); + Word w = bgWords.get(i); if (i > 0) reconstructed.append(' '); reconstructed.append(w.text()); final boolean shouldEndWithSpace = (i < bgWords.size() - 1); @@ -1243,16 +1243,16 @@ private static void flushBgLine( private static void stripBgWordParens(List bgWords) { if (bgWords.isEmpty()) return; - final Word first = bgWords.get(0); - final String strippedFirst = TextTrim.stripLeading( + Word first = bgWords.get(0); + String strippedFirst = TextTrim.stripLeading( first.text().replaceAll("^[((]+", "")); if (!strippedFirst.equals(first.text())) { bgWords.set(0, new Word(first.startMs(), first.endMs(), strippedFirst, first.romaji(), first.endsWithSpace())); } final int lastIdx = bgWords.size() - 1; - final Word last = bgWords.get(lastIdx); - final String strippedLast = TextTrim.stripTrailing( + Word last = bgWords.get(lastIdx); + String strippedLast = TextTrim.stripTrailing( last.text().replaceAll("[))]+$", "")); if (!strippedLast.equals(last.text())) { bgWords.set(lastIdx, new Word(last.startMs(), last.endMs(), @@ -1282,14 +1282,14 @@ private static void alignRomajiToWords(List words, int romanSearchStart = 0; for (int i = 0; i < words.size(); i++) { - final Word main = words.get(i); + Word main = words.get(i); double maxIou = 0; int bestIdx = -1; boolean fastMatched = false; int j = romanSearchStart; while (j < sidecar.size()) { - final RomajiSyllable sub = sidecar.get(j); + RomajiSyllable sub = sidecar.get(j); // Fast track: start times match within 2ms if (Math.abs(main.startMs() - sub.startMs()) <= FAST_TRACK_MS) { @@ -1322,7 +1322,7 @@ private static void alignRomajiToWords(List words, } if (!fastMatched && bestIdx >= 0 && maxIou >= MIN_IOU) { - final RomajiSyllable sub = sidecar.get(bestIdx); + RomajiSyllable sub = sidecar.get(bestIdx); words.set(i, new Word(main.startMs(), main.endMs(), main.text(), sub.text(), main.endsWithSpace())); romanSearchStart = bestIdx + 1; @@ -1333,10 +1333,10 @@ private static void alignRomajiToWords(List words, @Nullable private static List findSidecarRoman(String lineId, Map> sidecarRoman) { - final List exact = sidecarRoman.get(lineId); + List exact = sidecarRoman.get(lineId); if (exact != null) return exact; for (Map.Entry> e : sidecarRoman.entrySet()) { - final String key = e.getKey(); + String key = e.getKey(); final int sep = key.indexOf(':'); if (sep > 0 && key.substring(sep + 1).equals(lineId)) { return e.getValue(); @@ -1348,18 +1348,18 @@ private static List findSidecarRoman(String lineId, private static String buildLineRomaji(List words, @Nullable List sidecar) { if (sidecar != null && !sidecar.isEmpty()) { - final StringBuilder sb = new StringBuilder(); + StringBuilder sb = new StringBuilder(); for (RomajiSyllable s : sidecar) { if (s.text().isEmpty()) continue; sb.append(s.text()); } - final String result = sb.toString().trim(); + String result = sb.toString().trim(); if (!result.isEmpty() && LyricsRomanizer.isPurelyLatinScript(result)) { return result; } } - final StringBuilder perWord = new StringBuilder(); + StringBuilder perWord = new StringBuilder(); boolean hasPerWord = false; for (Word w : words) { if (w.romaji() != null && !w.romaji().isEmpty() && LyricsRomanizer.isPurelyLatinScript(w.romaji())) { @@ -1392,9 +1392,9 @@ private static void buildRomanizations(String lineId, int mainIndex, int bgCount) { // Keys may be "lang:lineId" or just "lineId" (no language) for (Map.Entry> entry : sidecarRoman.entrySet()) { - final String key = entry.getKey(); - final String forId; - final String lang; + String key = entry.getKey(); + String forId; + String lang; final int sep = key.indexOf(':'); if (sep > 0) { @@ -1407,17 +1407,17 @@ private static void buildRomanizations(String lineId, if (!lineId.equals(forId)) continue; - final List sidecar = entry.getValue(); + List sidecar = entry.getValue(); if (sidecar.isEmpty()) continue; if (sidecar.size() == 1 && sidecar.get(0).startMs() == 0 && sidecar.get(0).endMs() == 0) { continue; } - final List alignedWords = new ArrayList<>(words); + List alignedWords = new ArrayList<>(words); alignRomajiToWords(alignedWords, sidecar); - final StringBuilder sb = new StringBuilder(); + StringBuilder sb = new StringBuilder(); for (Word w : alignedWords) { if (w.romaji() != null && !w.romaji().isEmpty()) { sb.append(w.romaji()); @@ -1426,9 +1426,9 @@ private static void buildRomanizations(String lineId, } } } - final String text = sb.toString().trim(); - final String langKey = lang != null ? lang : "romaji"; - final List langLines = romanizations.computeIfAbsent( + String text = sb.toString().trim(); + String langKey = lang != null ? lang : "romaji"; + List langLines = romanizations.computeIfAbsent( langKey, k -> new ArrayList<>()); while (langLines.size() < mainIndex) { langLines.add(new LyricsLine(LyricsLine.NO_TIME, "")); @@ -1456,18 +1456,18 @@ private static void buildTranslations(String lineId, Map sidecarTrans, Map> translations, int mainIndex, int bgCount) { - final Map mainByLang = new HashMap<>(); - final Map bgByLang = new HashMap<>(); + Map mainByLang = new HashMap<>(); + Map bgByLang = new HashMap<>(); for (Map.Entry entry : sidecarTrans.entrySet()) { - final String key = entry.getKey(); + String key = entry.getKey(); final int sep = key.indexOf(':'); if (sep < 0) continue; - final String lang = key.substring(0, sep); - final String forId = key.substring(sep + 1); + String lang = key.substring(0, sep); + String forId = key.substring(sep + 1); if (!lineId.equals(forId)) continue; - final SidecarTranslation st = entry.getValue(); + SidecarTranslation st = entry.getValue(); mainByLang.put(lang, st); if (st.bgText() != null) { bgByLang.put(lang, st); @@ -1475,9 +1475,9 @@ private static void buildTranslations(String lineId, } for (Map.Entry entry : mainByLang.entrySet()) { - final String lang = entry.getKey(); - final SidecarTranslation st = entry.getValue(); - final List langLines = translations.computeIfAbsent( + String lang = entry.getKey(); + SidecarTranslation st = entry.getValue(); + List langLines = translations.computeIfAbsent( lang, k -> new ArrayList<>()); while (langLines.size() < mainIndex) { langLines.add(new LyricsLine(LyricsLine.NO_TIME, "")); @@ -1488,16 +1488,16 @@ private static void buildTranslations(String lineId, } for (Map.Entry entry : bgByLang.entrySet()) { - final String lang = "bg:" + entry.getKey(); - final SidecarTranslation st = entry.getValue(); - final List langLines = translations.computeIfAbsent( + String lang = "bg:" + entry.getKey(); + SidecarTranslation st = entry.getValue(); + List langLines = translations.computeIfAbsent( lang, k -> new ArrayList<>()); final int firstBg = mainIndex + 1; while (langLines.size() < firstBg) { langLines.add(new LyricsLine(LyricsLine.NO_TIME, "")); } if (langLines.size() == firstBg) { - final String bgText = st.bgText() == null ? "" : st.bgText(); + String bgText = st.bgText() == null ? "" : st.bgText(); langLines.add(bgText.isEmpty() ? new LyricsLine(LyricsLine.NO_TIME, "") : new LyricsLine(LyricsLine.NO_TIME, bgText, st.bgWords())); @@ -1509,7 +1509,7 @@ private static void buildTranslations(String lineId, private static String readTextContent(XmlPullParser p) throws XmlPullParserException, IOException { - final StringBuilder sb = new StringBuilder(); + StringBuilder sb = new StringBuilder(); int depth = 1; while (depth > 0) { final int event = p.next(); @@ -1521,7 +1521,7 @@ private static String readTextContent(XmlPullParser p) } else if (event == XmlPullParser.END_TAG) { depth--; } else if (event == XmlPullParser.TEXT) { - final String text = p.getText(); + String text = p.getText(); if (text != null) { sb.append(text); } @@ -1548,13 +1548,13 @@ private static void skipElement(XmlPullParser p) public static long parseTime(@Nullable String raw) { if (raw == null || raw.isEmpty()) return 0; - final String trimmed = raw.trim(); + String trimmed = raw.trim(); // Try colon-separated format first: HH:MM:SS.mmm or MM:SS.mmm - final Matcher cm = TIME_COLON.matcher(trimmed); + Matcher cm = TIME_COLON.matcher(trimmed); if (cm.matches()) { - final String g1 = cm.group(1); // hours (HH:MM:SS) or minutes (MM:SS) - final String g2 = cm.group(2); // minutes (HH:MM:SS only) + String g1 = cm.group(1); // hours (HH:MM:SS) or minutes (MM:SS) + String g2 = cm.group(2); // minutes (HH:MM:SS only) // Seconds are the only mandatory group of TIME_COLON. final double sec = Double.parseDouble(Objects.requireNonNull(cm.group(3))); if (g1 != null && g2 != null) { @@ -1570,7 +1570,7 @@ public static long parseTime(@Nullable String raw) { } // Try unit-suffixed format: 3.5s, 100ms, etc. - final Matcher m = TIME_UNIT.matcher(trimmed); + Matcher m = TIME_UNIT.matcher(trimmed); if (m.find()) { final double value = Double.parseDouble(Objects.requireNonNull(m.group(1))); return switch (Objects.requireNonNull(m.group(2))) { @@ -1606,18 +1606,18 @@ private static String localName(String name) { private static String getAttr(XmlPullParser p, @Nullable String ns, String local, @Nullable String fallbackQName) { if (ns != null) { - final String val = p.getAttributeValue(ns, local); + String val = p.getAttributeValue(ns, local); if (val != null) return val; } if (fallbackQName != null) { - final String val = p.getAttributeValue(null, fallbackQName); + String val = p.getAttributeValue(null, fallbackQName); if (val != null) return val; } for (int i = 0, n = p.getAttributeCount(); i < n; i++) { - final String name = p.getAttributeName(i); + String name = p.getAttributeName(i); if (name != null) { final int colon = name.indexOf(':'); - final String attrLocal = colon >= 0 ? name.substring(colon + 1) : name; + String attrLocal = colon >= 0 ? name.substring(colon + 1) : name; if (attrLocal.equals(local)) { return p.getAttributeValue(i); } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/AMLLProvider.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/AMLLProvider.java index 28c364b9c2..496b1cb47f 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/AMLLProvider.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/AMLLProvider.java @@ -41,7 +41,7 @@ public FetchResult fetch(TrackInfo track) throws Exception { return null; } - final JSONObject best = searchBest(track.title(), track.artist(), track.album()); + JSONObject best = searchBest(track.title(), track.artist(), track.album()); if (best == null) { return null; } @@ -95,7 +95,7 @@ private static String sourceUrl(JSONObject getData, JSONObject best) { @Nullable private static String firstPath(JSONObject object) { for (String key : new String[]{"path", "file", "filename", "filePath"}) { - final String value = LyricsRequests.optString(object, key); + String value = LyricsRequests.optString(object, key); if (value != null) { return value; } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/AppleMusicProvider.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/AppleMusicProvider.java index 86d92b65f8..3d9e48db59 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/AppleMusicProvider.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/AppleMusicProvider.java @@ -100,7 +100,7 @@ public boolean hasCandidates() { @Nullable @Override public FetchResult fetch(TrackInfo track) throws Exception { - final ResolvedContext ctx = resolveContext(); + ResolvedContext ctx = resolveContext(); if (ctx == null) { return fetchViaLyrically(track); } @@ -379,8 +379,8 @@ private void resolveStorefront(String userToken) { @Nullable private static String lyricsLanguageParam() { - final String target = LyricsRequests.translationLanguage(); - final String matched = matchSupportedLanguage(Locale.forLanguageTag(target.replace('_', '-'))); + String target = LyricsRequests.translationLanguage(); + String matched = matchSupportedLanguage(Locale.forLanguageTag(target.replace('_', '-'))); return matched != null ? matched : matchSupportedLanguage(Locale.getDefault()); } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/BinimumProvider.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/BinimumProvider.java index 63dff2af0f..7637ecabc8 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/BinimumProvider.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/BinimumProvider.java @@ -60,7 +60,7 @@ public FetchResult fetch(TrackInfo track) throws Exception { } // A title with a video tag is asked again without it. - final String cleanTitle = track.title() + String cleanTitle = track.title() .replace("(Official Video)", "") .replace("(Official Music Video)", "") .replace("(Official Audio)", "") diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/CaptionsFetcher.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/CaptionsFetcher.java index 25ad670860..c53a7b54a5 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/CaptionsFetcher.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/CaptionsFetcher.java @@ -976,7 +976,7 @@ private static String postInnertubePlayer(String bodyJson, @Nullable String cook public static boolean validateYouTubeCookies(String cookies) { if (cookies == null || cookies.trim().isEmpty()) return false; - final String normalized = normalizeCookies(cookies); + String normalized = normalizeCookies(cookies); if (!hasRequiredCookieKeys(normalized)) { Logger.printDebug(() -> "YouTube cookies are missing required keys"); return false; @@ -1165,7 +1165,7 @@ private static List wordTimings(JSONObject event, JSONArray segments, } Word last = words.get(total - 1); words.set(total - 1, new Word(last.startMs(), - eventEndMs > last.startMs() ? eventEndMs : last.startMs(), last.text(), + Math.max(eventEndMs, last.startMs()), last.text(), null, last.endsWithSpace())); return words; } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/DeezerProvider.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/DeezerProvider.java index bfa5114954..0832ab16ac 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/DeezerProvider.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/DeezerProvider.java @@ -223,8 +223,8 @@ private static TrackVersion lookupVersion(TrackInfo track) throws Exception { JSONArray data = getJson(url).optJSONArray("data"); if (data == null) throw new IOException("Deezer search has no results array"); - final String wantedTitle = TrackVersion.normalize(track.title()); - final String wantedArtist = TrackVersion.normalize(track.artist()); + String wantedTitle = TrackVersion.normalize(track.title()); + String wantedArtist = TrackVersion.normalize(track.artist()); // The full title is matched first. Short title equality alone would confuse the // original recording with a remix or acoustic version of that same song. for (int pass = 0; pass < 2; pass++) { @@ -235,9 +235,9 @@ private static TrackVersion lookupVersion(TrackInfo track) throws Exception { if (artist == null || !wantedArtist.equals(TrackVersion.normalize(artist.optString("name", "")))) { continue; } - final String full = item.optString("title", ""); - final String shortTitle = item.optString("title_short", ""); - final String version = item.optString("title_version", ""); + String full = item.optString("title", ""); + String shortTitle = item.optString("title_short", ""); + String version = item.optString("title_version", ""); final boolean matches = pass == 0 ? wantedTitle.equals(TrackVersion.normalize(full)) : TrackVersion.matchesStructuredTitle(track.title(), full, shortTitle, version); @@ -374,11 +374,11 @@ private static JSONObject postPipe(String body, String arl, boolean retryOnExpir @Nullable private static List creditLines(JSONObject lyrics) { List credits = new ArrayList<>(2); - final String writers = flatten(lyrics.opt("writers")); + String writers = flatten(lyrics.opt("writers")); if (writers != null) { credits.add("Writer(s): " + writers); } - final String copyright = LyricsRequests.optString(lyrics, "copyright"); + String copyright = LyricsRequests.optString(lyrics, "copyright"); if (copyright != null) { credits.add("Copyright: " + copyright); } @@ -394,7 +394,7 @@ private static String flatten(@Nullable Object value) { if (value instanceof JSONArray array) { StringBuilder builder = new StringBuilder(); for (int i = 0; i < array.length(); i++) { - final String entry = array.optString(i, "").trim(); + String entry = array.optString(i, "").trim(); if (entry.isEmpty()) continue; //noinspection SizeReplaceableByIsEmpty if (builder.length() > 0) builder.append(", "); diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/LRCLIBProvider.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/LRCLIBProvider.java index bc538b3ff0..a2439a93eb 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/LRCLIBProvider.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/LRCLIBProvider.java @@ -258,7 +258,7 @@ private Lyrics toLyrics(JSONObject response) { } final int id = response.optInt("id", -1); - final String sourceUrl = id > 0 ? "https://lrclib.net/tracks/" + id : null; + String sourceUrl = id > 0 ? "https://lrclib.net/tracks/" + id : null; String lyricsFile = LyricsRequests.optString(response, "lyricsFile"); if (lyricsFile == null) { diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/LyricifyProvider.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/LyricifyProvider.java index 4756b2251e..8a79d49004 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/LyricifyProvider.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/LyricifyProvider.java @@ -29,8 +29,8 @@ import app.morphe.extension.music.patches.lyrics.model.Lyrics; import app.morphe.extension.music.patches.lyrics.model.LyricsLine; import app.morphe.extension.music.patches.lyrics.model.TrackInfo; -import app.morphe.extension.shared.Logger; import app.morphe.extension.music.patches.lyrics.parsers.LyricifyParser; +import app.morphe.extension.shared.Logger; public final class LyricifyProvider implements LyricsProvider { @@ -58,7 +58,7 @@ public String name() { @Nullable @Override public FetchResult fetch(TrackInfo track) throws Exception { - final String cacheKey = track.title().toLowerCase(Locale.ROOT) + String cacheKey = track.title().toLowerCase(Locale.ROOT) + "|" + track.artist().toLowerCase(Locale.ROOT); Resolved resolved = resolvedCache.get(cacheKey); if (resolved == null) { @@ -70,19 +70,19 @@ public FetchResult fetch(TrackInfo track) throws Exception { if (resolved == null) { return null; } - final String isrc = resolved.isrc(); + String isrc = resolved.isrc(); - final String username = generateUsername(); - final String isrcParam = Base64.encodeToString( + String username = generateUsername(); + String isrcParam = Base64.encodeToString( isrc.getBytes(StandardCharsets.UTF_8), Base64.NO_WRAP); - final String isrcEncoded = LyricsRequests.encode(isrcParam); + String isrcEncoded = LyricsRequests.encode(isrcParam); - final String url = API_BASE + String url = API_BASE + "?username=" + username + "&isrc=" + isrcEncoded; LyricsRequests.throttle(lastRequestTime, REQUEST_THROTTLE_MS); - final HttpURLConnection conn = openApi(url); + HttpURLConnection conn = openApi(url); final int code = conn.getResponseCode(); if (code != 200) { @@ -91,7 +91,7 @@ public FetchResult fetch(TrackInfo track) throws Exception { return null; } - final String json = LyricsRequests.parseGzipString(conn); + String json = LyricsRequests.parseGzipString(conn); conn.disconnect(); if (json.isEmpty()) { @@ -104,17 +104,17 @@ public FetchResult fetch(TrackInfo track) throws Exception { return null; } - final String text = LyricsRequests.optString(response, "text"); + String text = LyricsRequests.optString(response, "text"); if (text == null || text.isEmpty()) { return null; } final int offset = response.optInt("offset", 0); - final String writer = LyricsRequests.optString(response, "writer"); - final String trans = LyricsRequests.optString(response, "trans"); + String writer = LyricsRequests.optString(response, "writer"); + String trans = LyricsRequests.optString(response, "trans"); final boolean isSyllable = text.contains("[from:AppleSyllable]"); - final List lines; + List lines; if (isSyllable) { lines = LyricifyParser.parseSyllable(text, offset); } else { @@ -125,23 +125,23 @@ public FetchResult fetch(TrackInfo track) throws Exception { return null; } - final List creditLines = new ArrayList<>(); + List creditLines = new ArrayList<>(); if (writer != null && !writer.isEmpty()) { creditLines.add("Written by " + writer); } - final Map> translations; + Map> translations; if ("zh".equals(LyricsRequests.primarySubtag(LyricsRequests.translationLanguage())) && trans != null && !trans.isEmpty()) { - final List translatedTexts = LyricifyParser.parseTranslation(trans, offset); + List translatedTexts = LyricifyParser.parseTranslation(trans, offset); if (!translatedTexts.isEmpty()) { while (translatedTexts.size() < lines.size()) { translatedTexts.add(""); } - final List translationLines = new ArrayList<>(lines.size()); + List translationLines = new ArrayList<>(lines.size()); for (int i = 0; i < lines.size(); i++) { - final LyricsLine original = lines.get(i); - final String tText = translatedTexts.get(i); + LyricsLine original = lines.get(i); + String tText = translatedTexts.get(i); if (tText.isEmpty()) { translationLines.add(new LyricsLine( original.startTimeMs(), original.endTimeMs(), "", List.of())); @@ -158,7 +158,7 @@ public FetchResult fetch(TrackInfo track) throws Exception { translations = null; } - final String formatType = isSyllable ? "lys" : "lyl"; + String formatType = isSyllable ? "lys" : "lyl"; return FetchResult.searched(new Lyrics( lines, name(), @@ -174,7 +174,7 @@ public FetchResult fetch(TrackInfo track) throws Exception { } private static HttpURLConnection openApi(String url) throws IOException { - final HttpURLConnection conn = + HttpURLConnection conn = (HttpURLConnection) new java.net.URL(url).openConnection(); conn.setRequestMethod("GET"); conn.setRequestProperty("User-Agent", USER_AGENT); @@ -189,7 +189,7 @@ private static HttpURLConnection openApi(String url) throws IOException { private static Resolved resolveFromCreditsFm(String title, String artist) { HttpURLConnection connection = null; try { - final String url = "https://api.credits.fm/v1/resolve/track"; + String url = "https://api.credits.fm/v1/resolve/track"; LyricsRequests.throttle(lastRequestTime, REQUEST_THROTTLE_MS); connection = (HttpURLConnection) new java.net.URL(url).openConnection(); connection.setRequestMethod("POST"); @@ -201,11 +201,11 @@ private static Resolved resolveFromCreditsFm(String title, String artist) { connection.setReadTimeout(15000); connection.setDoOutput(true); - final String body = new JSONObject() + String body = new JSONObject() .put("name", title) .put("artist", artist) .toString(); - final byte[] bodyBytes = body.getBytes(StandardCharsets.UTF_8); + byte[] bodyBytes = body.getBytes(StandardCharsets.UTF_8); connection.setFixedLengthStreamingMode(bodyBytes.length); try (OutputStream output = connection.getOutputStream()) { output.write(bodyBytes); @@ -213,22 +213,22 @@ private static Resolved resolveFromCreditsFm(String title, String artist) { final int code = connection.getResponseCode(); if (code == 200) { - final String responseBody = LyricsRequests.parseGzipString(connection); + String responseBody = LyricsRequests.parseGzipString(connection); if (responseBody.isEmpty()) { return null; } JSONObject response = new JSONObject(responseBody); - final String isrc = LyricsRequests.optString(response, "isrc"); + String isrc = LyricsRequests.optString(response, "isrc"); if (isrc == null || isrc.isEmpty()) { return null; } - final List titles = new ArrayList<>(2); - final String recordingTitle = + List titles = new ArrayList<>(2); + String recordingTitle = LyricsRequests.optString(response, "recording_title"); if (recordingTitle != null) { titles.add(recordingTitle); } - final String songTitle = LyricsRequests.optString(response, "song_title"); + String songTitle = LyricsRequests.optString(response, "song_title"); if (songTitle != null && !titles.contains(songTitle)) { titles.add(songTitle); } @@ -245,7 +245,7 @@ private static Resolved resolveFromCreditsFm(String title, String artist) { private static String generateUsername() { final int len = 5 + RNG.nextInt(8); - final StringBuilder sb = new StringBuilder(len); + StringBuilder sb = new StringBuilder(len); for (int i = 0; i < len; i++) { sb.append((char) ('a' + RNG.nextInt(26))); } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/LyricsProvider.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/LyricsProvider.java index fe0063c4d7..2856bc346f 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/LyricsProvider.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/LyricsProvider.java @@ -116,7 +116,7 @@ public static FetchResult searched(@Nullable Lyrics lyrics, if (titles == null || titles.isEmpty()) { return blind(lyrics); } - final List artistNames = + List artistNames = artists == null || artists.isEmpty() ? List.of("") : artists; for (String title : titles) { if (title == null || title.isEmpty()) { diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/LyricsRequests.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/LyricsRequests.java index 87928665dd..7555a7c2c8 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/LyricsRequests.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/LyricsRequests.java @@ -119,23 +119,23 @@ public static String primarySubtag(@Nullable String language) { @NonNull public static String resolvedScript(@Nullable String language) { - final String declared = declaredScript(language); + String declared = declaredScript(language); if (!declared.isEmpty()) { return declared; } if (!"zh".equals(primarySubtag(language))) { return ""; } - final String region = chineseScriptOfRegion(regionSubtag(language)); + String region = chineseScriptOfRegion(regionSubtag(language)); if (!region.isEmpty()) { return region; } - final Locale device = Locale.getDefault(); - final String script = device.getScript(); + Locale device = Locale.getDefault(); + String script = device.getScript(); if (script != null && !script.isEmpty()) { return script; } - final String country = chineseScriptOfRegion(device.getCountry()); + String country = chineseScriptOfRegion(device.getCountry()); // Chinese with no region settles is read as the mainland writes it. return country.isEmpty() ? "Hans" : country; } @@ -175,9 +175,9 @@ private static String regionSubtag(@Nullable String language) { return ""; } // The leading subtag is the language, so it is skipped. - final String[] parts = language.split("[-_]"); + String[] parts = language.split("[-_]"); for (int i = 1; i < parts.length; i++) { - final String part = parts[i]; + String part = parts[i]; if (part.length() == 2 && Character.isLetter(part.charAt(0)) && Character.isLetter(part.charAt(1))) { return part.toUpperCase(Locale.ROOT); @@ -367,7 +367,7 @@ static String optString(JSONObject object, String key) { if (object.isNull(key)) { return null; } - final String value = object.optString(key, ""); + String value = object.optString(key, ""); return value.trim().isEmpty() ? null : value; } @@ -375,9 +375,9 @@ static List stringList(JSONArray array) { if (array == null) { return List.of(); } - final List out = new ArrayList<>(array.length()); + List out = new ArrayList<>(array.length()); for (int i = 0; i < array.length(); i++) { - final String value = array.optString(i, "").trim(); + String value = array.optString(i, "").trim(); if (!value.isEmpty()) { out.add(value); } @@ -390,12 +390,12 @@ static List stringList(JSONObject object, String key) { } static String parseGzipString(HttpURLConnection connection) throws IOException { - final InputStream raw = connection.getInputStream(); - final InputStream stream = "gzip".equalsIgnoreCase(connection.getContentEncoding()) + InputStream raw = connection.getInputStream(); + InputStream stream = "gzip".equalsIgnoreCase(connection.getContentEncoding()) ? new GZIPInputStream(raw) : raw; try (BufferedReader reader = new BufferedReader( new InputStreamReader(stream, StandardCharsets.UTF_8))) { - final StringBuilder sb = new StringBuilder(); + StringBuilder sb = new StringBuilder(); String line; while ((line = reader.readLine()) != null) { sb.append(line).append('\n'); @@ -410,12 +410,12 @@ static JSONObject parseGzipJsonObject(HttpURLConnection connection) } static List parsePlainTextLines(String text) { - final List lines = new ArrayList<>(); + List lines = new ArrayList<>(); if (text == null || text.isEmpty()) { return lines; } for (String line : text.split("\\r?\\n")) { - final String trimmed = line.trim(); + String trimmed = line.trim(); if (!trimmed.isEmpty()) { lines.add(new LyricsLine(LyricsLine.NO_TIME, trimmed)); } @@ -614,11 +614,11 @@ public static final class PreparedTrack { public MatchVerdict evaluate(@Nullable String title, @Nullable String artist, long durationSec, @Nullable String album) { - final String t = normalizeForMatch(title); - final String a = normalizeForMatch(artist); - final String al = normalizeForMatch(album); + String t = normalizeForMatch(title); + String a = normalizeForMatch(artist); + String al = normalizeForMatch(album); - final Evidence titleEv; + Evidence titleEv; int titleScore; if (!titleValid) { titleEv = Evidence.MISSING; @@ -646,7 +646,7 @@ public MatchVerdict evaluate(@Nullable String title, @Nullable String artist, } } - final Evidence artistEv; + Evidence artistEv; int artistScore; if (a.isEmpty()) { artistEv = Evidence.MISSING; @@ -668,7 +668,7 @@ public MatchVerdict evaluate(@Nullable String title, @Nullable String artist, artistScore = 0; } - final Evidence durationEv; + Evidence durationEv; int durationScore; if (durationSec <= 0 || this.durationSec <= 0) { durationEv = Evidence.MISSING; @@ -690,7 +690,7 @@ public MatchVerdict evaluate(@Nullable String title, @Nullable String artist, } } - final Evidence albumEv; + Evidence albumEv; int albumScore; if (al.isEmpty() || this.album.isEmpty()) { albumEv = Evidence.MISSING; @@ -760,7 +760,7 @@ private static String stripDecorations(String s) { } private static String foldPunctuation(String s) { - final StringBuilder sb = new StringBuilder(s.length()); + StringBuilder sb = new StringBuilder(s.length()); for (int i = 0; i < s.length(); i++) { char c = s.charAt(i); if (c >= 0xFF01 && c <= 0xFF5E) { @@ -779,8 +779,8 @@ public static boolean containsEither(String a, String b) { return false; } if (a.contains(b) || b.contains(a)) { - final String shorter = a.length() <= b.length() ? a : b; - final String longer = a.length() <= b.length() ? b : a; + String shorter = a.length() <= b.length() ? a : b; + String longer = a.length() <= b.length() ? b : a; if (shorter.length() < 4 && !isCjk(shorter.charAt(0))) { return longer.length() <= shorter.length() + 6; } @@ -793,9 +793,9 @@ public static boolean containsEither(String a, String b) { } private static boolean splitArtistHits(String candidateArtist, String queryArtist) { - final String[] parts = candidateArtist.split("\\s*(?:和|&|feat\\.?|ft\\.?|,|/|×)\\s*"); + String[] parts = candidateArtist.split("\\s*(?:和|&|feat\\.?|ft\\.?|,|/|×)\\s*"); for (String part : parts) { - final String p = part.trim(); + String p = part.trim(); if (p.isEmpty()) { continue; } @@ -816,8 +816,8 @@ public static boolean isCjk(char c) { } static double jaccard(String a, String b) { - final java.util.Set ta = tokens(a); - final java.util.Set tb = tokens(b); + java.util.Set ta = tokens(a); + java.util.Set tb = tokens(b); if (ta.isEmpty() || tb.isEmpty()) { return 0; } @@ -832,8 +832,8 @@ static double jaccard(String a, String b) { } private static java.util.Set tokens(String s) { - final java.util.Set out = new java.util.LinkedHashSet<>(); - final StringBuilder cur = new StringBuilder(); + java.util.Set out = new java.util.LinkedHashSet<>(); + StringBuilder cur = new StringBuilder(); for (int i = 0; i < s.length(); ) { final int cp = s.codePointAt(i); i += Character.charCount(cp); diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/MusixmatchProvider.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/MusixmatchProvider.java index 0789808276..0dca06a862 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/MusixmatchProvider.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/MusixmatchProvider.java @@ -59,12 +59,12 @@ public String name() { @Override protected List fetchScoredSongs(TrackInfo track) throws Exception { - final String token = ensureToken(); + String token = ensureToken(); if (token == null) { return Collections.emptyList(); } - final List trackObjs; + List trackObjs; try { trackObjs = searchTracks(track, token); } catch (Exception ex) { @@ -75,12 +75,12 @@ protected List fetchScoredSongs(TrackInfo track) throws Exception { return Collections.emptyList(); } - final List scored = new ArrayList<>(); + List scored = new ArrayList<>(); ScoredSong firstAccepted = null; for (JSONObject trackObj : trackObjs) { final int trackId = trackObj.optInt("track_id", -1); if (trackId <= 0) continue; - final Lyrics lyrics; + Lyrics lyrics; try { lyrics = fetchLyricsByTrackId(trackId, token); } catch (Exception ex) { @@ -114,7 +114,7 @@ protected List fetchScoredSongs(TrackInfo track) throws Exception { @Nullable private String ensureToken() throws IOException, JSONException { synchronized (TOKEN_LOCK) { - final String userToken = Settings.MUSIXMATCH_TOKEN.get(); + String userToken = Settings.MUSIXMATCH_TOKEN.get(); if (!userToken.trim().isEmpty() && isUsableToken(userToken) && !userToken.equals(rejectedUserToken)) { cachedToken = userToken; @@ -125,12 +125,12 @@ private String ensureToken() throws IOException, JSONException { return cachedToken; } - final String url = TOKEN_URL + String url = TOKEN_URL + "?user_language=en" + "&app_id=" + APP_ID + "&t=" + requestId(); - final HttpURLConnection connection = openApi(url); + HttpURLConnection connection = openApi(url); try { final int httpCode = connection.getResponseCode(); if (httpCode != 200) { @@ -145,7 +145,7 @@ private String ensureToken() throws IOException, JSONException { JSONObject body = obj(obj(root, "message"), "body"); if (body != null) { - final String t = LyricsRequests.optString(body, "user_token"); + String t = LyricsRequests.optString(body, "user_token"); if (isUsableToken(t)) { cachedToken = t; } @@ -162,7 +162,7 @@ private List searchTracks(TrackInfo track, String token) final double durationSec = track.durationSeconds() > 0 ? (double) track.durationSeconds() : 0; - final StringBuilder url = new StringBuilder(SEARCH_URL) + StringBuilder url = new StringBuilder(SEARCH_URL) .append("?page_size=10&page=1&s_track_rating=desc") .append("&q_track=").append(LyricsRequests.encode(track.title())) .append("&q_artist=").append(LyricsRequests.encode(track.artist())) @@ -175,7 +175,7 @@ private List searchTracks(TrackInfo track, String token) } LyricsRequests.throttle(lastRequestTime, REQUEST_THROTTLE_MS); - final HttpURLConnection connection = openApi(url.toString()); + HttpURLConnection connection = openApi(url.toString()); try { final int httpCode = connection.getResponseCode(); if (httpCode != 200) { @@ -184,7 +184,7 @@ private List searchTracks(TrackInfo track, String token) JSONObject root = Requester.parseJSONObject(connection); final int status = headerStatus(root); - final String hint = headerHint(root); + String hint = headerHint(root); if (status == 401) { rejectToken(hint); return Collections.emptyList(); @@ -194,12 +194,12 @@ private List searchTracks(TrackInfo track, String token) } JSONObject body = obj(obj(root, "message"), "body"); - final JSONArray trackList = body != null ? body.optJSONArray("track_list") : null; + JSONArray trackList = body != null ? body.optJSONArray("track_list") : null; if (trackList == null || trackList.length() == 0) { return Collections.emptyList(); } - final List result = new ArrayList<>(); + List result = new ArrayList<>(); for (int i = 0; i < trackList.length(); i++) { JSONObject item = trackList.optJSONObject(i); JSONObject trackObj = item != null ? item.optJSONObject("track") : null; @@ -223,7 +223,7 @@ private List searchTracks(TrackInfo track, String token) private Lyrics fetchLyricsByTrackId(int trackId, String token) throws IOException, JSONException { - final String url = MACRO_URL + String url = MACRO_URL + "?namespace=lyrics_richsynched" + "&optional_calls=track.richsync" + "&subtitle_format=lrc" @@ -235,7 +235,7 @@ private Lyrics fetchLyricsByTrackId(int trackId, String token) + "&t=" + requestId(); LyricsRequests.throttle(lastRequestTime, REQUEST_THROTTLE_MS); - final HttpURLConnection connection = openApi(url); + HttpURLConnection connection = openApi(url); try { final int httpCode = connection.getResponseCode(); if (httpCode != 200) { @@ -244,7 +244,7 @@ private Lyrics fetchLyricsByTrackId(int trackId, String token) JSONObject root = Requester.parseJSONObject(connection); final int status = headerStatus(root); - final String hint = headerHint(root); + String hint = headerHint(root); if (status == 401) { rejectToken(hint); return null; @@ -261,8 +261,8 @@ private Lyrics fetchLyricsByTrackId(int trackId, String token) return null; } - final String copyright = extractCopyright(macroCalls); - final String sourceUrl = extractBacklinkUrl(macroCalls); + String copyright = extractCopyright(macroCalls); + String sourceUrl = extractBacklinkUrl(macroCalls); JSONObject richMsg = obj(obj(macroCalls, "track.richsync.get"), "message"); JSONObject richHeader = obj(richMsg, "header"); @@ -270,7 +270,7 @@ private Lyrics fetchLyricsByTrackId(int trackId, String token) if (richHeader != null && richStatus == 200) { JSONObject richBody = obj(obj(richMsg, "body"), "richsync"); if (richBody != null) { - final String richBodyStr = LyricsRequests.optString(richBody, "richsync_body"); + String richBodyStr = LyricsRequests.optString(richBody, "richsync_body"); if (richBodyStr != null && !richBodyStr.isEmpty()) { return appendCopyright(parseRichsync(richBodyStr, sourceUrl), copyright); } @@ -282,14 +282,14 @@ private Lyrics fetchLyricsByTrackId(int trackId, String token) final int subStatus = subHeader != null ? subHeader.optInt("status_code", 0) : 0; if (subHeader != null && subStatus == 200) { JSONObject subBody = obj(subMsg, "body"); - final JSONArray subList = subBody != null + JSONArray subList = subBody != null ? subBody.optJSONArray("subtitle_list") : null; if (subList != null && subList.length() > 0) { JSONObject subItem = subList.optJSONObject(0); JSONObject subtitle = subItem != null ? subItem.optJSONObject("subtitle") : null; if (subtitle != null) { - final String subtitleBody = LyricsRequests.optString(subtitle, "subtitle_body"); + String subtitleBody = LyricsRequests.optString(subtitle, "subtitle_body"); if (subtitleBody != null && !subtitleBody.isEmpty()) { return appendCopyright(parseSubtitles(subtitleBody, sourceUrl), copyright); } @@ -303,7 +303,7 @@ private Lyrics fetchLyricsByTrackId(int trackId, String token) if (lyricsHeader != null && lyricsStatus == 200) { JSONObject lyricsBody = obj(obj(lyricsMsg, "body"), "lyrics"); if (lyricsBody != null) { - final String lyricsText = LyricsRequests.optString(lyricsBody, "lyrics_body"); + String lyricsText = LyricsRequests.optString(lyricsBody, "lyrics_body"); if (lyricsText != null && !lyricsText.isEmpty()) { return appendCopyright(parseLyrics(lyricsText, sourceUrl), copyright); } @@ -321,19 +321,19 @@ private static String extractCopyright(JSONObject macro) { JSONObject lyricsMsg = obj(obj(macro, "track.lyrics.get"), "message"); JSONObject lyricsBody = obj(obj(lyricsMsg, "body"), "lyrics"); if (lyricsBody != null) { - final String c = LyricsRequests.optString(lyricsBody, "lyrics_copyright"); + String c = LyricsRequests.optString(lyricsBody, "lyrics_copyright"); if (c != null && !c.trim().isEmpty()) { return c.trim(); } } JSONObject subMsg = obj(obj(macro, "track.subtitles.get"), "message"); JSONObject subBody = obj(subMsg, "body"); - final JSONArray subList = subBody != null ? subBody.optJSONArray("subtitle_list") : null; + JSONArray subList = subBody != null ? subBody.optJSONArray("subtitle_list") : null; if (subList != null && subList.length() > 0) { JSONObject subItem = subList.optJSONObject(0); JSONObject subtitle = subItem != null ? subItem.optJSONObject("subtitle") : null; if (subtitle != null) { - final String c = LyricsRequests.optString(subtitle, "lyrics_copyright"); + String c = LyricsRequests.optString(subtitle, "lyrics_copyright"); if (c != null && !c.trim().isEmpty()) { return c.trim(); } @@ -342,7 +342,7 @@ private static String extractCopyright(JSONObject macro) { JSONObject richMsg = obj(obj(macro, "track.richsync.get"), "message"); JSONObject richBody = obj(obj(richMsg, "body"), "richsync"); if (richBody != null) { - final String c = LyricsRequests.optString(richBody, "lyrics_copyright"); + String c = LyricsRequests.optString(richBody, "lyrics_copyright"); if (c != null && !c.trim().isEmpty()) { return c.trim(); } @@ -355,7 +355,7 @@ private static String extractBacklinkUrl(JSONObject macro) { JSONObject lyricsMsg = obj(obj(macro, "track.lyrics.get"), "message"); JSONObject lyricsBody = obj(obj(lyricsMsg, "body"), "lyrics"); if (lyricsBody != null) { - final String url = LyricsRequests.optString(lyricsBody, "backlink_url"); + String url = LyricsRequests.optString(lyricsBody, "backlink_url"); if (url != null && !url.isEmpty()) { return stripTrackingParams(url); } @@ -368,7 +368,7 @@ private static Lyrics appendCopyright(@Nullable Lyrics lyrics, @Nullable String if (lyrics == null || copyright == null || copyright.isEmpty()) { return lyrics; } - final List songwriters = new ArrayList<>(); + List songwriters = new ArrayList<>(); if (lyrics.songwriters() != null) { songwriters.addAll(lyrics.songwriters()); } @@ -381,9 +381,9 @@ private static Lyrics appendCopyright(@Nullable Lyrics lyrics, @Nullable String private static String stripTrackingParams(String url) { final int queryStart = url.indexOf('?'); if (queryStart < 0) return url; - final String base = url.substring(0, queryStart); - final String query = url.substring(queryStart + 1); - final StringBuilder filtered = new StringBuilder(); + String base = url.substring(0, queryStart); + String query = url.substring(queryStart + 1); + StringBuilder filtered = new StringBuilder(); for (String param : query.split("&")) { if (!param.toLowerCase(Locale.ROOT).startsWith("utm")) { //noinspection SizeReplaceableByIsEmpty @@ -398,11 +398,11 @@ private static String stripTrackingParams(String url) { @Nullable private Lyrics parseRichsync(String body, @Nullable String sourceUrl) throws JSONException { - final JSONArray lines = new JSONArray(body); + JSONArray lines = new JSONArray(body); if (lines.length() == 0) { return null; } - final List result = new ArrayList<>(); + List result = new ArrayList<>(); for (int i = 0; i < lines.length(); i++) { JSONObject line = lines.optJSONObject(i); if (line == null) { @@ -412,10 +412,10 @@ private Lyrics parseRichsync(String body, @Nullable String sourceUrl) final double lineTe = line.optDouble("te", lineTs); final long lineStartMs = (long) (lineTs * 1000); final long lineEndMs = (long) (lineTe * 1000); - final String x = LyricsRequests.optString(line, "x"); + String x = LyricsRequests.optString(line, "x"); List words = null; - final JSONArray lArr = line.optJSONArray("l"); + JSONArray lArr = line.optJSONArray("l"); if (lArr != null && lArr.length() > 0) { words = new ArrayList<>(); int xCursor = 0; @@ -424,7 +424,7 @@ private Lyrics parseRichsync(String body, @Nullable String sourceUrl) if (w == null) { continue; } - final String rawChunk = LyricsRequests.optString(w, "c"); + String rawChunk = LyricsRequests.optString(w, "c"); if (rawChunk == null || rawChunk.isEmpty()) { continue; } @@ -494,7 +494,7 @@ private Lyrics parseRichsync(String body, @Nullable String sourceUrl) } } - final String text; + String text; if (words != null && !words.isEmpty()) { text = joinWords(words); } else { @@ -525,15 +525,15 @@ private Lyrics parseSubtitles(String body, @Nullable String sourceUrl) @Nullable private Lyrics parseSubtitlesLrc(String body, @Nullable String sourceUrl) { - final List result = new ArrayList<>(); + List result = new ArrayList<>(); for (String raw : body.split("\\n")) { - final String line = raw.trim(); + String line = raw.trim(); if (line.isEmpty()) continue; - final Matcher m = LRC_LINE_PATTERN.matcher(line); + Matcher m = LRC_LINE_PATTERN.matcher(line); if (!m.matches()) continue; final int min = Integer.parseInt(Objects.requireNonNull(m.group(1))); final int sec = Integer.parseInt(Objects.requireNonNull(m.group(2))); - final String text = m.group(3); + String text = m.group(3); if (text == null || text.isEmpty()) continue; final long startMs = min * 60_000L + sec * 1_000L; result.add(new LyricsLine(startMs, text)); @@ -545,11 +545,11 @@ private Lyrics parseSubtitlesLrc(String body, @Nullable String sourceUrl) { @Nullable private Lyrics parseSubtitlesJson(String body, @Nullable String sourceUrl) throws JSONException { - final JSONArray lines = new JSONArray(body); + JSONArray lines = new JSONArray(body); if (lines.length() == 0) { return null; } - final List result = new ArrayList<>(); + List result = new ArrayList<>(); for (int i = 0; i < lines.length(); i++) { JSONObject line = lines.optJSONObject(i); if (line == null) { @@ -573,7 +573,7 @@ private Lyrics parseSubtitlesJson(String body, @Nullable String sourceUrl) @Nullable private Lyrics parseLyrics(String body, @Nullable String sourceUrl) { - final List result = LyricsRequests.parsePlainTextLines(body); + List result = LyricsRequests.parsePlainTextLines(body); if (result.isEmpty()) { return null; } @@ -581,7 +581,7 @@ private Lyrics parseLyrics(String body, @Nullable String sourceUrl) { } private HttpURLConnection openApi(String url) throws IOException { - final HttpURLConnection connection = LyricsRequests.openConnection(url); + HttpURLConnection connection = LyricsRequests.openConnection(url); connection.setRequestProperty("User-Agent", USER_AGENT); connection.setRequestProperty("Cookie", COOKIE); return connection; @@ -639,13 +639,13 @@ private static JSONObject obj(JSONObject o, String key) { public static boolean validateToken(String token) { if (!isUsableToken(token)) return false; try { - final String url = SEARCH_URL + String url = SEARCH_URL + "?page_size=1&page=1&q_track=a&q_artist=a" + "&usertoken=" + LyricsRequests.encode(token) + "&format=json" + "&app_id=" + APP_ID + "&t=" + requestId(); - final HttpURLConnection connection = LyricsRequests.openConnection(url); + HttpURLConnection connection = LyricsRequests.openConnection(url); connection.setRequestProperty("User-Agent", USER_AGENT); connection.setRequestProperty("Cookie", COOKIE); final int httpCode = connection.getResponseCode(); @@ -655,7 +655,7 @@ public static boolean validateToken(String token) { } JSONObject root = Requester.parseJSONObject(connection); final int status = headerStatus(root); - final String hint = headerHint(root); + String hint = headerHint(root); connection.disconnect(); return status != 401 || !"renew".equalsIgnoreCase(hint); } catch (Exception ex) { @@ -665,9 +665,9 @@ public static boolean validateToken(String token) { } private static String joinWords(List words) { - final StringBuilder sb = new StringBuilder(); + StringBuilder sb = new StringBuilder(); for (int i = 0; i < words.size(); i++) { - final Word w = words.get(i); + Word w = words.get(i); sb.append(w.text()); if (w.endsWithSpace() && i < words.size() - 1) { sb.append(' '); diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/QQProvider.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/QQProvider.java index 67fdc2122c..a1aa4176e4 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/QQProvider.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/QQProvider.java @@ -131,7 +131,7 @@ private Lyrics fetchFromSong(JSONObject song) throws Exception { } final long songId = song.optLong("id"); - final String sourceUrl = "https://y.qq.com/n/ryqq/songDetail/" + songId; + String sourceUrl = "https://y.qq.com/n/ryqq/songDetail/" + songId; String original = decodeQqLyricPayload(data.optString("lyric", "")); Matcher qrcXmlMatcher = QRC_XML.matcher(original); diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/RMMProvider.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/RMMProvider.java index f57c53045d..7a56018133 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/RMMProvider.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/RMMProvider.java @@ -137,8 +137,8 @@ private static List searchItunesCandidates(TrackInfo track) { return new ArrayList<>(); } - final String title = track.title().toLowerCase(Locale.ROOT).trim(); - final String artist = track.artist().toLowerCase(Locale.ROOT).trim(); + String title = track.title().toLowerCase(Locale.ROOT).trim(); + String artist = track.artist().toLowerCase(Locale.ROOT).trim(); List scored = new ArrayList<>(); for (int i = 0; i < results.length(); i++) { diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/SimpMusicProvider.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/SimpMusicProvider.java index 6eabeac53f..6e373b3abb 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/SimpMusicProvider.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/SimpMusicProvider.java @@ -22,8 +22,8 @@ import java.util.regex.Pattern; import app.morphe.extension.music.patches.lyrics.model.Lyrics; -import app.morphe.extension.music.patches.lyrics.model.LyricsMerge; import app.morphe.extension.music.patches.lyrics.model.LyricsLine; +import app.morphe.extension.music.patches.lyrics.model.LyricsMerge; import app.morphe.extension.music.patches.lyrics.model.TrackInfo; import app.morphe.extension.music.patches.lyrics.parsers.LRCParser; import app.morphe.extension.music.shared.VideoInformation; @@ -47,7 +47,7 @@ public String name() { @Nullable @Override public FetchResult fetch(TrackInfo track) throws Exception { - final String videoId = VideoInformation.getVideoId(); + String videoId = VideoInformation.getVideoId(); if (videoId.isEmpty()) { return null; } @@ -58,7 +58,7 @@ public FetchResult fetch(TrackInfo track) throws Exception { private Lyrics fetchByVideoId(String videoId) { HttpURLConnection connection = null; try { - final String url = BASE_URL + LyricsRequests.encode(videoId); + String url = BASE_URL + LyricsRequests.encode(videoId); connection = LyricsRequests.openConnection(url); connection.setRequestProperty("User-Agent", CLIENT_USER_AGENT); connection.setRequestProperty("Accept", "application/json"); @@ -76,7 +76,7 @@ private Lyrics fetchByVideoId(String videoId) { if (!root.optBoolean("success", false)) { return null; } - final JSONArray data = root.optJSONArray("data"); + JSONArray data = root.optJSONArray("data"); if (data == null || data.length() == 0) { return null; } @@ -108,7 +108,7 @@ private Lyrics pickBestEntry(JSONArray data, String videoId) { if (vote < bestVote) { continue; } - final Lyrics lyrics = parseEntry(entry, videoId); + Lyrics lyrics = parseEntry(entry, videoId); if (lyrics != null && !lyrics.isEmpty()) { best = lyrics; bestVote = vote; @@ -119,18 +119,18 @@ private Lyrics pickBestEntry(JSONArray data, String videoId) { @Nullable private Lyrics parseEntry(JSONObject entry, String videoId) { - final String contributor = LyricsRequests.optString(entry, "contributor"); - final String richSync = LyricsRequests.optString(entry, "richSyncLyrics"); + String contributor = LyricsRequests.optString(entry, "contributor"); + String richSync = LyricsRequests.optString(entry, "richSyncLyrics"); if (richSync != null) { return withContributor(parseLrc(richSync, videoId), contributor); } - final String synced = LyricsRequests.optString(entry, "syncedLyrics"); + String synced = LyricsRequests.optString(entry, "syncedLyrics"); if (synced != null) { return withContributor(parseLrc(synced, videoId), contributor); } - final String plain = LyricsRequests.optString(entry, "plainLyric"); + String plain = LyricsRequests.optString(entry, "plainLyric"); if (plain != null) { - final List lines = LRCParser.parsePlain(plain); + List lines = LRCParser.parsePlain(plain); if (!lines.isEmpty()) { return withContributor(new Lyrics(lines, name(), false, null, null, null, null, plain, "plain", sourceUrl(videoId)), contributor); @@ -141,9 +141,9 @@ private Lyrics parseEntry(JSONObject entry, String videoId) { @Nullable private Lyrics parseLrc(String lrc, String videoId) { - final long[] offsetHolder = {0}; - final String sanitized = sanitizeLrc(lrc, offsetHolder); - final LRCParser.LrcParseResult result = LRCParser.parseSyncedWithCreditLines(sanitized); + long[] offsetHolder = {0}; + String sanitized = sanitizeLrc(lrc, offsetHolder); + LRCParser.LrcParseResult result = LRCParser.parseSyncedWithCreditLines(sanitized); if (result.lines.isEmpty()) { return null; } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/SpotifyProvider.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/SpotifyProvider.java index 4e0754b053..0cd2afad00 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/SpotifyProvider.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/SpotifyProvider.java @@ -86,7 +86,7 @@ public String name() { @Nullable @Override public FetchResult fetch(TrackInfo track) throws Exception { - final String spDc = Settings.SPOTIFY_TOKEN.get(); + String spDc = Settings.SPOTIFY_TOKEN.get(); if (spDc.trim().isEmpty()) { return null; } @@ -96,13 +96,13 @@ public FetchResult fetch(TrackInfo track) throws Exception { return null; } - final String sourceUrl = "https://open.spotify.com/track/" + hit.id(); + String sourceUrl = "https://open.spotify.com/track/" + hit.id(); JSONObject lyricsResponse = fetchLyrics(spDc, hit.id()); if (lyricsResponse == null) { return null; } - final String rawJson = lyricsResponse.toString(); + String rawJson = lyricsResponse.toString(); return FetchResult.searched(parseLyrics(lyricsResponse, rawJson, sourceUrl), List.of(hit.title()), hit.artists(), 0L, track); } @@ -111,17 +111,17 @@ private record SearchHit(String id, String title, List artists) {} @Nullable private SearchHit searchTrack(String spDc, String title, String artist) throws Exception { - final String accessToken = getAccessToken(spDc); + String accessToken = getAccessToken(spDc); if (accessToken == null) { return null; } - final String clientToken = getClientToken(spDc); + String clientToken = getClientToken(spDc); - final String query = title + " " + artist; + String query = title + " " + artist; for (String hash : SEARCH_HASHES) { - final String body = new JSONObject() + String body = new JSONObject() .put("extensions", new JSONObject() .put("persistedQuery", new JSONObject() .put("sha256Hash", hash) @@ -146,7 +146,7 @@ private SearchHit searchTrack(String spDc, String title, String artist) throws E @Nullable private SearchHit executeSearch(String accessToken, @Nullable String clientToken, String body) throws Exception { - final byte[] bodyBytes = body.getBytes(StandardCharsets.UTF_8); + byte[] bodyBytes = body.getBytes(StandardCharsets.UTF_8); long retryAfterMs = 0; for (int attempt = 0; attempt <= SEARCH_RETRIES; attempt++) { @@ -160,13 +160,13 @@ private SearchHit executeSearch(String accessToken, @Nullable String clientToken } } - final HttpURLConnection connection = + HttpURLConnection connection = openSearchConnection(accessToken, clientToken, bodyBytes); final int code = connection.getResponseCode(); if (code == 200) { - final String json = Requester.parseString(connection); + String json = Requester.parseString(connection); connection.disconnect(); SearchHit hit = parseSearchResult(json); @@ -192,7 +192,7 @@ private SearchHit executeSearch(String accessToken, @Nullable String clientToken private static HttpURLConnection openSearchConnection(String accessToken, @Nullable String clientToken, byte[] bodyBytes) throws IOException { - final HttpURLConnection connection = (HttpURLConnection) + HttpURLConnection connection = (HttpURLConnection) new java.net.URL(PARTNER_GRAPHQL_URL).openConnection(); connection.setRequestMethod("POST"); connection.setConnectTimeout(5000); @@ -263,13 +263,13 @@ private SearchHit extractSearchHit(JSONArray items) { String id = LyricsRequests.optString(node, "id"); if (id == null || id.isEmpty()) { - final String uri = LyricsRequests.optString(node, "uri"); + String uri = LyricsRequests.optString(node, "uri"); if (uri != null && uri.startsWith("spotify:track:")) { id = uri.substring("spotify:track:".length()); } } if (id != null && !id.isEmpty()) { - final String title = LyricsRequests.optString(node, "name"); + String title = LyricsRequests.optString(node, "name"); return new SearchHit(id, title != null ? title : "", artistNames(node)); } } @@ -278,13 +278,13 @@ private SearchHit extractSearchHit(JSONArray items) { } private static List artistNames(JSONObject node) { - final List out = new ArrayList<>(); - final JSONArray artists = node.optJSONArray("artists"); + List out = new ArrayList<>(); + JSONArray artists = node.optJSONArray("artists"); if (artists != null) { for (int i = 0; i < artists.length(); i++) { - final JSONObject artist = artists.optJSONObject(i); + JSONObject artist = artists.optJSONObject(i); if (artist == null) continue; - final String name = LyricsRequests.optString(artist, "name"); + String name = LyricsRequests.optString(artist, "name"); if (name != null) { out.add(name); } @@ -293,15 +293,15 @@ private static List artistNames(JSONObject node) { return out; } } - final JSONObject connection = node.optJSONObject("artists"); + JSONObject connection = node.optJSONObject("artists"); if (connection != null) { - final JSONArray edges = connection.optJSONArray("edges"); + JSONArray edges = connection.optJSONArray("edges"); if (edges != null) { for (int i = 0; i < edges.length(); i++) { - final JSONObject edge = edges.optJSONObject(i); - final JSONObject artistNode = + JSONObject edge = edges.optJSONObject(i); + JSONObject artistNode = edge != null ? edge.optJSONObject("node") : null; - final String name = artistNode != null + String name = artistNode != null ? LyricsRequests.optString(artistNode, "name") : null; if (name != null) { out.add(name); @@ -310,7 +310,7 @@ private static List artistNames(JSONObject node) { } } if (out.isEmpty()) { - final String artist = LyricsRequests.optString(node, "artist"); + String artist = LyricsRequests.optString(node, "artist"); if (artist != null) { out.add(artist); } @@ -320,12 +320,12 @@ private static List artistNames(JSONObject node) { @Nullable private JSONObject fetchLyrics(String spDc, String trackId) throws Exception { - final String accessToken = getAccessToken(spDc); + String accessToken = getAccessToken(spDc); if (accessToken == null) { return null; } - final String url = LYRICS_URL + trackId + "?format=json&market=from_token"; + String url = LYRICS_URL + trackId + "?format=json&market=from_token"; HttpURLConnection connection = LyricsRequests.openConnection(url); connection.setRequestProperty("Authorization", "Bearer " + accessToken); connection.setRequestProperty("User-Agent", USER_AGENT); @@ -337,7 +337,7 @@ private JSONObject fetchLyrics(String spDc, String trackId) throws Exception { return null; } - final String json = LyricsRequests.parseGzipString(connection); + String json = LyricsRequests.parseGzipString(connection); return new JSONObject(json); } @@ -348,11 +348,11 @@ private Lyrics parseLyrics(JSONObject response, String rawJson, @Nullable String return null; } - final String syncType = lyricsObj.optString("syncType", "UNSYNCED"); - final String provider = LyricsRequests.optString(lyricsObj, "provider"); - final String providerName = (provider != null && !provider.isEmpty()) + String syncType = lyricsObj.optString("syncType", "UNSYNCED"); + String provider = LyricsRequests.optString(lyricsObj, "provider"); + String providerName = (provider != null && !provider.isEmpty()) ? name() + " (via " + provider + ")" : name(); - final JSONArray linesArr = lyricsObj.optJSONArray("lines"); + JSONArray linesArr = lyricsObj.optJSONArray("lines"); if (linesArr == null || linesArr.length() == 0) { return null; } @@ -368,7 +368,7 @@ private Lyrics parseLyrics(JSONObject response, String rawJson, @Nullable String @Nullable private Lyrics parseSyncedLines(JSONArray linesArr, String providerName, String rawJson, @Nullable String sourceUrl) { - final List lines = new ArrayList<>(linesArr.length()); + List lines = new ArrayList<>(linesArr.length()); for (int i = 0; i < linesArr.length(); i++) { JSONObject lineObj = linesArr.optJSONObject(i); @@ -377,7 +377,7 @@ private Lyrics parseSyncedLines(JSONArray linesArr, String providerName, String } final long startTimeMs = parseStartTimeMs(lineObj); - final String text = lineObj.optString("words", "").trim(); + String text = lineObj.optString("words", "").trim(); if (text.isEmpty()) { continue; } @@ -432,7 +432,7 @@ private Lyrics parseSyllableLines(JSONArray linesArr, String providerName, } private static List parseSyllables(JSONArray syllablesArr, String lineText) { - final List words = new ArrayList<>(syllablesArr.length()); + List words = new ArrayList<>(syllablesArr.length()); int charOffset = 0; for (int j = 0; j < syllablesArr.length(); j++) { @@ -457,7 +457,7 @@ private static List parseSyllables(JSONArray syllablesArr, String lineText } final int end = Math.min(charOffset + numChars, lineText.length()); - final String wordText = lineText.substring(charOffset, end); + String wordText = lineText.substring(charOffset, end); final boolean spaceAfter = (end < lineText.length() && Character.isWhitespace(lineText.charAt(end))); words.add(new Word(startMs, endMs, wordText, null, spaceAfter)); charOffset = end; @@ -467,7 +467,7 @@ private static List parseSyllables(JSONArray syllablesArr, String lineText } private static long parseStartTimeMs(JSONObject obj) { - final String raw = LyricsRequests.optString(obj, "startTimeMs"); + String raw = LyricsRequests.optString(obj, "startTimeMs"); if (raw == null || raw.isEmpty()) { return LyricsLine.NO_TIME; } @@ -482,7 +482,7 @@ private static long parseStartTimeMs(JSONObject obj) { @Nullable private Lyrics parsePlainLines(JSONArray linesArr, String providerName, String rawJson, @Nullable String sourceUrl) { - final List lines = new ArrayList<>(linesArr.length()); + List lines = new ArrayList<>(linesArr.length()); for (int i = 0; i < linesArr.length(); i++) { JSONObject lineObj = linesArr.optJSONObject(i); @@ -490,7 +490,7 @@ private Lyrics parsePlainLines(JSONArray linesArr, String providerName, continue; } - final String text = lineObj.optString("words", "").trim(); + String text = lineObj.optString("words", "").trim(); if (text.isEmpty()) { continue; } @@ -512,16 +512,16 @@ private synchronized String getAccessToken(String spDc) { HttpURLConnection connection = null; try { ensureTotpSecrets(); - final String[] totpSecrets = cachedTotpSecrets; + String[] totpSecrets = cachedTotpSecrets; if (totpSecrets == null || totpSecrets.length == 0) { return null; } final long serverTimeMs = getServerTime(spDc); - final String totpValue = generateTotp(serverTimeMs, totpSecrets[0]); + String totpValue = generateTotp(serverTimeMs, totpSecrets[0]); - final String url = TOKEN_URL + String url = TOKEN_URL + "?reason=init&productType=web-player" + "&totp=" + totpValue + "&totpVer=" + cachedTotpVersion @@ -543,15 +543,15 @@ private synchronized String getAccessToken(String spDc) { return null; } - final String body = Requester.parseString(connection); + String body = Requester.parseString(connection); JSONObject json = new JSONObject(body); - final String token = json.optString("accessToken", ""); + String token = json.optString("accessToken", ""); if (token.trim().isEmpty()) { return null; } cachedAccessToken = token; - final String clientId = LyricsRequests.optString(json, "clientId"); + String clientId = LyricsRequests.optString(json, "clientId"); if (clientId != null && !clientId.isEmpty()) { cachedClientId = clientId; } @@ -573,17 +573,17 @@ private synchronized String getClientToken(String spDc) { } try { - final String accessToken = getAccessToken(spDc); + String accessToken = getAccessToken(spDc); if (accessToken == null) { return null; } - final String clientId = cachedClientId; + String clientId = cachedClientId; if (clientId == null || clientId.isEmpty()) { return null; } - final String deviceId = UUID.randomUUID().toString(); + String deviceId = UUID.randomUUID().toString(); JSONObject jsSdkData = new JSONObject() .put("device_brand", "unknown") .put("device_model", "unknown") @@ -600,7 +600,7 @@ private synchronized String getClientToken(String spDc) { JSONObject requestBody = new JSONObject() .put("client_data", clientData); - final HttpURLConnection connection = (HttpURLConnection) + HttpURLConnection connection = (HttpURLConnection) new java.net.URL(CLIENT_TOKEN_URL).openConnection(); connection.setRequestMethod("POST"); connection.setConnectTimeout(5000); @@ -612,7 +612,7 @@ private synchronized String getClientToken(String spDc) { connection.setRequestProperty("Origin", "https://open.spotify.com"); connection.setRequestProperty("Referer", "https://open.spotify.com"); - final byte[] bodyBytes = requestBody.toString().getBytes(StandardCharsets.UTF_8); + byte[] bodyBytes = requestBody.toString().getBytes(StandardCharsets.UTF_8); connection.setFixedLengthStreamingMode(bodyBytes.length); try (java.io.OutputStream os = connection.getOutputStream()) { os.write(bodyBytes); @@ -624,7 +624,7 @@ private synchronized String getClientToken(String spDc) { return null; } - final String body = Requester.parseString(connection); + String body = Requester.parseString(connection); JSONObject json = new JSONObject(body); JSONObject grantedToken = json.optJSONObject("granted_token"); if (grantedToken == null) { @@ -632,7 +632,7 @@ private synchronized String getClientToken(String spDc) { return null; } - final String token = grantedToken.optString("token", ""); + String token = grantedToken.optString("token", ""); if (token.isEmpty()) { connection.disconnect(); return null; @@ -668,7 +668,7 @@ private long getServerTime(String spDc) { connection = openServerTimeConnection(spDc, 5000); final int code = connection.getResponseCode(); if (code == 200) { - final String body = Requester.parseString(connection); + String body = Requester.parseString(connection); JSONObject json = new JSONObject(body); final long serverTime = json.optLong("serverTime", 0); if (serverTime > 0) { @@ -691,7 +691,7 @@ private void ensureTotpSecrets() { } try { - final String body = fetchSecrets(); + String body = fetchSecrets(); if (body != null) { JSONObject secrets = new JSONObject(body); @@ -710,15 +710,15 @@ private void ensureTotpSecrets() { } if (newestVersion >= 0) { - final String previousVersion = cachedTotpVersion; + String previousVersion = cachedTotpVersion; cachedTotpVersion = String.valueOf(newestVersion); - final JSONArray arr = secrets.getJSONArray(cachedTotpVersion); - final int[] secretData = new int[arr.length()]; + JSONArray arr = secrets.getJSONArray(cachedTotpVersion); + int[] secretData = new int[arr.length()]; for (int i = 0; i < arr.length(); i++) { secretData[i] = arr.getInt(i); } - final int[] xored = new int[secretData.length]; + int[] xored = new int[secretData.length]; for (int i = 0; i < secretData.length; i++) { xored[i] = secretData[i] ^ ((i % 33) + 9); } @@ -749,17 +749,17 @@ private static String generateTotp(long timestampMs, String secret) final long epochSeconds = timestampMs / 1000; final long counter = epochSeconds / 30; - final byte[] counterBytes = new byte[8]; + byte[] counterBytes = new byte[8]; long tmp = counter; for (int i = 7; i >= 0; i--) { counterBytes[i] = (byte) (tmp & 0xFF); tmp >>= 8; } - final Mac mac = Mac.getInstance("HmacSHA1"); - final byte[] secretBytes = secret.getBytes(StandardCharsets.UTF_8); + Mac mac = Mac.getInstance("HmacSHA1"); + byte[] secretBytes = secret.getBytes(StandardCharsets.UTF_8); mac.init(new SecretKeySpec(secretBytes, "HmacSHA1")); - final byte[] hash = mac.doFinal(counterBytes); + byte[] hash = mac.doFinal(counterBytes); final int offset = hash[hash.length - 1] & 0x0F; final int binary = ((hash[offset] & 0x7F) << 24) @@ -806,7 +806,7 @@ private static String fetchSecrets() { } private static String decimalStringFromInts(int[] values) { - final StringBuilder sb = new StringBuilder(values.length); + StringBuilder sb = new StringBuilder(values.length); for (int v : values) { sb.append(v); } @@ -814,7 +814,7 @@ private static String decimalStringFromInts(int[] values) { } private static long parseRetryAfter(HttpURLConnection connection) { - final String retryAfter = connection.getHeaderField("Retry-After"); + String retryAfter = connection.getHeaderField("Retry-After"); if (retryAfter != null) { try { final long seconds = Long.parseLong(retryAfter.trim()); diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/UnisonProvider.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/UnisonProvider.java index a55eca508c..9cf16d7160 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/UnisonProvider.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/UnisonProvider.java @@ -26,8 +26,8 @@ import java.util.Locale; import app.morphe.extension.music.patches.lyrics.model.Lyrics; -import app.morphe.extension.music.patches.lyrics.model.LyricsMerge; import app.morphe.extension.music.patches.lyrics.model.LyricsLine; +import app.morphe.extension.music.patches.lyrics.model.LyricsMerge; import app.morphe.extension.music.patches.lyrics.model.TrackInfo; import app.morphe.extension.music.patches.lyrics.parsers.LRCParser; import app.morphe.extension.music.patches.lyrics.parsers.TTMLParser; @@ -51,17 +51,17 @@ public String name() { @Nullable @Override public FetchResult fetch(TrackInfo track) throws Exception { - final String videoId = VideoInformation.getVideoId(); + String videoId = VideoInformation.getVideoId(); if (videoId.isEmpty()) { return null; } - final String title = track.title() != null ? track.title() : ""; - final String artist = track.artist() != null ? track.artist() : ""; + String title = track.title() != null ? track.title() : ""; + String artist = track.artist() != null ? track.artist() : ""; final long videoLengthMs = VideoInformation.getVideoLength(); final int duration = videoLengthMs > 0 ? (int) (videoLengthMs / 1000) : track.durationSeconds(); - final String album = track.album(); + String album = track.album(); return FetchResult.of(fetchByVideoId(videoId, title, artist, duration, album)); } @@ -71,7 +71,7 @@ private Lyrics fetchByVideoId(String videoId, String title, String artist, int duration, String album) { HttpURLConnection connection = null; try { - final StringBuilder url = new StringBuilder(BASE_URL) + StringBuilder url = new StringBuilder(BASE_URL) .append("?v=").append(LyricsRequests.encode(videoId)); if (!title.isEmpty()) { url.append("&song=").append(LyricsRequests.encode(title)); @@ -102,13 +102,13 @@ private Lyrics fetchByVideoId(String videoId, String title, String artist, if (data == null) { return null; } - final String format = LyricsRequests.optString(data, "format"); - final String lyrics = LyricsRequests.optString(data, "lyrics"); + String format = LyricsRequests.optString(data, "format"); + String lyrics = LyricsRequests.optString(data, "lyrics"); if (format == null || lyrics == null) { return null; } - final JSONObject submitter = data.optJSONObject("submitter"); - final String submitterName = submitter != null + JSONObject submitter = data.optJSONObject("submitter"); + String submitterName = submitter != null ? LyricsRequests.optString(submitter, "displayName") : null; return parseLyrics(format, lyrics, videoId, submitterName); @@ -138,14 +138,14 @@ private static String getKeyId() { private static String computeKeyId() { try { - final KeyPairGenerator generator = KeyPairGenerator.getInstance("EC"); + KeyPairGenerator generator = KeyPairGenerator.getInstance("EC"); generator.initialize(new ECGenParameterSpec("secp256r1")); - final ECPublicKey publicKey = (ECPublicKey) generator.generateKeyPair().getPublic(); - final String x = base64Url(publicKey.getW().getAffineX()); - final String y = base64Url(publicKey.getW().getAffineY()); - final String canonical = "{\"crv\":\"P-256\",\"kty\":\"EC\",\"x\":\"" + x + "\",\"y\":\"" + y + "\"}"; - final MessageDigest digest = MessageDigest.getInstance("SHA-256"); - final byte[] hash = digest.digest(canonical.getBytes(StandardCharsets.UTF_8)); + ECPublicKey publicKey = (ECPublicKey) generator.generateKeyPair().getPublic(); + String x = base64Url(publicKey.getW().getAffineX()); + String y = base64Url(publicKey.getW().getAffineY()); + String canonical = "{\"crv\":\"P-256\",\"kty\":\"EC\",\"x\":\"" + x + "\",\"y\":\"" + y + "\"}"; + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] hash = digest.digest(canonical.getBytes(StandardCharsets.UTF_8)); return toHex(hash); } catch (Exception ex) { Logger.printDebug(() -> "Could not compute Unison key ID", ex); @@ -154,8 +154,8 @@ private static String computeKeyId() { } private static String base64Url(BigInteger value) { - final byte[] bytes = value.toByteArray(); - final byte[] coordinate = new byte[32]; + byte[] bytes = value.toByteArray(); + byte[] coordinate = new byte[32]; int srcPos = 0; if (bytes.length > 32 && bytes[0] == 0) { srcPos = 1; @@ -167,7 +167,7 @@ private static String base64Url(BigInteger value) { } private static String toHex(byte[] bytes) { - final StringBuilder builder = new StringBuilder(bytes.length * 2); + StringBuilder builder = new StringBuilder(bytes.length * 2); for (byte b : bytes) { builder.append(Character.forDigit((b >> 4) & 0xF, 16)); builder.append(Character.forDigit(b & 0xF, 16)); @@ -196,7 +196,7 @@ private Lyrics parseLyrics(String format, String lyrics, String videoId, result.creditLines.isEmpty() ? null : result.creditLines, lyrics, "lrc", sourceUrl(videoId)), submitter); case "plain": - final List plain = LRCParser.parsePlain(lyrics); + List plain = LRCParser.parsePlain(lyrics); if (plain.isEmpty()) { return null; } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/YouTubeMusicProvider.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/YouTubeMusicProvider.java index f1f87776a2..b2c6a4f3a2 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/YouTubeMusicProvider.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/YouTubeMusicProvider.java @@ -49,29 +49,29 @@ public String name() { @Nullable @Override public FetchResult fetch(TrackInfo track) throws Exception { - final String videoId = VideoInformation.getVideoId(); + String videoId = VideoInformation.getVideoId(); if (videoId.isEmpty()) { return null; } - final String browseId = fetchLyricsBrowseId(videoId); + String browseId = fetchLyricsBrowseId(videoId); if (browseId == null) { return null; } - final Lyrics plainResult = fetchLyrics(browseId, false); + Lyrics plainResult = fetchLyrics(browseId, false); if (plainResult != null && !plainResult.isEmpty()) { if (plainResult.synced()) { return FetchResult.of(plainResult); } - final Lyrics timedResult = fetchLyrics(browseId, true); + Lyrics timedResult = fetchLyrics(browseId, true); if (timedResult != null && timedResult.synced()) { return FetchResult.of(timedResult); } return FetchResult.of(plainResult); } - final Lyrics androidResult = fetchLyrics(browseId, true); + Lyrics androidResult = fetchLyrics(browseId, true); if (androidResult != null && !androidResult.isEmpty()) { return FetchResult.of(androidResult); } @@ -87,7 +87,7 @@ private static String fetchLyricsBrowseId(String videoId) { body.put("videoId", videoId); body.put("playlistId", "RDAMVM" + videoId); - final HttpURLConnection conn = postInnertube(NEXT_URL, body.toString(), + HttpURLConnection conn = postInnertube(NEXT_URL, body.toString(), WEB_REMIX_VERSION, true); try { final int code = conn.getResponseCode(); @@ -109,7 +109,7 @@ private static String fetchLyricsBrowseId(String videoId) { @Nullable private static String extractLyricsBrowseId(JSONObject response) { try { - final JSONArray tabs = response + JSONArray tabs = response .getJSONObject("contents") .getJSONObject("singleColumnMusicWatchNextResultsRenderer") .getJSONObject("tabbedRenderer") @@ -140,14 +140,14 @@ private static String extractLyricsBrowseId(JSONObject response) { @Nullable private Lyrics fetchLyrics(String browseId, boolean useAndroidClient) { try { - final String clientVersion = useAndroidClient + String clientVersion = useAndroidClient ? ANDROID_MUSIC_VERSION : WEB_REMIX_VERSION; JSONObject body = new JSONObject(); body.put("context", buildContext( useAndroidClient ? "ANDROID_MUSIC" : "WEB_REMIX", clientVersion)); body.put("browseId", browseId); - final HttpURLConnection conn = postInnertube(BROWSE_URL, body.toString(), + HttpURLConnection conn = postInnertube(BROWSE_URL, body.toString(), clientVersion, false); try { final int code = conn.getResponseCode(); @@ -169,15 +169,15 @@ private Lyrics fetchLyrics(String browseId, boolean useAndroidClient) { @Nullable private static Lyrics parseLyricsResponse(JSONObject response) { JSONObject lyricsData = navigateTimedLyricsModel(response); - final String viaProvider = extractSourceProvider(lyricsData, response); - final String sourceTag = viaProvider != null + String viaProvider = extractSourceProvider(lyricsData, response); + String sourceTag = viaProvider != null ? "YouTube Music (via " + viaProvider + ")" : "YouTube Music"; if (lyricsData != null) { - final JSONArray timedArray = lyricsData.optJSONArray("timedLyricsData"); + JSONArray timedArray = lyricsData.optJSONArray("timedLyricsData"); if (timedArray != null && timedArray.length() > 0) { - final List lines = parseTimedLyrics(timedArray); + List lines = parseTimedLyrics(timedArray); if (!lines.isEmpty()) { boolean hasTimestamp = false; for (LyricsLine line : lines) { @@ -190,27 +190,27 @@ private static Lyrics parseLyricsResponse(JSONObject response) { return new Lyrics(lines, sourceTag, true, null, null, null, null, timedArray.toString(), "ytm.json", null); } - final StringBuilder sb = new StringBuilder(); + StringBuilder sb = new StringBuilder(); for (int i = 0; i < lines.size(); i++) { if (i > 0) sb.append('\n'); sb.append(lines.get(i).text()); } - final String plainText = sb.toString(); + String plainText = sb.toString(); return new Lyrics(LyricsRequests.parsePlainTextLines(plainText), sourceTag, false, null, null, null, null, plainText, "txt", null); } } - final String instrumental = lyricsData.optString("lyrics", ""); + String instrumental = lyricsData.optString("lyrics", ""); if (instrumental.contains("[Instrumental]") || instrumental.contains("♪")) { return Lyrics.NOT_FOUND; } } - final String plainText = extractPlainTextLyrics(response); + String plainText = extractPlainTextLyrics(response); if (plainText != null && !plainText.isEmpty()) { - final List lines = LyricsRequests.parsePlainTextLines(plainText); + List lines = LyricsRequests.parsePlainTextLines(plainText); if (!lines.isEmpty()) { return new Lyrics(lines, sourceTag, false, null, null, null, null, plainText, "txt", null); @@ -224,8 +224,8 @@ private static Lyrics parseLyricsResponse(JSONObject response) { private static String extractSourceProvider(@Nullable JSONObject lyricsData, JSONObject response) { if (lyricsData != null) { - final String source = lyricsData.optString("sourceMessage", ""); - final String parsed = parseSourceProvider(source); + String source = lyricsData.optString("sourceMessage", ""); + String parsed = parseSourceProvider(source); if (parsed != null) { return parsed; } @@ -238,7 +238,7 @@ private static String extractSourceProvider(@Nullable JSONObject lyricsData, .getJSONObject(0) .getJSONObject("musicDescriptionShelfRenderer") .getJSONObject("footer"); - final String source = footer.getJSONArray("runs") + String source = footer.getJSONArray("runs") .getJSONObject(0).optString("text", ""); return parseSourceProvider(source); } catch (Exception ex) { @@ -250,7 +250,7 @@ private static String extractSourceProvider(@Nullable JSONObject lyricsData, @Nullable private static String parseSourceProvider(String sourceMessage) { if (sourceMessage.startsWith("Source: ")) { - final String provider = sourceMessage.substring(8).trim(); + String provider = sourceMessage.substring(8).trim(); return provider.isEmpty() ? null : provider; } return null; @@ -277,7 +277,7 @@ private static JSONObject navigateTimedLyricsModel(JSONObject response) { @Nullable private static String extractPlainTextLyrics(JSONObject response) { try { - final JSONArray sectionContents = response + JSONArray sectionContents = response .getJSONObject("contents") .getJSONObject("sectionListRenderer") .getJSONArray("contents"); @@ -294,9 +294,9 @@ private static String extractPlainTextLyrics(JSONObject response) { if (description == null) { continue; } - final JSONArray runs = description.optJSONArray("runs"); + JSONArray runs = description.optJSONArray("runs"); if (runs != null && runs.length() > 0) { - final StringBuilder sb = new StringBuilder(); + StringBuilder sb = new StringBuilder(); for (int r = 0; r < runs.length(); r++) { JSONObject run = runs.optJSONObject(r); if (run != null) { @@ -313,13 +313,13 @@ private static String extractPlainTextLyrics(JSONObject response) { } private static List parseTimedLyrics(JSONArray timedArray) { - final List lines = new ArrayList<>(); + List lines = new ArrayList<>(); for (int i = 0; i < timedArray.length(); i++) { JSONObject entry = timedArray.optJSONObject(i); if (entry == null) { continue; } - final String text = entry.optString("lyricLine", "").trim(); + String text = entry.optString("lyricLine", "").trim(); if (text.isEmpty()) { continue; } @@ -335,7 +335,7 @@ private static long parseTimestamp(JSONObject cueRange, String key) { if (cueRange == null) { return LyricsLine.NO_TIME; } - final String raw = cueRange.optString(key, ""); + String raw = cueRange.optString(key, ""); if (raw.isEmpty()) { return LyricsLine.NO_TIME; } @@ -364,7 +364,7 @@ private static JSONObject buildContext(String clientName, String clientVersion) private static HttpURLConnection postInnertube(String url, String jsonBody, String clientVersion, boolean sendAuth) throws Exception { - final HttpURLConnection conn = Requester.openConnection(url); + HttpURLConnection conn = Requester.openConnection(url); conn.setRequestMethod("POST"); conn.setConnectTimeout(CONNECT_TIMEOUT_MS); conn.setReadTimeout(READ_TIMEOUT_MS); @@ -385,7 +385,7 @@ private static HttpURLConnection postInnertube(String url, String jsonBody, } } - final byte[] bytes = jsonBody.getBytes(java.nio.charset.StandardCharsets.UTF_8); + byte[] bytes = jsonBody.getBytes(java.nio.charset.StandardCharsets.UTF_8); conn.setFixedLengthStreamingMode(bytes.length); try (java.io.OutputStream out = conn.getOutputStream()) { out.write(bytes); diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/AlphabetRomanizer.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/AlphabetRomanizer.java index 72955fc31d..2c2c2666c5 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/AlphabetRomanizer.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/AlphabetRomanizer.java @@ -60,16 +60,16 @@ String romanize(String line) { continue; } - final String base = base(c); + String base = base(c); if (i + 1 < line.length() && pairsWith(line.charAt(i + 1))) { - final String pair = digraphs.get(base + base(line.charAt(i + 1))); + String pair = digraphs.get(base + base(line.charAt(i + 1))); if (pair != null) { word.append(cased(pair, c)); i += 2; continue; } } - final String reading = letters.get(base); + String reading = letters.get(base); word.append(reading != null ? cased(reading, c) : String.valueOf(c)); i++; } @@ -104,6 +104,7 @@ private static String cased(String reading, char source) { private static void append(StringBuilder out, String word) { if (word.isEmpty()) return; + //noinspection SizeReplaceableByIsEmpty if (out.length() > 0) out.append(' '); out.append(word); } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/JyutpingRomanizer.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/JyutpingRomanizer.java index 10692720c0..031682cae4 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/JyutpingRomanizer.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/JyutpingRomanizer.java @@ -20,7 +20,7 @@ private JyutpingRomanizer() { } public static String romanize(String line) { - final WordReadingTable table = CantoneseDictStore.word(); + WordReadingTable table = CantoneseDictStore.word(); return HanRomanizer.romanize(line, new HanRomanizer.Readings() { @Nullable @Override diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/LocalRomanizer.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/LocalRomanizer.java index 8e8321102b..50b2d9d12b 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/LocalRomanizer.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/LocalRomanizer.java @@ -79,7 +79,7 @@ public static Result romanize(List lines) { List result = new ArrayList<>(lines.size()); for (String line : lines) { - final Read read = readAs(line, ScriptDetector.detect(line), cantonese); + Read read = readAs(line, ScriptDetector.detect(line), cantonese); if (read.dictionary() != null) dictionary = read.dictionary(); if (!read.text().equals(line)) changed = true; result.add(read.text()); @@ -102,10 +102,10 @@ public static List alignWords(List lines, boolean aligned = false; for (int i = 0; i < lines.size(); i++) { - final LyricsLine line = lines.get(i); + LyricsLine line = lines.get(i); // Kana is what marks a line Japanese; a word of kanji alone says only Han, and would // then be looked up in the wrong dictionary. - final Script script = ScriptDetector.detect(line.text()); + Script script = ScriptDetector.detect(line.text()); List read = exactReadings(line.words(), script, cantonese); if (read == null) { read = splitReading(line, wholeLine.get(i).text()); @@ -129,11 +129,11 @@ public static List alignWords(List lines, private static List exactReadings(List words, @Nullable Script script, boolean cantonese) { if (words.isEmpty() || script == null) return null; - final List read = new ArrayList<>(words.size()); + List read = new ArrayList<>(words.size()); boolean any = false; for (Word word : words) { - final String reading = readAs(word.text(), script, cantonese).text(); + String reading = readAs(word.text(), script, cantonese).text(); if (reading.equals(word.text())) { read.add(word); continue; @@ -153,27 +153,27 @@ private static List exactReadings(List words, @Nullable Script scrip */ @Nullable private static List splitReading(LyricsLine line, @Nullable String wholeLine) { - final List words = line.words(); + List words = line.words(); if (wholeLine == null || words.isEmpty()) return null; - final String original = line.text(); - final String reading = wholeLine.trim(); + String original = line.text(); + String reading = wholeLine.trim(); if (reading.isEmpty() || original == null || reading.equalsIgnoreCase(original.trim())) { return null; } - final int[][] tokens = readingTokens(wholeLine); - final int[][] spread = spreadReading(words, wholeLine); + int[][] tokens = readingTokens(wholeLine); + int[][] spread = spreadReading(words, wholeLine); if (tokens.length == 0 || spread == null) return null; - final List read = new ArrayList<>(words.size()); + List read = new ArrayList<>(words.size()); for (int i = 0; i < words.size(); i++) { - final Word word = words.get(i); - final int[] range = spread[i]; + Word word = words.get(i); + int[] range = spread[i]; if (range[1] <= range[0]) { read.add(word); continue; } - final StringBuilder syllables = new StringBuilder(); + StringBuilder syllables = new StringBuilder(); for (int at = range[0]; at < range[1]; at++) { if (syllables.length() > 0) syllables.append(' '); syllables.append(reading, tokens[at][0], tokens[at][1]); @@ -192,7 +192,7 @@ public static int[][] readingTokens(@Nullable String reading) { if (reading == null) { return NO_TOKENS; } - final List tokens = new ArrayList<>(); + List tokens = new ArrayList<>(); int at = 0; while (at < reading.length()) { while (at < reading.length() && Character.isWhitespace(reading.charAt(at))) at++; @@ -214,10 +214,10 @@ public static int[][] readingTokens(@Nullable String reading) { */ @Nullable public static int[][] spreadReading(List words, @Nullable String reading) { - final int[][] tokens = readingTokens(reading); + int[][] tokens = readingTokens(reading); if (tokens.length == 0 || words.isEmpty()) return null; - final int[] spoken = new int[words.size()]; + int[] spoken = new int[words.size()]; int syllables = 0; int carried = 0; for (int i = 0; i < words.size(); i++) { @@ -229,7 +229,7 @@ public static int[][] spreadReading(List words, @Nullable String reading) final int length = tokens.length; if (length != syllables && length != carried) return null; - final int[][] spread = new int[words.size()][]; + int[][] spread = new int[words.size()][]; int token = 0; int placed = 0; for (int i = 0; i < words.size(); i++) { @@ -274,10 +274,10 @@ private static boolean isOnePerSyllable(char c) { private static void attachPunctuation(List read) { for (int i = 0; i < read.size(); i++) { if (read.get(i).romaji() != null) continue; - final String text = read.get(i).text().trim(); + String text = read.get(i).text().trim(); if (text.isEmpty() || speaks(text)) continue; for (int at = i - 1; at >= 0; at--) { - final Word previous = read.get(at); + Word previous = read.get(at); // One that already gave its marks away has nothing left to carry more. if (previous.romaji() == null || previous.romaji().isEmpty()) continue; if (!previous.romaji().endsWith(text)) { diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/PinyinRomanizer.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/PinyinRomanizer.java index 9e19406845..29ec8e3d00 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/PinyinRomanizer.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/PinyinRomanizer.java @@ -36,7 +36,7 @@ private PinyinRomanizer() { /** Pinyin for a Chinese line, with a space between readings. */ public static String romanize(String line) { - final WordReadingTable table = PinyinDictStore.word(); + WordReadingTable table = PinyinDictStore.word(); return HanRomanizer.romanize(line, new HanRomanizer.Readings() { @Nullable @Override diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/ScriptDetector.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/ScriptDetector.java index ec92fffb9a..a60f088004 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/ScriptDetector.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/ScriptDetector.java @@ -121,7 +121,7 @@ public static boolean contains(@Nullable CharSequence text, Character.UnicodeScr for (int i = 0; i < text.length(); ) { final int cp = Character.codePointAt(text, i); i += Character.charCount(cp); - final Character.UnicodeScript script = Character.UnicodeScript.of(cp); + Character.UnicodeScript script = Character.UnicodeScript.of(cp); for (Character.UnicodeScript wanted : scripts) { if (script == wanted) return true; } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/session/MiniplayerLyrics.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/session/MiniplayerLyrics.java index 4662b30993..018fafc979 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/session/MiniplayerLyrics.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/session/MiniplayerLyrics.java @@ -255,13 +255,13 @@ private static void restoreIfMirrored() { /** The title the app itself shows: the untouched metadata title when it is known. */ private static String displayTitleOr(TrackInfo track) { - final String raw = rawDisplayTitle; + String raw = rawDisplayTitle; return raw != null && !raw.trim().isEmpty() ? raw : track.title(); } /** The artist the app itself shows: the untouched metadata artist when it is known. */ private static String displayArtistOr(TrackInfo track) { - final String raw = rawDisplayArtist; + String raw = rawDisplayArtist; return raw != null && !raw.trim().isEmpty() ? raw : track.artist(); } @@ -271,8 +271,8 @@ private static String displayArtistOr(TrackInfo track) { * fullwidth punctuation to ASCII, which must not leak into what is displayed. */ private static String buildSubtitle(TrackInfo track) { - final String rawTitle = rawDisplayTitle; - final String rawArtist = rawDisplayArtist; + String rawTitle = rawDisplayTitle; + String rawArtist = rawDisplayArtist; if (rawTitle == null || rawTitle.trim().isEmpty() || rawArtist == null || rawArtist.trim().isEmpty()) { return LyricsPresentation.trackLabel(track.title(), track.artist()); @@ -282,11 +282,11 @@ private static String buildSubtitle(TrackInfo track) { private static void restoreField(TextView view, @Nullable String lastMirrored, @Nullable CharSequence preMirror, @Nullable String fallback) { - final CharSequence current = view.getText(); + CharSequence current = view.getText(); if (!TextUtils.equals(current, lastMirrored)) { return; } - final CharSequence restore = (preMirror != null && !preMirror.toString().isEmpty()) + CharSequence restore = (preMirror != null && !preMirror.toString().isEmpty()) ? preMirror : fallback; if (restore != null && !TextUtils.equals(current, restore)) { diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/storage/LyricsCache.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/storage/LyricsCache.java index c8d28258b9..026ead0ed7 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/storage/LyricsCache.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/storage/LyricsCache.java @@ -91,7 +91,7 @@ private LyricsCache() { @Nullable public static Lyrics get(TrackInfo track, String source, @Nullable String videoId) { - final String key = key(track, source); + String key = key(track, source); Lyrics lyrics = memoryCache.get(key); if (lyrics == null) { lyrics = readFromDisk(key); @@ -168,7 +168,7 @@ public static LyricsPreference getPreference(@Nullable String videoId, TrackInfo + " videoFile=" + name(videoFile) + " trackFile=" + name(file)); return null; } - final File source = file; + File source = file; Logger.printInfo(() -> "LyricsPref read: " + source.getName()); LyricsPreference preference = readPreferenceFile(source, @@ -553,7 +553,7 @@ private static File translationFile(TrackInfo track, String source, String langu private static File romanizationFile(TrackInfo track, String source, List sourceLines) { // Only a chosen language changes the result, so "automatic" keeps the file it had. - final String language = Settings.LYRICS_ROMANIZE_LANGUAGE.get(); + String language = Settings.LYRICS_ROMANIZE_LANGUAGE.get(); return derivedCacheFile(track, source, LyricsRequests.isDefaultRomanizationLanguage() ? ".rom.txt" : ".rom." + language.toLowerCase(Locale.ROOT) + ".txt", @@ -883,7 +883,7 @@ private static Lyrics parseContentLines(String content, boolean synced, String p // An LRC line cannot hold a reading on the word or the reading keyed by language, and // both are gone for good once written as one. Reading the file back brings them. if (TTML_FORMAT_TYPE.equals(formatType) && rawFormat != null && !rawFormat.isEmpty()) { - final Lyrics reread = TTMLParser.ttmlToLyrics(rawFormat, provider, sourceUrl); + Lyrics reread = TTMLParser.ttmlToLyrics(rawFormat, provider, sourceUrl); if (reread != null && !reread.isEmpty()) { return withAbout(reread, about); } @@ -957,7 +957,7 @@ private static void writeToDisk(String key, Lyrics lyrics) { try { List fileLines = new ArrayList<>(); - final String videoId = videoIdByKey.get(key); + String videoId = videoIdByKey.get(key); if (lyrics == Lyrics.NOT_FOUND || lyrics.isEmpty()) { // The marker ends the headers, so the video has to be named before it. appendVideoId(fileLines, videoId); @@ -1055,8 +1055,8 @@ private static synchronized void trimDiskCache() { return; } - final long[] modified = new long[mains.size()]; - final Integer[] oldestFirst = new Integer[mains.size()]; + long[] modified = new long[mains.size()]; + Integer[] oldestFirst = new Integer[mains.size()]; for (int i = 0; i < mains.size(); i++) { modified[i] = mains.get(i).lastModified(); oldestFirst[i] = i; @@ -1126,8 +1126,8 @@ private static String preferenceKey(@Nullable String videoId, TrackInfo track) { */ public static long clear() { long freed = 0; - final File directory = offlineDirectory(); - final File[] files = directory == null ? null : directory.listFiles(); + File directory = offlineDirectory(); + File[] files = directory == null ? null : directory.listFiles(); if (files != null) { for (File file : files) { freed += file.isDirectory() ? 0 : file.length(); diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/storage/TrackOffsetStore.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/storage/TrackOffsetStore.java index d472dac4b4..add760e13c 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/storage/TrackOffsetStore.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/storage/TrackOffsetStore.java @@ -13,12 +13,12 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import app.morphe.extension.music.patches.lyrics.model.TrackInfo; -import app.morphe.extension.shared.Utils; - import java.util.ArrayList; import java.util.List; +import app.morphe.extension.music.patches.lyrics.model.TrackInfo; +import app.morphe.extension.shared.Utils; + /** * The timing offset the user dragged out for one track, kept so a loop, a trip to another song * and a restart all find it again. @@ -58,7 +58,7 @@ public static void put(@NonNull TrackInfo track, int ms) { if (preferences == null) { return; } - final String key = key(track); + String key = key(track); SharedPreferences.Editor editor = preferences.edit(); if (ms == 0) { editor.remove(key); @@ -76,7 +76,7 @@ public static void put(@NonNull TrackInfo track, int ms) { private static List order(SharedPreferences preferences, String key, boolean present) { List order = new ArrayList<>(); - final String stored = preferences.getString(KEY_ORDER, null); + String stored = preferences.getString(KEY_ORDER, null); if (stored != null) { for (String entry : stored.split(SEPARATOR)) { if (!entry.isEmpty() && !entry.equals(key)) { diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsColors.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsColors.java index 093ad5a430..7325e29e42 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsColors.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsColors.java @@ -19,10 +19,16 @@ final class LyricsColors { static final String APP_SECONDARY_TEXT_COLOR = "ytm_text_color_secondary"; /** Dimmed until the word is sung. */ - static final float UNSUNG_ALPHA = 0.4f; + static final float UNSUNG_ALPHA = 0.35f; - /** Smaller than the lyric it reads, so at the same weight the two would read as one line. */ - static final float ACTIVE_READING_ALPHA = 0.4f; + /** The reading of the active line once it is lit, or all along when it is not read word by word. */ + static final float ACTIVE_READING_ALPHA = 0.85f; + + /** The translation of the active line. */ + static final float ACTIVE_TRANSLATION_ALPHA = 0.6f; + + /** A background vocal while it is shown. */ + static final float ACTIVE_BG_ALPHA = 0.8f; static int secondaryTextColor() { ensureColorCache(); @@ -34,7 +40,7 @@ private static int computeSecondaryColor(int base) { if (secondary != 0) { return secondary; } - return Color.argb(0x66, Color.red(base), Color.green(base), Color.blue(base)); + return Color.argb(0x99, Color.red(base), Color.green(base), Color.blue(base)); } private static int cachedLineColor; diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsFit.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsFit.java index 6984416e1b..83054bbd5b 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsFit.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsFit.java @@ -71,12 +71,12 @@ private static boolean hasOrphanTail(Layout layout, CharSequence text) { private static Layout buildCandidateLayout(TextView view, int sizePx, int width, CharSequence text, float letterSpacingEm, @Nullable String fontVariation) { - final TextPaint paint = new TextPaint(view.getPaint()); + TextPaint paint = new TextPaint(view.getPaint()); paint.setTextSize(sizePx); paint.setLetterSpacing(letterSpacingEm); paint.setFontVariationSettings(fontVariation); - final Layout current = view.getLayout(); - final Layout.Alignment alignment = current != null + Layout current = view.getLayout(); + Layout.Alignment alignment = current != null ? current.getAlignment() : Layout.Alignment.ALIGN_NORMAL; return StaticLayout.Builder.obtain(text, 0, text.length(), paint, width) .setAlignment(alignment) @@ -90,9 +90,9 @@ private static Layout buildCandidateLayout(TextView view, int sizePx, int width, private static boolean widthAxisSupported(TextPaint paint) { if (widthAxisSupport == null) { - final TextPaint probe = new TextPaint(paint); + TextPaint probe = new TextPaint(paint); probe.setTextSize(100f); - final String[] sample = {"WAVEWIDTH", "MOMENT", "LONGERSONGTEXT"}; + String[] sample = {"WAVEWIDTH", "MOMENT", "LONGERSONGTEXT"}; float before = 0f; for (String part : sample) { before += probe.measureText(part); @@ -128,7 +128,7 @@ private static CompressionFit findCompression(TextView view, int sizePx, int wid for (int i = 1; i <= FIT_MAX_STEPS; i++) { final float spacing = Math.max(FIT_MIN_LETTER_SPACING_EM, -i * FIT_LETTER_SPACING_STEP_EM); - final String variation = "'wdth' " + String variation = "'wdth' " + Math.max(FIT_MIN_WIDTH_AXIS, 100f - i * FIT_WIDTH_AXIS_STEP); if (!hasOrphanTail(buildCandidateLayout(view, sizePx, width, text, spacing, variation), text)) { @@ -156,7 +156,7 @@ static boolean applyOrphanFit(TextView view, int width, CharSequence text, float spacing = 0f; String variation = null; if (hasOrphanTail(base, text)) { - final CompressionFit fit = findCompression(view, sizePx, width, text); + CompressionFit fit = findCompression(view, sizePx, width, text); if (fit != null) { spacing = fit.letterSpacingEm; variation = fit.fontVariation; @@ -205,7 +205,7 @@ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { if (width <= 0) { return; } - final CharSequence text = getText(); + CharSequence text = getText(); if (text == null || text.length() == 0) { return; } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsLineView.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsLineView.java index e61a11a33c..45361a817b 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsLineView.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsLineView.java @@ -9,6 +9,7 @@ import android.animation.Animator; import android.animation.AnimatorListenerAdapter; +import android.animation.TimeInterpolator; import android.animation.ValueAnimator; import android.annotation.SuppressLint; import android.content.Context; @@ -19,23 +20,50 @@ import android.graphics.PorterDuff; import android.graphics.PorterDuffColorFilter; import android.icu.text.BreakIterator; -import android.os.Build; import android.text.Layout; +import android.text.StaticLayout; +import android.text.TextPaint; +import android.view.Gravity; import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.LinearInterpolator; +import android.view.animation.PathInterpolator; import android.widget.TextView; + import androidx.annotation.Nullable; + +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; +import java.util.Locale; +import java.util.Map; import java.util.Objects; +import java.util.Set; + +import app.morphe.extension.music.patches.lyrics.romanizers.ScriptDetector; import app.morphe.extension.music.settings.Settings; import app.morphe.extension.shared.Logger; +import app.morphe.extension.shared.ui.Dim; final class LyricsLineView extends TextView { private static final int REVEAL_CONTENT = 0; private static final int REVEAL_TRANS = 1; private static final int REVEAL_ROMA = 2; + /** A background vocal opens over this long when its lead line comes up, and closes faster. */ + private static final long SECONDARY_EXPAND_MILLISECONDS = 400; + private static final long SECONDARY_COLLAPSE_MILLISECONDS = 320; + /** A little ahead of the growth, so the vocal is readable before it settles. */ + private static final long SECONDARY_FADE_IN_MILLISECONDS = 380; + /** A closed background vocal is drawn this much smaller, so it grows out of its lead line. */ + private static final float SECONDARY_CLOSED_SCALE = 0.8f; + private static final TimeInterpolator SECONDARY_EXPAND_EASING = + new PathInterpolator(0.41f, 0f, 0.12f, 0.99f); + private List wordTimings = Collections.emptyList(); long positionMs = Long.MIN_VALUE; boolean allSung = false; @@ -61,6 +89,10 @@ final class LyricsLineView extends TextView { private CharSequence cachedText; private String cachedTextStr; + /** The hand-built layout of a line drawn with syllable readings, see {@link #syllableCacheFor}. */ + @Nullable + private SyllableRomanCache syllableCache; + @SuppressWarnings("ConstantValue") @Override public void setText(CharSequence text, BufferType type) { @@ -70,6 +102,7 @@ public void setText(CharSequence text, BufferType type) { allSung = false; cachedLayout = null; cachedText = null; + syllableCache = null; if (fittedState != null && !Objects.equals(fittedState.text, text)) { fittedState.width = -1; } @@ -83,6 +116,7 @@ public void setText(CharSequence text, BufferType type) { int romaStart = -1; int romaEnd = -1; boolean perWordReading; + boolean showSyllableRomanization; boolean smallRunHoldsWords; float lastTouchY; private final Paint layerPaint = new Paint(); @@ -90,6 +124,8 @@ public void setText(CharSequence text, BufferType type) { private int tintFilterColor; private int baseTextAlpha = 255; float lineAlpha = 1f; + float lineScale = 1f; + float fadeTargetAlpha = 1f; boolean activeLine; private int secondaryActiveColor; ValueAnimator fadeAnimator; @@ -101,10 +137,165 @@ public void setText(CharSequence text, BufferType type) { private ValueAnimator transRevealAnimator; private ValueAnimator romaRevealAnimator; + /** A background vocal, drawn in its lead line's row and only open around that line. */ + private boolean isSecondary; + private int parentLineIndex = -1; + /** Sung ahead of its lead line, so it sits above it and opens upwards. */ + private boolean bgBeforeMain; + private boolean isRightAligned; + /** How far a background vocal is open. Its measured height follows this. */ + private float secondaryFraction = 1f; + private boolean secondaryExpanded = true; + private int secondaryFullHeight; + @Nullable + private ValueAnimator secondaryAnimator; + LyricsLineView(Context context) { super(context); } + void setSecondary(boolean secondary) { + isSecondary = secondary; + } + + boolean isSecondary() { + return isSecondary; + } + + void setParentLineIndex(int index) { + parentLineIndex = index; + } + + int getParentLineIndex() { + return parentLineIndex; + } + + void setBgBeforeMain(boolean beforeMain) { + bgBeforeMain = beforeMain; + } + + void setIsRightAligned(boolean rightAligned) { + isRightAligned = rightAligned; + } + + boolean isRightAligned() { + return isRightAligned; + } + + /** The gap to the lead line, on whichever side of it this background vocal sits. */ + private void updateSecondaryMargin(int marginPx) { + if (getLayoutParams() instanceof ViewGroup.MarginLayoutParams params) { + params.topMargin = bgBeforeMain ? 0 : marginPx; + params.bottomMargin = bgBeforeMain ? marginPx : 0; + } + } + + /** + * Opens or closes a background vocal. A closed one is gone from the layout, so its lead line + * takes only its own room until the vocal is sung. + * + * @param instant Skips the animation, for a row that was only just built. + */ + void animateSecondaryExpansion(boolean expand, boolean instant) { + // Called on every highlight tick, so one already on its way there is left to finish. + if (!instant && expand == secondaryExpanded + && (secondaryAnimator != null || secondaryFraction == (expand ? 1f : 0f))) { + return; + } + // Cleared first, or the cancelled animator's end listener settles the old target. + ValueAnimator running = secondaryAnimator; + secondaryAnimator = null; + if (running != null) { + running.cancel(); + } + secondaryExpanded = expand; + if (instant) { + settleSecondary(expand); + return; + } + + setVisibility(View.VISIBLE); + final float startFraction = secondaryFraction; + final float startAlpha = getAlpha(); + ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f); + if (expand) { + animator.setDuration(SECONDARY_EXPAND_MILLISECONDS); + animator.setInterpolator(new LinearInterpolator()); + } else { + animator.setDuration(SECONDARY_COLLAPSE_MILLISECONDS); + animator.setInterpolator(LyricsViewSupport.FAST_OUT_SLOW_IN); + } + animator.addUpdateListener(animation -> { + final float progress = (float) animation.getAnimatedValue(); + final float alpha; + if (expand) { + secondaryFraction = startFraction + (1f - startFraction) + * SECONDARY_EXPAND_EASING.getInterpolation(progress); + final float fade = Math.min(1f, progress * SECONDARY_EXPAND_MILLISECONDS + / (float) SECONDARY_FADE_IN_MILLISECONDS); + alpha = startAlpha + (LyricsColors.ACTIVE_BG_ALPHA - startAlpha) + * LyricsViewSupport.LINEAR_OUT_SLOW_IN.getInterpolation(fade); + } else { + secondaryFraction = startFraction * (1f - progress); + alpha = startAlpha * (1f - progress); + } + applySecondaryFrame(alpha); + }); + animator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + if (secondaryAnimator == animation) { + secondaryAnimator = null; + settleSecondary(expand); + } + } + }); + secondaryAnimator = animator; + animator.start(); + } + + /** Grows out of the lead line's side while it opens, and shrinks back into it. */ + private void applySecondaryFrame(float alpha) { + final int fullHeight = secondaryFullHeight > 0 ? secondaryFullHeight : getHeight(); + final float scale = SECONDARY_CLOSED_SCALE + (1f - SECONDARY_CLOSED_SCALE) * secondaryFraction; + setPivotX(isRightAligned ? getWidth() : 0f); + setPivotY(bgBeforeMain ? fullHeight : 0f); + setScaleX(scale); + setScaleY(scale); + final float slide = Math.min(Dim.dp(18), Math.max(Dim.dp8, fullHeight * 0.35f)); + setTranslationY((bgBeforeMain ? 1f : -1f) * (1f - secondaryFraction) * slide); + setAlpha(alpha); + updateSecondaryMargin(Math.round(Dim.dp4 * secondaryFraction)); + requestLayout(); + invalidate(); + } + + private void settleSecondary(boolean expanded) { + secondaryFraction = expanded ? 1f : 0f; + final float scale = expanded ? 1f : SECONDARY_CLOSED_SCALE; + setScaleX(scale); + setScaleY(scale); + setTranslationY(0f); + setAlpha(expanded ? LyricsColors.ACTIVE_BG_ALPHA : 0f); + updateSecondaryMargin(expanded ? Dim.dp4 : 0); + setVisibility(expanded ? View.VISIBLE : View.GONE); + requestLayout(); + invalidate(); + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + if (isSecondary) { + final int pivotH = secondaryFullHeight > 0 ? secondaryFullHeight : h; + setPivotX(isRightAligned ? w : 0f); + setPivotY(bgBeforeMain ? pivotH : 0f); + } else { + setPivotX(isRightAligned ? w - getPaddingRight() : getPaddingLeft()); + setPivotY(h * 0.5f); + } + } + @Override public void setTextColor(int color) { baseTextAlpha = Color.alpha(color); @@ -112,14 +303,40 @@ public void setTextColor(int color) { } void setTranslationBounds(int start, int end) { - transStart = start; - transEnd = end; + if (transStart != start || transEnd != end) { + transStart = start; + transEnd = end; + syllableCache = null; + } } void setRomanizationBounds(int start, int end, boolean perWord) { - romaStart = start; - romaEnd = end; - perWordReading = perWord; + setRomanizationBounds(start, end, perWord, false); + } + + void setRomanizationBounds(int start, int end, boolean perWord, boolean syllableInline) { + if (romaStart != start || romaEnd != end + || perWordReading != perWord + || showSyllableRomanization != syllableInline) { + romaStart = start; + romaEnd = end; + perWordReading = perWord; + showSyllableRomanization = syllableInline; + syllableCache = null; + requestLayout(); + invalidate(); + } + } + + boolean hasRomanizationRegion() { + return romaStart >= 0 || showSyllableRomanization; + } + + int syllableRomanizationExtraHeight() { + if (!showSyllableRomanization || syllableCache == null) { + return 0; + } + return syllableCache.extraRomanHeightPx; } /** @@ -174,10 +391,15 @@ void cancelRevealAnimators() { romaRevealAnimator.cancel(); romaRevealAnimator = null; } + if (secondaryAnimator != null) { + ValueAnimator running = secondaryAnimator; + secondaryAnimator = null; + running.cancel(); + } } private void animateReveal(int kind, boolean entering) { - final ValueAnimator running = revealAnimator(kind); + ValueAnimator running = revealAnimator(kind); if (running != null) { running.cancel(); } @@ -187,7 +409,7 @@ private void animateReveal(int kind, boolean entering) { invalidate(); return; } - final ValueAnimator animator = ValueAnimator.ofFloat(reveal(kind), target); + ValueAnimator animator = ValueAnimator.ofFloat(reveal(kind), target); animator.setDuration(LyricsPanelView.SECONDARY_REVEAL_MILLISECONDS); animator.addUpdateListener(animation -> { setReveal(kind, (float) animation.getAnimatedValue()); @@ -237,6 +459,15 @@ private void setRevealAnimator(int kind, ValueAnimator animator) { @Nullable String getCopyTextForTouch(float touchY) { + SyllableRomanCache syllables = syllableCache; + if (syllables != null && syllables.hasSyllableRomanization) { + final float y = touchY - textOriginY(); + if (syllables.transLayout != null && (syllables.mainTopY > 0 + ? y < syllables.mainTopY : y >= syllables.transTopY)) { + return syllables.transText; + } + return syllables.mainText != null ? syllables.mainText.trim() : null; + } Layout layout = getLayout(); if (layout == null) { return null; @@ -295,6 +526,10 @@ void setHighlight(List timings, long posMs, boolea this.originalTextStart = origStart; if (structuralChange) { cachedLayout = null; + syllableCache = null; + if (showSyllableRomanization) { + requestLayout(); + } } invalidate(); } @@ -312,9 +547,9 @@ private void ensureWordCache(Layout layout, CharSequence text, Paint paint, cachedWordTrailX = new float[cachedWordCount]; cachedWordLine = new int[cachedWordCount]; cachedWordWrappedLine = new int[cachedWordCount]; - final int[] clusters = clusterBoundaries(text); + int[] clusters = clusterBoundaries(text); for (int i = 0; i < cachedWordCount; i++) { - final LyricsSpanBuilder.WordTiming timing = timings.get(i); + LyricsSpanBuilder.WordTiming timing = timings.get(i); final int s = timing.start() + origStart; final int e = Math.min(timing.end() + origStart, text.length()); if (s < 0 || s >= text.length() || s >= e) { @@ -375,29 +610,10 @@ private void ensureWordCache(Layout layout, CharSequence text, Paint paint, } } - /** - * The end offset reports along the line's primary direction, so a word ending its line comes back - * describing the next one and has to be sent to the far edge of the line it is on. Which side is - * far follows the line, and a right-to-left word reports its end left of its own lead, so the test - * mirrors with it: read as left-to-right such a word goes to the right edge, where its sweep grows - * the wrong way and shrinks the lit area as it is sung. - */ - private static float trailOnLine(float lineLeft, float lineRight, float lead, float end, - boolean rightToLeft) { - if (rightToLeft) { - if (end >= lead) { - return lineLeft; - } - } else if (end <= lead) { - return lineRight; - } - return Math.min(Math.max(end, lineLeft), lineRight); - } - private static int[] clusterBoundaries(CharSequence text) { final int length = text.length(); - final int[] boundaries = new int[length + 1]; - final BreakIterator iterator = BreakIterator.getCharacterInstance(); + int[] boundaries = new int[length + 1]; + BreakIterator iterator = BreakIterator.getCharacterInstance(); iterator.setText(text.toString()); int clusterStart = iterator.first(); while (clusterStart != BreakIterator.DONE && clusterStart <= length) { @@ -450,7 +666,7 @@ private void markSecondaryLines(Layout layout, int start, int end, boolean[] fla if (start < 0 || end <= start || flags.length == 0) { return; } - final CharSequence text = layout.getText(); + CharSequence text = layout.getText(); final int textLength = text.length(); if (start >= textLength) { return; @@ -485,9 +701,9 @@ private void drawTextRun(Canvas canvas, Layout layout, int firstLine, int lastLi canvas.save(); canvas.translate(textOriginX(), textOriginY()); canvas.clipRect(0, clipTop, contentWidth, bottom); - final Paint textPaint = getPaint(); + Paint textPaint = getPaint(); textPaint.setColor(getCurrentTextColor() | 0xFF000000); - final ColorFilter previousFilter = textPaint.getColorFilter(); + ColorFilter previousFilter = textPaint.getColorFilter(); if (tintColor != 0) { textPaint.setColorFilter(tintFilter(tintColor)); } @@ -519,11 +735,11 @@ private PorterDuffColorFilter tintFilter(int color) { * @return False if nothing was drawn and the default text drawing should be used. */ private boolean drawBaseText(Canvas canvas) { - final Layout layout = getLayout(); + Layout layout = getLayout(); if (layout == null) { return false; } - if (getWidth() <= 0 || getHeight() <= 0) { + if (getWidth() <= 0 || (!isSecondary && getHeight() <= 0)) { return false; } @@ -535,14 +751,18 @@ private boolean drawBaseText(Canvas canvas) { final float contentNow = contentReveal; final int mainAlpha = Math.round(((unsungColor != 0 && !wordTimings.isEmpty()) ? LyricsColors.UNSUNG_ALPHA * 255f : baseTextAlpha) * lineAlpha * contentNow); - final float secondaryWeight = lineAlpha - * (activeLine ? 255f : Color.alpha(LyricsColors.secondaryTextColor())); - final int secondaryAlpha = Math.round(secondaryWeight * contentNow); - final int romaAlpha = activeLine && perWordReading && Settings.LYRICS_WORD_SYNC.get() - ? Math.round(secondaryWeight * LyricsColors.ACTIVE_READING_ALPHA * contentNow) - : secondaryAlpha; - - final boolean[] secondaryLine = new boolean[lineCount]; + final float secondaryBaseAlpha = activeLine + ? LyricsColors.ACTIVE_TRANSLATION_ALPHA * 255f + : Color.alpha(LyricsColors.secondaryTextColor()); + final int secondaryAlpha = Math.round(lineAlpha * secondaryBaseAlpha * contentNow); + final float readingBaseAlpha = activeLine + ? (perWordReading && Settings.LYRICS_WORD_SYNC.get() + ? LyricsColors.UNSUNG_ALPHA * 255f + : LyricsColors.ACTIVE_READING_ALPHA * 255f) + : Color.alpha(LyricsColors.secondaryTextColor()); + final int romaAlpha = Math.round(lineAlpha * readingBaseAlpha * contentNow); + + boolean[] secondaryLine = new boolean[lineCount]; markSecondaryLines(layout, transStart, transEnd, secondaryLine); markSecondaryLines(layout, romaStart, romaEnd, secondaryLine); @@ -607,17 +827,26 @@ private static int rangeLine(Layout layout, int start, int end, boolean first) { protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); final int width = getMeasuredWidth() - getPaddingLeft() - getPaddingRight(); - if (width <= 0) { - return; - } - final CharSequence text = getText(); - if (text == null || text.length() == 0) { - return; + CharSequence text = getText(); + if (width > 0 && text != null && text.length() > 0) { + SyllableRomanCache syllables = showSyllableRomanization && !wordTimings.isEmpty() + ? syllableCacheFor(width, text) : null; + if (syllables != null) { + // Drawn by hand, so the height is the hand-built layout's and not the text's. + setMeasuredDimension(getMeasuredWidth(), resolveSize(getPaddingTop() + + syllables.totalHeightPx + getPaddingBottom(), heightMeasureSpec)); + } else if (secondaryAnimator == null + && LyricsFit.applyOrphanFit(this, width, text, fittedState)) { + // Not refitted while a background vocal opens, or its text jumps mid-animation. + cachedLayout = null; + cachedText = null; + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } } - if (LyricsFit.applyOrphanFit(this, width, text, fittedState)) { - cachedLayout = null; - cachedText = null; - super.onMeasure(widthMeasureSpec, heightMeasureSpec); + secondaryFullHeight = getMeasuredHeight(); + if (isSecondary && secondaryFraction < 0.999f) { + setMeasuredDimension(getMeasuredWidth(), + Math.max(1, Math.round(secondaryFullHeight * secondaryFraction))); } } @@ -625,6 +854,18 @@ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { @SuppressWarnings("SizeReplaceableByIsEmpty") @Override protected void onDraw(Canvas canvas) { + if (showSyllableRomanization && !wordTimings.isEmpty()) { + final int width = getWidth() - getPaddingLeft() - getPaddingRight(); + CharSequence text = getText(); + if (width > 0 && text != null && text.length() > 0) { + SyllableRomanCache syllables = syllableCacheFor(width, text); + if (syllables != null) { + drawSyllableRomanizedLine(canvas, syllables); + return; + } + } + } + final boolean baseDrawn = drawBaseText(canvas); if (!baseDrawn) { super.onDraw(canvas); @@ -660,7 +901,7 @@ protected void onDraw(Canvas canvas) { boolean allWordsDone = hasWords; boolean wordOnMainRun = false; - final boolean[] secondaryLine = new boolean[lineCount]; + boolean[] secondaryLine = new boolean[lineCount]; markSecondaryLines(layout, transStart, transEnd, secondaryLine); markSecondaryLines(layout, romaStart, romaEnd, secondaryLine); boolean anyMainLine = false; @@ -672,7 +913,7 @@ protected void onDraw(Canvas canvas) { } for (int i = 0; i < cachedWordCount; i++) { - final LyricsSpanBuilder.WordTiming timing = wordTimings.get(i); + LyricsSpanBuilder.WordTiming timing = wordTimings.get(i); final int s = timing.start() + origStart; final int e = Math.min(timing.end() + origStart, text.length()); if (s < 0 || s >= text.length() || s >= e) { @@ -804,7 +1045,7 @@ protected void onDraw(Canvas canvas) { ? cachedLineLeft[ln] : cachedLineRight[ln]; } } - final ColorFilter prevFilter = tp.getColorFilter(); + ColorFilter prevFilter = tp.getColorFilter(); int appliedColor = -1; canvas.save(); @@ -853,6 +1094,1097 @@ protected void onDraw(Canvas canvas) { } } + // Syllable romanization: each syllable's reading is drawn under it, and the karaoke sweep + // runs over both. The text is laid out by hand, as each reading can be wider than the + // syllable it reads and pushes the rest of the line along to make room. + + /** A word timing and where it sits in the laid out lyric. */ + private record AlignedSyllable(LyricsSpanBuilder.WordTiming syllable, int startOffset, int endOffset) { + } + + /** One syllable's reading, laid out on its own. */ + private record RomanizedWordCluster(int wordIndex, int startIndex, int lineIdx, + StaticLayout subLayout, float romanWidth) { + } + + /** One character of the lyric as the main layout places it. */ + private record ClusterMeta(int start, int end, int lineIdx, float x, float width) { + } + + /** A run of the lyric drawn in one piece: a whole syllable, or a character between syllables. */ + private record VisualCluster(int lineIdx, float x, float y, float width, StaticLayout subLayout, + int syllableIndex, int shiftIndex) { + } + + /** Where each visual cluster is drawn once the readings have pushed it along. */ + private record ClusterStatics(float[] baseX, float[] baseY) { + } + + private record RomanLineStatics(List clusters, float[] drawX, float y) { + } + + private static final class SyllableRomanCache { + final int widthPx; + final float fontSizePx; + /** False when the line has no syllable reading, so it is drawn as plain text. */ + final boolean hasSyllableRomanization; + @Nullable + final String mainText; + final int lineCount; + final List alignedSyllables; + final int extraRomanHeightPx; + final List clusters; + final ClusterStatics statics; + final RomanLineStatics[] romanLines; + final int[] sylLine; + final float[] sylShiftedLeadX; + final float[] sylShiftedTrailX; + final float[] sylProgressScratch; + @Nullable + final String transText; + @Nullable + final StaticLayout transLayout; + /** Where the translation and the lyric start; with the swap on the translation goes first. */ + final int transTopY; + final int mainTopY; + final int totalHeightPx; + + SyllableRomanCache(int widthPx, float fontSizePx, boolean hasSyllableRomanization, + @Nullable String mainText, int lineCount, List alignedSyllables, + int extraRomanHeightPx, List clusters, ClusterStatics statics, + RomanLineStatics[] romanLines, int[] sylLine, + float[] sylShiftedLeadX, float[] sylShiftedTrailX, + @Nullable String transText, @Nullable StaticLayout transLayout, + int transTopY, int mainTopY, int totalHeightPx) { + this.widthPx = widthPx; + this.fontSizePx = fontSizePx; + this.hasSyllableRomanization = hasSyllableRomanization; + this.mainText = mainText; + this.lineCount = lineCount; + this.alignedSyllables = alignedSyllables; + this.extraRomanHeightPx = extraRomanHeightPx; + this.clusters = clusters; + this.statics = statics; + this.romanLines = romanLines; + this.sylLine = sylLine; + this.sylShiftedLeadX = sylShiftedLeadX; + this.sylShiftedTrailX = sylShiftedTrailX; + this.sylProgressScratch = new float[alignedSyllables.size()]; + this.transText = transText; + this.transLayout = transLayout; + this.transTopY = transTopY; + this.mainTopY = mainTopY; + this.totalHeightPx = totalHeightPx; + } + + /** Remembers that a line has nothing to draw this way, so it is not worked out every frame. */ + static SyllableRomanCache none(int widthPx, float fontSizePx) { + return new SyllableRomanCache(widthPx, fontSizePx, false, null, 0, + Collections.emptyList(), 0, Collections.emptyList(), + new ClusterStatics(new float[0], new float[0]), + new RomanLineStatics[0], new int[0], new float[0], new float[0], + null, null, 0, 0, 0); + } + } + + /** Laid out far wider than it needs, so a syllable or its reading never wraps a character. */ + private static StaticLayout buildSingleLineLayout(String text, TextPaint paint) { + final float desiredW = Math.max(1f, Math.max( + paint.measureText(text), + Layout.getDesiredWidth(text, paint))); + final int layoutW = Math.max(16384, (int) Math.ceil(desiredW) + 64); + return StaticLayout.Builder.obtain(text, 0, text.length(), paint, layoutW) + .setAlignment(Layout.Alignment.ALIGN_NORMAL) + .setIncludePad(false) + .setMaxLines(1) + .build(); + } + + private static float singleLineWidth(String text, TextPaint paint, StaticLayout layout) { + final float desiredW = Math.max(paint.measureText(text), Layout.getDesiredWidth(text, paint)); + final float lineW = layout.getLineCount() > 0 + ? (layout.getLineRight(0) - layout.getLineLeft(0)) : 0f; + final float horizW = text.length() > 0 ? layout.getPrimaryHorizontal(text.length()) : 0f; + return Math.max(1f, Math.max(desiredW, Math.max(lineW, horizW))); + } + + /** + * The hand-built layout for a line with syllable readings, or null when it has none and is + * drawn as plain text. + */ + @Nullable + private SyllableRomanCache syllableCacheFor(int widthPx, CharSequence fullText) { + final float fontSizePx = getTextSize(); + if (syllableCache == null || syllableCache.widthPx != widthPx + || Math.abs(syllableCache.fontSizePx - fontSizePx) >= 0.5f) { + syllableCache = buildSyllableCache(widthPx, fontSizePx, fullText.toString()); + } + return syllableCache.hasSyllableRomanization ? syllableCache : null; + } + + private SyllableRomanCache buildSyllableCache(int widthPx, float fontSizePx, String fullStr) { + final int origStart = Math.max(0, Math.min(originalTextStart, fullStr.length())); + final int newline = fullStr.indexOf('\n', origStart); + final int origEnd = newline >= 0 ? newline : fullStr.length(); + String rawMainText = fullStr.substring(origStart, origEnd); + // The readings are placed left to right, which a right-to-left script would scramble. + if (rawMainText.trim().isEmpty() || ScriptDetector.containsRtl(rawMainText)) { + return SyllableRomanCache.none(widthPx, fontSizePx); + } + + final boolean isRightAligned = (getGravity() & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.END; + Layout.Alignment mainAlign = LyricsSpanBuilder.resolveAlignment(false, isRightAligned); + + TextPaint mainPaint = new TextPaint(getPaint()); + mainPaint.setTextSize(fontSizePx); + LyricsViewSupport.applyLyricsBoldPaint(mainPaint); + mainPaint.setLetterSpacing(LyricsViewSupport.lineLetterSpacing( + rawMainText, Settings.LYRICS_TEXT_SIZE.get())); + + TextPaint romPaint = new TextPaint(mainPaint); + romPaint.setTextSize(fontSizePx * LyricsSpanBuilder.TRANSLATION_RELATIVE_SIZE); + LyricsViewSupport.applyLyricsBoldPaint(romPaint); + romPaint.setLetterSpacing(0f); + + String formattedMainText = buildFormattedMainText( + rawMainText, wordTimings, mainPaint, romPaint, widthPx, fontSizePx); + + StaticLayout mainLayout = StaticLayout.Builder.obtain( + formattedMainText, 0, formattedMainText.length(), mainPaint, widthPx) + .setAlignment(mainAlign) + .setIncludePad(false) + .setLineSpacing(0f, LyricsSpanBuilder.MAIN_LINE_SPACING) + .build(); + + final int lineCount = mainLayout.getLineCount(); + float[] lineLefts = new float[lineCount]; + float[] lineStarts = new float[lineCount]; + float[] lineBaselines = new float[lineCount]; + float[] lineBottoms = new float[lineCount]; + boolean[] lineIsRtl = new boolean[lineCount]; + + float totalW = 0f; + for (int i = 0; i < lineCount; i++) { + lineLefts[i] = mainLayout.getLineLeft(i); + lineStarts[i] = totalW; + totalW += Math.max(1f, mainLayout.getLineRight(i) - lineLefts[i]); + lineBaselines[i] = mainLayout.getLineBaseline(i); + lineBottoms[i] = mainLayout.getLineBottom(i); + lineIsRtl[i] = mainLayout.getParagraphDirection(i) == Layout.DIR_RIGHT_TO_LEFT; + } + + List alignedSyllables = + alignSyllablesForFormatting(wordTimings, rawMainText, formattedMainText); + List romanClusters = + buildRomanizedWordClusters(alignedSyllables, formattedMainText, mainLayout, romPaint); + if (romanClusters.isEmpty()) { + return SyllableRomanCache.none(widthPx, fontSizePx); + } + + // Each lyric line makes room under it for its readings. + final float lineExtraHeight = fontSizePx * 1.05f; + final int bottomMargin = Math.round(fontSizePx * 0.25f); + final int extraRomanHeightPx = Math.round(lineCount * lineExtraHeight) + bottomMargin; + + float[] charXMap = buildCharXMap( + formattedMainText, mainLayout, lineCount, lineIsRtl, lineStarts); + float[] romanStartShift = buildRomanStartShift( + formattedMainText, charXMap, mainLayout, lineCount, + alignedSyllables, romanClusters, mainAlign); + + List clusters = buildVisualClusters( + formattedMainText, mainLayout, alignedSyllables, lineIsRtl, lineBaselines, mainPaint); + ClusterStatics statics = buildClusterStatics( + clusters, romanStartShift, lineExtraHeight); + List[] romanLines = + groupRomanClustersByLine(romanClusters, lineCount); + RomanLineStatics[] romanStatics = buildRomanStatics( + romanLines, charXMap, romanStartShift, lineBaselines, lineBottoms, + lineLefts, lineStarts, lineCount, fontSizePx, lineExtraHeight, totalW); + + // Where each syllable's sweep starts and ends, from the same positions it is drawn at. + final int sylCount = alignedSyllables.size(); + int[] sylLine = new int[sylCount]; + float[] sylShiftedLeadX = new float[sylCount]; + float[] sylShiftedTrailX = new float[sylCount]; + Arrays.fill(sylShiftedLeadX, Float.NaN); + Arrays.fill(sylShiftedTrailX, Float.NaN); + + for (int idx = 0; idx < clusters.size(); idx++) { + VisualCluster vc = clusters.get(idx); + final int sIdx = vc.syllableIndex(); + if (sIdx >= 0 && sIdx < sylCount) { + sylLine[sIdx] = vc.lineIdx(); + final float leftX = statics.baseX()[idx]; + final float rightX = leftX + vc.width(); + if (Float.isNaN(sylShiftedLeadX[sIdx]) || leftX < sylShiftedLeadX[sIdx]) { + sylShiftedLeadX[sIdx] = leftX; + } + if (Float.isNaN(sylShiftedTrailX[sIdx]) || rightX > sylShiftedTrailX[sIdx]) { + sylShiftedTrailX[sIdx] = rightX; + } + } + } + final int maxCharIdx = Math.max(0, formattedMainText.length() - 1); + for (int sIdx = 0; sIdx < sylCount; sIdx++) { + if (Float.isNaN(sylShiftedLeadX[sIdx])) { + AlignedSyllable syl = alignedSyllables.get(sIdx); + final int li = mainLayout.getLineForOffset(Math.max(0, Math.min(syl.startOffset(), maxCharIdx))); + sylLine[sIdx] = li; + final float shift = calculateGapShift(sIdx, romanStartShift); + final float sx = mainLayout.getPrimaryHorizontal(Math.max(0, Math.min(syl.startOffset(), formattedMainText.length()))) + shift; + final float ex = mainLayout.getPrimaryHorizontal(Math.max(0, Math.min(syl.endOffset(), formattedMainText.length()))) + shift; + sylShiftedLeadX[sIdx] = Math.min(sx, ex); + sylShiftedTrailX[sIdx] = Math.max(sx + 1f, ex); + } + } + + String transText = null; + StaticLayout transLayout = null; + final int mainSectionH = mainLayout.getHeight() + extraRomanHeightPx; + int transTopY = 0; + int mainTopY = 0; + int totalH = mainSectionH; + + if (transStart >= 0 && transEnd > transStart && transStart < fullStr.length()) { + transText = fullStr.substring(transStart, Math.min(transEnd, fullStr.length())).trim(); + if (!transText.isEmpty()) { + TextPaint transPaint = new TextPaint(getPaint()); + transPaint.setTextSize(fontSizePx * LyricsSpanBuilder.TRANSLATION_RELATIVE_SIZE); + LyricsViewSupport.applyLyricsBoldPaint(transPaint); + transPaint.setLetterSpacing(0f); + final boolean isTransRtl = LyricsSpanBuilder.isRtlText(transText); + Layout.Alignment transAlign = + LyricsSpanBuilder.resolveAlignment(isTransRtl, isRightAligned); + transLayout = StaticLayout.Builder.obtain( + transText, 0, transText.length(), transPaint, widthPx) + .setAlignment(transAlign) + .setIncludePad(false) + .setLineSpacing(0f, 1.18f) + .build(); + final int transH = transLayout.getHeight(); + if (transStart < origStart) { + mainTopY = transH + Dim.dp4; + } else { + transTopY = mainSectionH + Dim.dp4; + } + totalH = mainSectionH + Dim.dp4 + transH; + } + } + + return new SyllableRomanCache(widthPx, fontSizePx, true, rawMainText, lineCount, + alignedSyllables, extraRomanHeightPx, clusters, statics, romanStatics, + sylLine, sylShiftedLeadX, sylShiftedTrailX, + transText, transLayout, transTopY, mainTopY, totalH); + } + + /** Finds each word timing in the laid out text, which may carry bidi marks and added breaks. */ + private static List alignSyllablesForFormatting( + List words, + String rawMainText, + String targetMainText) { + if (words.isEmpty() || targetMainText.isEmpty()) { + return Collections.emptyList(); + } + List strippedToOriginal = new ArrayList<>(targetMainText.length() + 1); + StringBuilder strippedBuilder = new StringBuilder(targetMainText.length()); + for (int i = 0; i < targetMainText.length(); i++) { + final char c = targetMainText.charAt(i); + if (!LyricsSpanBuilder.isBidiOrInvisible(c)) { + strippedBuilder.append(c); + strippedToOriginal.add(i); + } + } + strippedToOriginal.add(targetMainText.length()); + String strippedMain = strippedBuilder.toString(); + String strippedLower = strippedMain.toLowerCase(Locale.ROOT); + + int totalSylLen = 0; + String[] sylTexts = new String[words.size()]; + for (int i = 0; i < words.size(); i++) { + LyricsSpanBuilder.WordTiming wt = words.get(i); + final int s = Math.max(0, Math.min(wt.start(), rawMainText.length())); + final int e = Math.max(s, Math.min(wt.end(), rawMainText.length())); + String st = LyricsSpanBuilder.stripBidiControls(rawMainText.substring(s, e)).trim(); + sylTexts[i] = st; + totalSylLen += st.length(); + } + totalSylLen = Math.max(1, totalSylLen); + + int searchIdx = 0; + List out = new ArrayList<>(words.size()); + for (int i = 0; i < words.size(); i++) { + LyricsSpanBuilder.WordTiming syl = words.get(i); + String sylText = sylTexts[i]; + String sylLower = sylText.toLowerCase(Locale.ROOT); + while (searchIdx < strippedMain.length() + && Character.isWhitespace(strippedMain.charAt(searchIdx))) { + searchIdx++; + } + final int found = sylLower.isEmpty() ? -1 : strippedLower.indexOf(sylLower, searchIdx); + final int si; + final int ei; + if (found != -1) { + si = strippedToOriginal.get(found); + final int endIdx = Math.min(found + sylText.length(), strippedToOriginal.size() - 1); + ei = strippedToOriginal.get(endIdx); + searchIdx = found + sylText.length(); + } else { + final int fallbackStripped = Math.min(searchIdx, strippedMain.length()); + final int propLen = Math.max(1, + (int) (((double) sylText.length() / totalSylLen) * strippedMain.length())); + final int endStripped = Math.min(fallbackStripped + propLen, strippedMain.length()); + si = strippedToOriginal.get(Math.min(fallbackStripped, strippedToOriginal.size() - 1)); + ei = strippedToOriginal.get(Math.min(endStripped, strippedToOriginal.size() - 1)); + searchIdx = endStripped; + } + out.add(new AlignedSyllable(syl, si, ei)); + } + return out; + } + + /** + * Breaks the lyric where its syllables, each as wide as the wider of itself and its reading, + * run out of room, so that no syllable is split from its reading by a wrap. + */ + private static String buildFormattedMainText( + String mainText, + List words, + TextPaint mainPaint, + TextPaint romPaint, + int widthPx, + float fontSizePx) { + if (mainText.isEmpty() || widthPx <= 0 || words.isEmpty()) { + return mainText; + } + List alignedSyllables = + alignSyllablesForFormatting(words, mainText, mainText); + if (alignedSyllables.isEmpty()) { + return mainText; + } + + final int unconstrainedWidth = Math.max(widthPx * 16, 16384); + StaticLayout unconstrained = StaticLayout.Builder.obtain( + mainText, 0, mainText.length(), mainPaint, unconstrainedWidth) + .setAlignment(Layout.Alignment.ALIGN_NORMAL) + .setIncludePad(false) + .build(); + + List romanClusters = + buildRomanizedWordClusters(alignedSyllables, mainText, unconstrained, romPaint); + if (romanClusters.isEmpty()) { + return mainText; + } + + Map clusterMap = new HashMap<>(romanClusters.size() * 2); + for (RomanizedWordCluster rc : romanClusters) { + clusterMap.put(rc.wordIndex(), rc); + } + + float[] sylSlotWidths = new float[alignedSyllables.size()]; + for (int sylIdx = 0; sylIdx < alignedSyllables.size(); sylIdx++) { + AlignedSyllable syl = alignedSyllables.get(sylIdx); + RomanizedWordCluster cluster = clusterMap.get(sylIdx); + final int startOff = Math.max(0, Math.min(syl.startOffset(), mainText.length())); + final int nextStartOff = (sylIdx + 1 < alignedSyllables.size()) + ? Math.max(syl.endOffset(), Math.min(alignedSyllables.get(sylIdx + 1).startOffset(), mainText.length())) + : Math.max(startOff, Math.min(syl.endOffset(), mainText.length())); + final boolean hasNewlineInGap = mainText.substring( + Math.max(0, Math.min(syl.endOffset(), mainText.length())), nextStartOff).indexOf('\n') >= 0; + final int slotEndOff = hasNewlineInGap + ? Math.max(startOff, Math.min(syl.endOffset(), mainText.length())) + : nextStartOff; + final float startX = unconstrained.getPrimaryHorizontal(startOff); + final float endX = unconstrained.getPrimaryHorizontal(slotEndOff); + final float slotMainWidth = Math.max(1f, endX - startX); + final float romWidth = cluster != null ? cluster.romanWidth() : 0f; + sylSlotWidths[sylIdx] = Math.max(slotMainWidth, romWidth); + } + + List breakIndices = new ArrayList<>(); + Set spaceReplaced = new HashSet<>(); + + float currentLineSlotSum = 0f; + int syllablesOnCurrentLine = 0; + int currentLineStartOffset = 0; + + for (int sylIdx = 0; sylIdx < alignedSyllables.size(); sylIdx++) { + AlignedSyllable syl = alignedSyllables.get(sylIdx); + final float slotW = sylSlotWidths[sylIdx]; + AlignedSyllable prevSyl = sylIdx > 0 ? alignedSyllables.get(sylIdx - 1) : null; + + if (prevSyl != null) { + final int gs = Math.max(0, Math.min(prevSyl.endOffset(), mainText.length())); + final int ge = Math.max(gs, Math.min(syl.startOffset(), mainText.length())); + if (mainText.substring(gs, ge).indexOf('\n') >= 0) { + currentLineSlotSum = 0f; + syllablesOnCurrentLine = 0; + currentLineStartOffset = Math.max(0, Math.min(syl.startOffset(), mainText.length())); + } + } + + final float breakThreshold = widthPx - fontSizePx * 0.1f; + if (syllablesOnCurrentLine > 0 && currentLineSlotSum + slotW > breakThreshold) { + final int rawBreakAt = Math.max(0, Math.min(syl.startOffset(), mainText.length())); + Integer breakAt = findSafeBreakOffset(mainText, rawBreakAt, currentLineStartOffset); + if (breakAt != null && breakAt > 0) { + breakIndices.add(breakAt); + if (breakAt - 1 < mainText.length() && mainText.charAt(breakAt - 1) == ' ') { + spaceReplaced.add(breakAt - 1); + } + currentLineStartOffset = breakAt; + currentLineSlotSum = slotW; + syllablesOnCurrentLine = 1; + } else { + currentLineSlotSum += slotW; + syllablesOnCurrentLine++; + } + } else { + currentLineSlotSum += slotW; + syllablesOnCurrentLine++; + } + } + + if (breakIndices.isEmpty()) { + return mainText; + } + + StringBuilder sb = new StringBuilder(mainText.length() + breakIndices.size()); + int prev = 0; + for (int bIdx : breakIndices) { + final int safeBIdx = Math.max(0, Math.min(bIdx, mainText.length())); + final int safePrev = Math.max(0, Math.min(prev, mainText.length())); + if (safeBIdx > safePrev) { + final int spaceIdx = safeBIdx - 1; + if (spaceReplaced.contains(spaceIdx) + && spaceIdx >= 0 && spaceIdx < mainText.length() + && mainText.charAt(spaceIdx) == ' ') { + sb.append(mainText, safePrev, spaceIdx); + sb.append('\n'); + prev = safeBIdx; + } else { + sb.append(mainText, safePrev, safeBIdx); + sb.append('\n'); + prev = safeBIdx; + } + } + } + final int finalPrev = Math.max(0, Math.min(prev, mainText.length())); + if (finalPrev < mainText.length()) { + sb.append(mainText.substring(finalPrev)); + } + return sb.toString(); + } + + /** Punctuation a line must not start with. */ + private static boolean isProhibitedLineStart(char c) { + return switch (c) { + case '、', '。', '!', '?', ')', ']', '}', '”', '’', '…', '—', + ')', '】', '』', '」', '≫', '》', '〉', '>', '›', '〕', '〛', + ',', '.', '!', '?', ':', ';', ' ' -> true; + default -> false; + }; + } + + @Nullable + private static Integer findSafeBreakOffset(String mainText, int rawBreakAt, int lineStartOffset) { + if (rawBreakAt <= lineStartOffset || rawBreakAt > mainText.length()) { + return null; + } + Character before = rawBreakAt - 1 >= 0 && rawBreakAt - 1 < mainText.length() + ? mainText.charAt(rawBreakAt - 1) : null; + Character at = rawBreakAt >= 0 && rawBreakAt < mainText.length() + ? mainText.charAt(rawBreakAt) : null; + + // Korean words are spaced, so a break inside one moves back to the last space. + final int lastSpaceOnLine = mainText.substring(lineStartOffset, rawBreakAt).lastIndexOf(' '); + if (lastSpaceOnLine >= 0 && (before == null || before != ' ')) { + final boolean isInsideHangulWord = before != null && at != null + && before >= 0xAC00 && before <= 0xD7AF + && at >= 0xAC00 && at <= 0xD7AF; + if (isInsideHangulWord) { + final int spaceBreak = lineStartOffset + lastSpaceOnLine + 1; + if (spaceBreak > lineStartOffset && spaceBreak < mainText.length() + && !isProhibitedLineStart(mainText.charAt(spaceBreak))) { + return spaceBreak; + } + } + } + + final boolean atCjkBoundary = + (before == null || before == ' ' || LyricsSpanBuilder.isCjkCodePoint(before)) + && (at == null || at == ' ' || LyricsSpanBuilder.isCjkCodePoint(at) || LyricsSpanBuilder.isCjkCodePoint(before)); + + int candidate = rawBreakAt; + if (!atCjkBoundary) { + int i = rawBreakAt; + while (i > lineStartOffset) { + final char c = mainText.charAt(i - 1); + Character next = i < mainText.length() ? mainText.charAt(i) : null; + if (c == ' ' || LyricsSpanBuilder.isCjkCodePoint(c) || (next != null && LyricsSpanBuilder.isCjkCodePoint(next))) { + candidate = i; + break; + } + i--; + } + if (i <= lineStartOffset) { + return null; + } + } + + while (candidate > lineStartOffset + && candidate < mainText.length() + && isProhibitedLineStart(mainText.charAt(candidate))) { + candidate--; + while (candidate > lineStartOffset + && !(mainText.charAt(candidate - 1) == ' ' || LyricsSpanBuilder.isCjkCodePoint(mainText.charAt(candidate - 1)))) { + candidate--; + } + } + + return candidate > lineStartOffset ? candidate : null; + } + + /** Lays out each syllable's reading on its own, empty when the line has no reading of its own. */ + private static List buildRomanizedWordClusters( + List alignedSyllables, + String mainText, + Layout textLayoutResult, + TextPaint romPaint) { + if (alignedSyllables.isEmpty() || mainText.isEmpty()) { + return Collections.emptyList(); + } + String cleanMainText = LyricsSpanBuilder.stripBidiControls(mainText).trim(); + StringBuilder assembled = new StringBuilder(); + for (AlignedSyllable syl : alignedSyllables) { + String r = syl.syllable().romaji(); + if (r != null) { + assembled.append(r); + } + } + String assembledRoman = LyricsSpanBuilder.stripBidiControls(assembled.toString()).trim(); + if (assembledRoman.isEmpty() || assembledRoman.equalsIgnoreCase(cleanMainText)) { + return Collections.emptyList(); + } + + List result = new ArrayList<>(alignedSyllables.size()); + final int maxCharIdx = Math.max(0, mainText.length() - 1); + for (int idx = 0; idx < alignedSyllables.size(); idx++) { + AlignedSyllable syl = alignedSyllables.get(idx); + String text = syl.syllable().romaji(); + if (text != null && !text.trim().isEmpty()) { + final int clampedStart = Math.max(0, Math.min(syl.startOffset(), maxCharIdx)); + final int li = textLayoutResult.getLineForOffset(clampedStart); + TextPaint subPaint = new TextPaint(romPaint); + StaticLayout subLayout = buildSingleLineLayout(text, subPaint); + final float romW = singleLineWidth(text, subPaint, subLayout); + result.add(new RomanizedWordCluster(idx, syl.startOffset(), li, subLayout, romW)); + } + } + return result; + } + + /** How far along the lyric each character starts, counting its lines as one long line. */ + private static float[] buildCharXMap( + String mainText, + Layout textLayoutResult, + int lineCount, + boolean[] lineIsRtl, + float[] lineStarts) { + final int len = mainText.length(); + float[] map = new float[len + 1]; + final int maxIdx = Math.max(0, len - 1); + for (int i = 0; i <= len; i++) { + final int idx = Math.min(i, maxIdx); + final int li = Math.max(0, Math.min(textLayoutResult.getLineForOffset(idx), lineCount - 1)); + final float h = textLayoutResult.getPrimaryHorizontal(Math.min(i, len)); + final float local = lineIsRtl[li] + ? textLayoutResult.getLineRight(li) - h + : h - textLayoutResult.getLineLeft(li); + map[i] = lineStarts[li] + local; + } + return map; + } + + /** + * How far each syllable moves right to make room for the readings before it that are wider + * than their syllables, centred or right aligned again with the line. + */ + private static float[] buildRomanStartShift( + String mainText, + float[] charXMap, + Layout textLayoutResult, + int lineCount, + List alignedSyllables, + List romanClusters, + Layout.Alignment alignment) { + float[] shiftByWordIndex = new float[romanClusters.size() + alignedSyllables.size() + 1]; + float[] lineTotalExtra = new float[lineCount]; + if (romanClusters.isEmpty()) { + return shiftByWordIndex; + } + + Map clusterMap = new HashMap<>(romanClusters.size() * 2); + for (RomanizedWordCluster rc : romanClusters) { + clusterMap.put(rc.wordIndex(), rc); + } + + float runningShift = 0f; + int currentLine = -1; + final int maxCharIdx = Math.max(0, mainText.length() - 1); + final int maxMapIdx = Math.max(0, charXMap.length - 1); + + for (int sylIdx = 0; sylIdx < alignedSyllables.size(); sylIdx++) { + AlignedSyllable syl = alignedSyllables.get(sylIdx); + final int li = textLayoutResult.getLineForOffset( + Math.max(0, Math.min(syl.startOffset(), maxCharIdx))); + if (li != currentLine) { + currentLine = li; + runningShift = 0f; + } + if (sylIdx < shiftByWordIndex.length) { + shiftByWordIndex[sylIdx] = runningShift; + } + + RomanizedWordCluster cluster = clusterMap.get(sylIdx); + if (cluster == null) { + continue; + } + final int sylEndLine = textLayoutResult.getLineForOffset( + Math.max(0, Math.min(syl.endOffset(), maxCharIdx))); + final int lineEndOff = textLayoutResult.getLineVisibleEnd(li); + + // Include any trailing space before the next syllable on the same line so we compare + // (word + space) in mainLayout against (romaji + space) in cluster.romanWidth(). + int effectiveEndOff = syl.endOffset(); + if (li == sylEndLine && sylIdx + 1 < alignedSyllables.size()) { + AlignedSyllable nextSyl = alignedSyllables.get(sylIdx + 1); + final int nextLi = textLayoutResult.getLineForOffset( + Math.max(0, Math.min(nextSyl.startOffset(), maxCharIdx))); + if (nextLi == li && nextSyl.startOffset() > syl.endOffset()) { + effectiveEndOff = nextSyl.startOffset(); + } + } + + final float startX = charXMap[Math.max(0, Math.min(syl.startOffset(), maxMapIdx))]; + final float endX = (li == sylEndLine) + ? charXMap[Math.max(0, Math.min(effectiveEndOff, maxMapIdx))] + : charXMap[Math.max(0, Math.min(lineEndOff, maxMapIdx))]; + final float mainWidth = Math.max(1f, endX - startX); + final float romWidth = cluster.romanWidth(); + + if (romWidth > mainWidth) { + final float extra = romWidth - mainWidth; + runningShift += extra; + if (li >= 0 && li < lineTotalExtra.length) { + lineTotalExtra[li] += extra; + } + } + } + + if (alignment == Layout.Alignment.ALIGN_CENTER || alignment == Layout.Alignment.ALIGN_OPPOSITE) { + for (int idx = 0; idx < alignedSyllables.size() && idx < shiftByWordIndex.length; idx++) { + AlignedSyllable syl = alignedSyllables.get(idx); + final int li = textLayoutResult.getLineForOffset( + Math.max(0, Math.min(syl.startOffset(), maxCharIdx))); + final float totalExtra = (li >= 0 && li < lineTotalExtra.length) ? lineTotalExtra[li] : 0f; + final float correction = (alignment == Layout.Alignment.ALIGN_CENTER) + ? totalExtra / 2f : totalExtra; + shiftByWordIndex[idx] -= correction; + } + } + + return shiftByWordIndex; + } + + private static float calculateGapShift(int syllableIndex, float[] romanStartShift) { + if (syllableIndex < 0 || syllableIndex >= romanStartShift.length) { + return 0f; + } + return romanStartShift[syllableIndex]; + } + + private static List buildVisualClusters( + String formattedMainText, + Layout textLayoutResult, + List alignedSyllables, + boolean[] lineIsRtl, + float[] lineBaselines, + TextPaint mainPaint) { + List metaList = new ArrayList<>(formattedMainText.length()); + final int len = formattedMainText.length(); + final int maxIdx = Math.max(0, len - 1); + int i = 0; + while (i < len) { + final int start = i; + final int end = (i + 1 < len + && Character.isHighSurrogate(formattedMainText.charAt(i)) + && Character.isLowSurrogate(formattedMainText.charAt(i + 1))) + ? i + 2 : i + 1; + i = end; + + if (formattedMainText.charAt(start) == '\n') { + continue; + } + final int safeStart = Math.max(0, Math.min(start, maxIdx)); + final int safeEnd = Math.min(end, maxIdx); + final int li = textLayoutResult.getLineForOffset(safeStart); + final int eli = textLayoutResult.getLineForOffset(safeEnd); + final float hStart = textLayoutResult.getPrimaryHorizontal(safeStart); + final float hEnd = (li == eli) + ? textLayoutResult.getPrimaryHorizontal(Math.min(end, len)) + : textLayoutResult.getLineRight(li); + final float x = Math.min(hStart, hEnd); + final float w = Math.abs(hEnd - hStart); + metaList.add(new ClusterMeta(start, end, li, x, w)); + } + + int[] sylIndices = new int[metaList.size()]; + int[] shiftIndices = new int[metaList.size()]; + int sylPointer = 0; + int lastSylOnLine = -1; + int curLine = -1; + for (int mIdx = 0; mIdx < metaList.size(); mIdx++) { + ClusterMeta m = metaList.get(mIdx); + if (m.lineIdx() != curLine) { + curLine = m.lineIdx(); + lastSylOnLine = -1; + } + while (sylPointer < alignedSyllables.size() + && alignedSyllables.get(sylPointer).endOffset() <= m.start()) { + sylPointer++; + } + if (sylPointer < alignedSyllables.size()) { + AlignedSyllable s = alignedSyllables.get(sylPointer); + if (m.start() >= s.startOffset() && m.end() <= s.endOffset()) { + sylIndices[mIdx] = sylPointer; + shiftIndices[mIdx] = sylPointer; + lastSylOnLine = sylPointer; + } else { + sylIndices[mIdx] = -1; + shiftIndices[mIdx] = lastSylOnLine >= 0 ? lastSylOnLine : sylPointer; + } + } else { + sylIndices[mIdx] = -1; + shiftIndices[mIdx] = lastSylOnLine; + } + } + + List list = new ArrayList<>(); + int idx = 0; + while (idx < metaList.size()) { + ClusterMeta m = metaList.get(idx); + final int runSylIdx = sylIndices[idx]; + final int runShiftIdx = shiftIndices[idx]; + int j = idx; + // Only merge consecutive characters that belong to the same valid syllable on the same line + if (runSylIdx >= 0) { + while (j + 1 < metaList.size() + && metaList.get(j + 1).lineIdx() == m.lineIdx() + && sylIndices[j + 1] == runSylIdx) { + j++; + } + } + float runLeft = Float.MAX_VALUE; + float runRight = -Float.MAX_VALUE; + for (int k = idx; k <= j; k++) { + ClusterMeta cm = metaList.get(k); + if (cm.x() < runLeft) runLeft = cm.x(); + if (cm.x() + cm.width() > runRight) runRight = cm.x() + cm.width(); + } + ClusterMeta last = metaList.get(j); + String runText = formattedMainText.substring( + Math.max(0, Math.min(m.start(), len)), + Math.max(0, Math.min(last.end(), len))); + TextPaint subPaint = new TextPaint(mainPaint); + StaticLayout subLayout = buildSingleLineLayout(runText, subPaint); + final float subW = Math.max(runRight - runLeft, singleLineWidth(runText, subPaint, subLayout)); + final float clusterY = lineBaselines[m.lineIdx()] - subLayout.getLineBaseline(0); + list.add(new VisualCluster(m.lineIdx(), runLeft, clusterY, Math.max(1f, subW), + subLayout, runSylIdx, runShiftIdx)); + idx = j + 1; + } + return list; + } + + private static ClusterStatics buildClusterStatics( + List clusters, + float[] romanStartShift, + float lineExtraHeight) { + final int n = clusters.size(); + float[] baseX = new float[n]; + float[] baseY = new float[n]; + for (int i = 0; i < n; i++) { + VisualCluster vc = clusters.get(i); + final int shiftIdx = vc.syllableIndex() >= 0 ? vc.syllableIndex() : vc.shiftIndex(); + baseX[i] = vc.x() + calculateGapShift(shiftIdx, romanStartShift); + baseY[i] = vc.y() + vc.lineIdx() * lineExtraHeight; + } + return new ClusterStatics(baseX, baseY); + } + + @SuppressWarnings("unchecked") + private static List[] groupRomanClustersByLine( + List romanClusters, int lineCount) { + if (romanClusters.isEmpty()) { + return new List[0]; + } + List[] lines = new List[lineCount]; + for (int li = 0; li < lineCount; li++) { + lines[li] = new ArrayList<>(); + } + for (RomanizedWordCluster rc : romanClusters) { + if (rc.lineIdx() >= 0 && rc.lineIdx() < lineCount) { + lines[rc.lineIdx()].add(rc); + } + } + return lines; + } + + private static RomanLineStatics[] buildRomanStatics( + List[] romanLines, + float[] charXMap, + float[] romanStartShift, + float[] lineBaselines, + float[] lineBottoms, + float[] lineLefts, + float[] lineStarts, + int lineCount, + float fontSizePx, + float lineExtraHeight, + float totalWidth) { + final int count = Math.min(lineCount, romanLines.length); + RomanLineStatics[] lines = new RomanLineStatics[count]; + + for (int li = 0; li < count; li++) { + List cs = romanLines[li]; + if (cs == null || cs.isEmpty()) { + continue; + } + final float lineLeft = li < lineLefts.length ? lineLefts[li] : 0f; + final float lineStart = li < lineStarts.length ? lineStarts[li] : 0f; + final int n = cs.size(); + float[] drawX = new float[n]; + for (int i = 0; i < n; i++) { + RomanizedWordCluster c = cs.get(i); + final float gx = c.startIndex() < 0 ? 0f + : (c.startIndex() >= charXMap.length ? totalWidth : charXMap[c.startIndex()]); + final float shift = (c.wordIndex() >= 0 && c.wordIndex() < romanStartShift.length) + ? romanStartShift[c.wordIndex()] : 0f; + drawX[i] = gx - lineStart + lineLeft + shift; + } + final float baseLineBottom = li < lineBottoms.length + ? lineBottoms[li] : (lineBaselines[li] + fontSizePx * 0.25f); + final float y = baseLineBottom + li * lineExtraHeight + fontSizePx * 0.08f; + lines[li] = new RomanLineStatics(cs, drawX, y); + } + return lines; + } + + /** Draws and sweeps a line the way {@link #onDraw} does, with each syllable's reading under it. */ + private void drawSyllableRomanizedLine(Canvas canvas, SyllableRomanCache cache) { + if (contentReveal <= 0f) { + return; + } + final float contentNow = contentReveal; + final float originX = textOriginX(); + final float originY = textOriginY(); + final float mainY = originY + cache.mainTopY; + + // The unsung colours, as drawBaseText picks them. + final int mainUnsungAlpha = Math.round(((unsungColor != 0 && !wordTimings.isEmpty()) + ? LyricsColors.UNSUNG_ALPHA * 255f : baseTextAlpha) * lineAlpha * contentNow); + final float secondaryBaseAlpha = activeLine + ? LyricsColors.ACTIVE_TRANSLATION_ALPHA * 255f + : Color.alpha(LyricsColors.secondaryTextColor()); + final int transAlpha = Math.round(lineAlpha * secondaryBaseAlpha * contentNow * transReveal); + final float readingBaseAlpha = activeLine + ? (Settings.LYRICS_WORD_SYNC.get() && unsungColor != 0 && !wordTimings.isEmpty() + ? LyricsColors.UNSUNG_ALPHA * 255f + : LyricsColors.ACTIVE_READING_ALPHA * 255f) + : Color.alpha(LyricsColors.secondaryTextColor()); + final int romaUnsungAlpha = Math.round(lineAlpha * readingBaseAlpha * contentNow * romaReveal); + + final int baseMainRgb = (getCurrentTextColor() | 0xFF000000) & 0x00FFFFFF; + final int readingRgb = ((activeLine && secondaryActiveColor != 0 + ? secondaryActiveColor : LyricsColors.secondaryTextColor()) | 0xFF000000) & 0x00FFFFFF; + + final int mainUnsungColor = baseMainRgb | (Math.max(0, Math.min(255, mainUnsungAlpha)) << 24); + final int romaUnsungColor = readingRgb | (Math.max(0, Math.min(255, romaUnsungAlpha)) << 24); + final int transColor = readingRgb | (Math.max(0, Math.min(255, transAlpha)) << 24); + + List clusters = cache.clusters; + ClusterStatics statics = cache.statics; + + // Everything unsung first, then the sung part on top of it. + if (mainUnsungAlpha > 0) { + for (int idx = 0; idx < clusters.size(); idx++) { + VisualCluster vc = clusters.get(idx); + TextPaint p = vc.subLayout().getPaint(); + p.setShader(null); + p.setColor(mainUnsungColor); + canvas.save(); + canvas.translate(originX + statics.baseX()[idx], mainY + statics.baseY()[idx]); + vc.subLayout().draw(canvas); + canvas.restore(); + } + } + + if (romaReveal > 0f && romaUnsungAlpha > 0) { + RomanLineStatics[] romLines = cache.romanLines; + for (int li = 0; li < romLines.length; li++) { + RomanLineStatics line = romLines[li]; + if (line == null) { + continue; + } + List cs = line.clusters(); + final float topY = mainY + line.y(); + for (int i = 0; i < cs.size(); i++) { + RomanizedWordCluster rc = cs.get(i); + TextPaint p = rc.subLayout().getPaint(); + p.setShader(null); + p.setColor(romaUnsungColor); + canvas.save(); + canvas.translate(originX + line.drawX()[i], topY); + rc.subLayout().draw(canvas); + canvas.restore(); + } + } + } + + if (cache.transLayout != null && transReveal > 0f && transAlpha > 0) { + TextPaint tp = cache.transLayout.getPaint(); + tp.setShader(null); + tp.setColor(transColor); + canvas.save(); + canvas.translate(originX, originY + cache.transTopY); + cache.transLayout.draw(canvas); + canvas.restore(); + } + + // The sweep, as onDraw runs it. + if (unsungColor == 0 || (wordTimings.isEmpty() && !allSung)) { + return; + } + + final int lineCount = cache.lineCount; + if (lineMaxSungX == null || lineMaxSungX.length < lineCount) { + lineMaxSungX = new float[lineCount]; + } + Arrays.fill(lineMaxSungX, 0, lineCount, Float.NaN); + + List alignedSyllables = cache.alignedSyllables; + final int sylCount = alignedSyllables.size(); + float[] sylProgress = cache.sylProgressScratch; + int firstSungLine = -1; + + for (int i = 0; i < sylCount; i++) { + LyricsSpanBuilder.WordTiming timing = alignedSyllables.get(i).syllable(); + final float progress; + if (allSung) { + progress = 1f; + } else if (positionMs >= timing.endMs()) { + progress = 1f; + } else if (positionMs <= timing.startMs()) { + progress = 0f; + } else { + progress = (float) (positionMs - timing.startMs()) + / (float) (timing.endMs() - timing.startMs()); + } + sylProgress[i] = progress; + if (progress <= 0f) { + continue; + } + final int lineNum = cache.sylLine[i]; + if (firstSungLine < 0) { + firstSungLine = lineNum; + } + final float lead = cache.sylShiftedLeadX[i]; + final float trail = cache.sylShiftedTrailX[i]; + final float edge = lead + (trail - lead) * progress; + if (lineNum >= 0 && lineNum < lineCount) { + lineMaxSungX[lineNum] = extendSung(lineMaxSungX[lineNum], edge, false); + } + } + + if (allSung && firstSungLine < 0) { + for (int ln = 0; ln < lineCount; ln++) { + lineMaxSungX[ln] = Float.MAX_VALUE; + } + } + + final int mainSungAlpha = Math.round(255f * contentNow); + final int mainSungColor = (LyricsColors.lineTextColor() & 0x00FFFFFF) + | (Math.max(0, Math.min(255, mainSungAlpha)) << 24); + + // Each lyric line is lit up to its furthest sung edge. + for (int idx = 0; idx < clusters.size(); idx++) { + VisualCluster vc = clusters.get(idx); + final int ln = vc.lineIdx(); + if (ln < 0 || ln >= lineCount) { + continue; + } + final float edge = lineMaxSungX[ln]; + if (Float.isNaN(edge)) { + continue; + } + final float cx = statics.baseX()[idx]; + if (edge <= cx) { + continue; + } + TextPaint p = vc.subLayout().getPaint(); + p.setShader(null); + p.setColor(mainSungColor); + canvas.save(); + final float tx = originX + cx; + final float ty = mainY + statics.baseY()[idx]; + if (edge < cx + vc.width()) { + canvas.clipRect(tx, ty - Dim.dp(4), originX + edge, ty + vc.subLayout().getHeight() + Dim.dp(4)); + } + canvas.translate(tx, ty); + vc.subLayout().draw(canvas); + canvas.restore(); + } + + // Each reading is lit as far as its own syllable is sung. + if (romaReveal > 0f) { + final int romaSungAlpha = Math.round(LyricsColors.ACTIVE_READING_ALPHA * 255f * contentNow * romaReveal); + final int romaSungColor = readingRgb | (Math.max(0, Math.min(255, romaSungAlpha)) << 24); + RomanLineStatics[] romLines = cache.romanLines; + + for (int li = 0; li < romLines.length; li++) { + RomanLineStatics line = romLines[li]; + if (line == null) { + continue; + } + List cs = line.clusters(); + final float topY = mainY + line.y(); + for (int i = 0; i < cs.size(); i++) { + RomanizedWordCluster rc = cs.get(i); + final float prog = (rc.wordIndex() >= 0 && rc.wordIndex() < sylCount) + ? sylProgress[rc.wordIndex()] + : (allSung ? 1f : 0f); + if (prog <= 0f) { + continue; + } + TextPaint p = rc.subLayout().getPaint(); + p.setShader(null); + p.setColor(romaSungColor); + final float rx = originX + line.drawX()[i]; + canvas.save(); + if (prog < 1f) { + canvas.clipRect(rx, topY - Dim.dp(2), + rx + rc.romanWidth() * prog, + topY + rc.subLayout().getHeight() + Dim.dp(2)); + } + canvas.translate(rx, topY); + rc.subLayout().draw(canvas); + canvas.restore(); + } + } + } + } + private static int lineDrawing(Layout layout, int offset) { int low = 0; int high = layout.getLineCount() - 1; @@ -874,7 +2206,7 @@ private static boolean isRightToLeftLine(Layout layout, int line) { if (line >= 0 && line < layout.getLineCount()) { final int start = layout.getLineStart(line); final int end = layout.getLineEnd(line); - final CharSequence text = layout.getText(); + CharSequence text = layout.getText(); for (int i = start; i < end && i < text.length(); i++) { byte dir = Character.getDirectionality(text.charAt(i)); if (dir == Character.DIRECTIONALITY_RIGHT_TO_LEFT diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsPanelInstaller.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsPanelInstaller.java index 1d4c3a4d80..65778b310e 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsPanelInstaller.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsPanelInstaller.java @@ -107,7 +107,6 @@ private static void onLyricsSettingChanged(String key) { return; } if (Settings.LYRICS_SOURCE.key.equals(key) - || Settings.LYRICS_SPLIT_BACKGROUND.key.equals(key) || LyricsFilterDefaults.LYRICS_CUSTOM_REGEX.key.equals(key) || LyricsFilterDefaults.LYRICS_TEXT_FILTER.key.equals(key) || LyricsFilterDefaults.LYRICS_CREDIT_LINE_REGEX.key.equals(key)) { @@ -139,7 +138,7 @@ private static void onLyricsSettingChanged(String key) { LockScreenLyrics.onSettingsChanged(); return; } - final LyricsPanelView panelView = panelReference.get(); + LyricsPanelView panelView = panelReference.get(); if (panelView == null) { return; } @@ -171,7 +170,7 @@ private static void onLyricsSettingChanged(String key) { private static void uninstallLyricsPanel() { installPending = false; - final LyricsPanelView panelView = panelReference.get(); + LyricsPanelView panelView = panelReference.get(); if (panelView == null) { return; } @@ -576,13 +575,13 @@ private static void walkForLyricsButton() { if (title == null) { return; } - final View root = activity.getWindow().getDecorView(); + View root = activity.getWindow().getDecorView(); enableButtonWalkPending = true; lastEnableButtonWalkUptimeMs = now; scheduleEnableButtonPass(root, title, 0); } - private static void scheduleEnableButtonPass(final View root, final String title, final int step) { + private static void scheduleEnableButtonPass(View root, String title, int step) { final long delay = step == 0 ? ENABLE_BUTTON_DELAYS_MS[0] : ENABLE_BUTTON_DELAYS_MS[step] - ENABLE_BUTTON_DELAYS_MS[step - 1]; diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsPanelView.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsPanelView.java index 22aca9374e..550f0971c2 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsPanelView.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsPanelView.java @@ -72,9 +72,11 @@ import app.morphe.extension.music.patches.lyrics.derive.LyricsTranslator; import app.morphe.extension.music.patches.lyrics.derive.QuickLRCClient; import app.morphe.extension.music.patches.lyrics.model.Lyrics; +import app.morphe.extension.music.patches.lyrics.model.LyricsBracketSplitter; import app.morphe.extension.music.patches.lyrics.model.LyricsLine; import app.morphe.extension.music.patches.lyrics.model.LyricsMerge; import app.morphe.extension.music.patches.lyrics.model.TrackInfo; +import app.morphe.extension.music.patches.lyrics.parsers.LRCParser; import app.morphe.extension.music.patches.lyrics.requests.LyricsRequests; import app.morphe.extension.music.patches.lyrics.romanizers.ScriptDetector; import app.morphe.extension.music.patches.lyrics.storage.LyricsFileSaver; @@ -228,7 +230,7 @@ enum OnlyMode { NONE, TRANS, ROMA, BOTH } private final List lineViews = new ArrayList<>(); - /** Wrapper holding the optional romanization line above each line of lyrics. */ + /** The row each of {@link #lineViews} sits in; a background vocal shares its lead line's row. */ private final List lineRows = new ArrayList<>(); /** @@ -318,6 +320,7 @@ public void run() { @Nullable private Lyrics lyrics; + private Lyrics lastRawInputLyrics; private Lyrics lastBuiltLyrics; private int highlightedIndex = -1; @@ -332,18 +335,52 @@ public int size() { @Override public boolean wordsEmpty(int line) { - return lineWordSpans.get(line).isEmpty(); + final int sz = size(); + if (line < 0 || line >= sz) { + return true; + } + if (!lineWordSpans.get(line).isEmpty()) { + return false; + } + for (int b = line + 1; b < sz && isSecondaryLineAt(b); b++) { + if (!lineWordSpans.get(b).isEmpty()) { + return false; + } + } + return true; } @Override public long firstStart(int line) { - return lineWordSpans.get(line).get(0).startMs(); + final int sz = size(); + long minStart = Long.MAX_VALUE; + if (line >= 0 && line < sz && !lineWordSpans.get(line).isEmpty()) { + minStart = lineWordSpans.get(line).get(0).startMs(); + } + for (int b = line + 1; b < sz && isSecondaryLineAt(b); b++) { + List bt = lineWordSpans.get(b); + if (!bt.isEmpty()) { + minStart = Math.min(minStart, bt.get(0).startMs()); + } + } + return minStart == Long.MAX_VALUE ? 0L : minStart; } @Override public long lastEnd(int line) { - final List timings = lineWordSpans.get(line); - return timings.get(timings.size() - 1).endMs(); + final int sz = size(); + long maxEnd = 0L; + if (line >= 0 && line < sz && !lineWordSpans.get(line).isEmpty()) { + List timings = lineWordSpans.get(line); + maxEnd = timings.get(timings.size() - 1).endMs(); + } + for (int b = line + 1; b < sz && isSecondaryLineAt(b); b++) { + List bt = lineWordSpans.get(b); + if (!bt.isEmpty()) { + maxEnd = Math.max(maxEnd, bt.get(bt.size() - 1).endMs()); + } + } + return maxEnd; } }; @@ -445,11 +482,13 @@ public void run() { public LyricsPanelView(Context context) { super(context); - final int horizontalPadding = Dim.dp32; + final int horizontalPadding = Dim.dp24; final int verticalPadding = Dim.dp16; linesContainer = new LinearLayout(context); linesContainer.setOrientation(LinearLayout.VERTICAL); + linesContainer.setClipChildren(false); + linesContainer.setClipToPadding(false); linesContainer.setPadding(horizontalPadding, verticalPadding, horizontalPadding, verticalPadding); footerView = new LyricsFit.FittingTextView(context); @@ -656,10 +695,10 @@ private void scheduleHideOffsetRuler() { } private void onOffsetLabelClicked() { - final LyricsManager manager = LyricsManager.getInstance(); + LyricsManager manager = LyricsManager.getInstance(); if (sbRecording) { final long endMs = manager.getVideoPositionMs(); - final String videoId = sbRecordVideoId; + String videoId = sbRecordVideoId; stopSbRecording(); scheduleHideOffsetRuler(); if (videoId == null || !Settings.LYRICS_SB_MATCHING.get()) { @@ -672,7 +711,7 @@ private void onOffsetLabelClicked() { startMs = stopMs; stopMs = swap; } - final Context context = getContext(); + Context context = getContext(); if (context == null) { return; } @@ -683,7 +722,7 @@ private void onOffsetLabelClicked() { if (!Settings.LYRICS_SB_MATCHING.get()) { return; } - final String videoId = manager.skipSegmentsVideoId(); + String videoId = manager.skipSegmentsVideoId(); if (videoId == null) { Utils.showToastShort(str("morphe_music_lyrics_sb_no_video")); return; @@ -744,6 +783,18 @@ protected void onDetachedFromWindow() { @Override public void onLyricsChanged(LyricsManager.State state, @Nullable Lyrics newLyrics) { try { + // Bracketed background vocals get lines of their own here, for this panel only. The + // same lyrics arriving again map to the same split, which the checks below rely on. + if (newLyrics != null && !newLyrics.isEmpty()) { + if (newLyrics == lastRawInputLyrics && lyrics != null) { + newLyrics = lyrics; + } else if (newLyrics != lyrics) { + lastRawInputLyrics = newLyrics; + newLyrics = LyricsBracketSplitter.apply(newLyrics); + } + } else { + lastRawInputLyrics = null; + } // Refresh keeps the current rendering (including scroll and translations) // until a replacement arrives. if (state == LyricsManager.State.LOADING && newLyrics == lyrics @@ -883,8 +934,8 @@ && applySectionLabelsInPlace(newLyrics)) { } private static boolean sameLinesExceptSongPart(@NonNull Lyrics a, @NonNull Lyrics b) { - final List left = a.lines(); - final List right = b.lines(); + List left = a.lines(); + List right = b.lines(); if (left.size() != right.size() || a.synced() != b.synced()) { return false; } @@ -903,7 +954,7 @@ private static boolean sameLinesExceptSongPart(@NonNull Lyrics a, @NonNull Lyric } private boolean applySectionLabelsInPlace(@NonNull Lyrics newLyrics) { - final List newLines = newLyrics.lines(); + List newLines = newLyrics.lines(); if (lineRows.size() != newLines.size() || lineViews.size() != newLines.size() || sectionTags.size() != newLines.size() || sectionDots.size() != newLines.size() @@ -919,7 +970,7 @@ private boolean applySectionLabelsInPlace(@NonNull Lyrics newLyrics) { for (int i = 0; i < newLines.size(); i++) { LyricsLine line = newLines.get(i); // The same rule buildLyrics applies, which it has to stay in step with. - final String songPart = line.songPart(); + String songPart = line.songPart(); final boolean opensSection = !line.isBG() && songPart != null && !songPart.isEmpty() && !songPart.equals(previousLeadSongPart); @@ -927,8 +978,8 @@ private boolean applySectionLabelsInPlace(@NonNull Lyrics newLyrics) { previousLeadSongPart = songPart; } - final TextView builtTag = sectionTags.get(i); - final FrameLayout builtSlot = sectionDotSlots.get(i); + TextView builtTag = sectionTags.get(i); + FrameLayout builtSlot = sectionDotSlots.get(i); if (opensSection && builtTag != null) { builtTag.setText(songPart); continue; @@ -963,7 +1014,7 @@ private boolean applySectionLabelsInPlace(@NonNull Lyrics newLyrics) { lyrics = newLyrics; lastBuiltLyrics = newLyrics; - final List newGaps = SectionDotsMotion.computeGaps(newLines); + List newGaps = SectionDotsMotion.computeGaps(newLines); sectionDotsGaps = newGaps; ensureDotsSlotsExist(context, newGaps); return true; @@ -981,7 +1032,7 @@ private void ensureDotsSlotsExist(Context context, List g || sectionTags.get(index) != null || sectionDotSlots.get(index) != null) { continue; } - final ViewGroup existingRow = asRow(lineRows.get(index)); + ViewGroup existingRow = asRow(lineRows.get(index)); if (existingRow == null) { continue; } @@ -1124,13 +1175,13 @@ private void buildLyrics(Lyrics newLyrics) { final boolean sameContent = !lineRows.isEmpty() && lastBuiltLyrics == newLyrics; final int prevCount = sameContent ? Math.min(lineViews.size(), newLyrics.lines().size()) : 0; - final List prevViews = sameContent + List prevViews = sameContent ? new ArrayList<>(lineViews) : Collections.emptyList(); - final int[] prevHeights = new int[prevCount]; + int[] prevHeights = new int[prevCount]; for (int i = 0; i < prevCount; i++) { prevHeights[i] = i < lineRows.size() ? lineRows.get(i).getHeight() : 0; } - final List> prevSpans = sameContent + List> prevSpans = sameContent ? new ArrayList<>(lineWordSpans) : Collections.emptyList(); final int prevHighlighted = highlightedIndex; lastBuiltLyrics = newLyrics; @@ -1149,22 +1200,19 @@ private void buildLyrics(Lyrics newLyrics) { final int foregroundColor = LyricsColors.lineTextColor(); final boolean lineGestures = Settings.LYRICS_TAP_TO_SEEK.get(); final boolean tapToSeek = newLyrics.synced() && lineGestures; - final Paint baseLinePaint = new Paint(); + Paint baseLinePaint = new Paint(); baseLinePaint.setTextSize(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, textSize, getResources().getDisplayMetrics())); - baseLinePaint.setTypeface(Typeface.DEFAULT_BOLD); + LyricsViewSupport.applyLyricsBoldPaint(baseLinePaint); baseLinePaint.setElegantTextHeight(true); final int generation = buildGeneration.get(); - final OnlyMode onlySnap = onlyMode; + OnlyMode onlySnap = onlyMode; builtOnlyMode = onlySnap; - final List romanizedSnap = romanizedLines; - final List translatedSnap = translatedLines; - final boolean perWordRomajiSnap = perWordRomaji; String previousLeadSongPart = null; // Needed up front: a line without a label only gets a slot if a silence calls for one. - final List builtGaps = SectionDotsMotion.computeGaps(newLyrics.lines()); - final boolean[] gapAnchors = new boolean[newLyrics.lines().size()]; + List builtGaps = SectionDotsMotion.computeGaps(newLyrics.lines()); + boolean[] gapAnchors = new boolean[newLyrics.lines().size()]; for (SectionDotsMotion.Gap gap : builtGaps) { final int anchor = gap.tagLineIndex(); if (anchor >= 0 && anchor < gapAnchors.length) { @@ -1179,21 +1227,38 @@ private void buildLyrics(Lyrics newLyrics) { break; } } - final boolean[] lineRightAligned = new boolean[newLyrics.lines().size()]; - for (int i = 0; i < lineRightAligned.length; i++) { + final int lineTotalCount = newLyrics.lines().size(); + boolean[] isSecFlags = new boolean[lineTotalCount]; + for (int i = 0; i < lineTotalCount; i++) { + isSecFlags[i] = isSecondaryLyricLine(i, newLyrics.lines()); + } + + boolean[] lineRightAligned = new boolean[lineTotalCount]; + int curParentForAlign = -1; + for (int i = 0; i < lineTotalCount; i++) { + if (!isSecFlags[i]) { + curParentForAlign = i; + } LyricsLine l = newLyrics.lines().get(i); boolean lineRtl = ScriptDetector.isLineRtl(l.text()); + boolean isDuetLine = l.isDuet() + || (isSecFlags[i] && curParentForAlign >= 0 + && newLyrics.lines().get(curParentForAlign).isDuet()); lineRightAligned[i] = isDuetSong - ? (lineRtl ? !l.isDuet() : l.isDuet()) + ? (lineRtl ? !isDuetLine : isDuetLine) : lineRtl; } - final int duetSideInset = Math.max(Dim.dp(24), - (int) (context.getResources().getDisplayMetrics().widthPixels * 0.09f)); + final int duetSideInset = Math.max(Dim.dp(48), + (int) (context.getResources().getDisplayMetrics().widthPixels * 0.18f)); + + LinearLayout currentParentRow = null; + int currentParentIdx = -1; - for (int i = 0; i < newLyrics.lines().size(); i++) { + for (int i = 0; i < lineTotalCount; i++) { LyricsLine line = newLyrics.lines().get(i); + final boolean isSec = isSecFlags[i] && currentParentRow != null; final boolean isRightAligned = lineRightAligned[i]; - final LyricsLineView prevView = i < prevCount + LyricsLineView prevView = i < prevCount && prevViews.get(i) instanceof LyricsLineView pv ? pv : null; final boolean reusePrevious = prevView != null && !contentInBuild; List gapTimings = reusePrevious @@ -1202,26 +1267,25 @@ private void buildLyrics(Lyrics newLyrics) { lineWordSpans.add(reusePrevious ? gapTimings : new ArrayList<>()); LyricsLineView lineView = new LyricsLineView(context); + lineView.setSecondary(isSec); + lineView.setIsRightAligned(isRightAligned); // Plain text first so the panel paints immediately; the karaoke spans are added on a // background thread (see the LINE_BUILDER_EXECUTOR pass below). A hide rebuild keeps // the surviving regions so its first frame matches the shrunken layout exactly, and a // same-content rebuild reuses the previous row so toggles never blink existing text. int gapOrigStart = 0; if (preserveHidePosition) { - gapOrigStart = computeOriginalTextStart(line, i, onlySnap, romanizedSnap, - translatedSnap, perWordRomajiSnap); LyricsSpanBuilder.BuildResult result; if (LyricsPresentation.isFormattingEnabled()) { // The previous spans are offsets in the filtered text, so the line is built // again from the raw word timings and filtered once more. - final List rawTimings = LyricsSpanBuilder + List rawTimings = LyricsSpanBuilder .computeWordTimings(newLyrics.lines(), i, onlySnap != OnlyMode.NONE); - final LyricsSpanBuilder.BuildResult raw = LyricsSpanBuilder.buildLineText(line, + LyricsSpanBuilder.BuildResult raw = LyricsSpanBuilder.buildLineText(line, rawTimings, i, perWordRomaji, romanizedLines, translatedLines, onlyMode, baseLinePaint, isRightAligned); - final LyricsSpanBuilder.PresentedLine presentation = LyricsSpanBuilder.present(raw, - LyricsViewSupport.wordSpansFor(raw, onlySnap != OnlyMode.NONE, rawTimings, - gapOrigStart), gapOrigStart); + LyricsSpanBuilder.PresentedLine presentation = LyricsSpanBuilder.present(raw, + LyricsViewSupport.wordSpansFor(raw, onlySnap != OnlyMode.NONE, rawTimings)); result = presentation.display(); gapTimings = presentation.words(); gapOrigStart = presentation.originalStart(); @@ -1230,23 +1294,26 @@ private void buildLyrics(Lyrics newLyrics) { result = LyricsSpanBuilder.buildLineText(line, gapTimings, i, perWordRomaji, romanizedLines, translatedLines, onlyMode, baseLinePaint, isRightAligned); + gapOrigStart = result.originalStart(); } lineView.setText(result.text()); lineView.setTranslationBounds(result.transStart(), result.transEnd()); lineView.setRomanizationBounds(result.romaStart(), result.romaEnd(), - result.readingSpans() != null && !result.readingSpans().isEmpty()); + result.readingSpans() != null && !result.readingSpans().isEmpty(), + result.showSyllableRomanization()); lineView.setSmallRunHoldsWords(result.smallRunHoldsWords()); } else if (reusePrevious) { lineView.setText(prevView.getText()); lineView.setTranslationBounds(prevView.transStart, prevView.transEnd); lineView.setRomanizationBounds(prevView.romaStart, prevView.romaEnd, - prevView.perWordReading); + prevView.perWordReading, prevView.showSyllableRomanization); lineView.setSmallRunHoldsWords(prevView.smallRunHoldsWords); lineView.transReveal = prevView.transReveal; lineView.romaReveal = prevView.romaReveal; gapOrigStart = prevView.originalTextStart; } else { - String rawText = line.text().isEmpty() ? "♪" : LyricsPresentation.formatText(line.text()); + String formatted = LyricsPresentation.formatText(LyricsSpanBuilder.displayLineText(line)); + String rawText = formatted.isEmpty() ? "♪" : formatted; boolean isRtl = ScriptDetector.isLineRtl(rawText); SpannableString initialText = new SpannableString(rawText); initialText.setSpan(new AlignmentSpan.Standard(LyricsSpanBuilder.resolveAlignment(isRtl, isRightAligned)), @@ -1258,19 +1325,28 @@ private void buildLyrics(Lyrics newLyrics) { lineView.setHighlight(gapTimings, prevView.positionMs, prevView.allSung, LyricsColors.unsungWordColor(), gapOrigStart); } - lineView.setActiveLine(reusePrevious && i == prevHighlighted, - LyricsColors.secondaryTextColor()); - lineView.setTextSize(TypedValue.COMPLEX_UNIT_SP, LyricsViewSupport.lineTextSizeSp(line.isBG(), textSize)); + final int effectiveLeadIdx = isSec ? currentParentIdx : i; + final boolean initialActive = !newLyrics.synced() + || (reusePrevious && (effectiveLeadIdx == prevHighlighted || i == prevHighlighted)); + lineView.setActiveLine(initialActive, LyricsColors.secondaryTextColor()); + lineView.setTextSize(TypedValue.COMPLEX_UNIT_SP, + LyricsViewSupport.lineTextSizeSp(isSec, textSize)); lineView.setTextColor(foregroundColor); - lineView.setAlpha(1f); - lineView.lineAlpha = newLyrics.synced() - ? (reusePrevious && i == prevHighlighted ? 1f : INACTIVE_LINE_ALPHA) - : 1f; + lineView.lineAlpha = initialActive ? 1f : (isSec ? 1f : INACTIVE_LINE_ALPHA); + lineView.lineScale = initialActive + ? LyricsViewSupport.ACTIVE_LINE_SCALE + : LyricsViewSupport.INACTIVE_LINE_SCALE; + if (!isSec) { + lineView.setAlpha(1f); + LyricsViewSupport.applyLineScaleTransform(lineView, lineView.lineScale); + } lineView.contentReveal = contentInBuild ? 0f : 1f; - lineView.setPadding(0, Dim.dp8, 0, Dim.dp8); + lineView.setPadding(0, isSec ? Dim.dp(2) : Dim.dp(8), 0, isSec ? Dim.dp(2) : Dim.dp(8)); lineView.setIncludeFontPadding(false); lineView.getPaint().setElegantTextHeight(true); lineView.setTypeface(null, Typeface.BOLD); + LyricsViewSupport.applyLyricsBoldPaint(lineView.getPaint()); + lineView.setLetterSpacing(LyricsViewSupport.lineLetterSpacing(line.text(), textSize)); if (newLyrics.synced()) { lineView.setTextColor(LyricsColors.unsungWordColor()); @@ -1279,7 +1355,7 @@ private void buildLyrics(Lyrics newLyrics) { if (tapToSeek) { lineView.setOnClickListener(view -> { final long videoLength = VideoInformation.getVideoLength(); - final LyricsManager manager = LyricsManager.getInstance(); + LyricsManager manager = LyricsManager.getInstance(); long target = manager.toVideoTimeMs(line.startTimeMs() + Settings.LYRICS_OFFSET_MS.get() + manager.getTemporaryOffsetMs()); @@ -1304,7 +1380,7 @@ private void buildLyrics(Lyrics newLyrics) { if (textToCopy == null || textToCopy.isEmpty()) { textToCopy = onlyMode != OnlyMode.NONE ? String.valueOf(lv.getText()) - : line.text(); + : (isSec ? LyricsBracketSplitter.stripOuterBrackets(line.text()) : line.text()); } if (textToCopy.isEmpty()) { return false; @@ -1326,8 +1402,42 @@ private void buildLyrics(Lyrics newLyrics) { lineView.setGravity(Gravity.START | Gravity.TOP); } + // A background vocal goes into its lead line's row, above the lead when sung ahead of it. + if (isSec && currentParentRow != null) { + final boolean bgBeforeMain = isBgBeforeMain(newLyrics.lines(), currentParentIdx, i); + lineView.setParentLineIndex(currentParentIdx); + lineView.setBgBeforeMain(bgBeforeMain); + + LinearLayout.LayoutParams bgParams = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT); + if (bgBeforeMain && currentParentIdx >= 0 && currentParentIdx < lineViews.size()) { + bgParams.bottomMargin = initialActive ? Dim.dp(4) : 0; + TextView parentMainView = lineViews.get(currentParentIdx); + final int mainIdxInRow = currentParentRow.indexOfChild(parentMainView); + currentParentRow.addView(lineView, Math.max(0, mainIdxInRow), bgParams); + } else { + bgParams.topMargin = initialActive ? Dim.dp(4) : 0; + currentParentRow.addView(lineView, bgParams); + } + lineView.animateSecondaryExpansion(initialActive, true); + + lineViews.add(lineView); + lineRows.add(currentParentRow); + sectionTags.add(null); + sectionDots.add(null); + sectionDotSlots.add(null); + lineUnsungSpans.add(null); + lineIsBackground.add(true); + continue; + } + + currentParentIdx = i; LinearLayout lineRow = new LinearLayout(context); lineRow.setOrientation(LinearLayout.VERTICAL); + lineRow.setClipChildren(false); + lineRow.setClipToPadding(false); + currentParentRow = lineRow; if (isDuetSong) { if (isRightAligned) { @@ -1339,16 +1449,13 @@ private void buildLyrics(Lyrics newLyrics) { lineRow.setPadding(0, Dim.dp(2), 0, Dim.dp(2)); } - // A background vocal sits under its lead line and carries the same label, so only a - // lead line opens a section. The dots measure the silence on the same lines, so a - // label and the dots standing in for it always land on the same line. - final String songPart = line.songPart(); - final boolean opensSection = !line.isBG() - && songPart != null && !songPart.isEmpty() + // The dots measure the silence on the same lines as the labels, so a label and the + // dots standing in for it always land on the same line. + String songPart = line.songPart(); + // Only a lead line gets here, so a background vocal never opens a section of its own. + final boolean opensSection = songPart != null && !songPart.isEmpty() && !songPart.equals(previousLeadSongPart); - if (!line.isBG()) { - previousLeadSongPart = songPart; - } + previousLeadSongPart = songPart; TextView sectionTag = createSectionTag(context, opensSection ? songPart : null); LinearLayout sectionDotsRow = null; @@ -1359,7 +1466,7 @@ private void buildLyrics(Lyrics newLyrics) { tagRow.addView(sectionTag, tagLayoutParams(isRightAligned)); tagRow.addView(sectionDotsRow, dotsLayoutParams(context, isRightAligned)); lineRow.addView(tagRow, slotLayoutParams()); - final TextView measuredTag = sectionTag; + TextView measuredTag = sectionTag; measuredTag.post(() -> { if (measuredTag.getHeight() > 0) { sectionTagHeightPx = measuredTag.getHeight(); @@ -1379,22 +1486,23 @@ private void buildLyrics(Lyrics newLyrics) { lineRow.addView(sectionDotSlot, slotParams); } - lineRow.addView(lineView, new LinearLayout.LayoutParams( + LinearLayout.LayoutParams lineViewParams = new LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT, - LinearLayout.LayoutParams.WRAP_CONTENT)); + LinearLayout.LayoutParams.WRAP_CONTENT); + lineRow.addView(lineView, lineViewParams); - // Before the last child, since the footer was added first and stays below the lyrics. - linesContainer.addView(lineRow, linesContainer.getChildCount() - 1, - new LinearLayout.LayoutParams( - LinearLayout.LayoutParams.MATCH_PARENT, - LinearLayout.LayoutParams.WRAP_CONTENT)); + LinearLayout.LayoutParams rowParams = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT); + // Above the footer, which stays below the lyrics. + linesContainer.addView(lineRow, linesContainer.indexOfChild(footerContainer), rowParams); lineViews.add(lineView); lineRows.add(lineRow); sectionTags.add(sectionTag); sectionDots.add(sectionDotsRow); sectionDotSlots.add(sectionDotSlot); lineUnsungSpans.add(null); - lineIsBackground.add(line.isBG()); + lineIsBackground.add(false); } sectionDotsGaps = builtGaps; @@ -1403,15 +1511,12 @@ private void buildLyrics(Lyrics newLyrics) { try { final int lineCount = newLyrics.lines().size(); List> allTimings = new ArrayList<>(lineCount); - List allOrigStarts = new ArrayList<>(lineCount); for (int i = 0; i < lineCount; i++) { if (generation != buildGeneration.get() || lyrics != newLyrics) { return; } allTimings.add(LyricsSpanBuilder.computeWordTimings( newLyrics.lines(), i, onlySnap != OnlyMode.NONE)); - allOrigStarts.add(computeOriginalTextStart(newLyrics.lines().get(i), i, - onlySnap, romanizedSnap, translatedSnap, perWordRomajiSnap)); } handler.post(() -> { if (generation != buildGeneration.get() || lyrics != newLyrics) { @@ -1419,18 +1524,18 @@ private void buildLyrics(Lyrics newLyrics) { } final boolean only = onlyMode != OnlyMode.NONE; final int count = Math.min(allTimings.size(), lineViews.size()); - final int[] plainHeights = new int[count]; - final boolean[] freshTrans = new boolean[count]; - final boolean[] freshRoma = new boolean[count]; + int[] plainHeights = new int[count]; + boolean[] freshTrans = new boolean[count]; + boolean[] freshRoma = new boolean[count]; for (int i = 0; i < count; i++) { final int h = lineRows.get(i).getHeight(); plainHeights[i] = h > 1 ? h : (i < prevHeights.length ? prevHeights[i] : h); - final TextView prevRaw = i < prevViews.size() ? prevViews.get(i) : null; - final LyricsLineView prevView = prevRaw instanceof LyricsLineView pv + TextView prevRaw = i < prevViews.size() ? prevViews.get(i) : null; + LyricsLineView prevView = prevRaw instanceof LyricsLineView pv ? pv : null; freshTrans[i] = !(prevView != null && prevView.transStart >= 0 && prevView.transReveal >= 1f); - freshRoma[i] = !(prevView != null && prevView.romaStart >= 0 + freshRoma[i] = !(prevView != null && prevView.hasRomanizationRegion() && prevView.romaReveal >= 1f); } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { @@ -1448,7 +1553,6 @@ private void buildLyrics(Lyrics newLyrics) { || i >= lineOriginalStarts.size()) { return; } - lineOriginalStarts.set(i, only ? 0 : allOrigStarts.get(i)); TextView tv = lineViews.get(i); if (i < newLyrics.lines().size()) { LyricsSpanBuilder.BuildResult result = @@ -1456,25 +1560,34 @@ private void buildLyrics(Lyrics newLyrics) { allTimings.get(i), i, perWordRomaji, romanizedLines, translatedLines, onlyMode, baseLinePaint, lineRightAligned[i]); - final LyricsSpanBuilder.PresentedLine presentation = LyricsSpanBuilder.present(result, - LyricsViewSupport.wordSpansFor(result, only, allTimings.get(i), allOrigStarts.get(i)), - only ? 0 : allOrigStarts.get(i)); + LyricsSpanBuilder.PresentedLine presentation = LyricsSpanBuilder.present(result, + LyricsViewSupport.wordSpansFor(result, only, allTimings.get(i))); result = presentation.display(); lineOriginalStarts.set(i, presentation.originalStart()); tv.setText(result.text()); lineUnsungSpans.set(i, result.unsungSpan()); - lineWordSpans.set(i, presentation.words()); + List resolvedSpans = presentation.words(); + lineWordSpans.set(i, resolvedSpans); if (tv instanceof LyricsLineView lineView) { lineView.setTranslationBounds(result.transStart(), result.transEnd()); lineView.setRomanizationBounds(result.romaStart(), result.romaEnd(), result.readingSpans() != null - && !result.readingSpans().isEmpty()); + && !result.readingSpans().isEmpty(), + result.showSyllableRomanization()); lineView.setSmallRunHoldsWords(result.smallRunHoldsWords()); + // The view lays syllable readings out from the words, so they + // go in now for the row to be measured with them below. + if (result.showSyllableRomanization()) { + lineView.setHighlight(resolvedSpans, lineView.positionMs, + lineView.allSung, LyricsColors.unsungWordColor(), + presentation.originalStart()); + } if (!contentInBuild && !preserveHidePosition) { if (result.transStart() >= 0 && freshTrans[i]) { lineView.transReveal = 0f; } - if (result.romaStart() >= 0 && freshRoma[i]) { + if ((result.romaStart() >= 0 || result.showSyllableRomanization()) + && freshRoma[i]) { lineView.romaReveal = 0f; } } @@ -1486,16 +1599,20 @@ private void buildLyrics(Lyrics newLyrics) { linesContainer.suppressLayout(false); } } - final List resizes = new ArrayList<>(); + List resizes = new ArrayList<>(); if (!contentInBuild) { for (int i = 0; i < count; i++) { + // A shared row is resized once, with its lead line. + if (i > 0 && lineRows.get(i) == lineRows.get(i - 1)) { + continue; + } final int oldH = plainHeights[i]; if (oldH <= 1 || lineRows.get(i).getWidth() <= 0 || !(lineViews.get(i) instanceof LyricsLineView view)) { continue; } - final TextView tv = lineViews.get(i); - final View row = lineRows.get(i); + TextView tv = lineViews.get(i); + View row = lineRows.get(i); row.measure(View.MeasureSpec.makeMeasureSpec( row.getWidth(), View.MeasureSpec.EXACTLY), View.MeasureSpec.makeMeasureSpec(0, @@ -1504,7 +1621,7 @@ private void buildLyrics(Lyrics newLyrics) { if (Math.abs(newH - oldH) <= 1) { continue; } - final Layout layout = tv.getLayout(); + Layout layout = tv.getLayout(); if (layout == null) { continue; } @@ -1513,12 +1630,13 @@ private void buildLyrics(Lyrics newLyrics) { + LyricsViewSupport.leadingRegionSize(layout, view.romaStart, view.romaEnd); final int trailing = LyricsViewSupport.trailingRegionSize(layout, view.transStart, view.transEnd) - + LyricsViewSupport.trailingRegionSize(layout, view.romaStart, view.romaEnd); + + LyricsViewSupport.trailingRegionSize(layout, view.romaStart, view.romaEnd) + + view.syllableRomanizationExtraHeight(); resizes.add(new RowResize(i, lineRows.get(i), tv, oldH, newH, leading, trailing)); } } - final Runnable revealTask = () -> { + Runnable revealTask = () -> { for (int i = 0; i < count && i < lineViews.size(); i++) { if (!(lineViews.get(i) instanceof LyricsLineView view)) { continue; @@ -1529,7 +1647,7 @@ private void buildLyrics(Lyrics newLyrics) { if (view.transStart >= 0 && freshTrans[i]) { view.startRegionIn(true); } - if (view.romaStart >= 0 && freshRoma[i]) { + if (view.hasRomanizationRegion() && freshRoma[i]) { view.startRegionIn(false); } } @@ -1646,7 +1764,7 @@ public boolean onPreDraw() { final boolean preserved = index == preserveIndex; final int maxScroll = Math.max(0, linesContainer.getHeight() - scrollView.getHeight()); - final TextView lineView = lineViews.get(index); + TextView lineView = lineViews.get(index); final int mainOffset = mainLineOffset(lineView); final int y = Math.max(0, Math.min(maxScroll, lineRows.get(index).getTop() @@ -2008,7 +2126,7 @@ private void fallBackFromDualMode(boolean romanizeFailed) { applyOnlyModeState(); updateDualPill(); updateFooter(); - final Lyrics current = lyrics; + Lyrics current = lyrics; if (current != null) { showLyrics(current); } @@ -2058,7 +2176,7 @@ private void exitDualMode(boolean keepData) { onlyMode = OnlyMode.NONE; applyOnlyModeState(); updateDualPill(); - final Lyrics current = lyrics; + Lyrics current = lyrics; if (current != null) { showLyrics(current); } @@ -2158,8 +2276,8 @@ private void captureOnlyModeScrollAnchor() { || highlightedIndex >= lineViews.size()) { return; } - final View row = lineRows.get(highlightedIndex); - final TextView child = lineViews.get(highlightedIndex); + View row = lineRows.get(highlightedIndex); + TextView child = lineViews.get(highlightedIndex); if (child.getHeight() <= 0 || linesContainer.getHeight() <= 0) { return; } @@ -2194,11 +2312,11 @@ private void onSourceClicked() { */ private String textToCopyAt(LyricsLine line, int index) { if (onlyMode == OnlyMode.TRANS) { - final String translation = LyricsSpanBuilder.translationAt(line, index, translatedLines); + String translation = LyricsSpanBuilder.translationAt(line, index, translatedLines); return translation != null ? translation : line.text(); } if (onlyMode == OnlyMode.ROMA) { - final String romanization = + String romanization = LyricsSpanBuilder.romanizationForOnlyMode(line, index, romanizedLines); return romanization != null ? romanization : line.text(); } @@ -2257,10 +2375,10 @@ private void onCopyLongPressed() { return; } saveInProgress = true; - final Context appContext = getContext().getApplicationContext(); + Context appContext = getContext().getApplicationContext(); Utils.runOnBackgroundThread(() -> { try { - final String savedPath = LyricsFileSaver.save(appContext, track, current); + String savedPath = LyricsFileSaver.save(appContext, track, current); Utils.runOnMainThread(() -> { saveInProgress = false; if (savedPath == null) { @@ -2561,7 +2679,7 @@ private boolean showTranscribePrompt() { if (!QuickLRCClient.isConfigured()) { return false; } - final TrackInfo track = LyricsManager.getInstance().getCurrentTrack(); + TrackInfo track = LyricsManager.getInstance().getCurrentTrack(); if (track == null) { return false; } @@ -2582,7 +2700,7 @@ private void showOnlyRefreshButton() { return; } for (int i = 0; i < buttonRow.getChildCount(); i++) { - final View button = buttonRow.getChildAt(i); + View button = buttonRow.getChildAt(i); if (button != refreshView && button.getVisibility() == VISIBLE) { button.setVisibility(GONE); hiddenToolbarButtons.add(button); @@ -2627,8 +2745,8 @@ private void cancelTranscription(boolean announce) { @SuppressWarnings("ConstantValue") private void startTranscription(@Nullable String lyrics) { - final TrackInfo track = LyricsManager.getInstance().getCurrentTrack(); - final String videoId = VideoInformation.getVideoId(); + TrackInfo track = LyricsManager.getInstance().getCurrentTrack(); + String videoId = VideoInformation.getVideoId(); if (track == null || videoId == null || videoId.isEmpty()) { Utils.showToastShort(str("morphe_music_lyrics_transcribe_local_unsupported")); return; @@ -2672,7 +2790,7 @@ private void startTranscription(@Nullable String lyrics) { * preserve, so it goes straight in. */ private void onTranscriptionReady(Lyrics result, TrackInfo track) { - final Lyrics current = lyrics; + Lyrics current = lyrics; if (current != null && !current.isEmpty() && current != Lyrics.NOT_FOUND) { pendingAlignment = result; pendingAlignmentTrack = track; @@ -2684,14 +2802,14 @@ private void onTranscriptionReady(Lyrics result, TrackInfo track) { } private void applyPendingAlignment() { - final Lyrics pending = pendingAlignment; - final TrackInfo track = pendingAlignmentTrack; + Lyrics pending = pendingAlignment; + TrackInfo track = pendingAlignmentTrack; pendingAlignment = null; pendingAlignmentTrack = null; if (pending == null) { return; } - final TrackInfo current = LyricsManager.getInstance().getCurrentTrack(); + TrackInfo current = LyricsManager.getInstance().getCurrentTrack(); if (track != null && current != null && !track.equals(current)) { // The track moved on while the answer was on its way, so it belongs to neither. Utils.showToastShort(str("morphe_music_lyrics_transcribe_cancelled")); @@ -2712,7 +2830,7 @@ private void updateTranscribeAffordances() { } // A pending result is offered in the search dialog; taking this tap instead made the // button read as dead. - final TextView alignButton = dialogAlignButton; + TextView alignButton = dialogAlignButton; if (alignButton != null) { alignButton.setText(pendingAlignment != null ? str("morphe_music_lyrics_transcribe_apply_align") @@ -2742,7 +2860,7 @@ private void openSearchDialog() { Utils.showToastShort(str("morphe_music_lyrics_search_no_providers")); return; } - final Context context = getContext(); + Context context = getContext(); if (context == null) { return; } @@ -2752,11 +2870,11 @@ private void openSearchDialog() { AutoCompleteTextView artistInput = LyricsSearchUI.createSearchInput(context, str("morphe_music_lyrics_search_artist_hint")); - final String rawTitle = manager.getCurrentRawTitle(); - final String rawArtist = manager.getCurrentRawArtist(); - final String defaultTitle = rawTitle != null && !rawTitle.trim().isEmpty() + String rawTitle = manager.getCurrentRawTitle(); + String rawArtist = manager.getCurrentRawArtist(); + String defaultTitle = rawTitle != null && !rawTitle.trim().isEmpty() ? rawTitle.trim() : track.title(); - final String defaultArtist = rawArtist != null && !rawArtist.trim().isEmpty() + String defaultArtist = rawArtist != null && !rawArtist.trim().isEmpty() ? rawArtist.trim() : track.artist(); String[] remembered = manager.rememberedSearchTerms(); String initialTitle = remembered != null && remembered[0] != null @@ -2807,10 +2925,10 @@ private void openSearchDialog() { cancelTranscription(true); return; } - final Lyrics current = lyrics; + Lyrics current = lyrics; String text = null; if (current != null && !current.isEmpty() && current != Lyrics.NOT_FOUND) { - final StringBuilder joined = createJoined(current); + StringBuilder joined = createJoined(current); text = joined.toString(); } if (text == null || text.trim().isEmpty()) { @@ -2869,8 +2987,8 @@ private void openSearchDialog() { // One round per dialog, fetched once it is on screen so the first keystroke never // waits for MusicBrainz. - final String titleSuggestion = titleInput.getText().toString(); - final String artistSuggestion = artistInput.getText().toString(); + String titleSuggestion = titleInput.getText().toString(); + String artistSuggestion = artistInput.getText().toString(); Utils.runOnBackgroundThread(() -> { List titles = LyricsRequests.MusicBrainzClient.suggestTitles(titleSuggestion); List artists = LyricsRequests.MusicBrainzClient.suggestArtists(artistSuggestion); @@ -2894,7 +3012,7 @@ private void openSearchDialog() { @SuppressWarnings("unused") @NonNull private static StringBuilder createJoined(Lyrics current) { - final StringBuilder joined = new StringBuilder(); + StringBuilder joined = new StringBuilder(); int sentLines = 0; int skippedBg = 0; for (LyricsLine line : current.lines()) { @@ -2902,7 +3020,7 @@ private static StringBuilder createJoined(Lyrics current) { skippedBg++; continue; } - final String lineText = line.text(); + String lineText = line.text(); if (lineText != null && !lineText.trim().isEmpty()) { joined.append(lineText).append('\n'); sentLines++; @@ -2931,8 +3049,8 @@ private void startRowHeightAnimation(List resizes, boolean releaseAtE && growthAnchorIndex >= 0 && growthAnchorIndex < lineRows.size() && growthAnchorIndex < lineViews.size() && linesContainer.getHeight() > 0) { - final View row = lineRows.get(growthAnchorIndex); - final TextView child = lineViews.get(growthAnchorIndex); + View row = lineRows.get(growthAnchorIndex); + TextView child = lineViews.get(growthAnchorIndex); scrollAnchorRowIndex = growthAnchorIndex; scrollAnchorBaseRowTop = row.getTop(); scrollAnchorChildTop = child.getTop() + mainLineOffset(child); @@ -2942,12 +3060,12 @@ private void startRowHeightAnimation(List resizes, boolean releaseAtE SystemClock.uptimeMillis() + SECONDARY_REVEAL_MILLISECONDS + 100); } for (RowResize resize : resizes) { - final ViewGroup.LayoutParams lp = resize.row.getLayoutParams(); + ViewGroup.LayoutParams lp = resize.row.getLayoutParams(); lp.height = resize.from; resize.row.setLayoutParams(lp); } - final boolean[] canceled = {false}; - final ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f); + boolean[] canceled = {false}; + ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f); animator.setDuration(SECONDARY_REVEAL_MILLISECONDS); animator.addUpdateListener(animation -> { final float t = (float) animation.getAnimatedValue(); @@ -2958,7 +3076,7 @@ private void startRowHeightAnimation(List resizes, boolean releaseAtE for (RowResize resize : resizes) { final int height = Math.round( resize.from + (resize.to - resize.from) * t); - final ViewGroup.LayoutParams lp = resize.row.getLayoutParams(); + ViewGroup.LayoutParams lp = resize.row.getLayoutParams(); lp.height = height; resize.row.setLayoutParams(lp); final int spread = resize.leading + resize.trailing; @@ -3001,7 +3119,7 @@ public void onAnimationEnd(Animator animation) { scrollAnchorRowIndex = -1; if (releaseAtEnd) { for (RowResize resize : resizes) { - final ViewGroup.LayoutParams lp = resize.row.getLayoutParams(); + ViewGroup.LayoutParams lp = resize.row.getLayoutParams(); lp.height = ViewGroup.LayoutParams.WRAP_CONTENT; resize.row.setLayoutParams(lp); resize.child.setTranslationY(0f); @@ -3045,7 +3163,7 @@ private int mainLineOffsetFor(TextView lineView) { if (!(lineView instanceof LyricsLineView view)) { return 0; } - final Layout layout = view.getLayout(); + Layout layout = view.getLayout(); if (layout == null) { return 0; } @@ -3060,7 +3178,7 @@ private void hideSecondaryRegions(boolean trans, Lyrics current) { if (!(lineView instanceof LyricsLineView view)) { continue; } - if (trans ? view.transStart >= 0 : view.romaStart >= 0) { + if (trans ? view.transStart >= 0 : view.hasRomanizationRegion()) { any = true; } } @@ -3079,7 +3197,7 @@ private void hideSecondaryRegions(boolean trans, Lyrics current) { if (!(lineView instanceof LyricsLineView view)) { continue; } - if (trans ? view.transStart >= 0 : view.romaStart >= 0) { + if (trans ? view.transStart >= 0 : view.hasRomanizationRegion()) { view.animateRegionOut(trans); } } @@ -3110,8 +3228,8 @@ private void captureHideScrollAnchor() { if (index < 0 || index >= lineRows.size() || index >= lineViews.size()) { return; } - final View row = lineRows.get(index); - final TextView child = lineViews.get(index); + View row = lineRows.get(index); + TextView child = lineViews.get(index); final int mainOffset = mainLineOffset(child); pendingHideAnchorIndex = index; pendingHideAnchorScreenY = row.getTop() + child.getTop() @@ -3127,20 +3245,33 @@ private void captureHideScrollAnchor() { } private void shrinkRegionRows(boolean trans, Runnable onEnd) { - final List resizes = new ArrayList<>(); + List resizes = new ArrayList<>(); for (int i = 0; i < lineViews.size() && i < lineRows.size(); i++) { - final View row = lineRows.get(i); + // A shared row is resized once, with its lead line. + if (i > 0 && lineRows.get(i) == lineRows.get(i - 1)) { + continue; + } + View row = lineRows.get(i); final int oldH = row.getHeight(); if (oldH <= 1 || !(lineViews.get(i) instanceof LyricsLineView view)) { continue; } + TextView tv = lineViews.get(i); + // Syllable readings are drawn by the view under the lyric, outside any text region. + if (!trans && view.showSyllableRomanization && view.romaStart < 0) { + final int extra = view.syllableRomanizationExtraHeight(); + final int targetH = oldH - extra; + if (extra > 1 && targetH > 0) { + resizes.add(new RowResize(i, row, tv, oldH, targetH, 0, extra)); + } + continue; + } final int start = trans ? view.transStart : view.romaStart; final int end = trans ? view.transEnd : view.romaEnd; if (start < 0 || end <= start) { continue; } - final TextView tv = lineViews.get(i); - final Layout layout = tv.getLayout(); + Layout layout = tv.getLayout(); if (layout == null || end > layout.getText().length()) { continue; } @@ -3179,8 +3310,10 @@ private void clearLines() { rowHeightAnimator.cancel(); rowHeightAnimator = null; } - for (View lineRow : lineRows) { - linesContainer.removeView(lineRow); + for (int i = 0; i < lineRows.size(); i++) { + if (i == 0 || lineRows.get(i) != lineRows.get(i - 1)) { + linesContainer.removeView(lineRows.get(i)); + } } lineViews.clear(); lineRows.clear(); @@ -3210,7 +3343,7 @@ private void clearLines() { private void forEachBgLine(int start, Lyrics current, IntConsumer action) { for (int b = 1; start + b < lineViews.size() && start + b < current.lines().size() - && current.lines().get(start + b).isBG(); b++) { + && isSecondaryLineAt(start + b); b++) { action.accept(start + b); } } @@ -3240,7 +3373,9 @@ private void updateHighlight() { LyricsManager manager = LyricsManager.getInstance(); final long pos = manager.getPositionMs(); - final int index = current.indexForPosition(pos, highlightedIndex); + final int index = resolveLeadLineIndex(current.indexForPosition(pos, highlightedIndex)); + updateBgLinesVisibility(pos, current, index); + if (index == highlightedIndex) { markActiveLine(index, current, true); if (!karaokeActive && index >= 0 && index < lineViews.size()) { @@ -3284,11 +3419,13 @@ private void updateHighlight() { applyLineOverlay(index); - markActiveLine(index, current, true); + final boolean withinWindow = isClusterStillActive(current, index, pos); + markActiveLine(index, current, withinWindow); - LyricsViewSupport.fadeTo(lineViews.get(index), 1f); + LyricsViewSupport.fadeTo(lineViews.get(index), withinWindow ? 1f : INACTIVE_LINE_ALPHA); if (!karaokeActive) { - lineViews.get(index).setTextColor(LyricsColors.lineTextColor()); + lineViews.get(index).setTextColor(withinWindow + ? LyricsColors.lineTextColor() : LyricsColors.unsungWordColor()); } forEachBgLine(index, current, i -> { LyricsViewSupport.fadeTo(lineViews.get(i), 1f); @@ -3357,7 +3494,7 @@ private void applySectionDotsState(SectionDotsMotion.State state, int index) { if (tag != null) { tag.setAlpha(state.pillAlpha()); } - final FrameLayout slot = index < sectionDotSlots.size() ? sectionDotSlots.get(index) : null; + FrameLayout slot = index < sectionDotSlots.size() ? sectionDotSlots.get(index) : null; if (slot != null) { // A line with no label gives the dots its space by opening up rather than by handing // over a label, so the space is driven from the same curve the fade is. @@ -3373,7 +3510,7 @@ private void applySectionDotsState(SectionDotsMotion.State state, int index) { return; } dotsRow.setAlpha(state.groupAlpha()); - final float[] dotAlpha = state.dotAlpha(); + float[] dotAlpha = state.dotAlpha(); for (int i = 0; i < dotsRow.getChildCount() && i < dotAlpha.length; i++) { dotsRow.getChildAt(i).setAlpha(dotAlpha[i]); } @@ -3383,15 +3520,15 @@ private void applySectionSlotMargin(int lineIndex, int marginPx) { if (lineIndex < 0 || lineIndex >= lineRows.size()) { return; } - final View row = lineRows.get(lineIndex); + View row = lineRows.get(lineIndex); if (!(row instanceof ViewGroup group) || group.getChildCount() == 0) { return; } - final View slot = group.getChildAt(0); + View slot = group.getChildAt(0); if (!(slot instanceof FrameLayout)) { return; } - final ViewGroup.MarginLayoutParams params = + ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) slot.getLayoutParams(); if (params == null || params.topMargin == marginPx) { return; @@ -3402,7 +3539,7 @@ private void applySectionSlotMargin(int lineIndex, int marginPx) { /** Sets a slot's height, asking for a layout only when it actually moved. */ private void applySectionSlotHeight(FrameLayout slot, int heightPx, int marginPx) { - final ViewGroup.MarginLayoutParams params = + ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) slot.getLayoutParams(); if (params == null || (params.height == heightPx && params.topMargin == marginPx)) { @@ -3505,12 +3642,12 @@ private void deferScrollToPreDraw(int anchor, boolean lineChanged) { if (!isAttachedToWindow()) { return; } - final ViewTreeObserver observer = linesContainer.getViewTreeObserver(); + ViewTreeObserver observer = linesContainer.getViewTreeObserver(); if (pendingAnchorScroll != null) { observer.removeOnPreDrawListener(pendingAnchorScroll); } final int generation = buildGeneration.get(); - final ViewTreeObserver.OnPreDrawListener listener = + ViewTreeObserver.OnPreDrawListener listener = new ViewTreeObserver.OnPreDrawListener() { @Override public boolean onPreDraw() { @@ -3594,7 +3731,8 @@ private void updateOnlyHighlight() { final boolean karaokeActive = Settings.LYRICS_WORD_SYNC.get(); final long pos = LyricsManager.getInstance().getPositionMs(); - final int index = current.indexForPosition(pos, highlightedIndex); + final int index = resolveLeadLineIndex(current.indexForPosition(pos, highlightedIndex)); + updateBgLinesVisibility(pos, current, index); if (index == highlightedIndex) { markActiveLine(index, current, true); applyLineOverlay(index); @@ -3688,7 +3826,8 @@ private void fadeLineAndBg(int line) { return; } LyricsViewSupport.fadeTo(lineViews.get(line), LyricsPanelView.INACTIVE_LINE_ALPHA); - forEachBgLine(line, lyrics, i -> LyricsViewSupport.fadeTo(lineViews.get(i), LyricsPanelView.INACTIVE_LINE_ALPHA)); + forEachBgLine(line, lyrics, i -> + LyricsViewSupport.fadeTo(lineViews.get(i), LyricsPanelView.INACTIVE_LINE_ALPHA)); } private void resetLineForWordSyncOff(int line, boolean activeLine) { @@ -3708,7 +3847,7 @@ private void resetLineForWordSyncOff(int line, boolean activeLine) { private void applyBgWordColors(int parentIndex, long positionMs, boolean allSung) { if (lyrics == null) return; List lines = lyrics.lines(); - for (int i = parentIndex + 1; i < lines.size() && lines.get(i).isBG(); i++) { + for (int i = parentIndex + 1; i < lines.size() && isSecondaryLineAt(i); i++) { applyWordColors(i, positionMs, allSung); } } @@ -3716,7 +3855,7 @@ private void applyBgWordColors(int parentIndex, long positionMs, boolean allSung private void resetBgWordColors(int parentIndex) { if (lyrics == null) return; List lines = lyrics.lines(); - for (int i = parentIndex + 1; i < lines.size() && lines.get(i).isBG(); i++) { + for (int i = parentIndex + 1; i < lines.size() && isSecondaryLineAt(i); i++) { applyWordColors(i, Long.MIN_VALUE, false); } } @@ -3726,32 +3865,6 @@ private void clearWordHighlight(int lineIndex) { resetBgWordColors(lineIndex); } - private int computeOriginalTextStart(LyricsLine line, int index, OnlyMode onlySnap, - @Nullable List romanizedSnap, @Nullable List translatedSnap, - boolean perWordRomajiSnap) { - if (onlySnap != OnlyMode.NONE) { - return 0; - } - String originalTrimmed = line.text().trim(); - // Mirrors the builder's own choice, or the line is measured against a reading - // laid out differently from the one drawn on it. - final String romanization = LyricsSpanBuilder.lineReading( - line, index, romanizedSnap, perWordRomajiSnap); - String translation = null; - if (translatedSnap != null && index < translatedSnap.size()) { - String t = translatedSnap.get(index).trim(); - if (!t.isEmpty() && !t.equalsIgnoreCase(originalTrimmed)) { - translation = t; - } - } - final boolean swap = Settings.LYRICS_SWAP_TRANS_ROMA.get(); - String above = swap ? translation : romanization; - if (above != null) { - return above.length() + 1; - } - return 0; - } - private void applyWordColors(int index, long positionMs, boolean allSung) { if (index < 0 || index >= lineWordSpans.size() || index >= lineViews.size()) { return; @@ -3771,8 +3884,10 @@ private void applyWordColors(int index, long positionMs, boolean allSung) { } if (lineView instanceof LyricsLineView) { + // A line with no word timings, such as a background vocal sung in one go, lights whole. ((LyricsLineView) lineView).setHighlight( - timings, positionMs, allSung, LyricsColors.unsungWordColor(), origStart); + timings, positionMs, allSung || (timings.isEmpty() && positionMs != Long.MIN_VALUE), + LyricsColors.unsungWordColor(), origStart); } } @@ -3824,9 +3939,9 @@ private int targetSectionSlotHeight() { } final float textPx = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, LyricsViewSupport.sectionTagTextSizeSp(), getResources().getDisplayMetrics()); - final Paint metricsPaint = new Paint(); + Paint metricsPaint = new Paint(); metricsPaint.setTextSize(textPx); - final Paint.FontMetrics fontMetrics = metricsPaint.getFontMetrics(); + Paint.FontMetrics fontMetrics = metricsPaint.getFontMetrics(); final int textHeight = Math.round(fontMetrics.descent - fontMetrics.ascent); return Math.max(0, textHeight + Dim.dp4 * 2); } @@ -3875,7 +3990,7 @@ public void applyShowSection() { resetSectionDots(); final boolean showing = visibility == VISIBLE; for (int i = 0; i < sectionTags.size(); i++) { - final TextView tag = sectionTags.get(i); + TextView tag = sectionTags.get(i); if (tag == null) { continue; } @@ -3955,7 +4070,7 @@ public void applyToolbarButtonSettings() { * change would only show on the next track. */ public void applyLyricLayoutSettings() { - final Lyrics current = lyrics; + Lyrics current = lyrics; if (current == null || current.isEmpty()) { return; } @@ -3971,7 +4086,7 @@ public void applyLyricLayoutSettings() { */ @SuppressWarnings("SizeReplaceableByIsEmpty") private void refreshDualPillIcons() { - final TextView pill = dualPillView; + TextView pill = dualPillView; if (!dualPillActive || pill == null || pill.getText().length() > 0) { return; } @@ -4019,7 +4134,7 @@ public void applyTextSize() { for (LinearLayout dotsRow : sectionDots) { LyricsViewSupport.applySectionDotSize(dotsRow); if (dotsRow != null) { - final ViewGroup.LayoutParams params = dotsRow.getLayoutParams(); + ViewGroup.LayoutParams params = dotsRow.getLayoutParams(); if (params instanceof FrameLayout.LayoutParams frameParams) { frameParams.leftMargin = LyricsViewSupport.sectionDotInset(dotsRow.getContext()); dotsRow.setLayoutParams(frameParams); @@ -4031,7 +4146,7 @@ public void applyTextSize() { if (tag == null || tag.getVisibility() != VISIBLE) { continue; } - final TextView measuredTag = tag; + TextView measuredTag = tag; measuredTag.post(() -> { if (measuredTag.getHeight() > 0) { sectionTagHeightPx = measuredTag.getHeight(); @@ -4042,7 +4157,7 @@ public void applyTextSize() { @SuppressWarnings("SizeReplaceableByIsEmpty") public void applyHideInfo() { - final CharSequence info = creditView.getText(); + CharSequence info = creditView.getText(); final boolean visible = !Settings.LYRICS_HIDE_INFO.get() && info != null && info.length() > 0; creditView.setVisibility(visible ? VISIBLE : GONE); @@ -4086,8 +4201,8 @@ private TextView createToolbarButton(LinearLayout parent, boolean enabled, * or margin behind to undo when the mode ends. */ private void mergeButtonsIntoPill() { - final TextView translate = translateView; - final TextView romanize = romanizeView; + TextView translate = translateView; + TextView romanize = romanizeView; if (translate == null || romanize == null || dualPillActive) { return; } @@ -4102,7 +4217,7 @@ private void mergeButtonsIntoPill() { romanize.setScaleY(1f); // Claimed before it is on screen so a tap during the cross-fade is read as the pill's. - final TextView pill = createDualPill(); + TextView pill = createDualPill(); dualPillView = pill; dualPillActive = true; // The slot the translation button holds, which is where the pill ends up: the gap of the @@ -4123,7 +4238,7 @@ private void mergeButtonsIntoPill() { // Added only once the two are gone rather than alongside them: a pill sharing the // row with the buttons it is replacing shifts sideways the moment they stop taking // up space, and no amount of easing on the pill hides that. - final LinearLayout.LayoutParams pillParams = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams pillParams = new LinearLayout.LayoutParams( LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT); pillParams.setMarginStart(Dim.dp12); @@ -4160,7 +4275,7 @@ private Drawable tintedIcon(@Nullable String iconName) { * place that decides between the label and the icons. */ private TextView createDualPill() { - final TextView pill = new TextView(getContext()); + TextView pill = new TextView(getContext()); applyButtonStyle(pill, null); pill.setOnClickListener(view -> exitDualMode(true)); pill.setOnLongClickListener(view -> { @@ -4197,7 +4312,7 @@ private void renderDualPill(TextView pill, boolean labelling) { } private void settleDualPill() { - final TextView pill = dualPillView; + TextView pill = dualPillView; if (!dualPillActive || pill == null) { return; } @@ -4207,9 +4322,9 @@ private void settleDualPill() { private void restoreButtonsFromPill() { - final TextView translate = translateView; - final TextView romanize = romanizeView; - final TextView pill = dualPillView; + TextView translate = translateView; + TextView romanize = romanizeView; + TextView pill = dualPillView; dualPillActive = false; dualPillView = null; dualPillTranslateIcon = null; @@ -4268,7 +4383,7 @@ private void applyButtonStyle(TextView button, @Nullable String iconName) { return; } - final Drawable icon = tintedIcon(iconName); + Drawable icon = tintedIcon(iconName); if (icon == null) { return; } @@ -4283,7 +4398,7 @@ private void applyButtonStyle(TextView button, @Nullable String iconName) { */ @SuppressWarnings("SizeReplaceableByIsEmpty") private void applyButtonAppearance(TextView button, boolean active) { - final CharSequence currentText = button.getText(); + CharSequence currentText = button.getText(); final boolean labelling = currentText != null && currentText.length() > 0; if (button == dualPillView) { // The pill names the mode before it stands for the two, so the icons follow the @@ -4320,4 +4435,211 @@ private void flashButtonLabel(@Nullable TextView button, @Nullable String text, handler.removeCallbacks(reset); handler.postDelayed(reset, durationMs); } + + // Background vocals: each sits in its lead line's row and only opens while it is sung. + + /** A lead line and its background vocals stay lit this much longer, unless the next line comes first. */ + private static final long RETIMING_MAX_EXTENSION_MS = 1300L; + /** Before a silence this long, the line goes out shortly after it ends instead. */ + private static final long LYRICS_GAP_MIN_MS = 5000L; + private static final long LYRICS_GAP_HOLD_MS = 300L; + /** A line lights this far ahead of its start, to meet the voice rather than trail it. */ + private static final long HIGHLIGHT_LOOK_AHEAD_MS = 190L; + /** A background vocal stays open this long after its last word. */ + private static final long BG_LINGER_MS = 250L; + /** How long a line with no end of its own lasts, at most before the next line and otherwise. */ + private static final long UNENDED_LINE_MAX_MS = 4500L; + private static final long UNENDED_LAST_LINE_MS = 5000L; + /** A line sung again this soon after itself is an echo, so a background vocal. */ + private static final long ECHO_MAX_GAP_MS = 3500L; + /** How far from its lead line a background vocal may start and still belong to it. */ + private static final long BG_MAX_LEAD_MS = 2000L; + private static final long BG_MAX_LAG_MS = 30000L; + private static final long BG_MAX_AFTER_LEAD_END_MS = 4000L; + private static final long BG_MAX_AFTER_LEAD_START_MS = 15000L; + + /** + * Whether a line is a background vocal of the lead line above it: one the source marks as such, + * one written whole in brackets, or an echo of the line before it. It has to start close + * enough to its lead to belong to it. + */ + private static boolean isSecondaryLyricLine(int index, List lines) { + if (index <= 0 || index >= lines.size()) { + return false; + } + LyricsLine line = lines.get(index); + boolean candidate = line.isBG() || LyricsBracketSplitter.isBracketedVocal(line.text()); + if (!candidate && !LyricsBracketSplitter.hasTagBrackets(line.text())) { + LyricsLine prev = lines.get(index - 1); + final long gap = line.startTimeMs() - prev.startTimeMs(); + if (gap >= 0 && gap <= ECHO_MAX_GAP_MS) { + String cleanCurr = LRCParser.cleanTextForComparison(line.text()); + candidate = !cleanCurr.isEmpty() + && cleanCurr.equals(LRCParser.cleanTextForComparison(prev.text())); + } + } + if (!candidate) { + return false; + } + + int parentIdx = index - 1; + while (parentIdx >= 0) { + LyricsLine p = lines.get(parentIdx); + if (!p.isBG() && !LyricsBracketSplitter.isBracketedVocal(p.text())) { + break; + } + parentIdx--; + } + if (parentIdx < 0) { + return false; + } + + if (line.isBG() && !LyricsBracketSplitter.hasTagBrackets(line.text())) { + return true; + } + + LyricsLine parentLine = lines.get(parentIdx); + if (line.startTimeMs() != LyricsLine.NO_TIME && parentLine.startTimeMs() != LyricsLine.NO_TIME) { + final long timeDiff = line.startTimeMs() - parentLine.startTimeMs(); + if (timeDiff < -BG_MAX_LEAD_MS || timeDiff > BG_MAX_LAG_MS) { + return false; + } + if (parentLine.endTimeMs() != LyricsLine.NO_TIME + && parentLine.endTimeMs() > parentLine.startTimeMs()) { + return line.startTimeMs() <= Math.max( + parentLine.endTimeMs() + BG_MAX_AFTER_LEAD_END_MS, + parentLine.startTimeMs() + BG_MAX_AFTER_LEAD_START_MS); + } + } + return true; + } + + /** Whether the built line at this index is a background vocal in its lead line's row. */ + private boolean isSecondaryLineAt(int index) { + return index > 0 && index < lineIsBackground.size() + && Boolean.TRUE.equals(lineIsBackground.get(index)); + } + + /** A background vocal is never highlighted on its own: its lead line is. */ + private int resolveLeadLineIndex(int index) { + int res = index; + while (res > 0 && isSecondaryLineAt(res)) { + res--; + } + return res; + } + + private int findParentLeadIndex(int bgIndex) { + if (bgIndex < lineViews.size() + && lineViews.get(bgIndex) instanceof LyricsLineView view + && view.getParentLineIndex() >= 0) { + return view.getParentLineIndex(); + } + return resolveLeadLineIndex(bgIndex); + } + + /** A background vocal sung ahead of its lead line sits above it. */ + private static boolean isBgBeforeMain(List lines, int parentIdx, int bgIdx) { + if (parentIdx < 0 || parentIdx >= lines.size() || bgIdx < 0 || bgIdx >= lines.size()) { + return false; + } + LyricsLine mainLine = lines.get(parentIdx); + LyricsLine bgLine = lines.get(bgIdx); + final long mainStart = !mainLine.words().isEmpty() + ? mainLine.words().get(0).startMs() : mainLine.startTimeMs(); + final long bgStart = !bgLine.words().isEmpty() + ? bgLine.words().get(0).startMs() : bgLine.startTimeMs(); + return mainStart >= 0 && bgStart >= 0 && bgStart < mainStart; + } + + /** When a line is done being sung: its last word, its own end, or a while after it starts. */ + private long computeLineActualEndMs(List lines, int idx) { + LyricsLine line = lines.get(idx); + if (idx < lineWordSpans.size() && !lineWordSpans.get(idx).isEmpty()) { + List spans = lineWordSpans.get(idx); + final long wordEnd = spans.get(spans.size() - 1).endMs(); + if (wordEnd > line.startTimeMs()) { + return wordEnd; + } + } + if (line.hasWords()) { + final long wordEnd = line.words().get(line.words().size() - 1).endMs(); + if (wordEnd > line.startTimeMs()) { + return wordEnd; + } + } + if (line.endTimeMs() > line.startTimeMs()) { + return line.endTimeMs(); + } + for (int j = idx + 1; j < lines.size(); j++) { + if (!isSecondaryLineAt(j) && lines.get(j).startTimeMs() > line.startTimeMs()) { + return Math.min(line.startTimeMs() + UNENDED_LINE_MAX_MS, lines.get(j).startTimeMs()); + } + } + return line.startTimeMs() + UNENDED_LAST_LINE_MS; + } + + /** When a lead line and the background vocals after it go out. */ + private long computeAdjustedClusterEndMs(Lyrics current, int leadIdx) { + List lines = current.lines(); + long clusterBaseEnd = computeLineActualEndMs(lines, leadIdx); + int clusterEndIdx = leadIdx; + while (clusterEndIdx + 1 < lines.size() && isSecondaryLineAt(clusterEndIdx + 1)) { + clusterEndIdx++; + clusterBaseEnd = Math.max(clusterBaseEnd, computeLineActualEndMs(lines, clusterEndIdx)); + } + for (int j = clusterEndIdx + 1; j < lines.size(); j++) { + if (!isSecondaryLineAt(j)) { + final long gap = lines.get(j).startTimeMs() - clusterBaseEnd; + if (gap >= LYRICS_GAP_MIN_MS) { + return clusterBaseEnd + LYRICS_GAP_HOLD_MS; + } + return clusterBaseEnd + Math.max(0L, Math.min(RETIMING_MAX_EXTENSION_MS, gap)); + } + } + return clusterBaseEnd + RETIMING_MAX_EXTENSION_MS; + } + + private boolean isClusterStillActive(Lyrics current, int leadIdx, long posMs) { + if (leadIdx < 0 || leadIdx >= current.lines().size()) { + return false; + } + LyricsLine lead = current.lines().get(leadIdx); + return posMs >= lead.startTimeMs() - HIGHLIGHT_LOOK_AHEAD_MS + && posMs <= computeAdjustedClusterEndMs(current, leadIdx); + } + + /** Open while its lead line is the active one, and while it is sung itself. */ + private boolean shouldBgLineBeVisible(Lyrics current, int activeLeadIdx, int bgIdx, long posMs) { + if (!current.synced()) { + return true; + } + if (activeLeadIdx >= 0 && findParentLeadIndex(bgIdx) == activeLeadIdx) { + return true; + } + if (Settings.LYRICS_WORD_SYNC.get() + && bgIdx < lineWordSpans.size() + && !lineWordSpans.get(bgIdx).isEmpty()) { + List bt = lineWordSpans.get(bgIdx); + if (posMs >= bt.get(0).startMs() - HIGHLIGHT_LOOK_AHEAD_MS + && posMs <= bt.get(bt.size() - 1).endMs() + BG_LINGER_MS) { + return true; + } + } + LyricsLine bgLine = current.lines().get(bgIdx); + return bgLine.startTimeMs() != LyricsLine.NO_TIME && bgLine.endTimeMs() > bgLine.startTimeMs() + && posMs >= bgLine.startTimeMs() - HIGHLIGHT_LOOK_AHEAD_MS + && posMs <= bgLine.endTimeMs() + BG_LINGER_MS; + } + + private void updateBgLinesVisibility(long posMs, Lyrics current, int activeLeadIdx) { + final int count = Math.min(lineViews.size(), current.lines().size()); + for (int i = 0; i < count; i++) { + if (lineViews.get(i) instanceof LyricsLineView view && view.isSecondary()) { + final boolean visible = shouldBgLineBeVisible(current, activeLeadIdx, i, posMs); + view.setActiveLine(visible, LyricsColors.secondaryTextColor()); + view.animateSecondaryExpansion(visible, false); + } + } + } } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsSpanBuilder.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsSpanBuilder.java index 77097319d7..a8a63e0a7e 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsSpanBuilder.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsSpanBuilder.java @@ -22,32 +22,60 @@ import androidx.annotation.Nullable; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Locale; import app.morphe.extension.music.patches.lyrics.LyricsPresentation; +import app.morphe.extension.music.patches.lyrics.derive.LyricsRomanizer; +import app.morphe.extension.music.patches.lyrics.model.LyricsBracketSplitter; import app.morphe.extension.music.patches.lyrics.model.LyricsLine; import app.morphe.extension.music.patches.lyrics.model.LyricsTextFormatter; import app.morphe.extension.music.patches.lyrics.model.Word; +import app.morphe.extension.music.patches.lyrics.romanizers.ArmenianRomanizer; +import app.morphe.extension.music.patches.lyrics.romanizers.CyrillicRomanizer; +import app.morphe.extension.music.patches.lyrics.romanizers.GeorgianRomanizer; +import app.morphe.extension.music.patches.lyrics.romanizers.GreekRomanizer; +import app.morphe.extension.music.patches.lyrics.romanizers.HangulRomanizer; +import app.morphe.extension.music.patches.lyrics.romanizers.KanaRomanizer; import app.morphe.extension.music.patches.lyrics.romanizers.LocalRomanizer; import app.morphe.extension.music.patches.lyrics.romanizers.ScriptDetector; import app.morphe.extension.music.settings.Settings; +import app.morphe.extension.shared.ui.Dim; final class LyricsSpanBuilder { - private static final float TRANSLATION_RELATIVE_SIZE = 0.7f; - private static final float MAIN_LINE_SPACING = 1.3f; + static final float TRANSLATION_RELATIVE_SIZE = 0.7f; + static final float MAIN_LINE_SPACING = 1.22f; -record WordTiming(int start, int end, long startMs, long endMs, - @Nullable String romaji) { + /** + * @param hasTrailingSpace Whether a space follows the word, which a syllable reading keeps so + * the readings of two words do not run together. + */ + record WordTiming(int start, int end, long startMs, long endMs, + @Nullable String romaji, boolean hasTrailingSpace) { + + /** A reading ending in a space stands for the space after its word. */ + WordTiming(int start, int end, long startMs, long endMs, @Nullable String romaji) { + this(start, end, startMs, endMs, romaji, + romaji != null && romaji.endsWith(" ")); + } } + /** + * @param showSyllableRomanization The reading is drawn under each syllable by the view, and + * {@code readingSpans} carry it. + * @param originalStart Where the original starts in {@code text}, which the word + * offsets are measured from. Zero when an only mode drops it. + */ record BuildResult(Spannable text, @Nullable ForegroundColorSpan unsungSpan, int transStart, int transEnd, int romaStart, int romaEnd, - @Nullable List readingSpans, boolean smallRunHoldsWords) { + @Nullable List readingSpans, boolean smallRunHoldsWords, + boolean showSyllableRomanization, int originalStart) { BuildResult(Spannable text, @Nullable ForegroundColorSpan unsungSpan, int transStart, int transEnd, int romaStart, int romaEnd) { - this(text, unsungSpan, transStart, transEnd, romaStart, romaEnd, null, false); + this(text, unsungSpan, transStart, transEnd, romaStart, romaEnd, null); } BuildResult(Spannable text, @Nullable ForegroundColorSpan unsungSpan, @@ -55,6 +83,13 @@ record BuildResult(Spannable text, @Nullable ForegroundColorSpan unsungSpan, @Nullable List readingSpans) { this(text, unsungSpan, transStart, transEnd, romaStart, romaEnd, readingSpans, false); } + + BuildResult(Spannable text, @Nullable ForegroundColorSpan unsungSpan, + int transStart, int transEnd, int romaStart, int romaEnd, + @Nullable List readingSpans, boolean smallRunHoldsWords) { + this(text, unsungSpan, transStart, transEnd, romaStart, romaEnd, + readingSpans, smallRunHoldsWords, false, 0); + } } private record Spacing(int mainClear, int smallClear, int boundaryClear) { @@ -67,33 +102,36 @@ record PresentedLine(BuildResult display, List words, int originalSt * Applies the lyrics text filters to a built line, and moves its spans and word offsets to * where they are in the filtered text. * - * @param words Word offsets relative to originalStart. + * @param words Word offsets relative to the original's start. */ - static PresentedLine present(BuildResult raw, List words, int originalStart) { + static PresentedLine present(BuildResult raw, List words) { + final int originalStart = raw.originalStart(); if (!LyricsPresentation.isFormattingEnabled()) { return new PresentedLine(raw, words, originalStart); } - final LyricsTextFormatter.Result formatted = LyricsPresentation.format(raw.text().toString()); + LyricsTextFormatter.Result formatted = LyricsPresentation.format(raw.text().toString()); if (formatted.text().contentEquals(raw.text())) { return new PresentedLine(raw, words, originalStart); } - final SpannableString text = new SpannableString(formatted.text()); + SpannableString text = new SpannableString(formatted.text()); for (Object span : raw.text().getSpans(0, raw.text().length(), Object.class)) { final int start = formatted.offset(raw.text().getSpanStart(span)); final int end = formatted.offset(raw.text().getSpanEnd(span)); if (end > start) text.setSpan(span, start, end, raw.text().getSpanFlags(span)); } final int base = formatted.offset(originalStart); - final List mapped = new ArrayList<>(words.size()); + List mapped = new ArrayList<>(words.size()); for (WordTiming word : words) { final int start = formatted.offset(word.start() + originalStart) - base; final int end = formatted.offset(word.end() + originalStart) - base; - if (end > start) mapped.add(new WordTiming(start, end, word.startMs(), word.endMs(), word.romaji())); + if (end > start) mapped.add(new WordTiming(start, end, word.startMs(), word.endMs(), + word.romaji(), word.hasTrailingSpace())); } return new PresentedLine(new BuildResult(text, raw.unsungSpan(), formatted.offset(raw.transStart()), formatted.offset(raw.transEnd()), formatted.offset(raw.romaStart()), formatted.offset(raw.romaEnd()), - raw.readingSpans(), raw.smallRunHoldsWords()), mapped, base); + raw.readingSpans(), raw.smallRunHoldsWords(), raw.showSyllableRomanization(), base), + mapped, base); } private static final class UniformLineSpacingSpan implements LineHeightSpan { @@ -123,7 +161,8 @@ public void chooseHeight(CharSequence text, int start, int end, int spanstartv, final boolean nextSmall = isSmallAt(text, end); final int clearance; if (small) { - clearance = nextSmall ? spacing.smallClear() : spacing.boundaryClear(); + clearance = (nextSmall && !endsWithNewline(text, end)) + ? spacing.smallClear() : spacing.boundaryClear(); } else { clearance = nextSmall ? spacing.boundaryClear() : spacing.mainClear(); } @@ -135,6 +174,10 @@ public void chooseHeight(CharSequence text, int start, int end, int spanstartv, } previousEnd = end; } + + private static boolean endsWithNewline(CharSequence text, int end) { + return end > 0 && end <= text.length() && text.charAt(end - 1) == '\n'; + } } private static boolean isSmallAt(CharSequence text, int offset) { @@ -142,7 +185,7 @@ private static boolean isSmallAt(CharSequence text, int offset) { return false; } final int window = Math.min(offset + 1, text.length()); - final RelativeSizeSpan[] spans = spanned.getSpans(offset, window, RelativeSizeSpan.class); + RelativeSizeSpan[] spans = spanned.getSpans(offset, window, RelativeSizeSpan.class); for (RelativeSizeSpan span : spans) { final int spanStart = spanned.getSpanStart(span); final int spanEnd = spanned.getSpanEnd(span); @@ -153,6 +196,72 @@ private static boolean isSmallAt(CharSequence text, int offset) { return false; } + /** A background vocal, whether the source says so or only writes it in brackets. */ + static boolean isSecondaryLine(LyricsLine line) { + return line.isBG() || LyricsBracketSplitter.isBracketedVocal(line.text()); + } + + /** The text a line is drawn with: a background vocal sits on a line of its own, so it loses its brackets. */ + static String displayLineText(LyricsLine line) { + String raw = line.text(); + return isSecondaryLine(line) ? LyricsBracketSplitter.stripAllBrackets(raw).trim() : raw; + } + + /** Characters that take no room, which a reading laid out against the text must not count. */ + static boolean isBidiOrInvisible(char c) { + return switch (c) { + case '\u200B', '\u200C', '\u200D', '\u200E', '\u200F', + '\u202A', '\u202B', '\u202C', '\u202D', '\u202E', + '\u2066', '\u2067', '\u2068', '\u2069', '\uFEFF' -> true; + default -> false; + }; + } + + static String stripBidiControls(@Nullable String s) { + if (s == null || s.isEmpty()) { + return ""; + } + StringBuilder sb = null; + for (int i = 0; i < s.length(); i++) { + final char c = s.charAt(i); + if (isBidiOrInvisible(c)) { + if (sb == null) { + sb = new StringBuilder(s.length()); + sb.append(s, 0, i); + } + } else if (sb != null) { + sb.append(c); + } + } + return sb != null ? sb.toString() : s; + } + + /** Arabic script or combining marks, whose letters join up and break apart under letter spacing. */ + static boolean containsJoiningScript(@Nullable CharSequence text) { + if (text == null || text.length() == 0) { + return false; + } + for (int i = 0; i < text.length(); ) { + final int cp = Character.codePointAt(text, i); + i += Character.charCount(cp); + if ((cp >= 0x0600 && cp <= 0x06FF) + || (cp >= 0x0750 && cp <= 0x077F) + || (cp >= 0x08A0 && cp <= 0x08FF) + || (cp >= 0xFB50 && cp <= 0xFDFF) + || (cp >= 0xFE70 && cp <= 0xFEFF) + || cp == 0x200D || cp == 0x200C) { + return true; + } + final int type = Character.getType(cp); + if (type == Character.NON_SPACING_MARK + || type == Character.COMBINING_SPACING_MARK + || type == Character.ENCLOSING_MARK) { + return true; + } + } + return false; + } + /** * The reading shown for a line, and where each word's own reading sits inside it. * @@ -168,25 +277,35 @@ record Reading(@Nullable String text, @Nullable List spans) { * whole-line reading is spread over. */ static Reading readingFor(LyricsLine line, int index, - @Nullable List romanizedLines, boolean perWordRomaji, - @Nullable List timings) { + @Nullable List romanizedLines, boolean perWordRomaji, + @Nullable List timings) { + String orig = displayLineText(line).trim(); + if (orig.isEmpty() || LyricsRomanizer.isPurelyLatinScript(orig)) { + return new Reading(null, null); + } if (perWordRomaji) { - final List words = readingWords(line, index, romanizedLines); + List words = readingWords(line, index, romanizedLines); // A word shown as written stands in for a reading only where the line is really read per word. if (!words.isEmpty() && anyReading(words)) { - final StringBuilder text = new StringBuilder(); - final List spans = new ArrayList<>(words.size()); - final String original = line.text(); + StringBuilder text = new StringBuilder(); + List spans = new ArrayList<>(words.size()); + String original = displayLineText(line); + // A background vocal is drawn without its brackets, so its readings lose them too. + final boolean secondary = isSecondaryLine(line); int at = 0; String previous = null; for (Word word : words) { - final String reading = word.romaji() != null ? word.romaji() : word.text().trim(); + String reading = word.romaji() != null ? word.romaji() : word.text().trim(); + if (secondary) { + reading = LyricsBracketSplitter.stripAllBrackets(reading); + } + reading = reading.trim(); if (reading.isEmpty()) { continue; } // The line's own spacing tells a word handed over in pieces from one written whole; an // unplaced word keeps the gap. - final String own = word.text().trim(); + String own = (secondary ? LyricsBracketSplitter.stripAllBrackets(word.text()) : word.text()).trim(); final int found = own.isEmpty() ? -1 : original.indexOf(own, at); //noinspection SizeReplaceableByIsEmpty if (text.length() > 0 && !sameWrittenWord(at, found, previous, own)) { @@ -208,23 +327,464 @@ static Reading readingFor(LyricsLine line, int index, return wholeLineReading(line, index, romanizedLines, timings); } + /** + * The line's word timings, each carrying the reading of its own syllable to be drawn under it. + * Words already in Latin letters read as written and carry none. + * + * @return Null when the line has no reading to split up this way. + */ + @Nullable + static List buildSyllableRomanizedTimings( + LyricsLine line, int index, + @Nullable List romanizedLines, + boolean perWordRomaji, + @Nullable List timings, + String mainText) { + if (timings == null || timings.isEmpty() || mainText.isEmpty()) { + return null; + } + if (romanizedLines == null && !perWordRomaji) { + return null; + } + String cleanMain = stripBidiControls(mainText).trim(); + if (cleanMain.isEmpty() || ScriptDetector.containsRtl(cleanMain) + || (ScriptDetector.detect(cleanMain) == ScriptDetector.Script.OTHER + && !ScriptDetector.containsCjk(cleanMain)) + || LyricsRomanizer.isPurelyLatinScript(cleanMain)) { + return null; + } + + String wholeRoman = lineRomanizationAt(line, index, romanizedLines); + if (wholeRoman != null && stripBidiControls(wholeRoman).trim().equalsIgnoreCase(cleanMain)) { + return null; + } + + final int n = timings.size(); + String[] ownWords = new String[n]; + boolean[] isLatinWord = new boolean[n]; + for (int i = 0; i < n; i++) { + WordTiming wt = timings.get(i); + final int s = Math.max(0, Math.min(wt.start(), mainText.length())); + final int e = Math.max(s, Math.min(wt.end(), mainText.length())); + String own = stripBidiControls(mainText.substring(s, e)).trim(); + ownWords[i] = own; + isLatinWord[i] = LyricsRomanizer.isPurelyLatinScript(own); + } + + String[] romTexts = new String[n]; + boolean[] romTrailingSpaces = new boolean[n]; + boolean populated = false; + + // A provider that reads every word itself has already said which reading goes where. + boolean hasNativeTimingRomaji = true; + boolean anyNativeNonEmpty = false; + for (int i = 0; i < n; i++) { + String tr = timings.get(i).romaji(); + if (!isLatinWord[i]) { + if (tr == null || tr.trim().isEmpty() || ScriptDetector.containsCjk(tr)) { + hasNativeTimingRomaji = false; + break; + } + anyNativeNonEmpty = true; + } + } + if (hasNativeTimingRomaji && anyNativeNonEmpty) { + for (int i = 0; i < n; i++) { + if (isLatinWord[i]) { + romTexts[i] = ""; + romTrailingSpaces[i] = false; + } else { + String r = timings.get(i).romaji(); + r = r != null ? stripBidiControls(isSecondaryLine(line) ? LyricsBracketSplitter.stripAllBrackets(r) : r) : ""; + romTrailingSpaces[i] = r.endsWith(" ") || timings.get(i).hasTrailingSpace(); + romTexts[i] = r.trim(); + } + } + populated = true; + } + + // Otherwise the line's whole reading, or one joined up from its words, is cut into pieces that + // best match a reading worked out locally for each syllable. + if (!populated) { + if (wholeRoman == null || wholeRoman.trim().isEmpty()) { + List rWords = readingWords(line, index, romanizedLines); + if (!rWords.isEmpty() && anyReading(rWords)) { + StringBuilder sb = new StringBuilder(); + for (Word rw : rWords) { + String r = rw.romaji(); + if (r != null && !r.trim().isEmpty() && !ScriptDetector.containsCjk(r)) { + if (isSecondaryLine(line)) { + r = LyricsBracketSplitter.stripAllBrackets(r); + } + if (sb.length() > 0 && sb.charAt(sb.length() - 1) != ' ') { + sb.append(' '); + } + sb.append(r.trim()); + if (rw.endsWithSpace() || r.endsWith(" ")) { + sb.append(' '); + } + } + } + String built = sb.toString().trim(); + if (!built.isEmpty()) { + wholeRoman = built; + } + } + } + + List guides = new ArrayList<>(n); + List syllables = new ArrayList<>(n); + for (int i = 0; i < n; i++) { + String own = ownWords[i]; + syllables.add(own); + guides.add(phoneticGuideFor(own, mainText, timings.get(i).end(), isLatinWord[i])); + } + + if (wholeRoman != null && !wholeRoman.trim().isEmpty()) { + List aligned = alignPhoneticRomaji( + stripBidiControls(wholeRoman).trim(), guides, syllables, isLatinWord); + for (int i = 0; i < n; i++) { + if (isLatinWord[i]) { + romTexts[i] = ""; + romTrailingSpaces[i] = false; + continue; + } + String chunk = i < aligned.size() && aligned.get(i) != null ? aligned.get(i) : ""; + final boolean chunkTrailingSpace = chunk.endsWith(" "); + chunk = chunk.trim(); + if (chunk.isEmpty() || ScriptDetector.containsCjk(chunk) + || chunk.equalsIgnoreCase(ownWords[i])) { + chunk = guides.get(i); + } + // A Korean word written without a space reads as one word too: + // "yeogijeogi seo" becomes "yeogijeogiseo". + if (!ownWords[i].contains(" ") && chunk.contains(" ") + && ScriptDetector.contains(ownWords[i], Character.UnicodeScript.HANGUL)) { + chunk = chunk.replace(" ", ""); + } + romTexts[i] = chunk; + romTrailingSpaces[i] = chunkTrailingSpace; + } + populated = true; + } else { + // No reading to cut up, so the local one is all there is. + boolean anyGuide = false; + for (int i = 0; i < n; i++) { + if (isLatinWord[i]) { + romTexts[i] = ""; + } else { + String g = guides.get(i); + romTexts[i] = (g != null && !g.equalsIgnoreCase(ownWords[i]) && !ScriptDetector.containsCjk(g)) + ? g.trim() : ""; + if (!romTexts[i].isEmpty()) { + anyGuide = true; + } + } + } + populated = anyGuide; + } + } + + if (!populated) { + return null; + } + + boolean anyRoman = false; + List out = new ArrayList<>(n); + for (int i = 0; i < n; i++) { + WordTiming wt = timings.get(i); + final int s = Math.max(0, Math.min(wt.start(), mainText.length())); + final int e = Math.max(s, Math.min(wt.end(), mainText.length())); + String own = ownWords[i]; + + String r = (isLatinWord[i] || romTexts[i] == null) ? "" : romTexts[i].trim(); + if (!r.isEmpty() && (r.equalsIgnoreCase(own) || ScriptDetector.containsCjk(r))) { + r = ""; + } + + final boolean mainHasTrailingSpace = wt.hasTrailingSpace() + || (e < mainText.length() && Character.isWhitespace(mainText.charAt(e))) + || (e > s && Character.isWhitespace(mainText.charAt(e - 1))) + || (i + 1 < n && timings.get(i + 1).start() > e + && mainText.substring(e, Math.min(mainText.length(), timings.get(i + 1).start())).indexOf(' ') >= 0); + + final boolean trailSpace = !r.isEmpty() && (mainHasTrailingSpace || romTrailingSpaces[i]); + if (!r.isEmpty()) { + anyRoman = true; + r = r + (trailSpace ? " " : ""); + } + out.add(new WordTiming(wt.start(), wt.end(), wt.startMs(), wt.endMs(), r, trailSpace)); + } + + return anyRoman ? out : null; + } + + /** + * A local reading of one syllable, so the alignment compares Latin letters with Latin letters + * and never with the script being read. + */ + private static String phoneticGuideFor( + String ownWord, + String fullLineText, + int wordEndOffset, + boolean isLatin) { + if (ownWord == null || ownWord.isEmpty()) { + return ""; + } + if (isLatin) { + return ownWord; + } + if (ScriptDetector.contains(ownWord, Character.UnicodeScript.HANGUL)) { + // A lone Hangul syllable is read with the one after it, which can change how it ends. + if (ownWord.length() == 1 && wordEndOffset < fullLineText.length()) { + final char nextC = fullLineText.charAt(wordEndOffset); + if (nextC >= 0xAC00 && nextC <= 0xD7A3) { + String twoChar = HangulRomanizer.romanize(ownWord + nextC); + String nextOnly = HangulRomanizer.romanize(String.valueOf(nextC)); + if (twoChar.length() > nextOnly.length() && twoChar.endsWith(nextOnly)) { + return twoChar.substring(0, twoChar.length() - nextOnly.length()).replace("-", ""); + } + } + } + return HangulRomanizer.romanize(ownWord).replace("-", "").trim(); + } + if (ScriptDetector.containsCjk(ownWord)) { + LocalRomanizer.Result local = LocalRomanizer.romanize(Collections.singletonList(ownWord)); + if (local != null && !local.lines.isEmpty()) { + String r = local.lines.get(0); + if (r != null && !r.isEmpty() && !ScriptDetector.containsCjk(r)) { + return r.trim(); + } + } + if (ScriptDetector.containsKana(ownWord)) { + return KanaRomanizer.romanize(ownWord).trim(); + } + return ""; + } + ScriptDetector.Script script = ScriptDetector.detect(ownWord); + if (script != null) { + return switch (script) { + case CYRILLIC -> CyrillicRomanizer.romanize(ownWord).trim(); + case GREEK -> GreekRomanizer.romanize(ownWord).trim(); + case GEORGIAN -> GeorgianRomanizer.romanize(ownWord).trim(); + case ARMENIAN -> ArmenianRomanizer.romanize(ownWord).trim(); + default -> ""; + }; + } + return ""; + } + + /** + * Cuts a whole-line reading into one piece per syllable, choosing the cuts that make each piece + * closest to that syllable's local reading. + */ + private static List alignPhoneticRomaji( + String fullRoman, + List guides, + List syllables, + boolean[] isLatinWord) { + final int N = fullRoman.length(); + final int M = guides.size(); + if (M == 0) { + return Collections.emptyList(); + } + if (N == 0) { + return new ArrayList<>(Collections.nCopies(M, "")); + } + + boolean anyGuide = false; + for (String g : guides) { + if (g != null && !g.trim().isEmpty()) { + anyGuide = true; + break; + } + } + if (!anyGuide) { + String[] split = fullRoman.trim().split("\\s+"); + List chunks = new ArrayList<>(M); + for (int i = 0; i < M; i++) { + final int tokStart = (i * split.length) / M; + final int tokEnd = (i == M - 1) ? split.length : (((i + 1) * split.length) / M); + StringBuilder sb = new StringBuilder(); + for (int k = tokStart; k < tokEnd && k < split.length; k++) { + //noinspection SizeReplaceableByIsEmpty + if (sb.length() > 0) sb.append(' '); + sb.append(split[k]); + } + //noinspection SizeReplaceableByIsEmpty + if (i < M - 1 && sb.length() > 0) sb.append(' '); + chunks.add(sb.toString()); + } + return chunks; + } + + double[][] dp = new double[M + 1][N + 1]; + int[][] path = new int[M + 1][N + 1]; + for (int i = 0; i <= M; i++) { + Arrays.fill(dp[i], Double.MAX_VALUE); + Arrays.fill(path[i], -1); + } + dp[0][0] = 0.0; + + for (int i = 1; i <= M; i++) { + String guideRom = guides.get(i - 1) != null ? guides.get(i - 1) : ""; + final boolean isLatin = isLatinWord[i - 1]; + String origSyl = syllables.get(i - 1); + final int guideLen = Math.max(1, guideRom.length()); + + final int minLen = isLatin ? Math.max(1, guideLen - 1) : 1; + final int maxLen = isLatin + ? guideLen + 4 + : Math.max(guideLen * 3 + 5, Math.max(origSyl.length() * 5, 16)); + + for (int j = 1; j <= N; j++) { + double bestCost = Double.MAX_VALUE; + int bestPrevJ = -1; + + for (int len = minLen; len <= maxLen; len++) { + final int k = j - len; + if (k < 0) { + break; + } + if (dp[i - 1][k] >= Double.MAX_VALUE * 0.5) { + continue; + } + String candidate = fullRoman.substring(k, j); + if (candidate.charAt(0) == ' ' && candidate.trim().length() > 0) { + continue; + } + final double cost = dp[i - 1][k] + phoneticMatchCost(candidate, guideRom, isLatin); + if (cost < bestCost) { + bestCost = cost; + bestPrevJ = k; + } + } + dp[i][j] = bestCost; + path[i][j] = bestPrevJ; + } + } + + List results = new ArrayList<>(Collections.nCopies(M, "")); + int currJ = N; + if (dp[M][N] >= Double.MAX_VALUE * 0.5) { + double minEndCost = Double.MAX_VALUE; + for (int k = N; k >= 0; k--) { + if (dp[M][k] < minEndCost) { + minEndCost = dp[M][k]; + currJ = k; + } + } + } + + for (int i = M; i > 0; i--) { + final int prevJ = path[i][currJ]; + if (prevJ < 0) { + results.set(i - 1, ""); + } else { + results.set(i - 1, fullRoman.substring(prevJ, currJ)); + currJ = prevJ; + } + } + + if (currJ > 0 && !results.isEmpty()) { + results.set(0, fullRoman.substring(0, currJ) + results.get(0)); + } + int totalLen = 0; + for (String r : results) { + totalLen += r.length(); + } + if (totalLen < N && !results.isEmpty()) { + final int safeStart = Math.min(N, Math.max(0, totalLen)); + results.set(M - 1, results.get(M - 1) + fullRoman.substring(safeStart)); + } + return results; + } + + private static double phoneticMatchCost(String candidate, String guide, boolean isLatin) { + String cTrim = candidate.trim(); + String gTrim = guide.trim(); + if (cTrim.isEmpty() && !gTrim.isEmpty()) { + return 50.0; + } + if (isLatin) { + final int dist = levenshtein(cTrim.toLowerCase(Locale.ROOT), gTrim.toLowerCase(Locale.ROOT)); + return dist * 50.0 + Math.abs(cTrim.length() - gTrim.length()) * 0.5; + } + if (gTrim.isEmpty()) { + return cTrim.length() * 1.2 + (candidate.endsWith(" ") ? -0.3 : 0.0); + } + String cCompact = compactPhonetic(cTrim, false); + String gCompact = compactPhonetic(gTrim, false); + final int rawDist = levenshtein(cCompact, gCompact); + final int phonDist = levenshtein(compactPhonetic(cTrim, true), compactPhonetic(gTrim, true)); + final double dist = Math.min((double) rawDist, phonDist + 0.25); + final int lenDiff = Math.abs(cCompact.length() - gCompact.length()); + final double spaceBonus = candidate.endsWith(" ") ? -0.35 : 0.0; + return dist * 2.0 + lenDiff * 0.8 + spaceBonus; + } + + private static String compactPhonetic(String s, boolean foldConsonants) { + StringBuilder sb = new StringBuilder(s.length()); + for (int i = 0; i < s.length(); i++) { + char c = Character.toLowerCase(s.charAt(i)); + if (c == ' ' || c == '-' || c == '\'') { + continue; + } + if (foldConsonants) { + c = switch (c) { + case 'r' -> 'l'; + case 'p' -> 'b'; + case 'k' -> 'g'; + case 't' -> 'd'; + default -> c; + }; + } + sb.append(c); + } + return sb.toString(); + } + + private static int levenshtein(String s1, String s2) { + final int len1 = s1.length(); + final int len2 = s2.length(); + if (len1 == 0) return len2; + if (len2 == 0) return len1; + int[] prev = new int[len2 + 1]; + int[] curr = new int[len2 + 1]; + for (int j = 0; j <= len2; j++) { + prev[j] = j; + } + for (int i = 1; i <= len1; i++) { + curr[0] = i; + final char c1 = s1.charAt(i - 1); + for (int j = 1; j <= len2; j++) { + final int cost = (c1 == s2.charAt(j - 1)) ? 0 : 1; + curr[j] = Math.min(Math.min(curr[j - 1] + 1, prev[j] + 1), prev[j - 1] + cost); + } + int[] tmp = prev; + prev = curr; + curr = tmp; + } + return prev[len2]; + } + /** * The stored string stays the one drawn, so the words are measured against it and not * against the original. */ private static Reading wholeLineReading(LyricsLine line, int index, @Nullable List romanizedLines, @Nullable List timings) { - final String whole = lineRomanizationAt(line, index, romanizedLines); + String whole = lineRomanizationAt(line, index, romanizedLines); if (whole == null || timings == null || timings.isEmpty()) { return new Reading(whole, null); } - return new Reading(whole, spreadOverWords(whole, line.text(), timings)); + return new Reading(whole, spreadOverWords(whole, displayLineText(line), timings)); } private static boolean anyReading(List words) { for (Word word : words) { - final String romaji = word.romaji(); - if (romaji != null && !romaji.isEmpty()) { + String romaji = word.romaji(); + if (romaji != null && !romaji.trim().isEmpty()) { return true; } } @@ -238,12 +798,14 @@ private static boolean anyReading(List words) { */ private static boolean sameWrittenWord(int from, int start, @Nullable String previous, String current) { - if (previous == null || start != from) return false; + if (previous == null || previous.isEmpty() || current.isEmpty() || start != from) { + return false; + } return !spellsSyllable(previous) && !spellsSyllable(current); } private static boolean spellsSyllable(String piece) { - return isCjkAt(piece, 0) || isCjkAt(piece, piece.length() - 1); + return !piece.isEmpty() && (isCjkAt(piece, 0) || isCjkAt(piece, piece.length() - 1)); } private static boolean isCjkAt(String text, int index) { @@ -260,7 +822,7 @@ private static boolean isCjkAt(String text, int index) { private static List readingWords(LyricsLine line, int index, @Nullable List romanizedLines) { if (romanizedLines != null && index < romanizedLines.size()) { - final List words = romanizedLines.get(index).words(); + List words = romanizedLines.get(index).words(); if (!words.isEmpty()) { return words; } @@ -269,25 +831,37 @@ private static List readingWords(LyricsLine line, int index, } private static List closeReadingGaps(List spans, LyricsLine line) { - final List words = line.words(); - final List closed = new ArrayList<>(spans.size()); + List words = line.words(); + List closed = new ArrayList<>(spans.size()); for (int i = 0; i < spans.size(); i++) { - final WordTiming span = spans.get(i); + WordTiming span = spans.get(i); long end = i + 1 < spans.size() ? spans.get(i + 1).startMs() : -1L; if (end < 0 && !words.isEmpty()) { end = words.get(words.size() - 1).endMs(); } closed.add(end > span.endMs() - ? new WordTiming(span.start(), span.end(), span.startMs(), end, span.romaji()) + ? new WordTiming(span.start(), span.end(), span.startMs(), end, + span.romaji(), span.hasTrailingSpace()) : span); } return closed; } + /** + * A one-word line is swept by character across its word's span, but only when it has one: a + * line timed only as a whole lights in one go like every other such line in the song. + */ + private static boolean hasTimedWord(LyricsLine line) { + List words = line.words(); + return words.size() > 1 + || (words.size() == 1 && words.get(0).endMs() > words.get(0).startMs()); + } + static List computeWordTimings(List lines, int index, boolean splitWordlessLine) { - final LyricsLine line = lines.get(index); - if (countWords(line.text()) == 1) { + LyricsLine line = lines.get(index); + String lineText = displayLineText(line); + if (countWords(lineText) == 1 && hasTimedWord(line)) { long rangeStart = line.startTimeMs(); if (rangeStart < 0 && line.hasWords()) { rangeStart = line.words().get(0).startMs(); @@ -302,8 +876,8 @@ static List computeWordTimings(List lines, int index, rangeEnd = line.words().get(line.words().size() - 1).endMs(); } if (rangeStart >= 0 && rangeEnd > rangeStart) { - final List synthesized = - synthesizeLineChars(line.text(), rangeStart, rangeEnd); + List synthesized = + synthesizeLineChars(lineText, rangeStart, rangeEnd); if (!synthesized.isEmpty()) { return synthesized; } @@ -317,8 +891,8 @@ static List computeWordTimings(List lines, int index, : index + 1 < lines.size() ? lines.get(index + 1).startTimeMs() : LyricsLine.NO_TIME; if (lineStart >= 0 && lineEnd > lineStart) { - final List synthesized = - synthesizeLineWords(line.text(), lineStart, lineEnd); + List synthesized = + synthesizeLineWords(lineText, lineStart, lineEnd); if (!synthesized.isEmpty()) { return synthesized; } @@ -335,8 +909,8 @@ static List computeWordTimings(List lines, int index, * spell, so they are taken a run at a time. */ private static List synthesizeLineWords(String text, long startMs, long endMs) { - final int[] bounds = wordBoundaries(text); - final List timings = new ArrayList<>(); + int[] bounds = wordBoundaries(text); + List timings = new ArrayList<>(); // A run and the character after it share a boundary, and the gaps between words are // spelled by the same list, so only the spans that hold text are words. int words = 0; @@ -356,8 +930,10 @@ private static List synthesizeLineWords(String text, long startMs, l } final long wordStart = startMs + span * seen / words; final long wordEnd = startMs + span * (seen + 1) / words; + final boolean trailSpace = bounds[i + 1] < text.length() + && Character.isWhitespace(text.charAt(bounds[i + 1])); timings.add(new WordTiming(bounds[i], bounds[i + 1], wordStart, - Math.max(wordEnd, wordStart + 1), null)); + Math.max(wordEnd, wordStart + 1), null, trailSpace)); seen++; } return timings; @@ -378,7 +954,7 @@ private static boolean isWord(String text, int start, int end) { /** Where each word of a line begins and ends, counted the way it is said. */ private static int[] wordBoundaries(String text) { - final List bounds = new ArrayList<>(); + List bounds = new ArrayList<>(); int runStart = -1; for (int i = 0; i < text.length(); ) { final int codePoint = text.codePointAt(i); @@ -406,7 +982,7 @@ private static int[] wordBoundaries(String text) { bounds.add(runStart); bounds.add(text.length()); } - final int[] result = new int[bounds.size()]; + int[] result = new int[bounds.size()]; for (int i = 0; i < bounds.size(); i++) { result[i] = bounds.get(i); } @@ -414,12 +990,12 @@ private static int[] wordBoundaries(String text) { } private static List synthesizeLineChars(String text, long startMs, long endMs) { - final int[] boundaries = clusterBoundaries(text); + int[] boundaries = clusterBoundaries(text); final int clusterCount = boundaries.length - 1; if (clusterCount < 2 || endMs <= startMs) { return Collections.emptyList(); } - final List timings = new ArrayList<>(clusterCount); + List timings = new ArrayList<>(clusterCount); for (int i = 0; i < clusterCount; i++) { final long clusterStart = startMs + (endMs - startMs) * i / clusterCount; final long clusterEnd = i + 1 == clusterCount @@ -431,9 +1007,9 @@ private static List synthesizeLineChars(String text, long startMs, l } private static int[] clusterBoundaries(String text) { - final BreakIterator iterator = BreakIterator.getCharacterInstance(); + BreakIterator iterator = BreakIterator.getCharacterInstance(); iterator.setText(text); - final List bounds = new ArrayList<>(); + List bounds = new ArrayList<>(); bounds.add(0); for (int boundary = iterator.first(); boundary != BreakIterator.DONE; @@ -442,7 +1018,7 @@ private static int[] clusterBoundaries(String text) { bounds.add(boundary); } } - final int[] result = new int[bounds.size()]; + int[] result = new int[bounds.size()]; for (int i = 0; i < bounds.size(); i++) { result[i] = bounds.get(i); } @@ -475,7 +1051,7 @@ private static int countWords(String text) { return words + (inWord ? 1 : 0); } - private static boolean isCjkCodePoint(int codePoint) { + static boolean isCjkCodePoint(int codePoint) { return (codePoint >= 0x3400 && codePoint <= 0x4DBF) || (codePoint >= 0x4E00 && codePoint <= 0x9FFF) || (codePoint >= 0xF900 && codePoint <= 0xFAFF) @@ -484,85 +1060,228 @@ private static boolean isCjkCodePoint(int codePoint) { || (codePoint >= 0x31F0 && codePoint <= 0x31FF); } + private static int indexOfIgnoreCase(String haystack, String needle, int fromIndex) { + final int max = haystack.length() - needle.length(); + final int nLen = needle.length(); + for (int i = Math.max(0, fromIndex); i <= max; i++) { + if (haystack.regionMatches(true, i, needle, 0, nLen)) { + return i; + } + } + return -1; + } + + /** The line's own word timings, with any text they leave out lit along with its neighbours. */ private static List computeWordTimingsFromWords(LyricsLine line) { if (!line.hasWordTimings()) { return Collections.emptyList(); } - List timings = new ArrayList<>(line.words().size()); - String text = line.text(); - int textLength = text.length(); + List timings = new ArrayList<>(line.words().size() + 2); + String text = displayLineText(line); + final int textLength = text.length(); + if (textLength == 0) { + return Collections.emptyList(); + } + + // The drawn text of a background vocal has lost its brackets, so its words lose them too. + final boolean secondary = isSecondaryLine(line); int offset = 0; for (Word word : line.words()) { - String wordText = word.text(); - int wordLength = wordText.length(); - if (wordLength == 0) { + String rawWordText = secondary + ? LyricsBracketSplitter.stripAllBrackets(word.text()) : word.text(); + String trimmed = rawWordText.trim(); + if (trimmed.isEmpty()) { continue; } - int start = text.indexOf(wordText, offset); - int len = wordLength; - if (start < 0) { - String trimmed = wordText.trim(); - if (!trimmed.isEmpty()) { - start = text.indexOf(trimmed, offset); - len = trimmed.length(); + final boolean wordTrailingSpace = word.endsWithSpace() || rawWordText.endsWith(" "); + String rawRom = word.romaji(); + if (rawRom != null && secondary) { + rawRom = LyricsBracketSplitter.stripAllBrackets(rawRom); + } + + // A timing covering several words is shared out between them by length. + if (trimmed.indexOf(' ') >= 0) { + String[] parts = trimmed.split("\\s+"); + String[] romParts = (rawRom != null && !rawRom.trim().isEmpty()) + ? rawRom.trim().split("\\s+") : null; + int totalChars = 0; + for (String p : parts) { + totalChars += p.length(); } + totalChars = Math.max(1, totalChars); + final long totalDur = Math.max(1L, word.endMs() - word.startMs()); + long curStartMs = word.startMs(); + + for (int pi = 0; pi < parts.length; pi++) { + String part = parts[pi]; + if (part.isEmpty()) { + continue; + } + final boolean isLast = (pi == parts.length - 1); + final long partDur = (totalDur * part.length()) / totalChars; + final long partEndMs = isLast + ? Math.max(curStartMs + 1L, word.endMs()) + : curStartMs + Math.max(1L, partDur); + String partRom = (romParts != null && romParts.length == parts.length) + ? romParts[pi] : null; + offset = appendMatchedWordTiming( + timings, text, textLength, offset, part, + curStartMs, partEndMs, partRom, !isLast || wordTrailingSpace); + curStartMs = partEndMs; + } + } else { + offset = appendMatchedWordTiming( + timings, text, textLength, offset, trimmed, + word.startMs(), word.endMs(), rawRom, wordTrailingSpace); } - if (start < 0) { - // Unmatched word: advance past it so following words stay aligned, - // rather than emitting a span that falls outside the line text. - offset = Math.min(offset + len, textLength); - continue; + } + + // Text after the last timed word, such as closing punctuation, is lit after it. + if (offset < textLength && !timings.isEmpty()) { + fillUntimedGap(timings, text, textLength, offset, textLength, + timings.get(timings.size() - 1).endMs(), + Math.max(line.endTimeMs(), timings.get(timings.size() - 1).endMs() + 200L)); + } + + return timings; + } + + private static int appendMatchedWordTiming( + List timings, + String text, + int textLength, + int offset, + String token, + long startMs, + long endMs, + @Nullable String rom, + boolean wordTrailingSpace) { + int start = text.indexOf(token, offset); + if (start < 0) { + start = indexOfIgnoreCase(text, token, offset); + } + if (start < 0) { + // Left where it was, so the words after an unmatched one can still be found. + return offset; + } + if (start > offset) { + final long gapStartMs = timings.isEmpty() + ? Math.max(0L, startMs - 150L) + : timings.get(timings.size() - 1).endMs(); + fillUntimedGap(timings, text, textLength, offset, start, gapStartMs, startMs); + } + final int end = Math.min(start + token.length(), textLength); + if (start >= end) { + return offset; + } + final boolean trailSpace = wordTrailingSpace + || (end < textLength && Character.isWhitespace(text.charAt(end))); + timings.add(new WordTiming(start, end, startMs, Math.max(endMs, startMs + 1L), rom, trailSpace)); + return end; + } + + private static void fillUntimedGap( + List timings, + String text, + int textLength, + int gapStart, + int gapEnd, + long startMs, + long endMs) { + String gap = text.substring(gapStart, gapEnd); + if (gap.trim().isEmpty()) { + if (!timings.isEmpty() && gap.indexOf(' ') >= 0) { + final int lastIdx = timings.size() - 1; + WordTiming prev = timings.get(lastIdx); + if (!prev.hasTrailingSpace()) { + timings.set(lastIdx, new WordTiming( + prev.start(), prev.end(), prev.startMs(), prev.endMs(), prev.romaji(), true)); + } } - int end = Math.min(start + len, textLength); - if (start >= end) { - continue; + return; + } + + // Collect non-whitespace tokens inside the untimed gap + List spans = new ArrayList<>(2); + int i = gapStart; + while (i < gapEnd) { + while (i < gapEnd && Character.isWhitespace(text.charAt(i))) { + i++; + } + final int s = i; + while (i < gapEnd && !Character.isWhitespace(text.charAt(i))) { + i++; + } + if (i > s) { + spans.add(new int[] {s, i}); } - timings.add(new WordTiming(start, end, word.startMs(), word.endMs(), word.romaji())); - offset = end; } - return timings; + if (spans.isEmpty()) { + return; + } + + final long safeStart = Math.max(0L, startMs); + final long safeEnd = Math.max(safeStart + spans.size(), endMs); + final long dur = safeEnd - safeStart; + for (int idx = 0; idx < spans.size(); idx++) { + int[] sp = spans.get(idx); + String tok = text.substring(sp[0], sp[1]); + // Attach pure punctuation to the preceding WordTiming if adjacent + if (!timings.isEmpty() && isPurePunctuation(tok)) { + final int lastIdx = timings.size() - 1; + WordTiming prev = timings.get(lastIdx); + if (prev.end() == sp[0]) { + final boolean trail = sp[1] < textLength && Character.isWhitespace(text.charAt(sp[1])); + timings.set(lastIdx, new WordTiming( + prev.start(), sp[1], prev.startMs(), prev.endMs(), prev.romaji(), trail)); + continue; + } + } + final long wStart = safeStart + (dur * idx) / spans.size(); + final long wEnd = safeStart + (dur * (idx + 1)) / spans.size(); + final boolean trail = sp[1] < textLength && Character.isWhitespace(text.charAt(sp[1])); + timings.add(new WordTiming(sp[0], sp[1], wStart, Math.max(wEnd, wStart + 1L), null, trail)); + } + } + + private static boolean isPurePunctuation(String s) { + for (int i = 0; i < s.length(); i++) { + if (Character.isLetterOrDigit(s.charAt(i))) { + return false; + } + } + return true; } /** - * Builds the displayed text for a line, appending the translation (when shown) in a - * smaller, dimmer style and coloring each word sung or unsung for the karaoke - * highlight. + * Builds the displayed text for a line, adding the translation and reading (when shown) in a + * smaller, dimmer style and coloring each word sung or unsung for the karaoke highlight. * *

    A fresh {@link SpannableString} is returned on every call so that * {@link android.widget.TextView#setText(CharSequence)} performs a full re-layout * and repaint. Mutating an existing Spannable in place was not reliably redrawn by * this TextView, which left the highlight invisible. - * */ static BuildResult buildLineText(LyricsLine line, List timings, int index, boolean perWordRomaji, List romanizedLines, List translatedLines, LyricsPanelView.OnlyMode onlyMode, Paint basePaint, boolean isRightAligned) { - final Paint.FontMetricsInt baseFm = new Paint.FontMetricsInt(); + Paint.FontMetricsInt baseFm = new Paint.FontMetricsInt(); basePaint.getFontMetricsInt(baseFm); - String original = line.text(); - - // A reading drawn over each word sits where the word's own highlight is painted, so the - // two land on top of each other. It goes on a line of its own instead, and takes its - // progress from the same words rather than from a second set of marks. - final Reading reading = readingFor(line, index, romanizedLines, perWordRomaji, timings); + String original = displayLineText(line); final int lineHeight = baseFm.descent - baseFm.ascent; - final int smallHeight = (int) (lineHeight * TRANSLATION_RELATIVE_SIZE + 0.5f); - final int smallAscent = (int) (-baseFm.ascent * TRANSLATION_RELATIVE_SIZE + 0.5f); - final int mainClear = (int) (lineHeight * MAIN_LINE_SPACING + 0.5f) - lineHeight; - final int smallClear = 0; - final int boundaryClear = - (int) (smallHeight * MAIN_LINE_SPACING + 0.5f) - baseFm.descent - smallAscent; - final Spacing spacing = new Spacing(mainClear, smallClear, boundaryClear); + final int mainClear = Math.max(0, (int) (lineHeight * MAIN_LINE_SPACING + 0.5f) - lineHeight); + final int smallClear = Math.max(0, (int) (lineHeight * TRANSLATION_RELATIVE_SIZE * 0.18f + 0.5f)); + final int boundaryClear = Math.max(Dim.dp4, mainClear + Dim.dp(2)); + Spacing spacing = new Spacing(mainClear, smallClear, boundaryClear); - String romanization = reading.text(); String translation = translationAt(line, index, translatedLines); // An only mode never drops a line: what the provider left untranslated keeps the // original rather than leaving a hole in the song. if (onlyMode == LyricsPanelView.OnlyMode.TRANS) { - final String content = translation != null ? translation : original; + String content = translation != null ? translation : original; SpannableString text = new SpannableString(content); final int transStart = content.isEmpty() ? -1 : 0; final int transEnd = content.isEmpty() ? -1 : content.length(); @@ -575,9 +1294,9 @@ static BuildResult buildLineText(LyricsLine line, List timings, int } if (onlyMode == LyricsPanelView.OnlyMode.ROMA) { - final Romanized roma = romanizedWithTimings(line, index, romanizedLines, timings); + Romanized roma = romanizedWithTimings(line, index, romanizedLines, timings); final boolean readsNothing = roma.text() == null; - final String content = readsNothing ? original : roma.text(); + String content = readsNothing ? original : roma.text(); SpannableString text = new SpannableString(content); attachUniformSpacing(text, spacing); if (content.isEmpty()) { @@ -586,7 +1305,7 @@ static BuildResult buildLineText(LyricsLine line, List timings, int text.setSpan(new AlignmentSpan.Standard(resolveAlignment(isRtlText(content), isRightAligned)), 0, text.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); // No bounds reported paints the whole line the lyric colour, and none of the reading's fade. - final List spans = readsNothing ? timings : roma.spans(); + List spans = readsNothing ? timings : roma.spans(); return new BuildResult(text, null, -1, -1, -1, -1, spans); } @@ -594,11 +1313,10 @@ static BuildResult buildLineText(LyricsLine line, List timings, int // the main slot. Decided per line, so a line left untranslated keeps its translation // rather than going thin. if (onlyMode == LyricsPanelView.OnlyMode.BOTH) { - final DualLine mainAndSmall = + DualLine mainAndSmall = dualLineFor(line, timings, index, translation, original, romanizedLines); SpannableString dual = new SpannableString(mainAndSmall.text()); attachUniformSpacing(dual, spacing); - final boolean mainIsRoma = mainAndSmall.mainIsRoma(); final int smallStart = mainAndSmall.smallStart(); final int smallEnd = mainAndSmall.smallEnd(); // The small run is the side that is not the main one, and the reading's when it landed there. @@ -619,7 +1337,7 @@ static BuildResult buildLineText(LyricsLine line, List timings, int // colour, which is what tells it apart: the lyric's colour would leave the two lines // looking alike. A reading that never arrived leaves the words on the lyric itself, // and a lyric in its own colour is what makes them light up. - final List dualSpans = mainAndSmall.wordSpans(); + List dualSpans = mainAndSmall.wordSpans(); return new BuildResult(dual, null, smallIsRoma ? -1 : smallStart, smallIsRoma ? -1 : smallEnd, @@ -627,57 +1345,62 @@ static BuildResult buildLineText(LyricsLine line, List timings, int dualSpans, mainAndSmall.wordsOnSmall()); } + // A reading drawn over each word sits where the word's own highlight is painted, so the + // two land on top of each other. With word sync on, the view draws each syllable's reading + // under it instead; otherwise the reading goes on a line of its own, and takes its progress + // from the same words rather than from a second set of marks. + List syllableRomanTimings = Settings.LYRICS_WORD_SYNC.get() + ? buildSyllableRomanizedTimings(line, index, romanizedLines, perWordRomaji, timings, original) + : null; + final boolean showSyllableRomanization = syllableRomanTimings != null; + Reading reading = showSyllableRomanization + ? new Reading(null, syllableRomanTimings) + : readingFor(line, index, romanizedLines, perWordRomaji, timings); + String romanization = reading.text(); + final boolean swap = Settings.LYRICS_SWAP_TRANS_ROMA.get(); StringBuilder builder = new StringBuilder(); int romaStart = -1; int romaEnd = -1; int transStart = -1; int transEnd = -1; - if (swap) { - if (translation != null) { + // The swap puts the translation above the lyric and the reading below it. + String above = swap ? translation : romanization; + if (above != null) { + builder.append(above); + builder.append('\n'); + if (swap) { transStart = 0; - builder.append(translation); - builder.append('\n'); transEnd = builder.length(); - } - final int originalStart = builder.length(); - builder.append(original); - final int originalEnd = builder.length(); - if (romanization != null) { - builder.append('\n'); - romaStart = builder.length(); - builder.append(romanization); + } else { + romaStart = 0; romaEnd = builder.length(); } - SpannableString text = new SpannableString(builder.toString()); - attachUniformSpacing(text, spacing); - ForegroundColorSpan unsungSpan = applySpans(text, timings, originalStart, originalEnd, - romaStart, romaEnd, transStart, transEnd, isRightAligned); - return new BuildResult(text, unsungSpan, transStart, transEnd, romaStart, romaEnd, - reading.spans()); - } - if (romanization != null) { - romaStart = 0; - builder.append(romanization); - builder.append('\n'); - romaEnd = builder.length(); } final int originalStart = builder.length(); builder.append(original); final int originalEnd = builder.length(); - if (translation != null) { + String below = swap ? romanization : translation; + if (below != null) { builder.append('\n'); - transStart = builder.length(); - builder.append(translation); - transEnd = builder.length(); + if (swap) { + romaStart = builder.length(); + builder.append(below); + romaEnd = builder.length(); + } else { + transStart = builder.length(); + builder.append(below); + transEnd = builder.length(); + } } SpannableString text = new SpannableString(builder.toString()); attachUniformSpacing(text, spacing); - ForegroundColorSpan unsungSpan = applySpans(text, timings, originalStart, originalEnd, - romaStart, romaEnd, transStart, transEnd, isRightAligned); + ForegroundColorSpan unsungSpan = applySpans(text, + showSyllableRomanization ? syllableRomanTimings : timings, + originalStart, originalEnd, romaStart, romaEnd, transStart, transEnd, isRightAligned); return new BuildResult(text, unsungSpan, transStart, transEnd, romaStart, romaEnd, - reading.spans()); + reading.spans(), false, showSyllableRomanization, originalStart); } private static void attachUniformSpacing(SpannableString text, Spacing spacing) { @@ -688,7 +1411,7 @@ private static void attachUniformSpacing(SpannableString text, Spacing spacing) Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } - /** The line's own translation, null when it has none or it only repeats the original + /** * One line of the dual mode: the text to show and where the small run sits in it. The copy * button reads the same call, so the two cannot disagree about which side is the main line. * @@ -705,15 +1428,15 @@ record DualLine(String text, boolean mainIsRoma, boolean smallIsRoma, int smallS static DualLine dualLineFor(LyricsLine line, @Nullable List lineTimings, int index, @Nullable String translation, String original, @Nullable List romanizedLines) { - final Romanized roma = romanizedWithTimings(line, index, romanizedLines, lineTimings); - final String romanization = roma.text(); + Romanized roma = romanizedWithTimings(line, index, romanizedLines, lineTimings); + String romanization = roma.text(); final boolean mainIsRoma; - final String main; + String main; // The main slot is what the reading would have held, so a line whose reading never arrived keeps // the original rather than a hole, and the translation, spoken as a whole and lighting nothing, // steps down to the small run instead of taking a line it could never light. final boolean swap = Settings.LYRICS_SWAP_TRANS_ROMA.get(); - final String small; + String small; if (translation != null && romanization != null) { mainIsRoma = !swap; main = mainIsRoma ? romanization : translation; @@ -737,7 +1460,7 @@ static DualLine dualLineFor(LyricsLine line, @Nullable List lineTimi } // The words follow the reading, and fall back to the original when there is none; the // translation never holds them. - final List words = romanization != null ? roma.spans() : lineTimings; + List words = romanization != null ? roma.spans() : lineTimings; final boolean wordsOnSmall = romanization != null ? !mainIsRoma : swap; if (small == null) { return new DualLine(main, mainIsRoma, false, -1, -1, words, false); @@ -753,14 +1476,15 @@ private static List shiftSpans(@Nullable List spans, int if (spans == null || spans.isEmpty() || by == 0) { return spans; } - final List shifted = new ArrayList<>(spans.size()); + List shifted = new ArrayList<>(spans.size()); for (WordTiming span : spans) { shifted.add(new WordTiming(span.start() + by, span.end() + by, - span.startMs(), span.endMs(), span.romaji())); + span.startMs(), span.endMs(), span.romaji(), span.hasTrailingSpace())); } return shifted; } + /** The line's own translation, null when it has none or it only repeats the original. */ @Nullable static String translationAt(LyricsLine line, int index, @Nullable List translatedLines) { @@ -771,8 +1495,19 @@ static String translationAt(LyricsLine line, int index, if (raw == null) { return null; } - final String trimmed = raw.trim(); - return !trimmed.isEmpty() && !trimmed.equalsIgnoreCase(line.text().trim()) + if (isSecondaryLine(line)) { + raw = LyricsBracketSplitter.stripAllBrackets(raw); + // The lead's translation carried down onto its background vocal would only repeat it. + if (index > 0) { + String prevRaw = translatedLines.get(index - 1); + if (prevRaw != null && prevRaw.trim().equalsIgnoreCase(raw.trim())) { + return null; + } + } + } + String trimmed = raw.trim(); + String origTrimmed = displayLineText(line).trim(); + return !trimmed.isEmpty() && !trimmed.equalsIgnoreCase(origTrimmed) ? trimmed : null; } @@ -783,8 +1518,27 @@ static String lineRomanizationAt(LyricsLine line, int index, if (romanizedLines == null || index >= romanizedLines.size()) { return null; } - String roma = romanizedLines.get(index).text().trim(); - return !roma.isEmpty() && !roma.equalsIgnoreCase(line.text().trim()) ? roma : null; + String origTrimmed = displayLineText(line).trim(); + if (origTrimmed.isEmpty() || LyricsRomanizer.isPurelyLatinScript(origTrimmed)) { + return null; + } + String roma = romanizedLines.get(index).text(); + if (roma == null) { + return null; + } + if (isSecondaryLine(line)) { + roma = LyricsBracketSplitter.stripAllBrackets(roma); + // The lead's reading carried down onto a far shorter background vocal is not its own. + if (index > 0) { + String prevRoma = romanizedLines.get(index - 1).text(); + if (prevRoma != null && prevRoma.trim().equalsIgnoreCase(roma.trim()) + && prevRoma.trim().length() > origTrimmed.length() * 2 + 4) { + return null; + } + } + } + roma = roma.trim(); + return !roma.isEmpty() && !roma.equalsIgnoreCase(origTrimmed) ? roma : null; } /** Text and words come back as one, so no word is lit at an offset from another string. */ @@ -796,7 +1550,7 @@ private static Romanized romanizedWithTimings(LyricsLine line, int index, @Nullable List lineTimings) { // The provider's own per-word reading comes first: it has already said which syllable // belongs to which word, and only a reading left whole is spread by syllable count. - final Reading reading = readingFor(line, index, romanizedLines, true, lineTimings); + Reading reading = readingFor(line, index, romanizedLines, true, lineTimings); return new Romanized(reading.text(), reading.spans()); } @@ -812,21 +1566,21 @@ private static List spreadOverWords(String whole, String text, if (lineTimings == null || lineTimings.isEmpty()) { return null; } - final int[][] tokens = LocalRomanizer.readingTokens(whole); + int[][] tokens = LocalRomanizer.readingTokens(whole); if (tokens.length == 0) { return null; } - final List words = wordsAt(text, lineTimings); + List words = wordsAt(text, lineTimings); if (words.isEmpty()) { return null; } - final int[][] spread = LocalRomanizer.spreadReading(words, whole); + int[][] spread = LocalRomanizer.spreadReading(words, whole); if (spread == null) { return null; } - final List spans = new ArrayList<>(tokens.length); + List spans = new ArrayList<>(tokens.length); for (int i = 0; i < words.size(); i++) { - final Word word = words.get(i); + Word word = words.get(i); final int from = spread[i][0]; final int to = Math.min(spread[i][1], tokens.length); if (to <= from) { @@ -846,7 +1600,7 @@ private static long windowEndFor(List words, int index, long wordEndMs) { if (index + 1 >= words.size()) { return wordEndMs; } - final Word word = words.get(index); + Word word = words.get(index); final long nextStart = words.get(index + 1).startMs(); if (nextStart > word.startMs() && nextStart != wordEndMs) { return nextStart; @@ -887,7 +1641,7 @@ private static void spreadWordWindow(List out, int[][] tokens, /** One word per timing, which is all the placement needs to know about the line. */ private static List wordsAt(String text, List lineTimings) { - final List words = new ArrayList<>(lineTimings.size()); + List words = new ArrayList<>(lineTimings.size()); for (WordTiming timing : lineTimings) { final int start = timing.start(); final int end = Math.min(timing.end(), text.length()); @@ -895,7 +1649,7 @@ private static List wordsAt(String text, List lineTimings) { continue; } words.add(new Word(timing.startMs(), timing.endMs(), text.substring(start, end), - null, false)); + timing.romaji(), timing.hasTrailingSpace())); } return words; } @@ -925,13 +1679,13 @@ private static ForegroundColorSpan applySpans(SpannableString text, List= 0) { + if (romaStart >= 0 && romaEnd > romaStart) { text.setSpan(new RelativeSizeSpan(TRANSLATION_RELATIVE_SIZE), romaStart, romaEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); text.setSpan(new ForegroundColorSpan(LyricsColors.secondaryTextColor() | 0xFF000000), romaStart, romaEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } - if (transStart >= 0) { + if (transStart >= 0 && transEnd > transStart) { text.setSpan(new RelativeSizeSpan(TRANSLATION_RELATIVE_SIZE), transStart, transEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); text.setSpan(new ForegroundColorSpan(LyricsColors.secondaryTextColor() | 0xFF000000), @@ -945,30 +1699,22 @@ private static ForegroundColorSpan applySpans(SpannableString text, List= 0 && romaEnd > romaStart) { - int spanEnd = (swap && romaEnd == text.length()) ? romaEnd - : (romaEnd < text.length() && text.charAt(romaEnd - 1) == '\n' ? romaEnd - : Math.min(text.length(), romaEnd + 1)); - text.setSpan(new AlignmentSpan.Standard(resolveAlignment(false, isRightAligned)), - romaStart, spanEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - } if (originalStart >= 0 && originalEnd > originalStart) { - int spanEnd = originalEnd; - if (!swap && transStart > originalEnd) { - spanEnd = transStart; - } else if (swap && romaStart > originalEnd) { - spanEnd = romaStart; - } + int spanEnd = originalEnd < text.length() && text.charAt(originalEnd) == '\n' + ? originalEnd + 1 : originalEnd; boolean isOrigRtl = isRtlText(text.subSequence(originalStart, originalEnd)); text.setSpan(new AlignmentSpan.Standard(resolveAlignment(isOrigRtl, isRightAligned)), originalStart, spanEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } + if (romaStart >= 0 && romaEnd > romaStart) { + int spanEnd = romaEnd < text.length() && text.charAt(romaEnd) == '\n' + ? romaEnd + 1 : romaEnd; + text.setSpan(new AlignmentSpan.Standard(resolveAlignment(false, isRightAligned)), + romaStart, spanEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } if (transStart >= 0 && transEnd > transStart) { - int spanEnd = (swap && transEnd < text.length() && text.charAt(transEnd - 1) == '\n') ? transEnd : transEnd; - if (!swap && transEnd < text.length() && text.charAt(transEnd - 1) != '\n') { - spanEnd = Math.min(text.length(), transEnd + 1); - } + int spanEnd = transEnd < text.length() && text.charAt(transEnd) == '\n' + ? transEnd + 1 : transEnd; boolean isTransRtl = isRtlText(text.subSequence(transStart, transEnd)); text.setSpan(new AlignmentSpan.Standard(resolveAlignment(isTransRtl, isRightAligned)), transStart, spanEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); @@ -991,4 +1737,4 @@ public static Layout.Alignment resolveAlignment(boolean isRtl, boolean isRightAl return isRtl ? Layout.Alignment.ALIGN_OPPOSITE : Layout.Alignment.ALIGN_NORMAL; } } -} +} \ No newline at end of file diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsViewSupport.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsViewSupport.java index fe6cfd4234..a857661c10 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsViewSupport.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsViewSupport.java @@ -12,15 +12,21 @@ import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ArgbEvaluator; +import android.animation.TimeInterpolator; import android.animation.ValueAnimator; import android.content.Context; import android.content.res.ColorStateList; +import android.graphics.Paint; +import android.graphics.Typeface; import android.graphics.drawable.Drawable; import android.graphics.drawable.GradientDrawable; +import android.os.Build; import android.text.Layout; import android.util.TypedValue; +import android.view.Gravity; import android.view.View; import android.view.ViewGroup; +import android.view.animation.PathInterpolator; import android.widget.LinearLayout; import android.widget.TextView; @@ -30,6 +36,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Locale; import app.morphe.extension.music.patches.lyrics.model.LyricsLine; import app.morphe.extension.music.settings.Settings; @@ -40,19 +47,60 @@ final class LyricsViewSupport { private static final float FOOTER_ALPHA = 0.6f; - private static final long HIGHLIGHT_FADE_DURATION_MILLISECONDS = 200; + private static final long HIGHLIGHT_FADE_DURATION_MILLISECONDS = 320; private static final String LYRICS_SOURCE_KEY = "morphe_music_lyrics_source_label"; private static final float FOOTER_TEXT_SIZE_SP = 16; - private static final float BACKGROUND_TEXT_SIZE_FACTOR = 0.85f; + private static final float BACKGROUND_TEXT_SIZE_FACTOR = 0.7f; private static final float SECTION_TAG_TEXT_SIZE_FACTOR = 0.45f; private static final float SECTION_DOT_INSET_FACTOR = 0.18f; + /** Lines other than the active one shrink a little, on top of dimming. */ + static final float ACTIVE_LINE_SCALE = 1.0f; + static final float INACTIVE_LINE_SCALE = 0.95f; + private static final ArgbEvaluator BUTTON_COLOR_EVALUATOR = new ArgbEvaluator(); + /** Compose LinearOutSlowInEasing. */ + static final TimeInterpolator LINEAR_OUT_SLOW_IN = + new PathInterpolator(0f, 0f, 0.2f, 1f); + + /** Compose FastOutSlowInEasing. */ + static final TimeInterpolator FAST_OUT_SLOW_IN = + new PathInterpolator(0.4f, 0f, 0.2f, 1f); + + /** + * MIUI and HyperOS draw the bold system font too thin, most visibly on CJK text, so these + * devices get a heavier weight on top. + */ + private static final boolean IS_XIAOMI = isXiaomi(); + + private static boolean isXiaomi() { + String manufacturer = Build.MANUFACTURER.toLowerCase(Locale.ROOT); + String brand = Build.BRAND.toLowerCase(Locale.ROOT); + return manufacturer.contains("xiaomi") || brand.contains("xiaomi") + || brand.contains("redmi") || brand.contains("poco"); + } + + /** Bolds a paint for lyrics; a typeface set on it afterwards undoes the weight. */ + static void applyLyricsBoldPaint(Paint paint) { + paint.setTypeface(Typeface.DEFAULT_BOLD); + paint.setFlags(paint.getFlags() + | Paint.ANTI_ALIAS_FLAG + | Paint.SUBPIXEL_TEXT_FLAG + | Paint.LINEAR_TEXT_FLAG); + paint.setFontVariationSettings(IS_XIAOMI ? "'wght' 750" : "'wght' 700"); + paint.setFakeBoldText(IS_XIAOMI); + } + + /** Draws a line a little tighter, except a joining script, which spacing would break apart. */ + static float lineLetterSpacing(@Nullable CharSequence text, float textSizeSp) { + return LyricsSpanBuilder.containsJoiningScript(text) ? 0f : -0.5f / Math.max(12f, textSizeSp); + } + private LyricsViewSupport() { } @@ -76,12 +124,12 @@ static int rowHeightOutsideText(View row, View text) { } int total = 0; for (int i = 0; i < group.getChildCount(); i++) { - final View child = group.getChildAt(i); - if (child == text) { + View child = group.getChildAt(i); + if (child == text || child.getVisibility() == View.GONE) { continue; } int height = child.getHeight(); - final ViewGroup.LayoutParams lp = child.getLayoutParams(); + ViewGroup.LayoutParams lp = child.getLayoutParams(); if (height <= 0 && lp != null && lp.height > 0) { height = lp.height; } @@ -100,11 +148,11 @@ static void applySectionDotSize(@Nullable LinearLayout row) { final int size = scaledPixels(row.getContext(), LyricsPanelView.SECTION_DOT_SIZE_FACTOR); final int gap = scaledPixels(row.getContext(), LyricsPanelView.SECTION_DOT_GAP_FACTOR); for (int i = 0; i < row.getChildCount(); i++) { - final View dot = row.getChildAt(i); + View dot = row.getChildAt(i); if (dot == null) { continue; } - final LinearLayout.LayoutParams params = + LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) dot.getLayoutParams(); if (params == null) { continue; @@ -120,17 +168,21 @@ static void applySectionDotSize(@Nullable LinearLayout row) { * The words the karaoke pass paints for one line. * *

    An only mode drops the original and measures its reading against the text it draws, so the - * reading's words already sit on the {@code origStart} base and the original's are not in that text + * reading's words already sit on the {@code originalStart} base and the original's are not in that text * at all; a plain mode keeps the original, which is the base {@link #withReading} moves the reading's * onto. Deciding by the reading alone would rob a plain mode of the words it paints. */ static List wordSpansFor( LyricsSpanBuilder.BuildResult result, boolean only, - List lineTimings, int origStart) { + List lineTimings) { if (!only) { - return withReading(lineTimings, result, origStart); + // Syllable readings are drawn by the view from the same timings that light the lyric. + if (result.showSyllableRomanization()) { + return result.readingSpans(); + } + return withReading(lineTimings, result); } - final List spans = result.readingSpans(); + List spans = result.readingSpans(); return spans == null || spans.isEmpty() ? Collections.emptyList() : spans; } @@ -141,13 +193,13 @@ static List wordSpansFor( */ static List withReading( List lineTimings, - LyricsSpanBuilder.BuildResult result, int origStart) { - final List readingSpans = result.readingSpans(); + LyricsSpanBuilder.BuildResult result) { + List readingSpans = result.readingSpans(); if (readingSpans == null || readingSpans.isEmpty() || result.romaStart() < 0) { return lineTimings; } - final int shift = result.romaStart() - origStart; - final List combined = + final int shift = result.romaStart() - result.originalStart(); + List combined = new ArrayList<>(lineTimings.size() + readingSpans.size()); combined.addAll(lineTimings); for (LyricsSpanBuilder.WordTiming span : readingSpans) { @@ -157,24 +209,48 @@ static List withReading( return combined; } - /** Eases the highlight between lines the way the built-in panel does. */ + /** Eases the highlight between lines the way the built-in panel does, shrinking the dimmed ones. */ static void fadeTo(TextView lineView, float alpha) { if (!(lineView instanceof LyricsLineView view)) { return; } + if (view.isSecondary()) { + // A background vocal shows and hides with its own expansion instead. + if (view.fadeAnimator != null) { + view.fadeAnimator.cancel(); + view.fadeAnimator = null; + } + view.lineAlpha = 1f; + view.fadeTargetAlpha = 1f; + return; + } + // Called on every highlight tick, so a fade already heading here is left to finish. + if (view.fadeAnimator != null && Math.abs(view.fadeTargetAlpha - alpha) < 0.005f) { + return; + } if (view.fadeAnimator != null) { view.fadeAnimator.cancel(); view.fadeAnimator = null; } - if (Math.abs(view.lineAlpha - alpha) < 0.01f) { + view.fadeTargetAlpha = alpha; + final float targetScale = alpha >= 1f ? ACTIVE_LINE_SCALE : INACTIVE_LINE_SCALE; + final float startAlpha = view.lineAlpha; + final float startScale = view.lineScale; + if (Math.abs(startAlpha - alpha) < 0.01f && Math.abs(startScale - targetScale) < 0.005f) { view.lineAlpha = alpha; + view.lineScale = targetScale; + applyLineScaleTransform(view, targetScale); view.invalidate(); return; } - final ValueAnimator animator = ValueAnimator.ofFloat(view.lineAlpha, alpha); + ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f); animator.setDuration(HIGHLIGHT_FADE_DURATION_MILLISECONDS); + animator.setInterpolator(FAST_OUT_SLOW_IN); animator.addUpdateListener(animation -> { - view.lineAlpha = (float) animation.getAnimatedValue(); + final float t = (float) animation.getAnimatedValue(); + view.lineAlpha = startAlpha + (alpha - startAlpha) * t; + view.lineScale = startScale + (targetScale - startScale) * t; + applyLineScaleTransform(view, view.lineScale); view.invalidate(); }); animator.addListener(new AnimatorListenerAdapter() { @@ -189,6 +265,24 @@ public void onAnimationEnd(Animator animation) { animator.start(); } + static void applyLineScaleTransform(LyricsLineView view, float scale) { + if (view.isSecondary()) { + return; + } + final int w = view.getWidth(); + final int h = view.getHeight(); + if (w > 0) { + final boolean isEnd = view.isRightAligned() + || (view.getGravity() & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.END; + view.setPivotX(isEnd ? w - view.getPaddingRight() : view.getPaddingLeft()); + } + if (h > 0) { + view.setPivotY(h * 0.5f); + } + view.setScaleX(scale); + view.setScaleY(scale); + } + static void applyFooterStyle(TextView footer) { footer.setTextSize(TypedValue.COMPLEX_UNIT_SP, FOOTER_TEXT_SIZE_SP); footer.setTextColor(LyricsColors.secondaryTextColor()); @@ -262,11 +356,11 @@ static void fadeButtonColors(TextView button, int background, int foreground) { button.setBackground(pill); } - final ColorStateList pillColor = pill.getColor(); + ColorStateList pillColor = pill.getColor(); final int fromBackground = pillColor == null ? background : pillColor.getDefaultColor(); final int fromForeground = button.getCurrentTextColor(); // All of them: the dual pill's far twin would keep the inactive tint while it is up. - final Drawable[] icons = button.getCompoundDrawablesRelative(); + Drawable[] icons = button.getCompoundDrawablesRelative(); // Nothing to ease from before the panel is on screen, or when nothing changed. if (!button.isAttachedToWindow() diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/SectionDotsMotion.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/SectionDotsMotion.java index b3c0960e87..d2d1955451 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/SectionDotsMotion.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/SectionDotsMotion.java @@ -10,13 +10,13 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import app.morphe.extension.music.patches.lyrics.model.LyricsLine; -import app.morphe.extension.music.patches.lyrics.model.Word; - import java.util.ArrayList; import java.util.Collections; import java.util.List; +import app.morphe.extension.music.patches.lyrics.model.LyricsLine; +import app.morphe.extension.music.patches.lyrics.model.Word; + public final class SectionDotsMotion { public static final int DOT_COUNT = 3; @@ -86,7 +86,7 @@ public static List computeGaps(@Nullable List lines) { firstIndex++; } if (firstIndex < lines.size()) { - final LyricsLine first = lines.get(firstIndex); + LyricsLine first = lines.get(firstIndex); if (first.startTimeMs() >= MIN_GAP_MS) { gaps = new ArrayList<>(4); gaps.add(new Gap(firstIndex, 0L, first.startTimeMs())); @@ -94,7 +94,7 @@ public static List computeGaps(@Nullable List lines) { } int previousLead = -1; for (int i = 0; i < lines.size(); i++) { - final LyricsLine line = lines.get(i); + LyricsLine line = lines.get(i); if (line == null || line.isBG()) { continue; } @@ -144,7 +144,7 @@ public static State stateAt(long gapStartMs, long gapEndMs, long positionMs) { // Staggered so the last dot finishes exactly when the row hides. final float dotSpan = Math.max(1f, (vanishStart - enterEnd) / DOT_COUNT); - final float[] dotAlpha = new float[DOT_COUNT]; + float[] dotAlpha = new float[DOT_COUNT]; for (int i = 0; i < DOT_COUNT; i++) { final float lit = DOT_UNLIT + (1f - DOT_UNLIT) * clamp((t - enterEnd - dotSpan * i) / dotSpan); @@ -170,7 +170,7 @@ private static long effectiveEndMs(@Nullable LyricsLine line) { if (end > line.startTimeMs()) { return end; } - final List words = line.words(); + List words = line.words(); for (int i = words.size() - 1; i >= 0; i--) { final long wordEnd = words.get(i).endMs(); if (wordEnd > line.startTimeMs()) { diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/SponsorBlockMusicSubmitDialog.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/SponsorBlockMusicSubmitDialog.java index 30b28b5587..c9b6412c64 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/SponsorBlockMusicSubmitDialog.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/SponsorBlockMusicSubmitDialog.java @@ -85,22 +85,22 @@ static String formatOffsetMs(long offsetMs) { static void show(Context context, String videoId, long startMs, long endMs, IntConsumer onOffsetSync, Runnable onExit) { final long videoLength = VideoInformation.getVideoLength(); - final long[] times = {clampTime(Math.min(startMs, endMs), videoLength), + long[] times = {clampTime(Math.min(startMs, endMs), videoLength), clampTime(Math.max(startMs, endMs), videoLength)}; - final int[] slots = {0, 0}; - final int[] selected = {DEFAULT_CATEGORY_INDEX}; - final boolean[] submitted = {false}; - final LyricsManager manager = LyricsManager.getInstance(); - - final TextView startView = createTimeView(context); - final TextView endView = createTimeView(context); - final TextView separator = new TextView(context); + int[] slots = {0, 0}; + int[] selected = {DEFAULT_CATEGORY_INDEX}; + boolean[] submitted = {false}; + LyricsManager manager = LyricsManager.getInstance(); + + TextView startView = createTimeView(context); + TextView endView = createTimeView(context); + TextView separator = new TextView(context); separator.setText(" - "); separator.setTextSize(22); separator.setTypeface(Typeface.DEFAULT_BOLD); separator.setTextColor(ThemeUtils.getAppForegroundColor()); - final OffsetSliderView offsetSlider = new OffsetSliderView(context); + OffsetSliderView offsetSlider = new OffsetSliderView(context); offsetSlider.setValue(manager.getTemporaryOffsetMs()); offsetSlider.listener = ms -> { manager.setTemporaryOffsetMs(ms); @@ -109,7 +109,7 @@ static void show(Context context, String videoId, long startMs, long endMs, onOffsetSync.accept(ms); }; - final Runnable refreshTimes = () -> { + Runnable refreshTimes = () -> { startView.setText(formatTimeMs(times[0])); endView.setText(formatTimeMs(times[1])); // The candidate skips the lyrics the moment it exists and on every resample, @@ -121,26 +121,26 @@ static void show(Context context, String videoId, long startMs, long endMs, bindTimeView(endView, times, slots, 1, refreshTimes); refreshTimes.run(); - final LinearLayout timesRow = new LinearLayout(context); + LinearLayout timesRow = new LinearLayout(context); timesRow.setOrientation(LinearLayout.HORIZONTAL); timesRow.setGravity(Gravity.CENTER); timesRow.addView(startView); timesRow.addView(separator); timesRow.addView(endView); - final Button offsetButtonA = createCircleButton(context, "A"); - final Button offsetButtonB = createCircleButton(context, "B"); + Button offsetButtonA = createCircleButton(context, "A"); + Button offsetButtonB = createCircleButton(context, "B"); offsetButtonA.setOnClickListener(v -> applyOffsetSlot(manager, offsetSlider, times, slots, 0, refreshTimes)); offsetButtonB.setOnClickListener(v -> applyOffsetSlot(manager, offsetSlider, times, slots, 1, refreshTimes)); - final LinearLayout offsetRow = new LinearLayout(context); + LinearLayout offsetRow = new LinearLayout(context); offsetRow.setOrientation(LinearLayout.HORIZONTAL); offsetRow.setGravity(Gravity.CENTER_VERTICAL); offsetRow.addView(offsetButtonA, new LinearLayout.LayoutParams( Dim.dp36, Dim.dp36)); - final LinearLayout.LayoutParams sliderParams = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams sliderParams = new LinearLayout.LayoutParams( 0, Dim.dp36, 1f); sliderParams.leftMargin = Dim.dp8; sliderParams.rightMargin = Dim.dp8; @@ -148,24 +148,24 @@ static void show(Context context, String videoId, long startMs, long endMs, offsetRow.addView(offsetButtonB, new LinearLayout.LayoutParams( Dim.dp36, Dim.dp36)); - final LinearLayout content = new LinearLayout(context); + LinearLayout content = new LinearLayout(context); content.setOrientation(LinearLayout.VERTICAL); content.addView(timesRow, new LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT)); content.addView(offsetRow, blockParams(Dim.dp28)); - final String[] categoryTitles = new String[CATEGORIES.length]; - final String[] categoryKeys = new String[CATEGORIES.length]; + String[] categoryTitles = new String[CATEGORIES.length]; + String[] categoryKeys = new String[CATEGORIES.length]; for (int i = 0; i < CATEGORIES.length; i++) { categoryTitles[i] = CATEGORIES[i].title.toString(); categoryKeys[i] = CATEGORIES[i].keyValue; } - final Button categoryButton = CustomDialog.createButton(context, null, + Button categoryButton = CustomDialog.createButton(context, null, categoryTitles[selected[0]], null, false, false); categoryButton.setOnClickListener(v -> { - final Dialog picker = CustomDialogListPreference.createListDialog( + Dialog picker = CustomDialogListPreference.createListDialog( context, str("morphe_sb_new_segment_choose_category"), categoryTitles, categoryKeys, categoryKeys[selected[0]], index -> { @@ -176,7 +176,7 @@ context, str("morphe_sb_new_segment_choose_category"), }); content.addView(categoryButton, blockParams(Dim.dp16)); - final Pair dialogPair = CustomDialog.create( + Pair dialogPair = CustomDialog.create( context, str("morphe_sb_new_segment_title"), null, @@ -190,7 +190,7 @@ context, str("morphe_sb_new_segment_choose_category"), str("morphe_sb_new_segment_start_is_before_end")); return; } - final SegmentCategory category = CATEGORIES[selected[0]]; + SegmentCategory category = CATEGORIES[selected[0]]; submitted[0] = true; Utils.runOnBackgroundThread(() -> submitSegment( videoId, category, start, end, videoLength, manager)); @@ -200,14 +200,14 @@ context, str("morphe_sb_new_segment_choose_category"), () -> copySpan(context, videoId, times), false); - final Dialog dialog = dialogPair.first; + Dialog dialog = dialogPair.first; dialog.setOnDismissListener(d -> { if (!submitted[0]) { manager.clearDialogSegment(); } onExit.run(); }); - final LinearLayout mainLayout = dialogPair.second; + LinearLayout mainLayout = dialogPair.second; mainLayout.addView(content, mainLayout.getChildCount() - 1, new LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT, @@ -250,9 +250,9 @@ private static void scheduleSegmentRefresh(String videoId, LyricsManager manager private static void copySpan(Context context, String videoId, long[] times) { final long offsetMs = LyricsManager.getInstance().getTemporaryOffsetMs(); - final String text = videoId + " [" + formatSeconds(times[0]) + ", " + String text = videoId + " [" + formatSeconds(times[0]) + ", " + formatSeconds(times[1]) + "] " + (offsetMs > 0 ? "+" : "") + offsetMs; - final ClipboardManager clipboard = (ClipboardManager) + ClipboardManager clipboard = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); if (clipboard != null) { clipboard.setPrimaryClip(ClipData.newPlainText("sponsorblock", text)); @@ -261,7 +261,7 @@ private static void copySpan(Context context, String videoId, long[] times) { } private static TextView createTimeView(Context context) { - final TextView view = new TextView(context); + TextView view = new TextView(context); view.setTextSize(22); view.setTypeface(Typeface.DEFAULT_BOLD); view.setTextColor(ThemeUtils.getAppForegroundColor()); @@ -270,7 +270,7 @@ private static TextView createTimeView(Context context) { } private static LinearLayout.LayoutParams blockParams(int topMargin) { - final LinearLayout.LayoutParams params = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams params = new LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT, Dim.dp36); params.topMargin = topMargin; return params; @@ -279,7 +279,7 @@ private static LinearLayout.LayoutParams blockParams(int topMargin) { @SuppressLint("ClickableViewAccessibility") private static void bindTimeView(TextView view, long[] times, int[] slots, int index, Runnable refreshTimes) { - final long[] downSample = new long[1]; + long[] downSample = new long[1]; view.setOnTouchListener((v, event) -> { if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { downSample[0] = LyricsManager.getInstance().getVideoPositionMs(); @@ -315,9 +315,9 @@ private static void swapSides(long[] times, int[] slots) { } private static Button createCircleButton(Context context, String text) { - final Button button = CustomDialog.createButton(context, null, text, null, + Button button = CustomDialog.createButton(context, null, text, null, false, false); - final GradientDrawable circle = new GradientDrawable(); + GradientDrawable circle = new GradientDrawable(); circle.setShape(GradientDrawable.OVAL); circle.setColor(ThemeUtils.getCancelOrNeutralButtonBackgroundColor()); button.setBackground(circle); @@ -446,7 +446,7 @@ public boolean onTouchEvent(@NonNull MotionEvent event) { protected void onDraw(@NonNull Canvas canvas) { textPaint.setTextSize(TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_SP, 16, getResources().getDisplayMetrics())); - final Paint.FontMetrics metrics = textPaint.getFontMetrics(); + Paint.FontMetrics metrics = textPaint.getFontMetrics(); canvas.drawText(formatOffsetMs(offsetMs), getWidth() / 2f, getHeight() / 2f - (metrics.ascent + metrics.descent) / 2f, textPaint); diff --git a/extensions/music/src/main/java/app/morphe/extension/music/settings/Settings.java b/extensions/music/src/main/java/app/morphe/extension/music/settings/Settings.java index 209db03f44..c744f0f386 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/settings/Settings.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/settings/Settings.java @@ -242,7 +242,6 @@ public class Settings extends SharedYouTubeSettings { public static final BooleanSetting LYRICS_ROMANIZE_ONLY = new BooleanSetting("morphe_music_lyrics_romanize_only", FALSE, false, parent(LYRICS_ENABLED)); public static final BooleanSetting LYRICS_WORD_SYNC = new BooleanSetting("morphe_music_lyrics_word_sync", TRUE, false, parent(LYRICS_ENABLED)); public static final BooleanSetting LYRICS_HIDE_PLAYED = new BooleanSetting("morphe_music_lyrics_hide_played", FALSE, false, parent(LYRICS_ENABLED)); - public static final BooleanSetting LYRICS_SPLIT_BACKGROUND = new BooleanSetting("morphe_music_lyrics_split_background", FALSE, false, parent(LYRICS_ENABLED)); public static final BooleanSetting LYRICS_HIDE_UNPLAYED = new BooleanSetting("morphe_music_lyrics_hide_unplayed", FALSE, false, parent(LYRICS_ENABLED)); public static final IntegerSetting LYRICS_TEXT_SIZE = new IntegerSetting("morphe_music_lyrics_text_size", 26, false, parent(LYRICS_ENABLED)); public static final IntegerSetting LYRICS_OFFSET_MS = new IntegerSetting("morphe_music_lyrics_offset_ms", 0, false, parent(LYRICS_ENABLED)); diff --git a/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsAiConfigPreference.java b/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsAiConfigPreference.java index b4e70392bc..3b965c2132 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsAiConfigPreference.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsAiConfigPreference.java @@ -110,7 +110,7 @@ private void showDialog() { instructionParams.bottomMargin = Dim.dp12; content.addView(instruction, instructionParams); - final String[] endpointFilter = {"all"}; + String[] endpointFilter = {"all"}; content.addView(createLabel(context, "morphe_music_lyrics_ai_config_endpoint_label")); Button endpointFilterButton = createButton(context, str("morphe_music_lyrics_ai_config_show_all")); content.addView(endpointFilterButton); diff --git a/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsAiPromptPreference.java b/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsAiPromptPreference.java index 52ca63a915..65d0737c8d 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsAiPromptPreference.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsAiPromptPreference.java @@ -231,7 +231,7 @@ private void enableAfterValidation(BooleanSetting switchSetting) { private static final ExecutorService VALIDATION = Executors.newSingleThreadExecutor( runnable -> { - final Thread thread = new Thread(runnable, "lyrics-ai-validate"); + Thread thread = new Thread(runnable, "lyrics-ai-validate"); thread.setDaemon(true); return thread; }); @@ -265,10 +265,10 @@ private View createReasoningRow(Context context) { row.setPadding(Dim.dp12, Dim.dp10, Dim.dp12, Dim.dp10); row.setVisibility(View.GONE); - final String stored = Settings.LYRICS_AI_REASONING_EFFORT.get(); + String stored = Settings.LYRICS_AI_REASONING_EFFORT.get(); value.setText(display(stored)); MODEL_QUERY.execute(() -> { - final String[] efforts = OpenAIClient.fetchReasoningEfforts( + String[] efforts = OpenAIClient.fetchReasoningEfforts( Settings.LYRICS_AI_BASE_URL.get(), Settings.LYRICS_AI_API_TOKEN.get(), Settings.LYRICS_AI_MODEL.get()); Utils.runOnMainThread(() -> { @@ -276,14 +276,14 @@ private View createReasoningRow(Context context) { return; } row.setVisibility(View.VISIBLE); - final CharSequence[] labels = new CharSequence[efforts.length]; + CharSequence[] labels = new CharSequence[efforts.length]; for (int i = 0; i < efforts.length; i++) { labels[i] = display(efforts[i]); } row.setOnClickListener(v -> CustomDialogListPreference.createListDialog(context, "Reasoning effort", labels, efforts, Settings.LYRICS_AI_REASONING_EFFORT.get(), index -> { - final String chosen = efforts[index]; + String chosen = efforts[index]; Settings.LYRICS_AI_REASONING_EFFORT.save(chosen); value.setText(display(chosen)); }) @@ -302,7 +302,7 @@ private static String display(String effort) { private static final ExecutorService MODEL_QUERY = Executors.newSingleThreadExecutor( runnable -> { - final Thread thread = new Thread(runnable, "lyrics-ai-models"); + Thread thread = new Thread(runnable, "lyrics-ai-models"); thread.setDaemon(true); return thread; }); diff --git a/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsClearCachePreference.java b/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsClearCachePreference.java index f03096f3c0..006ad03e1a 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsClearCachePreference.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsClearCachePreference.java @@ -77,7 +77,7 @@ protected void onClick() { /** Thousands of files can be stored, so they are deleted off the main thread. */ private static void clear() { Utils.runOnBackgroundThread(() -> { - final String freed = megabytes(LyricsCache.clear()); + String freed = megabytes(LyricsCache.clear()); Utils.runOnMainThread(() -> Utils.showToastShort( str("morphe_music_lyrics_clear_cache_done", freed))); }); diff --git a/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsLocalRomanizationPreference.java b/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsLocalRomanizationPreference.java index b62d0aec5f..e2e65b5837 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsLocalRomanizationPreference.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsLocalRomanizationPreference.java @@ -86,8 +86,8 @@ private void start() { JapaneseDictStore.ensure(listener(1)); } - private RomanizationDict.Listener listener(final int which) { - final Progress slot = progress[which]; + private RomanizationDict.Listener listener(int which) { + Progress slot = progress[which]; return new RomanizationDict.Listener() { @Override public void onDownload(String fileName, int percent) { diff --git a/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsOrderedListPreference.java b/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsOrderedListPreference.java index 7c35ebf0a9..a1ac8dbb80 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsOrderedListPreference.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsOrderedListPreference.java @@ -309,7 +309,7 @@ private View createRow(Item item, int itemsIndex) { name.setClickable(true); name.setOnClickListener(v -> showTokenDialogFor(item.id, null)); } else if (!locked && PROVIDER_URLS.containsKey(item.id)) { - final String url = PROVIDER_URLS.get(item.id); + String url = PROVIDER_URLS.get(item.id); name.setClickable(true); name.setOnClickListener(v -> openUrl(url)); } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsTokenDialogPreference.java b/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsTokenDialogPreference.java index a63107af84..46aee8af83 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsTokenDialogPreference.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsTokenDialogPreference.java @@ -195,7 +195,7 @@ public void showDialog(@Nullable Runnable onDismissed) { return; } - final String storedToken = setting.get(); + String storedToken = setting.get(); final boolean configured = storedToken != null && !storedToken.isBlank(); LinearLayout content = new LinearLayout(context); diff --git a/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsTranscribeKeyPreference.java b/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsTranscribeKeyPreference.java index 4befedbc7c..7b2a993811 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsTranscribeKeyPreference.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsTranscribeKeyPreference.java @@ -77,7 +77,7 @@ private void init() { @SuppressWarnings("ConstantValue") private void updateSummary() { - final String key = Settings.LYRICS_TRANSCRIBE_KEY.get(); + String key = Settings.LYRICS_TRANSCRIBE_KEY.get(); setSummary(key == null || key.trim().isEmpty() ? str("morphe_music_lyrics_transcribe_key_summary") : str("morphe_music_lyrics_transcribe_key_set_summary")); @@ -90,7 +90,7 @@ protected void onClick() { @SuppressWarnings("ConstantValue") private void showDialog() { - final Context context = getContext(); + Context context = getContext(); if (!(context instanceof Activity activity) || activity.isFinishing() || activity.isDestroyed()) { return; @@ -99,23 +99,23 @@ private void showDialog() { return; } - final String stored = Settings.LYRICS_TRANSCRIBE_KEY.get(); + String stored = Settings.LYRICS_TRANSCRIBE_KEY.get(); final boolean configured = stored != null && !stored.trim().isEmpty(); - final LinearLayout content = new LinearLayout(context); + LinearLayout content = new LinearLayout(context); content.setOrientation(LinearLayout.VERTICAL); - final TextView instruction = new TextView(context); + TextView instruction = new TextView(context); instruction.setText(str("morphe_music_lyrics_transcribe_key_dialog_instruction")); instruction.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14); instruction.setTextColor(ThemeUtils.getAppForegroundColor()); - final LinearLayout.LayoutParams instructionParams = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams instructionParams = new LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT); instructionParams.bottomMargin = Dim.dp12; content.addView(instruction, instructionParams); - final EditText keyInput = ThemedInput.create(context); + EditText keyInput = ThemedInput.create(context); keyInput.setHint("qlrc_..."); keyInput.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); if (configured) { @@ -126,11 +126,11 @@ private void showDialog() { LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT)); - final Button getKeyButton = CustomDialog.createButton(context, null, + Button getKeyButton = CustomDialog.createButton(context, null, str("morphe_music_token_dialog_get_token"), () -> { try { - final Intent intent = new Intent(Intent.ACTION_VIEW, + Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(DASHBOARD_URL)); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(intent); @@ -139,19 +139,19 @@ private void showDialog() { } }, false, false); - final LinearLayout.LayoutParams getKeyParams = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams getKeyParams = new LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT, Dim.dp36); getKeyParams.topMargin = Dim.dp12; content.addView(getKeyButton, getKeyParams); - final Pair dialogPair = CustomDialog.create( + Pair dialogPair = CustomDialog.create( context, str("morphe_music_lyrics_transcribe_key_title"), null, null, str("morphe_settings_save"), () -> { - final String key = keyInput.getText().toString().trim(); + String key = keyInput.getText().toString().trim(); if (key.isEmpty()) { // Saving nothing clears it, which is also what the reset button does. Settings.LYRICS_TRANSCRIBE_KEY.resetToDefault(); @@ -183,8 +183,8 @@ private void showDialog() { false ); - final Dialog dialog = dialogPair.first; - final LinearLayout mainLayout = dialogPair.second; + Dialog dialog = dialogPair.first; + LinearLayout mainLayout = dialogPair.second; mainLayout.addView(content, mainLayout.getChildCount() - 1, new LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT, diff --git a/extensions/music/src/test/java/app/morphe/extension/music/patches/lyrics/model/LyricsBracketSplitterTest.java b/extensions/music/src/test/java/app/morphe/extension/music/patches/lyrics/model/LyricsBracketSplitterTest.java new file mode 100644 index 0000000000..7f537b93e0 --- /dev/null +++ b/extensions/music/src/test/java/app/morphe/extension/music/patches/lyrics/model/LyricsBracketSplitterTest.java @@ -0,0 +1,78 @@ +package app.morphe.extension.music.patches.lyrics.model; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +import java.util.List; + +public class LyricsBracketSplitterTest { + + private static Lyrics synced(LyricsLine... lines) { + return new Lyrics(List.of(lines), "test", true, null, null, null, null, null, null, null); + } + + private static LyricsLine background(long startMs, String text) { + return new LyricsLine(startMs, text, List.of(), null, false, true); + } + + @Test public void bracketedVocalMovesToALineOfItsOwn() { + List lines = LyricsBracketSplitter.apply( + synced(new LyricsLine(1000, "I want you (oh)"))).lines(); + assertEquals(2, lines.size()); + assertEquals("I want you", lines.get(0).text()); + assertFalse(lines.get(0).isBG()); + assertEquals("oh", lines.get(1).text()); + assertTrue(lines.get(1).isBG()); + } + + @Test public void wordTimingsFollowTheirSide() { + LyricsLine line = new LyricsLine(1000, 3000, "I want you (oh)", List.of( + new Word(1000, 1400, "I ", null, true), + new Word(1400, 1900, "want ", null, true), + new Word(1900, 2400, "you ", null, true), + new Word(2400, 3000, "(oh)"))); + List lines = LyricsBracketSplitter.apply(synced(line)).lines(); + assertEquals(2, lines.size()); + assertEquals("I want you", lines.get(0).text()); + assertEquals(3, lines.get(0).words().size()); + assertEquals("oh", lines.get(1).text()); + assertEquals(1, lines.get(1).words().size()); + assertEquals(2400, lines.get(1).startTimeMs()); + } + + @Test public void backgroundLineIsNotCutIntoALead() { + List lines = LyricsBracketSplitter.apply(synced( + new LyricsLine(0, "Hello"), background(500, "(ooh) yeah"))).lines(); + assertEquals(2, lines.size()); + assertTrue(lines.get(1).isBG()); + } + + @Test public void wholeBracketedLineBecomesBackgroundOfTheLeadAbove() { + List lines = LyricsBracketSplitter.apply(synced( + new LyricsLine(0, "Hello"), new LyricsLine(1000, "(Ooh, yeah)"))).lines(); + assertEquals(2, lines.size()); + assertEquals("Ooh, yeah", lines.get(1).text()); + assertTrue(lines.get(1).isBG()); + } + + @Test public void sectionNamesAndTagsStayAsTheyAre() { + List lines = LyricsBracketSplitter.apply(synced( + new LyricsLine(0, "Hello"), new LyricsLine(1000, "(Chorus)"), + new LyricsLine(2000, "【Live】 Hello (oh)"))).lines(); + assertEquals(3, lines.size()); + assertEquals("(Chorus)", lines.get(1).text()); + assertFalse(lines.get(1).isBG()); + assertEquals("【Live】 Hello (oh)", lines.get(2).text()); + } + + @Test public void singleWordLineWithWordTimingHasWordTimings() { + LyricsLine singleWord = new LyricsLine(1000, 2000, "Yeah", List.of(new Word(1000, 2000, "Yeah"))); + assertTrue(singleWord.hasWordTimings()); + + LyricsLine fakeWordSync = new LyricsLine(1000, 2000, "I want you", List.of(new Word(1000, 2000, "I want you"))); + assertFalse(fakeWordSync.hasWordTimings()); + } +} diff --git a/extensions/music/src/test/java/app/morphe/extension/music/patches/lyrics/model/TrackFormatTest.java b/extensions/music/src/test/java/app/morphe/extension/music/patches/lyrics/model/TrackFormatTest.java index 3b61ade3da..01e3efd9a7 100644 --- a/extensions/music/src/test/java/app/morphe/extension/music/patches/lyrics/model/TrackFormatTest.java +++ b/extensions/music/src/test/java/app/morphe/extension/music/patches/lyrics/model/TrackFormatTest.java @@ -30,7 +30,7 @@ public class TrackFormatTest { } @Test public void sectionNeedsEveryVariableInside() { - final String template = "{t}: [{v} by {a}]:"; + String template = "{t}: [{v} by {a}]:"; assertEquals("Duvet [Live by bôa]", MiniLyricsFormatter.trackLabel(template, "Duvet", "bôa", "Live")); assertEquals("Duvet", MiniLyricsFormatter.trackLabel(template, "Duvet", "bôa", null)); assertEquals("Duvet", MiniLyricsFormatter.trackLabel(template, "Duvet", "", "Live")); @@ -53,7 +53,7 @@ public class TrackFormatTest { } @Test public void longTrackLineIsSplitIntoFramesThatFit() { - final List frames = MiniLyricsFormatter.trackFrames("{t} - {a}", + List frames = MiniLyricsFormatter.trackFrames("{t} - {a}", "Smells Like Teen Spirit", "Nirvana", null, 10, 1); assertTrue(frames.size() > 2); for (String frame : frames) { @@ -64,12 +64,12 @@ public class TrackFormatTest { @Test public void splitIsDecidedFromTheWholeLine() { // Neither variable is over the limit, but the rendered line is. - final List frames = MiniLyricsFormatter.trackFrames("{t} - {a}", "Duvet", "bôa", null, 8, 1); + List frames = MiniLyricsFormatter.trackFrames("{t} - {a}", "Duvet", "bôa", null, 8, 1); assertEquals(List.of("Duvet -", "bôa"), frames); } @Test public void fullWidthCharactersCountAsTheMultiplier() { - final String six = "あいうえおか"; + String six = "あいうえおか"; assertEquals(6.0, MiniLyricsFormatter.width(six, 1), 0.001); assertEquals(10.8, MiniLyricsFormatter.width(six, 1.8), 0.001); assertEquals(1, MiniLyricsFormatter.chunks(new LyricsLine(0, six), 5000, 10, false, false, 1).size()); @@ -78,8 +78,8 @@ public class TrackFormatTest { } @Test public void unspacedSplitGetsOneEllipsis() { - final String text = "あいうえおかきくけこ"; - final List chunks = + String text = "あいうえおかきくけこ"; + List chunks = MiniLyricsFormatter.chunks(new LyricsLine(0, text), 5000, 5, false, false, 1); assertEquals(2, chunks.size()); assertTrue(chunks.get(0).text().endsWith(ELLIPSIS)); @@ -88,24 +88,24 @@ public class TrackFormatTest { } @Test public void spacedSplitHasNoEllipsis() { - final List chunks = MiniLyricsFormatter.chunks( + List chunks = MiniLyricsFormatter.chunks( new LyricsLine(0, "don't stop me now"), 5000, 10, false, false, 1); assertEquals("don't stop", chunks.get(0).text()); assertEquals("me now", chunks.get(1).text()); } @Test public void hyphenatedAndPunctuatedWordsStayWhole() { - final List chunks = MiniLyricsFormatter.chunks( + List chunks = MiniLyricsFormatter.chunks( new LyricsLine(0, "well-known words, again!"), 5000, 10, false, false, 1); assertEquals("well-known words,", chunks.get(0).text()); assertEquals("again!", chunks.get(1).text()); } @Test public void wordTimestampsDecideTheSplit() { - final String text = "今日はいい天気"; - final LyricsLine line = new LyricsLine(0, text, List.of( + String text = "今日はいい天気"; + LyricsLine line = new LyricsLine(0, text, List.of( new Word(0, 500, "今日は"), new Word(500, 1000, "いい"), new Word(1000, 1500, "天気"))); - final List chunks = MiniLyricsFormatter.chunks(line, 2000, 3, false, false, 1); + List chunks = MiniLyricsFormatter.chunks(line, 2000, 3, false, false, 1); assertEquals(2, chunks.size()); assertEquals("今日はいい" + ELLIPSIS, chunks.get(0).text()); assertEquals("天気", chunks.get(1).text()); diff --git a/patches/src/main/kotlin/app/morphe/patches/music/layout/lyrics/LyricsPatch.kt b/patches/src/main/kotlin/app/morphe/patches/music/layout/lyrics/LyricsPatch.kt index 3a2ff28e78..a6d7f2cfe2 100644 --- a/patches/src/main/kotlin/app/morphe/patches/music/layout/lyrics/LyricsPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/music/layout/lyrics/LyricsPatch.kt @@ -118,7 +118,6 @@ val lyricsPatch = bytecodePatch( dependency = "morphe_music_lyrics_enabled" ), SwitchPreference("morphe_music_lyrics_word_sync", summary = true), - SwitchPreference("morphe_music_lyrics_split_background", summary = true), SwitchPreference("morphe_music_lyrics_show_section", summary = true), SwitchPreference("morphe_music_lyrics_hide_played", summary = true), SwitchPreference("morphe_music_lyrics_hide_unplayed", summary = true), diff --git a/patches/src/main/resources/addresources/values-af-rZA/music/strings.xml b/patches/src/main/resources/addresources/values-af-rZA/music/strings.xml index 6860b1a72e..3b6a9e5ac6 100644 --- a/patches/src/main/resources/addresources/values-af-rZA/music/strings.xml +++ b/patches/src/main/resources/addresources/values-af-rZA/music/strings.xml @@ -540,8 +540,6 @@ Vereiste koekies: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPISID Gestoor Raak aan ▲/▼ om die prioriteitsorde te stel, en die blokkie om elke verskaffer te aktiveer of deaktiveer. Alle verskaffers gedeaktiveer — ingeboude lirieke en ingebedde lirieke. - Wys agtergrondvokale afsonderlik - Verskuif \'n agtergrond-vokaal wat in hakies geskryf is, soos \"(Oh) ek wil hê jy\", uit die gesong lyn na \'n eie dowwer lyn Verskafferorde terugstel? Versteek liriekreëls bo die reël wat tans speel Versteek ongespeelde lirieke diff --git a/patches/src/main/resources/addresources/values-ar-rSA/music/strings.xml b/patches/src/main/resources/addresources/values-ar-rSA/music/strings.xml index e6841edb23..3c486eca83 100644 --- a/patches/src/main/resources/addresources/values-ar-rSA/music/strings.xml +++ b/patches/src/main/resources/addresources/values-ar-rSA/music/strings.xml @@ -530,8 +530,6 @@ tظهر المسارات التي تم تنزيلها هنا" محفوظة قم بتمييز الكلمات والمقاطع اللفظية المتزامنة أعطِ الأولوية للكلمات المتزامنة كلمةً بكلمة عند توفرها. وعندما تحمل الكلمات توقيتًا للكلمات، يتم تمييز كل كلمة أو مقطع في توقيت مع الموسيقى. - إظهار الأغاني الخلفية بشكل منفصل - يُحرك غناء الخلفية المكتوب بين قوسين، مثل \"(واو) أريدك\"، من سطر الغناء إلى سطر خاص به بأقل وضوح إخفاء السطر الجاري تشغيله إخفاء سطور الكلمات الموجودة فوق السطر الحالي إخفاء الكلمات غير المشغّلة diff --git a/patches/src/main/resources/addresources/values-az-rAZ/music/strings.xml b/patches/src/main/resources/addresources/values-az-rAZ/music/strings.xml index 8a71045109..32fb0e5624 100644 --- a/patches/src/main/resources/addresources/values-az-rAZ/music/strings.xml +++ b/patches/src/main/resources/addresources/values-az-rAZ/music/strings.xml @@ -545,8 +545,6 @@ Fon səsləndirmə zamanı analiz sinxronlaşdırılmasına əmin olmaq üçün Saxlanıldı Sinxronlaşdırılmış sözləri və ya hecaları vurğula Mümkün olduqda sözbəsöz sözlərə üstünlük verir. Sözlərdə zamanlama olduqda, musiqi ilə eyni vaxtda hər bir sözü və ya hecanı vurğulayır - Fon vokallarını ayrıca göstər - ​\"(Oh) I want you\" kimi mötərizədə yazılmış arxa plan vokalını oxunan sətirdən çıxararaq ayrı, solğun sətirə keçirir Oxudulan sözləri gizlət Mövcud oxunan sətirdən yuxarıdakı söz sətirlərini gizlət Oxudulmamış sözləri gizlət diff --git a/patches/src/main/resources/addresources/values-bg-rBG/music/strings.xml b/patches/src/main/resources/addresources/values-bg-rBG/music/strings.xml index ccb69b6829..4c2dfa6113 100644 --- a/patches/src/main/resources/addresources/values-bg-rBG/music/strings.xml +++ b/patches/src/main/resources/addresources/values-bg-rBG/music/strings.xml @@ -531,8 +531,6 @@ SABR възпроизвеждане (имитирането на видео по Запазено Открояване на синхронизираните думи или срички Дава приоритет на текст по думи, когато е наличен. Когато текстът има тайминг на думите, откроява всяка дума или сричка в синхрон с музиката. - Показвай фоновите вокали отделно - Премества фонови вокали, написани в скоби, като „(Ох) искам те“, извън пеената линия към по-бляскава линия на собствената си Скриване на вече изпетите редове Скриване на редовете от текста над реда, който се изпълнява в момента. Скриване на неизпетите редове diff --git a/patches/src/main/resources/addresources/values-bn-rBD/music/strings.xml b/patches/src/main/resources/addresources/values-bn-rBD/music/strings.xml index 96b85dd144..a7ca706aeb 100644 --- a/patches/src/main/resources/addresources/values-bn-rBD/music/strings.xml +++ b/patches/src/main/resources/addresources/values-bn-rBD/music/strings.xml @@ -540,8 +540,6 @@ SABR প্লেব্যাক (ভিডিও স্ট্রিম স্ সংরক্ষিত হয়েছে সিঙ্ক্রোনাইজড শব্দ বা সিলেবল হাইলাইট করুন যখন পাওয়া যায়, শব্দে শব্দে লিরিক্সকে অগ্রাধিকার দিন। যখন লিরিক্সে শব্দের সময় থাকে, তখন সঙ্গীতের সাথে সময় মিলিয়ে প্রতিটি শব্দ বা সিলেবল হাইলাইট হয়। - ব্যাকগ্রাউন্ড ভোক্যাল আলাদাভাবে দেখান - \"(Oh)\" ইচ্ছুক অংশ বন্ধনীতে লেখা ব্যাকগ্রাউন্ড ভোক্যালকে গানের লাইন থেকে সরিয়ে একটি নিঃস্ব যমের রেখে দেয় লিরিক্স লুকান […] বর্তমানে বাজতে থাকা লাইনের উপরের লিরিক্সের লাইনগুলো লুকান যে লিরিক্স এখনো বাজানো হয়নি সেগুলো লুকান diff --git a/patches/src/main/resources/addresources/values-cs-rCZ/music/strings.xml b/patches/src/main/resources/addresources/values-cs-rCZ/music/strings.xml index 03da3a06db..8cf5476fd4 100644 --- a/patches/src/main/resources/addresources/values-cs-rCZ/music/strings.xml +++ b/patches/src/main/resources/addresources/values-cs-rCZ/music/strings.xml @@ -540,8 +540,6 @@ Požadované cookies: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAP Uloženo Zvýrazňovat synchronizovaná slova nebo slabiky Upřednostnit texty slovo po slově, pokud jsou k dispozici. Když texty obsahují načasování slov, zvýrazní každé slovo nebo slabiku v rytmu hudby. - Zobrazit vokály pozadí samostatně - Přesune pozadí vokálu uvedeného v závorkách, například „(Oh) chci tě“, z původní zpěvné linky do samostatné tišší linky Skrýt […] texty. Skrýt řádky textu nad aktuálně přehrávaným řádkem Skrýt nepřehrávané texty diff --git a/patches/src/main/resources/addresources/values-da-rDK/music/strings.xml b/patches/src/main/resources/addresources/values-da-rDK/music/strings.xml index 5ea8157b42..d34ab78f5f 100644 --- a/patches/src/main/resources/addresources/values-da-rDK/music/strings.xml +++ b/patches/src/main/resources/addresources/values-da-rDK/music/strings.xml @@ -539,8 +539,6 @@ Krævede cookies: SID, HSID, SSID, SAPISID, __Secure-3PAPISID" Gemt Fremhæv synkroniserede ord eller stavelser Prioritér ord-for-ord-tekster, når de er tilgængelige. Når teksterne har ord-timing, fremhæves hvert ord eller stavelse i takt med musikken. - Vis baggrundsvokaler separat - Flytter en baggrundsvokal, der er skrevet i parenteser, som f.eks. \"(Oh) I want you\", ud af den sunge linje og over i en egen, svagere linje Skjul […] linje Skjul sangtekstlinjer over den aktuelt afspillede linje Skjul ikke-afspillede tekster diff --git a/patches/src/main/resources/addresources/values-de-rDE/music/strings.xml b/patches/src/main/resources/addresources/values-de-rDE/music/strings.xml index fd9d055f20..b8281bddbf 100644 --- a/patches/src/main/resources/addresources/values-de-rDE/music/strings.xml +++ b/patches/src/main/resources/addresources/values-de-rDE/music/strings.xml @@ -540,8 +540,6 @@ Erforderliche Cookies: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PA Gespeichert Hebe synchronisierte Wörter oder Silben hervor. Bevorzuge wortweise Songtexte, wenn verfügbar. Wenn Songtexte Wort-Timing enthalten, hebt dies jedes Wort oder jede Silbe synchron zur Musik hervor. - Hintergrundstimmen separat anzeigen - Verschiebt einen im Klammern notierten Hintergrundgesang, z. B. \"(Oh) ich will dich\", aus der gesungenen Zeile in eine eigene, gedimmte Zeile Nicht gespielte Songtexte ausblenden. Songtextzeilen oberhalb der aktuell abgespielten Zeile ausblenden Noch nicht abgespielte Songtextzeilen ausblenden diff --git a/patches/src/main/resources/addresources/values-el-rGR/music/strings.xml b/patches/src/main/resources/addresources/values-el-rGR/music/strings.xml index d09520ab93..8f4c2c8a14 100644 --- a/patches/src/main/resources/addresources/values-el-rGR/music/strings.xml +++ b/patches/src/main/resources/addresources/values-el-rGR/music/strings.xml @@ -540,8 +540,6 @@ Second \"item\" text" Αποθηκεύτηκε Επισήμανση συγχρονισμένων λέξεων ή συλλαβών Δώστε προτεραιότητα στους στίχους λέξη-λέξη όταν είναι διαθέσιμοι. Όταν οι στίχοι έχουν χρονισμό λέξεων, επισημαίνεται κάθε λέξη ή συλλαβή σε συγχρονισμό με τη μουσική. - Εμφάνιση των φωνητικών φόντου ξεχωριστά - Μετακινεί ένα φωνητικό υπόβαθρο που γράφεται σε αγκύλες, όπως \"(Oh) I want you\", εκτός της τραγουδισμένης γραμμής σε μια ξεχωριστή γραμμή με μικρότερη ένταση Απόκρυψη γραμμής που έχει παιχτεί. Απόκρυψη γραμμών στίχων πάνω από τον τρέχοντα στίχο Απόκρυψη στίχων που δεν έχουν παιχτεί. diff --git a/patches/src/main/resources/addresources/values-es-rES/music/strings.xml b/patches/src/main/resources/addresources/values-es-rES/music/strings.xml index f5d3e33c5d..cce19fff2c 100644 --- a/patches/src/main/resources/addresources/values-es-rES/music/strings.xml +++ b/patches/src/main/resources/addresources/values-es-rES/music/strings.xml @@ -545,8 +545,6 @@ Cookies requeridas: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPIS Guardado Resaltar palabras o sílabas sincronizadas Prioriza las letras palabra por palabra cuando estén disponibles. Cuando las letras incluyen tiempos por palabra, resalta cada palabra o sílaba al ritmo de la música - Mostrar coros por separado - Mueve una línea de coros de fondo escrita entre paréntesis, como \"(Oh) te quiero\", fuera de la línea principal cantada a una línea propia con menor intensidad Ocultar letras ya reproducidas Oculta las líneas de letra por encima de la línea que se está reproduciendo Ocultar letras no reproducidas diff --git a/patches/src/main/resources/addresources/values-fa-rIR/music/strings.xml b/patches/src/main/resources/addresources/values-fa-rIR/music/strings.xml index a5c29499a3..b8328daa60 100644 --- a/patches/src/main/resources/addresources/values-fa-rIR/music/strings.xml +++ b/patches/src/main/resources/addresources/values-fa-rIR/music/strings.xml @@ -545,8 +545,6 @@ Second \"item\" text" ذخیره‌شده برجسته‌کردن واژه‌ها یا هجاهای همگام‌شده اولویت دادن به متن ترانهٔ واژه‌به‌واژه در صورت وجود. وقتی متن ترانه زمان‌بندی واژه دارد، هر واژه یا هجا را هماهنگ با موسیقی برجسته می‌کند - نمایش صداهای پس‌زمینه به صورت جداگانه - یک خط خواندن پس‌زمینه را که در پرانتز نوشته شده است، مانند «(آه) من تو را می‌خواهم»، از خط اصلی خوانده‌شده خارج کرده و به یک خط جداگانه با شدت کمتر منتقل می‌کند پنهان‌کردن متن ترانه‌های پخش‌شده پنهان‌کردن خطوط متن ترانه بالای خطِ در حال پخش پنهان‌کردن متن ترانه‌های پخشنشده diff --git a/patches/src/main/resources/addresources/values-fi-rFI/music/strings.xml b/patches/src/main/resources/addresources/values-fi-rFI/music/strings.xml index aae94db1e2..d5dd235bef 100644 --- a/patches/src/main/resources/addresources/values-fi-rFI/music/strings.xml +++ b/patches/src/main/resources/addresources/values-fi-rFI/music/strings.xml @@ -540,8 +540,6 @@ Vaaditut evästeet: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPIS Tallennettu Korosta synkronoidut sanat tai tavut Priorisoi sana sanalta -lyriikat, kun ne ovat saatavilla. Kun lyriikoissa on sanan ajoitus, korostaa jokaisen sanan tai tavun musiikin tahdissa - Näytä taustalaulu erikseen - Siirtää sulkuihin kirjoitetun taustalaulun, kuten \"(Oh) I want you\", laulun sävellysviivan ulkopuolelle omaksi himmeämmäksi rivikseen Piilota soitetut lyriikat Piilota lyriikkarivit nykyisen soittorivin yläpuolelta Piilota soittamattomat lyriikat diff --git a/patches/src/main/resources/addresources/values-fil-rPH/music/strings.xml b/patches/src/main/resources/addresources/values-fil-rPH/music/strings.xml index 1b36685887..6e0a932650 100644 --- a/patches/src/main/resources/addresources/values-fil-rPH/music/strings.xml +++ b/patches/src/main/resources/addresources/values-fil-rPH/music/strings.xml @@ -540,8 +540,6 @@ Mga kinakailangang cookie: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure Na-save I-highlight ang mga salita o pantig na naka-sync Unahin ang word-by-word na lyrics kung available. Kapag may word timing ang lyrics, ine-highlight ang bawat salita o pantig na kasabay ng musika. - Ipakita hiwalay ang background vocals - Gine-likas ang background na bokol na nakasulat sa bracket, tulad ng \"(Oh) gusto kita\", mula sa linya ng kanta upang maging hiwalay na linya na may mas mahinaw na tunog Itago ang linyang na-play na. Itago ang mga linya ng lyrics sa itaas ng linyang kasalukuyang pinapatugtog Itago ang mga lyrics na hindi pa na-play. diff --git a/patches/src/main/resources/addresources/values-fr-rFR/music/strings.xml b/patches/src/main/resources/addresources/values-fr-rFR/music/strings.xml index 7610721258..f5719f7626 100644 --- a/patches/src/main/resources/addresources/values-fr-rFR/music/strings.xml +++ b/patches/src/main/resources/addresources/values-fr-rFR/music/strings.xml @@ -545,8 +545,6 @@ Cookies nécessaires : SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PA Enregistré Mettre en surbrillance les mots ou les syllabes synchronisés Privilégier les paroles mot à mot lorsqu\'elles sont disponibles. Lorsque les paroles disposent d\'un minutage mot à mot, mettre en surbrillance chaque mot ou chaque syllabe en rythme avec la musique - Afficher les voix d\'accompagnement séparément - Déplace une voix d\'accompagnement écrite entre parenthèses, comme \"(Oh) I want you\", hors de la ligne chantée pour l\'afficher sur sa propre ligne de façon plus discrète Masquer les paroles déjà lues Masquer les lignes de paroles au-dessus de la ligne en cours de lecture Masquer les paroles non lues diff --git a/patches/src/main/resources/addresources/values-ga-rIE/music/strings.xml b/patches/src/main/resources/addresources/values-ga-rIE/music/strings.xml index 75d665e7e3..36e80b1d99 100644 --- a/patches/src/main/resources/addresources/values-ga-rIE/music/strings.xml +++ b/patches/src/main/resources/addresources/values-ga-rIE/music/strings.xml @@ -545,8 +545,6 @@ Fianáin riachtanacha: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PA Sábháilte Aibhsigh focail nó siollaí sioncronaithe Tabhair tosaíocht do liricí a chuirtear i láthair focal ar fhocal nuair a bhíonn siad ar fáil. Nuair a bhíonn uainiú focal ag gabháil leis na liricí, déantar gach focal nó siolla a aibhsiú i gcomhthráth leis an gceol - Taispeáin ceol cúlra go hiomlán ar leithligh - Bogann sé guth cúlra atá scríofa idir lúibíní — mar shampla \"(Oh) I want you\" — amach as an líne a chantar, á chur i líne ar leith atá níos séimhe. Folaigh na liricí a seinneadh Folaigh línte na liricí os cionn na líne atá á seinm faoi láthair Folaigh liricí nár seinneadh diff --git a/patches/src/main/resources/addresources/values-hi-rIN/music/strings.xml b/patches/src/main/resources/addresources/values-hi-rIN/music/strings.xml index cf13d66e29..97ad7d64f8 100644 --- a/patches/src/main/resources/addresources/values-hi-rIN/music/strings.xml +++ b/patches/src/main/resources/addresources/values-hi-rIN/music/strings.xml @@ -540,8 +540,6 @@ SABR प्लेबैक (स्पूफ वीडियो स्ट्र सहेजा गया सिंक किए गए शब्दों या अक्षरों को हाइलाइट करें जब उपलब्ध हों तो शब्द-दर-शब्द गीतों को प्राथमिकता दें। जब गीतों में शब्द टाइमिंग होती है, तो संगीत के साथ समय पर प्रत्येक शब्द या अक्षर को हाइलाइट करता है - पृष्ठभूमि गायन को अलग से दिखाएं - कोष्ठक में लिखी पृष्ठभूमि वोकल, जैसे \"(ओह) मैं तुम चाहता हूँ\", को गाए गए लाइन से बाहर निकालकर अपनी खुद की धीमी लाइन में स्थानांतरित करता है बजाए गए गीत छिपाएँ वर्तमान में चल रही पंक्ति के ऊपर की गीत पंक्तियों को छिपाएँ नहीं बजाए गए गीत छिपाएँ diff --git a/patches/src/main/resources/addresources/values-hr-rHR/music/strings.xml b/patches/src/main/resources/addresources/values-hr-rHR/music/strings.xml index ea13580b45..f4cc29064a 100644 --- a/patches/src/main/resources/addresources/values-hr-rHR/music/strings.xml +++ b/patches/src/main/resources/addresources/values-hr-rHR/music/strings.xml @@ -545,8 +545,6 @@ Potrebni kolačići: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPI Spremljeno Istakni sinkronizirane riječi ili slogove Daj prednost tekstovima pjesama riječ po riječ kada su dostupni. Kada tekstovi nose vremenske oznake riječi, istakni svaku riječ ili slog u ritmu s glazbom. - Prikažite pozadinske vokale odvojeno - Premješta vokale pozadine upisane u zagradama, poput \"(Oh) I want you\", iz pjevne dionice u vlastitu liniju manjeg intenziteta Sakrij reproducirane tekstove pjesama Sakrij retke teksta pjesme iznad trenutačno reproduciranog retka. Sakrij nereproducirane tekstove pjesama diff --git a/patches/src/main/resources/addresources/values-hu-rHU/music/strings.xml b/patches/src/main/resources/addresources/values-hu-rHU/music/strings.xml index 379a1ab749..81cb235de7 100644 --- a/patches/src/main/resources/addresources/values-hu-rHU/music/strings.xml +++ b/patches/src/main/resources/addresources/values-hu-rHU/music/strings.xml @@ -545,8 +545,6 @@ Szükséges cookie-k: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAP Mentett Szinkronizált szavak vagy szótagok kiemelése Előnyben részesíti a szavankénti dalszövegeket, ha elérhetők. Ha a dalszöveg szószintű időzítést tartalmaz, a zene ütemére emeli ki az egyes szavakat vagy szótagokat. - A háttérvokál megjelenítése külön - A zárójelben megadott háttérvokált (például: „(Óh) téged szeretlek”) kiveszi a dalos sorból, és elkülönít egy halkabb, saját sort hoz létre neki. A lejátszott sor elrejtése Minilejátszó dalszövegének megjelenítése A még el nem hangzott dalszövegek elrejtése diff --git a/patches/src/main/resources/addresources/values-in-rID/music/strings.xml b/patches/src/main/resources/addresources/values-in-rID/music/strings.xml index 15406b3f5e..b4bda7be82 100644 --- a/patches/src/main/resources/addresources/values-in-rID/music/strings.xml +++ b/patches/src/main/resources/addresources/values-in-rID/music/strings.xml @@ -545,8 +545,6 @@ Cookie yang diperlukan: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1P Tersimpan Sorot kata atau suku kata yang disinkronkan Utamakan lirik kata per kata jika tersedia. Setiap kata atau suku kata akan disorot sesuai irama musik. - Tampilkan vokal latar secara terpisah - Memindahkan vokal latar belakang yang ditulis dalam tanda kurung, seperti \"(Oh) saya ingin kamu\", dari baris lagu ke baris redupnya sendiri Sembunyikan lirik yang telah diputar Sembunyikan baris lirik di atas garis yang sedang diputar Sembunyikan lirik yang belum diputar diff --git a/patches/src/main/resources/addresources/values-is-rIS/music/strings.xml b/patches/src/main/resources/addresources/values-is-rIS/music/strings.xml index d0b3f6dd45..d50d0053ea 100644 --- a/patches/src/main/resources/addresources/values-is-rIS/music/strings.xml +++ b/patches/src/main/resources/addresources/values-is-rIS/music/strings.xml @@ -543,8 +543,6 @@ Nauðsynlegar vafrakökur: SID, HSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPI Vistað Auðkenna samstillt orð eða atkvæði Sérsníddu texta til að sýna lög og frumtexta. - Birta bakraddir sérstaklega - Færir bakraddað söngorð sem eru í svigum, eins og \"(Ó)\", úr sönglínunni yfir á eigin deyfðari línu Fela spilaða texta Fela textalínur fyrir ofan línuna sem spilast núna Fela óspilaða texta diff --git a/patches/src/main/resources/addresources/values-it-rIT/music/strings.xml b/patches/src/main/resources/addresources/values-it-rIT/music/strings.xml index f3fe1cdbb0..83300dc28c 100644 --- a/patches/src/main/resources/addresources/values-it-rIT/music/strings.xml +++ b/patches/src/main/resources/addresources/values-it-rIT/music/strings.xml @@ -539,8 +539,6 @@ Cookie richiesti: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPISID Salvato Lingua dei testi Mostra i testi - Mostrare le vocali di sottofondo separatamente - Sposta il coro di sottofondo scritto tra parentesi, ad esempio \"(Oh) ti voglio\", dalla parte cantata in una riga distinta con minore intensità Testi sincronizzati Nascondi le righe dei testi sopra la riga attualmente in riproduzione Copia i testi diff --git a/patches/src/main/resources/addresources/values-iw-rIL/music/strings.xml b/patches/src/main/resources/addresources/values-iw-rIL/music/strings.xml index f578a8ed96..0403eaabbb 100644 --- a/patches/src/main/resources/addresources/values-iw-rIL/music/strings.xml +++ b/patches/src/main/resources/addresources/values-iw-rIL/music/strings.xml @@ -545,8 +545,6 @@ Second \"item\" text" נשמר הדגש מילים או הברות מסונכרנות תעדף מילים מילה-מילה כשהן זמינות. כשמילות השיר כוללות תזמון מילים, הדגש כל מילה או הברה בקצב המוזיקה - הצג ווקאלים ברקע בנפרד - מעביר ווקאל רקע הכתוב בסוגריים, כמו \"(או) אני רוצה אותך\", מקו השיר לקו עצמאי בעוצמה נמוכה יותר הסתר מילים שכבר נוגנו הסתר שורות מילים מעל השורה המושמעת כעת הסתר מילים שטרם נוגנו diff --git a/patches/src/main/resources/addresources/values-ja-rJP/music/strings.xml b/patches/src/main/resources/addresources/values-ja-rJP/music/strings.xml index a31113aba2..029e99e7e7 100644 --- a/patches/src/main/resources/addresources/values-ja-rJP/music/strings.xml +++ b/patches/src/main/resources/addresources/values-ja-rJP/music/strings.xml @@ -538,8 +538,6 @@ SABR再生(動画ストリームのスプーフがオフ)" 保存しました 同期中の単語や音節をハイライト表示 利用可能な場合は単語ごとのタイミング情報を含む歌詞を優先します。単語ごとのタイミング情報を含む歌詞を使用する場合は、楽曲に合わせて各単語または音節をハイライト表示します - バックコーラスを分離して表示 - 「(Oh) I want you」のように括弧で囲まれたバックコーラスを、歌詞の行から分離し、専用の独立した行に薄く表示します 再生済みの歌詞を非表示 現在再生中の行より上に表示される歌詞を非表示にします 未再生の歌詞を非表示 diff --git a/patches/src/main/resources/addresources/values-ko-rKR/music/strings.xml b/patches/src/main/resources/addresources/values-ko-rKR/music/strings.xml index 78e9ad9441..314f3e1bcb 100644 --- a/patches/src/main/resources/addresources/values-ko-rKR/music/strings.xml +++ b/patches/src/main/resources/addresources/values-ko-rKR/music/strings.xml @@ -547,8 +547,6 @@ LOGIN_INFO" 저장됨 동기화된 단어 또는 음절 강조 표시하기 가능한 경우 단어 단위로 표시되는 가사를 우선합니다. 가사에 단어별 타이밍 정보가 포함되어 있으면, 음악에 맞춰 각 단어 음절을 순서대로 강조 표시합니다 - 백그라운드 보컬 별도 표시하기 - \'(Oh) I want you\'와 같이 괄호로 작성된 백그라운드 보컬을 가사 줄에서 분리하여, 별도의 흐릿한 줄로 이동합니다 재생된 가사 숨기기 현재 재생 중인 줄 위의 가사 줄을 숨깁니다 재생되지 않은 가사 숨기기 diff --git a/patches/src/main/resources/addresources/values-lt-rLT/music/strings.xml b/patches/src/main/resources/addresources/values-lt-rLT/music/strings.xml index 20430fe6f0..5c8f358af7 100644 --- a/patches/src/main/resources/addresources/values-lt-rLT/music/strings.xml +++ b/patches/src/main/resources/addresources/values-lt-rLT/music/strings.xml @@ -545,8 +545,6 @@ Reikalingi slapukai: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPI Išsaugota Paryškinti sinchronizuotus žodžius ar skiemenis Teikti pirmenybę žodis po žodžio sinchronizuotiems dainų tekstams, kai jie prieinami. Kai dainų tekstai turi žodžių laiką, kiekvienas žodis ar skiemuo paryškinamas pagal muziką - Rodyti fono vokalus atskirai - Perkelia foninį vokalinį tekstą, esantį skliaustuose, pvz., „(Oh) I want you“, iš pagrindinio kūrio teksto atskira švelnesne eilute Slėpti jau atkurtus dainų tekstus Slėpti dainų teksto eilutes virš šiuo metu grojamos eilutės Slėpti neatkurtus dainų tekstus diff --git a/patches/src/main/resources/addresources/values-nb-rNO/music/strings.xml b/patches/src/main/resources/addresources/values-nb-rNO/music/strings.xml index d5f7bf0ac1..75a8a8fcc2 100644 --- a/patches/src/main/resources/addresources/values-nb-rNO/music/strings.xml +++ b/patches/src/main/resources/addresources/values-nb-rNO/music/strings.xml @@ -543,8 +543,6 @@ Nødvendige informasjonskapsler: SID, HSID, SSID, SAPISID, __Secure-1PAPISID, __ Lagret Marker synkroniserte ord eller stavelser Prioriter ord-for-ord-tekster når de er tilgjengelige. Når teksten har tidsstempling for ord, vises hvert ord eller hver stavelse synkronisert med musikken - Vis bakgrunnsvokaler separat - Flytter en bakgrunnsvokal skrevet i klammer, slik som \"(Å) jeg vil ha deg\", ut av den sunge linjen og over i en egen, svakere linje Skjul […] linje Skjul sangtekstlinjene over linjen som spilles for øyeblikket Skjul ikke-avspilte tekster diff --git a/patches/src/main/resources/addresources/values-nl-rNL/music/strings.xml b/patches/src/main/resources/addresources/values-nl-rNL/music/strings.xml index ee833fbc5c..702c4d1e9e 100644 --- a/patches/src/main/resources/addresources/values-nl-rNL/music/strings.xml +++ b/patches/src/main/resources/addresources/values-nl-rNL/music/strings.xml @@ -535,8 +535,6 @@ Vereiste cookies: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPISID Opgeslagen Gesynchroniseerde woorden of lettergrepen markeren Geef prioriteit aan woord-voor-woord songteksten wanneer beschikbaar. Wanneer songteksten woordtiming bevatten, wordt elk woord of elke lettergreep gemarkeerd in de maat met de muziek - Achtergrondvocalen apart weergeven - Verplaatst een achtergrondzang die tussen haakjes staat, zoals \"(Oh) I want you\", uit de gezongen regel naar een aparte zwakkere regel Verberg […] regel Verberg songtekstregels boven de regel die momenteel wordt afgespeeld Verberg niet-afgespeelde songteksten diff --git a/patches/src/main/resources/addresources/values-pl-rPL/music/strings.xml b/patches/src/main/resources/addresources/values-pl-rPL/music/strings.xml index 6702478320..4f22f46ccf 100644 --- a/patches/src/main/resources/addresources/values-pl-rPL/music/strings.xml +++ b/patches/src/main/resources/addresources/values-pl-rPL/music/strings.xml @@ -545,8 +545,6 @@ Wymagane ciasteczka: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPI Zapisano Podświetlaj zsynchronizowane słowa lub sylaby Nadaje priorytet tekstom synchronizowanym słowo po słowie, o ile są dostępne. Gdy tekst zawiera znaczniki czasu, podświetla każde słowo lub sylabę w rytm muzyki - Pokaż wokal w tle osobno - Przenosi wokal w tle zapisany w nawiasach, np. \"(Och) Chcę cię\", z głównego wersu do osobnej, przyciemnionej linii Ukryj odtworzony tekst Ukrywa linijki tekstu znajdujące się powyżej tej aktualnie odtwarzanej Ukryj nieodtworzony tekst diff --git a/patches/src/main/resources/addresources/values-pt-rBR/music/strings.xml b/patches/src/main/resources/addresources/values-pt-rBR/music/strings.xml index f23a9ca00e..e7e5658587 100644 --- a/patches/src/main/resources/addresources/values-pt-rBR/music/strings.xml +++ b/patches/src/main/resources/addresources/values-pt-rBR/music/strings.xml @@ -535,8 +535,6 @@ Cookies necessários: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAP Salvo Realçar palavras ou sílabas sincronizadas. Priorizar letras palavra por palavra quando disponíveis. Quando as letras têm sincronização por palavra, destaca cada palavra ou sílaba no tempo da música. - Mostrar vocais de fundo separadamente - Move uma vocal de fundo escrito entre parênteses, como \"(Oh) eu quero você\", da linha cantada para uma linha própria com menor volume Ocultar […] linha Ocultar linhas da letra acima da linha atualmente em reprodução Ocultar letras não reproduzidas. diff --git a/patches/src/main/resources/addresources/values-pt-rPT/music/strings.xml b/patches/src/main/resources/addresources/values-pt-rPT/music/strings.xml index 278f944a22..ac3c748769 100644 --- a/patches/src/main/resources/addresources/values-pt-rPT/music/strings.xml +++ b/patches/src/main/resources/addresources/values-pt-rPT/music/strings.xml @@ -545,8 +545,6 @@ Cookies necessários: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAP Guardado Destacar palavras ou sílabas sincronizadas Priorizar letras sincronizadas em nível de palavra - Mostrar vocais de fundo separadamente - Remove uma vocalização de fundo escrita entre parênteses, como \"(Oh) eu quero você\", da linha cantada e a coloca em sua própria linha mais suave Ocultar a linha reproduzida Ocultar linhas da letra acima da linha em reprodução Ocultar letras não reproduzidas diff --git a/patches/src/main/resources/addresources/values-ro-rRO/music/strings.xml b/patches/src/main/resources/addresources/values-ro-rRO/music/strings.xml index c5af8ecc9f..b746a55b31 100644 --- a/patches/src/main/resources/addresources/values-ro-rRO/music/strings.xml +++ b/patches/src/main/resources/addresources/values-ro-rRO/music/strings.xml @@ -528,8 +528,6 @@ Cookie-uri necesare: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPI Salvat Evidențiați cuvintele sau silabele sincronizate Nu s-a putut face romanizarea versurilor. - Afișați vocalurile de fundal separat - Mută vocea de fundal scrisă între paranteze, cum ar fi \"(Oh) te vreau\", din linia cântată într-o linie mai slabă Ascundeți versurile. Ascundeți rândurile de versuri de mai jos. Ascundeți versurile necunoscute. diff --git a/patches/src/main/resources/addresources/values-ru-rRU/music/strings.xml b/patches/src/main/resources/addresources/values-ru-rRU/music/strings.xml index 0b1566bacc..c55faf92cf 100644 --- a/patches/src/main/resources/addresources/values-ru-rRU/music/strings.xml +++ b/patches/src/main/resources/addresources/values-ru-rRU/music/strings.xml @@ -540,8 +540,6 @@ SABR-воспроизведение (подмена видеопотоков о Сохранено Подсвечивать синхронизированные слова по мере воспроизведения, если доступно Отдавать приоритет посекундному тексту, когда он доступен. Когда в тексте есть тайминги слов, подсвечивать каждое слово или слог в такт музыке. - Показывать бэк-вокал отдельно - Переносит фоновый вокал, записанный в скобках, например \"(О) я хочу тебя\", из основной строки в отдельную слабую строку Скрывать воспроизведённые строки Скрывать строки текста песни выше текущей воспроизводимой строки Скрывать невоспроизведённые строки diff --git a/patches/src/main/resources/addresources/values-sk-rSK/music/strings.xml b/patches/src/main/resources/addresources/values-sk-rSK/music/strings.xml index df8dbbe23b..8d4b5248f7 100644 --- a/patches/src/main/resources/addresources/values-sk-rSK/music/strings.xml +++ b/patches/src/main/resources/addresources/values-sk-rSK/music/strings.xml @@ -540,8 +540,6 @@ Vyžadované cookies: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAP Uložené Kopírovať pôvodný text piesne Kopírovať preložený text piesne - Zobraziť pozadové vokály samostatne - Presunie vokál v pozadí zapísaný v zátvorkách, napríklad \"(Ó) chcem ťa\", zo speváckej línie do vlastnej tichšej línie Pôvodný text piesne Preložený text piesne Preklad bol skopírovaný diff --git a/patches/src/main/resources/addresources/values-sl-rSI/music/strings.xml b/patches/src/main/resources/addresources/values-sl-rSI/music/strings.xml index 18add0c473..04a35359b9 100644 --- a/patches/src/main/resources/addresources/values-sl-rSI/music/strings.xml +++ b/patches/src/main/resources/addresources/values-sl-rSI/music/strings.xml @@ -531,8 +531,6 @@ Zahtevani piškotki: SID, HSID, SSID, SAPISID, __Secure-3APISID, __Secure-1APISI Shranjeno Poudarite sinhronizirane besede ali zloge Dajte prednost besedilom po posameznih besedah, kadar so na voljo. Ko ima besedilo časovne podatke za besede, poudari vsako besedo ali zlog v skladu z glasbo. - Prikaži spremljalni vokal ločeno - Premakne vokale v oklepajih, kot je \"(Oh) I want you\", iz glavne besedila v ločeno, tišjo črto Skrijte že predvajane vrstice besedila Skrijte vrstice besedila nad trenutno predvajano vrstico. Skrijte še nepredvane vrstice besedila diff --git a/patches/src/main/resources/addresources/values-sr-rCS/music/strings.xml b/patches/src/main/resources/addresources/values-sr-rCS/music/strings.xml index 891447f349..6d7ec60782 100644 --- a/patches/src/main/resources/addresources/values-sr-rCS/music/strings.xml +++ b/patches/src/main/resources/addresources/values-sr-rCS/music/strings.xml @@ -536,8 +536,6 @@ Potrebni kolačići: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPI Tekst se traži po naslovu numere i izvođaču, […] Možete i da pritisnete dugme za osvežavanje da biste ručno promenili verzije. […] Svi omogućeni izvori se ne traže uvek po vašem redosledu, već odgovaraju prema svojoj brzini. Istakni sinhronizovane reči ili slogove. Daj prednost tekstu reč po reč kada je dostupan. - Prikaži pozadinske vokale posebno - Premješta pozadinski vokal zapisan u zagradama, poput \"(Oh) želim te\", iz pevane linije u zasebnu, prigušeniju liniju Sakrij odsvirane stihove. Sakrij stihove iznad reda koji se trenutno pušta. Sakrij neodsvirane stihove. diff --git a/patches/src/main/resources/addresources/values-sr-rSP/music/strings.xml b/patches/src/main/resources/addresources/values-sr-rSP/music/strings.xml index 69629ab7dd..2999039921 100644 --- a/patches/src/main/resources/addresources/values-sr-rSP/music/strings.xml +++ b/patches/src/main/resources/addresources/values-sr-rSP/music/strings.xml @@ -540,8 +540,6 @@ Da biste osigurali sinhronizaciju scrobbling-a tokom reprodukcije u pozadini, po Сачувано Истакни синхронизоване речи или слогове. Дај предност текстовима реч по реч када су доступни. Када текстови носе време, истакните сваку реч или слог у складу са музиком. - Прикажите пратеће вокале одвојено - Премешта вокал у позадини написан у заградама, попут \"(Ох) желим те\", из линије певања у самосталну линију са смањеном јачином звука Сакриј […] линију Сакријте редове текста изнад тренутно репродукованог реда Сакриј неодсвиране текстове песама. diff --git a/patches/src/main/resources/addresources/values-sv-rSE/music/strings.xml b/patches/src/main/resources/addresources/values-sv-rSE/music/strings.xml index 69213f246e..1585a2ea93 100644 --- a/patches/src/main/resources/addresources/values-sv-rSE/music/strings.xml +++ b/patches/src/main/resources/addresources/values-sv-rSE/music/strings.xml @@ -540,8 +540,6 @@ Obligatoriska cookies: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PA Sparad Markera synkroniserade ord eller stavelser Prioritera ord-för-ord-text när det finns. När texten har ordtajming markeras varje ord eller stavelse i takt med musiken. - Visa bakgrundsvokaler separat - Flyttar en bakgrundsvokal som skrivits inom parenteser, till exempel \"(Åh) jag vill ha dig\", från sånglinjen till sin egen dämpade rad Dölj […] raden. Dölj textrader ovanför den raden som spelas för tillfället Dölj ospelade texter. diff --git a/patches/src/main/resources/addresources/values-th-rTH/music/strings.xml b/patches/src/main/resources/addresources/values-th-rTH/music/strings.xml index ded6a8a460..4d4fc258cf 100644 --- a/patches/src/main/resources/addresources/values-th-rTH/music/strings.xml +++ b/patches/src/main/resources/addresources/values-th-rTH/music/strings.xml @@ -545,8 +545,6 @@ Second \"item\" text" บันทึกแล้ว ไฮไลต์คำหรือพยางค์ที่ซิงก์ ให้ความสำคัญกับเนื้อเพลงแบบคำต่อคำเมื่อมีให้บริการ เมื่อเนื้อเพลงมีข้อมูลเวลาของคำ จะไฮไลต์แต่ละคำหรือพยางค์ให้ตรงกับจังหวะเพลง - แสดงเสียงร้องประกอบแยกต่างหาก - ย้ายเสียงร้องพื้นหลังที่เขียนอยู่ในวงเล็บ เช่น \"(โอ้) ฉันต้องการคุณ\" ออกจากท่อนร้องหลักไปยังบรรทัดแยกต่างหากที่มีความเบากว่าของตัวเอง ซ่อนบรรทัดที่เล่นแล้ว […] ซ่อนบรรทัดเนื้อเพลงที่อยู่เหนือบรรทัดที่กำลังเล่น ซ่อนเนื้อเพลงที่ยังไม่เล่น diff --git a/patches/src/main/resources/addresources/values-tr-rTR/music/strings.xml b/patches/src/main/resources/addresources/values-tr-rTR/music/strings.xml index d3fd62f699..c8ddfd2be9 100644 --- a/patches/src/main/resources/addresources/values-tr-rTR/music/strings.xml +++ b/patches/src/main/resources/addresources/values-tr-rTR/music/strings.xml @@ -545,8 +545,6 @@ Gerekli çerezler: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPISI Kaydedildi Senkronize kelimeleri veya heceleri vurgula Mümkün olduğunda kelime kelime şarkı sözlerine öncelik ver. Şarkı sözleri kelime zamanlaması içeriyorsa, her kelimeyi veya heceyi müzikle eş zamanlı olarak vurgular - Arka plan vokallerini ayrı göster - Parantez içinde yazılan arka plan vokalleri, örneğin \"(Oh) seni istiyorum\", söylenen satırdan ayrı bir daha düşük sesli satıra taşır Satırı gizle Şu anda çalınan satırın üzerindeki şarkı sözü satırlarını gizle Okunmamış şarkı sözlerini gizle diff --git a/patches/src/main/resources/addresources/values-uk-rUA/music/strings.xml b/patches/src/main/resources/addresources/values-uk-rUA/music/strings.xml index f8a1cdc662..8b1a3c7eb4 100644 --- a/patches/src/main/resources/addresources/values-uk-rUA/music/strings.xml +++ b/patches/src/main/resources/addresources/values-uk-rUA/music/strings.xml @@ -545,8 +545,6 @@ Second \"item\" text" Збережено Підсвічувати синхронізовані слова або склади Надавати пріоритет тексту пісні по словах, коли доступно. Якщо текст пісні містить синхронізацію по словах, підсвічує кожне слово або склад у такт музиці - Показувати бек-вокал окремо - Виносить бек-вокал, записаний у дужках, наприклад \"(Oh) I want you\", із рядка основного вокалу в окремий, тьмяніший рядок Приховувати відтворені рядки тексту Приховувати рядки тексту пісні над рядком, що зараз відтворюється Приховувати ще не відтворені рядки тексту diff --git a/patches/src/main/resources/addresources/values-vi-rVN/music/strings.xml b/patches/src/main/resources/addresources/values-vi-rVN/music/strings.xml index c685ad246f..5a51d9d42b 100644 --- a/patches/src/main/resources/addresources/values-vi-rVN/music/strings.xml +++ b/patches/src/main/resources/addresources/values-vi-rVN/music/strings.xml @@ -536,8 +536,6 @@ Các bước: Đã lưu Đánh dấu các từ hoặc âm tiết đồng bộ Ưu tiên lời bài hát từng từ khi có sẵn. Khi lời bài hát có thời gian cho từng từ, đánh dấu từng từ hoặc âm tiết theo nhạc - Hiển thị giọng hát nền riêng biệt - Di chuyển lời hát nền được viết trong ngoặc, ví dụ như \"(Ồ) anh muốn em\", khỏi dòng chính thành một dòng riêng biệt có độ mờ thấp hơn Ẩn lời bài hát đã phát Ẩn các dòng lời bài hát phía trên dòng đang phát Ẩn lời bài hát chưa phát diff --git a/patches/src/main/resources/addresources/values-zh-rCN/music/strings.xml b/patches/src/main/resources/addresources/values-zh-rCN/music/strings.xml index 9e7a97e908..04301a0d34 100644 --- a/patches/src/main/resources/addresources/values-zh-rCN/music/strings.xml +++ b/patches/src/main/resources/addresources/values-zh-rCN/music/strings.xml @@ -545,8 +545,6 @@ SABR 播放(模拟视频流已关闭)" 已保存 高亮显示同步的单词或音节 优先考虑可用逐字歌词。当歌词带有时间标志,随着音乐及时高亮每个单词或音节 - 单独显示背景人声 - 将写在括号中的背景人声(如“(Oh) I want you”)从演唱行中分离出来,形成独立的弱音行 隐藏已播歌词 隐藏当前播放行之上的歌词行 隐藏未播歌词 diff --git a/patches/src/main/resources/addresources/values-zh-rTW/music/strings.xml b/patches/src/main/resources/addresources/values-zh-rTW/music/strings.xml index 43077c0a50..5094345f03 100644 --- a/patches/src/main/resources/addresources/values-zh-rTW/music/strings.xml +++ b/patches/src/main/resources/addresources/values-zh-rTW/music/strings.xml @@ -545,8 +545,6 @@ SABR 播放(影片串流模擬已關閉)" 已儲存 強調已同步的歌詞 優先使用已同步歌詞而非未同步歌詞 - 單獨顯示背景和聲 - 將寫在括號中的背景人聲(如「(Oh) I want you」)從主唱行分離出來,變成獨立的較弱音量行 隱藏已播放行 隱藏目前播放中那一行上方的歌詞行 隱藏未播放的歌詞 diff --git a/patches/src/main/resources/addresources/values/music/strings.xml b/patches/src/main/resources/addresources/values/music/strings.xml index be02baccf7..f05ccdbd96 100644 --- a/patches/src/main/resources/addresources/values/music/strings.xml +++ b/patches/src/main/resources/addresources/values/music/strings.xml @@ -595,8 +595,6 @@ Required cookies: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPISID Saved Highlight synced words or syllables Prioritize word-by-word lyrics when available. When lyrics carry word timing, highlights each word or syllable in time with the music - Show background vocals separately - Moves a background vocal written in brackets, such as \"(Oh) I want you\", out of the sung line into a dimmer line of its own Hide played lyrics Hide lyrics lines above the currently playing line Hide unplayed lyrics From 6dbdf82457c5f8c7906b56f04640baf7e908060a Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Thu, 29 Oct 2026 08:49:52 +0100 Subject: [PATCH 151/189] feat(YouTube - Theme): Add "Glow Morphe" experimental splash screen type (#4135) BREAKING CHANGE: Patches now use Morphe Patcher 2.0.0 Co-authored-by: MarcaDian <152095496+MarcaDian@users.noreply.github.com> --- .../shared/patches/CustomBrandingPatch.java | 8 + .../shared/patches/SplashAnimationPatch.java | 52 +++- .../settings/SharedYouTubeSettings.java | 3 +- .../youtube/patches/theme/ThemePatch.java | 288 +++++++++++++++--- .../extension/youtube/settings/Settings.java | 9 +- .../youtube/layout/theme/Fingerprints.kt | 57 +++- .../layout/theme/GlowMorpheAnimationPatch.kt | 42 +++ .../layout/theme/SplashAnimationPatch.kt | 117 +++++-- .../values/shared-youtube/arrays.xml | 4 +- .../values/shared-youtube/strings.xml | 4 +- .../addresources/values/youtube/arrays.xml | 33 +- .../addresources/values/youtube/strings.xml | 2 + .../morphe_startup_animation_glow_1_dark.riv | Bin 0 -> 10091 bytes .../morphe_startup_animation_glow_1_light.riv | Bin 0 -> 10091 bytes ...phe_startup_animation_glow_1_play_dark.riv | Bin 0 -> 11807 bytes ...he_startup_animation_glow_1_play_light.riv | Bin 0 -> 11807 bytes .../morphe_startup_animation_glow_2_dark.riv | Bin 0 -> 6941 bytes .../morphe_startup_animation_glow_2_light.riv | Bin 0 -> 6941 bytes ...phe_startup_animation_glow_2_play_dark.riv | Bin 0 -> 9059 bytes ...he_startup_animation_glow_2_play_light.riv | Bin 0 -> 9065 bytes 20 files changed, 540 insertions(+), 79 deletions(-) create mode 100644 patches/src/main/kotlin/app/morphe/patches/youtube/layout/theme/GlowMorpheAnimationPatch.kt create mode 100644 patches/src/main/resources/custom-branding/raw/morphe_startup_animation_glow_1_dark.riv create mode 100644 patches/src/main/resources/custom-branding/raw/morphe_startup_animation_glow_1_light.riv create mode 100644 patches/src/main/resources/custom-branding/raw/morphe_startup_animation_glow_1_play_dark.riv create mode 100644 patches/src/main/resources/custom-branding/raw/morphe_startup_animation_glow_1_play_light.riv create mode 100644 patches/src/main/resources/custom-branding/raw/morphe_startup_animation_glow_2_dark.riv create mode 100644 patches/src/main/resources/custom-branding/raw/morphe_startup_animation_glow_2_light.riv create mode 100644 patches/src/main/resources/custom-branding/raw/morphe_startup_animation_glow_2_play_dark.riv create mode 100644 patches/src/main/resources/custom-branding/raw/morphe_startup_animation_glow_2_play_light.riv diff --git a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/patches/CustomBrandingPatch.java b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/patches/CustomBrandingPatch.java index d26da91f9f..8dcc9d0e3e 100644 --- a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/patches/CustomBrandingPatch.java +++ b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/patches/CustomBrandingPatch.java @@ -248,6 +248,14 @@ public static boolean iconHasStartupAnimation() { return getLauncherIconBranding().startupAnimationResourceName() != null; } + /** + * If the launcher icon is the Morphe play icon. + */ + public static boolean iconIsPlay() { + BrandingTheme branding = getLauncherIconBranding(); + return branding == BrandingTheme.PLAY || branding == BrandingTheme.PLAY_BLACK; + } + /** * The branded startup animation that matches the launcher icon. The original icon and a user * provided icon have no animation of their own, and use the animation of the default icon. diff --git a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/patches/SplashAnimationPatch.java b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/patches/SplashAnimationPatch.java index dfff6acf16..e8347b533a 100644 --- a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/patches/SplashAnimationPatch.java +++ b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/patches/SplashAnimationPatch.java @@ -54,6 +54,16 @@ public enum SplashScreenAnimationStyle { * Low memory devices cannot play it and get the Lottie animation of this style instead. */ RIVE_GLOW(1, false), + /** + * YouTube 21.30+ Rive animation of the Morphe logo coming into focus out of a glow, + * played in place of the Rive startup animation of the app. + * Low memory devices get the Morphe animation of {@link #MORPHE_COLOR} instead. + */ + RIVE_GLOW_MORPHE(1, false), + /** + * Another Morphe Glow animation, the same as {@link #RIVE_GLOW_MORPHE} except for the animation played. + */ + RIVE_GLOW_MORPHE_2(1, false), /** * Morphe animation of the launcher icon, which replaces the Lottie animation of the app. */ @@ -91,6 +101,20 @@ public static SplashScreenAnimationStyle styleFromOrdinal(int style) { this.style = style; this.isMonochrome = isMonochrome; } + + /** + * If the style plays a Rive animation, which YouTube 21.30+ can play. + */ + public boolean isRive() { + return this == RIVE_GLOW || isRiveGlowMorphe(); + } + + /** + * If the style plays a Morphe Rive animation in place of the Rive startup animation of the app. + */ + public boolean isRiveGlowMorphe() { + return this == RIVE_GLOW_MORPHE || this == RIVE_GLOW_MORPHE_2; + } } /** @@ -101,6 +125,31 @@ public static SplashScreenAnimationStyle styleFromOrdinal(int style) { private static final String BRAND_SECONDARY_COLOR = "[0,0.686275,0.682353,1]"; private static final String BRAND_GRADIENT = "[0,0.117647,0.352941,0.658824,1,0,0.686275,0.682353]"; + /** + * If the app plays the Rive animations, which YouTube 21.30+ does. + */ + private static boolean isRiveIncluded() { + return false; // Modified during patching. + } + + /** + * The default style, which is the Morphe glow animation if the app has the Rive animations. + */ + public static SplashScreenAnimationStyle getDefaultStyle() { + return isRiveIncluded() + ? SplashScreenAnimationStyle.RIVE_GLOW_MORPHE + : getDefaultLottieStyle(); + } + + /** + * The default style that is not a Rive animation. + */ + public static SplashScreenAnimationStyle getDefaultLottieStyle() { + return CustomBrandingPatch.isPatchIncluded() + ? SplashScreenAnimationStyle.MORPHE_COLOR + : SplashScreenAnimationStyle.FPS_60_ONE_SECOND; + } + public static boolean isMonochrome() { return SharedYouTubeSettings.SPLASH_SCREEN_STYLE.get().isMonochrome; } @@ -113,7 +162,8 @@ public static boolean isMonochrome() { */ public static boolean setBrandedSplashAnimation(LottieAnimationView view) { SplashScreenAnimationStyle style = SharedYouTubeSettings.SPLASH_SCREEN_STYLE.get(); - if (style != SplashScreenAnimationStyle.MORPHE_COLOR && style != SplashScreenAnimationStyle.MORPHE_MONOCHROME) { + if (style != SplashScreenAnimationStyle.MORPHE_COLOR && style != SplashScreenAnimationStyle.MORPHE_MONOCHROME + && !style.isRiveGlowMorphe()) { return false; } diff --git a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/settings/SharedYouTubeSettings.java b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/settings/SharedYouTubeSettings.java index 43d2b0673a..a7097edfd5 100644 --- a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/settings/SharedYouTubeSettings.java +++ b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/settings/SharedYouTubeSettings.java @@ -18,6 +18,7 @@ import app.morphe.extension.shared.patches.DownloadMode; import app.morphe.extension.shared.patches.PoTokenProviderPatch.PoTokenProviderAvailability; import app.morphe.extension.shared.patches.SkipSilencePatch.MinimumPause; +import app.morphe.extension.shared.patches.SplashAnimationPatch; import app.morphe.extension.shared.patches.SplashAnimationPatch.SplashScreenAnimationStyle; import app.morphe.extension.shared.settings.preference.ExternalDownloaderPreference.ExternalDownloaderAvailability; import app.morphe.extension.shared.spoof.SpoofVideoStreamsPatch.JavaScriptClientAvailability; @@ -80,7 +81,7 @@ public class SharedYouTubeSettings extends BaseSettings { public static final IntegerSetting THEME_LAST_SPLASH_SCREEN_INDEX = new IntegerSetting("morphe_theme_last_splash_screen_index", -1, false, false); // Custom branding - public static final EnumSetting SPLASH_SCREEN_STYLE = new EnumSetting<>("morphe_splash_screen_style", CustomBrandingPatch.isPatchIncluded() ? SplashScreenAnimationStyle.MORPHE_COLOR : SplashScreenAnimationStyle.FPS_60_ONE_SECOND, true); + public static final EnumSetting SPLASH_SCREEN_STYLE = new EnumSetting<>("morphe_splash_screen_style", SplashAnimationPatch.getDefaultStyle(), true); public static final EnumSetting CUSTOM_BRANDING_ICON = new EnumSetting<>("morphe_custom_branding_icon", CustomBrandingPatch.getDefaultIconStyle(), true, "morphe_custom_branding_icon_user_dialog_message"); public static final EnumSetting CUSTOM_BRANDING_NOTIFICATION_ICON = new EnumSetting<>("morphe_custom_branding_notification_icon", NotificationIconTheme.FOLLOW, true); public static final IntegerSetting CUSTOM_BRANDING_NAME = new IntegerSetting("morphe_custom_branding_name", CustomBrandingPatch.getDefaultAppNameIndex(), true); diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/theme/ThemePatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/theme/ThemePatch.java index 4137d53164..23925aaff0 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/theme/ThemePatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/theme/ThemePatch.java @@ -1,6 +1,7 @@ /* * Copyright 2026 Morphe. * https://github.com/MorpheApp/morphe-patches/pull/2524 + * https://github.com/MorpheApp/morphe-patches/pull/4135 * * Original hard forked code: * https://github.com/ReVanced/revanced-patches/commit/724e6d61b2ecd868c1a9a37d465a688e83a74799 @@ -34,6 +35,9 @@ import com.airbnb.lottie.LottieAnimationView; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.Map; @@ -42,6 +46,7 @@ import app.morphe.extension.shared.ResourceType; import app.morphe.extension.shared.ResourceUtils; import app.morphe.extension.shared.Utils; +import app.morphe.extension.shared.patches.CustomBrandingPatch; import app.morphe.extension.shared.patches.SplashAnimationPatch; import app.morphe.extension.shared.patches.SplashAnimationPatch.SplashScreenAnimationStyle; import app.morphe.extension.shared.settings.SharedYouTubeSettings; @@ -194,6 +199,16 @@ public static boolean useLotteLaunchSplashScreen(boolean original) { */ private static final int RIVE_SPLASH_SCREEN_SUPPORTED_FRAMES = 30; + /** + * Values of {@link Settings#RIVE_SPLASH_SCREEN_STATE}. The state is set to failed before the + * animation starts, and set to played when the animation has drawn. If the app closes in + * between, it stays failed and the animation is turned off on the next launch. + * After the animation has played once, it is not checked again. + */ + private static final int RIVE_SPLASH_SCREEN_STATE_NOT_PLAYED = 0; + private static final int RIVE_SPLASH_SCREEN_STATE_PLAYED = 1; + private static final int RIVE_SPLASH_SCREEN_STATE_FAILED = -1; + /** * Frames the glow animation still has to draw before the device is known to support it, * or 0 if the animation is not starting. @@ -205,17 +220,106 @@ public static boolean useLotteLaunchSplashScreen(boolean original) { */ private static volatile boolean riveSplashScreenFirstStart; + /** + * How long the Morphe Glow animation plays from its first frame until the splash screen closes. + * The animation ends after 1.3 seconds, and the timeout of the app would keep it longer. + */ + private static final long RIVE_GLOW_MORPHE_DURATION_MILLISECONDS = 1400; + + /** + * Closes the splash screen when the Morphe Glow animation has played, + * or null if the animation is not waiting for its first frame. + */ + @Nullable + private static volatile Runnable riveGlowMorpheSplashScreenCloser; + + /** + * The Morphe Glow 1 animation for the dark theme, which is played in place of the startup + * animation of the app. It has the same backdrop and artboard size as the glow animation, so it is changed the same + * way, and it fades in by itself. + */ + private static final String RIVE_GLOW_MORPHE_1_DARK_RESOURCE = "morphe_startup_animation_glow_1_dark"; + + /** + * The Morphe Glow 1 animation for the light theme, with the same glow colors as the dark theme + * animation but less bright. + */ + private static final String RIVE_GLOW_MORPHE_1_LIGHT_RESOURCE = "morphe_startup_animation_glow_1_light"; + + /** + * The Morphe Glow 1 animation for the dark theme with the Morphe play icon, + * played if the launcher icon is the play icon. + */ + private static final String RIVE_GLOW_MORPHE_1_PLAY_DARK_RESOURCE = "morphe_startup_animation_glow_1_play_dark"; + + /** + * The Morphe Glow 1 play animation for the light theme, with a less bright glow. + */ + private static final String RIVE_GLOW_MORPHE_1_PLAY_LIGHT_RESOURCE = "morphe_startup_animation_glow_1_play_light"; + + /** + * The Morphe Glow 2 animation for the dark theme, which is played in place of the startup animation of the app. + */ + private static final String RIVE_GLOW_MORPHE_2_DARK_RESOURCE = "morphe_startup_animation_glow_2_dark"; + + /** + * The Morphe Glow 2 animation for the light theme, with a less bright glow. + */ + private static final String RIVE_GLOW_MORPHE_2_LIGHT_RESOURCE = "morphe_startup_animation_glow_2_light"; + + /** + * The Morphe Glow 2 animation for the dark theme with the Morphe play icon, + * played if the launcher icon is the play icon. + */ + private static final String RIVE_GLOW_MORPHE_2_PLAY_DARK_RESOURCE = "morphe_startup_animation_glow_2_play_dark"; + + /** + * The Morphe Glow 2 play animation for the light theme, with a less bright glow. + */ + private static final String RIVE_GLOW_MORPHE_2_PLAY_LIGHT_RESOURCE = "morphe_startup_animation_glow_2_play_light"; + + /** + * The Morphe Glow 2 animations are made for the square view, and have no backdrop. + */ + private static final float RIVE_GLOW_MORPHE_2_ARTBOARD_WIDTH = 432; + private static final float RIVE_GLOW_MORPHE_2_ARTBOARD_HEIGHT = 432; + + /** + * The artboard size of the Morphe Glow 2 animations, as it is written in the Rive file. + */ + private static final byte[] RIVE_GLOW_MORPHE_2_ARTBOARD_SIZE = { + 0x07, 0x00, 0x00, (byte) 0xD8, 0x43, + 0x08, 0x00, 0x00, (byte) 0xD8, 0x43 + }; + private static boolean riveSplashScreenCrashChecked; private static boolean isRiveSplashScreenEnabled() { + return SharedYouTubeSettings.SPLASH_SCREEN_STYLE.get().isRive(); + } + + /** + * If the glow animation of the app is played. Otherwise, the Morphe Glow styles + * play their own animation in place of the startup animation of the app. + */ + private static boolean isRiveGlowStyle() { return SharedYouTubeSettings.SPLASH_SCREEN_STYLE.get() == SplashScreenAnimationStyle.RIVE_GLOW; } + /** + * If the animation played has the backdrop and artboard size of the glow animation of the app: + * the glow animation, or the Morphe Glow 1 animation. + */ + private static boolean isRiveGlowLayout() { + SplashScreenAnimationStyle style = SharedYouTubeSettings.SPLASH_SCREEN_STYLE.get(); + return style == SplashScreenAnimationStyle.RIVE_GLOW || style == SplashScreenAnimationStyle.RIVE_GLOW_MORPHE; + } + /** * Some devices crash when drawing the glow animation, because the Rive renderer - * uses GPU features they do not have. If the app closed before the animation could draw, - * the animation is turned off. Checked once when the app starts, before this launch - * shows the animation. + * uses GPU features they do not have. If the app closed before the animation could draw + * the last time it started, the animation is turned off. Checked once when the app starts, + * before this launch shows the animation. */ private static synchronized void checkRiveSplashScreenCrash() { if (riveSplashScreenCrashChecked) { @@ -223,16 +327,26 @@ private static synchronized void checkRiveSplashScreenCrash() { } riveSplashScreenCrashChecked = true; - if (Settings.RIVE_SPLASH_SCREEN_STARTING.get()) { - Logger.printInfo(() -> "App closed before the glow splash screen could draw, resetting splash screen style"); - Settings.RIVE_SPLASH_SCREEN_STARTING.save(false); - SharedYouTubeSettings.SPLASH_SCREEN_STYLE.resetToDefault(); + if (Settings.RIVE_SPLASH_SCREEN_STATE.get() == RIVE_SPLASH_SCREEN_STATE_FAILED) { + Logger.printInfo(() -> "App closed before the glow splash screen could draw, changing splash screen style"); + Settings.RIVE_SPLASH_SCREEN_STATE.save(RIVE_SPLASH_SCREEN_STATE_NOT_PLAYED); + // The default style can be a Rive animation. + SharedYouTubeSettings.SPLASH_SCREEN_STYLE.save(SplashAnimationPatch.getDefaultLottieStyle()); Utils.showToastLong(str("morphe_splash_screen_style_glow_not_supported")); } } - private static boolean isRiveGlowFile(int resourceId) { - return resourceId == ResourceUtils.getIdentifier(ResourceType.RAW, "rive_phantom"); + /** + * The Rive file of the splash screen: the glow animation of the app for the glow style, + * and the startup animation of the light or the dark theme for the Morphe Glow styles. + */ + private static boolean isRiveSplashScreenFile(int resourceId) { + if (isRiveGlowStyle()) { + return resourceId == ResourceUtils.getIdentifier(ResourceType.RAW, "rive_phantom"); + } + + return resourceId == ResourceUtils.getIdentifier(ResourceType.RAW, "rive_startup_animation_light") + || resourceId == ResourceUtils.getIdentifier(ResourceType.RAW, "rive_startup_animation_dark"); } /** @@ -250,6 +364,16 @@ public static boolean useRiveSplashScreen(boolean original) { return isRiveSplashScreenEnabled(); } + /** + * Injection point. + *

    + * 21.39+ plays the glow animation if this is on, and otherwise the startup animation + * of the light or the dark theme. + */ + public static boolean useRiveGlowSplashScreen(boolean original) { + return useRiveSplashScreen(original) && isRiveGlowStyle(); + } + /** * The background color the system drew the splash screen of this launch with. * @@ -277,6 +401,8 @@ private static int getSplashScreenBackgroundColor(Activity activity) throws Pack ? new int[]{android.R.attr.windowBackground, android.R.attr.windowSplashScreenBackground} : new int[]{android.R.attr.windowBackground}; + // TypedArray is AutoCloseable only on Android 12+, so it is recycled in finally. + //noinspection resource TypedArray array = theme.obtainStyledAttributes(attributes); try { for (int i = attributes.length - 1; i >= 0; i--) { @@ -327,7 +453,10 @@ public static void setRiveSplashScreenBackground(View animationView) { float artboardHeight = 0; ViewGroup.LayoutParams params = animationView.getLayoutParams(); if (context instanceof Activity activity && params.width > 0) { - final float artboardUnitsPerPixel = RIVE_GLOW_ARTBOARD_WIDTH / params.width; + final float originalArtboardWidth = isRiveGlowLayout() + ? RIVE_GLOW_ARTBOARD_WIDTH + : RIVE_GLOW_MORPHE_2_ARTBOARD_WIDTH; + final float artboardUnitsPerPixel = originalArtboardWidth / params.width; Rect windowBounds = getWindowBounds(activity); artboardWidth = windowBounds.width() * artboardUnitsPerPixel; artboardHeight = windowBounds.height() * artboardUnitsPerPixel; @@ -348,9 +477,11 @@ public static void setRiveSplashScreenBackground(View animationView) { keepSplashScreenUntilRiveFrame(activity); } - // Cleared when the animation has drawn, or when the app is stopped. - Settings.RIVE_SPLASH_SCREEN_STARTING.save(true); - riveSplashScreenFramesUntilSupported = RIVE_SPLASH_SCREEN_SUPPORTED_FRAMES; + // Set to played when the animation has drawn, and back to not played if the app is stopped first. + if (Settings.RIVE_SPLASH_SCREEN_STATE.get() != RIVE_SPLASH_SCREEN_STATE_PLAYED) { + Settings.RIVE_SPLASH_SCREEN_STATE.save(RIVE_SPLASH_SCREEN_STATE_FAILED); + riveSplashScreenFramesUntilSupported = RIVE_SPLASH_SCREEN_SUPPORTED_FRAMES; + } riveSplashScreenFirstStart = true; } catch (Exception ex) { Logger.printException(() -> "setRiveSplashScreenBackground failure", ex); @@ -395,9 +526,16 @@ public static void onRiveFrameAvailable() { Utils.runOnMainThreadNowOrLater(ThemePatch::removeRiveSplashScreen); } + Runnable closer = riveGlowMorpheSplashScreenCloser; + if (closer != null) { + riveGlowMorpheSplashScreenCloser = null; + Utils.runOnMainThreadDelayed(closer, RIVE_GLOW_MORPHE_DURATION_MILLISECONDS); + } + final int framesUntilSupported = riveSplashScreenFramesUntilSupported; if (framesUntilSupported == 1) { - clearRiveSplashScreenStarting(); + riveSplashScreenFramesUntilSupported = 0; + Settings.RIVE_SPLASH_SCREEN_STATE.save(RIVE_SPLASH_SCREEN_STATE_PLAYED); } else if (framesUntilSupported > 1) { riveSplashScreenFramesUntilSupported = framesUntilSupported - 1; } @@ -414,32 +552,38 @@ public static void onRiveFrameAvailable() { public static void onSplashScreenStop() { try { if (riveSplashScreenFramesUntilSupported > 0) { - clearRiveSplashScreenStarting(); + riveSplashScreenFramesUntilSupported = 0; + Settings.RIVE_SPLASH_SCREEN_STATE.save(RIVE_SPLASH_SCREEN_STATE_NOT_PLAYED); } } catch (Exception ex) { Logger.printException(() -> "onSplashScreenStop failure", ex); } } - private static void clearRiveSplashScreenStarting() { - riveSplashScreenFramesUntilSupported = 0; - Settings.RIVE_SPLASH_SCREEN_STARTING.save(false); - } - /** * Injection point. *

    * A home feed that is shown quickly closes the glow animation before it has drawn * anything, so on the first start the timeout of the app closes it instead. + * The Morphe Glow animation is shorter, and closes when it has played. * Stopping the app cancels the timeout, and then the home feed closes it as usual. * + * @param splashScreen Removes the splash screen when run. * @param riveAnimationShown If the app shows a Rive animation. * @return If the home feed does not close the animation. */ - public static boolean skipSplashScreenCloseOnHomeFeed(boolean riveAnimationShown) { - if (riveAnimationShown && riveSplashScreenFirstStart) { - riveSplashScreenFirstStart = false; - return true; + public static boolean skipSplashScreenCloseOnHomeFeed(Runnable splashScreen, boolean riveAnimationShown) { + try { + if (riveAnimationShown && riveSplashScreenFirstStart) { + riveSplashScreenFirstStart = false; + + if (SharedYouTubeSettings.SPLASH_SCREEN_STYLE.get().isRiveGlowMorphe()) { + riveGlowMorpheSplashScreenCloser = splashScreen; + } + return true; + } + } catch (Exception ex) { + Logger.printException(() -> "skipSplashScreenCloseOnHomeFeed failure", ex); } return false; } @@ -457,7 +601,7 @@ private static Rect getWindowBounds(Activity activity) { * Injection point. *

    * The app keeps the Rive file of a raw resource in memory, and the key of the glow animation - * is changed for each background color and artboard size. Otherwise a file changed for + * is changed for each background color, artboard size and logo. Otherwise, a file changed for * an earlier launch would be used. * * @param key The key of a raw resource, which is the prefix followed by the resource id. @@ -468,9 +612,11 @@ public static String getRiveRawResourceKey(String key) { final int backgroundColor = riveGlowBackgroundColor; if (backgroundColor != 0 && key.startsWith(RIVE_RAW_RESOURCE_KEY_PREFIX) - && isRiveGlowFile(Integer.parseInt(key.substring(RIVE_RAW_RESOURCE_KEY_PREFIX.length())))) { + && isRiveSplashScreenFile(Integer.parseInt(key.substring(RIVE_RAW_RESOURCE_KEY_PREFIX.length())))) { return key + "_" + Integer.toHexString(backgroundColor) - + "_" + riveGlowArtboardWidth + "x" + riveGlowArtboardHeight; + + "_" + riveGlowArtboardWidth + "x" + riveGlowArtboardHeight + + "_" + SharedYouTubeSettings.SPLASH_SCREEN_STYLE.get().name() + + (CustomBrandingPatch.iconIsPlay() ? "_play" : ""); } } catch (Exception ex) { Logger.printException(() -> "getRiveRawResourceKey failure", ex); @@ -484,6 +630,7 @@ && isRiveGlowFile(Integer.parseInt(key.substring(RIVE_RAW_RESOURCE_KEY_PREFIX.le *

    * Called when the app reads the Rive file of a raw resource it has not cached yet. * The splash screen reads its file after the background and size are set. + * The Morphe Glow styles play their own animation in place of the startup animation of the app. * * @param fileBytes The Rive file of a raw resource, or null if it could not be read. * @param resourceId The raw resource the file was read from. @@ -493,18 +640,64 @@ && isRiveGlowFile(Integer.parseInt(key.substring(RIVE_RAW_RESOURCE_KEY_PREFIX.le public static byte[] getRiveFile(@Nullable byte[] fileBytes, int resourceId) { try { final int backgroundColor = riveGlowBackgroundColor; - if (fileBytes == null || backgroundColor == 0 || !isRiveGlowFile(resourceId)) { + if (fileBytes == null || backgroundColor == 0 || !isRiveSplashScreenFile(resourceId)) { return fileBytes; } + final float artboardWidth = riveGlowArtboardWidth; + final float artboardHeight = riveGlowArtboardHeight; + final boolean resizeArtboard = artboardWidth > 0 && artboardHeight > 0; + + if (!isRiveGlowStyle()) { + // The app plays the startup animation of the light theme in the light theme. + final boolean lightTheme = resourceId + == ResourceUtils.getIdentifier(ResourceType.RAW, "rive_startup_animation_light"); + + if (!isRiveGlowLayout()) { + // The Morphe Glow 2 animations have no backdrop, and fade in by themselves. + final String resourceName; + if (CustomBrandingPatch.iconIsPlay()) { + resourceName = lightTheme + ? RIVE_GLOW_MORPHE_2_PLAY_LIGHT_RESOURCE + : RIVE_GLOW_MORPHE_2_PLAY_DARK_RESOURCE; + } else { + resourceName = lightTheme + ? RIVE_GLOW_MORPHE_2_LIGHT_RESOURCE + : RIVE_GLOW_MORPHE_2_DARK_RESOURCE; + } + fileBytes = readRawResource(ResourceUtils.getIdentifier(ResourceType.RAW, resourceName)); + return resizeArtboard + ? setRiveArtboardSize(fileBytes, RIVE_GLOW_MORPHE_2_ARTBOARD_SIZE, + RIVE_GLOW_MORPHE_2_ARTBOARD_WIDTH, RIVE_GLOW_MORPHE_2_ARTBOARD_HEIGHT, + artboardWidth, artboardHeight) + : fileBytes; + } + + final String resourceName; + if (CustomBrandingPatch.iconIsPlay()) { + resourceName = lightTheme + ? RIVE_GLOW_MORPHE_1_PLAY_LIGHT_RESOURCE + : RIVE_GLOW_MORPHE_1_PLAY_DARK_RESOURCE; + } else { + resourceName = lightTheme + ? RIVE_GLOW_MORPHE_1_LIGHT_RESOURCE + : RIVE_GLOW_MORPHE_1_DARK_RESOURCE; + } + fileBytes = readRawResource(ResourceUtils.getIdentifier(ResourceType.RAW, resourceName)); + } + if (backgroundColor != RIVE_GLOW_ORIGINAL_BACKGROUND_COLOR) { setRiveGlowBackgroundColor(fileBytes, backgroundColor); } - final float artboardWidth = riveGlowArtboardWidth; - final float artboardHeight = riveGlowArtboardHeight; - if (artboardWidth > 0 && artboardHeight > 0) { - fileBytes = setRiveGlowArtboardSize(fileBytes, artboardWidth, artboardHeight); + if (resizeArtboard) { + fileBytes = setRiveArtboardSize(fileBytes, RIVE_GLOW_ARTBOARD_SIZE, + RIVE_GLOW_ARTBOARD_WIDTH, RIVE_GLOW_ARTBOARD_HEIGHT, artboardWidth, artboardHeight); + } + + // The Morphe Glow 1 animation fades in by itself. + if (!isRiveGlowStyle()) { + return fileBytes; } final int timelineIndex = indexOf(fileBytes, RIVE_GLOW_TIMELINE); @@ -522,6 +715,18 @@ public static byte[] getRiveFile(@Nullable byte[] fileBytes, int resourceId) { return fileBytes; } + private static byte[] readRawResource(int resourceId) throws IOException { + try (InputStream input = Utils.getContext().getResources().openRawResource(resourceId)) { + ByteArrayOutputStream output = new ByteArrayOutputStream(); + byte[] buffer = new byte[8192]; + int length; + while ((length = input.read(buffer)) != -1) { + output.write(buffer, 0, length); + } + return output.toByteArray(); + } + } + private static void setRiveGlowBackgroundColor(byte[] fileBytes, @ColorInt int color) { final int index = indexOf(fileBytes, RIVE_GLOW_BACKGROUND_KEYFRAME); if (index < 0) { @@ -535,17 +740,24 @@ private static void setRiveGlowBackgroundColor(byte[] fileBytes, @ColorInt int c .putInt(color); } - private static byte[] setRiveGlowArtboardSize(byte[] fileBytes, float width, float height) { - final int index = indexOf(fileBytes, RIVE_GLOW_ARTBOARD_SIZE); + /** + * @param sizeBytes The width and height of the artboard, as they are written in the Rive file. + * @param originalWidth The width of the artboard in the Rive file. + * @param originalHeight The height of the artboard in the Rive file. + */ + private static byte[] setRiveArtboardSize(byte[] fileBytes, byte[] sizeBytes, + float originalWidth, float originalHeight, + float width, float height) { + final int index = indexOf(fileBytes, sizeBytes); if (index < 0) { - Logger.printException(() -> "Could not find the artboard size of the glow animation"); + Logger.printException(() -> "Could not find the artboard size of the Rive animation"); return fileBytes; } // The content is drawn from the origin of the artboard, which is a fraction of its size. // The origin keeps the content in the middle of the resized artboard. - final float originX = (width - RIVE_GLOW_ARTBOARD_WIDTH) / 2 / width; - final float originY = (height - RIVE_GLOW_ARTBOARD_HEIGHT) / 2 / height; + final float originX = (width - originalWidth) / 2 / width; + final float originY = (height - originalHeight) / 2 / height; // Each property is a one byte property key followed by a float. byte[] artboardProperties = ByteBuffer.allocate(4 * (1 + Float.BYTES)) @@ -556,7 +768,7 @@ private static byte[] setRiveGlowArtboardSize(byte[] fileBytes, float width, flo .put(RIVE_PROPERTY_KEY_ORIGIN_Y).putFloat(originY) .array(); - return replaceBytes(fileBytes, index, RIVE_GLOW_ARTBOARD_SIZE.length, artboardProperties); + return replaceBytes(fileBytes, index, sizeBytes.length, artboardProperties); } private static byte[] replaceBytes(byte[] array, int index, int length, byte[] replacement) { diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/Settings.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/Settings.java index b2794c1ee7..a6bd1239bc 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/Settings.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/Settings.java @@ -553,7 +553,7 @@ public class Settings extends SharedYouTubeSettings { public static final BooleanSetting TABLET_LAYOUT_IN_PLAYER = new BooleanSetting("morphe_tablet_layout_in_player", FALSE, true, new TabletLayoutInPlayerAvailability()); public static final BooleanSetting BYPASS_IMAGE_REGION_RESTRICTIONS = new BooleanSetting("morphe_bypass_image_region_restrictions", FALSE, true); public static final BooleanSetting GRADIENT_LOADING_SCREEN = new BooleanSetting("morphe_gradient_loading_screen", FALSE, true); - public static final BooleanSetting RIVE_SPLASH_SCREEN_STARTING = new BooleanSetting("morphe_rive_splash_screen_starting", FALSE, false, false); + public static final IntegerSetting RIVE_SPLASH_SCREEN_STATE = new IntegerSetting("morphe_rive_splash_screen_state", 0, false, false); public static final EnumSetting HEADER_LOGO = new EnumSetting<>("morphe_header_logo", HeaderLogo.DEFAULT, true); public static final BooleanSetting DISABLE_SIGN_IN_TO_TV_POPUP = new BooleanSetting("morphe_disable_sign_in_to_tv_popup", FALSE); public static final BooleanSetting DISABLE_CONNECT_YOUR_DEVICES_POPUP = new BooleanSetting("morphe_disable_connect_your_devices_popup", FALSE); @@ -1046,9 +1046,10 @@ public class Settings extends SharedYouTubeSettings { RESTORE_OLD_PLAYER_BUTTONS.resetToDefault(); } - // The Rive startup animation requires 21.39+. - if (!VersionCheckPatch.IS_21_39_OR_GREATER - && SharedYouTubeSettings.SPLASH_SCREEN_STYLE.get() == SplashScreenAnimationStyle.RIVE_GLOW) { + // The glow startup animation requires 21.39+, and Morphe Glow requires 21.30+. + SplashScreenAnimationStyle splashScreenStyle = SharedYouTubeSettings.SPLASH_SCREEN_STYLE.get(); + if (splashScreenStyle == SplashScreenAnimationStyle.RIVE_GLOW && !VersionCheckPatch.IS_21_39_OR_GREATER + || splashScreenStyle.isRiveGlowMorphe() && !VersionCheckPatch.IS_21_30_OR_GREATER) { Logger.printInfo(() -> "Resetting Rive splash screen animation style"); SharedYouTubeSettings.SPLASH_SCREEN_STYLE.resetToDefault(); } diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/theme/Fingerprints.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/theme/Fingerprints.kt index d7513c6bb8..9384f11d13 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/theme/Fingerprints.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/theme/Fingerprints.kt @@ -1,6 +1,6 @@ /* * Copyright 2026 Morphe. - * https://github.com/MorpheApp/morphe-patches + * https://github.com/MorpheApp/morphe-patches/pull/4135 * * Original hard forked code: * https://github.com/ReVanced/revanced-patches/commit/724e6d61b2ecd868c1a9a37d465a688e83a74799 @@ -106,7 +106,7 @@ internal object RiveSplashScreenViewFingerprint : Fingerprint( ) /** - * 21.39+ Sets the view that the Rive animation is drawn in. + * 21.30 - 21.31 and 21.35+ sets the view that the Rive animation is drawn in. */ internal object RiveSplashScreenAnimationViewFingerprint : Fingerprint( definingClass = YOUTUBE_MAIN_ACTIVITY_CLASS_TYPE, @@ -135,6 +135,37 @@ internal object RiveSplashScreenAnimationViewFingerprint : Fingerprint( ) ) +/** + * 21.32 - 21.34 sets the view that the Rive animation is drawn in, + * with a resource id that was loaded earlier for another view. + */ +internal object RiveSplashScreenAnimationViewLegacyFingerprint : Fingerprint( + definingClass = YOUTUBE_MAIN_ACTIVITY_CLASS_TYPE, + name = "onCreate", + returnType = "V", + parameters = listOf("Landroid/os/Bundle;"), + filters = listOf( + methodCall(name = "findViewById"), + opcode( + opcode = Opcode.MOVE_RESULT_OBJECT, + location = MatchAfterImmediately() + ), + checkCast( + "Landroid/widget/ImageView;", + location = MatchAfterImmediately() + ), + fieldAccess( + opcode = Opcode.IPUT_OBJECT, + type = "Landroid/widget/ImageView;", + location = MatchAfterImmediately() + ), + literal( + RIVE_THEMED_SPLASH_SCREEN_FEATURE_FLAG, + location = MatchAfterWithin(2) + ) + ) +) + /** * Closes the startup animation as soon as the home feed is shown. */ @@ -154,6 +185,18 @@ internal object SplashScreenOnStopFingerprint : Fingerprint( returnType = "V" ) +/** + * Removes the splash screen. + */ +internal object SplashScreenRemoveFingerprint : Fingerprint( + classFingerprint = RiveSplashScreenViewFingerprint, + returnType = "V", + parameters = listOf(), + filters = listOf( + string("SAU_REMOVE_SPLASH_ANIMATION|ANIMATION_SHOWN") + ) +) + /** * Creates the source of a Rive file that is a raw resource, * with the key the app keeps the file in memory with. @@ -167,8 +210,7 @@ internal object RiveRawResourceSourceFingerprint : Fingerprint( opcode = Opcode.IPUT_OBJECT, type = "Ljava/lang/String;", location = MatchAfterWithin(3) - ), - string("resId must not be 0") + ) ) ) @@ -198,6 +240,13 @@ internal object RiveCanvasSessionSetLatestBitmapFingerprint : Fingerprint( parameters = listOf("Lapp/rive/RiveCanvasSession;", "Landroid/graphics/Bitmap;") ) +internal object SplashAnimationIsRiveIncludedFingerprint : Fingerprint( + definingClass = "Lapp/morphe/extension/shared/patches/SplashAnimationPatch;", + name = "isRiveIncluded", + returnType = "Z", + parameters = listOf() +) + internal object LottieLayerColorFingerprint : Fingerprint( parameters = listOf( "Landroid/graphics/Canvas;", diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/theme/GlowMorpheAnimationPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/theme/GlowMorpheAnimationPatch.kt new file mode 100644 index 0000000000..7d5322cf48 --- /dev/null +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/theme/GlowMorpheAnimationPatch.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/4135 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.patches.youtube.layout.theme + +import app.morphe.patcher.patch.resourcePatch +import app.morphe.patches.youtube.misc.playservice.is_21_30_or_greater +import app.morphe.patches.youtube.misc.playservice.versionCheckPatch +import app.morphe.util.ResourceGroup +import app.morphe.util.copyResources + +/** + * Adds the animations of the Morphe Glow 1 and 2 splash screen styles: the Morphe logo comes into + * focus out of a glow. Each has a dark and a light theme animation. + */ +internal val glowMorpheAnimationResourcePatch = resourcePatch { + dependsOn(versionCheckPatch) + + execute { + // The Rive startup animation exists in 21.30+. + if (is_21_30_or_greater) { + copyResources( + "custom-branding", + ResourceGroup( + "raw", + "morphe_startup_animation_glow_1_dark.riv", + "morphe_startup_animation_glow_1_light.riv", + "morphe_startup_animation_glow_1_play_dark.riv", + "morphe_startup_animation_glow_1_play_light.riv", + "morphe_startup_animation_glow_2_dark.riv", + "morphe_startup_animation_glow_2_light.riv", + "morphe_startup_animation_glow_2_play_dark.riv", + "morphe_startup_animation_glow_2_play_light.riv" + ) + ) + } + } +} diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/theme/SplashAnimationPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/theme/SplashAnimationPatch.kt index 207e74445f..a10f30cbde 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/theme/SplashAnimationPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/theme/SplashAnimationPatch.kt @@ -2,6 +2,7 @@ * Copyright 2026 Morphe. * https://github.com/MorpheApp/morphe-patches/pull/3178 * https://github.com/MorpheApp/morphe-patches/pull/4109 + * https://github.com/MorpheApp/morphe-patches/pull/4135 * * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. */ @@ -17,6 +18,8 @@ import app.morphe.patcher.literal import app.morphe.patcher.methodCall import app.morphe.patcher.patch.bytecodePatch import app.morphe.patcher.util.getReference +import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable +import app.morphe.patcher.util.returnEarly import app.morphe.patches.shared.layout.branding.startupAnimationResourcePatch import app.morphe.patches.shared.misc.lottie.LOTTIE_ANIMATION_VIEW_CLASS_TYPE import app.morphe.patches.shared.misc.lottie.LottieAnimationViewSetAnimationIntFingerprint @@ -26,6 +29,9 @@ import app.morphe.patches.youtube.layout.seekbar.LottieSplashScreenFeatureFlagFi import app.morphe.patches.youtube.layout.seekbar.LottieSplashScreenFeatureFlagLegacyFingerprint import app.morphe.patches.youtube.misc.extension.sharedExtensionPatch import app.morphe.patches.youtube.misc.playservice.is_20_31_or_greater +import app.morphe.patches.youtube.misc.playservice.is_21_30_or_greater +import app.morphe.patches.youtube.misc.playservice.is_21_32_or_greater +import app.morphe.patches.youtube.misc.playservice.is_21_35_or_greater import app.morphe.patches.youtube.misc.playservice.is_21_39_or_greater import app.morphe.patches.youtube.misc.playservice.versionCheckPatch import app.morphe.patches.youtube.misc.settings.PreferenceScreen @@ -34,9 +40,12 @@ import app.morphe.patches.youtube.shared.YouTubeActivityOnCreateFingerprint import app.morphe.util.findInstructionIndicesReversedOrThrow import app.morphe.util.fiveRegisters import app.morphe.util.insertLiteralOverride +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction import com.android.tools.smali.dexlib2.iface.reference.FieldReference +import com.android.tools.smali.dexlib2.immutable.ImmutableMethod private const val EXTENSION_CLASS = "Lapp/morphe/extension/youtube/patches/theme/ThemePatch;" @@ -52,12 +61,18 @@ val splashAnimationPatch = bytecodePatch { settingsPatch, lottieAnimationPatch, versionCheckPatch, - startupAnimationResourcePatch + startupAnimationResourcePatch, + glowMorpheAnimationResourcePatch ) execute { - // 21.39+ also has the glow Rive animation. - val arrayKeySuffix = if (is_21_39_or_greater) "_21_39" else "" + // 21.30+ also has the Morphe glow Rive animation, and 21.39+ has the glow animation + // with the YouTube logo as well. + val arrayKeySuffix = when { + is_21_39_or_greater -> "_21_39" + is_21_30_or_greater -> "_21_30" + else -> "" + } PreferenceScreen.GENERAL.addPreferences( ListPreference( @@ -117,33 +132,62 @@ val splashAnimationPatch = bytecodePatch { ) } - if (is_21_39_or_greater) { - arrayOf( - RiveSplashScreenFeatureFlagFingerprint, - RiveGlowSplashScreenFeatureFlagFingerprint - ).forEach { fingerprint -> - fingerprint.matchAll().forEach { + // The Rive styles are not available before 21.30: + // - 21.21 and earlier do not include the Rive library. + // - 21.22 - 21.28 play the Rive startup animation with an older Rive view, but 21.22 - 21.23 + // crash when the view is created, because the app build removed Java code that the native + // Rive library calls. 21.24 - 21.28 are only experimental targets and are not supported. + // - 21.29 does not include the native library of the older view, and does not have + // the newer Rive startup animation of 21.30+. + if (is_21_30_or_greater) { + // The Morphe glow animation is the default style. + SplashAnimationIsRiveIncludedFingerprint.method.returnEarly(true) + + RiveSplashScreenFeatureFlagFingerprint.matchAll().forEach { + it.method.insertLiteralOverride( + it.instructionMatches.first().index, + "$EXTENSION_CLASS->useRiveSplashScreen(Z)Z" + ) + } + + // 21.39+ plays the glow animation if this flag is on, and otherwise the startup animation + // of the light or the dark theme, which the Morphe Glow style replaces. Before 21.39 the flag + // selects the older Rive view, which crashes because its library is not included. + RiveGlowSplashScreenFeatureFlagFingerprint.matchAll().forEach { + if (is_21_39_or_greater) { it.method.insertLiteralOverride( it.instructionMatches.first().index, - "$EXTENSION_CLASS->useRiveSplashScreen(Z)Z" + "$EXTENSION_CLASS->useRiveGlowSplashScreen(Z)Z" ) + } else { + it.method.insertLiteralOverride(it.instructionMatches.first().index, false) } } - // The themed Rive animation is used instead of the glow animation if it is on. - RiveThemedSplashScreenFeatureFlagFingerprint.matchAll().forEach { - it.method.apply { - findInstructionIndicesReversedOrThrow( - literal(RIVE_THEMED_SPLASH_SCREEN_FEATURE_FLAG) - ).forEach { index -> - insertLiteralOverride(index, false) + // The themed Rive animation of 21.32+ is used instead of the glow animation if it is on. + if (is_21_32_or_greater) { + RiveThemedSplashScreenFeatureFlagFingerprint.matchAll().forEach { + it.method.apply { + findInstructionIndicesReversedOrThrow( + literal(RIVE_THEMED_SPLASH_SCREEN_FEATURE_FLAG) + ).forEach { index -> + insertLiteralOverride(index, false) + } } } } - RiveSplashScreenAnimationViewFingerprint.let { + // The match of the instruction that sets the view field. + val (animationViewFingerprint, animationViewMatchIndex) = + if (is_21_32_or_greater && !is_21_35_or_greater) { + RiveSplashScreenAnimationViewLegacyFingerprint to 3 + } else { + RiveSplashScreenAnimationViewFingerprint to 4 + } + + animationViewFingerprint.let { it.method.apply { - val index = it.instructionMatches.last().index + val index = it.instructionMatches[animationViewMatchIndex].index val viewRegister = getInstruction(index).registerA addInstruction( @@ -155,7 +199,8 @@ val splashAnimationPatch = bytecodePatch { } // The backdrop of the glow animation is changed to the color of the splash screen - // when the file is read, and the app keeps the file in memory with a key of that color. + // when the file is read, and the animation is given the shape of the screen. + // The app keeps the file in memory with a key of that color and shape. RiveRawResourceSourceFingerprint.let { it.method.apply { val index = it.instructionMatches[1].index @@ -198,9 +243,37 @@ val splashAnimationPatch = bytecodePatch { "invoke-static { }, $EXTENSION_CLASS->onSplashScreenStop()V" ) + // The splash screen is a Runnable that removes it, so the extension can close + // the Morphe glow animation when it has played. + SplashScreenRemoveFingerprint.let { + it.classDef.apply { + interfaces.add("Ljava/lang/Runnable;") + + ImmutableMethod( + type, + "run", + emptyList(), + "V", + AccessFlags.PUBLIC.value, + null, + null, + MutableMethodImplementation(1) + ).toMutable().apply { + addInstructions( + 0, + """ + invoke-virtual { p0 }, ${it.method} + return-void + """ + ) + }.let(methods::add) + } + } + // The Rive animation is loaded in the background, and a home feed that is // shown quickly closes the animation before it has drawn anything. On the first - // start the animation is instead closed by the timeout of the app. + // start the animation is instead closed by the timeout of the app, or by the + // extension when the Morphe glow animation has played. val riveAnimationShownField = RiveSplashScreenViewFingerprint.instructionMatches[1] .instruction.getReference()!! @@ -208,7 +281,7 @@ val splashAnimationPatch = bytecodePatch { 0, """ iget-boolean v0, p0, $riveAnimationShownField - invoke-static { v0 }, $EXTENSION_CLASS->skipSplashScreenCloseOnHomeFeed(Z)Z + invoke-static { p0, v0 }, $EXTENSION_CLASS->skipSplashScreenCloseOnHomeFeed(Ljava/lang/Runnable;Z)Z move-result v0 if-eqz v0, :close_on_home_feed return-void diff --git a/patches/src/main/resources/addresources/values/shared-youtube/arrays.xml b/patches/src/main/resources/addresources/values/shared-youtube/arrays.xml index 342388cc56..fc77683b00 100644 --- a/patches/src/main/resources/addresources/values/shared-youtube/arrays.xml +++ b/patches/src/main/resources/addresources/values/shared-youtube/arrays.xml @@ -21,15 +21,15 @@ @string/morphe_splash_screen_style_entry_1 @string/morphe_splash_screen_style_entry_2 - @string/morphe_splash_screen_style_entry_3 @string/morphe_splash_screen_style_entry_4 + @string/morphe_splash_screen_style_entry_3 @string/morphe_splash_screen_style_entry_5 DISABLED FPS_60_ONE_SECOND - MORPHE_COLOR FPS_60_BLACK_AND_WHITE + MORPHE_COLOR MORPHE_MONOCHROME diff --git a/patches/src/main/resources/addresources/values/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values/shared-youtube/strings.xml index 0f1e025ff6..2940559ecd 100644 --- a/patches/src/main/resources/addresources/values/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values/shared-youtube/strings.xml @@ -20,9 +20,9 @@ Second \"item\" text" Startup animation Disabled Color - Color Morphe + Morphe Color Monochrome - Monochrome Morphe + Morphe Monochrome Restart to finish setting up diff --git a/patches/src/main/resources/addresources/values/youtube/arrays.xml b/patches/src/main/resources/addresources/values/youtube/arrays.xml index f4614c00d0..a70b226b84 100644 --- a/patches/src/main/resources/addresources/values/youtube/arrays.xml +++ b/patches/src/main/resources/addresources/values/youtube/arrays.xml @@ -334,22 +334,45 @@ - - + + @string/morphe_splash_screen_style_entry_1 @string/morphe_splash_screen_style_entry_2 - @string/morphe_splash_screen_style_entry_3 @string/morphe_splash_screen_style_entry_4 + @string/morphe_splash_screen_style_entry_3 @string/morphe_splash_screen_style_entry_5 + @string/morphe_splash_screen_style_entry_7 + @string/morphe_splash_screen_style_entry_8 + + + DISABLED + FPS_60_ONE_SECOND + FPS_60_BLACK_AND_WHITE + MORPHE_COLOR + MORPHE_MONOCHROME + RIVE_GLOW_MORPHE + RIVE_GLOW_MORPHE_2 + + + @string/morphe_splash_screen_style_entry_1 + @string/morphe_splash_screen_style_entry_2 + @string/morphe_splash_screen_style_entry_4 @string/morphe_splash_screen_style_entry_6 + @string/morphe_splash_screen_style_entry_3 + @string/morphe_splash_screen_style_entry_5 + @string/morphe_splash_screen_style_entry_7 + @string/morphe_splash_screen_style_entry_8 DISABLED FPS_60_ONE_SECOND - MORPHE_COLOR FPS_60_BLACK_AND_WHITE - MORPHE_MONOCHROME RIVE_GLOW + MORPHE_COLOR + MORPHE_MONOCHROME + RIVE_GLOW_MORPHE + RIVE_GLOW_MORPHE_2 @string/morphe_theme_color_entry_1 diff --git a/patches/src/main/resources/addresources/values/youtube/strings.xml b/patches/src/main/resources/addresources/values/youtube/strings.xml index 5b2ef3e2a4..9c397ea710 100644 --- a/patches/src/main/resources/addresources/values/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values/youtube/strings.xml @@ -1368,6 +1368,8 @@ Limitation: If \'Restore old video action bar\' is turned off, \'More videos\' o Glow + Morphe Glow 1 + Morphe Glow 2 Glow splash screen is not supported by this device Light theme background White diff --git a/patches/src/main/resources/custom-branding/raw/morphe_startup_animation_glow_1_dark.riv b/patches/src/main/resources/custom-branding/raw/morphe_startup_animation_glow_1_dark.riv new file mode 100644 index 0000000000000000000000000000000000000000..5c4100a6a46fd57c01b94d4abf0b5e24ec2848ba GIT binary patch literal 10091 zcmb7K3vdaNU@?svg~hU+>YgZ@-Mp0oeo3H}die3X6(MO3TX4VIwL=jtYjtm62#w ztfsauUcXpdrmfJnYVF$5ZlCx+^?&C7+<(~L=0D0+f2;syJ+3jhBH zex>;N@mqtR4?n_l@k{;hX&C1Tco#i{*ca(3#I$I;Cj39SX4;JDjSYE|r%t>3Xg5tX zV`%MDzxJ75``oV`_G@i^?TBA%_iHEo+DX5rV~DTTh{r>jbm`mv ztBOEBArck3Ph{)eL{8a5;l^td1PheY4FL<-Hh)t67id^ zDh;xat?Q;FjUn$WPt*(z)V9=C2BwGVXs9?{T*g}k*~yeR&i3El5^s89rRAe}dU?_q zrBx?ucK06>sy)AWgzbY`?4$X^B?%)pXHTML-Ku_xT662bP~AUg+#ZY{8s0BuE-&0N zBw_j3IAXBvgIerkmhbV9^bxXr%-JxdB~IR$(Z@G5+-V%<)}LF9p^?V0v43OlWDU)r zcE~$9p}N9L1EF|fb&wcN?zc?5G;Hh<)nRMScaA<7tfRTbsio&QtAqaGnn%vG25Y~* z4-7YNlF>XpGnA6IV(pcd__~kAh1ds2v;5_qiJH+R{S&o$ds@ElgBd&u`L=2xtK`jK zoW_F>CtiCMJ)#+mc$x9;oV|&ZXb^de<%t+KzHKIrjKjJ1D)O9pMm%R?a5b?H#L)8n zIZK9TymA!J%npXqnx1$xlt~v$X@gpqR}9#-^XeeUt<gQ#89h?7;1G%hNrrSp;i|&lPz#6{Y5_@xXS{M0PXW#COhAdWBj0=tXTINRX^*7^FjLNXFrg%H=%fdJ zS6X_IC~7e=MXe^LsO98RJoUsBwV*7DQc+A%ONuFKO)*6+DyFDa1&Uk;wXT-RrQ#Cm z>|M&Gcq)r2YH7I?&sqgVMy*y?6Gg3-7gN;wVv1T|F2yrycOXxRfpX3VjP-f?$kd0_ zO48V>bd5cet}&R)@O=hH!xns_5h6%@${6}~rMJ~x>y0UDy)i|tH<#k6H>RldW>J)S zV~SdDOi}BNDQdkjMXfhbho_7{?EWFFqeA53uSgy}lkK_PVJ z13(tXVZ8_LO7{WN^|4z9bK$Z)zO}GM=)JS?@}=LA9^*JKl$M7%>`!vjb8CpnmF@?bDKER+XxP}(K% zWuvIHw6sHS_KCq_aIonKBM_#5(eAl+a3Fl%dt<3hw?dO96$itnxjs-ye#8YM^w_l9 zX3ZSA);CJ4m?I1mJM*AnXm{Wn-__%Y`b3XWQcG7_f)ps<)XHg_X&F#&UnI$xLzHOLzFni>* z!2%-XQ6G7wfM|?)L9(oTU4}cSC%=@maE|izpMDbtN+~0hp2jI?e(wF7fl}58^*yjN zEKy%<)4x`;Y2}UxACA4ub?%F0n>B12CPk2G=O7W}9OQ9A5<&RhVC4%%G-z1H4`3PQ zk+iHHx3W4G5zvBAh=#e{NooOQ@1apnjnUZ)a13Kn1y%q(``Ldej!4sG3m0vD=?~pNh%KrxttbRIjj&%q|~DnYMDN z#rpJaqB5dBtvjww0-*vVZ$}!+9B5LBtRZ{0_oU7CS;}w`BA<%Dd-v)^Q=Eib zC2&fWdF4m11<)LeW_6!dYBpjvhIygwIWh@|5E&HhJZm`;6ra5`$f{v}+;tc@ZE#xP ztoQ9pl2Z4ZM-3qPAuT|t$Rt7sS|CUZ5IW*Yg#Kg!1Ze?sEHbV|BHYI?Cj)inmr*~C zDv9Qo|I!R-8Ri%eS-(-f#}m8NZ9Ztt7jv|wT+f)GRhVHl3Bp@(Zp4C~rqoEt zFn*AyQrA%!8)Kh&nYIpZJ3MFa+qfo?(e@J0F?nAAQV7qa2$b!9iRwY%rM_d zb6io$$%4dFB~gShu^&gqb0J$R4$OkGM3?)fWc@vb{>(s_0K@`?=PVBs+3&EQL@8~3F z5O9v}bk}og4XR7X>KIbAHfcqP(db1(B5Acok_$ScZu=4CP$tAtvMcfpe)kQ}`q1@+?I_>fVfeI%lAv4KPg@V#2!L4H|LT%g+&P4AelQHg(*C*o zg<;|i#aaBcjb>KQidW@eSN%EevG_l3qbY?)$`?H`>pNbUK~IS1xJ}QJqR4BqZI}xw zFd{>z^(Xc|ML0H%`s07Eq~=}DUBf(0Oy1S0T%stcs?}63cR1w&%P?Cdi&i6B79Lro z4fq&fKQdwFMoPIrp=86X0!A{XU5}4OzZc2$c$gAV-cK zq#%geV>$e2YOP=tYN^Vfo_a{L8z{w!M?|ehD;2>XAP5RtKu%4+duQj(u4G=+b9Q`p zzVF?4-+lLc^Jco&SMUAdWk zv^rK>S08U!uC3D6XgjrI+VLKr_&@c3=0EH|;y>!|@E`Mk=|AZ|uAk7wS|P-9soy&M z|8Mw};pfM11AadI2+zka{lBMRTp-|G^b+EONN*u#MvrO2|D$VW&YsoOSTJqI%)5{G z&_oM{);{%XpZT@Je(i`~JL=au{Ms?U_N8At>DP1&(f_Oe-542AC{9FR^~e(9Q7fA+ z{g3^!GB`kpM5XQ%xq1(gSN>qQDO`BxxLXrNBQ{S@7E??2i=4poFNn@kici-aEBerk zAX^)5$nPxu>HcWQwS-tmR#t%quRVt>?tj)YR=jq)t(IKW(WSokxPexD-Y;&o^uAzB zykK;zF?q)(t9I*e%UkP)H{Ng6FPale#PjnO+l(zo&n0Xh+lJl{s@u}9(W;;QZB`wC2Yjv9jr6D{W}|>dHZ{@3}feax3%c#UfAt3FP$N0U>O0mB2c@ zyKGKr^r2%x9Tsjc3E8svHcprih`|6fxAAk_=Bx5ksvhW+*j9 z44#Xprih`|6fxAAB8FO1 zQR~g6ct-6G5legI)dpsdWV9!|Io|H!f6NRwHBQ{uxm{5 zj2nCYC`WymkV;A59(~5UXD}sg-b3D+2$7;o#_H~NB96E=(wbN`wy9C9zb{Y z#^&+OjyEex|5*ew(7)`I(~L`I(~L z`I(~L`I(~L`9YEEpx*hH`kbZm-1%LK=g!X*_0I27JoivgWYp@N{}GCsDW2c1nWEnL znWEnLU5aPa?m(V9KPWxiKO7CBtsEyMf36T$Mh4SGUrB#7!X+C!{_ZM$wNLK&GlmF# z4Vn1Z9Sn3zb3txJh|EqK#+Rb23&|tWv276Kt*8VC2`=cDpHF+?Yoi@sYIVkNC z__9$_R#w)jxA?>`F)Y;lxDgCfz-ag0I4l^x;Jpddrd!t3sew?~G`9q+$d9;iv>uyz z+uS*0Hu}bDmGgyRVrL#YvO?rXwFv&07r8vD#h7WB4-gU%C6UXbTEkb(q7%)#&^*;r z(20(vDfC+0I@&I_09~z7d)E+xpQhgB@K_FyZ`9n9MDs=9F?!AcDojCZ*Y^)QP}|mD zCjb?tpw4$Ll)b*5Jl+CSoPuK4FHJbTiar8lm`#8`*Ku^1pvdTIX_Zm zW7K!C#0+x}1xuY7-W%prU^YJPT>Y#M{#%@MU_uR@P4HO~v8egjb(EPEGLJ=d zN;%&)OniLcltACOe;6?9Ees!YO24#bC;` zol7m&uWvJz5%p<*?b;*|DnRnq(OB+y@pFT9j zskl`Nr&O6&e)LKZ%?W7M^lPVP6J}$W7o|K$rUDTni=tgZ1Ze?6M_h@}pDchNEkI60Cbmh0`xxdlpw9fV z8YWUD(fkTuoJ~#4&t50P91kMvH_G>TVz;`@W%hhA$CQ*C7!$M#v+X89cq`72*w9my z8VMQZ&jCBdw6AqJ71KRf2Q(Kt#l*J_b7@8n3+%#u?7IO_8Q=jz0i*}V5!l!PionGT z^UVy$l~tT9NIYEZV??-|OuT)N{uOk_44ELFv6tZR-f3u%2~d=N(-L-V}I8 zCpm+Fb99%xo>Oa3T|!pJ@RE&5J4%d3FB%fbs5L4tiQN4~RoITP%RYwr4<|(6Ld>w! zi*-v+?@7s_yCl=<4L;2JYKH~OF!6?=oC`g~WL)2KF4bp>f52Rxp$Mp4=^1nC56Eyy zq}Fb3MS%a3)1aw#x!d4PL^%g~vy*ceHS@tF=(hGV)`KM`G)oqLav>^la4z* zfzk|`X6OA)rr{gX@ZKqWDc!@rgrfdWq}tTKPYPd@hV%K3{?!!l zmGms5bAT^~18AuFPn4BMZLL`rnf75q0cWJ-VoYsZ-#9(=;KfOtiIX!A6S)KfnWmW< z2nRbdbKHT{1Pld2%4>e)Iy5huG>`^!kBB2j_q=}AjLCDF#F*up|C-6u?+_soUZeSh MCC07Ngb0fN145CSuK)l5 literal 0 HcmV?d00001 diff --git a/patches/src/main/resources/custom-branding/raw/morphe_startup_animation_glow_1_play_dark.riv b/patches/src/main/resources/custom-branding/raw/morphe_startup_animation_glow_1_play_dark.riv new file mode 100644 index 0000000000000000000000000000000000000000..6a09d3067eb071c61cbc7987dcbe50f0d0166038 GIT binary patch literal 11807 zcmb7K33L=y7OiT!SR!D6fK&zpNz6i!$T?*oh;((QLytPjDE1N-Wk5tUj3|WVfI2LK zf;xhVEP@N5LpC>*Jn zAshe zYJ6Py5T1ok;=h++Tp_``)JT%{_!~>oi1H({0Ih+97CmQT551zj{dbFsg_dA4-i5351CB+c};ps!{}ZY%XP zm!w#!;`-g!Gn58Wo1(EBI>obpEbJH0Sy$E9m;3qo*StzYsa1Y;zpUB5*0M1f zwFMU*S>|DD`Lk)Y1yf>0I$JB=l!!Tztn>~tyf$pRecTN`^^JRf)Po!MHi?FURYUaf z^tt)oNRKZs^D0fj_~pNQylQpH>)v1{G3a{uw#v4#$lj}DA2WsyEsCpCZtEHk{xqvh z57G7TthD!|k&XjC(U~!=-~N~y>mP~-U(G0rg{lH)^zg!kKYJo;EAxzA4s`9eL7hJ@ zGad{))G-#K>)|I~0b@$6IAI(UGFHceC3Ev*p?`XdyzAQzBisr zEv0pxNVl=9@+XFS6x+IHZ@KPR9O*Xjsig4v2Y4~p1gqE1ve>6>u}|A#pY3?{+*q42 z4~~kdg^zTP2PpwU^c9}nX|*2Nw04|F$p&Lx)rgoHukIW-W;|o%B#$xUIWs@?I*ja< zO??J~t{aTvnbkToR&RF4)hFKz`ix%ok8kP?XYKi8EK)gtjltjyxaH9)}mzt&K*e_5194?&YQHkwyDDcZ&yi4%wxL=z93d zr{mE``oV6-Tox=UjH}9GJs!M$z)3x{IJdheoL5sFjXZQ}Wl#3;_t6D$b@a3Tc(D1o z6ME=_w^sBFFZuboXk_e$H;s`xnK+~ddG^@SHG25yi^#L}2NoC%C#I(+fI-*86xaxT zp%h{2W9rKq(pc(Ii`8lvlA)R5PxEn@JTj~)KAFNm7kVAK{f@){o0*$1!ATl=wo zR1Kl4-rJ*xQOi-I#>oe(09W6H%C=sk#_#*Ws=m(9wE6R%^0uHaL+dg(K402$YfX&@ zf|pU~Qo2fQlx|4zUHc@-kgHNyhiXbk6_h`5#G}AsZOTWKYhlfHgHEyTir&R4)zcbZ zN|hppSfz;J~W-2DPo9KiWp**>S;&C$y7?sUsrzsp2XE}Gg4MRF~rqR z3~}|_jFi<+3~}`nLtOpD5LZ7joE1)1{lsu$nymT*cIarO5}exgD3->jR}-(PBzv$dwmm;27P%`s&FDxb>S(1AN(9sm6G zcM68hI_gni6?}gF5j}s;-k1anIxv8v-XG%->%d@Zq$zcP7-AhDhLa`9bbuIQ9k9od zQU{13)&XLObzra^6(?tt=|CksiFLqcq|^aoh;@J%VjZvJS9bk%B2$&*P0;Y(ifGJ`vV2W4_KuP-kBT)^O z$apAtl{COP4ZH?3;XwcED20;7Cf0GFvW>( zGD!f%UI(!xMC+FXridkhDPl=ridYhuB9;WEh$Vq3Vo3ny5=mIqLFt|-37bh0w$zn` zkLpSS%-MM3i}a4-t*<2M#HYC9-umR>KlRH4Q^fMX6enAf$pcfw^5Bdmr93c2EDuZ( z%L6EOc$@@ICJ#I6mj{QEQXZHhmItPY<-wt(ln17W<$)<;d0>iI9+=`RcrtlliWA*r z@&Jmx4q|yYP`^AdMJx|Y5z7Np#PYxtu{wX#q<)r2yY|e*#*jhbvb(V zZf`EN6rg+*F<(%gA@ks7eqV_{n!t1%ouwfp^8uh0kHdJmalWn(n2wL1?ZI^1bM}(K z08MoCOI=4W9Y>Q~7|f&tYvS4Wd^Ki$&iQ>GcF(=8V(&Ty0Q)c}G{rP)3npZ&@F;Aa zoq?yX_ES6QhIwMSHC>=i509e<1z(BYW{`Pq8g6HAY#w{t^2X-ZyDe`p9dA#QH+;3q z|K&dGZSX8I&&;vBv3WRZd1G_mSC%)Jj<@H@8@@1@sg}37WFCLZ^2X*jJ1lQ(nlGZU zurM8O3*n73L_w=n{)LAvmoJg|^)C3}Kf=ItZdzpDgjTEk^OLQcmejooCMf`NvA&IJ zsS6bGqb*a!8x>O=-AcAmF-5#lIb%t=Q87ilQ87ilQGsHI$C1)x8`Vk*kNDBnp`_fX zm?GY&m?GY&97@WKiYelaiYelaiYelaiYd;5C)=o);zT#uMg@w!4&se!9j$}-(UvLV zjfyGajfyGajfyGajfyGajfyGajS7?o&L8?)Qhwqb>dug)8~m;5OZhj%fxcQ`gW6oC zY=R1y8`OkrBxN(1*wj4vwYE7SqgW@i)<8*OY_y)vwo+&KGCTXk?rh^03~Za-nPYNi zFl!BzJF`#{HN3OSD=aLmRkpjNc2YZi{yf#=qXDDcczHXI?}}Z$sZE!>Lx<+;K22NY zDI-5p`)*2L#9gCDbzko4A(xJmR1F(RW=XM>RWAGS;|u?F<#K?Ts#Yxn$(Qo{*Oto_ zXXZ<_XjV~kxmJrCuQSnGap}-TX%*1tWomEABpA;DnI%Q=SOkxs-1L_?nk#_^Xj=x9 zZ9=asZRWF}=<2`50Ogoa?e_NeT|W%E(F-WogaWre7PIck*$+t7rc#GT6{?tod(XPzoBM zbbLrav$H0x2TE}x)HSWfXQQsbx{sE!X+)0*U-rJ%aqbFaXhAkrjYiOdaseZX=^TU( zrUBopjesGIGE35b154HBkV9G%c5=c9Yj2EzR)mJAYCl*}&7ts3?_p&b9ToxK2?rGO zQlRI*+etCM6U~bsFDBv)iaB3aHN1;5A`3?tELMiYii?zm$l<_1r77hTayU@AU^k_c z5l=br>FFju6B$$7nj&q)QVujtn@@Q}_iX(5);JJKLGpI^?jp+@H&tzVve}NWGu!_W zB@ZJXj)A8&X+l$+LIW>=SIW&B_OJDz*&EH9nr@`#Fic0)t_O#cdngb|N~hsYG}a?V z12oqLVO6!6^^XJRBaRE4rByx?w@ZJ0kCs${hzk(PG8>^II1t1I2p!68g#K~}1aSd! z+}}59BYcmlJ(@Vd^oqW^Il=6eBdLiA8Y`u0gW!i}SBxbtluqNZ5@=4e(upst+UqoEJ9Rr4sXO=A1E55} zCPD*992}3p!1mDy98A?N)^S`~#?eBNCl*C1`i3U^2XHDIOOCS3@-Hz4!0mNtgGbp7 z4GxTXvTqDcx*8bsaG(qjmXV>a%6rqz4EavIOZ7|nO?L%eP&Qgn!}mK=QBZ;tlz7+a zcMj{wtgY7AS=C7+z-3jZf(8Lc|D?iaB^&B4W~ay1dCTL*DA5}|8`(&mWTX7Dk+CPr ze8w1)-lJ-#tsw%Jq~S(*v2KasebFuux=OOtYnKZ%x!GdDQZ+mj3Fkr^nLV!GIG0}M za(~BMYG^K$uC&dZUWf?i`GZDtH3m4@o(4_TRM)zCgNG;K9Oz-xocbz%-20rer$EAXyYIvx!7Qc?tjBZ@I28f|FfBHux#p3@yrRf2W6ffE{D{7aG zq&?y_Zqsg(=U;2LRc)yiT-0gB$sG#`hiFus_*WS*=Rl5Til`*LvACJhl9B9+&YI8jp=`!^K zX$c{>j{AU)JdMriPNiw#m!^~-^UOBwEKN-O3A%>go)Z4D*(N;A#QErgU!xMflyLfG zD)7rpoPFZAkc7WMIQ=dXIL^?$6QsHg%sJ1ed73d@ zVYX?`ZB6{S1iU%{r*)+`k#~ti=9JoclNN z_KE$QIQMVj9TNLDaqge-l5P!ee_;5aQNyI}Gi3KIgYLaw(k0&<*(G_Up0j01@<{&& Dn!}kB literal 0 HcmV?d00001 diff --git a/patches/src/main/resources/custom-branding/raw/morphe_startup_animation_glow_1_play_light.riv b/patches/src/main/resources/custom-branding/raw/morphe_startup_animation_glow_1_play_light.riv new file mode 100644 index 0000000000000000000000000000000000000000..4cf45ab5c0596b065a71de2464264c2efee1526a GIT binary patch literal 11807 zcmb7K33L=y7OiT!SR!D6fE0rvi&+Sg;hZv%h;((QLytPjDE1N-Wk5tUjHra=fa9Jt6AAOxq0~og+;|$NuScPzPi^}?yu+{2!_Iu z${KmTyg+_iJ|fpQ``Z1D`&;)x_aXOTcb)r)`#bjucfE2*RmZNL= z8@K$eTR!NP54q*TZn@4aA92gyx#bgXSwRBR7_M z+DcNaRB`?0>lsQjsZ-I!%{}7TKNJp*=WMJV+N746n^Q@0@hCPks50AN1gsy{)35VD(5n zJZE9PH`4cui@Zu}Fn<2;9IvX2>~Mis@?88`Nh2hYzd z*F$tYJTL97Xr%kl4|QhD8oWQI#)gFA!51=$Vxj85DLuS&>5rbshN?WHmjk^AZ&nvC z>JkqI?&}^4(e?1d&w()`R-7=7DH-cx!IFjfvCu!g#ojPRq&p+!x!iXmJr=|aV#Y`@ zD?LK9;&D`8Hq@t9oa?zcc=qD=qM^@SgY+<3dW2eFWGHS#dX8_jFZ-T;BVsu{U%Mlo zOD&~iqeyqMtcr)mcof_E{oV@Qu{hG5;8RKA^LOxKt_fDJt7WlI+hU)##Xh_8>bbEt z6Yd!oQw#4eiw7wILi81$-(#H~d3VDkkCF|>#_F*#HD22@Zp^r5%`}fO;{|g+@;Z#{ zHLZOHgRUEl;<>dtGuCZ&$JK}54El^-_D^o@4QKB8Lo8CYc)h{k47mP*UcTVCc2PYv z@91)lYc%q=^YunA%;_UwHYCPedaVH@{+x)XBt=+mL4uu2`>!k3Nk&+jL-w!Ej=FVhR{^Jxqa( z&=-24%2BM!`SdulZCH|Q>j?&DTbq3;vW*y=v8TqM`o1}c+S)(4QS}jH$Ic6@Z%108 zH?npL>VgbDswtDhL+ z>L-S{`fWza>L-S{`iUW~eqxBLpBT;xC#!y9I5ACD{Q*1nyoxESe;_=GtDhL+>L-S{ z`iUW~eqxBLpBUomCx*EC2PQw!p!%=V&FUW_b;{Ykt=gCS`i`wJWhg42%G}U_n_(S@ zjefmg^t_`U1y;dlXYbeZqwz;1SkQr?9Q7d>hgb(jSR+lT1H=&P05O~_Nu~qD5bJIkfcN% zm?}%{lxZ?5zAlE-)O2d$L|{Iaz(lzNrhOmOrV4%*QN+(DOc9qnQ=E)QR`W~|7rir< zlvU3ZaoICPT=$^Z;c>DwS>-=U;SraMXUf! zaTYw81Te*kZZZu3#a;)o3Oq&YAeI59h;@J|Vj*CPSP7USmI9`TwSXyNF#sj$`;SC5 zST5tC;04kE=QQvl%!CI4n2-c;DAwRNB?&8=mIS7VC4nhUh9r{&ridlM8B0n@V2W50 zm?D-0Q0(wHNt;X();28(4ke`|Fhwj0Oc6_hLrEzKOc6^0Q^bE1g3~3fhl51V2W50m?D-0ridj0lnW$bZI)7&C<$9h65eYp z3GX+S1eo*j#uw=w#amxV(uq%T$GuI-!@rxB2d0SSfhkV5B$EfGh~>c}VoJ<}*X<8l}N=kWPidY_)B9;e-l2RU+B9;fHh~!z!b4OFhwj6OcBciQ^fMX6tO%oMJx}XTp$lGAO4u$BHHf}@OK#; z2aZA{iX`docn-dNWYUpAlJXly;oadeTQ2Cyf=qhD+EYPaPLrsM5V@`kSt z`9I%jy$zm6=BWjiH#X~{mNz!{eQ9}v>3DmRyx|LjnQ3`jNanFuEpKdo{fXs`P4h)G z78a)CZ7IBQhA3!t$Upa2%jGjV9!otu`~H=)%b|Lk<@rsa)qf=LR1Tx@Ei zTHyjk{AkM*@kYfIN4Jt~R7??XRL)pZZd6PWZ&XYXZ&aYz;c=ui*+#X7!XtjPbtow} zDyE1xDyE1xDuBrZ~|}wo!p%uY-7_+DPjlezawZ zc%x#9c%x#9c%x#9c%x#9c%x#9c%uTPne&JK_LQGEhq^N)>1uyR`cnP{aiFhu*r2wS zEAK)D%nfQxXGz&gCN?!seuHgJ$tc#zY%oxg7#nS(v+dLwzRb=(v^(4K9tO6(f$9KuD0o0~T-ch6Sb)TlK z^^}tzsaqc}KEf{qSqNfO1VJaMOb^>#m&rfK+WJb$C?4)sZ|0 zuNTc_)FY(;Fg%oG`pXTBZhpYVRPBs)Gs8Q491Bdx#W(l=#Dxw%AP&U?GpcMD4WD^_ zFEtkqq|iJ@ljqB-hJ!?kpyDMeBmUwF7rCVoQ72P%vW$pbfh(tPUlTWQ9_Bljp7#Nz zpb<*PhXgb`bJ`}L6gNU$v$pwc)D_tH{t7mY=n>(I0arNAU4aZO$fl~%2-;CDU_>#U zgOFt!@V(9m7}BUqN&2r~soDZ^NNd7QjvHYefDzD&&=6Jadn>9r6uya>R+iCW5%4W= zKrt@`TK5XJ`#k?GXr6yMNW>b7IbT*ayo)j-3r83%R))ihiI8e1@ zH>HyiPdV_>$yPoSnNZx3B5lM{4zx~NOnF52Z291ZI1oxf@>X|Sk>!n>sx~LtY$rFG z?f;09hmnuLz|&f_qAAXxffv9l<>uAk*>?nrnlws@mM9$AR+^#|6&Psve8mrN60fd#XUh1qfxCjnEMs2;u^S4&^pNe>nt# zxBxlk9~89_zDLy_NSt7LSh_KyYQC- zphUnXLIX$~9FM@j_R$C&Ox1pBO)i32Y-4%FB*-|9(4K?zP! z;$0_SuhWrP+pV#)s*^^5%c@QV4FZn-Nrlf!Hq>3rPLIp;R>h4`qBnXrvXMr~M)_qU z6OWhsj4>v?N7YVRLj*2KV~p@(-4et5v|S){m1L>^E*EBUoyCHsYIrCT&V@EIdtASA zF8$Bs{)V}1qq$JJ(l&E)DI%Qb4;syN7~ph!8Z=c?T^s8S9-f4ApodX=4!y_DNwFTJ z1aM>&|M$J#aOU6}HrrW`3k5^f@K9wfej}wB-?DT)5JPGH_>V}6#s6(e(-$5oUbJUc zHq0)gJ>o@f({7UI-(a^@ZG{zF)M@3xO}7vZ(WtC{t%#b{)>Tz|-i|Y+O9fJ@|EH8L zcUkEIOV#ShpDh>}cH+_6P;CHhZ*GJzGyR2}pr~Y3yBQdjF{vIOkI1(i=+xtKb3GX8 zHS-*42_ZL3dYg_sEzRl9q-o)orj#Fx%r@;TtxWtOx`yAL68@~&COpl=`RIaQqY}P? zaQbB`@QX~Ged4!}guhHU{VozX&d|n5z!ktDRPVo=qB7U0HLsUU<2WIJ8-=6?rq(Yl zLc1QkSb@`UdggBYy%2Cs)69-=wj;BH(~+2fu0SxoX8H%B*?s86)R}WiRJBXYIWMMp znlW8%wrS3tP5g-jyfy)+b*2=2FackZfOj$R=?OTWo9UOr@PBDy|7Og&e>3KpiT#^6 z_iy6e68krC?%%|-68krC?w|3JKFx2sd(7~0qouMM*?s-+JMNNn$+tjuNnWYnd|8q_ G(*FT3tCBSU literal 0 HcmV?d00001 diff --git a/patches/src/main/resources/custom-branding/raw/morphe_startup_animation_glow_2_dark.riv b/patches/src/main/resources/custom-branding/raw/morphe_startup_animation_glow_2_dark.riv new file mode 100644 index 0000000000000000000000000000000000000000..0e7a919e3e3dbf16a0a2819165b913ed93b2af69 GIT binary patch literal 6941 zcmcJUZERCj7{|};dN*tW#ckm3AlNMIMU+)(#>(6J21Q~iitUO9W45(r)v+#J39=ZS zZ^8J2FUdm22NM-!K^=*j`GT^oL8G`iLsUpK2II>oi61mZzc`=gIp_Ap(Q`ie;db}# z{GaFipY#0A?e5*~9i7{%7_;hCixw}jSJy79GaL<#P0cN9Ty5>{4Vye(pFgm5TQCys zj6J1|Xh*ek+OOIboxRK$J0*_4;AaCIRyfAsu)u+1m2f!VxQ=Po%AM)VKyL!(x)fN1 zALlek1X`V0Z?o9~>48I;)V|)VW8KZ`);qYk)p2Jc;poj~2L?Afo%>SR-UHpOJ?Z^U zKI|J9aPFt9Yr8XD{qa=)KAVjaRt3`i*+hSKF=KlIOYox_Y~W-Tt2LC~mu3}ug;n?W zCHmuhWT|d-XslA#>+Xu|$QiHJ--usvSD@aQJGXtB=h$m&6FKnA%;C%D@bVnH-$_lzFq`Sae^noU zTEgZmhyBLAQx0_c*<`eBrq$Qkwywh)Yd?JtSIn=z)^8jbu}??PQ4~KqtGC#_vDX_O zGjo8`KP_MQBB>YFd=&lhj}c$zLxaJo*p8afe2yH?aj^bwPjvPBkBeF`8##qpat6%q zFPg>PJm-ns?5W^69)58bt*KD77!|%wT-_L+Q4YoqiySBa%6Xy~$yYr;3n?1jyfIfa z3)UTb^wu+8X7;b9j%v0aY4H{5s+1{5v=;%pj5^(okZ>TczJax3=}<&f?t@*&+%IE#Eg zxt04#IF;-t%CTTS(URf^>?fg+G~cakKGh;^zEG6ilV<;dyymiaSL#9KMidH3MM338 z6h6vs!~%-_iLMNw7%D<>C!rWtq8L`97*?W4Iix6(4=IW`iyXzU5=G%uf+FRRq9_!S z#)XwAQY}&xg+hWN@84b}ihGqP3WcPiy-E~?4=IX^=&raw6$cW#i%9Gt65~n|<4O|a zN)joDltl6&B@t(llNeW$D4a@2q#RNbg+kJ|xROMwMM|PjNJ!-U%P2|AC`l9wNkthY ziNc4JME?DJFcrt|=Ld>VJVYoCDN!6!qBx{Pk#a~;Bp*@~aTYm>LrN5dQwfTcLyDqM zNE$b!M3HKdq9_y+6nX!i)RZiOgjP=SY-LI$MS(>1bY*&x@@S(wV|QOVy&r%)0h;m>^L-^HUc2LqE?VWz&>M`Un!$ zqXT)7KRcKb37?{yFR1|qi3(6tA}I_cDnLyyazI1$!mGRQK*Ijh=gX&n(&L)7Vppbn zF&qBb4=?3TXtj*J5?IED&j;w`m5qmF6J33Fc!A=9kdvh$W5pr0y0yi^mg}dqHBfgB z-q9WQvl{(%t!eD;RrAe=Ex#N;&G2&A!)o<<3K=gAIb9k8m&GB?5K^tbr8VL&Aw?@) zzoRwcG;rNcDJM!(zNH|7K3^1@|jA#e%<4^iO9rGcMlO}{)$N?I_uJo~A+U|{uQ417+b z$n&L|_X01S@t!3$dB+mo=i zFnGZnT&A!8o1K4#dfj7i>>wjFjVU?e|v0nRQz^!CM2}iw~N=Z z?#&y*o8YG4vatHLYPKrCf|y{dW!pCzTrGvGVb4|#1lCdkj<%F&H=5d2fld5WBnfgE zCNUN!!7!K*^NBDNR)BCR*{K~r>;XF$QQ$VGsBgQr{Gj1lpN}T{<)X_B7A*r#x`Z1P z#WcW`7gvOr52s=vUx(rPK!f?Ak$g2Tc?BHRp2JR zEW$_!_lv7IS!Cqc=oMeV$g$B!xRLI>k!13<@KOc0iJ}BaBy~Jri02oS7d~- zL4UyrtcZ^!59Ez7cDcxi?c!s&+-Aqi400pMWYTO6o#u_;9-vu1D>3qWk&$FF$Bi68 z31=^vY$|CUbgJN_%%Hg>K*mBy!a>-y+{qUuPWA)}O9KD4@RQl&D6tbp_%#8FuQ8?H zY_PdwU9Les+43Nrin2GGI>U7WGcD#o!Z(cBAyn+as?CCDXWq!SZ?oAxbLp=-Xt+)m zT;exV!6m-vr`&u=)Go*VHMxd`C44 N^Rg`?@Smv;_Aet%nN0uy literal 0 HcmV?d00001 diff --git a/patches/src/main/resources/custom-branding/raw/morphe_startup_animation_glow_2_light.riv b/patches/src/main/resources/custom-branding/raw/morphe_startup_animation_glow_2_light.riv new file mode 100644 index 0000000000000000000000000000000000000000..1f91ca1d296229fa02a7ad2e03ded0db7bf01de4 GIT binary patch literal 6941 zcmcJUZERCj7{||TdpBINb&QGLsk2!a8z7s~lx+fQZwtl6G8Ebs4aV%H-HO|~bR~0y z=

    oYVaj4X8T|wq7cxL2*DTBy#$Rfn-CL)MB_`8Z}16Y^o#R(&N=tCmvJ9H`Jv^W zo!|4E|8t)IIc43BmbNX0PUcbzmlT({%gS%QRr6F;udJzE?e#S@Hf`_+^k8W7mT)xI z7JptDQ(jljDwmZjDtnVLc1*thhJUNz<$~8ycsbyOa|Q77!0Q&KxC*wVGXp(|LdGtJ zmf+i21rh=4y1%F>lpYw)q`G^uo^^MxTkjEQoo8nv;pxd{2L|u0ukTJ}d-itJb*B63 z#k4*!P~XRMuIqF6&S?#TgJ+sjt-7SIG?Rr#i!{9hRsVWfegGXnypN6i^M87yx zG96p{%38hcv&>`uc>DPkfuL@qp4R`e;WJ!&oRC2iNWx6yk*(AnPXPu7^-;KA6xO!GqM)U#+AYbIu>X+wT-Po_uvX-&0A{YEHckcQ=ktJO^W=GRKj>XZUmtDa(XIWcVNG+E`$C-rl5AZ>B>xWO^tgx_DtmGK;Bv= zVOtRrP!aFW^(#Vx>)<)ASJ5(Xsro>BCOc^O&j|-4)z#ITP&Hml#SNWy8?S|O8WP}E z!f8m*P7|w8PD6rrn#e&pP3BnSv}gY^C zGo4BB+q)o=$Yv86bv-{V4IqQ=Y$Fi#&5Z;uX@7Ie9K?A-f<8}T73w@8L7yj)gE~(# z$D;GZw-n!0ldx?*5_I#04(jGM3EMN-X!eiSY-{f@?+$fyn}lseNYDon3Hl(0O)K^% z+A|g_MtCXPAO&s3Rv>6AB7t49x1!8JSP==@ieeSYib&8_6gem>${d6hO~STuNYGXk zIw&iegsl}@&Hm9=+|9c~SG((Ijl0*k$&Qc4CHihjOAx z*j9uD?L;JKCuYn`aVXVg{d|6$mof|~Xe;gmg0>9?FY3#I`1m&~D@iZAU;Ht|ERjcJ!vx zeU`iNpu#J93H8#Jd>IkilN_N$r0L?av{o>9HEUVvrx`7vJlqHA+~+w2yITO zg>q*Ov9%{4_?tiPCT-9+ZfwvTVp|wTXoGTuHYgxq&s!u`iL%cr?Zo!c`V z#cbqKP(7?LS2<&cLN~LKuS4qFc_GN>PRAJ?{4)|7U`D*+X4Zffo z4XVc!wru6mhbtFq(6;Jv98R8KI2`fg5JM^~A(PgS6V?zIVn_`utHwO)`(j)NV_Zf( zCB_Ywl&3Mr!l!$riVtvjd7)dXfZ>=+1qKsuvjm>ufv?rbz)x`a)0I^+5QY#43u z0;hT4{ySvg3=Yr#QY!;t2!X(0;IJj|b0jYfSIfXJaQNAm#)iQV0)fFmaAE#de~swH zG#3Y3{S6K$KQtm?2$8^GBy_i|`q%WG3+g%iF?_u`QXj%+oP<7>RiB)Kz5On45}t4O zL(`fA6XTj+_j*q@2eF6n(@jMengiHg{kMJPe&eSTRU>Gcj z`D7S^P5?`}obt(0KghX&K%=olg>R|-q~=|3MsxXg(KeHL+klIf2!XPgDi{TDM@0GX zC@0K1H1Ed>tPi~uyLmB?lRrOqSAg$k#8fyx_o97$Hj{=z-kQqLU1D3LkY=G5cX29D z$i(;+Jtt&h{D2VBVG2nm-?0~BGkJ{=Gz4gpLR!P~b92qS+_QN?7#j@cgussI9C_Rn z!q~5QLW(XtjoWQ>+-6b;NhXseI=gv2ha+9-H_?p?Ee8;$lETYue2%?qDChD!Ww%BjS! z`gv}#B{xmzA=j5>CTt(lN$(pY+azu&$4lD)tF z(?53I%kTR;_x#Q|pL4jZ&(8HYQW!?7OG!;j&&bRg$?*o`gxraf@=S$A#WPC;i`8Z? zb2wdYkGP%P#lFdQvHk2_9n;P*%m?!80sL!(mlj_8;iZ8WT1$qP0bWlqtTy=>UvpEf z7w&b*o{HYOSTF=y9i#o3UT^m`t!%DaTI)AVpEiAlfr#@B3%p)Kt>53&GOM6qX`R1z zSyg_uuc3e}Tbr5+8nD%=Rn3)+QeERxy&fA(vHKeR-bQ~K!z{F?qqj_O1Kmuc)t38~ z`j{kLl2&J{_clsoWrR*^V3}l{j+;EN&c>fH-9F*^JmXIR_lvDA!GrdS=q;is_~peD zF7y*Odh>u*5ID0wTrPM9cKt&Tx9DeDuwu)kIvZd4 zlhIb!u{*;8?!q>wCGIZ6&oDMVePOpX>MrP35kJm#%?a?!9v0XVDi#OX5PxW-KU!g3 zGua{2=O`!Xz^hi}`1;?616MA5r=c8DLgpw{L}y}e9u z&&@2ccy9kLJt($nQi$T?_sc@Om|bB*5fj`GYqH|cvg=(?yp;tCbOI8w{wwDL;-M~G zDUJuWvEsM$ZLUv81O)e%Cz4C!;(-;HJLZR?Y?OHa7!VzYSz;sD26tI7rXu9ZJ%3qn z&uL?av~fHS6cIo6>Q4ql*U1eebBmYQLj0}s&xKqkW)BGN(Wh1k9;a_#KrElM&x}3Y zQECnGPiJNx zr4I4yicF#i{#h5|*QPHIxxPJC5OfQD6D^*Oi>J+^&(T5df;0EQIDg3XV$bBD`}8NB z=D2v^>^BXc7JZg4_WWQLyEp&Tiap)i4HYz_rMuH*dMh{RZofCz8fODnp<(Ul&Zu}i z_}ef+oO-^3czU%DDtYdy?4ax2`L_e^oX>0)PwHm}1kwI+g}^A(z zYYw;f?IRX`%(3gy)SxUV^(@=?@wtQ1-b54{L~KQ6v%iH9o1`;pQ(V3#gSEK<92tl4 z9nWn>U`=-bSa6VlduQe5*kT;d;jjo|La@ejxYYg-TPCfjOmZsjp|FhSoM>mX|bUl&kdEHew3b1NqS^Iicor{qx8HJpC0)vV~0o&Iu-T(*&rCH6{xY|Nm z5&8s8&ZyPfe15;LL5|W?9Ho2`rLC(UO}uP5pR_^KqRVm{jI*4qlWj<-g}s4V*kvg$ z()pYowXnw$x3GK?C4b@;mQNymIJSjZPz!r0B?XmvKFO9R+bUdiFXv+U%8W zsLWo77-jZ4qRMpW+3VnFvll$2%w9QjDzjJ4zRK)%kd~t|d*v!rnY|8q4d8!GTZ#3=AeRCpySyx=JXUO96r@XFa&fmfo!D_5Zkyb={& zLd{w#ylbiO$~IKtO~sS9p-uvJH^9Sqc=B#!F}<5uD!z(DgDP6)P$vD-soyK|&kEiWL=@K|-S$B#~bkhn~eC zp$%h@(11q1grBvBeiR1DP!C(t&%z*~Jq&|{_B0F<+DMjhdWWTQ4HDWDv0_D)Y%-Q# zu9ML3)O(`QZYOBJM@L(csGtoJ8rmSCp$!rm+908!4H6pKAfcfR5*pedp`i^D8rq5# z6|@zJ3fdr{p{-a^K^r7Av_V2c8zeNeK|(_t`9eV(Bs8=^LPHxQG_*lNLtDn_1RZUV z(9l+_sG_aML%XWp=W77A1JP^;3EQ1?wiSsA+aRH_4H6pLAfd4h5*phep|K4T8rvYD zu?-R$+aRH_tyocETal=+4H6pLiWL>MK|*62Bs8``LSq{wG`5j16t+P^V;dwiwn0K; z8zeNgWt^_i*#-%XZN-W@+vWJnuB!TF-i(`PMg;-zFIjEU{N}1OX8kQ2d?9z0&0?7M z>?4`=-FE!fGd){lBLs5(lDcRH`zRAp)+J0bzzNdY|=+7 zL*veMow71N(%oWn_b<*FoQVE5NiGk6$e_#h0+Xd1hd19zwE6F`o8dBg^HkWJsq1Ga zpdW&;aoM+1-#SZq}mvjxbv*|NKh7px}JQL_!FKU*l(Uo=}#VOX!P$`X{H z?rv+7f7;tqE^EgW$zSaj&z?E2gkgA-hIz6ulgY6&PGq1=<9J;IS0mtRy->!2KpQJy zVRcfBc9x>ev6qmKvJoedAtS~|ji7CCLu60hhJ;BV9D&`*I{bBK?#Fh;ntpooy;!$)}`d=B0;z{33@ktCXzED`s6d!|@$ zG^@fM_V%QHcrE#-n92&k5KP zPqxN*vd|uh3H;{=8+P_fbh8XxNNC`I;x)v9O-ZrBtjW|uZZ^t^Kg!3Rt>}2uRb(lR zlqAe5B}GWF5S>~ng5I8k;rz{3@~4D;$VJ|CJ>n&raS<=!3RT(v?&XdDo0o+}84LWC qey`z~%Iey>Mz7&%X6AM_rMz;bw;4VQjP*@c!&sQoT`a?xng0UyV3n-^ literal 0 HcmV?d00001 diff --git a/patches/src/main/resources/custom-branding/raw/morphe_startup_animation_glow_2_play_light.riv b/patches/src/main/resources/custom-branding/raw/morphe_startup_animation_glow_2_play_light.riv new file mode 100644 index 0000000000000000000000000000000000000000..0d7760bbfbe0ac25b012d2957dd34b2262213b95 GIT binary patch literal 9065 zcmcJVdr(x@8NlztUO)_BC>pO?M68HJ#kfT@%HDmzT3ezt?n6xz%VT*sz!F%E&jw$O z4?2l6+60r#NX9r$e5BP*v^8ayl|+l6cEtLKtudpHoodHSXtZrC{l4$+-3M~N^q)F& zanGLL`R;emIluD(%k%S`Me(tMUW`vjN=`{l8$MFX7?(MILe|9W97A6I%vrN#li6Y| zEV9`hPS-ZwF5R2DPTgJIJyB>A1mT0=wIBY^h06k0Gh8`v?SX3#xJRu3H9lV zJyjktMu^pmmRfgxh0+r%49n1odLeG)q`p@z(iy|;6ZX$i{@`;gYq85t9PFY9qDtic z^wJ4CzU9C#PGDu%M=RFIA`%zNe`b*$9?Un{+qUlWIfj0?LU#7;`kU<9oH)xw#OCZ8 zi&Xl9(PsOxJ41YqyjGiOz+Hx(5iC;jqAqj9UGP@nJkE8_^-0Sg_=NJ6*%7SO))tiy>0coc1$i;h@K^L0@9F$)fRig z?d7s#Ug~U<^Y*Wk{jQdnc!l`*{lb9c8dGAy8Iv84D$@o&%dU5Ucq!tUu-HEx=93+ppNcCOFdsx*X2p0;SE0%!NGTHIykZS?IR{>?6Dh>*5E2A`xyy}@%?iLBeRJlV)SB3X`{DEH8xfsZ}&80 zm>cWFOiE-t1lfdQO)mnm;6Z$j9i^KhH{R~l^!y1!v6yYl%V)cm@kJsy|i6Hcnl7Rj09y@fk)U* zJi@M2M9nl3iFkxPp>>2IL9^u5I>L~knLZqSgqiRNdpSPdEFwXxJ%DO_Bmq{XRHc>AsWBW}2PPT$}56Ej`t%5S050JoYv`3CCW4rY|ygj4{};cd{Ig zy||zE70_KOmcy}Em-FL*{cHssdpGX%6*!Y#@9?<}q&s9HT)W{=+x%cjz&`yaU&@Z& z^-kG2rhC87)%`?|k%;i2cJ~1sYOA+(8J(};p|RogqH zi#8 z;A<}u;cYrt?L~sGy-4u2m+}Z5?t|4{BnCU&!?hO)zV;%)*Ip#}+FKN|hOfOZVK!{- zMS`!rFNNf-cf%=(-#n}GmQfnz5Q3MtLM1fiMS_ zA!~Sf*I_oSyh!l!t^;`!sPfj;R7APEUPozc&_N8|-i;wdQ(r=Oe+l6QCWJSb5ME(I zc!vq$B_@Qom=IoLLU@nEQ8YykBbp`?!mAvPqUkaryv&5~HWR|@ObG9@k_#&|A-vIq z@JbWHJ52~LH6ZxoFzLi={SNBHdrb&0HX!Qv)UVW3M1N1cN6Dov6HX*#)gz#z;!mCXPuQnmP+Jx|G6T+(LXqVM`JatiJ+ovSs z4~eVkc1H-&bej;~Z9;gr3E|x)gm;?|-fcp7w+Z3hCWLpJ5Z>)@6iv6oh^E_w@NS2r zXu3@Z?=~U4+l26L6T-W#pyHjqS8Q~|vV;3}*B?-;9EaG2uf<8?U-m?xDn!Bvx^@t%}cWG_PXDRdK0Yh#? z5BWkTq-DpzCt4$NYxyr~nuleK?IdjCZH)uea}n}Hm}{A6bu1P2muU+*r3yh@h^(8_PfCt|HkI| z>)F8=(1aMkV2n1c82vct)~X4?7|k)zY(K3|GBhCuFc<@N-{1?i2?t&FQ1Y-)o3VNL zxEceR5Ca&D0lOr1!sni`{o+=98$PCO&b8ue?1cT0I^n1f)}tk&6JC#zVabiA-K~;r zHW-c?;bYup8Qv{Oyks=t<}lw_ohGaA?rv=jzT4AX9AwAl2S3`KIcL`V*@7S$VuVq7 zsY1F{uwj70n4&jgpfwCy&2pg*0DUx|gWl{Y_F0upx6W2x27}lX41;7o9t4NM4KZGD z7=p0?4x@C^4!~zilL;*o&WQ5( z@G{sDdJf6Zr-S>$S}L=7*%VH{r+c!AX0t4mVNZ99_Va0Usu|MKq+2H|(-KU?sN{>Y z_|J$$cDLR$hZEV|x?V|SJtE3XS61%R&P1bgKqj~&AgN#`^KCud-9~!e|3qXW2u+r7 zCNL#tLg42gH4{PjJR+0COB->*)iu}XR5GcotW+0=UWPJ33+Of*ai@nC=c|ZJDk}p@ zCcbbcm6cgh%Li)}&P1bAofFs_k8hLtHqchdWJ^>gi>%=}fj@t+P_j@|M&+f@M#7Q< z#OvsRMO|XmS%aZTx!DLCy$n9~9F>y{aMFy*OH#HZlq+?KkXVFItxkPU_rcKm%~97+ zo%%sLB?HtkHA}2=;VhwryX=3@5{|t8QP*B7R+zzVg!>=unYda#VGt2;aik? literal 0 HcmV?d00001 From 793f0447ce2ec70d7c02c8ca005b09000a0f70b0 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Thu, 29 Oct 2026 08:18:38 +0000 Subject: [PATCH 152/189] bump(Reddit): Add experimental support for `2026.44.0` --- .../communities/HideCommunitiesShelfPatch.kt | 15 +++++++++++---- .../reddit/misc/version/VersionCheckPatch.kt | 3 +++ .../app/morphe/patches/reddit/shared/Constants.kt | 6 +++--- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/patches/src/main/kotlin/app/morphe/patches/reddit/layout/communities/HideCommunitiesShelfPatch.kt b/patches/src/main/kotlin/app/morphe/patches/reddit/layout/communities/HideCommunitiesShelfPatch.kt index 154b68358d..0ff714fac8 100644 --- a/patches/src/main/kotlin/app/morphe/patches/reddit/layout/communities/HideCommunitiesShelfPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/reddit/layout/communities/HideCommunitiesShelfPatch.kt @@ -13,6 +13,7 @@ import app.morphe.patcher.patch.bytecodePatch import app.morphe.patches.reddit.misc.settings.settingsPatch import app.morphe.patches.reddit.misc.version.is_2026_16_0_or_greater import app.morphe.patches.reddit.misc.version.is_2026_18_0_or_greater +import app.morphe.patches.reddit.misc.version.is_2026_44_0_or_greater import app.morphe.patches.reddit.misc.version.versionCheckPatch import app.morphe.patches.reddit.shared.Constants.COMPATIBILITY_REDDIT import app.morphe.util.setExtensionIsPatchIncluded @@ -44,11 +45,17 @@ val hideCommunitiesShelfPatch = bytecodePatch( ) } - val legacyFingerprint = if (is_2026_18_0_or_greater) CommunityRecommendationSection_2026_18_Fingerprint - else if (is_2026_16_0_or_greater) CommunityRecommendationSection_2026_16_Fingerprint - else CommunityRecommendationSectionLegacyFingerprint + if (is_2026_18_0_or_greater) { + // List style section was removed in 2026.44.0. + if (!is_2026_44_0_or_greater) { + CommunityRecommendationSection_2026_18_Fingerprint.applyHideShelf() + } + } else if (is_2026_16_0_or_greater) { + CommunityRecommendationSection_2026_16_Fingerprint.applyHideShelf() + } else { + CommunityRecommendationSectionLegacyFingerprint.applyHideShelf() + } - legacyFingerprint.applyHideShelf() CommunityRecommendationsComposeMethodFingerprint.applyHideShelf() setExtensionIsPatchIncluded(EXTENSION_CLASS) diff --git a/patches/src/main/kotlin/app/morphe/patches/reddit/misc/version/VersionCheckPatch.kt b/patches/src/main/kotlin/app/morphe/patches/reddit/misc/version/VersionCheckPatch.kt index a5db7bac7c..67d4fc2ac7 100644 --- a/patches/src/main/kotlin/app/morphe/patches/reddit/misc/version/VersionCheckPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/reddit/misc/version/VersionCheckPatch.kt @@ -43,6 +43,8 @@ var is_2026_39_0_or_greater: Boolean by Delegates.notNull() private set var is_2026_43_0_or_greater: Boolean by Delegates.notNull() private set +var is_2026_44_0_or_greater: Boolean by Delegates.notNull() + private set val versionCheckPatch = bytecodePatch { execute { @@ -69,5 +71,6 @@ val versionCheckPatch = bytecodePatch { is_2026_38_0_or_greater = isEqualsOrGreaterThan("2026.38.0") is_2026_39_0_or_greater = isEqualsOrGreaterThan("2026.39.0") is_2026_43_0_or_greater = isEqualsOrGreaterThan("2026.43.0") + is_2026_44_0_or_greater = isEqualsOrGreaterThan("2026.44.0") } } diff --git a/patches/src/main/kotlin/app/morphe/patches/reddit/shared/Constants.kt b/patches/src/main/kotlin/app/morphe/patches/reddit/shared/Constants.kt index 11de165596..88d6cd29aa 100644 --- a/patches/src/main/kotlin/app/morphe/patches/reddit/shared/Constants.kt +++ b/patches/src/main/kotlin/app/morphe/patches/reddit/shared/Constants.kt @@ -15,17 +15,17 @@ internal object Constants { ), targets = listOf( AppTarget( - version = "2026.43.0", + version = "2026.44.0", minSdk = 29, isExperimental = true ), AppTarget( - version = "2026.42.0", + version = "2026.43.0", minSdk = 29, isExperimental = true ), AppTarget( - version = "2026.41.0", + version = "2026.42.0", minSdk = 29, isExperimental = true ), From 9fda3d2786ceb69ab499a8fda93f744d6dea7494 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Thu, 29 Oct 2026 08:23:43 +0000 Subject: [PATCH 153/189] chore: Release v1.49.0-dev.11 [skip ci] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## [1.49.0-dev.11](https://github.com/MorpheApp/morphe-patches/compare/v1.49.0-dev.10...v1.49.0-dev.11) (2026-10-29) ### ⚠ BREAKING CHANGES * **YouTube - Theme:** Patches now use Morphe Patcher 2.0.0 Co-authored-by: MarcaDian <152095496+MarcaDian@users.noreply.github.com> ### 🐛 Bug Fixes * **YouTube - Ambient mode:** Ambient mode is replaced by a black background with the new watch page experiment ([43a9e4c](https://github.com/MorpheApp/morphe-patches/commit/43a9e4c8935627af8f1748071438c25b653deefb)) * **YouTube - Ambient mode:** Bypass Ambient mode restrictions does not work with 21.43 ([aeac8c9](https://github.com/MorpheApp/morphe-patches/commit/aeac8c9c15f93127a41326a3e5ae913a7c75dbae)) * **YouTube - Feed card layout:** Movies converted to cards are not hidden when "Hide movies section" is enabled ([5c6357f](https://github.com/MorpheApp/morphe-patches/commit/5c6357fed1f52c5ed5d0236a393392b860e8d5ae)) * **YouTube - Feed view count:** View count badge of a card that enters from the bottom loads only when its title row is shown ([26715b1](https://github.com/MorpheApp/morphe-patches/commit/26715b1723f5755dc13a64c8b41be4a499f4f2cb)) * **YouTube - Hide layout components:** "Visit the community" entry point is not hidden in the comments of a video opened from a playlist ([e11da94](https://github.com/MorpheApp/morphe-patches/commit/e11da94ca64632956222074390f67f7f13bd82f9)) * **YouTube - Video quality:** Buffered video is cleared with the placeholder playback times of live streams after selecting a quality ([36e3612](https://github.com/MorpheApp/morphe-patches/commit/36e3612c0bfa7435e47fa9529b12a819b11ac843)) * **YouTube - Video quality:** Extend the background resume fix of regular videos to Shorts ([958aa55](https://github.com/MorpheApp/morphe-patches/commit/958aa55c56ccef1e36ea1003f0f869203cc828b7)) * **YouTube - Video quality:** Quality selected for a regular video is lost when the video reloads ([16d4e60](https://github.com/MorpheApp/morphe-patches/commit/16d4e602c7c8961b35bd737b5e5770312ef1781c)) * **YouTube - Video quality:** Regular video returns to the default quality when it resumes from the background ([e5cd752](https://github.com/MorpheApp/morphe-patches/commit/e5cd7523ba2963b29fff6c3169cc71e1a58f405d)) * **YouTube - Video quality:** Short loop restarts with the previous quality when the quality is selected in its first segment ([a325744](https://github.com/MorpheApp/morphe-patches/commit/a325744ef3434bf0fe40525d4e2da9b696a2f2cb)) * **YouTube - Video quality:** Short plays the previous quality segments again when it loops ([328b85a](https://github.com/MorpheApp/morphe-patches/commit/328b85acb6fe32b7722dda7fd41ff2a8988e9c7d)) * **YouTube - Video quality:** Video restarts or plays faster after selecting a quality ([c000b53](https://github.com/MorpheApp/morphe-patches/commit/c000b53eeae02941f82a37e1c66c12a06c3d1059)) * **YouTube Music - Change miniplayer color:** Miniplayer color does not update after track changes ([e1410e3](https://github.com/MorpheApp/morphe-patches/commit/e1410e3189531c1fb4ab41c576c4fdd20aa89e36)) * **YouTube:** Feed thumbnail badges are shown on the page header of a channel that is live ([394ab97](https://github.com/MorpheApp/morphe-patches/commit/394ab97af76d788d6b13fc4c47f8e2ba4d892cfc)) * **YouTube:** Feed thumbnail badges disappear while scrolling, are missing on cards with replaced titles and are shown on live videos ([4344fab](https://github.com/MorpheApp/morphe-patches/commit/4344fab6ef2e8aa6694072f38478238b84e0f42a)) * **YouTube:** Fix TalkBack not reading captions ([#4136](https://github.com/MorpheApp/morphe-patches/issues/4136)) ([b22841c](https://github.com/MorpheApp/morphe-patches/commit/b22841c5a6ddf6ff38b49f262ca5aa893348a600)) * **YouTube:** Prevent TalkBack crashes caused by Litho hover recursion ([#4129](https://github.com/MorpheApp/morphe-patches/issues/4129)) ([4c00210](https://github.com/MorpheApp/morphe-patches/commit/4c002101e3472e9700acb8f92a63291f69b61116)) * **YouTube:** Watch history search bar from getting hidden when selected ([#4130](https://github.com/MorpheApp/morphe-patches/issues/4130)) ([a4ecfcf](https://github.com/MorpheApp/morphe-patches/commit/a4ecfcfc3ebe99ba452e5e87c9767a83c4760c25)) ### ✨ New Features * **YouTube - Hide layout components:** Add "Hide 'Unavailable videos are hidden' banner" setting ([#4134](https://github.com/MorpheApp/morphe-patches/issues/4134)) ([a7ce357](https://github.com/MorpheApp/morphe-patches/commit/a7ce357d2b654aed2a241c901bf2bc3df2ce7274)) * **YouTube - Minimal miniplayer:** Swipe the bar up to move it to the top of the screen instead of expanding the player ([f8180da](https://github.com/MorpheApp/morphe-patches/commit/f8180da70c848560c4fe17106afb49ff6852e564)) * **YouTube - Theme:** Add "Glow Morphe" experimental splash screen type ([#4135](https://github.com/MorpheApp/morphe-patches/issues/4135)) ([6dbdf82](https://github.com/MorpheApp/morphe-patches/commit/6dbdf82457c5f8c7906b56f04640baf7e908060a)) * **YouTube Music - Third-party lyrics:** Per-syllable romanization layout, dynamic secondary vocals, and typography refinements ([#4090](https://github.com/MorpheApp/morphe-patches/issues/4090)) ([a5d943b](https://github.com/MorpheApp/morphe-patches/commit/a5d943b40369e1a6050db6b5a51a5104a2190ab9)) ### 🚀 Updated App Support * **Reddit:** Add experimental support for `2026.44.0` ([793f044](https://github.com/MorpheApp/morphe-patches/commit/793f0447ce2ec70d7c02c8ca005b09000a0f70b0)) * **YouTube:** Add support for `21.30.216` ([89defd5](https://github.com/MorpheApp/morphe-patches/commit/89defd5d039e9dbcc139866cb5941cb3b592559d)) --- CHANGELOG.md | 41 + README.md | 10 +- gradle.properties | 2 +- patches-bundle.json | 8 +- patches-list.json | 2032 ++++++++++++++----------------------------- 5 files changed, 689 insertions(+), 1404 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3561b1910a..d2464322a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,44 @@ +## [1.49.0-dev.11](https://github.com/MorpheApp/morphe-patches/compare/v1.49.0-dev.10...v1.49.0-dev.11) (2026-10-29) + +### ⚠ BREAKING CHANGES + +* **YouTube - Theme:** Patches now use Morphe Patcher 2.0.0 + +Co-authored-by: MarcaDian <152095496+MarcaDian@users.noreply.github.com> + +### 🐛 Bug Fixes + +* **YouTube - Ambient mode:** Ambient mode is replaced by a black background with the new watch page experiment ([43a9e4c](https://github.com/MorpheApp/morphe-patches/commit/43a9e4c8935627af8f1748071438c25b653deefb)) +* **YouTube - Ambient mode:** Bypass Ambient mode restrictions does not work with 21.43 ([aeac8c9](https://github.com/MorpheApp/morphe-patches/commit/aeac8c9c15f93127a41326a3e5ae913a7c75dbae)) +* **YouTube - Feed card layout:** Movies converted to cards are not hidden when "Hide movies section" is enabled ([5c6357f](https://github.com/MorpheApp/morphe-patches/commit/5c6357fed1f52c5ed5d0236a393392b860e8d5ae)) +* **YouTube - Feed view count:** View count badge of a card that enters from the bottom loads only when its title row is shown ([26715b1](https://github.com/MorpheApp/morphe-patches/commit/26715b1723f5755dc13a64c8b41be4a499f4f2cb)) +* **YouTube - Hide layout components:** "Visit the community" entry point is not hidden in the comments of a video opened from a playlist ([e11da94](https://github.com/MorpheApp/morphe-patches/commit/e11da94ca64632956222074390f67f7f13bd82f9)) +* **YouTube - Video quality:** Buffered video is cleared with the placeholder playback times of live streams after selecting a quality ([36e3612](https://github.com/MorpheApp/morphe-patches/commit/36e3612c0bfa7435e47fa9529b12a819b11ac843)) +* **YouTube - Video quality:** Extend the background resume fix of regular videos to Shorts ([958aa55](https://github.com/MorpheApp/morphe-patches/commit/958aa55c56ccef1e36ea1003f0f869203cc828b7)) +* **YouTube - Video quality:** Quality selected for a regular video is lost when the video reloads ([16d4e60](https://github.com/MorpheApp/morphe-patches/commit/16d4e602c7c8961b35bd737b5e5770312ef1781c)) +* **YouTube - Video quality:** Regular video returns to the default quality when it resumes from the background ([e5cd752](https://github.com/MorpheApp/morphe-patches/commit/e5cd7523ba2963b29fff6c3169cc71e1a58f405d)) +* **YouTube - Video quality:** Short loop restarts with the previous quality when the quality is selected in its first segment ([a325744](https://github.com/MorpheApp/morphe-patches/commit/a325744ef3434bf0fe40525d4e2da9b696a2f2cb)) +* **YouTube - Video quality:** Short plays the previous quality segments again when it loops ([328b85a](https://github.com/MorpheApp/morphe-patches/commit/328b85acb6fe32b7722dda7fd41ff2a8988e9c7d)) +* **YouTube - Video quality:** Video restarts or plays faster after selecting a quality ([c000b53](https://github.com/MorpheApp/morphe-patches/commit/c000b53eeae02941f82a37e1c66c12a06c3d1059)) +* **YouTube Music - Change miniplayer color:** Miniplayer color does not update after track changes ([e1410e3](https://github.com/MorpheApp/morphe-patches/commit/e1410e3189531c1fb4ab41c576c4fdd20aa89e36)) +* **YouTube:** Feed thumbnail badges are shown on the page header of a channel that is live ([394ab97](https://github.com/MorpheApp/morphe-patches/commit/394ab97af76d788d6b13fc4c47f8e2ba4d892cfc)) +* **YouTube:** Feed thumbnail badges disappear while scrolling, are missing on cards with replaced titles and are shown on live videos ([4344fab](https://github.com/MorpheApp/morphe-patches/commit/4344fab6ef2e8aa6694072f38478238b84e0f42a)) +* **YouTube:** Fix TalkBack not reading captions ([#4136](https://github.com/MorpheApp/morphe-patches/issues/4136)) ([b22841c](https://github.com/MorpheApp/morphe-patches/commit/b22841c5a6ddf6ff38b49f262ca5aa893348a600)) +* **YouTube:** Prevent TalkBack crashes caused by Litho hover recursion ([#4129](https://github.com/MorpheApp/morphe-patches/issues/4129)) ([4c00210](https://github.com/MorpheApp/morphe-patches/commit/4c002101e3472e9700acb8f92a63291f69b61116)) +* **YouTube:** Watch history search bar from getting hidden when selected ([#4130](https://github.com/MorpheApp/morphe-patches/issues/4130)) ([a4ecfcf](https://github.com/MorpheApp/morphe-patches/commit/a4ecfcfc3ebe99ba452e5e87c9767a83c4760c25)) + +### ✨ New Features + +* **YouTube - Hide layout components:** Add "Hide 'Unavailable videos are hidden' banner" setting ([#4134](https://github.com/MorpheApp/morphe-patches/issues/4134)) ([a7ce357](https://github.com/MorpheApp/morphe-patches/commit/a7ce357d2b654aed2a241c901bf2bc3df2ce7274)) +* **YouTube - Minimal miniplayer:** Swipe the bar up to move it to the top of the screen instead of expanding the player ([f8180da](https://github.com/MorpheApp/morphe-patches/commit/f8180da70c848560c4fe17106afb49ff6852e564)) +* **YouTube - Theme:** Add "Glow Morphe" experimental splash screen type ([#4135](https://github.com/MorpheApp/morphe-patches/issues/4135)) ([6dbdf82](https://github.com/MorpheApp/morphe-patches/commit/6dbdf82457c5f8c7906b56f04640baf7e908060a)) +* **YouTube Music - Third-party lyrics:** Per-syllable romanization layout, dynamic secondary vocals, and typography refinements ([#4090](https://github.com/MorpheApp/morphe-patches/issues/4090)) ([a5d943b](https://github.com/MorpheApp/morphe-patches/commit/a5d943b40369e1a6050db6b5a51a5104a2190ab9)) + +### 🚀 Updated App Support + +* **Reddit:** Add experimental support for `2026.44.0` ([793f044](https://github.com/MorpheApp/morphe-patches/commit/793f0447ce2ec70d7c02c8ca005b09000a0f70b0)) +* **YouTube:** Add support for `21.30.216` ([89defd5](https://github.com/MorpheApp/morphe-patches/commit/89defd5d039e9dbcc139866cb5941cb3b592559d)) + ## [1.49.0-dev.10](https://github.com/MorpheApp/morphe-patches/compare/v1.49.0-dev.9...v1.49.0-dev.10) (2026-10-28) ### 🐛 Bug Fixes diff --git a/README.md b/README.md index bf1cc0c853..092775f673 100644 --- a/README.md +++ b/README.md @@ -35,15 +35,15 @@ All modifications made by Morphe, along with their dates, can be found in the Gi ## 🩹 Patches list -> **[v1.49.0-dev.10](https://github.com/MorpheApp/morphe-patches/releases/tag/v1.49.0-dev.10)**  •  `dev`  •  193 patches total +> **[v1.49.0-dev.11](https://github.com/MorpheApp/morphe-patches/releases/tag/v1.49.0-dev.11)**  •  `dev`  •  193 patches total

    📦 YouTube  •  108 patches
    **🎯 Supported versions:** -| 🧪 21.43.523 | 🧪 21.42.241 | 🧪 21.28.208 | 21.23.492 | 21.20.405 | 21.16.256 | 20.51.39 | 20.26.46 | -| :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | +| 🧪 21.43.523 | 🧪 21.42.241 | 21.30.216 | 21.28.208 | 21.23.492 | 20.51.39 | 20.26.46 | +| :---: | :---: | :---: | :---: | :---: | :---: | :---: | | 💊 Patch | 📜 Description | ⚙️ Options | |----------|----------------|-----------| @@ -111,7 +111,7 @@ All modifications made by Morphe, along with their dates, can be found in the Gi | [Hide video action buttons](#hide-video-action-buttons) | Adds options to hide video action buttons in fullscreen and portrait modes. | | | [Loop video](#loop-video) | Adds an option to loop videos and display loop video button in the video player. | | | [Media notification controls](#media-notification-controls) | Adds options to disable the seekbar and previous/next buttons in the media notification and headphone controls. | | -| [Miniplayer](#miniplayer) | Adds options to change the in-app minimized player. Patching 21.28.206 and lower has more miniplayer types to choose from. | | +| [Miniplayer](#miniplayer) | Adds options to change the in-app minimized player. Patching 21.28.208 and lower has more miniplayer types to choose from. | | | [Mute button](#mute-button) | Adds an option to show a player button that mutes the video audio. | | | [Navigation bar](#navigation-bar) | Adds options to hide and change the bottom navigation bar (such as the Shorts button) and the upper navigation toolbar. | • Hide Search app shortcut
    • Hide Subscriptions app shortcut | | [Network proxy](#network-proxy) | Adds settings to route supported network requests through an HTTP or HTTPS proxy. Including this patch may cause connectivity problems on certain devices | | @@ -227,7 +227,7 @@ All modifications made by Morphe, along with their dates, can be found in the Gi **🎯 Supported versions:** -| 🧪 2026.43.0 | 🧪 2026.42.0 | 🧪 2026.41.0 | 2026.40.0 | 2026.24.0 | 2026.10.0 | +| 🧪 2026.44.0 | 🧪 2026.43.0 | 🧪 2026.42.0 | 2026.40.0 | 2026.24.0 | 2026.10.0 | | :---: | :---: | :---: | :---: | :---: | :---: | | 💊 Patch | 📜 Description | ⚙️ Options | diff --git a/gradle.properties b/gradle.properties index 62a696316f..b5f91d873a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,4 +5,4 @@ android.useAndroidX = true # Extensions are Java only. Prevents AGP from adding Kotlin compilation and the Kotlin stdlib. android.builtInKotlin = false kotlin.code.style = official -version = 1.49.0-dev.10 +version = 1.49.0-dev.11 diff --git a/patches-bundle.json b/patches-bundle.json index 0a3300ecaa..82015a8cc9 100644 --- a/patches-bundle.json +++ b/patches-bundle.json @@ -1,8 +1,8 @@ { - "created_at": "2026-10-28T08:22:03", - "description": "## [1.49.0-dev.10](https://github.com/MorpheApp/morphe-patches/compare/v1.49.0-dev.9...v1.49.0-dev.10) (2026-10-28)\n\n### 🐛 Bug Fixes\n\n* **YouTube - Ask flyout menu:** Ask chat window can be hidden by onscreen keyboard ([7532430](https://github.com/MorpheApp/morphe-patches/commit/7532430e096f5730ba9321b32d8b4bd2c54c77f6))\n* **YouTube - Hide status bar:** Top bars are covered by a second status bar when the app is kept out of the camera cutout ([#4112](https://github.com/MorpheApp/morphe-patches/issues/4112)) ([9d3b53c](https://github.com/MorpheApp/morphe-patches/commit/9d3b53c92cb877f309b4463b7d5067aaa1a996a3))\n* **YouTube - Spoof video streams:** Patch can be included for root installs ([3edf03b](https://github.com/MorpheApp/morphe-patches/commit/3edf03b8efc2f5ea0458456002938c7dbd4bf8d8))\n* **YouTube - Theme:** Glow startup animation does not extend to fullscreen ([7f1a6b3](https://github.com/MorpheApp/morphe-patches/commit/7f1a6b35c8a3989cd2b06a1e81d817015f2ca433))\n* **YouTube - Video quality:** Selected quality is not applied to the already buffered video ([#4117](https://github.com/MorpheApp/morphe-patches/issues/4117)) ([7faa488](https://github.com/MorpheApp/morphe-patches/commit/7faa48834b1f78f8ca572c773392ed9513de8ea3))\n* **YouTube:** Restore TalkBack access to Shorts comments and descriptions ([#4128](https://github.com/MorpheApp/morphe-patches/issues/4128)) ([9542d24](https://github.com/MorpheApp/morphe-patches/commit/9542d24adf3b5ec2c25fce931418160dadbde93c))", - "download_url": "https://github.com/MorpheApp/morphe-patches/releases/download/v1.49.0-dev.10/patches-1.49.0-dev.10.mpp", + "created_at": "2026-10-29T08:20:48", + "description": "## [1.49.0-dev.11](https://github.com/MorpheApp/morphe-patches/compare/v1.49.0-dev.10...v1.49.0-dev.11) (2026-10-29)\n\n### ⚠ BREAKING CHANGES\n\n* **YouTube - Theme:** Patches now use Morphe Patcher 2.0.0\n\nCo-authored-by: MarcaDian <152095496+MarcaDian@users.noreply.github.com>\n\n### 🐛 Bug Fixes\n\n* **YouTube - Ambient mode:** Ambient mode is replaced by a black background with the new watch page experiment ([43a9e4c](https://github.com/MorpheApp/morphe-patches/commit/43a9e4c8935627af8f1748071438c25b653deefb))\n* **YouTube - Ambient mode:** Bypass Ambient mode restrictions does not work with 21.43 ([aeac8c9](https://github.com/MorpheApp/morphe-patches/commit/aeac8c9c15f93127a41326a3e5ae913a7c75dbae))\n* **YouTube - Feed card layout:** Movies converted to cards are not hidden when \"Hide movies section\" is enabled ([5c6357f](https://github.com/MorpheApp/morphe-patches/commit/5c6357fed1f52c5ed5d0236a393392b860e8d5ae))\n* **YouTube - Feed view count:** View count badge of a card that enters from the bottom loads only when its title row is shown ([26715b1](https://github.com/MorpheApp/morphe-patches/commit/26715b1723f5755dc13a64c8b41be4a499f4f2cb))\n* **YouTube - Hide layout components:** \"Visit the community\" entry point is not hidden in the comments of a video opened from a playlist ([e11da94](https://github.com/MorpheApp/morphe-patches/commit/e11da94ca64632956222074390f67f7f13bd82f9))\n* **YouTube - Video quality:** Buffered video is cleared with the placeholder playback times of live streams after selecting a quality ([36e3612](https://github.com/MorpheApp/morphe-patches/commit/36e3612c0bfa7435e47fa9529b12a819b11ac843))\n* **YouTube - Video quality:** Extend the background resume fix of regular videos to Shorts ([958aa55](https://github.com/MorpheApp/morphe-patches/commit/958aa55c56ccef1e36ea1003f0f869203cc828b7))\n* **YouTube - Video quality:** Quality selected for a regular video is lost when the video reloads ([16d4e60](https://github.com/MorpheApp/morphe-patches/commit/16d4e602c7c8961b35bd737b5e5770312ef1781c))\n* **YouTube - Video quality:** Regular video returns to the default quality when it resumes from the background ([e5cd752](https://github.com/MorpheApp/morphe-patches/commit/e5cd7523ba2963b29fff6c3169cc71e1a58f405d))\n* **YouTube - Video quality:** Short loop restarts with the previous quality when the quality is selected in its first segment ([a325744](https://github.com/MorpheApp/morphe-patches/commit/a325744ef3434bf0fe40525d4e2da9b696a2f2cb))\n* **YouTube - Video quality:** Short plays the previous quality segments again when it loops ([328b85a](https://github.com/MorpheApp/morphe-patches/commit/328b85acb6fe32b7722dda7fd41ff2a8988e9c7d))\n* **YouTube - Video quality:** Video restarts or plays faster after selecting a quality ([c000b53](https://github.com/MorpheApp/morphe-patches/commit/c000b53eeae02941f82a37e1c66c12a06c3d1059))\n* **YouTube Music - Change miniplayer color:** Miniplayer color does not update after track changes ([e1410e3](https://github.com/MorpheApp/morphe-patches/commit/e1410e3189531c1fb4ab41c576c4fdd20aa89e36))\n* **YouTube:** Feed thumbnail badges are shown on the page header of a channel that is live ([394ab97](https://github.com/MorpheApp/morphe-patches/commit/394ab97af76d788d6b13fc4c47f8e2ba4d892cfc))\n* **YouTube:** Feed thumbnail badges disappear while scrolling, are missing on cards with replaced titles and are shown on live videos ([4344fab](https://github.com/MorpheApp/morphe-patches/commit/4344fab6ef2e8aa6694072f38478238b84e0f42a))\n* **YouTube:** Fix TalkBack not reading captions ([#4136](https://github.com/MorpheApp/morphe-patches/issues/4136)) ([b22841c](https://github.com/MorpheApp/morphe-patches/commit/b22841c5a6ddf6ff38b49f262ca5aa893348a600))\n* **YouTube:** Prevent TalkBack crashes caused by Litho hover recursion ([#4129](https://github.com/MorpheApp/morphe-patches/issues/4129)) ([4c00210](https://github.com/MorpheApp/morphe-patches/commit/4c002101e3472e9700acb8f92a63291f69b61116))\n* **YouTube:** Watch history search bar from getting hidden when selected ([#4130](https://github.com/MorpheApp/morphe-patches/issues/4130)) ([a4ecfcf](https://github.com/MorpheApp/morphe-patches/commit/a4ecfcfc3ebe99ba452e5e87c9767a83c4760c25))\n\n### ✨ New Features\n\n* **YouTube - Hide layout components:** Add \"Hide 'Unavailable videos are hidden' banner\" setting ([#4134](https://github.com/MorpheApp/morphe-patches/issues/4134)) ([a7ce357](https://github.com/MorpheApp/morphe-patches/commit/a7ce357d2b654aed2a241c901bf2bc3df2ce7274))\n* **YouTube - Minimal miniplayer:** Swipe the bar up to move it to the top of the screen instead of expanding the player ([f8180da](https://github.com/MorpheApp/morphe-patches/commit/f8180da70c848560c4fe17106afb49ff6852e564))\n* **YouTube - Theme:** Add \"Glow Morphe\" experimental splash screen type ([#4135](https://github.com/MorpheApp/morphe-patches/issues/4135)) ([6dbdf82](https://github.com/MorpheApp/morphe-patches/commit/6dbdf82457c5f8c7906b56f04640baf7e908060a))\n* **YouTube Music - Third-party lyrics:** Per-syllable romanization layout, dynamic secondary vocals, and typography refinements ([#4090](https://github.com/MorpheApp/morphe-patches/issues/4090)) ([a5d943b](https://github.com/MorpheApp/morphe-patches/commit/a5d943b40369e1a6050db6b5a51a5104a2190ab9))\n\n### 🚀 Updated App Support\n\n* **Reddit:** Add experimental support for `2026.44.0` ([793f044](https://github.com/MorpheApp/morphe-patches/commit/793f0447ce2ec70d7c02c8ca005b09000a0f70b0))\n* **YouTube:** Add support for `21.30.216` ([89defd5](https://github.com/MorpheApp/morphe-patches/commit/89defd5d039e9dbcc139866cb5941cb3b592559d))", + "download_url": "https://github.com/MorpheApp/morphe-patches/releases/download/v1.49.0-dev.11/patches-1.49.0-dev.11.mpp", "signature_download_url": "", - "version": "1.49.0-dev.10", + "version": "1.49.0-dev.11", "public_key": "g8Ys9leTu2yeixojeR35HrNkK6eOSLpA6nNIizRutqI=" } diff --git a/patches-list.json b/patches-list.json index e60c5550d5..6e0df41732 100644 --- a/patches-list.json +++ b/patches-list.json @@ -1,6 +1,6 @@ { "NOTE": "Do NOT manually edit this file. This file is automatically updated when semantic release (release.yml) runs. Manually editing this file can break your releases and break third party tools that use this file.", - "version": "1.49.0-dev.10", + "version": "1.49.0-dev.11", "patches": [ { "name": "AI channel filter", @@ -35,28 +35,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -114,28 +107,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -193,28 +179,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -315,21 +294,21 @@ "formFactor": "MOBILE", "targets": [ { - "version": "2026.43.0", + "version": "2026.44.0", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "2026.42.0", + "version": "2026.43.0", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "2026.41.0", + "version": "2026.42.0", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -452,28 +431,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -531,28 +503,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -610,28 +575,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -689,28 +647,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -768,28 +719,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -847,28 +791,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -994,28 +931,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -1223,28 +1153,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -1302,28 +1225,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -1381,28 +1297,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -1518,28 +1427,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -1620,21 +1522,21 @@ "formFactor": "MOBILE", "targets": [ { - "version": "2026.43.0", + "version": "2026.44.0", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "2026.42.0", + "version": "2026.43.0", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "2026.41.0", + "version": "2026.42.0", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -1699,28 +1601,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -1778,28 +1673,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -2010,28 +1898,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -2111,21 +1992,21 @@ "formFactor": "MOBILE", "targets": [ { - "version": "2026.43.0", + "version": "2026.44.0", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "2026.42.0", + "version": "2026.43.0", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "2026.41.0", + "version": "2026.42.0", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -2245,21 +2126,21 @@ "formFactor": "MOBILE", "targets": [ { - "version": "2026.43.0", + "version": "2026.44.0", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "2026.42.0", + "version": "2026.43.0", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "2026.41.0", + "version": "2026.42.0", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -2324,28 +2205,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -2403,28 +2277,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -2467,21 +2334,21 @@ "formFactor": "MOBILE", "targets": [ { - "version": "2026.43.0", + "version": "2026.44.0", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "2026.42.0", + "version": "2026.43.0", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "2026.41.0", + "version": "2026.42.0", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -2546,28 +2413,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -2683,28 +2543,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -2828,28 +2681,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -2907,28 +2753,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -2986,28 +2825,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -3065,28 +2897,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -3202,28 +3027,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -3281,28 +3099,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -3360,28 +3171,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -3439,28 +3243,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -3503,21 +3300,21 @@ "formFactor": "MOBILE", "targets": [ { - "version": "2026.43.0", + "version": "2026.44.0", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "2026.42.0", + "version": "2026.43.0", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "2026.41.0", + "version": "2026.42.0", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -3582,28 +3379,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -3661,28 +3451,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -3740,28 +3523,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -3804,21 +3580,21 @@ "formFactor": "MOBILE", "targets": [ { - "version": "2026.43.0", + "version": "2026.44.0", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "2026.42.0", + "version": "2026.43.0", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "2026.41.0", + "version": "2026.42.0", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -3883,28 +3659,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -3962,28 +3731,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -4041,28 +3803,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -4120,28 +3875,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -4184,21 +3932,21 @@ "formFactor": "MOBILE", "targets": [ { - "version": "2026.43.0", + "version": "2026.44.0", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "2026.42.0", + "version": "2026.43.0", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "2026.41.0", + "version": "2026.42.0", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -4263,28 +4011,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -4400,28 +4141,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -4595,28 +4329,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -4848,28 +4575,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -4927,28 +4647,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -4991,21 +4704,21 @@ "formFactor": "MOBILE", "targets": [ { - "version": "2026.43.0", + "version": "2026.44.0", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "2026.42.0", + "version": "2026.43.0", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "2026.41.0", + "version": "2026.42.0", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -5056,42 +4769,35 @@ "formFactor": "MOBILE", "targets": [ { - "version": "21.43.523", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.42.241", + "version": "21.43.523", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.28.208", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -5149,28 +4855,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -5228,28 +4927,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -5307,28 +4999,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -5444,28 +5129,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -5566,21 +5244,21 @@ "formFactor": "MOBILE", "targets": [ { - "version": "2026.43.0", + "version": "2026.44.0", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "2026.42.0", + "version": "2026.43.0", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "2026.41.0", + "version": "2026.42.0", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -5630,21 +5308,21 @@ "formFactor": "MOBILE", "targets": [ { - "version": "2026.43.0", + "version": "2026.44.0", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "2026.42.0", + "version": "2026.43.0", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "2026.41.0", + "version": "2026.42.0", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -5709,28 +5387,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -5846,28 +5517,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -5910,21 +5574,21 @@ "formFactor": "MOBILE", "targets": [ { - "version": "2026.43.0", + "version": "2026.44.0", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "2026.42.0", + "version": "2026.43.0", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "2026.41.0", + "version": "2026.42.0", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -5974,21 +5638,21 @@ "formFactor": "MOBILE", "targets": [ { - "version": "2026.43.0", + "version": "2026.44.0", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "2026.42.0", + "version": "2026.43.0", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "2026.41.0", + "version": "2026.42.0", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -6053,28 +5717,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -6136,21 +5793,21 @@ "formFactor": "MOBILE", "targets": [ { - "version": "2026.43.0", + "version": "2026.44.0", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "2026.42.0", + "version": "2026.43.0", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "2026.41.0", + "version": "2026.42.0", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -6258,21 +5915,21 @@ "formFactor": "MOBILE", "targets": [ { - "version": "2026.43.0", + "version": "2026.44.0", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "2026.42.0", + "version": "2026.43.0", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "2026.41.0", + "version": "2026.42.0", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -6337,28 +5994,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -6416,28 +6066,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -6538,21 +6181,21 @@ "formFactor": "MOBILE", "targets": [ { - "version": "2026.43.0", + "version": "2026.44.0", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "2026.42.0", + "version": "2026.43.0", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "2026.41.0", + "version": "2026.42.0", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -6617,28 +6260,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -6696,28 +6332,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -6833,28 +6462,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -6970,28 +6592,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -7034,21 +6649,21 @@ "formFactor": "MOBILE", "targets": [ { - "version": "2026.43.0", + "version": "2026.44.0", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "2026.42.0", + "version": "2026.43.0", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "2026.41.0", + "version": "2026.42.0", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -7156,21 +6771,21 @@ "formFactor": "MOBILE", "targets": [ { - "version": "2026.43.0", + "version": "2026.44.0", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "2026.42.0", + "version": "2026.43.0", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "2026.41.0", + "version": "2026.42.0", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -7235,28 +6850,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -7314,28 +6922,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -7393,28 +6994,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -7472,28 +7066,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -7536,21 +7123,21 @@ "formFactor": "MOBILE", "targets": [ { - "version": "2026.43.0", + "version": "2026.44.0", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "2026.42.0", + "version": "2026.43.0", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "2026.41.0", + "version": "2026.42.0", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -7615,28 +7202,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -7694,28 +7274,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -7773,28 +7346,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -7895,21 +7461,21 @@ "formFactor": "MOBILE", "targets": [ { - "version": "2026.43.0", + "version": "2026.44.0", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "2026.42.0", + "version": "2026.43.0", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "2026.41.0", + "version": "2026.42.0", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -7974,28 +7540,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -8111,28 +7670,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -8175,21 +7727,21 @@ "formFactor": "MOBILE", "targets": [ { - "version": "2026.43.0", + "version": "2026.44.0", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "2026.42.0", + "version": "2026.43.0", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "2026.41.0", + "version": "2026.42.0", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -8239,21 +7791,21 @@ "formFactor": "MOBILE", "targets": [ { - "version": "2026.43.0", + "version": "2026.44.0", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "2026.42.0", + "version": "2026.43.0", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "2026.41.0", + "version": "2026.42.0", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -8287,7 +7839,7 @@ }, { "name": "Miniplayer", - "description": "Adds options to change the in-app minimized player. Patching 21.28.206 and lower has more miniplayer types to choose from.", + "description": "Adds options to change the in-app minimized player. Patching 21.28.208 and lower has more miniplayer types to choose from.", "default": true, "category": null, "compatiblePackages": [ @@ -8318,28 +7870,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -8455,28 +8000,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -8592,28 +8130,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -8748,28 +8279,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -8827,28 +8351,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -8906,28 +8423,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -8985,28 +8495,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -9049,21 +8552,21 @@ "formFactor": "MOBILE", "targets": [ { - "version": "2026.43.0", + "version": "2026.44.0", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "2026.42.0", + "version": "2026.43.0", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "2026.41.0", + "version": "2026.42.0", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -9113,21 +8616,21 @@ "formFactor": "MOBILE", "targets": [ { - "version": "2026.43.0", + "version": "2026.44.0", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "2026.42.0", + "version": "2026.43.0", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "2026.41.0", + "version": "2026.42.0", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -9192,28 +8695,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -9271,28 +8767,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -9343,35 +8832,28 @@ "description": null }, { - "version": "21.42.241", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.28.208", + "version": "21.42.241", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -9429,28 +8911,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -9516,28 +8991,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -9653,28 +9121,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -9732,28 +9193,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -9811,28 +9265,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -9948,28 +9395,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -10027,28 +9467,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -10232,28 +9665,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -10296,21 +9722,21 @@ "formFactor": "MOBILE", "targets": [ { - "version": "2026.43.0", + "version": "2026.44.0", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "2026.42.0", + "version": "2026.43.0", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "2026.41.0", + "version": "2026.42.0", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -10375,28 +9801,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -10439,21 +9858,21 @@ "formFactor": "MOBILE", "targets": [ { - "version": "2026.43.0", + "version": "2026.44.0", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "2026.42.0", + "version": "2026.43.0", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "2026.41.0", + "version": "2026.42.0", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -10518,28 +9937,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -10771,28 +10183,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -10835,21 +10240,21 @@ "formFactor": "MOBILE", "targets": [ { - "version": "2026.43.0", + "version": "2026.44.0", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "2026.42.0", + "version": "2026.43.0", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "2026.41.0", + "version": "2026.42.0", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -10972,28 +10377,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -11051,28 +10449,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -11188,28 +10579,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -11310,21 +10694,21 @@ "formFactor": "MOBILE", "targets": [ { - "version": "2026.43.0", + "version": "2026.44.0", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "2026.42.0", + "version": "2026.43.0", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "2026.41.0", + "version": "2026.42.0", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -11389,28 +10773,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -11468,28 +10845,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -11605,28 +10975,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -11684,28 +11047,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -11763,28 +11119,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -11900,28 +11249,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -11979,28 +11321,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -12058,28 +11393,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -12132,21 +11460,21 @@ "formFactor": "MOBILE", "targets": [ { - "version": "2026.43.0", + "version": "2026.44.0", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "2026.42.0", + "version": "2026.43.0", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "2026.41.0", + "version": "2026.42.0", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -12211,28 +11539,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -12348,28 +11669,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -12485,28 +11799,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -12622,28 +11929,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -12701,28 +12001,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -12773,21 +12066,21 @@ "formFactor": "MOBILE", "targets": [ { - "version": "2026.43.0", + "version": "2026.44.0", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "2026.42.0", + "version": "2026.43.0", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "2026.41.0", + "version": "2026.42.0", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -12917,28 +12210,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -12981,21 +12267,21 @@ "formFactor": "MOBILE", "targets": [ { - "version": "2026.43.0", + "version": "2026.44.0", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "2026.42.0", + "version": "2026.43.0", "versionCodes": null, "isExperimental": true, "minSdk": 29, "description": null }, { - "version": "2026.41.0", + "version": "2026.42.0", "versionCodes": null, "isExperimental": true, "minSdk": 29, @@ -13060,28 +12346,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -13223,28 +12502,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -13409,28 +12681,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -13488,28 +12753,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -13567,28 +12825,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, @@ -13646,28 +12897,21 @@ "description": null }, { - "version": "21.28.208", - "versionCodes": null, - "isExperimental": true, - "minSdk": 29, - "description": null - }, - { - "version": "21.23.492", + "version": "21.30.216", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.20.405", + "version": "21.28.208", "versionCodes": null, "isExperimental": false, - "minSdk": 28, + "minSdk": 29, "description": null }, { - "version": "21.16.256", + "version": "21.23.492", "versionCodes": null, "isExperimental": false, "minSdk": 28, From 60b7bef7413b092c9c54171e976e440b1a7c3d5f Mon Sep 17 00:00:00 2001 From: Anurag Rai <129479513+anuraggr@users.noreply.github.com> Date: Thu, 29 Oct 2026 14:01:01 +0530 Subject: [PATCH 154/189] fix(YouTube) Seeking fails for downloaded videos when internet is active (#4151) --- .../shared/spoof/SpoofVideoStreamsPatch.java | 33 +++++++++++++++++++ .../youtube/patches/InAppDownloadsPatch.java | 21 ++++++++++++ .../patches/spoof/SpoofVideoStreamsPatch.java | 4 +++ .../misc/spoof/SpoofVideoStreamsPatch.kt | 2 +- 4 files changed, 59 insertions(+), 1 deletion(-) diff --git a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/spoof/SpoofVideoStreamsPatch.java b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/spoof/SpoofVideoStreamsPatch.java index ef80d92f95..7817eb2749 100644 --- a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/spoof/SpoofVideoStreamsPatch.java +++ b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/spoof/SpoofVideoStreamsPatch.java @@ -237,6 +237,29 @@ public static long getVideoLengthSeconds(String videoId) { } } + public interface OfflineResponseResolver { + boolean isOffline(@NonNull String videoId, @Nullable Object playerProto); + } + + @Nullable + private static volatile OfflineResponseResolver offlineResponseResolver; + + public static void setOfflineResponseResolver(@Nullable OfflineResponseResolver resolver) { + offlineResponseResolver = resolver; + } + + private static boolean isOfflinePlayerResponse(@NonNull String videoId, @Nullable Object playerProto) { + try { + OfflineResponseResolver resolver = offlineResponseResolver; + if (resolver != null) { + return resolver.isOffline(videoId, playerProto); + } + } catch (Exception ex) { + Logger.printException(() -> "isOfflinePlayerResponse failure", ex); + } + return false; + } + /** * Injection point. */ @@ -368,12 +391,22 @@ public static void fetchStreams(String url, Map requestHeaders) */ @Nullable public static byte[] getStreamingData(String videoId) { + return getStreamingData(videoId, null); + } + + @Nullable + public static byte[] getStreamingData(String videoId, @Nullable Object playerProto) { if (SPOOF_VIDEO_STREAMS) { if (Boolean.TRUE.equals(keepStreamsOnThread.get())) { Logger.printDebug(() -> "Keeping the streams of the player response: " + videoId); return null; } + if (isOfflinePlayerResponse(videoId, playerProto)) { + Logger.printDebug(() -> "Not overriding streaming data (saved offline video): " + videoId); + return null; + } + // Without a connection the replacement streams cannot load, // and a video that is saved plays with the streams of its own player response. if (!Utils.isNetworkConnected()) { diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/InAppDownloadsPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/InAppDownloadsPatch.java index 981f38d910..8742c49044 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/InAppDownloadsPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/InAppDownloadsPatch.java @@ -11,6 +11,7 @@ import android.net.Uri; import android.util.Base64; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.google.protobuf.ByteString; @@ -242,6 +243,26 @@ public static byte[] replaceOfflineUpsell(@Nullable MessageLite command, @Nullab } } + /** + * Determines whether the given player response proto represents a playable offline video. + */ + public static boolean isOfflinePlayerResponse(@NonNull String videoId, @Nullable Object playerProto) { + try { + if (!(playerProto instanceof MessageLite message)) { + return false; + } + OfflinePlayerResponse response = OfflinePlayerResponse.parseFrom(message.toByteArray()); + if (response.hasOfflineState() + && response.getOfflineState().getActionValue() == OfflineStateAction.OFFLINE_STATE_ACTION_OK_VALUE) { + Logger.printDebug(() -> "Detected playable offline video: " + videoId); + return true; + } + } catch (Exception ex) { + Logger.printException(() -> "isOfflinePlayerResponse failure", ex); + } + return false; + } + /** * Injection point. *

    diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/spoof/SpoofVideoStreamsPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/spoof/SpoofVideoStreamsPatch.java index 2b801fc55f..a39169a8a3 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/spoof/SpoofVideoStreamsPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/spoof/SpoofVideoStreamsPatch.java @@ -6,6 +6,7 @@ import app.morphe.extension.shared.spoof.ClientType; import app.morphe.extension.youtube.patches.DisableVideoCodecsPatch; import app.morphe.extension.youtube.patches.DisableVideoCodecsPatch.VideoCodec; +import app.morphe.extension.youtube.patches.InAppDownloadsPatch; import app.morphe.extension.youtube.settings.Settings; @SuppressWarnings("unused") @@ -29,6 +30,9 @@ public List> getParentSettings() { * Injection point. */ public static void setClientOrderToUse() { + app.morphe.extension.shared.spoof.SpoofVideoStreamsPatch.setOfflineResponseResolver( + InAppDownloadsPatch::isOfflinePlayerResponse); + ClientType client = Settings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get(); // Use [Android XR, Android XR Downgraded, visonOS 1.03] client that has AV1 if user settings allow it. diff --git a/patches/src/main/kotlin/app/morphe/patches/shared/misc/spoof/SpoofVideoStreamsPatch.kt b/patches/src/main/kotlin/app/morphe/patches/shared/misc/spoof/SpoofVideoStreamsPatch.kt index 49adb30347..596f9ec1b8 100644 --- a/patches/src/main/kotlin/app/morphe/patches/shared/misc/spoof/SpoofVideoStreamsPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/shared/misc/spoof/SpoofVideoStreamsPatch.kt @@ -260,7 +260,7 @@ internal fun spoofVideoStreamsPatch( if-eqz v2, :disabled # Get streaming data. - invoke-static { v2 }, $EXTENSION_CLASS->getStreamingData(Ljava/lang/String;)[B + invoke-static { v2, p2 }, $EXTENSION_CLASS->getStreamingData(Ljava/lang/String;Ljava/lang/Object;)[B move-result-object v3 if-eqz v3, :disabled From c596d42afb48dd79be022f81f3b731eda8870941 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Thu, 29 Oct 2026 08:38:27 +0000 Subject: [PATCH 155/189] build: Treat breaking changes as major releases The custom release rules matched breaking commits by type first, so the default breaking rule was never applied and a breaking change was released as a minor version. BREAKING CHANGE: Patches now use Morphe Patcher 2.0.0 --- .releaserc | 1 + 1 file changed, 1 insertion(+) diff --git a/.releaserc b/.releaserc index 332cf7089c..c55c1910c6 100644 --- a/.releaserc +++ b/.releaserc @@ -10,6 +10,7 @@ [ "@semantic-release/commit-analyzer", { "releaseRules": [ + { "breaking": true, "release": "major" }, { "type": "fix", "release": "patch" }, { "type": "feat", "release": "minor" }, { "type": "bump", "release": "patch" }, From 68cbc78c71388ced7d6cb053f440e1d02c82a2ed Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Thu, 29 Oct 2026 09:38:47 +0000 Subject: [PATCH 156/189] chore(YouTube - Spoof video streams): App crashes on video open with TV SABR client --- .../misc/spoof/SpoofVideoStreamsPatch.kt | 37 ++++++++++++------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/patches/src/main/kotlin/app/morphe/patches/shared/misc/spoof/SpoofVideoStreamsPatch.kt b/patches/src/main/kotlin/app/morphe/patches/shared/misc/spoof/SpoofVideoStreamsPatch.kt index 596f9ec1b8..367824355e 100644 --- a/patches/src/main/kotlin/app/morphe/patches/shared/misc/spoof/SpoofVideoStreamsPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/shared/misc/spoof/SpoofVideoStreamsPatch.kt @@ -235,6 +235,22 @@ internal fun spoofVideoStreamsPatch( nop """ + // Parses the player config bytes in v3 to a new player config in v5. + val parsePlayerConfig = """ + sget-object v4, $playerConfigClass->a:$playerConfigClass + invoke-virtual { v4 }, $createBuilderMethod + move-result-object v4 + $castInstruction + invoke-static { }, Lcom/google/protobuf/ExtensionRegistryLite;->getGeneratedRegistry()Lcom/google/protobuf/ExtensionRegistryLite; + move-result-object v5 + invoke-virtual { v4, v3, v5 }, $mergeFromMethod + move-result-object v5 + check-cast v5, $castReference + invoke-virtual { v5 }, $buildMethod + move-result-object v5 + check-cast v5, $playerConfigClass + """ + val helperMethod = ImmutableMethod( it.classDef.type, "patch_setStreamingData", @@ -282,18 +298,7 @@ internal fun spoofVideoStreamsPatch( invoke-static { v2 }, $EXTENSION_CLASS->getPlayerConfig(Ljava/lang/String;)[B move-result-object v3 if-eqz v3, :disabled - sget-object v4, $playerConfigClass->a:$playerConfigClass - invoke-virtual { v4 }, $createBuilderMethod - move-result-object v4 - $castInstruction - invoke-static { }, Lcom/google/protobuf/ExtensionRegistryLite;->getGeneratedRegistry()Lcom/google/protobuf/ExtensionRegistryLite; - move-result-object v5 - invoke-virtual { v4, v3, v5 }, $mergeFromMethod - move-result-object v5 - check-cast v5, $castReference - invoke-virtual { v5 }, $buildMethod - move-result-object v5 - check-cast v5, $playerConfigClass + $parsePlayerConfig # Check if player config contains android media lib config. invoke-static { v2 }, $EXTENSION_CLASS->hasAndroidMedia(Ljava/lang/String;)Z @@ -305,11 +310,15 @@ internal fun spoofVideoStreamsPatch( iget-object v7, v6, $mediaUstreamerRequestConfig if-eqz v7, :disabled - # Set media uStreamer request config. + # Set media uStreamer request config in a copy of the app player config. + # The app config may already be serialized, which caches its size, + # and changing it then breaks serializing it again. iget-object v5, p2, $setPlayerConfigField + invoke-virtual { v5 }, $playerConfigClass->toByteArray()[B + move-result-object v3 + $parsePlayerConfig iget-object v6, v5, $mediaCommonConfigField iput-object v7, v6, $mediaUstreamerRequestConfig - iput-object v6, v5, $mediaCommonConfigField :override_all_player_config # Set player config. From 5f09be081b9cabeca78da0ae3946c498d6c4346e Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Thu, 29 Oct 2026 09:56:13 +0000 Subject: [PATCH 157/189] chore: After changing a restart required setting and exiting the main setting menu, force restarting the app to apply the changes --- .../music/settings/MusicActivityHook.java | 2 +- .../reddit/settings/RedditActivityHook.java | 4 ++-- .../shared/settings/BaseActivityHook.java | 2 +- .../AbstractPreferenceFragment.java | 22 ++++++++++--------- .../youtube/settings/YouTubeActivityHook.java | 2 +- 5 files changed, 17 insertions(+), 15 deletions(-) diff --git a/extensions/music/src/main/java/app/morphe/extension/music/settings/MusicActivityHook.java b/extensions/music/src/main/java/app/morphe/extension/music/settings/MusicActivityHook.java index 3d2930973f..909efde05a 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/settings/MusicActivityHook.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/settings/MusicActivityHook.java @@ -174,7 +174,7 @@ protected PreferenceFragment createPreferenceFragment() { @SuppressWarnings("unused") public static boolean handleFinish(Activity activity) { return MusicSearchViewController.handleFinish(searchViewController) - || AbstractPreferenceFragment.showRestartDialogIfNeeded(activity, activity::finish); + || AbstractPreferenceFragment.showRestartDialogIfNeeded(activity); } /** diff --git a/extensions/reddit/src/main/java/app/morphe/extension/reddit/settings/RedditActivityHook.java b/extensions/reddit/src/main/java/app/morphe/extension/reddit/settings/RedditActivityHook.java index 9b87041bf5..c7e80aa86c 100644 --- a/extensions/reddit/src/main/java/app/morphe/extension/reddit/settings/RedditActivityHook.java +++ b/extensions/reddit/src/main/java/app/morphe/extension/reddit/settings/RedditActivityHook.java @@ -109,7 +109,7 @@ public static boolean handleFinish(Activity activity) { return false; } - return AbstractPreferenceFragment.showRestartDialogIfNeeded(activity, activity::finish); + return AbstractPreferenceFragment.showRestartDialogIfNeeded(activity); } /** @@ -187,7 +187,7 @@ public void onBackPressed() { * Closes the settings, after first showing the restart dialog if a changed setting requires it. */ private void closeSettings() { - if (!AbstractPreferenceFragment.showRestartDialogIfNeeded(getActivity(), this::dismiss)) { + if (!AbstractPreferenceFragment.showRestartDialogIfNeeded(getActivity())) { dismiss(); } } diff --git a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/settings/BaseActivityHook.java b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/settings/BaseActivityHook.java index 902a51d761..a55ee82f68 100644 --- a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/settings/BaseActivityHook.java +++ b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/settings/BaseActivityHook.java @@ -108,7 +108,7 @@ public static void initialize(BaseActivityHook hook, Activity activity) { * Closes the settings, after first showing the restart dialog if a changed setting requires it. */ public static void closeSettings(Activity activity) { - if (!AbstractPreferenceFragment.showRestartDialogIfNeeded(activity, activity::finish)) { + if (!AbstractPreferenceFragment.showRestartDialogIfNeeded(activity)) { activity.finish(); } } diff --git a/extensions/shared/library/src/main/java/app/morphe/extension/shared/settings/preference/AbstractPreferenceFragment.java b/extensions/shared/library/src/main/java/app/morphe/extension/shared/settings/preference/AbstractPreferenceFragment.java index 662b854458..8fd1279ee5 100644 --- a/extensions/shared/library/src/main/java/app/morphe/extension/shared/settings/preference/AbstractPreferenceFragment.java +++ b/extensions/shared/library/src/main/java/app/morphe/extension/shared/settings/preference/AbstractPreferenceFragment.java @@ -555,21 +555,23 @@ public static void setRestartNeeded() { * Shows the restart dialog if a changed setting requires an app restart. * Must be called when the user closes the settings. * - * @param closeSettings Closes the settings, if the user does not restart. * @return If the dialog is shown, and the settings must not close yet. */ - public static boolean showRestartDialogIfNeeded(Context context, Runnable closeSettings) { + public static boolean showRestartDialogIfNeeded(Context context) { if (!restartNeeded) { return false; } restartNeeded = false; - Dialog dialog = showRestartDialog(context); - dialog.setOnDismissListener(d -> closeSettings.run()); + showRestartDialog(context); return true; } - public static Dialog showRestartDialog(Context context) { + /** + * Shows a dialog that restarts the app when it is closed in any way, + * including the back button. + */ + public static void showRestartDialog(Context context) { Utils.verifyOnMainThread(); if (restartDialogTitle == null) { restartDialogTitle = str("morphe_settings_restart_title"); @@ -587,16 +589,16 @@ public static Dialog showRestartDialog(Context context) { restartDialogMessage, // Message. null, // No EditText. restartDialogButtonText, // OK button text. - () -> Utils.restartApp(context), // OK button action. - () -> {}, // Cancel button action (dismiss only). + () -> {}, // OK button action (dismiss restarts). + null, // No Cancel button. null, // No Neutral button text. null, // No Neutral button action. true // Dismiss dialog when onNeutralClick. ); - // Show the dialog. - dialogPair.first.show(); - return dialogPair.first; + Dialog dialog = dialogPair.first; + dialog.setOnDismissListener(d -> Utils.restartApp(context)); + dialog.show(); } /** diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/YouTubeActivityHook.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/YouTubeActivityHook.java index 4e1a215504..8b3f5ba73e 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/YouTubeActivityHook.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/YouTubeActivityHook.java @@ -187,7 +187,7 @@ public static boolean handleBackPress(Activity activity) { if (searchViewController != null && searchViewController.isSearchActive()) { return searchViewController.handleBackPress(); } - return AbstractPreferenceFragment.showRestartDialogIfNeeded(activity, activity::finish); + return AbstractPreferenceFragment.showRestartDialogIfNeeded(activity); } /** From 996bc000491cb0a23e08d856108b715b0dd31740 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Thu, 29 Oct 2026 11:12:13 +0000 Subject: [PATCH 158/189] chore(YouTube - Series tracker): Change patch to not depend on `Navigation bar` patch --- .../buttons/navigation/NavigationBarPatch.kt | 3 ++ .../youtube/video/series/HistoryPatch.kt | 40 +++++++++++++------ .../video/series/SeriesTrackerPatch.kt | 2 - 3 files changed, 30 insertions(+), 15 deletions(-) diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/buttons/navigation/NavigationBarPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/buttons/navigation/NavigationBarPatch.kt index b602bb24c6..beb29c8418 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/buttons/navigation/NavigationBarPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/buttons/navigation/NavigationBarPatch.kt @@ -626,6 +626,9 @@ val navigationBarPatch = bytecodePatch( } PivotBarRendererListFingerprint.let { + // The Series tracker patch hooks the same index, which shifts it. + it.clearMatch() + it.method.apply { val insertMatch = it.instructionMatches[2] val insertIndex = insertMatch.index diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/video/series/HistoryPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/video/series/HistoryPatch.kt index 94d351b364..7b1bca8306 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/video/series/HistoryPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/video/series/HistoryPatch.kt @@ -7,6 +7,7 @@ package app.morphe.patches.youtube.video.series +import app.morphe.patcher.Fingerprint import app.morphe.patcher.extensions.InstructionExtensions.addInstructions import app.morphe.patcher.extensions.InstructionExtensions.getInstruction import app.morphe.patcher.fieldAccess @@ -26,12 +27,12 @@ import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction import com.android.tools.smali.dexlib2.iface.instruction.RegisterRangeInstruction +import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction import com.android.tools.smali.dexlib2.iface.reference.FieldReference import com.android.tools.smali.dexlib2.iface.reference.MethodReference import com.android.tools.smali.dexlib2.immutable.ImmutableMethod import com.android.tools.smali.dexlib2.immutable.ImmutableMethodParameter -private const val NAV = "Lapp/morphe/extension/youtube/patches/NavigationBarPatch;" private const val OUR_NAV = "${SERIES_TRACKER_EXTENSION_PREFIX}HistoryNavigation;" internal fun BytecodePatchContext.wireHistory() { @@ -102,19 +103,32 @@ internal fun BytecodePatchContext.wireHistory() { ) } - PivotBarRendererListFingerprint.method.apply { - val index = indexOfFirstInstructionOrThrow( - methodCall(definingClass = NAV, name = "getPivotBarRendererList") - ) - val register = getInstruction(index + 1).registerA + // Add the Series button to the navigation bar items before they are stored. + // Independent of the Navigation bar patch, which hooks the same index and also converts + // the list back to a proto list, so the hooks chain in either order. The match is redone + // because the other hook shifts the index. + PivotBarRendererListFingerprint.let { + it.clearMatch() - addInstructions( - index + 2, - """ - invoke-static/range { v$register .. v$register }, $OUR_NAV->navigation(Ljava/util/List;)Ljava/util/List; - move-result-object v$register - """ - ) + it.method.apply { + val insertMatch = it.instructionMatches[2] + val register = getInstruction(insertMatch.index).registerA + val protoListBuilderMethod = Fingerprint( + accessFlags = listOf(AccessFlags.PUBLIC, AccessFlags.STATIC), + returnType = insertMatch.instruction.getReference()!!.type, + parameters = listOf("Ljava/util/Collection;") + ).method + + addInstructions( + insertMatch.index, + """ + invoke-static/range { v$register .. v$register }, $OUR_NAV->navigation(Ljava/util/List;)Ljava/util/List; + move-result-object v$register + invoke-static/range { v$register .. v$register }, $protoListBuilderMethod + move-result-object v$register + """ + ) + } } YouTubeMainActivityOnBackPressedFingerprint.method.addInstructions( diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/video/series/SeriesTrackerPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/video/series/SeriesTrackerPatch.kt index 6bcadc17b9..f5bbfdd8b1 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/video/series/SeriesTrackerPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/video/series/SeriesTrackerPatch.kt @@ -17,7 +17,6 @@ import app.morphe.patches.shared.misc.settings.preference.NonInteractivePreferen import app.morphe.patches.shared.misc.settings.preference.PreferenceScreenPreference import app.morphe.patches.shared.misc.settings.preference.PreferenceScreenPreference.Sorting import app.morphe.patches.shared.misc.settings.preference.SwitchPreference -import app.morphe.patches.youtube.layout.buttons.navigation.navigationBarPatch import app.morphe.patches.youtube.layout.player.icons.copyPlayerButtonIcons import app.morphe.patches.youtube.layout.playlistautoplay.navigationIntentHook import app.morphe.patches.youtube.misc.extension.sharedExtensionPatch @@ -82,7 +81,6 @@ val seriesTrackerPatch = bytecodePatch( settingsPatch, versionCheckPatch, videoInformationPatch, - navigationBarPatch, navigationBarHookPatch, seriesTrackerResourcesPatch, legacyPlayerControlsPatch, From f049c91c540f74da8dffe462a8fa5b10cb073305 Mon Sep 17 00:00:00 2001 From: ILoveOpenSourceApplications <117499019+ILoveOpenSourceApplications@users.noreply.github.com> Date: Thu, 29 Oct 2026 18:05:01 +0530 Subject: [PATCH 159/189] bump(YouTube Music): Add support for `9.30.53` --- .../main/kotlin/app/morphe/patches/music/shared/Constants.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/patches/src/main/kotlin/app/morphe/patches/music/shared/Constants.kt b/patches/src/main/kotlin/app/morphe/patches/music/shared/Constants.kt index 13222321ca..3ba25c30b3 100644 --- a/patches/src/main/kotlin/app/morphe/patches/music/shared/Constants.kt +++ b/patches/src/main/kotlin/app/morphe/patches/music/shared/Constants.kt @@ -33,7 +33,7 @@ internal object Constants { isExperimental = true, ), AppTarget( - version = "9.20.53", + version = "9.30.53", minSdk = 26 ), AppTarget( From 69b4632abef9bbd1a720ce6516b336b0edebca3b Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Thu, 29 Oct 2026 14:14:29 +0000 Subject: [PATCH 160/189] chore(YouTube - Series tracker): Patching fails when the Navigation bar patch is not applied first --- .../morphe/patches/youtube/video/series/Fingerprints.kt | 8 -------- .../morphe/patches/youtube/video/series/HistoryPatch.kt | 9 ++------- .../patches/youtube/video/series/SeriesTrackerPatch.kt | 2 ++ 3 files changed, 4 insertions(+), 15 deletions(-) diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/video/series/Fingerprints.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/video/series/Fingerprints.kt index f63ce6a786..4488ee8d2f 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/video/series/Fingerprints.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/video/series/Fingerprints.kt @@ -62,14 +62,6 @@ internal object CurrentAccountProviderFingerprint : Fingerprint( ) ) -internal class RegisteredParseFingerprint(parse: MethodReference) : Fingerprint( - definingClass = parse.definingClass, - name = "parseFrom", - parameters = parse.parameterTypes.map(CharSequence::toString) + - "Lcom/google/protobuf/ExtensionRegistryLite;", - returnType = parse.returnType, -) - internal object GeneratedRegistryFingerprint : Fingerprint( definingClass = "Lcom/google/protobuf/ExtensionRegistryLite;", name = "getGeneratedRegistry", diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/video/series/HistoryPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/video/series/HistoryPatch.kt index 7b1bca8306..b1f3541d96 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/video/series/HistoryPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/video/series/HistoryPatch.kt @@ -17,11 +17,11 @@ import app.morphe.patcher.patch.PatchException import app.morphe.patcher.util.getReference import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable import app.morphe.patcher.util.registersUsed +import app.morphe.patches.shared.misc.fix.proto.parseByteArrayWithRegistryMethodRef import app.morphe.patches.youtube.layout.buttons.navigation.PivotBarRendererFingerprint import app.morphe.patches.youtube.layout.buttons.navigation.PivotBarRendererListFingerprint import app.morphe.patches.youtube.misc.backgesture.YouTubeMainActivityOnBackPressedFingerprint import app.morphe.util.findInstructionIndicesReversedOrThrow -import app.morphe.util.indexOfFirstInstructionOrThrow import app.morphe.util.indexOfFirstInstructionReversedOrThrow import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation @@ -29,7 +29,6 @@ import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction import com.android.tools.smali.dexlib2.iface.instruction.RegisterRangeInstruction import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction import com.android.tools.smali.dexlib2.iface.reference.FieldReference -import com.android.tools.smali.dexlib2.iface.reference.MethodReference import com.android.tools.smali.dexlib2.immutable.ImmutableMethod import com.android.tools.smali.dexlib2.immutable.ImmutableMethodParameter @@ -38,11 +37,7 @@ private const val OUR_NAV = "${SERIES_TRACKER_EXTENSION_PREFIX}HistoryNavigation internal fun BytecodePatchContext.wireHistory() { // Build the Series navigation button from the same renderer factory YouTube uses. val factory = PivotBarRendererFingerprint.method - val parseIndex = factory.indexOfFirstInstructionOrThrow( - methodCall(name = "parseFrom", parameters = listOf("L", "[B")) - ) - val parse = factory.getInstruction(parseIndex).getReference()!! - val registeredParse = RegisteredParseFingerprint(parse).method + val registeredParse = parseByteArrayWithRegistryMethodRef.get()!! val registry = GeneratedRegistryFingerprint.method val wrapperType = factory.parameterTypes.single().toString() val default = classDefBy(wrapperType).fields.single { diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/video/series/SeriesTrackerPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/video/series/SeriesTrackerPatch.kt index f5bbfdd8b1..94cdd94f4a 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/video/series/SeriesTrackerPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/video/series/SeriesTrackerPatch.kt @@ -13,6 +13,7 @@ import app.morphe.patcher.extensions.InstructionExtensions.getInstruction import app.morphe.patcher.patch.bytecodePatch import app.morphe.patcher.patch.resourcePatch import app.morphe.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable +import app.morphe.patches.shared.misc.fix.proto.fixProtoLibraryPatch import app.morphe.patches.shared.misc.settings.preference.NonInteractivePreference import app.morphe.patches.shared.misc.settings.preference.PreferenceScreenPreference import app.morphe.patches.shared.misc.settings.preference.PreferenceScreenPreference.Sorting @@ -80,6 +81,7 @@ val seriesTrackerPatch = bytecodePatch( sharedExtensionPatch, settingsPatch, versionCheckPatch, + fixProtoLibraryPatch, videoInformationPatch, navigationBarHookPatch, seriesTrackerResourcesPatch, From 4b3a8dc9d98fde8192aa252e822bea307a5bf732 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Thu, 29 Oct 2026 14:23:52 +0000 Subject: [PATCH 161/189] chore(YouTube - Miniplayer): Restore 21.30 in-app message of where the old miniplayers went --- .../youtube/layout/miniplayer/MiniplayerPatch.kt | 15 ++++++++++++--- .../addresources/values/youtube/strings.xml | 2 ++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/miniplayer/MiniplayerPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/miniplayer/MiniplayerPatch.kt index 92abc34cb3..d7d08ee99a 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/miniplayer/MiniplayerPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/miniplayer/MiniplayerPatch.kt @@ -40,6 +40,7 @@ import app.morphe.patches.youtube.misc.playservice.is_20_37_or_greater import app.morphe.patches.youtube.misc.playservice.is_21_17_or_greater import app.morphe.patches.youtube.misc.playservice.is_21_29_or_greater import app.morphe.patches.youtube.misc.playservice.is_21_30_or_greater +import app.morphe.patches.youtube.misc.playservice.is_21_31_or_greater import app.morphe.patches.youtube.misc.playservice.is_21_32_or_greater import app.morphe.patches.youtube.misc.playservice.is_21_42_or_greater import app.morphe.patches.youtube.misc.playservice.is_21_43_or_greater @@ -100,15 +101,23 @@ val miniplayerPatch = bytecodePatch( if (is_20_37_or_greater) { // 21.29 removed all modern miniplayers except modern 4 - preferences += if (!is_21_29_or_greater) { - ListPreference("morphe_miniplayer_type") + if (!is_21_29_or_greater) { + preferences += ListPreference("morphe_miniplayer_type") } else { // Only modern 4 is left, plus the minimal bar rebuilt on top of it. - ListPreference( + preferences += ListPreference( key = "morphe_miniplayer_type", entriesKey = "morphe_miniplayer_type_21_29_entries", entryValuesKey = "morphe_miniplayer_type_21_29_entry_values" ) + // TODO: Eventually remove this. + // Tell stable users upgrading from 21.23 where the other miniplayers went. + if (!is_21_31_or_greater) { + preferences += NonInteractivePreference( + key = "morphe_miniplayer_not_available", + selectable = false + ) + } } } else { preferences += ListPreference( diff --git a/patches/src/main/resources/addresources/values/youtube/strings.xml b/patches/src/main/resources/addresources/values/youtube/strings.xml index 9c397ea710..8e3f21997d 100644 --- a/patches/src/main/resources/addresources/values/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values/youtube/strings.xml @@ -1323,6 +1323,8 @@ Limitation: If \'Restore old video action bar\' is turned off, \'More videos\' o Disables the \'Continue watching\' miniplayer, a YouTube Premium feature that appears on app startup Miniplayer Customize the appearance and behavior of the minimized video player + Additional miniplayers + YouTube removed all additional miniplayers. To restore more miniplayer types, then patch YouTube 21.28.208 or older Miniplayer type Hide title and channel Disabled From 136851e5a5690fd9e2488eb3f0e57537bdfffc2a Mon Sep 17 00:00:00 2001 From: Anurag Rai <129479513+anuraggr@users.noreply.github.com> Date: Thu, 29 Oct 2026 20:14:43 +0530 Subject: [PATCH 162/189] feat(YouTube): Add offline sponsorblock segment support for in-app downloads (#4140) Co-authored-by: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> --- .../sponsorblock/MusicSponsorBlockConfig.java | 1 + .../SegmentPlaybackController.java | 15 +- .../shared/sponsorblock/SponsorBlockApi.java | 1 + .../SponsorBlockOfflineStorage.java | 179 ++++++++++++++++++ .../extension/youtube/settings/Settings.java | 1 + .../YouTubeSponsorBlockConfig.java | 39 +++- .../interaction/downloads/DownloadsPatch.kt | 24 ++- .../layout/sponsorblock/SponsorBlockPatch.kt | 9 + .../values/sponsorblock/strings.xml | 2 + 9 files changed, 252 insertions(+), 19 deletions(-) create mode 100644 extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/sponsorblock/SponsorBlockOfflineStorage.java diff --git a/extensions/music/src/main/java/app/morphe/extension/music/sponsorblock/MusicSponsorBlockConfig.java b/extensions/music/src/main/java/app/morphe/extension/music/sponsorblock/MusicSponsorBlockConfig.java index 0a1807e73b..abf1d16c4c 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/sponsorblock/MusicSponsorBlockConfig.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/sponsorblock/MusicSponsorBlockConfig.java @@ -154,6 +154,7 @@ private MusicSponsorBlockConfig() {} @Override public @NonNull BooleanSetting toastOnConnectionError() { return Settings.SB_TOAST_ON_CONNECTION_ERROR; } @Override public @NonNull StringSetting apiUrl() { return Settings.SB_API_URL; } @Override public @Nullable BooleanSetting videoLengthWithoutSegments() { return null; } + @Override public @Nullable BooleanSetting offlineSegments() { return null; } @Override public @Nullable BooleanSetting toastOnWhitelistedChannel() { return null; } @Override public @Nullable BooleanSetting autoHideSkipButton() { return null; } @Override public @Nullable BooleanSetting userIsVip() { return null; } diff --git a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/sponsorblock/SegmentPlaybackController.java b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/sponsorblock/SegmentPlaybackController.java index aa7603a09b..414188973a 100644 --- a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/sponsorblock/SegmentPlaybackController.java +++ b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/sponsorblock/SegmentPlaybackController.java @@ -401,7 +401,7 @@ public static void setCurrentVideoId(@Nullable String videoId) { Logger.printDebug(() -> "Ignoring Short"); return; } - if (!Utils.isNetworkConnected()) { + if (!Utils.isNetworkConnected() && !SponsorBlockOfflineStorage.hasSavedSegments(videoId)) { Logger.printDebug(() -> "Network not connected, ignoring video"); return; } @@ -432,7 +432,16 @@ public static void executeDownloadSegments(String videoId) { Objects.requireNonNull(videoId); Utils.verifyOffMainThread(); - SponsorSegment[] segments = SBRequester.getSegments(videoId); + SponsorSegment[] segments; + if (Utils.isNetworkConnected()) { + segments = SBRequester.getSegments(videoId); + // Keep the saved segments of downloaded videos up to date. + if (segments.length > 0 && SponsorBlockOfflineStorage.hasSavedSegments(videoId)) { + SponsorBlockOfflineStorage.saveSegments(videoId, segments); + } + } else { + segments = SponsorBlockOfflineStorage.loadSegments(videoId); + } Utils.runOnMainThread(() -> { if (!videoId.equals(currentVideoId)) { @@ -856,7 +865,7 @@ private static void skipSegment(SponsorSegment segmentToSkip, boolean userManual if (timeSavedCount != null) timeSavedCount.save(timeSavedCount.get() + 1); BooleanSetting trackSkipCount = settings().trackSkipCount(); - if (trackSkipCount == null || trackSkipCount.get()) { + if ((trackSkipCount == null || trackSkipCount.get()) && Utils.isNetworkConnected()) { Utils.runOnBackgroundThread(() -> SBRequester.sendSegmentSkippedViewedRequest(segmentToSkip)); } } diff --git a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/sponsorblock/SponsorBlockApi.java b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/sponsorblock/SponsorBlockApi.java index 31217a0ac8..b6e47ae7a5 100644 --- a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/sponsorblock/SponsorBlockApi.java +++ b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/sponsorblock/SponsorBlockApi.java @@ -141,6 +141,7 @@ public interface SettingsAdapter { @NonNull StringSetting apiUrl(); @Nullable BooleanSetting videoLengthWithoutSegments(); + @Nullable BooleanSetting offlineSegments(); @Nullable BooleanSetting toastOnWhitelistedChannel(); @Nullable BooleanSetting autoHideSkipButton(); @Nullable BooleanSetting userIsVip(); diff --git a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/sponsorblock/SponsorBlockOfflineStorage.java b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/sponsorblock/SponsorBlockOfflineStorage.java new file mode 100644 index 0000000000..f63f8a6329 --- /dev/null +++ b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/sponsorblock/SponsorBlockOfflineStorage.java @@ -0,0 +1,179 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/4140 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to Morphe contributions. + */ + +package app.morphe.extension.shared.sponsorblock; + +import android.util.AtomicFile; + +import androidx.annotation.NonNull; + +import org.json.JSONArray; +import org.json.JSONObject; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +import app.morphe.extension.shared.Logger; +import app.morphe.extension.shared.Utils; +import app.morphe.extension.shared.settings.BooleanSetting; +import app.morphe.extension.shared.sponsorblock.SponsorBlockApi.SettingsAdapter; +import app.morphe.extension.shared.sponsorblock.objects.SegmentCategory; +import app.morphe.extension.shared.sponsorblock.objects.SponsorSegment; +import app.morphe.extension.shared.sponsorblock.requests.SBRequester; + +/** + * Segments of downloaded videos, so they can be skipped while offline. + */ +public final class SponsorBlockOfflineStorage { + + private static final String DIRECTORY_NAME = "morphe_sb_offline"; + private static final String JSON_KEY_CATEGORY = "category"; + private static final String JSON_KEY_UUID = "uuid"; + private static final String JSON_KEY_START = "start"; + private static final String JSON_KEY_END = "end"; + private static final String JSON_KEY_LOCKED = "locked"; + + private SponsorBlockOfflineStorage() {} + + private static SettingsAdapter settings() { + return SponsorBlockApi.config().settings(); + } + + private static boolean isEnabled() { + BooleanSetting setting = settings().offlineSegments(); + return setting != null && settings().sbEnabled().get() && setting.get(); + } + + private static AtomicFile segmentsFile(String videoId) { + File directory = new File(Utils.getContext().getFilesDir(), DIRECTORY_NAME); + return new AtomicFile(new File(directory, videoId + ".json")); + } + + /** + * @return If segments are saved for the video and saving segments is enabled. + */ + public static boolean hasSavedSegments(String videoId) { + return isEnabled() && segmentsFile(videoId).getBaseFile().exists(); + } + + /** + * Fetches the segments of a video that is downloaded, and saves them. + */ + public static void fetchAndSaveSegments(String videoId) { + if (!isEnabled() || !Utils.isNetworkConnected()) { + return; + } + + // The categories to fetch are loaded when the player is created, + // which has not happened yet if the video is downloaded from the feed. + SegmentCategory.loadAllCategoriesFromSettings(); + + Utils.runOnBackgroundThread(() -> { + try { + SponsorSegment[] segments = SBRequester.getSegments(videoId); + if (segments.length > 0) { + saveSegments(videoId, segments); + } + } catch (Exception ex) { + Logger.printException(() -> "fetchAndSaveSegments failure", ex); + } + }); + } + + /** + * Must be called off main thread. + */ + public static void saveSegments(String videoId, SponsorSegment[] segments) { + Utils.verifyOffMainThread(); + AtomicFile file = segmentsFile(videoId); + FileOutputStream stream = null; + try { + JSONArray array = new JSONArray(); + for (SponsorSegment segment : segments) { + JSONObject object = new JSONObject(); + object.put(JSON_KEY_CATEGORY, segment.category.keyValue); + object.put(JSON_KEY_UUID, segment.UUID); + object.put(JSON_KEY_START, segment.start); + object.put(JSON_KEY_END, segment.end); + object.put(JSON_KEY_LOCKED, segment.isLocked); + array.put(object); + } + + File directory = file.getBaseFile().getParentFile(); + if (directory != null && !directory.exists() && !directory.mkdirs()) { + throw new IOException("Could not create directory: " + directory); + } + + stream = file.startWrite(); + stream.write(array.toString().getBytes(StandardCharsets.UTF_8)); + file.finishWrite(stream); + Logger.printDebug(() -> "Saved " + segments.length + " segments for: " + videoId); + } catch (Exception ex) { + if (stream != null) { + file.failWrite(stream); + } + Logger.printException(() -> "saveSegments failure", ex); + } + } + + /** + * Must be called off main thread. + * + * @return The saved segments, or an empty array if none are saved. + */ + @NonNull + public static SponsorSegment[] loadSegments(String videoId) { + Utils.verifyOffMainThread(); + if (!hasSavedSegments(videoId)) { + return new SponsorSegment[0]; + } + + AtomicFile file = segmentsFile(videoId); + try { + JSONArray array = new JSONArray(new String(file.readFully(), StandardCharsets.UTF_8)); + List segments = new ArrayList<>(array.length()); + for (int i = 0, length = array.length(); i < length; i++) { + JSONObject object = array.getJSONObject(i); + String categoryKey = object.getString(JSON_KEY_CATEGORY); + SegmentCategory category = SegmentCategory.byCategoryKey(categoryKey); + if (category == null) { + Logger.printException(() -> "Unknown saved category: " + categoryKey); + continue; + } + + segments.add(new SponsorSegment( + category, + object.isNull(JSON_KEY_UUID) ? null : object.getString(JSON_KEY_UUID), + object.getLong(JSON_KEY_START), + object.getLong(JSON_KEY_END), + object.getBoolean(JSON_KEY_LOCKED) + )); + } + + Logger.printDebug(() -> "Loaded " + segments.size() + " saved segments for: " + videoId); + return segments.toArray(new SponsorSegment[0]); + } catch (Exception ex) { + Logger.printException(() -> "loadSegments failure", ex); + return new SponsorSegment[0]; + } + } + + /** + * Deletes the saved segments of a video that is removed from the downloads. + */ + public static void deleteSegments(String videoId) { + AtomicFile file = segmentsFile(videoId); + if (file.getBaseFile().exists()) { + file.delete(); + Logger.printDebug(() -> "Deleted saved segments for: " + videoId); + } + } +} diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/Settings.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/Settings.java index a6bd1239bc..cc941914a9 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/Settings.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/Settings.java @@ -770,6 +770,7 @@ public class Settings extends SharedYouTubeSettings { public static final EnumSetting SB_AUTO_HIDE_SKIP_BUTTON_DURATION = new EnumSetting<>("morphe_sb_auto_hide_skip_button_duration", SponsorBlockDuration.FOUR_SECONDS, parent(SB_ENABLED)); public static final BooleanSetting SB_TOAST_ON_SKIP = new BooleanSetting("morphe_sb_toast_on_skip", TRUE, parent(SB_ENABLED)); public static final EnumSetting SB_TOAST_ON_SKIP_DURATION = new EnumSetting<>("morphe_sb_toast_on_skip_duration", SponsorBlockDuration.FOUR_SECONDS, parentsAll(SB_ENABLED, SB_TOAST_ON_SKIP)); + public static final BooleanSetting SB_OFFLINE_SEGMENTS = new BooleanSetting("morphe_sb_offline_segments", TRUE, parent(SB_ENABLED)); public static final BooleanSetting SB_TOAST_ON_CONNECTION_ERROR = new BooleanSetting("morphe_sb_toast_on_connection_error", TRUE, parent(SB_ENABLED)); public static final BooleanSetting SB_TRACK_SKIP_COUNT = new BooleanSetting("morphe_sb_track_skip_count", TRUE, parent(SB_ENABLED)); public static final FloatSetting SB_SEGMENT_MIN_DURATION = new FloatSetting("morphe_sb_min_segment_duration", 0F, parent(SB_ENABLED)); diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/sponsorblock/YouTubeSponsorBlockConfig.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/sponsorblock/YouTubeSponsorBlockConfig.java index a5ac1dcca1..505fea8385 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/sponsorblock/YouTubeSponsorBlockConfig.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/sponsorblock/YouTubeSponsorBlockConfig.java @@ -13,6 +13,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import app.morphe.extension.shared.Logger; import app.morphe.extension.shared.Utils; import app.morphe.extension.shared.settings.BooleanSetting; import app.morphe.extension.shared.settings.FloatSetting; @@ -29,8 +30,10 @@ import app.morphe.extension.shared.sponsorblock.SponsorBlockApi.SettingsAdapter; import app.morphe.extension.shared.sponsorblock.SponsorBlockApi.UiBridge; import app.morphe.extension.shared.sponsorblock.SponsorBlockApi.VideoInformationAdapter; +import app.morphe.extension.shared.sponsorblock.SponsorBlockOfflineStorage; import app.morphe.extension.shared.sponsorblock.objects.SegmentCategory; import app.morphe.extension.shared.sponsorblock.objects.SponsorSegment; +import app.morphe.extension.youtube.patches.DownloadsPatch; import app.morphe.extension.youtube.patches.VideoInformation; import app.morphe.extension.youtube.settings.Settings; import app.morphe.extension.youtube.shared.PlayerControlsVisibility; @@ -50,6 +53,12 @@ public final class YouTubeSponsorBlockConfig implements Configuration { private static final YouTubeSponsorBlockConfig INSTANCE = new YouTubeSponsorBlockConfig(); + /** + * Names of the app enum values of the offline video endpoint action. + */ + private static final String OFFLINE_VIDEO_ACTION_ADD = "ACTION_ADD"; + private static final String OFFLINE_VIDEO_ACTION_REMOVE = "ACTION_REMOVE"; + private static volatile boolean installed; /** @@ -65,9 +74,7 @@ public static void install() { SegmentPlaybackController.onPlayerTypeChanged(); }); - VideoInformation.onChannelIdChange.addObserver((String channelId) -> { - SegmentPlaybackController.onChannelIdChange(channelId); - }); + VideoInformation.onChannelIdChange.addObserver(SegmentPlaybackController::onChannelIdChange); // Mirrors the side effects the legacy SponsorBlockPreferenceGroup applied from its // updateUI() pass when the user changed an SB setting. @@ -149,6 +156,31 @@ public static void setAdProgressTextVisibility(int visibility) { SegmentPlaybackController.setAdProgressTextVisibility(visibility); } + /** + * Injection point. + *

    + * Called when a video is saved or removed with the offline video endpoint. + * Removing with a prompt is ignored, since the prompt can be cancelled. + * + * @param action Action of the offline video endpoint. + */ + @SuppressWarnings("unused") + public static void offlineVideoAction(String videoId, Enum action) { + try { + String actionName = action.name(); + if (actionName.equals(OFFLINE_VIDEO_ACTION_ADD)) { + // The external downloader is used instead. + if (DownloadsPatch.isDownloadModeHandledByMorphe()) return; + + SponsorBlockOfflineStorage.fetchAndSaveSegments(videoId); + } else if (actionName.equals(OFFLINE_VIDEO_ACTION_REMOVE)) { + SponsorBlockOfflineStorage.deleteSegments(videoId); + } + } catch (Exception ex) { + Logger.printException(() -> "offlineVideoAction failure", ex); + } + } + private YouTubeSponsorBlockConfig() {} @Override public boolean undoToastEnabled() { return true; } @@ -177,6 +209,7 @@ private YouTubeSponsorBlockConfig() {} @Override public @NonNull BooleanSetting toastOnConnectionError() { return Settings.SB_TOAST_ON_CONNECTION_ERROR; } @Override public @NonNull StringSetting apiUrl() { return Settings.SB_API_URL; } @Override public @NonNull BooleanSetting videoLengthWithoutSegments() { return Settings.SB_VIDEO_LENGTH_WITHOUT_SEGMENTS; } + @Override public @NonNull BooleanSetting offlineSegments() { return Settings.SB_OFFLINE_SEGMENTS; } @Override public @NonNull BooleanSetting toastOnWhitelistedChannel() { return Settings.SB_TOAST_ON_WHITELISTED_CHANNEL; } @Override public @NonNull BooleanSetting autoHideSkipButton() { return Settings.SB_AUTO_HIDE_SKIP_BUTTON; } @Override public @NonNull BooleanSetting userIsVip() { return Settings.SB_USER_IS_VIP; } diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/downloads/DownloadsPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/downloads/DownloadsPatch.kt index 8914610847..6245a0cbcc 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/downloads/DownloadsPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/downloads/DownloadsPatch.kt @@ -139,19 +139,17 @@ val downloadsPatch = bytecodePatch( setExtensionIsPatchIncluded(EXTENSION_SHORTS_DOWNLOAD_BUTTON) - OfflineVideoEndpointFingerprint.method.apply { - addInstructionsWithLabels( - 0, - """ - invoke-static/range { p3 .. p3 }, $EXTENSION_CLASS->inAppDownloadButtonOnClick(Ljava/lang/String;)Z - move-result v0 - if-eqz v0, :show_native_downloader - return-void - :show_native_downloader - nop - """ - ) - } + OfflineVideoEndpointFingerprint.method.addInstructionsWithLabels( + 0, + """ + invoke-static/range { p3 .. p3 }, $EXTENSION_CLASS->inAppDownloadButtonOnClick(Ljava/lang/String;)Z + move-result v0 + if-eqz v0, :show_native_downloader + return-void + :show_native_downloader + nop + """ + ) // Open the external downloader with the playlist link, instead of saving the playlist. OfflinePlaylistEndpointFingerprint.method.addInstructionsWithLabels( diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/sponsorblock/SponsorBlockPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/sponsorblock/SponsorBlockPatch.kt index 09f9f889b9..e52e6cf517 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/sponsorblock/SponsorBlockPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/sponsorblock/SponsorBlockPatch.kt @@ -26,6 +26,7 @@ import app.morphe.patches.shared.misc.settings.preference.PreferenceScreenPrefer import app.morphe.patches.shared.misc.settings.preference.SwitchPreference import app.morphe.patches.shared.misc.settings.preference.TextPreference import app.morphe.patches.shared.misc.settings.preference.screenInfoPreferenceCategory +import app.morphe.patches.youtube.interaction.downloads.OfflineVideoEndpointFingerprint import app.morphe.patches.youtube.layout.player.icons.copyPlayerButtonIcons import app.morphe.patches.youtube.misc.extension.sharedExtensionPatch import app.morphe.patches.youtube.misc.playercontrols.addTopControl @@ -128,6 +129,7 @@ private val sponsorBlockResourcePatch = resourcePatch { key = "morphe_sb_general", sorting = PreferenceScreenPreference.Sorting.UNSORTED, preferences = setOf( + SwitchPreference("morphe_sb_offline_segments", summary = true), SwitchPreference("morphe_sb_toast_on_connection_error", summary = true), SwitchPreference("morphe_sb_track_skip_count", summary = true), TextPreference( @@ -344,6 +346,13 @@ val sponsorBlockPatch = bytecodePatch( } } + // Save and remove the segments of downloaded videos. + OfflineVideoEndpointFingerprint.method.addInstruction( + 0, + "invoke-static/range { p3 .. p4 }, $EXTENSION_SEGMENT_PLAYBACK_CONTROLLER_CLASS->" + + "offlineVideoAction(Ljava/lang/String;Ljava/lang/Enum;)V" + ) + // FIXME: Skip buttons do not show in new player controls layout. // Skip buttons may need to be added at runtime like other overlay buttons. if (is_21_36_or_greater) { diff --git a/patches/src/main/resources/addresources/values/sponsorblock/strings.xml b/patches/src/main/resources/addresources/values/sponsorblock/strings.xml index af2d4acae8..ab8eac8d74 100644 --- a/patches/src/main/resources/addresources/values/sponsorblock/strings.xml +++ b/patches/src/main/resources/addresources/values/sponsorblock/strings.xml @@ -57,6 +57,8 @@ Second \"item\" text" Already read Show me General + Save segments of downloaded videos + Allows skipping segments of downloaded videos while offline Show connection error alerts Shows a notification when the SponsorBlock server is unavailable Enable skip count tracking From de2d7e94cf661ea558ee257ca2cfa7facc81a608 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Thu, 29 Oct 2026 14:46:17 +0000 Subject: [PATCH 163/189] chore(YouTube Music): For now keep `9.20.53` and remove a little later --- .../main/kotlin/app/morphe/patches/music/shared/Constants.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/patches/src/main/kotlin/app/morphe/patches/music/shared/Constants.kt b/patches/src/main/kotlin/app/morphe/patches/music/shared/Constants.kt index 3ba25c30b3..f91bb92a90 100644 --- a/patches/src/main/kotlin/app/morphe/patches/music/shared/Constants.kt +++ b/patches/src/main/kotlin/app/morphe/patches/music/shared/Constants.kt @@ -36,6 +36,10 @@ internal object Constants { version = "9.30.53", minSdk = 26 ), + AppTarget( + version = "9.20.53", + minSdk = 26 + ), AppTarget( version = "9.15.51", minSdk = 26 From a3b34d75d399fcc7dd3f9cb2c68f32fc112d979b Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Thu, 29 Oct 2026 18:25:53 +0100 Subject: [PATCH 164/189] feat(YouTube): Highlight new in-app settings (#4162) --- .../preference/MusicPreferenceFragment.java | 3 + .../settings/SharedYouTubeSettings.java | 7 + .../AppRefreshRateListPreference.java | 5 + .../ExternalDownloaderPreference.java | 5 + .../preference/NewSettingsIndicator.java | 222 ++++++++++++++++++ .../preference/ToolbarPreferenceFragment.java | 5 +- .../CustomDialogListPreference.java | 37 ++- .../settings/preference/NewIndicatorSpan.java | 102 ++++++++ .../preference/SeekBarPreference.java | 16 +- .../CustomVideoSpeedListPreference.java | 4 + .../SwipeVolumeStepsPreference.java | 5 + .../preference/YouTubePreferenceFragment.java | 3 + .../music/misc/settings/SettingsPatch.kt | 5 + .../youtube/misc/settings/SettingsPatch.kt | 5 + .../values/shared-youtube/strings.xml | 5 + 15 files changed, 423 insertions(+), 6 deletions(-) create mode 100644 extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/settings/preference/NewSettingsIndicator.java create mode 100644 extensions/shared/library/src/main/java/app/morphe/extension/shared/settings/preference/NewIndicatorSpan.java diff --git a/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/MusicPreferenceFragment.java b/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/MusicPreferenceFragment.java index 5da5dbcc28..35eb781418 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/MusicPreferenceFragment.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/MusicPreferenceFragment.java @@ -18,6 +18,7 @@ import app.morphe.extension.shared.Logger; import app.morphe.extension.shared.patches.GmsCoreSupportPatch; import app.morphe.extension.shared.settings.SharedYouTubeSettings; +import app.morphe.extension.shared.settings.preference.NewSettingsIndicator; import app.morphe.extension.shared.settings.preference.ToolbarPreferenceFragment; /** @@ -53,6 +54,8 @@ protected void initialize() { SharedYouTubeSettings.CUSTOM_BRANDING_NOTIFICATION_ICON.key, SharedYouTubeSettings.CUSTOM_BRANDING_NAME.key); } + + NewSettingsIndicator.markNewPreferences(preferenceScreen); } catch (Exception ex) { Logger.printException(() -> "initialize failure", ex); } diff --git a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/settings/SharedYouTubeSettings.java b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/settings/SharedYouTubeSettings.java index a7097edfd5..145bcf2f85 100644 --- a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/settings/SharedYouTubeSettings.java +++ b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/settings/SharedYouTubeSettings.java @@ -2,6 +2,7 @@ import static java.lang.Boolean.FALSE; import static java.lang.Boolean.TRUE; +import static app.morphe.extension.shared.StringRef.sf; import static app.morphe.extension.shared.settings.Setting.migrateOldSettingToNew; import static app.morphe.extension.shared.settings.Setting.parent; import static app.morphe.extension.shared.settings.Setting.parentNot; @@ -21,6 +22,8 @@ import app.morphe.extension.shared.patches.SplashAnimationPatch; import app.morphe.extension.shared.patches.SplashAnimationPatch.SplashScreenAnimationStyle; import app.morphe.extension.shared.settings.preference.ExternalDownloaderPreference.ExternalDownloaderAvailability; +import app.morphe.extension.shared.settings.preference.SeekBarPreference; +import app.morphe.extension.shared.settings.preference.SeekBarPreference.SeekBarConfig; import app.morphe.extension.shared.spoof.SpoofVideoStreamsPatch.JavaScriptClientAvailability; import app.morphe.extension.shared.spoof.js.JavaScriptVariant; import app.morphe.extension.shared.theme.ThemeColorPatch.ThemeColorChangeForegroundAvailability; @@ -40,6 +43,7 @@ public class SharedYouTubeSettings extends BaseSettings { public static final BooleanSetting SETTINGS_SEARCH_HISTORY = new BooleanSetting("morphe_settings_search_history", TRUE, true); public static final StringSetting SETTINGS_SEARCH_ENTRIES = new StringSetting("morphe_settings_search_entries", ""); + public static final IntegerSetting SETTINGS_NEW_INDICATOR_DAYS = new IntegerSetting("morphe_settings_new_indicator_days", 3); // Network proxy // There are multiple endpoint calls due to the spoof video streams patch. Place the proxy settings first. @@ -147,6 +151,9 @@ public class SharedYouTubeSettings extends BaseSettings { private static final EnumSetting DEPRECATED_SPLASH_SCREEN_ANIMATION_STYLE = new EnumSetting<>("morphe_splash_screen_animation_style", SplashScreenAnimationStyle.FPS_60_ONE_SECOND); static { + SeekBarPreference.register(new SeekBarConfig(SETTINGS_NEW_INDICATOR_DAYS, + 0, 30, 1, "", 1, sf("morphe_settings_new_indicator_days_off"), null)); + // TODO: Eventually remove these migrations migrateOldSettingToNew(DEPRECATED_SANITIZE_URL_QUERY, SANITIZE_SHARING_LINKS); diff --git a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/settings/preference/AppRefreshRateListPreference.java b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/settings/preference/AppRefreshRateListPreference.java index 4b086d47f8..bafed475d8 100644 --- a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/settings/preference/AppRefreshRateListPreference.java +++ b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/settings/preference/AppRefreshRateListPreference.java @@ -60,4 +60,9 @@ private void init() { setEntries(entries.toArray(new CharSequence[0])); setEntryValues(values.toArray(new CharSequence[0])); } + + @Override + public boolean tracksNewEntries() { + return false; // Entries depend on the refresh rates of the device. + } } diff --git a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/settings/preference/ExternalDownloaderPreference.java b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/settings/preference/ExternalDownloaderPreference.java index 912a8f6135..594e16f64d 100644 --- a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/settings/preference/ExternalDownloaderPreference.java +++ b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/settings/preference/ExternalDownloaderPreference.java @@ -555,4 +555,9 @@ public static boolean showDialogIfAppIsNotInstalled(Context context, String pack return true; } + + @Override + public boolean tracksNewEntries() { + return false; // Entries depend on the installed apps. + } } diff --git a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/settings/preference/NewSettingsIndicator.java b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/settings/preference/NewSettingsIndicator.java new file mode 100644 index 0000000000..aee013d3e0 --- /dev/null +++ b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/settings/preference/NewSettingsIndicator.java @@ -0,0 +1,222 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/4162 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to Morphe contributions. + */ + +package app.morphe.extension.shared.settings.preference; + +import android.content.SharedPreferences; +import android.preference.Preference; +import android.preference.PreferenceGroup; +import android.preference.PreferenceScreen; + +import androidx.annotation.ColorInt; + +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +import app.morphe.extension.shared.Logger; +import app.morphe.extension.shared.settings.SharedYouTubeSettings; + +/** + * Shows a dot next to settings and list entries that are new to the user, + * for the number of days set with {@link SharedYouTubeSettings#SETTINGS_NEW_INDICATOR_DAYS}. + *

    + * A setting is new from the first time its key is in the settings menu. + * The first time this runs, all current keys are saved as already known, + * so nothing is new after a fresh install or after updating to the first version with this feature. + */ +@SuppressWarnings("deprecation") +public final class NewSettingsIndicator { + + /** + * Morphe teal, the light end of the logo gradient. + */ + @ColorInt + private static final int DOT_COLOR = 0xFF00AFAE; + + private static final SharedPreferences preferences = + new SharedPrefCategory("morphe_new_settings").preferences; + + /** + * All preference keys and list entries that were seen in the settings menu. + */ + private static final String KNOWN_IDS_KEY = "known_ids"; + + /** + * Prefix of the keys that store the time an id was first seen. + * Only ids first seen within the max days of the seek bar are stored. + */ + private static final String FIRST_SEEN_KEY_PREFIX = "first_seen:"; + + /** + * Separates the preference key and the entry value in the id of a list entry. + */ + private static final String LIST_ENTRY_SEPARATOR = "|"; + + private static String listEntryId(String key, CharSequence entryValue) { + return key + LIST_ENTRY_SEPARATOR + entryValue; + } + + /** + * Saves the keys that are seen for the first time, and adds a dot to new preferences, + * list entries and the preference screens that contain them. + * Must be called after all preferences are added to and removed from the screen. + */ + public static void markNewPreferences(PreferenceScreen screen) { + try { + Set currentIds = new HashSet<>(); + collectIds(screen, currentIds); + + Map firstSeenTimes = updateFirstSeenTimes(currentIds); + + final int days = SeekBarPreference.clampToRange(SharedYouTubeSettings.SETTINGS_NEW_INDICATOR_DAYS); + if (days == 0 || firstSeenTimes.isEmpty()) { + return; + } + + final long newAfter = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(days); + Set newIds = new HashSet<>(); + for (Map.Entry entry : firstSeenTimes.entrySet()) { + if (entry.getValue() > newAfter) { + newIds.add(entry.getKey()); + } + } + if (newIds.isEmpty()) { + return; + } + + Logger.printDebug(() -> "New settings: " + newIds); + markGroup(screen, newIds); + } catch (Exception ex) { + Logger.printException(() -> "markNewPreferences failure", ex); + } + } + + private static void collectIds(PreferenceGroup group, Set ids) { + for (int i = 0, count = group.getPreferenceCount(); i < count; i++) { + Preference pref = group.getPreference(i); + if (pref instanceof PreferenceGroup subGroup) { + // Group keys include the sorting and are not tracked. + // A group is new if anything inside it is new. + collectIds(subGroup, ids); + continue; + } + + String key = pref.getKey(); + if (key == null) continue; + ids.add(key); + + if (pref instanceof CustomDialogListPreference listPref && listPref.tracksNewEntries()) { + CharSequence[] entryValues = listPref.getEntryValues(); + if (entryValues != null) { + for (CharSequence entryValue : entryValues) { + ids.add(listEntryId(key, entryValue)); + } + } + } + } + } + + /** + * Saves ids seen for the first time, and removes first seen times older than the max days. + * + * @return The first seen times of the ids added within the max days. + */ + private static Map updateFirstSeenTimes(Set currentIds) { + if (!preferences.contains(KNOWN_IDS_KEY)) { + Logger.printDebug(() -> "Saving " + currentIds.size() + " known settings"); + preferences.edit().putStringSet(KNOWN_IDS_KEY, currentIds).apply(); + return Collections.emptyMap(); + } + + Set knownIds = new HashSet<>(preferences.getStringSet(KNOWN_IDS_KEY, Collections.emptySet())); + SharedPreferences.Editor editor = preferences.edit(); + boolean changed = false; + final long now = System.currentTimeMillis(); + Map firstSeenTimes = new HashMap<>(); + for (String id : currentIds) { + if (knownIds.add(id)) { + editor.putLong(FIRST_SEEN_KEY_PREFIX + id, now); + firstSeenTimes.put(id, now); + changed = true; + } + } + if (changed) { + editor.putStringSet(KNOWN_IDS_KEY, knownIds); + } + + SeekBarPreference.SeekBarConfig config = + SeekBarPreference.configFor(SharedYouTubeSettings.SETTINGS_NEW_INDICATOR_DAYS); + final long expiredBefore = now - TimeUnit.DAYS.toMillis(config.max()); + for (Map.Entry entry : preferences.getAll().entrySet()) { + String key = entry.getKey(); + if (!key.startsWith(FIRST_SEEN_KEY_PREFIX)) continue; + + final long firstSeen = (Long) entry.getValue(); + if (firstSeen < expiredBefore) { + editor.remove(key); + changed = true; + } else { + firstSeenTimes.put(key.substring(FIRST_SEEN_KEY_PREFIX.length()), firstSeen); + } + } + + if (changed) { + editor.apply(); + } + return firstSeenTimes; + } + + /** + * @return If the group contains a new preference or list entry. + */ + private static boolean markGroup(PreferenceGroup group, Set newIds) { + boolean groupHasNew = false; + + for (int i = 0, count = group.getPreferenceCount(); i < count; i++) { + Preference pref = group.getPreference(i); + final boolean isNew; + + if (pref instanceof PreferenceGroup subGroup) { + isNew = markGroup(subGroup, newIds); + } else { + String key = pref.getKey(); + if (key == null) continue; + + boolean hasNewEntry = false; + if (pref instanceof CustomDialogListPreference listPref && listPref.tracksNewEntries()) { + CharSequence[] entryValues = listPref.getEntryValues(); + if (entryValues != null) { + Set newEntryValues = new HashSet<>(); + for (CharSequence entryValue : entryValues) { + if (newIds.contains(listEntryId(key, entryValue))) { + newEntryValues.add(entryValue.toString()); + } + } + if (!newEntryValues.isEmpty()) { + listPref.setNewEntryValues(newEntryValues, DOT_COLOR); + hasNewEntry = true; + } + } + } + + isNew = hasNewEntry || newIds.contains(key); + } + + // Categories are not marked, since the new preference is shown right below them. + if (isNew && (pref instanceof PreferenceScreen || !(pref instanceof PreferenceGroup))) { + pref.setTitle(NewIndicatorSpan.addTo(pref.getTitle(), DOT_COLOR)); + } + groupHasNew |= isNew; + } + + return groupHasNew; + } +} diff --git a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/settings/preference/ToolbarPreferenceFragment.java b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/settings/preference/ToolbarPreferenceFragment.java index fa7c099d93..07533c8529 100644 --- a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/settings/preference/ToolbarPreferenceFragment.java +++ b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/settings/preference/ToolbarPreferenceFragment.java @@ -121,7 +121,8 @@ protected void setPreferenceScreenToolbar(PreferenceScreen parentScreen) { } Toolbar toolbar = new Toolbar(childScreen.getContext()); - toolbar.setTitle(childScreen.getTitle()); + // The title can have a new setting dot, which is not shown here. + toolbar.setTitle(childScreen.getTitle().toString()); toolbar.setNavigationIcon(getBackButtonDrawable()); toolbar.setNavigationContentDescription(str("morphe_settings_navigate_up")); toolbar.setNavigationOnClickListener(view -> preferenceScreenDialog.dismiss()); @@ -138,7 +139,7 @@ protected void setPreferenceScreenToolbar(PreferenceScreen parentScreen) { if (screenInfo != null) { addToolbarButton(toolbar, "morphe_settings_screen_00_about", str("morphe_settings_screen_info_title"), - () -> screenInfo.showDialog(toolbar.getContext(), childScreen.getTitle())); + () -> screenInfo.showDialog(toolbar.getContext(), childScreen.getTitle().toString())); } // Allow package-specific toolbar customization. diff --git a/extensions/shared/library/src/main/java/app/morphe/extension/shared/settings/preference/CustomDialogListPreference.java b/extensions/shared/library/src/main/java/app/morphe/extension/shared/settings/preference/CustomDialogListPreference.java index e3c07a57cf..1abd5c53e2 100644 --- a/extensions/shared/library/src/main/java/app/morphe/extension/shared/settings/preference/CustomDialogListPreference.java +++ b/extensions/shared/library/src/main/java/app/morphe/extension/shared/settings/preference/CustomDialogListPreference.java @@ -25,9 +25,11 @@ import android.widget.ListView; import android.widget.TextView; +import androidx.annotation.ColorInt; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import java.util.Set; import java.util.function.IntConsumer; import app.morphe.extension.shared.ResourceType; @@ -54,6 +56,10 @@ public class CustomDialogListPreference extends ListPreference { private String staticSummary = null; private CharSequence[] highlightedEntriesForDialog = null; + @Nullable + private Set newEntryValues; + @ColorInt + private int newEntryColor; /** * Set a static summary that will not be overwritten by value changes. @@ -96,12 +102,41 @@ public void clearHighlightedEntriesForDialog() { this.highlightedEntriesForDialog = null; } + /** + * @return If an entry that was not shown before is a new option added by an update. + * False if the entries depend on the device, installed apps or user data. + */ + public boolean tracksNewEntries() { + return true; + } + + /** + * Sets the entry values that are shown with a dot in the dialog. + */ + public void setNewEntryValues(@Nullable Set values, @ColorInt int color) { + this.newEntryValues = values; + this.newEntryColor = color; + } + /** * Returns entries for display in the dialog. * If highlighted entries exist, they are used; otherwise, the original entries are returned. + * New entries are shown with a dot. */ protected CharSequence[] getEntriesForDialog() { - return highlightedEntriesForDialog != null ? highlightedEntriesForDialog : getEntries(); + CharSequence[] entries = highlightedEntriesForDialog != null ? highlightedEntriesForDialog : getEntries(); + CharSequence[] entryValues = getEntryValues(); + if (newEntryValues == null || entries == null || entryValues == null) { + return entries; + } + + CharSequence[] marked = entries.clone(); + for (int i = 0, length = Math.min(entries.length, entryValues.length); i < length; i++) { + if (newEntryValues.contains(entryValues[i].toString())) { + marked[i] = NewIndicatorSpan.addTo(entries[i], newEntryColor); + } + } + return marked; } /** diff --git a/extensions/shared/library/src/main/java/app/morphe/extension/shared/settings/preference/NewIndicatorSpan.java b/extensions/shared/library/src/main/java/app/morphe/extension/shared/settings/preference/NewIndicatorSpan.java new file mode 100644 index 0000000000..1427279ee5 --- /dev/null +++ b/extensions/shared/library/src/main/java/app/morphe/extension/shared/settings/preference/NewIndicatorSpan.java @@ -0,0 +1,102 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/4162 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to Morphe contributions. + */ + +package app.morphe.extension.shared.settings.preference; + +import android.graphics.Canvas; +import android.graphics.Paint; +import android.text.SpannableString; +import android.text.Spanned; +import android.text.TextDirectionHeuristics; +import android.text.TextUtils; +import android.text.style.ReplacementSpan; + +import androidx.annotation.ColorInt; +import androidx.annotation.Nullable; + +/** + * Draws a small dot after the last character of a text, to show a setting or list entry is new. + *

    + * The span covers the existing last character and draws it together with the dot, + * so the text itself does not change. Code that reads the text, such as settings search, + * sorting or the copied preference path, does not see the dot. + */ +public class NewIndicatorSpan extends ReplacementSpan { + + /** + * Dot radius and the gap before the dot, as a fraction of the text size. + */ + private static final float DOT_RADIUS_TEXT_SIZE_RATIO = 0.18f; + private static final float DOT_GAP_TEXT_SIZE_RATIO = 0.3f; + + /** + * @return A copy of the text with the dot added, or the text itself if it is empty. + */ + public static CharSequence addTo(@Nullable CharSequence text, @ColorInt int color) { + if (TextUtils.isEmpty(text)) { + return text; + } + + SpannableString spannable = new SpannableString(text); + final int end = spannable.length(); + final int start = Character.offsetByCodePoints(spannable, end, -1); + // The last character of right to left text is shown on the left. + final boolean rtl = TextDirectionHeuristics.FIRSTSTRONG_LTR.isRtl(spannable, 0, end); + spannable.setSpan(new NewIndicatorSpan(color, rtl), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + return spannable; + } + + @ColorInt + private final int color; + private final boolean rtl; + + private NewIndicatorSpan(@ColorInt int color, boolean rtl) { + this.color = color; + this.rtl = rtl; + } + + private static float dotWidth(Paint paint) { + final float textSize = paint.getTextSize(); + return textSize * (DOT_GAP_TEXT_SIZE_RATIO + 2 * DOT_RADIUS_TEXT_SIZE_RATIO); + } + + @Override + public int getSize(Paint paint, CharSequence text, int start, int end, + @Nullable Paint.FontMetricsInt fm) { + if (fm != null) { + paint.getFontMetricsInt(fm); + } + return Math.round(paint.measureText(text, start, end) + dotWidth(paint)); + } + + @Override + public void draw(Canvas canvas, CharSequence text, int start, int end, + float x, int top, int y, int bottom, Paint paint) { + final float textWidth = paint.measureText(text, start, end); + final float dotWidth = dotWidth(paint); + final float radius = paint.getTextSize() * DOT_RADIUS_TEXT_SIZE_RATIO; + + final float dotCenterX; + if (rtl) { + canvas.drawText(text, start, end, x + dotWidth, y, paint); + dotCenterX = x + radius; + } else { + canvas.drawText(text, start, end, x, y, paint); + dotCenterX = x + textWidth + dotWidth - radius; + } + // Vertically centered on the letters. + final float dotCenterY = y + (paint.ascent() + paint.descent()) / 2; + + final int originalColor = paint.getColor(); + final Paint.Style originalStyle = paint.getStyle(); + paint.setColor(color); + paint.setStyle(Paint.Style.FILL); + canvas.drawCircle(dotCenterX, dotCenterY, radius, paint); + paint.setColor(originalColor); + paint.setStyle(originalStyle); + } +} diff --git a/extensions/shared/library/src/main/java/app/morphe/extension/shared/settings/preference/SeekBarPreference.java b/extensions/shared/library/src/main/java/app/morphe/extension/shared/settings/preference/SeekBarPreference.java index fd637460fe..235f39920e 100644 --- a/extensions/shared/library/src/main/java/app/morphe/extension/shared/settings/preference/SeekBarPreference.java +++ b/extensions/shared/library/src/main/java/app/morphe/extension/shared/settings/preference/SeekBarPreference.java @@ -39,11 +39,13 @@ public class SeekBarPreference extends Preference { /** + * @param minLabel If not null, shown instead of the number when the value is the min, + * such as for a min value that means "off". * @param maxLabel If not null, shown instead of the number when the value is the max, * such as for a max value that means "no limit". */ - public record SeekBarConfig(IntegerSetting setting, int min, int max, int step, - String unit, int divisor, @Nullable StringRef maxLabel) { + public record SeekBarConfig(IntegerSetting setting, int min, int max, int step, String unit, + int divisor, @Nullable StringRef minLabel, @Nullable StringRef maxLabel) { public SeekBarConfig(IntegerSetting setting, int min, int max, int step, String unit) { this(setting, min, max, step, unit, 1); } @@ -51,6 +53,11 @@ public SeekBarConfig(IntegerSetting setting, int min, int max, int step, String public SeekBarConfig(IntegerSetting setting, int min, int max, int step, String unit, int divisor) { this(setting, min, max, step, unit, divisor, null); } + + public SeekBarConfig(IntegerSetting setting, int min, int max, int step, String unit, + int divisor, @Nullable StringRef maxLabel) { + this(setting, min, max, step, unit, divisor, null, maxLabel); + } } private static final Map REGISTRY = new HashMap<>(); @@ -194,7 +201,7 @@ public void onProgressChanged(SeekBar bar, int progress, boolean fromUser) { Pair dialogPair = CustomDialog.create( context, - getTitle(), + getTitle() != null ? getTitle().toString() : "", null, null, null, @@ -224,6 +231,9 @@ public static String formatLabel(int value, SeekBarConfig config) { } private static String formatValue(int value, SeekBarConfig config) { + if (value == config.min && config.minLabel != null) { + return config.minLabel.toString(); + } if (value == config.max && config.maxLabel != null) { return config.maxLabel.toString(); } diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/preference/CustomVideoSpeedListPreference.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/preference/CustomVideoSpeedListPreference.java index 1be97caeb4..21f9650032 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/preference/CustomVideoSpeedListPreference.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/preference/CustomVideoSpeedListPreference.java @@ -55,4 +55,8 @@ public CustomVideoSpeedListPreference(Context context) { super(context); } + @Override + public boolean tracksNewEntries() { + return false; // Entries are the custom speeds of the user. + } } diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/preference/SwipeVolumeStepsPreference.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/preference/SwipeVolumeStepsPreference.java index 1266ff260b..579dd9fdb8 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/preference/SwipeVolumeStepsPreference.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/preference/SwipeVolumeStepsPreference.java @@ -88,4 +88,9 @@ private void init() { setEntries(shownEntries.toArray(new CharSequence[0])); setEntryValues(shownValues.toArray(new CharSequence[0])); } + + @Override + public boolean tracksNewEntries() { + return false; // Entries depend on the volume levels of the device. + } } diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/preference/YouTubePreferenceFragment.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/preference/YouTubePreferenceFragment.java index 3918f21a5d..2aee95b58e 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/preference/YouTubePreferenceFragment.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/preference/YouTubePreferenceFragment.java @@ -17,6 +17,7 @@ import app.morphe.extension.shared.Logger; import app.morphe.extension.shared.patches.GmsCoreSupportPatch; import app.morphe.extension.shared.settings.SharedYouTubeSettings; +import app.morphe.extension.shared.settings.preference.NewSettingsIndicator; import app.morphe.extension.shared.settings.preference.ToolbarPreferenceFragment; import app.morphe.extension.youtube.patches.HideStatusBarPatch; import app.morphe.extension.youtube.settings.YouTubeActivityHook; @@ -54,6 +55,8 @@ protected void initialize() { SharedYouTubeSettings.CUSTOM_BRANDING_NOTIFICATION_ICON.key, SharedYouTubeSettings.CUSTOM_BRANDING_NAME.key); } + + NewSettingsIndicator.markNewPreferences(preferenceScreen); } catch (Exception ex) { Logger.printException(() -> "initialize failure", ex); } diff --git a/patches/src/main/kotlin/app/morphe/patches/music/misc/settings/SettingsPatch.kt b/patches/src/main/kotlin/app/morphe/patches/music/misc/settings/SettingsPatch.kt index 9cc0f9d78d..bd681f905c 100644 --- a/patches/src/main/kotlin/app/morphe/patches/music/misc/settings/SettingsPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/music/misc/settings/SettingsPatch.kt @@ -176,6 +176,11 @@ val settingsPatch = bytecodePatch( ) PreferenceScreen.MISC.addPreferences( + NonInteractivePreference( + key = "morphe_settings_new_indicator_days", + tag = "app.morphe.extension.shared.settings.preference.SeekBarPreference", + selectable = true + ), TextPreference( key = null, titleKey = "morphe_pref_import_export_title", diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/misc/settings/SettingsPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/settings/SettingsPatch.kt index b33c760520..1e6343d307 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/misc/settings/SettingsPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/settings/SettingsPatch.kt @@ -275,6 +275,11 @@ val settingsPatch = bytecodePatch( ) PreferenceScreen.MISC.addPreferences( + NonInteractivePreference( + key = "morphe_settings_new_indicator_days", + tag = "app.morphe.extension.shared.settings.preference.SeekBarPreference", + selectable = true + ), TextPreference( key = null, titleKey = "morphe_pref_import_export_title", diff --git a/patches/src/main/resources/addresources/values/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values/shared-youtube/strings.xml index 2940559ecd..506985d02c 100644 --- a/patches/src/main/resources/addresources/values/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values/shared-youtube/strings.xml @@ -24,6 +24,11 @@ Second \"item\" text" Monochrome Morphe Monochrome + + Highlight new settings + Number of days a dot is shown next to settings and options added by an update + Off + Restart to finish setting up From 0c5dc17e215ad63b748570adea5894553d489dd6 Mon Sep 17 00:00:00 2001 From: RAMiK <60112744+RAMiK4@users.noreply.github.com> Date: Thu, 29 Oct 2026 21:42:47 +0400 Subject: [PATCH 165/189] fix(YouTube - Download post images): Match posts with texts of many lines or a single emoji (#4167) --- .../patches/PostImageDownloadPatch.java | 63 +++++++----- .../originaltitles/OriginalDescription.java | 19 +--- .../RestoreOriginalTitlesPatch.java | 4 +- .../youtube/patches/utils/ProtoNode.java | 95 ++++++++++++++++--- 4 files changed, 129 insertions(+), 52 deletions(-) diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/PostImageDownloadPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/PostImageDownloadPatch.java index 16ec22e997..fc7eb986b8 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/PostImageDownloadPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/PostImageDownloadPatch.java @@ -209,11 +209,30 @@ private PostImageDownloadPatch() { // region Elements private static boolean isPostText(String text) { - return text.length() >= MIN_TEXT_LENGTH + return (text.length() >= MIN_TEXT_LENGTH || isEmojiText(text)) && !text.contains(".eml") && !text.startsWith("http"); } + /** + * @return If the text has only emojis or symbols, such as the text of a post that is a single emoji. + * Short texts with letters or numbers, such as counts, are shown by many posts. + */ + private static boolean isEmojiText(String text) { + boolean hasSymbol = false; + for (int i = 0, length = text.length(); i < length; ) { + final int codePoint = text.codePointAt(i); + if (Character.isLetterOrDigit(codePoint)) { + return false; + } + if (Character.getType(codePoint) == Character.OTHER_SYMBOL) { + hasSymbol = true; + } + i += Character.charCount(codePoint); + } + return hasSymbol; + } + private static String normalize(String text) { return WHITESPACE_PATTERN.matcher(text).replaceAll(" ").trim(); } @@ -287,12 +306,13 @@ public static byte[] onElementLoaded(byte[] bytes) { if (root == null) { return bytes; } - List textNodes = ProtoNode.textNodes(root); + // Post texts with line breaks are not parsed as text fields. + List texts = ProtoNode.texts(root); String identifier = null; - for (ProtoNode node : textNodes) { - if (node.getText().contains(".eml")) { - identifier = node.getText(); + for (String text : texts) { + if (text.contains(".eml")) { + identifier = text; break; } } @@ -302,16 +322,16 @@ public static byte[] onElementLoaded(byte[] bytes) { } if (!elementIdentifier.contains("shelf")) { - registerPost(textNodes, elementIdentifier, isImagePost(textNodes, elementIdentifier)); + registerPost(texts, elementIdentifier, isImagePost(texts, elementIdentifier)); return bytes; } // A shelf of many posts. Each post starts with the avatar of its channel. - List> posts = new ArrayList<>(); - List current = new ArrayList<>(); + List> posts = new ArrayList<>(); + List current = new ArrayList<>(); boolean currentHasImage = false; - for (ProtoNode node : textNodes) { - final UrlKind kind = getUrlKind(node.getText()); + for (String text : texts) { + final UrlKind kind = getUrlKind(text); if (kind == UrlKind.AVATAR && currentHasImage) { posts.add(current); current = new ArrayList<>(); @@ -319,13 +339,13 @@ public static byte[] onElementLoaded(byte[] bytes) { } else if (kind == UrlKind.IMAGE) { currentHasImage = true; } - current.add(node); + current.add(text); } if (currentHasImage) { posts.add(current); } Logger.printDebug(() -> "Post shelf " + elementIdentifier + " with " + posts.size() + " posts"); - for (List post : posts) { + for (List post : posts) { registerPost(post, elementIdentifier, isImagePost(post, null)); } } catch (Exception ex) { @@ -340,13 +360,12 @@ public static byte[] onElementLoaded(byte[] bytes) { * * @param identifier The identifier of the element of the post, or null for a post of a shelf. */ - private static boolean isImagePost(List textNodes, @Nullable String identifier) { + private static boolean isImagePost(List texts, @Nullable String identifier) { if (identifier != null && !identifier.startsWith("images_post")) { Logger.printDebug(() -> "Not an image post: " + identifier); return false; } - for (ProtoNode node : textNodes) { - String text = node.getText(); + for (String text : texts) { if (text.contains(".eml") && (text.contains("options_post") || text.contains("poll_post") || text.contains("videos_post") || text.contains("shared_post"))) { Logger.printDebug(() -> "Not an image post: " + text); @@ -365,11 +384,11 @@ private static boolean isImagePost(List textNodes, @Nullable String i * a poll, are mapped too, so its card is not matched to another post that shares some * of its texts. */ - private static void registerPost(List textNodes, String elementIdentifier, + private static void registerPost(List texts, String elementIdentifier, boolean isImagePost) { String key = NOT_IMAGE_POST; if (isImagePost) { - Map images = findImages(textNodes); + Map images = findImages(texts); if (images.isEmpty()) { return; } @@ -382,8 +401,8 @@ private static void registerPost(List textNodes, String elementIdenti } synchronized (textPostKeys) { - for (ProtoNode node : textNodes) { - String text = normalize(node.getText()); + for (String postText : texts) { + String text = normalize(postText); if (!isPostText(text)) { continue; } @@ -400,10 +419,10 @@ private static void registerPost(List textNodes, String elementIdenti /** * @return Base url -> image, in the order of the post. */ - private static Map findImages(List textNodes) { + private static Map findImages(List texts) { Map images = new LinkedHashMap<>(); - for (ProtoNode node : textNodes) { - Matcher matcher = IMAGE_URL_PATTERN.matcher(node.getText()); + for (String text : texts) { + Matcher matcher = IMAGE_URL_PATTERN.matcher(text); while (matcher.find()) { String url = matcher.group(); if (url.startsWith("//")) { diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/originaltitles/OriginalDescription.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/originaltitles/OriginalDescription.java index 788e464c89..a7bef654a3 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/originaltitles/OriginalDescription.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/originaltitles/OriginalDescription.java @@ -145,8 +145,8 @@ private static List findBodySegments(List message) { ProtoNode content = segmentText == null || segmentText.children == null ? null : ProtoNode.field(segmentText.children, TEXT_CONTENT_FIELD); - if (content != null && content.getVarint() == null) { - String decoded = decodeText(content); + if (content != null) { + String decoded = content.decodeText(); hasText |= decoded != null && !decoded.isEmpty(); } } @@ -166,21 +166,6 @@ private static List findBodySegments(List message) { return null; } - /** - * @return The field decoded as text, or null if the field is not a text, such as bytes of other data. - */ - @Nullable - static String decodeText(ProtoNode field) { - String decoded = field.decodeUtf8(); - for (int i = 0, length = decoded.length(); i < length; i++) { - final char c = decoded.charAt(i); - if (c == '\uFFFD' || (Character.isISOControl(c) && c != '\n' && c != '\r' && c != '\t')) { - return null; - } - } - return decoded; - } - /** * @return The original texts of the text segments, or null if an attachment * is not found in the original description. diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/originaltitles/RestoreOriginalTitlesPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/originaltitles/RestoreOriginalTitlesPatch.java index 71adbcaf99..3003f8e0bd 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/originaltitles/RestoreOriginalTitlesPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/originaltitles/RestoreOriginalTitlesPatch.java @@ -1239,10 +1239,10 @@ private static ProtoNode findShortsDescription(List message, @Nullabl description = findShortsDescription(field.children, description); continue; } - if (field.getFieldNumber() != OriginalDescription.TEXT_CONTENT_FIELD || field.getVarint() != null) { + if (field.getFieldNumber() != OriginalDescription.TEXT_CONTENT_FIELD) { continue; } - String text = OriginalDescription.decodeText(field); + String text = field.decodeText(); if (text != null && isLabelCandidate(text) && !ENTITY_KEY_PATTERN.matcher(text).matches() && (description == null || text.length() > description.decodeUtf8().length())) { description = field; diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/ProtoNode.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/ProtoNode.java index 3f511e79e4..fc1b5c9648 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/ProtoNode.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/ProtoNode.java @@ -24,6 +24,8 @@ import java.util.Arrays; import java.util.List; +import app.morphe.extension.shared.Logger; + /** * Field of a proto message parsed without a schema. *

    @@ -37,6 +39,12 @@ public final class ProtoNode { private static final int MAX_DEPTH = 64; + /** Decoders are not thread safe, and elements are parsed on many threads. */ + private static final ThreadLocal UTF_8_DECODER = ThreadLocal.withInitial(() -> + StandardCharsets.UTF_8.newDecoder() + .onMalformedInput(CodingErrorAction.REPORT) + .onUnmappableCharacter(CodingErrorAction.REPORT)); + @Nullable private final ProtoNode parent; private final int fieldNumber; @@ -82,15 +90,12 @@ private ProtoNode(@Nullable ProtoNode parent, int fieldNumber, byte[] bytes, */ @Nullable public static List parse(byte[] bytes) { - CharsetDecoder decoder = StandardCharsets.UTF_8.newDecoder() - .onMalformedInput(CodingErrorAction.REPORT) - .onUnmappableCharacter(CodingErrorAction.REPORT); - return parse(bytes, 0, bytes.length, null, 0, decoder); + return parse(bytes, 0, bytes.length, null, 0); } @Nullable private static List parse(byte[] bytes, int start, int end, @Nullable ProtoNode parent, - int depth, CharsetDecoder decoder) { + int depth) { if (depth > MAX_DEPTH || start >= end) { return null; } @@ -124,9 +129,9 @@ private static List parse(byte[] bytes, int start, int end, @Nullable ProtoNode node = new ProtoNode(parent, WireFormat.getTagFieldNumber(tag), bytes, tagStart, valueStart, valueEnd, lengthDelimited); if (lengthDelimited) { - node.text = decodeText(bytes, valueStart, valueEnd, decoder); + node.text = decodeText(bytes, valueStart, valueEnd, false); if (node.text == null) { - node.children = parse(bytes, valueStart, valueEnd, node, depth + 1, decoder); + node.children = parse(bytes, valueStart, valueEnd, node, depth + 1); } } nodes.add(node); @@ -139,10 +144,12 @@ private static List parse(byte[] bytes, int start, int end, @Nullable } /** + * @param allowLineBreaks If line breaks and tabs are allowed. Fields with line breaks are + * not parsed as text, because the tag of field 1 of a message is a line break. * @return The text, or null if the bytes are not printable UTF-8 text. */ @Nullable - private static String decodeText(byte[] bytes, int start, int end, CharsetDecoder decoder) { + private static String decodeText(byte[] bytes, int start, int end, boolean allowLineBreaks) { if (start >= end) { return null; } @@ -150,24 +157,31 @@ private static String decodeText(byte[] bytes, int start, int end, CharsetDecode // Fast check for control characters, which messages usually start with. for (int i = start; i < end; i++) { final byte b = bytes[i]; - if ((b >= 0 && b < 0x20) || b == 0x7F) { + if (((b >= 0 && b < 0x20) || b == 0x7F) && !(allowLineBreaks && isLineBreakOrTab(b))) { return null; } } try { - String text = decoder.decode(ByteBuffer.wrap(bytes, start, end - start)).toString(); + String text = UTF_8_DECODER.get().decode(ByteBuffer.wrap(bytes, start, end - start)).toString(); for (int i = 0, length = text.length(); i < length; i++) { - if (Character.isISOControl(text.charAt(i))) { + final char c = text.charAt(i); + if (Character.isISOControl(c) && !(allowLineBreaks && isLineBreakOrTab(c))) { return null; } } return text; } catch (CharacterCodingException ex) { + Logger.printDebug(() -> "Could not decode bytes: " + + Arrays.toString(Arrays.copyOfRange(bytes, start, end)), ex); return null; } } + private static boolean isLineBreakOrTab(int c) { + return c == '\n' || c == '\r' || c == '\t'; + } + public int getFieldNumber() { return fieldNumber; } @@ -200,6 +214,39 @@ public String decodeUtf8() { : new String(bytes, valueStart, valueEnd - valueStart, StandardCharsets.UTF_8); } + /** + * @return The text of a length delimited field, including text with line breaks that is not + * parsed as text, or null if the payload is not printable UTF-8 text, such as other data. + */ + @Nullable + public String decodeText() { + if (text != null) { + return text; + } + if (!lengthDelimited) { + return null; + } + return payload != null + ? decodeText(payload, 0, payload.length, true) + : decodeText(bytes, valueStart, valueEnd, true); + } + + /** + * @return The text of a field with line breaks, such as a post text, which is not parsed + * as text, or null if the field is not a text with line breaks. + */ + @Nullable + private String getMultiLineText() { + if (text != null) { + return null; + } + String decoded = decodeText(); + // A message starts with the tag of its first field, which can be a line break. + return decoded != null && !isLineBreakOrTab(decoded.charAt(0)) && decoded.indexOf('\n') > 0 + ? decoded + : null; + } + public void setText(String text) { this.text = text; children = null; @@ -324,6 +371,32 @@ private static void write(List message, CodedOutputStream output) thr } } + /** + * @return All texts of the message in order, including the texts with line breaks, + * which are not parsed as text fields. + */ + public static List texts(List message) { + List texts = new ArrayList<>(); + addTexts(message, texts); + return texts; + } + + private static void addTexts(List message, List texts) { + for (ProtoNode node : message) { + if (node.text != null) { + texts.add(node.text); + continue; + } + if (node.children != null) { + addTexts(node.children, texts); + } + String multiLineText = node.getMultiLineText(); + if (multiLineText != null) { + texts.add(multiLineText); + } + } + } + /** * @return All text fields of the message, in order. */ From e003b1a7f9b005422b150086472c3f66be9e9482 Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Thu, 29 Oct 2026 18:28:15 +0000 Subject: [PATCH 166/189] chore: Update translations from Crowdin --- .../values-af-rZA/shared-youtube/strings.xml | 4 +- .../values-af-rZA/youtube/strings.xml | 6 + .../values-am-rET/music/strings.xml | 2 - .../values-am-rET/shared-youtube/strings.xml | 3 +- .../values-am-rET/youtube/strings.xml | 5 +- .../values-ar-rSA/shared-youtube/strings.xml | 4 +- .../values-ar-rSA/sponsorblock/strings.xml | 2 + .../values-ar-rSA/youtube/strings.xml | 8 + .../values-as-rIN/music/strings.xml | 2 - .../values-as-rIN/shared-youtube/strings.xml | 2 + .../values-as-rIN/youtube/strings.xml | 9 + .../values-az-rAZ/shared-youtube/strings.xml | 8 +- .../values-az-rAZ/sponsorblock/strings.xml | 2 + .../values-az-rAZ/youtube/strings.xml | 8 + .../values-be-rBY/music/strings.xml | 2 - .../values-be-rBY/shared-youtube/strings.xml | 26 + .../values-be-rBY/youtube/strings.xml | 61 + .../values-bg-rBG/shared-youtube/strings.xml | 3 +- .../values-bg-rBG/youtube/strings.xml | 6 + .../values-bn-rBD/shared-youtube/strings.xml | 3 +- .../values-bn-rBD/youtube/strings.xml | 4 + .../values-bs-rBA/music/strings.xml | 611 +++++++ .../values-bs-rBA/reddit/strings.xml | 114 ++ .../values-bs-rBA/shared-youtube/strings.xml | 248 +++ .../values-bs-rBA/shared/strings.xml | 196 +++ .../values-bs-rBA/sponsorblock/strings.xml | 208 +++ .../values-bs-rBA/youtube/strings.xml | 1412 ++++++++++++++++ .../values-ca-rES/music/strings.xml | 602 +++++++ .../values-ca-rES/reddit/strings.xml | 113 ++ .../values-ca-rES/shared-youtube/strings.xml | 249 +++ .../values-ca-rES/shared/strings.xml | 194 +++ .../values-ca-rES/sponsorblock/strings.xml | 212 +++ .../values-ca-rES/youtube/strings.xml | 1402 ++++++++++++++++ .../values-ckb-rIR/music/strings.xml | 529 ++++++ .../values-ckb-rIR/reddit/strings.xml | 12 + .../values-ckb-rIR/shared-youtube/strings.xml | 169 ++ .../values-ckb-rIR/shared/strings.xml | 111 ++ .../values-ckb-rIR/youtube/strings.xml | 1099 +++++++++++++ .../values-cs-rCZ/shared-youtube/strings.xml | 3 +- .../values-cs-rCZ/youtube/strings.xml | 6 + .../values-da-rDK/shared-youtube/strings.xml | 3 +- .../values-da-rDK/youtube/strings.xml | 6 + .../values-de-rDE/shared-youtube/strings.xml | 3 +- .../values-de-rDE/youtube/strings.xml | 6 + .../values-el-rGR/shared-youtube/strings.xml | 4 +- .../values-el-rGR/youtube/strings.xml | 6 + .../values-es-rES/shared-youtube/strings.xml | 6 +- .../values-es-rES/sponsorblock/strings.xml | 2 + .../values-es-rES/youtube/strings.xml | 4 + .../values-et-rEE/music/strings.xml | 617 +++++++ .../values-et-rEE/reddit/strings.xml | 121 +- .../values-et-rEE/shared-youtube/strings.xml | 251 +++ .../values-et-rEE/shared/strings.xml | 198 +++ .../values-et-rEE/sponsorblock/strings.xml | 212 +++ .../values-et-rEE/youtube/strings.xml | 1425 ++++++++++++++++ .../values-eu-rES/music/strings.xml | 4 +- .../values-eu-rES/shared-youtube/strings.xml | 12 + .../values-eu-rES/shared/strings.xml | 15 + .../values-eu-rES/youtube/strings.xml | 40 +- .../values-fa-rIR/shared-youtube/strings.xml | 4 +- .../values-fa-rIR/youtube/strings.xml | 4 + .../values-fi-rFI/shared-youtube/strings.xml | 3 +- .../values-fi-rFI/youtube/strings.xml | 6 + .../values-fil-rPH/shared-youtube/strings.xml | 4 +- .../values-fil-rPH/youtube/strings.xml | 4 + .../values-fr-rFR/shared-youtube/strings.xml | 6 +- .../values-fr-rFR/sponsorblock/strings.xml | 2 + .../values-fr-rFR/youtube/strings.xml | 8 + .../values-ga-rIE/music/strings.xml | 52 +- .../values-ga-rIE/shared-youtube/strings.xml | 2 + .../values-ga-rIE/youtube/strings.xml | 49 +- .../values-gl-rES/music/strings.xml | 581 +++++++ .../values-gl-rES/reddit/strings.xml | 88 + .../values-gl-rES/shared-youtube/strings.xml | 247 +++ .../values-gl-rES/shared/strings.xml | 137 ++ .../values-gl-rES/sponsorblock/strings.xml | 211 +++ .../values-gl-rES/youtube/strings.xml | 1280 +++++++++++++++ .../values-gu-rIN/music/strings.xml | 4 - .../values-gu-rIN/reddit/strings.xml | 6 +- .../values-gu-rIN/shared-youtube/strings.xml | 4 +- .../values-gu-rIN/shared/strings.xml | 1 - .../values-gu-rIN/youtube/strings.xml | 9 +- .../values-hi-rIN/shared-youtube/strings.xml | 4 +- .../values-hi-rIN/youtube/strings.xml | 4 + .../values-hr-rHR/shared-youtube/strings.xml | 4 +- .../values-hr-rHR/youtube/strings.xml | 5 +- .../values-hu-rHU/shared-youtube/strings.xml | 4 +- .../values-hu-rHU/youtube/strings.xml | 6 + .../values-hy-rAM/music/strings.xml | 4 +- .../values-hy-rAM/shared-youtube/strings.xml | 10 + .../values-hy-rAM/sponsorblock/strings.xml | 5 + .../values-hy-rAM/youtube/strings.xml | 22 +- .../values-in-rID/music/strings.xml | 8 +- .../values-in-rID/reddit/strings.xml | 6 +- .../values-in-rID/shared-youtube/strings.xml | 4 +- .../values-in-rID/youtube/strings.xml | 22 +- .../values-is-rIS/shared-youtube/strings.xml | 3 +- .../values-is-rIS/youtube/strings.xml | 4 + .../values-it-rIT/shared-youtube/strings.xml | 6 +- .../values-it-rIT/sponsorblock/strings.xml | 2 + .../values-it-rIT/youtube/strings.xml | 8 + .../values-iw-rIL/shared-youtube/strings.xml | 4 +- .../values-iw-rIL/youtube/strings.xml | 6 + .../values-ja-rJP/music/strings.xml | 16 +- .../values-ja-rJP/shared-youtube/strings.xml | 4 +- .../values-ja-rJP/youtube/strings.xml | 79 +- .../values-ka-rGE/music/strings.xml | 616 +++++++ .../values-ka-rGE/reddit/strings.xml | 116 ++ .../values-ka-rGE/shared-youtube/strings.xml | 246 +++ .../values-ka-rGE/shared/strings.xml | 197 +++ .../values-ka-rGE/sponsorblock/strings.xml | 210 +++ .../values-ka-rGE/youtube/strings.xml | 1421 ++++++++++++++++ .../values-kk-rKZ/music/strings.xml | 615 +++++++ .../values-kk-rKZ/reddit/strings.xml | 15 +- .../values-kk-rKZ/shared-youtube/strings.xml | 248 +++ .../values-kk-rKZ/shared/strings.xml | 197 +++ .../values-kk-rKZ/sponsorblock/strings.xml | 2 + .../values-kk-rKZ/youtube/strings.xml | 1425 ++++++++++++++++ .../values-km-rKH/music/strings.xml | 16 - .../values-km-rKH/reddit/strings.xml | 10 +- .../values-km-rKH/shared-youtube/strings.xml | 13 +- .../values-km-rKH/shared/strings.xml | 32 - .../values-km-rKH/sponsorblock/strings.xml | 1 - .../values-km-rKH/youtube/strings.xml | 33 +- .../values-kmr-rTR/music/strings.xml | 400 +++++ .../values-kmr-rTR/shared-youtube/strings.xml | 82 + .../values-kmr-rTR/shared/strings.xml | 80 + .../values-kmr-rTR/youtube/strings.xml | 538 +++++++ .../values-kn-rIN/reddit/strings.xml | 4 +- .../values-kn-rIN/shared-youtube/strings.xml | 5 +- .../values-kn-rIN/shared/strings.xml | 1 - .../values-kn-rIN/sponsorblock/strings.xml | 1 - .../values-kn-rIN/youtube/strings.xml | 5 +- .../values-ko-rKR/music/strings.xml | 30 +- .../values-ko-rKR/reddit/strings.xml | 2 +- .../values-ko-rKR/shared-youtube/strings.xml | 8 +- .../values-ko-rKR/sponsorblock/strings.xml | 2 + .../values-ko-rKR/youtube/strings.xml | 46 +- .../values-ky-rKG/music/strings.xml | 611 +++++++ .../values-ky-rKG/reddit/strings.xml | 12 + .../values-ky-rKG/shared-youtube/strings.xml | 247 +++ .../values-ky-rKG/shared/strings.xml | 194 +++ .../values-ky-rKG/sponsorblock/strings.xml | 2 + .../values-ky-rKG/youtube/strings.xml | 1396 ++++++++++++++++ .../values-lo-rLA/music/strings.xml | 617 +++++++ .../values-lo-rLA/reddit/strings.xml | 15 +- .../values-lo-rLA/shared-youtube/strings.xml | 251 +++ .../values-lo-rLA/shared/strings.xml | 198 +++ .../values-lo-rLA/sponsorblock/strings.xml | 2 + .../values-lo-rLA/youtube/strings.xml | 1427 +++++++++++++++++ .../values-lt-rLT/shared-youtube/strings.xml | 4 +- .../values-lt-rLT/youtube/strings.xml | 6 + .../values-lv-rLV/music/strings.xml | 601 +++++++ .../values-lv-rLV/reddit/strings.xml | 12 + .../values-lv-rLV/shared-youtube/strings.xml | 246 +++ .../values-lv-rLV/shared/strings.xml | 194 +++ .../values-lv-rLV/sponsorblock/strings.xml | 2 + .../values-lv-rLV/youtube/strings.xml | 1374 ++++++++++++++++ .../values-mai-rIN/music/strings.xml | 117 -- .../values-mai-rIN/reddit/strings.xml | 29 +- .../values-mai-rIN/shared-youtube/strings.xml | 56 +- .../values-mai-rIN/shared/strings.xml | 99 -- .../values-mai-rIN/sponsorblock/strings.xml | 201 --- .../values-mai-rIN/youtube/strings.xml | 312 +--- .../values-mk-rMK/music/strings.xml | 542 +++++++ .../values-mk-rMK/reddit/strings.xml | 12 + .../values-mk-rMK/shared-youtube/strings.xml | 178 ++ .../values-mk-rMK/shared/strings.xml | 83 + .../values-mk-rMK/sponsorblock/strings.xml | 2 + .../values-mk-rMK/youtube/strings.xml | 843 ++++++++++ .../values-mn-rMN/music/strings.xml | 616 +++++++ .../values-mn-rMN/reddit/strings.xml | 15 +- .../values-mn-rMN/shared-youtube/strings.xml | 252 +++ .../values-mn-rMN/shared/strings.xml | 198 +++ .../values-mn-rMN/sponsorblock/strings.xml | 2 + .../values-mn-rMN/youtube/strings.xml | 1424 ++++++++++++++++ .../values-mr-rIN/music/strings.xml | 407 +++++ .../values-mr-rIN/shared-youtube/strings.xml | 99 ++ .../values-mr-rIN/shared/strings.xml | 86 + .../values-mr-rIN/youtube/strings.xml | 605 +++++++ .../values-ms-rMY/music/strings.xml | 533 ++++++ .../values-ms-rMY/reddit/strings.xml | 12 + .../values-ms-rMY/shared-youtube/strings.xml | 233 +++ .../values-ms-rMY/shared/strings.xml | 188 +++ .../values-ms-rMY/sponsorblock/strings.xml | 2 + .../values-ms-rMY/youtube/strings.xml | 1287 +++++++++++++++ .../values-my-rMM/shared-youtube/strings.xml | 1 + .../values-my-rMM/shared/strings.xml | 1 - .../values-my-rMM/youtube/strings.xml | 1 + .../values-nb-rNO/shared-youtube/strings.xml | 3 +- .../values-nb-rNO/youtube/strings.xml | 4 + .../values-ne-rIN/shared-youtube/strings.xml | 1 + .../values-ne-rIN/youtube/strings.xml | 1 + .../values-nl-rNL/shared-youtube/strings.xml | 4 +- .../values-nl-rNL/youtube/strings.xml | 6 + .../values-or-rIN/shared-youtube/strings.xml | 1 + .../values-or-rIN/youtube/strings.xml | 1 + .../values-pa-rIN/shared-youtube/strings.xml | 1 + .../values-pa-rIN/youtube/strings.xml | 1 + .../values-pl-rPL/music/strings.xml | 4 +- .../values-pl-rPL/shared-youtube/strings.xml | 6 +- .../values-pl-rPL/youtube/strings.xml | 64 +- .../values-pt-rBR/shared-youtube/strings.xml | 4 +- .../values-pt-rBR/youtube/strings.xml | 4 + .../values-pt-rPT/shared-youtube/strings.xml | 3 +- .../values-pt-rPT/youtube/strings.xml | 4 + .../values-ro-rRO/shared-youtube/strings.xml | 4 +- .../values-ro-rRO/youtube/strings.xml | 4 + .../values-ru-rRU/shared-youtube/strings.xml | 6 +- .../values-ru-rRU/youtube/strings.xml | 9 +- .../values-si-rLK/music/strings.xml | 11 - .../values-si-rLK/shared-youtube/strings.xml | 2 +- .../values-si-rLK/shared/strings.xml | 2 - .../values-si-rLK/youtube/strings.xml | 8 +- .../values-sk-rSK/shared-youtube/strings.xml | 4 +- .../values-sk-rSK/youtube/strings.xml | 4 + .../values-sl-rSI/shared-youtube/strings.xml | 3 +- .../values-sl-rSI/youtube/strings.xml | 4 + .../values-sr-rCS/shared-youtube/strings.xml | 4 +- .../values-sr-rCS/youtube/strings.xml | 4 + .../values-sr-rSP/shared-youtube/strings.xml | 3 +- .../values-sr-rSP/youtube/strings.xml | 4 + .../values-sv-rSE/shared-youtube/strings.xml | 3 +- .../values-sv-rSE/youtube/strings.xml | 4 + .../values-sw-rKE/music/strings.xml | 616 +++++++ .../values-sw-rKE/reddit/strings.xml | 15 +- .../values-sw-rKE/shared-youtube/strings.xml | 251 +++ .../values-sw-rKE/shared/strings.xml | 197 +++ .../values-sw-rKE/sponsorblock/strings.xml | 2 + .../values-sw-rKE/youtube/strings.xml | 1426 ++++++++++++++++ .../values-ta-rIN/music/strings.xml | 4 - .../values-ta-rIN/reddit/strings.xml | 5 +- .../values-ta-rIN/shared-youtube/strings.xml | 2 +- .../values-ta-rIN/shared/strings.xml | 1 - .../values-ta-rIN/sponsorblock/strings.xml | 1 - .../values-ta-rIN/youtube/strings.xml | 14 +- .../values-te-rIN/music/strings.xml | 7 - .../values-te-rIN/reddit/strings.xml | 4 +- .../values-te-rIN/shared-youtube/strings.xml | 2 +- .../values-te-rIN/shared/strings.xml | 4 - .../values-te-rIN/youtube/strings.xml | 17 +- .../values-th-rTH/shared-youtube/strings.xml | 4 +- .../values-th-rTH/youtube/strings.xml | 4 + .../values-tr-rTR/shared-youtube/strings.xml | 10 +- .../values-tr-rTR/sponsorblock/strings.xml | 2 + .../values-tr-rTR/youtube/strings.xml | 8 + .../values-uk-rUA/shared-youtube/strings.xml | 6 +- .../values-uk-rUA/youtube/strings.xml | 12 +- .../values-ur-rIN/reddit/strings.xml | 5 +- .../values-ur-rIN/shared-youtube/strings.xml | 1 + .../values-ur-rIN/shared/strings.xml | 71 - .../values-ur-rIN/youtube/strings.xml | 4 +- .../values-uz-rUZ/music/strings.xml | 394 +++++ .../values-uz-rUZ/shared-youtube/strings.xml | 82 + .../values-uz-rUZ/shared/strings.xml | 37 + .../values-uz-rUZ/youtube/strings.xml | 532 ++++++ .../values-vi-rVN/shared-youtube/strings.xml | 4 +- .../values-vi-rVN/youtube/strings.xml | 4 + .../values-zh-rCN/shared-youtube/strings.xml | 3 +- .../values-zh-rCN/youtube/strings.xml | 4 + .../values-zh-rTW/shared-youtube/strings.xml | 4 +- .../values-zh-rTW/youtube/strings.xml | 4 + .../values-zu-rZA/music/strings.xml | 2 - .../values-zu-rZA/shared-youtube/strings.xml | 2 + .../values-zu-rZA/youtube/strings.xml | 44 +- 265 files changed, 38630 insertions(+), 1387 deletions(-) diff --git a/patches/src/main/resources/addresources/values-af-rZA/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-af-rZA/shared-youtube/strings.xml index 4d1f963860..62a06c4402 100644 --- a/patches/src/main/resources/addresources/values-af-rZA/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-af-rZA/shared-youtube/strings.xml @@ -21,9 +21,9 @@ Second \"item\" text" Opstart-animasie Deaktiveer Kleur - Kleur Morphe Monochroom - Monochroom Morphe + + Af Herbegin om die opstelling te voltooi diff --git a/patches/src/main/resources/addresources/values-af-rZA/youtube/strings.xml b/patches/src/main/resources/addresources/values-af-rZA/youtube/strings.xml index 59042218cb..f738319c38 100644 --- a/patches/src/main/resources/addresources/values-af-rZA/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-af-rZA/youtube/strings.xml @@ -113,6 +113,9 @@ Veranderinge sluit in: Verberg noodbokse Verberg opnames en aansporings oor voer-aanbevelings akkuraatheid Verberg mediese panele + + Steek \'Onbeskikbare video\'s is versteek\' banier weg + Steek die \'Onbeskikbare video\'s is versteek\' banier in speellyste weg Verberg opylaaibody "Beperkings: @@ -1249,6 +1252,9 @@ Beperking: Indien 'Herstel ou video-aksiebalk' afgeskakel is, mag die 'Meer vide Hou die uitleg van die statusbalk konsekwent, sodat dit nie die boonste barkies bedek en geen spasie bo die speler los nie. Skakel dit af as die app ineenstort of die boonste barkies verkeerd lyk Gloei + Morphe Gloei 1 + Morphe Gloei 2 + Glow-spatskerm word nie deur hierdie toestel ondersteun nie Ligte tema-agtergrond Wit Jou Materiaal (Wit) diff --git a/patches/src/main/resources/addresources/values-am-rET/music/strings.xml b/patches/src/main/resources/addresources/values-am-rET/music/strings.xml index 2b85e97486..53d9d6d864 100644 --- a/patches/src/main/resources/addresources/values-am-rET/music/strings.xml +++ b/patches/src/main/resources/addresources/values-am-rET/music/strings.xml @@ -545,8 +545,6 @@ SABR የመጫወቻ ሂደት (የቪዲዮ ስትሪሞች ማስመሰል ተ ተቀምጧል የተቀናጁ ቃላትን ወይም የድምፅ ክፍሎችን አድምቀው ያሳዩ ቃላት በቃላት የሚገለጹ ግጥሞች በሚኖሩበት ጊዜ ቅድሚያ ይስጡ። ግጥሞቹ የቃላት ጊዜያትን የሚይዙ ከሆነ፣ እያንዳንዱን ቃል ወይም የድምፅ ክፍል ከሙዚቃው ጋር በተናበበ ጊዜ ያደምቃሉ - የጀርባ ድምጾችን ለየብቻ አሳይ - በቅንፍ ውስጥ የተጻፉ የጀርባ ድምጾችን፣ ለምሳሌ \"(Oh) I want you\"፣ ከሚዘመርበት መስመር አውጥቶ ወደ ራሱ ደብዘዝ ያለ መስመር ያሸጋግረዋል የተጫወቱ ግጥሞችን ደብቁ በአሁኑ ጊዜ እየተጫወተ ካለው መስመር በላይ ያሉትን የግጥም መስመሮች ደብቁ ያልተጫወቱ ግጥሞችን ደብቁ diff --git a/patches/src/main/resources/addresources/values-am-rET/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-am-rET/shared-youtube/strings.xml index a851c60154..40447a5b9b 100644 --- a/patches/src/main/resources/addresources/values-am-rET/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-am-rET/shared-youtube/strings.xml @@ -21,9 +21,8 @@ Second \"item\" text" የመጀመሪያ ማስጀመሪያ አኒሜሽን ተዘግቷል ቀለም - የቀለም Morphe ሞኖክሮም - ሞኖክሮም Morphe + ቅንብሩን ለማጠናቀቅ እንደገና ያስነሱ diff --git a/patches/src/main/resources/addresources/values-am-rET/youtube/strings.xml b/patches/src/main/resources/addresources/values-am-rET/youtube/strings.xml index e2e5093c08..97a1dedb2e 100644 --- a/patches/src/main/resources/addresources/values-am-rET/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-am-rET/youtube/strings.xml @@ -113,6 +113,9 @@ Second \"item\" text" የዳሰሳ ጥናቶችን ደብቅ ስለ ፊድ የጥቆማ ትክክለኛነት የሚጠይቁ ጥናቶችን እና ማሳሰቢያዎችን ደብቅ የትኬት መደርደሪያውን ደብቅ + + \'ያልተገኙ ቪዲዮዎች ተደብቀዋል\' የሚለውን ባነር ደብቅ + በየመጫወቻ ዝርዝሮች ውስጥ \'ያልተገኙ ቪዲዮዎች ተደብቀዋል\' የሚለውን ባነር ይደብቃል የተጫነበትን ጊዜ ደብቅ "ገደቦች፦ @@ -1249,6 +1252,7 @@ OpenRouter.aiን ለመክፈት እዚህ ይጫኑ" የሁኔታ ማሳወቂያ ስትሪክ አቀማመጥ ወጥ እንዲሆን በማድረግ የላይኞቹን ባሮች እንዳይሸፍን እና ከቪዲዮ ማጫወቻው በላይ ምንም ባዶ ቦታ እንዳይኖር ያደርጋል። መተግበሪያው ካቆመ ወይም የላይኞቹ ባሮች የተሳሳተ አቀማመጥ ካሳዩ ይህንን ያጥፉት ብልጭታ + ይህ መሣሪያ የGlow ስፕላሽ ስክሪንን አይደግፍም የቀላል ጭብጥ ዳራ ነጭ Material You (ነጭ) @@ -1566,7 +1570,6 @@ AVC (H.264) ክፍሎች እንደተመለከቱ ተሰምረዋል ቀይር አግብ - {one, 1 አዲስ ክፍል, other, {count} አዳዲስ ክፍሎች} %1$s አዳዲስ ክፍሎች በዚህ መሳሪያ ላይ ሂደትን መዝግብ "የተከታተሏቸው ተከታታይ ክፍሎች እና የደረሱበት ደረጃ በዚህ መሳሪያ ላይ ይቀመራሉ፣ እንዲሁም በYouTube አካውንቶች መካከል ይጋራሉ። የYouTube ታሪክ ቁጥጥሮች እነዚህን አይለውጡም። የደረሱበትን ደረጃ ከተከታታይ ክፍሎች ማውጫ ውስጥ ያጽዱ diff --git a/patches/src/main/resources/addresources/values-ar-rSA/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-ar-rSA/shared-youtube/strings.xml index e18564bec8..deea54e52d 100644 --- a/patches/src/main/resources/addresources/values-ar-rSA/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ar-rSA/shared-youtube/strings.xml @@ -21,9 +21,11 @@ Second \"item\" text" تأثير حركة بدء التشغيل معطّل اللون - تلوين Morphe + لون Morphe أحادي اللون Morphe أحادي اللون + + متوقف إعادة التشغيل لإنهاء الإعداد diff --git a/patches/src/main/resources/addresources/values-ar-rSA/sponsorblock/strings.xml b/patches/src/main/resources/addresources/values-ar-rSA/sponsorblock/strings.xml index abd9c1b6c3..7586530e5c 100644 --- a/patches/src/main/resources/addresources/values-ar-rSA/sponsorblock/strings.xml +++ b/patches/src/main/resources/addresources/values-ar-rSA/sponsorblock/strings.xml @@ -58,6 +58,8 @@ Second \"item\" text" تمت قراءتها اعرضها لي عام + حفظ مقاطع من الفيديوهات التي تم تنزيلها + يتيح تخطي أجزاء من الفيديوهات التي تم تنزيلها أثناء العمل دون اتصال بالإنترنت عرض تنبيهات خطأ الاتصال يعرض إشعارًا عند تعذر توفر خادم SponsorBlock تمكين تتبع مرات التخطي diff --git a/patches/src/main/resources/addresources/values-ar-rSA/youtube/strings.xml b/patches/src/main/resources/addresources/values-ar-rSA/youtube/strings.xml index e7dfbb45d3..be31ec16de 100644 --- a/patches/src/main/resources/addresources/values-ar-rSA/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ar-rSA/youtube/strings.xml @@ -113,6 +113,9 @@ Second \"item\" text" إخفاء الاستطلاعات إخفاء استطلاعات الرأي والتنبيهات حول دقّة توصية الخلاصة إخفاء رف التذاكر + + إخفاء لافتة \'يتم إخفاء الفيديوهات غير المتوفّرة\' + يخفي لافتة \'يتم إخفاء الفيديوهات غير المتوفّرة\' في قوائم التشغيل إخفاء وقت التحميل "القيود: @@ -1212,6 +1215,8 @@ Mistral Nemo هو الخيار الأرخص، بينما توفر Gemma 4 أعل يعطّل مشغل \'متابعة المشاهدة\' المصغر، وهي ميزة YouTube Premium تظهر عند بدء تشغيل التطبيق المشغل المصغر تخصيص مظهر ونمط مشغل الفيديو المصغر + مشغلات مصغرة إضافية + أزال YouTube كل مشغلات الفيديو المصغرة الإضافية. لاستعادة المزيد من أنواع المشغلات الصغيرة، يُرجى تعديل YouTube إصدار 21.28.208 أو أقدم نوع المشغل المصغر إخفاء العنوان والقناة معطّل @@ -1253,6 +1258,9 @@ Mistral Nemo هو الخيار الأرخص، بينما توفر Gemma 4 أعل يحافظ على تخطيط شريط الحالة بشكل متسق، بحيث لا يغطي الأشرطة العلوية ولا يترك مساحة فارغة فوق المشغل. قم بإيقاف تشغيل هذا إذا تعطل التطبيق أو ظهرت الأشرطة العلوية بشكل خاطئ توهج + Morphe Glow 1 + Morphe Glow 2 + شاشة البدء المتوهجة غير مدعومة في هذا الجهاز خلفية المظهر الفاتح أبيض Material You (أبيض) diff --git a/patches/src/main/resources/addresources/values-as-rIN/music/strings.xml b/patches/src/main/resources/addresources/values-as-rIN/music/strings.xml index 6021bb398e..6fca42d6b7 100644 --- a/patches/src/main/resources/addresources/values-as-rIN/music/strings.xml +++ b/patches/src/main/resources/addresources/values-as-rIN/music/strings.xml @@ -545,8 +545,6 @@ SABR প্লেবেক (স্পুফ ভিডিঅ’ ষ্ট্ৰ সংৰক্ষিত চিঙ্ৰোনাইজ কৰা শব্দ বা শব্দাংশসমূহ হাইলাইট কৰক উপলব্ধ থাকিলে শব্দ-অনুসৰি লৰিক্সক অগ্ৰাধিকাৰ দিয়ক। যেতিয়া লৰিক্সত শব্দৰ সময় থাকে, ই সংগীতৰ সৈতে মিল খাই প্ৰতিটো শব্দ বা শব্দাংশ হাইলাইট কৰে - বেকগ্ৰাউণ্ড ভ’কালবোৰ পৃথককৈ দেখুৱাওক - ব্ৰেকেটত লিখা বেকগ্ৰাউণ্ড ভ’কাল, যেনে \"(Oh) I want you\", ইয়াক গানৰ লাইনটোৰ পৰা আঁতৰাই এটা পৃথক ম্লান লাইনলৈ স্থানান্তৰ কৰে বাজি উঠা লৰিক্সসমূহ লুকুৱাওক বৰ্তমান বাজি থকা লাইনটোৰ ওপৰৰ লৰিক্সৰ লাইনসমূহ লুকুৱাওক নাবাজি উঠা লৰিক্সসমূহ লুকুৱাওক diff --git a/patches/src/main/resources/addresources/values-as-rIN/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-as-rIN/shared-youtube/strings.xml index f44d966cb0..0febfa4b77 100644 --- a/patches/src/main/resources/addresources/values-as-rIN/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-as-rIN/shared-youtube/strings.xml @@ -21,6 +21,8 @@ Second \"item\" text" ষ্টাৰ্টআপ এনিমেশ্বন নিষ্ক্ৰীয় ৰং + মোন’ক্ৰোম + ছেটিং আপ শেষ কৰিবলৈ পুনৰ আৰম্ভ কৰক diff --git a/patches/src/main/resources/addresources/values-as-rIN/youtube/strings.xml b/patches/src/main/resources/addresources/values-as-rIN/youtube/strings.xml index 16d628a4a5..1feedab53e 100644 --- a/patches/src/main/resources/addresources/values-as-rIN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-as-rIN/youtube/strings.xml @@ -113,6 +113,9 @@ Second \"item\" text" জৰীপসমূহ লুকুৱাওক ফীডৰ পৰামৰ্শৰ সঠিকতাৰ বিষয়ে সমীক্ষা আৰু নজৰ কাঢ়ি লোৱা বাৰ্তাসমূহ লুকুৱাওক টিকট শ্বেল্ফটো লুকুৱাওক + + \'অনুপলব্ধ ভিডিঅ\'সমূহ লুকুৱাই থোৱা হৈছে\' বেনাৰটো লুকুৱাওক + প্লেলিষ্টত \'অনুপলব্ধ ভিডিঅ\'সমূহ লুকুৱাই থোৱা হৈছে\' বেনাৰটো লুকুৱায় আপলোড কৰা সময় লুকুৱাওক "সীমাবদ্ধতা: @@ -1248,6 +1251,8 @@ Settings → Playback (বা Autoplay) → Autoplay next video" ষ্টেটাছ বাৰে টপ বাৰসমূহ ঢাকি থকা সমস্যাটো সমাধান কৰক ষ্টেটাছ বাৰৰ লেআউটো সংগত কৰি ৰাখে, যাতে ই টপ বাৰসমূহক ঢাকি নেৰাখে আৰু প্লেয়াৰৰ ওপৰত কোনো খালী ঠাই নেৰে। যদি এপটো ক্ৰাশ কৰে বা টপ বাৰবোৰ ভুলকৈ দেখায় তেন্তে ইয়াক বন্ধ কৰক + গ্লৌ + এই ডিভাইচটোৱে গ্লৌ স্প্ল্যাশ স্ক্ৰীনৰ সহায় নকৰে লাইট থিমৰ পৃষ্ঠভূমি বগা Material You (বগা) @@ -1270,6 +1275,9 @@ Settings → Playback (বা Autoplay) → Autoplay next video" ভিডীঅ\' প্ৰগতি বাৰৰ গৌণ ৰং নিৰ্ধাৰণ কৰক অবৈধ seekbar ৰঙৰ মান + অৰিজিনেল এনালগ + অৰিজিনেল কেক + অৰিজিনেল মেটাল হেডাৰ ল’গ’ ডিফল্ট @@ -1562,6 +1570,7 @@ AV1 ৰ সৈতে ভিডিঅ' প্লেবেকেত্ বাধ এপিসোডসমূহ চাইছোঁ বুলি চিহ্নিত কৰা হৈছে বাতিল কৰক সক্ৰিয় কৰক + ১ টা নতুন এপিসোড %1$s টা নতুন এপিসোড এই ডিভাইচত প্ৰগতি ৰেকৰ্ড কৰক "আপোনাৰ ছিৰিজ আৰু প্ৰগতি এই ডিভাইচটোত থাকিব, যিবোৰ বিভিন্ন YouTube একাউণ্টৰ মাজত ভাগ কৰি লোৱা হয়। YouTube ইতিহাস নিয়ন্ত্ৰকবোৰে ইয়াক সলনি নকৰে। ছিৰিজ মেনুৰ পৰা প্ৰগতি মুছক। diff --git a/patches/src/main/resources/addresources/values-az-rAZ/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-az-rAZ/shared-youtube/strings.xml index 6b9047cf75..f5c376f000 100644 --- a/patches/src/main/resources/addresources/values-az-rAZ/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-az-rAZ/shared-youtube/strings.xml @@ -21,9 +21,13 @@ Second \"item\" text" Başlanğıc animasiyası Qapalıdı Rəng - Morphe-ni Rənglə + Morphe Rəngi Saya - Saya Morphe + Morphe Saya + + Yeni tənzimləmələri vurğula + Yeniləmə ilə əlavə edilən tənzimləmələr və seçimlər ilə yanaşı nöqtənin göstərildiyi günlərin sayı + Qapalı Tənzimləməni bitirmək üçün yenidən başlat diff --git a/patches/src/main/resources/addresources/values-az-rAZ/sponsorblock/strings.xml b/patches/src/main/resources/addresources/values-az-rAZ/sponsorblock/strings.xml index 88851743a4..8d2d592ced 100644 --- a/patches/src/main/resources/addresources/values-az-rAZ/sponsorblock/strings.xml +++ b/patches/src/main/resources/addresources/values-az-rAZ/sponsorblock/strings.xml @@ -58,6 +58,8 @@ Second \"item\" text" Artıq oxudum Mənə göstər Ümumi + Yüklənmiş videoların bölümlərini saxla + Oflayn olarkən yüklənmiş videoların bölümlərini ötürməyə imkan verir Bağlantı xətası xəbərdarlıqlarını göstər SponsorBlock serveri əlçatmaz olduqda bildiriş göstərir Ötürmə sayının izlənilməsini aktivləşdir diff --git a/patches/src/main/resources/addresources/values-az-rAZ/youtube/strings.xml b/patches/src/main/resources/addresources/values-az-rAZ/youtube/strings.xml index 3e88733268..895ebad399 100644 --- a/patches/src/main/resources/addresources/values-az-rAZ/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-az-rAZ/youtube/strings.xml @@ -113,6 +113,9 @@ Dəyişikliklər ehtiva edir: Təftişləri gizlət Axın tövsiyəsi dəqiqliyi haqqında təftişləri və xatırlatmaları gizlət Bilet bölməsin gizlət + + \"Əlçatmaz videolar gizlidir\" panelini gizlət + Pleylistlərdə \"Əlçatmaz videolar gizlidir\" panelini gizlədir Yüklənilmə vaxtını gizlət "Məhdudiyyətlər: @@ -1208,6 +1211,8 @@ Məhdudiyyət: 'Köhnə video fəaliyyət cizgisini bərpa et' qapadılıbsa, 'D Tətbiqin açılışında görünən, YouTube Premium xüsusiyyəti olan \"İzləməyə davam et\" kiçik oynadıcısını qapadır Kiçik oynadıcı Kiçildilmiş video oynadıcı görünüşünü və davranışını fərdiləşdir + Əlavə kiçik oynadıcılar + YouTube bütün əlavə kiçik oynadıcıları sildi. Daha çox kiçik oynadıcı növünü bərpa etmək üçün YouTube 21.28.208-i və ya daha köhnəni yamaqla Kiçik oynadıcı növü Başlığı və kanalı gizlət Qapalıdı @@ -1249,6 +1254,9 @@ Məhdudiyyət: 'Köhnə video fəaliyyət cizgisini bərpa et' qapadılıbsa, 'D Status zolağının tərtibatını sabit saxlayır, beləliklə o, yuxarı zolaqları örtmür və oynadıcı yuxarısında boş yer saxlamır. Tətbiq çöksə və ya yuxarı zolaqlar yanlış görünsə, bunu qapat Parlama + Morphe Parıltı 1 + Morphe Parıltı 2 + Parlaq effektli ekran bu cihaz tərəfindən dəstəklənmir İşıqlı kolorit fonu Ağ Material You (Ağ) diff --git a/patches/src/main/resources/addresources/values-be-rBY/music/strings.xml b/patches/src/main/resources/addresources/values-be-rBY/music/strings.xml index a14d354f4a..aba142390e 100644 --- a/patches/src/main/resources/addresources/values-be-rBY/music/strings.xml +++ b/patches/src/main/resources/addresources/values-be-rBY/music/strings.xml @@ -545,8 +545,6 @@ Second \"item\" text" Захавана Выдзяляць сінхранізаваныя словы або склады Аддаваць прыярытэт тэксту па словах, калі ён даступны. Калі тэкст мае часовыя меткі для слоў, кожнае слова або склад будзе выдзяляцца ў такт з музыкай - Паказваць бэк-вакал асобна - Пераносіць бэк-вакал, запісаны ў дужках, напрыклад, \"(Oh) I want you\", з асноўнага радка ў асобны радком з прыглушаным тэкстам Схаваць праграныя радкі тэксту Схаваць радкі тэксту вышэй за той, які праграваецца ў дадзены момант Схаваць непраграныя радкі тэксту diff --git a/patches/src/main/resources/addresources/values-be-rBY/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-be-rBY/shared-youtube/strings.xml index f87c67b8dc..211df85f2f 100644 --- a/patches/src/main/resources/addresources/values-be-rBY/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-be-rBY/shared-youtube/strings.xml @@ -22,6 +22,8 @@ Second \"item\" text" Адключана Колер Манахромны + + Выкл. Перазагрузіце прыладу, каб завяршыць налады @@ -51,8 +53,15 @@ Second \"item\" text" Звычайны Спампаваць у праграме Зональны загрузчык + Назва пакета завантажніка + Назва пакета вашай усталяванай знешняй праграмы-завантажніка Увядзіце назву пакета + Іншае + Праграма не ўсталявана %s не ўсталяваны. Калі ласка, ўсталюйце яго + "Не удалось знайсці ўсталяваную праграму з назвай пакета: %s + +Пераправерце, ці правільна паказана назва пакета і ці ўсталявана праграма" Назва пакета не можа быць пустай Карыстальніцкае @@ -76,6 +85,8 @@ Second \"item\" text" • Значок Android Auto Каб змяніць значок усюды, выканайце патч паўторна і змяніце параметр патча \"Значок праграмы\"" + Іконка паведамленняў + Адпаведнасць іконцы праграмы Morphe play Паведамленне Morphe @@ -83,6 +94,7 @@ Second \"item\" text" Больш не паказваць Падарабіць відэапатокі + Наладзіць спафінг кліента, каб пазбегнуць праблем з віднаўленнем "Выпраўляе праблемы з відэапрайміўкасцю для więksнасці карыстальнікаў праз запыт сумяшчальнага відэапатока Калі вы карыстальнік YouTube Premium, гэтая настройка можа быць не патрэбнай" @@ -154,6 +166,7 @@ Second \"item\" text" Адключаць DRC-аудыё Прымусіць арыгінальную мову аўдыя + Заўсёды праграваць арыгінальную аўдыядарожку відэа замест дубляжа Прапускаць цішыню Аўтаматычна прапускае ціхія паўзы ў аўдыядарожцы відэа @@ -179,6 +192,7 @@ Second \"item\" text" Дазволіць прамое падключэнне ў выпадку збою Падключацца прама, калі праксі не працуе + Схаваць паўнаэкранную рэкламу Каб скарыстацца гэтай функцыяй, змяніце \"Spoof video streams\" на любога кліента, акрамя \"Android Studio\" і \"Android VR Downgraded\" @@ -187,14 +201,20 @@ Second \"item\" text" Выдаляе параметры адсочвання з размерных спасылак Змяніць спасылкі для падзелу на youtube.com Замяняе music.youtube.com на youtube.com у паділеных спасылках + Змяніць спасылкі для яго пашырэння на youtu.be Прымушае відэа выкарыстоўваць скарочаны фармат URL youtu.be + Протакол буфера адладчыкага лога + Дадае дадатковую інфармацыю ў логі для распрацоўшчыкаў, каб ідэнтыфікаваць кампаненты праграмы "Уключанне гэтай налады дазволіць запіс дадатковых даных размяшчання, уключаючы тэкст на экране для некаторых кампанентаў інтэрфейсу. Гэта можа дапамагчы ідэнтыфікаваць кампаненты пры стварэнні ўласных фільтраў. Аднак уключанне гэтай функцыі таксама прывядзе да запісу некаторых карыстальніцкіх даных, такіх як ваш IP-адрас" + Спецыяльны фільтр + Настройце спецыяльныя фільтры, каб схаваць любы кампанент + Уключыць спецыяльны фільтр Спіс радкоў пабудоўніка шляху кампанента для фільтрацыі, аддзяленых новымі радкамі Няправільны карыстальніцкі фільтр: %s @@ -242,6 +262,8 @@ Second \"item\" text" Звычайны %s FPS Перакрыць частату абнаўлення + Заўсёды + Віднаўленне ў партатратным рэжыме Працяўканьне на ўвесь экран Працяўканьне ў партрэтным рэжыме і на ўвесь экран @@ -282,8 +304,12 @@ ID відэа, якое вы глядзіце, адпраўляецца ў API R Дыслайкі часова недаступныя — дзейнічае абмежаванне хуткасці Client API API атрымання галасаў, колькасць выклікаў Сеткавых выклікаў не было + %s сеткавых выклікаў зроблена API атрымання галасаў, колькасць таймаўтаў Сеткавых выклікаў з таймаўтамі не было + %s сеткавых выклікаў пабылі па часе Ліміты частаты запытаў API-кліента Ліміты частаты запытаў кліента не сустрэкаліся + Ліміт запытаў кліента перакрачаны %s разоў + %s мілісекундаў diff --git a/patches/src/main/resources/addresources/values-be-rBY/youtube/strings.xml b/patches/src/main/resources/addresources/values-be-rBY/youtube/strings.xml index 9ce9b26822..9cb9149d8d 100644 --- a/patches/src/main/resources/addresources/values-be-rBY/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-be-rBY/youtube/strings.xml @@ -113,6 +113,9 @@ Second \"item\" text" Схаваць апытанні Схаваць апытанні і падказкі аб дакладнасці рэкамендацый стужкі Схаваць паліцу з білетамі + + Схаваць банер \"Недаступныя відэа схаваныя\" + Схаваць банер \"Недаступныя відэа схаваныя\" ў плейлістах Схаваць час загрузкі "Абмежаванні: @@ -511,6 +514,10 @@ Second \"item\" text" Пастаўшчык перакладу Гучнасць арыгінальнага аўдыё Гучнасць арыгінальнага аўдыё YouTube, пакуль ідзе пераклад + Гучнасць перакладу + Гучнасць голаса дубляжанага перакладу + Максимальная хуткасць маўлення + Максімальны множнік хуткасці пры ўмясчанні маўлення ў слот субцітраў "Бясплатны API перакладу, ключ API не патрабуецца. Да 5 000 сімвалаў у дзень на бясплатным тарыфе або да 50 000 сімвалаў у дзень пры наяўнасці адраса электроннай пошты. Націсніце тут, каб адкрыць MyMemory.translated.net" @@ -530,6 +537,11 @@ Second \"item\" text" %s за 100 відэагадзін Голас Сістэма + Мноўмоўны мужчына + Мноўмоўная жанчына + Мужчына + Жанчына + Час марфінга! Пераклад недаступны "Служба перакладу вернула памылку HTTP %s. Ваша IP-адрас, верагодна, быў абмежаваны па колькасці запытаў на тэрмін да 24 гадзін. @@ -722,16 +734,24 @@ Second \"item\" text" Уключаць жэст «націсніць, каб праціркнуць» Патрабуецца націсканне і ўтрыманне перад тым, як будзе распазнаны жэст працягвання Уключаць тактыльны водгук + Захаваць і аднавіць яскрасць Запамінае ўзровень яскравасці пры ўваходзе / выхадзе з паўнаэкраннага рэжыму + Уключыць жэст аўтаяскрасці Перамяшчэнне паўзунка яскравасці да мінімальнага значэння актывуе сістэмную аўтаяскравасць + Аўта + Таймаўт панэлі прагавання + Колькасць мілісекунд, працягваючыя якія панэль візібельная + Празрачнасць фона панэлі прагавання Наладзіце ўзровень празрыстасці (0 — празрысты, 100 — непрозрачны) Колер пакрыцця прагарткі яскравасці Усталюйце колер палоскі прагрэсу рэгулятара яскрасці + Колер гучнасці на панэлі прагавання Усталюйце колер палоскі прагрэсу рэгулятара гучнасці Колер паказчыка хуткасці свайпа Усталюйце колер палоскі прагрэсу элементаў кіравання хуткасцю прайгравання Памер тэксту ў накладання свайпа Наладзіце памер тэксту на экране (1–30) + Парог велічыні прагавання Усталюйце неабходную дыстанцыю свайпа для актывацыі рэгуляцыі Частривливасть свайпа граммисты гусникки Вышейшыйся значенниусть паребвавниутся большй свайп на кожни ступень гусникки @@ -747,7 +767,11 @@ Second \"item\" text" Вышыня зоны хуткасці Вышыня зоны жэсту хуткасці як працэнт ад вышыні плеера (5-75) Хуткасць + Стыль панэлі прагавання Гарызантальны слой + Гарызонтальнае накладанне (мінімалістычнае - зверху) + Гарызонтальнае накладанне (мінімалістычнае - па цэнтры) + Круглае накладанне Круглае пакрыццё (мінімалістычнае) Вертыкальнае пакрыццё Вертыкальнае пакрыццё (мінімалістычнае) @@ -942,12 +966,14 @@ Second \"item\" text" Схаваць кнопку Cast Схаваць кнопку \"Згарнуць\" Схаваць кнопку \"Паўны экран\" + Схаваць фон элементаў кіравання плеярам Хавае цёмную зацянелую вобласць вакол элементаў кіравання відэаплэерам Празрачнасць фона кнопак кіравання Наладаць празрыстасць фона за кнопкамі кіравання плеярам і палосай часу (0 — нябачны, 100 — сплошны) Схаваць кнопкі «Папярэдні» і «Наступны» Схаваць кнопку \"Налады\" + Схаваць карткі фінальнага экрана Атмасферны рэжым Кіраванне бачнасцю рэжыму Ambient і абмежаваннямі па харчаванні @@ -956,9 +982,13 @@ Second \"item\" text" Адключаць атмасферны рэжым Адключаць рэжым Ambient у паўнаэкранным рэжыме + Схаваць інфармацыйныя карткі + Адключаць анімацыю пракруткі лікаў Спыняе анімацыю лічыльніка лайкаў і праглядаў + Схаваць палосу пракруткі відэаплеяра + Схаваць палосу пракруткі з мініятурамі відэа Схаваць палоску прагрэсу прагляду, якая паказваецца на мініятурах відэа, якія вы ўжо паглядзелі Уключаць паўнаэкранны вялікі паўзунак пракруткі @@ -976,12 +1006,14 @@ Second \"item\" text" Адключаць пастаўленне лайка па двайным націсканні Схаваць кнопку AI Схаваць пазначку \"Аўтаматычна дубляваны\" + Схаваць панэль канала Схаваць кнопку \"Каментарыі\" Схаваць кнопку \"Эфект\" Схаваць надпіс спасылкі на відэа Схаваць падказкі па жэстах Схаваць кнопку \"Зялёны экран\" Схаваць кнопку \"Хэштэг\" + Схаваць панэль інфармацыі Схаваць кнопку \"Далучыцца\" Схаваць кнопку \"Падабаецца\" @@ -1003,12 +1035,14 @@ Second \"item\" text" Схаваць кнопку \"Паділіцца\" Схаваць кнопку \"Крама\" Схаваць кнопку гуку + Схаваць ярлычок метаданых гуку Схаваць кнопку \"Падпісацца\" Схаваць кнопку \"Купіць Super Thanks\" Схаваць пазначаныя тавары Схаваць кнопку \"Далейшае\" Схаваць кнопку \"Выкарыстаць гэты гук\" Схаваць кнопку \"Выкарыстаць гэты шаблон\" + Схаваць назву відэа Схаваць рэкамендаванае відэа ў канцы "Хавае ўкладку з рэкамендацыйнымі відэа, якая з'яўляецца пасля заканчэння відэа @@ -1111,6 +1145,8 @@ Second \"item\" text" Перакрыць кнопкі YouTube Music Адкрывае вашу прыляганую музыку замест стандартнай праграмы YouTube Music + Назва пакета музычнага праграмы + Назва пакета вашай усталяванай музычнай праграмы Стартовая старонка Звычайны @@ -1177,6 +1213,7 @@ Second \"item\" text" Настройце выгляд і паводзіны зведзенага відэаплэера Тып мініплэера Схаваць загаловак і канал + Адключана Звычайны Мінімалістычны Мінімалістычны 2 @@ -1188,6 +1225,7 @@ Second \"item\" text" Адключаць закругленыя куты Адключаць перацягванне Забараняе перацягванне зведзенага плэера па розных кутах экрана + Адключаць жэст гарызантальнага перацягвання Забараняе закрываць мініплэер шляхам гарызантальнага працягвання за межы экрана Паўза ў працьоўванні пры гарызантальным перацягванні Відпраўка спыніцца, калі мініплэер будзе перацягнуты за межы экрана @@ -1213,12 +1251,22 @@ Second \"item\" text" Выправіць перакрыццё верхніх панэляў панэллю стану Забяспечвае стабільнасць размяшчаўкі панэлі стану, каб яна не перакрывала верхнія панэлі і не пакідала пустага месца над плэерам. Выключайце гэтую функцыю, калі праграма вылетае або верхнія панэлі выглядаюць няправільна + Свячэнне + Morphe Glow 1 + Morphe Glow 2 + Гэтая прылада не падтрымлівае эфект святлення для экрана загрузкі Фон светлай тэмы Белы Material You (белы) Святла-рожавы + Святла-блакітны + Святло-зялёны + Святло-жоўты Святла-аранжавы + Святло-чырвоны + Свой святлы фон Усталяваць колер фона для светлай тэмы + Змяніць колер передняга плана Выкарыстоўваць колер тэмы для тэксту і іконак Уключыць градыянтны экран загрузкі Паказвае градыентны фон на экране загрузкі замест аднатоннага колары @@ -1229,6 +1277,9 @@ Second \"item\" text" Усталюйце дапаможны колер палоскі прагрэсу відэа Непрыемнае значэнне колеру палоскі пракруткі + Арыгінальны аналог + Арыгінальны торт + Арыгінальны метал Логотып загалоўка Звычайны @@ -1475,6 +1526,12 @@ AVC (H.264) Пазначыць як непраглянутае Выкарыстоўваць аўтаматычны статус Перазапусціць з 0:00 + Паўторзіць прагляд з 0:00 + Статус захаваны + Паўторыць захаванне + Папярэднія 100 + Наступныя 100 + Адкрыць захаванае відэа Локальны прагрэс ачышчаны Адкрыйце «Гісторыю», каб пабачыць свае серыялы Гісторыя прагляду @@ -1510,6 +1567,9 @@ AVC (H.264) Праглядзець серыялы Працягнуць Пачніце тут + Пункт пачатку захаваны + Паглядзена да гэтага месца + Эпізоды пазначаны як праглядзеныя Скасаваць Уключыць %1$s новых серый @@ -1554,6 +1614,7 @@ AVC (H.264) Застацца секунд (0-300; 0 выключае) Увядзіце значэнне ад %1$s да %2$s Выкарыстоўваць прагрэс YouTube + Працягніце прагляд серыялаў пасля прагляду на іншай прыладнасці Чытаць статус прагляду і пазіцыі ўзоўлення для вашых падпісаных серыялаў з бягучага ўліковага запісу YouTube. На гэтым тэлефоне захраняюцца толькі адпаведныя эпізоды. Праверкі выконваюцца, пакуль адкрыта бібліятэка серыялаў і перад «Працягнуць». Гісторыя YouTube павінна быць уключана на іншай прыладзе Праверка прагрэсу ў YouTube… Прагрэс YouTube недаступны. Выкарыстоўваецца захаваны прагрэс diff --git a/patches/src/main/resources/addresources/values-bg-rBG/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-bg-rBG/shared-youtube/strings.xml index 390535e0a0..47d5c9302b 100644 --- a/patches/src/main/resources/addresources/values-bg-rBG/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-bg-rBG/shared-youtube/strings.xml @@ -21,9 +21,8 @@ Second \"item\" text" Анимация при стартиране Изключено Цвят - Цветен Morphe Монохром - Монохромно Morphe + Рестартирайте, за да завършите настройката diff --git a/patches/src/main/resources/addresources/values-bg-rBG/youtube/strings.xml b/patches/src/main/resources/addresources/values-bg-rBG/youtube/strings.xml index 13309cd987..4badbdf109 100644 --- a/patches/src/main/resources/addresources/values-bg-rBG/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-bg-rBG/youtube/strings.xml @@ -113,6 +113,9 @@ Second \"item\" text" Скриване на анкетите Скрива проучвания и подкани относно точността на препоръките за канала Скриване на рафта за билети + + Скриване на банера \"Недоступните видеоклипове са скрити\" + Скрива банера \"Недоступните видеоклипове са скрити\" в плейлистовете Скрийте информационни карти "Ограничения: @@ -1249,6 +1252,9 @@ Second \"item\" text" Поддържа последователен макет на статус бара, така че той да не покрива горните ленти и да не оставя празно пространство над плеъра. Изключете тази опция, ако приложението се счупи или горните ленти изглеждат неправилно Сяяне + Morphe Glow 1 + Morphe Glow 2 + Екранът за зареждане с ефект на сияние не се поддържа от това устройство Фон на светлата тема Бяло Material You (Бяло)‎ diff --git a/patches/src/main/resources/addresources/values-bn-rBD/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-bn-rBD/shared-youtube/strings.xml index 323762f45e..2499b5085b 100644 --- a/patches/src/main/resources/addresources/values-bn-rBD/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-bn-rBD/shared-youtube/strings.xml @@ -21,9 +21,8 @@ Second \"item\" text" স্টার্টআপ অ্যানিমেশন বন্ধ রাখুন রং - কালার Morphe মনোক্রোম - মনোক্রোম Morphe + সেটআপ সম্পন্ন করতে রিস্টার্ট করুন diff --git a/patches/src/main/resources/addresources/values-bn-rBD/youtube/strings.xml b/patches/src/main/resources/addresources/values-bn-rBD/youtube/strings.xml index 518f279e71..fb2123392c 100644 --- a/patches/src/main/resources/addresources/values-bn-rBD/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-bn-rBD/youtube/strings.xml @@ -113,6 +113,9 @@ Second \"item\" text" সার্ভে লুকান ফিডের সুপারিশ নির্ভুলতা সম্পর্কে সমীক্ষা এবং নাজগুলি লুকায়। টিকিট তাক লুকান + + \' অনুপলব্ধ ভিডিওগুলি লুকানো হয়েছে\' ব্যানারটি হাইড করুন + প্লেলিস্টে \'অনুপলব্ধ ভিডিওগুলি লুকানো হয়েছে\' ব্যানারটি হাইড করে আপলোডের সময় লুকান "সীমাবদ্ধতা: @@ -1249,6 +1252,7 @@ OpenRouter.ai ওপেন করতে এখানে ট্যাপ কর স্ট্যাটাস বারের লেআউট ধ্রুবক রাখে, তাই এটি উপরের বারগুলি ঢাকবে না এবং প্লেয়ারের উপরে কোনো খালি জায়গা থাকবে না। অ্যাপটি ক্র্যাশ হলে বা উপরের বারগুলো ভুল দেখালে এই সেটিংটি বন্ধ করুন গ্লো + এই ডিভাইসটি গ্লো স্প্ল্যাশ স্ক্রিন সমর্থন করে না হালকা থিমের পটভূমি সাদা Material You (সাদা) diff --git a/patches/src/main/resources/addresources/values-bs-rBA/music/strings.xml b/patches/src/main/resources/addresources/values-bs-rBA/music/strings.xml index 0874681a3c..0bdce6a4b5 100644 --- a/patches/src/main/resources/addresources/values-bs-rBA/music/strings.xml +++ b/patches/src/main/resources/addresources/values-bs-rBA/music/strings.xml @@ -17,64 +17,675 @@ Second \"item\" text" --> + Posljednje pušteno + Pretplate + Videozapisi + Ostali rezultati + Otpremljene pjesme + Otpremljeni albumi + Otpremljeni izvođači + Ažurirajte Jam zakrpu domaćina kako biste koristili pauziranje i nastavak + Dijeljenje Jam reda čekanja + Budite domaćin ili se pridružite obližnjem Jam-u i uređujte njegov red čekanja putem Jam sloja + Omogući dijeljenje Jam reda čekanja + Omogućava Jam integraciju nakon ponovnog pokretanja YouTube Music-a + Preuzmite Jam Layer kako biste ga pokrenuli + Otvorite najnovije izdanje Jam sloja u svom pregledniku + Jam kontrole + Budite domaćin ili se pridružite obližnjem Jam-u i upravljajte sesijom + Jam Layer paket + Odaberite instaliranu prateću aplikaciju koja se koristi za dijeljenje reda čekanja u blizini + Koristite instalirani prateći paket. Uparivanje zahtijeva vaše odobrenje i lokalni token mogućnosti + Uključiti Wi-Fi? + Uključite Wi-Fi, a zatim pokušajte ponovo pokrenuti ili se pridružiti Jam-u + Otvori Wi-Fi postavke + Otkaži + Koristi paket + Unesite ispravno ime Android paketa + Paket je sačuvan. Podesite Jam Layer za povezivanje + Jam je prekinut + Prvo omogućite Jam sloj + Omogućite dijeljenje Jam reda u postavkama, a zatim ponovo pokrenite YouTube Music + Prvo instalirajte odabrani Jam Layer paket + Unesite kod prikazan na hostu. Wi-Fi Aware može povezati uređaje u blizini čak i kada se njihove LAN mreže razlikuju + Pridruži se pomoću koda + Pridruži se + Skeniraj QR + Unesite osmocifreni kod ili zalijepite pozivnicu + Pridruži se svom Jam-u + Kopiraj kod + Jam kod + Prikaži QR + Gotovo + Pozovi u svoj Jam + Skeniraj da se pridružiš i uređuješ ovu rednu listu. Prekid Jam sesije poništava poziv + Kopiraj vezu + Jam pozivnica + Jam se ponovo povezuje; pokušajte ponovo uskoro + Dodano u Jam + Jam nije dostupan; traka nije dodana + Vaš Jam + Zajedničko slušanje + Pridruživanje Jam sesiji + Slušaj zajedno + Ažuriranje Jam sesije… + Traženje vašeg domaćina na Wi-Fi mreži… + Čekanje ljudi + %s osoba je povezana + %s osoba je povezano + Unesite ovaj kod na uređaju u blizini. Wi-Fi Aware radi preko zasebnih LAN-ova kada je podržano. Ističe za %s minuta + Pomakni na %s? + Uredivanja su otvorena + Uredivanja su zaključana + Povezano putem Wi-Fi Aware + Povezano putem Wi-Fi mreže + Povezano putem Bluetooth-a + Ne podržava se tokom Jam sesije + Pridruživanje otkazano + Ponovno povezivanje s domaćinom… + Podijelite red čekanja. Dozvolite svima da dodaju pjesme + Pozovite ljude + Gosti mogu uređivati red čekanja + Završi Jam + Domaćin pušta muziku. Dodajte, pomjerajte ili uklanjajte pjesme direktno u YouTube Music redu čekanja + Držite oba uređaja u blizini. Možete otkazati dok tražimo domaćina + Otkaži pridruživanje + Napusti Jam + Započnite Jam + Postavite Jam sloj + Upravljanje + Detalji + Otvori + Učitavanje opcija pjesme… + Opcije pjesme nisu dostupne; pokušajte ponovo + Nije moguće otvoriti opcije pjesme + Nema dostupnog preglednika za preuzimanje Jam sloja + Čekanje u redu domaćina + Prvo otvorite pjesmu u YouTube Music-u + Reprodukuj kod domaćina + Napusti Jam i reprodukuj lokalno + Reprodukuj sljedeće u Jam-u + Dodaj u Jam red + Reprodukuj ovu Jam traku + Odaberite gdje reprodukovati + Premjesti domaćina ovdje + Pjesma nije postala spremna za pretraživanje + Red je promijenjen; pokušajte ponovo s gestom The The + Prilagođeno + Reprodukuj audio trake albuma + Reprodukuj samo audio verziju pjesama s albuma umjesto muzičkog videa. Zahtijeva \'Spoof video streamova\' + Omogući prebacivanje audio/video + Pokušaj prebacivanja između audio verzije trake i službenog muzičkog videa + Preuzmi muzičke spotove, podkaste ili pjesme pomoću aplikacija trećih strana za preuzimanje + Koristi eksterni preuzimač za meni Preuzmi + Opcija Preuzmi u menijima otvara vaš eksterni preuzimač + Preuzimanja + "Nema preuzimanja +Preuzete pjesme se pojavljuju ovdje" + %1$s • %2$s • %3$s MB + %1$s pjesama • %2$s MB + Pretraži preuzimanja + Sortiraj + Sortiraj po izvođaču + Sortiraj po naslovu + Sortiraj po veličini + Obriši sva preuzimanja + Obriši sva preuzimanja? + Sve preuzete pjesme će biti obrisane s ovog uređaja + Preuzimanja su obrisana + Ništa nije pronađeno + Akcije za %s + Reprodukuj + Reprodukuj ili pauziraj + Prethodna + Sljedeća + Pauziraj + Zatvori + Obriši + Obriši preuzimanje + Obrisati preuzimanje? + %s će biti obrisano s ovog uređaja + Preuzimanje je obrisano + Preuzimanje nije moguće obrisati + Preuzimanje nije moguće reprodukovati + Preuzimanja audio zapisa + Reprodukcija offline + Nije moguće identifikovati traku za preuzimanje + Preuzimanje je već u toku + Već je preuzeto + Preuzimanje je započeto + Pokretanje preuzimanja… + Preuzimanje %s%% + Preuzimanje završeno + Preuzimanje nije uspjelo + O Morphe + Reklame Feed Opširno Plejer + Tekstovi pjesama + Razno + Jezik YouTube Music-a + Sistemski jezik + Zahtijeva Android 13 ili noviji + Sakrij oznaku \'Preuzmi Music Premium\' + Sakrij Music Premium promocije + Sakrij video reklame + Sakrij Audio / Video preklopnik + Sakrij dugme Preuzmi + Sakriva dugme Preuzmi u podcast emisiji + Sakrij Explore policu + Sakrij traku filtera + Sakrij mrežne police + Sakrij horizontalne police + Sakrij liste polica + Panel sa tekstovima pjesama + Sakrij dugme Podijeli + Sakrij dugme Prevedi + Sakrij policu \"Novo od\" + Sakrij police s plejlistama + Sakrij dugme za ponavljanje + Sakrij dugme za nasumično puštanje + Sakrij policu \"Brzi poziv\" + Komentari + Odaberite koje komponente se pojavljuju u odjeljku sa komentarima + Sakrij smjernice zajednice + Sakrij kontekstualnu poruku + Sakrij dugme za emojije + Sakrij info dugme + Sakrij dugme za vremenski žig + Crossfade + Glatko blendajte trake jednu u drugu + Omogući crossfade + Kriva fade-a + Jednakna snaga + Suptilno zadržavanje + Blago ublažavanje + Glatka S-kriva + Odlazna + Dolazna + Trajanje crossfade-a + 250 milisekundi + 500 milisekundi + 750 milisekundi + 1 sekunda + 2 sekunde + 3 sekunde + 4 sekunde + 5 sekundi + 6 sekundi + 7 sekundi + 8 sekundi + 9 sekundi + 10 sekundi + 11 sekundi + 12 sekundi + Crossfade pri ručnom preskakanju + Primjenjuje crossfade efekt kada ručno preskočite na sljedeću pjesmu + Crossfade pri automatskom prelasku + Primjenjuje crossfade efekt kada se pjesma prirodno završi + Kontrola sesije + Dugo pritisnite dugme za nasumično puštanje kako biste pauzirali ili nastavili crossfade + Crossfade je pauziran za ovu sesiju + Crossfade je nastavljen + Video režim nije dostupan dok je crossfade omogućen + Crossfade je isključen prilikom SABR reprodukcije + Crossfade je isključen tokom castanja + Kako funkcioniše + "Crossfade preklapa kraj jedne pjesme s početkom sljedeće, koristeći vaše trajanje i krivulju + +Radi pri automatskom prelasku i pri ručnom preskakanju, svaki sa vlastitim prekidačem. Kod automatskog prelaska miješanje počinje nekoliko sekundi prije nego što pjesma završi" + Ograničenja + "Muzički spotovi se reprodukuju samo kao audio dok je crossfade uključen. Dugim pritiskom na shuffle pauzirajte crossfade i gledajte video + +Muzički spot koji započne reprodukciju ostaje u video režimu do sljedeće pjesme + +Pjesme kraće od trajanja fade-a nisu crossfaded + +Zaključani ekran ili obavještenje mogu nakratko prikazati zastarjelo stanje usred tranzicije + +Pauziranje crossfade-a radi gledanja muzičkog spota može ostaviti neke video zapise crnim. Pauzirajte crossfade prije nego što odaberete pjesmu kako biste to izbjegli" + Nije podržano + "Castanje ili mirroring audia (Chromecast, mirroring ekrana ili audia) + +SABR reprodukcija (isključeni spoof video streamovi)" + Zasluge + Funkcija crossfade-a od VazerOG + Zapamti stanje ponavljanja + Zapamti stanje nasumičnog puštanja + Brzina reprodukcije + Brzina koja se koristi za sve trake. Na 100% koristi se brzina aplikacije, kao što je brzina reprodukcije podkasta + Promijeni visinu tona s brzinom + Onemogući preusmjeravanje pri nevoljenju + Sprečava preskakanje na sljedeću pjesmu kada se pritisne dugme za nevoljenje + Akcija pri dodiru trake u plejlisti + Reprodukuj plejlistu + Reprodukuj samo odabranu traku + Dodaj odabranu traku u red čekanja + Dodano u red čekanja + Sakrij Cast dugme + Sakrij dugme Historija + Sakrij dugme Obavijesti + Sakrij dugme Pretraga + Sakrij dugme za glasovnu pretragu + Sakrij dugme za pretragu zvuka + Sakrij dugme Novo + Sakriva dugme Novo u kartici Biblioteka + Dugmad za akcije + Odaberite koja dugmad za akcije će se pojaviti ispod plejera + Sakrij traku akcija + Sakrij dugmad Sviđa mi se i Ne sviđa mi se + Sakrij dugme Komentari + Sakrij dugme \'Ponovni prikaz ćaskanja uživo\' + Sakrij dugme Tekst pjesme + Sakrij dugme Dijeli + Sakrij dugme Sačuvaj + Sakrij dugme Preuzmi + Sakrij dugme \'Pokreni radio\' + Flyout meni + Odaberite koje komponente se pojavljuju u flyout menijima playera i reda za čekanje + Sakrij komponentu sa 3 kolone + Sakrij dugmad Sviđa mi se i Ne sviđa mi se + Sakrij meni \"Taste match\" + Sakrij meni \"Dodaj u listu \'Slušaj kasnije\'\" + Sakrij meni \"Dodaj u red čekanja\" + Sakrij meni za titlove + Sakrij meni \"Obriši listu pjesama\" + Sakrij meni \"Poništi red čekanja\" + Sakrij meni \"Ne preporučuj ovog izvođača\" + Sakrij meni za preuzimanje + Sakrij meni \"Uredi listu pjesama\" + Sakrij meni \"Idi na album\" + Sakrij meni \"Idi na izvođača\" + Sakrij meni \"Idi na epizodu\" + Sakrij meni \"Idi na podcast\" + Sakrij meni za pomoć + Sakrij meni \"Označi epizodu kao puštenu\" + Sakrij meni \"Nije me zanimalo\" + Sakrij meni \"Zakači na brzi biralj\" + Sakrij meni \"Pusti sljedeće\" + Sakrij meni za kvalitet + Sakrij meni \"Ukloni iz biblioteke\" + Sakrij meni \"Ukloni iz plejliste\" + Sakrij meni za prijavu + Sakrij meni \"Sačuvaj epizodu za kasnije\" / \"Sačuvaj u biblioteku\" + Sakrij meni \"Spremi u listu pjesama\" + Sakrij meni za dijeljenje + Sakrij meni \"Nasumično puštanje\" + Sakrij meni \"Tajmer za spavanje\" + Sakrij meni \"Pokreni radio\" + Sakrij meni \"Statistika za ljubitelje detalja\" + Sakrij meni za pretplatu + Sakrij meni \"Odkači sa brzog biranja\" + Sakrij meni \"Pogledaj autore pjesme\" + Početna stranica The + Grafikoni + Epizode za kasnije + Istraži + Historija + Biblioteka + Omiljena muzika + Playliste + Pretraga + Pretplate + Prisili portretnu orijentaciju + Zadržava aplikaciju u portretnoj orijentaciji kada se uređaj rotira + Promijeni boju miniplayera + Prilagođava boju mini-plejera boji plejera u punom zaslonu + Promijeni boju navigacijske trake + Prilagođava boju navigacijske trake fullscreen plejeru + Prikaži dugme za sljedeću pjesmu u miniplayeru + Prikaži dugme za prethodnu pjesmu u miniplayeru + Omogući prisilni miniplayer + Sprečava plejer da se automatski proširi na puni zaslon kada počne nova pjesma + Omogući prevlačenje za zatvaranje mini-plejera + Omogućava zatvaranje mini-pleyera prevlačenjem prema dolje + Navigacijska traka + Odaberite koje će se dugmad pojaviti u donjoj navigacijskoj traci + Sakrij dugme Početna + Sakrij dugme Uzorci + Sakrij dugme Istražite + Sakrij dugme Biblioteka + Sakrij dugme Nadogradnja + Sakrij navigacijsku traku + Sakrij oznake navigacijskih dugmadi + Niste prijavljeni. Dodirnite za konfiguraciju + Odjavi se + Odjavljeno + Omogući scrobbling + Pošalji status \'Pušta se sada\' + Minimalno trajanje pjesme + Pjesme kraće od ovoga neće biti scrobbled + Postotak scrobble kašnjenja + Pokreni scrobble nakon slušanja ovog postotka pjesme + Kašnjenje scrobble-a u sekundama + Pokreni scrobble nakon slušanja određenog broja sekundi + ListenBrainz račun + Prijavljeni ste. Dodirnite za upravljanje računom + Unesite svoj ListenBrainz korisnički token kako biste omogućili scrobbling + Token + Provjeri + Molimo unesite token + Validacija… + Važeći token! Korisnik: %s + Nevažeći token: %s + nepoznato + Verifikacija nije uspjela: %s + Token je obrisan + Token je sačuvan + Upozorenje: Sačuvani token je nevažeći! + Označi zapise koji su „sviđaju mi se“ kao omiljene + Automatski označi zapise kao omiljene na Last.fm-u kada ih označite sa „sviđa mi se“ na YouTube Music-u (i ukloni oznaku omiljenog kada se ukloni „sviđa mi se“ ili se označi „ne sviđa mi se“) + Last.fm račun + Prijavljeni ste kao %s. Dodirnite za upravljanje računom + Unesite svoje Last.fm podatke za prijavu + Korisničko ime + Lozinka + Prijava + Molimo unesite korisničko ime i lozinku + Prijava… + Uspješno prijavljeno! + Neuspjelo preuzimanje sesije sa Last.fm + Prijava nije uspjela: %s + Last.fm vjerodajnice su sačuvane! + Čišćenje metapodataka + Očisti metapodatke + Automatski očistite uobičajene nepotrebne podatke (poput \"Official Video\", \"Remaster\", \"Live\") iz naslova, izvođača i albuma prije scrobblinga + Prilagođeno regex čišćenje + Unesite prilagođeni regularni izraz kako biste uklonili dodatni tekst iz naslova pjesama, izvođača ili albuma. Primjer: (?i)\\s*\\((clean|explicit)\\) + Parsiraj izvođača i traku iz naslova + Izvucite ime izvođača i naziv pjesme iz naslova videa koristeći separator \" - \". Korisno za ponovo otpremljeni sadržaj gdje se naziv kanala može koristiti kao izvođač + Popuni nedostajući album + Kada muzika ne uključuje album, potražite ga na Last.fm-u kako bi vaš scrobble bio potpun + "Scrobbling automatski sinhronizuje vašu upotrebu YouTube Music-a sa vašim Last.fm ili ListenBrainz profilom + +Kako biste osigurali da se scrobbling sinhronizuje tokom reprodukcije u pozadini, provjerite da li je upotreba baterije aplikacije YouTube Music postavljena na \"Neograničeno\"" + Koristi tekstove pjesama trećih strana + Prikazuje tekstove pjesama sa usluge trećih strana, vraćajući se na ugrađene tekstove kada nijedan nije pronađen + Drži ekran uključenim tokom pregledavanja teksta pjesme + Sprečava isključivanje ekrana dok je panel sa tekstom pjesme otvoren + Servis za tekstove pjesama + Prikaz + Sinhronizacija + Panel + AI krajnja tačka + Svi provajderi su onemogućeni - ugrađeni tekstovi i embedded tekstovi + Resetovati redoslijed pružalaca? + Vratite pružalace na podrazumijevani redoslijed prioriteta + Obriši sačuvane tekstove + Obrišite tekstove pjesama sačuvanih na ovom uređaju + Očišćeno %1$s MB + Preuzmi token + Apple Music Media-user-token + "Zalijepite media-user-token kolačić sa music.apple.com + +Koraci: +1. Prijavite se na music.apple.com kao registrovani korisnik +2. Otvorite DevTools → Application → Cookies → music.apple.com +3. Kopirajte vrijednost \"media-user-token\"" + Apple Music: media-user-token je sačuvan + Apple Music: media-user-token je obrisan + Apple Music: nevažeći media-user-token - provjerite i pokušajte ponovo + Spotify SpDc + "Zalijepite sp_dc kolačić sa Spotify-a. + +Koraci: +1. Prijavite se na open.spotify.com +2. Otvorite DevTools → Application → Cookies → open.spotify.com +3. Kopirajte vrijednost \"sp_dc\"" + Spotify: sp_dc je sačuvan + Spotify: sp_dc je obrisan + Spotify: nevažeći sp_dc - provjerite i pokušajte ponovo + Apple Music token je opcionalan. Bez njega, Lyrically će se koristiti kao rezervna opcija + Deezer ARL + "Zalijepite arl kolačić sa Deezer-a. + +Koraci: +1. Prijavite se na deezer.com +2. Otvorite DevTools → Application → Cookies → deezer.com +3. Kopirajte vrijednost \"arl\"" + Deezer: arl sačuvan + Deezer: arl je obrisan + Deezer: nevažeći arl - provjerite i pokušajte ponovo + Musixmatch token + Dodajte token samo ako želite koristiti vlastiti račun. On mora biti preuzet iz Musixmatch Android aplikacije: tokeni kopirani sa musixmatch.com pripadaju povučenom desktop klijentu i API ih odbija + Musixmatch radi i bez tokena, tako da ovo polje može ostati prazno + korisnički token + Musixmatch: korisnički token je sačuvan + Musixmatch: korisnički token je obrisan + Musixmatch: nevažeći korisnički token - provjerite i pokušajte ponovo + YouTube kolačići + "Zalijepite svoje YouTube kolačiće kako biste pretraživali titlove i vremenski tekst sa YouTube-a. + +Koraci: +1. Otvorite youtube.com u pregledniku i prijavite se +2. Otvorite DevTools (F12) → Application → Cookies → youtube.com +3. Kopirajte cijeli niz kolačića (parovi ključ=vrednost odvojeni tačka-zarezom) + +Potrebni kolačići: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPISID, LOGIN_INFO" + YouTube: kolačići su sačuvani + YouTube: kolačići su obrisani + YouTube: nevažeći kolačići - provjerite i pokušajte ponovo + Dodirnite i držite red + Dodirnite red kako biste preskočili na taj trenutak u pjesmi, dugo pritisnite da kopirate riječ ispod prsta ili cijeli red ako pritisak nije na riječi. Isključivanje uklanja oba gesta + Prikaži oznake sekcija + Označi svaku novu sekciju teksta pjesme malom oznakom, kao što su Strofa ili Refren + Prikaži dugme Kopiraj + Dodirnite da kopirate originalni tekst kao običan tekst; dugo pritisnite da sačuvate izvorni fajl teksta u Preuzimanja + Prikaži dugme Prevedi + Dodirnite za promjenu prikaza prijevoda; dugo pritisnite samo za prijevod. Koristi prijevode pružatelja, Google Translate ili OpenAI kompatibilni API + Prikaži dugme za romanizaciju + Dodirnite za promjenu prikaza romanizacije; dugo pritisnite samo za romanizaciju. Koristi romanizaciju pružatelja, Google Translate ili OpenAI kompatibilni API + Lokalna romanizacija i poravnanje + Romanizirajte na uređaju kada pružalac, vaš model i Google Translate zakažu. Čitanje prati svaku riječ sinkronizovanog teksta pjesme; sinkronizacija riječi mora biti uključena. Kineski, kantonski, japanski, korejski, grčki, ruski, gruzijski, armenski; oko 120 MB + Čitanje %1$s: %2$d%% + Očitanja su spremna (%1$d unosa) + Preuzimanje nije uspjelo - isključite i ponovo uključite ovu opciju kako biste pokušali ponovo + Prikaži dugme za osvježavanje + Dodirnite za prebacivanje na sljedećeg kandidata za tekst pjesme; dugo pritisnite za pretragu teksta pjesme po naslovu pjesme i izvođaču + Pretraži tekstove pjesama + Naslov pjesme + Umjetnik + Unesite naziv pjesme i umjetnika + Nijedan od omogućenih pružalaca usluga ne može pretražiti druge pjesme + Sačuvano + Istakni sinhronizovane riječi ili slogove + Prioritizuj tekst pjesme riječ po riječ kada je dostupan. Kada tekst ima vremensko markiranje riječi, ističe svaku riječ ili slog u skladu s muzikom + Sakrij odsvirani tekst pjesme + Sakrij redove teksta iznad reda koji se trenutno pušta + Sakrij nepuštene tekstove + Sakrij redove teksta ispod reda koji se trenutno pušta + Prikaži MediaSession tekstove na zaključanom ekranu + Prikazuje trenutni red na zaključanom ekranu, Android Automatskom i Bluetooth-u. Zahtijeva Android 11 ili noviji + Prikaži tekstove u miniplayeru + Prikazuje trenutni red u minimu plejeru, pri čemu je traka pomjerena u potpis + Format naslova pjesme/izvođača + Format trake koji se prikazuje uz tekst pjesme u Miniplayeru i MediaSession-u. {t} je naslov, {a} je izvođač i {v} je verzija, kao što je Acoustic, kada izvor teksta pjesme to pruža. Tekst između dvije dvotačke prikazuje se samo kada svaka varijabla unutar nje ima vrijednost; napišite \\: za običnu dvotačku. Svaka linija se prikazuje redom, prelazeći na sljedeću kad god se promijeni tekst pjesme + Format linije teksta pjesme + Format linije teksta pjesme u Miniplayeru i MediaSession-u. {lrc} je linija, a {n} je broj puta koliko se ona zaredom pjeva. Tekst između dvotačaka prikazuje se samo kada se linija ponavlja. Koristite \\: za dvotačku + Format mora sadržavati {lrc} + Šablon linije izvođača i vrijeme animacije + Minimalno vrijeme na ekranu (ms) + Maksimalno vrijeme ekrana (ms) + Samo za animacije u više redova. Zadržite svaki okvir najmanje minimalno vrijeme. Na maksimalnom, pomjerite liniju izvođača čak i kada tekst pjesme ostane isti. + Minimum mora biti 0 ili veći. Maksimum mora biti veći od 0 i najmanje jednak minimumu. + Ograničenja dužine reda + Ograničenja redova za tekst pjesme i izvođača, te težina karaktera pune širine + "Ograničenje reda teksta pjesme +Podijelite duže stihove na uravnotežene polovine. 0 onemogućava podjelu." + "Ograničenje reda izvođača +Podijelite popunjeni format trake u ciklične okvire. 0 onemogućava podjelu." + "Težina karaktera pune širine +Primjenjuje se na oba ograničenja. Na 1.75, četiri karaktera pune širine računaju se kao 7." + Unesite cijele brojeve za limite od 0 do 10000 i težinu znakova veću od 0 + Instrumentalna pauza + Prikazuje se u miniplayeru i MediaSession-u tokom instrumentalne pauze. Svaka linija je jedan okvir animacije. {t} je naslov, {a} je izvođač, a {lrc} je posljednja linija teksta + Vremenski pomak za miniplayer i MediaSession + Pomjera trenutak kada se sljedeća linija prikazuje u miniplayeru i MediaSession-u. Koristite negativnu vrijednost ako se linije pojavljuju sa zakašnjenjem, na primjer na zaključanom ekranu + Filteri + Regex metapodataka + Regularni izraz koji se primjenjuje na naslove, izvođače i albume prije pretraživanja teksta pjesama i prilikom ocjenjivanja toga da li se dva naslova podudaraju. Ostavite prazno da biste onemogućili oba. Podrazumijevano uklanja uobičajene smetnje kao što su \"Official Video\", \"Remaster\", \"【插曲】\" i \"- Topic\" + Regex filter za tekst pjesme + Regularni izraz primijenjen samo na originalni tekst pjesme (ne na prijevode ili romanizaciju). Ostavite prazno da onemogućite + Markeri linija informacija + Ako su tekstovi pjesme pogrešno filtrirani na dno, prilagodite ili obrišite ovo. Markeri odvojeni zarezom: linije koje sadrže marker nakon kojeg slijedi dvotačka prikazuju se kao informacije o autorima na dnu. Ostavite prazno da onemogućite + Veliko početno slovo + Piše prvo slovo svakog reda teksta velikim slovom + Ukloni krajnju interpunkciju + Uklanja tačke, zareze, dvotačke i tačka-zareze na kraju redova teksta pjesme. Upitnici, uzvičnici, tri tačke, navodnici i zagrade se zadržavaju + Veličina teksta + Veličina teksta teksta pjesme + Globalni pomak vremena + Sinhronizirani tekstovi prate reprodukciju. Ako odstupaju, prilagodite vremenski pomak. Pomjerite sinhronizirane tekstove unaprijed ili nazad. Koristite pozitivnu vrijednost ako tekstovi ispred prednjače pjesmi + Koristi SponsorBlock segmente za pomak teksta pjesme + Uskladi bez segmenata koje SponsorBlock preskače i pomakni tekst na objavljeni audio zapis. Isključeno potpuno ignoriše SponsorBlock + Ova traka nema video ID za slanje + Izvor: %s + Dugim pritiskom na dugme za kopiranje sačuvajte ovaj LRC; ako možete precizirati tajminge i podijeliti ih s community provajderima, bili bismo zahvalni + Preveo Google + Romanizirao Google + Preveo %s + Romanizovao %s + Romanizirao %s + Prevedeno + Jezik prevoda + Jezik romanizacije + Prevođenje… + Nije bilo moguće prevesti tekst pjesme + Romanizovano + Automatski + Romanizacija… + Tekst pjesme nije mogao biti romanizovan + Samo prevedeno + Samo romanizovano + Dualni režim + Kopirano u međupamćenik + Osvježavanje + Osvježeno + QuickLRC API ključ + QuickLRC transkribuje ili vremenski usklađuje tekstove za trenutnu pjesmu. To je zasebna usluga, nepovezana sa gore navedenom OpenAI kompatibilnom krajnjom tačkom + QuickLRC ključ je sačuvan. Koristite dugme za usklađivanje u dijalogu za pretragu tekstova, ili prompt u panelu kada pjesma nema tekst + QuickLRC API ključ je sačuvan + QuickLRC nije prihvatio taj ključ + QuickLRC API ključ je obrisan + QuickLRC određuje vrijeme za tekstove koji ga nemaju, ili ih transkribuje iz pjesme, uz trošak kredita. Kada je ključ postavljen, pjesme bez teksta nude transkripciju, a dijalog za pretragu dobija dugme za usklađivanje + Zatraži QuickLRC poravnanje + Poravnavanje + Primijeni poravnanje + Zatraži QuickLRC transkripciju + Transkribovanje + Transkripcija otkazana + Samo pjesme puštene s YouTube-a mogu se transkribovati + Nema tekstova za usklađivanje - umjesto toga se vrši transkripcija + QuickLRC nije mogao procesuirati ovu pjesmu + QuickLRC krediti za ovu pjesmu su potrošeni + "Tekstovi pjesama se podudaraju prema naslovu i izvođaču; neobični metapodaci mogu rezultirati time da ništa ne bude pronađeno ili da bude pronađena pogrešna verzija. Koristite osvježavanje za promjenu verzija. Provajderi označeni sa * zahtijevaju token. Svi omogućeni provajderi nisu uvijek pretraživani vašim redoslijedom, već odgovaraju na osnovu svoje brzine.\n\nTekstovi pjesama dolaze iz usluga trećih strana koje nisu povezane s Morphe, sponzorisane niti podržane od strane Morphe. Svaka se koristi onakvama kakva jeste: tekstovi, njihova dostupnost i njihovo licenciranje ostaju kod tih usluga i njihovih licencera, a njihovo korištenje može biti podložno njihovim uslovima pružanja usluge. Ono što stiže do omogućene usluge je samo pretraga koju ste tražili - naslov, izvođač i, gdje ta usluga to zahtijeva, ID videa. Opcioni AI endpoint i QuickLRC transkripcija pokreću se samo nakon što ih konfigurišete i prime tekst pjesme ili URL videa traka." + Omogućite samo pružaoče usluga koje koristite ili zadržite zadane postavke - konstantni zahtjevi mogu preopteretiti manje zajedničke stranice sa tekstovima pjesama + Sakrij liniju kredita + Sakrij liniju kredita koja se prikazuje ispod teksta pjesme + Zamijeni prevod i romanizaciju + UKLJUČENO stavlja romanizaciju ispod teksta pjesme, a prevod iznad; ISKLJUČENO drži romanizaciju iznad, a prevod ispod + Prevod + Romanizacija + Provajder prevoda + Google (podrazumijevano, besplatno) + OpenAI kompatibilno (prilagođeno) + Provajder romanizacije + Google (podrazumijevano) + Lokalni / Offline rječnik + OpenAI kompatibilno (Prilagođeno) + AI sekcija + Koristi se samo kada pružatelj ne šalje oznake sekcija kao što su Refren ili Strofa, koje zatim detektuje vaš OpenAI-kompatibilni model + AI & Cloud krajnje tačke + Konfigurišite API ključeve, modele, krajnje tačke i šablone upita za AI provajdere. + URL API krajnje tačke (npr. https://api.openai.com/v1/chat/completions) + Šablon prompta + Placeholderi: {language}, {task}, {title}, {artist}, {count}, {lines} + Prompt je sačuvan + Prompt je sačuvan, ali AI krajnja tačka još uvijek nije upotrebljiva. Prvo je konfigurišite u AI postavkama + Prompt je resetovan + Nije validan JSON. Popravite ga ili ispraznite polje prije spremanja + Postavke prevoda & romanizacije su sačuvane + Postavke su vraćene na zadane vrijednosti + Bazni URL mora početi sa http:// ili https:// + Testiraj vezu + Konfigurišite krajnje tačke + Prikaži sve krajnje tačke + Validiranje konekcije… + Provajder prevoda + Provajder romanizacije + Preuzmi modele + Preuzimanje modela… + Odaberite model + Preuzimanje modela nije uspjelo. Provjerite API ključ ili vezu. + Postavke Gemini API-ja + OpenAI kompatibilno (prilagođena krajnja tačka) + Postavke DeepL API-ja + API ključ + Model + Osnovni URL + API ključ (opcionalno) + Prompt za prevod + Dodatno tijelo zahtjeva (opcionalni JSON) + Autentikacijski ključ (Besplatni ili Pro) + Konfigurišite API ključeve, modele, krajnje tačke i promptove + 8.23.51 - Onemogući podebljene ikone + 9.13.51 - Vrati kartice Sljedeće, Tekstovi pjesama i Srodno u plejeru diff --git a/patches/src/main/resources/addresources/values-bs-rBA/reddit/strings.xml b/patches/src/main/resources/addresources/values-bs-rBA/reddit/strings.xml index 8baee53f12..caf5922771 100644 --- a/patches/src/main/resources/addresources/values-bs-rBA/reddit/strings.xml +++ b/patches/src/main/resources/addresources/values-bs-rBA/reddit/strings.xml @@ -16,8 +16,122 @@ Second \"item\" text" --> + Reddit politika privatnosti + Google prijava nije dostupna. Koristite e-mail za prijavu Reklame + Sakrij oglase u feedu + Sakrij reklame u komentarima + Navigacijska traka + Sakrij dugme Odgovori + Sakrij dugme Chat + Sakrij dugme Kreiraj + Sakrij dugme Istraži ili Zajednice + Sakrij dugme Igre + Bočna traka + Sakrij policu Nedavno posjećeno + Sakrij policu Igre na Redditu + Sakrij policu Reddit Pro + Sakrij policu O aplikaciji + Sakrij policu Resursi + Izgled + Kopiraj tekst + Omogućava odabir i kopiranje teksta u objavama i komentarima. Onemogućava skupljanje komentara + Skupljanje komentara će prestati raditi dok je opcija Kopiraj tekst omogućena + Prilagođeni font + Učitajte prilagođenu TTF ili OTF datoteku fonta sa putanje ispod. Omogućavanje ove opcije također isključuje Prisilni sistemski font + Datoteka prilagođenog fonta + Dodirnite da odaberete TTF ili OTF datoteku fonta + Odabrani font - %s + Prvo odaberite datoteku prilagođenog fonta + Molimo odaberite .ttf ili .otf datoteku fonta + Nije uspjelo učitavanje prilagođenog fonta. Odaberite drugu datoteku + Onemogući moderno korisničko sučelje početne stranice + Onemogući iskačući prozor za snimku ekrana + Onemogućava iskačući prozor koji se pojavljuje prilikom pravljenja snimke ekrana + Forsiraj sistemski font + Koristite sistemski font uređaja umjesto Reddit Sans-a ili Roboto-a. Omogućavanje ove opcije također isključuje Prilagođeni font + Sakrij dugme Pitaj + Sakriva dugme \"Ask\" u traci za pretragu + Sakrij policu zajednica + Sakriva policu povezanih ili predloženih zajednica u subredditovima + Sakrij Trending police + Sakriva Trending police iz feeda i prijedloga za pretragu + Ukloni dijalog za prijedloge obavijesti + Automatski zatvara dijalog sa prijedlogom obavještenja prilikom posjete subredditu + Prikaži flairove u početnom feedu + Prikazuje bedževe za flair objava u početnom feedu + Neke stavke u feedu možda neće prikazivati flair + Prikaži broj pregleda na objavama + Ograničenje: Dostupno samo ako API odgovor uključuje broj pregleda + Preokreni traku s radnjama objave + Pomjera dugmad za glasanje i komentisanje objava na desnu stranu, a dugmad za crosspost i dijeljenje na lijevu stranu + Zamijeni gumbe za glasanje i komentisanje + Prikazuje gumb za komentisanje objava prije gumba za glasanje + Ukloni dijalog upozorenja za NSFW sadržaj + Automatski prihvata upozorenje za NSFW sadržaj prilikom posjete subredditu + Razno O Morphe + Verzija zakrpe i službeni linkovi + Očisti linkove za dijeljenje + Uklanja parametre upita za praćenje iz podijeljenih poveznica + Otvaraj poveznice direktno + Zaobilazi URL-ove preusmjeravanja prilikom otvaranja vanjskih poveznica + Mediji u feedu pune širine + Prikazuje slike i videozapise od ruba do ruba u feedu i detaljima objave, bez bočnog razmaka i zaobljenih uglova + Jačina zatamnjivanja preglednika medija + Jačina tamnog zatamnjivanja iza natpisa prilikom pregledavanja slika i videozapisa, od 0 (bez zatamnjivanja) do 1 (Redditov zadanost) + Zadrži poziciju feed-a + Zadržava feed na istoj poziciji nakon zatvaranja objave, slike ili videa, umjesto pomjeranja otvorene objave na vrh + Maksimalna visina medija u feed-u + Maksimalna visina slika, videa i galerija u feed-u, kao množilac njihove širine. Viši mediji će biti isječeni. 1.33 je Redditov standard + Reprodukuj Redgifse sa zvukom + Reprodukuje originalni Redgifs video umjesto Redditove kopije, koja često nema zvuk + Redgifs HD kvalitet + Reprodukuje HD Redgifs video. HD videi su veći i mogu se pauzirati radi učitavanja + Lokacija preuzimanja + Svi računi + Redditov standard: slike u Pictures/Reddit, videi u Download + Mapa: %s + Naziv datoteke: %s + Redditovi nazivi datoteka + Koristi \"Sve račune\" + Koristi vlastitu lokaciju + Sačuvaj u + Redditovi zadani folderi The + Podfolder + Dodajte podfoldere pomoću / u nazivu datoteke + Pretraži… + Na ovom uređaju nije dostupan birač foldera + Naziv datoteke + Redditovo ime datoteke + Dodirnite token da biste ga umetnuli. / kreira podfolder. Nepoznate vrijednosti postaju \"unknown\" + Slike: %s + Videozapisi: %s + Sačuvaj + Ponovi + Obriši + Ponoviti lokaciju preuzimanja za sve račune na Redditovu zadanu lokaciju? + Obrisati lokaciju preuzimanja za %s? + Nije bilo moguće sačuvati na lokaciju preuzimanja. Umjesto toga je sačuvano u Redditov zadani folder + Mapa za preuzimanje više nije dostupna. Umjesto toga je sačuvano u Redditovom zadanim mapi + Preuzeti video nije moguće sačuvati + Ova mapa se ne može koristiti za preuzimanja + %s (trenutno) + %s (niste prijavljeni) + Anonimno (odjavljeno ili inkognito) + Sakrij overlay za pregled medija + Sakriva naslov, dugmad i kontrole videa prilikom otvaranja slike ili videa. Dodirnite medij koji želite prikazati + Sakrij dugme \"See the conversation\" + Sakriva dugme \"Pogledaj konverzaciju\" na dnu pregledača medija + Crna pozadina pregledača medija + Koristi crnu pozadinu iza slika i videozapisa u pregledaču medija, umjesto boje pozadine teme + Otvaraj poveznice izvan aplikacije + Otvara poveznice u vašem pretraživaču umjesto u pretraživaču unutar aplikacije + Zapamti sklopljene komentare + Komentari koje sklopite ostaju sklopljeni kada ponovo otvorite objavu + Ikona aplikacije + Izaberite ikonu aplikacije The + Nepoznato diff --git a/patches/src/main/resources/addresources/values-bs-rBA/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-bs-rBA/shared-youtube/strings.xml index 1dfd978911..099235b2d3 100644 --- a/patches/src/main/resources/addresources/values-bs-rBA/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-bs-rBA/shared-youtube/strings.xml @@ -18,49 +18,297 @@ Second \"item\" text" --> + Animacija pokretanja + Onemogućeno + Boja + Monohromatska + + Ponovo pokreni za završetak konfiguracije + Naziv aplikacije + Pozadina tamne teme + Čisto crna + Material You (Čisto crna) + Klasični YouTube + Tamno ružičasta + Tamno plava + Tamnozelena + Tamnožuta + Tamno narandžasta + Tamno crvena + Prilagođena tamna pozadina + Postavite boju pozadine tamne teme + Zadano za aplikaciju + Material You (Neutralna) + Material You (Primarna) + Material You (Sekundarna) + Material You (Tercijarna) + Prilagođeno + Preuzimanja + Režim preuzimanja The + Preuzmi u aplikaciji + Eksterni preuzivač + Naziv paketa preuzivača + Naziv paketa vaše instalirane eksterne aplikacije za preuzimanje + Unesite naziv paketa + Ostalo + Aplikacija nije instalirana + %s nije instaliran. Molimo instalirajte ga + "Nije moguće pronaći instaliranu aplikaciju s nazivom paketa: %s + +Provjerite je li naziv paketa ispravan i je li aplikacija instalirana" + Naziv paketa ne može biti prazan + Prilagođeno + Ikona aplikacije + Original + Morphe crna + Morphe tamna + Morphe svijetla + Morphe play svjetlo + Morphe play crna + Prilagođeno + "Ovo mijenja ikonu prikazanu u pokretaču aplikacija. + +Ova postavka neće promijeniti ikonu na sljedećim lokacijama: +• Lista aplikacija u sistemskim postavkama uređaja +• Određene push obavijesti +• Birač aplikacija 'Otvori pomoću' +• Android Auto ikona + +Da biste promijenili ikonu svuda, ponovite patch i promijenite opciju patcha 'Ikona aplikacije'" + Ikona obavještenja + Usklađivanje s ikonom aplikacije + Morphe play + Morphe obavještenje + Vaša historija gledanja se ne sprema.<br><br>Ovo je najvjerovatnije uzrokovano DNS blokatorom oglasa ili mrežnim proxyjem.<br><br>Da biste ovo popravili, dodajte <b>s.youtube.com</b> na bijelu listu ili isključite sve DNS blokatore i proxyje + Ne prikazuj ponovo + Simuliraj video streamove + Konfigurišite imitaciju klijenta kako biste spriječili probleme sa reprodukcijom + "Ispravlja probleme sa reprodukcijom za većinu korisnika zahtijevanjem kompatibilnog video strima + +Ako ste YouTube Premium korisnik, ovo podešavanje možda nije potrebno" + Nije bilo moguće preuzeti klijentske strimove + Zadani klijent ne radi + "%1$s nije radio nekoliko puta zaredom, ali je %2$s radio. + +Postaviti %2$s kao zadani klijent?" + Promijeni + Isključivanje ove postavke može uzrokovati probleme sa reprodukcijom + Pokušajte omogućiti: Spoof video strimova > Prijava u Android VR + Zadani klijent + Prijava u Android VR + Niste prijavljeni na Android VR. Dodirnite ovdje da se prijavite + Trenutno ste prijavljeni na Android VR + Da biste koristili ovu funkciju, promijenite \'Zadani klijent\' u Android VR + Prijavite se na Android VR + "Prijava na Android VR može riješiti probleme sa reprodukcijom za neke korisnike + +Da biste se prijavili, pritisnite „nastavi“ ispod" + Odjavi se + Uputstva + "Kod za aktivaciju: %s + +1. Dodirnite \"Otvori web-stranicu\" u nastavku da biste otvorili web-stranicu \"Povežite uređaj\" +2. Zalijepite iz zaslonka (ili upišite) kod za aktivaciju koji je prikazan iznad +3. Prijavite se na Google ako vam se to zatraži. Ako imate više računa, odaberite ovaj YouTube račun +4. Dodirnite \"Dopusti pristup YouTube VR-u\" +5. Vratite se u ovu aplikaciju nakon što vidite poruku \"Uređaj je povezan\"" + Krenula je greška prilikom izdavanja koda za aktivaciju + Krenula je greška prilikom izdavanja tokena za autentifikaciju + Uspješno ste se prijavili + "Autentifikacija je sačuvana + +• Vaša Morphe postavke neće uvoziti/izvoziti Android VR autentifikaciju +• Ako se ova aplikacija ne koristi 6 mjeseci, Google će ukloniti ovu autentifikaciju +• Morat ćete se ponovo prijaviti ako promijenite svoju Google lozinku ili odjavite ovaj uređaj iz svojih Google postavki" + OAuth2 trenutno nije dostupan (API je istekao) + Android VR prijava je istekla, pokušajte se ponovo prijaviti na VR + OAuth2 trenutno nije dostupan (status %s) + OAuth2 trenutno nije dostupan + Nije moguće prijaviti se na VR, autentifikacija putem web stranice nije odobrena + Nije moguće prijaviti se na VR: %s + Odjavljeno sa Android VR-a + Nevalidan token za VR prijavu, pokušajte se ponovo prijaviti na VR + Kontrole medija u obavijestima + Prilagodite kontrole reprodukcije koje se prikazuju na vašem zaključanom ekranu i panelu obavještenja + Sakrij dugmad Prethodno & Sljedeće + Sakriva dugmad Prethodno i Sljedeće u obavještenju o medijima, na zaključanom ekranu i kontrolama slušalica + Onemogući traku napretka + Onemogućava povlačenje trake napretka na zaključanom ekranu i panelu obavještenja + Pretraživanje pomoću dugmadi Prethodno & Sljedeće + Mijenja kontrole za obavijest o medijima, ekran zaključavanja i slušalice kako bi pretraživale umjesto mijenjanja videozapisa + Dužina vraćanja + Sekunde za vraćanje pomoću dugmeta Prethodno + Dužina brzog pomaka + Sekunde za brzom pomak pomoću dugmeta Sljedeće + Dužina pretraživanja mora biti između 1 i %s sekundi. Koristi se zadanja vrijednost + Onemogući QUIC protokol + Onemogući DRC audio + Forsiraj originalni jezik audia + Uvijek reproducira originalni audio zapis videa umjesto sinkronizirane verzije + Preskoči tišinu + Automatski preskače tihe pauze u audio zapisu videa + Minimalno trajanje pauze + Samo pauze koje traju najmanje ovoliko će biti skraćene. Stupa na snagu kada sljedeći video zapadne + Mrežni proxy + Usmjerite podržane mrežne zahtjeve preko proxyja + Omogući proxy + Usmjerava podržane mrežne zahtjeve preko konfiguriranog HTTP ili HTTPS proxyja + Proxy host + Naziv hosta ili IP adresa proxy servera + Proxy port + Port proxy servera + Koristi HTTPS proxy + Koristi HTTPS za povezivanje na proxy server umjesto HTTP-a + Proxy autentifikacija + Šalje Basic autentifikaciju uz proxy zahtjeve + Korisničko ime proxyja + Korisničko ime za osnovnu proxy autentifikaciju + Lozinka proxyja + Lozinka za osnovnu proxy autentifikaciju + Dozvoli direktno povratno rješenje + Poveži se direktno ako proxy ne uspije + Sakrij oglase preko cijelog ekrana + Da biste koristili ovu funkciju, promijenite \'Spoof video streams\' na bilo koji klijent osim \'Android Studio\' i \'Android VR Downgraded\' + Očisti linkove za dijeljenje + Uklanja parametre upita za praćenje iz linkova za dijeljenje + Promijeni linkove za dijeljenje u youtube.com + Zamjenjuje music.youtube.com sa youtube.com u podijeljenim linkovima + Promijeni linkove za dijeljenje u youtu.be + Prisiljava videozapise da koriste skraćeni youtu.be URL format + Debug log protocol buffer + Dodaje dodatne informacije o zapisivanju za programere kako bi identifikovali komponente aplikacije + "Omogućavanjem ove postavke bilježit će se dodatni podaci o izgledu, uključujući tekst na ekranu za neke UI komponente. + +Ovo može pomoći u identifikaciji komponenti prilikom kreiranja prilagođenih filtera. + +Međutim, omogućavanjem ovoga također će se bilježiti neki korisnički podaci kao što je vaša IP adresa" + Prilagođeni filter + Konfigurišite prilagođene filtere kako biste sakrili bilo koju komponentu + Omogući prilagođeni filter + Lista stringova za izgradnju putanje komponente koje treba filtrirati, odvojenih novim redovima + Neispravan prilagođeni filter: %s + Filter menija postavki + Sakrij stavke na standardnom ekranu s postavkama aplikacije prema njihovom vidljivom nazivu + Otvorite standardni ekran postavki aplikacije jednom kako biste popunili listu + Ukloni dijalog o diskreciji gledatelja + Ovo ne zaobilazi ograničenje starosti. Samo je automatski prihvata + PoToken provider + Konfigurišite PoToken provider kako biste spriječili probleme sa reprodukcijom + Eksterni PoToken provider + "Popravlja probleme sa reprodukcijom koristeći eksterni PoToken minter (Eksperimentalno) + +Ako ste korisnik YouTube Premium usluge, ovo podešavanje možda nije potrebno" + "Popravlja probleme s reprodukcijom koristeći vanjski PoToken minter (Eksperimentalno) + +Da biste koristili ovu funkciju, isključite 'Lažiraj video streamove'" + Ako ne radi, pokušajte prisilno zatvoriti MicroG + Otvori PotHelper postavke + "Ova funkcija je trenutno u razvoju i možda neće raditi + +Dodirnite ovdje da preuzmete PotHelper" + PotHelper više ne radi + "Token ugrađen u ovu verziju PotHelper-a više ne radi + +Uključite 'Spoof video streamova' kako biste popravili reprodukciju" + Uključi + Lažiranje verzije aplikacije + Prevari server tako što će misliti da koristite stariju verziju aplikacije kako biste vratili zastarjele funkcije korisničkog interfejsa + "Verzija aplikacije će biti imitirana kao starija verzija. + +Ovo će promijeniti izgled i funkcije aplikacije, ali mogu se pojaviti nepoznati sporedni efekti. + +Ako se kasnije isključi, preporučuje se brisanje podataka aplikacije kako bi se spriječili problemi sa korisničkim interfejsom" + Cilj za lažiranje verzije aplikacije + Brzina osvježavanja aplikacije The + %s FPS + Ručno postavite brzinu osvježavanja + Uvijek + Reprodukcija u portretnom načinu + Reprodukcija u punom ekranu + Reprodukcija u portretnom i punom zaslonu + Font The + Sistemski font uređaja + Prilagođena datoteka fonta + Prilagođena datoteka fonta + Odaberite TTF ili OTF datoteku fonta + Odabrana datoteka nije TTF ili OTF font + Prilagođeni font nije mogao biti učitan i isključen je + Dislike-ovi trenutno nisu dostupni (API je istekao) + Dislike-ovi nisu dostupni (status %s) + Dislike-ovi nisu dostupni (limit klijentskog API-ja) + Dislike-ovi nisu dostupni (%s) + Sakrio vlasnik + Ne sviđa se ovaj video, kao i još %s drugim osobama + Ne sviđa se ovaj video, %s glasova su negativni + Omogući Return YouTube Dislike + Prikaži dislike-ove kao procenat + Prikazuje dislike-ove kao procenat like-ova umjesto broja + Prikaži procijenjene like-ove + Prikazuje procijenjeni broj like-ova na videima gdje je kreator onemogućio like-ove + Prikaži upozorenja o grešci u povezivanju + Prikazuje obavijest kada Return YouTube Dislike server nije dostupan + "Return YouTube Dislike vraća broj dislike-ova koji je YouTube sakrio 2021. godine. Brojevi dolaze iz podataka arhiviranih prije uklanjanja, a za novije videozapise procjenjuju se na osnovu glasova korisnika, tako da su približni i ažuriraju se svakih nekoliko dana + +ID videozapisa koji gledate šalje se Return YouTube Dislike API-ju, a vaši like-ovi i dislike-ovi se šalju anonimno kako bi se poboljšale procjene" + Return YouTube Dislike API statistika ovog uređaja + Vrijeme odgovora API-ja, prosjek + Vrijeme odgovora API-ja, minimum + Vrijeme odgovora API-ja, maksimum + Vrijeme odgovora API-ja, posljednji medij + Dislike-ovi privremeno nisu dostupni - na snazi je ograničenje stope Client API-ja + API preuzimanje glasova, broj poziva + Nije napravljeno mrežnih poziva + %s mrežnih poziva je izvršeno + API preuzimanje glasova, broj timeout-a + Nijedan mrežni poziv nije istekao + %s mrežnih poziva je isteklo + Ograničenja stope API klijenta + Nisu pronađena ograničenja stope klijenta + Klijentsko ograničenje brzine je naišlo %s puta + %s milisekundi diff --git a/patches/src/main/resources/addresources/values-bs-rBA/shared/strings.xml b/patches/src/main/resources/addresources/values-bs-rBA/shared/strings.xml index d47c99fd08..1a215eff51 100644 --- a/patches/src/main/resources/addresources/values-bs-rBA/shared/strings.xml +++ b/patches/src/main/resources/addresources/values-bs-rBA/shared/strings.xml @@ -17,23 +17,219 @@ Second \"item\" text" --> + Postavke + Jeste li sigurni da želite nastaviti? + Spremi + Ponovi + Ponovi boju + Nevažeća boja + Potreban ponovni pokret + Ponovo pokreni aplikaciju kako bi ova promjena stupila na snagu + Ponovni pokret + Uvezi + Kopiraj + Morphe postavke su vraćene na zadane + Uvezeno %s postavki + Uvoz nije uspio: %s + Navigiraj prema gore + Pretraži postavke O Morphe + Nema rezultata za \'%s\' + Pokušajte s drugom ključnom riječi + Nedavne pretrage + Ukloni iz historije pretrage? + Ukloni iz historije pretrage + Obriši historiju pretrage + Jeste li sigurni da želite obrisati cijelu historiju pretrage? + Savjeti za pretragu + "• Dodirnite putanju da biste navigirali do nje +• Dugo pritisnite postavku da biste navigirali do nje +• Pritisnite Enter da biste sačuvali upit za pretragu u historiju +• Pretraga ignoriše velika i mala slova kao i interpunkciju +• Nadređene postavke pojavljuju se iznad onemogućenih podređenih postavki" + Historija pretrage je prazna + Da biste sačuvali historiju pretrage, upišite upit za pretragu i pritisnite Enter + Prikaži historiju pretrage postavki + Naziv Morphe postavki The + Dodaci + Tvicovi + Vrijeme morfinga + Prilagođeno + Unesite naziv + Naziv ne može biti prazan + Prikaži Morphe ikone menija + Kopirano: %s + Morphe jezik + "Prevodi za neke jezike mogu nedostajati ili biti nepotpuni. + +Da biste preveli nove jezike ili poboljšali postojeće prevode, posjetite morphe.software/translate" + Jezik aplikacije + Uvoz / Izvoz + Uvoz / Izvoz Morphe postavki + Uvezi iz datoteke + Postavke su uspješno uvežene, dodirnite Spremi da ih primijenite + Neuspješno uvoženje postavki + Izvezi u datoteku + Postavke su uspješno izvezene + Neuspješno izvoženje postavki + Koristite najnoviju verziju Morphe Patches <i>%s</i> + Koristite Morphe Patches verziju <i>%1$s</i><br><br><b>Dostupno ažuriranje: <i>%2$s</i></b><br><br>Da ažurirate svoje zakrpe, koristite Morphe kako biste ponovo zakrpili ovu aplikaciju + Napomena + Ova verzija je pred-izdanje i možete naići na neočekivane probleme + Zvanični linkovi + Web stranica + Doniraj + Prijevodi + Zasluge + Ažurirano + Dostupno ažuriranje + Trenutni razvoj + Prethodni razvoj + Aktivni razvoj + 2022 ‑ 2025 + 2022 ‑ 2025 + 2019 ‑ 2022 + Obavijesti o licenci + Učitavanje… + Otklanjanje grešaka + Konfigurišite napredne opcije za programere i dijagnostičke dnevnike + Omogući debug zapisivanje + Stack trace dnevnika ispravljanja grešaka + Uključuje stack trace-ove u debug dnevnicima kako bi pomoglo u identifikaciji grešaka + Prikaži obavještenje pri Morphe grešci + "Isključivanje toast obavještenja o greškama skriva sva Morphe obavještenja o greškama. + +Nećete biti obaviješteni o neočekivanim događajima" + Izvezi debug dnevnike + Izvezi Morphe debug dnevnike + Debug dnevnici uspješno izvezeni + Neuspješan izvoz debug dnevnika + Pretraži debug zapise… + Obriši + Debug zapisivanje je onemogućeno + Nisu pronađeni debug dnevnici + Debug dnevnici kopirani + Neuspješan izvoz debug dnevnika: %s + Debug dnevnici obrisani + Menadžer zastavica funkcionalnosti + Upravljanje boolean zastavicama funkcionalnosti + Pretraži zastavice funkcionalnosti… + Neaktivno (%s) + Aktivno (%s) + Blokirano (%s) + Prisilno (%s) + Auto + Isključeno + Uključeno + Prisilno uključi + Blokiraj + Ukloni + Zastavice funkcionalnosti spremljene + Zastavice funkcionalnosti su resetovane + Zastavice funkcionalnosti su kopirane u međupamćenik + Ponovo pokrenite aplikaciju da biste koristili ovo podešavanje + Izvoz i uvoz + Uvezeno %s zastavica funkcija + Nisu pronađene zastavice funkcija + Zastavice funkcija su uspješno izvezene + Izvoz zastavica funkcija nije uspio + Zastavice funkcija su učitane, dodirnite Spremi da primijenite + Čitanje datoteke nije uspjelo + Binarna pretraga + "Pronalazi koja zastavica funkcije uzrokuje ponašanje aplikacije. + +Polovica od %s zastavica je blokirana, a nakon svakog ponovnog pokretanja navodite da li je ponašanje i dalje prisutno. + +Reprodukujte ponašanje jednom prije početka, inače će u pretrazi nedostajati zastavice koje aplikacija još nije zatražila" + Započni + "Nema zastavica funkcionalnosti za pretraživanje. + +Samo zastavice koje je aplikacija aktivirala, a koje trenutno nisu prisilno uključene, mogu se koristiti u binarnoj pretrazi" + "Korak %1$s + +%2$s zastavica preostalo, %3$s od njih je blokirano za ovaj korak. + +Ponovo pokrenite aplikaciju, provjerite da li je ponašanje prisutno, a zatim odgovorite ispod" + Pretraga zastavica funkcionalnosti je u toku + Ponašanje je prisutno + Ponašanje nije prisutno + Otkaži binarnu pretragu + Binarna pretraga je otkazana + "Zastavica funkcionalnosti %s uzrokuje ponašanje. + +Sada je blokirana" + "Nije pronađena zastavica funkcionalnosti. + +Čini se da ponašanje aplikacije nije kontrolisano zastavicom funkcionalnosti" + "Nije pronađena ni jedna zastavica funkcionalnosti. + +Ponašanje je nestalo ranije u pretrazi, tako da može biti uključeno više zastavica ili je jedan od odgovora bio pogrešan" + Otvorite postavke MicroG + MicroG nije instaliran. Instalirajte ga + Potrebna akcija + "Vaša verzija MicroG v%1s je zastarjela i može uzrokovati probleme. + +Molimo ažurirajte MicroG na najnoviju verziju v%2s" + Ažuriraj + Ignoriši + "Instalirani MicroG nije preporučen od strane Morphe: + +%1s + +Dodirnite da ga deinstalirate, zatim ponovo otvorite ovu aplikaciju i instalirajte MicroG RE" + Deinstaliraj + "MicroG nema dozvolu za rad u pozadini. + +Pratite \"Don't kill my app\" vodič za svoj telefon i primijenite instrukcije na vašu MicroG instalaciju. + +Ovo je neophodno da bi aplikacija radila" + Otvori web stranicu + "Optimizacije baterije za MicroG moraju biti onemogućene kako bi se spriječili problemi. + +Onemogućavanje optimizacije baterije za MicroG neće negativno uticati na potrošnju baterije. + +Dodirnite dugme za nastavak i dopustite izmjene optimizacije" + Nastavi + Prikaži dijalog za optimizaciju baterije za MicroG + "Onemogućavanjem ovoga sakrit će se MicroG zahtjev za optimizaciju baterije. + +Ako se optimizacije baterije ne promijene, može doći do sljedećeg: +• Aplikacija se može nasumično zatvoriti +• Obavijesti možda neće raditi +• Reprodukcija u pozadini možda neće raditi" + Rukovanje poveznicama aplikacije + Konfigurišite linkove koji se otvaraju u ovoj aplikaciji + Korak 1: Onemogući originalne poveznice aplikacije + Otvorite postavke originalne aplikacije i onemogućite sve poveznice domena + Korak 2: Omogući poveznice zakrpljene aplikacije + Otvorite postavke zakrpljene aplikacije i omogućite sve poveznice domena + Otvori postavke + 🚨 Eksperimentalno upozorenje 🚨 + "Koristite eksperimentalnu verziju aplikacije ⚠️ <b>%1$s</b> +<br/><br/> +🔧 Očekujte neobično ponašanje aplikacije ili neidentifikovane greške dok preciziramo zakrpe za ovu verziju aplikacije. +<br/><br/> +Ako želite iskustvo bez najviše problema, tada <b>deinstalirajte</b> ovo i instalirajte preporučenu verziju aplikacije ✅ <b>%2$s</b>" + ⚠️ Želim eksperimentalno + ✅ Želim stabilno + Zapis o padu aplikacije je sačuvan u korisničkim dokumentima: %s + Zapis o padu aplikacije je sačuvan: %s diff --git a/patches/src/main/resources/addresources/values-bs-rBA/sponsorblock/strings.xml b/patches/src/main/resources/addresources/values-bs-rBA/sponsorblock/strings.xml index a1c1761d21..5aed16449e 100644 --- a/patches/src/main/resources/addresources/values-bs-rBA/sponsorblock/strings.xml +++ b/patches/src/main/resources/addresources/values-bs-rBA/sponsorblock/strings.xml @@ -16,8 +16,216 @@ Second \"item\" text" --> + Omogući SponsorBlock + SponsorBlock je crowdsourced sistem za preskakanje dosadnih dijelova YouTube videa + Izgled + Prikaži dugme za glasanje + Dodaje dugme za pozitivno ili negativno glasanje za segmente + Koristi kvadratni izgled + Koristi kvadratne uglove za SponsorBlock dugmad i kontrole + Koristi kompaktno dugme za preskakanje + Smanjuje širinu dugmeta za preskakanje kako bi uštedjelo prostor + Automatski sakrij dugme Preskoči + Sakriva dugme za preskakanje nakon nekoliko sekundi umjesto da ga drži vidljivim tokom cijelog segmenta + Trajanje dugmeta Preskoči + Postavite koliko dugo dugme Preskoči ostaje vidljivo + Prikaži obavještenje o preskakanju + Prikazuje obavještenje kada je segment automatski preskočen + Trajanje obavještenja o preskakanju + Postavite koliko dugo obavještenje o poništavanju ostaje vidljivo + 1 sekunda + 2 sekunde + 3 sekunde + 4 sekunde + 5 sekundi + 6 sekundi + 7 sekundi + 8 sekundi + 9 sekundi + 10 sekundi + Prikaži dužinu videa bez segmenata + Oduzima sve segmente od ukupnog trajanja videa prikazanog na traci trajanja + Kreiranje novih segmenata + Prikaži dugme \'Kreiraj novi segment\' + Dodaje dugme u video plejer koje vam omogućava da pošaljete nove SponsorBlock segmente + Korak prilagođavanja vremena + Iznos pomaka dugmadi za prilagođavanje vremena (u milisekundama) + Vrijednost mora biti pozitivan broj + Smjernice za slanje + Smjernice sadrže pravila i savjete za kreiranje novih segmenata + Pratite smjernice + Pročitajte SponsorBlock smjernice prije kreiranja novih segmenata + Već pročitano + Pokaži mi Opširno + Prikaži upozorenja o grešci u povezivanju + Prikazuje obavijest kada SponsorBlock server nije dostupan + Omogući praćenje broja preskakanja + Šalje broj preskakanja na SponsorBlock tabelu lidera kako bi se pratilo koliko je vremena uštedjeno + Minimalno trajanje segmenta + Segmenti kraći od ove vrijednosti (u sekundama) neće biti prikazani niti preskočeni + Nevalidno trajanje vremena + Vaš privatni korisnički ID + Ovo treba ostati privatno. Ovo je kao lozinka i ne smije se dijeliti ni s kim. Ako neko posjeduje ovo, može se izdavati za vas + Privatni korisnički ID mora imati najmanje 30 znakova + Promijeni API URL + Adresa koju SponsorBlock koristi za pozive serveru + Bijela lista kanala + SponsorBlock je onemogućen za kanale na bijeloj listi + Obavijesti na kanalu s bijele liste + Prikazuje obavijest kada je SponsorBlock onemogućen za kanal na bijeloj listi + SponsorBlock je onemogućen za ovaj kanal + Resetovanje API URL-a + API URL je nevažeći + API URL je promijenjen + Postavke uvoza / izvoza + Kopiraj + Vaša SponsorBlock JSON konfiguracija koja se može uvesti / izvesti u Morphe i druge SponsorBlock platforme + "Vaša SponsorBlock JSON konfiguracija koja se može uvesti / izvesti u Morphe i druge SponsorBlock platforme + +Ovo uključuje vaš privatni korisnički ID. Budite oprezni s dijeljenjem ovih informacija" + Postavke su uspješno uvezeno + Uvoz nije uspio: %s + Izvoz nije uspio: %s + "Vaša podešavanja sadrže privatni SponsorBlock korisnički ID. + +Vaš korisnički ID je kao lozinka i nikada ga ne biste trebali dijeliti" + Ne prikazuj ponovo + Promijeni ponašanje segmenta + Sponzor + Plaćena promocija, plaćeni referali i direktni oglasi. Nije za samopromociju ili besplatno reklamiranje ciljeva / kreatora / web stranica / proizvoda koji im se sviđaju + Neplaćena / Samopromocija + Slično kao Sponzor, osim da je za neplaćenu / samopromociju. Uključuje sekcije o prodaji robe, donacijama ili informacije o tome s kim su sarađivali + Podsjetnik na interakciju (Pretplata) + Kratki podsjetnik da zaprate, pretplate ili zaprate kreatora usred sadržaja. Ako je dugačak ili se odnosi na nešto specifično, trebao bi biti pod samopromocijom + Istaknuti dio + Dio videa koji većina ljudi traži + Intermezo / Uvodna animacija + Interval bez stvarnog sadržaja. Može biti pauza, statičan kadar ili ponavljajuća animacija. Ne uključuje prijelaze koji sadrže informacije + Završni ekrani / Kreditna imena + Krediti ili kada se pojave YouTube završne kartice. Ne za zaključke s informacijama + Uvod / Pozdravi + Pričani najavi za nadolazeći video, pozdravi i oproštaji. Ne uključuje sekcije koje dodaju dodatni sadržaj + Pregled / Rekapitulacija + Kolekcija isječaka koji pokazuju šta slijedi ili šta se dogodilo u videu ili u drugim videima serije, gdje su sve informacije ponovljene na drugom mjestu + Tangente / Šale + Tangencijalne scene ili šale koje nisu potrebne za razumijevanje glavnog sadržaja videa. Ne uključuje sekcije koje pružaju kontekst ili detalje u pozadini + Muzika: Sekcija bez muzike + Samo za upotrebu u muzičkim spotovima. Sekcije muzičkih spotova bez muzike koje već nisu obuhvaćene drugom kategorijom + Preskoči + Istaknuto + Preskoči sponzora + Preskoči promo + Preskoči interakciju + Preskoči do istaknutog dijela + Preskoči uvod + Preskoči prekid + Preskoči prekid + Preskoči završnice + Preskoči hook + Preskoči pregled + Preskoči pregled + Preskoči rezime + Preskoči tangent + Preskoči nemuziku + Preskoči segment + Preskočen sponzor + Preskočena samopromocija + Preskočeno dosadno podsjećanje + Preskočeno do istaknutog dijela + Preskočen uvod + Preskočen predah + Preskočen predah + Preskočen završetak + Preskočen uvodni dio + Preskočen pretpreview + Preskočen uvod + Preskočen rezime + Preskočeno odstupanje + Preskočen odjeljak bez muzike + Preskočen nepredati segment + Preskočeno više segmenata + Preskači automatski + Preskači automatski jednom + Prikaži dugme Preskoči + Prikaži u traci trajanja + Onemoguči + Nije moguće poslati segment: %s + SponsorBlock je privremeno nedostupan + Nije moguće poslati segment (status: %1$s %2$s) + Nije moguće poslati segment. Ograničenje brzine (previše zahtjeva sa istog korisnika ili IP adrese) + Segment se ne može poslati: %s + "Segment se ne može poslati. +Već postoji" + Segment je uspješno poslan + SponsorBlock je privremeno nedostupan (API timeout) + SponsorBlock trenutno nije dostupan (status %s) + SponsorBlock je privremeno nedostupan + Nije moguće glasati za segment (API je istekao) + Nije moguće glasovati za segment (status: %1$s %2$s) + Nije moguće glasati za segment: %s + Glasaj za + Glasaj protiv + Promijeni kategoriju + Nema segmenata za glasanje + %1$s do %2$s + Odaberite kategoriju segmenta + Kategorija je onemogućena u podešavanjima. Omogućite kategoriju da biste poslali segment + Novi SponsorBlock segment + Postavite %s kao početak ili kraj novog segmenta? + Početak + Kraj + Sada + Vrijeme početka segmenta + Vrijeme završetka segmenta + Jesu li vremena ispravna? + "Segment je od + +%1$s +do +%2$s + +(%3$s) + +Spremni za slanje?" + Pošalji + Početak mora biti prije kraja + Prvo označite lokaciju na vremenskoj traci + Pregledajte segment i uvjerite se da preskače glatko + Ručno uredite vrijeme segmenta + Želite li urediti vrijeme početka ili kraja segmenta? + Uneseno je neispravno vrijeme + Pošalji istaknuti dio + "Je li vrijeme istaknutog dijela ispravno? + +%s + +Ako ne šaljete istaknuti dio, dodirnite Otkaži i označite drugu lokaciju" + Pošalji istaknuti dio + Statistika + Statistika trenutno nije dostupna (API je nedostupan) + Učitavanje… + SponsorBlock je onemogućen + Vaše korisničko ime: <b>%s</b> + Dodirnite ovdje da promijenite svoje korisničko ime + Nije moguće promijeniti korisničko ime: Status: %1$s %2$s + Korisničko ime je uspješno promijenjeno + Vaša reputacija je <b>%.2f</b> + Kreirali ste <b>%s</b> segmenta + Dodirnite ovdje da vidite svoje segmente + SponsorBlock tabela lidera + Sparingali ste ljude od <b>%s</b> segmenata + Dodirnite ovdje da vidite globalnu statistiku i najbolje doprinosioce + To je <b>%s</b> njihovih života.<br>Dodirnite ovdje da vidite tabelu lidera + Preskočili ste <b>%s</b> segmenata + To je <b>%s</b> + Resetujte brojač preskočenih segmenata? + Prozirnost: + Boja: + "SponsorBlock je crowdsourced baza podataka video segmenata kao što su sponzori, uvodi, završnici i podsjetnici za pretplatu. Segmente šalje i glasa zajednica, a baza je otvorena i besplatna za preuzimanje + +ID videa koji gledate šalje se SponsorBlock API-ju kako bi se dobili njegovi segmenti" diff --git a/patches/src/main/resources/addresources/values-bs-rBA/youtube/strings.xml b/patches/src/main/resources/addresources/values-bs-rBA/youtube/strings.xml index 68bd74274d..fa3e910cd0 100644 --- a/patches/src/main/resources/addresources/values-bs-rBA/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-bs-rBA/youtube/strings.xml @@ -24,221 +24,1633 @@ Second \"item\" text" Opširno Plejer Shorts + Traka za traženje + Kontrole prevlačenja + Razno + Video + Vratite stari ekran s postavkama YouTube-a + Ukloni ograničenja reprodukcije u pozadini + Onemogući reprodukciju u pozadini + Automatska pauza kada je uređaj zaključan + Isključeno + Uvijek pauziraj kada je zaključano + Pauziraj osim ako bežični audio nije povezan + Onemogući ograničenje brzine skrolovanja + Onemogući ažuriranja izgleda + Sprječava server da ažurira izgled aplikacije + "Izgled aplikacije se vraća na izgled koji je korišten pri prvoj instalaciji. + +Neki izgledi na strani servera se možda neće vratiti. + +Promjene uključuju: +• Komponente u iskačućem meniju playera (ili povezane postavke) možda neće raditi +• Brojevi koji se vrte nisu animirani +• Tab Biblioteka je vidljiv +• Muzički dio opisa videa možda neće raditi +• Stavke iskačućeg menija playera se ne mogu sakriti +• Dugme za promjenu računa se možda neće pojaviti na tabu Biblioteka" + Omogući filter flyout menija feed-a + Filter flyout menija feed-a + Unesite nazive flyout menija za filtriranje, razdvojene novim redovima + Omogući filter donjeg menija Biblioteka / Vi + Filter donjeg menija Biblioteka / Vi + Unesite nazive donjeg menija Biblioteka / Vi po kojima želite filtrirati, razdvojene novim redovima + Sakrij kartice albuma + Skriva kartice albuma ispod opisa izvođača + Sakrij kartice izvođača + Skriva karticu izvođača na vrhu rezultata pretrage + Sakrij dugmad kanala + Sakriva dugmad kao što su Pretplati se, Pogledaj kanal, Zajednica i YouTube Music iz profila kanala u rezultatima pretrage + Sakrij objave zajednice + Sakrij kompaktne banere + Skriva kompaktne banere i razmake između određenih komponenti + Proširive kartice ispod videozapisa + Prikaži sve + Sakrij samo karticu sa sažetkom + Sakrij sve + Sakrij plivajuće dugme mikrofona + Skriva plutajućudugme mikrofona u donjem desnom uglu ekrana s prijedlozima za pretragu + Sakrij dugme \"Pređi na Premium\" + Sakrij policu Historije + Sakrij horizontalne police + Sakriva horizontalne police kao što su Najnovije vijesti, Objava zajednice koje vam mogu biti zanimljive, Nastavite gledati, Istražite više kanala, Istražite više tema, Za vas, Najnoviji videozapisi, Najrelevantnije, Objave iz zajednice kreatora, Kupovina i Pogledajte ponovo + Sakrij policu slika + Skriva police slika u rezultatima pretrage + Sakrij karticu \"Pozovite druge da šalju poruke\" + Sakriva karticu poziva koja se prikazuje u odjeljku Poruke kartice Obavještenja + Sakrij gumb \"Najnoviji videozapisi\" + Sakriva plavi baner \'Najnoviji videozapisi\' koji se pojavljuje na vrhu početnog feeda + Sakrij prijenose uživo + Sakriva videozapise koji su trenutno uživo u feedu početne stranice i rezultatima pretraživanja + Sakrij mix plejliste + Sakrij odjeljak sa filmovima + Sakrij zaglavlje menija Obavještenja + Sakriva zaglavlje menija Obavještenja prilikom klika na dugme zvona + Sakrij dugme \"Obavijesti me\" + Sakriva taster za podsjetnik za zakazane prijenose uživo i premijere + Sakrij Playables + Sakriva funkciju koja vam omogućava igranje igara + Sakrij sličice pojmova za pretragu + Sakriva sličice u listi automatskog dopunjavanja pretrage + Sakrij dugme \"Prikaži više\" + Sakriva dugme za proširenje ispod videozapisa u rezultatima pretrage + Sakrij traku pretplaćenih kanala + Sakrij nazive pretplaćenih kanala + Sakriva nazive kanala u traci pretplaćenih kanala + Sakrij ankete + Sakrij ankete i podsjetnike o točnosti preporuka feed-a + Sakrij policu tiketa + + Sakrij baner \"Nedostupni videozapisi su skriveni\" + Skriva baner \"Nedostupni videozapisi su skriveni\" u listama reprodukcije + Sakrij vrijeme učitavanja + "Ograničenja: +• Shorts police, stranice kanala i rezultati pretrage i dalje mogu prikazivati vrijeme učitavanja +• Ne radi s automobilskim izgledom" + Sakrij sličicu videozapisa + Sakrij oznake videa + Sakriva oznake kao što su \'Automatski prevedeno\' / \'Prevedeno\', \'Zarađuje proviziju\', \'Hype\', \'Ljudi su gledali i\', \'proizvod(i)\' i \'Možda će vam se svidjeti\' + Sakrij broj pregleda + "Ograničenja: +• Shorts police, stranice kanala i rezultati pretrage i dalje mogu prikazivati broj pregleda +• Ne radi s automobilskim izgledom" + Sakrij rezultate pretrage veba + Sakrij odjeljak \'Rezultat s weba\' iz rezultata pretraživanja + Sakrij sekciju \"Možda će vam se svidjeti\" + Sakrij YouTube Doodles + Sakriva sezonske animacije i animacije zasnovane na događajima na YouTube logotipu + Prikazivanje YouTube Doodles-a može spriječiti prikazivanje prilagođenih zaglavlja + Sakrij traku kanala + Sakrij vodeni žig kanala + Sakrij poglavlja i dugme \'U ovom videu\' + Sakrij okvir za crowdfunding + Sakrij okvire za hitne slučajeve + Sakrij info panele + Sakriva informativne kontekstualne panele, kao što su medicinske informacije + Sakrij dugme Pridruži se + Sakrij gumb \"Ponovna reprodukcija chata uživo\" + Sakriva dugme za ponovni prikaz chata u fullscreen načinu rada + Sakrij medicinske panele + Sakrij smjernice za gestove playera + Sakriva smjernice koje se prikazuju tokom gestova playera, kao što su \"Povucite prema gore za precizno traženje\", \"Pustite za otkazivanje\" i \"2x\" + Sakrij snackbar + Sakrij smjernice za pretplatnike + Sakrij dugme \"Sinkronizuj sa videom\" + Sakrij gradijent za dodir i držanje + Sakriva gradijent kontrola plejera prilikom pomjeranja prsta dok držite za ubrzanje + Sakrij vremenski ograničene reakcije + Skriva funkciju koja korisnicima omogućava reagovanje emojijima u odjeljku sa komentarima i u live chatu + Sakrij meni \'Pomoć & povratne informacije\' + Sakriva meni \'Pomoć & povratne informacije\' pri korištenju dugmeta sa tri tačke, kao što je slučaj na rezultatima pretrage + Sakrij handle + Sakriva handle vašeg kanala u kartici \'Vi\' + Sakrij naslov videa + Skriva naslov videa u fullscreen režimu + Očisti titlove videa + Uklanja fraze kao što su \'#\', \'Prikupljanje sredstava\', \'proizvod(i)\' i \'Prodavnica\' iz podnaslova videa + Sakrij gumb \'Pitaj Gemini\' + Sakriva gumb \'Pitaj Gemini\' na dnu panela \'Pitaj o ovom videu\' + Opis videa + Odaberite koje komponente se pojavljuju u opisu videa + Sakrij odjeljak \"AI-generisani sažetak videa\" + Sakrij odjeljak Ask + Sakrij odjeljak Attributes + Skriva odjeljke Featured places, Games, Music i People mentioned + Sakrij odjeljak poveznica kanala + Sakrij odjeljak Chapters + Sakrij odjeljak Corrections + Sakrij odjeljak \"Napredak kursa\" + Sakrij sekciju Explore + Sakriva sekcije Explore this course i Explore the podcast + Sakrij sekciju \'Explore this course\' + Sakrij sekciju \'Explore the podcast\' + Sakrij odjeljak istaknutih kanala + Sakrij sekciju Featured links + Sakrij odeljak istaknutih plejlista + Sakrij sekciju Featured videos + Sakrij odjeljak sa hashtagovima + Sakrij odjeljak \"Kako je ovo napravljeno\" + Sakrij Hype bodove + Sakrij sekciju Info cards + Sakrij sekciju \'Ključni koncepti\' + Sakrij sekciju Quizzes + Sakrij odjeljak \"Pretraži unutar ovog videozapisa\" + Sakrij dugme Pretplati se + Sakrij sekciju Transcript + Sakrij sekciju \'Video details\' + Trakom za filtriranje + Odaberite gdje želite sakriti traku za skrolovanje filtera + Sakrij na stranici kanala + Sakrij u komentarima + Sakrij u feedovima + Sakrij u srodnim videozapisima + Sakrij u rezultatima pretrage + Sakrij u historiji gledanja + Stranica kanala + Odaberite koje komponente se pojavljuju na stranici kanala + Omogući filter kartice kanala + Filter kartice kanala + Unesite nazive kartica kanala po kojima želite filtrirati, razdvojene novim redovima + Sakrij dugme Zajednica + Sakrij dugme Pridruži se + Sakrij pregled poveznica + Skriva klikabilne web poveznice u zaglavlju kanala + Sakrij oznaku \'Samo za članove\' + Sakriva oznaku \'Samo za članove\' u traci filtera kanala + Sakrij policu članova + Skriva listu članova kanala + Sakrij policu objava + Sakrij dugme Prodavnica + Sakrij dugme Pretplati se + Meni Istražite + Odaberite koje komponente se pojavljuju u meniju Istražite + Sakrij meni Tečajevi + Sakrij dugme Istraži + Sakrij meni \"Moda & ljepota\" + Sakrij Gaming meni + Sakrij Hype meni + Sakrij Learning meni + Sakrij Live meni + Sakrij meni Članstva + Sakrij meni \"Filmovi & TV\" + Sakrij meni Muzika + Sakrij meni Vijesti + Sakrij meni Playables + Sakrij meni Podkaste + Sakrij meni Kupovina + Sakrij meni Sport + Sakrij YouTube Premium meni + Sakrij YouTube Studio meni + Sakrij YouTube Music meni + Sakrij YouTube Kids meni + Sakrij meni YouTube Create + Sakrij meni \"Kako YouTube radi\" + Sakrij pravne i privatnosti podnožje + Komentari + Odaberite koje komponente se pojavljuju u odjeljku komentara + Omogući filter vrtulje komentara + "• Obavezno unesite tačan naziv stavke u vrtešci (npr. Pitaj, Hype ovaj video, Slušaj na YouTube Music) +• Sakrivanje stavki u vrtešci može imati neželjene sporedne efekte +• Ako su sve stavke u vrtešci sakrivene, cijeli odjeljak sa komentarima će također biti sakriven" + Filter vrtulje komentara + Unesite nazive vrtuljaka komentara po kojima želite filtrirati, razdvojene novim redovima + Sakrij AI sažetak chata + Sakrij smjernice kanala + Sakrij kontekstualne poruke + Sakrij zaglavlje \"Komentari članova\" + Sakrij odjeljak s komentarima + Sakrij odjeljak komentara u početnom feedu + Sakrij Pravila zajednice + Sakrij dugme \"Kreiraj Short\" + Sakrij dugme za Dislike + Sakrij dugme za emojije + Sakrij opcije trake za filtriranje + Sakriva sve opcije trake za filtriranje osim \'Najpopularnije\' i \'Najnovije\' + Sakrij animaciju poklona i kartice + Sakrij dugme za poklon + Sakrij Info dugme + Sakrij traku donatora u chatu uživo + Sakrij dugme za pomjeranje na dno live chata + Sakrij tooltipove za live chat + Sakriva tooltip prozore koji se prikazuju pored trake za unos chata, kao što je obavještenje o chatu samo za pretplatnike + Sakrij gumb Meni + Sakriva gumb izbornika s 3 točke za svaki komentar + Sakrij prethodni prikaz komentara + Skriva pregled komentara ispod videa prije otvaranja cijelog odjeljka komentara + Komentar je sakrio Morphe… + Omogući dugme za minimalističke komentare + Sakriva klizne kartice i njihov selektor stranica, koji se prikazuju na dugmetu za komentare + Sakrij dugme Zahvali + Sakrij dugme za vremenski žig + Sakrij dugme \"Top fans\" + Sakrij gumb Prevedi + Sakrij poruku \"Još nema komentara\" + Očisti istaknute poveznice pretraživanja + Čisti istaknute poveznice pretraživanja, ostavljajući samo originalni obični tekst + Filter ključnih riječi + Odaberite specifične riječi ili fraze za skrivanje videa u feedovima i pretrazi + Sakrij komentare prema ključnim riječima + Sakrij početne videozapise prema ključnim riječima + Sakrij rezultate pretrage prema ključnim riječima + Sakrij videozapise pretplata prema ključnim riječima + Ključne riječi za skrivanje + "Unesite ključne riječi i fraze koje želite sakriti, razdvojene novim redovima + +Ključne riječi mogu biti bilo koji tekst prikazan u naslovima videa ili nazivima kanala + +Riječi sa velikim slovima u sredini moraju se tačno podudarati (npr. iPhone, TikTok, LeBlanc)" + "Početna / Pretplata / Pretraga / Komentari su filtrirani kako bi se sakrio sadržaj koji odgovara ključnim riječima + +Ograničenja: +• Shorts se ne mogu sakriti prema nazivu kanala +• Neke UI komponente možda neće biti skrivene +• Pretraživanje ključne riječi može prikazati nula rezultata" + Podudaranje cijelih riječi + Opkruživanje ključne riječi/fraze navodnicima spriječit će djelmična podudaranja naslova videozapisa i imena kanala<br><br>Na primjer,<br><b>\"ai\"</b> će sakriti video: <b>How does AI work?</b><br>ali neće sakriti: <b>What does fair use mean?</b> + Ne može se koristiti ključna riječ: %s + Dodajte navodnike da biste koristili ključnu riječ: %s + Ključna riječ ima suprotstavljene deklaracije: %s + Ključna riječ je prekratka i zahtijeva navodnike: %s + Ključna riječ će sakriti sve videozapise: %s + "• Početna: %s +• Preplate: %s +• Pretraga: %s +• Komentari: %s +• Ukupno: %s" + Statistika + Sakriveni videozapisi, zadnjih 24 sata + Sakriveni videozapisi, ukupno + Resetuj brojač za zadnjih 24 sata? + Resetovati svevremenski brojač? + AI filter kanala + Sakrij videozapise sa kanala koje je AiSList zajednička lista označila kao sadržaj generisan vještačkom inteligencijom + Lista blokiranja: Visoko povjerenje + Lista upozorenja: Umjereno povjerenje + Sakrij videozapise na Početnoj + Sakrij videozapise u pretrazi + "• Početna: %s +• Pretraga: %s +• Ukupno: %s" + Kanali na crnoj listi + Kanali na listi upozorenja + Omogućite filter za učitavanje kanala + Učitavanje kanala… + "Početna stranica i rezultati pretrage su filtrirani kako bi se sakrili videozapisi s kanala koje je označio AiSList + +Ograničenja: +• Filtrirani AI Shorts mogu se prikazivati kao prazan prostor +• Neki AI videozapisi se i dalje mogu prikazivati" + "AiSList je lista YouTube kanala otvorenog koda koji objavljuju sadržaj generisan vještačkom inteligencijom, koju održavaju volonteri. Kanale prijavljuju korisnici i oni se pregledaju prije dodavanja + +Lista se preuzima sa GitHub-a, a videozapisi se filtriraju na vašem uređaju" + Prijavi AI kanale + Dodaj unos u izvještaj u video meni + Dodaj unos u video meni koji prijavljuje kanal videa u AiSList + Ime podnosioca + Ime koje se prikazuje pored vaših prijava na javnoj AiSList tabeli lidera. Prijavljivanje nije dostupno dok je ovo prazno + Prijavi AI kanal + "Pošaljite %s u AiSList, koristeći ovaj video kao dokaz? + +Prijavite samo kanale koji: +• Koriste AI glasove, vizuale, skripte ili muziku kao svoj glavni sadržaj +• Masovno proizvode sadržaj pomoću AI alata uz minimalan ljudski rad + +Ne prijavljujte kanale koji samo diskutuju o AI-u ili koriste AI za određene dijelove svojih videa. + +Prijava je javna i pregledana prije nego što kanal bude dodat na listu. Uklanjanje je moguće samo putem zahtjeva na GitHub-u" + Ovaj kanal se već nalazi na AiSList blok-listi + Ovaj kanal se već nalazi na AiSList listi upozorenja + Prijava + Postavite ime pošiljača u postavkama filtera AI kanala kako biste prijavili kanale + Tražim kanal… + Kanal ovog videa nije pronađen + Kanal je prijavljen i čeka pregled + AiSList nije mogao sačuvati prijavu, pokušajte ponovo kasnije + Poslano je previše prijava, pokušajte ponovo kasnije + AiSList je odbio prijavu + Prijava nije uspjela, provjerite svoju vezu + Bijela lista oglasa kanala + Videozapisi sa kanala na bijeloj listi reproduciraju se s oglasima, kako bi njihovi kreatori bili plaćeni. Oglasi ostaju blokirani na svim ostalim kanalima + Dodaj kanal na bijelu listu oglasa + Ukloni kanal sa bijele liste oglasa + Sakrij policu trgovine kreatora + Sakrij baner trgovine na završnom ekranu + Sakrij opće oglase + Sakrij banere za merchendise + Sakrij oznake plaćene promocije + Sakrij popup oglase plejera + Sakrij samosponzorisane kartice + Sakrij shopping poveznice + Sakrij video oglase + Sakrij YouTube Premium promocije + Učitavanje… + Trenutni kanal: + Dodaj na bijelu listu + Ukloni + Kanali na bijeloj listi + Nijedan kanal nije na bijeloj listi + Kanal je dodan na bijelu listu + Kanal je uklonjen sa bijele liste + Pretraži unutar kanala + Dodaje drugu prepaidu za pretragu na stranicama kanala kako bi se pretraživao samo taj kanal + Pretraži kanal + Rezultati pretrage kanala + Na ovom kanalu ništa nije pronađeno + Pretraga ovog kanala nije uspjela + Videozapisi + Objave + Nema pronađenih objava na ovom kanalu + Relevantnost + Najviše pregledano + Najmanje pregledano + Najnovije + Najstarije + Najkraći Short + Najduži + Link je kopiran u međumemoriu + Link sa vremenskim oznakom je kopiran u međumemoriu + Prikaži dugme Kopiraj link videa + Dodirnite da kopirate link videa, dodirnite i držite da kopirate sa vremenskom oznakom + Kopiraj vremensku oznaku po zadanim postavkama + Zamjenjuje akcije dodira i dodira-i-držanja dugmeta \'Kopiraj link videa\' + Kopiraj naslov videa + Dodirnite i zadržite naslov videa da biste ga kopirali + Naslov videa je kopiran u međupamćenik + Kopiraj komentare + Dodirnite i zadržite komentar da biste ga kopirali + Komentar je kopiran u međupamćenik + Dodirnite i zadržite za otvaranje menija videa + Dodirivanjem i držanjem bilo gdje na videu u feed-u otvara se njegov meni + Prikaži dugme Gledaj kasnije na sličicama + Dugme Gledaj kasnije prikazuje se na sličicama videa u feedu. Pritiskanjem i držanjem otvara se opcija Sačuvaj u plejlistu + Preuzmi slike objava + Dugme za preuzimanje prikazuje se pored dugmeta Dijeli za objave sa slikama ili GIF-ovima, a stavka za preuzimanje je dodana u njihov meni + Preuzmi slike + Preuzmi sliku + Preuzimanje %s slika… + Preuzimanje slike… + %1$s slika je sačuvano u %2$s + Slika je sačuvana u %s + Slika nije mogla biti preuzeta + U ovoj objavi nije pronađena slika + Dodaj stavku Pitaj u flyout meni + Dodaje stavku za sažetak videa u flyout meni feed-a + "Pitaj zahtijeva prijavljen račun u zemlji u kojoj je Pitaj dostupno. + +Nakon što se jednom otvori video s dugmetom Pitaj, funkcija radi sa svim videima čak i ako trenutna zemlja nema funkciju Pitaj." + Sažmi video + Nije bilo moguće otvoriti Pitaj za ovaj video + Ask nije dostupno za vaš račun ili trenutnu državu + Prikaži broj pregleda na sličicama + Broj pregleda videa u feed-u prikazuje se u donjem lijevom uglu njihovih sličica umjesto ispod naslova. Neki videi, kao oni u policama, možda neće prikazivati broj pregleda + Prikaži broj lajkova na sličicama u feedu + Prikaži postotak lajkova na sličici feed-a + Pretraži komentare + Dodirnite dugme za pretragu u komentarima kako biste prikazali samo učitane komentare koji sadrže sve pretražene riječi. Stavite riječi pod navodnike kako biste pretražili frazu + Pretraži komentare + Riječi za pretragu + Pretraži + Obriši + Promijenite redoslijed sortiranja ili ponovo otvorite komentare kako biste ih pretražili + Pretraga je očišćena. Promijenite redoslijed sortiranja ili ponovo otvorite komentare kako biste prikazali sve komentare + Onemogući iskačući prozor \'Prijavite se na TV\' + Onemogući iskačući prozor \"Povežite svoje uređaje\" + Onemogući preskakanje poglavlja dvostrukim dodirom + Onemogućava gest dvostrukog dodira dvama prstima za preskakanje poglavlja, koji se može slučajno aktivirati jednim prstom + Preuzmite videozapise, podkaste ili pjesme pomoću aplikacija trećih strana za preuzimanje + Prikaži preklopno dugme za preuzimanje u playeru + Prikazuje dugme u video playeru koje otvara vaš eksterni downloader + Brzo preuzimanje na dodir + Zahtijeva preuzimanje u pozadini. Dodirnite i držite da otvorite program za preuzimanje. Radi sa YTDLnis i YTindir + Označi preuzimanja kao pogledana + Videozpreuzeti pomoću eksternog preuzivača dodaju se u vašu YouTube historiju pregleda + Full HD (1080p) + Srednja (192 kbps) + Brzo preuzimanje + Video + Audio + Sve opcije + Prikaži preuzimanje u Shorts meniju + Dodaje stavku menija za eksterni preuzimač u Shorts plejer + Preuzmi + Glasovni prevod + Naglas pročitaj titlove videa koristeći uređajnu pretvorbu teksta u govor, sinhronizovano sa reprodukcijom + Prikazuje overlay dugme u video playeru. Dodirnite za prebacivanje prevoda, dodirnite i držite za brza podešavanja + Jezik + Provajder prevoda + Glasnoća originalnog audio zapisa + Glasnoća originalnog YouTube audio zapisa dok prevod govori + Glasnoća prevoda + Glasnoća glasa sinhronizovanog prevoda + Maksimalna brzina govora + Maksimalni množilac brzine prilikom uklapanja govora u slot za titlove + "Besplatan API za prevođenje, nije potreban API ključ. Do 5.000 znakova dnevno u besplatnom nivou, ili do 50.000 znakova dnevno uz e-mail adresu. + +Dodirnite ovdje da otvorite MyMemory.translated.net" + MyMemory e-mail + Opcionalno. Unesite svoju MyMemory e-mail adresu kako biste povećali dnevni limit sa 5.000 na 50.000 znakova. Ostavite prazno da biste koristili anonimni limit + "Plaćeni AI prevod putem OpenRouter-a. Dopunite kredit jednom i prevedite hiljade videozapisa. Mistral Nemo je najjeftinija opcija, Gemma 4 nudi najviši kvalitet. + +Dodirnite ovdje da otvorite OpenRouter.ai" + OpenRouter API ključ + Vaš OpenRouter API ključ. Kreirajte ključ na OpenRouter.ai > Workspace > API Keys + OpenRouter model + Prilagođeno + OpenRouter nije konfigurisan + Unesite svoj OpenRouter API ključ u postavkama prije odabira ovog pružaoca + Besplatno + %s po 100 sati videa + Glas + Sistem + Višejezični muški + Višejezični ženski + Muški + Ženski + Vrijeme je za morfing! + Prevod nije dostupan + "Usluga prevođenja je vratila HTTP grešku %s. Vaša IP adresa je vjerovatno ograničena na do 24 sata. + +Da biste nastavili koristiti prevode, prebacite se na drugog sağlayevala prevođenja u postavkama ili koristite VPN" + Ne prikazuj ponovo + "Vaše postavke sadrže OpenRouter API ključ. + +Vaš API ključ nikada ne smije biti podijeljen. Svako ko ima pristup njemu može koristiti vaše OpenRouter kredite" + Dostignut limit OpenRouter kredita + Dostigli ste limit OpenRouter kredita. Dopunite stanje ili povećajte limit potrošnje ključa na OpenRouter.ai + Otvori OpenRouter.ai + Prevod OpenRouter-a nije uspio (HTTP %s) + Dnevni limit MyMemory-ja je dostignut + Sljedeće dostupno u %s + Iskoristili ste današnju besplatnu MyMemory kvotu za prevod (5.000 znakova). Dodajte MyMemory e-mail u postavkama kako biste povećali dnevni limit na 50.000 znakova, ili prebacite na drugu uslugu prevoda + Iskoristili ste današnju MyMemory kvotu za prevod (50.000 znakova). Prebacite na drugu uslugu prevoda ili pokušajte ponovo sutra + Prevod MyMemory-ja nije uspio (status %s) + Omogući ponavljanje videa + Ne zapamti ponavljanje videa + Spreči da se trenutno stanje ponavljanja videa sačuva za naredne videozapise + Prikaži dugme za ponavljanje videa + Ponavljanje videa je uključeno + Ponavljanje videa je isključeno + Opseg petlje + Početak + Kraj + [hh:]mm:ss + [hh:]mm:ss (prazno = kraj videa) + Postavi + Obriši + Petljanje %1$s - %2$s + Nevalidan vremenski opseg + Prikaži dugme Slika-u-Slici + Prikaži meni Slika-u-Slici + Prebaci na Slika-u-Slici + Prikaži dugme Pusti sve + "Dodirnite za generisanje plejliste svih video zapisa sa kanala, dodrnite i držite za poništavanje + +Info: Možda neće raditi na prijenosima uživo" + Režim generisanja plejliste + Svi sadržaji (Sortiraj po vremenu, uzlazno) + Svi sadržaji (Sortiraj po vremenu) + Svi sadržaji (Sortiraj po popularnosti) + Samo videozapisi (Sortiraj po vremenu) + Samo videozapisi (Sortiraj po popularnosti) + Samo Shorts (Sortiraj po vremenu) + Samo Shorts (Sortiraj po popularnosti) + Samo streamovani videozapisi (Sortiraj po vremenu) + Samo streamovani videozapisi (Sortiraj po popularnosti) + Samo sadržaji za članove + Videozapisi samo za članove + Shorts samo za članove + Live streamovi samo za članove + Nije moguće generisati plejlistu zbog neslaganja ID-a kanala + Prikaži dugme za ponovno učitavanje videa + ID videa nije dostupan. Molimo ručno ponovite učitavanje videa + Prikaži dugme Spremi za kasnije + Dodirnite da sačuvate u Gledaj kasnije, dodirnite i zadržite za opcije reda čekanja + Video je uspješno sačuvan u listi \"Gledaj kasnije\" + Ovaj video je već sačuvan u listi \"Gledaj kasnije\" + Nemoguće sačuvati ovaj video u listi \"Gledaj kasnije\" + Sačuvaj u Gledaj kasnije + Prikaži dugme Sačuvaj za kasnije u iskačućem prozoru postavki plejera + Omogućuje spremanje trenutnog videa koji gledate u \"Gledaj kasnije\" iz iskačućih postavki videa + Prikaži dugme Sačuvaj u Gledaj kasnije u dječjim videozapisima + Omogućuje spremanje videa za djecu u \"Gledaj kasnije\" iz iskačućeg izbornika + Prikaži gumb Spremi za kasnije u iskačućem izborniku za Shorts + Omogućava spremanje Shortsa u \"Gledaj kasnije\" iz izbornika + Preusmjeri \"Pogledaj kasnije\" na plejlistu + Pogledaj kasnije + Dodaj u Morphe red čekanja + Zadrži red čekanja između sesija + Red čekanja se zadržava nakon ponovnog pokretanja aplikacije + Zamijeni akcije Spremi i Redaj + Dodirnite za opcije reda čekanja, dodirnite i zadržite da sačuvate u Gledaj kasnije + Vrsta \'Reprodukuj sljedeće u redu\' The + Izvorni red + Morphe red čekanja + Morphe red čekanja na strani klijenta + Dodaj stavku menija reda čekanja u iskačući prozor feed-a + Dodaje dodatnu stavku u iskačući prozor. Omogući ovo ako \"Dodaj u red čekanja\" nije prikazano + Red + Red čekanja (%s) + Obriši sve + Ništa nije u redu + Koristite „Reprodukuj sljedeće u redu“ na bilo kojem videu + Trenutno se reprodukuje + Reprodukuj sljedeće u redu + Reprodukuj posljednje u redu + Promijeni redoslijed + Ukloni + Dodano u red + Ovaj video nije moguće dodati u red + Plejliste se ne mogu dodati u red + Red je pun + Video nije moguće pokrenuti + Bijela lista oglasa u iskačućem meniju + Dodaje stavku u iskačući meni koja omogućava prikaz oglasa na kanalu tog videa + Ova funkcija može zahtijevati korištenje PotHelpera ili promjenu opcije \"Spoof video streams\" na TV ili Android VR prijavu + Bijela lista brzine u iskačućem meniju + Dodaje stavku u iskačući izbornik koja uvijek pušta kanal tog videa brzinom 1.0x + Niste prijavljeni + Nema ID-a liste reprodukcije + Red je prazan + Nema ID-a videa + Neuspješno: %s + Neuspješno dodavanje u red + Neuspješno kreiranje reda + Neuspješno uklanjanje iz reda + Neuspješno spremanje reda + Dodano u red + Red je kreiran + Uklonjeno iz reda + Red čekanja je sačuvan u %s + Morphe red čekanja + Dodaj u red čekanja + Dodaj u red čekanja i otvori + Dodaj u red čekanja i pusti + Dodaj i ponovo učitaj video + Ukloni iz reda čekanja + Ukloni i otvori red čekanja + Ukloni i ponovo učitaj video + Otvori red čekanja + Spremi red čekanja u listu pjesama + Playlista je sačuvana + Sačuvano u %s + Očisti red čekanja + Red čekanja je očišćen + Red čekanja + Reprodukuj posljednje u redu + Reprodukuj sljedeće u redu + YouTube nije mogao prikazati red čekanja. + Red čekanja nije spreman za ovaj video. Otvorite video plejer i pokušajte ponovo. + Dodaj Mix u red čekanja ili gledaj Mix? + Gledaj + Dodaj u red čekanja + Učitavanje Mix-a… + Otvori kanal iz avatara uživo + Sprečava otvaranje prijenosa uživo prilikom dodira avatara kanala + Onemogući gestu za precizno traženje + Onemogućava režim preciznog traženja koji se aktivira povlačenjem trake za traženje prema gore + Omogući DVR za prijenose uživo + Omogućava premotavanje na svim prijenosima uživo, čak i na onima koji to inače ograničavaju + Omogući sličice trake za pomjeranje + Prikazuje sličicu videa prilikom povlačenja trake za pomjeranje + Najviše puštano + "\"Omogući DVR za prijenose uživo\" možda nije dostupno + +Da biste ovo popravili, uključite \"Lažiraj video strimove\" i promijenite \"Podrazumijevani klijent\" na \"TV Simply\" ili \"visionOS\"" + Proširi trajanje DVR-a za prijenose uživo + Omogućava vam previjanje prijenosa uživo do 7 dana unazad + "'Proširi trajanje DVR-a za prijenose uživo' možda nije dostupno + +Da biste ovo popravili, uključite 'Spoof video streamova' i promijenite 'Podrazumijevani klijent' u 'TV Simply' ili 'visionOS'" + Omogući prevlačenje za traženje + Omogućava klizanje za traženje umjesto reprodukcije brzinom 2x prilikom pritiskanja i držanja video plejera + Omogući dodir za traženje + Samo dodirnite traku za traženje kako biste skočili na tu poziciju bez njenog povlačenja + Gesta prevlačenja ulijevo + Gesta prevlačenja udesno + Gesta prevlačenja prema gore + Širina lijeve/desne zone + Postotak svake ivice ekrana rezervisan za gesture povlačenja lijevo i desno (5-50) + Jasnoća + Glasnoća + Izvorna + Isključeno + Ignoriši povlačenja kada je zaključano + Gesture povlačenja se ignorišu dok je plejer zaključan opcijom \'Zaključaj ekran\' + Omogući gestu pritisni za prevlačenje + Zahtijeva pritiskanje i držanje prije nego što se prepozna gesta prevlačenja + Omogući haptički povratni signal + Sačuvaj i vrati osvjetljenost + Pamti nivo osvjetljenja prilikom ulaska / izlaska iz fullscreen načina + Omogući gestu za auto-osvjetljenje + Prevlačenje osvjetljenja na najnižu vrijednost aktivira sistemsko automatsko osvjetljenje + Automatski + Vremensko ograničenje overlay-a za prevlačenje + Broj milisekundi tokom kojih je overlay vidljiv + Prozirnost pozadine overlay-a za prevlačenje + Prilagodi nivo prozirnosti (0 je prozirno, 100 je neprozirno) + Boja svjetline overlay-a za prevlačenje + Postavi boju trake napretka kontrola osvjetljenja + Boja overlay-a za glasnoću pri prevlačenju + Postavi boju trake napretka kontrola glasnoće + Boja brzine overlay prevlačenja + Postavite boju trake napretka kontrola brzine reprodukcije + Veličina teksta overlay-a pri prevlačenju + Prilagodite veličinu teksta na ekranu (1-30) + Prag magnitude prevlačenja + Postavite potrebnu udaljenost prevlačenja prije nego što se aktivira prilagodba + Osjetljivost swipe-a za zvuk + Veće vrijednosti zahtijevaju više swipe-ovanja po koraku zvuka + Koraci zvuka + Pojačanje zvuka + Prevlačenje prstom prema gore iznad maksimalne glasnoće pojačava je, što može izobličiti glasnu audio zapise + Zadano za uređaj + Osjetljivost prevlačenja svjetline + Veće vrijednosti zahtijevaju više prevlačenja po koraku svjetline + Osjetljivost prevlačenja brzine + Veće vrijednosti zahtijevaju više prevlačenja po koraku brzine + Korak brzine + Visina zone brzine + Visina zone gesta za brzinu kao postotak visine plejera (5-75) + Brzina + Stil overlay-a pri prevlačenju + Horizontalni overlay + Horizontalni overlay (minimalistički - vrh) + Horizontalni overlay (minimalistički - centar) + Kružni overlay + Kružni overlay (minimalistički) + Vertikalni overlay + Vertikalni overlay (minimalistički) + Omogući prevlačenje za promjenu videa + Prevucite u režimu punog ekrana kako biste preskočili na sljedeći ili prethodni video + Akciona dugmad + Odaberite koja će se akcijska dugmad pojaviti u portretnom režimu i režimu punog ekrana + Dugmad za portret + Onemogući sjaj za Sviđaj mi i Pretplati se + Sprečava da dugmad \"Sviđa mi se\" i \"Pretplati se\" zasvijete kada ih kreator spomene + Sakrij traku akcija + Sakrij dugmad \"Sviđa mi se\" i \"Ne sviđa mi se\" + Sakrij dugme \"Podijeli\" + Sakrij dugme \"Zaustavi oglase\" + Dugme Sakrij komentare + Sakrij dugme Poveži + Dugme Sakrij prijavi + Dugme Sakrij Remix + Dugme Sakrij Preuzmi + Dugme Sakrij Hype + Dugme Sakrij Promoviši + Dugme Sakrij Zahvali se + Sakrij dugme Pitaj + Sakrij kanal i dugme Pretplati se + Sakriva avatar kanala i dugme Pretplati se na početku trake akcija + Sakrij dugme Pretplati se + Sakriva dugmad Pretplati se i Pridruži se pored avatara kanala na početku trake sa akcijama + Sakrij dugme Isečak + Ovo možda neće raditi za neke korisničke račune + Sakrij dugme Prodavnica + Sakrij dugme Sačuvaj + Sakrij dugme Više + Dugmad za cijeli ekran + Sakrij dugme Ne sviđa mi se + Sakrij dugme Sviđa mi se + Sakrij dugme \" uživo ćaskanje\" + Sakrij dugme Mix + Sakrij dugme \"Još videozapisa\" + Sakrij dugme za Playlistu + Gornja margina brzih radnji + Postavite gornju marginu trake brzih akcija (0-32 dp) + Titlovi + Upravljaj automatskim titlovima i kolačićima za titlove + Vidljivost automatskih titlova + Uvijek prikaži + Nikada ne prikazuj + Prikaži kada zvuk nije isključen + Prikaži kada je prigušeno + Preferirani jezik titlova + Prvo bira titlove dobavljača, a zatim ih automatski prevodi na preferirani jezik + Isključeno + Dodaj dodatne jezike za auto-prevod + Dodaje više jezika u meni za auto-prevod + Postavi kolačiće za titlove + "Popravlja fabrički YouTube bag gdje auto-prevod titlova možda nije dostupan zbog nedostatka kolačića + +'Kolačići za titlove' moraju biti navedeni" + Kolačići za titlove + Kolačići koji će biti postavljeni u zahtjevima za Timed Text API (Caption Data API) + Preuzmi kolačiće za titlove + Kliknite ovdje da preuzmete kolačiće + Neuspjelo preuzimanje kolačića + Kolačići već postoje + Kolačići su uspješno ažurirani + Popravi transkript + Rješava neuspjeh provjere preduvjeta koji može spriječiti učitavanje transkripta + Prikaži titlove u slika-u-slici načinu + Navigacijski gumbi + Odaberite koje će se dugmad pojaviti u donjoj navigacijskoj traci + Sakrij dugme Početna + Sakrij dugme Shorts + Sakrij dugme Kreiraj + Sakrij dugme Preplate + Sakrij dugme Obavještenja + Prikaži dugme Pretraga + Pozicija dugmeta Pretraga + Prikaži dugme za postavke + Pozicija dugmeta za postavke + Akcija dugmeta za postavke + Otvara Morphe postavke kada je omogućeno, ili YouTube postavke kada je onemogućeno + Zamijeni dugme \'Kreiraj\' sa dugmetom \'Obavještenja\' + Ako promjena ove postavke nema efekta, pokušajte prebaciti na Inkognito način + Sakrij oznake navigacijskih gumba + Sakrij tačku za novi sadržaj + Sakrij navigacijsku traku + "Ovo potpuno skriva navigacijsku traku, uključujući karticu 'Vi'. + +Da biste pristupili Postavkama, omogućite gumb Postavke putem Općenito → Traka s alatima → Prikaži postavke" + Omogući uske navigacijske gumbe + Smanjuje horizontalni razmak između dugmadi + Onemogući navigacijske gumbe bez oznaka + Prikazuje navigacijske gumbe s oznakama, ako ih YouTube šalje bez oznaka + Onemogući navigacijske kartice alatne trake + Prikazuje Preplate u navigacijskoj traci, ako YouTube to premjesti u karticu na vrhu Početne stranice + Omogući animacije navigacijske trake + Animira prijelaze prilikom prebacivanja između kartica + Onemogući automatsko skrivanje navigacijske trake + Zadržava navigacijsku traku vidljivom tokom skrolovanja + Stil navigacijske trake The + Polutransparentno + Transparentno + Neprozirno + Ovaj stil možda nije podržan od strane vašeg uređaja ili verzije YouTube-a, i može uzrokovati probleme s izgledom ili rušenje aplikacije + Alatka + Odaberite koje komponente se pojavljuju u alatnoj traci + Sakrij dugme za Cast + Sakrij dugme za ćaskanje + Sakrij dugme za mikrofon + Sakrij dugme za pretragu + Kada je omogućeno, dotični otvoraju Morphe postavke, a dugi pritisak otvara YouTube postavke; onemogučite da zamijenite + Flyout meni + Odaberite koje komponente se pojavljuju u flyout meniju + Sakrij meni \"Dodatne postavke\" / Više + Sakrij meni \"Ambijentalni režim\" + "Meni 'Audio Track' je skriven + +Da biste prikazali meni 'Audio Track', promijenite 'Spoof video streams' na bilo koji klijent osim 'Android Studio' i 'Android VR Downgraded'" + Sakrij meni \"Audio zapis\" + Sakrij podnožje menija \"Audio zapis\" + Sakrij meni Titlovi + Sakrij podnožje menija Titlovi + Sakrij zaglavlje menija Titlovi + Sakrij meni \"Pomoć & povratne informacije\" + Sakrij meni \"Slušaj putem YouTube Music-a\" + Sakrij meni \"Zaključaj zaslon\" + Sakrij meni \"Ponavljaj video\" + Sakrij meni \"U hodu\" + Sakrij meni Kvalitet + Sakrij podnožje menija Kvalitet + Sakrij zaglavlje menija Kvalitet + Sakrij meni \"Tajmer za spavanje\" + Sakrij meni \"Brzina reprodukcije\" + Sakrij meni \"Stabilna glasnoća\" + Sakrij meni \"Gledaj u VR-u\" + Sakrij predpregled automatskog puštanja + Sakriva predpregled sljedećeg videa u punom zaslonu prije početka automatskog puštanja + Overlay dugmad + Odaberite koje se dugmad prikazuju preko video playera + Sakrij dugme za automatsko puštanje + Sakrij dugme za titlove + Sakrij dugme za Cast + Sakrij dugme za kolapsiranje + Sakrij dugme za cijeli ekran + Sakrij pozadinu kontrola plejera + Sakriva tamnu sjenku oko kontrola video plejera + Prozirnost pozadine kontrolnih dugmadi + Prilagodite prozirnost pozadine iza kontrolnih tastera plejera i trake vremena (0 je nevidljivo, 100 je potpuno vidljivo) + Sakrij dugmad Prethodno & Sljedeće + Sakrij dugme za postavke + Sakrij kartice na završnom ekranu + Ambijentalni način + Upravljajte vidljivošću Ambient režima i ograničenjima napajanja + Zaobiđi ograničenja ambijentalnog načina + Omogućava Ambient režimu da ostane aktivan u režimu štednje baterije + Onemogući Ambient način rada + Onemogući Ambient režim u punom ekranu + Sakrij info kartice + Onemogući animacije brojeva koji se vrte + Zaustavlja animaciju brojanja kod broja lajkova i broja pregleda + Sakrij traku za traženje video plejera + Sakrij sličice na traci za traženje + Sakriva traku napretka gledanja koja se prikazuje na sličicama videa koje ste već pogledali + Omogući veliku traku za traženje u punom ekranu + Sakrij Shorts + Sakrij na stranici kanala + Sakrij u Home feedu & povezanim videima + Sakrij u rezultatima pretrage + Sakrij u feedu Pretplata + Sakrij u opisu videa + Sakrij u historiji gledanja + Shorts plejer + Odaberite koje komponente se pojavljuju u Shorts playeru + Onemogući dvostruki dodir za dopisivanje + Sakrij AI dugme + Sakrij oznaku \"Automatski sinhronizovano\" + Sakrij traku kanala + Sakrij dugme Komentari + Sakrij dugme Efekt + Sakrij oznaku poveznice videa + Sakrij smjernice za gestove + Sakrij dugme \"Zeleni ekran\" + Sakrij dugme Hashtag + Sakrij info panel + Sakrij dugme Pridruži se + Sakrij dugme Sviđa mi se + Sakrij animaciju fontane dugmeta Sviđa mi se + Sakrij predpregled uživo + Sakrij oznaku lokacije + Sakrij navigacijsku traku + Sakrij dugme \"Nove objave\" + Sakrij prekrijajuća dugmad za pauziranje + Sakrij pregled komentara + Sakrij dugme Remix + Sakrij dugme Spremi + Sakrij dugme \"Sačuvaj muziku\" + Sakrij prijedloge za pretragu + Sakrij dugme Dijeli + Sakrij dugme Shop + Sakrij dugme Zvuk + Sakrij oznaku metapodataka zvuka + Sakrij gumb Pretplati se + Sakrij dugme \"Kupi Super Thanks\" + Sakrij označene proizvode + Sakrij dugme Predstojeće + Sakrij dugme \"Koristi ovaj zvuk\" + Sakrij dugme \"Koristi ovaj šablon\" + Sakrij naslov videa + Sakrij predloženi video na završnom ekranu + "Sakriva overlay sa predloženim videom koji se pojavljuje nakon što se video završi + +Ova postavka radi samo ako je automatsko puštanje isključeno. Možete je promijeniti u YouTube-u: +Postavke → Reprodukcija (ili Automatsko puštanje) → Automatsko puštanje sljedećeg videa" + Sakrij povezane videozapise + Sakrij preklop \'Više videa\' + Sakriva preklop \'Više videa\' koji se prikazuje prilikom prevlačenja nagore u režimu punog ekrana + Sakrij vremensku oznaku videa + Onemogući popup panele plejera + Onemogućava automatsko otvaranje panela playliste i live chata + Onemogući automatsko puštanje plejliste + Sprečava plejliste da automatski prelaze na sljedeći video + Onemogući upit \'Nastavite s gledanjem\' + Izađi iz fullscreen moda na kraju videa + Onemogućeno + Samo portret + Samo pejzaž + Portret i pejzaž + Forsiraj pejzažni način u punom zaslonu + Rotiraj plejer u pejzažni način pri prelasku na puni zaslon. Nije dostupno na malim ekranima koji već otvaraju puni zaslon u pejzažnom načinu + Gestovi za puni ekran + Geste igrača u režimu punog ekrana + Onemogući gest povlačenja nagore u portretnom načinu + Onemogući gest povlačenja nadole u portretnom načinu + Onemogući gest povlačenja nadole u pejzažnom načinu + Onemogući gestu štipanja za zumiranje + Vrati pinch to zoom + Otvori videozapise u fullscreen načinu rada + Onemogućeno + Portret + Pejzaž + Skala videa u punom zaslonu The + Rastegnuto (popuni zaslon) + Zumiraj (isjeci da popuniš) + Prikaži dugme za skalu videa + Dodirnite za ciklus: Podrazumijevano, Razvuci i Zumiraj + Prikaži samo u fullscreenu + Sakrij dugme za skaliranje videa u portretnom playeru + Prikaži dugme za utišavanje + Neprozirnost overlay-a plejera + Prilagodi nivo prozirnosti (0 je prozirno, 100 je neprozirno) + Traka za pretragu + Onemogućeno + Široka + Ekstra široko + Izgled uređaja + Zadano + Telefon + Tablet + Automobilizam + "Promjene uključuju: + +Izgled za tablet +• Objava zajednice su skrivene +• Postavka reprodukcije u feedovima nije dostupna +• Dugme Remix i dugme Sound nisu dostupni u Shorts +• Overlay \"Više videozapisa\" možda nije dostupan +• Prikazuje se stara traka s akcijama za videozapise koja nije skupljena +• Stavke u flyout meniju možda neće raditi ispravno + +Automobilski izgled +• Feed je organizovan po temama i kanalima +• Postavka reprodukcije u feedovima nije dostupna +• Dugme Remix i dugme Sound nisu dostupni u Shorts +• Overlay \"Više videozapisa\" možda nije dostupan +• Shorts se otvaraju u običnom plejeru +• Prikazuje se stara traka s akcijama za videozapise koja nije skupljena" + Omogući izgled za tablet u plejeru + "Vraća stari izgled plejera + +Ograničenje: Ako je 'Vrati staru traku s akcijama videa' isključeno, preklop 'Više videa' možda neće biti dostupan" + 21.11.48 - Vrati staro ponašanje titlova + 20.39.41 - Vrati staro čuvanje u plejlistu + 20.28.41 - Onemogući bold ikone + 20.13.41 - Vratite naziv kanala ispod video plejera + Otvori sistemski prozor za dijeljenje + Otvara sistemski prozor za dijeljenje umjesto onog u aplikaciji prilikom dijeljenja sadržaja + Prepiši YouTube Music dugmad + Otvara vašu omiljenu aplikaciju za muziku umjesto fabričke YouTube Music aplikacije + Ime paketa muzičke aplikacije + Ime paketa vaše instalirane muzičke aplikacije + Početna stranica + Zadano + Sve pretplate + Pretraži kanale + Kursevi / Učenje + Istraži + Moda & ljepota + Gejming + Historija + Biblioteka + Sviđeni videi + Ao živo + Filmovi + Muzika + Vijesti + Obavještenja + Playliste + Pretraga + Kupovina Shorts + Sport + Pretplata + Trendovi + Virtualna stvarnost + Gledaj kasnije + Vaši isječci + Onemogući nastavak pri pokretanju + Otvori Shorts pomoću + Shorts player + Standardni player + Standardni player u punom ekranu + Automatsko puštanje Shorts videa + Automatski prelazi na sljedeći Short umjesto ponavljanja trenutnog + Automatsko puštanje u pozadini + Automatski prelazi na sljedeći Short tokom reprodukcije u pozadini + Preskoči live Shorts + Preskače live streamove koji se pojavljuju u Shorts feedu + Reprodukcija u feedovima + Uvijek uključeno + Samo Wi-Fi + Isključeno + Vrati originalne naslove + Prikazuje originalne naslove videa, opise videa i opise kanala umjesto onih koji su automatski prevedeni + Učitavanje naslova… + Onemogući automatsko osvježavanje feeda + Sprečava da se feedovi automatski osvježe nakon što zastariju + Izgled kartica feed-a + Klasično + Kartice + Kartice sa dugmadima za sviđa mi se, komentar i spremanje + Komentari + Onemogući nastavak reprodukcije u miniplayeru + Onemogućava mini-player \'Nastavi gledati\', YouTube Premium funkciju koja se pojavljuje pri pokretanju aplikacije + Mini-player + Prilagodite izgled i ponašanje minimiziranog video plejera + Tip mini-playera + Sakrij naslov i kanal + Onemogućeno + Zadano + Minimalistički + Minimalistički 2 + Tablet + Moderan 1 + Moderan 2 + Moderan 3 + Moderan 4 + Onemogući zaobljene uglove + Onemogući prevlačenje i puštanje + Onemogućava prevlačenje minimiziranog plejera u različite uglove ekrana + Onemogući horizontalnu gestu povlačenja + Sprječava zatvaranje miniplayera horizontalnim prevlačenjem s ekrana + Pauziraj reprodukciju horizontalnim povlačenjem + Reprodukcija će biti pauzirana kada se miniplayer prevuče van ekrana + Zapamti poziciju stanja povlačenja + Povlačenje mini-plejera van ekrana očuvat će njegovo stanje korisničkog sučelja + Sakrij overlay dugmad + Zadana veličina + Postavite početnu veličinu na ekranu, u pikselima + Veličina piksela mora biti između %1$s i %2$s + Prozirnost overlay-a + Prilagodite razinu prozirnosti (0 je prozirno, 100 je neprozirno) + Dugme \'nazad\' izlazi iz feeda + Pritiskanje nazad uvijek izlazi iz aplikacije umjesto skrolovanja do vrha feeda + Prisili animacije The + Uvijek uključeno + Uvijek isključeno + Sakrij statusnu traku + Skriva sistemsku traku stanja. Prevucite prema dolje s gornjeg ruba da je nakratko prikažete + Popravi statusnu traku koja prekriva gornje trake + Održava konzistentan izgled statusne trake, tako da ne prekriva gornje trake i ne ostavlja prazan prostor iznad plejera. Isključite ovo ako aplikacija puca ili ako gornje trake izgledaju pogrešno + Sjaj + Svijetleći početni ekran nije podržan na ovom uređaju + Pozadina svijetle teme + Bijela + Material You (Bijela) + Svijetlo roza + Svjetlo plava + Svjetlo zelena + Svjetlo žuta + Svjetlo narandžasta + Svjetlo crvena + Prilagođena svijetla pozadina + Postavite boju pozadine svijetle teme + Promijeni boju prednjeg plana + Koristite boju teme za tekst i ikone + Omogući gradijentni ekran za učitavanje + Prikazuje gradijentnu pozadinu na ekranu za učitavanje umjesto jednobojne boje + Omogući prilagođenu boju trake za traženje + Boja trake za pomicanje + Postavite primarnu boju trake napretka videa + Akcentna boja trake za pomicanje + Postavite sekundarnu boju trake napretka videa + Neispravna vrijednost boje trake za traženje + Originalni analog + Originalna torta + Originalni metal + Logotip zaglavlja + Podrazumijevano + Regularno + Prilagođeno + Zaobiđi ograničenja regije slika + Preusmjerava zahtjeve za sličice na alternativni server kako bi se popravile nedostajuće slike u ograničenim zemljama + DeArrow naslovi + Početna kartica + Tab Preplate + Vi tab + Plejer liste reprodukcije & preporuke + Rezultati pretrage + Prikaži DeArrow ikonu + Prikazuje DeArrow ikonu na naslovima videozapisa iz crowd-sourcinga + Casual način rada + Prikazuje originalni naslov umjesto DeArrow naslova ako su DeArrow korisnici glasali da im se više sviđa originalni naslov + Minimalan broj casual glasova za prikaz originalnog naslova + Broj casual glasova u jednoj kategoriji, kao što su smiješno ili kreativno, koji su potrebni za prikaz originalnog naslova + DeArrow sličice + Originalne sličice + DeArrow & originalne sličice + DeArrow & statični snimci + Statični snimci + "DeArrow zamjenjuje clickbait naslove i sličice onima iz crowdsourcinga koji su precizniji i manje senzacionalistički. Naslove piše i o njima glasa zajednica, a sličice su kadrovi preuzeti iz videa + +Videozapisi se zahtijevaju putem kratkog prefiksa hashiranog ID-a videa, tako da server ne može tačno znati koji video gledate. Ako video nema DeArrow sličicu, prikazuje se originalna ili snimak ekrana" + Prikaži upozorenja o grešci u povezivanju + Prikaže obavijest kada DeArrow server nije dostupan + API krajnja tačka + URL krajnje tačke keša DeArrow sličica + Statični snimci videa + Statični snimci se i dalje uzimaju s početka / sredine / kraja svakog videa. Ove slike su ugrađene u YouTube i ne koristi se nikakav vanjski API + Izdvoji okvir videa iz + Početak + Sredina + Kraj + DeArrow trenutno nije dostupan (kod statusa: %s) + DeArrow trenutno nije dostupan + Vrati stari stil dugmadi plejera + Vraća podebljane kontrole video plejera na stariji, tanji vizuelni stil + Simuliraj dimenzije uređaja + Omogućavanje ovoga može otključati veće kvalitete videa + Omogućavanje ovoga može uzrokovati trzanje pri reprodukciji videa, kraći vijek trajanja baterije i nepoznate nuspojave + Haptički povratni signal + Odaberite koje interakcije pokreću vibraciju uređaja + Onemogući haptiku za poglavlja + Onemogući haptiku za precizno pretraživanje + Onemogući haptiku za poništavanje pretraživanja + Onemogući haptiku pri dodiru i držanju + Onemogući haptiku za zumiranje + Ako ste nedavno promijenili podatke za prijavu na račun, deinstalirajte i ponovno instalirajte MicroG + Zaobiđi preusmjeravanja linkova + Otvara originalni link direktno, zaobilazeći link za praćenje (youtube.com/redirect) + Otvaraj poveznice u pretraživaču + Otvara poveznice u vanjskom pregledniku umjesto u pregledniku unutar aplikacije + Veličina bafera za reprodukciju The + Niska + Srednja + Maksimalna + Kašnjenje videa + Odlaže video kako bi ga uskladio sa zakašnjenjem zvuka, kao što su Bluetooth slušalice (0-5000 ms) + Odloži samo kod Bluetooth audio zapisa + Odlaže video samo dok je povezan Bluetooth audio, tako da zvučnik telefona ostane sinhronizovan + Prikaži dugme za odlaganje videa + Dodirnite i držite da biste resetovali odlaganje na 0 ms + Prikaži meni za odlaganje videa + Odlaganje u milisekundama + Automatski + Zapamti kvalitet videa + Primjenjuje vaš ručni odabir kvaliteta na sve buduće videozapise + Obavještenja o promjeni kvaliteta + Prikazuje obavijest kada se spasi nova zadana kvaliteta + Zadana kvaliteta videa na Wi-Fi mreži + Zadana kvaliteta videa na mobilnoj mreži + Zapamti kvalitet Shorts videa + Primjenjuje vaš ručni odabir kvaliteta na sve buduće Shorts videe + Zadana kvaliteta Shorts videa na Wi-Fi mreži + Podrazumijevani kvalitet Shorts videa na mobilnoj mreži + Promijenjena je zadana mobilna kvaliteta u: %s + Promijenjena je zadana Wi-Fi kvaliteta u: %s + Promijenjena je mobilna kvaliteta za Shorts u: %s + Promijenjena je kvaliteta Shorts Wi-Fi veze na: %s + Prikaži dugme za brzinu reprodukcije + Dodirnite i zadržite da biste resetovali brzinu na podrazumijevanu + Prikaži dugme za kvalitet + Dodirnite i zadržite da biste resetovali kvalitet na podrazumijevani + Prikaži rezoluciju na dugmetu Kvalitet + Prikaži rezoluciju, kao što je 1080, umjesto oznake kao što je FHD + Prilagođeni meni brzine reprodukcije + Vrati stari meni brzine reprodukcije + Prilagođene brzine reprodukcije + Unesite željenu opciju brzine reprodukcije + Prilagođene brzine reprodukcije moraju biti manje od %s + Neispravne prilagođene brzine reprodukcije + Automatski + Dodirnite i zadržite brzinu reprodukcije + Postavite množilac brzine pri pritiskanju i držanju ekrana, postavite na 0 da onemogućite + Zapamti brzinu reprodukcije + Primjenjuje vaš ručni odabir brzine na sve buduće videozapise + Obavještenja o promjeni brzine + Prikazuje obavijest kada se spasi nova zadana brzina reprodukcije + Podrazumijevana brzina reprodukcije + Zadana brzina reprodukcije na Shorts + Zapamti brzinu reprodukcije na Shorts + Primjenjuje vaš ručni izbor brzine Shorts videa na sve buduće Shorts videe. Ako je isključeno, brzina se vraća na zadanu nakon zatvaranja Shorts playera + Prikaži brzinu reprodukcije u Shorts meniju + Dodaje stavku Brzina reprodukcije u meni Shorts playera + Brzina reprodukcije · %s + Zadana brzina reprodukcije promijenjena u: %s + Koristi normalnu brzinu za muziku + Uvijek puštaj muzičke spotove brzinom 1.0x + Bijela lista brzina kanala + Videozapisi s kanala na bijeloj listi uvijek se puštaju brzinom 1.0x + Dodaj kanal na bijelu listu brzina + Ukloni kanal s bijele liste brzina + Omogući kontrole visine zvuka + Podrazumijevana visina zvuka pri reprodukciji + Podrazumijevana visina zvuka promijenjena u: %s + Vremensko rastezanje zvuka + Omogućava nezavisno postavljanje brzine i visine zvuka, što može uzrokovati audio artefakte + Zapamti poziciju reprodukcije prijenosa uživo + Za prijenose uživo koji su u toku, zapamti i nastavi vašu poziciju reprodukcije umjesto prebacivanja na trenutnu poziciju uživo + Ova funkcija ne radi sa 24/7 live streamovima i drugim streamovima koji su \"uvijek uključeni\" + Onemogući HDR video + Prisili HDR video + Prisiljava aplikaciju da prepozna ovaj uređaj kao HDR-sposoban + "Koristite ovo ako HDR dekodiranje radi, ali YouTube ne nudi HDR, kao što je slučaj sa nekim HyperOS uređajima koji skrivaju HLG. + +Ograničenja: +• Boje mogu izgledati pogrešno ako zaslon zapravo ne može prikazati HDR +• Reprodukcija može propasti ako dekoder ne može obrađivati HDR +• HDR i dalje zavisi od klijenta koji emulira video strimove" + Video kodek The + Da biste forsirali AVC, promijenite \'Simuliraj video tokove\' u \'Android Studio\', \'Android VR\' ili \'visionOS\' + "VP9 +• Može poboljšati trajanje baterije i popraviti trzanje pri reprodukciji +• Koristi više internet podataka od AV1 + +AVC (H.264) +• Zahtijeva 'Android Studio', 'Android VR' ili 'visionOS' klijent za simulaciju video tokova +• Može još više poboljšati trajanje baterije +• Može vratiti nedostajuće video rezolucije na starijim uređajima +• Maksimalna rezolucija je 1080p +• HDR videozapismi možda neće koristiti AVC +• Neki uređaji ne mogu forsirati AVC" + Otvori meni za naprednu kvalitetu + Preskače meni za odabir kvalitete i direktno otvara napredni meni za kvalitetu + Sakrij Premium opcije kvaliteta + Prioritizuj kvalitet videa + Prioritizira najviši dostupni kvalitet videa u odnosu na efikasnost kodeka + Ako je ova postavka isključena, stariji videozapisi možda neće prikazivati opcije kvaliteta videa + Dozvoli \'Android VR\' / visionOS AV1 + Omogućava softversko AV1 dekodiranje pri korištenju \'Android VR\' / visionOS klijenta + "Omogućavanjem ove postavke može se koristiti softversko AV1 dekodiranje. + +Reprodukcija videa sa AV1-om može kočiti ili gubiti sličice" + Nuspojave spoofinga + • Reprodukcija može zakazati nakon 1 minute + • Nedostaje meni \'Audio Track\' + • Nema AV1 video kodeka + • 360° VR imersivni način rada nije dostupan + • Stabilna glasnoća nije dostupna + • Reprodukcija može završiti 1 sekundu ranije + • Ako deobfuskacija ne uspije, reprodukcija možda neće raditi + • Opcija „Prisili originalni audio“ nije dostupna + • Potrebna je prijava na Android VR + • Kvaliteta može biti ograničena na 720p + JavaScript varijanta plejera + Prikaži simulaciju u \'Statistici za programere\' + Vrati staru traku s radnjama za videozapise + "Ispravlja problem gdje se gumbi za video akcije ne mogu sakriti + +Ograničenje: Vraća stari stil overlay-a 'Više videa'" + Stil ikone plejera + Automatski The + Tanko + Bold + Prilagođeno + Stil Shorts ikone + Pratitelj serija + Pratite plejliste kao serije i zapamtite gdje ste stali + Omogući praćenje serija + Dodaje stranicu Serija u Historiju i opciju Prati seriju u plejliste + Prikaži Serije u navigacijskoj traci + Prikaži dugme Series Tracker-a + Dodaje dugme u plejer za praćenje ili otvaranje serijala trenutnog videa + Zapisuj napredak praćenih serijala + Zapisuj reprodukciju na ovom telefonu za sačuvane serijale i znakove za prečicu + Otkaži + Sačuvaj + Sačuvaj znak za prečicu + Naziv serijala + Opcije serijala + Opcije biblioteke + Osvježi epizode + Preimenuj + Otvori plejlistu u YouTube-u + Ukloni iz biblioteke + Link ili ID plejliste + Obriši sav lokalni napredak + Obriši historiju + Obrisati sav lokalni napredak? + Ovim se brišu pozicije, pregledani nadjačaji i selekcije za nastavak za svaki video. Sačuvane serije i njihovi katalozi ostaju. Novi napredak reprodukcije će biti zapisan ako je snimanje omogućeno + Označi kao viđeno + Označi kao neviđeno + Koristi automatski status + Ponovo pokreni od 0:00 + Pusti ponovo od 0:00 + Status sačuvan + Ponovi čuvanje + Prethodnih 100 + Sljedećih 100 + Otvori sačuvani video + Lokalni napredak obrisan + Otvorite Historiju iz You sekcije da biste vidjeli svoje serije + Historija gledanja + Serije + Učitavanje sačuvanih serija… + Pratite plejlistu kako biste pratili njene epizode + Zatraga za plejlistom + Osvježavanje epizoda… + Sve serije + Gledano + Učitavanje epizoda… + Više opcija + "Snimaj napredak samo za svoje sačuvane serije i označene video zapise. + +Ova biblioteka je pohranjena na ovom uređaju i dijeli se između YouTube računa. YouTube kontrole za pauziranje i brisanje historije je ne mijenjaju. Koristite Serije → Više → Obriši lokalni napredak da biste je izbrisali. + +Automatsko snimanje prestaje u inkognito modu, kada identitet računa nije dostupan ili kada pređete na drugi račun. Ponovo omogućite praćenje kako biste ga koristili s tim računom" + Praćenje se ne može omogućiti u inkognito modu ili prije nego što YouTube učita trenutno stanje računa. Vratite se na Početnu i pokušajte ponovo. Prijava nije potrebna + Omogući lokalno praćenje + Snimanje napretka je isključeno + Nastavi na %1$s + %1$s / %2$s epizoda pogledano + Epizoda %1$s · %2$s + Epizode %1$s-%2$s od %3$s + Osvježite kako biste pokušali učitati epizode. Privatne liste reprodukcije zahtijevaju pristup računu; njihovi znakovnici se i dalje otvaraju u YouTube-u + YouTube nije mogao otvoriti ovu epizodu + YouTube nije mogao otvoriti ovu listu reprodukcije + Odaberite epizodu + Ažurirano + Nema epizoda koje se mogu pustiti + Sačuvan video · %1$s + Prati seriju + Pogledaj seriju + Nastavi + Počni ovdje + Početna tačka je sačuvana + Gledano do ovdje + Epizode označene kao gledane + Poništi + Omogući + %1$s novih epizoda + Zapisuj napredak na ovom uređaju + "Vaše serije i napredak ostaju na ovom uređaju, zajednički za YouTube račune. YouTube kontrole historije ih ne mijenjaju. Obrišite napredak iz menija Serije. + +Automatsko snimanje je opcionalno. Ono prestaje u inkognito modu ili kada se promijeni račun" + Epizode nisu mogle biti učitane. Playlistu možete sačuvati kao oznaku + Previše nespremijenih promjena. Prvo pokušajte ponovo spremiti + Spremanje je u toku. Pokušajte ponovo + Učitavanje epizoda je u toku. Pokušajte ponovo + Skladište nije dostupno. Vaši spremljeni podaci su zadržani + Promjene nisu mogle biti spremljene. Pokušajte ponovo spremiti prije zatvaranja YouTube aplikacije + Ova serija je već spremljena ili je njena playlista promijenjena. Ponovo je otvorite + Ova serija je promijenjena ili uklonjena. Ponovo otvorite biblioteku + Playlista je promijenjena. Ponovo odaberite epizodu + Ova epizoda nije dostupna. Odaberite drugu + Unesite naziv serije + Unesite poveznicu na YouTube playlistu ili ID playliste + Molimo pričekajte prije ponovnog osvježavanja + Epizode se nisu mogle učitati. Pokušajte ponovo kasnije + Promjena nije mogla biti izvršena. Pokušajte ponovo + Nastavak nije mogao biti potvrđen. Provjerite player i pokušajte ponovo + Prate serije + Ovaj video nije u ovoj playlisti + Tražim liste reprodukcije… + Zalijepi poveznicu liste reprodukcije + Lista reprodukcije nije pronađena + Nema dostupnih epizoda + Sakrij pogledane + Označi prethodne epizode kao pogledane + Počinje sa: %1$s + Redoslijed epizoda + Redoslijed plejliste + Obrnuti redoslijed plejliste + %1$s pregleda + %1$s pregled + Označi epizode kao pogledane + Najmanje %1$s%% pogledano s najviše 90 sekundi preostalo, ili %2$s sekundi preostalo + Automatski označite epizodu kao viđenu nakon bilo kojeg od pragova. Pravilo postotka također zahtijeva da preostane najviše 90 sekundi, tako da se kraj dugačkih epizoda ne preskače. Pravilo preostalog vremena primjenjuje se na videozapise duže od jedne minute; postavite ga na 0 da biste ga onemogućili. Promjene se primjenjuju na buduća ažuriranja napretka. Ručni izbori viđeno/neviđeno se zadržavaju + Postotak viđenog (1-100) + Preostale sekunde (0-300; 0 onemogućuje) + Unesite vrijednost od %1$s do %2$s + Koristi YouTube napredak + Nastavite pratiti serije nakon gledanja na drugom uređaju + Čitaj status viđenih i pozicije nastavka za tvoje praćene serije s trenutnog YouTube računa. Na ovom telefonu se spremaju samo podudarne epizode. Provjere se izvršavaju dok je biblioteka serija otvorena i prije Nastavi. YouTube historija mora biti omogućena na tvom drugom uređaju + Provjeravanje napretka na YouTube-u… + YouTube napredak nije dostupan. Koristi se spremljeni napredak + Neki YouTube napredak nije dostupan. Spremljene pozicije su zadržane + Sigurnosna kopija i oporavak + Spremite svoju biblioteku serija u datoteku ili oporavite sigurnosnu kopiju + Spremi praćene serije, pozicije epizoda i izbore pregledavanja. Dozvole za snimanje i napredak na YouTube-u nisu uključene + Spremi sigurnosnu kopiju + Oporavi sigurnosnu kopiju + Dodajte %1$s serija. %2$s serija koje su već u vašoj biblioteci će biti zadržane. Postojeći napredak videa je zadržan + Sigurnosna kopija serija je spremljena + %1$s serija je vraćeno + Ova datoteka nije podržana rezervna kopija serija ili je prevelika. Vaša biblioteka nije izmijenjena + Rezervna kopija nije mogla biti sačuvana. Pokušajte na drugoj lokaciji + Serija uklonjena + Ova epizoda se sada pojavljuje više puta. Odaberite od koje želite nastaviti + Vaša sačuvana epizoda više nije u ovoj listi reprodukcije. Vaša sačuvana pozicija je zadržana + Nastavite sa epizodom %1$s · %2$s + Također možete odabrati bilo koju epizodu ispod. Ranije epizode nisu promijenjene + Ova plejlista je promijenjena. Otvorite seriju kako biste odabrali gdje nastaviti + Nema više epizoda u ovom smjeru + Nastavi %1$s + Pretraži serije + Obriši pretragu + Nije pronađena nijedna serija + Epizoda bez naslova + Nedostupna epizoda + Niconico komentari + Prikaži live chat i ponovnice chata kao komentare koji klize preko videa + Prikazuje overlay dugme u video plejeru. Dodirnite za prikaz ili skrivanje komentara, dodirnite i držite za brza podešavanja + Veličina fonta komentara + Veličina teksta komentara (12-40 sp) + Prozirnost komentara + Prozirnost teksta i pozadina komentara + Trajanje putovanja + Koliko sekundi je potrebno komentarima da pređu ekran. Manje vrijednosti se kreću brže + Maksimalna dužina komentara + Skriva komentare koji imaju više znakova od navedenog. Svaki emoji se računa kao jedan znak + Bez ograničenja diff --git a/patches/src/main/resources/addresources/values-ca-rES/music/strings.xml b/patches/src/main/resources/addresources/values-ca-rES/music/strings.xml index ad460303d9..03bb04867f 100644 --- a/patches/src/main/resources/addresources/values-ca-rES/music/strings.xml +++ b/patches/src/main/resources/addresources/values-ca-rES/music/strings.xml @@ -17,73 +17,675 @@ Second \"item\" text" --> + Última reproducció + Subscripcions + Vídeos + Altres resultats + Cançons pujades + Àlbums pujats + Artistes pujats + Actualitza el patch de Jam de l\'anfitrió per utilitzar la pausa i la reproducció + Compartir la cua de Jam + Sigueu l\'anfitrió o uniteu-vos a un Jam proper i editeu la cua a través de Jam Layer + Habilitar el compartiment de la cua de Jam + Habilita la integració de Jam després que YouTube Music es reiniciï + Descarrega Jam Layer per començar a funcionar + Obre l\'última versió de Jam Layer al teu navegador + Controls de Jam + Sigueu l\'anfitrió o uniteu-vos a un Jam proper i gestioneu la sessió + Paquet de Jam Layer + Trieu l\'aplicació acompanyant instal·lada utilitzada per compartir la cua propera + Utilitza un paquet complementari instal·lat. El vincle requereix la teva aprovació i un token de capacitat local + Activar el Wi-Fi? + Activa el Wi-Fi i torna a intentar iniciar o unir-te al Jam + Obrir la configuració del Wi-Fi Cancel·lar + Utilitzar paquet + Introduïu un nom de paquet d\'Android vàlid + Paquet desat. Configura Jam Layer per connectar + Jam desconnectat + Activeu primer Jam Layer + Activeu l\'establiment de la cua de Jam a la configuració i, després, reinicieu YouTube Music + Instal·la primer el paquet de Jam Layer seleccionat + Introdueix el codi que apareix a l\'host. Wi-Fi Aware pot connectar dispositius propers encara que les seves LAN siguin diferents + Uneix-te amb un codi + Uneix-te + Escaneja el QR + Introdueix el codi de vuit caràcters o enganxa una invitació + Uneix-te al teu Jam + Copia el codi + Codi del Jam + Mostra el QR + Fet + Convida al teu Jam + Escaneja per unir-te i editar aquesta cua. Finalitzar la Jam revoca la invitació + Copiar enllaç + Invitació a Jam + La Jam s\'està reconnectant; torna-ho a intentar d\'aquí a poc + Afegit a Jam + Jam no està disponible; la pista no s\'ha afegit + El teu Jam + Escoltant junts + Unint-se a Jam + Escolta conjuntament + Actualitzant Jam… + Buscant el vostre host a Wi-Fi… + Esperant gent + %s persona connectada + %s persones connectades + Introduïu aquest codi en un dispositiu proper. Wi-Fi Aware funciona a través de xarxes LAN separades quan és compatible. Caduca d\'aquí a %s minuts + Desplaçar a %s? + Edicions obertes + Edicions bloquejades + Connectat via Wi-Fi Aware + Connectat via Wi-Fi + Connectat via Bluetooth + No suportat durant la Jam + Unió cancel·lada + Reconnectant amb l\'anfitrió… + Compartiu una cua. Deixeu que tothom hi afegeixi cançons + Convida gent + Els convidats poden editar la cua + Finalitza la Jam + L\'anfitrió reprodueix la música. Afegeix, mou o elimina cançons directament a la cua de YouTube Music + Mantingueu els dos dispositius propers. Podeu cancel·lar mentre busquem l\'anfitrió + Cancel·la la unió + Sortiu de la Jam + Inicia una Jam + Configura la Capa de la Jam + Gestiona + Detalls Obrir + Carregant opcions de la cançó… + Les opcions de la cançó no estan disponibles; torna-ho a intentar + No s\'han pogut obrir les opcions de la cançó + No hi ha cap navegador disponible per descarregar Jam Layer + Esperant la cua de l\'anfitrió + Obre primer una cançó a YouTube Music + Reprodueix a l\'anfitrió + Surt de Jam i reprodueix localment + Reprodueix el següent a Jam + Afegeix a la cua de Jam + Reprodueix aquesta pista de Jam + Tria on reproduir + Mou l\'anfitrió aquí + La cançó no s\'ha preparat per a la cerca + La cua ha canviat; intenta el gest nourament + Logo del capçallet + Per defecte + Personalitzat + Reprodueix les pistes d\'àudio de l\'àlbum + Reprodueix la versió només d\'àudio de l\'àlbum de les cançons en lloc d\'un vídeo musical. Requereix \'Simular fluxos de vídeo\' + Habilita el commutador d\'àudio/vídeo + Intenta canviar entre la versió de la pista d\'àudio i el vídeo musical oficial + Descarrega vídeos musicals, podcasts o cançons amb aplicacions de descàrrega de tercers + Utilitza un descarregador extern per al menú de Descàrrega + L\'opció de Descàrrega dels menus obre el teu descarregador extern en el seu lloc + Descàrregues + "No hi ha descàrregues +Les pistes descarregades apareixen aquí" + %1$s • %2$s • %3$s MB + %1$s pistes • %2$s MB + Cerca descàrregues + Ordena + Ordena per artista + Ordena per títol + Ordena per mida + Suprimeix totes les descàrregues + Suprimeix totes les descàrregues? + Totes les pistes descarregades s\'eliminaran d\'aquest dispositiu + Descàrregues eliminades + No s\'ha trobat res + Accions per a %s + Reprodueix + Reprodueix o pausa + Anterior + Següent + Pausa Tanca Eliminar + Eliminar descàrrega + Eliminar descàrrega? + %s s\'eliminarà d\'aquest dispositiu + Descàrrega eliminada + No s\'ha pogut eliminar la descàrrega + No s\'ha pogut reproduir la descàrrega + Descàrregues d\'àudio + Reproducció sense connexió + No s\'ha pogut identificar la pista per descarregar + La descarrega ja està en curs + Ja descarregat + Descarrega iniciada + Iniciant la descarrega… + Descarregant %s%% + Descarrega completada + La descarrega ha fallat + Sobre Morphe + Publicitat Canal General Reproductor + Letres Miscel·lània + Idioma de YouTube Music + Idioma del sistema + Requireix Android 13 o posterior + Ocultar l\'etiqueta \"Obtingiu Music Premium\" + Ocultar promocions de Music Premium + Ocultar anuncis de vídeo + Ocultar el selector d\'Àudio / Vídeo + Ocultar el botó de descàrrega + Oculta el botó de descàrrega a l\'emissió del podcast + Ocultar l\'estant d\'exploració + Ocultar la barra de filtres + Ocultar els estants de quadrícula + Ocultar els estants horitzontals + Ocultar els estants de llista + Panell de lets + Ocultar el botó de Compartir + Ocultar el botó de traduir + Ocultar l\'estant \"Nou de\" + Ocultar els estants de llistes de reproducció + Ocultar el botó de Repetir + Amaga el botó de Aleatori + Amaga l\'estanteria de \"Marcació ràpida\" + Comentaris + Tria quins components apareixen a la secció de comentaris + Amaga les Normes de la Comunitat + Ocultar missatge de context + Amaga el botó d\'Emoji + Amaga el botó d\'Informació + Amaga el botó de la marca temporal + Crossfade + Mescla les pistes suaument entre elles + Habilita el crossfade + Curva de fade + Potència igual + Retenció subtil + Suavització lleugera + Curva S suau + Sortida + Entrada + Durada del crossfade + {one, 250 mil·lisegons, other, 250 mil·lisegons} + {one, 500 mil·lisegons, other, 500 mil·lisegons} + {one, 750 mil·lisegons, other, 750 mil·lisegons} + 1 segon + 2 segons + 3 segons + 4 segons + 5 segons + 6 segons + 7 segons + 8 segons + 9 segons + 10 segons + 11 segons + 12 segons + Crossfade en salt manual + Aplica l\'efecte de dissolvència quan saltes manualment a la pista següent + Dissolvència en l\'avanç automàtic + Aplica l\'efecte de dissolvència quan una pista finalitza naturalment + Control de la sessió + Mantingueu premut el botó de reproducció aleatòria per pausar o reprendre la dissolvència + Dissolvència pausada per a aquesta sessió + Dissolvència reprèsa + El mode de vídeo no està disponible mentre la dissolvència està activada + El crossfade està desactivat amb la reproducció SABR + El crossfade està desactivat mentre s\'està transmitent + Com funciona + "El crossfade solapa el final d'una cançó amb l'inici de la següent, utilitzant la durada i la corba seleccionades + +S'executa en l'avanç automàtic i en el salt manual, cadascun amb el seu interruptor. En l'avanç automàtic, la mescla comença uns segons abans que la cançó s'acabi" + Limitacions + "Els vídeos musicals es reproduen només com a àudio mentre el crossfade està activat. Mantingueu premut el botó d'aleatorietat per pausar el crossfade i veure el vídeo + +Un vídeo musical que comença la reproducció es manté en mode vídeo fins a la cançó següent + +Les cançons més curtes que la durada de la transició no tenen crossfade + +La pantalla de bloqueig o la notificació poden mostrar breument un estat Antic durant la transició + +Pausar el crossfade per veure un vídeo musical pot deixar alguns vídeos en negre. Pausar el crossfade abans de triar la cançó evita que passi" + No suportat + "Transmissió o mirroring d'àudio (Chromecast, mirroring de pantalla o d'àudio) + +Reproducció SABR (fluxos de vídeo simulats desactivats)" + Crèdits + Funcionalitat de crossfade per VazerOG + Recordar l\'estat de repetició + Recordar l\'estat de reproducció aleatòria Velocitat de reproducció + Velocitat utilitzada per a totes les pistes. Al 100% s\'utilitza la velocitat de l\'aplicació, com la velocitat de reproducció del podcast Canvia l\'afinació amb la velocitat + Desactivar la redirecció de no m\'agrada + Evita saltar a la següent cançó quan es prem el botó no m\'agrada + Acció de tocar una pista de la llista de reproducció + Reprodueix la llista de reproducció + Reprodueix només la pista seleccionada + Afegeix la pista seleccionada a la cua + S\'ha afegit a la cua + Amaga el botó de Cast + Amaga el botó d\'Historial + Amaga el botó de Notificacions + Amaga el botó de Cerca + Amaga el botó de cerca per veu + Amaga el botó de cerca per so + Amaga el botó Nou + Amaga el botó Nou a la pestanya de la Biblioteca + Botons d\'acció + Tria quins botons d\'acció apareixen sota el reproductor + Ocultar la barra d\'accions + Amaga el botó de M\'agrada i No m\'agrada + Amaga el botó de Comentaris + Amaga el botó \'Reprodueix el xat en viu\' + Amaga el botó de Letres + Ocultar el botó Compartir + Ocultar el botó Desa + Ocultar el botó Descarrega + Amaga el botó \'Inicia la ràdio\' + Menú desplegable + Tria quins components apareixen als menús desplegables del reproductor i de la cua + Amaga el component de 3 columnes + Ocultar els botons de \"M\'agrada\" i \"No m\'agrada\" + Amaga el menú \"Taste match\" + Ocultar el menú \'Afegeix a la llista d\'espera\' + Ocultar el menú \'Afegeix a la cua\' + Ocultar el menú de subtítols + Amaga el menú \"Elimina la llista de reproducció\" + Ocultar el menú \'Neteja la cua\' + Amaga el menú \"No recomani aquest artista\" + Ocultar el menú de descàrrega + Amaga el menú \"Edita la llista de reproducció\" + Amaga el menú \"Ves a l\'àlbum\" + Amaga el menú \"Ves a l\'artista\" + Ocultar el menú \"Anar a l\'episodi\" + Amaga el menú \"Ves al podcast\" + Ocultar el menú d\'Ajuda + Amaga el menú \"Marca l\'episodi com a reproduït\" + Ocultar el menú \"No m\'interessa\" + Ocultar el menú \"Fixar a marcadors\" + Ocultar el menú \"Reproduir després\" + Ocultar el menú de Qualitat + Ocultar el menú \"Eliminar de la biblioteca\" + Ocultar el menú \"Eliminar de la llista de reproducció\" + Ocultar el menú d\'Informar + Ocultar el menú \"Desar l\'episodi per més tard\" / \"Desar a la biblioteca\" + Ocultar el menú \"Desar a la llista de reproducció\" + Ocultar el menú de Compartir + Ocultar el menú \"Reproducció aleatòria\" + Ocultar el menú \"Temporitzador de suspensió\" + Ocultar el menú \"Iniciar ràdio\" + Ocultar el menú \"Estadístiques per a experts\" + Ocultar el menú de Subscriure\'s + Ocultar el menú \"Desfixar de la marcació ràpida\" + Ocultar el menú \"Veure crèdits de la cançó\" + Pàgina d\'inici + Per defecte + Llistes d\'èxits + Episodis per a més tard + Explorar + Historial + Llibreria + Música agradada + Llistes de reproducció + Cercar + Subscripcions + Forçar l\'orientació vertical + Manté l\'aplicació en orientació vertical quan es gira el dispositiu + Canviar el color del miniplayer + Ajusta el color del miniplayer al del reproductor a pantalla completa + Canvia el color de la barra de navegació + Ajusta el color de la barra de navegació al reproductor de pantalla completa + Mostrar el botó següent al miniplayer + Mostrar el botó anterior al miniplayer + Habilitar el miniplayer forçat + Evita que el reproductor s\'expanda automàticament a pantalla completa quan comença una nova cançó + Habilita el desllisement per tancar el miniplayer + Permet tancar el miniplayer desllisant-lo cap avall + Barra de navegació + Tria quins botons apareixen a la barra de navigació inferior + Amaga el botó d\'Inici + Amaga el botó de Mostres + Amaga el botó d\'Explorar + Ocultar el botó de Biblioteca + Ocultar botó de Actualitzar + Ocultar la barra de navegació + Ocultar les etiquetes dels botons de navegació + No has iniciat sessió. Toca per configurar + Tanca la sessió + Sessiont tancada + Activar scrobbling + Enviar l\'estat de \"S\'està reproduint\" + Durada mínima de la cançó + Les cançons més curtes que aquest temps no es faran scrobble + Percentatge de retard de scrobble + Activar el scrobble després d\'escoltar aquest percentatge de la cançó + Segons de retard del scrobble + Activa el scrobble després d\'escoltar aquest nombre de segons + Compte de ListenBrainz + Session obertura. Toca per gestionar el compte + Introdueix el teu Token d\'usuari de ListenBrainz per activar el scrobbling + Token + Verificar + Si us plau, introdueix un token + Validant… + Token vàlid! Usuari: %s + Token invàlid: %s desconegut + Error de verificació: %s + Token eliminat + Token desat + Avís: El token desat és invàlid! + Marcar com a \"encanta\" les pistes agradades + Marcar automàticament com a \"encanta\" les pistes a Last.fm quan t\'agradin a YouTube Music (i eliminar l\'estat d\'\"encanta\" quan no t\'agradin o les marquis com a no agradades) + Compte de Last.fm + Session iniciada com a %s. Toca per gestionar el compte + Introdueix les teves credencials de Last.fm per iniciar sessió + Nom d\'usuari + Contrasenya + Inicia sessió + Si us plau, introduceu el nom d\'usuari i la contrasenya + Iniciant sessió… + S\'ha iniciat la sessió correctament! + Error en obtenir la sessió de Last.fm + Error en l\'inici de sessió: %s + Credencials de Last.fm desades! + Neteja de metadades + Netejar metadades + Neteja automàticament el desordre habitual (com \"Official Video\", \"Remaster\", \"Live\") dels títols, artistes i àlbums abans del scrobbling + Neteja personalitzada per regex + Introdiu una expressió regular personalitzada per eliminar text addicional dels títols de les cançons, artistes o àlbums. Exemple: (?i)\\s*\\((clean|explicit)\\) + Analitza l\'artista i la cançó des del títol + Extreu el nom de l\'artista i de la cançó del títol del vídeo utilitzant el separador \" - \". Útil per a contingut tornat a pujar on el nom del canal pot ser utilitzat com a artista + Omple l\'àlbum faltant + Quan la música no inclou un àlbum, busca\'l a Last.fm perquè el teu scrobble estigui complet + "El scrobbling sincronitza automàticament el vostre ús de YouTube Music amb el vostre perfil de Last.fm o ListenBrainz + +Per assegurar que el scrobbling es sincronitzi durant la reproducció en segon pla, assegureu-vos que l'ús de la bateria de l'aplicació de YouTube Music estigui configurat com a \"Sense restriccions\"" + Utilitzar letres de tercers + Mostra les letres d\'un servei de tercers, revertint a les letres integrades quan no se\'n troba cap + Mantenir la pantalla encesa mentre es veuen les lletres + Evita que la pantalla s\'apagi mentre el panell de lletres està obert + Servei de lets + Visualització + Sincronització + Panell + Endpoint de la IA + Tots els proveïdors desactivats: lletres integrades i lletres incrustades + Restaurar l\'ordre del proveïdor? + Restaura els proveïdors a l\'ordre de prioritat per defecte + Esborrar les lets guardades + Elimina les lets guardades en aquest dispositiu + S\'han esborrat %1$s MB + Obté el token + Token d\'usuari de mitjans d\'Apple Music + "Enganxa la cookie media-user-token de music.apple.com + +Passos: +1. Inicia sessió a music.apple.com com a usuari membre +2. Obre DevTools → Application → Cookies → music.apple.com +3. Copia el valor de \"media-user-token\"" + Apple Music: media-user-token desat + Apple Music: media-user-token esborrat + Apple Music: media-user-token no vàlid; si us plau, comproveu-lo i torneu-ho a intentar + Spotify SpDc + "Enganxa la cookie sp_dc de Spotify. + +Passos: +1. Inicia sessió a open.spotify.com +2. Obre DevTools → Application → Cookies → open.spotify.com +3. Copia el valor de \"sp_dc\"" + Spotify: sp_dc desat + Spotify: sp_dc eliminat + Spotify: sp_dc no vàlid; si us plau, comproveu-lo i torneu-ho a intentar + El token d\'Apple Music és opcional. Sense ell, s\'utilitzarà Lyrically com a alternativa + Deezer ARL + "Enganxa la cookie arl de Deezer. + +Passos: +1. Inicia sessió a deezer.com +2. Obre DevTools $→$ Application $→$ Cookies $→$ deezer.com +3. Copia el valor de \"arl\"" + Deezer: arl desat + Deezer: arl eliminat + Deezer: arl no vàlid - si us plau, comproveu-lo i torneu a provar + Token de Musixmatch + Afegeix un token només per utilitzar el teu propi compte. Ha de provenir de l\'aplicació d\'Android de Musixmatch: els tokens copiats de musixmatch.com pertanyen al client d\'escriptori retirat i l\'API els rebutja + Musixmatch funciona sense un token, per la qual cosa aquest camp pot quedar buit + token d\'usuari + Musixmatch: token d\'usuari desat + Musixmatch: token d\'usuari esborrat + Musixmatch: token d\'usuari no vàlid - si us plau, comproveu-lo i torneu a provar + Cookies de YouTube + "Engranya les teves cookies de YouTube per consultar els subtítols i el text temporal de YouTube. + +Passos: +1. Obre youtube.com en un navegador i inicia la sessió +2. Obre DevTools (F12) → Application → Cookies → youtube.com +3. Copia la cadena completa de cookies (parelles clau=valor separades per punts i coma) + +Cookies requerides: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPISID, LOGIN_INFO" + YouTube: cookies desades + YouTube: cookies esborrades + YouTube: cookies no vàlides - si us plau, comproveu-les i torneu a provar + Mantingueu premuda una línia + Toca una línia per saltar a aquest moment de la cançó, mantingueu-la premuda per copiar la paraula sota el dit, o la línia sencera quan la pressió no sigui sobre una paraula. L\'opció desactivada elimina ambdós gestos + Mostrar etiquetes de secció + Etiqueta cada secció nova de la lletra amb una etiqueta petita, com ara Vers o Estribi + Mostrar botó de Copiar + Toca per copiar la lletra original com a text pla; manté premut per desar el fitxer font de la lletra a Descàrregues + Mostra el botó de Traduir + Toca per alternar la traducció; manté premut per veure només la traducció. Utilitza traduccions del proveïdor, Google Translate o una API compatible amb OpenAI + Mostrar botó de romanització + Toca per alternar la romanització; manté premut per a romanització només. Utilitza la romanització del proveïdor, Google Translate o una API compatible amb OpenAI + Romanització i alignació locals + Romanitza al dispositiu quan el proveïdor, el teu model i Google Translate fallen. La lectura segueix cada paraula d\'una lletra sincronitzada; la sincronització de paraules ha d\'estar activada. Xinès, cantonès, japonès, coreà, grec, rus, georgià, armèni; uns 120 MB + Llegint %1$s: %2$d%% + {0, plural, one {Lectures llistes (1 entrada)} other {Lectures llistes (%1$d entrades)}} + Error de descàrrega: desactivi i torni a activar aquesta opció per reintentar-ho + Mostrar botó d\'actualització + Toca per canviar al següent candidat de lletres; manté premut per buscar lletres pel títol de la cançó i l\'artista + Cerca lets Títol de la cançó Artista Introdueix un títol de la cançó i un artista + Cap dels proveïdors habilitats pot cercar altres cançons + Desat + Resaltar paraules o sílabes sincronitzades + Prioritzar les lets paraula per paraula quan estiguin disponibles. Quan les lets tenen temporització de paraules, resalta cada paraula o sílaba al ritme de la música + Ocultar lets reproduïdes + Ocultar les línies de lets per sobre de la línia que s\'està reproduint actualment + Ocultar lets no reproduïdes + Ocultar les línies de lets per sota de la línia que s\'està reproduint actualment + Mostrar les lets de MediaSession a la pantalla de bloqueig + Mostra la línia actual a la pantalla de bloqueig, Android Auto i Bluetooth. Requereix Android 11 o posterior + Mostrar lletres al miniplayer + Mostra la línia actual al reproductor minimitzat, amb la pista moguda al subtítol + Format del títol de la cançó/artista + Format de la pista que es mostra amb la lletra al miniplayer i a la MediaSession. {t} és el títol, {a} és l\'artista i {v} és la versió, com ara Acústic, quan la font de la lletra en proporcioni una. El text entre dos dos punts només es mostra quan cada variable a l\'interior té un valor; escriviu \\: per a dos punts simples. Cada línia es mostra per torns, avançant sempre que la lletra canviï + Format de la línia de la lletra + Format de la línia de la lletra al reproductor minimitzat i a la MediaSession. {lrc} és la línia i {n} és quantes vegades seguides es canta. El text entre dos punts només es mostra quan la línia es repeteix. Utilitzeu \\: per a els dos punts + El format ha de contenir {lrc} + Plantilla de la línia de l\'artista i temporització de l\'animació + Temps mínim a la pantalla (ms) + Temps màxim a la pantalla (ms) + Només per a animacions de diverses línies. Mantingueu cada quadre durant almenys el temps mínim. Al màxim, avanceu la línia de l\'artista encara que la lletra es mantingui igual. + El mínim ha de ser 0 o superior. El màxim ha de ser superior a 0 i, com a mínim, el valor del mínim. + Limits de longitud de línia + Limits de línia de la lletra i de l\'artista, i pes dels caracters de ple ampleur + "Limit de línia de la lletra +Divideix les lletres més llargues en mitjats equilibrats. El 0 desactiva la divisió." + "Limit de línia de l'artista +Divideix el format de pista omplert en marcs cíclics. El 0 desactiva la divisió." + "Pes dels caracters de ple ampleur +S'aplica a ambos limits. A 1,75, quatre caracters de ple ampleur compten com a 7." + Introduïu limits de nombres enters de 0 a 10000 i un pes de caracters superior a 0 + Interval instrumental + Mostrat al reproductor minimitzat i a la MediaSession durant un interval instrumental. Cada línia és un quadre d\'animació. {t} és el títol, {a} és l\'artista i {lrc} és l\'última línia de la lletra + Desplaçament temporal del reproductor minimitzat i de la MediaSession + Desplaça quan es mostra la següent línia al reproductor minimitzat i a la MediaSession. Utilitzeu un valor negatiu si les línies apareixen tard, per exemple, a la pantalla de bloqueig + Filtres + Regex de metadades + Expressió regular aplicada a títols, artistes i àlbums abans de buscar les lletres i en puntuar si dos títols coincideixen. Deixa-ho buit per desactivar ambdós. Per defecte, s\'eliminen elements comuns com \"Official Video\", \"Remaster\", \"【插曲】\" i \"- Topic\" + Regex del filtre de lletres + Expressió regular aplicada només al text original de les lletres (no a les traduccions ni a les romanitzacions). Deixa\'ho buit per desactivar-ho + Marcadors de línies d\'informació + Si les lletres es filtren incorrectament cap a sota, ajusta o neteja això. Marcadors separats per comes: les línies que contenen un marcador seguit de dos punts es mostren com a informació de crèdits a la part inferior. Deixa\'ho buit per desactivar-ho + Posa la primera lletra en majúscula + Posa en majúscula la primera lletra de cada línia de la lletra + Elimina la puntuació final + Elimina els punts finals, les comes, els dos punts i els punt i coma al final de les línies de la lletra. Es mantenen els signes d\'interrogació, els signes d\'exclamació, les el·lipsis, les cometes i els brackets + Mida del text + Mida del text de les letres + Desfasament temporal global + Les lletres sincronitzades segueixen la reproducció. Si es desfasen, ajusteu el desplaçament temporal. Desplaceu les lletres sincronitzades cap endavant o cap enrere. Utilitzeu un valor positiu si les lletres van per davant de la cançó + Utilitza els segments de SponsorBlock per al desplaçament de les lletres + Coincideix sense els segments que SponsorBlock salta i desplaça les lletres sobre l\'àudio publicat. L\'opció desactivada ignora SponsorBlock completament + Aquesta pista no té cap id de vídeo per enviar + Font: %s + Mantingueu premut el botó de copiar per desar aquest LRC; si podeu ajustar els temps i compartir-los amb els proveïdors de la comunitat, us ho agrairem + Traduït per Google + Romanitzat per Google + Traduït per %s + Romanitzat per %s + Romanitzat per %s + Traduït + Idioma de traducció + Idioma de romanització + Traduint… + No s\'han pogut traduir les letres + Romanitzat Automàtic + Romanitzant… + No s\'han pogut romanitzar les lletres + Només traduït + Només romanitzat + Mode dual + Copiat al porta-retals + Actualitzant + Actualitzat + Clau de l\'API de QuickLRC + QuickLRC transcribeix o aligna temporalment les lletres de la pista actual. Un servei independent, no relacionat amb l\'endpoint compatible amb OpenAI d\'a dalt + Clau de QuickLRC des guarded. Utilitzeu el botó d\'alineació al diàleg de cerca de lletres, o l\'avís del panell quan una pista no tingui lletres + Clau de l\'API de QuickLRC des guardedada + QuickLRC no ha acceptat aquesta clau + S\'ha esborrat la clau de l\'API de QuickLRC + QuickLRC assigna temps a les lletres que no en tenen, o les transcribeix de la pista, a canvi de crèdits. Amb una clau configurada, les pistes sense lletres ofereixen la transcripció i el diàleg de cerca obté un botó d\'alineació + Sol·licitar l\'alineació de QuickLRC + Alineant + Aplicar alineació + Sol·licitar la transcripció de QuickLRC + Transcribent + Transcripció cancel·lada + Només es poden transcrire les pistes reproduïdes des de YouTube + No hi ha lletres per alinear; s\'està transcripció en el seu lloc + QuickLRC no ha pogut processar aquesta pista + S\'han esgotat els crèdits de QuickLRC per a aquesta pista + "Les lletres coincideixen per títol i artista; els metadades inusuals poden no trobar res o la versió incorrecta. Utilitzeu l'actualització per canviar de versió. Els proveïdors marcats amb * requereixen un token. No tots els proveïdors habilitats es solen sol·licitar en el vostre ordre, sinó que responen segons la seva velocitat.\n\nLes lletres provenen de serveis de tercers que no estan afiliats, patrocinats ni avalats per Morphe. Cadascun s'utilitza tal com és: les lletres, la seva disponibilitat i la seva llicència pertanyen a aquests serveis i als seus licenciants, i el seu ús pot estar subjecte als seus termes de servei. El que arriba a un servei habilitat és només la cerca que heu sol·licitat: el títol, l'artista i, quan aquest servei ho requereixi, l'ID del vídeo. El punt final d'IA opcional i la transcripció QuickLRC s'executen només després de configurar-los i reben el text de les lletres o la URL del vídeo de la pista." + Activeu només els proveïdors que utilitzeu, o mantingueu els per defecte: les sol·licituds constants poden saturar els lletres de petits llocs comunitaris + Ocultar línia de crèdits + Oculta la línia de crèdits que apareix sota la lletra + Intercanvia la traducció i la romanització + ACTIVAT posa la romanització sota la lletra i la traducció a sobre; DESACTIVAT manté la romanització a sobre i la traducció a sota + Traducció + Romanització + Proveïdor de traducció + Google (per defecte, gratuït) + Compatible amb OpenAI (personalitzat) + Proveïdor de romanització + Google (per defecte) + Diccionari local / fora de línia + Compatible amb OpenAI (Personalitzat) + Secció de IA + Només s\'utilitza quan el proveïdor no envia etiquetes de secció com el tornestor o la strofa, que el vostre model compatible amb OpenAI detecta posteriorment + IA & Cloud Endpoints + Configureu les claus API, els models, els punts d\'extremitat i les plantilles de prompt per als proveïdors de IA. + URL de l\'endpoint de l\'API (p. ex. https://api.openai.com/v1/chat/completions) + Plantilla del prompt + Marcadors de posició: {language}, {task}, {title}, {artist}, {count}, {lines} + Prompt desat + Prompt guardat, però l\'endpoint de la IA encara no és utilitzable. Configureu-lo primer a l\'apartat de Configuració de la IA + Prompt reiniciat + JSON no vàlid. Corregiu-lo o buideu el camp abans de desar + Configuració de traducció & romanització guardada + Configuració restableixida per defecte + L\'URL base ha de començar per http:// o https:// + Provar connexió + Configurar endpoints + Mostrar tots els endpoints + Validant la connexió… + Proveïdor de traducció + Proveïdor de romanització + Obtenir models + Obtint models… + Seleccioneu un model + No s\'han pogut obtenir els models. Comproveu la clau API o la connexió. + Configuració de l\'API de Gemini + Compatible amb OpenAI (punta final personalitzada) + Configuració de l\'API de DeepL + Clau de l\'API + Model + URL base + Clau de l\'API (opcional) + Prompt de traducció + Cos de la sol·licitud extra (JSON opcional) + Clau d\'autenticació (Gratuïta o Pro) + Configureu les claus de l\'API, els models, els punts finals i els prompts + 8.23.51 - Desactiva les icones en negreta + 9.13.51 - Restaura les pestanyes Pròxim del reproductor, Letres i Relacionats diff --git a/patches/src/main/resources/addresources/values-ca-rES/reddit/strings.xml b/patches/src/main/resources/addresources/values-ca-rES/reddit/strings.xml index dce31d6240..3a38d4a075 100644 --- a/patches/src/main/resources/addresources/values-ca-rES/reddit/strings.xml +++ b/patches/src/main/resources/addresources/values-ca-rES/reddit/strings.xml @@ -16,9 +16,122 @@ Second \"item\" text" --> + Política de privadesa de Reddit + L\'inici de sessió de Google no està disponible. Utilitzeu el correu electrònic per iniciar la sessió Publicitat + Ocultar anuncis del feed + Ocultar anuncis dels comentaris + Barra de navegació + Ocultar el botó de Respostes + Ocultar el botó de Xat + Ocultar el botó de Crear + Ocultar el botó de Descobrir o Comunitats + Amaga el botó de Jocs + Barra lateral + Amaga l\'estant de Visitades recentment + Amaga l\'estant de Jocs a Reddit + Amaga l\'estant de Reddit Pro + Amaga l\'estant d\'Informació + Amaga l\'estant de Recursos + Disseny + Copiar text + Permet seleccionar i copiar text a les publicacions i els comentaris. Desactiva la col·lapsació de comentaris + La col·lapsació de comentaris deixarà de funcionar mentre l\'opció Copiar text estigui activada + Tipografia personalitzada + Carrega un arxiu de tipografia TTF o OTF personalitzat des de la ruta següent. En activar-ho també s\'desactiva Forçar la tipografia del sistema + Arxiu de tipografia personalitzada + Toca per triar un arxiu de tipografia TTF o OTF + Tipografia seleccionada - %s + Tria primer un arxiu de tipografia personalitzada + Si us plau, selecciona un arxiu de tipografia .ttf o .otf + Error en carregar la tipografia personalitzada. Tria un arxiu diferent + Desactiva la UI moderna de l\'inici + Desactivar la finestra emergent de captures de pantalla + Desactiva la finestra emergent que apareix quan es fa una captura de pantalla + Força la font del sistema + Utilitza la tipografia del sistema del dispositiu en lloc de Reddit Sans o Roboto. En activar-ho també s\'desactiva la Tipografia personalitzada + Ocultar el botó de Preguntar + Oculta el botó de Preguntar a la barra de cerca + Oculta el prestatge de comunitats + Oculta el prestatge de comunitats relacionades o suggerides als subreddits + Oculta els prestatges de Tendències + Oculta els prestatges de Tendències del feed i les suggerències de cerca + Elimina el diàleg de suggeriment de notificació + Descarta automàticament el diàleg de suggeriment de notificació quan visites un subreddit + Mostrar etiquetes al feed d\'inici + Mostra les insignies d\'etiquetes de publicació al feed d\'inici + Alguns elements del feed potser no mostrin una etiqueta + Mostrar el nombre de visualitzacions als posts + Limitació: Només disponible si la resposta de l\'API inclou el nombre de visualitzacions + Invertir la barra d\'accions de la publicació + Desplaça els botons de votació i comentaris de les publicacions al costat dret, i els botons de publicació creuada i compartir al costat esquerre + Intercanviar els botons de votació i comentaris + Mostra el botó de comentaris de les publicacions abans dels botons de votació + Elimina el diàleg d\'advertència NSFW + Accepta automàticament l\'avís de contingut NSFW quan visites un subreddit Miscel·lània Quant a + Versió del patch i enllaços oficials + Neteja els enllaços per compartir + Elimina els paràmetres de consulta de seguiment dels enllaços compartits + Obre els enllaços directament + Omiteix les URL de redireccionament en obrir enllaços externs + Mitjans del feed a tot l\'amplada + Mostra imatges i vídeos de vorige a vorige al feed i als detalls de la publicació, sense padding lateral ni cantonades arrodonides + Intensitat de l\'esvaniment del visor de mitjans + Intensitat de l\'efecte d\'ombrejat fosc darrere del subtítol en visualitzar imatges i vídeos, de 0 (sense ombrejat) a 1 (per defecte de Reddit) + Mantenir la posició del feed + Manté el feed a la mateixa posició després de tancar una publicació, imatge o vídeo, en lloc de moure la publicació oberta a la part superior + Alçada màxima de els mitjans del feed + Altura màxima de les imatges, els vídeos i les galeries al feed, com a múltiple de la seva amplada. El contingut més alt es retalla. 1,33 és el valor per defecte de Reddit + Reproduir Redgifs amb àudio + Reprodueix el vídeo original de Redgifs en lloc de la còpia de Reddit, que sovint no té àudio + Qualitat HD de Redgifs + Reprodueix el vídeo de Redgifs en HD. Els vídeos en HD són més grans i poden pausar-se per carregar + Ubicació de descàrrega + Tots els comptes + Per defecte a Reddit: imatges a Pictures/Reddit, vídeos a Download + Carpeta: %s + Nom del fitxer: %s + Noms de fitxer de Reddit + Utilitza \"Tots els comptes\" + Utilitza una ubicació pròpia + Desa a + Carpetes per defecte de Reddit + Per defecte + Subcarpeta + Afegeix subcarpetes amb / al nom del fitxer + Navega… + No hi ha cap selector de carpetes disponible en aquest dispositiu + Nom del fitxer + Nom del fitxer de Reddit + Toca un token per inserir-lo. Una / crea una subcarpeta. Els valors desconeguts es converteixen en \"desconegut\" + Imatges: %s + Vídeos: %s + Desa + Restaurar Eliminar + Restaurar la ubicació de descàrrega de tots els comptes a la configuració per defecte de Reddit? + Es borrarà la ubicació de descàrrega de %s? + No s\'ha pogut desar a la ubicació de descàrrega. S\'ha desar a la carpeta per defecte de Reddit en el seu lloc + La carpeta de descàrrega ja no està disponible. S\'ha desar a la carpeta per defecte de Reddit en el seu lloc + No s\'ha pogut desar el vídeo descarregat + Aquesta carpeta no es pot utilitzar per a descàrregues + %s (actual) + %s (sense iniciar sessió) + Anònim (sessió tancada o incògnit) + Ocultar la capa del visor de mitjans + Oculta el títol, els botons i els controls de vídeo en obrir una imatge o un vídeo. Toqueu el contingut per mostrar-los + Ocultar el botó Veu la conversa + Oculta el botó \"Veu la conversa\" a la part inferior del visor de mitjans + Fons negre del visor de mitjans + Utilitza un fons negre darrere les imatges i els vídeos al visor de mitjans, en lloc del color de fons del tema + Obrir els enllaços externament + Obre els enllaços al vostre navegador en lloc del navegador intern de l\'aplicació + Recordar els comentaris recol·lits + Els comentaris que col·lapses es mantenen col·lapsats quan tornes a obrir la publicació + Icona de l\'aplicació + Trieu la icona de l\'aplicació + Per defecte Desconegut diff --git a/patches/src/main/resources/addresources/values-ca-rES/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-ca-rES/shared-youtube/strings.xml index e86ad344e1..2f02d82a8b 100644 --- a/patches/src/main/resources/addresources/values-ca-rES/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ca-rES/shared-youtube/strings.xml @@ -18,47 +18,296 @@ Second \"item\" text" --> + Animació d\'inici Deshabilitat + Color + Monocrom + + Desactivat + Reinicia per acabar la configuració + Nom de l\'aplicació + Fons del tema fosc + Negre pur + Material You (Negre pur) + YouTube clàssic + Rosa fosc + Blau fosc + Verd fosca + Groc fosc + Taronja fosca + Vermell fosca + Fons fosca personalitzat + Estableix el color de fons del tema fosca + Per defecte de l\'app + Material You (Neutre) + Material You (Primari) + Material You (Secundari) + Material You (Terciari) + Personalitzat + Descàrregues + Mode de descàrrega + Per defecte + Descarregar a l\'aplicació + Descarregador extern + Nom del paquet del descarregador + Nom del paquet de l\'aplicació de descàrrega externa instal·lada + Introdueix el nom del paquet + Altre + L\'aplicació no està instal·lada + %s no està instal·lat. Si us plau, instal·leu-lo + "No s'ha pogut trobar cap aplicació instal·lada amb el nom de paquet: %s + +Verifiqueu que el nom del paquet sigui correcte i que l'aplicació estigui instal·lada" + El nom del paquet no es pot deixar buit + Personalitzat + Icona de l\'aplicació + Original + Morphe negre + Morphe fosc + Llum Morphe + Morphe play clar + Morphe play negre + Personalitzat + "Això canvia la icona que apareix al llançador d'aplicacions. + +Aquest ajust no canviarà la icona en cap d'aquestes ubicacions: +• Llista d'aplicacions de l'ajust del sistema del dispositiu +• Algunes notificacions push +• Selector d'aplicacions 'Obrir amb' +• Icona d'Android Auto + +Per canviar la icona a tot arreu, aplica el patch novament i canvia l'opció de patch 'Icona de l'app'" + Icona de notificació + Coincidència amb la icona de l\'app + Morphe play + Avís de Morphe + L\'historial de visionat no s\'està desant.<br><br> Això és causat molt probablement per un bloquejador d\'anuncis DNS o un proxy de xarxa.<br><br> Per solucionar-ho, afegiu <b>s.youtube.com</b> a la llista blanca o desactiveu tots els bloquejadors DNS i proxies + No torni a mostrar + Simular fluxos de vídeo + Configura la simulació del client per evitar problemes de reproducció + "Soluciona problemes de reproducció per a la majoria d'usuaris sol·licitant un flux de vídeo compatible + +Si sou un usuari de YouTube Premium, potser aquest ajust no sigui necessari" + No s\'ha pogut recuperar cap flux de client + El client per defecte no funciona + "%1$s no ha funcionat diverses vegades seguides, però %2$s sí. + +Defineix %2$s com el client per defecte?" + Canviar + Desactivar aquest ajust pot causar problemes de reproducció + Intenti d\'activar: Simulació de fluxos de vídeo > Iniciar sessió a Android VR + Client per defecte + Inici de sessió a Android VR + No heu iniciat sessió a Android VR. Toqueu aquí per iniciar sessió + Sessió iniciada actualment a Android VR + Per utilitzar aquesta funció, canvieu el \"Client per defecte\" a Android VR + Inicia sessió a Android VR + "Iniciar sessió a Android VR pot solucionar els problemes de reproducció per a alguns usuaris + +Per iniciar la sessió, premeu continuar a continuació" + Tanca la sessió + Instruccions + "Codi d'activació: %s + +1. Toqueu 'Obre el lloc web' a sota per obrir el lloc web 'Connecta un dispositiu' +2. Engaltréu des del porta papers (o escriviu) el codi d'activació indicat a dalt +3. Inicia la sessió a Google si se n'hi sol·licita. Si teniu diversos comptes, trieu aquest compte de YouTube +4. Toqueu 'Permet l'accés a YouTube VR' +5. Torneu a aquesta aplicació després de veure el missatge 'Dispositiu connectat'" + S\'ha produït un error en emissió el codi d\'activació + S\'ha produït un error en emissió del token d\'autenticació + S\'ha iniciat la sessió correctament + "Autenticació guardada + +• La configuració de Morphe no importarà/exportarà l'autenticació d'Android VR +• Si aquesta aplicació no s'utilitza durant 6 mesos, Google eliminarà aquesta autenticació +• Haurà de tornar a iniciar la sessió si canvia la contrasenya de Google o desregistra aquest dispositiu de la configuració de Google" + OAuth2 no està disponible temporalment (esvaeix el temps d\'espera de l\'API) + La sessió de VR d\'Android ha caducat, intenteu iniciar sessió a VR novament + OAuth2 no està disponible temporalment (estat %s) + OAuth2 no està disponible temporalment + No es pot iniciar sessió a VR, l\'autenticació del lloc web no s\'ha aprovat + No es pot iniciar sessió a VR: %s + S\'ha tancat la sessió d\'Android VR + Token d\'inici de sessió de VR invàlid, intentiu iniciar sessió a VR novament + Controls de mitjans de la notificació + Personalitza els controls de reproducció que apareixen a la pantalla de bloqueig i al panell de notificacions + Ocultar els botons Anterior & Següent + Oculta els botons Anterior i Següent a la notificació de mitjans, a la pantalla de bloqueig i als controls dels auriculars + Desactivar la barra de cerca + Impedix d\'arrossegar la barra de cerca a la pantalla de bloqueig i al panell de notificacions + Desplaçar amb els botons Anterior & Següent + Canvia els controls de la notificació de mitjans, la pantalla de bloqueig i els auriculars per desplaçar-se en lloc de canviar de vídeo + Longitud del retrocess + Segons per retrocedir amb el botó Anterior + Longitud de l\'avanç ràpid + Segons per avançar ràpidament amb el botó Següent + La durada de la cerca ha de ser entre 1 i %s segons. S\'utilitza el valor per defecte + Desactiva el protocol QUIC + Desactivar l\'àudio DRC + Forçar l\'idioma de l\'àudio original + Sempre reprodueix la pista d\'àudio original del vídeo en lloc d\'una versió doblada + Omitir el silenci + Omet automàticament les pauses silencioses en l\'àudio del vídeo + Longitud mínima de la pausa + Nomes s\'acurtiran les pauses que siguin almenys d\'aquesta durada. Tindrà efecte quan comenci el següent vídeo + Proxy de xarxa + Enrutar les sol·licituds de xarxa compatibles a través d\'un proxy + Habilitar el proxy + Enruta les sol·licituds de xarxa compatibles a través d\'un proxy HTTP o HTTPS configurat + Host del proxy + Nom de l\'host o adreça IP del servidor proxy + Port del proxy + Port del servidor proxy + Utilitzar proxy HTTPS + Utilitzar HTTPS per connectar-se al servidor proxy en lloc de HTTP + Autenticació de proxy + Envia l\'autenticació bàsica amb les sol·licituds de proxy + Nom d\'usuari del proxy + Nom d\'usuari per a l\'autenticació bàsica del proxy + Contrasenya del proxy + Contrasenya per a l\'autenticació bàsica del proxy + Permetre el ressòrt directe + Connecta\'t directament si el proxy falla + Oculta els anuncis a pantalla completa + Per utilitzar aquesta funció, canvieu \'Spoof video streams\' a qualsevol client excepte \'Android Studio\' i \'Android VR Downgraded\' + Netejar els enllaços per compartir + Elimina els paràmetres de consulta de seguiment dels enllaços compartits + Canviar els enllaços per compartir a youtube.com + Substitueix music.youtube.com per youtube.com als enllaços compartits + Canvia els enllaços de compartir a youtu.be + Força els vídeos a utilitzar el format d\'URL curt youtu.be + Buffer de protocol del registre de depuració + Afegeix informació de registre addicional per a els desenvolupadors per identificar els components de l\'app + "En activar aquest ajust s'enregistrarán dades addicionals de la disposició, incloent-hi el text a la pantalla de alguns components de la interfície d'usuari. + +Això pot ajudar a identificar components en crear filtres personalitzats. + +No obstant això, l'activació d'això també registraràAlgunes dades de l'usuari, com l'adreça IP" + Filtre personalitzat + Configureu filtres personalitzats per ocultar qualsevol component + Habilita el filtre personalitzat + Llista de cadenes del constructor de rutes de components per filtrar, separades per salts de línia + Filtre personalitzat no vàlid: %s + Filtre del menú de configuració + Oculteu elements de la pantalla de configuració estàndard de l\'aplicació pel seu nom visible + Obriu la pantalla de configuració estàndard de l\'aplicació un cop per omplir la llista + Eliminar el diàleg de discreció de l\'espectador + Això no ignora la restricció d\'edat. Simplement l\'accepta automàticament + Provider de PoToken + Configura el provider de PoToken per evitar problemes de reproducció + Proveïdor خارجي de PoToken + "Soluciona problemes de reproducció utilitzant un generador de PoToken extern (experimental) + +Si ets un usuari de YouTube Premium, és possible que aquesta configuració no sigui necessària" + "Soluciona problemes de reproducció utilitzant un generador de PoToken extern (experimental) + +Per utilitzar aquesta funció, desactiva \"Simular fluxos de vídeo\"" + Si no funciona, intenta forçar el tancament de MicroG + Obre la configuració de PotHelper + "Aquesta funció està actualment en desenvolupament i és possible que no funcioni + +Toca aquí per descarregar PotHelper" + PotHelper ja no funciona + "El token integrat en aquesta versió de PotHelper ja no funciona + +Activa 'Simular fluxos de vídeo' per solucionar la reproducció" + Activa + Simular versió de l\'app + Enganya el servidor fent-li creure que fas servir una versió d\'app més antiga per restaurar funcions de la interfície heredades + "La versió de l'app es simularà com una versió anterior. + +Això canviarà l'aparença i les funcions de l'app, però poden occorrer efectes secundaris desconeguts. + +Si més tard s'estapat, es recomana esborrar les dades de l'app per evitar errors de la interfície d'usuari" + Objetiu de simulació de versió de l\'app + Freqüència d\'actualització de l\'app + Per defecte + %s FPS + Anular la freqüència d\'actualització + Sempre + Reproducció en vertical + Reproducció a pantalla completa + Reproducció en vertical i a pantalla completa + interactors + Per defecte + Font del sistema del dispositiu + Arxiu de font personalitzada + Arxiu de font personalitzada + Selecciona un arxiu de font TTF o OTF + L\'arxiu seleccionat no és una font TTF o OTF + No s\'ha pogut carregar la tipografia personalitzada i s\'ha desactivat + Els \"no m\'agrada\" no estan disponibles temporalment (temps d\'espera de l\'API esgotat) + Els \"no m\'agrada\" no estan disponibles (estat %s) + Els \"no m\'agrada\" no estan disponibles (límit de l\'API del client) + Els \"no m\'agrada\" no estan disponibles (%s) + Ocultat per el propietari + No agrada aquest vídeo, juntament amb %s altres persones + No agrada aquest vídeo, %s dels vots són negatives + Habilitar Return YouTube Dislike + Mostrar els \"no m\'agrada\" com a percentatge + Mostra els \"no m\'agrada\" com a un percentatge dels \"m\'agrada\" en lloc d\'un número + Mostrar els \"m\'agrada\" estimats + Mostra un recompte estimat de \"m\'agrada\" en els vídeos on el creador ha deshabilitat els \"m\'agrada\" + Mostrar alertes d\'error de connexió + Mostra una notificació quan el servidor de Return YouTube Dislike no està disponible + "Return YouTube Dislike restaura els recomptes de \"no m'agrada\" que YouTube va ocultar el 2021. Els recomptes provenen de dades arxivades abans de l'eliminació i s'estimen per als vídeos més recents a partir dels vots dels seus usuaris, per la qual cosa són aproximats i s'actualitzen cada pocs dies + +L'ID del vídeo que Miros veu s'envia a l'API de Return YouTube Dislike, i els vostres \"m'agrada\" i \"no m'agrada\" s'envien de forma anònima per millorar les estimacions" + Estadístiques de l\'API de Return YouTube Dislike d\'aquest dispositiu + Temps de resposta de l\'API, mitjana + Temps de resposta de l\'API, mínim + Temps de resposta de l\'API, màxim + Temps de resposta de l\'API, últim contingut multimèdia + Els \"no m\'agrada\" no estan disponibles temporalment - El límit de velocitat de l\'API del client està actiu + API de recuperació de vots, nombre de trucades + No s\'han realitzat trucades de xarxa + API de recuperació de vots, nombre de temporitzadors esgotats + Ninguna trucada de xarxa ha esgotat el temps d\'espera + Límits de velocitat del client de l\'API + No s\'han trobat límits de velocitat del client + S\'ha trobat el límit de freqüència del client %s vegades + %s mil·lisegons diff --git a/patches/src/main/resources/addresources/values-ca-rES/shared/strings.xml b/patches/src/main/resources/addresources/values-ca-rES/shared/strings.xml index 2a1f9a739a..d3e4bb3a13 100644 --- a/patches/src/main/resources/addresources/values-ca-rES/shared/strings.xml +++ b/patches/src/main/resources/addresources/values-ca-rES/shared/strings.xml @@ -17,25 +17,219 @@ Second \"item\" text" --> + Configuració + Està segur que vols procedir? + Desar + Restaurar + Restaurar color + Color invàlid + Cal reiniciar + Reinicia l\'aplicació perquè aquest canvi surti ef {ect} + Reiniciar + Importar + Copiar + S\'han restableit els ajustos de Morphe als valors per defecte + S\'han importat %s configuracions + Error d\'importació: %s + Navega cap amunt + Cerca ajustos Quant a + No s\'han trobat resultats per a \'%s\' + Prova una altra paraula clau + Cerques recents + Vols eliminar-ho de l\'historial de cerques? + Elimina de l\'historial de cerca + Netejar l\'historial de cerques + Està segur que vol netejar tot l\'historial de cerques? + Consells de cerca + "• Toqueu una ruta per navegar-hi +• Mantingueu premut un ajust per navegar-hi +• Premeu Enter per desar una consulta de cerca a l'historial +• La cerca ignora les majúscules i la puntuació +• Els ajusts pare apareixen per sobre dels ajusts fills deshabilitats" + L\'historial de cercas està buit + Per desar l\'historial de cercas, escriviu una consulta de cerca i premeu Enter + Mostrar l\'historial de cercas d\'ajusts + Nom de la configuració de Morphe + Per defecte + Extras + Ajustos + Temps de morphing + Personalitzat + Introduïu un nom + El nom no es pot deixar buit + Mostrar les icones d\'ajusts de Morphe + Copiat: %s + Idioma de Morphe + "És possible que faltin traduccions o que estiguin incompletes per a alguns idiomes. + +Per traduir nous idiomes o millorar les traduccions existents, visita morphe.software/translate" + Idioma de l\'aplicació + Importar / Exportar + Importar / Exportar configuració de Morphe + Importar des d\'un fitxer + Configuració importada correctament, toca Desa per aplicar-la + Error en importar la configuració + Exportar a un fitxer + Configuració exportada correctament + Error en exportar la configuració + Estàs utilitzant l\'última versió de Morphe Patches <i>%s</i> + Estàs utilitzant la versió <i>%1$s</i> de Morphe Patches<br><br><b>Actualització disponible: <i>%2$s</i></b><br><br>Per actualitzar els teus retalls, utilitza Morphe per tornar a retallar aquesta aplicació + Nota + Aquesta versió és una versió preliminar i és possible que experiencieu problemes inesperats + Enllaços oficials + Lloc web + Donació + Traduccions + Crèdits + Actualitzat + Actualització disponible + Desenvolupament actual + Desenvolupament anterior + Desenvolupament actiu + 2022 ‑ 2025 + 2022 ‑ 2025 + 2019 ‑ 2022 + Avisos de llicència + Carregant… + Depuració + Configureu les opcions avançades de desenvolupador i els registres de diagnòstic + Habiliteu el registre de depuració + Seguiment de la pila del registre de depuració + Inclou rastres de la pila als registres de depuració per ajudar a identificar errors + Mostrar notificació en cas d\'error de Morphe + "Desactivar les notificacions d'error oculta totes les notificacions d'error de Morphe. + +No rebràs cap notificació d'esdeveniments inesperats" + Exportar registres de depuració + Exportar registres de depuració de Morphe + Registres de depuració exportats correctament + Error en exportar els registres de depuració + Cerca als registres de depuració… + Buida + El registre de depuració està desactivat + No s\'han trobat registres de depuració + Registres de depuració copiats + Error en exportar els registres de depuració: %s + Registres de depuració eliminats + Gestor de banderes de funcions + Gestiona els indicadors de funcions booleanes + Cerca indicadors de funcions… + No actiu (%s) + Actius (%s) + Bloquejats (%s) + Forçats (%s) + Automàtic Apagat Activat + Forçar activació + Bloquejar Eliminar + Indicadors de funcions guardats + Indicadors de funcions restableixits + Indicadors de funcions copiats al porta papers + Reinicia l\'aplicació per utilitzar aquest ajust + Exportar i importar + S\'han importat %s indicadors de funcionalitat + No s\'han trobat indicadors de funcionalitat + Indícadors de funcions exportats correctament + Error en exportar els indicadors de funcions + Indicadors de funcions carregats, toca Desa per aplicar + S\'ha produït un error en llegir el fitxer + Cerca binària + "Troba quin indicador de funcions causa un comportament de l'aplicació. + +La meitat dels %s indicadors estan bloquejats i, després de cada reinici, indiques si el comportament encara persisteix. + +Reproduï el comportament una vegada abans de començar; si no, els indicadors que l'aplicació encara no ha sol·licitat no apareixeran a la cerca" + Comença + "No hi ha indicadors de funcions per buscar. + +Només s'hi poden utilitzar els indicadors que l'aplicació ha detectat i que actualment no estan forçats a l'estat activat" + "Pass %1$s + +Queden %2$s indicadors de funcions, %3$s d'ells estan bloquejats per a aquest pass. + +Reinicia l'aplicació, comprova si el comportament persisteix i després respon a continuació" + Cerca d\'indicadors de funcions en curs + El comportament està present + El comportament no està present + Cancel·lar la cerca binària + Cerca binària cancel·lada + "L'indicador de funcions %s causa el comportament. + +Ara està bloquejat" + "No s'ha pogut trobar cap indicador de funcions. + +Sembla que el comportament de l'aplicació no estigui controlat per un indicador de funcions" + "No s'ha pogut trobar cap indicador de funcions. + +El comportament ha desaparegut abans a la cerca, per la qual cosa és possible que hi hagi més d'un indicador implicat o que una de les respostes fos incorrecta" + Obre els ajusts de MicroG + MicroG no està instal·lat. Instal·la\'l + Cal realitzar una acció + "La versió %1s de MicroG està desactualitzada i pot causar problemes. + +Si us plau, actualitzeu MicroG a l'última versió v%2s" + Actualitzar + Ignorar + "El MicroG instal·lat no és recomanat per Morphe: + +%1s + +Toca per desinstal·lar-lo, torna a obrir aquesta aplicació i instal·la MicroG RE" + Desinstal·lar + "MicroG no té permís per s'executar en segon pla. + +Segueix la guia \"Don't kill my app\" per al teu telèfon i aplica les instruccions a la teva instal·lació de MicroG. + +Això és necessari perquè l'aplicació funcioni" + Obre el lloc web + "Les optimitzacions de bateria de MicroG s'han de desactivar per evitar problemes. + +Desactivar les optimitzacions de bateria per a MicroG no afectarà negativament l'ús de la bateria. + +Toca el botó de continuar i permet els canvis d'optimització" + Continua + Mostra el diàleg d\'optimització de la bateria de MicroG + "Si desactiveu això, s'ocultarà l'avís d'optimització de la bateria de MicroG. + +Si no es canvien les optimitzacions de la bateria, és possible que experimenteu: +• L'aplicació es pot tancar aleatòriament +• Les notificacions podrien no funcionar +• La reproducció en segon pla podria no funcionar" + Gestió d\'enllaços de l\'app + Configureu els enllaços per obrir-los en aquesta aplicació + Pass 1: Desactiva els enllaços de l\'app original + Obre la configuració de l\'aplicació original i desactiva tots els enllaços de domini + Pass 2: Activa els enllaços de l\'app amb retalls + Obre la configuració de l\'aplicació amb retalls i activa tots els enllaços de domini + Obre la configuració + 🚨 Alerta experimental 🚨 + "Estàs utilitzant una versió experimental de l'aplicació de ⚠️ <b>%1$s</b> +<br/><br/> +🔧 És possible que l'aplicació tingui un comportament estrany o errors no identificats mentre ajustem els retalls per a aquesta versió. +<br/><br/> +Si vols l'experiència amb menys problemes, <b>desinstal·la</b> aquesta i instal·la la versió recomanada de ✅ <b>%2$s</b>" + ⚠️ Vull l\'experimental + ✅ Vull la versió estable + Registre d\'errors desat als documents de l\'usuari: %s + Registre d\'errors desat: %s diff --git a/patches/src/main/resources/addresources/values-ca-rES/sponsorblock/strings.xml b/patches/src/main/resources/addresources/values-ca-rES/sponsorblock/strings.xml index 9d2d4ab7d5..0da66df96b 100644 --- a/patches/src/main/resources/addresources/values-ca-rES/sponsorblock/strings.xml +++ b/patches/src/main/resources/addresources/values-ca-rES/sponsorblock/strings.xml @@ -16,7 +16,219 @@ Second \"item\" text" --> + Habilita SponsorBlock + SponsorBlock és un sistema basat en la col·laboració popular per saltar les parts molestes dels vídeos de YouTube + Aparença + Mostra el botó de votació + Afegeix un botó per votar positivament o negativament els segments + Utilitza el disseny quadrat + Utilitza cantonades quadrades per als botons i controls de SponsorBlock + Utilitza el botó de salt compacte + Redueix l\'amplada del botó de salt per estalviar espai + Ocultar automàticament el botó de saltar + Oculta el botó de saltar després de uns pocs segons en lloc de mantenir-lo visible durant tot el segment + Durada del botó de saltar + Estableix quant de temps roman visible el botó de saltar + Mostrar notificació de salt + Mostra una notificació quan un segment es salta automàticament + Durada de la notificació de salt + Estableix quant de temps roman visible la notificació de desfer + 1 segon + 2 segons + 3 segons + 4 segons + 5 segons + 6 segons + 7 segons + 8 segons + 9 segons + 10 segons + Mostra la durada del vídeo sense segments + Soutraeu tots els segments de la durada total del vídeo mostrada a la barra de recerca + Creació de nous segments + Mostra el botó \"Crear nou segment\" + Afegeix un botó al reproductor de vídeo que permet enviar nous segments de SponsorBlock + Pass de l\'ajust de temps + Quantitat de desplaçament dels botons d\'ajust de temps (en mil·lisegons) + El valor ha de ser un número positiu + Directrius d\'enviament + Les directrius contenen regles i consells per a crear nous segments + Segueix les directrius + Llegeu les directrius de SponsorBlock abans de crear nous segments + Ja l\'he llegit + Mostra\'m-ho + General + Mostrar alertes d\'error de connexió + Mostra una notificació quan el servidor de SponsorBlock no està disponible + Habilitar el seguiment del recompte de salts + Envia els recomptes de salt a la taula de classificació de SponsorBlock per seguir quant de temps s\'estalvia + Durada mínima del segment + Els segments més curts que aquest valor (en segons) no es mostraran ni es saltaran + Durada de temps no vàlida + El vostre ID d\'usuari privat + Això s\'ha de mantenir privat. És com una contrasenya i no s\'ha de compartir amb ningú. Si algú té això, pot suplantar-vos + L\'ID d\'usuari privat ha de tenir almenys 30 caràcters de longitud + Canviar l\'URL de l\'API + L\'adreça que SponsorBlock utilitza per fer trucades al servidor + Llista blanca de canals + SponsorBlock està deshabilitat per als canals de la llista blanca + Notificar en canal de la llista blanca + Mostra una notificació quan SponsorBlock està deshabilitat per a un canal de la llista blanca + SponsorBlock deshabilitat per a aquest canal + URL de l\'API restablerta + L\'URL de l\'API no és vàlida + S\'ha canviat l\'URL de l\'API + Configuració d\'importació / exportació + Copiar + La vostra configuració JSON de SponsorBlock que es pot importar / exportar a Morphe i a altres plataformes de SponsorBlock + "La vostra configuració JSON de SponsorBlock que es pot importar / exportar a Morphe i a altres plataformes de SponsorBlock + +Això inclou el vostre ID d'usuari privat. Assegureu-vos de compartir-lo amb prudència" + Configuració importada correctament + Error en l\'importació: %s + Error en l\'exportació: %s + "La teva configuració conté un ID d'usuari privat de SponsorBlock. + +El teu ID d'usuari és com una contrasenya i mai s'ha de compartir" + No torni a mostrar + Canvia el comportament del segment + Patrocinador + Promoció pagada, referències pagades i anuncis directes. No per a l\'autopromoció ni per a mencions gratuïtes a causes / creadors / webs / productes que els agradin + Autopromoció / No pagat + Similar al Patrocinador, excepte per a l\'autopromoció / no pagat. Inclou seccions sobre mercadandria, donacions o informació sobre amb qui han col·laborat + Recordatori d\'interacció (Subscriure\'s) + Un recordatori curt per donar \'like\', subscriure\'s o seguir-los a mitjans del contingut. Si és llarg o sobre alguna cosa específica, hauria d\'estar sota autopromoció + Destacat + La part del vídeo que la majoria de gent busca + Intermissió / Animació d\'intro + Un interval sense contingut real. Pot ser una pausa, un quadre estàtic o una animació repetitiva. No inclou transicions que contenguin informació + Pantalles finals / Crèdits + Crèdits o quan apareixen les pantalles finals de YouTube. No per a conclusions amb informació + Ganxo / Salutacions + Tràilers narrats per al proper vídeo, salutacions i descriptors. No inclou seccions que afegeixin contingut addicional + Avança / Recapitulació + Col·lecció de clips que mostren què vindrà o què ha passat al vídeo o en altres vídeos d\'una sèrie, on tota la informació es repeteix en una altra part + Digressió / Bromes + Escenes digressives o bromes que no són necessàries per entendre el contingut principal del vídeo. No inclou seccions que proporcionin context o detalls de fons + Música: Secció sense música + Només per a ús en vídeos musicals. Seccions de vídeos musicals sense música que no estiguin cobertes per una altra categoria + Saltar + Destacat + Saltar patrocinador + Saltar promo + Saltar interacció + Saltar al fragment highlight + Saltar introducció + Saltar intermissió + Saltar intermissió + Saltar final + Saltar ganxo + Saltar vista prèvia + Saltar vista prèvia + Saltar resum + Saltar digressió + Salta el contingut no musical + Salta el segment + Patrocinador saltat + Autopromoció saltada + Recordatori molest molt saltat + Saltat fins al punt destacat + Introducció saltada + Intermissió saltada + Intermissió saltada + Final saltat + Ganxo saltat + Vista prèvia saltada + Vista prèvia saltada + Resum saltat + Digressió saltada + S\'ha saltat una secció sense música + Segment no enviat saltat + Diversos segments saltats + Saltar automàticament + Saltar automàticament un cop + Mostrar un botó de saltar + Mostrar a la barra de cerca + Deshabilitar + No s\'ha pogut enviar el segment: %s + SponsorBlock no està disponible temporalment + No s\'ha pogut enviar el segment (estat: %1$s %2$s) + No s\'ha pogut enviar el segment. Límite de freqüència (tropats d\'un mateix usuari o IP) + No es pot enviar el segment: %s + "No es pot enviar el segment. +Ja existeix" + Segment enviat correctament + SponsorBlock no està disponible temporalment (temps d\'espera de l\'API esgotat) + SponsorBlock no està disponible temporalment (estat %s) + SponsorBlock no està disponible temporalment + No s\'ha pogut votar el segment (temps d\'espera de l\'API esgotat) + No s\'ha pogut votar el segment (estat: %1$s %2$s) + No s\'ha pogut votar el segment: %s + Vot positiu + Vot negatiu + Canvia la categoria + No hi ha segments per votar + %1$s a %2$s + Trieu la categoria del segment + La categoria està deshabilitada a la configuració. Habiliteu la categoria per enviar-la + Nou segment de SponsorBlock + Establir %s com l\'inici o el final d\'un nou segment? + Inici + Final + Ara + Hora en què comença el segment + Hora en què acaba el segment + Són correctes les hores? + "El segment és des de + +%1$s +fins a +%2$s + +(%3$s) + +Preparat per enviar?" + Enviar + L\'inici ha de ser abans del final + Primer marca una ubicació a la barra de temps + Fes una vista prèvia del segment i assegura\'t que es salti correctament + Edita el temps del segment manualment + Vols editar el temps d\'inici o de final del segment? + Hora introduïda no vàlida + Enviar fragment highlighted + "És correcte l'hora del fragment highlighted? + +%s + +Si no estàs enviant un fragment highlighted, toca Cancel·lar i marca la segona ubicació" + Enviar fragment highlighted + Estadístiques + Les estadístiques no estan disponibles temporalment (l\'API no funciona) + Carregant… + SponsorBlock està desactivat + El teu nom d\'usuari: <b>%s</b> + Toca aquí per canviar el teu nom d\'usuari + No s\'ha pogut canviar el nom d\'usuari: Estat: %1$s %2$s + Nom d\'usuari canviat correctament + La teva reputació és <b>%.2f</b> + Has creat <b>%s</b> segments + Toca aquí per veure els teus segments + Taula de classificació de SponsorBlock + Has estalviat gent de <b>%s</b> segments + Toca aquí per veure les estadístiques globals i els principals col·laboradors + Això són <b>%s</b> de les seves vides.<br>Toca aquí per veure la taula de classificació + Has saltat <b>%s</b> segments + Són <b>%s</b> + Restaurar el comptador de segments saltats? + %1$s hores %2$s minuts + %1$s minuts %2$s segons + %s segons + Opacitat: + Color: + "SponsorBlock és una base de dades col·laborativa de segments de vídeo com ara patrocinadors, intros, outros i recordatoris de subscripció. Els segments són enviats i votats per la comunitat, i la base de dades és oberta i gratuïta per descarregar + +L'ID del vídeo que Miros veu s'envia a l'API de SponsorBlock per obtenir els seus segments" diff --git a/patches/src/main/resources/addresources/values-ca-rES/youtube/strings.xml b/patches/src/main/resources/addresources/values-ca-rES/youtube/strings.xml index c721d2b6f5..752f495912 100644 --- a/patches/src/main/resources/addresources/values-ca-rES/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ca-rES/youtube/strings.xml @@ -23,232 +23,1634 @@ Second \"item\" text" Canal General Reproductor + Shorts Barra de cerca Controls de lliscada Miscel·lània Vídeo + Restaurar la pantalla de configuració antiga de YouTube + Eliminar restriccions de reproducció en segon pla + Desactivar la reproducció en segon pla + Pausa automàtica quan el dispositiu està bloquejat Apagat + Pausa sempre quan està bloquejat + Pausa a menys que hi hagi un àudio sense fils connectat + Desactivar el límit de velocitat de desplaçament + Desactivar actualitzacions de la disposició + Evita que el servidor actualitzi la disposició de l\'aplicació + "La disposició de l'aplicació torna a la disposició utilitzada quan s'instal·là per primera vegada. + +Algunes disposicions del servidor potser no tornin. + +Els canvis inclouen: +• Els components del menú desplegable del reproductor (o la configuració relacionada) potser no funcionin +• Els números progressius no estan animats +• Es mostra la pestanya de Biblioteca +• La secció de música de la descripció del vídeo potser no funcioni +• Els elements del menú desplegable del reproductor no es poden ocultar +• El botó de canvi de compte potser no aparegui a la pestanya de Biblioteca" + Activar filtre del menú desplegable del feed + Filtre del menú desplegable del feed + Introduïu els noms dels menus desplegables per filtrar, separats per noves línies + Habilita el filtre del menú inferior de Biblioteca / Tu + Filtre del menú inferior de Biblioteca / Tu + Introdueix els noms del menú inferior de Biblioteca / Tu per filtrar, separats per noves línies + Ocultar targetes d\'àlbums + Amaga les targetes d\'àlbums sota la descripció de l\'artista + Ocultar targetes d\'artistes + Amaga la targeta de l\'artista a la part superior dels resultats de la cerca + Ocultar botons del canal + Oculta botons com Subscriure\'s, Veure canal, Comunitat i YouTube Music dels perfils de canal als resultats de cerca + Ocultar publicacions de la comunitat + Ocultar bannes compactes + Amaga els bàners compactes i l\'espai entre alguns components + Targetes desplegables sota els vídeos + Mostra-ho tot + Amaga només la targeta de resum + Amaga-ho tot + Ocultar botó flotant del micròfon + Amaga el botó del micròfon flotant a la cantonada inferior dreta de la pantalla de suggeriments de cerca + Amaga el botó \"Get Premium\" + Ocultar la prestatgia d\'Historial + Ocultar prestatges horitzontals + Amaga els prestatges horitzontals com ara Notícies d\'última hora, Publicacions de la comunitat que et poden agradar, Continua mirant, Explora més canals, Explora més temes, Per a tu, Vídeos recents, Més rellevants, Publicacions de la comunitat del creador, Shopping i Torna\'l a mirar + Ocultar prestatge d\'imatges + Amaga els baladros d\'imatges als resultats de la cerca + Ocultar la targeta \"Convida altres a enviar missatges\" + Oculta la targeta d\'invitació que apareix a la secció de Missatges de la pestanya de Notificacions + Amaga el botó \"Vídeos recents\" + Amaga el bàner blau de \"Vídeos recents\" que apareix a la part superior del feed de l\'Inici + Ocultar retransmissions en directe + Oculta els vídeos que estan en directe ara al feed d\'inici i als resultats de cerca Amaga llistes de reproducció de Mix Amagar secció de pel·lícules + Ocultar l\'encapçalament del menú de Notificacions + Oculta l\'encapçalament del menú de Notificacions quan es prem el botó de la campana Amaga el botó \"Notifica\'m\" + Oculta el botó de recordatori per a les retransmissions en directe i els estrens programats + Amaga els Playables + Amaga la funció que permet jugar a jocs + Ocultar miniatures dels termes de cerca + Amaga les miniatures de previr en la llista d\'autocompletat de la cerca + Amaga el botó \"Mostra\'n més\" + Amaga el botó d\'expandir sota els vídeos dels resultats de la cerca + Ocultar barra de canals subscrits + Ocultar noms de canals subscrits + Oculta els noms dels canals a la barra de canals subscrits + Amaga les enquestes + Amagar les enquestes i els avisos sobre la precisió de les recomanacions del flux + Amaga la prestatgeria de kindness + + Ocultar el bàner \"Els vídeos no disponibles estan ocults\" + Oculta el bàner \"Els vídeos no disponibles estan ocults\" a les llistes de reproducció + Amagar l\'hora de pujada + "Limitacions: +• És possible que els estanteres de Shorts, les pàgines dels canals i els resultats de la recerca encara mostrin l'hora de pujada +• No funciona amb la disposició d'automòbil" + Ocultar la miniatura del vídeo + Ocultar etiquetes de vídeo + Oculta etiquetes com \'Doblat automàticament\' / \'Doblat\', \'Guanya comissions\', \'Tendència\', \'La gent també ha vist\', \'producte(s)\' i \'Potser també us agradi\' + Amagar el recompte de visites + "Limitacions: +• És possible que els estanteres de Shorts, les pàgines dels canals i els resultats de la recerca encara mostrin el nombre de visualitzacions +• No funciona amb la disposició d'automòbil" + Ocultar resultats de cerca web + Amaga la secció \'Resultats de la web\' dels resultats de cerca + Ocultar la secció \"Potser t\'agradi\" + Amaga els Doodles de YouTube + Amaga les animacions estacionals i basades en esdeveniments del logotip de YouTube + Mostrar els Doodles de YouTube pot impedir que es mostrin els encapçalaments personalitzats + Amaga la barra de canals + Ocultar marca d\'aigua del canal + Oculta els capítols i el botó \'En aquest vídeo\' + Ocultar caixa de micromecenatge + Ocultar caixes d\'emergència + Ocultar panells d\'informació + Amaga els panells de context informatius, com la informació mèdica + Amaga el botó \"Unir-se\" + Amaga el botó \"Repetició del xat en directe\" + Amaga el botó de reproducció del xat en el mode de pantalla completa + Amaga els panells mèdics + Ocultar els indicadors de gestos del reproductor + Oculta els indicadors que apareixen durant els gestos del reproductor, com \"Desllisa cap amunt per a una recerca precisa\", \"Allibera per cancel·lar\" i \"2x\" + Oculta la barra de notificacions + Amaga les directrius per als subscriptors + Ocultar el botó \'Sincronitzar amb el vídeo\' + Ocultar el degradat de tocar i mantenir + Oculta el degradat dels controls del reproductor quan llisques el dit mentre mantens premut per accelerar + Amagar reaccions temporitzades + Amaga la funció que permet als usuaris reaccionar amb emojis a la secció de comentaris i al xat en directe + Ocultar el menú \'Ajuda & comentaris\' + Oculta el menú \'Ajuda & comentaris\' quan s\'utilitza el botó de tres punts, com ara als resultats de la cerca + Ocultar identificador + Oculta l\'identificador del teu canal a la pestanya \'Tu\' + Ocultar títol del vídeo + Amaga el títol del vídeo en el mode de pantalla completa + Netejar els subtítols del vídeo + Elimina frases com \'#\', \'Fundraiser\', \'product(s)\' i \'Shop\' dels subtítols del vídeo + Ocultar el botó \"Pregunta a Gemini\" + Oculta el botó \"Pregunta a Gemini\" a la part inferior del panell \"Pregunta sobre aquest vídeo\" + Descripció del vídeo + Tria quins components apareixen a la descripció del vídeo + Amagar la secció \"Resum del vídeo generat per IA\" + Amagar la secció de Preguntes + Amagar la secció d\'Atributs + Amaga les seccions de Llocs destacats, Jocs, Música i Persones esmentades + Amaga la secció d\'enllaços del canal + Amagar la secció de Capítols + Amagar la secció de Correccions + Amagar la secció \'Progress del curs\' + Amagar la secció d\'Exploració + Amaga les seccions \'Explora aquest curs\' i \'Explora el podcast\' + Amagar la secció \'Explora aquest curs\' + Amagar la secció \'Explora el podcast\' + Ocultar la secció de canals destacats + Amagar la secció d\'Enllaços destacats + Amagar la secció de llistes de reproducció destacades + Amagar la secció de Vídeos destacats + Amagar la secció d\'etiquetes + Ocultar la secció \"Com s\'ha fet\" + Amagar punts de Hype + Amagar secció de targetes d\'informació + Ocultar la secció \"Conceptes clau\" + Amagar secció de qüestionaris + Amagar la secció \'Cerca dins d\'aquest vídeo\' + Ocultar el botó de Subscriure\'s + Amagar secció de transcripció + Amagar secció \'Detalls del vídeo\' + Barra de filtres + Trieu on ocultar la barra de filtres desplaçable + Ocultar a la pàgina del canal + Ocultar als comentaris + Ocultar als feeds + Ocultar als vídeos relacionats + Ocultar als resultats de cerca + Ocultar a l\'historial de reproducció + Pàgina del canal + Trieu quins components apareixen a la pàgina del canal + Habilitar el filtre de la pestanya del canal + Filtre de la pestanya del canal + Introduïu els noms de les pestanyes del canal per filtrar, separats per salts de línia + Ocultar el botó de Comunitat + Amaga el botó \"Unir-se\" + Amagar vista prèvia dels enllaços + Amaga els enllaços web clicables al capçalet del canal + Ocultar l\'etiqueta \"Només membres\" + Oculta l\'etiqueta \"Només membres\" a la barra de filtres del canal + Amagar prestatgeria de membres + Amaga la llista de membres del canal + Ocultar la prestatgia de publicacions + Amaga el botó \"Botiga\" + Amaga el botó \"Subscriure\'s\" + Menú d\'exploració + Trieu quins components apareixen al menú d\'exploració + Ocultar el menú de Cursos + Ocultar el botó d\'Explorar + Ocultar el menú de \'Fashion & Beauty\' + Ocultar el menú de Gaming + Ocultar el menú de Hype + Amaga el menú d\'Aprenentatge + Ocultar el menú de Live + Ocultar el menú de Membresies + Ocultar el menú \'Pel·lícules & TV\' + Ocultar el menú de Música + Ocultar el menú de Notícies + Ocultar el menú de Playables + Ocultar el menú de Podcasts + Ocultar el menú de Shopping + Ocultar el menú d\'Esports + Ocultar el menú de YouTube Premium + Ocultar el menú de YouTube Studio + Amaga el menú de YouTube Music + Amaga el menú de YouTube Kids + Amaga el menú de YouTube Create + Amaga el menú \"How YouTube works\" + Ocultar els peus de pàgina legals i de privacitat + Comentaris + Trieu quins components apareixen a la secció de comentaris + Habilitar el filtre del carrusel de comentaris + "• Assegureu-vos d'introduir el nom exacte de l'element del carrossel (p. ex., Pregunta, Dona hype a aquest vídeo, Escolta a YouTube Music) +• Ocultar els elements del carrossel pot tenir efectes secundaris no desitjats +• Si s'oculten tots els elements del carrossel, també s'ocultarà tota la secció de comentaris" + Filtre del carrusel de comentaris + Introduïu els noms dels carrisels de comentaris per filtrar, separats per salts de línia + Amagar resum del xat per IA + Amagar directrius del canal + Amaga els missatges de context + Amagar l\'encapçalat \'Comentaris dels membres\' + Amagar secció de comentaris + Ocultar la secció de comentaris al feed de Inici + Amaga les Normes de la Comunitat + Amagar el botó \'Crea un Short\' + Amaga el botó de No m\'agrada + Amaga el botó d\'Emoji + Ocultar opcions de la barra de filtres + Oculta totes les opcions de la barra de filtres exceptu \'Principals\' i \'Més recents\' + Amaga l\'animació i les targetes de regal + Amaga el botó de Regal + Ocultar el botó d\'Informació + Oculta la barra de donadors del xat en directe + Oculta el botó de desplaçament fins a baix del xat en viu + Ocultar els avisos del xat en viu + Oculta les finestres emergents d\'ajuda que apareixen prop de la barra d\'entrada del xat, com l\'avís de xat només per a subscriptors + Ocultar botó de menú + Amaga el botó del menú de 3 punts de cada comentari + Amagar l\'aperçú del comentari + Oculta la vista prèvia del comentari sota el vídeo abans d\'obrir la secció completa de comentaris + Comentari ocult per Morphe… + Habilitar el botó de comentaris minimalistes + Amaga les cartes lliscants i el seu selector de pàgina, mostrat al botó de comentaris + Amagar el botó de Gràcies + Amaga el botó de la marca temporal + Amaga el botó \"Fans principals\" + Amaga el botó de Tradueix + Ocultar el missatge \"Encara no hi ha comentaris\" + Netejar els enllaços de cerca ressaltats + Neteja els enllaços de cerca ressaltats, deixant només el text pla original + Filtre de paraules clau + Trieu paraules o frases específiques per ocultar vídeos als feeds i a la cerca + Oculta els comentaris per paraules clau + Amagar vídeos de l\'Inici per paraules clau + Amagar resultats de cerca per paraules clau + Amagar vídeos de Subscripcions per paraules clau + Paraules clau per ocultar + "Introduïu paraules clau i frases per ocultar, separades per salts de línia + +Les paraules clau poden ser qualsevol text que aparegui als títols dels vídeos o als noms dels canals + +Les paraules amb lletres majúscules a la meitat han de coincidir exactament (per exemple, iPhone, TikTok, LeBlanc)" + "Inici / Subscripcions / Cerca / Els comentaris es filtren per ocultar el contingut que coincideix amb frases clau + +Limitacions: +• Els Shorts no es poden ocultar pel nom del canal +• Alguns components de la interfície d'usuari podrien no ocultar-se +• La cerca d'una paraula clau podria no mostrar resultats" + Coincidència amb paraules completes + Envolir una paraula clau/frase amb cometes evitarà coincidències partials dels títols dels vídeos i dels noms dels canals<br><br>Per exemple,<br><b>\"ai\"</b> ocultarà el vídeo: <b>How does AI work?</b><br>però no ocultarà: <b>What does fair use mean?</b> + No es pot utilitzar la paraula clau: %s + Afegeix cometes per utilitzar la paraula clau: %s + La paraula clau té declaracions contradictoryes: %s + La paraula clau és massa curta i requereix cometes: %s + La paraula clau ocultarà tots els vídeos: %s + "• Inici: %s +• Subscripcions: %s +• Cerca: %s +• Comentaris: %s +• Total: %s" + Estadístiques + Vídeos ocults, últimes 24 hores + Vídeos ocults, tot el temps + Reiniciar el comptador de les últimes 24 hores? + Reiniciar el comptador de tot el temps? + Filtre de canals d\'IA + Oculta els vídeos de canals marcats com a contingut generat per IA per la llista de la comunitat AiSList + Llista negra: Confiança alta + Llista d\'advertència: Confiança moderada + Ocultar vídeos a l\'Inici + Ocultar vídeos a la cerca + "• Inici: %s +• Cerca: %s +• Total: %s" + Canals de la llista negra + Canals de la llista d\'advertència + Habilita un filtre per carregar els canals + Carregant canals… + "L'inici i els resultats de la cerca es filtren per ocultar els vídeos de canals marcats per AiSList + +Limitacions: +• Els Shorts de IA filtrats poden aparèixer com un espai buit +• Alguns vídeos de IA poden seguir apareixent" + "AiSList és una llista de codi obert de canals de YouTube que publiquen contingut generat per IA, mantenida per voluntaris. Els canals són reportats per els usuaris i revisats abans d'ser afegits + +La llista es descarrega des de GitHub i els vídeos es filtren al vostre dispositiu" + Denunciar canals d\'IA + Afegir entrada de denúncia al menú de vídeo + Afegir una entrada al menú de vídeo que denuncia el canal d\'un vídeo a AiSList + Nom de qui denuncia + Nom mostrat al costat de les teves denúncies a la taula de classificació pública d\'AiSList. La denúncia no està disponible mentre aquest camp estigui buit + Denunciar canal d\'IA + "Enviar %s a AiSList, utilitzant aquest vídeo com a prova? + +Només denuncia els canals que: +• Utilitzen veus, visuals, guions o música d'IA com a contingut principal +• Produئين contingut en massa amb eines d'IA i poca intervenció humana + +No denunciis canals que només discutin la IA o n'utilitzin per a algunes parts dels seus vídeos. + +Els informes són públics i es revisen abans d'afegir un canal a una llista. L'eliminació només és possible mitjançant una sol·licitud a GitHub" + Aquest canal ja està a la llista negra d\'AiSList + Aquest canal ja està a la llista d\'advertències de l\'AiSList + Informar + Estableix un nom de l\'emissor a la configuració del filtre de canals d\'IA per informar canals + Buscant el canal… + No es pot trobar el canal d\'aquest vídeo + Canal denunciat i a l\'espera de revisió + AiSList no ha pogut desar la denúncia, torneu-ho a intentar més tard + S\'han enviat massa denúncies, torneu-ho a intentar més tard + AiSList ha rebutjat la denúncia + Error en la denúncia, comproveu la vostra connexió + Llista blanca d\'anuncis del canal + Els vídeos dels canals de la llista blanca es reproduen amb anuncis, així els seus creadors reben un pagament. Els anuncis es mantenen bloquejats en tots els altres canals + Afegeix el canal a la llista blanca d\'anuncis + Elimina el canal de la llista blanca d\'anuncis + Ocultar l\'estanteria de la botiga del creador + Ocultar el bàner de la botiga de la pantalla final + Ocultar anuncis generals + Ocultar bàners de merchandise + Amaga les etiquetes de promocions pagades + Ocultar els anuncis pop-up del reproductor + Ocultar targetes auto-patrocinades + Ocultar enllaços de compres + Ocultar anuncis de vídeo + Ocultar les promocions de YouTube Premium + Carregant… + Canal actual: + Afegeix a la llista blanca Eliminar + Canals de la llista blanca + No hi ha canals a la llista blanca + Canal afegit a la llista blanca + Canal eliminat de la llista blanca + Cerca dins del canal + Afegeix un segon botó de cerca a les pàgines dels canals per cercar només en aquest canal + Cerca canal + Resultats de la cerca de canals + No s\'ha trobat res en aquest canal + No s\'ha pogut cercar en aquest canal + Vídeos + Publicacions + No hi ha publicacions en aquest canal que coincideixin + Relevància + Més vistos + Menys vists + Més recents + Més antic + Més curt + Més llarg + Enllaç copiat al porta papers + Enllaç amb marca de temps copiat al porta papers + Mostrar el botó de Copiar l\'enllaç del vídeo + Toca per copiar l\'enllaç del vídeo, mantingui toca per copiar amb la marca de temps + Copiar la marca de temps per defecte + Inverteix les accions de tocar i mantenir tocat del botó \'Copiar l\'enllaç del vídeo\' + Copia el títol del vídeo + Mantingueu premt el títol del vídeo per copiar-lo + Títol del vídeo copiat al porta papers + Copia els comentaris + Mantingueu premt un comentari per copiar-lo + Comentari copiat al porta papers + Manté premut per obrir el menú del vídeo + Si mantens premut qualsevol part d\'un vídeo del feed, s\'obrirà el seu menú + Mostra el botó Veure més tard a les miniatures + A les miniatures dels vídeos del feed es mostra un botó Veure més tard. Si el mantens premut, s\'obrirà Desarar a la llista de reproducció + Descarregar imatges de publicacions + Es mostra un botó de descàrrega al costat del botó Compartir de les publicacions amb imatges o GIFs, i s\'afegeix un element de descàrrega al seu menú + Descarregar imatges + Descarregar imatge + Descarregant %s imatges… + Descarregant imatge… + %1$s imatges guardades a %2$s + Imatge guardada a %s + No s\'ha pogut descarregar la imatge + No s\'ha trobat cap imatge en aquesta publicació + Afegeix l\'element Pregunta al menú desplegable + Afegeix un element de resum de vídeo al menú desplegable del feed + "Per utilitzar Ask, inicialment cal tenir un compte iniciat en un país on Ask estigui disponible. + +Un cop s'ha obert un vídeo amb el botó Ask, la funció funciona amb tots els vídeos, encara que el país actual no tingui la funció Ask." + Resumir el vídeo + No s\'ha pogut obrir Ask per a aquest vídeo + Ask no està disponible per al teu compte o per al teu país actual + Mostra el nombre de visualitzacions a les miniatures + El nombre de visualitzacions dels vídeos del feed es mostra a la part inferior esquerra de les seves miniatures en lloc de sota el títol. Alguns vídeos, com els de les prestatgeries, potser no mostren el nombre de visualitzacions + Mostrar el recompte de m\'agrada de la miniatura del feed + Mostrar el percentatge de m\'agrada de la miniatura del feed + Cercar comentaris + Toqueu el botó de cerca als comentaris per mostrar només els comentaris carregats que contenguin totes les paraules cercades. Poseu les paraules entre cometes per cercar una frase + Cerca comentaris + Paraules per a la cerca Cercar Buida + Canvieu l\'ordre de classificació o obriu els comentaris de nou per cercar-los + Cerca esborrada. Canvieu l\'ordre de classificació o obriu els comentaris de nou per mostrar tots els comentaris + Desactiva la finestra emergenta \"Inicia sessió a la TV\" + Desactivar la finestra desplegable \"Connecta els teus dispositius\" + Desactiva el salt de capítol amb toc doble + Desactiva el gest de doble toc amb dos dits que salta capítols, que es pot activar accidentalment amb un sol dit + Descarrega vídeos, podcasts o cançons amb aplicacions de descàrrega de tercers + Mostra el botó de descàrrega de la capa del reproductor + Mostra un botó al reproductor de vídeo que obre el teu descarregador extern + Descarga ràpida amb un toc + Sol·licita la descàrrega en segon pla. Mantingueu premut per obrir el descarregador. Funciona amb YTDLnis i YTindir + Marcar descàrregues com a vist + Els vídeos descarregats amb un descarregador extern s\'afegeixen a l\'historial de reproducció de YouTube + Full HD (1080p) + Mitjana (192 kbps) + Descarga ràpida Vídeo + Àudio + Totes les opcions + Mostra la descàrrega al menú de Shorts + Afegeix un element de menú desplegable de descàrrega externa al reproductor de Shorts + Descárrega + Traducció de veu en off + Llegeix els subtítols del vídeo en veu alta mitjançant el text a veu del dispositiu, sincronitzat amb la reproducció + Mostra un botó superposat al reproductor de vídeo. Toca per alternar la traducció, mantingui premut per a la configuració ràpida + Idioma + Proveïdor de traducció + Volum de l\'àudio original + Volum de l\'àudio original de YouTube mentre la traducció està parlant + Volum de la traducció + Volum de la veu de la traducció doblada + Velocitat màxima de parla + Multiplicador de velocitat màxima per ajustar la parla a l\'espai d\'un subtítol + "API de traducció gratuïta, no requerix clau d'API. Fins a 5.000 caràcters per dia en el nivell gratuït, o fins a 50.000 caràcters per dia amb una adreça de correu electrònic. + +Toqueu aquí per obrir MyMemory.translated.net" + Correu electrònic de MyMemory + Opcional. Introduïu la vostra adreça de correu electrònic de MyMemory per augmentar el límit diari de 5.000 a 50.000 caràcters. Deixeu-ho buit per utilitzar el límit anònim + "Traducció per IA de pagament via OpenRouter. Recarregueu un cop i traduïu milers de vídeos. Mistral Nemo és l'opció més econòmica, Gemma 4 ofereix la qualitat més alta. + +Toqueu aquí per obrir OpenRouter.ai" + Clau API d\'OpenRouter + La vostra clau API d\'OpenRouter. Creeu una clau a OpenRouter.ai > Workspace > API Keys + Model d\'OpenRouter + Personalitzat + OpenRouter no configurat + Introduïu la vostra clau API d\'OpenRouter a la configuració abans de seleccionar aquest proveïdor + Gratuït + %s per cada 100 hores de vídeo + Veu + Sistema + Masculí multilingüe + Femenina multilingüe + Masculí + Femenina + És l\'hora de morphing! + Traducció no disponible + "El servei de traducció ha retornat l'error HTTP %s. És probable que la vostra adreça IP hagi estat limitada durant fins a 24 hores. + +Per continuar utilitzant les traduccions, canvieu a un proveïdor de traducció diferent a la configuració o utilitzeu una VPN" + No torni a mostrar + "La vostra configuració conté una clau API d'OpenRouter. + +La vostra clau API mai s'ha de compartir. Qualsevol persona amb accés a ella pot utilitzar els vostres crèdits d'OpenRouter" + S\'ha assolit el límit de crèdit d\'OpenRouter + S\'ha assolit el límit de crèdit d\'OpenRouter. Recarregueu el vostre saldo o augmenteu el límit de despesa de la clau a OpenRouter.ai + Obre OpenRouter.ai + La traducció d\'OpenRouter ha fallat (HTTP %s) + S\'ha assolit el límit diari de MyMemory + Següent disponible a %s + Heu utilitzat la quota de traducció gratuïta de MyMemory d\'avui (5.000 caràcters). Afegiu un correu electrònic de MyMemory a la configuració per augmentar el límit diari a 50.000 caràcters, o canvieu a un servei de traducció diferent + Heu utilitzat la quota de traducció de MyMemory d\'avui (50.000 caràcters). Canvieu a un servei de traducció diferent o torneu-ho a intentar demà + La traducció de MyMemory ha fallat (estat %s) + Habilitar la repetició del vídeo + No recordis el vídeo en bucle + Evita que l\'estat actual de la repetició del vídeo es desi per a els vídeos posteriors + Mostra el botó de Repetir vídeo + La repetició del vídeo està activada + El bucle de vídeo està desactivat + Interval de bucle + Inici + Fi + [hh:]mm:ss + [hh:]mm:ss (buit = fi del vídeo) + Establir + Buida + Repetint %1$s - %2$s + Interval de temps no vàlid + Mostrar el botó de Picture-in-Picture + Mostrar el menú de Picture-in-Picture + Passar a Picture-in-Picture + Mostra el botó de Reproduir tot + "Toca per generar una llista de reproducció de tots els vídeos del canal, manté premut per desfer + +Info: Potser no funcioni en les retransmissions en directe" + Mode de generació de llista de reproducció + Tots els continguts (ordenats per temps, ascendent) + Tots els continguts (ordenats per temps) + Tots els continguts (ordenats per popularitat) + Només vídeos (ordenats per temps) + Només vídeos (ordenats per popularitat) + Només Shorts (ordenats per temps) + Només Shorts (Ordinar per popularitat) + Només vídeos transmitits (Ordinar per temps) + Només vídeos transmitits (Ordinar per popularitat) + Només continguts per a membres + Vídeos només per a membres + Shorts només per a membres + Transmissions en directe només per a membres + No s\'ha pogut generar la llista de reproducció per a una incoherència de l\'ID del canal + Mostra el botó de Recarregar vídeo + L\'ID del vídeo no està disponible. Si us plau, recarrega el vídeo manualment + Mostra el botó de Desar per veure més tard + Toca per desar a Veure més tard, mantingues premut per a les opcions de la cua + Vídeo desat correctament a la llista de reproducció \"Veure més tard\" + Aquest vídeo ja està desat a la llista de reproducció \"Veure més tard\" + No es pot desar aquest vídeo a la llista de reproducció \"Veure més tard\" + Desar a Veure més tard + Mostra el botó Desar per veure més tard al menú desplegable de configuració del reproductor + Permet desar el vídeo que s\'està veient actualment a \'Veure més tard\' des del menú desplegable de configuració del vídeo + Mostrar el botó de Desau a Veure més tard en els vídeos per a nens + Permet desar els vídeos infantils a \'Veure més tard\' des del menú desplegable + Mostra el botó de desar a Veure més tard al desplegable de shorts + Permet desar els shorts a \'Veure més tard\' des del menú desplegable + Redirigeix \"Veure més tard\" a una llista de reproducció + Veure més tard + Afegir a la cua de Morphe + Manté la cua entre sessions + La cua es manté després de reiniciar l\'aplicació + Intercanvia les accions de Desa i Cua + Toca per a les opcions de la cua, mantingues premut per desar a Veure més tard + Tipus de Reproduir a continuació a la cua + Per defecte + Cua nativa + Cua de Morphe + Cua de Morphe del costat del client + Afegeix l\'element del menú desplegable de la cua del feed + Afegeix un element desplegable addicional. Activeu-ho si no apareix \"Afegeix a la cua\" + Cua + Cua (%s) + N\'elimina tots + No hi ha res a la cua + Utilitza \"Reprodueix el següent a la cua\" en qualsevol vídeo + Reproduint ara + Reprodueix el següent a la cua + Reprodueix l\'últim de la cua + Reordenar Eliminar + Afegit a la cua + No s\'ha pogut afegir aquest vídeo a la cua + No es poden afegir llistes de reproducció a la cua + La cua està plena + No s\'ha pogut iniciar el vídeo + Llista blanca d\'anuncis al menú desplegable + Afegeix un element al desplegable que permet que els anuncis es reproduïn al canal d\'aquest vídeo + Aquesta funció pot requerir l\'ús de PotHelper o canviar \"Spoof video streams\" a l\'inici de sessió de TV o Android VR + Llista blanca de velocitat al menú desplegable + Afegeix un element al menú desplegable que sempre reprodueix el canal d\'aquest vídeo a velocitat 1.0x + No has iniciat sessió + No hi ha ID de llista de reproducció + La cua està buida + No hi ha ID de vídeo + Error: %s + No s\'ha pogut afegir a la cua + No s\'ha pogut crear la cua + No s\'ha pogut eliminar de la cua + No s\'ha pogut desar la cua + Afegit a la cua + Cua creada + Eliminat de la cua + Cua desada a %s + Cua de Morphe + Afegir a la cua + Afegeix a la cua i obre + Afegeix a la cua i reproduix + Afegeix i recarrega el vídeo + Elimina de la cua + Elimina i obre la cua + Elimina i recarrega el vídeo + Obre la cua + Desa la cua a la llista de reproducció + Llista de reproducció des guarded + Desguardat a %s + Neteja la cua + Cua netejada + Cua + Reproduir l\'últim de la cua + Reproduir el següent de la cua + YouTube no ha pogut mostrar la cua. + La cua no està llesta per a aquest vídeo. Obriu el reproductor de vídeo i torneu-ho a intentar. + Afegir el Mix a la cua o veure el Mix? + Veure + Afegir a la cua + Carregant Mix… + Obre el canal des de l\'avatar en directe + Evita que la retransmissió en directe s\'obri en tocar l\'avatar del canal + Desactiva el gest de recerca precisa + Desactiva el mode de cerca precisa que s\'activa en arrossegar la barra de cerca cap amunt + Habilitar el DVR de retransmissions en directe + Permet fer una recerca en totes les retransmissions en directe, fins i tot en aquelles que normalment ho resten + Habilitar les miniatures de la barra de cerca + Mostra una miniatura del vídeo mentre s\'arrossega la barra de cerca + Més reproduït + "\"Habilitar el DVR de retransmissions en directe\" pot no estar disponible + +Per solucionar-ho, activa \"Simular retransmissions de vídeo\" i canvia el \"Client per defecte\" a \"TV Simply\" o \"visionOS\"" + Amplia la durada del DVR de la retransmissió en directe + Permet retrocedir les retransmissions en directe fins a 7 dies en el passat + "'Expandir la durada del DVR de la transmissió en directe' pot no estar disponible + +Per solucionar-ho, activa 'Spoof video streams' i canvia el 'Client per defecte' a 'TV Simply' o 'visionOS'" + Activar lliscar per buscar + Habilita lliscar per cercar en lloc de reproduir a velocitat 2x quan mantens premut el reproductor de vídeo + Activa tocar per recercar + Simplement toca la barra de cerca per saltar a aquesta posició sense arrossegar-la + Gest d\'arrosgament a l\'esquerra + Gest d\'arrosgament a la dreta + Gest d\'arrosgament a dalt + Amplada de la zona esquerra/dreta + Percentatge de cada vorare de la pantalla reservat per als gestos d\'arrosgament a l\'esquerra i a la dreta (5-50) + Luminositat + Volum + Natiu Apagat + Ignorar els arrosgaments quan està bloquejat + Els gestos d\'arrosgament s\'ignoren mentre el reproductor està bloquejat amb \"Bloqueja la pantalla\" + Activa el gest de mantenir premut per lliscar + Requerix mantenir premut abans que es reconegui el gest de descriptors + Activa la retroalimentació hàptica + Desa i restaura la Brightness + Recorda el nivell de brillantor en entrar / sortir del mode de pantalla completa + Activa el gest de brillantor automàtica + Desllisar la brillantor fins al seu valor més baix activa la brillantor automàtica del sistema + Automàtic + Temps d\'espera de la capa de lliscat + La quantitat de mil·lisegons que la capa és visible + Opacitat del fons de la capa de desllisement + Ajusta el nivell de transparència (0 és transparent, 100 és opac) + Color de brillantor de la capa de desllisement + Estableix el color de la barra de progressió dels controls de brillantor + Color de volum de la capa de desllisement + Estableix el color de la barra de progressió dels controls de volum + Color de la velocitat de la capa de desllisement + Estableix el color de la barra de progrés dels controls de velocitat de reproducció + Mida del text de la capa de desllisement + Ajusta la mida del text a la pantalla (1-30) + Llindar de magnitud del desllisement + Estableix la distància de desllisement requerida abans que s\'activi un ajust + Sensibilitat del desllisement de volum + Els valors més alts requereixen més desllisement per cada pas de volum + Passos de volum + Augment de volum + Desllisar cap amunt passat el volum màxim l\'augmenta, cosa que pot distorsionar l\'àudio fort + Per defecte del dispositiu + Sensibilitat del desllisement de luminositat + Els valors més alts requereixen més desllisement per cada pas de luminositat + Sensibilitat del desllisement de velocitat + Els valors més alts requereixen més desllisement per cada pas de velocitat + Pass de velocitat + Alçada de la zona de velocitat + Alçada de la zona del gest de velocitat com a percentatge de l\'alçada del reproductor (5-75) + Velocitat + Estil de la capa de desllisement + Capa horitzontal + Capa horitzontal (minimalista - superior) + Capa horitzontal (minimalista - centre) + Superposició circular + Superposició circular (minimalista) + Superposició vertical + Superposició vertical (minimalista) + Habilita el desllisement per canviar de vídeo + Desllisa en mode de pantalla completa per saltar al vídeo següent o al anterior + Botons d\'acció + Tria quins botons d\'acció apareixen en els modes retrat i de pantalla completa + Botons de mode retrat + Desactiva la resplendor de \"M\'agrada\" i \"Subscriure\'s\" + Evita que els botons de \"Like\" i \"Subscriure\'s\" brillin quan un creador els menciona + Ocultar la barra d\'acció + Amaga el botó de Like i No m\'agrada + Amaga el botó de Compartir + Amaga el botó \"Atura els anuncis\" + Amaga el botó de Comentaris + Oculta el botó Connectiu + Amaga el botó de Denunciar + Amaga el botó de Remix + Amaga el botó \"Descarregar\" + Amaga el botó \"Hype\" + Amaga el botó \"Promociona\" + Amaga el botó \"Gràcies\" + Amaga el botó \"Pregunta\" + Amaga el canal i el botó de Subscriure\'s + Amaga l\'avatar del canal i el botó de Subscriure\'s al començament de la barra d\'acció + Ocultar el botó de Subscriure + Oculta els botons de Subscriure i Unir-se al costat de l\'avatar del canal al començament de la barra d\'acció + Amaga el botó \"Clip\" + Potser no funcioni per a alguns comptes d\'usuari + Amaga el botó de la Botiga + Amaga el botó de Desar + Amaga el botó Més + Botons de pantalla completa + Amaga el botó de No m\'agrada + Amaga el botó de M\'agrada + Amaga el botó de \"Xat en directe\" + Amaga el botó de Mix + Amaga el botó \"Més vídeos\" + Amaga el botó de Llista de reproducció + Margen superior d\'accions ràpides + Establir el margen superior de la barra d\'accions ràpides (0-32 dp) + Subtítols + Gestiona els subtítols automàtics i les cookies de subtítols + Visibilitat dels subtítols automàtics + Mostra sempre + No mostrar mai + Mostra quan no estigui silenciat + Mostrar quan estigui silenciat + Idioma de subtítols preferit + Selecciona primer els subtítols del proveïdor i després els tradueix automàticament a l\'idioma preferit Apagat + Afegeix idiomes de traducció automàtica addicionals + Afegeix més idiomes al menú de traducció automàtica + Establir cookies de subtítols + "Soluciona un error de YouTube on els subtítols de traducció automàtica poden no estar disponibles per la falta de cookies + +Cal proporcionar les 'cookies de subtítols'" + Cookies dels subtítols + Cookies que s\'han d\'establir en les sol·licituds de l\'API de Text Temporitzat (API de dades de subtítols) + Obtenir cookies dels subtítols + Fes clic aquí per obtenir les cookies + S\'ha produït un error en obtenir les cookies + Les cookies ja existeixen + Les cookies s\'han actualitzat correctament + Corrigir transcripció + Soluciona un error de comprovació de precondició que pot impedir que es carregui la transcripció + Mostrar els subtítols en el mode picture-in-picture + Botons de navegació + Tria quins botons apareixen a la barra de navigació inferior + Amaga el botó d\'Inici + Amaga el botó de Shorts + Amaga el botó de Crear + Amaga el botó de Subscripcions + Amaga el botó de Notificacions + Mostra el botó de Cerca + Posició del botó de Cerca + Mostra el botó de Configuració + Posició del botó de Configuració + Acció del botó de Configuració + Obre els ajusts de Morphe quan està activat, o els ajusts de YouTube quan està desactivat + Intercanvia el botó de Crear amb el de Notificacions + Si canviar aquest ajust no té efecte, intenta passar al mode Incògnit + Ocultar les etiquetes dels botons de navegació + Amaga el punt de contingut nou + Ocultar la barra de navegació + "Això oculta completament la barra de navegació, inclosa la pestanya 'Tu'. + +Per accedir a la Configuració, activa el botó de Configuració a través de General → Barra d'eines → Mostrar configuració" + Habilitar botons de navegació estrets + Redueix l\'espai horitzontal entre els botons + Desactiva els botons de navegació sense etiqueta + Mostra els botons de navegació amb etiqueta, si YouTube els envia sense etiqueta + Desactivar les pestanyes de navegació de la barra d\'eines + Mostra les Subscripcions a la barra de navegació si YouTube les mou a una pestanya a la part superior de l\'Inici + Habilitar les animacions de la barra de navegació + anima les transicions en canviar d\'estil + Desactiva l\'ocultació automàtica de la barra de navegació + Manté la barra de navegació visible mentre es desplaça + Estil de la barra de navegació + Per defecte + Translúcid + Transparent + Opac + És possible que el vostre dispositiu o la versió de YouTube no admetin aquest estil, i això pot causar problemes de disposició o errors + Barra d\'eines + Trieu quins components apareixen a la barra d\'eines + Ocultar el botó de Cast + Oculta el botó de Xat + Ocultar el botó del Micròfon + Ocultar el botó de cerca + Quan està activat, un toc obre els ajustos de Morphe i una pressió llarga obre els ajustos de YouTube; desactiva per intercanviar + Menú desplegable + Trieu quins components apareixen al menú desplegable + Ocultar el menú \"Configuració addicional\" / Més + Ocultar el menú \"Mode ambient\" + "El menú 'Audio Track' està ocult + +Per mostrar el menú 'Audio Track', canvia 'Spoof video streams' a qualsevol client excepta 'Android Studio' i 'Android VR Downgraded'" + Ocultar el menú \"Pista d\'àudio\" + Ocultar el peu de pàgina del menú \"Pista d\'àudio\" + Ocultar el menú de Subtítols + Ocultar el peu de pàgina del menú de Subtítols + Ocultar el capçalet del menú de Subtítols + Amaga el menú \"Ajuda i comentaris\" + Ocultar el menú \"Escolta amb YouTube Music\" + Ocultar el menú \"Bloqueja la pantalla\" + Ocultar el menú \"Bucle de vídeo\" + Amaga el menú \"Sobre la marxa\" + Ocultar el menú de Qualitat + Ocultar el peu del menú de Qualitat + Ocultar el capdelit del menú de Qualitat + Amaga el menú \"Temporitzador de suspensió\" + Ocultar el menú \"Velocitat de reproducció\" + Ocultar el menú \"Volum estabil\" + Ocultar el menú \"Mira en VR\" + Ocultar vista prèvia de reproducció automàtica + Amaga la vista prèvia del vídeo següent en pantalla completa abans que comenci la reproducció automàtica + Botons de superposició + Tria quins botons apareixen sobre el reproductor de vídeo + Amaga el botó de Reproducció automàtica + Amaga el botó de Subtítols + Amaga el botó de Cast + Amaga el botó de Contraure + Amaga el botó de Pantalla completa + Ocultar el fons dels controls del reproductor + Amaga l\'ombrejat fosc que envolta els controls del reproductor de vídeo + Opacitat del fons dels botons de control + Ajusta l\'opacitat del fons darrere dels botons de control del reproductor i de la barra de temps (0 és invisible, 100 és sòlid) + Amaga els botons Anterior & Següent + Amaga el botó de Configuració + Ocultar les targetes de la pantalla final + Mode ambient + Gestiona la visibilitat i les restriccions d\'alimentació del mode Ambient + Omiteix les restriccions del mode ambient + Permet que el mode Ambient es mantingui actiu en el mode d\'estalvi de bateria + Desactivar el mode ambient + Desactivar el mode Ambient a pantalla completa + Ocultar les targetes d\'informació + Desactiva les animacions de números giratoris + Atura l\'animació de comptador en el nombre de m\'agrada i de visualitzacions + Amaga la barra de cerca del reproductor de vídeo + Amaga la barra de cerca amb miniatures de vídeo + Oculta la barra de progressió de visualització mostrada a les miniatures dels vídeos que ja has vist + Habilita la barra de cerca gran a pantalla completa + Ocultar Shorts + Amaga a la pàgina del canal + Amagar al feed de Inici & vídeos relacionats + Amagar als resultats de cerca + Amagar al feed de Subscripcions + Amagar a la descripció del vídeo + Amagar a l\'historial de reproducció + Reproductor de Shorts + Tria quins components apareixen al reproductor de Shorts + Desactivar el toc doble per a contenir \'M\'en gusta\' + Ocultar botó d\'IA + Amaga l\'etiqueta \"Doblat automàticament\" + Ocultar la barra del canal + Ocultar el botó de Comentaris + Amaga el botó d\'efectes + Ocultar l\'etiqueta de l\'enllaç del vídeo + Ocultar indicadors de gestos + Amaga el botó \"Pantalla verda\" + Amaga el botó de Hashtag + Ocultar el panell d\'informació + Amaga el botó \"Unir-se\" + Amaga el botó \"Like\" + Ocultar l\'animació de font del botó de M\'agrada + Amaga la vista prèvia en directe + Ocultar l\'etiqueta de ubicació + Ocultar la barra de navegació + Amaga el botó \"Noves publicacions\" + Ocultar els botons de la capa de pausa + Ocultar la vista prèvia del comentari + Ocultar el botó Remix + Ocultar el botó Desar + Amaga el botó \"Desa la música\" + Ocultar les suggerències de cerca + Ocultar el botó Compartir + Ocultar el botó de Botiga + Ocultar el botó de So + Ocultar l\'etiqueta de metadades del so + Ocultar el botó de Subscriure\'s + Amaga el botó \"Compra Super Thanks\" + Ocultar els productes etiquetats + Ocultar el botó de Pròximament + Amaga el botó \"Utilitza aquest so\" + Amaga el botó \"Utilitza aquesta plantilla\" + Ocultar el títol del vídeo + Ocultar el vídeo suggerit de la pantalla final + "Oculta la capa de vídeo suggerit que apareix quan el vídeo acaba + +Aquest ajust només funciona si la reproducció automàtica està desactivada. Pots canviar-ho a YouTube: +Configuració → Reproducció (o Reproducció automàtica) → Reproducció automàtica del vídeo següent" + Ocultar vídeos relacionats + Ocultar la capa superposada de \'Més vídeos\' + Amaga la superposició \"Més vídeos\" que apareix quan es llisca cap amunt en el mode de pantalla completa + Ocultar la marca de temps del vídeo + Deshabilitar els panells pop-up del reproductor + Desactiva l\'obertura automàtica dels panells de llista de reproducció i xat en viu + Desactiva la reproducció automàtica de la llista de reproducció + Evita que les llistes de reproducció avancin automàticament al vídeo següent + Desactiva l\'avís \"Vols continuar mirant\" + Sortir del mode pantalla completa quan acabi el vídeo + Deshabilitat + Només vertical + Només horitzontal + Retrat i paisatge + Forçar mode horitzontal a pantalla completa + Gira el reproductor a mode horitzontal en entrar a pantalla completa. No disponible en pantalles petites, que ja s\'obren a pantalla completa en mode horitzontal + Gestos de pantalla completa + Gestos del reproductor en mode pantalla completa + Desactiva el gest de lliscar cap amunt en vertical + Desactiva el gest de lliscar cap avall en vertical + Desactiva el gest de lliscar cap avall en horitzontal + Desactiva el gest de pellizcar per zoomar + Restaurar el gest de pessat per zooming + Obre els vídeos en mode pantalla completa + Deshabilitat + Vertical + Horizontal + Escala del vídeo a pantalla completa + Per defecte + Estirar (omplir pantalla) + Zoom (recortar per omplir) + Mostrar botó d\'escala de vídeo + Toca per alternar entre Per defecte, Estirar i Zoom + Mostrar només en pantalla completa + Oculta el botó d\'escala del vídeo al reproductor en mode vertical + Mostrar botó de silenciar + Opacitat de la capa superposada del reproductor + Ajusta el nivell de transparència (0 és transparent, 100 és opac) + Barra de cerca Deshabilitat + Ampla + Extra ampli + Disseny del dispositiu + Per defecte + Telèfon + Tauleta + Automoció + "Els canvis inclouen: + +Disseny de tauleta +• Les publicacions de la comunitat estan ocultes +• L'ajust de reproducció als feeds no està disponible +• El botó de Remix i el botó de So no estan disponibles als Shorts +• L'estrat 'Més vídeos' pot no estar disponible +• Es mostra la barra d'acions de vídeo antiga no col·lapsada +• És possible que els elements del menú desplegable no funcionin correctament + +Disseny d'automoció +• El feed està organitzat per temes i canals +• L'ajust de reproducció als feeds no està disponible +• El botó de Remix i el botó de So no estan disponibles als Shorts +• L'estrat 'Més vídeos' pot no estar disponible +• Els Shorts s'obrin al reproductor habitual +• Es mostra la barra d'acions de vídeo antiga no col·lapsada" + Habilitar la disposició de tauleta al reproductor + "Restaura la disposició antiga del reproductor + +Limitació: Si 'Restaura la barra d'accions de vídeo antiga' està desactivada, és possible que la capa de 'Més vídeos' no estigui disponible" + 21.11.48 - Restaurar el comportament antic dels subtítols + 20.39.41 - Restaurar l\'opció antiga de desar a la llista de reproducció + 20.28.41 - Desactivar icones en negreta + 20.13.41 - Restaurar el nom del canal sota el reproductor de vídeo + Obrir el full de compartició del sistema + Obre el menú de compartir del sistema en lloc del de l\'aplicació en compartir contingut + Anular els botons de YouTube Music + Obre la vostra aplicació de música preferida en lloc de l\'aplicació YouTube Music predeterminada + Nom del paquet de l\'app de música + Nom del paquet de la vostra aplicació de música instal·lada + Pàgina d\'inici + Per defecte + Totes les subscripcions + Navega per els canals + Cursos / Aprenentatge + Explora + Moda & Bellesa + Jocs + Historial + Llibreria + Vídeos que t\'agraden + En directe + Pel·lícules + Música + Notícies + Notificacions + Llistes de reproducció + Cercar + compreses + Shorts + Esports + Subscripcions + Tendències + Realitat virtual + Veure més tard + Els teus clips + Desactiva la represa en l\'inici + Obre els Shorts amb + Reproductor de Shorts + Reproductor ordinari + Reproductor ordinari a pantalla completa + Reproducció automàtica de Shorts + Avança al següent Short automàticament en lloc de repetir l\'actual + Reproducció automàtica en segon pla + Avança al següent Short automàticament durant la reproducció en segon pla + Salta els Shorts en viu + Omet els streams en directe que apareixen al feed de Shorts + Reproducció als feeds + Sempre activat + Només Wi-Fi Apagat + Restaurar els títols originals + Mostra els títols originals dels vídeos, les descripcions dels vídeos i les descripcions dels canals en lloc dels traduïts automàticament + Carregant títol… + Desactiva l\'actualització automàtica del feed + Evita que els feeds s\'actualitzin automàticament quan queden obsolets + Disseny de la targeta del feed + Clàssic + Targetes + Targetes amb botons de \"M\'agrada\", comentari i desar + Comentaris + Desactivar la represa del miniplayer + Desactiva el miniplayer de \"Continuar veient\", una funció de YouTube Premium que apareix en iniciar l\'aplicació + Miniplayer + Personalitza l\'aparença i el comportament del reproductor de vídeo minimitzat + Tipus de miniplayer + Ocultar títol i canal + Deshabilitat + Per defecte + Minimalista + Minimal 2 + Tableta + Modern 1 + Modern 2 + Modern 3 + Modern 4 + Deshabilitar cantonades arrodonides + Deshabilitar arrossegar i soltar + Evita arrossegar el reproductor minimitzat a diferents cantonades de la pantalla + Deshabilitar gest de desplaçament horitzontal + Evita tancar el reproductor minimitzat lliscant-lo horitzontalment fora de la pantalla + Atura la reproducció amb un arrossegament horitzontal + La reproducció s\'aturarà quan es desplaci el miniplayer fora de la pantalla + Recordar la posició de l\'estat d\'arrossegament + Si arrossegeu el reproductor minimitzat fora de la pantalla, es preservarà l\'estat de la seva interfície d\'usuari + Ocultar botons de superposició + Mida per defecte + Estableix la mida inicial a la pantalla, en píxels + La mida del píxel ha de estar entre %1$s i %2$s + Opacitat de la capa superposada + Ajusteu el nivell de transparència (0 és transparent, 100 és opac) + El botó d\'tornar surt del feed + Premer tornar sempre surt de l\'app en lloc de desplassar-se fins a la part superior del feed + Forçar animacions + Per defecte + Sempre activat + Sempre desactivat + Ocultar la barra d\'estat + Oculta la barra d\'estat del sistema. Desllisa cap avall des de la part superior per mostrar-la durant un moment + Soluciona la barra d\'estat que cobreix les barres superiors + Manté la disposició de la barra d\'estat coherent, de manera que no cobreixi les barres superiors i no deixi espai buit sobre el reproductor. Desactiva aquesta opció si l\'aplicació es tanca inesperadament o si les barres superiors no es veuen bé + Resplendor + Morphe Glow 1 + Morphe Glow 2 + La pantalla de iniciació resplandecent no és compatible amb aquest dispositiu + Fons del tema clar + Blanc + Material You (Blanc) + Rosa clar + Blau clar + Verd clar + Groc clar + Taronja clar + Vermell clar + Fons clar personalitzat + Estableix el color del fons del tema clar + Canvia el color del primer pla + Utilitza el color del tema per al text i les icones + Habilitar la pantalla de càrrega amb degradat + Mostra un fons degradat a la pantalla de càrrega en lloc d\'un color sòlid + Habilitar un color personalitzat per a la barra de progressió + Color de la barra de cerca + Establiu el color primari de la barra de progrés del vídeo + Color d\'accent de la barra de cerca + Establiu el color secundari de la barra de progrés del vídeo + Valor del color de la barra de cerca no vàlid + Analògic original + Pastís original + Metal original + Logo del capçalera + Per defecte + Regular + Personalitzat + Omplir les restriccions de regió d\'imatge + Redirecciona les sol·licituds de miniatures a un servidor alternatiu per solucionar la manca d\'imatges en països restringits + Títols de DeArrow + Pestaña Inicio + Pestaña Suscripciones + Pestaña Tú + Llistes de reproducció & recomanacions del reproductor + Resultats de la cerca + Mostrar la icona de DeArrow + Mostra la icona de DeArrow als títols de vídeos col·laboratius + Mode casual + Mostra el títol original en lloc del títol de DeArrow si els usuaris de DeArrow han votat que els agrada el títol original + Vots casuals mínims per mostrar el títol original + El nombre de vots casuals en una sola categoria, com ara divertit o creatiu, necessaris per mostrar el títol original + Miniatures de DeArrow + Miniatures originals + Miniatures de DeArrow & originals + DeArrow & captures fixes + Captures fixes + "DeArrow substitueix els títols i miniatures clickbait per uns basats en la col·laboració popular que són més precisos i menys sensacionalistes. Els títols són escrits i votats per la comunitat, i les miniatures són fotogrames extrets del vídeo + +Els vídeos es sol·liciten mitjançant un prefix curt de l'ID del vídeo hashat, de manera que el servidor no pot saber exactament quin vídeo esteu mirant. Si un vídeo no té una miniatura de DeArrow, es mostra l'original o una captura fixa" + Mostrar alertes d\'error de connexió + Mostra una notificació quan el servidor de DeArrow no està disponible + Endpoint de l\'API + La URL de l\'endpoint de la memòria cau de miniatures de DeArrow + Captures de vídeo fixes + Les captures fixes encara es prenen del començament / mig / final de cada vídeo. Aquestes imatges estan integrades a YouTube i no s\'utilitza cap API externa + Extreure fotograma del vídeo de + Principi + Mitja + Final + DeArrow no està disponible temporalment (codi d\'estat: %s) + DeArrow no està disponible temporalment + Restaura l\'estil antic dels botons del reproductor + Reverteix els controls en negreta del reproductor de vídeo a l\'estil visual antic i més prim + Simular les dimensions del dispositiu + Activar això pot desbloquejar qualitats de vídeo més altes + Activar això pot causar talls en la reproducció del vídeo, empitjorar la durada de la bateria i produceir efectes secundaris desconeguts + Resposta hàptica + Trieu quines interaccions activen la vibració del dispositiu + Desactivar la resposta hàptica dels capítols + Desactivar la resposta hàptica de la recerca precisa + Desactivar la resposta hàptica de l\'anul·lació de la recerca + Deshabilitar l\'hàptica de mantenir premut + Desactivar la hàptica de zoom + Si has canviat recentment les dades d\'accés del teu compte, desinstal·la i torna a instal·lar MicroG + Omitir redireccions d\'enllaços + Obre l\'enllaç original directament, ometent l\'enllaç de seguiment (youtube.com/redirect) + Obrir els enllaços al navegador + Obre els enllaços en un navegador extern en lloc del navegador de l\'aplicació + Mida del buffer de reproducció + Per defecte + Baixa + Mitjana + Màxima + Retard del vídeo + Retarda el vídeo per mantenir-lo sincronitzat amb l\'àudio que té retard, com els auriculars Bluetooth (0-5000 ms) + Retardar només amb àudio Bluetooth + Només retarda el vídeo mentre l\'àudio Bluetooth està connectat, de manera que l\'altveu del telèfon es mantingui sincronitzat + Mostrar el botó de retard de vídeo + Mantingueu premut per restableixir el retard a 0 ms + Mostrar el menú de retard de vídeo + Retard en mil·lisegons + Automàtic + Recordar la qualitat del vídeo + Aplica la teva selecció manual de qualitat a tots els vídeos futurs + Notificacions de canvi de qualitat + Mostra una notificació quan es desava una nova qualitat per defecte + Qualitat de vídeo per defecte en xarxa Wi-Fi + Qualitat de vídeo per defecte en xarxa mòbil + Recorda la qualitat dels Shorts + Aplica la teva selecció manual de qualitat a tots els Shorts futurs + Qualitat de Shorts per defecte en xarxa Wi-Fi + Qualitat de Shorts per defecte en xarxa mòbil + S\'ha canviat la qualitat mòbil per defecte a: %s + S\'ha canviat la qualitat de Wi-Fi per defecte a: %s + S\'ha canviat la qualitat de Shorts per a mòbil a: %s + S\'ha canviat la qualitat de Shorts per a Wi-Fi a: %s + Mostra el botó de velocitat de reproducció + Mantingueu premut per restablir la velocitat per defecte + Mostra el botó de qualitat + Mantineu premut per restableir la qualitat per defecte + Mostra la resolució al botó de Qualitat + Mostra la resolució, com ara 1080, en lloc d\'una etiqueta com ara FHD + Menú de velocitat de reproducció personalitzada + Restaura el menú antic de velocitat de reproducció + Velocitats de reproducció personalitzades + Introduïu l\'opció de velocitat de reproducció preferida + Les velocitats de reproducció personalitzades han de ser inferiors a %s + Velocitats de reproducció personalitzades no vàlides + Automàtic + Mantineu premuta la velocitat de reproducció + Estableix el multiplicador de velocitat quan mantinguis premuta la pantalla; estableix a 0 per desactivar-ho + Recorda la velocitat de reproducció + Aplica la teva selecció manual de velocitat a tots els vídeos futurs + Notificacions de canvi de velocitat + Mostra una notificació quan es desava una nova velocitat de reproducció per defecte + Velocitat de reproducció per defecte + Velocitat de reproducció per defecte a Shorts + Recorda la velocitat de reproducció en els Shorts + Aplica la teva selecció manual de velocitat dels Shorts a tots els Shorts futurs. Si està desactivat, la velocitat torna a la predeterminada després de tancar el reproductor de Shorts + Mostra la velocitat de reproducció al menú de Shorts + Afegeix un element de velocitat de reproducció al menú del reproductor de Shorts + Velocitat de reproducció · %s + S\'ha canviat la velocitat de reproducció per defecte a: %s + Utilitzar la velocitat normal per a la música + Reproduir sempre els vídeos musicals a velocitat 1.0x + Llista blanca de velocitat del canal + Els vídeos dels canals de la llista blanca sempre es reproduئين a velocitat 1.0x + Afegeix el canal a la llista blanca de velocitat + Elimina el canal de la llista blanca de velocitat + Habilitar els controls de tony immoral de l\'àudio + Tony immoral d\'àudio de reproducció per defecte + S\'ha canviat el tony immoral d\'àudio per defecte a: %s + Estirament temporal de l\'àudio + Permet ajustar la velocitat i el tony immoral de manera independent, cosa que pot causar artefacts d\'àudio + Recordar la posició de reproducció de la transmissió en directe + Per a les transmissions en directe en curs, recorda i repren la posició de reproducció en lloc de saltar a la posició actual en directe + Aquesta funció no funciona amb transmissions en directe 24/7 i altres transmissions que estiguin \"sempre actives\" + Desactivar vídeo HDR + Forçar vídeo HDR + Força l\'aplicació a reconèixer aquest dispositiu com a compatible amb HDR + "Utilitzeu això si la descodificació HDR funciona però YouTube no ofereix HDR, com passa en alguns dispositius HyperOS que oculten HLG. + +Limitacions: +• Els colors poden veure's incorrectament si la pantalla no pot mostrar HDR realment +• La reproducció pot fallar si el descodificador no pot gestionar HDR +• L'HDR encara depèn del client que emula els fluxos de vídeo" + Còdec de vídeo + Per defecte + Per forçar AVC, canvia \'Simular fluxos de vídeo\' a \'Android Studio\', \'Android VR\' o \'visionOS\' + "VP9 +• Pot millorar la durada de la bateria i solucionar els salts de la reproducció +• Utilitza més dades d'internet que AV1 + +AVC (H.264) +• Requereix el client de Simular flux de vídeo 'Android Studio', 'Android VR' o 'visionOS' +• Pot millorar encara més la durada de la bateria +• Pot restaurar resolucions de vídeo desaparegudes en dispositius antics +• La resolució màxima és 1080p +• Els vídeos HDR potser no utilitzin AVC +• Alguns dispositius no poden forçar AVC" + Obre el menú de qualitat avançada + Omet el menú de selecció de qualitat i obre directament el menú de qualitat avançada + Ocultar opcions de qualitat Premium + Prioritzar la qualitat del vídeo + Prioritza la qualitat de vídeo més alta disponible per sobre de l\'eficiència del còdec + Si aquest ajust està desactivat, els vídeos més antics podrien no mostrar opcions de qualitat de vídeo + Permetre \'Android VR\' / visionOS AV1 + Permet la decodificació AV1 per programari quan s\'utilitza el client \'Android VR\' / visionOS + "Activar aquesta configuració pot utilitzar la descodificació AV1 per programari. + +La reproducció de vídeos amb AV1 pot tenir talls o pèrdues de fotogrames" + Efectes secundaris de la falsificació + • La reproducció pot fallar al cap d\'1 minut + • Falta el menú \'Pista d\'àudio\' + • Sense còdec de vídeo AV1 + • El mode immersiu VR de 360° no està disponible + • El volum estable no està disponible + • La reproducció pot acabar 1 segon abans + • Si la desobfuscació falla, la reproducció podria fallar + • Forçar l\'àudio original no està disponible + • Cal iniciar sessió a Android VR + • La qualitat pot estar limitada a 720p + Variant de JavaScript del reproductor + Mostrar la simulació a \'Estadístiques per a experts\' + Restaura la barra d\'acció de vídeo antiga + "Soluciona que els botons d'acció del vídeo no es puguin ocultar + +Limitació: Torna a l'estil antic de la capa de 'Més vídeos'" + Estil de la icona del reproductor Automàtic + Per defecte + Fina + Negreta + Personalitzat + Estil de la icona de Shorts + Seguiment de sèries + Segueix llistes de reproducció com a sèries i mantingues la teva posició + Habilitar el seguiment de sèries + Afegeix una pàgina de Sèries a l\'Historial i una opció de Seguir sèrie a les llistes de reproducció + Mostrar Sèries a la barra de navegació + Mostrar botó de Seguiment de sèries + Afegeix un botó al reproductor per seguir o obrir la sèrie del vídeo actual + Registrar el progrés de les sèries segudes + Registra la reproducció en aquest telèfon per a les sèries guardades i els marcadors Cancel·lar + Desar, + Desa el marcador + Nom de la sèrie + Opcions de la sèrie + Opcions de la biblioteca + Actualitzar episodis + Canviar nom + Obrir la llista de reproducció a YouTube + Eliminar de la biblioteca + Enllaç o ID de la llista de reproducció + Netejar tot el progrés local + Netejar l\'historial + Esborrar tot el progrés local? + Això elimina les posicions, les anul·lacions de vídeos vistos i les seleccions de reproducció per a cada vídeo. Les sèries guardades i els seus catàlegs es mantenen. Es registrarà una nova reproducció si l\'enregistrament està activat + Marcar com a vist + Marcar com a no vist + Utilitzar l\'estat automàtic + Reiniciar des de 0:00 + Tornar a reproduir des de 0:00 + Estat desat + Reintentar de desar + 100 anteriors + 100 següents + Obre el vídeo desat + S\'ha esborrat el progrés local + Obre l\'Historial de \"Tu\" per veure les teves sèries + Historial de reproducció + Sèries + Carregant sèries desates… + Segueix una llista de reproducció per fer seguiment dels seus episodis + Marcador de llista de reproducció + Actualitzant episodis… + Totes les sèries + Vist + Carregant episodis… + Més opcions + "Registra el progrés només per a les teves sèries desades i els vídeos amb marcadors. + +Aquesta biblioteca s'emmagatzema en aquest dispositiu i es comparteix entre els comptes de YouTube. Els controls de pausa i d'eliminació de l'historial de YouTube no la modifiquen. Utilitza Sèries → Més → Eliminar progrés local per borrar-la. + +El registre automàtic s'atura en mode incògnit, quan la identitat del compte no està disponible o quan canvies a un compte diferent. Activa el seguiment novament per utilitzar-lo amb aquest compte" + El seguiment no es pot activar en mode incògnit o abans que YouTube hagi carregat l\'estat actual del compte. Torna a l\'Inici i torna-ho a intentar. No cal iniciar sessió + Habilitar el seguiment local + El registre del progrés està desactivat + Reprèn a %1$s + Episodi %1$s · %2$s + Episodis %1$s-%2$s de %3$s + Actualitza per intentar carregar els episodis. Les llistes de reproducció privades requereixen accés al compte; el seu marcador encara s\'obre a YouTube + YouTube no ha pogut obrir aquest episodi + YouTube no ha pogut obrir aquesta llista de reproducció + Trieu un episodi + Actualitzat + No hi ha episodis reproduïbles + Vídeo desat · %1$s + Seguir sèrie + Veure sèries + Continuar + Comença aquí + Point de partida desat + Vist fins aquí + Episodis marcats com a vistos + Desfer + Habilitar + Registrar el progrés en aquest dispositiu + "Les teves sèries i el teu progrés es mantenen en aquest dispositiu, compartits entre comptes de YouTube. Els controls de l'historial de YouTube no els modifiquen. Esborra el progrés des del menú de Sèries. + +El registre automàtic és opcional. S'atura en mode incògnit o quan canvia el compte" + No s\'han pogut carregar els episodis. Pots desar la llista de reproducció com a marcador + Hi ha massa canvis no desats. Intenta desar-ho primer + L\'operació de desar està ocupada. Intenta-ho de nou + La càrrega d\'episodis està ocupada. Intenta-ho de nou + L\'emmagatzematge no està disponible. Les vostres dades desades s\'han conservat + No s\'han pogut desar els canvis. Torna a intentar-ho abans de tancar YouTube + Aquesta sèrie ja està desada, o la seva llista de reproducció ha canviat. Torna a obrir-la + Aquesta sèrie ha canviat o ha estat eliminada. Torna a obrir la biblioteca + La llista de reproducció ha canviat. Seleccioneu l\'episodi de nou + Aquest episodi no està disponible. Trieu-ne un altre + Introduïu el nom d\'una sèrie + Introduïu un enllaç o l\'ID d\'una llista de reproducció de YouTube + Si us plau, espereu abans de tornar a actualitzar + No s\'han pogut carregar els episodis. Torna-ho a intentar més tard + No s\'ha pogut completar el canvi. Torna-ho a intentar + No s\'ha pogut confirmar la reproducció. Comprova el reproductor i torna-ho a intentar + Sèries seguidos + Aquest vídeo no és part d\'aquesta llista de reproducció + Buscant llistes de reproducció… + Enganxa l\'enllaç de la llista de reproducció + No s\'ha pogut trobar cap llista de reproducció + No hi ha episodis disponibles + Ocultar els vistos + Marcar els episodis anteriors com a vistos + Comença per: %1$s + Ordre dels episodis + Ordre de la llista de reproducció + Invertir l\'ordre de la llista de reproducció + %1$s visualitzacions + %1$s visada + Marcar episodis com a vistos + Almenys un %1$s%% vist amb un màxim de 90 segons restants, o %2$s segons restants + Marca automàticament un episodi com a vist després de qualsevol dels dos llindors. La regla del percentatge també requereix que no restin més de 90 segons, perquè no es salti el final dels episodis llargs. La regla del temps restant s\'aplica a vídeos de més d\'un minut; estableix-la a 0 per desactivar-la. Els canvis s\'aplicaran a les futures actualitzacions del progrés. Es mantenen les opcions manuals de vist/no vist + Percentatge vist (1-100) + Segons restants (0-300; 0 desactiva) + Introdueix un valor des de %1$s a %2$s + Utilitza el progrés de YouTube + Reprèn les sèries seguides després de veure-les en un altre dispositiu + Llegeix l\'estat de vist i les posicions de reproducció de les sèries que segueixes des del compte actual de YouTube. Només es desenvolupen els episodis coincidents en aquest telèfon. Les comprovacions s\'executen mentre la biblioteca de sèries està oberta i abans de Continuar. L\'historial de YouTube ha d\'estar activat en l\'altre dispositiu + Comprobant el progrés de YouTube… + El progrés de YouTube no està disponible. S\'està utilitzant el progrés desat + Part del progrés de YouTube no està disponible. Es mantenen les posicions desades + Còpia de seguretat i restauració + Desa la teva biblioteca de sèries en un arxiu o restaura una còpia de seguretat + Desa les sèries segudes, les posicions dels episodis i les opcions de vist. No s\'inclouen els permisos de gravació i de progrés de YouTube + Desa la còpia de seguretat + Restaura la còpia de seguretat + Afegeix %1$s sèries. Es mantenen les %2$s sèries que ja són part de la teva biblioteca. Es manté el progrés del vídeo existent + Còpia de seguretat de la sèrie desada + S\'han restaurat %1$s sèries + Aquest fitxer no és una còpia de seguretat de sèrie compatible, o és massa gran. La vostra biblioteca no s\'ha modificat + No s\'ha pogut desar la còpia de seguretat. Proveu una altra ubicació + Sèries eliminades + Aquest episodi ara apareix més d\'una vegada. Escolliu des de quin voleu continuar + L\'episodi desat ja no està en aquesta llista de reproducció. S\'ha conservat la vostra posició desada + Continua amb l\'episodi %1$s · %2$s + També podeu triar qualsevol episodi a continuació. Els episodis anteriors no s\'han modificat + Aquesta llista de reproducció ha canviat. Obriu la sèrie per triar on continuar + No hi ha més episodis en aquesta direcció + Continua %1$s + Cercar sèries + Netejar cerca + No s\'han trobat sèries + Episodi sense títol + Episodi no disponible + Comentaris de Niconico + Mostra el xat en directe i la repetició del xat com a comentaris que desllisen per sobre del vídeo + Mostra un botó superposat al reproductor de vídeo. Toqueu per mostrar o ocultar els comentaris, mantingueu premut per a la configuració ràpida + Mida de la font dels comentaris + Mida del text dels comentaris (12-40 sp) + Opacitat dels comentaris + Opacitat del text i dels fons dels comentaris + Durada del trajecte + Quants segons triguen els comentaris a creuar la pantalla. Els valors més baixos es mouen més ràpid + Longitud màxima del comentari + Amaga els comentaris que tinguin més caràcters que aquest. Cada emoji compta com a un caràcter + Sense límit diff --git a/patches/src/main/resources/addresources/values-ckb-rIR/music/strings.xml b/patches/src/main/resources/addresources/values-ckb-rIR/music/strings.xml index 3d440fad04..a1502a75f6 100644 --- a/patches/src/main/resources/addresources/values-ckb-rIR/music/strings.xml +++ b/patches/src/main/resources/addresources/values-ckb-rIR/music/strings.xml @@ -17,21 +17,157 @@ Second \"item\" text" --> + دوایین جار لێدراوە بەشداربوون + ڤیدیۆکان + ئەنجامەکانی تر + گۆرانییەکانی بەرزکراوە + ئەلبوومە ئەپڵۆدکراوەکان + ئەرتستە ئەپڵۆدکراوەکان + نوێکردنەوەی پاتچی Jamی خازنەکە بۆ بەکارهێنانی ڕاگرتن و بەردەوامکردن + بەکارهێنانی ڕیزبەندی Jam + خازنبە یان پەیوەندببە بە Jamێکی نزیک و ڕیزبەندییەکەی لە ڕێگەی Jam Layerـەوە دەستکاری بکە + چالاککردنی بەشی هاوبەشی ڕیزبەندی Jam + چالاککردنی ئینتگراسیۆنی Jam دوای ڕێستارتکردنەوەی YouTube Music + چینە Jam دابەزێنە بۆ ئەوەی کار بکات + کۆتا وەشانى Jam Layer لە وێبگەڕەکەتدا بکەرەوە + کۆنتڕۆڵەکانی Jam + خازنبە یان پەیوەندببە بە Jamێکی نزیک و بەڕێوەبردنی دانیشتنەکە + پاکەتی Jam Layer + ئەپپ هاوەڵە دامەزراوەکە هەڵبژێرە کە بۆ هاوبەشکردنی ڕیزبەندیی نزیک بەکاردێت + بەکارهێنانی پاکێجێکی هاوەڵە دامەزراو. لێکدانەوە پێویستی بە ڕەزامەندی تۆ و تۆکنی توانای ناوخۆیی هەیە + وای-فای کار پێ بکرێت؟ + Wi-Fi چالاک بکە، پاشان دووبارە هەوڵ بدەرەوە بۆ دەستپێکردن یان پەیوەندبوون بە Jam + کردنەوەی ڕێکخستنەکانی وای-فای پاشگەز + بەکارهێنانی پاکەت + ناوەیەکی دروستی پاکەتی Android بنووسە + پاکێجەکە پاشکەوت کرا. Jam Layer ڕێکبخە بۆ بەستنەوە + Jam پەیوەندی پچڕاوە + سەرەتا Jam Layer چالاک بکە + بەشەکەی هاوبەسیکردنی ڕیزبەندی Jam لە ڕێکخستنەکاندا چالاک بکە، پاشان YouTube Music دووبارە دەستپێبکەرەوە + سەرەتا پاکێجی Jam Layer کە هەڵبژێردراوە دابەزێنە + کۆدەکەی کە لەسەر میزباندەکە host پیشان دەدرێت بنووسە. Wi-Fi Aware دەتوانێت ئامێرەکانی نزیک بەستنەوە، تەنانەت ئەگەر LAN-ەکانیان جیاواز بن + بە کۆدێک پەیوەند ببە + پەیوەندی بکە + سکانکردنی QR + کۆدی هەشت پیت بنوسە یان بانگهێشتنامەیەک لێرە دابنێ + بپیوە بە Jam-ەکەت + کۆپی کۆدەکە + کۆدی Jam + نیشاندانی QR + تەواو + بانگی کەس بکە بۆ Jam-ەکەت + سکان بکە بۆ بەشداربوون و دەستکاریکردنی ئەم ڕیزە. کۆتاییهێنان بە Jam بانگهێشتەکە هەڵدگرێتەوە + کۆپی لینکەکە + بانگی Jam + Jam دووبارە دەبەسترێتەوە؛ دوای کەمێک دووبارە هەوڵ بدەرەوە + زیادکرا بۆ Jam + Jam بەردەست نییە؛ گۆرانییەکە زیاد نەکرا + جەمەکەی تۆ + پێکەوە گوێگرتن + بەشداری کردن لە Jam + پێکەوە گوێ بگرن + نوێکردنەوەی Jam… + گەڕان بەدوای میواندارەکەتدا لەسەر Wi-Fi… + چاوەڕوانی خەڵکە + %s کەس بەستراوەتەوە + %s کەس بەستراونەتەوە + ئەم کۆدە لە ئامێرێکی نزیک بنوسە. Wi-Fi Aware لەسەر LAN-ی جیاواز کار دەکات ئەگەر پشتگیری لێبکرێت. لە %s خولەکدا بەسەردەچێت + بڕۆ بۆ %s؟ + دەستکارییەکان کراوەن + دەستکارییەکان قوفڵکراون + بەڕێوەبراوە لە ڕێگەی Wi-Fi Aware + پەیوەندکراوە لە ڕێگەی Wi-Fi + پەیوەندکراوە لە ڕێگەی Bluetooth + ناپسۆرە لە کاتی Jam + بەشداربوون هەڵوەشاوە + دووبارە پەیوەندکردن بە میواندارەکەت… + ڕیزێک بەش بکە. ڕێگە بدە هەمووان گۆرانی زیاد بکەن + بانوێتی خەڵک بکە + مێوان دەتوانن ڕیزبەندییەکە دەستکاری بکەن + کۆتایی هێنان بە Jam + میواندار میوزیکەکە لێدەدات. گۆرانییەکان ڕاستەوخۆ لە ڕیزبەندی YouTube Musicدا زیاد بکە، بگۆڕە یان لایببە + هەردوو ئامێرەکە لە نزیک یەکتر بگرە. دەتوانیت وەستانێیت لە کاتێکدا ئێمە بەدوای میواندارەکەدا دەگەڕێین + پاشگەزبوونەوە لە بەشداربوون + جێهێشتنی Jam + دەستپێکردنی Jam + ڕێکخستنی چینى Jam + بەڕێوەبردن وردەکاری کردنەوە + بارکردنی بژاردەکانی گۆرانی... + بژاردەکانی گۆرانی بەردەست نین؛ دووبارە هەوڵ بدەوە + نەتوانی ڕیکخستنی گۆرانی بکرێتەوە + هیچ وێبگەڕێک بەردەست نییە بۆ دابەزاندنی چینى Jam + چاوەڕوانی ڕیزبەندی میواندار + سەرەتا گۆرانیەک لە YouTube Music بکەرەوە + لەسەر میزباند لێبدە + جەم جێبهێلە و بە شێوەی خۆماوەی لێبدە + دواتر لە جەم لێبدە + بۆ ڕیزی جەم زیاد بکە + ئەم تراکی جەمە لێبدە + دیاری بکە لە کوێ لێبدە + میزباندەکە بهێنە بۆ ئێرە + گۆرانییەکە ئامادەی نەبوو بۆ گەڕان + ڕیزەکە گۆڕاوە؛ دووبارە هەوڵ بدەوە لۆگۆی سەرەوە بنەڕەت باو + لێدانی تڕاکە دەنگییەکانی ئەلبەمەکە + لێدانی وەشانە دەنگییەکەی گۆرانییەکانی ئەلبەمەکە لە جیاتی ڤیدیۆ میوزیک. پێویستی بە \'Spoof video streams\' هەیە + چالاککردنی گۆڕینی دەنگ/ڤیدیۆ + هەوڵ بدە وەشانى تراکى دەنگ و ڤیدیۆی فەرمی مۆسیقاکە بگۆڕیت + دابەزاندنی ڤیدیۆی موزیک، پۆدکاست، یان گۆرانی بە بەرنامەکانی دابەزاندنی لایەنی سێیەم + بەکارهێنانی دابەزێنەری دەرەکی بۆ مینیو دابەزاندن + بژاردەی دابەزاندنی مینیوکان لە جیاتی ئەوە دابەزێنەری دەرەکییەکەت دەکاتەوە + دابەزاندنەکان + "هیچ دابەزاندنێک نییە +تراکە دابەزێندراوەکان لێرە دەردەکەون" + %1$s • %2$s • %3$s MB + %1$s ট্র্যাক • %2$s MB + گەڕان لە دابەزاندنەکاندا + ڕیزکردن + ڕیزکردن بەپێی هونەرمەند + ڕیزکردن بەپێی ناونیشان + ڕیزکردن بەپێی قەبارە + سڕینەوەی هەموو داگرتنییەکان + هەموو داگرتنییەکان بسڕینەوە؟ + هەموو تراکە داگرتراوەکان لەم ئامێرە دەسڕێنەوە + داگرتنییەکان سڕاونەوە + هیچ شتێک نەدۆزرایەوە + کردارەکان بۆ %s + لێدان + لێدان یان وەستان + پێشوو + دواتر + ڕاگرتن + داخستن + سڕینەوە + سڕینەوەی دابەزاندنەکە + دابەزاندنەکە بسڕینەوە؟ + %s لەم ئامێرە دەسڕێتەوە + دابەزاندنەکە سڕایەوە + نەتوانرا دابەزاندنەکە بسڕێتەوە + نەتوانرا داگرتنەکە لێ بدرێت + دابەزاندنی دەنگ + لێدانی ئۆفلاین + نەتوانرا ناساندنی تراكەکە بۆ دابەزاندن + دابەزاندن پێشتر دەستی پێکراوە + پێشتر دابەزێنراوە + دابەزاندن دەستی پێکرد + دەستپێکردنی دابەزاندن… + دابەزاندنی %s%% + دابەزاندن تەواو بوو + دابەزاندنەکە سەرکەوتوو نەبوو دربارە @@ -39,17 +175,42 @@ Second \"item\" text" پێشهات گشتی لێدەر + تێکستی گۆرانی هەمەجۆر + زمانی YouTube Music + زمانی سیستم + پێویستی بە ئەندرۆید ١٣ یان نوێتر هەیە شاردنەوەی پلیتی \'Get Music Premium\' + پڕۆمۆشنەکانی Music Premium بشارەوە شاردنەوەی ڕێکلامی ڤیدیۆکان + گۆڕەری Audio / Video بشارەوە Hide Download button + دوگمەی داگرتنی لە پیشاندانی پۆدکاستدا دەشارێتەوە + ڕەفەی Explore بشارەوە + شریتی فلتەر بشارەوە + ڕەفەکانی تۆڕەکە بشارەوە + ڕەفە ئاسۆییەکان بشارەوە + ڕەفەکانی لیستەکە بشارەوە + پانێڵی تێکستەکان جفيء دسته بتونه + دوگمەی وەرگێڕان بشارەوە + ڕەفەی \'New from\' بشارەوە + ڕەفەکانی لیستی گۆرانییەکان بشارەوە + دوگمەی دووبارەکردنەوە بشارەوە + شاردنەوەی دوگمەی تێکەڵکردن + شاردنەوەی ڕەفەی \'پەیوەندی خێرا\' + کۆمێنتەکان Choose which components appear in the comments section + شاردنەوەی ڕێنماییەکانی کۆمەڵگە + نامەی کۆنتێکست بشارەوە + شاردنەوەی دوگمەی ئیمۆجی دوگمەی شاردنەوەی زانیارییەکان + دوگمەی کات و بەرواری تۆمارکردن بشارەوە پەڕینەوە + بە شێوەیەکی نەرم گۆرانییەکان تێکەڵ بکە لەگەڵ یەکتر چالاککردنی پەڕاندنەوەی یەکتربڕ کاڵبوونەوەی چەماوە هێزی یەکسان @@ -57,6 +218,8 @@ Second \"item\" text" ئاسانی نەرم هێڵی لار-S چەماوە + دەرچوون + حاترکەوتن ماوەی کڕکاڵبوونەوە 250 میلی چرکە 500 میلی چرکە @@ -82,20 +245,65 @@ Second \"item\" text" پەڕینەوە وەستا بۆ ئەم دانیشتنە پەڕینەوە دەستی پێکردەوە دۆخی ڤیدیۆ بەردەست نیە کاتێک پەڕینەوە چالاک کراوە + Crossfade لە کاتی لێدانی SABR دا کوژاوەتەوە + Crossfade لە کاتی کاستینگدا کوژاوەتەوە + چۆنیەتی کارکردنی + "Crossfade کۆتایی گۆرانییەک لەگەڵ سەرەتای گۆرانییەکی تردا تێکەڵ دەکات، بە بەکارهێنانی ماوە و خشتەی تۆ + +ئەمە لە کاتی پێشکەوتنی خۆکار و گۆڕینی دەستی کاردەکات، هەر یەکێکیان گۆڕینەوەی تایبەتی خۆی هەیە. لە پێشکەوتنی خۆکاردا، تێکەڵکردنەکە چەند چرکەیەک پێش کۆتاییهاتنی گۆرانییەکە دەست پێ دەکات" + سەرهاتییەکان + "ڤیدیۆکانی مۆسیقا تەنها وەک دەنگ لێدەدرێن کاتێک crossfade چالاکە. بۆ ڕاگرتنی crossfade و بینینی ڤیدیۆکە، پەستانی درێژ لەسەر shuffle بکە + +ئەو ڤیدیۆیەی مۆسیقایەی کە دەست دەکات، لە مۆدی ڤیدیۆدا دەمێنێت تا گۆرانی داهاتووە + +ئەو گۆرانیانەی کە ماوەیان لە ماوەی fade کورتترە، crossfade نابن + +شاشەی قفڵکردن یان ئاگادارکردنەوە ڕەنگە بۆ ماوەیەکی کورت دۆخێکی کۆنی نیشان بدات لە ناوەڕاستی گۆڕانەکەدا + +ڕاگرتنی crossfade بۆ بینینی ڤیدیۆیەکی مۆسیقا ڕەنگە هەندێک ڤیدیۆ ڕەش بکات. ڕاگرتنی crossfade پێش هەڵبژاردنی گۆرانیەکە ڕێگری لێ دەکات" + پشتیوونی ناکات + "کاستینگ یان ڕەنگدانەوەی دەنگ (Chromecast، ڕەنگدانەوەی شاشە یان دەنگ) + +لێدانی SABR (چەمانەوەی ستریمەکانی ڤیدیۆ کوژاوەتەوە)" + کۆددان + تایبەتمەندی Crossfade لەلایەن VazerOG-ەوە + بیرهێنانی دۆخی دووبارەکردنەوە + بیرهێنانی دۆخی تێکەڵکردن + خێرایی لێدانی موزیک + خێرایی بەکارهاتوو بۆ هەموو تراکەکان. لە ١٠٠٪ خێرایی ئەپەکە بەکاردێت، وەک خێرایی لێدانی پۆدکاست + گۆڕینی تۆن لەگەڵ خێرایی + ڕاگرتنی ئاڕاستەکردنی ناڕەزایەتی + ڕێگری دەکات لە ڕۆیشتن بۆ گۆرانییەکی داهاتوو کاتێک دوگمەی ناڕەزایەتی دەخرێتە سەر + کردارەی کلیک کردن لەسەر تراکی لیستى گۆرانییەکان + لستى گۆرانییەکان لێبدە + تەنها تراکە هەڵبژێراوەکە لێبدە + تراکە هەڵبژێراوەکە زیاد بکە بۆ ڕیزبەندی + زیاد کرا بۆ ڕیزبەندی دوگمەی پەخشکردن بشارەوە + دوگمەی مێژوو بشارەوە + دوگمەی ئاگاداری بشارەوە + دوگمەی گەڕان بشارەوە + دوگمەی گەڕانی دەنگی بشارەوە + دوگمەی گەڕانی دەنگ بشارەوە + شاردنەوەی دوگمەی نوێ + دوگمەی نوێ لە بەشی کتێبخانەدا دەشارێتەوە + دووگمەکانی کردار + دیاری بکە کام دووگمەی کردار لە ژێر پلەیەرەکە دەردەکەون شاردنەوەی باری کردار + دوگمەی بەدڵخواز و نابەدڵخواز بشارەوە شاردنەوەی دوگمەی کۆمێنتە دوگم کردن دوگمەی \"دووبارە بینەوەی چاتە زیندییەکە\" + دوگمەی تێکستی گۆرانییەکان بشارەوە جفيء دسته بتونه @@ -103,10 +311,44 @@ Second \"item\" text" Hide Download button + دوگمەی \'ڕادیۆ دەستپێبکە\' بشارەوە + مەنیوی فلاین ئاوت + دیاری بکە کام پێکهاتەکان لە مەنیو فلاین ئاوتی پلەیەر و ڕیزبەندییەکەدا دەربکەون + شاردنەوەی پێکهاتەی ٣ ستوون + شاردنەوەی دوگمەکانی بەلاڤکردن و نابەلاڤکردن + شاردنەوەی مێنیۆی \'گونجانی تام\' + شاردنەوەی مەنیوی \'زیادکردن بۆ لیستی گوێگرتنی دواتر\' + شاردنەوەی مینیۆی \'زیادکردن بۆ ڕیزبەندی\' Hide Captions menu + شاردنەوەی مینیۆی \'سڕینەوەی لیستی موزیک\' + شاردنەوەی مینیۆی \'سڕینەوەی ڕیزبەندی\' + شاردنەوەی مینیۆی \'پێشنیاری ئەم هونەرمەندە مەکە\' + شاردنەوەی مێنیۆی داگرتن + شاردنەوەی مینیۆی \'دەستکاریکردنی لیستی موزیک\' + شاردنەوەی مینیۆی \'چوون بۆ ئەلبەم\' + شاردنەوەی مینیو \'بچۆ بۆ هونەرمەند\' + شاردنەوەی مینیو \'بچۆ بۆ ئەپیزۆد\' + شاردنەوەی مینیو \'بچۆ بۆ پۆدکاست\' + شاردنەوەی مێنیۆی هاوکاری + شاردنەوەی مینیو \'ئەپیزۆدەکە وەک بینراو نیشان بدە\' + شاردنەوەی مینیو \'سەرنجم ڕاناکێشێت\' + شاردنەوەی مینیو \'پێگیراوە بە پەیوەندی خێرا\' + شاردنەوەی مینیۆی \'پێشتر لێبدە\' Hide Quality menu + شاردنەوەی مینیۆی \'لابەردانە لە کتێبخانە\' + شاردنەوەی مینیۆی \'لابەردانە لە لیستى لێدان\' + شاردنەوەی مێنیۆی ڕاپۆرت + شاردنەوەی مینیۆی \'پاشکەوتکردنی ئەپیزۆد بۆ دواتر\' / \'پاشکەوتکردن لە کتێبخانە\' + شاردنەوەی مینیۆی \'پاشکەوتکردن لە لیستی لێدان\' + شاردنەوەی مێنیۆی بەشیکردن + شاردنەوەی مینیۆی \'لێدانی هەڵتەپەسەر\' Hide \'Sleep timer\' menu + شاردنەوەی مێنیۆی \'دەستپێکردنی ڕادیۆ\' + شاردنەوەی مێنیۆی \'ئامارەکان بۆ شارەزایان\' + شاردنەوەی مێنیۆی سەبسکرایب + شاردنەوەی مێنیۆی \'لادانی پین لە پەیوەندی خێرا\' + شاردنەوەی مێنیۆی \'بینینی زانیارییەکانی گۆرانی\' پەڕەی سەرەتا بنەڕەت @@ -120,9 +362,13 @@ Second \"item\" text" گەڕان بەشداربوون + سەپاندنی ئاراستەی ستوونی + ئەپەکە لە شێوازی ستوونی دەهێڵێت کاتێک ئامێرەکە دەسوڕێنرێت گۆڕینی ڕەنگی miniplayer ڕەنگی miniplayer لەگەڵ لێدەری پڕ شاشە دەگونجێنێت + گۆڕینی ڕەنگی شریتی گەڕان + ڕەنگی شریتی گەڕان لەگەڵ لێدەری پڕ شاشە دەگونجێنێت پیشاندانی دوگمەی داهاتوو لە miniplayer پیشاندانی دوگمەی پێشوو لە miniplayer @@ -130,6 +376,8 @@ Second \"item\" text" چالاککردنی زۆرەمڵێی miniplayer ناهێڵێت لێدەر بە شێوەیەکی ئۆتۆماتیکی فراوان ببێت بۆ پڕ شاشە کاتێک تراکێکی نوێ دەست پێدەکات + چالاککردنی ڕاکێشان بۆ داخستنی مینیپڵەیەر + ڕێگە دەدات مینیپڵەیەرەکە بە ڕاکێشانی خوارەوە ببڕیت شریتی گەشتکردن هەڵبژێرە کام دوگمەیە دەردەکەوێت لە شریتی گەشتکردن @@ -146,17 +394,298 @@ Second \"item\" text" شریتی گەشتکردن بشارەوه ناونیشانەکانی دوگمەی گەشتکردن بشارەوە + نەچووەتە ژوورەوە. بۆ ڕێکخستن کلیک بکە + دەرچوون + دەرچووە + چالاککردنی scrobbling + ناردنی ستاتوسی \'ئێستا لێدەدرێت\' + کەمترین ماوەی گۆرانی + ئەو گۆرانییانەی لەم ماوەیە کورتترن scrobble ناکرێن + ڕێژەی دواکەوتنی scrobble + دوای گوێگرتنی ئەم ڕێژەیەی گۆرانییەکە، scrobble چالاک بکە + چرکەی دواکەوتنی scrobble + دوای گوێگرتن بۆ ئەم هەموو چرکەیە scrobble بکە + هژماری ListenBrainz + چووەتە ژوورەوە. بۆ بەڕێوەبردنی هەژمارەکە کلیک بکە + تۆکنی بەکارهێنەری ListenBrainz بنوسە بۆ چالاککردنی scrobbling + تۆکەن + پشکنین + تکایە تۆکەنێک بنوسە + پشکنین دەکرێت... + تۆکەنەکە دروستە! بەکارهێنەر: %s + تۆکەنەکە هەڵەیە: %s + نەناسراو + پشکنینەکە شکستی سەند: %s + تۆکەنەکە پاککراوە + تۆکەنەکە پاشکەوت کرا + ئاگاداری: تۆکەنی پاشکەوتکراو هەڵەیە! + خۆشویستنی گۆرانییە لایککراوەکان + بە شێوەیەکی ئۆتۆماتیکی گۆرانییەکان لە Last.fm خۆشەوíst بکە کاتێک لە YouTube Music لایکیان دەکەیت (و خۆشەوístەکە لابدە کاتێک لایکەکەت لادەبەیت یان نا لایک دەکەیت) + ئاکاونتی Last.fm + چووەتە ژوورەوە وەک %s. کلیک بکە بۆ بەڕێوەبردنی ئاکاونتەکە + زانیارییەکانی Last.fm بنوسە بۆ چوونه‌وه‌ + ناوی بەکارهێنەر + وشەی نهێنی + چوونە ژوورەوە + تکایە ناوی بەکارهێنەر و وشەی نهێنی بنووسە + چوونە ژوورەوە... + بە سەرکەوتوویی چووە ژوورەوە! + نەتوانرا دانیشتن لە Last.fm وەربگیرێت + چوونە ژوورەوە سەرکەوتوو نەبوو: %s + زانیارییەکانی Last.fm پاشکەوت کران! + پاککردنەوەی مێتاداتا + پاککردنەوەی مێتاداتا + بە شێوەیەکی ئۆتۆماتیکی شتە زیادەکانی باو (وەک \"Official Video\", \"Remaster\", \"Live\") لە ناونیشانەکان، هونەرمەندان و ئەلبوومەکان پاک بکەرەوە پێش scrobbling + پاککردنەوەی تایبەتی regex + دەربڕینێکی ڕێکی تایبەت بنووسە بۆ لادانی دەقە زیادەکان لە ناوی گۆرانی، ئەستێد، یان ئەلبوومەکان. نموونە: (?i)\\s*\\((clean|explicit)\) + هونەرمەند و تراک لە ناونیشانەکە وەربگرە + ناوی هونەرمەند و گۆرانییەکە لە ناونیشانی ڤیدیۆکە دەربهێنە بە بەکارهێنانی جیاکەری \" - \". ئەمە بەسوودە بۆ ئەو ناوەڕۆکانەی دووبارە 업لۆد کراون کە ناوی کەناڵەکە وەک هونەرمەند بەکاردێت + پڕکردنەوەی ئەلبەمی ونبوو + کاتێک مۆسیقاکە ئەلبومێکی تێدا نەبێت، لە Last.fm بگەڕێ بۆ ئەوەی scrobble-ەکەت تەواو بێت + "scrobbling بە شێوەیەکی ئۆتۆماتیکی بەکارهێنانی YouTube Music ڕێکدەخات لەگەڵ پرۆفایلی Last.fm یان ListenBrainz + +بۆ دڵنیابوون لەوەی scrobbling لە کاتی لێدانی گۆرانی لە پشتپاشان ڕێکدەخرێت، دڵنیاببە کە بەکارهێنانی باتری ئەپی YouTube Music لەسەر \"Unrestricted\" ڕاگراوە" + بەکارهێنانی تێکستی گۆرانی لەلایەن لایەنی سێیەمەوە + تێکستی گۆرانی لە خزمەتگوزارییەکی لایەنی سێیەم نیشان دەدات، و ئەگەر هیچێکیان نەدۆزرانەوە دەگەڕێتەوە بۆ تێکستی گۆرانی ناوخۆیی + شاشەکە چالوو بهێڵە لە کاتی بینینی تێکستەکاندا + رێگری دەکات لە کوژانەوەی شاشەکە کاتێک پانێڵی تێکستەکان کراوەیە + خزمەتگوزاری تێکستی گۆرانییەکان + پیشاندان + هاوسەنگکردن + پانێڵ + کۆتایی AI + هەموو دابینکەران ڕاگیراون - دەقە ناوخۆییەکان و دەقە لەناوبووەکان + ڕێکخستنی دابینکەران بگەڕێنرێتەوە بۆ سەرەتایی؟ + گەڕاندنەوەی دابینکەران بۆ ڕێکی پێشنیارکراوی سەرەتایی + پاککردنەوەی تێکستە پاشکەوتکراوەکان + تێکستەکانی گۆرانی کە لەم ئامێرەدا هەڵگیراون بسڕەوە + %1$s MB پاککرایەوە + وەرگرتنی تۆکن + تۆکنی بەکارهێنەری میدیای Apple Music + "کووکی media-user-token لە music.apple.com لێرە دابنێ + + هەنگاوەکان: +١. لە music.apple.com وەک ئەندام بچۆ ژوورەوە +٢. DevTools → Application → Cookies → music.apple.com بکەرەوە +٣. بەهای \"media-user-token\" کۆپی بکە" + Apple Music: media-user-token پاشکەوت کرا + Apple Music: media-user-token پاککراوە + Apple Music: media-user-token نادروستە - تکایە پشکنینی بکە و دووبارە هەوڵ بدەرەوە + Spotify SpDc + "کووکی sp_dc لە Spotify لێرە دابنێ. + +هەنگاوەکان: +١. بچۆوە ناو open.spotify.com و داخڵبە +٢. DevTools → Application → Cookies → open.spotify.com بکەرەوە +٣. بەهای \"sp_dc\" کۆپی بکە" + Spotify: sp_dc پاشکەوت کرا + Spotify: sp_dc پاککراوە + Spotify: sp_dc نادروستە - تکایە پشکنینی بکە و دووبارە هەوڵ بدەرەوە + تۆکنی Apple Music ئارەزوومەندانەیە. بەبێ ئەوە، Lyrically وەک جێگەیگۆڕە بەکاردێت + Deezer ARL + "کووکی arl لە Deezer لێرە دابنێ. + +هەنگاوەکان: +١. بچۆوە ناو deezer.com و داخڵبە +٢. DevTools → Application → Cookies → deezer.com بکەرەوە +٣. بەهای \"arl\" کۆپی بکە" + Deezer: arl پاشکەوت کرا + Deezer: arl پاککراوە + Deezer: arl نادروستە - تکایە پشکنینی بکە و دووبارە هەوڵ بدەرەوە + تۆکنی Musixmatch + تۆکنێک تەنها بۆ ئەوە زیاد بکە کە هەژماری خۆت بەکاربهێنیت. دەبێت لە ئەپڵیکەیشنی Musixmatch بۆ ئەندرۆیدەوە بێت: ئەو تۆکنانەی لە musixmatch.com کۆپی دەکرێن سەر بە کلایانتی دیسکتۆپی کۆنەن و API رەتی دەکاتەوە + Musixmatch بەبێ تۆکن کاردەکات، بۆیە ئەم خانەیە دەکرێت بەتاڵ بمێنێت + تۆکنی بەکارهێنەر + Musixmatch: تۆکنی بەکارهێنەر پاشکەوت کرا + Musixmatch: تۆکنی بەکارهێنەر پاککرا + Musixmatch: تۆکەنی بەکارهێنەر نادروستە - تکایە پشکنینی بکە و دووبارە هەوڵ بدەرەوە + کوکیەکانی YouTube + "کوکییەکانی YouTube لێرە دابنێ بۆ داواکردنی ژێرنووس و دەقی کاتەکان لە YouTube. + +هەنگاوەکان: +1. youtube.com لە وێبگەڕێکدا بکەرەوە و بچۆ ژوورەوە +2. DevTools (F12) بکەرەوە ← Application ← Cookies ← youtube.com +3. تەواوی ستێرنگی کوکییەکان کۆپی بکە (جووتەکانی key=value کە بە خاڵ و کۆما جیاکراون) + +کوکییە پێویستەکان: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPISID, LOGIN_INFO" + YouTube: کوکییەکان پاشکەوتکران + YouTube: کوکییەکان پاککرانەوە + YouTube: کوکییەکان نادروستن - تکایە پشکنینیان بکە و دووبارە هەوڵ بدەرەوە + هێڵێک دابگرە + سەر هێڵێک دابگرە بۆ ئەوەی بچیت بۆ ئەو ساتە لە گۆرانییەکەدا، بۆ ماوەیەک دایبگرە بۆ کۆپیکردنی وشەی ژێر پەنجەت، یان هەموو هێڵەکە ئەگەر پەنجەت سەر وشەیەک نەبێت. لێدانی \"کۆتایی\" هەردوو ئەم جوڵەیە لادەبات + پیشاندانی تاگی بەشەکان + تاگی بچووک بخە سەر هەر بەشێکی نوێی تێکستی گۆرانییەکان، وەک Verse یان Chorus + نیشاندانی دوگمەی کۆپی + دابگرە بۆ کۆپیکردنی تێکستی ڕەسەنی گۆرانییەکە وەک تێکستی سادە؛ بۆ ماوەیەک دایبگرە بۆ پاشکەوتکردنی فایلەکەی سەرچاوەی تێکستەکە لە Downloads + نیشاندانی دوگمەی وەرگێڕان + دابگرە بۆ گۆڕینی وەرگێڕان؛ بۆ ماوەیەک دایبگرە تەنها بۆ وەرگێڕان. وەرگێڕانی دابینکەر، Google Translate، یان APIیەکی هاوتا بە OpenAI بەکاردێنێت + نیشاندانی دوگمەی رۆمانایز + دابگرە بۆ گۆڕینی ڕۆمانایزەکردن؛ بۆ ماوەیەک دایبگرە تەنها بۆ ڕۆمانایزەکردن. ڕۆمانایزەکردنی دابینکەر، Google Translate، یان APIیەکی هاوتا بە OpenAI بەکاردێنێت + ڕۆمانایزکردن و ڕێکخستنی ناوخۆیی + لەسەر ئامێرەکە ڕۆمانایز بکە کاتێک دابینکەر، مۆدێلەکەت و Google Translate سەرکەوتوو نەبن. خوێندنەوەکە دوای هەر وشەیەکی تێکستی کاتی دەکەوێت؛ دەبێت هاوسەنگکردنی وشەکان چالاک بێت. چینی، کانتۆنی، ژاپانی، کۆری، یۆنانی، ڕوسی، جۆرجی، ئەرمەنی؛ نزیکەی ١٢٠ مێگابایت + خوێندنەوەی %1$s: %2$d%% + خوێندنەوەکان ئامادەن (%1$d تۆمار) + داگرتنی نەسەرکەوت + نیشاندانی دوگمەی نوێکردنەوە + کلیک بکە بۆ گۆڕین بۆ کاندیدی داهاتووی تێکست؛ پەستانی درێژ بکە بۆ گەڕان بەدوای تێکستدا بەپێی ناوی گۆرانی و هونەرمەند + گەڕانی تێکستەکانی گۆرانی + ناونیشانی گۆرانی + هونەرمەند + ناونیشانی گۆرانی و هونەرمەندێک بنووسە + هیچ کام لە دابینکەرە چالاککراوەکان ناتوانن گەڕان بۆ گۆرانییەکانی تر بکەن + پاشکەوتکراو + دیاریکردنی وشە یان syllable-ە هاوکاتکراوەکان + پێشینەدان بە تێکستی وشە بە وشە کاتێک بەردەست بێت. کاتێک تێکستەکە کاتی وشەکانی تێدابێت، هەر وشە یان syllable-ێک لەگەڵ کاتی مۆسیقاکەدا دیاری دەکات + شاردنەوەی تێکستی لێدراو + شاردنەوەی دێڕەکانی تێکست لەسەر ئەو دێڕەی ئێستا لێ دەدرێت + شاردنەوەی تێکستی نەلێدراو + شاردنەوەی دێڕەکانی تێکست لە خوار ئەو دێڕەی ئێستا لێ دەدرێت + پیشاندانی تێکستی MediaSession لە شاشەی قفڵکراو + دێڕی ئێستا لە شاشەی قفڵ، Android Auto و Bluetooth پیشان دەدات. پێویستی بە Android 11 یان نوێترە + پیشاندانی تێکستی مینیپڵەیەر + هێڵی ئێستا لە پلییەرە بچووکەکەدا پیشان دەدات، لەگەڵ گواستنەوەی تڕاکەکە بۆ ژێرنووسەکە + فۆرماتی ناونیشانی گۆرانی/هونەرمەند + فۆرماتی تڕاکەکە کە لەگەڵ تێکستەکە لە مینیپڵەیەر و MediaSession پیشان دەدرێت. {t} ناونیشانەکەیە، {a} هونەرمەندەکەیە و {v} وەشانەکەیە، وەک Acoustic، کاتێک سەرچاوەی تێکستەکە دانەیە. تێکستی نێوان دوو کۆلۆن تەنها کاتێک پیشان دەدرێت کە هەموو گۆڕاوەکانی ناوەوەی بەهایان هەبێت؛ بۆ کۆلۆنێکی ئاسایی \\: بنوسە. هەر دێڕێک بە ڕیزبەندی پیشان دەدرێت و دەگۆڕێت هەر کاتێک تێکستەکە بگۆڕێت + فۆرماتی دێڕی تێکست + شێوازی دێڕی تێکستی گۆرانییەکە لە Miniplayer و MediaSession. {lrc} دێڕەکەیە و {n} ژمارەی جارەکانیەتی کە بە ڕیزبەندی دەوترێت. دەقەکەی نێوان کۆلۆنەکان تەنها کاتێک پیشان دەدرێت کە دێڕەکە دووبارە ببێتەوە. بۆ کۆلۆنێک \\: بەکاربهێنە + شێوازەکە دەبێت {lrc} ی تێدابێت + قالبى دێڕی هونەرمەند و کاتی ئەنیمەیشن + کەمترین کاتی شاشە (ms) + زۆرترین کاتی شاشە (ms) + تەنها بۆ ئەنیمەیشنی چەند دێڕی. هەر فریمێک بۆ کەمترین کات دابنێ. لە زۆرترین کاتدا، دێڕی هونەرمەندەکە پێببە تەنانەت ئەگەر تێکستی گۆرانییەکە هەمان شت بووبێت. + کەمترین دەبێت 0 یان زیاتر بێت. زۆرترین دەبێت زیاتر بێت لە 0 و لانی کەم هەمان کاتی کەمترین بێت. + سەنگەکانی درێژی دێڕ + سەنگەکانی دێڕی تێکست و ئەستەت، و کێشی کارەکتەرە پانەکانیش + "سەنگی دێڕی تێکست +تێکستە درێژەکان دابەش بکە بۆ دوو بەشی هاوسەنگ. 0 دابەشکردن ڕادەگرێت." + "سەنگی دێڕی ئەستەت +فۆرماتی تراکی پڕکراو دابەش بکە بۆ فریمەکانی سوڕانەوە. 0 دابەشکردن ڕادەگرێت." + "کێشی کارەکتەرە پانەکانیش +بۆ هەردوو سەنگەکە جێبەجێ دەکرێت. لە ١.٧٥، هەر چوار کارەکتەرێکی پان وەک ٧ هەژمار دەکرێن." + سەنگەکانی ژمارەی تەواو لە ٠ تا ١٠٠٠٠ و کێشی کارەکتەرێک زیاتر لە ٠ بنوسە + پشکی ئامرازی مۆسیقا + لە مینیپڵەیەر و MediaSession لە کاتی پشکێنی ئامێرەکاندا پیشان دەدرێت. هەر دێڕێک فریمێکی ئەنیمەیشنە. {t} ناونیشانەکەیە، {a} هونەرمەندەکەیە و {lrc} دوا دێڕی تێکستەکەیە + کاتی جیاوازی مینیپڵەیەر و MediaSession + کاتەکەی دەگۆڕێت کە دێڕی داهاتوو لە مینیپڵەیەر و MediaSession پیشان دەدرێت. بەهای نەرێنی بەکاربهێنە ئەگەر دێڕەکان بە دواوە دەرکەون، بۆ نموونە لە شاشەی قفڵکردندا + فلتەرەکان + Regex مێتاداتا + دەربڕینێکی ڕێگایەکی ئاسایی کە لەسەر ناونیشانەکان، هونەرمەندەکان و ئەلبوومەکان جێبەجێ دەکرێت پێش گەڕان بەدوای تێکستەکانی گۆرانی و لە کاتی هەڵسەنگاندنی ئەوەی ئایا دوو ناونیشان دەگەنە یەک. بۆ لادانی هەردووکیان بە بەتاڵی جێبهێڵە. ڕێکخستنی سەرەکی شتە زیادەی باوەکان لادەبات وەک \"Official Video\", \"Remaster\", \"【插曲】\", و \"- Topic\" + ڕێگاکسپرێشنی فلتەری تێکستی گۆرانییەکان + ڕێگاکسپرێشنێک کە تەنها لەسەر تێکستی ڕەسەنی گۆرانییەکان جێبەجێ دەکرێت (نەک وەرگێڕانەکان یان ڕۆمانایزەکان). بۆ لایبەرکردنی بەتاڵى بهێڵە + نیشانەکانی هێڵی زانیاری + ئەگەر تێکستی گۆرانییەکان بە هەڵە فلتەر کران بۆ خوارەوە، ئەمەی ڕێک بخە یان سڕی up. نیشانەکانی بە کۆما جیاکراون: ئەو هێڵانەی نیشانەیەک تێدایە و دوایش کەلۆن (:) دێت، وەک زانیاریی خاوەنماڵ لە خوارەوە پیشان دەدرێن. بۆ لایبەرکردنی بەتاڵى بهێڵە + پیتێکی یەکەمی بە پیتی گەورە بنووسە + پیتێکی یەکەمی هەر دێڕێکی تێکست بە پیتی گەورە دەنووسێت + لابردنی نیشانەکانی کۆتایی ڕستە + خاڵ، کۆما، دووخاڵ و نیوخاڵ لە کۆتایی دێڕەکانی تێکستەکە لادەبات. نیشانەی پرسیار، نیشانەی سەرسوڕمان، سێ خاڵ، نیشانەی کوتاس و کەوانەکان دەمێننەوە + قەباره‌ی تێکست + قەباره‌ی تێکستی گۆرانییەکان + گۆڕانی کاتی گشتی + تێکستی گۆرانییە هاوخەونەکان لەگەڵ لێدانی موزیکەکە دەڕۆن. ئەگەر لادان هەبوو، کاتی گۆڕانکاری offset ڕێک بخە. تێکستە هاوخەونەکان بەرەو پێش یان دواوە بگوازرە. بەهای ئەرێنی بەکاربهێنە ئەگەر تێکستەکە پێش موزیکەکە دەڕۆی + بەکارهێنانی بەشەکانی SponsorBlock بۆ گۆڕینی شوێنی تێکستەکان + بەستنەوە بێ ئەو بەشانەی کە SponsorBlock تێپەڕیان دەکات و گواستنەوەی تێکستەکان بۆ سەر دەنگی بڵاوکراوەکە. لایەنی کوژاندنەوە بە تەواوی پشتگوێی SponsorBlock دەکات + ئەم تراکە هیچ ناسنامەیەکی ڤیدیۆی نییە بۆ ناردن + سەرچاوە: %s + دوگمەی کۆپیەکە بۆ ماوەیەک دابگرە بۆ پاشکەوتکردنی ئەم LRC؛ ئەگەر بتوانی کاتەکانی ڕێک بخەیت و لەگەڵ دابینکەرانی کۆمەڵگەکە بەشیکەیتەوە، سوپاسگوزارت دەبین + وەرگێڕدراوە لەلایەن Google + ڕۆمانایزکراوە لەلایەن Google + وەرگێڕدراوە لەلایەن %s + ڕۆمانای کردراوە لەلایەن %s + ڕۆمانیزەکراوە لەلایەن %s + وەرگێڕدراوە + زمانی وەرگێڕان + زمانی ڕۆمانیزەکردن + وەرگێڕان دەکرێت… + نەتوانرا وەرگێڕانی تێکستەکەی گۆرانییەکە بکرێت + ڕۆمانیزە خۆکار + ڕۆمانیزە دەکرێت… + نەتوانرا تێکستی گۆرانییەکە ڕۆمانیزە بکرێت + تەنها وەرگێڕدراو + تەنها ڕۆمانایز + مۆدی دوولایەنە + کۆپی کرا بۆ ڕوونووسگە + نوێکردنەوە + نوێکرا + کلیلى API QuickLRC + QuickLRC تێکستی گۆرانییەک دەنووسێتەوە یان کاتی ڕێکدەخات بۆ تراکەکەی ئێستا. خزمەتگارییەکی جیاوازە و پەیوەندی بە خاڵی کۆتایی OpenAI کە لەسەرەوە ئاماژەی پێدراوە نییە + کلیلەکەی QuickLRC پاشکەوت کرا. دوگمەی ڕێکخستن لەناو دایالۆگی گەڕانی تێکستەکان بەکاربهێنە، یان ئامۆژگاری پانێڵەکە کاتێک تراکێک تێکستی نییە + کلیلەکانی APIی QuickLRC پاشکەوت کرا + QuickLRC ئەو کلێلەی نەناساند + کلیلەکانی APIی QuickLRC پاککراەوە + QuickLRC کاتی ئەو تێکستانە ڕێکدەخات کە کاتیان نییە، یان لە تراکەکەوە دەینووسێتەوە، بە بەرامبەر بە مەبەلی کرێدیت. بە هەبوونی کلیلێک، ئەو تراکانەی تێکستیان نییە گۆڕینی بۆ نووسین پێشکەش دەکەن و دایالۆگی گەڕان دوگمەی ڕێکخستنی تێدا دەبێت + داواکردنی ڕێکخستنی QuickLRC + خەریکی ڕێکخستنە + جێبەجێکردنی ڕێکخستن + داواکردنی گۆڕینی دەنگ بۆ نووسین لە QuickLRC + خەریکی گۆڕینی دەنگە بۆ نووسین + گۆڕینی دەنگ بۆ نووسین هەڵوەشێندرایەوە + تەنها ئەو گۆرانیانەی لە YouTube لێدەدرێن دەکرێت وەربگێڕدرێن بۆ دەق + هیچ تێکستێک نییە بۆ ڕێکخستن - لەجیاتی ئەوە دەینووسێتەوە + QuickLRC نەیتوانی ئەم گۆرانییە پرۆسە بکات + کrédit-ەکانی QuickLRC بۆ ئەم گۆرانییە تەواو بوون + "تێکستەکان بەپێی ناونیشان و ئاهەنگساز دەگونجێن، زانیاریی گشتیی نائاسایی ڕەنگە هیچ شتێک نەدۆزێتەوە یان وەشانێکی هەڵ بدۆزێتەوە. نوێکردنەوە بەکاربهێنە بۆ گۆڕینی وەشانەکان. ئەو دابینکەرانەی کە بە * نیشانەکراون پێویستیان بە تۆکەن. هەموو دابینکەرە چالاککراوەکان هەمیشە بەپێی ڕیزبەندیی تۆ داوا ناکرێن، بەڵکو بەپێی خێرایی خۆیان وەڵام دەدەنەوە.\n\nتێکستەکان لە خزمەتگوزارییەکانی لایەنی سێیەم دێن کە پەیوەندییان بە Morphe نایە، سپۆنسەر نین یان پشتگیری ناکەن. هەر یەکێکیان بەو شێوەیەی هەین بەکاردێن: تێکستەکان، بەردەستبوونیان و مۆڵەتیان لای ئەو خزمەتگوزارییانە و مۆڵەتدەرەکانیان دەمێنێتەوە، و بەکارهێنانیان ڕەنگە مەرجەکانی خزمەتگوزارییەکانیان لێبکەوێت. ئەوەی دەگاتە خزمەتگوزارییەکی چالاککراو تەنها ئەو گەڕانەیە کە تۆ داوای کردووە - ناونیشانەکە، ئاهەنگسازەکە، و لەو شوێنەی خزمەتگوزارییەکە پێویستی پێیە، ناسنامەی ڤیدیۆکە (video ID). خاڵی کۆتایی AI ئارەزوومەندانە و گۆڕانی QuickLRC تەنها دوای ئەوەی ڕێکیان دەخەیت کار دەکەن، و تێکستی تێکستەکە یان ناونیشانی ڤیدیۆی تڕاکەکە وەردەگرن." + تەنها ئەو دابینکەرانە چالاک بکە کە بەکاریان دەهێنیت، یان هەر هەر ئێستا هەرکە بنەمکە - داواکارییەکانی بەردەوام دەتوانن فشار بخەنە سەر سایتە بچووکەکانی تێکستی گۆرانی + شاردنەوەی هێڵی ناسنامە + شاردنەوەی هێڵی ناسنامەی کە لە خوار تێکستی گۆرانییەکەدا پیشان دەدرێت + گۆڕینی شوێنی وەرگێڕان و ڕۆمانیزە + چالاک (ON) ڕۆمانیزە دەخاتە خوار تێکست و وەرگێڕان دەخاتە سەرەوە؛ ناچالاک (OFF) ڕۆمانیزە لە سەرەوە و وەرگێڕان لە خوارەوە دەهێڵێتەوە + وەرگێڕان + ڕۆمانایزەکردن + دابینکەرى وەرگێڕان + گوگڵ (پێشفرۆش، بێبەرامبەر) + گونجاو لەگەڵ OpenAI (پێشراو) + دابینکەری ڕۆمانیزەیشن + گوگڵ (پێشفرۆش) + فەرھەنگێی ناوخۆیی / ئۆفلاین + گونجاو لەگەڵ OpenAI (پێشراو) + بەشی ژیری دەستکرد AI + تەنها ئەو کاتە بەکاردێت کە دابینکەر هیچ نیشانەیەک بۆ بەشەکان ناردووبێت وەک Chorus یان Verse، کە دواتر مۆدێلی OpenAI-compatibleت دەیناسیناتەوە + AI و خاڵەکانی کۆڵاپ + ڕێکخستنی کلیلەکانی API، مۆدێلەکان، خاڵەکانی کۆڵاپ، و شابلۆنی پرۆمپت بۆ دابینکەرانی AI. + ناونیشانی URLی خاڵی کۆتایی API (بۆ نموونە https://api.openai.com/v1/chat/completions) + قاڵبی پرۆمپت + جێنشینەکان: {language}, {task}, {title}, {artist}, {count}, {lines} + پرۆمپتەکە پاشکەوت کرا + ئامۆژگارییەکە پاشکەوت کرا، بەڵام خاڵی کۆتایی AI هێشتا بەکارناهێنرێت. سەرەتا لە ڕێکخستنەکانی AI ڕێکی بخە + پرۆمپتەکە سفر کرا + JSONـی دروست نییە. چاکی بکە یان کێڵەکە خاڵی بکە پێش پاشکەوتکردن + ڕێکخستنەکانی وەرگێڕان & ڕۆمانیزە کردن پاشکەوتکران + ڕێکخستنەکان گەڕانەوە بۆ دۆخی سەرەتایی + ناونیشانی بنەڕەتی دەبێت بە http:// یان https:// دەست پێ بکات + تاقیکردنەوەی پەیوەندی + ڕێکخستنی خاڵەکانی کۆتایی + پیشاندانی هەموو خاڵەکانی کۆتایی + پشکنینی پەیوەندییەکە دەکرێت… + دابینکەرى وەرگێڕان + دابینکەری ڕۆمانایزەیشن + هێنانی مۆدێلەکان + هێنانى مۆدێلەکان... + مۆدێلێک هەڵبژێرە + کێشەیەک لە هێنانی مۆدێلەکاندا ڕوویدا. تکایە کلیلی API یان پەیوەندی ئینتەرنێتەکە بپشکنە. + ڕێکخستنەکانی Gemini API + گونجاوە لەگەڵ OpenAI (سندووقی کۆتایی تایبەت) + ڕێکخستنەکانی DeepL API + کلیلى API + مۆدێل + ناونیشانی بنەڕەتی (Base URL) + کلیلى API (ئارەزوومەندانە) + پڕۆمپتی وەرگێڕان + لەشی گەشتی زیادە (JSON ئارەزوومەندانە) + کۆدی سەلماندنی ناسنامە (بێبەرامبەر یان پرۆ) + ڕێکخستنی کۆدەکانی API، مۆدێلەکان، خاڵەکانی کۆتایی و ئاماژەکان + 8.23.51 - ڕاگرتنی ئایکۆنە ئەستوورەکان + 9.13.51 - گەڕاندنەوەی بەسەکانی Player Up next و Lyrics و Related diff --git a/patches/src/main/resources/addresources/values-ckb-rIR/reddit/strings.xml b/patches/src/main/resources/addresources/values-ckb-rIR/reddit/strings.xml index 3aff6d7e6d..d6eac570b6 100644 --- a/patches/src/main/resources/addresources/values-ckb-rIR/reddit/strings.xml +++ b/patches/src/main/resources/addresources/values-ckb-rIR/reddit/strings.xml @@ -16,6 +16,8 @@ Second \"item\" text" --> + سیاسەتی تایبەتیی ڕێدیت + چوونەژوورەوەی گووگڵ بەردەست نییە. ئیمەیڵ بەکاربهێنە بۆ چوونەژوورەوە ڕێکلام هێڵپەڕەکانی بڵاوکردنەوە قایپ بکه دابەزاندنی ڕاوەستێنانی کۆمێنتەکان @@ -31,8 +33,18 @@ Second \"item\" text" ڕەفەی دەربارە بشارەوە ڕەفەی سەرچاوەکان بشارەوە سازان + فۆنتی تایبەت + فایلی فۆنتێکی تایبەتی TTF یان OTF لە ڕێگەی ئەو ڕێگایەی خوارەوە لایبەرە. چالاککردنی ئەمەیەش فۆنتی سیستەم زۆر کردن (Force system font) ڕادەگرێت + فایلی فۆنتی تایبەت + کلیک بکە بۆ هەڵبژاردنی فایلی فۆنتێکی TTF یان OTF + فۆنتی هەڵبژێردراو - %s + سەرەتا فایلی فۆنتێکی تایبەت هەڵبژێرە + تکایە فایلی فۆنتێکی .ttf یان .otf هەڵبژێرە + کەوتن لە بارکردنی فۆنتی تایبەت. فایلێکی تر هەڵبژێرە خۆراکێکی ڕەخساوییەکە ڕاگەخشاوە خۆراکێکی ڕەخساوییەکە ڕاگەخشاوە دێت کاتێک تۆ دەگرێتە ڕەسی ڕەخساو + فۆنتی سیستەم زۆر بکە + فۆنتی سیستەمی ئامێرەکە بەکاربهێنە لەجیاتی Reddit Sans یان Roboto. چالاککردنی ئەمەیەش فۆنتی تایبەت ڕادەگرێت دۆزینەوەی پەیامگە دەربخۆ دوگمەی پرسیار (Ask) لە ناو ڕیزبەندی گەڕان دەشارێتەوە دیالۆگی پێشنیاری ئاگادارکردنەوە لابەرە diff --git a/patches/src/main/resources/addresources/values-ckb-rIR/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-ckb-rIR/shared-youtube/strings.xml index 917f5facd8..5f55bf602e 100644 --- a/patches/src/main/resources/addresources/values-ckb-rIR/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ckb-rIR/shared-youtube/strings.xml @@ -18,19 +18,51 @@ Second \"item\" text" --> + ئەنیمەیشنی دەستپێکردن لەکارخراو ڕەنگ منۆكرۆم + + کوژاوە دەستپێکردنەوە بۆ تەواوکردنی ئامادەکردن ناوی ئەپ + پاشبنەمای تێمای تاریک + ڕەشی بێگەرد + Material You (ڕەشی بێگەرد) + YouTubeی کلاسیک + پەمەیی تۆخ + شینی تۆخ + سەوزی تۆخ + زەردی تۆخ + پورتەقاڵی تۆخ + سووری تۆخ + پاشبنەمای تاریکی تایبەت + ڕەنگی پاشبنەمای تێمای تاریک دیاری بکە + پێشنیاری ئەپڵیکەیشن + Material You (بێلایەن) + Material You (سەرەکی) + Material You (لاوەکی) + Material You (سێیەم) باو + دابەزاندنەکان + شێوازی دابەزاندن بنەڕەت + دابەزاندن لە ناو ئەپەکەدا + دابەزاندەری دەرەکی Downloader package name + ناوی پاکێجی ئەپڵیکەیشنی دابەزاندنی دەرەکی کە دامەزراوە + ناوی پاکێج بنووسە دوان + ئەپڵیکەیشنەکە دامەزراونێت + %s دامەزراندراوە. تکایە دایبمەزرێنە + "نەتوانرا ئەپڵیکەیشنی دامەزراو بەم ناوە پاکێجییە بدۆزینەوە: %s + +دڵنیابە کە ناوی پاکێجییەکە ڕاستە و ئەپڵیکەیشنەکە دامەزراوە" + ناوی پاکێج ناتوانێت بەتاڵ بێت باو وێنۆکەی ئەپ @@ -39,19 +71,41 @@ Second \"item\" text" Morphe تاریک Morphe ڕۆشن + ڕووناکیی لێدانی Morphe + Morphe play ڕەش باو + "ئەمە وێنۆکەی پیشاندراو لە لانچەری ئەپەکەت دەگۆڕێت. + +ئەم ڕێکخستنە وێنۆکە لە هیچ کام لەم شوێنانەدا ناگۆڕێت: +• لیستی ئەپەکانی ڕێکخستنی سیستەمی ئامێرەکە +• هەندێک لە ئاگانامەکانی پۆش +• هەڵبژاردەری ئەپی 'کردنەوە بە' +• وێنۆکەی Android Auto + +بۆ گۆڕینی وێنۆکەکە لە هەموو شوێنێک، دووبارە پاتچ بکەرەوە و بژاردەی پاتچی 'App icon' بگۆڕە" وێنۆکەی ئاگانامە هاوتاکردنی وێنۆکەی ئەپ + Morphe play ڕاگەیاندنی Morphe + مێژووی بینینت پاشکەوت ناکرێت.<br><br>ئەمە ئەگەری زۆرە بەهۆی بلۆککەری ڕیکلامی DNS یان پرۆکسیکانی تۆڕ بێت.<br><br>بۆ چاککردنی ئەمە، <b>s.youtube.com</b> بخەرە لیستی ڕێگەپێدراوەکان یان هەموو بلۆککەرەکانی DNS و پرۆکسیکان ڕابکە نیشانی مەدەوە دووبارە گەڕانەوە سەر ڕاستەقینەی ڕاستگۆ شێوەپێدانی spoofing کلایەنت بۆ ڕێگرتن لە کێشەکانی پەخش کردنەوە + "کێشەکانی لێدانی ڤیدیۆ بۆ زۆربەی بەکارهێنەران چاک دەکات لە ڕێگەی داواکردنی ستریمێکی ڤیدیۆی گونجاو + +ئەگەر تۆ بەکارهێنەری YouTube Premium بیت، ڕەنگە ئەم ڕێکخستنە پێویست نەبێت" هیچ ڕاستگۆیەکی ئەندازی نەیادەخرێت + کلایەنت وەک خۆی کار ناکات + "%1$s چەندین جاربەجۆر بەردەوام کار نەکرد، بەڵام %2$s کاری کرد. + +ئایا %2$s بکرێت بە کلایەنت وەک خۆی؟" + گۆڕین + کۆتناهینانی ئەم ڕێکخستنە ڕەنگە ببێتە هۆی کێشە لە کاتی لێدانی ڤیدیۆکاندا دۆخراوه‌ت: ڕاستکردنه‌وه‌ی ڕێگه‌ی ڤیدیو > چوارده‌وه‌ی Android VR-ه‌ ده‌رباره‌ی نۆی‌ن @@ -76,12 +130,16 @@ Da te'îl bikin, li ser berdewam bikin xwarê" 3. بةبێ ئەگەر پێویست بوو، لە Google داخڵ بن. ئەگەر حساباته كۆمەڵةكانيان هەيە، حساباتی YouTube يان بيگرە 4. دابەزینە 'Allow YouTube VR' ڕازەكان 5. گەڕابە سەر ئەم بەرنامەیە دواتر كاتێك كە پێغەیە لەگەڵ 'Device connected' بەرنامەیە" + دەرکردنی کۆدی چالاککردن سەرکەوتوو نەبوو + دەرکردنی تۆکنی پشتڕاستکردنەوە سەرکەوتوو نەبوو + بە سەرکەوتوویی داخڵ بوویت "‌ڕێكخستن هه‌ڵگیراوه • ڕێكخستنه‌كانی Morphe-ت ناهێننه‌وه‌/نه‌هاته‌وه‌ Android VR • كاتێكی ٦ مانگ ڕابێت، گۆگڵ ئەم ڕێكخستنە ده‌سڕێت • كاتێك وشه‌ختكه‌وتۆت، پێویسته‌ جارێكی تری ده‌رببێ بۆچون" OAuth2 بەردەست نیە ( کاتەکەی بەسەرچووە API) + ماوەی داخڵبوونی Android VR بەسەرچوو، دووبارە هەوڵ بدە بۆ داخڵبوونەوە بە VR OAuth2 هۊتا سەرلەشêره (ئاستی %s) OAuth2 بەشێوەیەکی کاتی بەردەست نیە نەتوانم چواردوورە ببنێم، پشتەڕوایی ئەندازەی سایت پەسەند نەکرا @@ -89,9 +147,19 @@ Da te'îl bikin, li ser berdewam bikin xwarê" ڳ0ڱڰڱڱڰڱ إواندرواد قوميرواد VR نسيحو دواكني ذيسنه لمن روسينه ، دواكه دوروسينه به امنهسي + کۆنترۆڵەکانی میدیای ئاگانامە Customize the playback controls shown on your lock screen and notification panel + دوگمەکانی پێشوەختە & دواتر بشارەوە Hides Previous and Next buttons in media notification, lock screen, and headphone controls + بەکارنەهێنانی seekbar Prevents dragging the seekbar on the lock screen and notification panel + گەڕان بە دوگمەکانی پێشوو و داهاتوو & داهاتوو + گۆڕانی کۆنترۆڵەکانی ئاگادارکردنەوەی میدیا، شاشەی قفڵ، و 헤دفۆن بۆ گەڕان لەجیاتی گۆڕینی ڤیدیۆکان + درێژی گەڕانەوە + چەند چرکە گەڕانەوە بێت بە دوگمەی Previous + درێژی پێشوەختکردن + چەند چرکە پێشوەختکردن بێت بە دوگمەی Next + درێژی گەڕان دەبێت لە نێوان ١ و %s چرکە بێت. بەکارهێنانی پێشنیارکراو پڕۆتۆكۆلی QUIC ڕاكوچە بكە @@ -100,17 +168,43 @@ Da te'îl bikin, li ser berdewam bikin xwarê" به‌زانیشتنی زمانی دەنگی ئەسڵی هەمیشە تراکی دەنگی ڕەسەنی ڤیدیۆکە لێدەدات لەجیاتی وەشانی دۆبلاژکراو + پەڕاندنی بێدەنگی + بە شێوەیەکی ئۆتۆماتیکی وەستانە بێدەنگەکانی لە دەنگی ڤیدیۆدا دەپەڕێنێت + کەمترین درێژی وەستان + تەنها ئەو وەستانەی کە بە لانی کەم بەم درێژییەن کورت دەکرێنەوە. کاتێک ڤیدیۆی داهاتوو دەست پێ دەکات کاریگەر دەبێت + پڕۆکسیکردنی تۆڕ + ڕەوانەی داواکارییەکانی تۆڕی پشتگیرکراو دەکات لە ڕێگەی پڕۆکسییەکەوە + چالاککردنی پڕۆکسی + ڕەوانەی داواکارییەکانی تۆڕی پشتگیرکراو دەکات لە ڕێگەی پڕۆکسی HTTP یان HTTPS ی ڕێکخراوەوە + میزبان پڕۆکسی + ناوی میزبان یان ناونیشانی IPی سێرڤەری پڕۆکسی + پۆرتی پڕۆکسی + پۆرتی سێرڤەری پڕۆکسی + بەکارهێنانی پڕۆکسی HTTPS + بەکارهێنانی HTTPS بۆ پەیوەستبوون بە سێرڤەری پڕۆکسی لە جیاتی HTTP + ڕەسەنایەتی پرۆکسی + پڕۆکسى بە auth-ی بنەڕەتی دەنێرێت + ناوی بەکارهێنەری پڕۆکسى + ناوی بەکارهێنەر بۆ auth-ی بنەڕەتی پڕۆکسى + وشەی نهێنی پڕۆکسى + وشەی نهێنی بۆ auth-ی بنەڕەتی پڕۆکسى + ڕێگەی گەڕانەوەی ڕاستەوخۆ بدە + بە ڕاستەوخۆ پەیوەندی بکە ئەگەر پڕۆکسى شکست بهێنێت شاردنەوەی ڕیکلامەکانی پڕ شاشە + بۆ بەکارهێنانی ئەم تایبەتمەندییە، \'Spoof video streams\' بگۆڕە بۆ هەر کلاینتێک جگە لە \'Android Studio\' و \'Android VR Downgraded\' خەڵەتەکانی هەڵواسانی دەړووستن پارامیتەرەکانی بەدواکەوتنی پرس لە لینکە هاوبەشەکان لادەبات گۆڕینەوەی سیستەمەکانی هەڵواسە بۆ youtube.com + music.youtube.com دەگۆڕێت بۆ youtube.com لە لینکە هاوبەشکراوەکاندا گۆڕینی لینکەکانی هاوبەشکردن بۆ youtu.be + ڤیدیۆکان ناچار دەکات بۆ بەکارهێنانی فۆرماتی کورتکراوەی youtu.be URL + پڕۆتۆکۆلی بافەری دێباگی لۆگ Adds additional logging information for developers to identify app components "چالاک کردن ئەم ڕێنماییە زانیاریی چۆر لەسەر دابخستن تۆمار دەکات، دەوێتە سەرەخۆش کردنی دەستکارییەک و نێو نووسراوەکانی بۆ چەندین پێکهاتەی کاربەرەوە. @@ -118,29 +212,104 @@ Da te'îl bikin, li ser berdewam bikin xwarê" بەڵام، چالاک کردنەوە ئەمەش ڕەنگە هەندێك لە زانیارییەکانی کاربەر تۆمار بکات وەک: ناوی IP-ت" + فلتەری تایبەت + فلتەرە تایبەتەکان ڕێکبخە بۆ شاردنەوەی هەر پێکهاتەیەک + چالاککردنی فلتەری تایبەت List of component path builder strings to filter, separated by new lines + فلتەری تایبەت نادروستە: %s + فلتەری لیستی ڕێکخستنەکان + بەناردانی بابەتەکان لە شاشەی ڕێکخستنە ستانداردی ئەپەکە بەپێی ناوی دیاریکراویان + یەکجار شاشەی ڕێکخستنە ستانداردی ئەپەکە بکەرەوە بۆ پڕکردنەوەی لیستەکە + لابردنی دیالۆگی ئاگادارکردنەوەی بینەر هقاسي نابي سفاح را مانعي تيستيًد نابي. قاس فاري قبلة ديرياحيدي + دابینکەری PoToken + شێوەپێدانی دابینکەری PoToken بۆ ڕێگرتن لە کێشەکانی پەخش کردنەوە + دابینکەری PoToken دەرەکی + "کێشەکانی لێدانی ڤیدیۆ چاک دەکات بە بەکارهێنانی PoToken minter derve (تاقیکاری) + +ئەگەر بەکارهێنەری YouTube Premium بیت، ڕەنگە ئەم ڕێکخستنە پێویست نەبێت" + "کێشەکانی لێدانی ڤیدیۆ چاکدەکات بە بەکارهێنانی PoToken minter دەرەکی (تاقیکاری) + +بۆ بەکارهێنانی ئەم تایبەتمەندییە، 'Spoof video streams' بکە بە خامۆش" + ئەگەر کارنەکرد، هەوڵ بدە MicroG بە زۆر دابخیت + کردنەوەی ڕێکخستنەکانی PotHelper + "ئەم تایبەتمەندییە لە ئێستادا لە قۆناغی پەرەپێدان دایە و ڕەنگە کارنەکات + +لێرە کلیک بکە بۆ دابەزاندنی PotHelper" + PotHelper ئیتر کار ناکات + "تۆکنەکەی ناو ئەم وەشانەی PotHelper ئیتر کار ناکات + +بۆ چاککردنی لایڤی، 'Spoof video streams' چالاک بکە" + چالاککردن + شێوەگۆڕینی وەشانى ئەپڵیکەیشن Tricks the server into thinking you are using an older app version to restore legacy UI features + "وەشانی ئەپەکە دەگۆڕدرێت بۆ وەشانێکی کۆنتر. + +ئەمە شێوە و تایبەتمەندییەکانی ئەپەکە دەگۆڕێت، بەڵام ڕەنگە کاریگەری لاوەکی نادیار ڕووبدات. + +ئەگەر دواتر کوژایەوە، پێشنیار دەکرێت داتاکانی ئەپەکە پاک بکرێنەوە بۆ ڕێگری لە کێشەکانی UI" + ئامانجی شێوەگۆڕینی وەشانى ئەپڵیکەیشن + ڕێژەی نوێبوونەوەی ئەپ بنەڕەت + %s FPS + گۆڕینی ڕێژەی نوێبوونەوە + هەمیشە + لێدان لە شێوەی ستوونی + لێدان لە شێوەی شاشەی تەواو + لەکاتی لێدانی ستایلی ستوونی و تەواوی شاشە + فۆنت بنەڕەت + فۆنتی سیستەمی ئامێر + فایلی فۆنتی تایبەت + فایلی فۆنتی تایبەت + فایلێکی فۆنتی TTF یان OTF هەڵبژێرە + ئەو فایلەی هەڵبژێردراوە فۆنتی TTF یان OTF نییە + نەتوانرا فۆنتە تایبەتەکە بار بکرێت و ڕایگیراوە + دیسلایکەکان بۆ ماوەیەکی کاتی بەردەست نین (کاتی API بەسەرچووە) + نارەزایەتییەکان بەردەست نین (ئاستی %s) + دیسلایکەکان بەردەست نین (سەروو سنووری client API) + دیسلایکەکان بەردەست نین (%s) + لەلایەن خاوەنەکەوە شاردراوەتەوە + نەگۆڕانەی ئەم ڤیدیۆیە، لەگەڵ %s کەسی تر + نەگۆڕانەی ئەم ڤیدیۆیە، %s لە دەنگەکان نەگۆڕانەن Enable Return YouTube Dislike Show dislikes as percentage Shows dislikes as a percentage of likes instead of a number + پیشاندانی لایکی خەباندراو Shows an estimated like count on videos where the creator has disabled likes نشان دادن ڕێنمایەکانی هەڵە یەکباری پەیوەندیمان Shows a notification when the Return YouTube Dislike server is unavailable + "Return YouTube Dislike ئەو ژمارەی ناخۆشانە دەگەڕێنێتەوە کە YouTube لە ساڵی ٢٠٢١دا شاردبوونی. ژمارەکان لە داتاکانی ئەرشیفکراو پێش لادانەکە دێن و بۆ ڤیدیۆ نوێکترەکان لە ڕێگەی دەنگدانی بەکارهێنەرەکانی خۆیەوە خەمڵاندراون، بۆیە نزیکەکەن و هەموو چەند ڕۆژێک نوێ دەبنەوە + +ناسنامەی ئەو ڤیدیۆیەی سەیر دەیکەیت بۆ APIی Return YouTube Dislike دەنێردرێت، وە لایکی و ناخۆشانی تۆ بە شێوەی بەناونیشان دەنێردرێن بۆ باشترکردنی خەمڵاندنەکان" + ئامارەکانی APIی Return YouTube Dislike بۆ ئەم ئامێرە + کاتی وەڵامی API، تێکڕاو + کاتی وەڵامی API، کەمترین + کاتی وەڵامی API، زۆرترین + کاتی وەڵامی API، دوایین میدیا + دیسلایکەکان بۆ ماوەیەکی کاتی بەردەست نین - سنووری ڕێگەی APIی کلاینت جێبەجێ کراوە + هێنانی دەنگەکانی API، ژمارەی بانگەوازەکان + هیچ پەیوەندییەکی تۆڕ نەکراوە + %s پەیوەندی تۆڕ کراوە + API fetch votes، ژمارەی کاتەکانی کۆتاییهاتن + هیچ پەیوەندییەکی تۆڕ بەهۆی کات کۆتایی نەهاتووە + %s پەیوەندی تۆڕ کاتیان بەسەرچووە + سەنگینەکانى خێرایی کلایانتى API + هیچ سەنگینەیەکی خێرایی کلایانت ڕووبەڕوو نەبووە + سەنگینەی داواکارییەکانی کلاینت %s جار ڕوویداوە + %s میلی چرکە diff --git a/patches/src/main/resources/addresources/values-ckb-rIR/shared/strings.xml b/patches/src/main/resources/addresources/values-ckb-rIR/shared/strings.xml index d0f3729ef9..f4aff44609 100644 --- a/patches/src/main/resources/addresources/values-ckb-rIR/shared/strings.xml +++ b/patches/src/main/resources/addresources/values-ckb-rIR/shared/strings.xml @@ -24,18 +24,22 @@ Second \"item\" text" ڕه‌نگ سازبکه‌ ڕه‌نگ نامه‌رێت ئه‌گه‌رى دووباره‌کردنه‌وه‌ هه‌یه‌ + بۆ ئەوەی ئەم گۆڕانکارییە کار بکات، ئەپڵیکەیشنەکە دووبارە پێداگێڕانەوە بکەرەوە دووباره‌سه‌ره‌کهێنه‌ واردکه‌ نوسه‌ سازکه‌يه‌كانی Morphe گه‌رى سه‌ره‌وه ببه‌سته + ڕێکخستنەکانی %s هاوردە کران به‌خۆشه‌وه‌ `)` نه‌که‌ویته‌وه: %s + گەڕانەوە بۆ سەرەوە ئه‌لیکه‌ره‌خانه‌يه‌کى ڕاگرتنه‌کان دربارە به‌هۆهی هیچ نه‌ماوه‌ بۆ `%s` به‌ڵكوو وتاره‌كه‌كه‌كێت تيايه‌ به‌ژياره‌كانه‌ كه‌ ئه‌ى به‌زه‌سرایی مهسةله‌يه‌كه‌ ببه‌ستيه‌ به‌ده‌سته؟ + لابردن لە مێژووی گەڕان زمووكی به‌ژياره‌كه‌كه‌يه‌كه‌ سه‌ره‌ده‌كامه‌ ئایا خؤت به‌خۆشه‌وه‌ده‌يكه‌ى زه‌مه‌نوه‌كانه‌ به‌ده‌سته؟ رزگارێ لێکدان @@ -44,54 +48,148 @@ Second \"item\" text" • هەڵکەوتینێ دەربارەوێ بپارێزە بابەتەکە بۆ مێشەڕە • لێکۆڵینەوێک نەخوێندراوێتە سەرەتایەک و نیشانەیەک • دەستکارییەکانی باوک دەلێن سەردا دەستکارییەکانی منداڵێ نەبێت" + مێژووی گەڕان بەتاڵە دقققبردانه ريستوريان دموسهيةريانه، دقققبردقاريان بررسوريانه إنترار إنترارقه دۆزینەوەی مێشۆکەکان، تێکستی ڕاگەڕەیی دەربارەی مێشۆکانی دەستکارییەک. + ناوی ڕێکخستنەکانی Morphe بنەڕەت + زیادەکان + گۆڕانکارییەکان + کاتی گۆڕان باو + ناوێک بنووسە + ناو ناتوانرێت بەتاڵ بێت نیشانەکانی ڕێکدالەکانی مورفە بنێرن + کۆپی کرا: %s زمانی مورفە "فريتة‌ي بة‌شي‌كه‌ي زوانه‌كانيش نه‌بوون، يان بة‌تەواوه‌ نه‌كه‌ون.\n\nبۆ وه‌رگێڕانێكی زوانه‌ تازه‌كان ياخود باشكردنێكی وه‌رگێڕانه‌كانی ئێستا، سه‌رداني morphe.software/translate بكه‌" زمانی ئەپ هێنانی / ئەگەڕانەوە هێنانی / دەربڕینی ڕێکخستنەکانی Morphe + هاوردەکردن لە فایل + ڕێکخستنەکان بە سەرکەوتوویی هاوردەکران، کلیک لەسەر Save بکە بۆ جێبەجێکردن نەگەشت بە هاتنەوەی ڕێکخستنەکان + هەناردەکردن بۆ فایل ئەم فێڵانەی دەستخاتن سەرکەوتێت \"نەگەشت بە دەربازبوونی ڕێکخستنەکان\" طلوا جهأست إرا ءا ءعلوا ءأاءة الرسءات <i>%s</i> + تۆ وەشان <i>%1$s</i>ی Morphe Patches بەکاردەهێنیت<br><br><b>نوێکردنەوە بەردەستە: <i>%2$s</i></b><br><br>بۆ نوێکردنەوەی پاتچەکانت، Morphe بەکاربهێنە بۆ پاتچکردنەوەی ئەم ئەپڵیکەیشنە نوسین صورو اين عميادي هستني استماران إسباقريانها إسباقالدها سەرچاوەکانی ڕەسمی + وێب سایت خۆڵبەخش + وەرگێڕانەکان کۆددان + نوێترین وەشانە + نوێکردنەوە بەردەستە + گەشەپێدانی ئێستا + گەشەپێدانی پێشوو + گەشەپێدانی چالاک + 2022 ‑ 2025 + 2022 ‑ 2025 + 2019 ‑ 2022 دەربارەی نوسینەکانی لایسەنسی بارکردن… سەرپێچوان + ڕێکخستنی بژاردەکانی پێشکەوتووی گەشەپێدەر و تۆمارەکانی دەستنیشانکردن + چالاککردنی تۆمارکردنی دیباگ + شۆردنی شوێنی هەڵەی لۆگی دیباگ + شووێنەکان (stack traces) لە تۆمارەکانی دیباگدا دەخاتە ناوەوە بۆ یارمەتیدانی دەستنیشانکردنی هەڵەکان نیشاندانی ڕێوشوێنی Morphe-ی نیشاده "کوژاندنەوەی toastsـی هەڵە هەموو ئاگادارکردنەوەکانی هەڵەی Morphe دەشارێتەوە. لە هیچ ڕووداوێکی چاوەڕواننەکراو ئاگادار ناکرێیتەوە" + هەناردەکردنی لۆگەکانی دیباگ + هەناردەکردنی لۆگەکانی دیباگی Morphe + لۆگەکانی دیباگ بە سەرکەوتوویی هەناردەکران + نەگەشت بە هەناردەکردنی لۆگەکانی دیباگ + گەڕان لە تۆمارەکانی دیباگ... + پاککردنەوە تۆمارکردنی چاککردنەوە لەکارخراوە + هیچ لۆگێکی دیباگ نەدۆزرایەوە + لۆگەکانی دیباگ کۆپی کران + هەناردەکردنی تۆمارەکانی دیباگی سەرکەوتوو نەبوو: %s + تۆمارەکانی دیباگ پاککرانەوە پارێژكاري فڵاگەل و مۆر بەڕێوەبردنی ئاڵای تایبەتمەندی بوولی + گەڕان لە نیشانەکانی تایبەتمەندی... + چالاک نییە (%s) + چالاکە (%s) + بلۆک کراوە (%s) + سەپاندراوە (%s) خۆرا کوژاوە هەڵکراو + سەپاندنی چالاککردن + بلۆککردن سڕینەوە + فڵاگەڵەکانی تایبەتمەندی پاشکەوتکران + فڵاگەڵەکانی تایبەتمەندی ڕیسێتکران + فڵاگەڵەکانی تایبەتمەندی کۆپی کران بۆ ڕوونووسگە + بۆ بەکارهێنانی ئەم ڕێکخستنە، ئەپەکە دووبارە پێداگێڕانەوە بکەرەوە + هەناردەکردن و وەردگرتن + وەردگرتنی %s نیشانەی تایبەتمەندی + هیچ فلیگی تایبەتمەندییەک نەدۆزرایەوە + فلیگەکانی تایبەتمەندی بە سەرکەوتوویی هەناردەکران + هەناردەکردنی فلیگەکانی تایبەتمەندی سەرکەوتوو نەبوو + فلیگەکانی تایبەتمەندی بارکراون، بۆ جێبەجێکردنی کلیک لەسەر پاشکەوتکردن بکە + خوێندنەوەی فایلەکە سەرکەوتوو نەبوو + گەڕانی باینەری + "دەدۆزێتەوە کام نیشانەی تایبەتمەندی دەبێتە هۆی ڕەفتارێکی ئەپەکە + +نیوەی %s نیشانەکە بلۆک کراون، و دوای هەر ڕێستارتێک دەڵێیت ئایا ڕەفتارەکە هێشتا بوونی هەیە + +پێش دەستپێکردن جارێک ڕەفتارەکە دووبارە بکەرەوە، ئەگەر نا ئەو نیشانانەی کە ئەپەکە هێشتا داوای نەکردووە لە گەڕانەکەدا نابین" دەستپێکردن + "هیچ feature flagـێک نییە بۆ گەڕان + +تەنها ئەو فلاگانەی کە ئەپەکە بەکاری هێناون و لە ئێستادا بە زۆر چالاک نەکراون دەتوانرێت لە گەڕانی باینەریدا بەکاربهێنرێن" + "هەنگاوی %1$s + +%2$s فلاگ ماون، %3$sـیان بۆ ئەم هەنگاوە بلۆک کراون + +ئەپەکە ڕێستارت بکەرەوە، بپشکنە ئایا ڕەفتارەکە هەیە، پاشان لە خوارەوە وەڵام بدەرەوە" + گەڕانی فلایگی feature لە دەرەوشەخاندایە + ڕەفتارەکە بوونی هەیە + ڕەفتارەکە بوونی نییە + پاشگەزبوون لە گەڕانی باینەری + گەڕانی باینەری هەڵوەشێندرا + "فلایگی ڕەوشمی %s دەبێتە هۆی ئەم ڕەفتارە. + +ئێستا بلۆک کراوە" + "توانرا هیچ فلایگی ڕەوشمێک نەدۆزرێتەوە. + +وادیارە ڕەفتاری ئەپڵیکەیشنەکە لەلایەن فلایگی ڕەوشمێکەوە کۆنترۆڵ ناکرێت" + "توانرا تەنها یەک فلایگی ڕەوشم نەدۆزرێتەوە. + +ڕەفتارەکە لە سەرەتای گەڕانەکەدا ون بووە، بۆیە ڕەنگە زیاتر لە یەک فلایگی تێپەڕبووبێت یان یەکێک لە وەڵامەکان هەڵە بووبێت" + کڕینی ڕێکخستنەکانی MicroG MicroG نەستاوە. ئەوەی بێخە پێویستییه کار + "MicroG v%1s کۆنە و ڕەنگە ببێتە هۆی کێشە. + +تکایە MicroG نوێ بکەرەوە بۆ نوێترین وەشانی v%2s" + نوێکردنەوە + پشتگوێخستن + "ئەو MicroGـەی دامەزراوە لەلایەن Morpheـەوە پێشنیار نەکراوە: + +%1s + +کلیک بکە بۆ سڕینەوەی، پاشان ئەم ئەپڵیکەیشنە دووبارە بکەرەوە و MicroG RE دامەزرێنە" + سڕینەوە "MicroG ڕێگە بەوە نادات لە پشتەوە کار بکات. @@ -105,6 +203,7 @@ Second \"item\" text" پەنجە بنێ بە دوگمەی بەردەوام و ڕێگە بە گۆڕانکارییەکانی باشترکردن بدە" بەردەوامبوون + پیشاندانی دیالۆگی ئۆپتیمایزەکردنی باتری MicroG "لەکارخستنی ئەمە دەبێتە هۆی شاردنەوەی پرۆمپتی باشکردنی پاتری MicroG. ئەگەر باشکردنی پاتری نەگۆڕدرا ئەوا لەوانەیە تووشی ئەمانە بیت: @@ -112,12 +211,24 @@ Second \"item\" text" • لەوانەیە ئاگادارکردنەوەکان کار نەکەن • ڕەنگە پەخشکردنی پاشبنەما کار نەکات" + بەڕێوەبردنی لینکی ئەپ + ڕێکخستنی لینکەکان بۆ ئەوەی لەم ئەپڵیکەیشنەدا بکرێنەوە + قۆناغی ١: ڕاگرتنی لینکە ڕەسەنەکانی ئەپ + ڕێکخستنەکانی ئەپی ڕەسەن بکەرەوە و هەموو لینکەکانی دۆمەینەکە ڕابگرە + قۆناغی ٢: چالاککردنی لینکەکانی ئەپی دەستکاریکراو + ڕێکخستنەکانی ئەپڵیکەیشنی چاککراوەکە بکەرەوە و هەموو لینکەکانی دۆمەین چالاک بکە + کردنەوەی ڕێکخستنەکان + 🚨 ئاگاداری تاقیکاری 🚨 "ئةوه ئەپى وერსۆنى تاقیكەرەوەى ⚠️ <b>%1$s</b> دةربخۆیت. <br/><br/> 🔧 ڕاهێنانی سەرنجدان: ئەپى بەرهەڵەكان یان خۆتێکزارى نادیار دەربخۆیت چونكە خەڵاتەكان دابةزین بۆ ئەم وერსۆنەى ئەپە. <br/><br/> ئەگەر تؤیترین توشی هەر كێشەیەك نةبوونى، ئەوا <b>راكړه‌تەر</b> و بؤ وერსۆنى مسه‌تەبی <b>%2$s</b>✅ په‌چۆكه" + ⚠️ تاقیکاری دەمەوێت + ✅ جێگیر دەمەوێت + لۆگی هەرستە بڕاوە بۆ بەڵگەنامەکانی بەکارهێنەر: %s + لۆگی هەرستە بڕاوە: %s diff --git a/patches/src/main/resources/addresources/values-ckb-rIR/youtube/strings.xml b/patches/src/main/resources/addresources/values-ckb-rIR/youtube/strings.xml index 376d0857eb..71e44045f1 100644 --- a/patches/src/main/resources/addresources/values-ckb-rIR/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ckb-rIR/youtube/strings.xml @@ -28,121 +28,375 @@ Second \"item\" text" کۆنتڕۆڵەکانی لێڕاندن تشتەکان ڤیدێۆ + گەڕاندنەوەی شاشەی ڕێکخستنە کۆنەکانی YouTube + لابردنی سنووردارکردنی لێدانی پاشخان + ناچالاککردنی لێدانی پاشخان + بە شێوەیەکی ئۆتۆماتیکی ڕاوەستێ کاتێک ئامێرەکە قوفڵ دەبێت کوژاوە + هەمیشە ڕاوەستێ کاتێک قوفڵە + ڕاوەستێ مەگەر ئەگەر ئۆدیۆ بێسیم بەستراوەبێت + کۆتاییهێنان بە سنووری خێرایی سکرۆڵکردن + بێکارکردنی نوێکردنەوەی ڕیزبەند + رێگری دەکات لەوەی سێرڤەر شێوەزاری ئەپەکە نوێ بکاتەوە + "شێوەی ڕێخستنی ئەپەکە دەگەڕێتەوە بۆ ئەو شێوەیەی کە لەکاتی یەکەمین دامەزراندندا بەکارهاتبوو. + +لەوانەیە هەندێک لە شێوەی ڕێخستنەکانی لای سێرڤەر نەگەڕێنەوە. + +گۆڕانکارییەکان بریتین لە: +• پێکهاتەکانی مینیووەکەی دەرەکی پلەیەر (یان ڕێکخستنە پەیوەندیدارەکان) لەوانەیە کارنەکەن +• ژمارە گۆڕاوەکان ئەنیمەیشن نین +• تاپەکەی کتێبخانە پیشان دەدرێت +• بەشی مۆسیقای وەسفی ڤیدیۆ لەوانەیە کارنەکات +• بڕگەکانی مینیووەکەی دەرەکی پلەیەر ناتوانرێت بشاردرێنەوە +• دوگمەی گۆڕینی ئەکاونت لەوانەیە لە تاپەکەی کتێبخانەدا دەرنەکەوێت" فلتەری مێنۆی فڕینی پێشهات چالاک بکە فلتەری مێنۆی فلای ئاوتی پێشهات + ناوەکانی مێنیوی flyout بنووسە بۆ فلتەرکردن، بە دابەشکردنیان بە دێڕی نوێ + چالاککردنی فلتەری مێنیۆی خوارەوەی کتێبخانە / تۆ + فلتەری مێنیۆی خوارەوەی کتێبخانە / تۆ + ناوەکانی مێنیۆی خوارەوەی کتێبخانە / تۆ بنووسە بۆ فلتەرکردنی، بە دێڕی نوێ جیاکەرەوە كارتەکانی ئەلبەم قایپ بكة + کارتەکانی ئەلبووم لە ژێر وەسفی هونەرمەندەکە دەشارێتەوە شاردنەوەی کارته‌کانی هونه‌رمه‌ند + کارتی هونەرمەندەکە لە سەرەوەی ئەنجامەکانی گەڕان دەشارێتەوە + شاردنەوەی دوگمەکانی کەناڵ + دوگمەکانی وەک Subscribe، View Channel، Community، و YouTube Music دەشارێتەوە لە پرۆفایلی کەناڵەکان لە ئەنجامەکانی گەڕاندا به‌شارووشتنی بابەتەکانی كۆمەڵگە إقاده إلسأرننان + بانەرە کۆمپاکتەکان و بۆشایی نێوان هەندێک پێکهاتە دەشارێتەوە + کارتە فراوانبووەکان لە ژێر ڤیدیۆکاندا + نیشاندانی هەمووی + تەنها کارتی پوختە بشارەوە Hide all + دوگمەی مایکرۆفۆنی گشتگیر بشارەوە + دوگمەی مایکرۆفۆنی هەوار لە گۆشەی خوارەوەی لای ڕاستی شاشەی پێشنیازەکانی گەڕان دەشارێتەوە + دوگمەی \'وەرگرتنی پرێمیۆم\' بشارەوە + ڕەفەی مێژوو بشارەوە + ڕەفە ئاسۆییەکان بشارەوە + ڕەفە ئاسۆییەکان دەشارێتەوە وەک هەواڵە بەپەلەکان، پۆستەکانی کۆمەڵگەی کە ڕەنگە حەزت لێبێت، بەردەوامبە لە بینین، زیاتر گەڕان لە چەناڵەکاندا، زیاتر گەڕان لە بابەتەکاندا، بۆ تۆ، نوێترین ڤیدیۆکان، پەیوەندیدارترینەکان، پۆستەکانی کۆمەڵگەی دروستکەر، شۆپینگ، و دووبارە بینینەوە + ڕەفەی وێنە بشارەوە + پەلکەکانی وێنە لە ئەنجامەکانی گەڕاندا دەشارێتەوە + شاردنەوەی کارتی \"داوێتی کەسانی تر بۆ نامە بنێریت\" + کارت بانگهێشتەکە دەشاردەوە کە لە بەشی نامەکانی تابی ئاگادارکەرییەکاندا پیشان دەدرێت دوگمەی \'نەویدەیترین ڤیدیۆکان\' بشارەوە + بەنەری شینی \'دوایین ڤیدیۆکان\' دەشارێتەوە کە لە سەرووی فیدەکەی سەرەکی دەردەکەوێت + شاردنەوەی ستریمەکانی ڕاشکاو + ئەو ڤیدیۆیانە دەشارێتەوە کە ئێستا ڕاشکاون لە فییدی سەرەکی و ئەنجامەکانی گەڕان + لیستى گۆرانییە تێکەڵەکان بشارەوە إوحي بورغه رو فيلي + شاردنەوەی سەردێڕی مینیو ئاگادارکردنەوەکان + سەردێڕی مینیو ئاگادارکردنەوەکان دەشارێتەوە کاتێک کلیک لە دوگمەی زەنگەکە دەکرێت پشکنەی \'ئاگادارکردنەوە\' بێکە + دوگمەی یاداگری بۆ پەخشانە ڕاستەوخۆکانی خشتەکراو و پێشاندرانی یەکەمجار دەشارێتەوە خاوێن کردنەوەی \'Playables\' + ئەو تایبەتمەندییە دەشارێتەوە کە ڕێگەت پێ دەدات یاری بکەیت + وێنەی پێشوەختای وشەی گەڕان بشارەوە + وێنەی پێشوەختەی بچووک لە لیستی تەواوکردنی خۆکارى گەڕان دەشارێتەوە خاوێن کردنەوەی پشکنەی \'زیادکردنەوە\' + دوگمەی فراوانکردن دەشاردەوە لە ژێر ڤیدیۆکان لە ئەنجامەکانی گەڕاندا شانڵی سەرقاڵکراوەکان بشارەوە + ناوی کەناڵە سەبسکرایبکراوەکان بشارەوە + ناوی کەناڵەکان دەشارێتەوە لە شریتی کەناڵە سەبسکرایبکراوەکاندا خاوێن کردنەوەی ڕێنمایەکان + ڕاپرسی و ئاگادارکردنەوەکان دەربارەی وردی پێشنیارەکانی فید دەشارێتەوە خاوێن کردنەوەی پەلیک دەبێت + + شاردنەوەی بانەری \'ڤیدیۆ بەردەستەکان شاراوەن\' + بانەری \'ڤیدیۆ بەردەستەکان شاراوەن\' لە لیستەکانی ڤیدیۆدا دەشارێتەوە خه‌ڵه‌ت‌نووس قایم بکه + "سەنگینەکان: +• ڕەبکەکانی Shorts، لاپەڕەکانی کەناڵ و ئەنجامەکانی گەڕان لەوانەیە هێشتا کاتی ئەپڵۆدکردن پیشان بدەن +• لەگەڵ دیزاینی ئۆتۆمۆبیل کار ناکات" + شاردنەوەی وێنەی پێشوەختای ڤیدیۆ + شاردنەوەی لیبێڵەکانی ڤیدیۆ + لیبێڵەکانی وەک \'Auto-dubbed\' / \'Dubbed\'، \'Earns commission\'، \'Hyped\'، \'People also watched\'، \'product(s)\' و \'You might also like\' دەشارێتەوە ژمێرەی بینه‌ر قایم بکه + "سەنگینەکان: +• ڕەبکەکانی Shorts، لاپەڕەکانی کەناڵ و ئەنجامەکانی گەڕان لەوانەیە هێشتا ژمارەی بینەران پیشان بدەن +• لەگەڵ دیزاینی ئۆتۆمۆبیل کار ناکات" ئەنجامی گەڕانی وێب بشارەوە + بەشی \'ئەنجامی وێب\' لە ئەنجامەکانی گەڕان دەشارێتەوە بەدری بخە بەشی \'دەکرێت بە دڵت بێت\' خاوێن کردنەوەی \'Doodles\' ی یوتوب + ئەنیمەیشنی وەرزەکان و ڕووداوەکان دەشاردەوە لەسەر لۆگۆی YouTube + پیشاندانی YouTube Doodles ڕەنگە ڕێگری بکات لە پیشاندانی سەرپێیە تایبەتەکان خاوێن کردنەوەی بەری ڕاهێنانی کەناڵ خاوێن کردنەوەی ئاوەزی کەناڵ + بشارەرەوە بەشەکان و دوگمەی \'In this video\' Qutîya hevkariyê veşêre خاوێن کردنەوەی قەزای خەیاڵی خاوێن کردنەوەی دەرکەوتەکانی زانیاری + پانێڵەکانی زانیاری دەشاردەوە، وەک زانیارییە پزیشکییەکان خاوێن کردنەوەی پشکنەی ئەندامبون دوگم کردن دوگمەی \"دووبارە بینەوەی چاتە زیندییەکە\" + دوگمەی دووبارە لایڤ چاتەکە دەشاردەوە لە شێوەی شاشەی تەواو خاوێن کردنەوەی دەرکەوتەکانی پزیشکی + شاردنەوەی ئاماژەکانی جوڵەی پلەیەر + ئاماژەکان دەشاردەوە کە لە کاتی جوڵەی پلەیەردا پیشان دەدرێن، وەک \'بکێشه سەرەوە بۆ گەڕانێکی وردتر\'، \'بەرهەڵە بکە بۆ هەڵوەشاندنەوە\' و \'2x\' + شاردنەوەی سنەکبار خاوێن کردنەوەی ڕێنمایەکانی ئەندامەکانی + بشارەوەی دوگمەی \'هماهەنگکردن لەگەڵ ڤیدیۆ\' + شاردنەوەی گ ڕادێینتی دەستلێدان و ڕاگرتن + گ ڕادێینتی کۆنتڕۆڵەکانی پلەیەر دەشارێتەوە کاتێک پەنجەت دەسڕیت لە کاتی ڕاگرتندا بۆ خێراکردن واپاسدان بۆ بەرەبەرەکانی کات‌بڕكراو + ئەو تایبەتمەندییە دەشارێتەوە کە ڕێگە بە بەکارهێنەران دەدات بە ئیمۆجی کاردانەوە نیشان بدەن لە بەشی کۆمێنت و چاتی ڕاستەوخۆدا Hide \'Help & feedback\' menu + مییۆی \'یارمەتی وamp; فیدباک\' دەشارێتەوە کاتێک دوگمەی سێ خاڵەکە بەکاردێت، وەک لە ئەنجامەکانی گەڕاندا + هەندڵەکە بشارەوە + ناونیشانی کەناڵەکەت لە بەشی \'تۆ\' دەشارێتەوە سەرنیشانی ڤیدۆ دەبێتە قایم + ناونیشانی ڤیدیۆکە دەشارێتەوە لە شێوازی شاشەی تەواودا + پاککردنەوەی ژێرنووسی ڤیدیۆ + پاککردنەوەی دەستەواژەکانی وەک \'#\'، \'Fundraiser\'، \'product(s)\' و \'Shop\' لە ژێرنووسی ڤیدیۆ + شاردنەوەی دوگمەی \'Ask Gemini\' + دوگمەی \'Ask Gemini\' دەشارێتەوە لە خوارەوەی پانێڵی \'Ask about this video\' پێشكەشەى وتە + دیاری بکە کام پێکهاتەکان لە وەسفی ڤیدیۆکەدا دەردەکەون + شاری بەشی \'کورتەی ڤیدیۆی دروستکراوی AI\' + شاری بەشی Ask + بەشی تایبەتمەندییەکان بشارەوە + شاردنەوەی بەشەکانی شوێنە دیارەکان، یارییەکان، موزیک، و بەشەکانی کەسانی ئاماژەپێکراو + بەشی لینکەکانی کەناڵ بشارەوە + بەشی بەشەکان بشارەوە + بەشی ڕاستکردنەکان بشارەوە + بەشی \'پێشکەوتنی خول\' بشارەوە + بەشی گەڕان بشارەوە + بەشەکانی \"ئەم کۆرسە بدۆزەرەوە\" و \"پۆدکاستەکە بدۆزەرەوە\" دەشارێتەوە + شاری بەشی \'ئەم کۆرسە بدۆزەرەوە\' + شاری بەشی \'پۆدکاستەکە بدۆزەرەوە\' + شاردنەوەی بەشی کەناڵە دیارکراوەکان + شاری بەشی لینکە دیارکراوەکان + شاری کردنی بەشی لیستەکانی ڤیدیۆی دیاریکراو + شاری بەشی ڤیدیۆ دیارکراوەکان + بەشی هەشتەگ بشارەوە + بەدری بخە بەشی \'چۆنیەتی دروستکردنی ئەمە\' خۆپاراستن بۆ خاڵە نیكەكان + شاری بەشی کارتەکانی زانیاری + شاری بەشی \'چەمکە سەرەکییەکان\' + بەشی تاقیکردنەوەکان بشارە + شاری کردنی بەشی \'گەڕان لە ناو ئەم ڤیدیۆیەدا\' شاردنەوەی دوگمەی سەبسکرایب + بەشی دەقە نووسراوەکان بشارە + بەشی \'زانیارییەکانی ڤیدیۆ\' بشارە بارەكانى فیلترین + دیاری بکە لەکوێ شریتی فلتەری سکرۆڵکەر نیشان نەدرێت Hide in channel page + لە کۆمێنتەکاندا بنوسە في پێشكەشانەكاندا نێش شاردنەوە لە ڤیدیۆ پەیوەندیدارەکان Hide in search results Hide in watch history لاپەڕەی کەناڵ + دیاری بکە کام پێکهاتەکان لە لاپەڕەی کەناڵەکەدا دەردەکەون فلتەری تابی کەناڵ چالاک بکە فلتەری تابی کەناڵ + ناوی تابەکانی کەناڵەکە بنووسە بۆ فلتەرکردن، بە دێڕی نوێ جیاکەرەوە شاردنەوەی دوگمەی کۆمەڵگە شاردنەوەی دوگمەی بەشداریکردن پێشۆخەکانی ڕێکخستن مەشارپێنە + لینکە وەب و کلیککراوەکانی سەرپەڕی کەناڵەکە دەشارێتەوە + شاردنەوەی چیپى \'تەنها بۆ ئەندامان\' + چیپى \'تەنها بۆ ئەندامان\' لە شریتی فلتەری کەناڵەکەدا دەشارێتەوە سەکۆی ئەندامان قایم بکه + لیستی ئەندامانی کەناڵەکە دەشارێتەوە + ڕەفەی پۆستەکان بشارەوە دوکانی ڤەشارەوە شاردنەوەی دوگمەی سەبسکرایب + مێنیوی گەڕان + دیاری بکە کام پێکهاتە لە مێنیوی گەڕان دەردەکەون + شاردنەوەی مێنیۆی خولەکان دوگمەی گەڕیدە بشارەوە + شاردنەوەی مێنیۆی \'Fashion & Beauty\' + شاردنەوەی مێنیۆی یارییەکان + شاردنەوەی مێنیۆی Hype + شاردنەوەی مینیو-ی فێربوون + شاردنەوەی مێنیۆی ڕاستەوخۆ + شاردنەوەی مێنیۆی ئەندامێتییەکان + شاردنەوەی مێنیووی \'Movies & TV\' + شاردنەوەی مێنیووی Music + شاردنەوەی مێنیۆی هەواڵەکان + مێنیۆی Playables بشارەوە + مێنیۆی پۆدکاستەکان بشارەوە + مێنیۆی بازاڕکردن بشارەوە + مێنیۆی وەرزش بشارەوە + شاردنەوەی مێنیۆی YouTube Premium + شاردنەوەی مێنیۆی YouTube Studio + شاردنەوەی مێنیۆی YouTube Music + شاردنەوەی مێنیۆی YouTube Kids + شاردنەوەی مێنیۆی YouTube Create + شاردنەوەی مینیو-ی \"چۆنیەتی کاری YouTube\" + شاردنەوەی پێزانامەکانی یاسایی و تایبەتمەندی بێ‌کێکان + دیاری بکە کام پێکهاتەکان لە بەشی کۆمێنتەکاندا دەردەکەون فلتەری کارۆسێلی سەرنجەکان چالاک بکە + "• دڵنیابە لەوەی ناوی وردی ئایتیمی carousel دەنووسیت (بۆ نموونە: Ask, Hype this video, Listen on YouTube Music) +• شارینەوەی ئایتیمەکانی carousel ڕەنگە کاریگەری لاوەکی نەخواستراوی هەبێت +• ئەگەر هەموو ئایتیمەکانی carousel شاراوە بن، تەواوی بەشی کۆمێنتەکانیش دەشارێتەوە" فلتەری کارۆسێلی کۆمێنتەکان + ناوەکانی کارۆسێڵی کۆمێنتەکان بنوسە بۆ فلتەرکردن، بە دێڕی نوێ جیاکراون خۆشکردنی پێشخانی چاتەکی پیشاندانی زانستی ڕێنمایەکانی کەناڵ قایم بکه + شاردنەوەی نامەکانی کۆنتێکست سه‌رنوشته‌ی “بێ‌کێکان لەلایەن ئەندامانەوە” قایم بکه بەشی بێ‌کێکان قایم بکه + بەشی کۆمێنتەکان لە فییدی سەرەکی بشارە + شاردنەوەی ڕێنماییەکانی کۆمەڵگە پاشکۆی دروستکردنی کورتی قایم بکه + دوگمەی Dislike بشارەوە + دوگمەی ئیمۆجی بشارەوە + بڕوانەوە بە بژاردەکانی شریتی فلتەر بنوسە + هەموو بژاردەکانی شریتی فلتەر دەشارێتەوە جگە لە \'بەرزترین\' و \'نوێترین\' + ئەنیمەیشن و کارتەکانی دیاری بشارەوە + دوگمەی دیاری بشارەوە دوگمەی شاردنەوەی زانیارییەکان Hide live chat donators bar + دوگمەی سکرۆڵی بۆ خوارەوەی چاتی ڕاستەوخۆ بشارەوە + شاردنەوەی تێبینییەکانی چاتی ڕاستەوخۆ + پۆپئەپەکانی تێبینی دەشارێتەوە کە لە نزیک شریتی داخڵکردنی چات دەردەکەون، وەک ئاگادارییەکەی چاتی تەنها بۆ بەشداربووان + دوگمەی مینو بشارەوە + دوگمەی مینووی ٣-خاڵەکەی هەر کۆمێنتێک دەشارێتەوە پێشخانی بێ‌کێکە قایم بکه + پێش کردنەوەی بەشی تەواوی کۆمێنتەکان، پێشوەیەتای کۆمێنتەکان لە ژێر ڤیدیۆکە دەشارێتەوە + کۆمێنتەکە لەلایەن Morpheـەوە شاردراوەتەوە… + چالاککردنی دوگمەی کۆمێنتە کەمترینەکان + کارتە پانببۆوەکان و هەڵبژاردەری لاپەڕەکەی دەشارێتەوە، کە لەسەر دوگمەی کۆمێنتەکان پیشان دەدرێت پاشکۆی پێشێرتن قایم بکه + دوگمەی کاتی نیشاندەر بشارەوە + دوگمەی \'فانە سەرەکەیان\' بشارەوە + دوگمەی وەرگێڕان بشارەوە + نامەی \'هێشتا هیچ کۆمێنتێک نییە\' بشارەوە + پاککردنەوەی لینکەکانی گەڕانی دیاریکراو + لینکەکانی گەڕانی دیاریکراو پاک دەکاتەوە، تەنها دەقە ئەسڵییە سادەکە دەهێڵێتەوە + فلتەری وشەی سەرەکی + وشە یان دەستەواژەی دیاریکراو هەڵبژێرە بۆ شاردنەوەی ڤیدیۆکان لە فید و گەڕانەکاندا + شاردنەوەی کۆمێنتەکان بەپێی وشەی سەرەکی بەڵگەدانەوەکانی سەرەکی بە دەستەکان خفەت بکە ئەنجامەکانی گەڕانەوە بە دەستەکان خفەت بکە بەڵگەدانەوەکانی ئەندێریانەکان بە دەستەکان خفەت بکە دەستەواژەکان بۆ شاردنەوە + "وشە سەرەکییەکان و دەستەواژەکانی شاردنەوە بنووسە، کە بە دێڕی نوێ جیاکەرەوە بن + +وشە سەرەکییەکان دەتوانن هەر دەقێک بن کە لە ناونیشانی ڤیدیۆکان یان ناوی کەناڵەکاندا دەردەکەوێت + +ئەو وشانەی کە پیتی گەورەیان لە ناوەڕاستدایە دەبێت ڕێک وەک خۆیان بنوسرێن (بۆ نموونە: iPhone, TikTok, LeBlanc)" + "سەرەتا / بەشدارکردنەکان / گەڕان / کۆمێنتەکان فلتەر دەکرێن بۆ شاردنەوەی ئەو ناوەڕۆکانەی کە لەگەڵ دەستەواژەی وشەی سەرەکی دەگونجێن + +سەرووکارییەکان: +• Shorts ناتوانرێت بەپێی ناوی کەناڵ بشاردرێنەوە +• هەندێک لە پێکهاتەکانی UI ڕەنگە نەشاردرێنەوە +• گەڕان بۆ وشەیەکی سەرەکی ڕەنگە هیچ ئەنجامێکی نادات" گونجاندنی وشە تەواوەکان + دانانی وشە یان دەستەواژەیەک لە نێوان نیشانەی هاوتایین (\"\") ڕێگری دەکات لە دۆزینەوەی بەشێکی ناو ناوی ڤیدیۆکان و ناوی کەناڵەکان<br><br>بۆ نموونە،<br><b>\"ai\"</b> ڤیدیۆی : <b>How does AI work?</b> دەشارێتەوە<br>بەڵام ئەمە ناشارێتەوە: <b>What does fair use mean?</b> ناتوانرێت دەستەواژەی: %s بەکاربهێنرێت زیادکردنی کۆتەیشن بۆ بەکارهێنانی دەستەواژە: %s دەستەواژە ڕاگەیاندنی ناکۆکی هەیە: %s دەستەواژە زۆر کورتە و پێویستی بە کۆتەیشن هەیە: %s دەستەواژە هەموو ڤیدیۆکان دەشارێتەوە: %s + "• سێوە: %s +• بەشدارکێرییەکان: %s +• گەڕان: %s +• کۆمێنتەکان: %s +• کۆی گشتی: %s" دۆخ + ڤیدیۆکانی شاراوە، ٢٤ کاتژمێری ڕابردوو + ڤیدیۆکانی شاراوە، هەموو کات + ژمێرەرى ٢٤ کاتژمێری ڕابردوو سفر بکەیتەوە؟ + ژمێرەرى هەموو کات سفر بکەیتەوە؟ + فلتەری کەناڵی AI + شاردرەوەی ڤیدیۆکان لەو کەناڵانەی کە لەلایەن لیستی کۆمەڵگەی AiSList وەک ناوەڕۆکی بە AI دروستکراو نیشانەدراون + لیستی بلۆککردن: متمانەی بەرز + لیستی ئاگادارکردنەوە: متمانەی مامناوەند + شاردنەوەی ڤیدیۆکان لە سەرەتا + شاردنەوەی ڤیدیۆکان لە گەڕان + "• سەرەتا: %s +• گەڕان: %s +• کۆی گشتی: %s" + کەناڵەکانی لیستی بلۆککراو + کەناڵەکانی لیستی ئاگادارکردنەوە + فلتەرێک چالاک بکە بۆ بارکردنی کەناڵەکان + بارکردنی کەناڵەکان... + "ئەنجامەکانی سەرەتا و گەڕان فلتەر دەکرێن بۆ شاردنەوەی ڤیدیۆکانی ئەو چەناڵانەی کە لەلایەن AiSList نیشانەیان پێدراوە + +سەرووێتییەکان: +• AI Shorts کە فلتەر کراوە ڕەنگە وەک بۆشاییەک دەربکەوێت +• هەندێک ڤیدیۆی AI ڕەنگە هێشتا دەربکەون" + "AiSList لیستێکی سەرچاوە کراوەیە لە کەناڵەکانی YouTube کە ناوەڕۆکی دروستکراوی AI بڵاو دەکەنەوە، و لەلایەن خۆبەخشییەوە بەڕێوە دەبرێت. کەناڵەکان لەلایەن بەکارهێنەرانەوە ڕاپۆرت دەکرێن و پێش ئەوەی زیاد بکرێن پێداچوونەوەیان بۆ دەکرێت + +لیستەکە لە GitHub دایگرتراوە و ڤیدیۆکان لەسەر ئامێرەکەت فلتەر دەکرێن" + ڕاپۆرتی کەناڵەکانی AI بکە + بەنکەی ڕاپۆرت زیاد بکە بۆ مێنیوی ڤیدیۆ + بەنکەیەک زیاد بکە بۆ مێنیوی ڤیدیۆ کە ڕاپۆرت لەسەر کەناڵی ڤیدیۆیەک بۆ AiSList بنێرێت + ناوی ناردەر + ئەوەی لە تەنیشت ڕاپۆرتەکانت لە لیستی سەرکەوتوونی گشتی AiSList پیشان دەدرێت. ڕاپۆرتکردن بەردەست نییە ئەگەر ئەم خانەیە بەتاڵ بێت + ڕاپۆرتکردنی کەناڵی AI + "%s بنێرە بۆ AiSList، بەکارهێنانی ئەم ڤیدیۆیە وەک بەڵگە؟ + +تەنها ئەو کەناڵانە ڕاپۆرت بکە کە: +• دەنگ، دیمەن، سکریپت یان موزیکی AI وەک ناوەڕۆکی سەرەکی بەکاربهێنن + +ناوەڕۆک بە شێوەیەکی بەکۆمەڵ بە ئامرازەکانی AI و بەبێ هەوڵی مرۆڤ بەرهەم بهێنن + +ئەو کەناڵانە ڕاپۆرت مەکە کە تەنها باسی AI دەکەن، یان AI بۆ هەندێک بەشی ڤیدیۆکانیان بەکاربهێنن. + +ڕاپۆرتەکان گشتیین و پێش ئەوەی کەناڵێک زیاد بکرێت بۆ لیستەکە پێداچوونەوەیان بۆ دەکرێت. لابردن تەنها لە ڕێگەی داواکارییەک لە GitHub دەکرێت" + ئەم کەناڵە پێشتر لە لیستی بلۆککردنی AiSListدایە + ئەم کەنالە پێشتر لە لیستی ئاگادارکردنەوەی AiSList دا بووە + ڕاپۆرتکردن + ناوی ناردەرێک لە ڕێکخستنەکانی فلتەری کەناڵی AI دابنێ بۆ ڕاپۆرتکردنی کەنالەکان + گەڕان بەدوای کەناڵەکەدا... + ناتوانرێت کەناڵی ئەم ڤیدیۆیە بدۆزرێتەوە + کەنالەکە ڕاپۆرتی کرا و چاوەڕوانی پێداچوونەوەیە + AiSList نەیتوانی ڕاپۆرتەکە پاشکەوت بکات، دواتر هەوڵ بدەرەوە + ڕاپۆرتی زۆر نێردراوە، دواتر هەوڵ بدەرەوە + AiSList ڕاپۆرتەکەی ڕەتکردەوە + ڕاپۆرتکردنەکە سەرکەوتوو نەبوو، تکایە پەیوەندییەکەت بپشکنە + لیستی ڕێگەپێدراوی ڕیکلامی کەناڵەکان + ڤیدیۆکانی کەناڵە ڕێگەپێدراوەکان بە ڕیکلام دەپاتەن، بۆ ئەوەی دراو بە دروستکەرەکانیان بدرێت. ڕیکلامەکان لە هەموو کەناڵەکانی تر دەمێننەوە بە بلۆککراو + کەناڵێک زیاد بکە بۆ لیستی ڕێگەپێدراوی ڕیکلامەکان + کەناڵێک لابدە لە لیستی ڕێگەپێدراوی ڕیکلامەکان ڕەفەی فرۆشگای دروستکەر بشارەوە شاردنەوەی بانەری فرۆشگای شاشەی کۆتایی شاردنەوەی ڕێکلامە گشتیەکان شاردنەوەی بانەری کاڵاکان + شاردنەوەی لیبێڵەکانی ڕیکلامی پارەدار + شاردانەوەی ڕیکلامی پاپ ئەپی پلەیەر کارتی خۆ سپۆنسەرکراو بشارەوە بەستەرەکانی بازاڕکردن بشارەوە شاردنەوەی ڕێکلامی ڤیدیۆکان + شاردنەوەی ڕیکلامەکانی YouTube Premium بارکردن… @@ -156,72 +410,364 @@ Second \"item\" text" کەناڵ لە لیستی ڕێگەپێدراوەکان لابرا + گەڕان لە ناو کەناڵەکەدا + دووەم دوگمەیەکی گەڕان زیاد دەکات لە لاپەڕەکانی کەناڵ بۆ گەڕان تەنها لەو کەناڵەدا + گەڕان لە کەناڵ + ئەنجامەکانی گەڕانی کەناڵ + هیچ شتێک لەم چەناڵەدا نەدۆزرایەوە + نەتوانرا گەڕان لەم چەناڵەدا بکرێت + ڤیدیۆکان + پۆستەکان + هیچ پۆستێک لەم کەنایەدا نەدۆزرایەوە + گرنگی + زۆرترین بینەر + کەمترین بینەر + نوێترین + کۆنترین + کورتترین + درێژترین + لینکی کۆپی کرا بۆ ڕوونووسگە + لینکی لەگەڵ کاتی دیاریکراو کۆپی کرا بۆ ڕوونووسگە + پیشاندانی دوگمەی کۆپی کردنی لینکی ڤیدیۆ + کلیک بکە بۆ کۆپیکردنی لینکی ڤیدیۆکە، کلیک بکە و ڕایگرە بۆ کۆپیکردنی لەگەڵ کاتی دیاریکراو + کۆپیکردنی کاتی ڤیدیۆ بە شێوەی پێشفرۆش + کردارەکانی کلیک و کلیک-و-ڕایگرتنی دوگمەی \'کۆپیکردنی لینکی ڤیدیۆ\' پێچەوانە دەکاتەوە + کۆپی کردنی ناونیشانی ڤیدیۆ + ناونیشانی ڤیدیۆکە دابگرە بۆ کۆپی کردنی + ناونیشانی ڤیدیۆکە کۆپی کرا بۆ ڕوونووسگە + کۆپی کردنی کۆمێنتەکان + کۆمێنتێک دابگرە بۆ کۆپی کردنی + کۆمێنتەکە کۆپی کرا بۆ ڕوونووسگە + کلیک بکە و ڕاگرە بۆ کردنەوەی مێنیوی ڤیدیۆ + کلیککردن و ڕاگرتن لە هەر شوێنێکی ڤیدیۆیەکی فید، مێنیوەکەی دەکاتەوە + نیشاندانی دوگمەی \"دواتر بینەری بکە\" لەسەر وێنەی پێشوەختەکان + دوگمەی \"دواتر بینین\" لەسەر وێنەی پێشەکی ڤیدیۆکانی فید دەردەکەوێت. پەستانی درێژخایەن لەسەری بژاردەی \"پاشکەوتکردن لە لیستی ڤیدیۆکان\" دەکاتەوە + دابەزاندنی وێنەکانی پۆست + دوگمەیەکی دابەزاندن لە تەنیشت دوگمەی بەشیکردنی ئەو پۆستانە دەردەکەوێت کە وێنە یان GIFـیان تێدایە، هەروەها بژاردەیەکی دابەزاندن زیاد دەکرێت بۆ مێنیووەکانیان + دابەزاندنی وێنەکان + دابەزاندنی وێنەکە + دابەزاندنی %s وێنە… + دابەزاندنی وێنەکە… + %1$s وێنە پاشکەوت کرا بۆ %2$s + وێنەکە پاشکەوت کرا بۆ %s + نەتوانرا وێنەکە دابەزێنرێت + هیچ وێنەیەک لەم پۆستەدا نەدۆزرایەوە + بەنem­کەری Ask زیاد بکە بۆ مێنیو­ی flyout + بەنem­کەری کورتکردنەوەی ڤیدیۆ زیاد دەکات بۆ مێنیو­ی flyout­ی فیدەکە + "Ask لە سەرەتادا پێویستی بە هەژمارێکی چنراوە لە وڵاتێک کە Ask تێیدا بەردەست بێت. + +دوای ئەوەی جارێک ڤیدیۆیەک کە دوگمەی Ask­ی تێدایە بکرێتەوە، ئەم تایبەتمەندییە لەگەڵ هەموو ڤیدیۆکاندا کار دەکات تەنانەت ئەگەر وڵاتی ئێستاش تایبەتمەندی Ask­ی تێدا نەبێت." + کورتکردنەوەی ڤیدیۆکە + نەتوانرا Ask بۆ ئەم ڤیدیۆیە بکرێتەوە + Ask بۆ هەژماری تۆ یان وڵاتی ئێستات بەردەست نییە + ژمارەی بینەران لەسەر وێنەی پێشەکییەکان پیشان بدە + ژمارەی بینەرانی ڤیدیۆکانی فید لە خوارەوەی لای چەپی وێنەی پێشەکییەکانیان پیشان دەدرێت لەجیاتی ئەوەی لە خوار ناونیشانەکە بێت. هەندێک ڤیدیۆ، وەک ئەوانەی لە ڕەفەکاندان، ڕەنگە ژمارەی بینەران پیشان نادەن + پیشاندانی ژمارەی لایکی وێنەی پێشەکی فید + ڕێژەی لایکی وێنەی پێشەکی فید پیشان بدە + گەڕان لە کۆمێنتەکان + کلیک لە دوگمەی گەڕان لە کۆمێنتەکان بکە بۆ ئەوەی تەنها ئەو کۆمێنتانە نیشان بدات کە هەموو وشەکانی گەڕانەکەیان تێدایە. وشەکان بخە ناو نیشانەی کوتاس \" \" بۆ گەڕان بەدوای دەستەواژەیەکدا + گەڕان لە کۆمێنتەکان + ئەو وشانەی کە دەگەڕێیت بۆیان گەڕان + پاککردنەوە + ڕیزبەندی گۆڕین یان دووبارە کردنەوەی کۆمێنتەکان بۆ گەڕان تێیاندا + گەڕان پاککراوە. ڕیزبەندی گۆڕین یان دووبارە کردنەوەی کۆمێنتەکان بۆ پیشاندانی هەموو کۆمێنتەکان + بێکارکردنی پەنجەرەی \'چوونەژوورەوە بۆ تەلەڤیژن\' + بێکارکردنی پاپ ئەپی \'بەستنەوەی ئامێرەکانت\' + بەخراپکردنی تێپەڕاندنی بەشی ڤیدیۆ بە دوو کلیکی خێرا + ئەو جوڵەی دوو پەنجارەی دوو جار لێدانی شاشەی کە بەشەکان دەپەڕێنێت ڕادەگرێت، کە ڕەنگە بە هەڵە بە یەک پەنجەرە کاربکەوێت + داگرتنی ڤیدیۆ، پۆدکاست، یان گۆرانی بە بەکارهێنانی ئەپەکانی داگرتنی لایەنی سێیەم + دوگمەی داگرتنی سەرپۆشی پلەیەر پیشان بدە + دوگمەیەک لە پلەیەری ڤیدیۆدا پیشان دەدات کە داگرتەری دەرەکییەکەت دەکاتەوە + داگرتنی خێرا بە کلیک کردن + داواکاری بۆ داگرتن لە پشتپاوە. کلیک بکە و ڕایگرە بۆ کردنەوەی داگرتنەکە. لەگەڵ YTDLnis و YTindir کار دەکات + نیشاندانی داگرتنی وەک بینراو + ئەو ڤیدیۆیانەی بە داگرتنێکی دەرەکی داگرتراون، زیاد دەکرێن بۆ مێژووی بینینی YouTube + Full HD (1080p) + ناوەند (192 kbps) + داگرتنی خێرا ڤیدێۆ + دەنگ + هەموو هەڵبژاردەکان + پیشاندانی داگرتن لە مینیوی Shorts + بەنemûنەیەک بۆ دابەزاندنی دەرەکی زیاد دەکات بۆ مێنیۆی Shorts player دابەزاندن + وەرگێڕانی دەنگی + خوێندنەوەی سەری نوسینەکانی ڤیدیۆ بە دەنگ بە بەکارهێنانی تایبەتمەندی دەق-بۆ-دەنگی ئامێرەکە، کە لەگەڵ لێدانی ڤیدیۆکە هاوسەنگ دەبێت + دوگمەیەکی سەرپۆش لە پلەیەری ڤیدیۆدا نیشان دەدات. دابگرە بۆ گۆڕینی وەرگێڕان، دابگرە و ڕایگرە بۆ ڕێکخستنە خێراکان + زمان + دابینکەری وەرگێڕان + دەنگی دەنگاوە ڕەسەنەکە + دەنگی دەنگاوە ڕەسەنی YouTube کاتێک وەرگێڕانەکە دەدوێت + دەنگی وەرگێڕان + دەنگدانی وەرگێڕانی دۆبلێنەکراو + بەرزترین ڕێگەی قسەکردن + بەرزترین گونجاندەری خێرایی کاتێک قسەکردن دەخرێتە ناو شوێنی ژێرنووس + "API وەرگێڕانی بێبەرامبەر، هیچ کلیلێکی API پێویست نییە. تا ٥,٠٠٠ کارەکتەر لە ڕۆژێکدا لە ئاستی بێبەرامبەر، یان تا ٥٠,٠٠٠ کارەکتەر لە ڕۆژێکدا بە ئیمەیڵێک. + +لێرە دابگرە بۆ کردنەوەی MyMemory.translated.net" + ئیمەیڵی MyMemory + ئارەزوومەندانەیە. ناونیشانی ئیمەیڵی MyMemory بنووسە بۆ زیادکردنی سنووری ڕۆژانە لە 5,000 بۆ 50,000 کارەکتەر. بۆ بەکارهێنانی سنووری ناناسناوە، بەتاڵیی جێبهێڵە + "وەرگێڕانی AI بە پارە لە ڕێگەی OpenRouter. جارێک باڕی زیاد بکە و هەزاران ڤیدیۆ وەربگێڕە. Mistral Nemo هەرزانترین بژاردەیە، Gemma 4 بەرزترین کوالێتی پێشکەش دەکات. + +لێرە دابگرە بۆ کردنەوەی OpenRouter.ai" + کلیلى APIی OpenRouter + کلیلى APIی OpenRouterی تۆ. کلیلێک دروست بکە لە OpenRouter.ai > Workspace > API Keys + مۆدێلی OpenRouter باو + OpenRouter ڕێکنراونە + پێش هەڵبژاردنی ئەم دابینکەرە، کلیلی APIی OpenRouter لە ڕێکخستنەکاندا بنووسە + بێبەرامبەر + %s بۆ هەر ١٠٠ کاتژمێر ڤیدیۆ + دەنگ سیستەم + نێر فرەزانی + مێ فرەزانی + نێر + مێ + کاتی گۆڕانکارییە! + وەرگێڕان بەردەست نییە + "خزمەتگوزاری وەرگێڕان هەڵەی HTTP %sی گەڕاندەوە. ئەگەریt هەیە ناونیشانی IP-ت بۆ ماوەی ٢٤ کاتژمێر سنووردار کرابێت. + +بۆ بەردەوامبوون لە بەکارهێنانی وەرگێڕانەکان، لە ڕێکخستنەکاندا بگۆڕە بۆ دابینکەرێکی تری وەرگێڕان یان VPN بەکاربهێنە" نیشانی مەدەوە دووبارە + "ڕێکخستنەکانت کلیلێکی API OpenRouterـیان تێدایە. + +نابێت هەرگیز کلیلەکەی API-ت بڵاو بکەیتەوە. هەر کەسێک دەستی پێ بگات دەتوانێت کرێدیتەکانی OpenRouter-ت بەکاربهێنێت" + سێنووری کرێدیتەکانی OpenRouter گەیشتە کۆتایی + سێنووری کرێدیتەکانی OpenRouter-ت گەیشتە کۆتایی. باڵانسەکەت زیاد بکە یان سێنووری خەرجکردنی کلیلەکەت لە OpenRouter.ai زیاد بکە + کردنەوەی OpenRouter.ai + وەرگێڕانی OpenRouter سەرکەوتوو نەبوو (HTTP %s) + سێنووری ڕۆژانەی MyMemory گەیشتە کۆتایی + دیسان بەردەست دەبێت لە %s + کۆتای کوتای وەرگێڕانی بێبەرامبەری MyMemory بۆ ئەمڕۆت بەکارهێناوە (٥,٠٠٠ پیت). ئیمەیڵێکی MyMemory لە ڕێکخستنەکاندا زیاد بکە بۆ ئەوەی سێنووری ڕۆژانە زیاد بکرێت بۆ ٥٠,٠٠٠ پیت، یان بگۆڕە بۆ خزمەتگوزاری وەرگێڕانێکی تر + کۆتای کوتای وەرگێڕانی MyMemory بۆ ئەمڕۆت بەکارهێناوە (٥٠,٠٠٠ پیت). بگۆڕە بۆ خزمەتگوزاری وەرگێڕانێکی تر یان سبەینە دووبارە تاقی بکەرەوە + وەرگێڕانی MyMemory شکست هێنا (ستاتوسی %s) ڤیدیۆ دوبارەبوونەوە چالاک بکە + دووبارەبوونەوەی ڤیدیۆکە به یاد مەهێنە + رێگری لە پاشکەوتکردنی دۆخی ئێستای ڤیدیۆی لووپی بۆ ڤیدیۆکانی داهاتوو بکە + پیشاندانی دوگمەی دووبارەکردنەوەی ڤیدیۆ وەڵا پێكردنەوەی ڤیدۆ ساراوە وەڵا پێكردنەوەی ڤیدۆ نا ساراوە + مەودای دووبارەبوونەوە دەستپێکردن كۆتایـی + [hh:]mm:ss + [hh:]mm:ss (بەتاڵ = کۆتایی ڤیدیۆکە) + دیاری بکە + سڕینەوە + دووبارەبوونەوەی %1$s - %2$s + مەودای کاتی نادروستە + پیشاندانی دوگمەی وێنە-لە-ناو-وێنە + پیشاندانی مینیۆی وێنە-لە-ناو-وێنە + گۆڕین بۆ وێنە-لە-ناو-وێنە + پیشاندانی دوگمەی لێدانی هەموویان + "کلیک بکە بۆ دروستکردنی لیستێکی ڤیدیۆیی لە هەموو ڤیدیۆکانی کەناڵەکە، کلیک بکە و ڕایگرە بۆ گەڕاندنەوە + +زانیاری: ڕەنگە لە ستریمەکانی ڕاستەوخۆدا کار نەکات" + مۆدی دروستکردنی لیستەی لێستن + هەموو ناوەڕۆکەکان (ڕیزبەندی بەپێی کات، بەپێی ڕێزبەندی زیادبوون) + هەموو ناوەڕۆکەکان (ڕیزبەندی بەپێی کات) + هەموو ناوەڕۆکەکان (ڕیزبەندی بەپێی باوترین) + تەنها ڤیدیۆکان (ڕیزبەندی بەپێی کات) + تەنها ڤیدیۆکان (ڕیزبەندی بەپێی بەناوبانگی) + تەنها Shorts (ڕیزبەندی بەپێی کات) + تەنها Shorts (ڕیزبەندی بەپێی بەناوبانگی) + تەنها ڤیدیۆکانی ستریم (ڕیزبەندی بەپێی کات) + تەنها ڤیدیۆکانی ستریم (ڕیزبەندی بەپێی بەناوبانگی) + تەنها ناوەڕۆکی تایبەت بە ئەندامان + ڤیدیۆکانی تایبەت بە ئەندامان + تەنها Shorts بۆ ئەندامان + تەنها پەخشانە ڕاستەوخۆکان بۆ ئەندامان + ناتوانرێت لیستی مۆسیقا دروست بکرێت بەهۆی ناگونجانی ناسنامەی کەناڵەکە + پیشاندانی دوگمەی دووبارەبارکردنەوەی ڤیدیۆ + ناسنامەی ڤیدیۆکە بەردەست نییە. تکایە بە دەست ڤیدیۆکە دووبارە باربکەرەوە + دوگمەی پاشکەوتکردن بۆ دواتر بینە + بکە بە کلیک بۆ پاشکەوتکردن بۆ دواتر، کلیکی درێژ بکە بۆ بژاردەکانی ڕیزبەندی + ڤیدیۆکە بە سەرکەوتوویی لە لیستی \"دواتر بینین\" پاشکەوتکرا + ئەم ڤیدیۆیە پێشتر لە لیستی \"دواتر بینین\" پاشکەوتکراوە + ناتوانرێت ئەم ڤیدیۆیە لە لیستی \"دواتر بینین\" پاشکەوت بکرێت + پاشکەوتکردن بۆ دواتر + دوگمەی \"پاشکەوتکردن بۆ دواتر بینین\" لە ڕێکخستنەکانی پەیەردا پیشان بدە + ڕێگە دەدات ڤیدیۆی ئێستای بینین لە ڕێگەی ڕێکخستنەکانی ڤیدیۆوە لە \"دواتر بینین\" پاشکەوت بکرێت + دوگمەی پاشکەوتکردن بۆ دواتر لە ڤیدیۆی منداڵاندا بینە + ڕێگە دەدات ڤیدیۆکانی پەیوەست بە منداڵان لە ڕێگەی مێنیوی لاوە لە \"دواتر بینین\" پاشکەوت بکرێن + دوگمەی \"پاشکەوتکردن بۆ دواتر بینین\" لە شەرتس پیشان بدە + ڕێگە دەدات شەرتس لە ڕێگەی مێنیوی لاوە لە \"دواتر بینین\" پاشکەوت بکرێن + گواستنەوەی \"دواتر بینە」بۆ لیستێکی گۆرانی + دواتر بینە + زیادکردن بۆ ڕیزی Morphe + پاراستنی ڕیزبەندی نێوان دانیشتنەکان + ڕیزبەندییەکە دەپارێزرێت دوای ڕێستارتکردنەوەی ئەپەکە + گۆڕینی شوێنی کردارەکانی پاشکەوتکردن و کێو + بکە بە کلیک بۆ بژاردەکانی ڕیزبەندی، کلیکی درێژ بکە بۆ پاشکەوتکردن بۆ دواتر + جۆری لێدانی داهاتو لە ڕیزبەندی بنەڕەت + ڕیزبەندی ڕەسەن + ڕیزبەندی Morphe + ڕیزبەندی Morphe لە لای کلاینت + زیادکردنی بژاردەی ڕیزبەندی بۆ مێنیۆی فید + بەنemەیەکی تری flyout زیاد دەکات. ئەمە چالاک بکە ئەگەر \"Add to queue\" پیشان نەدرێت + ڕیزبەندی + ڕیزبەندی (%s) + سڕینەوەی هەموویان + هیچ شتێک لە ڕیزبەندی نییە + بەکارهێنانی Play next in queue لەسەر هەر ڤیدیۆیەک + ئێستا لێدەدرێت + لێدانی داهاتووی ڕیزبەندی + لێدانی دوایین ڤیدیۆ لە ڕیزبەندییەکەدا + دووبارە ڕیزبەندکردن لابردن + زیادکرا بۆ ڕیزبەندی + نەتوانرا ئەم ڤیدیۆیە بە ڕیزبەندییەکە زیاد بکرێت + ناتوانرێت لیستی لێدانەکان بە ڕیزبەندییەکە زیاد بکرێت + ڕیزبەندی پڕە + نەتوانرا ڤیدیۆکە دەستپێبکرێت + لیستی ڕێگەپێدراوی ڕیکلام لە مینیووی فลาย ئاوات + بەنۆمەیەکی flyout زیاد دەکات کە ڕێگە دەدات ڕیکلامەکان لە کەناڵی ئەو ڤیدیۆیەدا لێ بدەن + ئەم تایبەتمەندییە ڕەنگە پێویستی بە بەکارهێنانی PotHelper هەبێت یان گۆڕینی \"Spoof video streams\" بۆ TV یان Android VR sign-in + لیستی ڕێگەپێدراوی خێرایی لە منووی flyout + بەنۆمەیەکی flyout زیاد دەکات کە هەمیشە کەناڵی ئەو ڤیدیۆیە بە خێرایی 1.0x لێ دەدات + نەچووەتە ژوورەوە + هیچ ناسنامەیەکی لیستێکی موزیک نییە + ڕیزەکە بەتاڵە + هیچ ناسنامەیەکی ڤیدیۆ نییە + شکستی هێنا: %s + شکستی هێنا لە زیادکردن بۆ ڕیزەکە + شکستی هێنا لە دروستکردنی ڕیزەکە + شکستی هێنا لە لادان لە ڕیزەکە + شکستی هێنا لە پاشەکەوت کردنی ڕیزەکە + زیادکرا بۆ ڕیزەکە + ڕیزەکە دروستکرا + لە ڕیزەکە لابررا + ڕیزەکە پاشکەوت کرا بۆ %s + ڕیزبەندی Morphe + زیادکردن بۆ ڕیز + زیادکردن بۆ ڕیز و کردنەوە + زیادکردن بۆ ڕیز و لێدان + زیادکردن و دووبارە بارکردنی ڤیدیۆ + لابردن لە ڕیز + لابردن و کردنەوەی ڕیزەکە + لابردن و دووبارە بارکردنی ڤیدیۆ + کێوەکە بکەرەوە + پاشکەوتکردنی کێوەکە بۆ لیستێکی प्लेइंग + لیستی پەخشکردنەکە پاشکەوت کرا + پاشکەوت کرا بۆ %s + پاککردنەوەی ڕیزبەندی + ڕیزبەندییەکە پاککراوە + ڕیزبەندی + لێدانی دوایین لە ڕیزبەندی + دواتر لە ڕیزەکە لێبدە + YouTube نەیتوانی ڕیزبەندییەکە پیشان بدات. + ڕیزەکە ئامادە نییە بۆ ئەم ڤیدیۆیە. یاریزای ڤیدیۆکە بکەرەوە و دووبارە هەوڵ بدەرەوە. + Mix زیاد بکرێت بۆ ڕیزەکە یان بینینراوە؟ + بینین + زیادکردن بۆ ڕیزەکە + بارکردنی Mix… + کردنەوەی کەناڵ لە ڕێگەی ئەڤەتاری ڕاستەوخۆوە + ڕێگری دەکات لەوەی ستریمی ڕاستەوخۆ بکرێتەوە کاتێک کلیک لە ئەڤەتاری کەناڵەکە دەکرێت + بێکارکردنی ئاماژەی گەڕانی ورد + مۆدی گەڕانی ورد بێکار دەکات کە کاتێک شریتی گەڕان بەرز دەکرێتەوە چالاک دەبێت + چالاککردنی DVRی پەخشانی ڕاستەوخۆ + ڕێگەی گەڕان دەدات لە هەموو پەخشانە ڕاستەوخۆکاندا، تەنانەت ئەوانەی بە شێوەیەکی ئاسایی ڕێگری لێدەکەن + پیشاندانی وێنەی پێشوەختەی seekbar چالاک بکە + وێنەیەکی پێشوەختەی ڤیدیۆ پیشان دەدات لە کاتی ڕاکێشانی seekbar + زۆرترین دووبارە بینراو + "ڕەنگە 'Enable live stream DVR' بەردەست نەبێت + +بۆ چاککردنی ئەمە، 'Spoof video streams' چالاک بکە و 'Default client' بگۆڕە بۆ 'TV Simply' یان 'visionOS'" + درێژکردنەوەی ماوەی DVRی ڕاستەوخۆ Allows you to rewind live broadcasts up to 7 days into the past + "ڕەنگە 'Expand live stream DVR duration' بەردەست نەبێت + +بۆ چاککردنی ئەمە، 'Spoof video streams' چالاک بکە و 'Default client' بگۆڕە بۆ 'TV Simply' یان 'visionOS'" + چالاککردنی پێشخستن بە لادان + چالاککردنی گەڕان بە ڕاکێشان لە جیاتی لێدانی ڤیدیۆکە بە خێرایی 2x کاتێک پەنجە لەسەر دیسکی ڤیدیۆکە دەخرێت و ڕادەگیرێت دەست لێدان بۆ گەڕان چالاک بکە + تەنها کلیک لە شریتی گەڕان بکە بۆ بازدان بۆ ئەو شوێنە بەبێ ڕاکێشانی شریتەکە + ئاماژەی سوایپی چەپ + ئاماژەی سوایپی ڕاست + ئاماژەی سوایپی سەرەوە + پانی زۆنی چەپ/ڕاست + ڕێژەی هەر لایەک لە شاشەکە کە بۆ جوڵەی سوایپی چەپ و ڕاست تەرخان کراوە (٥-٥٠) + ڕووناکی + دەنگ ڕەسەن کوژاوە + پێشانەپێنای سوایپ ببەستە کاتێک قفڵکراوە + جوڵەکانی سوایپ پشتگوێ دەخرێن کاتێک پلەیەرەکە بە \'قفلکردنی شاشە\' قفڵکراوە پەستان چالاک بکە بۆ ئاماژەدان بە لێدان + پێویستی بە پەستان و ڕاگرتنهەیە پێش ئەوەی جوڵەی سویپ بناسرێت ئامۆژگاری هاپتیک چالاک بکە پاشەکەوتکردن و گەڕاندنەوەی ڕووناکی + ئاستی ڕووناکی دەپارێزێت لە کاتی چوونە ناو / هاتنە دەرەوە لە شێوازی شاشەی تەواو ئاماژەی ئۆتۆماتیکی ڕووناکی چالاک بکە + سەرکشانى ڕووناکی بۆ نزمترین بەها، ڕووناکی خۆکارى سیستم چالاک دەکات خۆرا ئەگەر شاشە دەربازە، کاتی کۆتایی دەگات خۆڵە دەقەکانی میلیلزە کە پۆشێنیكە زەمان دەربخە ناڕوونی لێدانی سەرپۆشی پاشخانی ءيجست سير رنغدي - (0 ري رنحانًح, 100 ري دواق) شۆڕکەرەوە ڕەنگی زادە + ڕەنگی شریتی پێشکەوتنی کۆنترۆڵەکانی ڕووناکی دیاری بکە شۆڕکەرەوە ڕەنگی دەربەست + ڕەنگی شریتی پێشکەوتنی کۆنترۆڵەکانی دەنگ دیاری بکە + ڕەنگی خێرایی سەرپۆشی لێدان + ڕەنگی شریتی پێشکەوتنی کۆنترۆڵەکانی خێرایی لێدان دیاری بکە لێدانی قەبارەی دەقی سەرپۆش + قەبارەی دەقی سەر شاشەکە ڕێکبخە (١-٣٠) مارگی ڕەهیشەی شۆڕکەرەوە + مەودای پێویستی سوڕاندن دیاری بکە پێش ئەوەی گۆڕانکارییەک ڕووبدات + حەساسیەتی ڕاکێشانی دەنگ + بەها بەرزترەکان پێویستیان بە سوایپکردنی زیاتر هەیە بۆ هەر هەنگاوێکی دەنگ + هەنگاوەکانی دەنگ + بەرزکردنەوەی دەنگ + سەرخستن بۆ سەرەوە لە ئاستی بەرزترین دەنگدا، دەنگەکە زیاد دەکات، کە ڕەنگە ببێتە هۆی شێواندنی دەنگە بەرزەکان + کەموکوڕی ئامێرەکە + حەساسیەتی سوڕانەوەی ڕووناکی + بەها بەرزترەکان پێویستیان بە سوڕانەوەی زیاترە بۆ هەر هەنگاوێکی ڕووناکی + حەساسیەتی سوڕانەوەی خێرایی + بەها بەرزترەکان پێویستیان بە سوڕانەوەی زیاترە بۆ هەر هەنگاوێکی خێرایی + هەنگاوی خێرایی + بەرزی زۆنی خێرایی + بەرزی زۆنی ئاماژەی خێرایی وەک ڕێژەیەک لە بەرزی پلەیەرەکە (٥-٧٥) + خێرایی شووشكە ڕووەکەی پێشێل پاشخۆشە ئاسا ئاسا (گەفی گەشە - سەرەوە) @@ -231,114 +777,241 @@ Second \"item\" text" پۆشینی خشتەوەر پۆشینی خشتەوەر (زۆر كەمتەرخەم) خۆشکردن بۆ گۆڕینی ڤیدێۆکان + لە شێوەی شاشەی تەواودا بسوڕێنە بۆ ڕۆشتن بۆ ڤیدیۆکەی داهاتوو یان پێشوو + دووگمەکانی کردار + دیاری بکە کام دوگمەی کردار لە شێوەی ستاندارد و شاشەی تەواودا دەردەکەون + دووگمەکانی ستایلی ستوونی چۆنەکردنی ڕەهەق و نوسین + ڕێگری دەکات لە درەوشانەوەی دوگمەکانی لایک و سەبسکرایب کاتێک دروستکەرەکە ئاماژەیان پێ دەکات شاردنەوەی باری کردار + دوگمەی لایک و دیسلایک بشارەوە دکمه‌که‌ی بهێنه‌یه‌وه‌ ببه‌جێ ده‌ربخه‌ + دوگمەی \'ڕاگرتنی ڕیکلامەکان\' بشارەوە خۆشکردنەوەی کۆمێنتەکان + شاردنەوەی دوگمەی بەستنەوە + دوگمەی ڕاپۆرتکردن بشارەوە + دوگمەی ریمیکس بشارەوە + دوگمەی داگرتن بشارەوە + دوگمەی Hype بشارەوە + دوگمەی Promote بشارەوە + دوگمەی سوپاس بشارەوە دۆزینەوەی پەیامگە دەربخۆ + شاردنەوەی کەناڵ و دوگمەی سەبسکرایب + ئەڤاتاری کەناڵ و دوگمەی سەبسکرایب لە سەرەتای شریتی کرداری دەشارێتەوە شاردنەوەی دوگمەی سەبسکرایب + دوگمەکانی Subscribe و Join دەشارێتەوە لە تەنیشت وێنەی پرۆفایلی کەناڵەکە لە سەرەتای باری کرداردا + دوگمەی Clip بشارەوە + ئەمە ڕەنگە بۆ هەندێک هەژماری بەکارهێنەر کارنەکات + دوگمەی فرۆشگا بشارەوە محفض دمودي + شاردنەوەی دوگمەی زیاتر + دووگمەکانی شاشەی تەواو + دوگمەی نادوستن بشارەوە + دوگمەی بەلاڤکردن بشارەوە + شاردنەوەی دوگمەی \'چاتە زیندییەکە\' + شاردنەوەی دوگمەی تێکەڵکردن + شاردنەوەی دوگمەی \'ڤیدیۆی زیاتر\' + شاردنەوەی دوگمەی لیستی ڤیدیۆکان + مارجینی سەرەوەی کردارە خێراکان + مارجینی سەرەوەی شریتی کردەوە خێراکان دیاری بکە (0-32 dp) زیرەکان + بەڕێوەبردنی ژێرنووسی خۆکار و کوکییەکانی ژێرنووس دیارە ڕەنگەکانی زیرەکان + هەمیشە پیشانی بدە تۆ نەربێنە + نیشان بدە کاتێک بێدەنگ نییە هنگێو سڕکرێتەوە نیشانە + زمانی دەرکەوتنی دڵخواز + سەرەتا ژێرنووسی دابینکەرەکە هەڵدەبژێرێت، پاشان بە شێوەیەکی ئۆتۆماتیکی وەردەگێڕێت بۆ زمانی دڵخواز کوژاوە + زیادکردنی زمانەکانی تری وەرگێڕانی خۆکار + زمانێکی زیاتر بۆ مینیوی وەرگێڕانی خۆکار زیاد دەکات + دیاریکردنی کووکییەکانی سەپشەن + "کێشەیەکی ستۆکی YouTube چاک دەکات کە تێیدا وەرگێڕانی ئۆتۆماتیکی ژێرنووسەکان ڕەنگە بەردەست نەبن بەهۆی نەبوونی کوکییەکان + +دەبێت 'Caption cookies' دابین بکرێن" کێکەکان + کووکییەکان کە دەبێت لە داواکارییەکانی Timed Text API (Caption Data API) دا دیار بکرێن کێکەکانی ناوی سەره‌نگ + لێرە کلیک بکە بۆ هێنانی کووکییەکان شکستی هێنا لە هێنانی کووکی کێکەکان پێشتر دەستخۆش بوون کێکەکان سەرکەوتوانە ڕێکخراون ناونیشانی دۆزینەوەی ڕێکخستەکان ڕێکخا + چارەسەری شکستێکی پشکنینی پێشەکی دەکات کە ڕەنگە ڕێگری بکات لە بارکربوونی دەقەکەی ڤیدیۆکە + پیشاندانی سەردێڕەکان لە وێنە-لە-ناو-وێنەدا دووگمەکانی گەشتکردن هەڵبژێرە کام دوگمەیە دەردەکەوێت لە شریتی گەشتکردن دوگمەی ماڵەوە بشارەوە + شاردنەوەی دوگمەی Shorts جفيء پشكه دروستكردن + شاردنەوەی دوگمەی بەشدارکردنەکان دوگمەی ئاگاداری بشارەوە + نیشاندانی دوگمەی گەڕان + شوێنی دوگمەی گەڕان + نیشاندانی دوگمەی ڕێکخستنەکان + شوێنی دوگمەی ڕێکخستنەکان کرداری دوگمەی ڕێکخستن\" + کاتێک چالاک بێت، ڕێکخستنەکانی Morphe دەکاتەوە، یان ڕێکخستنەکانی YouTube کاتێک ناچالاک بێت + گۆڕینی شوێنی دوگمەی دروستکردن بە دوگمەی ئاگادارکردنەوەکان ئەگەر گۆڕینی ئەم ڕێکخستنە کاریگەری نەبوو، هەوڵبدە بگۆڕیت بۆ دۆخی نادیار شاردنەوەی ناوەکانی دوگمەی گەشتکردن + شاردنەوەی خاڵی ناوەڕۆکی نوێ شاردنەوەی پەڕەکانی گەشتکردن + "ئەمە شریتی گەڕان بە تەواوی دەشارێتەوە، لەوانەش تابی 'تۆ'. + +بۆ گەیشتن بە ڕێکخستنەکان، دوگمەی ڕێکخستنەکان چالاک بکە لە ڕێگەی گشتی → ئامرازی ئامادەسازی → نیشاندانی ڕێکخستنەکان" جوڵە پێکهاتەکانی دەستنیشانکردن بە پەیوەست بێن + بۆشایی ئاسۆیی لە نێوان دوگمەکانی گۆڕین کەمدەکاتەوە + بێکارکردنی دوگمەکانی گەشتکردن کە بێ ناونیشانن + دوگمەکانی گەڕان بە ناوکێشەکان نیشان دەدات، ئەگەر YouTube بەبێ ناوکێشەیان بنێرێت + بێکارکردنی تابات گەڕانی ئامرازەدەست + بەشدارکێرییەکان لە شریتی گەڕان نیشان دەدات، ئەگەر YouTube بیگوازێت بۆ تابات سەرەوەی Home ئەنیمەیشنی پەڕەکانی گەشتکردن چالاک بکە + گۆڕانکارییەکان بە ئەنیمەیشن نیشان دەدات لە کاتی گۆڕین لە نێوان تابهکاندا + بێکارکردنی شاردانەوەی خۆکارى شریتی گەڕان + شریتی گەڕان بەردەوام دیار دەکات لە کاتی سکرۆڵکردندا + ستایلی شریتی گەشتکردن بنەڕەت + نیم-ڕوون + ڕوون + ناڕوون + ئەم شێوازە لەوانەیە لەلایەن ئامێرەکەت یان وەشانەکەی YouTube پشتگیری نەکرێت، و دەبێت ببێتە هۆی کێشەی ڕێکخستن یان داخرانی بەرنامەکە بەڵتە + دیاری بکە کام پێکهاتەکان لە شریتی ئامرازەکاندا دەربکەون دوگمەی پەخشکردن بشارەوە + دوگمەی چات بشارەوە + شاردنەوەی دوگمەی مایکرۆفۆن + دوگمەی گەڕان بشارەوە + کاتێک چالاک بێت، کلیک کردن ڕێکخستنەکانی Morphe دەکاتەوە و پەستانی درێژ ڕێکخستنەکانی YouTube دەکاتەوە، بۆ گۆڕینیان ناچالاکی بکە خەڵەتە ڕیزكارییەكە + دیاری بکە کام پێکهاتەکان لە مینیۆی flyout دەردەکەون + شاردنەوەی مێنیوی \'ڕێکخستنە زیادەکانی\' / زیاتر + شاردنەوەی مێنیۆی \'مۆدی دەوروبەر\' + "مێنیۆی 'Audio Track' شاردراوەتەوە + +بۆ پیشاندانی مێنیۆی 'Audio Track'، 'Spoof video streams' بگۆڕە بۆ هەر کلاینتێک جگە لە 'Android Studio' و 'Android VR Downgraded'" + مینیۆی \'ڕێگەی دەنگ\' بشارەوە + شاردنەوەی خستەی خوارەوەی مێنیو\'Audio Track\' + شاردنەوەی مێنیوی ژێرنووسەکان + شاردنەوەی خستەی خوارەوەی مێنیوی ژێرنووسەکان + شاردنەوەی سەری مێنیوی ژێرنووسەکان + منووی \'Yarmati & feedback\' بشارە + منووی \'Guhdarî bke bi YouTube Music\' بشارە + منووی \'Qulkirinî şaşە\' بشارە + شاردنەوەی مێنیۆی \'دووبارەکردنەوەی ڤیدیۆ\' + مینیۆی \'لە ڕێگادام\' بشارەوە + شاردنەوەی مینووی کوالێتی + شاردنەوەی خوارەوەی مینووی کوالێتی + شاردنەوەی سەرووی مینووی کوالێتی + شاردنەوەی مێنیۆی \'تایمەری خەوتن\' + منووی \'Xitîyî lixwenan\' بشارە + شاردنەوەی مێنیۆی \'دەنگدەرکەفتنی جێگیر\' + شاردنەوەی مینووی \'بینین لە VR\' پیشۆخێزیی خۆکار قایم بێت + پێش دەستپێکردنی لێدانی خۆکار، پێشوەگەرێی ڤیدیۆکەی داهاتوو لە شاشەی تەواودا دەشارێتەوە + دووگمەکانی سەرپۆش + دیاری بکە کام دوگمەکان لەسەر دیسپڵەی ڤیدیۆکە دەربکەون + دوگمەی خۆکار-لێدانی بشارەوە + دوگمەی ژێرنووسەکان بشارەوە دوگمەی پەخشکردن بشارەوە + دوگمەی کۆکردنەوە بشارەوە + دوگمەی شاشەی تەواو بشارەوە شاردنەوەی کۆنتڕۆڵەکانی پاشخانی پەخش + سێبەری تاریکی دەوروبەری کۆنترۆڵەکانی پلەیەرى ڤیدیۆکە دەشارێتەوە + ڕوونی پاشبنەمای دوگمەکانی کۆنتڕۆڵ + ڕێکخستنی ڕوونی پاشبنەماکەی پشت دوگمەکانی کۆنترۆڵی پلەیەر و شریتی کات (٠ نادیارە، ١٠٠ ڕەنگێکی تۆخە) + شاردنەوەی دوگمەکانی پێشوو & داهاتوو + شاردنەوەی دوگمەی ڕێکخستنەکان کارتەکانی کۆتایی دەمۆڵە دەخرێنە پاڵ شێواز ڕەوایی + بەڕێوەبردنی بینراوی شێوازی Ambient و سنووردارکردنی وزە تێکەڵوچون ڕەوا دەستخۆش هاتی + ڕێگە دەدات شێوازی Ambient چالاک بمێنێت لە کاتی شێوازی پاشکەوتکردنی باتریدا + بەکارنەهێنانی شێوازی Ambient + بێکارکردنی شێوازی Ambient لە شاشەی تەواو كارتەکانی زانین باش بكة ئەنگێستەکانی ژمارە هەڵسا لە کار دە کەون + ئەنیمەیشنی ژمارەدان لە ژمارەی لایکین و بینەران ڕادەگرێت بەکەیەو ڕا دەگەیەنە سەر شەشێک بەکەیەو ڕا دەگەیەنە سەر نەخشبەکان + پشتکەوتاندنی شریتی پێشکەوتنی بینین کە لەسەر وێنەی پێشوەختەی ئەو ڤیدیۆیانە دەردەکەوێت کە پێشتر بینین وێیەکباری زستان دەرهەقە دەیت + شاردنەوەی Shorts + بشارە لە لاپەڕەی کەناڵەکە + بشارە لە فییدی سەرەکی & ڤیدیۆکانی پەیوەندیدار شاردنەوە لە ئەنجامی گەڕان + شاردنەوە لە فییدی بەشدارکەرییەکان + شاردنەوە لە وەسفی ڤیدیۆکە + شاردنەوە لە مێژووی بینین + پەیەرى Shorts + دیاری بکە کام پێکهاتەکان لە پلەیەری Shorts دەردەکەون + ناچالاککردنی دووجار کلیککردن بۆ لایک + شاردنەوەی دوگمەی AI نشانەدانی ناونیشانی \'كۆتاییكراو\' قایم بكات خەڵه‌ت کردنه‌وه‌ی بارەکه‌ی کانال خەڵه‌ت کردنه‌وه‌ی دگمه‌که‌ی Comments خەڵکەواتە دەپۆی بەزر دەوێت بخرێتە پاڵ خەڵه‌ت کردنه‌وه‌ی لۆگۆی ڕێنمایه‌و ڤیدۆ + شاردنەوەی ئاماژەکانی جوڵەی دەست دوگمەی \'سکرینی خۆر\' باشارەوە دووگمەی هەشتەك بشارەوە خەڵه‌ت کردنه‌وه‌ی پانه‌ڵی زانیاری @@ -373,42 +1046,108 @@ Second \"item\" text" خەڵه‌ت کردنه‌وه‌ی سه‌رنیشانی ڤیدۆ نه‌ماڵینی ڤیدۆی ڕێنمایه‌و به‌کۆتایی هاتن + "ئەو ڤیدیۆ پێشنیازکراوانە دەشارێتەوە کە دوای کۆتاییهاتنی ڤیدیۆکە دەردەکەون + +ئەم ڕێکخستنە تەنها ئەگەر autoplay کوژاوە بێت کاردەکات. دەتوانیت لە YouTube بیگۆڕیت: +ڕێکخستنەکان ← Playback (یان Autoplay) ← Autoplay next video" خه‌مه‌كانی ڕێخه‌ڵه‌ت خه‌مه‌كان ڕه‌نگ ده‌كه‌ن. + شاردنەوەی ڕاوێژی \'ڤیدیۆی زیاتر\' + ڕاوێژی \'ڤیدیۆی زیاتر\' دەشارێتەوە کە لە کاتی ڕاکەشاندنی سەرەوە لە شێوازی شاشەی تەواودا دەردەکەوێت خەڵه‌ت کردنه‌وه‌ی نێوچه‌که‌ی خولیایه‌کان خه‌مه‌كانی ڕێخه‌ڵه‌ت خه‌مه‌كان ڕه‌نگ ده‌كه‌ن. + ڕێگری دەکات لەوەی پانێڵەکانی لیست و چاتی ڕاستەوخۆ بە شێوەیەکی ئۆتۆماتیکی بکرێنەوە + بێکارکردنی پەخشی خۆکارەی لیستەکان + ڕێگری دەکات لەوەی لیستەکانی ڤیدیۆ بە شێوەیەکی خۆکار بچن بۆ ڤیدیۆی داهاتوو + بەکارنەهێنانی داوای \'بەردەوامبە لە بینین\' + دەرچوون لە شێوازی تەواو شاشە لە کۆتایی ڤیدیۆکەدا نه‌كردوه‌ + تەنها ستاندارد + تەنها ئاسۆیی + ستوونی و ئاسۆیی + سەپاندنی شێوەی ئاسۆیی لە شاشەی تەواودا + پەیەرەکە وەبگەرێنە بۆ ئاسۆیی لەکاتی چوونە ناو شاشەی تەواو. لە شاشە بچووکەکاندا بەردەست نییە، کە پێشتر لە شاشەی تەواودا بە شێوەی ئاسۆیی دەکرێنەوە + ئاماژەکانی شاشەی تەواو + ئاماژەکانی پلەیەر لە شێوەی شاشەی تەواو + ناچالاککردنی ئاماژەی لێدانی سەرەوە لە شێوازی ستوونی + ناچالاککردنی ئاماژەی لێدانی خوارەوە لە شێوازی ستوونی + ناچالاککردنی ئاماژەی لێدانی خوارەوە لە شێوازی ئاسۆیی + ناچالاککردنی ئاماژەی pinch-to-zoom + گەڕاندنەوەی زوومکردن بە پەنجە + کردنەوەی ڤیدیۆکان بە شێوەی شاشەی تەواو لەکارخراو + ستوونی + ئاڕاسی + پێوانەی ڤیدیۆی شاشە تەواوە بنەڕەت + کێشان (پڕکردنی شاشە) + زووم (بڕین بۆ پڕکردن) + نیشاندانی دوگمەی پێوانەی ڤیدیۆ + کلیک بکە بۆ گۆڕانکاری لە نێوان Default، Stretch، و Zoom + تەنها لە شاشەی تەواودا نیشان بدە + شاردنی دوگمەی پێوانەی ڤیدیۆ لە پلەیەری ستوونی + نیشاندانی دوگمەی بێدەنگکردن زینەی دەربانەی خەڵاتکار ءيجست سير رنغدي - (0 ري رنحانًح, 100 ري دواق) + یاسای گەڕان لەکارخراو + پەراو + زۆر پەراو + دابەشبوونی ئامێر بونیاد تەلەفۆن تەبلێت ئۆتۆمۆبێل + "گۆڕانکارییەکان ئەمانەن: + +دیزاینی تابلێت +• پۆستەکانی کۆمەڵگە شاراوەن +• ڕێکخستنی لێدانی لە فیدەکاندا بەردەست نییە +• دوگمەی Remix و دوگمەی دەنگ لە Shorts بەردەست نین +• ڕەنگە پەردەی 'ڤیدیۆی زیاتر' بەردەست نەبێت +• شریتی کردارەکانی ڤیدیۆی کۆنی نەقۆشراو نیشان دەدرێت +• ڕەنگە بژاردەکانی مینیو-ی فڵای ئاوت بە دروستی کار نەکەن + +دیزاینی ئۆتۆمبێل +• فید بەپێی بابەت و کەناڵەکان ڕێکخراوە +• ڕێکخستنی لێدانی لە فیدەکاندا بەردەست نییە +• دوگمەی Remix و دوگمەی دەنگ لە Shorts بەردەست نین +• ڕەنگە پەردەی 'ڤیدیۆی زیاتر' بەردەست نەبێت +• Shorts لە پلەیەری ئاسایی دەکرێنەوە +• شریتی کردارەکانی ڤیدیۆی کۆنی نەقۆشراو نیشان دەدرێت" + چالاککردنی شێوازی تابلێت لە پلەیەردا + "شێوەی کۆنی پلەیەرەکە دەگەڕێنێتەوە + +سەرهاتی: ئەگەر 'Restore old video action bar' کوژاوە بێت، ڕەنگە لایەری 'More videos' بەردەست نەبێت" + 21.11.48 - گەڕاندنەوەی ڕەفتاری کۆنی ژێرنووسەکان 20.39.41 - باشدانی دۆزینەوەی خەمینەڵ نیگاتە 20.28.41 - کۆدی ڕەوا نەخۆش هاتبون + 20.13.41 - گەڕاندنەوەی ناوی کەناڵەکە لە خوارەوەی د柾انی ڤیدیۆ كرنەوەی لۆمەی بەڵگە پێشكێش كردنی سیستەم + کاتێک ناوەڕۆک بڵاودەکرێتەوە، لاپەڕەی بەشکردنی سیستەم دەکاتەوە لە جیاتی لاپەڕەی ناو ئەپەکە + سەرکوتنی دوگمەکانی YouTube Music + ئەپڵیکەیشنی موزیکی دڵخوازی تۆ دەکاتەوە لە جیاتی ئەپڵیکەیشنی ستۆکی YouTube Music + ناوی پاکەتی ئەپی مۆسیقا + ناوی پاکەتی ئەپی مۆسیقای دامەزراوی تۆ پەڕەی سەرەتا بونیاد @@ -437,6 +1176,7 @@ Second \"item\" text" خۆیانی پەل کڵی تۆ + ناچالەکردنی بەردەوامبوون لە کاتی دەستپێکردن ئەفەرمە Shorts بە خەڵکی Shorts @@ -444,33 +1184,101 @@ Second \"item\" text" ئەگەر خەڵکی بەربەست نمایشە سەرەکییە شورتەکان خۆیان بەگشتی دەست پێ دەکەن + بە شێوەیەکی ئۆتۆماتیکی دەچێت بۆ Short ی داهاتوو لە جیاتی دووبارەکردنەوەی ئێستاکە + پێکهاتەی ئۆتۆماتیکی لە پاشبنەما + بە شێوەیەکی ئۆتۆماتیکی دەچێت بۆ Short ی داهاتوو لە کاتی لێدانی پاشبنەما + بە تێپەڕاندنی Shorts-ی ڕاستەوخۆ + بە تێپەڕاندنی پەخشانە ڕاستەوخۆکان کە لە فییدی Shorts دەردەکەون + لێدانەوە لە فیدەکاندا + هەمیشە چالاکە + تەنها وای فای کوژاوە + گەڕاندنەوەی ناونیشانە ئەسڵییەکان + ناونیشانە ڕەسەنەکانی ڤیدیۆ، وەسفی ڤیدیۆ و وەسفی کەناڵەکان نیشان دەدات لە جیاتی ئەوانەی خۆکارانە وەردەگێڕدران + بارکردنی ناونیشان… + بەکارنەهێنانی نوێکردنەوەی خۆکارەی فید + ڕێگری دەکات لە نوێکردنەوەی خۆکارەی فیدەکان دوای ئەوەی کۆن دەبن + شێوەی ڕێخستنی کارتی فید + کلاسیک + کارتەکان + کارتەکان لەگەڵ دوگمەکانی لایک، کۆمێنت و پاشکەوتکردن + کۆمێنتەکان + ناچالاککردنی بەردەوامبوونی مینی پلەیەر + بەکارنەهێنانی مینیپڵەیەری \'بەردەوامبە لە بینین\'، تایبەتمەندییەکی YouTube Premium کە لە کاتی دەستپێکردنی ئەپەکەدا دەردەکەوێت خوارەوە + شێوە و ڕەفتاری دیسپڵەی ڤیدیۆی بچکۆکراو دەستکاری بکە جۆرەی خوارەوە + ناونیشان و کەناڵەکە بشارەوە نەخراوەتە کار ئاسمان لاواز + سادەی ٢ + تابلێت + مۆدێرن ١ + مۆدێرن ٢ + مۆدێرن ٣ + مۆدێرن ٤ خۆگرتنەوەی خوارەوەی نەچووە کار خۆگرتنەوەی خوارەوەی له‌ھەڵدان + ڕێگری دەکات لە ڕاکشانی دیسپڵەی بچکۆکراو بۆ گۆشە جیاوازەکانی شاشەکە خۆگرتنەوەی خوارەوەی ڕەڵد بکە + ڕێگری دەکات لە داخستنی مینی-پڵەیەرەکە لە ڕێگەی ڕاکشانی ئاسۆیی بۆ دەرەوەی شاشەکە + ڕاگرتنی لێدانەوە بە ڕاکەشانی ئاسۆیی + لەکاتی ڕاکشانی مینیپلەیەرەکە لە شاشەکە دەرهێنرا، لێدانەکە دەوەستێت + بیرهێنانی شوێنی دۆخی ڕاکەشان + ڕاکەشانی مینیپڵەیەرەکە لە شاشەکە دەربکەوێت، دۆخی UIیەکەی دەپارێزێت خۆگرتنەوەی پشتخاڵەکان بپارێزە + قەبارەی پێشنیارکراو + قەبارەی سەرەتایی شاشەکە دیاری بکە، بە پێکسڵ قەرەبایی دەبێت لە نێۆ %1$s و %2$s بێت شفافی پشتخاڵ ءيجست سير رنغدي - (0 ري رنحانًح, 100 ري دواق) + دوگمەی گەڕانەوە فیدەکە دادەخات + داگرتنی دوگمەی گەڕانەوە هەمیشە ئەپەکە دادەخات لەجیاتی ئەوەی بەرەو سەرەوەی فیدەکە بگەڕێت + سەپاندنی ئەنیمەیشنییەکان بنەڕەت + هەمیشە کارپێکراو + هەمیشە ناچالاک + شاردنەوەی باری ستاتوس + باری ستاتوسی سیستم دەشارێتەوە. بۆ ئەوەی بۆ ساتێکی کورت دەرکەوێت، لە لای سەرەوە بەرەو خوارەوە ڕبکێشە + چاککردنی شریتی بارودۆخی کە شریتەکانی سەرەوە دادەپۆشێت + شێوەی شریتی بارودۆخەکە جێگیر دەکات، بە جۆرێک کە شریتەکانی سەرەوە نەپۆشێت و هیچ بۆشاییەکی بەجێ نەهێڵێت لە سەرووی پلەیەرەکە. ئەم بژاردەیە بکە بە کوژاندنەوە ئەگەر ئەپەکە دانیشتا یان شریتەکانی سەرەوە بە شێوەیەکی هەڵا دەردەکەوتن + درەوشاوە + شاشەی دەسپێکی درەوشاوە لەلایەن ئەم ئامێرەوە پشتگیر ناکرێت + پاشبنەمای تێمای ڕوون + سپى + ماتریال یو (سپینگ) + پەمبەیی کاڵ + شینی کاڵ + سەوزی کاڵ + زەردی کاڵ + نارنجی کاڵ + سوری کاڵ + پاشبنەمای کاڵی تایبەت + ڕەنگی پشتاوەی تێمای ڕوون ڕێکبخە + گۆڕینی ڕەنگی پێشەوە + ڕەنگی تێمەکە بەکاربهێنە بۆ دەق و ئایکۆنەکان سکرینی بارکردنێکی فڕکێنەرەوە سازشە + پاشبنەمایەکەی شاشەی بارکردن بە شێوەی گڕادینت پیشان دەدات لە جیاتی ڕەنگێکی یەکڕەنگ سازشە ڕەنگی دیاری پێ بکە + ڕەنگی Seekbar + ڕەنگی سەرەکی شریتی پێشکەوتنی ڤیدیۆکە دیاری بکە + ڕەنگی زیادەی Seekbar + ڕەنگی لاوەکی شریتی پێشکەوتنی ڤیدیۆکە دیاری بکە نرخی ڕەنگی سیگبار ناگەڕێت + ئەنەلۆگی ڕەسەن + کێکی ڕەسەن + مەتاڵی ڕەسەن لوگۆی سەرەوە تاوکی @@ -479,78 +1287,369 @@ Second \"item\" text" باو نەماڵەکانی ئەو سنارانەی وێنە دەڕەقە + داواکارییەکانی وێنەی بچووک ڕەوانە دەکات بۆ سێرڤەرێکی جێگرەوە بۆ چاککردنی وێنە نەدۆزراوەکان لەو وڵاتانەی کە سنووردارکراون + ناونیشانەکانی DeArrow + بەشی سەرەکی + بەشی بەشداربوونەکان + بەشی تۆ + لیستەکانی çalاکەری & پێشنیارەکان + ئەنجامەکانی گەڕان + نیشاندانی ئایکۆنی DeArrow + ئایکۆنی DeArrow لەسەر ناونیشانەکانی ڤیدیۆی کۆمەڵانگاری نیشان دەدات + مۆدی ئاسایی + ناونیشانی ئەسڵی نیشان دەدات لە جیاتی ناونیشانی DeArrow ئەگەر بەکارهێنەرانی DeArrow دەنگیان داوە کە ناونیشانی ئەسڵیان بەدڵە + کەمترین دەنگی ئاسایی بۆ نیشاندانی ناونیشانی ئەسڵی + ژمارەی دەنگە ئاساییەکان لە یەکتەنییەکدا، وەک کۆمیدی یان داهێنەرانە، کە پێویستە بۆ پیشاندانی ناونیشانی ڕەسەن + وێنەی پێشوەختەی DeArrow ثومب‌نەییلە سەرەکیەکان DeArrow & ثومب‌نەییلە سەرەکیەکان DeArrow & پێگه‌شبونه‌وه‌کانی ثومب‌نەییل پێگه‌شبونه‌وه‌کانی ثومب‌نەییل + "DeArrow ناونیشان و وێنەی سەرکێشی کلیکبایت دەگۆڕێت بۆ ئەوانەی لەلایەن کۆمەڵگەوە کۆکراونەتەوە کە وردترن و کەمتر سەرنجڕاکێشن. ناونیشانەکان لەلایەن کۆمەڵگەوە دەنووسرێن و دەنگ لێ دەدرێت، و وێنەکانی سەرکێشیش فریمین کە لە ڤیدیۆکە وەرگیراون + +ڤیدیۆکان لە ڕێگەی پێشەکییەکی کورت لە IDی هاشکراوی ڤیدیۆکە داوا دەکرێن، بۆ ئەوەی سێرڤەر نەزانێت بە تەواوی سەیری کام ڤیدیۆت دەکەیت. ئەگەر ڤیدیۆیەک وێنەی سەرکێشی DeArrowی نەبێت، وێنەی ئەسڵییەکە یان وێنەیەکی وەستاو نیشان دەدرێت" Show connection error alerts + ئاگاداریکردنەوەیەک پیشان دەدات کاتێک سێرڤەری DeArrow بەردەست نییە + خاڵی کۆتایی API URLی کۆیله‌کانی DeArrow ڕه‌هه‌ه‌ه‌ پێگه‌شبونه‌وه‌کانی ڤیدۆ ئەگەرە قجەرەکان لێ دەخرێن لە سەرەتا / نێوەڕاستی / کۆتایەکی هەر ڤیدۆیە‌ک. ئەو وێنە دەچنە یوتوب و هیچ API دەرایە‌تیش بەکارنابێت + دەرخستنی فریمێکی ڤیدیۆ لە + سەرەتا + ناوەند كۆتایـی + DeArrow بۆ ماوەیەکی کاتی بەردەست نییە (کۆدی ئاست: %s) + DeArrow بەشێوەیەکی کاتی بەردەست نیە + گەڕاندنەوەی شێوازی کۆنی دوگمەکانی پلەیەر + کۆنترۆڵە ئەستوورەکانی پلەیەری ڤیدیۆ دەگێڕێتەوە بۆ شێوازە بینراوە کۆنتر و باریکترەکە بەڵكو لە ڕەوشتەوە پاشخانی كەشناسی دەربخە + چالاککردنی ئەمە دەکرێت ڕێگەیبکات بە بەکارهێنانی کوالێتی ڤیدیۆی بەرزتر چەستیكردنی ئەم شێوە دەتوانێت دەربازبوونی ڤیديۆ كەش بكەوێتە خوارووی، زۆر كاری شارژە زۆر بێت، و لاوازییەكانی نەزانراو دروست بكات پێدایەکی هێما (Haptic feedback) + دیاری بکە کام کارلێکەکان دەبنە هۆی لەرینەوەی ئامێرەکە بکوژین هەستی haptics بۆ بەشەکان عیبهی پێوەکەگەل تەقەدێر ڕێگە سازی ئىنى فچاو سارا كەلناته عیبهی دهوكەر ڕاستەگی ئىینی داخىلى ساراوە هۊندرو خه‌مه‌كانی پۆشینی ڕیوه‌ڵه‌وه‌ هه‌ڵده‌ده‌سته‌. عیبهی زۈم دەس نالوײەە تا ڕاۈە + ئەگەر لەم دواییانەدا زانیارییەکانی چوونەژوورەوەی هەژمارت گۆڕیوە، ئەوا MicroG بسڕەوە و دووبارە دایببەرهێنە + تێپەڕاندنی ئاراستەکردنی لینکەکان + لینکی ڕەسەنەکە ڕاستەوخۆ دەکاتەوە و لینکی شوێنکەوتن (youtube.com/redirect) تێپەڕ دەکات + کردنەوەی لینکەکان لە وێبگەڕ + لینکەکان لە وێبگەڕێکی دەرەکی دەستی پێ دەکات لەجیاتی وێبگەڕی ناو ئەپەکە + قەبارەی بەفەری پەخش بنەڕەت + نزم + ناوەند + زۆرترین + دوانەوەی ڤیدیۆ + ڤیدیۆکە دواناداتەوە بۆ ئەوەی لەگەڵ دەنگە خاوەکانی وەک ہیڵۆفۆنەکانی بلوتووس هاوسەنگ بێت (٠-٥٠٠٠ ملی چرکە) + تەنها دواکەوتن لەگەڵ ئۆدیۆی بلۆتوس + تەنها ڤیدیۆکە دوا دەکەوێت کاتێک ئۆدیۆی بلۆتوس بەستراوەتەوە، بۆ ئەوەی سپیکەری مۆبایلەکە لەگەڵدا هاوسەنگ بێت + پیشاندانی دوگمەی دواکەوتنی ڤیدیۆ + پێی بگرە بۆ گەڕاندنەوەی دواکەوتن بۆ 0 ms + پیشاندانی مینیۆی دواکەوتنی ڤیدیۆ + دواکەوتن بە میلی چرکە ئۆتە + کوالیتی ڤیدیۆکە بهێڵە وە ببیرێت + دیاریکردنی دەستی کوالیتییەکەت بۆ هەموو ڤیدیۆکانی داهاتوو جێبەجێ دەکات + ئاگادارکەرەوەکانی گۆڕانی کوالیتی + ئاگادارکردنەوەیەک پیشان دەدات کاتێک کوالێتی پێشنیارکراوی نوێ پاشکەوت دەکرێت + کوالێتی ڤیدیۆی پێشنیارکراو لە تۆڕی Wi-Fi + کوالێتی ڤیدیۆی پێشنیارکراو لە تۆڕی مۆبایل + کوالێتی Shorts بیربکە + هەڵبژاردنی دەستی کوالێتی تۆ بۆ هەموو Shorts-ەکانی داهاتوو جێبەجێ دەکات کیفیتی پیش‌نیازکراوی Shorts لە تۆڕی Wi-Fi کیفیتی پیش‌نیازکراوی Shorts لە تۆڕی مۆبایل + کوالێتی پێشنیارکراوی مۆبایل گۆڕدرا بۆ: %s + کوالێتی پێشنیارکراوی وایفای گۆڕدرا بۆ: %s + کوالێتی مۆبایلی Shorts گۆڕدرا بۆ: %s + کوالێتی Wi-Fiی Shorts گۆڕدرا بۆ: %s + دوگمەی خێرایی لایەنبردن نیشان بدە + پەنجە لێ بدە و ڕایگرە بۆ گەڕاندنەوەی خێرایی بۆ ئاسایی + دوگمەی پیشاندانی کوالێتی نیشان بدە + پەنجە لێبدە و بیگرە بۆ گەڕاندنەوەی کوالێتی بۆ دۆخی سەرەتایی + پیشاندانی جەستەی وێنە لەسەر دوگمەی کوالێتی + جەستەی وێنەکە پیشان دەدات، وەک ١٠٨٠، لەجیاتی ناونیشانێک وەک FHD + مێنیۆی خێرایی لێدانی تایبەت + گەڕاندنەوەی مێنیۆی کۆنی خێرایی لێدان پێکهاتەکانی خوارەوە + بژاردەی خێرایی لێدانی دڵخواهەکەت بنووسە + خێراییەکانی لێدانی تایبەت دەبێت کەمتر بن لە %s سوريت تي سرعيات باستي خودکار + پەنجە لێبدە و خێرایی لێدانەکە بیگرە + جێگیرکەری خێرایی دیاری بکە کاتێک شاشەکە دەگریت، بۆ ڕاگرتن دایبنێ لەسەر 0 + خێرایی لێدانەوە به মনে بهێڵە + هەڵبژاردنی خێرایی دەستکاریەکەت بۆ هەموو ڤیدیۆکانی داهاتوو جێبەجێ دەکات + ئاگادارکەرەوەکانی گۆڕانی خێرایی + ئاگادارکردنەوەیەک پیشان دەدات کاتێک خێرایی لێدانی پێشنیارکراوی نوێ پاشکەوت دەکرێت کیفیتی پیش‌نیازکراوی پێکهاتە + خێرایی پە perturbەی پێشنیارکراو لە Shorts + خێرایی لێدانی لە Shorts بیربکەوێت + هەڵبژاردنی دەستی خێرایی Shorts بۆ هەموو Shorts-ەکانی داهاتوو جێبەجێ دەکات. ئەگەر کوژرابێت، خێراییەکە دەگەڕێتەوە بۆ بارودۆخی سەرەکی دوای داخستنی پخشکەری Shorts + پیشاندانی خێرایی لێدان لە مینیوی Shorts + بڕەیەکی خێرایی لێدان زیاد دەکات بۆ مینیوی پخشکەری Shorts + خێرایی لێدانی · %s + خێرایی لێدانی پێشنیارکراو گۆڕدرا بۆ: %s + بەکارهێنانی خێرایی ئاسایی بۆ موزیک + هەمیشە ڤیدیۆکانی موزیک بە خێرایی 1.0x لێ بدە + لیستی ڕێگەپێدراوی خێرایی کەناڵ + ڤیدیۆکانی کەناڵە ڕێگەپێدراوەکان هەمیشە بە خێرایی 1.0x لێ دەدرێن + کەناڵ زیاد بکە بۆ لیستی ڕێگەپێدراوی خێرایی + کەناڵ لابەرە لە لیستی ڕێگەپێدراوی خێرایی + چالاککردنی کۆنترۆڵەکانی تۆنی دەنگ + تۆنی پێشنیاری دەنگی لێدانەوە + تۆنی پێشنیاری دەنگ گۆڕدرا بۆ: %s + کێشانی کاتی دەنگ + ڕێگە دەدات خێرایی و تۆن بە سەربەخۆیی دابنرێن، ڕەنگە ببێتە هۆی دروستبوونی دەنگە ناڕاستەقینەکان + بیرهاتنی شوێنی لێدانی پەخشی ڕاستەوخۆ + بۆ پەخشنە ڕاستەوخۆ بەردەوامەکان، شوێنی لێدانی تۆ بیردەگرێتەوە و لەوێوە دەست پێدەکاتەوە لەجیاتی ئەوەی بچێت بۆ شوێنی ئێستای ڕاستەوخۆکە + ئەم تایبەتمەندییە لەگەڵ پەخشنە ڕاستەوخۆکانی ٢٤/٧ و پەخشنەکانی تر کە \'هەمیشە کاردەکەن\' کار ناکات + بێکارکردنی ڤیدیۆی HDR + سەپاندنی ڤیدیۆی HDR + وا دەکات ئەپەکە ئەم ئامێرە وەک ئامێرێکی توانای HDR بناسێت + "ئەمە بەکاربهێنە ئەگەر دیکۆدکردنی HDR کاردەکات بەڵام YouTube HDR پێشکەش ناکات، وەک هەندێک ئامێری HyperOS کە HLG دەشارێنەوە. + +سەرهاتییەکان: +• ڕەنگەکان ڕەنگە هەڵە بن ئەگەر شاشەکە نەتوانێت بە ڕاستی HDR پیشان بدات +• ڕەنگە پەخشکردنەکە سەرکەوتوو نەبێت ئەگەر دیکۆدەرەکە نەتوانێت HDR مامەڵەی پێوە بکات +• HDR هێشتا بەستراوەتەوە بە کلایانتی سترێمی ڤیدیۆی spoof" + کۆدێکی ڤیدیۆ بنەڕەت + بۆ زۆردانی AVC، \'چەواککردنی ڕەوتی ڤیدیۆ\' بگۆڕە بۆ \'Android Studio\'، \'Android VR\' یان \'visionOS\' + "VP9 +• دەتوانێت تەمەنی باتری باشتر بکات و کێشەی لرزین لە کاتی لێدانی ڤیدیۆ چارەسەر بکات +• داتای ئینتەرنێتی زیاتر بەکاردێنێت وەک لە AV1 + +AVC (H.264) +• پێویستی بە کلاینتی چەواککردنی ڕەوتی ڤیدیۆی 'Android Studio'، 'Android VR' یان 'visionOS' هەیە +• دەتوانێت تەمەنی باتری زیاتر باشتر بکات +• دەتوانێت ڕەزۆلۆشنەکانی ونبووی ڤیدیۆ لە ئامێرە کۆنەکاندا گەڕێنێتەوە +• بەرزترین ڕەزۆلۆشن ١٠٨٠pـە +• ڤیدیۆکانی HDR ڕەنگە AVC بەکارنەهێنن +• هەندێک ئامێر ناتوانن AVC زۆر بکەن" + کردنەوەی مینی کوالێتی پێشکەوتوو + مەنۆی هەڵبژاردنی کوالێتی دەپەڕێتەوە و ڕاستەوخۆ مەنۆی کوالێتی پێشکەوتووە دەکاتەوە + شاردنەوەی بژاردەکانی کوالێتی Premium به‌هۆی باوه‌ڕی به‌رهه‌می ڤیدۆ، خاڵه‌کان پێشێل بکه‌. + پێشینەی بەرزترین کوالیتی ڤیدیۆی بەردەست دەدات بە کاریگەری کۆدێک + ئەگەر ئەم ڕێکخستنە کوژێنرا بێت، ئەوا ڤیدیۆ کۆنەکان ڕەنگە هیچ بژاردەیەکی کوالیتی ڤیدیۆ پیشان نادەن + ڕێگەبدە بە \'Android VR\' / visionOS AV1 + ڕێگە بە دیکۆدکردنی نەرمەک झालेی AV1 دەدات لە کاتی بەکارهێنانی کلایانتی \'Android VR\' / visionOS "چالاکی کردنی ئەم ڕێکخستنە دەکرێت بەکارهێنانێک لە دەربڕەخستنی نەرمەکاری AV1" لائەكانی دروستكردن + • ڕەنگە لێدانی ڤیدیۆ لە خولەکێکی دوای دەستپێکردندا تووشی کێشە بێت + • مێنیووی \'Audio Track\' نەماوە • ناکۆكی كۆدێكی ڤیدیۆ ڤێ (AV1) نییه + • شێوازی immersive VR 360° بەردەست نییە • دەربارەی پێشبڕکێی دەنگی پاوەستادار، ئەو پێشبڕکێی پێشبڕکێ دەست نەکەوێت + • ڕەنگە پەخشکردن ١ چرکە پێش کاتی تەواوبوون کۆتایی بێت • ئەگەر لەشێربوونه‌وه ناکه‌وێت، نمایشکردن دەکرێتە خاڵی خه‌ڵوه‌ت • دەنگی سەرەکیی تایبەت ناخۆش بە دەستەبەرە + • چوونەژوورەوە بۆ Android VR پێویستە + • ڕەنگە کوالێتییەکە بە 720p سنووردار بکرێت ژیان JavaScript-ی خاس + پیشاندانی فێڵکردن لە \'Stats for nerds\' + گەڕاندنەوەی شریتی کرداری کۆنی ڤیدیۆ + "چارەسەرکردنی کێشەی نەشاردراوی دوگمەکانی کرداری ڤیدیۆ + +سەنگەست: دەگەڕێتەوە بۆ شێوازی کۆنی 'ڤیدیۆی زیاتر'" + ستایلی ئایکۆنی پلەیەر خۆکار بنەڕەت + تەنک + تەخام باو + ستایلی ئایکۆنی Shorts + شوێنکەوتووی زنجیرەکان + پلی لیستەکان وەک زنجیرە شوێن بکە و شوێنی خۆت بپارێزە + چالاککردنی بەدواداچوونی زنجیرەکان + لاپەڕەی زنجیرەکان زیاد دەکات بۆ مێژوو و بژاردەی بەدواداچوونی زنجیرەکان زیاد دەکات بۆ لیستەکانی لێست + پیشاندانی زنجیرەکان لە شریتی گەڕان + پیشاندانی دوگمەی بەدواداچوونی زنجیرەکان + دوگمەیەکی دابەزاندنی پلەیەر زیاد دەکات بۆ شوێنکەوتنی یان کردنەوەی زنجیرەی ڤیدیۆکەی ئێستا + تۆمارکردنی پێشکەوتنی زنجیرە شوێنکەوتراوەکان + تۆمارکردنی لایەنی لێدانی لەم مۆبایلە بۆ زنجیرە پاشکەوتکراوەکان و نیشانەکان پاشگەز + پاشکەوتکردن + پاشکەوتکردنی نیشانە + ناوی زنجیرە + بژاردەکانی زنجیرە + بژاردەکانی کتێبخانە + نوێکردنەوەی ئەپیزۆدانەکان + گۆڕینی ناو + کردنەوەی لیستی گوێستن لە YouTube + سڕینەوە لە کتێبخانە + لینکی لیستی گوێستن یان ناسنامە + سڕینەوەی هەموو پێشکەوتنە ناوخۆییەکان + سڕینەوەی مێژوو + هەموو پێشکەوتنە ناوخۆییەکان بسڕێیتەوە؟ + ئەمە شوێنەکان، گۆڕانکارییەکانی بینراو و هەڵبژاردنەکانی بەردەوامبوون بۆ هەموو ڤیدیۆیەکی پاک دەکاتەوە. زنجیرە هەڵگیراوەکان و کاتالۆگەکانیان دەمێننەوە. لێدانی نوێ تۆمار دەکرێت ئەگەر تۆمارکردن چالاک بێت + نیشانەی بینراو دابنێ + نیشانەی نەبینراو دابنێ + بەکارهێنانی ستاتوسی ئۆتۆماتیکی + دووبارە دەستپێکردنەوە لە 0:00 + دووبارە لێدەریانە لە 0:00 + بارودۆخەکە پاشەکەوت کرا + دووبارە هەوڵ بدەرەوە بۆ پاشەکەوتکردن + ١٠٠ پێشوو + ١٠٠ داهاتوو + کردنەوەی ڤیدیۆی پاشەکەوتکراو + پێشکەوتی ناوخۆیی پاککراوە + مێژووی بینین بکەرەوە لە بەشی \"تۆ\" بۆ بینینی زنجیرەکانت + مێژووی بینین + زنجیرەکان + بارکردنی زنجیرە پاشەکەوتکراوەکان… + دووڤی لیستێک دابگرە بۆ بەدواداچوونی ئەڵقەکانی + نیشانەی لیستی دەنگوگوشار + نوێکردنەوەی ئەڵقەکان… + هەموو زنجیرەکان + بینراوە + بارکردنی ئەڵقەکان… + زیاتر لە هەڵبژاردەکان + "تەنها پێشکەوتنەکانی ئەو زنجیرانە و ڤیدیۆیانە تۆمار بکە کە پاشکەوتت کردووە + +ئەم کتێبخانەیە لەسەر ئەم ئامێرە پاشکەوت دەبێت و لەنێوان هەژمارەکانی YouTubeدا بەش دەکات. کۆنترۆڵەکانی ڕاگرتن و پاککردنەوەی مێژووی YouTube کاریگەری لەسەر نابێت. بۆ سڕینەوەی، بەکارھێنانی Series ← More ← Clear local progress بکە + +تۆمارکردنی ئۆتۆماتیکی لە شێوەی incognito دەوەستێت، یان کاتێک ناسنامەی هەژمارەکە بەردەست نەبێت، یان کاتێک بگۆڕیت بۆ هەژمارێکی تر. دووبارە بەکارهێنانی بەدواداچوون چالاک بکەرەوە بۆ ئەوەی لەگەڵ ئەو هەژمارە بەکاریبهێنیت" + ناتوانرێت بەدواداچوون لە شێوەی incognito یان پێش ئەوەی YouTube دۆخی هەژماری ئێستا باربکات چالاک بکرێت. بگەڕێوە بۆ سەرەتا و دووبارە هەوڵ بدەرەوە. پێویست بە چوونەژوور نییە + بەدواداچوونی خۆی چالاک بکە + تۆمارکردنی پێشکەوتن بەستراوە + دووبارە دەستپێبکەرەوە لە %1$s + %1$s / %2$s ئەڵقە بینراوە + ئەڵقەی %1$s · %2$s + ئەڵقەکانی %1$s-%2$s لە %3$s + نوێکردنەوە بکە بۆ تاقیکردنەوەی بارکردنی ئەپیزۆدانەکان. لیستە نوێوەی تایبەت پێویستیان بە دەستڕەسی هەژمارە؛ نیشانەکەیان هێشتا لە YouTube دەکرێتەوە + YouTube نەیتوانی ئەم ئەپیزۆدەی بکاتەوە + YouTube نەیتوانی ئەم لیستە نوێوەیە بکاتەوە + ئەپیزۆدێک هەڵبژێرە + نوێکراوەتەوە + هیچ ئەپیزۆدێکی قابل بە لایەنەوە نییە + ڤیدیۆی پاشکەوتکراو · %1$s + بەدواداچوونی زنجیرەکە + بینینی زنجیرە بەردەوامبوون + لێرەوە دەست پێبکە + خاڵی دەستپێک پاشکەوت کرا + تا لێرە بینراوە + ئەڵقە بینراوەکانی نیشانە کراوە گەڕانەوە + چالاککردن + %1$s ئەپیزۆدی نوێ + تۆمارکردنی پێشکەوتن لەسەر ئەم ئامێرە + "زنجیرەکان و پێشکەوتنەکەت لەسەر ئەم ئامێرە دەمێننەوە، و لە نێوان هەژمارەکانی YouTube دابەش دەبن. کۆنترۆڵەکانی مێژووی YouTube گۆڕانکارییان تێدا ناکەن. پێشکەوتنەکان لە مینیووی زنجیرەکان پاککەرەوە + +تۆمارکردنی ئۆتۆماتیکی ئارەزووە. لە شێوەی نادیار یان کاتێک هەژمارەکە دەگۆڕدرێت دەوەستێت" + پێشەکان نەتوانرا بار بکرێن. دەتوانیت لیستی لاسایەتییەکە وەک نیشانەی پەڕە پاشکەوت بکەیت + گۆڕانکاریی زۆر هەن کە پاشکەوت نەکراون. سەرەتا هەوڵ بدە دیسان پاشکەوتی بکەیت + پاشکەوتکردن سەرقاڵە. دووبارە هەوڵ بدەرەوە + بارکردنی ئەڵقەکە سەرقاڵە. دووبارە هەوڵ بدەرەوە + کۆگاکە بەردەست نییە. داتاکانی پاشکەوتکراوت پارێزراون + گۆڕانکارییەکان نەتوانرا پاشکەوت بکرێن. پێش داخستنی YouTube دووبارە هەوڵ بدەرەوە بۆ پاشکەوتکردن + ئەم زنجیرەیە پێشتر پاشکەوت کراوە، یان لیستەکەی گۆڕاوە. دووبارە بیبەرەوە + ئەم زنجیرەیە گۆڕاوە یان لابراوە. دووبارە کتێبخانەکە بکەرەوە + لیستی ڤیدیۆکان گۆڕاوە. دووبارە ئەڵقەکە هەڵبژێرەوە + ئەم ئەڵقەیە بەردەست نییە. دانەیەکی تر هەڵبژێرە + ناوی زنجیرەیەک بنوسە + لینکی لیستی پاشکەوتەکەی YouTube یان ناسنامەی لیستی پاشکەوتەکە بنوسە + تکایە پێش ئەوەی دیسان نوێ بکەیتەوە چاوەڕوان بە + پێشەکان نەتوانرا لۆد بکرێن. دواتر هەوڵ بدەرەوە + گۆڕانکارییەکە نەتوانرا تەواو بکرێت. دیسان هەوڵ بدەرەوە + نەتوانرا دۆخی بەردەوامبوون پشتڕاست بکرێتەوە. پەیەرەکە پشکنین بکە و دیسان هەوڵ بدەرەوە + سێرییە شوێنکەوتراوەکان + ئەم ڤیدیۆیە لەم لیستە ڤیدیۆییەدا نییە + گەڕان بەدوای لیستەکانی دەنگ و وێنەدا... + لینکەکەی لیستی دەنگ و وێنەکە لێرە دابنێ + نەتوانرا لیستی دەنگ و وێنەیەک بدۆزرێتەوە + هیچ ئەڵقەیەک بەردەست نییە + شاردنەوەی بینراوەکان + نیشانەکردنی ئەڵقەکانی پێشوو وەک بینراو + دەست پێ دەکات بە: %1$s + ڕیزبەندی ئەڵقەکان + ڕیزبەندی لیستی دەنگوگۆ + پێچەوانەکردنی ڕیزبەندی لیستی دەنگوگۆ + %1$s بینین + %1$s بینین + نیشانەدانی ئەڵقە بینراوەکان + لانی کەم %1$s%% بینراوە و زۆترینما ٩٠ چرکەی ماوە، یان %2$s چرکەی تری ماوە + بە شێوەیەکی ئۆتۆماتیکی ئەڵقەیەک وەک بینراو نیشانە بدە دوای گەیشتن بە هەر یەکێک لە هەردوو ڕێژەکە. یاسای ڕێژەی سەدی هەروەها داوا دەکات کە زیاتر لە ٩٠ چرکە نەمێنێت، بۆ ئەوەی کۆتایی ئەڵقە درێژەکان لێیان نەپەڕێت. یاسای کاتی ماوەوە بۆ ئەو ڤیدیۆیانەیە کە زیاتر لە یەک خولەک بن؛ بۆ ڕایاندان دایبنێ بە ٠. گۆڕانکارییەکان بۆ نوێکردنەوەی پێشکەوتنەکانی داهاتوو جێبەجێ دەبن. هەڵبژاردنە دەستییەکانی بینراو/نەبینراو دەمێننەوە + ڕێژەی بینراو (١-١٠٠) + چەند چرکەیەک ماوە (٠-٣٠٠؛ ٠ بێکار دەکات) + بهایەک بنوسە لە %1$s تا %2$s + بەکارهێنانی پێشکەوتنی YouTube + بەردەوامبە لە زنجیرە شوێنکەوتراوەکان دوای بینین لە ئامێرێکی تر + بارودۆخی بینراو و شوێنی بەردەوامبوون بۆ ئەو زنجیرانەی شوێنکەوتوونیان کردووی لە هەژماری ئێستای YouTube بخوێنرێتەوە. تەنها ئەو ئەڵقایانەی گونجاوە لەسەر ئەم مۆبایلە پاشکەوت دەکرێن. پشکنینەکان کاتێک دەکرێن کە کتێبخانەی زنجیرەکان کراوە و پێش بەردەوامبوون. مێژووی YouTube دەبێت لە ئامێرەکەی تری تۆدا چالاک بێت + پشکنینی پێشکەوتنی YouTube... + پێشکەوتنی YouTube بەردەست نییە. بەکارهێنانی پێشکەوتنی پاشکەوتکراو + هەندێک لە پێشکەوتنی YouTube بەردەست نییە. شوێنە پاشکەوتکراوەکان دەمێننەوە + پشتگیری و گەڕاندنەوە + کتێبخانەی زنجیرەکانی خۆت لە فایلێکدا پاشکەوت بکە یان پشتگیرییەک گەڕанەوە بکە + زنجیرە شوێنکەوتراوەکان، شوێنی ئەڵقاکان و هەڵبژاردنی بینراوەکان پاشکەوت دەکات. مۆڵەتی تۆمارکردن و پێشکەوتنی YouTube لە ناوەدا نین + پاشکەوتکردنی کۆپییە یەدەگ + گەڕانەوەی پشتپشتی + زیادکردنی %1$s زنجیرە. %2$s زنجیرە کە پێشتر لە کتێبخانەکەتدا بوون دەمێننەوە. پێشکەوتنی ڤیدیۆکانی ئێستا دەمێننەوە + پشتپشتی زنجیرەکە پاشکەوت کرا + گەڕانەوەی %1$s زنجیرە + ئەم فایلە پشتەباکی زنجیرەیەکی پشتگیرنەکراو نییە، یان زۆر گەورەیە. کتێبخانەکەت گۆڕانکاری تێدا نەکرا + نەتوانرا پشتەباکەکە پاشکەوت بکرێت. شوێنێکی تر تاقیبکەرەوە + زنجیرەکە لابروا + ئەم ئەڵقەیە ئێستا زیاتر لە جارێک دەرکەوت. هەڵبژێرە لە کامەیانەوە بەردەوام بیت + ئەپیزۆدەکەی تۆی پاشەکەوت کردبوو ئێستا لەم لیستەپەیڵستەیەدا نییە. شوێنی پاشەکەوتکراوی تۆ پارێزراوە + بەردەوامبە لەگەڵ ئەپیزۆدی %1$s · %2$s + دەتوانی هەر ئەپیزۆدێک لە خوارەوە هەڵبژێریت. ئەپیزۆدەکانی پێشوو دەستکاری نەکراون + ئەم لیستەپەیڵستەیە گۆڕاوە. زنجیرەکە بکەرەوە بۆ ئەوەی هەڵبژێریت لە کوێوە بەردەوام بیت + هیچ ئەڵقەیەکی تر لەم ئاراستەیەدا نییە + بەردەوام بە لە %1$s + گەڕان بۆ زنجیرەکان + پاککردنەوەی گەڕان + هیچ زنجیرەیەک نەدۆزرایەوە + ئەپیزۆدێکی بێ ناوی + ئەپیزۆدێکی نەبەردەست + کۆمێنتەکانی Niconico + چاتی ڕاستەوخۆ و دووبارە لێدانی چات وەک کۆمێنت پیشان بدە کە لەسەر ڤیدیۆکە دەجوڵێن + دوگمەیەکی سەرپۆش لەسەر لێدەرە ڤیدیۆییەکە پیشان دەدات. بۆ پیشاندان یان شاردنەوەی کۆمێنتەکان کلیک بکە، بۆ ڕێکخستنی خێرا کلیک بکە و بیگرە + قەبارەی فۆنتی کۆمێنت + قەبارەی تێکستی کۆمێنتەکان (12-40 sp) + لێڵیی کۆمێنت + ڕوونی دەقی کۆمێنت و پاشبنەماکان + ماوەی گەشت + چەند چرکە کۆمێنتەکان پێ دەوێت بۆ بڕینی شاشەکە. نرخە نزمەکان خێراتر دەجولێن + درێژترین کۆمێنت + ئەو کۆمێنتانە دەشارێتەوە کە ژمارەی پیتەکانیان لەم بڕە زیاتر بێت. هەر ئیمۆجییەک وەک یەک پیت هەژمار دەکرێت + بێ سنوور diff --git a/patches/src/main/resources/addresources/values-cs-rCZ/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-cs-rCZ/shared-youtube/strings.xml index afd8699a58..a40928309a 100644 --- a/patches/src/main/resources/addresources/values-cs-rCZ/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-cs-rCZ/shared-youtube/strings.xml @@ -21,9 +21,8 @@ Second \"item\" text" Animace při spuštění Vypnuto Barva - Barevné Morphe Monochrom - Monochromatické Morphe + Restartovat pro dokončení nastavení diff --git a/patches/src/main/resources/addresources/values-cs-rCZ/youtube/strings.xml b/patches/src/main/resources/addresources/values-cs-rCZ/youtube/strings.xml index d48ee0d2de..69201bc5cd 100644 --- a/patches/src/main/resources/addresources/values-cs-rCZ/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-cs-rCZ/youtube/strings.xml @@ -113,6 +113,9 @@ Změny zahrnují: Skrýt průzkumy Skryje průzkumy a pobídky týkající se přesnosti doporučeného obsahu Skrýt sekci se vstupenkami + + Skrýt banner „Nedostupná videa jsou skryta“ + Skryje banner „Nedostupná videa jsou skryta“ v playlistech Skrýt čas nahrání "Omezení: @@ -1247,6 +1250,9 @@ Omezení: Pokud je vypnutá možnost 'Obnovit starou lištu akcí videa', překr Udržuje konzistentní rozvržení stavového řádku, takže nezakrývá horní lišty a nezanechává prázdný prostor nad přehrávačem. Toto vypněte, pokud aplikace havaruje nebo pokud horní lišty vypadají špatně Záře + Morphe Glow 1 + Morphe Glow 2 + Tento zařízení nepodporuje zářící obrazovku při spuštění Pozadí světlého motivu Bílá Material You (bílá) diff --git a/patches/src/main/resources/addresources/values-da-rDK/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-da-rDK/shared-youtube/strings.xml index b7b00009dd..4cc2b49dac 100644 --- a/patches/src/main/resources/addresources/values-da-rDK/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-da-rDK/shared-youtube/strings.xml @@ -21,9 +21,8 @@ Second \"item\" text" Startanimation Deaktiveret Farve - Farve Morphe Monokrom - Monokrom Morphe + Genstart for at færdiggøre opsætningen diff --git a/patches/src/main/resources/addresources/values-da-rDK/youtube/strings.xml b/patches/src/main/resources/addresources/values-da-rDK/youtube/strings.xml index ea8ddaa4f9..a60de6ee8c 100644 --- a/patches/src/main/resources/addresources/values-da-rDK/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-da-rDK/youtube/strings.xml @@ -113,6 +113,9 @@ Nogle server-side layouts kan måske ikke gå tilbage. Skjul undersøgelser Skjuler undersøgelser og forslag vedrørende nøjagtigheden af feedanbefalinger Skjul billetreolen + + Skjul banneret \"Ikke tilgængelige videoer er skjult\" + Skjuler banneret \"Ikke tilgængelige videoer er skjult\" i playlister Skjul uploadtidspunkt "Begrænsninger: @@ -1245,6 +1248,9 @@ Begrænsning: Hvis 'Gendan gammel video-handlingslinje' er slået fra, er 'Flere Gør layoutet af statuslinjen konsistent, så det ikke dækker de øverste felter og ikke efterlader tom plads over afspilleren. Slå fra, hvis appen krascher, eller de øverste felter ser forkerte ud Glød + Morphe Glow 1 + Morphe Glow 2 + Glow-splashskærm understøttes ikke af denne enhed Lyst tema baggrund Hvid Material You (Hvid)! diff --git a/patches/src/main/resources/addresources/values-de-rDE/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-de-rDE/shared-youtube/strings.xml index 653f29dbf9..68de89c93d 100644 --- a/patches/src/main/resources/addresources/values-de-rDE/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-de-rDE/shared-youtube/strings.xml @@ -21,9 +21,8 @@ Second \"item\" text" Startanimation Deaktiviert Farbe - Farbiges Morphe Einfarbig - Monochromes Morphe + Starte die App neu, um die Einrichtung abzuschließen diff --git a/patches/src/main/resources/addresources/values-de-rDE/youtube/strings.xml b/patches/src/main/resources/addresources/values-de-rDE/youtube/strings.xml index 01684ed801..016f178874 100644 --- a/patches/src/main/resources/addresources/values-de-rDE/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-de-rDE/youtube/strings.xml @@ -113,6 +113,9 @@ Folgende Änderungen sind enthalten: Umfragen ausblenden Blendet Umfragen und Hinweise zur Genauigkeit der Feed-Empfehlungen aus Ticket-Bereich ausblenden + + Banner \"Nicht verfügbare Videos sind ausgeblendet\" ausblenden + Blendet den Banner \"Nicht verfügbare Videos sind ausgeblendet\" in Playlists aus Upload-Zeit ausblenden "Einschränkungen: @@ -1250,6 +1253,9 @@ Einschränkung: Wenn \"Alte Video-Aktionsleiste wiederherstellen\" deaktiviert i Behält das Layout der Statusleiste bei, sodass sie die oberen Leisten nicht überdeckt und über dem Player kein leerer Raum entsteht. Schalten Sie diese Option aus, wenn die App abstürzt oder die oberen Leisten falsch aussehen Leuchten + Morphe Glow 1 + Morphe Glow 2 + Glow-Splash-Screen wird von diesem Gerät nicht unterstützt Hintergrund des hellen Designs Weiß Material You (Weiß) diff --git a/patches/src/main/resources/addresources/values-el-rGR/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-el-rGR/shared-youtube/strings.xml index 09f1e211df..77f73e129f 100644 --- a/patches/src/main/resources/addresources/values-el-rGR/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-el-rGR/shared-youtube/strings.xml @@ -21,9 +21,9 @@ Second \"item\" text" Κινούμενο σχέδιο εκκίνησης Απενεργοποιημένο Χρωματιστή - Έγχρωμο Morphe Μονόχρωμο - Μονόχρωμο Morphe + + Ανενεργό Επανεκκινήστε για να ολοκληρωθεί η διαδικασία diff --git a/patches/src/main/resources/addresources/values-el-rGR/youtube/strings.xml b/patches/src/main/resources/addresources/values-el-rGR/youtube/strings.xml index 9b8fcad100..4827b39d99 100644 --- a/patches/src/main/resources/addresources/values-el-rGR/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-el-rGR/youtube/strings.xml @@ -113,6 +113,9 @@ Second \"item\" text" Έρευνες Κρύβει έρευνες και προτάσεις σχετικά με την ακρίβεια των προτάσεων ροής Ενότητα εισιτηρίων + + Απόκρυψη του banner \"Τα μη διαθέσιμα βίντεο είναι κρυμμένα\" + Κρύβει το banner \"Τα μη διαθέσιμα βίντεο είναι κρυμμένα\" στις λίστες αναπαραγωγής Χρόνος μεταμόρφωσης στη ροή και στα αποτελέσματα αναζήτησης "Περιορισμοί: @@ -1249,6 +1252,9 @@ Second \"item\" text" Διατηρεί τη διάταξη της μπάρας κατάστασης συνεπή, ώστε να μην καλύπτει τις πάνω μπάρες και δεν αφήνει κενό χώρο πάνω από τον παίκτη. Απενεργοποιήστε αυτό αν η εφαρμογή καταρρεύσει ή οι πάνω μπάρες φαίνονται λάθος Λάμψη + Morphe Glow 1 + Morphe Glow 2 + Η οθόνη έναρξης με λάμψη δεν υποστηρίζεται από αυτή τη συσκευή Φόντο φωτεινού θέματος Λευκό Material You (Λευκό) diff --git a/patches/src/main/resources/addresources/values-es-rES/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-es-rES/shared-youtube/strings.xml index 2af080645d..c98bf9befb 100644 --- a/patches/src/main/resources/addresources/values-es-rES/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-es-rES/shared-youtube/strings.xml @@ -21,9 +21,11 @@ Second \"item\" text" Animación de inicio Desactivado Color - Color Morphe Monocromático - Morphe monocromo + + Destacar nuevos ajustes + Número de días que se muestra un punto junto a los ajustes y las opciones añadidos por una actualización + Desactivado Reiniciar para finalizar la configuración diff --git a/patches/src/main/resources/addresources/values-es-rES/sponsorblock/strings.xml b/patches/src/main/resources/addresources/values-es-rES/sponsorblock/strings.xml index 9c061ac19a..e8799f4475 100644 --- a/patches/src/main/resources/addresources/values-es-rES/sponsorblock/strings.xml +++ b/patches/src/main/resources/addresources/values-es-rES/sponsorblock/strings.xml @@ -58,6 +58,8 @@ Second \"item\" text" Ya leídas Muéstrame General + Guardar segmentos de vídeos descargados + Permite omitir los segmentos de los vídeos descargados sin conexión Mostrar alertas de error de conexión Muestra una notificación cuando el servidor de SponsorBlock no esté disponible Activar seguimiento del conteo de omisiones diff --git a/patches/src/main/resources/addresources/values-es-rES/youtube/strings.xml b/patches/src/main/resources/addresources/values-es-rES/youtube/strings.xml index ed620d1fd2..69372ead21 100644 --- a/patches/src/main/resources/addresources/values-es-rES/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-es-rES/youtube/strings.xml @@ -113,6 +113,9 @@ Los cambios incluyen: Ocultar encuestas Oculta encuestas y sugerencias sobre la precisión de la recomendación de contenido Ocultar estantería de tickets + + Ocultar el banner \"Los videos no disponibles están ocultos\" + Oculta el banner \"Los videos no disponibles están ocultos\" en las listas de reproducción Ocultar hora de subida "Limitaciones: @@ -1248,6 +1251,7 @@ Limitación: si la opción 'Restaurar antigua barra de acciones de vídeo' está Mantiene la uniformidad del diseño de la barra de estado, de modo que no cubra las barras superiores y no deje ningún espacio vacío encima del reproductor. Desactiva esta opción si la aplicación se bloquea o las barras superiores no se ven bien. Resplandor + La pantalla de inicio con efecto de resplandor no es compatible con este dispositivo Fondo del tema claro Blanco Material You (Blanco) diff --git a/patches/src/main/resources/addresources/values-et-rEE/music/strings.xml b/patches/src/main/resources/addresources/values-et-rEE/music/strings.xml index ccfea4ed71..c352bf37a7 100644 --- a/patches/src/main/resources/addresources/values-et-rEE/music/strings.xml +++ b/patches/src/main/resources/addresources/values-et-rEE/music/strings.xml @@ -17,58 +17,675 @@ Second \"item\" text" --> + Viimsi esitatud + Tellimused + Videod + Muud tulemused + Üleslaaditud laulud + Üleslaaditud albumid + Üleslaaditud artistid + Uuenda hosti Jam-pätšit, et kasutada pause ja jätkamist + Jam-loetelistu jagamine + Loo lähedal Jam või liitu sellega ning muuda selle owni Jam Layeri kaudu + Activeeri Jam-loetelistu jagamine + Enablerib Jam-integratsiooni pärast YouTube Musici taaskäivitamist + Laadi alla Jam Layer, et see töötaks + Ava uusim Jam Layeri versioon oma brauseris + Jam-juhtimine + Loo lähedal Jam või liitu sellega ning owni sessiooni + Jam Layer pakett + Vali installitud kaasutav rakendus lähedase owni jagamiseks + Kasuta installitud kaasutavat paketti. Paarimine nõuab teie heaks kiitmist ja kohalikku võimekuse tõendit + Lülitada Wi-Fi sisse? + Lülita Wi-Fi sisse ja proovi Jam-i käivitamist või sellega liitumist uuesti + Ava Wi-Fi seaded + Loobu + Kasuta paketti + Sisestage korrektne Androidi paketi nimi + Pakett on salvestatud. Ühendamiseks seadistage Jam Layer + Jam on katkestatud + Aktiveerige esmalt Jam Layer + Aktiveerige seadetest Jam-i järjekorra jagamine ja taaskäivitage seejärel YouTube Music + Installige esmalt valitud Jam Layeri pakett + Sisestage hostis kuvatav kood. Wi-Fi Aware suudab ühendada lähedal asuvad seadmed isegi siis, kui nende LAN-võrgud erinevad + Liitu koodiga + Liitu + Skanni QR-kood + Sisestage kaheksa sümboli pikkune kood või kleepige kutse + Liitu oma Jamiga + Kopeeri kood + Jami kood + Näita QR-koodi + Valmis + Kutsu oma Jamisse + Skanni, et liituda selle järjekorraga ja seda redigeerida. Jam-i lõpetamine tühistab kutse + Kopeeri link + Jami kutse + Jami ühendatakse uuesti; proovi hetke pärast uuesti + Lisatud Jamisse + Jami pole saadaval; rada ei lisatud + Sinu Jam + Kuulatakse koos + Liitutakse Jamiga + Kuulake koos + Uuendatakse Jammit… + Otsitakse hosti Wi-Fi võrgust… + O’ pea inimesi + %s isik on ühendatud + %s isikut on ühendatud + Sisesta see kood lähedases seadmes. Wi-Fi Aware töötab erinevate kohalike võrkude vahel, kui see on toetatud. Kehtivus lõpeb %s minuti pärast + Kerida kohale %s? + Muudatused lubatud + Muudatused lukustatud + Ühendatud Wi-Fi Aware\'i kaudu + Ühendatud Wi-Fi kaudu + Ühendatud Bluetoothi kaudu + Jam\'i ajal ei toetata + Liitumine katkestatud + Ühendatakse uuesti hostiga… + Jaga järjekorda. Lase kõigil lugusid lisada + Kutsu inimesi + Külalised saavad järjekorda redigeerida + Lõpeta Jam + Muusikat mängib võrgutsida. Lisa, liiguta või eemalda lugusid otse YouTube Musici järjekorrast + Hoidke mõlemad seadmed lähedal. Saate toimingu katkestada, kuid me otsime võrgutsida + Katkesta liitumine + Lahku jammist + Alusta jammist + Seadista jammi kiht + Halda + Üksikasjad + Ava + Laaditakse lauluvalikuid… + Lauluvalikud pole saadaval; proovige uuesti + Lauluvalikuid ei õnnestunud avada + Jam Layeri allalaadimiseks pole saadaval lendikut + Oodatakse võrgupostitajat + Avage esmalt lugus YouTube Musicis + Esita võrgumajandaja seadmes + Lõpeta Jam ja esita kohalikult + Esita Jami järgmist lugust + Lisa Jami loendisse + Esita seda Jami lugust + Vali esitusseade + Liiguta võrgumajandaja roll siia + Laul ei saanud otsinguks valmis + Loend muutus; proovi žestil toimingut uuesti + Pealkirja logo + Vaikimisi + Koolitatud + Esita albumi heliradu + Esita laulude albumi heliversiooni muusikavideo asemel. Võib nõuda funktsiooni \"Spoof video streams\" + Luba audio/video lülitus + Proovi vahetada heliraja versiooni ja ametlikku muusikavideot + Laadi alla muusikavideoid, podcaste või lugusid kolmandate osapoolte allalaadimisrakendustega + Kasuta allalaadimise menüü jaoks välise allalaadijat + Menüü allalaadimise valik avab selle asemel sinu välise allalaadija + Allalaaditud + "Puuduvad allalaadimised +Allalaaditud trekid kuvatakse siin" + %1$s • %2$s • %3$s MB + %1$s trekki • %2$s MB + Otsi allalaaditudest + Sorteeri + Sorteeri artisti järgi + Sorteeri pealkirja järgi + Sorteeri suuruse järgi + Kustuta kõik allalaaditud failid + Kustutada kõik allalaaditud failid? + Kõik allalaaditud rajad kustutatakse sellest seadmest + Allalaaditud failid kustutati + Midagi ei leitud + Tegevused elemendi %s jaoks + Esita + Esita või peata + Eelmine + Järgmine + Paus + Sulge + Kustuta + Kustuta allalaaditud fail + Kustutada allalaaditud fail? + %s kustutatakse sellest seadmest + Allalaaditud fail on kustutatud + Allalaaditud faili ei õnnestunud kustutada + Allalaaditud faili ei õnnestunud esitada + Audiofailide allalaadimine + Võrguta taastoidek + Allalaaditavat rada ei õnnestunud tuvastada + Allalaadimine on juba käigu + Juba allalaaditud + Allalaadimine alustatud + Alustatakse allalaadimist… + Laaditakse %s%% + Allalaadimine lõpule viidud + Allalaadimine ebaõnnestus + Rakenduse kohta + Reklaamid + Uuudisvoog + Üldine + Player + Laulud + Muu + YouTube Musici keel + Süsteemi keel + Võib nõuda Androidi versiooni 13 või uuema + Peida etikett \"Hangi Music Premium\" + Peida Music Premium reklaame + Peida videoreklaamid + Peida heli/video lüliti + Peida alllaadimunupp + Peidab podcasti saates allaadimunupu + Peida avastamise riiul + Peida filtri riba + Peida ruudiku kujulise riiulid + Peida horisontaalsed riiulid + Peida loendi kujulised riiulid + Lüürikupaneel + Peida jaga nupp + Peida tõlkimise nupp + Peida riiul \"Uut kellelt\" + Peida playlistide riiulid + Peida kordamise nuppu + Peida segamise nuppu + Peida riiul \"Kiirvalim\" + Kommentaarid + Valige, millised komponendid kommentaaride jaotises kuvatakse + Peida kogukonna reeglid + Peida kontekstisõnum + Peida emoji nuppu + Peida info nuppu + Peida ajatempli nuppu + Crossfade + Segage rajad sujuvalt kokku + Luba üleminek + Häivitumiskõver + Võrdne võimsus + Häälestatud hoidik + Pehmelt aeglustatud + Sile S-kuver + Välisev + Sissetulev + Ülemineku kestus + 250 millisekundit + 500 millisekundit + 750 millisekundit + 1 sekund + 2 sekundit + 3 sekundit + 4 sekundit + 5 sekundit + 6 sekundit + 7 sekundit + 8 sekundit + 9 sekundit + 10 sekundit + 11 sekundit + 12 sekundit + Kestvuse üleminek manuaalse hüppe korral + Rakendab ristikandumisefekti, kui hüppate käsitsi järgmisele looile + Kestvuse üleminek automaatse edendamise korral + Rakendab ristikandumisefekti, kui loo valib loomupärast lõppeb + Seansi juhtimine + Suruge segamise nuppu, et ristikandumine peatada või jätkata + Üleminek on selle seansi jaoks peatatud + Krossfade on jätkatud + Video režiim ei ole saadaval, kui ristikandumine on aktiveeritud + SABR-taastuse ajal on haangimine väljas + Voimimise ajal on haangimine väljas + Kuidas see toimib + "Crossfade üleskisi ühe laulu lõpu ja järgmise alguse, kasutades teie poolt määratud kestust ja kõverust + +See toimib automaatse edendamise ja manuaalse hüppe ajal, millest kummalgi on oma lüliti. Automaatse edendamise korral algab segamine mõne sekundi enne laulu lõppmist" + Piirangud + "Kui haangimine on sisse lülitatud, esitatakse muusikavideoid ainult helina. Pidevage segamise nuppu, et haangimine peatada ja videot vaadata + +Muusikavideo, mille esitus on juba alganud, jääb videorežiimi kuni järgmise lauluni + +Laule, mis on lühemad kui haangimise kestus, ei haangita + +Lukusekreet või teavitus võib ülemineku ajal korraks näidata aegunud olekut + +Muusikavideo vaatamiseks haangimise peatamine võib jätta mõned videod mustaks. Selle vältimiseks peatke haangimine enne laulu valimist" + Ei toetata + "Heli voimimine või peegeldamine (Chromecast, ekraani või heli peegeldamine) + +SABR-taastus (video-voimiste imiteerimine on väljas)" + Autor + Üleminekute funktsiooni looja on VazerOG + Kordamise olekku mäletamine + Segamise olekku mäletamine + Taastoomise kiirus + Kiirus, mida kasutatakse kõigi lugude jaoks. 100% juures kasutatakse rakenduse kiirust, näiteks podcastide taesetuse kiirust + Muuda toon kiirusega + Keela mitmeolematuse ümbersuunamine + Hoiab ära järgmise loo kuvamise, kui vajutatakse nuppu \"Ei meeldi\" + Playliste loo puudutustegevus + Esita playlisti + Esita ainult valitud loo + Lisa valitud loo loetellu + Lisatud loetellu + Peida Casti nupp + Peida ajaloo nupp + Peida teavituste nupp + Peida otsingu nupp + Peida hääleotsingu nupp + Peida heliotsingu nupp + Peida nupp Uus + Peidab nupu Uus vaheleheku Raamatust + Tegevusnupud + Valige, millised tegevusnupud kuvatakse mängija all + Peida tegevusriba + Peida nuppud Meeldiv ja Mitte meeldiv + Peida nupp Kommentaarid + Peita nuppu \"Livevestluse taastamine\" + Peida nupp Laulusõnad + Peida nupp Jaga + Peida nupp Salvesta + Peida nuppu \"Laadi alla\" + Peita nuppu \"Alusta raadiot\" + Hüppmenüü + Vali, millised komponendid kuvatakse mängija ja järjekorra hüppmenüüs + Peida 3-tulbline komponent + Peida meeldiva ja meeldemitmata nuppud + Peida menüü \"Maitse vastavus\" + Peida menüüd \"Lisa loendisse \'Kuula hiljem\'\" + Peida menüüd \"Lisa järjekorda\" + Peida altekstide menüüd + Peida menüüd \"Kustuta loend\" + Peida menüüd \"Tühjenda järjekord\" + Peida menüüd \"Ära soita seda artisti\" + Peida allalaadimise menüüd + Peida menüüd \"Muuda loendit\" + Peida menüüd \"Mine albumile\" + Peida menüüd \"Mine artisti juurde\" + Peida menüüd \"Mine episodile\" + Peida menüü \"Mine podcastisse\" + Peida abi menüüd + Peida menüü \"Märgi osa kulatuks\" + Peida menüü \"Ei huvi\" + Peida menüü \"Kuuluta kiirkõnest\" + Peida menüü \"Esita järgmine\" + Peida kvaliteedi menüüd + Peida menüü \"Eemalda raamatust\" + Peida menüüt \"Eemalda loendist\" + Peida teatamise menüüd + Peida menüüt \"Salvesta osa hilisemaks\" / \"Salvesta raamatukku\" + Peida menüüt \"Salvesta loendisse\" + Peida jagamise menüüd + Peida menüüt \"Segi mängi\" + Peida menüüt \"Unenõõristis\" + Peida menüüt \"Alusta raadiot\" + Peida menüüt \"Statistika nerdidele\" + Peida tellimise menüüd + Peida menüüt \"Eemalda kiirvalikust\" + Peida menüüd \"Vaata loo autoreid\" + Alglehekülg + Vaikimisi + Edetabelid + Episoodid hiljem jaoks + Avasta + Ajalugu + Raamatukogu + Meeldiva muusika + Playlistid + Otsi + Telliid + Sunni portreiorientatsioon + Hoiab rakenduse portrait-orientatsioonis, isegi kui seadet pööratakse + Muuda minisinuplayeri värvi + Muudab minisanda värvi vastavaks täisekraani esitajaga + Muuda navigeerimisriba värvi + Muudab navigeerimisriba värvi vastavalt täisekraanile mängijale + Kuva miniplayeris nupp \"Järgmine\" + Kuva miniplayeris nupp \"Eelmine\" + Luba sundminiplayer + Hoiab esitajat automaatse täisekraani laiendamisest uue loo alguse korral + Luba minipleieri pühkimisega sulgemine + Võimaldab minipleieri allapoole pühkimisega sulgeda + Navigeerimisriba + Valige, millised nupud kuvatakse allumise navigatsioonisribal + Peida nupp Avakuv + Peida nupp Proovid + Peida nupp Avasta + Peida nupp Raamatukogu + Peida nupp Uuenda + Peida navigeerimisriba + Peida navigeerimisnuppide pealkirjad + Pole sisse logitud. Konfigureerimiseks puudutage + Logi välja + Logitud välja + Luba scrobbling + Saada staatust \" mängib praegu\" + Laulu minimaalne kestus + Laule, mis on sellest lühemad, ei scrobble\'d + Scrobble viivimise protsent + Käivita scrobble pärast laulu selle protsendi kuulamist + Scrobble viivituse sekundid + Triggeri scrobble pärast seda, kui oled kuulanud mitmeid sekunde + ListenBrainz konto + Sisselogitud. Puuduta kontohalduseks + Sisestage oma ListenBrainz kasutajatoken, et lubada scrobbling + Token + Kinnita + Palun sisestage token + Kinnitamisel… + Kehtiv token! Kasutaja: %s + Kehtmatum token: %s + tundmatu + Kinnitamine ebaõnnestus: %s + Token tühistatud + Token salvestatud + Hoiatus: Salvestatud token on kehtlemata! + Lisa meeldivatei radad lemmikutele + Lisa radad automaatselt Last.fm lemmikutele, kui lisad neid YouTube Musicis meeldivatei (ning eemalda lemmikut, kui märkest own meeldivatei või märgistad mitte-meeldivaks) + Last.fm konto + Sisselogitud kasutajana %s. Puuduta kontot haldamiseks + Sisselogimiseks sisesta oma Last.fm credentials + Kasutajanimi + Parool + Logi sisse + Palun sisestage kasutajanimi ja parool + Sisselogimisel… + Sisselogimine õnnestus! + Last.fm-ist sessiooni hankimine ebaõnnestus + Sisselogimine ebaõnnestus: %s + Last.fm credentials on salvestatud! + Metandandmete puhastamine + Puhasta metandandmed + Puhasta automaatselt tavaline prügi (nt \"Official Video\", \"Remaster\", \"Live\") pealkirjadest, artistitest ja albumbitest enne scrobblingu protsessi + Kohandatud regex-puhastus + Sisestage kohandatud regulaarsuvandus, et eemaldada täiendavat teksti laulude pealkirjadest, artistitest või albumitest. Näide: (?i)\\s*\\((clean|explicit)\\) + Analüüsi artist ja rada pealkirjast + Erakuva artist ja loo nimi videotiitlist, kasutades eraldatist \" - \". Kasulik uuesti üleslaetud sisu puhul, kus kanalit võib kasutada artistina + Täida puuduv album + Kui muusikal puudub album, otsi see üles Last.fm-st, et su scrobble oleks täielik + "Scrobbling sünkroniseerib automaatselt teie YouTube Musici kasutamise teie Last.fm või ListenBrainz profiiliga + +Et tagada scrobblingu sünkroniseerimine taustal esituse ajal, veenduge, et YouTube Musici rakenduse akukasutus on seatud olekule \"Piiranguteta\"" + Kasuta kolmandate osapoolte lüürikuid + Kuvab lüürikuid kolmanda osapoole teenusest; kui neid ei leidu, siis kasutatakse sisseekitud lüürikuid + Hoidke ekraan sisse lüüdes lüürika vaatamisel + Hoidab ekraani sisse lüüduna, kui lüürika paneel on avatud + Laulusõnade teenus + Kuva + Sünkroniseerimine + Paneel + AI lõpppunkt + Kõik pakkujad on välja lülitatud - sisseehitatud lüürik ja manuseeritud lüürik + Lä resetting pakkujate järjekord? + Taasta pakkujate vaikimisprioriteetsed järjekord + Tõrge salvestatud sõnad + Kustuta selles seadmes salvestatud lüürikad + Tõhjutatud %1$s MB + Hangi token + Apple Music Media-user-token + "Kleepige music.apple.com cookie \"media-user-token\" + +Sammud: +1. Logige sisse music.apple.com lehel liikmena +2. Avage DevTools → Application → Cookies → music.apple.com +3. Kopeerige väärtus \"media-user-token\"" + Apple Music: media-user-token on salvestatud + Apple Music: media-user-token on kustutatud + Apple Music: kehtetu media-user-token – palun kontrollige ja proovige uuesti + Spotify SpDc + "Kleepige Spotify cookie \"sp_dc\". + +Sammud: +1. Logige sisse open.spotify.com lehel +2. Avage DevTools → Application → Cookies → open.spotify.com +3. Kopeerige väärtus \"sp_dc\"" + Spotify: sp_dc on salvestatud + Spotify: sp_dc on kustutatud + Spotify: kehtetu sp_dc – palun kontrollige ja proovige uuesti + Apple Musici token on valikuline. Selle puudumisel kasutatakse varulahendusena Lyrically\'t + Deezer ARL + "Kleetite Deezeri arl-cookie. + +Sammud: +1. Logi sisse aadressil deezer.com +2. Ava DevTools → Application → Cookies → deezer.com +3. Kopeeri väärtus \"arl\"" + Deezer: arl salvestatud + Deezer: arl tühjendatud + Deezer: kehtetu arl – palun kontrollige ja proovige uuesti + Musixmatchi token + Lisage tokeni ainult juhul, kui soovite kasutada oma kontot. See peab pärinema Musixmatchi Androidi rakendusest: musixmatch.com lehelt kopeeritud tokenid kuuluvad endise töölauakliendi juurde ja API keeldub neid kasutamast + Musixmatch toimib ilma tokenita, seega võib see väli jääda tühjaks + kasutaja token + Musixmatch: kasutaja token salvestatud + Musixmatch: kasutaja token tühjendatud + Musixmatch: kehtetu kasutajatoken – palun kontrollige ja proovige uuesti + YouTube\'i küpsised + "Kleepige oma YouTube'i küpsised, et päriidavalt kuvada altekste ja ajatekste YouTube'ist. + +Sammud: +1. Avage brauseris youtube.com ja logige sisse +2. Avage DevTools (F12) → Application → Cookies → youtube.com +3. Kopeerige kogu küpsiste tekst (võrkude key=value paarid, mida eraldavad semicolonid) + +Kohalduvad küpsised: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPISID, LOGIN_INFO" + YouTube: küpsised salvestatud + YouTube: küpsised kustutatud + YouTube: cookies on kehtetud - palun kontrollige ja proovige uuesti + Puudutage ja hoidke sõna või rida + Puudutage rida, et hüppata laulu vastavasse kohta; hoidke sõna all, et seda kopeerida, või rida, et kopeerida kogu rida. Väljalülitamisel kaovad mõlemad žestid + Kuva jaotuse märgid + Märgi iga uus lauluteksti jaotus väikese märgiiga, näiteks Salm või Refrään + Kuva kopeerimise nupp + Puudutage, et kopeerida originaallüürikad lihtsa tekstina; hoidke all, et salvestada lüürikate allikafail kaustesse \"Downloads\" + Kuva tõlkimise nupp + Puudutage tõlke lülitamiseks; hoidke all, et näidata ainult tõlget. Kasutab pakkujate tõlkeid, Google Translate\'i või OpenAI-ga yhteilva API-t + Kuva romaniseerimise nupp + Puudutage romaniseerimise lülitamiseks; hoidke all, et näidata ainult romaniseerimist. Kasutab pakkujate romaniseerimist, Google Translate\'i või OpenAI-ga yhteilva API-t + Kohalik romaniseerimine ja sünkroniseerimine + Romaniseeri seadmes, kui teenusepakkuja, sinu mudel ja Google Translate ei õnnestu. Lugemine järgneb iga ajastatud lüürika sõna; sõnasünkroniseerimine peab olema sisse lülitatud. Hiina, kantoneese, jaapani, korea, kreeka, vene, gruusia, armenia; umbes 120 MB + Loetakse %1$s: %2$d%% + Lugemised on valmis (%1$d kirjet) + Allalaadimine ebaõnnestus - uuesti proovimiseks lülita see välja ja siis uuesti sisse + Kuva värskendamise nupp + Puuduta, et owneda järgmisele lüürika kandidaadile; hoia all, et otsida lüürikat laulu pealkirja ja artisti järgi + Otsi laureate + Laulu pealkiri + Artist + Sisestage laulu pealkiri ja artist + Ükski aktiveeritud pakkujatest ei saa otsida teisi lugusid + Salvestatud + Esita esiletõstetult sünkronitud sõnu või silbe + Andke eel töötamisele sõna-sõnalistele lüürikatele, kui need on saadaval. Kui lüürikal on märgitud sõnade ajastus, esitatakse iga sõna või silp esiletõstetult koos muusikaga + Peita esitatud lüürika + Peida lüürika read praegusest mängitavast readst üle + Peida mängimata lüürikat + Peida lüürika read praegusest mängitavast readst all + Kuva MediaSessioni lüürika lukusekna ekraanil + Kuvab praegust rida lukuseknal, Android Auto ja Bluetoothis. Vajalik Android 11 või uuem + Kuva miniplayeri lüürikad + Kuvab praegust rida minipleieris, liigutades rada altekstiks + Laulu pealkirja/esitaja vorming + Treki vorming, mida kuvatakse koos lüürikaga minipleieris ja MediaSessionis. {t} on pealkiri, {a} on esitaja ja {v} on versioon, näiteks akustiline, kui lüürikaallikas seda pakub. Tekst kahe kooloni vahel kuvatakse ainult siis, kui kõigil selle sisemus olevatel muutujatel on väärtus; tavalise kooloni jaoks kirjutage \\: Iga rida kuvatakse järjekorda, liigudes edasi igal lüürika muutusel + Lüürika rea vorming + Lüürika rea vorming minipleieris ja MediaSessionis. {lrc} on rea tekst ja {n} on arv, mitu korda seda järjest lauldi. Tekst koolonide vahel kuvatakse ainult siis, kui rida kordub. Kooloni jaoks kasutage \\: + Vorming peab sisaldama {lrc} + Artisti rea mall ja animatsiooni aeg + Minimaalne ekraanil viibimise aeg (ms) + Maksimaalne ekraanil viibimise aeg (ms) + Ainult mitmerealistele animatsioonidele. Hoidke iga kaadrit vähemalt minimaalse aja jooksul. Maksimaalse aegluse korral liigutage artisti rida edasi isegi siis, kui lüüblik jääb samaks. + Minimaalne peab olema 0 või suurem. Maksimaalne peab olema suurem kui 0 ja vähemalt minimaalse aegne. + Rea pikkuse piirangud + Lüübliku ja artisti rea piirangud ning täislaia märgi kaal + "Lüübliku rea piirang +Jagage pikemad lüüblikud tasakaalustatud poolteks. 0 jätab jagamise disabledi." + "Artisti rea piirang +Jagage täidetud rada tsüklilisteks raamiteks. 0i korral on jagamine välja lülitatud." + "Täislaia märgi kaal +Kuvatakse mõlema piirangu puhul. Väärtuse 1,75 juures loetakse neli täislaia märki kui 7." + Sisestage tervarvudena piirangud vahemikus 0–10 000 ja märgikaal, mis on suurem kui 0 + Instrumentaalvahe + Kuvatakse minipleieris ja MediaSessionis instrumentaalpausi ajal. Iga rida on üks animatsiooni kaader. {t} on pealkiri, {a} on artist ja {lrc} on viimane lüürika rida + Minipleieri ja MediaSessioni ajähõljutus + Nihutab aega, millal järgmine rida minipleieris ja MediaSessionis kuvatakse. Kasutage negatiivset väärtust, kui read kuvatakse hiljuti, näiteks lukusekranil + Filtrid + Metandandite regex + Regulaarne väljend, mida rakendatakse pealkirjadele, artistidele ja albumeid enne lüürikate otsingut ja kahe pealkirja ühtimise hindamisel. Et ownad mõlemad funktsioonid, jäta väli tühjaks. Vaikeväärtus eemaldab tavalise täite nagu \"Official Video\", \"Remaster\", \"【插曲】\" ja \"- Topic\" + Lüürika filtri regex + Regulaarne väljend, mida rakendatakse ainult algse lüürika teksti kohta (mitte tõlgete või romaniseerimise kohta). Jätke tühjaks, et pöörata välja + Inforealide märgised + Kui lüürikad on valesti filtritud lõppu, siis reguleerige või tühjendage seda. Koomaga eraldatud märgised: read, mis sisaldavad märgisest järgnevat koolont, kuvatakse allpool kui krediidiinfona. Jätke tühjaks, et pöörata välja + Kirjutage esimene täht suurtähega + Kirjutab iga lüürika rea esimese tähe suurtähega + Eemalda lõpppunktuatsioon + Eemaldab punktid, komad, koolonid ja semikolonid lüürika rida lõpust. Küsimärgid, huutähed, ellipsid, tsitaadimärgid ja sulgud jäävad alles + Teksti suurus + Lüürikuteksti suurus + Globaalne ajanihe + Sünkronitud lüürikajalgad järgivad esitust. Kui need nihkuvad, reguleerige ajastuse nihket. Liigutage sünkronitud lüürikuid edasi või tagasi. Kasutage positiivset väärtust, kui lüürid jookse laulust ette + Kasuta SponsorBlocki segmente lüürika nihke jaoks + Sobista ilma SponsorBlocki poolt飛ばatavate segmentideta ja nihuta lüürika väljalasetud helile. Väljake ignoreerib SponsorBlocki täielikult + Sellel rajal puudub esitav video-ID + Allikas: %s + Kopieerimise nupu pikaajaline vajutamine salvestab selle LRC-i; kui suudate ajastust täpsustada ja selle kogukonnapakkujatega jagada, oleksime tänulikud + Tõlkinud Google + Romaniseerinud Google + Tõlkinud %s + Romaniseerinud %s + Romaniseerinud %s + Tõlgitud + Tõlkimise keel + Romaniseerimise keel + Tõlgitakse… + Lüürikute tõlkimine ebaõnnestus + Romaniseeritud + Automaatne + Romaniseeritakse… + Lüürika romaniseerimine ebaõnnestus + Ainult tõlgitud + Ainult romaniseeritud + Dual-režiim + Kopeeritud lõikelauale + Värskendamine + Värskendatud + QuickLRC API võti + QuickLRC transkribeerib või sünkroonib sõnu praeguse loo jaoks. See on eraldi teenus, mis ei ole seotud ülaltoodud OpenAI-ga ühilduva lõpppunktiga + QuickLRC-i võti on salvestatud. Kasutage sõnumite otsingudialogis sünkroonimise nuppu või paneeli teavitust, kui lool puuduvad sõnumed + QuickLRC API võti on salvestatud + QuickLRC ei võtnud seda võtit vastu + QuickLRC API võti on kustutatud + QuickLRC seab sõnad neile lugudele, kellel neid puudub, või transkribeerib need helifailist krediitide vastu. Võtme määramise korral pakuvad sõnadeta lugud transkribeerimist ja otsingudialogile lisandub sünkroonimise nupp + Taotle QuickLRC-i sünkroonimist + Sünkroonitakse + Rakenda sünkroonimist + Taotle QuickLRC-i transkribeerimist + Transkribeeritakse + Transkribeerimine katkestatud + Transkribeerida saab ainult YouTube\'ist esitatud paused + Sõnad sünkroonimiseks puuduvad - transkribeeritakse + QuickLRC ei suutnud seda pause töödelda + Selle pause jaoks on QuickLRC krediidid otsa saanud + "Laulusõnad tuvastatakse pealkirja ja esitaja järgi; ebatavaline metadata võib tulemuseks anda tühja vastuse või vale versiooni. Versioonide vaheksuleks kasutage värskendamist. Teenusteks, mille kõrval on märk *, on vaja tokent. Kõiki aktiveeritud teenuseid ei päristata alati teie järjekorras, vaid vastused saabuvad vastavalt nende kiirusele.\n\nLaulusõnad tulevad kolmandate osapoolte teenustest, mis ei ole seotud Morphe'iga, ei ole selle poolt sponsoritatud ega heaks kiidetud. Iga teenus kasutatakse sellisena, nagu see on: laulusõnad, nende kättesaadavus ja litsentsid jäävad vastavate teenuste ja nende litsentieerijate valdusse ning nende kasutamine võib olla allutatud nende kasutustingimustele. Aktiveeritud teenusele edastatakse ainult teie poolt soovitud päring – pealkiri, esitaja ja juhul, kui teenus seda vajab, video-ID. Valikuline AI-punkt ja QuickLRC transkriptsioon käivituvad alles pärast nende konfigureerimist ning saavad laulusõnade teksti või loo video-URL-i." + Lülitage sisse ainult need tarnijad, mida kasutate, või hoidke vaikimisseadet - pidevad päringud võivad väikseid kogukonna lüürikasaite ülelaadida + Peita autoriinfo + Peita lüürika all kuvatavat autoriinfot + Vaheta tõlke ja romaniseerimise koht + SÜÜDA paneb romaniseerimise lüürika alla ja tõlke üles; VÄLJA jätab romaniseerimise ülesse ja tõlke alla + Tõlge + Romaniseerimine + Tõlketeenuse pakkuja + Google (vaike, tasuta) + OpenAI-ga ühilduv (kohandatud) + Romaniseerimise pakkija + Google (vaike) + Kohalik / võrdluseta sõnastik + OpenAI ühilduv (kohandatud) + AI jaotus + Kasutatakse ainult siis, kui teenusepakkuja ei edasta jaotuse märke nagu Refrään või Salm, mille teie OpenAI-ga ühilduv mudel seejärel tuvastab + AI & pilve lõpppunktid + Konfigureerige AI pakkujate API võtmed, mudelid, lõpppunktid ja tõenäidiste šabloonid. + API lõpppunkti URL (nt https://api.openai.com/v1/chat/completions) + Prompti mall + Asendussümbolid: {language}, {task}, {title}, {artist}, {count}, {lines} + Prompt on salvestatud + Prompt on salvestatud, kuid AI lõpppunkt ei ole veel kasutatav. Konfigureerige see esmalt AI seadete alt + Prompt on lähtestatud + Ei ole korrektne JSON. Parandage see või tühjendage väli enne salvestamist + Tõlkimise & romaniseerimise seaded on salvestatud + Seaded lähtestatud vaikeseadetele + Põhi-URL peab algama http:// või https:// + Testi ühendust + Konfigureeri lõpppunktid + Kuva kõik lõpppunktid + Ühendust kontrollitakse… + Tõlkusteenuse pakkuja + Romaniseerimise pakkuja + Hangi mudelid + Mudelite hankimine… + Vali mudel + Mudelite hankimine ebaõnnestus. Kontrolli API võtmet või ühendust. + Gemini API seaded + OpenAI ühilduv (kohandatud lõpppunkt) + DeepL API seaded + API võti + Mudel + Põhi-URL + API võti (valikuline) + Tõlkimise juhise + Lisa päringsisu (valikuline JSON) + Autentimise võti (Free või Pro) + Konfigureerige API võtmed, mudelid, lõpppunktid ja juhised + 8.23.51 - Keela paksud ikoonid + 9.13.51 - Taasta mängija vahekaadid \"Järgmine\", \"Laulud\" ja \"Seotud\" diff --git a/patches/src/main/resources/addresources/values-et-rEE/reddit/strings.xml b/patches/src/main/resources/addresources/values-et-rEE/reddit/strings.xml index cf7776fdaf..f09843a0fe 100644 --- a/patches/src/main/resources/addresources/values-et-rEE/reddit/strings.xml +++ b/patches/src/main/resources/addresources/values-et-rEE/reddit/strings.xml @@ -15,4 +15,123 @@ Second \"item\" text" --> - + + Redditi privaatsuspoliitika + Google\'i sisselogimine ei ole saadaval. Kasutage sisselogimiseks e-posti + Reklaamid + Peida feedi reklaamid + Peida kommentaaride reklaamid + Navigeerimisriba + Peida Vastused nupp + Peida Vestlus nupp + Peida Loomise nupp + Peida Avasta või Kogukonnad nupp + Peida mängude nupp + Külgriba + Peida viimaselt külastatud riiul + Peida Reddit shelfist mängud + Peida Reddit Pro riiul + Peida \"About\" riiul + Peida ressursiriba + Paigutus + Teksti kopeerimine + Võimaldab teksti valimist ja kopeerimist postitustes ja kommentaarides. See keelab kommentaaride kokkupressimise + Kommentaaride kokkupressimine lakab töötamast, kui funktsioon \"Teksti kopeerimine\" on aktiveeritud + Kohandatud font + Laadi allpoolse tee järgi kohandatud TTF või OTF fondifail. Selle aktiveerimine lülitab välja ka süsteemifondi kasutamise + Kohandatud fondifail + Puudutage, et valida TTF või OTF fondifail + Valitud font - %s + Vali esmalt kohandatud fondifail + Palun vali .ttf või .otf fondifail + Kohandatud fonti ei õnnestunud laadida. Vali teine fail + ্ষিকী kaasaegset avalehe kasutajaliidest + Keela ekranipildi popup + Keelab popupi, mis ilmub ekranipildi pööramisel + Järjesta süsteemifont + Kasuta seadme süsteemifonti asemel Reddit Sansi või Robotot. Selle aktiveerimine lülitab välja ka kohandatud fonti + Peida nuppu Küsi + Peidab otsinguribalt nupu Küsi + Peida kogukondade riiul + Peidab seotud või soovitatud kogukondade riiuli alareddites + Peida populaarsuste riiulid + Peidab populaarsuste riiulid voost ja otsingupakkumistest + Eemaldage teavituste soovitusestatuse dialoog + Sulgeb automaatselt teavituste soovitusdialoogi subredditis külastamisel + Kuva märgiseid avaliku voo sees + Kuvab postituste märgiseid avaliku voo sees + Mõnel voo üksusel ei prugu märgise olla + Kuva postituste vaatamiskordi arv + Piirang: Saadaval ainult juhul, kui API vastus sisaldab vaatamiskordide arvu + Postituse toimingute riba peegeldamine + Liigutab postituste hääletus- ja kommentaarinupud paremale poole ning ristpostituse ja jagamise nupud vasemale poole + Hääletus- ja kommentaarinupude vahetamine + Kuvab postituste kommentaarinupu enne hääletusnuppe + Eemaldage NSFW hoiatuse dialoog + Nõustub automaatselt NSFW sisu hoiatusega subredditis külastamisel + Meelevalis + Teave + Paigutuse versioon ja ametlikud lingid + Puhasta jagamislinke + Eemaldab jagatud linkidest jälgimisparameetrid + Ava lingid otse + Möödub suunamis-URL-idest väliseid linke avades + Uuenduse meedia täislaiususega + Kuvab pildid ja videod uuenduses ja postituse üksikasjades servast servani, ilma küljepispahted ja ümarate nurkadeta + Meediavaatlaja hajumise intensiivsus + Tume hajumise intensiivsus pealkirja taga piltide ja videote vaatamisel, vahemikus 0 (põhja hajumine) kuni 1 (Reddit\'i vaikimisi seade) + Säilita feedi positsioon + Säilitab feedi samas positsioonis pärast postituse, pildi või video sulgemist, selle asemel et liigutada avatud postitus üles + Feedi meedia maksimaalne kõrgus + Piltide, videote ja galeriiide maksimaalne kõrgus voos, väljendatud nende laia kordades. Kõrgemad meedialuused lõigatakse. 1,33 on Redditi vaikimisseade + Esita Redgifse heliga + Esitab algset Redgifse videot asemel Redditi koopiast, millel puudub sageli heli + Redgifse HD kvaliteet + Esitab HD Redgifs video. HD videod on suuremad ja võivad laadimiseks peatuda + Allalaadimiskoha asukoht + Kõik kontod + Reddit\'i vaikevalik: pildid Pictures/Reddit, videod Download + Kaust: %s + Failinimi: %s + Reddit\'i failinimed + Kasutab valikut \"Kõik kontod\" + Kasuta oma asukohta + Salvesta kohta + Reddit\'i vaikekaustad + Vaikimisi + Alamkaust + Lisage failinimele / märkiga alamkaustad + Siru… + Selle seadme puhul pole kaustide valijat saadaval + Failinimi + Reddit\'i failinimi + Puudutage märget, et see sisestada. Märk / loob alamkausta. Tundmatutest väärtustest saab \"unknown\" + Pildid: %s + Videod: %s + Salvesta + Läbi seatamine + Kustuta + Kas lähtestada kõigi kontade alllaadimisasukoht Redditi vaikeseadetele? + Kas kustutada allalaadimisasukoht %s? + Ei õnnestunud salvestada allalaadimiskoha + Allalaadimiskaust ei ole enam saadaval. Salvestati selle asemel Redditi vaikimiskausta + Allalaaditud videot ei õnnestunud salvestada + Seda kaustu ei saa allalaadimisteks kasutada + %s (praegune) + %s (pole sisse logitud) + Anonüümne (väljalogitud või incognito) + Peida meedialuuse vaataja pealkatte + Peidab pealkirja, nuppe ja video juhtimise nuppe pildi või video avamisel. Puudutage meediat, et need kuvada + Peida nupp \"Vaata vestlust\" + Peidab nuppu \"Vaata vestlust\" meedialuuse vaataja allosas + Meedialuuse vaataja must taust + Kasutab meedialuuse vaatajas piltide ja videote taga musta tausta asemel teema taustavärvi + Ava lingid välise rakendusega + Avab lingid teie brauseris asemel rakenduse siseses brauseris + Male kommentaaride kokkupressitud olek + Kõik kokkupressitud kommentaarid jäävad kokku pressitud ka siis, kui avate postituse uuesti + Rakenduse ikoon + Valige rakenduse ikoon + Vaikimisi + Tundmatu + diff --git a/patches/src/main/resources/addresources/values-et-rEE/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-et-rEE/shared-youtube/strings.xml index fd007eeb78..49e48272f7 100644 --- a/patches/src/main/resources/addresources/values-et-rEE/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-et-rEE/shared-youtube/strings.xml @@ -18,46 +18,297 @@ Second \"item\" text" --> + Käivitusanimatsioon + Välja lülitatud + Värv + Monokroomne + + Seadistamise lõpetamiseks taaskäivitage + Rakenduse nimi + Tume teema taust + Täismust + Material You (Täismust) + Klassikaline YouTube + Tume roosa + Tume sinine + Tume roheline + Tume kollane + Tume oranž + Tume punane + Kohandatud tume taust + Määra tumeda teema taustavärv + Rakenduse vaikimisseadistus + Material You (Neutraalne) + Material You (pähi) + Material You (teisejärgmine) + Material You (kolmandajärgmine) + Kohandatud + Allahlaadimised + Allalaadimise režiim + Vaikimisi + Laada alla rakenduses + Välise rakendusega allalaadimine + Allalaadja paketi nimi + Paigaldatud välise allalaadja rakenduse paketi nimi + Sisestage paketi nimi + Muud + Rakendust ei ole installitud + %s ei ole installitud. Palun installige see + "Ei leidud installitud rakendust paketinimega: %s + +Veenduge, et paketinimi on õige ja rakendus on installitud" + Paketi nimi ei saa olla tühi + Kohandatud + Rakenduse ikoon + Originaal + Morphe must + Morphe tume + Morphe hele + Morphe mängutuli + Morphe play mustas + Customiseeritud + "See muudab teie rakenduste käivitajas kuvatavat ikooni. + +See seade ei muuda ikooni järgmistes kohtades: +• Seadme süsteemi seadete rakenduste nimekiri +• Teatud push-teavitused +• Rakenduse valija \"Ava rakendusega\" +• Android Auto ikoon + +Ikooni kõikjal muutmiseks rakendage paigala uuesti ja muutke paigala valikut \"Rakenduse ikoon\"" + Teavituse ikoon + Vastaava rakenduse ikoon + Morphe mäng + Morphe teavitus + Teie vaatamish Историю ei salvestata.<br><br>See on tõenäoliselt tingitud DNS-i reklaamiblokeerijast või võrguproxyst.<br><br>Selle parandamiseks lisage <b>s.youtube.com</b> lubatud loendisse või lülitage välja kõik DNS-i blokeerijad ja proxyd + Ära näita uuesti + Videovoogude imiteerimine + Konfigureeri kliendi imiteerimist, et vältida taasestuse probleeme + "Parandab esituseprobleeme enamiku kasutajate jaoks, päides ühilduvat videovooli + +Kui olete YouTube Premium kasutaja, ei pruugige seda seadet olla vajalik" + Kliendivideovoolude hankimine ebaõnnestus + Vaikimisi klient ei tööta + "%1$s ei töötanud mitu korda järjest, kuid %2$s töötas. + +Kas määrata %2$s vaikimisi kliendiks?" + Vaheta + Selle seade väljalülitamine võib põhjustada esituse tõrkeid + Proovi aktiveerida: Videovoolude imiteerimine > Android VR sisselogimine + Vaikimisi klient + Android VR sisselogimine + Android VR-i kontole pole sisse logitud. Logimiseks puudutage siia + Hetkel on sisse logitud Android VR-i kontole + Selle funktsiooni kasutamiseks muutke \'Vaikim klient\' Android VR-iks + Logige sisse Android VR-i kontole + "Android VR-i sisselogimine võib mõne kasutaja puhul parandada taastoomise probleeme + +Sisse logimiseks vajutage allpoolset nuppu Jätka" + Logi välja + Juhendid + "Aktiveerimiskood: %s + +1. Puudutage allpool nuppu \"Ava veebisait\", et avada veebisait \"Seadme ühendamine\" +2. Kleepige lõikelaualt (või sisestage) kohal olev aktiveerimiskood +3. Logige sisse Google'isse, kui teid palutakse. Kui teil on mitu kontot, valige see YouTube konto +4. Puudutage nuppu \"Luba YouTube VR juurdepääs\" +5. Pöörge tagasi sellele rakendusele pärast teate \"Seade ühendatud\" ilmumist" + Aktiveerimiskoodi väljastamine ebaõnnestus + Autentimisstokeni väljastamine ebaõnnestus + Sisselogimine õnnestus + "Autentimine on salvestatud + +• Teie Morphe seaded ei importeeri ega eksporteeri Android VR autentimist +• Kui seda rakendust ei kasutata 6 kuud, eemaldab Google selle autentimise +• Peate uuesti sisse logima, kui muudate oma Google'i parooli või eemaldate selle seadme oma Google'i seadetest" + OAuth2 ei ole hetkel saadaval (API aeg läks läbi) + Android VR sisselogimine aegus, proovige VR-i uuesti sisse logida + OAuth2 ei ole ajutiselt saadaval (staatus %s) + OAuth2 ei ole hetkel saadaval + VR-i sisse logimine ebaõnnestus, veebilehe autentimist ei heaks kiidud + VR-i sisse logimine ebaõnnestus: %s + Logitud välja Android VR-ist + VR sisselogimise token on kehtKmatu, proovige VR-i uuesti sisse logida + Teavituste meediakontrollid + Kohandage lukuseknal ja teavituste paneelil kuvatavaid taasestuskontrolle + Peida nuppud Eelmine & Järgmine + Peidab nupud Eelmine ja Järgmine meediatavitustes, lukuseknal ja kõhulaugu kontrollides + Keela otsinguliist + Hoiab ära otsinguliista lohimise lukuseknal ja teavituste paneelil + Otsi nuppidega Eelmine & Järgmine + Muudab meediauuenduste, lukusekraani ja kõrvaklappide juhtimised nii, et need mähivad videoid edasi või tagasi selle asemel, et videoid vahetada + Tagasi kerimise pikkus + Sekundid, mille võrra nupul \"Eelmine\" vajutades tagasi keritakse + Edasi kerimise pikkus + Sekundid, mille võrra nupul \"Järgmine\" vajutades edasi keritakse + Otsingu pikkus peab olema 1 ja %s sekundi vahel. Kasutatakse vaikimisseadet + Võtta QUIC protokoll kasutusest + Võtta DRC audio kasutusest + sunni originaalne helikeel + Esitab alati video algset heliratsiooni dubleeritud versiooni asemel + Vaata vaikimist üle + Vaatab automaatselt üle vaikseid pause videovideos + Minimaalne pausepikkus + Lühendatakse ainult pause, mis on vähemalt selle pikkused. Jõustub järgmise video käivitamisel + Võrguproxü + Suunake toetatud võrgupäringud läbi proksi + Luba proxü + Suunab toetatud võrgupäringud läbi konfigureeritud HTTP- või HTTPS-proxü + Proxü host + Proxüserveri hostnimi või IP-aadress + Proxü port + Proxüserveri port + Kasuta HTTPS-volitust + Kasuta proxy-serveriga ühendamiseks HTTPS-i asemel HTTP-d + Proksi autentimine + Saadab proksipäringutega koos Basic-autentimise + Proksiservuri kasutajanimi + Kasutajanimi proksiservuri põhialase autentimise jaoks + Proksiservuri parool + Parool proksiservuri põhialase autentimise jaoks + Luba otsene varulahendus + Ühendu otse, kui proksiservur ei tööta + Peida täisekraani reklaamid + Selle funktsiooni kasutamiseks muutke \"Spoof video streams\" milleks tahaks kliendiks, välja arvatud \"Android Studio\" ja \"Android VR Downgraded\" + Jagamislinkide puhastamine + Eemaldab jagatud linkidest jälgimisparametrid + Muuda jagamislinkideks youtube.com + Asendab jagatud linkides aadressi music.youtube.com aadressiga youtube.com + Muuda jagamislinkideks youtu.be + Sunnib videoid kasutama lühendatud youtu.be URL-i vormingut + Veaotsingu logi protokollipuhver + Lisab arendajatele täiendavat logiteavet rakenduse komponentide tuvastamiseks + "Selle seade aktiveerimine logib täiendavat paigutuse andmeid, sealhulgas mõne kasutajaliidese komponendi ekraaniteksti. + +See võib aidata tuvastada komponente kohandatud filtrite loomisel. + +Kuid selle aktiveerimine logib ka mõningaid kasutajandmeid, näiteks teie IP-aadressi" + Krediitfiltri loomine + Konfigureerige kohandatud filtreid, et peita ükski komponent + Lülita sisse kohandatud filter + Komponendi teepuurendaja sõunete nimekiri filtreerimiseks, eraldatud uute radadega + Kehtetu kohandatud filter: %s + Seadete menüü filter + Peida rakenduse standardse seadete ekraani üksused nende näidise nime järgi + Avage rakenduse standardne seadete ekraan üks kord, et nimekiri täita + Eemalda vaataja hoiatuse dialoog + See ei mööda vanusepiirangut. See lihtsalt nõustub sellega automaatselt + PoTokeni pakkuja + Konfigureerige PoTokeni pakkujat, et vältida taasestamise probleeme + Väline PoTokeni pakkija + "Parandab esituse probleeme, kasutades välist PoTokeni loojat (eksperimentaalne) + +Kui olete YouTube Premiumi kasutaja, ei pruu see seade olla vajalik" + "Parandab esituse probleeme, kasutades välist PoTokeni genereerijat (eksperimentaalne) + +Selle funktsiooni kasutamiseks lülitage välja \"Võõra video-voogude imitatsioon\"" + Kui see ei toimi, proovige MicroG-d jõuvalt sulgeda + Ava PotHelperi seaded + "See funktsioon on hetkel arendamisel ja ei pruugida töötada + +Puudutage siia, et alla laadida PotHelper" + PotHelper ei tööta enam + "Selles PotHelperi versioonis sisseehitatud token ei tööta enam + +Taasplaneerimiseks lülitage sisse \"Spoof video streams\"" + Lülita sisse + Imiteeri rakenduse versiooni + Kelab serverit arvama, et kasutate rakenduse vanemat versiooni, et taastada pärand-UI funktsioonid + "Rakenduse versioon esitletakse vanema versioonina. + +See muudab rakenduse välimust ja funktsioone, kuid võivad esineda tundmatud kõrvaltoimed. + +Kui see hiljem välja lülitada, on soovitatav puhastada rakenduse andmed, et vältida kasutajaliidese tõrkeid" + Rakenduse versiooni imiteerimise siht + Rakenduse värskendussagedus + Vaikimisi + %s FPS + Ülekirjutage värskendussagedus + Alati + Taastoomine portreettrežiimis + Taastoomine täisekraanis + Taastoomine portreettrežiimis ja täisekraanil + Font + Vaikimisi + Seadme süsteemifont + Krediitfondi fail + Krediitfondi fail + Valige TTF või OTF fondifail + Valitud fail ei ole TTF või OTF font + Kohandatud fonti ei õnnistunud laadida ja see on välja lülitatud + Mittemeeldimised ei ole hetkel saadaval (API aeg läks läbi) + Meelemitamatuse märkimise funktsioon ei ole saadaval (staatus %s) + Meelemitamatused ei ole saadaval (kliendi API piirang) + Meelemitamatused ei ole saadaval (%s) + Omaniku poolt peidetud + Mitte meelda - selle video koos %s teise inimesega + Ei meeldi seda videot, %s hääletest on negatiivseid + Lülita Return YouTube Dislike sisse + Kuva meelemitamatused protsendina + Kuvab meelemitamatused protsendina meeldimistest asemel arvuna + Kuva hinnangulikud meeldimised + Kuvab hinnanguliku meeldimiste arvu videotel, kus on looja meeldimised võtta välja + Kuva ühenduse vea teavitused + Kuvab teavituse, kui Return YouTube Dislike server ei ole saadaval + "Return YouTube Dislike taastab dislike'ide arvud, mille YouTube 2021. aastal peitis. Arvud pärinevad enne eemaldamist arhiveeritud andmetest ja uemate videote puhul on need hinnatud kasutajate hääletuste põhjal, seega on need ligikaudsed ja uuenevad iga mõne päeva tagant + +Vaadatud video ID saadetakse Return YouTube Dislike API-sse ning teie like'id ja dislike'id saadetakse anonüümselt, et täpsustada hinnanguid" + Selle seadme Return YouTube Dislike API statistika + API vastuse aeg, keskmine + API vastuse aeg, minimaalne + API vastuse aeg, maksimaalne + API vastuse aeg, viimane meedia + Mitte meeldimised ei ole hetkel saadaval - kehtib kliendi API kiirusepiirang + API häälete kuvamine, kõvede arv + Võrgukõnesid ei tehtud + %s võrgukutset tehtud + API häälete kuvamine, aegloppu läinud kõvede arv + Ükski võrgukõne ei läbinud aegloppu + %s võrgukutsest aeg läbenud + API kliendi kiirusepiirangud + Kliendi kiirusepiirangutega ei kokku tulle + Kliendi kiirusepiiranguks kokku %s korda + %s millisekundit diff --git a/patches/src/main/resources/addresources/values-et-rEE/shared/strings.xml b/patches/src/main/resources/addresources/values-et-rEE/shared/strings.xml index 31cdbe4dcb..ea7609b341 100644 --- a/patches/src/main/resources/addresources/values-et-rEE/shared/strings.xml +++ b/patches/src/main/resources/addresources/values-et-rEE/shared/strings.xml @@ -17,21 +17,219 @@ Second \"item\" text" --> + Seaded + Kas olete kindel, et soovite jätkata? + Salvesta + Lä resetting + Lähtesta värv + Keelatud värv + Vajalik taaskäivitus + Selle muudatuse jõustumiseks taaskäivitage rakendus + Taaskäivita + Importee + Kopeeri + Morphe seaded lähtestati vaigväärtustele + Imporditi %s seadet + Importimine ebaõnnestus: %s + Navigeeri üles + Otsi seadetest + INFO + Tulemusi \'%s\' jaoks ei leitud + Proovige teist märksõna + Viimased otsingud + Eemaldada otsingutooriust? + Eemalda otsinguloolt + Tõhista otsingutoorius + Kas olete kindel, et soovite kogu otsingutooriuse tõhista? + Otsingunõuanded + "• Puudutage rada, et sellele navigeerida +• Pikkvajutage seadet, et sellele navigeerida +• Vajutage Enterit, et salvestada otsingupäring tooriusse +• Otsing ignoreerib suurt/väikset tähte ja kirjutsümboleid +• Vanemad seaded kuvatakse üle keelatud alumiste seadete" + Otsingukugu on tühi + Otsingukuguse salvestamiseks sisestage otsingupärisus ja vajutage Enterit + Kuva seadete otsingukugu + Morphe seadete nimi + Vaikimisi + Lisad + Kohandused + Morfingu aeg + Kohandatud + Sisestage nimi + Nimi ei saa olla tühi + Kuva Morphe seadete ikoonid + Kopeeritud: %s + Morphe keel + "Mõne keele tõlked võivad puududa või olla mittäielikud. + +Uute keelte tõlmiseks või olemasolevate tõldete parandamiseks külastage morphe.software/translate" + Rakenduse keel + Import / Eksport + Importeerite / Eksporteeri Morphe seadeid + Impordi failist + Seaded imporditi edukalt, rakendamiseks puuduta Salvesta + Seadete import ebaõnnestus + Ekspordi faili + Seaded eksporditi edukalt + Seadete eksport ebaõnnestus + Kasutate uusimat Morphe Patches versiooni <i>%s</i> + Kasutate Morphe Patches versiooni <i>%1$s</i><br><br><b>Uuendus saadaval: <i>%2$s</i></b><br><br>Patshide uuendamiseks kasutage Morphe\'i selle rakenduse uuesti patshimiseks + Märkus + See versioon on eelväljalaskmine ja võite kohata ootamatuid tõrkeid + Ametlikud lingid + Veebisait + Doniere + Tõlked + Tunnustused + Uuimõid puuduvad + Uuendus saadaval + Praegune arendus + Eelmine arendus + Aktiivne arendus + 2022 ‑ 2025 + 2022 ‑ 2025 + 2019 ‑ 2022 + Litsentsiteavitused + Laaditakse… + Veaotsing + Konfigureerige arendaja täpsemad suvimused ja diagnostikalogid + Luba veaotsingulogid + Veaotsingu logi stack trace + Lisab v veaotsingulogidele viitevoodid, et aidata tõrkeid tuvastada + Kuva Morphe tõrke korral teavitus + "Vea teavituste väljalülitamine peidab kõik Morphe vea teavitused. + +Teid ei teavitatakse ühegi ootamatust sündmusest" + Eksporteeri veaotsingupäevikud + Eksporteeri Morphe debug-logid + Veaotsingupäevikud eksporteeriti edukalt + Veaotsingupäevikute eksport ebaõnnestus + Otsi debugi logidest… + Tühista + Veaotsingu logimine on keelatud + Veaotsingupäevikuid ei leitud + Veaotsingupäevikud kopeeritud + Veaotsingupäevikute eksport ebaõnnestus: %s + Veaotsingupäevikud tühistatud + Funktsioone lipukite haldard + Halda boolease funktsioone lipukite + Otsi funktsioonilippude hulgast… + Mitte aktiveeritud (%s) + Aktiveeritud (%s) + Blokeeritud (%s) + Sunnitud (%s) + Automaatne + Väljas + Sisse + Sunni sisse + Blokeeri + Eemalda + Funktsiooni lipud salvestatud + Funktsiooni lipud lähtestatud + Funktsiooni lipud kopeeritud lõikelauale + Selle seade kasutamiseks taaskäivitage rakendus + Eksportimine ja importimine + Importeeritud %s funktsiooni lippu + Funktsiooni lippe ei leitud + Funktsiooni lipud eksporteeriti edukalt + Funktsiooni lippude eksport ebaõnnestus + Funktsiooni lipud laaditi, puudutage \"Salvesta\", et rakendada + Faili lugemine ebaõnnestus + Binaarne otsing + "Leiab, milline funktsiooni lipp põhjustab rakenduse käitumist. + +Poolest %s lipud on blokeeritud ja pärast iga taaskäivitamist teatate, kas käitumine on endiselt olemas. + +Taastage käitumine üks kord enne alustamist, vastasel juhul puuduvad otsingust lipud, mida rakendus veel ei ole küsitud" + Alusta + "Otsimiseks ei ole funktsiooni lippe + +Binaarses otsingus saab kasutada ainult neid lippe, millega rakendus on kokku põlnud ja mis ei ole hetkel sunnitud olema sisselülitatud" + "Samm %1$s + +Jäljках on %2$s lippi, millest %3$s on selle sammu jaoks blokeeritud. + +Taaskäivitage rakendus, kontrollige, kas käitumine esineb, ja vastake allpool" + Funktsiooni lipi otsing on käigus + Käitumine esineb + Käitumine ei esine + Tühista binaarne otsing + Binaarne otsing tühistatud + "Funktsiooni lipi %s põhjustab käitumist. + +See on nüüd blokeeritud" + "Nuancesed nüanssilippi ei õnnestunud leida. + +PaistCaractéristiques, et rakenduse käitumist ei owni nüanssiputk kontrollima" + "Ühte nüanssiputke ei õnnestunud leida. + +Käitumine kadus otsingul varem, seega võib olla kaasatud rohkem kui üks lipp või oli üks vastustest vale" + Ava MicroG seaded + MicroG ei ole installitud. Installige see + Vajalik tegevus + "Teie MicroG versioon %1s on aegunud ja võib põhjustada tõrkeid. + +Palun uuendage MicroG uusimale versioonile %2s" + Uuenda + Ignoreeri + "Installitud MicroG versiooni ei soita Morphe: + +%1s + +Puudutage, et see eemaldada, avage seejärel see rakendus uuesti ja installige MicroG RE" + Desinstalleeri + "MicroG-il ei ole luba taustal töötada. + +Järgige oma telefoni kohta oleva \"Don't kill my app\" juhendit ja rakendage juhiseid oma MicroG installatsiooni suhtes. + +See on rakenduse toimimiseks vajalik" + Ava veebisait + "Et vältida probleeme, tuleb MicroG akupõhised optimeerimised keelata. + +MicroG akupõhiste optimeerimiste keelamine ei mõjuta negatiivselt aku kulu. + +Puudutage jätkamise nuppu ja lubage optimeerimise muudatused" + Jätka + Kuva MicroG akupila optimeerimise dialoog + "Selle väljalülitamine peidab MicroG aku optimeerimise teavituse. + +Kui aku optimeerimist ei muudeta, võib esineda järgmist: +• Rakendus võib juhuslikult sulguda +• Teavitused ei prugu töötada +• Taustal taastoomine ei prugu töötada" + Rakenduse linkide haldamine + Konfigureeri linkide avamine selles rakenduses + Samm 1: Keela algse rakenduse lingid + Avage algse rakenduse seaded ja keelake kõik domeenilingid + Samm 2: Luba paigatud rakenduse lingid + Avage paigatud rakenduse seaded ja lubage kõik domeenilingid + Ava seaded + 🚨 Eksperimentaalne teavitus 🚨 + "Kasutate ⚠️ <b>%1$s</b> eksperimentaalset rakenduse versiooni +<br/><br/> +🔧 Oodake rakenduse käitumisel erisusi või tuvastamata tõrkeid, kuna me viime selle versiooni plaasterid täpsusele. +<br/><br/> +Kui soovite võimalikult tõrgetemat kogemust, siis <b>eemaldage</b> see ja paigaldage soovitatud rakenduse versioon ✅ <b>%2$s</b>" + ⚠️ Soovin eksperimentaalset + ✅ Soovin stabiilset + Crash-log salvestati kasutaja dokumentidesse: %s + Crash-log salvestatud: %s diff --git a/patches/src/main/resources/addresources/values-et-rEE/sponsorblock/strings.xml b/patches/src/main/resources/addresources/values-et-rEE/sponsorblock/strings.xml index 9d2d4ab7d5..9af20d3629 100644 --- a/patches/src/main/resources/addresources/values-et-rEE/sponsorblock/strings.xml +++ b/patches/src/main/resources/addresources/values-et-rEE/sponsorblock/strings.xml @@ -16,7 +16,219 @@ Second \"item\" text" --> + Luba SponsorBlock + SponsorBlock on rahvussõnumisel põhinev süsteem YouTube\'i videote ärritavate osade möödamineku jaoks + Välimus + Kuva hääletamisnupp + Lisab nupu segmentidele positiivse või negatiivse hääle andmiseks + Kasuta ruudukujulist paigutust + Kasutab SponsorBlocki nuppide ja juhtimiste jaoks nurkkasutust + Kasuta kompaktnuppu \"Mööda\" + Vähendab nupu \"Mööda\" laialgust, et säästa ruumi + Peida \"Jäta üle\" nupp automaatselt + Peidab \"Jäta üle\" nupu mõne sekundi pärast selle asemel, et hoida seda kogu segmendi vältel nähtaval + \"Jäta üle\" nupu kestus + Määra, kui kaua \"Jäta üle\" nupp jääb nähtavaks + Kuva teavitus jätmise kohta + Kuvab teavituse, kui segment jäetakse automaatselt üle + Jätmise teavituse kestus + Määra, kui kaua undo-teavitus jääb nähtavaks + 1 sekund + 2 sekundit + 3 sekundit + 4 sekundit + 5 sekundit + 6 sekundit + 7 sekundit + 8 sekundit + 9 sekundit + 10 sekundit + Kuva video pikkust ilma segmentiteta + Käibemajutisse lisatud video kestusest lahutatakse kõik segmendid + Uute segmentide loomine + Kuva nupp \"Loo uus segment\" + Lisab videovõljatuse nüppi, mis võimaldab esitada uusi SponsorBlock segmente + Aja reguleerimise samm + Aja reguleerimise nuppide liigutamise kogus (millisekundites) + Väärtus peab olema positiivne arv + Esitamise juhised + Juhised sisaldavad regleid ja nõuandeid uute segmentide loomiseks + Järgige juhiseid + Lugege enne uute segmentide loomist SponsorBlock juhiseid + Juba lugenud + Näita mulle + Üldine + Kuva ühendusvigade teavitused + Kuvab teavituse, kui SponsorBlock server ei ole saadaval + Luba möödaminekute arvamist + Saadab läskamise arvud SponsorBlocki edetabelisse, et jälgida säästetud aega + Segmendi minimaalne kestus + Segmente, mis on sellest väärtusest (sekundites) lühemad, ei kuvata ega läskita + Keelatud aja kestus + Sinu privaatne kasutaja-ID + Seda tuleks hoida privaatseks. See on nagu parool ja seda ei tohi kellegagi jagada. Kui keegi selle omab, saab ta end sinuna välja anda + Privaatne kasutaja-ID peab olema vähemalt 30 märki pikkune + Muuda API URL-i + Aadress, mida SponsorBlock kasutab serveriga suhtlemiseks + Kanalite valge nimekirja + SponsorBlock on valge nimekirja kanalite puhuliisvitatud + Teavita valge nimekirja kanali puhul + Kuva teavitus, kui SponsorBlock on valge nimekirja kanali puhul tuvitatud + SponsorBlock on selle kanali puhul tuvitatud + API URL lähtestamine + API URL on validity + API URL on muutunud + Seadete import / eksport + Kopeeri + Teie SponsorBlock JSON-konfiguratsioon, mida saab importida/eksportida Morphe\'i ja teistesse SponsorBlock platvormidesse + "Teie SponsorBlock JSON-konfiguratsioon, mida saab importida/eksportida Morphe'i ja teistesse SponsorBlock platvormidesse + +Selle hulgas on teie privaatne kasutaja ID. Jagage seda targalt" + Seaded eksporteeriti edukalt + Importimine ebaõnnestus: %s + Eksportimine ebaõnnestus: %s + "Teie seadistused sisaldavad privaatset SponsorBlock kasutaja ID-d. + +Teie kasutaja ID on nagu parool ja seda ei tohi kunagi jagada" + Ära näita uuesti + Muuda segmendi käitumist + Sponsor + Tasutatud reklaam, tasutatud viitused ja otsesed reklaamid. Mitte enese reklaamimiseks või tasuta reklaamiks põhjustele / loojatele / veebilehtedele / toitetele, mida nad poelavad + Tasuta / Enese reklaamimine + Sarnaselt Sponsorile, kuid tasuta / enese reklaamimiseks. Sisaldab jaotiseid merchendise, annetuste või teavet koostööpartnerite kohta + Interaktsiooni meeldetuletus (Telli) + Lühike meeldetuletus meelda jätta, tellida või jälgida sisu keskel. Kui see on pikk või puudutab midagi konkreetset, peaks see olema jaotises \"enese reklaam\" + Olulisem osa + Video osa, mida enamik inimesi otsib + Vahepeatus / sissejuletuse animatsioon + Intervall ilma tegeliku sisuga. Võib olla paus, staatiline kaader või korduv animatsioon. Ei sisalda infot sisaldavaid üleminekuid + Lõppkaardid / lõpptiitrid + Lõpptiitrid või aeg, mil ilmuvad YouTube\'i lõppkaardid. Mitte info sisaldavate kokkuvõtete jaoks + Püütme / Tervitused + Kõrvaliselt jutustatud eelse video treilerid, tervitused ja hüvastused. Ei sisalda jaotiseid, mis lisavad täiendavat sisu + Eelvaade / Kokkuvõte + Klippide kogumik, mis näitavad, mis on tulemas või mis juhtus videos või sarja teistes videotes, kus kogu info kordub elsewhere + Kõrvateemad / Jutud + Kõrvateemad või jutud, mida ei ole vaja video peasisu mõistmiseks. Ei sisalda osa, mis annavad konteksti või taustainfot + Muusika: mitte-muusikaline osa + Kasutada ainult muusikavideote puhul. Muusikavideote osad ilma muusikata, mida ei ole juba teise kategooria alla võetud + Määra mööda + Rõhuta + Määra sponsor mööda + Määra reklaam mööda + Vaata interaktatsiooni üle + Hüppa põhipunktide juurde + Hüppa sissejuhatuse lõpule + Hüppa vaheaja lõpule + Hüppa vaheaja lõpule + Vaata lõppu ära + Skippa sissejuhatus + Vaata eelvaadet ära + Vaata eelvaadet ära + Vaata kokkuvõtet ära + Vaata kõrvalteemat ära + Vaata üle mitte-muusikat + Vaata üle segment + Sponsori osa oli mööda läinud + Enese reklaam oli mööda läinud + Tüütu meeldetuletus oli mööda läinud + Hüpatud olulisele osale + Sissejuhatus oli mööda läinud + Vaheala oli mööda läinud + Vaheala oli mööda läinud + Lõpposa oli mööda läinud + Kutse osa oli mööda läinud + Vaadati eelvaade ära + Vaadati eelvaade ära + Vaadati kokkuvõte ära + Vaadati kõrvalteema ära + Vaidatud ära osa, kus ei ole muusikat + Jäeti mööda esitamata segment + Jäeti mööda mitmeid segmente + Jäta automaatselt mööda + Jäta üks kord automaatselt mööda + Kuva nupp \"Jäta mööda\" + Kuva seekbaris + Välja lülita + Segmendi esitamisel tekkis viga: %s + SponsorBlock on ajutiselt unavailable + Segmendi saatmine ebaõnnestus (staatus: %1$s %2$s) + Segmendi esitamine ebaõnnestus. Kiirusepiirang (liiga palju samu kasutajat või IP-adresse) + Segmendi ei saa esitada: %s + "Segmendi ei saa esitada. +See on juba olemas" + Segment esitatud edukalt + SponsorBlock ei ole ajutiselt saadaval (API aeg läbis) + SponsorBlock ei ole hetkel saadaval (staatus %s) + SponsorBlock ei ole hetkel saadaval + Segmendi hääletamine ebaõnnestus (API aeg lõppes) + Hääletamine segmendi eest ebaõnnestus (staatus: %1$s %2$s) + Segmendi hääletamine ebaõnnestus: %s + Hääle pluss + Hääle miinus + Muuda kategooriat + Hääletamiseks ei ole segmente + %1$s kuni %2$s + Valige segmendi kategooria + Kategooria on seadistustesietatud. Saatmiseks aktiveerige kategooria + Uus SponsorBlock segment + Kas määrata %s uue segmendi alguseks või lõpuks? + Algus + Lõpp + Praegu + Aeg, millal segment algab + Aeg, millega segment lõppeb + Kas ajad on õiged? + "Segment on + +%1$s + kuni +%2$s + +(%3$s) + +Kas on valmis esitama?" + Saada + Algus peab olema lõpust enne + Märgi esmalt asukoht ajavõtl complimentsul + Vaata segmenti eelvaadet ja veendu, et see hüppab sujuvalt + Muuda segmendi ajastust käsitsi + Kas soovid muuta segmendi alguse või lõpu ajastust? + Sisestatud aeg on invalid + Saada märgistus + "Kas märgistuse aeg on õige? + +%s + +Kui te ei soovi märgistust saata, puudutage nuppu Tühista ja märkige teine asukoht" + Saada märgistus + Statistika + Statistika ei ole hetkel saadaval (API on katkestatud) + Laaditakse… + SponsorBlock oni välja lülitatud + Sinu kasutajanimi: <b>%s</b> + Kasutajanime muutmiseks puuduta siia + Kasutajanime ei õnnestunud muuta: Staatus: %1$s %2$s + Kasutajanimi on edukalt muutetud + Sinu maine on <b>%.2f</b> + Oled loonud <b>%s</b> segmenti + Puuduta siia, et näha oma segmente + SponsorBlock edukeed + Oled päästanud inimesi <b>%s</b> segmendist + Puuduta siia, et näha globaalseid statistikas ja peamisi panustajaid + See on <b>%s</b> nende elust.<br>Puuduta siia, et näha edukeeda + Oled jätnud mööda <b>%s</b> segmenti + See on <b>%s</b> + Kas lähtestada möödanud segmentide loendaja? + %1$s tundi %2$s minutit + %1$s minutit %2$s sekundit + %s sekundit + Läbipaistvus: + Värv: + "SponsorBlock on kogukonna poolt loodud videosegmentide andmebaas, mis sisaldab näiteks sponsoreid, sissejuhatusi, lõpptekste ja meeldetuletusi tellimise kohta. Segmente esitab ja hääletab kogukond ning andmebaas on avatud ja tasuta allalaaditav + +Vaadatud video ID saadetakse SponsorBlocki API-le, et saada selle segmentid" diff --git a/patches/src/main/resources/addresources/values-et-rEE/youtube/strings.xml b/patches/src/main/resources/addresources/values-et-rEE/youtube/strings.xml index 70ce03fc67..cf23c5c038 100644 --- a/patches/src/main/resources/addresources/values-et-rEE/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-et-rEE/youtube/strings.xml @@ -18,213 +18,1638 @@ Second \"item\" text" + Morphe\'st + Reklaamid + Uuendused + Üldine + pleier + Shorts + Keri riba + Puhkimise juhtimised + Muu + Video + Taasta YouTube\'i vana seadete ekraan + Eemalda taustal esituse piirangud + ্ষিকী taustal mängimise keelamine + Automaatne pause seadme lukustamisel + Väljas + Pauseeri alati lukustamisel + Pauseeri, välja arvatud kui juhtmevaba heli on ühendatud + Keela kerimise kiiruse piirang + Keela paigutuse uuendused + Hoiab serverit rakenduse paigutuse uuendamise eest + "Rakenduse paigutus taastatakse algse installimise ajal kasutusse võetud paigutusele. + +Mõned serveripoolsed paigutused ei pruugud taastuda. + +Muudatused hõlmavad: +• Mängija kiiremenüü komponendid (või seotud seaded) ei pruugu töötada +• Liiguvad numbrid ei ole animeeritud +• Raamatukogu vahekeel on nähtav +• Videokirjelduse muusikaosa ei pruugu töötada +• Mängija kiiremenüü üksusi ei saa peita +• Kontohalduse nupp ei pruugu Raamatukogu vahekeeles ilmuda" + Enableda feedi jutusemenüü filtrit + Feedi riputmenu filter + Sisestage uue rea abil eraldatud lendmenüü nimed filtreerimiseks + Luba Raamatukogu / Sina alumise menüü filter + Raamatukogu / Sina alumise menüü filter + Sisestage Raamatukogu / Sina alumise menüü nimed filtreerimiseks, eraldades neid uue rea abil + Peida albumikaarte + Peidab albumikaardid artisti kirjelduse alt + Peida artistikaarte + Peidab artisti kaardi otsingutulemuste ülaosast + Peida kanali nupud + Peidab nuppude nagu Tellige, Vaata kanalit, Kogukond ja YouTube Music kanaliprofiilidest otsingutulemustes + Peida kogukonnapostitusi + Peida kompaktsed bännerid + Peidab kompaktsed bännerid ja vahemaa mõne komponendi vahel + Laiaendavad kaardid videote all + Näita kõiki + Peida ainult kokkuvõtte kaart + Peida kõik + Peida ujuv mikrofoni nupp + Peidab ujuva mikrofoninupu otsingusoovituste ekraani paremas alumises nurgas + Peida nuppu \"Hangi Premium\" + Peita ajaloo riiul + Peida horisontaalsed riiulid + Peidab horisontaalseid riiulid, nagu Uudised, Postitused kogukonnast, mis võivad teile meelda, Jätka vaatamist, Avasta rohkem kanaleid, Avasta rohkem teemasid, Sulle, Uusimad videod, Kõige asjakohasemad, Postitused looja kogukonnast, Ostlemine ja Vaata uuesti + Peida pildiriiul + Peidab pildiribasid otsingutulemustes + Peida kaart \"Kutsu teisi sõnumitama\" + Peidab kutsekaardi, mis on näidatud teavituste vahekuva sõnumite jaotises + Peida nuppu \"Uuimad videod\" + Peidab sinise banneri \"Uuimad videod\", mis ilmub Avalehe voo ülaosasse + Peida otseülekanded + Peidab videoid, mis on praegu otseülekandena avatud koduse voogu ja otsingutulemused + Peida miks-loende + Peida filmide sektsiooni + Peida Teavituste menüü päis + Peidab kellanupu klõpsamisel Teavituste menüü päise + Peida nuppu \"Teavita mind\" + Peidab meletuse nuppu planeeritud otseülekannete ja esilinnauste puhul + Peida mängid + Peidab funktsiooni, mis võimaldab mängida mängusid + Peida otsingutermini pöialpildid + Peidab miniatuurvaateid otsingu automaalse täiendamise loendis + Peida nuppu \"Kuva rohkem\" + Peidab laiendamise nuppu otsingutulemustes videote all + Peida tellitud kanalite riba + Peida tellitud kanalite nimed + Peidab kanalite nimed tellitud kanalite riba jaost + Peida küsitlused + Peida küitlused ja meeldetuletused voo soovituste täpsuse kohta + Peida piletite shelfi + + Peida bännerit \"Mittetöötavad videod on peidetud\" + Peidab playlists bänneri \"Mittetöötavad videod on peidetud\" + Peida üleslaadimise aega + "Piirangud: +• Shortside riiulitel, kanalite lehtedel ja otsingutulemustel võib uploadi aeg ikka olla näidatud +• Ei toimi auto makettidega" + Peida video miniatuuri + Peida video märkmed + Peidab märkmeid nagu \"Automaatselt dubleeritud\" / \"Dubleeritud\", \"Teenib komisjoni\", \"Hüpitatud\", \"Inimesed vaatasid ka\", \"toode(d)\" ja \"Teile võib meelda\" + Peida vaatamiskordade arvu + "Piirangud: +• Shortside riiulitel, kanalite lehtedel ja otsingutulemustel võib vaadlstmiste arv ikka olla näidatud +• Ei toimi auto makettidega" + Peida veebiotsingutulemused + Peidab jaotuse \"Tulemused veebist\" otsingutulemustest + Peida jaotis \"Võite pidada huvitavaks\" + Peida YouTube Doodles\'id + Peidab hooajalist ja sündmustepõhiseid animatsioone YouTube\'i logos + YouTube Doodles\'i kuvamine võib takistada kohandatud päisete pistikute kuvamist + Peida kanali riba + Peida kanali vesimärki + Peita peatükid ja nupp \'Selles videos\' + Peida rahastuskasti + Peida häireteateid + Peida infrapaneele + Peidab informatiivseid kontekstpaneele, näiteks meditsiinilist teavet + Peida nuppu Liitu + Peida nuppu \"Otsevestluse taastuletus\" + Peidab vestluse taastuletamise nuppu täisekraani režiimis + Peida meditsiinilisi paneele + Peida pleieri žestivihjed + Peidab pleieri žestide ajal kuvatavad vihjed, näiteks \"Tõmba üles täpse otsingu jaoks\", \"Lase lahti tühistamiseks\" ja \"2x\" + Peida teavitusriba + Peida tellijate juhiseid + Peida nuppu \"Sünkronise videoga\" + Peida puuduta ja hoidi gradient + Peidab pleieri juhtimise gradienti, kui sõrme kiirendamiseks hoidmisel liigutatakse + Peida ajastatud reaktsioone + Peidab funktsiooni, mis võimaldab kasutajatel reageerida emojitega kommentaarides ja otsevestlussis + Peida menüü \"Abi ja tagasiside\" + Peidab menüü \"Abi ja tagasiside\", kui kasutatakse kolme punktiga nuppu, näiteks otsingutulemustel + Peida kasutajanimi + Peidab teie kanali kasutajanime vahelekuist \'Sina\' vahelekuist + Peida video pealkirja + Peidab video pealkirja täisekraani režiimis + Puhasta video altekst + Eemaldab videote altekstidest fraase nagu \'#\', \'Fundraiser\', \'product(s)\' ja \'Shop\' + Peida nupp \"Küsi Geminilt\" + Peidab nupu \"Küsi Geminilt\" paneeli \"Küsi selle video kohta\" allosast + Video kirjeldus + Valige, millised komponendid video kirjelduseis kuvatakse + Peida jaotis \"AI-ga loodud video kokkuvõte\" + Peida jaotis Küsi + Peida jaotis Atribuudid + Peidab jaotused \"Esiletõstetud kohad\", \"Mängud\", \"Muusika\" ja \"Mainitud inimesed\" + Peida kanalite linkide jaotist + Peida jaotust \"Peatükid\" + Peida jaotist Parandused + Peida jaotist \"Kursuse edenemine\" + Peida jaotist Avasta + Peidab jaotised Avasta seda kurssi ja Avasta podcaste + Peida jaotist \"Avasta seda kurssi\" + Peida jaotist \"Avasta podcaste\" + Peida esiletõstetud kanalite jaotus + Peida jaotist Esiletõstetud lingid + Peida esiletõstetud loendite jaotis + Peida jaotist Esiletõstetud videod + Peida hashtagide jaotis + Peida jaotis \"Kuidas seda tehti\" + Peida Hype-punkte + Peida jaotist Infokaardid + Peida jaotis \"Key Concepts\" + Peida jaotist Viktorid + Peida jaotis \'Otsi videost\' + Peida tellimusnupp + Peida transkriptiooni jaotist + Peida jaotist \"Video üksikasjad\" + Filtriv riba + Valige, kus soovite peita rullivat filtri riba + Peida kanali lehel + Peida kommentaarides + Peida voogudes + Peida seotud videotest + Peida otsingutulemustest + Peida vaatamishistooryast + Kanali lehekülg + Valige, millised komponendid kuvatakse kanali lehel + Luba kanali vahekaardi filter + Kanali vahekaardi filter + Sisestage kanali vahekülgide nimed filtreerimiseks, eraldades need uue rea abil + Peida kogukonnanupp + Peida liitumisnupp + Peida linkide eelvaadet + Peidab klõpsatavad veebilingid kanali päises + Peida märkend \"Ainult liikmetele\" + Peidab kanali filtriribalt märkendi \"Ainult liikmetele\" + Peida liikmete riiulit + Peidab kanali liikmete nimekirja + Peida postituste riiul + Peida nuppu Pood + Peida nuppu Tellimine + Avasta menüü + Valige, millised komponendid kuvatakse avastamismenüüs + Peita kursuste menüü + Peita nuppu \"Avasta\" + Peita menüüd \"Mood & ilu\" + Peita mängude menüüd + Peita Hype\'i menüüd + Peida õppimismenüü + Peita otseülekande menüüd + Peita liikomuse menüüd + Peida menüü \'Filmid & TV\' + Peida muusika menüü + Peita uudiste menüüd + Peida Playables menüüd + Peida podkastide menüüd + Peida ostlemise menüüd + Peida spordi menüüd + Peida YouTube Premium menüüd + Peida YouTube Studio menüüd + Peida YouTube Musici menüüst + Peida YouTube Kidsi menüüst + Peida YouTube Create\'i menüüst + Peida menüü \"Kuidas YouTube toimib\" + Peida õiguslikud ja privaatsusega teated jaluses + Kommentaarid + Valige, millised komponendid kuvatakse kommentaaride jaotises + Luba kommentaaride karusselli filter + "• Veenduge, et sisestate karusselli üksuse täpse nime (nt Küsi, Hype'i see video, Kuula YouTube Musicis) +• Karusselli üksuste peitmisel võib olla tahtmatuid kõrvaltoimeid +• Kui kõik karusselli üksused on peidetud, peidetakse ka kogu kommentaaride jaotis" + Kommentaaride karusselli filter + Sisestage kommentaaride karusselli nimed filtreerimiseks, eraldades need uue rea abil + Peida AI vestluse kokkuvõtet + Peida kanali reegleid + Peida kontekstisõnumeid + Peida päis \"Liikmete kommentaarid\" + Peida kommentaaride jaotist + Peida kommentaaride jaotis Avalehe voos + Peida kogukonna reegleid + Peida nuppu \"Loo Short\" + Peita nupp \"Ei meeldi\" + Peida emoji nuppu + Peida filtri riba valikuid + Peidab kõik filtri riba valikud peale \'Top\' ja \'Uusimast\' + Peida kingianimatsioonid ja kaardid + Peida nupp Kingitus + Peida info nupp + Peida live-채ti annetajate riba + Peida otsevestluse rullituskäigu nupp allpoolse + Peida otsevestluse tooltipid + Peidab vestluse sisendivälja juures kuvatavaid tooltipseid, näiteks teavitust ainult tellijatele avatud vestluse kohta + Peita menüünupp + Peidab iga kommentaari 3-punktilise menüünupu + Peida kommentaari eelvaadet + Peidab kommentaaride eelvaate video alt enne täieliku kommentaaride jaotise avamist + Kommentaar on Morphe poolt peidetud… + Luba minimalistlik kommentaaride nupp + Peidab libisevad kaardid ja nende leheseliku valimiku, mis kuvatakse kommentaaride nupul + Peida Tänu nuppu + Peida ajatempli nuppu + Peida nupp \"Peakfännid\" + Peita tõlkimise nupp + Peida teadet \"Kommentaare pole veel\" + Puhasta esile tõstetud otsingulingid + Puhastab esile tõstetud otsingulingid, jättes alles ainult algse lihtsa teksti + Märksõnafilter + Valige kindlad sõnad või fraasid, et peita videoid voogudes ja otsingutulemustes + Peida kommentaare märksõnade põhjal + Peida Avalehe videoid märksõnade põhjal + Peida otsingutulemusi märksõnade põhjal + Peida Tellimuste videoid märksõnade põhjal + Peidetavad märksõnad + "Sisestage peidetavaks soovitud märksõnad ja fraasid, eraldades need uue rea abil + +Märksõnad võivad olla suvaline tekst, mis kuvatakse videote pealkirjades või kanalite nimedes + +Sõnad, mille keskel on suurad tähed, peavad ülespaisema täpselt (nt iPhone, TikTok, LeBlanc)" + "Avaleht / Tellimused / Otsing / Kommentaarid on filtreeritud, et peita sisu, mis vastab märksõnadele + +Piirangud: +• Shorts-videoid ei saa peita kanali nime järgi +• Mõned kasutajaliidese komponendid ei prugu peidata +• Märksõna otsing ei prugu tulemusi anda" + Vastaid tervikuna + Märksõna või fraasi surroundingimine double-quotesiga takistab videote pealkirjade ja kanalite nimede osalist vastamist<br><br>Näiteks,<br><b>\"ai\"</b> peidab video: <b>How does AI work?</b><br>kuid ei peida: <b>What does fair use mean?</b> + Märksõna ei saa kasutada: %s + Märksõna kasutamiseks lisa tsitaadimärgid: %s + Märksõnal on vastupidised deklaratsioonid: %s + Märksõna on liiga lühike ja vajab tsitaadimärke: %s + Märksõna peidab kõik videod: %s + "• Avaleht: %s +• Tellimused: %s +• Otsing: %s +• Kommentaarid: %s +• Kokku: %s" + Statistika + Peidetud videod, viimased 24 tundi + Peidetud videod, kogu aeg + Kas lähtestada viimase 24 tunni loendaja? + Kas lähtestada kogu aja loendaja? + AI kanalite filter + Peida videod kanalitelt, mille on AiSList kogukonna nimekirjas märgitud AI-väljaja sisu + Blokeerimisnimekiri: kõrge kindlus + Hoiatusnimekiri: mõõdukas kindlus + Peida videod Avalehel + Peida videod otsingult + "• Avaleht: %s +• Otsing: %s +• Kokku: %s" + Blokeeritud kanalid + Hoiatusnimekirja kanalid + Lülitage sisse filter, et laadida kanaleid + Laaditakse kanaleid… + "Avaleht ja otsingutulemused on filtreeritud, et peita AiSList poolt märgitud kanalite videod + +Piirangud: +• Filtreeritud AI Shorts võivad nähtav olla kui tühi ala +• Mõned AI videod võivad siiski näha" + "AiSList on avatud lähtekoodiga YouTube'i kanalite nimekiri, mis avaldavad AI-ga loodud sisu ja mida hoiavad vabapiirid. Kanalid on kasutajate poolt teatatud ja enne lisamist kontrollitud + +Nimekiri downloadable on GitHubist ja videod filtreeritakse teie seadmel" + Teata AI kanalitest + Lisa teate kirje videomenüüsse + Lisa videomenüüsse kirje, mis teatab videole kanalist AiSList-ile + Teataja nimi + Nimi, mis kuvatakse avalikus AiSList edetabelis teie teatuste kõrval. Kui see on tühi, pole teatamine saadav + Teata AI kanalist + "Saada %s AiSList-ile, kasutades seda videot tõendina? + +Teata ainult kanalitest, mis: +• Kasutavad peamiseks sisuks AI hääli, visuaale, skripte või muusikat +• Tootavad AI tööriistadega ja vähese inimliku tööga sisu massiliselt + +Ära tea kanalitest, mis ainult AI-st räägivad või kasutavad AI-d vaid osade videote jaoks. + +Teated on avalikud ja neid ülevaadatakse enne kanali nimekirja lisamist. Eemaldamine on võimalik ainult GitHubi kaudu esitatud taotluse alusel" + See kanal on juba AiSList blokeerimisnimekirjas + See kanal on juba AiSList hoiatusnimekirjas + Teata + Kanalitest teatamiseks määra AI kanalfiltri seadetes esitaja nimi + Kanalit otsitakse… + Selle video kanalit ei õnnestunud leida + Kanalist on teatatud ja see ootab ülevaatust + AiSList ei suutnud raportit salvestada, proovige hiljem uuesti + Saadeti liiga palju reporteid, proovige hiljem uuesti + AiSList keeldus raportist + Raporti saatmine ebaõnnestus, kontrollige oma ühendust + Kanalite reklaamide valge nimekiri + Videod lubatud kanalitelt esitatakse koos reklaamidega, et nende loojad saaksid tasustatud. Kõikidel teistel kanalitel jäävad reklaamid blokeeritud + Lisa kanal reklaamide valgeloendile + Eemalda kanal reklaamide valgeloendilt + Peida looja poe riiul + Peida lõuerekraani poebänner + Peida üldised reklaamid + Peida kaubandusbännerid + Peida tasulise reklaami märkmeid + Peida mängija hüppaklaasreklaamid + Peida isiklikud sponsorikaardid + Peida ostulingid + Peida videoreklaamid + Peida YouTube Premiumi reklaamいd + Laaditakse… + Käesolev kanal: + Lisa valgeloendile + Eemalda + Lubatud kanalid + Ükski kanal ei ole valgeloendis + Kanal lisatud valgeloendile + Kanal eemaldatud valgeloendilt + Otsi kanalist + Lisab kanalite lehtedele teise otsingunupu, et otsida ainult sellest kanalist + Otsi kanalist + Kanali otsingutulemused + Selle kanali kohta ei leitud midagi + Seda kanalit ei õnnestunud otsida + Videod + Postitused + Selle kanalil ei leidud vastavaid postitusi + Asjakohsus + Kõige rohkem vaadatud + Kõige vähem vaadatud + Uusimad + Vanimad + Lühimad Shortid + Kõige pikemad + Link kopeeriti lõikelauale + Link ajatempliga kopeeriti lõikelauale + Näita nupku Kopeeri video link + Puudutage, et kopeerida video link; puudutage ja hoidke, et kopeerida koos ajatempliga + Kopeeri vaikimisi ajatempliga + Vahetab nupu \"Kopeeri videolink\" puudutamise ja vajutamise toimingut + Kopeeri video pealkiri + Kopeerimiseks puudutage ja hoidke sõrmel video pealkirja + Video pealkiri kopeeriti lõikelauale + Kopeeri kommentärid + Kopeerimiseks puudutage ja hoidke sõrmel kommentaari + Kommentaar kopeeriti lõikelauale + Puudutage ja hoidke all, et avada video menüü + Feed\'i video pikaajaline vajutamine avab selle menüü + Kuva \"Vaata hiljem\" nupp pikkpõhilistel piltedel + Vooguvide들의 miniatuurpiltedele kuvatakse nupp \"Vaata hiljem\". Selle pikavajutus avab valiku \"Salvesta loendisse\" + Laadi alla postituse pildid + Piltide või GIF-idega postituste \"Jaga\" nupu kõrvale kuvatakse allalaadimise nupp ning nende menüüsse lisatakse allalaadimise üksus + Laadi pildid alla + Laadi pilt alla + {one, one:\"Laaditakse pilti…\", other:\"Laaditakse %s pilti…\"} + Laaditakse pilti… + Pilt salvestati asukohta %s + Pildi allalaadimine ebaõnnestus + Selle postitusest ei leitud ühtki pilti + Lisa \"Küsige\" üksus riputmenuusse + Lisab video kokkuvõtte üksuse feedi riputmenuusse + "Küsige nõuab algselt sisselogitud kontot riigis, kus on funktsioon Küsige saadaval. + +Kui video, millel on nupp Küsige, on üks kord avatud, töötab see funktsioon kõigi videote puhul, isegi kui praeguses riigis funktsiooni Küsige ei ole." + Kokkuvõte videost + Selle video puhul ei õnnestunud funktsiooni Küsige avamist + Küsimine ei ole teie kontol või praeguses riigis saadaval + Kuva vaatamiste arv miniatuurpiltedel + Vooguvideote vaatamiste arv kuvatakse nende miniatuurpilte vasakus alumises nurgas, mitte pealkirja all. Mõne video puhul, näiteks riiulitel, ei pruugida vaatamiste arvu kuvatada + Kuva voo miniatuurpilte like\'ide arv + Kuva voo miniatuuri meeldimiste protsent + Otsi kommentaaridest + Puudutage kommentaarides otsingunuppu, et näidata ainult laaditud kommentaare, mis sisaldavad kõiki otsitavaid sõnu. Fraasi otsimiseks pange sõnad tsitaatidesse + Otsi kommentaaridest + Otsitavad sõnad + Otsi + Tõhjenda + Muutke sorteerimist või avage kommentaarid uuesti, et neid otsida + Otsing puhastatud. Muutke sorteerimiskorda või avage kommentaarid uuesti, et näidata kõiki kommentaare + Keela hüppakenekkus \"Logi sisse telerisse\" + Keela riistmete ühendamise hüppakeneken + Võta kasutusele topeltpuudutusega peatükide möödahüpamine + Võibib kahe sõrmega topeltpuudutuse žesti, mis hüppab peatükkide vahel ja mida võib kogemata ühe sõrmega käivitada + Laadige alla videoid, podcaste või lugusid kolmandate osapoolte allalaadimisrakendustega + Kuva mängija pealkatte allalaadimisnupp + Kuvab videomängijasse nupu, mis avab teie välise allalaadija + Kiirallalaadimine puudutusega + Kutsub allalaadima taustal. Allalaadija avamiseks puudutage ja hoidke all. Toetab YTDLnis ja YTindiri + Märgi allalaadimised vaadatudaks + Välise allalaadijaga allalaaditud videod lisatakse teie YouTube\'i vaatamish சாப்பிடusse + Full HD (1080p) + Keskmine (192 kbps) + Kiirallalaadimine + Video + Audio + Kõik valikud + Kuva allalaadimine Shorts\'i menüüs + Lisab Shorts playerile välise allalaadija ristmenüü üksust + Laadi alla + Hääletõlge + Loetakse video altekirju kõvali seadme tekst-kõne funktsiooni abil, sünkroonides need taastusega + Kuvab videopleieris pealise nuppu. Puudutage tõlgimise lülitamiseks, hoidke all kiirseadete jaoks + Keel + Tõlketeenuse pakkuja + Originaali heli kõvus + YouTube\'i originaali heli kõvus tõlke esitamise ajal + Tõlkimise helitugevus + Dubleeritud tõlkehääle helitugevus + Maksimaalne kõnekiirus + Maksimaalne kiirusekordaja, kui kõnet sobitatakse altekstislooti + "Tasuta tõlkimise API, API võtmet ei vaja. Tasuta tasandil kuni 5000 märki päevas või e-posti aadressiga kuni 50 000 märki päevas. + +Puudutage siid, et avada MyMemory.translated.net" + MyMemory e-post + Valikuline. Sisestage oma MyMemory e-posti aadress, et tõsta päevast piirangut 5 000lt 50 000le tänne. Jätke väli tühjaks, et kasutada anonüümset piirangut + "Tasuline AI tõlge OpenRouteri kaudu. Täitke üks kord saldo ja tõlkige tuhandeid videoid. Mistral Nemo on odavamaim valik, Gemma 4 pakub kõrgeimat kvaliteeti. + +Puudutage siia, et avada OpenRouter.ai" + OpenRouteri API võti + Teie OpenRouteri API võti. Looge võti aadressil OpenRouter.ai > Workspace > API Keys + OpenRouteri mudel + Custom + OpenRouter pole konfigureeritud + Enne selle teenuse valimist sisestage seadistesse oma OpenRouteri API võti + Tasuta + %s iga 100 videotunni kohta + Hääl + Süsteem + Multikeelne mees + Multikeelne naine + Mees + Naine + On aeg muuta kuju! + Tõlge pole saadaval + "Tõlketeenus tagastas HTTP- vea %s. Teie IP-aadress on tõenäoliselt piiratud kuni 24 tunniga. + +Tõlgete kasutamise jätkamiseks vahetage seadetes tõlketeenuse pakkujat või kasutage VPN-i" + Ära rohkem näita + "Teie seadetes on OpenRouteri API võti. + +Teie API võtit ei tohi kunagi jagada. Kõik, kellel on sellele juurdepääs, saavad kasutada teie OpenRouteri krediite" + OpenRouteri krediidipiirang on saavutatud + Teie OpenRouteri krediidipiirang on saavutatud. Täitke oma saldo või suurendage võti kulutilangi aadressil OpenRouter.ai + Ava OpenRouter.ai + OpenRouteri tõlge ebaõnnestus (HTTP %s) + MyMemory päevane piirang on saavutatud + Järgmine saadaval %s + Olete kasutanud tänase tasuta MyMemory tõlkekvooti (5000 märki). Lisage seadetesse MyMemory e-posti aadress, et tõsta päevast piirangit 50 000 märgile, või vaheldage tõlketeenust + Olete kasutanud tänast MyMemory tõlkekvooti (50 000 märki). Vaheldage tõlketeenust või proovige homme uuesti + MyMemory tõlge ebaõnnestus (staatus %s) + Luba video kordamine + Ära mäleta video korduvust + Hoiab ära video korduvuse praeguse oleku salvestamise järgmistele videotele + Kuva video tsükli nupp + Video kordamine on sisse lülitatud + Video tsükkel on väljas + Korduse vahemik + Algus + Lõpp + [hh:]mm:ss + [hh:]mm:ss (tühi = video lõpp) + Määra + Tühista + Korduv: %1$s - %2$s + Keelatud ajavahemikk + Kuva pildis-pilt nupp + Kuva pildis-pilt menüü + Lülita pildis-pildile + Kuva nupp \"Esita kõiki\" + "Puudutage, et luua kõigi kanali videote loend, hoidke all, et tõtta tagasi + +Info: Ei prugu töötada otseütluste puhul" + Playlisti genereerimise režiim + Kõik sisu (sorteeritud aja järgi, tõusev) + Kõik sisu (sorteeritud aja järgi) + Kõik sisu (sorteeritud populaarsuse järgi) + Ainult videod (Sorteeri aja järgi) + Ainult videod (Sorteeri populaarsuse järgi) + Ainult Shorts (Sorteeri aja järgi) + Ainult Shorts (Sorteeri populaarsuse järgi) + Ainult voimuvideod (Sorteeri aja järgi) + Ainult voimuvideod (Sorteeri populaarsuse järgi) + Kõik sisu ainult liikmetele + Videod ainult liikmetele + Ainult liikmetele mõeldud Shorts + Ainult liikmetele mõeldud otseüsteid + 재생목록을 생성할 수 없습니다, kuna kanali ID ei üksi + Kuva video uuesti laadimise nupp + Video ID pole saadaval. Palun laadi video käsitsi uuesti + Näita nuppu \"Salvesta hiljem vaatamiseks\" + Puudutage, et salvestada hiljem vaatamiseks; puudutage ja hoidke nuppu, et näha järjekorra valikuid + Video salvestati edukalt loendisse \"Vaata hiljem\" + See video on juba salvestatud loendisse \"Vaata hiljem\" + Seda videot ei saa salvestada loendisse \"Vaadata hiljem\" + Salvesta hiljem vaatamiseks + Kuva nupp \"Salvesta hiljem vaatamiseks\" mängija seadete menüüs + Võimaldab salvestada praegu vaadatavat videot jaotisse \"Hiljem vaadata\" video seadete menüüst + Näita nuppu \"Salvesta hiljem vaatamiseks\" lastevideotes + Võimaldab salvestada lastelülitega seotud videoid jaotisse \"Hiljem vaadata\" menüüst + Kuva nupp \"Salvesta hiljem vaatamiseks\" Shortsi menüüs + Võimaldab salvestada lühivideoid jaotisse \"Watch later\" menüüst + Suuna \"Vaata hiljem\" plaalistisse + Vaata hiljem + Lisa Morphe järjekorda + Säilita järjekord istunde vahel + Järjekord säilib pärast rakenduse taaskäivitamist + Vaheta Salvestamise ja Kolele lisamise tegusid kohapeal + Puudutage, et näha järjekorra valikuid; puudutage ja hoidke nuppu, et salvestada hiljem vaatamiseks + Esita järgmisena järjekorras tüüp + Vaikimisi + Kohaliku järjekord + Morphe järjekord + Morphe järjekord kliendi pooled + Lisa voo liugmenüü järjekorra üksus + Lisab täiendava liugmenüü üksuse. Aktiveeri see, kui \"Lisa järjekorda\" ei kuvata + Järjekord + Järjekord (%s) + Kõigi tühistamine + K队列 on tühi + Kasuta funktsiooni \"Esita järgmisena\" mõnal või teisel videol + Praegu mängib + Esita järgmisena + Esita järjekorra viimaset + Keskkorduta + Eemalda + Lisatud järjekorda + Seda videot ei õnnestunud järjekorda lisada + Playliste ei saa järjekorda lisada + Järjekord on täis + Videot ei õnnestunud käivitada + Reklaamide valge nimekiri riputmenuus + Lisab riputmenu üksuse, mis lubab reklaamidelt mängida selle video kanalil + Selle funktsiooni kasutamiseks võib olla vaja PotHelperit või \"Spoof video streams\" muutmist TV-le või Android VR sisselogimisele + Kiiruse valge nimekiri riputmenuus + Lisab kiiremenüü üksust, mis esitab selle video kanalit alati kiirusega 1,0x + Ei ole sisselogitud + Puudubloendi ID + Järjekord on tühi + Puudub video ID + Ebaõnnestus: %s + Järjekorda lisamine ebaõnnestus + Järjekorra loomine ebaõnnestus + Eemaldamine järjekorrast ebaõnnestus + Järjekorra salvestamine ebaõnnestus + Lisatud järjekorda + Järjekord loodud + Eemaldatud järjekorrast + Järjekord salvestatud asukohta %s + Morphe järjekord + Lisa järjekorda + Lisa järjekorda ja ava + Lisa järjekorda ja esita + Lisa ja laadi video uuesti + Eemalda kolejast + Eemalda ja ava kole + Eemalda ja laadi video uuesti + Ava kole + Salvesta järjekord loendisse + Loend on salvestatud + Salvestatud kohta %s + Tühjenda java + Järjekord tühjendatud + Järjekord + Esita järjekorda viimasena + Esita järjekorda järgmisena + YouTube ei saanud tugevaatset kuvada. + Järjekord ei ole selle video jaoks valmis. Avage videopleier ja proovige uuesti. + Lisata Mixi järjekorda või vaadata Mixi? + Vaata + Lisa järjekorda + Laaditakse Mixi… + Ava kanal otseütluse avatari kaudu + Hoidab otseütlust avanmast, kui puudutatakse kanali avatari + Keela täpne otsingužest + Võib välja lülitada täpse otsingu režiimi, mis aktiveerub otsinguriiba üles tõmbamisel + Luba otseülekande DVR + Võimaldab mähistamist kõigis otseülekannetes, isegi neis, kus see on tavaliselt piiratud + Luba seekbari pöhialkume + Näitab video pöhialkume seekbari lohistamise ajal + Kõige rohkem kordatud + "\"Luba otseülekande DVR\" ei pruugi olla saadaval + +Selle parandamiseks luba \"Imiteeri videovogusid\" ja muuda \"Vaikimiskliendi\" väärtuseks \"TV Simply\" või \"visionOS\"" + Laienda otseülekande DVR-i kestust + Võimaldab otseülekandeid kuni 7 päeva tagasi kerida + "\"Otseülekande DVR-i kestuse laiendamine\" ei owni ole saadaval + +Selle parandamiseks lülita sisse \"Videovoogude imiteerimine\" ja muuda \"Vaikimiskliendiks\" \"TV Simply\" või \"visionOS\"" + Luba libistamine otsingu jaoks + Lülitab sisse libistamise abil otsingu asemel 2x kiiremaga esituse, kui videopleeri all hoida + Luba puudutusega otsing + Puudutage lihtsalt otsinguriiba, et hüpata vastavasse kohta ilma seda lohistamata + Vasakule pühkimise žest + Paremale pühkimise žest + Üles pühkimise žest + Vasak/parek tsooni laius + Iga ekraaniserva protsent, mis on reserveeritud vasakule ja paremale pühkimise žestide jaoks (5-50) + Heledus + Helitugevus + Põhiline + Väljas + Ignoreeri pühkimist lukustatud olekul + Pühkimise žeste ignoreeritakse, kui mängija on lukustatud funktsiooniga \"Ekraani lukustamine\" + Luba pühkimise žest vajutusega + Nõuab vajutamist ja hoidmist enne, kui pühkimisžest tuvastatakse + Luba haptiline tagasiside + Salvesta ja taasta helelus + Mäletab heleduse taset täisekraani režiimi sisumisel / sellest väljumisel + Luba automaatse heleluse žest + Heledu nihutamine madalaimale väärtusele aktiveerib süsteemi automaatse heleduse + Automaatne + Pühkimise katte aeglimine + Millisekundeid, mille vältel on kate nähtav + Pühkimise katte taustaläbipaistvus + Reguleer läbipaistvuse taset (0 on läbipaistev, 100 on läbipaistmatu) + Pühkimise katte heleduse värv + Määra heleduse reguleerimise edenemisriba värv + Pühkimise katte helitugevuse värv + Määra helitugevuse reguleerimise edenemisriba värv + Käigu kiiruse riba värv + Määra taastoetuskiiruse juhtimise edenduse riba värv + Pühkimise katte teksti suurus + Reguleeri ekraaniteksti suurust (1-30) + Pühkimise suuruse lävi + Määra vajalik pühkimise kaugus enne reguleerimise käivitamist + Heli tugevuse pühkimise tundlikkus + Kõrgemad väärtused nõuavad ühe heliastme kohta rohkem pühkimist + Heliastmed + Heli tõstmine + Maksimaalsest helitutasest üles pühimine tõstab heli veelgi, mis võib valjat helisignaali moonutada + Seadme vaikimisseadistus + Heledu käigu tundlikkus + Kõrgemad väärtused nõuavad rohkem pühkimist ühe heledu sammu kohta + Kiiruse käigu tundlikkus + Kõrgemad väärtused nõuavad rohkem pühkimist ühe kiiruse sammu kohta + Kiiruse samm + Kiiruse tsooni kõrgus + Kiiruse žestitsooni kõrgus protsendina mängija kõrgusest (5-75) + Kiirus + Pühkimise katte stiil + Horisontaalne kate + Horisontaalne kate (minimalistlik - ülal), + Horisontaalne kate (minimalistlik - keskel) + Ringikujuline kate + Ringikujuline ülevõte (minimalistlik) + Vertikaalne ülevõte + Vertikaalne ülevõte (minimalistlik) + Luba videote vahetamine pühkimisega + Pühkige täisekraani režiimis, et hüppata järgmise või eelmise videole + Toimingunupud + Valige, millised tegevusnupud kuvatakse portreetri- ja täisekraani režiimides + Portreenuputud + Keela Meeldiva ja Tellimise sära + Hoiab Like\'i ja Tellimise nuppe säramast, kui looja neid mainitseb + Peida tegutsusriba + Peida Like\'i ja mitte-meeldiva nupud + Peida Jaga nupp + Peida nuppu \"Lõpeta reklaamid\" + Peida kommentaaride nupp + Peida nuppu Ühenda + Peida raporti nupp + Peida Remixi nupp + Peida allalaadimise nupp + Peida Hype\'i nupp + Peida reklaami nupu + Peida tänu nupu + Peida nuppu \"Küsi\" + Peida kanal ja tellimusnupp + Peidab kanali avataari ja tellimisnupu tegevusriba algusest + Peida tellimisnupp + Peidab kanalitähise kõrval toiminguriba alguses nuppud \"Telli\" ja \"Liitu\" + Peida nuppu \"Lõika\" + See ei pruugi mõne kasutajakonto puhul töötada + Peida nuppu \"Pood\" + Peida nuppu \"Salvesta\" + Peida nuppu Lisalt + Täisekraninuputud + Peida nuppu \"Ei meeldi\" + Peida nuppu \"Meeldib\" + Peida nuppu \"Live-vestlus\" + Peida nuppu Mix + Peida nuppu \"Rohkem videoid\" + Peida nuppu \"Playlista\" + Kiirtoimingute ülemine marginaal + Määra kiirtoimingute riba ülemine marginaal (0–32 dp) + Altekirjad + Hali automaticasest pealkirjadest ja pealkirjade küpsistest haldamine + Automaatsete altekstide nähtavus + Näita alati + Ära kunagi näita + Näita, kui heli on lämas + Näita, kui on muteeritud + Eelistatud altekstide keel + Valib esmalt pakkuja altekstid ja tõlgib need seejärel automaatselt eelistatud keelde + Väljas + Lisa täiendavaid automaatse tõlkimise keeli + Lisab automaatse tõlkimise menüüsse rohkem keeli + Määra alteksti küpsised + "Parandab YouTube'i vaikimispärase vea, mille tõttu võivad puuduvate küpsiste tõttu olla automaatset tõlke alpead saadaval olematud + +'Alpeade küpsiseid' tuleb pakkuda" + Altekirjade küpsised + Küpsised, mida määratakse Timed Text API (Caption Data API) päringutes + Hangi altekirjade küpsised + Klõpsake siia küpsiste hankimiseks + Küpsiste hankimine ebaõnnestus + Küpsised on juba olemas + Küpsised uuendati edukalt + Paranda transkriptsiooni + Lahendab eeltingimuste kontrolli ebaõnnestumise, mis võib takistada transkriptsiooni laadimist + Kuva altekstid pildis-pildis + Navigeerimisnupud + Valige, millised nupud alumises navigatsioonisribas kuvatakse + Peida nupp Avaleht + Peida nupp Shorts + Peida nupp Loomine + Peida Tellimused nupp + Peida nuppu \"Teavitused\" + Näita nuppu \"Otsi\" + Otsinuppu asukoht + Näita nuppu \"Seaded\" + Seadete nuppu asukoht + Seadistuste nupu tegevus + Avab Morphe seaded, kui on aktiveeritud, või YouTube seaded, kui on väljatud + Vaheta Loo nuppu Teavituste nupuga + Kui selle seade muutmise tõttu midagi ei muutu, proovi lülitada Incognito režiimi + Peida navigeerimisnuppude märgid + Peida uue sisu märk + Peida navigatsiooniriba + "See peidab täielikult navigatsiooniriba, sealhulgas vahekaardi \"Sina\". + +Seadetele pääsemiseks aktiveerige seadete nupp jaotises Üldine → Rippbaar → Näita seadeid" + Luba kitsad navigeerimisnupud + Vähendab nuppude vahelist horisontaalset vahe + Keela navigatsiooninupud ilma märgistusteta + Kuva navigatsiooninupud märgistustega, kui YouTube saadab need ilma märgistusteta + ্ষিকীuuliku navigatsioonitabide keelamine + Kuvab Tellimused navigatsiooniribas, kui YouTube liigutab selle Avalehe ülaosas olevasse tabi + Luba navigeerimisriba animatsioonid + Animeerib üleminekud vahekate vahel lülitades + ্ষিকী navigeerimise riba automaatset peitmist + Hoiab navigatsiooniriba kerkimise ajal nähtavna + Navigatsiooniriba stiil + Vaikimisi + Poolläbipaistev + Läbipaistev + Läbivaatamatu + Seda stiili ei prugu teie seade või YouTube\'i versioon toetada, mis võib põhjustada paigutuse probleeme või rakenduse kokkujärge + Rippriba + Valige, millised komponendid kuvatakse tööriistapaneelil + Peida Casti nupp + Peida vestluse nupp + Peida mikrofoni nupp + Peida otsingu nupp + Kui see on aktiveeritud, avab puudutus Morphe seadeid ja pikk vajutus YouTube\'i seadeid; asendamiseks disableeri + Hüppmenüü + Valige, millised komponendid kuvatakse ristiku menus + Peida menüü \"Täisetteved\" / Rohkem + Peida menüü \"Ümbritsev režiim\" + "Menüü \"Audio Track\" on peidetud + +Menüü \"Audio Track\" kuvamiseks muutke \"Spoof video streams\" milleks igaks kliendiks peale \"Android Studio\" ja \"Android VR Downgraded\"" + Peida menüü \"Helirada\" + Peida menüü \"Helirada\" jaluse + Peida menüü \"Altekirjad\" + Peida menüü \"Altekirjad\" jaluse + Peida menüü \"Altekirjad\" päis + Peida menüü \"Abi ja tagasiside\" + Peida menüüt \"Kuula YouTube Musicis\" + Peida menüüt \"Lukitusekraan\" + Peida menüü \"Kordi esitamine\" + Peida menüü \"Liikumise ajal\" + Peida kvaliteedi menüüt + Peida kvaliteedi menüü jalust + Peida kvaliteedi menüü päis + Peida menüü \"Unutaja\" + Peida menüüt \"Taasestuskiirus\" + Peida menüü \"Stabiilne helitugevus\" + Peida menüüt \"Vaata VR-is\" + Peida automaatse esituse eelvaadet + Peidab täisekraani režiimis tuleviku video eelvaate enne automaatset esitust + Ülevoidenupud + Valige, millised nupud kuvatakse üle videopleieri + Peida nuppu \"Automaatne esitus\" + Peida altekirjade nupp + Peida Casti nupp + Peida kokkupressimise nupp + Peida täisekraani nupp + Peida mängija juhtimiste taust + Peidab videoesitaja juhtimispaneeli ümbritseva tumeda varjenduse + Juhtnupude tausta läbipaistvus + Reguleeri mängija juhtnupude ja ajav riba tausta läbipaistvust (0 on nähtamatu, 100 on läbimääramatu) + Peida nupud Eelmine & Järgmine + Peida seadete nupp + Peida lõpuekraani kaardid + Ümbritsev režiim + Juhi Ambient-režiimi nähtavust ja energiapiiranguid + Määra Ümbritseva režiimi piirangutele mööda + Llubab Ambient-režiimil jääda aktiivseks akusäästurežiimis + Vuüli Ümbritsev režiim + Keela Ambient-režiim täisekraanil + Peida infokaardid + Keela jooksva numbri animatsioonid + Peatab meeldimiste ja vaatamiste arvude loendamise animatsiooni + Peida videomängija edendamise riba + Peida videominiatuuride edendamise riba + Peidab vaatamise edenemise riba videote põikpiltedest, mida oled juba vaatanud + Luba täisekraani suur edendamise riba + Peida Shorts + Peida kanali lehel + Peida Avalehe voos & seotud videotes + Peida otsingutulemustest + Peida Tellimuste voos + Peida video kirjeldusest + Peida vaatamishistooryas + Shorts mängija + Valige, millised komponendid Shortsi pleieris kuvatakse + Vuüli kahekordselt puudutades meeldimise märgistamine + Peida AI nupp + Peida märget \"Automaatselt dubleeritud\" + Peida kanali riba + Peida nupp \"Komentärid\" + Peita efekti nupp + Peida videolinki märge + Peida žestivihjeid + Peida nuppu \"Roheline ekran\" + Peita hashtagi nupp + Peida infopaneel + Peita liitu nupp + Peida nupp \"Meeldiv\" + Peida nuppu \"Meeldiv\" fountain-animatsioon + Peida otseülekande eelvaadet + Peita asukoha märge + Peida navigatsiooniriba + Peida nuppu \"Uued postitused\" + Peita pause nimistiku nupud + Peita kommentaari eelvaade + Peida nupp \"Remiks\" + Peida nupp \"Salvesta\" + Peida nuppu \"Salvesta muusika\" + Peita otsingupakkumised + Peida nupp \"Jaga\" + Peita poodi nupp + Peida nupp \"Helid\" + Peida heli metandandmete märge + Peida Tellimise nuppu + Peida nuppu \"Osta Super tänut\" + Peida märgitud tooted + Peida nupp \"Tulevad\" + Peida nuppu \"Kasuta seda heli\" + Peida nuppu \"Kasuta seda malli\" + Peida video pealkiri + Peida lõppekraani soovitatud video + "Peidab soovitatud videokatte, mis ilmub pärast video lõppemist + +See seade töötab ainult siis, kui autoplaat on välja lülitatud. Seda saate muuta YouTube'is: +Seaded → Taasestus (või Autoplaat) → Järgmise video autoplaat" + Peida seotud videod + Peida «Rohkem videoid» üleskatte + Peidab \'Rohkem videoid\' katset, mis kuvatakse täisekraani režiimis ülespidi pühkimisel + Peida video ajatempel + Keela mängija hüppaklaaspanelid + Võibib playlists ja live-채ti paneelide automaatse avamise + Keela loendite automaatne esitus + Hoiab ära loendite automaatse likumise järgmise videoni + Võta kasutuse \"Jätkata vaatamist?\" prompts + Põlliekraani režiimist väljumine video lõpus + Välja lülitatud + Ainult portret + Ainult landscape + Portret ja maastik + Sunni maastikurežiim täisekraanil + Pöörda mängija maastikurežiimi pöördes täisekraanile. Ei ole saadaval väikese ekraaniga seadmetel, kus täisekraan avaneb juba maastikurežiimis + Täisekraani žestid + Mängija žestid täisekraani režiimis + Keela portreitrežiimi ülespidi pühimine + Keela portreitrežiimi allasidi pühimine + Keela pööratud ekraani allapoole pühimine + Keela loputamise abil zoomimise žest + Taasta loetamise zoomimine + Ava videod täisekraanirežiimis + Välja lülitatud + Vertikaalne + Horisontaalne + Täisekraani video skaal + Vaikimisi + Venitus (täida ekraan) + Zoom (lõika täitmiseks) + Näita video skaala nuppu + Puuduta, et vaheldada vaikimisi, venituse ja zoomi vahel + Kuva ainult täisekranis + Peida video skaalimise nuppu portreipleieris + Näita mute-nuppu + Playeri katse läbipaistvus + Reguleeri läbipaistvuse taset (0 on läbipaistev, 100 on läbimatu) + Otsinguriba + Välja lülitatud + Lai + Extra lai + Seadme paigutus + Vaikimisi + Telefon + Tahvler + Autotransport + "Muudatused hõlmavad: + +Tahvli paigutus +• Kogukonnapostitused on peidetud +• Voogudes esitamise seade ei ole saadaval +• Remixi nupp ja heli nupp ei ole Shortsis saadaval +• Ülejõu \"Rohkem videoid\" ei pruugi olla saadaval +• Kuvatakse vana video tegevusriba, mis pole kokku pressitud +• Liugmenüü üksused ei pruugi töötada korrektselt + +Auto paigutus +• Voog on organizeeritud teemade ja kanalite kaupa +• Voogudes esitamise seade ei ole saadaval +• Remixi nupp ja heli nupp ei ole Shortsis saadaval +• Ülejõu \"Rohkem videoid\" ei pruugi olla saadaval +• Shortsid avanevad tavalises pleieris +• Kuvatakse vana video tegevusriba, mis pole kokku pressitud" + Luba tahvli paigutus pleieris + "Taastab mängija vana paigutuse + +Piirang: kui \"Taasta vana video tegevusriba\" on välja lülitatud, ei prugu olla saadaval ülekatet \"Rohkem videoid\"" + 21.11.48 - Taastatakse alumiste kirjade vana käitumine + 20.39.41 - Taasta vana salvestus loendisse + 20.28.41 - Keela paksud ikoonid + 20.13.41 - Taastab kanali nime videopleyeri all + Ava süsteemi jagamisakend + Sisu jagamisel avatakse rakenduse asemel süsteemi jagamisleht + Asenda YouTube Musici nupud + Avab teie eelistatud muusikaäpi asemel YouTube Musici vaikimispärase rakenduse + Muusika rakenduse paketi nimi + Paigaldatud muusikaäpi paketi nimi + Algleht + Vaikimisi + Kõik tellimused + Sirgi kanaleid + Kursused / Õpe + Avasta + Mood & ilu + Mängud + Ajalugu + Raamatukogu + Hinnatud videod + Otseüste + Filmid + Muusika + Uutised + Teavitused + Playlistid + Otsing + Ostlemine + Shorts + Sport + Tellimused + Trendis + Virtuaalne reaalsus + Vaata hiljem + Sinu klipid + Võta kasutusele käivitamisel jätkamise keelustamine + Ava Shorts rakendusega + Shortsi pleier + Tavaline pleier + Tavaline pleier täisekraanil + Shortside automaatne esitus + Liigub automaatselt järgmisele Shortile asemel, et kortsata praegust + Automaatne esitus taustal + Liigub taustal esitamise ajal automaatselt järgmisele Shortile + Vaata üle otse-Shortse + Jätab mööda otseütlemised, mis ilmunevad Shorts\'i voos + Taastoomine feedides + Alati sisse + Ainult Wi-Fi + Väljas + Taasta algsed pealkirjad + Kuvab video algsed pealkirjad, videokirjeldused ja kanalite kirjeldused automaatsete tõlgete asemel + Laaditakse pealkirja… + Keela voo automaatne värskendamine + Hoiab ära voode automaatse värskendamise pärast nende aegsumist + Voogu kaardi paigutus + Klassikaline + Kaardid + Kaardid meeldimis-, kommentaar- ja salvestamisnupudega + Kommentaarid + Keela minipleieri jätkamine + Lülitab välja \'Jätka vaatamist\' minimängija, mis on YouTube Premium funktsioon ja ilmub rakenduse käivitamisel + Minipleier + Kohandage minimeeritud videopleieri välimust ja käitumist + Minipleieri tüüp + Peita pealkiri ja kanal + Välja lülitatud + Vaikimisi + Minimaalne + Minimalistlik 2 + Tahvel + Kaasaegne 1 + Kaasaegne 2 + Kaasaegne 3 + Kaasaegne 4 + Keela ümarad nurgad + Keela lohistamine ja kukutamine + Hoiab ära minimeeritud pleieri ekraani erinevatele nurkadele lohimise + Keela horisontaalne loh gesture + Hoiab ära minipleieri sulgemise, kui seda ekraanilt horisontaalselt külge tõmbatakse + Peatage taastamine horisontaalse lohistamisega + Taastoomine peatub, kui minipleierit lohistatakse ekraanilt välja + Keelelohistamise asukoha meletamine + Minimängija ekraanilt lohistamine säilitab selle kasutajaliidese oleku + Peida pealnupped + Vaikimisi suurus + Määra algne ekraanisuurus pikslites + Pikslite suurus peab olema vahemikus %1$s ja %2$s + Katte läbipaistvus + Reguleeri läbipaistvuse taset (0 on läbipaistev, 100 on läbimatu) + Tagasi nupp suleb feedi + Tagasi nuppu vajutamine sulgeb alati rakenduse selle asemel, et skrollida feedi ülaosasse + Sunni animatsioonid + Vaikimisi + Alati sisse + Alati väljas + Peida staatust riba + Peidab süsteemi olekuriba. Libista ülevalt alla, et näidata seda hetkeks + Paranda staatusriba asukut, et see ei kataks ülaribasid + Hoiab staatusriba paigutuse ühiseks, et see ei kataks ülaribasid ega jätaks mängija kohale tühja ruumi. Lülitage see välja, kui rakendus tõrkes või kui ülaribad näevad valesti + Kere + Seda seadet ei toeta säraefektiga avamise ekraan + Helle teema taust + Valge + Material You (valge) + Heleroosa + Hele sinine + Heleroheline + Hellekollane + Heleoranž + Helerõhke + Kobi helerebane taust + Määra hele teema taustavärv + Muuda etteplaani värvi + Kasuta teemavärvi teksti ja ikoonide jaoks + Luba gradiendne laadimise ekraan + Kuva laadimise ekraanil ühtlase värvi asemel gradienttaust + Luba kohandatud seekbari värv + Kerimisriba värv + Määra video edendamisriba peamine värv + Edendamisriba aktsentvärv + Määra video edendamisriba teisestkordne värv + Liigutuse riba invalide värviväärtus + Originaalne analoog + Originaalkook + Originaalne metall + Pealkirja logo + Vaikimisi + Tavaline + Kohandatud + Määra pildide piirkondlikud piirangud kõrvale + Suunab minipiltide päringud alternatiivsele serverile, et parandada puuduvate piltide probleemi piiratud riikides + DeArrow pealkirjad + Avaleht + Tellimused + Sina + Playeri loendid & soovitused + Otsingutulemused + Näita DeArrow ikooni + Näitab DeArrow ikooni kogukonna poolsete videopealkirjade juures + Vaba režiim + Kuivatab algse pealkirja asemel DeArrow pealkirja, kui DeArrow kasutajad on hääletanud, et neile meeldib algne pealkiri + Minimaalne häältes arv algse pealkirja kuvamiseks + Vabade häältes arv ühes kategoorias, näiteks naljakas või loov, mis on vajalik algse pealkirja kuvamiseks + DeArrow\' pöialpildid + Originaalsed põikpildid + DeArrow & originaalsed põikpildid + DeArrow & kaadutõmmised + Püsikaadrid + "DeArrow asendab klikkaatruse pealkirjad ja põhiste tõsasemate ja vähem sensatsiooniliste kogukonna loodud versioonidega. Pealkirjad on kogukonna kirjutatud ja hääletatud ning põhised on videost võetud kaadrid + +Videoid küsitakse video ID krüpteeritud prefiksi abil, et server ei saaks täpselt teada, millist videot vaadate. Kui videol puudub DeArrow põhis, kuvatakse originaalne või ekraanipilt" + Kuva ühenduse vea teavitused + Kuvab teavituse, kui DeArrow server ei ole saadaval + API lõpppunkt + DeArrow\' thumbnailide vahemälu lõpppunkti URL + Video püsipildid + Kõik pildid võetakse iga video algusest / keskelt / lõpust. Need pildid on sisseehitatud YouTube YouTube\'i ja välist API API ei kasutata + Ekstraheeri videokaader ajamast + Algus + Kesk + Lõpp + DeArrow ei ole hetkel saadaval (staatuskood: %s) + DeArrow ei ole hetkel saadaval + Taasta mängija nuppide vana stiil + Taastab videovõlutaja paksud juhtnupud vanasse, õhemasse visuaalsesse stiili + Seadme mõõtmete imiteerimine + Selle aktiveerimine võib avada kõrgema video kvaliteedid + Selle aktiveerimine võib põhjustada video esituse tõrgeteid, akkumahutuse kiiremat kulumist ja tundmatuid kõrvaltoimeid + Haptiline tagasiside + Valige, millised interaktsioonid käivitavad seadme vibramise + Peaatruku haptika keelamine + Täpse otsingu haptika keelamine + Otsingu tagasivõtmise haptika keelamine + Keela puuduta ja hoidi haptika + Zoomimise haptika keelamine + Kui muutsite hiljuti oma konto sisselogimise andmeid, siis eemaldage MicroG ja installige see uuesti + Mööda läheb linkide ümbersuunamisest + Avab algse lingi otse, mööberdades jälgimislingi (youtube.com/redirect) + Ava lingid brauseris + Avab lingid välisele brauserile asemel rakenduse sisesele brauserile + Taasestuse puhvri suurus + Vaikimisi + Madal + Keskmine + Maksimum + Video viivitus + Viivitab videot, et hoida see sünkroonis viivitatud heliga, näiteks Bluetoothi kõrvaklappidega (0–5000 ms) + Viivitus ainult Bluetoothi heliga + Viivitab videot ainult siis, kui on ühendatud Bluetoothi heli, et telefoni kõlariparalell oleks sünkroonis + Näita video viivitusnupku + Puudutage ja hoidke all, et lähtestada viivitus 0 ms-le + Näita video viivituse menüüd + Viivitus millisekundides + Automaatselt + Male videokvaliteedi + Rakendab teie manuaalse kvaliteedivaliku kõigile tulevasedele videotele + Teavitused kvaliteedi muutmisest + Kuvab teavituse, kui uus vaikimiskvaliteet on salvestatud + Vaikimisi videokvaliteet Wi-Fi võrgus + Vaikimisi videokvaliteet mobiilvõrgus + Male Shorts kvaliteedi + Rakendab teie käsitsi valitud kvaliteeti kõigile tulevasedele Shorts videotele + Vaikimisi Shorts-ide kvaliteet Wi-Fi võrgus + Vaikimisi Shorts-ide kvaliteet mobiilvõrgus + Muudeti vaikimisi mobiilikvaliteediks: %s + Muudeti vaikimisi Wi-Fi kvaliteediks: %s + Muudeti Shorts\'i mobiilkvaliteediks: %s + Muudeti Shorts\'i Wi-Fi kvaliteediks: %s + Näita taasestuskiiruse nuppu + Puudutage ja hoidke all, et lähtestada kiirus vaikimispärasele + Näita kvaliteedi nuppu + Puudutage ja hoidke all, et lähtestada kvaliteet vaikimispärasele + Kuva lahutus kvaliteednupul + Kuva lahvus, näiteks 1080, selle asemel, et kasutada märget nagu FHD + Krediitud taespaiski kiirusemenüü + Taasta vana taespase kiiruse menüü + Kohandatud taespide kiirused + Sisestage oma eelistatud taasestuskiiruse valik + Kohandatud taastoomikiirused peavad olema väiksemad kui %s + Kehtematud kohandatud taespide kiirused + Automaatne + Puudutage ja hoidke all taasestuskiirust + Määrake kiiruse kordistaja ekraani vajutamisel ja all hoidmisel; keelamiseks määrake 0 + Mäleta taespiski kiirust + Rakendab teie manuaalse kiirusevaliku kõigile tulevasedele videotele + Teavitused kiiruse muutmisest + Kuvab teavituse, kui uus vaikimisesituskiirus on salvestatud + Vaikimisi taespide kiirus + Vaikimisi taespaiski kiirus Shortsis + Mäleta taasestuskiirust Shorts + Rakendab teie manuaalset Shortsi kiiruse valikut kõigile tulevasedele Shortsidele. Kui see on väljas, taastub kiirus pärast Shortsi mängija sulgemist vaikimisi seadistusele + Kuva taeseski kiirus Shortside menüüs + Lisab Shortside mängija menüüsse jaanku taeseski kiirusega + Taeseski kiirus · %s + Vaikimisi taastoomikiirus on muutunud väärtuseks: %s + Kasuta muusika puhul tavakiirust + Esita muusikavideoid alati kiirusega 1.0x + Kanalite kiiruse valge nimekiri + Valge nimekirja kanalite videod esitatakse alati kiirusega 1,0x + Lisa kanal kiiruse valge nimekirja + Eemalda kanal kiiruse valge nimekirjast + Luba heli kõrguse juhtimine + Vaikimisi esituse heli kõrgus + Muudeti vaikimisi heli kõrgust väärtusele: %s + Heli ajalise venituse funktsioon + Võimaldab määrata kiirust ja kõrgust sõltumatult, võib põhjustada heliartefakte + Pea meeles otseülekande taastamise asukoht + Käimasolevate otseülekannete puhul jääb meelde ja jätkab taastamist sinna, kus jätsid, selle asemel, et liigutada praegusesse otseülekande asukohta + See funktsioon ei tööta 24/7 otseülekannete ja teiste \"alati sisse\" olevate voogude puhul + Võtta HDR-video kasutuselevõtt pois + Sunni HDR-video + Sunnib rakendust tuvastama seda seadet HDR-i toetavana + "Kasutage seda, kui HDR-i dekoodimine toimib, kuid YouTube ei paku HDR-i, näiteks mõnedel HyperOS seadmetel, mis peidavad HLG. + +Piirangud: +• Värvid võivad nähtav olla valed, kui ekraan ei suuda HDR-i tegelikult kuvada +• Esitamine võib ebaõnnestuda, kui dekooder ei suuda HDR-iga toime tulla +• HDR sõltub endiselt videovoolide imiteeriva kliendi seadetest" + Video koodek + Vaikimisi + AVC-i sundmiseks muuda «Imiteeri videovogusid« väärtuseks «Android Studio«, «Android VR« või «visionOS« + "VP9 +• Võib parandada akkumeele ja kõrvaldada taastootmise tõukumist +• Kasutab rohkem internetiandmeid kui AV1 + +AVC (H.264) +• Jäis videovoguste imiteerimiskliendiks «Android Studio«, «Android VR« või «visionOS« +• Võib akkumeele veelgi parandada +• Võib taastada puuduvad videoerilahjusest vanematel seadmetel +• Maksimaalne erilahjus on 1080p +• HDR-videod ei pruugi AVC-d kasutada +• Mõned seadmed ei saa AVC-d sundida" + Ava täpsem kvaliteedimenüü + Mootab kvaliteedi valikumenüü ja avab otse täisandliku kvaliteedimenüü + Peida Premium kvaliteedi valikud + Prioritiseeri video kvaliteeti + Prioritiseerib kõige kõrgemat saadaval olevat video kvaliteeti kodeki efektiivsuse asemel + Kui see seade on väljas, siis vanematel videodel ei pruugi olla video kvaliteedi valikuid + Luba \'Android VR\' / visionOS AV1 + Luba tarkvaraline AV1 dekodeerimine, kui kasutatakse \'Android VR\' / visionOS klienti + "Selle seade aktiveerimine võib kasutada tarkvaralist AV1 dekoodimist. + +AV1-i videote esitus võib tõrgetele tunduda või raame kaotada" + Väärentamise kõrvaltoimed + • Esitlus võib katkeda 1. minutil + • Menüü \'Helirada\' puudub + • Puudub AV1 videokoodek + • 360° VR imersiivne režiim ei ole saadaval + • Stabiilne helitugevus ei ole saadaval + • Esitus võib lõppeda 1 sekund enne aegust + • Kui deobfuskatsioon ebaõnnestub, võib esitlus peatuda + • Algse heli sundimine ei ole saadaval + • On vaja sisselogida Android VR-i + • Kvaliteet võib olla piiratud 720p-ga + Mängija JavaScripti variant + Kuva imiteerimine jaotises \'Statistika nerdidele\' + Taasta vana video tegevusriba + "Parandab probleemi, kus video tegevusnuppe ei saa peita + +Piirang: taastab vana stiili \"Rohkem videoid\" pealkatte" + Pleieri ikooni stiil + Automaatne + Vaikimisi + Õõsas + Paks + Kohandatud + Shorts ikooni stiil + Sarjade jälgija + Jälgi loendeid kui sarje ja säleta oma kohta + Luba sarjade jälgimine + Lisab ajaloo jaotisesse sarjade lehe ning loenditele võimaluse sarja jälgida + Kuva sarjad navigatsiooniribas + Näita sarjade jälgija nuppu + Lisab pleieri nuppu, et jälgida või avada praeguse video sarja + Registreeri jälgitavate sarjade edenemine + Registreeri esitus selles telefonis salvestatud sarjade ja märkide jaoks + Loppu + Salvesta + Salvesta märkija + Sarja nimi + Sarja valikud + Raamatukogu valikud + Värskenda episoode + Nime own + Ava loend YouTube\'is + Eemalda raamatust + Loendi link või ID + Kustuta kogu kohalik edenemine + Kustuta ajalugu + Kas kustutada kogu kohalik edenemine? + See tõrgutab kõigi videote positsioonid, vaadatud asendused ja jätkamisvalikud. Salvestatud sarjad ja nende kataloogid jäävad alles. Kui salvestamine on aktiveeritud, salvestatakse uus esitus + Märgi vaadatudaks + Märgi vaatamata + Kasuta automaatset staatust + Alusta uuesti algusest 0:00 + Esita uuesti algusest 0:00 + Staatus salvestatud + Korda salvestamist + Eelmised 100 + Järgmine 100 + Ava salvestatud video + Kohalik edenemine tühistatud + Ava ajalugu jaemõttes „Sina“, et näha oma sarje + Vaatamishistoria + Sarjad + Laaditakse salvestatud sarje… + Jälgige loendit, et pidada ülevaadet selle episoodidest + Loendi märkija + Värskendatakse episoode… + Kõik sarjad + Vaadatud + Laaditakse episoode… + Rohkem valikuid + "Salvesta edenemist ainult oma salvestatud sarjade ja märkitud videote puhul. + +See raamatukogu on salvestatud selles seadmes ja jagatud YouTube'i kontode vahel. YouTube'i paused ja ajaloo puhastamise juhtimispaneelid seda ei muuda. Puhastamiseks kasutage Sarah → Rohkem → Puhasta kohalik edenemine. + +Automaatne salvestamine peatub incognito režiimis, kui konto identiteet pole saadaval või kui vahetate kontot. Selle kasutamiseks selle kontoga aktiveerige jälitamine uuesti" + Jälitamist ei saa aktiveerida incognito režiimis või enne kui YouTube on laadinud konto praeguse oleku. Naasge Avaleleheküle ja proovige uuesti. Sisselogimist ei nõuta + Lülita sisse kohalik jälgimine + Edukumõõtmine on välja lülitatud + Jätka kohast %1$s + vaadatud episoodi %1$s / %2$s + Episood %1$s · %2$s + Episodid %1$s-%2$s kokku %3$s + Värskendage, et proovida episodide laadimist. Privaatsetele loenditele on vaja kontopääsu; nende kirjemärk avaneb siiski YouTube\'is + YouTube ei suutnud seda episoodi avada + YouTube ei suutnud seda loendit avada + Vali osa + Uuendatud + Puuduvad esitatavad osad + Salvestatud video · %1$s + Jälgi sarja + Vaata sarja + Jätka + Alusta siit + Alguspunkt salvestatud + Vaadatud siiani + Episoodid märgitud vaadatudaks + Undoi + Luba + %1$s uut episoodi + Registreeri edenemine selles seadmes + "Teie sarjad ja edukujutlus jäävad sellele seadmele ja on jagatud erinevate YouTube'i kontode vahel. YouTube'i ajaloo juhtimised neid ei muuda. Puhastage edukujutlus sarjade menüüst. + +Automaatne salvestamine on valikuline. See peatub incognito režiimis või kontot vahetades" + Episode ei õnnistunud laadida. Võite loendi kirjemärgina salvestada + Liiga palju salvestamata muudatusi. Proovige esmalt uuesti salvestada + Salvestamine on käigus. Proovige uuesti + Episoodi laadimine on käigus. Proovige uuesti + Salvestusala ei ole saadaval. Teie salvestatud andmed säilitati + Muudatusi ei õnnestunud salvestada. Proovige enne YouTube\'i sulgemist uuesti salvestada + See sari on juba salvestatud või on selle loend muutunud. Avage see uuesti + See sari on muutunud või eemaldatud. Avage raamatukogu uuesti + Loend on muutunud. Valige episood uuesti + See episood ei ole saadaval. Valige teine + Sisestage sarja nimi + Sisestage YouTube\'i loendi link või loendi ID + Palun oodake enne uuesti värskendamist + Episode ei õnnestunud laadida. Proovige hiljem uuesti + Muudatust ei õnnestunud teostada. Proovige uuesti + Jätkamist ei õnnestunud kinnitada. Kontrollige pleieri ja proovige uuesti + Jälgitavad sarjad + See video ei ole selles loendis + Loetakse playliste... + Kleepi playlisti link + Playlisti ei õnnistunud leida + Osad pole saadaval + Peida vaadatud + Märgi eelsemad episoodid vaadatudaks + Algab sõnaga: %1$s + Episoodide järjestus + Playlisti järjestus + Playlisti järjestus pööratud + %1$s vaatamist + %1$s vaatamine + Märgi episoodid vaadatudaks + Vaadatud on vähemalt %1$s%% ja alles on maha jäänud maksimaalselt 90 sekundit või %2$s sekundit + Märgi episood automaatselt vaadatud pärast mõlema lävuse saavutamist. Protsendi reegel nõuab samal ajal, et allesjäänud aeg oleks mitte rohkem kui 90 sekundit, et pikkade episoodide lõppu ei jäetaks mööda. Jäänud aja reegel kehtib videotele, mis on pikemad kui üks minut; selle väljalülitamiseks määra väärtuseks 0. Muudatused kehtivad tulevaste edenemise värskenduste kohta. Manuaalsed vaadatud/vaatamata valikud säilivad + Vaadatud protsent (1-100) + Jäänud sekundid (0-300; 0 lülitab välja) + Sisestage väärtus vahemikus %1$s kuni %2$s + Kasuta YouTube\'i edenemist + Jätka jälgitud sarjade vaatamist pärast teisel seadmel vaatamist + Loeta vaadatud staatus ja jätkamisasemed jälgitavate sarjade kohta praeguselt YouTube kontolt. Sellele telefonile salvestatakse ainult vastavad episoodid. Kontrollid toimuvad sarjade raamatukogu avamisel ja enne jätkamist. Teisel seadmel peab YouTube ajalugu olema lubatud + Kontrollitakse YouTube\'i edenemist… + YouTube edukusega ei ole võimalik ühendada. Kasutatakse salvestatud edukust + Osa YouTube edukusest pole saadaval. Salvestatud asemed säilivad + Varundamine ja taastamine + Salvesta oma sarjade raamatukogu faili või taasta varukoopia + Salvastab jälgitud sarjad, episoodide asukohad ja vaatamisvalikud. Salvestamise ja YouTube\'i edenemise loasid ei sisaldata + Salvesta varukoopia + Taasta varukoopia + Lisatakse %1$s sarja. %2$s sarja, mis on juba teie raamatukogas, jäävad alles. Olemasolev videoedenemus säilib + Sarjade varukoopia on salvestatud + Taastatud %1$s sarja + See fail ei ole toetatud sarjade varukoopiaga või on liiga suur. Teie raamatukogu ei muutunud + Varukoopia salvestamine ebaõnnestus. Proovige teist asukohta + Sari eemaldatud + See episood ilmub nüüd rohkem kui üks kord. Valige, millega jätkata + Teie salvestatud episood ei ole enam selles loendis. Teie salvestatud asukoht säilib + Jätka episoodiga %1$s · %2$s + Saate valida ka alltpool ükskõik millist episoodi. Varsemaid episoode ei muuteta + See esitlusloend on muutunud. Avage sari, et valida, kust jätkata + Selles suunas pole enam episoode + Jätka %1$s + Otsi sarju + Tühjenda otsing + Sarje ei leitud + Pealkirjata episood + Mittesaadav episood + Niconico kommentärid + Kuva otsevestlus ja vestluse taasesitus kommentäridena, mis liiguvad üle video + Kuvab videopleieris pealiku nupu. Puudutage, et kommentäre näidata või peita; vajutage ja hoidke kiirseadete avamiseks + Kommentaari kirja suurus + Kommentäride teksti suurus (12–40 sp) + Kommentaari läbipaistvus + Kommentaari teksti ja taustade läbipaistvus + Liikumise kestus + Kui palju sekunde kulub kommentaaridel ekraani läbimiseks. Madalamad väärtused liiguvad kiiremini + Kommendi maksimaalne pikkus + Peidab kommentaare, millel on sellest rohkem märke. Iga emoji loetakse ühe märgi eest + Piirangut pole diff --git a/patches/src/main/resources/addresources/values-eu-rES/music/strings.xml b/patches/src/main/resources/addresources/values-eu-rES/music/strings.xml index 3b5f8a8955..2b6c64a009 100644 --- a/patches/src/main/resources/addresources/values-eu-rES/music/strings.xml +++ b/patches/src/main/resources/addresources/values-eu-rES/music/strings.xml @@ -133,6 +133,7 @@ Second \"item\" text" "Ez da deskargaik Deskargatutako pisteak hemen agertuko dira" %1$s • %2$s • %3$s MB + %1$s pista • %2$s MB Bilatu deskargak Ordenatu Artista arabera ordenatu @@ -532,6 +533,7 @@ Cookie beharrezkoak: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPI Romanizazio eta Azeratze Lokalizatuak Romanizatu gailuaren barruan itzultzaileak, zure modeloa eta Google Translate fallatzen direnean. Irakurkizuna letra sinkronizatuaren hitz bakoitzaren ondoren dator; hitz sinkronizazioa aktibo egon behar da. Txinoa, Kantonesa, Japoniera, Koreera, Grekera, Errusiera, Georgiana, Armeniera; 120 MB inguru %1$s irakurtzen: %2$d%% + Irakurketa prest (%1$d entradak) Deskarga huts egin du - desaktibatu eta aktibatu berriro saiatzeko Aktualizatzeko botoia erakutsi Sakatu hurrengo letra kandidatora aldatzeko; sakatu luze abestuaren izenaren eta artistaren arabera letra bilatzeko @@ -543,8 +545,6 @@ Cookie beharrezkoak: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPI Gordea da Markatu sinkronizatutako hitzak edo silabak Lehenetsi hitz-by-hitz letak eskuragarriak direnean. Letak hitzen timinga dutenean, musikaurreko hitz edo silaba bakoitza nabementzen ditu - Erakutsi atzeko ahizpak separat Updateren - Parantezian idatzitako ahozko fondoak, \"(Oh) I want you\" bezalakoak, abestu egin den lerrotik atera eta lerro propioago eta diseinuzko batean jartzen ditu Ezikusi abestutako hitzak Ezikusi gaur egun dozen linearen gaindiko hitz-lerroak Ezagutu gabeko letra ezkutatu diff --git a/patches/src/main/resources/addresources/values-eu-rES/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-eu-rES/shared-youtube/strings.xml index 94b6645916..dbb14a267e 100644 --- a/patches/src/main/resources/addresources/values-eu-rES/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-eu-rES/shared-youtube/strings.xml @@ -21,6 +21,8 @@ Second \"item\" text" Hasi-animazioa Desaktibatuta Kolorea + Monokromoa + Berrifiziatu konfiguratzea amaitzeko @@ -36,11 +38,14 @@ Second \"item\" text" HoriDARK horia Urare iluna Gorri iluna + Atzeondo iluna pertsonalizatua + Konfigura gai ilunaren atzeondo kolorea Aplikazioaren lehenestekoa Material You (Neutroa) Material You (Primaria) Material You (Sekundaria) Material You (Terziaria) + Pertsonalizatua Deskargak Deskarga modu @@ -52,6 +57,7 @@ Second \"item\" text" Sartu paketearen izena Besteak Aplikazioa ez dago instalatuta + %s ez dago instalatuta. Mesedez, instalatu "Ezin izan da pakete-izen honek instalatutako aplikazioik aurkitu: %s Egiaztatu pakete-izena zuzena dela eta aplikazioa instalatuta dagoela" @@ -83,6 +89,7 @@ Ikonoa guzten guzten aldatzeko, patch-atu berriro eta aldatu 'Aplikazio-ikonoa' Morphe play Morphe Ohartupena + Zure ikusteko historia ez da gordetzen.<br><br>Hau DNS publizitate blokeatzaile batek edo sare proxy batek eragiki du probablementen.<br><br>Hau konpontzeko, gehitu <b>s.youtube.com</b> zscalerra edo desaktiba DNS blokeatzaile eta proxy guztiak Ez erakutsi berriz Bideo-fluxuak imitatu @@ -218,6 +225,8 @@ Hala ere, hau aktibatzeak erabiltzailearen datu batzuk ere erregistratuko ditu, Ikustzailearen diskrezio-dialogoa kendu Honek ez du adinkako gehikarria gainditu. Automatiki onartzen du soilik + PoToken hornitzailea + Konfigura PoToken hornitzailea erreprodukzio arazoak saihesteko PoToken hornitzaile kanpokoa "PoToken minter kanpoko bat erabiliz erreprodukzio arazoak konpondu egiten ditu (Experimentala) @@ -294,9 +303,12 @@ Ikusten ari zaren bideoaren IDa Return YouTube Dislike API-ra bidaltzen da, eta Ezonukak batzuetan eskuragarri ez dira - Bezeroaren API tasa muga aplikatuta dago API botoen eskaerak, deiekin kopurua Ez da sare-deiik egin + %s sare-deia egin dira API botoen eskaerak, timeout-en kopurua Ezik sare-deiik timeout-etan amaitu + %s sare-deien denbora iraiki da API bezeroaren tasa mugaak Ez da bezeroaren tasa mugarik aurkitu Klienteren tasa-muga %s aldiz aurkitu da + %s milisegundo diff --git a/patches/src/main/resources/addresources/values-eu-rES/shared/strings.xml b/patches/src/main/resources/addresources/values-eu-rES/shared/strings.xml index 9143e44840..7252344ff8 100644 --- a/patches/src/main/resources/addresources/values-eu-rES/shared/strings.xml +++ b/patches/src/main/resources/addresources/values-eu-rES/shared/strings.xml @@ -71,6 +71,9 @@ Hizkuntza berriak itzuli edo itzulpen existenteak hobetzeko, bisitatu morphe.sof Fitxutxutik importatu ⚙️ Konfigurazioa arrakastaz inportatu da, 保存 sakatu aplikatzeko Konfigurazioak inportatzea huts egin du + Fitxera batera esportatu + ⚙️ Konfigurazioa arrakastaz esportatu da + Konfigurazioa esportatzea huts egin du Morphe Patches-en azken bertsioa erabiltzen ari zara: <i>%s</i> Morphe Patches-en <i>%1$s</i> bertsioa erabiltzen ari zara<br><br><b>Eguneratzea eskuragarri: <i>%2$s</i></b><br><br>Patch-ak eguneratzeko, erabili Morphe aplikazio honek berriz patchatzeko @@ -89,11 +92,13 @@ Hizkuntza berriak itzuli edo itzulpen existenteak hobetzeko, bisitatu morphe.sof 2022 ‑ 2025 2022 ‑ 2025 2019 ‑ 2022 + Lizentzia-ohartupenak Kargatzen… Debugatzea Konfiguratu garapide aurreratuko aukerak eta diagnostiko-logak Aktibu debug logak + Debug log-en stack trace-a Sartzen ditu stack trace-ak debug logetan erroreak identifikatzen laguntzeko Erakutsi jakinarazupen laburra Morphe errorean "Erroen toastak txukituak egiteak Morphe-ren erroen jakinarazpen guztiak ezkutatzen ditu. @@ -135,6 +140,7 @@ Ez zaizkizu ezusteko gertakari batzeik jakinaraziko" Ezaugarrien flagak pantailako kopiatzailean kopiatuta Berrifiziatu aplikazioa konfigurazio hau erabiltzeko Esportatu eta importatu + %s ezaugarri-bandera inportatu dira Ez da ezaugarri-banderaik aurkitu Ezaugarri-banderak ar വിജയtsunez esportatuak dira Ezaugarri-banderak esportatzea huts egin du @@ -178,6 +184,8 @@ Jokamodua bilaketan aurrago desagertu zen, beraz, flag bat baino gehiago egon da "Zure MicroG v%1s bertsioa zaharkituta dago eta arazoak sor ditzake. Eguneratu MicroG v%2s azken bertsiora" + Eguneratu + Ezaintzu "Instaluratutako MicroG bertsioa ez da Morphe-k gomendatzen duena: %1s @@ -198,6 +206,12 @@ MicroG-ren bateria optimizazioak desaktibatzeak ez du eragina negatiboa izango b Sakatu jarraitu botoia eta onartu optimizazio aldaketak" Jarraitu Erakutsi MicroG bateria optimizazioaren elkarrizuna + "Hau desaktuatzeak MicroG-ren bateria optimizazio-ohartupena ezkutuko du. + +Bateria optimizazioak aldatzen ez badira, honako hau gertatu daiteke: +• App-a ausazki ixteko aukera dago +• Jakinarazpenek funtzionatu behaz ez dute +• Atzealdeko erreprodukzioak funtzionatu behaz ez du" Aplikazio-estek들의 kudeaketa Konfiguratu estekak aplikazio honetan irekitzeko @@ -205,6 +219,7 @@ Sakatu jarraitu botoia eta onartu optimizazio aldaketak" Ireki aplikazio originalaren konfigurazioa eta desaktibu domeinu-esteka guztiak 2. urratsa: Aktibu patched aplikazioaren estekak Ireki patched aplikazioaren konfigurazioa eta aktibu domeinu-esteka guztiak + Ireki konfigurazioa 🚨 Ohartupen Esperimentala 🚨 "⚠️ <b>%1$s</b> aplikazioaren bertsio esperimental bat erabiltzen ari zara diff --git a/patches/src/main/resources/addresources/values-eu-rES/youtube/strings.xml b/patches/src/main/resources/addresources/values-eu-rES/youtube/strings.xml index e63cc902de..7939414eab 100644 --- a/patches/src/main/resources/addresources/values-eu-rES/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-eu-rES/youtube/strings.xml @@ -75,6 +75,7 @@ Liburutegi fitxa erakusten da Denak ezkutatu Ezkutatu mikrofono botoi flotantea Bilaketa-iradokizunen pantailaren beheko eskuineko mikrofono botoi flotantea ezkutatzen du + Ezkutatu \'Lortu Premium\' botoia Ezkutatu Historia estantea Ezkutatu estante horizontalak Estante horizontalak ezkutatzen ditu, adibidez: Azken berriak, Gustuko izan neckline dirudien komunitate-postak, Jarraitu ikusten, Arakatu kanal gehiago, Arakatu gai gehiago, Zuretzat, Azken bideoak, Relevantenak, Sortzailearen komunitateko postak, Shopping eta Berriro ikusi @@ -113,6 +114,9 @@ Liburutegi fitxa erakusten da Ezkutatu inkestaak Ezkutu inkestak eta feed-eko gomendapenen zehaztasunari buruzko ohartupenak Ezkutatu txitiketen estantea + + Ezkutatu \"Bideo eskuragarriak ez direnak ezkututa daude\" bannerra + Ezkutatzen du \"Bideo eskuragarriak ez direnak ezkututa daude\" bannerra playlistetan Ezkutatu igotze-denbora "Mugaak: @@ -265,8 +269,10 @@ Liburutegi fitxa erakusten da \"Kideen komentarioak\" nagusiak ezkutatu Ezkutatu iruzkinen atala Ezkutatu iruzkinen atala Home feed-ean + Ezagutu Komunitatearen Arauak Ezkutatu \'Short bat sortu\' botoia Ezukitu Ez-gustatzen botoia + Ezagutu Emoji botoia Ezkutatu iragazki-barrako aukerak \'Goikoak\' eta \'Berrienak\' ezukez iragazki-barrako gainerako aukerak guztiak ezkutatzen ditu Ezkutatu opari-animazioak eta txartelak @@ -284,6 +290,7 @@ Liburutegi fitxa erakusten da Gaitu iruzkin minimalistikoen botoia Ezkutatzen ditu iruzkin-botoian agertzen diren tx thẻ iraleak eta haien orrialde-hautagailua Ezkutatu Eskerrak botoia + EzkutatuTimeStamp botoia Ezkutatu \'Fan top\' botoia Ezkutatu Itzuli botoia Ezkututu \"Irten ez da commentsik\" mezua @@ -384,6 +391,7 @@ Txostenak publikoak dira eta kanal bat zerrendara gehitu aurretik berrikusi egit Ezkutatu amaiera-pantailako denda-bannerra Ezkutatu iragarki orokorrak Ezkutatu merchandise-bannerrak + Ezkutatu promozio ordainduen etiketak Ezagutu seinaleak ezkutatu Ezkutatu auto-patrozinatutako txartelak Ezkutatu inkaupazio-estekak @@ -444,6 +452,7 @@ Txostenak publikoak dira eta kanal bat zerrendara gehitu aurretik berrikusi egit Deskargatu irudia %s irudi deskargatzen ari da… Irudia deskargatzen ari da… + %1$s irudi %2$sra gorde dira Irudia %snon gorde da Ezin izan da irudia deskargatu Ez da irurik aurkitu post honetan @@ -601,6 +610,7 @@ Informazioa: Agian ez funtzionatzen duen zuzeneko emanbisteetan" Bideoa arrakastetsunez gordetako \"gero ikusi\" zerrendan Bideo hau dagoすでに \"gero ikusi\" zerrendan gorde Bideo hau ezin da \"gero ikusi\" zerrendan gorde + Gorde geroago ikusteko Erakutsi \'Gero ikusteko gorde\' botoia seiatzailearen konfigurazio-menuan Gaur egungo bideoa \'Gero ikusteko\' gorde sailkatzailearen konfigurazio-menuatik Erakutsi \"Gero ikusi\"n gorde botoia haurren bideoetan @@ -670,6 +680,15 @@ Informazioa: Agian ez funtzionatzen duen zuzeneko emanbisteetan" Playlista gorde da %s-n gorde da Hutsi herrenidea + Hutsatu da herrenkada + Herrenkada + Jou aforementioned herrenkadako azkena + Jou aforementioned herrenkadako hurrengoa + YouTube-ek ezin du herrenkada erakutsi. + Herrenkada ez dago prest bideo honentzat. Ireki bideo reproduktorea eta saiatu berriro. + Mixa gehitu herrenkadara ala Mixa ikusi? + Ikusi + Gehitu herrenkadara Mixa kargatzen ari da… Ireki kanala zuzeneko avatarretik @@ -749,6 +768,11 @@ Hona konponduz, aktibu 'Bideo-emisioak imitatzea' eta alda 'Beherako bezeroa' 'T Abiadura-eremunaren altura Abiadura-kezkatu-eremunaren altura, playback-er reproduciblearen altueren ehuneko gisa (5-75) Abiadura + Irraspen overlay-aren estiloa + Overlay horizontala + Overlay horizontala (minimalista - goian) + Overlay horizontala (minimalista - erdigunean) + Overlay zirkulartza Overlay zirkularra (minimalista) Overlay bertikalak Overlay bertikalak (minimalista) @@ -943,6 +967,7 @@ Konfiguraziora sartzeko, aktibu Konfigurazio botoia Oroharra → Barrako tresnak Ezkutatu Cast botoia Ezkutatu Itxi botoia Ezkutatu Pantaila osoa botoia + Ezagutuatzea playback-kontrolen fondoa Bideo-atarijainaren kontrolak inguratzen dituen itzalilaguna ezkutatzen du Kontrol botoien fondoaren opazitatea Doituatuzatzeen kontrol botoien eta denbora-barrari atzean dagoen fondoaren opazitatea (0 ikusgarri gabe, 100 solido) @@ -1047,6 +1072,8 @@ Konfigurazioa → Erregengatzea (edo Autoplay) → Hurrengo bideoa automatikoki Kutsu bakarrik Zubildun eta landscape + Force landscape pantaila osoan + Biratu playera landscape modura pantaila osoan sartzean. Ez dago eskuragarri pantaila txikietan, pantaila osoa landscape moduan irekitzen dutelako Pantaila osoko keze-gestuak Atzalkada-keiztuak pantaila osoko moduan @@ -1147,6 +1174,7 @@ Limitazioa: 'Bideo ekintza barra zaharra berreskuratu' tắt dago bada, 'Bideo g Joerak Errealitate Birtuala Gero ikusi + Zure klipak Desaktibu hasieran berreskuratzea @@ -1224,6 +1252,10 @@ Limitazioa: 'Bideo ekintza barra zaharra berreskuratu' tắt dago bada, 'Bideo g Status-barrak goiko barrak estaltzen dituen arazoa konpondu Status-barrak layout koherentea mantentzen du, goiko barrak estaltu ez ditzan eta playera gaindik hutsik ez utzi. Desaktibu aukera honek aplikazioa crashes-era eramaten badu edo goiko barrak gaizki agertzen badira + Distira + Morphe Glow 1 + Morphe Glow 2 + Gailu honek ez du distira duten splash screen-ak onartzen Tema argiaren atzeondoa Zuria Material You (Zuria) @@ -1246,6 +1278,9 @@ Limitazioa: 'Bideo ekintza barra zaharra berreskuratu' tắt dago bada, 'Bideo g Katu bideoaren progresbarrako kolore sekundarioa Seekbar-ren kolore-balioa ez da baliagarria + Analogikoa jatorrizkoa + Kake jatorrizkoa + Metala jatorrizkoa Header-ren logotipoa Lehenetsua @@ -1458,6 +1493,9 @@ Muga: 'Bideo gehiago' overlay zaharkundean itzultzen da" Atarijoko ikonoen estiloa Automatikoa + Lehenagikoa + Leun + Lodi Kustomizatua Shorts ikonoen estiloa @@ -1535,7 +1573,7 @@ Grabaketa automatikoa gelditu egiten da inkognito moduan, kontuen identitatea es Episodioak ikusita markatu dira Utzi Gaitu - {count, plural, one {1 episodio berri} other {# episodio berri}} + {%1$s} episodico berri Grabatu progresua gailu honetan "Zure sailak eta progresua gaildu honetan geratzen dira, YouTube kontu guztien artean partekatuz. YouTube historiako kontrolak ez dituzte aldatzen. Series menutik garbitu progresua. diff --git a/patches/src/main/resources/addresources/values-fa-rIR/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-fa-rIR/shared-youtube/strings.xml index fcf33fc67d..a20de9a779 100644 --- a/patches/src/main/resources/addresources/values-fa-rIR/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-fa-rIR/shared-youtube/strings.xml @@ -21,9 +21,9 @@ Second \"item\" text" انیمیشن راه‌اندازی غیرفعال رنگی - Morphe رنگی تک‌رنگ - Morphe تک‌رنگ + + خاموش برای پایان راه‌اندازی، مجدداً راه‌اندازی کنید diff --git a/patches/src/main/resources/addresources/values-fa-rIR/youtube/strings.xml b/patches/src/main/resources/addresources/values-fa-rIR/youtube/strings.xml index 8f7741db8a..075a1277dd 100644 --- a/patches/src/main/resources/addresources/values-fa-rIR/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-fa-rIR/youtube/strings.xml @@ -113,6 +113,9 @@ Second \"item\" text" پنهان‌کردن نظرسنجی‌ها نظرسنجی‌ها و پیشنهادهای مربوط به دقت پیشنهادهای خوراک را پنهان می‌کند پنهان‌کردن قفسهٔ بلیت + + مخفی کردن بنر \"ویدیوهای در دسترس نیستند مخفی شده‌اند\" + بنر \"ویدیوهای در دسترس نیستند مخفی شده‌اند\" را در لیست‌های پخش مخفی می‌کند پنهان‌کردن زمان بارگذاری "محدودیت‌ها: @@ -1248,6 +1251,7 @@ Second \"item\" text" چیدمان نوار وضعیت را یکپارچه نگه می‌دارد، بنابراین بر نوارهای بالایی پوشانده نمی‌شود و فضای خالی در بالای پخش‌کننده باقی نمی‌گذارد. اگر برنامه کرش کرد یا نوارهای بالایی به درستی نمایش داده نشدند، این گزینه را غیرفعال کنید درخشش + صفحه شروع Glow توسط این دستگاه پشتیبانی نمی‌شود پس‌زمینهٔ طرح روشن سفید Material You (سفید) diff --git a/patches/src/main/resources/addresources/values-fi-rFI/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-fi-rFI/shared-youtube/strings.xml index 649ff1a5f0..8ca950b362 100644 --- a/patches/src/main/resources/addresources/values-fi-rFI/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-fi-rFI/shared-youtube/strings.xml @@ -21,9 +21,8 @@ Second \"item\" text" Käynnistysanimaatio Pois käytöstä Väri - Värillinen Morphe Monokromi - Monokromi Morphe + Käynnistä uudelleen valituksen suorittamiseksi diff --git a/patches/src/main/resources/addresources/values-fi-rFI/youtube/strings.xml b/patches/src/main/resources/addresources/values-fi-rFI/youtube/strings.xml index 4605c585f0..3f81f88e63 100644 --- a/patches/src/main/resources/addresources/values-fi-rFI/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-fi-rFI/youtube/strings.xml @@ -113,6 +113,9 @@ Muutoksiin kuuluvat: Piilota kyselyt Piilottaa kyselyt ja ehdotukset syötteen suositustarkkuudesta Piilota lippuhylly + + Piilota \"Saatavilla olevat videot on piilotettu\" -palkki + Piilottaa soittolistojen \"Saatavilla olevat videot on piilotettu\" -palkin Piilota julkaisuaika "Rajoitukset: @@ -1249,6 +1252,9 @@ Rajoitus: Jos \"Palauta vanha videotoimintopalkki\" on poissa käytöstä, \"Lis Pysäyttää tilapalkin asettelun johdonmukaisuuden, joten se ei peitä yläpalkkeja ja jättää tyhjää tilaa soittimen yläpuolelle. Poista tämä käytöstä, jos sovellus kaatuu tai yläpalkit näyttävät vääriltä Hehku + Morphe Glow 1 + Morphe Glow 2 + Tämä laite ei tue Glow-aloitusnäyttöä Vaalean teeman tausta Valkoinen Material You (Valkoinen)() diff --git a/patches/src/main/resources/addresources/values-fil-rPH/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-fil-rPH/shared-youtube/strings.xml index f123208d8c..914312aeca 100644 --- a/patches/src/main/resources/addresources/values-fil-rPH/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-fil-rPH/shared-youtube/strings.xml @@ -21,9 +21,9 @@ Second \"item\" text" Animasyon sa pagsisimula Nakapatay Kulay - Color Morphe Monokromo - Monochrome Morphe + + Naka-off I-restart upang tapusin ang pag-setup diff --git a/patches/src/main/resources/addresources/values-fil-rPH/youtube/strings.xml b/patches/src/main/resources/addresources/values-fil-rPH/youtube/strings.xml index 7293c1bc3c..3070aeea80 100644 --- a/patches/src/main/resources/addresources/values-fil-rPH/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-fil-rPH/youtube/strings.xml @@ -113,6 +113,9 @@ Kasama sa mga pagbabago ang: Itago ang mga pagsusuri Itinatago ang mga survey at mga pahiwatig tungkol sa katumpakan ng rekomendasyon ng feed Itago ang ticket shelf + + Itago ang banner na \"Nakatagong mga video na hindi available\" + Itinatago ang banner na \"Nakatagong mga video na hindi available\" sa mga playlist Itago ang oras ng pag-upload "Mga Limitasyon: @@ -1247,6 +1250,7 @@ Limitasyon: Kung ang 'Ibalik ang lumang video action bar' ay naka-off, maaaring Pinapanatiling consistent ang layout ng status bar upang hindi ito sumakop sa mga itaas na bar at walang walang laman na espasyo sa itaas ng player. I-off ito kung crash ang app o magmukha nang mali ang mga itaas na bar Kislap + Hindi suportado ng device na ito ang glow splash screen Likuran ng maliwanag na tema Puti Material You (Puti) diff --git a/patches/src/main/resources/addresources/values-fr-rFR/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-fr-rFR/shared-youtube/strings.xml index 8af88523e4..5bc729a229 100644 --- a/patches/src/main/resources/addresources/values-fr-rFR/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-fr-rFR/shared-youtube/strings.xml @@ -21,9 +21,11 @@ Second \"item\" text" Animation de démarrage Désactivé Coloré - Couleur de Morphe + Morphe Coloré Monochrome - Morphe monochrome + Morphe Monochrome + + Désactivé Redémarrez pour terminer la configuration diff --git a/patches/src/main/resources/addresources/values-fr-rFR/sponsorblock/strings.xml b/patches/src/main/resources/addresources/values-fr-rFR/sponsorblock/strings.xml index 0a4b3e81ea..8ecd944d8c 100644 --- a/patches/src/main/resources/addresources/values-fr-rFR/sponsorblock/strings.xml +++ b/patches/src/main/resources/addresources/values-fr-rFR/sponsorblock/strings.xml @@ -58,6 +58,8 @@ Second \"item\" text" Déjà lues Montrez-les-moi Général + Enregistrer des segments des vidéos téléchargées + Permet de passer des segments des vidéos téléchargées hors ligne Afficher les alertes d\'erreur de connexion Affiche une notification lorsque le serveur SponsorBlock est indisponible Activer le compteur de segments passés diff --git a/patches/src/main/resources/addresources/values-fr-rFR/youtube/strings.xml b/patches/src/main/resources/addresources/values-fr-rFR/youtube/strings.xml index 13aadc07f1..d0a4dc45a8 100644 --- a/patches/src/main/resources/addresources/values-fr-rFR/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-fr-rFR/youtube/strings.xml @@ -113,6 +113,9 @@ Les changements incluent : Masquer les sondages Masque les sondages et les incitations concernant la pertinence des recommandations du flux Masquer l\'étagère des \'Places de concert\' + + Masquer la bannière \'Les vidéos non disponibles ont été masquées\' + Masquer la bannière \'Les vidéos non disponibles ont été masquées\' dans les playlists Masquer l\'heure de mise en ligne "Limitations : @@ -1209,6 +1212,8 @@ Limitation : Si 'Restaurer l'ancienne barre d'actions vidéo' est désactivé, l Désactive le mini-lecteur \'Continuer à regarder\', une fonctionnalité de YouTube Premium qui s\'affiche au démarrage de l\'application Mini-lecteur Personnalisez l\'apparence et le comportement du mini-lecteur vidéo + Mini-lecteurs supplémentaires + YouTube a supprimé tous les mini-lecteurs supplémentaires. Pour rétablir d\'autres types de mini-lecteurs, veuillez patcher YouTube version 21.28.208 ou antérieure Type de mini-lecteur Masquer le titre et la chaîne Désactivé @@ -1250,6 +1255,9 @@ Limitation : Si 'Restaurer l'ancienne barre d'actions vidéo' est désactivé, l Maintient une mise en page cohérente de la barre d\'état, afin qu\'elle ne masque pas les barres supérieures et ne laisse aucun espace vide au-dessus du lecteur. Désactivez cette option si l\'application plante ou si les barres supérieures s\'affichent incorrectement Lueur + Morphe Lueur 1 + Morphe Lueur 2 + L\'écran de démarrage lueur n\'est pas pris en charge par cet appareil Arrière-plan du thème clair Blanc Material You (Blanc) diff --git a/patches/src/main/resources/addresources/values-ga-rIE/music/strings.xml b/patches/src/main/resources/addresources/values-ga-rIE/music/strings.xml index 36e80b1d99..f864b5e562 100644 --- a/patches/src/main/resources/addresources/values-ga-rIE/music/strings.xml +++ b/patches/src/main/resources/addresources/values-ga-rIE/music/strings.xml @@ -553,29 +553,29 @@ Fianáin riachtanacha: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PA Taispeánann sé an líne reatha ar an scáileán glasála, ar Android Auto agus trí Bluetooth. Teastaíonn Android 11 nó níos nuaí Taispeáin liricí an mhionimreora Taispeánann sé an líne reatha sa mhionsheinnteoir, agus an rian bogtha go dtí an fotheideal - Format teideal amhráin/ealaíontóra + Formáid teideal an amhráin/an ealaíontóra Format an riana a thaispeántar leis na liricí sa Miniplayer agus sa MediaSession. Is é {t} an teideal, is é {a} an t-artist agus is é {v} an leagan, mar shampla Acoustic, nuair a sholáthraíonn an t-uairceann liricí é. Ní thaispeántar téacs idir beirt gcolóin ach amháin nuair a bhíonn luach ag gach hathraí istigh ann; scríobh \\: le haghaidh colóin shimplí. Taispeántar gach líne in aice lena chéile, ag bogadh air gach uair a athraíonn an liric - Format líne liricí - Format na líne liricí sa Miniplayer agus sa MediaSession. Is é {lrc} an líne agus is é {n} an líon uair a chanfar i ndiaidh a chéile. Ní thaispeántar téacs idir colóin ach amháin nuair a athchríonnann an líne. Úsáid \\: le haghaidh colóin - Caithfidh an format {lrc} a chomhሌir - Teampláin líne ealaíontóra agus timing animationa - Minimum ama scréine (ms) - Maximum ama scréine (ms) - Do animationa il-líne amháin. Coinnigh gach fréama arís an t-am íosaithe. Ag an íomhánú, sleamhnaigh líne an ealaíontóra fiú nuair a fhanann an liricneas an cineál céanna. - Caithfidh an íosaithe a bheith 0 nó níos mó. Caithfidh an íomhánú a bheith níos mó ná 0 agus ar a laghad mar an íosaithe. - Teorainneacha fad líne - Teorainneacha líne liricneasa agus ealaíontóra, agus meáchan caractair lánleathnasaigh - "Teorainneach líne liricneasa -Rogaigh liricneasa níos faide ina leathanaí cothrom. Díallaíonn 0 an rogha rothaireachta." - "Cuirteas líne an ealaíontóra -Scail formáid rian pobuailte ina fhreamaí rothúla. Dícheistíonn 0 an scailte." - "Meáchan carachtair lánleathas -Cuirtear leis an dá chuirteas. Ag 1.75, cunann ceithre carachtar lánleathas mar 7." - Cuir isteach cuirteas uimhireán ó 0 go 10000 agus meáchan carachtair níos mó ná 0 - Cuairt uirlise - Taispeánfar sa mhinipléire agus sa Session Meán le linn cuairte uirlise. Is freama animation amháin gach líne. Is é {t} an teideal, is é {a} an ealaíontóir agus is í {lrc} an líne lirice is déanaí - Oifset timing an mhinipléire agus na Session Meán - Aistriaíonn é an am nuair a thaispeántaíonn an chéad líne eile sa Miniplayer agus sa MediaSession. Úsáid luach negativity má thaispeántaíonn líneanna go mall, mar shampla ar an scáileán glasa + Formáid líne lirice + Formáid na líne liricí sa Miniplayer agus sa MediaSession. Seasann {lrc} don líne, agus seasann {n} don líon uaireanta as a chéile a chantar í. Ní thaispeántar an téacs idir an dá colon ach amháin nuair a dhéantar an líne a athrá. Úsáid \\: chun colon a chur isteach. + Caithfidh an fhormáid {lrc} a bheith inti + Teimpléad línte ealaíontóra agus uainiú beochana + Íos-am scáileáin (ms) + Uastréimhse scáileáin (ms) + I gcás beochan il-líne amháin. Coinnigh gach fráma ar feadh an íosfhad ama ar a laghad. Ar a mhéad, bog an líne ealaíontóra ar aghaidh fiú nuair a fhanann an liric mar an gcéanna. + Caithfidh an t-íosmhéid a bheith 0 nó níos mó. Caithfidh an t-uasmhéid a bheith níos mó ná 0 agus ar a laghad cothrom leis an íosmhéid. + Teorainneacha ar fhad líne + Teorainneacha línte do liricí agus d’ealaíontóirí, agus tiús carachtar ar leithead iomlán + "Teorainn le líon na línte liricí +Roinn liricí níos faide ina dhá leath chothromaithe. Má shocraítear ar 0 é, ní dhéanfar aon roinnt." + "Teorainn líne an ealaíontóra +Roinn an fhormáid riain líonta ina frámaí rothlacha. Má shocraítear é go 0, díchumasaítear an roinnt." + "Meáchan carachtar lánleithead +Baineann sé leis an dá theorainn. Ag 1.75, comhairtear ceithre charachtar lánleithead mar 7." + Cuir isteach teorainneacha slánuimhreach ó 0 go 10000 agus meáchan carachtair atá níos mó ná 0 + Sos uirlise + Ar taispeáint sa Mhini-imreoir agus sa MediaSession le linn sos uirlise. Is fráma beochana amháin é gach líne. Seasann {t} don teideal, {a} don ealaíontóir, agus {lrc} don líne dheireanach de na liricí. + Fritháireamh ama idir an mionimreoir agus MediaSession + Athraíonn sé an tráth a thaispeántar an chéad líne eile sa Mhini-imreoir agus sa MediaSession. Úsáid luach diúltach má thaispeántar na línte ró-dhéanach, mar shampla ar an scáileán glasála. Scagairí Regex meiteashonraí Cuirtear gnáthshlonn i bhfeidhm ar theidil, ar ealaíontóirí, agus ar albaim roimh liricí a lorg agus nuair a dhéantar scóráil chun a chinneadh an meaitseálann dhá theideal. Fág folamh é chun an dá cheann a dhíchumasú. Baineann an réamhshocrú amach an tranglam coitianta ar nós \"Official Video\", \"Remaster\", \"【插曲】\", agus \"- Topic\". @@ -583,10 +583,10 @@ Cuirtear leis an dá chuirteas. Ag 1.75, cunann ceithre carachtar lánleathas ma Slonn rialta a chuirtear i bhfeidhm ar théacs bunaidh na liricí amháin (ní ar aistriúcháin ná ar leaganacha rómánaithe). Fág folamh é chun é a dhíchumasú Marcóirí na líne eolais Má dhéantar na liricí a scagadh go mícheart go dtí an bun, déan é seo a choigeartú nó a ghlanadh. Marcóirí scartha le camóga: taispeántar línte ina bhfuil marcóir agus ansin idirponc mar eolas creidmheasa ag bun an leathanaigh.. Fág folamh é chun an ghné seo a dhíchumasú - Cuir an chéad litir i bhmór - Cuireann sé an chéad litir de gach líne liric i bhmór - Bain puinctailleas ó thaobh thiar - Baineann sé phuinctálacha, commas, colóna agus semicolóna de scríobh ag críoch líneacha liric. Coimeádfar comhrácaí, comharthaí mianach, ellipsis, comhrais agus breisleanna + Cuir an chéad litir i gceannlitir + Cuireann sé an chéad litir de gach líne den amhrán i gceannlitir + Bain an poncaíocht ag an deireadh + Baineann sé pointí, camóga, colonanna agus leathcholonanna ag deireadh línte liricí. Coinnítear comharthaí ceiste, comharthaí exclamation, trí phonc, comharthaí athfhriotail agus lúibíní. Méid an téacs Méid téacs na liricí Aistriú ama domhanda diff --git a/patches/src/main/resources/addresources/values-ga-rIE/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-ga-rIE/shared-youtube/strings.xml index 584a49168d..2f1e6806fa 100644 --- a/patches/src/main/resources/addresources/values-ga-rIE/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ga-rIE/shared-youtube/strings.xml @@ -24,6 +24,8 @@ Second \"item\" text" Dath Morphe Monacrómach Morphe Monacrómach + + As Atosaigh chun an socrú a chríochnú diff --git a/patches/src/main/resources/addresources/values-ga-rIE/youtube/strings.xml b/patches/src/main/resources/addresources/values-ga-rIE/youtube/strings.xml index d2bfdbb5a8..4e3c5b5ddd 100644 --- a/patches/src/main/resources/addresources/values-ga-rIE/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ga-rIE/youtube/strings.xml @@ -113,6 +113,9 @@ I measc na n-athruithe tá: Folaigh suirbhéanna Folaigh suirbhéanna agus brúidí faoi chruinneas moltaí beatha Folaigh an seilf ticéad + + Folaigh an meirg \'Tá físeáin nach bhfuil ar fáil i bhfolach\' + Folaíonn sé an meirgí \'Tá físeáin nach bhfuil ar fáil i bhfolach\' i seinmliostaí Folaigh am uaslódála "Teorainneacha: @@ -500,8 +503,8 @@ Má oscailtí físeán a bhfuil cnaipe Iarrtas aige uair amháin, beidh an ghné Físeán Fuaim Gach rogha - Taispeáin íoslódáil sa chláir Shorts - Cuir earráid íoslódála seachtracha isteach i bpróiseas Shorts + Taispeáin an íoslódáil i roghchlár na Shorts + Cuireann sé mír roghchláir aníos le haghaidh íoslódálaí seachtrach leis an seinnteoir Shorts Íoslódáil Aistriúchán gutha @@ -613,7 +616,7 @@ Eolas: Seans nach n-oibreoidh sé ar shruthanna beo" Ceadaítear físeáin a bhaineann le páistí a shábháil sa liosta \'Féach níos déanaí\' ón roghchlár aníos. Taispeáin an cnaipe \'Sábháil le breathnú air níos déanaí\' sa phainéal aníos do na físeáin ghearra. Ceadaíonn sé físeáin ghearra a shábháil sa liosta \'Féach níos déanaí\' ón roghchlár aníos. - Athdhionaigh \"Féach níos déanaí\" chuig liosta pleanála + Atreoraigh \'Féach níos déanaí\' chuig seinmliosta Féach níos déanaí Cuir leis an scuaine Morphe Coinnigh scuaine idir seisiúin @@ -778,8 +781,8 @@ Chun é seo a shocrú, cuir 'Spoof video streams' ar siúl agus athraigh an 'Cli Cnaipí gníomhaíochta Roghnaigh cé na cnaipí gníomhaíochta a thaispeántar i mód portráide agus lánscáileáin Cnaipí portráide - Díchumasaigh lonradh na cnaipí Is maith liom / Liostáil - Coscann sé seo na cnaipí Is maith liom agus Liostáil ó bheith ag lonrú nuair a luann cruthaitheoir iad + Díchumasaigh breo na cnaipí Is maith liom / Liostáil + Cuireann sé cosc ​​ar na cnaipí \'Is maith liom\' agus \'Liostáil\' a bheith ag breo nuair a luann cruthaitheoir iad Folaigh barra gníomhaíochta Folaigh an cnaipe Is maith liom agus Ní maith liom @@ -1115,14 +1118,15 @@ Leagan amach táibléid • Tá poist phobail i bhfolach • Níl an socrú athsheinm sna fothaí ar fáil • Níl an cnaipe athmheasctha ná an cnaipe Fuaime ar fáil i Shorts -• B’fhéidir nach mbeidh an forleagan ‘Tuilleadh físeáin’ ar fáil +• Seans nach mbeidh an forleagan 'Tuilleadh físeáin' ar fáil • Taispeántar barra gníomhaíochta seanfhíseáin neamh-fhillte +• Seans nach n-oibreoidh míreanna roghchláir eitilte i gceart -Leagan amach gluaisteán +Leagan amach feithicleach • Eagraítear an fotha de réir topaicí agus bealaí • Níl an socrú athsheinm sna fothaí ar fáil • Níl an cnaipe athmheasctha ná an cnaipe Fuaime ar fáil i Shorts -• B’fhéidir nach mbeidh an forleagan ‘Tuilleadh físeáin’ ar fáil +• Seans nach mbeidh an forleagan 'Tuilleadh físeáin' ar fáil • Oscailtear Shorts sa ghnáthimreoir • Taispeántar barra gníomhaíochta seanfhíseáin neamh-fhillte" Cumasaigh leagan amach táibléid sa seinnteoir @@ -1207,6 +1211,8 @@ Teorainn: Mura bhfuil 'Barra gníomhaíochta seanfhíseáin athchóirithe' múch Díchumasaíonn sé an miniplayer \'Lean ar aghaidh ag breathnú\', gné de chuid YouTube Premium a thaispeántar nuair a thosaítear an aip Mion seinnteoir Saincheap cuma agus iompar an seinnteora físe íoslaghdaithe + Mionimreoirí breise + Bhain YouTube gach mionimreoir breise amach. Chun cineálacha breise mionimreoirí a thabhairt ar ais, déan paisteáil ar YouTube 21.28.208 nó leagan níos sine. Cineál Mion seinnteoir Folaigh an teideal agus an cainéal Díchumasaithe @@ -1248,6 +1254,9 @@ Teorainn: Mura bhfuil 'Barra gníomhaíochta seanfhíseáin athchóirithe' múch Coinníonn sé leagan amach an bharra stádais comhsheasmhach, ionas nach gclúdaíonn sé na barraí uachtaracha agus nach bhfágann sé spás folamh os cionn an imreora. Múch an rogha seo má thuairteálann an aip nó má bhíonn cuma mhícheart ar na barraí uachtaracha. Breo + Morphe Breo 1 + Morphe Breo 2 + Ní thacaíonn an gléas seo le scáileán splancscáileáin Breo Cúlra téama éadrom Bán Material You (Bán) @@ -1270,9 +1279,9 @@ Teorainn: Mura bhfuil 'Barra gníomhaíochta seanfhíseáin athchóirithe' múch Socraigh an dath tánaisteach den bharra dul chun cinn físe Luach datha barra cuardaigh neamhbhailí - Anallóg Bunaidh + Analógach bunaidh Cáca bunaidh - Miatal bunaidh + Miotal bunaidh Lógó ceanntásca Réamhshocrú @@ -1638,15 +1647,15 @@ Tá taifeadadh uathoibríoch roghnach. Stopann sé i mód incognito nó nuair a Eipeasóid nach bhfuil ar fáil Tuairimí Niconico - Taispeáin comhrá beo agus athsheallú comhrá mar thuairimí a scrolálann trasna an fhíseáin - Taispeánann sé cnaipe forleagan san imreoir físeáin. Tapáil chun na tuairimí a thaispeánadh nó a fholamh, tapáil agus coinnigh le haghaidh socruithe tapa - Méid font tuairimí - Méid téacs na tuairimí (12-40 sp) - Trédemsse tuairimí - Tránspaireacht téacs na NósCórasa agus na gcumhachraí - Fad taistealaigh - Cé mhéad seconda a thógann NósCórasa an scréin a thrasnú. Bogann luachanna íseacha níos tapúla - Fad iomlán anúllaithe - Cheanglaíonn sé údarás. Gach emoji á ríomh mar shuíomh aonair + Taispeáin comhrá beo agus athsheinm comhrá mar thuairimí a scrollaíonn trasna an fhíseáin + Taispeánann sé cnaipe forleagtha sa seinnteoir físeáin. Beartaigh chun na tuairimí a thaispeáint nó a cheilt; beartaigh agus coinnigh chun rochtain a fháil ar shocruithe tapa + Méid cló an tráchtaireachta + Méid téacs na dtuairimí (12-40 sp) + Teimhneacht an tráchtaireachta + Teimhneacht théacs na dtráchtanna agus na gcúlraí + Fad an turais + Cé mhéad soicind a thógann sé ar thráchtanna an scáileán a thrasnú. Bogann luachanna níos ísle níos tapúla. + Uasfhad tráchtaireachta + Folaíonn sé tuairimí a bhfuil níos mó carachtar iontu ná an méid seo. Áirítear gach emoji mar charachtar amháin Gan teorainn diff --git a/patches/src/main/resources/addresources/values-gl-rES/music/strings.xml b/patches/src/main/resources/addresources/values-gl-rES/music/strings.xml index 4b87168f19..2c39c37e61 100644 --- a/patches/src/main/resources/addresources/values-gl-rES/music/strings.xml +++ b/patches/src/main/resources/addresources/values-gl-rES/music/strings.xml @@ -17,23 +17,157 @@ Second \"item\" text" --> + Reproducido por última vez Subscripcións + Vídeos + Outros resultados + Cancións subidas + Álbuns subidos + Artistas subidos + Actualiza o parche de Jam do anfitrión para usar pausa e reanudación + Compartir a cola de Jam + Serea anfitrión ou únete a un Jam próximo e edita a súa cola a través de Jam Layer + Activar a compartición da cola de Jam + Habilita a integración de Jam despois de que YouTube Music reinicie + Descarga Jam Layer para que funcione + Abre a última versión de Jam Layer no teu navegador + Controis de Jam + Serea anfitrión ou únete a un Jam próximo e xestiona a sesión + Paquete de Jam Layer + Elixe a aplicación compañe instalada que se usa para a condivisión de colas cercanas + Use un paquete compañeiro instalado. O emparellamento require a súa aprobación e un token de capacidade local + Activar o Wi-Fi? + Activa o Wi-Fi e, logo, tenta iniciar ou unirte á Jam de novo + Abrir a configuración de Wi-Fi Cancelar + Usar paquete + Introduza un nome de paquete de Android válido + Paquete gardado. Configura Jam Layer para conectar + Jam desconectado + Activa primeiro a Capa Jam + Activa a condivisión da cola de Jam na configuración e, a continuación, reinicia YouTube Music + Instala primeiro o paquete da Capa Jam seleccionado + Introduce o código que aparece no anfitrión. Wi-Fi Aware pode conectar dispositivos próximos aínda que as súas LANs sexan diferentes + Unirse cun código + Unirse + Escanear QR + Introduce o código de oito caracteres ou pega unha invitación + Únete á túa Jam + Copiar código + Código da Jam + Amosar QR + Feito + Convida á túa Jam + Escanea para unirte e editar esta cola. Finalizar a Jam revoga a invitación + Copiar ligazón + Convite para a Jam + A Jam se está reconectando; tente novamente en breve + Engadido á Jam + A Jam non está dispoñible; a pista non foi engadida + A túa Jam + Escoitando xuntos + Uníndose á Jam + Escoitar xuntos + Actualizando Jam… + Buscando o teu anfitrión en Wi-Fi… + Agardando a que chegue xente + %s persoa conectada + %s persoas conectadas + Introduce este código nun dispositivo próximo. Wi-Fi Aware funciona a través de LANs separadas cando está soportado. Expira en %s minutos + Ir ata %s? + Edicións abertas + Edicións bloqueadas + Conectado via Wi-Fi Aware + Conectado via Wi-Fi + Conectado via Bluetooth + Non soportado durante a Jam + Unión cancelada + Reconectando co seu anfitrión… + Compartir unha cola. Deixar que todos engadan cancións + Invitar xente + Os convidados poden editar a cola + Finalizar Jam + O anfitrión reproduce a música. Engade, move ou elimina cancións directamente na cola de YouTube Music + Mantén ambos dispositivos próximos. Podes cancelar mentres buscamos ao anfitrión + Cancelar unión + Saír da Jam + Iniciar unha Jam + Configurar Capa de Jam + Xestionar Detalles Abrir + Cargando opcións da canción… + Opcións da canción non dispoñibles; tente de novo + Non foi posible abrir as opcións da canción + Non hai ningún navegador dispoñible para descargar Jam Layer + Agardando a cola do anfitrión + Abra primeiro unha canción en YouTube Music + Reproducir no anfitrión + Saír de Jam e reproducir localmente + Reproducir a seguinte en Jam + Engadir á cola de Jam + Reproducir esta pista de Jam + Elixe onde reproducir + Mover o anfitrión aquí + A canción non está lista para buscar + A cola cambiou; intenta o xesto de novo Logotipo da cabeceira Predefinido Personalizado + Reproducir pistas de audio do álbum + Reproducir a versión sóo de audio do álbum das cancións en lugar dun vídeo musical. Require \'Simular fluxos de vídeo\' + Activar a concreción de audio/vídeo + Tenta cambiar entre a versión da pista de audio e o vídeo musical oficial + Descargar vídeos musicais, podcasts ou cancións con aplicacións de descarga de terceiros + Usar descarga externa para o menú de Descargas + A opción de Descarga dos menus abre o seu descarga externa en seu lugar + Descargas + "Non hai descargas +As pistas descargadas aparecen aquí" + %1$s • %2$s • %3$s MB + %1$s pistas • %2$s MB + Buscar nas descargas + Ordenar + Ordenar por artista + Ordenar por título + Ordenar por tamaño + Eliminar todas as descargas + Eliminar todas as descargas? + Todas as pistas descargadas serán eliminadas deste dispositivo + Descargas eliminadas + Non se atopou nada + Accións para %s + Reproducir + Reproducir ou pausar + Anterior + Seguinte + Pausa Pechar Eliminar + Eliminar descarga + Eliminar descarga? + %s será eliminado deste dispositivo + Descarga eliminada + Non foi posible borrar a descarga + Non foi posible reproducir a descarga + Descargas de audio + Reprodución sen conexión + Non se puido identificar a pista para descargar + A descarga xa está en curso + Xa descargado + Descarga iniciada + Iniciando descarga… + Descargando %s%% + Descarga completada + Erro na descarga Acerca de @@ -41,31 +175,182 @@ Second \"item\" text" Fuente Xeral Reprodutor + Letras Miscelánea + Idioma de YouTube Music + Idioma do sistema + Require Android 13 ou posterior Ocultar etiqueta \'Obter Music Premium\' + Ocultar promocións de Music Premium Ocultar anuncios de vídeo + Ocultar conmutador de Audio / Vídeo + Ocultar o botón de Descargar + Oculta o botón de Descargar no programa do podcast + Agochar a balda de Explorar + Ocultar barra de filtros + Ocultar baldas de grella Ocultar baldas horizontais + Ocultar baldas de lista + Panel de letras + Ocultar botón de compartir + Ocultar botón de traducir + Ocultar a sección \"Novidades de\" + Ocultar as seccións de listas de reprodución + Ocultar o botón Repetir + Ocultar o botón Aleatorio + Ocultar a sección \"Marcación rápida\" + Comentarios + Elixe que compoñentes aparecen na sección de comentarios + Ocultar Directrices da comunidade + Ocultar mensaxe de contexto + Ocultar o botón de Emoji + Ocultar o botón de Info + Ocultar o botón da marca temporal + Crossfade + Mestura as pistas suavemente unha coa outra + Activar crossfade + Curva de desvanecemento + Potencia igual + Sostén sutil + Suavizado leve + Curva en S suave + Saída + Entrada + Duración do crossfade + 250 milisegundos + 500 milisegundos + 750 milisegundos + 1 segundo + 2 segundos + 3 segundos + 4 segundos + 5 segundos + 6 segundos + 7 segundos + 8 segundos + 9 segundos + 10 segundos + 11 segundos + 12 segundos + Crossfade ao saltar manualmente + Aplica o efecto de fundido cruzado cando saltas manualmente á seguinte pista + Fundido cruzado no avance automático + Aplica o efeito de fundido cruzado cando unha pista remata naturalmente + Control da sesión + Mantén premido o botón de aleatorio para pausar ou reanudar o crossfade + Fundido cruzado pausado para esta sesión + Fundido cruzado retomado + O modo de vídeo non está dispoñible mentres o fundido cruzado está activado + O crossfade está desactivado coa reprodución SABR + O crossfade está desactivado durante a transmisión + Como funciona + "O crossfade solapa o final dunha canción co inicio da seguinte, usando a túa duración e curva + +Execútase no avance automático e no salto manual, cada un co seu interruptor. No avance automático, a mestura comeza uns segundos antes de que a canción remate" + Limitacións + "Os vídeos musicais reprodúcense só como audio mentres o crossfade está activado. Mantén premido o botón de aleatorio para pausar o crossfade e ver o vídeo + +Un vídeo musical que comeza a reproducirse permanece en modo vídeo ata a seguinte canción + +As cancións máis curtas que a duración do fade non teñen crossfade + +A pantalla de bloqueo ou a notificación poden amosar brevemente un estado obsoleto durante a transición + +Pausar o crossfade para ver un vídeo musical pode deixar algúns vídeos en negro. Pausar o crossfade antes de elixir a canción evita isto" + Non compatibles + "Transmisión ou espellado de audio (Chromecast, espellado de pantalla ou de audio) + +Reprodución SABR (simulación de fluxos de vídeo desactivada)" + Créditos + Función de crossfade por VazerOG + Lembrar estado de repetición + Lembrar estado de reprodución aleatoria + Velocidade de reprodución + Velocidade utilizada para todas as pistas. Ao 100% úsase a velocidade da aplicación, como a velocidade de reprodución do podcast + Cambiar o ton coa velocidade + Desactivar redireccionamento de non gustar + Evita saltar á seguinte pista cando se preme o botón de non gustar + Acción ao tocar unha pista da lista de reprodución + Reproducir a lista de reprodución + Reproducir só a pista seleccionada + Engadir a pista seleccionada á cola + Engadido á cola + Ocultar botón de emisión + Ocultar botón de historial + Ocultar botón de notificación + Ocultar botón de busca + Ocultar botón de busca por voz + Ocultar botón de busca por son + Ocultar botón Novo + Oculta o botón Novo na pestana Biblioteca + Botóns de acción + Elixe que botóns de acción aparecen baixo o reprodutor + Ocultar barra de acción + Ocultar botóns de Gústame e Non me gusta + Ocultar botón de Comentarios Agochar o botón «Reprodución do chat en directo» + Ocultar botón de Letras + Ocultar botón de Compartir + Ocultar botón Gardar + Ocultar botón Descargar + Agochar o botón «Iniciar radio» + Menú desplegable + Escolle que compoñentes aparecen nos menús desplegables do reproductor e da cola + Ocultar compoñente de 3 columnas + Ocultar botóns de Gústame e Non me gusta + Ocultar menú \'Afinidade de gusto\' + Agochar o menú \'Engadir á lista de reprodución Escoitar máis tarde\' + Agochar o menú \'Engadir á cola\' + Ocultar menú de subtítulos + Agochar o menú \'Eliminar lista de reprodución\' + Agochar o menú \'Descartar cola\' + Agochar o menú \'Non recomendar este artista\' + Ocultar menú de descarga + Agochar o menú \'Editar lista de reprodución\' + Agochar o menú \'Ir ao álbum\' + Ocultar menú \'Ir ao artista\' + Ocultar menú \'Ir ao episodio\' + Ocultar menú \'Ir ao podcast\' + Ocultar menú de Axuda + Ocultar menú \'Marcar episodio como reproducido\' + Ocultar menú \'Non me interesa\' + Ocultar menú \'Fixar na marcación rápida\' + Ocultar menú \'Reproducir a continuación\' + Ocultar menú de Calidade + Ocultar menú \'Eliminar da biblioteca\' + Ocultar o menú \"Eliminar da lista de reprodución\" + Ocultar menú de Informe + Ocultar o menú \"Gardar episodio para máis tarde\" / \"Gardar na biblioteca\" + Ocultar o menú \"Gardar na lista de reprodución\" + Ocultar menú de Compartir + Ocultar o menú \"Reproducir aleatoriamente\" + Ocultar o menú \"Temporizador de sono\" + Ocultar o menú \"Iniciar radio\" + Ocultar o menú \"Estatísticas para expertos\" + Ocultar menú de Subscribirse + Ocultar o menú \"Desfixar da marcación rápida\" + Ocultar menú \'Ver créditos da canción\' + Páxina de inicio Predefinido Gráficas Episodios para máis tarde @@ -77,34 +362,330 @@ Second \"item\" text" Busca Subscripcións + Forzar orientación vertical + Mantén a aplicación en orientación vertical cando se xira o dispositivo Cambiar cor do reprodutor minimizado + Axusta a cor do reprodutor minimizado á do reprodutor de pantalla completa + Cambiar a cor da barra de navegación + Axusta a cor da barra de navegación ao reprodutor a pantalla completa + Amosar o botón de seguinte no reprodutor minimizado + Amosar o botón de anterior no reprodutor minimizado Activar forzar reprodutor minimizado + Impede que o reprodutor se expanda automaticamente a pantalla completa cando comeza unha nova pista + Activar o deslizamento para pechar o miniplayer + Permite pechar o miniplayer deslizando o dedo cara abaixo sobre el Barra de navegación + Elixe que botóns aparecen na barra de navegación inferior + Ocultar botón Inicio + Ocultar botón de Mostras + Ocultar botón de Explorar + Ocultar botón de Biblioteca + Ocultar botón Premium Ocultar barra de navegación Ocultar etiquetas dos botóns de navegación + Non iniciou sesión. Toca para configurar + Cerrar sesión + Sesión cerrada + Activar scrobbling + Enviar estado de \'Reproducindo agora\' + Duración mínima da canción + As cancións máis curtas que isto non serán scrobbled + Porcentaxe de atraso de scrobble + Activar scrobble despois de escoitar esta porcentaxe da canción + Segundos de atraso do scrobble + Activar o scrobble despois de escoitar este número de segundos + Conta de ListenBrainz + Sesión iniciada. Toca para xestionar a conta + Introduza o seu Token de Usuario de ListenBrainz para activar o scrobbling + Token + Verificar + Por favor, introduza un token + Validando… + Token válido! Usuario: %s + Token inválido: %s + descoñecido + Erro na verificación: %s + Token borrado + Token gardado + Aviso: O token gardado non é válido! + Marcar como favoritos as pistas que gusten + Marcar automaticamente como favoritas as pistas en Last.fm cando gusten en YouTube Music (e quitar o favorito cando non gusten ou sexan marcadas como non desexadas) + Conta de Last.fm + Sesión iniciada como %s. Toca para xestionar a conta + Introduce as túas credenciais de Last.fm para iniciar sesión + Nome de usuario + Contrasiña + Iniciar sesión + Por favor, introduce o nome de usuario e a contrasiña + Iniciando sesión… + Sesión iniciada con éxito! + Fallou a obtención da sesión de Last.fm + Erro ao iniciar sesión: %s + Credenciais de Last.fm gardadas! + Limpeza de metadatos + Limpar metadatos + Limpar automaticamente o ruído común (como \"Official Video\", \"Remaster\", \"Live\") dos títulos, artistas e álbums antes do scrobbling + Limpeza personalizada mediante regex + Introduza unha expresión regular personalizada para eliminar texto adicional dos títulos das cancións, artistas ou álbums. Exemplo: (?i)\\s*\\((clean|explicit)\\) + Analisar artista e pista a partir do título + Extraer o nome do artista e da pista do título do vídeo usando o separador \" - \". Útil para contido resubido onde o nome do canal pode ser usado como o artista + Preencher álbum faltante + Cando a música non inclúa un álbum, búscao en Last.fm para que o teu scrobble estea completo + "O scrobbling sincroniza automaticamente o seu uso de YouTube Music co seu perfil de Last.fm ou ListenBrainz + +Para garantir que o scrobbling se sincronice durante a reprodución en segundo plano, asegúrese de que o uso da batería da aplicación de YouTube Music estea configurado como \"Sin restricións\"" + Usar letras de terceiros + Mostra as letras dun servizo de terceiros, recurrindo ás letras integradas cando non se atopen ningunhas + Manter a pantalla acesa ao ver as letras + Impede que a pantalla se apague mentres o panel de letras está aberto + Servizo de letras + Amosar + Sincronización + Panel + Endpoint de IA + Todos os provedores desactivados: letras integradas e letras incrustadas + Reestablecer a orde dos provedores? + Restaurar os provedores á orde de prioridade predeterminada + Limpar letras almacenadas + Eliminar as letras gardadas neste dispositivo + Limparon %1$s MB + Obter token + Token de usuario de Apple Music Media + "Pegue a cookie media-user-token de music.apple.com + +Pasos: +1. Inicie sesión en music.apple.com como usuario membro +2. Abra DevTools → Application → Cookies → music.apple.com +3. Copie o valor de \"media-user-token\"" + Apple Music: media-user-token gardado + Apple Music: media-user-token borrado + Apple Music: media-user-token inválido - comprobe e tente de novo + Spotify SpDc + "Pegue a cookie sp_dc de Spotify. + +Pasos: +1. Inicie sesión en open.spotify.com +2. Abra DevTools → Application → Cookies → open.spotify.com +3. Copie o valor de \"sp_dc\"" + Spotify: sp_dc gardado + Spotify: sp_dc eliminado + Spotify: sp_dc inválido - comprobe e tente de novo + O token de Apple Music é opcional. Sen el, usarase Lyrically como alternativa + Deezer ARL + "Pegue a cookie arl de Deezer. + +Pasos: +1. Inicie sesión en deezer.com +2. Abra DevTools → Application → Cookies → deezer.com +3. Copie o valor de \"arl\"" + Deezer: arl gardado + Deezer: arl eliminado + Deezer: arl inválido - comprobe e tente de novo + Token de Musixmatch + Engade un token nuramente para usar a túa propia conta. Ten que vir da aplicación de Musixmatch para Android: os tokens copiados de musixmatch.com pertencen ao cliente de escritorio retirado e a API rexeítaos + Musixmatch funciona sen un token, polo que este campo pode quedar baleiro + token do usuario + Musixmatch: token de usuario gardado + Musixmatch: token de usuario borrado + Musixmatch: token de usuario inválido - comprobe e tente de novo + Cookies de YouTube + "Pegue as cookies de YouTube para consultar as lexendas e o texto temporal de YouTube. + +Pasos: +1. Abra youtube.com nun navegador e acceda +2. Abra DevTools (F12) → Application → Cookies → youtube.com +3. Copie a cadea completa de cookies (pares clave=valor separados por puntos e comas) + +Cookies necesarias: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPISID, LOGIN_INFO" + YouTube: cookies gardadas + YouTube: cookies borradas + YouTube: cookies inválidos - comprobe e tente de novo + Mantén premida unha liña + Toca unha liña para saltar a ese momento da canción, mantén premida para copiar a palabra baixo o dedo, ou a liña completa cando a presión non sexa sobre unha palabra. Desactivado elimina ambos xestos + Mostrar etiquetas de sección + Etiqueta cada nova sección da letra cunha etiqueta pequena, como Verso ou Estribillo + Amosar botón de copiar + Toca para copiar a letra orixinal como texto sin formato; mantén premido para gardar o arquivo fonte da letra en Descargas + Mostrar botón de Traducir + Toca para alternar a tradución; mantén premido para ver só a tradución. Utiliza traducións do provedor, Google Translate ou unha API compatible con OpenAI + Mostrar botón de Romanizar + Toca para alternar a romanización; mantén premido para ver só a romanización. Utiliza a romanización do provedor, Google Translate ou unha API compatible con OpenAI + Romanización e alineación local + Romaniza no dispositivo cando o provedor, o teu modelo e Google Translate fallen. A lectura segue a cada palabra dunha letra sincronizada; a sincronización de palabras debe estar activada. Chinés, cantonés, xaponés, coreano, grego, ruso, xeorxiano, armenio; arredor de 120 MB + Lendo %1$s: %2$d%% + Lecturas listas (%1$d entradas) + Erro na descarga: desactiva e activa isto de novo para reintentalo + Mostrar botón de Actualizar + Toca para cambiar ao seguinte candidato de letras; mantén premido para buscar letras polo título da canción e o artista + Buscar letras + Título da canción + Artista + Introduce un título de canción e un artista + Ningún dos provedores activados pode buscar outras cancións Gardado + Resaltar palabras ou sílabas sincronizadas + Prioriza as letras palabra por palabra cando estean dispoñibles. Cando as letras inclúen a sincronización por palabra, resálcase cada palabra ou sílaba ao ritmo da música + Ocultar letras reproducidas + Ocultar as liñas de letras por riba da liña que se está reproducindo + Ocultar letras non reproducidas + Ocultar as liñas de letras por baixo da liña que se está reproducindo + Mostrar letras de MediaSession na pantalla de bloqueo + Amosa a liña actual na pantalla de bloqueo, Android Auto e Bluetooth. Require Android 11 ou posterior + Mostrar letras no miniplayer + Mostra a liña actual no reprodutor minimizado, coa pista movida ao subtítulo + Formato de título da canción/artista + Formato da pista que se mostra coa letra no reprodutor minimizado e na MediaSession. {t} é o título, {a} é o artista e {v} é a versión, como Acústica, cando a fonte da letra proporciona unha. O texto entre dous puntos só se mostra cando cada variable dentro del ten un valor; escriba \\: para un punto normal. Cada liña móstrase a súa vez, avanzando sempre que a letra cambie + Formato da liña da letra + Formato da liña da letra no reprodutor minimizado e na MediaSession. {lrc} é a liña e {n} é cantas veces seguidas se canta. O texto entre puntos só se mostra cando a liña se repite. Use \\: para un punto + O formato debe conter {lrc} + Modelo de artista-liña e tempo de animación + Tempo mínimo de pantalla (ms) + Tempo máximo de pantalla (ms) + Só para animacións multiliña. Mantén cada cadre polo menos o tempo mínimo. No máximo, avanza a liña do artista aínda que a letra se manteña igual. + O mínimo debe ser 0 ou maior. O máximo debe ser maior que 0 e polo menos o mínimo. + Limites de lonxitude da liña + Limites de liña de letra e artista, e peso do carácter de ancho completo + "Limite da liña da letra +Divide as letras máis longas en halves equilibradas. 0 desactiva a división." + "Limite da liña do artista +Divide o formato da pista pobladas en cadres cíclicos. 0 desactiva a división." + "Peso do carácter de ancho completo +Aplícase a ambos límites. Con 1.75, catro caracteres de ancho completo contouren como 7." + Introduza límites de números enteiros do 0 ao 10000 e un peso de carácter superior a 0 + Intermezo instrumental + Móstrase no miniplayer e na MediaSession durante un intermezo instrumental. Cada liña é un cadre de animación. {t} é o título, {a} é o artista e {lrc} é a última liña da letra + Desviación temporal do miniplayer e da MediaSession + Desplaza cando se mostra a seguinte liña no miniplayer e na MediaSession. Use un valor negativo se as liñas aparecen tarde, por exemplo, na pantalla de bloqueo + Filtros + Regex de metadatos + Expresión regular aplicada aos títulos, artistas e álbums antes de buscar as letras e ao calcular se dous títulos coinciden. Deíxao baleiro para desactivar ámbas funcións. O valor predefinido elimina elementos comúns como \"Official Video\", \"Remaster\", \"【插曲】\" e \"- Topic\" + Regex do filtro de letras + Expresión regular aplicada só ao texto orixinal das letras (non ás traducións nin romanizacións). Deixae vacío para desactivar + Marcadores de liña de info + Se as letras se filtran incorrectamente cara ao final, axústao ou límpao. Marcadores separados por comas: as liñas que conteñan un marcador seguido duno dous puntos móstranse como información de créditos ao final. Deixae vacío para desactivar + Poñer a primeira letra en maiúscula + Pon en maiúscula a primeira letra de cada liña da letra + Eliminar puntuación final + Elimina puntos, comas, dous puntos e puntos e comas ao final das liñas da letra. Mantéñense os signos de interrogación, de exclamación, as puntos suspensivos, as combas e os corchetes + Tamaño do texto + Tamaño do texto da letra + Desviación temporal global + As letras sincronizadas siguen a reprodución. Se se desvían, axuste o desfasementos de tempo. Desplace as letras sincronizadas cara adiante ou atrás. Use un valor positivo se as letras van adiante da canción + Usar segmentos de SponsorBlock para a desviación da letra + Coincidir sen os segmentos que SponsorBlock salta e desplazar as letras ao audio lanzado. Desactivado ignora SponsorBlock por completo + Esta pista non ten ningún id de vídeo para enviar + Fonte: %s + Mantén premido o botón de copiar para gardar este LRC; se podes axustar os tempos e compartelos cos provedores da comunidade, estaríamos agradecidos + Traducido por Google + Romanizado por Google + Traducido por %s + Romanizado por %s + Romanizado por %s + Traducido + Lingua de tradución + Lingua de romanización + Traducindo… + Non foi posible traducir a letra + Romanizado Automático + Romanizando… + Non se puideron romanizar as letras + Só traducido + Só romanizado + Modo dual + Copiado ao portapapeis + Actualizando + Actualizado + Chave API de QuickLRC + QuickLRC transcribe ou sincroniza os tempos das letras da pista actual. Un servizo separado, sen relación co endpoint compatible con OpenAI anterior + Clave de QuickLRC gardada. Usa o botón de sincronizar no diálogo de busca de letras, ou a indicación do panel cando unha pista non teña letras + Chave API de QuickLRC gardada + QuickLRC non aceptou esa chave + Chave API de QuickLRC eliminada + QuickLRC sincroniza os tempos de letras que non teñen, ou transcríbeas da pista, a cambio de créditos. Cunha clave configurada, as pistas sen letras ofrecen transcrición e o diálogo de busca engade un botón de sincronización + Solicitar alineación de QuickLRC + Alineando + Aplicar alineación + Solicitar transcrición de QuickLRC + Transcribindo + Transcrición cancelada + Só as pistas reproducidas desde YouTube poden transcribirse + Non hai letras para sincronizar - transcribindo en seu lugar + QuickLRC non puido procesar esta pista + Os créditos de QuickLRC esgotáronse para esta pista + "As letras coinciden polo título e o artista; os metadatos inusuais poden non atopar nada ou a versión incorrecta. Use a actualización para cambiar de versión. Os provedores marcados con * requiren un token. Non sempre se solicita a todos os provedores activados na súa orde, senón que responden segundo a súa velocidade.\n\nAs letras provén de servizos de terceiros que non están afiliados, patrocinados nin avalados por Morphe. Cada un deles úsase tal cal é: as letras, a súa dispoñibilidade e as súas licenzas permanecen nos servizos e nos seus licenciantes, e o seu uso pode estar suxeito aos seus termos de servizo. O que chega a un servizo activado é só a busca que solicitou: o título, o artista e, cando o servizo o requira, o ID do vídeo. O endpoint de IA opcional e a transcrición de QuickLRC funcionan só despois de configuralos e reciben o texto das letras ou a URL do vídeo da pista." + Activa nuras os provedores que uses, ou mantén os predefinidos - as solicitudes constantes poden saturar os sitios de letras de comunidades pequenas + Ocultar liña de créditos + Ocultar a liña de créditos que aparece debaxo das letras + Trocar a tradución e a romanización + ACTIVO coloca a romanización abaixo da letra e a tradución arriba; DESACTIVADO mantén a romanización arriba e a tradución abaixo + Tradución + Romanización + Provedor de tradución + Google (Predeterminado, Gratuíto) + Compatible con OpenAI (Personalizado) + Proveedor de romanización + Google (Predeterminado) + Dicionario local / sen conexión + Compatible con OpenAI (Personalizado) + Sección de IA + Só se usa cando o provedor non envía etiquetas de sección como Estribillo ou Verso, que o seu modelo compatible con OpenAI detecta + AI & Puntos final da nube + Configure as claves API, modelos, puntos final e plantillas de prompts para os provedores de AI. + URL do endpoint da API (ex. https://api.openai.com/v1/chat/completions) + Modelo de Prompt + Marcadores de posición: {language}, {task}, {title}, {artist}, {count}, {lines} + Prompt gardado + Indicación gardada, pero o endpoint da IA aínda non é utilizable. Configurao primeiro en Ajustes de IA + Prompt restablecido + JSON non válido. Corrixao ou limpe o campo antes de gardar + Configuracións de tradución & romanización gardadas + Configuracións restablecidas aos valores predeterminados + A URL base debe comezar por http:// ou https:// + Probar conexión + Configurar endpoints + Amosar todos os endpoints + Validando conexión… + Proveedor de tradución + Proveedor de romanización + Obter modelos + Obtendo modelos… + Seleccionar un modelo + Erro ao obter os modelos. Comprobe a clave API ou a conexión. + Ajustes da API de Gemini + Compatible con OpenAI (Endpoint personalizado) + Ajustes da API de DeepL + Clave API + Modelo + URL base + Clave API (opcional) + Prompt de tradución + Corpo de solicitude extra (JSON opcional) + Clave de autenticación (Free ou Pro) + Configurar claves API, modelos, endpoints e prompts + 8.23.51 - Desactivar iconas en negrita + 9.13.51 - Restaurar pestanas de A continuación, Letras e Relacionados do reprodutor diff --git a/patches/src/main/resources/addresources/values-gl-rES/reddit/strings.xml b/patches/src/main/resources/addresources/values-gl-rES/reddit/strings.xml index 65bb5f74d1..7bf57dffdf 100644 --- a/patches/src/main/resources/addresources/values-gl-rES/reddit/strings.xml +++ b/patches/src/main/resources/addresources/values-gl-rES/reddit/strings.xml @@ -16,6 +16,8 @@ Second \"item\" text" --> + Política de privacidade de Reddit + O inicio de sesión de Google non está dispoñible. Use o correo electrónico para iniciar sesión Anuncios Ocultar anuncios no feed Ocultar anuncios en los comentarios @@ -25,25 +27,111 @@ Second \"item\" text" Ocultar botón Crear Ocultar botón Descubrir ou Comunidades Ocultar botón Xogos + Barra lateral Ocultar sección Visitados Recentemente Ocultar sección Xogos en Reddit Ocultar sección Reddit Pro Oculta a sección Acerca de Oculta a sección Recursos Disposición + Copiar texto + Permite seleccionar e copiar texto en publicacións e comentarios. Desactiva o colapso de comentarios + O colapso de comentarios deixará de funcionar mentres Copiar texto estea activado + Fonte personalizada + Carga un ficheiro de fonte TTF ou OTF personalizado dende a ruta indicada abaixo. Activar isto tamén desactiva Forzar fonte do sistema + Fichero de fonte personalizada + Toca para elixir un ficheiro de fonte TTF ou OTF + Fonte seleccionada - %s + Escolla primeiro un ficheiro de fonte personalizado + Por favor, seleccione un ficheiro de fonte .ttf ou .otf + Erro ao cargar a fonte personalizada. Escolla un ficheiro diferente + Desactivar a interface de inicio moderna Desactivar ventá emerxente de captura de pantalla Desactiva a ventá emerxente ao tomar unha captura de pantalla + Forzar a fonte do sistema + Use a fonte do sistema do dispositivo en lugar de Reddit Sans ou Roboto. Activar isto tamén desactiva a fonte personalizada + Ocultar botón de Preguntar + Oculta o botón de Preguntar na barra de busca + Ocultar a sección de comunidades + Oculta a sección de comunidades relacionadas ou suxeridas nos subreddits + Ocultar as seccións de Tendencias + Oculta as seccións de Tendencias do feed e das suxestións de busca Eliminar diálogo de suxestión de notificación + Descarta automaticamente o diálogo de suxestión de notificación ao visitar un subreddit + Mostrar etiquetas no feed de inicio + Mostra as etiquetas de publicación no feed de inicio + Alguns elementos do feed poden non mostrar etiqueta + Mostrar conta de visualizacións nas publicacións + Limitación: Só dispoñible se a resposta da API inclúe a conta de visualizacións + Inverter a barra de acción da publicación + Move os botóns de votar e comentar das publicacións ao lado dereito, e os botóns de compartir e publicar en outro sitio ao lado esquerdo + Trocar os botóns de votar e comentar + Mostra o botón de comentar das publicacións antes dos botóns de votar Eliminar diálogo de aviso NSFW + Acepta automaticamente a advertencia de contido NSFW ao visitar un subreddit Miscelánea Acerca de Versión de parche e ligazóns oficiais Limpar enlaces compartidos + Elimina os parámetros de consulta de seguimento dos ligazóns compartidas Abrir enlaces directamente + Omiten as URL de redireccionamento ao abrir ligazóns externas + Media do feed a ancho completo + Amosa imaxes e vídeos de borde a borde no feed e nos detalles da publicación, sen padding lateral nin esquinas arredondadas + Intensidade do desvanecido do visor de media + Intensidade do desvanecido escuro detrás da attribute cando se visualizan imaxes e vídeos, desde 0 (sen desvanecido) ata 1 (predeterminada de Reddit) + Manter a posición do feed + Mantén o feed na mesma posición despois de pechar unha publicación, imaxe ou vídeo, en lugar de mover a publicación aberta ao tope + Altura máxima do multimedia do feed + Altura máxima de imaxes, vídeos e galerías no feed, como un múltiplo da súa largura. O multimedia máis alto recórtase. 1.33 é o valor predefinido de Reddit + Reproducir Redgifs con audio + Reproduce o vídeo orixinal de Redgifs en lugar da copia de Reddit, que a miúdo non ten audio + Calidade HD de Redgifs + Reproduce o vídeo de Redgifs en HD. Os vídeos HD son máis grandes e poden pausarse para cargar + Localización de descarga + Todas as contas + Predeterminado de Reddit: imaxes en Pictures/Reddit, vídeos en Download + Cartafol: %s + Nome do ficheiro: %s + Nomes de ficheiro de Reddit + Usa \"Todas as contas\" + Usar localización propia + Gardar en + Carpetas predeterminadas de Reddit Predefinido + Subcarpeta + Engadir subcarpetas con / no nome do ficheiro + Navegar… + Non hai ningún selector de carpetas dispoñible neste dispositivo + Nome do ficheiro + Nome do ficheiro de Reddit + Toca un token para insertalo. Un / crea unha subcarpeta. Os valores descoñecidos convértense en \"descoñecido\" + Imaxes: %s + Vídeos: %s Gardar Restablecer Eliminar + Restaurar a localización de descarga de todas as contas ao valor predefinido de Reddit? + Eliminar a localización de descarga de %s? + Non foi posible gardar na localización de descarga. Gardouse na carpeta predefinida de Reddit en seu lugar + A carpeta de descarga xa non está dispoñible. Gardouse na carpeta predefinida de Reddit en seu lugar + Non foi posible gardar o vídeo descargado + Esta carpeta non pode usarse para descargas + %s (actual) + %s (non iniciado sesión) + Anónimo (sen sesión iniciada ou en modo incógnito) + Ocultar a superposición do visor de multimedia + Oculta o título, os botóns e os controls de vídeo ao abrir unha imaxe ou vídeo. Toca no multimedia para amosalos + Ocultar o botón Ver a conversa + Oculta o botón \"Ver a conversa\" na parte inferior do visor de multimedia + Fondo negro do visor de multimedia + Usa un fondo negro detrás das imaxes e vídeos no visor de multimedia, en lugar da cor de fondo do tema + Abrir enlaces externamente + Abre as ligazóns no seu navegador en lugar do navegador interno da aplicación + Lembrar comentarios colapsados + Os comentarios que colapsas permanecen colapsados cando reabres a publicación + Icona da aplicación + Escolla a icona da aplicación Predefinido Descoñecido diff --git a/patches/src/main/resources/addresources/values-gl-rES/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-gl-rES/shared-youtube/strings.xml index 5f957ff04d..493e3dda36 100644 --- a/patches/src/main/resources/addresources/values-gl-rES/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-gl-rES/shared-youtube/strings.xml @@ -18,51 +18,298 @@ Second \"item\" text" --> + Animación de inicio Desactivado + Cor + Monocromo + + Apagado + Reinicia para rematar a configuración + Nome da aplicación + Fondo do tema escuro + Preto puro + Material You (Preto puro) + YouTube Clásico + Rosa escuro + Azul escuro + Verde escuro + Amarelo escuro + Laranxa escuro + Vermello escuro + Fondo escuro personalizado + Define a cor do fondo do tema escuro + Predefinido da app + Material You (Neutral) + Material You (Primario) + Material You (Secundario) + Material You (Terciario) Personalizado + Descargas + Modo de descarga Predefinido + Descargar na aplicación + Descargador externo + Nome do paquete do Downloader + Nome do paquete da aplicación de descarga externa instalada + Introducir o nome do paquete + Outro + Aplicación non instalada + %s non está instalado. Por favor, instáleo + "Non se puido atopar ningunha aplicación instalada co nome de paquete: %s + +Verifique que o nome do paquete sexa correcto e que a aplicación este instalada" + O nome do paquete non pode estar baleiro + Personalizado + Icona da aplicación + Original + Morphe negro + Morphe escuro + Morphe light + Morphe play light + Morphe play black + Personalizado + "Isto cambia o icona que se amosa no lanzador de aplicacións. + +Esta configuración non cambiará o icona en ningunha destas localizacións: +• Lista de aplicacións de configuración do sistema do dispositivo +• Algunhas notificacións push +• Selector de aplicacións \"Abrir con\" +• Icona de Android Auto + +Para cambiar o icona en todas as partes, aplica o parche de novo e cambia a opción de parche \"Icona da aplicación\"" + Icona de notificación + Coincidir coa icona da aplicación + Morphe play + Aviso de Morphe + O teu historial de visualización non se está a gardar.<br><br>Isto probablemente se debe a un bloqueador de anuncios DNS ou a un proxy de rede.<br><br>Para solucionalo, engade <b>s.youtube.com</b> á lista branca ou desactiva todos os bloqueadores DNS e proxies + Non amosar de novo + Simular fluxos de vídeo + Configura a imitación do cliente para evitar problemas de reprodución + "Corrixe problemas de reprodución para a maioría dos usuarios solicitando un fluxo de vídeo compatible + +Se vostede é un usuario de YouTube Premium, pode que esta configuración non sexa necesaria" + Non foi posible obter ningún fluxo de clientes + O cliente predefinido non funciona + "%1$s non funcionou varias veces seguidas, pero %2$s si. + +Establecer %2$s como o cliente predefinido?" + Cambiar + Desactivar este axuste pode causar problemas de reproducición + Tente activar: Spoof de fluxos de vídeo > Inicio de sesión en Android VR + Cliente predefinido + Inicio de sesión en Android VR + Non iniciou sesión en Android VR. Toque aquí para iniciar sesión + Actualmente iniciou sesión en Android VR + Para usar esta función, cambie \'Cliente predefinido\' a Android VR + Inicia sesión en Android VR + "Iniciar sesión en Android VR pode solucionar problemas de reprodución para algúns usuarios + +Para iniciar sesión, preme en continuar abaixo" + Saír + Instrucións + "Código de activación: %s + +1. Toca en \"Abrir sitio web\" abaixo para abrir o sitio web \"Conectar un dispositivo\" +2. Pega desde o portapapeles (ou escribe) o código de activación que aparece arriba +3. Inicia sesión en Google se se solicita. Se tes varias contas, elixe esta conta de YouTube +4. Toca en \"Permitir acceso a YouTube VR\" +5. Regresa a esta aplicación despois de chegar á mensaxe \"Dispositivo conectado\"" + Fallou a emisión dun código de activación + Fallou a emisión dun token de autenticación + Sesión iniciada correctamente + "Autenticación gardada + +• As súas configuracións de Morphe non importarán/exportarán a autenticación de Android VR +• Se esta aplicación non se usa durante 6 meses, Google eliminará esta autenticación +• Terá que iniciar sesión novamente se cambia a súa contrasinal de Google ou se desrexistra este dispositivo nas súas configuracións de Google" + OAuth2 non está dispoñible temporalmente (tempo límite da API esgotado) + A sesión de Android VR expirou, intenta iniciar sesión en VR de novo + OAuth2 non está dispoñible temporalmente (estado %s) + OAuth2 non está dispoñible temporalmente + Non se pode iniciar sesión en VR, a autenticación do sitio web non foi aprobada + Non se pode iniciar sesión en VR: %s + Saída de Android VR + Token de inicio de sesión de VR inválido, tenta iniciar sesión en VR de novo + Controls de multimedia de notificacións + Personaliza os controls de reprodución que se amosan na pantalla de bloqueo e no panel de notificacións + Ocultar botóns Anterior & Seguinte + Oculta os botóns Anterior e Seguinte na notificación de multimedia, na pantalla de bloqueo e nos controls dos auriculares + Desactivar barra de busca + Impede arrastar a barra de busca na pantalla de bloqueo e no panel de notificacións + Buscar coas botóns Anterior & Seguinte + Cambia os controlos da notificación de multimedia, da pantalla de bloqueo e dos auriculares para buscar en lugar de cambiar de vídeo + Lonxitude do retroceso + Segundos para retroceder co botón Anterior + Duración do avance rápido + Segundos para avanzar rápido co botón Seguinte + A duración do salto debe estar entre 1 e %s segundos. Usando o valor predefinido + Desactivar o protocolo QUIC + Desactivar o audio DRC + Forzar o idioma orixinal do audio + Reproduce sempre a pista de audio orixinal do vídeo en lugar dunha versión dobre + Saltar silencio + Salta automaticamente as pausas silenciosas no audio do vídeo + Lonxitude mínima da pausa + Só se acurtan as pausas que teñan polo menos esta lonxitude. Terá efecto cando comece o seguinte vídeo + Proxy de rede + Enruta as solicitudes de rede compatibles a través dun proxy + Activar proxy + Enruta as solicitudes de rede compatibles a través dun proxy HTTP ou HTTPS configurado + Anfitrión do proxy + Nome do anfitrión ou dirección IP do servidor proxy + Porto do proxy + Porto do servidor proxy + Usar proxy HTTPS + Usar HTTPS para conectarse ao servidor proxy en lugar de HTTP + Autenticación de proxy + Envia autenticación básica coas solicitudes de proxy + Nome de usuario do proxy + Nome de usuario para a autenticación básica de proxy + Contrasiña do proxy + Contrasiña para a autenticación básica de proxy + Permitir fallback directo + Conectar directamente se o proxy falla + Ocultar anuncios a pantalla completa + Para usar esta función, cambia \'Spoof video streams\' por calquera cliente que non sexa \'Android Studio\' ou \'Android VR Downgraded\' + Limpar enlaces compartidos + Elimina os parámetros de consulta de seguimento dos ligazóns compartidas + Cambiar os enlaces de compartir a youtube.com + Substitúe music.youtube.com por youtube.com nos enlaces compartidos + Cambiar os ligazóns de compartir a youtu.be + Obliga aos vídeos a usar o formato de URL acortado youtu.be + Protocol buffer do rexistro de depuración + Engade información de rexistro adicional para que os desenvolvedores identifiquen os compoñentes da aplicación + "Activar esta configuración rexistrará datos adicionais do deseño, incluíndo o texto en pantalla de algúns compoñentes da UI. + +Isto pode axudar a identificar compoñentes ao crear filtros personalizados. + +Porén, activar isto tamén rexistrará algúns datos do usuario, como a súa dirección IP" + Filtro personalizado + Configure filtros personalizados para ocultar calquera compoñente + Activar filtro personalizado + Lista de cadeas do construtor de rutas de compoñentes para filtrar, separadas por novas liñas + Filtro personalizado non válido: %s + Filtro do menú de axustes + Ocultar elementos na pantalla de axustes estándar da aplicación polo seu nome visible + Abra a pantalla de axustes estándar da aplicación unha vez para poblar a lista + Eliminar o diálogo de discreción do visor + Isto non ignora a restricción de idade. Simplemente acéptaa automaticamente + Proveedor de PoToken + Configure o provedor de PoToken para evitar problemas de reprodución + Proveedor de PoToken externo + "Soluciona problemas de reprodución usando un minter de PoToken externo (Experimental) + +Se é usuario de YouTube Premium, pode que esta configuración non sexa necesaria" + "Corrixe problemas de reprodución usando un xerador de PoToken externo (Experimental) + +Para usar esta función, desactiva 'Falsificar fluxos de vídeo'" + Se non funciona, tenta forzar o peche de MicroG + Abrir a configuración de PotHelper + "Esta función está actualmente en desenvolvemento e pode que non funcione + +Toca aquí para descargar PotHelper" + PotHelper xa non funciona + "O token integrado nesta versión de PotHelper xa non funciona + +Activa 'Simular fluxos de vídeo' para solucionar a reprodución" + Activar + Simular versión da aplicación + Engaña ao servidor para que pense que estás a usar unha versión máis antiga da aplicación para restaurar funcións da interface de usuario herdanas + "A versión da aplicación será simulada como unha versión máis antiga. + +Isto cambiará a aparencia e as funcións da aplicación, pero poden producirse efectos secundarios descoñecidos. + +Se se desactiva posteriormente, recoméndase borrar os datos da aplicación para evitar erros na UI" + Simular obxectivo da versión da aplicación + Tasa de refresco da aplicación Predefinido + %s FPS + Anular a taxa de actualización + Sempre + Reprodución en vertical + Reprodución en pantalla completa + Reprodución en vertical e pantalla completa + Fonte Predefinido + Fonte do sistema do dispositivo + Fichero de fonte personalizada + Fichero de fonte personalizada + Escolla un ficheiro de fonte TTF ou OTF + O ficheiro seleccionado non é unha fonte TTF ou OTF + Non se puido cargar a fonte personalizada e desactivouse + Os dislikes non están dispoñibles temporalmente (tempo limite da API) + Os \"non me gusta\" non están dispoñibles (estado %s) + Os dislikes non están dispoñibles (limite da API do cliente) + Os dislikes non están dispoñibles (%s) + Ocultos polo propietario + Non gosta deste vídeo, xunto con outras %s persoas + Non gústalle este vídeo, o %s dos votos son negativos + Activar Return YouTube Dislike + Amosar dislikes como porcentaxe + Amosa os dislikes como unha porcentaxe de likes en lugar dun número + Amosar likes estimados + Mostra unha estimación do número de likes en vídeos onde o creador desactivou os likes + Mostrar alertas de erro de conexión + Mostra unha notificación cando o servidor de Return YouTube Dislike non está dispoñible + "Return YouTube Dislike restaura as contaxes de \"non gosta\" que YouTube ocultou en 2021. As contaxes provén de datos arquivados antes da eliminación e, para os vídeos máis recentes, estímanse a partir dos votos dos seus usuarios, polo que son aproximadas e actualízanse cada Reservation poucos días + +O ID do vídeo que ves envíase á API de Return YouTube Dislike, e os teus \"gústame\" e \"non gosta\" envíanse anonimamente para mellorar as estimacións" + Estatísticas da API de Return YouTube Dislike deste dispositivo + Tempo de resposta da API, media + Tempo de resposta da API, mínimo + Tempo de resposta da API, máximo + Tempo de resposta da API, última multimedia + Os \"non gosta\" non están dispoñibles temporalmente - límite de frecuencia da API do cliente activo + Obtención de votos da API, número de chamadas + Non se realizaron chamadas de rede + %s chamadas de rede realizadas + Obtención de votos da API, número de tempos esgotados + Ningunha chamada de rede esgotou o tempo + %s chamadas de rede caducaron + Límites de frecuencia do cliente API + Non se atoparon límites de frecuencia do cliente + Atopouse o límite de solicitudes do cliente %s veces + %s milisegundos diff --git a/patches/src/main/resources/addresources/values-gl-rES/shared/strings.xml b/patches/src/main/resources/addresources/values-gl-rES/shared/strings.xml index 05165c78ac..cdf7d18727 100644 --- a/patches/src/main/resources/addresources/values-gl-rES/shared/strings.xml +++ b/patches/src/main/resources/addresources/values-gl-rES/shared/strings.xml @@ -24,6 +24,7 @@ Second \"item\" text" Restablecer cor Cor non válida Reinicio necesario + Reinicie a aplicación para que o cambio teña efecto Reiniciar Importar @@ -31,6 +32,7 @@ Second \"item\" text" As configuracións de Morphe se restablecerán os valores predeterminados Configuracións %s importadas Importación falida: %s + Navegar cara arriba Buscar axustes Acerca de Sen resultados para \'%s\' @@ -47,9 +49,34 @@ Second \"item\" text" • As buscas ignoran maiúsculas e signos de puntuación • Os axustes pais aparecen por riba dos axustes fillos desactivados" O historial de buscas está baleiro + Para gardar o historial de busca, escribe unha consulta de busca e preme Enter + Amosar o historial de busca da configuración + Nome dos axustes de Morphe Predefinido + Extras + Ajustes + Tempo de morfado Personalizado + Introduza un nome + O nome non pode estar baleiro + Amosar os icones de configuración de Morphe + Copiado: %s + Idioma de Morphe + "As traducións de algunhas linguas poden faltar ou estar incompletas. + +Para traducir novas linguas ou mellorar as traducións existentes, visita morphe.software/translate" + Idioma da aplicación + Importar / Exportar + Importar / Exportar a configuración de Morphe + Importar desde ficheiro + Ajustes importados con éxito, toca Garda para aplicalos + Non se puido importar os axustes + Exportar a ficheiro + Ajustes exportados con éxito + Fallo ao exportar os axustes + Estás usando a versión máis recente de Morphe Patches <i>%s</i> + Estás usando a versión <i>%1$s</i> de Morphe Patches<br><br><b>Actualización dispoñible: <i>%2$s</i></b><br><br>Para actualizar os teus parches, usa Morphe para volver a parchear esta aplicación Nota Esta é una versión preliminar e podería experimentar problemas inesperados Ligazóns oficiais @@ -65,34 +92,144 @@ Second \"item\" text" 2022 ‑ 2025 2022 ‑ 2025 2019 ‑ 2022 + Avisos de licenza Cargando… Depuración + Configura as opcións avanzadas de desenvolvedor e os rexistros de diagnóstico + Activar o rexistro de depuración + Rastrexamento en pila do rexistro de depuración + Inclúe seguimentos de pila nos rexistros de depuración para axudar a identificar erros Mostrar mensaxe toast en caso de erro de Morphe "Desactivar as mensaxes toast en caso de erro ocultará tódalas notificacións de erros de Morphe. Non se lle notificará de ningún evento inesperado" Exportar rexistros de depuración + Exportar rexistros de depuración de Morphe + Rexistros de depuración exportados con éxito + Fallo ao exportar os rexistros de depuración + Buscar nos rexistros de depuración… + Limpar O rexistro de depuración está desactivado + Non se atoparon rexistros de depuración + Rexistros de depuración copiados + Fallo ao exportar os rexistros de depuración: %s + Rexistros de depuración borrados + Xestor de bandeiras de funcións + Xestorar as bandeiras de funcións booleanas + Buscar sinalizadores de funcións… + Non activo (%s) + Activo (%s) + Bloqueado (%s) + Forzado (%s) + Automático Off On + Forzar activación + Bloquear Eliminar + Indicadores de funcións gardados + Indicadores de funcións restablecidos + Indicadores de funcións copiados ao portapapeis + Reinicie a aplicación para usar esta configuración + Exportar e importar + Importáronse %s sinalizadores de función + Non se atoparon sinalizadores de función + Sinalizadores de función exportados con éxito + Fallo ao exportar as etiquetas de función + Etiquetas de función cargadas, toca en Gardar para aplicalas + Fallo ao ler o ficheiro + Busca binaria + "Busca cal etiqueta de función causa un comportamento da aplicación + +A metade das %s etiquetas están bloqueadas e, despois de cada reinicio, indicas se o comportamento aínda está presente + +Reproduce o comportamento unha vez antes de comezar; de contrario, as etiquetas que a aplicación aínda non solicitou non aparecerán na busca" + Comezar + "Non hai etiquetas de función para buscar + +Só as etiquetas que a aplicación detectou e que non están forzadas actualmente poden usarse nunha busca binaria" + "Paso %1$s + +Quedan %2$s sinalizadores, %3$s deles bloqueados para este paso. + +Reinicia a aplicación, comproba se o comportamento persiste e responde abaixo" + Búsqueda de sinalizadores de funcións en curso + O comportamento está presente + O comportamento non está presente + Cancelar a busca binaria + Busca binaria cancelada + "O sinalizador de función %s causa o comportamento. + +Agora está bloqueado" + "Non se puido atopar ningún sinalizador de funcións. + +Parece que o comportamento da aplicación non está controlado por un sinalizador de funcións" + "Non se puido atopar ningunha bandeira de función. + +O comportamento desapareceu anteriormente na busca, polo que poden estar implicadas máis dunha bandeira ou unha das respostas era incorrecta" + Abrir os axustes de MicroG MicroG non está instalado. Instáleo Acción necesaria + "A súa versión de MicroG v%1s está obsoleta e pode causar problemas. + +Actualice MicroG á última versión v%2s" + Actualizar + Ignorar + "O MicroG instalado non é recomendado por Morphe: + +%1s + +Toca para desinstalalo, logo reabre esta aplicación e instala MicroG RE" + Desinstalar + "MicroG non ten permiso para executarse en segundo plano. + +Segue a guía \"Don't kill my app\" para o teu teléfono e aplica as instrucións á túa instalación de MicroG. + +Isto é necesario para que a aplicación funcione" Abrir sitio web + "As optimizacións de batería de MicroG deben desativarse para evitar problemas. + +Desativar as optimizacións de batería para MicroG non afectará negativamente ao uso da batería. + +Toca o botón de continuar e permite os cambios de optimización" Continuar + Amosar o diálogo de optimización da batería de MicroG + "Desactivar isto ocultará o aviso de optimización da batería de MicroG. + +Se non se cambian as optimizacións da batería, poderías experimentar: +• A aplicación pode pecharse aleatoriamente +• As notificacións poden non funcionar +• A reprodución en segundo plano pode non funcionar" + Xestión de ligazóns da app + Configurar os ligazóns para que se abran nesta aplicación + Paso 1: Desactivar os ligazóns da aplicación orixinal + Abra a configuración da aplicación orixinal e desactive todos os ligazóns de dominio + Paso 2: Activar as ligazóns da aplicación parcheada + Abre a configuración da aplicación parcheada e activa todos os vleos de dominio + Abrir configuracións + 🚨 Alerta Experimental 🚨 + "Estás usando unha versión experimental da app de ⚠️ <b>%1$s</b> +<br/><br/> +🔧 Espera comportamentos estraños da app ou erros non identificados mentres axustamos os parches para esta versión da app. +<br/><br/> +Se queres a experiencia sen problemas, <b>desinstala</b> esta e instala a versión recomendada da app de ✅ <b>%2$s</b>" + ⚠️ Quero a experimental + ✅ Quero a estable + Rexo do erro gardado nos documentos do usuario: %s + Rexo do erro gardado: %s diff --git a/patches/src/main/resources/addresources/values-gl-rES/sponsorblock/strings.xml b/patches/src/main/resources/addresources/values-gl-rES/sponsorblock/strings.xml index 70c7c39b36..3d85f7e86e 100644 --- a/patches/src/main/resources/addresources/values-gl-rES/sponsorblock/strings.xml +++ b/patches/src/main/resources/addresources/values-gl-rES/sponsorblock/strings.xml @@ -16,8 +16,219 @@ Second \"item\" text" --> + Activar SponsorBlock + SponsorBlock é un sistema baseado en crowdsourcing para saltar as partes molestas dos vídeos de YouTube + Apariencia + Mostrar botón de votación + Engade un botón para votar a favor ou en contra de segmentos + Usar deseño cadrado + Usa esquinas cuadradas para os botóns e controls de SponsorBlock + Usar botón de Saltar compacto + Reduce a largura do botón de Saltar para aforrar espazo + Ocultar automaticamente o botón de Saltar + Oculta o botón de Saltar pasados uns segundos en vez de mantelo visible durante todo o segmento + Duración do botón de Saltar + Define canto tempo permanece visible o botón de Saltar + Mostrar notificación de salto + Mostra unha notificación cando un segmento se salta automaticamente + Duración da notificación de salto + Define canto tempo permanece visible a notificación de desfacer + 1 segundo + 2 segundos + 3 segundos + 4 segundos + 5 segundos + 6 segundos + 7 segundos + 8 segundos + 9 segundos + 10 segundos + Amosar a duración do vídeo sen segmentos + Subtrae todos os segmentos da duración total do vídeo amosada na barra de busca + Crear novos segmentos + Amosar o botón \'Crear novo segmento\' + Engade un botón ao reprodutor de vídeo que permite enviar novos segmentos de SponsorBlock + Paso de axuste de tempo + Cantidade de salto dos botóns de axuste de tempo (en milisegundos) + O valor debe ser un número positivo + Directrices de envío + As directrices contén regras e consellos para crear novos segmentos + Siga as directrices + Lea as directrices de SponsorBlock antes de crear novos segmentos + Xa lido + Amósame + Xeneral + Amosar alertas de erro de conexión + Amosa unha notificación cando o servidor de SponsorBlock non está dispoñible + Activar o seguimento do recunto de saltos + Envia os recuntos de saltos á táboa de clasificación de SponsorBlock para seguir canto tempo se aforra + Duración mínima do segmento + Os segmentos máis curtos que este valor (en segundos) non se amosarán nin se saltarán + Duración de tempo inválida + O seu ID de usuario privado + Isto debe mantense privado. É como unha contrasinal e non debe compartirse con ninguén. Se alguén ten isto, pode suplantalo + O ID de usuario privado debe ter polo menos 30 caracteres + Cambiar URL da API + A dirección que SponsorBlock usa para facer chamadas ao servidor + Lista branca de canles + SponsorBlock está desactivado para os canles da lista branca + Notificar en canle na lista branca + Mostra unha notificación cando SponsorBlock está desactivado para unha canle na lista branca + SponsorBlock desactivado para esta canle + URL da API restablecida + A URL da API non é válida + URL da API cambiada + Importar / Exportar configuracións + Copiar + A túa configuración JSON de SponsorBlock que pode ser importada / exportada a Morphe e outras plataformas de SponsorBlock + "A túa configuración JSON de SponsorBlock que pode ser importada / exportada a Morphe e outras plataformas de SponsorBlock + +Isto inclúe o teu ID de usuario privado. Asegúrate de compartilo con prudencia" + Configuracións importadas con éxito + Fallo ao importar: %s + Fallo ao exportar: %s + "As túas configuracións conteñen un ID de usuario privado de SponsorBlock. + +O teu ID de usuario é como unha contrasinal, e nunca debe compartirse" + Non amosar de novo + Cambiar o comportamento do segmento + Patrocinador + Promoción paga, referencias pagas e anuncios directos. Non para autopromoción ou mencións gratuítas a causas / creadores / sitios web / produtos que lles gusten + Autopromoción / Non paga + Similar ao de Patrocinador, excepto que se trata de autopromoción / non paga. Inclúe seccións sobre mercadoría, doazóns ou información sobre con quen colaboraron + Lembrete de interacción (Subscribirse) + Un lembrete curto para dar \"like\", subscribirse ou seguilos no medio do contido. Se é longo ou sobre algo específico, debería estar en autopromoción + Destacado + A parte do vídeo que a maioría da xente busca + Intermedio / Animación de intro + Un intervalo sen contido real. Pode ser unha pausa, un cadre estático ou unha animación repetitiva. Non inclúe transicións que contiñan información + Tarxetas finais / Créditos + Créditos ou cando aparecen as tarxetas finais de YouTube. Non para conclusións con información + Gancho / Saúdos + Trailers narrados para o próximo vídeo, saúdos e despedidas. Non inclúe seccións que engadan contido adicional + Vista previa / Resumo + Colección de clips que amosan o que virá ou o que pasou no vídeo ou en outros vídeos dunha serie, onde toda a información se repite noutro lugar + Digresión / Bromas + Escenas tangenciais ou bromas que non son necesarias para comprender o contido principal do vídeo. Non inclúe seccións que proporcionen contexto ou detalles de fondo + Música: Sección sen música + Só para uso en vídeos musicais. Seccións de vídeos musicais sen música que non estean xa cubertas por outra categoría + Saltar + Destacado + Saltar patrociador + Saltar promoción + Saltar interacción + Saltar ao destacado + Saltar intro + Saltar intermedio + Saltar intermedio + Saltar outro + Saltar gancho + Saltar vista previa + Saltar vista previa + Saltar resumo + Saltar digresión + Saltar non-música + Saltar segmento + Saltado patrocinador + Saltada autopromoción + Saltado lembrador molesto + Saltado ata o punto destacado + Saltada a intro + Saltado o intermedio + Saltado o intermedio + Saltada a outro + Saltado o gancho + Saltou a anticipación + Saltou a vista previa + Saltou o resumo + Saltou a digresión + Saltou unha sección sen música + Saltou un segmento non enviado + Saltou varios segmentos + Saltar automaticamente + Saltar automaticamente unha vez + Amosar un botón de saltar + Amosar na barra de progreso + Desactivar + Imposible enviar o segmento: %s + SponsorBlock está temporalmente fora de servizo + Imposible enviar o segmento (estado: %1$s %2$s) + Imposible enviar o segmento. Límite de solicitudes alcanzado (demasiadas vanuit do mesmo usuario ou IP) + Non se pode enviar o segmento: %s + "Non se pode enviar o segmento. +Xa existe" + Segmento enviado con éxito + SponsorBlock non está dispoñible temporalmente (expirou o tempo da API) + SponsorBlock non está dispoñible temporalmente (estado %s) + SponsorBlock non está dispoñible temporalmente + Imposible votar polo segmento (expirou o tempo da API) + Imposible votar polo segmento (estado: %1$s %2$s) + Imposible votar polo segmento: %s + Voto positivo + Voto negativo + Cambiar categoría + Non hai segmentos para votar + %1$s a %2$s + Escolle a categoría do segmento + A categoría está desactivada nas configuracións. Activa a categoría para enviar + Novo segmento de SponsorBlock + Establecer %s como o inicio ou fin dun novo segmento? + Inicio + Fin + Agora + Hora na que comeza o segmento + Hora na que remata o segmento + Son correctas as horas? + "O segmento é dende + +%1$s +a +%2$s + +(%3$s) + +Listo para enviar?" + Enviar + O inicio debe ser antes do fin + Primeiro marca unha localización na barra de tempo + Fai unha vista previa do segmento e asegúrate de que salte suavemente + Editar a temporización do segmento manualmente + Queres editar a temporización do inicio ou do fin do segmento? + Hora proporcionada non válida + Enviar destacada + "É correcta a hora do destacado? + +%s + +Se non estás enviando un destacado, toca en Cancelar e marca a segunda localización" + Enviar destacado + Estatísticas + As estatísticas non están dispoñibles temporalmente (a API está caída) Cargando… + SponsorBlock está desactivado + O teu nome de usuario: <b>%s</b> + Toca aquí para cambiar o teu nome de usuario + Imposible cambiar o nome de usuario: Estado: %1$s %2$s + Nome de usuario cambiado con éxito + A túa reputación é <b>%.2f</b> + Creaste <b>%s</b> segmentos + Toca aquí para ver os teus segmentos + Clasificación de SponsorBlock + Ahoubraste a xente de <b>%s</b> segmentos + Toca aquí para ver as stats globais e os principais contribuidores + Iso son <b>%s</b> das súas vidas.<br>Toca aquí para ver a clasificación + Saltaste <b>%s</b> segmentos + Iso son <b>%s</b> + Reiniciar o contador de segmentos saltados? + %1$s horas %2$s minutos + %1$s minutos %2$s segundos + %s segundos + Opacidade: + Cor: + "SponsorBlock é unha base de datos colaborativa de segmentos de vídeo como patroinios, intros, outros e lembranzas de subscrición. Os segmentos son enviados e votados pola comunidade, e a base de datos é aberta e gratuíta para descargar + +O ID do vídeo que ves envíase á API de SponsorBlock para obter os seus segmentos" diff --git a/patches/src/main/resources/addresources/values-gl-rES/youtube/strings.xml b/patches/src/main/resources/addresources/values-gl-rES/youtube/strings.xml index bb084a48b4..7ad4f6e4a7 100644 --- a/patches/src/main/resources/addresources/values-gl-rES/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-gl-rES/youtube/strings.xml @@ -113,6 +113,9 @@ Os cambios inclúen: Agochar enquisas Agochar enquisas e suxestións sobre a precisión das recomendacións no feed Agochar estante de etiquetas + + Ocultar o banner \"Os vídeos non dispoñibles están ocultos\" + Oculta o banner \"Os vídeos non dispoñibles están ocultos\" nas listas de reprodución Agochar a hora da subida "Limitacións: @@ -154,223 +157,1500 @@ Os cambios inclúen: These hints appear in the video player while dragging the seekbar or holding to speed up playback. --> Agocha as suxestións que aparecen durante os xestos do usuario, como «Arrastra cara arriba para unha busca precisa», «Solta para cancelar» e «2x». Agochar a barra de mensaxes + Agochar as directrices dos subscritores + Agochar o botón «Sincronizar co vídeo» + Ocultar o gradiente de manter premido + Oculta o gradiente dos controlos do reprodutor ao deslizar o dedo mentres manténs premido para acelerar + Ocultar as reaccións temporizadas + Oculta a función que permite aos usuarios reaccionar con emojis na sección de comentarios e no chat en directo + Ocultar o menú de \"Axuda & comentarios\" + Oculta o menú de \"Axuda & comentarios\" ao usar o botón dos tres puntos, como nos resultados da busca + Ocultar identificador + Oculta o identificador do teu canal na pestana \'Ti\' + Ocultar título do vídeo + Oculta o título do vídeo no modo de pantalla completa + Desinfectar subtítulos de vídeo + Elimina frases como \'#\', \'Fundraiser\', \'product(s)\' e \'Shop\' dos subtítulos do vídeo + Ocultar o botón «Ask Gemini» + Oculta o botón \'Preguntar a Gemini\' na parte inferior do panel \'Preguntar sobre este vídeo\' + Descrición do vídeo + Elixe que compoñentes aparecen na descrición do vídeo + Ocultar a sección \"Resumo do vídeo xerado por IA\" + Ocultar a sección Preguntar + Ocultar a sección Atributos + Oculta as seccións de Lugares destacados, Xogos, Música e Persoas mencionadas + Agochar a sección de ligazóns do canal + Ocultar a sección de Capítulos + Ocultar a sección de Correccións + Ocultar a sección «Progreso do curso» + Ocultar a sección Explorar + Oculta as seccións Explorar este curso e Explorar o podcast + Ocultar a sección «Explorar este curso» + Ocultar a sección \'Explorar o podcast\' + Ocultar a sección de canles destacadas + Ocultar a sección de ligazóns destacadas + Ocultar a sección de Listas de reproducir destacadas + Ocultar a sección de vídeos destacados + Ocultar a sección de hashtags + Agochar a sección «Como se fixo isto» + Ocultar os puntos Hype + Ocultar a sección de tarxetas de información + Ocultar a sección \'Conceptos clave\' + Ocultar a sección de cuestionarios + Ocultar a sección \'Buscar dentro deste vídeo\' + Ocultar botón de Suscrición + Ocultar a sección de transcrición + Ocultar a sección \'Detalles do vídeo\' + Barra de filtros + Escolla onde ocultar a barra de filtros deslizante + Agochar na páxina do canle + Ocultar nos comentarios + Ocultar nos feeds + Ocultar nos vídeos relacionados + Ocultar nos resultados da busca + Ocultar no historial de visualización + Páxina do canal + Elixe que compoñentes aparecen na páxina do canal + Activar o filtro da pestana do canal + Filtro da pestana do canal + Introduce os nomes das pestanas do canal polos que filtrar, separados por saltos de liña + Ocultar botón de comunidade + Ocultar o botón Unirse + Ocultar a vista previa dos enlaces + Oculta os ligazóns web clicables no cabeceiro do canal + Agochar a etiqueta \"Só para membros\" + Agocha o chip \'Só para membros\' na barra de filtros do canal + Ocultar a sección de membros + Oculta a lista de membros do canal + Ocultar a sección de publicacións + Ocultar botón de Loja + Ocultar botón de Suscribirse + Menú Explorar + Elixe que compoñentes aparecen no menú explorar + Ocultar o menú de Cursos + Ocultar o botón Explorar + Ocultar o menú \'Fashion & Beauty\' + Ocultar o menú de Gaming + Ocultar o menú de Hype + Ocultar menú de Aprendizaxe + Ocultar menú de Directos + Ocultar menú de Membresías + Ocultar menú \'Películas & TV\' + Ocultar menú Música + Ocultar menú de Noticias + Ocultar menú de Playables + Ocultar menú de Podcasts + Ocultar menú de Shopping + Ocultar menú de Deportes + Ocultar o menú de YouTube Premium + Ocultar o menú de YouTube Studio + Ocultar o menú de YouTube Music + Ocultar o menú de YouTube Kids + Ocultar o menú de YouTube Create + Ocultar menú \'Como funciona YouTube\' + Ocultar os pés de páxina lexais e de privacidade + Comentarios + Elixe que compoñentes aparecen na sección de comentarios + Activar o filtro do carrusel de comentarios + "• Asegúrate de introducir o nome exacto do elemento do carrusel (ex. Preguntar, Dar hype a este vídeo, Escoitar en YouTube Music) +• Ocultar elementos do carrusel pode ter efectos secundarios non desexados +• Se todos os elementos do carrusel están ocultos, a sección completa de comentarios tamén se ocultará" + Filtro do carrusel de comentarios + Introduce os nomes do carrusel de comentarios polos que filtrar, separados por saltos de liña + Ocultar o resumo do chat de IA + Ocultar as directrices da canle + Ocultar mensaxes de contexto + Ocultar o encabezado \'Comentarios dos membros\' + Ocultar a sección de comentarios + Ocultar a sección de comentarios no feed de Inicio + Ocultar as Normas da comunidade + Ocultar o botón \'Crear un Short\' + Ocultar botón de Non gustar + Ocultar o botón de emojis + Ocultar opcións da barra de filtros + Oculta todas as opcións da barra de filtros excepto \'Principais\' e \'Máis recentes\' + Ocultar animacións e tarxetas de agasallo + Ocultar botón de Agasallo + Ocultar o botón de Información + Ocultar a barra de doadores do chat en directo + Ocultar o botón de desplazar ao final do chat en directo + Ocultar as bowlings do chat en directo + Oculta as ventanas emergentes de axuda que aparecen preto da barra de entrada do chat, como o aviso de chat só para suscritores + Ocultar o botón do Menú + Oculta o botón do menú de 3 puntos de cada comentario + Ocultar a vista previa do comentario + Oculta a vista previa do comentario baixo o vídeo antes de abrir a sección completa de comentarios + Comentario oculto por Morphe… + Activar botón de comentarios minimalistas + Oculta as tarxetas deslizantes e o seu selector de páxinas, amosados no botón de comentarios + Ocultar o botón de Grazas + Ocultar botón de marca temporal + Agochar o botón \'Fans principais\' + Ocultar o botón Traducir + Ocultar a mensaxe \'Aínda non hai comentarios\' + Desinfectar ligazóns de busca resaltadas + Desinfecta as ligazóns de busca resaltadas, deixando nuras o texto orixinal + Filtro de palabras clave + Escolle palabras ou frases específicas para ocultar vídeos nos feeds e nas buscas + Ocultar comentarios por palabras clave + Ocultar vídeos de Inicio por palabras clave + Ocultar resultados de busca por palabras clave + Ocultar vídeos de Suscricións por palabras clave + Palabras clave para ocultar + "Introduza palabras clave e frases para ocultar, separadas por novas liñas + +As palabras clave poden ser calquera texto que apareza nos títulos dos vídeos ou nos nomes dos canles + +As palabras con letras maiúsculas no medio deben coincidir exactamente (ex. iPhone, TikTok, LeBlanc)" + "Inicio / Suscricións / Busca / Os comentarios filtranse para ocultar o contido que coincide coas frases clave + +Limitacións: +• Non se poden ocultar os Shorts polo nome do canal +• É posible que algúns compoñentes da interface de usuario non se oculten +• A busca dunha palabra clave pode non mostrar resultados" + Coincidir con palabras completas + Rodear unha palabra clave/frase con comillas evitará coincidencias parciais nos títulos dos vídeos e nos nomes dos canles<br><br>Por exemplo,<br><b>\"ai\"</b> ocultará o vídeo: <b>How does AI work?</b><br>pero non ocultará: <b>What does fair use mean?</b> + Non se pode usar a palabra clave: %s + Engache aspas para usar a palabra clave: %s + A palabra clave ten declaracións conflituantes: %s + A palabra clave é demasiado curta e require aspas: %s + A palabra clave ocultará todos os vídeos: %s + "• Inicio: %s +• Suscricións: %s +• Busca: %s +• Comentarios: %s +• Total: %s" + Estatísticas + Vídeos ocultos, últimas 24 horas + Vídeos ocultos, todo o tempo + Reiniciar o contador das últimas 24 horas? + Reiniciar o contador de todo o tempo? + Filtro de canles de IA + Ocultar vídeos de canles marcadas como contido xerado por IA pola lista da comunidade AiSList + Lista negra: Confianza alta + Lista de aviso: Confianza moderada + Ocultar vídeos na Inicio + Ocultar vídeos na busca + "• Inicio: %s +• Busca: %s +• Total: %s" + Canles da lista de bloqueo + Canles da lista de aviso + Activa un filtro para cargar as canles + Cargando canles… + "A páxina de inicio e os resultados da busca están filtrados para ocultar vídeos de canles sinaladas por AiSList + +Limitacións: +• Os Shorts de IA filtrados poden aparecer como un espazo vacío +• Alguns vídeos de IA poden seguir aparecendo" + "AiSList é unha lista de código aberto de canles de YouTube que publican contido xerado por IA, mantida por voluntarios. As canles son reportadas polos usuarios e revisadas antes de ser engadidas + +A lista descárgase desde GitHub e os vídeos fíltranse no seu dispositivo" + Denunciar canles de IA + Engadir entrada de informe ao menú de vídeo + Engadir unha entrada ao menú de vídeo que informa do canle dun vídeo a AiSList + Nome do informador + Nome que aparece xunto aos teus informes na táboa de clasificación pública de AiSList. A función de informar non está dispoñible mentres este campo estea vacío + Informar de canle de IA + "Enviar %s a AiSList, usando este vídeo como proba? + +Só reporta canles que: +• Usen voces, visuais, guións ou música de IA como contido principal +• Produzan contido en masa con ferramentas de IA e pouca intervención humana + +Non reportes canles que só falen de IA, ou que usen a IA para algunhas partes dos seus vídeos. + +Os reportes son públicos e revísanse antes de que unha canle sexa engadida a unha lista. A eliminación só é posible mediante unha solicitude en GitHub" + Esta canle xa está na lista de bloqueo de AiSList + Esta canle xa está na lista de aviso de AiSList + Reportar + Define un nome de remitente nos axustes do filtro de canles de IA para reportar canles + Buscando o canal… + Non se pode atopar o canle deste vídeo + Canle denunciada e á espera de revisión + AiSList non puido gardar a denuncia, tente máis tarde + Enviaron demasiadas denuncias, tente máis tarde + AiSList rexeitou a denuncia + Erro na denuncia, comprobe a súa conexión + Lista branca de anuncios do canle + Os vídeos das canles na lista branca reprodúcense con anuncios, así os seus creadores reciben o pago. Os anuncios permanecen bloqueados en todas as demais canles + Engadir canle á lista branca de anuncios + Eliminar canle da lista branca de anuncios + Agochar a balda da tenda do creador + Ocultar o banner da tenda na pantalla final + Ocultar anuncios xerais + Ocultar banners de mercadoría + Ocultar etiquetas de promoción pagađa + Ocultar os anuncios pop-up do reprodutor + Ocultar tarxetas auto patrocinadas + Ocultar ligazóns de compras + Ocultar anuncios de vídeo + Ocultar promocións de YouTube Premium Cargando… + Canle actual: + Engadir á lista branca Eliminar + Canles na lista branca + Non hai canles na lista branca + Canle engadida á lista branca + Canle eliminada da lista branca + Buscar dentro do canal + Engade un segundo botón de busca nas páxinas do canle para buscar só nese canle + Buscar canle + Resultados da busca de canles + Non se atopou nada neste canal + Non foi posible buscar neste canal + Vídeos + Publicacións + Ningúnha publicación neste canal coincide + Relevancia + Máis vistos + Menos vistos + Máis recentes + Máis antigos + Máis curto + Máis longos + Ligazón copiada ao portapapeis + Ligazón con marca temporal copiada ao portapapeis + Mostrar botón de Copiar ligazón do vídeo + Toca para copiar a ligazón do vídeo, mantén premido para copiar coa marca temporal + Copiar a marca temporal por defecto + Inverte as accións de tocar e manter premido do botón \'Copiar ligazón do vídeo\' + Copiar o título do vídeo + Mantén premido o título do vídeo para copialo + Título do vídeo copiado ao portapapeis + Copiar comentarios + Mantén premido un comentario para copialo + Comentario copiado ao portapapeis + Mantén premido para abrir o menú do vídeo + Ao manter premido en calquera parte dun vídeo do feed ábrese o seu menú + Mostrar o botón de Ver máis tarde nas miniaturas + Móstrase un botón de Ver máis tarde nas miniaturas dos vídeos do feed. Ao mantelo premido ábrese Gardar na lista de reprodución + Descargar imaxes da publicación + Amósase un botón de descarga xunto ao botón de Compartir nas publicacións con imaxes ou GIFs, e engádase un elemento de descarga ao seu menú + Descargar imaxes + Descargar imaxe + Descargando %s imaxes… + Descargando imaxe… + %1$s imaxes gardadas en %2$s + Imaxe gardada en %s + Non foi posible descargar a imaxe + Non se atopou ningunha imaxe nesta publicación + Engadir o elemento Preguntar ao menú desplegable + Engade un elemento de resumir vídeo ao menú desplegable do feed + "Preguntar require inicialmente unha conta sesión iniciada nun país onde Preguntar este dispoñible. + +Despois de abrir unha vez un vídeo co botón Preguntar, a función funciona con todos os vídeos aínda que o país actual non teña a función Preguntar." + Resumir o vídeo + Non se puido abrir Preguntar para este vídeo + Ask non está dispoñible para a túa conta ou o teu país actual + Amosar o número de visualizacións nas miniaturas + O número de visualizacións dos vídeos do feed amósase na parte inferior esquerda das súas miniaturas en lugar de baixo do título. Alguns vídeos, como os das estantes, poden non amosar o número de visualizacións + Mostrar a contaxe de likes da miniatura do feed + Amosar a porcentaxe de likes na miniatura do feed + Buscar nos comentarios + Toca o botón de busca nos comentarios para mostrar só os comentarios cargados que contíñan todas as palabras buscadas. Pon as palabras entre comillas para buscar unha frase + Buscar nos comentarios + Palabras para buscar Busca Limpar + Cambia a orde de clasificación ou abre os comentarios de novo para buscalos + Busca limpada. Cambia a orde de ordenación ou abre os comentarios de novo para amosar todos os comentarios + Desactivar a ventá emerxente \'Iniciar sesión na TV\' + Desactivar a ventá emerxente \'Conecta os teus dispositivos\' + Desactivar o salto de capítulo con dobre toque + Desactiva o xesto de dobre toque con dous dedos que salta capítulos, que pode activarse accidentalmente cun só dedo + Descarga vídeos, podcasts ou cancións con aplicacións de descarga de terceiros + Mostrar o botón de descarga na superposición do reproductor + Mostra un botón no reproductor de vídeo que abre o teu descargador externo + Descarga rápida ao tocar + Suxestións para descargar en segundo plano. Mantén premido para abrir o descargador. Funciona con YTDLnis e YTindir + Marcar descargas como vistas + Os vídeos descargados cun descargador externo engádense ao teu historial de visualización de YouTube + Full HD (1080p) + Media (192 kbps) + Descarga rápida Vídeo + Audio + Todas as opcións + Amosar descarga no menú de Shorts + Engade un elemento ao menú des same do descargador externo no reprodutor de Shorts Descargar + Tradución con voz + Ler as subtitulacións do vídeo en voz alta usando a síntese de voz do dispositivo, sincronizada coa reprodución + Mostra un botón superposto no reprodutor de vídeo. Toca para activar/desactivar a tradución, mantén premido para a configuración rápida + Idioma + Proveedor de tradución + Volume do audio orixinal + Volume do audio orixinal de YouTube mentres a tradución está falando + Volume da tradución + Volume da voz da tradución dobrexada + Velocidade máxima de fala + Multiplicador de velocidade máxima ao axustar a fala nun espazo de subtítulo + "API de tradución gratuíta, non se require clave API. Ata 5.000 caracteres por día no nivel gratuíto, ou ata 50.000 caracteres por día cun enderezo de correo electrónico. + +Toca aquí para abrir MyMemory.translated.net" + Correo electrónico de MyMemory + Opcional. Introduza o seu enderezo de correo electrónico de MyMemory para aumentar o límite diario de 5.000 a 50.000 caracteres. Déixeo vacío para usar o límite anónimo + "Tradución de IA de pago a través de OpenRouter. Recarga unha vez e traduce miles de vídeos. Mistral Nemo é a opción máis barata, Gemma 4 ofrece a maior calidade. + +Toca aquí para abrir OpenRouter.ai" + Chave API de OpenRouter + A súa chave API de OpenRouter. Cree unha chave en OpenRouter.ai > Workspace > API Keys + Modelo de OpenRouter Personalizado + OpenRouter non configurado + Introduza a súa chave API de OpenRouter nos axustes antes de seleccionar este provedor + Gratis + %s por cada 100 horas de vídeo + Voz Sistema + Masculino multilingüe + Feminino multilingüe + Masculino + Feminino + ¡É a hora de morphing! + Tradución non dispoñible + "O servizo de tradución devolveu o erro HTTP %s. É probable que o seu enderezo IP teña unha restricción de límite de solicitudes durante ata 24 horas. + +Para continuar usando as traducións, cambie a un provedor de tradución diferente nos axustes ou use unha VPN" + Non mostrar de novo + "As súas configuracións contén unha clave API de OpenRouter. + +A súa clave API nunca debe ser compartida. Calquera persoa con acceso a ela pode usar os seus créditos de OpenRouter" + Límite de crédito de OpenRouter alcanzado + Alcanzou o límite de crédito de OpenRouter. Recargue o seu saldo ou aumente o límite de gasto da súa clave en OpenRouter.ai + Abrir OpenRouter.ai + Fallou a tradución de OpenRouter (HTTP %s) + Límite diario de MyMemory alcanzado + Próximo dispoñible ás %s + Consumiches a cota de tradución gratuíta de MyMemory de hoxe (5.000 caracteres). Engade un correo electrónico de MyMemory na configuración para aumentar o límite diario a 50.000 caracteres, ou cambia a un servizo de tradución diferente + Usaste a cota de tradución de MyMemory de hoxe (50.000 caracteres). Cambia a un servizo de tradución diferente ou inténtao mañá + A tradución de MyMemory fallou (estado %s) + Activar a repetición do vídeo + Non lembrar o vídeo en bucle + Impedir que o estado actual do vídeo en bucle se gardes para os vídeos seguintes + Mostrar botón de reproducir vídeo en bucle + A repetición do vídeo está activada + A repetición do vídeo está desactivada + Rango de bucle + Inicio + Fin + [hh:]mm:ss + [hh:]mm:ss (vacío = fin do vídeo) + Establecer + Limpar + Bucle %1$s - %2$s + Rango de tempo inválido + Mostrar botón de صورة-en-foto + Amosar o menú de Picture-in-Picture + Cambiar a Picture-in-Picture + Mostrar botón de reproducir todo + "Toca para xerar unha lista de reprodución de todos os vídeos do canle, mantén premido para desfacer + +Info: Pode que non funcione en transmisións en directo" + Modo de xerar lista de reprodución + Todo o contido (Ordenar por tempo, ascendente) + Todo o contido (Ordenar por tempo) + Todo o contido (Ordenar por popularidade) + Só vídeos (Ordenar por tempo) + Só vídeos (Ordenar por popularidade) + Só Shorts (Ordenar por tempo) + Só Shorts (Ordenar por popularidade) + Só vídeos transmitidos (Ordenar por tempo) + Só vídeos transmitidos (Ordenar por popularidade) + Só contidos para membros + Vídeos só para membros + Shorts só para membros + Transmisións en directo só para membros + Imposible xerar a lista de reprodución debido a unha discordancia no ID do canal + Mostrar botón de recargar vídeo + O ID do vídeo non está dispoñible. Recarga o vídeo manualmente + Amosar o botón Gardar para ver máis tarde + Toca para gardar en Ver máis tarde, mantén premido para as opcións da cola + Vídeo gardado con éxito na lista de reprodución \"Ver máis tarde\" + Este vídeo xa está gardado na lista de reprodución \"Ver máis tarde\" + Non se pode gardar este vídeo na lista de reprodución \"Ver máis tarde\" + Gardar para ver máis tarde + Mostrar o botón Gardar para ver máis tarde no menú desplegable de axustes do reproductor + Permite gardar o vídeo que se está a ver no apartado \'Ver máis tarde\' desde o menú desplegable de axustes do vídeo + Amosar o botón Gardar para ver máis tarde nos vídeos infantis + Permite gardar vídeos infantis no apartado \'Ver máis tarde\' desde o menú desplegable + Mostrar o botón Gardar para ver máis tarde no menú desplegable de shorts + Permite gardar shorts no apartado \'Ver máis tarde\' desde o menú desplegable + Redirixir Ver máis tarde a unha lista de reprodución + Ver máis tarde + Engadir á cola de Morphe + Manter a cola entre sesións + A cola mantense despois de reiniciar a aplicación + Trocar as accións de Gardar e Cola + Toca para as opcións da cola, mantén premido para gardar en Ver máis tarde + Tipo de Reproducir a continuación na cola Predefinido + Cola nativa + Cola de Morphe + Cola de Morphe no lado do cliente + Engadir elemento ao menú da cola do flyout do feed + Engade un elemento flyout adicional. Actívao se non se amosa \"Engadir á cola\" + Cola + Cola (%s) + Limpar todo + Non hai nada na cola + Usa \'Reproducir a continuación na cola\' en calquera vídeo + Reproducíndose agora + Reproducir a continuación na cola + Reproducir o último da cola + Reordenar Eliminar + Engadido á cola + Non foi posible engadir este vídeo á cola + As listas de reprodución non se poden engadir á cola + A cola está chea + Non foi posible iniciar o vídeo + Lista branca de anuncios no menú despregable + Engade un elemento ao menú desplegable que permite que os anuncios se reproduzan no canle dese vídeo + Esta función pode requirir o uso de PotHelper ou cambiar \"Spoof video streams\" a inicio de sesión de TV ou Android VR + Lista branca de velocidade no menú desplegable + Engade un elemento ao menú desplegable que sempre reproduce o canal dese video a velocidade 1.0x + Sesión non iniciada + Non hai ID de lista de reprodución + A cola está vacía + Non hai ID de vídeo + Fallou: %s + Fallou ao engadir á cola + Fallou ao crear a cola + Fallou ao eliminar da cola + Fallou ao gardar a cola + Engadido á cola + Cola creada + Eliminado da cola + Cola gardada en %s + Cola de Morphe + Engadir á cola + Engadir á cola e abrir + Engadir á cola e reproducir + Engadir e recargar vídeo + Eliminar da cola + Eliminar e abrir cola + Eliminar e recargar o vídeo + Abrir cola + Gardar a cola na lista de reprodución + Lista de reprodución gardada + Gardado en %s + Limpar cola + Cola limpada + Cola + Reproducir o último da cola + Reproducir o seguinte da cola + YouTube non puido mostrar a cola. + A cola non está lista para este vídeo. Abre o reprodutor de vídeo e tenta de novo. + Engadir Mix á cola ou ver Mix? + Ver + Engadir á cola + Cargando Mix… + Abrir canal desde o avatar en directo + Impede que a transmisión en directo se abra ao tocar o avatar do canal + Desactivar o xesto de busca precisa + Desactiva o modo de busca precisa que se activa ao deslizar a barra de busca cara arriba + Activar o DVR de transmisións en directo + Permite buscar en todas as transmisións en directo, mesmo naquelas que normalmente o restrinxen + Activar as miniaturas da barra de busca + Mostra unha miniatura do vídeo ao arrastrar a barra de busca + Máis reproducido + "'Activar o DVR de transmisións en directo' pode non estar dispoñible + +Para solucionar isto, activa 'Falsificar transmisións de vídeo' e cambia o 'Cliente predefinido' a 'TV Simply' ou 'visionOS'" + Ampliar a duración do DVR de transmisións en directo + Permite retroceder emisións en directo ata 7 días no pasado + "'Ampliar duración do DVR de transmisión en directo' pode non estar dispoñible + +Para solucionar isto, actíva 'Spoof video streams' e cambia o 'Cliente predefinido' a 'TV Simply' ou 'visionOS'" + Activar deslizar para buscar + Activa o deslizamento para buscar en lugar de reproducir á 2x velocidade ao manter premido o reprodutor de vídeo + Activar tocar para buscar + Basta con tocar a barra de busca para saltar a esa posición sen arrastala + Xesto de deslizar á esquerda + Xesto de deslizar á dereita + Xesto de deslizar cara arriba + Anchura da zona esquerda/dereita + Porcentaxe de cada bordo da pantalla reservada para xestos de deslizamento esquerda e dereita (5-50) + Brillo + Volume Nativo Off + Ignorar deslizamentos cando stea bloqueado + Os xestos de deslizamento ignóranse mentres o reprodutor está bloqueado con \'Bloquear pantalla\' + Activar o xesto de premer para deslizar + Require premir e manter premido antes de que se recoñeza un xesto de deslizamento + Activar a retroalimentación háptica + Gardar e restaurar o brillo + Lembra o nivel de brillo ao entrar / saír do modo de pantalla completa + Activar o xesto de brillo automático + Deslizar o brillo ata o seu valor máis baixo activa o brillo automático do sistema + Automático + Tempo de espera da superposición do deslizamento + A cantidade de milisegundos que a superposición é visible + Opacidade do fondo da superposición do deslizamento + Axusta o nivel de transparencia (0 é transparente, 100 é opaco) + Cor do brillo da superposición do deslizamento + Define a cor da barra de progreso dos controls de brillo + Cor do volume da superposición do deslizamento + Define a cor da barra de progreso dos controls de volume + Cor da velocidade da superposición de deslize + Define a cor da barra de progreso dos controls de velocidade de reproducición + Tamaño do texto da superposición do deslizamento + Axusta o tamaño do texto na pantalla (1-30) + Limiar de magnitude do deslizamento + Define a distancia de deslizamento necesaria antes de que se active un axuste + Sensibilidade do deslize de volume + Os valores máis altos requiren máis deslize por cada paso de volume + Pasos de volume + Aumento de volume + Deslizar cara arriba pasando o volume máximo auméntao, o que pode distorsionar o audio alto + Predefinido do dispositivo + Sensibilidade do deslize do brillo + Os valores máis altos requiren máis deslizes por cada paso de brillo + Sensibilidade do deslize da velocidade + Os valores máis altos requiren máis deslizes por cada paso de velocidade + Intervalo de velocidade + Altura da zona de velocidade + Altura da zona do xesto de velocidade como porcentaxe da altura do reproductor (5-75) + Velocidade + Estilo da superposición do deslizamento + Superposición horizontal + Superposición horizontal (minimalista - arriba) + Superposición horizontal (minimalista - centro) + Superposición circular + Superposición circular (minimalista) + Superposición vertical + Superposición vertical (minimalista) + Activar o deslizamento para cambiar vídeos + Desliza no modo de pantalla completa para saltar ao seguinte vídeo ou ao anterior + Botóns de acción + Elixe que botóns de acción aparecen nos modos retrato e pantalla completa + Botóns en modo retrato + Desactivar o brillo de \"Gusta\" e \"Subscribirse\" + Impede que os botóns de Gústame e Suscribirse brillen cando un creador os menciona + Ocultar a barra de accións + Agochar o botón de Gústame e Non me gusta + Ocultar botón Compartir + Ocultar botón \"Detener anuncios\" + Ocultar botón Comentarios + Ocultar o botón Conectar + Ocultar botón Denunciar + Ocultar botón Remix + Ocultar o botón Descargar + Ocultar o botón Hype + Ocultar o botón Promocionar + Ocultar o botón Grazas + Ocultar o botón Preguntar + Ocultar canle e botón de Subscribirse + Oculta o avatar da canle e o botón de Subscribirse ao inicio da barra de accións + Ocultar botón de Suscribirse + Oculta os botóns de Suscribirse e Unirse ao lado do avatar do canal ao inicio da barra de accións + Ocultar o botón Clip + Isto pode non funcionar para algunhas contas de usuario + Ocultar o botón Loja + Ocultar o botón Gardar + Ocultar o botón Máis + Botóns en pantalla completa + Ocultar o botón Non me gusta + Ocultar o botón Gústame + Agochar o botón «Chat en directo» + Agochar o botón Mix + Agochar o botón «Máis vídeos» + Agochar o botón de Lista de reprodución + Marxe superior das accións rápidas + Establece a marxe superior da barra de accións rápidas (0-32 dp) + Subtítulos + Xestión dos subtítulos automáticos e das cookies de subtítulos + Visibilidade das subtitulacións automáticas + Amosar sempre + Non amosar nunca + Amosar cando non estea silenciado + Amosar cando estea silenciado + Lingua de subtitulado preferida + Selecciona primeiro os subtítulos do provedor e logo tradúceos automaticamente á lingua preferida Off + Engadir linguas de autotradución adicionais + Engade máis linguas ao menú de autotradución + Establecer cookies de subtítulos + "Corrixe un erro de YouTube onde as subtitulacións de tradución automática poden non estar dispoñibles debido á falta de cookies + +Deben proporcionarse as 'cookies de subtitulacións'" + Cookies de subtítulos + Cookies que se deben establecer nas solicitudes da API de Texto Temporizado (API de Datos de Subtítulos) + Obter cookies de subtítulos + Fai clic aquí para obter as cookies + Fallou a obtención dos cookies + As cookies xa existen + As cookies foron actualizadas correctamente + Corrixir transcrición + Resolve un fallo na comprobación de precondicións que pode impedir a carga da transcrición + Mostrar subtítulos en picture-in-picture + Botóns de navegación + Escolla que botóns aparecen na barra de navegación inferior + Ocultar botón Inicio + Ocultar botón Shorts Ocultar botón Crear + Ocultar botón Suscricións + Ocultar botón de notificacións + Mostrar botón de busca + Posición do botón de busca + Mostrar botón de Axustes + Posición do botón de Axustes + Acción do botón de Axustes + Abre os axustes de Morphe cando está activado, ou os axustes de YouTube cando está desactivado + Trocar o botón Crear polo botón de Notificacións + Se cambiar esta configuración non surte efecto, tente cambiar ao modo Incógnito + Ocultar etiquetas dos botóns de navegación + Ocultar punto de novo contido Ocultar barra de navegación + "Isto oculta completamente a barra de navegación, incluíndo a pestana 'Ti'. + +Para acceder aos Axustes, activa o botón de Axustes a través de Xeral → Barra de ferramentas → Mostrar axustes" + Activar os botóns de navegación estreitos + Reduce o espaciado horizontal entre os botóns + Desactivar botóns de navegación sen etiquetas + Mostra os botóns de navegación con etiquetas, se YouTube os envía sen etiquetas + Desactivar as pestanas de navegación da barra de ferramentas + Mostra as Suscricións na barra de navegación, se YouTube as move a unha pestana na parte superior de Inicio + Activar animacións da barra de navegación + Anima as transicións ao cambiar entre pestanas + Desactivar a ocultación automática da barra de navegación + Mantén a barra de navegación visible ao desplazar + Estilo da barra de navegación Predefinido + Translúcido + Transparente + Opaco + É posible que este estilo non sexa compatible co seu dispositivo ou coa versión de YouTube, e pode causar problemas deそこに disposición ou fallos + Barra de ferramentas + Elixe que compoñentes aparecen na barra de ferramentas + Ocultar botón de emisión Ocultar botón Chat + Ocultar botón do micrófono + Ocultar botón de busca + Cando está activado, un toque abre os axustes de Morphe e unha presión longa abre os axustes de YouTube; desactívaa para trocalos + Menú desplegable + Elixe que compoñentes aparecen no menú desplegable + Ocultar o menú \'Ajustes adicionais\' / Máis + Ocultar o menú \'Modo ambiente\' + "O menú 'Audio Track' está oculto + +Para amosar o menú 'Audio Track', cambie 'Spoof video streams' por calquera cliente que non sexa 'Android Studio' ou 'Android VR Downgraded'" + Ocultar o menú \"Pista de audio\" + Ocultar o pé de páxina do menú \"Pista de audio\" + Ocultar o menú de Subtítulos + Ocultar o pé de páxina do menú de Subtítulos + Ocultar a cabeceira do menú de Subtítulos + Ocultar o menú \"Axuda e comentarios\" + Ocultar menú \'Escoitar con YouTube Music\' + Ocultar menú \'Bloquear pantalla\' + Ocultar o menú \'Bucle de vídeo\' + Agochar o menú \'En movemento\' + Ocultar o menú de Calidade + Ocultar o pé de páxina do menú de Calidade + Ocultar a cabeceira do menú de Calidade + Ocultar o menú \'Temporizador de apagado\' + Ocultar menú \'Velocidade de reproducición\' + Ocultar o menú \'Volume estable\' + Ocultar menú \'Ver en RV\' + Agochar a vista previa de reprodución automática + Oculta a vista previa do seguinte vídeo en pantalla completa antes de que comece a reprodución automática + Botóns de superposición + Elixe que botóns aparecen sobre o reproductor de vídeo + Ocultar o botón de Reprodución automática + Ocultar o botón de Subtítulos + Ocultar botón de emisión + Ocultar botón Recoller + Ocultar botón de pantalla completa + Ocultar o fondo dos controls do reproductor + Oculta o sombreado escuro que rodea os controlos do reprodutor de vídeo + Opacidade do fondo dos botóns de control + Axusta a opacidade do fondo detrás dos botóns de control do reproductor e da barra de tempo (0 é invisible, 100 é sólido) + Ocultar os botóns Anterior & Seguinte + Ocultar o botón de Configuración + Ocultar as tarxetas da pantalla final + Modo ambiente + Xestionar a visibilidade do modo Ambiente e as restricións de enerxía + Omitir as restricións do modo ambiente + Permite que o modo Ambiente permaneza activo no modo de aforro de batería + Desactivar o modo ambiente + Desactivar o modo ambiente en pantalla completa + Agochar tarxetas de información + Desactivar animacións de números deslizantes + Detén a animación de contaba nos likes e nas visualizacións + Agochar a barra de busca do reproductor de vídeo + Agochar as miniaturas da barra de busca do vídeo + Oculta a barra de progreso de visualización que aparece nas miniaturas dos vídeos que xa viste + Activar a barra de busca grande en pantalla completa + Ocultar Shorts + Ocultar na páxina do canle + Ocultar no feed de Inicio e vídeos relacionados + Ocultar nos resultados da busca + Ocultar no feed de Suscricións + Ocultar na descrición do vídeo + Ocultar no historial de visualización + Reproductor de Shorts + Escolle que compoñentes aparecen no reproductor de Shorts + Desactivar o dobre toque para dar Like + Ocultar o botón de IA + Ocultar a etiqueta \"Doblado automaticamente\" + Agochar a barra de canles + Ocultar botón Comentarios + Ocultar botón de efecto + Agochar a etiqueta do enlace do vídeo + Ocultar as suxestións dos xestos + Ocultar o botón \"Pantalla verde\" + Ocultar botón de hashtag + Ocultar panel de información + Ocultar o botón Unirse + Ocultar botón Gústame + Ocultar animación de fonte do botón de Like + Ocultar a vista previa en directo + Ocultar a etiqueta de localización + Ocultar barra de navegación + Ocultar o botón \"Novas publicacións\" + Ocultar os botóns da superposición de pausa + Ocultar a vista previa do comentario + Ocultar botón Remix + Ocultar botón Gardar + Agochar o botón \"Gardar música\" + Ocultar as suxestións de busca + Ocultar botón Compartir + Ocultar botón de tenda + Ocultar botón Son + Agochar a etiqueta de metadatos do son + Ocultar botón de subscrición + Ocultar o botón \"Comprar Super Thanks\" + Ocultar produtos etiquetados + Ocultar botón de Próximamente + Agochar o botón \"Usar este son\" + Agochar o botón \"Usar esta plantilla\" + Agochar o título do vídeo + Agochar o vídeo suxido da pantalla final + "Oculta a superposición do vídeo suxerido que aparece cando remata o vídeo + +Esta configuración só funciona se a reprodución automática está desactivada. Pode cambialo en YouTube: +Configuración → Reprodución (ou Reprodución automática) → Reprodución automática do seguinte vídeo" + Ocultar vídeos relacionados + Ocultar a superposición de \'Máis vídeos\' + Oculta a superposición de \'Máis vídeos\' que aparece ao deslizar cara arriba no modo de pantalla completa + Agochar a marca temporal do vídeo + Desactivar os paneles emerxentes do reproductor + Desactiva a apertura automática dos páneles de lista de reprodución e chat en directo + Desactivar reprodución automática de lista de reprodución + Impede que as listas de reprodución avancen automaticamente ao seguinte vídeo + Desactivar o aviso de \"Continuar vendo\" + Saír do modo de pantalla completa ao rematar o vídeo + Desactivado + Só retrato + Só paisaxe + Retrato e paisaxe + Forzar modo horizontal en pantalla completa + Xirar o reproductor a modo horizontal ao entrar en pantalla completa. Non dispoñible en pantallas pequenas, que xa abren a pantalla completa en modo horizontal + Xestos de pantalla completa + Xestos do reprodutor en modo pantalla completa + Desactivar o xesto de deslizar cara arriba en vertical + Desactivar o xesto de deslizar cara abaixo en vertical + Desactivar o xesto de deslizar cara abaixo en horizontal + Desactivar xesto de pinzado para zoom + Restaurar o xesto de pinzar para ampliar + Abrir vídeos en modo de pantalla completa Desactivado + Vertical + Horizontal + Escala de vídeo a pantalla completa Predefinido + Estirar (encher pantalla) + Zoom (recortar para encher) + Mostrar botón de escala de vídeo + Toca para alternar entre Predesignado, Estirar e Zoom + Mostrar nurga pantalla completa + Ocultar o botón de escala do vídeo no reprodutor vertical + Amosar botón de silencio + Opacidade da superposición do reproductor + Axusta o nivel de transparencia (0 é transparente, 100 é opaco) + Barra de busca Desactivado + Ancha + Extra ancha + Disposición do dispositivo + Predefinido + Teléfono + Tablet + Automobilístico + "Os cambios inclúen: + + precludes layout de tableta +• As publicacións da comunidade están ocultas +• A configuración de reprodución nos feeds non está dispoñible +• O botón Remix e o botón de Son non están dispoñibles en Shorts +• A superposición de 'Máis vídeos' pode non estar dispoñible +• Móstrase a barra de acción de vídeo antiga non colapsada +• Os elementos do menú des same poden non funcionar correctamente + +Layout automotriz +• O feed está organizado por temas e canles +• A configuración de reprodución nos feeds non está dispoñible +• O botón Remix e o botón de Son non están dispoñibles en Shorts +• A superposición de 'Máis vídeos' pode non estar dispoñible +• Os Shorts ábrense no reprodutor habitual +• Móstrase a barra de acción de vídeo antiga non colapsada" + Activar o deseño de tableta no reprodutor + "Restaura o deseño antigo do reprodutor + +Limitación: Se 'Restaura a barra de acción de vídeo antiga' está desactivada, pode que a superposición de 'Máis vídeos' non estea dispoñible" + 21.11.48 - Restaurar o comportamento antigo das subtitulacións + 20.39.41 - Restaurar o antigo gardar na lista de reproducición + 20.28.41 - Desactivar iconas en negrita + 20.13.41 - Restaurar o nome do canle baixo o reprodutor de vídeo + Abrir a folla de compartido do sistema + Abre a folla de condivisión do sistema en lugar da da aplicación ao compartir contido + Anular os botóns de YouTube Music + Abre a túa aplicación de música preferida en lugar da aplicación predefinida de YouTube Music + Nome do paquete da aplicación de música + Nome do paquete da súa aplicación de música instalada + Páxina de inicio + Predefinido + Todas as subscripcións + Explorar canles + Cursos / Aprendizaxe + Explorar + Moda & Beleza + Xogos + Historial + Biblioteca + Vídeos que gustan + En directo + Películas + Música + Noticias + Notificacións + Listas de reprodución + Busca + Compras + Shorts + Deportes + Subscripcións + Tendencias + Realidade Virtual + Ver máis tarde + Os teus clips + Desactivar a continuación ao iniciar + Abrir Shorts con + Reprodutor de Shorts + Reprodutor normal + Reprodutor normal a pantalla completa + Reprodución automática de Shorts + Avanza ao seguinte Short automaticamente en lugar de repetir o actual + Autoreprodución en segundo plano + Avanza ao seguinte Short automaticamente durante a reprodución en segundo plano + Saltar Shorts en directo + Salta as transmisións en directo que aparecen no feed de Shorts + Reprodución nos feeds + Sempre activo + Só Wi-Fi Off + Restaurar os títulos orixinais + Amosa os títulos orixinais do vídeo, as descricións do vídeo e as descricións do canal en lugar das auto-traducidas + Cargando título… + Desactivar a actualización automática do feed + Impide que os feeds se actualicen automaticamente cando queden obsoletos + Layout da tarxeta do feed + Clásico + Tarxetas + Tarxetas con botóns de gustar, comentar e gardar + Comentarios + Desactivar a reanudación do miniplayer + Desactiva o miniplayer de \'Continuar vendo\', unha función de YouTube Premium que aparece ao iniciar a aplicación + Mini-reprodutor + Personaliza a aparencia e o comportamento do reproductor de vídeo minimizado + Tipo de miniplayer + Ocultar título e canle + Desactivado + Predefinido + Minimalista + Minimalista 2 + Tablet + Moderno 1 + Moderno 2 + Moderno 3 + Moderno 4 + Desactivar cantos arredondados + Desactivar arrastrar e soltar + Impede arrastrar o reproductor minimizado a diferentes esquinas da pantalla + Desactivar o xesto de arrastre horizontal + Impede pechar o miniplayer deslizando horizontalmente fóra da pantalla + Pausar a reprodución con arrastre horizontal + A reprodución detense cando se arrastra o miniplayer fóra da pantalla + Lembrar a posición do estado de arrastre + Arrastrar o reproductor minimizado fóra da pantalla conservará o seu estado de UI + Ocultar os botóns de superposición + Tamaño predefinido + Define o tamaño inicial na pantalla, en píxeles + O tamaño do píxel debe estar entre %1$s e %2$s + Opacidade da superposición + Axausta o nivel de transparencia (0 é transparente, 100 é opaco) + O botón Atrás sae do feed + Ao premer atrás sempre se sae da aplicación en vez de desplazar ata a parte superior do feed + Forzar animacións Predefinido + Sempre activado + Sempre desactivado + Ocultar barra de estado + Oculta a barra de estado do sistema. Desliza cara baixo desde a borda superior para amosala por un momento + Corrixir que a barra de estado cubra as barras superiores + Mantén a disposición da barra de estado consistente, para que non cubra as barras superiores e non deixe espazo vacío sobre o reprodutor. Desactiva isto se a aplicación falla ou as barras superiores se ven mal + Brillo + Este dispositivo non admite a pantalla de inicio Glow + Fondo do tema claro + Branco + Material You (Branco) + Rosa claro + Azul claro + Verde claro + Amarelo claro + Laranja claro + Vermello claro + Fondo claro personalizado + Define a cor de fondo do tema claro + Cambiar a cor do primeiro plano + Use a cor do tema para o texto e os icones + Activar a pantalla de carga con degradado + Amosa un fondo degradado na pantalla de carga en lugar dunha cor sólida + Activar a cor personalizada da barra de busca + Cor da barra de busca + Establece a cor primaria da barra de progreso do vídeo + Cor de acento da barra de busca + Establece a cor secundaria da barra de progreso do vídeo + Valor da cor da barra de busca non válido + Analóxico orixinal + Bolo orixinal + Metal orixinal + Logotipo da cabeceira + Predefinido + Regular Personalizado + Omitir as restricións de rexión de imaxe + Redirixe as solicitudes de miniaturas a un servidor alternativo para solucionar a falta de imaxes en países con restriccións + Títulos de DeArrow + Pestana Inicio + Pestana Subscripcións + Pestana Ti + Listas de reprodución & recomendacións do reprodutor + Resultados da busca + Amosar o icon de DeArrow + Mostra o icon do DeArrow nos títulos de vídeos procedentes de crowdsourcing + Modo casual + Mostra o título orixinal en lugar do título de DeArrow se os usuarios de DeArrow votaron que lles gusta o título orixinal + Votos casuais mínimos para mostrar o título orixinal + O número de votos casuais nunha única categoría, como engraçada ou creativa, necesarios para mostrar o título orixinal + Miniaturas de DeArrow + Miniaturas orixinais + Miniaturas de DeArrow & orixinais + DeArrow & capturas estáticas + Capturas estáticas + "DeArrow substitúe os títulos e miniaturas clickbait por outros procedentes de crowdsourcing que son máis precisos e menos sensacionalistas. Os títulos son escritos e votados pola comunidade, e as miniaturas son fotogramas extraídos do vídeo + +Os vídeos solicítanse mediante un prefixo curto do ID do vídeo hasheado, polo que o servidor non pode saber exactamente que vídeo estás a ver. Se un vídeo non ten unha miniatura de DeArrow, móstrase a orixinal ou unha captura estática" + Mostrar alertas de erro de conexión + Amosa unha notificación cando o servidor de DeArrow non está dispoñible + Endpoint da API + A URL do endpoint da caché de miniaturas de DeArrow + Capturas de vídeo estáticas + As capturas fixas tómase do principio / medio / final de cada vídeo. Estas imaxes están integradas en YouTube e non se utiliza ningunha API externa + Extraer fotograma de vídeo de + Inicio + Medio + Fin + DeArrow non está dispoñible temporalmente (código de estado: %s) + DeArrow non está dispoñible temporalmente + Restaurar o estilo antigo dos botóns do reprodutor + Reverte os controlos en negrita do reprodutor de vídeo ao estilo visual antigo e máis fino + Simular as dimensións do dispositivo + Activar isto pode desbloquear calidades de vídeo máis altas + Activar isto pode causar saltos na reprodución do vídeo, unha peor duración da batería e efectos secundarios descoñecidos + Resposta háptica + Elixe que interaccións activen a vibración do dispositivo + Desactivar a haptica dos capítulos + Desactivar a haptica da busca precisa + Desactivar a resposta háptica de anular busca + Desactivar a háptica de tocar e manter + Desactivar a háptica do zoom + Se cambiou recentemente os datos de acceso da súa conta, desinstale e reinstale MicroG + Omitir redireccións de ligazóns + Abre a ligazón orixinal directamente, omitindo a ligazón de seguimento (youtube.com/redirect) + Abrir ligazóns no navegador + Abre os ligazóns nun navegador externo en lugar do navegador interno da aplicación + Tamaño do buffer de reprodución Predefinido + Baixo + Medio + Máximo + Retraso de vídeo + Retrasa o vídeo para mantelo sincronizado con audio con lag, como auriculares Bluetooth (0-5000 ms) + Retrasar só con audio Bluetooth + Só atrasa o vídeo mentres o audio Bluetooth está conectado, para que o altofalante do teléfono manteña a sincronización + Mostrar botón de retraso de vídeo + Mantén premido para restablecer o retraso a 0 ms + Mostrar menú de retraso de vídeo + Retraso en milisegundos + Automático + Lembrar calidade do vídeo + Aplica a túa selección manual de calidade a todos os vídeos futuros + Notificacións de cambio de calidade + Mostra unha notificación cando se garda unha nova calidade predefinida + Calidade de vídeo predefinida en rede Wi-Fi + Calidade de vídeo predefinida en rede móbil + Lembrar a calidade dos Shorts + Aplica a túa selección manual de calidade a todos os Shorts futuros + Calidade de Shorts predefinida en rede Wi-Fi + Calidade predefinida de Shorts na rede móbil + Cambiouse a calidade móbil predefinida a: %s + Cambiouse a calidade Wi-Fi predefinida a: %s + Cambiouse a calidade móbil de Shorts a: %s + Cambiouse a calidade Wi-Fi de Shorts a: %s + Mostrar botón de velocidade de reprodución + Mantén premido para restablecer a velocidade por defecto + Mostrar botón de Calidade + Mantén premido para restablecer a calidade por defecto + Amosar resolución no botón de Calidade + Amosa a resolución, como 1080, en lugar dunha etiqueta como FHD + Menú de velocidade de reprodución personalizada + Restaurar o menú de velocidade de reprodución antigo + Velocidades de reprodución personalizadas + Introduce a túa opción de velocidade de reprodución preferida + As velocidades de reprodución personalizadas deben ser menores que %s + Velocidades de reprodución personalizadas non válidas + Automático + Mantén premida a velocidade de reprodución + Establece o multiplicador de velocidade ao manter premida a pantalla, establece en 0 para desactivar + Lembrar a velocidade de reprodución + Aplica a túa selección manual de velocidade a todos os vídeos futuros + Notificacións de cambio de velocidade + Mostra unha notificación cando se garda unha nova velocidade de reproducición predefinida + Velocidade de reprodución predefinida + Velocidade de reprodución predeterminada nos Shorts + Lembrar a velocidade de reprodución nos Shorts + Aplica a túa selección manual de velocidade de Shorts a todos os Shorts futuros. Se está desactivado, a velocidade volve á predeterminada despois de pechar o reprodutor de Shorts + Amosar a velocidade de reprodución no menú de Shorts + Engade un elemento de velocidade de reprodución ao menú do reprodutor de Shorts + Velocidade de reprodución · %s + Cambiouse a velocidade de reprodución predefinida a: %s + Usar velocidade normal para a música + Reproducir sempre os vídeos musicais a velocidade 1.0x + Lista branca de velocidade do canal + Os videos dos canais na lista branca reprodúcense sempre a velocidade 1.0x + Engadir canal á lista branca de velocidade + Eliminar canal da lista branca de velocidade + Activar os controls de ton de audio + Tono de audio de reprodución predefinido + Cambiouse o ton de audio predefinido a: %s + Estiramento temporal de audio + Permite axustar a velocidade e o ton de forma independente, pode causar artefactos de audio + Lembrar posición de reproducición de transmisión en directo + Para transmisións en directo en curso, lembra e retoma a túa posición de reproducición en lugar de ir á posición actual en directo + Esta función non funciona con transmisións en directo 24/7 e outras transmisións que estean \'sempre activas\' + Desactivar vídeo HDR + Forzar vídeo HDR + Forza a aplicación a recoñecer este dispositivo como compatible con HDR + "Use isto se a decodificación HDR funciona pero YouTube non ofrece HDR, como ocorre en algúns dispositivos HyperOS que ocultan HLG. + +Limitacións: +• As cores poden parecer incorrectas se a pantalla non pode amosar HDR +• A reprodución pode fallar se o decodificador non pode xestorar HDR +• O HDR aínda depende do cliente que emula os fluxos de vídeo" + Códec de vídeo Predefinido + Para forzar AVC, cambia \'Simular fluxos de vídeo\' a \'Android Studio\', \'Android VR\' ou \'visionOS\' + "VP9 +• Pode mellorar a duración da batería e solucionar os saltos na reprodución +• Usa máis datos de internet que AV1 + +AVC (H.264) +• Require o cliente de simulación de fluxos de vídeo 'Android Studio', 'Android VR' ou 'visionOS' +• Pode mellorar aínda máis a duración da batería +• Pode restaurar resolucións de vídeo que faltan en dispositivos máis antigos +• A resolución máxima é 1080p +• Os vídeos HDR poden non usar AVC +• Alguns dispositivos non poden forzar AVC" + Abrir menú de calidade avanzada + Salta o menú de selección de calidade e abre directamente o menú de calidade avanzada + Ocultar opcións de calidade Premium + Priorizar a calidade do vídeo + Prioriza a calidade de vídeo máis alta dispoñible sobre a eficiencia do códec + Se esta configuración está desactivada, os vídeos máis antigos poden non mostrar opcións de calidade de vídeo + Permitir \'Android VR\' / visionOS AV1 + Permite a decodificación AV1 por software ao usar o cliente \'Android VR\' / visionOS + "Activar esta configuración pode utilizar a decodificación de software AV1. + +A reprodución de vídeo con AV1 pode ter tirones ou perder fotogramas" + Efectos secundarios do spoofing + • A reprodución pode fallar ao minuto 1 + • Falta o menú \'Pista de audio\' + • Sen codec de vídeo AV1 + • O modo inmersivo VR 360° non está dispoñible + • O volume estable non está dispoñible + • A reprodución pode rematar 1 segundo antes + • Se a desobfuscación falla, a reprodución pode fallar + • Forzar o audio orixinal non está dispoñible + • É necesario iniciar sesión en Android VR + • A calidade pode estar limitada a 720p + Variante JavaScript do reprodutor + Amosar a emulación en \'Estatísticas para nerds\' + Restaurar a barra de acción de vídeo antiga + "Corrixe que os botóns de acción do vídeo non se poden ocultar + +Limitación: Reverte ao estilo antigo da superposición de 'Máis vídeos'" + Estilo do icon do reproductor Automático Predefinido + Fino + Negrita Personalizado + Estilo do icon de Shorts + Seguidor de series + Segue as listas de reprodución como series e mantén o teu progreso + Activar o seguimento de series + Engade unha páxina de Series ao Historial e unha opción de Seguir serie ás listas de reprodución + Mostrar Series na barra de navegación + Amosar botón do Seguimento de Series + Engade un botón ao reprodutor para seguir ou abrir a serie do vídeo actual + Rexistrar o progreso das series seguidas + Rexistrar a reprodución neste teléfono para as series gardadas e os marcadores Cancelar Gardar + Gardar marcador + Nome da serie + Opcións da serie + Opcións da biblioteca + Actualizar episodios + Renomear + Abrir a lista de reprodución en YouTube + Eliminar da biblioteca + Ligazón ou ID da lista de reprodución + Limpar todo o progreso local + Limpar historial + Limpar todo o progreso local? + Isto limpa as posicións, as anulacións de vídeos vistos e as seleccións de continuación de cada vídeo. As series gardadas e os seus catálogos permanecen. Se a grabación está activada, rexistrarseá unha nova reprodución + Marcar como visto + Marcar como non visto + Usar estado automático + Reiniciar desde 0:00 + Reprodur de novo desde 0:00 + Estado gardado + Reintentar gardar + Anteriores 100 + Próximos 100 + Abrir vídeo gardado + Progreso local borrado + Abre o Historial desde You para ver as túas series + Historial de visualización + Series + Cargando series gardadas… + Siga unha lista de reprodución para facer seguimento dos seus episodios + Marcador de lista de reprodución + Actualizando episodios… + Todas as series + Visto + Cargando episodios… + Máis opcións + "Rexistrar o progreso nuras series gardadas e vídeos marcados. + +Esta biblioteca almacénase neste dispositivo e compártese entre as contas de YouTube. Os controls de pausa e limpeza de historial de YouTube non a modifican. Use Series → Máis → Limpar progreso local para borrala. + +A grabación automática detense no modo incógnito, cando a identidade da conta non está dispoñible ou cando cambia a outra conta. Active o seguimento novamente para utilizalo con esa conta" + Non se pode activar o seguimento no modo incógnito ou antes de que YouTube cargue o estado da conta actual. Regresa ao Inicio e inténtao de novo. Non se require iniciar sesión + Activar seguimento local + O rexistro do progreso está desactivado + Retomar en %1$s + %1$s / %2$s episodios vistos + Episodio %1$s · %2$s + Episodios %1$s-%2$s de %3$s + Actualiza para intentar cargar os episodios. As listas de reprodución privadas requiren acceso á conta; o seu marcador aínda abre en YouTube + YouTube non puido abrir este episodio + YouTube non puido abrir esta lista de reprodución + Escolle un episodio Ao día + Non hai episodios reproducibles + Video gardado · %1$s + Seguir serie + Ver serie Continuar + Comezar aquí + Punto de inicio gardado + Visto ata aquí + Episodios marcados como vistos Anular + Activar + 1 episodio novo + Rexistrar o progreso neste dispositivo + "As túas series e o teu progreso permanecen neste dispositivo, compartíndose entre contas de YouTube. Os controls do historial de YouTube non os cambian. Borra o progreso desde o menú de Series. + +A gravación automática é opcional. Detense en modo incógnito ou cando cambie a conta" + Non se puideron cargar os episodios. Podes gardar a lista de reprodución como un marcador + Hai demasiados cambios non gardados. Tenta gardalos primeiro + O gardado está ocupado. Tenta de novo + A carga de episodios está ocupada. Tenta de novo + O almacenamento non está dispoñible. Os teus datos gardados foron conservados + Non se puideron gardar os cambios. Tenta gardalos antes de pechar YouTube + Esta serie xa está gardada, ou a súa lista de reprodución cambiou. Ábrea de novo + Esta serie cambiou ou foi eliminada. Reabre a biblioteca + A lista de reprodución cambiou. Selecciona o episodio de novo + Este episodio non está dispoñible. Escolle outro + Introduce un nome para a serie + Introduce un enlace ou un ID de lista de reprodución de YouTube + Agarda un pouco antes de volver actualizar + Non se puideron cargar os episodios. Téntao de novo máis tarde + Non se puido completar o cambio. Téntao de novo + Non se puido confirmar a reprodución. Comproba o reprodutor e tenta de novo + Series que sigo + Este vídeo non está nesta lista de reprodución + Buscando listas de reprodución… + Pegar enlace da lista de reprodución + Non se puido atopar ningunha lista de reprodución + Non hai episodios dispoñibles + Ocultar vistos + Marcar episodios anteriores como vistos + Comeza con: %1$s + Orde dos episodios + Orde da lista de reprodución + Inverter a orde da lista de reprodución + %1$s visualizacións + %1$s visualización + Marcar episodios como vistos + polo menos un %1$s%% visto con como máximo 90 segundos restantes, ou %2$s segundos restantes + Marcar automaticamente un episodio como visto despois de calquera dos dous límites. A regra do porcentaxe tamén require que queden no máximo 90 segundos, para que non se salte o final dos episodios longos. A regra do tempo restante aplícase a vídeos de máis dun minuto; establecéa en 0 para desativala. Os cambios aplícanse ás futuras actualizacións do progreso. Mantensemos as escollas manuais de visto/non visto + Porcentaxe vista (1-100) + Segundos restantes (0-300; 0 desactiva) + Introduza un valor entre %1$s e %2$s + Usar o progreso de YouTube + Retoma as series seguidas despois de velas noutro dispositivo + Ler o estado de visto e as posicións de reanudación das túas series seguidas dende a conta actual de YouTube. Só se gardan neste teléfono os episodios correspondentes. As comprobacións execútanse mentres a biblioteca de series está aberta e antes de Continuar. O historial de YouTube debe estar activado no outro dispositivo teu + Comprobando o progreso de YouTube… + O progreso de YouTube non está dispoñible. Usando o progreso gardado + Parte do progreso de YouTube non está dispoñible. Mantense as posicións gardadas + Copia de seguridade e restauración + Garda a túa biblioteca de series nun ficheiro ou restaura unha copia de seguridade + Garda as series seguidas, as posicións dos episodios e as eleccións de vistos. Non se inclúen os permisos de gravação e de progreso de YouTube + Gardar copia de seguridade + Restaurar copia de seguridade + Engadir %1$s series. Mantense as %2$s series que xa están na túa biblioteca. Mantense o progreso dos vídeos existentes + Copia de seguridade de series gardada + Restauráronse %1$s series + Este ficheiro non é unha copia de seguridade de series compatibles, ou é demasiado grande. A túa biblioteca non foi modificada + Non foi posible gardar a copia de seguridade. Proba noutra localización + Serie eliminada + Este episodio agora aparece máis dunha vez. Elixe desde cal continuación + O episodio gardado xa non está nesta lista de reprodución. A túa posición gardada mantense + Continuar co episodio %1$s · %2$s + Tamén podes elixir calquera episodio a continuación. Os episodios anteriores non cambian + Esta lista de reprodución cambiou. Abre a serie para elixir onde continuar + Non hai máis episodios nesta dirección + Continuar %1$s + Buscar series + Limpar busca + Non se atoparon series + Episodio sen título + Episodio non dispoñible + Comentarios de Niconico + Amosar o chat en directo e a reprodución do chat como comentarios que se desprazan polo vídeo + Amosa un botón de superposición no reproductor de vídeo. Toca para amosar ou ocultar os comentarios, mantén premido para a configuración rápida + Tamaño da fonte do comentario + Tamaño do texto dos comentarios (12-40 sp) + Opacidade do comentario + Opacidade do texto e dos fondos dos comentarios + Duración do movemento + Cantidade de segundos que tardan os comentarios en atravesar a pantalla. Os valores máis baixos móvense máis rápido + Lonxitude máxima do comentario + Oculta os comentarios con máis caracteres que este. Cada emoji conta como un carácter + Sen límite diff --git a/patches/src/main/resources/addresources/values-gu-rIN/music/strings.xml b/patches/src/main/resources/addresources/values-gu-rIN/music/strings.xml index 0f9e386e8a..ccfea4ed71 100644 --- a/patches/src/main/resources/addresources/values-gu-rIN/music/strings.xml +++ b/patches/src/main/resources/addresources/values-gu-rIN/music/strings.xml @@ -20,7 +20,6 @@ Second \"item\" text" - ડિફૉલ્ટ @@ -48,8 +47,6 @@ Second \"item\" text" - ડિફૉલ્ટ - શોધો @@ -69,7 +66,6 @@ Second \"item\" text" - અજ્ઞાત diff --git a/patches/src/main/resources/addresources/values-gu-rIN/reddit/strings.xml b/patches/src/main/resources/addresources/values-gu-rIN/reddit/strings.xml index 6bc44878aa..cf7776fdaf 100644 --- a/patches/src/main/resources/addresources/values-gu-rIN/reddit/strings.xml +++ b/patches/src/main/resources/addresources/values-gu-rIN/reddit/strings.xml @@ -15,8 +15,4 @@ Second \"item\" text" --> - - ડિફૉલ્ટ - ડિફૉલ્ટ - અજ્ઞાત - + diff --git a/patches/src/main/resources/addresources/values-gu-rIN/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-gu-rIN/shared-youtube/strings.xml index c545f6036d..77635ba31c 100644 --- a/patches/src/main/resources/addresources/values-gu-rIN/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-gu-rIN/shared-youtube/strings.xml @@ -18,11 +18,11 @@ Second \"item\" text" --> + - ડિફૉલ્ટ @@ -56,9 +56,7 @@ Second \"item\" text" - ડિફૉલ્ટ - ડિફૉલ્ટ diff --git a/patches/src/main/resources/addresources/values-gu-rIN/shared/strings.xml b/patches/src/main/resources/addresources/values-gu-rIN/shared/strings.xml index 181898b82d..31cdbe4dcb 100644 --- a/patches/src/main/resources/addresources/values-gu-rIN/shared/strings.xml +++ b/patches/src/main/resources/addresources/values-gu-rIN/shared/strings.xml @@ -18,7 +18,6 @@ Second \"item\" text" - ડિફૉલ્ટ diff --git a/patches/src/main/resources/addresources/values-gu-rIN/youtube/strings.xml b/patches/src/main/resources/addresources/values-gu-rIN/youtube/strings.xml index 8e19bda11c..4ec76b1150 100644 --- a/patches/src/main/resources/addresources/values-gu-rIN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-gu-rIN/youtube/strings.xml @@ -31,6 +31,7 @@ Second \"item\" text" This button usually appears in the Subscriptions feed for future live streams or unreleased videos. --> + @@ -68,7 +69,6 @@ Second \"item\" text" - શોધો @@ -81,7 +81,6 @@ Second \"item\" text" - ડિફૉલ્ટ @@ -124,7 +123,6 @@ Second \"item\" text" - ડિફૉલ્ટ @@ -170,7 +168,6 @@ Second \"item\" text" - ડિફૉલ્ટ @@ -191,7 +188,6 @@ Second \"item\" text" - ડિફૉલ્ટ @@ -212,7 +208,6 @@ Second \"item\" text" - ડિફૉલ્ટ @@ -222,7 +217,6 @@ Second \"item\" text" - ડિફૉલ્ટ @@ -232,7 +226,6 @@ Second \"item\" text" - ડિફૉલ્ટ diff --git a/patches/src/main/resources/addresources/values-hi-rIN/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-hi-rIN/shared-youtube/strings.xml index 2bf11fc6ec..d3a39cd9a4 100644 --- a/patches/src/main/resources/addresources/values-hi-rIN/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-hi-rIN/shared-youtube/strings.xml @@ -21,9 +21,9 @@ Second \"item\" text" स्टार्टअप एनीमेशन बंद किया गया रंगीन - कलर Morphe एक रंग का - मोनोक्रोम Morphe + + बंद सेटिंग को पूरा करने के लिए रीस्टार्ट करें diff --git a/patches/src/main/resources/addresources/values-hi-rIN/youtube/strings.xml b/patches/src/main/resources/addresources/values-hi-rIN/youtube/strings.xml index 1f6b727c1a..86d4b758bf 100644 --- a/patches/src/main/resources/addresources/values-hi-rIN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-hi-rIN/youtube/strings.xml @@ -113,6 +113,9 @@ Second \"item\" text" सर्वेक्षण छुपाएं फ़ीड अनुशंसा सटीकता के बारे में सर्वेक्षण और नज़दीकी को छुपाता है छिपाएं टिकट शेल्फ + + \'अनुपलब्ध वीडियो छिपे हुए हैं\' बैनर छिपाएं + प्लेलिस्ट में \'अनुपलब्ध वीडियो छिपे हुए हैं\' बैनर को छिपाता है अपलोड समय छुपाएं "सीमाएँ: @@ -1249,6 +1252,7 @@ OpenRouter.ai खोलने के लिए यहां टैप करे स्टेटस बार की लेआउट को स्थिर रखता है, ताकि यह टॉप बार्स को न कवर करे और प्लेयर के ऊपर खाली जगह न छोड़े। अगर ऐप क्रैश हो जाए या टॉप बार्स गलत दिखें तो इसे बंद करें ग्लो + यह डिवाइस ग्लो स्प्लैश स्क्रीन का समर्थन नहीं करता है लाइट थीम की पृष्ठभूमि सफेद मटेरियल यू (सफेद) diff --git a/patches/src/main/resources/addresources/values-hr-rHR/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-hr-rHR/shared-youtube/strings.xml index 82dff48f8f..f0c16d25fc 100644 --- a/patches/src/main/resources/addresources/values-hr-rHR/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-hr-rHR/shared-youtube/strings.xml @@ -21,9 +21,9 @@ Second \"item\" text" Animacija pokretanja Onemogućeno Boja - Kolorna Morphe Monokromatski - Monokromatska Morphe + + Isključeno Ponovno pokrenite da biste dovršili postavljanje diff --git a/patches/src/main/resources/addresources/values-hr-rHR/youtube/strings.xml b/patches/src/main/resources/addresources/values-hr-rHR/youtube/strings.xml index 75989c399a..9f91f45c16 100644 --- a/patches/src/main/resources/addresources/values-hr-rHR/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-hr-rHR/youtube/strings.xml @@ -103,6 +103,9 @@ Second \"item\" text" Sakrij oglase Spremi ankete i poticaje o točnosti preporuka vijesti Sakrij \'Kanalna traka\' + + Sakrij natpis \"Nedostupni videozapisi su skriveni\" + Skriva natpis \"Nedostupni videozapisi su skriveni\" u popisima reprodukcije Sakrij broj pregleda "Ograničenja: @@ -1237,6 +1240,7 @@ Ograničenje: Ako je 'Vrati staru traku s radnjama za videozapise' isključeno, Održava dosljednost rasporeda statusne trake, tako da ne prekriva gornje trake i ne ostavlja prazan prostor iznad playera. Isključite ovo ako aplikacija zapne ili gornje trake izgledaju pogrešno Sjaj + Ovaj uređaj ne podržava Glow zaslon s početnim prikazom Boja pozadine u svijetloj temi Bijela Material You (Bijela) @@ -1548,7 +1552,6 @@ Ograničenje: Vraća se na stari stil 'More videos' prekrivača" Epizode obilježene kao gledane Poništi Omogući - 1 nova epizoda %1$s nove epizode Snimanje napretka na ovom uređaju "Vaše serije i napredak ostaju na ovom uređaju, dijeleći se između YouTube računa. Kontrola povijesti YouTuba ne mijenja te podatke. Očistite napredak iz izbornika Serije.\n\nAutomatsko snimanje je opcionalno. Zaustavlja se u incognito načinu rada ili kada se račun promijeni" diff --git a/patches/src/main/resources/addresources/values-hu-rHU/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-hu-rHU/shared-youtube/strings.xml index 80249e17e1..6c448ee61a 100644 --- a/patches/src/main/resources/addresources/values-hu-rHU/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-hu-rHU/shared-youtube/strings.xml @@ -21,9 +21,9 @@ Second \"item\" text" Indítási animáció Kikapcsolva Színes - Színes Morphe Monokróm - Monokróm Morphe + + Ki Indíts újra a beállítások befejezéséhez diff --git a/patches/src/main/resources/addresources/values-hu-rHU/youtube/strings.xml b/patches/src/main/resources/addresources/values-hu-rHU/youtube/strings.xml index 88e5465935..e2dd2b2a5a 100644 --- a/patches/src/main/resources/addresources/values-hu-rHU/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-hu-rHU/youtube/strings.xml @@ -113,6 +113,9 @@ A változások a következőket tartalmazzák: Felmérések elrejtése Értékelési felmérések és javaslatok elrejtése a hírcsatorna pontosságával kapcsolatban Jegyértékesítési polc elrejtése + + Elrejtés az „Az elérhetetlen videók rejtve vannak” sávő + Elrejti az „Az elérhetetlen videók rejtve vannak” sávot a lejátszási listákban Feltöltési idő elrejtése "Korlátozások: @@ -1249,6 +1252,9 @@ Korlátozás: Ha a „Régi videós eszköztár visszaállítása” ki van kapc A status sáv elrendezése konzisztens marad, így nem takarja le a felső sávokat, és nincs üres hely a lejátszó felett. Kapcsolja ki ezt, ha az alkalmazás összeomlik vagy a felső sávok rossz megjelenésűek Ragyogás + Morphe Glow 1 + Morphe Glow 2 + A Glow beugratási képernyőt ez az eszköz nem támogatja Világos téma háttér Fehér Material You (Fehér) diff --git a/patches/src/main/resources/addresources/values-hy-rAM/music/strings.xml b/patches/src/main/resources/addresources/values-hy-rAM/music/strings.xml index 37297b6f2c..f3065be6be 100644 --- a/patches/src/main/resources/addresources/values-hy-rAM/music/strings.xml +++ b/patches/src/main/resources/addresources/values-hy-rAM/music/strings.xml @@ -133,6 +133,7 @@ Second \"item\" text" "Ներբեռնումներ չկան Ներբեռնված թրեկները կհայտնվեն այստեղ" %1$s • %2$s • %3$s ՄԲ + %1$s տրեկ • %2$s ՄԲ Որոնել ներբեռնումների մեջ Տեսակավորել Տեսակավորել ըստ կատարողի @@ -532,6 +533,7 @@ SABR վերարտադրում (Spoof տեսանյութերի հոսքերը ա Տեղային տառադարձում և համապատասխանեցում Կատարել տառադարձում սարքի վրա, երբ մատակարարը, ձեր մոդելը և Google Translate-ը չեն աշխատում: Կարդոմությունը հաջորդում է ժամանակային տեքստի յուրաքանչյուր բառին. բառերի սինխրոնացումը պետք է միացված լինի: Չինարեն, կանտոներեն, ճապոներեն, կորեերեն, հունարեն, ռուսերեն, վրացերեն, հայերեն; մոտ 120 ՄԲ Կարդացվում է %1$s-ը՝ %2$d%% + Կարդոմները պատրաստ են (%1$d գրառում) Ներբեռնումը ձախողվեց. անջատեք և նորից միացրեք սա՝ կրկնելու համար Ցույց տալ Refresh կոճակը Հպեք՝ հաջորդ տեքստի տարբերակին անցնելու համար. սեղմեք երկար՝ ըստ երգի վերնագրի և կատարողի տեքստեր փնտրելու համար @@ -543,8 +545,6 @@ SABR վերարտադրում (Spoof տեսանյութերի հոսքերը ա Պահպանված է Ընդգծել սինխրոնացված բառերը կամ վանկերը Առաջնահերթություն տալ բառ առ բառ տեքստերին, երբ դրանք հասանելի են: Երբ տեքստերը ունեն բառերի ժամանակային տվյալներ, ընդգծվում է յուրաքանչյուր բառ կամ վանկ՝ համահունչ երաժշտությանը - Ցուցադրել հետնապլանային ձայները առանձին - Փակագծերով գրված հետնափոնային վոկալը, օրինակ՝ \"(Oh) I want you\", տեղափոխում է երգվող տողից դեպի իր սեփական ավելի մթնագույն տողը Թաքցնել հնչեցված տեքստերը Թաքցնել տեքստի տողերը, որոնք գտնվում են ընթացիկ տողից վերև Թաքցնել չհնչեցված տեքստերը diff --git a/patches/src/main/resources/addresources/values-hy-rAM/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-hy-rAM/shared-youtube/strings.xml index 2768210b7e..a71a712c36 100644 --- a/patches/src/main/resources/addresources/values-hy-rAM/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-hy-rAM/shared-youtube/strings.xml @@ -21,6 +21,8 @@ Second \"item\" text" Մեկնարկային անիմացիա Անջատված Գույն + Մոնոխրոմ + Վերագործարկեք կարգավորումներն ավարտելու համար @@ -47,6 +49,7 @@ Second \"item\" text" Ներբեռնումներ Ներբեռնման ռեժիմ + Լռում Ներբեռնում հավելվածի միջոցով Արտաքին ներբեռնող Ներբեռնողի փաթեթի անվանում @@ -61,6 +64,11 @@ Second \"item\" text" Փաթեթի անվանումը չի կարող լինել դատարկ Պատ춤ային + Հավելվածի սիմվոլ + Բնօրինակ + Morphe սև + Morphe մուգ + Morphe բաց Morphe play light @@ -133,6 +141,7 @@ Second \"item\" text" Android VR-ի մուտքի ժամանակը լիքն է, փորձեք նորից մուտք գործել VR OAuth2-ը ժամանակավորապես հասանելի չէ (կարգավիճակ %s) OAuth2-ը ժամանակավորապես հասանելի չէ + Չի հաջողվում մուտք գործել VR, կայքի նույնականացումը չի հաստատվել Հնարավոր չէ մուտք գործել VR. %s Ելք է կատարվել Android VR-ից VR մուտքի տոկենը անվավեր է, փորձեք կրկին մուտք գործել VR @@ -273,6 +282,7 @@ Second \"item\" text" Բացասական գնահատականները հասանելի չեն (%s) Թաքցված է սեփականատիրոջ կողմից + Չի դուր գալիս այս տեսանյութը, ինչպես նաև %s այլ մարդկանց Չհավանել այս տեսանյութը, ձայների %s-ը չհավանություններ են Միացնել Return YouTube Dislike-ը Ցուցադրել անհավանմանները տոկոսով diff --git a/patches/src/main/resources/addresources/values-hy-rAM/sponsorblock/strings.xml b/patches/src/main/resources/addresources/values-hy-rAM/sponsorblock/strings.xml index 133986071b..89c2bc62ea 100644 --- a/patches/src/main/resources/addresources/values-hy-rAM/sponsorblock/strings.xml +++ b/patches/src/main/resources/addresources/values-hy-rAM/sponsorblock/strings.xml @@ -217,10 +217,15 @@ Second \"item\" text" Դուք ստեղծել եք <b>%s</b> սեգմենտներ Հպեք այստեղ՝ ձեր հատվածները տեսնելու համար SponsorBlock-ի առաջատարների ցուցակը + Դուք փրկել եք մարդկանց <b>%s</b> հատվածներից Հպեք այստեղ՝ գլոբալ վիճակագրությունը և լավագույն ներդ contribu-տորները տեսնելու համար Դա նրանց կյանքի <b>%s</b>-ն է։<br>Հպեք այստեղ՝ առաջատարների ցուցակը տեսնելու համար + Դուք բաց եք թողել <b>%s</b> հատվածներ Դա <b>%s</b> է Վերակայել բաց թողնված հատվածների հաշվիչը՞ + %1$s ժամ %2$s րոպե + %1$s րոպե %2$s վայրկյան + %s վայրկյան Թափանցիկություն՝ Գույն՝ "SponsorBlock-ը տեսանյութերի հատվածների հանրային տվյալների բազա է, ինչպիսիք են հովናരերը, ներածությունները, եզրափակությունները և բաժանորդագրվելու հիշեցումները: Հատվածները ներկայացվում և քվեարկվում են համայնքի կողմից, իսկ տվյալների բազան բաց է և անվճար ներբեռնման համար diff --git a/patches/src/main/resources/addresources/values-hy-rAM/youtube/strings.xml b/patches/src/main/resources/addresources/values-hy-rAM/youtube/strings.xml index d63dd8a3ca..98cbf834da 100644 --- a/patches/src/main/resources/addresources/values-hy-rAM/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-hy-rAM/youtube/strings.xml @@ -113,6 +113,9 @@ Second \"item\" text" Թաքցնել հարցումները Թաքցնել հարցումները և հուշումները ленտայի առաջարկությունների ճշգրտության վերաբերյալ Թաքցնել տոմսերի բաժինը + + Թաքցնել \"Անհասանելի տեսանյութերը թաքցված են\" բաները + Թաքցնում է \"Անհասանելի տեսանյութերը թաքցված են\" բաները ಪլեյլիստներում Թաքցնել վերբեռնման ժամանակը "Սահմանափակումներ. @@ -171,6 +174,7 @@ Second \"item\" text" Մաքրում է տեսանյութի ենթագրերից այնպիսի արտահայտություններ, ինչպիսիք են՝ \'#\', \'Fundraiser\', \'product(s)\' և \'Shop\' Թաքցնել \"Հարցրու Gemini\" կոճակը Թաքցնում է \"Հարցրու Gemini\" կոճակը \"Հարցրու այս տեսանյութի մասին\" վահանակի ներքևում + Տեսանյութի նկարագրություն Ընտրեք, թե որ բաղադրիչները հայտնվեն տեսանյութի նկարագրության մեջ Թաքցնել «AI-ով գեներացված տեսանյութի ամփոփում» բաժինը Թաքցնել Հարցման բաժինը @@ -198,6 +202,7 @@ Second \"item\" text" Թաքցնել «Բաժանորդագրվել» կոճակը Թաքցնել «Տրանսկրիպցիա» բաժինը Թաքցնել «Տեսանյութի մանրամասները» բաժինը + Զտիչների վահանակ Ընտրեք, թե որտեղ թաքցնել թողնմամբ ֆիլտրի զուգแถվը Թաքցնել ալիքի էջում Թաքցնել մեկնաբանություններում @@ -205,6 +210,7 @@ Second \"item\" text" Թաքցնել կապված տեսանյութերում Թաքցնել որոնման արդյունքներում Թաքցնել դիտումների պատմության մեջ + Ալիքի էջ Ընտրեք, թե որ բաղադրիչները հայտնվեն ալիքի էջում Միացնել ալիքի ներ পাতাի զտիչը Ալիքի ներ পাতাի զտիչ @@ -213,9 +219,11 @@ Second \"item\" text" Թաքցնել «Համայնք» կոճակը Թաքցնել «Միանալ» կոճակը + Թաքցնել հղումների նախադիտումը Թաքցնում է սեղմելի վեբ հղումները ալիքի վերնագրում Թաքցնել \"Միայն անդամների համար\" չիպը Թաքցնում է «Միայն անդամների համար» չիպը ալիքի զտիչների տողում + Թաքցնել անդամների բաժինը Թաքցնում է ալիքի անդամների ցուցակը Թաքցնել գրառումների դարակը @@ -246,6 +254,7 @@ Second \"item\" text" Թաքցնել YouTube Create-ի մենյուն Թաքցնել \"Ինչպես է աշխատում YouTube-ը\" մենյուն Թաքցնել իրավական և գաղտնիության մասին տեղեկությունների ստորին հատվածները + Մեկնաբանություններ Ընտրեք, թե որ բաղադրիչները հայտնվեն մեկնաբանությունների բաժնում Միացնել մեկնաբանությունների կարուսելի զտիչը "• Համոզվեք, որ մուտքագրում եք կարուսելի տարրի ճիշտ անվանումը (օր.՝ Ask, Hype this video, Listen on YouTube Music) @@ -253,7 +262,10 @@ Second \"item\" text" • Եթե բոլոր կարուսելի տարրերը թաքցված են, ապա մեկնաբանությունների ողջ բաժինը նույնպես կթաքցվի" Մեկնաբանությունների կարուսելի զտիչ Մուտքագրեք մեկնաբանությունների կարուսելի անվանումները զտման համար՝ բաժանելով նոր տողերով + Թաքցնել AI չատի ամփոփումը + Թաքցնել ալիքի կանոնները Թաքցնել համատեքստային հաղորդագրությունները + Թաքցնել \"Անդամների մեկնաբանություններ\" վերնագիրը Թաքցնել մեկնաբանությունների բաժինը Թաքցնել մեկնաբանությունների բաժինը Գլխավոր ленտայում Թաքցնել համայնքի կանոնները @@ -767,6 +779,7 @@ Second \"item\" text" Սահsetեք լրամասն էկրանի ռեժիմում՝ անցնելու հաջորդ կամ նախորդ տեսանյութին Գործողությունների կոճակներ + Ընտրեք, թե որ գործողությունների կոճակները հայտնվեն դիվերտիզիոն և լրիվ էկրանի ռեժիմներում Դիզaynի կոճակներ Անջատել «Հավանել» և «Բաժանորդագրվել» կոճակների փայլը Կանխում է «Հավանել» և «Բաժանորդագրվել» կոճակների փայլեցումը, երբ ստեղծողը հիշատակում է դրանք @@ -1238,6 +1251,10 @@ Second \"item\" text" Ուղղել կարգավիճակի տողի ծածկելը վերին տողերի Պահպանում է կարգավիճակի տողի դասավորությունը հետևողական, որպեսզի այն չծածկի վերին տողերը և չթողնի դատարկ տարածք նվագարկիչի վերևում: Անջատեք սա, եթե հավելվածը փակվում է կամ վերին տողերը սխալ են երևում + Փայլ + Morphe Glow 1 + Morphe Glow 2 + Փայլուն սկսնական էկրանը չի աջակցվում այս սարքի կողմից Լուսավոր թեմայի ֆոն Սպիտակ Material You (Սպիտակ) @@ -1260,6 +1277,9 @@ Second \"item\" text" Սահմանեք տեսանյութի առաջդիմության ստորի երկրորդական գույնը Սլայդերի գույնի անվավեր արժեք + Բնօրինակ անալոգ + Բնօրինակ տորթ + Բնօրինակ մետաղ Վերնագրի լոգո Լռելյայն @@ -1533,6 +1553,7 @@ AV1-ով տեսանյութի վերարտադրությունը կարող է Միացնել տեղային հետևումը Առաջընթացի գրանցումն անջատված է Շարունակել %1$s-ից + %1$s / %2$s դիտված էպիզոդներ Էպիզոդ %1$s · %2$s Էպիզոդներ %1$s-%2$s %3$s-ից Թարմացրեք՝ էպիզոդների բեռնումը փորձելու համար: Մասնավոր ցուցակների համար անհրաժեշտ է հաշվի հասանելիություն. դրանց նշումները դեռևս բացվում են YouTube-ում: @@ -1551,7 +1572,6 @@ AV1-ով տեսանյութի վերարտադրությունը կարող է Էպիզոդները նշված են որպես դիտված Չեղարկել Միացնել - {\"one\":\"1 նոր էպիզոդ\",\"other\":\"{} նոր էպիզոդ\"} Գրանցել առաջընթացը այս սարքի վրա "Ձեր սերիալները և առաջընթացը մնում են այս սարքի վրա՝ հասանելի լինելով YouTube-ի հաշիվների համար: YouTube-ի պատմության կառավարման գործիքները չեն փոխում դրանք: Մաքրեք առաջընթացը «Սերիալներ» մենյուից: diff --git a/patches/src/main/resources/addresources/values-in-rID/music/strings.xml b/patches/src/main/resources/addresources/values-in-rID/music/strings.xml index b4bda7be82..4568e4dc6f 100644 --- a/patches/src/main/resources/addresources/values-in-rID/music/strings.xml +++ b/patches/src/main/resources/addresources/values-in-rID/music/strings.xml @@ -248,7 +248,7 @@ Lagu yang diunduh akan muncul di sini" Crossfade nonaktif saat pemutaran SABR Crossfade nonaktif saat casting Cara kerjanya - "Crossfade menumpangtindihkan akhir satu lagu dengan awal lagu berikutnya, menggunakan durasi dan kurva Anda + "Crossfade menumpangkan akhir satu lagu dengan awal lagu berikutnya, menggunakan durasi dan kurva Anda Ini berjalan pada lanjut otomatis dan lewati manual, masing-masing dengan sakelarnya sendiri. Pada lanjut otomatis, perpaduan dimulai beberapa detik sebelum lagu berakhir" Keterbatasan @@ -635,9 +635,7 @@ Berlaku untuk kedua batas. Pada 1,75, empat karakter lebar penuh dihitung sebaga 1 lirik untuk diselaraskan - melainkan ditranskripsikan QuickLRC tidak dapat memproses trek ini Kredit QuickLRC telah habis untuk trek ini - "Lirik dicocokkan berdasarkan judul dan artis, metadata yang tidak biasa mungkin tidak menemukan apa-apa atau versi yang salah. Gunakan refresh untuk beralih versi. Penyedia yang ditandai * memerlukan token. Tidak semua penyedia yang diaktifkan selalu diminta sesuai urutan Anda, tetapi merespons berdasarkan kecepatan mereka. - -Lirik berasal dari layanan pihak ketiga yang tidak berafiliasi, disponsori, atau didukung oleh Morphe. Masing-masing digunakan apa adanya: lirik, ketersediaan, dan lisensinya tetap berada di layanan tersebut dan pemberi lisensi, serta menggunakannya dapat tunduk pada syarat layanan mereka. Yang dikirim ke layanan yang diaktifkan hanyalah lookup yang Anda minta - yaitu judul, artis, dan, dimana layanan itu memerlukannya, ID video. Titik akhir AI opsional dan transkripsi QuickLRC hanya berjalan setelah Anda mengkonfigurasinya, dan menerima teks lirik atau URL video trek." + "Lirik dicocokkan berdasarkan judul dan artis, metadata yang tidak biasa mungkin tidak menemukan apa-apa atau versi yang salah. Gunakan refresh untuk beralih versi. Penyedia yang ditandai * memerlukan token. Tidak semua penyedia yang diaktifkan selalu diminta sesuai urutan Anda, tetapi merespons berdasarkan kecepatan mereka.\n\nLirik berasal dari layanan pihak ketiga yang tidak berafiliasi, disponsori, atau didukung oleh Morphe. Masing-masing digunakan apa adanya: lirik, ketersediaan, dan lisensinya tetap berada di layanan tersebut dan pemberi lisensi, serta menggunakannya dapat tunduk pada syarat layanan mereka. Yang dikirim ke layanan yang diaktifkan hanyalah lookup yang Anda minta - yaitu judul, artis, dan, dimana layanan itu memerlukannya, ID video. Titik akhir AI opsional dan transkripsi QuickLRC hanya berjalan setelah Anda mengkonfigurasinya, dan menerima teks lirik atau URL video trek." Hanya aktifkan penyedia yang Anda gunakan, atau biarkan default - permintaan terus-menerus dapat membebani situs lirik komunitas kecil Sembunyikan baris kredit Sembunyikan baris kredit yang ditampilkan di bawah lirik @@ -658,7 +656,7 @@ Lirik berasal dari layanan pihak ketiga yang tidak berafiliasi, disponsori, atau Konfigurasikan kunci API, model, endpoint, dan templat prompt untuk penyedia AI. URL endpoint API (contoh: https://api.openai.com/v1/chat/completions) Templat Prompt - Penampung: {language}, {task}, {title}, {artist}, {count}, {lines} + Placeholder: {language}, {task}, {title}, {artist}, {count}, {lines} Prompt disimpan Permintaan disimpan, tetapi endpoint AI belum dapat digunakan. Konfigurasikan terlebih dahulu di Pengaturan AI Prompt diatur ulang diff --git a/patches/src/main/resources/addresources/values-in-rID/reddit/strings.xml b/patches/src/main/resources/addresources/values-in-rID/reddit/strings.xml index 1dbfcdbedf..3d88e23131 100644 --- a/patches/src/main/resources/addresources/values-in-rID/reddit/strings.xml +++ b/patches/src/main/resources/addresources/values-in-rID/reddit/strings.xml @@ -58,9 +58,9 @@ Second \"item\" text" Menyembunyikan rak-rak Trending dari feed dan saran pencarian Hapus dialog saran notifikasi Secara otomatis menutup dialog saran notifikasi saat mengunjungi subreddit - Tampilkan flair di umpan rumah - Menampilkan badge flair postingan di umpan rumah - Beberapa item umpan mungkin tidak menampilkan flair + Tampilkan flair di feed beranda + Menampilkan badge flair postingan di feed beranda + Beberapa item feed mungkin tidak menampilkan flair Tampilkan jumlah tayangan di postingan Batasan: Hanya tersedia jika respons API menyertakan jumlah tampilan Balik bilah aksi postingan diff --git a/patches/src/main/resources/addresources/values-in-rID/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-in-rID/shared-youtube/strings.xml index 64c1a0f440..f6dbb56316 100644 --- a/patches/src/main/resources/addresources/values-in-rID/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-in-rID/shared-youtube/strings.xml @@ -21,9 +21,9 @@ Second \"item\" text" Animasi startup Dinonaktifkan Berwarna - Warna Morphe Monokrom - Monokrom Morphe + + Nonaktif Mulai ulang untuk menyelesaikan penyiapan diff --git a/patches/src/main/resources/addresources/values-in-rID/youtube/strings.xml b/patches/src/main/resources/addresources/values-in-rID/youtube/strings.xml index c13c7204f8..05c506f1af 100644 --- a/patches/src/main/resources/addresources/values-in-rID/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-in-rID/youtube/strings.xml @@ -113,6 +113,9 @@ Perubahan mencakup: Sembunyikan survei Sembunyikan survei dan pengingat mengenai akurasi rekomendasi feed Sembunyikan rak tiket + + Sembunyikan banner \"Video yang tidak tersedia disembunyikan\" + Menyembunyikan banner \"Video yang tidak tersedia disembunyikan\" di daftar putar Sembunyikan waktu unggah "Batasan: @@ -438,9 +441,9 @@ Laporan bersifat publik dan ditinjau sebelum saluran ditambahkan ke daftar. Peng Komentar disalin ke papan klip Ketuk dan tahan untuk membuka menu video - Mengetuk dan menahan di mana saja pada video umpan membuka menunya + Mengetuk dan menahan di mana saja pada video feed membuka menunya Tampilkan tombol Tonton nanti pada thumbnail - Tombol Tonton nanti ditampilkan pada thumbnail video umpan. Mengetuk dan menekannya akan membuka Simpan ke daftar putar + Tombol Tonton nanti ditampilkan pada feed thumbnail video. Mengetuk dan menekannya akan membuka Simpan ke daftar putar Unduh gambar postingan Tombol unduh ditampilkan di samping tombol Bagikan pada postingan dengan gambar atau GIF, dan item unduh ditambahkan ke menu mereka @@ -453,18 +456,18 @@ Laporan bersifat publik dan ditinjau sebelum saluran ditambahkan ke daftar. Peng Tidak dapat mengunduh gambar Tidak ditemukan gambar dalam postingan ini - Tambahkan item Ask ke menu flyout - Menambahkan item ringkasan video ke menu flyout umpan + Tambahkan item Tanya ke menu flyout + Menambahkan item ringkasan video ke menu flyout feed - "Ask awalnya memerlukan akun yang sudah masuk di negara tempat Ask tersedia. + "Tanya awalnya memerlukan akun yang sudah masuk di negara tempat Tanya tersedia. -Setelah video dengan tombol Ask dibuka satu kali, fitur ini berfungsi untuk semua video, bahkan jika negara saat ini tidak memiliki fitur Ask." +Setelah video dengan tombol Tanya dibuka satu kali, fitur ini berfungsi untuk semua video, bahkan jika negara saat ini tidak memiliki fitur Tanya." Ringkas video - Tidak dapat membuka Ask untuk video ini + Tidak dapat membuka Tanya untuk video ini - Ask tidak tersedia untuk akun atau negara Anda saat ini + Tanya tidak tersedia untuk akun atau negara Anda saat ini Tampilkan jumlah tayangan pada thumbnail Jumlah tayangan video feed ditampilkan di sudut kiri bawah thumbnail mereka, bukan di bawah judul. Beberapa video, seperti di rak, mungkin tidak menampilkan jumlah tayangan @@ -488,7 +491,7 @@ Setelah video dengan tombol Ask dibuka satu kali, fitur ini berfungsi untuk semu Menonaktifkan gerakan ketuk dua jari yang melewati bab, yang mungkin terpicu secara tidak sengaja dengan satu jari Unduh video, podcast, atau lagu menggunakan aplikasi pihak ketiga - Tampilkan tombol unduh tumpang tindih pemutar + Tampilkan tombol unduh di hamparan pemutar Menampilkan tombol di pemutar video yang membuka pengunduh eksternal Anda Unduhan cepat sekali ketuk Perintah untuk mengunduh di latar belakang. Ketuk dan tahan untuk membuka pengunduh. Berfungsi dengan YTDLnis dan YTindir @@ -1248,6 +1251,7 @@ Batasan: Jika 'Kembalikan bilah tindakan video lama' dinonaktifkan, hamparan 'Vi Menjaga tata letak status bar konsisten, sehingga tidak menutupi bar atas dan tidak meninggalkan ruang kosong di atas pemutar. Matikan ini jika aplikasi mengalami crash atau bar atas terlihat salah Bercahaya + Layar splash glow tidak didukung oleh perangkat ini Latar belakang tema terang Putih Material You (Putih) diff --git a/patches/src/main/resources/addresources/values-is-rIS/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-is-rIS/shared-youtube/strings.xml index c192a5339a..e8d02363be 100644 --- a/patches/src/main/resources/addresources/values-is-rIS/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-is-rIS/shared-youtube/strings.xml @@ -21,9 +21,8 @@ Second \"item\" text" Ræsingarhreyfimynd Óvirkt Litur - Litur Morphe Einstafengur - Einstafengur Morphe + Endurræstu til að lokar á uppsetninguna diff --git a/patches/src/main/resources/addresources/values-is-rIS/youtube/strings.xml b/patches/src/main/resources/addresources/values-is-rIS/youtube/strings.xml index 72a143f6e2..66923ba52e 100644 --- a/patches/src/main/resources/addresources/values-is-rIS/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-is-rIS/youtube/strings.xml @@ -103,6 +103,9 @@ Second \"item\" text" Fela kannelana Hylur kannanir og hröggur um nákvæmni um tillögur um umhverfi Fela miðaskjá + + Fela bannera „Óaðgengileg myndbönd eru falin“ + Felur bannera „Óaðgengileg myndbönd eru falin“ í spilunum Fela hleðslutíma vídeós "Takmarkanir: @@ -1217,6 +1220,7 @@ Takmörkun: Ef 'Endurskapa gamla stjórnbara myndbands' er slökkt, gæti 'Fleir Heldir útliti stöðustikunnar í samræmi, svo hún þeki ekki efri tækin og skilur eftir sig enginn tómarúm ofan við spilarann. Slökktu á þessu ef forritið brotnar eða efri tækin líta ranglega út Glóð + Glow-skjárstýring er ekki studduð af þessu tæki Ljósþemabakgrunnur Hvítur Material You (hvítur) diff --git a/patches/src/main/resources/addresources/values-it-rIT/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-it-rIT/shared-youtube/strings.xml index 0a0640aaa8..61a6514499 100644 --- a/patches/src/main/resources/addresources/values-it-rIT/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-it-rIT/shared-youtube/strings.xml @@ -21,9 +21,11 @@ Second \"item\" text" Animazione di avvio Disattivato Colore - Color Morphe + Morphe Colorato Monocromatico - Morphe monocromatico + Morphe Monocromatico + + Spento Riavvia per completare la configurazione diff --git a/patches/src/main/resources/addresources/values-it-rIT/sponsorblock/strings.xml b/patches/src/main/resources/addresources/values-it-rIT/sponsorblock/strings.xml index ff8bc9996b..6a562ee27a 100644 --- a/patches/src/main/resources/addresources/values-it-rIT/sponsorblock/strings.xml +++ b/patches/src/main/resources/addresources/values-it-rIT/sponsorblock/strings.xml @@ -58,6 +58,8 @@ Second \"item\" text" Già lette Mostramele Generale + Salva segmenti di video scaricati + Consente di saltare segmenti di video scaricati mentre si è offline Mostra avvisi di errore di connessione Mostra una notifica quando il server SponsorBlock non è disponibile Attiva il monitoraggio del conteggio dei salti diff --git a/patches/src/main/resources/addresources/values-it-rIT/youtube/strings.xml b/patches/src/main/resources/addresources/values-it-rIT/youtube/strings.xml index 3f749e2a7a..cb7843d1e5 100644 --- a/patches/src/main/resources/addresources/values-it-rIT/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-it-rIT/youtube/strings.xml @@ -113,6 +113,9 @@ Le modifiche includono: Nascondi sondaggi Nasconde i sondaggi e i suggerimenti relativi alla precisione dei consigli nel feed Nascondi la sezione dei biglietti + + Nascondi il banner \"I video non disponibili sono nascosti\" + Nasconde il banner \"I video non disponibili sono nascosti\" nelle playlist Nascondi l\'ora di caricamento "Limitazioni: @@ -1207,6 +1210,8 @@ Limitazione: se \"Ripristina la vecchia barra delle azioni video\" è disattivat Impedisce al lettore minimizzato \'Continua a guardare\' di essere mostrato all\'avvio dell\'app Lettore minimizzato Personalizza l\'aspetto e il comportamento del lettore minimizzato + Miniplayer aggiuntivi + YouTube ha rimosso tutti i miniplayer aggiuntivi. Per ripristinare ulteriori tipologie di miniplayer, è necessario applicare una patch a YouTube versione 21.28.208 o precedente. Tipo di Lettore minimizzato Nascondi titolo e canale Disattivato @@ -1248,6 +1253,9 @@ Limitazione: se \"Ripristina la vecchia barra delle azioni video\" è disattivat Mantiene coerente il layout della barra di stato, in modo che non copra le barre superiori e non lasci spazio vuoto sopra il player. Disattiva questa opzione se l\'app si arresta in modo anomalo o se le barre superiori appaiono errate Bagliore + Morphe Luminoso 1 + Morphe Luminoso 2 + La schermata di avvio Glow non è supportata da questo dispositivo Sfondo del tema chiaro Bianco Material You (Bianco) diff --git a/patches/src/main/resources/addresources/values-iw-rIL/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-iw-rIL/shared-youtube/strings.xml index 128d3c2bd3..c24a7a412c 100644 --- a/patches/src/main/resources/addresources/values-iw-rIL/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-iw-rIL/shared-youtube/strings.xml @@ -21,9 +21,9 @@ Second \"item\" text" אנימציית אתחול מושבת צבע - Color Morphe מונוכרום - Monochrome Morphe + + כבוי הפעל מחדש כדי לסיים את ההגדרות diff --git a/patches/src/main/resources/addresources/values-iw-rIL/youtube/strings.xml b/patches/src/main/resources/addresources/values-iw-rIL/youtube/strings.xml index 1108d764cb..f54f0bb73f 100644 --- a/patches/src/main/resources/addresources/values-iw-rIL/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-iw-rIL/youtube/strings.xml @@ -113,6 +113,9 @@ Second \"item\" text" הסתר סקרים מסתיר סקרים והנחיות לגבי דיוק המלצות הפיד הסתר מדף כרטיסים + + הסתר את הבאנר \"סרטונים שאינם זמינים מוסתרים\" + מסתיר את הבאנר \"סרטונים שאינם זמינים מוסתרים\" ברשימות השמעה הסתר זמן העלאה "הגבלות: @@ -1249,6 +1252,9 @@ Second \"item\" text" שומר על עקביות תצורת מסך החיווי, כך שהוא לא יכסה את הפסים העליונים ולא יישאר רווח ריק מעל הנגן. כבה אפשרות זו אם האפליקציה קורסת או שהפסים העליונים נראים לא תקין זוהר + Morphe Glow 1 + Morphe Glow 2 + מסך פתיחה עם אפקט Glow אינו נתמך במכשיר זה רקע של ערכת נושא בהירה לבן Material You (לבן) diff --git a/patches/src/main/resources/addresources/values-ja-rJP/music/strings.xml b/patches/src/main/resources/addresources/values-ja-rJP/music/strings.xml index 029e99e7e7..82e4d9ee9a 100644 --- a/patches/src/main/resources/addresources/values-ja-rJP/music/strings.xml +++ b/patches/src/main/resources/addresources/values-ja-rJP/music/strings.xml @@ -546,14 +546,14 @@ SABR再生(動画ストリームのスプーフがオフ)" ロック画面、Android Auto、Bluetooth で現在の行を表示。Android 11 以降が必要 ミニプレイヤーの歌詞を表示 ミニプレイヤーで現在行を表示し、トラックをサブタイトルに移動 - 曲名 / アーティスト名のフォーマット - ミニプレイヤーおよびMediaSessionで歌詞と共に表示されるトラックの形式です。{t}は曲名、{a}はアーティスト、{v}は歌詞ソースが提供している場合のアコースティックなどのバージョンを表します。2つのコロンで囲まれたテキストは、その中のすべての変数に値がある場合にのみ表示されます。通常のコロンを表示するには\\:と入力してください。各行は歌詞が変わるたびに順番に表示されます - 歌詞行の形式 - ミニプレイヤーおよびMediaSessionにおける歌詞行の形式です。{lrc}は歌詞の行、{n}はその行が連続して歌われる回数を表します。コロンで囲まれたテキストは、行が繰り返される場合にのみ表示されます。コロンを表示するには\\:を使用してください - 形式には {lrc} を含める必要があります - アーティスト行のテンプレートとアニメーションタイミング - 最小表示時間 (ms) - 最大表示時間 (ms) + 曲 / アーティスト行のフォーマット + ミニプレーヤーとメディア コントロールの歌詞行の隣に、曲情報を一行で表示する機能で使用するフォーマット。{t} が曲名、{a} がアーティスト名、{v} がアコースティックなどのバージョンを表す変数です。バージョンは提供されない場合があり、その場合は \{v\} に値が入りません。2 つのコロンで囲まれたテキストは、その中に含まれるすべての変数に値が入っている場合にのみ表示されます。通常のコロンを表示するには、\\: のように記述してください。文字数制限により複数行に分割された場合、歌詞行の歌詞が更新されるたびに行を切り替えながら、各行を一行ずつ順番に表示します + 歌詞行のフォーマット + ミニプレーヤーとメディア コントロールに、歌詞を一行ずつ表示する機能で使用するフォーマット。{lrc} が一行の歌詞、{n} がその歌詞が連続して繰り返し歌われる回数を表す変数です。2 つのコロンで囲まれたテキストは、\{n\} が 1 より大きい場合にのみ表示されます。通常のコロンを表示するには、\\: のように記述してください + このフォーマットには {lrc} が必須です + 曲 / アーティスト行のテンプレートと分割表示する場合の表示方法 + 最小表示時間 (ミリ秒) + 最大表示時間 (ミリ秒) 複数行アニメーションのみに適用されます。各フレームを少なくとも最小時間分保持します。最大時間に達すると、歌詞が同じままでもアーティスト行を進めます。 最小値は0以上である必要があります。最大値は0より大きく、かつ最小値以上である必要があります。 行の長さ制限 diff --git a/patches/src/main/resources/addresources/values-ja-rJP/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-ja-rJP/shared-youtube/strings.xml index ac745e1b19..4a8e595355 100644 --- a/patches/src/main/resources/addresources/values-ja-rJP/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ja-rJP/shared-youtube/strings.xml @@ -21,9 +21,9 @@ Second \"item\" text" 起動時のアニメーション 無効 カラー - カラー Morphe モノクロ - モノクロ Morphe + + オフ 設定を完了するには再起動してください diff --git a/patches/src/main/resources/addresources/values-ja-rJP/youtube/strings.xml b/patches/src/main/resources/addresources/values-ja-rJP/youtube/strings.xml index 3626cff102..6e7f8f8c69 100644 --- a/patches/src/main/resources/addresources/values-ja-rJP/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ja-rJP/youtube/strings.xml @@ -41,16 +41,16 @@ Second \"item\" text" レイアウトの更新を無効化 サーバーによるアプリのレイアウトの更新を防ぎます - "アプリのレイアウトは、初回インストール時に使用されるレイアウトに戻ります。 + "アプリのレイアウトは、初回インストール時のレイアウトに戻ります。 -サーバー側のレイアウトの一部は戻らない場合があります。 +サーバー側のレイアウトによっては、元に戻らない場合があります。 変更内容: -• プレイヤーのフライアウトメニュー(または関連設定)内のコンポーネントが正常に動作しない場合があります +• プレーヤーのフライアウト メニュー(または関連設定)内のコンポーネントが正常に動作しない場合があります • 数字の回転アニメーションは表示されません • ライブラリタブが表示されます -• 動画説明の音楽セクションが正常に動作しない場合があります -• プレイヤーのフライアウトメニュー項目は非表示にできません +• 動画の概要欄の音楽セクションが機能しない場合があります +• プレーヤーのフライアウト メニューの項目は非表示にできません • アカウント切替ボタンはライブラリタブに表示されない場合があります" フィードのフライアウト メニューのフィルタを有効化 @@ -113,6 +113,9 @@ Second \"item\" text" アンケートを非表示 フィードのおすすめ精度に関するアンケートやフィードバック要求を非表示にします チケット欄を非表示 + + 「利用できない動画は非表示になっています」バナーを非表示にする + プレイリスト内の「利用できない動画は非表示になっています」バナーを非表示にします 公開からの経過時間 / 予定時間を非表示 "制限事項: @@ -891,7 +894,7 @@ APIキーは決して共有しないでください。アクセス権を持つ ボタンの間隔を詰める ナビゲーション ボタンの間隔を詰めて表示します ラベルのないナビゲーションボタンを無効にする - YouTubeがラベルなしで送信する場合、ナビゲーションボタンをラベル付きで表示します + YouTube からラベルなしのナビゲーション ボタンが送信された場合、ナビゲーション ボタンをラベル付きで表示します ナビゲーション ボタンの最上部への移動を無効化 YouTube が「登録チャンネル」ボタンをホームの最上部に移動するレイアウトを有効にした場合に、このレイアウトを無効にし、最下部のナビゲーション バーに戻します ナビゲーション バーのアニメーションを有効化 @@ -967,7 +970,7 @@ APIキーは決して共有しないでください。アクセス権を持つ プレーヤーのコントロールの背景を非表示 動画プレーヤーのコントロール (再生ボタンなど) の周辺に表示される、ふちどりのための影を非表示にします プレーヤーのコントロールの背景の透明度 - プレイヤーのコントロールボタンとタイムバーの背後にある背景の不透明度を調整します(0は透明、100は不透明) + プレーヤーのコントロール ボタンとタイムバーの背景の不透明度を調整します(0 は透明、100 は不透明) 前の動画ボタンと次の動画ボタンを非表示 設定ボタンを非表示 @@ -1110,22 +1113,23 @@ APIキーは決して共有しないでください。アクセス権を持つ スマートフォン タブレット Automotive - "変更点: + "以下の変更を伴います: タブレット レイアウト -• コミュニティ投稿は非表示になります -• フィードでの再生設定は無効です -• Shortsでのリミックス ボタンとサウンド ボタンは無効です +• コミュニティ投稿が表示されません +• YouTube の設定項目の [フィードで再生] が表示されません +• ショート動画のリミックス ボタンとサウンドボタンが利用できません • 「その他の動画」オーバーレイが表示されない場合があります -• 折りたたみ前の旧動画アクションバーが表示されます +• 動画プレーヤーのアクション ボタンにボタン名が表示されます (旧仕様) +• フライアウト メニューの項目が正しく機能しない場合があります Automotive レイアウト -• フィードはトピックやチャンネルごとに整理されます -• フィードでの再生設定は無効です -• Shortsでのリミックス ボタンとサウンド ボタンは無効です +• フィードがトピックやチャンネルごとに分類された形で表示されます +• YouTube の設定項目の [フィードで再生] が表示されません +• ショート動画のリミックス ボタンとサウンドボタンが利用できません • 「その他の動画」オーバーレイが表示されない場合があります -• Shortsは通常のプレーヤーで開きます -• 折りたたみ前の旧動画アクションバーが表示されます" +• ショート動画は通常のプレーヤーで再生されます +• 動画プレーヤーのアクション ボタンにボタン名が表示されます (旧仕様)" プレーヤーにのみタブレット レイアウトを適用 "動画プレーヤーのレイアウトを以前のスタイルに戻します @@ -1249,6 +1253,9 @@ Automotive レイアウト ステータスバーのレイアウトを統一し、上部のバーが覆われないようにして、プレイヤーの上に空白ができないようにします。アプリがクラッシュしたり、上部のバーの表示がおかしい場合は、これをオフにしてください グロウ + Morphe Glow 1 + Morphe Glow 2 + このデバイスではグロー・スプラッシュ画面はサポートされていません ライトモードのテーマ ホワイト Material You(ホワイト) @@ -1297,7 +1304,7 @@ Automotive レイアウト DeArrow アイコンを表示 クラウドソースされた動画タイトルに DeArrow アイコンを表示します カジュアルモード - DeArrowユーザーが元のタイトルを好むと投票した場合、DeArrowタイトルではなく元のタイトルを表示します + DeArrow ユーザーが元のタイトルを「いいね」と投票した場合、DeArrow のタイトルではなく元のタイトルが表示されます 元のタイトルを表示するためのカジュアル投票の最小数 元のタイトルを表示するために必要な、面白いやクリエイティブなどの単一カテゴリでのカジュアル投票の数 DeArrow サムネイル @@ -1305,9 +1312,9 @@ Automotive レイアウト DeArrow & デフォルト DeArrow & 静止画 静止画サムネイル - "DeArrowは、クリックベイトのタイトルとサムネイルを、より正確で扇動的でないクラウドソースのものに置き換えます。タイトルはコミュニティによって書かれ投票され、サムネイルは動画から取られたフレームです + "DeArrow は、クリックベイト的なタイトルやサムネイルを、より正確でセンセーショナルさが抑えられた、クラウド ソーシングによるものに置き換えます。タイトルはコミュニティによって作成・投票され、サムネイルは動画から切り出されたフレームが使用されます -動画はハッシュ化された動画IDの短いプレフィックスによってリクエストされるため、サーバーはあなたが見ている動画を正確に特定できません。動画にDeArrowのサムネイルがない場合、オリジナルまたは静止画が表示されます" +動画は、ハッシュ化された動画 ID の短いプレフィックスでリクエストされるため、サーバーはユーザーがどの動画を閲覧しているかを正確に把握することはできません。動画に DeArrow のサムネイルがない場合は、元の画像または静止画が表示されます" 接続エラーの通知を表示 DeArrow サーバーが利用できない場合に通知を表示します API エンドポイント @@ -1323,7 +1330,7 @@ Automotive レイアウト DeArrow は一時的に利用できません - 以前のプレーヤーボタンのデザインに戻す + 以前のプレーヤー ボタンのデザインに戻す 動画プレーヤーのコントロールのデザインを、現在の太字デザインから以前の細字デザインに戻します デバイスの画面サイズを偽装 @@ -1498,7 +1505,7 @@ AV1 での動画再生はカクつくか、フレームが欠落する可能性 履歴にシリーズページを追加し、プレイリストにシリーズをフォローするオプションを追加します ナビゲーション バーにシリーズを表示 シリーズ トラッカー ボタンを表示 - 現在の動画のシリーズをフォローまたは開くためのプレーヤーボタンを追加します + 現在の動画のシリーズをフォローまたは開くためのプレーヤー ボタンを追加します フォロー中のシリーズの進捗を記録する 保存されたシリーズとブックマークに対して、この電話での再生を記録します キャンセル @@ -1509,7 +1516,7 @@ AV1 での動画再生はカクつくか、フレームが欠落する可能性 ライブラリオプション エピソードを更新 名前を変更 - YouTubeでプレイリストを開く + YouTube でプレイリストを開く ライブラリから削除 プレイリストリンクまたはID すべてのローカル進捗を消去 @@ -1538,19 +1545,19 @@ AV1 での動画再生はカクつくか、フレームが欠落する可能性 視聴済み エピソードを読み込み中… その他のオプション - "保存されたシリーズとブックマークされた動画についてのみ進行状況を記録します。 + "保存したシリーズ動画とブックマークした動画のみ、進捗状況を記録します。 -このライブラリはデバイス上に保存され、YouTube アカウント間で共有されます。YouTube の一時停止および履歴消去コントロールはこのライブラリを変更しません。削除するには「シリーズ」→「詳細」→「ローカル進行状況を消去」を使用します。 +このライブラリはデバイス上に保存され、複数の YouTube アカウントで共有されます。YouTube の一時停止や履歴を削除する機能では変更されません。削除するには、「シリーズ」→「その他」→「ローカルの進行状況を消去」を選択してください。 -匿名モード、アカウントIDが利用できない場合、または異なるアカウントに切り替えた場合、自動記録は停止します。そのアカウントで使用するには追跡を再度有効にしてください" - 匿名モードでは、または YouTube が現在のアカウント状態を読み込む前に、追跡を有効にすることはできません。ホームに戻ってもう一度お試しください。サインインは必要ありません +シークレット モードが有効の場合、アカウント ID が利用できない場合、または別のアカウントに切り替えた場合、自動記録は停止します。そのアカウントで使用するには、追跡を再度有効にしてください" + シークレット モードが有効の場合、または YouTube が現在のアカウント状態を読み込むまでは、追跡機能を有効にすることはできません。ホーム画面に戻って、もう一度お試しください。ログインは不要です ローカル追跡を有効にする 進捗記録はオフです %1$s で再開 %1$s / %2$s エピソード視聴済み エピソード %1$s · %2$s エピソード %1$s-%2$s of %3$s - 読み込みを試すために更新してください。プライベートプレイリストにはアカウントアクセスが必要です。ブックマークは YouTube で開きます + ページを更新して、エピソードの読み込みを試してみてください。非公開プレイリストを再生するにはアカウントへのアクセス権が必要です。ブックマークは引き続き YouTube で開きます YouTube はこのエピソードを開けませんでした YouTube はこのプレイリストを開けませんでした エピソードを選択 @@ -1569,9 +1576,9 @@ AV1 での動画再生はカクつくか、フレームが欠落する可能性 新作エピソード1個 新エピソード %1$s このデバイスでの進捗記録 - "シリーズと進行状況はこのデバイス上に留まり、YouTube アカウント間で共有されます。YouTube の履歴コントロールはこれを変更しません。「シリーズ」メニューから進行状況を消去できます。 + "シリーズと進捗状況は、このデバイスに保存され、YouTube アカウント間で共有されます。YouTube の履歴コントロールでは、これらの情報は変更されません。「シリーズ」メニューから進捗状況をクリアしてください。 -自動記録はオプションです。匿名モードまたはアカウント変更時に停止します" +自動記録はオプションです。シークレット モードが有効になっている場合、またはアカウントが変更された際に停止します" エピソードを読み込めませんでした。プレイリストをブックマークとして保存できます 保存されていない変更が多すぎます。最初に保存を再試行してください 保存操作中です。再試行してください @@ -1583,7 +1590,7 @@ AV1 での動画再生はカクつくか、フレームが欠落する可能性 プレイリストが変更されました。エピソードを再度選択してください このエピソードは利用できません。別のもので選んでください シリーズ名を入力してください - YouTube プレイリストリンクまたはプレイリスト ID を入力してください + YouTube プレイリスト リンクまたはプレイリスト ID を入力してください 再更新するまでお待ちください エピソードを読み込めませんでした。後でもう一度お試しください 変更を完了できませんでした。再試行してください @@ -1608,10 +1615,10 @@ AV1 での動画再生はカクつくか、フレームが欠落する可能性 視聴済み割合 (1-100) 残り秒数 (0-300; 0 で無効化) %1$s から %2$s の値を入力してください - YouTubeの進捗を使用 + YouTube の進捗状況を使用 他のデバイスで視聴した後、フォロー中のシリーズを引き継ぐ 現在の YouTube アカウントからフォロー中のシリーズの視聴済み状態と再開位置を読み取ります。一致するエピソードのみがこのスマートフォンに保存されます。チェックはシリーズライブラリが開いている間と、続きを見る前に実行されます。別のデバイスで YouTube 履歴を有効にする必要があります - YouTubeの進捗を確認中… + YouTube の進捗状況を確認中… YouTube の進行状況は利用できません。保存された進行状況を使用しています 一部の YouTube 進行状況が利用できません。保存された位置は保持されます バックアップと復元 @@ -1640,14 +1647,14 @@ AV1 での動画再生はカクつくか、フレームが欠落する可能性 ニコニコ動画風のコメント表示 ライブチャットとチャットのリプレイを、ビデオ上を流れるコメントとして表示します - ビデオプレーヤーにオーバーレイボタンを表示します。タップするとコメントの表示/非表示を切り替え、長押しすると簡易設定を表示します + プレーヤー オーバーレイにボタンを表示します。タップするとコメントの表示 / 非表示を切り替えます。長押しすると簡易設定メニューを表示します コメントのフォントサイズ コメントのテキストサイズ (12-40 sp) コメントの不透明度 コメントのテキストと背景の不透明度 移動時間 コメントが画面を横切るまでの秒数です。値を小さくすると移動速度が上がります - コメントの最大文字数 - これより長い文字数のコメントを非表示にします。絵文字は1文字としてカウントされます + コメントの文字数の上限 + この値より文字数が多いコメントを非表示にします。絵文字はそれぞれ 1 文字としてカウントされます 制限なし diff --git a/patches/src/main/resources/addresources/values-ka-rGE/music/strings.xml b/patches/src/main/resources/addresources/values-ka-rGE/music/strings.xml index 0c2cf6832f..aa9c984bcd 100644 --- a/patches/src/main/resources/addresources/values-ka-rGE/music/strings.xml +++ b/patches/src/main/resources/addresources/values-ka-rGE/music/strings.xml @@ -17,59 +17,675 @@ Second \"item\" text" --> + ბოლოს ნათქვამი + გამოწერები + ვიდეოები + სხვა შედეგები + ატვირთული სიმღერები + ატვირთული ალბომები + ატვირთული შემსრულებლები + განაახლეთ ჰოსტის Jam პატჩი პაუზისა და განახლების გამოსაყენებლად + Jam რიგის გაზიარება + გახდით ახლომდებარე Jam-ის მასპინძელი ან შეუერთდით მას და დაარედაქტირეთ მისი განრიგი Jam Layer-ის მეშვეობით + Jam რიგის გაზიარების ჩართვა + რთავს Jam-ის ინტეგრაციას YouTube Music-ის რესტარტის შემდეგ + ჩამოტვირთეთ Jam Layer მუშაობისთვის + გახსენით Jam Layer-ის უახლესი ვერსია თქვენს ბრაუზერში + Jam-ის მართვა + გახდით ახლომდებარე Jam-ის მასპინძელი ან შეუერთდით მას და მართეთ სესია + Jam Layer პაკეტი + აირჩიეთ დაინსტალირებული დამხმარე აპლიკაცია ახლომდებარე რიგის გასაზიარებლად + გამოიყენეთ დაინსტალირებული დამხმარე პაკეტი. დაწყვილება მოითხოვს თქვენს დასტურს და ლოკალურ შესაძლებლობის ტოკენს + ჩართოთ Wi-Fi? + ჩართეთ Wi-Fi, შემდეგ სცადეთ Jam-ის ხელახლა დაწყება ან მასთან შეერთება + Wi-Fi პარამეტრების გახსნა + გაუქმება + გამოიყენეთ პაკეტი + შეიყვანეთ Android-ის პაკეტის ვალიდური სახელი + პაკეტი შენახულია. დასაკავშირებლად დააყენეთ Jam Layer + Jam გაწყვეტილია + ჯერ გააქტიურეთ Jam Layer + გააქტიურეთ Jam-ის რიგის გაზიარება პარამეტრებში, შემდეგ კი გადატვირთეთ YouTube Music + ჯერ დააინსტალირეთ არჩეული Jam Layer პაკეტი + შეიყვანეთ მასპინძელზე მითითებული კოდი. Wi-Fi Aware-ს შეუძლია ახლომდებარე მოწყობილობების დაკავშირება მაშინაც კი, როდესაც მათი LAN-ები განსხვავდება + присоедиება კოდით + შეერთება + QR-ის სკანირება + შეიყვანეთ რვასიმბოლოიანი კოდი ან ჩასვით მოწვევა + შემოუერთდით თქვენს Jam-ს + კოდის კოპირება + Jam-ის კოდი + QR-ის ჩვენება + დასრულდა + მოიწვიეთ თქვენს Jam-ში + დაასკანერეთ სრიალში მოსაწვევად და რედაქტირებისთვის. Jam-ის დასრულება აუქმებს მოწვევას + ბმულის კოპირება + Jam-ის მოწვევა + Jam ხელახლა უკავშირდება; სცადეთ მოგვიანებით + დაემატა Jam-ს + Jam недоступურია; ტრეკი არ დაემატა + თქვენი Jam + ერთად მოსმენა + Jam-ის შეერთება + მოისმინეთ ერთად + Jam-ის განახლება… + თქვენი ჰოსტის ძებნა Wi-Fi-ზე… + ხალხის ლოდინი + %s პირი ჩართულია + %s პირი ჩართულია + შეიყვანეთ ეს კოდი მეზობელ მოწყობილობაში. Wi-Fi Aware მუშაობს განცალკევებულ ლოკალურ ქსელებში, თუ მხარდაჭერილია. ვადა იწურება %s წუთში + გადავ scrolls %s-ზე? + რედაქტირება ღიაა + რედაქტირება დაბლოკილია + დაკავშირებულია Wi-Fi Aware-ის მეშვეობით + დაკავშირებულია Wi-Fi-ს მეშვეობით + დაკავშირებულია Bluetooth-ის მეშვეობით + Jam-ის დროს არ არის მხარდაჭერილი + przyсоедиება გაუქმდა + ხელახლა დაკავშირება ჰოსტთან… + გააზიარეთ მორიგი. საშუალება მიეცით ყველას, დაამატონ სიმღერები + მოიწვიეთ ადამიანები + სტუმრებს შეუძლიათ რიგის რედაქტირება + Jam-ის დასრულება + მუსიკას რთავს ჰოსტი. დაამატეთ, გადაადგილეთ ან წაშალეთ სიმღერები პირდაპირ YouTube Music-ის მორიგში + ორივე მოწყობილობა ახლოს დატოვეთ. შეგიძლიათ გააუქმოთ, სანამ ჰოსტს ვეძებთ + przyсоедиების გაუქმება + Jam-ის დატოვება + ჯემ-სესიის დაწყება + ჯემ-ფენის გამართვა + მართვა + დეტალები + გახსნა + სიმღერის პარამეტრების ჩატვირთვა… + სიმღერის პარამეტრები недоступბელია; სცადეთ ხელახლა + სიმღერის პარამეტრების გახსნა ვერ მოხერხდა + Jam Layer-ის ჩამოსატვირთად ბრაუზერი არ არის ხელმისაწვდომი + მესპრიან Gastgeber-ის რიგის მოლოდინში + ჯერ გახსენით სიმღერა YouTube Music-ში + განამეზის Gastgeber-ზე + გაიტოვეთ Jam და ჩართეთ локаლურად + ჩართეთ შემდეგი Jam-ში + დაამატეთ Jam-ის რიგში + განიშნავეთ ეს Jam ტრეკი + აირჩიეთ დაკვრის ადგილი + გადაიტანეთ ჰოსტერი აქ + სიმღერა არ არის მზად გადასახტობად + რიგი შეიცვალა; სცადეთ ჟესტი ხელახლა + ზედა პანელის ლოგო + ნაგულისხმები + საკუთარი + ალბომის აუდიო ტრეკების დაკვრა + სიმღერების ალბომის მხოლოდ აუდიო ვერსიის დაკვრა მუსიკალური ვიდეოს ნაცვლად. საჭიროებს \'ვიდეო ნაკადების იმიტაციას\' + აუდიო/ვიდეო გადამრთველის ჩართვა + სცადეთ აუდიო ტრეკის ვერსიისა და ოფიციალური მუსიკალური ვიდეოს გადართვა + ჩამოტვირთეთ მუსიკალური ვიდეოები, პოდკასტები ან სიმღერები მესამე მხარის ჩამტვირთავი აპლიკაციების მეშვეობით + გამოიყენეთ გარე ჩამტვირთავი Download მენიუსთვის + მენიუს Download ოფცია ამის ნაცვლად ხსნის თქვენს გარე ჩამტვირთავს ჩამოტვირთულები + "ჩამოტვირთვები არ არის +ჩამოტვირთული ტრეკები აქ გამოჩნდება" + %1$s • %2$s • %3$s მბ + %1$s ტრეკი • %2$s მბ + ჩამოტვირთვების ძებნა + სორტირება + სორტირება შემსრულებლის მიხედვით + სორტირება სათაურის მიხედვით + სორტირება ზომის მიხედვით + ყველა ჩამოტვირთვის წაშლა + წაშალოთ ყველა ჩამოტვირთვა? + ყველა გადმოწერილი ტრეკი წაიშლება ამ მოწყობილობიდან + გადმოწერილი ფაილები წაიშალა + ვერაფერი მოიძებნა + მოქმედებები %s-ისთვის + გనახეთ + გనახეთ ან შეაჩერეთ + წინა + შემდეგი + შეჩერება + დახურვა + წაშლა + ჩამოტვირთვის წაშლა + წავშალო ჩამოტვირთვა? + %s წაიშლება ამ მოწყობილობიდან + ჩამოტვირთვა წაშლილია + ჩამოტვირთვის წაშლა ვერ მოხერხდა + ჩამოტვირთულის დაკვრა ვერ მოხერხდა + აუდიო ჩამოტვირთვები + ოფლაინ დაკვრა + ჩამოსატვირთი ტრეკის იდენტიფიცირება ვერ მოხერხდა + ჩამოტვირთვა უკვე მიმდინარეობს + უკვე ჩამოტვირთულია + ჩამოტვირთვა დაიწყო + ჩამოტვირთვის დაწყება… + ჩამოტვირთვა %s%% + ჩამოტვირთვა დასრულდა + ჩამოტვირთვა ჩაიშალა + Morphe-ის შესახებ + რეკლამები + ფიდი + ზოგადი + ფლეიერი + ტექსტები + სხვადასხვა + YouTube Music-ის ენა + სისტემის ენა + საჭიროა Android 13 ან უფრო ახალი ვერსია + წარწერის \"მიიღეთ Music Premium\" დამალვა + 숨겨 Music Premium-ის აქციები + ვიდეორეკლამების დამალვა + 숨겨 აუდიოს / ვიდეოს გადამრთველი + ჩამოტვირთვის ღილაკის დამალვა + მალავს ჩამოტვირთვის ღილაკს პოდკასთის შოუში + დამალეთ Explore-ის თარო + მოიხსნათ ფილტრების პანელი + ბადისებრი თვლების დამალვა + ჰორიზონტალური თვლების დამალვა + სпиისებრი თვლების დამალვა + ტექსტების პანელი + გაზიარების ღილაკის დამალვა + თარგმნის ღილაკის დამალვა + დამალეთ თარო „ახალია“ + ფლეილისტების თვლების დამალვა + გამეორების ღილაკის დამალვა + გაურკვეველი თანმიმდევრობის ღილაკის დამალვა + \"სწრაფი წვდომის\" თვლის დამალვა + კომენტარები + აირჩიეთ, რომელი კომპონენტები გამოჩნდეს კომენტარების სექციაში + საზოგადოების წესების დამალვა + კონტექსტური შეტყობინების დამალვა + ემოჯი ღილაკის დამალვა + ინფოს ღილაკის დამალვა + დროის აღნიშვნის ღილაკის დამალვა + კროსფეიდი + ტრეკების გლუვად გადაბმა ერთმანეთში + კროსფეიდის ჩართვა + გაუფერულების მრუდი + თანაბარი სიმძლავრე + მსუბუქი შეკავება + რბილი გადასვლა + გლუვი S-მრუდი + გამავალი + შემავალი + კროსფეიდის ხანგრძლივობა + 250 მილიწამი + 500 მილიწამი + 750 მილიწამი + 1 წამი + 2 წამი + 3 წამი + 4 წამი + 5 წამი + 6 წამი + 7 წამი + 8 წამი + 9 წამი + 10 წამი + 11 წამი + 12 წამი + Crossfade-ი ხელით გადართვისას + გამოიყენებს Crossfade-ის ეფექტს, როდესაც შემდეგ ტრეკზე ხელით გადართავთ + Crossfade-ი ავტომატური გადართვისას + გამოიყენებს crossfade ეფექტს, როდესაც ტრეკი ბუნებრივად სრულდება + სესიის კონტროლი + shuffle ღილაკზე დააჭირეთ დიდხანს crossfade-ის შეჩერებისთვის ან განახლებისთვის + ამ სესიისთვის Crossfade-ი შეჩერებულია + Crossfade-ი განახლდა + ვიდეო რეჟიმი недоступებელია Crossfade-ის ჩართვისას + Crossfade გამორთულია SABR დაკვრისას + Crossfade გამორთულია ტრანსლაციისას + როგორ მუშაობს + "Crossfade ერთ სიმღერის დასრულებას ओवरლაპს მომდევნოს დასაწყისთან, თქვენი ხანგრძლივობისა და მრუდის გამოყენებით + +ის მუშაობს ავტომატურ გადასვლაზე და მანუალური გადახტომისას, თითოეულს თავისი ჩართვის ღილაკი აქვს. ავტომატური გადასვლისას შერწყმა სიმღერის დასრულებამდე რამდენიმე წამით ადრე იწყება" + შეზღუდვები + "მუსიკალური ვიდეოები მხოლოდ აუდიოს სახით უკრავს, როდესაც crossfade ჩართულია. გრძლად დააჭირეთ shuffle-ს crossfade-ის გასაჩერებლად და ვიდეოს სანახავად + +მუსიკალური ვიდეო, რომელიც იწყებს დაკვრას, რჩება ვიდეო რეჟიმში მომდევნო სიმღერამდე + +სიმღერები, რომლებიც crossfade-ის ხანგრძლივობაზე მოკლეა, არ გადადის crossfade-ში + +დაბლოკვის ეკრანმა ან შეტყობინებამ შესაძლოა გარდამავალ პროცესში მოკლედ აჩვენოს მოძველებული მდგომარეობა + +crossfade-ის შეჩერებამ მუსიკალური ვიდეოს სანახავად შესაძლოა ზოგიერთი ვიდეო გაშავოს. სიმღერის არჩევამდე crossfade-ის შეჩერება ამის თავიდან აცილებას უზრუნველყოფს" + არ არის მხარდაჭერილი + "აუდიოს გადაცემა ან სარკისებური ასახვა (Chromecast, ეკრანის ან აუდიოს სარკისებური ასახვა) + +SABR დაკვრა (ვიდეო ნაკადების იმიტაცია გამორთულია)" + კრედიტები + Crossfade ფუნქცია VazerOG-სგან + გაიმეორების მდგომარეობის запоმინება + გამოიხსენე არევის მდგომარეობა + უკუკავშირის სიჩქარე + სიჩქარე, რომელიც გამოიყენება ყველა ტრეკისთვის. 100%-ის შემთხვევაში გამოიყენება აპლიკაციის სიჩქარე, მაგალითად, პოდკასტის დაკვრის სიჩქარე + ტონალობის შეცვლა სიჩქარესთან ერთად + არმოწონების გადამისამართების გამორთვა + თავიდან აგვარდინებს შემდეგ ტრეკზე გადასვლას \"არ მომწონს\" ღილაკზე დაჭერისას + პლეილისტის ტრეკზე დაჭერის მოქმედება + პლეილისტის დაკვრა + მხოლოდ არჩეული ტრეკის დაკვრა + დაამატეთ არჩეული ტრეკი რიგში + დაემატა რიგში + Cast ღილაკის დამალვა + ისტორიის ღილაკის დამალვა + შეტყობინების ღილაკის დამალვა + ძებნის ღილაკის დამალვა + ხმოვანი ძიების ღილაკის დამალვა + ბგერის ძებნის ღილაკის დამალვა + ახლის ღილაკის დამალვა + მალავს \"ახლის\" ღილაკს ბიბლიოთეკის ჩანართში + მოქმედების ღილაკები + აირჩიეთ, რომელი მოქმედების ღილაკები გამოჩნდეს პლეერის ქვემოთ + მოქმედებების პანელის დამალვა + მოწონებისა და არმოწონების ღილაკების დამალვა + კომენტარების ღილაკის დამალვა + დამალეთ ღილაკი \"ჩატის რეპლეი\" + ტექსტების ღილაკის დამალვა + გაგვიზიარეთ ღილაკის დამალვა + შენახვის ღილაკის დამალვა + გადმოწერის ღილაკის დამალვა + დამალეთ ღილაკი \"რადიოს ჩართვა\" + გასაშლელი მენიუ + აირჩიეთ, რომელი კომპონენტები გამოჩნდეს პლეიერისა და რიგის გასაშლელ მენიუებში + დამალეთ 3-სვეტიანი კომპონენტი + მოიხსნათ მოწონებისა და არმოწონების ღილაკები + დამალეთ მენიუ \"გემოვნების შესაბამისობა\" + დამალეთ მენიუ \"დაამატეთ სიაში \'მოგვიანებით მოსასმენი\'\" + დამალეთ მენიუ \"დაამატეთ რიგში\" + მოიხსნათ სუბტიტრების მენიუ + დამალეთ მენიუ \"წაშალეთ ფლეილისტი\" + დამალეთ მენიუ \"გაასუფთავეთ რიგი\" + დამალეთ მენიუ \"ნუ მომირჩიეთ ეს არტისტი\" + მოიხსნათ ჩამოტვირთვის მენიუ + დამალეთ მენიუ \"ప్ლეილისტის რედაქტირება\" + დამალეთ მენიუ \"გადასვლა ალბომზე\" + დამალეთ მენიუ \"გადასვლა არტისტზე\" + დამალეთ მენიუ \"გადასვლა ეპიზოდზე\" + დამალეთ მენიუ \"გადასვლა პოდკასტზე\" + მოიხსნათ დახმარების მენიუ + დამალეთ მენიუ \"მონიშნეთ ეპიზოდი როგორც მოსმენილი\" + დამალეთ მენიუ \"не დაინტერესებული\" + დამალეთ მენიუ \'მიმაგრება სწრაფი ამორჩევითი განყოფილების\' + 숨기기 \'შემდეგის დაკვრა\' მენიუ + მოიხსნათ ხარისხის მენიუ + 숨기기 \'ბიბლიოთეკიდან წაშლა\' მენიუ + 숨기기 \'ფლეილისტიდან წაშლა\' მენიუ + მოიხსნათ შეტყობინების მენიუ + 숨기기 \'ეპიზოდის შენახვა მოგვიანებითთვის\' / \'ბიბლიოთეკაში შენახვა\' მენიუ + 숨기기 \'ფლეილისტში შენახვა\' მენიუ + გაგვიფარულდეს გაზიარების მენიუ + 숨기기 \'გაურandomებული დაკვრა\' მენიუ + 숨기기 \'ძილის ტაიმერი\' მენიუ + დამალეთ მენიუ \"გაააქტიურეთ რადიო\" + დამალეთ მენიუ \"სტატისტიკა ენთუზიასტებისთვის\" + მოიხსნათ გამოწერის მენიუ + დამალეთ მენიუ \"მოხსენით სწრაფი წვდომიდან\" + დამალეთ მენიუ \"ნახეთ სიმღერის ავტორები\" + საწყისი გვერდი + ნაგულისხმები + ჩარტები + ეპიზოდები მოგვიანებითთვის + კვლევა + ისტორია + ბიბლიოთეკა + მოწონებული მუსიკა + ფლეილისტები + ძებნა + გამოწერები + პორტრეტული ორიენტაციის იძულება + ინახავს აპლიკაციას პორტრეტულ ორიენტაციაში მოწყობილობის დატრიალებისას + მინიპლეერის ფერის შეცვლა + მინიპლეიერის ფერს ავსებს სრულეკრანიანი პლეიერის ფერამდე + შეცვალეთ навіგაციის პანელის ფერი + ანესაბამებს навіგაციის პანელის ფერს სრულეკრანიან პლეერს + შემდეგ ღილაკის ჩვენება მინიპლეერში + წინა ღილაკის ჩვენება მინი-პლეერში + მინიპლეერის იძულებითი ჩართვა + თავიდან აგდებს პლეიერის ავტომატურ გაფართოებას სრულეკრანზე ახალი ტრეკის დაწყებისას + ჩართეთ მინი-პლეიერის გადატაცებით დახურვა + აძლევს საშუალებას დახუროთ მინი-პლეიერი მასზე ქვემოთ გადატაცებით + ნავიგაციის პანელი + აირჩიეთ, რომელი ღილაკები გამოჩნდეს ქვედა навіგაციის პანელში + მთავარი ღილაკის დამალვა + Samples ღილაკის დამალვა + დამალეთ ღილაკი „კვლევა“ + დამალეთ ღილაკი „ბიბლიოთეკა“ + დამალეთ ღილაკი „განახლება“ + ნავიგაციის პანელის დამალვა + ნავიგაციის ღილაკების წარწერების დამალვა + არ ხართ ავტორიზებული. კონფიგურაციისთვის დააჭირეთ + გამოვლა + გამოვლა მოხდა + ჩართეთ scrobbling + გაგზავნეთ \"ახლა უკრავს\" სტატუსი + სიმღერის მინიმალური ხანგრძლივობა + ამაზე მოკლე სიმღერები არ იქნება scrobbled + Scrobble-ის დაგვიანების პროცენტი + გააქტიურეთ scrobble სიმღერის ამ პროცენტის მოსმენის შემდეგ + Scrobble-ის დაგვიანების წამები + გააქტიურეთ scrobble ამ რაოდენობის წამის მოსმენის შემდეგ + ListenBrainz-ის ანგარიში + ავტორიზებული. შეხეთქეთ ანგარიშის სამართავად + შეიყვანეთ თქვენი ListenBrainz-ის მომხმარებლის ტოკენი scrobbling-ის გასააქტიურებლად + ტოკენი + ვერიფიკაცია + გთხოვთ შეიყვანოთ ტოკენი + ვალიდაცია... + ვალიდური ტოკენია! მომხმარებელი: %s + არავალიდური ტოკენია: %s + უცნობი + ვერიფიკაცია ჩაიშალა: %s + ტოკენი წაშლილია + ტოკენი შენახულია + გაფრთხილება: შენახული ტოკენი არასწორია! + მოიყვარეთ მოწონებული ტრეკები + ავტომატურად მოიყვარეთ ტრეკები Last.fm-ზე, როდესაც მათ მოიწონებთ YouTube Music-ზე (და წაშალეთ სიყვარული, როდესაც მოიხსნით მოწონებას ან აღნიშნავთ, რომ არ მოგწონთ) + Last.fm ანგარიში + ავტორიზებულია როგორც %s. დააჭირეთ ანგარიშის სამართავად + ავტორიზაციისთვის შეიყვანეთ თქვენი Last.fm მონაცემები + მომხმარებლის სახელი + პაროლი + შესვლა + გთხოვთ, შეიყვანოთ მომხმარებლის სახელი და პაროლი + ავტორიზაცია... + წარმატებით შეხვედით! + Last.fm-იდან სესიის მიღება ვერ მოხერხდა + შესვლა ვერ მოხერხდა: %s + Last.fm-ის მონაცემები შენახულია! + მეტამონაცემების გასუფთავება + გასუფთავეთ მეტამონაცემები + სკრობლინგამდე სათაურებიდან, შემსრულებლებიდან და ალბომებიდან ავტომატურად წაშალეთ გავრცელებული ზედმეტი ტექსტები (მაგ. \"Official Video\", \"Remaster\", \"Live\") + Custom regex გასუფთავება + შეიყვანეთ თქვენი რეგულარული გამოსახულება სულთებიდან, შემსრულებლებიდან ან ალბომებიდან დამატებითი ტექსტის მოსაშორებლად. მაგალითად: (?i)\\s*\\((clean|explicit)\\) + გამოყავით შემსრულებელი და ტრეკი სათაურიდან + ამოიღეთ შემსრულებლისა და ტრეკის სახელი ვიდეოს სათაურიდან \" - \" გამყოფი ნიშნით. სასარგებლოა ხელახლა ატვირთული კონტენტისთვის, სადაც არხის სახელი შეიძლება იყოს გამოყენებული როგორც შემსრულებელი + აკლებული ალბომის შევსება + როდესაც მუსიკას არ ახლავს ალბომი, მოძებნეთ იგი Last.fm-ზე, რათა თქვენი scrobble იყოს სრული + "scrobbling ავტომატურად ასინქრონიზებს თქვენს YouTube Music-ის გამოყენებას Last.fm-ის ან ListenBrainz-ის პროფილთან + +იმისათვის, რომ ფონურ რეჟიმში დაკვრისას scrobbling სინქრონიზებული იყოს, დარწმუნდით, რომ YouTube Music-ის აპლიკაციის ელემენტის გამოყენება დაყენებულია როგორც \"შეზღუდვების გარეშე\"" + მესამე მხარის ტექსტების გამოყენება + აჩვენებს ტექსტებს მესამე მხარის სერვისიდან; თუ ტექსტები ვერ მოიძებნა, გამოიყენება ჩაშენებული ტექსტები + ეკრანის ჩართვა ტექსტის ნახვისას + თავიდან აგდებს ეკრანის გამორთვას, სანამ ტექსტის პანელი ღიაა + ტექსტების სერვისი + ჩვენება + სინქრონიზაცია + პანელი + AI ენდპოინტი + ყველა პროვაიდერი გათიშულია - ჩაშენებული და ინტეგრირებული ტექსტები + მ sağlayებლების თანმიმდევრობის განახლება? + მ sağlayებლების პრიორიტეტების აღდგენა ნაგულისხმევ მდგომარეობაში + შენახული ტექსტების გასუფთავება + წაშალეთ ამ მოწყობილობაზე შენახული ტექსტები + გასუფთავებულია %1$s მბ + ტოკენის მიღება + Apple Music Media-user-token + "ჩასვით media-user-token ქუქი music.apple.com-იდან + +ნაბიჯები: +1. გაიარეთ ავტორიზაცია music.apple.com-ზე როგორც წევრი მომხმარებელი +2. გახსენით DevTools → Application → Cookies → music.apple.com +3. დააკოპირეთ \"media-user-token\"-ის მნიშვნელობა" + Apple Music: media-user-token შენახულია + Apple Music: media-user-token წაშლილია + Apple Music: არასწორი media-user-token - გთხოვთ შეამოწმოთ და სცადოთ ხელახლა + Spotify SpDc + "ჩასვით sp_dc ქუქი Spotify-დან. + +ნაბიჯები: +1. გაიარეთ ავტორიზაცია open.spotify.com-ზე +2. გახსენით DevTools → Application → Cookies → open.spotify.com +3. დააკოპირეთ \"sp_dc\"-ის მნიშვნელობა" + Spotify: sp_dc შენახულია + Spotify: sp_dc წაშლილია + Spotify: არასწორი sp_dc - გთხოვთ შეამოწმოთ და სცადოთ ხელახლა + Apple Music-ის ტოკენი არასავალდებულოა. მის გარეშე, სათაურებისთვის გამოყენებული იქნება Lyrically + Deezer ARL + "ჩასვით arl ქუქი Deezer-იდან. + +ნაბიჯები: +1. გაიარეთ ავტორიზაცია deezer.com-ზე +2. გახსენით DevTools → Application → Cookies → deezer.com +3. დააკოპირეთ \"arl\"-ის მნიშვნელობა" + Deezer: arl შენახულია + Deezer: arl გასუფთავდა + Deezer: არასწორი arl - გთხოვთ შეამოწმოთ და სცადოთ თავიდან + Musixmatch-ის ტოკენი + დაამატეთ ტოკენი მხოლოდ საკუთარი ანგარიშის გამოსაყენებლად. ის უნდა იყოს აღებული Musixmatch-ის Android აპლიკაციიდან: musixmatch.com-დან კოპირებული ტოკენები ეკუთვნის მოძველებულ დესკტოპ კლიენტს და API მათ უარყოფს + Musixmatch მუშაობს ტოკენის გარეშეც, ამიტომ ეს ველი შეიძლება დარჩეს ცარიელი + მომხმარებლის ტოკენი + Musixmatch: მომხმარებლის ტოკენი შენახულია + Musixmatch: მომხმარებლის ტოკენი გასუფთავდა + Musixmatch: მომხმარებლის არასწორი ტოკენი - გთხოვთ შეამოწმოთ და სცადოთ თავიდან + YouTube-ის ქუქიები + "ჩასვით თქვენი YouTube-ის ქუქიები, რათა მოიძიოთ სუბტიტრები და დროის ტექსტი YouTube-იდან. + +ნაბიჯები: +1. გახსენით youtube.com ბრაუზერში და გაიარეთ ავტორიზაცია +2. გახსენით DevTools (F12) → Application → Cookies → youtube.com +3. დააკოპირეთ ქუქიების სრული სტრიქონი (გას separating semicolons-ით გამოყოფილი key=value წყვილები) + +საჭირო ქუქიები: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPISID, LOGIN_INFO" + YouTube: ქუქიები შენახულია + YouTube: ქუქიები წაშლილია + YouTube: არასწორი ქუქი-ფაილები - გთხოვთ შეამოწმოთ და სცადოთ თავიდან + დააჭირეთ და გეჭირავთ ხაზს + შეეხეთ ხაზს სიმღერის შესაბამის მომენტზე გადასასვლელად, გრძლად დააჭირეთ თითისქვეშა სიტყვის დასაკოპირებლად, ხოლო თუ დაჭერა არ მოხდა სიტყვაზე, კოპირდება მთლიანი ხაზი. გამორთვა რთავს ორივე ჟესტს + სექციების ტეგების ჩვენება + ტექსტის თითოეულ ახალ სექციას მონიშნეთ პატარა ტეგით, მაგალითად: კუპლეტი ან გუნდი + კოპირების ღილაკის ჩვენება + შეეხეთ ორიგინალი ტექსტის ჩვეულებრივ ტექსტად დასაკოპირებლად; გრძლად დააჭირეთ ტექსტის საწყის ფაილს ჩამოტვირთვებში შესანახად + თარგმნის ღილაკის ჩვენება + დააჭირეთ თარგმანის ჩასართავად/გამოსართავად; გრძელვად დააჭირეთ მხოლოდ თარგმანისთვის. გამოიყენება პროვაიდერის თარგმანები, Google Translate ან OpenAI-ს თავსებადი API + რომაიზაციის ღილაკის ჩვენება + დააჭირეთ რომანიზაციის ჩასართავად/გამოსართავად; გრძელვად დააჭირეთ მხოლოდ რომანიზაციისთვის. გამოიყენება პროვაიდერის რომანიზაცია, Google Translate ან OpenAI-ს თავსებადი API + ლოკალური რომანიზაცია და გასწორება + გახელვეთ რომანიზაცია მოწყობილობაზე, როდესაც პროვაიდერი, თქვენი მოდელი და Google Translate ვერ იმუშავებენ. წაკითხვა მიყვება სინქრონიზებული ტექსტის თითოეულ სიტყვას; სიტყვების სინქრონიზაცია უნდა იყოს ჩართული. ჩინური, კანტონური, იაპონური, კორეული, ბერძნული, რუსული, ქართული, სომხური; დაახლ. 120 მბ + კითხვა %1$s: %2$d%% + მონაცემები მზადაა (%1$d ჩანაწერი) + ჩამოტვირთვა ვერ მოხერხდა - სცადეთ ხელახლა ამ ფუნქციის გამორთვა და ჩართვა + განახლების ღილაკის ჩვენება + დააჭირეთ შემდეგი ტექსტის ვარიანტისთვის; გრძელვად დააჭირეთ ტექსტის მოსაძებნად სიმღერის სათაურისა და შემსრულებლის მიხედვით + ტექსტების ძიება + სიმღერის სათაური + მომღერალი + შეიყვანეთ სიმღერის სათაური და მომღერალი + აქტივურ პროვაიდერებს არ შეუძლიათ სხვა სიმღერების ძიება + შენახულია + სინქრონიზებული სიტყვების ან მარცვლების გამოკვეთა + უპირატესობა მიენიჭება სიტყვა-სიტყვა ტექსტს, როდესაც ის ხელმისაწვდომია. როდესაც ტექსტს აქვს სიტყვების ტაიმინგი, მუსიკის ტემპთან სინქრონულად გამოიკვეთება თითოეული სიტყვა ან მარცვა + გასათამაშები ტექსტის დამალვა + ტექსტის იმ ხაზების დამალვა, რომლებიც იმყოფება მიმდინარე სათამაშო ხაზის ზემოთ + დამალეთ განუთამაშებელი ტექსტი + დამალეთ ტექსტის სტრიქონები მიმდინარე სტრიქონის ქვემოთ + აჩვენე MediaSession-ის ტექსტები ჩაკეტილ ეკრანზე + აჩვენებს მიმდინარე ხაზს ბლოკირების ეკრანზე, Android Auto-სა და Bluetooth-ზე. საჭიროა Android 11 ან უფრო ახალი ვერსია + აჩვენე მინი-პლეიერის ტექსტები + აჩვენებს მიმდინარე ხაზს მინი-პლეიერში, ხოლო ტრეკი გადატანილია ქვესათაურში + სიმღერის სათაურის/შემსრულებლის ფორმატი + ტრეკის ფორმატი, რომელიც ჩანს ტექსტთან ერთად მინიპლეერში და MediaSession-ში. {t} არის სათაური, {a} არის შემსრულებელი და {v} არის ვერსია, მაგალითად Acoustic, თუ ტექსტის წყარო ამის მიწოდებას ახდენს. ორი ორწერილს შორის მოქცეული ტექსტი ჩანს მხოლოდ მაშინ, როდესაც მასში არსებულ ყველა ცვლადს აქვს მნიშვნელობა; ჩვეულებრივი ორწერილისთვის გამოიყენეთ \\:. თითოეული ხაზი ჩნდება რიგრიგად, ტექსტის შეცვლისთანავე + ტექსტის ხაზის ფორმატი + სიმღერის ტექსტის ხაზის ფორმატი მინიპლეერში და MediaSession-ში. {lrc} არის ხაზი და {n} არის ის, თუ რამდენჯერ იმღერა ის ზედიზედ. ორწერილებს შორის მოქცეული ტექსტი ჩნდება მხოლოდ ხაზის განმეორებისას. ორწერილისთვის გამოიყენეთ \\: + ფორმატი უნდა შეიცავდეს {lrc} + არტისტის ხაზის შაბლონი და ანიმაციის დრო + ეკრანზე wyświetების მინიმალური დრო (მს) + ეკრანზე wyświetების მაქსიმალური დრო (მს) + მხოლოდ მრავალხაზიანი ანიმაციებისთვის. შეინარჩუნეთ თითოეული კადრი მინიმუმ მინიმალური დროის განმავლობაში. მაქსიმალურის შემთხვევაში, გადაიტანეთ არტისტის ხაზი მაშინაც კი, როდესაც ტექსტი იგივე რჩება. + მინიმუმი უნდა იყოს 0 ან მეტი. მაქსიმუმი უნდა იყოს მეტი 0-ზე და მინიმუმ მინიმალურის ტოლი. + ხაზის სიგრძის შეზღუდვები + ტექსტისა და არტისტის ხაზის შეზღუდვები და სრული სიგანის სიმბოლოების წონა + "ლირიკის ხაზის ლიმიტი +გაყავით გრძელი ლირიკა ბალანსირებულ ნახევრად. 0 გამორთავს გაყოფას." + "არტისტის ხაზის ლიმიტი +გაყავით შევსებული ტრეკის ფორმატი ციკლურ კადრებად. 0 გამორთავს გაყოფას." + "სრული სიგანის სიმბოლოს წონა +გამოიყენება ორივე ლიმიტისთვის. 1.75-ის შემთხვევაში, ოთხი სრული სიგანის სიმბოლო ჩაითვლება როგორც 7." + შეიყვანეთ მთელი რიცხვის ლიმიტები 0-დან 10000-მდე და სიმბოლოს წონა, რომელიც მეტია 0-ზე + ინსტრუმენტული პაუზა + ჩანს მინი-პლეიერში და MediaSession-ში ინსტრუმენტული პაუზის დროს. თითოეული ხაზია ერთი საანიმაციო კადრი. {t} არის სათაური, {a} არის არტისტი და {lrc} არის ლირიკის ბოლო ხაზი + მინიპლეიერისა და MediaSession-ის დროის განზომილების განზუსტება + აისმ desplazamiento იმის, თუ როდის გამოჩნდება შემდეგი ხაზი მინიპლეერში და MediaSession-ში. გამოიყენეთ უარყოფითი მნიშვნელობა, თუ ხაზები გვიან ჩნდება, მაგალითად, დაბლოკილ ეკრანზე + ფილტრები + მეტამონაცემების regex + რეგულარული გამოსახულება, რომელიც გამოიყენება სათაურების, შემსრულებლებისა და ალბომების მიმართ ტექსტების ძებნამდე და ორი სათაურის შესაბამისობის შეფასებისას. გამორთვას სულ რაღაც ცარიელი დატოვებით. ნაგულისხმევი პარამეტრი აქრობს გავრცელებულ ზედმეტ ტექსტებს, როგორიცაა \"Official Video\", \"Remaster\", \"【插曲】\" და \"- Topic\" + ტექსტის ფილტრის regex + რეგულარული გამოსახულება, რომელიც გამოიყენება მხოლოდ ორიგინალი ტექსტისთვის (არა თარგმანებისთვის ან რომანიზაციისთვის). გამოსართავად დატოვეთ ცარიელი + ინფორმაციის ხაზის მარკერები + თუ ტექსტი არასწორად ფილტრდება ქვედა ნაწილში, შეცვალეთ ან წაშალეთ ეს პარამეტრი. მძიმით გამოყოფილი მარკერები: ხაზები, რომლებიც შეიცავს მარკერს და მას მოსდევს ორწერტილი, ჩანს როგორც საკრედიტო ინფორმაცია ქვემოთ. გამორთვას დატოვეთ ცარიელი + პირველი ასოს დაწერა დიდი ასოთი + ტექსტის თითოეული ხაზის პირველი ასოს ხდის დიდს + ბოლო სასვენი ნიშნების მოცილება + შესაბამისად აქრობს წერტილებს, მძიმეებს, ორწერტილებსა და წერტილ-მძიმეებს ტექსტის ხაზების ბოლოს. კითხვის ნიშნები, ძახილის ნიშნები, მრავალწერტილები, ბრჭყალები და ფრჩხილები რჩება + ტექსტის ზომა + სათytuላო ტექსტის ზომა + გლობალური დროის зміწევა + სინქრონიზებული ტექსტი მიყვება დაკვრას. თუ ისინი ს deviates, შეცვალეთ დროის зміდან. გადაწიეთ სინქრონიზებული ტექსტი წინ ან უკან. გამოიყენეთ დადებითი მნიშვნელობა, თუ ტექსტი სულს უსწრებს + გამოიყენეთ SponsorBlock სეგმენტები ტექსტის зміწევისთვის + შესაბამისობის მოძებნა SponsorBlock-ის მიერ გამოტოვებული სეგმენტების გარეშე და ტექსტის გადატანა გამოშვებულ აუდიოზე. გამორთვ absence-ის შემთხვევაში SponsorBlock სრულად იგნორირდება + ამ ტრეკს არ აქვს ვიდეოს ID გამოსაგზავნად + წყარო: %s + გააწყვეტინეთ კოპირების ღილაკს ამ LRC-ის შესანახად; თუ შეძლებთ ტაიმინგების დაზუსტებას და მათ გაზიარებას საზოგადოების პროვაიდერებისთვის, მადლობელი ვიქნებით + თარგმნილია Google-ის მიერ + რომანიზებულია Google-ის მიერ + თარგმნილია %s-ის მიერ + რომანიზირებულია %s-ის მიერ + რომანიზებულია %s-ის მიერ + თარგმნილია + თარგმანის ენა + რომანიზაციის ენა + თარგმნა... + სათytuላო ტექსტის თარგმნა ვერ მოხერხდა + რომანიზებულია + ავტომატური + რომანიზება... + ტექსტის რომანიზება ვერ მოხერხდა + მხოლოდ თარგმნილი + მხოლოდ რომანიზებული + ორმაგი რეჟიმი + გადაკოპირებულია ბუფერში + განახლება + განახლებულია + QuickLRC API გასაღები + QuickLRC ახდენს ტრეკის ტექსტის ტრანსკრიფციას ან დროში გასწორებას. ეს არის ცალკე სერვისი, რომელიც არ არის დაკავშირებული ზემოთ მოცემულ OpenAI-ს თავსებად ენდპოინტთან + QuickLRC გასაღები შენახულია. გამოიყენეთ გასწორების ღილაკი ტექსტის ძიების დიალოგში, ან პანელის შეტყობინება, როდესაც ტრეკს არ აქვს ტექსტი + QuickLRC API გასაღები შენახულია + QuickLRC-მა არ მიიღო ეს გასაღები + QuickLRC API გასაღები წაიშალა + QuickLRC გასწორებს ტექსტებს, რომლებსაც არ აქვთ დროის აღნიშვნები, ან ტრანსკრიპირებს მათ ტრეკიდან კრედიტების სანაცვლოდ. გასაღების დაყენების შემთხვევაში, ტრეკები ტექსტის გარეშე შესთავაზებენ ტრანსკრიპციას, ხოლო ძიების დიალოგში გამოჩნდება სინქრონიზაციის ღილაკი + მოითხოვეთ QuickLRC გასწორება + გასწორება 진행վումაა + გასწორების გამოყენება + მოითხოვეთ QuickLRC ტრანსკრიფცია + ტრანსკრიფცია 진행დება + ტრანსკრიფცია გაუქმა + ტრანსკრიფცია შესაძლებელია მხოლოდ YouTube-დან ჩართული ტრეკებისთვის + სინქრონიზაციისთვის ტექსტი არ მოიპოვება - ტრანსკრიპირება მიმდინარეობს + QuickLRC-მა ვერ დაამუშავა ეს ტრეკი + ამ ტრეკისთვის QuickLRC-ის კრედიტები ამოწურულია + "ტექსტები სრულდება სათაურისა და შემსრულებლის მიხედვით; უჩვეულო მეტამონაცემებმა შესაძლოა არაფერი იპოვოს ან არასწორი ვერსია მოიძიოს. ვერსიების შესაცვლელად გამოიყენეთ განახლება. * ნიშნით მონიშნული პროვაიდერები საჭიროებენ ტოკენს. ყველა ჩართული პროვაიდერი ყოველთვის თქვენს მიერ განსაზღვრული თანმიმდევრობით არ მოითხოვება, არამედ პასუხობენ მათი სისწრაფის მიხედვით.\n\nტექსტები მოდის მესამე მხარის სერვისებიდან, რომლებიც არ არიან დაკავშირებულნი Morphe-თან, არ არიან მისი სპონსორები ან რეკომენდებული. თითოეული მათგანი გამოიყენება ისე, როგორც არის: ტექსტები, მათი ხელმისაწვდომობა და ლიცენზირება რჩება ამ სერვისებსა და მათ ლიცენზიარებს, ხოლო მათი გამოყენება შესაძლოა ექვემდებარებოდეს მათ მომსახურების პირობებს. ჩართულ სერვისამდე აღწევს მხოლოდ თქვენ მიერ მოთხოვნილი მოძიება - სათაური, შემსრულებელი და, იქ სადაც სერვისს ეს სჭირდება, ვიდეოს ID. სურვილისამებრ AI ენდპოინტი და QuickLRC ტრანსკრიფცია მუშაობს მხოლოდ თქვენი კონფიგურაციის შემდეგ და იღებს ტექსტის ტექსტს ან ტრეკის ვიდეო URL-ს." + გააქტიურეთ მხოლოდ ის პროვაიდერები, რომლებსაც იყენებთ, ან დატოვეთ ნაგულისხმევი პარამეტრები - მუდმივმა მოთხოვნებმა შესაძლოა გადატვირთოს პატარა საზოგადოებო ტექსტების საიტები + მფლობელის ხაზის დამალვა + დამალეთ ტექსტის ქვემოთ მოცემული მფლობელის ხაზი + თარგმანისა და რომანიზაციის ადგილების შეცვლა + ON განათავსებს რომანიზაციას ტექსტის ქვემოთ და თარგმანს ზემოთ; OFF ტოვებს რომანიზაციას ზემოთ და თარგმანს ქვემოთ + თარგმანი + რომანიზაცია + თარგმანის პროვაიდერი + Google (ნაგულისხმევი, უფასო) + OpenAI თავსებადი (საკუთარი) + რომანიზაციის პროვაიდერი + Google (ნაგულისხმევი) + ლოკალური / ოფლაინ ლექსიკონი + OpenAI თავსებადი (საკუთარი) + AI სექცია + გამოიყენება მხოლოდ მაშინ, როდესაც პროვაიდერი არ გვიგზავნის სექციის ეტიკეტებს, როგორიცაა Chorus ან Verse, რომლებსაც შემდეგ თქვენი OpenAI-ს თავსებადი მოდელი აღმოაჩენს + AI & ღრუბლოვანი Endpoints-ები + გააკონფიგურირეთ API გასაღებები, მოდელები, endpoint-ები და prompt-ის შაბლონები AI პროვაიდერებისთვის. + API endpoint-ის URL (მაგ. https://api.openai.com/v1/chat/completions) + პრომპტის შაბლონი + პლეისჰოლდერები: {language}, {task}, {title}, {artist}, {count}, {lines} + პრომპტი შენახულია + პრომპტი შენახულია, მაგრამ AI endpoint ჯერჯერობით მიუწვდომელია. ჯერ დააკონფიგურირეთ იგი AI პარამეტრებში + პრომპტი გასუფთავდა + არასწორი JSON. გამოსწორეთ ან გასუფთავეთ ველი შენახვამდე + თარგმნისა & რომანიზაციის პარამეტრები შენახულია + პარამეტრები განახლდა ნაგულისხმევ მდგომარეობაში + ბაზისური URL უნდა იწყებოდეს http:// ან https:// + კავშირის შემოწმება + Endpoint-ების კონფიგურაცია + ყველა End-point-ის ჩვენება + კავშირის ვალიდაცია… + თარგმანის პროვაიდერი + რომანიზაციის პროვაიდერი + მოდელების წამოღება + მოდელების ჩატვირთვა… + აირჩიეთ მოდელი + მოდელების ჩატვირთვა ვერ მოხერხდა. შეამოწმეთ API გასაღები ან კავშირი. + Gemini API პარამეტრები + OpenAI თავსებადი (საკუთარი End-point) + DeepL API პარამეტრები + API გასაღები + მოდელი + ბაზისური URL + API გასაღები (არასავალდებულო) + თარგმანის პრომპტი + მოთხოვნის დამატებითიტექსტი (არასავალდებულო JSON) + ავთენტიფიკაციის გასაღები (Free ან Pro) + API გასაღებების, მოდელების, ენდპოინტების და პრომპტების კონფიგურაცია + 8.23.51 - სქელი ხაზის ღილაკების გამორთვა + 9.13.51 - აღდგინეთ პლეერის შემდეგი, ტექსტებისა და დაკავშირებული ჩანართები diff --git a/patches/src/main/resources/addresources/values-ka-rGE/reddit/strings.xml b/patches/src/main/resources/addresources/values-ka-rGE/reddit/strings.xml index 433a84e61b..486405d9d4 100644 --- a/patches/src/main/resources/addresources/values-ka-rGE/reddit/strings.xml +++ b/patches/src/main/resources/addresources/values-ka-rGE/reddit/strings.xml @@ -16,6 +16,122 @@ Second \"item\" text" --> + Reddit-ის კონფიდენციალობის პოლიტიკა + Google-ით ავტორიზაცია მიუწვდომელია. შესასვლელად გამოიყენეთ ელ-ფოსტა + რეკლამები + ფიდის რეკლამების დამალვა + კომენტარების რეკლამების დამალვა + ნავიგაციის პანელი + პასუხების ღილაკის დამალვა + ჩატის ღილაკის დამალვა + შექმნის ღილაკის დამალვა + აღმოჩენის ან საზოგადოებების ღილაკის დამალვა + თამაშების ღილაკის დამალვა + გვერდითი პანელი + ბოლო დროს ნანახი თაროს დამალვა + Reddit-ზე თამაშების თაროს დამალვა + Reddit Pro თაროს დამალვა + შესახებ თაროს დამალვა + რესურსების თაროს დამალვა + განლაგება + ტექსტის კოპირება + საშვები პოსტებსა და კომენტარებში ტექსტის არჩევითა და კოპირებით. თიშავს კომენტარების ჩაკეცვას + კომენტარების ჩაკეცვა შეწყდება „ტექსტის კოპირების“ ჩართვისას + მორგებული ფონტი + ჩატვირთეთ მორგებული TTF ან OTF ფონტის ფაილი ქვემოთ მოცემული გზიდან. ამ ფუნქციის ჩართვა ასევე გამორთავს სისტემური ფონტის იძულებით გამოყენებას + მორგებული ფონტის ფაილი + დააჭირეთ TTF ან OTF ფონტის ფაილის ასარჩევად + შერჩეული ფონტი - %s + ჯერ აირჩიეთ მორგებული ფონტის ფაილი + გთხოვთ, აირჩიოთ .ttf ან .otf ფონტის ფაილი + მორგებული ფონტის ჩატვირთვა ვერ მოხერხდა. აირჩიეთ სხვა ფაილი + გათიშოს მთავარი გვერდის თანამედროვე ინტერფეისი + სქრინშოთის ფანჯრის გამორთვა + რთავს იმ ფანჯარას, რომელიც ჩნდება სქრინშოთის გადაღებისას + სისტემური ფონტის იძულებით გამოყენება + გამოიყენეთ მოწყობილობის სისტემური ფონტი Reddit Sans-ის ან Roboto-ს ნაცვლად. ამ ფუნქციის ჩართვა ასევე გამორთავს მორგებულ ფონტს + ღილაკის \"Ask\" დამალვა + მალავს ღილაკს \"Ask\" საძიებო ზოლში + მალავს საზოგადოებების თვრს + მალავს დაკავშირებული ან რეკომენდებული საზოგადოებების თაროს subreddits-ებში + პოპულარული თვრების დამალვა + მალავს პოპულარული თვრებს ფიდიდან და ძიების რეკომენდაციებიდან + შეტყობინების შეთავაზების დიალოგის მოცილება + ავტომატურად ხურავს შეტყობინების შეთავაზების დიალოგის ფანჯარას subreddit-ის მონახულებისას + აჩვენე თეგები მთავარ ფიდზე + აჩვენებს პოსტის თეგებს მთავარ ფიდზე + ფიდის ზოგიერთ ელემენტს შესაძლოა არ ჰქონდეს თეგი + ნახვების რაოდენობის ჩვენება პოსტებზე + შეზღუდვა: ხელმისაწვდომია მხოლოდ იმ შემთხვევაში, თუ API-ს პასუხი შეიცავს ნახვების რაოდენობას + პოსტის მოქმედებათა პანელის შეცვლა + აპЕР 옮깁ს პოსტების ხმის მიცემისა და კომენტარის ღილაკებს მარჯვენა მხარეს, ხოლო კროსპოსტისა და გაზიარების ღილაკებს - მარცხენა მხარეს + ხმის მიცემისა და კომენტარის ღილაკების ადგილების შეცვლა + აჩვენებს პოსტების კომენტარის ღილაკს ხმის მიცემის ღილაკებამდე + NSFW გაფრთხილების დიალოგის მოცილება + ავტომატურად ადასტურებს NSFW კონტენტის გაფრთხილებას subreddit-ის მონახულებისას + სხვადასხვა + შესახებ + პატჩის ვერსია და ოფიციალური ბმულები + გაზიარების ბმულების გასუფთავება + შესაბამისად აშორებს თვალთვალის მოთხოვნის პარამეტრებს გაზიარებულ ბმულებს + ბმულების პირდაპირ გახსნა + უგუგავს გადამისამართების URL-ებს გარე ბმულების გახსნისას + ფიდის მედიის სრული სიგანე + აჩვენებს სურათებსა და ვიდეოებს ფიდსა და პოსტის დეტალებში კიდიდან კიდემდე, გვერდითა დაშორებებისა და მომრგვალებული კუთხეების გარეშე + მედია ჩამტვირთავის გაფერადების ინტენსივობა + ბნელი ფონის სიმძიმე სუბტიტრების უკან სურათებისა და ვიდეოების ნახვისას, 0-დან (ფონის გარეშე) 1-მდე (Reddit-ის სტანდარტი) + შეინარჩუნეთ ფიდის პოზიცია + ინარჩუნებს ფიდს იმავე პოზიციაზე პოსტის, სურათის ან ვიდეოს დახურვის შემდეგ, ნაცვლად იმისა, რომ გახსნილი პოსტი გადაიტანოს ზედა ნაწილში + ფიდის მედიის მაქსიმალური სიმაღლე + სურათების, ვიდეოებისა და გალერეების მაქსიმალური სიმაღლე ფიდში, როგორც მათი სიგანის მრავლადელი. უფრო მაღალი მედია იჭრება. 1.33 არის Reddit-ის ნაგულისხმევი მნიშვნელობა + Redgifs-ის გაშვება აუდიოთი + აშვებს Redgifs-ის ორიგინალ ვიდეოს Reddit-ის ასლის ნაცვლად, რომელსაც ხშირად არ აქვს აუდიო + Redgifs HD ხარისხი + თამაშობს Redgifs-ის HD ვიდეოს. HD ვიდეოები უფრო დიდია და შესაძლოა შეჩერდეს ჩატვირთვისას + ჩამოტვირთვის მდებარეობა + ყველა ანგარიში + Reddit-ის სტანდარტი: სურათები Pictures/Reddit-ში, ვიდეოები Download-ში + საფოლდრე: %s + ფაილის სახელი: %s + Reddit-ის ფაილების სახელები + გამოიყენებს \"ყველა ანგარიშს\" + საკუთარი მდებარეობის გამოყენება + შენახვა + Reddit-ის სტანდარტული საფოლდრეები ნაგულისხმები + ქვესაქაღალდე + დაამატეთ ქვესაქაღალდეები ფაილის სახელში / სიმბოლოს გამოყენებით + ძებნა… + ამ მოწყობილობაზე საქაღალდეების ამ chọn-ის ფუნქცია недоступбеლია + ფაილის სახელი + Reddit-ის ფაილის სახელი + დააჭირეთ ტოკენს მის ჩასასმელად. / ქმნის ქვესაქაღალდეს. უცნობი მნიშვნელობები ჩანაცვლდება \"unknown\"-ით + სურათები: %s + ვიდეოები: %s + შენახვა + გასუფთავება + წაშლა + ყველა ანგარიშის ჩამოტვირთვის მდებარეობის განახლება Reddit-ის ნაგულისხმევი პარამეტრებამდე? + წავშალოთ %s-ის ჩამოტვირთვის მდებარეობა? + ჩამოტვირთვის მდებარეობაში შენახვა ვერ მოხერხდა. ფაილი შენახულია Reddit-ის ნაგულისხმევ საქაღალდეში + ჩამოტვირთვის საქაღალდე აღარ არის ხელმისაწვდომი. ფაილი შენახულია Reddit-ის ნაგულისხმევ საქაღალდეში + ჩამოტვირთული ვიდეოს შენახვა ვერ მოხერხდა + ამ საქაღალდის გამოყენება ჩამოტვირთვისთვის შეუძლებელია + %s (მიმდინარე) + %s (არ არის ავტორიზებული) + ანონიმური (გასული სისტემიდან ან ინკოგნიტო რეჟიმი) + მედია ჩამტვირთავის ზედ layer-ის დამალვა + მალავს სათაურს, ღილაკებს და ვიდეოს მართვის პანელს სურათის ან ვიდეოს გახსნისას. მათი გამოსაჩენად დააჭირეთ მედიას + ღილაკის \"ნახეთ საუბარი\" დამალვა + მალავს ღილაკს \"ნახეთ საუბარი\" მედია ჩამტვირთავის ქვედა ნაწილში + მედია ჩამტვირთავის შავი ფონი + იყენებს შავ ფონს სურათებისა და ვიდეოების უკან მედია ჩამტვირთავში, თემის ფონის ფერის ნაცვლად + ბმულების გარე პროგრამით გახსნა + ხსნის ბმულებს თქვენს ბრაუზერში და არა აპლიკაციის შიდა ბრაუზერში + ჩაკეცილი კომენტარების დამახსოვრება + კომენტარები, რომლებსაც ჩაკეცავთ, ჩაკეცილი დარჩება პოსტის ხელახლა გახსნისას + აპლიკაციის ხატულა + აირჩიეთ აპლიკაციის ხატულა ნაგულისხმები + უცნობია diff --git a/patches/src/main/resources/addresources/values-ka-rGE/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-ka-rGE/shared-youtube/strings.xml index 7c826093c6..c1370fa392 100644 --- a/patches/src/main/resources/addresources/values-ka-rGE/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ka-rGE/shared-youtube/strings.xml @@ -18,51 +18,297 @@ Second \"item\" text" --> + გაშვების ანიმაცია + გამორთული + ფერი + მონოქრომული + + გადატვირთეთ კონფიგურაციის დასასრულებლად + აპლიკაციის სახელი + მუქი თემის ფონი + სუფთა შავი + Material You (სუფთა შავი) + კლასიკური YouTube + მუქი ვარდისფერი + მუქი ლურჯი + მუქი მწვანე + მუქი ყვითელი + მუქი ნარინჯისფერი + მუქი წითელი + საკუთარი მუქი ფონი + განსაზღვრეთ მუქი თემის ფონის ფერი + აპლიკაციის ნაგულისხმევი + Material You (ნეიტრალური) + Material You (ძირითადი) + Material You (მეორადი) + Material You (მესამე) + საკუთარი ჩამოტვირთულები ჩამოტვირთვა რეჟიმი ნაგულისხმები + ჩამოტვირთვა აპლიკაციიდან + გარე ჩამტვირთავი + დამტვირთვის პაკეტის სახელი + თქვენი დაინსტალირებული გარე დამტვირთვის აპლიკაციის პაკეტის სახელი + შეიყვანეთ პაკეტის სახელი + სხვა + აპლიკაცია არ არის დაინსტალირებული + %s არ არის დაინსტალირებული. გთხოვთ, დააინსტალიროთ + "ვერ მოხერხდა დაინსტალირებული აპლიკაციის პოვნა პაკეტის სახელით: %s + +შეამოწმეთ, რომ პაკეტის სახელი სწორია და აპლიკაცია დაინსტალირებულია" + პაკეტის სახელი არ შეიძლება იყოს ცარიელი + საკუთარი + აპლიკაციის იქონი + ორიგინალი + Morphe black + Morphe dark + Morphe light + Morphe play light + Morphe play black + საკუთარი + "ეს ცვლის აპლიკაციების გამშვებში გამოჩენილ ხატულას. + +ეს პარამეტრი არ შეცვლის ხატულას შემდეგ ადგილებში: +• მოწყობილობის სისტემური პარამეტრების აპლიკაციების სია +• გარკვეული push შეტყობინებები +• 'გახსენი შემდეგით' აპლიკაციების ამომრჩევი +• Android Auto-ს ხატულა + +ხატულის ყველგან შესაცვლელად, თავიდან გამოიყენეთ პატჩი და შეცვალეთ 'App icon' პატჩის ოფცია" + შეტყობინების ხატულა + აპლიკაციის ხატულის შესაბამისობა + Morphe play + Morphe-ის შეტყობინება + თქვენი ნახვის ისტორია არ ინახება.<br><br>ეს, სავარაუდოდ, გამოწვეულია DNS რეკლამის ბლოკირებით ან ქსელური პროქსით.<br><br>ამის გამოსასწორებლად, დაამატეთ <b>s.youtube.com</b> თეთრ სიაში ან გამორთეთ ყველა DNS ბლოკერი და პროქსი + აღარ გამოჩნდეს + ვიდეო ნაკადების იმიტაცია + გააკონფიგურირეთ კლიენტის იმიტაცია, რათა თავიდან აიცილოთ დაკვრის პრობლემები + "ასწორებს დაკვრის პრობლემებს მომხმარებელთა უმეტესობისთვის თავსებადი ვიდეო ნაკადის მოთხოვნით + +თუ თქვენ ხართ YouTube Premium-ის მომხმარებელი, ეს პარამეტრი შესაძლოა საჭირო არ იყოს" + კლიენტის სტრიმების ჩატვირთვა ვერ მოხერხდა + ნაგულისხმევი კლიენტი არ მუშაობს + "%1$s რამდენჯერმე ზედიზედ არ ამუშავდა, თუმცა %2$s-მა იმუშავა. + +გსურთ %2$s გახდეს ნაგულისხმევი კლიენტი?" + გადართვა + ამ პარამეტრის გამორთვამ შესაძლოა გამოიწვიოს დაკვრის პრობლემები + სცადეთ ჩართვა: ვიდეო სტრიმების იმიტაცია > Android VR-ზე ავტორიზაცია + ნაგულისხმევი კლიენტი + Android VR-ზე ავტორიზაცია + Android VR-ში არ ხართ ავტორიზებული. შესასვლელად დააჭირეთ აქ + ამჟამად ავტორიზებული ხართ Android VR-ში + ამ ფუნქციის გამოსაყენებლად, შეცვალეთ \'ნაგულისხმევი კლიენტი\' Android VR-ით + ავტორიზება Android VR-ში + "Android VR-ში ავტორიზაციამ შესაძლოა გამოსწოროს დაკვრის პრობლემები ზოგიერთი მომხმარებლისთვის + +ავტორიზაციისთვის დააჭირეთ ქვემოთ მოცემულ ღილაკს \"გაგრძელება\"" + გამოსვლა + ინსტრუქციები + "გააქტიურების კოდი: %s + +1. ქვემოთ დააჭირეთ ღილაკს „გახსენით ვებგვერდი“, რათა გახსნათ ვებგვერდი „მოწყობილობის დაკავშირება“ +2. ჩასვით გადმოკლადირებიდან (ან ჩაწერეთ ხელით) ზემოთ მოცემული გააქტიურების კოდი +3. მოთხოვნის შემთხვევაში, შეйდეთ Google-ში. თუ გაქვთ რამდენიმე ანგარიში, აირჩიეთ YouTube-ის ეს ანგარიში +4. დააჭირეთ „ნებადართეთ YouTube VR-ისთვის“ წვდომა +5. შეხვედით ამ აპლიკაციაში მას შემდეგ, რაც დაინახავთ შეტყობინებას „მოწყობილობა დაკავშირებულია“" + გააქტიურების კოდის გაცემა ვერ მოხერხდა + ავთენტიფიკაციის ტოკენის გაცემა ვერ მოხერხდა + ავტორიზაცია წარმატებით დასრულდა + "ავთენტიფიკაცია შენახულია + +• თქვენი Morphe-ის პარამეტრები არ მოახდენს Android VR ავთენტიფიკაციის იმპორტს/ექსპორტს +• თუ ეს აპლიკაცია 6 თვის განმავლობაში არ იქნება გამოყენებული, Google წაშლის ამ ავთენტიფიკაციას +• დაგჭირდებათ ხელახლა ავტორიზაცია, თუ შეცვლით Google-ის პაროლს ან წაშლით ამ მოწყობილობას თქვენი Google-ის პარამეტრებიდან" + OAuth2 დროებით მიუწვდომელია (API-ს დრო დასრულდა) + Android VR-ის ავტორიზაციის ვადა ამოიწურა, სცადეთ VR-ში ხელახლა შესვლა + OAuth2 დროებით მიუწვდომელია (სტატუსი %s) + OAuth2 დროებით მიუწვდომელია + VR-ში შესვლა შეუძლებელია, ვებსაიტის ავტორიზაცია არ تمت დადასტურება + VR-ში შესვლა შეუძლებელია: %s + გადმოწერილი ხართ Android VR-დან + VR-ის ავტორიზაციის ტოკენი არასწორია, სცადეთ VR-ში ხელახლა შესვლა + შეტყობინებების მედია điều khiểnება + მოახდინეთ დაკვრის მართვის ელემენტების პერსონალიზაცია, რომლებიც ჩანს თქვენს ჩაკეტილი ეკრანისა და შეტყობინებების პანელზე + „წინა“ & „შემდეგი“ ღილაკების დამალვა + მალავს „წინა“ და „შემდეგი“ ღილაკებს მედია შეტყობინებაში, ჩაკეტილ ეკრანსა და ყურსაზურის მართვის ელემენტებში + მითითების ზოლის გათიშვა + 금კრძალავს მითითების ზოლის გადაადგილებას ჩაკეტილ ეკრანსა და შეტყობინებების პანელზე + ძებნა \"წინა\" & \"შემდეგი\" ღილაკებით + ცვლის მედია შეტყობინების, ჩაკეტილი ეკრანისა და ყურსაზურების მართვის ღილაკებს, რათა ისინი ვიდეოების შეცვლის ნაცვლად განვადების ფუნქციით იმუშაონ + უკან გადახვევის ხანგრძლივობა + წამები, რის მიხედვითაც მოხდება უკან გადახვევა წინა ღილაკით + წინ გადახვევის ხანგრძლივობა + წამები, რომლითაც მოხდება გადახვევა წინ \"შემდეგ」 ღილაკით + ძებნის სიგრძე უნდა იყოს 1-დან %s წამამდე. გამოიყენება ნაგულისხმევი მნიშვნელობა + გამორთეთ QUIC პროტოკოლი + გამორთეთ DRC აუდიო + აუდიოს ორიგინალი ენის იძულებითი გამოყენება + ყოველთვის ფლეიერებს ვიდეოს ორიგინალ აუდიოტრეკს დუბლირებული ვერსიის ნაცვლად + სიჩუმის გამოტოვება + ავტომატურად გამოტოვებს ჩუმ პაუზებს ვიდეოს აუდიოში + პაუზის მინიმალური სიგრძე + მხოლოდ მინიმუმ ამ სიგრძის პაუზები შემცირდება. ძალაში შედის შემდეგი ვიდეოს დაწყებისას + ქსელის პროქსი + გზამკვლევი მხარდაჭერილ ქსელურ მოთხოვნებს პროქსის მეშვეობით წარმართოს + პროქსის ჩართვა + მართავს მხარდაჭერილ ქსელურ მოთხოვნებს კონფიგურირებული HTTP ან HTTPS პროქსის მეშვეობით + პროქსის ჰოსტი + პროქსი სერვერის ჰოსტის სახელი ან IP მისამართი + პროქსის პორტი + პროქსი სერვერის პორტი + HTTPS პროქსის გამოყენება + გამოიყენეთ HTTPS პროქსი სერვერთან დასაკავშირებლად HTTP-ის ნაცვლად + პროქსის ავტორიზაცია + გზავნის Basic ავტორიზაციას პროქსის მოთხოვნებთან ერთად + პროქსის მომხმარებლის სახელი + მომხმარებლის სახელი Basic პროქსი ავტორიზაციისთვის + პროქსის პაროლი + პაროლი Basic პროქსი ავტორიზაციისთვის + პირდაპირი კავშირის დაშვება + დაუკავშირდით პირდაპირ, თუ პროქსი ვერ იმუშავებს + მთლიანი ეკრანის რეკლამების დამალვა + ამ ფუნქციის გამოსაყენებლად, შეცვალეთ \'Spoof video streams\' ნებისმიერი კლიენტით, გარდა \'Android Studio\'-სა და \'Android VR Downgraded\'-ისა + გაზიარების ბმულების გასუფთავება + შესაბამისად ასუფთავებს გაზიარებული ბმულები თრექინგის მოთხოვნის პარამეტრებისგან + გაუცვალეთ გაზიარების ბმულები youtube.com-ით + გაზიარებულ ბმულებში music.youtube.com-ს ანაცვლებს youtube.com-ით + გაზიარების ბმულების შეცვლა youtu.be-ით + აიძულებს ვიდეოებს გამოიყენონ youtu.be URL-ის შემოკლებული ფორმატი + Debug log protocol buffer + ამატებს დამატებით ლოგირების ინფორმაციას დეველოპერებისთვის აპლიკაციის კომპონენტების ამოსაცნობად + "ამ პარამეტრის ჩართვა დააფიქსირებს დამატებით განლაგების მონაცემებს, მათ შორის ეკრანზე არსებულ ტექსტს ზოგიერთი UI კომპონენტისთვის. + +ეს დაგეხმარებათ კომპონენტების იდენტიფიცირებაში საკუთარი ფილტრების შექმნისას. + +თუმცა, ამის ჩართვა ასევე დააფიქსირებს ზოგიერთ მომხმარებლის მონაცემს, როგორიცაა თქვენი IP მისამართი" + საკუთარი ფილტრი + გააკონფიგურირეთ საკუთარი ფილტრები ნებისმიერი კომპონენტის დასამალად + საკუთარი ფილტრის ჩართვა + გასაფილტრებელი კომპონენტის გზის შემქმნელ სტრიქონთა სია, განმყოფელად გამოიყენეთ ახალი ხაზი + არასწორი საკუთარი ფილტრი: %s + პარამეტრების მენიუს ფილტრი + დამალეთ ელემენტები აპლიკაციის სტანდარტული პარამეტრების ეკრანზე მათი ხილული სახელის მიხედვით + გახსენით აპლიკაციის სტანდარტული პარამეტრების ეკრანი ერთხელ სიის შესავსებად + გააუქმეთ მაყურებლის სიფხიზლიანობის დიალოგი + ეს არ გვლის ასაკობრივ შეზღუდვას. ის უბრალოდ ავტომატურად ადასტურებს მას + PoToken-ის პროვაიდერი + გააკონფიგურირეთ PoToken-ის პროვაიდერი დაკვრის პრობლემების თავიდან ასაცილებლად + PoToken-ის გარე პროვაიდერი + "ასწორებს დაკვრის პრობლემებს PoToken-ის გარე მინტერის (minter) გამოყენებით (ექსპერიმენტული) + +თუ ხართ YouTube Premium-ის მომხმარებელი, ეს პარამეტრი შესაძლოა არ იყოს საჭირო" + "ასწორებს დაკვრის პრობლემებს გარე PoToken minter-ის გამოყენებით (ექსპერიმენტული) + +ამ ფუნქციის გამოსაყენებლად, გამორთეთ 'ვიდეო ნაკადების სპუფინგი'" + თუ არ მუშაობს, სცადეთ MicroG-ის იძულებითი დახურვა + PotHelper-ის პარამეტრების გახსნა + "ეს ფუნქცია ამჟამად განვითარების პროცესშია და შესაძლოა არ ამუშაოს + +დააჭირეთ აქ PotHelper-ის ჩამოსატვირთად" + PotHelper აღარ მუშაობს + "PotHelper-ის ამ ვერსიაში ჩაშენებული ტოკენი აღარ მუშაობს + +გადმოწერის გამოსასწორებლად ჩართეთ \"ვიდეო ნაკადების იმიტაცია\"" + ჩართვა + აპლიკაციის ვერსიის ფალსიფიკაცია + ატყუებს სერვერს, რათა ჩათვალოს, რომ იყენებთ აპლიკაციის ძველ ვერსიას, რათა აღადგინოთ ინტერფეისის ძველი ფუნქციები + "აპლიკაციის ვერსია ჩანაცვლებული იქნება უფრო ძველი ვერსიით. + +ეს შეცვლის აპლიკაციის გარეგნულ მხარესა და ფუნქციებს, თუმცა შესაძლოა წარმოიშვას უცნობი გვერდითი ეფექტები. + +გვიანად გამორთვის შემთხვევაში, რეკომენდებულია აპლიკაციის მონაცემების გასუფთავება UI ხარვეზების თავიდან ასაცილებლად" + აპლიკაციის ვერსიის ფალსიფიკაციის სამიზნე + აპლიკაციის განახლების სიხშირე ნაგულისხმები + %s FPS + განახლების სიხშირის გადაფარვა + ყოველთვის + დაკვრა პორტრეტულ რეჟიმში + დაკვრა სრული ეკრანით + უკრაინული რეჟიმით და სრული ეკრანით დაკვრა + ფონტი ნაგულისხმები + მოწყობილობის სისტემური ფონტი + ფონტის პერსონალური ფაილი + ფონტის პერსონალური ფაილი + აირჩიეთ TTF ან OTF ფონტის ფაილი + შერჩეული ფაილი არ არის TTF ან OTF ფონტი + საკუთალ ფონტს ვერ ჩატვირთა და ის გამორთულია + დისლაიქები დროებით недоступებელია (API-ს დრო დასრულდა) + დისლაიქები nedostupელია (სტატუსი %s) + დისლაიქები მიუწვდომელია (კლიენტის API ლიმიტი) + დისლაიქები მიუწვდომელია (%s) + მფლობელის მიერ დამალულია + არ მოეწონათ ამ ვიდეოს, ასევე სხვა %s ადამიანს + არ მოეწონათ ამ ვიდეოს, ხმათა %s არის უარყოფითი + ჩართეთ Return YouTube Dislike + აჩვენეთ დისლაიქები პროცენტულად + აჩვენებს დისლაიქებს ლაიქების პროცენტად და არა რაოდენობად + აჩვენეთ სავარაუდო ლაიქები + აჩვენებს ლაიქების სავარაუდო რაოდენობას იმ ვიდეოებზე, სადაც ავტორმა გამორთო ლაიქები + კავშირის შეცდომის შეტყობინებების ჩვენება + აჩვენებს შეტყობინებას, როდესაც Return YouTube Dislike-ის სერვერი nedost கிடைக்கადია + "Return YouTube Dislike აღადგენს იმ დისლაიქების რაოდენობას, რომელიც YouTube-მა 2021 წელს დამალა. რაოდენობები მოდის წაშლამდე დაარქივებული მონაცემებიდან, ხოლო ახალი ვიდეოებისთვის ისინი გამოთვლილია მომხმარებელთა ხმების საფუძველზე, ამიტომ ისინი მიახლოებითია და განახლდება რამდენიმე დღეში + +ვიდეოს ID, რომელსაც უყურებთ, იგზავნება Return YouTube Dislike-ის API-ზე, ხოლო თქვენი მოწონებები და დისლაიქები იგზავნება ანონიმურად შეფასებების გასაუმჯობესებლად" + Return YouTube Dislike API-ის სტატისტიკა ამ მოწყობილობისთვის + API-ს პასუხის დრო, საშუალო + API-ს პასუხის დრო, მინიმალური + API-ს პასუხის დრო, მაქსიმალური + API-ის პასუხის დრო, ბოლო მედია + დისლაიკები დროებით მიუწვდომელია - მოქმედებს Client API-ის rate limit + API-ით ხმების წამოღება, გამოძახებების რაოდენობა + ქსელური გამოძახებები არ განხორციელებულა + გაბმავზე განხორციელდა %s გამოძახება + API-ით ხმების წამოღება, ტაიმაუტების რაოდენობა + ქსელური გამოძახებების ტაიმაუტი არ დაფიქსირებულა + %s ქსელური გამოძახების დრო დასრულდა + API კლიენტის rate limit-ები + კლიენტის rate limit-ები არ დაფიქსირებულა + კლიენტის განაკვეთის ლიმიტი შეხვდა %s-ჯერ + %s მილიწამი diff --git a/patches/src/main/resources/addresources/values-ka-rGE/shared/strings.xml b/patches/src/main/resources/addresources/values-ka-rGE/shared/strings.xml index c0dfadb4dd..58972e7236 100644 --- a/patches/src/main/resources/addresources/values-ka-rGE/shared/strings.xml +++ b/patches/src/main/resources/addresources/values-ka-rGE/shared/strings.xml @@ -17,22 +17,219 @@ Second \"item\" text" --> + პარამეტრები + დარწმუნებული ხართ, რომ გსურთ გაგრძელება? + შენახვა + გაუქმება + ფერების განახლება + არასწორი ფერი + საჭიროა გადატვირთვა + გადატვირთეთ აპლიკაცია, რათა ეს ცვლილება ძალაში შევიდეს + გადატვირთვა + იმპორტი + კოპირება + Morphe-ის პარამეტრები განახლდა ნაგულისხმევზე + იმპორტირებული %s პარამეტრები + იმპორტი ვერ მოხერხდა: %s + ზევით გადასვლა + პარამეტრების ძებნა + შესახებ + \'%s\'-ისთვის შედეგები ვერ მოიძებნა + სცადეთ სხვა საკვანძო სიტყვა + ბოლოდროინდელი მოძებნებები + წავშალოთ ძებნის ისტორიიდან? + წაშლა ძებნის ისტორიიდან + ძებნის ისტორიის გასუფთავება + დარწმუნებული ხართ, რომ გსურთ ძებნის მთელი ისტორიის გასუფთავება? + რჩევები ძებნისთვის + "• დააჭირეთ გზას მასზე გადასასვლელად +• გრძლად დააჭირეთ პარამეტრს მასზე გადასასვლელად +• დააჭირეთ Enter-ს ძებნის მოთხოვნის ისტორიაში შესანახად +• ძებნა არ ითვალისწინებს რეგისტრს და პუნქტუაციას +• მშობელი პარამეტრები ჩნდება გამორთული შვილობილი პარამეტრების ზემოთ" + ძებნის ისტორია ცარიელია + ძებნის ისტორიის შესანახად, ჩაწერეთ საძიებო მოთხოვნა და დააჭირეთ Enter-ს + პარამეტრების ძიების ისტორიის ჩვენება + Morphe-ის პარამეტრების სახელი ნაგულისხმები + დამატებითი + მორგვები + მორფინგის დრო + საკუთარი + შეიყვანეთ სახელი + სახელი არ შეიძლება იყოს ცარიელი + Morphe-ის პარამეტრების ხატულების ჩვენება + კოპირებულია: %s + Morphe-ის ენა + "ზოგიერთი ენის თარგმანები შესაძლოა იყოს ნაკლულები ან არასრული. + +ახალი ენების თარგმნისთვის ან არსებული თარგმანების გასაუმჯობესებლად, ეწვიეთ morphe.software/translate" + აპლიკაციის ენა + იმპორტი / ექსპორტი + Morphe-ის პარამეტრების იმპორტი / ექსპორტი + იმპორტი ფაილიდან + პარამეტრები წარმატებით იქნა იმპორტირებული, შესატطبيقებლად დააჭირეთ „შენახვას“ + პარამეტრების იმპორტი ვერ მოხერხდა + ექსპორტი ფაილში + პარამეტრები წარმატებით იქნა ექსპორტირებული + პარამეტრების ექსპორტი ვერ მოხერხდა + თქვენ იყენებთ Morphe Patches-ის უახლეს ვერსიას <i>%s</i> + თქვენ იყენებთ Morphe Patches-ის ვერსიას <i>%1$s</i><br><br><b>ხელმისაწვდომია განახლება: <i>%2$s</i></b><br><br>პატჩების განაახლებად, გამოიყენეთ Morphe ამ აპლიკაციის ხელახალი დაპატჩვისთვის + შენიშვნა + ეს ვერსია არის წინასწარი რელიზი და შესაძლოა შეგვხვდეს მოულოდნელი პრობლემები + ოფიციალური ბმულები + ვებგვერდი + დონაცია + თარგმანები + ავტორები + განახლებულია + ხელმისაწვდომია განახლება + მიმდინარე დეველოპმენტი + წინა დეველოპმენტი + აქტიური დეველოპმენტი + 2022 ‑ 2025 + 2022 ‑ 2025 + 2019 ‑ 2022 + ლიცენზიის შეტყობინებები + იტვირთება… + დებაგინგი + დეველოპერის გაფართოებული პარამეტრებისა და დიაგნოსტიკური ლოგების კონფიგურაცია + დებაგირების ლოგირების ჩართვა + Debug ლოგის stack trace + მოიცავს სტეკის კვალს დებაგირების ლოგებში შეცდომების იდენტიფიცირებისთვის + აჩვენე შეტყობინება Morphe-ის შეცდომისას + "შეცდომის შეტყობინებების გამორთვა მალავს Morphe-ის ყველა შეცდომის შეტყობინებას. + +თქვენ არ მიიღებთ შეტყობინებას არანაირი მოულოდნელი მოვლენის შესახებ" + სადებაგო ლოგების ექსპორტი + Morphe-ის სადებაგო ლოგების ექსპორტი + სადებაგო ლოგები წარმატებით იქნა ექსპორტირებული + სადებაგო ლოგების ექსპორტი ვერ მოხერხდა + სადებაგო ჩანაწერების ძებნა… + გასუფთავება + დებაგინგის ლოგირება გამორთულია + სადებაგო ლოგები ვერ მოიძებნა + სადებაგო ლოგები დაკოპირებულია + სადებაგო ლოგების ექსპორტი ვერ მოხერხდა: %s + სადებაგო ლოგები გასუფთავებულია + ფუნქციების დროშების მენეჯერი + მართეთ ბულეანური ფუნქციების დროშები + ფუნქციების დროშების ძებნა… + არააქტიური (%s) + აქტიური (%s) + დაბლოკილი (%s) + აიძულებული (%s) + ავტომატური + გამორთული + ჩართული + იძულებითი ჩართვა + დაბლოკვა + წაშლა + ფუნქციების დროშები შენახულია + ფუნქციების დროშები განის resetting-და + ფუნქციების დროშები კოპირებულია ბუფერში + გადმოტვირთეთ აპლიკაცია ამ პარამეტრის გამოსაყენებლად + ექსპორტი და იმპორტი + იმპორტირებული %s ფუნქციური დროშა + ფუნქციური დროშები ვერ მოიძებნა + ფუნქციური დროშები წარმატებით იქნა ექსპორტირებული + ფუნქციური დროშების ექსპორტი ვერ მოხერხდა + ფუნქციური დროშები ჩატვირთულია, გამოსაყენებლად დააჭირეთ შენახვას + ფაილის წაკითხვა ვერ მოხერხდა + ბინარული ძიება + "ადგენს, თუ რომელი ფუნქციური დროშა იწვევს აპლიკაციის ქცევას. + +%s დროშის ნახევარი დაბლოკილია და ყოველი რესტარტის შემდეგ თქვენ მიუთითებთ, კვლავაც შეინარჩუნა თუ არა აღნიშნულმა ქცევამ. + +დაიწყებამდე ერთხელ აღადგინეთ ქცევა, წინააღმდეგ შემთხვევაში ძიებიდან გამორიცხული იქნება ის დროშები, რომლებიც აპლიკაციამ ჯერ არ მოითხოვა" + დაწყება + "საძიებო ფუნქციური დროშები არ მოიძებნა. + +ბინარული ძიებისთვის შეიძლება გამოყენებულ იქნას მხოლოდ ის დროშები, რომლებსაც აპლიკაციამ მიმართა და რომლებიც ამჟამად არ არის იძულებითად ჩართული" + "ნაბიჯი %1$s + +დარჩენილია %2$s დროშები, რომელთაგან %3$s დაბლოკილია ამ ნაბიჯისთვის. + +გადატვირთეთ აპლიკაცია, შეამოწმეთ მოცემული ქცევის არსებობა და შემდეგ უპასუხეთ ქვემოთ" + ფუნქციური დროშების ძიება მიმდინარეობს + ქცევა სახეზეა + ქცევა არ არის სახეზე + ბინარული ძიების გაუქმება + ბინარული ძიება გაუქმებულია + "ფუნქციური დროშა %s იწვევს ამ ქცევას. + +ის ახლა დაბლოკილია" + "ვერ მოხერხდა ფუნქციური დროშის მოძებნა. + +როგორც ჩანს, აპლიკაციის ქცევა არ იმართება ფუნქციური დროშით" + "ვერ მოხერხდა ერთი კონკრეტული ფუნქციური დროშის მოძებნა. + +ქცევა ძიების ადრეულ ეტაპზე გაქრა, ამიტომ შესაძლოა რამდენიმე დროშა იყოს ჩართული ან ერთ-ერთი პასუხი არასწორი იყო" + გახსენით MicroG-ის პარამეტრები + MicroG არ არის დაინსტალირებული. დააინსტალირეთ + საჭიროა მოქმედება + "თქვენი MicroG v%1s მოძველებულია და შესაძლოა პრობლემები გამოიწვიოს. + +გთხოვთ, განაახლოთ MicroG უახლეს ვერსიამდე v%2s" + განახლება + იგნორირება + "დაინსტალირებული MicroG არ არის რეკომენდებული Morphe-ის მიერ: + +%1s + +დააჭირეთ მის წასაშლელად, შემდეგ ხელახლა გახსენით ეს აპლიკაცია და დააინსტალირეთ MicroG RE" + წაშლა + "MicroG-ს არ აქვს ფონურ რეჟიმში მუშაობის უფლება. + +გაეცანით \"Don't kill my app\" გზამკვლევს თქვენი ტელეფონისთვის და მიჰყევით ინსტრუქციებს MicroG-ის ინსტალაციისთვის. + +ეს აუცილებელია აპლიკაციის გამართული მუშაობისთვის" + გახსენით ვებგვერდი + "პრობლემების თავიდან ასაცილებლად, MicroG-ის ელემენტის ოპტიმიზაცია უნდა იყოს გამორთული. + +MicroG-ისთვის ელემენტის ოპტიმიზაციის გამორთვა უარყოფითად არ იმოქმედებს ელემენტის მოხმარებაზე. + +დააჭირეთ გაგრძელების ღილაკს და დაეთანხმეთ ოპტიმიზაციის ცვლილებებს" + გაგრძელება + MicroG-ის ელემენტის ოპტიმიზაციის დიალოგის ჩვენება + "ამის გამორთვა დამალავს MicroG-ის ელემენტის ოპტიმიზაციის შეტყობინებას. + +თუ ელემენტის ოპტიმიზაცია არ შეიცვლება, შესაძლოა შეგვხვდეს: +• აპლიკაცია შეიძლება შემთხვევით დაიხუროს +• შეტყობინებებმა შესაძლოა არ იმუშაოს +• ფონურ რეჟიმში დაკვრამ შესაძლოა არ იმუშაოს" + აპლიკაციის ბმულების მართვა + ბმულების კონფიგურაცია ამ აპლიკაციაში გასახსნელად + ნაბიჯი 1: გამორთეთ ორიგინალი აპლიკაციის ბმულები + გახსენით ორიგინალი აპლიკაციის პარამეტრები და გამორთეთ ყველა დომენის ბმული + ნაბიჯი 2: ჩართეთ პატჩირებული აპლიკაციის ბმულები + გახსენით პატჩირებული აპლიკაციის პარამეტრები და ჩართეთ ყველა დომენის ბმული + პარამეტრების გახსნა + 🚨 ექსპერიმენტული გაფრთხილება 🚨 + "თქვენ იყენებთ ⚠️ <b>%1$s</b>-ის ექსპერიმენტულ ვერსიას +<br/><br/> +🔧 მოსალოდნელია აპლიკაციის უჩვეულო მუშაობა ან გაურკვეველი შეცდომები, რადგან ამ ვერსიის პატჩებს ვახდენთ ოპტიმიზაციას. +<br/><br/> +თუ გსურთ მაქსიმალურად შეუფერხებელი მუშაობა, მაშინ <b>წაშალეთ</b> ეს ვერსია და დააინსტალირეთ რეკომენდებული სტაბილური ვერსია ✅ <b>%2$s</b>" + ⚠️ მსურს ექსპერიმენტული + ✅ მსურს სტაბილური + ავარიის ჩანაწერი შენახულია მომხმარებლის დოკუმენტებში: %s + ავარიის ჩანაწერი შენახულია: %s diff --git a/patches/src/main/resources/addresources/values-ka-rGE/sponsorblock/strings.xml b/patches/src/main/resources/addresources/values-ka-rGE/sponsorblock/strings.xml index 9d2d4ab7d5..4fc880f93f 100644 --- a/patches/src/main/resources/addresources/values-ka-rGE/sponsorblock/strings.xml +++ b/patches/src/main/resources/addresources/values-ka-rGE/sponsorblock/strings.xml @@ -16,7 +16,217 @@ Second \"item\" text" --> + ჩართეთ SponsorBlock + SponsorBlock არის კოლექტიური სისტემა YouTube ვიდეოების გამაღიზიანებელი ნაწილების გადასახტობად + გარეგნული სახე + ხმის მიცემის ღილაკის ჩვენება + ამატებს ღილაკს სეგმენტების დადებითი ან უარყოფითი შეფასებისთვის + კვადრატული განლაგების გამოყენება + იყენებს კვადრატულ კუთხეებს SponsorBlock-ის ღილაკებისა და კონტროლერებისთვის + კომპაქტური გამოტოვების ღილაკის გამოყენება + ამცირებს გამოტოვების ღილაკის სიგანეს სივრცის დასაზოგად + გამოტოვების ღილაკის ავტომატური დამალვა + მალავს გამოტოვების ღილაკს რამდენიმე წამის შემდეგ, ნაცვლად იმისა, რომ ის მთელი სეგმენტის განმავლობაში ხილული იყოს + გამოტოვების ღილაკის ხილვადობის ხანგრძლივობა + განსაზღვრეთ, რამდენ ხანს დარჩეს გამოტოვების ღილაკი ხილვადი + გამოტოვების შეტყობინების ჩვენება + აჩვენებს შეტყობინებას, როდესაც სეგმენტი ავტომატურად გამოიტოვება + გამოტოვების შეტყობინების ხანგრძლივობა + განსაზღვრეთ, რამდენ ხანს დარჩეს გაუქმების შეტყობინება ხილვადი + 1 წამი + 2 წამი + 3 წამი + 4 წამი + 5 წამი + 6 წამი + 7 წამი + 8 წამი + 9 წამი + 10 წამი + ვიდეოს სიგრძის ჩვენება სეგმენტების გარეშე + გამოკლებს ყველა სეგმენტს seekbar-ზე ჩვენებულ ვიდეოს საერთო ხანგრძლივობას + ახალი სეგმენტების შექმნა + აჩვენე ღილაკი \'ახალი სეგმენტის შექმნა\' + ამატებს ღილაკს ვიდეო ప్ლეიერში, რაც საშუალებას გაძლევთ გაგზავნოთ SponsorBlock-ის ახალი სეგმენტები + დროის კორექტირების ნაბიჯი + დროის კორექტირების ღილაკების გადახვევის რაოდენობა (მილიწამებში) + მნიშვნელობა უნდა იყოს დადებითი რიცხვი + გაგზავნის სახელმძღვანელო + სახელმძღვანელო შეიცავს წესებსა და რჩევებს ახალი სეგმენტების შესაქმნელად + მიჰყევით სახელმძღვანელოს + გაეცანით SponsorBlock-ის სახელმძღვანელოს ახალი სეგმენტების შექმნამდე + უკვე წაკითხულია + მაჩვენე + ზოგადი + კავშირის შეცდომის შეტყობინებების ჩვენება + აჩვენებს შეტყობინებას, როდესაც SponsorBlock-ის სერვერი მიუწვდომელია + გამოტოვების რაოდენობის თვალყურის დევნების ჩართვა + გზავნის გამოტოვების რაოდენობას SponsorBlock-ის სატაბლოზე, რათა დაითვალოს დაზოგვის დრო + სეგმენტის მინიმალური ხანგრძლივობა + სეგმენტები, რომლებიც ამ მნიშვნელობაზე (წამებში) მოკლეა, არ გამოჩნდება და არ იქნება გამოტოვებული + დროის არასწორი ხანგრძლივობა + თქვენი პირადი მომხმარებლის ID + ეს ინფორმაცია კონფიდენციალურად უნდა დარჩეს. ეს ჰგავს პაროლს და არ უნდა გაუზიაროთ სხვას. თუ ვინმე მოიპოვებს მას, შეძლებს თქვენს სახელით მოქმედებას + პირადი მომხმარებლის ID უნდა შედგებოდეს სულ მცირე 30 სიმბოლოსგან + API URL-ის შეცვლა + მისამართი, რომელსაც SponsorBlock იყენებს სერვერზე მოთხოვნების გასაგზავნად + არხების თეთრი სია + SponsorBlock გათიშულია თეთრ სიაში მყოფი არხებისთვის + შეტყობინება თეთრ სიაში მყოფი არხის შესახებ + აჩვენებს შეტყობინებას, როდესაც SponsorBlock გათიშულია თეთრ სიაში მყოფი არხისთვის + SponsorBlock გათიშულია ამ არხისთვის + API URL-ის განახლება + API URL არასწორია + API URL შეიცვალა + პარამეტრების იმპორტი / ექსპორტი + კოპირება + თქვენი SponsorBlock JSON კონფიგურაცია, რომელიც შეიძლება được იმპორტირებული / ექსპორტირებული იყოს Morphe-სა და სხვა SponsorBlock პლატფორმებზე + "თქვენი SponsorBlock JSON კონფიგურაცია, რომელიც შეიძლება được იმპორტირებული / ექსპორტირებული იყოს Morphe-სა და სხვა SponsorBlock პლატფორმებზე + +ეს მოიცავს თქვენს კერძო მომხმარებლის ID-ს. გააზარეთ ამის გაზიარება" + პარამეტრები წარმატებით იქნა იმპორტირებული + იმპორტი ვერ მოხერხდა: %s + ექსპორტი ვერ მოხერხდა: %s + "თქვენი პარამეტრები შეიცავს SponsorBlock-ის პირადი მომხმარებლის ID-ს. + +თქვენი მომხმარებლის ID ჰგავს პაროლს და მისი გაზიარება არასოდეს არ უნდა მოხდეს" + აღარასოდეს გამოჩენა + სეგმენტის ქცევის შეცვლა + სპონსორი + ფასიანი რეკლამა, ფასიანი რეფერალები და პირდაპირი რეკლამები. არა თვითპრომოციისა ან უფასო რეკლამებისთვის საქმეების / კრეატორების / ვებსაიტების / პროდუქტების, რომლებიც მათ მოსწონთ + უანაზღაურებელი / თვითპრომოცია + მსგავსია სპონსორისა, გარდა უანაზღაურებელი / თვითპრომოციისა. მოიცავს სექციებს მერჩანდაიზის, შემოწირულებების ან იმის შესახებ, ვისთანაც ისინი თანამშრომლობდნენ + ინტერაქციის შეხსენება (გამოწერა) + მოკლე შეხსენება მოწონების, გამოწერის ან გამორჩეულობის შესახებ კონტენტის შუაში. თუ ის გრძელია ან რაიმე კონკრეტულს ეხება, ის უნდა განთავსდეს თვითპრომოციის განყოფილებაში + მთავარი მომენტი + ვიდეოს ის ნაწილი, რომელსაც ადამიანთა უმეტესობა ეძებს + შესვენება / შესავალი ანიმაცია + ინტერვალი რეალური კონტენტის გარეშე. შესაძლოა იყოს პაუზა, სტატიკური კადრი ან განმეორებადი ანიმაცია. არ მოიცავს ინფორმაციას შემცველ გადასვლებს + დასკვნითი ბარათები / ტიტრები + ტიტრები ან მომენტი, როდესაც YouTube-ის დასკვნითი ბარათები ჩნდება. არ არის განკუთვნილი ინფორმაციას შემცველი დასკვნებისთვის + დასაინტერესებელი დასაწყისი / მისალმება + მომავალი ვიდეოს ትრეილერები გახმოვანებით, მისალმებები და დამშვიდობებები. არ მოიცავს სექციებს, რომლებიც ამატებს დამატებით კონტენტს + წინასწარი ნახვა / შეჯამება + კლიპების კოლექცია, რომელიც აჩვენებს, რა მოხდება ან რა მოხდა ვიდეოში ან სერიის სხვა ვიდეოებში, სადაც მთელი ინფორმაცია სხვაგანაც მეორდება + გვერდითი თემები / ხუმრობები + გვერდითი სცენები ან ხუმრობები, რომლებიც არ არის საჭირო ვიდეოს მთავარი შინაარსის გასაგებად. არ მოიცავს სექციებს, რომლებიც უზრუნველყოფს კონტექსტს ან დეტალებს + მუსიკა: არამუსიკალური სექცია + მხოლოდ მუსიკალური ვიდეოებისთვის. მუსიკალური ვიდეოების ის სექციები მუსიკის გარეშე, რომლებიც უკვე არ არის მოცული სხვა კატეგორიით + გამოტოვება + მნიშვნელოვანი მომენტი + სპონსორის გამოტოვება + პრომოს გამოტოვება + გამოტოვე ინტერაქცია + გადმოხტი გამოკვეთილზე + გამოტოვე შესავალი + გამოტოვე შესვენება + გამოტოვე შესვენება + გამოტოვე დასასრული + გამოტოვე ჰუკი + გამოტოვე პრევიუ + გამოტოვე პრევიუ + გამოტოვე შეჯამება + გამოტოვე გვერდითი თემა + გამოტოვე არამუსიკალური + სეგმენტის გამოტოვება + სპონსორის გამოტოვება + თვითპრომოუშენის გამოტოვება + მღ烦ირებელი შეხსენების გამოტოვება + გაგრძელება მთავარ ნაწილზე + ინტროს გამოტოვება + შუალედის გამოტოვება + შუალედის გამოტოვება + აუტროს გამოტოვება + ჰუკის გამოტოვება + პრევიუს გამოტოვება + გამოტოვებული წინასწარი ნახვა + გამოტოვებული შეჯამება + გამოტოვებული विषयाრიგვნება + გამოტოვებული არამუსიკალური სექცია + გამოტოვებული გაუგზავებელი სეგმენტი + გამოტოვებული რამდენიმე სეგმენტი + ავტომატურად გამოტოვება + ერთხელ ავტომატურად გამოტოვება + გამოტოვების ღილაკის ჩვენება + ჩვენება გადამ winding-ის ზოლში + გამორთვა + სეგმენტის გაგზავნა ვერ მოხერხდა: %s + SponsorBlock დროებით გათიშულია + სეგმენტის გაგზავნა ვერ მოხერხდა (სტატუსი: %1$s %2$s) + სეგმენტის გაგზავნა ვერ მოხერხდა. მოთხოვნების ლიმიტი (ზედმეტად ბევრი მოთხოვნა ერთი მომხმარებლისგან ან IP-ისგან) + სეგმენტის გაგზავნა შეუძლებელია: %s + "სეგმენტის გაგზავნა შეუძლებელია. +ის უკვე არსებობს" + სეგმენტი წარმატებით გაიგზავნა + SponsorBlock დროებით მიუწვდომელია (API-ის დრო დასრულდა) + SponsorBlock დროებით მიუწვდომელია (სტატუსი %s) + SponsorBlock დროებით მიუწვდომელია + სეგმენტზე ხმის მიცემა ვერ მოხერხდა (API-ის დრო დასრულდა) + სეგმენტზე ხმის მიცემა ვერ მოხერხდა (სტატუსი: %1$s %2$s) + სეგმენტზე ხმის მიცემა ვერ მოხერხდა: %s + დადებითი ხმა + უარყოფითი ხმა + კატეგორიის შეცვლა + ხმის მისაცემად სეგმენტები არ არსებობს + %1$s-დან %2$s-მდე + აირჩიეთ სეგმენტის კატეგორია + კატეგორია გამორთულია პარამეტრებში. გაგზავნისთვის ჩართეთ კატეგორია + SponsorBlock-ის ახალი სეგმენტი + განვსაზღვროთ %s ახალი სეგმენტის დასაწყისად ან დასასრულად? + დასაწყისი + დასასრული + ახლა + დრო, როდესაც სეგმენტი იწყება + დრო, როდესაც სეგმენტი სრულდება + არის დრო სწორად მითითებული? + "სეგმენტია + +%1$s-დან +%2$s-მდე + +(%3$s) + +მზად ხართ გასაგზავნად?" + გაგზავნა + დასაწყისი უნდა იყოს დასრულების წინ + ჯერ მონიშნეთ ადგილი დროის ზოლზე + გადახედეთ სეგმენტს და დარწმუნდით, რომ იგი შეუფერხებლად გადავარდნილია + სეგმენტის დროის ხელით რედაქტირება + გსურთ სეგმენტის დასაწყისის თუ დასრულების დროის რედაქტირება? + მითითებული დრო არასწორია + გამოკვეთილის გაგზავნა + "არის თუ არა გამოკვეთილის დრო სწორი? + +%s + +თუ არ აგზავნით გამოკვეთილს, დააჭირეთ გაუქმებას და მონიშნეთ მეორე მდებარეობა" + გაგზავნეთ მონიშვნა + სტატისტიკა + სტატისტიკა დროებით მიუწვდომელია (API გათიშულია) + იტვირთება… + SponsorBlock გათიშულია + თქვენი მომხმარებლის სახელი: <b>%s</b> + დააჭირეთ აქ მომხმარებლის სახელის შესაცვლელად + მომხმარებლის სახელის შეცვლა ვერ მოხერხდა: სტატუსი: %1$s %2$s + მომხმარებლის სახელი წარმატებით შეიცვალა + თქვენი რეპუტაციაა <b>%.2f</b> + თქვენ შექმენით <b>%s</b> სეგმენტი + შეეხეთ აქ თქვენი სეგმენტების სანახავად + SponsorBlock-ის ლიდერბორდი + თქვენ ადამიანები <b>%s</b> სეგმენტისგან დაიცავით + შეეხეთ აქ გლობალური სტატისტიკისა და საუკეთესო კონტრიბუტორების სანახავად + ეს მათი ცხოვრების <b>%s</b> ნაწილია.<br>შეეხეთ აქ ლიდერბორდის სანახავად + თქვენ გადახტეთ <b>%s</b> სეგმენტს + ეს არის <b>%s</b> + გსურთ გადახტნილი სეგმენტების მთვლელის განულება? + %1$s საათი %2$s წუთი + %1$s წუთი %2$s წამი + გამჭვირვალობა: + ფერი: + "SponsorBlock არის ვიდეოს სეგმენტების კროვსორსინგული მონაცემთა ბაზა, როგორიცაა სპონსორები, შესავალი, დასავალი და გამოწერის შეხსენებები. სეგმენტებს წარადგენს და ხვადს საზოგადოება, ხოლო მონაცემთა ბაზა ღიაა და უფასოა გადმოსაწერად + +თქვენ მიერ ნანახი ვიდეოს ID იგზავნება SponsorBlock-ის API-ში სეგმენტების მისაღებად" diff --git a/patches/src/main/resources/addresources/values-ka-rGE/youtube/strings.xml b/patches/src/main/resources/addresources/values-ka-rGE/youtube/strings.xml index 947132f120..a9de56cb06 100644 --- a/patches/src/main/resources/addresources/values-ka-rGE/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ka-rGE/youtube/strings.xml @@ -18,220 +18,1641 @@ Second \"item\" text" + Morphe-ს შესახებ + რეკლამები + ფიდი + ზოგადი + პლეიერი + Shorts + გადამ winding-ის ზოლი + სვაიპის კონტროლი + სხვადასხვა + ვიდეო + YouTube-ის პარამეტრების ძველი ეკრანის აღდგენა + ფონური დაკვრის შეზღუდვების მოხსნა + ფონური დაკვრის გათიშვა + ავტომატური andის პაუზა მოწყობილობის ჩაკეტვისას + გამორთვებული + ყოველთვის დააპაუზეთ დაბლოკვისას + დააპაუზეთ, თუ უსადენო აუდიო არ არის დაკავშირებული + გათიშეთ სქროლვის სიჩქარის შეზღუდვა + გამორთეთ განლაგების განახლებები + თავიდან აცილებს სერვერის მიერ აპლიკაციის განლაგების განახლებას + "აპლიკაციის განლაგება უბრუნდება იმ განლაგებას, რომელიც გამოიყენებოდა პირველი ინსტალაციის დროს. + +ზოგიერთი სერვერზე განთავსებული განლაგება შესაძლოა არ დაუბრუნდეს. + +ცვლილებები მოიცავს: +• პლეიერის გამოსვლილი მენიუს კომპონენტებმა (ან შესაბამისმა პარამეტრებმა) შესაძლოა არ იმუშაონ +• მბრუნავი ციფრები არ არის ანიმირებული +• ჩანს ბიბლიოთეკის ჩანართი +• ვიდეოს აღწერილობის მუსიკალურმა სექციამ შესაძლოა არ იმუშაოს +• პლეიერის გამოსვლილი მენიუს ელემენტების დამალვა შეუძლებელია +• ანგარიშის გადართვის ღილაკი შესაძლოა არ გამოჩნდეს ბიბლიოთეკის ჩანართზე" + გააქტიურეთ ფიდის ჩამოსაშლელი მენიუს ფილტრი + ფიდის გასაშლელი მენიუს ფილტრი + შეიყვანეთ გამოსაშლელი მენიუს სახელები ფილტრაციისთვის, განყოფილის ახალი ხაზებით + ჩართეთ ბიბლიოთეკის / თქვენი ქვედა მენიუს ფილტრი + ბიბლიოთეკის / თქვენი ქვედა მენიუს ფილტრი + შეიყვანეთ ბიბლიოთეკის / თქვენი ქვედა მენიუს სახელები გასაფილტრად, გან隔ებული ახალი ხაზებით + ალბომის ბარათების დამალვა + მალავს ალბომის ბარათებს არტისტის აღწერილობის ქვემოთ + არტისტების ბარათების დამალვა + მალავს არტისტის ბარათს ძებნის შედეგების თავში + მალეთ არხის ღილაკები + მალავს ისეთ ღილაკებს, როგორიცაა გამოწერა, არხის ნახვა, საზოგადოება და YouTube Music, საძიებო შედეგებში არხების პროფილებიდან + კომუნიტეტის პოსტების დამალვა + კომპაქტური ბანერების დამალვა + მალავს კომპაქტურ ბანერებს და დაშორებებს ზოგიერთ კომპონენტს შორის + გასაშლელი ბარათები ვიდეოების ქვემოთ + ყველას ჩვენება + დამალეთ მხოლოდ შეჯამების ბარათი + ყოველივეს დამალვა + მიკროფონის მცურავი ღილაკის დამალვა + მალავს მიკროფონის მოტივებულ ღილაკს ძებნის შეთავაზებების ეკრანის ქვედა მარჯვენა კუთხეში + \"გაიწერეთ Premium\" ღილაკის დამალვა + ისტორიის პოლკის დამალვა + ჰორიზონტალური თაროების დამალვა + მალავს ჰორიზონტალურ თაროებს, როგორიცაა: Breaking news, Community posts you may like, Continue watching, Explore more channels, Explore more topics, For You, Latest videos, Most relevant, Posts from Creator\'s Community, Shopping და Watch it again + სურათების თაროს დამალვა + მალავს სურათების თაროებს ძებნის შედეგებში + დამალეთ ბარათი \"მოიწვიეთ სხვები შეტყობინებების გასაგზავნად\" + მალავს მოწვევის ბარათს, რომელიც ჩნდება შეტყობინებების ჩანართის შეტყობინებების სექციაში + ღილაკის \"უახლესი ვიდეოები\" დამალვა + მალავს ლურსად „უახლეს ვიდეოების“ ბანერს, რომელიც ჩნდება მთავარი ფიდის ზედა ნაწილში + მறைცხეთ პირდაპირი ტრანსლაციები + მალავს ვიდეოებს, რომლებიც ამჟამად პირდაპირ ეთერშია მთავარ ფიდსა და ძებნის შედეგებში + მიქს პლეილისტების დამალვა + ფილმების სექციის დამალვა + შეტყობინებების მენიუს სათაურის დამალვა + მალავს შეტყობინებების მენიუს სათაურს ზარის ღილაკზე დაჭერისას + 숨기기 \'შემატყობინე\' ღილაკი + მალავს შეხსენების ღილაკს დაგეგმილი პირდაპირი ჩართვებისა და პრემიერებისთვის + 숨기기 Playables + მალავს ფუნქციას, რომელიც საშუალებას გაძლევთ ითამაშოთ თამაშები + ძებნის ტერმინების მინიატურების დამალვა + მალავს მინიატურების წინასწარ просмоტვას ძიების ავტოშევსების სიაში + 숨기기 \'მეტი\' ღილაკი + მალავს გაფართოების ღილაკს ძიების შედეგებში ვიდეოების ქვემოთ + დამალეთ გამოწერილ არხებზე მუშაობის პანელი + გამოწერილი არხების სახელების დამალვა + მალავს არხების სახელებს გამოწერილი არხების პანელში + კვლევების დამალვა + გამოიყვანოს გამოკითხვები და შეხსენებები ფიდის რეკომენდაციების სიზუსტის შესახებ + ბილეთების თაროს დამალვა + + დამალეთ ბანერი \"ხელმისაწვდომი არMediaQuery ვიდეოები დამალულია\" + მალავს ბანერს \"ხელმისაწვდომი არMediaQuery ვიდეოები დამალულია\" პლეილისტებში + ატვირთვის დროის დამალვა + "შეზღუდვები: +• Shorts-ის თაროებზე, არხის გვერდებსა და ძიების შედეგებში შესაძლოა კვლავ ჩანდეს ატვირთვის დრო +• არ მუშაობს ავტომობილის განლაგებასთან" + ვიდეოს მინიატიურის დამალვა + ვიდეო სტიკერების დამალვა + მალავს ისეთ სტიკერებს, როგორიცაა „ავტომატურად დუბლირებული“ / „დუბლირებული“, „აღიქვამს საკომისიოს“, „ჰაიპირებული“, „ხალხმა ასევე უყურა“, „პროდუქტი(ები)“ და „შესაძლოა ასევე მოგეწონოთ“ + ნახვების რაოდენობის დამალვა + "შეზღუდვები: +• Shorts-ის თაროებზე, არხის გვერდებსა და ძიების შედეგებში შესაძლოა კვლავ ჩანდეს ნახვების რაოდენობა +• არ მუშაობს ავტომობილის განლაგებასთან" + ვებ-ძიების შედეგების დამალვა + მალავს სექციას \'Result from the web\' ძიების შედეგებიდან + სექციის \"შესაძლოა მოგეწონოთ\" დამალვა + YouTube Doodles-ის დამალვა + მალავს სეზონურ და ღონისძიებებზე დაფუძნებულ ანიმაციებს YouTube-ის ლოგოზე + YouTube Doodles-ის ჩვენებამ შესაძლოა შეზღუდოს პერსონალიზებული ჰედერების გამოჩენა + არხის პანელის დამალვა + არხის წყალსანიშნულის დამალვა + დამალეთ თავები და ღილაკი „ამ ვიდეოში“ + ქრაუდფანდინგის ბლოკის დამალვა + საგანგებო შეტყობინებების ბლოკების დამალვა + ინფორმაციული პანელების დამალვა + მალავს საინფორმაციო კონტექსტური პანელებს, მაგალითად, სამედიცინო ინფორმაციას + ღილაკის „გაწევრიანება“-ს დამალვა + ღილაკის \"ცოცხალი ჩატის განმეორება\" დამალვა + მალავს ჩატის განმეორების ღილაკს სრულეკრანიან რეჟიმში + სამედიცინო პანელების დამალვა + დამალეთ პლეერის ჟესტების მინიშნებები + მალავს მოთამაშის ჟესტების დროს გამოჩენილ მინიშნებებს, როგორიცაა „ზემოთ წამოქაჩეთ ზუსტი ძებნებისთვის“, „გაუშვათ გასაუქმებლად“ და „2x“ + Snackbar-ის დამალვა + გამოწერების წესებისა და გაიდლაინების დამალვა + დამალეთ ღილაკი \"სინქრონიზაცია ვიდეოსთან\" + დაჭერისა და გაჭერილის გრადიენტის დამალვა + მალავს პლეერის მართვის გრადიენტს თითის გადასვლისას, როდესაც დაჭერილს გინდავთ სიჩქარის გასაზრდელად + დროით შეზღუდული რეაქციების დამალვა + მალავს ფუნქციას, რომელიც მომხმარებლებს საშუალებას აძლევს გამოიყენონ ემოჯი-რეაქციები კომენტარების სექციასა და პირდაპირ ჩატში + დამალეთ მენიუ \"დახმარება & უკუკავშირი\" + მალავს მენიუს \'დახმარება & უკუკავშირი\' სამწერტიანი ღილაკის გამოყენებისას, მაგალითად, ძიების შედეგებში + ჰენდლის დამალვა + მალავს თქვენი არხის ჰენდლს ჩანართში \"თქვენ\" + ვიდეოს სათაურის დამალვა + მალავს ვიდეოს სათაურს სრულეკრანულ რეჟიმში + ვიდეო სუბტიტრების გასუფთავება + ასუფთავებს ვიდეოს სუბტიტრებს ისეთი ფრაზებისგან, როგორიცაა „#“, „ფონდის შეგროვება“, „პროდუქტი(ები)“ და „მაღაზია“ + დამალოს ღილაკი \'Ask Gemini\' + მალავს ღილაკს \'Ask Gemini\' პანელის \'Ask about this video\' ქვედა ნაწილში + ვიდეოს აღწერა + აირჩიეთ, რომელი კომპონენტები გამოჩნდეს ვიდეოს აღწერაში + დამალეთ სექცია „AI-გენერირებული ვიდეო რეზიუმე“ + დამალეთ კითხვების სექცია + დამალეთ ატრიბუტების სექცია + მალავს სექციებს: რეკომენდებული ადგილები, თამაშები, მუსიკა და ხსენებული პირები + დამალეთ არხის ბმულების სექცია + დამალეთ თავების სექცია + დამალეთ შესწორებების სექცია + დამალეთ სექცია „კურსის პროგრესი“ + სექციის „გამოკვლევა“ დამალვა + მალავს სექციებს \"გამოიკვევეთ ეს კურსი\" და \"გამოიკვევეთ პოდკასტი\" + დამალეთ სექცია \"გამოიკვევეთ ეს კურსი\" + დამალეთ სექცია \"გამოიკვევეთ პოდკასტი\" + დამალეთ რეკომენდირებული არხების სექცია + დამალეთ რეკომენდებული ბმულების სექცია + გამოფაქიზებული პლეილისტების სექციის დამალვა + დამალეთ რეკომენდებული ვიდეოების სექცია + ჰეშთეგების სექციის დამალვა + დამალეთ სექცია \"როგორ შეიქმნა ეს\" + Hype ქულების დამალვა + ინფო ბარათების სექციის დამალვა + 숨기기 \'ძირითადი კონცეფციების\' სექცია + ვიქტორიზების სექციის დამალვა + \"ძებნა ამ ვიდეოს შიგნით\" სექციის დამალვა + გამოწერის ღილაკის დამალვა + ტრანსკრიპტის სექციის დამალვა + სექციის \"ვიდეოს დეტალები\" დამალვა + ფილტრების პანელი + აირჩიეთ, სად დამალოთ სქროლვადი ფილტრების პანელი + დამალვა არხის გვერდზე + დამალეთ კომენტარებში + დამალეთ ფიდებში + დამალეთ დაკავშირებულ ვიდეოებში + დამალეთ ძებნის შედეგებში + დამალეთ ნახვის ისტორიაში + არხის გვერდი + აირჩიეთ, რომელი კომპონენტები გამოჩნდეს არხის გვერდზე + ჩანალის ჩანართის ფილტრის ჩართვა + ჩანალის ჩანართის ფილტრი + შეიყვანეთ არხის ჩანართების სახელები ფილტრაციისთვის, განყოფილებით ახალ ხაზებზე + სოციალური спіვობების ღილაკის დამალვა + გაწევრიანების ღილაკის დამალვა + ბმულების პრिवიუის დამალვა + მალავს დაწკაპუნებად ვებბმულ რბმულებს არხის სათაურში + \"მხოლოდ წევრებისთვის\" ჩიპის დამალვა + მალავს \"მხოლოდ წევრებისთვის\" ჩიპს არხის ფილტრების პანელზე + წევრების სექციის დამალვა + მალავს არხის წევრების სიას + პოსტების პოლკის დამალვა + მაღაზიის ღილაკის დამალვა + გამოწერის ღილაკის დამალვა + კვლევის მენიუ + აირჩიეთ, რომელი კომპონენტები გამოჩნდეს კვლევის მენიუში + კურსების მენიუს დამალვა + ფ exploring-ის ღილაკის დამალვა + დამალეთ \"Fashion & Beauty\" მენიუ + გეიმინგის მენიუს დამალვა + Hype-ის მენიუს დამალვა + სწავლის მენიუს დამალვა + Live მენიუს დამალვა + გამოწერების მენიუს დამალვა + დამალეთ მენიუ \'Movies & TV\' + დამალეთ მუსიკის მენიუ + სიახლეების მენიუს დამალვა + Playables-ის მენიუს დამალვა + პოდკასტების მენიუს დამალვა + სავაჭრო მენიუს დამალვა + სპორტის მენიუს დამალვა + YouTube Premium-ის მენიუს დამალვა + YouTube Studio-ს მენიუს დამალვა + YouTube Music-ის მენიუს დამალვა + YouTube Kids-ის მენიუს დამალვა + YouTube-ის შექმნის მენიუს დამალვა + \"როგორ მუშაობს YouTube\" მენიუს დამალვა + იურიდიული და კონფიდენციალურობის ქვესადისების დამალვა + კომენტარები + აირჩიეთ, რომელი კომპონენტები გამოჩნდეს კომენტარების სექციაში + ჩართეთ კომენტარების კარუსელის ფილტრი + "• დარწმუნდით, რომ შეიყვანეთ კარუსელის ელემენტის ზუსტი სახელი (მაგ. Ask, Hype this video, Listen on YouTube Music) +• კარუსელის ელემენტების დამალვამ შესაძლოა გამოიწვიოს გაუთვალისწინებელი გვერდითი ეფექტები +• თუ ყველა კარუსელის ელემენტი იქნება დამალული, კომენტარების მთელი სექციაც დამალული იქნება" + კომენტარების კარუსელის ფილტრი + შეიყვანეთ კომენტარების კარუსელის სახელები ფილტრაციისთვის, განყოფილებით ახალ ხაზებზე + AI ჩატის შეჯამების დამალვა + არხის წესების დამალვა + კონტექსტური შეტყობინებების დამალვა + \"წევრების კომენტარები\" სათაურის დამალვა + კომენტარების სექციის დამალვა + კომენტარების სექციის დამალვა მთავარ ფიდზე + დამალეთ społeczების წესები + ღილაკის \"შექმენი Short\" დამალვა + მინუსის ღილაკის დამალვა + Emoji ღილაკის დამალვა + ფილტრის პანელის ოფციების დამალვა + მალავს ფილტრის პანელის ყველა ოფციას, გარდა \"ტოპისა\" და \"უახლესისა\" + საჩუქრების ანიმაციისა და ბარათების დამალვა + საჩუქრის ღილაკის დამალვა + დამალეთ ინფოს ღილაკი + დამალეთ პირდაპირი ჩატის დონატორების პანელი + ლაივ ჩატის ქვემოთ ჩასქროლვის ღილაკის დამალვა + ცოცხალი ჩატის მინიშნებების დამალვა + მალავს ჩატის შეყვანის ზოლთან გამოჩენილ მინიშნებებს, როგორიცაა მხოლოდ გამომწერებისთვის განკუთვნილი ჩატის შეტყობინება + მენიუს ღილაკის დამალვა + მალავს თითოეული კომენტარის 3-წერტილიან მენიუს ღილაკს + კომენტარის პრევიზის დამალვა + მალავს კომენტარის პრევიუს ვიდეოს ქვეშ, სრული კომენტარების სექციის გახსნამდე + კომენტარი დამალულია Morphe-ის მიერ… + მინიმალისტური კომენტარების ღილაკის ჩართვა + მალავს მოცურავე ბარათებსა და მათ გვერდების ამომრჩევს, რომლებიც კომენტარების ღილაკზე ჩნდება + ღილაკის \"მადლობა\" დამალვა + დროის აღნიშვნის ღილაკის დამალვა + დამალეთ ღილაკი \"ტოპ ფანები\" + თარგმნის ღილაკის დამალვა + დამალე შეტყობინება \"კომენტარები ჯერ არ არის\" + გამოკვეთილი საძიებო ბმულების გასუფთავება + სუფთავებს გამოკვეთილ საძიებო ბმულებს და ტოვებს მხოლოდ ორიგინალ ჩვეულებრივ ტექსტს + საკვანძო სიტყვების ფილტრი + აირჩიეთ კონკრეტული სიტყვები ან ფრაზები ვიდეოების დასამალად ფიდებში და ძებნაში + კომენტარების დამალვა საკვანძო სიტყვებით + მთავარი გვერდის ვიდეოების დამალვა საკვანძო სიტყვების მიხედვით + ძიების შედეგების დამალვა საკვანძო სიტყვების მიხედვით + გამოწერების ვიდეოების დამალვა საკვანძო სიტყვების მიხედვით + საკვანძო სიტყვები დასამალად + "შეიყვანეთ დასამალავი საკვანძო სიტყვები და ფრაზები, რომლებიც განყოფილ იქნება ახალი ხაზებით + +საკვანძო სიტყვები შეიძლება იყოს ნებისმიერი ტექსტი, რომელიც ჩანს ვიდეოს სათაურებში ან არხის სახელებში + +სიტყვები, რომელთა შუაშია დიდი ასოები, უნდა დაემთხვეს ზუსტად (მაგ. iPhone, TikTok, LeBlanc)" + "მთავარი / გამოწერები / ძებნა / კომენტარები ფილტრდება იმ კონტენტის დასამალად, რომელიც შეესაბამება საკვანძო ფრაზებს + +შეზღუდვები: +• Shorts-ების დამალვა არხის სახელის მიხედვით შეუძლებელია +• ზოგიერთი UI კომპონენტი შესაძლოა არ დაიმალოს +• საკვანძო სიტყვის ძებნამ შესაძლოა არაფერი აჩვენოს" + მთლიანი სიტყვების დამთხვევა + საკვანძო სიტყვის/ფრაზის მოყვანა ბრჭყალებში გამორიცხავს ვიდეოს სათაურებისა და არხების სახელების ნაწილობრივ დამთხვევებს<br><br>მაგალითად,<br><b>\"ai\"</b> დამალავს ვიდეოს: <b>How does AI work?</b><br>მაგრამ არ დამალავს: <b>What does fair use mean?</b> + საკვანძო სიტყვის გამოყენება შეუძლებელია: %s + საკვანძო სიტყვის გამოსაყენებლად დაამატეთ ბრჭყალები: %s + საკვანძო სიტყვას აქვს ურთიერთგამომრიცხავი დეკლარაციები: %s + საკვანძო სიტყვა ძალიან მოკლეა და საჭიროებს ბრჭყალებს: %s + საკვანძო სიტყვა დამალავს ყველა ვიდეოს: %s + "• მთავარი: %s +• გამოწერები: %s +• ძიება: %s +• კომენტარები: %s +• სულ: %s" + სტატისტიკა + დამალული ვიდეოები, ბოლო 24 საათი + დამალული ვიდეოები, მთელი პერიოდი + გსურთ ბოლო 24 საათის მთვლელის განულება? + გსურთ მთელი პერიოდის მთვლელის განულება? + AI არხების ფილტრი + დამალეთ ვიდეოები იმ არხებიდან, რომლებიც AiSList-ის საზოგადოების სიის მიერ მონიშნულია როგორც AI-გენერირებული კონტენტი + ბლოკირების სია: მაღალი სანდოობა + გაფრთხილების სია: საშუალო სანდოობა + ვიდეოების დამალვა მთავარ გვერდზე + ვიდეოების დამალვა ძებნაში + "• მთავარი: %s +• ძებნა: %s +• სულ: %s" + Blocklist-ის არხები + Warnlist-ის არხები + ჩართეთ ფილტრი არხების ჩასატვირთად + არხების ჩატვირთვა… + "მთავარი გვერდი და ძებნის შედეგები გაფილტრულია AiSList-ის მიერ მონიშნული არხების ვიდეოების დასამალად + +შეზღუდვები: +• გაფილტრული AI Shorts შესაძლოა გამოჩნდეს როგორც ცარიელი სივრცე +• ზოგიერთი AI ვიდეო შესაძლოა მაინც გამოჩნდეს" + "AiSList არის YouTube არხების ღია კოდის სია, რომლებიც აქვეყნებენ AI-გენერირებულ კონტენტს და მას მოხალისეები მართავენ. არხებს მომხმარებლები აღნიშნავენ და ისინი გადამოწმებულია დამატებამდე + +სია გადმოიწერება GitHub-იდან და ვიდეოები ფილტრაცია ხდება თქვენს მოწყობილობაზე" + AI არხების შესახებ შეტყობინება + დაამატეთ ანგარიშის ჩანაწერი ვიდეოს მენიუში + დაამატეთ ჩანაწერი ვიდეოს მენიუში, რომელიც AiSList-ში შეტყობინებს ვიდეოს არხის შესახებ + გამომგზავნის სახელი + სახელი, რომელიც გამოჩნდება თქვენს რეპორტებთან AiSList-ის საჯარო რეიტინგში. რეპორტირება შეუძლებელია, სანამ ეს ველი ცარიელია + AI არხის რეპორტირება + "გსურთ %s-ის გაგზავნა AiSList-ში, ამ ვიდეოს მტკიცებულებად? + +რეპორტირება მოახდინეთ მხოლოდ იმ არხებზე, რომლებიც: +• იყენებენ AI ხმებს, ვიზუალებს, სკრიპტებს ან მუსიკას, როგორც ძირითად კონტენტს +• მასობრივად ქმნიან კონტენტს AI ხელსაწყოებით და მინიმალური ადამიანური ჩართულობით + +ნუ მოახდენთ რეპორტირებას იმ არხებზე, რომლებიც მხოლოდ AI-ზე მსჯელობენ ან ვიდეოს გარკვეულ ნაწილებში იყენებენ AI-ს. + +რეპორტები საჯაროა და გადამოწმებულია არხის სიაში დამატებამდე. წაშლა შესაძლებელია მხოლოდ GitHub-ის მეშვეობით მოთხოვნის გაგზავნით" + ეს არხი უკვე არის AiSList-ის ბლოკლისტში + ეს არხი უკვე არის AiSList-ის გაფრთხილების სიაში + შეტყობინება + არხების შესატყობინებლად მიუთითეთ გამგზავნის სახელი AI არხის ფილტრის პარამეტრებში + არხის ძებნა… + ამ ვიდეოს არხი ვერ მოიძებნა + არხი შეტყობინებულია და ელოდება განხილვას + AiSList-მა ვერ შეინახა შეტყობინება, სცადეთ მოგვიანებით + გაგზავნილია ძალიან ბევრი შეტყობინება, სცადეთ მოგვიანებით + AiSList-მა ანგარიში უარყო + ანგარიშის გაგზავნა ვერ მოხერხდა, შეამოწმეთ თქვენი კავშირი + არხების სრეფელი რეკლამებისთვის + სრ ეფელში მყოფი არხების ვიდეოები რეკლამებით ჩაირთვება, რათა მათმა შემქმნელებმა ანაზღაურება მიიღონ. რეკლამები დატბლოკილი რჩება ყველა სხვა არხზე + დაამატეთ არხი რეკლამების სრ ეფელში + ამოიღეთ არხი რეკლამების სრ ეფლიდან + კრეატორის მაღაზიის თაროს დამალვა + საბოლოო ეკრანის მაღაზიის ბანერის დამალვა + ზოგადი რეკლამების დამალვა + მერჩანდაიზინგის ბანერების დამალვა + ფასიანი რეკლამის იარლიყების დამალვა + მოთამაშის ფანჯრის სარეკლამო შეტყობინებების დამალვა + თვითსპონსორირებული ბარათების დამალვა + სავაჭრო ბმულების დამალვა + ვიდეორეკლამების დამალვა + YouTube Premium-ის პრომოუშენების დამალვა + იტვირთება… + მიმდინარე არხი: + დაამატეთ სრ ეფელში + ამოშლა + თეთრი სიაში მყოფი არხები + თეთრ სიაში არხები არ არის + არხი დაემატა თეთრ სიაში + არხი წაიშალა თეთრი სიიდან + ძებნა არხის შიგნით + ამატებს მეორე ძებნის ღილაკს არხების გვერდებზე მხოლოდ ამ არხში საძიებლად + არხის ძებნა + არხის ძებნის შედეგები + ამ არხზე არაფერი მოიძებნა + ამ არხის ძებნა ვერ მოხერხდა + ვიდეოები + პოსტები + ამ არხზე შესაბამისი პოსტები ვერ მოიძებნა + რელევანტურობა + ყველაზე ნახული + ნაკლებად ნახული + უახლესი + უძველესი + ყველაზე მოკლე + ყველაზე გრძელი + ბმული კოპირებულია გადმოწერილ დაფაზე + ბმული დროის აღნიშვნით დაკოპირდა ბუფერში + ვიდეო ბმულის კოპირების ღილაკის ჩვენება + დააჭირეთ ვიდეო ბმულის კოპირებისთვის, გრძლად დააჭირეთ დროის აღნიშვნით კოპირებისთვის + დროის აღნიშვნის კოპირება ნაგულისხმევი პარტნიორით + ცვლის \"ვიდეო ბმულის კოპირების\" ღილაკის დაჭერისა და გრძლად დაჭერის მოქმედებებს + ვიდეოს სათაურის კოპირება + დააჭირეთ და გეჭირავთ ვიდეოს სათაურს მის საკოპირებლად + ვიდეოს სათაური კოპირებულია ბუფერში + კომენტარების კოპირება + დააჭირეთ და გეჭირავთ კომენტარს მის საკოპირებლად + კომენტარი კოპირებულია ბუფერში + დააჭირეთ და გეჭიროთ ვიდეოს მენიუს გასახსნელად + ფიდის ვიდეოს ნებისმიერ ადგილას დაჭერით და გეჭიროთ მისი მენიუს გახსნა + აჩვენეთ \"მოგვიანებით ნახვა\" ღილაკი მინიატურებზე + ვიდეოების ფიდის მინიატიურებზე ჩნდება ღილაკი „ნახე მოგვიანებით“. მისი დაჭერა და nicht გაშვება ხსნის ფუნქციას „შენახვა ფლეილისტში\" + პოსტის სურათების ჩამოტვირთვა + სურათების ან GIF-ების მქონე პოსტების „გაზიარების“ ღილაკის გვერდით ჩნდება ჩამოტვირთვის ღილაკი, ხოლო მათ მენიუს ჩამოტვირთვის პუნქტი ემატება + სურათების ჩამოტვირთვა + სურათის ჩამოტვირთვა + %s სურათის ჩამოტვირთვა… + სურათის ჩამოტვირთვა… + %1$s სურათი შენახულია %2$s-ში + სურათი შენახულია %s-ში + სურათის ჩამოტვირთვა ვერ მოხერხდა + ამ პოსტში სურათი ვერ მოიძებნა + Ask-ის ელემენტის დამატება ჩამოსაშლელ მენიუში + ამატებს ვიდეოს შეჯამების ელემენტს ფიდის გასაშლელ მენიუში + "Ask-ისთვის თავდაპირველად საჭიროა შესული ანგარიში იმ ქვეყანაში, სადაც Ask ხელმისაწვდომია. + +მას შემდეგ, რაც ერთხელ გაიხსნება ვიდეო Ask ღილაკით, ფუნქცია იმუშავებს ყველა ვიდეოსთვის, იმ შემთხვევაშიც კი, თუ მიმდინარე ქვეყანაში Ask ფუნქცია არ არის ხელმისაწვდომი." + შეაჯამეთ ვიდეო + ამ ვიდეოსთვის Ask-ის გახსნა ვერ მოხერხდა + Ask არ არის ხელმისაწვდომი თქვენი ანგარიშისთვის ან მიმდინარე ქვეყნისთვის + აჩვენე ნახვების რაოდენობა მინიატურებზე + ფიდის ვიდეოების ნახვების რაოდენობა ჩანს მათი მინიატურების ქვედა მარცხენა კუთხეში და არა სათაურის ქვემოთ. ზოგიერთ ვიდეოს, მაგალითად თაროებში, შესაძლოა არ ჰქონდეს ნახვების რაოდენობა + ფიდის მინიატიურის მოწონებების რაოდენობის ჩვენება + აჩვენე ფიდის მინიატურის მოწონებების პროცენტული მაჩვენებელი + კომენტარების ძიება + დააჭირეთ ძებნის ღილაკს კომენტარებში, რათა დაინახოთ მხოლოდ ჩატვირთული კომენტარები, რომლებიც შეიცავს ყველა საძებნო სიტყვას. ფრაზის მოსაძებნად სიტყვები ჩასვით ბრჭყალებში + კომენტარების ძებნა + სიტყვები საძიებლად + ძიება + გასუფთავება + შეცვალეთ სორტირების რიგითი ან თავიდან გახსენით კომენტარები მათින් ჩასაძიებლად + ძიება გასუფთავდა. შეცვალეთ სორტირების क्रमი ან ხელახლა გახსენით კომენტარები ყველა კომენტარის სანახავად + გამორთეთ \'Sign in to TV\' ფანჯარა + გათიშეთ ფანჯარა \"დაუკავშირეთ თქვენი მოწყობილობები\" + თავის გადახტომის ორჯერ დაჭერით გათიშვა + გთიშავს ორი თითით ორჯერ დაჭერის ჟესტს, რომელიც თავებს გადაпускаვს და შესაძლოა შემთხვევით ერთი თითით გააქტიურდეს + ჩამოტვირთეთ ვიდეოები, პოდკასტები ან სიმღერები მესამე მხარის ჩამოტვირთვის აპლიკაციების მეშვეობით + მოთამაშის გადმოწერის ღილაკის ჩვენება + ვიდეო მოთამაშერში აჩვენებს ღილაკს, რომელიც ხსნის თქვენს გარე ჩამტვირთველს + სწრაფი ჩამოტვირთვა შეხლებით + გთავაზობთ ჩამოტვირთვას ფონურ რეჟიმში. დააჭირეთ და გეჭირთ ჩამოტვირთვის პროგრამის გასახსნელად. მუშაობს YTDLnis და YTindir-თან ერთად + მონიშნე ჩამოტვირთვები, როგორც ნანახი + გარე ჩამტვირთველით გადმოწერილი ვიდეოები ემატება თქვენს YouTube-ის ნახვების ისტორიას + Full HD (1080p) + საშუალო (192 kbps) + სწრაფი ჩამოტვირთვა + ვიდეო + აუდიო + ყველა ოფცია + ჩამოტვირთვის ჩვენება Shorts-ის მენიუში + ამატებს გარე ჩამტვირთავის ჩამოსაშლელ მენიუს ელემენტს Shorts-ის ప్ლეიერში + ჩამოტვირთვა + ხმის ზედsetოვანი თარგმანი + წაიკითხეთ ვიდეოს სუბტიტრები ხმამაღლა მოწყობილობის ტექსტის მეტყველების ფუნქციით, დაკვრასთან სინქრონულად + აჩვენებს ზემოთ მდებარე ღილაკს ვიდეო პლეერში. დააჭირეთ თარგმანის გადასართავად, დააჭირეთ და გეჭიროთ სწრაფი პარამეტრებისთვის + ენა + თარგმანის პროვაიდერი + ორიგინალი აუდიოს ხმა + YouTube-ის ორიგინალი აუდიოს ხმა თარგმანის წაკითხვისას + თარგმანის ხმოვნება + დუბლირებული თარგმანის ხმის ხმოვნება + მეტყველების მაქსიმალური სიჩქარე + სიმეტრიული სიჩქარის მაქსიმალური მულტიპლიკატორი მეტყველების სუბტიტრის სლოტში მოსაეტევად + "უფასო თარგმანის API, API გასაღები არ არის საჭირო. უფასო ტარიფით დღეში 5,000 სიმბოლო, ხოლო ელფოსტის მისამართით — დღეში 50,000 სიმბოლო. + +დააჭირეთ აქ MyMemory.translated.net-ის გასახსნელად" + MyMemory-ს ელფოსტა + სურვილისამებრ. შეიყვანეთ თქვენი MyMemory-ს ელფოსტის მისამართი, რათა დღიური ლიმიტი 5,000-დან 50,000 სიმბოლომდე გაიზარდოს. დატოვეთ ცარიელი ანონიმური ლიმიტის გამოსაყენებლად + "ფასიანი AI თარგმანი OpenRouter-ის მეშვეობით. შეავსეთ ბალანსი ერთხელ და თარგმნეთ ათასობით ვიდეო. Mistral Nemo ყველაზე იაფი ვარიანტია, Gemma 4 კი უმაღლეს ხარისხს გვთავაზობს. + +დააჭირეთ აქ OpenRouter.ai-ს გასახსნელად" + OpenRouter-ის API გასაღები + თქვენი OpenRouter-ის API გასაღები. შექმენით გასაღები OpenRouter.ai > Workspace > API Keys + OpenRouter-ის მოდელი + საკუთარი + OpenRouter არ არის კონფიგურირებული + ამ პროვაიდერის არჩევამდე, შეიყვანეთ თქვენი OpenRouter API გასაღები პარამეტრებში + უფასო + %s ყოველ 100 ვიდეო საათზე + ხმა + სისტემური + მულტიენოვანი მამაკაცი + მულტიენოვანი ქალი + მამრობითი + მდედრობითი + მორფინგის დროა! + თარგმანი недоступელია + "თარგმნის სერვისმა დააბრუნა HTTP შეცდომა %s. სავარაუდოდ, თქვენი IP მისამართი შეზღუდულია 24 საათამდე. + +თარგმნების გამოყენების გასაგრძელებლად, შეცვალეთ თარგმანის პროვაიდერი პარამეტრებში ან გამოიყენეთ VPN" + აღარ გამოჩნდეს + "თქვენი პარამეტრები შეიცავს OpenRouter API გასაღებს. + +თქვენი API გასაღები არასოდეს უნდა გააზიაროთ. ნებისმიერს, ვისაც მასზე წვდომა აქვს, შეუძლია გამოიყენოს თქვენი OpenRouter კრედიტები" + OpenRouter-ის კრედიტების ლიმიტი ამომწურავად არის გამოყენებული + თქვენი OpenRouter კრედიტების ლიმიტი ამომწურავად არის გამოყენებული. შეავსეთ ბალანსი ან გაზარდეთ გასაღების ხარჯვის ლიმიტი OpenRouter.ai-ზე + გახსენით OpenRouter.ai + OpenRouter-ის თარგმნა ვერ მოხერხდა (HTTP %s) + MyMemory-ს დღიური ლიმიტი исчерწულია + შემდეგი ხელმისაწვდომია %s + თქვენ გამოიყენეთ MyMemory-ს დღიური უფასო თარგმნის კვოტა (5,000 სიმბოლო). დაამატეთ MyMemory-ს ელ-ფოსტა პარამეტრებში, რათა დღიური ლიმიტი 50,000 სიმბოლომდე გაიზარდოს, ან გადაერთეთ სხვა თარგმანის სერვისზე + თქვენ გამოიყენეთ MyMemory-ს დღიური თარგმნის კვოტა (50,000 სიმბოლო). გადაერთეთ სხვა თარგმანის სერვისზე ან სცადეთ ხელახლა ხვალ + MyMemory-ს თარგმნა ვერ მოხერხდა (სტატუსი %s) + ჩართეთ ვიდეოს განმეორება + არ დაიმახსოვრო ვიდეოს განმეორება + თავიდან აცილეთ ვიდეოს განმეორების მიმდინარე მდგომარეობის შენახვა მომდევნო ვიდეოებისთვის + ვიდეოს ციკლურად გამეორების ღილაკის ჩვენება + ვიდეოს განმეორება ჩართულია + ვიდეოს განმეორება გამორთულია + ციკლის დიაპაზონი + დასაწყისი + დასასრული + [სა:]წწ:წწ + [სა:]წწ:წწ (ცარიელი = ვიდეოს დასასრული) + დაყენება + გასუფთავება + ციკლური განმეორება %1$s - %2$s + დროის არასწორი დიაპაზონი + სურათის სურათში ღილაკის ჩვენება + სურათის სურათში მენიუს ჩვენება + გადმორთვა სურათის სურათში რეჟიმზე + ყველას გაშვების ღილაკის ჩვენება + "დააჭირეთ არხის ყველა ვიდეოსგან პლეილისტის შესაქმნელად, დააჭირეთ და გეჭიროთ გაუქმებისთვის + +ინფო: შესაძლოა არ იმუშაოს პირდაპირ ეთერებში" + პლეილისტის გენერირების რეჟიმი + სრული კონტენტი (დალაგება დროის მიხედვით, ზრდად) + ყველა შინაარსი (დალაგება დროით) + ყველა შინაარსი (დალაგება პოპულარობით) + მხოლოდ ვიდეოები (დალაგება დროით) + მხოლოდ ვიდეოები (დალაგება პოპულარობით) + მხოლოდ Shorts (დალაგება დროით) + მხოლოდ Shorts (სორტირება პოპულარობით) + მხოლოდ სტრიმინგის ვიდეოები (სორტირება დროით) + მხოლოდ სტრიმინგის ვიდეოები (სორტირება პოპულარობით) + მხოლოდ წევრებისთვის განკუთვნილი კონტენტი + მხოლოდ წევრებისთვის განკუთვნილი ვიდეოები + მხოლოდ წევრებისთვის განკუთვნილი Shorts + მხოლოდ წევრებისთვის განკუთვნილი პირდაპირი ჩართვები + ფლეილისტის გენერირება შეუძლებელია არხის ID-ის შეუსაბამობის გამო + ვიდეოს გადატვირთვის ღილაკის ჩვენება + ვიდეოს ID недоступებელია. გთხოვთ, თავად გადატვირთოთ ვიდეო + აჩვენე ღილაკი „შენახვა მოგვიანებით საყურებლად“ + დააჭირეთ შენახვას „მოგვიანებით საყურებლად“, დააჭირეთ და გეჭიროთ რიგის პარამეტრებისთვის + ვიდეო წარმატებით შეინახა სიაში \"უგვიანეს ვუყურო\" + ეს ვიდეო უკვე შეინახულია სიაში \"უგვიანეს ვუყურო\" + ამ ვიდეოს შეინახვა სიაში \"უგვიანეს ვუყურო\" შეუძლებელია + შენახვა მოგვიანებით საყურებლად + აჩვენეთ ღილაკი „შენახვა მოგვიანებით საყურებლად“ მოთამაშის პარამეტრების მენიუში + სტრიმინგის პარამეტრების მენიუდან ამტვირთავს მიმდინარე ვიდეოს სექციაში „მოგვიანებით საყურებლად“ + აჩვენე ღილაკი „შენახვა მოგვიანებით საყურებლად“ ბავშვების ვიდეოებში + სამენიურო პანელიდან ამტვირთავს ბავშვებისთვის განკუთვნილ ვიდეოებს სექციაში „მოგვიანებით საყურებლად“ + აჩვენეთ ღილაკი „შენახვა მოგვიანებით საყურებლად“ Shorts-ის მენიუში + სამენიურო პანელიდან ამტვირთავს Shorts-ებს სექციაში „მოგვიანებით საყურებლად“ + გადაამისამართეთ „მოგვიანებით ნახვა“ ფლეილისტზე + მოგვიანებით ნახვა + დაამატეთ Morphe-ის რიგში + შეინარჩუნეთ რიგი სესიებს შორის + რიგი ნარჩუნდება აპლიკაციის გადატვირთვის შემდეგ + შენახვისა და რიგში დადების მოქმედებების გაცვლა + დააჭირეთ რიგის პარამეტრებისთვის, დააჭირეთ და გეჭიროთ შენახვას „მოგვიანებით საყურებლად“ + რიგის შემდეგი ტიპი ნაგულისხმები + ნატიური რიგი + Morphe რიგი + Morphe რიგი კლიენტის მხარეს + დაამატეთ ფიდის ჩამოსაშლელი მენიუს რიგის ელემენტი + ამატებს დამატებით ჩამოსაშლელ ელემენტს. ჩართეთ ეს, თუ „დამატება რიგში“ არ ჩანს + რიგი + რიგი (%s) + ყველას გასუფთავება + რიგში არაფერია + გამოიყენეთ \'Play next in queue\' ნებისმიერ ვიდეოზე + ახლა უკრავს + განიავეთ შემდეგი რიგში + გააგრძელეთ განრიგის ბოლო ელემენტით + გადაკვრიფა + წაშლა + დაემატა რიგში + ამ ვიდეოს განრიგში დამატება ვერ მოხერხდა + ფლეილისტების დამატება განრიგში შეუძლებელია + რიგი სავსეა + ვიდეოს ჩართვა ვერ მოხერხდა + რეკლამების თეთრი სია ჩამოსაშლელ მენიუში + ამატებს ჩამოსაშლელ ელემენტს, რომელიც საშუალებას იძლევა რეკლამები ჩაირთოს ამ ვიდეოს არხზე + ამ ფუნქციას შესაძლოა დასჭირდეს PotHelper-ის გამოყენება ან \"ვიდეო ნაკადების სპუფინგი\"-ს შეცვლა TV-თი ან Android VR ავტორიზაციით + სიჩქარის თეთრი სია ჩამოსაშლელ მენიუში + ამატებს ჩამოსაშლელ ელემენტს, რომელიც ამ ვიდეოს არხს ყოველთვის 1.0x სიჩქარით ჩართავს + არ ხართ ავტორიზებული + პლეილისტის ID არ მოიპოვება + რიგის სიტყვა ცარიელია + ვიდეოს ID არ მოიპოვეს + ვერ მოხერხდა: %s + სრილში დამატება ვერ მოხერხდა + სრილის შექმნა ვერ მოხერხდა + სრილიდან წაშლა ვერ მოხერხდა + სრილის შენახვა ვერ მოხერხდა + დაემატა სრილში + სრილი შეიქმნა + ამოღებულია რიგიდან + სრილი შეინახა %s-ში + Morphe-ის რიგი + დაამატეთ რიგში + დაამატეთ რიგში და გახსენით + დაამატეთ რიგში და ჩართეთ + დაამატეთ და გადატვირთეთ ვიდეო + ამოიღეთ რიგიდან + ამოიღეთ და გახსენით რიგი + ამოიღეთ და გადატვირთეთ ვიდეო + გახსენით რიგი + შეინახეთ რიგი ფლეილისტში + ფლეილისტი შენახულია + შენახულია %s-ში + სრილით გაწმენდა + სრილი გასუფთავებულია + სრილი + გააწყვეტინეთ სრილის ბოლოს + გააწყვეტინეთ სრილში შემდეგი + YouTube-მა ვერ შეძლო სრილის ჩვენება. + სრილი არ არის მზად ამ ვიდეოსთვის. გახსენით ვიდეოპლეერი და სცადეთ თავიდან. + დაამატოთ მიქსი სრილში თუ გსურთ მიქსის ნახვა? + ნახვა + დაამატეთ რიგში + Mix-ის ჩატვირთვა… + არხის გახსნა პირდაპირი ეთერის ავატარიდან + თავიდან აგდებს პირდაპირი ეთერის გახსნას არხის ავატარზე დაჭერისას + ზუსტი გადამ winding-ის ჟესტის გამორთვა + გთიშავს ზუსტი ძებნის რეჟიმს, რომელიც აქტიურდება სლაიდერის ზემოთ გაწევისას + ჩართეთ პირდაპირი ჩართვის DVR + იძლევა ყველა პირდაპირ ტრანსლაციაზე გადაwinding-ის შესაძლებლობას, იმ შემთხვევაშიც კი, როდესაც ეს ჩვეულებრივ შეზღუდულია + გადამ winding-ის მინიატურების ჩართვა + აჩვენებს ვიდეოს მინიატურას გადამ winding-ის გადაადგილებისას + ყველაზე ხშირად განმეორებული + "'პირდაპირი ტრანსლაციის DVR-ის ჩართვა' შესაძლოა недоступებლი იყოს + +ამის გამოსასწორებლად, ჩართეთ 'ვიდეო ნაკადების społუფირება' და შეცვალეთ 'ნაგულისხმევი კლიენტი' მნიშვნელობouთ 'TV Simply' ან 'visionOS'" + პირდაპირი ტრანსლაციის DVR-ის ხანგრძლივობის გაზრდა + გძლევთ პირდაპირი ტრანსლაციების 7 დღით უკან გადაwinding-ის შესაძლებლობას + "'პირდაპირი ტრანსლაციის DVR ხანგრძლივობის გაზრდა' შესაძლოა недоступებელი იყოს + +ამის გამოსასწორებლად ჩართეთ 'ვიდეო ნაკადების იმიტაცია' და შეცვალეთ 'ნაგულისხმევი კლიენტი' მნიშვნელობით 'TV Simply' ან 'visionOS'" + ჩართეთ გადართვისთვის სლაიდირება + რთავს სლაიდერით ძებნას 2-ჯერ სიჩქარით უკმისა და გრძელვად დაჭერის ნაცვლად + ჩართეთ გადახვევა შეხებით + უბრალოდ დააჭირეთ სლაიდერს იმ პოზიციაზე გადასასვლელად გაწევის გარეშე + მარცხნივ სვაიპის ჟესტი + მარჯვნივ სვაიპის ჟესტი + ზემოთ სვაიპის ჟესტი + მარცხენა/მარჯვენა ზონის სიგანე + ეკრანის თითოეული კიდის პროცენტული წილი, რომელიც დაჯავშნილია მარცხენა და მარჯვენა სვაიპ ჟესტებისთვის (5-50) + სიკაშკაშე + ხმა + ნატიური + გამორთვრა + სვაიპების იგნორირება ჩაკეტვისას + სვაიპ ჟესტები იგნორირდება, როდესაც პლეიერი ჩაკეტილია ფუნქციით \"ეკრანის ჩაკეტვა\" + ჩართეთ გადასმევის ჟესტი დაჭერით + სანამ სვაიპის ჟესტი将被 recognizes, საჭიროა დაჭერა და and hold + ჩართეთ ჰაპტიკური უკუკავშირი + სინათლის შენახვა და აღდგენა + იმახსოვრებს სიკაშკაშის დონეს სრულად ეკრანზე გადასვლისას / გამოსვლისას + ჩართეთ სინათლის ავტომატური რეგულირების ჟესტი + სიკაშკაშის მინიმალურ მნიშვნელობამდე დაწევა რთავს სისტემის ავტო-სიკაშკაშეს + ავტომატური + გადასმევის ფენის დროის ლიმიტი + მილიწამების რაოდენობა, რომლის განმავლობაშიც ფენა ხილულია + გადასმევის ფენის ფონის გამჭვირვალობა + გაასწორეთ გამჭვირვალობის დონე (0 არის გამჭვირვალე, 100 არის andუგადის) + სვაიპის ზეფენის სიკაშკაშის ფერი + სიკაშკაშის მართვის პროგრესის ზოლის ფერის დაყენება + სვაიპის ზეფენის ხმის ფერი + ხმის მართვის პროგრესის ზოლის ფერის დაყენება + სვაიპის ფენის სიჩქარის ფერი + განსაზღვრეთ დაკვრის სიჩქარის კონტროლის პროგრესის ზოლის ფერი + სვაიპის ზეფენის ტექსტის ზომა + დაარეგულირეთ ეკრანზე გამოსახული ტექსტის ზომა (1-30) + სვაიპის სიდიდის ზღვარი + განსაზღვრეთ სლაიდის საჭირო მანძილი რეგულაციის გასააქტიურებლად + ხმის რეგულირების სვების სენსიტივობა + მაღალი მნიშვნელობები მოითხოვს მეტ სვებას ხმის ყოველი საფეხურისთვის + ხმის საფეხურები + ხმის გაძლიერება + მაქსიმალური ხმის დონის ზემოთ სვა მას კიდევ უფრო ამაღლებს, რამაც შესაძლოა ხმის დამახინჯება გამოიწვიოს + მოწყობილობის სტანდარტი + სიკაშკაშის სლაიდინგის მგრძნობელობა + მაღალი მნიშვნელობები სიკაშკაშის ყოველი ნაბიჯისთვის მეტ სლაიდინგს მოითხოვს + სიჩქარის სლაიდინგის მგრძნობელობა + მაღალი მნიშვნელობები სიჩქარის ყოველი ნაბიჯისთვის მეტ სლაიდინგს მოითხოვს + სიჩქარის ნაბიჯი + სიჩქარის ზონის სიმაღლე + სიჩქარის ჟესტების ზონის სიმაღლე პლეიერის სიმაღლის პროცენტულად (5-75) + სიჩქარე + სვაიპის ზეფენის სტილი + ჰორიზონტალური ზეფენი + ჰორიზონტალური ზეფენი (მინიმალისტური - ზემოთ) + ჰორიზონტალური ზეფენი (მინიმალისტური - ცენტრში) + წრესმაგვარი ზეფენი + წრიული ზეფენა (მინიმალისტური) + ვერტიკალური ზეფენა + ვერტიკალური ზეფენა (მინიმალისტური) + ჩართეთ ვიდეოების შესაცვლელად გადასვლების ფუნქცია + გადაისლაიდეთ სრული ეკრანის რეჟიმში შემდეგ ან წინა ვიდეოზე გადასასვლელად + მოქმედების ღილაკები + აირჩიეთ, რომელი მოქმედების ღილაკები გამოჩნდეს პორტრეტულ და სრული ეკრანის რეჟიმებში + პორტრეტული რეჟიმის ღილაკები + გამორთეთ მოწონებისა და გამოწერის ღილაკების განათება + თავიდან აგვაცილებს \"მოწონების\" და \"გამოწერის\" ღილაკების განათებას, როდესაც კრეატორი ახსენებს მათ + დამალეთ მოქმედებების პანელი + მოწონებისა და არმოწონების ღილაკების დამალვა + გაზიარების ღილაკის დამალვა + ღილაკის \"გააჩერეთ რეკლამები\" დამალვა + კომენტარების დამალვის ღილაკი + ღილაკის \"დაკავშირება\" დამალვა + ღილაკის „შეტყობინება“ დამალვა + ღილაკის „რემიქსი“ დამალვა + ღილაკის „გადმოწერა“ დამალვა + ღილაკის „ჰაიპი“ დამალვა + ღილაკის „პრომოცია“ დამალვა + ღილაკის „მადლობა“ დამალვა + ღილაკის „კითხვა“ დამალვა + არხისა და გამოწერის ღილაკის დამალვა + მალავს არხის ავატარს და გამოწერის ღილაკს მოქმედებათა პანელის დასაწყისში + გამოწერის ღილაკის დამალვა + მალავს გამოწერისა და შეერთების ღილაკებს არხის ავატარის გვერდით მოქმედებათა პანელის დასაწყისში + მომჭრელების ღილაკის დამალვა + ეს შესაძლოა არ იმუშაოს ზოგიერთი მომხმარებლის ანგარიშისთვის + მაღაზიის ღილაკის დამალვა + შენახვის ღილაკის დამალვა + ღილაკის \"მეტი\" დამალვა + სრულეკრანიანი რეჟიმის ღილაკები + მოწონების αντίწონების ღილაკის დამალვა + მოწონების ღილაკის დამალვა + ღილაკის \"ცოცხალი ჩატირ“ის დამალვა + მიქსის ღილაკის დამალვა + ღილაკის \"მეტი ვიდეო\"-ს დამალვა + ప్ლეილისტის ღილაკის დამალვა + სწრაფი მოქმედებების ზედა გარდამავალი სივრცე + განსაზღვრეთ სწრაფი მოქმედებების პანელის ზედა კიდის დაცილება (0-32 dp) + სუბტიტრები + ავტომატური სუბტიტრების და სუბტიტრების ქუქი-ფაილების მართვა + ავტომატური სუბტიტრების ხილვადობა + ყოველთვის ჩვენება + არასოდეს გამოაჩინოს + ჩვენება ხმის ჩართვისას + გამოაჩინოს ხმის გათიშვისას + სუბტიტრების სასურველი ენა + ჯერ ირჩევს პროვაიდერის სუბტიტრებს, შემდეგ კი ავტომატურად თარგმნის მათ სასურველ ენაზე + გამორთვებული + დაამატეთ ავტომატური თარგმანის დამატებითი ენები + ამატებს მეტი ენას ავტომატური თარგმანის მენიუში + სუბტიტრების ქუქი-ფაილების დაყენება + "ასწორებს YouTube-ის სტანდარტულ შეცდომას, როდესაც სუბტიტრების ავტომატური თარგმანი შესაძლოა არ იყოს ხელმისაწვდომი cookies-ების არარსებობის გამო + +უნდა მიუთითოთ \"Caption cookies\"" + სუბტიტრების ქუქი-ფაილები + Cookies, რომლებიც უნდა დაისetelოს Timed Text API (Caption Data API) მოთხოვნებში + სუბტიტრების ქუქი-ფაილების მიღება + დააჭირეთ აქ cookies-ების მოსაგზავნად + ქუქი-ფაილების წამოღება ვერ მოხერხდა + ქუქი-ფაილები უკვე არსებობს + ქუქი-ფაილები წარმატებით განახლდა + ტრანსკრიპტის გასწორება + ასწორებს წინაპირობის შემოწმების შეცდომას, რამაც შესაძლოა ტრანსკრიპტის ჩატვირთვა წაშალოს + სუბტიტრების ჩვენება სურათში-სურათის რეჟიმში + ნავიგაციის ღილაკები + აირჩიეთ, რომელი ღილაკები გამოჩნდეს ქვედა навіგაციის პანელზე + მთავარი ღილაკის დამალვა + Shorts ღილაკის დამალვა + დამალეთ შექმნის ღილაკი + დამალეთ გამოწერების ღილაკი + დამალეთ შეტყობინებების ღილაკი + აჩვენეთ ძიების ღილაკი + ძიების ღილაკის პოზიცია + აჩვენეთ პარამეტრების ღილაკი + პარამეტრების ღილაკის პოზიცია + პარამეტრების ღილაკის მოქმედება + გახსნის Morphe-ის პარამეტრებს ჩართვისას, ან YouTube-ის პარამეტრებს გამორთვისას + შექმნის ღილაკის შეცვლა შეტყობინებების ღილაკით + თუ ამ პარამეტრის შეცვლა შედეგს არ ఇძლევა, სცადეთ ინკოგნიტო რეჟიმზე გადართვა + დამალეთ ნავიგაციის ღილაკების წარწერები + ახალი კონტენტის წერტილის დამალვა + დამალეთ навіგაციის პანელი + "ეს სრულად მალავს навіგაციის პანელს, მათ შორის 'თქვენ' ჩანართს. + +პარამეტრებში შესასვლელად, გააქტიურეთ პარამეტრების ღილაკი: ზოგადი → ინსტრუმენტების პანელი → პარამეტრების ჩვენება" + გააქტიურეთ ვიწრო навіგაციის ღილაკები + ამცირებს ჰორიზონტალურ დაცილებას ღილაკებს შორის + გათიშოს ნავიგაციის ღილაკები წარწერების გარეშე + აჩვენებს ნავიგაციის ღილაკებს წარწერებით, თუ YouTube მათ წარწერების გარეშე გზავნის + გამორთეთ ხელსაწყოების პანელის навіგაციის ჩანართები + აჩვენებს გამოწერებს ნავიგაციის პანელში, თუ YouTube მას მთავარი გვერდის ზედა ჩანართში გადაიტანს + ჩართეთ ნავიგაციის პანელის ანიმაციები + ანიმირებს გადასვლებას ტაბებს შორის გადართვისას + გამოთიშე навіგაციის პანელის ავტომატური დამალვა + ინარჩუნებს навіგაციის პანელის ხილვადობას სქროლვისას + ნავიგაციის პანელის სტილი ნაგულისხმები + ნახევრადგამჭვირვალე + გამჭვირვალე + გამუქებული + ეს სტილი შესაძლოა არ იყოს მხარდაჭერილი თქვენი მოწყობილობის ან YouTube-ის ვერსიის მიერ, რამაც შეიძლება გამოიწვიოს განლაგების პრობლემები ან გათიშვები + ინსტრუმენტების პანელი + აირჩიეთ, რომელი კომპონენტები გამოჩნდეს ხელსაწყოების პანელზე + Cast ღილაკის დამალვა + დამალეთ ჩატის ღილაკი + მიკროფონის ღილაკის დამალვა + ძებნის ღილაკის დამალვა + ჩართვისას, დაჭერით იხსნება Morphe-ის პარამეტრები, ხოლო გრძელი დაჭერით YouTube-ის პარამეტრები; გამორთეთ მათი გასაცვლელად + Flyout მენიუ + აირჩიეთ, რომელი კომპონენტები გამოჩნდეს გასაშლელ მენიუში + \"დამატებითი პარამეტრების\" / \"მეტი\" მენიუს დამალვა + დამალეთ მენიუ „გარემო განათება“ + "\"აუდიო ტრეკის\" მენიუ დამალულია + +\"აუდიო ტრეკის\" მენიუს გამოსაჩენად, შეცვალეთ \"ვიდეო ნაკადების იმიტაცია\" ნებისმიერი კლიენტის სახელად, გარდა \"Android Studio\"-სა და \"Android VR Downgraded\"-ისა" + \"აუდიოტრეკის\" მენიუს დამალვა + \"აუდიოტრეკის\" მენიუს ქვედა ნაწილის დამალვა + \"სუბტიტრების\" მენიუს დამალვა + \"სუბტიტრების\" მენიუს ქვედა ნაწილის დამალვა + \"სუბტიტრების\" მენიუს ზედა ნაწილის დამალვა + დამალეთ მენიუ „დახმარება და უკუკავშირი“ + დამალეთ მენიუ „მოსმენა YouTube Music-ით“ + დამალეთ მენიუ „ეკრანის დაბლოკვა“ + დამალეთ მენიუ „ვიდეოს განმეორება“ + დამალეთ მენიუ \"გზაში\" + დამალეთ ხარისხის მენიუ + დამალეთ ხარისხის მენიუს ქვედა ნაწილი + დამალეთ ხარისხის მენიუს ზედა ნაწილი + \"ძილის ტაიმერის\" მენიუს დამალვა + დამალეთ მენიუ „აღდგენის სიჩქარე“ + დამალეთ მენიუ „სტაბილური ხმა“ + დამალეთ მენიუ „ნახვა VR-ით“ + დამალეთ ავტომატური განახლების წინასწარი просмоტვები + მალავს მომდევნო ვიდეოს პრევიუს სრულអេკრანზე მანამ, სანამ ავტოაღ रिप्रोडება დაიწყება + ზედა ფენის ღილაკები + აირჩიეთ, რომელი ღილაკები გამოჩნდეს ვიდეო პლეერის თავზე + დამალეთ ავტოაღ रिप्रोडების ღილაკი + სუბტიტრების დამალვის ღილაკი + Cast ღილაკის დამალვა + Collapse ღილაკის დამალვა + მთლიან ეკრანზე გადაყვანის ღილაკის დამალვა + მოთამაშის მართვის ღილაკების ფონის დამალვა + ამალებს მუქ განათებას ვიდეო მოთამაშის მართვის ღილაკების გარშემო + მართვის ღილაკების ფონის გამჭვირვალობა + დაარეგულირეთ ფონის გამჭვირვალობა პლეერის მართვის ღილაკებისა და დროის ზოლის უკან (0 არის უხილავი, 100 არის სრულად ფერადი) + წინა & შემდეგი ღილაკების დამალვა + პარამეტრების ღილაკის დამალვა + დასრულების ეკრანის ბარათების დამალვა + Ambient რეჟიმი + მართეთ Ambient რეჟიმის ხილვადობა და ენერგიის შეზღუდვები + Ambient რეჟიმის შეზღუდვების გვერდის ავლით + საშვები Ambient რეჟიმს საშუალებას აძლევს დარჩეს აქტიური ელემენტის დაზოგვის რეჟიმში + Ambient რეჟიმის გამორთვა + Ambient რეჟიმის გამორთვა სრული ეკრანის დროს + ინფორმაციული ბარათების დამალვა + ციფრების滚动 ანიმაციების გამორთვა + აჩერებს დათვლის ანიმაციას მოწონებებისა და ნახვების რაოდენობისას + ვიდეო მოთამაშის seekbar-ის დამალვა + ვიდეო მინიატიურების seekbar-ის დამალვა + მალავს ნახვის პროგრესის ზოლს, რომელიც ჩანს უკვე ნანახი ვიდეოების მინიატურებზე + სრული ეკრანის დიდი seekbar-ის ჩართვა + Shorts-ების დამალვა + დამალვა არხის გვერდზე + დამალვა მთავარ გვერდსა და დაკავშირებულ ვიდეოებში + დამალვა ძიების შედეგებში + დამალვა გამოწერებების სტრიმში + დამალეთ ვიდეოს აღწერაში + დამალეთ ნახვის ისტორიაში + Shorts-ის პლეერი + აირჩიეთ, რომელი კომპონენტები გამოჩნდეს Shorts-ის პლეერში + გამოთიშეთ ორჯერ დაჭერით მოწონება + 숨기기 AI ღილაკი + \"ავტომატურად გახმოვანებული\" სტიკერის დამალვა + არხის პანელის დამალვა + კომენტარების ღილაკის დამალვა + ეფექტის ღილაკის დამალვა + ვიდეოს ბმულის ეტიკეტის დამალვა + ჟესტების მინიშნებების დამალვა + \"მწვანე ეკრანის\" ღილაკის დამალვა + ჰეშთეგის ღილაკის დამალვა + ინფორმაციის პანელის დამალვა + ღილაკის \"გაწევრიანება\" დამალვა + მოწონების ღილაკის დამალვა + მოწონების ღილაკის ფრანკოსტამბის ანიმაციის დამალვა + ცოცხალი წინასწარი ნახვის დამალვა + ლოკაციის სტიკერის დამალვა + ნავიგაციის პანელის დამალვა + \"ახალი პოსტების\" ღილაკის დამალვა + პაუზის დროს გამოჩენილი ღილაკების დამალვა + კომენტარის პრევიუს დამალვა + რემიქსის ღილაკის დამალვა + დამალეთ შენახვის ღილაკი + 숨기기 \'მუსიკის შენახვის\' ღილაკი + ძიების რეკომენდაციების დამალვა + გაზიარების ღილაკის დამალვა + მაღაზიის ღილაკის დამალვა + ხმის ღილაკის დამალვა + ხმის მეტამონაცემების ეტიკეტის დამალვა + დამალეთ გამოწერის ღილაკი + \"Super Thanks-ის ყიდვა\" ღილაკის დამალვა + მონიშნული პროდუქტების დამალვა + მომავლის ღილაკის დამალვა + დამალეთ ღილაკი \"გამოიყენეთ ეს ბგერა\" + დამალეთ ღილაკი \"გამოიყენეთ ეს შაბლონი\" + ვიდეოს სათაურის დამალვა + საბოლოო ეკრანზე რეკომენდებული ვიდეოს დამალვა + "მალავს რეკომენდებული ვიდეოს ზედსართავს, რომელიც ჩნდება ვიდეოს დასრულების შემდეგ + +ეს პარამეტრი მუშაობს მხოლოდ იმ შემთხვევაში, თუ ავტომატური nicht-აღვთქმა გამორთულია. თქვენ შეგიძლიათ შეცვალოთ იგი YouTube-ში: +პარამეტრები → გამოსახულება (ან ავტომატური აღვთქმა) → შემდეგი ვიდეოს ავტომატური აღვთქმა" + დამალეთ დაკავშირებული ვიდეოები + \'მეტი ვიდეო\' ფენის დამალვა + მალავს \'მეტი ვიდეო\' ფენას, რომელიც ჩანს სრული ეკრანის რეჟიმში ზემოთ გადასვლისას + ვიდეოს დროის აღნიშვნის დამალვა + მოთამაშის ფანჯრის პანელების გამორთვა + თიშავს პლეილისტისა და პირდაპირი ჩატის პანელების ავტომატურად გახსნას + გამოიყვეთ პლეილისტის ავტომატური დაკვრა + თავიდან აცილებს პლეილისტების ავტომატურად შემდეგ ვიდეოზე გადასვლას + გამოთიშეთ შეტყობინება \"გსურთ ყურების გაგრძელება\" + გასვლა სრული ეკრანის რეჟიმიდან ვიდეოს დასრულებისას + გამორთულია + მხოლოდ პორტრეტული + მხოლოდ ლანდშაფტური + ვერტიკალური და ჰორიზონტალური + გამოიძージეთ ჰორიზონტალური რეჟიმი სრულ ეკრანზე + სრულ ეკრანზე გადასვლისას მოთამაშის გადაყვანა ჰორიზონტალურ რეჟიმში. nedost கிடைக்கებელია მცირე ეკრანებზე, სადაც სრული ეკრანი ავტომატურად იხსნება ჰორიზონტალურად + მთელეკრანიანი ჟესტები + მოთამაშის ჟესტები სრული ეკრანის რეჟიმში + გამოიყვეთ პორტრეტული რეჟიმის ზემოთ სვების ჟესტი + გამოიყვეთ პორტრეტული რეჟიმის ქვემოთ სვების ჟესტი + გამოიყვეთ ლანდშაფტური რეჟიმის ქვემოთ სვების ჟესტი + გამოიყვეთ pinch-to-zoom ჟესტი + მასშტაბირების pinch to zoom-ის აღდგენა + ვიდეოების გახსნა სრული ეკრანის რეჟიმში + გამორთვებული + ვერტიკალური + ჰორიზონტალური + სრულეკრანიანი ვიდეოს მასშტაბი ნაგულისხმები + გაწელვა (ეკრანის შევსება) + ზუმი (შესაბამისად ჩამოჭრა) + ვიდეოს მასშტაბის ღილაკის ჩვენება + დააჭირეთ ნაგულისხმევი, გაწელვისა და ზუმის რეჟიმების შესაცვლელად + აჩვენე მხოლოდ სრულეკრანზე + ვიდეოს მასშტაბირების ღილაკის დამალვა პორტრეტულ პლეიერში + ხმის გათიშვის ღილაკის ჩვენება + პლეიერის ზედაფენის გამჭვირვალობა + გაასწორეთ გამჭვირვალობის დონე (0 არის გამჭვირვალე, 100 არის andამჭვირვალე) + საძიებო პანელი + გათიშულია + ფართო + ზედმეტად ფართო + მოწყობილობის განლაგება + ნაგულისხმები + ტელეფონი + პლანშეტი + ავტომობილი + "ცვლილებები მოიცავს: + +პლანშეტის განლაგება +• საზოგადოების პოსტები დამალულია +• ფიდებში დაკვრის პარამეტრს არ აქვს წვდომა +• Remix-ისა და ბგერის ღილაკები არ არის ხელმისაწვდომი Shorts-ში +• „მეტი ვიდეო“ გადაფარვა შესაძლოა არ იყოს ხელმისაწვდომი +• ჩანს ვიდეოს ძველი მოქმედებების პანელი, რომელიც არ არის ჩაკეცილი +• ჩამოსაშლელი მენიუს ელემენტებმა შესაძლოა არ იმუშაონ სწორად + +ავტომობილის განლაგება +• ფიდი ორგანიზებულია თემებისა და არხების მიხედვით +• ფიდებში დაკვრის პარამეტრს არ აქვს წვდომა +• Remix-ისა და ბგერის ღილაკები არ არის ხელმისაწვდომი Shorts-ში +• „მეტი ვიდეო“ გადაფარვა შესაძლოა არ იყოს ხელმისაწვდომი +• Shorts იხსნება ჩვეულებრივ პლეერში +• ჩანს ვიდეოს ძველი მოქმედებების პანელი, რომელიც არ არის ჩაკეცილი" + გააქტიურეთ პლანშეტის განლაგება პლეიერში + "აღადგენს პლეერის ძველ განლაგებას + +შეზღუდვა: თუ \"ვიდეოს ძველი მოქმედებების პანელის აღდგენა\" გამორთულია, \"მეტი ვიდეო\" ზეფენა შესაძლოა недоступებელი იყოს" + 21.11.48 - სუბტიტრების ძველი ქცევის აღდგენა + 20.39.41 - პლეილისტში შენახვის ძველი ვერსიის აღდგენა + 20.28.41 - სქელი ხაზის მქონე იკონების გამორთვა + 20.13.41 - აღადგინეთ არხის სახელი ვიდეო პლეერის ქვემოთ + სისტემური გაზიარების ფანჯრის გახსნა + კონტენტის გაზიარებისას ხსნის სისტემურ გაზიარების პანელს აპლიკაციის შიდა პანელის ნაცვლად + YouTube Music-ის ღილაკების ჩანაცვლება + აღებს თქვენს სასურველ მუსიკალურ აპლიკაციას YouTube Music-ის სტანდარტული აპლიკაციის ნაცვლად + მუსიკალური აპლიკაციის პაკეტის სახელი + თქვენი დაინსტალირებული მუსიკალური აპლიკაციის პაკეტის სახელი + საწყისი გვერდი + ნაგულისხმები + ყველა გამოწერა + არხების დათვალიერება + კურსები / სწავლა + კვლევა + მოდა & სილამაზე + გეიმინგი + ისტორია + ბიბლიოთეკა + მოწონებული ვიდეოები + პირდაპირი ეთერი + ფილმები + მუსიკა + სიახლეები + შეტყობინებები + ფლეილისტები + ძებნა + შოპინგი + Shorts + სპორტი + გამოწერები + პოპულარული + ვირტუალური რეალობა + ნახვა მოგვიანებით + თქვენი კლიპები + გათიშეთ გაგრძელება გაშვებისას + გახსენით Shorts შემდეგი საშუალებით + Shorts-ის პლეერი + ჩვეულებრივი ప్ლეიერი + ჩვეულებრივი პლეიერი მთელ ეკრანზე + Shorts-ის ავტომატური დაკვრა + ავტომატურად გადადის შემდეგ Short-ზე მიმდინარესის განმეორების ნაცვლად + ავტომატური დაკვრა ფონურ რეჟიმში + ფონურ რეჟიმში დაკვრისას ავტომატურად გადადის შემდეგ Short-ზე + ცოცხალი Shorts-ების გამოტოვება + 飛ばებს პირდაპირ ეთერებს, რომლებიც ჩნდება Shorts-ის ფიდში + აღចვევა ფიდებში + ყოველთვის ჩართული + მხოლოდ Wi-Fi-ით + გამორთვას + ორიგინალი სათაურების აღდგენა + აჩვენებს ვიდეოს ორიგინალ სათაურებს, ვიდეოს აღწერილობებსა და არხის აღწერილობებს ავტომატურად თარგმნილის ნაცვლად + სათაურის ჩატვირთვა… + გამორთეთ ფიდის ავტომატური განახლება + თავიდან აგდებს ფიდების ავტომატურ განახლებას მათი მოძველების შემდეგ + ფიდის ბარათის განლაგება + კლასიკური + ბარათები + ბარათები მოწონების, კომენტარის და შენახვის ღილაკებით + კომენტარები + მინი-პლეიერის განახლების გათიშვა + გათიშავს „გააგრძელეთ ყურება“-ს მინი-პლეერს, YouTube Premium-ის ფუნქციას, რომელიც ჩნდება აპლიკაციის გაშვებისას + მინიპლეიერი + მორგეთ მინიმალისტური ვიდეო პლეერის გარეგნული სახე და ქცევა + მინიპლეიერის ტიპი + სათაურისა და არხის დამალვა + გამორთვებული + ნაგულისხმები + მინიმალისტური + მინიმალისტური 2 + პლანშეტი + თანამედროვე 1 + თანამედროვე 2 + თანამედროვე 3 + თანამედროვე 4 + დამრგვალებული კუთხეების გათიშვა + Drag and drop-ის გათიშვა + 금იკვეთს მინიმალისტური პლეერის გადატანა ეკრანის სხვადასხვა კუთხეში + ჰორიზონტალური გადატაცების ჟესტის გათიშვა + თავიდან აცილებს მინი-პლეიერის დახურვას მისი ჰორიზონტალურად ეკრანის გარეთ გადატანით + შეაჩერეთ დაკვრა ჰორიზონტალური გადატარებით + აღ रिप्रोडუქცია შეჩერდება, როდესაც მინი-პლეიერი ეკრანიდან გარეთ将被აჩოჩხალებს + გადატარების მდგომარეობის პოზიციის মনেკვრევა + მინი-პლეიერის ეკრანის გარეთ გადატანა შეინარჩუნებს მის ინტერფეისის მდგომარეობას + ზედა ფენის ღილაკების დამალვა + სტანდარტული ზომა + განსაზღვრეთ საწყისი ზომა ეკრანზე, პიქსელებში + პიქსელის ზომა უნდა იყოს %1$s-სა და %2$s-ს შორის + Overlay-ის გამჭვირვალობა + დაარეგულირეთ გამჭვირვალობის დონე (0 არის გამჭვირვალე, 100 არის andგაუმჭვირვალე) + უკან დაბრუნების ღილაკი გამოდის ფიდიდან + უკან დაბრუნების ღილაკის დაჭერა ყოველთვის გამოდის აპლიკაციიდან, ნაცვლად იმისა, რომ ფიდის დასაწყისზე ავიდეს + ანიმაციების იძულებით ჩართვა ნაგულისხმები + ყოველთვის ჩართულია + ყოველთვის გამორთულია + სტატუსის პანელის დამალვა + მალავს სისტემის სტატუსის პანელს. მისი დროებითი გამოჩენისთვის ჩამოსვލეთ ზედა კიდიდან + სტატუსის პანელის გამოსწორება, რომელიც ფარავს ზედა პანელებს + ინარჩუნებს სტატუსის პანელის განლაგების თანმიმდევრულობას, რათა მან არ დაფაროს ზედა პანელები და არ დატოვოს ცარიელი სივრცე პლეერის ზემოთ. გამორთეთ ეს ფუნქცია, თუ აპლიკაცია ითიშება ან ზედა პანელები არასწორად ჩანს + ნათება + Morphe Glow 1 + Morphe Glow 2 + Glow ეფექტის საწყისი ეკრანი არ არის მხარდაჭერილი ამ მოწყობილობის მიერ + ღია თემის ფონი + თეთრი + Material You (თეთრი) + ღია ვარდისფერი + ღია ცისფერი + ღია მწვანე + ღია ყვითელი + ღია ნარინჯისფერი + ღია წითელი + საკუთარი ღია ფონი + ღია თემის ფონის ფერის დაყენება + წინა პლანის ფერის შეცვლა + თემის ფერის გამოყენება ტექსტისა და ხატულებისთვის + ჩატვირთვის ეკრანის გრადიენტის ჩართვა + ჩატვირთვის ეკრანზე გამოსახავს გრადიენტულ ფონს ერთფეროვანი ფერის ნაცვლად + seekbar-ის მორგებული ფერის ჩართვა + Seekbar-ის ფერი + განსაზღვრეთ ვიდეოს პროგრესის ზოლის ძირითადი ფერი + Seekbar-ის აქცენტირებული ფერი + განსაზღვრეთ ვიდეოს პროგრესის ზოლის მეორადი ფერი + seekbar-ის ფერის არასწორი მნიშვნელობა + ორიგინალი ანალოგი + ორიგინალი ტორტი + ორიგინალი მეტალი + Header-ის ლოგო + ნაგულისხმები + ჩვეულებრივი + საკუთარი + გამიჯნ시오 სურათების რეგიონალური შეზღუდვები + გადაამისამართეთ მინიატურების მოთხოვნები ალტერნატიულ სერვერზე შეზღუდული ქვეყნების მიერ გამოწვეული გამოსახულებების ნაკლებობის გამოსასწორებლად + DeArrow სათაურები + მთავარი ჩანართი + გამოწერების ჩანართი + თქვენ + პლეიერის ფლეილისტები & რეკომენდაციები + ძებნის შედეგები + DeArrow ხატულის ჩვენება + აჩვენებს DeArrow-ის ხატულას კრაუდსორსინგის ვიდეოების სათაურებზე + არაფორმალური რეჟიმი + აჩვენებს ორიგინალ სათაურს DeArrow-ის სათაურის ნაცვლად, თუ DeArrow-ის მომხმარებლებმა ხმა მისცეს, რომ ორიგინალი სათაური მოსწონთ + არაფორმალური ხმების მინიმუმი ორიგინალი სათაურის საჩვენებლად + ერთ კატეგორიაში, მაგალითად სასაცილო ან კრეატიული, იმ არაფორმალური ხმაების რაოდენობა, რომელიც საჭიროა ორიგინალი სათაურის საჩვენებლად + DeArrow მინయాტიურები + ორიგინალი მინიატურები + DeArrow & ორიგინალი მინიატურები + DeArrow & სტატიკური კადრები + სტატიკური კადრები + "DeArrow ანაცვლებს კლიკბეიტურ სათაურებსა და მინయాტურებს საზოგადოების მიერ შექმნილით, რომლებიც უფრო ზუსტია და ნაკლებად სენსაციური. სათაურებს წერს და ხსნის საზოგადოება, ხოლო მინიატურები არის კადრები აღებული ვიდეოდან + +ვიდეოები მოთხოვნილია ჰეშირებული ვიდეო ID-ის მოკლე პრეფიქსით, ამიტომ სერვერმა ზუსტად ვერ განსაზღვრავს, რომელ ვიდეოს ნახულობთ. თუ ვიდეოს არ აქვს DeArrow-ის მინიატურა, ჩნდება ორიგინალი ან სტატიკური კადრი" + კავშირის შეცდომის შეტყობინებების ჩვენება + აჩვენებს შეტყობინებას, როდესაც DeArrow სერვერი недоступებელია + API-ის ბოლო წერტილი + DeArrow-ის მინიატურების ქეშის endpoint-ის URL + ვიდეოს სტატიკური კადრები + სტატიკური კადრები იღება თითოეული ვიდეოს დასაწყისიდან / შუაშიდან / დასასრულიდან. ეს სურათები ჩაშენებულია YouTube-ში და არ გამოიყენება გარე API + ვიდეო კადრის ამოსაღებად + დასაწყისი + შუა + დასასრული + DeArrow დროებით მიუწვდომელია (სტატუსის კოდი: %s) + DeArrow დროებით მიუწვდომელია + მოთხოვეთ მოთამაშის ღილაკების ძველი სტილი + აბრუნებს ვიდეო ప్ლეერის გამუქებულ მართვის ღილაკებს ძველ, უფრო თხელ ვიზუალურ სტილზე + მოწყობილობის განზომილებების имиტაცია + ამ ფუნქციის ჩართვამ შესაძლოა განაპირობოს ვიდეოს უფრო მაღალი ხარისხის ხელმისაწვდომობა + ამის გააქტიურებამ შესაძლოა გამოიწვიოს ვიდეოს უკუკვევის შეფერხება, ელემენტის მუხტგადების დაჩქარება და გაურკვეველი გვერდითი ეფექტები + ჰაპტიკური უკუკავშირი + აირჩიეთ, რომელმა ინტერაქციებმა გამოიწვიოს მოწყობილობის ვიბრაცია + თავების ჰაპტიკის გამორთვა + ზუსტი ძებნის ჰაპტიკის გამორთვა + ძებნის გაუქმების ჰაპტიკის გამორთვა + დაჭერისა და გრძელვადი დაჭერით ჰაპტიკური გამოხმაურების გამორთვა + ზუმის ჰაპტიკის გამორთვა + თუ ახლახან შეცვალეთ თქვენი ანგარიშის შესვლის მონაცემები, წაშალეთ და ხელახლა დააინსტალირეთ MicroG + ბმულების გადამისამართების გვერ Elbowი + აღებს პირდაპირ ორიგინალ ბმულს, თვალის გარეშე გადასვლის ბმულზე (youtube.com/redirect) + ბმულების გახსნა ბრაუზერში + ხსნის ბმულებს გარე ბრაუზერში და არა აპლიკაციის შიგნით არსებულ ბრაუზერში + აღ रिप्रोडების ბუფერის ზომა ნაგულისხმები + დაბალი + საშუალო + მაქსიმალური + ვიდეოს დაგვიანება + ავკვრით ვიდეოს, რათა ის სინქრონიზებული იყოს დაგვიანებულ აუდიოსთან, მაგალითად, Bluetooth ყურსასმენების შემთხვევაში (0-5000 მს) + დაგვიანება მხოლოდ Bluetooth აუდიოსთვის + ავიდეოს დაგვიანება ხდება მხოლოდ Bluetooth აუდიოს დაკავშირებისას, რათა ტელეფონის დინამიკი სინქრონიზებული დარჩეს + ვიდეოს დაგვიანების ღილაკის ჩვენება + დააჭირეთ და გეჭიროთ დაყოვნების 0 მზ-ზე დასაყენებლად + ვიდეოს დაგვიანების მენიუს ჩვენება + დაგვიანება მილიწამებში + ავტომატური + ვიდეოს ხარისხის запоმნება + თქვენ მიერ ხელით არჩეულ ხარისხს განაცხადებს ყველა მომავალ ვიდეოზე + შეტყობინებები ხარისხის ცვლილების შესახებ + აჩვენებს შეტყობინებას, როდესაც ახალი ნაგულისხმევი ხარისხი შეინახება + ვიდეოს ნაგულისხმევი ხარისხი Wi-Fi ქსელში + ვიდეოს ხარისხი ნაგულისხმევი მობილურ ქსელში + Shorts-ის ხარისხის запоმნება + თქვენ მიერ ხელით არჩეულ ხარისხს განაცხადებს ყველა მომავალ Shorts-ზე + Shorts-ის ხარისხი ნაგულისხმევი Wi-Fi ქსელში + Shorts-ის ხარისხი ნაგულისხმევი მობილურ ქსელში + მობილური ქსელისთვის ნაგულისხმევი ხარისხი შეცვლილია შემდეგზე: %s + Wi-Fi-სთვის ნაგულისხმევი ხარისხი შეცვლილია შემდეგზე: %s + Shorts-ის მობილური ხარისხი შეცვლილია შემდეგზე: %s + Shorts-ის Wi-Fi ხარისხი შეცვლილია შემდეგზე: %s + აჩვენე დაკვრის სიჩქარის ღილაკი + დააჭირეთ და გეჭიროთ სიჩქარის საწყის მნიშვნელობამდე დასაბრუნებლად + აჩვენე ხარისხის ღილაკი + დააჭირეთ და გეჭიროთ ხარისხის საწყის მნიშვნელობამდე დასაბრუნებლად + გაფართოების ჩვენება ხარისხის ღილაკზე + აჩვენებს გაფართოებას, მაგალითად 1080-ს, ნაცვლად ისეთი ეტიკეტისა, როგორიცაა FHD + უკორექტირებელი დაკვრის სიჩქარის მენიუ + დააბრუნეთ დაკვრის სიჩქარის ძველი მენიუ + უკორექტირებელი დაკვრის სიჩქარეები + შეიყვანეთ თქვენთვის სასურველი დაკვრის სიჩქარე + უკუკავშირის სიჩქარე უნდა იყოს %s-ზე ნაკლები + ნორმაგარეშე უკორექტირებელი დაკვრის სიჩქარეები + ავტომატური + დაკვრის სიჩქარის დაჭერა და გეჭიროთ + განსაზღვრეთ სიჩქარის მამრავლი ეკრანზე დაჭერისა და გეჭიროთ; გამოსაჩერებლად მიუთითეთ 0 + დაიმახსოვრე დაკვრის სიჩქარე + თქვენს მიერ არჩეული სიჩქარე გავრცელდება ყველა მომავალ ვიდეოზე + სიჩქარის ცვლილების შეტყობინებები + აჩვენებს შეტყობინებას, როდესაც დაკვრის სიჩქარის ახალი ნაგულისხმევი მნიშვნელობა შეინახება + დაკვრის სიჩქარის ნაგულისხმევი მნიშვნელობა + ნაგულისხმევი დაკვრის სიჩქარე Shorts-ზე + გამოთვლის სიჩქარის დამახსოვრება Shorts-ზე + გამოიყენებს Shorts-ების სიჩქარის თქვენს მიერ მHარულად არჩეულ მნიშვნელობას ყველა მომავალი Shorts-ისთვის. თუ გამორთულია, სიჩქარე defaults-ზე ბრუნდება Shorts-ების ప్ლეერის დახურვის შემდეგ + აჩვენეთ დაკვრის სიჩქარე Shorts-ების მენიუში + ამატებს დაკვრის სიჩქარის პუნქტს Shorts-ების პლეერის მენიუში + უკუკავშირის სიჩქარე · %s + ნაგულისხმევი უკუკავშირის სიჩქარე შეიცვალა: %s + მუსიკისთვის გამოიყენეთ ჩვეულებრივი სიჩქარე + მუსიკალური ვიდეოების ყოველთვის დაკვრა 1.0x სიჩქარით + არხების სიჩქარის თეთრი სია + თეთრ სიაში მყოფი არხების ვიდეოები ყოველთვის 1.0x სიჩქარით ჩართვება + არხის დამატება სიჩქარის თეთრ სიაში + არხის წაშლა სიჩქარის თეთრი სიიდან + აუდიო ტონალობის მართვის ჩართვა + უკუკავშირისა და დაკვრის აუდიო ტონალობის ნაგულისხმევი მნიშვნელობა + აუდიო ტონალობის ნაგულისხმევი მნიშვნელობა შეიცვალა: %s + აუდიო დროის გაწელვა + აძლევს საშუალებას სიჩქარისა და ტონალობის დამოუკიდებლად დაყენება, რამაც შესაძლოა გამოიწვიოს აუდიო არტეფაქტები + პირდაპირი ტრანსლაციის დაკვრის პოზიციის დამახსოვრება + მიმდინარე პირდაპირი ტრანსლაციებისთვის, ახსოვებს და განაახლებს თქვენს დაკვრის პოზიციას მიმდინარე პირდაპირ პოზიციაზე გადასვლის ნაცვლად + ეს ფუნქცია არ მუშაობს 24/7 პირდაპირ ტრანსლაციებთან და სხვა ნაკადებთან, რომლებიც \'ყოველთვის ჩართულია\' + გათიშეთ HDR ვიდეო + HDR ვიდეოს იძულება + აიძულებს აპლიკაციას, აღიქვას ეს მოწყობილობა როგორც HDR-ის მხარდამჭერი + "გამოიყენეთ ეს, თუ HDR დეკოდირება მუშაობს, მაგრამ YouTube არ გვთავაზობს HDR-ს, მაგალითად, ზოგიერთ HyperOS მოწყობილობაზე, რომლებიც მალავენ HLG-ს. + +შეზღუდვები: +• ფერები შესაძლოა არასწორად გამოჩნდეს, თუ დისპლეის რეალურად არ შეუძლია HDR-ის ჩვენება +• დაკვრა შესაძლოა ჩაიშალოს, თუ დეკოდერს არ შეუძლია HDR-ის დამუშავება +• HDR კვლავ დამოკიდებულია ვიდეო ნაკადების იმტაცირებელი კლიენტის spoof-ზე" + ვიდეოკოდეკი ნაგულისხმები + AVC-ის იძულებითი გამოყენებისთვის, შეცვალეთ \"ვიდეო ნაკადების იმიტაცია\" მნიშვნელობouთ: \"Android Studio\", \"Android VR\" ან \"visionOS\" + "VP9 +• შესაძლოა გააუმჯობესოს ელემენტის მუხტის ხანგრძლივობა და აღმოფხვრას დაკვრის შეფერხებები +• მოიხმარს მეტ ინტერნეტ ტრაფიკს, ვიდრე AV1 + +AVC (H.264) +• საჭიროებს 'Android Studio', 'Android VR', ან 'visionOS' Spoof ვიდეო ნაკადის კლიენტს +• შესაძლოა კიდევ უფრო გააუმჯობესოს ელემენტის მუხტის ხანგრძლივობა +• შესაძლოა აღადგინოს დაკარგული ვიდეო რეზოლუციები ძველ მოწყობილობებზე +• მაქსიმალური რეზოლუციაა 1080p +• HDR ვიდეოებმა შესაძლოა არ გამოიყენონ AVC +• ზოგიერთ მოწყობილობას არ შეუძლია AVC-ის იძულებით გამოყენება" + გახსენით ხარისხის გაფართოებული მენიუ + გამოტოვებს ხარისხის შერჩევის მენიუს და პირდაპირ ხარისხის გაფართოებულ მენიუს აღებს + 숨기기 Premium ხარისხის ოფციები + ვიდეოს ხარისხის პრიორიტეტიზაცია + ანიჭებს პრიორიტეტს მაქსიმალურ ხელმისაწვდომ ვიდეო ხარისხს კოდეკის ეფექტურობაზე + ამ პარამეტრის გამორთვის შემთხვევაში, ძველ ვიდეოებს შესაძლოა არ ჰქონდეთ ვიდეოს ხარისხის არჩევანის შესაძლებლობა + ნებადართვას \'Android VR\' / visionOS AV1 + ნებადართავს პროგრამულ AV1 დეკოდირებას \'Android VR\' / visionOS კლიენტის გამოყენებისას + "ამ პარამეტრის გააქტიურებამ შესაძლოა გამოიწვიოს AV1-ის პროგრამული დეკოდირების გამოყენება. + +AV1-ით ვიდეოს უკუკვევა შესაძლოა იყოს შეფერხებული ან კადრები დაიკარგოს" + სპუფინგის გვერდითი ეფექტები + • დაკვრა შესაძლოა შეწყდეს 1 წუთზე + • მენიუ \'აუდიო ტრეკი\' გამორჩეულია + • არ არის AV1 ვიდეო კოდეკი + • 360° VR იმერსიული რეჟიმი არ არის ხელმისაწვდომი + • სტაბილური ხმის დონე недоступებლია + • შესაძლოა დაკვრა 1 წამით ადრე დასრულდეს + • თუ დეობფუსკაცია ჩაიშალა, დაკვრა შესაძლოა ვერ მოხერხდეს + • \"ორიგინალი აუდიოს და强制ება\" მიუწვდომელია + • საჭიროა Android VR-ში შესვლა + • ხარისხი შესაძლოა შეზღუდული იყოს 720p-მდე + პლეერის JavaScript ვარიანტი + აჩვენე სპუფინგი \'სტატისტიკაში გიგანტებისთვის\' + ვიდეოს ძველი მოქმედებების პანელის აღდგენა + "ასწორებს ვიდეოს მოქმედების ღილაკების დამალვის პრობლემას + +შეზღუდვა: ბრუნდება ძველი სტილის 'მეტი ვიდეო' ფენაზე" + პლეიერის ხატულის სტილი + ავტომატური ნაგულისხმები + თხელი + სქელი + საკუთარი + Shorts-ის ხატულის სტილი + სერიების თრექერი + თვალყურს ადევნეთ პლეილისტებს, როგორც სერიებს და შეინარჩუნეთ თქვენი პოზიცია + ჩართეთ სერიების თრექინგი + ამატებს სერიების გვერდს ისტორიაში და სერიების თვალყურის დევნების ოფციას პლეილისტებში + სერიების ჩვენება ნავიგაციის პანელში + სერიების თრექერის ღილაკის ჩვენება + ამატებს პლეერის ღილაკს მიმდევრობისთვის ან მიმდინარე ვიდეოს სერიის გასახსნელად + მიდევნებული სერიების პროგრესის ჩაწერა + ჩაწერეთ დაკვრის პროცესი ამ ტელეფონზე შენახული სერიებისა და ჩანიშნულებისთვის + გაუქმება + შენახვა + ჩანიშნულის შენახვა + სერიის სახელი + სერიის პარამეტრები + ბიბლიოთეკის პარამეტრები + ეპიზოდების განახლება + გადარქმევა + ప్ლეილისტის გახსნა YouTube-ში + ბიბლიოთეკიდან წაშლა + ప్ლეილისტის ბმული ან ID + ლოკალური პროგრესის სრულად გასუფთავება + ისტორიის გასუფთავება + წაშალოთ მთელი ლოკალური პროგრესი? + ეს ასუფთავებს პოზიციებს, ნანახ გადაფარვებსა და განახლების არჩევანს ყველა ვიდეოსთვის. შენახული სერიები და მათი კატალოგები რჩება. ახალი დაკვერა ჩაიწერება, თუ ჩაწერა ჩართულია + მონიშნეთ ნახულად + მონიშნეთ როგორც დაមិនნახული + გამოიყენეთ ავტომატური სტატუსი + გადატვირთვა 0:00-დან + კვლავ გაშვება 0:00-დან + სტატუსი შენახულია + შენახვის ხელახალი მცდელობა + წინა 100 + შემდეგი 100 + შენახული ვიდეოს გახსნა + ლოკალური პროგრესი გასუფთავდა + გახსენით ისტორია \"თქვენგან\", რათა ნახოთ თქვენი სერიები + ნახვის ისტორია + სერიები + შენახული სერიების ჩატვირთვა… + მიჰყევით ფლეილისტს მისი ეპიზოდების სათვალთვალოდ + პლეილისტის სანიშნულო + ეპიზოდების განახლება… + ყველა სერია + ნახული + ეპიზოდების ჩატვირთვა… + მეტი ოფცია + "ჩაწერეთ პროგრესი მხოლოდ თქვენი შენახული სერიებისა და ჩანიშნული ვიდეოებისთვის. + +ეს ბიბლიოთეკა ინახება ამ მოწყობილობაზე და shared-ია YouTube ანგარიშებს შორის. YouTube-ის ისტორიის დაპაუზების და გასუფთავების კონტროლი მას არ ცვლის. მის წასაშლელად გამოიყენეთ სერიები $→$ მეტი $→$ ლოკალური პროგრესის გასუფთავება. + +ავტომატური ჩაწერა წყდება ინკოგნიტო რეჟიმში, როდესაც ანგარიშის ვინაობა მიუწვდომელია, ან როდესაც გადადიხართ სხვა ანგარიშზე. ამ ანგარიშზე გამოსაყენებლად კვლავ ჩართეთ თვალთვალი" + თვალთვალის ჩართვა შეუძლებელია ინკოგნიტო რეჟიმში ან მანამ, სანამ YouTube ჩატვირთავს მიმდინარე ანგარიშის მდგომარეობას. დაბრუნდით მთავარ გვერდზე და სცადეთ ხელახლა. ავტორიზაცია არ არის საჭირო + ლოკალური თვალთვალის ჩართვა + პროგრესის ჩაწერა გამორთულია + განაახლე %1$s-ზე + ნანახი ეპიზოდები: %1$s / %2$s + ეპიზოდი %1$s · %2$s + ეპიზოდები %1$s-%2$s %3$s-დან + განაახლეთ ეპიზოდების ჩატვირთვის სცადရန်. პრივატული ფლეილისტებისთვის საჭიროა ანგარიშზე წვდომა; მათი ჩანაწერი კვლავ იხსნება YouTube-ზე + YouTube-მა ვერ გახსნა ეს ეპიზოდი + YouTube-მა ვერ გახსნა ეს ფლეილისტი + აირჩიეთ ეპიზოდი + აქტუალურია + אין დასაკმელი ეპიზოდები + შენახული ვიდეო · %1$s + თვალყურის დევნება სერიალს + სერიალის ნახვა + გაგრძელება + დაიწყეთ აქედან + სამიტო điểmი შენახულია + ნანახია აქამდე + ეპიზოდები მონიშნულია როგორც ნანახი + გაუქმება + ჩართვა + %1$s ახალი ეპიზოდი + პროგრესის ჩაწერა ამ მოწყობილობაზე + "თქვენი სერიალები და პროგრესი რჩება ამ მოწყობილობაზე და გაზიარებულია YouTube-ის ანგარიშებს შორის. YouTube-ის ისტორიის მართვის ფუნქციები მათ არ ცვლის. პროგრესის გასუფთავება მოახდინეთ სერიალების მენიუდან. + +ავტომატური ჩაწერა სურვილისამებრაა. ის ჩერდება ინკოგნიტო რეჟიმში ან ანგარიშის შეცვლისას" + ეპიზოდების ჩატვირთვა ვერ მოხერხდა. თქვენ შეგიძლიათ შეინახოთ ფლეილისტი ჩანიშნულად + ზედმეტად ბევრი შეუნახავი ცვლილებაა. ჯერ სცადეთ შენახვა + შენახვის პროცესი მიმდინარეობს. სცადეთ ხელახლა + ეპიზოდების ჩატვირთვა დასრულ되지ა. სცადეთ ხელახლა + მეხსიერება недоступებელია. თქვენი შენახული მონაცემები დაცულია + ცვლილებების შენახვა ვერ მოხერხდა. სცადეთ შენახვა YouTube-ის დახურვამდე + ეს სერიალი უკვე შენახულია, ან მისი ფლეილისტი შეიცვალა. გახსენით იგი ხელახლა + ეს სერიალი შეიცვალა ან წაიშალა. გახსენით ბიბლიოთეკა ხელახლა + ფლეილისტი შეიცვალა. თავიდან აირჩიეთ ეპიზოდი + ეს ეპიზოდი მიუწვდომელია. აირჩიეთ სხვა + შეიყვანეთ სერიალის სახელი + შეიყვანეთ YouTube-ის ფლეილისტის ბმული ან ID + გთხოვთ, დაელოდოთ განახლებამდე + ეპიზოდების ჩატვირთვა ვერ მოხერხდა. სცადეთ მოგვიანებით + ცვლილების განხორციელება ვერ მოხერხდა. სცადეთ ხელახლა + გაგრძელება ვერ დადასტურდა. შეამოწმეთ პლეიერი და სცადეთ ხელახლა + გამოწერილი სერიალები + ეს ვიდეო არ არის ამ ფლეილისტში + ფლეილისტების ძებნა... + ფლეილისტის ბმულის ჩასმა + ფლეილისტი ვერ მოიძებნა + ხელმისაწვდომი ეპიზოდები არ არის + 숨겨진 ნანახი + მონიშნე წინა ეპიზოდები როგორც ნანახი + იწყება: %1$s + ეპიზოდების თანმიმდევრობა + პლეილისტის თანმიმდევრობა + პლეილისტის თანმიმდევრობის შეცვლა + %1$s ნახვა + %1$s ნახვა + მონიშნე ეპიზოდები როგორც ნანახი + ნახულია სულ მცირე %1$s%%, მაქსიმუმ 90 წამით დასრულებამდე, ან დარჩენილია %2$s წამი + ავტომატურად მონიშნეთ ეპიზოდი ნახულად ნებისმიერი ზღურብლის მიღწევის შემდეგ. პროცენტის წესისამებრ ასევე საჭიროა, რომ დარჩენილი იყოს არაუმეტეს 90 წამის, რათა გრძელი ეპიზოდების დასასრული არ გამოტოვდეს. დარჩენილი დროის წესი ვრცელდება ერთ წუთზე მეტი ხანგრძლივობის ვიდეოებზე; გამოსაჩერებლად მიუთითეთ 0. ცვლილებები გავრცელდება პროგრესის მომავალ განახლებებზე. ნახულის/უნახავის მHანუალური არჩევანი ნარჩუნდება + ნახული პროცენტი (1-100) + დარჩენილი წამები (0-300; 0 რთავს) + შეიყვანეთ მნიშვნელობა %1$s-დან %2$s-მდე + გამოიყენეთ YouTube-ის პროგრესი + გააგრძელეთ გამოწერილი სერიალების ყურება სხვა მოწყობილობაზე ყურების შემდეგ + წაიკითხეთ ნახვის სტატუსი და გაგრძელების პოზიციები თქვენი გამკვლევ სერიალებისთვის მიმდინარე YouTube ანგარიშიდან. ამ ტელეფონზე ინახვება მხოლოდ შესაბამისი ეპიზოდები. შემოწმება სრულდება სერიალების ბიბლიოთეკის გახსნისას და ღილაკ „გაგრძელების“ დაჭერამდე. თქვენს სხვა მოწყობილობაზე YouTube-ის ისტორია უნდა იყოს ჩართული + YouTube-ის პროგრესის შემოწმება… + YouTube-ის პროგრესი მიუწვდომელია. გამოიყენება შენახული პროგრესი + YouTube-ის პროგრესის ნაწილი მიუწვდომელია. შენახული პოზიციები შენარჩუნებულია + სარეზერვო ასლის შექმნა და აღდგენა + შეინახეთ სერიალების ბიბლიოთეკა ფაილში ან აღადგინეთ სარეზერვო ასლი + ინახავს გამოწერილ სერიალებს, ეპიზოდების პოზიციებსა და ნახულ არჩევანს. ჩაწერის და YouTube-ის პროგრესის ნებართვები არ შედის + სარეზერვო ასლის შენახვა + სარეზერვო ასლის აღდგენა + დაამატეთ %1$s სერიალი. თქვენს ბიბლიოთეკაში უკვე არსებული %2$s სერიალი შენარჩუნდება. ვიდეოს არსებული პროგრესი შენარჩუნდება + სერიალების სარეზერვო ასლი შენახულია + აღდგენილია %1$s სერიალი + ეს ფაილი არ არის მხარდაჭერილი სერიალების სარეზერვო ასლი ან ძალიან დიდია. თქვენი ბიბლიოთეკა არ შეცვლილა + სარეზერვო ასლის შენახვა ვერ მოხერხდა. სცადეთ სხვა მდებარეობა + სერიალი წაშლილია + ეს ეპიზოდი ახლა ერთზე მეტჯერ ჩანს. აირჩიეთ, რომელიდან გააგრძელოთ + თქვენი შენახული ეპიზოდი აღარ არის ამ ფლეილისტში. თქვენი შენახული პოზიცია შენარჩუნებულია + გააგრძელეთ ეპიზოდით %1$s · %2$s + შეგიძლიათ ასევე აირჩიოთ ნებისმიერი ეპიზოდი ქვემოთ. ადრეული ეპიზოდები არ შეცვლილა + ეს ფლეილისტი შეიცვალა. გახსენით სერიალი, რათა აირჩიოთ, საიდან გააგრძელოთ + ამ მიმართულებით ეპიზოდები აღარ არის + გააგრძელეთ %1$s + სერიალების ძებნა + ძებნის გასუფთავება + სერიალი ვერ მოიძებნა + უფერო ეპიზოდი + ხელმიუწვდომელი ეპიზოდი + Niconico-ს კომენტარები + აჩვენეთ პირდაპირი ჩატი და ჩატის ჩანაწერი კომენტარების სახით, რომლებიც ვიდეოს commentators გასტაცდება + ვიდეოპლეერში ჩნდება ზედაპირული ღილაკი. დააჭირეთ კომენტარების გამოსაჩენად ან დასამალად, დააჭირეთ და გეჭიროთ სწრაფი პარამეტრებისთვის + კომენტარის შრიფტის ზომა + კომენტარების ტექსტის ზომა (12-40 sp) + კომენტარის გამჭვირვალობა + კომენტარის ტექსტისა და ფონების გამჭვირვალობა + მოგზაურობის ხანგრძლივობა + რამდენ წამს χρειდება კომენტარებს ეკრანის გადაკვეთისთვის. დაბალი მნიშვნელობები უფრო სწრაფად მოძრაობენ + კომენტარის მაქსიმალური სიგრძე + მალავს კომენტარებს, რომელთაც ამაზე მეტი სიმბოლო აქვთ. თითოეული ემოჯი ითვლება როგორც ერთი სიმბოლო + შეზღუდვის გარეშე diff --git a/patches/src/main/resources/addresources/values-kk-rKZ/music/strings.xml b/patches/src/main/resources/addresources/values-kk-rKZ/music/strings.xml index ccfea4ed71..aecaaca051 100644 --- a/patches/src/main/resources/addresources/values-kk-rKZ/music/strings.xml +++ b/patches/src/main/resources/addresources/values-kk-rKZ/music/strings.xml @@ -17,58 +17,673 @@ Second \"item\" text" --> + Соңғы ойнатылған + Жазылымдар + Бейнелер + Басқа нәтижелер + Жүктелген әндер + Жүктелген альбомдар + Жүктелген артистер + Кезбеу және қайта бастау функцияларын қолдану үшін хосттың Джем патчын жаңартыңыз + Jam кезегін бөлісу + Жақындағы Джемді бастаңыз немесе қосылыңыз және оның кезегін Джем қабаты арқылы өңдеңіз + Jam кезегін бөлісуді қосу + YouTube Music қайта қосылғаннан кейін Jam интеграциясын қосады + Жұмыс істеуі үшін Jam Layer-ді жүктеңіз + Jam Layer-дің соңғы нұсқасын браузерде ашыңыз + Jam басқару панелі + Жақын маңдағы Jam сессиясын бастаңыз немесе қосылыңыз және оны басқарыңыз + Jam Layer пакеті + Жақын маңдағы кезекпен бөлісу үшін қолданылатын орнатылған қосалқы қолданбаны таңдаңыз + Орнатылған қосалқы пакетті қолданыңыз. Жұптастыру үшін сіздің келісіміңіз және жергілікті мүмкіндік токені қажет + Wi-Fi қосу? + Wi-Fi-ді қосыңыз, содан кейін Jam-ді қайта бастауға немесе қосылуға тырысыңыз + Wi-Fi параметрлерін ашу + Бас тарту + Пакетті пайдалану + Android пакетінің дұрыс атауын енгізіңіз + Пакет сақталды. Қосылу үшін Jam Layer-ді баптаңыз + Jam ажыратылды + Алдымен Jam қабатын қосыңыз + Баптауларда Jam кезекін бөлісуді қосыңыз, содан кейін YouTube Music қолданбасын қайта қосыңыз + Алдымен таңдалған Jam қабаты пакетін орнатыңыз + Хост экранында көрсетілген кодты енгізіңіз. Wi-Fi Aware жергілікті желілері әртүрлі болса да, жақын маңдағы құрылғыларды қоса алады + Код арқылы қосылу + Қосылу + QR-кодты сканерлеу + Секіз таңбалы кодты енгізіңіз немесе шақыруды қойыңыз + Jam-ге қосылыңыз + Кодты көшіру + Джем коды + QR көрсету + Дайын + Джемге шақыру + Бұл кезекке қосылу және оны өңдеу үшін сканерлеңіз. Jam-ді аяқтау шақыруды жояды + Сілтемені көшіру + Джемге шақыру + Джем қайта қосылуда; сәлден кейін қайталаңыз + Джемге қосылды + Джем қолжетімсіз; трек қосылмады + Сіздің Jam-іңіз + Бірге тыңдау + Jam-ге қосылу + Бірге тыңдаңыз + Jam жаңартылуда… + Wi-Fi желісінен хост ізделуде… + Адамдар күтілуде + %s адам қосылды + %s адам қосылды + Осы кодты жақындағы құрылғыға енгізіңіз. Wi-Fi Aware қолдау көрсетілген жағдайда бөлек LAN желілерінде жұмыс істейді. Мерзімі %s минуттан кейін аяқталады + %s уақытына өту? + Өңдеулер ашық + Өңдеулер бұғатталған + Wi-Fi Aware арқылы қосылды + Wi-Fi арқылы қосылды + Bluetooth арқылы қосылды + Jam кезінде қолдау көрсетілмейді + Қосылу тоқтатылды + Хостқа қайта қосылуда… + Кезекпен бөлісіңіз. Әркімге әндерді қосуға мүмкіндік беріңіз + Адамдарды шақыру + Қонақтар кезекті өңдей алады + Джемді аяқтау + Музыканы хост қосады. YouTube Music кезекке әндерді тікелей қосыңыз, жылжытыңыз немесе жойыңыз + Екі құрылғыны да жақын ұстаңыз. Біз хостты іздеп жатқанда әрекетті тоқтата аласыз + Қосылуды 취кіру + Джемді meninggalkan + Jam бастау + Jam қабатын баптау + Басқару + Мәліметтер + Ашу + Ән параметрлері жүктелуде… + Ән параметрлері қолжетімді емес; қайта көріңіз + Ән параметрлерін ашу мүмкін болмады + Jam қабатын жүктеу үшін браузер қолжетімді емес + Хост кезекін күту + Алдымен YouTube Music-те әнді ашыңыз + Хост құрылғысында ойнату + Джемді тоқтатып, жергілікті түрде ойнату + Джемде келесіні ойнату + Джем кезекке қосу + Осы Джем трегін ойнату + Қай жерде ойнатуды таңдаңыз + Хостты осы жерге көшіру + Ән іздеуге дайын болмады + Кезек өзгертілді; жестті қайталап көріңіз + Тақырып логотипі + Әдепті + Жеке + Альбомның аудио тректерін ойнату + Музыкалық бейнеклиптің орнына әндердің тек аудио нұсқасын ойнату. \"Бейне ағындарды имитациялау\" функциясы қажет + Аудио/видео ауыстырып қосуды қосу + Аудио жолағының нұсқасы мен ресми музыкалық бейнебаянды ауыстырып көріңіз + Музыкалық бейнеклиптерді, подкасттарды немесе әндерді үшінші тараптың жүктеу қосымшалары арқылы жүктеңіз + Жүктеу мәзірі үшін сыртқы жүктегішті пайдалану + Мәзірдің Жүктеу опциясы оның орнына сыртқы жүктегішіңізді ашады + Жүктемелер + "Жүктемелер жоқ +Жүктелген тректер осы жерде көрсетіледі" + %1$s • %2$s • %3$s МБ + Жүктемелерден іздеу + Сұрыптау + Орындаушы бойынша сұрыптау + Атауы бойынша сұрыптау + Көлемі бойынша сұрыптау + Барлық жүктеулерді жою + Барлық жүктеулерді жою керек пе? + Осы құрылғыдағы барлық жүктелген тректер жойылады + Жүктеулер жойылды + Ештеңе табылмады + %s үшін әрекеттер + Ойнату + Ойнату немесе кідірту + Алдыңғы + Келесі + Кезектету + Жабу + Өшіру + Жүктелгенді өшіру + Жүктеуді жою? + %s осы құрылғыдан жойылады + Жүктеу жойылды + Жүктеуді жою мүмкін болмады + Жүктелген файлды ойнату мүмкін болмады + Аудио файлдарын жүктеу + Офлайн ойнату + Жүктеу үшін тректі анықтау мүмкін болмады + Жүктеу процесі жүріп жатыр + Жүктелді + Жүктеу басталды + Жүктеу басталуда… + Жүктелуі %s%% + Жүктеу аяқталды + Жүктеу сәтсіз аяқталды + Morphe туралы + Жарнамалар + Лента + Жалпы + Плеер + Мәтіндер + Әртүрлі + YouTube Music тілі + Жүйе тілі + Android 13 немесе одан кейінгі нұсқасы қажет + \"Music Premium алу\" таңбасын жасыру + Music Premium жарнамаларын жасыру + Бейнежарнамаларды жасыру + Аудио / Видео ауыстырып қосу түймесін жасыру + Жүктеу түймесін жасыру + Подкаст шоуында Жүктеу түймесін жасырады + «Explore» сөресін жасыру + Сүзгі жолағын жасыру + Торлы сөрелерді жасыру + Көлденең сөрелерді жасыру + Тізім сөрелерін жасыру + Мәтіндер панелі + Бөлісу батырмасын жасыру + Аудару батырмасын жасыру + «New from» сөресін жасыру + Плейлист сөрелерін жасыру + Қайталау түймесін жасыру + Араластыру түймесін жасыру + «Speed dial» сөресін жасыру + Пікірлер + Пікірлер бөлімінде қай компоненттер көрінетінін таңдаңыз + Қауымдастық ережелерін жасыру + Контекстік хабарламаны жасыру + Emoji батырмасын жасыру + Ақпарат түймесін жасыру + Уақыт белгісі түймесін жасыру + Кросфейд + Тректерді бір-біріне бірқалыпты қосыңыз + Кросфейдті қосу + Бәсеңдету қисығы + Тең қуат + Жеңіл ұстау + Жайлап бәсеңдету + Тегіс S-қисығы + Шығатын + Кіретін + Кросфейд ұзақтығы + 250 миллисекунд + 500 миллисекунд + 750 миллисекунд + 1 секунд + 2 секунд + 3 секунд + 4 секунд + 5 секунд + 6 секунд + 7 секунд + 8 секунд + 9 секунд + 10 секунд + {\"one\":\"11 секунд\",\"other\":\"11 секунд\"} + {\"one\":\"12 секунд\",\"other\":\"12 секунд\"} + Қолмен өткізіп жібергенде кроссфейд + Келесі трекке қолмен ауысқанда crossfade эффектісін қолданады + Автоматты түрде ауыстырғанда кроссфейд + Трек табиғи түрде аяқталғанда crossfade эффектісін қолданады + Сессияны басқару + Crossfade-ті тоқтату немесе қайта бастау үшін араластыру түймесін ұзақ басыңыз + Бұл сессия үшін кроссфейд тоқтатылды + Кросфейд қайта қорылды + Crossfade қосылып тұрғанда бейне режимі қолжетімді емес + SABR ойнату кезінде Crossfade өшірулі + Трансляция кезінде Crossfade өшірулі + Жұмыс істеу принципі + "Crossfade бір әннің соңын келесі әннің басымен сіз таңдаған ұзақтық пен қисықтықты қолданып överläp етеді + +Ол автоматты түрде ауыстыру кезінде және қолмен өткізіп жібергенде жұмыс істейді, әрқайсысының өз ауыстырып қосу батырмасы бар. Автоматты түрде ауыстыру кезінде араласу ән аяқталуына бірнеше секунд қалғанда басталады" + Шектеулер + "Кроссфейд қосулы болғанда музыкалық клиптер тек аудио ретінде ойнатылады. Кроссфейдті тоқтатып, видеоны көру үшін араластыру түймесін ұзақ басыңыз + +Ойнатыла бастаған музыкалық клип келесі әнге дейін видео режимінде қалады + +Ұзақтығы кроссфейд уақытынан қысқа әндер үшін кроссфейд қолданылмайды + +Блоктау экранында немесе хабарламада ауысу кезінде қысқа мерзімді ескі күй көрсетілуі мүмкін + +Музыкалық клипті көру үшін кроссфейдті тоқтату кейбір видеоларды қараңғы қалдыруы мүмкін. Әнді таңдамас бұрын кроссфейдті тоқтату бұл мәселені болдырмайды" + Қолдау көрсетілмейді + "Аудионы трансляциялау немесе бейнелеу (Chromecast, экранды немесе аудионы бейнелеу) + +SABR ойнатылымы (Видео ағындарын имитациялау өшірілген)" + Авторы + Кросфейд мүмкіндігі VazerOG жасаған + Қайталау күйін есте сақтау + Араластыру күйін есте сақтау + Ойнату жылдамдығы + Барлық тректер үшін қолданылатын жылдамдық. 100% кезінде қолданбаның жылдамдығы, мысалы, подкастты ойнату жылдамдығы қолданылады + Жылдамдықпен бірге тонды өзгерту + Ұнамау бағыттауын өшіру + Ұнамау түймесі басылғанда келесі трекке ауысуды болдырмайды + Плейлист трегін басу әрекеті + Плейлистті ойнату + Тек таңдалған тректі ойнату + Таңдалған тректі кезекке қосу + Кезекке қосылды + Cast батырмасын жасыру + Тарих батырмасын жасыру + Хабарлама батырмасын жасыру + Іздеу батырмасын жасыру + Дауыспен іздеу батырмасын жасыру + Дыбыспен іздеу батырмасын жасыру + Жаңа батырмасын жасыру + Кітапхана қойындысындағы Жаңа батырмасын жасырады + Әрекет батырмалары + Плеердің астында қай әрекет батырмалары көрінетінін таңдаңыз + Әрекеттер жолағын жасыру + «Лайк» және «Дислайк» батырмаларын жасыру + Пікірлер батырмасын жасыру + \"Тікелей чат қайта ойнату\" батырмасын жасыру + Мәтін батырмасын жасыру + Бөлісу батырмасын жасыру + Сақтау батырмасын жасыру + Жүктеу түймесін жасыру + \"Радионы қосу\" батырмасын жасыру + Ашылатын мәзір + Плеер мен кезектің ашылатын мәзірлерінде қай компоненттер көрсетілетінін таңдаңыз + 3 бағанды компонентті жасыру + \"Ұнатылған\" және \"Ұнамаған\" батырмаларын жасыру + \'Табиғат сәйкестігі\' мәзірін жасыру + \"Кейінірек тыңдау\" тізіміне қосу\" мәзірін жасыру + \"Кезекке қосу\" мәзірін жасыру + Субтитрлер мәзірін жасыру + \"Плейлистті жою\" мәзірін жасыру + \"Кезекті тазалау\" мәзірін жасыру + \"Бұл орындаушыны ұсынбау\" мәзірін жасыру + Жүктеу мәзірін жасыру + \"Плейлистті өңдеу\" мәзірін жасыру + \"Альбомға өту\" мәзірін жасыру + \"Орындаушыға өту\" мәзірін жасыру + \"Эпизодқа өту\" мәзірін жасыру + \"Подкастқа өту\" мәзірін жасыру + Көмек мәзірін жасыру + \"Эпизодты тыңдалған деп белгілеу\" мәзірін жасыру + \"Қызықты емес\" мәзірін жасыру + \"Жылдам теріп алуға бекіту\" мәзірін жасыру + \"Келесісін ойнату\" мәзірін жасыру + Сапа мәзірін жасыру + \"Кітапханадан жою\" мәзірін жасыру + \"Плейлисттен жою\" мәзірін жасыру + Шағымдау мәзірін жасыру + \'Эпизодты кейінірек үшін сақтау\' / \'Кітапханаға сақтау\' мәзірін жасыру + \"Плейлистке сақтау\" мәзірін жасыру + Бөлісу мәзірін жасыру + \"Араластырып ойнату\" мәзірін жасыру + \"Ұйықтау таймері\" мәзірін жасыру + \"Радионы қосу\" мәзірін жасыру + \"Нердтерге арналған статистика\" мәзірін жасыру + Жазылу мәзірін жасыру + \"Жылдам теріп алудан алып тастау\" мәзірін жасыру + \"Ән авторларын қарау\" мәзірін жасыру + Бастапқы бет + Әдепті + Кестелер + Кейінірек тыңдау үшін эпизодтар + Зерттеу + Тарих + Кітапхана + Ұнаған музыка + Плейлисттер + Іздеу + Жазылымдар + Портретті бағдарламалауды мәжбүрлеу + Құрылғы бұрылған кезде қолданбаны портрет бағдарламасында сақтайды + Миниплеер түсін өзгерту + Миниплеер түсін толық экранды плеер түсімен сәйкестендіреді + Навигациялық жолақтың түсін өзгерту + Навигациялық жолақтың түсін толық экранды плеерге сәйкестендіреді + Кішіткі плеерде \"Келесі\" түймесін көрсету + Кішіткі плеерде \"Алдыңғы» түймесін көрсету + Кішіткі плеерді мәжбүрлеп қосуды فعال ету + Жаңа трек басталғанда плеердің автоматты түрде толық экранды режимге ауысуын болдырмайды + Миниплеерді сырғытып жабуды қосу + Миниплеерді төмен қарай сырғыту арқылы жабуға мүмкіндік береді + Навигациялық панель + Төменгі навигациялық панельде қай түймелер болатынын таңдаңыз + Басты бет түймесін жасыру + Үлгілер түймесін жасыру + Зерттеу түймесін жасыру + Кітапхана түймесін жасыру + Жаңарту түймесін жасыру + Навигациялық панельді жасыру + Навигациялық батырмалардың таңбаларын жасыру + Жүйеге кірілмеген. Баптау үшін басыңыз + Шығу + Жүйеден шығылды + Scrobbling мүмкіндігін қосу + \"Қазір ойнатылып жатыр\" мәртебесін жіберу + Әннің минималды ұзақтығы + Бұл уақыттан қысқа әндер scrobbled болмайды + Scrobble кідірісінің пайызы + Әннің осы пайызы тыңдалғаннан кейін scrobble іске қосылады + Scrobble кешіктіру секундтары + Осы көп секунд тыңдалғаннан кейін scrobble іске қосылады + ListenBrainz аккаунты + Кіріс жасалды. Аккаунтты басқару үшін түртіңіз + Scrobbling функциясын қосу үшін ListenBrainz пайдаланушы токенін енгізіңіз + Токен + Растау + Токенді енгізіңіз + Тексерілуде… + Жарамды токен! Пайдаланушы: %s + Жарамсыз токен: %s + белгісіз + Кіріктіру сәтсіз аяқталды: %s + Токен жойылды + Токен сақталды + Ескерту: Сақталған токен жарамсыз! + Ұнаған тректерді жақсы көру + YouTube Music-те тректерді ұнатқанда, оларды Last.fm-де автоматты түрде жақсы көру (және ұнамаған немесе жақпаған жағдайда жақсы көруді алып тастау) + Last.fm аккаунты + %s ретінде кіріп тұрсыз. Аккаунтты басқару үшін түртіңіз + Кіру үшін Last.fm деректемелерін енгізіңіз + Пайдаланушы аты + Құпия сөз + Кіру + Пайдаланушы атын және құпия сөзді енгізіңіз + Кіруде… + Сәтті кірілді! + Last.fm қызметінен сессияны алу мүмкін болмады + Кіру сәтсіз аяқталды: %s + Last.fm деректері сақталды! + Метадеректерді тазалау + Метадеректерді тазалау + Scrobbling алдында тақырыптардан, орындаушылардан және альбомдардан жиі кездесетін артық мәліметтерді (мысалы, \"Official Video\", \"Remaster\", \"Live\") автоматты түрде тазалау + Custom regex тазалауы + Ән тақырыптарынан, орындаушылардан немесе альбомдардан қосымша мәтінді алып тастау үшін жеке тұрақты өрнекті енгізіңіз. Мысалы: (?i)\\s*\\((clean|explicit)\) + Тақырыптан орындаушыны және тректі талдау + Видео тақырыбынан орындаушы мен трек атауын \" - \" бөлгіші арқылы бөліп алыңыз. Бұл арна атауы орындаушы ретінде қолданылуы мүмкін қайта жүктелген контент үшін пайдалы + Жетіспейтін альбомды толтыру + Музыкада альбом көрсетілмеген жағдайда, scrobble толық болуы үшін оны Last.fm-нен іздеу + "Scrobbling сіздің YouTube Music қолдануыңызды Last.fm немесе ListenBrainz профиліңізбен автоматты түрде синхрондайды + +Фондық ойнату кезінде scrobbling синхрондалғанына көз жеткізу үшін, YouTube Music қолданбасының батареяны пайдалану параметрі \"Шектеусіз\" деп орнатылғанына көз жеткізіңіз" + Үшінші тарап мәтіндерін қолдану + Үшінші тарап сервисінен мәтіндерді көрсетеді, егер мәтіндер табылмаса, кірістірілген мәтіндерге ауысады + Мәтінді қарағанда экранды қосып ұстау + Мәтін панелі ашық болғанда дисплейдің өшуіне жол бермейді + Мәтін сервисі + Көрсету + Синхрондау + Панель + AI endpoint + Барлық провайдерлер өшірілді - кірістірілген мәтіндер және ішкі мәтіндер + Провайдерлер ретін sıфрлау? + Провайдерлерді әдепкі басымдық ретіне қайтару + Сақталған мәтіндерді тазалау + Осы құрылғыда сақталған мәтіндерді жойыңыз + %1$s МБ тазартылды + Токен алу + Apple Music Media-user-token + "music.apple.com сайтынан media-user-token кукиін қойыңыз + +Қадамдар: +1. music.apple.com сайтына мүше ретінде кіріңіз +2. DevTools → Application → Cookies → music.apple.com бөлімін ашыңыз +3. \"media-user-token\" мәнін көшіріңіз" + Apple Music: media-user-token сақталды + Apple Music: media-user-token жойылды + Apple Music: жарамсыз media-user-token - тексеріп, қайтадан көріңіз + Spotify SpDc + "Spotify-ден sp_dc кукиін қойыңыз. + +Қадамдар: +1. open.spotify.com сайтына кіріңіз +2. DevTools → Application → Cookies → open.spotify.com бөлімін ашыңыз +3. \"sp_dc\" мәнін көшіріңіз" + Spotify: sp_dc сақталды + Spotify: sp_dc тазартылды + Spotify: жарамсыз sp_dc - тексеріп, қайтадан көріңіз + Apple Music токені қосымша болып табылады. Ол болмаса, резервті нұсқа ретінде Lyrically қолданылады + Deezer ARL + "Deezer-ден arl cookie-ін қойыңыз. + +Қадамдар: +1. deezer.com сайтына кіріңіз +2. DevTools → Application → Cookies → deezer.com бөлімін ашыңыз +3. \"arl\" мәнін көшіріңіз" + Deezer: arl сақталды + Deezer: arl тазартылды + Deezer: жарамсыз arl - тексеріп, қайтадан көріңіз + Musixmatch токені + Жеке аккаунтыңызды пайдалану үшін ғана токен қосыңыз. Ол Musixmatch Android қолданбасынан алынуы тиіс: musixmatch.com сайтынан көшірілген токендер ескірген жұмыс үстелі клиентіне тиесілі және API оларды қабылдамайды + Musixmatch токенсіз жұмыс істейді, сондықтан бұл өріс бос қалуы мүмкін + пайдаланушы токені + Musixmatch: пайдаланушы токені сақталды + Musixmatch: пайдаланушы токені тазартылды + Musixmatch: жарамсыз пайдаланушы токені - тексеріп, қайтадан көріңіз + YouTube cookie-лері + "YouTube-дан субтитрлер мен уақыт мәтіндерін сұрату үшін YouTube cookie-лерін қойыңыз. + +Қадамдары: +1. Браузерде youtube.com сайтына кіріп, авторизациядан өтің +2. DevTools (F12) → Application → Cookies → youtube.com бөлімін ашыңыз +3. Толық cookie жолын көшіріңіз (нүктелі үтірмен бөлінген key=value жұптары) + +Қажетті cookie-лер: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPISID, LOGIN_INFO" + YouTube: cookie-лер сақталды + YouTube: cookie-лер тазартылды + YouTube: cookie-файлдар дұрыс емес - тексеріп, қайта försöніңіз + Жолды басып тұрыңыз + Әннің сол сәтіне көшу үшін жолды басыңыз, саусағыңыздың астындағы сөзді немесе басылған жер сөз болмаса бүкіл жолды көшіру үшін оны ұзақ басыңыз. Өшіру екі жестті де жояды + Бөлім тегтерін көрсету + Лириканың әрбір жаңа бөлімін «Куплет» немесе «Қайырмасы» сияқты кішігірім тегпен белгілеу + Көшіру түймесін көрсету + Түпнұсқа мәтінді қарапайым мәтін ретінде көшіру үшін басыңыз; мәтіннің бастапқы дереккөзі файлын «Жүктеулер» бөліміне сақтау үшін ұзақ басыңыз + Аудару түймесін көрсету + Аударманы қосу/өшіру үшін басыңыз; тек аударма үшін ұзақ басыңыз. Провайдер аудармалары, Google Translate немесе OpenAI-мен үйлесімді API қолданылады + Романизация түймесін көрсету + Романизацияны қосу/өшіру үшін басыңыз; тек романизация үшін ұзақ басыңыз. Провайдер романизациясы, Google Translate немесе OpenAI-мен үйлесімді API қолданылады + Жергілікті романизация және туралау + Провайдер, сіздің моделіңіз және Google Translate істемеген жағдайда құрылғыда романизация жасаңыз. Оқу уақыты бойынша синхрондалған ән мәтінінің әр сөзінен кейін жүреді; сөздерді синхрондау қосулы болуы керек. Қытай, кантон, жапон, корей, грек, орыс, грузин, армян тілдері; шамамен 120 МБ + Оқылуы %1$s: %2$d%% + Жүктеу сәтсіз аяқталды - қайталау үшін бұл функцияны өшіріп, қайта қосыңыз + Жаңарту түймесін көрсету + Келесі мәтін нұсқасына ауысу үшін түртіңіз; ән атауы мен орындаушы бойынша мәтінді іздеу үшін ұзақ басыңыз + Мәтіндерді іздеу + Ән атауы + Орындаушы + Ән атауы мен орындаушыны енгізіңіз + Қосылған провайдерлердің ешқайсысы басқа әндерді іздей алмайды + Сақталды + Синхрондалған сөздерді немесе буындарды ерекшелеу + Мүмкіндігі болған жағдайда сөздік мәтінге басымдық беру. Мәтінде сөздердің уақыты көрсетілгенде, әр сөз немесе буын музыкамен үйлесімді түрде ерекшеленеді + Орындалған мәтінді жасыру + Қазіргі орындалып жатқан жолдың жоғарғы мәтін жолдарын жасыру + Орындалмаған мәтінді жасыру + Қазіргі орындалып жатқан жолдың төменгі мәтін жолдарын жасыру + Бұғатталған экранда MediaSession мәтінін көрсету + Бұғаттау экранында, Android Auto және Bluetooth-та ағымдағы жолды көрсетеді. Android 11 немесе одан жоғары нұсқасы қажет + Миниплеер мәтіндерін көрсету + Миниплеерде ағымдағы жолды көрсетеді, ал трек субтитрге ауысады + Ән атауының/орындаушысының форматы + Miniplayer және MediaSession-да мәтінмен бірге көрсетілетін тректің форматы. {t} — атауы, {a} — орындаушысы және {v} — нұсқасы, мысалы, Acoustic, егер мәтін көзі оны ұсынса. Екі нүкте арасындағы мәтін тек оның ішіндегі әрбір айнымалының мәні болғанда ғана көрсетіледі; жай нүкте үшін \\: жазыңыз. Әр жол кезекпен көрсетіледі және мәтін өзгерген сайын келесісіне ауысады + Мәтін жолының форматы + Миниплеер мен MediaSession-дегі мәтін жолының форматы. {lrc} — бұл жол, ал {n} — оның қатарынан неше рет айтылатыны. Қос нүкте арасындағы мәтін жол қайталанғанда ғана көрсетіледі. Қос нүкте үшін \\: белгісін қолданыңыз + Формат {lrc} белгісін қамтуы тиіс + Орындаушы-жол шаблоны және анимация уақыты + Минималды экран уақыты (мс) + Максималды экран уақыты (мс) + Тек көп жолды анимациялар үшін. Әр кадрды кемінде минималды уақыт бойы сақтаңыз. Максималды уақытта, мәтін өзгермесе де, орындаушы жолын алға жылжытыңыз. + Минималды мән 0 немесе одан көп болуы тиіс. Максималды мән 0-ден үлкен және кемінде минималды мәнге тең болуы тиіс. + Жол ұзындығының шектеулері + Лирика мен орындаушы жолының шектеулері және толық енді символдардың салмағы + "Лирика жолының шектеуі +Ұзын лирикаларды тең бөліктерге бөліңіз. 0 бөлуді өшіреді." + "Орындаушы жолының шектеуі +Толтырылған трек форматын циклдік кадрларға бөліңіз. 0 бөлуді өшіреді." + "Толық енді символдардың салмағы +Екі шектеуге де қолданылады. 1.75 мәнінде төрт толық енді символ 7 деп есептеледі." + 0-ден 10000-ға дейінгі бүтін сан шектеулерін және 0-ден жоғары символ салмағын енгізіңіз + Инструменталды үзіліс + Инструменталды үзіліс кезінде Миниплеерде және MediaSession-да көрсетіледі. Әр жол — бір анимация кадры. {t} — тақырыбы, {a} — орындаушысы және {lrc} — мәтіннің соңғы жолы + Миниплеер мен MediaSession уақыт ығысуы + Келесі жол Миниплеерде және MediaSession-да көрсетілетін уақытты ығыстырады. Егер жолдар кешігіп көрсетілсе, мысалы, бұғатталған экранда, теріс мәнді қолданыңыз + Сүзгілер + Метадеректер regex-і + Мәтіндерді іздегенде және екі атаудың сәйкестігін бағалағанда атауларға, орындаушыларға және альбомдарға қолданылатын тұрақты өрнек. Екеуін де өшіру үшін бос қалдырыңыз. Әдепкі параметрлер \"Official Video\", \"Remaster\", \"【插曲】\" және \"- Topic\" сияқты жиі кездесетін артық мәтіндерді алып тастайды + Мәтін сүзгісінің regex-і + Тек түпнұсқа мәтінге қолданылатын тұрақты өрнек (аудармаларға немесе романизацияға қолданылмайды). Өшіру үшін бос қалдырыңыз + Ақпараттық жол маркерлері + Егер мәтіндер төменгі жаққа қате сүзілсе, мұны реттеңіз немесе тазалаңыз. Үтірмен бөлінген маркерлер: маркерден кейін қос нүкте келетін жолдар төменгі жақта кредиттік ақпарат ретінде көрсетіледі. Өшіру үшін бос қалдырыңыз + Бірінші әріпті бас әріппен жазу + Мәтін жолдарының әрқайсысының бірінші әріптерін бас әріппен жазады + Соңғы тыныс белгілерін жою + Лирика жолдарының соңындағы нүктелерді, үтірлерді, қос нүктелерді және нүктелі үтірлерді жояды. Сұрақ белгілері, леп белгілері, көп нүктелер, тырнақшалар мен жақшалар сақталады + Мәтін өлшемі + Ән мәтінінің өлшемі + Жалпы уақытты ығыстыру + Синхрондалған мәтіндер ойнатуды бақылайды. Егер олар ауытқыса, уақытты ығысуды реттеңіз. Синхрондалған мәтіндерді алға немесе артқа жылжытыңыз. Егер мәтіндер әннен озып кетсе, оң мәнді қолданыңыз + Мәтін ығыстыру үшін SponsorBlock сегменттерін пайдалану + SponsorBlock өткізіп жіберетін сегменттерсіз сәйкестендіру және мәтінді шығарылған аудиоға ығыстыру. Өшірілген күйі SponsorBlock-ты мүлдем ескермейді + Бұл тректе жіберу үшін бейне ID жоқ + Дереккөз: %s + Осы LRC-ні сақтау үшін көшіру батырмасын ұзақ басыңыз; егер сіз уақытын нақтылап, оны қауымдастық провайдерлерімен бөліссеңіз, біз риза болар едік + Google аударды + Google романизациялады + %s аударды + %s романизациялады + Романизациясы: %s + Аударылды + Аударма тілі + Романизация тілі + Аударылуда… + Мәтінді аудару мүмкін болмады + Романизацияланды + Автоматты + Романизациялау... + Мәтінді романизациялау мүмкін болмады + Тек аударылған + Тек романизация + Қос режим + Жалпақшаға көшірілді + Жаңартылуда + Жаңартылды + QuickLRC API кілті + QuickLRC ағымдағы трек үшін мәтінді транскрипциялайды немесе уақыт бойынша сәйкестендіреді. Бұл жоғарыдағы OpenAI-мен үйлесімді endpoint-ке қатысы жоқ бөлек сервис + QuickLRC кілті сақталды. Мәтінді іздеу диалогындағы сәйкестендіру түймесін немесе тректе мәтін болмаған жағдайда панельдік нұсқауды қолданыңыз + QuickLRC API кілті сақталды + QuickLRC бұл кілтті қабылдамады + QuickLRC API кілті жойылды + QuickLRC кредиттер есебіне мәтіні жоқ әндерге уақытты сәйкестендіреді немесе оларды тректен транскрипциялайды. Кілт орнатылғанда, мәтіні жоқ тректер үшін транскрипция мүмкіндігі пайда болады және іздеу диалогында сәйкестендіру түймесі шығады + QuickLRC туралауын сұрату + Туралануда + Туралауды қолдану + QuickLRC транскрипциясын сұрату + Транскрипция жасалуда + Транскрипция тоқтатылды + Тек YouTube-тан ойнатылған тректерді транскрипциялауға болады + Сәйкестендіретін мәтін жоқ - оның орнына транскрипциялау жүзеге асырылуда + QuickLRC бұл тректі өңдей алмады + Бұл трек үшін QuickLRC кредиттері таусылды + "Мәтіндер атауы мен орындаушысы бойынша сәйкестендіріледі, ерекше метадеректер ештеңе таппауы мүмкін немесе қате нұсқаны көрсетуі мүмкін. Нұсқаларды ауыстыру үшін жаңартуды қолданыңыз. * белгісімен белгіленген провайдерлерге токен қажет. Барлық қосылған провайдерлер әрдайым сіздің ретіңізбен сұратылмайды, бірақ олардың жылдамдығына байланысты жауап береді.\n\nMәтіндер Morphe-мен байланысы жоқ, демеуленбеген немесе мақұлданбаған үшінші тарап қызметтерінен алынады. Әрқайсысы сол күйінде қолданылады: мәтіндер, олардың қолжетімділігі және лицензиялауы сол қызметтерде және олардың лицензиарларында қалады, ал оларды қолдану олардың қызмет көрсету шарттарына байланысты болуы мүмкін. Қосылған қызметке тек сіз сұраған іздеу мәліметтері ғана жетеді - атауы, орындаушысы және, егер сол қызметке қажет болса, бейне ID-і. Опционалды AI нүктесі және QuickLRC транскрипциясы тек сіз оларды баптағаннан кейін жұмыс істейді және мәтін мәтінін немесе тректің бейне URL-мекенжайын алады." + Тек қолданатын провайдерлеріңізді қосыңыз немесе әдепкі мәнді қалдырыңыз - тұрақты сұраныстар кішігірім қоғамдық мәтін сайттарын шамадан тыс жүктеуі мүмкін + Кредит жолын жасыру + Мәтін төменінде көрсетілген кредит жолын жасыру + Аударма мен романизацияны алмастыру + ҚОСИЛДЫ — романизация мәтіннен төмен, аударма жоғары орналасады; ӨШІРІЛДІ — романизация жоғары, аударма төмен қалады + Аударма + Романизация + Аударма провайдері + Google (Әдепкі, тегін) + OpenAI Compatible (Жеке) + Романизация провайдері + Google (Әдепті) + Жергілікті / Офлайн сөздік + OpenAI үйлесімді (Custom) + AI бөлімі + Тек провайдер «Қайырмасы» немесе «Куплет» сияқты бөлім белгілерін жібермеген жағдайда қолданылады, бұл белгілерді сіздің OpenAI-мен үйлесімді моделіңіз анықтайды + ЖИ & Бұлтты нүктелер + ЖИ провайдерлері үшін API кілттерін, модельдерді, нүктелерді және промпт шаблондарын баптаңыз. + API нүктесінің URL-мекенжайы (мысалы, https://api.openai.com/v1/chat/completions) + Промпт үлгісі + Орынтегімішілер: {language}, {task}, {title}, {artist}, {count}, {lines} + Промпт сақталды + Нұсқау сақталды, бірақ AI endpoint әлі қолжетімді емес. Алдымен оны AI баптаулары бөлімінде баптаңыз + Промпт sıғыртылды + Жарамсыз JSON. Сақтамас бұрын оны түзетіңіз немесе өрісті тазалаңыз + Аударма & романизация баптаулары сақталды + Баптаулар әдепті мәнге қайтарылды + Базалық URL http:// немесе https:// басталуы керек + Байланысты тексеру + Шегін точкаларын баптау + Барлық шегін точкаларын көрсету + Байланыс тексерілуде… + Аударма провайдері + Романизация провайдері + Модельдерді алу + Модельдер алынуда… + Модельді таңдаңыз + Модельдерді алу сәтсіз аяқталды. API кілтін немесе байланысты тексеріңіз. + Gemini API параметрлері + OpenAI-мен үйлесімді (Custom Endpoint) + DeepL API параметрлері + API кілті + Модель + Базалық URL + API кілті (міндетті емес) + Аударма сұранысы + Қосымша сұраныс денесі (міндетті емес JSON) + Аутентификация кілті (Free немесе Pro) + API кілттерін, модельдерді, соңғы нүктелерді және сұраныстарды баптаңыз + 8.23.51 - Қалың белгілерді өшіру + 9.13.51 - Плеердің \"Келесі\", \"Мәтіндер» және «Ұқсас» қойындыларын қалпына келтіру diff --git a/patches/src/main/resources/addresources/values-kk-rKZ/reddit/strings.xml b/patches/src/main/resources/addresources/values-kk-rKZ/reddit/strings.xml index cf7776fdaf..7a7fcbd701 100644 --- a/patches/src/main/resources/addresources/values-kk-rKZ/reddit/strings.xml +++ b/patches/src/main/resources/addresources/values-kk-rKZ/reddit/strings.xml @@ -15,4 +15,17 @@ Second \"item\" text" --> - + + Reddit құпиялылық саясаты + Google-мен кіру қолжетімді емес. Кіру үшін электронды поштаны қолданыңыз + Жеке шрифт + Төмендегі жол бойынша TTF немесе OTF қаріп файлын жүктеңіз. Бұл параметрді қосу «Force system font» функциясын өшіреді + Құнды шрифт файлы + TTF немесе OTF қаріп файлын таңдау үшін түртіңіз + Таңдалған қаріп - %s + Алдымен жеке қаріп файлын таңдаңыз + Тыныш, .ttf немесе .otf қаріп файлын таңдаңыз + Жеке қаріпті жүктеу сәтсіз аяқталды. Басқа файлды таңдаңыз + Жүйелік қаріпті мәжбүрлеу + Reddit Sans немесе Roboto орнына құрылғы жүйесінің қаріпін қолданыңыз. Бұл параметрді қосу «Custom font» функциясын өшіреді + diff --git a/patches/src/main/resources/addresources/values-kk-rKZ/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-kk-rKZ/shared-youtube/strings.xml index fd007eeb78..3e6757f243 100644 --- a/patches/src/main/resources/addresources/values-kk-rKZ/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-kk-rKZ/shared-youtube/strings.xml @@ -18,46 +18,294 @@ Second \"item\" text" --> + Бастапқы анимация + Өшірілген + Түс + Монохромды + + Баптауды аяқтау үшін қайта қосыңыз + Қосымша атауы + Қараңғы тақырыптың фоны + Таза қара + Material You (Таза қара) + Классикалық YouTube + Қою қызғылт + Қою көк + Қою жасыл + Қою сары + Қою қызғылт сары + Қою қызыл + Жеке қараңғы фон + Қараңғы тақырыптың фон түсін орнатыңыз + Қосымша әдепті + Material You (Бейтарап) + Material You (Негізгі) + Material You (Қосымша) + Material You (Үшінші) + Жеке + Жүктеулер + Жүктеу режимі + Әдепкі + Қосымша ішінде жүктеу + Сыртқы жүктегіш + Жүктегіш пакетінің атауы + Орнатылған сыртқы жүктегіш қолданбасының пакет атауы + Пакет атауын енгізіңіз + Басқа + Қолданба орнатылмаған + %s орнатылмаған. Оны орнатыңыз + "Пакет атауымен орнатылған қолданбаны табу мүмкін болмады: %s + +Пакет атауының дұрыстығын және қолданбаның орнатылғанын тексеріңіз" + Пакет атауы бос болмауы керек + Custom + Қосымша белгішесі + Түпнұсқа + Morphe қара + Morphe қою + Morphe ашық + Morphe play light + Morphe play black + Жетек + "Бұл қолданба іске қосушысында көрсетілетін белгішені өзгертеді. + +Бұл параметр келесі жерлердегі белгішені өзгертпейді: +• Құрылғы жүйесінің параметрлеріндегі қолданбалар тізімі +• Кейбір push-хабарламалар +• \"Мынамен ашу\" қолданба таңдағышы +• Android Auto белгішесі + +Белгішені барлық жерде өзгерту үшін, патчты қайта орнатып, \"App icon\" патч параметрін өзгертіңіз" + Хабарландыру белгішесі + Қолданба белгішесіне сәйкестендіру + Morphe play + Morphe хабарламасы + Сіздің қарау тарихыңыз сақталмай тұр.<br><br>Бұл, сірә, DNS жарнама бұғаттағышының немесе желілік проксидің әсерінен болуы мүмкін.<br><br>Мұны түзету үшін <b>s.youtube.com</b> адресын ақ тізімге қосыңыз немесе барлық DNS бұғаттағыштар мен проксилерді өшіріңіз + Қайта көрсетпеу + Бейнеағындарды имитациялау + Ойнату кезіндегі мәселелердің алдын алу үшін клиенттік спуфингті баптаңыз + "Сәйкес келетін бейнеағынды сұрау арқылы көптеген пайдаланушылар үшін ойнату мәселелерін шешеді + +Егер сіз YouTube Premium пайдаланушысы болсаңыз, бұл параметр қажет емес болуы мүмкін" + Клиенттік стримдерді алу мүмкін болмады + Әдепкі клиент жұмыс істемейді + "%1$s қатарынан бірнеше рет жұмыс істемеді, бірақ %2$s жұмыс істеді. + +%2$s клиентін әдепкі ету керек пе?" + Ауыстыру + Бұл параметрді өшіру ойнату кезіндегі мәселелерге әкелуі мүмкін + Мынаны қосып көріңіз: Бейне стримдерді имитациялау > Android VR кіру + Әдепкі клиент + Android VR кіру + Android VR-де кіріс жасалмаған. Кіру үшін осында түртіңіз + Қазіргі уақытта Android VR-де кіріс жасалған + Бұл функцияны пайдалану үшін \"Әдепкі клиент\" параметрін Android VR-ге өзгертіңіз + Android VR-ге кіру + "Android VR жүйесіне кіру кейбір пайдаланушылар үшін ойнату мәселелерін шеше алады + +Кіру үшін төмендегі «Жалғастыру» түймесін басыңыз" + Шығу + Нұсқаулықтар + "Белсендіру коды: %s + +1. «Веб-сайтты ашу» батырмасын басып, «Құрылғыны қосу» веб-сайтына өтіңіз +2. Жоғарыда көрсетілген белсендіру кодын алмасу буферінен қойыңыз (немесе теріңіз) +3. Сұралған жағдайда Google жүйесіне кіріңіз. Егер бірнеше аккаунтыңыз болса, осы YouTube аккаунтын таңдаңыз +4. «YouTube VR-ға рұқсат беру» батырмасын басыңыз +5. «Құрылғы қосылды» хабарламасы шыққаннан кейін осы қолданбаға қайтыңыз" + Белсендіру кодын алу сәтсіз аяқталды + Аутентификация токенін алу сәтсіз аяқталды + Жүйеге сәтті кірілді + "Аутентификация сақталды + +• Morphe баптаулары Android VR аутентификациясын импорттамайды/экспорттамайды +• Егер бұл қолданба 6 ай бойы пайдаланылмаса, Google бұл аутентификацияны жояды +• Егер Google құпия сөзіңізді өзгертсеңіз немесе бұл құрылғыны Google баптауларынан тіркеуден шығарсаңыз, қайта кіруіңіз қажет болады" + OAuth2 уақытша қолжетімді емес (API уақыты бітті) + Android VR кіру мерзімі аяқталды, VR-ге қайта кіріп көріңіз + OAuth2 уақытша қолжетімсіз (мәні %s) + OAuth2 уақытша қолжетімді емес + VR-ге кіру мүмкін емес, веб-сайт аутентификациясы мақұлданбады + VR-ге кіру мүмкін емес: %s + Android VR жүйесінен шығылды + VR-ге кіру токені жарамсыз, VR-ге қайта кіріп көріңіз + Хабарламадағы медиа басқару элементтері + Блоктау экраны мен хабарламалар панелінде көрсетілетін ойнату басқару элементтерін баптаңыз + \"Алдыңғы\" & \"Келесі\" батырмаларын жасыру + Медиа хабарламада, блоктау экранында және құлаққап басқару элементтерінде \"Алдыңғы\" және \"Келесі\" батырмаларын жасырады + Іздеу жолағын өшіру + Блоктау экраны мен хабарламалар панелінде іздеу жолағын жылжытуды болдырмайды + \"Алдыңғы\" & \"Келесі\" батырмаларымен іздеу + Медиа хабарламасы, бұғатталған экран және құлаққап басқару элементтерін бейнелерді ауыстырудың орнына іздеуге өзгертеді + Кері орату ұзындығы + \"Алдыңғы\" батырмасымен кері орату секундтары + Алға жылжыту ұзындығы + \"Келесі\" батырмасымен алға жылжыту секундтары + Іздеу ұзындығы 1 және %s секунд арасында болуы керек. Әдепкі мән қолданылады + QUIC хаттамасын өшіру + DRC аудиосын өшіру + Түпнұсқа аудио тілін мәжбүрлеу + Дубляжталған нұсқасының орнына әрқашан бейнебаянның түпнұсқа аудио жолағын ойнатады + Тыныштықты өткізіп жіберу + Бейне аудиосындағы үнсіз үзілістерді автоматты түрде өткізіп жібереді + Үзілістің минималды ұзақтығы + Тек кемінде осы уақытша үзілістер қысқартылады. Келесі бейнебаян басталғанда күшіне енгізіледі + Желілік прокси + Қолдау көрсетілетін желілік сұраныстарды прокси арқылы бағыттау + Проксині қосу + Қолдау көрсетілетін желілік сұраныстарды бапталып қойылған HTTP немесе HTTPS прокси арқылы бағыттайды + Прокси хосты + Прокси серверінің хост атауы немесе IP-мекенжайы + Прокси порты + Прокси серверінің порты + HTTPS проксисін қолдану + Прокси серверіне қосылу үшін HTTP орнына HTTPS қолданыңыз + Прокси аутентификациясы + Прокси сұраныстарымен бірге Basic аутентификациясын жібереді + Прокси пайдаланушысы + Базалық прокси аутентификациясы үшін пайдаланушы аты + Прокси құпия сөзі + Базалық прокси аутентификациясы үшін құпия сөз + Тікелей резервтік нұсқаға рұқсат беру + Прокси істемейтін жағдайда тікелей қосылыңыз + Толық экранды жарнамаларды жасыру + Бұл мүмкіндікті пайдалану үшін \'Spoof video streams\' параметрін \'Android Studio\' және \'Android VR Downgraded\' клиенттерінен басқа кез келген клиентке өзгертіңіз + Бөлісу сілтемелерін тазарту + Бөлісілген сілтемелерден бақылау сұраныс параметрлерін алып тастайды + Бөлісу сілтемелерін youtube.com-ға өзгерту + Бөлісілген сілтемелерде music.youtube.com мекенжайын youtube.com мекенжайына ауыстырады + Бөлісу сілтемелерін youtu.be-ге өзгерту + Бейнероликтер үшін қысқартылған youtu.be URL форматын қолдануды мәжбүрлейді + Debug log protocol буфері + Әзірлеушілерге қолданба компоненттерін анықтау үшін қосымша логтау ақпаратын қосады + "Бұл параметрді қосу кейбір UI компоненттерінің экрандағы мәтінін қоса алғанда, қосымша лейаут деректерін тіркейдеді. + +Бұл custom фильтрлерді жасау кезінде компоненттерді анықтауға көмектеседі. + +Дегенмен, бұл параметрді қосу сіздің IP-мекенжайыңыз сияқты кейбір пайдаланушы деректерін де тіркейдеді" + Custom сүзгі + Кез келген компонентті жасыру үшін custom filter-лерді баптаңыз + Custom filter-ді қосу + Сүзгіден өткізілетін жаңа жолдармен бөлінген компонент жолы құрылымы жолдарының тізімі + Жарамсыз жеке сүзгі: %s + Баптаулар мәзірінің сүзгісі + Қолданбаның стандартты баптаулар экранындағы элементтерді көрінетін атаулары бойынша жасыру + Тізімді толтыру үшін қолданбаның стандартты баптаулар экранын бір рет ашыңыз + Көрерменнің қарайылы диалогын алып тастау + Бұл жас restrictions шектеуін айналып өтпейді. Ол жай ғана оны автоматты түрде қабылдайды + PoToken провайдері + Ойнату мәселелерін болдырмау үшін PoToken провайдерін баптаңыз + Сыртқы PoToken провайдері + "Сыртқы PoToken минтерігн қолдану арқылы ойнату мәселелерін түзету (Эксперименталдық) + +Егер сіз YouTube Premium пайдаланушысы болсаңыз, бұл баптау қажет емес болуы мүмкін" + "Сыртқы PoToken минтерігүн қолдана отырып, ойнату мәселелерін шешеді (Эксперименталдық) + +Бұл функцияны пайдалану үшін \"Видео ағындарын жалғандау\" функциясын өшіріңіз" + Егер жұмыс істемесе, MicroG қолданбасын мәжбүрлі түрде жауып көріңіз + PotHelper баптауларын ашу + "Бұл функция қазіргі уақытта әзірленуде және жұмыс істемеуі мүмкін + +PotHelper жүктеу үшін осында түртіңіз" + PotHelper енді жұмыс істемейді + "PotHelper-дің бұл нұсқасына енгізілген токен енді жұмыс істемейді + +Ойнатуды түзету үшін «Бейне ағындарын имитациялау» функциясын қосыңыз" + Қосу + Қосымша нұсқасын жалғандау + Ескі UI мүмкіндіктерін қалпына келтіру үшін серверді сіздің қосымшаның ескі нұсқасын қолданып жатқаныңызға сендіреді + "Қосымша нұсқасы ескі нұсқаға имитацияланады. + +Бұл қосымшаның сыртқы түрін және мүмкіндіктерін өзгертеді, бірақ белгісіз жанама әсерлер болуы мүмкін. + +Кейінірек өшірілген жағдайда, интерфейс қателерін болдырмау үшін қосымша деректерін тазалау ұсынылады" + Қосымша нұсқасының нысанасын имитациялау + Қосымшаның жаңарту жиілігі + Әдепті + %s FPS + Жаңарту жиілігін қайта анықтау + Әрқашан + Портретті ойнату + Ойнату толық экранда + Ойнату портрет және толық экранда + Қаріп + Әдепті + Құрылғы жүйесінің қаріпі + Арнайы қаріп файлы + Қосымша шрифт файлы + TTF немесе OTF шрифт файлын таңдаңыз + Таңдалған файл TTF немесе OTF шрифті емес + Custom шрифт жүктелмеді және өшірілді + Ұнамағандар уақытша қолжетімді емес (API уақыты бітті) + Ұнамады белгілеу қолжетімді емес (мәрек %s) + Ұнамағандар қолжетімді емес (клиенттік API шектеуі) + Ұнамағандар қолжетімді емес (%s) + Иесімен жасырылған + Бұл бейнеқатарды ұнатпағандар: дауыстардың %s бөлігі + Return YouTube Dislike функциясын қосу + Күнәліліктерді пайыз ретінде көрсету + Күнәліліктерді сан ретінде емес, лайктардың пайызы ретінде көрсетеді + Болжамды лайктарды көрсету + Автор лайктарды өшірген видеоларда лайктардың болжамды санын көрсетеді + Байланыс қатесі туралы ескертулерді көрсету + Return YouTube Dislike сервері қолжетімсіз болғанда хабарламаны көрсетеді + "Return YouTube Dislike YouTube 2021 жылы жасырған ұнамау сандарын қайтарады. Сандар жойылғанға дейін мұрағатталған деректерден алынады және жаңа бейнебаяндар үшін пайдаланушылардың дауыстарына сүйене отырып есептеледі, сондықтан олар шамамен көрсетіледі және бірнеше күн сайын жаңартылады + +Сіз көріп отырған бейнебаянның ID-і Return YouTube Dislike API-іне жіберіледі, ал сіздің ұнатуларыңыз бен ұнамауларыңыз есептеулерді жақсарту үшін анонимді түрде жіберіледі" + Осы құрылғының Return YouTube Dislike API статистикасы + API жауап беру уақыты, орташа + API жауап беру уақыты, минималды + API жауап беру уақыты, максималды + API жауап беру уақыты, соңғы медиа + Ұнамайтындар уақытша қолжетімді емес - Клиент API жылдамдық шектеуі күшінде + API дауыстарды алу, шақырулар саны + Желілік шақырулар жасалмады + Дауыстарды API арқылы алу, таймауттар саны + Желілік шақыруларда таймауттар болған жоқ + API клиентінің жылдамдық шектеулері + Клиенттің жылдамдық шектеулері кездестірілген жоқ + Клиенттік шектеу лимиті %s рет кездесті + %s миллисекунд diff --git a/patches/src/main/resources/addresources/values-kk-rKZ/shared/strings.xml b/patches/src/main/resources/addresources/values-kk-rKZ/shared/strings.xml index 31cdbe4dcb..4318a5e7b9 100644 --- a/patches/src/main/resources/addresources/values-kk-rKZ/shared/strings.xml +++ b/patches/src/main/resources/addresources/values-kk-rKZ/shared/strings.xml @@ -17,21 +17,218 @@ Second \"item\" text" --> + Баптаулар + Жалғастырғыңыз келетініңізге сенімдісіз бе? + Сақтау + Қайта орнату + Түсті қайта орнату + Жарамсыз түс + Қайта қосу қажет + Осы өзгерістер күшіне енуі үшін қолданбаны қайта қосыңыз + Қайта қосу + Импорт + Көшіру + Morphe баптаулары әдепкі мәнге қайта орнатылды + %s баптаулары импортталды + Импорт сәтсіз аяқталды: %s + Жоғары жылжыту + Баптауларды іздеу + Жоғарырақ + \'%s\' бойынша нәтижелер табылмады + Басқа кілт сөзді қолданып көріңіз + Соңғы іздеулер + Іздеу тарихынан жоюды қалайсыз ба? + Іздеу тарихынан жою + Іздеу тарихын тазалау + Барлық іздеу тарихын тазалауға сенімдісіз бе? + Іздеу бойынша кеңестер + "• Оған өту үшін жолдың үстін басыңыз +• Баптауға өту үшін оны ұзақ басып тұрыңыз +• Іздеу сұранысын тарихқа сақтау үшін Enter басыңыз +• Іздеу кезінде регистр және тыныс белгілері ескерілмейді +• Басты баптаулар өшірілген қосалқы баптаулардан жоғары көрсетіледі" + Іздеу тарихы бос + Іздеу тарихын сақтау үшін іздеу сұранысын енгізіп, Enter пернесін басыңыз + Баптаулардың іздеу тарихын көрсету + Morphe параметрлерінің атауы + Әдепкі + Қосымша + Түзетулер + Морфинг уақыты + Жеке + Атауды енгізіңіз + Аты бос болмауы керек + Morphe баптауларының белгішелерін көрсету + Көшірілді: %s + Morphe тілі + "Кейбір тілдер үшін аудармалар жоқ немесе толық емес болуы мүмкін. + +Жаңа тілдерді аудару немесе қолданыстағы аудармаларды жақсарту үшін morphe.software/translate бетіне өтіңіз" + Қосымша тілі + Импорт / Экспорт + Morphe баптауларын импорттау / экспорттау + Файлдан импорттау + Баптаулар сәтті импортталды, қолдану үшін Сақтау түймесін басыңыз + Баптауларды импорттау сәтсіз аяқталды + Файлға экспорттау + Баптаулар сәтті экспортталды + Баптауларды экспорттау сәтсіз аяқталды + Сіз Morphe Patches нұсқасының ең соңғы <i>%s</i> нұсқасын қолданып жатырсыз + Сіз Morphe Patches нұсқасының <i>%1$s</i> нұсқасын қолданып жатырсыз<br><br><b>Жаңарту қолжетімді: <i>%2$s</i></b><br><br>Патчтарыңызды жаңарту үшін Morphe қолданып, осы қолданбаны қайта патчтаңыз + Ескертпе + Бұл нұсқа алдын ала шығарылған нұсқа және күтпеген мәселелер болуы мүмкін + Ресми сілтемелер + Веб-сайт + Қайырымдылық жасау + Аудармалар + Авторы + Жаңартылған + Жаңарту қолжетімді + Ағымдағы әзірлеу + Алдыңғы әзірлеу + Белсенді әзірлеу + 2022 ‑ 2025 + 2022 ‑ 2025 + 2019 ‑ 2022 + Лицензиялық ескертулер + Жүктелуде… + Дебагтау + Кеңейтілген әзірлеуші параметрлері мен диагностикалық логтарды баптау + Дебаг логтарды қосу + Дебаг лог стегінің ізі + Қателерді анықтауға көмектесу үшін дебаг логтарға стек трассаларын қосады + Morphe қатесі кезінде хабарламаны көрсету + "Қателіктер туралы toast-хабарламаларды өшіру Morphe-тің барлық қателік хабарландыруларын жасырады. + +Кез келген күтпеген оқиғалар туралы сізге хабарланбайды" + Дебаг логтарын экспорттау + Morphe беттау логтарын экспорттау + Беттау логтары сәтті экспортталды + Беттау логтарын экспорттау сәтсіз аяқталды + Дебаг логтарын іздеу… + Тазалау + Дебаг логтау өшірілген + Беттау логтары табылмады + Беттау логтары көшірілді + Беттау логтарын экспорттау сәтсіз аяқталды: %s + Беттау логтары тазартылды + Функция белгілерінің менеджері + Бульдір функция белгілерін басқару + Функция белгілерін іздеу… + Белсенді емес (%s) + Белсенді (%s) + Блокталған (%s) + Мәжбүрленген (%s) + Авто + Өшіру + Қосу + Мәжбүрлеп қосу + Блоктау + Жою + Функция флагтары сақталды + Функция белгілері sıғырылды + Функция белгілері алмасу буферіне көшірілді + Осы параметрді қолдану үшін қолданбаны қайта қосыңыз + Экспорт және импорт + Функция белгілері табылмады + Функция белгілері сәтті экспортталды + Функция белгілерін экспорттау сәтсіз аяқталды + Функция белгілері жүктелді, қолдану үшін «Сақтау» түймесін басыңыз + Файлды оқу мүмкін болмады + Екілік іздеу + "Қолданба мінез-құлқына қай функция белгісі әсер ететінін анықтайды. + +%s белгісінің жартысы бұғатталған, әр қайта қосудан кейін сіз мінез-құлықтың әлі де бар-жоғын айтасыз. + +Бастамас бұрын мінез-құлықты бір рет қайталаңыз, әйтпесе қолданба әлі сұрамаған белгілер іздеуден тыс қалады" + Бастау + "Іздеу үшін мүмкіндік белгілері жоқ. + +Екілік іздеуде тек қолданба қолданған және қазіргі уақытта мәжбүрлеп қосылмаған белгілерді ғана пайдалануға болады" + "Қадам %1$s + +%2$s белгі қалды, оның %3$s-і осы қадам үшін бұғатталды. + +Қолданбаны қайта қосыңыз, мінез-құлықтың бар-жоғын тексеріңіз, содан кейін төменде жауап беріңіз" + Мүмкіндік белгісін іздеу жүріп жатыр + Мінез-құлық бар + Мінез-құлық жоқ + Екілік іздеуді тоқтату + Екілік іздеу тоқтатылды + "%s мүмкіндік белгісі осы мінез-құлыққа себеп болады. + +Ол қазір бұғатталды" + "Функция белгісі табылмады. + +Қосымша мінез-құлқы функция белгісімен басқарылмайтын сияқты" + "Бір ғана функция белгісін табу мүмкін болмады. + +Іздеу кезінде мінез-құлық ерте жоғалды, сондықтан бірнеше белгі қатысуы мүмкін немесе жауаптардың бірі қате болды" + MicroG параметрлерін ашу + MicroG орнатылмаған. Оны орнатыңыз + Әрекет қажет + "Сіздің MicroG v%1s нұсқасы ескірген және мәселелер тудыруы мүмкін. + +MicroG-ді соңғы v%2s нұсқасына жаңартыңыз" + Жаңарту + Елемеу + "Орнатылған MicroG Morphe тарапынан ұсынылмаған: + +%1s + +Оны жою үшін басыңыз, содан кейін осы қолданбаны қайта ашып, MicroG RE орнатыңыз" + Жою + "MicroG қолданбасында фондық режимде жұмыс істеуге рұқсат жоқ. + +Телефоныңыз үшін \"Don't kill my app\" нұсқаулығын оқып, оны MicroG орнатылған жерге қолданыңыз. + +Бұл қолданбаның жұмыс істеуі үшін қажет" + Веб-сайтты ашу + "Мәселелердің алдын алу үшін MicroG батареясын оңтайландыру мүмкіндігі өшірілуі тиіс. + +MicroG үшін батареяны оңтайландыруды өшіру батареяны пайдалануға кері әсер етпейді. + +«Жалғастыру» батырмасын басып, оңтайландыру өзгерістеріне рұқсат беріңіз" + Жалғастыру + MicroG батареясын оңтайландыру диалогын көрсету + "Мұны өшіру MicroG батареясын оңтайландыру туралы хабарламасын жасырады. + +Егер батареяны оңтайландыру өзгертілмесе, келесі жағдайлар болуы мүмкін: +• Қосымша кездейсоқ жабылуы мүмкін +• Хабарламалар жұмыс істемеуі мүмкін +• Фонда ойнату жұмыс істемеуі мүмкін" + Қолданба сілтемелерін өңдеу + Сілтемелерді осы қолданбада ашуды баптау + 1-қадам: Түпнұсқа қолданба сілтемелерін өшіру + Түпнұсқа қолданбаның параметрлерін ашып, барлық домен сілтемелерін өшіріңіз + 2-қадам: Патчтелген қолданба сілтемелерін қосу + Патчтелген қолданбаның параметрлерін ашып, барлық домен сілтемелерін қосыңыз + Параметрлерді ашу + 🚨 Эксперименталдық ескерту 🚨 + "Сіз ⚠️ <b>%1$s</b> қолданбасының эксперименталдық нұсқасын қолданып жатырсыз +<br/><br/> +🔧 Осы нұсқа үшін патчтарды баптай келе, қолданбаның ерекше жұмысын немесе белгісіз қателерді күтіңіз. +<br/><br/> +Егер сізге ең тұрақты жұмыс керек болса, онда мұны <b>жойыңыз</b> және ✅ <b>%2$s</b> ұсынылған нұсқасын орнатыңыз" + ⚠️ Маған эксперименталдық нұсқа керек + ✅ Маған тұрақты нұсқа керек + Қателік журналы пайдаланушы құжаттарына сақталды: %s + Қателік журналы сақталды: %s diff --git a/patches/src/main/resources/addresources/values-kk-rKZ/sponsorblock/strings.xml b/patches/src/main/resources/addresources/values-kk-rKZ/sponsorblock/strings.xml index 9d2d4ab7d5..93e63bf3bf 100644 --- a/patches/src/main/resources/addresources/values-kk-rKZ/sponsorblock/strings.xml +++ b/patches/src/main/resources/addresources/values-kk-rKZ/sponsorblock/strings.xml @@ -16,7 +16,9 @@ Second \"item\" text" --> + SponsorBlock батырмалары мен басқару элементтері үшін шаршы тәрізді бұрыштарды қолданады + Жүктелуде… diff --git a/patches/src/main/resources/addresources/values-kk-rKZ/youtube/strings.xml b/patches/src/main/resources/addresources/values-kk-rKZ/youtube/strings.xml index 70ce03fc67..e29dadd823 100644 --- a/patches/src/main/resources/addresources/values-kk-rKZ/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-kk-rKZ/youtube/strings.xml @@ -18,213 +18,1638 @@ Second \"item\" text" + Morphe туралы + Жарнамалар + Лента + Жалпы + Плеер + Shorts + Іздеу жолағы + Сырғыту басқаруы + Әртүрлі + Бейне + YouTube-тің ескі баптаулар экранын қалпына келтіру + Фонда ойнату шектеулерін алып тастау + Фонда ойнатуды өшіру + Құрылғы бұғатталғанда автоматты түрде кідірту + Өшіру + Бұғатталғанда әрқашан кідірту + Сымсыз аудио қосылмаған жағдайда кідірту + Жіркіту жылдамдығының шектеуін өшіру + Макет жаңартуларын өшіру + Сервердің қолданба лейаутын жаңартуын болдырмайды + "Қосымшаның макеті алғаш орнатылғандағы макетке қайтарылады. + +Кейбір сервер жағындағы макеттер қайтарылмауы мүмкін. + +Өзгерістерге мыналар кіреді: +• Плеердің жылжымалы мәзіріндегі компоненттер (немесе тиісті баптаулар) жұмыс істемеуі мүмкін +• Айналмалы сандар анимацияланбайды +• Кітапхана қойындысы көрсетіледі +• Бейне сипаттамасының музыка бөлімі жұмыс істемеуі мүмкін +• Плеердің жылжымалы мәзірінің элементтерін жасыру мүмкін емес +• Кітапхана қойындысында аккаунтты ауыстыру түймесі пайда болмауы мүмкін" + Лентаның жылжымалы мәзірі сүзгісін қосу + Лентаның жылжымалы мәзірі сүзгісі + Сүзгіден өткізу үшін жаңа жолдармен бөлінген flyout мәзірінің атауларын енгізіңіз + Кітапхана / Сіз төменгі мәзір фильтрін қосу + Кітапхана / Сіз төменгі мәзір фильтрі + Сүзгілеу үшін Кітапхана / Сіз төменгі мәзірінің атауларын жаңа жолдармен бөліп енгізіңіз + Альбом карталарын жасыру + Орындаушы сипаттамасының астындағы альбом карточкаларын жасырады + Әртіст карталарын жасыру + Іздеу нәтижелерінің жоғарғы жағындағы орындаушы карточкасын жасырады + Арна батырмаларын жасыру + Іздеу нәтижелеріндегі арна профильдерінен Жазылу, Арнаны қарау, Қоғамдастық және YouTube Music сияқты батырмаларды жасырады + Қоғамдастық жазбаларын жасыру + Ықшамдалған баннерлерді жасыру + Ықшам баннерлерді және кейбір компоненттер арасындағы бос орындарды жасырады + Бейнебаяндардың астындағы жайылатын карточкалар + Барлығын көрсету + Тек қорытынды картасын жасыру + Барлығын жасыру + Жүзіп жүрген микрофон батырмасын жасыру + Іздеу ұсыныстары экранының төменгі оң жақ бұрышындағы қалқымалы микрофон батырмасын жасырады + \"Premium алу\" түймесін жасыру + Тарих сөресін жасыру + Көлденең сөрелерді жасыру + Келесідей көлденең сөрелерді жасырады: Шұғыл жаңалықтар, Сізге ұнауы мүмкін қауымдастық жазбалары, Көруді жалғастыру, Басқа арналарды зерттеу, Басқа тақырыптарды зерттеу, Сіз үшін, Соңғы бейнелер, Ең өзектілері, Автордың қауымдастығынан жазбалар, Сауда және Қайта көру + Кескін сөресін жасыру + Іздеу нәтижелеріндегі кескіндер сөресін жасырады + \"Басқаларды хабарлама жіберуге шақыру\" карточкасын жасыру + Хабарландырулар қойындысының Хабарламалар бөлімінде көрсетілетін шақыру карточкасын жасырады + \"Жаңа бейнебаяндар\" батырмасын жасыру + Басты лентаның жоғарғы жағында пайда болатын көк \"Соңғы видеолар\" баннерін жасырады + Тікелей эфирлерді жасыру + Басты лентада және іздеу нәтижелерінде қазір тікелей эфирде жүрген бейнелерді жасырады + Микс плейлисттерін жасыру + Фильмдер бөлімін жасыру + «Хабарландырулар» мәзірінің тақырыбын жасыру + Қоңырау батырмасын басқанда Хабарландырулар мәзірінің тақырыбын жасырады + \"Маған хабарла\" түймесін жасыру + Жоспарланған тікелей эфирлер мен премьералар үшін еске алу түймесін жасырады + Playables бөлімін жасыру + Ойындарды ойнауға мүмкіндік беретін функцияны жасырады + Іздеу терминдерінің миниатюраларын жасыру + Іздеуді автоматты түрде толықтыру тізіміндегі миниатюралардың алдын ала қарауын жасырады + \"Толығырақ көрсету\" түймесін жасыру + Іздеу нәтижелеріндегі видеолардың астындағы кеңейту түймесін жасырады + Жазылған арналар жолағын жасыру + Жазылған арналардың атауларын жасыру + Жазылған арналар жолағындағы арна атауларын жасырады + Сауалнамаларды жасыру + Лента ұсыныстарының дәлдігі туралы сауалнамалар мен ескертулерді жасыру + Билеттер сөресін жасыру + + \"Қолжетімсіз бейнебаяндар жасырылды\" баннерін жасыру + Плейлистердегі \"Қолжетімсіз бейнебаяндар жасырылды\" баннерін жасырады + Жүктеу уақытын жасыру + "Шектеулер: +• Shorts сөрелерінде, арна беттерінде және іздеу нәтижелерінде жүктеу уақыты әлі де көрсетілуі мүмкін +• Автокөлік интерфейсімен жұмыс істемейді" + Видео мұқабасын жасыру + Бейне белгілерін жасыру + \'Автоматты түрде дубляжталған\' / \'Дубляжталған\', \'Комиссия табыс әкеледі\', \'Хайп\', \'Адамдар соны да қарады\', \'өнім(дер)\' және \'Сізге ұнауы мүмкін\' сияқты белгілерді жасырады + Көру санын жасыру + "Шектеулер: +• Shorts сөрелерінде, арна беттерінде және іздеу нәтижелерінде қаралым саны әлі де көрсетілуі мүмкін +• Автокөлік интерфейсімен жұмыс істемейді" + Веб-іздеу нәтижелерін жасыру + Іздеу нәтижелерінен \"Веб-желідегі нәтиже\" бөлімін жасырады + \"Сізге ұнауы мүмкін\" бөлімін жасыру + YouTube Doodles элементтерін жасыру + YouTube логотипіндегі маусымдық және іс-шараларға негізделген анимацияларды жасырады + YouTube Doodles көрсетілуі арнайы тақырыптардың көрсетілуіне кедергі келтіруі мүмкін + Арна жолағын жасыру + Арна су белгісін жасыру + Бөлімдерді және \"Осы видеода\" батырмасын жасыру + Краудфандинг блогын жасыру + Төтенше жағдайлар туралы хабарламаларды жасыру + Ақпараттық панельдерді жасыру + Медициналық ақпарат сияқты ақпараттық контекст панельдерін жасырады + \"Қосылу\" батырмасын жасыру + \"Тікелей чат қайталамасы\" батырмасын жасыру + Толық экранды режимдегі чат қайта ойнату түймесін жасырады + Медициналық панельдерді жасыру + Плеер ишараларының нұсқауларын жасыру + Ойыншы жесттері кезінде көрсетілетін «Дәл іздеу үшін жоғары тартыңыз», «Бас тарту үшін жіберіңіз» және «2x» сияқты кеңестерді жасырады + Snackbar-ды жасыру + Жазылушыларға арналған ережелерді жасыру + \"Бейнемен синхрондау\" батырмасын жасыру + Басып тұру градиентін жасыру + Жылдамдыру үшін басып тұрып, саусағыңызды сырғытқанда плеер басқару элементтерінің градиентін жасырады + Уақытша реакцияларды жасыру + Пайдаланушыларға пікірлер бөлімінде және тікелей чатта эмодзилермен реакция білдіруге мүмкіндік беретін функцияны жасырады + \"Көмек & кері байланыс\" мәзірін жасыру + Іздеу нәтижелеріндегі сияқты үш нүктелі түймені қолданғанда \"Көмек & кері байланыс\" мәзірін жасырады + Дружинені жасыру + \"Сіз\" қойындысындағы арнаңыздың дружиненін жасырады + Бейнебаян тақырыбын жасыру + Толтыр экран режимінде бейне атауын жасырады + Бейнесубатитрлерін тазарту + Бейне субтитрлерінен \'#\', \'Қаржы жинау\', \'өнім(дер)\' және \'Дүкен\' сияқты тіркестерді тазартады + \"Gemini-ден сұрау\" батырмасын жасыру + \"Осы видео туралы сұрау\" панелінің төменгі жағындағы \"Gemini-ден сұрау\" батырмасын жасырады + Бейнебаян сипаттамасы + Бейне сипаттамасында қай компоненттер көрінетінін таңдаңыз + \"AI жасаған бейнеқоспа\" бөлімін жасыру + Сұрақ бөлімін жасыру + Атрибуттар бөлімін жасыру + Танымал жерлер, Ойындар, Музыка және Аталған адамдар бөлімдерін жасырады + Арна сілтемелері бөлімін жасыру + Тараулар бөлімін жасыру + Түзетулер бөлімін жасыру + \"Курс барысы\" бөлімін жасыру + Кашшту Кезгелкенаны болимини + Осы курсты зерттеу және Подкастты зерттеу бөлімдерін жасырады + \"Осы курсты зерттеу\" бөлімін жасыру + \"Explore the podcast\" бөлімін жасыру + Таңдаулы арналар бөлімін жасыру + Таңдаулы сілтемелер бөлімін жасыру + Таңдаулы плейлисттер бөлімін жасыру + Таңдаулы бейнелер бөлімін жасыру + Хэштег бөлімін жасыру + «Бұл қалай жасалды» бөлімін жасыру + Hype ұпайларын жасыру + Ақпараттық карталар бөлімін жасыру + \'Негізгі ұғымдар\' бөлімін жасыру + Викториналар бөлімін жасыру + \"Осы бейнеден іздеу\" бөлімін жасыру + Жазылу түймесін жасыру + Транскрипция бөлімін жасыру + \"Бейне деректері\" бөлімін жасыру + Сүзу жолағы + Жылжымалы фильтр жолағын қай жерде жасыруды таңдаңыз + Арна бетінде жасыру + Пікірлерде жасыру + Ленталарда жасыру + Ұқсас бейнелерде жасыру + Іздеу нәтижелерінде жасыру + Көру тарихында жасыру + Арна беті + Арна бетінде қай компоненттер пайда болатынын таңдаңыз + Арна қосымша бетінің сүзгісін қосу + Арна қосымша бетінің сүзгісі + Сүзгіден өткізу үшін арна қойындыларының атауларын жаңа жолдармен бөліп енгізіңіз + Қоғамдастық түймесін жасыру + Қосылу түймесін жасыру + Сілтемелер алдын ала қарауын жасыру + Арнаның жоғарғы бөлігіндегі басылатын веб-сілтемелерді жасырады + \"Тек мүшелер үшін\" белгісін жасыру + Арна сүзгі жолағындағы \"Тек мүшелер үшін\" белгісін жасырады + Мүшелер сөресін жасыру + Арна мүшелерінің тізімін жасырады + Жазбалар сөресін жасыру + \"Дүкен\" батырмасын жасыру + \"Жазылу\" батырмасын жасыру + Зерттеу мәзірі + Зерттеу мәзірінде қай компоненттер көрсетілетінін таңдаңыз + Курстар мәзірін жасыру + Зерттеу түймесін жасыру + \'Сән & сұлулық\' мәзірін жасыру + Гейминг мәзірін жасыру + Hype мәзірін жасыру + Оқу мәзірін жасыру + Тікелей эфир мәзірін жасыру + Мүшелік мәзірін жасыру + «Кинолар & теледидар» мәзірін жасыру + Музыка мәзірін жасыру + Жаңалықтар мәзірін жасыру + Playables мәзірін жасыру + Подкасттар мәзірін жасыру + Shopping мәзірін жасыру + Спорт мәзірін жасыру + YouTube Premium мәзірін жасыру + YouTube Studio мәзірін жасыру + YouTube Music мәзірін жасыру + YouTube Kids мәзірін жасыру + YouTube Create мәзірін жасыру + \"YouTube қалай жұмыс істейді\" мәзірін жасыру + Заңдық және құпиялылық туралы төменгі ақпаратты жасыру + Пікірлер + Пікірлер бөлімінде қай компоненттер пайда болатынын таңдаңыз + Пікірлер каруселінің сүзгісін қосу + "• Карусель элементінің дәл атауын енгізіңіз (мысалы, Сұрау, Бұл видеоны жарнамалау, YouTube Music-те тыңдау) +• Карусель элементтерін жасыру күтілмеген жанама әсерлерге әкелуі мүмкін +• Егер барлық карусель элементтері жасырылса, пікірлер бөлімі де толықтай жасырылады" + Пікірлер каруселінің сүзгісі + Сүзгіден өткізу үшін пікірлер каруселінің атауларын жаңа жолдармен бөліп енгізіңіз + ЖИ чат тұйындымасын жасыру + Арна нұсқаулықтарын жасыру + Контекст хабарламаларын жасыру + \"Мүшелердің пікірлері\" тақырыбын жасыру + Пікірлер бөлімін жасыру + Басты лентадағы пікірлер бөлімін жасыру + Қауымдастық нұсқауларын жасыру + \"Short жасау\" батырмасын жасыру + \"Ұнамады\" түймесін жасыру + Emoji түймесін жасыру + Сүзу жолағы параметрлерін жасыру + \"Танымал\" және \"Жаңасы\" параметрлерінен басқа барлық сүзу жолағы параметрлерін жасырады + Сыйлық анимациясы мен карталарын жасыру + Сыйлық батырмасын жасыру + Ақпарат батырмасын жасыру + Тікелей чат донаторларының жолағын жасыру + Тікелей чаттың төменге айналдыру түймесін жасыру + Тікелей чат көмекші мәтіндерін жасыру + Чат енгізу жолағының жанында көрсетілетін көмекші мәтіндерді, мысалы, тек жазылушыларға арналған чат туралы хабарламаны жасырады + Мәзір түймесін жасыру + Әр пікірдің 3-нүктелі мәзір түймесін жасырады + Пікір алдын ала қарауын жасыру + Толық пікірлер бөлімін ашпас бұрын бейнебаян астындағы пікірлер алдын ала қарауын жасырады + Пікір Morphe арқылы жасырылды… + Минималистік пікірлер батырмасын қосу + Пікірлер батырмасында көрсетілетін жылжымалы карточкаларды және олардың бет таңдағышын жасырады + \"Рахмет\" батырмасын жасыру + Уақыт белгісі түймесін жасыру + \"Топ-жанкүйерлер\" батырмасын жасыру + Аудару түймесін жасыру + \'Әлі пікірлер жоқ\' хабарламасын жасыру + Белгіленген іздеу сілтемелерін тазарту + Белгіленген іздеу сілтемелерін тазартады, тек түпнұсқа қарапайым мәтінді қалдырады + Кілт сөз сүзгіші + Ленталарда және іздеуде бейнелерді жасыру үшін нақты сөздерді немесе тіркестерді таңдаңыз + Тігілмелерді кілт сөздер бойынша жасыру + Басты беттегі бейнелерді кілт сөздер бойынша жасыру + Іздеу нәтижелерін кілт сөздер бойынша жасыру + Жазылымдардағы бейнелерді кілт сөздер бойынша жасыру + Жасырылатын кілт сөздер + "Жасыру керек түйін сөздерді және тіркестерді жаңа жолдармен бөліп енгізіңіз + +Түйін сөздер видео тақырыптарында немесе арна атауларында көрсетілетін кез келген мәтін болуы мүмкін + +Ортасында бас әріптері бар сөздер дәл сәйкес келуі тиіс (мысалы, iPhone, TikTok, LeBlanc)" + "Басты бет / Жазылымдар / Іздеу / Тігілмелерде кілт сөз тіркестеріне сәйкес келетін мазмұнды жасыру үшін сүзгі қойылған + +Шектеулері: +• Shorts бейнелерін арна атауы бойынша жасыру мүмкін емес +• Кейбір интерфейс компоненттері жасырылмауы мүмкін +• Кілт сөзді іздегенде нәтижелер көрсетілмеуі мүмкін" + Сөздерді толық сәйкестендіру + Кілт сөзді/фразаны қос тырнақшаға алу видео тақырыптары мен арна атауларының ішінара сәйкес келуін болдырмайды<br><br>Мысалы,<br><b>\"ai\"</b> келесі видеоны жасырады: <b>How does AI work?</b><br>бірақ мынаны жасырмайды: <b>What does fair use mean?</b> + Кілт сөзді қолдану мүмкін емес: %s + Кілт сөзді қолдану үшін тырнақша қосыңыз: %s + Кілт сөзде қайшылықты декларациялар бар: %s + Кілт сөз тым қысқа, тырнақшалар қажет: %s + Кілт сөз барлық бейнелерді жасырады: %s + "• Басты бет: %s +• Жазылымдар: %s +• Іздеу: %s +• Пікірлер: %s +• Жиынтығы: %s" + Статистика + Жасырылған бейнелер, соңғы 24 сағат + Жасырылған бейнелер, барлық уақыт + Соңғы 24 сағат есепшісін нөлге түзету? + Барлық уақыт есепшісін нөлге түзету? + AI арналар сүзгісі + AiSList қауымдастық тізімімен AI-генерацияланған контент ретінде белгіленген арналардың видеоларын жасыру + Блок тізімі: Жоғары сенімділік + Ескерту тізімі: Орташа сенімділік + Басты беттегі видеоларды жасыру + Іздеудегі видеоларды жасыру + "• Басты бет: %s +• Іздеу: %s +• Барлығы: %s" + Блок тізіміндегі арналар + Ескерту тізіміндегі арналар + Арналарды жүктеу үшін сүзгіні қосыңыз + Арналар жүктелуде… + "Басты бет пен іздеу нәтижелері AiSList белгілеген арналардың видеоларын жасыру үшін сүзгіленген + +Шектеулер: +• Сүзгіленген AI Shorts бос орын ретінде көрінуі мүмкін +• Кейбір AI видеолары әлі де көрінуі мүмкін" + "AiSList — бұл еріктілер жүргізетін, AI арқылы жасалған контентті жариялайтын YouTube арналарының ашық бастапқы тізімі. Арналар қолданушылармен хабарланады және қосылмас бұрын тексеріледі + +Тізім GitHub-тан жүктеледі және бейнелер сіздің құрылғыңызда сүзгіден өткізіледі" + AI арналарын хабарлау + Бейне мәзіріне хабарлау жазбасын қосу + Бейне мәзіріне бейне арнасын AiSList-ке хабарлайтын жазбаны қосу + Жіберушінің аты + AiSList қоғамдық көшбасшылар кестесінде сіздің хабарламаларыңыздың жанында көрсетілетін ат. Бұл өріс бос болса, хабарлау мүмкін емес + AI арнасын хабарлау + "Осы бейнені дәлел ретінде қолдана отырып, %s каналын AiSList-ке жіберу керек пе? + +Тек мынадай каналдарды хабарлаңыз: +• Негізгі контент ретінде AI дауыстарын, визуалды элементтерін, сценарийлерін немесе музыкасын қолданатындар +• AI құралдарымен адамның аз қатысуымен контентті көп мөлшерде шығаратындар + +Тек AI туралы талқылайтын немесе бейнелерінің кейбір бөліктерінде AI қолданатын каналдарды хабарламаңыз. + +Шағымдар жария етіледі және канал тізімге қосылмас бұрын қаралады. Жою тек GitHub-тағы сұраныс арқылы ғана мүмкін" + Бұл канал redan AiSList бұғаттау тізімінде бар + Бұл канал redan AiSList ескерту тізімінде бар + Хабарлау + Каналдарды хабарлау үшін AI канал сүзгісінің параметрлерінде жіберуші есімін орнатыңыз + Арна ізделіп жатыр… + Осы бейненің арнасы табылмады + Арна туралы шағым жіберілді және қаралуды күтуде + AiSList шағымды сақтай алмады, кейінірек қайталап көріңіз + Тым көп шағым жіберілді, кейінірек қайталап көріңіз + AiSList шағымды қабылдамады + Шағым жіберу сәтсіз аяқталды, қосылымды тексеріңіз + Арна жарнамаларының әкта wherever-тізімі + Ақ тізімге енгізілген арналардың видеолары жарнамамен көрсетіледі, осылайша олардың авторларына төлем жасалады. Басқа барлық арналарда жарнама бұғатталған күйінде қалады + Арнаны жарнама ақ тізіміне қосу + Арнаны жарнама ақ тізімінен алып тастау + Автордың дүкен сөресін жасыру + Соңғы экрандағы дүкен баннерін жасыру + Жалпы жарнамаларды жасыру + Тауар баннерлерін жасыру + Ақылы жарнама белгілерін жасыру + Плеердегі терезелі жарнамаларды жасыру + Өз-өзіне демеулік карталарын жасыру + Сауда сілтемелерін жасыру + Бейнежарнамаларды жасыру + YouTube Premium жарнамаларын жасыру + Жүктелуде… + Ағымдағы арна: + Ақ тізімге қосу + Алып тастау + Ақ тізімдегі арналар + Ақ тізімде арналар жоқ + Арна ақ тізімге қосылды + Арна ақ тізімнен алынып тасталды + Арна ізінен іздеу + Тек осы арнаны іздеу үшін арна беттеріне екінші іздеу түймесін қосады + Арнаны іздеу + Арнаны іздеу нәтижелері + Бұл арнада ештеңе табылмады + Бұл арнаны іздей алмады + Бейнелер + Жазбалар + Бұл арнада сәйкес келетін жазбалар жоқ + Сәйкестігі + Ең көп қаралғандар + Ең аз қаралғандар + Ең жаңалары + Ең ескілері + Ең қысқа + Ең ұзын + Сілтеме алмасу буферіне көшірілді + Уақыт белгісі бар сілтеме алмасу буферіне көшірілді + «Бейне сілтемесін көшіру» батырмасын көрсету + Бейне сілтемесін көшіру үшін бір рет басыңыз, уақыт белгісімен көшіру үшін басып тұрыңыз + Әдепкі бойынша уақыт белгісін көшіру + «Бейне сілтемесін көшіру» батырмасының бір рет басу және басып тұру әрекеттерін ауыстырады + Бейне атауын көшіру + Бейне атауын көшіру үшін оны басып тұрыңыз + Бейне атауы алмасу буферіне көшірілді + Пікірлерді көшіру + Пікірді көшіру үшін оны басып тұрыңыз + Пікір алмасу буферіне көшірілді + Бейнеменюді ашу үшін басып тұрыңыз + Тапбағаш бейнесінің кез келген жерін басып тұру оның мәзірін ашады + Шағын суреттерде «Кейінірек көру» батырмасын көрсету + Тапбағаш бейнелерінің шағын суреттерінде «Кейінірек көру» батырмасы көрсетіледі. Оны басып тұру «Плейлистке сақтау» функциясын ашады + Жазба суреттерін жүктеу + Суреттері немесе GIF-тері бар жазбалардың «Бөлісу» батырмасының жанында жүктеу батырмасы көрсетіледі және олардың мәзіріне жүктеу элементі қосылады + Суреттерді жүктеу + Суретті жүктеу + %s сурет жүктелуде… + Кескінді жүктеу... + Кескіні %s жеріне сақталды + Кескінді жүктеп алу мүмкін болмады + Бұл жазбада кескіндер табылмады + Жалпы мәзірге Сұрау элементін қосу + Лентаның жалпы мәзіріне бейнені жинақтау элементін қосады + "Сұрау функциясын бастапқы түрде қолдану үшін Сұрау қолжетімді елде тіркелген аккаунт қажет. + +Сұрау батырмасы бар видео бір рет ашылғаннан кейін, бұл мүмкіндік ағымдағы елде Сұрау функциясы болмаса да, барлық видеолармен жұмыс істейді." + Видеоны жинақтау + Осы видео үшін Сұрауды ашу мүмкін болмады + Сұрау функциясы сіздің аккаунтыңыз немесе ағымдағы еліңіз үшін қолжетімді емес + Мұқабаларда қаралым санын көрсету + Лентадағы бейнелердің қаралым саны тақырыптың астында емес, мұқабаларының төменгі сол жағында көрсетіледі. Кейбір бейнелерде, мысалы, сөрелерде, қаралым саны көрсетілмеуі мүмкін + Лентадағы превьюда лайк санын көрсету + Лентадағы превьюда лайк пайызын көрсету + Пікірлерді іздеу + Тек ізделіп табылған барлық сөздері бар жүктелген пікірлерді көрсету үшін пікірлердегі іздеу түймесін басыңыз. Фразаны іздеу үшін сөздерді тырнақшаға алыңыз + Пікірлерді іздеу + Ізделетін сөздер + Іздеу + Тазалау + Сұрыптау ретін өзгертіңіз немесе іздеу үшін пікірлерді қайта ашыңыз + Іздеу тазартылды. Барлық пікірлерді көрсету үшін сұрыптау ретін өзгертіңіз немесе пікірлерді қайта ашыңыз + \"ТВ-ге кіру\" терезесін өшіру + \"Құрылғыларыңызды қосыңыз\" терезесін өшіру + Бөлімді екі рет түрту арқылы өткізуді өшіру + Бөлімдерді өткізетін екі саусақпен екі рет ұрып, белгілеу қимылын өшіреді, бұл қимыл бір саусақпен байқаусызда іске қосылуы мүмкін + Бейнелерді, подкасттарды немесе әндерді үшінші тараптың жүктеу қосымшалары арқылы жүктеңіз + Плеер қабатындағы жүктеу түймесін көрсету + Бейне плеерінде сыртқы жүктейтініңізді ашатын түймені көрсетеді + Тіккенде жылдам жүктеу + Фонда жүктеу туралы хабарлама көрсетеді. Жүктегішті ашу үшін басып тұрыңыз. YTDLnis және YTindir қолданбаларымен жұмыс істейді + Жүктелген файлдарды қаралды деп белгілеу + Сыртқы жүктегішпен жүктелген бейнелер YouTube қарау тарихыңызға қосылады + Full HD (1080p) + Орташа (192 kbps) + Жылдам жүктеу + Бейне + Аудио + Барлық опциялар + Жүктеуді Shorts мәзірінде көрсету + Shorts плееріне сыртқы жүктегіштің теңіздік мәзір элементін қосады + Жүктеу + Думбис авазарлама авазарланды + Видео субтитриаларондырыны курлгалыкта кургиленди меканизмен мълдерининини мълдерининини кургиленди ощавылылыкта + Бейнеойнатқышта қабат батырмасын көрсетеді. Аударманы қосу/өшіру үшін бір рет басыңыз, жылдам параметрлер үшін басып тұрыңыз + Тилни + Авазарландыры кҁизгири + Фабизинининий аудионинининий денелевели + Авазарланды авазарландынынининий YouTube аудионионининининионини авазарланды ощылылыкшандыганды + Аударма дыбысы + Дубляжталған аударма дауысының деңгейі + Максималды сөйлеу жылдамдығы + Сөйлеуді субтитр ұяшығына сыйдыру кезіндегі максималды жылдамдық коэффициенті + "Тегін аударма API, API кілті талап етілмейді. Тегін тариф бойынша күніне 5 000 таңбаға дейін, ал электронды пошта мекенжайымен күніне 50 000 таңбаға дейін. + +MyMemory.translated.net сайтына өту үшін осы жерді басыңыз" + MyMemory электронды поштасы + Міндетті емес. Күнделікті лимитті 5 000 таңбадан 50 000 таңбаға дейін арттыру үшін MyMemory электронды поштаңызды енгізіңіз. Анонимді лимитті пайдалану үшін бос қалдырыңыз + "OpenRouter арқылы ақылы AI аудармасы. Бір рет толтырып, мыңдаған бейнероликтерді аударыңыз. Mistral Nemo - ең арзан нұсқа, Gemma 4 - ең жоғары сапаны ұсынады. + +OpenRouter.ai сайтына өту үшін осы жерді басыңыз" + OpenRouter API кілті + Сіздің OpenRouter API кілтіңіз. Кілтті OpenRouter.ai > Workspace > API Keys бөлімінде жасаңыз + OpenRouter моделі + Custom + OpenRouter бапталмаған + Осы провайдерді таңдамас бұрын, баптаулар бөліміне OpenRouter API кілтін енгізіңіз + Тегін + %s 100 бейне-сағат үшін + Дауыс + Жүйе + Көптілді ер + Көптілді әйел + Ер + Әйел + Морфинг уақыты келді! + Аударма қолжетімсіз + "Аударма қызметі %s HTTP қатесін қайтарды. Сіздің IP-мекенжайыңызға 24 сағатқа дейін шектеу қойылған болуы мүмкін. + +Аудармаларды қолдануды жалғастыру үшін баптауларда басқа аударма провайдеріне ауысыңыз немесе VPN қолданыңыз" + Қайта көрсетпеу + "Баптауларыңызда OpenRouter API кілті бар. + +API кілтін ешқашан бөліспеңіз. Оған қолжетімділігі бар кез келген адам сіздің OpenRouter кредитіңізді қолдана алады" + OpenRouter кредитінің лимиті таусылды + OpenRouter кредитінің лимиті таусылды. Теңіміңізді толтырыңыз немесе OpenRouter.ai сайтында кілттің шығындар лимитін арттырыңыз + OpenRouter.ai сайтына өту + OpenRouter аудармасы сәтсіз аяқталды (HTTP %s) + MyMemory күнделікті лимиті таусылды + Келесісі %s уақытта қолжетімді болады + Сіз бүгінгі MyMemory тегін аударма квотасын (5 000 таңба) пайдаландыңыз. Күнделікті лимитті 50 000 таңбаға дейін арттыру үшін баптауларға MyMemory электронды поштасын қосыңыз немесе басқа аударма қызметіне ауысыңыз + Сіз бүгінгі MyMemory аударма квотасын (50 000 таңба) пайдаландыңыз. Басқа аударма қызметіне ауысыңыз немесе ертең қайта försöріңіз + MyMemory аудармасы сәтсіз аяқталды (мәрезі %s) + Бейнені қайталап ойнатуды қосу + Бейне қайталануын есте сақтамау + Бейне қайталануының ағымдағы күйінің келесі бейнелер үшін сақталуын болдырмау + Бейнені қайталау түймесін көрсету + Бейнені қайталап ойнату қосылды + Видеоны қайталау өшірілді + Цикл диапазоны + Басы + Соңы + [сс:]мм:сс + [сс:]мм:сс (бос = бейне соңы) + Орнату + Тазалау + %1$s - %2$s аралығы қайталанып жатыр + Уақыт аралығы дұрыс емес + Суретті суретте көрсету түймесін көрсету + Суретті суретте көрсету мәзінін көрсету + Суретті суретте көрсету режиміне ауысу + «Барлығын ойнату» түймесін көрсету + "Арнадағы барлық бейнелердің плейлистін жасау үшін түктіңіз, қайтару үшін басып тұрыңыз + +Ақпарат: Тікелей эфирлерде жұмыс істемеуі мүмкін" + Плейлистті жасау режимі + Барлық мазмұн (Уақыты бойынша сұрыптау, өсу ретімен) + Барлық мазмұн (Уақыты бойынша сұрыптау) + Барлық мазмұн (Танымалдылығы бойынша сұрыптау) + Тек видеолар (уақыты бойынша сұрыптау) + Тек видеолар (танымалдылығы бойынша сұрыптау) + Тек Shorts (уақыты бойынша сұрыптау) + Тек Shorts (танымалдылығы бойынша сұрыптау) + Тек тікелей эфир видеолары (уақыты бойынша сұрыптау) + Тек тікелей эфир видеолары (танымалдылығы бойынша сұрыптау) + Тек мүшелерге арналған барлық контент + Тек мүшелерге арналған видеолар + Тек мүшелерге арналған Shorts + Тек мүшелерге арналған тікелей эфирлер + Арна ID-сы сәйкес келмегендіктен плейлист жасау мүмкін емес + «Бейнені қайта жүктеу» түймесін көрсету + Бейне ID қолжетімсіз. Бейнені қолмен қайта жүктеңіз + \"Кейінірек көруге сақтау\" батырмасын көрсету + «Кейінірек қарауға» сақтау үшін түртіңіз, кезек опциялары үшін басып тұрыңыз + Бейне «Кейінірек көру» плейлистіне сәтті сақталды + Бұл бейне «Кейінірек көру» плейлистінде redan сақталған + Бұл бейнені «Кейінірек көру» плейлистіне сақтау мүмкін емес + «Кейінірек қарауға» сақтау + Плеер параметрлерінің мәзірінде «Кейінірек қарауға сақтау» батырмасын көрсету + Видео параметрлерінің мәзірінен қазіргі қаралып жатқан видеоны \"Кейінірек қарау\" тізіміне сақтауға мүмкіндік береді + Балалар видеоларында «Кейінірек қарауға» сақтау батырмасын көрсету + Мәзірден балаларға арналған видеоларды \"Кейінірек қарау\" тізіміне сақтауға мүмкіндік береді + Shorts мәзірінде \"Кейінірек қарауға сақтау\" батырмасын көрсету + Мәзірден Shorts видеоларын \"Кейінірек қарау\" тізіміне сақтауға мүмкіндік береді + «Кейінірек көруді» плейлистке қайта бағыттау + Кейінірек көру + Morphe кезекке қосу + Сессиялар арасында кезекті сақтау + Қосымшаны қайта қосығаннан кейін кезек сақталады + Сақтау және Кезек әрекеттерін алмастыру + Кезек опциялары үшін түртіңіз, «Кейінірек қарауға» сақтау үшін басып тұрыңыз + Кезектегі келесіні ойнату түрі + Әдепті + Түпкілікті кезек + Morphe кезегі + Morphe кезекі клиенттік жағы + Таغذір апайылмасының кезек мәзірі элементін қосу + Қосымша апайылма элементін қосады. Егер «Кезекке қосу» көрсетілмесе, осыны қосыңыз + Кезек + Кезек (%s) + Барлығын тазалау + Кезекте ештеңе жоқ + Кез келген видео үшін «Кезектегі келесіні ойнату» функциясын қолданыңыз + Қазір ойнатылуда + Кезектегі келесіні ойнату + Кезектегі соңғысын ойнату + Реттеу + Жою + Кезекке қосылды + Бұл бейнені кезекке қосу мүмкін болмады + Плейлисттерді кезекке қосу мүмкін емес + Кезек толы + Бейнебаянды бастау мүмкін болмады + Жалпы мәзірдегі жарнамалардың fehér тізімі + Осы бейнежазбаның арнасында жарнамалардың ойнауына мүмкіндік беретін жылжымалы элементті қосады + Бұл мүмкіндік PotHelper-ді қолдануды немесе \"Бейне ағындарын жалғандау\" параметрін TV немесе Android VR кіруіне өзгертуді талап етуі мүмкін + Ашылмалы мәзірдегі жылдамдықтардың әккі тізімі + Осы бейненің арнасын әрдайым 1.0x жылдамдықпен ойнататын ашылмалы элементті қосады + Жүйеге кірмеген + Плейлист ID жоқ + Кезек бос + Видео ID жоқ + Қате: %s + Кезекке қосу сәтсіз аяқталды + Кезек құру сәтсіз аяқталды + Кезектен жою сәтсіз аяқталды + Кезекті сақтау сәтсіз аяқталды + Кезекке қосылды + Кезек жасалды + Кезектен алынды + Кезек %s жеріне сақталды + Morphe кезекі + Кезекке қосу + Кезекке қосып, ашу + Кезекке қосып, ойнату + Қосып, бейнені қайта жүктеу + Кезектен алып тастау + Алып тастап, кезекті ашу + Алып тастап, бейнені қайта жүктеу + Кезекті ашу + Кезекті плейлистқа сақтау + Плейлист сақталды + %s файлына сақталды + Кезекті тазалау + Кезек тазартылды + Кезек + Кезектегі соңғысын ойнату + Кезектегі келесісін ойнату + YouTube кезекті көрсете алмады. + Бұл бейне үшін кезек дайын емес. Бейне ойнатқышты ашып, қайта көріңіз. + Миксті кезекке қосу немесе Миксті көру? + Көру + Кезекке қосу + Микс жүктелуде… + Тікелей эфир аватарынан арнаны ашу + Арнаның аватарын түктіңіз кезінде тікелей эфирдің ашылуын болдырмайды + Дәл іздеу қимылын өшіру + Іздеу жолағын жоғары тартқанда іске қосылатын дәл іздеу режимін өшіреді + Тікелей эфир DVR мүмкіндігін қосу + Барлық тікелей эфирлерде, тіпті оны әдетте шектейтіндерде де, алға/артқа оралуға мүмкіндік береді + Seekbar превьюларын қосу + Seekbar-ды жылжыту кезінде бейнепревьюсын көрсетеді + Көбірек қайта ойнатылған + "\"Тікелей эфир DVR мүмкіндігін қосу\" қолжетімді болмауы мүмкін + +Бұны түзету үшін \"Бейне ағындарын имитациялау\" функциясын қосыңыз және \"Әдепкі клиентті\" \"TV Simply\" немесе \"visionOS\" мәніне өзгертіңіз" + Тікелей эфирдің DVR ұзақтығын кеңейту + Тікелей эфирлерді 7 күнге дейін артқа қайрауға мүмкіндік береді + "\"Тікелей эфирдің DVR ұзақтығын кеңейту\" қолжетімді болмауы мүмкін + +Бұны түзету үшін \"Бейнеағындарды жалғандау\" функциясын қосыңыз және \"Әдепкі клиентті\" \"TV Simply\" немесе \"visionOS\" мәніне өзгертіңіз" + Ізінді сырғытуды қосу + Бейнеойнатқышты басып тұрғанда 2x жылдамдықпен ойнатудың орнына, іздеу үшін сырғытуды қосады + Тігу үшін түртуді қосу + Сүргітпей-ақ, сол позицияға ауысу үшін іздеу жолағын басыңыз + Солға сырпыру қимылы + Оңға сырпыру қимылы + Жоғарыға сырпыру қимылы + Сол/Оң аймақ ені + Экранның сол және оң жағындағы сырғыту қимылдары үшін резервтелген аймақтың пайызы (5-50) + Жарықтық + Дыбыс + Түпнұсқа + Өшіру + Бұғатталғанда сырғытуды елемеу + Плеер «Экранды бұғаттау» арқылы бұғатталғанда сырғыту қимылдары ескерілмейді + Сырғыту үшін басу қимылын қосу + Сырғыту қимылы танылмас бұрын басып тұруды талап етеді + Гаптикалық кері байланысты қосу + Жарықты сақтау және қалпына келтіру + Толтырылған экран режиміне кіргенде / шыққанда жарықтық деңгейін есте сақтайды + Автожарықтылық қимылын қосу + Жарықтықты ең төменгі мәніне дейін сырғыту жүйелік автожарықтылықты қосады + Авто + Сырғыту қабатыныңタイムауты + Қабат көрінетін миллисекундтар саны + Сырғыту қабатының фонының мөлдірлігі + Мөлдірлік деңгейін реттеу (0 — мөлдір, 100 — мөлдір емес) + Сырғыту қабатының жарықтық түсі + Жарықтық басқару жолағының түсін орнатыңыз + Сырғыту қабатының дыбыс түсі + Дыбыс деңгейін басқару жолағының түсін орнатыңыз + Сырғыту қабатының жылдамдық түсі + Ойнату жылдамдығы бақылауының прогресс жолағының түсін орнатыңыз + Сырғыту қабаты мәтінінің өлшемі + Экрандағы мәтін өлшемін реттеңіз (1-30) + Сырғыту амплитудасының шегі + Реттеу іске қосылғанға дейінгі қажетті сырғыту қашықтығын орнатыңыз + Дыбыс деңгейін сырғыту сезімталдығы + Жоғары мәндер дыбыс деңгейінің әр қадамы үшін көбірек сырғытуды талап етеді + Дыбыс қадамдары + Дыбыс күшейту + Максималды дыбыс деңгейінен жоғары қарай сырғыту оны күшейтеді, бұл қатты аудионы бұзуы мүмкін + Құрылғы бойынша әдепкі + Жарықтылықты сырғыту сезімталдығы + Жоғары мәндер жарықтылық қадамына көбірек сырғытуды талап етеді + Жылдамдықты сырғыту сезімталдығы + Жоғары мәндер жылдамдық қадамына көбірек сырғытуды талап етеді + Жылдамдық қадамы + Жылдамдық аймағының биіктігі + Жылдамдық жест аймағының биіктігі плеер биіктігінің пайызы ретінде (5-75) + Жылдамдық + Сырғыту қабатының стилі + Көлденең қабат + Көлденең қабат (минималистік - жоғары) + Көлденең қабат (минималистік - орта) + Дөңгелек қабат + Дөңгелек қабат (минималистік) + Тігінен қабат + Тігінен қабат (минималистік) + Бейнелерді ауыстыру үшін сырғытуды қосу + Келесі немесе алдыңғы бейнеге ауысу үшін толық экранды режимде сырғытыңыз + Әрекет батырмалары + Портрет және толық экранды режимдерде қай әрекет түймелері пайда болатынын таңдаңыз + Портреттік батырмалар + \"Лайк\" пен \"Жазылу\" түймелерінің жарқырауын өшіру + Автор атап өткенде «Лайк» және «Жазылу» түймелерінің жарқырауын болдырмайды + Әрекеттер панелін жасыру + «Лайк» және «Лайк жоқ» түймелерін жасыру + «Бөлісу» түймесін жасыру + «Жарнаманы тоқтату» түймесін жасыру + Пікірлерді жасыру түймесі + Коннектеу бафвасындыры кегеритини + «Шағымдану» түймесін жасыру + «Ремикс» түймесін жасыру + Іукртеу Тусіру бафтироstарынын + Іукртеу Хайп бафтироstарынын + Іукртеу Нагававатын бафтироstарынын + \"Рахмет\" түймесін жасыру + \"Сұрау\" түймесін жасыру + Арнаны және Жазылу батырмасын жасыру + Әрекеттер жолағының басындағы арна аватарын және Жазылу батырмасын жасырады + Жазылу батырмасын жасыру + Әрекеттер жолағының басында канал аватарының жанындағы Жазылу және Қосылу батырмаларын жасырады + \"Клип\" түймесін жасыру + Бұл кейбір пайдаланушы аккаунттары үшін жұмыс істемеуі мүмкін + \"Дүкен\" түймесін жасыру + \"Сақтау\" түймесін жасыру + Тобылы бафвасындыры кегеритини + Толық экранды батырмалар + Ұнамады түймесін жасыру + Ұнады түймесін жасыру + \"Тікелей чат\" түймесін жасыру + Іукртеу Микс бафтироstарынын + Іукртеу \"Қобирек бейнекетер\" бафтироstарынын + Плейлист түймесін жасыру + Жылдам әрекеттердің жоғарғы жиегі + Жылдам әрекеттер жолағының жоғарғы жиегін орнатыңыз (0-32 dp) + Субтитрлер + Автоматты субтитрлерді және субтитр куки-файлдарын басқару + Автоматты субтитрлердің көрінуі + Әрқашан көрсету + Ешқашан көрсетпеу + Дыбыс қосылғанда көрсету + Дыбыс өшірілгенде көрсету + Субтитрлердің күндізгі тілі + Алдымен провайдер субтитрлерін таңдайды, содан кейін автоматты түрде таңдалған тілге аударады + Өшіру + Қосымша авто-аударма тілдерін қосу + Авто-аударма мәзіріне көбірек тілдерді қосады + Субтитрлер үшін cookie файлдарын орнату + "Cookie файлдарының жоқтығына байланысты автоматты аудару субтитрлері қолжетімді болмауы мүмкін YouTube стандартты қатесін жөндеу + +«Субтитр cookie-лері» көрсетілуі тиіс" + Субтитр кукилері + Timed Text API (Caption Data API) сұраныстарында орнатылатын cookie файлдары + Субтитр кукилерін алу + Cookie файлдарын алу үшін осы жерді басыңыз + Кукилерді алу мүмкін болмады + Cookies redan бар + Cookies сәтті жаңартылды + Транскриптті түзету + Транскрипт жүктелуіне кедергі келтіруі мүмкін алдын ала тексерудің сәтсіздігін жояды + Суреттегі сурет режимінде субтитрлерді көрсету + Навигациялық түймелер + Төменгі навигациялық панельде қай түймелер пайда болатынын таңдаңыз + Басты бет түймесін жасыру + Shorts түймесін жасыру + Жарату түймесін жасыру + Жазылымдар түймесін жасыру + Хабарламалар түймесін жасыру + Іздеу түймесін көрсету + Іздеу түймесінің орны + Баптаулар түймесін көрсету + Баптаулар түймесінің орны + Баптаулар батырмасының әрекеті + Қосылғанда Morphe баптауларын, ал өшірілгенде YouTube баптауларын ашады + \"Жасау\" түймесін \"Хабарландырулар\" түймесімен алмастыру + Егер бұл параметрді өзгерту әсер етпесе, Инкогнито режиміне ауысып көріңіз + Навигациялық түйме белгілерін жасыру + Жаңа мазмұн нүктесін жасыру + Навигациялық панельді жасыру + "Бұл навигациялық панельді, соның ішінде \"Сіз\" қойындысын толықтай жасырады. + +Баптауларға кіру үшін Жалпы → Құралдар панелі → Баптауларды көрсету арқылы Баптаулар түймесін қосыңыз" + Тар навигациялық батырмаларды қосу + Түймелер арасындағы көлденең арақашықтықты азайтады + Таңбалары жоқ навигациялық батырмаларды өшіру + Егер YouTube навигациялық батырмаларды таңбасыз жіберсе, оларды таңбаларымен көрсетеді + Құрал жолағының навигациялық қойындыларын өшіру + Егер YouTube оны Басты беттің жоғарғы қойындысына ауыстырса, навигациялық жолақта Жазылымдарды көрсетеді + Навигациялық панель анимацияларын қосу + Бөлімдер арасында ауысқанда өтпелі анимацияларды қосады + Навигациялық панельдің автоматты түрде жасырылуын өшіру + Скроллинг кезінде навигациялық панельді көрінетін күйде сақтайды + Навигациялық панель стилі + Әдепкі + Жартылай мөлдір + Мөлдір + Мөлдір емес + Бұл стиль сіздің құрылғыңызда немесе YouTube нұсқасында қолдау табылмауы мүмкін, сондай-ақ ол интерфейстің бұзылуына немесе қосымшаның жабылуына әкелуі мүмкін + Құралдар жолағы + Құрал жолағында қай компоненттер пайда болатынын таңдаңыз + Трансляция батырмасын жасыру + Чат батырмасын жасыру + Микрофон батырмасын жасыру + Іздеу батырмасын жасыру + Қосулы болғанда, бір рет басу Morphe параметрлерін, ал ұзақ басу YouTube параметрлерін ашады; ауыстыру үшін өшіріңіз + Ашылмалы мәзір + Жылдам мәзірде қай компоненттер пайда болатынын таңдаңыз + \"Қосымша параметрлер\" / Көбі мәзірін жасыру + \"Айнала режимі\" мәзірін жасыру + "\"Audio Track\" мәзірі жасырылған + +\"Audio Track\" мәзірін көрсету үшін \"Spoof video streams\" параметрін \"Android Studio\" және \"Android VR Downgraded\" клиенттерінен басқа кез келген клиентке өзгертіңіз" + «Аудио жолағы» мәзірін жасыру + «Аудио жолағы» мәзірінің төменгі бөлігін жасыру + Субтитрлер мәзірін жасыру + Субтитрлер мәзірінің төменгі бөлігін жасыру + Субтитрлер мәзірінің жоғарғы бөлігін жасыру + \"Көмек & кері байланыс\" мәзірін жасыру + \"YouTube Music арқылы тыңдау\" мәзірін жасыру + \"Экранды бұғаттау\" мәзірін жасыру + \"Бейнені қайталау\" мәзірін жасыру + \"Жолда\" мәзірін жасыру + Сапа мәзірін жасыру + Сапа мәзірінің төменгі бөлігін жасыру + Сапа мәзірінің жоғарғы бөлігін жасыру + \"Ұйықтау таймері\" мәзірін жасыру + \"Ойнату жылдамдығы\" мәзірін жасыру + \"Тұрақты дыбыс деңгейі\" мәзірін жасыру + \"VR-де көру\" мәзірін жасыру + Автоойнату алдын ала қарауын жасыру + Автоойнату басталғанға дейін толық экрандағы келесі бейнебаян алдын ала қарауын жасырады + Үстінді батырмалар + Бейнеплеердің үстінде қай түймелер пайда болатынын таңдаңыз + Автоойнату түймесін жасыру + Субтитрлер түймесін жасыру + Трансляция түймесін жасыру + Жинақтау түймесін жасыру + Толық экран түймесін жасыру + Плеер басқару элементтерінің фонын жасыру + Бейнеплеер басқару элементтерінің айналасындағы қою көлеңкелеуді жасырады + Басқару батырмалары фонының мөлдірлігі + Плеер басқару түймелері мен уақыт жолағының артындағы фонның мөлдірлігін реттеңіз (0 — көрінбейді, 100 — толық түсті) + Алдыңғы & Келесі түймелерін жасыру + Баптаулар түймесін жасыру + Аяқталу экранындағы карточкаларды жасыру + Ambient режимі + Ambient режимінің көрінуі мен қуат шектеулерін басқарыңыз + Ambient режимінің шектеулерін айналып өту + Батареяны үнемдеу режимінде Ambient режимінің белсенді болуына мүмкіндік береді + Ambient режимін өшіру + Толық экрандағы Ambient режимін өшіру + Ақпараттық карточкаларды жасыру + Сандардың айналу анимациясын өшіру + Лайктар мен қаралымдар санындағы санау анимациясын тоқтатады + Бейнеплеердің іздеу жолағын жасыру + Бейнеプレердің мініатюралы іздеу жолағын жасыру + Сіз көрген бейнелердің мұқабаларында көрсетілетін қарау прогресі жолағын жасыру + Толық экрандағы үлкен іздеу жолағын қосу + Shorts бөлімін жасыру + Арна бетінде жасыру + Басты бет лентасы & байланысты бейнебаяндарда жасыру + Іздеу нәтижелерінде жасыру + Жазылымдар таблосында жасыру + Бейне сипаттамасында жасыру + Көру тарихында жасыру + Shorts плееры + Shorts плеерінде қай компоненттер пайда болатынын таңдаңыз + Лайк қою үшін екі рет кетуді өшіру + AI батырмасын жасыру + \"Автоматты түрде дубляждалған\" белгісін жасыру + Арна жолағын жасыру + Пікірлер түймесін жасыру + Эффект батырмасын жасыру + Бейне сілтемесінің белгісін жасыру + Жест нұсқауларын жасыру + \"Жасыл экран» батырмасын жасыру + Хэштег батырмасын жасыру + Ақпараттық панельді жасыру + Қосылу батырмасын жасыру + Лайк түймесін жасыру + Лайк түймесінің фонтан анимациясын жасыру + Тікелей эфир алдын қарамасын жасыру + Орналасқан жер белгісін жасыру + Навигация жолағын жасыру + \'Жаңа жазбалар\' батырмасын жасыру + Кезекте күту қабаттамасының батырмаларын жасыру + Пікір алдын ала қарауын жасыру + Ремикс түймесін жасыру + \'Сақтау\' батырмасын жасыру + \'Музыканы сақтау\' батырмасын жасыру + Іздеу ұсыныстарын жасыру + Бөлісу түймесін жасыру + Дүкен батырмасын жасыру + Дыбыс түймесін жасыру + Дыбыс метадеректерінің белгісін жасыру + Жазылу түймесін жасыру + \"Super Thanks сатып алу\" батырмасын жасыру + Белгіленген өнімдерді жасыру + Алдымен түймесін жасыру + \'Осы дыбысты пайдалану\' батырмасын жасыру + \'Осы шаблонды пайдалану\' батырмасын жасыру + Бейне атауын жасыру + Аяқтау экранындағы ұсынылған бейнені жасыру + "Бейнеролик аяқталғаннан кейін пайда болатын ұсынылған бейнеқабатты жасырады + +Бұл параметр тек автоойнату өшірілген жағдайда жұмыс істейді. Оны YouTube-те өзгертуге болады: +Баптаулар → Ойнату (немесе Автоойнату) → Келесі бейнені автоойнату" + Ұқсас видеоларды жасыру + \'Көбірек видеолар\' қабатын жасыру + Толық экрандық режимде жоғары қарай сырғытқанда көрсетілетін \"Көбірек видеолар\" қабатын жасырады + Бейне уақытын жасыру + Плеердегі терезелі панельдерді өшіру + Плейлист пен тікелей чат панельдерінің автоматты түрде ашылуын өшіреді + Плейлисттің автоматты түрде ойнатылуын өшіру + Плейлистердің келесі видеоға автоматты түрде ауысуын болдырмайды + \"Көруді жалғастыру?\" сұранысын өшіру + Бейне аяқталғанда толық экран режимінен шығу + Өшірілген + Тек портрет + Текканактыганак ганап + Портрет және ландшафт + Толық экранда альбомдық бағытты мәжбүрлеу + Толық экранға өткенде плеерді альбомдық бағытқа бұру. Толық экранды альбомдық бағытта ашатын кішкентай экрандарда қолжетімді емес + Толық экранды жестер + Толық экран режиміндегі плеер қимылдары + Портреттік жоғары сырғыту жестін өшіру + Портреттік төмен сырғыту жестін өшіру + Көлденең бағытта төмен қарай сырғыту қимылын өшіру + Жоғартқыш-кеңейту қимылын өшіру + Жқындату үшін қисаюды қалпына келтіру + Бейнелерді толық экран режимінде ашу + Өшірілген + Портрет + Көлденең + Толтырылған экрандағы бейне масштабы + Әдепті + Созу (экранды толтыру) + Зум (толтыру үшін қию) + Бейне масштабы түймесін көрсету + Әдепті, Созу және Зум режиmleri арасында ауысу үшін түртіңіз + Тек толық экранда көрсету + Портретті ойнатқышта бейне масштабы батырмасын жасыру + Дыбысты өшіру түймесін көрсету + Плеер қабатының мөлдірлігі + Манивдилик апаренттилик денгейи (0 – маганган, 100 – акпакын) + Іздеу жолағы + Өшірілген + Кең + Өте кең + Құрылғының макеті + Әдепті + Телефон + Планшет + Автокөлік + "Өзгерістерге мыналар кіреді: + +Планшеттің макеті +• Қоғамдастық жазбалары жасырылған +• Таспаларда ойнату параметрі қолжетімді емес +• Shorts-та Remix түймесі мен Дыбыс түймесі қолжетімді емес +• \"Көбірек бейнебаяндар\" қабаты қолжетімді болмауы мүмкін +• Бейнебаяндардың жинақталмаған ескі әрекеттер жолағы көрсетіледі +• Жылдам мәзір элементтері дұрыс жұмыс істемеуі мүмкін + +Автокөлік макеті +• Таспа тақырыптар мен арналар бойынша ұйымдастырылған +• Таспаларда ойнату параметрі қолжетімді емес +• Shorts-та Remix түймесі мен Дыбыс түймесі қолжетімді емес +• \"Көбірек бейнебаяндар\" қабаты қолжетімді болмауы мүмкін +• Shorts әдеттегі плеерде ашылады +• Бейнебаяндардың жинақталмаған ескі әрекеттер жолағы көрсетіледі" + Плеерде планшеттің макетін қосу + "Плеердің ескі макетін қалпына келтіреді + +Шектеу: Егер \"Ескі бейне әрекеттер жолағын қалпына келтіру\" өшірілсе, \"Көбірек бейнелер\" қабаты қолжетімді болмауы мүмкін" + 21.11.48 - Субтитрлердің ескі жұмыс істеу принципін қалпына келтіру + 20.39.41 - Ескі сақталымды плейлистке қалпына келтіру + 20.28.41 - Қалың белгішелерді өшіру + 20.13.41 - Видео плеерінің астындағы арна атауын қалпына келтіру + Жүйелік бөлісу парағын ашу + Мазмунда былгирензен контенттини былгирензен Қонтенттинини орнени Қолламадан орнени + YouTube Music батырмаларын алмастыру + Стандартты YouTube Music қолданбасының орнына сіз таңдаған музыка қолданбасын ашады + Музыка қолданбасының пакеттік атауы + Орнатылған музыка қолданбасыңыздың пакеттік атауы + Бастабшы бетс + Әдепті + Барлық жазылымдар + Арналарды қарау + Курстар / Оқу + Зерттеу + Сән & Сұлулық + Гейминг + Тарих + Кітапхана + Ұнаған видеолар + Тікелей эфир + Фильмдер + Музыка + Жаңалықтар + Хабарламалар + Плейлисттер + Іздеу + Сауда + Shorts + Спорт + Жазылымдар + Трендтегілер + Виртуалды шындық + Кейінірек көру + Сіздің клиптеріңіз + Іске қосу кезінде жалғастыруды өшіру + Shorts-ты мынамен ашу + Shorts плеері + Әдеттегі плеер + Әдеттегі плеердің толық экранды режимі + Shorts видеоларын автоматты түрде ойнату + Ағымдағыны қайталаудың орнына келесі Short-қа автоматты түрде ауысады + Фонда автоматты түрде ойнату + Фондық ойнату кезінде келесі Short-қа автоматты түрде ауысады + Тікелей Shorts бейнелерін өткізіп жіберу + Shorts табинде пайда болатын тікелей эфирлерді өткізіп жібереді + Ленталарда ойнату + Әрқашан қосу + Тек Wi-Fi + Өшіру + Түпнұсқа тақырыптарды қалпына келтіру + Автоматты түрде аударылған мәтіндердің орнына бейнебаянның түпнұсқа атауларын, сипаттамаларын және арна сипаттамаларын көрсетеді + Тақырып жүктелуде… + Таغذының автоматты жаңартылуын өшіру + Таغذылар ескіргеннен кейін олардың автоматты түрде жаңартылуын болдырмайды + Лента картасының макеті + Классикалық + Карточкалар + Лайк, пікір және сақтау батырмалары бар карточкалар + Пікірлер + Миниплеердің қайта басталуын өшіру + Қосымша іске қосылғанда пайда болатын YouTube Premium мүмкіндігі — «Көруді жалғастыру» миниплеерін өшіреді + Шағын плеер + Бапталған бейнеойнатқыштың сыртқы түрін және жұмысын баптаңыз + Шағын плеер түрі + Тақырып пен арнаны жасыру + Өшірілген + Әдепкі + Минималистік + Минималистік 2 + Планшет + Заманауи 1 + Заманауи 2 + Заманауи 3 + Заманауи 4 + Дөңгелек бұрыштарды өшіру + Сүйретуді өшіру + Бапталған ойнатқышты экранның әртүрлі бұрыштарына сүйретуді болдырмайды + Көлденең сүйрету қимылын өшіру + Миниплеерді экраннан көлденеңінен сырғызу арқылы жабуды болдырмайды + Көлденең сүйреу арқылы ойнатуды тоқтату + Миниплеер экраннан сыртқа сүйретілгенде ойнату тоқтатылады + Сүйреу күйінің позициясын есте сақтау + Миниплеерді экраннан сыртқа сүйреу оның UI күйін сақтайды + Қабат батырмаларын жасыру + Әдепкі өлшемі + Экрандағы бастапқы өлшемді пиксельдермен орнатыңыз + Пиксель өлшемі %1$s пен %2$s арасында болуы керек + Қабат мөлдірлігі + Мөлдірлік деңгейін реттеңіз (0 — мөлдір, 100 — мөлдір емес) + Артқа батырма таспадан шығарады + Артқа батырманы басу таспаның жоғарғы жағына жылжытудың орнына әрдайым қолданбадан шығарады + Анимацияларды мәжбүрлеу + Әдепті + Әрқашан қосу + Әрқашан өшіру + Мәзір жолағын жасыру + Жүйелік мәзір жолағын жасырады. Оны сәтке көрсету үшін жоғарғы жиектен төмен қарай сырғызыңыз + Мәзір жолақтарын жауып тұрған мәртебе жолағын түзету + Мәртебе жолағының макетін біркелкі сақтайды, сонда ол жоғарғы жолақтарды жаппайды және плеердің үстінде бос орын қалдырмайды. Егер қолданба сбой берсе немесе жоғарғы жолақтар дұрыс көрінбесе, бұл функцияны өшіріңіз + Жылтыр + Жалтыраған кіріс экраны бұл құрылғыда қолдауылмайды + Жарық тақырыптың фоны + Ақ + Material You (Ақ) + Ақшыл қызғылт + Ашық көгілдір + Ашық жасыл + Ашық сары + Ашық қызғылт сары + Ашық қызыл + Жалғастырылған ашық фон + Ашық тақырыптың фон түсін орнатыңыз + Алдыңғы жоспар түсін өзгерту + Мәтін мен белгішелер үшін тақырып түсін қолдану + Градиентті жүктеу экранын қосу + Жүктеу экранында біртекті түстің орнына градиентті фонды көрсетеді + Seekbar-дің жеке түсін қосу + Іздеу жолағының түсі + Бейне прогресс жолағының негізгі түсін орнатыңыз + Seekbar акценттік түсі + Бейне прогресс жолағының қосалқы түсін орнатыңыз + Seekbar түсінің жарамсыз мәні + Түпнұсқа аналог + Түпнұсқа торт + Түпнұсқа металл + Тақырып логотипі + Әдепкі + Кәдімгі + Жетістірілген + Кескіндердің аймақтық шектеулерін айналып өту + Шектеу қойылған елдердегі жоқ кескіндерді түзету үшін превью сұраныстарын баламалы серверге бағыттайды + DeArrow тақырыптары + Басты бет қосымшасы + Жазылымдар қосымшасы + Сіз қосымшасы + Плеер плейлистері & ұсыныстары + Іздеу нәтижелері + DeArrow белгішесін көрсету + Көпшіліктің қатысуымен жасалған бейнебаяндардың тақырыптарында DeArrow белгішесін көрсетеді + Еркін режим + Егер DeArrow пайдаланушылары түпнұсқа тақырыпты ұнатты деп дауыстаса, DeArrow тақырыбының орнына түпнұсқа тақырыпты көрсетеді + Түпнұсқа атауды көрсету үшін қажетті минималды еркін дауыстар саны + Түпнұсқа атауды көрсету үшін бір категориядағы, мысалы, күлкілі немесе шығармашылық, қажетті еркін дауыстар саны + DeArrow мұқиялары + Түпнұсқан縮бэсттер + DeArrow & түпнұсқа мұқабалары + DeArrow & тоқтатылған кадрлар + Тұрақты кадрлар + "DeArrow кликбейт тақырыптар мен мұқабаларды дәлірек және аз сенсациялы, көпшіліктің күшімен жасалған нұсқалармен алмастырады. Тақырыптар қауымдастық тарапынан жазылады және оларға дауыс беріледі, ал мұқабалар бейнеден алынған кадрлар болып табылады + +Бейнелер хэштелген бейне ID-інің қысқа префиксі арқылы сұратылады, сондықтан сервер сіздің нақты қай бейнені қарап отырғаныңызды біле алмайды. Егер бейненің DeArrow мұқабасы болмаса, түпнұсқа немесе тоқтатылған кадр көрсетіледі" + Байланыс қатесі туралы ескертулерді көрсету + DeArrow сервері қолжетімсіз болғанда хабарламаны көрсетеді + API нүктесі + DeArrow макеттері кэші нүктесінің URL-мекенжайы + Бейнеойнатудың тұрақты кадрлары + Статикалық кадрлар әлі де әр бейнебаянның басынан / ортасынан / соңынан алынады. Бұл кескіндер YouTube жүйесіне енгізілген және ешқандай сыртқы API қолданылмайды + Бейне кадрын алу уақыты + Басы + Ортасы + Соңы + DeArrow уақытша қолжетімсіз (мәртебе коды: %s) + DeArrow уақытша қолжетімсіз + Плеер батырмаларының ескі стилін қалпына келтіру + Бейнеойнатқыштың жуан басқару элементтерін ескі, жіңішке визуалды стильге қайтарады + Құрылғы өлшемдерін имитациялау + Мұны қосу видеоның жоғарырақ сапасын бұғаттан шығаруы мүмкін + Бұл функцияны қосу бейне қойылғанда кебуге, батарея қуатының тезірек таусылуына және белгісіз жанама әсерлерге әкелуі мүмкін + Тактильді кері байланыс + Құрылғы дірілін қай әрекеттер тудыратынын таңдаңыз + Бөлімдердің тактильді кері байланысын өшіру + Дәл іздеу тактильді кері байланысын өшіру + Іздеуді артқа қайтару тактильді кері байланысын өшіру + Басып тұру тактильді кері байланысын өшіру + Жобартқыштың тактильді кері байланысын өшіру + Егер сіз жақында аккаунт кіріс деректерін өзгерткен болсаңыз, онда MicroG қолданбасын жойып, қайта орнатыңыз + Сілтеме қайта жо向けлтілімдерін айналып өту + Бақылау сілтемесін (youtube.com/redirect) айналып өтіп, түпнұсқа сілтемені тікелей ашады + Сілтемелерді браузерде ашу + Сілтемелерді қолданба ішіндегі браузердің орнына сыртқы браузерде ашады + Ойнату буферінің өлшемі + Әдепкі + Төмен + Орташа + Максималды + Бейнекенің кешіктірілуі + Бейнекені Bluetooth құлаққаптары сияқты кешігуі бар аудиомен синхронды ұстау үшін кешіктіреді (0-5000 мс) + Тек Bluetooth аудиосымен кешіктіру + Бейнекені тек Bluetooth аудиосы қосылған кезде кешіктіреді, осылайша телефон динамигі синхронды қалады + Бейне кешіктіру түймесін көрсету + Кешіктіруді 0 мс-ке қайтару үшін басып тұрыңыз + Бейне кешіктіру мәзірін көрсету + Кешіктіру миллисекундпен + Автоматты + Бейне сапасын есте сақтау + Сіз қолмен таңдаған сапаны барлық болашақ бейнелерге қолданады + Сапаның өзгергені туралы хабарламалар + Жаңа әдепкі сапа сақталғанда хабарландыру көрсетеді + Wi-Fi желісіндегі бейне сапасының әдепкі мәні + Мобильді желідегі бейне сапасының әдепкі мәні + Shorts сапасын есте сақтау + Сіз қолмен таңдаған сапаны барлық болашақ Shorts видеоларына қолданады + Wi-Fi желісіндегі Shorts сапасының әдепті мәні + Мобильді желідегі Shorts сапасының әдепті мәні + Әдемі мобильді сапасы мынаған өзгертілді: %s + Әдемі Wi-Fi сапасы мынаған өзгертілді: %s + Shorts мобильді сапасы мынаған өзгертілді: %s + Shorts Wi-Fi сапасы %s мәніне өзгертілді + Ойнату жылдамдығы батырмасын көрсету + Жылдамдықты әдепкі мәнге қайтару үшін басып тұрыңыз + Сапа батырмасын көрсету + Сапаны әдепкі мәнге қайтару үшін басып тұрыңыз + Сапа батырмасында рұқсатты көрсету + FHD сияқты белгінің орнына 1080 сияқты рұқсатты көрсету + Ойнату жылдамдығының жеке мәзірі + Ойнату жылдамдығының ескі мәзірін қалпына келтіру + Ойнатудың жеке жылдамдықтары + Өзіңізге ыңғайлы ойнату жылдамдығын енгізіңіз + Жеке ойнату жылдамдығы %s мәнінен аз болуы керек + Ойнатудың жарамсыз жеке жылдамдықтары + Авто + Ойнату жылдамдығын басып тұру + Экранды басып тұрғандағы жылдамдық көбейткішін орнатыңыз, өшіру үшін 0 мәнін қойыңыз + Ойнату жылдамдығын есте сақтау + Қолмен таңдалған жылдамдықты барлық келесі бейнелерге қолданады + Жылдамдықтың өзгергені туралы хабарламалар + Жаңа әдепкі ойнату жылдамдығы сақталғанда хабарландыру көрсетеді + Ойнатудың әдепкі жылдамдығы + Shorts-тағы әдепкі ойнату жылдамдығы + Shorts-тағы ойнату жылдамдығын есте сақтау + Сіздің Shorts жылдамдығын қолмен таңдауыңыз алдағы барлық Shorts-қа қолданылады. Өшірілген болса, Shorts плеерін жапқаннан кейін жылдамдық әдепкі мәнге қайтады + Shorts мәзірінде ойнату жылдамдығын көрсету + Shorts плеерінің мәзіріне ойнату жылдамдығы тармағын қосады + Ойнату жылдамдығы · %s + Әдемі ойнату жылдамдығы өзгертілді: %s + Музыка үшін қалыпты жылдамдықты қолдану + Музыкалық бейнероликтерді әрдайым 1.0x жылдамдығында ойнату + Арналар жылдамдығының әккі тізімі + Әккі тізіміндегі арналардың бейнелері әрдайым 1.0x жылдамдықпен ойнатылады + Арнаны жылдамдықтардың әккі тізіміне қосу + Арнаны жылдамдықтардың әккі тізімінен алып тастау + Аудио тон басқаруын қосу + Ойнатудың әдепкі аудио тоны + Әдепкі аудио тон келесі мәнге өзгертілді: %s + Аудио уақытын созу + Жылдамдықты және тонды тәуелсіз баптауға мүмкіндік береді, бұл аудио артефакттарына әкелуі мүмкін + Тікелей эфирді ойнату позициясын есте сақтау + Жалғасып жатқан тікелей эфирлер үшін ағынның ағымдағы нүктесіне өтудің орнына ойнату позициясын есте сақтайды және сол жерден жалғастырады + Бұл функция 24/7 тікелей эфирлермен және басқа да \"әрқашан қосулы\" эфирлермен жұмыс істемейді + HDR бейнесін өшіру + HDR бейнесін мәжбүрлеу + Қолданбаға бұл құрылғыны HDR-ге қабілетті деп тануды мәжбүрлейді + "HDR декодтау жұмыс істеп, бірақ YouTube HDR-ді ұсынбаған жағдайда қолданыңыз, мысалы, HLG-ні жасыратын кейбір HyperOS құрылғыларында. + +Шектеулері: +• Егер дисплей HDR-ді шығара алмаса, түстер дұрыс көрінбеуі мүмкін +• Егер декодер HDR-ді өңдей алмаса, ойнату сәтсіз аяқталуы мүмкін +• HDR әлі де бейне ағындарын имитациялайтын клиентке байланысты" + Бейнекодек + Әдепкі + AVC-ны күштеу үшін «Бейне ағындарын имитациялау» параметрін «Android Studio», «Android VR» немесе «visionOS» мәніне өзгертіңіз + "VP9 +• Батарея қуатының ұзақтығын жақсартып, ойнату кезіндегі секіруді жоя алады +• AV1-ге қарағанда көбірек интернет деректерін пайдаланады + +AVC (H.264) +• «Android Studio», «Android VR» немесе «visionOS» бейне ағынын имитациялау клиентін қажет етеді +• Батарея қуатының ұзақтығын бұдан да жақсарта алады +• Ескі құрылғыларда жетіспейтін бейне ажыратымдылығын қалпына келтіре алады +• Максималды ажыратымдылық — 1080p +• HDR бейнелері AVC-ны пайдаланбауы мүмкін +• Кейбір құрылғыларда AVC-ны күштеу мүмкін емес" + Кеңейтілген сапа мәзірін ашу + Сапаны таңдау мәзірін аттап өтіп, Кеңейтілген сапа мәзінін тікелей ашады + Premium сапа параметрлерін жасыру + Бейне сапасына басымдық беру + Кодек тиімділігінен гөрі қолжетімді ең жоғары бейне сапасына басымдық береді + Егер бұл параметр өшірілсе, ескі бейнелерде сапа таңдау мүмкіндіктері көрсетілмеуі мүмкін + \'Android VR\' / visionOS AV1 рұқсат беру + \'Android VR\' / visionOS клиентін қолданғанда AV1 бағдарламалық декодтауға рұқсат береді + "Бұл параметрді қосу бағдарламалық AV1 декодтауды пайдалануы мүмкін. + +AV1-мен бейнеойнату кезінде секірулер болуы немесе кадрлар түсіп қалуы мүмкін" + Спуфингтің жанама әсерлері + • Ойнату 1 минуттан кейін тоқтауы мүмкін + • \'Аудио жолағы\' мәзірі жоқ + • AV1 бейне кодегі жоқ + • 360° VR иммерсивті режимі қолжетімді емес + • Тұрақты дыбыс деңгейі қолжетімді емес + • Ойнату 1 секунд ерте аяқталуы мүмкін + • Деобфускация сәтсіз аяқталса, ойнату істемеуі мүмкін + • Түпнұсқа аудионы күштеу қолжетімді емес + • Android VR-ге кіру талап етіледі + • Сапа 720p-мен шектелуі мүмкін + Плеердің JavaScript нұсқасы + \'Нердтерге арналған статистикада\' имитациялауды көрсету + Видеонининининие ескизги видео акцияларыныны + "Бейнебаян әрекет түймелерін жасыру мүмкін еместігі түзетілді + +Шектеу: Ескі стильдегі \"Көбірек бейнебаяндар\" қабатына қайтады" + Плеер белгішесінің стилі + Автоматты + Әдепті + Жіңішке + Қалың + Custom + Shorts белгішесінің стилі + Серияларды бақылау + Плейлисттерді серия ретінде бақылаңыз және өз орныңызды сақтаңыз + Серияларды бақылауды қосу + Тарихқа «Сериялар» бетін және плейлисттерге «Серияны бақылау» мүмкіндігін қосады + Навигациялық жолақта серияларды көрсету + «Серияларды бақылау» батырмасын көрсету + Ағымдағы бейненің сериясын бақылау немесе ашу үшін плеер батырмасын қосады + Бақыланатын сериялардың прогресін жазу + Сақталған сериялар мен белгілер үшін осы телефонда ойнатуды жазу + Бас тарту + Сақтау + Белгіні сақтау + Серия атауы + Серия параметрлері + Кітапхана параметрлері + Эпизодтарды жаңарту + Қайта атау + Плейлистті YouTube-те ашу + Кітапханадан жою + Плейлист сілтемесі немесе ID + Барлық жергілікті прогресті тазалау + Тарихты тазалау + Барлық жергілікті прогресті тазалау керек пе? + Бұл әрбір бейне үшін позицияларды, қаралғандық белгілеулерді және жалғастыру таңдауларын тазартады. Сақталған сериялар мен олардың каталогтары қалады. Егер жазу қосылған болса, жаңа ойнату жазылады + Көрдім деп белгілеу + Көрген жоқпын деп белгілеу + Автоматты мәзінді қолдану + 0:00-ден қайта бастау + 0:00-ден қайта ойнату + Мәзінд сақталды + Сақтауды қайта försöну + Алдыңғы 100 + Келесі 100 + Сақталған бейнені ашу + Жергілікті прогресс тазартылды + Серияларыңызды көру үшін «Сіз» бөлімінен Тарихты ашыңыз + Көру тарихы + Сериялар + Сақталған сериялар жүктелуде… + Эпизодтарды бақылау үшін плейлистке жазылыңыз + Плейлист белгісі + Эпизодтар жаңартылуда… + Барлық сериялар + Көрілді + Эпизодтар жүктелуде… + Көбірек опциялар + "Прогресті тек сақталған серияларыңыз бен белгіленген бейнелер үшін жазыңыз + +Бұл кітапхана осы құрылғыда сақталады және YouTube аккаунттары арасында бөліседі. YouTube-тің тарихты тоқтату және тазалау бағдарлаулары оны өзгертпейді. Оны өшіру үшін Сериялар → Көбірек → Жергілікті прогресті тазалау функциясын қолданыңыз + +Автоматты жазу инкогнито режимінде, аккаунт деректері қолжетімсіз болғанда немесе басқа аккаунтқа ауысқанда тоқтайды. Оны сол аккаунтпен қолдану үшін бақылауды қайта қосыңыз" + Бақылауды инкогнито режимінде немесе YouTube ағымдағы аккаунт күйін жүктегенге дейін қосу мүмкін емес. Басты бетке оралып, қайтадан байқап көріңіз. Кіру талап етілмейді + Жергілікті бақылауды қосу + Прогресті жазу өшірілген + %1$s кезінде жалғастыру + %1$s / %2$s эпизод қаралды + %1$s-эпизод · %2$s + %3$s сериясының %1$s-%2$s бөлімдері + Бөлімдерді жүктеуді қайта көру үшін жаңартыңыз. Жеке плейлисттерге аккаунтқа кіру қажет; олардың белгілемесі YouTube-те әлі де ашылады + YouTube бұл бөлімді аша алмады + YouTube бұл плейлистті аша алмады + Эпизодты таңдаңыз + Жаңартылған + Ойнатылатын эпизодтар жоқ + Сақталған видео · %1$s + Сериалды бақылау + Сериалды қарау + Жалғастыру + Осы жерден бастау + Басталу нүктесі сақталды + Осы жерге дейін қаралды + Эпизодтар қаралды деп белгіланды + Кері алу + Қосу + %1$s жаңа бөлім + Прогресті осы құрылғыда жазу + "Сіздің серияларыңыз бен прогресіңіз осы құрылғыда сақталады және YouTube аккаунттары арасында бөліседі. YouTube тарихының басқару құралдары оларды өзгертпейді. Прогресті «Сериялар» мәзірінен тазалаңыз. + +Автоматты жазу қосымша функция болып табылады. Ол инкогнито режимінде немесе аккаунт өзгергенде тоқтайды" + Бөлімдерді жүктеу мүмкін болмады. Плейлистті белгілеме ретінде сақтай аласыз + Сақталмаған өзгерістер тым көп. Алдымен қайта сақтауды көріңіз + Сақтау процесі жүріп жатыр. Қайта көріңіз + Эпизодты жүктеу процесі жүріп жатыр. Қайта көріңіз + Жад қолжетімді емес. Сақталған деректеріңіз сақталды + Өзгерістерді сақтау мүмкін болмады. YouTube-ті жаппас бұрын қайта сақтап көріңіз + Бұл сериал сақталған немесе оның плейлисті өзгерген. Оны қайта ашыңыз + Бұл сериал өзгертілді немесе жойылды. Кітапхананы қайта ашыңыз + Плейлист өзгерді. Эпизодты қайта таңдаңыз + Бұл эпизод қолжетімсіз. Басқасын таңдаңыз + Сериал атауын енгізіңіз + YouTube плейлистінің сілтемесін немесе плейлист ID-ін енгізіңіз + Қайта жаңарту алдында күтіңіз + Эпизодтарды жүктеу мүмкін болмады. Кейінірек қайталап көріңіз + Өзгерісті аяқтау мүмкін болмады. Қайта försöңіз + Жалғастыруды растау мүмкін болмады. Плеерді тексеріп, қайта försöңіз + Бақыланатын сериялар + Бұл бейне осы плейлисте жоқ + Плейлисттер ізделіп жатыр… + Плейлист сілтемесін қойыңыз + Плейлист табылмады + Епизодтар қолжетімді емес + Көрілгендерді жасыру + Алдыңғы эпизодтарды көрілген деп белгілеу + Басталуы: %1$s + Эпизодтар реті + Плейлист реті + Плейлист ретін кері айналдыру + %1$s қаралым + %1$s қаралым + Эпизодтарды қаралды деп белгілеу + Кеміңі %1$s%% қаралды, сонымен қатар 90 секундтан аз уақыт қалды немесе %2$s секунд қалды + Кез келген шекті мәнге жеткенде серияны автоматты түрде көрілді деп белгілеу. Пайыздық ереже бойынша соңына 90 секундтан көп уақыт қалмауы тиіс, сонда ұзақ сериялардың соңы өткізіліп кетпейді. Қалған уақыт ережесі бір минуттан ұзақ бейнебаяндарға қолданылады; оны өшіру үшін 0 мәнін орнатыңыз. Өзгерістер болашақ прогресс жаңартуларына қолданылады. Қолмен белгіленген көрілді/көрілмеді таңдаулары сақталады + Көрілген пайызы (1-100) + Қалған секундтар (0-300; 0 өшіреді) + %1$s пен %2$s аралығындағы мәнді енгізіңіз + YouTube прогресін қолдану + Басқа құрылғыда қарағаннан кейін бақыланатын сериалды жалғастыру + Ағымдағы YouTube аккаунтынан бақылап отырған серияларыңыздың қаралған мәртебесін және тоқтатылған позицияларын оқыңыз. Тек сәйкес келетін сериялар осы телефонда сақталады. Тексерулер сериялар кітапханасы ашық кезде және «Жалғастыру» батырмасынан бұрын орындалады. Басқа құрылғыңызда YouTube тарихы қосулы болуы керек + YouTube прогресі тексерілуде… + YouTube прогресі қолжетімсіз. Сақталған прогрес қолданылуда + YouTube прогресінің кейбірі қолжетімсіз. Сақталған позициялар сақталады + Резервтік көшірме жасау және қалпына келтіру + Сериалдар кітапханасын файлға сақтаңыз немесе резервтік көшірмені қалпына келтіріңіз + Бақылап отырған серияларды, серия позицияларын және қаралған таңдауларды сақтайды. Жазу және YouTube прогресіне рұқсаттар қосылған жоқ + Резервтік көшірмені сақтау + Резервтік көшірмені қалпына келтіру + %1$s серия қосылды. Кітапханаңыздағы %2$s серия сақталады. Қолданды видео прогресі сақталады + Сериалдың резервтік көшірмесі сақталды + %1$s серия қалпына келтірілді + Бұл файл қолдау көрсетілмейтін серия резервтік көшірмесі немесе тым үлкен. Кітапханаңыз өзгертілмеді + Резервтік көшірмені сақтау мүмкін болмады. Басқа орнын таңдап көріңіз + Сериал жойылды + Бұл серия енді бірнеше рет көрсетіледі. Қайсысынан жалғастыруды таңдаңыз + Сіз сақтаған серия енді бұл плейлистте жоқ. Сақталған позицияңыз сақталды + %1$s · %2$s сериясынан жалғастыру + Сондай-ақ төмендегі кез келген серияны таңдай аласыз. Алдыңғы сериялар өзгертілмейді + Бұл плейлист өзгертілді. Қай жерден жалғастыруды таңдау үшін серияны ашыңыз + Бұл бағытта бөлімдер қалмады + %1$s жалғастыру + Сериалдарды іздеу + Іздеуді тазалау + Сериалдар табылмады + Атауы жоқ бөлім + Қолжетімсіз бөлім + Niconico пікірлері + Тікелей чат пен чат қайталануын бейнебаян бойымен жылжитын пікірлер ретінде көрсету + Бейнеойнатқышта қабат батырмасын көрсетеді. Пікірлерді көрсету немесе жасыру үшін түртіңіз, жылдам параметрлер үшін басып тұрыңыз + Пікір қаріп өлшемі + Пікірлер мәтінінің өлшемі (12-40 sp) + Пікір мөлдірлігі + Пікір мәтіні мен фондарының мөлдірлігі + Жылжу ұзақтығы + Пікірлердің экранды кесіп өтуіне кететін секундтар саны. Төменгі мәндер жылдамырақ қозғалады + Пікірдің максималды ұзындығы + Таңдалған санынан көп таңбасы бар пікірлерді жасырады. Әр эмодзи бір таңба ретінде есептеледі + Шектеу жоқ diff --git a/patches/src/main/resources/addresources/values-km-rKH/music/strings.xml b/patches/src/main/resources/addresources/values-km-rKH/music/strings.xml index 41f12e10e6..ccfea4ed71 100644 --- a/patches/src/main/resources/addresources/values-km-rKH/music/strings.xml +++ b/patches/src/main/resources/addresources/values-km-rKH/music/strings.xml @@ -18,11 +18,8 @@ Second \"item\" text" - បោះបង់ - ព័ត៌មានលម្អិត - ផ្ទាល់ខ្លួន @@ -30,15 +27,7 @@ Second \"item\" text" - អំពីកម្មវិធីនេះ - ពាណិជ្ជកម្ម - ព័ត៌មាន - ជាទូទៅ - ការីចាក់ - ផ្សេងៗទៀត - លាក់ស្លាក “ទទួលយក Music Premium” - លាក់វីដេអូពាណិជ្ជកម្មតាមវីដេអូ @@ -60,19 +49,15 @@ Second \"item\" text" - ផ្លាស់ប្តូរពណ៌ការីចាក់ខ្នាតតូច - របាររុករក - លាក់របាររុករក - លាក់ស្លាកប៊ូតុងរុករក @@ -85,6 +70,5 @@ Second \"item\" text" - ស្វ័យប្រវត្តិ diff --git a/patches/src/main/resources/addresources/values-km-rKH/reddit/strings.xml b/patches/src/main/resources/addresources/values-km-rKH/reddit/strings.xml index 1946b2cc77..cf7776fdaf 100644 --- a/patches/src/main/resources/addresources/values-km-rKH/reddit/strings.xml +++ b/patches/src/main/resources/addresources/values-km-rKH/reddit/strings.xml @@ -15,12 +15,4 @@ Second \"item\" text" --> - - ពាណិជ្ជកម្ម - របាររុករក - ផ្សេងៗទៀត - អំពីកម្មវិធីនេះ - រក្សាទុក - កំណត់សាថ្មី - អាយខនកម្មវិធី - + diff --git a/patches/src/main/resources/addresources/values-km-rKH/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-km-rKH/shared-youtube/strings.xml index d88f93dad4..77635ba31c 100644 --- a/patches/src/main/resources/addresources/values-km-rKH/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-km-rKH/shared-youtube/strings.xml @@ -18,31 +18,20 @@ Second \"item\" text" --> - បានផ្ដាច់ + - ឈ្មោះកម្មវិធី - ផ្ទាល់ខ្លួន - ផ្ទាល់ខ្លួន - អាយខនកម្មវិធី - ច្បាប់ដើម - Morphe ខ្មៅ - Morphe ងងឹត - Morphe ភ្លឺ - ផ្ទាល់ខ្លួន - កំណត់សម្គាល់ Morphe - អតិថូបករណ៍បុរេជម្រើស diff --git a/patches/src/main/resources/addresources/values-km-rKH/shared/strings.xml b/patches/src/main/resources/addresources/values-km-rKH/shared/strings.xml index e5865a9d54..31cdbe4dcb 100644 --- a/patches/src/main/resources/addresources/values-km-rKH/shared/strings.xml +++ b/patches/src/main/resources/addresources/values-km-rKH/shared/strings.xml @@ -17,48 +17,16 @@ Second \"item\" text" --> - ការកំណត់ - តើអ្នកពិតជាចង់ដំណើរការមែនឬ? - រក្សាទុក - កំណត់សាថ្មី - កំណត់ពណ៌សាថ្មី - ពណ៌មិនត្រឹមត្រូវ - ត្រូវការចាប់ផ្តើមសាជាថ្មី - ចាប់ផ្តើមសាថ្មី - នាំចូល - ចម្លង - ការកំណត់របស់ Morphe ត្រូវស្ដារទៅបុរេជម្រើស - បាននាំចូលការកំណត់ %s - ការនាំចូលបានបរាជ័យ៖ %s - ស្វែងរកការកំណត់ - អំពីកម្មវិធីនេះ - គ្មានលទ្ធផលឡើយ សម្រាប់ \'%s\' - សាកល្បងពាក្យគន្លឹះដទៃ - ការស្វែងរកថ្មីៗ - ដកចេញពីប្រវត្តិស្វែងរកមែនឬដកចេញពីប្រវត្តិស្វែងរកមែន - ជម្រះប្រវត្តិស្វែងរក - ប្រវត្តិស្វែងរក ទទេរ - ផ្ទាល់ខ្លួន - ភាសាកម្មវិធី - នាំចូល / នាំចេញ - ការកំណត់នាំចូល / នាំចេញរបស់ Morphe - បញ្ជាប់ផ្លូវការ - វេបសាយ - ការបកប្រែ - ការបំបាត់កំហុស - បិទ - បើក - ដកចេញ diff --git a/patches/src/main/resources/addresources/values-km-rKH/sponsorblock/strings.xml b/patches/src/main/resources/addresources/values-km-rKH/sponsorblock/strings.xml index 84ae48349f..9d2d4ab7d5 100644 --- a/patches/src/main/resources/addresources/values-km-rKH/sponsorblock/strings.xml +++ b/patches/src/main/resources/addresources/values-km-rKH/sponsorblock/strings.xml @@ -19,5 +19,4 @@ Second \"item\" text" - %s វិនាទី diff --git a/patches/src/main/resources/addresources/values-km-rKH/youtube/strings.xml b/patches/src/main/resources/addresources/values-km-rKH/youtube/strings.xml index 3b8e359e4e..4ec76b1150 100644 --- a/patches/src/main/resources/addresources/values-km-rKH/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-km-rKH/youtube/strings.xml @@ -18,22 +18,10 @@ Second \"item\" text" - អំពីកម្មវិធីនេះ - ពាណិជ្ជកម្ម - ព័ត៌មាន - ជាទូទៅ - ការីចាក់ - ផ្សេងៗទៀត - វីដេអូ - បិទ - លាក់កាតអាល់ប៊ុម - លាក់កាតសិល្បករ - លាក់សារបង្ហោះសហគមន៍ - លាក់ផ្ទាំងបដាតូចៗ + @@ -66,7 +55,6 @@ Second \"item\" text" - ដកចេញ @@ -84,12 +72,8 @@ Second \"item\" text" - វីដេអូ - ដោនឡូដ - ផ្ទាល់ខ្លួន - ប្រព័ន្ធ @@ -97,7 +81,6 @@ Second \"item\" text" - ដកចេញ @@ -109,8 +92,6 @@ Second \"item\" text" - ដើម - បិទ @@ -133,7 +114,6 @@ Second \"item\" text" - បិទ @@ -143,7 +123,6 @@ Second \"item\" text" - លាក់របាររុករក @@ -188,12 +167,10 @@ Second \"item\" text" - បានផ្ដាច់ - បានផ្ដាច់ @@ -205,7 +182,6 @@ Second \"item\" text" - បិទ @@ -217,7 +193,6 @@ Second \"item\" text" - ផ្ទាល់ខ្លួន @@ -251,12 +226,6 @@ Second \"item\" text" - ស្វ័យប្រវត្តិ - ផ្ទាល់ខ្លួន - បោះបង់ - រក្សាទុក - ប្ដូរឈ្មោះ - អាន់ឌូ diff --git a/patches/src/main/resources/addresources/values-kmr-rTR/music/strings.xml b/patches/src/main/resources/addresources/values-kmr-rTR/music/strings.xml index ebb57aa448..256abc1697 100644 --- a/patches/src/main/resources/addresources/values-kmr-rTR/music/strings.xml +++ b/patches/src/main/resources/addresources/values-kmr-rTR/music/strings.xml @@ -17,18 +17,157 @@ Second \"item\" text" --> + Ya dawî hat lêdan + Abuneyar + Vîdyoyên stranê + Encamên din + Stranên uploadkirî + Albûmên barkirî + Hunermendên barkirî + Patch\'a Jam a mîvandar update bike da ku pause û resume bikar bîne + Parvekirina rêza Jam + Jam\'ekî nêzîk mîvandar bike an tevlî wê bibe û rêzekê bi rêya Jam Layer edît bike + Parvekirina rêza Jam çalak bike + Entegrasyona Jam piştî ku YouTube Music dest pê bike dîsa çalak dike + Ji bo ku bixebite Jam Layer daxîne + Verda herî dawî ya Jam Layer di browserê xwe de veke + Kontrolên Jam + Jam\'ekî nêzîk mîvandar bike an tevlî wê bibe û sesyonê birêve bibe + Pakêta Jam Layer + App\'a heval a sazkirî ya ji bo parvekirina rêzekê ya nêzîk hilbijêre + Pakêta heval a sazkirî bikar bîne. Pairing pêdiviya bi pejirandina te û tokenekî şiyaneyê ya lokal heye + Wi-Fi vekî? + Wi-Fi veke, piştre rastbinê destpêkirin an tevlîbûna Jam biçirîne + Mîhengên Wi-Fi veke + Betal bike + Pakêtê bikar bîne + Navê pakêta Androidekî rast binivîse + Pakêj hat savekirin. Ji bo girêdanê Jam Layer sazbike + Jam hat qutkirin + Pêşî Jam Layer çalak bike + Parvekirina rêza Jam di mîhengên de çalak bike, paşê YouTube Music ji nû ve veke + Pêşî pakêta Jam Layer a hatî hilbijartinî sazkirine + Koda ku li ser host xuya dibe binivîse. Wi-Fi Aware dikare amûrên nêzîk girê bide, her çend LANên wan cuda bin jî + Bi koda te tevli bube + Tevlî be + QR scan bike + Koda heşt-karakteran binivîse an jî daxwistinekî bikele + Tevlî Jamê xwe be + Koda kopî bike + Koda Jam + QR nîşan bide + Tamam + Davet bike Jam\'ê xwe + Ji bo tebikirin û edîtkirina vê rêzê scan bike. Bi bidanîna Jamê, daxwîn tê betalkirin + Lînk kopî bike + Daveta Jam + Jam dubare girêdide; pişt re dîsa biceribîne + Hat lêzêkirin Jam\'ê + Jam bernim e; track nehat zêdekirin + Jam\'ê Te + Bi hev re guhdar dikin + Têketina Jam\'ê + Bi hev re guhdar bin + Nûkirina Jam\'ê… + Lêgerîna mîvanê te li ser Wi-Fi… + Li benda mirovan im + %s kes têkildar bûye + %s kes hatine girêdanê + Vî kodî li ser amûreke nêzîk binivîse. Wi-Fi Aware li ser LANên cuda dixebite gava pişgirî hebe. Di %s xulekan de diçe + Biçe %s? + Çêkirin vekirî ne + Çiyandina edîtan hat qiflkirin + Bi Wi-Fi Aware hatiye girêdan + Bi Wi-Fi hatiye girêdan + Bi Bluetooth hatiye girêdan + Di Jamê de nayê piştkirin + Tekmilkirina tevkêrê hat betalkirin + Ji nû ve girêdanê bi mêzvanê we re dikirim... + Reyeyekê parve bike. Bihêle her kes stranan zêde bike + Mirovleyan vexwîne + Mêvan dikarin rêza pazariyê biguherînin + Jam bidawîne + Mêzvan muzîkê lê dide. Stranan rasterast di reyeya YouTube Music de zêde bike, biguherîne yan jî jê bibe + Her du amûran nêzîk hev bihêle. Tu dikarî betal bike gava em li mêzvan digerin + Tevkêrê betalkirin + Ji Jam derkeve + Jamek destpê bike + Çireya Jamê amade bike + Bîrvestin + Hûrgulî + Vebike + Vegerandina vebijarkên stranê… + Vebijarkên stranê tune ne; careke din ceribîne + Nekarî vebijarkên stranê vebike + Ji bo daxistina Çireya Jamê tu browser tune ne + Li benda rêza mîvandarê ye + Pêşî stranekê di YouTube Music de veke + Li ser mîvandar lî bide + Ji Jam derkeve û bi awayekî herêmî lî bide + Di Jam de ya din lî bide + Zêde bikeye rêza Jam + Vê parçeya Jam lî bide + Bijêre ku li ku lî bide + Mîvandar bîne vir + Stran amade nebû ku were lêgerîn + Rewşê rêzekê hat guhertin; careke din hewl bide Logo-ya serê Bixwe Şêwazî + Trackên audio yên albûmê bide lîstik + Li şûna vîdyoya muzîkê, tenê versiyona audio ya albûmê ya stranan bide lîstik. \'Spoof video streams\' pêwîst e + Guherîna audio/video çalak bike + Hevlêrîna guherîna Versiyona Track-a Audio û Vîdyoya Muzîkê ya Fermî biceribîne Vîdyoyên muzîkê, podcast an stran bi sepanên dakêşkirina sêjiyê dakêşîne + Ji bo menyuya Download daxînerê derve bikar bîne + Optiona Download a menyuyan li şûma wê daxînerê te yê derve vedike + Dawnlod + "Tu dawnlod nîn in +Trackên dawnlodkirî li vir xuya dibin" + %1$s • %2$s • %3$s MB + %1$s track • %2$s MB + Li dawnlodan bigere + Rêzkirin + Piştî hunermendê rêzkirin + Piştî navê rêzkirin + Li gorî mezinahiyê sor bike + Hemû daxezan jêbike + Hemû daxezan jêbike? + Hemû trackên daxezî dê ji vê amûrê bêne jêbirin + Daxez hatin jêbirin + Tiştek nehat dîtin + Çalakiyên ji bo %s + Bileyîne + Bileyîne an rawestîne + Berê + Piştve + Rawestîne + Bigire + Jêbire + Dawnloda jêbire + Dawloda jêbire? + %s dê ji vê ampirê were jêbirin + Dawlod hat jêbirin + Nehat şiyandin ku dawlod were jêbirin + Nexweş bû lîstkirina daxistinê + Daxistinên audio + Lîstkirina offline + Nexweş bû nasîna trackê ji bo daxistinê + Daxistin jixwe di pêşveye + Jixwe hatiye daxistin + Daxistin dest pê kir + Daxistin dest pê dike… + Daxistin %s%% + Daxistin bi dawî bû + Daxistin bi ser neket Derbar @@ -38,12 +177,17 @@ Second \"item\" text" Lêrîner Şarkî Dîsan + Zimanê YouTube Music + Zimanê sîstemê + Android 13 an nûtir dixwaze \'Get Music Premium\' etîketê veşêre Şîrketê pêşniyarên Premium-a muzîkê veşêrin Reklamên wîdyoyê veşêre Veşartina Guhestina Deng/Vîdyoyê + Butona Dadegêşandinê veşêre + Butona Dadegêşandinê di bernameya podkasta de veşêre Veşartina Refê Explore Pêvekirina bara fîltreyê veke Veşartina Refên Torê @@ -101,7 +245,26 @@ Second \"item\" text" Navkirina ji bo vê seseyê rawestiya Navkirin dubare hat destpêkirin Moda vîdyoyê dema navkirinê çalak e, nayê xebitîn + Crossfade bi lîstana SABR ve vekirî nayê + Crossfade gava ku casting tê kirin vekirî nayê Çawa dixebite + "Crossfade dawiya straneke bi destpêka ya din re diguhêre, bi bikaranîna dirêjî û xaniyên we + +Li ser pêşxistina otomatîk û li ser derbasbûna destkarî dixebite, her yek bi guhertina xwe. Di pêşxistina otomatîk de, tevliheviyê çend saniyan beriya dawiya stranê dest pê dike" + Sînoryet + "Vîdyoyên muzîkê gava crossfade vekirî be tenê wekî deng lê didin. Ji bo rawestandina crossfade û temaşekirina vîdyoyê, shuffle bi dirêjî bidar xine + +Vîdyoyeke muzîkê ya ku dest bi lîstanê dike, heta strana din di moda vîdyoyê de dimîne + +Stranên ku ji dirêjahiya fade kêm in, crossfade nabin + +Ekranê qiflkirî an agahdariyên (notification) dikare di navbera guherînê de ji bo demekê rewşeke kevn nîşan bide + +Rawestandina crossfade ji bo temaşekirina vîdyoyeke muzîkê dikare hin vîdyoyan reş bihêle. Rawestandina crossfade berî ku tu stranê hilbijêrî, vê yekê rêdigire" + Nihêr nayê kirin + "Castkirin an nîşandana deng (Chromecast, nîşandana ekranê an a deng) + +Lîstkirina SABR (stremên vîdyoyên spoof hatine guhertin)" Kreya xwedî Taybeta crossfade ji aliyê VazerOG ve hatî çêkirin @@ -109,16 +272,28 @@ Second \"item\" text" Rewindê bikin, rewşa tevlihevkirinê + Leza lêdanê + Leza ku ji bo hemû trakan tê bikaranîn. Li %100 leza sepanê tê bikaranîn, wekî leza lêdana podkastê + Pitch bi lezê biguherîne Rihiya kirê destûrê da veguherandina hez kirinê Êdî dema butonê hez kirinê kişandinê, veguherandina stranê paşê nabe + Çalakiya lîstika tracka playlistê + Playlistê lê bide + Tenê tracka hatî hilbijartin lê bide + Tracka hatî hilbijartin zêde bikeye rêza li benda çalakiyê + Hat zêdekirin rêza li benda çalakiyê Butona Şîrkirinê Veşêrin Butona Dîrokê Veşêrin Butona Notîfîkirinê Veşêrin Butona Lêkirinê Veşêrin + Butona lêgerîna dengî veşêre + Butona lêgerîna sînorî veşêre + Butona Nû veşêre + Butona Nû di tabê Pirtûkxaneyê de veşêre Butonên Çalakiyê Hilbijêrin ku kîjan butonên çalakiyê li bin playerê xuya bibin @@ -126,6 +301,7 @@ Second \"item\" text" Butona Hezdikirin û Nefesgirtinê veşêrin Butona Şîroveyan veşêrin + Bijmê \'Live chat replay\' veşêre Butona Sözan veşêrin @@ -135,6 +311,7 @@ Second \"item\" text" Dîmena Butona Dakêşkirinê + Bijmê \'Start radio\' veşêre Menûya pêşbaz Hilbijêre ku kîjan pêkhatî di menuya lîstikvan de xuya bibin @@ -185,6 +362,8 @@ Second \"item\" text" Bigere Refqandin + Sînorê portreyê bi zorê bikin + Dema amûr tê zivirandin, sepanê di sînorê portreyê de diheleve Rengê miniplayer-a biguhezîne Rengê lîstikvanê biçîk bi rengê lîstikvanê bi tevahî yê veguherîne @@ -267,6 +446,9 @@ Second \"item\" text" Paqijkirina regex-a xwerû Bi rêzekê ya pêşniyazkirî bikar bîne da ku nivîsa zêde ji sernavên stran, hunermend an albûman were hildan. Mînak: (?i)\\s*\\((clean|explicit)\\) Hûnerê ji sernavê de dîtin û şîrovekirin + Navê hunermend û trackê bi karanî veqetandina \" - \" ji navê vîdyoyê derxe. Ji bo naveroka dubare barkirî ya ku navê kanalê wekî hunermend tê bikaranîn, girîng e + Albûma winda tije bike + Gava muzîk albûmekê nayê wêyî, li Last.fm lê bigere da ku scrobble\'ê te temam bibe "Scrobbling bi xwezayî karanîna YouTube Music ya we bi profîla we ya Last.fm an ListenBrainz re hevdeng dike. @@ -274,17 +456,235 @@ Ji bo ku bêyî astengiyê scrobbling li ser lîstikên paşnavê were hevdengki Şarkîyên partiyê sêyemîn bikar bîne Şarkîyên ji karûbarê sêyemîn yê ku dîtin, wê demê vegere şarkîyên paşnavê yên di hundirê de + Dema ku gotinên stranê tên dîtin ekranê vekişîne + Rênegehê ku ekran vemirîn gava panela gotinên stranê vekirî ye + Xizmata gotinên stranan + Nîşandan + Senkronîzasyon + Panel + Endpointê AI + Hemû dabinkaran hatin betalkirin - lyricên avakirî û lyricên entegre + Rêza dabinkeran vegerîne? + Dabinkeran vegerîne rêza pêşîn a default + Gotinên jiyerî yên hatine tohîmkirin paqij bike + Gotincên li ser vê amûrê hatine parastin jê bibe + %1$s MB hat paqijkirin + Token bistîne + Apple Music Media-user-token + "Kukiyên \"media-user-token\" ji music.apple.com bisekin + +Gav: +1. Li music.apple.com wekî endam têkeve +2. DevTools veke → Application → Cookies → music.apple.com +3. Nirxa \"media-user-token\" kopye bike" + Apple Music: media-user-token hat parastin + Apple Music: media-user-token hat paqijkirin + Apple Music: media-user-token ne rast e - ji kerema xwe kontrol bike û dîsa biceribîne + Spotify SpDc + "Cookie-ya sp_dc ji Spotify bikelepe. + +Gav: +1. Li open.spotify.com têkeve +2. DevTools → Application → Cookies → open.spotify.com veke +3. Nirxa \"sp_dc\" kopî bike" + Spotify: sp_dc hat savekirin + Spotify: sp_dc hat paqijkirin + Spotify: sp_dc ne rast e - ji kerema xwe kontrol bike û dîsa biceribîne + Tokenê Apple Music vebijêr e. Bêyî wê, Lyrically dê wekî alternatîv bê bikaranîn + Deezer ARL + "Cookie-ya arl ji Deezer bikelepe. + +Gav: +1. Li deezer.com têkeve +2. DevTools → Application → Cookies → deezer.com veke +3. Nirxa \"arl\" kopî bike" + Deezer: arl hat savekirin + Deezer: arl hat paqijkirin + Deezer: arl nebaş e - ji kerema xwe kontrol bike û dîsa biceribe + Tokenê Musixmatch + Tokenekê tenê lê zêde bike ku tu hesabê xwe bikar bînî. Divê ew ji appa Musixmatch a Android bê: tokenên ku ji musixmatch.com hatine kopyekirin bi klîyentê desktopê yê şopîtî ve girêdayî ne û API wan red dike + Musixmatch bêyî tokenekê jî dixebite, lewma ev qad dikare vala bimîne + tokenê bikarhêner + Musixmatch: tokenê bikarhêner hat tomar kirin + Musixmatch: tokenê bikarhêner hat paqijkirin + Musixmatch: tokenê bikarhêner nebaş e - ji kerema xwe kontrol bike û dîsa biceribe + Kukiyên YouTube + "Kukiyên YouTube ên xwe bikelevine da ku nivîsên jêrîn û wextê nivîsan ji YouTube bipirsî. + +Gav: +1. youtube.com di browserê de veke û têkeve +2. DevTools (F12) → Application → Cookies → youtube.com veke +3. Stringê kukiyên tevahî kopî bike (çuvanên key=value yên ku bi semîkolonê veqetandine) + +Kukiyên pêwîst: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPISID, LOGIN_INFO" + YouTube: kukî hatin tomar kirin + YouTube: kûkiyên hatin paqijkirin + YouTube: cookies nebaş in - ji kerema xwe kontrol bike û dîsa biceribe + Xeta yekî bi tekanê bigire + Ji bo ku tu herî wê kêliyê ya stranê, xetê betike; ji bo kopîkirina peyva bin tiliyê xwe yan jî tevahiya xetê (gera tekanê ne li ser peyvek be) wê dirêj bigire. \"Off\" her du tevgeran jî radike + Tagên beşan nîşan bide + Her beşa nû ya gotinên stranê bi tageke biçûk nîşan bike, wekî Verse an Chorus Nîşanê kopîkirinê bibîne + Ji bo kopîkirina gotincên orîjînal wekî nivîsa sade betike; ji bo parastina dosyaya çavkaniyê ya gotincên xav li Downloads wê dirêj bigire Nîşanê wergerandinê bibîne + Ji bo guhertina wergerê betike; tenê ji bo wergerê wê dirêj bigire. Wergerên daberê, Google Translate, an APIyekî li gorî OpenAIyê bikar tîne + Bişkoja Romanize nîşan bide + Ji bo guhertina romanîzasyonê bitapîne; ji bo tenê romanîzasyonê dirêj bitapîne. Romanîzasyona dabinker, Google Translate, an APIyekî li gorî OpenAI bikar tîne + Romanîzasyon û Hevrêkirina Herî + Gava dabinker, modela we û Google Translate bi ser nekevin, li ser amûrê romanîzasyonê bike. Xwendin li pişt her peyvên gotinên demdar tê; hevsengiya peyvan divê vekirî be. Çînî, Kantonî, Japonî, Koreyî, Yewnanî, Rûsî, Gurucî, Ermenî; nêzîkî 120 MB + Xwendina %1$s: %2$d%% + Xwendin amade ne (%1$d qedaran) + Dawberdan bi ser neket - ji bo dubarekirinê vê yekê veguherîne ser \'off\' û paşê dîsa \'on\' bike + Bişkoja Nûkirinê nîşan bide + Ji bo guhartina berhevê gotinên din bitapîne; ji bo lêgerîna gotinan bi navê stran û hunermend dirêj bitapîne + Gotinên stranê bigere + Navê stranê + Hunermend + Navê stranekê û hunermendekî binivîse + Hîç yek ji daberên çalak nikarin li stranên din bigerin + Hat savekirin + Peyv an heceyên senkronize kirî nîşan bide + Dema ku hebin, pêşînî bide gotinên peyv bi peyv. Dema ku gotin demên peyvan hildigirin, her peyv an heceyê li gorî muzîkê nîşan dide + Gotinên hatine lêdan veşêre + Hêlên gotinên li ser hêla lêdanê ya niha veşêre + Gotînên neyeşandî veşêre + Hêlên gotinên stranê yên xwarê ya niha ya lêdanê veşire + Gotinên MediaSession ên li ser ekranê ya qiflockê nîşan bide + Hêla niha li ser ekrana qulîkirî, Android Auto û Bluetooth nîşan dide. Android 11 an nûtir pêwîst e + Gotinên miniplayer-î nîşan bide + Xeta niha di miniplayer-ê de nîşan dide, û şop di beşa bin-nivîsan de tê nîşandan + Formatê navnûka stranê/hunermend + Formata tracka ku bi gotinên stranê re di Miniplayer û MediaSession de tê nîşandan. {t} navnûk e, {a} hunermend e û {v} versiyon e, mînak Acoustic, gava çavkaniya gotinan yekê peyda dike. Teksta di nav du xalan de tenê tê nîşandan gava her guherîna di nav wê de nirxek hebe; ji bo xaleke sade \\: binivîse. Her satır bi rêz tê nîşandan, û her carê gotin diguherin, derbas dibe + Formata satira gotinên stranê + Formatê rêya gotinên stranê di Miniplayer û MediaSession de. {lrc} rêya gotinê ye û {n} nîşan dide ka çend caran bi rêz hatî strana wê. Teksta navbera du-nuqteyan tenê gava rêyê dubare dibe tê nîşandan. Ji bo du-nuqteyek \\: bikar bîne + Format divê {lrc} dihewîne + Templateyê rêya hunermend û wextê animasyonê + Demê herî kêm ê ekranê (ms) + Demê herî zêde ê ekranê (ms) + Tenê ji bo animasyonên pir-rêyan. Her frame ji bo herî kêm demê minimum biparêze. Di demê maksimum de, rêya hunermend pêş bibe her çawa gotinên stranê wekî hev bin. + Kêmkirin divê 0 an meztir be. Zêdekirin divê ji 0 meztir be û herî kêm wekî kêmkirinê be. + Sînorên dirêjiya rêxê + Sînorên rêxeya gotin û hunermend, û girêjkiya karakterên tam-berz + "Sînora rêxeya gotinan +Gotinên dirêj li du nîvên hevseng dabeş bike. 0 dabeşkirinê radigire." + "Sînora rêxeya hunermend +Formatê traka tijî li çarçoveyên xwezî dabeş bike. 0 dabeşkirinê radigire." + "Girêjkiya karakterên tam-berz +Li her du sînoran tê sepandin. Li ser 1.75, çar karakterên tam-berz wekî 7 tê hesabkirin." + Sînorên jimarên tam ji 0 heta 10000 û girêjkiya karakterekî ku ji 0 meztir be binivîse + Beşa amûrî + Di Miniplayer û MediaSession de di wextê bêhnvedana amûrî de tê nîşandan. Her xet frameka animasyonê ye. {t} navnûk e, {a} hunermend e û {lrc} dawînîn xeta gotinan e + Offsetê wextê Miniplayer û MediaSession + Dema ku xeta din di Miniplayer û MediaSession de tê nîşandan diguherîne. Heke xet dereng derkevin, bo nimûne li ser ekrana qifil, nirxek negatîf bikar bîne + Fîltre + Regex-ê metadata-yê + îfadeya regular (regex) li ser navên stranan, hunermendan û albûman tê bikaranîn berî lêgerîna gotinên stranan û gava tê nirxandin ka du nav li hev tên an na. Ji bo herduyan bêkarhînin, wê vala bihêle. Ya default tiştên bêwate wekî \"Official Video\", \"Remaster\", \"【插曲】\", û \"- Topic\" radike + Regex-ê fîltreya gotinên stranê + îfadeya regular tenê li ser teksta gotinên stranê yên orîjînal tê sepandin (ne li ser wergeran an romanîzasyonan). Ji bo bêkar kirinê vala bihêle + Nîşanên hêlên agahiyê + Heke gotinên stranê bi şaşî ber bi jêr ve hatin filtrarkirin, vê rast bike an paqij bike. Nîşanên bi komayê veqetandî: rêzên ku nîşanek û piştî wê du-nuqteyek hebe, wekî agahiyên kredîtê li jêr têne nîşandan. Ji bo bêkar kirin vala bihêle + Tîpa yekem mezin bike + Tîpa yekem a her xeta gotinên stranê mezin dike + Nîşanên xutmekirinê yên dawiyê jêbide + Xalan, komayan, kolonan û nîşanên nîv-xutmekirinê yên li dawiya xetên gotinên stranê jêdide. Nîşanên pirsê, nîşanên bangsaziyê, xalên sê, quotation mark û bracket tên parastin Mezinahiyê nivîsê Mezinahiyê nivîsa stranê + Offset-a demê ya global + Gotinên senkronize li pey lîstina stranê tên. Heke wan li şaşî bikevin, offseta demê rast bike. Gotinên senkronize ber bi pêş an paş ve biguherîne. Heke gotin ji stranê pêş ketibin, nirxekê pozîtîf bikar bîne + Ji bo offset-a lyrics-an, segmentên SponsorBlock bikar bîne + Bêyî segmentên ku SponsorBlock derbas dike li hev bîne û lyrics-an ber bi audioya weşakirî ve bişîne. \'Off\' bi temamî SponsorBlock jibêre + Ev track tu video id nîne ku were şandin Çavkanî: %s + Bi dirêjî li ser butonka kopîkirinê bitikîne da ku vê LRCyê save bikî; heke tu bikaribî demên wê rast bikî û bi dabinkarên civakê re parve bikî, em ê spasdar bin + Ji hêla Google ve hat wergerandin + Ji hêla Google ve hat romanîzekirin + Ji hêla %s ve hat wergerandin + Ji hêla %s ve hatiye romanîkirin + Ji hêla %s ve hatî romanîzekirin + Hat wergerandin + Zimanê wergêranê + Zimanê romanîzasyonê Wergerandin… Nefibû ku nivîs were wergerandin + Hat romanîzekirin Otomatîk + Romanîzekirinê... + Gotinên stranê nehatin romanîzekirin + Tenê wergerandî + Tenê romanîkirin + Moda dual Li bernameyê hatî kopî kirin + Nûkirinê + Nûkirin + Kliya API ya QuickLRC + QuickLRC gotinên stranê yên tracksê ya niha dinivîse an li gorî demê rast dike. Xizmeteke cuda ye, ku bi endpointa OpenAI ya li jor re tune ye + Kêlîta QuickLRC hat save kirin. Butonka align a di dialoga lêgerîna gotinên stranê de, an jî agahdariya panê bikar bîne gava trackek gotinên stranê tune bin + Kliya API ya QuickLRC hat parastin + QuickLRC-ê ew kliya qebûl nekir + Kliya API ya QuickLRC hat paqijkirin + QuickLRC ji bo gotinên bêdem, demên wan diyar dike, an jî ji trackê dinivîse, bi lêçandina kredîtan. Bi kêlîteke hatî set kirin, tracksên bê gotin transcription pêşkêş dikin û dialoga lêgerînan butonkek alignmentê bi dest dixîne + Daxwaza hevrêkiyê ya QuickLRC bike + Hevrêkirinê dike + Hevrêkiyê biharîne + Daxwaza transkripsiyona QuickLRC bike + Transkrîbkirin + Transkrîbkirin hat betalkirin + Tenê trackên ku ji YouTube hatine lêdan dikarin werin transkrîbkirin + Tu gotinên stranê tune ne ku alignment lê were kirin - li şûma wê transcription tê kirin + QuickLRC nikarî vê trackê proses bike + Kredîtên QuickLRC ji bo vê trackê qediyan + "Gotinên stranê bi nav û hunermend re tên meşkirin, metadata ya neasayî dibe ku tiştekî nenîbe an jî versiyona şaş bibîne. Ji bo guhartina versiyonan 'refresh' bikar bîne. Dabinkeran ku bi * hatine nîşankirin tokenekî dixwazin. Hemû dabinkernên çalak her tim bi rêza we nayên daxwazkirin, lê li gorî leza xwe bersiv didin.\n\nGotinên stranê ji xizmetên ownê yên sêyem tên ku ne bi Morphe ve girêdayî ne, ne ji aliyê wê ve hatine sponzorkirin an pesendkirin. Her yek wekî ku heye tê bikaranîn: gotin, berdestbûna wan û lisansa wan bi van xizmetan û lisansdarkirên wan re dimîne, û bikaranîna wan dikare bibe mijara şert û mercên xizmetê yên wan. Tiştê ku digihîje xizmetekî çalak tenê ew lêgerîna we xwestiye ye - nav, hunermend, û li wê derê ku xizmetê pêdivî be, ID-ya vîdyoyê. Endpoint-a AI ya bijartî û transkripsiyona QuickLRC tenê piştî ku hûn wan konfîgure bikin, xebitînin û nivîsa gotinên stranê an URL-ya vîdyoya track-ê werdigirin." + Tenê wan dabinkeran çalak bike yên ku tu bikar tînî, an jî yên pêşîn bihêle - daxwazên berdewam dikarin malperên lyrics ên piçûk ên civakî winda bikin + Xuşana xeta kredê veşêre + Hêla kredîtê ya ku li xwarê gotinên stranê tê nîşandan veşire + Wergera û romanîzasyonê biguherîne + ON romanîzasyonê li jêr û wergerê li ser gotinên stranê datîne; OFF romanîzasyonê li ser û wergerê li jêr dihiştîne + Werger + Romanîzasyon + Dahêkerê wergerê + Google (Default, Belaş) + Li gorî OpenAI (Custom) + Dahêkerê romanîzasyonê + Google (Default) + Ferhenga Local / Offline + OpenAI Compatible (Custom) + Beşa AI + Tenê tê bikaranîn gava daberkar tu nîşanên beşan wekî Chorus an Verse neşîde, ku piştre modela we ya li gorî OpenAI wan tîne ziman + Endpointên AI & Cloud + Kîlleyên API, model, endpoint û şabloneyên promptê ji bo dabinkarên AI konfîgure bike. + URL-ê endpoint-ê API (mînak https://api.openai.com/v1/chat/completions) + Şablonê Promptê + Şûneke vekirî: {language}, {task}, {title}, {artist}, {count}, {lines} + Prompt hat tomar kirin + Prompt hat save kirin, lê endpointa AI hîşta neyê bikaranîn. Berê wê di Settingsên AI de configure bike + Prompt hat vegerandin + JSON ne valid e. Berî savekirinê wê rast bike an qada wê paqij bike + Mîhengên wergerandinê & Romanîzasyonê hatin savekirin + Mîheng hatin vegerandin ser default + URL-ê bingehîn divê bi http:// an https:// dest pê bike + Taqikirina Girêdanê + Endpoint-an Konfîgure Bike + Hemû Endpoint-an Nîşan Bide + Gihîştan tê kontrolkirin… + Dahêkerê Wergerandinê + Dahêkerê Romanîzasyonê + Hênerîna Modelan + Hêvîkirina modelan… + Modelekê Hilbijêre + Hêvîkirina modelan bi ser neket. Keya API-yê an jî girêdanê kontrol bike. + Mîhengên API-ya Gemini + Li gorî OpenAI (Endpoint-a Taybet) + Mîhengên API-ya DeepL + Keya API-yê + Model + URL-ya Bingehîn + Kîleya API (valîye) + Promptê wergerê + Laşê daxwaza zêde (JSON-ê valîye) + Kîleya otentîkasyonê (Belaş an Pro) + Kîleyên API, model, endpoint û promptan konfîgur bike 8.23.51 - Îkonên stûr neçalak bike 9.13.51 - Vegerandina tabên Player ên \'Up next\', \'Lyrics\' û \'Related\' diff --git a/patches/src/main/resources/addresources/values-kmr-rTR/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-kmr-rTR/shared-youtube/strings.xml index 51a0178559..efe451a818 100644 --- a/patches/src/main/resources/addresources/values-kmr-rTR/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-kmr-rTR/shared-youtube/strings.xml @@ -18,8 +18,12 @@ Second \"item\" text" --> + Anîmasyona destpêkê Vemirandî Reng + Monokrom + + Dabe Dest pê bike ku sazkirinê temam bike @@ -44,12 +48,17 @@ Second \"item\" text" Material You (Sêyem) Taybet + Daxizandin + Moda daxizandinê Bixweber + Di hundurê appê de download bike + Downloaderê derve Navê pakêta dakêşkerê Navê pakêta sepanê we yê dakêşkerê yê sazkirî Navê pakêtê têkevin Din Sepanê sazkirî nîn e + %s neyê sazkirin. Ji kerema xwe wê sazkirin "Naxwest sepana sazkirî bi navê pakêtê %s bibîne Verast bike navê pakêtê rast e û sepan sazkirî ye" @@ -67,11 +76,21 @@ Verast bike navê pakêtê rast e û sepan sazkirî ye" Reşê yê Morphe Xwerî + "Ev îkona ku di weşandina app-ên we de xuya dibe diguherîne. + +Ev mînasdarî îkonê di van cihên xwarê de nayê guhertin: +• Lîsteya app-ên mînasdarîya pergala amûrê +• Hin agahdariyên push +• Hilbijartina app-ê 'Bi ... ve veke' +• Îkona Android Auto + +Ji bo guhertina îkonê li her derê, careke din patch bikin û vebijartiya patch-a 'Îkona App' guherînin" Aîkona notifikasyonê Îkona serîlêdana guhêrîn Morphe lîstikê Îlanê Morphe + Dîroka lêdirîna we nayê paranormalkirin.<br><br>Ev yek bi gelemperî ji ber blokerê DNS-ê yê reklamên yan proxyyên torê ye.<br><br>Ji bo rastkirina vê, <b>s.youtube.com</b> bike whitelists yan jî hemû blokerên DNS û proxyyan bigire Ji nûve ne nîşan bide Goşiyên wîdyoyê spoof bikin @@ -81,6 +100,12 @@ Verast bike navê pakêtê rast e û sepan sazkirî ye" Ger hûn bikarhênerê YouTube Premium nebin, ev mîheng dikare pêwîst nebe." Tiştekî ji goşiyên klijentê neyê pêşbaz kirin + Klîyentê default naçale + "%1$s çend caran bi rêz nabû, lê %2$s bû. + +%2$s bike klîyentê default?" + Guherîne + Girtina vê mîhengê dibe ku bibe sedema pirsgirêkan di lîstina vîdyoyan de Bila veşêre: Spoof video streams > Îqneya VR-a Android @@ -113,12 +138,26 @@ Da ku teqezîya nêzîkê bikin, li jêr bidixin 'bidin pêşbaz'." • Ger hûn peyva Google-a xwe biguhênin an ev alav ji avakerên Google-a we ji holê rabikin, wê demê divê we îze ba nêzîkê bikin." OAuth2 niha rê ne. (API derket). Teqezîya VR bi rêz ve ne, teqezîya malpera otantîkasyonê ne-qebûl kirin. + OAuth2 bi demî neyê gihîştin (statû %s) OAuth2 niha rê ne. Ne çawa teqezîya VR bikin: otantîkasyonê malpera qebûl kirin. Ne çawa teqezîya VR bikin: %s. Ji Android VR derketim. Teqezîya VR bi derexaneyê, zî dosya teqezîya nêzîkê bî. + Kontrolên medyaya agahdariyê + Kontrolên lîstanê yên ku li ser ekrana girtî û panela agahdariyê xuya dibin rast bikin + Bişkoanê Berê & Paşve veşêre + Bişkokên \"Berê\" û \"Piştê\" di agahdariya medyayê, şaşana qiflişandî û kontrolên headphone\'an veşerîne + Seekbar bêkar bike + Rê li kişandina seekbarê li ser şaşana qiflişandî û panela agahdariyê digire + Bi butonên Berê & Paşê bigere + Kontrolên agahdarîya medyayê, ekrana qiflkirî, û headphonean diguherîne da ku li şûna guhertina vîdyoyan, lê bigerin + Dirêjiya paşvegerê + Saniyan ku bi butona Berê ve bêne paşvegerandin + Dirêjiya pêşvegerê + Saniyan ku bi butona Paşê ve bêne pêşvegerandin + Dirêjiya lêgerînê divê di navbera 1 û %s saniyan de be. Ya default tê bikaranîn Protocols QUIC qut bikin @@ -127,6 +166,10 @@ Da ku teqezîya nêzîkê bikin, li jêr bidixin 'bidin pêşbaz'." Zimanê dengê orjinal timê bike Her gav şopa dengê orjînal yê vîdyoyê lê dide şûna guhertoyek dublajkirî + Bêdengiyê derbas bike + Bi şêweyekî otomatîk bêdengiyên nav daxistinên dengê vîdyoyê derbas dike + Dirêjahiya herî kêm a bêdengiyê + Tenê bêdengiyên ku herî kêm bi vî dirêjiyê ne tên kurtkirin. Dema vîdyoya din dest pê bike dê dest bi kar bike Nûnerê pêvekirî Daxistina hejarekê da ku daxwazên torê yên piştgirîkirî bidin @@ -150,6 +193,7 @@ Da ku teqezîya nêzîkê bikin, li jêr bidixin 'bidin pêşbaz'." Rekel kiradên ekranê tam + Ji bo bikaranîna vê taybetmendiyê, \'Spoof video streams\' biguherîneye her kliyentekî ji derveyî \'Android Studio\' û \'Android VR Downgraded\' Lînkên parvekirinê paqij bike Parametreyên pirsîna şopandinê ji girêdên parvekirî têne çêkirin @@ -160,6 +204,11 @@ Da ku teqezîya nêzîkê bikin, li jêr bidixin 'bidin pêşbaz'." Protokola buffer-a logê çepkirin Agahiyên din ên logê ji bo pêşdebiran da ku pêkhêleyên app-ê nas bikin zêde dike + "Çalakirina vê mîhengê dê datên layout ên zêdetir tomarkirin, di nav wan de nivîsên li ser ekranê ji bo hinek pêkhudên UI. + +Ev yek dikare bibe alîkar ji bo naskirina pêkhudan gava hûn fîltreyên taybet çêdikin. + +Lêbelê, çalakirina vê dê hinek datên bikaran jî tomarkirin, wekî navnîşana we ya IP" Filtra niştecî Mîhengkirina fîltreên niştecî da ku her pêkhateyek veşêrin @@ -191,15 +240,39 @@ Ji bo bikaranîna vê taybetmendiyê, neçalak bike 'sextekirina streamên vîdy "Ev taybetmendî niha di bin pêşveçûnê de ye û dibe ku nexebite Ji bo dakêşana PotHelper li vir bikirtînin." + PotHelper êdî naxebite + "Tokenê ku di vê versiyona PotHelper de hatî avakirin êdî naxebite + +Ji bo rastkirina lîstkirinê 'Spoof video streams' veke" + Veke Guherîna guhertoya sepanê Serverê dixapînin ku hûn guhertoyek kevnêtir ê sepanê bikar tînin da ku taybetmendiyên berê yên navnîşê vegerînin + "Versiyona appê dê wekî versiyoneke kevin bê nîşanandin. + +Ev yek dê görünî û taybetmendiyên appê biguherîne, lê dibe ku bandorên nediyar çêbibin. + +Heke piştre were guhertin, ji bo rêkxistina UI pêşniyar tê kirin ku daneyên appê werin paqijkirin" Armancê guherîna guhertoya sepanê + Rêya nûkirina appê Bixweber + %s FPS + Rêya nûkirinê serdest bike + Hemişê + Lîstana portreyê + Lîstana ekrana tam + Lîstana portre û tam-ekran + Font Bixweber + Fontê sîstema amûrê + Dosyaya fonta taybet + Dosyaya fonta taybet + Dosyaye fonteke TTF an OTF hilbijêre + Dosyaya hatî hilbijartin fonteke TTF an OTF nîne + Fonta taybet nehat barkirin û hat guhertinye girtî Nexweşiyan bi demkî nehelin (API hat serxweşîn) @@ -208,6 +281,8 @@ Ji bo dakêşana PotHelper li vir bikirtînin." Nexweşiyan hene nehelin (%s) Xwedî veşartiye + Vîdyoyê nebeznînin, ligel %s kesên din + Vîdyoyê nebeznînin, %s ji dengan nebeznînin Return YouTube Dislike çalak bike Nexweşiyan wekî ji sedî nîşan bide Nexweşiyan wekî ji sedîyê li şûna hejmarê nîşan dide @@ -215,6 +290,9 @@ Ji bo dakêşana PotHelper li vir bikirtînin." Hejmara nexweşiyên texmînî li ser vîdyoyên ku afirîner nexweşiyan dezaktîv kiriye nîşan dide Îşaretên çewtîyê nîşan bide Dê îşaretê nîşan bide gava ku servera Return YouTube Dislike hewîr be + "Return YouTube Dislike hejmarên dislike ên ku YouTube di sala 2021'an de veşartibû dîsa zindî dike. Hejmar ji daneyên ku beriya rakirinê hatine arşîvkirin tên û ji bo vîdyoyên nû li gorî vote'yên bikaranhatîyên wê tên xประมาณkirin, lewma ew Approximate in û her çend rojan own dîtin + +ID'ya vîdyoya ku hûn temaşe dikin ji bo Return YouTube Dislike API tê şandin, û like û dislike'ên we bi awayekî anonîm têne şandin da ku xประมาณkirin bêne baştirkirin" Statîstîkên API-ya Return YouTube Dislike ya vê cîhazê Demê bersîva API-yê, bi navînî @@ -224,8 +302,12 @@ Ji bo dakêşana PotHelper li vir bikirtînin." Nexweşiyan bi demkî nehelin - Sînorkirina API-ya mişterî di heyaşînê de Bangên API-yê, hejmara bangên Tîkî bangên tîkînê nehatine kirin + %s bangên torê hatin kirin Bangên API-yê, hejmara demên serxweşînê Tîkî bangên serxweşînê hene + %s bangên torê bi demê ve hatin qedandîn Sînorkirinên API-yê yên mişterî Tîkî sînorkirinên mişterî hene + Sînorê rêya klîyentî %s caran hat dîtin + %s mîlîsekond diff --git a/patches/src/main/resources/addresources/values-kmr-rTR/shared/strings.xml b/patches/src/main/resources/addresources/values-kmr-rTR/shared/strings.xml index 6d2274336b..9362e03494 100644 --- a/patches/src/main/resources/addresources/values-kmr-rTR/shared/strings.xml +++ b/patches/src/main/resources/addresources/values-kmr-rTR/shared/strings.xml @@ -24,18 +24,22 @@ Second \"item\" text" Reng saz bike Reng nebaş Nûtin hewce ye + Ji bo ku ev guhertin pêk bê, appê ji nû ve veke Nûtinê bike Importe kirin Nuşînekiye Sazkarên Morphe pêşdizitin + Rêvekirinên %s hatin împortkirin Importe kirin ne serket: %s + Biçe jor Dizimên sazkarê bigere Derbarê Ji bo \'%s\' tu encam ne dîtin Pêşniyarê din bilêvêre Dizimên nêzîk Ji dîroka dizimê ji holê bibe? + Ji dîroka lêgeran rakişe Dîroka dizimê paqij bike Êdî dixwest hemû dîroka dizimê paqij bike? Tişeyên dizimê @@ -47,7 +51,14 @@ Second \"item\" text" Dîroka dizimê vala ye Bo raya şewitîna doza lêgerînê şopandî, î reîka lêgerînê binivîsîne û Enter bikşîne Nîşan da dîroka dizimê + Navê mîhengên Morphe Bixweber + Zêdekirin + Tweak + Dema morphingê + Taybet + Navekî binivîse + Nav nikare vala be Îkonên sazkirina Morphe nîşan da Kopîkirî: %s Zimanê Morphe @@ -90,6 +101,9 @@ Da ku zimanên nû wergerînin an wergerandinên hebin baş bikin, malpera morph Rêveşeke logsê debug Hêlanîna şopên pelan di tomarên teşhîsê de da ku alîkariya dîtina xeletiyan bike Qextinê li toastê ser Morphe error + "Girtina toastên şaşiyan hemû agahdariyên şaşiya Morphe veşeride. + +Tu nayên agahdarkirin derbarê tu bûyereke neyîspediya" Rapora logên çepkirinê Logs debug ên Morphe vekişîne Logs debug bi serketî hatin vekirin @@ -105,27 +119,91 @@ Da ku zimanên nû wergerînin an wergerandinên hebin baş bikin, malpera morph Bayinatan booleanan sererast bike Nîşanên taybetmendiyê bigerin… + Naçalak (%s) + Çalak (%s) + Blokkirî (%s) + Zoravî (%s) + Otomatîk Dabe Bo + Bi zor veke + Bloke bike Lêkirin Nîşanan xwertek hatin vekirin Nîşanan xwertek hatin veşartin Nîşanan xwertek lûsteyê hatin vekirin Serîlêdanê appê nûbike ku ev ayar bikarînin + Eksport û import + %s nîşanên taybetmendiyê hatin zeviyan + Tu nîşanên taybetmendiyê nehatin dîtin + Nîşanên taybetmendiyê bi serketî hatin derxistin + Derxistina nîşanên taybetmendiyê bi ser neket + Nîşanên taybetmendiyê hatin barkirin, ji bo sepandin li Save bitikîne + Xwendina pelê bi ser neket + Lêgerîna bîner + "Dibise ku kîjan nîşaneya taybetmendiyê dibe sedema tevgera aplikasyonê. + +Nîv ji %s nîşaneyan hatine bloka kirin, û piştî her destpêkirinekê tu dibêjî ka tevger hîna heye. + +Berî destpêkirinê, carekê tevgerê biduhurîne, wegera nîşaneyên ku aplikasyon hîna xwestine ji lêgerînan winda dibin" Destpêk + "Tu nîşaneyên taybetmendiyê ji bo lêgerînê tune ne. + +Tenê ew nîşaneyên ku aplikasyon gihîştiye û niha bi zorê nehatine vekirin dikarin di lêgerîna binarî de werin bikaranîn" + "Gav %1$s + +%2$s nîşaneyên mayî, %3$s ji wan ji bo vê gavê hatine bloka kirin. + +Sîstema aplikasyonê dîsa destpêbike, kontrol bike ka tevger heye, paşê li jêr bersiv bide" + Lêgerîna nîşaneya taybetmendiyê di pêşve çûyê + Tevger heye + Tevger tune ye + Lêgerîna binarî betal bike + Lêgerîna binary hat betkirin + "Flag-ê taybetmendiyê %s dibe sedema tevgerê. + +Niha hat blokkirin" + "Neqeredibû flag-ek taybetmendiyê. + +Wusa xuya dike ku tevgera app-ê nayê kontrolkirin bi flag-ek taybetmendiyê" + "Neqeredibû tenê flag-ek taybetmendiyê. + +Tevger di destpêka lêgerînan de winda bû, lewma dibe ku ji yekî zêdetir flag-ên têkilidar bin an jî yek ji bersivan şaş bû" Sazîna MicroG veke + MicroG nehatiye sazkirin. Besaz bike Çawa kirin heye + "MicroG v%1s ya we kevne û dibe ku bibe sedema pirsgirekan. + +Ji kerema xwe MicroG nû bikeye versiyona herî dawî v%2s" + Nûkirin + Paşguh kirin + "MicroG ê hatî sazkirin ji aliyê Morphe ve nayê pêşniyar kirin: + +%1s + +Ji bo jêkirina wê bitikîne, piştre vê sepanê veke û MicroG RE sazkirine" + Ji eskî kirin + "MicroG destûr nîne ku li piştperdeyan bixebite. + +Ji bo telefona xwe rêberiya \"Don't kill my app\" bişopîne, û rênimayiyan li ser sazkirina MicroG a xwe bicîbîne. + +Ev ji bo xebatkirina sepanê pêwîst e" Malper veke + "Divê optimîzasyonên bataryayê yên MicroG bên guhertin da ku pirsgirêk pêş bikevin. + +Ragirtina optimîzasyonên bataryayê ji bo MicroG bandoreke neyênî li ser bikaranîna bataryayê nake. + +Bi botãoê berdewamkirinê bitikîne û guhertinên optimîzasyonê qebûl bike" Bidomîne Nîşanîya dîyaloga çêkirina hestiyariya pîlê ya MicroG "Birçawiya vê, dê ragirinê MicroG optimîzasyona bateriyê veşêre. @@ -152,4 +230,6 @@ Ger hûn ezmûnek bêkêmasî dixwazin, <b>veguherînin</b> evê û ⚠️ Ez versiyona lêgerînî dixwazim ✅ Ez versiyona pêşbaz dixwazim + Loga têkçûyînê li belgeyên bikarhêner hat save kirin: %s + Loga têkçûyînê hat save kirin: %s diff --git a/patches/src/main/resources/addresources/values-kmr-rTR/youtube/strings.xml b/patches/src/main/resources/addresources/values-kmr-rTR/youtube/strings.xml index d84369e152..fe027f966b 100644 --- a/patches/src/main/resources/addresources/values-kmr-rTR/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-kmr-rTR/youtube/strings.xml @@ -32,11 +32,26 @@ Second \"item\" text" Hilbijartina parastina ji bo lîstikê di binê de tunekirin Lêkirina lîstikê paşnavê veqetîne + Gava amûr hat qiflkirin, bi xwe rake Dabe + Gava hat qiflkirin her tim rake + Rake, mêjî ku dengê bê-têl hat girêdan + Sînora leza scrollkirinê ravekîne Nûvekirinên qeydarê qûldar bike. Server ji nûkirina sêwirana appê re pêşî digire + "Layoutê appê vedigereye layoutê ku dema yekem hatî sazkirin hatî bikaranîn. + + هندek layoutên aliyê serverê dibe ku nevedgerin. + +Guherînle ev in: +• Komponentên di menyuya flyout a playerê de (an jî mîhengên têkilidar) dibe ku nekarin bixebitin +• Jimarên herikandî nayên animatekirin +• Tabê Library tê nîşandan +• Beşa muzîkê ya şiroveya vîdyoyê dibe ku nekaribit bixebite +• Maddên menyuya flyout a playerê nayên veşartin +• Buttona guhartina hesabê dibe ku li ser tabê Library nayê nîşandan" Filtira menuya flyout-a xurekê çêbike Filtira menuya flyout-a xurekê @@ -48,6 +63,8 @@ Second \"item\" text" Kartên albûmê di bin nivîsa hunermend de veşêrin Kartên hunermend hidin Karta hunermendê li serê encamên lêgerînê de veşêrin + Butonên kanalê veşêre + Butonên wekî Subscribe, View Channel, Community, û YouTube Music ji profîlên kanalê di encamên lêgerînê de veşêre Postanên civakî hidin Bannereyên kompakt hidin Bannerên kêmkirî û valahiya di navbera pêkhateyên hinekî de veşêrin @@ -58,6 +75,7 @@ Second \"item\" text" Bicikên dirêj ên mikrifonê bidin veqêşînin Dugmeya mikrofonê ya mayî di kûçeya jorîn-rastê de li ser ekrana pêşniyarên lêgerînê veşêrin Butona \'Premium Bistîne\' veşêre + Rafa Dîrokê vedişêre Şelfeyên qereziya nîşandinê hidin Hides horizontal shelves such as Breaking news, Community posts you may like, Continue watching, Explore more channels, Explore more topics, For You, Latest videos, Most relevant, Posts from Creator\'s Community, Shopping, and Watch it again Şelfa wênekê bidin veqêşînin @@ -70,6 +88,8 @@ Second \"item\" text" This button usually appears in the Home feed for refreshing the feed. --> \'Vîdyoîên نوێ\' قایپە Bannerên \'Vîdyoyên herî dawî\' ên şîn li jorê feeda malê de veşêrin + Rewşên rasterast veşêre + Vîdyoyên ku niha rasterast in di feed-a malperê û encamên lêgerîgan de veşêre Lîstikên mix veşêrin Beşa fîlm veşêrin @@ -78,6 +98,7 @@ Second \"item\" text" Bi kurtahiya \'Agahdar bikin\' veşêrin + Bişkokê ya bîranînê ji bo streamên zindeyên bernamekirî û premiereyên wan vedişêre Playables veşêrin Taybeta ku dihêle hûn lîstikên derbas kirin veşêrin Navê tasgîrên termên lêgerînê veşêre @@ -87,9 +108,14 @@ Second \"item\" text" Bi kurtahiya \'Zêdetir nîşan bide\' veşêrin Dugmeya firehkirina di bin vîdyoyên di encamên lêgerînê de veşêrin Barê kanalan ên abone veşêre + Navên kanalên abonebûyî veşêre + Navên kanalên di şirîta kanalên abonebûyî de veşêre Erdekan veşêrin Hide surveys and nudges about feed recommendation accuracy Sernaketa biletê veşêrin + + Bannerê \'Vîdyoyên nedestgeş hatine veşartin\' veşêre + Bannerê \'Vîdyoyên nedestgeş hatine veşartin\' di lîsteyên vîdyoyan de veşêre Demîlê barkêşîyê veşêre "Senetkirîn: @@ -97,6 +123,8 @@ Second \"item\" text" • Li ser layouta otomobîlan xebatê neke" Wêneya biçîkê vîdyoyê veşêrin + Labelên vîdyoyê veşêre + Labelên wekî \'Bi şêweya otomatîk hatî dublekirin\' / \'Hatî dublekirin\', \'Komîsyon qezenc dike\', \'Hype\' kirî, \'Mirovên din jî temaşe kirin\', \'berhem(ên)\' û \'Dibe ku tu jê hez bikî\' veşêre Hejmara dîtinên veşêre "Senetkirîn: @@ -108,6 +136,7 @@ Second \"item\" text" YouTube Doodles veşêrin Anîmasyonên demî û bûyerên ser YouTube logo veşêrin + Nîşandana YouTube Doodles dibe ku rê li ber nîşandana sernavên taybet bigire Barê kanalê veşêrin Avahiya kanalê veşêrin Veşartina bişkokên \'Beş\' û \'Di vî vîdyoyê de\' @@ -130,18 +159,28 @@ Second \"item\" text" Liê hejîkirina navnîşê Hêbûna rêberiyên nermalina internetê ve nebînîne. Bişînin \'Senkronîzekirina bi vîdyoyê re\' bikirin + Veşêre gradienta lêdan û girtinê + Gradienta kontrolên playerê veşêre gava tu tiliyê dixî û digirî ji bo lezkirinê Reaksiyonên demdirêj ve nebînîne. Taybeta ku dihêle bavêjan emojiyan di beşa şîroveyan de û di chatê de bersîv bidin veşêrin + Menûya \'Alîkarî û feedback\' veşêre + Dema ku bi buttona sê-xalan tê bikaranîn, wekî di encamên lêgerîgan de, menûya \'Alîkarî û feedback\' veşêre + Handle veşêre + Handle\'ê kanala te di tab\'a \'Tu\' de veşêre Sernuke video قایپە Sêserê vîdyoyê di moda tîkêldîtinê de veşêrin Sêrkirina nivîsa serînivîsa vîdyoyê + Gotinên wekî \'#\', \'Komegera pereyan\', \'berhem(ên)\', û \'Dukan\' ji binnavê vîdyoyê paqij dike + Buttona \'Ask Gemini\' veşêre + Buttona \'Ask Gemini\' li bin panela \'Ask about this video\' veşêre Têgeşîna vîdyoyê. Hilbijêrin ku pêkhateyên di sêserê vîdyoyê de xuya bibin Bêjeya \'Vîdyoya hunerî ya çêkirî bi çavan\' veşêrin Bêjeya \'Pirs\' veşêrin Bêjeya \'Taybetmendî\' veşêrin Bêjeya Cihên taybet, Lîstik, Muzîk û Kesên hatine behs kirin veşêrin + Beşa girêdanên Kanalê vedişêre Bêjeya Beşên veşêrin Bêjeya Rêveçî veşêrin Bêjeya \'Pêşkeftina Kursê\' veşêrin @@ -157,6 +196,7 @@ Second \"item\" text" Beşa \'Çawa ev hatiye çêkirin\' veşêre Zorîyên Heyecan ve nebînîne. Bêjeya \'Dîtinên sereke\' veşêrin + Beşa \'Konseptên Sereke\' veşêre Bêjeya Transkrîpsiyon veşêrin Vê beşa \'Li vê vîdyoyê têkeve\' veşêrin Bişîniya butona \'Subscribe\' veşêrin @@ -230,6 +270,7 @@ Second \"item\" text" سەرجەمی კომنت‌ها لە ڕه‌وشدارى سەرەكی قاىپه‌ Şîrkirina Rêgezirên Civatê Bişkoka \'Kurîna Short\' veşêrin + Butona Nehejandin veşêre Şîrkirina Butona Emojiyê Vebijarkên barekê veşêre Vebijarkên barekê ji bilî \'Herî jor\' û \'Nûترین\' veşêre @@ -237,12 +278,21 @@ Second \"item\" text" Butona Hediyê veşêrin Dîmena Butona Agahî Barê xwerasên chatê veşêre + Buttona herêmana jêrîn a chatê ya zindî veşêre Pêşniyarên çata zindî veşêre Pêşniyarên ku nêzîka barê nivîsînê ya çatê têne nîşandan, wekî agahdariya çata tenê abone, vedişêre. + Butona Menu veşêre + Butona menyuya 3-xalan a her şîroveyê vedişêre Pêşbîna şîroveyê veşêrin Hemû vebijarkên sortkirina şîroveyan ji bilî \'Herî jor\' û \'Nûjen\' veşêrin + Koment ji hêla Morphe ve hat veşartin… + Butona şîroveyên mînîmal çalak bike + Kartên xişandinê û bijartoka rûpelê ya wê ya ku li ser butona şîroveyan tê nîşandan, vedişêre Bişkoka \'Spas\' veşêrin Şîrkirina Butona Demê + Bişkoja \'Fanên herî serkeftî\' vedişêre + Butona Wergerînê vedişêre + Payama \'Hêj tu koment nînin\' veşêre Girêdên lêgerînê yên ronahîkirî paqij bike Girêdên lêgerînê yên ronahîkirî paqij dike, tenê nivîsa asayî tê hiştin Filtreya peywirdan @@ -302,7 +352,40 @@ Sînorkirin: Sînorkirin: • Kurtefîlmên AI-yê yên fîlterkirî dibin valahiyek vala • Dibe ku hinek vîdyoyên AI-yê hîn jî nîşan bidin" + "AiSList lîsteyek vekirî ya kanalên YouTube e ku naveroka AI-çêkirî belav dikin û ji hêla xabanvanan ve tê parastin. Kanal ji hêla bikarhêneran ve tên ragihandin û berî ku werin zêdekirin tên kontrolkirin + +Lîste ji GitHub tê daxistin û vîdyoyên li ser amûrê te tên fîltrekirin" + Kanalên AI rapor bike + Veguhereya raporê li menyuya vîdyoyê zêde bike + Veguhereyekê li menyuya vîdyoyê zêde bike ku kanala vîdyoyê ji AiSList re rapor dike + Navê raportker + Navê ku li cem raporên te li ser tabloya pêşbiryariya giştî ya AiSList tê nîşandan. Gava ev vala be, raporkirin nayê kirin + Kanala AI rapor bike + "%s bi vî vîdyoyê wekî belgeyê bidê AiSList? + +Tenê wan kanalên rapor bike ku: +• Dengên AI, dîtinên AI, skript an muzîka AI wekî naveroka xwe ya sereke bi kar tînin +• Naverokan bi amûrên AI û kêm karê mirovan bi gelekî hilberînin + +Wan kanalên ku tenê li ser AI axivin, an AI ji bo hin beşên vîdyoyên xwe bi kar tînin rapor nekin + +Rapor giştî ne û berî ku kanal bikeve lîsteyê tên kontrolkirin. Jêbirin tenê bi daxwazekê li ser GitHub pêkan e" + Ev kanal jixwe di lîsteya bloka AiSList de ye + Ev kanal jixwe li ser lîsteya hişyariyê ya AiSList e + Raport bike + Ji bo raportkirina kanalan, navê pêşkêşkerî di mîhengên fîltra kanala AI de belge bike + Lêgerîna kanalê tê kirin… + Kanala vê vîdyoyê nayê dîtin + Kanal hat raportkirin û li benda kontrolkirinê ye + AiSList nekarî raportê save bike, paşê dîsa biceribîne + Gelek raport hatin şandin, paşê dîsa biceribîne + AiSList rapor red kir + Rapor başarneqet, girêdana xwe kontrol bike + Lîsteya spî ya reklamên kanalê + Vîdyoyên ji kanalên di lîsteya spî de reklamên wan têne lîstinkirin, da ku afirînerên wan payments bistînin. Li ser her kanaleke din reklam hatine blokkirin + Kanalê zêde bikeye lîsteya spî ya reklamê + Kanalê ji lîsteya spî ya reklamê rakire Daste bibin zivaniyê qewryn Bannera firoşgeya store\'yê ekranê dabezîne Reklamên gelemperî veşêrin @@ -315,6 +398,7 @@ Sînorkirin: Promosyonên YouTube Premium veşêre + Barkirin… Kanalê niha: Tê bikirin listeya spî @@ -325,6 +409,22 @@ Sînorkirin: Kanalê ji listeya spî de hat lêkirin + Di kanalê de bigere + Bi botãoek lêgerînê ya duyemîn li ser rûpelên kanalê zêde dike da ku tenê di wê kanalê de lêgerîn bike + Li kanalê bigere + Encamên lêgerîna kanalê + Li vê kanalê tiştek nehat dîtin + Nexwestiya lêgerîna vê kanalê + Vîdyoyan + Postan + Tu postên li ser vê kanalê nayên dîtin + Têkilî + Yên herî zêde hatine dîtin + Yên herî kêm hatine dîtin + Herî nû + Herî kevin + Herî kurt + Herî dirêj Lînkê hat kopîkirin ser klîpbordê Lînkê bi demê hat kopîkirin ser klîpbordê @@ -333,16 +433,56 @@ Sînorkirin: Copy timestamp by default Tevgera bikirinê û bikirin û girtinê ya bişêvekirina \'Girêdana vîdyoyê kopî bikin\' bişêvekirin + Naveroka vîdyoyê kopî bike + Ji bo kopîkirinê, li ser naveroka vîdyoyê bitapîne û bihêle + Naveroka vîdyoyê hat kopîkirin Clipboard + Komentan kopî bike + Ji bo kopîkirina şîroveyekê, lê bitikîne û bihêle + Şîrove hate kopîkirin + Ji bo vekirina menyuya vîdyoyê, bitikînin û bihêlin + Bi tikînin û bihêlan li her derê li ser vîdyoyeke feedê, menyuya wê vedibe + Butona \"Paşê temaşe bike\" li ser thumbnailan nîşan bide + Bişka \"Paşê temaşe bike\" li ser thumbnailên vîdyoyên feedê tê nîşandan. Bi lêdan û girtina wê, \"Save to playlist\" vedikeve + Wêne yên postan daxîne + Bişkojeka daxistinê li gel bişkojeka Parvekirinê ya postên bi wêne yan GIF-an re tê nîşandan, û aleyek daxistinê li menuya wan tê zêdekirin + Wêne danê + Wêne danê + Danêdana %s wêneyan… + Danêdana wêne… + %1$s wêne hatin savekirin di %2$s + Wêne hat savekirin di %s + Nekir hat wêne danê + Di vê postê de tu wêne nehat dîtin + Karekê Askê lê menuyê flyout zêde bike + Karekê kurteya vîdyoyê lê menuyê flyout ê feed zêde dike + "Ask di destpêkê de hesabekî têkhatî di welatekî ku Ask lê berdest e dixwaze. + +Piştî ku vîdyoyek bi butonka Askê carekê hat vekirin, ev taybetmendî bi hemû vîdyoyan re dixebite, her çend welatê niha Ask lê nebe jî." + Kurteya vîdyoyê bide + Ask ji bo vê vîdyoyê nehat vekirin + Ask ji bo hesabê we an welatê we yê niha bernîna wê tune ye + Hejmara dîtinan li ser thumbnailan nîşan bide + Hejmara dîtinên vîdyoyên fîd li binê çepê thumbnailên wan tê nîşandan, ne li bin nivîsê. Hin vîdyoyan, wekî yên di rafên de, dibe ku hejmara dîtinan nîşan nedin + Hejmara like-an a thumbnailên feedê nîşan bide + Rêjeya like\'ên thumbnailên fîd nîşan bide + Li şîroveyan bigere + Li şîroveyan li butonê lêgerînê bitikîne da ku tenê şîroveyên barkirî yên ku hemû peyvên lêgerînê tê de hebin bên nîşandan. Peyvan di nav nîşanên quoted de binînin da ku li phrasekê bigerîn + Li şîroveyan bigere + Peyvên lêgerînê + Lêgerîn + Paqij bike + Rêza sorandina biguherîne an jî comments tekrar veke da ku lêgerî wan bikî + Lêgerîn hat paqijkirin. Rêza sorandina biguherîne an jî comments tekrar veke da ku hemû comments nîşan bidî \'Nêzîk kirin li TV\'ê\' asteng bikin Devreyê \'Cihazên te pêvekirin\' pop-up\'ê neçalak bike @@ -351,9 +491,25 @@ Sînorkirin: Disables the two-finger double-tap gesture that skips chapters, which may be accidentally triggered with one finger Vîdyoyê, podcastan, an stranê bi rêya sepanên downloadkirina sêjê dakêşîne. + Buttona daxistina serperdaya playerî nîşan bide + Buttonekê di playerê vîdyoyê de nîşan dide ku daxistina we ya derve vekedike + Bi lîdanekê zû daxazinê + Daxwaz dike ku li piştperde were daxizandin. Bidê û bihêle da ku daxiziner veke. Bi YTDLnis û YTindir re dixebite + Daxazinê wekî hatine temaşe kirin nîşan bike + Vîdyoyên ku bi daxînerekî derve hatine daxistin, tên zêdekirin di dîroka lêdirêwana YouTube\'a te de + Full HD (1080p) + Navîn (192 kbps) + Daxistina bilez + Vîdyo + Deng + Hemû vebijark + Daxînan di menyuya Shorts de nîşan bide + Amendekî daxînerê derve li menyuya Shorts player zêde dike + Daxîne Wergerê dîtinî Jê wergirê nivîskên vîdyoyê bi teknolojîya veguherandina nivîsê ya cîhazê, bi hevdîtina lîstikê, bi dengî bixîne + Biştekî overlay di deryayê ya vîdyoyê de nîşan dide. Ji bo guhertina wergerê bitikîne, ji bo mînasên bilez bitikîne û bihêle Ziman Pêşangeha wergerê Hêza dengê ya orjînal @@ -399,6 +555,7 @@ Divê anahtara we ne were parve kirin. Kesê ku bigihe wê dikare bikar bîne kr Veke OpenRouter.ai Wergera OpenRouter çê bû (HTTP %s) Sînore rojane ya MyMemory gihîştiye + Yê din li %s destgehatî ye Hêj we bikar hînê kotaya wergerê ya belaş a MyMemory-yê ya roja îro (5,000 karakteran). Zêde bike e-maila we ya MyMemory-yê li ser mîhengên da zêde bike sînorê rojane berbi 50,000 karakteran, an guhêrîne dabînkerê wergerê din Hêj we bikar hînê kotaya wergerê ya MyMemory-yê (50,000 karakteran). Guhêrîne dabînkerê wergerê din an bicerîbîne dîsa yarîn Wergera MyMemory çê bû (hewl %s) @@ -420,6 +577,9 @@ Divê anahtara we ne were parve kirin. Kesê ku bigihe wê dikare bikar bîne kr Deryîne %1$s - %2$s Hêmana demê ne derust e + Buttona Wêne-di-Wêne nîşan bide + Menûya Wêne-di-Wêne nîşan bide + Biguherîneye Picture-in-Picture Butona hemûyan lîstik bike nîşan bide "Tap to generate a playlist of all videos from channel, tap and hold to undo @@ -450,18 +610,48 @@ Info: May not work on live streams" Vîdyo jixwe di lîsteya tomar kirina ji bo paşê dîtinê de hatî tomarkirin Nikare vî vîdyoyê di lîsteya tomar kirina ji bo paşê dîtinê de tomar bike Paşê temaşekirinê tomar bike + Bişkoja \'Ji bo paşê temaşe bike\' di mînuya mîhengên playerê de nîşan bide + Rê dide ku vîdyoya niha ya temaşeyî di mînuya mîhengên vîdyoyê de di \'Ji bo paşê temaşe bike\' de were parastin Bişkoka \'Paşê temaşekirinê tomar bike\' di vîdyoyên zarokan de nîşan bide + Rê dide ku vîdyoyên derê bi zarokan re di mînuya mîhengên vîdyoyê de di \'Ji bo paşê temaşe bike\' de werin parastin + Bişka \'Save to Watch later\' di flyout-a shorts de nîşan bide + Rê li savekirina shortsan di \'Paşê temaşe bike\' de ji menuya flyout dide + Watch later ji bo playlistê veguhêre Dîrûvaniya paş Zêde bikeye rêza Morphe Lîsteyê di nav beşan de bidomîne Lîste piştî destkirina serî lêve tê parastin Veguherîneya kirina tomarkirinê û kirina rêzê + Ji bo veçarkên rêzê bitikîne, ji bo savekirina \'Paşê temaşe bike\' bitikîne û bihêle + Cureya \'Yekê din di rêzê de lê bide\' Bixweber + Rêza xwerû + Reqha Morphe + Client side ya rêza Morphe Bendeleya rêza flyout-a feed-ê zêde bike Bendeleya flyout-ê ya zêde zêde dike. Heke \"Zêde bikeye rêzê\" nîşan neyê, vê çalak bike. + Reze + Reze (%s) + Hemû paqij bike + Tiştek li rezyê tune + Li ser her vîdyoyekî \'Play next in queue\' bikar bîne + Niha tê lîstkirin + Di rêza paşve bi lîst kirin + Yekî dawî di qatarê de bileyî + Rêzkirin Lêkirin + Hat lêzêkirin + Nevêroya vîdyoyê nekaribit were zêdekirin qatarê + Playlist nayên zêdekirin qatarê + Rêz tije ye + Nexebitand vîdyoyê destpê bike + Lîsteya spî ya reklamê di menyuya flyout de + Karekî flyout zêde dike ku rê dide reklam di kanala wî vîdyoyî de bilîzin + Ev taybetmendî dibe ku bikaranîna PotHelper an guhertina \"Spoof video streams\" bo têketina TV an Android VR bixwaze + Listeya spî ya lezê di menyuya flyout de + Karekî flyout zêde dike ku her tim kanala wî vîdyoyî bi leza 1.0x lîstik dike Tewrê nîşanî dayî ne Îd-a lîsteyê tune ye Lîste vala ye @@ -486,6 +676,19 @@ Info: May not work on live streams" Lîsteyê vekirin Lîsteyê li lîsteyê parastin + Playlist hat parastin + Hat parastin di %s de + Rêzê paqij bike + Rêz hat paqijkirin + Rêz + Yê dawî di rêzê de lêbide + Yê din di rêzê de lêbide + YouTube nekarî rêza vedanê nîşan bide. + Rêza vedanê ji bo vê vîdyoyê amade nîn e. Lîstika vîdyoyê veke û careke din hewil bide. + Mixê zêde bikeye rêza vedanê an Mixê temaşe bike? + Temaşe bike + Zêde bikeye rêza vedanê + Mix tê barkirin… Open channel from live avatar Prevents the live stream from opening when tapping the channel\'s avatar @@ -493,12 +696,22 @@ Info: May not work on live streams" Tiliyê rovnêr a lêkirinê qut bikin Disables the precise seeking mode that activates when pulling up the seekbar + DVR ya stream a zindeyî çalak bike + Rê dide ku di hemû streamên zindeyî de lêgerîn bêje, tevî wan yên ku bi asayî vê yekê qedexe dikin Mîhengkirina hûrimên şopê ji bo dîtina hêsantirî Nîşana vîdyoyê dema ku şopa dîtinê diherikînê da dîmenê Herî zêde vekirî + "'DVR ya stream a zindeyî çalak bike' dibe ku bernivîs nebe + +Ji bo rastkirina vê, 'Spoofkirina streamên vîdyoyê' veke û 'Client a default' biguhere bo 'TV Simply' an 'visionOS'" + Dirîjkirinaleya DVR-a streaming-a zindî + Rêwîteya we didebide ku hûn weşanên zindî heta 7 rojan paşve bikişînin + "Dibêje ku 'Dirîjkirinaleya DVR-a streaming-a zindî' dibe ku neberdest be + +Ji bo çareserkirina vê, 'Spozîkirina streaming-ên vîdyoyê' vekirin û 'Klîyenta pêxemîn' biguherin bo 'TV Simply' an 'visionOS'" Slide to seek çalak bike Enables slide to seek instead of playing at 2x speed when pressing and holding the video player @@ -540,6 +753,12 @@ Info: May not work on live streams" Tevahiyê nivîsê li ser ekranê guherîne (1-30) Astê sêwirîna germê Mesafeya destgîrkirinê ji bo guhertina pêşniyarî + Hessasiyeta swipe ya deng + Bihayên bilindtir ji bo her gava denga zêdetir swipe dixwazin + Gavên deng + Zêdekirina deng + Hêj bêtir berdane ber bi jor ji bo zêdekirina dengê herî zêde, lê ev yek dikare dengê bilind xira bike + Defaulta ampirî Hêsîtîya swipe ya ronahiyê Nirxên bilindtir pêdivî bi swipe zêdetir her gav ronahiyê hewînin Hêsîtîya swipe ya lezê @@ -591,6 +810,10 @@ Info: May not work on live streams" Butona pirsê veşêrin + Kanal û buttona Subscribe veşêre + Avatarê kanalê û buttona Subscribe li destpêka action bar veşêre + Butona Abonebe vedişêre + Bişkokên \"Subscribe\" û \"Join\" ên li kêleka avatarê kanalê di destpêka action bar de vedişêre Butona qutkirinê veşêrin Dibe ku ji bo hin hesaban xebit nebike @@ -618,8 +841,12 @@ Info: May not work on live streams" Dema ku bêdeng dibe nîşan bide Dibe ku darvekirin + Zimanê nivîsên binî yê tercîhkirî + Pêşî nivîsên binî yên dabinkerê dibijêre, paşê bi awayekî otomatîk wergerîne zimanê tercîhkirî Dabe + Zimanên auto-translate yên zêde lê zêde bike + Zimanên zêdetir lê menyuya auto-translate zêde dike Set caption cookies "Pirsgirêka YouTube ya standard rast dike ku jêrnivîsên wergerandina otomatîk dibe ku ji ber kêmasiya kukiyan ne berdest bin @@ -635,6 +862,7 @@ Divê 'kukiyên jêrnivîsan' bên peyda kirin." Sererast بونى ناخش Çareserî di qada pêşî de pişkekî ku dikare astengîya nivîskê veşêre + Captionan di picture-in-picture de nîşan bide Gomeqên hevveşan Hilbijêre ku kîjan pêkhateyên di toolbarê de xuya dibe @@ -657,14 +885,27 @@ Divê 'kukiyên jêrnivîsan' bên peyda kirin." Swap Create button with Notifications button Ger guherîna vê mîhengî bandor neke, biceribîne ku veguhere moda Incognito Çavkaniyên butona naweşandinê + Xala naveroka nû vedişêre Navîgasûk veşêre + "Ev şirîta navberê bi temamî veşerîne, di nav beraberî tabê 'Tu' + +Ji bo gihîştina Settings, bi rêya General → Toolbar → Show Settings bişka Settings çalak bike" Çavkaniyên rêznavî yên tir Rêbaza desteyê tevahiyê bi hêsanî kêş dibe + Buttonên navîgasyonê yên bê nivîs betal bike + Buttonên navîgasyonê bi nivîs nîşan dide, heke YouTube wan bêyî nivîs bişîne + Tabên navîgasyonê yên toolbarê bêkar bike + Subscriptions di şiriyeya navîgasyonê de nîşan dide, heke YouTube wê bibe tabek li serê Sêrutinê Animasyonên barê naweşandinê çalak bike Tevahîyên veşêrinî dema ku di navbera taban de guherîn Kêmbara otomatîk a kêmbara navîgasyonê qeybidine. Desteyê nivîskî bi hevsengî neşêre dema ku bi hêsanî tîkê dike + Stîla şirîta navberê Bixweber + Nîv-şefaf + Şefaf + Têr + Dibe ku ev şêwaz ji hêla amûrê we an versiyona YouTube-ê neyê piştgirîkirin, û dibe ku bibe sedema pirsgirêkên layoutê an xira bûnê Rêz Hilbijêre ku kîjan pêkhateyên di menuya veşêrîn de xuya dibe Butona birêveberînê veşêrin @@ -700,6 +941,7 @@ Ji bo nîşandana menûya 'Audio Track', 'Spoof video streams' biguherîne her c Dîmena sernijêra \'Vîdeyo dubare kirin\' + Menujên \'Li rêyê\' vedişêre Dîmena sernijêra Kalîteyê Dîmena sernijêra jêrî Kalîteyê @@ -726,6 +968,8 @@ Ji bo nîşandana menûya 'Audio Track', 'Spoof video streams' biguherîne her c Dîmena Butona Tam Ekranê Pêşgîhastî bajarê kontrolên lîstikê veşêre Şevkirina şûşa tarî yê ku dorî kontrolên lîstikvanê vîdeoyê ye + Şeffafiya paşbinga bişkokên kontrolê + Opacityya paş bingeha li pişt bişkokên kontrola player û şirîta demê rast bike (0 nedîtî ye, 100 teqet e) Butonê tomarkirina berîyê veşêrin Dîmena Butona Astînjan @@ -766,6 +1010,7 @@ Ji bo nîşandana menûya 'Audio Track', 'Spoof video streams' biguherîne her c Şertekê komentar veşêre Dane şikilê Effectê Etîketa girêdayîa video veşêre + Hîneyên îşaretan vedişêre \'Ekranê hişyar\' veşêre Dane şikilê hashtagê Panelê info veşêre @@ -815,7 +1060,10 @@ Settings → Playback (an Autoplay) → Autoplay next video" Panelên popup-a lîstikvanê quteyî bike Panelên lîstikên navnîşanê û sohbeta zindî nekarin bi xweber vekirin + Autoplay\'ê lîsteyên vîdyoyan ravekine + Rê li pêşîgirtina otomatîk a lîsteyên vîdyoyan ber bi vîdyoya dêmevê ve digire + \'Wereyê temaşe bikin?\' weşandina pirsê ragire Moda tam ekran li benda dawiya vîdeoyê derbeve Birçîbûn. @@ -823,6 +1071,8 @@ Settings → Playback (an Autoplay) → Autoplay next video" Horîzontal tenê Portrait and landscape + Landscape di fullscreen de zorê bike + Gava di fullscreen de derkeve, playerê ber bi landscape ve zivirîne. Li ser ekranên piçûk, yên ku jixwe di landscape de fullscreen vedikin, nayê bikaranîn Têkelên tevna tîkê Tevgerên player di moda ekrana tijî de @@ -830,6 +1080,7 @@ Settings → Playback (an Autoplay) → Autoplay next video" Portret mod da xêrkirina destûrê sekinîn Portreta horizontal da xêrkirina destûrê sekinîn Tevgera zoomê ya bi çikandinê neçalak bike + Pinch to zoom vegerîne Vîdyoyên bi modê tîrêjê vekin Vemirandî @@ -842,6 +1093,8 @@ Settings → Playback (an Autoplay) → Autoplay next video" Zûmkirin (ji bo dagirtinê) Nîşandana bişkoka pîvana vîdyoyê Bikirtînin ji bo guhertina Default, Dirêjkirin û Zûmkirin + Tenê di ekrana tam de nîşan bide + Bişka pîvana vîdyoyê di deryayê ya portreyê de veşêre Bişkojka bêdengiyê nîşan bide @@ -859,12 +1112,31 @@ Settings → Playback (an Autoplay) → Autoplay next video" Telefon Tabled Otomobîl + "Guherîn ev in: + +Layouta tabletê +• Postên civakê veşartî ne + +• Mîhengê 'Playback in feeds' tune ye +• Bişkeya Remix û bişkeya Deng di Shorts de tune ne +• Overlayê 'Vîdyoyên zêdetir' dibe ku tune be +• Action barê vîdyoyên kevin ê ne-collapsed tê nîşandan +• Elementên menuya flyout dibe ku rast nekarin + +Layouta otomotîvê +• Feed li gorî mijar û kanalan hatiye organîzekirin +• Mîhengê 'Playback in feeds' tune ye +• Bişkeya Remix û bişkeya Deng di Shorts de tune ne +• Overlayê 'Vîdyoyên zêdetir' dibe ku tune be +• Shorts di playerê asayî de vedibin +• Action barê vîdyoyên kevin ê ne-collapsed tê nîşandan" Rêvekirina sêwirana tabletê di lîstikvan de "Sêwîna lîstikvanê kevnarî dîsa çê bike Sînordarî: Ger 'Restore old video action bar' veqetandibe, 'More videos' dîtin ne mumkin e" + 21.11.48 - Rêveberiya berê ya subtitles vegerîne 20.39.41 - Dabeşîna kevnayî pêl vekişîne ser lîste 20.28.41 - Nexalînetî ikonên giran 20.13.41 - Restore channel name below video player @@ -915,23 +1187,37 @@ Sînordarî: Ger 'Restore old video action bar' veqetandibe, 'More videos' dîti Bi xweber ber bi Shortsê din ve bazide ji bilî dubarekirina niha Bi xweber di demê dema lîstikê de bazide Hêlîgiriyê ji lîstikvanê hîlîkirî li ser ekranê heya berî destpêkirina app-ê dîrokî bike + Shortsên zindî derbas bike + Streamingên zindî yên ku di feeda Shorts de derdikevin derbas dike Lêkirina di navnîşan de Her tim hemî de Tenê ser Wi-Fi Dabe + Naverokên orîjînal vegere + Navên vîdyoyên orîjînal, şiroveya vîdyoyan û şiroveya kanalê nîşan dide li şûna wan ên ku bi awayê otomatîk hatine wergerandin + Naverok tê barkirin… + Nûkirina feed-a otomatîk bêkar bike + Rê li feed-an digire ku piştî ku kevneşop bûn, bi xwe nû bibin + Layoutê karta feedê + Klasîk + Kart + Kartên bi butonên qebûlkirin, şirovekirin û savekirinê + Şiroveyên Mîni-lîstikê vekirin qedexe kirin Mînîlîstê \'Devamkirina dîkirinê\' asteng dike, fektorek premîum a YouTube ku di destpêka sepanê de xuya dibe Minileýner Sazkirina şeffafiyê (0 şeffaf e, 100 neşeffaf e) Minileýner + Nav û kanal veşêre Vemirandî Bixweber Kêm + Kêm 2 Tablet Modern 1 Modern 2 @@ -953,10 +1239,21 @@ Sînordarî: Ger 'Restore old video action bar' veqetandibe, 'More videos' dîti Astê dorpelê Rengê têkildar ê Sebarê + Bi bişkokeya vegerê ji feedê derkeve + Bi guhertina vegerê her carê ji appê derkeve li şûma vegera serê feedê + Anîmasyonan zor bike Bixweber + Her tim vekirî + Her tim girtî + Status bar veşêre + Status barê pergale veşêre. Ji bo ku demeke kurt xuya bibe, ji jor ber bi xwarê ve bikişîne + Çareserkirina status bar a ku şirîtên jorîn dadiposhîne + Layouta status barê li hev digire, da ku şirîtên jorîn nediposhîne û li ser playerê tu qada vala nêhêle. Heke app crash bibe an şirîtên jorîn şaş xuya bin, vê veguherîne + Ronahî + Ekranê destpêkê ya tîrêjî ji hêla vê amûrê ve nayê piştkirin Paşxaneya temaya ronî Spî Material You (Spî)², @@ -969,6 +1266,7 @@ Sînordarî: Ger 'Restore old video action bar' veqetandibe, 'More videos' dîti Paşxaneya ronî ya taybet Rengê paşxaneya temaya ronî saz bike Rengê pêşberê biguherîne + Rengê temayê ji bo nivîs û îkonan bikar bîne Çalak bike ekranê barkerdina gradyantê Destbîne ranga duyem a barkê pêşkeftina vîdeoyê Sêwîna rengên xwekêşandî çalak bike @@ -978,6 +1276,9 @@ Sînordarî: Ger 'Restore old video action bar' veqetandibe, 'More videos' dîti Bo lîstikvanê lîstikvanan & pêşnîyaran Qîmêya seekbarê nêrasta + Analogê orjînal + Keka orîjînal + Metalê orjînal Lokay logo Bixwe @@ -989,15 +1290,32 @@ Sînordarî: Ger 'Restore old video action bar' veqetandibe, 'More videos' dîti Bo encamên lîstikê + Navên DeArrow + Sêra Sernav + Tabê Abonebûnan + Tabê Tu + Lîsteyên vedagirtinê & pêşniyarên Player + Encamên lêgerînê + Îkona DeArrow nîşan bide + Îkona DeArrow li ser navên vîdyoyên ji aliyê civakê ve hatine amadekirin nîşan dide + Moda asayî + Heke bikaranerên DeArrow vote dane ku ji nivîsê yên asal hez dikin, li şûma nivîsa DeArrow, nivîsa asal nîşan dide + Kêmkirina votên asayî ji bo nîşandana nivîsa asal + Hejmara dengêncasual di kategoriyekê de, wekî funny an creative, ku ji bo nîşandana nivîsê orijinal pêwîst e + Thumbnailsên DeArrow Miniaturekan orjinal DeArrow & Thumbnailên orîjinal DeArrow & Gîrkirinên statik Gîrkirinên statik + "DeArrow nav û thumbnailên clickbait bi yên ji aliyê civakê ve hatine amadekirin diguherîne ku rasttir in û kêm Senaşonal in. Nav ji hêla civakê ve tên nivîsîn û lê vote tê kirin, û thumbnail jî framekên ji vîdyoyê hatine girtin in + +Vîdyo bi pêşegirêkî kurt ê ID-ya vîdyoyê ya hashed tê xwestin, lewma server nikare bi rastî bizanibe tu li kîjan vîdyoyê dinêrî. Ger vîdyoyek thumbnailê DeArrow tune be, ya orîjînal an jî wêneyekî statîk tê nîşandan" Pêstirî dîsêne berî nebeko şok û sînûse şîn-şûşê bikar bici kîrinê. Giritik ne tiştek şûşin kîninî dîşker di heşîn de şêrîne. Îşaretên xeletêya têkiliyê nîşan bide + Endpointê API URL-ya xwecihê cache thumbnail DeArrow Gîrkirinên vîdyoyê statik Goharan ji destpêk, navend / dawiya her vîdyoyê werin girtin. Van goharên YouTube rêz dixebitîne û populerdi ne @@ -1015,6 +1333,7 @@ Sînordarî: Ger 'Restore old video action bar' veqetandibe, 'More videos' dîti Çalak bikin dimensiyonên alavê Enabling this can unlock higher video qualities + Çalakkirina vê yekê dibe sedema qutbûna lîstana vîdyoyê, kêmkirina temenê bataryayê û bandorên yan ên nezanin Gava jêbirinê Hêrs kirina vegerêdana cîhazê hilbijêre @@ -1024,6 +1343,7 @@ Sînordarî: Ger 'Restore old video action bar' veqetandibe, 'More videos' dîti Haptîka xistina li ser dane bin Jêbire gava jêbirina zoomê + Heke te nêzîkî vê yekê agahiyên têketina hesabê xwe guhertibe, MicroG jê bibe û dîsa installation bike Berbestkirina veguhastinên girêdanê Girêdana orjînal rasterast vekirin, veguhastina şopandinê dîtin (youtube.com/redirect) @@ -1031,8 +1351,20 @@ Sînordarî: Ger 'Restore old video action bar' veqetandibe, 'More videos' dîti Lînkên vekin li navigerê Opens links in an external browser instead of the in-app browser + Mezinahiya bufferê playback Bixweber + Kêm + Navîn + Herî zêde + Derengiya vîdyoyê + Vîdyoyê derengdike da ku bi dengekî hêdî (wek headphone\'ên Bluetooth) re li hev be (0-5000 ms) + Tenê bi dengê Bluetooth re dereng bide + Tenê gava dengê Bluetooth hatiye girêdan vîdyoyê dereng dike, da ku speakerê telefonê li hev be + Bişka derengiya vîdyoyê nîşan bide + Ji bo ku derengiyê vegere 0 ms, li ser bigire + Menuyê derengiya vîdyoyê nîşan bide + Derengî bi mîlîsaniyan Otomatîk @@ -1056,6 +1388,8 @@ Sînordarî: Ger 'Restore old video action bar' veqetandibe, 'More videos' dîti Şîroveka rêzika bilindê nîşan bide Tap bike û nişê bibe ji bo vegerandina kalîteya pêşîn + Rezolusyon li ser buttona Kalîteyê nîşan bide + Rezolusyonê, wekî 1080, li şûna etiketekê wekî FHD nîşan bide Rêveiranê naverokê rêvebirî Rêveiranê kımeziya teng-çavê girtin @@ -1072,19 +1406,53 @@ Sînordarî: Ger 'Restore old video action bar' veqetandibe, 'More videos' dîti Agahdarkirina Guherîna Lezê Shows a notification when a new default playback speed is saved Kêşeya sere ya lêxistinê + Leza lîstana default li ser Shorts + Leza lîstana li ser Shorts bi bîr bîne + Hilbijartina we ya destê ya leza Shorts ji bo hemû Shortsên pêşerojê bikar tîne. Ger vekirî be, lez piştî girtina lîstika Shorts vegerî default e + Leza lîstana di menuyê ya Shorts de nîşan bide + Bendeke leza lîstana li menuyê ya lîstika Shorts zêde dike + Leza lîstana · %s Leza hêsîna serî ya pêşînkirina lîstikê hate guherandin: %s Mûzîkê bi serîhêdîya normal bikar bine Vîdyoyên muzîkê her gav bi leza 1.0x bilîzin + Listeya spî ya leza kanalê + Vîdyoyên ji kanalên di listeya spî de her tim bi leza 1.0x dilîzin + Kanalê zêde bikeye listeya spî ya lezê + Kanalê ji listeya spî ya lezê derxe Kontrola pîvana dengan çalak bike Pîvana dengan ya xwerû ya lîstinê Pîvana dengan ya xwerû hat guherandin: %s Dirêjkirina dema dengan Dihele lez û pîvan bi serbixwe bên danîn, dibe ku qusûrên dengan çêbibin + Bîranîna pozîsiyona lîstana streaming-a zindî + Ji bo streaming-ên zindî yên berdewam, pozîsiyona lîstana we bi bîr tîne û wê dewamdide, li şûma ku her tim biçe pozîsiyona zindî ya niha + Ev taybetmendî bi streamingên zindeyî yên 24/7 û streamingên din ên ku \'herdem vekirî ne\' nayê bikaranîn Ji vegirtina vîdyoyê ya HDR qut biqevîne + Vîdyoya HDR bi zorê + Appê bi zorê mecbur dike ku vê amûrê wekî şiyandarê HDR nas bike + "Heke dekodkirina HDR dixebite lê YouTube HDR'ê pêşkêş nake, wek hinek amûrên HyperOS yên ku HLG vedişêrin, vê bikar bîne. + +Sînoryas: +• Heke ekran nekaribe HDR nîşan bide, reng ango şaş derkevin +• Heke dekoder nekaribe HDR bi rê ve bibe, lîstana vîdyoyê dibe ku biçe +• HDR hîn jî girêdayî klîyentê streamên vîdyoyên spoof e" + Kôdekê vîdyoyê Bixweber + Ji bo zor kirina AVC, \'Sîmulyasyona herikên vîdyoyê\' biguhere bo \'Android Studio\', \'Android VR\' an \'visionOS\' + "VP9 +• Dikare temena bataryayê baştir bike û lerizîna lîstana rabe +• Li gorî AV1’ê zêdetir daneyên înternetê bikar tîne + +AVC (H.264) +• Klîyentê spoofkirina revşana vîdyoyê yê 'Android Studio', 'Android VR', an 'visionOS' dixwaze +• Dikare temena bataryayê hîn zêdetir baştir bike +• Dikare rezolûsyonên vîdyoyên winda kirî li ser amûrên kevin vegere +• Rezolûsyona herî bilind 1080p ye +• Dibe ku vîdyoyên HDR AVC'yê nebekar bînin +• Hinek amûr nikarin AVC'yê bi zorê bikar bînin" Veke ji menuya kalîteya pêşkeftî ve bike Dibe menuya hilbijartina kalîteyê derbas dike û rasterast menuya kalîteya Pêşkeftî vekirin @@ -1097,11 +1465,16 @@ Sînordarî: Ger 'Restore old video action bar' veqetandibe, 'More videos' dîti Destûr bide \'Android VR\' / visionOS AV1 Dema ku xerîdarê \'Android VR\' / visionOS tê bikaranîn, destûr dide deşîfrekirina AV1 bi nermalavê. + "Çalakkirina vê mîhengê dibe ku decoding-a software AV1 bikar bîne. + +Lîstana vîdyoyê bi AV1 dibe ku qutb bibe an framing-an winda bike" Efectê spoofingê + • Dibe ku lîstan li xuleka 1emîn biser bike • Menûya \'Audio Track\' tune ye. • Kodeka vîdyoyê ya AV1 tune • Moda berfireh a VRê 360° niye. • Volumê stabil tune + • Playback dibe ku 1 saniye zûtoyî bi dawî bibe • Ger deobfuscation jê birçîbû, playback dikare jê birçîbû. • Hişandinê orîjinal audio tune @@ -1112,9 +1485,174 @@ Sînordarî: Ger 'Restore old video action bar' veqetandibe, 'More videos' dîti Sexandinê di \'Stats for nerds\' de nîşan bide Çêkirina dîskê ya pelê vîdyoya kevnê + "Çareser dike ku butonekele aksiyona vîdyoyê nayên veşartin + +Sînorkirin: Dizivirîne şêwazê kevin ê 'Vîdyoyên zêdetir' overlay" + Stîla îkona Player\'ê Otomatîk Bixweber + Tûk + Stûr + Custom + Stîla îkona Shorts + Şopikera Rêze + Lîsteyên lîstana we wekî rêze şop bikin û cihê xwe biparêzin + Şopikera rêzeyê çalak bike + Rûpeleke Rêzeyê li Dîrokê û vebijarka Şopkirina rêzeyê li lîsteyên lîstana zêde dike + Rêze li şirîta navberê nîşan bide + Bişkoja Şopikera Rêzeyê nîşan bide + Bişkokeke playerê zêde dike da ku rêze ya vîdyoya niha bişopîne an veke + Pêşkeftina rêzên şopkirî tomare bike + Lêdana replayê li ser vê telefonê ji bo rêzên hazirkirî û nîşanên pêjeyê tomare bike + Betal bike + Save bike + Nîşana pêjeyê save bike + Navê rêzê + Veçarkên rêzê + Veçarkên pirtûkxaneyê + Epîzodan nû bike + Nav guhêre + Lîsteya playlistsê li YouTube veke + Ji pirtûxanekê rakirine + Link an ID ya playlistê + Hemû pêşkeftên herêmî paqij bike + Dîrokê paqij bike + Hemû pêşkeftên herêmî paqij bike? + Ev pozîsyonan, guhertinên şopkirî û hilbijartinên berdewamkirinê ji bo her vîdyoyê paqij dike. Reyanên savekirî û kataloga wan dimînin. Ger amadekirina tomarî vekirî be, lîstana nû dê bêyê tomar kirin + Wekî hatî dîtin nîşan bike + Wekî nehatî dîtin nîşan bike + Stata otomatîk bikar bîne + Ji 0:00 ve destpê bike + Careke din ji 0:00 bide destpêkirin + Rewş hat savekirin + Savekirinê dubare bicerxe + 100 yên berê + 100 yên din + Vîdyoya savekirî veke + Pêşkeftina lokal hat paqijkirin + Ji bo dîtina rêzeyên xwe, Dîroka ji Te veke + Dîroka temaşekirinê + Rêze + Barandina rêzên hatine savekirin… + Ji bo şopkirina epizodan, lîsteyeke vîdyoyan şop bike + Nîşaneya playlistê + Nûkirina epizodan… + Hemû rêze + Hatî temaşe kirin + Barandina epizodan… + Bêçavên din + "Tenê ji bo reyanên savekirî û vîdyoyên nîşanîdar pêşkeftinê tomar bike + +Ev pirtûkxane li ser vê amûrê tê tohîmkirin û di nav hesabên YouTube de tê parvekirin. Kontrolên rawestandin û paqijkirina dîroka YouTube wê naguherînin. Ji bo paqijkirina wê, bi rêya Reyan → Zêdetir → Paqijkirina pêşkeftina lokal bikar bîne + +Tomarkirina otomatîk di moda incognito de, gava nasnameya hesabê tune be, yan gava tu hesabekê biguherîn, rawestî dike. Ji bo bikaranîna wê bi wê hesabê re, şopkirinê dîsa veke" + Şopkirin nikare di moda incognito de yan berî ku YouTube rewşa hesabê niha barkirine, bê vekirin. Vegere Malê û dîsa biceribîne. Çalakirina hesabê ne pêwîst e + Şûnvekirina şopandina lokal + Tomarkirina pêşveçûnê girtî ye + Li %1$s berdewam bike + %1$s / %2$s epizod hatin temaşekirin + Epîzoda %1$s · %2$s + Epîzodên %1$s-%2$s ji %3$s + Ji bo tecrubekirina barkirina beşan nû bike. Lîsteyên naskirî hewcedariya gihîştina hesabê dikin; nîşanên wan hîşta li YouTube vekirên + YouTube nikaribû vê beşê veke + YouTube nikaribû vê lîsteyê veke + Beştekî hilbijêre + Heta îro + Tu beşên playable tune ne + Vîdyoya hatî save kirin · %1$s + Şop bike series + Series temaşe bike + Bidome + Li vir destpê bike + Xala destpêkê hat save kirin + Heta vir hatime temaşe kirin + Beş hatine nîşandan wekî hatine temaşe kirin + Vegerande + Çalak bike + 1 salve nû + %1$s beşên nû + Pêşkeftiyê li ser vê amûrê tomar bike + "Rêze û pêşkeftina we li ser vê amûrê dimîne, di nav hesabên YouTube de tê parvekirin. Kontrolên dîroka YouTube wan naguherînin. Pêşkeftinê ji menuyê Rêze paqij bike. + + tomar kirina otomatîkî ya bijarteyî ye. Di moda incognito de an gava hesab diguhere rawestîye" + Beş nikaribûn bên barkirin. Hûn dikarin lîsteyê wekî nîşanekî tomar bikin + Guhertinên neyên tomar kirin pir zêde ne. Pêşî tecrubeya tomar kirinê bike + Parastin tê kirin. Careke din biceribîne + Barkirina beşa tê kirin. Careke din biceribîne + Hêjmar neyê gihîştin. Daneyên te yên parastî hatin girtin + Guherîn nehatin parastin. Berî ku tu YouTube bigire, careke din biceribîne + Ev rêze jixwe hatiye parastin, yan jî lîsteya wê ya vîdyoyan guheriye. Ji nû ve veke + Ev rêze guheriye yan jî hatiye jêbirin. Pustxaneyê ji nû ve veke + Lîsteya vîdyoyan guheriye. Beşê careke din hilbijêre + Ev beş neyê gihîştin. Yekî din hilbijêre + Navê rêzeyê binivîse + Link an lîsteya YouTube an ID ya lîstey yâ binivîse + Ji kerema xwe berî ku tekrar nû bike, li benda me be + Nexweşandinê ya beşan neserkeft. Paşê tekrar ceribîne + Guherîn nekaribû bê temamkirin. Tekrar ceribîne + Domandina vîdyoyâ nekaribû bê piştrastkirin. Lîstera kontrol bike û tekrar ceribîne + Rêzên şopkirî + Ev vîdyo di vê lîsteyê de nîn e + Lêgerîna lîsteyên avahiyê... + Lînkê lîsteya avahiyê bikeyên + Nebûyê lîsteyekî avahiyê + Tu salve tune + Yên hatine temaşekirin veşere + Salve yên berê wekî hatine temaşekirin nîşan bike + Bi %1$s dest pê dike + Rêza epizodan + Rêza lîsteya avahiyê + Rêza lîsteya avahiyê ya ters + %1$s dîtin + %1$s dîtin + Epizodan wekî hatine dîtin nîşan bike + Herî kêm %1$s%% hatî temaşekirin bi herî zêde 90 saniyan ku man, an %2$s saniyan man + Bi awayekî otomatîk beân wek temaşekirî nîşan bike piştî her yek ji astên diyarkirî. Rêleya ji sadede her weha daxwaz dike ku ne zêdetir ji 90 saniyan bimînin, da ku dawiya beân dirêj neyê weşandin. Rêleya demê ya mayî ji bo vîdyoyan yên dirêjtir ji yek xulekê ye; ji bo ku wê ragire, wê bike 0. Guherîn li ser nûkirinên pêşveçûn ên pêşerojê têne bikaranîn. Hilbijartinên temaşekirî/netemaşekirî yên destî têne parastin + Çanda hat dîtin (1-100) + Saniyeyên mayî (0-300; 0 bêkar dike) + Bihayekê ji %1$s heta %2$s binivîse + Pêşkeftina YouTube bikar bîne + Piştî dîtina li ser amûreke din, rêzeya seriyan şop bike + Rewşa temaşekirinê û pozîsyonên berdewamiyê ji bo rêzeya we ya şopkirî ji hesabê YouTube yê current bixwîne. Tenê epizodan yên ku li hev tênin li ser vê telefonê tên savekirin. Kontrol tên kirin gava librêya rêzeyan vekirî ye û beriya Continue. Dîroka YouTube divê li ser amîra we ya din hatibe çalak kirin + Pêşkeftina YouTube tê kontrolkirin… + Pêşkevtina YouTube bernivîs e. Pêşkevtina savekirî tê bikaranîn + Hinek pêşkevtinên YouTube bernivîs in. Pozîsyonên savekirî tên parastin + Backup û vegêran + Kutubxaneya rêzehên xwe di faylekê de tomar bike an jî backupa yekê vegere + Rêzeyên şopkirî, pozîsyonên epizodan û bijardeyên temaşekirinê save dike. Destûrên recording û pêşkevtina YouTube nayên navandikirin + Backup tomar bike + Backup vegere + %1$s rêzeyan zêde bike. %2$s rêzeyên ku jixwe di librêya we de ne tên parastin. Pêşkevtina vîdyoyên heyî tê parastin + Backupa rêzeyan hat tomar kirin + Rêza %1$s hat vegerandin + Ev fayl ne backupeke rêzê ya pişgirîkirî ye, an jî pir mezin e. Pirtûkxaneya we nehat guhertin + Nebûya backup hat şandina. Cihê din bikey + Rêze hat jêbor kirin + Ev epizod niha zêdetirî carekê xuya dibe. Bijêre ka tu ji kîjanê bidomînî + Epizoda we ya savekirî êdî di vê playlistê de nîn e. Pozîsyona we ya savekirî hat parastin + Bi epizoda %1$s · %2$s bidome + Hûn dikarin her epizodekê li jêr bijêrin. Epizodên berê nayên guhertin + Ev playlist guherî. Rêzê veke da ku hûn bijêrin li ku bidomînin + Di vê rêveyê de êdî tu epizodan nîn in + %1$s bidomîne + Li rêzeyan bigere + Lêgerînê paqij bike + Tu rêze nehatin dîtin + Beşa bênav + Beşa nedestgehatî + Komentên Niconico + Chat-a zindî û replay-a chat-ê wekî komentên ku li ser vîdyoyê diherikin nîşan bide + Bişkozekê li ser dîtina vîdyoyê nîşan dide. Ji bo nîşandan an veşartina komentan lê bitikîne, ji bo mîhengên bilez lê bitikîne û bihêle + Mezinahiya fonta komentê + Mezinahiya nivîsa komentan (12-40 sp) + Opasîtiya komentê + Opasîtiya nivîs û paşbingavên komentê + Dirîjeya rêwiyê + Çend saniye tên girtin da ku komentaran ekranê derbas bikin. Nirxên kêm zûtir diçin + Dirêjiya herî zêde ya komentan + Komentên ku ji vê hejmarê karakteran zêdetir bin veşerîne. Her emoji wekî yek karakter tê hesabkirin + Tu sînor nînîn diff --git a/patches/src/main/resources/addresources/values-kn-rIN/reddit/strings.xml b/patches/src/main/resources/addresources/values-kn-rIN/reddit/strings.xml index 685403f5a6..cf7776fdaf 100644 --- a/patches/src/main/resources/addresources/values-kn-rIN/reddit/strings.xml +++ b/patches/src/main/resources/addresources/values-kn-rIN/reddit/strings.xml @@ -15,6 +15,4 @@ Second \"item\" text" --> - - App icon - + diff --git a/patches/src/main/resources/addresources/values-kn-rIN/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-kn-rIN/shared-youtube/strings.xml index e0ff8463ca..77635ba31c 100644 --- a/patches/src/main/resources/addresources/values-kn-rIN/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-kn-rIN/shared-youtube/strings.xml @@ -18,26 +18,23 @@ Second \"item\" text" --> + - Custom - Your watch history is not being saved.<br><br>This most likely is caused by a DNS ad blocker or network proxy.<br><br>To fix this, whitelist <b>s.youtube.com</b> or turn off all DNS blockers and proxies - Turning off this setting may cause playback issues - OAuth2 is temporarily not available (status %s) diff --git a/patches/src/main/resources/addresources/values-kn-rIN/shared/strings.xml b/patches/src/main/resources/addresources/values-kn-rIN/shared/strings.xml index 879428163b..31cdbe4dcb 100644 --- a/patches/src/main/resources/addresources/values-kn-rIN/shared/strings.xml +++ b/patches/src/main/resources/addresources/values-kn-rIN/shared/strings.xml @@ -18,7 +18,6 @@ Second \"item\" text" - Custom diff --git a/patches/src/main/resources/addresources/values-kn-rIN/sponsorblock/strings.xml b/patches/src/main/resources/addresources/values-kn-rIN/sponsorblock/strings.xml index d6765cd1db..9d2d4ab7d5 100644 --- a/patches/src/main/resources/addresources/values-kn-rIN/sponsorblock/strings.xml +++ b/patches/src/main/resources/addresources/values-kn-rIN/sponsorblock/strings.xml @@ -16,7 +16,6 @@ Second \"item\" text" --> - Do not show again diff --git a/patches/src/main/resources/addresources/values-kn-rIN/youtube/strings.xml b/patches/src/main/resources/addresources/values-kn-rIN/youtube/strings.xml index f5ee93a510..4ec76b1150 100644 --- a/patches/src/main/resources/addresources/values-kn-rIN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-kn-rIN/youtube/strings.xml @@ -31,6 +31,7 @@ Second \"item\" text" This button usually appears in the Subscriptions feed for future live streams or unreleased videos. --> + @@ -72,9 +73,7 @@ Second \"item\" text" - Custom - Do not show again @@ -227,8 +226,6 @@ Second \"item\" text" - Custom - Continue diff --git a/patches/src/main/resources/addresources/values-ko-rKR/music/strings.xml b/patches/src/main/resources/addresources/values-ko-rKR/music/strings.xml index 314f3e1bcb..e404e8de31 100644 --- a/patches/src/main/resources/addresources/values-ko-rKR/music/strings.xml +++ b/patches/src/main/resources/addresources/values-ko-rKR/music/strings.xml @@ -475,7 +475,7 @@ SABR 프로토콜을 사용하여 재생하는 경우 (동영상 스트림 속 단계: 1. music.apple.com에 회원 사용자로 로그인합니다 -2. 개발자 도구 → 응용 프로그램 → 쿠키 → music.apple.com을 엽니다 +2. 개발자 도구 → 애플리케이션 → 쿠키 → music.apple.com을 엽니다 3. 'media-user-token' 값을 복사합니다" Apple Music: media-user-token이 저장됨 Apple Music: media-user-token이 삭제됨 @@ -485,7 +485,7 @@ SABR 프로토콜을 사용하여 재생하는 경우 (동영상 스트림 속 단계: 1. open.spotify.com에 로그인합니다 -2. 개발자 도구 → 응용 프로그램 → 쿠키 → open.spotify.com을 엽니다 +2. 개발자 도구 → 애플리케이션 → 쿠키 → open.spotify.com을 엽니다 3. 'sp_dc' 값을 복사합니다" Spotify: sp_dc가 저장됨 Spotify: sp_dc가 삭제됨 @@ -496,7 +496,7 @@ SABR 프로토콜을 사용하여 재생하는 경우 (동영상 스트림 속 단계: 1. deezer.com에 로그인합니다 -2. 개발자 도구 → 응용 프로그램 → 쿠키 → deezer.com을 엽니다 +2. 개발자 도구 → 애플리케이션 → 쿠키 → deezer.com을 엽니다 3. 'arl' 값을 복사합니다" Deezer: arl 저장됨 Deezer: arl 삭제됨 @@ -513,7 +513,7 @@ SABR 프로토콜을 사용하여 재생하는 경우 (동영상 스트림 속 단계: 1. 브라우저에서 youtube.com을 열고 로그인합니다 -2. 개발자 도구(F12) → 응용 프로그램 → 쿠키 → youtube.com을 엽니다 +2. 개발자 도구 (F12) → 애플리케이션 → 쿠키 → youtube.com을 엽니다 3. 전체 쿠키 문자열(세미콜론으로 구분된 키=값 쌍)을 복사합니다 필수 쿠키: SID, HSID, SSID, SAPISID, @@ -565,17 +565,17 @@ LOGIN_INFO" 최대 화면 표시 시간 (ms) 여러 줄로 구성된 애니메이션에만 적용됩니다. 각 프레임은 최소 표시 시간 동안 유지됩니다. 최대 표시 시간에 도달하면 가사가 바뀌지 않더라도 아티스트 표시 줄로 넘어갑니다 최소값은 0 이상이어야 합니다. 최대값은 0보다 커야 하며, 최소값 이상이어야 합니다 - 라인 길이 제한 - 가사 및 아티스트 라인 제한, 전각 문자 가중치 - "가사 라인 제한 -긴 가사를 균형 잡힌 두 부분으로 나눕니다. 0으로 설정하면 분할 기능이 비활성화됩니다." - "아티스트 라인 제한 -입력된 트랙 형식을 순환 프레임으로 분할합니다. 0으로 설정하면 분할되지 않습니다." + 줄 길이 제한 + 가사 및 아티스트 줄 제한, 전각 문자 가중치 + "가사 줄 제한 +긴 가사를 균형 잡힌 두 부분으로 나눕니다. 0으로 설정하면 분할 기능이 비활성화됩니다" + "아티스트 줄 제한 +입력된 트랙 형식을 순환 프레임으로 분할합니다. 0으로 설정하면 분할되지 않습니다" "전각 문자 가중치 -두 제한 사항 모두에 적용됩니다. 1.75로 설정하면 전각 문자 4개가 7개로 계산됩니다." +두 가지 제한 모두에 적용됩니다. 가중치를 1.75로 설정하면 4개의 전각 문자가 7로 계산됩니다" 0에서 10000 사이의 정수 제한 값과 0보다 큰 문자 가중치를 입력하세요 - 연주 간주 - 연주 구간에서 미니 플레이어와 MediaSession에 표시됩니다. 각 줄은 애니메이션의 한 프레임입니다. {t}는 노래 제목, {a}는 아티스트, {lrc}는 마지막 가사 줄을 의미합니다 + 간주 + 긴주 구간에서 미니 플레이어와 MediaSession에 표시됩니다. 각 줄은 애니메이션의 한 프레임입니다. {t}는 노래 제목, {a}는 아티스트, {lrc}는 마지막 가사 줄을 의미합니다 미니 플레이어 및 MediaSession 타이밍 오프셋 미니 플레이어와 MediaSession에서 다음 가사 줄이 표시되는 시점을 조정합니다. 예를 들어 잠금 화면에서 가사가 늦게 표시된다면 음수 값을 사용하세요 필터 @@ -585,9 +585,9 @@ LOGIN_INFO" 원본 가사 텍스트에만 적용되는 정규식입니다. 번역이나 로마자 표기에는 적용되지 않습니다. 비워두면 비활성화됩니다 정보 줄 마커 가사가 잘못 필터링되어 하단으로 내려가는 경우 이 설정을 조정하거나 비워 두세요. 쉼표로 구분된 마커: 마커가 포함된 줄에 콜론(:)이 뒤따르면 해당 줄은 크레딧 정보로 간주되어 하단에 표시됩니다. 비워두면 비활성화됩니다 - 첫 글자 대문자로 표시 + 첫 글자 대문자로 표시하기 각 가사 줄의 첫 글자를 대문자로 표시합니다 - 끝 문장 부호 제거 + 끝 문장 부호 제거하기 가사 줄 끝에 있는 마침표, 쉼표, 콜론 및 세미콜론을 제거합니다. 물음표, 느낌표, 말줄임표, 따옴표 및 괄호는 유지됩니다 텍스트 크기 가사 텍스트 크기 diff --git a/patches/src/main/resources/addresources/values-ko-rKR/reddit/strings.xml b/patches/src/main/resources/addresources/values-ko-rKR/reddit/strings.xml index bad4f2e429..2c9c3ccf4d 100644 --- a/patches/src/main/resources/addresources/values-ko-rKR/reddit/strings.xml +++ b/patches/src/main/resources/addresources/values-ko-rKR/reddit/strings.xml @@ -58,7 +58,7 @@ Second \"item\" text" 피드 및 검색 추천에서 \'인기 급상승 중\' 선반을 숨깁니다 알림 제안 대화 상자 제거하기 서브레딧 방문 시 나타나는 알림 제안 대화 상자를 자동으로 닫습니다 - 홈 피드에서 분류 태그 배지 표시하기 + 홈 피드에 분류 태그 배지 표시하기 홈 피드에 게시글 분류 태그 배지를 표시합니다 일부 피드 항목에는 분류 태그 배지가 표시되지 않을 수 있습니다 게시물에서 조회수 표시하기 diff --git a/patches/src/main/resources/addresources/values-ko-rKR/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-ko-rKR/shared-youtube/strings.xml index 25d68baed0..9a8303ead6 100644 --- a/patches/src/main/resources/addresources/values-ko-rKR/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ko-rKR/shared-youtube/strings.xml @@ -21,9 +21,13 @@ Second \"item\" text" 앱 시작 애니메이션 사용 안함 컬러 - 컬러 Morphe + Morphe 컬러 흑백 - 모노크롬 Morphe + Morphe 흑백 + + 새 설정 강조 표시하기 + 업데이트로 추가된 설정 및 옵션 옆에 점이 표시되는 기간을 지정할 수 있습니다 (일수) + 사용 안함 설정을 완료하려면 다시 시작하세요 diff --git a/patches/src/main/resources/addresources/values-ko-rKR/sponsorblock/strings.xml b/patches/src/main/resources/addresources/values-ko-rKR/sponsorblock/strings.xml index 3b103b0aee..54cf02f791 100644 --- a/patches/src/main/resources/addresources/values-ko-rKR/sponsorblock/strings.xml +++ b/patches/src/main/resources/addresources/values-ko-rKR/sponsorblock/strings.xml @@ -58,6 +58,8 @@ Second \"item\" text" 이미 읽음 보기 일반 + 다운로드한 동영상의 특정 구간 저장하기 + 오프라인 상태에서 다운로드한 동영상의 특정 구간을 건너뛸 수 있습니다 연결 오류 알림 표시하기 SponsorBlock 서버를 사용할 수 없을 경우에 알림을 표시합니다 건너뛴 횟수 기록 활성화하기 diff --git a/patches/src/main/resources/addresources/values-ko-rKR/youtube/strings.xml b/patches/src/main/resources/addresources/values-ko-rKR/youtube/strings.xml index 3442bef0db..b4908cb262 100644 --- a/patches/src/main/resources/addresources/values-ko-rKR/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ko-rKR/youtube/strings.xml @@ -113,6 +113,9 @@ Second \"item\" text" 설문 조사 숨기기 피드 추천 정확도에 대한 설문 조사 및 안내 메시지를 숨깁니다 콘서트 선반 숨기기 + + \'사용할 수 없는 동영상이 숨겨졌습니다\' 배너 숨기기 + 재생목록에서 \'사용할 수 없는 동영상이 숨겨졌습니다\' 배너를 숨깁니다 업로드 시간 숨기기 "제한 사항: @@ -439,7 +442,7 @@ Second \"item\" text" 길게 탭하여 동영상 메뉴 열기 피드 동영상에서 아무 곳이나 길게 탭하면 해당 동영상 메뉴가 열립니다 - 썸네일에서 \'나중에 볼 동영상에 저장\' 버튼 표시하기 + 썸네일에 \'나중에 볼 동영상에 저장\' 버튼 표시하기 피드 동영상 썸네일에 \'나중에 볼 동영상에 저장\' 버튼을 표시합니다. 버튼을 길게 탭하면 재생목록에 저장됩니다 게시물 이미지 다운로드하기 @@ -466,11 +469,11 @@ Second \"item\" text" \'질문하기\'는 사용자 계정 또는 현재 국가에서 사용할 수 없습니다 - 썸네일에서 조회수 표시하기 + 썸네일에 조회수 표시하기 피드 동영상 조회수를 제목 아래가 아닌 썸네일 왼쪽 하단에 표시합니다. 선반에 표시되는 일부 동영상은 조회수가 표시되지 않을 수 있습니다 - 피드 썸네일에서 좋아요 수 표시하기 - 피드 썸네일에서 좋아요 비율 표시하기 + 피드 썸네일에 좋아요 수 표시하기 + 피드 썸네일에 좋아요 비율 표시하기 댓글 검색 댓글에서 검색 버튼을 탭하면 현재 불러온 댓글 중 검색한 단어가 모두 포함된 댓글만 표시됩니다. 구문을 검색하려면 단어를 따옴표로 묶으세요 @@ -500,8 +503,8 @@ Second \"item\" text" 동영상 오디오 모든 옵션 - Shorts 메뉴에서 다운로드 표시 - Shorts 플레이어에 외부 다운로더 플라이아웃 메뉴 항목을 추가합니다 + Shorts 메뉴에 다운로드 표시하기 + Shorts 플레이어에 외부 다운로더 메뉴 항목을 추가합니다 다운로드 음성 번역 @@ -615,7 +618,7 @@ API 키는 절대로 다른 사람과 공유해서는 안 됩니다. API 키에 피드 메뉴로 관련 동영상에 있는 Kids 동영상을 \'나중에 볼 동영상\'에 저장할 수 있습니다 Shorts에 \'나중에 볼 동영상에 저장\' 메뉴 표시하기 피드 메뉴로 Shorts를 \'나중에 볼 동영상\'에 저장할 수 있습니다 - 나중에 볼 동영상을 재생목록으로 리디렉션 + 나중에 볼 동영상을 재생목록으로 리디렉션하기 나중에 볼 동영상 Morphe 현재 재생목록에 추가 세션 간에 현재 재생목록 유지하기 @@ -859,9 +862,9 @@ API 키는 절대로 다른 사람과 공유해서는 안 됩니다. API 키에 이미 쿠키가 존재합니다 쿠키를 성공적으로 업데이트하였습니다 - 스크립트 고치기 + 스크립트 수정하기 스크립트가 불러오지 않을 수 있는 사전 조건 검사 실패 문제를 해결할 수 있습니다 - PIP 모드에서 자막 표시하기 + PIP 모드에 자막 표시하기 하단바 버튼 하단바에 표시할 버튼을 설정할 수 있습니다 @@ -1209,6 +1212,8 @@ API 키는 절대로 다른 사람과 공유해서는 안 됩니다. API 키에 앱 시작 시 \'이어서 시청하기\' 미니플레이어가 나타나지 않도록 합니다. 이 기능은 YouTube Premium 기능입니다 미니 플레이어 최소화된 동영상 플레이어의 레이아웃 및 동작을 설정할 수 있습니다 + 추가 미니 플레이어 + YouTube에서 추가 미니 플레이어 기능이 모두 제거되었습니다. 다양한 유형의 미니 플레이어를 복원하려면 YouTube 21.28.208 이하 버전에 패치를 적용하세요 미니 플레이어 유형 제목 및 채널 숨기기 사용 안함 @@ -1246,10 +1251,13 @@ API 키는 절대로 다른 사람과 공유해서는 안 됩니다. API 키에 상태바 숨기기 시스템 상태바를 숨깁니다. 상단 가장자리에서 아래로 스와이프하면 잠시 표시됩니다 - 상단 막대가 상태 표시줄에 가려지는 것을 해결 - 상태 표시줄 레이아웃을 일정하게 유지하여 상단 바가 덮이지 않도록 하고 플레이어 위에 빈 공간이 생기지 않습니다. 앱이 강제 종료되거나 상단 바가 잘못 보이는 경우 이 기능을 끄세요 + 상태바가 상단바를 가리는 문제 수정하기 + 상태바 레이아웃을 일정하게 유지하여 상단바가 가려지지 않도록 하고, 플레이어 위쪽에 빈 공간이 생기지 않도록 합니다. 앱이 강제 종료되거나 상단바가 비정상적으로 표시되면 이 기능을 비활성화하세요 글로우 + Morphe 글로우 1 + Morphe 글로우 2 + 이 기기에서는 글로우 스플래시 화면을 지원하지 않습니다 밝은 테마 배경 색상 하얀색 Material You (하얀색) @@ -1272,7 +1280,7 @@ API 키는 절대로 다른 사람과 공유해서는 안 됩니다. API 키에 동영상 진행률의 보조 색상을 설정할 수 있습니다 잘못된 재생바 색상입니다 - 원본 아날로그 + 오리지널 아날로그 오리지널 케이크 오리지널 메탈 @@ -1478,7 +1486,7 @@ AV1이 사용된 동영상 재생이 끊기거나 프레임이 손실될 수 있 • 일부 동영상의 최대 화질이 720p로 제한될 수 있습니다 플레이어 자바스크립트 변형 - 전문 통계에서 속이기 정보 표시하기 + 전문 통계에 속이기 정보 표시하기 이전 동영상 작업 버튼바 복원하기 "동영상 작업 버튼을 숨길 수 없는 문제를 수정합니다 @@ -1640,15 +1648,15 @@ AV1이 사용된 동영상 재생이 끊기거나 프레임이 손실될 수 있 사용할 수 없는 에피소드 Niconico 댓글 - 실시간 채팅 및 채팅 재생을 동영상 위에 흐르는 댓글로 표시합니다 - 동영상 플레이어에 오버레이 버튼을 표시합니다. 버튼을 탭하여 댓글을 표시하거나 숨길 수 있고, 길게 탭하면 빠른 설정이 실행됩니다 + 실시간 채팅과 채팅 다시보기를 동영상 위로 흘러가는 댓글로 표시합니다 + 동영상 플레이어에 오버레이 버튼을 표시합니다. 버튼을 탭하면 댓글을 표시하거나 숨길 수 있으며, 길게 탭하면 빠른 설정을 열 수 있습니다 댓글 글꼴 크기 - 댓글의 텍스트 크기 (12-40 sp) + 댓글 텍스트 크기 (12-40 sp) 댓글 불투명도 - 댓글 텍스트 및 배경의 불투명도 + 댓글 텍스트 및 배경 불투명도 이동 지속 시간 - 댓글이 화면을 가로지르는 데 걸리는 초 수입니다. 값이 낮을수록 더 빠르게 이동합니다 + 댓글이 화면을 가로지르는 데 걸리는 시간을 설정할 수 있습니다 (초). 값이 낮을수록 댓글이 더 빠르게 이동합니다. 댓글 최대 길이 - 이 값보다 글자 수가 많은 댓글을 숨깁니다. 각 이모지는 1글자로 계산됩니다 + 표시할 댓글의 최대 글자 수를 설정할 수 있습니다. 설정한 글자 수를 초과하는 댓글은 숨겨집니다. 이모티콘 하나는 글자 1개로 계산됩니다 제한 없음 diff --git a/patches/src/main/resources/addresources/values-ky-rKG/music/strings.xml b/patches/src/main/resources/addresources/values-ky-rKG/music/strings.xml index 6b7114bf21..e25bddbf86 100644 --- a/patches/src/main/resources/addresources/values-ky-rKG/music/strings.xml +++ b/patches/src/main/resources/addresources/values-ky-rKG/music/strings.xml @@ -17,63 +17,674 @@ Second \"item\" text" --> + Акыркы жолу ойнотулган + Жазылуулар + Видеолор + Башка натыйжалар + Жүктөлгөн ырлар + Жүктөлгөн альбомдор + Жүктөлгөн артисттер + Түрөттүүнү жана кайра başlatууну колдонуу үчүн хосттун Jam патчын жаңыртыңыз + Jam кезектерин бөлүшүү + Жакын жердеги Jam-ди түзүңүз же ага кошулуңуз жана Jam Layer аркылуу анын кезегин түзөтүңүз + Jam кезектерин бөлүшүүнү иштетүү + YouTube Music кайра ишкө киргенден кийин Jam интеграциясын активдештирет + Иштетүү үчүн Jam Layer\'ди жүктөп алыңыз + Браузериңизде Jam Layer-дин акыркы версиясын ачыңыз + Jam башкаруулары + Жакын жердеги Jam-ди түзүңүз же ага кошулуңуз жана сессияны башкарыңыз + Jam Layer пакети + Жакын жердеги кезекти бөлүшүү үчүн колдонулуучу орнотулган коштош колдонмону тандаңыз + Орнотулган коштош пакетти колдонуңуз. Жупташтыруу үчүн сиздин макулдугуңуз жана жергиликтүү мүмкүнчүлүк токени талап кылынат + Wi-Fi\'ни күйгүзөсүзбү? + Wi-Fi күйгүзүңүз, андан соң Jam\'ди кайра баштоого же ага кошулууга аракет кылыңыз + Wi-Fi жөндөөлөрүн ачуу + Жокко чыгаруу + Пакетти колдонуу + Туура Android пакеттин атын киргизиңиз + Пакет сакталды. Туташуу үчүн Jam Layer\'ди жөндөңүз + Jam үзүлдү + Алгач Jam Layer\'ди иштетиңиз + Жөндөөлөрдөн Jam кезекти бөлүшүүнү иштетиңиз, андан кийин YouTube Music\'ти кайра баштаңыз + Алгач тандалган Jam Layer пакетин орнотуңуз + Хостто көрсөтүлгөн кодду киргизиңиз. Wi-Fi Aware жакын жердеги түзмөктөрдү, алардын LAN тармактары ар башка болсо да, туташтыра алат + Код менен кошулуу + Кошулуу + QR сканерлөө + Сэгиз символдуу кодду киргизиңиз же чакырууну чаптаңыз + Jam-га кошулуңуз + Кодду көчүрүү + Jam коду + QR көрсөтүү + Даяр + Jam-га чакыруу + Бул кезекке кошулуу жана аны түзөтүү үчүн сканерлеңиз. Jamды токтотуу чакырууну жокко чыгарат + Шилтемени көчүрүү + Jam-га чакыруу + Jam кайра туташууда; бир азdan кийин кайра аракет кылыңыз + Jam-га кошулду + Jam жеткиликсиз; трек кошулган жок + Сиздин Jam-иңиз + Бирге угуу + Jam-ге кошулуу + Бирге угуңуз + Jam жаңыртылууда… + Wi-Fi аркылуу хост издөө… + Адамдар күтүлүүдө + %s адам кошулду + %s адам кошулду + Бул кодду жакын жердеги түзүлүшкө киргизиңиз. Wi-Fi Aware колдосо, өзүнчө LAN тармактарында иштейт. Мөөнөтү %s мүнөттөн кийин бүтөт + %s убактысына өтүү? + Оңдоолор ачык + Оңдоолор бөгөттөлгөн + Wi-Fi Aware аркылуу туташтырылды + Wi-Fi аркылуу туташтырылды + Bluetooth аркылуу туташтырылды + Jam учурунда колдость + Кошулуу жокко чыгарылды + Хостко кайра туташуу正在... + Кезекти бөлүшүңүз. Баарына ырларды кошууга мүмкүнчүлүк бериңиз + Адамдарды чакыруу + Коноктор кезекти оңдоп бериши мүмкүн + Джемди бүтүрүү + Музыканы хост ойнотот. YouTube Music кезегине ырларды түздөн-түз кошуңуз, жылдырыңыз же өчүрүңүз + Эки түзүлүштү тең жакын жерде кармаңыз. Хостту издеп жаткан учурда жокко чыгара аласыз + Кошулууну жокко чыгаруу + Джемден чыгуу + Джемди баштоо + Джем катмарын орнотуу + Башкаруу + Толук маалымат + Ачуу + Ырдын параметрлери жүктөлүүдө… + Ырдын параметрлери жеткиликсиз; кайра аракет кылыңыз + Ырдын параметрлерин ачуу мүмкүн болбоду + Jam Layer жүктөө үчүн браузер табылган жок + Хосттук кезекти күтүү + Алгач YouTube Music\'тен ырды ачыңыз + Хостто ойнотуу + Jam\'ден чыгуу жана жергиликтүү түрдө ойнотуу + Jam\'де кийинкисин ойнотуу + Jam кезегине кошуу + Бул Jam трегин ойнотуу + Каякта ойнотулушун тандаңыз + Хостту бул жерге көчүрүү + Ыр издөөгө даяр болгон жок + Кезек өзгөрдү; жестти кайра байкап көрүңүз + Башкы логотип + Негизги + Ыйыкталган + Альбомдун аудио тректарын ойнотуу + Музыкалык видеонун ордуна альбомдун ырларынын аудио версиясын гана ойнотуу. \'Видео агымдарын имитациялоо\' функциясы талап кылынат + Аудио/видео которууну күйгүзүү + Аудио трегинин версиясын жана расмий музыкалык видеону алмаштырып көрүңүз + Үчүнчү тараптын жүктөө колдонмолору менен музыкалык клиптерди, подкасттарды же ырларды жүктөп алуу + «Жүктөө» менюсу үчүн тышкы жүктөөчү колдонмосун колдонуу + Менюлардын «Жүктөө» опциясы анын ордуна сиздин тышкы жүктөөчү колдонмоңузду ачат + Жүктүрүүлөр + "Жүктүрүлгөн файлдар жок +Жүктүрүлгөн тректер ушул жерде пайда болот" + %1$s • %2$s • %3$s МБ + %1$s трек • %2$s МБ + Жүктүрүлгөн файлдарды издөө + Сортоо + Артист боюнча сүзүү + Аталышы боюнча сүзүү + Көлөмү боюнча сүзүү + Бардык жүктүрүлгөн файлдарды өчүрүү + Бардык жүктүрүлгөн файлдарды өчүрөсүзбү? + Бул түзүлүштөн бардык жүктүрүлгөн тректер өчүрүлөт + Жүктүрүлгөн файлдар өчүрүлдү + Эч нерсе табылган жок + %s үчүн аракеттер + Ойнотуу + Ойнотуу же токтотуу + Мурунку + Кийинки + Токтотуу + Жабуу + Өчүрүү + Жүктүрүлгөн файлды өчүрүү + Жүктүрүлгөн файлды өчүрөсүзбү? + %s бул түзмөктөн өчүрүлөт + Жүктүрүлгөн файл өчүрүлдү + Жүктүрүлгөн файлды өчүрүү мүмкүн болгон жок + Жүктүрүлгөн файлды ойнотуу мүмкүн болгон жок + Аудио файлдарды жүктөө + Офлайн ойнотуу + Жүктөө үчүн трек аныкталган жок + Жүктөө учурда жүрүп жатат + Жүктөөлгөн + Жүктөө башталды + Жүктөө башталууда… + Жүктөлүү %s%% + Жүктөө tamamдалды + Жүктөө катасы + Маалымат + Жарнама + Лента Жалпы + Плеер + Сөздөрү Майда-чүйдө + YouTube Music тили + Системалык тил + Android 13 же андан кийинки версиясы талап кылынат + \'Music Premium алуу\' жапкычын жашыруу + Music Premium жарнамаларын жашыруу + Видео жарнамаларын жашыруу + Аудио/Видео которуучусун жашыруу + Жүктөө баскычын жашыруу + Подкаст шоусунда Жүктөө баскычын жашырат + Explore текшерин жашыруу + Фильтр панелин жашыруу + Торчо текшерлерин жашыруу + Горизонталдуу текшерлерди жашыруу + Тизме текшерлерин жашыруу + Лирика панели + Бөлүшүү баскычын жашыруу + Котормо баскычын жашыруу + \"Жаңылыктары\" текшерин жашыруу + Плейлист текшерлерин жашыруу + Кайталоо баскычын жашыруу + Аралаштыруу баскычын жашыруу + \"Тез чалуу\" текшесин жашыруу Пикирлер + Комментарийлер бөлүмүндө кайсы компоненттер көрүнөрин тандаңыз + Жамааттык эрежелерди жашыруу + Контексттик билдирүүнү жашыруу + Эмодзи баскычын жашыруу + Маалымат баскычын жашыруу + Убакыт белгиси баскычын жашыруу + Кросфейд + Тректерди бири-бирине жумшак өткөрүү + Кросфейдди күйтүү + Фейд ийрилиги + Тең кубаттуулук + Жумшак кармоо + Жумшак өтүш + Түз S-ийрилиги + Чыгуучу + Кирүүчү + Кросфейд узактыгы + 250 миллисекунд + 500 миллисекунд + 750 миллисекунда + {\"one\":\"1 секунд\",\"other\":\"{} секунд\"} + 2 секунд + 3 секунд + 4 секунд + 5 секунд + 6 секунд + 7 секунд + 8 секунд + 9 секунд + 10 секунд + 11 секунд + 12 секунд + Колдонуучу өткөргөндө кроссфейд + Кийинки трекке кол менен өткөндө кроссфейд эффектин колдонот + Автоматтык түрдө өткөргөндө кроссфейд + Трек табигый түрдө аяктаганда кроссфейд эффектин колдонот + Сессияны башкаруу + Кроссфейдди токтотуу же кайра жандандыруу үчүн аралаштыруу баскычын узак басыңыз + Бул сессия үчүн кроссфейд токтотулду + Кроссфейд кайра жанданды + Кроссфейд күйгүзүлгөндө видео режими жеткиликсиз болот + SABR ойнотуусунда кроссфейд өчүрүлгөн + Трансляция учурунда кроссфейд өчүрүлгөн + Ал кандай иштейт + "Crossfade бир ырдын аягын кийинкисинин башталышы менен сиздин узактыгыңызды жана ийрилигиңизди колдонуп кайчитып өткөрөт + +Ал автоматтык түрдө өтүүдө жана кол менен өткөрүүдө иштейт, ар биринин өзүнүн которуучусу бар. Автоматтык түрдө өтүүдө аралаштыруу ыр бүтөргөнгө бир нече секунд калганда башталат" + Чектөөлөр + "Кроссфейд күйүк болгондо музыкалык видеолор аудио катары гана ойнотулат. Кроссфейдди токтотуп, видеону көрүү үчүн аралаштырууну узак басыңыз + +Ойнотула баштаган музыкалык видео кийинки ырга чейин видео режиминде калат + +Узактыгы кроссфейд убактысынан кыска ырлар кроссфейдсиз ойнотулат + +Бөгөттөөчү экран же билдирүү өткөрүү учурунда кыскача эски абалды көрсөтүшү мүмкүн + +Музыкалык видеону көрүү үчүн кроссфейдди токтотуу кээ бир видеолорду кара кылып коюшу мүмкүн. Бул жагдайды болтурбоо үчүн ырды тандоодон мурун кроссфейдди токтотуңуз" + Колдонулбайт + "Аудиону трансляциялоо же күзгүлөө (Chromecast, экранды же аудиону күзгүлөө) + +SABR ойнотуусу (Видео агымдарын имитациялоо өчүрүлгөн)" + Авторлор + VazerOG тарабынан жасалган Crossfade функциясы + Кайталоо абалын эстеп калуу + Аралаштыруу абалын эстеп калуу + Оynтуу ылдамдыгы + Бардык тректер үчүн колдонулган ылдамдык. 100% болгондо колдонмонун ылдамдыгы, мисалы, подкастты ойнотуу ылдамдыгы колдонулат + Ылдамдык менен тембрди өзгөртүү + Жакпайт кайра багыттоосун өчүрүү + \"Жакпайт\" баскычы басылганда кийинки трекке өтүүнү алдын алат + Плейлисттин трегин басуу аракетти + Плейлистти ойнотуу + Тандалган тректи гана ойнотуу + Тандалган тректи кезекке кошуу + Кезекке кошулду + Cast баскычын жашыруу + Тарых баскычын жашыруу + Билдирүү баскычын жашыруу + Издөө баскычын жашыруу + Үн менен издөө баскычын жашыруу + Үн күйгүнү издөө баскычын жашыруу + Жаңы баскычын жашыруу + Китепкана өтмөгүндөгү Жаңы баскычын жашырат + Аракет баскычтары + Плеердин астында кайсы аракет баскычтары көрүнөөрүн тандаңыз + Action-барды жашыруу + Жакты жана Жактырбадым баскычтарын жашыруу + Комментарийлер баскычын жашыруу + \"Түз эфирдеги чаттын кайталануусун\" баскычын жашыруу + Текстти жашыруу баскычын жашыруу + Бөлүшүү баскычын жашыруу + Сактоо баскычын жашыруу + Кыжырсыз жүктөө баскычын жашыруу + \"Радиону başlatуу\" баскычын жашыруу + Чыга турган меню + Плеердин жана кезектин чыга турган менюларында кайсы компоненттер көрүнөрүн тандаңыз + 3 маалымдуу компонентти жашыруу + Жакты жана Жакпады баскычтарын жашыруу + \"Табитиңизге ылайык» менюсун жашыруу + \"Кийинчерээк угуу\" плейлистине кошуу менюсун жашыруу + \"Кезекке кошуу\" менюсун жашыруу + Жапкаларга менюсун жашыруу + \"Плейлистти өчүрүү\" менюсун жашыруу + \"Кезекти тазалоо\" менюсун жашыруу + \"Бул артистти сунуштабаңыз\" менюсун жашыруу + Кыжылдатуу менюсун жашыруу + \"Плейлистти түзөтүү\" менюсун жашыруу + \"Альбомго өтүү\" менюсун жашыруу + \"Артистке өтүү\" менюсун жашыруу + \"Эпизодго өтүү\" менюсун жашыруу + \"Подкастко өтүү\" менюсун жашыруу + Жардам менюсун жашыруу + \"Эпизодду угалган деп белгилөө\" менюсун жашыруу + \"Кызыксыз\" менюсун жашыруу + \"Тез чалууга бекитүү\" менюсун жашыруу + \"Кийинкисин ойнотуу\" менюсун жашыруу + Сапат менюсун жашыруу + \"Китепканадан өчүрүү\" менюсун жашыруу + \"Плейлисттен өчүрүү\" менюсун жашыруу + Отчёт менюсун жашыруу + \'Эпизодду кийинчерээк сактоо\' / \'Китепкана сактоо\' менюсун жашыруу + \'Плейлистке сактоо\' менюсун жашыруу + Бөлүшүү менюсун жашыруу + \'Аралаш ойнотуу\' менюсун жашыруу + \'Уйку таймери\' менюсун жашыруу + \'Радиону başlatуу\' менюсун жашыруу + \'Техникалык маалыматтар\' менюсун жашыруу + Жазылуу менюсун жашыруу + \'Тез чалуулардан алып салүү\' менюсун жашыруу + \'Ырдын авторлорун көрүү\' менюсун жашыруу + Баштапкы бет + Негизги + Чарты + Кийинчерээк угуу үчүн эпизоддор + Издөө + Тарых + Китепкана + Жактырылган музыка + Плейлисттер + Издөө + Жазылуулар + Портрет багытын мажбурлоо + Түзүлүш айландырылганда колдонмону портрет багытында кармайт + Миниплеердин түсүн өзгөртүү + Мини-плеердин түсүн толук экрандуу плеерге ылайыктайт + Навигация панели түсүн өзгөртүү + Навигация панелинин түсүн толук экрандуу плеерге ылайыкташтырат + Миниплеерде \"Кийинки\" баскычын көрсөтүү + Миниплеерде \"Мурунку\" баскычын көрсөтүү + Мажбурланган миниплеерди күйгүзүү + Жаңы трек башталганда плеердин автоматтык түрдө толук экранга кеңейттилишин алдын алат + Мини-плеерди сыдырып жабууну күйгүзүү + Миниплеерди төмөн карай сыйпатып жабууга мүмкүндүк берет + Навигациялык панель + Төмөнкү навигациялык панелде кайсы баскычтар көрүнөрүн тандаңыз Башкы бет баскычын жашыруу + Үлгүлөр баскычын жашыруу + Издөө баскычын жашыруу + Китепкана баскычын жашыруу + Жаңыртуу баскычын жашыруу + Навигациялык панелди жашыруу + Навигация баскычтарынын жапкычтарын жашыруу + Кирбеген. Конфигурациялоо үчүн басыңыз + Чыгуу + Кирүүдөн чыгылды + Scrobbling функциясын күйгүзүү + \'Азыр ойноп жатат\' статусун жөнөтүү + Ырдын минималдуу узактыгы + Бул убакыттан кыска ырлар scrobble болбойт + Scrobble кечигүү пайызы + Ырдын бул пайызы угарылган соң scrobble кылууну баштоо + Scrobble кечигүү секундтары + Ушулчалык секунд угандан кийин scrobble кылууну иштетүү + ListenBrainz аккаунту + Кирилди. Аккаунтту башкаруу үчүн басыңыз + Scrobbling функциясын иштетүү үчүн ListenBrainz колдонуучу токенин киргизиңиз + Токен + Тастыктоо + Токенди киргизиңиз + Текшерилүүдө… + Туура токен! Колдонуучу: %s + Туура эмес токен: %s + белгисиз + Верификация таптак болду: %s + Токен тазаланды + Токен сакталды + Эскертүү: Сакталган токен жарамсыз! + Жактырылган тректерди сүйүү + YouTube Music\'те тректерди жактырганда, аларды Last.fm\'де автоматтуу түрдө «сүйүү» (жана жактырууну алып салганда же жактырбаганда «сүйүүсүн» алып салуу) + Last.fm аккаунту + %s катары кирдиңиз. Аккаунтту башкаруу үчүн басыңыз + Кирүү үчүн Last.fm маалыматтарыңызды киргизиңиз + Колдонуучунун аты + Сыр сөз + Кирүү + Сураныч, колдонуучунун атын жана сыр сөздү киргизиңиз + Кирүү жүрүп жатат… + Ийгиликтүү кирдиңиз! + Last.fm сайтынан сессияны алуу мүмкүн болгон жок + Кирүү катасы: %s + Last.fm маалыматтары сакталды! + Метамаалыматтарды тазалоо + Метамаалыматтарды тазалоо + Scrobbling кылардан мурун аталыштардан, артисттерден жана альбомдордон көп кездешкен ашыкча маалыматтарды (мисалы, \"Official Video\", \"Remaster\", \"Live\") автоматтык түрдө тазалоо + Custom regex тазалоо + Ырлардын аталыштарынан, артисттерден же альбомдордон кошумча текстти алып салуу үчүн өзүңүздүн регулярдык туюктуулугуңузду киргизиңиз. Мисалы: (?i)\\s*\\((clean|explicit)\\) + Аткаруучуну жана тректи аталышынан анализдөө + Видеонун аталышынан \" - \" бөлүүчүсүн колдонуп артисттин жана тректин атын бөлүп алыңыз. Бул каналдын аты артист катары колдонулган кайра жүктөлгөн контент үчүн пайдалуу + Жеткилбеген альбомду толтуруу + Музыкада альбом көрсөтүлбөгөн учурда, scrobble толук болушу үчүн аны Last.fm сайтынан издеп тапсын + "Scrobbling сиздин YouTube Music колдонууңузду Last.fm же ListenBrainz профилиңиз менен автоматтык түрдө синхрондойт + +Фондук ойнотуу учурунда scrobbling синхрондолгонун камсыз кылуу үчүн, YouTube Music колдонмосунун батареяны колдонуу жөндөөлөрү \"Чектөөсүз\" деп коюлганын текшериңиз" + Үчүнчү тараптын тексттерин колдонуу + Үчүнчү тараптын кызматынан тексттерди көрсөтөт, эгерде табылмаса, орнотулган тексттерге кайтарылат + Текстти көрүп жатканда экранды күйгүзүп кармоо + Текст панели ачык болгондо экрандын өчүп калуусуна жол бербейт + Тексттер кызматы + Көрсөтүү + Синхрондоштуруу + Панель + AI чекити + Бардык камсыздоочулар өчүрүлгөн - камтылган тексттер жана киргизилген тексттер + Провайдерлердин тартибин кайра орнотуу? + Провайдерлерди стандарттык приоритеттүү тартипке келтирүү + Сакталган ырларды тазалоо + Бул түзүлүштө сакталган ыр сөздөрүн өчүрүү + %1$s МБ тазаланды + Токенди алуу + Apple Music Media-user-token + "Paste the media-user-token cookie from music.apple.com + +Steps: +1. Sign in at music.apple.com as a member user +2. Open DevTools → Application → Cookies → music.apple.com +3. Copy the value of \"media-user-token\"" + Apple Music: media-user-token сакталды + Apple Music: media-user-token тазаланды + Apple Music: жарамсыз media-user-token - текшерип, кайра аракет кылып көрүңүз + Spotify SpDc + "Spotify сайтындагы sp_dc кукисин чаптаңыз. + +Кадамдар: +1. open.spotify.com сайтына киргиңиз +2. DevTools → Application → Cookies → open.spotify.com бөлүмүн ачыңыз +3. \"sp_dc\" маанисин көчүрүңүз" + Spotify: sp_dc сакталды + Spotify: sp_dc тазаланды + Spotify: жарамсыз sp_dc - текшерип, кайра аракет кылып көрүңүз + Apple Music токенир жайылбас. Ал жок болсо, Lyrically резервдик вариант катары колдонулат + Deezer ARL + "Deezerден arl cookie'син чаптаңыз. + +Кадамдар: +1. deezer.com сайтына кириңиз +2. DevTools → Application → Cookies → deezer.com бөлүмүн ачыңыз +3. \"arl\" маанисин көчүрүңүз" + Deezer: arl сакталды + Deezer: arl тазаланды + Deezer: жарамсыз arl - текшерип, кайра аракет кылып көрүңүз + Musixmatch токени + Өзүңүздүн аккаунтуңузду колдонуу үчүн гана токенди кошуңуз. Ал Musixmatch Android тиркемесинен алынышы керек: musixmatch.com сайтынан көчүрүлгөн токендер эскирген компьютердик клиентке таандык жана API аларды четке кагат + Musixmatch токенсиз да иштейт, ошондуктан бул талаа бош калышы мүмкүн + колдонуучунун токени + Musixmatch: колдонуучунун токени сакталды + Musixmatch: колдонуучунун токени тазаланды + Musixmatch: жарамсыз колдонуучу токени - текшерип, кайра аракет кылып көрүңүз + YouTube кукилери + "YouTube'дан субтитрлерди жана убакыт тексттерин суроо үчүн YouTube кукилерин чаптаңыз. + +Кадамдар: +1. Браузерден youtube.com сайтын ачыңыз жана кириңиз +2. DevTools (F12) → Application → Cookies → youtube.com бөлүмүн ачыңыз +3. Толук куки сабын көчүрүңүз (чекитүү белгилери менен бөлүнгөн ачкыч=маани жуптары) + +Керектүү кукилер: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPISID, LOGIN_INFO" + YouTube: кукилер сакталды + YouTube: кукилер тазаланды + YouTube: куки-файлдар жарамсыз - сураныч, текшерип, кайра аракет кылыңыз + Сапты басып кармаңыз + Ырдын ошол учуруна өтүү үчүн сапты басыңыз, сөздү көчүрүү үчүн аны узак басыңыз же, эгер басуу сөздүн үстүндө болбосо, бүтүндөй сапты көчүрүңүз. Өчүрүү эки жестти тең алып салат + Бөлүм тегдерин көрсөтүү + Лириканын ар бир жаңы бөлүмүн Verse же Chorus сыяктуу кичинекей тег менен белгилөө + Көчүрүү баскычын көрсөтүү + Түп нускадагы ыр сөздөрүн жөнөкөй текст катары көчүрүү үчүн басыңыз; шире файлды «Жүктүүлөр» папкасына сактоо үчүн узак басыңыз + Котормо баскычын көрсөтүү + Котормону күйгүзүү/өчүрүү үчүн басыңыз; бир гана котормону көрүү үчүн узак басыңыз. Провайдерлердин котормолорун, Google Translate же OpenAI-га шайкеш келген API колдонулат + Романизация баскычын көрсөтүү + Романизацияны күйгүзүү/өчүрүү үчүн басыңыз; романизацияны гана көрсөтүү үчүн узак басыңыз. Провайдердин романизациясын, Google Translate же OpenAI-га шайкеш келген API колдонулат + Жергиликтүү романизация жана тегирөөлөө + Провайдер, сиздин моделиңиз жана Google Translate иштебей калганда, түзүлүштө романизациялоо. Окуу убактысы белгиленген ырдын ар бир сөзүнөн кийин келет; сөздөрдүн синхрондошуусу күйүк болушу керек. Кытай, кантон, жапон, корей, грек, орус, грузин, армян тилдери; болжол менен 120 МБ + Окулуп жатат %1$s: %2$d%% + Жүктөө катасы - кайра аракет кылуу үчүн аны өчүрүп, кайра күйгүзүңүз + Жаңыртуу баскычын көрсөтүү + Кийинки ыр варианттарына өтүү үчүн басыңыз; ырдын аталышы жана artisti боюнча издөө үчүн узак басыңыз + Сөздөрдү издөө + Ырдын аталышы + Артист + Ырдын аталышын жана artistiнин атын жазыңыз + Иштетилген провайдерлердин эч бири башка ырларды издей албайт + Сакталды + Синхрондолгон сөздөрдү же муунду белгилөө + Мүмкүн болгон учурда сөз сайын берилген тексттерге артыкчылык бериңиз. Тексттерде сөздөрдүн убактысы көрсөтүлгөн учурда, ар бир сөз же муун музыка менен бирге белгиленет + Аткарылган тексттерди жашыруу + Учурда ойноп жаткан саптан жогорудагы текст саптарын жашыруу + Аткарылбаган тексттерди жашыруу + Учурда ойноп жаткан саптан төмөндөгү текст саптарын жашыруу + Бөгөттүү экранда MediaSession текстинин сөздөрүн көрсөтүү + Учурдагы сапты кулпулоо экранында, Android Auto жана Bluetooth\'та көрсөтөт. Android 11 же андан жаңыраак версиясы талап кылынат + Миниплеердин сөздөрүн көрсөтүү + Учурдагы сапты миниплеерде көрсөтөт, ал эми трек субтитрге жылдырылат + Ырдын аталышынын/аткаруучусунун форматы + Миниплеерде жана MediaSession\'да ырдын тексти менен көрсөтүлүүчү тректин форматы. {t} — бул аталышы, {a} — аткаруучусу жана {v} — версиясы, мисалы, Acoustic, эгерде тексттин булагы аны берсе. Эки чекиттин ортосундагы текст андагы бардык өзгөрмөлөр мааниге ээ болгондо гана көрсөтүлөт; жөнөкөй чекит үчүн \\: деп жазыңыз. Ар бир сап кезеги менен көрсөтүлүп, текст өзгөргөндө кийинкисине өтөт + Тексттин сабынын форматы + Миниплеердеги жана MediaSession\'деги ыр саптарынын форматы. {lrc} — бул сап, ал эми {n} — анын катарынан канча жолу ырдалганы. Колчектен кийинки текст сап кайталанганда гана көрсөтүлөт. Колчектен үчүн \\: белгисин колдонуңуз + Формат {lrc} белгисин камтышы керек + Артист-сап шаблону жана анимациянын убактысы + Экрандагы минималдуу убакыт (мс) + Экрандагы максималдуу убакыт (мс) + Көп саптуу анимациялар үчүн гана. Ар бир кадрды кеминде минималдуу убакыт бою сактаңыз. Максималдуу убакытта, ыр сабы өзгөрбөсө да, артист сабын алдыга жылдырыңыз. + Минималдуу маани 0 же андан көп болушу керек. Максималдуу маани 0дөн көп жана кеминде минималдуу мааниден жогору болушу керек. + Саптын узундугунун чектөөлөрү + Лириканын жана артисттин саптарынын чектөөлөрү, ошондой эле толук тууралуу белгилердин салмагы + "Лирика сабынын лимити +Узун лирикаларды тең салмактуу экиге бөлүңүз. 0 бөлүүнү өчүрөт." + "Артист сабынын лимити +Толтурулган трек форматын циклдүү фреймдерге бөлүңүз. 0 бөлүүнү өчүрөт." + "Толук тууралуу белгинин салмагы +Эки лимитке тең колдонулат. 1.75 болгондо, төрт толук тууралуу белги 7 деп эсептелет." + 0дөн 10000га чейинки бүтүн сандуу лимиттерди жана 0дөн чоң белги салмагын киргизиңиз + Инструменталдык пауза + Инструменталдык тыныгуу учурунда Миниплеерде жана MediaSession\'да көрсөтүлөт. Ар бир сап бир анимациялык кадр болуп саналат. {t} - бул аталышы, {a} - artisti жана {lrc} - акыркы лирика сабы + Миниплеердин жана MediaSession\'дин убакыттык четтөөсү + Кийинки сап Миниплеерде жана MediaSession\'да качан көрсөтүлөрүн жылдырат. Эгер саптар кеч чыкса, мисалы, кулпулоо экранында, терс маанини колдонуңуз + Фильтрлер + Метамаалыматтын regexи + Тексттерди издөөдө жана эки аталыштын окшоштугун баалоодо аталыштарга, артисттерге жана альбомдорго колдонулган регулярдуу туюнтма. Эки функцияны тең өчүрүү үчүн бош калтырыңыз. Сыңыр боюнча \"Official Video\", \"Remaster\", \"【插曲】\" жана \"- Topic\" сыяктуу көп кездешкен ашыкча маалыматтар алынат + Сөздөрдүн фильтринин regexи + Текстин oorsнолгон сөздөргө гана колдонулуучу регулярдуу туюнтма (котормолоруна же романизациясына эмес). Өчүрүү үчүн бош калтырыңыз + Маалымат саптарынын маркерлери + Эгерде сөздөр төмөнкү bahagianа ката фильтрленсе, муну жөнгө салыңыз же тазалаңыз. Үтүр менен бөлүнгөн маркерлер: маркерден кийин кош чекит коюлган саптар төмөндө кредиттик маалымат катары көрсөтүлөт. Өчүрүү үчүн бош калтырыңыз + Биринчи тамганы чоң тамга менен жазуу + Лириканын ар бир сабынын биринчи тамгасын чоң тамга менен жазат + Аягындагы пунктуация белгилерин алып салуу + Лрика саптарынын аягындагы чекиттерди, үтүрлөрдү, эки чекиттерди жана сызыкчаларды алып салат. Суроо белгилери, удилуу белгилери, көп чекиттер, тырмакчалар жана кашаалар сакталат + Тексттин өлчөмү + Тексттин өлчөмү + Жалпы убакыттык оффсет + Синхрондолгон тексттер ойнотууну ээлейт. Эгерде алар дал келбесе, убакыттык четтөөлөрдү жөнгө салыңыз. Синхрондолгон тексттерди алдыга же артка жылдырыңыз. Эгерде текст ырдан алдыда жүрсө, оң сандуу маанини колдонуңуз + Лирика оффсети үчүн SponsorBlock сегменттерин колдонуу + SponsorBlock өткөрүп жиберген сегменттерсиз далдоо жана лириканы чыгарылган аудио менен синхрондоо. Өчүрүлгөн учурда SponsorBlock толугу менен эске алынбайт + Бул тректе жөнөтүү үчүн видео ID жок + Булагы: %s + Бул LRC файлын сактоо үчүн көчүрүү баскычын узак басыңыз; эгер сиз убактысын тактап, аны коомдук провайдерлер менен бөлүшө алсаңыз, абдан ыраазы болобуз + Google тарабынан которулган + Google тарабынан романизацияланган + %s тарабынан которулган + %s тарабынан романизацияланган + Романизациялоону %s жасаган + Которулган + Котормонун тили + Романизация тили + Которууда… + Текстти которуу мүмкүн болгон жок + Романизацияланган Өзүнөн өзү + Романизацияланууда… + Текстти романизациялоо мүмкүн болгон жок + Котормосу гана + Тек гана романизация + Кош режим + Жалпы алмаштырып коюучу буферге көчүрүлдү + Жаңыртылууда + Жаңыртылды + QuickLRC API ачкычы + QuickLRC учурдагы трек үчүн ырлардын тексттерин транскрипциялайт же убакытка ылайыктайт. Бул жогорудагы OpenAI-га шайкеш чекиттен көз карандысыз, өзүнчө кызмат + QuickLRC ачкычы сакталды. Ырларды издөө диалогиндагы ылайыктоо баскычын же тректе ырлар жок болгондо панель сунушун колдонуңуз + QuickLRC API ачкычы сакталды + QuickLRC бул ачкычты кабыл алган жок + QuickLRC API ачкычы тазаланды + QuickLRC кредиттердин эсебинен убакыты жок ырларды убакытка ылайыктайт же аларды тректен транскрипциялайт. Ачкыч орнотулганда, ырлары жок тректер транскрипциялоону сунуштайт жана издөө диалогина ылайыктоо баскычы кошулат + QuickLRC тегиздикке келтирүүнү суроо + Тегизделүүдө + Тегиздикти колдонуу + QuickLRC транскрипциясын суроо + Транскрипция жазылууда + Транскрипция токтотулду + Транскрипциялоо үчүн YouTube\'дан ойнотулган тректер гана колдосо болот + Ылайыктай турган ырлар жок - ордуна транскрипцияланууда + QuickLRC бул тректи иштете алган жок + Бул трек үчүн QuickLRC кредиттери түгөндү + "Тексттер аталышы жана artisti боюнча дал келиştirүү аркылуу табылат, alış असामान्य метамаалыматтар эч нерсе таппай калышы мүмкүн же ката версияны көрсөтүшү мүмкүн. Версияларды алмаштыруу үчүн жаңыртууну колдонуңуз. * белгиси менен белгиленген провайдерлер үчүн токен талап кылынат. Иштетилген бардык провайдерлер дайыма сиздин тартибиңиз менен суралып турбайт, бирок алардын ылдамдыгына жараша жооп беришет.\n\nMатндуу ырлар Morphe компаниясына байланыштуу эмес, дем салбаган же колдобогон үчүнчү тараптын кызматтарынан келет. Ар бири кандай болсо ошондой колдонулат: ырлардын тексти, алардын жеткиликтүүлүгү жана лицензиялары ошол кызматтарда жана алардын лицензиарларында калат, ал эми аларды колдонуу алардын кызмат көрсөтүү шарттарына баш ийүү мүмкүнчүлүгүнө ээ. Иштетилген кызматка сиз сураган издөө гана жетет - аталышы, artisti жана ошол кызматка керек болгон жерде видеонун IDси. Кошумча AI чекитти жана QuickLRC транскрипциясы сиз аларды конфигурациялагандан кийин гана иштейт жана ырлардын текстин же тректин видео URL-дарегин алат." + Сиз колдонгон провайдерлерди гана активдештириңиз же стандарттуу жөндөөлөрдү калтырыңыз - туруктуу суроо-талаптар кичинекей коомдук ырлардын сайттарын overloaded кылышы мүмкүн + Кредиттик сапты жашыруу + Тексттин төмөнүндө көрсөтүлгөн кредиттик сапты жашыруу + Котормо менен романизацияны алмаштыруу + ON романизацияны ырлардын төмөнүнө, ал эми котормону жогорусуна жайгаштырат; OFF романизацияны жогоруда, котормону төмөндө калтырат + Котормо + Романизация + Котормо провайдери + Google (Негизги, акысыз) + OpenAI менен шайкеш (Custom) + Романизациялоо камсыздоочусу + Google (Негизги) + Жергиликтүү / Офлайн сөздүк + OpenAI менен шайкеш (Custom) + AI бөлүмү + Провайдер Кайырма же Куплетки сыяктуу бөлүмдөрдүн белгилерин жөнөтпөгөндө жана сиздин OpenAI- менен шайкеш моделиңиз аларды аныктаганда гана колдонулат + AI & Cloud Endpoints + ЖИ sağlayıcıлары үчүн API ачкычтарын, моделдерин, чекиттерин жана промпт шаблондорун жөндөңүз. + API чекитинин URL дареги (мисалы, https://api.openai.com/v1/chat/completions) + Промпт шаблону + Плейсхолдерлер: {language}, {task}, {title}, {artist}, {count}, {lines} + Промпт сакталды + Сунуш сакталды, бирок AI чекити али колдонулуучу эмес. Алдында аны AI жөндөөлөрүнөн конфигурациялаңыз + Промпт кайрылды + JSON жарамдуу эмес. Сактоодон мурун аны оңдоңуз же талааны тазалаңыз + Котормо & романизация жөндөөлөрү сакталды + Жөндөөлөр стандарттуу абалга кайтарылды + Негизги URL http:// же https:// менен башталышы керек + Байланышты текшерүү + Чекиттерди конфигурациялоо + Бардык чекиттерди көрсөтүү + Байланыш текшерилүүдө… + Котормо sağlayучу + Романизация sağlayучу + Моделдерди алуу + Моделдер алынууда… + Моделди тандаңыз + Моделдерди алуу мүмкүн болгон жок. API ачкычын же байланышты текшериңиз. + Gemini API жөндөөлөрү + OpenAI менен шайкеш (Custom Endpoint) + DeepL API жөндөөлөрү + API ачкычы + Модель + Негизги URL + API ачкычы (факультативдүү) + Котормо суроосу + Кошумча суроо-талаптын денеси (факультативдүү JSON) + Аутентификациялоо ачкычы (Free же Pro) + API ачкычтарын, моделдерди, чекиттерди жана промптторду конфигурациялоо + 8.23.51 - Калың белгилерди өчүрүү + 9.13.51 - Плеердин \"Кийинкиси\", Лирика жана Байланыштуу өтмөктөрүн калыбына келтирүү diff --git a/patches/src/main/resources/addresources/values-ky-rKG/reddit/strings.xml b/patches/src/main/resources/addresources/values-ky-rKG/reddit/strings.xml index 82374e4fb0..8ce37ce67b 100644 --- a/patches/src/main/resources/addresources/values-ky-rKG/reddit/strings.xml +++ b/patches/src/main/resources/addresources/values-ky-rKG/reddit/strings.xml @@ -16,8 +16,20 @@ Second \"item\" text" --> + Reddit\'тин купуялуулук саясаты + Google аркылуу кирүү мүмкүн эмес. Кирүү үчүн электрондук почтаны колдонуңуз Жарнама Жасоо баскычын жашыруу + Custom шрифт + Төмөнкү жолдон custom TTF же OTF шрифт файлын жүктөңүз. Бул функцияны күйгүзүү Системалык шрифтти мажбурлоону өчүрөт + Custom шрифт файлы + TTF же OTF шрифт файлын тандоо үчүн басыңыз + Тандалган шрифт - %s + Алгач custom шрифт файлын тандаңыз + Сураныч, .ttf же .otf шрифт файлын тандаңыз + Custom шрифтти жүктөө мүмкүн болгон жок. Башка файлды тандаңыз + Системалык шрифтти мажбурлоо + Reddit Sans же Roboto ордуна түзмөктүн системалык шрифтин колдонуңуз. Бул функцияны күйгүзүү Custom шрифтти өчүрөт Майда-чүйдө Маалымат diff --git a/patches/src/main/resources/addresources/values-ky-rKG/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-ky-rKG/shared-youtube/strings.xml index 054a3d967c..09b2c35c5c 100644 --- a/patches/src/main/resources/addresources/values-ky-rKG/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ky-rKG/shared-youtube/strings.xml @@ -18,47 +18,294 @@ Second \"item\" text" --> + Ишке киргизүү анимациясы Иштен чыгарылган + Түсү + Монохромдук + + Өчүк + Орнотууну бүтүрүү үчүн кайра баштаңыз + Тиркеменин аталышы + Коңуру теманын фону + Таза кара + Material You (Таза кара) + Классикалык YouTube + Койу кызгылт + Койу көк + Койу жашыл + Койу сары + Койу кызгылт сары + Коюу кызыл + Custom кара фон + Кара теманын фон түсүн орнотуңуз + Тиркеменин стандарттуу жөндөөсү + Material You (Нейтралдуу) + Material You (Негизги) + Material You (Экинчи) + Material You (Үчүнчү) + Ыйыкталган + Жүктүүлөр + Жүктөө режими + Жалпы + Тиркеменин ичинде жүктөө + Сырткы жүктөөчү + Жүктөөчү пакеттин аталышы + Орнотулган тышкы жүктөөчү колдонмоңуздун пакеттин аталышы + Пакеттин атын киргизиңиз + Башка + Тиркеме орнотулган эмес + %s орнотулган эмес. Сураныч, аны орнотуңуз + "Пакеттин аталышы менен орнотулган тиркеме табылган жок: %s + +Пакеттин аталышы туура экенин жана тиркеме орнотулганын текшериңиз" + Пакеттин аты бош болбошу керек + Ыйыкталган + Тиркеменин иконкасы + Түп нускасы + Morphe кара + Morphe коңуру + Morphe ачык + Morphe play light + Morphe play black + Ыйыкталган + "Бул сиздин тиркеме иштеткичиңизде көрсөтүлгөн белгини өзгөртөт. + +Бул жөндөө төмөнкү жерлердеги белгини өзгөртпөйт: +• Түзмөктүн системалык жөндөөлөрүнүн тиркемелер тизмеси +• Кээ бир push-билдирүүлөр +• \"Ачуу» тиркеме тандоочусу +• Android Auto белгиси + +Белгини бардык жерде өзгөртүү үчүн, кайра патч жасаңыз жана \"Тиркеме белгиси\" патч опциясын өзгөртүңүз" + Билдирүүнүн белгиси + Тиркеменин белгисине ылайыкташтыруу + Morphe play + Morphe билдирүүсү + Сиздин көрүү тарыхыңыз сакталбай жатат.<br><br>Бул, кыязы, DNS жарнама бөгөттөөчүсү же тармактык прокси-серверден улам болушу мүмкүн.<br><br>Муну оңдоо үчүн <b>s.youtube.com</b> дарегин ак тизмеге кошуңуз же бардык DNS бөгөттөөчүлөрүн жана прокси-серверлерин өчүрүңүз + Кайра көрсөтпө + Видео агымдарын имитациялоо + Плеердин каталарын алдын алуу үчүн клиенттин имитациясын жөндөө + "Шайкеш видео агымын суроо менен көпчүлүк колдонуучулар үчүн ойнотуу көйгөйлөрүн оңдойт + +Эгер сиз YouTube Premium колдонуучусу болсоңуз, бул жөндөө талап кылынбашы мүмкүн" + Клиенттик стримдерди алуу мүмкүн болгон жок + Ыйык клиент иштебей жатат + "%1$s катар-катар бир нече жолу иштеген жок, бирок %2$s иштеди. + +%2$s клиентти ыйык кылуу керекпи?" + Алмаштыруу + Бул жөндөөнү өчүрүү ойнотууда көйгөйлөргө алып келиши мүмкүн + Активациялоону байкап көрүңүз: Видео стримдерди имитациялоо > Android VR кириш + Стандарттуу клиент + Android VR кириш + Android VR-ге кирген жоксуз. Кирүү үчүн бул жерди басыңыз + Учурда Android VR-ге киргенсиз + Бул функцияны колдонуу үчүн, \'Негизги клиентти\' Android VR-ге өзгөртүңүз + Android VR-ге кириңиз + "Android VR'ге кирүү айрым колдонуучулар үчүн ойнотуу көйгөйлөрүн чече алат + +Кирүү үчүн төмөндөгү \"улантуу\" баскычын басыңыз" + Чыгуу + Нускамалар + "Активдештирүү kodu: %s + +1. 'Веб-сайтты ачуу' баскычын басып, 'Түзмөктү туташтыруу' веб-сайтына өтүңүз +2. Жогоруда көрсөтүлгөн активдештирүү кодун алмаштыруу буферинен чаптаңыз (же кол менен жазыңыз) +3. Суралса, Google аккаунтуңузга кириңиз. Эгерде бир нече аккаунт болсо, ушул YouTube аккаунтун тандаңыз +4. 'YouTube VR'га уруксат берүү баскычын басыңыз +5. 'Түзмөк туташтырылды' деген билдирүү чыккандан кийин ушул колдонмого кайтыңыз" + Активдештирүү кодун алуу мүмкүн болгон жок + Авторизация токенин алуу мүмкүн болгон жок + Ийгиликтүү кирдиңиз + "Авторизация сакталды + +• Сиздин Morphe жөндөөлөрүңүз Android VR авторизациясын импорттобойт/экспорттобойт +• Эгер бул колдонмо 6 ай бою колдонулбаса, Google бул авторизацияны өчүрөт +• Эгер сиз Google сырсөзүн өзгөртсөңүз же бул түзмөктү Google жөндөөлөрүнөн алып салсаңыз, кайрадан кирүү керек болот" + OAuth2 убактылуу жеткиликсиз (API убактысы бүттү) + Android VR кирүү мөөнөтү бүттү, VR\'ге кайра кирүүнү аракет кылып көрүңүз + OAuth2 убактылуу жеткиликсиз (статусу %s) + OAuth2 убактылуу жеткиликсиз + VR\'ге кирүү мүмкүн эмес, веб-сайттын аутентификациясы бекитилген жок + VR\'ге кирүү мүмкүн эмес: %s + Android VR\'ден чыгылды + VR кирүү токени жарамсыз, VR\'ге кайра кирүүнү аракет кылып көрүңүз + Билдирүүнүн медиа башкаруулары + Бөгөттөөчү экранда жана билдирүүлөр панелинде көрсөтүлгөн ойнотуу башкарууларын ылайыктаңыз + \"Мурунку жана Кийинки баскычтарын жашыруу\" + \"Медиа билдирүүлөрүндө, бөгөттөөчү экранда жана кулакчын башкарууларында Мурунку жана Кийинки баскычтарын жашырат\" + Издөө тилкесин өчүрүү + Бөгөттөөчү экранда жана билдирүүлөр панелинде издөө тилкесин сүйрөөнү болтурбайт + \"Мурунку\" & \"Кийинки\" баскычтары менен издөө + Видеолорду алмаштыруунун ордуна издөө үчүн медиа билдирүүлөрүнүн, кулпуланган экрандын жана кулакчындын башкаруу элементтерин өзгөртөт + Кайтуу узундугу + \"Мурунку\" баскычы менен кайтуу секундтары + Алдыга жылдыруу узундугу + \"Кийинки\" баскычы менен алдыга жылдыруу секундтары + Издөө узундугу 1 жана %s секундтун ортосунда болушу керек. Жөнөкөй маани колдонулууда + QUIC протоколун өчүрүү + DRC аудиосун өчүрүү + Түпкү аудио тилин мажбурлоо + Дубляждалган версиянын ордуна ар дайым видеонун түпкү аудио жологун ойнотуу + Тыныгууларды өткөрүп жиберүү + Видео аудиосундагы унсуз тыныгууларды автоматтуу түрдө өткөрүп жиберет + Тыныгуунун минималдуу узундугу + Кеминде ушунча узалыктагы тыныгуулар гана кыскартылат. Кийинки видео башталганда күчүнө кирет + Тармак проксиси + Колдоого алынган тармактык суроо-талаптарды прокси аркылуу багыттаңыз + Проксиyi күйгүзүү + Маршруттар конфигурацияланган HTTP же HTTPS прокси аркылуу тармактык суроо-талаптарды колдойт + Прокси хосту + Прокси-сервердин хост аты же IP-дареги + Прокси-порт + Прокси-сервердин порту + HTTPS проксисин колдонуу + Прокси-серверге туташуу үчүн HTTP ордуна HTTPS колдонуу + Прокси-авторизация + Прокси-суроо-билдирүүлөрү менен бирге Basic авторизациясын жөнөтөт + Прокси колдонуучунун аты + Basic прокси аутентификациясы үчүн колдонуучунун аты + Прокси сырсөзү + Basic прокси аутентификациясы үчүн сырсөз + Түз байланышууга уруксат берүү + Эгерде прокси иштебей калса, түз байланышыңыз + Толук экрандуу жарнамаларды жашыруу + Бул функцияны колдонуу үчүн, \'Spoof video streams\' параметрин \'Android Studio\' жана \'Android VR Downgraded\' эмес каалаган клиентке өзгөртүңүз + Бөлүшүү шилтемелерин тазалоо + Бөлүшүлгөн шилтемелерден көзөмөлдөө параметрлерин алып салуу + Бөлүшүү шилтемелерин youtube.com сайтына өзгөртүү + Бөлүшүлгөн шилтемелерде music.youtube.com дарегин youtube.com менен алмаштырат + Бөлүшүү шилтемелерин youtu.be шилтемелерине алмаштыруу + Видеолорду кыскартылган youtu.be URL форматын колдонууга мажбурлайт + Debug лог протокол буфери + Иштеп чыгуучуларга колдонмонун компоненттерин аныктоо үчүн кошумча логгинг маалыматтарын кошот + "Бул жөндөөнү күйгүзүү айрым UI компоненттери үчүн экрандагы текстти кошто uključтап, кошумча макет маалыматтарын жаздырат. + +Бул custom фильтрлерди түзүүдө компоненттерди аныктоого жардам берет. + +Бирок, муну күйгүзүү сиздин IP дарегиңиз сыяктуу айрым колдонуучунун маалыматтарын да жаздырат" + Custom фильтр + Кандайдыр бир компонентти жашыруу үчүн custom фильтрлерди жөндөңүз + Custom фильтрди күйгүзүү + Сүзүү үчүн жаңы саптар менен бөлүнгөн компоненттин жолунун түзүүчү саптарынын тизмеси + Жарамсыз自定义 сүзгүч: %s + Жөндөөлөр менюсунун фильтри + Стандарттык тиркеме жөндөөлөрү экранындагы элементтерди алардын көрүнүктүү аталышы боюнча жашыруу + Тизмени толтуруу үчүн стандарттык тиркеме жөндөөлөрү экранын бир жолу ачыңыз + Көрүүчүлөрдүн ырайымана байланыштуу диалогду алып салуу + Бул жаш чектөөсүн айланып өтбөйт. Ал аны жөн гана автоматтык түрдө кабыл алат + PoToken провайдери + Плеердеги көйгөйлөрдү алдын алуу үчүн PoToken провайдерин конфигурациялаңыз + Сырткы PoToken провайдери + "Сырткы PoToken minter колдонуу менен ойнотуудагы көйгөйлөрдү оңдойт (Эксперименталдык) + +Эгерде сиз YouTube Premium колдонуучусу болсоңуз, бул жөндөө талап кылынбашы мүмкүн" + "Тышкы PoToken минтерин колдонуу менен ойнотуу көйгөйлөрүн оңдойт (Эксперименталык) + +Бул функцияны колдонуу үчүн, 'Видео агымдарын жасалмалоо' функциясын өчүрүңүз" + Эгерде иштебесе, MicroG тиркемесин мажбурлап жабууну байкап көрүңүз + PotHelper жөндөөлөрүн ачуу + "Бул функция учурда иштелип жатат жана иштебеши мүмкүн + +PotHelper тиркемесин жүктөө үчүн бул жерди басыңыз" + PotHelper эми иштебейт + "PotHelper'дин бул версиясына орнотулган токен эми иштебейт + +Плеерди оңдоо үчүн 'Видео агымдарын имитациялоо' функциясын күйгүзүңүз" + Күйгүзүү + Тиркеменин версиясын жасалгалоо + Эски UI функцияларын калыбына келтирүү үчүн серверди тиркеменин эски версиясын колдонуп жатканыңызга ишендирет + "Тиркеменин версиясы эски версияга окшотулат. + +Бул тиркеменин көрүнүшүн жана функцияларын өзгөртөт, бирок белгисиз терс таасирлер болушу мүмкүн. + +Кийин өчүрүлсө, интерфейстин каталарын алдын алуу үчүн тиркеменин маалыматтарын тазалоо сунушталат" + Тиркеменин версиясынын максатын имитациялоо + Тиркеменин жаңылануу ылдамдыгы + Жанаша + \"%s FPS\" + Жаңылануу ылдамдыгын алмаштыруу + Дайыма + Портрет режиминде ойнотуу + Толук экранда ойнотуу + Портреттик жана толук экрандуу ойнотуу + Шрифт + Негизги + Түзмөктүн системалык шрифти + Ыйыкталган шрифт файлы + Ыйыкталган шрифт файлы + TTF же OTF шрифт файлын тандаңыз + Тандалган файл TTF же OTF шрифти эмес + Custom шрифт жүктөлгөн жок жана өчүрүлдү + Жакпагандар убактылуу жеткиликсиз (API убактысы бүттү) + Жактырбоолор жеткиликсиз (статусу %s) + Жакпагандар жеткиликсиз (клиенттик API чектөөсү) + Жакпагандар жеткиликсиз (%s) + Ээси тарабынан жашырылган + {\"one\":\"Бул видеону сиз жана башка бир адам жактырган жок\",\"other\":\"Бул видеону сиз жана башка %s адам жактырган жок\"} + Бул видеону жактырбагандыр, добуштардын %s бөлүгү жактырбагандыгы + Return YouTube Dislike функциясын күйгүзүү + Жакпагандарды пайыз менен көрсөтүү + Жакпагандарды сан түрүндө эмес, жактыргандардын пайызы катары көрсөтөт + Болжолдуу лайктарды көрсөтүү + Автор лайктарды өчүрГөн видеолордо болжолдуу лайктардын санын көрсөтөт + Байланыш катасы боюнча эскертүүлөрдү көрсөтүү + Return YouTube Dislike сервери жеткиликсиз болгондо билдирүүнү көрсөтөт + "Return YouTube Dislike YouTube 2021-жылы жашырган жактырбагандордун санын калыбына келтирет. Сандар өчүрүлгөнгө чейин архивделген маалыматтардан алынат жана жаңы видеолор үчүн колдонуучулардын добуштары менен бааланат, ошондуктан алар болжолчо жана бир нече күндө бир жолу жаңыртылып турат + +Сиз көрүп жаткан видеонун IDси Return YouTube Dislike APIсине жөнөтүлөт, ал эми сиздин жактырууларыңыз жана жактырбагандарыңыз баалоолорду жакшыртуу үчүн анонимдүү түрдө жөнөтүлөт" + Бул түзүлүштүн Return YouTube Dislike API статистикасы + API жооп берүү убактысы, орточо + API жооп берүү убактысы, minimum + API жооп берүү убактысы, maximum + API жооп берүү убактысы, акыркы медиа + Жакпагандар убактылуу жеткиликсиз - Client API чектөөсү колдонулууда + API добуштарды алуу, чакыруулардын саны + Тармактык чакыруулар жасалган жок + API добуштарды алуу, убактысы бүткөндөрдүн саны + Тармак чакырууларынын убактысы бүткөн жок + API клиенттин чектөөлөрү + Клиенттин чектөөлөрү боюнча маселелер жок diff --git a/patches/src/main/resources/addresources/values-ky-rKG/shared/strings.xml b/patches/src/main/resources/addresources/values-ky-rKG/shared/strings.xml index 72d53be441..caf7331d2e 100644 --- a/patches/src/main/resources/addresources/values-ky-rKG/shared/strings.xml +++ b/patches/src/main/resources/addresources/values-ky-rKG/shared/strings.xml @@ -17,24 +17,218 @@ Second \"item\" text" --> + Оңдоолор + Улантууну каалайсызбы? + Сактоо + Калыбына келтирүү + Түстү калыбына келтир + Туура эмес түс + Кайра жүктөө талап кылынат + Бул өзгөртүүлөр күчүнө кириши үчүн колдонмону кайра başlatыңыз + Кайра жүктөө + Импорт + Көчүрүү + Morphe жөндөөлөрү стандарттуу абалга кайтарылды + %s жөндөөлөрү импортталды + Импорттоо катачылык кетти: %s + Жогоруга өтүү + Жөндөөлөрдү издөө Маалымат + \'%s\' үчүн жыйынтыктар табылган жок + Башка ачкыч сөздү колдонуп көрүңүз + Акыркы издөөлөр + Издөө тарыхынан өчүрөсүзбү? + Издөө тарыхынан өчүрүү + Издөө тарыхын тазалоо + Издөө тарыхынын баарын тазалоону каалайсызбы? + Издөө боюнча кеңештер + "• Жолго басып, ага өтүңүз +• Жөндөөлөрдү узак басып, ага өтүңүз +• Издөө суроосун тарыхка сактоо үчүн Enter баскычын басыңыз +• Издөө учурунда чоң-кичине тамгалар жана пунктуация эске алынбайт +• Ата-жөндөөлөр өчүрүлгөн балам-жөндөөлөрдүн жогору жагында көрүнөт" + Издөө тарыхы бош + Издөө тарыхын сактоо үчүн, издөө суроосун жазып, Enter баскычын басыңыз + Жөндөөлөрдүн издөө тарыхын көрсөтүү + Morphe жөндөөлөрүнүн аталышы + Негизги + Кошумчалар + Жөндөөлөр + Морфинг убактысы + Ыйыкталган + Атын киргизиңиз + Аты бош болбошу керек + Morphe жөндөөлөрүнүн белгилерин көрсөтүү + Көчүрүлдү: %s + Morphe тили + "Кээ бир тилдер үчүн котормолор жок болушу мүмкүн же толук эмес. + +Жаңы тилдерди которуу же mevcut котормолорду жакшыртуу үчүн morphe.software/translate сайтына кириңиз" + Тиркеменин тили + Импорт / Экспорт + Morphe жөндөөлөрүн импорттоо / экспорттоо + Файылдан импорттоо + Жөндөөлөр ийгиликтүү импорттолду, колдонуу үчүн «Сактоо» баскычын басыңыз + Жөндөөлөрдү импорттоо мүмкүн болгон жок + Файлга экспорттоо + Жөндөөлөр ийгиликтүү экспорттолду + Жөндөөлөрдү экспорттоо мүмкүн болгон жок + Сиз Morphe Patchesтин акыркы <i>%s</i> версиясын колдонуп жатасыз + Сиз Morphe Patchesтин <i>%1$s</i> версиясын колдонуп жатасыз<br><br><b>Жаңыртуу бар: <i>%2$s</i></b><br><br>Патчтарыңызды жаңыртуу үчүн, бул тиркемени кайра патчтөө үчүн Morphe колдонүңүз + Эскертүү + Бул версия алдын ала чыгарылган версия, ошондуктан күтүлбөгөн көйгөйлөр болушу мүмкүн + Расмий шилтемелер + Веб-сайт + Жардам бериңиз + Котормолор + Авторлор + Актуалдуу + Жаңыртуу бар + Учурдагы иштеп чыгуу + Мурунку иштеп чыгуу + Активдүү иштеп чыгуу + 2022 ‑ 2025 + 2022 ‑ 2025 + 2019 ‑ 2022 + Лицензиялык эскертүүлөр + Жүктөлүүдө… + Дебаггинг + Кеңейтилген разработчик опцияларын жана диагностикалык логдорду конфигурациялоо + Отладка логдорун күйгүзүү + Ката издөө журналынын стек изи + Каталарды аныктоого жардам берүү үчүн отладка логдоруна стек трассаларын кошуу + Morphe катасы болгондо билдирүүнү көрсөтүү + "Каталык билдирүүлөрүн өчүрүү бардык Morphe каталык билдирүүлөрүн жашырат. + +Сизге эч кандай күтүлбөгөн окуялар жөнүндө билдирилбейт" + Ката издөө журналдарын экспорттоо + Morphe ката издөө журналдарын экспорттоо + Ката издөө журналдары ийгиликтүү экспорттолду + Ката издөө журналдарын экспорттоо мүмкүн болбоду + Отладка логдорун издөө… + Тазалоо + Дебаг логирование өчүрүлгөн + Ката издөө журналдары табылган жок + Дебаг логдору көчүрүлдү + Дебаг логдорун экспорттоо мүмкүн болгон жок: %s + Дебаг логдору тазаланды + Функциялардын белгилерин башкаруучу + Булево функциялардын белгилерин башкаруу + Функциялык флагдарды издөө… + Активдүү эмес (%s) + Активдүү (%s) + Блоктолгон (%s) + Мөجبүрлөнгөн (%s) + Авто Өчүк Күйүк + Мөجبүрлөп күйтүү + Блоктоо + Өчүрүү + Функциялардын белгилери сакталды + Функциялардын белгилери кайтарылды + Функциялардын белгилери алмаштыруу буферine көчүрүлдү + Бул жөндөөнү колдонуу үчүн тиркемени кайра иштетиңиз + Экспорттоо жана импорттоо + Функциялардын эч бир белгиси табылган жок + Функциялардын белгилери ийгиликтүү экспорттолду + Функциялардын белгилерин экспорттоо мүмкүн болгон жок + Функциялардын белгилери жүктөлдү, колдонуу үчүн Сактоо баскычын басыңыз + Файлды окуу мүмкүн болгон жок + Бинарлык издөө + "Тиркеменин жүрүм-турумуна кайсы функция белгиси себеп болорун аныктайт. + +%s белгилердин жарымы бөгөттөлгөн, жана ар бир кайра жүктөөдөн кийин жүрүм-турум дагы деле сакталгандыгын айтасыз. + +Баштаганга чейин жүрүм-турумду бир жолу кайталаңыз, антпесе тиркеме али сурабаган белгилер издөөдөн калат" + Баштоо + "Издөө үчүн функциялардын белгилери жок. + +Бинардык издөөдө тиркеме колдонуп, учурда мажбурлап күйгүзүлбөгөн белгилер гана колдонулалат" + "Кадам %1$s + +%2$s белги калды, алардын %3$s бул кадам үчүн бөгөттөлгөн. + +Тиркемени кайра баштаңыз, жүрүм-турумдун бар экенин текшериңиз, андан кийин төмөндө жооп бериңиз" + Функциялардын белгилерин издөө жүрүп жатат + Жүрүм-турум бар + Жүрүм-турум жок + Бинардык издөөны жокко чыгаруу + Бинардык издөө жокко чыгарылды + "%s функциясынын белгиси жүрүм-турумга алып келет. + +Ал эми бөгөттөлдү" + "Функциянын белгиси табылган жок. + +Тиркеменин жүрүм-туруму функциянын белгиси менен башкарылбаган окшойт" + "Бир гана функциянын белгисин табуу мүмкүн болгон жок. + +Жүрүм-турум издөө учурунда эрте жоголгон, ошондуктан бирден көп белги катышы мүмкүн же жооптордун бири ката болгон" + MicroG жөндөөлөрүн ачуу + MicroG орнотулган эмес. Орнотуңуз + Аракет талап кылынат + "Сиздин MicroG v%1s версияңыз эскирген жана көйгөйлөрдү жаратышы мүмкүн. + +Сураныч, MicroG'ди акыркы v%2s версиясына чейин жаңыртыңыз" + Жаңыртуу + Эске албоо + "Орнотулган MicroG Morphe тарабынан сунушталбайт: + +%1s + +Аны өчүрүү үчүн басыңыз, андан кийин бул колдонмону кайра ачып, MicroG RE орнотуңуз" + Өчүрүү + "MicroG тиркемесинин фондо иштөөгө уруксаты жок. + +Телефонуңуз үчүн \"Don't kill my app\" колдонмосун караңыз жана көрсөтмөлөрдү MicroG орнотуусуна колдонуңуз. + +Бул тиркеменин иштеши үчүн зарыл" + Веб-сайтты ачуу + "Көйгөйлөрдүн алдын алуу үчүн MicroG үчүн батареяны оптималаштыруу өчүрүлүшү керек. + +MicroG үчүн батареяны оптималаштырууну өчүрүү батареянын колдонулушуна терс таасирин тийгизбейт. + +Улантуу баскычын басыңыз жана оптималаштыруу өзгөртүүлөрүнө уруксат бериңиз" + Улантуу + MicroG батареясын оптималдаштыруу диалогун көрсөтүү + "Муну өчүрүү MicroG батареясын оптималдаштыруу суроосунун көрсөтүлбөй калышына алып келет. + +Эгерде батареяны оптималдаштыруу өзгөртүлбөсө, төмөнкүлөр болушу мүмкүн: +• Тиркеме кокустуктан жабылып калышы мүмкүн +• Билдирүүлөр иштебей калышы мүмкүн +• Фондук ойнотуу иштебей калышы мүмкүн" + Тиркеме шилтемелерин иштетүү + Шилтемелерди ушул тиркемеде ачууну конфигурациялоо + 1-кадам: Түп нуска тиркеменин шилтемелерин өчүрүү + Түп нуска тиркеменин жөндөөлөрүн ачып, бардык домендик шилтемелерди өчүрүңүз + 2-кадам: Патчтелген тиркеменин шилтемелерин күйгүзүү + Патчтелген тиркеменин жөндөөлөрүн ачып, бардык домендик шилтемелерди күйгүзүңүз + Жөндөөлөрдү ачуу + 🚨 Эксперименталдык эскертүү 🚨 + "Сиз ⚠️ <b>%1$s</b> тиркемесинин эксперименталдык версиясын колдонуп жатасыз +<br/><br/> +🔧 Бул версия үчүн патчтарды тактап жаткан учурда, тиркеменин өзгөчө жүрүм-турумуна же аныкталбаган каталарга күтүңүз. +<br/><br/> +Эгерде сиз эң көйгөйсүз колдонууну кааласаңыз, анда муну <b>өчүрүңүз</b> жана ✅ <b>%2$s</b> сунушталган версиясын патчтеңиз" + ⚠️ Мага эксперименталдык версия керек + ✅ Мага туруктуу версия керек + Каталыктардын журналы колдонуучунун документтерине сакталды: %s + Каталыктардын журналы сакталды: %s diff --git a/patches/src/main/resources/addresources/values-ky-rKG/sponsorblock/strings.xml b/patches/src/main/resources/addresources/values-ky-rKG/sponsorblock/strings.xml index f542174c58..2d905679ef 100644 --- a/patches/src/main/resources/addresources/values-ky-rKG/sponsorblock/strings.xml +++ b/patches/src/main/resources/addresources/values-ky-rKG/sponsorblock/strings.xml @@ -16,8 +16,10 @@ Second \"item\" text" --> + SponsorBlock баскычтары жана башкаруу элементтери үчүн тик бурчтуу бурчтарды колдонуу Жалпы + Жүктөлүүдө… diff --git a/patches/src/main/resources/addresources/values-ky-rKG/youtube/strings.xml b/patches/src/main/resources/addresources/values-ky-rKG/youtube/strings.xml index 015546adb1..ff600cc82f 100644 --- a/patches/src/main/resources/addresources/values-ky-rKG/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ky-rKG/youtube/strings.xml @@ -20,240 +20,1636 @@ Second \"item\" text" Маалымат Жарнама + Лента Жалпы + Плеер + Shorts + Издөө тилкеси + Сыргуу башкаруулары Майда-чүйдө Видео + YouTube\'дун эски жөндөөлөр экранын калыбына келтирүү + Фондо ойнотуу чектөөлөрүн алып салуу + Фондо oynatууну өчүрүү + Түзмөк кулпуланганда автоматтык түрдө токтотуу Өчүк + Кулпуланганда дайыма токтотуу + Жигексиз аудио байланышкан эмес болсо токтотуу + Скроллинг ылдамдыгынын чектөөсүн өчүрүү + Макеттин жаңыртууларын өчүрүү + Серверге тиркеменин жасалгасын жаңыртоткун токтотот + "Тиркеменин макети алгач орнотулгандогу макетке кайтарылат. + +Кээ бир сервердик макеттер кайтарылбашы мүмкүн. + +Өзгөртүүлөргө төмөнкүлөр кирет: +• Плеердин тез менюсундагы компоненттер (же тиешелүү жөндөөлөр) иштебей калышы мүмкүн +• Сандардын алмашуусу анимацияланбайт +• Китепкана өтмөгү көрсөтүлөт +• Видео сүрөттөөнүн музыка бөлүгү иштебей калышы мүмкүн +• Плеердин тез менюсунун элементтерин жашыруу мүмкүн эмес +• Китепкана өтмөгүндө аккаунтту алмаштыруу баскычы көрүнбөй калышы мүмкүн" + Лентанын кошумча менюсунун фильтрин күйгүзүү + Лентанын тез менюсунун фильтри + Сүзүү үчүн flyout менюларынын аттарын жазыңыз, аларды жаңы саптар менен бөлүңүз + Китепкана / Сиз төмөнкү менюнун фильтрин күйгүзүү + Китепкана / Сиз төмөнкү менюнун фильтри + Фильтрлөө үчүн Китепкана / Сиз төмөнкү менюсунун аталыштарын жаңы саптар менен бөлүп жазыңыз + Альбом карталарын жашыруу + Артисттин сүрөттөмөсүнүн астындагы альбом карталарын жашырат + Артист карталарын жашыруу + Издөө жыйынтыктарынын жогору жагындагы артисттин картасын жашырат + Канал баскычтарын жашыруу + Издөө натыйжаларындагы канал профилдеринен Жазылуу, Каналды көрүү, Коомдук жана YouTube Music сыяктуу баскычтарды жашырат + Коомдук жазууларды жашыруу + Компакттуу баннерлерди жашыруу + Компакттуу баннерлерди жана айрым компоненттердин ортосундагы боштуктарды жашырат + Видеолордун астындагы кеңейтиле турган карталар Баарын көрсөтүү + Жыйынтыктоочу картаны гана жашыруу Баарын жашыруу + Сүзгүч микрофон баскычын жашыруу + Издөө сунуштары экранынын төмөнкү оң бурчундагы калкыган микрофон баскычын жашырат + \"Premium алуу\" баскычын жашыруу + Тарых текшесин жашыруу + Горизонталдык текчелерди жашыруу + Төмөнкүдөй горизонталдык текчелерди жашырат: Тез жаңылыктар, Сизге жагышы мүмкүн болгон коомдук жарыялар, Көрүүнү улантуу, Көбүрөөк каналдарды изилдөө, Көбүрөөк темаларды изилдөө, Сиз үчүн, Акыркы видеолор, Эң актуалдуулары, Автордун коомдустугунан келген жарыялар, Соода жана Кайра көрүү + Сүрөт текчесин жашыруу + Издөө жыйынтыктарындагы сүрөт тектерин жашырат + \"Башкаларды билдирүү жөнөтүүгө чакыруу\" картасын жашыруу + Билдирүүлөр өтмөгүндөгү Билдирүүлөр бөлүмүндө көрсөтүлгөн чакыруу картасын жашырат + \"Акыркы видеолор\" баскычын жашыруу + Башкы лентанын жогору жагында пайда болгон көк түстөгү \'Акыркы видеолор\' баннерин жашырат + Түз эфирлерди жашыруу + Башкы лентада жана издөө натыйжаларында учурда түз эфирде болгон видеолорду жашырат + Аралаш плейлисттерди жашыруу + Фильмдер бөлүмүн жашыруу + Билдирүүлөр менюсунун башкы бөлүгүн жашыруу + Коңгуроо баскычын басканда Билдирүүлөр менюсунун башкы бөлүгүн жашырат + \"Мага кабарлаңыз\" баскычын жашыруу + Жазылган түз эфирлер жана премьералар үчүн эскертүү баскычын жашырат + Playables бөлүмүн жашыруу + Оюндарды ойноо мүмкүнчүлүгүн жашырат + Издөө терминдеринин миниатюраларын жашыруу + Издөөдөгү авто-толуктоо тизмесиндеги кичинекей алдын ала көрүнүштөрдү жашырат + \"Көбүрөөк көрсөт\" баскычын жашыруу + Издөө натыйжаларындагы видеолордун астындагы кеңейтүү баскычын жашырат + Жазылган каналдардын тилкесин жашыруу + Жазылган каналдардын аттарын жашыруу + Жазылган каналдардын панелиндеги каналдардын аттарын жашырат + Сурамжылоолорду жашыруу + Лентанын сунуштамаларынын тактыгы жөнүндө сурамжылоолорду жана эскертүүлөрдү жашыруу + Билеттер текшесин жашыруу + + \"Жеткилбес видеолор жашырылган\" баннерин жашыруу + Плейлисттердеги \"Жеткилбес видеолор жашырылган\" баннерин жашырат Жүктөлгөн убакытты жашыруу + "Чектөөлөр: +• Shorts текчелеринде, канал баракчаларында жана издөө жыйынтыктарында жүктөө убактысы көрсөтүлүшү мүмкүн +• Автомобиль интерфейси менен иштебейт" + Видеонун кичинекей сүрөтүн жашыруу + Видео белгилерин жашыруу + \'Автоматтык түрдө дубляж செய்யப்பட்ட\' / \'Дубляж செய்யப்பட்ட\', \'Комиссия табылат\', \'Популярдуу\', \'Башкалар да көргөн\', \'продукция(лар)\' жана \'Сизге жагышы мүмкүн\' сыяктуу белгилерди жашырат + Көрүүлгөн санын жашыруу + "Чектөөлөр: +• Shorts текчелеринде, канал баракчаларында жана издөө жыйынтыктарында көрүүлөр саны көрсөтүлүшү мүмкүн +• Автомобиль интерфейси менен иштебейт" + Веб-издөө жыйынтыктарын жашыруу + Издөө натыйжаларынан \"Вебден алынган жыйынтык\" бөлүмүн жашырат + \"Сизге жагышы мүмкүн\" бөлүмүн жашыруу + YouTube Doodles\'торду жашыруу + YouTube логотибиндеги сезондук жана иш-чараларга байланыштуу анимацияларды жашырат + YouTube Doodles көрсөтүү custom заголовоктордун көрүнбөй калышына алып келиши мүмкүн + Канал панелисин жашыруу + Каналдын суу белгисин жашыруу + Бөлүмдөрдү жана \"Бул видеодо\" баскычын жашыруу + Краудфандинг блогун жашыруу + Төதைлген кутуларды жашыруу + Маалымат панелдерин жашыруу + Маалыматтык контексттик панелдерди, мисалы, медициналык маалыматтарды жашырат + \"Кошулуу\" баскычын жашыруу + \"Түз эфирдеги чаттагы кайталоо\" баскычын жашыруу + Толук экрандуу режимдеги чатты кайра ойнотуу баскычын жашырат + Медициналык панелдерди жашыруу + Плеердин жест-кеңештерин жашыруу + Ойнотуучунун жесттери учурунда көрсөтүлгөн «Так издөө үчүн жогору тартыңыз», «Жокко чыгаруу үчүн коёруңуз» жана «2x» сыяктуу кеңештерди жашырат + Снэкбарды жашыруу + Жазылуучулардын эрежелерин жашыруу + \'Видео менен синхрондостоо\' баскычын жашыруу + Басып кармоо градиентин жашыруу + Ылдамдыкты жогорулатуу үчүн басып кармап, манжаңызды жылдырганда ойноткучтун башкаруу градиентин жашырат + Убакыттык реакцияларды жашыруу + Колдонуучуларга комментарийлер бөлүмүндө жана түз эфирдеги чатта эмодзилер менен реакция кылууга мүмкүндүк берүүчү функцияны жашырат + \'Жардам & кайтарым\' менюсун жашыруу + Издөө жыйынтыктарындагы сыяктуу үч чекиттүү баскычты колдонгондо \'Жардам & кайтарым\' менюсун жашырат + Псевдонимди жашыруу + \'Сиз\' өтмөгүндө каналдын туткасын жашырат + Видеонун аталышын жашыруу + Толук экрандуу режимде видеонун аталышын жашырат + Видеосубтитрлерди тазалоо + Видео субтитрлеринен \'#\', \'Фандрейзинг\', \'продукция(лар)\' жана \'Дүкөн\' сыяктуу сөздөрдү тазалайт + \'Gemini\'ден суроо\' баскычын жашыруу + \'Бул видео жөнүндө суроо\' панелинин төмөгүндөгү \'Gemini\'ден суроо\' баскычын жашырат + Видеонун сүрөттөлүшү + Видеонун сыпаттамасында кайсы компоненттер көрүнөрин тандаңыз + \"Жасалма интеллект тарабынан түзүлгөн видеонун кыскача мазмуну\" бөлүмүн жашыруу + Суроо бөлүмүн жашыруу + Атрибуттар бөлүмүн жашыруу + Белгиленген жерлер, Оюндар, Музыка жана Эскерилеткен адамдар бөлүмдөрүн жашырат + Каналдардын шилтемелери бөлүмүн жашыруу + Бөлүмдөр бөлүмүн жашыруу + Оңдоолор бөлүмүн жашыруу + \"Курстун жүрүшү\" бөлүмүн жашыруу + «Издөө» бөлүмүн жашыруу + \"Бул курсту изилдөө\" жана \"Подкастты изилдөө\" бөлүмдөрүн жашырат + \"Курсту изилдөө\" бөлүмүн жашыруу + \"Подкастты изилдөө\" бөлүмүн жашыруу + Тандалган каналдар бөлүмүн жашыруу + Тандалган шилтемелер бөлүмүн жашыруу + Тандалган плейлисттер бөлүмүн жашыруу + Тандалган видеолор бөлүмүн жашыруу + Хэштег бөлүмүн жашыруу + \"Бул кантип жасалды\" бөлүмүн жашыруу + Hype упайларын жашыруу + Маалымат карталары бөлүмүн жашыруу + \'Негизги түшүнүктөр\' бөлүмүн жашыруу + Викториналар бөлүмүн жашыруу + \"Бул видеонун ичинен издөө\" бөлүмүн жашыруу + Жазылуу баскычын жашыруу + Транскрипт бөлүмүн жашыруу + \"Видеонун чоо-жайы\" бөлүмүн жашыруу + Филтрлөө панели + Тайындалган фильтр панели кайсы жерде жашырыларын тандаңыз + Канал баракчасында жашыруу + Комментарийлерде жашыруу + Ленталарда жашыруу + Окшош видеолордо жашыруу + Издөө натыйжаларынан жашыруу + Көрүү тарыхынан жашыруу + Канал баракчасы + Канал баракчасында кайсы компоненттер көрүнөрүн тандаңыз + Канал өтмөгүнүн фильтрин күytyрүү + Канал өтмөгүнүн фильтри + Фильтрлөө үчүн канал табысынын аттарын жаңы саптар менен бөлүп жазыңыз + Коомдум баскычын жашыруу + Кошулуу баскычын жашыруу + Шилтемелердин алдын көрүүнү жашыруу + Каналдын башкы бөлүгүндөгү басыла турган веб-шилтемелерди жашырат + \'Мүчөлөр үчүн гана\' белгисин жашыруу + Каналдын чыпкалоо панелиндеги \'Мүчөлөр үчүн гана\' белгисин жашырат + Мүчөлөр теккесин жашыруу + Канал мүчөлөрүнүн тизмесин жашырат + Посттор текчесин жашыруу + Дүкөн баскычын жашыруу + Жазылуу баскычын жашыруу + Издөө менюсу + Издөө менюсунда кайсы компоненттер көрүнөрүн тандаңыз + Курстар менюсун жашыруу + Издөө баскычын жашыруу + \'Fashion & Beauty\' менюсун жашыруу + Гейминг менюсун жашыруу + Hype менюсун жашыруу + Окуу менюсун жашыруу + Live менюсун жашыруу + Мүчөлүк менюсун жашыруу + \'Кинолор & ТВ\' менюсун жашыруу + Музыка менюсун жашыруу + Жаңылыктар менюсун жашыруу + Playables менюсун жашыруу + Подкасттар менюсун жашыруу + Соода менюсун жашыруу + Спорт менюсун жашыруу + YouTube Premium менюсун жашыруу + YouTube Studio менюсун жашыруу + YouTube Music менюсун жашыруу + YouTube Kids менюсун жашыруу + YouTube Create менюсун жашыруу + \"YouTube кандай иштейт\" менюсун жашыруу + Юридикалык жана купуялуулук төмөнкү бөлүктөрүн жашыруу Пикирлер + Комментарийлер бөлүмүндө кайсы компоненттер көрүнөрүн тандаңыз + Комментарийлер каруселинин фильтрин күйгүзүү + "• Карусель элементтинин так атын киргизиңиз (мисалы, Ask, Hype this video, Listen on YouTube Music) +• Карусель элементтерин жашыруу күтүлбөгөн терс таасирлерге алып келиши мүмкүн +• Эгерде карусельдин бардык элементтери жашырылса, бүткүл комментарийлер бөлүмү да жашырылат" + Комментарийлер каруселинин фильтри + Фильтрлөө үчүн комментарийлер каруселинин аттарын жаңы саптар менен бөлүп жазыңыз + AI чаттын кыскача мазмунун жашыруу + Каналдын эрежелерин жашыруу + Контексттик билдирүүлөрдү жашыруу + \"Мүчөлөрдүн комментарийлери\" башын жашыруу + Комментарийлер бөлүмүн жашыруу + Башкы лентадагы комментарийлер бөлүмүн жашыруу + Жамааттык эрежелерди жашыруу + \"Shortс түзүү\" баскычын жашыруу + Жакпады баскычын жашыруу + Эмодзи баскычын жашыруу + Фильтр панелинин параметрлерин жашыруу + \'Эң мыкты\' жана \'Жаңы\' параметрлеринден башка бардык фильтр панелинин параметрлерин жашырат + Белектердин анимациясын жана карталарын жашыруу + Белек баскычын жашыруу + Маалымат баскычын жашыруу + Түз эфирдеги чаттын донаторлор тилкесин жашыруу + Түз эфирдеги чаттын төмөн карай скроллдоо баскычын жашыруу + Түз эфирдеги чаттын кеңири маалыматтарын жашыруу + Чаттын киргизүү панелинин жанында көрсөтүлгөн кеңири маалыматтарды, мисалы, жазылуучулар үчүн гана чат эскертүүсүн жашырат + Меню баскычын жашыруу + Ар бир комментарийдин 3 чекиттик меню баскычын жашырат + Комментарийдин алдын көрүүнү жашыруу + Толук комментарийлер бөлүмүн ачкыча чейин видеонун астындагы комментарийлердин алдын ала көрүнүшүн жашырат + Комментарий Morphe тарабынан жашырылган… + Минималистикалык комментарийлер баскычын күйгүзүү + Комментарий баскычында көрсөтүлгөн жылдыргыч карталарды жана алардын барак тандоочусун жашырат + Ыраазычылык билдирүү баскычын жашыруу + Убакыт белгисинин баскычын жашыруу + \'Эң мыкты күйөлдөр\' баскычын жашыруу + Котормо баскычын жашыруу + \"Азырынча комментарийлер жок\" билдирүүсүн жашыруу + Белгиленген издөө шилтемелерин тазалоо + Белгиленген издөө шилтемелерин тазалап, оригиналдуу жөнөкөй текстти гана калтырат + Ачкыч сөздөр боюнча чыпка + Ленталарда жана издөөдө видеолорду жашыруу үчүн белгилүү бир сөздөрдү же сөздөрдүн айкалыштарын тандаңыз + Комментарийлерди ачкыч сөздөр боюнча жашыруу + Башкы беттеги видеолорду ачкыч сөздөр боюнча жашыруу + Издөө жыйынтыктарын ачкыч сөздөр боюнча жашыруу + Жазылуулар видеолорун ачкыч сөздөр боюнча жашыруу + Жашырыла турган ачкыч сөздөр + "Жашыруу керек болгон ачкыч сөздөрдү жана сүрөттөмөлөрдү жаңы саптар менен бөлүп киргизиңиз + +Ачкыч сөздөр видеонун аталыштарында же каналдардын аттарында көрсөтүлгөн каалаган текст болушу мүмкүн + +Ортосунда чоң тамгалары бар сөздөр так дал келиши керек (мисалы, iPhone, TikTok, LeBlanc)" + "Башкы бет / Жазылуулар / Издөө / Комментарийлер ачкыч сөздөргө дал келген мазмунду жашыруу үчүн фильтрлөө + +Чектөөлөр: +• Shorts видеолорун каналдын аталышы боюнча жашыруу мүмкүн эмес +• Кээ бир UI компоненттери жашырылбашы мүмкүн +• Ачкыч сөздү издөөдө эч кандай жыйынтык чыкпай калышы мүмкүн" + Сөздөрдү толугу менен дал келтирүү + Ачкыч сөздү/фразаны кош тырмакчага алуу видео аталыштарынын жана канал аттарынын жарым-жартылай дал келишин алдын алат<br><br>Мисалы,<br><b>\"ai\"</b> видеону жашырат: <b>How does AI work?</b><br>бирок мына bunu жашырбайт: <b>What does fair use mean?</b> + Ачкыч сөздү колдонуу мүмкүн эмес: %s + Ачкыч сөздү колдонуу үчүн тырмакчаларды кошуңуз: %s + Ачкыч сөздүн карама-каршы declarations есть: %s + Ачкыч сөз өтө кыска жана тырмакчаларды талап кылат: %s + Ачкыч сөз бардык видеолорду жашырат: %s + "• Башкы бет: %s +• Жазылуулар: %s +• Издөө: %s +• Комментарийлер: %s +• Жалпы: %s" + Статистика + Жашырылган видеолор, акыркы 24 саат + Жашырылган видеолор, бардык убакыт + Акыркы 24 сааттык эсептегичти нөлдөө? + Жалпы убакыттын эсептегичин нөлдөө? + AI каналдардын фильтри + AiSList коомчулук тизмеси тарабынан AI-генерацияланган контент катары белгиленген каналдардын видеолорун жашыруу + Блоктоо тизмеси: Жогорку ишенимдүүлүк + Эскертүү тизмеси: Орточо ишенимдүүлүк + Баштапкы беттеги видеолорду жашыруу + Издөөдөгү видеолорду жашыруу + "• Башкы бет: %s +• Издөө: %s +• Жалпы: %s" + Блокко алынган каналдар + Эскертүү тизмесиндеги каналдар + Каналдарды жүктөө үчүн чыпканы иштетиңиз + Каналдар жүктөлүүдө… + "Башкы бет жана издөө жыйынтыктары AiSList тарабынан белгиленген каналдардын видеолорун жашыруу үчүн чыпкаланган + +Чектөөлөр: +• Чыпкаланган AI Shorts бош боштук катары көрүнүшү мүмкүн +• Кээ бир AI видеолору дагы деле көрүнүшү мүмкүн" + "AiSList — бул ыктыярчылар тарабынан башкарылуучу, AI тарабынан жасалган контентти жарыялаган YouTube каналдарынын ачык булактуу тизмеси. Каналдар колдонуучулар тарабынан билдирилип, кошулгуну kautta каралат + +Тизме GitHub'дан жүктөлөт жана видеолор сиздин түзмөгүңүздө сүзүлөт" + AI каналдарын билдирүү + Видео менюсуна отчет жазуусун кошуу + Видео менюсуна видеонун каналын AiSList кызматына билдирүүчү жазууну кошуу + Жөнөтүүчүнүн аты + Коомдук AiSList лидербордунда сиздин отчетторуңуздун жанында көрсөтүлүүчү ат. Бул талаа бош болсо, отчет берүү мүмкүн эмес + AI каналга отчет берүү + "Бул видеону далил катары колдонуп, %s каналын AiSListтиге жөнөтөсүзбү? + +Төмөнкү талаптарга жооп берген каналдарды гана отчеттоңуз: +• Негизги контент катары AI үнүн, визуалдарын, сценарийлерин же музыкасын колдонот +• AI куралдары менен адамдын кичинекей катышуусу менен контентти массалык түрдө чыгарат + +AI жөнүндө гана талкуулаган же видеолорунун айрым бөлүктөрүндө AI колдонот каналдарды отчеттобоңуз. + +Отчеттор ачык жана канал тизмеге кошулгуча каралат. Тизмеден алып салуу GitHub аркылуу суроо жөнөтүү менен гана мүмкүн" + Бул канал алдында эле AiSList бөгөттөө тизмесинде бар + Бул канал алдында эле AiSList эскертүү тизмесинде болгон + Катасын билдирүү + Каналдарды билдирүү үчүн AI канал фильтринин жөндөөлөрүнөн жөнөтүүчүнүн атын белгилеңиз + Канал издерялууда… + Бул видеонун каналы табылган жок + Канал боюнча арыз берилди жана каралууда + AiSList арызды сактай алган жок, кийинчерээк кайра аракет кылыңыз + Арыздар өтө көп жөнөтүлдү, кийинчерээк кайра аракет кылыңыз + AiSList арызды четке какты + Арыз жөнөтүү окшошту, байланышыңызды текшериңиз + Каналдын жарнамаларынын ак тизмеси + Ак beyaz тизмесиндеги каналдардын видеолору жарнамалар менен ойнотулат, ошондуктан алардын автору акы алат. Калган бардык каналдарда жарнамалар бөгөттөлөт + Каналды жарнамалардын ак тизмесине кошуу + Каналды жарнамалардын ак тизмесинен алып салуу + Креатордун дүкөн текшесин жашыруу + Аяктоо экранындагы дүкөн баннерин жашыруу + Жалпы жарнамаларды жашыруу + Мерчандайзинг баннерлерин жашыруу + Акылуу жарнамалык белгилерди жашыруу + Плеердеги терезе жарнамаларын жашыруу + Өзүн-өзү жарнамалаган карточкаларды жашыруу + Соода шилтемелерин жашыруу Видео жарнамаларын жашыруу + YouTube Premium жарнамаларын жашыруу + Жүктөлүүдө… + Учурдагы канал: + Ак тизмеге кошуу + Алып салуу + Ак тизмедеги каналдар + Ак тизмеде каналдар жок + Канал ак тизмеге кошулду + Канал ак тизмеден алынды + Каналдын ичинен издөө + Каналдардын барактарына ошол каналды гана издөө үчүн экинчи издөө баскычын кошот + Каналды издөө + Каналды издөө жыйынтыктары + Бул каналдан эч нерсе табылган жок + Бул каналды издөө мүмкүн болбоду + Видеолор + Посттор + Бул каналда дал келген посттор жок + Тиешелүүлүгү + Эң көп көрүлгөндөр + Эң аз көрүлгөндөр + Эң жаңылары + Эң эскилери + Эң кыска Short видеолор + Эң узундары + Шилтеме алмашкачтыркычка көчүрүлдү + Убактысы көрсөтүлгөн шилтеме алмашкачтыркычка көчүрүлдү + Видео шилтемесин көчүрүү баскычын көрсөтүү + Видео шилтемесин көчүрүү үчүн басыңыз, убактысы менен көчүрүү үчүн басып туруңуз + Убакыт белгисин стандарттык түрдө көчүрүү + «Видео шилтемесин көчүрүү» баскычынын басуу жана басып туруу аракеттерин тескери кылат + Видеонун аталышын көчүрүү + Видеонун аталышын көчүрүү үчүн аны басып туруңуз + Видеонун аталышы алмаштыруу буферге көчүрүлдү + Пыкирлерди көчүрүү + Пыкирди көчүрүү үчүн аны басып туруңуз + Пыкир алмаштыруу буферге көчүрүлдү + Видео менюсун ачуу үчүн басып тургула + Лентадагы видеонун каалаган жерин басып тургуңуз, анда анын менюсу ачылат + Миниатюраларда «Кийинчерээк көрүү» баскычын көрсөтүү + Лентадагы видеолордун миниатюраларында «Кийинчерээк көрүү» баскычы көрсөтүлөт. Аны басып турсаңыз, «Плейлистке сактоо» функциясы ачылат + Посттогу сүрөттөрдү жүктөө + Сүрөттөрү же GIF-тери бар посттордун \"Бөлүшүү\" баскычынын жанында жүктөө баскычы пайда болот жана алардын менюсуна жүктөө пункту кошулат + Сүрөттөрдү жүктөө + Сүрөттү жүктөө + %s сүрөт жүктөлүп жатат… + Сүрөт жүктөлүп жатат… + %1$s сүрөт %2$s сакталды + Сүрөт %s сакталды + Сүрөттү жүктөп алуу мүмкүн болгон жок + Бул постто сүрөт табылган жок + Суроо пунктун тез менюга кошуу + Лентанын тез менюсуна видеону кыскача баяндоо пунктун кошот + "Суроо функциясы үчүн алгач Суроо жеткиликтүү болгон өлкөдөгү кирген аккаунт талап кылынат. + +Суроо баскычы бар видео бир жолу ачылгандан кийин, бул функция учурдагы өлкөдө Суроо функциясы жок болсо да, бардык видеолор менен иштейт." + Видеону кыскача баяндоо + Бул видео үчүн Суроо панелини ачуу мүмкүн болгон жок + Суроо функциясы сиздин аккаунтуңуз же учурдагы өлкөңүз үчүн жеткиликсиз + Миниатюраларда көрүүлгөн сандарын көрсөтүү + Лентадагы видеолордун көрүүлгөн саны аталышынын астында эмес, миниатюралардын төмөнкү сол бурчунда көрсөтүлөт. Кээ бир видеолордо, мисалы, текчелерде, көрүүлгөн саны көрсөтүлбөшү мүмкүн + Лентадагы миниатюралардын лайктарынын санын көрсөтүү + Лентадагы миниатюралардын лайктарынын пайызын көрсөтүү + Комментарийлерди издөө + Издөө сөздөрүнүн баарын камтыган жүктөлгөн комментарийлерди гана көрсөтүү үчүн комментарийлердеги издөө баскычын басыңыз. Фразаны издөө үчүн сөздөрдү тырмакчага алыңыз + Комментарийлерди издөө + Издөө үчүн сөздөр + Издөө + Тазалоо + Сорттоо тартибин өзгөртүңүз же издөө үчүн комментарийлерди кайра ачыңыз + Издөө тазаланды. Бардык комментарийлерди көрсөтүү үчүн сорттоо тартибин өзгөртүңүз же комментарийлерди кайра ачыңыз + \"ТВ\'ге кирүү\" терезесин өчүрүү + \"Түзүлүштөрүңүздү байланыштырыңыз\" терезесин өчүрүү + Главаны эки жолу басып өткөрүүнү өчүрүү + Бөлүмдөрдү өткөрүп жиберүүчү эки манжа менен эки жолу басуу жестрин өчүрөт, ал бир манжа менен кокустан иштеп кетиши мүмкүн + Үчүнчү тараптын жүктөө колдонмолору менен видеолорду, подкасттарды же ырларды жүктөп алыңыз + Плеердин үстүндөгү жүктөө баскычын көрсөтүү + Видео плеерде сырткы жүктөгүчтү ача турган баскычты көрсөтөт + Басканда тез жүктөө + Фондо жүктөө үчүн суроолор. Жүктөгүчтү ачуу үчүн басып тургулаңыз. YTDLnis жана YTindir менен иштейт + Жүктелгендерди көрүлдү деп белгилөө + Сырткы жүктөгүч менен жүктөлгөн видеолор сиздин YouTube көрүү тарыхыңызга кошулат + Full HD (1080p) + Орточо (192 kbps) + Тез жүктөө Видео + Аудио + Бардык параметрлер + Shorts менюсунда жүктөөлөрдү көрсөтүү + Shorts ойноткучуна тышкы жүктөөчү үчүн тез меню пунктун кошот + Кыскартылган жерге жүктөө + Котормону үн менен окуу + Видеонун жапкычтарын түзүлүштүн текстти сүйлөтүү функциясын колдонуп, ойнотуу менен синхрондоштурулган түрдө үн чыгарып окуу + Видео ойноткучта үстүңкү баскычты көрсөтөт. Котормону күйгүзүү/өчүрүү үчүн басыңыз, тез жөндөөлөр үчүн басып туруңуз + Тил + Котормо sağlayıcıсы + Түп нуска аудионун дүрөйлүгү + Котормо окулуп жаткан учурда YouTube аудиосунун түп нуска дүрөйлүгү + Котормонун дүрөйлүгү + Дубляждалган котормо үнүнүн дүрөйлүгү + Сүйлөөnүн максималдуу ылдамдыгы + Сүйлөөнү субтитр слотуна ылайыкташтыруудагы максималдуу ылдамдык коэффициенти + "Акысыз котормо API, API ачкычы талап кылынбайт. Акысыз тарифте күнүнө 5 000 белгиге чейин, же электрондук почта дареги менен күнүнө 50 000 белгиге чейин. + +MyMemory.translated.net сайтын ачуу үчүн бул жерди басыңыз" + MyMemory электрондук почтасы + Ыктыярдуу. Күнүмдүк лимитти 5 000 белгиден 50 000 белгиге чейин көбөйтүү үчүн MyMemory электрондук почтаңызды киргизиңиз. Анонимдүү лимитти колдонуу үчүн бош калтырыңыз + "OpenRouter аркылуу акы төлөнүүчү AI котормосу. Бир жолу балансты толуктап, миңдеген видеолорду которуңуз. Mistral Nemo эң арзан вариант, Gemma 4 эң жогорку сапатты сунуштайт. + +OpenRouter.ai сайтын ачуу үчүн бул жерди басыңыз" + OpenRouter API ачкычы + Сиздин OpenRouter API ачкычыңыз. Ачкычты OpenRouter.ai > Workspace > API Keys бөлүмүнөн түзүңүз + OpenRouter модели + Ыйыкталган + OpenRouter конфигурацияланган эмес + Бул провайдерди тандоодон мурун жөндөөлөргө OpenRouter API ачкычыңызды киргизиңиз + Акысыз + 100 видео саатына %s + Үн + Система + Көп тилдүү эркек + Көп тилдүү аял + Эркек + Аял + Морфинг убактысы келди! + Котормо жеткиликсиз + "Котормо кызматы HTTP %s катасын кайтарды. Сиздин IP дарегиңизге 24 саатка чейин чектөө коюлган болушу мүмкүн. + +Котормолорду колдонууну улантуу үчүн, жөндөөлөрдөн башка котормо sağlayıcына өтүңүз же VPN колдонуңуз" + Кайра көрсөтпө + "Сиздин жөндөөлөрүңүздө OpenRouter API ачкычы бар. + +Сиздин API ачкычыңыз эч качан бөлүшүлбөшү керек. Ага мүмкүнчүлүгү бар адамга сиздин OpenRouter кредиттериңизди колдоно алат" + OpenRouter кредиттик лимити түгөндү + Сиздин OpenRouter кредиттик лимитиңиз түгөндү. Балансыңызды толуктаңыз же OpenRouter.ai сайтында ачкычтын чыгым лимитин жогорулатыңыз + OpenRouter.ai сайтын ачуу + OpenRouter которуусу катачылык менен аяктады (HTTP %s) + MyMemory күндүк лимити түгөндү + Кийинкиси %s күнү жеткиликтүү болот + Сиз бүгүнкү MyMemory акысыз которуу квотасын (5 000 белги) колдондуңуз. Күндүк лимитти 50 000 белгиге чейин жогорулатуу үчүн жөндөөлөргө MyMemory электрондук почтасын кошуңуз же башка которуу кызматына өтүңүз + Сиз бүгүнкү MyMemory которуу квотасын (50 000 белги) колдондуңуз. Башка которуу кызматына өтүңүз же эртең кайра аракет кылып көрүңүз + MyMemory которуусу катачылык менен аяктады (статус %s) + Видеону кайталоону күйгүзүү + Видеону кайталоону эстеп калбоо + Кийинки видеолор үчүн видеону кайталоонун учурдагы абалын сактоонун алдын алуу + Видеону кайталоо баскычын көрсөтүү + Видеону кайталоо күйгүзүлдү + Видеону кайталоо өчүрүлдү + Циклдин диапазону + Башталышы + Аягы + [сс:]мм:сс + [сс:]мм:сс (бош = видеонун аягы) + Орнотуу + Тазалоо + %1$s - %2$s аралыгы кайталанууда + Убакыттын туура эмес аралыгы + Сүрөт-сүрөттө баскычын көрсөтүү + Сүрөт-сүрөттө менюсун көрсөтүү + Сүрөт-сүрөттө режимине өтүү + Баарын ойнотуу баскычын көрсөтүү + "Каналдагы бардык видеолордун плейлистин түзүү үчүн басыңыз, кайтаруу үчүн басып туруңуз + +Маалымат: Түз эфирлерде иштебеши мүмкүн" + Плейлистти түзүү режими + Бардык мазмун (Убактысы боюнча сорттоо, өсүүчү тартипте) + Бардык мазмун (Убактысы боюнча сорттоо) + Бардык мазмун (Популярдуулугу боюнча сорттоо) + Видеолор гана (Убактысы боюнча сорттоо) + Видеолор гана (Популярдуулук боюнча сорттоо) + Shorts гана (Убактысы боюнча сорттоо) + Shorts гана (Популярдуулук боюнча сорттоо) + Түз эфир видеолору гана (Убактысы боюнча сорттоо) + Түз эфир видеолору гана (Популярдуулук боюнча сорттоо) + Мүчөлөр үчүн гана болгон бардык мазмундар + Мүчөлөр үчүн гана видеолор + Жардамчылар үчүн гана Shorts + Мүчөлөр үчүн гана түз эфирлер + Каналдын ID-си дал келбегендиктен плейлистти түзүү мүмкүн болбоду + Видеону кайра жүктөө баскычын көрсөтүү + Видеонун ID-си жеткиликсиз. Сураныч, видеону кол менен кайра жүктөңүз + \"Кийин көрүүгө сактоо\" баскычын көрсөтүү + Кейин көрүүгө сактоо үчүн басыңыз, кезекти башкаруу үчүн басып туруңуз + Видео \"Кийин көрүү\" тизмесине ийгиликтүү сакталды + Бул видео \"Кийин көрүү\" тизмесине сакталган + Бул видеону \"Кийин көрүү\" тизмесине сактоо мүмкүн эмес + Кейин көрүүгө сактоо + Плеер жөндөөлөрүнүн менюсунда \"Кийин көрүүгө сактоо\" баскычын көрсөтүү + Видео жөндөөлөрүнүн менюсунан учурдагы көрүлүп жаткан видеону \"Кийин көрүү\" бөлүмүнө сактоого мүмкүндүк берет + Балдар үчүн видеолордо \"Кейин көрүүгө сактоо\" баскычын көрсөтүү + Менюдан балдарга арналган видеолорду \"Кийин көрүү\" бөлүмүнө сактоого мүмкүндүк берет + Shorts менюсүндө «Кийин көрүүгө сактоо» баскычын көрсөтүү + Shorts видеолорду менюдан «Кийин көрүүгө» сактоого мүмкүндүк берет + \"Кийин көрүү\" функциясын плейлистке багыттаңыз + Кийин көрүү + Morphe кезекке кошуу + Сессиялардын ортосунда кезекти сактоо + Тиркемени кайра başlatкандан кийин кез сакталат + Сактоо жана Кезекти иш-аракеттерин алмаштыруу + Кезек опциялары үчүн басыңыз, \"Кийинчерээк көрүүгө\" сактоо үчүн басып туруңуз + Кезектеги кийинкини ойнотуу түрү + Негизги + Түпкүл кезек + Morphe кезеги + Morphe кезеги клиенттик тарапта + Лентанын тез менюсуна кезекке кошуу пунктун кошуу + Кошумча тез меню пунктун кошот. Эгер «Кезекке кошуу» көрсөтүлбөсө, муну иштетиңиз + Кезек + Кезек (%s) + Баарын тазалоо + Кезекте эч нерсе жок + Кезектеги каалаган видео үчүн \'Кезектеги кийинкини ойнотуу\' функциясын колдонуңуз + Азыр ойноп жатат + Кезектеги кийинкини ойнотуу + Кезектеги эң акыркысын ойнотуу + Реттоо + Өчүрүү + Кезекке кошулду + Бул видеону кезекке кошууга мүмкүн болгон жок + Плейлисттерди кезекке кошууга болбойт + Кезек толду + Видеоду баштоо мүмкүн болгон жок + Ачылуучу менюдагы жарнамалардын ак тизмеси + Ошол видеонун каналында жарнамалардын ойнолушуна мүмкүнчүлүк берген ачылуучу менюнун пунктун кошот + Бул функция үчүн PotHelper колдонуу же \"Видео агымдарын жасалгалоо\" параметрин ТВ же Android VR киришүүсүнө өзгөртүү талап кылынышы мүмкүн + Ачылуучу менюдагы ылдамдыктын ак тизмеси + Ошол видеонун каналын ар дайым 1.0x ылдамдыкта ойноту는 ачылуучу менюнун пунктун кошот + Кириш жүргүзүлгөн жок + Плейлисттин IDси жок + Кез бош + Видеонун IDси жок + Ката кетти: %s + Кезекке кошуу мүмкүн болгон жок + Кезекти түзүү мүмкүн болгон жок + Кезектен алып салуу мүмкүн болгон жок + Кезекти сактоо мүмкүн болгон жок + Кезекке кошулду + Кезек түзүлдү + Кезектен алынды + Кезек %s сакталды + Morphe кезекти + Кезекке кошуу + Кезекке кошуп, ачуу + Кезекке кошуп, ойнотуу + Кошуп, видеону кайра жүктөө + Кезектен алып салуу + Алып салып, кезекти ачуу + Видеону алып салып, кайра жүктөө + Кезекти ачуу + Кезекти плейлистке сактоо + Плейлист сакталды + %s жерге сакталды + Кезекти тазалоо + Кезек тазаланды + Кезек + Кезектеги акыркысын ойнотуу + Кезектеги кийинкисин ойнотуңуз + YouTube кезекти көрсөтө алган жок. + Бул видео үчүн кезек даяр эмес. Видео ойноткучту ачып, кайра аракет кылыңыз. + Миксти кезекке кошосузбу же көрөсүзбү? + Көрүү + Кезекке кошуу + Микс жүктөлүүдө… + Түз эфир аватарасынан каналды ачуу + Каналдын аватарын басканда түз эфирдин ачылышын алдын алат + Так издөө жестин өчүрүү + Издөө тилкесин жогору тартканда активдешкен так издөө режимин өчүрөт + Түз эфир DVR функциясын күйгүзүү + Бардык түз эфирлерде, анын ичинде адатта чектөөлөрү бар эфирлерде да кайра карап чыгууга мүмкүндүк берет + Seekbar миниатюраларын күйгүзүү + Seekbarды жылдырганда видеонун миниатюрасын көрсөтөт + Көп кайталанган + "\"Түз эфир DVR функциясын күйгүзүү\" жеткиликтүү болушу мүмкүн эмес + +Муну оңдоо үчүн, \"Видео агымдарын имитациялоо\" функциясын күйгүзүңүз жана \"Адаттагы клиентти\" \"TV Simply\" же \"visionOS\" деп өзгөртүңүз" + Түз эфирдин DVR узартмасын кеңейтүү + Түз эфирлерди 7 күнгө чейин артка кайрардырууга мүмкүндүк берет + "\"Түз эфирдин DVR узартмасын кеңейтүү\" жеткиликсиз болушу мүмкүн + +Муну оңдоо үчүн, \"Видео агымдарын имитациялоо\" функциясын күйгүзүңүз жана \"Адаттагы клиентти\" \"TV Simply\" же \"visionOS\" деп өзгөртүңүз" + Издөө үчүн сыйгарууну күydүрүү + Видео плеерини басып кармаганда 2x ылдамдыкта ойнотуунун ордуна, издөө үчүн сыдырууну активлештирет + Издөө үчүн басууну күйгүзүү + Сыдырбастан, ошол позицияга өтүү үчүн жөн гана издөө тилкесин басыңыз + Солго сыдыруу жести + Оңго сыдыруу жести + Жогору сыдыруу жести + Сол/Оң зонанын туурасы + Сол жана оң жакка сырмалоо жесттери үчүн экрандын ар бир четинин резервделген пайызы (5-50) Жарыктык Үн + Түпкү Өчүк + Булга Locked болгондо сырмалоолорду эске албоо + Плеер \'Экранды бекитүү\' менен бекитилгенде сырмалоо жесттери эске алынбайт + Сыдыруу үчүн басуу жестин күйгүзүү + Сыргуу жестi таанылышынан мурун басып кармап турууну талап кылат + Гаптик кайтарымды күйгүзүү + Жарыкты сактоо жана калыбына келтирүү + Толук экран режимине киргенде / чыкканда жарыктын деңгээлин эстеп калат + Автоматтык жарык жестин күйгүзүү + Жарыкты эң төмөнкү маанисине чейин сыдыруу системалык авто-жарыкты активдештирет + Автоматтык + Сыдыруу катмарынын убактысы + Кабулун көрүнүү миллисекундаларынын саны + Сырмалоо катмарынын фонунун тунуктугу + Тунуктук деңгээлин жөнгө салыңыз (0 - тунук, 100 - колтурмуу) + Сырмалоо катмарынын жарыктуулук түсү + Жарыкты башкаруучу прогресс панелинин түсүн белгилеңиз + Сырмалоо катмарынын үн түсү + Үндү жөнгө салгычтын прогресс панелинин түсүн белгилеңиз + Сыргуучу катмардын ылдамдык түсү + Ылдамдыкты башкаруучу панелдин түсүн белгилеңиз + Сырмалоо катмарынын текстинин өлчөмү + Экрандагы тексттин өлчөмүн жөнгө салыңыз (1-30) + Сырмалоонун чоңдугунун чеги + Жөнгө салтуу иштегенге чейинки талап кылынган сыргылоо аралыгын белгилеңиз + Үнүн өзгөртүү үчүн сырмоонун сезимталдыгы + Жогору баалуулуктар үн кадамын өзгөртүү үчүн көбүрөөк сырмоону талап кылат + Үн кадамдары + Үндү күчөтүү + Максималдуу үн деңгээлинен жогору сыргыңыз үндү күчөтөт, бул катуу аудиону бузуп жибериши мүмкүн + Түзмөктүн стандарттуу жөндөөсү + Жарыктык сыргуусунун сезгичтиги + Жогорку маанилер жарыктык кадам сайын көбүрөөк сыргоону талап кылат + Ылдамдык сыргуусунун сезгичтиги + Жогорку маанилер ылдамдык кадам сайын көбүрөөк сыргоону талап кылат + Ылдамдык кадамы + Ылдамдык зонасынын бийиктиги + Ылдамдык жест зонасынын бийиктиги плеердин бийиктигинин пайызы менен (5-75) + Ылдамдык + Сырмалоо катмарынын стили + Горизонталдык катмар + Горизонталдык катмар (минималистикалык - жогоруда) + Горизонталдык катмар (минималистикалык - борбордо) + Тегерек оверлей + Тегерек оверлей (минималистика) + Вертикалдык оверлей + Вертикалдык оверлей (минималистика) + Видеолорду алмаштыруу үчүн сыргытууну күйгүзүү + Кийинки же мурунку видеого өтүү үчүн толук экрандуу режимде сыргыңыз + Аракет баскычтары + Портрет жана толук экрандуу режимде кайсы аракет баскычтары пайда болорун тандаңыз + Портрет баскычтары + \"Лайк\" жана \"Жазылуу\" жаркыратуусун өчүрүү + Автор алар жөнүндө айтканда, «Жакты» жана «Жазылуу» баскычтарынын жаркырашын алдын алат + Аракеттер панелин жашыруу + «Жакты» жана «Жактырбадым» баскычтарын жашыруу + «Бөлүшүү» баскычын жашыруу + «Жарнамаларды токтотуу» баскычын жашыруу + Комментарийлерди жашыруу баскычы + «Жанашуу» баскычын жашыруу + Арыз жасоо баскычын жашыруу + Ремикс баскычын жашыруу + Жүктөп алуу баскычын жашыруу + Хайп баскычын жашыруу + Жайылтуу баскычын жашыруу + Ыраазычылык билдирүү баскычын жашыруу + «Суроо» баскычын жашыруу + Каналды жана Жазылуу баскычын жашыруу + Аракеттик панелдин башында каналдын аватарын жана Жазылуу баскычын жашырат + Жазылуу баскычын жашыруу + Иш-аракеттер панелинин башында каналдын аватасынын жанындагы Жазылуу жана Кошулуу баскычтарын жашырат + «Клип» баскычын жашыруу + Бул айрым колдонуучулардын аккаунттары үчүн иштебеши мүмкүн + «Дүкөн» баскычын жашыруу + «Сактоо» баскычын жашыруу + \"Көбүрөөк\" баскычын жашыруу + Толук экран баскычтары + «Жакпайт» баскычын жашыруу + «Жагат» баскычын жашыруу + «Түз эфирдеги чат» баскычын жашыруу + «Микс» баскычын жашыруу + «Көбүрөөк видеолор» баскычын жашыруу + Плейлист баскычын жашыруу + Тез аракеттердин жогорку чети + Тез аракеттер панелинин жогорку четин белгилөө (0-32 dp) + Субтитрлер + Автоматтык субтитрлерди жана субтитр кукилерини башкаруу + Автоматтык субтитрлердин көрүнүүсү + Дайыма көрсөтүү + Эч качан көрсөтбө + Үн күйгүзүлгөндө көрсөтүү + Үнү өчкөндө көрсөтүү + Көчникардын артыкчылыктуу тили + Алгач провайдердин субтитрлерин тандайт, андан кийин автоматтык түрдө тандалган тилге которат Өчүк + Кошумча авто-котормо тилдерин кошуу + Авто-котормо менюсуна көбүрөөк тилдерди кошот + Субтитр кукилерин орнотуу + "Кукилердин жоктугуна байланыштуу авто-котормо субтитрлери жеткиликсиз болушу мүмкүн болгон YouTube'дун стандарттык катасын оңдойт + +«Субтитр кукилери» берилиши керек" + Субтитрлердин кукилери + Timed Text API (Caption Data API) суроо-талаптарында орнотула турган кукилер + Субтитрлердин кукилерин алуу + Кукилерди алуу үчүн бул жерди басыңыз + Кукилерди алуу мүмкүн болгон жок + Кукилер буга чейин бар + Кукилер ийгиликтүү жаңыртылды + Транскриптти оңдоо + Транскрипттин жүктөлүшүнө тоскоол болгон алдын ала текшерүүнүн катасын жоёт + Сүрөттөгү сүрөттө субтитрлерди көрсөтүү + Навигациялык баскычтар + Төмөнкү навигациялык панелде кайсы баскычтар көрүнөөрүн тандаңыз Башкы бет баскычын жашыруу + Shorts баскычын жашыруу Жасоо баскычын жашыруу Жазылуулар баскычын жашыруу + Билдирүүлөр баскычын жашыруу Издөө баскычын көрсөтүү Издөө баскычынын оорду Оңдоолор баскычын көрсөтүү Оңдоолор баскычынын оорду + Жөндөөлөр баскычынын аракеттери + Иштетилгенде Morphe жөндөөлөрүн, ал эми өчүрүлгөндө YouTube жөндөөлөрүн ачат + «Жаратуу» баскычын «Билдирүүлөр» баскычы менен алмаштыруу + Эгер бул жөндөөнү өзгөртүү натыйжа бербесе, Инкогнито режимине өтүп көрүңүз + Навигация баскычтарынын жапкычтарын жашыруу + Жаңы мазмундун чекитин жашыруу + Навигациялык панельди жашыруу + "Бул навигациялык панельди, анын ичинде 'Сиз' өтмөгүн толугу менен жашырат. + +Жөндөөлөргө кирүү үчүн, Жалпы $\rightarrow$ Куралдар панели $\rightarrow$ Жөндөөлөрдү көрсөттөө аркылуу Жөндөөлөр баскычын активдештириңиз" + Тар навигациялык баскычтарды иштетүү + Баскычтардын ортосундагы горизонталдык аралыкты азайтат + Жазуусу жок навигациялык баскычтарды өчүрүү + Эгер YouTube навигациялык баскычтарды жазуусуз жиберсе, анда аларды жазуулары менен көрсөтөт + Курал панелинин навигациялык өтмөлөрүн өчүрүү + Эгер YouTube аны Башкы беттин жогору жагындагы өтмөлөргө көчүрсө, навигациялык панелинде Жазылууларды көрсөтөт + Навигациялык панель анимацияларын күйгүзүү + Вкладкалардын ортосунда которулганда өтүүлөрдү анимациялайт + Навигация панелинин автоматтык түрдө жашырылышын өчүрүү + Скроллинг учурунда навигациялык панельди көрүнүштө сактайт + Навигациялык панельдин стили + Негизги + Жарым ачык + Тунук + Жыткыч + Бул стиль сиздин түзмөгүңүз же YouTube версиясы тарабынан колдонулбашы мүмкүн жана бул макеттеги каталарга же программанын иштебей калышына алып келиши мүмкүн + Куралдар панели + Куралдар панелинде кайсы компоненттер пайда болорун тандаңыз + Cast баскычын жашыруу + Чат баскычын жашыруу + Микрофон баскычын жашыруу Издөө баскычын жашыруу + Иштетилгенде, басуу Morphe жөндөөлөрүн, ал эми узак басуу YouTube жөндөөлөрүн ачат; алмаштыруу үчүн өчүрүңүз + Ачыла турган меню + Ачылма менюда кайсы компоненттер пайда болорун тандаңыз + \"Кошумча жөндөөлөр\" / Кененирээк менюсун жашыруу + \"Айланалык режим\" менюсун жашыруу + "\"Audio Track\" менюсу жашыруун + +\"Audio Track\" менюсун көрсөтүү үчүн, \"Spoof video streams\" параметрин \"Android Studio\" жана \"Android VR Downgraded\" эмес башка каалаган клиентке өзгөртүңүз" + \"Аудио треги\" менюсун жашыруу + \"Аудио треги\" менюсунун төмөнкү бөлүгүн жашыруу + Субутирлер менюсун жашыруу + Субутирлер менюсунун төмөнкү бөлүгүн жашыруу + Субутирлер менюсунун жогорку бөлүгүн жашыруу + \"Жардам & кайтарым байланыш\" менюсун жашыруу + \"YouTube Music менен угуу\" менюсун жашыруу + \"Экранды бөгөттөө\" менюсун жашыруу + \"Видеону кайталоо\" менюсун жашыруу + \'Жолдо\' менюсун жашыруу + Сапат менюсун жашыруу + Сапат менюсунун төмөнкү бөлүгүн жашыруу + Сапат менюсунун жогорку бөлүгүн жашыруу + \"Уйку таймери\" менюсун жашыруу + \"Оynatationылдыгы\" менюсун жашыруу + \"Туруктуу үн\" менюсун жашыруу + \"VR\'де көрүү\" менюсун жашыруу + Автоплей алдын ала көрүүсүн жашыруу + Автоплей башталардан мурун толук экрандагы алдыдагы видеонун алдын көрүүнү жашырат + Үстүңкү баскычтар + Видео плеердин үстүндө кайсы баскычтар пайда болорун тандаңыз + Автоплей баскычын жашыруу + Субтитрлер баскычын жашыруу + Cast баскычын жашыруу + Жыгуу баскычын жашыруу + Толук экран баскычын жашыруу + Плеер башкаруу баскычтарынын фонун жашыруу + Видео плеердин башкаруу баскычтарын курчоочу кара көлөкөнү жашырат + Башкаруу баскычтарынын фонунун тузралыгы + Плеердин башкаруу баскычтары жана убакыт панелинин артындагы фондун тунуктугун жөнгө салыңыз (0 - көрүнбөйт, 100 - толук түстө) + Мурунку & Кийинки баскычтарын жашыруу + Оңдоолор баскычын жашыруу + Аяктоо экранынын карталарын жашыруу + Ambient режими + Ambient режимдин көрүнүүсүн жана кубаттуулук чектөөлөрүн башкаруу + Ambient режиминин чектөөлөрүн айланып өтүү + Ambient режимге батареяны үнөмдөө режиминде активдүү калууга мүмкүнчүлүк берет + Ambient режимин өчүрүү + Толук экранда Ambient режимин өчүрүү + Маалымат карталарын жашыруу + Сандардын айлануу анимацияларын өчүрүү + Лайктардын жана көрүүлөрдүн санындагы саналуу анимациясын токтотот + Видео ойноткучтун издөө тилкесин жашыруу + Видео миниатюраларынын издөө тилкесин жашыруу + Сиз көргөн видеолордун кичинекей сүрөттөрүндө көрсөтүлгөн көрүү прогрессинин тилкесин жашырат + Толук экрандагы чоң издөө тилкесин күйгүзүү + Shorts бөлүмүн жашыруу + Канал баракчасында жашыруу + Баштапкы беттин лентасында & байланышкан видеолордо жашыруу + Издөө натыйжаларында жашыруу + Жазылуулар лентасында жашыруу + Видеонун сүрөттөөсүндө жашыруу + Көрүлгөн видеолордун тарыхында жашыруу + Shorts ойноткучу + Shorts плееринде кайсы компоненттер көрүнөрүн тандаңыз + Жактоо үчүн эки жолу басуу функциясын өчүрүү + AI баскычын жашыруу + \'Автоматтык түрдө дубляждалган\' белгисин жашыруу + Канал панелисин жашыруу + «Комментарийлер» баскычын жашыруу + Эффект баскычын жашыруу + Видео шилтемесинин белгисин жашыруу + Жесттердин коңсилүүлөрүн жашыруу + \'Жашыл экран\' баскычын жашыруу + Хэштег баскычын жашыруу + Маалымат панелин жашыруу + Кошулуу баскычын жашыруу + «Жакты» баскычын жашыруу + «Жакты» баскычынын фонтан анимациясын жашыруу + Түз эфир алдын көрүүнү жашыруу + Жайгашуу белгисин жашыруу + Навигациялык панельди жашыруу + \'Жаңы билдирүүлөр\' баскычын жашыруу + Токтотулгандагы бетти жаап турган баскычтарды жашыруу + Алдын ала көрүү комментарийин жашыруу + «Ремикс» баскычын жашыруу + \'Сактоо\' баскычын жашыруу + \'Музыканы сактоо\' баскычын жашыруу + Издөө сунуштарын жашыруу + «Бөлүшүү» баскычын жашыруу + Дүкөн баскычын жашыруу + «Үн» баскычын жашыруу + Үндүн метамаалымат белгисин жашыруу + Жазылуу баскычын жашыруу + \'Super Thanks сатып алуу\' баскычын жашыруу + Белгиленген товарларды жашыруу + «Келечектеги» баскычын жашыруу + \'Бул үндү колдонуу\' баскычын жашыруу + \"Бул шаблонду колдонуу\" баскычын жашыруу + Видеонун аталышын жашыруу + Аяктоо экранындагы сунушталган видеону жашыруу + "Видео аяктагандан кийин пайда болгон сунушталган видеолордун катмарын жашырат + +Бул жөндөө автоплей өчүрүлгөн учурда гана иштейт. Сиз аны YouTube'да өзгөртө аласыз: +Жөндөөлөр → Кайра ойнотуу (же Автоплей) → Кийинки видеону автоплей кылуу" + Жакын темадагы видеолорду жашыруу + \"Көбүрөөк видеолор\" катмарын жашыруу + Толук экрандуу режимде жогору жылдырганда көрсөтүлгөн \"Көбүрөөк видеолор\" катмарын жашырат + Видеонун убактысын жашыруу + Плеердин терезе панелдерин өчүрүү + Плейлист жана түз эфирдеги чат панелдеринин автоматтык түрдө ачылышын өчүрөт + Плейлисттин автоплейин өчүрүү + Плейлисттердин кийинки видеого автоматтык түрдө өтүшүн алдын алат + \'Көрүүгө улантасызбы?\' деген суроону өчүрүү + Видео аяктаганда толук экрандуу режимден чыгуу + Өчүрүлгөн + Портрет гана + Ландшафт гана + Портрет жана ландшафт + Толук экранда альбомдук багытты мажбурлоо + Толук экранга өткөндө плеерди альбомдук багытка буруу. Толук экран альбомдук багытта ачыла турган кичинекей экрандарда жеткиликсиз + Толук экрандык жесттер + Толук экрандуу режимдеги плеер жесттери + Портреттик режимдеги жогору сыргуу жестин өчүрүү + Портреттик режимдеги төмөн сыргуу жестин өчүрүү + Горизонттук багытта ылдый сыпыруу ишаратын өчүрүү + Жакындатуу жесттерин өчүрүү + Чымчыктап масштабтоону калыбына келтирүү + Видеолорду толук экрандуу режимде ачуу Иштен чыгарылган + Портрет + Ландшафт + Толук экрандагы видеонун масштабы + Негизги + Созуу (экранды толтуруу) + Жакындатуу (толугу менен толтуруу үчүн кесүү) + Видео масштабдоо баскычын көрсөтүү + Default, Stretch жана Zoom режимдерин алмаштыруу үчүн басыңыз + Толук экранда гана көрсөтүү + Портреттик плеерде видео масштабтоо баскычын жашыруу + Үнсүз кылуу баскычын көрсөтүү + Плеердин катмарынын тунуктугу + Тунуктуулук деңгээлин жөгөлтүү (0 - тунук, 100 - жабык) + Издөө панели Иштен чыгарылган + Кең + Абдан кең + Түзмөктүн жасалышы + Негизги + Телефон + Планшет + Автомобиль + "Өзгөртүүлөр төмөнкүлөрдү камтыйт: + +Планшеттин макети +• Коомчулук жазуулары жашырылган +• Лентадагы ойнотуу жөндөөсү жеткиликсиз +• Shorts бөлүмүндө Ремикс баскычы жана Үн баскычы жеткиликсиз +• \"Көбүрөөк видеолор\" катмары жеткиликсиз болушу мүмкүн +• Эски видеонун бүктөлбөгөн аракеттер панели көрсөтүлгөн +• Чыгуучу менюнун элементтери туура иштебеши мүмкүн + +Автоунаа макети +• Лента темалар жана каналдар боюнча уюштурулган +• Лентадагы ойнотуу жөндөөсү жеткиликсиз +• Shorts бөлүмүндө Ремикс баскычы жана Үн баскычы жеткиликсиз +• \"Көбүрөөк видеолор\" катмары жеткиликсиз болушу мүмкүн +• Shorts кадимки плеерде ачылат +• Эски видеонун бүктөлбөгөн аракеттер панели көрсөтүлгөн" + Плеерде планшеттин макетин күйгүзүү + "Плеердин эски макетин калыбына келтирет + +Чектөө: Эгер 'Эски видео action панелин калыбына келтирүү' өчүрүлгөн болсо, 'Көбүрөөк видеолор' катмары жеткиликсиз болушу мүмкүн" + 21.11.48 - Котормолордун эски жүрүм-турумун калыбына келтирүү + 20.39.41 - Плейлистке сактоонун эски вариантын калыбына келтирүү + 20.28.41 - Калың белгилерди өчүрүү + 20.13.41 - Видео ойноткучтун төмөнүндө каналдын атын калыбына келтирүү + Системалык бөлүшүү баракчасын ачуу + Мазмунду бөлүшкөндө колдонмонун ичиндеги эмес, системалык бөлүшүү баракчасын ачат + YouTube Music баскычтарын алмаштыруу + Стандарттык YouTube Music колдонмосунун ордуна сиз тандаган музыкалык колдонмону ачат + Музыкалык колдонмонун пакеттин аталышы + Орнатылган музыкалык колдонмоңуздун пакеттин аталышы + Баштапкы барак + Ыраазы болгон + Бардык жазылуулар + Каналдарды издөө + Курстар / Окуу + Издөө + Мода & сулуулук + Оюндар + Тарых + Китепкана + Жаккан видеолор + Түз эфир + Фильмдер + Музыка + Жаңылыктар + Билдирүүлөр + Плейлисттер + Издөө + Соода + Shorts + Спорт + Жазылуулар + Тренддер + Виртуалдык реалдуулук + Кийин көрүү + Сиздин клиптериңиз + Ишке киргенде кайра улантууну өчүрүү + Shortsту ачуу үчүн + Shorts плеери + Кадимки плеер + Кадимки плеер толук экранда + Shorts автоплей + Учурдагы видеону кайталоонун ордуна автоматтык түрдө кийинки Short видеосно өтөт + Арткы планда автоматтык түрдө ойнотуу + Арткы планда ойнотуу учурунда кийинки Short-ко автоматтык түрдө өтөт + Түз эфирдеги Shorts видеолорун өткөрүп жиберүү + Shorts лентасында пайда болгон түз эфирлерди өткөрүп жиберет + Ленталарда ойнотуу + Дайыма күйүк + Жалгыз Wi-Fi аркылуу Өчүк + Түпкү аталыштарды калыбына келтирүү + Автоматтык түрдө которулган версиялардын ордуна видеолордун түпкү аталыштарын, видеолордун сүрөттөмөлөрүн жана каналдардын сүрөттөмөлөрүн көрсөтөт + Аталыш жүктөлүүдө… + Лентаны автоматтык түрдө жаңыртууну өчүрүү + Ленталар эскиргенден кийин алардын автоматтык түрдө жаңыртылышын алдын алат + Лентанын картасынын макетти + Классикалык + Карталар + Жактыруу, комментарий жана сактоо батмалары бар карталар Пикирлер + Миниплеерди кайра жандарууну өчүрүү + Тиркеме ишке киргенде пайда болгон YouTube Premium функциясы болгон «Көрүүгө улантуу» миниплеерин өчүрөт + Миниплеер + Кичирейтилген видео ойноткучтун көрүнүшүн жана жүрүм-турумун жөнгө салыңыз + Мини-плеердин түрү + Аталышын жана каналды жашыруу + Иштен чыгарылган + Чыгармал + Минималдык + Минималдуу 2 + Планшет + Заманбап 1 + Заманбап 2 + Заманбап 3 + Заманбап 4 + Тегеректелген бурчтарды өчүрүү + Сүйрөп келүү функциясын өчүрүү + Кичирейтилген ойноткучту экрандын ар кандай бурчтарына сүйрөөнү алдын алат + Горизонталдык сүйрөө ишаратын өчүрүү + Мини-ойноткучту экрандан горизонталдуу сүйрөө аркылуу жабууну алдын алат + Горизонталдуу сүйрөө менен ойнотууну токтотуу + Мини-плеер экрандан сыртка сүйрөлгөндө ойнотуу токтотулат + Сүйрөө абалынын позициясын эстеп калуу + Миниплеерди экрандан сыртка сүйрөө анын UI абалын сактайт + Үстүңкү баскычтарды жашыруу + Негизги өлчөм + Экрандагы баштапкы өлчөмдү пиксель менен белгилеңиз + Пиксель өлчөмү %1$s жана %2$s ортосунда болушу керек + Кабулунун тунуктугу + Тунуктукулук деңгээлин жөндөңүз (0 - тунук, 100 - өтпөс) + Артка кайтуу баскычы лентадан чыгат + Артка кайтууну басса, лентанын жогорусуна жылдыруунун ордуна ар дайым колдонмодон чыгат + Анимацияларды мажбурлоо + Жөнөкөй + Дайыма күйүк + Дайыма өчүк + Статус балкасын жашыруу + Системалык статус балкасын жашырат. Аны убактылуу көрсөтүү үчүн жогорку чеtten төмөн сүрүңүз + Статус барынын жогорку панелдерди жаап алганын оңдоо + Статус барынын түзүлүшүн бирдей сактайт, ошондо ал жогорку панелдерди жабабыз жана плеердин үстүндө боштук калбайт. Эгер колдонмо ката берип жабылса же жогорку панелдер туура эмес көрүнсө, бул функцияны өчүрүңүз + Жалтырак + Бул түзүлүш Glow жүктөө экранын колдобойт + Жарык теманын фону + Ак + Material You (Ак) + Ачык кызгылт + Ачык көк + Ачык жашыл + Ачык сары + Ачык кызгылт сары + Ачык кызыл + Custom ачык фон + Ачык теманын фон түсүн орнотуңуз + Алдыңкы түстү өзгөртүү + Текст жана иконкалар үчүн тема түсүн колдонуу + Градиенттүү жүктөө экранын күйгүзүү + Жүктөө экранында бир түстүн ордуна градиенттик фонду көрсөтөт + SeekBar\'дин自定义 түсүн күйгүзүү + Издөө панелинин түсү + Видеонун жүрүш панелинин негизги түсүн белгилеңиз + Издөө панелинин акценттик түсү + Видеонун жүрүш панелинин экинчи түсүн белгилеңиз + SeekBar\'дин түсүнүн мааниси туура эмес + Түп нуска аналог + Түп нуска торт + Түп нуска металл + Башкы логотип + Сыягыт + Кадимки + Ыйыкталган + Сүрөттөрдүн региондук чектөөлөрүн айландыруу + Чектөөлөр коюлган өлкөлөрдө жок сүрөттөрдү оңдоо үчүн миниатюра суроо-талаптарын альтернативдүү серверге багыттайт + DeArrow аталыштары + Башкы бет + Жазылуулар + Сиз + Плеердин плейлисттери & сунуштар + Издөө жыйынтыктары + DeArrow белгисин көрсөтүү + Коллективдик видео аталыштарында DeArrow белгисин көрсөтөт + Casual режими + Эгер DeArrow колдонуучулары түп нусқа аталышын жактырышат деп добуш беришсе, DeArrow аталышынын ордуна түп нусқа аталышын көрсөтөт + Түп нусқа аталышын көрсөтүү үчүн minimum casual добуштары + Түпкү аталышын көрсөтүү үчүн керек болгон бир категориядагы, мисалы, кызыктуу же чыгармачылык, жөнөкөй добуштардын саны + DeArrow миниатюралары + Түп нуска миниатюралар + DeArrow & түп нуска миниатюралары + DeArrow & статикалык кадрлар + Статикалык кадрлар + "DeArrow кликбейт标题ларды жана miniatureларды такыраак жана аз сенсациялуу, коомчулук тарабынан түзүлгөн варианттары менен алмаштырат. Аталыштар коомчулук тарабынан жазылып, добуш берилет, ал эми miniatureлар видеодон алынган кадрлар болуп саналат + +Видеолор хэштелген видео ID'синин кыска префикси аркылуу суралган, ошондуктан сервер сиз так кайсы видеону көрүп жатканыңызды биле албайт. Эгерде видеонун DeArrow miniatureсы жок болсо, түп нускасы же стоп-кадр көрсөтүлөт" + Байланыш катасы жөнүндө эскертүүлөрдү көрсөтүү + DeArrow сервери жеткиликсиз болгондо билдирүүнү көрсөтөт + API чекит (endpoint) + DeArrow миниатюраларынын кэш чекиттеринин URL дареги + Видеонун статикалык кадрлары + Статикалык кадрлар ар бир видеонун башынан / ортосунан / аягынан алынат. Бул сүрөттөр YouTube платформасына камтылган жана эч кандай тышкы API колдонулбайт + Видео кадрын алуу + Башынан + Ортосунан + Аягынан + DeArrow убактылуу жеткиликсиз (статус коду: %s) + DeArrow убактылуу жеткиликсиз + Плеердин баскычтарынын эски стилин калыбына келтирүү + Видео плеердин калың башкаруу баскычтарын мурунку жука визуалдык стилге кайтарат + Түзүлүштүн өлчөмдөрүн имитациялоо + Муну күйгүзүү видеонун жогорураак сапатын ачууга мүмкүнчүлүк берет + Муну активдештирүү видео ойнотулуусунун үзгүлтүккө учурашына, батареянын тез түгөнүшүнө жана белгисиз терс таасирлерге алып келиши мүмкүн + Гаптикый кайтарым + Түзмөктүн вибригациясын кайсы аракеттер күчүртөрүн тандаңыз + Бөлүмдөрдүн тактилдик кайтарымын өчүрүү + Так издөөнүн тактилдик кайтарымын өчүрүү + Издөө артка алуу тактилдик реакциясын өчүрүү + Басып кармоодагы тактилдик кайтарымды өчүрүү + Жакындатуу тактилдик реакциясын өчүрүү + Эгерде сиз жакында аккаунтуңуздун кирүү маалыматтарын өзгөрткөн болсоңуз, анда MicroG колдонмосун өчүрүп, кайра орнотуңуз + Шилтемелердин кайра багытталышын айланып өтүү + Слептөөчү шилтемени (youtube.com/redirect) айланып өтүп, түздөн-түз түп нуска шилтемени ачат + Шилтемелерди браузерде ачуу + Шилтемелерди колдонмонун ичиндеги браузердин ордуна тышкы браузерде ачат + Оynтуу буфердин өлчөмү + Сыйыртуу + Төмөн + Орточо + Максималдуу + Видео кечигүүсү + Видеону Bluetooth кулакчындар сыяктуу кечигип жаткан аудио менен синхрондоштуруу үчүн кечиктирет (0-5000 мс) + Кечигүүны Bluetooth аудиосу менен гана колдонуу + Видеону Bluetooth аудиосу туташтырылганда гана кечиктирет, ошондо телефондун динамиги синхрондуу бойдон калат + Видео кечиктирүү баскычын көрсөтүү + Кечиктирүүнү 0 мс кылып кайра орнотуу үчүн басып туруңуз + Видео кечиктирүү менюсун көрсөтүү + Миллисекундалар менен кечиктирүү + Авто + Видео сапатын эстеп калуу + Сиз тандаган сапатты кийинки бардык видеолорго колдонот + Сапаттын өзгөрүшү жөнүндө билдирүүлөр + Жаңы стандарттуу сапат сакталгандыгында билдирүү көрсөтөт + Wi-Fi тармагындагы видеонун стандарттуу сапаты + Мобилдик тармактагы видеонун стандарттуу сапаты + Shorts сапатын эстеп калтыруу + Сиз тандаган сапатты кийинки бардык Shorts видеолоруна колдонот + Wi-Fi тармагындагы Shorts стандарттуу сапаты + Мобилдик тармактагы Shorts сапатынын стандарттык mứcү + Мобилдик байланыш үчүн стандарттуу сапат %s болуп өзгөрдү + Wi-Fi үчүн стандарттуу сапат %s болуп өзгөрдү + Shorts мобилдик сапаты %s болуп өзгөрдү + Shorts Wi-Fi сапаты %s кылып өзгөртүлдү + Жайылтуу ылдамдыгы баскычын көрсөтүү + Ылдамдыкты стандарттуу абалга кайтаруу үчүн басып кармаңыз + Сапат баскычын көрсөтүү + Сапатын стандарттуу абалга келтирүү үчүн басып тургула + Сапат баскычында резолюцияны көрсөтүү + FHD сыяктуу белгинин ордуна 1080 сыяктуу резолюцияны көрсөтүү + Жайылтуу ылдамдыгынын自定义 менюсү + Жайылтуу ылдамдыгынын эски менюсун калыбына келтирүү + Жайылтуунун自定义 ылдамдыктары + Каалаган ойнотуу ылдамдыгын киргизиңиз + Жакшыртылган ойнотуу ылдамдыгы %s баатынан аз болушу керек + Жайылтуунун自定义 ылдамдыктары туура эмес + Автоматтык + Оюнтуу ылдамдыгын басып тургула + Экранды басып турганда ылдамдыкты көбөйтүүнү белгилеңиз, өчүрүү үчүн 0 белгилеңиз + Ойнотуу ылдамдыгын эстеп калуу + Сиз тандаган ылдамдыкты кийинки бардык видеолорго колдонот + Ылдамдыктын өзгөргөндүгү жөнүндө билдирүүлөр + Жаңы стандарттуу ойнотуу ылдамдыгы сакталганда билдирүү көрсөтөт + Жайылтуунун стандарттык ылдамдыгы + Shorts видеолорунун стандарттуу ойнотуу ылдамдыгы + Shorts видеолорунда ойнотуу ылдамдыгын эстеп калуу + Сиз тандаган Shorts ылдамдыгы келүүчү бардык Shorts видеолоруна колдонулат. Эгер өчүрүлсө, Shorts плеерин жапкандан кийин ылдамдык стандарттуу абалга кайтат + Shorts менюсунда ойнотуу ылдамдыгын көрсөтүү + Shorts ойноткучүнүн менюсуна «Оynтуу ылдамдыгы» пунктун кошот + Ойнотуу ылдамдыгы · %s + Ыйыкталган ойнотуу ылдамдыгына өзгөртүлдү: %s + Музыка үчүн кадимки ылдамдыкты колдонуу + Музыкалык видеолорду ар дайым 1.0x ылдамдыкта ойнотуу + Каналдын ылдамдык ак тизмеси + Ак тизмеге кирген каналдардын видеолору ар дайым 1.0x ылдамдыкта ойнотулат + Каналды ылдамдыктын ак тизмесине кошуу + Каналды ылдамдыктын ак тизмесинен алып салуу + Аудио тембринин башкаруусун күйгүзүү + Оynтуунун стандарттык аудио тембри + Стандарттык аудио тембри төмөнкүлөргө өзгөртүлдү: %s + Аудио убакытын чоюу + Ылдамдыкты жана тембрди өз алдынча жөндөөнү мүмкүндүк берет, аудио артефакттарына алып келиши мүмкүн + Түз эфирди ойнотуу позициясын эстеп калуу + Убактылуу түз эфирлер үчүн, учурдагы түз позицияга өтүүнүн ордуна, сиздин ойнотуу позицияңызды эстеп калат жана ошол жерден улантат + Бул функция 24/7 түз эфирлер жана башка «дайыма иштеген» эфирлер менен иштебейт + HDR видеону өчүрүү + HDR видеону мажбурлоо + Тиркемени бул түзмөктү HDR мүмкүнчүлүгүнө ээ деп таанууга мажбурлайт + "Эгерде HDR декоддоо иштесе, бирок YouTube HDR сунуштабаса, мисалы, HLG'ни жашырган айрым HyperOS түзмөктөрү сыяктуу, муну колдонуңуз. + +Чектөөлөр: +• Эгерде экран чындап эле HDR көрсөтө албаса, түстөр туура эмес көрүнүшү мүмкүн +• Эгерде декодер HDR'ди иштете албаса, ойнотуу ката болушу мүмкүн +• HDR дагы деле видео агымдарын имитациялаган кардардан көз каранды" + Видео кодеги + Негизги + AVC\'ны мажбурлоо үчүн, \'Видео агымдарын имитациялоо\' бөлүмүн \'Android Studio\', \'Android VR\' же \'visionOS\' деп өзгөртүңүз + "VP9 +• Батареянын çalışma убактысын жакшыртып, ойнотуудагы үзгүлтүктөрдү жоюшу мүмкүн +• AV1 караганда көбүрөөк интернет трафигин колдонот + +AVC (H.264) +• 'Android Studio', 'Android VR' же 'visionOS' видео агымын имитациялоочу кардарды талап кылат +• Батареянын çalışma убактысын дагы да жакшыртышы мүмкүн +• Эски түзүлүштөрдө жоголгон видео resoluçãoларын калыбына келтире алат +• Максималдуу resolução 1080p +• HDR видеолор AVC колдобошу мүмкүн +• Кээ бир түзүлүштөрдө AVC'ни мажбурлап күйгүзүү мүмкүн эмес" + Кеңейтилген сапат менюсун ачуу + Сапатты тандоо менюсүн аттап, түз эле Жогорку сапат менюсун ачат + Premium сапаттын опцияларын жашыруу + Видео сапатына артыкчылык берүү + Кодектин натыйжалуулугунан көрө жеткиликтүү эң жогорку видео сапатына артыкчылык берет + Эгер бул жөндөө өчүрүлгөн болсо, анда эски видеолордо видео сапатынын параметрлери көрсөтүлбөй калышы мүмкүн + \'Android VR\' / visionOS AV1 үчүн уруксат берүү + \'Android VR\' / visionOS кардары колдонулганда программалык AV1 декоддоосуна уруксат берет + "Бул жөндөөнү активдештирүү программалык AV1 декодингди колдонушу мүмкүн. + +AV1 менен видео ойнотулганда катырылыштар болушу же кадрлар түшүп калышы мүмкүн" + Спуфингдин терс таасирлери + • 1-минутта ойнотуу катасы болушу мүмкүн + • \'Аудио жологу\' менюсу жок + • AV1 видео кодеги жок + • 360° VR иммерсивдүү режим жеткиликсиз + • Туруктуу үн жеткиликтүү эмес + • Жайылтуу 1 секунд эрте аякташы мүмкүн + • Деобфускация ката болсо, ойнотуу иштебей калышы мүмкүн + • Түп нуска аудиону мажбурлоо жеткиликтүү эмес + • Android VR эсебине кирүү талап кылынат + • Сапат 720p менен чектелиши мүмкүн + Плеердин JavaScript варианты + \'Нерддер үчүн статистикада\' имитациялоону көрсөтүү + Эски видео колдонуу панелин калыбына келтирүү + "Видеонун аракет баскычтарын жашыруу мүмкүн болбогон катаны оңдойт + +Чектөө: Эски стильдеги «Көбүрөөк видеолор» катмарына кайтарылат" + Плеердин иконкасынын стили Өзүнөн өзү + Жалпы + Жука + Калың + Ыйыкталган + Shorts иконкасынын стили + Сериялардын трекери + Плейлисттерди серия катары байкап, өз ордуңуซду сактаңыз + Серияларды байкоону күйгүзүү + Тарыхка Сериялар барагын жана плейлисттерге Серияны байкоо мүмкүнчүлүгүн кошот + Навигация панелинде серияларды көрсөтүү + Сериялардын трекери баскычын көрсөтүү + Учурдагы видеонун сериясын байкап туруу же ачуу үчүн плеерге баскыч кошот + Көз жоологон сериялардын жүрүшүн жаздоо + Сакталган сериялар жана закладкалар үчүн бул телефондо ойнотууну жаздоо + Жокко чыгаруу + Сактоо + Закладканы сактоо + Сериянын аталышы + Сериянын параметрлери + Китепкананын параметрлери + Эпизоддорду жаңыртуу + Атын өзгөртүү + Плейлистти YouTube\'дан ачуу + Китепканадан алып салуу + Плейлисттин шилтемеси же IDси + Жергиликтүү прогресстин баарын тазалоо + Тарыхты тазалоо + Жергиликтүү прогресстин баарын тазалайсызбы? + Бул ар бир видео үчүн позицияларды, каралган четтөөлөрдү жана улантуу тандоолорун тазалайт. Сакталган сериялар жана алардын каталогдору калат. Эгер жазуу күйгүзүлгөн болсо, жаңы ойнотуу жазылат + Көрүлгөн деп белгилөө + Көрүлбөгөн деп белгилөө + Автоматтык статусду колдонуу + 0:00дөн кайра баштоо + 0:00тан кайра ойнот + Статус сакталды + Сактоону кайра аракет кылуу + Мурунку 100 + Кийинки 100 + Сакталган видеону ачуу + Жергиликтүү прогресс тазаланды + Сериалдарыңызды көрүү үчүн \"Сиз\" бөлүмүнөн Тарыхты ачыңыз + Көрүү тарыхы + Сериалдар + Сакталган сериалдар жүктөлүп жатат… + Эпизоддорун көзөө үчүн плейлистти 구독 кылыңыз + Плейлист белгиси + Эпизоддор жаңыланып жатат… + Бардык сериялар + Көрүлгөн + Эпизоддор жүктөлүүдө… + Көбүрөөк параметрлер + "Прогрессти сакталган серияларыңыз жана закладкаланган видеолоруңуз үчүн гана жазыңыз + +Бул китепкана ушул түзмөктө сакталат жана YouTube аккаунттары арасында бөлүшүлөт. YouTube'дун пауза коюу жана тарыхты тазалоо башкаруулары аны өзгөртпөйт. Аны өчүрүү үчүн Сериялар → Көбүрөөк → Жергиликтүү прогрессти тазалоо функциясын колдонуңуз + +Автоматтык жазуу инкогнито режиминде, аккаунттун идентификатору жеткиликсиз болгондо же сиз башка аккаунтка өткөндө токтойт. Ал аккаунт менен колдонуу үчүн көзөөнү кайра иштетиңиз" + Көзөөнү инкогнито режиминде же YouTube учурдагы аккаунттун абалын жүктөөдөн мурун иштетүү мүмкүн эмес. Башкы бетке кайтып, кайра аракет кылыңыз. Кирүү талап кылынбайт + Жергиликтүү көзөмөлдү күйгүзүү + Прогрессти жаздыруу өчүрулгөн + %1$s жерден улантуу + %1$s / %2$s эпизод каралды + Эпизод %1$s · %2$s + %3$s эпизоддун %1$s-%2$s эпизоддору + Эпизоддорду жүктөө үчүн жаңыртыңыз. Жеке плейлисттер үчүн аккаунтка кирүү керек; алардын закладкалары YouTube\'да дагы деле ачылат + YouTube бул эпизодду ача алган жок + YouTube бул плейлистти ача алган жок + Эпизодду тандаңыз + Актуалдуу + Ойното турган эпизоддор жок + Сакталган видео · %1$s + Сериалды theo кылуу + Сериалды көрүү + Улантуу + Бул жерден баштоо + Башталышы сакталды + Бул жерге чейин көрүлдү + Эпизоддор көрүлдү деп белгиленди + Артка кайтаруу + Ишке киргизүү + Прогрессти ушул түзүлүштө жаздоо + "Сиздин серияларыңыз жана прогрессиңиз ушул түзүлүштө сакталат жана YouTube аккаунттары арасында бөлүштүрүлөт. YouTube тарыхынын башкаруу器лары аларды өзгөртпөйт. Прогрессти Сериялар менюсунан тазалаңыз. + +Автоматтык жазуу ыктыيارдуу. Ал инкогнито режиминде же аккаунт алмашканда токтойт" + Эпизоддорду жүктөө мүмкүн болгон жок. Сиз плейлистти закладка катары сактап алсаңыз болот + Сакталбаган өзгөртүүлөр өтө көп. Алгач кайра сактоого аракет кылыңыз + Сактоо процесси жүрүп жатат. Кайра аракет кылыңыз + Эпизоддорду жүктөө процесси жүрүп жатат. Кайра аракет кылыңыз + Сактоо мүмкүн эмес. Сакталган маалыматтарыңыз сакталды + Өзгөртүүлөрдү сактоого мүмкүн болгон жок. YouTube\'ду жабуудан мурун кайра сактап көрүңүз + Бул серия сакталып калган же анын плейлисттиси өзгөргөн. Аны кайра ачыңыз + Бул серия өзгөргөн же өчүрүлгөн. Китепкананы кайра ачыңыз + Плейлист өзгөрдү. Эпизодду кайра тандаңыз + Бул эпизод жеткиликсиз. Башкасын тандаңыз + Сериянын атын киргизиңиз + YouTube плейлистинин шилтемесин же IDсин киргизиңиз + Жана кайра жаңылоодон мурун күтө туруңуз + Эпизоддорду жүктөө мүмкүн болгон жок. Кийинчерээк кайра аракет кылып көрүңүз + Өзгөртүүнү аткаруу мүмкүн болгон жок. Кайра аракет кылып көрүңүз + Улантууну тастыктоо мүмкүн болгон жок. Плеерди текшерип, кайра аракет кылып көрүңүз + Көзделген сериялар + Бул видео бул плейлистте жок + Плейлисттер издөө... + Плейлисттин шилтемесин чаптоо + Плейлист табылган жок + Эпизоддор жок + Көрүлгөндөрдү жашыруу + Мурунку эпизоддорду көрүлдү деп белгилөө + Башталышы: %1$s + Эпизоддордун тартиби + Плейлисттин тартиби + Плейлисттин тартибин тескери кылуу + %1$s көрүүлүү + %1$s көрүүлүү + Эпизоддорду көрүлдү деп белгилөө + Кеминде %1$s%% көрүлдү жана максималдуу 90 секунд калды, же %2$s секунд калды + Эпизодду белгиленген чектен кийин автоматтык түрдө көрүлдү деп белгилөө. Пайыздык эреже ошондой эле 90 секунддан көп убакыт калбашын талап кылат, ошентип узун эпизоддордун аягы өткөрүлүп кетпейт. Калган убакыт эрежеси бир мүнөттөн узун видеолорго колдонулат; аны өчүрүү үчүн 0 деп коюңуз. Өзгөртүүлөр келечектеги прогресс жаңыртууларына колдонулат. Кол менен белгиленген көрүлдү/көрүлгөн жок тандоолору сакталат + Көрүлгөн пайызы (1-100) + Калган секундтар (0-300; 0 өчүрөт) + %1$s башынан %2$s чейинки маанини киргизиңиз + YouTube прогрессин колдонуу + Башка түзүлүштө көргөндөн кийин, байкаган серияларыңызды улантыңыз + Сиз катталган сериялардын көрүлгөн статусун жана улантуу позицияларын учурдагы YouTube аккаунтунан окуйт. Бул телефонго дал келген эпизоддор гана сакталат. Текшерүүлөр сериялардын китепканасы ачыкканда жана \"Улантуу\" баскычынан мурун жүргүзүлөт. Башка түзмөгүңүздө YouTube тарыхы күйгүзүлгөн болушу керек + YouTube прогресси текшерилүүдө… + YouTube прогресси жеткиликсиз. Сакталган прогресс колдонулууда + YouTube прогрессинин бир бөлүгү жеткиликсиз. Сакталган позициялар сакталды + Резервдик көчүрүү жана калыбына келтирүү + Сериялардын китепканасын файлга сактаңыз же резервдик көчүрмөнү калыбына келтириңиз + Катталган серияларды, эпизоддордун позицияларын жана көрүлгөн тандауларды сактайт. Жазуу жана YouTube прогрессинин уруксаттары камтылбайт + Резервдик көчүрмөнү сактоо + Резервдик көчүрмөнү калыбына келтирүү + %1$s сериалды кошуу. Китепканаңыздагы %2$s сериал сакталат. Видеонун учурдагы прогресси сакталат + Сериялардын резервдик көчүрмөсү сакталды + %1$s сериал калыбына келтирилди + Бул файл колдоого алынбаган сериалдын резервдик көчүрмөсү же өтө чоң. Сиздин китепканаңыз өзгөргөн жок + Резервдик көчүрмөнү сактоо мүмкүн болбоду. Башка жайды байкап көрүңүз + Серия өчүрүлдү + Бул эпизод эми бирден көп жолу көрүнүп жатат. Кайсынысынан улантууну тандаңыз + Сиз сактаган эпизод бул плейлистте жок. Сакталган позицияңыз сакталган + %1$s эпизоддон улантуу · %2$s + Төмөндөгү каалаган эпизодду да тандасаңыз болот. Мурдагы эпизоддор өзгөрбөйт + Бул плейлист өзгөрдү. Кайсы жерден улантууну тандоо үчүн сериалды ачыңыз + Бул багытта башка эпизоддор жок + %1$s улантуу + Серияларды издөө + Издөөнү тазалоо + Сериялар табылган жок + Аталы жок серия + Жеткилбеген серия + Niconico комментарийлери + Түз эфирдеги чатты жана чаттын кайталоосун видео боюнча жылган комментарийлер катары көрсөтүү + Видео плеерде үстүңкү баскычты көрсөтөт. Комментарийлерди көрсөтүү же жашыруу үчүн басыңыз, тез жөндөөлөр үчүн басып туруңуз + Комментарийдин шрифтинин өлчөмү + Комментарийлердин тексттин өлчөмү (12-40 sp) + Комментардын тунуктугу + Комментардын текстинин жана фонунун тунуктугу + Кыймалоо убактысы + Комментарлар экранды кесип өтүү үчүн канча секунд керектелет. Төмөнкү маанилер тезирээк кыймылдатат + Комментардын максималдуу узундугу + Бул жердегиден көп белгиси бар комментарларды жашырат. Ар бир эмодзи бир белги катары эсептелет + Чектелбеген diff --git a/patches/src/main/resources/addresources/values-lo-rLA/music/strings.xml b/patches/src/main/resources/addresources/values-lo-rLA/music/strings.xml index ccfea4ed71..ec5c625d39 100644 --- a/patches/src/main/resources/addresources/values-lo-rLA/music/strings.xml +++ b/patches/src/main/resources/addresources/values-lo-rLA/music/strings.xml @@ -17,58 +17,675 @@ Second \"item\" text" --> + ຫຼິ້ນຫຼ້າສຸດ + ການສະໝັກ + ວິດີໂອ + ຜົນລັບອື່ນໆ + ເພງທີ່ອັບໂຫຼດແລ້ວ + ອັນບັ້ມທີ່ອັບໂຫຼດແລ້ວ + ສິນລະປິນທີ່ອັບໂຫຼດແລ້ວ + ອັບເດດ patch Jam ຂອງໂຮສ໌ ເພື່ອໃຊ້ການຢຸດຊົ່ວຄາວ ແລະ ເລີ່ມໃໝ່ + ການແບ່ງປັນຄິວ Jam + ເປັນໂຮສ໌ ຫຼື ເຂົ້າຮ່ວມ Jam ທີ່ຢູ່ໃກ້ຄຽງ ແລະ ແກ້ໄຂລາຍຄິວຜ່ານ Jam Layer + ເປີດໃຊ້ງານການແບ່ງປັນຄິວ Jam + ເປີດໃຊ້ການເຊື່ອມຕໍ່ Jam ຫຼັງຈາກ YouTube Music ເລີ່ມເຮັດວຽກໃໝ່ + ດາວໂຫຼດ Jam Layer ເພື່ອເລີ່ມເຮັດວຽກ + ເປີດ Jam Layer ເວີຊັນຫຼ້າສຸດໃນບຣາວເຊີຂອງທ່ານ + ການຄວບຄຸມ Jam + ເປັນໂຮສ໌ ຫຼື ເຂົ້າຮ່ວມ Jam ທີ່ຢູ່ໃກ້ຄຽງ ແລະ ຈັດການເຊດຊັນ + ແພັກເກດ Jam Layer + ເລືອກແອັບຄູ່ຫູທີ່ຕິດຕັ້ງໄວ້ ເພື່ອໃຊ້ໃນການແບ່ງປັນລາຍຄິວທີ່ຢູ່ໃກ້ຄຽງ + ໃຊ້ແພັກເກັດຄູ່ຫູທີ່ຕິດຕັ້ງໄວ້. ການຈັບຄູ່ຕ້ອງໄດ້ຮັບການອະນຸມັດຈາກທ່ານ ແລະ ຕ້ອງມີໂທເຄັນຄວາມສາມາດທ້ອງຖິ່ນ + ເປີດ Wi-Fi ບໍ? + ເປີດ Wi-Fi, ຈາກນັ້ນລອງເລີ່ມ ຫຼື ເຂົ້າຮ່ວມ Jam ອີກຄັ້ງ + ເປີດການຕັ້ງຄ່າ Wi-Fi + ຍົກເລີກ + ໃຊ້ແພັກເກັດ + ປ້ອນຊື່ແພັກເກັດ Android ທີ່ຖືກຕ້ອງ + ບັນທຶກແພັກເກັດແລ້ວ. ຕັ້ງຄ່າ Jam Layer ເພື່ອເຊື່ອມຕໍ່ + ການເຊື່ອມຕໍ່ Jam ຖືກຕັດ + ກະລຸນາເປີດໃຊ້ Jam Layer ກ່ອນ + ເປີດໃຊ້ການແບ່ງປັນຄິວ Jam ໃນການຕັ້ງຄ່າ, ຈາກນັ້ນໃຫ້ເລີ່ມ YouTube Music ໃໝ່ + ກະລຸນາຕິດຕັ້ງແພັກເກັດ Jam Layer ທີ່ເລືອກກ່ອນ + ປ້ອນລະຫັດທີ່ສະແດງຢູ່ເຄື່ອງໂຮສ. Wi-Fi Aware ສາມາດເຊື່ອມຕໍ່ອຸປະກອນທີ່ຢູ່ໃກ້ຄຽງໄດ້ ເຖິງແມ່ນວ່າ LAN ຈະແຕກຕ່າງກັນ + ເຂົ້າຮ່ວມດ້ວຍລະຫັດ + ເຂົ້າຮ່ວມ + ສະແກນ QR + ປ້ອນລະຫັດແປດຕົວອັກສອນ ຫຼື ວາງຄຳເຊີນ + ເຂົ້າຮ່ວມ Jam ຂອງທ່ານ + ຄັດລອກລະຫັດ + ລະຫັດ Jam + ສະແດງ QR + ສຳເລັດ + ເຊີນເຂົ້າຮ່ວມ Jam ຂອງທ່ານ + ສະແກນເພື່ອເຂົ້າຮ່ວມ ແລະ ແກ້ໄຂລາຍການຄິວນີ້. ການສິ້ນສຸດການ Jam ຈະຍົກເລີກຄຳເຊີນ + ຄັດລອກລິ້ງ + ຄຳເຊີນ Jam + Jam ກຳລັງເຊື່ອມຕໍ່ຄືນ; ກະລຸນາລອງໃໝ່ໃນອີກສັກຄາວ + ເພີ່ມເຂົ້າໃນ Jam ແລ້ວ + Jam ບໍ່ສາມາດນຳໃຊ້ໄດ້; ແທຣັກບໍ່ຖືກເພີ່ມເຂົ້າ + Jam ຂອງທ່ານ + ກຳລັງຟັງນຳກັນ + ກຳລັງເຂົ້າຮ່ວມ Jam + ຟັງນຳກັນ + ກຳລັງອັບເດດ Jam… + ກຳລັງຊອກຫາໂຮສຂອງທ່ານໃນ Wi-Fi… + ກຳລັງລໍຖ້າຜູ້ຄົນ + %s ຄົນເຊື່ອມຕໍ່ແລ້ວ + %s ຄົນເຊື່ອມຕໍ່ແລ້ວ + ປ້ອນລະຫັດນີ້ໃນອຸປະກອນທີ່ຢູ່ໃກ້ຄຽງ. Wi-Fi Aware ສາມາດເຮັດວຽກຜ່ານ LAN ທີ່ແຍກກັນໄດ້ເມື່ອໄດ້ຮັບການຮອງຮັບ. ໝົດອາຍຸໃນ %s ນາທີ + ຍ້າຍໄປທີ່ %s ບໍ່? + ເປີດໃຫ້ແກ້ໄຂ + ລັອກການແກ້ໄຂ + ເຊື່ອມຕໍ່ຜ່ານ Wi-Fi Aware + ເຊື່ອມຕໍ່ຜ່ານ Wi-Fi + ເຊື່ອມຕໍ່ຜ່ານ Bluetooth + ບໍ່ຮອງຮັບໃນລະຫວ່າງການ Jam + ການເຂົ້າຮ່ວມຖືກຍົກເລີກ + ກຳລັງເຊື່ອມຕໍ່ຄືນກັບໂຮສຂອງທ່ານ… + ແບ່ງປັນລາຍການຄິວ. ໃຫ້ທຸກຄົນເພີ່ມເພງໄດ້ + ເຊີນຜູ້ຄົນ + ແຂກສາມາດແກ້ໄຂລາຍຄິວໄດ້ + ສິ້ນສຸດ Jam + ໂຮສເປັນຜູ້ເປີດເພງ. ເພີ່ມ, ຍ້າຍ ຫຼື ລຶບເພງໂດຍກົງໃນຄິວຂອງ YouTube Music + ວາງອຸປະກອນທັງສອງໄວ້ໃກ້ກັນ. ທ່ານສາມາດຍົກເລີກໄດ້ໃນຂະນະທີ່ພວກເຮົາກໍາລັງຊອກຫາໂຮສ + ຍົກເລີກການເຂົ້າຮ່ວມ + ອອກຈາກ Jam + ເລີ່ມ Jam + ຕັ້ງຄ່າ Jam Layer + ຈັດການ + ລາຍລະອຽດ + ເປີດ + ກຳລັງໂຫຼດຕົວເລືອກເພງ… + ບໍ່ມີຕົວເລືອກເພງ; ກະລຸນາລອງໃໝ່ + ບໍ່ສາມາດເປີດຕົວເລືອກເພງໄດ້ + ບໍ່ມີບຣາວເຊີສຳລັບດາວໂຫຼດ Jam Layer + ກຳລັງລໍຖ້າຄິວຂອງໂຮສ + ກະລຸນາເປີດເພງໃນ YouTube Music ກ່ອນ + ຫຼິ້ນຢູ່ເຄື່ອງໂຮສ + ອອກຈາກ Jam ແລະ ຫຼິ້ນໃນເຄື່ອງ + ຫຼິ້ນລຳດັບຖັດໄປໃນ Jam + ເພີ່ມເຂົ້າໃນຄິວຂອງ Jam + ຫຼິ້ນແທຣັກ Jam ນີ້ + ເລືອກບ່ອນທີ່ຈະຫຼິ້ນ + ຍ້າຍໂຮສມາທີ່ນີ້ + ເພງຍັງບໍ່ທັນພ້ອມສຳລັບການເລື່ອນຫາ + ຄິວມີການປ່ຽນແປງ; ກະລຸນາລອງໃຊ້ທ່າທາງອີກຄັ້ງ + ໂລໂກ້ສ່ວນຫົວ + ຄ່າເລີ່ມຕົ້ນ + ກຳນົດເອງ + ຫຼິ້ນແທຣັກສຽງຂອງອັນບັ້ມ + ຫຼິ້ນເພງເວີຊັນສຽງຂອງອັນບັ້ມເທົ່ານັ້ນ ແທນທີ່ຈະເປັນວິດີໂອເພງ. ຕ້ອງໃຊ້ \'ປອມແປງສະຕຣີມວິດີໂອ\' + ເປີດໃຊ້ການສະຫຼັບສຽງ/ວິດີໂອ + ລອງປ່ຽນເວີຊັນຂອງແທຣັກສຽງ ແລະ ມິວສິກວິດີໂອທາງການ + ດາວໂຫຼດວິດີໂອເພງ, ພອດຄਾਸ, ຫຼື ເພງ ດ້ວຍແອັບດາວໂຫຼດຂອງບຸກຄົນທີສາມ + ໃຊ້ໂປຣແກຣມດາວໂຫຼດພາຍນອກສຳລັບເມນູ ດາວໂຫຼດ + ຕົວເລືອກ ດາວໂຫຼດ ຂອງເມນູຈະເປີດໂປຣແກຣມດາວໂຫຼດພາຍນອກຂອງທ່ານແທນ + ການດາວໂຫຼດ + "ບໍ່ມີການດາວໂຫຼດ +ເພງທີ່ດາວໂຫຼດແລ້ວຈະປາກົດຢູ່ທີ່ນີ້" + %1$s • %2$s • %3$s MB + %1$s ເພງ • %2$s MB + ຄົ້ນຫາການດາວໂຫຼດ + ຈັດລຽງ + ຈັດລຽງຕາມສິລະປິນ + ຈັດລຽງຕາມຊື່ + ຈັດລຽງຕາມຂະໜາດ + ລຶບທັງໝົດທີ່ດາວໂຫຼດ + ລຶບທັງໝົດທີ່ດາວໂຫຼດບໍ? + ແທຣັກທັງໝົດທີ່ດາວໂຫຼດຈະຖືກລຶບອອກຈາກອຸປະກອນນີ້ + ລຶບລາຍການດາວໂຫຼດແລ້ວ + ບໍ່ພົບຂໍ້ມູນ + ການດຳເນີນການສຳລັບ %s + ຫຼິ້ນ + ຫຼິ້ນ ຫຼື ຢຸດຊົ່ວຄາວ + ກ່ອນໜ້ານີ້ + ຖັດໄປ + ຢຸດຊົ່ວຄາວ + ປິດ + ລຶບ + ລຶບໄຟລ໌ທີ່ດາວໂຫຼດ + ລຶບໄຟລ໌ທີ່ດາວໂຫຼດບໍ? + %s ຈະຖືກລຶບອອກຈາກອຸປະກອນນີ້ + ລຶບໄຟລ໌ທີ່ດາວໂຫຼດແລ້ວ + ບໍ່ສາມາດລຶບໄຟລ໌ທີ່ດາວໂຫຼດໄດ້ + ບໍ່ສາມາດຫຼິ້ນໄຟລ໌ທີ່ດາວໂຫຼດໄດ້ + ດາວໂຫລດສຽງ + ການຫຼິ້ນແບບອອຟລາຍ + ບໍ່ສາມາດລະບຸເພງທີ່ຈະດາວໂຫຼດໄດ້ + ກຳລັງດາວໂຫຼດຢູ່ + ດາວໂຫຼດແລ້ວ + ເລີ່ມການດາວໂຫຼດແລ້ວ + ກຳລັງເລີ່ມດາວໂຫຼດ… + ກຳລັງດາວໂຫຼດ %s%% + ດາວໂຫຼດສຳເລັດແລ້ວ + ດາວໂຫຼດຫຼົ້ມເຫຼວ + ກ່ຽວກັບ Morphe + ໂຄສະນາ + ຟີດ + ທົ່ວໄປ + ເຄື່ອງຫຼິ້ນ + ເນື້ອເພງ + ອື່ນໆ + ພາສາຂອງ YouTube Music + ພາສາຂອງລະບົບ + ຕ້ອງໃຊ້ Android 13 ຫຼື ໃໝ່ກວ່ານັ້ນ + ເຊື່ອງປ້າຍ \'ຮັບ Music Premium\' + ເຊື່ອງໂປຣໂມຊັນ Music Premium + ເຊື່ອງໂຄສະນາວິດີໂອ + ເຊື່ອງປຸ່ມປ່ຽນ ລະຫັດສຽງ / ວິດີໂອ + ເຊື່ອງປຸ່ມດາວໂຫຼດ + ເຊື່ອງປຸ່ມດາວໂຫຼດໃນລາຍການພອດແຄສ + ເຊື່ອງຊັ້ນ Explore + ເຊື່ອງແຖບກັ່ນຕອງ + ເຊື່ອງຊັ້ນແບບຕາຕະລາງ + ເຊື່ອງຊັ້ນແບບແນວນອນ + ເຊື່ອງຊັ້ນແບບລາຍການ + ແຜງເນື້ອເພງ + ເຊື່ອງປຸ່ມແຊຣ໌ + ເຊື່ອງປຸ່ມແປພາສາ + ເຊື່ອງຊັ້ນ \'ຂອງໃໝ່ຈາກ\' + ເຊື່ອງຊັ້ນລາຍການເພງ + ເຊື່ອງປຸ່ມຫຼິ້ນຊ້ຳ + ເຊື່ອງປຸ່ມສຸ່ມເພງ + ເຊື່ອງຊັ້ນ \'ໂທດ່ວນ\' + ຄຳຄິດເຫັນ + ເລືອກສ່ວນປະກອບທີ່ຈະໃຫ້ສະແດງໃນພາກສ່ວນຄຳຄິດເຫັນ + ເຊື່ອງຄຳແນະນຳຂອງຊຸມຊົນ + ເຊື່ອງຂໍ້ຄວາມບໍລິບົດ + ເຊື່ອງປຸ່ມ Emoji + ເຊື່ອງປຸ່ມຂໍ້ມູນ + ເຊື່ອງປຸ່ມປະທັບເວລາ + ການຜ່ານສຽງ + ປະສົມຜສານແທຣັກເຂົ້າກັນຢ່າງນຸ້ມນວນ + ເປີດໃຊ້ງານ crossfade + ເສັ້ນໂຄ້ງການຄ່ອຍໆປ່ຽນ + ກຳລັງໄຟເທົ່າກັນ + ຄ້າງໄວ້ແບບບາງເບົາ + ຜ່ອນຄ່ອຍໆ + ເສັ້ນໂຄ້ງ S ແບບລຽບ + ຂາອອກ + ຂາເຂົ້າ + ໄລຍະເວລາການປ່ຽນຜ່ານ + 250 ມິນລິວິນາທີ + 500 ມິນລິວິນາທີ + 750 ມິນລິວິນາທີ + 1 ວິນາທີ + 2 ວິນາທີ + 3 ວິນາທີ + 4 ວິນາທີ + 5 ວິນາທີ + 6 ວິນາທີ + 7 ວິນາທີ + 8 ວິນາທີ + 9 ວິນາທີ + 10 ວິນາທີ + 11 ວິນາທີ + 12 ວິນາທີ + Crossfade ເມື່ອຂ້າມດ້ວຍຕົນເອງ + ນຳໃຊ້ເອັບເຟັກ crossfade ເມື່ອທ່ານຂ້າມໄປແທຣັກຕໍ່ໄປດ້ວຍຕົນເອງ + Crossfade ເມື່ອເລື່ອນໄປອັດຕະໂນມັດ + ນຳໃຊ້ເອັບເຟັກ crossfade ເມື່ອແທຣັກສິ້ນສຸດລົງຕາມປົກກະຕິ + ການຄວບຄຸມເຊດຊັນ + ກົດປຸ່ມສຸ່ມຄ້າງໄວ້ ເພື່ອຢຸດ ຫຼື ດຳເນີນການ crossfade ຕໍ່ + ຢຸດການເຮັດ Crossfade ຊົ່ວຄາວສຳລັບເຊດຊັນນີ້ + ດຳເນີນການ Crossfade ຕໍ່ + ໂໝດວິດີໂອບໍ່ສາມາດໃຊ້ໄດ້ໃນຂະນະທີ່ເປີດໃຊ້ Crossfade + Crossfade ຖືກປິດໃນຂະນະທີ່ຫຼິ້ນດ້ວຍ SABR + Crossfade ຖືກປິດໃນຂະນະທີ່ກຳລັງ cast + ວິທີການເຮັດວຽກ + "Crossfade ຈະເຮັດໃຫ້ຕອນທ້າຍຂອງເພງໜຶ່ງທັບຊ້ອນກັບຕອນເລີ່ມຕົ້ນຂອງເພງຕໍ່ໄປ ໂດຍໃຊ້ໄລຍະເວລາ ແລະ ເສັ້ນໂຄ້ງຂອງທ່ານ + +ມັນຈະເຮັດວຽກໃນການເລື່ອນໄປໂດຍອັດຕະໂນມັດ ແລະ ໃນການຂ້າມດ້ວຍຕົນເອງ ເຊິ່ງແຕ່ລະອັນຈະມີປຸ່ມເປີດ/ປິດຂອງມັນເອງ. ໃນການເລື່ອນໄປໂດຍອັດຕະໂນມັດ ການປະສົມສຽງຈະເລີ່ມຕົ້ນບໍ່ກີ່ວິນາທີກ່ອນທີ່ເພງຈະຈົບ" + ຂໍ້ຈຳກັດ + "ວິດີໂອເພງຈະຫຼິ້ນເປັນສຽງເທົ່ານັ້ນໃນຂະນະທີ່ເປີດ crossfade. ກົດ shuffle ຄ້າງໄວ້ເພື່ອຢຸດ crossfade ແລະ ເບິ່ງວິດີໂອ + +ວິດີໂອເພງທີ່ເລີ່ມຫຼິ້ນຈະຢູ່ໃນໂໝດວິດີໂອຈົນກວ່າຈະຮອດເພງຕໍ່ໄປ + +ເພງທີ່ສັ້ນກວ່າໄລຍະເວລາ fade ຈະບໍ່ມີການ crossfade + +ໜ້າຈໍລັອກ ຫຼື ການແຈ້ງເຕືອນອາດຈະສະແດງສະຖານະເກົ່າຊົ່ວຄາວໃນລະຫວ່າງການປ່ຽນຜ່ານ + +ການຢຸດ crossfade ເພື່ອເບິ່ງວິດີໂອເພງອາດເຮັດໃຫ້ບາງວິດີໂອກາຍເປັນສີດຳ. ການຢຸດ crossfade ກ່ອນທີ່ທ່ານຈະເລືອກເພງຈະຊ່ວຍຫຼີກລ່ຽງບັນຫານີ້" + ບໍ່ຮອງຮັບ + "ການສົ່ງສັນຍານ ຫຼື ການຈໍາລອງສຽງ (Chromecast, ການຈໍາລອງໜ້າຈໍ ຫຼື ສຽງ) + +ການຫຼິ້ນ SABR (ປິດການຈໍາລອງສະຕຣີມວິດີໂອ)" + ເຄຣດິດ + ຄຸນສົມບັດ Crossfade ໂດຍ VazerOG + ຈົດຈຳສະຖານະການຫຼິ້ນຊ້ຳ + ຈົດຈຳສະຖານະການສຸ່ມ + ຄວາມໄວໃນການຫຼິ້ນ + ຄວາມໄວທີ່ໃຊ້ສຳລັບທຸກແທຣັກ. ທີ່ 100% ຈະໃຊ້ຄວາມໄວຂອງແອັບ, ເຊັ່ນ ຄວາມໄວໃນການຫຼິ້ນພອດແຄສ + ປ່ຽນລະດັບສຽງຕາມຄວາມໄວ + ປິດການປ່ຽນເສັ້ນທາງການບໍ່ມັກ + ປ້ອງກັນການຂ້າມໄປແທຣັກຖັດໄປເມື່ອມີການກົດປຸ່ມບໍ່ມັກ + ການເຮັດວຽກເມື່ອແຕະແທຣັກໃນລາຍການຫຼິ້ນ + ຫຼິ້ນລາຍການຫຼິ້ນ + ຫຼິ້ນສະເພາະແທຣັກທີ່ເລືອກ + ເພີ່ມແທຣັກທີ່ເລືອກລົງໃນຄິວ + ເພີ່ມລົງໃນຄິວແລ້ວ + ເຊື່ອງປຸ່ມ Cast + ເຊື່ອງປຸ່ມປະຫວັດ + ເຊື່ອງປຸ່ມການແຈ້ງເຕືອນ + ເຊື່ອງປຸ່ມຄົ້ນຫາ + ເຊື່ອງປຸ່ມຄົ້ນຫາດ້ວຍສຽງ + ເຊື່ອງປຸ່ມຄົ້ນຫາດ້ວຍສຽງເພງ + ເຊື່ອງປຸ່ມໃໝ່ + ເຊື່ອງປຸ່ມໃໝ່ໃນແຖບຄັງເພງ + ປຸ່ມການຈັດການ + ເລືອກປຸ່ມການຈັດການທີ່ຈະໃຫ້ສະແດງຢູ່ພາຍໃຕ້ເຄື່ອງຫຼິ້ນ + ເຊື່ອງແຖບການຈັດການ + ເຊື່ອງປຸ່ມ ມັກ ແລະ ບໍ່ມັກ + ເຊື່ອງປຸ່ມ ຄຳຄິດເຫັນ + ເຊື່ອງປຸ່ມ \'ຫຼິ້ນຊ້ຳການແຊັດສົດ\' + ເຊື່ອງປຸ່ມ ເນື້ອເພງ + ເຊື່ອງປຸ່ມ ແຊຣ໌ + ເຊື່ອງປຸ່ມ ບັນທຶກ + ເຊື່ອງປຸ່ມດາວໂຫລດ + ເຊື່ອງປຸ່ມ \'ເລີ່ມວິທະຍຸ\' + ເມນູ Flyout + ເລືອກສ່ວນປະກອບທີ່ຈະໃຫ້ປາກົດໃນເມນູ flyout ຂອງເຄື່ອງຫຼິ້ນ ແລະ ຄິວ + ເຊື່ອງສ່ວນປະກອບ 3 ຄໍລຳ + ເຊື່ອງປຸ່ມ ຖືກໃຈ ແລະ ບໍ່ຖືກໃຈ + ເຊື່ອງເມນູ \'Taste match\' + ເຊື່ອງເມນູ \'ເພີ່ມໃນລາຍການ \'ຟັງພາຍຫຼັງ\' + ເຊື່ອງເມນູ \'ເພີ່ມເຂົ້າໃນລາຍຄິວ\' + ເຊື່ອງເມນູ ຄຳບັນຍາຍ + ເຊື່ອງເມນູ \'ລຶບລາຍການຫຼິ້ນ\' + ເຊື່ອງເມນູ \'ຍົກເລີກລຳດັບການຫຼິ້ນ\' + ເຊື່ອງເມນູ \'ບໍ່ແນະນຳສິລະປິນຜູ້ນີ້\' + ເຊື່ອງເມນູດາວໂຫລດ + ເຊື່ອງເມນູ \'ແກ້ໄຂລາຍການຫຼິ້ນ\' + ເຊື່ອງເມນູ \'ໄປທີ່ອັລບັ້ມ\' + ເຊື່ອງເມນູ \'ໄປທີ່ສິລະປິນ\' + ເຊື່ອງເມນູ \'ໄປຫາຕອນ\' + ເຊື່ອງເມນູ \'ໄປຫາພອດແຄສ\' + ເຊື່ອງເມນູช่วยเหลือ + ເຊື່ອງເມນູ \'ໝາຍຕອນວ່າຫຼິ້ນແລ້ວ\' + ເຊື່ອງເມນູ \'ບໍ່ສົນໃຈ\' + ເຊື່ອງເມນູ \'ປັກໝຸດໄວ້ໃນໝາຍເລກດ່ວນ\' + ເຊື່ອງເມນູ \'ຫຼິ້ນຕໍ່ໄປ\' + ເຊື່ອງເມນູຄຸນນະພາບ + ເຊື່ອງເມນູ \'ລຶບອອກຈາກຫໍສະມຸດ\' + ເຊື່ອງເມນູ \'ລຶບອອກຈາກລາຍການຫຼິ້ນ\' + ເຊື່ອງເມນູລາຍງານ + ເຊື່ອງເມນູ \'ບັນທຶກຕອນໄວ້ເບິ່ງພາຍຫຼັງ\' / \'ບັນທຶກລົງໃນຄັງ\' + ເຊື່ອງເມນູ \'ບັນທຶກລົງໃນລາຍການຫຼິ້ນ\' + ເຊື່ອງເມນູແບ່ງປັນ + ເຊື່ອງເມນູ \'ຫຼິ້ນແບບສຸ່ມ\' + ເຊື່ອງເມນູ \'ຕົວຈັບເວລາການນອນ\' + ເຊື່ອງເມນູ \'ເລີ່ມວິທະຍຸ\' + ເຊື່ອງເມນູ \'ສະຖິຕິສຳລັບຜູ້ຊ່ຽວຊານ\' + ເຊື່ອງເມນູຕິດຕາມ + ເຊື່ອງເມນູ \'ປົດໝຸດຈາກການໂທດ່ວນ\' + ເຊື່ອງເມນູ \'ເບິ່ງຂໍ້ມູນຜູ້ສ້າງເພງ\' + ໜ້າເລີ່ມຕົ້ນ + ຄ່າເລີ່ມຕົ້ນ + ແຜນພູມ + ຕອນໄວ້ເບິ່ງພາຍຫຼັງ + ສຳຫຼວດ + ປະຫວັດ + ຫ້ອງສະໝຸດ + ເພງທີ່ມັກ + ລາຍການເພງ + ຄົ້ນຫາ + ການສະໝັກ + ບັງຄັບໃຫ້ເປັນແນວຕັ້ງ + ຮັກສາໃຫ້ແອັບຢູ່ໃນແນວຕັ້ງ ເມື່ອໝຸນອຸປະກອນ + ປ່ຽນສີເຄື່ອງຫຼິ້ນຂະໜາດນ້ອຍ + ປັບສີຂອງຕົວຫຼິ້ນຂະໜາດນ້ອຍໃຫ້ກົງກັບຕົວຫຼິ້ນແບບເຕັມຈໍ + ປ່ຽນສີແຖບນຳທາງ + ປັບສີແຖບນຳທາງໃຫ້ກົງກັບເຄື່ອງຫຼິ້ນແບບເຕັມຈໍ + ສະແດງປຸ່ມຖັດໄປໃນຕົວຫຼິ້ນຂະໜາດນ້ອຍ + ສະແດງປຸ່ມກ່ອນໜ້ານີ້ໃນຕົວຫຼິ້ນຂະໜາດນ້ອຍ + ເປີດໃຊ້ງານຕົວຫຼິ້ນຂະໜາດນ້ອຍແບບບັງຄັບ + ປ້ອງກັນບໍ່ໃຫ້ຕົວຫຼິ້ນຂະຫຍາຍເປັນແບບເຕັມຈໍໂດຍອັດຕະໂນມັດ ເມື່ອເລີ່ມຫຼິ້ນແທຣັກໃໝ່ + ເປີດໃຊ້ງານການປັດເພື່ອປິດເຄື່ອງຫຼິ້ນຂະໜາດນ້ອຍ + ອະນຸຍາດໃຫ້ປິດ miniplayer ໂດຍການປ້ອນລົງ + ແຖບນຳທາງ + ເລືອກປຸ່ມທີ່ຈະໃຫ້ປະກົດໃນແຖບນຳທາງດ້ານລຸ່ມ + ເຊື່ອງປຸ່ມໜ້າຫຼັກ + ເຊື່ອງປຸ່ມຕົວຢ່າງ + ເຊື່ອງປຸ່ມສຳຫຼວດ + ເຊື່ອງປຸ່ມຄັງປຶ້ມ + ເຊື່ອງປຸ່ມອັບເກຣດ + ເຊື່ອງແຖບນຳທາງ + ເຊື່ອງປ້າຍຊື່ປຸ່ມນຳທາງ + ຍັງບໍ່ໄດ້ເຂົ້າສູ່ລະບົບ. ແຕະເພື່ອຕັ້ງຄ່າ + ອອກຈາກລະບົບ + ອອກຈາກລະບົບແລ້ວ + ເປີດໃຊ້ງານ scrobbling + ສົ່ງສະຖານະ \'ກຳລັງຫຼິ້ນ\' + ໄລຍະເວລາຕ່ຳສຸດຂອງເພງ + ເພງທີ່ສັ້ນກວ່ານີ້ຈະບໍ່ຖືກ scrobbled + ເປີເຊັນການຊັກຊ້າຂອງ scrobble + ກະຕຸ້ນ scrobble ຫຼັງຈາກຟັງເພງເຖິງເປີເຊັນນີ້ + ວິນາທີຊັກຊ້າຂອງ scrobble + ເລີ່ມ scrobble ຫຼັງຈາກຟັງເປັນເວລາຫຼາຍວິນາທີນີ້ + ບັນຊີ ListenBrainz + ເຂົ້າສູ່ລະບົບແລ້ວ. ແຕະເພື່ອຈັດການບັນຊີ + ປ້ອນ ListenBrainz User Token ຂອງທ່ານເພື່ອເປີດໃຊ້ scrobbling + ໂທເຄັນ + ຢືນຢັນ + ກະລຸນາປ້ອນ token + ກຳລັງກວດສອບ… + Token ຖືກຕ້ອງ! ຜູ້ໃຊ້: %s + Token ບໍ່ຖືກຕ້ອງ: %s + ບໍ່ຮູ້ຈັກ + ການຢືນຢັນລົ້ມເຫຼວ: %s + ລຶບ Token ແລ້ວ + ບັນທຶກ Token ແລ້ວ + ຄຳເຕືອນ: Token ທີ່ບັນທຶກໄວ້ບໍ່ຖືກຕ້ອງ! + Love ເພງທີ່ມັກ + Love ເພງໃນ Last.fm ໂດຍອັດຕະໂນມັດ ເມື່ອທ່ານກົດມັກໃນ YouTube Music (ແລະ ຍົກເລີກ love ເມື່ອບໍ່ມັກ ຫຼື ກົດ dislike) + ບັນຊີ Last.fm + ເຂົ້າສູ່ລະບົບໃນນາມ %s. ແຕະເພື່ອຈັດການບັນຊີ + ປ້ອນຂໍ້ມູນການເຂົ້າສູ່ລະບົບ Last.fm ຂອງທ່ານເພື່ອເຂົ້າສູ່ລະບົບ + ຊື່ຜູ້ໃຊ້ + ລະຫັດຜ່ານ + ເຂົ້າສູ່ລະບົບ + ກະລຸນາປ້ອນຊື່ຜູ້ໃຊ້ ແລະ ລະຫັດຜ່ານ + ກຳລັງເຂົ້າສູ່ລະບົບ… + ເຂົ້າສູ່ລະບົບສຳເລັດແລ້ວ! + ບໍ່ສາມາດດຶງຂໍ້ມູນ session ຈາກ Last.fm ໄດ້ + ການເຂົ້າສູ່ລະບົບລົ້ມເຫຼວ: %s + ບັນທຶກຂໍ້ມູນການເຂົ້າໃຊ້ Last.fm ແລ້ວ! + ການລ້າງຂໍ້ມູນ Metadata + ລ້າງຂໍ້ມູນ Metadata + ລ້າງຂໍ້ມູນທີ່ບໍ່ຈຳເປັນທົ່ວໄປໂດຍອັດຕະໂນມັດ (ເຊັ່ນ \"Official Video\", \"Remaster\", \"Live\") ຈາກຊື່ເພງ, ສິລະປິນ ແລະ ອັນບັ້ມ ກ່ອນການ scrobbling + ການລ້າງຂໍ້ມູນດ້ວຍ regex ແບບກຳນົດເອງ + ປ້ອນ regular expression ແບບກຳນົດເອງ ເພື່ອລົບຂໍ້ຄວາມເພີ່ມເຕີມອອກຈາກຊື່ເພງ, ສິລະປິນ ຫຼື ອັນບັ້ມ. ຕົວຢ່າງ: (?i)\\s*\\((clean|explicit)\\) + ວິເຄາະສິລະປິນ ແລະ ເພງຈາກຫົວຂໍ້ + ດຶງເອົາຊື່ສິລະປິນ ແລະ ຊື່ເພງອອກຈາກຫົວຂໍ້ວິດີໂອ ໂດຍໃຊ້ຕົວຄັ້ນ \" - \". ມີປະໂຫຍດສຳລັບເນື້ອຫາທີ່ຖືກອັບໂຫຼດໃໝ່ ເຊິ່ງຊື່ຊ່ອງອາດຖືກນຳໃຊ້ເປັນຊື່ສິລະປິນ + ຕື່ມຂໍ້ມູນອັນບັ້ມທີ່ຂາດຫາຍ + ເມື່ອເພງບໍ່ມີຂໍ້ມູນອັນບັ້ມ, ໃຫ້ຄົ້ນຫາໃນ Last.fm ເພື່ອໃຫ້ການ scrobble ຂອງທ່ານສົມບູນ + "Scrobbling ຈະຊິງຄ໌ການນຳໃຊ້ YouTube Music ຂອງທ່ານກັບໂປຣໄຟລ໌ Last.fm ຫຼື ListenBrainz ໂດຍອັດຕະໂນມັດ + +ເພື່ອໃຫ້ແນ່ໃຈວ່າ scrobbling ຖືກຊິງຄ໌ໃນລະຫວ່າງການຫຼິ້ນໃນພື້ນຫຼັງ, ໃຫ້ກວດສອບວ່າການນຳໃຊ້ແບັດເຕີຣີຂອງແອັບ YouTube Music ຖືກຕັ້ງຄ່າເປັນ \"ບໍ່ຈຳກັດ\"" + ໃຊ້ເນື້ອເພງຈາກບຸກຄົນທີສາມ + ສະແດງເນື້ອເພງຈາກບໍລິການຂອງບຸກຄົນທີສາມ, ໂດຍຈະກັບໄປໃຊ້ເນື້ອເພງທີ່ຕິດຕັ້ງມາໃນຕົວເຄື່ອງເມື່ອບໍ່ພົບເນື້ອເພງ + ເປີດໜ້າຈໍໄວ້ໃນຂະນະທີ່ເບິ່ງເນື້ອເພງ + ປ້ອງກັນບໍ່ໃຫ້ໜ້າຈໍປິດໃນຂະນະທີ່ແຜງເນື້ອເພງເປີດຢູ່ + ບໍລິການເນື້ອເພງ + ການສະແດງຜົນ + ການຊິງໂຄຣໄນເຊຊັນ + ແຜງຄວບຄຸມ + AI endpoint + ປິດການນຳໃຊ້ຜູ້ໃຫ້ບໍລິການທັງໝົດ - ເນື້ອເພງໃນຕົວ ແລະ ເນື້ອເພງທີ່ຝັງມາ + ຣີເຊັດລຳດັບຜູ້ໃຫ້ບໍລິການ? + ຄືນຄ່າຜູ້ໃຫ້ບໍລິການໄປເປັນລຳດັບຄວາມສຳຄັນເລີ່ມຕົ້ນ + ລຶບເນື້ອເພງທີ່ບັນທຶກໄວ້ + ລຶບເນື້ອເພງທີ່ເກັບໄວ້ໃນອຸປະກອນນີ້ + ລຶບແລ້ວ %1$s MB + ຮັບໂທເຄັນ + Apple Music Media-user-token + "ວາງຄຸກກີ້ media-user-token ຈາກ music.apple.com + +ຂັ້ນຕອນ: +1. ເຂົ້າສູ່ລະບົບທີ່ music.apple.com ໃນນາມຜູ້ໃຊ້ສະມາຊິກ +2. ເປີດ DevTools → Application → Cookies → music.apple.com +3. ຄັດລອກຄ່າຂອງ \"media-user-token\"" + Apple Music: ບັນທຶກ media-user-token ແລ້ວ + Apple Music: ລ້າງ media-user-token ແລ້ວ + Apple Music: media-user-token ບໍ່ຖືກຕ້ອງ - ກະລຸນາກວດສອບ ແລະ ລອງໃໝ່ອີກຄັ້ງ + Spotify SpDc + "ວາງຄຸກກີ້ sp_dc ຈາກ Spotify. + +ຂັ້ນຕອນ: +1. ເຂົ້າສູ່ລະບົບທີ່ open.spotify.com +2. ເປີດ DevTools → Application → Cookies → open.spotify.com +3. ຄັດລອກຄ່າຂອງ \"sp_dc\"" + Spotify: ບັນທຶກ sp_dc ແລ້ວ + Spotify: ລຶບ sp_dc ແລ້ວ + Spotify: sp_dc ບໍ່ຖືກຕ້ອງ - ກະລຸນາກວດສອບ ແລະ ລອງໃໝ່ອີກຄັ້ງ + Apple Music token ແມ່ນທາງເລືອກ. ຖ້າບໍ່ມີ, Lyrically ຈະຖືກນຳໃຊ້ເປັນຕົວສຳຮອງ + Deezer ARL + "ວາງຄຸກກີ້ arl ຈາກ Deezer. + +ຂັ້ນຕອນ: +1. ເຂົ້າສູ່ລະບົບທີ່ deezer.com +2. ເປີດ DevTools → Application → Cookies → deezer.com +3. ຄັດລອກຄ່າຂອງ \"arl\"" + Deezer: ບັນທຶກ arl ແລ້ວ + Deezer: ລຶບ arl ແລ້ວ + Deezer: arl ບໍ່ຖືກຕ້ອງ - ກະລຸນາກວດສອບ ແລະ ລອງໃໝ່ອີກຄັ້ງ + Musixmatch Token + ເພີ່ມ token ເພື່ອໃຊ້ບັນຊີຂອງທ່ານເອງເທົ່ານັ້ນ. ມັນຕ້ອງມາຈາກແອັບ Musixmatch Android: token ທີ່ຄັດລອກມາຈາກ musixmatch.com ແມ່ນຂອງ client ເດັສທັອບທີ່ຍົກເລີກການນຳໃຊ້ແລ້ວ ແລະ API ຈະປະຕິເສດພວກມັນ + Musixmatch ເຮັດວຽກໄດ້ໂດຍບໍ່ຕ້ອງມີ token, ດັ່ງນັ້ນຊ່ອງນີ້ສາມາດປະໄວ້ໃຫ້ວ່າງໄດ້ + ໂທເຄັນຜູ້ໃຊ້ + Musixmatch: ບັນທຶກ user token ແລ້ວ + Musixmatch: ລຶບ user token ແລ້ວ + Musixmatch: user token ບໍ່ຖືກຕ້ອງ - ກະລຸນາກວດສອບ ແລະ ລອງໃໝ່ອີກຄັ້ງ + ຄຸກກີ້ YouTube + "ວາງຄຸກກີ້ YouTube ຂອງທ່ານເພື່ອສອບຖາມຄຳບັນຍາຍ ແລະ ຂໍ້ຄວາມຕາມເວລາຈາກ YouTube. + +ຂັ້ນຕອນ: +1. ເປີດ youtube.com ໃນບຣາວເຊີ ແລະ ເຂົ້າສູ່ລະບົບ +2. ເປີດ DevTools (F12) → Application → Cookies → youtube.com +3. ຄັດລອກສະຕຣິງຄຸກກີ້ທັງໝົດ (ຄູ່ key=value ທີ່ແຍກດ້ວຍເຄື່ອງໝາຍ semicolon) + +ຄຸກກີ້ທີ່ຈຳເປັນ: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPISID, LOGIN_INFO" + YouTube: ບັນທຶກຄຸກກີ້ແລ້ວ + YouTube: ລຶບຄຸກກີ້ແລ້ວ + YouTube: ຄຸກກີ້ບໍ່ຖືກຕ້ອງ - ກະລຸນາກວດສອບ ແລະ ລອງໃໝ່ + ແຕະຄ້າງໄວ້ທີ່ແຖວໃດໜຶ່ງ + ແຕະທີ່ແຖວເພື່ອຂ້າມໄປຍັງຊ່ວງເວລານັ້ນໃນເພງ, ກົດຄ້າງໄວ້ເພື່ອຄັດລອກຄຳສັບທີ່ຢູ່ໃຕ້ລ້ວນິ້ວມືຂອງທ່ານ, ຫຼື ຄັດລອກທັງໝົດແຖວເມື່ອການກົດບໍ່ໄດ້ຢູ່ເທິງຄຳສັບ. ການປິດຈະນຳການເຮັດທັງສອງຢ່າງນີ້ອອກ + ສະແດງແທັກສ່ວນ + ຕິດປ້າຍແທັກນ້ອຍໆໃຫ້ແຕ່ລະສ່ວນໃໝ່ຂອງເນື້ອເພງ ເຊັ່ນ: Verse ຫຼື Chorus + ສະແດງປຸ່ມຄັດລອກ + ແຕະເພື່ອຄັດລອກເນື້ອເພງຕົ້ນສະບັບເປັນຂໍ້ຄວາມທຳມະດາ; ກົດຄ້າງໄວ້ເພື່ອບັນທຶກໄຟລ໌ແຫຼ່ງເນື້ອເພງດິບລົງໃນ Downloads + ສະແດງປຸ່ມແປ + ແຕະເພື່ອສະຫຼັບການແປ; ກົດຄ້າງໄວ້ເພື່ອເບິ່ງສະເພາະການແປ. ໃຊ້ການແປຈາກຜູ້ໃຫ້ບໍລິການ, Google Translate, ຫຼື OpenAI-compatible API + ສະແດງປຸ່ມ Romanize + ແຕະເພື່ອສະຫຼັບການຖອດສຽງເປັນອັກສອນໂຣມັນ; ກົດຄ້າງໄວ້ເພື່ອເບິ່ງສະເພາະການຖອດສຽງ. ໃຊ້ການຖອດສຽງຈາກຜູ້ໃຫ້ບໍລິການ, Google Translate, ຫຼື OpenAI-compatible API + ການຖອດສຽງເປັນອັກສອນໂຣມັນ ແລະ ການຈັດວາງໃຫ້ກົງກັນໃນເຄື່ອງ + ຖອດສຽງເປັນອັກສອນໂຣມັນໃນອຸປະກອນ ເມື່ອຜູ້ໃຫ້ບໍລິການ, ໂມເດລຂອງທ່ານ ແລະ Google Translate ບໍ່ສາມາດເຮັດວຽກໄດ້. ການອ່ານຈະຕາມຫຼັງແຕ່ລະຄຳຂອງເນື້ອເພງທີ່ກຳນົດເວລາໄວ້; ຕ້ອງເປີດການຊິງຄ໌ຄຳສັບ. ພາສາຈີນ, ກວາງຕຸ້ງ, ຍີ່ປຸ່ນ, ເກົາຫຼີ, ກຣີກ, ຣັດເຊຍ, ຈໍຣເຈຍ, ອາມີເນຍ; ປະມານ 120 MB + ກຳລັງອ່ານ %1$s: %2$d%% + ການອ່ານພ້ອມແລ້ວ (%1$d ລາຍການ) + ການດາວໂຫລດລົ້ມເຫຼວ - ໃຫ້ປິດ ແລະ ເປີດສິ່ງນີ້ອີກຄັ້ງເພື່ອລອງໃໝ່ + ສະແດງປຸ່ມ Refresh + ແຕະເພື່ອປ່ຽນໄປຫາເນື້ອເພງອື່ນທີ່ເປັນໄປໄດ້; ແຕະຄ້າງໄວ້ເພື່ອຄົ້ນຫາເນື້ອເພງຕາມຊື່ເພງ ແລະ ສິລະປິນ + ຄົ້ນຫາເນື້ອເພງ + ຊື່ເພງ + ສິລະປິນ + ປ້ອນຊື່ເພງ ແລະ ສິລະປິນ + ບໍ່ມີຜູ້ໃຫ້ບໍລິການທີ່ເປີດໃຊ້ງານໃດສາມາດຄົ້ນຫາເພງອື່ນໄດ້ + ບັນທຶກແລ້ວ + ເນັ້ນຄຳສັບ ຫຼື ພະຍາງທີ່ຊິງຄ໌ແລ້ວ + ໃຫ້ບຸລິມະສິດແກ່ເນື້ອເພງແບບຄຳຕໍ່ຄຳເມື່ອມີໃຫ້. ເມື່ອເນື້ອເພງມີການກຳນົດເວລາຂອງຄຳສັບ, ມັນຈະເນັ້ນແຕ່ລະຄຳ ຫຼື ພະຍາງໃຫ້ຕົງກັບຈັງຫວະເພງ + ເຊື່ອງເນື້ອເພງທີ່ຫຼິ້ນຜ່ານໄປແລ້ວ + ເຊື່ອງແຖວເນື້ອເພງທີ່ຢູ່ເໜືອແຖວທີ່ກຳລັງຫຼິ້ນຢູ່ + ເຊື່ອງເນື້ອເພງທີ່ຍັງບໍ່ທັນໄດ້ຫຼິ້ນ + ເຊື່ອງແຖວເນື້ອເພງທີ່ຢູ່ລຸ່ມແຖວທີ່ກຳລັງຫຼິ້ນຢູ່ + ສະແດງເນື້ອເພງ MediaSession ຢູ່ໃນໜ້າຈໍລັອກ + ສະແດງແຖວປັດຈຸບັນໃນໜ້າຈໍລັອກ, Android Auto ແລະ Bluetooth. ຕ້ອງໃຊ້ Android 11 ຫຼື ໃໝ່ກວ່ານັ້ນ + ສະແດງເນື້ອເພງໃນ Miniplayer + ສະແດງແຖວປັດຈຸບັນໃນເຄື່ອງຫຼິ້ນຂະໜາດນ້ອຍ, ໂດຍຍ້າຍຊື່ເພງໄປໄວ້ທີ່ຄຳບັນຍາຍຍ່ອຍ + ຮູບແບບຊື່ເພງ/ສິລະປິນ + ຮູບແບບຂອງແທຣັກທີ່ສະແດງພ້ອມກັບເນື້ອເພງໃນ Miniplayer ແລະ MediaSession. {t} ແມ່ນຊື່ເພງ, {a} ແມ່ນສິລະປິນ ແລະ {v} ແມ່ນເວີຊັນ, ເຊັ່ນ Acoustic, ເມື່ອແຫຼ່ງເນື້ອເພງມີໃຫ້. ຂໍ້ຄວາມລະຫວ່າງສອງຈຸດຈະສະແດງກໍຕໍ່ເມື່ອທຸກຕົວປ່ຽນພາຍໃນນັ້ນມີຄ່າ; ຂຽນ \\: ສຳລັບຈຸດທຳມະດາ. ແຕ່ລະແຖວຈະຖືກສະແດງຕາມລຳດັບ, ໂດຍຈະປ່ຽນໄປເມື່ອເນື້ອເພງປ່ຽນ + ຮູບແບບແຖວຂອງເນື້ອເພງ + ຮູບແບບຂອງແຖວເນື້ອເພງໃນ Miniplayer ແລະ MediaSession. {lrc} ແມ່ນແຖວ ແລະ {n} ແມ່ນຈຳນວນຄັ້ງທີ່ຮ້ອງຊ້ຳກັນ. ຂໍ້ຄວາມລະຫວ່າງເຄື່ອງໝາຍຈໍ້າສອງຈະສະແດງສະເພາະເມື່ອແຖວນັ້ນຮ້ອງຊ້ຳ. ໃຊ້ \\: ສຳລັບເຄື່ອງໝາຍຈໍ້າສອງ + ຮູບແບບຕ້ອງມີ {lrc} + ແມ່ແບບແຖວສິລະປິນ ແລະ ການກຳນົດເວລາຂອງແອນິເມຊັນ + ເວລາສະແດງໜ້າຈໍຕ່ຳສຸດ (ms) + ເວລາໜ້າຈໍສູງສຸດ (ms) + ສຳລັບການເຄື່ອນໄຫວແບບຫຼາຍແຖວເທົ່ານັ້ນ. ຮັກສາແຕ່ລະເຟຣມໄວ້ຢ່າງໜ້ອຍຕາມເວລາຕ່ຳສຸດ. ໃນເວລາສູງສຸດ, ໃຫ້ເລື່ອນແຖວສິລະປິນຕໍ່ໄປ ເຖິງແມ່ນວ່າເນື້ອເພງຈະຍັງຄືເກົ່າ. + ຄ່າຕ່ຳສຸດຕ້ອງເປັນ 0 ຫຼື ຫຼາຍກວ່າ. ຄ່າສູງສຸດຕ້ອງຫຼາຍກວ່າ 0 ແລະ ຢ່າງໜ້ອຍຕ້ອງເທົ່າກັບຄ່າຕ່ຳສຸດ. + ຂີດຈຳກັດຄວາມຍາວຂອງແຖວ + ຂີດຈຳກັດແຖວຂອງເນື້ອເພງ ແລະ ສິລະປິນ, ແລະ ນ້ຳໜັກຂອງຕົວອັກສອນຄວາມກວ້າງເຕັມ + "ຂີດຈຳກັດແຖວຂອງເນື້ອເພງ +ແບ່ງເນື້ອເພງທີ່ຍາວເກີນໄປໃຫ້ເປັນສອງສ່ວນທີ່ສົມດຸນ. 0 ແມ່ນປິດການແບ່ງ." + "ຂີດຈຳກັດແຖວຂອງສິລະປິນ +ແບ່ງຮູບແບບແທຣັກທີ່ມີຂໍ້ມູນໃຫ້ເປັນເຟຣມທີ່ໝຸນວຽນ. 0 ແມ່ນປິດການແບ່ງ." + "ນ້ຳໜັກຂອງຕົວອັກສອນຄວາມກວ້າງເຕັມ +ນຳໃຊ້ກັບທັງສອງຂີດຈຳກັດ. ທີ່ 1.75, ຕົວອັກສອນຄວາມກວ້າງເຕັມສີ່ຕົວຈະນັບເປັນ 7." + ປ້ອນຂີດຈຳກັດເປັນຈຳນວນເຕັມຕັ້ງແຕ່ 0 ຫາ 10000 ແລະ ນ້ຳໜັກຕົວອັກສອນທີ່ຫຼາຍກວ່າ 0 + ຊ່ວງດົນຕີບັນເລງ + ສະແດງໃນ Miniplayer ແລະ MediaSession ໃນຊ່ວງພັກດົນຕີ. ແຕ່ລະແຖວແມ່ນໜຶ່ງເຟຣມການເຄື່ອນໄຫວ. {t} ແມ່ນຊື່ເພງ, {a} ແມ່ນສິລະປິນ ແລະ {lrc} ແມ່ນເນື້ອເພງແຖວສຸດທ້າຍ + ຄ່າຊົດເຊີຍເວລາຂອງ Miniplayer ແລະ MediaSession + ປັບເວລາໃນການສະແດງແຖວຕໍ່ໄປໃນ Miniplayer ແລະ MediaSession. ໃຊ້ຄ່າລົບ ຫາກແຖວສະແດງຊ້າເກີນໄປ, ຕົວຢ່າງ: ໃນໜ້າຈໍລັອກ + ຕົວຕອງ + Metadata regex + Regular expression ທີ່ໃຊ້ກັບຊື່ເພງ, ສິລະປິນ, ແລະ ອາລບັ້ມ ກ່ອນທີ່ຈະຄົ້ນຫາເນື້ອເພງ ແລະ ເມື່ອໃຫ້ຄະແນນວ່າຊື່ສອງຊື່ກົງກັນຫຼືບໍ່. ປ່ອຍໃຫ້ວ່າງເພື່ອປິດການໃຊ້ງານທັງສອງ. ຄ່າເລີ່ມຕົ້ນຈະລຶບສິ່ງທີ່ບໍ່ຈຳເປັນທົ່ວໄປ ເຊັ່ນ \"Official Video\", \"Remaster\", \"【插曲】\", ແລະ \"- Topic\" + Lyrics filter regex + Regular expression ທີ່ນຳໃຊ້ກັບຂໍ້ຄວາມເນື້ອເພງຕົ້ນສະບັບເທົ່ານັ້ນ (ບໍ່ລວມທັງຄຳແປ ຫຼື ການທັບສັບ). ປ່ອຍວ່າງໄວ້ເພື່ອປິດການນຳໃຊ້ + ເຄື່ອງໝາຍບັນທັດຂໍ້ມູນ + ຖ້າເນື້ອເພງຖືກຕອງໄປໄວ້ທາງລຸ່ມຢ່າງບໍ່ຖືກຕ້ອງ, ໃຫ້ປັບປ່ຽນ ຫຼື ລຶບສິ່ງນີ້. ເຄື່ອງໝາຍທີ່ແຍກດ້ວຍຈຸດ: ບັນທັດທີ່ມີເຄື່ອງໝາຍຕາມດ້ວຍເຄື່ອງໝາຍຈຸດສອງ ຈະຖືກສະແດງເປັນຂໍ້ມູນໃຫ້ເຄຣດິດຢູ່ທາງລຸ່ມ. ປ່ອຍວ່າງໄວ້ເພື່ອປິດການນຳໃຊ້ + ປ່ຽນຕົວອັກສອນທຳອິດໃຫ້ເປັນຕົວພິມໃຫຍ່ + ປ່ຽນຕົວອັກສອນທຳອິດຂອງແຕ່ລະແຖວເນື້ອເພງໃຫ້ເປັນຕົວພິມໃຫຍ່ + ລຶບເຄື່ອງໝາຍວັກຕອນທ້າຍປະໂຫຍກ + ລຶບຈຸດ, ຈຸດຈຸດ, ຈຸດສອງຈຸດ ແລະ ຈຸດຈຸດເຄິ່ງ ທີ່ຢູ່ທ້າຍແຖວເນື້ອເພງ. ສ່ວນເຄື່ອງໝາຍຄຳຖາມ, ເຄື່ອງໝາຍຕົກໃຈ, ຈຸດສາມຈຸດ, ເຄື່ອງໝາຍຄຳເວົ້າ ແລະ ວົງເລັບ ຈະຍັງຄົງໄວ້ + ຂະໜາດຕົວອັກສອນ + ຂະໜາດຂອງຕົວອັກສອນເນື້ອເພງ + ການຊົດເຊີຍເວລາທົ່ວໂລກ + ເນື້ອເພງທີ່ຊິງຄ໌ແລ້ວຈະໄປຕາມການຫຼິ້ນ. ຖ້າມັນບໍ່ຕົງກັນ, ໃຫ້ປັບຄ່າການຊິຟ໌ເວລາ. ເລື່ອນເນື້ອເພງທີ່ຊິງຄ໌ແລ້ວໄປທາງໜ້າ ຫຼື ທາງຫຼັງ. ໃຊ້ຄ່າບວກ ຖ້າເນື້ອເພງໄປກ່ອນເພງ + ໃຊ້ສ່ວນຂອງ SponsorBlock ສຳລັບການຊົດເຊີຍເນື້ອເພງ + ຈັບຄູ່ໂດຍບໍ່ມີສ່ວນທີ່ SponsorBlock ຂ້າມ ແລະ ເລື່ອນເນື້ອເພງໃຫ້ຕົງກັບສຽງທີ່ປ່ອຍອອກມາ. ປິດ ຈະເປັນການບໍ່ສົນໃຈ SponsorBlock ໂດຍສິ້ນເຊີງ + ແທຣັກນີ້ບໍ່ມີ id ວິດີໂອເພື່ອສົ່ງ + ແຫຼ່ງທີ່ມາ: %s + ກົດປຸ່ມຄັດລອກຄ້າງໄວ້ເພື່ອບັນທຶກ LRC ນີ້; ຖ້າທ່ານສາມາດປັບຈູນເວລາໃຫ້ລະອຽດ ແລະ ແບ່ງປັນໃຫ້ກັບຜູ້ໃຫ້ບໍລິການຊຸມຊົນ, ພວກເຮົາຈະຂອບໃຈເປັນຢ່າງສູງ + ແປໂດຍ Google + ຖອດສຽງເປັນອັກສອນໂຣມັນໂດຍ Google + ແປໂດຍ %s + ທຳນອງອັກສອນໂລມັນໂດຍ %s + ຖອດລະຫັດເປັນອັກສອນໂຣມັນໂດຍ %s + ແປແລ້ວ + ພາສາໃນການແປ + ພາສາໃນການຖອດລະຫັດເປັນອັກສອນໂຣມັນ + ກຳລັງແປ… + ບໍ່ສາມາດແປເນື້ອເພງໄດ້ + ຖອດສຽງເປັນອັກສອນໂຣມັນ + ອັດຕະໂນມັດ + ກຳລັງຖອດສຽງເປັນອັກສອນໂຣມັນ… + ບໍ່ສາມາດຖອດສຽງເນື້ອເພງເປັນອັກສອນໂຣມັນໄດ້ + ແປເທົ່ານັ້ນ + ທຳນອງອັກສອນໂຣມັນເທົ່ານັ້ນ + ໂໝດຄູ່ + ຄັດລອກໄປທີ່ຄລິບບອດແລ້ວ + ກຳລັງຣີເຟຣຊ + ຣີເຟຣຊແລ້ວ + ຄີ API ຂອງ QuickLRC + QuickLRC ຖອດຄວາມ ຫຼື ປັບເວລາເນື້ອເພງໃຫ້ກົງກັບແທຣັກປັດຈຸບັນ. ເປັນບໍລິການແຍກຕ່າງຫາກ, ບໍ່ກ່ຽວຂ້ອງກັບ endpoint ທີ່ເຂົ້າກັນໄດ້ກັບ OpenAI ທາງເທິງ + ບັນທຶກຄີ QuickLRC ແລ້ວ. ໃຊ້ປຸ່ມ align ໃນບ່ອນຄົ້ນຫາເນື້ອເພງ, ຫຼື ໃຊ້ຄຳແນະນຳໃນແຜງຄວບຄຸມເມື່ອແທຣັກບໍ່ມີເນື້ອເພງ + ບັນທຶກຄີ API ຂອງ QuickLRC ແລ້ວ + QuickLRC ບໍ່ຍອມຮັບຄີນີ້ + ລຶບຄີ API ຂອງ QuickLRC ແລ້ວ + QuickLRC ຈະກຳນົດເວລາໃຫ້ເນື້ອເພງທີ່ບໍ່ມີເວລາ, ຫຼື ຖອດສຽງຈາກແທຣັກ, ໂດຍມີຄ່າໃຊ້ຈ່າຍເປັນເຄຣດິດ. ເມື່ອຕັ້ງຄ່າຄີແລ້ວ, ແທຣັກທີ່ບໍ່ມີເນື້ອເພງຈະມີຕົວເລືອກການຖອດສຽງ ແລະ ປ່ອງຢ້ຽມການຄົ້ນຫາຈະມີປຸ່ມຈັດຕຳແໜງ + ຮ້ອງຂໍການຈັດວາງຂອງ QuickLRC + ກຳລັງຈັດວາງ + ນຳໃຊ້ການຈັດວາງ + ຮ້ອງຂໍການຖອດຄວາມຂອງ QuickLRC + ກຳລັງຖອດຄວາມ + ຍົກເລີກການຖອດຄວາມແລ້ວ + ມີພຽງແຕ່ເພງທີ່ຫຼິ້ນຈາກ YouTube ເທົ່ານັ້ນທີ່ສາມາດຖອດຂໍ້ຄວາມໄດ້ + ບໍ່ມີເນື້ອເພງທີ່ຈະຈັດຕຳແໜງ - ຈະປ່ຽນເປັນການຖອດສຽງແທນ + QuickLRC ບໍ່ສາມາດປະມວນຜົນເພງນີ້ໄດ້ + ເຄຣດິດ QuickLRC ສໍາລັບເພງນີ້ໄດ້ໝົດແລ້ວ + "ເນື້ອເພງຈະຖືກຈັບຄູ່ໂດຍຊື່ເພງ ແລະ ສິລະປິນ, ຂໍ້ມູນ metadata ທີ່ຜິດປົກກະຕິອາດຈະບໍ່ພົບ ຫຼື ພົບເວີຊັນທີ່ຜິດ. ໃຊ້ການຣີເຟຣຊເພື່ອປ່ຽນເວີຊັນ. ຜູ້ໃຫ້ບໍລິການທີ່ໝາຍດ້ວຍ * ຕ້ອງມີ token. ຜູ້ໃຫ້ບໍລິການທີ່ເປີດໃຊ້ງານທັງໝົດອາດຈະບໍ່ຖືກຮ້ອງຂໍຕາມລຳດັບຂອງທ່ານສະເໝີ, ແຕ່ຈະຕອບສະໜອງຕາມຄວາມໄວຂອງພວກເຂົາ.\n\nເນື້ອເພງມາຈາກບໍລິການຂອງບຸກຄົນທີສາມທີ່ບໍ່ມີສ່ວນກ່ຽວຂ້ອງ, ບໍ່ໄດ້ຮັບການອຸປະຖຳ, ຫຼື ບໍ່ໄດ້ຮັບການຮັບຮອງໂດຍ Morphe. ແຕ່ລະບໍລິການຖືກນຳໃຊ້ຕາມສະພາບທີ່ເປັນ: ເນື້ອເພງ, ຄວາມພ້ອມໃນການນຳໃຊ້ ແລະ ການໃຫ້ອະນຸຍາດຂອງພວກເຂົາຍັງຄົງເປັນຂອງບໍລິການເຫຼົ່ານັ້ນ ແລະ ຜູ້ໃຫ້ອະນຸຍາດ, ແລະ ການນຳໃຊ້ບໍລິການເຫຼົ່ານັ້ນອາດຈະຢູ່ພາຍໃຕ້ເງື່ອນໄຂການບໍລິການຂອງພວກເຂົາ. ສິ່ງທີ່ສົ່ງໄປຫາບໍລິການທີ່ເປີດໃຊ້ງານມີພຽງແຕ່ການຄົ້ນຫາທີ່ທ່ານຮ້ອງຂໍ - ຊື່ເພງ, ສິລະປິນ, ແລະ ໃນກໍລະນີທີ່ບໍລິການນັ້ນຕ້ອງການ, ID ຂອງວິດີໂອ. ຈຸດປາຍ AI ທີ່ເປັນທາງເລືອກ ແລະ ການຖອດຄວາມ QuickLRC ຈະເຮັດວຽກຫຼັງຈາກທີ່ທ່ານກຳນົດຄ່າພວກມັນເທົ່ານັ້ນ, ແລະ ຈະໄດ້ຮັບຂໍ້ຄວາມເນື້ອເພງ ຫຼື URL ວິດີໂອຂອງເພງນັ້ນ." + ເປີດໃຊ້ງານສະເພາະຜູ້ໃຫ້ບໍລິການທີ່ທ່ານໃຊ້, ຫຼື ໃຊ້ຄ່າເລີ່ມຕົ້ນ - ການຮ້ອງຂໍຢ່າງຕໍ່ເນື່ອງອາດເຮັດໃຫ້ເວັບໄຊເນື້ອເພງຂອງຊຸມຊົນຂະໜາດນ້ອຍເຮັດວຽກໜັກເກີນໄປ + ເຊື່ອງແຖວໃຫ້ເຄຣດິດ + ເຊື່ອງແຖວໃຫ້ເຄຣດິດທີ່ສະແດງຢູ່ລຸ່ມເນື້ອເພງ + ສະຫຼັບການແປ ແລະ ການຖອດສຽງ + ເປີດ ຈະວາງການຖອດສຽງໄວ້ລຸ່ມເນື້ອເພງ ແລະ ການແປໄວ້ທາງເທິງ; ປິດ ຈະວາງການຖອດສຽງໄວ້ທາງເທິງ ແລະ ການແປໄວ້ທາງລຸ່ມ + ການແປ + ການທັບສັບ + ຜູ້ໃຫ້ບໍລິການແປພາສາ + Google (ເລີ່ມຕົ້ນ, ຟຣີ) + OpenAI Compatible (ກຳນົດເອງ) + ຜູ້ໃຫ້ບໍລິການການທັບສັບ + Google (ຄ່າເລີ່ມຕົ້ນ) + ວັດຈະນານຸກົມທ້ອງຖິ່ນ / ອອຟໄລນ໌ + ທີ່ເຂົ້າກັນໄດ້ກັບ OpenAI (ກຳນົດເອງ) + ພາກສ່ວນ AI + ໃຊ້ສະເພາະໃນເວລາທີ່ຜູ້ໃຫ້ບໍລິການບໍ່ໄດ້ສົ່ງປ້າຍກຳກັບພາກສ່ວນ ເຊັ່ນ Chorus ຫຼື Verse, ເຊິ່ງຕົວແບບທີ່ເຂົ້າກັນໄດ້ກັບ OpenAI ຂອງທ່ານຈະກວດຫາໃນພາຍຫຼັງ + AI & Cloud Endpoints + ກຳນົດຄ່າ API keys, ໂມເດລ, endpoints, ແລະ ແມ່ແບບຄຳສັ່ງສຳລັບຜູ້ໃຫ້ບໍລິການ AI. + URL ຂອງ API endpoint (ຕົວຢ່າງ: https://api.openai.com/v1/chat/completions) + ແມັອບແບບພຣອມພ໌ + ຕົວແທນ: {language}, {task}, {title}, {artist}, {count}, {lines} + ບັນທຶກພຣອມພ໌ແລ້ວ + ບັນທຶກ Prompt ແລ້ວ, ແຕ່ endpoint ຂອງ AI ຍັງບໍ່ທັນສາມາດນຳໃຊ້ໄດ້. ກະລຸນາຕັ້ງຄ່າໃນ AI Settings ກ່ອນ + ຣີເຊັດພຣອມພ໌ແລ້ວ + ບໍ່ແມ່ນ JSON ທີ່ຖືກຕ້ອງ. ກະລຸນາແກ້ໄຂ ຫຼື ລ້າງຊ່ອງຂໍ້ມູນກ່ອນບັນທຶກ + ບັນທຶກການຕັ້ງຄ່າການແປ & ການທັບສັບແລ້ວ + ຣີເຊັດການຕັ້ງຄ່າເປັນຄ່າເລີ່ມຕົ້ນແລ້ວ + Base URL ຕ້ອງເລີ່ມຕົ້ນດ້ວຍ http:// ຫຼື https:// + ທົດສອບການເຊື່ອມຕໍ່ + ກຳນົດຄ່າ Endpoints + ສະແດງ Endpoints ທັງໝົດ + ກຳລັງກວດສອບການເຊື່ອມຕໍ່… + ຜູ້ໃຫ້ບໍລິການການແປ + ຜູ້ໃຫ້ບໍລິການ Romanization + ດຶງຂໍ້ມູນໂມເດລ + ກຳລັງດຶງຂໍ້ມູນໂມເດລ… + ເລືອກໂມເດລ + ດຶງຂໍ້ມູນໂມເດລບໍ່ສຳເລັດ. ກະລຸນາກວດສອບ API key ຫຼື ການເຊື່ອມຕໍ່. + ການຕັ້ງຄ່າ Gemini API + OpenAI Compatible (Custom Endpoint) + ການຕັ້ງຄ່າ DeepL API + ລະຫັດ API key + ໂມເດລ + Base URL + ລະຫັດ API key (ທາງເລືອກ) + ຄໍາສັ່ງການແປ + ເນື້ອໃນການຮ້ອງຂໍເພີ່ມເຕີມ (JSON ທາງເລືອກ) + ລະຫັດຢືນຢັນຕົວຕົນ (ຟຣີ ຫຼື Pro) + ກຳນົດຄ່າ API keys, ໂມເດລ, endpoints ແລະ prompts + 8.23.51 - ປິດການໃຊ້ງານໄອຄອນຕົວເຂັ້ມ + 9.13.51 - ກູ້ຄືນແທັບ ຫຼັງຈາກນີ້, ເນື້ອເພງ, ແລະ ທີ່ກ່ຽວຂ້ອງ ຂອງເຄື່ອງຫຼິ້ນ diff --git a/patches/src/main/resources/addresources/values-lo-rLA/reddit/strings.xml b/patches/src/main/resources/addresources/values-lo-rLA/reddit/strings.xml index cf7776fdaf..ff0b27735d 100644 --- a/patches/src/main/resources/addresources/values-lo-rLA/reddit/strings.xml +++ b/patches/src/main/resources/addresources/values-lo-rLA/reddit/strings.xml @@ -15,4 +15,17 @@ Second \"item\" text" --> - + + ນະໂຍບາຍຄວາມເປັນສ່ວນຕົວຂອງ Reddit + ບໍ່ສາມາດເຂົ້າສູ່ລະບົບດ້ວຍ Google ໄດ້. ກະລຸນາໃຊ້ ອີເມວ ເພື່ອເຂົ້າສູ່ລະບົບ + ຟອນທີ່ກຳນົດເອງ + ໂຫຼດໄຟລ໌ຟອນ TTF ຫຼື OTF ທີ່ກຳນົດເອງຈາກທີ່ຢູ່ດ້ານລຸ່ມ. ການເປີດໃຊ້ງານສິ່ງນີ້ ຈະປິດການໃຊ້ງານບັງຄັບໃຊ້ຟອນຂອງລະບົບນຳດ້ວຍ + ໄຟລ໌ຟອນທີ່ກຳນົດເອງ + ແຕະເພື່ອເລືອກໄຟລ໌ຟອນ TTF ຫຼື OTF + ຟອນທີ່ເລືອກ - %s + ກະລຸນາເລືອກໄຟລ໌ຟອນທີ່ກຳນົດເອງກ່ອນ + ກະລຸນາເລືອກໄຟລ໌ຟອນ .ttf ຫຼື .otf + ໂຫຼດຟອນທີ່ກຳນົດເອງບໍ່ສຳເລັດ. ກະລຸນາເລືອກໄຟລ໌ອື່ນ + ບັງຄັບໃຊ້ຟອນຂອງລະບົບ + ໃຊ້ຟອນຂອງລະບົບອຸປະກອນ ແທນ Reddit Sans ຫຼື Roboto. ການເປີດໃຊ້ງານສິ່ງນີ້ ຈະປິດການໃຊ້ງານຟອນທີ່ກຳນົດເອງນຳດ້ວຍ + diff --git a/patches/src/main/resources/addresources/values-lo-rLA/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-lo-rLA/shared-youtube/strings.xml index fd007eeb78..06740bb065 100644 --- a/patches/src/main/resources/addresources/values-lo-rLA/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-lo-rLA/shared-youtube/strings.xml @@ -18,46 +18,297 @@ Second \"item\" text" --> + ແອນິເມຊັນຕອນເລີ່ມຕົ້ນ + ປິດການໃຊ້ງານ + ສີ + ໂທນສີຂາວດຳ + + ເລີ່ມຕົ້ນລະບົບໃໝ່ເພື່ອຕັ້ງຄ່າໃຫ້ສຳເລັດ + ຊື່ແອັບ + ພື້ນຫຼັງຂອງຮູບແບບມືດ + ສີດຳລ້ວນ + Material You (ສີດຳລ້ວນ) + YouTube ແບບຄລາສສິກ + ສີບົວເຂັ້ມ + ສີຟ້າເຂັ້ມ + ສີຂຽວເຂັ້ມ + ສີເຫຼືອງເຂັ້ມ + ສີສົ້ມເຂັ້ມ + ສີແດງເຂັ້ມ + ພື້ນຫຼັງສີເຂັ້ມແບບກຳໜົດເອງ + ກຳນົດສີພື້ນຫຼັງຂອງຮູບແບບສີເຂັ້ມ + ຄ່າເລີ່ມຕົ້ນຂອງແອັບ + Material You (ເປັນກາງ) + Material You (ຫຼັກ) + Material You (ສຳຮອງ) + Material You (ສຳຮອງອັນດັບສາມ) + ກຳໜົດເອງ + ການດາວໂຫຼດ + ໂໝດດາວໂຫຼດ + ຄ່າເລີ່ມຕົ້ນ + ດາວໂຫຼດໃນແອັບ + ຕົວດາວໂຫຼດພາຍນອກ + ຊື່ແພັກເກັດຂອງຕົວດາວໂຫຼດ + ຊື່ແພັກເກັດຂອງແອັບຕົວດາວໂຫຼດພາຍນອກທີ່ຕິດຕັ້ງໄວ້ + ປ້ອນຊື່ແພັກເກດ + ອື່ນໆ + ບໍ່ໄດ້ຕິດຕັ້ງແອັບ + %s ບໍ່ໄດ້ຖືກຕິດຕັ້ງ. ກະລຸນາຕິດຕັ້ງມັນ + "ບໍ່ພົບແອັບທີ່ຕິດຕັ້ງໄວ້ທີ່ມີຊື່ແພັກເກັດ: %s + +ກະລຸນາກວດສອບວ່າຊື່ແພັກເກັດຖືກຕ້ອງ ແລະ ແອັບໄດ້ຖືກຕິດຕັ້ງແລ້ວ" + ຊື່ແພັກເກດບໍ່ສາມາດວ່າງໄດ້ + ກຳນົດເອງ + ໄອຄອນແອັບ + ຕົ້ນສະບັບ + Morphe ສີດຳ + Morphe ສີເຂັ້ມ + Morphe ສີອ່ອນ + Morphe ຫຼິ້ນໄຟ + Morphe play ສີດຳ + ກຳນົດເອງ + "ສິ່ງນີ້ຈະປ່ຽນໄອຄອນທີ່ສະແດງໃນຕົວປ່ອຍແອັບຂອງທ່ານ. + +ການຕັ້ງຄ່ານີ້ຈະບໍ່ປ່ຽນໄອຄອນໃນຕຳແໜ່ງເຫຼົ່ານີ້: +• ລາຍການແອັບໃນການຕັ້ງຄ່າລະບົບຂອງອຸປະກອນ +• ການແຈ້ງເຕືອນບາງຢ່າງ +• ຕົວເລືອກແອັບ 'ເປີດດ້ວຍ' +• ໄອຄອນ Android Auto + +ເພື່ອປ່ຽນໄອຄອນໃນທຸກບ່ອນ, ໃຫ້ patch ອີກຄັ້ງ ແລະ ປ່ຽນຕົວເລືອກ patch 'ໄອຄອນແອັບ'" + ໄອຄອນການແຈ້ງເຕືອນ + ໃຫ້ຄືກັບໄອຄອນແອັບ + ຫຼິ້ນ Morphe + ການແຈ້ງເຕືອນຈາກ Morphe + ປະຫວັດການເບິ່ງຂອງທ່ານບໍ່ຖືກບັນທຶກ<br><br>ສິ່ງນີ້ອາດເກີດຈາກຕົວບລັອກໂຄສະນາ DNS ຫຼື ພຣັອກຊີເຄືອຂ່າຍ<br><br>ເພື່ອແກ້ໄຂບັນຫານີ້, ໃຫ້ເພີ່ມ <b>s.youtube.com</b> ເຂົ້າໃນລາຍຊື່ທີ່ອະນຸຍາດ ຫຼື ປິດຕົວບລັອກ DNS ແລະ ພຣັອກຊີທັງໝົດ + ບໍ່ໃຫ້ສະແດງອີກ + ປອມແປງສະຕຣີມວິດີໂອ + ກຳນົດຄ່າການປອມແປງຄລາຍເອັນ ເພື່ອປ້ອງກັນບັນຫາໃນການຫຼິ້ນວິດີໂອ + "ແກ້ໄຂບັນຫາການຫຼິ້ນສຳລັບຜູ້ໃຊ້ສ່ວນໃຫຍ່ ໂດຍການຮ້ອງຂໍສະຕຣີມວິດີໂອທີ່ເຂົ້າກັນໄດ້ + +ຖ້າທ່ານເປັນຜູ້ໃຊ້ YouTube Premium, ການຕັ້ງຄ່ານີ້ອາດຈະບໍ່ຈຳເປັນ" + ບໍ່ສາມາດດຶງຂໍ້ມູນສະຕຣີມຂອງລູກຄ້າໄດ້ + ຄລາຍເອັນເລີ່ມຕົ້ນບໍ່ເຮັດວຽກ + "%1$s ບໍ່ເຮັດວຽກຫຼາຍຄັ້ງຕິດຕໍ່ກັນ, ແຕ່ %2$s ເຮັດວຽກໄດ້. + +ຕັ້ງຄ່າ %2$s ໃຫ້ເປັນຄລາຍເອັນເລີ່ມຕົ້ນບໍ?" + ປ່ຽນ + ການປິດການຕັ້ງຄ່ານີ້ອາດເຮັດໃຫ້ເກີດບັນຫາໃນການຫຼິ້ນວິດີໂອ + ລອງເປີດໃຊ້: Spoof video streams > ການເຂົ້າສູ່ລະບົບ Android VR + ໄຄລຽນເລີ່ມຕົ້ນ + ການເຂົ້າສູ່ລະບົບ Android VR + ຍັງບໍ່ທັນໄດ້ເຂົ້າສູ່ລະບົບ Android VR. ແຕະບ່ອນນີ້ເພື່ອເຂົ້າສູ່ລະບົບ + ປັດຈຸບັນໄດ້ເຂົ້າສູ່ລະບົບ Android VR ແລ້ວ + ເພື່ອໃຊ້ຄຸນສົມບັດນີ້, ໃຫ້ປ່ຽນ \'ລູກຄ້າເລີ່ມຕົ້ນ\' ເປັນ Android VR + ເຂົ້າສູ່ລະບົບ Android VR + "ການເຂົ້າສູ່ລະບົບ Android VR ສາມາດແກ້ໄຂບັນຫາການຫຼິ້ນສຳລັບບາງຜູ້ໃຊ້ + +ເພື່ອເຂົ້າສູ່ລະບົບ, ໃຫ້ກົດສືບຕໍ່ລຸ່ມນີ້" + ອອກຈາກລະບົບ + ຄຳແນະນຳ + "ລະຫັດການເປີດນຳໃຊ້: %s + +1. ແຕະ 'ເປີດເວັບໄຊ' ດ້ານລຸ່ມເພື່ອເປີດເວັບໄຊ 'ເຊື່ອມຕໍ່ອຸປະກອນ' +2. ວາງລະຫັດການເປີດນຳໃຊ້ທີ່ສະແດງຢູ່ຂ້າງເທິງຈາກຄລິບບອດ (ຫຼື ພິມໃສ່) +3. ເຂົ້າສູ່ລະບົບ Google ຖ້າມີການຮ້ອງຂໍ. ຖ້າທ່ານມີຫຼາຍບັນຊີ ໃຫ້ເລືອກບັນຊີ YouTube ນີ້ +4. ແຕະ 'ອະນຸຍາດ' ໃຫ້ YouTube VR ເຂົ້າເຖິງ +5. ກັບຄືນມາທີ່ແອັບນີ້ຫຼັງຈາກເຫັນຂໍ້ຄວາມ 'ເຊື່ອມຕໍ່ອຸປະກອນແລ້ວ'" + ບໍ່ສາມາດອອກລະຫັດການເປີດນຳໃຊ້ໄດ້ + ບໍ່ສາມາດອອກໂທເຄັນການຢືນຢັນຕົວຕົນໄດ້ + ເຂົ້າສູ່ລະບົບສຳເລັດແລ້ວ + "ບັນທຶກການຢືນຢັນຕົວຕົນແລ້ວ + +• ການຕັ້ງຄ່າ Morphe ຂອງທ່ານຈະບໍ່ນຳເຂົ້າ/ສົ່ງອອກ ການຢືນຢັນຕົວຕົນ Android VR +• ຖ້າແອັບນີ້ບໍ່ໄດ້ຖືກນຳໃຊ້ເປັນເວລາ 6 ເດືອນ Google ຈະລຶບການຢືນຢັນຕົວຕົນນີ້ອອກ +• ທ່ານຈະຕ້ອງເຂົ້າສູ່ລະບົບຄືນໃໝ່ ຖ້າທ່ານປ່ຽນລະຫັດຜ່ານ Google ຫຼື ຍົກເລີກການລົງທະບຽນອຸປະກອນນີ້ຈາກການຕັ້ງຄ່າ Google ຂອງທ່ານ" + OAuth2 ບໍ່ສາມາດນຳໃຊ້ໄດ້ຊົ່ວຄາວ (API หมดເວລາ) + ການເຂົ້າສູ່ລະບົບ Android VR ໝົດອາຍຸ, ກະລຸນາລອງເຂົ້າສູ່ລະບົບ VR ອີກຄັ້ງ + OAuth2 ບໍ່ສາມາດນຳໃຊ້ໄດ້ຊົ່ວຄາວ (ສະຖານະ %s) + OAuth2 ບໍ່ສາມາດນຳໃຊ້ໄດ້ຊົ່ວຄາວ + ບໍ່ສາມາດເຂົ້າສູ່ລະບົບ VR ໄດ້, ການຢືນຢັນຕົວຕົນຜ່ານເວັບໄຊບໍ່ໄດ້ຮັບການອະນຸມັດ + ບໍ່ສາມາດເຂົ້າສູ່ລະບົບ VR ໄດ້: %s + ອອກຈາກລະບົບຂອງ Android VR ແລ້ວ + ໂທເຄັນການເຂົ້າສູ່ລະບົບ VR ບໍ່ຖືກຕ້ອງ, ກະລຸນາລອງເຂົ້າສູ່ລະບົບ VR ອີກຄັ້ງ + ການຄວບຄຸມສື່ໃນການແຈ້ງເຕືອນ + ປັບແຕ່ງການຄວບຄຸມການຫຼິ້ນທີ່ສະແດງໃນໜ້າຈໍລັອກ ແລະ ແຜງການແຈ້ງເຕືອນຂອງທ່ານ + ເຊື່ອງປຸ່ມ ກ່ອນໜ້ານີ້ & ຖັດໄປ + ເຊື່ອງປຸ່ມ ກ່ອນໜ້ານີ້ ແລະ ຖັດໄປ ໃນການແຈ້ງເຕືອນສື່, ໜ້າຈໍລັອກ, ແລະ ການຄວບຄຸມຫູຟັງ + ປິດການໃຊ້ງານ seekbar + ປ້ອງກັນການລາກ seekbar ຢູ່ໜ້າຈໍລັອກ ແລະ ແຜງການແຈ້ງເຕືອນ + ຊອກຫາດ້ວຍປຸ່ມ ກ່ອນໜ້ານີ້ & ຖັດໄປ + ປ່ຽນການຄວບຄຸມສຳລັບການແຈ້ງເຕືອນສື່, ໜ້າຈໍລັອກ, ແລະ ຫູຟັງ ເພື່ອຊອກຫາແທນການປ່ຽນວິດີໂອ + ຄວາມຍາວໃນການຍ້ອນກັບ + ຈຳນວນວິນາທີທີ່ຈະຍ້ອນກັບດ້ວຍປຸ່ມ ກ່ອນໜ້ານີ້ + ຄວາມຍາວໃນການກ້າວຂ້າມ + ຈຳນວນວິນາທີທີ່ຈະກ້າວຂ້າມດ້ວຍປຸ່ມ ຖັດໄປ + ຄວາມຍາວຂອງການ Seek ຕ້ອງຢູ່ລະຫວ່າງ 1 ຫາ %s ວິນາທີ. ຈະໃຊ້ຄ່າເລີ່ມຕົ້ນ + ປິດການໃຊ້ງານໂປຣໂຕຄໍ QUIC + ປິດການໃຊ້ງານສຽງ DRC + ບັງຄັບໃຊ້ພາສາສຽງດັ້ງເດີມ + ຫຼິ້ນແທຣັກສຽງດັ້ງເດີມຂອງວິດີໂອສະເໝີ ແທນທີ່ເວີຊັນພາກສຽງ + ຂ້າມຄວາມງຽບ + ຂ້າມຊ່ວງເວລາທີ່ງຽບໃນສຽງຂອງວິດີໂອໂດຍອັດຕະໂນມັດ + ຄວາມຍາວຂອງການຢຸດທີ່ຕ່ຳສຸດ + ມີພຽງແຕ່ການຢຸດພັກທີ່ຍາວຢ່າງໜ້ອຍເທົ່ານີ້ເທົ່ານັ້ນທີ່ຈະຖືກເຮັດໃຫ້ສັ້ນລົງ. ຈະມີຜົນເມື່ອວິດີໂອຖັດໄປເລີ່ມເຮັດວຽກ + ພຣັອກຊີເຄືອຂ່າຍ + ກຳນົດເສັ້ນທາງການຮ້ອງຂໍເຄືອຂ່າຍທີ່ຮອງຮັບຜ່ານ proxy + ເປີດໃຊ້ proxy + ກຳນົດເສັ້ນທາງການຮ້ອງຂໍເຄືອຂ່າຍທີ່ຮອງຮັບຜ່ານ HTTP ຫຼື HTTPS proxy ທີ່ຖືກຕັ້ງຄ່າໄວ້ + ໂຮສຂອງ proxy + ຊື່ໂฮສ ຫຼື ທີ່ຢູ່ IP ຂອງເຊີເວີ proxy + ພອດຂອງ proxy + ພອດຂອງເຊີເວີ proxy + ໃຊ້ HTTPS proxy + ໃຊ້ HTTPS ເພື່ອເຊື່ອມຕໍ່ກັບເຊີເວີ proxy ແທນທີ່ HTTP + ການຢືນຢັນຕົວຕົນຂອງ proxy + ສົ່ງການຢືນຢັນຕົວຕົນແບບ Basic ໄປກັບການຮ້ອງຂໍ proxy + ຊື່ຜູ້ໃຊ້ Proxy + ຊື່ຜູ້ໃຊ້ສຳລັບການຢືນຢັນຕົວຕົນ proxy ແບບ Basic + ລະຫັດຜ່ານ Proxy + ລະຫັດຜ່ານສຳລັບການຢືນຢັນຕົວຕົນ proxy ແບບ Basic + ອະນຸຍາດໃຫ້ກັບໄປໃຊ້ການເຊື່ອມຕໍ່ໂດຍກົງ + ເຊື່ອມຕໍ່ໂດຍກົງ ຖ້າ proxy ຫຼົ້ມເຫຼວ + ເຊື່ອງໂຄສະນາເຕັມໜ້າຈໍ + ເພື່ອໃຊ້ຄຸນສົມບັດນີ້, ໃຫ້ປ່ຽນ \'Spoof video streams\' ເປັນຄລາຍເອນໃດໜຶ່ງ ຍົກເວັ້ນ \'Android Studio\' ແລະ \'Android VR Downgraded\' + ລຶບຂໍ້ມູນສ່ວນຕົວຈາກລິ້ງແບ່ງປັນ + ລຶບພາຣາມິເຕີການຕິດຕາມອອກຈາກລິ້ງທີ່ແບ່ງປັນ + ປ່ຽນລິງກ໌ການແບ່ງປັນເປັນ youtube.com + ປ່ຽນ music.youtube.com ເປັນ youtube.com ໃນລິ້ງທີ່ແບ່ງປັນ + ປ່ຽນລິ້ງແບ່ງປັນເປັນ youtu.be + ບັງຄັບໃຫ້ວິດີໂອໃຊ້ຮູບແບບ URL youtu.be ທີ່ສັ້ນລົງ + ບັນທຶກການດີບັກ protocol buffer + ເພີ່ມຂໍ້ມູນການບັນທຶກເພີ່ມເຕີມສຳລັບນັກພັດທະນາ ເພື່ອລະບຸສ່ວນປະກອບຂອງແອັບ + "ການເປີດການຕັ້ງຄ່ານີ້ຈະບັນທຶກຂໍ້ມູນເລເອົາທ໌ເພີ່ມເຕີມ, ລວມທັງຂໍ້ຄວາມເທິງໜ້າຈໍສຳລັບບາງສ່ວນປະກອບຂອງ UI. + +ສິ່ງນີ້ສາມາດຊ່ວຍລະບຸສ່ວນປະກອບຕ່າງໆ ເມື່ອສ້າງຕົວຕອງທີ່ກຳນົດເອງ. + +ຢ່າງໃດກໍຕາມ, ການເປີດໃຊ້ງານສິ່ງນີ້ຈະບັນທຶກຂໍ້ມູນຜູ້ໃຊ້ບາງຢ່າງ ເຊັ່ນ: ທີ່ຢູ່ IP ຂອງທ່ານ" + ຕົວຕອງແບບກຳນົດເອງ + ກຳນົດຄ່າຕົວຕອງແບບກຳນົດເອງເພື່ອເຊື່ອງສ່ວນປະກອບໃດໜຶ່ງ + ເປີດໃຊ້ຕົວຕອງແບບກຳນົດເອງ + ລາຍການຂອງຂໍ້ຄວາມ component path builder ທີ່ຈະກັ່ນຕອງ, ແຍກດ້ວຍການຂຶ້ນແຖວໃໝ່ + ຕົວຕອງທີ່ກຳນົດເອງບໍ່ຖືກຕ້ອງ: %s + ຕົວຕອງເມນູການຕັ້ງຄ່າ + ເຊື່ອງລາຍການໃນໜ້າຈໍການຕັ້ງຄ່າແອັບມາດຕະຖານ ໂດຍອີງຕາມຊື່ທີ່ປາກົດ + ເປີດໜ້າຈໍການຕັ້ງຄ່າແອັບມາດຕະຖານໜຶ່ງຄັ້ງເພື່ອສ້າງລາຍການ + ລຶບກ່ອງໂທລະນາຍການພິຈາລະນາຂອງຜູ້ຊົມ + ສິ່ງນີ້ບໍ່ໄດ້ຂ້າມການຈຳກັດອາຍຸ. ມັນພຽງແຕ່ຍອມຮັບໂດຍອັດຕະໂນມັດ + ຜູ້ໃຫ້ບໍລິການ PoToken + ກຳນົດຄ່າຜູ້ໃຫ້ບໍລິການ PoToken ເພື່ອປ້ອງກັນບັນຫາໃນການຫຼິ້ນ + ຜູ້ໃຫ້ບໍລິການ PoToken ພາຍນອກ + "ແກ້ໄຂບັນຫາການຫຼິ້ນໂດຍໃຊ້ PoToken minter ພາຍນອກ (ທົດລອງ) + +ຖ້າທ່ານເປັນຜູ້ໃຊ້ YouTube Premium, ການຕັ້ງຄ່ານີ້ອາດຈະບໍ່ຈຳເປັນ" + "ແກ້ໄຂບັນຫາການຫຼິ້ນໂດຍໃຊ້ PoToken minter ພາຍນອກ (ທົດລອງ) + +ເພື່ອໃຊ້ຄຸນສົມບັດນີ້, ໃຫ້ປິດ 'ປອມແປງສະຕຣີມວິດີໂອ'" + ຖ້າມັນບໍ່ເຮັດວຽກ, ໃຫ້ລອງປິດ MicroG ແບບບັງຄັບ + ເປີດການຕັ້ງຄ່າ PotHelper + "ຟີເຈີນີ້ກຳລັງຢູ່ໃນຂັ້ນຕອນການພັດທະນາ ແລະ ອາດຈະບໍ່ເຮັດວຽກ + +ແຕະທີ່ນີ້ເພື່ອດາວໂຫຼດ PotHelper" + PotHelper ບໍ່ເຮັດວຽກອີກຕໍ່ໄປ + "ໂທເຄັນທີ່ຕິດຕັ້ງມາໃນ PotHelper ເວີຊັນນີ້ ບໍ່ສາມາດໃຊ້ງານໄດ້ອີກຕໍ່ໄປ + +ເປີດ 'Spoof video streams' ເພື່ອແກ້ໄຂການຫຼິ້ນວິດີໂອ" + ເປີດ + ປອມແປງເວີຊັນຂອງແອັບ + ຫຼອກເຊີເວີໃຫ້ຄິດວ່າທ່ານກຳລັງໃຊ້ແອັບເວີຊັນເກົ່າ ເພື່ອຟື້ນຟູຄຸນສົມບັດ UI ແບບເກົ່າ + "ເວີຊັນຂອງແອັບຈະຖືກປອມແປງເປັນເວີຊັນທີ່ເກົ່າກວ່າ. + +ສິ່ງນີ້ຈະປ່ຽນຮູບລັກສະນະ ແລະ ຄຸນສົມບັດຂອງແອັບ, ແຕ່ອາດເກີດຜົນຂ້າງຄຽງທີ່ບໍ່ຮູ້ສາເຫດ. + +ຫາກປິດການນຳໃຊ້ໃນພາຍຫຼັງ, ແນະນຳໃຫ້ລຶບຂໍ້ມູນແອັບເພື່ອປ້ອງກັນຂໍ້ຜິດພາດຂອງ UI" + ປອມແປງເວີຊັນເປົ້າໝາຍຂອງແອັບ + ອັດຕາການຣີເຟຣຊຂອງແອັບ + ຄ່າເລີ່ມຕົ້ນ + %s FPS + ປ່ຽນແທນອັດຕາການຣີເຟຣຊ + ຕະຫຼອດເວລາ + ການຫຼິ້ນແບບແນວຕັ້ງ + ຫຼິ້ນແບບເຕັມຈໍ + ຫຼິ້ນແບບຕັ້ງ ແລະ ເຕັມຈໍ + ຟອນ + ຄ່າເລີ່ມຕົ້ນ + ຟອນລະບົບຂອງອຸປະກອນ + ໄຟລ໌ຟອນທີ່ກຳນົດເອງ + ໄຟລ໌ຟອນທີ່ກຳນົດເອງ + ເລືອກໄຟລ໌ຟອນ TTF ຫຼື OTF + ໄຟລ໌ທີ່ເລືອກບໍ່ແມ່ນຟອນ TTF ຫຼື OTF + ບໍ່ສາມາດໂຫຼດຟອນຕ໌ທີ່ກຳນົດເອງໄດ້ ແລະ ຖືກປິດການໃຊ້ງານ + ບໍ່ສາມາດເບິ່ງຈຳນວນ Dislike ໄດ້ຊົ່ວຄາວ (API หมดເວລາ) + ບໍ່ສາມາດນຳໃຊ້ການບໍ່ມັກໄດ້ (ສະຖານະ %s) + ບໍ່ສາມາດເບິ່ງຈຳນວນ Dislike ໄດ້ (ຂໍ້ຈຳກັດ API ຂອງລູກຄ້າ) + ບໍ່ສາມາດເບິ່ງຈຳນວນ Dislike ໄດ້ (%s) + ຖືກເຊື່ອງໂດຍເຈົ້າຂອງ + ບໍ່ມັກວິດີໂອນີ້, ພ້ອມກັບອີກ %s ຄົນ + ບໍ່ມັກວິດີໂອນີ້, %s ຂອງຄະແນນສຽງແມ່ນບໍ່ມັກ + ເປີດໃຊ້ງານ Return YouTube Dislike + ສະແດງການບໍ່ມັກເປັນເປີເຊັນ + ສະແດງການບໍ່ມັກເປັນເປີເຊັນຂອງການມັກ ແທນທີ່ຈະເປັນຕົວເລກ + ສະແດງການຄາດຄະເນການມັກ + ສະແດງຈຳນວນການຄາດຄະເນການມັກໃນວິດີໂອ ທີ່ຜູ້ສ້າງໄດ້ປິດການມັກ + ສະແດງການແຈ້ງເຕືອນຂໍ້ຜິດພາດໃນການເຊື່ອມຕໍ່ + ສະແດງການແຈ້ງເຕືອນເມື່ອເຊີບເວີ Return YouTube Dislike ບໍ່ສາມາດນຳໃຊ້ໄດ້ + "Return YouTube Dislike ຈະກູ້ຄືນຈຳນວນການບໍ່ມັກທີ່ YouTube ໄດ້ເຊື່ອງໄວ້ໃນປີ 2021. ຈຳນວນເຫຼົ່ານີ້ມາຈາກຂໍ້ມູນທີ່ຖືກເກັບໄວ້ກ່ອນການລຶບອອກ ແລະ ແມ່ນການຄາດຄະເນສຳລັບວິດີໂອໃໝ່ໆ ຈາກການລົງຄະແນນສຽງຂອງຜູ້ໃຊ້, ດັ່ງນັ້ນພວກມັນຈຶ່ງເປັນຄ່າໂດຍປະມານ ແລະ ຈະອັບເດທ໌ທຸກໆ ບໍ່ກີ່ມື້ + +ID ຂອງວິດີໂອທີ່ທ່ານເບິ່ງຈະຖືກສົ່ງໄປຍັງ Return YouTube Dislike API, ແລະ ການມັກ ແລະ ບໍ່ມັກຂອງທ່ານຈະຖືກສົ່ງແບບບໍ່ລະບຸຕົວຕົນ ເພື່ອປັບປຸງການຄາດຄະເນໃຫ້ດີຂຶ້ນ" + ສະຖິຕິ Return YouTube Dislike API ຂອງອຸປະກອນນີ້ + ເວລາຕອບສະໜອງຂອງ API, ສະເລ່ຍ + ເວລາຕອບສະໜອງຂອງ API, ຕ່ຳສຸດ + ເວລາຕອບສະໜອງຂອງ API, ສູງສຸດ + ເວລາຕອບສະໜອງຂອງ API, ສື່ຫຼ້າສຸດ + ບໍ່ສາມາດເບິ່ງຈຳນວນ Dislikes ໄດ້ຊົ່ວຄາວ - ຂໍ້ຈຳກັດອັດຕາ API ຂອງ Client ກຳລັງມີຜົນ + ການດຶງຂໍ້ມູນຄະແນນ API, ຈຳນວນການເອີ້ນໃຊ້ + ບໍ່ມີການຮຽກໃຊ້ເຄືອຂ່າຍ + ມີການເອີ້ນໃຊ້ເຄືອຂ່າຍ %s ຄັ້ງ + ການດຶງຂໍ້ມູນຄະແນນສຽງຈາກ API, ຈຳນວນທີ່ໝົດເວລາ + ບໍ່ມີການເອີ້ນເຄືອຂ່າຍທີ່ໝົດເວລາ + ການເອີ້ນໃຊ້ເຄືອຂ່າຍ %s ຄັ້ງໝົດເວລາ + ຂີດຈຳກັດອັດຕາຂອງ API client + ບໍ່ພົບຂີດຈຳກັດອັດຕາຂອງ client + ພົບຂໍ້ຈຳກັດອັດຕາການຮ້ອງຂໍຂອງຄລາຍເອນ %s ຄັ້ງ + %s ມິນລິວິນາທີ diff --git a/patches/src/main/resources/addresources/values-lo-rLA/shared/strings.xml b/patches/src/main/resources/addresources/values-lo-rLA/shared/strings.xml index 31cdbe4dcb..72ea0acd96 100644 --- a/patches/src/main/resources/addresources/values-lo-rLA/shared/strings.xml +++ b/patches/src/main/resources/addresources/values-lo-rLA/shared/strings.xml @@ -17,21 +17,219 @@ Second \"item\" text" --> + ການຕັ້ງຄ່າ + ທ່ານແນ່ໃຈຫຼືບໍ່ວ່າຕ້ອງການດຳເນີນການຕໍ່? + ບັນທຶກ + ຣີເຊັດ + ຣີເຊັດສີ + ສີບໍ່ຖືກຕ້ອງ + ຈຳເປັນຕ້ອງຣີສຕາດ + ກະລຸນາເລີ່ມແອັບໃໝ່ ເພື່ອໃຫ້ການປ່ຽນແປງນີ້ມີຜົນ + ຣີສຕາດ + ນຳເຂົ້າ + ຄັດລອກ + ຣີເຊັດການຕັ້ງຄ່າ Morphe ເປັນຄ່າເລີ່ມຕົ້ນ + ນຳເຂົ້າການຕັ້ງຄ່າ %s ແລ້ວ + ການນຳເຂົ້າລົ້ມເຫຼວ: %s + ຍ້ອນກັບໄປ + ຄົ້ນຫາການຕັ້ງຄ່າ + ກ່ຽວກັບ + ບໍ່ພົບຜົນລັດສຳລັບ \'%s\' + ລອງໃຊ້ຄຳຄົ້ນຫາອື່ນ + ການຄົ້ນຫາຫຼ້າສຸດ + ລຶບອອກຈາກປະຫວັດການຄົ້ນຫາບໍ? + ລຶບອອກຈາກປະຫວັດການຄົ້ນຫາ + ລ້າງປະຫວັດການຄົ້ນຫາ + ທ່ານແນ່ໃຈບໍວ່າຕ້ອງການລ້າງປະຫວັດການຄົ້ນຫາທັງໝົດ? + ເຄັດລັບການຄົ້ນຫາ + "• ແຕະທີ່ເສັ້ນທາງເພື່ອໄປຫາບ່ອນນັ້ນ +• ກົດຄ້າງທີ່ການຕັ້ງຄ່າເພື່ອໄປຫາບ່ອນນັ້ນ +• ກົດ Enter ເພື່ອບັນທຶກຄຳຄົ້ນຫາລົງໃນປະຫວັດ +• ການຄົ້ນຫາຈະບໍ່ຄຳນຶງເຖິງຕົວອັກສອນໃຫຍ່-ນ້ອຍ ແລະ ເຄື່ອງໝາຍວັກຕອນ +• ການຕັ້ງຄ່າຫຼັກຈະປາກົດຢູ່ເໜືອການຕັ້ງຄ່າຍ່ອຍທີ່ຖືກປິດໃຊ້ງານ" + ປະຫວັດການຄົ້ນຫາວ່າງເປົ່າ + ເພື່ອບັນທຶກປະຫວັດການຄົ້ນຫາ, ໃຫ້ພິມຄຳຄົ້ນຫາ ແລະ ກົດ Enter + ສະແດງປະຫວັດການຄົ້ນຫາການຕັ້ງຄ່າ + ຊື່ການຕັ້ງຄ່າ Morphe + ຄ່າເລີ່ມຕົ້ນ + ສ່ວນເສີມ + ການປັບແຕ່ງ + ເວລາໃນການ Morphing + ກຳນົດເອງ + ປ້ອນຊື່ + ຊື່ບໍ່ສາມາດປ່ອຍວ່າງໄດ້ + ສະແດງໄອຄອນການຕັ້ງຄ່າ Morphe + ຄັດລອກແລ້ວ: %s + ພາສາ Morphe + "ການແປສຳລັບບາງພາສາອາດຈະຂາດຫາຍ ຫຼື ບໍ່ສົມບູນ. + +ເພື່ອແປພາສາໃໝ່ ຫຼື ປັບປຸງການແປທີ່ມີຢູ່, ໃຫ້ໄປທີ່ morphe.software/translate" + ພາສາຂອງແອັບ + ນຳເຂົ້າ / ສົ່ງອອກ + ນຳເຂົ້າ / ສົ່ງອອກ ການຕັ້ງຄ່າ Morphe + ນຳເຂົ້າຈາກໄຟລ໌ + ນຳເຂົ້າການຕັ້ງຄ່າສຳເລັດແລ້ວ, ແຕະ ບັນທຶກ ເພື່ອປ່ຽນແປງ + ນຳເຂົ້າການຕັ້ງຄ່າບໍ່ສຳເລັດ + ສົ່ງອອກເປັນໄຟລ໌ + ສົ່ງອອກການຕັ້ງຄ່າສຳເລັດແລ້ວ + ສົ່ງອອກການຕັ້ງຄ່າບໍ່ສຳເລັດ + ທ່ານກຳລັງໃຊ້ Morphe Patches ເວີຊັນຫຼ້າສຸດ <i>%s</i> + ທ່ານກຳລັງໃຊ້ Morphe Patches ເວີຊັນ <i>%1$s</i><br><br><b>ມີການອັບເດດ: <i>%2$s</i></b><br><br>ເພື່ອອັບເດດ patches ຂອງທ່ານ, ໃຫ້ໃຊ້ Morphe ເພື່ອ patch ແອັບນີ້ຄືນໃໝ່ + ໝາຍເຫດ + ເວີຊັນນີ້ແມ່ນເວີຊັນກ່ອນການປ່ອຍຕົວ ແລະ ທ່ານອາດຈະພົບກັບບັນຫາທີ່ບໍ່ຄາດຄິດ + ລິງກ໌ທາງການ + ເວັບໄຊທ໌ + ບໍລິຈາກ + ການແປພາສາ + ເຄຣດິດ + ເປັນເວີຊັນຫຼ້າສຸດແລ້ວ + ມີການອັບເດດໃໝ່ + ການພັດທະນາປັດຈຸບັນ + ການພັດທະນາກ່ອນໜ້ານີ້ + ການພັດທະນາທີ່ກຳລັງດຳເນີນການ + 2022 ‑ 2025 + 2022 ‑ 2025 + 2019 ‑ 2022 + ແຈ້ງການອະນຸຍາດ + ກຳລັງໂຫຼດ… + ການກວດສອບຂໍ້ຜິດພາດ + ກຳນົດຄ່າຕົວເລືອກນັກພັດທະນາຂັ້ນສູງ ແລະ ບັນທຶກການວິນິດໄສ + ເປີດການບັນທຶກ debug + ລາຍລະອຽດ stack trace ຂອງບັນທຶກການດີບັກ + ລວມ stack traces ໃນບັນທຶກ debug ເພື່ອຊ່ວຍໃນການລະບຸຂໍ້ຜິດພາດ + ສະແດງການແຈ້ງເຕືອນແບບ popup ເມື່ອເກີດຂໍ້ຜິດພາດໃນ Morphe + "ການປິດການແຈ້ງເຕືອນຂໍ້ຜິດພາດແບບ toast ຈະເຊື່ອງທຸກການແຈ້ງເຕືອນຂໍ້ຜິດພາດຂອງ Morphe. + +ທ່ານຈະບໍ່ໄດ້ຮັບການແຈ້ງເຕືອນກ່ຽວກັບເຫດການທີ່ບໍ່ຄາດຄິດໃດໆ" + ສົ່ງອອກບັນທຶກການກວດສອບຂໍ້ຜິດພາດ + ສົ່ງອອກບັນທຶກການກວດສອບຂໍ້ຜິດພາດຂອງ Morphe + ສົ່ງອອກບັນທຶກການກວດສອບຂໍ້ຜິດພາດສຳເລັດແລ້ວ + ການສົ່ງອອກບັນທຶກການກວດສອບຂໍ້ຜິດພາດລົ້ມເຫຼວ + ຄົ້ນຫາບັນທຶກການດີບັກ… + ລ້າງ + ການບັນທຶກການກວດສອບຂໍ້ຜິດພາດຖືກປິດໃຊ້ງານ + ບໍ່ພົບລາຍການບັນທຶກການກວດສອບຂໍ້ຜິດພາດ + ຄັດລອກບັນທຶກການກວດສອບຂໍ້ຜິດພາດແລ້ວ + ການສົ່ງອອກບັນທຶກການກວດສອບຂໍ້ຜິດພາດລົ້ມເຫຼວ: %s + ລ້າງບັນທຶກການກວດສອບຂໍ້ຜິດພາດແລ້ວ + ຕົວຈັດການ Feature flags + ຈັດການ boolean feature flags + ຄົ້ນຫາ feature flags… + ບໍ່ໄດ້ເປີດໃຊ້ (%s) + ເປີດໃຊ້ຢູ່ (%s) + ຖືກບລັອກ (%s) + ບັງຄັບ (%s) + ອັດຕະໂນມັດ + ປິດ + ເປີດ + ບັງຄັບໃຫ້ເປີດ + ບລັອກ + ລຶບອອກ + ບັນທຶກການຕັ້ງຄ່າ Feature flags ແລ້ວ + ຣີເຊັດ Feature flags ແລ້ວ + ຄັດລອກ Feature flags ໄປยังຄລິບບອດແລ້ວ + ເລີ່ມແອັບໃໝ່ເພື່ອໃຊ້ການຕັ້ງຄ່ານີ້ + ສົ່ງອອກ ແລະ ນຳເຂົ້າ + ນຳເຂົ້າ feature flags %s ສຳເລັດແລ້ວ + ບໍ່ພົບ feature flags + ສົ່ງອອກ Feature flags ສຳເລັດແລ້ວ + ການສົ່ງອອກ feature flags ລົ້ມເຫຼວ + ໂຫຼດ Feature flags ແລ້ວ, ແຕະ ບັນທຶກ ເພື່ອໃຊ້ງານ + ການອ່ານໄຟລ໌ລົ້ມເຫຼວ + ການຄົ້ນຫາແບບ Binary search + "ຄົ້ນຫາວ່າ feature flag ໃດທີ່ເຮັດໃຫ້ເກີດພຶດຕິກຳຂອງແອັບ. + +ເຄິ່ງໜຶ່ງຂອງ %s flags ຖືກບລັອກ, ແລະ ຫຼັງຈາກການເລີ່ມຕົ້ນໃໝ່ແຕ່ລະຄັ້ງ ທ່ານຕ້ອງບອກວ່າພຶດຕິກຳນັ້ນຍັງຄົງມີຢູ່ຫຼືບໍ່. + +ເຮັດໃຫ້ເກີດພຶດຕິກຳນັ້ນຄັ້ງໜຶ່ງກ່ອນເລີ່ມຕົ້ນ, ບໍ່ດັ່ງນັ້ນ flags ທີ່ແອັບຍັງບໍ່ທັນໄດ້ຮຽກຮ້ອງຈະຫາຍໄປຈາກການຄົ້ນຫາ" + ເລີ່ມຕົ້ນ + "ບໍ່ມີ feature flags ທີ່ຈະຄົ້ນຫາ. + +ສະເພາະ flags ທີ່ແອັບໄດ້ເຂົ້າເຖິງ ແລະ ບໍ່ໄດ້ຖືກບັງຄັບໃຫ້ເປີດໃຊ້ໃນປັດຈຸບັນເທົ່ານັ້ນ ທີ່ສາມາດໃຊ້ໃນການຄົ້ນຫາແບບ binary search ໄດ້" + "ຂັ້ນຕອນ %1$s + +ເຫຼືອ %2$s flags, %3$s ຂອງພວກມັນຖືກບລັອກສຳລັບຂັ້ນຕອນນີ້. + +ເລີ່ມແອັບໃໝ່, ກວດສອບວ່າພຶດຕິກຳດັ່ງກ່າວຍັງມີຢູ່ຫຼືບໍ່, ຈາກນັ້ນຕອບຄຳຖາມລຸ່ມນີ້" + ກຳລັງຄົ້ນຫາ Feature flag + ພຶດຕິກຳດັ່ງກ່າວມີຢູ່ + ພຶດຕິກຳດັ່ງກ່າວບໍ່ມີຢູ່ + ຍົກເລີກການຄົ້ນຫາແບບ Binary search + ຍົກເລີກການຄົ້ນຫາແບບ Binary search ແລ້ວ + "Feature flag %s ເປັນສາເຫດຂອງພຶດຕິກຳນີ້. + +ຕອນນີ້ມັນຖືກບລັອກແລ້ວ" + "ບໍ່ສາມາດຊອກຫາ feature flag ໄດ້. + +ພຶດຕິກຳຂອງແອັບເບິ່ງຄືວ່າບໍ່ໄດ້ຖືກຄວບຄຸມໂດຍ feature flag" + "ບໍ່ສາມາດຊອກຫາ feature flag ດຽວໄດ້. + +ພຶດຕິກຳດັ່ງກ່າວໄດ້ຫາຍໄປກ່ອນໜ້ານີ້ໃນການຄົ້ນຫາ, ດັ່ງນັ້ນອາດຈະມີ flag ຫຼາຍກວ່າໜຶ່ງອັນທີ່ກ່ຽວຂ້ອງ ຫຼື ຄຳຕອບໜຶ່ງໃນນັ້ນບໍ່ຖືກຕ້ອງ" + ເປີດການຕັ້ງຄ່າ MicroG + ບໍ່ໄດ້ຕິດຕັ້ງ MicroG. ກະລຸນາຕິດຕັ້ງ + ຕ້ອງດຳເນີນການ + "MicroG v%1s ຂອງທ່ານເກົ່າເກີນໄປ ແລະ ອາດເຮັດໃຫ້ເກີດບັນຫາ. + +ກະລຸນາອັບເດດ MicroG ເປັນເວີຊັນຫຼ້າສຸດ v%2s" + ອັບເດດ + ເມີນເສີຍ + "MicroG ທີ່ຕິດຕັ້ງໄວ້ບໍ່ໄດ້ຮັບການແນະນຳໂດຍ Morphe: + +%1s + +ແຕະເພື່ອຖອນການຕິດຕັ້ງ, ຈາກນັ້ນເປີດແອັບນີ້ຄືນໃໝ່ ແລະ ຕິດຕັ້ງ MicroG RE" + ຖອນການຕິດຕັ້ງ + "MicroG ບໍ່ມີສິດໃນການເຮັດວຽກຢູ່ເບื้องຫຼັງ. + +ໃຫ້ປະຕິບັດຕາມຄຳແນະນຳ \"Don't kill my app\" ສຳລັບໂທລະສັບຂອງທ່ານ, ແລະ ນຳໃຊ້ຂັ້ນຕອນດັ່ງກ່າວເຂົ້າໃນການຕິດຕັ້ງ MicroG ຂອງທ່ານ. + +ສິ່ງນີ້ແມ່ນຈຳເປັນເພື່ອໃຫ້ແອັບສາມາດເຮັດວຽກໄດ້" + ເປີດເວັບໄຊທ໌ + "ຕ້ອງປິດການປັບປຸງການໃຊ້ພະລັງງານຂອງ MicroG ເພື່ອປ້ອງກັນບັນຫາ. + +ການປິດການປັບປຸງພະລັງງານສຳລັບ MicroG ຈະບໍ່ສົ່ງຜົນກະທົບທາງລົບຕໍ່ການໃຊ້ແບັດເຕີຣີ. + +ແຕະປຸ່ມດຳເນີນການຕໍ່ ແລະ ອະນຸຍາດໃຫ້ມີການປ່ຽນແປງການປັບປຸງ" + ດຳເນີນການຕໍ່ + ສະແດງບ່ອນໂต้ຕອບການປັບປຸງແບັດເຕີຣີຂອງ MicroG + "ການປິດການນຳໃຊ້ອັນນີ້ ຈະຊ່ອນການແຈ້ງເຕືອນການປັບປຸງແບັດເຕີຣີຂອງ MicroG. + +ຫາກບໍ່ມີການປ່ຽນແປງການປັບປຸງແບັດເຕີຣີ, ທ່ານອາດຈະພົບກັບ: +• ແອັບອາດຈະປິດລົງແບບສຸ່ມ +• ການແຈ້ງເຕືອນອາດຈະບໍ່ເຮັດວຽກ +• ການຫຼິ້ນໃນພື້ນຫຼັງອາດຈະບໍ່ເຮັດວຽກ" + ການຈັດການລິ້ງຂອງແອັບ + ກຳນົດຄ່າລິ້ງເພື່ອເປີດໃນແອັບນີ້ + ຂັ້ນຕອນທີ 1: ປິດການໃຊ້ງານລິ້ງຂອງແອັບຕົ້ນສະບັບ + ເປີດການຕັ້ງຄ່າຂອງແອັບຕົ້ນສະບັບ ແລະ ປິດການໃຊ້ງານລິ້ງໂດເມນທັງໝົດ + ຂັ້ນຕອນທີ 2: ເປີດການໃຊ້ງານລິ້ງຂອງແອັບທີ່ຜ່ານການ Patch + ເປີດການຕັ້ງຄ່າຂອງແອັບທີ່ຜ່ານການ Patch ແລະ ເປີດການໃຊ້ງານລິ້ງໂດເມນທັງໝົດ + ເປີດການຕັ້ງຄ່າ + 🚨 ການແຈ້ງເຕືອນການທົດລອງ 🚨 + "ທ່ານກຳລັງໃຊ້ແອັບເວີຊັນທົດລອງຂອງ ⚠️ <b>%1$s</b> +<br/><br/> +🔧 ອາດຈະມີການເຮັດວຽກຂອງແອັບທີ່ຜິດປົກກະຕິ ຫຼື ບັກທີ່ຍັງບໍ່ທັນຮູ້ ເນື່ອງຈາກພວກເຮົາກຳລັງປັບປຸງ patch ສຳລັບແອັບເວີຊັນນີ້. +<br/><br/> +ຖ້າທ່ານຕ້ອງການປະສົບການທີ່ບໍ່ມີບັນຫາ, ກະລຸນາ <b>ຖອນການຕິດຕັ້ງ</b> ອັນນີ້ ແລະ ຕິດຕັ້ງແອັບເວີຊັນທີ່ແນະນຳຂອງ ✅ <b>%2$s</b>" + ⚠️ ຂ້ອຍຕ້ອງການແບບທົດລອງ + ✅ ຂ້ອຍຕ້ອງການແບບສະຖຽນ + ບັນທຶກປຶ້ມບັນທຶກການຂັດຂ້ອງໄວ້ໃນເອກະສານຂອງຜູ້ໃຊ້: %s + ບັນທຶກປຶ້ມບັນທຶກການຂັດຂ້ອງແລ້ວ: %s diff --git a/patches/src/main/resources/addresources/values-lo-rLA/sponsorblock/strings.xml b/patches/src/main/resources/addresources/values-lo-rLA/sponsorblock/strings.xml index 9d2d4ab7d5..d16714bc30 100644 --- a/patches/src/main/resources/addresources/values-lo-rLA/sponsorblock/strings.xml +++ b/patches/src/main/resources/addresources/values-lo-rLA/sponsorblock/strings.xml @@ -16,7 +16,9 @@ Second \"item\" text" --> + ໃຊ້ມຸມສີ່ຫຼ່ຽມສຳລັບປຸ່ມ ແລະ ຕົວຄວບຄຸມຂອງ SponsorBlock + ກຳລັງໂຫຼດ… diff --git a/patches/src/main/resources/addresources/values-lo-rLA/youtube/strings.xml b/patches/src/main/resources/addresources/values-lo-rLA/youtube/strings.xml index 70ce03fc67..196457ac7d 100644 --- a/patches/src/main/resources/addresources/values-lo-rLA/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-lo-rLA/youtube/strings.xml @@ -18,213 +18,1640 @@ Second \"item\" text" + ກ່ຽວກັບ Morphe + ໂຄສະນາ + ຟີດ + ທົ່ວໄປ + ເຄື່ອງຫຼິ້ນ + Shorts + ແຖບເລື່ອນເວລາ + ການຄວບຄຸມດ້ວຍການປ້ັດ + ເບັດເຕເລັດ + ວິດີໂອ + ກູ້ຄືນໜ້າຈໍການຕັ້ງຄ່າ YouTube ແບບເກົ່າ + ເອົາຂໍ້ຈຳກັດການຫຼິ້ນເບื้องຫຼັງອອກ + ປິດການຫຼິ້ນເບื้องຫຼັງ + ຢຸດຊົ່ວຄາວອັດຕະໂນມັດເມື່ອລັອກອຸປະກອນ + ປິດ + ຢຸດຊົ່ວຄາວສະເໝີເມື່ອລັອກ + ຢຸດຊົ່ວຄາວ ຍົກເວັ້ນແຕ່ເຊື່ອມຕໍ່ສຽງແບບໄຮ້ສາຍ + ປິດການຈຳກັດຄວາມໄວໃນການເລື່ອນ + ປິດການອັບເດດເລআᱣຕ໌ + ປ້ອງກັນບໍ່ໃຫ້ເຊີເວີອັບເດດເຄົ້າໂຄງຂອງແອັບ + "ຮູບແບບແອັບຈະກັບຄືນໄປສູ່ຮູບແບບທີ່ໃຊ້ໃນຕອນຕິດຕັ້ງຄັ້ງທຳອິດ. + +ບາງຮູບແບບທາງຝັ່ງເຊີບເວີອາດຈະບໍ່ກັບຄືນ. + +ການປ່ຽນແປງລວມມີ: +• ສ່ວນປະກອບໃນເມນູ flyout ຂອງເຄື່ອງຫຼິ້ນ (ຫຼື ການຕັ້ງຄ່າທີ່ກ່ຽວຂ້ອງ) ອາດຈະບໍ່ເຮັດວຽກ +• ຕົວເລກທີ່ກຳລັງໝຸນຈະບໍ່ມີການເຄື່ອນໄຫວ +• ແຖບຫ້ອງສະໝຸດຈະຖືກສະແດງ +• ສ່ວນດົນຕີຂອງລາຍລະອຽດວິດີໂອອາດຈະບໍ່ເຮັດວຽກ +• ລາຍການເມນູ flyout ຂອງເຄື່ອງຫຼິ້ນບໍ່ສາມາດເຊື່ອງໄດ້ +• ປຸ່ມສະຫຼັບບັນຊີອາດຈະບໍ່ປາກົດໃນແຖບຫ້ອງສະໝຸດ" + ເປີດໃຊ້ຕົວຕອງເມນູ flyout ຂອງ feed + ຕົວຕອງເມນູ flyout ຂອງ feed + ປ້ອນຊື່ເມນູ flyout ທີ່ຕ້ອງການກັ່ນຕອງ, ໂດຍແຍກດ້ວຍການຂຶ້ນແຖວໃໝ່ + ເປີດໃຊ້ການກັ່ນกรອງເມນູລຸ່ມ ຫ້ອງສະໝຸດ / ທ່ານ + ການກັ່ນกรອງເມນູລຸ່ມ ຫ້ອງສະໝຸດ / ທ່ານ + ປ້ອນຊື່ເມນູລຸ່ມ ຫ້ອງສະໝຸດ / ທ່ານ ທີ່ຕ້ອງການກັ່ນกรອງ, ໂດຍແຍກດ້ວຍການຂຶ້ນແຖວໃໝ່ + ເຊື່ອງບັດອັນບັມ + ເຊື່ອງບັດອັນບັ້ມ ພາຍໃຕ້ຄໍາອະທິບາຍຂອງສິລະປິນ + ເຊື່ອງບັດສິລະປິນ + ເຊື່ອງບັດສິລະປິນ ຢູ່ສ່ວນເທິງຂອງຜົນການຄົ້ນຫາ + ເຊື່ອງປຸ່ມຊ່ອງ + ເຊື່ອງປຸ່ມຕ່າງໆ ເຊັ່ນ: ຕິດຕາມ, ເບິ່ງຊ່ອງ, ຊຸມຊົນ, ແລະ YouTube Music ຈາກໂປຣໄຟລ໌ຊ່ອງໃນຜົນການຄົ້ນຫາ + ເຊື່ອງໂພສຊຸມຊົນ + ເຊື່ອງແບນເນີແບບກະທັດຮັດ + ເຊື່ອງແບນເນີແບບກະທັດຮັດ ແລະ ໄລຍະຫ່າງລະຫວ່າງບາງອົງປະກອບ + ບັດທີ່ຂະຫຍາຍໄດ້ ພາຍໃຕ້ວິດີໂອ + ສະແດງທັງໝົດ + ເຊື່ອງສະເພາະການັດສະຫຼຸບ + ເຊື່ອງທັງໝົດ + ເຊື່ອງປຸ່ມໄມໂຄຣໂຟນແບບລອຍ + ເຊື່ອງປຸ່ມໄມໂຄຣໂຟນແບບລອຍ ຢູ່ມຸມຂວາລຸ່ມຂອງໜ້າຈໍຄໍາແນະນໍາການຄົ້ນຫາ + ເຊື່ອງປຸ່ມ \'ຊື້ Premium\' + ເຊື່ອງຊັ້ນປະຫວັດ + ເຊື່ອງຊັ້ນວາງແນວນອນ + ເຊື່ອງຊັ້ນແນວນອນ ເຊັ່ນ ຂ່າວດ່ວນ, ໂພສຊຸມຊົນທີ່ທ່ານອາດຈະມັກ, ເບິ່ງຕໍ່, ສຳຫຼວດຊ່ອງເພີ່ມເຕີມ, ສຳຫຼວດຫົວຂໍ້ເພີ່ມເຕີມ, ສຳລັບທ່ານ, ວິດີໂອຫຼ້າສຸດ, ທີ່ກ່ຽວຂ້ອງທີ່ສຸດ, ໂພສຈາກຊຸມຊົນຂອງຜູ້ສ້າງ, ການຊື້ເຄື່ອງ, ແລະ ເບິ່ງອີກຄັ້ງ + ເຊື່ອງຊັ້ນວາງຮູບພາບ + ເຊື່ອງຊັ້ນຮູບພາບ ໃນຜົນການຄົ້ນຫາ + ເຊື່ອງບັດ \'ເຊີນຜູ້ນື່ນໃຫ້ສົ່ງຂໍ້ຄວາມ\' + ເຊື່ອງບັດເຊີນທີ່ສະແດງໃນສ່ວນຂໍ້ຄວາມຂອງແທັບການແຈ້ງເຕືອນ + ເຊື່ອງປຸ່ມ \'ວິດີໂອຫຼ້າສຸດ\' + ເຊື່ອງແບນເນີ \'ວິດີໂອຫຼ້າສຸດ\' ສີຟ້າທີ່ປາກົດຢູ່ດ້ານເທິງຂອງຟີດໜ້າຫຼັກ + ເຊື່ອງການຖ່າຍທອດສົດ + ເຊື່ອງວິດີໂອທີ່ກຳລັງໄລຟ໌ສົດໃນຟີດໜ້າຫຼັກ ແລະ ຜົນການຄົ້ນຫາ + ເຊື່ອງເພລລິສມິກສ໌ + ເຊື່ອງພາກສ່ວນຮູບເງົາ + ເຊື່ອງຫົວຂໍ້ເມນູການແຈ້ງເຕືອນ + ເຊື່ອງຫົວຂໍ້ເມນູການແຈ້ງເຕືອນເມື່ອຄລິກປຸ່ມລະຄັງ + ເຊື່ອງປຸ່ມ \'ແຈ້ງເຕືອນฉัน\' + ເຊື່ອງປຸ່ມເຕືອນສຳລັບການໄລຟ໌ສະຕຣີມ ແລະ ການເປີດຕົວທີ່ກຳນົດເວລາໄວ້ + ເຊື່ອງ Playables + ເຊື່ອງຟີເຈີທີ່ອະນຸຍາດໃຫ້ທ່ານຫຼິ້ນເກມ + ເຊື່ອງຮູບຕົວຢ່າງຂອງຄຳຄົ້ນຫາ + ເຊື່ອງຕົວຢ່າງຮູບຂະໜາດນ້ອຍໃນລາຍການຄົ້ນຫາແບບອັດຕະໂນມັດ + ເຊື່ອງປຸ່ມ \'ເບິ່ງເພີ່ມເຕີມ\' + ເຊື່ອງປຸ່ມຂະຫຍາຍຢູ່ກ້ອງວິດີໂອໃນຜົນການຄົ້ນຫາ + ເຊື່ອງແຖບຊ່ອງທີ່ຕິດຕາມ + ເຊື່ອງຊື່ຊ່ອງທີ່ຕິດຕາມແລ້ວ + ເຊື່ອງຊື່ຊ່ອງໃນແຖບຊ່ອງທີ່ຕິດຕາມແລ້ວ + ເຊື່ອງການສຳຫຼວດ + ເຊື່ອງການສຳຫຼວດ ແລະ ການເຕືອນກ່ຽວກັບຄວາມຖືກຕ້ອງຂອງການແນະນຳ feed + ເຊື່ອງຊັ້ນບັດ + + ເຊື່ອງແບນເນີ \'ວິດີໂອທີ່ບໍ່ມີໃຫ້ຮັບຊົມຖືກເຊື່ອງໄວ້\' + ເຊື່ອງແບນເນີ \'ວິດີໂອທີ່ບໍ່ມີໃຫ້ຮັບຊົມຖືກເຊື່ອງໄວ້\' ໃນລາຍການວິດີໂອ + ເຊື່ອງເວລາອັບໂຫຼດ + "ຂໍ້ຈຳກັດ: +• ຊັ້ນ Shorts, ໜ້າຊ່ອງ, ແລະ ຜົນການຄົ້ນຫາ ອາດຈະຍັງສະແດງເວລາອັບໂຫຼດ +• ບໍ່ສາມາດໃຊ້ໄດ້ກັບ লেઆເອົາສຳລັບລົດຍົນ" + ເຊື່ອງຮູບຕົວຢ່າງວິດີໂອ + ເຊື່ອງປ້າຍກຳກັບວິດີໂອ + ເຊື່ອງປ້າຍກຳກັບ ເຊັ່ນ \'ພາກສຽງອັດຕະໂນມັດ\' / \'ພາກສຽງ\', \'ໄດ້ຮັບຄ່ານາຍໜ້າ\', \'ຍອດນິຍົມ\', \'ຜູ້ຄົນຍັງໄດ້ເບິ່ງ\', \'ຜະລິດຕະພັນ(s)\' ແລະ \'ທ່ານອາດຈະມັກ\' + ເຊື່ອງຈຳນວນການເບິ່ງ + "ຂໍ້ຈຳກັດ: +• ຊັ້ນ Shorts, ໜ້າຊ່ອງ, ແລະ ຜົນການຄົ້ນຫາ ອາດຈະຍັງສະແດງຈຳນວນການເບິ່ງ +• ບໍ່ສາມາດໃຊ້ໄດ້ກັບ ເລອາເອົາສຳລັບລົດຍົນ" + ເຊື່ອງຜົນການຄົ້ນຫາເວັບ + ເຊື່ອງພາກ \'ຜົນລັດຈາກເວັບ\' ຈາກຜົນການຄົ້ນຫາ + ເຊື່ອງສ່ວນ \'ທ່ານອາດຈະມັກ\' + ເຊື່ອງ YouTube Doodles + ເຊື່ອງແອນິເມຊັນຕາມລະດູການ ແລະ ຕາມເຫດການໃນໂລໂກ້ YouTube + ການສະແດງ YouTube Doodles ອາດຈະຂັດຂວາງການສະແດງຜົນຂອງສ່ວນຫົວທີ່ກຳນົດເອງ + ເຊື່ອງແຖບຊ່ອງ + ເຊື່ອງລາຍນ້ຳຂອງຊ່ອງ + ເຊື່ອງບົດ ແລະ ປຸ່ມ \'ໃນວິດີໂອນີ້\' + ເຊື່ອງກ່ອງລະດົມທຶນ + ເຊື່ອງກ່ອງຂໍ້ຄວາມສຸກເສີນ + ເຊື່ອງແຜງຂໍ້ມູນ + ເຊື່ອງແຜງຂໍ້ມູນບໍລິບົດ, ເຊັ່ນ ຂໍ້ມູນທາງການແພດ + ເຊື່ອງປຸ່ມເຂົ້າຮ່ວມ + ເຊື່ອງປຸ່ມ \'ຫຼິ້ນຊ້ຳການແຊັດສົດ\' + ເຊື່ອງປຸ່ມຫຼິ້ນແຊັດຄືນໃໝ່ໃນໂໝດເຕັມຈໍ + ເຊື່ອງແຜງຂໍ້ມູນທາງການແພດ + ເຊື່ອງຄຳແນະນຳການໃຊ້ທ່າທາງຂອງເຄື່ອງຫຼິ້ນ + ເຊື່ອງຄຳແນະນຳທີ່ສະແດງໃນລະຫວ່າງການເຮັດທ່າທາງຂອງຜູ້ຫຼິ້ນ, ເຊັ່ນ \'ລາກຂຶ້ນເພື່ອຊອກຫາຢ່າງຊັດເຈນ\', \'ປ່ອຍເພື່ອຍົກເລີກ\' ແລະ \'2x\' + ເຊື່ອງ snackbar + ເຊື່ອງຄຳແນະນຳສຳລັບຜູ້ຕິດຕາມ + ເຊື່ອງປຸ່ມ \'ຊິງຄ໌ກັບວິດີໂອ\' + ເຊື່ອງການໄລ່ສີຂອງການແຕະ and ຄ້າງໄວ້ + ເຊື່ອງການໄລ່ສີຂອງຕົວຄວບຄຸມເຄື່ອງຫຼິ້ນ ເມື່ອເລື່ອນນິ້ວມືໃນຂະນະທີ່ກົດຄ້າງໄວ້ເພື່ອເພີ່ມຄວາມໄວ + ເຊື່ອງການຕອບສະໜອງແບບກຳນົດເວລາ + ເຊື່ອງຟີເຈີທີ່ຊ່ວຍໃຫ້ຜູ້ໃຊ້ສາມາດຕອບໂຕ້ດ້ວຍອີໂມຈິໃນສ່ວນຄຳຄິດເຫັນ ແລະ ແຊັດສົດ + ເຊື່ອງເມນູ \'ຄວາມຊ່ວຍເຫຼືອ & ຄຳຄິດເຫັນ\' + ເຊື່ອງເມນູ \'ຄວາມຊ່ວຍເຫຼືອ & ຄຳຄິດເຫັນ\' ເມື່ອໃຊ້ປຸ່ມສາມຈຸດ, ເຊັ່ນ ໃນຜົນການຄົ້ນຫາ + ເຊື່ອງ handle + ເຊື່ອງ handle ຂອງຊ່ອງທ່ານໃນແຖບ \'ທ່ານ\' + ເຊື່ອງຫົວຂໍ້ວິດີໂອ + ເຊື່ອງຫົວຂໍ້ວິດີໂອໃນໂໝດເຕັມຈໍ + ລ້າງຂໍ້ມູນຄຳບັນຍາຍວິດີໂອ + ກຳຈັດວະລີເຊັ່ນ \'#\', \'Fundraiser\', \'product(s)\', ແລະ \'Shop\' ອອກຈາກຄຳບັນຍາຍວິດີໂອ + ເຊື່ອງປຸ່ມ \'ຖາມ Gemini\' + ເຊື່ອງປຸ່ມ \'ຖາມ Gemini\' ຢູ່ລຸ່ມສຸດຂອງແຜງ \'ຖາມກ່ຽວກັບວິດີໂອນີ້\' + ຄຳອະທິບາຍວິດີໂອ + ເລືອກສ່ວນປະກອບທີ່ຈະໃຫ້ສະແດງໃນລາຍລະອຽດຂອງວິດີໂອ + ເຊື່ອງສ່ວນ \'ບົດສະຫຼຸບວິດີໂອທີ່ສ້າງໂດຍ AI\' + ເຊື່ອງສ່ວນ Ask + ເຊື່ອງສ່ວນ Attributes + ເຊື່ອງສ່ວນ ສະຖານທີ່ທີ່ໂດດເດັ່ນ, ເກມ, ດົນຕີ, ແລະ ບຸກຄົນທີ່ຖືກກ່າວເຖິງ + ເຊື່ອງສ່ວນລິງ unforeseen ຂອງຊ່ອງ + ເຊື່ອງສ່ວນ Chapters + ເຊື່ອງສ່ວນ Corrections + ເຊື່ອງສ່ວນ \'ຄວາມຄືບໜ້າຂອງຫຼັກສູດ\' + ເຊື່ອງພາກສ່ວນສຳຫຼວດ + ເຊື່ອງພາກສ່ວນສຳຫຼວດຫຼັກສູດນີ້ ແລະ ສຳຫຼວດພອດແຄສ + ເຊື່ອງພາກ \'ສຳຫຼວດຫຼັກສູດນີ້\' + ເຊື່ອງພາກ \'ສຳຫຼວດພອດແຄສ\' + ເຊື່ອງພາກສ່ວນຊ່ອງທີ່ໂດດເດັ່ນ + ເຊື່ອງພາກລິງກ໌ທີ່ໂດດເດັ່ນ + ເຊື່ອງສ່ວນລາຍການຫຼິ້ນທີ່ໂດດເດັ່ນ + ເຊື່ອງພາກວິດີໂອທີ່ໂດດເດັ່ນ + ເຊື່ອງສ່ວນ Hashtag + ເຊື່ອງພາກ \'ວິທີສ້າງສິ່ງນີ້\' + ເຊື່ອງຄະແນນ Hype + ເຊື່ອງພາກບັດຂໍ້ມູນ + ເຊື່ອງພາກ \'ແນວຄິດຫຼັກ\' + ເຊື່ອງພາກແບບທົດສອບ + ເຊື່ອງພາກ \'ຄົ້ນຫາພາຍໃນວິດີໂອນີ້\' + ເຊື່ອງປຸ່ມຕິດຕາມ + ເຊື່ອງພາກບົດຖອດຄວາມ + ເຊື່ອງພາກ \'ລາຍລະອຽດວິດີໂອ\' + ແຖບກັ່ນຕອງ + ເລືອກບ່ອນທີ່ຈະເຊື່ອງແຖບຕົວຕອງແບບເລື່ອນ + ເຊື່ອງໃນໜ້າຊ່ອງ + ເຊື່ອງໃນຄຳຄິດເຫັນ + ເຊື່ອງໃນຟີດ + ເຊື່ອງໃນວິດີໂອທີ່ກ່ຽວຂ້ອງ + ເຊື່ອງໃນຜົນການຄົ້ນຫາ + ເຊື່ອງໃນປະຫວັດການເບິ່ງ + ໜ້າຊ່ອງ + ເລືອກສ່ວນປະກອບທີ່ຈະໃຫ້ປາກົດໃນໜ້າຊ່ອງ + ເປີດໃຊ້ຕົວຕອງແຖບຊ່ອງ + ຕົວຕອງແຖບຊ່ອງ + ປ້ອນຊື່ແທັບຂອງຊ່ອງເພື່ອຕອງ, ໂດຍແຍກດ້ວຍການຂຶ້ນແຖວໃໝ່ + ເຊື່ອງປຸ່ມຊຸມຊົນ + ເຊື່ອງປຸ່ມເຂົ້າຮ່ວມ + ເຊື່ອງການສະແດງຕົວຢ່າງລິງກ໌ + ເຊື່ອງລິງກ໌ເວັບທີ່ສາມາດຄລິກໄດ້ໃນສ່ວນຫົວຂອງຊ່ອງ + ເຊື່ອງປ້າຍ \'ສະເພາະສະມາຊິກ\' + ເຊື່ອງປ້າຍ \'ສະເພາະສະມາຊິກ\' ໃນແຖບກັ່ນຕອງຂອງຊ່ອງ + ເຊື່ອງຊັ້ນສະມາຊິກ + ເຊື່ອງລາຍຊື່ສະມາຊິກຂອງຊ່ອງ + ເຊື່ອງຊັ້ນໂພສ + ເຊື່ອງປຸ່ມຮ້ານຄ້າ + ເຊື່ອງປຸ່ມຕິດຕາມ + ເມນູສຳຫຼວດ + ເລືອກສ່ວນປະກອບທີ່ຈະໃຫ້ສະແດງໃນເມນູສຳຫຼວດ + ເຊື່ອງເມນູຫຼັກສູດ + ເຊື່ອງປຸ່ມສຳຫຼວດ + ເຊື່ອງເມນູ \'ແຟຊັນ & ຄວາມງາມ\' + ເຊື່ອງເມນູເກມ + ເຊື່ອງເມນູ Hype + ເຊື່ອງເມນູການຮຽນຮູ້ + ເຊື່ອງເມນູໄລຟ໌ + ເຊື່ອງເມນູການເປັນສະມາຊິກ + ເຊື່ອງເມນູ \'ຮູບເງົາ & ທີວີ\' + ເຊື່ອງເມນູເພງ + ເຊື່ອງເມນູຂ່າວ + ເຊື່ອງເມນູ Playables + ເຊື່ອງເມນູ Podcasts + ເຊື່ອງເມນູ Shopping + ເຊື່ອງເມນູ Sports + ເຊື່ອງເມນູ YouTube Premium + ເຊື່ອງເມນູ YouTube Studio + ເຊື່ອງເມນູ YouTube Music + ເຊື່ອງເມນູ YouTube Kids + ເຊື່ອງເມນູ YouTube Create + ເຊື່ອງເມນູ \'YouTube ເຮັດວຽກແນວໃດ\' + ເຊື່ອງສ່ວນທ້າຍຂອງກົດໝາຍ ແລະ ຄວາມເປັນສ່ວນຕົວ + ຄວາມຄິດເຫັນ + ເລືອກສ່ວນປະກອບທີ່ຈະໃຫ້ປາກົດໃນສ່ວນຄໍາຄິດເຫັນ + ເປີດໃຊ້ງານຕົວຕອງ carousel ຂອງຄຳຄິດເຫັນ + "• ກະລຸນາປ້ອນຊື່ທີ່ຖືກຕ້ອງຂອງລາຍການໃນ carousel (ຕົວຢ່າງ: Ask, Hype this video, Listen on YouTube Music) +• ການເຊື່ອງລາຍການໃນ carousel ອາດສົ່ງຜົນກະທົບທີ່ບໍ່ໄດ້ຄາດຄິດ +• ຖ້າລາຍການໃນ carousel ທັງໝົດຖືກເຊື່ອງ, ສ່ວນຄຳຄິດເຫັນທັງໝົດຈະຖືກເຊື່ອງໄປນຳ" + ຕົວຕອງ carousel ຂອງຄຳຄິດເຫັນ + ປ້ອນຊື່ carousel ຂອງຄໍາຄິດເຫັນເພື່ອຕອງ, ໂດຍແຍກດ້ວຍການຂຶ້ນແຖວໃໝ່ + ເຊື່ອງບົດສະຫຼຸບການແຊັດດ້ວຍ AI + ເຊື່ອງຄຳແນະນຳຂອງຊ່ອງ + ເຊື່ອງຂໍ້ຄວາມບໍລິບົດ + ເຊື່ອງຫົວຂໍ້ \"ຄວາມຄິດເຫັນໂດຍສະມາຊິກ\" + ເຊື່ອງພາກສ່ວນຄຳຄິດເຫັນ + ເຊື່ອງພາກສ່ວນຄຳຄິດເຫັນໃນ feed ໜ້າຫຼັກ + ເຊື່ອງຄຳແນະນຳຊຸມຊົນ + ເຊື່ອງປຸ່ມ \'ສ້າງ Short\' + ເຊື່ອງປຸ່ມບໍ່ມັກ + ເຊື່ອງປຸ່ມ Emoji + ເຊື່ອງຕົວເລືອກແຖບການກັ່ນกรອງ + ເຊື່ອງທຸກຕົວເລືອກແຖບການກັ່ນกรອງ ຍົກເວັ້ນ \'ຍອດນິຍົມ\' ແລະ \'ໃໝ່ສຸດ\' + ເຊື່ອງແອນິເມຊັນຂອງຂວັນ ແລະ ການັດ + ເຊື່ອງປຸ່ມຂອງຂວັນ + ເຊື່ອງປຸ່ມຂໍ້ມູນ + ເຊື່ອງແຖບຜູ້ບໍລິຈາກໃນແຊັດສົດ + ເຊື່ອງປຸ່ມເລື່ອນລົງລຸ່ມສຸດຂອງການແຊັດສົດ + ເຊື່ອງຄຳແນະນຳຂອງແຊັດສົດ + ເຊື່ອງປັອບອັບຄຳແນະນຳທີ່ສະແດງໃກ້ກັບແຖບປ້ອນຂໍ້ມູນແຊັດ, ເຊັ່ນ ແຈ້ງເຕືອນການແຊັດສະເພາະຜູ້ສະໝັກຮັບຂໍ້ມູນ + ເຊື່ອງປຸ່ມເມນູ + ເຊື່ອງປຸ່ມເມນູ 3 ຈຸດ ຂອງແຕ່ລະຄຳເຫັນ + ເຊື່ອງການເບິ່ງຄຳຄິດເຫັນລ່ວງໜ້າ + ເຊື່ອງຕົວຢ່າງຄໍາຄິດເຫັນພາຍໃຕ້ວິດີໂອກ່ອນທີ່ຈະເປີດສ່ວນຄໍາຄິດເຫັນທັງໝົດ + ຄວາມຄິດເຫັນຖືກເຊື່ອງໂດຍ Morphe… + ເປີດໃຊ້ປຸ່ມຄຳຄິດເຫັນແບບມິນິມອລ + ເຊື່ອງບັດທີ່ເລື່ອນໄດ້ ແລະ ຕົວເລືອກໜ້າຂອງມັນ, ທີ່ສະແດງຢູ່ໃນປຸ່ມຄຳຄິດເຫັນ + ເຊື່ອງປຸ່ມຂອບໃຈ + ເຊື່ອງປຸ່ມປະທັບເວລາ + ເຊື່ອງປຸ່ມ \'ແຟນໂຕຍິ່ງ\' + ເຊື່ອງປຸ່ມແປພາສາ + ເຊື່ອງຂໍ້ຄວາມ \'ຍັງບໍ່ມີຄຳຄິດເຫັນ\' + ເຮັດຄວາມສະອາດລິ້ງການຄົ້ນຫາທີ່ເນັ້ນ + ເຮັດຄວາມສະອາດລິ້ງການຄົ້ນຫາທີ່ເນັ້ນ, ໃຫ້ເຫຼືອພຽງແຕ່ຂໍ້ຄວາມທຳມະດາຕົ້ນສະບັບ + ຕົວຕອງຄຳສັບຫຼັກ + ເລືອກຄໍາ ຫຼື ວະລີສະເພາະເພື່ອເຊື່ອງວິດີໂອໃນ feed ແລະ ການຄົ້ນຫາ + ເຊື່ອງຄຳຄິດເຫັນຕາມຄຳຄົ້ນຫາ + ເຊື່ອງວິດີໂອໃນໜ້າຫຼັກດ້ວຍຄຳຄົ້ນຫາ + ເຊື່ອງຜົນການຄົ້ນຫາດ້ວຍຄຳຄົ້ນຫາ + ເຊື່ອງວິດີໂອໃນການຕິດຕາມດ້ວຍຄຳຄົ້ນຫາ + ຄຳຄົ້ນຫາທີ່ຈະເຊື່ອງ + "ປ້ອນຄຳສຳຄັນ ແລະ ວະລີທີ່ຕ້ອງການເຊື່ອງ, ໂດຍແຍກດ້ວຍການຂຶ້ນແຖວໃໝ່ + +ຄຳສຳຄັນສາມາດເປັນຂໍ້ຄວາມໃດກໍ່ໄດ້ທີ່ສະແດງໃນຫົວຂໍ້ວິດີໂອ ຫຼື ຊື່ຊ່ອງ + +ຄຳທີ່ມີຕົວອັກສອນໃຫຍ່ຢູ່ທາງກາງຕ້ອງຂຽນໃຫ້ຖືກຕ້ອງທັງໝົດ (ຕົວຢ່າງ: iPhone, TikTok, LeBlanc)" + "ໜ້າຫຼັກ / ການຕິດຕາມ / ການຄົ້ນຫາ / ຄຳຄິດເຫັນຖືກກັ່ນຕອງເພື່ອເຊື່ອງເນື້ອຫາທີ່ກົງກັບວະລີຄີເວີດ + +ຂໍ້ຈຳກັດ: +• Shorts ບໍ່ສາມາດເຊື່ອງໄດ້ດ້ວຍຊື່ຊ່ອງ +• ບາງສ່ວນປະກອບຂອງ UI ອາດຈະບໍ່ຖືກເຊື່ອງ +• ການຄົ້ນຫາຄີເວີດອາດຈະບໍ່ພົບຜົນລັດ" + ຈັບຄູ່ຄຳສັບທັງໝົດ + ການໃສ່ເຄື່ອງໝາຍຄຳເວົ້າຄູ່ລ້ອມຮອບຄຳສັບ/ວະລີ ຈະປ້ອງກັນການຈັບຄູ່ບາງສ່ວນຂອງຊື່ວິດີໂອ ແລະ ຊື່ຊ່ອງ<br><br>ຕົວຢ່າງ,<br><b>\"ai\"</b> ຈະເຊື່ອງວິດີໂອ: <b>How does AI work?</b><br>ແຕ່ຈະບໍ່ເຊື່ອງ: <b>What does fair use mean?</b> + ບໍ່ສາມາດໃຊ້ຄຳສັບ: %s + ກະລຸນາໃສ່ເຄື່ອງໝາຍຄຳເວົ້າເພື່ອໃຊ້ຄຳສັບ: %s + ຄຳສັບມີການປະກາດທີ່ຂັດແຍ່ງກັນ: %s + ຄຳສັບສັ້ນເກີນໄປ ແລະ ຕ້ອງໃສ່ເຄື່ອງໝາຍຄຳເວົ້າ: %s + ຄຳຄົ້ນຫາຈະເຊື່ອງທຸກວິດີໂອ: %s + "• ໜ້າຫຼັກ: %s +• ການຕິດຕາມ: %s +• ຄົ້ນຫາ: %s +• ຄໍາຄິດເຫັນ: %s +• ທັງໝົດ: %s" + ສະຖິຕິ + ວິດີໂອທີ່ຖືກເຊື່ອງ, 24 ຊົ່ວໂມງທີ່ຜ່ານມາ + ວິດີໂອທີ່ຖືກເຊື່ອງ, ທັງໝົດ + ຣີເຊັດຕົວນັບ 24 ຊົ່ວໂມງຜ່ານມາບໍ? + ຣີເຊັດຕົວນັບທັງໝົດບໍ? + ຕົວຕອງຊ່ອງ AI + ເຊື່ອງວິດີໂອຈາກຊ່ອງທີ່ຖືກໝາຍວ່າເປັນເນື້ອຫາທີ່ສ້າງໂດຍ AI ໂດຍລາຍການຊຸມຊົນ AiSList + ລາຍການບລັອກ: ຄວາມໝັ້ນໃຈສູງ + ລາຍການເຕືອນ: ຄວາມໝັ້ນໃຈປານກາງ + ເຊື່ອງວິດີໂອໃນໜ້າຫຼັກ + ເຊື່ອງວິດີໂອໃນການຄົ້ນຫາ + "• ໜ້າຫຼັກ: %s +• ຄົ້ນຫາ: %s +• ທັງໝົດ: %s" + ຊ່ອງໃນລາຍຊື່ບລັອກ + ຊ່ອງໃນລາຍຊື່ເຕືອນ + ເປີດໃຊ້ຕົວຕອງເພື່ອໂຫລດຊ່ອງ + ກຳລັງໂຫລດຊ່ອງ… + "ໜ້າຫຼັກ ແລະ ຜົນການຄົ້ນຫາຖືກກັ່ນຕອງເພື່ອເຊື່ອງວິດີໂອຈາກຊ່ອງທີ່ຖືກໝາຍໂດຍ AiSList + +ຂໍ້ຈຳກັດ: +• AI Shorts ທີ່ຖືກກັ່ນຕອງອາດຈະສະແດງເປັນພື້ນທີ່ວ່າງ +• ວິດີໂອ AI ບາງລາຍການອາດຈະຍັງສະແດງຢູ່" + "AiSList ແມ່ນລາຍຊື່ open source ຂອງຊ່ອງ YouTube ທີ່ເຜີຍແຜ່ເນື້ອຫາທີ່ສ້າງໂດຍ AI, ເຊິ່ງຖືກຈັດການໂດຍອາສາມັກຄີ. ຊ່ອງຕ່າງໆຖືກລາຍງານໂດຍຜູ້ໃຊ້ ແລະ ຖືກກວດສອບກ່ອນທີ່ຈະຖືກເພີ່ມເຂົ້າໄປ + +ລາຍຊື່ແມ່ນຖືກດາວໂຫຼດຈາກ GitHub ແລະ ວິດີໂອຈະຖືກກັ່ນຕອງໃນອຸປະກອນຂອງທ່ານ" + ລາຍງານຊ່ອງ AI + ເພີ່ມລາຍການລາຍງານໃນເມນູວິດີໂອ + ເພີ່ມລາຍການໃນເມນູວິດີໂອ ເພື່ອລາຍງານຊ່ອງຂອງວິດີໂອໄປຍັງ AiSList + ຊື່ຜູ້ສົ່ງລາຍງານ + ຊື່ທີ່ສະແດງຂ້າງໆການລາຍງານຂອງທ່ານໃນຕາຕະລາງຈັດອັນດັບ AiSList ສາທາລະນະ. ການລາຍງານຈະບໍ່ສາມາດນຳໃຊ້ໄດ້ໃນຂະນະທີ່ຊ່ອງນີ້ວ່າງຢູ່ + ລາຍງານຊ່ອງ AI + "ສົ່ງ %s ໄປຍັງ AiSList ໂດຍໃຊ້ວິດີໂອນີ້ເປັນຫຼັກຖານ? + +ລາຍງານສະເພາະຊ່ອງທີ່: +• ໃຊ້ສຽງ, ຮູບພາບ, ບົດ ຫຼື ດົນຕີ AI ເປັນເນື້ອຫາຫຼັກ +• ຜະລິດເນື້ອຫາຈຳນວນຫຼາຍດ້ວຍເຄື່ອງມື AI ແລະ ມີການເຮັດວຽກຂອງມະນຸດໜ້ອຍ + +ຢ່າລາຍງານຊ່ອງທີ່ພຽງແຕ່ສົນທະນາກ່ຽວກັບ AI ຫຼື ໃຊ້ AI ໃນບາງສ່ວນຂອງວິດີໂອຂອງພວກເຂົາ. + +ການລາຍງານແມ່ນເປັນສາທາລະນະ ແລະ ຈະຖືກກວດສອບກ່ອນທີ່ຊ່ອງຈະຖືກເພີ່ມເຂົ້າໃນລາຍຊື່. ການລຶບອອກສາມາດເຮັດໄດ້ໂດຍຜ່ານການຮ້ອງຂໍໃນ GitHub ເທົ່ານັ້ນ" + ຊ່ອງນີ້ຢູ່ໃນລາຍຊື່ບລັອກ AiSList ແລ້ວ + ຊ່ອງນີ້ຢູ່ໃນລາຍຊື່ເຕືອນຂອງ AiSList ແລ້ວ + ລາຍງານ + ກະລຸນາຕັ້ງຊື່ຜູ້ສົ່ງໃນການຕັ້ງຄ່າຕົວຕອງຊ່ອງ AI ເພື່ອລາຍງານຊ່ອງ + ກຳລັງຄົ້ນຫາຊ່ອງ... + ບໍ່ສາມາດຊອກຫາຊ່ອງຂອງວິດີໂອນີ້ໄດ້ + ລາຍງານຊ່ອງແລ້ວ ແລະ ກຳລັງລໍຖ້າການກວດສອບ + AiSList ບໍ່ສາມາດບັນທຶກການລາຍງານໄດ້, ກະລຸນາລອງໃໝ່ໃນພາຍຫຼັງ + ມີການສົ່ງລາຍງານຫຼາຍເກີນໄປ, ກະລຸນາລອງໃໝ່ໃນພາຍຫຼັງ + AiSList ປະຕິເສດການລາຍງານ + ການລາຍງານລົ້ມເຫຼວ, ກະລຸນາກວດສອບການເຊື່ອມຕໍ່ຂອງທ່ານ + ລາຍຊື່ຊ່ອງໂຄສະນາທີ່ຍອມຮັບ + ວິດີໂອຈາກຊ່ອງທີ່ຢູ່ໃນລາຍການອະນຸຍາດຈະຫຼິ້ນພ້ອມກັບໂຄສະນາ ເພື່ອໃຫ້ຜູ້ສ້າງໄດ້ຮັບເງິນ. ໂຄສະນາຈະຍັງຄົງຖືກບລັອກໃນທຸກຊ່ອງອື່ນໆ + ເພີ່ມຊ່ອງເຂົ້າໃນລາຍການອະນຸຍາດໂຄສະນາ + ລຶບຊ່ອງອອກຈາກລາຍການອະນຸຍາດໂຄສະນາ + ເຊື່ອງຊັ້ນວາງຮ້ານຄ້າຂອງຜູ້ສ້າງ + ເຊື່ອງປ້າຍໂຄສະນາຮ້ານຄ້າໃນໜ້າຈົບ + ເຊື່ອງໂຄສະນາທົ່ວໄປ + ເຊື່ອງປ້າຍໂຄສະນາສິນຄ້າ + ເຊື່ອງປ້າຍການສົ່ງເສີມທີ່ຊຳລະເງິນ + ເຊື່ອງໂຄສະນາປັອບອັບຂອງເຄື່ອງຫຼິ້ນ + ເຊື່ອງບັດໂຄສະນາທີ່ສະປອນເຊີຕົນເອງ + ເຊື່ອງລິ້ງຊື້ເຄື່ອງ + ເຊື່ອງໂຄສະນາວິດີໂອ + ເຊື່ອງໂປຣໂມຊັນ YouTube Premium + ກຳລັງໂຫຼດ… + ຊ່ອງປັດຈຸບັນ: + ເພີ່ມເຂົ້າໃນລາຍການອະນຸຍາດ + ລຶບ + ຊ່ອງທີ່ຢູ່ໃນລາຍການອະນຸຍາດ + ບໍ່ມີຊ່ອງໃນລາຍການອະນຸຍາດ + ເພີ່ມຊ່ອງເຂົ້າໃນລາຍການອະນຸຍາດແລ້ວ + ລຶບຊ່ອງອອກຈາກລາຍການອະນຸຍາດແລ້ວ + ຄົ້ນຫາພາຍໃນຊ່ອງ + ເພີ່ມປຸ່ມຄົ້ນຫາທີສອງໃນໜ້າຊ່ອງ ເພື່ອຄົ້ນຫາສະເພາະຊ່ອງນັ້ນ + ຄົ້ນຫາຊ່ອງ + ຜົນການຄົ້ນຫາຊ່ອງ + ບໍ່ພົບຂໍ້ມູນໃນຊ່ອງນີ້ + ບໍ່ສາມາດຄົ້ນຫາໃນຊ່ອງນີ້ໄດ້ + ວິດີໂອ + ໂພສ + ບໍ່ມີໂພສໃນຊ່ອງນີ້ທີ່ກົງກັນ + ຄວາມກ່ຽວຂ້ອງ + ມີຜູ້ຊົມຫຼາຍທີ່ສຸດ + ມີຜູ້ຊົມໜ້ອຍທີ່ສຸດ + ໃໝ່ສຸດ + ເກົ່າສຸດ + ສັ້ນທີ່ສຸດ + ຍາວທີ່ສຸດ + ຄັດລອກລິ້ງໄປຍັງຄລິບບອດແລ້ວ + ຄັດລອກລິ້ງພ້ອມເວລາທີ່ກຳນົດໄວ້ໄປຍັງຄລິບບອດແລ້ວ + ສະແດງປຸ່ມຄັດລອກລິ້ງວິດີໂອ + ແຕະເພື່ອຄັດລອກລິ້ງວິດີໂອ, ແຕະຄ້າງໄວ້ເພື່ອຄັດລອກພ້ອມເວລາທີ່ກຳນົດໄວ້ + ຄັດລອກເວລາທີ່ລະບຸໂດຍຄ່າເລີ່ມຕົ້ນ + ປ່ຽນການເຮັດວຽກຂອງການແຕະ ແລະ ການແຕະຄ້າງໄວ້ຂອງປຸ່ມ \'ຄັດລອກລິ້ງວິດີໂອ\' + ຄັດລອກຫົວຂໍ້ວິດີໂອ + ແຕະ ແລະ ຄ້າງໄວ້ທີ່ຫົວຂໍ້ຂອງວິດີໂອເພື່ອຄັດລອກ + ຄັດລອກຫົວຂໍ້ວິດີໂອລົງໃນຄລິບບອດແລ້ວ + ຄັດລອກຄຳຄິດເຫັນ + ແຕະ ແລະ ຄ້າງໄວ້ທີ່ຄຳຄິດເຫັນເພື່ອຄັດລອກ + ຄັດລອກຄຳຄິດເຫັນລົງໃນຄລິບບອດແລ້ວ + ແຕະ and ກົດຄ້າງໄວ້ເພື່ອເປີດເມນູວິດີໂອ + ການແຕະ ແລະ ກົດຄ້າງໄວ້ຢູ່ບ່ອນໃດກໍໄດ້ໃນວິດີໂອຂອງຟີດ ຈະເປີດເມນູຂອງວິດີໂອນັ້ນ + ສະແດງປຸ່ມ ເບິ່ງພາຍຫຼັງ ໃນຮູບຕົວຢ່າງ + ປຸ່ມ ເບິ່ງພາຍຫຼັງ ຈະຖືກສະແດງໃນຮູບຕົວຢ່າງຂອງວິດີໂອໃນຟີດ. ການແຕະ ແລະ ກົດຄ້າງໄວ້ທີ່ປຸ່ມນີ້ ຈະເປີດ ບັນທຶກລົງໃນລາຍການຫຼິ້ນ + ດາວໂຫຼດຮູບພາບໂພສ + ປຸ່ມດາວໂຫຼດຈະສະແດງຂ້າງປຸ່ມແຊຣ໌ຂອງໂພສທີ່ມີຮູບພາບ ຫຼື GIFs, ແລະ ລາຍການດາວໂຫຼດຈະຖືກເພີ່ມເຂົ້າໃນເມນູຂອງພວກມັນ + ດາວໂຫຼດຮູບພາບ + ດາວໂຫຼດຮູບພາບ + ກຳລັງດາວໂຫຼດ %s ຮູບພາບ… + ກຳລັງດາວໂຫຼດຮູບພາບ… + ບັນທຶກຮູບພາບ %1$s ໃສ່ %2$s ແລ້ວ + ບັນທຶກຮູບພາບໃສ່ %s ແລ້ວ + ບໍ່ສາມາດດາວໂຫຼດຮູບພາບໄດ້ + ບໍ່ພົບຮູບພາບໃນໂພສນີ້ + ເພີ່ມລາຍການ Ask ໄປยังເມນູ flyout + ເພີ່ມລາຍການສະຫຼຸບວິດີໂອໄປยังເມນູ flyout ຂອງ feed + "ໃນເບື້ອງຕົ້ນ, Ask ຕ້ອງໃຊ້ບັນຊີທີ່ລົງຊື່ເຂົ້າໃຊ້ໃນປະເທດທີ່ມີ Ask ໃຫ້ບໍລິການ. + +ຫຼັງຈາກເປີດວິດີໂອທີ່ມີປຸ່ມ Ask ຄັ້ງໜຶ່ງແລ້ວ, ຟີເຈີນີ້ຈະໃຊ້ໄດ້ກັບທຸກວິດີໂອ ເຖິງແມ່ນວ່າປະເທດປັດຈຸບັນຈະບໍ່ມີຟີເຈີ Ask ກໍຕາມ." + ສະຫຼຸບເນື້ອໃນວິດີໂອ + ບໍ່ສາມາດເປີດ Ask ສຳລັບວິດີໂອນີ້ໄດ້ + Ask ບໍ່ມີໃຫ້ບໍລິການສຳລັບບັນຊີ ຫຼື ປະເທດປັດຈຸບັນຂອງທ່ານ + ສະແດງຈໍານວນການເບິ່ງໃນຮູບຕົວຢ່າງ + ຈໍານວນການເບິ່ງຂອງວິດີໂອໃນ feed ຈະຖືກສະແດງຢູ່ມຸມລຸ່ມຊ້າຍຂອງຮູບຕົວຢ່າງ ແທນທີ່ຈະຢູ່ກ້ອງຊື່ເລື່ອງ. ບາງວິດີໂອ, ເຊັ່ນໃນ shelves, ອາດຈະບໍ່ສະແດງຈໍານວນການເບິ່ງ + ສະແດງຈຳນວນການຖືກໃຈຂອງຮູບຕົວຢ່າງໃນຟີດ + ສະແດງເປີເຊັນການຖືກໃຈຂອງຮູບຕົວຢ່າງໃນຟີດ + ຄົ້ນຫາຄຳຄິດເຫັນ + ແຕະປຸ່ມຄົ້ນຫາໃນຄຳຄິດເຫັນ ເພື່ອສະແດງສະເພາະຄຳຄິດເຫັນທີ່ໂຫຼດແລ້ວ ເຊິ່ງມີຄຳສັບທັງໝົດທີ່ຄົ້ນຫາ. ໃສ່ຄຳສັບໃນເຄື່ອງໝາຍຄຳເວົ້າ ເພື່ອຄົ້ນຫາວະລີ + ຄົ້ນຫາຄຳຄິດເຫັນ + ຄຳສັບທີ່ຕ້ອງການຄົ້ນຫາ + ຄົ້ນຫາ + ລ້າງຂໍ້ມູນ + ປ່ຽນລຳດັບການຈັດຮຽງ ຫຼື ເປີດຄຳຄິດເຫັນອີກຄັ້ງເພື່ອຄົ້ນຫາ + ລ້າງການຄົ້ນຫາແລ້ວ. ປ່ຽນລຳດັບການຈັດຮຽງ ຫຼື ເປີດຄຳຄິດເຫັນອີກຄັ້ງເພື່ອສະແດງທຸກຄຳຄິດເຫັນ + ປິດປັອບອັບ \'ລົງຊື່ເຂົ້າໃຊ້ໃນ TV\' + ປິດປັອບອັບ \'ເຊື່ອມຕໍ່ອຸປະກອນຂອງທ່ານ\' + ປິດການຂ້າມບົດດ້ວຍການແຕະສອງຄັ້ງ + ປິດການນຳໃຊ້ທ່າທາງການແຕະສອງຄັ້ງດ້ວຍສອງນິ້ວເພື່ອຂ້າມບົດ, ເຊິ່ງອາດຈະຖືກກະຕຸ້ນໂດຍບັງເອີນດ້ວຍນິ້ວດຽວ + ດາວໂຫຼດວິດີໂອ, ພອດແຄສ ຫຼື ເພງ ດ້ວຍແອັບດາວໂຫຼດຂອງບຸກຄົນທີສາມ + ສະແດງປຸ່ມດາວໂຫຼດໃນຊັ້ນທັບຂອງເຄື່ອງຫຼິ້ນ + ສະແດງປຸ່ມໃນເຄື່ອງຫຼິ້ນວິດີໂອ ທີ່ຈະເປີດໂປຣແກຣມດາວໂຫຼດພາຍນອກຂອງທ່ານ + ດາວໂຫຼດໄວເມື່ອກົດ + ແຈ້ງເຕືອນໃຫ້ດາວໂຫຼດໃນພື້ນຫຼັງ. ກົດຄ້າງໄວ້ເພື່ອເປີດຕົວດາວໂຫຼດ. ໃຊ້ໄດ້ກັບ YTDLnis ແລະ YTindir + ໝາຍການດາວໂຫຼດວ່າເບິ່ງແລ້ວ + ວິດີໂອທີ່ດາວໂຫຼດດ້ວຍຕົວດາວໂຫຼດພາຍນອກ ຈະຖືກເພີ່ມເຂົ້າໃນປະຫວັດການເບິ່ງ YouTube ຂອງທ່ານ + Full HD (1080p) + ປານກາງ (192 kbps) + ດາວໂຫຼດໄວ + ວິດີໂອ + ສຽງ + ທຸກຕົວເລືອກ + ສະແດງການດາວໂຫລດໃນເມນູ Shorts + ເພີ່ມລາຍການເມນູ flyout ຂອງຕົວດາວໂຫລດພາຍນອກໃນເຄື່ອງຫຼິ້ນ Shorts + ດາວໂຫລດ + ການແປແບບພາກສຽງ + ອ່ານຄຳບັນຍາຍວິດີໂອອອກສຽງໂດຍໃຊ້ລະບົບປ່ຽນຂໍ້ຄວາມເປັນສຽງໃນເຄື່ອງ, ເຊິ່ງຈະເຮັດວຽກສອດຄ່ອງກັບການຫຼິ້ນວິດີໂອ + ສະແດງປຸ່ມ overlay ໃນເຄື່ອງຫຼິ້ນວິດີໂອ. ແຕະເພື່ອປ່ຽນການແປພາສາ, ແຕະຄ້າງໄວ້ສຳລັບການຕັ້ງຄ່າແບບດ່ວນ + ພາສາ + ຜູ້ໃຫ້ບໍລິການແປພາສາ + ລະດັບສຽງຂອງສຽງຕົ້ນສະບັບ + ລະດັບສຽງຂອງສຽງ YouTube ຕົ້ນສະບັບໃນຂະນະທີ່ສຽງແປກຳລັງເວົ້າ + ລະດັບສຽງຂອງການແປ + ລະດັບສຽງຂອງສຽງພາກທີ່ແປ + ຄວາມໄວໃນການເວົ້າສູງສຸດ + ຕົວຄູນຄວາມเร็วສູງສຸດ ເມື່ອປັບສຽງເວົ້າໃຫ້ພໍດີກັບຊ່ອງຄຳບັນຍາຍ + "API ການແປພາສາຟຣີ, ບໍ່ຕ້ອງໃຊ້ API key. ສູງສຸດ 5,000 ຕົວອັກສອນຕໍ່ມື້ ສຳລັບລະດັບຟຣີ, ຫຼື ສູງສຸດ 50,000 ຕົວອັກສອນຕໍ່ມື້ ຫາກມີທີ່ຢູ່ອີເມວ. + +ແຕະບ່ອນນີ້ເພື່ອເປີດ MyMemory.translated.net" + ອີເມວ MyMemory + ທາງເລືອກ. ປ້ອນທີ່ຢູ່ອີເມວ MyMemory ຂອງທ່ານ ເພື່ອເພີ່ມຂີດຈຳກັດລາຍວັນຈາກ 5,000 ເປັນ 50,000 ຕົວອັກສອນ. ປ່ອຍວ່າງໄວ້ເພື່ອໃຊ້ຂີດຈຳກັດແບບບໍ່ລະບຸຕົວຕົນ + "ການແປພາສາດ້ວຍ AI ແບບເສຍເງິນຜ່ານ OpenRouter. ເຕີມເງິນຄັ້ງດຽວ ແລະ ແປວິດີໂອໄດ້ນັບພັນ. Mistral Nemo ແມ່ນຕົວເລືອກທີ່ຖືກທີ່ສຸດ, Gemma 4 ໃຫ້ຄຸນນະພາບສູງທີ່ສຸດ. + +ແຕະບ່ອນນີ້ເພື່ອເປີດ OpenRouter.ai" + ຄີ API ຂອງ OpenRouter + OpenRouter API key ຂອງທ່ານ. ສ້າງ key ທີ່ OpenRouter.ai > Workspace > API Keys + ໂມເດລ OpenRouter + ກຳນົດເອງ + OpenRouter ບໍ່ທັນໄດ້ຕັ້ງຄ່າ + ກະລຸນາປ້ອນ OpenRouter API key ຂອງທ່ານໃນການຕັ້ງຄ່າ ກ່ອນທີ່ຈະເລືອກຜູ້ໃຫ້ບໍລິການນີ້ + ຟຣີ + %s ຕໍ່ 100 ຊົ່ວໂມງຂອງວິດີໂອ + ສຽງ + ລະບົບ + ຜູ້ຊາຍຫຼາຍພາສາ + ຜູ້ຍິງຫຼາຍພາສາ + ຜູ້ຊາຍ + ຜູ້ຍິງ + ໄດ້ເວລາປ່ຽນຮູບຮ່າງແລ້ວ! + ບໍ່ມີການແປ + "ບໍລິການແປພາສາສົ່ງຂໍ້ຜິດພາດ HTTP %s ກັບຄືນມາ. ທີ່ຢູ່ IP ຂອງທ່ານອາດຈະຖືກຈຳກັດການນຳໃຊ້ເປັນເວລາສູງສຸດ 24 ຊົ່ວໂມງ. + +ເພື່ອສືບຕໍ່ນຳໃຊ້ການແປ, ໃຫ້ປ່ຽນໄປໃຊ້ຜູ້ໃຫ້ບໍລິການແປພາສາອື່ນໃນການຕັ້ງຄ່າ ຫຼື ໃຊ້ VPN" + ບໍ່ໃຫ້ສະແດງອີກ + "ການຕັ້ງຄ່າຂອງທ່ານມີ OpenRouter API key. + +ທ່ານບໍ່ຄວນແບ່ງປັນ API key ຂອງທ່ານໃຫ້ຜູ້ອື່ນ. ໃຜກໍ່ຕາມທີ່ເຂົ້າເຖິງມັນໄດ້ ສາມາດໃຊ້ເຄຣດິດ OpenRouter ຂອງທ່ານໄດ້" + ເຄຣດິດ OpenRouter ສິ້ນສຸດລິມິດແລ້ວ + ທ່ານໄດ້ໃຊ້ວົງເງິນ OpenRouter ຄົບກຳນົດແລ້ວ. ກະລຸນາຕື່ມເງິນ ຫຼື ເພີ່ມວົງເງິນການໃຊ້ຈ່າຍຂອງຄີຂອງທ່ານຢູ່ທີ່ OpenRouter.ai + ເປີດ OpenRouter.ai + ການແປ OpenRouter ລົ້ມເຫຼວ (HTTP %s) + ໃຊ້ໂຄຕ້າປະຈຳວັນຂອງ MyMemory ຄົບແລ້ວ + ຈະມີໃຫ້ໃຊ້ງານຄັ້ງຕໍ່ໄປໃນ %s + ທ່ານໄດ້ໃຊ້ໂຄຕ້າການແປຟຣີຂອງ MyMemory ປະຈຳວັນນີ້ແລ້ວ (5,000 ຕົວອັກສອນ). ເພີ່ມອີເມວ MyMemory ໃນການຕັ້ງຄ່າເພື່ອເພີ່ມຂີດຈຳກັດປະຈຳວັນເປັນ 50,000 ຕົວອັກສອນ, ຫຼື ປ່ຽນໄປໃຊ້ບໍລິການແປພາສາອື່ນ + ທ່ານໄດ້ໃຊ້ໂຄຕ້າການແປ MyMemory ປະຈຳວັນນີ້ແລ້ວ (50,000 ຕົວອັກສອນ). ປ່ຽນໄປໃຊ້ບໍລິການແປພາສາອື່ນ ຫຼື ລອງໃໝ່ໃນມື້ອື່ນ + ການແປ MyMemory ລົ້ມເຫຼວ (ສະຖານະ %s) + ເປີດໃຊ້ການຫຼິບວິດີໂອຊ້ຳ + ບໍ່ຈົດຈຳການວົນຊ້ຳວິດີໂອ + ປ້ອງກັນບໍ່ໃຫ້ບັນທຶກສະຖານະປັດຈຸບັນຂອງການວົນຊ້ຳວິດີໂອ ສຳລັບວິດີໂອຕໍ່ໄປ + ສະແດງປຸ່ມວົນຊ້ຳວິດີໂອ + ເປີດການຫຼິບວິດີໂອຊ້ຳແລ້ວ + ປິດການຫຼິບວິດີໂອຊ້ຳແລ້ວ + ຊ່ວງການວົນຊ້ຳ + ເລີ່ມ + ສິ້ນສຸດ + [hh:]mm:ss + [hh:]mm:ss (ວ່າງໄວ້ = ສິ້ນສຸດວິດີໂອ) + ກຳນົດ + ລ້າງ + ກຳລັງວົນຊ້ຳ %1$s - %2$s + ຊ່ວງເວລາບໍ່ຖືກຕ້ອງ + ສະແດງປຸ່ມ Picture-in-Picture + ສະແດງເມນູ Picture-in-Picture + ປ່ຽນເປັນ Picture-in-Picture + ສະແດງປຸ່ມຫຼິ້ນທັງໝົດ + "ແຕະເພື່ອສ້າງລາຍການຫຼິ້ນຂອງວິດີໂອທັງໝົດຈາກຊ່ອງ, ແຕະຄ້າງໄວ້ເພື່ອຍົກເລີກ + +ຂໍ້ມູນ: ອາດຈະບໍ່ໃຊ້ໄດ້ກັບການຖ່າຍທອດສົດ" + ໂໝດສ້າງລາຍການຫຼິ້ນ + ເນື້ອຫາທັງໝົດ (ຈັດລຽງຕາມເວລາ, ຈາກນ້ອຍໄປຫາຫຼາຍ) + ເນື້ອຫາທັງໝົດ (ຈັດລຽງຕາມເວລາ) + ເນື້ອຫາທັງໝົດ (ຈັດລຽງຕາມຄວາມນິຍົມ) + ສະເພາະວິດີໂອ (ຈັດລຽງຕາມເວລາ) + ສະເພາະວິດີໂອ (ຈັດລຽງຕາມຄວາມນິຍົມ) + ສະເພາະ Shorts (ຈັດລຽງຕາມເວລາ) + Shorts ເທົ່ານັ້ນ (ຈັດລຽງຕາມຄວາມນິຍົມ) + ວິດີໂອທີ່ສະຕຣີມເທົ່ານັ້ນ (ຈັດລຽງຕາມເວລາ) + ວິດີໂອທີ່ສະຕຣີມເທົ່ານັ້ນ (ຈັດລຽງຕາມຄວາມນິຍົມ) + ເນື້ອຫາສຳລັບສະມາຊິກທັງໝົດເທົ່ານັ້ນ + ວິດີໂອສຳລັບສະມາຊິກເທົ່ານັ້ນ + Shorts ສຳລັບສະມາຊິກເທົ່ານັ້ນ + ການສະຕຣີມສົດສຳລັບສະມາຊິກເທົ່ານັ້ນ + ບໍ່ສາມາດສ້າງລາຍການຫຼິ້ນໄດ້ ເນື່ອງຈາກ ID ຊ່ອງບໍ່ກົງກັນ + ສະແດງປຸ່ມໂຫຼດວິດີໂອຄືນໃໝ່ + ບໍ່ພົບ ID ຂອງວິດີໂອ. ກະລຸນາໂຫຼດວິດີໂອຄືນໃໝ່ດ້ວຍຕົນເອງ + ສະແດງປຸ່ມບັນທຶກໄວ້ເບິ່ງພາຍຫຼັງ + ແຕະເພື່ອບັນທຶກໄວ້ເບິ່ງພາຍຫຼັງ, ແຕະ and ຄ້າງໄວ້ສຳລັບຕົວເລືອກຄິວ + ບັນທຶກວິດີໂອລົງໃນລາຍການຫຼິ້ນ ດູພາຍຫຼັງ ສຳເລັດແລ້ວ + ວິດີໂອນີ້ຖືກບັນທຶກໄວ້ໃນລາຍການເບິ່ງພາຍຫຼັງແລ້ວ + ບໍ່ສາມາດບັນທຶກວິດີໂອນີ້ໃນລາຍການເບິ່ງພາຍຫຼັງໄດ້ + ບັນທຶກໄວ້ເບິ່ງພາຍຫຼັງ + ສະແດງປຸ່ມບັນທຶກໄວ້ເບິ່ງພາຍຫຼັງໃນເມນູຕັ້ງຄ່າຂອງເຄື່ອງຫຼິ້ນ + ອະນຸຍາດໃຫ້ບັນທຶກວິດີໂອທີ່ກຳລັງເບິ່ງຢູ່ໃນ \'ເບິ່ງພາຍຫຼັງ\' ຈາກເມນູຕັ້ງຄ່າຂອງວິດີໂອ + ສະແດງປຸ່ມບັນທຶກໄວ້ເບິ່ງພາຍຫຼັງໃນວິດີໂອສຳລັບເດັກ + ອະນຸຍາດໃຫ້ບັນທຶກວິດີໂອສຳລັບເດັກນ້ອຍໃນ \'ເບິ່ງພາຍຫຼັງ\' ຈາກເມນູ + ສະແດງປຸ່ມບັນທຶກໄວ້ເບິ່ງພາຍຫຼັງໃນເມນູຂອງ Shorts + ອະນຸຍາດໃຫ້ບັນທຶກ Shorts ໃນ \'ເບິ່ງພາຍຫຼັງ\' ຈາກເມນູ + ປ່ຽນເສັ້ນທາງ \'ເບິ່ງພາຍຫຼັງ\' ໄປຍັງລາຍການຫຼິ້ນ + ເບິ່ງພາຍຫຼັງ + ເພີ່ມໃນຄິວຂອງ Morphe + ຮັກສາລາຍການລໍຖ້າລະຫວ່າງເຊສຊັນ + ລາຍການລໍຖ້າຈະຖືກຮັກສາໄວ້ຫຼັງຈາກການເລີ່ມແອັບໃໝ່ + ສະຫຼັບການດຳເນີນການ ບັນທຶກ ແລະ ຈັດລາຍການລໍຖ້າ + ແຕະສຳລັບຕົວເລືອກຄິວ, ແຕະ and ຄ້າງໄວ້ເພື່ອບັນທຶກໄວ້ເບິ່ງພາຍຫຼັງ + ປະເພດຫຼິ້ນຕໍ່ໄປໃນຄິວ + ຄ່າເລີ່ມຕົ້ນ + ຄິວພື້ນຖານ + ຄິວ Morphe + ຄິວ Morphe ຝັ່ງລູກຄ້າ + ເພີ່ມລາຍການເມນູຄິວໃນ flyout ຂອງຟີດ + ເພີ່ມລາຍການ flyout ເສີມ. ເປີດໃຊ້ສິ່ງນີ້ຖ້າ \"ເພີ່ມໃນຄິວ\" ບໍ່ສະແດງ + ຄິວ + ຄິວ (%s) + ລຶບທັງໝົດ + ບໍ່ມີຫຍັງໃນຄິວ + ໃຊ້ \'ຫຼິ້ນຕໍ່ໄປໃນຄິວ\' ໃນວິດີໂອໃດກໍໄດ້ + ກຳລັງຫຼິ້ນ + ຫຼິ້ນຕໍ່ໄປໃນຄິວ + ຫຼິ້ນລາຍການສຸດທ້າຍໃນຄິວ + ຈັດລຳດັບໃໝ່ + ລຶບອອກ + ເພີ່ມເຂົ້າໃນຄິວແລ້ວ + ບໍ່ສາມາດເພີ່ມວິດີໂອນີ້ລົງໃນຄິວໄດ້ + ບໍ່ສາມາດເພີ່ມລາຍການຫຼິ້ນລົງໃນຄິວໄດ້ + ຄິວເຕັມແລ້ວ + ບໍ່ສາມາດເລີ່ມວິດີໂອໄດ້ + ລາຍຊື່ຂາວໂຄສະນາໃນເມນູ flyout + ເພີ່ມລາຍການ flyout ທີ່ອະນຸຍາດໃຫ້ໂຄສະນາຫຼິ້ນໃນຊ່ອງຂອງວິດີໂອນັ້ນ + ຟີເຈີນີ້ອາດຈະຕ້ອງໃຊ້ PotHelper ຫຼື ປ່ຽນ \"Spoof video streams\" ເປັນການເຂົ້າສູ່ລະບົບຂອງ TV ຫຼື Android VR + ລາຍຊື່ຂາວຄວາມໄວໃນເມນູ flyout + ເພີ່ມລາຍການ flyout ທີ່ຫຼິ້ນຊ່ອງຂອງວິດີໂອນັ້ນດ້ວຍຄວາມໄວ 1.0x ສະເໝີ + ບໍ່ໄດ້ເຂົ້າສູ່ລະບົບ + ບໍ່ມີ ID ຂອງລາຍການຫຼິ້ນ + ຄິວວ່າງເປົ່າ + ບໍ່ມີ ID ວິດີໂອ + ລົ້ມເຫຼວ: %s + ເພີ່ມເຂົ້າໃນຄິວບໍ່ສຳເລັດ + ສ້າງຄິວບໍ່ສຳເລັດ + ເອົາອອກຈາກຄິວບໍ່ສຳເລັດ + ບັນທຶກຄິວບໍ່ສຳເລັດ + ເພີ່ມເຂົ້າໃນຄິວແລ້ວ + ສ້າງຄິວແລ້ວ + ເອົາອອກຈາກຄິວແລ້ວ + ບັນທຶກຄິວລົງໃນ %s ແລ້ວ + ລາຍການລໍຖ້າຂອງ Morphe + ເພີ່ມເຂົ້າໃນຄິວ + ເພີ່ມເຂົ້າໃນຄິວ ແລະ ເປີດ + ເພີ່ມເຂົ້າໃນຄິວ ແລະ and ຫຼິ້ນ + ເພີ່ມ ແລະ ໂຫຼດວິດີໂອໃໝ່ + ລຶບອອກຈາກຄິວ + ລຶບ ແລະ ເປີດຄິວ + ລຶບ ແລະ ໂຫຼດວິດີໂອໃໝ່ + ເປີດຄິວ + ບັນທຶກຄິວໄວ້ໃນລາຍການຫຼິ້ນ + ບັນທຶກລາຍການຫຼິ້ນແລ້ວ + ບັນທຶກໄວ້ໃນ %s + ລ້າງຄິວ + ລ້າງຄິວແລ້ວ + ຄິວ + ຫຼິ້ນເປັນລຳດັບສຸດທ້າຍໃນຄິວ + ຫຼິ້ນເປັນລຳດັບຖັດໄປໃນຄິວ + YouTube ບໍ່ສາມາດສະແດງຄິວໄດ້. + ຄິວຍັງບໍ່ພ້ອມສຳລັບວິດີໂອນີ້. ກະລຸນາເປີດເຄື່ອງຫຼິ້ນວິດີໂອ ແລະ ລອງຄືນໃໝ່. + ເພີ່ມ Mix ເຂົ້າໃນຄິວ ຫຼື ເບິ່ງ Mix? + ເບິ່ງ + ເພີ່ມເຂົ້າໃນລາຍຄິວ + ກຳລັງໂຫຼດ Mix… + ເປີດຊ່ອງຈາກຮູບໂປຣໄຟລ໌ການຖ່າຍທອດສົດ + ປ້ອງກັນບໍ່ໃຫ້ການຖ່າຍທອດສົດເປີດຂຶ້ນເມື່ອແຕະຮູບໂປຣໄຟລ໌ຂອງຊ່ອງ + ປິດການນຳໃຊ້ທ່າທາງການຊອກຫາແບບລະອຽດ + ປິດໂໝດການຊອກຫາທີ່ຊັດເຈນ ເຊິ່ງຈະຖືກເປີດໃຊ້ເມື່ອດຶງແຖບຊອກຫາຂຶ້ນ + ເປີດໃຊ້ DVR ສຳລັບໄລຟ໌ສະຕຣີມ + ອະນຸຍາດໃຫ້ເລື່ອນຫາຈຸດທີ່ຕ້ອງການໃນທຸກໄລຟ໌ສະຕຣີມ, ແມ່ນແຕ່ໄລຟ໌ທີ່ປົກກະຕິຈະຈຳກັດການເຮັດດັ່ງກ່າວ + ເປີດໃຊ້ຮູບຕົວຢ່າງຂອງ seekbar + ສະແດງຮູບຕົວຢ່າງຂອງວິດີໂອໃນຂະນະທີ່ລາກ seekbar + ຖືກເປີດຊ້ຳຫຼາຍທີ່ສຸດ + "'ເປີດໃຊ້ DVR ສຳລັບໄລຟ໌ສະຕຣີມ' ອາດຈະບໍ່ສາມາດໃຊ້ໄດ້ + +ເພື່ອແກ້ໄຂບັນຫານີ້, ໃຫ້ເປີດ 'ປອມແປງສະຕຣີມວິດີໂອ' ແລະ ປ່ຽນ 'ຄລາຍເອນເລີ່ມຕົ້ນ' ເປັນ 'TV Simply' ຫຼື 'visionOS'" + ຂະຫຍາຍໄລຍະເວລາ DVR ຂອງການຖ່າຍທອດສົດ + ຊ່ວຍໃຫ້ທ່ານສາມາດຍ້ອນກັບໄປເບິ່ງການຖ່າຍທອດສົດໄດ້ສູງສຸດ 7 ວັນທີ່ຜ່ານມາ + "'ຂະຫຍາຍໄລຍະເວລາ DVR ຂອງການຖ່າຍທອດສົດ' ອາດຈະບໍ່ສາມາດນຳໃຊ້ໄດ້ + +ເພື່ອແກ້ໄຂບັນຫານີ້, ໃຫ້ເປີດ 'ປອມແປງສະຕຣີມວິດີໂອ' ແລະ ປ່ຽນ 'ລູກຄ້າເລີ່ມຕົ້ນ' ເປັນ 'TV Simply' ຫຼື 'visionOS'" + ເປີດໃຊ້ງານການເລື່ອນເພື່ອຊອກຫາ + ເປີດໃຊ້ການເລື່ອນເພື່ອຊອກຫາ ແທນທີ່ຈະຫຼິ້ນດ້ວຍຄວາມໄວ 2x ເມື່ອກົດຄ້າງໄວ້ທີ່ເຄື່ອງຫຼິ້ນວິດີໂອ + ເປີດໃຊ້ງານການແຕະເພື່ອຊອກຫາ + ພຽງແຕ່ແຕະທີ່ແຖບຊອກຫາເພື່ອຂ້າມໄປຫາຕຳແໜ່ງນັ້ນໂດຍບໍ່ຕ້ອງລາກ + ທ່າທາງການປັດໄປທາງຊ້າຍ + ທ່າທາງການປັດໄປທາງຂວາ + ທ່າທາງການປັດຂຶ້ນເທິງ + ຄວາມກວ້າງຂອງເຂດ ຊ້າຍ/ຂວາ + ເປີເຊັນຂອງຂອບໜ້າຈໍແຕ່ລະດ້ານທີ່ສຳຮອງໄວ້ສຳລັບການປັດຊ້າຍ ແລະ ຂວາ (5-50) + ຄວາມສະຫວ່າງ + ລະດັບສຽງ + Native + ປິດ + ບໍ່ສົນໃຈການປັດເມື່ອລັອກຢູ່ + ການປັດຈະຖືກບໍ່ສົນໃຈໃນຂະນະທີ່ເຄື່ອງຫຼິ້ນຖືກລັອກດ້ວຍ \'ລັອກໜ້າຈໍ\' + ເປີດໃຊ້ງານທ່າທາງການກົດເພື່ອປັດ + ຕ້ອງກົດຄ້າງໄວ້ກ່ອນທີ່ການຮູດຈະຖືກເຮັດວຽກ + ເປີດໃຊ້ງານການຕອບສະໜອງແບບສັ່ນ + ບັນທຶກ ແລະ ຟື້ນຟູຄວາມສະຫວ່າງ + ຈົດຈຳລະດັບຄວາມສະຫວ່າງເມື່ອເຂົ້າ / ອອກຈາກໂໝດເຕັມຈໍ + ເປີດໃຊ້ງານທ່າທາງຄວາມສະຫວ່າງອັດຕະໂນມັດ + ການເລື່ອນຄວາມສະຫວ່າງໄປຫາຄ່າຕ່ຳສຸດຈະເປີດໃຊ້ຄວາມສະຫວ່າງອັດຕະໂນມັດຂອງລະບົບ + ອັດຕະໂນມັດ + ເວລາໝົດອາຍຸຂອງການສະແດງຜົນທັບຊ້ອນການປັດ + ຈຳນວນມິນລິວິນາທີທີ່ການສະແດງຜົນທັບຊ້ອນປະກົດໃຫ້ເຫັນ + ຄວາມໂປ່ງໃສຂອງພື້ນຫຼັງການສະແດງຜົນທັບຊ້ອນການປັດ + ປັບລະດັບຄວາມໂປ່ງໃສ (0 ແມ່ນໂປ່ງໃສ, 100 ແມ່ນທຶບ) + ສີຂອງແຖບປົກຄຸມຄວາມສະຫວ່າງໃນການປັດ + ກຳນົດສີຂອງແຖບຄວາມຄືບໜ້າໃນການຄວບຄຸມຄວາມສະຫວ່າງ + ສີຂອງແຖບປັບລະດັບສຽງໃນການປັດ + ກຳນົດສີຂອງແຖບຄວາມຄືບໜ້າໃນການຄວບຄຸມລະດັບສຽງ + ສີຄວາມໄວຂອງການປັດຊ້ອນທັບ + ກຳນົດສີຂອງແຖບຄວາມຄືບໜ້າຂອງການຄວບຄຸມຄວາມໄວໃນການຫຼິ້ນ + ຂະໜາດຕົວອັກສອນຂອງແຖບປັດ + ປັບຂະໜາດຂອງຂໍ້ຄວາມໃນໜ້າຈໍ (1-30) + ເກນໄລຍະການປັດ + ກຳນົດໄລຍະການປັດທີ່ຈຳເປັນ ກ່ອນທີ່ການປັບປຸງຈະເຮັດວຽກ + ຄວາມໄວໃນການປັດປັບລະດັບສຽງ + ຄ່າທີ່ສູງຂຶ້ນຈະຕ້ອງປັດຫຼາຍຂຶ້ນຕໍ່ແຕ່ລະຂັ້ນຂອງລະດັບສຽງ + ຂັ້ນລະດັບສຽງ + ເພີ່ມລະດັບສຽງ + ການປັດຂຶ້ນເກີນລະດັບສຽງສູງສຸດຈະຊ່ວຍເພີ່ມສຽງ, ເຊິ່ງອາດເຮັດໃຫ້ສຽງທີ່ດັງເກີນໄປມີຄວາມບິດເບືອນ + ຄ່າເລີ່ມຕົ້ນຂອງອຸປະກອນ + ຄວາມອ່ອນໄຫວຂອງການປັດປ່ຽນຄວາມສະຫວ່າງ + ຄ່າທີ່ສູງຂຶ້ນຈະຕ້ອງປັດຫຼາຍຂຶ້ນຕໍ່ແຕ່ລະຂັ້ນຂອງຄວາມສະຫວ່າງ + ຄວາມອ່ອນໄຫວຂອງການປັດປ່ຽນຄວາມໄວ + ຄ່າທີ່ສູງຂຶ້ນຈະຕ້ອງປັດຫຼາຍຂຶ້ນຕໍ່ແຕ່ລະຂັ້ນຂອງຄວາມໄວ + ຂັ້ນຕອນຄວາມໄວ + ຄວາມສູງຂອງເຂດຄວາມໄວ + ຄວາມສູງຂອງເຂດການປັດຄວາມໄວ ເປັນເປີເຊັນຂອງຄວາມສູງຂອງເຄື່ອງຫຼິ້ນ (5-75) + ຄວາມໄວ + ຮູບແບບແຖບປັດ + ແຖບປັດແນວນອນ + ແຖບປັດແນວນອນ (ແບບມິນິມໍ - ດ້ານເທິງ) + ແຖບປັດແນວນອນ (ແບບມິນິມໍ - ກາງ) + ແຖບປັດແບບວົງມົນ + ຊັ້ນທັບຮູບວົງມົນ (ແບບມິນິມໍ) + ຊັ້ນທັບແນວຕັ້ງ + ຊັ້ນທັບແນວຕັ້ງ (ແບບມິນິມໍ) + ເປີດໃຊ້ການປັດເພື່ອປ່ຽນວິດີໂອ + ປັດໃນໂໝດເຕັມໜ້າຈໍ ເພື່ອຂ້າມໄປວິດີໂອຖັດໄປ ຫຼື ວິດີໂອກ່ອນໜ້ານີ້ + ປຸ່ມການເຮັດວຽກ + ເລືອກປຸ່ມການຈັດການທີ່ຈະໃຫ້ສະແດງໃນໂໝດແຕ່ວງ ແລະ ໂໝດເຕັມໜ້າຈໍ + ປຸ່ມແນວຕັ້ງ + ປິດການໃຊ້ງານແສງເຮືອງຂອງ ຖືກໃຈ ແລະ ຕິດຕາມ + ປ້ອງກັນບໍ່ໃຫ້ປຸ່ມ ຖືກໃຈ ແລະ ສະໝັກລົງທະບຽນ ເປັນແສງວິບວັບ ເມື່ອຜູ້ສ້າງເນື້ອຫາອ້າງເຖິງພວກມັນ + ເຊື່ອງແຖບການເຮັດວຽກ + ເຊື່ອງປຸ່ມ ຖືກໃຈ ແລະ ບໍ່ຖືກໃຈ + ເຊື່ອງປຸ່ມ ແຊຣ໌ + ເຊື່ອງປຸ່ມ \'ຢຸດໂຄສະນາ\' + ເຊື່ອງປຸ່ມຄວາມຄິດເຫັນ + ເຊື່ອງປຸ່ມເຊື່ອມຕໍ່ + ເຊື່ອງປຸ່ມລາຍງານ + ເຊື່ອງປຸ່ມຣີມິກຊ໌ + ເຊື່ອງປຸ່ມດາວໂຫຼດ + ເຊື່ອງປຸ່ມ Hype + ເຊື່ອງປຸ່ມໂປຣໂມດ + ເຊື່ອງປຸ່ມ ຂອບໃຈ + ເຊື່ອງປຸ່ມ ຖາມ + ເຊື່ອງຊ່ອງ ແລະ ປຸ່ມຕິດຕາມ + ເຊື່ອງຮູບໂປຣຟາຍຂອງຊ່ອງ ແລະ ປຸ່ມຕິດຕາມ ຢູ່ທີ່ຕອນເລີ່ມຕົ້ນຂອງແຖບການເຮັດວຽກ + ເຊື່ອງປຸ່ມຕິດຕາມ + ເຊື່ອງປຸ່ມຕິດຕາມ ແລະ ປຸ່ມເຂົ້າຮ່ວມ ທີ່ຢູ່ຂ້າງຮູບໂປຣໄຟລ໌ຂອງຊ່ອງ ຢູ່ສ່ວນເລີ່ມຕົ້ນຂອງແຖບການເຮັດວຽກ + ເຊື່ອງປຸ່ມ ຄລິບ + ສິ່ງນີ້ອາດຈະບໍ່ໃຊ້ໄດ້ກັບບາງບັນຊີຜູ້ໃຊ້ + ເຊື່ອງປຸ່ມ ຮ້ານຄ້າ + ເຊື່ອງປຸ່ມ ບັນທຶກ + ເຊື່ອງປຸ່ມຕື່ມອີກ + ປຸ່ມເຕັມຈໍ + ເຊື່ອງປຸ່ມ ບໍ່ມັກ + ເຊື່ອງປຸ່ມ ມັກ + ເຊື່ອງປຸ່ມ \'ແຊັດສົດ\' + ເຊື່ອງປຸ່ມ ມິກສ໌ + ເຊື່ອງປຸ່ມ \'ວິດີໂອເພີ່ມເຕີມ\' + ເຊື່ອງປຸ່ມລາຍການຫຼິ້ນ + ຂອບເທິງຂອງການດຳເນີນການດ່ວນ + ກຳນົດຂອບເທິງຂອງແຖບການດຳເນີນການດ່ວນ (0-32 dp) + ຄຳບັນຍາກຳກັບ + ຈັດການຄຳບັນຍາກົດອັດຕະໂນມັດ ແລະ ຄຸກກີຂອງຄຳບັນຍາກົດ + ການເບິ່ງເຫັນຄຳບັນຍາກທີ່ອັດຕະໂນມັດ + ສະແດງສະເໝີ + ບໍ່ໃຫ້ສະແດງເລີຍ + ສະແດງເມື່ອເປີດສຽງ + ສະແດງເມື່ອປິດສຽງ + ພາສາຄຳບັນຍາຍທີ່ມັກ + ເລືອກຄຳບັນຍາຍຈາກຜູ້ໃຫ້ບໍລິການກ່ອນ, ຈາກນັ້ນຈະແປເປັນພາສາທີ່ມັກໂດຍອັດຕະໂນມັດ + ປິດ + ເພີ່ມພາສາແປອັດຕະໂນມັດເພີ່ມເຕີມ + ເພີ່ມພາສາຫຼາຍຂຶ້ນໃນເມນູແປອັດຕະໂນມັດ + ຕັ້ງຄ່າຄຸກກີ້ຂອງຄຳບັນຍາກຳກັບ + "ແກ້ໄຂບັກຂອງ YouTube ທີ່ຄຳບັນຍາກົດອັດຕະໂນມັດອາດຈະບໍ່ສາມາດນຳໃຊ້ໄດ້ ເນື່ອງຈາກຄຸກກີ້ຫາຍໄປ + +ຕ້ອງລະບຸ 'ຄຸກກີ້ຄຳບັນຍາກົດ'" + ຄຸກກີ້ຂອງຄຳບັນຍາກຳກັບ + ຄຸກກີ້ທີ່ຈະຖືກຕັ້ງໃນຄຳຮ້ອງຂໍ Timed Text API (Caption Data API) + ດຶງຂໍ້ມູນຄຸກກີ້ຂອງຄຳບັນຍາກຳກັບ + ຄລິກທີ່ນີ້ເພື່ອດຶງຂໍ້ມູນຄຸກກີ້ + ດຶງຂໍ້ມູນຄຸກກີ້ບໍ່ສຳເລັດ + ມີຄຸກກີ້ຢູ່ແລ້ວ + ອັບເດດຄຸກກີ້ສຳເລັດແລ້ວ + ແກ້ໄຂບົດບັນທຶກ + ແກ້ໄຂຄວາມລົ້ມເຫຼວຂອງການກວດສອບເງື່ອນໄຂເບື້ອງຕົ້ນ ທີ່ອາດຈະປ້ອງກັນບໍ່ໃຫ້ໂຫຼດບົດບັນທຶກການສົນທະນາໄດ້ + ສະແດງຄຳບັນຍາຍໃນຮູບພາບໃນຮູບພາບ + ປຸ່ມນຳທາງ + ເລືອກປຸ່ມທີ່ຈະໃຫ້ປາກົດໃນແຖບນຳທາງດ້ານລຸ່ມ + ເຊື່ອງປຸ່ມໜ້າຫຼັກ + ເຊື່ອງປຸ່ມ Shorts + ເຊື່ອງປຸ່ມສ້າງ + ເຊື່ອງປຸ່ມການຕິດຕາມ + ເຊື່ອງປຸ່ມການແຈ້ງເຕືອນ + ສະແດງປຸ່ມຄົ້ນຫາ + ຕຳແໜ່ງປຸ່ມຄົ້ນຫາ + ສະແດງປຸ່ມການຕັ້ງຄ່າ + ຕຳແໜ່ງປຸ່ມການຕັ້ງຄ່າ + ການເຮັດວຽກຂອງປຸ່ມຕັ້ງຄ່າ + ເປີດການຕັ້ງຄ່າ Morphe ເມື່ອເປີດໃຊ້ງານ, ຫຼື ການຕັ້ງຄ່າ YouTube ເມື່ອປິດໃຊ້ງານ + ສະຫຼັບປຸ່ມສ້າງ ດ້ວຍປຸ່ມການແຈ້ງເຕືອນ + ຖ້າການປ່ຽນແປງການຕັ້ງຄ່ານີ້ບໍ່ມີຜົນ, ໃຫ້ລອງປ່ຽນໄປໃຊ້ໂໝດບໍ່ລະບຸຕົວຕົນ + ເຊື່ອງປ້າຍຊື່ປຸ່ມນຳທາງ + ເຊື່ອງຈຸດເນື້ອຫາໃໝ່ + ເຊື່ອງແຖບການນຳທາງ + "ສິ່ງນີ້ຈະເຊື່ອງແຖບການນຳທາງທັງໝົດ, ລວມທັງແທັບ 'ທ່ານ'. + +ເພື່ອເຂົ້າເຖິງການຕັ້ງຄ່າ, ໃຫ້ເປີດປຸ່ມການຕັ້ງຄ່າຜ່ານ ທົ່ວໄປ → ແຖບເຄື່ອງມື → ສະແດງການຕັ້ງຄ່າ" + ເປີດໃຊ້ປຸ່ມນຳທາງແບບແຄບ + ຫຼຸດໄລຍະຫ່າງຕາມແນວນອນລະຫວ່າງປຸ່ມ + ປິດການນຳໃຊ້ປຸ່ມນຳທາງທີ່ບໍ່ມີປ້າຍກຳກັບ + ສະແດງປຸ່ມນຳທາງພ້ອມກັບປ້າຍກຳກັບ, ຖ້າ YouTube ສົ່ງພວກມັນມາໂດຍບໍ່ມີປ້າຍກຳກັບ + ປິດການນຳໃຊ້ແຖບການນຳທາງຂອງແຖບເຄື່ອງມື + ສະແດງລາຍການຕິດຕາມໃນແຖບການນຳທາງ, ຖ້າ YouTube ຍ້າຍມັນໄປໄວ້ທີ່ແຖບດ້ານເທິງຂອງໜ້າຫຼັກ + ເປີດໃຊ້ງານແອນິເມຊັນຂອງແຖບນຳທາງ + ສ້າງແອນິເມຊັນການປ່ຽນຜ່ານເມື່ອສະຫຼັບລະຫວ່າງແທັບ + ປິດການຊ່ອນແຖບນຳທາງອັດຕະໂນມັດ + ໃຫ້ແຖບນຳທາງສະແດງຢູ່ສະເໝີໃນຂະນະທີ່ເລື່ອນ + ຮູບແບບແຖບນຳທາງ + ຄ່າເລີ່ມຕົ້ນ + ໂປ່ງໃສ + ໂປ່ງໃສ + ທຶບ + ຮູບແບບນີ້ອາດຈະບໍ່ຮອງຮັບໂດຍອຸປະກອນ ຫຼື ເວີຊັນ YouTube ຂອງທ່ານ, ແລະ ສາມາດເຮັດໃຫ້ເກີດບັນຫາໃນການຈັດວາງ ຫຼື ເຮັດໃຫ້ແອັບປິດລົງ + ແຖບເຄື່ອງມື + ເລືອກສ່ວນປະກອບທີ່ຈະໃຫ້ປາກົດໃນແຖບເຄື່ອງມື + ເຊື່ອງປຸ່ມ Cast + ເຊື່ອງປຸ່ມແຊັດ + ເຊື່ອງປຸ່ມໄມໂຄຣໂຟນ + ເຊື່ອງປຸ່ມຄົ້ນຫາ + ເມື່ອເປີດໃຊ້ງານ, ແຕະເພື່ອເປີດການຕັ້ງຄ່າ Morphe ແລະ ກົດຄ້າງໄວ້ເພື່ອເປີດການຕັ້ງຄ່າ YouTube, ປິດເພື່ອສະຫຼັບກັນ + ເມນູ Flyout + ເລືອກສ່ວນປະກອບທີ່ຈະໃຫ້ປາກົດໃນເມນູ flyout + ເຊື່ອງເມນູ \'ການຕັ້ງຄ່າເພີ່ມເຕີມ\' / ອື່ນໆ + ເຊື່ອງເມນູ \'ໂໝດແສງສະຫວ່າງອ້ອມຂ້າງ\' + "ເມນູ 'Audio Track' ຖືກເຊື່ອງໄວ້ + +ເພື່ອສະແດງເມນູ 'Audio Track', ໃຫ້ປ່ຽນ 'Spoof video streams' ເປັນຄລາຍເອນໃດໜຶ່ງ ຍົກເວັ້ນ 'Android Studio' ແລະ 'Android VR Downgraded'" + ເຊື່ອງເມນູ \'ແທຣັກສຽງ\' + ເຊື່ອງສ່ວນລຸ່ມຂອງເມນູ \'ແທຣັກສຽງ\' + ເຊື່ອງເມນູ ຄຳບັນຍາຍ + ເຊື່ອງສ່ວນລຸ່ມຂອງເມນູ ຄຳບັນຍາຍ + ເຊື່ອງສ່ວນຫົວຂອງເມນູ ຄຳບັນຍາຍ + ເຊື່ອງເມນູ \'ຄວາມຊ່ວຍເຫຼືອ & ຄຳຄິດເຫັນ\' + ເຊື່ອງເມນູ \'ຟັງດ້ວຍ YouTube Music\' + ເຊື່ອງເມນູ \'ລັອກໜ້າຈໍ\' + ເຊື່ອງເມນູ \'ວົນຊ້ຳວິດີໂອ\' + ເຊື່ອງເມນູ \'ລະຫວ່າງການເດີນທາງ\' + ເຊື່ອງເມນູຄຸນນະພາບ + ເຊື່ອງສ່ວນທ້າຍຂອງເມນູຄຸນນະພາບ + ເຊື່ອງສ່ວນຫົວຂອງເມນູຄຸນນະພາບ + ເຊື່ອງເມນູ \'ຕົວຈັບເວລາການນອນ\' + ເຊື່ອງເມນູ \'ຄວາມໄວໃນການຫຼິ້ນ\' + ເຊື່ອງເມນູ \'ລະດັບສຽງຄົງທີ່\' + ເຊື່ອງເມນູ \'ເບິ່ງໃນ VR\' + ເຊື່ອງການທົດລອງເປີດອັດຕະໂນມັດ + ເຊື່ອງການທ່ອງເບິ່ງວິດີໂອລ່ວງໜ້າໃນໜ້າຈໍເຕັມ ກ່ອນທີ່ການຫຼິ້ນອັດຕະໂນມັດຈະເລີ່ມຂຶ້ນ + ປຸ່ມຊ້ອນທັບ + ເລືອກປຸ່ມທີ່ຈະໃຫ້ປາກົດເທິງເຄື່ອງຫຼິ້ນວິດີໂອ + ເຊື່ອງປຸ່ມຫຼິ້ນອັດຕະໂນມັດ + ປຸ່ມເຊື່ອງຄຳບັນຍາຍ + ປຸ່ມເຊື່ອງ Cast + ປຸ່ມເຊື່ອງCollapse + ປຸ່ມເຊື່ອງໜ້າຈໍເຕັມ + ເຊື່ອງພື້ນຫຼັງຂອງປຸ່ມຄວບຄຸມເຄື່ອງຫຼິ້ນ + ເຊື່ອງການແລັງສີເຂັ້ມທີ່ລ້ອມຮອບປຸ່ມຄວບຄຸມເຄື່ອງຫຼິ້ນວິດີໂອ + ຄວາມໂປ່ງໃສຂອງພື້ນຫຼັງປຸ່ມຄວບຄຸມ + ປັບຄວາມໂປ່ງໃສຂອງພື້ນຫຼັງທາງຫຼັງປຸ່ມຄວບຄຸມເຄື່ອງຫຼິ້ນ ແລະ ແຖບເວລາ (0 ແມ່ນບໍ່ເຫັນ, 100 ແມ່ນທຶບ) + ເຊື່ອງປຸ່ມກ່ອນໜ້ານີ້ & ຖັດໄປ + ປຸ່ມເຊື່ອງການຕັ້ງຄ່າ + ເຊື່ອງບັດໃນໜ້າຈໍຈົບ + ໂໝດ Ambient + ຈັດການການເບິ່ງເຫັນໂໝດ Ambient ແລະ ຂໍ້ຈຳກັດດ້ານພະລັງງານ + ຂ້າມຂໍ້ຈຳກັດຂອງໂໝດ Ambient + ອະນຸຍາດໃຫ້ໂໝດ Ambient ຍັງຄົງເຮັດວຽກໃນໂໝດປະຢັດແບັດເຕີຣີ + ປິດການນຳໃຊ້ໂໝດ Ambient + ປິດໂໝດ Ambient ໃນໜ້າຈໍເຕັມ + ເຊື່ອງບັດຂໍ້ມູນ + ປິດແອນິເມຊັນຕົວເລກກິ້ງ + ຢຸດການເຄື່ອນໄຫວການນັບຈຳນວນໄລອ໌ ແລະ ຈຳນວນການເບິ່ງ + ເຊື່ອງແຖບເລື່ອນຂອງເຄື່ອງຫຼິ້ນວິດີໂອ + ເຊື່ອງຮູບຕົວຢ່າງໃນແຖບເລື່ອນວິດີໂອ + ເຊື່ອງແຖບຄວາມຄືບໜ້າການເບິ່ງ ທີ່ສະແດງໃນຮູບຕົວຢ່າງຂອງວິດີໂອທີ່ທ່ານໄດ້ເບິ່ງແລ້ວ + ເປີດໃຊ້ແຖບເລື່ອນຂະໜາດໃຫຍ່ໃນໜ້າຈໍເຕັມ + ເຊື່ອງ Shorts + ເຊື່ອງໃນໜ້າຊ່ອງ + ເຊື່ອງໃນຟີດໜ້າຫຼັກ & ວິດີໂອທີ່ກ່ຽວຂ້ອງ + ເຊື່ອງໃນຜົນການຄົ້ນຫາ + ເຊື່ອງໃນຟີດການຕິດຕາມ + ເຊື່ອງໃນຄຳອະທິບາຍວິດີໂອ + ເຊື່ອງໃນປະຫວັດການເບິ່ງ + ເຄື່ອງຫຼິ້ນ Shorts + ເລືອກສ່ວນປະກອບທີ່ຈະໃຫ້ສະແດງໃນເຄື່ອງຫຼິ້ນ Shorts + ປິດການໃຊ້ງານການແຕະສອງຄັ້ງເພື່ອຖືກໃຈ + ເຊື່ອງປຸ່ມ AI + ເຊື່ອງປ້າຍກຳກັບ \'ພາກສຽງອັດຕະໂນມັດ\' + ເຊື່ອງແຖບຊ່ອງ + ເຊື່ອງປຸ່ມຄວາມຄິດເຫັນ + ເຊື່ອງປຸ່ມເອັບເຟັກ + ເຊື່ອງປ້າຍລິ້ງວິດີໂອ + ເຊື່ອງຄຳແນະນຳການໃຊ້ທ່າທາງ + ເຊື່ອງປຸ່ມ \'ຈໍສີຂຽວ\' + ເຊື່ອງປຸ່ມແຮຊແທັກ + ເຊື່ອງແຜງຂໍ້ມູນ + ເຊື່ອງປຸ່ມເຂົ້າຮ່ວມ + ເຊື່ອງປຸ່ມຖືກໃຈ + ເຊື່ອງແອນິເມຊັນນ້ຳພຸຂອງປຸ່ມຖືກໃຈ + ເຊື່ອງການເບິ່ງລ່ວງໜ້າແບບສົດ + ເຊື່ອງປ້າຍບອກສະຖານທີ່ + ເຊື່ອງແຖບນຳທາງ + ເຊື່ອງປຸ່ມ \'ໂພສໃໝ່\' + ເຊື່ອງປຸ່ມຊ້ອນທັບໃນຂະນະທີ່ຢຸດຊົ່ວຄາວ + ເຊື່ອງການເບິ່ງຄຳຄິດເຫັນລ່ວງໜ້າ + ເຊື່ອງປຸ່ມຣີມິກຊ໌ + ເຊື່ອງປຸ່ມບັນທຶກ + ເຊື່ອງປຸ່ມ \'ບັນທຶກເພງ\' + ເຊື່ອງຄຳແນະນຳໃນການຄົ້ນຫາ + ເຊື່ອງປຸ່ມແຊຣ໌ + ເຊື່ອງປຸ່ມຮ້ານຄ້າ + ເຊື່ອງປຸ່ມສຽງ + ເຊື່ອງປ້າຍຂໍ້ມູນເມຕາດາຕາຂອງສຽງ + ເຊື່ອງປຸ່ມຕິດຕາມ + ເຊື່ອງປຸ່ມ \'ຊື້ Super Thanks\' + ເຊື່ອງສິນຄ້າທີ່ຖືກແທັກ + ເຊື່ອງປຸ່ມທີ່ກຳລັງຈະມາເຖິງ + ເຊື່ອງປຸ່ມ \'ໃຊ້ສຽງນີ້\' + ເຊື່ອງປຸ່ມ \'ໃຊ້ແມ່ແບບນີ້\' + ເຊື່ອງຫົວຂໍ້ວິດີໂອ + ເຊື່ອງວິດີໂອທີ່ແນະນຳໃນໜ້າຈໍຈົບ + "ເຊື່ອງວິດີໂອແນະນຳທີ່ປາກົດຂຶ້ນຫຼັງຈາກວິດີໂອສິ້ນສຸດ + +ການຕັ້ງຄ່ານີ້ຈະໃຊ້ໄດ້ກໍ່ຕໍ່ເມື່ອປິດການຫຼິ້ນອັດຕະໂນມັດ. ທ່ານສາມາດປ່ຽນແປງມັນໄດ້ໃນ YouTube: +ການຕັ້ງຄ່າ → ການຫຼິ້ນ (ຫຼື ການຫຼິ້ນອັດຕະໂນມັດ) → ຫຼິ້ນວິດີໂອຕໍ່ໄປອັດຕະໂນມັດ" + ເຊື່ອງວິດີໂອທີ່ກ່ຽວຂ້ອງ + ເຊື່ອງສ່ວນທັບຊ້ອນ \'ວິດີໂອເພີ່ມເຕີມ\' + ເຊື່ອງສ່ວນທັບຊ້ອນ \'ວິດີໂອເພີ່ມເຕີມ\' ທີ່ສະແດງເມື່ອປັດຂຶ້ນໃນໂໝດເຕັມຈໍ + ເຊື່ອງປະທັບເວລາຂອງວິດີໂອ + ປິດໃຊ້ແຜງປັອບອັບຂອງເຄື່ອງຫຼິ້ນ + ປິດການເປີດແຜງລາຍການຫຼິ້ນ ແລະ ແຊັດສົດໂດຍອັດຕະໂນມັດ + ປິດການຫຼິ້ນອັດຕະໂນມັດຂອງລາຍການຫຼິ້ນ + ປ້ອງກັນບໍ່ໃຫ້ລາຍການຫຼິ້ນເພງເລື່ອນໄປວິດີໂອຖັດໄປໂດຍອັດຕະໂນມັດ + ປິດການໃຊ້ງານການເຕືອນ \'ເບິ່ງຕໍ່ຫຼືບໍ່?\' + ອອກຈາກໂໝດເຕັມຈໍເມື່ອວິດີໂອສິ້ນສຸດ + ປິດການໃຊ້ງານ + ແນວຕັ້ງເທົ່ານັ້ນ + ແນວນອນເທົ່ານັ້ນ + ແນວຕັ້ງ ແລະ ແນວນອນ + ບັງຄັບໃຫ້ເປັນແນວນອນໃນໂໝດເຕັມຈໍ + ໝູນເຄື່ອງຫຼິ້ນໃຫ້ເປັນແນວນອນເມື່ອເຂົ້າສູ່ໂໝດເຕັມຈໍ. ບໍ່ສາມາດໃຊ້ໄດ້ໃນໜ້າຈໍຂະໜາດນ້ອຍ ທີ່ເປີດເຕັມຈໍເປັນແນວນອນຢູ່ແລ້ວ + ທ່າທາງໜ້າຈໍເຕັມ + ທ່າທາງຂອງເຄື່ອງຫຼິ້ນໃນໂໝດເຕັມຈໍ + ປິດການໃຊ້ທ່າທາງປັດຂຶ້ນໃນແນວຕັ້ງ + ປິດການໃຊ້ທ່າທາງປັດລົງໃນແນວຕັ້ງ + ປິດການໃຊ້ທ່າທາງປັດລົງໃນແນວນອນ + ປິດການນຳໃຊ້ທ່າທາງໜີບເພື່ອຊູມ + ກູ້ຄືນການໃຊ້ສອງນິ້ວເພື່ອຊູມ + ເປີດວິດີໂອໃນໂໝດເຕັມຈໍ + ປິດການໃຊ້ງານ + ແນວຕັ້ງ + ແນວນອນ + ຂະໜາດວິດີໂອເຕັມຈໍ + ຄ່າເລີ່ມຕົ້ນ + ຢືດ (ເຕັມຈໍ) + ຊູມ (ຕັດໃຫ້ເຕັມ) + ສະແດງປຸ່ມຂະໜາດວິດີໂອ + ແຕະເພື່ອປ່ຽນລະຫວ່າງ ຄ່າເລີ່ມຕົ້ນ, ຢືດ, ແລະ ຊູມ + ສະແດງສະເພາະໃນໂໝດເຕັມຈໍ + ເຊື່ອງປຸ່ມປັບຂະໜາດວິດີໂອໃນເຄື່ອງຫຼິ້ນແນວຕັ້ງ + ສະແດງປຸ່ມປິດສຽງ + ຄວາມໂປ່ງໃສຂອງບ່ອນຊ້ອນທັບເຄື່ອງຫຼິ້ນ + ປັບລະດັບຄວາມໂປ່ງໃສ (0 ແມ່ນໂປ່ງໃສ, 100 ແມ່ນທຶບ) + ແຖບຄົ້ນຫາ + ປິດການໃຊ້ງານ + ແບບກວ້າງ + ກວ້າງພິເສດ + ຮູບແບບອຸປະກອນ + ຄ່າເລີ່ມຕົ້ນ + ໂທລະສັບ + ແທັບເລັດ + ຍານພາຫະນະ + "ການປ່ຽນແປງລວມມີ: + +ຮູບແບບ Tablet +• ໂພສຂອງຊຸມຊົນຖືກເຊື່ອງໄວ້ +• ການຕັ້ງຄ່າການຫຼິ້ນໃນ feed ບໍ່ສາມາດນຳໃຊ້ໄດ້ +• ປຸ່ມ Remix ແລະ ປຸ່ມ ສຽງ ບໍ່ສາມາດນຳໃຊ້ໄດ້ໃນ Shorts +• ບ່ອນວາງຊ້ອນ 'ວິດີໂອເພີ່ມເຕີມ' ອາດຈະບໍ່ສາມາດນຳໃຊ້ໄດ້ +• ແຖບການເຮັດວິດີໂອແບບເກົ່າທີ່ບໍ່ໄດ້ຍຸບຈະສະແດງຂຶ້ນ +• ລາຍການເມນູ Flyout ອາດຈະເຮັດວຽກບໍ່ຖືກຕ້ອງ + +ຮູບແບບ Automotive +• feed ຖືກຈັດລະບຽບຕາມຫົວຂໍ້ ແລະ ຊ່ອງ +• ການຕັ້ງຄ່າການຫຼິ້ນໃນ feed ບໍ່ສາມາດນຳໃຊ້ໄດ້ +• ປຸ່ມ Remix ແລະ ປຸ່ມ ສຽງ ບໍ່ສາມາດນຳໃຊ້ໄດ້ໃນ Shorts +• ບ່ອນວາງຊ້ອນ 'ວິດີໂອເພີ່ມເຕີມ' ອາດຈະບໍ່ສາມາດນຳໃຊ້ໄດ້ +• Shorts ຈະເປີດໃນເຄື່ອງຫຼິ້ນປົກກະຕິ +• ແຖບການເຮັດວິດີໂອແບບເກົ່າທີ່ບໍ່ໄດ້ຍຸບຈະສະແດງຂຶ້ນ" + ເປີດໃຊ້ການຈັດວາງແບບແທັບເລັດໃນເຄື່ອງຫຼິ້ນ + "ຟື້ນຟູຮູບແບບເຄື່ອງຫຼິ້ນແບບເກົ່າ + +ຂໍ້ຈຳກັດ: ຖ້າ 'ຟື້ນຟູແຖບການເຮັດວຽກຂອງວິດີໂອແບບເກົ່າ' ຖືກປິດ, ປ່ອງຢ້ຽມ 'ວິດີໂອເພີ່ມເຕີມ' ອາດຈະບໍ່ສາມາດໃຊ້ງານໄດ້" + 21.11.48 - ກູ້ຄືນພຶດຕິກຳຂອງຄຳບັນຍາກິດແບບເກົ່າ + 20.39.41 - ກູ້ຄືນການບັນທຶກເກົ່າໄປຍັງລາຍການຫຼິ້ນ + 20.28.41 - ປິດໄອຄອນຕົວໜາ + 20.13.41 - ຄືນຄ່າຊື່ຊ່ອງລຸ່ມເຄື່ອງຫຼິ້ນວິດີໂອ + ເປີດແຜ່ນແບ່ງປັນຂອງລະບົບ + ເປີດແຜງແບ່ງປັນຂອງລະບົບ ແທນທີ່ແຜງແບ່ງປັນພາຍໃນແອັບ ເມື່ອແບ່ງປັນເນື້ອຫາ + ປັບປ່ຽນປຸ່ມ YouTube Music + ເປີດແອັບເພງທີ່ທ່ານມັກ ແທນທີ່ແອັບ YouTube Music ທີ່ຕິດຕັ້ງມາພ້ອມກັບເຄື່ອງ + ຊື່ແພັກເກດຂອງແອັບເພງ + ຊື່ແພັກເກດຂອງແອັບເພງທີ່ທ່ານຕິດຕັ້ງໄວ້ + ໜ້າເລີ່ມຕົ້ນ + ຄ່າເລີ່ມຕົ້ນ + ການຕິດຕາມທັງໝົດ + ຊອກຫາຊ່ອງ + ຫຼັກສູດ / ການຮຽນຮູ້ + ສຳຫຼວດ + ແຟຊັນ & ຄວາມງາມ + ເກມ + ປະຫວັດ + ຄັງຂໍ້ມູນ + ວິດີໂອທີ່ມັກ + ໄລຟ໌ + ຮູບເງົາ + ເພງ + ຂ່າວ + ການແຈ້ງເຕືອນ + ລາຍການຫຼິ້ນ + ຄົ້ນຫາ + ຊື້ເຄື່ອງ + Shorts + ກິລາ + ການຕິດຕາມ + ທີ່ກຳລັງຮິດ + ຄວາມເປັນຈິງສະເໝືອນ + ເບິ່ງພາຍຫຼັງ + ຄລິບຂອງທ່ານ + ປິດການດຳເນີນການຕໍ່ໃນຕອນເລີ່ມຕົ້ນ + ເປີດ Shorts ດ້ວຍ + ເຄື່ອງຫຼິ້ນ Shorts + ເຄື່ອງຫຼິ້ນປົກກະຕິ + ເຄື່ອງຫຼິ້ນປົກກະຕິແບບເຕັມຈໍ + ຫຼິ້ນ Shorts ອັດຕະໂນມັດ + ເລື່ອນໄປຫາ Short ຖັດໄປໂດຍອັດຕະໂນມັດ ແທນທີ່ຈະຫຼິ້ນອັນປັດຈຸບັນຊ້ຳ + ຫຼິ້ນອັດຕະໂນມັດໃນພື້ນຫຼັງ + ເລື່ອນໄປຫາ Short ຖັດໄປໂດຍອັດຕະໂນມັດ ໃນລະຫວ່າງການຫຼິ້ນໃນພື້ນຫຼັງ + ຂ້າມ Shorts ສົດ + ຂ້າມການຖ່າຍທອດສົດທີ່ປາກົດໃນຟີດ Shorts + ການຫຼິ້ນໃນຟີດ + ເປີດຕະຫຼອດ + ສະເພາະ Wi-Fi ເທົ່ານັ້ນ + ປິດ + ຄືນຄ່າຫົວຂໍ້ດັ້ງເດີມ + ສະແດງຫົວຂໍ້ວິດີໂອ, ຄຳອະທິບາຍວິດີໂອ ແລະ ຄຳອະທິບາຍຊ່ອງຕົ້ນສະບັບ ແທນທີ່ການແປອັດຕະໂນມັດ + ກຳລັງໂຫຼດຫົວຂໍ້… + ປິດການຣີເຟຣຊຟີດອັດຕະໂນມັດ + ປ້ອງກັນບໍ່ໃຫ້ຟີດຣີເຟຣຊອັດຕະໂນມັດຫຼັງຈາກທີ່ມັນເກົ່າໄປ + ຮູບແບບບັດຟີດ + ແບບຄລາສສິກ + ບັດ + ບັດທີ່ມີປຸ່ມຖືກໃຈ, ຄອມເມັ້ນ ແລະ ບັນທຶກ + ຄຳຄິດເຫັນ + ປິດການຫຼິ້ນຕໍ່ຂອງ miniplayer + ປິດການໃຊ້ງານເຄື່ອງຫຼິ້ນຂະໜາດນ້ອຍ \'ເບິ່ງຕໍ່\', ເຊິ່ງເປັນຄຸນສົມບັດຂອງ YouTube Premium ທີ່ປາກົດຂຶ້ນເມື່ອເປີດແອັບ + ເຄື່ອງຫຼິ້ນຂະໜາດນ້ອຍ + ປັບແຕ່ງຮູບລັກສະນະ ແລະ ພຶດຕິກຳຂອງເຄື່ອງຫຼິ້ນວິດີໂອຂະໜາດນ້ອຍ + ປະເພດເຄື່ອງຫຼິ້ນຂະໜາດນ້ອຍ + ເຊື່ອງຫົວຂໍ້ ແລະ ຊ່ອງ + ປິດການໃຊ້ງານ + ຄ່າເລີ່ມຕົ້ນ + ແບບລຽບງ່າຍ + ແບບມິນິມໍ 2 + ແທັບເລັດ + ແບບທັນສະໄໝ 1 + ແບບທັນສະໄໝ 2 + ແບບທັນສະໄໝ 3 + ແບບທັນສະໄໝ 4 + ປິດການນຳໃຊ້ມຸມມົນ + ປິດການນຳໃຊ້ການລາກ ແລະ ວາງ + ປ້ອງກັນການລາກເຄື່ອງຫຼິ້ນຂະໜາດນ້ອຍໄປຍັງມຸມຕ່າງໆຂອງໜ້າຈໍ + ປິດທ່າທາງການລາກໃນແນວນອນ + ປ້ອງກັນການປິດເຄື່ອງຫຼິ້ນຂະໜາດນ້ອຍ ໂດຍການປັດມັນຕາມລວງນອນອອກຈາກໜ້າຈໍ + ຢຸດການຫຼິ້ນດ້ວຍການລາກຕາມແນວນອນ + ການຫຼິ້ນຈະຢຸດຊົ່ວຄາວເມື່ອລາກເຄື່ອງຫຼິ້ນຂະໜາດນ້ອຍອອກຈາກໜ້າຈໍ + ຈົດຈຳຕຳແໜ່ງສະຖານະການລາກ + ການລາກເຄື່ອງຫຼິ້ນຂະໜາດນ້ອຍອອກຈາກໜ້າຈໍຈະຮັກສາສະຖານະ UI ຂອງມັນໄວ້ + ເຊື່ອງປຸ່ມທີ່ຊ້ອນທັບ + ຂະໜາດເລີ່ມຕົ້ນ + ກຳນົດຂະໜາດເລີ່ມຕົ້ນໃນໜ້າຈໍ, ເປັນພິກເຊລ + ຂະໜາດພິກເຊລຕ້ອງຢູ່ລະຫວ່າງ %1$s ແລະ %2$s + ຄວາມໂປ່ງໃສຂອງການຊ້ອນທັບ + ປັບລະດັບຄວາມໂປ່ງໃສ (0 ແມ່ນໂປ່ງໃສ, 100 ແມ່ນທຶບ) + ປຸ່ມກັບຄືນຈະອອກຈາກຟີດ + ການກົດປຸ່ມກັບຄືນຈະອອກຈາກແອັບສະເໝີ ແທນທີ່ຈະເລື່ອນໄປຫາສ່ວນເທິງຂອງຟີດ + ບັງຄັບໃຊ້ແອນິເມຊັນ + ຄ່າເລີ່ມຕົ້ນ + ເປີດຕະຫຼອດ + ປິດຕະຫຼອດ + ເຊື່ອງແຖບສະຖານະ + ເຊື່ອງແຖບສະຖານະຂອງລະບົບ. ປັດລົງຈາກຂອບເທິງເພື່ອສະແດງມັນຊົ່ວຄາວ + ແກ້ໄຂແຖບສະຖານະທີ່ປິດບັງແຖບດ້ານເທິງ + ຮັກສາຮູບແບບຂອງແຖບສະຖານະໃຫ້ຄົງທີ່ ເພື່ອບໍ່ໃຫ້ມັນປິດບັງແຖບດ້ານເທິງ ແລະ ບໍ່ໃຫ້ມີຊ່ອງວ່າງເປົ່າຢູ່ເໜືອເຄື່ອງຫຼິ້ນ. ປິດຟັງຊັນນີ້ ຫາກແອັບມີບັນຫາ ຫຼື ແຖບດ້ານເທິງສະແດງຜົນຜິດພາດ + ແສງສະຫວ່າງ + ໜ້າຈໍເລີ່ມຕົ້ນແບບ Glow ບໍ່ຮອງຮັບໂດຍອຸປະກອນນີ້ + ພື້ນຫຼັງຂອງຮູບແບບສີອ່ອນ + ສີຂາວ + Material You (ສີຂາວ) + ສີບົວອ່ອນ + ສີຟ້າອ່ອນ + ສີຂຽວອ່ອນ + ສີເຫຼືອງອ່ອນ + ສີສົ້ມອ່ອນ + ສີແດງອ່ອນ + ພື້ນຫຼັງສີອ່ອນແບບກຳນົດເອງ + ຕັ້ງຄ່າສີພື້ນຫຼັງຂອງຮູບແບບສີອ່ອນ + ປ່ຽນສີພື້ນໜ້າ + ໃຊ້ສີຂອງຮູບແບບສຳລັບຂໍ້ຄວາມ ແລະ ໄອຄອນ + ເປີດໃຊ້ໜ້າຈໍໂຫຼດແບບໄລ່ສີ + ສະແດງພື້ນຫຼັງແບບໄລ່ສີໃນໜ້າຈໍກຳລັງໂຫລດ ແທນທີ່ຈະເປັນສີດຽວ + ເປີດໃຊ້ສີແຖບເລື່ອນທີ່ກຳນົດເອງ + ສີຂອງແຖບເລື່ອນ + ກຳນົດສີຫຼັກຂອງແຖບຄວາມຄືບໜ້າຂອງວິດີໂອ + ສີເນັ້ນຂອງແຖບເລື່ອນ + ຕັ້ງຄ່າສີສຳຮອງຂອງແຖບຄວາມຄືບໜ້າຂອງວິດີໂອ + ຄ່າສີຂອງ seekbar ບໍ່ຖືກຕ້ອງ + Analog ຕົ້ນສະບັບ + ເຄັກດັ້ງເດີມ + ໂລຫະດັ້ງເດີມ + ໂລໂກ້ສ່ວນຫົວ + ຄ່າເລີ່ມຕົ້ນ + ປົກກະຕິ + ກຳນົດເອງ + ຂ້າມການຈຳກັດພື້ນທີ່ຂອງຮູບພາບ + ປ່ຽນເສັ້ນທາງການຮ້ອງຂໍຮູບຕົວຢ່າງໄປຍັງເຊີບເວີທາງເລືອກ ເພື່ອແກ້ໄຂບັນຫາຮູບພາບທີ່ຫາຍໄປໃນປະເທດທີ່ຖືກຈຳກັດ + ຫົວຂໍ້ DeArrow + ແທັບໜ້າຫຼັກ + ແທັບການຕິດຕາມ + ແທັບທ່ານ + ລາຍການວິດີໂອ & ຄຳແນະນຳຂອງເຄື່ອງຫຼິ້ນ + ຜົນການຄົ້ນຫາ + ສະແດງໄອຄອນ DeArrow + ສະແດງໄອຄອນ DeArrow ໃນຫົວຂໍ້ວິດີໂອທີ່ມາຈາກ crowdsourced + ໂໝດແບບບໍ່ເປັນທາງການ + ສະແດງຫົວຂໍ້ດັ້ງເດີມ ແທນຫົວຂໍ້ຂອງ DeArrow ຖ້າຜູ້ໃຊ້ DeArrow ໂຫວດວ່າພວກເຂົາມັກຫົວຂໍ້ດັ້ງເດີມ + ຈຳນວນໂຫວດແບບບໍ່ເປັນທາງການຂັ້ນຕ່ຳ ເພື່ອສະແດງຫົວຂໍ້ດັ້ງເດີມ + ຈຳນວນໂຫວດແບບບໍ່ເປັນທາງການໃນໝວດໝູ່ດຽວ, ເຊັ່ນ ຕະລົກ ຫຼື ສ້າງສັນ, ທີ່ຈຳເປັນເພື່ອສະແດງຫົວຂໍ້ດັ້ງເດີມ + ຮູບຕົວຢ່າງ DeArrow + ຮູບຕົວຢ່າງຕົ້ນສະບັບ + DeArrow & ຮູບຕົວຢ່າງຕົ້ນສະບັບ + DeArrow & ຮູບທີ່ຈັບພາບໄວ້ + ຮູບພາບນິ່ງ + "DeArrow ປ່ຽນຫົວຂໍ້ ແລະ ພາບຕົວຢ່າງທີ່ເປັນ clickbait ດ້ວຍສິ່ງທີ່ໄດ້ມາຈາກ crowdsourced ເຊິ່ງມີຄວາມຖືກຕ້ອງກວ່າ ແລະ ບໍ່ເກີນຈິງ. ຫົວຂໍ້ຖືກຂຽນ ແລະ ໂຫວດໂດຍຊຸມຊົນ, ແລະ ພາບຕົວຢ່າງແມ່ນເຟຣມທີ່ຖ່າຍມາຈາກວິດີໂອ + +ວິດີໂອຖືກຮ້ອງຂໍໂດຍໃຊ້ prefix ສັ້ນໆຂອງ hashed video ID, ດັ່ງນັ້ນເຊີບເວີຈຶ່ງບໍ່ສາມາດບອກໄດ້ວ່າທ່ານກຳລັງເບິ່ງວິດີໂອໃດຢ່າງຊັດເຈນ. ຖ້າວິດີໂອບໍ່ມີພາບຕົວຢ່າງຂອງ DeArrow, ມັນຈະສະແດງອັນເດີມ ຫຼື ພາບນິ່ງທີ່ຈັບໄດ້" + ສະແດງການແຈ້ງເຕືອນຂໍ້ຜິດພາດໃນການເຊື່ອມຕໍ່ + ສະແດງການແຈ້ງເຕືອນເມື່ອເຊີບເວີ DeArrow ບໍ່ສາມາດນຳໃຊ້ໄດ້ + ຈຸດປາຍທາງ API + URL ຂອງຈຸດປາຍທາງแคຊຮູບຕົວຢ່າງ DeArrow + ການຈັບພາບວິດີໂອແບບນິ່ງ + ຍັງຄົງມີການຈັບພາບນ靜ຈາກຕອນເລີ່ມຕົ້ນ / ກາງ / ຕອນທ້າຍ ຂອງແຕ່ລະວິດີໂອ. ຮູບພາບເຫຼົ່ານີ້ຖືກສ້າງຂຶ້ນໃນ YouTube ແລະ ບໍ່ມີການໃຊ້ API ພາຍນອກ + ດຶງເຟຣມວິດີໂອຈາກ + ຈຸດເລີ່ມຕົ້ນ + ກາງ + ທ້າຍ + DeArrow ບໍ່ສາມາດໃຊ້ງານໄດ້ຊົ່ວຄາວ (ລະຫັດສະຖານະ: %s) + DeArrow ບໍ່ສາມາດໃຊ້ງານໄດ້ຊົ່ວຄາວ + ກູ້ຄືນຮູບແບບປຸ່ມເຄື່ອງຫຼິ້ນແບບເກົ່າ + ປ່ຽນປຸ່ມຄວບຄຸມເຄື່ອງຫຼິ້ນວິດີໂອແບບໜາ ໃຫ້ກັບໄປເປັນຮູບແບບທີ່ບາງກວ່າໃນສະບັບກ່ອນ + ປອມແປງຂະໜາດຂອງອຸປະກອນ + ການເປີດໃຊ້ງານສິ່ງນີ້ສາມາດປົດລັອກຄຸນນະພາບວິດີໂອທີ່ສູງຂຶ້ນ + ການເປີດໃຊ້ສິ່ງນີ້ອາດເຮັດໃຫ້ການຫຼິ້ນວິດີໂອກະຕຸກ, ແບັດເຕີຣີໝົດໄວຂຶ້ນ, ແລະ ມີຜົນກະທົບດ້ານລົບອື່ນໆທີ່ບໍ່ທັນຮູ້ + ການຕອບສະໜອງແບບສຳຜັດ + ເລືອກການໂຕ້ຕອບທີ່ຈະເຮັດໃຫ້ອຸປະກອນສັ່ນ + ປິດການຕອບສະໜອງແບບສຳຜັດຂອງບົດ + ປິດການຕອບສະໜອງແບບສຳຜັດຂອງການຄົ້ນຫາແບບລະອຽດ + ປິດການຕອບສະໜອງແບບສຳຜັດຂອງການຍົກເລີກການຄົ້ນຫາ + ປິດໃຊ້ການສັ່ນສະເທືອນເມື່ອແຕະຄ້າງ + ປິດການຕອບສະໜອງແບບສຳຜັດຂອງການຊູມ + ຖ້າທ່ານຫາກໍ່ປ່ຽນລາຍລະອຽດການເຂົ້າສູ່ລະບົບຂອງບັນຊີ, ໃຫ້ຖອນການຕິດຕັ້ງ ແລະ ຕິດຕັ້ງ MicroG ໃໝ່ + ຂ້າມການປ່ຽນເສັ້ນທາງຂອງລິ້ງ + ເປີດລິ້ງຕົ້ນສະບັບໂດຍກົງ, ໂດຍຂ້າມລິ້ງຕິດຕາມ (youtube.com/redirect) + ເປີດລິ້ງໃນບຣາວເຊີ + ເປີດລິ້ງໃນບຣາວເຊີພາຍນອກ ແທນທີ່ບຣາວເຊີພາຍໃນແອັບ + ຂະໜາດບັບເຟີການຫຼິ້ນ + ຄ່າເລີ່ມຕົ້ນ + ຕ່ຳ + ປານກາງ + ສູງສຸດ + ການໜ່ວງວິດີໂອ + ໜ່ວງວິດີໂອເພື່ອໃຫ້ສອດຄ່ອງກັບສຽງທີ່ຊ້າ, ເຊັ່ນ: ຫູຟັງ Bluetooth (0-5000 ms) + ໜ່ວງສະເພາະເວລາໃຊ້ສຽງ Bluetooth ເທົ່ານັ້ນ + ໜ່ວງວິດີໂອສະເພາະໃນຂະນະທີ່ເຊື່ອມຕໍ່ສຽງ Bluetooth ເທົ່ານັ້ນ, ເພື່ອໃຫ້ລຳໂພງໂທລະສັບຍັງຄົງສອດຄ່ອງກັນ + ສະແດງປຸ່ມໜ່ວງວິດີໂອ + ແຕະ and ຄ້າງໄວ້ເພື່ອຣີເຊັດການໜ່ວງເປັນ 0 ms + ສະແດງເມນູໜ່ວງວິດີໂອ + ການໜ່ວງເປັນມິນລິວິນາທີ + ອັດຕະໂນມັດ + ຈື່ຄຸນນະພາບວິດີໂອ + ນຳໃຊ້ການເລືອກຄຸນນະພາບດ້ວຍຕົນເອງຂອງທ່ານກັບທຸກວິດີໂອໃນອະນາຄົດ + ການແຈ້ງເຕືອນການປ່ຽນແປງຄຸນນະພາບ + ສະແດງການແຈ້ງເຕືອນເມື່ອມີການບັນທຶກຄຸນນະພາບເລີ່ມຕົ້ນໃໝ່ + ຄຸນນະພາບວິດີໂອເລີ່ມຕົ້ນໃນເຄືອຂ່າຍ Wi-Fi + ຄຸນນະພາບວິດີໂອເລີ່ມຕົ້ນໃນເຄືອຂ່າຍມືຖື + ຈື່ຄຸນນະພາບ Shorts + ນຳໃຊ້ການເລືອກຄຸນນະພາບດ້ວຍຕົນເອງຂອງທ່ານກັບທຸກໆ Shorts ໃນອະນາຄົດ + ຄຸນນະພາບ Shorts ເລີ່ມຕົ້ນໃນເຄືອຂ່າຍ Wi-Fi + ຄຸນນະພາບ Shorts ເລີ່ມຕົ້ນໃນເຄືອຂ່າຍມືຖື + ປ່ຽນຄຸນນະພາບມືຖືເລີ່ມຕົ້ນເປັນ: %s + ປ່ຽນຄຸນນະພາບ Wi-Fi ເລີ່ມຕົ້ນເປັນ: %s + ປ່ຽນຄຸນນະພາບ Shorts ມືຖືເປັນ: %s + ປ່ຽນຄຸນນະພາບ Wi-Fi ຂອງ Shorts ເປັນ: %s + ສະແດງປຸ່ມຄວາມໄວໃນການຫຼິ້ນ + ແຕະ and ຄ້າງໄວ້ເພື່ອຣີເຊັດຄວາມໄວເປັນຄ່າເລີ່ມຕົ້ນ + ສະແດງປຸ່ມຄຸນນະພາບ + ແຕະ and ຄ້າງໄວ້ເພື່ອຣີເຊັດຄຸນນະພາບເປັນຄ່າເລີ່ມຕົ້ນ + ສະແດງຄວາມລະອຽດໃນປຸ່ມຄຸນນະພາບ + ສະແດງຄວາມລະອຽດ, ເຊັ່ນ 1080, ແທນທີ່ຈະເປັນປ້າຍກຳກັບ ເຊັ່ນ FHD + ເມນູຄວາມໄວໃນການຫຼິ້ນແບບກຳນົດເອງ + ຄືນຄ່າເມນູຄວາມໄວໃນການຫຼິ້ນແບບເກົ່າ + ຄວາມໄວໃນການຫຼິ້ນແບບກຳນົດເອງ + ປ້ອນຕົວເລືອກຄວາມໄວໃນການຫຼິ້ນທີ່ທ່ານຕ້ອງການ + ຄວາມໄວໃນການຫຼິ້ນແບບກຳນົດເອງຕ້ອງໜ້ອຍກວ່າ %s + ຄວາມໄວໃນການຫຼິ້ນແບບກຳນົດເອງບໍ່ຖືກຕ້ອງ + ອັດຕະໂນມັດ + ແຕະ and ຄ້າງໄວ້ເພື່ອປັບຄວາມໄວໃນການຫຼິ້ນ + ກຳນົດຕົວຄູນຄວາມໄວເມື່ອກົດ ແລະ ຄ້າງໄວ້ໃນໜ້າຈໍ, ກຳນົດເປັນ 0 ເພື່ອປິດການນຳໃຊ້ + ຈື່ຄວາມໄວໃນການຫຼິ້ນ + ນຳໃຊ້ການເລືອກຄວາມໄວດ້ວຍຕົນເອງຂອງທ່ານກັບທຸກວິດີໂອໃນອະນາຄົດ + ການແຈ້ງເຕືອນການປ່ຽນຄວາມໄວ + ສະແດງການແຈ້ງເຕືອນເມື່ອມີການບັນທຶກຄວາມໄວໃນການຫຼິ້ນເລີ່ມຕົ້ນໃໝ່ + ຄວາມໄວໃນການຫຼິ້ນຄືນເລີ່ມຕົ້ນ + ຄວາມໄວໃນການຫຼິ້ນເລີ່ມຕົ້ນໃນ Shorts + ຈື່ຄວາມໄວໃນການຫຼິ້ນໃນ Shorts + ນຳໃຊ້ການເລືອກຄວາມໄວ Shorts ດ້ວຍຕົນເອງຂອງທ່ານໃຫ້ກັບ Shorts ທັງໝົດໃນອະນາຄົດ. ຖ້າປິດ, ຄວາມໄວຈະກັບຄືນເປັນຄ່າເລີ່ມຕົ້ນຫຼັງຈາກປິດເຄື່ອງຫຼິ້ນ Shorts + ສະແດງຄວາມໄວໃນການຫຼິ້ນໃນເມນູ Shorts + ເພີ່ມລາຍການຄວາມໄວໃນການຫຼິ້ນລົງໃນເມນູເຄື່ອງຫຼິ້ນ Shorts + ຄວາມໄວໃນການຫຼິ້ນ · %s + ປ່ຽນຄວາມໄວໃນການຫຼິ້ນເລີ່ມຕົ້ນເປັນ: %s + ໃຊ້ຄວາມໄວປົກກະຕິສຳລັບເພງ + ຫຼິ້ນວິດີໂອເພງດ້ວຍຄວາມໄວ 1.0x ສະເໝີ + ລາຍຊື່ຂາວຄວາມໄວຂອງຊ່ອງ + ວິດີໂອຈາກຊ່ອງທີ່ຢູ່ໃນລາຍຊື່ຂາວຈະຫຼິ້ນດ້ວຍຄວາມໄວ 1.0x ສະເໝີ + ເພີ່ມຊ່ອງເຂົ້າໃນລາຍຊື່ຂາວຄວາມໄວ + ລຶບຊ່ອງອອກຈາກລາຍຊື່ຂາວຄວາມໄວ + ເປີດໃຊ້ການຄວບຄຸມລະດັບສຽງ + ລະດັບສຽງຫຼິ້ນຕາມຄ່າເລີ່ມຕົ້ນ + ປ່ຽນລະດັບສຽງເລີ່ມຕົ້ນເປັນ: %s + ການຍືດເວລາຂອງສຽງ + ອະນຸຍາດໃຫ້ຕັ້ງຄ່າຄວາມໄວ ແລະ ລະດັບສຽງແຍກກັນ, ເຊິ່ງອາດເຮັດໃຫ້ເກີດສຽງລົບກວນ + ຈື່ຕຳແໜ່ງການຫຼິ້ນການຖ່າຍທອດສົດ + ສຳລັບການຖ່າຍທອດສົດທີ່ກຳລັງດຳເນີນຢູ່, ຈະຈື່ ແລະ ເລີ່ມຫຼິ້ນຕໍ່ຈາກຕຳແໜ່ງທີ່ທ່ານຢຸດໄວ້ ແທນທີ່ຈະໄປທີ່ຕຳແໜ່ງສົດໃນປັດຈຸບັນ + ຟີເຈີນີ້ບໍ່ເຮັດວຽກກັບການຖ່າຍທອດສົດ 24/7 ແລະ ການຖ່າຍທອດສົດອື່ນໆທີ່ \'ເປີດໄວ້ຕະຫຼອດ\' + ປິດການໃຊ້ງານວິດີໂອ HDR + ບັງຄັບວິດີໂອ HDR + ບັງຄັບໃຫ້ແອັບຮັບຮູ້ວ່າອຸປະກອນນີ້ຮອງຮັບ HDR + "ໃຊ້ສິ່ງນີ້ຖ້າການຖອດລະຫັດ HDR ໃຊ້ງານໄດ້ ແຕ່ YouTube ບໍ່ສະເໜີ HDR, ເຊັ່ນ ອຸປະກອນ HyperOS ບາງລຸ້ນທີ່ເຊື່ອງ HLG. + +ຂໍ້ຈຳກັດ: +• ສີອາດຈະຜິດພ້ຽນ ຖ້າໜ້າຈໍບໍ່ສາມາດສະແດງ HDR ໄດ້ແທ້ +• ການຫຼິ້ນອາດຈະລົ້ມເຫຼວ ຖ້າຕົວຖອດລະຫັດບໍ່ສາມາດຈັດການ HDR ໄດ້ +• HDR ຍັງຂຶ້ນກັບການປອມແປງ stream ວິດີໂອຂອງ client" + ໂຄເດັກວິດີໂອ + ຄ່າເລີ່ມຕົ້ນ + ເພື່ອບັງຄັບໃຊ້ AVC, ໃຫ້ປ່ຽນ \'ປອມແປງສະຕຣີມວິດີໂອ\' ເປັນ \'Android Studio\', \'Android VR\' ຫຼື \'visionOS\' + "VP9 +• ສາມາດປັບປຸງອາຍຸການໃຊ້ງານແບັດເຕີຣີ ແລະ ແກ້ໄຂການຕິດຂັດຂອງການຫຼິ້ນ +• ໃຊ້ຂໍ້ມູນອິນເຕີເນັດຫຼາຍກວ່າ AV1 + +AVC (H.264) +• ຕ້ອງໃຊ້ໄຄລຽນ ປອມແປງສະຕຣີມວິດີໂອ 'Android Studio', 'Android VR', ຫຼື 'visionOS' +• ສາມາດປັບປຸງອາຍຸການໃຊ້ງານແບັດເຕີຣີໄດ້ຫຼາຍຂຶ້ນຕື່ມອີກ +• ສາມາດກູ້ຄືນຄວາມລະອຽດຂອງວິດີໂອທີ່ຫາຍໄປໃນອຸປະກອນລຸ້ນເກົ່າ +• ຄວາມລະອຽດສູງສຸດແມ່ນ 1080p +• ວິດີໂອ HDR ອາດຈະບໍ່ໃຊ້ AVC +• ອຸປະກອນບາງລຸ້ນບໍ່ສາມາດບັງຄັບໃຊ້ AVC ໄດ້" + ເປີດເມນູຄຸນນະພາບຂັ້ນສູງ + ຂ້າມເມນູເລືອກຄຸນນະພາບ ແລະ ເປີດເມນູຄຸນນະພາບຂັ້ນສູງໂດຍກົງ + ເຊື່ອງຕົວເລືອກຄຸນນະພາບ Premium + ໃຫ້ບຸລິມະສິດດ້ານຄຸນນະພາບຂອງວິດີໂອ + ໃຫ້ບຸລິມະສິດຄຸນນະພາບວິດີໂອທີ່ສູງທີ່ສຸດທີ່ມີຢູ່ ເໜືອກວ່າປະສິດທິພາບຂອງ codec + ຖ້າປິດການຕັ້ງຄ່ານີ້, ວິດີໂອເກົ່າອາດຈະບໍ່ສະແດງຕົວເລືອກຄຸນນະພາບຂອງວິດີໂອ + ອະນຸຍາດ \'Android VR\' / visionOS AV1 + ອະນຸຍາດການຖອດລະຫັດ AV1 ດ້ວຍຊອບແວ ເມື່ອໃຊ້ຄລາຍເອນ \'Android VR\' / visionOS + "ການເປີດໃຊ້ການຕັ້ງຄ່ານີ້ອາດຈະໃຊ້ການຖອດລະຫັດ AV1 ດ້ວຍຊອບແວ. + +ການຫຼິ້ນວິດີໂອດ້ວຍ AV1 ອາດຈະກະຕຸກ ຫຼື ມີການຕົກຂອງເຟຣມ" + ຜົນກະທົບທາງລົບຂອງການປອມແປງ + • ການຫຼິ້ນອາດຈະລົ້ມເຫຼວໃນນາທີທີ 1 + • ບໍ່ມີເມນູ \'ແທຣັກສຽງ\' + • ບໍ່ມີຕົວຖອດລະຫັດວິດີໂອ AV1 + • ໂໝດ VR immersive 360° ບໍ່ສາມາດນຳໃຊ້ໄດ້ + • ບໍ່ມີລະດັບສຽງທີ່ຄົງທີ່ + • ການຫຼິ້ນອາດຈະສິ້ນສຸດລົງກ່ອນ 1 ວິນາທີ + • ຖ້າການຖອດລະຫັດລົ້ມເຫຼວ, ການຫຼິ້ນອາດຈະລົ້ມເຫຼວ + • ບໍ່ມີການບັງຄັບໃຊ້ສຽງດັ້ງເດີມ + • ຈໍາເປັນຕ້ອງໄດ້ລົງຊື່ເຂົ້າໃຊ້ Android VR + • ຄຸນນະພາບອາດຈະຖືກຈຳກັດຢູ່ທີ່ 720p + ຮຸ່ນ JavaScript ຂອງເຄື່ອງຫຼິ້ນ + ສະແດງການປອມແປງໃນ \'ສະຖິຕິສຳລັບນັກຮຽນຮູ້\' + ຄືນຄ່າແຖບການຈັດການວິດີໂອແບບເກົ່າ + "ແກ້ໄຂປຸ່ມການຈັດການວິດີໂອທີ່ບໍ່ສາມາດເຊື່ອງໄດ້ + +ຂໍ້ຈຳກັດ: ກັບໄປໃຊ້ຮູບແບບເກົ່າຂອງການວາງຊ້ອນ 'ວິດີໂອເພີ່ມເຕີມ'" + ຮູບແບບໄອຄອນຂອງເຄື່ອງຫຼິ້ນ + ອັດຕະໂນມັດ + ຄ່າເລີ່ມຕົ້ນ + ບາງ + ເຂັ້ມ + ກຳນົດເອງ + ຮູບແບບໄອຄອນ Shorts + ເຄື່ອງຕິດຕາມຊີຣີ + ຕິດຕາມລາຍການຫຼິ້ນເປັນຊີຣີ ແລະ ບັນທຶກຈຸດທີ່ທ່ານເບິ່ງຄ້າງໄວ້ + ເປີດໃຊ້ການຕິດຕາມຊີຣີ + ເພີ່ມໜ້າຊີຣີໃນປະຫວັດ ແລະ ຕົວເລືອກຕິດຕາມຊີຣີໃຫ້ກັບລາຍການຫຼິ້ນ + ສະແດງຊຸດລາຍການໃນແຖບນຳທາງ + ສະແດງປຸ່ມເຄື່ອງຕິດຕາມຊີຣີ + ເພີ່ມປຸ່ມໃນເຄື່ອງຫຼິ້ນເພື່ອຕິດຕາມ ຫຼື ເປີດຊີຣີຂອງວິດີໂອປັດຈຸບັນ + ບັນທຶກຄວາມຄືບໜ້າຂອງຊີຣີທີ່ຕິດຕາມ + ບັນທຶກການຫຼິ້ນໃນໂທລະສັບເຄື່ອງນີ້ສຳລັບຊີຣີ ແລະ ບຸກມາກທີ່ບັນທຶກໄວ້ + ຍົກເລີກ + ບັນທຶກ + ບັນທຶກບຸກມາກ + ຊື່ຊີຣີ + ຕົວເລືອກຊີຣີ + ຕົວເລືອກຫ້ອງສະໝຸດ + ຣີເຟຣຊຕອນ + ປ່ຽນຊື່ + ເປີດເພລລິສໃນ YouTube + ລຶບອອກຈາກຫ້ອງສະໝຸດ + ລິ້ງ ຫຼື ID ຂອງເພລລິສ + ລຶບຄວາມຄືບໜ້າທ້ອງຖິ່ນທັງໝົດ + ລຶບປະຫວັດ + ລຶບຄວາມຄືບໜ້າທ້ອງຖິ່ນທັງໝົດບໍ? + ສິ່ງນີ້ຈະລຶບຕຳແໜ່ງ, ການປັບແກ້ການເບິ່ງ ແລະ ການເລືອກຫຼິ້ນຕໍ່ສຳລັບທຸກວິດີໂອ. ຊີຣີທີ່ບັນທຶກໄວ້ ແລະ ແຄັດຕາລັອກຂອງພວກມັນຈະຍັງຄົງຢູ່. ການຫຼິ້ນຄັ້ງໃໝ່ຈະຖືກບັນທຶກຖ້າຫາກການບັນທຶກຖືກເປີດໃຊ້ + ໝາຍວ່າເບິ່ງແລ້ວ + ໝາຍວ່າບໍ່ທັນໄດ້ເບິ່ງ + ໃຊ້ສະຖານະອັດຕະໂນມັດ + ເລີ່ມໃໝ່ຈາກ 0:00 + ຫຼິ້ນອີກຄັ້ງຈາກ 0:00 + ບັນທຶກສະຖານະແລ້ວ + ລອງບັນທຶກໃໝ່ + 100 ກ່ອນໜ້ານີ້ + 100 ອັນຖັດໄປ + ເປີດວິດີໂອທີ່ບັນທຶກໄວ້ + ລ້າງຄວາມຄືບໜ້າທ້ອງຖິ່ນແລ້ວ + ເປີດປະຫວັດຈາກ ທ່ານ ເພື່ອເບິ່ງຊີຣີຂອງທ່ານ + ປະຫວັດການເບິ່ງ + ຊີຣີ + ກຳລັງໂຫລດຊີຣີທີ່ບັນທຶກໄວ້… + ຕິດຕາມລາຍການຫຼິ້ນເພື່ອຕິດຕາມຕອນຕ່າງໆຂອງມັນ + ບຸກມາກເພລລິສ + ກຳລັງອັບເດດຕອນຕ່າງໆ… + ທຸກຊີຣີ + ເບິ່ງແລ້ວ + ກຳລັງໂຫຼດຕອນ... + ຕົວເລືອກເພີ່ມເຕີມ + "ບັນທຶກຄວາມຄືບໜ້າສະເພາະຊີຣີທີ່ທ່ານບັນທຶກໄວ້ ແລະ ວິດີໂອທີ່ໝາຍບຸກມາກໄວ້ເທົ່ານັ້ນ. + +ຫໍສະໝຸດນີ້ຖືກເກັບໄວ້ໃນອຸປະກອນນີ້ ແລະ ແບ່ງປັນໃນບັນຊີ YouTube ຕ່າງໆ. ການຄວບຄຸມການຢຸດຊົ່ວຄາວ ແລະ ການລຶກປະຫວັດຂອງ YouTube ຈະບໍ່ປ່ຽນແປງມັນ. ໃຊ້ ຊີຣີ → ອື່ນໆ → ລຶບຄວາມຄືບໜ້າທ້ອງຖິ່ນ ເພື່ອລຶບມັນ. + +ການບັນທຶກອັດຕະໂນມັດຈະຢຸດລົງໃນໂໝດບໍ່ລະບຸຕົວຕົນ, ເມື່ອບໍ່ສາມາດລະບຸຕົວຕົນຂອງບັນຊີໄດ້, ຫຼື ເມື່ອທ່ານປ່ຽນໄປໃຊ້ບັນຊີອື່ນ. ເປີດໃຊ້ການຕິດຕາມອີກຄັ້ງເພື່ອໃຊ້ງານກັບບັນຊີນັ້ນ" + ບໍ່ສາມາດເປີດໃຊ້ການຕິດຕາມໃນໂໝດບໍ່ລະບຸຕົວຕົນ ຫຼື ກ່ອນທີ່ YouTube ຈະໂຫຼດສະຖານະບັນຊີປັດຈຸບັນ. ກະລຸນາກັບໄປທີ່ໜ້າຫຼັກ ແລະ ລອງໃໝ່ອີກຄັ້ງ. ບໍ່ຈຳເປັນຕ້ອງລົງຊື່ເຂົ້າໃຊ້ + ເປີດໃຊ້ການຕິດຕາມທ້ອງຖິ່ນ + ການບັນທຶກຄວາມຄືບໜ້າຖືກປິດຢູ່ + ດຳເນີນການຕໍ່ທີ່ %1$s + ເບິ່ງໄປແລ້ວ %1$s / %2$s ຕອນ + ຕອນທີ %1$s · %2$s + ຕອນທີ %1$s-%2$s ຈາກ %3$s + ຣີເຟຣຊເພື່ອລອງໂຫຼດຕອນຕ່າງໆ. ພລິສເຕລໄຊສ່ວນຕົວຕ້ອງມີການເຂົ້າເຖິງບັນຊີ; ແຕ່ບຸກມາກຂອງພວກມັນຍັງຄົງເປີດໄດ້ໃນ YouTube + YouTube ບໍ່ສາມາດເປີດຕອນນີ້ໄດ້ + YouTube ບໍ່ສາມາດເປີດລາຍການຫຼິ້ນນີ້ໄດ້ + ເລືອກຕອນ + ເປັນປັດຈຸບັນແລ້ວ + ບໍ່ມີຕອນທີ່ສາມາດຫຼິ້ນໄດ້ + ວິດີໂອທີ່ບັນທຶກໄວ້ · %1$s + ຕິດຕາມຊີຣີ + ເບິ່ງຊີຣີ + ສືບຕໍ່ + ເລີ່ມຈາກບ່ອນນີ້ + ບັນທຶກຈຸດເລີ່ມຕົ້ນແລ້ວ + ເບິ່ງມາຮອດບ່ອນນີ້ + ໝາຍວ່າເບິ່ງຕອນເຫຼົ່ານີ້ແລ້ວ + ຍົກເລີກ + ເປີດໃຊ້ + 1 ຕອນໃໝ່ + %1$s ຕອນໃໝ່ + ບັນທຶກຄວາມຄືບໜ້າໃນອຸປະກອນນີ້ + "ຊີຣີ ແລະ ຄວາມຄືບໜ້າຂອງທ່ານຈະຖືກເກັບໄວ້ໃນອຸປະກອນນີ້, ເຊິ່ງຖືກແບ່ງປັນລະຫວ່າງບັນຊີ YouTube. ການຄວບຄຸມປະຫວັດຂອງ YouTube ຈະບໍ່ປ່ຽນແປງສິ່ງເຫຼົ່ານີ້. ລຶບຄວາມຄືບໜ້າອອກຈາກເມນູຊີຣີ. + +ການບັນທຶກອັດຕະໂນມັດແມ່ນທາງເລືອກ. ມັນຈະຢຸດເຮັດວຽກໃນໂໝດບໍ່ລະບຸຕົວຕົນ ຫຼື ເມື່ອມີການປ່ຽນບັນຊີ" + ບໍ່ສາມາດໂຫຼດຕອນໄດ້. ທ່ານສາມາດບັນທຶກລາຍການຫຼິ້ນເປັນບຸກມາກໄດ້ + ມີການປ່ຽນແປງທີ່ຍັງບໍ່ທັນໄດ້ບັນທຶກຫຼາຍເກີນໄປ. ກະລຸນາລອງບັນທຶກຄືນໃໝ່ກ່ອນ + ກຳລັງດຳເນີນການບັນທຶກ. ກະລຸນາລອງໃໝ່ + ກຳລັງດຳເນີນການໂຫຼດຕອນ. ກະລຸນາລອງໃໝ່ + ບໍ່ສາມາດນຳໃຊ້ບ່ອນເກັບຂໍ້ມູນໄດ້. ຂໍ້ມູນທີ່ບັນທຶກໄວ້ຂອງທ່ານຖືກເກັບຮັກສາໄວ້ + ບໍ່ສາມາດບັນທຶກການປ່ຽນແປງໄດ້. ກະລຸນາລອງບັນທຶກຄືນໃໝ່ກ່ອນປິດ YouTube + ຊີຣີນີ້ຖືກບັນທຶກໄວ້ແລ້ວ, ຫຼື ພລິສລິດຂອງມັນມີການປ່ຽນແປງ. ກະລຸນາເປີດມັນຄືນໃໝ່ + ຊີຣີນີ້ມີການປ່ຽນແປງ ຫຼື ຖືກລຶບອອກ. ກະລຸນາເປີດຄັງເກັບຂໍ້ມູນຄືນໃໝ່ + ພລິສລິດມີການປ່ຽນແປງ. ກະລຸນາເລືອກຕອນອີກຄັ້ງ + ຕອນນີ້ບໍ່ສາມາດນຳໃຊ້ໄດ້. ກະລຸນາເລືອກຕອນອື່ນ + ກະລຸນາປ້ອນຊື່ຊີຣີ + ກະລຸນາປ້ອນລິ້ງພລິສລິດ YouTube ຫຼື ID ຂອງພລິສລິດ + ກະລຸນາລໍຖ້າກ່ອນທີ່ຈະຣີເຟຣຊອີກຄັ້ງ + ບໍ່ສາມາດໂຫຼດຕອນໄດ້. ກະລຸນາລອງໃໝ່ໃນພາຍຫຼັງ + ບໍ່ສາມາດດຳເນີນການປ່ຽນແປງໄດ້ສຳເລັດ. ກະລຸນາລອງໃໝ່ + ບໍ່ສາມາດຢືນຢັນການເບິ່ງຕໍ່ໄດ້. ກະລຸນາກວດສອບເຄື່ອງຫຼິ້ນ ແລະ ລອງໃໝ່ + ກຳລັງຕິດຕາມຊີຣີ + ວິດີໂອນີ້ບໍ່ຢູ່ໃນລາຍການຫຼິ້ນນີ້ + ກຳລັງຊອກຫາເພລລິສ... + ວາງລິ້ງເພລລິສ + ບໍ່ພົບເພລລິສ + ບໍ່ມີຕອນທີ່ສາມາດຮັບຊົມໄດ້ + ເຊື່ອງຕອນທີ່ເບິ່ງແລ້ວ + ໝາຍຕອນກ່ອນໜ້ານີ້ວ່າເບິ່ງແລ້ວ + ເລີ່ມຕົ້ນດ້ວຍ: %1$s + ລຳດັບຕອນ + ລຳດັບເພລລິສ + ປີ້ນກັບລຳດັບເພລລິສ + %1$s ຄັ້ງຂອງການຮັບຊົມ + %1$s ວິວ + ໝາຍຕອນທີ່ເບິ່ງແລ້ວ + ເບິ່ງແລ້ວຢ່າງໜ້ອຍ %1$s%% ໂດຍມີເວລາເຫຼືອບໍ່ເກີນ 90 ວິນາທີ, ຫຼື ເຫຼືອເວລາ %2$s ວິນາທີ + ໝາຍຕອນວ່າເບິ່ງແລ້ວໂດຍອັດຕະໂນມັດຫຼັງຈາກຮອດເກນໃດໜຶ່ງ. ກົດເກນເປີເຊັນຍັງກຳນົດໃຫ້ເຫຼືອເວລາບໍ່ເກີນ 90 ວິນາທີ ເພື່ອບໍ່ໃຫ້ຂ້າມຕອນຈົບຂອງຕອນທີ່ຍາວ. ກົດເກນເວລາທີ່ເຫຼືອແມ່ນໃຊ້ກັບວິດີໂອທີ່ຍາວເກີນໜຶ່ງນາທີ; ຕັ້ງເປັນ 0 ເພື່ອປິດການໃຊ້ງານ. ການປ່ຽນແປງຈະມີຜົນຕໍ່ການອັບເດດຄວາມຄືບໜ້າໃນອະນາຄົດ. ທາງເລືອກເບິ່ງແລ້ວ/ຍັງບໍ່ທັນໄດ້ເບິ່ງ ທີ່ເລືອກດ້ວຍຕົນເອງຈະຖືກຮັກສາໄວ້ + ເປີເຊັນທີ່ເບິ່ງແລ້ວ (1-100) + ວິນາທີທີ່ເຫຼືອ (0-300; 0 ແມ່ນປິດການໃຊ້ງານ) + ປ້ອນຄ່າຈາກ %1$s ຫາ %2$s + ໃຊ້ຄວາມຄືບໜ້າຂອງ YouTube + ຕິດຕາມຊີຣີທີ່ຕິດຕາມໄວ້ ຫຼັງຈາກເບິ່ງໃນອຸປະກອນອື່ນ + ອ່ານສະຖານະການເບິ່ງ ແລະ ຕຳແໜ່ງການເບິ່ງຕໍ່ສຳລັບຊີຣີທີ່ທ່ານຕິດຕາມຈາກບັນຊີ YouTube ປັດຈຸບັນ. ພຽງແຕ່ຕອນທີ່ກົງກັນເທົ່ານັ້ນທີ່ຈະຖືກບັນທຶກໄວ້ໃນໂທລະສັບເຄື່ອງນີ້. ການກວດສອບຈະເຮັດວຽກໃນຂະນະທີ່ຄັງຊີຣີຖືກເປີດ ແລະ ກ່ອນການດຳເນີນການຕໍ່. ປະຫວັດ YouTube ຕ້ອງໄດ້ຖືກເປີດໃຊ້ງານໃນອຸປະກອນອື່ນຂອງທ່ານ + ກຳລັງກວດສອບຄວາມຄືບໜ້າຂອງ YouTube… + ບໍ່ສາມາດນຳໃຊ້ຄວາມຄືບໜ້າຂອງ YouTube ໄດ້. ພວມນຳໃຊ້ຄວາມຄືບໜ້າທີ່ບັນທຶກໄວ້ + ຄວາມຄືບໜ້າຂອງ YouTube ບາງສ່ວນບໍ່ສາມາດນຳໃຊ້ໄດ້. ຕຳແໜ່ງທີ່ບັນທຶກໄວ້ຈະຖືກເກັບຮັກສາໄວ້ + ສຳຮອງ ແລະ ກູ້ຄືນ + ບັນທຶກຄັງຊີຣີຂອງທ່ານລົງໃນໄຟລ໌ ຫຼື ກູ້ຄືນຂໍ້ມູນທີ່ສຳຮອງໄວ້ + ບັນທຶກຊີຣີທີ່ຕິດຕາມ, ຕຳແໜ່ງຂອງຕອນ ແລະ ທາງເລືອກການເບິ່ງ. ການອະນຸຍາດບັນທຶກ ແລະ ຄວາມຄືບໜ້າຂອງ YouTube ບໍ່ໄດ້ລວມຢູ່ໃນນີ້ + ບັນທຶກການສຳຮອງ + ກູ້ຄືນການສຳຮອງ + ເພີ່ມ %1$s ຊີຣີ. %2$s ຊີຣີທີ່ມີຢູ່ໃນຄັງຂອງທ່ານແລ້ວຈະຖືກເກັບຮັກສາໄວ້. ຄວາມຄືບໜ້າຂອງວິດີໂອທີ່ມີຢູ່ຈະຖືກເກັບຮັກສາໄວ້ + ບັນທຶກການສຳຮອງຊີຣີແລ້ວ + ກູ້ຄືນ %1$s ຊີຣີແລ້ວ + ໄຟລ໌ນີ້ບໍ່ແມ່ນການສຳຮອງຂໍ້ມູນຊຸດທີ່ຮອງຮັບ, ຫຼື ມີຂະໜາດໃຫຍ່ເກີນໄປ. ຫ້ອງສະໝຸດຂອງທ່ານບໍ່ມີການປ່ຽນແປງ + ບໍ່ສາມາດບັນທຶກການສຳຮອງຂໍ້ມູນໄດ້. ກະລຸນາລອງໃຊ້ຕຳແໜ່ງອື່ນ + ລົບຊີຣີສ໌ອອກແລ້ວ + ຕອນນີ້ປາກົດຂຶ້ນຫຼາຍກວ່າໜຶ່ງຄັ້ງ. ເລືອກຕອນທີ່ທ່ານຕ້ອງການຈະສືບຕໍ່ + ຕອນທີ່ທ່ານບັນທຶກໄວ້ບໍ່ມີຢູ່ໃນລາຍການຫຼິ້ນນີ້ອີກຕໍ່ໄປ. ຕຳແໜ່ງທີ່ບັນທຶກໄວ້ຂອງທ່ານຍັງຄົງຢູ່ + ສືບຕໍ່ດ້ວຍຕອນ %1$s · %2$s + ທ່ານຍັງສາມາດເລືອກຕອນໃດກໍໄດ້ລຸ່ມນີ້. ຕອນກ່ອນໜ້ານີ້ຈະບໍ່ມີການປ່ຽນແປງ + ລາຍການຫຼິ້ນນີ້ໄດ້ປ່ຽນແປງ. ເປີດຊຸດເພື່ອເລືອກຈຸດທີ່ຈະສືບຕໍ່ + ບໍ່ມີຕອນອື່ນອີກໃນທິດທາງນີ້ + ສືບຕໍ່ %1$s + ຄົ້ນຫາຊີຣີສ໌ + ລ້າງການຄົ້ນຫາ + ບໍ່ພົບຊີຣີສ໌ + ຕອນທີ່ບໍ່ມີຊື່ + ຕອນທີ່ບໍ່ສາມາດນຳໃຊ້ໄດ້ + ຄຳຄິດເຫັນແບບ Niconico + ສະແດງແຊັດສົດ ແລະ ການຫຼິ້ນແຊັດຄືນໃໝ່ເປັນຄຳຄິດເຫັນທີ່ເລື່ອນຜ່ານວິດີໂອ + ສະແດງປຸ່ມຊ້ອນທັບໃນເຄື່ອງຫຼິ້ນວິດີໂອ. ແຕະເພື່ອສະແດງ ຫຼື ຊ່ອນຄຳຄິດເຫັນ, ແຕະຄ້າງໄວ້ສຳລັບການຕັ້ງຄ່າແບບດ່ວນ + ຂະໜາດຕົວອັກສອນຂອງຄຳຄິດເຫັນ + ຂະໜາດຕົວອັກສອນຂອງຄຳຄິດເຫັນ (12-40 sp) + ຄວາມໂປ່ງໃສຂອງຄຳຄິດເຫັນ + ຄວາມໂປ່ງໃສຂອງຂໍ້ຄວາມຄຳຄິດເຫັນ ແລະ ພື້ນຫຼັງ + ໄລຍະເວລາໃນການເຄື່ອນທີ່ + ຈຳນວນວິນາທີທີ່ຄຳຄິດເຫັນໃຊ້ໃນການເລື່ອນຜ່ານໜ້າຈໍ. ຄ່າທີ່ຕ່ຳກວ່າຈະເຄື່ອນທີ່ໄວຂຶ້ນ + ຄວາມຍາວສູງສຸດຂອງຄຳຄິດເຫັນ + ເຊື່ອງຄຳຄິດເຫັນທີ່ມີຕົວອັກສອນຫຼາຍກວ່ານີ້. ແຕ່ລະ emoji ຖືກນັບເປັນໜຶ່ງຕົວອັກສອນ + ບໍ່ມີຂີດຈຳກັດ diff --git a/patches/src/main/resources/addresources/values-lt-rLT/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-lt-rLT/shared-youtube/strings.xml index 60f93a1943..a9b0e03764 100644 --- a/patches/src/main/resources/addresources/values-lt-rLT/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-lt-rLT/shared-youtube/strings.xml @@ -21,9 +21,9 @@ Second \"item\" text" Paleidimo animacija Išjungta Spalva - Spalvota Morphe Vienspalvė - Monochromatinė Morphe + + Išjungta Paleiskite iš naujo, kad užbaigtumėte sąranką diff --git a/patches/src/main/resources/addresources/values-lt-rLT/youtube/strings.xml b/patches/src/main/resources/addresources/values-lt-rLT/youtube/strings.xml index 9973cb2560..14fa40547d 100644 --- a/patches/src/main/resources/addresources/values-lt-rLT/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-lt-rLT/youtube/strings.xml @@ -113,6 +113,9 @@ Pakeitimai apima: Slėpti apklausas Slėpti apklausas ir raginimus apie kanalo rekomendacijų tikslumą Slėpti bilietų lentyną + + Slėpti bannerį „Nepriejamos videoklipai yra slėpti“ + Slėpia bannerį „Nepriejamos videoklipai yra slėpti“ grožiuose Slėpti įkėlimo laiką "Apribojimai: @@ -1249,6 +1252,9 @@ Apribojimas: jei parinktis „Atkurti senąją vaizdo įrašo veiksmų juostą Laiko būsenos juostos išdėstymą nuoseklų, kad ji nengeltų viršutinių juostų ir nepaliktų tuščios vietos virš grojimo įrenginio. Išjunkite šią parinktį, jei programėlė žlunga arba viršutinės juostos atrodo netinkamai Švytėjimas + Morphe Glow 1 + Morphe Glow 2 + Šis įrenginys nepalaiko švytinčio įvadimo ekrano stiliaus Šviesios temos fonas Balta Material You (balta) diff --git a/patches/src/main/resources/addresources/values-lv-rLV/music/strings.xml b/patches/src/main/resources/addresources/values-lv-rLV/music/strings.xml index 1014dcdfc8..2c0cdfa143 100644 --- a/patches/src/main/resources/addresources/values-lv-rLV/music/strings.xml +++ b/patches/src/main/resources/addresources/values-lv-rLV/music/strings.xml @@ -17,72 +17,673 @@ Second \"item\" text" --> + Pēdējais atskaņotais Abonementi + Video + Citi rezultāti + Augšupielādētas dziesmas + Augšupielādēti albumi + Augšupielādēti izpildītāji + Atjauniniet uzņēmēja Jam 패čs, lai izmantotu pauzes un atsākšanas funkcijas + Jam rindas kopīgošana + Kļūvieties par uzņēmēju vai pievienojieties tuvējam Jam un redizējiet tā navas Jam slāņā + Iespējot Jam rindas kopīgošanu + Iespējo Jam integrāciju pēc YouTube Music restartēšanas + Lejupielādējiet Jam Layer, lai sāktu darbi + Atveriet jaunāko Jam slāņa izlaidī savā pārlūkprogrammā + Jam vadība + Kļuviet par Jam viessaimnieku vai pievienojieties tuvējā Jam un pārvaldiet sesiju + Jam Layer pakete + Izvēlieties instalēto pavadītāja lietotni, ko izmantot tuvējā rindas kopīgošanai + Izmantojiet instalēto papildpakotni. Pārslēgšanai nepieciešama jūsu apstiprinājums un vietējais iespēju tokens + Ieslēgt Wi-Fi? + Iespēciet Wi-Fi, pēc tam mēģiniet vēlreiz başlatit vai pievienoties Jam sesijai + Atvērt Wi-Fi iestatījumus Atcelt + Izmantot paketi + Ievadiet derīgu Android paketes nosaukumu + Pakotne saglabāta. Konfigurējiet Jam Layer, lai izveidotu savienojumu + Jam atvienots + Vispirms iespējojiet Jam Layer + Iespējojiet Jam rindas kopīgošanu iestatījumos, pēc tam restartējiet YouTube Music + Vispirms instalējiet izvēlēto Jam Layer paketi + Ievadiet hostā redzamā koda. Wi-Fi Aware var savienot tuvumā esošas ierīces, pat ja to vietējie tīkli atšķiras + Pievienoties ar kodu + Pievienoties + Skenēt QR + Ievadiet astu rakstzīmju kodu vai ielīmējiet ielūgumu + Pievienojieties savam Jam + Kopēt kodu + Jam koda + Rādīt QR + Gatavs + Uzaicināt uz savu Jam + Noskenējiet, lai pievienotos un redzētu šo rindu. Beidzot Jam sesiju atceļ ielūgumu + Kopēt saiti + Jam uzaicinājums + Jam tiek atkārtoti savienots; mēģiniet vēlreiz pēc brīža + Pievienots Jam + Jam nav pieejams; trekjs netika pievienots + Tavs Jam + Klausāmies kopā + Pievienošanās Jam sesijai + Klausīties kopā + Jam sesijas atjaunināšana… + Meklējam jūsu vieshôte Wi-Fi tīklā… + Gaidām cilvēkus + Pievienojusies %s persona + Pievienojušies %s cilvēki + Ievadiet šo kodu tuvumā esošajā ierīcē. Wi-Fi Aware darbojas dažādos vietējos tīklos, ja tas tiek atbalstīts. Nosacījumu derīgums beidzas pēc %s minūtēm + Kustīties uz %s? + Labojumi atvērti + Labojumi bloķēti + Savienots caur Wi-Fi Aware + Savienots caur Wi-Fi + Savienots caur Bluetooth + Neatbalstīts Jam sesijas laikā + Pievienošanās atcelta + Atgrīžas pie jūsu vadītāja… + Dalies ar rindā. Ļauj visiem pievienot dziesmas + Uzaicināt cilvēkus + Viesu var rediģēt rindu + Beigt Jam sesiju + Mūziku atskaņo vieshôte. Pievieno, pārvieto vai dzēš dziesmas tieši YouTube Music rindā + Turi abu ierīces tuvumā. Varat atcelt, kamēr mēs meklējam vieshoti + Atcelt pievienošanos + Pamest Jamu + Sākt Jam sesiju + Iestatīt Jam slāni + Pārvaldīt + Sīkāk Atvērt + Ielādē dziesmas iespējas… + Dziesmas iespējas nav pieejamas; mēģiniet vēlreiz + Neizdevās atvērt dziesmas iespējas + Nav pieejama pārlūka, lai lejupielādētu Jam Layer + Gaidām uz vieshôteja rindu + Vispirms atvēriet dziesmu YouTube Music + Atskaņot vieshôtejam + Iziet no Jam un atskaņot lokāli + Atskaņot nākamo Jam sesijā + Pievienot Jam rindai + Atskaņot šo Jam izpildījumu + Izvēlieties, kur atskaņot + Pārcelt uzņēmēju šurp + Dziesma neguva gatavību pārvēršanai + Navas changed; mēģiniet žestu vēlreiz + Augšējās lentes logotips + Noklusējums + Pielagots + Atskaņot albuma audio trekus + Atskaņot dziesmu albuma audio versiju vietā mūzikas video. Nepieciešams iespazot video straumes + I iespējot audio/video pārslēgumu + Mēģiniet pārslēgties starp audio izgaubzes versiju un oficiālo mūzikas video + Lejupielādēt mūzikas video, podkastus vai dziesmas, izmantojot trešo pušu lejupielādes lietotnes + Izmantot ārējo lejupielādētāju Lejupielādēt izvēlnē + Izvēlnes Lejupielādēt opcija tā vietā atvērs jūsu ārējo lejupielādētāju + Lejupielādē + "Nav lejupielādēšanām +Lejupielādētās dziesmas parādīsies šeit" + %1$s • %2$s • %3$s MB + Meklēt lejupielādētos failos + Kārtot + Kārtot pēc izpildītāja + Kārtot pēc nosaukuma + Kārtot pēc izmēra + Dzēst visas lejupielādētās datnes + Dzēst visas lejupielādētās datnes? + Visi lejupielādētie trekumī iztīrsīti no šīs ierīces + Lejupielādētās datnes dzēstas + Nekas netika atrasts + Darbības elementam %s + Atskaņot + Atskaņot vai apturēt + Iepjšējais + Nākamais + Pauzēt + Aizvērt + Dzēst + Dzēst lejupielādi + Dzēst lejupielādi? + %s tiks dzēsts no šīs ierīces + Lejupielāde dzēsta + Neizdevās dzēst lejupielādi + Neizdevās atskaņot lejupielādēto failu + Audio lejupielādes + Atskaņošana bezsaistē + Neizdevās identificēt lejupielādējamu glužu + Lejupielāde jau notiek + Jau lejupielādēts + Lejupielāde sākta + Sāk leisure lejupielāde… + Lejupielādē %s%% + Lejupielāde pabeigta + Lejupielāde neizdevās + Par Morphe + Reklāmas Plūsma Vispārīgi Atskaņotājs + Dziesmu teksti Dažādi + YouTube Music valoda + Sistēmas valoda + Nepieciešams Android 13 vai jaunāka versija + Slēpt etiķeti \'Iegūt Music Premium\' + Slēpt Music Premium reklāmas + Slēpt video reklāmas + Slēpt Audio / Video pārslēgumu + Slēpt lejupielādes pogu + Slēpj lejupielādes pogu podkāstu šovā + Slēpt Explore plauktu + Slēpt filtra joslu + Slēpt tīklveida plauktus + Slēpt horizontālos plauktus + Slēpt saraksta plauktus + Teksta panele + Slēpt Kopīgošanas pogu + Slēpt Tulkošanas pogu + Slēpt plauktu \"Jaunumi no\" + Slēpt atskaņošanas saraksta plauktus + Slēpt atkārtošanas pogu + Slēpt nejaušas secības pogu + Slēpt plauktu \"Ātrā iecākšana\" Komentāri + Izvēlieties, kuras komponentes rādīt komentāru sadaļā + Slēpt kopienas noteikumus + Slēpt konteksta ziņojumu + Slēpt Emoji pogu + Slēpt Info pogu + Slēpt laika zīmju pogu + Krosfeids + Plūstoši savienot trekus viens ar otru + Iespējot pāreju + Pārejas līkne + Vienāda jauda + Klusā pieturēšana + Maiga mīkstināšana + Gluda S-krīve + Izeja + Ieeja + Krosfeida ilgums + 250 milisekundes + 500 milisekundes + 750 milisekundes + 1 sekunde + 2 sekundes + 3 sekundes + 4 sekundes + 5 sekundes + 6 sekundes + 7 sekundes + 8 sekundes + 9 sekundes + 10 sekundes + 11 sekundes + 12 sekundes + Krosfeids manuālai izlecīšanai + Pielieto pārejas efektu, kad manuāli pārejat uz nākamo izpildījumu + Krosfeids automātiskai pārejai + Pielieto pārejas efektu, kad izpildījums beidzas dabiski + Sesijas kontrolē + Ilgi nospiediet nejaušas atskaņošanas pogu, lai apturētu vai atsāktu pāreju + Šai sesijai krosfeids ir apturēts + Krosfeids atsaucēts + Video režīms nav pieejams, kamēr krosfeids ir iespējots + SABR atskaņošanas laikā krosfēidings ir izslēgts + Pārraidīšanas laikā krosfēidings ir izslēgts + Kā tas darbojas + "Krosfēidings pārklāj vienas dziesmas beigas ar nākamās sākumu, izmantojot jūsu norādīto ilgumu un līkni + +Tas darbojas automātiskas pārejas un manuālas izlaišanas laikā, katram ir savs pārslēgšanās slēdzis. Automātiskas pārejas laikā sapiešana sākas dažas sekundes pirms dziesmas beigām" + Ierobežojumi + "Mūzikas video tiek atskaņoti tikai kā audio, kamēr pārejas (crossfade) ir ieslēgtas. Ilgāk nospiediet nejaušu kārtību, lai apturētu pārejas un noskatītos video + +Mūzikas video, kura atskaņošana sākas, paliek video režīmā līdz nākamajai dziesmai + +Dziesmas, kas ir īsākas par pārejas ilgumu, netiek apvienotas ar pāreju + +Bloķēšanas ekrānā vai paziņojumos pārejas laikā īslaicīgi var būt redzams novecošs stāvoklis + +Pāreju apturēšana mūzikas video skatīšanai var izraisīt melnu ekrānu dažos video. No tā var izvairīties, apturējot pārejas pirms dziesmas izvēles" + Neatbalstīts + "Audio straumēšana vai spoguļošana (Chromecast, ekrāna vai audio spoguļošana) + +SABR atskaņošana (Video straumem imitēšana izslēgta)" + Kredīts + Krosfēda funkciju izveidojis VazerOG + Atcerēties atkārtošanas stāvokli + Atcerēties nejaušas atskaņāšanas stāvokli + Atgrozīšanas ātrums + Ātrums, kas tiek izmantots visiem trekkiem. Pie 100% tiek izmantota lietotnes ātrums, piemēram, podkāstu atgrozīšanas ātrums + Mainīt augstumu kopā ar ātrumu + Aizliegt nepatikšanās novirzīšanu + Novērš pāreju uz nākamo iz tracksuitu, kad tiek nospiest nepatikšanās poga + Atskaņināšanas saraksta trekam pieskaršanās darbība + Atskaņot sarakstu + Atskaņot tikai izvēlēto treku + Pievienot izvēlēto treku rindai + Pievienots rindai + Slēpt Cast pogu + Slēpt Vēstures pogu + Slēpt Paziņojumu pogu + Slēpt Meklēšanas pogu + Slēpt balss meklēšanas pogu + Slēpt skaņas meklēšanas pogu + Slēpt pogu \"Jauns\" + Slēpj pogu \"Jauns\" bibliotēkas cilnē + Darbības pogas + Izvēlieties, kuras darbības pogas rādīt zem spēlētāja + Slēpt darbību joslu + Slēpt pogas \"Patīk\" un \"Nepatīk\" + Slēpt komentāru pogu + Slēpt pogu \"Atspolēt tiešraides tērzēšanu\" + Slēpt teksta pogu + Slēpt kopīgošanas pogu + Slēpt saglabāšanas pogu + Slēpt lejupielādes pogu + Slēpt pogu \"Sākt radio\" + Izlaiduma izvēlne + Izvēliecies, kuras komponentes parādīties atskaņotāja un rindas izlaiduma izvēlnēs + Slēpt 3 kolonnu komponentu + Slēpt Patīk un Nepatīk pogas + Slēpt izvēlni \"Taste match\" + Slēpt izvēlni \"Pievienot sarakstam \'Vēlāk noklausīties\'\" + Slēpt izvēlni \"Pievienot rindai\" + Slēpt Subtitru izvēlni + Slēpt izvēlni \"Dzēst atskaņošanas sarakstu\" + Slēpt izvēlni \"Notīrīt rindu\" + Slēpt izvēlni \"Nerekomendēt šo izpildītāju\" + Slēpt Lejupielādes izvēlni + Slēpt izvēlni \"Rediģēt atskaņošanas sarakstu\" + Slēpt izvēlni \"Doties uz albumu\" + Slēpt izvēlni \"Doties uz izpildītāju\" + Slēpt izvēlni \"Doties uz epizodi\" + Slēpt izvēlni \"Doties uz podkāstu\" + Slēpt palīgības izvēlni + Slēpt izvēlni \"Atzīmēt epizodi kā noklausītu\" + Slēpt izvēlni \"Neinteresē\" + Slēpt izvēlni \"Pievienot ātrajai atlasei\" + Slēpt izvēlni \"Atskaņot nākamo\" + Slēpt kvalitātes izvēlni + Slēpt izvēlni \"Noņemt no bibliotēkas\" + Slēpt izvēlni \"Noņemt no atskaņošanas saraksta\" + Slēpt ziņojumu izvēlni + Slēpt izvēlni \"Saglabāt epizodi vēlāk\" / \"Saglabāt bibliotēkā\" + Slēpt izvēlni \"Saglabāt atskaņošanas sarakstā\" + Slēpt kopīgošanas izvēlni + Slēpt izvēlni \" nejaušā kārtībā\" + Slēpt izvēlni \"Miega taimeris\" + Slēpt izvēlni \"Sākt radio\" + Slēpt izvēlni \"Stats for nerds\" + Slēpt abonēšanas izvēlni + Slēpt izvēlni \"Atvienot no ātrās izvēles\" + Slēpt izvēlni \"Skatīt dziesmas autorus\" + Sākuma lapa + Noklusējums + Toplistes + Epizodes vēlāk + Atklājiet Skatīšanās vēsture Bibliotēka + Patīkamākā mūzika Atskaņošanas saraksti Meklēt Abonementi + Piespiedu portreta orientācija + Satur lietotni portreta orientācijā, pat ja ierīce tiek pagriezta + Mainīt miniplejera krāsu + Saskaņo miniatskaņotāja krāsu ar pilnas ekrāna atskaņotāju + Mainīt navigācijas joslas krāsu + Saskaņo navigācijas joslas krāsu ar piln ekranā esošo atskaņotāju + Rādīt pogu \"Nākamais\" miniplayerī + Rādīt pogu \"Iepjšējais\" miniplayerī + Iespējot piespiedu miniplayeri + Novērš atskaņotāja automātisku paplašināšanos uz pilnu ekrānu, kad sākas jauns izpildījums + Iespējot miniplayera aizvēršanu ar slīdumu + ļauj aizvērt miniplayeru, velkot to uz leju + Navigācijas josla + Izvēlieties, kuras pogas rādīt apakšējā navigācijas joslā + Slēpt pogu Sākums + Slēpt poga Paraugi + Slēpt pogu Atklāt + Slēpt pogu Bibliotēka + Slēpt pogu Uzlabot + Slēpt navigācijas joslu + Slēpt navigācijas pogu etiķetes + Nav pieslēdzies. Pieskarieties, lai konfigurētu + Iziet + Izgāja no konta + Ienable scrobbling + Iesūtīt statusu \"At 재생t šobrīd\" + Minimālais dziesmas ilgums + Dziesmas, kas ir īsākas par šo, netiks scrobbled + Scrobble aizkavējuma procents + Izaisīt scrobble pēc šī dziesmas procenta noklausīšanās + Scrobble aizkavējuma sekundes + Izaisīt scrobble pēc šādu sekundes noklausīšanās + ListenBrainz konts + Pieslēdzies. Pieskarieties, lai pārvaldītu kontu + Ievadiet savu ListenBrainz lietotāja tokenu, lai iespējotu scrobbling + Tokens + Apstiprināt + Lūdzu, ievadiet tokenu + Validēšana… + Derīgs marķeris! Lietotājs: %s + Nederīgs marķeris: %s + nepazīsts + Verifikācija neizdevās: %s + Marķeris dzēsts + Marķeris saglabāts + Brīdinājums: Saglabātais marķeris ir nederīgs! + Patikt patikšanām trekkiem + Automātiski atzīmēt trekus kā \"mīļos\" Last.fm, kad jums tie patīk YouTube Music (un noņemt atzīmi, ja trekurs vairs nepatīk vai tiek atzīmēts kā nepatīkams) + Last.fm konts + Ielogots kā %s. Pieskarieties, lai pārvaldītu kontu + Ievadiet savus Last.fm kredenciālus, lai ielogotos + Lietotājvārds + Parole + Ielogoties + Lūdzu, ievadiet lietotājvārdu un paroli + Pierakstīšanās… + Ielogošanās veiksmīga! + Neizdevās iegūt sesiju no Last.fm + Pierakstīšanās neizdevās: %s + Last.fm pilnvaras saglabātas! + Metadatu tīrīšana + Notīrīt metadatus + Automātiski notīrīšanas bieži sastopāmu liekumu (piemēram, \"Official Video\", \"Remaster\", \"Live\") no nosērumiem, izpildītājiem un albumiem pirms scrobbling + Pielagota regex tīrīšana + Ievadiet pielāgotu regulāru izrauda, lai nodzēstu papildu tekstu no dziesmu nosaukumiem, izpildītājiem vai albumiem. Piemērs: (?i)\\s*\\((clean|explicit)\\) + Analizēt izpildītāju un treku no nosēruma + Izvelkt mākslinieka un trekā vārdu no video nosaukuma, izmantojot separatoru \" - \". Noderīgi atkārti augšupielādētam saturam, kur kanāla nosaukums var tikt izmantots kā mākslinieks + Aizpildīt trūkstošo albumu + Ja mūzikai nav norādīts albums, meklāt to Last.fm, lai jūsu scrobble būtu pilnīgs + "Scrobbling automātiski sinhronizē jūsu YouTube Music lietojumu ar jūsu Last.fm vai ListenBrainz profilu + +Lai nodrošinātu, ka scrobbling tiek sinhronizēts fona atskaņošanas laikā, pārliecinieties, ka YouTube Music lietotnes akumulatora lietojums ir iestatīts uz \"Bez ierobežojumiem\"" + Izmantot trešo pušu dziesmu tekstus + Rāda dziesmu tekstus no trešo pušu pakalpojuma; ja tie netiek atrasti, tiek izmantoti iebūvētie teksti + Turēt ekrānu ieslēgtu dziesmu tekstu skatīšanas laikā + Novērs ekrāna izslēgšanos, kamēr ir atvērts dziesmu tekstu panelis + Dziesmu tekstu pakalpojums + Rādīt + Sinhronizācija + Panelis + AI gala punkts + Visi pakalpojumu sniedzēji ir deaktivēti - iebūvētie teksti un iekļautie teksti + Atiestatīt nodrošinātāju secību? + Atjaunot nodrošinātāju nokārtojumu pēc prioritātes nokārtojuma vērtībām + Notīrīt saglabātos dziesmu tekstus + Dzēst šajā ierīcē saglabātos dziesmu tekstus + Notīrīts %1$s MB + Iegūt marķītāju + Apple Music mediju lietotāja marķeris + "Ielīmēt \"media-user-token\" sīkfailu no music.apple.com + +Soļi: +1. Pierakstieties music.apple.com kā reģistrēts lietotājs +2. Atveriet DevTools → Application → Cookies → music.apple.com +3. Nokopējiet \"media-user-token\" vērtību" + Apple Music: media-user-token saglabāts + Apple Music: media-user-token dzēsts + Apple Music: nederīgs media-user-token - lūdzu, pārbaudiet un mēģiniet vēlreiz + Spotify SpDc + "Ielīmējiet sp_dc cookie no Spotify. + +Soļi: +1. Pieslēdzieties vietnē open.spotify.com +2. Atveriet DevTools → Application → Cookies → open.spotify.com +3. Kopējiet \"sp_dc\" vērtību" + Spotify: sp_dc saglabāts + Spotify: sp_dc dzēsts + Spotify: nederīgs sp_dc - lūdzu, pārbaudiet un mēģiniet vēlreiz + Apple Music marķeris ir neobligāts. Bez tā tiks izmantots Lyrically kā rezervētais variants + Deezer ARL + "Ielīmējiet arl cookie no Deezer. + +Soļi: +1. Pieslēdzieties deezer.com +2. Atveriet DevTools → Application → Cookies → deezer.com +3. Kopējiet \"arl\" vērtību" + Deezer: arl saglabāts + Deezer: arl dzēsts + Deezer: neatbilstošs arl - lūdzu, pārbaudiet un mēģiniet vēlreiz + Musixmatch Tokens + Pievienojiet tokenu tikai tad, ja vēlaties izmantot savu kontu. Tam jābūt iegūtam no Musixmatch Android lietotnes: musixmatch.com kopētie tokeni pieder novecotam datora klientam, un API tos noraida + Musixmatch darbojas bez tokena, tāpēc šis lauks var palikt tukšs + lietotāja tokens + Musixmatch: lietotāja tokens saglabāts + Musixmatch: lietotāja marķeris dzēsts + Musixmatch: neatbilstošs lietotāja marķeris - lūdzu, pārbaudiet un mēģiniet vēlreiz + YouTube sīkfaili + "Ielīmējiet savas YouTube cookies, lai iz queriedt subtitrus un laika tekstu no YouTube. + +Soļi: +1. Atveriet youtube.com pārlūkprogrammā un piesakieties +2. Atveriet DevTools (F12) → Application → Cookies → youtube.com +3. Nokopējiet pilnu cookie augu (key=value pāri, kurus atdala semikoloni) + +Obligātās cookies: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPISID, LOGIN_INFO" + YouTube: cookies saglabātas + YouTube: cookies dzēstas + YouTube: neatbilstoši failiņi - lūdzu, pārbaudiet un mēģiniet vēlreiz + 𝒞uviet un aizturiet rindu + Pieskarieties rindai, lai pārvirzītos uz šo brīdi dziesmā, vai ilgāk nospiediet to, lai nokopētu vārdu zem pirksta vai visu rindu, ja nospiešana nav vārda vietā. Izslēdzot šo funkciju, abi žesti tiks atslēgti + Rādīt sadaļas tagiem + Markēt katru jaunu dziesmas teksta sadaļu ar mazu tagu, piemēram, Versa vai Pieliedums + Rādīt kopēšanas pogu + Pieskarieties, lai nokopētu oriģinālo tekstu kā plain text; ilgāk nospiediet, lai saglabātu neapstrādāto teksta avotu failu Lejupielādēs + Rādīt tulkošanas pogu + Pieskarieties, lai pārslēgtu tulkojumu; ilgāk nospiediet, lai redzētu tikai tulkojumu. Izmanto pakalpojumu tulkojumus, Google Translate vai OpenAI saderīgu API + Rādīt romanizācijas pogu + Pieskarieties, lai pārslēgtu romanizāciju; ilgāk nospiediet, lai redzētu tikai romanizāciju. Izmanto pakalpojuma romanizāciju, Google Translate vai OpenAI saderīgu API + Lokāla romanizācija un izlīzināšana + Veikt romanizāciju ierīcē, ja pakalpojums, jūsu modelis un Google Translate neveiksmīgi. Lasīšana seko aiz katra vārda sinhronizētajā tekstā; vārdu sinhronizācijai jābūt ieslēgtā. Ķīniešu, kantonešu, japāņu, korejiešu, grieķu, krievu, gruzīnu, armēņu valodām; aptuveni 120 MB + Lasīšana %1$s: %2$d%% + Lejupielāde neizdevās - izslēdziet un ieslēdziet šo iespēju miezēšanai + Rādīt atjaunināšanas pogu + Kliediet, lai pārslēgtos uz nākamo dziesmu teksta variantu; aizturiet, lai meklētu dziesmu tekstu pēc dziesmas nosaukuma un izpildītāja + Meklēt dziesmu tekstu + Dziesmas nosaukums + Mākslinieks + Ievadiet dziesmas nosaukumu un mākslinieku + Netaisnodzītie pakalpojumu sniedzēji nevar meklēt citas dziesmas + Saglabāts + Izcelt sinhronizētus vārdus vai slabes + Došanai prioritāti jāsasniedz vārdu pa vārdiem sarakstītiem tekstiem, ja tie ir pieejami. Ja tekstam ir vārdu noraidīšanas laiki, tiek izcelts katrs vārds vai slabe mūzikas ritmā + Slēpt atskaņotus teksta fragmentus + Slēpt teksta rindas virs pašlaik atskaņātās rindas + Slēpt neatskaņotus teksta fragmentus + Slēpt teksta rindas zem pašlaik atskaņātās rindas + Rādīt MediaSession tekstu bloķēšanas ekrānā + Rāda pašreizējo rindu ekrāna bloķētājā, Android Auto un Bluetooth. Nepieciešams Android 11 vai jaunāks + Rādīt dziesmu vārdus minimspēlētājā + Rāda pašreizējo rindu minimālais spēlētājā, pārvietojot izpildījumu uz apakštitru sadaļu + Dziesmas nosaukuma/artiste formāts + Treka formāts, kas tiek rādīts kopā ar tekstu minimāltajā spēlētājā un MediaSession. {t} ir nosaukums, {a} ir izpildītājs un {v} is versija, piemēram, Acoustic, ja teksta avots to nodrošina. Teksts starp diviem kolonnām tiek rādīts tikai tad, ja katrai tajā esošajai mainīgajai ir vērtība; rakstiet \\:, lai ievietotu parastu kolonnu. Katra rinda tiek rādīta pēc kārtas, mainoties kopā ar tekstu + Teksta rindas formāts + Dziesmas teksta rindas formāts miniaturizētā spēlētājā un MediaSession. {lrc} ir rinda un {n} ir skaits, cik reizes pēc kārtas tā tiek dziedāta. Teksts starp divpunktiem tiek rādīts tikai tad, ja rinda atkārtojas. Divpunktiem izmantojiet \\: + Formātam jāsatur {lrc} + Mākslinieka rindas šablons un animācijas synchronously + Minimālais ekrāna laiks (ms) + Maksimālais ekrāna laiks (ms) + Tikai daudzrīngām animācijām. Saglabājiet katru kadru vismaz minimālo laiku. Pie maksimālā laika virziet mākslinieka rindu tālāk pat tad, ja dziesmas teksts paliek nemainīgs. + Minimālajam vērtījumam jābūt 0 vai lielākam. Maksimālajam vērtījumam jābūt lielākam par 0 un vismaz minimālā vērtas līmenī. + Līnijas garuma ierobežojumi + Dziesmas teksta un izpildītāja līnijas ierobežojumi un pilna platuma rakstzīmju svars + "Dziesmas teksta līnijas ierobežojums +Sadalīt garāku tekstu līdzsvarotās pusēs. 0 izslēdz sadalīšanu." + "Izpildītāja līnijas ierobežojums +Sadalīt aizpildīto trekuma formātu cikliski mainīgajos kadros. 0 izslēdz sadalīšanu." + "Pilna platuma rakstzīmju svars + Piemērojams abiem ierobežojumiem. Pie 1,75 četras pilna platuma rakstzīmes skaitās par 7." + Ievadiet ierobežojumus kā veselskaitļus no 0 līdz 10000 un rakstzīmju svaru, kas ir lielāks par 0 + Instrumentāla pauze + Rādā Miniplejerī un MediaSession laikā instrumentālo pārtraukumu. Katra rinda ir viens animācijas kadrs. {t} ir nosaukums, {a} ir mākslinieks un {lrc} ir pēdējā dziesmas rindas teksts + Miniplejera un MediaSession laika novēlējums + Pamaina brīdi, kad nākamā rindas tiek rādīta Miniatūrspēlētājā un MediaSession. Izmantojiet negatīvu vērtību, ja rindas parādās vēlu, piemēram, bloķēšanas ekrānā + Filtri + Metadatu regulārā izprimitive + Regulāra izaire, kas tiek pielietota nosaukumiem, māksliniekiem un albumiem pirms teksta meklēšanas un vērtēšanas, vai divi nosaukumi sakrīt. Atstājiet tukšu, lai deaktivētu abus. Noklusējums noņem izplatītu lieku informāciju, piemēram, \"Official Video\", \"Remaster\", \"【插曲】\" un \"- Topic\" + Dziesmu vārdu filtra regulārā izprimitive + Regulārā izprimitive tiek piemērota tikai oriģinālajam dziesmu vārdu tekstam (ne tulkojumiem vai romanizācijām). Atstājiet tukšu, lai nicht aktivizētu + Informācijas rindu marķieri + Ja dziesmu vārdi tiek nepareizi filtrēti uz apakšu, pielāgojiet vai izdzīvējiet šo vērtību. Ar komatu atdalīti marķieri: rindas, kas satur marķieri, kam seko divm chấmis, tiek rādītas kā kredīta informācija apakšā. Atstājiet tukšu, lai nicht aktivizētu + Likt pirmo burtu ar lielu burtu + Liek katras dziesmas vārdu rindas pirmo burtu ar lielu burtu + Noņemt interpunkcijas zīmes rindas beigās + Noņem punktus, komatus, divpunktus un semikolonus dziesmas vārdu rindas beigās. Jautājuma zīmes, izsaucības zīmes, tripmarķi, citāti unaunas paliek + Teksta izmērs + Dziesmu teksta izmērs + Globāls laika novīkums + Sinhronizētie teksti sekot līdzi atskaņošanai. Ja tie novirza, pielāgojiet laika novirzes vērtību. Pamidziniet sinhronizētos tekstus uz priekšu vai atpakaļ. Izmantojiet pozitīvu vērtību, ja teksti skrien priekšā dziesmai + Izmantot SponsorBlock segmentus dziesmu teksta novīkumam + Saskaņot, izlaidot SponsorBlock izlaidotos segmentus, un novirzīt tekstu uz izlaisto audio. Izslēgts pilnībā ignorē SponsorBlock + Šim trekam nav video ID iesniegšanai + Avots: %s + Ilgi nospiediet kopēšanas pogu, lai saglabātu šo LRC; ja varat precizēt laika intervālus un kopīgot tos ar kopienas sniedzējiem, mēs būtu pateicīgi + Tulkoja Google + Romanizējis Google + Tulkoja %s + Romanizēja %s + Romanizējis %s + Tulkots + Tulkojuma valoda + Romanizācijas valoda + Tulkošana… + Neizdevās iztulkot dziesmu tekstu + Romanizēts Automātiski + Romanizē... + Neizdevās romanizēt dziesmu vārdus + Tikai tulkots + Tikai romanizēts + Divērtības režīms + Kopēts staruklājlapā + Atjaunīšana + Atjaunots + QuickLRC API atslēga + QuickLRC transkribē vai laika ziņojumā saskaņo vārdu tekstu pašai izselei. Atsevišķs pakalpojums, kas nav saistīts ar augstāk minēto OpenAI saderīgo gala punktu + QuickLRC atslēga saglabāta. Izmantojiet saskaņošanas pogu vārdu tekstu meklēšanas dialogā vai paneļa aicinājumu, ja izselei nav vārdu teksta + QuickLRC API atslēga saglabāta + QuickLRC nenoņēma šo atslēgu + QuickLRC API atslēga dzēsta + QuickLRC piešķir laika zīmes vārdu tekstiem, kuriem tās nav, vai transkribē tos no izseles par kredītu maksu. Ja atslēga ir iestatīta, izseles bez vārdu teksta piedāvā transkribēšanu, un meklēšanas dialogā parādās saskaņošanas poga + Pieprasīt QuickLRC izlīzināšanu + Izlīzināšana + Pielietot izlīzināšanu + Pieprasīt QuickLRC transkripciju + Transkribēšana + Transkribēšana atcelta + Var transkribēt tikai no YouTube atskaņotus trekus + Nav vārdu teksta saskaņošanai - tā vietā tiek veikta transkribēšana + QuickLRC nevara apstrādāt šo treku + Šim trekam QuickLRC kredīti ir izlietēti + "Dziesmu teksti tiek meklēti pēc nosaukuma un izpildītāja; neparasta metadata var izraisīt neveiksmīgu meklēšanu vai nepareizas versijas atradēšanu. Izmantojiet atjaunināšanu, lai pārslēgtos starp versijām. Pakalpojumu sniedzēji, kas apzīmēti ar *, prasa tokenu. Visi iespējotie sniedzēji ne vienmēr tiek pieprasīti jūsu norādītajā secībā, bet atbild atkarībā no to ātruma.\n\nDziesmu teksti tiek saņemti no trešo pušu pakalpojumiem, kas nav saistīti ar Morphe, nav sponsorēti vai atbalsīti no Morphe. Katrs no tiem tiek izmantots tāds, kāds tas ir: teksti, to pieejamība un licencēšana paliek šo pakalpojumu un to licencējušu personu tuvumā, un to izmantošana var būt pakļauta viņu lietošanas noteikumiem. Aktivizētajam pakalpojumam tiek nosūtīts tikai jūsu pieprasītais meklējums - nosaukums, izpildītājs un, ja pakalpojumam tas ir nepieciešams, video ID. Papildus AI gala punkts un QuickLRC transkripcija darbojas tikai pēc to konfigurēšanas un saņem dziesmu tekstu vai izpildījuma video URL." + Iespējojiet tikai tos paklājējus, kurus izmantojat, vai atstājiet noklājumu - pastāvīgi pieprasījumi var pārslogīt mazus kopienas dziesmu tekstu vietnes + Slēpt kredītlīniju + Slēpt kredītlīniju, kas parādās zem dziesmu teksta + Mainīt tulkojuma un romanizācijas vietas + IEKLĒGTS novieto romanizāciju zem teksta un tulkojumu virs; IZSLĒGTS atstāj romanizāciju virs un tulkojumu zem + Tulkojums + Romanizācija + Tulkojuma nod வழங்குējs + Google (pēc noklusējuma, bezmaksas) + OpenAI saderīgs (pielāgots) + Romanizācijas pakalpojumu sniedzējs + Google (pēc noklusējuma) + Lokālais / bezsaistes vārdnīca + OpenAI saderīgs (pielāgots) + AI sadaļa + Tiek izmantots tikai tad, ja pakalpojumu sniedzējs nenodrošina sadaļu apzīmējumus, piemēram, Prijiedienums vai Kuplets, kurus pēc tam atpozīst jūsu OpenAI saderīgas modelis + AI & mākoņpakalpojumu gala punkti + Konfigurēt API atslēgas, modeļus, gala punktus un uzvednes šablonus AI pakalpojumu sniedzējiem. + API gala punkta URL (piem. https://api.openai.com/v1/chat/completions) + Uzvednes šablons + Vietneces: {language}, {task}, {title}, {artist}, {count}, {lines} + Uzvedne saglabāta + Aicinājums saglabāts, taču AI gala punkts vēl nav izmantojams. Vispirms konfigurējiet to AI iestatījumos + Uzvedne atiestatīta + Nevalids JSON. Izlabojiet to vai izdzīvējiet lauku pirms saglabāšanas + Tulkojuma & romanizācijas iestatījumi saglabāti + Iestatījumi atiestatīti nokārtāvējumā + Pamata URL adresei jāsāk ar http:// vai https:// + Pārbaudīt savienojumu + Konfigurēt gala punktus + Rādīt visus gala punktus + Savienojuma pārbaude… + Tulkojuma nodrošinātājs + Romanizācijas nodrošinātājs + Iegūt modeļus + Modeļi tiek ieguvti… + Izvēlieties modeli + Neizdevās ieguvēt modeļus. Pārbaudiet API atslēgu vai savienojumu. + Gemini API iestatījumi + OpenAI saderīgs (pielagots gala punkts) + DeepL API iestatījumi + API atslēga + Modelis + Pamata URL + API atslēga (neobligāta) + Tulkošanas uzvedne + Papildu pieprasījuma korpuss (neobligāts JSON) + Autentifikācijas atslēga (Free vai Pro) + Konfigurēt API atslēgas, modeļus, gala punktus un uzvednes + 8.23.51 - Atslēgt tremūsas ikonas + 9.13.51 - Atjaunot atskaņotāja cilnes Tālāk, Dziesmu teksti un Saistītie diff --git a/patches/src/main/resources/addresources/values-lv-rLV/reddit/strings.xml b/patches/src/main/resources/addresources/values-lv-rLV/reddit/strings.xml index d0b4d81136..1a956baaca 100644 --- a/patches/src/main/resources/addresources/values-lv-rLV/reddit/strings.xml +++ b/patches/src/main/resources/addresources/values-lv-rLV/reddit/strings.xml @@ -16,5 +16,17 @@ Second \"item\" text" --> + Reddit privātuma politika + Google pieslēgšanās nav pieejama. Pieslēdzieties, izmantojot e-pastu Reklāmas + Pielāgots šrifts + Ielādēt pielāgotu TTF vai OTF šrifta failu no zemāk norādītā ceļa. Iespējojot šo opciju, tiek izslēgts Piespiedtais sistēmas šrifts + Pielāgota šrifta fails + Krājiet, lai izvēlētos TTF vai OTF šrifta failu + Izvēlētais šrifts - %s + Vispirms izvēlieties pielāgota šrifta failu + Lūdzu, izvēlieties .ttf vai .otf šrifta failu + Neizdevās ielādēt pielāgotu šriftu. Izvēlieties citu failu + piespieduši izmantot sistēmas fontu + Izmantot ierīces sistēmas šriftu vietā Reddit Sans vai Roboto. I iespējojot šo opciju, tiek izslēgts Pielāgots šrifts diff --git a/patches/src/main/resources/addresources/values-lv-rLV/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-lv-rLV/shared-youtube/strings.xml index 45e6b0aefc..6b92c2f4b9 100644 --- a/patches/src/main/resources/addresources/values-lv-rLV/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-lv-rLV/shared-youtube/strings.xml @@ -18,47 +18,293 @@ Second \"item\" text" --> + Startēšanas animācija Atspējots + Krāsa + Monohroms + + Izslēgts + Restartējiet, lai pabeigtu konfigurēšanu + Lietotnes nosaukums + Tumšās tēmas fons + Tīrs melns + Material You (Tīrs melns) + Klasiskais YouTube + Tumšrozā + Tumši zila + Tumši zaļa + Tumši dzeltenā + Tumši oranža + Tumši sarkanā + Pielagots tumšs fons + Iestatīt tumšās tēmas fona krāsu + Lietotnes noklājums + Material You (Neitrāls) + Material You (pirmārīgā) + Material You (Sekundārs) + Material You (Terciārs) + Pielagots + Lejupielādes + Lejupielādes režīms + Noklusējums + Lejupielādēt lietotnē + Ārējais lejupielādētājs + Lejupielādētāja paketes nosaukums + Jūsu instalētās ārējās lejupielādētāja lietotnes paketes nosaukums + Ievadiet paketes nosaukumu + Citi + Lietotne nav instalēta + %s nav instalēts. Lūdzu, instalējiet to + "Neizdevās atrast instalētu lietotni ar paketes nosaukumu: %s + +Pārbaudiet, vai paketes nosaukums ir pareizs un vai lietotne ir instalēta" + Paketes nosaukums nevar būt tukšs + Pielāgots + Lietotnes ikona + Oriģināls + Morphe melns + Morphe tumšs + Morphe gaišs + Morphe play gaišs + Morphe play melns + Pielāgots + "Tas maina ikonu, kas tiek rādīta jūsu lietotņu palavā. + +Šis iestatījums nemainīs ikonu nevienā no šīm vietām: +• Ierīces sistēmas iestatījumu lietotņu saraksts +• Dažas push paziņojumi +• 'Atvērt ar' lietotņu izvēle +• Android Auto ikona + +Lai mainītu ikonu visur, veiciet 패čošanu vēlreiz un mainiet 'Lietotnes ikona' 패čošanas opciju" + Paziņojumu ikona + Saskaņot ar lietotnes ikonu + Morphe atskaņošana + Morphe paziņojums + Jūsu skatīšanas vēsture netiek saglabāta.<br><br>Tas, visticamāk, ir saistīts ar DNS reklāmu bloķētāju vai tīkla proksy.<br><br>Lai to labotu, iekļaujiet <b>s.youtube.com</b> baltajā sarakstā vai izslēdziet visus DNS bloķētājus un proksy + Nerādīt vēlreiz + Viltot video plūmes + Konfigurēt klienta imitāciju, lai novērstu atskaņošanas problēmas + "Novērš atskaņošanas problēmas lielākajai lietotāju daļai, pieprasot saderīgu video plūmi + +Ja esat YouTube Premium lietotājs, šis iestatījums var nebūt nepieciešams" + Neizdevās izgūgt nevienu klienta straume + Noklusējumalients nedarbojas + "%1$s vairākas reizes pēc kārtas nedarbojās, bet %2$s darbojās. + +Izveidot %2$s par noklusējuma klientu?" + Pārslēgt + Šī iestatījuma izslēgšana var izraisīt atskaņošanas problēmas + Mēģiniet iespējot: Video straume spoofing > Android VR pieslēgšanās + Piecīlējais klients + Android VR pieslēgšanās + Android VR nav pieslēgts. Pieskarieties šeit, lai pieslēgtos + Šobrīd pieslēgts pie Android VR + Lai izmantotu šo funkciju, mainiet \'Noklusējumalients\' uz Android VR + Pierakstieties Android VR + "Pieslēgšanās Android VR var novērst atskaņošanas problēmas dažiem lietotājiem + +Lai pieslēgtos, nospiediet turpmāk zemāk" + Izrakstīties + Instrukcijas + "Aktivizācijas kods: %s + +1. Zemāk nospiediet \"Atvērt tīmekļa vietni\", lai atvērtu vietni \"Pievienot ierīci\" +2. No starpnes ielīmējiet (vai ierakstiet) augstāk redzamo aktivizācijas kodu +3. Ja tiek lūgts, pieslēdzieties Google. Ja jums ir vairāki konti, izvēlieties šo YouTube kontu +4. Nospiediet \"Atļaut YouTube VR\" piekļuvi +5. Atgriezieties šajā lietotnē, kad parādīsies ziņojums \"Ierīce pievienota\"" + Neizdevās izdot aktivizācijas kodu + Neizdevās izdot autentifikācijas tokenu + Pierakstīšanās veiksmīga + "Autentifikācija saglabāta + +• Jūsu Morphe iestatījumi neimportēs/neeksportēs Android VR autentifikāciju +• Ja šī lietotne netiek izmantota 6 mēnešus, Google izdzēs šo autentifikāciju +• Jums būs jāpieslēdzas vēlreiz, ja mainīsiet savu Google paroli vai noņemiet šo ierīci no saviem Google iestatījumiem" + OAuth2 pagaidām nav pieejams (API laika limits beidzies) + Android VR pieslēgšanās termiņš ir beidzies, mēģinieties pieslēgties VR vēlreiz + OAuth2 pagaidām nav pieejams (statuss %s) + OAuth2 pagaidām nav pieejams + Nevar pieslēgties VR, tīmekļa vietnes autentifikācija netika apstiprināta + Nevar pieslēgties VR: %s + Iz girişts no Android VR + VR pieslēgšanās marķeris ir neatbilstošs, mēģinieties pieslēgties VR vēlreiz + Paziņojumu mediju vadības elementi + Pielagojiet atskaņošanas vadības elementus, kas tiek rādīti jūsu bloķēšanas ekrānā un paziņojumu panelī + Slēpt pogas \"Iepriekšējais\" & \"Nākamais\" + Slēpj pogas \"Iepriekšējais\" un \"Nākamais\" mediju paziņojumā, bloķēšanas ekrānā un austiņu vadībā + Atslēgt meklēšanas joslu + Novērs iespēju velkt meklēšanas joslu uzslēgas ekrānā un paziņojumu panelī + Meklēt ar pogām \"Iepriekšējais\" & \"Nākamais\" + Maina mediju paziņojuma, bloķēšanas ekrāna un austiņu vadības elementus, lai meklētu vietā, lai mainītu video + Atsvinšanas garums + Sekundes, cik atsvinīt ar pogu \"Iepriekšējais\" + Ātrās pārslēgšanas garums + Sekundes, par kurām pārlēkt uz priekšu, nospiežot pogu \"Tālāk\" + Meklēšanas garumam jābūtu no 1 līdz %s sekundēm. Tiek izmantots nokalpošanas pluckedums + Atslēgt QUIC protokolu + Atslēgt DRC audio + Atspiest oriģinālo audio valodu + Vienmēr atskaņo video oriģinālo audio izsauci dublētas versijas vietā + Izlaist klusumu + Automātiski izlaika klusos pauzes video audio ierakstos + Minimālais pauzes garums + Tiek saīsinātas tikai tās pauzes, kas ir vismaz šāda garuma. Stājas spēkā, kad sāks nākamais video + Tīkla proksis + Maršrutēt atbalstītus tīkla pieprasījumus caur proksija + Ienablet proksija + Maršrutē tīkla pieprasījumus caur konfigurētu HTTP vai HTTPS proksija + Proksies hosts + Proksies servera hosta nosaukums vai IP adrese + Proksies ports + Proksies servera ports + Izmantot HTTPS proksies + Savienojumam ar proksies serveri HTTP vietā izmantot HTTPS + Proksies autentifikācija + Sūta Basic autentifikāciju kopā ar proksies pieprasījumiem + Proksija lietotājvārds + Lietotājvārds Basic proksies autentifikācijai + Proksija parole + Parole pamata proksija autentifikācijai + Atļaut tiešu rezervu + Savienoties tieši, ja proksija neveiksmīgi darbojas + Slēpt pilnkrāna reklāmas + Lai izmantotu šo funkciju, mainiet \'Spoof video streams\' uz jebkuru klientu, izņemot \'Android Studio\' un \'Android VR Downgraded\' + Tīrīt kopīgošanas saites + Noņem izsekošanas va之duros parametrus no kopīgotām saitēm + Mainīt kopīgošanas saites uz youtube.com + Kopīgotajās saitēs aizstāj music.youtube.com ar youtube.com + Mainīt kopīgošanas saites uz youtu.be + Piespiež video izmantot saīsināto youtu.be URL formātu + Atlandošanas žurnāls protocol buffer + Pievieno papildu žurnā vērtjamu informāciju izstrādātājiem, lai identificētu lietotnes komponentes + "Iespējojot šo iestatījumu, tiks reģistrēti papildu izkārtojuma dati, tostarp ekrāna teksts dažām UI komponentēm. + +Tas var palīdzēt identificēt komponentes, veidojot pielāgotus filtrus. + +Tomēr, iespējojot šo, tiks reģistrēti arī 일부 lietotāja dati, piemēram, jūsu IP adrese" + Pielagots filtrs + Konfigurējiet pielagotus filtrus, lai slēptu jebkuru komponenti + Iespējot pielagotu filtru + Saraksts ar komponentu ceļa veidotāja saitēm filtrēšanai, atdalītas ar jaunām rindām + Nederīgs pielagots filtrs: %s + Iestatījumu izvēlnes filtrs + Slēpt elementus standarta lietotnes iestatījumu ekrānā pēc to redzamā nosaukuma + Vienreiz atveriet standarta lietotnes iestatījumu ekrānu, lai aizpildītu sarakstu + Noņemt skatītāja atlaižu dialogu + Tas neapiesta vecuma ierobežojumu. Tas to vienkārši apstiprina automātiski + PoToken pakalpojumu sniedzējs + Konfigurēt PoToken pakalpojumu sniedzēju, lai novērstu atskaītes problēmas + Ārējais PoToken pakalpojumu sniedzējs + "Novērš atskaidzes problēmas, izmantojot ārēju PoToken minteru (eksperimentāli) + +Ja esat YouTube Premium lietotājs, šī iestatījums var nebūt nepieciešams" + "Novērš atskaitīšanas problēmas, izmantojot ārēju PoToken minteru (eksperimentāli) + +Lai izmantotu šo funkciju, izslēdziet \"Spoof video streams\"" + Ja tas nedarbojas, mēģiniet piespiedu veidā aizvērt MicroG + Atvērt PotHelper iestatījumus + "Šī funkcija pašlaik tiek izstrādāta un var nedarboties + +Krājiet šeit, lai lejupielādētu PotHelper" + PotHelper vairs nedarbojas + "Šajā PotHelper versijā iebūvētais tokens vairs nedarbojas + +Iesaɪciet 'Viltot video plūdes', lai labotu atskaņošanu" + Iesaɪciet + Imitēt lietotnes versiju + manipulator serveri, liekot domāt, ka izmantojat vecāku lietotnes versiju, lai atjaunotu novecojušas UI funkcijas + "Lietotnes versija tiks imitēta kā vecāka versija. + +Tas mainīs lietotnes izskatu un funkcijas, taču var rasties nezināmi blaknefekti. + +Ja vēlāk šī funkcija tiek izslēgta, ieteicams izdzēst lietotnes datus, lai novērstu UI kļūdas" + Lietotnes versijas mērķa imitācija + Lietotnes atjaunināšanas ātrums + Noklusētais + %s FPS + Pārrakstīt atjaunināšanas ātrumu + Vienmēr + Atskaite portreta režīmā + At playback fullscreen + Atskaņošana portreta režīmā un pilnā ekrāna režīmā + Fonts + Noklusējums + Ierīces sistēmas fonts + Pielagots fonta fails + Pielagots fonta fails + Izvēlieties TTF vai OTF fonta failu + Izvēlētais fails nav TTF vai OTF fonts + Customizētu fontu neizdevās ielādēt, tāpēc tas ir izslēgts + Nepatīkamie pagaidām nav pieejami (API laika beigas) + Nepatīkami nav pieejami (statuss %s) + Nepatīkamie nav pieejami (klienta API ierobežojums) + Nepatīkamie nav pieejami (%s) + Īpašnieks ir paslēpis + Šis video nav paticis, %s no balsīm ir negatīvas + Iespaiot Return YouTube Dislike + Rādīt nepatikšanas kā procentuāli + Rāda nepatikšanas kā procentuāli no patikšanām, nevis kā skaitli + Rādīt aptuveno patikšanu skaitu + Rāda aptuvenu patikšanu skaitu video, kuros izveidotājs ir izslēdzis patikšanas + Rādīt savienojuma kļūdu brīdinājumus + Rāda paziņojumu, ja Return YouTube Dislike serveris nav pieejams + "Return YouTube Dislike atjaunina nepatikšanās skaitīklus, kurus YouTube slēpa 2021. gadā. Skaitļi nāk no datiem, kas tika arhivēti pirms noņemšanas, un jaunākiem video tievi tiek aprēķināti, pamatojoties uz lietotāju balsām, tāpēc tie ir aptuveni un tiek atjaunināti katras dažas dienas + +Video ID, kuru jūs skatāties, tiek nosūtīts Return YouTube Dislike API, bet jūsu patikšanās un nepatikšanās tiek nosūtītas anonīmi, lai uzlabotu aprēķinājumus" + Return YouTube Dislike API statistika šai ierīcei + API atbildes laiks, vidēji + API atbildes laiks, minimums + API atbildes laiks, maksimums + API atbildes laiks, pēdējam medijam + Negatīvie vērtējumi pagaidām nav pieejami - spēkā ir klienta API ātruma ierobežojums + API balsu iegūšana, zvaniðu skaits + Tīkla zvani netika veikti + API balsu iegūšana, Time-outu skaits + Neviena tīkla zvana laika robeža neatseka + API klienta ātruma ierobežojumi + Klienta ātruma ierobežojumi netika konstatēti diff --git a/patches/src/main/resources/addresources/values-lv-rLV/shared/strings.xml b/patches/src/main/resources/addresources/values-lv-rLV/shared/strings.xml index 03a6547eb9..6690d5d7be 100644 --- a/patches/src/main/resources/addresources/values-lv-rLV/shared/strings.xml +++ b/patches/src/main/resources/addresources/values-lv-rLV/shared/strings.xml @@ -17,25 +17,219 @@ Second \"item\" text" --> + Iestatījumi + Vai esat pārliecināti, ka vēlaties turpināt? + Saglabāt + Atiestatīt + Atiestatīt krāsu + Negaidīta krāsa + Nepieciešams restartēt + Restartējiet lietotni, lai šī izmaiņa stājustu spēkā + Restartēt + Importēt + Kopēt + Morphe iestatījumi atiestatīti uz noklusējuma vērtībām + Importēti %s iestatījumi + Importēšana neizdevās: %s + Navigēt uz augšu + Meklēt iestatījumos Par + Rezultāti \'%s\' netika atrasti + Pamēģiniet citu atslēgvārdu + Kürzākie meklējumi + Izdzēst no meklēšanas vēstures? + Izdzēst no meklēšanas vēstures + Notīrīt meklēšanas vēsturi + Vai tiešām vēlaties notīrīt visu meklēšanas vēsturi? + Meklēšanas padomi + "• Pieskarieties ceļam, lai pārcestu uz tā +• Ilgi nospiediet iestatījumu, lai pārcestu uz tā +• Nospiediet Enter, lai saglabātu meklēšanas vaicājumu vēsturē +• Meklēšanā tiek ignorēti burutu reģistrs un interpunkcija +• Parent iestatījumi parādās virs nichtētiem child iestatījumiem" + Meklēšanas vēsture ir tukša + Lai saglabātu meklēšanas vēsturi, ierakstiet meklēšanas vaicājumu un nospiediet Enter + Rādīt iestatījumu meklēšanas vēsturi + Morphe iestatījumu nosaukums + Noklusējums + Papildinājumi + Pielāgojumi + Morfēšanas laiks + Pielagots + Ievadiet nosaukumu + Nosaukums nedrīkst būt tukšs + Rādīt Morphe iestatījumu ikonas + Kopēts: %s + Morphe valoda + "Dažuvさんu valodu tulkojumi var būt nepilnīgi vai trūkāt. + +Lai tulkotu jaunas valodas vai uzlabotu esošos tulkojumus, apmeklējiet morphe.software/translate" + Lietotnes valoda + Importēt / eksportēt + Importēt / eksportēt Morphe iestatījumus + Importēt no faila + Iestatījumi veiksmīgi importēti, nospiediet \"Saglabāt\", lai tos pielāgotu + Iestatījumu importēšana neizdevās + Eksportēt failā + Iestatījumi veiksmīgi eksportēti + Iestatījumu eksportēšana neizdevās + Jūs izmantojat jaunāko Morphe Patches versiju <i>%s</i> + Jūs izmantojat Morphe Patches versiju <i>%1$s</i><br><br><b>Pieejams atjauninājums: <i>%2$s</i></b><br><br>Lai atjauninātu patchus, izmantojiet Morphe, lai pārlaboitu šo lietotni + Piezīme + Šī versija ir pirmizlaide, un iespējams, ka tiks novērotas negaidītas kļūdas + Oficiālās saites + Tīmekļa vietne + Ziedot + Tulkojumi + Krediti + Aktuāli + Pieejams atjauninājums + Pašreizējā izstrāde + Iepēkšējā izstrāde + Aktīva izstrāde + 2022 ‑ 2025 + 2022 ‑ 2025 + 2019 ‑ 2022 + Licences paziņojumi + Ielādē… + Atklāšana + Konfigurēt papildus izstrādātāja iespējas un diagnostikas žurnālos + Ienablekt atklaides žurnāšanu + Atklādes žurnāla steka pēdums + Iekļauj steku pēdījumus atklaides žurnālos, lai palīdzētu identificēt kļūdas + Rādīt paziņojumu par Morphe kļūdu + "Izslēdzot kļūdu paziņojumus, tiek slēpts visi Morphe kļūdu paziņojumi. + +Jūs nesaņemsiet paziņojumus par nevēlamiem notikumiem" + Eksportēt atklādes žurnālus + Eksportēt Morphe atklādes žurnālus + Atklādes žurnāli veiksmīgi eksportēti + Neizdevās eksportēt atklādes žurnālus + Meklēt atklaides žurnālos… + Notīrīt + Atklāšanas žurnālos rakstīšana ir izslēgta + Atkārtotie žurnāli nav atrasti + Atkārtotie žurnāli kopēti + Neizdevās eksportēt atkārtotie žurnāli: %s + Atkārtotie žurnāli dzēsti + Funkciju flags vadītājs + Pārvalstīt boolean funkciju flags + Meklēt funkciju karogus… + Neaktīvs (%s) + Aktīvi (%s) + Bloķēti (%s) + Atspiesti (%s) + Auto Izslēgts Ieslēgts + Atspiest izslēgumu + Bloķēt Noņemt + Funkciju flags saglabāti + Funkciju flags atiestatīti + Funkciju flags kopēti starukopētāpapanā + Restartējiet lietotni, lai izmantotu šo iestatījumu + Eksportēt un importēt + Importēti %s funkciju karodzi + Nevēno funkciju karogus + Funkciju karogi eksportēti veiksmīgi + Funkciju karogu eksportēšana neizdevās + Funkciju karogi ielādēti, nospiediet Saglabāt, lai tos pielāgotu + Faila nolasīšana neizdevās + Binārā meklēšana + "Atrod, kurš funkciju karogs rada lietotnes uzvedību. + +Pusi no %s karogiem ir bloķēti, un pēc katra restartēšanas jūs paziņojat, vai uzvedība joprojām ir novērojama. + +Pirms sākšanas vienreiz reproducējiet uzvedību, pretējā gadījumā meklēšanā truks karogi, kurus lietotne vēl nav pieprasījusi" + Sākt + "Nav funkciju karogu meklēšanai. + +Binārai meklēšanai var izmantot tikai tos karogus, kurus lietotne ir sasniegusi un kuri pašlaik nav piespiedīgi ieslēgti" + "Solis %1$s + +Atliku %2$s karstोगी karogi, no tiem %3$s bloķēti šim solim. + +Restartējiet lietotni, pārbaudiet, vai rīcība joprojām ir klāt, tad atbildiet zemāk" + Notiek funkciju karogu meklēšana + Rīcība ir klāt + Rīcība nav klāt + Atcelt bināro meklēšanu + Binārā meklēšana atcelta + "Funkciju karogs %s izraisa šo rīcību. + +Tas tagad ir bloķēts" + "Neizdevās atrast funkciju karogu. + +Šķiet, ka lietotnes rīcību nepavاده funkciju karogs" + "Neizdevās atrast nevienu funkcijas karogu. + +Uzvedība izspyra agrāk meklēšanas procesā, tāpēc var būt iesaistīti vairāki karogi vai viena no atbildēm bija nepareiza" + Atvērt MicroG iestatījumus + MicroG nav instalēts. Instalējiet to + Nepieciešama rīcība + "Jūsu MicroG v%1s versija ir novecotēja un var radīt problēmas. + +Lūdzu, atjauniniet MicroG līdz jaunākajai versijai v%2s" + Atjaunināt + Ignorēt + "Instalēto MicroG neiesaka Morphe: + +%1s + +pieskarieties, lai to deinstalētu, pēc tam atvēriet šo lietotni vēlreiz un instalējiet MicroG RE" + Atinstalēt + "MicroG nav atļaujas darboties fonā. + +Sekojiet rokasgrāmatai \"Don't kill my app\" savam tālrunim un piemērojiet instrukcijas savai MicroG instalācijai. + +Tas ir nepieciešams, lai lietotne darbotos" + Atvērt tīmekļa vietni + "Lai izvairītos no problēmām, MicroG akumulatora optimizācijai jābūt izslēgtai. + +Akumulatora optimizācijas izslēgšana MicroG neiets negatīvi akumulatora izmantošanai. + +Nospiediet turpināt pogu un atļaujiet mainīt optimizāciju" + Turpināt + Rādīt MicroG akumulatora optimizācijas dialogu + "Izslēdzot šo iespēju, tiks slēpts MicroG akumulatora optimizācijas azaudzums. + +Ja akumulatora optimizācijas netiek mainītas, iespējams, varat novērot: +• Lietotne var nejauši aizvērties +• Paziņojumi var nedarboties +• Fona atskaņošana var nedarboties" + Lietotnes saites apstrāde + Konfigurēt saites, lai tās atvērtu šajā lietotnē + 1. solis: Atspēkējiet oriģinālās lietotnes saites + Atveriet oriģinālās lietotnes iestatījumus un atspēkējiet visas domēnu saites + 2. solis: Aktivizējiet labotas lietotnes saites + Atveriet labotas lietotnes iestatījumus un aktivizējiet visas domēnu saites + Atvērt iestatījumus + 🚨 Eksperimentāls brīdinājums 🚨 + "Jūs izmantojat eksperimentālu lietotnes versiju ⚠️ <b>%1$s</b> +<br/><br/> +🔧 Būdiem iespējama dīvaina lietotnes darbība vai neatklāti kļūdas, kamēr mēs precizējam šīs versijas labojumus. +<br/><br/> +Ja vēlaties pēc iespējas stabilāku pieredzi, tad <b>deinstalējiet</b> šo versiju un instalējiet iesakāmo lietotnes versiju ✅ <b>%2$s</b>" + ⚠️ Es vēlos eksperimentālo versiju + ✅ Es vēlos stabilo versiju + Avārijas žurnāls saglabāts lietotāja dokumentos: %s + Avārijas žurnāls saglabāts: %s diff --git a/patches/src/main/resources/addresources/values-lv-rLV/sponsorblock/strings.xml b/patches/src/main/resources/addresources/values-lv-rLV/sponsorblock/strings.xml index cbef5f916a..729d5e0c8e 100644 --- a/patches/src/main/resources/addresources/values-lv-rLV/sponsorblock/strings.xml +++ b/patches/src/main/resources/addresources/values-lv-rLV/sponsorblock/strings.xml @@ -17,6 +17,7 @@ Second \"item\" text" --> Izskats + Izmantot kvadrātveida stūrņus SponsorBlock pogām un vadības elementiem Kopēt Sponsoru reklāma @@ -25,6 +26,7 @@ Second \"item\" text" Statistika + Ielādē… %1$s minūtes %2$s sekundes %s sekundes diff --git a/patches/src/main/resources/addresources/values-lv-rLV/youtube/strings.xml b/patches/src/main/resources/addresources/values-lv-rLV/youtube/strings.xml index 567f102aa6..f28922fb39 100644 --- a/patches/src/main/resources/addresources/values-lv-rLV/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-lv-rLV/youtube/strings.xml @@ -32,247 +32,1621 @@ Second \"item\" text" Noņemt atskaņošanas fonā ierobežojumus Atspējot atskaņošanu fonā + Automātiski apturēt, kad ierīce ir bloķēta Izslēgts + Vienmēr apturēt, kad bloķēta + Apturēt, ja nav pievienots bezvadu audio + Atslēgt ritēšanas ātruma ierobežojumu + Aizliegt izkārtojuma atjauninājumus + Novērs serverim atjaunināt lietotnes izkārtojumu + "Lietotnes izkārtojums tiek atgriezts pie izkārtojuma, kas tika izmantots pirmajās instalēšanas laikā. + +Daži servera puses izkārtojumi var netikt atgriezti. + +Izmaiņas ietver: +• Komponenti atskaņotāja izvērtējuma izvēlnē (vai saistītos iestatījumos) var nedarboties +• Skaitītāja cipari netiek animēti +• Tiek rādīta bibliotēkas cilne +• Video apraksta mūzikas sadaļa var nedarboties +• Atskaņotāja izvērtējuma izvēlnes elementus nevar slēpt +• Konta pārslēgšanas poga var neparādīties bibliotēkas cilnē" + Iespējot plūsmas izlaiduma izvēlnes filtrs + Plūsmas izlaiduma izvēlnes filtrs + Ievadiet izlīdjošās izvēlnes nosaukumus filtrēšanai, atdalot tos ar jaunām rindām + Iespējot Bibliotēkas / Tavas apakšizvēlnes filtru + Bibliotēkas / Tavas apakšizvēlnes filtrs + Ievadiet Bibliotēkas / Tavas apakšizvēlnes nosērumus filtrēšanai, atdalot tos ar jaunām rindām + Slēpt albumu kartes + Paslēpj albumu kartes zem mākslinieka apraksta + Slēpt mākslinieku kartes + Paslēpj mākslinieka karti meklēšanas rezultātu augšdaļā + Slēpt kanāla pogas + Slēpj tādas pogas kā Pierakstīties, Skatīt kanālu, Kopiena un YouTube Music no kanālu profiliem meklēšanas rezultātos + Slēpt kopienas ierakstus + Slēpt kompaktos bannerus + Paslēpj kompaktos bannerus un atstarpes starp dažām komponentēm + Izvidablejas kartes zem video + Rādīt visus + Slēpt tikai kopsavilkuma karti + Slēpt visu + Slēpt pelējošo mikrofona pogu + Paslēpj pelējošo mikrofona pogu meklēšanas ieteikumu ekrāna apakšējā labajā stūrī + Slēpt \"Iegūt Premium\" pogu + Slēpt vēstures pl Auktu + Slēpt horizontālās plauktus + Slēpj horizontālos plauktiem, piemēram, \"Breaking news\", \"Community posts you may like\", \"Continue watching\", \"Explore more channels\", \"Explore more topics\", \"For You\", \"Latest videos\", \"Most relevant\", \"Posts from Creator\'s Community\", \"Shopping\" un \"Watch it again\" + Slēpt attēlu plauktu + Nerādīt attēlu plauktus meklēšanas rezultātos + Slēpt karti \"Uzaicini citus sūtīt ziņas\" + Slēpj uzaicinājuma karti, kas tiek rādīta paziņojumu kolnas ziņojumu sadaļā + Slēpt pogu \"Jaunākie video\" + Nerādīt zilo banneri \"Jaunākie video\", kas parādās Sākuma plūdes augšdaļā + Slēpt tiešraides + Slēpj video, kas šobrīd tiek translēti tiešraidē sākumlapas plūdumā un meklēšanas rezultātos + Slēpt miksu atskaņošanas sarakstus + Slēpt filmu sadaļu + Slēpt Paziņojumu izvēlnes galveni + Slēpj Paziņojumu izvēlnes galveni, nospiežot zvana pogu + Slēpt pogu \"Brīdināt mani\" + Slēpj atgādinājuma pogu plānotajām tiešraidēm un premjerām + Slēpt spēles + Klusināt funkciju, kas ļauj spēlēt spēles + Slēpt meklēšanas terminu miniaturām + Nerādīt miniaturu prizkadrus meklēšanas automātiskās papildināšanas sarakstā + Slēpt pogu \"Rādīt vairāk\" + Nerādīt paplašināšanas pogu zem video meklēšanas rezultātos + Slēpt pierakstīto kanālu joslu + Slēpt pierakstīto kanālu nosaukumus + Slēpj kanālu nosaukumus pierakstīto kanālu joslā + Slēpt aptaujas + Slēpt aptaujas un atgādinājumus par plūdes rekomendāciju precizitāti + Slēpt biļešu plakti + + Slēpt uznīci \"Nepieejamie video ir slēpti\" + Slēpj uznīci \"Nepieejamie video ir slēpti\" atskaņošanas sarakstos + Slēpt augšupielādes laiku + "Ierobežojumi: +• Shorts plauktos, kanālu lapās un meklēšanas rezultātos joprojām var būt redzams augšupielādes laiks +• Nedarbojas ar automobiļa izkārtojumu" + Slēpt video miniaturu + Slēpt video etiķetes + Slēpj tādas etiķetes kā „Autodublēts“ / „Dublēts“, „Saņem komisiju“, „Hype“, „Citi arī skatīja“, „produkts(-i)“ un „Jums var patikt arī“ + Slēpt skatījumu skaitu + "Ierobežojumi: +• Shorts plauktos, kanālu lapās un meklēšanas rezultātos joprojām var būt redzams skatījumu skaits +• Nedarbojas ar automobiļa izkārtojumu" + Slēpt tīmekļa meklēšanas rezultātus + Slēpj sadaļu \"Result from the web\" meklēšanas rezultātos + Slēpt sadaļu \"Varbūt jums patiks\" + Slēpt YouTube Doodles + Nerādīt sezonālas un ar pasākumiem saistītas animācijas YouTube logotipā + YouTube Doodles rādīšana var traucēt pielāgotu galvņu rādīšanai + Slēpt kanāla joslu + Slēpt kanāla ūdenszīmi + Slēpt nodaļas un pogu \"Šajā video\" + Slēpt crowdfunding sadaļu + Slēpt ārkārtas situāciju informācijas kastes + Slēpt informācijas paneļus + Nerādīt informatīvus konteksta paneļus, piemēram, medicīnisku informāciju + Slēpt pogu \"Pievienot\" + Slēpt pogu \"Tiešraidē veiktās sarunas atkārtošana\" + Slēpj čata atkārtošanas pogu piln ekrāna režīm + Slēpt medicīnas paneļus + Slēpt atskaņātāja žestu padomus + Slēpj uzvedības pamācības, kas tiek rādītas spēlētāja žestu laikā, piemēram, \"Velciet uz augšu, lai precīzi meklētu\", \"Atlaižiet, lai atceltu\" un \"2x\" + Slēpt paziņojuma joslu + Slēpt abonentu noteikumus + Slēpt pogu \'Sincronizēt ar video\' + Slēpt pieskaršanās un turošanas gradientu + Slēpj atskaņātāja vadības gradientu, bīdot pirkstu, turot pogu, lai paātrinātu atskaņošanu + Slēpt laikاً ierobežotas reakcijas + Slēpj funkciju, kas ļauj lietotājiem reaģēt ar emocijzīmēm komentāru sadaļā un tiešraidē + Slēpt izvēlni \"Palīdzība un atsauksmes\" + Slēpj izvēlni \"Palīdzība un atsauksmes\", izmantojot trīs punktu pogu, piemēram, meklēšanas rezultātos + Slēpt rokturi + Slēpj jūsu kanāla pseidonomu cilnē \"Jūs\" + Slēpt video nosaukumu + Slēpj video nosaukumu piln ekrāna režīm + Tīrīt video subtitrus + No video subtitriem izdzēš tādas frāzes kā \'#\', \'Ziedot\', \'produkts(i)\' un \'Veikals\' + Slēpt pogu \"Ask Gemini\" + Slēpj pogu \"Ask Gemini\" paneļa \"Ask about this video\" apakšdaļā + Video apraksts + Izvēlieties, kuras komponentes parādīties video aprakstā + Slēpt sadaļu \"AI ģenerēts video kopsavilkums\" + Slēpt \"Ask\" sadaļu + Slēpt \"Attributes\" sadaļu + Slēpj sadaļas \"Featured places\", \"Games\", \"Music\" un \"People mentioned\" + Slēpt kanāla saišu sadaļu + Slēpt nodaļu sadaļu + Slēpt labojumu sadaļu + Slēpt sadaļu \"Kursa progress\" + Slēpt sadaļu Explore + Slēpj sadaļas Explore this course un Explore the podcast + Slēpt sadaļu \"Explore this course\" + Slēpt sadaļu \"Explore the podcast\" + Slēpt izceltو kanālu sadaļu + Slēpt sadaļu Featured links + Slēpt sadaļu \"Izceltās atskaņošanas sarakstu\" + Slēpt sadaļu Featured videos + Slēpt sadaļu \"Hashtags\" + Slēpt sadaļu \"Kā šis tika izveidots\" + Slēpt Hype punktus + Slēpt sadaļu Info cards + Slēpt sadaļu \"Galvenie jēdzieni\" + Slēpt sadaļu Quizzes + Slēpt sadaļu \"Meklēt šajā video\" + Slēpt pogu \"Pierakstīties\" + Slēpt sadaļu Transcript + Slēpt sadaļu \"Video detaļas\" + Filtra josla + Izvēlieties, kur slēpt ritinājamo filtru joslu + Slēpt filtru joslu kanāla lapā + Slēpt komentāros + Slēpt plūsmās + Slēpt saistītos video + Nerādīt meklēšanas rezultātos + Slēpt skatīšanās vēsturē Kanāla lapa + Izvēlieties, kuri komponenti parādīsies kanāla lapā + Iespējot kanāla cilnes filtrs + Kanāla cilnes filtrs + Ievadiet kanāla cilneņu nosaukumus filtrēšanai, atdalot tos ar jaunām rindām + Slēpt pogu \"Kopiena\" + Slēpt pogu \"Pievienoties\" + Slēpt saišu prizkadru + Slēpj noklikšķinājamās tīmekļa saites kanāla galvenajā daļā + Slēpt marķējumu \"Tikai dalībniekiem\" + Slēpj mikroshēmu \'Tikai biedriem\' kanālu filtru joslā + Slēpt dalībnieku plักti + Slēpj kanāla dalībnieku sarakstu + Slēpt ierakstu plakti + Slēpt pogu \"Veikals\" + Slēpt pogu \"Pierakstīties\" + Izpētīt izvēlni + Izvēlieties, kuras komponentes parādīs izpētīšanas izvēlnē + Slēpt Kursu izvēlni + Slēpt Izpētīt pogu + Slēpt \'Mode & skaistums\' izvēlni + Slēpt Spēļu izvēlni + Slēpt Hype izvēlni + Slēpt mācīšanās izvēlni + Slēpt Tiešraides izvēlni + Slēpt Dalības izvēlni + Slēpt izvēlni \'Filmas & TV\' + Slēpt izvēlni \'Mūzika\' + Slēpt Ziņu izvēlni + Paslēpt Playables izvēlni + Paslēpt podkāstu izvēlni + Paslēpt iepirkšanās izvēlni + Paslēpt sporta izvēlni + Paslēpt YouTube Premium izvēlni + Paslēpt YouTube Studio izvēlni + Slēpt YouTube Music izvēlni + Slēpt YouTube Kids izvēlni + Slēpt YouTube izveides izvēlni + Slēpt izvēlni \"Kā darbojas YouTube\" + Slēpt juridiskos paziņojumus un privātuma informāciju apakšējā joslā Komentāri + Izvēlieties, kuras komponentes parādīt komentāru sadaļā + Iespējot komentāru karuseles filtru + "• Pārliecinieties, ka ievادات precīzu karuseles elementa nosaukumu (piemēram, Ask, Hype this video, Listen on YouTube Music) +• Karuseles elementu slēpšanai var būt nevēlamas blakusiedarbības +• Ja visi karuseles elementi ir slēpti, tiks slēpts arī viss komentāru sadaļa" + Komentāru karuseles filtrs + Ievadiet komentāru karuseles nosaukumus filtrēšanai, atdalot tos ar jaunām rindām + Slēpt AI tērzēšanas kopsavilkumu + Slēpt kanāla noteikumus + Slēpt konteksta ziņojumus + Slēpt virsrakstu \"Komentāri no dalībniekiem\" + Slēpt komentāru sadaļu + Slēpt komentāru sadaļu Sākuma plūdumā + Slēpt kopienas noteikumus Slēpt \"Izveidot īso video\" pogu + Slēpt pogu \"Nepatika\" + Slēpt Emoji pogu + Slēpt filtra joslas iespējas + Slēpj visas filtra joslas iespējas, izņemot \"Populārākos\" un \"Jaunākos\" + Slēpt dāvanu animāciju un kartes + Slēpt dāvanu pogu + Slēpt informācijas pogu + Slēpt tiešraidē tēlatāju ziedotāju joslu + Slēpt tiešraidēs čata pogu \"skrollēt līdz apakšai\" + Slēpt tiešraides tērzēšanas paskaidrojumus + Slēpj paskaidrojumu uznirstošos logus, kas parādās tērzēšanas ievades joslas tuvumā, piemēram, paziņojumu par tērzēšanu tikai abonentiem + Slēpt izvēlnes pogu + Slēpj 3 punktu izvēlnes pogu katram komentāram + Slēpt prizkadra komentāru + Slēpj komentāru priekšskatu zem video pirms pilnas komentāru sadaļas atvēršanas + Komentāru slēpis Morphe… + Iespēgt minimālu komentāru pogu + Pāslēpj slīdošās kartes un tās lapas atlasītāju, kas tiek rādītas uz komentāru pogas + Slēpt Paldies pogu + Slēpt laika zīmju pogu + Slēpt pogu \"Top fans\" + Slēpt tulkošanas pogu + Slēpt ziņojumu \"Komentāru vairs nav\" + Tīrīmeklēt izceltās meklēšanas saites + Tīrīmeklē izceltās meklēšanas saites, atstājot tikai oriģinālo vienkāršo tekstu + Atslēgvārdu filtrs + Izvēlieties konkrētus vārdus vai frāzes, lai slēptu video plūsmēs un meklēšanā + Slēpt komentārus pēc atslēgvārdiem + Slēpt Sākuma lapas video pēc atslēgvārdiem + Slēpt meklēšanas rezultātus pēc atslēgvārdiem + Slēpt Pierakstīšanos video pēc atslēgvārdiem + Atslēgvārdi, kas jāslēpj + "Ievadiet atslēgvārdus un frāzes, kuras vēlaties slēpt, atdalot tās ar jaunām rindām + +Atslēgvārdi var būt jebkurš teksts, kas redzams video nosaukumos vai kanālu nosaukumos + +Vārdiem ar lielajām burvām vidū jābūt precīzi saskaņotiem (piemēram, iPhone, TikTok, LeBlanc)" + "Sākums / Abonētie / Meklēt / Komentāri tiek filtrēti, lai slēptu saturu, kas atbilst atslēgvārdu frāzēm + +ierobežojumi: +• Shorts nav iespējams slēpt pēc kanāla nosaukuma +• Daži UI komponenti var nebūt slēpti +• Meklējot atslēgvārdu, rezultāti var nebūt" + Skaidrot tikai veselus vārdus + Atstarojot atslēgvārdu/frāzi ar dubultajām quotation markām, tiks novērstas video nosaukumu un kanālu nosaukumu daļējas sakritmes<br><br>Piemēram,<br><b>\"ai\"</b> slēps video: <b>How does AI work?</b><br>bet neslēps: <b>What does fair use mean?</b> + Nevar izmantot atslēgvārdu: %s + Pievienojiet quotation markas, lai izmantotu atslēgvārdu: %s + Atslēgvārdam ir pretrunīgas deklarācijas: %s + Atslēgvārds ir pārāk īss un tam nepieciešamas quotation markas: %s + Atslēgvārds slēps visus video: %s + "• Sākums: %s +• Abonementi: %s +• Meklēšana: %s +• Komentāri: %s +• Kopā: %s" Statistika + Slēptie video, pēdējās 24 stundas + Slēpti video, kopā + Atstatīt pēdējo 24 stundu skaitītāju? + Atstatīt visu laika skaitītāju? + AI kanālu filtrs + Slēpt video no kanāliem, kurus AiSList kopienas sarakstā identificējis kā AI ģenerētu saturu + Bloķēšanas saraksts: Augsts pārliecinājums + Brīdinājumu saraksts: Vidējs pārliecinājums + Slēpt video Sākumlapā + Slēpt video meklēšanā + "• Sākums: %s +• Meklēšana: %s +• Kopā: %s" + Bloķēšanas saraksta kanāli + Brīdinājumu saraksta kanāli + Ienable filtru, lai ielādētu kanālus + Ielādē kanālus… + "Sākuma lapas un meklēšanas rezultāti tiek filtrēti, lai slēptu video no kanāliem, kurus atzīmējis AiSList + +ierobežojumi: +• Filtrēti AI Shorts var izs Modes kā tukšs alanı +• Daži AI video var neatjaugot" + "AiSList ir atklātā pirmkoda YouTube kanālu saraksts, kuros publicē mākslīgā intelekta ģenerēts saturs, un to uztur brīvprātīgie. Kanālus ziņo lietotāji, un tie tiek pārbaudīti pirms pievienošanas + +Saraksts tiek lejupielādēts no GitHub, un video tiek filtrēti jūsu ierīcē" + Ziņot par AI kanāliem + Pievienot ziņošanas ierakstu video izvēlnei + Pievienot ierakstu video izvēlnei, lai ziņotu par video kanālu AiSList + Ziņojošā vārds + Vārds, kas redzams blakus jūsu ziņojumiem publiskajā AiSList līderu tabulā. Ziņošana nav pieejama, kamēr šis lauks ir tukšs + Ziņot par AI kanālu + "Iesniegt %s AiSList, izmantojot šo video kā pierādījumu? + +Ziņojiet tikai par kanāliem, kas: +• izmanto AI balsis, vizuālos tēlainus, scenārijus vai mūziku kā savu galveno saturu +• masveidā rada saturu ar AI rīkiem un minimālu cilvēka līdzdalību + +Neziņojiet par kanāliem, kas tikai diskutē par AI vai izmanto AI dažām savu video daļām. + +Ziņojumi ir publiski un tiek pārskatīti pirms kanāla pievienošanas sarakstam. Noņemšana ir iespējama tikai, iesniedzot pieprasījumu GitHub" + Šis kanāls jau atrodas AiSList bloķēšanas sarakstā + Šis kanāls jau atrodas AiSList brīdinājumu sarakstā + Ziņot + Lai ziņotu par kanāliem, norādiet iesniedzēja vārdu AI kanālu filtra iestatījumos + Meklē kanālu… + Nevar atrast šī video kanālu + Kanāls ziņots un gaida pārbaudi + AiSList nevarēja saglabāt ziņojumu, mēģiniet vēlreiz vēlāk + Nosūtītu ziņojumu terlalu daudz, mēģiniet vēlreiz vēlāk + AiSList noraidīja ziņojumu + Ziņojuma sūtīšana neizdevās, pārbaudiet savu savienojumu + Kanālu reklāmu baltais saraksts + Video no baltā sarakstā iekļautajiem kanāliem tiek atskaņoti ar reklāmām, lai to izveidotāji saņemtu samaksu. Visos pārējos kanālos reklāmas paliek bloķētas + Pievienot kanālu reklāmu baltajam sarakstam + Noņemt kanālu no reklāmu baltā saraksta + Slēpt izveidēja veikala plauktu + Slēpt veikala baneru noslēguma ekrānā + Slēpt vispārīgas reklāmas + Slēpt preču bannerus + Slēpt apmaksātas reklāmas marķējumu + Slēpt sasaules uznirstošās reklāmas + Slēpt pašreklāmas kartes + Slēpt iepirkšanās saites + Slēpt video reklāmas + Slēpt YouTube Premium reklāmas + Ielādē… + Pašreizējais kanāls: + Pievienot baltajam sarakstam Noņemt + Baltā sarakstā iekļautie kanāli + Neviens kanāls nav iekļauts baltajā sarakstā + Kanāls pievienots baltajam sarakstam + Kanāls noņemts no baltā saraksta + Meklēt kanālā + Pievieno otro meklēšanas pogu kanālu lapās, lai meklētu tikai tajā kanālā + Meklēt kanālu + Kanālu meklēšanas rezultāti + Šajā kanālā nekas netika atrasts + Neizdevās meklēt šajā kanālā + Video + Kārtījumā + Šajā kanālā nav atrastu kārtījumu + Atbilstība + Visvairāk skatītie + Vismazāk skatītie + Jaunākie + Vecākie + Īsākie Short + Garākie + Saite nokopēta kopieršanas trauksmei + Saite ar laika zīmi nokopēta kopieršanas trauksmei + Rādīt pogu \'Kopēt video saiti\' + Kārtā, lai nokopētu video saiti; touch un turi, lai nokopētu ar laika zīmi + Nokopēt laika zīmītu noklusinājumu pēc noklusējuma + Maina pogas \'Kopēt video saiti\' pieskaršanās un ilgās turēšanas darbības + Kopēt video nosaukumu + Kopēt video nosaukumu, pieskaroties tam un turot + Video nosaukums nokopēts starukopētni + Kopēt komentārus + Kopēt komentāru, pieskaroties tam un turot + Komentārs nokopēts starukopētni + Pieskarieties un aizturiet, lai atvērtu video izvēlni + Viڈیو plūdes jebkurā vietā ilgi pieskaroties atver tā izvēlni + Rādīt pogu \"Skatīt vēlāk\" miniaturēs + Video thumbnailiem plūsmā tiek rādīta poga \"Skatīt vēlāk\". Nospiežot un turot to, atveras iespēja \"Saglabāt sarakstā\" + Lejupielādēt ieraksta attēlus + Lejupielādes poga tiek rādīta blakus Dalīties pogai ierakstiem ar attēliem vai GIF failiem, un to izvēlne tiek papildīta ar lejupielādes vienumu + Lejupielādēt attēlus + Lejupielādēt attēlu + Lejupielādē attēlu… + Attēls saglabāts %s + Attēla lejupielāde neizdevās + Šajā ierakstā attēli netika atrasti + Pievienot \"Ask\" vienumu izlaidnotifiera izvēlnei + Pievieno video kopsav𝟐uma vienumu plūsmas izlaidnotifiera izvēlnei + "Lai izmantotu funkciju Uzdot jautājumu, sākotnēji nepieciešams pierakstīties kontā valstī, kurā funkcija Uzdot jautājumu ir pieejama. + +Kad videoklips ar pogu Uzdot jautājumu ir atvērts vienreiz, šī funkcija darbojas visos videoklipos, pat ja pašreizējā valstī funkcija Uzdot jautājumu nav pieejama." + Apkopot videoklipu + Neizdevās atvērt Uzdot jautājumu šim videoklipam + Funkcija Uzdot jautājumu nav pieejama jūsu kontam vai pašreizējai valstij + Rādīt skatījumu skaitu miniaturēs + Sīles video skatījumu skaits tiek rādīts miniaturu apakšējā kreisajā stūrī, nevis zem virsraksta. Daži video, piemēram, plauktos, var nerādīt skatījumu skaitu + Rādīt plūmes miniaturu patika skaitu + Rādīt plūmes miniaturu patika procentu + Meklēt komentāros + Nospiediet meklēšanas pogu komentāros, lai rādītu tikai tādus ielādētus komentārus, kuros ir visi meklētie vārdi. Ielieciet vārdus quote zīmēs, lai meklētu frāzi + Meklēt komentāros + Vārdi meklēšanai Meklēt + Notīrīt + Mainiet šķirošanas secību vai atveriet komentārus vēlreiz, lai meklētu tos + Meklēšana notīrīta. Mainiet šķirošanas secību vai atveriet komentārus vēlreiz, lai rādītu visus komentārus + Atslēgt uznirstošo logu \"Pierakstīties TV\" + Aizliegt uznirstošo logu \"Pieslēdziet savas ierīces\" + Atslēgt nodaļu izlaišanu ar dubulto pieskārumu + Atslēgt divu pirkstu dubultā pieskāriena žestu nodaļu izlaišanai, kuru var nejauši aktivizēt ar vienu pirkstu + Lejupielādēt video, podkastus vai dziesmas, izmantojot trešo pušu lejupielādes lietotnes + Rādīt atladēšanas pogu atskaitslēdēja pārklājumā + Rāda pogu video atskaitslēdējā, kas atver jūsu ārējo lejupielādētāju + Ātra lejupielāde ar vienu pieskārumu + Aicinājums lejupielādēt fonā. Pieskaras un aizturi, lai atvērtu lejupielādētāju. Strādā ar YTDLnis un YTindir + Atzīmēt lejupielādes kānoskatītas + Ar ārējo lejupielādētāju lejupielādētās video tiek pievienotas jūsu YouTube skatīšanas vēsturei + Pilns HD (1080p) + Vidējs (192 kbps) + Ātra lejupielāde Video + Audio + Visas opcijas + Rādīt lejupielādi Shorts izvēlnē + Pievieno ārējo lejupielādētāju izvēlnes vienumu Shorts sākņu atskaitevietnē + Lejupielādēt + Balss tulkojums + Lasīt video titrus skaļā, izmantojot ierīces teksta sintēzi, sinhronizēti ar atskaņošanu + Rāda pārklājuma pogu video spēlētājā. Pieskarieties, lai pārslēgtu tulkojumu, vai nospiediet un aizturiet, lai piekļūtu ātrajām iestatījumiem + Valoda + Tulkošanas pakalpojumu sniedzējs + Oriģinālā skaņa + Oriģinālā YouTube skaņas līmenis laikā, kad tiek atskaņots tulkojums + Tulkojuma skaļums + Dublētā tulkojuma balss skaļums + Maksimālais runas ātrums + Maksimālais ātruma reizēkslējeklis, pielāgojot runu subtitru slotam + "Bezmaksas tulkošanas API, API atslēga nav nepieciešama. Bezmaksas līmenī līdz 5 000 rakstzīmēm dienā, vai līdz 50 000 rakstzīmēm dienā ar e-pasta adresi. + + pieskarieties šeit, lai atvērtu MyMemory.translated.net" + MyMemory e-pasts + Neobligāti. Ievadiet savu MyMemory e-pasta adresi, lai palielitu päivas limitu no 5 000 līdz 50 000 rakstzīmēm. Atstājiet tukšu, lai izmantotu anonīmo limitu + "Maksīgs AI tulkojums caur OpenRouter. Uzlādējiet saldo vienreizēji un tulkojiet tūklaļus video. Mistral Nemo ir lētākā izvēle, Gemma 4 piedāvā augstāko kvalitāti. + +pieskarieties šeit, lai atvērtu OpenRouter.ai" + OpenRouter API atslēga + Jūsu OpenRouter API atslēga. Izveidojiet atslēgu vietnē OpenRouter.ai > Workspace > API Keys + OpenRouter modelis + Pielāgots + OpenRouter nav konfigurēts + Ievadiet savu OpenRouter API atslēgu iestatījumos, pirms izvēlaties šo pakalpojumu sniedzēju + Bezmaksas + %s par 100 video stundām + Balss + Sistēma + Daudzvalodu vīrietis + Daudzvalodu sieviešu balss + Vīriešu balss + Sieviete + Ir morfinga laiks! + Tulkojums nav pieejams + "Tulkošanas pakalpojums atgrieza HTTP kļūdu %s. Jūsu IP adrese, visticamāk, ir ierobežota līdz pat 24 stundām. + +Lai turpinātu izmantot tulkojumus, iestatījumos pārslēdzieties uz citu tulkošanas pakalpojumu vai izmantojiet VPN" + Nerādīt vairs + "Jūsu iestatījumos ir OpenRouter API atslēga. + +Jūsu API atslēgu nedrīkst nekad kopīgot. Ikvaiens, kuram ir piekļuve tai, var izmantot jūsu OpenRouter kredītus" + Sasiets OpenRouter kredīta limits + Jūs sasniegāt savu OpenRouter kredītu limitu. Uzlādējiet savu saldo vai palieliniet atslēgas tērēšanas limitu vietnē OpenRouter.ai + Atvērt OpenRouter.ai + OpenRouter tulkošana neizdevās (HTTP %s) + Sasięgts MyMemory dienas limits + Nākamais pieejams %s + Jūs izmantojāt šodienas MyMemory bezmaksas tulkošanas kvotu (5 000 rakstzīmes). Pievienojiet MyMemory e-pastu iestatījumos, lai palielitu dienas limitu līdz 50 000 rakstzīmēm, vai pārslēdzieties uz citu tulkošanas pakalpojumu + Jūs izmantojāt šodienas MyMemory tulkošanas kvotu (50 000 rakstzīmes). Pārslēdzieties uz citu tulkošanas pakalpojumu vai mēģiniet vēlreiz rītdien + MyMemory tulkošana neizdevās (statuss %s) + Iespēgt video atkārtošanu + Neatcerēties video atkārtošanos + Noglabāt video atkārtošanas pašreizējo stāvokli nākamajiem video + Rādīt pogu \"Atskaidīt cilpu\" + Video atkārtošana ir iespēgta + Video atkārtošana ir izslēgta + Atkārtošanās diapazons + Sākums + Nobeigums + [st:]mm:ss + [st:]mm:ss (tukšs = video beigas) + Iestatīt + Notīrīt + Atkārtots %1$s - %2$s + Kļūda laika diapazonā + Rādīt pogu \"Attēls attēlā\" + Rādīt izvēlni \"Attēls attēlā\" + Pāriet uz režīmu \"Attēls attēlā\" + Rādīt pogu \"Atskaidīt visu\" + "Pieskarcieties, lai izveidotu atskaņošanas sarakstu ar visiem kanāla video, pieskarieties un aizturiet, lai atceltu + +Info: Var nedarboties tiešraidēs" + Saraksta ģenerēšanas režīms + Visos saturos (kārtot pēc laika, augoši) + Visos saturos (kārtot pēc laika) + Visos saturos (kārtot pēc popularitātes) + Tikai video (kārtot pēc laika) + Tikai video (kārtot pēc populārākajiem) + Tikai Shorts (kārtot pēc laika) + Tikai Shorts (kārtot pēc populārākajiem) + Tikai tiešraides video (kārtot pēc laika) + Tikai tiešraides video (kārtot pēc populārākajiem) + Visi tikai dalībniekiem pieejamie saturi + Tikai dalībniekiem pieejami video + Tikai dalībniekiem paredzēti Shorts + Tikai dalībniekiem paredzētas tiešraides + Nevar izveidot atskaņojuma sarakstu kanāla ID neatbilstības dēļ + Rādīt pogu \"Atjaunot video\" + Video ID nav pieejams. Lūdzu, manuāli pārlādējiet video + Rādīt pogu Saglabāt skatīšanai vēlāk + Knīciet, lai saglabātu sadaļā \"Skatīsim vēlāk\", vai knīciet un turmeric, lai izvēlētos rindas iespējas + Video veiksmīgi saglabāts sarakstā \"Skatīt vēlāk\" + Šis video jau ir saglabāts sarakstā \"Skatīt vēlāk\" + Šo video nevar saglabāt sarakstā \"Skatīt vēlāk\" + Saglabāt sadaļā \"Skatīsim vēlāk\" + Rādīt pogu \"Saglabāt skatīšanai vēlāk\" spelētāja iestatījumu izvēlnē + Ļauj saglabāt pašlaik skatīto video sadaļā \"Skatīšanai vēlāk\" no video iestatījumu izvēlnes + Rādīt pogu \"Saglabāt sadaļā \"Skatīsim vēlāk\"\" bērnu video + Ļauj saglabāt bērniem paredzētus video sadaļā \"Skatīšanai vēlāk\" no izvēlnes + Rādīt pogu \"Saglabāt skatīšanai vēlāk\" Shorts izvēlnē + Ļauj saglabāt Shorts video sadaļā \"Skatīšanai vēlāk\" no izvēlnes + Pārvirzīt \"Skatīt vēlāk\" uz sarakstu + Skatīt vēlāk + Pievienot Morphe rindai + Saglabāt rindu starp sesijām + Rinda tiek saglabāta pēc lietotnes restartēšanas + Apmainīt saglabāšanas un rindas darbības + Knīciet, lai izvēlētos rindas iespējas, vai knīciet un turmeric, lai saglabātu sadaļā \"Skatīsim vēlāk\" + Atskaņot nākamo rindā tips + Pēc noklusējuma + Dzimtā rinda + Morphe rinda + Morphe rinda klienta pusē + Pievienot plūmes izlaiduma rindas izvēlnes elementu + Pievieno papildu izlaiduma elementu. I iespējo šo, ja \"Pievienot rindai\" netiek rādīts + At 재생as cērtne + Karta (%s) + Notīrīt visu + Cērtne ir tukša + Jebkuram video izmantojiet iespēju \"Atskaņot nākamo cērtnē\" + Atskaņo tagad + Atskaņot nākamo cērtnē + Atskaņot pēdējo rindā + Mainīt secību Noņemt + Pievienots cērtnei + Neizdevās pievienot šo video rindai + Atvasu nevar pievienot rindai + Cērtne ir pilna + Neizdevās started video + Reklāmu baltais saraksts izlaiduma izvēlnē + Pievieno izlaiduma elementu, kas ļauj reklāmām atskaņoties šī video kanālā + Šī funkcija var prasīt PotHelper izmantošanu vai \"Video straumju imitēšanu\" mainīšanu uz TV vai Android VR pieslēgšanos + Ātruma baltais saraksts izlaiduma izvēlnē + Pievieno izlaiduma elementu, kas vienmēr atskaņo šī video kanālu ar 1.0x ātrumu + Nav pieslēdzies + Nav saraksta ID + Cirta ir tukša + Nav video ID + Kļūda: %s + Neizdevās pievienot cirtai + Neizdevās izveidot cirtu + Neizdevās izdzēst no cirtas + Neizdevās saglabāt cirtu + Pievienots cirtai + Cirta izveidota + Noņemts no rindas + Karta saglabāta %s + Morphe rinda + Pievienot pie rindas + Pievienot rindai un atvērt + Pievienot rindai un atskaņot + Pievienot un pārlādēt video + Noņemt no rindas + Noņemt un atvērt rindu + Noņemt un pārlādēt video + Atvērt rindu + Saglabāt rindu sarakstā + Atskaņošanas saraksts saglabāts + Saglabāts %s + Notīrīt rindu + Karta iztīrīta + Karta + Atskaņot pēdējo kartē + Atskaņot nākamo kartē + YouTube nevarēja rādīt kartu. + Navējas nav gatava šim video. Atveriet videoatskaidītāju un mēģiniet vēlreiz. + Pievienot miksu navējai vai skatīt miksu? + Skatīt + Pievienot navējai + Ielādē miksu… + Atvērt kanālu no tiešraidēs esošā avatāra + Novērš tiešraides atvēršanu, nospiežot kanāla avatāru + Atslēgt precīzas pārvirzīšanas žestu + Atslēdz precīzas meklēšanas režīmu, kas aktivizējas, velkot meklēšanas joslu uz augšu + Iespējot tiešraides DVR + Ļauj pārvietoties visos tiešraides straumojumos, pat tajos, kur tas parasti ir ierobežots + I iespējot meklēšanas joslas miniaturu + Rāda video miniaturu, velkot meklēšanas joslu + Visbiežāk atkārtotais + "\"Iespējot tiešraides DVR\" var nebūt pieejams + +Lai to labotu, ieslēdziet \"Viltot video straumes\" un mainiet \"Noklusējuma klientu\" uz \"TV Simply\" vai \"visionOS\"" + Paplašināt tiešraides DVRilgumu + Ļauj atkārtoti noskatīties tiešraides līdz pat 7 dienām atpakaļ + "\"Paplašināt tiešraides DVRilgumu\" var nebūt pieejams + +Lai to labotu, ieslēdziet \"Viltot video straumes\" un mainiet \"Privāto klientu\" uz \"TV Simply\" vai \"visionOS\"" + I iespējot pārvilkšanu meklēšanai + Iespējo slīdēšanu meklēšanai tā vietā, lai atskaņotu ar 2x ātrumu, nospiežot un turot video plejeru + Iespējot pieskārumu meklēšanai + Vienkārši pieskarieties meklēšanas joslai, lai pārejiet uz šo pozīciju, nevelkot to + Slaucības žests pa kreisi + Slaucības žests pa labi + Slaucības žests uz augšu + Kreisās/labās zonas platums + Kopējā ekrāna malas procentuālais daļums, kas rezervēts kreisajiem un labajiem slīdšanas žestiem (5-50) + Spidrums + Skaļums + Dzimtais Izslēgts + Ignorēt slīdīšanas žestus, kad ekrāns ir bloķēts + Slīdīšanas žesti netiek ņemti vērā, kamēr atskaņotājs ir bloķēts ar funkciju \"Bloķēt ekrānu\" + Iespējot nospiešanas un slīdināšanas žestu + Prasa nospiest un aizturēt, pirms tiek atpzīts slaucīšanas žests + Iespējot haptisko atgriezenisko saiti + Saglabāt un atjaunot spidrumu + Atceras spidruma līmeni, ieejot / izejot no piln ekrāna režīma + Iespējot automātiskā spidruma žestu + Spidruma slīdīšana līdz pašai zemākajai vērtībai aktivizē sistēmas automātisko spidrumu + Automātiski + Slīdināšanas pārklājuma taimauts + Milisekundu skaits, kam laikā pārklājums ir redzams + Slīdināšanas pārklājuma fona caurspīdums + Adjustējiet caurskatu līmeni (0 ir caurskatu, 100 ir necaurskatu) + Slīda pārklājuma spidruma krāsa + Iestatiet spidruma vadības progresa joslas krāsu + Slīda pārklājuma skaļuma krāsa + Iestatiet skaļuma vadības progresa joslas krāsu + Slaucīšanas pārklājuma ātruma krāsa + Iestatīt atskaidzes ātruma vadības progresa joslas krāsu + Slīda pārklājuma teksta izmērs + Izmainiet ekrāna teksta izmēru (1-30) + Slīduma spēka sliekšņa + Iestatiet nepieciešamo slaidēšanas distances garumu, pirms tiek aktivizēta regulēšana + Skaļuma slaidēšanas jutlums + Augstākās vērtības prasa vairāk slaidēšanas katram skaļuma solim + Skaļuma soļi + Skaļuma pastiprinājums + Sviršana uz augšu pāri maksimālajam skaļumam to pastiprina, kas var izkropot skaļu audio + Ierīces noklusinājums + Spidra slaucīšanas jutlība + Augstākām vērtībām ir nepieciešams vairāk slaucīšanas katram spožuma solim + Ātruma slaucīšanas jutlība + Augstākām vērtībām ir nepieciešams vairāk slaucīšanas katram ātruma solim + Ātruma solis + Ātruma zonas augstums + Ātruma žestu zonas augstums kā procentuālā daļa no plejera augstuma (5-75) + Ātrums + Slīda pārklājuma stils + Horizontāls pārklājums + Horizontāls pārklājums (minimalistisks - augšā) + Horizontāls pārklājums (minimalistisks - centrā) + Apaļš pārklājums + Apaļa pārklājuma slāņa (minimalistiska) + Vertikāls pārklājums + Vertikāls pārklājums (minimalistisks) + Ienablekt slīdumu video maiņai + Slaidējiet pilnस्क्रीन režīmā, lai pārejiet uz nākamo vai iepriekšējo video + Darbības pogas + Izvēlieties, kuras darbības pogas rādīt portreta un pilnस्क्रीन režīmos + Portreta režīma pogas + Atslēgt \"Patikt\" un \"Pierakstīties\" mirdzēšanu + Novērš \"Patīkt\" un \"Pierakstīties\" pogu mirdzēšanu, ja izveidotājs tās piemin + Slēpt darbību joslu + Slēpt \"Patīkt\" un \"Nepatīkt\" pogas + Slēpt \"Kopīgot\" pogu + Slēpt pogu \" aptur reklāmas\" + Slēpt komentārus poga + Slēpt Pievienot pogu + Slēpt ziņot pogu + Slēpt pogu Remix + Slēpt pogu Lejupielādēt + Slēpt pogu Hype + Slēpt pogu Reklamēt + Slēpt pogu Paldies + Slēpt pogu Jautāt + Pāslēpt kanālu un Pierakstīties pogu + Pāslēpj kanāla avatāru un Pierakstīties pogu darbību joslas sākumā + Slēpt abonēšanas pogu + Slēpj abonēšanas un pievienošanās pogas blakus kanāla avataram darbības joslas sākumā + Slēpt pogu Izveidot klipu + Tas var nedarboties dažiem lietotāju kontiem + Slēpt pogu Veikals + Slēpt pogu Saglabāt + Slēpt pogu \"Vairāk\" + Piln ekrāna pogas + Slēpt pogu Nepatīk + Slēpt pogu Patīk + Slēpt \"Tiešsaistes tērzēšana\" pogu + Slēpt Mix pogu + Slēpt \"Vairāk video\" pogu + Slēpt At 재생listu pogu + Ātrās darbības augšējā margināle + Iestatīt ātrās darbības joslas augšējo margināli (0-32 dp) + Subtitri + Pārvaldistiet automātiskos titrus un titru failus pārlūka izvēlnē + Automātiskie subtitri - redzamība + Rādīt vienmēr + Neraidīt nekad + Rādīt, kad skaña ir ieslēgta + Raidīt, ja skaņa ir izslēgta + Vēlamā titru valoda + Vispirms izvēlas nodrošinātāja subtitrus, pēc tam automātiski tulko vēlamajā valodā Izslēgts + Pievienot papildu automātiska tulkošanas valodas + Pievieno vairāk valodu automātiskās tulkošanas izvēlnē + Iestatīt titru failu cookie + "Labo oriģinālo YouTube kļūdu, kuras dēļ automātiski tulkotie subtitri var nebūt pieejami trūkstošu failu izpērekšņu (cookies) dēļ + +Jānorāda \"Subtitru failu izpērekšņi\"" + Subtitru sīkfaili + Cookie, kas tiks iestatīti Timed Text API (Caption Data API) pieprasījumos + Iegūt titru sīkfailus + Noklikšķiniet šeit, lai iegūtu cookie + Neizdevās iegūt failus cookie + Faili cookie jau eksistē + Faili cookie veiksmīgi atjaunināti + Labot transkriptu + Novērš priekšnosacību pārbaudes kļūdu, kas var traucēt transkripcijas ielādei + Rādīt titrus attēlā attēlā + Navigācijas pogas + Izvēlieties, kuras pogas rādīt apakšējā navigācijas joslā + Paslēpt pogu Sākums + Paslēpt Shorts pogu + Paslēpt pogu Izveidot + Paslēpt pogu Abonētie + Paslēpt paziņojumu pogu + Rādīt meklēšanas pogu + Meklēšanas pogas pozīcija + Rādīt iestatījumu pogu + Iestatījumu pogas pozīcija + Iestatījumu pogas darbība + Ja iespējots, atver Morphe iestatījumus, ja nicht — YouTube iestatījumus + Zamainīt pogu \"Izveidot\" ar pogu \"Paziņojumi\" + Ja šī iestatījuma maiņa nesniedz rezultātu, mēģiniet pārslēgties uz inkognito režīmu + Slēpt navigācijas pogu etiķetes + Pāslēpt jauna satura punktu + Slēpt navigācijas joslu + "Tas pilnībā slēpj navigācijas joslu, ieskaitot cilni 'Tū'. + +Lai piekļūtu iestatījumiem, iespējojiet iestatījumu pogu sadaļā Vispārēji → Rīkjosla → Rādīt iestatījumus" + Ienablekt šauras navigācijas pogas + Samazina horizontālo attālumu starp pogām + Atslēgt navigācijas pogas bez etiķetēm + Rāda navigācijas pogas ar etiķetēm, ja YouTube tās sūta bez etiķetēm + Atslēgt rīkjoslānavigācijas cilnes + Rāda abonementus navigācijas joslā, ja YouTube pārvieto tos uz cilni mājaslapas augšdaļā + Ienablekt navigācijas joslas animācijas + Animē pārejas, pārslēdzoties starp cilnēm + Aizliegt navigācijas joslas automātisko slēgšanu + Atstāj navigācijas joslu redzamu ritināšanas laikā + Navigācijas joslas stils + Piecīrtā vērtība + Pusskaidrs + Caurskati + Nepārredzams + Šis stils var nebūt atbalstīts jūsu ierīcē vai YouTube versijā, kas var izraisīt izkārtojuma problēmas vai programmas avārijas + Rīkjosla + Izvēlieties, kuri komponenti parādās rīkjoslā + Slēpt Cast pogu + Slēpt tērzēšanas pogu + Slēpt mikrofona pogu + Slēpt meklēšanas pogu + Ja šī opcija ir iespējota, pieskaršanās atvērs Morphe iestatījumus, bet ilgspiešana atvērs YouTube iestatījumus; izslēdziet, lai apmainītu tos + Izlēktā izvēlne + Izvēlieties, kuras komponentes parādīties izlaidējamajā izvēlnē + Slēpt izvēlni \"Papildu iestatījumi\" / Pluss + Slēpt izvēlni \" Cidera režīms\" + "Izvēlnه 'Audio Track' ir slēpta + +Lai rādītu izvēlni 'Audio Track', mainiet 'Spoof video streams' uz jebkura klienta, izņemot 'Android Studio' un 'Android VR Downgraded'" + Slēpt izvēlni \"Audiocīkla izvēle\" + Slēpt izvēlnes \"Audiocīkla izvēle\" apakšu + Slēpt izvēlni \"Subtitri\" + Slēpt subtitru izvēlnes apakšpusi + Slēpt subtitru izvēlnes galveni + Slēpt izvēlni \"Palīdzība & atsauksmes\" + Slēpt izvēlni \"Klausīties, izmantojot YouTube Music\" + Slēpt izvēlni \"Bloķēt ekrānu\" + Slēpt izvēlni \"Atskaidīt video atkārtoti\" + Slēpt izvēlni \"Ceļā\" + Slēpt izvēlni \"Kvalitāte\" + Slēpt kvalitātes izvēlnes apakšu + Slēpt kvalitātes izvēlnes augšu + Slēpt izvēlni \"Tīmekļa taimeris\" + Slēpt izvēlni \"Atgrozīšanas ātrums\" + Slēpt izvēlni \"Stabilais skaļums\" + Slēpt izvēlni \"Skatīt VR\" + Slēpt automātisku atskaīšanas priekšskatījumu + Slēpj nākamā video priekšskatu pilnā ekrāna režīm pirms automātiskās atskaņāšanas sākuma + Kārklāja pogas + Izvēlieties, kuras pogas rādīt video pleaierā + Slēpt automātiskās atskaņāšanas pogu + Slēpt subtitru pogu + Slēpt Cast pogu + Slēpt salūkšanas pogu + Slēpt pilna ekrāna pogu + Slēpt spēlētāja vadības pogu fonu + Slēpj tumšo ēnu ap video plejera vadības elementiem + Pārvaldības pogu fona caurspīdums + Adjustējiet atspeles vadības pogu un laika joslas fona caurspīdumu (0 ir neredzams, 100 ir pilnībā aizsegts) + Slēpt pogas \"Iepriekšējais\" & \"Nākamais\" + Slēpt iestatījumu pogu + Slēpt beigu ekrāna kartes + Ambient režīms + Pārvalda Ambient režīma redzamību un stravas ierobežojumus + Apmalcić Ambient režīma ierobežojumus + Ļauj Ambient režīmam palikt aktīvam akumulatora taupīšanas režīmā + Atslēgt Ambient režīmu + Atslēgt videorežīmu pilnā ekrāna režīmā + Slēpt info kartes + Atslēgt skaitļu ritināšanas animācijas + Aptur skaitīšanas animāciju patīšanas un skatījumu skaitā + Slēpt video spēlētāja meklēšanas joslu + Slēpt video miniaturu meklēšanas joslu + Paslēpj skatīšanas progresa joslu, kas redzama video miniaturēs, kurus jau esat skatījis + Iespēgt lielu meklēšanas joslu pilnā ekrāna režīmā Paslēpt Shorts + Slēpt kanāla lapā + Slēpt Sākuma plūsmā & saistītos video + Nerādīt meklēšanas rezultātos + Nerādīt abonementu plūdumā + Nerādīt video aprakstā + Nerādīt skatīšanās vēsturē + Shorts atskaņotājs + Izvēlieties, kuras komponentes parādās Shorts plejerā + Aizliegt dubulto pieskārumu, lai patiktu + Slēpt AI pogu + Slēpt etiķeti \"Auto-dubbed\" + Slēpt kanāla joslu + Slēpt pogu \"Komentāri\" + Slēpt efektu pogu + Slēpt video saites etiķeti + Slēpt žestu norādījumus + Slēpt pogu \"Green screen\" + Slēpt vortu pogu + Slēpt info paneli + Slēpt pogu Pievienoties + Slēpt pogu \"Patīkt\" + Slēpt pogas \"Patīkt\" fontāna animāciju + Slēpt tiešo priekšskatu + Slēpt atrašanās vietas apzīmētāju + Slēpt navigācijas joslu + Slēpt pogu \"New posts\" + Slēpt pauzes pārklājuma pogas + Slēpt komentāra priekšskatījumu + Slēpt pogu \"Remikss\" + Slēpt pogu \'Saglabāt\' + Slēpt \"Saglabāt mūziku\" pogu + Slēpt meklēšanas ieteikumus + Slēpt pogu \"Kopīgot\" + Slēpt Veikala pogu + Slēpt skaņas pogu + Slēpt skaņas metadatu etiķeti + Slēpt \"Pierakstīties\" pogu + Slēpt pogu \"Buy Super Thanks\" + Slēpt atzīmētos produktus + Slēpt \"Gaidāmie\" pogu + Slēpt \"Izmantot šo skaņu\" pogu + Slēpt \"Izmantot šo šablonu\" pogu + Slēpt video nosaukumu + Slēpt beigu ekrāna ieteikтого video + "Slēpj ieteikto video pārklājumu, kas parādās pēc video beigām + +Šis iestatījums darbojas tikai tad, ja automātiska atskaņošana ir izslēgta. To var mainīt YouTube: +Iestatījumi → Atskaņošana (vai Automātiska atskaņošana) → Automātiski atskaņot nākamo video" + Slēpt saistītos video + Slēpt pārklājumu \"Vairāk video\" + Slēpj pārklājumu \"Vairāk video\", kas parādās, velkot uz augšu piln ekrāna režīm + Slēpt video laika zīmīku + Aizliegt sasaules uznirstošos paneļus + Aizliegt saraksta un tiešraidēs tēla paneļu automātisko atvēršanu + Aizliegt saraksta automātisku atskaņāšanu + Novērs sarakstu automātisku pāreju uz nākamo video + Atslēgt uzvedumu \"Turpināt skatīties\" + Iznest no pilnas ekrāna režīma video beigās Atspējots + Tikai portrets + Tikai ainava + Portrets un ainava + Piespieduši albums pilnā ekrāna režīmā + Pagriezt spelētāju uz albums režīmu, ieejot pilnā ekrāna režīmā. Nav pieejams mazos ekrāniem, kuros pilns ekrāns jau tiek atvērts albums režīmā + Piln ekrāna žesti + Splejera žesti piln ekrāna režīmā + Atslēgt portreta režīma žestu \'svaidīt uz augšu\' + Atslēgt portreta režīma žestu \'svaidīt uz leju\' + Aizliegt ainavisko swipe-down žestu + Atslēgt pincēšanas palīgierīču žestu zumam + Atjaunot pincēšanas zumu + Atvērt video pilnā ekrāna režīmā Atspējots + Portrets + Ainaviss + Pilna ekrāna video mērogs + Noklusējums + Stretch (aizpildīt ekrānu) + Zoom (apkopt, lai aizpildītu) + Rādīt video mēroga pogu + Krājiet, lai pārslēgtos starp noklusējuma, stretch un zoom režīmiem + Rādīt tikai pilnā ekrāna režīmā + Slēpt video mēroga pogu portreta režīma spēlētājā + Rādīt izslēgšanas pogu + Skaidrums spēlētāja pārklājumam + Koreģēt caurspīdības līmeni (0 ir caurspīdīgs, 100 ir necaurspīdīgs) + Meklēšanas josla Atspējots + Plata + Ekstra plašs + Ierīces izkārtojums + Defaults + Tālrunis + Planše + Automašīna + "Izmaiņās ir: + +Plansetes izkārtojums +• Kopienas ieraksti ir slēpti +• Iestatījums \"Atskaņošana plūsmēs\" nav pieejams +• Remix pogas un Skaņas pogas nav pieejamas Shorts +• Pārklājums \"Vairāk video\" var nebūt pieejams +• Tiek rādīta vecā video rīkjosla, kas nav salikta +• Izlēktās izvēlnes elementi var nestrādāt pareizi + +Autovietojuma izkārtojums +• Plūsma ir organizēta pēc tēmām un kanāliem +• Iestatījums \"Atskaņošana plūsmēs\" nav pieejams +• Remix pogas un Skaņas pogas nav pieejamas Shorts +• Pārklājums \"Vairāk video\" var nebūt pieejams +• Shorts tiek atvērtas parastajā skaņotājā +• Tiek rādīta vecā video rīkjosla, kas nav salikta" + I iespējot planšetes izkārtojumu atskaņotājā + "Atjaunojiet veco spēlētāja izkārtojumu + +Ierobežojums: ja \"Atjaunot veco video rīkku joslu\" ir izslēgts, pārklājums \"Vairāk video\" var nebūt pieejams" + 21.11.48 - Atjaunot veco titru darbību + 20.39.41 - Atjaunot veco saglabājumu sarakstā + 20.28.41 - Atslēgt tremojošas ikonas + 20.13.41 - Atjaunot kanāla nosaukumu zem video plejera + Atvērt sistēmas kopīšanas logu + Satura kopīgošanai atvērt sistēmas kopīgošanas logu vietā lietotnes logam + Pārrakstīt YouTube Music pogas + Atver jūsu izvēlēto mūzikas lietotni standarta YouTube Music lietotnes vietā + Mūzikas lietotnes paketes nosaukums + Jūsu instalētās mūzikas lietotnes paketes nosaukums + Sākuma lapa + Noklusējums + Visi abonementi + Apskatīt kanālus + Kursi / Mācīšanās + Izpētīt + Mode & Skaistums + Spēles Skatīšanās vēsture Bibliotēka + Patīkoties video + Tiešraides Filmas Mūzika + Ziņas Paziņojumi Atskaņošanas saraksti Meklēt + Iepirkšanās + Shorts Sports Abonementi Pašlaik populārs + Virtuālā realitāte + Skatīt vēlāk + Tavi klipi + Aizliegt turpināšanu pēc startēšanas Atskaņot īsos video ar Īso video atskaņotājs + Parasts atskaņotājs + Parasts atskaņotājs pilnā ekrānā Automātiski atskaņot īsos video + Automātiski pāriet uz nākamo Short vietā atkārtot pašreizējo + Autopils fonā + Atskaņošanas laikā fonā automātiski pāriet uz nākamo Short + Izlaist tiešraidē esošos Shorts + Izlaida tiešraides sietums, kas parādās Shorts plūdumā + Atskaņošana plūmēs + Vienmēr ieslēgts + Tikai Wi-Fi Izslēgts + Atjaunot oriģinālos nosaukumus + Rāda oriģinālos video nosaukumus, video aprakstus un kanālu aprakstus automātiski tulkotājos vietā + Ielādē nosaukumu… + Aizliegt automātisku plūsmes atjaunināšanu + Novērš plūsmu automātisku atjaunināšanu, kad tās kļūst novecotušas + Plūmes kartes izkārtojums + Klasisks + Kartes + Kartes ar pogām Patikt, Komentēt un Saglabāt Komentāri + Atslēgt miniplesieri neveca + Atslēg \'Turpināt skatīšanu\' miniplayeri—YouTube Premium funkciju, kas parādās lietotnes startā + Miniatūra programma + Pielago minimizētā video spēlētāja izskatu un darbību + Miniatūras programmas tips + Slēpt nosērumu un kanālu Atspējots + Noklusējums + Minimalistisks + Minimalistisks 2 + Planšetdators + Moderns 1 + Moderns 2 + Moderns 3 + Moderns 4 + Aizliegt noapaļotus stūrus + Aizliegt velkt un palaist + Novērš minimizētā spēlētāja velšanu ekrāna dažādos stūros + Aizliegt horizontālo velšanas žestu + Novērs miniplejerīša aizvēršanu, horizontally sviljot to ārpus ekrāna + Pauzēt atskaņošanu ar horizontālu velcienu + At 재생ums tiks apturēts, ja minimizētais plejeris tiek velkts ārpus ekrāna + Atcerēties velciena stāvokļa pozīciju + Velkot minimizēto spēlētāju ārpus ekrāna, tiks saglabāts tā lietotāja saskarnes stāvoklis + Slēpt pārklājuma pogas + Noklusējuma izmērs + Iestatiet sākotnējo ekrāna izmēru pikseļos + Pikseļu lielumam jābūt starp %1$s un %2$s + Klājuma caurspīdums + Adjustējiet caurspīdības līmeni (0 ir caurspīdīgs, 100 ir necaurspīdīgs) + Atpakaļpoga izved no plūmes + Nospiežot atpakaļ, vienmēr tiek izkļūts no lietotnes, nevis scrolls līdz plūmes augšgalvei + Piespiedu animācijas + Noklusējums + Vienmēr ieslēgts + Vienmēr izslēgts + Slēpt statusa joslu + Slēpj sistēmas statusa joslu. Norullājiet lejup no augšējās malas, lai to uz brīdi rādītu + Labot statusa joslas pārklājumu augšējām joslām + Uztur statusa joslas izkārtojumu konsekventu, lai tā nepārklājtu augšējās joslas un nebūtu tukšas vietas virs atskaidītāja. Izslēdziet šo iespēju, ja lietotne crashes vai augšējās joslas izskatās nepareizi + Mirdzēšana + Šī ierīce neatbalsta mirdzošo ekrānu + Gaišās tēmas fons + Balts + Material You (balts) + Gaiši rozā + Gaiši zils + Gaiši zaļa + Gaiši dzeltena + Gaiši oranža + Gaiši sarkanā + Pielagots gaišs fons + Iestatīt gaišās tēmas fona krāsu + Mainīt priekšplana krāsu + Izmantot tēmas krāsu tekstam un ikonām + Ienablekot gradienta ielādes ekrānu + Rāda gradienta fonu ielādes ekrānā tā vietā, kur ir vienmērīga krāsa + Ienablekot pielāgotu meklēšanas joslas krāsu + Atvēršaslīdēja krāsa + Iestatiet video progresa joslas galveno krāsu + Atvēršaslīdēja akcenta krāsa + Iestatiet video progresa joslas sekundāro krāsu + Kļūda meklēšanas joslas krāsas vērtībā + Oriģinālais analogs + Oriģināls kūkas tēls + Oriģināls metāls + Galvene logotips + Pēc noklusējuma + Parastais + Pielagots + Apmalcināt attēlu reģionālos ierobežojumus + Pārvirza miniaturu pieprasījumus uz alternatīvu serveri, lai novērstu trūkstošos attēlus ierobežotās valstīs + DeArrow nosaukumi + Sākuma cilne + Abonementu cilne + \"Jūs\" cilne + Skaņotāja atskaņošanas saraksti & rekomendācijas + Meklēšanas rezultāti + Rādīt DeArrow ikonu + Rāda DeArrow ikonu kopīgi izveidotiem video nosaukumiem + Neformālais režīms + Rāda oriģinālo nosaukumu DeArrow nosaukuma vietā, ja DeArrow lietotāji ir balsojuši, ka viņiem patīk oriģinālais nosaukums + Minimālais neformālo balsu skaits oriģinālā nosaukuma rādīšanai + Neformālo balsu skaits vienā kategorijā, piemēram, smieklīgā vai radošajā, kas nepieciešams, lai rādītu oriģinālo nosaukumu + DeArrow miniaturīsas + Oriģinālie miniaturējumi + DeArrow & oriģinālie miniaturu attēli + DeArrow & statiskie kadri + Statiskie kadri + "DeArrow aizstāj klikšķu piesaidītāju nosaukumus un miniaturēm ar kopienas izveidētiem, kas ir precīzāki un mazāk sensacionāli. Nosaukumus raksta un par tiem balso kopiena, bet miniaturēm ir kadri, kas uzņemti no video + +Video tiek pieprasīti, izmantojot īsu hashēta video ID prefiksu, tādēļ serveris nevar precīzi noteikt, kuru video jūs skatāties. Ja video nav DeArrow miniaturēm, tiek rādīta oriģinālā vai fiksēta attēla kopija" + Rādīt savienojuma kļūdas brīdinājumus + Rāda paziņojumu, ja DeArrow serveris nav pieejams + API gala punkts + DeArrow miniaturu kešolšanas gala punkta URL adrese + Statiskie video kadri + Kadrus fiksē no katra video sākuma / vidus / beigām. Šie attēli ir iebūvēti YouTube un ārējā API netiek izmantota + Izņemt video kadru no + Sākuma + Vidus + Beigu + DeArrow pagaidām nav pieejams (statuss: %s) + DeArrow pagaidām nav pieejams + Atjaunot veco atskaitsprogrammas pogu stilu + Atgriež video spēlētāja vadības pogas vecajā, tēvākā vizuālajā stilā + Imitēt ierīces izmērus + Iespējot šo, var atbloķēt augstāku video kvalitāti + Šīs funkcijas iespējošana var izraisīt video atskaņošanas kropļojumus, sliktāku akumulatora darbības laiku un nezināmus blakneffektus + Haptiskā atgriezeniskā saite + Izvēlieties, kuras mijiedarbības izraisa ierīces vibrāciju + Atslēgt nodalumu haptiku + Atslēgt precīzas meklēšanas haptiku + Aizliegt meklēšanas atsauksmes haptisko atgriezenisko saiti + Aizliegt pieskaršanās un turēšanas haptisko atgriezenisko saiti + Atslēgt zumēšanas haptiku + Ja nesen esat mainījuši sava konta pieteikšanās datus, tad atinstalējiet un instalējiet MicroG no jauna + Apgrozīt saites novirzīšanas + Tieši atver oriģinālo saiti, apejot izsekošanas saiti (youtube.com/redirect) + Atvērt saites pārlūkprogrammā + Atver saites ārējā pārlūkprogrammā nevis lietotnes pārlūkprogrammā + Atgriešanas bufera izmērs + Pēc noklusējuma + Zems + Vidējs + Maksimāls + Video aizkavējums + Aizkavē video, lai to синхronizētu ar aizkavēto audio, piemēram, Bluetooth austiņām (0-5000 ms) + Aizkavēt tikai ar Bluetooth audio + Aizkavē video tikai tad, kad ir pieslēgts Bluetooth audio, lai tālruņa skaļrunis saglabātu sinhronizāciju + Rādīt video aizkavējuma pogu + Turiet, lai atiestatītu aizkavējumu uz 0 ms + Rādīt video aizkavējuma izvēlni + Aizkavējums milisekundēs + Automātiski + Atcerēties video kvalitāti + Pielieto jūsu manuālo kvalitātes izvēli visiem turpmākajiem video + Paziņojumi par kvalitātes maiņu + Rāda paziņojumu, kad tiek saglabāta jaunā noklusējuma kvalitāte + Noklusējuma video kvalitāte Wi-Fi tīklā + Video noklusējuma kvalitāte mobilajā tīklā + Atcerēties Shorts kvalitāti + Pielieto jūsu manuālo kvalitātes izvēli visiem turpmākajiem Shorts Īso video noklusējuma kvalitāte, kad izmanto bezvadu, Wi-Fi, tīklu + Shorts noklusējuma kvalitāte mobilajā tīklā + Mainīta noklusēta mobilā kvalitāte uz: %s + Mainīta noklusēta Wi-Fi kvalitāte uz: %s + Mainīta Shorts mobilā kvalitāte uz: %s + Mainīts Shorts Wi-Fi kvalitāte uz: %s + Rādīt atskaidzes ātruma pogu + Kustiniet un aizturiet, lai atiestatītu ātrumu uz noklutinējumu + Rādīt kvalitātes pogu + Kārtāpiesaunāšana uz noklūstīgās kvalitātes: pieskarieties un aizturiet + Rādīt izšķirtspēju Kvalitātes pogā + Rādīt izšķirtspēju, piemēram, 1080, vietā par apzīmēkli, piemēram, FHD + Pielagota atskaītes ātruma izvēlne + Atjaunot veco atskaītes ātruma izvēlni + Pielagoti atskaītes ātrumi + Ievadiet sev vēlamo atskaitīšanas ātruma opciju + Pielagoti atskaitīšanas ātrumiem jābūt mazākiem par %s + Kļūda pielagotos atskaītes ātrumos + Automātiski + Pieskarieties un aizturiet atskaitīšanas ātrumu + Iestatiet ātruma reizintāju, nospiežot un aizturot ekrānu; iestatiet 0, lai deaktivizētu + Atcerēties atskaitīšanas ātrumu + Pielieto jūsu manuālo ātruma izvēli visiem nākamajiem video + Paziņojumi par ātruma maiņu + Rāda paziņojumu, kad tiek saglabāta jaunā noklusējuma atskaņošanas ātrums + Noklusējuma atskaītes ātrums + Kūdainā atskaņāšanas ātrums Shorts + Atcerēties atskaidzes ātrumu Shorts + Pielieto jūsu manuāli izvēlēto Shorts ātrumu visiem nākamajiem Shorts. Ja šī opcija ir izslēgta, pēc Shorts atskaidītāja aizvēršanas ātrums atgriezīsies pie noklājuma + Rādīt atskaitīšanas ātrumu Shorts izvēlnē + Pievieno atskaitīšanas ātruma vienumu Shorts pleaiera izvēlnei + Atskaitīšanas ātrums · %s + Atskaitīšanas ātrums pēc noklusējuma mainīts uz: %s + Mūzikai izmantot normālu ātrumu + Mūzikas video vienmēr atskaņot ar 1.0x ātrumu + Kanālu ātruma baltais saraksts + Video no baltā saraksta kanāliem vienmēr tiek atskaņoti ar 1.0x ātrumu + Pievienot kanālu ātruma baltajam sarakstam + Noņemt kanālu no ātruma baltā saraksta + Kārtot audio auguma kontroli + At reproduction audio augums pēc noklusējuma + Audio augums pēc noklusējuma mainīts uz: %s + Audio laika stretchyngs + Ļauj neatkarīgi iestatīt ātrumu un augstumu, kas var izraisīt audio artefaktus + Atcerēties tiešraides atskaidzes pozīciju + Aktuālām tiešraidēm atceras un atsāk jūsu atskaidzes pozīciju, nevis pārvietojas uz pašreizējo tiešo pozīciju + Šī funkcija nedarbojas ar 24/7 tiešraidēm un citām straumēm, kas ir \"vienmēr ieslēgtas\" + Aizliegt HDR video + Piespiedu HDR video + Piespiež lietotni atpazīt šo ierīci kā HDR saderīgu + "Izmantojiet šo, ja HDR dekodēšana darbojas, bet YouTube nepiedāvā HDR, piemēram, dažām HyperOS ierīcēm, kas slēpj HLG. + +ierobežojumi: +• Krāsas var izskatīties nepareizi, ja displejs faktiski nevar rādīt HDR +• Atskaņošana var neveiksmīgi beigties, ja dekoderis nevar apstrādāt HDR +• HDR joprojām ir atkarīgs no imitētajām video plūsmām klientā" + Video kodeks + Noklusējums + Lai piespiedu AVC, mainiet \'Viltot video plūdes\' uz \'Android Studio\', \'Android VR\' vai \'visionOS\' + "VP9 +• Var uzlabot akumulatora darbības laiku un novērst atskaites raukšumu +• Patērē vairāk interneta datu nekā AV1 + +AVC (H.264) +• Nepieciešams 'Android Studio', 'Android VR' vai 'visionOS' viltot video plūdes klients +• Var vēl vairāk uzlabot akumulatora darbības laiku +• Var atjaunot trūkstošas video izšķirtspējas vecākās ierīcēs +• Maksimālā izšķirtspēja ir 1080p +• HDR video var neizmantot AVC +• Dažas ierīces nevar piespiedu AVC" + Atvērt paplašinātās kvalitātes izvēlni + Izlaika kvalitātes izvēles izvēlni un tieši atvērs paplašinātās kvalitātes izvēlni + Slēpt Premium kvalitātes opcijas + Prioritizēt video kvalitāti + Prioritizē augstāko pieejamo video kvalitāti, novērtējot to augstāk par kodeka efektivitāti + Ja šis iestatījums ir izslēgts, vecākiem video var nebūt pieejamas video kvalitātes iespējas + Atļaut \'Android VR\' / visionOS AV1 + Atļauj programmatūras AV1 dekodēšanu, izmantojot \'Android VR\' / visionOS klientu + "Šī iestatījuma iespējošana var izmantot programmatūras AV1 dekodēšanu. + +Video atskaņošana ar AV1 var kļūt neregulāra vai var izkrist kadri" + Imitēšanas blakusefekti + • Atskaite var neveiksmīgi beigties pēc 1 minūtes + • Trūkst izvēlnes \'Audio izvele\' + • Nav AV1 video kodeka + • 360° VR imersīvais režīms nav pieejams + • Stabilais skaļums nav pieejams + • Atskaņošana var beigties 1 sekundi agrāk + • Ja deobfuskācija neizdodas, atskaņošana var neveikties + • Piespiedtais oriģinālais audio nav pieejams + • Nepieciešams pieslēgties Android VR + • Kvalitāte var būt ierobežota līdz 720p + Skaņotāja JavaScript variants + Rādīt imitāciju sadaļā \'Statistika nerdiem\' + Atjaunot veco video rīkjoslani + "Labojums: video darbības pogas kļūst slēpjamas + +ierobežojums: Atgriežas pie vecā stila pārklājuma \"Vairāk video\"" + Skaņavadītāja ikonas stils Automātiski + Privārts + Tons + Treknraksts + Pielagots + Shorts ikonas stils + Sērijas izsekotojs + Seko plašinājumu sarakstiem kā seriāliem un atceries savu vietu + Ienable seriāļu izsekošanu + Pievieno sērijas lapu Vēsturei un iespēju sekot sērijai at 재생ningsplāniem + Rādīt sērijas navigācijas joslā + Rādīt sērijas izsekotāja pogu + Pievieno atskaidītāja pogu, lai sekotu pašreizējā video sērijai vai atvērtu to + Ierakstīt sekoto sērijas progresu + Ierakstīt atskaidīšanu šajā tālrunī saglabātajām sērijām un grāmatzīmēm Atcelt + Saglabāt + Saglabāt grāmatzīmīkli + Sērijas nosaukums + Sērijas iespējas + Bibliotēkas iespējas + Atjaunināt epizodes + Pārvērst + Atvērt atskaņošanas sarakstu YouTube + Noņemt no bibliotēkas + Atskaņošanas saraksta saite vai ID + Notīrīt visu vietējo progresu + Notīrīt vēsturi + Dzēst visu vietējo progresu? + Tas dzēs pozīcijas, skatīto satura pārrakstījumus un atskaņošanas toothpaste izvēles katram video. Saglabātie seriāli un to katalogi paliek. Jauna atskaņošana tiks reģistrēta, ja reģistrācija ir iespējota + Atzīmēt kā noskatītu + Atzīmēt kā nenoskatītu + Izmantot automātisko statusu + Sākt no jauna no 0:00 + Atskaidīt vēlreiz no 0:00 + Statuss saglabāts + Mēģināt saglabāt vēlreiz + Iepjšējie 100 + Nākamie 100 + Atvērt saglabāto video + Lokālais progresss dzēsts + Atvēriet sadaļu \"Vēsture\" no sadaļas \"Jūs\", lai aplūkotu savus seriālus + Atskatīšanas vēsture + Seriāli + Ielādē saglabātos seriālus… + Sekojiet atskaņošanas sarakstam, lai sekotu tā epizodēm + Atzīme sarakstam + Atjaunina epizodes… + Visi seriāli + Noskatīts + Epizodes tiek ielādētas… + Vairāk iespēju + "Reģistrēt progresu tikai jūsu saglabātajiem seriāliem un grāmatzīmēs pievienotajiem video. + +Šī bibliotēka tiek glabāta šajā ierīcē un kopīgotā starp YouTube kontiem. YouTube pauzes un vēstures dzēšanas vadības elementi to nemaina. Lai to dzēsitu, izmantojiet Seriāli → Vairāk → Dzēst vietējo progresu. + +Automātiskā reģistrācija pārtraucas incognito režīmā, ja konta identitāte nav pieejama vai, ja pārslēdzaties uz citu kontu. Lai izmantotu to ar šo kontu, iespējojiet izsekošanu no jauna" + Izsekošanu nevar iespējot inkognito režīm vai pirms YouTube ir ielādējis pašreizējo konta stāvokli. Atgriezieties uz sākuma lapu un mēģiniet vēlreiz. Pieslēgšanās nav nepieciešama + Iespēgt vietējo izsekošanu + Kuces ierakstīšana ir izslēgta + Turpināt pie %1$s + Epizode %1$s · %2$s + Epizodes %1$s-%2$s no %3$s + Atjauniniet, lai mēģinātu ielādēt epizodes. Privātajām sarakstiem ir nepieciešama piekļuve kontam; to grāmatzīmes joprojām atveras YouTube + YouTube nevarēja atvērt šo epizodi + YouTube nevarēja atvērt šo at 재생umu + Izvēlieties epizodi + Aktuāli + Nav atskaidāmas epizodes + Saglabāta video pozīcija · %1$s + Sekot sērijai + Skatīt sēriju + Turpināt + Sākt še + Sākuma punkts saglabāts + Noskatīts līdz še + Epizodes atzīmētas kā noskatītas Atsaukt + Iespējot + Reģistrēt progresu šajā ierīcē + "Jūsu sērijas un progress paliek šajā ierīcē un tiek kopīgots starp YouTube kontiem. YouTube vēstures kontroles tos nemaina. Dzēsiet progresu sērijas izvēlnē. + +Automātiskā ierakstīšana ir neobligāta. Tā tiek apturēta inkognito režīmā vai konta maiņas laikā" + Epizodes neizdevās ielādēt. At 재생ņume lūdzam saglabāt at 재생ņume sarakstu kā grāmatzīmīti + Pārāk daudz nesaglabātu izmaiņu. Vispirms mēģiniet saglabāt vēlreiz + Saglabāšana ir aizņemta. Mēģiniet vēlreiz + Epizodes ielāde ir aizņemta. Mēģiniet vēlreiz + Atmiņa nav pieejama. Jūsu saglabātie dati tika saglabāti + Izmaiņas neizdevās saglabāt. Mēģiniet saglabāt vēlreiz pirms YouTube aizvēršanas + Šī sērija jau ir saglabāta vai tās at 재생ņume saraksts ir mainīts. Atvēriet to no jauna + Šis sērijas saturs ir mainīts vai dzēsts. Atveriet bibliotēku vēlreiz + At 재생as saraksts ir mainīts. Izvēlieties epizodi vēlreiz + Šī epizode nav pieejama. Izvēlieties citu + Ievadiet sērijas nosaukumu + Ievadiet YouTube atskauslēšanas saraksta saiti vai ID + Lūdzu, uzgaidiet pirms atkārtotas atjaunināšanas + Epizodes neizdevās ielādēt. Mēģiniet vēlreiz vēlāk + Izmaiņumus neizdevās veikties. Mēģiniet vēlreiz + Atpūtnings neizdevās apstiprināt. Pārbaudiet atskaitītāju un mēģiniet vēlreiz + Sekot sērijām + Šī video nav šajā at 재생as sarakstā + Meklē sarakstus… + Ielīmēt saraksta saiti + Neizdevās atrast atskaņošanas sarakstu + Nav pieejamu epizodēm + Slēpt noskatītās + Atzīmēt iepriekšējās epizodes kā noskatītas + Sākums ar: %1$s + Epizodes secība + Atskaņošanas saraksta secība + Atvienot atskaņošanas saraksta secību + %1$s skatījumi + %1$s skatījums + Atzīmēt episodes kānoskatītas + Noskatīts vismaz %1$s%% ar ne vairāk kā 90 sekundēm līdz beigām vai atlikušas %2$s sekundes + Automātiski atzīmēt epizodi kā noskatītu pēc jebkuras no šīm robežām. Procentu likums prasa arī, lai būtu atlicināts ne vairāk kā 90 sekundes, tādēļ garu epizodu beigas netiek izlaistas. Atlikušā laika likums attiecas uz video, kas ir garāki par vienu minūti; iestatiet to uz 0, lai to deaktivētu. Izmaiņas stājas spēkā nākamajiem progresa atjauninājumiem. Manuālie izvēles varianti \"noskatīts/nenoskatīts\" tiek saglabāti + Noskatīto procents (1-100) + Atlikušās sekundes (0-300; 0 deaktivē) + Ievadiet vērtību no %1$s līdz %2$s + Izmantot YouTube progresu + Turpināt sekotos seriālus pēc skatīšanas citā ierīcē + Nolasīt noskatīto statusu un turpināšanas pozīcijas jūsu sekotajiem seriāliem no pašreizējā YouTube konta. Šajā tālrunī tiek saglabātas tikai atbilstošas epizodes. Pārbaudes tiek veiktas, kamēr ir atvērta seriāļu bibliotēka un pirms turpināšanas. YouTube vēsture jūsu citā ierīcē jābūt iespējotai + Pārbauda YouTube progresu… + YouTube progressi nav pieejams. Tiek izmantots saglabātais progresss + Daļa no YouTube progresa nav pieejama. Saglabātās pozīcijas tiek saglabātas + Rezervkopija un atjaudināšana + Saglabājiet savu seriālu bibliotēku failā vai atjaudiniet rezervkopiju + Saglabā sekotajiem seriāliem, epizodes pozīcijām un izvēlēm par noskatīto. Ierakstu un YouTube progresa atļaujas nav iekļautas + Saglabāt rezervkopiju + Atjaudināt rezervkopiju + Pievienot %1$s seriālu. %2$s seriāli, kas jau ir jūsu bibliotēkā, tiek saglabāti. Esošais video progresss tiek saglabāts + Seriālu rezervkopija saglabāta + Atjaunoti %1$s seriāli + Šis fails nav atbalstīta seriālu rezervējuma kopija vai ir pārāk liels. Jūsu bibliotēka netika mainīta + Neizdevās saglabāt rezervējuma kopiju. Mēģiniet citu atrašanās vietu + Seriāls dzēsts + Šī epizode tagad parādās vairākas reizes. Izvēlieties, kuru turpināt + Jūsu saglabātais epizodes ieraksts vairs neatrodas šajā sarakstā. Jūsu saglabātā pozīcija ir saglabāta + Turpināt ar epizodi %1$s · %2$s + Jūs varat izvēlēties jebkuru epizodi zemāk. Iepriekšējās epizodes netiek mainītas + Šis atskaņošanas saraksts ir mainījies. Atvēriet seriālu, lai izvēlētos, kur turpināt + Šajā virzienā vairs nav epizodes + Turpināt %1$s + Meklēt seriālus + Notīrīt meklēšanu + Sērijas netika atrastas + Epizode bez nosaukuma + Nepieejama epizode + Niconico komentāri + Rādīt tiešo tērzēšanu un tērzēšanas atkārtošanos kā komentārus, kas skrolē pāri video + Rāda pārklājuma pogu video plejerī. Pieskarieties, lai rādītu vai slēptu komentārus, vai pieturiet, lai piekļūtu ātriem iestatījumiem + Komentāru šrifta lielums + Komentāru teksta lielums (12-40 sp) + Komentāru caurspīdums + Komentāru teksta un fona caurspīdums + Kustības ilgums + Cik sekundes komentāriem vajag, lai šķērsotu ekrānu. Mazākas vērtības nozīmē ātrāku kustību + Komentāra maksimālais garums + Slēpj komentārus, kamēr rakstzīmju skaits pārsniedz šo vērtību. Katrs emoji tiek skaitīts kā viens rakstzīme + Bez ierobežojuma diff --git a/patches/src/main/resources/addresources/values-mai-rIN/music/strings.xml b/patches/src/main/resources/addresources/values-mai-rIN/music/strings.xml index d17650dedc..ccfea4ed71 100644 --- a/patches/src/main/resources/addresources/values-mai-rIN/music/strings.xml +++ b/patches/src/main/resources/addresources/values-mai-rIN/music/strings.xml @@ -20,9 +20,6 @@ Second \"item\" text" - हेडर लोगो - डिफ़ॉल्ट - कस्टम @@ -30,52 +27,10 @@ Second \"item\" text" - बारे में - विज्ञापन - \'म्यूजिक प्रीमियम प्राप्त करें\' लेबल छिपाएं - वीडियो विज्ञापन छिपाएं - क्रॉसफ़ेड - ट्रैक सभ के आपसी में सहजता सँ मिलाऊ - क्रॉसफ़ेड सक्षम करें - फेडिंग वक्र - समान शक्ति - सपाट पकड़ - सौम्य सहजता - चिकनी एस-वक्र - बाहर जाइत - अंदर आइत - क्रॉसफ़ेड अवधि - 250 मिलीसेकंड - 500 मिलीसेकंड - 750 मिलीसेकंड - 1 सेकंड - 2 सेकंड - 3 सेकंड - 4 सेकंड - 5 सेकंड - 6 सेकंड - 7 सेकंड - 8 सेकंड - 9 सेकंड - 10 सेकंड - 11 सेकेन्ड - 12 सेकेन्ड - मैन्युअल स्किप पर क्रॉसफ़ेड - जखन अहाँ मैन्युअल रूप सँ अगिला ट्रैक पर जाइत छी, तँ क्रॉसफेड प्रभाव लागू होइत छै - मैन्युअल स्किप क्रॉसफ़ेड नहीं करेंगे - जखन कोनो ट्रैक प्राकृतिक रूप सँ समाप्त होइत छै, तँ क्रॉसफेड प्रभाव लागू होइत छै - ऑटो-एडवांस क्रॉसफ़ेड नहीं करेगा - क्रॉसफेड के रोकय या पुनरारंभ करय लेल शफल बटन के देर सँ दबाऊ - क्रॉसफ़ेड इस सत्र के लिए रोका गया - क्रॉसफ़ेड फिर से शुरू हुआ - क्रॉसफ़ेड सक्षम होने पर वीडियो मोड उपलब्ध नहीं है - ई कोना काम करइत छै - क्रेडिट - VazerOG द्वारा क्रॉसफेड फीचर @@ -92,98 +47,26 @@ Second \"item\" text" - आरंभ पृष्ठ - डिफ़ॉल्ट - चार्ट - बाद के लिए एपिसोड - अन्वेषण करें - इतिहास - लाइब्रेरी - पसंदीदा संगीत - प्लेलिस्ट - खोज - सदस्यताएं - मिनीप्लेयर रंग बदलें - मिनीप्लेयर क रंग के फुलस्क्रीन प्लेयर सँ मिलाइत छै - मिनीप्लेयर में अगला बटन दिखाएं - मिनीप्लेयर में पिछला बटन दिखाएं - बलपूर्वक मिनीप्लेयर सक्षम करें - नय ट्रैक शुरू होइत समय प्लेयर के स्वचालित रूप सँ फुलस्क्रीन में विस्तार करय सँ रोकित छै - मिनीप्लेयर के हटाब लेल स्वाइप सक्षम करू - मिनीप्लेयर पर नीचे स्वाइप क\'र के ओकरा हटाब क अनुमति दैत छै - नेविगेशन बार - चुनू कि निचला नेविगेशन बार मे कोना बटन देखायब - होम बटन छुपाऊ - सैंपल्स बटन छुपाऊ - एक्सप्लोर बटन छुपाऊ - लाइब्रेरी बटन छुपाऊ - अपग्रेड बटन छुपाऊ - नेविगेशन बार छिपाएं - नेविगेशन बटन लेबल छिपाएं - लॉग इन नहि छै। कॉन्फ़िगर करय लेल टैप करू - लॉग आउट करू - लॉग आउट कएल गेल - Scrobbling सक्षम करू - न्यूनतम गाना अवधि - एतय सँ छोट गाना scrobble नहि होयत - Scrobble विलंब प्रतिशत - गानाक एतय प्रतिशत सुनय के बाद scrobble ट्रिगर करू - Scrobble विलंब सेकेन्ड - एतय सेकेन्ड धरि सुनय के बाद scrobble ट्रिगर करू - ListenBrainz अकाउंट - लॉग इन छै। अकाउंट मैनेजमेंट लेल टैप करू - Scrobbling सक्षम करय लेल अपन ListenBrainz यूजर टोकन दर्ज करू - टोकन - सत्यापित करू - कृपया टोकन दर्ज करू - मान्य टोकन! यूजर: %s - अमान्य टोकन: %s - अज्ञात - सत्यापन विफल रहल: %s - टोकन हटा दएल गेल - टोकन सेव कएल गेल - चेतावनी: सेव कएल गेल टोकन अमान्य छै! - पसंदक ट्रैक सभ के लव करू - जखन अहाँ YouTube Music पर ट्रैक पसंद करैत छी, तँ स्वचालित रूप सँ Last.fm पर लव करू (आ अनलाइक या डिसलाइक कयला पर लव हटाऊ) - Last.fm अकाउंट - %s के रूप में लॉग इन छै। अकाउंट मैनेजमेंट लेल टैप करू - लॉग इन करय लेल अपन Last.fm क्रेडेंशियल दर्ज करू - उपयोगकर्ता नाम - पासवर्ड - लॉग इन करू - कृपया उपयोगकर्ता नाम आ पासवर्ड दर्ज करू - सफलतापूर्वक लॉग इन कएल गेल! - Last.fm सँ सेशन प्राप्त करय मे विफल रहल - लॉगइन विफल रहल: %s - Last.fm क्रेडेंशियल सेव कएल गेल! - मेटाडेटा सफाई - मेटाडेटा साफ करू - Scrobbling सँ पहिलै शीर्षक, कलाकार, आ एल्बम सँ सामान्य अनावश्यक शब्द (जेना \"Official Video\", \"Remaster\", \"Live\") के स्वचालित रूप सँ साफ करू - कस्टम regex सफाई - गानाक शीर्षक, कलाकार, या एल्बम सँ अतिरिक्त टेक्स्ट हटाब लेल एक कस्टम नियमित अभिव्यक्ति दर्ज करू। उदाहरण: (?i)\\s*\\((clean|explicit)\\) - "Scrobbling स्वचालित रूप सँ अहाँक YouTube Music उपयोग के अहाँक Last.fm या ListenBrainz प्रोफाइल सँ सिंक करैत छै - -ई सुनिश्चित करय लेल जे बैकग्राउंड प्लेबैक क दौरान scrobbling सिंक होय, ई सुनिश्चित करू जे YouTube Music क ऐप बैटरी उपयोग \"Unrestricted\" पर सेट छै" diff --git a/patches/src/main/resources/addresources/values-mai-rIN/reddit/strings.xml b/patches/src/main/resources/addresources/values-mai-rIN/reddit/strings.xml index cf1057a67a..cf7776fdaf 100644 --- a/patches/src/main/resources/addresources/values-mai-rIN/reddit/strings.xml +++ b/patches/src/main/resources/addresources/values-mai-rIN/reddit/strings.xml @@ -15,31 +15,4 @@ Second \"item\" text" --> - - विज्ञापन - फीड विज्ञापन छुपाउ - टिप्पणी विज्ञापन छुपाउ - उत्तर बटन छुपाउ - चैट बटन छुपाउ - बनाउ बटन छुपाउ - डिस्कभर या समुदाय बटन छुपाउ - गेम्स बटन छुपाउ - हालमे भेटल गेल शेल्फ छुपाउ - रेडिटमे गेम्स शेल्फ छुपाउ - रेडिट प्रो शेल्फ छुपाउ - बारेमे शेल्फ छुपाउ - संसाधन शेल्फ छुपाउ - लेआउट - स्क्रीनशॉट पॉपअप अक्षम करू - स्क्रीनशॉट लेलावे समयमे देखल जाएवाला पॉपअप अक्षम करेला - पूछब बटन छुपाउ - सर्च बारमे पूछब बटन छुपाउवेला - सूचना सुझाव संवाद अक्षम करू - एनएसएफडब्ल्यू चेतावनी संवाद अक्षम करू - विविध - बारेमे - पैच संस्करण एफ्क आधिकारिक लिंक - साझा लिंकसँ स्वच्छता कराउ - सीधा लिंक खोलू - बाहर लिंक खोलू - + diff --git a/patches/src/main/resources/addresources/values-mai-rIN/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-mai-rIN/shared-youtube/strings.xml index bd6bb03297..77635ba31c 100644 --- a/patches/src/main/resources/addresources/values-mai-rIN/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-mai-rIN/shared-youtube/strings.xml @@ -18,87 +18,33 @@ Second \"item\" text" --> - रंग + - सेटअप पूरा करबाक लेल पुनः आरंभ करू - एप्पक नाम - कस्टम - एप्प आइकन - ओरिजनल - मॉरफे डार्क - मॉरफे लाइट - कस्टम - नोटिफिकेशन आइकन - मॉरफे नोटिस - फेर सँ नाहीं देखाउ - वीडियो स्ट्रीम स्पूफ करू - क्लाइंट स्ट्रीम प्राप्त कर सकल नाहीं - प्रयास करू: स्पूफ वीडियो स्ट्रीम > Android VR साइन इन - डिफ़ॉल्ट क्लाइंट - Android VR साइन इन - Android VR में साइन इन नहि कएल गेल अछि। साइन इन करबाक लेल ई पर टैप करू। - Android VR में वर्तमान में साइन इन कएल गेल अछि। - एहि फीचरक प्रयोग करबाक लेल, ‘डिफ़ॉल्ट क्लाइंट’के Android VR में बदलू। - Android VR में साइन इन करू - "कुछ यूजरस के प्लेबैक समस्यास के ठीक करबाक लेल Android VR में साइन इन करबा सकैत अछि - -साइन इन करबाक लेल, नीचे जारी रखू पर क्लिक करू।" - साइन आउट करू - निर्देश - "एक्टिवेशन कोड: %s - -1. ‘डिवाइस कनेक्ट करबा’ वेबसाइट खोलबाक लेल नीचे ‘वेबसाइट खोलू’ पर टैप करू। -2. ऊपर देखायल गेल एक्टिवेशन कोडके क्लिपबोर्ड सँ पेस्ट करू (अथवा टाइप करू)। -3. संकेतित भए Google में साइन इन करू। यदि तहुँ मे कईटा अकाउंटमे सँ एक चुनुने पडतअ ताहि YouTube अकाउंट चुनु। -4. ‘YouTube VR एक्सेसक अनुमति देबू’ पर टैप करू। -5. ‘डिवाइस कनेक्ट कएल गेल’ संदेश पहुँचेला बाद एह एप्प में वापस आउ।" - एक्टिवेशन कोड निर्गमन में विफलता - प्रमाणीकरण टोकन निर्गमन में विफलता - सफलतापूर्वक साइन इन कएल गेल - "प्रमाणीकरण बचायल गेल अछि - -• Morphe सेटिंग Android VR प्रमाणीकरण आयात/निर्यात नहि करी पाबी। -• यदि एह एप्प 6 मसीह धरि अप्रयुक्त रहत अछि त Google एह प्रमाणीकरणक हटा देबी। -• यदि ताहि Google पासवर्ड बदलाब या Google सेटिंग सँ एह डिवाइसके डि-रजिस्टर करेस, ताहि ताहि साइन इन करबाक आवश्यकता होई।" - OAuth2 अस्थायी रूप सँ अनुपलब्ध अछि (API समय सीमा समाप्त)। - Android VR साइन इन समाप्त भेल अछि, VR में फेर सँ साइन इन करबाक प्रयास करू। - OAuth2 अस्थायी रूप सँ अनुपलब्ध अछि। - VR में साइन इन नहि करी पाउब अछि, वेबसाइट प्रमाणीकरण स्वीकृत नहि कएल गेल अछि। - VR में साइन इन नहि करी पाउब अछि: %s - Android VR सँ लॉग आउट कएल गेल - अवैध VR साइन इन टोकन, VR में फेर सँ साइन इन करबाक प्रयास करू। - QUIC प्रोटोकॉल बंद करू - DRC ऑडियो बंद करू - ओरिजनल ऑडियो भाषा फ़ोर्स करू - फुलस्क्रीन विज्ञापन छुपाउ - शेयरिंग लिंकसके सेनिटाइज करू - शेयर लिंकके youtube.com में बदलू - शेअर लिंकके youtu.be में बदलू diff --git a/patches/src/main/resources/addresources/values-mai-rIN/shared/strings.xml b/patches/src/main/resources/addresources/values-mai-rIN/shared/strings.xml index b60e0c898d..31cdbe4dcb 100644 --- a/patches/src/main/resources/addresources/values-mai-rIN/shared/strings.xml +++ b/patches/src/main/resources/addresources/values-mai-rIN/shared/strings.xml @@ -17,85 +17,9 @@ Second \"item\" text" --> - सेटिङहरू - के तपाईं जारी गर्न चाहनुहुन्छ? - सेभ गर्नुहोस् - रिसेट गर्नुहोस् - रङ रिसेट गर्नुहोस् - अवैध रङ - पुनःसुरु आवश्यक - पुनःसुरु गर्नुहोस् - आयात गर्नुहोस् - कपि गर्नुहोस् - मर्फ सेटिङहरू पूर्वनिर्धारितमा रिसेट गरियो - आयात असफल भयो: %s - सेटिङहरू खोज्नुहोस् - के बारे में - \'%s\' को लागि कुनै परिणाम फेला परेन - अर्को कीवर्ड प्रयास गर्नुहोस् - हालैका खोजहरू - खोज इतिहासबाट हटाउनुहोस्? - खोज इतिहास खाली गर्नुहोस् - के तपाईं सबै खोज इतिहास खाली गर्न निश्चित हुनुहुन्छ? - खोज सल्लाहहरू - "• नेभिगेट गर्न एउटा मार्गमा ट्याप गर्नुहोस् -• नेभिगेट गर्न एउटा सेटिङमा लामो प्रेस गर्नुहोस् -• इतिहासमा खोज प्रश्न सुरक्षित गर्न Enter थिच्नुहोस् -• खोजले केस र विरामचिह्नलाई बेवास्ता गर्छ -• अभिभावक सेटिङहरू असक्षम गरिएका बच्चा सेटिङहरूमाथि देखा पर्दछन्" - खोज इतिहास खाली छ - खोज इतिहास सुरक्षित गर्न, खोज प्रश्न टाइप गर्नुहोस् र Enter थिच्नुहोस् - सेटिङ खोज इतिहास देखाउनुहोस् - मर्फ सेटिङ आइकनहरू देखाउनुहोस् - मर्फ भाषा - "केही भाषाका लागि अनुवादहरू हराइरहेको वा अधूरै हुन सक्छन्। - -नयाँ भाषाहरू अनुवाद गर्न वा अवस्थित अनुवादहरू सुधार गर्न, morphe.software/translate मा जानुहोस्" - एप भाषा - आयात / निर्यात - मर्फ सेटिङहरू आयात / निर्यात गर्नुहोस् - फाइलबाट आयात गर्नुहोस् - सेटिङहरू सफलतापूर्वक आयात गरियो, लागू गर्न सेभमा ट्याप गर्नुहोस् - सेटिङहरू आयात गर्न असफल भयो - फाइलमा निर्यात गर्नुहोस् - सेटिङहरू सफलतापूर्वक निर्यात गरियो - सेटिङहरू निर्यात गर्न असफल भयो - तपाईं नवीनतम Morphe प्याचहरू संस्करण <i>%s</i> प्रयोग गर्दै हुनुहुन्छ। - तपाईं मर्फ प्याचहरू संस्करण <i>%1$s</i><br><br><b>अपडेट उपलब्ध: <i>%2$s</i></b><br><br> आफ्नो प्याचहरू अपडेट गर्न, यो एप प्याच गर्न मर्फ प्रयोग गर्नुहोस् - नोट - यो संस्करण एक पूर्व-रिलीज हो र तपाईं अप्रत्याशित समस्याहरूको अनुभव गर्न सक्नुहुन्छ - आधिकारिक लिङ्कहरू - वेबसाइट - दान गर्नुहोस् - अनुवादहरू - क्रेडिटहरू - अप-टु-डेट - अपडेट उपलब्ध - हालको विकास - अघिल्लो विकास - सक्रिय विकास - २०२२ – २०२५ - २०२२ – २०२५ - २०१९ – २०२२ - लाइसेन्स सूचनाहरू - डिबगिङ - डिबग लग स्ट्याक ट्रेस - मर्फ त्रुटिमा टोस्ट देखाउनुहोस् - डिबग लगहरू निर्यात गर्नुहोस् - मर्फ डिबग लगहरू निर्यात गर्नुहोस् - डिबग लगहरू सफलतापूर्वक निर्यात गरियो - डिबग लगहरू निर्यात गर्न असफल भयो - क्लियर गर्नुहोस् - डिबग लगिङ असक्षम गरिएको छ - कुनै डिबग लगहरू फेला परेन - डिबग लगहरू क्लिपबोर्डमा प्रतिलिपि गरियो - डिबग लगहरू निर्यात गर्न असफल: %s - डिबग लगहरू खाली गरियो - विशेषता झण्डा प्रबन्धक - बूलियन सुविधा झण्डाहरू प्रबन्ध गर्नुहोस् @@ -103,34 +27,11 @@ Second \"item\" text" - सुविधा झण्डाहरू सुरक्षित गरियो - सुविधा झण्डाहरू रिसेट गरियो - सुविधा झण्डाहरू क्लिपबोर्डमा प्रतिलिपि गरियो - यो सेटिङ प्रयोग गर्न एप पुनःसुरु गर्नुहोस् - शुरू - माइक्रोज सेटिङहरू खोल्नुहोस् - कारबाही आवश्यक - वेबसाइट खोल्नुहोस् - जारी राख्नुहोस् - "यो अक्षम गर्नाले माइक्रोजी ब्याट्री अनुकूलन प्रम्प्ट लुकाउनेछ। - -यदि ब्याट्री अनुकूलनहरू परिवर्तन गरिएको छैन भने तपाईँले अनुभव गर्न सक्नुहुन्छ: -• एपले अनियमित रूपमा बन्द हुन सक्छ -• सूचनाहरू काम नगर्न सक्छ -• पृष्ठभूमि प्लेब्याक काम नगर्न सक्छ" - 🚨 प्रयोगात्मक सचेत 🚨 - "तपाईं <b>%1$s</b> को प्रयोगात्मक एप संस्करण प्रयोग गर्दै हुनुहुन्छ -<br/><br/> -🔧 हामी यस एप संस्करणको लागि प्याचहरूलाई फाइन-ट्यून गर्दा विचित्र एप व्यवहार वा पहिचान नहुने बगहरूको अपेक्षा गर्नुहोस्। -<br/><br/> -यदि तपाईं सबैभन्दा समस्या-मुक्त अनुभव चाहनुहुन्छ भने, <b>अनइन्स्टल</b> गर्नुहोस् र <b>%2$s</b> को सिफारिस गरिएको एप संस्करण प्याच गर्नुहोस्।" - ⚠️ म प्रयोगात्मक चाहन्छु - ✅ म स्थिर चाहन्छु diff --git a/patches/src/main/resources/addresources/values-mai-rIN/sponsorblock/strings.xml b/patches/src/main/resources/addresources/values-mai-rIN/sponsorblock/strings.xml index 80185c122c..9d2d4ab7d5 100644 --- a/patches/src/main/resources/addresources/values-mai-rIN/sponsorblock/strings.xml +++ b/patches/src/main/resources/addresources/values-mai-rIN/sponsorblock/strings.xml @@ -16,208 +16,7 @@ Second \"item\" text" --> - SponsorBlock सक्षम गर्नुहोस् - SponsorBlock YouTube भिडियोहरूका कष्टप्रद भागहरू छोड्न क्राउडसोर्स गरिएको प्रणाली हो - स्वरूप - मत बटन देखाउनुहोस् - खण्डहरूमाथि वा तल मतदान गर्न बटन जोड्छ - वर्ग लेआउट प्रयोग गर्नुहोस् - कम्प्याक्ट स्किप बटन प्रयोग गर्नुहोस् - स्थान बचत गर्न स्किप बटनको चौडाई घटाउँछ - स्किप बटन स्वचालित रूपमा लुकाउनुहोस् - सम्पूर्ण खण्डको लागि यसलाई दृश्यमान राख्नको सट्टा केही सेकेन्डपछि स्किप बटन लुकाउँछ - स्किप बटनको अवधिसम्म - स्किप बटनहरू कति समयसम्म दृश्यमान रहने सेट गर्नुहोस् - स्किप सूचना देखाउनुहोस् - खण्ड स्वचालित रूपमा छोडियो भने सूचना देखाउँछ - स्किप सूचनाको अवधिसम्म - अनडू सूचना कति समयसम्म दृश्यमान रहने सेट गर्नुहोस् - 1 सेकेन्ड - 2 सेकेन्ड - 3 सेकेन्ड - 4 सेकेन्ड - 5 सेकेन्ड - 6 सेकेन्ड - 7 सेकेन्ड - 8 सेकेन्ड - 9 सेकेन्ड - 10 सेकेन्ड - खण्डहरू बिना भिडियोको लम्बाइ देखाउनुहोस् - कुल भिडियोको अवधिका लागि सिकबार्मा समावेश सबै खण्डहरू घटाउँछ - नयाँ खण्डहरू सिर्जना गर्दै - \'नयाँ खण्ड सिर्जना गर्नुहोस्\' बटन देखाउनुहोस् - तपाईंलाई नयाँ SponsorBlock खण्डहरू सबमिट गर्न अनुमति दिने भिडियो प्लेयरमा बटन जोड्छ - समय समायोजन चरण - समय समायोजन बटनहरूको खोजी रकम (मिसेकन्डमा) - मान सकारात्मक संख्या हुनुपर्छ - सबमिशन दिशानिर्देशहरू - दिशानिर्देशहरूमा नयाँ खण्डहरू सिर्जना गर्न नियमहरू र सुझावहरू समावेश छन् - निर्देशिकाहरूलाई ध्यान दिनुहोस् - नयाँ खण्डहरू सिर्जना गर्नु अघि SponsorBlock दिशानिर्देशहरू पढ्नुहोस् - देखाउनुहोस् - सामान्य - सामान्य - जडान त्रुटि अलर्टहरू देखाउनुहोस् - SponsorBlock सर्भर अनुपलब्ध हुँदा सूचना देखाउँछ - स्किप गणना ट्र्याकिंग सक्षम गर्नुहोस् - कितो समय बचत भएको ट्र्याक गर्न SponsorBlock लिडरबोर्डमा स्किप गणनाहरू पठाउँछ - न्यूनतम खण्ड अवधि - यी मानभन्दा छोटा खण्डहरू (सेकेन्डमा) देखाएर वा छोडेर हटाइने छैनन् - अमान्य समय अवधि - तपाईंको निजी प्रयोगकर्ता आईडी - यो गोप्य राखिनु पर्छ। यो पासवर्ड जस्तै हो र कसैसँग साझा गर्नु हुँदैन। यदि कसैसँग यो छ भने, उनी तपाईंको रूपमा प्रस्तुत गर्न सक्छन् - निजी प्रयोगकर्ता आईडी कम्तीमा ३० क्यारेक्टर लामो हुनुपर्छ। - API URL परिवर्तन गर्नुहोस् - SponsorBlock ले सर्भरमा कल गर्न प्रयोग गर्ने ठेनाना - च्यानल whitelist - SponsorBlock whitelist गरिएका च्यानलहरूको लागि असक्षम गरिएको छ - ह्वाइटलिस्ट गरिएको च्यानलमा सूचना दिहोस् - SponsorBlock whitelist गरिएका च्यानलको लागि असक्षम गरिएको छ भने सूचना देखाउँछ - SponsorBlock यस च्यानलको लागि अक्षम गरिएको छ - API URL परिवर्तन गरियो - आयात / निर्यात सेटिङहरू - प्रतिलिपि गर्नुहोस् - सेटिङहरू आयात / निर्यात गर्नुहोस् - सेटिंगहरू सफलतापूर्वक आयात गरियो - तपाईंको SponsorBlock JSON कन्फिगरेसन जुन Morphe र अन्य SponsorBlock प्लेटफर्महरूमा आयात / निर्यात गर्न सकिन्छ - "तपाईंको SponsorBlock JSON कन्फिगरेसन जुन Morphe र अन्य SponsorBlock प्लेटफर्महरूमा आयात / निर्यात गर्न सकिन्छ - -यसमा तपाईंको निजी प्रयोगकर्ता ID समावेश छ। बुद्धिमान भएर यसलाई साझा गर्नुहोस्" - सेटिङहरू सफलतापूर्वक आयात गरियो - आयात गर्न असफल भयो: %s - निर्यात गर्न असफल भयो: %s - नबैंक गरिएको / स्व-प्रचार - खण्ड व्यवहार परिवर्तन गर्नुहोस् - प्रायोजक - भुक्तानी गरिएको प्रवर्द्धन, भुक्तानी गरिएको रेफरल र प्रत्यक्ष विज्ञापन। स्व-प्रमोशन वा कारण/निर्माता/वेबसाइट/तीलाई मन परेका उत्पादनहरूका लागि निःशुल्क विज्ञापनको लागि होइन - हाइलाइट - प्रायोजक जस्तै तर अवैतनिक/स्व-प्रमोशनको लागि। व्यापारिक सामान, दान, वा उनीहरूले कोसँग सहकार्य गरेका छन् भन्ने बारे जानकारीसहित खण्डहरू समावेश छन् - प्रतिक्रिया अनुस्मारक (सदस्यता लिनुहोस्) - भिडियोको बीचमा लाइक, सदस्यता लिन वा फलो गर्नको लागि संक्षिप्त सम्झाउन। यदि यो लामो छ वा केहि विशिष्टको बारेमा हो भने, यसले स्व-प्रमोशन अन्तर्गत हुनुपर्छ - एन्डकार्डहरू / क्रेडिटहरू - भिडियोको त्यो भाग जुन धेरैजसो मानिसहरू हेर्न खोज्दै छन् - हुक / अभिवादन - वास्तविक सामग्री बिनाको अन्तराल। यसमा पज, स्थिर फ्रेम, वा पुनरावृत्ति एनिमेशन हुन सक्छ। जानकारी समावेश गर्ने संक्रमणहरू समावेश छैन - पूर्वावलोकन / सम्झना - क्रेडिटहरू वा जब YouTube endcards देखा पर्दछ। जानकारीसहित निष्कर्षहरू छैन - टेन्जेन्ट / मजाकहरू - आगामी भिडियोको लागि प्रमण्डित ट्रेलर, अभिवादन र विदाई। थप सामग्री थप्ने खण्डहरू समावेश छैन - संगीत: गैर-संगीत खण्ड - क्लिपहरूको संग्रह जसले भिडियोमा के आउँदैछ वा शृङ्खलाको अन्य भिडियोहरूमा के भयो देखाउँछ, जहाँ सबै जानकारी अन्यत्र दोहोर्याइएको छ - हाइलाइट छोड्नुहोस् - टाँगामेन्ट दृश्यहरू वा मजाकहरू जुन भिडियोको मुख्य सामग्री बुझ्न आवश्यक छैन। सन्दर्भ वा पृष्ठभूमि विवरण प्रदान गर्ने खण्डहरू समावेश छैन - प्रमो छोड्नुहोस् - केवल संगीत भिडियोहरूमा प्रयोगको लागि। संगीत नभएका खण्डहरू जुन पहिले नै अन्य श्रेणीमा कभर गरिएको छैन - हाइलाइटमा जानुहोस् - परिचय छोड्नुहोस् - बिचको भाग छोड्नुहोस् - अन्तिम भाग छोड्नुहोस् - हुक छोड्नुहोस् - पूर्वावलोकन छोड्नुहोस् - सम्झना छोड्नुहोस् - टेन्जेन्ट छोड्नुहोस् - नॉन-संगीत छोड्नुहोस् - सेग्मेन्ट छोड्नुहोस् - प्रायोजक छोडियो - स्व-प्रचार छोडियो - अनावश्यक सम्झाउनी छोडियो - हाइलाइटमा जानुहोस् - परिचय छोडियो - बिचको भाग छोडियो - अन्तिम भाग छोडियो - स्पोन्सर छुटाइयो - स्व-प्रचार छुटाइयो - असुविधाजनक रिमाइन्डर छुटाइयो - मुख्य भागमा छुटाइयो - परिचय छुटाइयो - विराम छुटाइयो - विराम छुटाइयो - आउट्रोSkip भइल - हुकSkip भइल - प्रीव्यूSkip भइल - प्रीव्यूSkip भइल - रि recapSkip भइल - टेंजेंटSkip भइल - नॉन-म्यूजिक सेक्शनSkip भइल - अनसबमिटेड सेगमेंटSkip भइल - कईटा सेगमेंटSkip भइल - ऑटोमेटिकलीSkip करी - एक बेर ऑटोमेटिकलीSkip करी - Skip बटन देखावे - सीकबार में देखावे - डिसएबल करी - सेगमेंट सबमिट करने में असमर्थ: %s - स्पोंसरब्लॉक अस्थायी रूप से डाउन है - सेगमेंट सबमिट करने में असमर्थ (स्थिति: %1$s %2$s) - सेगमेंट सबमिट करने में असमर्थ। दर सीमित (एक ही उपयोगकर्ता या आईपी से बहुत अधिक) - खण्ड सबमिट गर्न सकिएन: %s - "खंड जमा कएल नहि जा सकल। -डुप्लिकेट अछि।" - सेगमेंट सफलतापूर्वक सबमिट भइल - स्पोंसरब्लक अस्थायी रूपमा उपलब्ध छैन (API समय समाप्त भयो) - स्पोंसरब्लक अस्थायी रूपमा उपलब्ध छैन (स्थिति %s) - स्पोंसरब्लॉक अस्थायी रूप से उपलब्ध नहीं है - सेगमेंट पर वोट करने में असमर्थ (एपीआई टाइम आउट भइल) - सेगमेंट पर वोट करने में असमर्थ (स्थिति: %1$s %2$s) - सेगमेंट पर वोट करने में असमर्थ: %s - अपवोट - डाउनवोट - श्रेणी बदलें - वोट करने के लिए सेगमेंट कवनो नाईं - %1$s से %2$s - सेगमेंट श्रेणी चुनें - नया स्पोंसरब्लॉक सेगमेंट - क्या आप एक नए सेगमेंट की शुरुआत या अंत के रूप में %s सेट करना चाहत हैं? - शुरू - अंत - अब - सेगमेंट कब शुरू होता है - सेगमेंट कब खत्म होता है - क्या समय सही बा? - "सेगमेंट %1$s से शुरू होखेला - -%2$s तक - -(%3$s) - -सबमिट करे के खातिर तैयार बानी?" - सबमिट गर्नुहोस् - शुरू अंत से पहिले होखे के चाहिए - पहिले टाइम बारमा स्थान चिन्ह गर्नुहोस् - सेगमेंट के ठीक सेSkip होखत के खातिर Preview करी - सेगमेंट के समय मैनुअली एडिट करी - क्या आप सेगमेंट के शुरू या अंत के समय एडिट करना चाहत बानी? - अमान्य समय प्रदान किया गया - हाइलाइट सबमिट गर्नुहोस् - "हाइलाइट समय सही छ? - -%s - -यदि तपाइँ हाइलाइट सबमिट गर्दै हुनुहुन्छ भने, रद्द गर्नुहोस् र दोस्रो स्थान चिन्ह गर्नुहोस्" - हाइलाइट सबमिट गर्नुहोस् - सांख्यिकी - सांख्यिकी अस्थायी रूप से उपलब्ध नहीं है (एपीआई डाउन है) - स्पोंसरब्लॉक अक्षम बा - तहार उपयोगकर्ता नाम: <b>%s</b> - अपना उपयोगकर्ता नाम बदलने के लिए इहाँ टैप करी - उपयोगकर्ता नाम बदलने में असमर्थ: स्थिति: %1$s %2$s - उपयोगकर्ता नाम सफलतापूर्वक बदलल गइल - तपाईंको प्रतिष्ठा <b>%.2f</b> छ - तुम <b>%s</b> सेगमेंट बनवले हउ - तपाईंका खण्डहरू हेर्न यहाँ ट्याप गर्नुहोस् - स्पोंसरब्लॉक लीडरबोर्ड - तुम <b>%s</b> सेगमेंट से बचावले हउ - ग्लोबल तथ्याङ्क र शीर्ष योगदानकर्ताहरू हेर्न यहाँ ट्याप गर्नुहोस् - यो <b>%s</b> को तिनीहरूको जीवन हो।<br>लिडरबोर्ड हेर्न यहाँ ट्याप गर्नुहोस् - तुम <b>%s</b> सेगमेंटSkip कइले हउ - यो <b>%s</b> हो - Skip किए गए सेगमेंट काउंटर रीसेट करी? - %1$s घंटे %2$s मिनट - %1$s मिनट %2$s सेकंड - %s सेकंड - अपारदर्शिता: - रंग: diff --git a/patches/src/main/resources/addresources/values-mai-rIN/youtube/strings.xml b/patches/src/main/resources/addresources/values-mai-rIN/youtube/strings.xml index 7574feda5a..4ec76b1150 100644 --- a/patches/src/main/resources/addresources/values-mai-rIN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-mai-rIN/youtube/strings.xml @@ -18,133 +18,40 @@ Second \"item\" text" - लगभग - विज्ञापन - फीड - सामान्य - प्लेयर - सीकबार - स्वाइप नियंत्रण - विविध - वीडियो - पृष्ठभूमि प्ले प्रतिबन्धहरू हटाउनुहोस् - लेआउट अपडेटहरू अक्षम गर्नुहोस् - फिड फ्लाईआउट मेनु फिल्टर सक्षम गर्नुहोस् - फिड फ्लाईआउट मेनु फिल्टर - एल्बम कार्ड छिपाएं - कलाकार कार्ड छिपाएं - सामुदायिक पोस्ट छिपाएं - कॉम्पैक्ट बैनर छिपाएं - फ़्लोटिंग माइक्रोफ़ोन बटन छिपाएं - क्षैतिज शेल्फ छिपाएं - छवि शेल्फ छिपाएं - “नवीनतम भिडियोहरू” बटन लुकाउनुहोस् - मिक प्लेलिस्ट छिपाएं - मूवी सेक्शन छिपाएं - \'मुझे सूचित करें\' बटन छिपाएं - प्लेएबल छिपाएं - खोज शब्द थम्बनेलहरू लुकाउनुहोस् - \'और दिखाएं\' बटन छिपाएं - सदस्यता लिइएको च्यानल बार लुकाउनुहोस् - चिकित्सा पैनल छिपाएं - टिकट शेल्फ छिपाएं - खोज परिणाम कीवर्ड द्वारा फ़िल्टर नहीं किए गए हैं + - होम टैब में वीडियो कीवर्ड द्वारा फ़िल्टर किए गए हैं - वेब खोज परिणाम छिपाएं - ‘तपाईंले मनपर्न सक्नुहुन्छ’ खण्ड लुकाउनुहोस् - YouTube डूडल छिपाएं - चैनल बार छिपाएं - चैनल वॉटरमार्क छिपाएं - क्राउडफंडिंग बॉक्स छिपाएं - आपातकालीन बॉक्स छिपाएं - जानकारी पैनल छिपाएं - \'जुड़ें\' बटन छिपाएं - “लाइभ च्याट रिप्ले” बटन लुकाउनुहोस् - चिकित्सा पैनल छिपाएं - सदस्य दिशानिर्देश छिपाएं - समयबद्ध प्रतिक्रियाएं छिपाएं - भिडियो शीर्षक लुकाउनुहोस् - वीडियो विवरण - हाइप पॉइंट छिपाएं - सब्सक्राइब बटन छिपाइ - फ़िल्टर बार - लिंक पूर्वावलोकन छिपाएं - च्यानल ट्याब फिल्टर सक्षम गर्नुहोस् - च्यानल ट्याब फिल्टर - सामुदायिक बटन छिपाइ - जुडबा बटन छिपाइ - सदस्य शेल्फ दिखाया गया है - टिप्पणियां - पोस्ट शेल्फ लुकाउनुहोस् - स्टोर बटन छिपाइ - सब्सक्राइब बटन छिपाइ - चैनल दिशानिर्देश छिपे हुए हैं - कमेन्ट क्यारोसेल फिल्टर सक्षम गर्नुहोस् - कमेन्ट क्यारोसेल फिल्टर - \'सदस्यों द्वारा टिप्पणियाँ\' हेडर छिपाएं - समुदाय दिशानिर्देश छिपाएं - \'एक शॉर्ट बनाएं\' बटन छिपाएं - इमोजी और टाइमस्टैम्प बटन छिपाएं - होममा भिडियोहरूमा कमेन्ट खण्ड लुकाउनुहोस् - धन्यवाद बटन छिपाएं - जानकारी लुकाउने बटन - कस्टम फ़िल्टर सक्षम है - घटक पथ बिल्डर तारों की सूची नई लाइन से अलग करके फ़िल्टर करें - कीवर्ड फिल्टर - होममा भिडियोहरूलाई कीवर्डबाट लुकाउनुहोस् - कीवर्ड द्वारा खोजी परिणामहरू लुकाउनुहोस् - सबकीवर्ड द्वारा सदस्यता वीडियो छिपाएं - छिपाने के लिए कीवर्ड - पूरे शब्द मिलान करें - डबल उद्धरण के साथ एक कीवर्ड/वाक्यांश को घेरने से वीडियो शीर्षक और चैनल नामों के आंशिक मिलान को रोका जाएगा<br><br>उदाहरण के लिए,<br><b>\"ai\"</b> वीडियो को छिपा देगा: <b>AI कैसे काम करता है?</b><br>लेकिन यह नहीं छिपाएगा: <b>उचित उपयोग का क्या अर्थ है?</b> - कीवर्ड का उपयोग नहीं कर सकते: %s - कीवर्ड का उपयोग करने के लिए उद्धरण जोड़ें: %s - कीवर्ड में विरोधाभासी घोषणाएँ हैं: %s - कीवर्ड बहुत छोटा है और उद्धरण की आवश्यकता है: %s - कीवर्ड सभी वीडियो छिपा देगा: %s - सांख्यिकी - क्रिएटर स्टोर शेल्फ छिपाएं - एंड स्क्रीन स्टोर बैनर छिपाएं - सामान्य विज्ञापन छिपाएं - मर्चेंडाइज बैनर छिपाएं - प्लेयर पपअप विज्ञापनहरू लुकाउनुहोस् - स्व-प्रायोजित कार्ड छिपाएं - शॉपिंग लिंक छिपाएं - वीडियो विज्ञापन छिपाएं - YouTube Premium प्रमोशन छिपाइ @@ -163,81 +70,30 @@ Second \"item\" text" - \'टीव्ही में साइन इन करी\' पॉपअप अक्षम करी - नबैंक गरिएको / स्व-प्रचार - लूप वीडियो सक्षम करी - लूप वीडियो चालू अछि - लूप वीडियो बंद अछि - शुरू - अंत - प्लेसूची मोड उत्पन्न गर्नुहोस् - सबै सामग्री (समय अनुसार क्रमबद्ध गर्नुहोस्, बढ्दो) - सबै सामग्री (क्रमबद्ध गर्नुहोस्) - सबै सामग्री (लोकप्रिय अनुसार क्रमबद्ध गर्नुहोस्) - भिडियोहरू मात्र (क्रमबद्ध गर्नुहोस्) - भिडियोहरू मात्र (लोकप्रिय अनुसार क्रमबद्ध गर्नुहोस्) - स्ट्रिम गरिएका भिडियोहरू मात्र (क्रमबद्ध गर्नुहोस्) - स्ट्रिम गरिएका भिडियोहरू मात्र (लोकप्रिय अनुसार क्रमबद्ध गर्नुहोस्) - सबै सदस्य मात्र सामग्री - सदस्यहरू मात्र भिडियोहरू - सदस्यहरू मात्र लाइभ स्ट्रिमहरू - च्यानल ID बेमेलको कारण प्लेसूची उत्पन्न गर्न असमर्थ - भिडियो ID उपलब्ध छैन। कृपया भिडियोलाई म्यानुअल रूपमा रिलोड गर्नुहोस् - भिडियोलाई सफलतापूर्वक \'पछि हेर्नको लागि\' प्लेलिस्टमा बचत गरियो - यो भिडियो पहिले नै \'पछि हेर्नको लागि\' प्लेलिस्टमा बचत गरिएको छ - यो भिडियोलाई \'पछि हेर्नको लागि\' प्लेलिस्टमा बचत गर्न सकिएन - सटीक अनुरोध जेस्चर अक्षम करें - स्लाइड टू सीक सक्षम करी - टैप टू सीक सक्षम करें - प्रेस टू स्वाइप जेस्चर सक्षम करें - हैप्टिक फीडबैक सक्षम करें - चमक सहेजें और पुनर्स्थापित करें - स्वचालित चमक जेस्चर सक्षम करें - स्वचालित - स्वाइप ओवरले टाइमआउट - ओवरले दिखाई देने वाले मिलीसेकंड की मात्रा - स्वाइप ओवरले पृष्ठभूमि अस्पष्टता - स्वाइप ओवरले चमक रंग - स्वाइप ओवरले वॉल्यूम रंग - स्वाइप ओवरले टेक्स्ट आकार - स्वाइप परिमाण थ्रेशोल्ड - स्वाइप ओवरले शैली - क्षैतिज ओवरले - क्षैतिज ओवरले (न्यूनतम - ऊपर) - क्षैतिज ओवरले (न्यूनतम - केंद्र) - वृत्ताकार ओवरले - वृत्ताकार ओवरले (न्यूनतम) - ऊर्ध्वाधर ओवरले - ऊर्ध्वाधर ओवरले (न्यूनतम) - वीडियो बदलने के लिए स्वाइप सक्षम करें - कारबाही बटनहरू - पोर्ट्रेट बटनहरू - लाइक और सब्सक्राइब चमक छिपाएं - एक्शन बार लपاوुहोस् @@ -255,41 +111,19 @@ Second \"item\" text" - फुलस्क्रीन बटनहरू - क्याप्शनहरू - ऑटो कैप्शन दृश्यता - कविकैर नहि देखाइत - म्यूटित कब स देखाइत अछि - क्याप्शन कुकीहरू - कुकीहरू प्राप्त गर्न क्लिक गर्नुहोस् - -कुकीहरू स्वचालित रूपमा बचत हुनेछन् यदि ती मान्य छन् - कुकीहरू पहिले नै अवस्थित छन् - कुकीहरू सफलतापूर्वक अद्यातन गरिए - प्रतिलिपि ठीक गर्नुहोस् - प्रतिलिपि उपलब्ध छ - नेविगेशन बटन - सेटिङ बटन कार्य - नेभिगेशन बटन लेबलहरू लुकाउनुहोस् - नेभिगेसन बार लुकाउनुहोस् - संकरी नेविगेशन बटन सक्षम करी - नेभिगेशन बार एनिमेसनहरू सक्षम गर्नुहोस् - स्वचालित रूपमा लुकाउने नेभिगेसन बारलाई अक्षम गर्नुहोस् - टूलबार - फ्लाईआउट मेनु - ऑटोप्ले पूर्वावलोकन छिपाइ - ओभरले बटनहरू - प्लेयर नियन्त्रण बटन पृष्ठभूमि लुकाउनुहोस् - एन्ड स्क्रिन कार्डहरू लुकाउनुहोस् - एम्बियेंट मोड - एम्बियेंट मोड प्रतिबन्धहरू बाइपास गर्नुहोस् - एम्बियेंट मोड अक्षम गर्नुहोस् - फुलस्क्रिनमा एम्बियन्ट मोडलाई अक्षम गर्नुहोस् - जानकारी कार्डहरू लुकाउनुहोस् - रोलिङ संख्या एनिमेसनहरूलाई अक्षम गर्नुहोस् - भिडियो प्लेयर सीकबार लुकाउनुहोस् - भिडियो थम्बनेल सीकबार लुकाउनुहोस् - फुलस्क्रिनमा ठूलो सीकबार सक्षम गर्नुहोस् - एआई बटन लुकाउनुहोस् - \'ऑटो-डब\' लेबल छिपाइ - च्यानल बार लुकाउनुहोस् - टिप्पणी बटन लुकाउनुहोस् - इफेक्ट बटन लुकाउनुहोस् - भिडियो लिङ्क लेबल लुकाउनुहोस् - \'ग्रीन स्क्रीन\' बटन छिपाइ - ह्यासट्याग बटन लुकाउनुहोस् - जानकारी प्यानल लुकाउनुहोस् - जवाइन बटन लुकाउनुहोस् - लाइक बटन लुकाउनुहोस् - लाइक बटन फाउन्टेशन लुकाउनुहोस् - लाइव पूर्वावलोकन छिपाइ - स्थान लेबल लुकाउनुहोस् - नेभिगेसन बार लुकाउनुहोस् - \'नया पोस्ट\' बटन छिपाइ - पज गरिएका ओभरले बटनहरू लुकाउनुहोस् - टिप्पणी पूर्वावलोकन लुकाउनुहोस् - रिमिक्स बटन लुकाउनुहोस् - \'संगीत सहेज\' बटन छिपाइ - साउंड बटन देखाइएको छ - साझेदारी बटन लुकाउनुहोस् - सर्च सुझावहरू देखाइका छन् - ध्वनि बटन लुकाउनुहोस् - ध्वनि मेटाडाटा लेबल लुकाउनुहोस् - स्टिकरहरू देखाइका छन् - \'सुपर धन्यवाद\' बटन छिपाइ - सब्सक्राइब बटन देखाइएको छ - ट्याग्ड उत्पादनहरू देखाइका छन् - \'एह ध्वनि क उपयोग करी\' बटन छिपाइ - \'एह टेम्पलेट क उपयोग करी\' बटन छिपाइ - भिडियो शीर्षक लुकाउनुहोस् - किनाराको स्क्रिनमा सुझाव भएको भिडियो लुकाउनुहोस् - प्लेयर सम्बन्धित भिडियोहरू लुकाउनुहोस् - भिडियो टाइमस्ट्याम्प लुकाउनुहोस् - प्लेयर पपअप प्यानलहरू अक्षम गर्नुहोस् - निष्क्रिय - प्लेयर ओभरले अपारदर्शिता - डिफ़ॉल्ट - फ़ोन - टैबलेट - ऑटोमोटिव - 20.39.41 - पुरानो बचतलाई प्लेलिस्टमा पुनर्स्थापित गर्नुहोस् - 20.28.41 - बोल्ड आइकनहरू अक्षम गर्नुहोस् - सिस्टम सेयर शीट खोल्नुहोस् - YouTube संगीत बटनहरूलाई ओभरराइड गर्नुहोस् - डिफ़ॉल्ट - सब सबस्क्रिपशंस - चैनल ब्राउज़ करी - पाठ्यक्रम/शिक्षण - एक्सप्लोर करी - फैशन और सौंदर्य - गेमिंग - इतिहास - लाइब्रेरी - पसंद के वीडियो - लाइव - मूवी - म्यूजिक - खबर - सूचनाएं - प्लेलिस्ट - सर्च करी - शपिंग - स्पोर्ट्स - सबस्क्रिपशंस - ट्रेंडिंग - वर्चुअल रियलिटी - बाद में देखी - आपके क्लिप - रेगुलर प्लेयर - रेगुलर प्लेयर फुलस्क्रीन - मिनीप्लेयरको पुन: सुरुवात अक्षम गर्नुहोस् - मिनीप्लेयर - मिनीप्लेयर प्रकार - अक्षम करी - डिफ़ॉल्ट - न्यूनतम - गोल कोनों के डिसेबल करी - ड्रैग और ड्रॉप डिसेबल करी - क्षैतिज ड्रैग जेस्चर डिसेबल करी - ओवरले बटन छुपावे - पिक्सेल आकार %1$s और %2$s के बीच होखे के चाहिए - ओवरले अपारदर्शिता - सक्षम ग्रेडियेंट लोडिंग स्क्रीन - कस्टम सीकबार रंग सक्षम करी - अमान्य सीकबार रंग मान - हेडर लोगो - डिफ़ॉल्ट - रेगुलर - कस्टम - इमेज रीजियन प्रतिबंध बाईपास करी - मूल थंबनेल - DeArrow र मौलिक थम्बनेलहरू - DeArrow र स्थिर क्याप्चरहरू - अभी के कैप्चर - DeArrow थम्बनेल क्याशे endpoint क URL - स्टिल वीडियो कैप्चर - स्टिल कैप्चर प्रत्येक वीडियो क शुरुआत / बीच / अंत स लेल जाइत अछि। एहि इमेजेस YouTube में बनेल अछि आ बाहरी API क उपयोग नहि कएल जाईत अछि। - अंत - DeArrow अस्थायी रूप क्षतिग्रस्त अछि (स्टेटस कोड: %s) - DeArrow अस्थायी रूप क्षतिग्रस्त अछि - पुरानो प्लेयर बटन शैली पुनर्स्थापित गर्नुहोस् - डिवाइस आयाम क स्पूफ करी - हैप्टिक फीडबैक - अध्याय हैप्टिक्स अक्षम करी - सटीक खोज हैप्टिक्स अक्षम करी - खोज पूर्ववत हैप्टिक्स अक्षम करी - ट्याप र होल्ड ह्याप्टिक प्रतिक्रिया अक्षम गर्नुहोस् - ज़ूम हैप्टिक्स अक्षम करी - लिंक ब्राउजर में खोलि - ऑटो - Wi-Fi नेटवर्क पर डिफ़ॉल्ट वीडियो गुणवत्ता - मोबाइल नेटवर्क पर डिफ़ॉल्ट वीडियो गुणवत्ता - कस्टम प्लेबैक स्पीड मेनू - पुरान प्लेबैक स्पीड मेनू पुनर्स्थापित करी - कस्टम प्लेबैक स्पीड - अनुकूल प्लेब्याक गति %s भन्दा कम हुनुपर्छ - अमान्य कस्टम प्लेबैक स्पीड - ऑटो - डिफ़ॉल्ट प्लेबैक स्पीड - बदलिएको पूर्वनिर्धारित प्लेब्याक गति: %s - HDR वीडियो अक्षम करी - प्रीमियम गुणस्तर विकल्पहरू लुकाउनुहोस् - स्पूफिंग साइड इफेक्ट - • AV1 वीडियो कोडेक नहि अछि। - • ३६०° VR इमर्सिभ मोड उपलब्ध छैन - • स्थिर वॉल्यूम उपलब्ध नहि अछि। - • यदि डिओबफस्केशन असफल भयो भने, प्लेब्याक असफल हुन सक्छ - • मूल ऑडियो फोर्स उपलब्ध नहि अछि। - प्लेयर जावास्क्रिप्ट भिन्नता diff --git a/patches/src/main/resources/addresources/values-mk-rMK/music/strings.xml b/patches/src/main/resources/addresources/values-mk-rMK/music/strings.xml index 54abe572ab..60933b346e 100644 --- a/patches/src/main/resources/addresources/values-mk-rMK/music/strings.xml +++ b/patches/src/main/resources/addresources/values-mk-rMK/music/strings.xml @@ -17,30 +17,197 @@ Second \"item\" text" --> + Последно пуштено + Претплати + Видео записи + Други резултати + Прикачени песни + Прикачени албуми + Прикачени изведувачи + Ажурирајте го Jam пачот на домаќинот за да користите пауза и продолжување + Споделување на Jam редица + Бидете домаќин или придружете се на близок Jam и уредувајте го неговиот ред преку Jam слојот + Овозможете го споделувањето на Jam редица + Ја овозможува Jam интеграцијата по рестартирањето на YouTube Music + Преземете го Jam Layer за да започне да работи + Отворете ја најновата верзија на Jam слојот во вашиот прелистувач + Jam контроли + Бидете домаќин или придружете се на близок Jam и управувајте со сесијата + Jam Layer пакет + Изберете ја инсталираната придружна апликација што се користи за споделување на редот во близина + Користете инсталиран придружен пакет. Спарувањето бара ваше одобрение и локален токен за можност + Да се вклучи Wi-Fi? + Вклучете го Wi-Fi, а потоа обидете се повторно да започнете или да се приклучите на Jam + Отвори ги поставките за Wi-Fi Откажи + Користи пакет + Внесете валидно име на Android пакет + Пакетот е зачуван. Поставете го Jam Layer за да се поврзете + Jam не е поврзано + Прво овозможете го Jam слојот + Овозможете го споделувањето на Jam редица во поставките, а потоа рестартирајте го YouTube Music + Прво инсталирајте го избраниот Jam Layer пакет + Внесете го кодот прикажан на হোস্ট системот. Wi-Fi Aware може да поврзе уреди во близина дури и кога нивните LAN мрежи се различни + Придружете се со код + Придружи се + Скенирај QR + Внесете го осмосимјавниот код или залепете покана + Придружете се на вашиот Jam + Копирај код + Jam код + Покажи QR Готово + Покани во твојот Jam + Скенирајте за да се приклучите и да ја уредите оваа листа. Прекинувањето на Jam-от ја повлекува поканата + Копирај линк + Покана за Jam + Jam-от се поврзува повторно; обиди се повторно набрзо + Додадено во Jam + Jam-от е недостапен; траката не беше додадена + Твојот Jam + Слушаме заедно + Примемовање во Jam + Слушајте заедно + Ажурирање на Jam… + Пребарување на вашиот домаќин на Wi-Fi… + Чекање луѓе + %s личност е поврзана + %s луѓе се поврзани + Внесете го овој код на уред во близина. Wi-Fi Aware работи преку посебни LAN мрежи кога е поддржано. Завршува за %s минути + Премотај до %s? + Уредувањето е отворено + Уредувањето е заклучено + Поврзано преку Wi-Fi Aware + Поврзано преку Wi-Fi + Поврзано преку Bluetooth + Неподдржано за време на Jam + Приклучувањето е откажано + Повторно поврзување со вашиот домаќин… + Споделете редица. Дозволете сите да додаваат песни + Поканете луѓе + Гостите можат да ја уредуваат листата + Заврши Jam + Хостувачот ја пушта музиката. Додавајте, поместувајте или отстранувајте песни директно во редицата на YouTube Music + Држете ги двата уреди во близина. Можете да откажете додека го бараме хостувачот + Откажи приклучување + Напушти Jam + Започни Jam + Постави Jam слој + Управувај Детали + Отвори + Вчитување опции за песни... + Опциите за песни се недостапни; обиди се повторно + Не можеа да се отворат опциите за песни + Нема достапен прелистувач за преземање на Jam слојот + Чекање на редот на домаќинот + Прво отвори песна во YouTube Music + Пушти на домаќинот + Напуштете го Jam-от и пуштете локално + Пуштете следно во Jam + Додај во Jam редата + Пуштете ја оваа Jam трака + Изберете каде да пуштите + Преместите го домаќинот овде + Песната не стана подготвена за премотување + Редот се промени; обидете се повторно со гестот Лого на заглавието Стандардно По нарачка + Пуштај аудио песни од албумот + Пуштај ја верзијата само со аудио од албумот наместо музичкиот спот. Потребно е \'Лажирани видео стримови\' + Овозможи прекинувач за аудио/видео + Обидете се да ги смените аудио верзијата на траката и официјалното музичко видео + Преземете музички видеа, подкасти или песни со апликации за преземање од трети страни + Користете надворешен преземач за менито за Преземање + Опцијата за Преземање во менијата наместо тоа го отвора вашиот надворешен преземач + Преземања + "Нема преземања +Преземените песни се појавуваат овде" + %1$s • %2$s • %3$s MB + %1$s песни • %2$s MB + Пребарај преземања + Сортирај + Сортирај по изведувач + Сортирај по наслов + Сортирај по големина + Избриши ги сите преземања + Дали сакате да ги избришете сите преземања? + Сите преземени песни ќе бидат избришани од овој уред + Преземањата се избришани + Ништо не е пронајдено + Активности за %s + Пуштај + Пуштај или паузирај + Претходна Следно + Паузирај + Затвори + Избриши + Избриши преземање + Избриши преземање? + %s ќе биде избришан од овој уред + Преземањето е избришано + Не успеа да се избрише преземањето + Не успеа да се пушти преземањето + Преземања на аудио + Воспроизведување офлајн + Не успеа да се идентификува траката за преземање + Преземањето е веќе во тек + Веќе е преземено + Преземањето започна + Започнување преземање… + Преземање %s%% + Преземањето е завршено + Преземањето не успеа За апликацијата Реклами Содржини + Општо + Играч + Текстови + Разно + Јазик на YouTube Music + Системски јазик + Потребен е Android 13 или понова верзија Скриј „Music Premium“ ознака + Скриј ги Music Premium промоциите Скриј видео реклами + Скриј прекинувач за аудио / видео + Скриј копче за преземање + Го крие копчето за преземање во подкаст емисијата + Скриј полица за истражување + Скриј лента за филтрирање + Скриј мрежни полици + Скриј хоризонтални полици + Скриј листа-полици + Панел за текстови + Скриј копче за споделување + Скриј копче за преведување + Скриј полица \"Ново од\" + Скриј полици за плејлисти + Скриј копче за повторување + Скриј копче за Shuffle + Скриј ја полицата \'Speed dial\' + Коментари + Изберете кои компоненти ќе се појавуваат во делот за коментари + Скриј ги насоките за заедницата + Скриј порака за контекст + Скриј копче за Emoji + Скриј копче за Инфо + Скриј копче за временски печат Бледеење Плавно преклопување на патите еден во друг @@ -78,21 +245,110 @@ Second \"item\" text" Фадење е паузирано за оваа сесија Фадење е продолжено Режим на видео не е достапен додека е вклучено преминувањето + Кросфејд е исклучен при SABR репродукција + Кросфејдот е исклучен при пренос + Како функционира + "Crossfade-от го преклопува крајот на една песна со почетокот на следната, користејќи го вашето времење и крива + +Работи при автоматско преминување и при рачно прескокнување, секој со сопствен прекинувач. При автоматско преминување, мешањето започнува неколку секунди пред песната да заврши" + Ограничувања + "Музичките видеа се репродуцираат само како аудио додека кросфејдот е вклучен. Долг притисок на shuffle за да го паузирате кросфејдот и да го гледате видеото + +Музичко видео што започнува со репродукција останува во видео режим до следната песна + +Песните пократки од времетраењето на преливањето не се кросфејдуваат + +Заклучениот екран или известувањето накратко можат да покажат застареана состојба во средината на транзицијата + +Паузирањето на кросфејдот за гледање музичко видео може да остави некои видеа црни. Паузирајте го кросфејдот пред да ја изберете песната за да го избегнете тоа" + Не е поддржано + "Пренос или рефлектирање на аудио (Chromecast, рефлектирање на екран или аудио) + +SABR репродукција (имитирање на видео стримови е исклучено)" + Кредити + Функција Crossfade од VazerOG + Запомни состојба на повторување + Запомни состојба на мешање + Брзина на репродукција + Брзина што се користи за сите песни. На 100% се користи брзината на апликацијата, како на пример брзината на репродукција на подкасти + Променете го тонот со брзината + Оневозможи пренасочување при неидејк + Спречува прескокување на следната нутра кога е притиснато копчето за несакано + Акција при допир на трака од листата за репродукција + Пуштете ја листата за репродукција + Пуштете ја само избраната трака + Додадете ја избраната трака во редот за репродукција + Додадено во редата + Скриј копче за емитување + Скриј копче за историја + Скриј копче за известувања + Скриј копче за пребарување + Скриј го копчето за гласовно пребарување + Скриј го копчето за пребарување по звук + Скриј го копчето „Ново“ + Го крие копчето „Ново“ во табулаторот Библиотека + Копчиња за акции + Изберете кои копчиња за акции да се појавуваат под плејорот + Скриј ја лентата за акции + Скриј ги копчињата за „Ми се допаѓа“ и „Не ми се допаѓа“ + Скриј копче за коментари + Скриј копче за \'Повторно пуштање на разговор во живо\' + Скриј копче за текст од песната + Скриј го копчето Сподели + Скриј копче Зачувај + Скриј копче Преземи + Скриј копче за \'Стартувај радио\' + Искакучко мени + Изберете кои компоненти ќе се појавуваат во искакувачките менија на плеерот и редата за репродукција + Скриј ја компонентата од 3 колони + Скриј копчиња за „Ми се допаѓа“ и „Не ми се допаѓа“ + Скриј го менито \"Поклопување на вкус\" + Скриј го менито „Додај во плејлистата \'Подоцна\'“ + Скриј го менито „Додај во редата“ + Скриј мени за титлови + Скриј го менито „Избриши плејлиста“ + Скриј го менито „Испразни ја редата“ + Скриј го менито \"Не препорачувај го овој артист\" + Скриј мени за преземање + Скриј го менито \"Уреди ја листата за 재생\" + Скриј го менито \"Оди до албумот\" + Скриј го менито \"Оди до изведувачот\" + Скриј го менито \"Оди до епизодата\" + Скриј го менито \"Оди до подкастот\" + Скриј мени за помош + Скриј го менито \"Означи ја епизодата како пуштена\" + Скриј го менито \"Не сум заинтересиран\" + Скриј го менито \"Зацврсти на брзо избирање\" + Скриј го менито \"Пушти следно\" + Скриј мени за квалитет + Скриј го менито \"Отстрани од библиотеката\" + Скриј го менито \"Отстрани од листата за 재생\" + Скриј мени за пријавување + Скриј го менито \"Зачувај ја епизодата за подоцна\" / \"Зачувај во библиотека\" + Скриј го менито „Зачувај во плејлиста“ + Скриј мени за споделување + Скриј го менито „Насумиран залог“ + Скриј го менито „Тајмер за спиење“ + Скриј го менито \"Започни радио\" + Скриј го менито \"Статистика за експерти\" + Скриј мени за претплата + Скриј го менито \"Откажи прицврстување од брзо бирање\" + Скриј го менито \"Прегледај кредити на песната\" Почетна страница Стандардно @@ -106,9 +362,13 @@ Second \"item\" text" Пребарување Претплати + Присилно стави портретна ориентација + Ја задржува апликацијата во портретна ориентација кога уредот се ротира Промени ја бојата на мини-плеер Бојата на мини-плеер се прилагодува на целосен екран + Промени ја бојата на лентата за навигација + Ја усогласува бојата на лентата за навигација со полноекранскиот плеер Прикажи копче „Следна“ во мини-плеер Прикажи копче „Претходна“ во мини-плеер @@ -116,6 +376,8 @@ Second \"item\" text" Вклучи принуден мини-плеер Спречува автоматско прекинување на плеер во целосен екран кога почнува нова песна + Овозможи прелистување за затворање на мини-плејер + Овозможува затворање на мини-плејерот со прелистување надолу врз него Навигациска лента Изберете кои копчиња да се појавуваат во долната навигациска лента @@ -132,17 +394,297 @@ Second \"item\" text" Скриј навигациска лента Скриј ознаки на копчињата за навигација + Не сте најавени. Допрете за да конфигурирате + Одјави се + Одјавен + Овозможи scrobbling + Испрати статус „Се пушта сега“ + Минимално времетраење на песна + Песните пократки од ова нема да бидат scrobbled + Процент на одложување на scrobble + Покрети scrobble по слушање на овој процент од песната + Секунди одложување на scrobble + Покрети scrobble по слушање во текот на овој број секунди + ListenBrainz сметка + Најавени. Допрете за управување со сметката + Внесете го вашиот ListenBrainz кориснички токен за да го овозможите scrobbling + Токен + Потврди + Ве молиме внесете токен + Валидирање… + Валиден токен! Корисник: %s + Невалиден токен: %s + непознато + Верификацијата не успеа: %s + Токенот е избришан + Токенот е зачуван + Предупредување: Зачуваниот токен е невалиден! + Сакај ги омилените песни + Автоматски сакај песни на Last.fm кога ќе ги омилиш на YouTube Music (и отстрани го сакањето кога ќе ги тргнеш од омилени или ќе ги означиш како несакани) + Last.fm сметка + Најавен како %s. Допрете за управување со сметката + Внесете ги вашите Last.fm податоци за најавување + Корисничко име + Лозинка + Најави се + Ве молиме внесете корисничко име и лозинка + Најавување… + Успешно се најавивте! + Неуспешно добивање сесија од Last.fm + Најавувањето не успеа: %s + Креденцијалите за Last.fm се зачувани! + Чистење на метаподатоци + Исчисти метаподатоци + Автоматски чистење на вообичаениот непотребен текст (како \"Official Video\", \"Remaster\", \"Live\") од насловите, изведувачите и албумите пред scrobbling + Прилагодено regex чистење + Внесете прилагоден регуларен израз за да отстраните дополнителен текст од насловите на песните, изведувачите или албумите. Пример: (?i)\\s*\\((clean|explicit)\\) + Извлечи изведувач и песна од насловот + Издвојте го името на изведувачот и навивот од насловот на видеото користејќи го одделителoт \" - \". Корисно за повторно прикачено содржина каде што името на каналот може да се користи како изведувач + Пополни недостаточен албум + Кога музиката не вклучува албум, побарај го на Last.fm за вашиот scrobble да биде комплетен + "Scrobbling автоматски ја синхронизира вашата употреба на YouTube Music со вашиот Last.fm или ListenBrainz профил + +За да се осигурате дека scrobbling е синхронизиран за време на репродукција во позадина, проверете дали употребата на батеријата на апликацијата YouTube Music е наместена на \"Неограничена\"" + Користи текстови од трети страни + Прикажува текстови од сервис на трета страна, враќајќи се на вградените текстови кога не се пронајдени такви + Држи го екранот вклучен додека ги гледате текстовите + Спречува екранот да се исклучи додека е отворен панелот за текстови + Сервис за текстови + Приказ + Синхронизација + Панел + AI крајна точка + Сите извори се исклучени - вградени текстови и вметнати текстови + Ресетирај го редоследот на провајдерите? + Врати ги провајдерите на стандардниот редослед на приоритет + Исчисти ги зачуваните текстови + Избришете ги текстовите за песните зачувани на овој уред + Исчистено %1$s MB + Земи токен + Apple Music Media-user-token + "Залепете го media-user-token колачињато од music.apple.com + +Чекори: +1. Најавете се на music.apple.com како корисник член +2. Отворете DevTools → Application → Cookies → music.apple.com +3. Копирајте ја вредноста на \"media-user-token\"" + Apple Music: media-user-token зачувано + Apple Music: media-user-token избришано + Apple Music: невалиден media-user-token - ве молиме проверете и обидете се повторно + Spotify SpDc + "Залепете ја sp_dc колачињата од Spotify. + +Чекори: +1. Најавете се на open.spotify.com +2. Отворете DevTools → Application → Cookies → open.spotify.com +3. Копирајте ја вредноста на \"sp_dc\"" + Spotify: sp_dc зачувано + Spotify: sp_dc е избришан + Spotify: невалиден sp_dc - ве молиме проверете и обидете се повторно + Apple Music токенот е опционален. Без него, Lyrically ќе се користи како резерва + Deezer ARL + "Залепете го arl колачињата од Deezer. + +Чекори: +1. Најавете се на deezer.com +2. Отворете DevTools → Application → Cookies → deezer.com +3. Копирајте ја вредноста на \"arl\"" + Deezer: arl е зачувано + Deezer: arl е исчистено + Deezer: невалиден arl - ве молиме проверете и обидете се повторно + Musixmatch токен + Додадете токен само за да ја користите сопствената сметка. Тој мора да дојде од Musixmatch Android апликацијата: токените копирани од musixmatch.com припаѓаат на повлечениот десктоп клиент и API-то ги одбива + Musixmatch работи без токен, па затоа ова поле може да остане празно + кориснички токен + Musixmatch: корисничкиот токен е зачуван + Musixmatch: корисничкиот токен е исчистен + Musixmatch: невалиден токен на корисникот - ве молиме проверете и обидете се повторно + YouTube колачиња + "Залепете ги вашите YouTube колачиња за да пребарувате написи и временски текст од YouTube. + +Чекори: +1. Отворете youtube.com во прелистувач и најавете се +2. Отворете DevTools (F12) → Application → Cookies → youtube.com +3. Копирајте го целиот стринг на колачиња (парови клуч=вредност одделни со точка-запирка) + +Потребни колачиња: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPISID, LOGIN_INFO" + YouTube: колачињата се зачувани + YouTube: колачињата се избришани + YouTube: невалидни колачиња - ве молиме проверете и обидете се повторно + Допрете и задржете линија + Допрете линија за да скокнете до тој момент во песната, задржете ја за да го копирате зборот под вашиот прст, или целата линија кога притисокот не е на збор. Исклучувањето ги отстранува двете гестури + Прикажи ознаки за секции + Означи ја секоја нова секција од текстот со мала ознака, како што е Строфа или Рефрен + Прикажи копче за копирање + Допрете за да ги копирате оригиналните текстови како обичен текст; задржете за да ја зачувате изворната датотека на текстот во Downloads + Прикажи копче за преведување + Допрете за да го смените претставот на преводот; задржете само за превод. Користи преводи од провајдерот, Google Translate или OpenAI-компатибилен API + Прикажи копче за романизација + Допрете за да ја смените романизацијата; задржете само за романизација. Користи романизација од провајдерот, Google Translate или OpenAI-компатибилен API + Локална романизација и усогласување + Романизирај на уредот кога провајдерот, вашиот модел и Google Translate не успеат. Читањето го следи секој збор од синхронизираниот текст; синхронизацијата на зборовите мора да биде вклучена. Кинески, кантонски, јапонски, корејски, грчки, руски, грузински, ерменски; околу 120 MB + Читање на %1$s: %2$d%% + Преземањето не успеа - исклучете го и повторно вклучете го ова за да се обидете повторно + Прикажи копче за освежување + Допрете за да преминете на следниот кандидат за текст; задржете за пребарување текст според насловот на песната и изведувачот + Пребарај текст на песни + Наслов на песната + Уметник + Внесете наслов на песна и уметник + Ниту еден од овозможените даватели не може да пребарува други песни + Зачувано + Истакни синхронизирани зборови или слогови + Дај приоритет на текстот збор-по-збор кога е достапен. Кога текстот има временски ознаки за зборовите, се истакнува секој збор или слог во синхронизација со музиката + Скриј ги препеаните стихови + Скриј ги стиховите надlineTo ја currently пуштаната линија + Скриј ги непропеаните стихови + Скриј ги стиховите подlineTo ја currently пуштаната линија + Прикажи MediaSession стихови на заклучен екран + Го прикажува тековниот ред на заклучувачкиот екран, Android Auto и Bluetooth. Потребен е Android 11 или понов + Прикажи стихови во мини-плеер + Го прикажува тековниот ред во мини-плејорот, при што траката е преместена во поднасловот + Формат на наслов на песна/изведувач + Формат на траката прикажана со текстот во Miniplayer-от и MediaSession. {t} е насловот, {a} е изведувачот, а {v} е верзијата, како на пример Acoustic, кога изворот на текстот обезбедува таква. Текстот помеѓу две две точки се прикажува само кога секоја променлива во него има вредност; напишете \\: за обична две точки. Секоја линија се прикажува по ред, менувајќи се whenever текстот се менува + Формат на линија од текстот + Формат на линијата од текстот во Miniplayer-от и MediaSession. {lrc} е линијата, а {n} е колку пати по ред се пее. Текстот помеѓу две точки се прикажува само кога линијата се повторува. Користете \\: за две точки + Формато мора да го содржи {lrc} + Шаблон за линија на изведувачот и време на анимација + Минимално време на екранот (ms) + Максимално време на екранот (ms) + Само за анимации со повеќе линии. Задржете го секој кадар најмалку за минималното време. На максимумот, напредувајте со линијата на изведувачот дури и кога текстот останува ист. + Минималното мора да биде 0 или повеќе. Максималното мора да биде поголемо од 0 и најмалку колку што е минималното. + Лимити за должина на линијата + Лимити за линијата на текстот и изведувачот, и тежина на карактери со полна ширина + "Лимит за линијата на текстот +Поделете ги подолгите текстови на балансирани половини. 0 го исклучува делењето." + "Лимит за линијата на изведувачот +Поделете го пополнетиот формат на траката во циклички рамки. 0 го исклучува делењето." + "Тежина на карактери со полна ширина +Се применува на двата лимити. На 1.75, четири карактери со полна ширина се сметаат како 7." + Внесете цели броеви за лимитите од 0 до 10000 и тежина на карактерот поголема од 0 + Инструментална пауза + Се прикажува во миниплеерот и MediaSession за време на инструментална пауза. Секоја линија е еден кадар на анимацијата. {t} е насловот, {a} е изведувачот, а {lrc} е последната линија од текстот + Временско поместување за миниплеерот и MediaSession + Поместува кога следниот ред се прикажува во минимизираниот плејер и MediaSession. Користете негативна вредност ако редовите се појавуваат доцна, на пример на заклучениот екран + Филтри + Метаподатоци regex + Регуларен израз применен на насловите, изведувачите и албумите пред пребарувањето на текстовите и при оценувањето дали два наслови се совпаѓаат. Оставете празно за да ги оневозможите двете. Стандардната опција ги отстранува вообичаените дополнителни информации како \"Official Video\", \"Remaster\", \"【插曲】\" и \"- Topic\" + Филтер за стихови regex + Регуларен израз применен само врз оригиналниот текст на песната (не врз преводите или романизацијата). Оставете празно за да го оневозможите + Маркери за линија со информации + Ако текстот на песната е неправилно филтриран на дното, прилагодете го или избришете го ова. Маркери одделни со запирка: линиите што содржат маркер по кој следи две точки се прикажуваат како информации за авторите на дното. Оставете празно за да го оневозможите + Напиши ја првата буква со голема + Ја пишува првата буква од секој ред од текстот со голема + Отстрани завршна интерпункција + Отстранува точки, запирки, две точки и точка-запирки на крајот од стиховите. Прашалниците, извичниците, трите точки, наводниците и заградите се зачувуваат + Големина на текстот + Големина на текстот на песните + Глобално одстапување на времето + Синхронизираниот текст ги следи песните. Ако се одвојат, прилагодете го временскиот поместувач. Поместете го синхронизираниот текст напред или назад. Користете позитивна вредност ако текстот оди пред песната + Користете сегменти од SponsorBlock за одстапување на текстот + Поклопи ги без сегментите што SponsorBlock ги прескокнува и помести го текстот на објавената аудио запис. Исклученото целосно го игнорира SponsorBlock + Оваа трака нема видео ID за поднесување + Извор: %s + Долго притиснете го копчето за копирање за да го зачувате овој LRC; доколку можете да ги прилагодите времињата и да ги споделите со заедничките извори, бивме благодарни + Преведено од Google + Романизирано од Google + Преведено од %s + Романизирано од %s + Романизирано од %s + Преведено + Јазик за превод + Јазик за романизација + Преведување… + Не успеа да се преведат текстовите + Романизирано Автоматски + Романизирање… + Не успеа романизацијата на текстот + Само преведено + Само романизирано + Двоен режим + Копирано во меморијата + Освежување + Освежено + QuickLRC API клуч + QuickLRC ги транскрибира или ги усогласува по време текстовите за тековната нутра. Посебна услуга, неповрзана со OpenAI компатибилниот краен точка погоре + Клучот за QuickLRC е зачуван. Користете го копчето за порамнување во дијалогот за пребарување текстови, или наведењето во панелот кога траката нема текст + QuickLRC API клучот е зачуван + QuickLRC не го прифати тој клуч + QuickLRC API клучот е избришан + QuickLRC додава времиња на текстовите што немаат такви или ги транскрибира од траката, за одредена цена во кредити. Со поставен клуч, траките без текст нудат транскрипција, а дијалогот за пребарување добива копче за порамнување + Побарај QuickLRC усогласување + Усогласување + Примени усогласување + Побарај QuickLRC транскрипција + Транскрибирање + Транскрипцијата е откажана + Можат да се транскрибираат само песничките пуштени од YouTube + Нема текст за порамнување - наместо тоа се врши транскрипција + QuickLRC не може да ја обработи оваа песна + Кредитите на QuickLRC се исцрпени за оваа песна + "Текстовите се совпаѓаат според насловот и изведувачот; необичните метаподатоци може да не најдат ништо или погрешна верзија. Користете го освежувањето за промена на верзиите. Давателите означени со * бараат токен. Сите овозможени даватели не се секогаш бараат според вашиот редослед, туку одговараат врз основа на нивната брзина.\n\nТекстовите доаѓаат од услуги од трети страни кои не се поврзани со Morphe, не се спонзорирани или одобрени од Morphe. Секоја од нив се користи таква каква што е: текстовите, нивната достапност и нивното лиценцирање остануваат кај тие услуги и нивните лиценциратели, а нивното користење може да подлежи на нивните услови за користење. Она што стигнува до овозможената услуга е само пребарувањето што го побаравте - насловот, изведувачот и, таму каде што таа услуга го бара, видео ИД-то. Опционалниот AI крајен точка и QuickLRC транскрипцијата се извршуваат само откако ќе ги конфигурирате, и примаат го текстот на песната или видео URL-тата на траката." + Овозможете ги само провајдерите што ги користите или задржете ги стандардните - постојаните барања можат да ги преоптоварат малите заеднички сајтови за текстови + Скриј ја линијата за заслуги + Скриј ја линијата за заслуги прикажана под текстот + Замени го преводот и романизацијата + ВКЛ ја става романизацијата под текстот и преводот над; ИСКЛ ја држи романизацијата над и преводот под + Превод + Романизација + Провајдер за превод + Google (Стандардно, Бесплатно) + Компатибилно со OpenAI (Прилагодено) + Доставувач на романизација + Google (Стандардно) + Локален / Офлајн речник + Компатибилно со OpenAI (Персонализирано) + AI секција + Се користи само кога давачот не испраќа ознаки за секции како Рефрен или Строфа, кои потоа ги детектира вашиот модел компатибилен со OpenAI + AI & Cloud крајни точки + Конфигурирајте API клучеви, модели, крајни точки и шаблони за прашања за AI дагувачи. + URL на API крајната точка (на пр. https://api.openai.com/v1/chat/completions) + Шаблон за промпт + Заменители: {language}, {task}, {title}, {artist}, {count}, {lines} + Налогот е зачуван + Наведењето е зачувано, но AI крајната точка сè уште не е употреблива. Прво конфигурирајте ја во AI поставките + Налогот е ресетиран + Невалиден JSON. Поправете го или исчистете го полето пред зачувување + Подесувањата за превод и & романизација се зачувани + Подесувањата се ресетирани на стандардните + Основната URL адреса мора да започнува со http:// или https:// + Тестирај ја конекцијата + Конфигурирајте крајни точки + Прикажи ги сите крајни точки + Валидирање на конекцијата… + Дагувач за превод + Дагувач за романизација + Преземи модели + Преземање модели… + Изберете модел + Не успеа преземањето на моделите. Проверете го API клучот или конекцијата. + Поставки за Gemini API + Компатибилно со OpenAI (Custom Endpoint) + Поставки за DeepL API + API клуч + Модел + Основен URL + API клуч (опционално) + Нарачка за превод + Дополнително тело на барањето (опционален JSON) + Клуч за автентикација (Бесплатен или Pro) + Конфигурирајте API клучеви, модели, крајни точки и промптовен текст + 8.23.51 - Оневозможи задебелени икони + 9.13.51 - Врати ги табовите Следувачки, Текстови и Поврзано во плејачот diff --git a/patches/src/main/resources/addresources/values-mk-rMK/reddit/strings.xml b/patches/src/main/resources/addresources/values-mk-rMK/reddit/strings.xml index d634ae01c8..ad3166ee2d 100644 --- a/patches/src/main/resources/addresources/values-mk-rMK/reddit/strings.xml +++ b/patches/src/main/resources/addresources/values-mk-rMK/reddit/strings.xml @@ -16,6 +16,8 @@ Second \"item\" text" --> + Политика за приватност на Reddit + Најавувањето преку Google не е достапно. Користете е-пошта за најавување Огласи Скриј реклами во feed Скриј реклами во коментари @@ -30,8 +32,18 @@ Second \"item\" text" Скриј полица „За“ Скриј полица „Ресурси“ Изглед + Прилагоден фонт + Вчитувајте прилагодена TTF или OTF датотека со фонт од патеката подолу. Активирањето на оваа опција исто така го исклучува Присили системен фонт + Датотека со прилагоден фонт + Допрете за да изберете TTF или OTF датотека со фонт + Избран фонт - %s + Прво изберете датотека со прилагоден фонт + Ве молиме изберете .ttf или .otf датотека со фонт + Не успеа вчитувањето на прилагодениот фонт. Изберете друга датотека Исклучи искачувачки прозорец за screenshots Го исклучува искачувачкиот прозорец што се појавува кога се прави screenshot + Присили системен фонт + Користете го системниот фонт на уредот наместо Reddit Sans или Roboto. Активирањето на оваа опција исто така го исклучува Прилагодениот фонт Скриј копче „Прашај“ Го крие копчето „Прашај“ во лентата за пребарување Уклони дијалог за предлози за известувања diff --git a/patches/src/main/resources/addresources/values-mk-rMK/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-mk-rMK/shared-youtube/strings.xml index 9f4fa20d1f..912ce5faf4 100644 --- a/patches/src/main/resources/addresources/values-mk-rMK/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-mk-rMK/shared-youtube/strings.xml @@ -18,16 +18,51 @@ Second \"item\" text" --> + Анимација при стартување Оневозможено Боја + Монохромски + + Исклучено Рестартирај за да завршиш со поставување Име на апликацијата + Позадина на темна тема + Чиста црна + Material You (Чиста црна) + Класичен YouTube + Темно розова + Темно сина + Темнозелена + Темножолта + Темнопортокалова + Темноцрвена + Прилагодена темна позадина + Поставете ја бојата на позадината за темната тема + Стандардно за апликацијата + Material You (Неутрална) + Material You (Примарна) + Material You (Секундарна) + Material You (Третијарна) По нарачка + Преземања + Режим на преземање Стандардно + Преземи во апликацијата + Насловен преземач + Име на пакетот на преземачот + Име на пакетот на вашата инсталирана надворешна апликација за преземање + Внесете го името на пакетот + Друго + Апликацијата не е инсталирана + %s не е инсталиран. Ве молиме инсталирајте го + "Не може да се најде инсталирана апликација со име на пакет: %s + +Проверете дали името на пакетот е точно и дали апликацијата е инсталирана" + Името на пакетот не може да биде празно Прилагодено Икона на апликацијата @@ -41,11 +76,21 @@ Second \"item\" text" Morphe play црно Прилагодено + "Ова ја менува иконата прикажана во вашиот помагач за апликации. + +Оваа поставка нема да ја промени иконата на ниту една од овие локации: +• Листа со апликации во системските поставки на уредот +• Одредени push известувања +• Бирач на апликации „Отвори со“ +• Икона за Android Auto + +За да ја промените иконата насекаде, примените patch повторно и променете ја опцијата „Икона на апликација“" Икона за известување Усогласување со икона на апликацијата Morphe play Известување на Morphe + Вашата историја на гледање не се зачувува.<br><br>Ова најверојатно е предизвикано од DNS блокирач на реклами или мрежен прокси.<br><br>За да го поправите ова, додајте го <b>s.youtube.com</b> на белата листа или исклучете ги сите DNS блокирачи и прокси-сервери Не покажувај повторно Лажирај видео стримови @@ -55,6 +100,12 @@ Second \"item\" text" Ако сте корисник на YouTube Premium, можеби нема да ви треба оваа поставка." Не може да се преземат клиенти за стриминг + Стандардниот клиент не работи + "%1$s не работеше неколку пати по ред, но %2$s работеше. + +Дали да го поставите %2$s како стандардни клиент?" + Префрли + Исклучувањето на оваа поставка може да предизвика проблеми со репродукцијата Обидете се да го овозможите: Лажирај видео стримови > Пријава во Android VR @@ -89,12 +140,26 @@ Second \"item\" text" • Ќе треба повторно да се најавите ако ја промените вашата Google лозинка или го откажете овој уред од вашите поставки на Google." OAuth2 е привремено недостапен (истекот на рокот на API) Истечен пријава во Android VR, обидете се да се најавите во VR повторно + OAuth2 е привремено недоступен (статус %s) OAuth2 е привремено недостапен Не може да се најавите во VR, одобрувањето на веб-страницата не е одобрено Не може да се најавите во VR: %s Одјавени од Android VR Невалиден токен за пријава во VR, обидете се да се најавите во VR повторно + Контроли за медиуми во известувањата + Прилагодете ги контролите за воспроизведување прикажани на вашиот заклучен екран и панелот за известувања + Скриј ги копчињата Претходно & Следно + Ги крие копчињата за Претходно и Следно во медиумското известување, заклучениот екран и контролите на слушалките + Оневозможи ја лентата за премотување + Спречува влечење на лентата за премотување на заклучениот екран и панелот за известувања + Пребарувај со копчињата Претходно & Следно + Ги менува контролите за медиумско известување, заклучен екран и слушалки за да пребаруваат наместо да менуваат видеа + Должина на навраќање + Секунди за навраќање со копчето Претходно + Должина на забрзување + Секунди за забрзување со копчето Следно + Должината на пребарувањето мора да биде помеѓу 1 и %s секунди. Се користи стандардната вредност Исклучи QUIC протокол @@ -103,11 +168,34 @@ Second \"item\" text" Принудно оригинално аудио Секогаш пуштај оригинален аудио трак за видео наместо добликан верзија + Прескокни тишина + Автоматски ги прескокнува тивките паузи во аудиото на видеото + Минимална должина на паузата + Само паузите со најмалку оваа должина се скратуваат. Стапува на сила кога ќе започне следното видео + Мрежен прокси + Пренасочете ги поддржаните мрежни барања преку прокси + Овозможи прокси + Ги пренасочува поддржаните мрежни барања преку конфигуриран HTTP или HTTPS прокси + Хост на прокси + Име на хост или IP адреса на прокси серверот + Прокси порта + Порта на прокси серверот + Користи HTTPS прокси + Користете HTTPS за поврзување со прокси серверот наместо HTTP + Прокси автентикација + Испраќа Basic автентикација со прокси барањата + Корисничко име за прокси + Корисничко име за Basic прокси автентикација + Лозинка за прокси + Лозинка за Basic прокси автентикација + Дозволи директно враќање + Поврзи се директно ако проксито не успее Скриј реклами со целоцен екран + За да ја користите оваа функција, променете го \"Spoof video streams\" во кој било клиент освен \"Android Studio\" и \"Android VR Downgraded\" Дезинфицирај линкови за споделување Отстранува параметри за отследување од линковите за споделување. @@ -116,22 +204,112 @@ Second \"item\" text" Промени линковите за споделување на youtu.be Принудува видеата да го користат скратениот формат на URL youtu.be + Протокол пуфер за дебаг лог Додава дополнителни информации за евидентира ње за програмери за да ги идентификуваат компонентите на апликацијата + "Активирањето на оваа поставка ќе запише дополнителни податоци за распоредот, вклучително и текст на екранот за некои UI компоненти. + +Ова може да помогне во идентификувањето на компонентите при креирање на прилагодени филтри. + +Сепак, активирањето на ова ќе запише и некои кориснички податоци, како на пример вашата IP адреса" + Прилагоден филтер + Конфигурирајте прилагодени филтри за да skjкриете која било компонента + Овозможи прилагоден филтер + Листа на низоци за градење на патеката до компонентата за филтрирање, одделене со нови редови + Невалиден прилагоден филтер: %s + Филтер за менито за подесувања + Криете ставки на стандардниот екран за подесувања на апликацијата според нивното видливо име + Отворете го стандардниот екран за подесувања на апликацијата еднаш за да се пополни листата + Отстрани го дијалогот за дискреција на гледачот + Ова не го заобиколува ограничувањето за возраст. Само автоматски го прифаќа + PoToken провајдер + Конфигурирајте го PoToken провајдерот за да спречите проблеми со репродукцијата + Набавно PoToken обезбедувач + "Поправа проблеми со репродукцијата користејќи набавен PoToken minter (Експериментално) + +Ако сте корисник на YouTube Premium, ова поставување може да не биде потребно" + "Зачувува проблеми со репродукцијата со користење на надворешен PoToken minter (Експериментално) + +За да ја користите оваа функција, исклучете ја опцијата 'Лажирај видео стримови'" + Ако не работи, обидете се присилно да го затворите MicroG + Отвори ги поставувањата на PotHelper + "Оваа функција моментално е во развој и можеби нема да работи + +Допрете овде за да го преземете PotHelper" + PotHelper повеќе не работи + "Токенот вграден во оваа верзија на PotHelper повеќе не работи + +Вклучете го 'Лажирај видео стримови' за да го поправите репродуцирањето" + Вклучи + Лажирај ја верзијата на апликацијата + Го замамува серверот да мисли дека користите постара верзија на апликацијата за да ги врати застарените функции на корисничкиот интерфејс + "Верзијата на апликацијата ќе биде имитирана како постара верзија. + +Ова ќе го промени изгледот и функциите на апликацијата, но може да се појават непознати странични ефекти. + +Ако подоцна се исклучи, се препорачува да се избришат податоците на апликацијата за да се спречат грешки во корисничкиот интерфејс" + Замамување на целната верзија на апликацијата + Стапка на освежување на апликацијата Стандардно + %s FPS + Замена на стапката на освежување + Секогаш + Репродукција во портрет + Репродукција во цел екран + Воспроизведување во портретен режим и цел екран + Фонт Стандардно + Системски фонт на уредот + Датотека со сопствен фонт + Датотека со сопствен фонт + Изберете TTF или OTF датотека со фонт + Избраната датотека не е TTF или OTF фонт + Прилагодениот фонт не можеше да се вчита и е исклучен + Непадувањата привремено не се достапни (истечен тајмер на API) + Несимпатиите не се достапни (статус %s) + Непадувањата не се достапни (лимит на клиентскиот API) + Нелајковите не се достапни (%s) + Скриено од сопственикот + Не ви се допаѓа ова видео, заедно со %s други луѓе + Не ви се допаѓа ова видео, %s од гласовите се негативни + Овозможи Return YouTube Dislike + Прикажи нелајкови како процент + Прикажува нелајкови како процент од лајковите наместо како број + Прикажи процени за лајкови + Прикажува процена на бројот на лајкови кај видеата каде што креаторот ги оневозможил лајковите + Прикажи известувања за грешка при поврзување + Прикажува известување кога серверот Return YouTube Dislike не е достапен + "Return YouTube Dislike ги враќа бројниците на несимпатии што YouTube ги сокри во 2021 година. Бројките доаѓаат од податоци архивирани пред removing-то и се проценуваат за поновите видеа врз основа на гласата на неговите корисници, па затоа се приближни и се ажурираат на неколку дена + +ID-то на видеото што го гледате се испраќа до Return YouTube Dislike API-то, а вашите симпатии и несимпатии се испраќаат анонимно за да се подобрат проценките" + Статистика на Return YouTube Dislike API за овој уред + Просечно време на одговор на API + Минимално време на одговор на API + Максимално време на одговор на API + Време на одговор на API, последна медија + Неподобрувањата привремено не се достапни - во сила е ограничувањето на стапката на Client API + API преземање гласови, број на повици + Не се направиле мрежни повици + Направени се %s мрежни повици + API преземање гласови, број на прекини + Нема мрежни повици со истек на време + %s мрежни повици истекоа + Ограничувања на стапката на API клиентот + Не се сретнати ограничувања на стапката на клиентот + Ограничувањето на стапката на клиентот беше сретнато %s пати + %s милисекунди diff --git a/patches/src/main/resources/addresources/values-mk-rMK/shared/strings.xml b/patches/src/main/resources/addresources/values-mk-rMK/shared/strings.xml index e6bcb4b44f..c016487ac3 100644 --- a/patches/src/main/resources/addresources/values-mk-rMK/shared/strings.xml +++ b/patches/src/main/resources/addresources/values-mk-rMK/shared/strings.xml @@ -24,18 +24,22 @@ Second \"item\" text" Врати ја бојата Невалидна боја Рестартирање е потребно + Рестартирајте ја апликацијата за оваа промена да стапи на сила Рестартирај Импортирај Копирај Паденијата на Morphe се вратени на нивните вредности по дефолт + Импортирани се %s поставки Импортирањето не успеа: %s + Навигација нагоре Пребарување на паденија Филмови Не се пронајдени резултати за „%s“ Обидете се со друг клуч Неодамнешни пребарувања Дали да се отстрани од историјата на пребарувања? + Отстрани од историјата на пребарување Избриши ја историјата на пребарувања Дали сте сигурни дека сакате да ја избришете целата историја на пребарувања? Совет за пребарување @@ -47,9 +51,16 @@ Second \"item\" text" Нема пронајдено историја на пребарување За да ја зачувате историјата на пребарување, откучете пребарувачка прашање и притиснете Enter Прикажи историја на пребарувања на поставки + Име на поставките за Morphe Стандардно + Дополнителни + Прилагодувања + Време на морфирање По нарачка + Внесете име + Името не може да биде празно Прикажи икони за поставки Morphe + Копирано: %s Јазик на Morphe "Преводи за некои јазици може да им недостигаат или да бидат нецелосни. @@ -82,6 +93,7 @@ Second \"item\" text" 2022 – 2025 2019 – 2022 Известувања за лиценца + Вчитување… Дебугирање Конфигурирајте ги напредните опции за развивач и завршните зафати @@ -89,10 +101,14 @@ Second \"item\" text" Дебугирај го тракот од позиција во скок Вклучува завршни завршни завршни завршни завршни зафати за завршни зафати Прикажи тост при грешка во Morphe + "Исклучувањето на нотификациите за грешки ги крие сите нотификации за грешки од Morphe. + +Нема да бидете известени за никакви неочекувани настани" Извези ги завршните поруки за грешки Извези завршни поруки за грешки од Morphe Завршни поруки за грешки се извезони успешно Неуспешно извезување на завршни поруки за грешки + Пребарај дебаг записници… Обриши Дебугирањето е исклучено Не се пронајдени завршни поруки за грешки @@ -101,28 +117,93 @@ Second \"item\" text" Завршни поруки за грешки се обришани Менаџер за ознаки за функции Управление на булеан ознаки за функции + Пребарај ознаки за функции… + Неактивни (%s) + Активни (%s) + Блокирани (%s) + Присилени (%s) + Автоматски Исклучено Вклучено + Присилно вклучи + Блокирај Отстрани Ознаките за функции се зачувани Ознаките за функции се вратени Ознаките за функции се копирани во привремениот простор Рестартирај ја апликацијата за да ја користите оваа настройка + Извоз и увоз + Увезени се %s ознаки за функции + Не се пронајдени ознаки за функции + Ознаките за функции се извозени успешно + Неуспешен извоз на ознаките за функции + Заставоците за функции се вчитани, допрете Зачувај за да ги примените + Не успеа дрвењето на датотеката + Бинарна пребарувачка + "Наоѓа кој заставок за функција предизвикува одредено однесување на апликацијата. + +Половина од %s заставоците се блокирани и по секое рестартирање вие кажувате дали однесувањето е сè уште присутно. + +Репродуцирајте го однесувањето еднаш пред да започнете, во спротивно заставоците за кои апликацијата сè уште не побарала ќе недостасуваат од пребарувањето" Табела на водечки спонзори SponsorBlock + "Нема заставоци за функции за пребарување. + +Во бинарната пребарувачка можат да се користат само заставоци до кои апликацијата стигнала и кои моментално не се присилно вклучени" + "Чекор %1$s + +Остануваат %2$s заставоци, од кои %3$s се блокирани за овој чекор. + +Рестартирајте ја апликацијата, проверете дали однесувањето е присутно, па одговорете подолу" + Пребарувањето на заставоците за функции е во тек + Однесувањето е присутно + Однесувањето не е присутно + Откажи бинарно пребарување + Бинарното пребарување е откажано + "Знамто за функција %s го предизвикува однесувањето. + +Сега е блокирано" + "Не може да се најде знаме за функција. + +Изгледа дека однесувањето на апликацијата не е контролирано од знаме за функција" + "Не може да се најде ниту едно знаме за функција. + +Однесувањето исчезна порано во пребарувањето, па затоа можеби се вклучени повеќе од едно знаме или еден од одговорите бил погрешен" Отвори ги поставките на MicroG + MicroG не е инсталирано. Инсталирајте го Потребна е акција + "Вашата верзија на MicroG v%1s е застарена и може да предизвика проблеми. + +Ве молиме ажурирајте го MicroG на најновата верзија v%2s" + Ажурирај + Игнорирај + "Инсталираниот MicroG не е препорачан од Morphe: + +%1s + +Допрете за да го деинсталирате, потоа повторно отворете ја оваа апликација и инсталирајте MicroG RE" + Деинсталирај + "MicroG нема дозвола да работи во позадина. + +Следете го водичот \"Don't kill my app\" за вашиот телефон и применете ги упатствата за вашата инсталација на MicroG. + +Ова е потребно за работата на апликацијата" Отвори веб-страница + "Оптимизациите на батеријата за MicroG мора да се исклучат за да се спречат проблеми. + +Исклучувањето на оптимизациите на батеријата за MicroG нема негативно да влијае на користењето на батеријата. + +Допрете го копчето за продолжување и дозволете ги промените во оптимизацијата" Продолжи Прикажи го дијалогот за оптимизација на батеријата на MicroG "Исклучувањето на ова ќе го скрие повикувањето за оптимизација на батеријата на MicroG. @@ -149,4 +230,6 @@ Second \"item\" text" ⚠️ Сакам експериментално ✅ Сакам стабилно + Дневникот за во рок се зачуван во корисничките документи: %s + Дневникот за во рок е зачуван: %s diff --git a/patches/src/main/resources/addresources/values-mk-rMK/sponsorblock/strings.xml b/patches/src/main/resources/addresources/values-mk-rMK/sponsorblock/strings.xml index 6ed3620587..b59bff7154 100644 --- a/patches/src/main/resources/addresources/values-mk-rMK/sponsorblock/strings.xml +++ b/patches/src/main/resources/addresources/values-mk-rMK/sponsorblock/strings.xml @@ -16,6 +16,7 @@ Second \"item\" text" --> + Користи квадратни агли за копчињата и контролите на SponsorBlock Поставете ја времетраењето за кое копчето Skip ќе остане видливо Поставете го времетраењето за кое зачувувањето на известување ќе биде видливо 1 секунда @@ -163,6 +164,7 @@ Second \"item\" text" Телефон Статистиките се привремено недоступни (API е недолучен) + Вчитување… SponsorBlock е исклучен Корисничкото име: <b>%s</b> Допрете овде за да го промените вашето корисничко име diff --git a/patches/src/main/resources/addresources/values-mk-rMK/youtube/strings.xml b/patches/src/main/resources/addresources/values-mk-rMK/youtube/strings.xml index c0bc012951..85abb3038b 100644 --- a/patches/src/main/resources/addresources/values-mk-rMK/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-mk-rMK/youtube/strings.xml @@ -32,42 +32,73 @@ Second \"item\" text" Отстрани го оганичувањето за пуштање видеа во позадина Исклучи пуштање видеа во позадина + Автоматска пауза кога уредот е заклучен Исклучено + Секогаш паузирај кога е заклучено + Паузирај освен ако не е поврзан бежичен аудио уред + Оневозможи го ограничувањето на брзината на скролање Исклучи ги ажурирањата на изгледот Спречува дека серверот ќе го ажурира изгледот на апликацијата + "Изгледот на апликацијата се враќа на изгледот што се користел при првото инсталирање. + +Некои од изгледите од страната на серверот можеби нема да се вратат. + +Промените вклучуваат: +• Компонентите во искакувачкото мени на плеерот (или поврзаните поставки) можеби нема да работат +• Броењето на бројките не е анимирано +• Прикажана е картичката Библиотека +• Музичкиот дел од описот на видеото можеби нема да работи +• Елементите во искакувачкото мени на плеерот не можат да се сокријат +• Копчето за промена на сметката можеби нема да се појави на картичката Библиотека" Вклучи филтер за менито Flyout Филтер на менито Flyout Внесе ги имињата на менијата за плава папка за филтрирање, одделени со нови редови + Овозможи филтер за долното мени Библиотека / Вие + Библиотека / Вие филтер во долното мени + Внесете ги имињата на менито Библиотека / Вие во долниот дел за филтрирање, одвоени со нови редови Скриј ги албумските картички Прикриј ги картичките на албуми под описот на изведувачот Скриј ги картичките на изведувачите Прикриј ја картичката на изведувачот на врвот од резултатите од пребарувањето + Скриј ги копчињата за канали + Ги крие копчињата како Претплати се, Види го каналот, Заедница и YouTube Music од профилите на каналите во резултатите од пребарувањето Скриј ги објавите од заедницата Скриј ги компактните банери Прикриј компактни транспаренти и простор помеѓу некои компоненти Картички што можат да се прошират под видеа Прикажи ги сите + Скриј само ја картичката со резиме + Скриј сè Скриј го плукачкото микрофонско копче Прикриј копче за микрофон со лебдечко движење во долниот десен агол од екранот за предлози за пребарување + Скриј копче „Земи Premium“ + Скриј ја полицата за историја Скриј ги хоризонталните полици Ги крие хоризонталните полици како што се „Вести во живо“, објави од заедницата кои можеби ќе ви се допаднат, „Продолжување со гледање“, истражете повеќе канали, истражете повеќе теми, За вас, најнови видеа, најрелевантни, објави од заедницата на креаторот, шопинг и гледајте го повторно Скриј ја полицата со слики Прикриј полици со слики во резултатите од пребарувањето + Скриј ја картичката „Поканете други да праќаат пораки“ + Ги крие картичките за покана прикажани во делот Пораки во табулаторот Известувања Скриј копче \"Последни видеа\" Прикриј го синиот транспарент „Најнови видеа“ што се појавува на врвот од „Почетна“ + Скриј ги во живо емисиите + Ги крие видеата што се пренесуваат во живо во моментот во почетниот feed и резултатите од пребарувањето Скриј ги плејлистите со микс Скриј ја секцијата со филмови + Скриј ја заглавието на менито Известувања + Го крие заглавието на менито Известувања при кликање на копчето со ѕвонче Скриј го копчето „Извести ме“ + Го крие копчето за потсетник за закажаните во живо преноси и премиери Скриј ги „Плејлистови“ Прикриј функција која овозможува играње игри Прикриј минијатури за термини за пребарување @@ -77,22 +108,38 @@ Second \"item\" text" Скриј го копчето „Покажи повеќе“ Прикриј копче за проширување под видеа во резултатите од пребарувањето Скриј лента за претплатени канали + Крии ги имињата на каналите на кои сте претплатени + Ги крие имињата на каналите во лентата за претплати Скриј ги анкетите Ги крие анкетите и поттикнувањата за прецизноста на препораките за фид Скриј ја полицата за билети + + Скриј го банерот „Недостапните видеа се скриени“ + Го крие банерот „Недостапните видеа се скриени“ во листите за репродукција Скриј го времето на објавување + "Ограничувања: +• Полиците за Shorts, страниците на каналите и резултатите од пребарувањето сè уште може да ги прикажуваат времето на прикачување +• Не работи со автомобилски распоред" + Сокриј ја сливицата од видеото + Скриј ги ознаките на видеото + Ги крие ознаките како „Автоматски дублирано“ / „Дублирано“, „Заработува провизија“, „Хиперирано“, „Луѓето гледале и“, „производ(и)“ и „Можеби ќе ви се допадне и“ Скриј ја бројката на прегледи + "Ограничувања: +• Полиците за Shorts, страниците на каналите и резултатите од пребарувањето сè уште може да ги прикажуваат бројот на прегледи +• Не работи со автомобилски распоред" Скриј резултати од пребарување на интернет Ја крие секцијата „Резултат од интернет“ од резултатите од пребарувањето Скриј дел „Можеби ќе ти се допадне“ Скриј ги YouTube Doodle-ите Прикриј сезонски и настани-засновани анимирани слики на логотипот на YouTube + Прикажувањето на YouTube Doodles може да спречи прикажувањето на прилагодените заглавија Скриј ја лентата на канали Скриј го воден белег на каналот + Скриј ги поглавјата и копчето \'Во ова видео\' Скриј го кутијата за финансирање Скриј ги кутиите за итни случаи Скриј ги информативните панели @@ -105,20 +152,35 @@ Second \"item\" text" Скриј копче \"Репродукција на чат во живо\" Прикриј го копчето за репродукција на разговор во живо во полноекрански режим Скриј ги медицинските панели + Крии ги помошните напомени за гестови на плеерот + Ги крие напомените прикажани за време на гестовите на плеерот, како што се „Повлечи нагоре за прецизно пребарување“, „Пушти за да откажеш“ и „2x“ + Скриј го snackbar-от Скриј ги упатствата за претплатници + Скриј го копчето \'Синхронизирај со видео\' + Скриј го градиентот при допир и држење + Ги крие градиентите на контролите на плеерот при лизгање со прстот додека држите за забрзување Скриј ги реакциите во време Прикриј функција која овозможува на корисниците да реагираат со емотикони во делот со коментари и разговор во живо + Скриј го менито „Помош и повратни информации“ + Го крие менито „Помош и повратни информации“ при користење на копчето со три точки, како на резултатите од пребарувањето + Крии ја рачката + Ја крие рачката на вашиот канал во табот „Вие“ Скриј наслов на видео Прикриј го насловот на видеото во полноекрански режим + Прочисти ги преводите на видеото + Ги чисти фразите како „#“, „Фонд за собирање средства“, „производ(и)“ и „Продавница“ од поднасловот на видеото + Скриј го копчето „Прашај Gemini“ + Го крие копчето „Прашај Gemini“ на дното на панелот „Прашај за ова видео“ Опис на видео Изберете ги компонентите кои ќе се појавуваат во описот на видеото Прикриј дел „AI-генерирано резиме на видео“ Прикриј дел „Прашај“ Прикриј дел „Атрибути“ Прикриј ги „Преферираните места“, „Игри“, „Музика“ и „Споменати луѓе“ + Скриј дел со линкови до канали Прикриј дел „Глави“ Прикриј дел „Корекции“ Прикриј дел „Напредок во курсот“ @@ -128,14 +190,26 @@ Second \"item\" text" Прикриј дел „Истражувај го подкастот“ Скриј ја секцијата со препорачани канали Прикриј дел „Препорачани линкови“ + Скриј ја секцијата за истакнати плејлисти Прикриј дел „Препорачани видеа“ + Скриј ја секцијата за хаштагови + Сокриј го делот „Како е направено ова“ Скриј ги поените за поттик Прикриј дел „Информациски картички“ + Скриј го делот \'Клучни концепти\' Прикриј дел „Квизови“ + Скриј ја секцијата \"Пребарај во ова видео\" Скриј копче за претплатување Прикриј дел „Транскрипт“ Прикриј дел „Детали за видеото“ Трка од филтри + Изберете каде да се сокрие лентата за филтрирање со скролање + Скриј на страницата на каналот + Скриј во коментарите + Скриј во фидовите + Скриј во поврзаните видеа + Скриј во резултатите од пребарувањето + Скриј во историјата на гледање Страница на канал Изберете ги компонентите кои ќе се појавуваат на страницата со канал Вклучи филтер за картичката Канал @@ -147,6 +221,8 @@ Second \"item\" text" Скриј копче за приклучување Скриј преглед на врски Прикри ги кликаблите веб-врски во горниот дел од каналот + Скриј го знакот „Само за членови“ + Го крие знакот „Само за членови“ во лентата за филтрирање на каналот Скриј полица за членови Прикри ги членовите на каналот Прикриј полица со објави @@ -154,29 +230,91 @@ Second \"item\" text" Скриј копче за продавница Скриј копче за претплатување + Мени за истражување + Изберете кои компоненти да се појавуваат во менито за истражување + Скриј го менито Курсеви + Скриј го копчето за истражување + Скриј го менито \'Мода & убавина\' + Скриј го менито за гејминг + Скриј го менито Hype + Скриј мени за учење + Скриј го менито Live + Скриј го менито за членства + Скриј го менито „Movies & TV“ + Скриј го менито Music + Скриј го менито за вести + Скриј го менито Playables + Скриј го менито за подкасти + Скриј го менито за купување + Скриј го менито за спорт + Скриј го менито за YouTube Premium + Скриј го менито за YouTube Studio + Скриј го менито на YouTube Music + Скриј го менито на YouTube Kids + Скриј го менито на YouTube Create + Скриј го менито „Како работи YouTube“ + Скриј ги правните и приватност футерите Коментари Изберете ги компонентите што ќе се прикријат со помош на прилагодени филтри Вклучете филтер за менито со коментари + "• Погрижете се да го внесете точното име на ставката од каруселот (на пр. Ask, Hype this video, Listen on YouTube Music) +• Криењето ставки од каруселот може да има ненамерни странични ефекти +• Ако сите ставки од каруселот се скриени, целиот дел за коментари исто така ќе биде скриен" Филтер за менито со коментари Внесете ги имињата на каруселот со коментари за филтрирање, одделени со нови редови Скриј сумирање на AI разговор Скриј ги правилата за канали + Скриј пораки за контекст Скриј ги насловите „Коментари од членови“ Скриј дел со коментари Скриј дел за коментари во Feed + Скриј насоки за заедницата Скриј го копчето „Креирај Shorts“ + Скриј копче за несимпатија + Скриј копче за емотикони + Скриј ги опциите на лентата за филтрирање + Ги крие сите опции на лентата за филтрирање освен „Најдобри“ и „Најнови“ + Скриј ја анимацијата за подароци и картичките + Скриј го копчето за подарок Скриј копче за информации + Скриј ја лентата за донатори во живиот чет + Скриј го копчето за скролање до дното на живиот чет + Скриј ги алартните пораки на живиот чет + Ги крие искакувачките алартни пораки прикажани до лентата за внесување во четот, како што е известувањето за чет само за претплатници + Скриј копче за мени + Ги крие копчињата за мени со 3 точки од секој коментар Скриј коментар за преглед Прикри го прегледот на коментари пред отворање на целиот дел со коментари + Коментарот е скриен од Morphe… + Овозможи минималистичко копче за коментари + Ги крие лизгачките картички и нивниот селектор на страници, прикажани на копчето за коментари Скриј го копчето „Благодарам“ + Скриј копче за временски печат + Скриј копче „Најдобри обожаватели“ + Скриј копче за превод + Скриј ја пораката \"Сè уште нема коментари\" + Исчисти ги истакнатите линкови за пребарување + Ги чисти истакнатите линкови за пребарување, оставајќи го само оригиналниот обичен текст Филтер за клучни зборови Изберете специфични зборови или фрази за да скриете видеа во feed и пребарување + Скриј коментари според клучни зборови Скриј видеа во почетна страница со клучни зборови Скриј резултати од пребарување со клучни зборови Скриј видеа од претплатите со клучни зборови Клучни зборови за скривање + "Внесете клучни зборови и фрази за криење, одделени со нови редови + +Клучните зборови можат да бидат било каков текст прикажан во насловите на видеата или имињата на каналите + +Зборовите со големи букви во средината мора точно да се совпаѓаат (на пр. iPhone, TikTok, LeBlanc)" + "Почетна / Претплати / Пребарување / Коментарите се филтрирани за да се сокрие содржината што одговара на клучните фрази + +Ограничувања: +• Shorts не можат да се сокријат според името на каналот +• Некои UI компоненти можеби нема да се сокријат +• Пребарувањето за клучна збор може да не покаже резултати" Оптимизација на целосно зборот Окружувањето на клучна реч/фраза со двојни наводници ќе спречи делумни совпаѓања на наслови на видеа и имиња на канали<br><br>На пример,<br><b>\"ai\"</b> ќе го скрие видеото: <b>Како работи AI?</b><br>но нема да го скрие: <b>Што значи фер употреба?</b> @@ -186,12 +324,73 @@ Second \"item\" text" Клучната реч има конфликтни декларации: %s Клучната реч е премногу кратка и бара наводници: %s Клучната реч ќе скрие сите видеа: %s + "• Почетна: %s +• Претплати: %s +• Пребарување: %s +• Коментари: %s +• Вкупно: %s" Телефон + Скриени видеа, последните 24 часа + Скриени видеа, за секое време + Ресетирај го броителот за последните 24 часа? + Ресетирај го броителот за секое време? + AI филтер за канали + Скриј видеа од канали означени како AI-генерирана содржина од листата на заедницата AiSList + Црна листа: Висока доверба + Листа за предупредување: Умерена доверба + Скриј видеа на Почетна + Скриј видеа во пребарувањето + "• Почетна: %s +• Пребарување: %s +• Вкупно: %s" + Канали на листа за блокирање + Канали на листа за предупредување + Овозможете филтер за вчитување на каналите + Вчитување канали… + "Почетната страница и резултатите од пребарувањето се филтрирани за да се сокријат видеата од каналите означени од AiSList + +Ограничувања: +• Филтрираните AI Shorts може да се прикажат како празен простор +• Некои AI видеа сè уште може да се појават" + "AiSList е листа со отворен код на YouTube канали кои објавуваат содржини генерирани од ВИ, одржувана од волонтери. Каналите се пријавуваат од корисниците и се прегледуваат пред да се додадат + +Листата се презема од GitHub, а видеата се филтрираат на вашиот уред" + Пријави AI канали + Додај ставка за пријавување во видео менито + Додај ставка во видео менито која го пријавува каналот на видеото до AiSList + Име на пријавувачот + Име прикажано покрај вашите извештаи на јавната AiSList табела. Пријавувањето е недостапно додека ова е празно + Пријави AI канал + "Испрати го %s до AiSList, користејќи го ова видео како доказ? + +Пријавувајте само канали што: +• Користат AI гласови, визуелни ефекти, сценарија или музика како нивна главна содржина +• Масовно произведуваат содржина со AI алатки и малку човечка работа + +Не пријавувајте канали кои само дискутираат за AI или користат AI за одредени делови од нивните видеа. + +Извештаите се јавни и се прегледуваат пред каналот да се додаде на листата. Отстранувањето е возможно само преку барање на GitHub" + Овој канал веќе се нагаја на AiSList листата за блокирање + Овој канал веќе е на листата за предупредување на AiSList + Пријави + Поставете име на пријавувач во поставките за филтер на AI канали за да пријавувате канали + Пребарување на каналот… + Не може да се најде каналот на ова видео + Каналот е пријавен и се чека преглед + AiSList не може да ја зачува пријавата, обидете се повторно подоцна + Испратени се премногу пријави, обидете се повторно подоцна + AiSList го одби го известувањето + Известувањето не успеа, проверете ја вашата конекција + Бела листа на канали за реклами + Видеата од каналите на белата листа се репродуцираат со реклами, за нивните креатори да бидат платени. Рекламите остануваат блокирани на сите останати канали + Додај канал во белата листа за реклами + Отстрани канал од белата листа за реклами Скриј полица со продажна продавница Скриј банер од продажна продавница на крајот од екранот Скриј општи реклами Скриј рекламни банери за стока + Скриј ознаки за платена промоција Скриј реклами во прозорецот на плеер Скриј картички за само-спонзорирани реклами Скриј линкови за купување @@ -199,35 +398,167 @@ Second \"item\" text" Скриј промоции за YouTube Premium + Вчитување… + Тековен канал: + Додај во белата листа Отстрани + Бела листа на канали + Нема канали на белата листа + Каналот е додаден на белата листа + Каналот е отстранет од белата листа + Пребарај во каналот + Додава второ копче за пребарување на страниците на каналите за пребарување само во тој канал + Пребарај канал + Резултати од пребарувањето на канали + Ништо не е пронајдено во овој канал + Неуспешно пребарување во овој канал + Видео + Објави + Нема објави на овој канал што одговараат + Релевантност + Најмногу гледано + Најмалку гледано + Најново + Најстаро + Најкратко + Најдолги + Линкот е копиран во Through clipboard + Линкот со временски печат е копиран во Through clipboard + Прикажи копче за копирање врска до видеото + Допрете за да ја копирате врската до видеото, допрете и држете за да копирате со временски стамп Копирајте го временскиот печат стандардно + Ги заменува акциите при допир и допир-и-држење на копчето \'Копирај врска до видеото\' + Копирај го насловот на видеото + Допрете и држете ја насловот на видеото за да го копирате + Насловот на видеото е копиран во Clipboard + Копирај коментари + Допрете и држете некој коментар за да го копирате + Коментарот е копиран во работната област + Допрете и држете за да го отворите менито за видео + Допирот и држењето било каде на видеото во фидото го отвора неговото мени + Прикажи го копчето „Гледај подоцна“ на сликите во минијатура + Копчето „Гледај подоцна“ се прикажува на сликите од видеата во фидот. Долго притискање го отвора „Зачувај во плејлиста“ + Преземај слики од објави + Копче за преземање се прикажува покрај копчето за споделување на објавите со слики или GIF-ови, а ставка за преземање е додадена во нивното мени + Преземи слики + Преземи слика + Преземање на %s слики… + Преземање на слика… + %1$s слики се зачувани во %2$s + Сликата е зачувана во %s + Не може да се преземе сликата + Во оваа објава нема пронајдено слики + Додај ставка „Прашај“ во менито + Додава ставка за резиме на видео во менито на фидот + "За „Прашај“ првично е потребен најавен профил во земја каде што „Прашај“ е достапно. + +Откако еднаш ќе се отвори видео со копчето „Прашај“, функцијата работи со сите видеа, дури и ако во моменталната земја не е достапна функцијата „Прашај“." + Направи резиме на видеото + Не може да се отвори „Прашај“ за ова видео + Прашај не е достапно за вашата сметка или моменталната држава + Прикажи број на прегледи на сликите-минијатури + Бројот на прегледи на видеата во фидот се прикажува во долниот лев агол на нивните слики-минијатури наместо под насловот. Некои видеа, како што е во полиците, можеби нема да прикажат број на прегледи + Прикажи број на лајкови на сликата во фидот + Прикажи ја сликата на фидот како процент на лајкови + Пребарај коментари + Допрете го копчето за пребарување во коментарите за да се прикажат само вчитаните коментари што ги содржат сите пребарувани зборови. Ставете ги зборовите во наводници за да пребарате фраза + Пребарај коментари + Зборови за пребарување + Пребарај + Исчисти + Променете го редоследот на сортирање или отворете ги коментарите повторно за да ги пребарувате + Пребарувањето е избришано. Променете го редоследот на сортирање или отворете ги коментарите повторно за да ги прикажете сите коментари Исклучи прозорче за најава на ТВ + Оневозможи го прозорецот „Поврзи ги твоите уреди“ Изберете ја надворешната апликација што ќе ги обработува вашите преземања Ја исклучува двојната допренска гестикулација со два прста што ги прескокнува поглавјата, која може случајно да се активира со еден прст + Преземете видеа, подкасти или песни со помош на апликации за преземање од трети страни + Прикажи го копчето за преземање во преклопот на плеерот + Прикажува копче во видео плеерот кое го отвора вашиот надворешен преземач + Брзо преземање на допир + Понуди за преземање во позадина. Допрете и држете за да го отворите преземачот. Работи со YTDLnis и YTindir + Означи преземањата како изгледани + Видеоклиповите преземени со надворешен преземач се додаваат во вашата историја на гледање на YouTube + Full HD (1080p) + Среден (192 kbps) + Брзо преземање + Видео + Аудио + Сите опции + Прикажи преземање во менито за Shorts + Додава ставка во искакувачкото мени за надворешен преземач во Shorts плеерот Преземи + Превод со глас + Гласно читање на титловите на видеото со помош на вградениот текст-во-говор, синхронизирано со репродукцијата + Прикажува копче-прекривање во видео плеерот. Допрете за да ја смените преводот, допрете и држете за брзи поставки + Јазик + Давлател на преводот + Гласност на оригиналната аудио запис + Гласност на оригиналното YouTube аудио додека се слуша преводот + Гласност на преводот + Гласност на гласот за дубиран превод + Максимална брзина на говорот + Максимален множител на брзина при прилагодување на говорот во слот за титлови + "Бесплатен API за превод, не е потребен API клуч. До 5.000 карактери дневно во бесплатниот план, или до 50.000 карактери дневно со е-пошта. + +Допрете тука за да го отворите MyMemory.translated.net" + MyMemory е-пошта + Опционално. Внесете ја вашата MyMemory е-пошта за да го зголемите дневниот лимит од 5.000 на 50.000 карактери. Оставете празно за да го користите анонимниот лимит + "Платен AI превод преку OpenRouter. Дополнете еднаш и преведете илјадници видеа. Mistral Nemo е најевтината опција, Gemma 4 нуди највисок квалитет. + +Допрете тука за да го отворите OpenRouter.ai" + OpenRouter API клуч + Вашиот OpenRouter API клуч. Креирајте клуч на OpenRouter.ai > Workspace > API Keys + OpenRouter модел По нарачка + OpenRouter не е конфигуриран + Внесете го вашиот OpenRouter API клуч во поставките пред да го изберете овој провајдер + Бесплатно + %s на 100 видео часа + Глас Систем + Мултијазичен машки + Мултијазичен женски + Машки + Женски + Време е за морфирање! + Преводот не е достапен + "Услугата за превод врати HTTP грешка %s. Веројатно вашата IP адреса е ограничена за до 24 часа. + +За да продолжите со користењето на преводите, сменете го преведувачот во поставките или користете VPN" Не ме прашувај повторно + "Вашите поставки содржат OpenRouter API клуч. + +Вашиот API клуч не треба никогаш да се споделува. Секој што има пристап до него може да ги користи вашите OpenRouter кредити" + Достигнат е лимитот на кредитите на OpenRouter + Достигнат е вашиот лимит на кредити на OpenRouter. Дополнете го вашиот баланс или зголемете го лимитот за трошење на вашиот клуч на OpenRouter.ai + Отвори OpenRouter.ai + Преводот преку OpenRouter не успеа (HTTP %s) + Достигнат е дневниот лимит на MyMemory + Следно достапно на %s + Ја искористивте денешната бесплатна квота за превод на MyMemory (5.000 карактери). Додадете е-пошта за MyMemory во поставките за да го зголемите дневниот лимит на 50.000 карактери, или сменете ја услугата за превод + Ја искористивте денешната квота за превод на MyMemory (50.000 карактери). Сменете ја услугата за превод или обидете се повторно утре + Преводот преку MyMemory не успеа (статус %s) Вклучи повторување на видеото Не се сеќавам видео во повторување @@ -246,6 +577,9 @@ Second \"item\" text" Повторување од %1$s до %2$s Невалиден временски опсег + Прикажи копче за слика во слика + Прикажи мени за слика во слика + Префрли на слика во слика Прикажи копче за репродукција на сите "Допрете за да генерирате листа на репродукција со сите видеа од каналот, допрете и задржете за да го поништите" @@ -268,14 +602,91 @@ Second \"item\" text" Прикажи копче за повторно вчитање на видео ID-то на видеото не е достапно. Ве молиме, мануелно преземете го видеото + Прикажи копче Зачувај за подоцна + Допрете за да зачувате во Гледај подоцна, допрете и држете за опции за редица Видеото успешно е зачувано на плејлиста „Гледај подоцна“ Ова видео веќе е зачувано на плејлистата „Гледај подоцна“ Не може да се зачува ова видео во плејлистата „Гледај подоцна“ + Зачувај во Гледај подоцна + Прикажи го копчето „Зачувај за подоцна“ во менито со поставки на плеерот + Овозможува зачувување на моменталното видео во „Гледај подоцна“ од менито со поставки на видеото + Прикажи копче Зачувај за подоцна во видеата за деца + Овозможува зачувување на видеа поврзани со деца во „Гледај подоцна“ од искакувачкото мени + Прикажи копче Зачувај за подоцна во искакувачкото мени за кратки видеа + Овозможува зачувување на кратки видеа во „Гледај подоцна“ од искакувачкото мени + Пренасочи „Гледај подоцна“ кон листа за 재생 + Гледај подоцна + Додај во Morphe редослед + Зачувај ја редоследноста помеѓу сесиите + Редоследноста се зачувува по рестартирање на апликацијата + Замени ги акциите Зачувај и Ред + Допрете за опции за редица, допрете и држете за да зачувате во Гледај подоцна + Тип на „Пушти следно во редот“ Стандардно + Нативен ред + Morphe ред + Morphe ред на страна на клиентот + Ставка во менито за додавање на фид во редослед + Додава дополнителна ставка во менито. Овозможете го ова ако \"Додај во редослед\" не се прикажува + Ред за чекање + Ред за чекање (%s) + Исчисти сè + Ништо нема во редот за чекање + Користете „Пушти следно во редот“ на кое било видео + Се репродуцира + Пушти следно во редата + Пушти го последното во редата + Промени редослед Отстрани + Додадено во редата + Ова видео не може да се додаде во редата + Плејлистите не можат да се додадат во редата + Редата е полна + Не успеа да се стартува видеото + Бела листа за реклами во пливачкото мени + Додава ставка во пливачкото мени која дозволува реклами да се репродуцираат на каналот на тоа видео + Оваа функција може да бара користење на PotHelper или промена на \"Лажирај видео стримови\" во најава за TV или Android VR + Бела листа за брзина во пливачкото мени + Додава ставка во пливачкото мени која секогаш го репродуцира каналот на тоа видео со брзина од 1.0x + Не сте најавени + Нема ID на плејлиста + Редоследноста е празна + Нема ID на видео + Неуспешно: %s + Неуспешно додавање во редот + Неуспешно создавање на редот + Неуспешно отстранување од редот + Неуспешно зачувување на редот + Додадено во редот + Редот е создаден + Отстрането од редот + Редот е зачуван во %s + Morphe Ред + Додај во редот + Додај во редот и отвори + Додај во редот и пушти + Додај и освежи го видеото + Отстрани од редот + Отстрани и отвори го редот + Отстрани и освежи го видеото + Отвори го редот + Зачувај го редот во плејлиста + Плејлистата е зачувана + Зачувано во %s + Исчисти ред + Редот е исчистен + Ред + Пушти го последниот во редот + Пушти го следниот во редот + YouTube не може да ја прикаже листата за редица. + Редицата не е подготвена за ова видео. Отворете го видео плеерот и обидете се повторно. + Додај Mix во редицата или гледај Mix? + Гледај + Додај во редица + Вчитување Mix… Отворете канал од аватарот во живо Спречува отворање на ливниот пренос кога ќе допрете на аватарот на каналот @@ -283,9 +694,22 @@ Second \"item\" text" Оневозможете гест за прецизно премотување Ја исклучува прецизната гестикулација за премотување која се активира кога ќе го повлечете лентата за премотување + Овозможи DVR за преноси во живо + Овозможува превиткање на сите преноси во живо, дури и на оние што вообичаено го ограничуваат тоа + Овозможи слики-минијатури на лентата за премотување + Прикажува слика-минијатура од видеото додека се влече лентата за премотување + Најмногу редирирано + "\"Овозможи DVR за во живо стриминг\" можеби не е достапно + +За да го поправите ова, вклучете го \"Лажирај видео стримови\" и променете го \"Стандарден клиент\" во \"TV Simply\" или \"visionOS\"" + Прошири го времетраењето на DVR за во живо стриминг + Ви овозможува да навратите преноси во живо до 7 дена во минатото + "'Прошири го времетраењето на DVR за во живо стримање' можеби не е достапно + +За да го поправите ова, вклучете го 'Лажирани видео стримови' и променете го 'Стандардниот клиент' во 'TV Simply' или 'visionOS'" Вклучи премотување со повлекување Овозможува премотување со лизгање наместо репродукција со 2x брзина кога се притиска и задржува видео плеер @@ -293,12 +717,20 @@ Second \"item\" text" Овозможете допир за премотување Само допрете ја лентата за премотување за да прескокнете до таа позиција без да ја влечете + Гест на лизгање налево + Гест на лизгање надесно + Гест на лизгање нагоре + Ширина на левата/десната зона + Процент од секој раб на екранот резервиран за гестови на лизгање налево и надесно (5-50) Осветленост Волумен Оригинален Исклучено + Игнорирај прелистување кога е заклучено + Гестовите за прелистување се игнорираат додека плеерот е заклучен со „Заклучи го екранот“ Овозможете гест за притискање и повлекување + Потребно е притискање и држење пред да се препознае гестум за поместување Овозможете хаптичен поврат Зачувајте и вратете ја светлоста Зачувува ниво на осветленост кога влегувате/излегувате од полноекрански режим @@ -319,10 +751,17 @@ Second \"item\" text" Прилагодете ја големината на текстот на екранот (1-30) Праг на гестот за повлекување Поставете ја потребната оддалеченост на кликнување пред да се активира прилагодувањето + Осетливост на прелистање за гласномер + Повисоките вредности бараат повеќе прелистање по чекор на гласномоста + Чекори на гласномоста + Зголемување на гласноста + Повлекувањето нагоре над максималната гласност ја зголемува таа, што може да го изкриви гласниот звук + Зададено за уредот Чуствителност на движење за осветленост Поголемите вредности бараат повеќе движења за секое ниво на осветленост Чуствителност на движење за брзина Поголемите вредности бараат повеќе движења за секое ниво на брзина + Чекор на брзина Висина на зоната за брзина Висина на зоната за гест за брзина како процент од висината на плеер (5-75) Брзина @@ -351,6 +790,7 @@ Second \"item\" text" Прикријте го копчето за Коментари + Скриј копче за поврзување Прикријте го копчето за Пријавување @@ -368,13 +808,18 @@ Second \"item\" text" Прикријте го копчето за Ask + Скриј го каналот и копчето за претплата + Ги крие аватарот на каналот и копчето за претплата на почетокот на лентата за акции Скриј копче за претплата + Ги крие копчињата Претплати се и Приклучи се дорпадот на аватарот на каналот на почетокот на лентата за акции Прикријте го копчето за Clip + Ова можеби нема да работи за некои кориснички сметки Прикријте го копчето за Shop Прикријте го копчето за Save + Скриј копче за повеќе Копчиња за полноекрански режим Прикријте го копчето за Dislike Прикријте го копчето за Like @@ -394,10 +839,17 @@ Second \"item\" text" Прикажи кога е исклучен звук Покажи кога е исклучен звукот + Препочитан јазик за титлови + Прво ги избира титловите од провајдерот, а потоа автоматски ги преведува на преферираниот јазик Исклучено + Додај дополнителни јазици за автоматски превод + Додава повеќе јазици во менито за автоматски превод Поставете колачиња за натписи + "Поправа стандарден YouTube баг каде што авто-преводот на титлите може да не биде достапен поради недостасувачки колачиња + +Морат да се наведат 'Колачиња за титлови'" Колчиња за титри Колачиња што треба да се постават во барањата на Timed Text API (Caption Data API) Преземи колчиња за титри @@ -408,6 +860,7 @@ Second \"item\" text" Поправете го транскриптот Отстранува проблем со претходни услови што може да спречи да се вчита транскрипт + Прикажи написи во слика-во-слика Копчиња за навигација Изберете кои копчиња да се појавуваат во лентата за навигација @@ -428,18 +881,33 @@ Second \"item\" text" Акција на копчето за поставки Кога е овозможено, допир го отвора Morphe поставените параметри, а долго притискање го отвора YouTube поставените параметри Заменете го копчето „Креирај“ со копчето „Известувања“ + Ако промената на оваа поставка нема ефект, обидете се да преминете во Incognito режим Скриј ги ознаките на копчињата за навигација + Скриј точка за нова содржина Скриј лента за навигација + "Ова целосно ја крие лентата за навигација, вклучително и табулаторот 'Вие'. + +За пристап до Подесувањата, овозможете го копчето за Подесувања преку Општо → Алатка → Покажи Подесувања" Вклучи тесни навигациски копчиња Намалување на хоризонталното растојание меѓу копчињата + Оневозможи ги копчињата за навигација без етикети + Ги прикажува копчињата за навигација со етикети, доколку YouTube ги испрати без етикети + Оневозможи ги разблаганите табови за навигација + Прикажува Претплати во лентата за навигација, доколку YouTube ги премести во таб на врвот од Почетна Овозможете анимации на лентата за навигација Анимира преоди кога се префрлувате меѓу картичките Исклучи автоматско скривање на навигацискиот панел Ја задржува лентата за навигација видлива додека лизгате + Стил на лента за навигација Стандардно + Проѕирно + Транспарентно + Непроѕрно + Овој стил можеби не е поддржан од вашиот уред или верзијата на YouTube, и може да предизвика проблеми со распоредот или паѓања на апликацијата Toolbar Изберете кои компоненти да се појавуваат во лентата со алатки Прикријте го копчето Cast + Скриј копче за чет Прикријте го копчето Microphone Прикријте го копчето Search Кога е овозможено, допир го отвора Morphe поставените параметри, а долго притискање го отвора YouTube поставените параметри @@ -452,6 +920,9 @@ Second \"item\" text" Прикријте го менито \'Ambient mode\' + "Менито 'Audio Track' е скриено + +За да го прикажете менито 'Audio Track', променете го 'Spoof video streams' во кој било клиент освен 'Android Studio' и 'Android VR Downgraded'" Прикријте го менито Audio Track Прикријте го подножјето на менито Audio Track @@ -468,6 +939,7 @@ Second \"item\" text" Прикријте го менито \'Loop video\' + Скриј мени „Во движење“ Прикријте го менито Quality Прикријте го подножјето на менито Quality @@ -494,6 +966,8 @@ Second \"item\" text" Прикријте го копчето Fullscreen Скриј ја позадината на копчињата на плеер Прикрива ја темната нијанса околу контролите на видео плејерот + Проѕирност на позадината на копчињата за контрола + Прилагодете ја проѕирноста на позадината зад копчињата за контрола на плеерот и лентата за време (0 е невидливо, 100 е цврсто) Прикријте ги копчињата за претходна и следна Прикријте го копчето Settings @@ -534,6 +1008,7 @@ Second \"item\" text" Скриј копче „Коментари“ Скриј копче за ефект Скриј ознака за линк до видео + Скриј ги помошните забелешки за гестури Скриј копче \"Зелен екран\" Скриј копче за хаштаг Скриј информативен панел @@ -551,6 +1026,7 @@ Second \"item\" text" Скриј копче „Ремикс“ + Скриј копче за зачувување Скриј копче \"Зачувај музика\" Скриј предлози за пребарување @@ -567,6 +1043,10 @@ Second \"item\" text" Скриј наслов на видео Скриј предложени видеа на крајот од екранот + "Го крие прекривањето со предложено видео што се појавува откако видеото ќе заврши + +Оваа поставка работи само ако автоматското пуштање е исклучено. Можете да ја промените во YouTube: +Поставки → Репродукција (или Автоматско пуштање) → Автоматско пуштање на следното видео" Скриј поврзани видеа @@ -578,7 +1058,10 @@ Second \"item\" text" Исклучи панели во прозорецот на плеер Исклучува ги панелите за автоматско појавување на плејлистите и live chat + Оневозможи автоматско пуштање на плејлиста + Спречува плејлистите автоматски да преминуваат на следното видео + Оневозможи ја пораката „Продолжи со гледањето?“ Излез од режимот на полн екран на крајот од видеото Оневозможено @@ -586,17 +1069,40 @@ Second \"item\" text" Само хоризонтален режим Портрет и пејзаж + Присилно пејзажен режим во цел екран + Ротирај го плеерот во пејзажен режим при влегување во цел екран. Недоступно на мали екрани, кои веќе го отвораат целиот екран во пејзажен режим + Гестови во цел екран + Гестови на плеерот во режим на цел екран + Оневозможи гест за повлекување нагоре во портрет + Оневозможи гест за повлекување надолу во портрет + Оневозможи гест за повлекување надолу во пејзаж + Оневозможи го гестот за зумирање со две прсти + Врати го зумирањето со две прсти + Отвори видеа во режим на цел екран Оневозможено + Портрет + Пејзаж + Скалирање на видео во цел екран Стандардно + Растегнување (пополни го екранот) + Зум (исечи за да пополниш) + Прикажи го копчето за скалирање на видеото + Допрете за да менувате помеѓу Стандардно, Растегнување и Зум + Прикажи само во цел екран + Скриј го копчето за скалирање на видеото во портретниот плеер + Прикажи копче за исклучување звук Непроѕирност на прекривката на плеер Прилагодете ја провидноста (0 е провидно, 100 е непровидно) + Лента за пребарување Оневозможено + Широка + Екстра широка Поставување на уред @@ -604,8 +1110,30 @@ Second \"item\" text" Пребарување Шопинг Спорт + "Промените вклучуваат: + +Распоред за таблет +• Објавите од заедницата се skjieni +• Подесувањето за репродукција во фидовите не е достапно +• Копчето Remix и копчето Sound не се достапни во Shorts +• Прекривањето „Повеќе видеа“ може да не биде достапно +• Се прикажува стара лента за акции на видео која не е склопена +• Стапките во менито Flyout може да не работат правилно + +Автомобилски распоред +• Фидот е организиран по теми и канали +• Подесувањето за репродукција во фидовите не е достапно +• Копчето Remix и копчето Sound не се достапни во Shorts +• Прекривањето „Повеќе видеа“ може да не биде достапно +• Shorts се отвораат во вообичаениот плеер +• Се прикажува стара лента за акции на видео која не е склопена" + Овозможи таблет-изглед во плеерот + "Го враќа стариот распоред на плеерот + +Ограничување: Ако „Врати ја старата лента за акции на видеото“ е исклучено, прекривањето „Повеќе видеа“ може да не биде достапно" + 21.11.48 - Враќање на старото однесување на titlovite 20.39.41 - Врати ја старата постава за зачувување во листа за репродукција 20.28.41 - Исклучи болдирани икони 20.13.41 - Враќање на името на каналот под видео плеер @@ -614,6 +1142,9 @@ Second \"item\" text" Користи системски коноп на хака за вика како маст за споделување Замени ги копчињата на YouTube Music + Го отвора вашиот омилен музички приложение наместо стандардната YouTube Music апликација + Име на пакетот на музичката апликација + Име на пакетот на вашата инсталирана музичка апликација Почистете датум Почетна @@ -653,24 +1184,51 @@ Second \"item\" text" Аоларка Спот отвори Аотоматски оток на кошули Аотоматски лор и ризик во окртна спостр + Прескокни живи Shorts + Ги прескокнува живите стримови што се појавуваат во фидот за Shorts + Воспроизведување во фидовите + Секогаш вклучено + Само Wi-Fi Исклучено + Врати ги оригиналните наслови + Ги прикажува оригиналните наслови на видеата, описите на видеата и описите на каналите наместо автоматски преведените + Вчитување на насловот… + Оневозможи автоматско освежување на фидот + Спречува фидовите автоматски да се освежуваат откако станат застарени + Изглед на картичката во фидот + Класичен + Картички + Картички со копчиња за лајк, коментар и зачувување + Коментари Исклучи продолжување на мини-плеер + Го оневозможува миниплеерот „Продолжи со гледање“, функција на YouTube Premium која се појавува при стартување на апликацијата Мини-плеер Прилагодете го изгледот и однесувањето на минимизираниот видео плеер Тип на мини-плеер + Скриј ја насловот и каналот Оневозможено Поставено како стандардно Минималистички + Минималистички 2 + Таблет + Модерен 1 + Модерен 2 + Модерен 3 + Модерен 4 Исклучи заоблени агли Исклучи превлекување и пуштање Се напркат гнау за препри за перу Исклучи хоризонтално превлекување Спречува затворање на мини-плеер со хоризонтално лизгање надвор од екранот + Паузирајте го репродукувањето со хоризонтално влечење + Воспроизведувањето ќе се паузира кога мини-плеерот ќе се повлече надвор од екранот + Запомни ја позицијата на состојбата на влечење + Превлекувањето на мини-плејерот надвор од екранот ќе ја зачува неговата UI состојба Скриј наслаги Стандардна големина Поставете ја почетната големина на екранот, во пиксели @@ -678,10 +1236,34 @@ Second \"item\" text" Непровидност на наслагата Прилагодете ја транспарентноста (0 е транспарентно, 100 е непровидно) + Копчето „Назад“ излегува од фидот + Притискањето на копчето за назад секогаш ја затвора апликацијата наместо да скрола до врвот на фидот + Присилни анимации Стандардно + Секогаш вклучено + Секогаш исклучено + Крии ја лентата за статус + Ја крие системската лента за статус. Повлечи надолу од горната работна површина за да ја прикажеш накратко + Поправи ја лентата со статус што ги покрива горните ленти + Ја одржува конзистентна поставката на лентата со статус, така што таа нема да ги покрива горните ленти и нема да остави празен простор над плеерот. Исклучете го ова ако апликацијата се руши или горните ленти изгледаат погрешно + Сјај + Светкавиот почетен екран не е поддржан од овој уред + Позадина на светла тема + Бела + Material You (Бела) + Светло розева + Светло сина + Светло зелена + Светло жолта + Светло портокалова + Светло црвена + Прилагодена светла позадина + Поставете ја бојата на позадината за светлата тема + Променете ја бојата на преден план + Користете ја бојата на темата за текст и икони Вклучи анимиран екран за вчитување Прикажува градиентна позадина на екранот за вчитување наместо еднобојна Вклучи прилагодена боја на лентата за премотување @@ -691,6 +1273,9 @@ Second \"item\" text" Поставете ја секундарната боја на лентата за премотување на видеото Невалидна вредност за боја на лентата за премотување + Оригинална analogska + Оригинална торта + Оригинален метал Лого на насловната страница Поставено како стандардно @@ -702,15 +1287,32 @@ Second \"item\" text" Пренасочува барања за слички до алтернативен сервер за да се реши проблемот со недостатните слики во ограничените земји + DeArrow наслови + Таб Почетна + Таб Претплати + Вие + Плејлисти & препораки од плеерот + Резултати од пребарувањето + Прикажи DeArrow икона + Прикажува ја DeArrow иконата на насловите на видеата од заедницата + Опуштен режим + Го прикажува оригиналниот наслов наместо насловот од DeArrow ако корисниците на DeArrow гласале дека им се допаѓа оригиналниот наслов + Минимален број гласови во casual режим за прикажување на оригиналната наслов + Бројот на неформални гласови во една категорија, како што се смешна или креативна, потребни за прикажување на оригиналната насловна страница + DeArrow слики-минијатури Оригинални слики DeArrow & Оригинални слики DeArrow & Снимки од екранот Снимки од екранот + "DeArrow ги заменува насловите и сликите од видеата кои се кликбејт со такви од заедницата кои се поточни и помалку сензационалистички. Насловите се пишуваат и гласаат членовите на заедницата, а сликите се кадри земени од видеото + +Видеата се бараат со краток префикс од хашираниот ID на видеото, па серверот не може точно да знае кое видео го гледате. Ако видеото нема DeArrow слика, се прикажува оригиналната или статична слика" Прикажи известувања за грешка во поврзувањето Прикажува известување кога серверот DeArrow не е достапен + API крајна точка URL-адреса на крајната точка за кешот на сликите на DeArrow Снимки од екранот од видеа Снимките од екранот се преземаат од почетокот / средината / крајот на секое видео. Овие слики се вградени во YouTube и не се користи надворешен API @@ -728,6 +1330,7 @@ Second \"item\" text" Спојување на димензии на уредот Овозможувањето на ова може да отклучи повисоки квалитети на видеа + Овозможувањето на ова може да предизвика прекинување на репродукцијата на видеото, пократок век на батеријата и непознати странични ефекти Хептична повратна врска Изберете ги интеракциите што ја зачекуваат вибрацијата на уредот @@ -737,13 +1340,28 @@ Second \"item\" text" Исклучи хептична повратна врска при допир и задржување Исклучете хептична повратна врска за зумирање + Ако неодамна ги променивте податоците за најава на вашата сметка, тогаш деинсталирајте го и повторно инсталирајте го MicroG + Заобиколете пренасочувања на врски + Ја отвора оригиналната врска директно, заобиколувајќи ја врската за следење (youtube.com/redirect) Отворете ги линковите во прегледувач Отвара линкови во надворешен пребарувач наместо во пребарувач во апликацијата + Големина на баферот за репродукција Стандардно + Ниска + Средна + Максимална + Одлочка на видео + Го одлочува видеото за да остане синхронизирано со аудиото што задоцнува, како што се Bluetooth слушалките (0-5000 ms) + Одлочка само со Bluetooth аудио + Само го одложува видеото додека е поврзано аудио преку Bluetooth, за звучникот на телефонот да остане синхронизиран + Прикажи копче за одложување на видеото + Допрете и држете за да го ресетирате одложувањето на 0 ms + Прикажи мени за одложување на видеото + Одложување во милисекунди Авто @@ -757,12 +1375,18 @@ Second \"item\" text" Ги применува вашите мануелни избори за квалитет до сите идни Shorts Стандарден квалитет на Shorts на Wi-Fi мрежа Стандарден квалитет на Shorts на мобилна мрежа + Променет е стандардниот квалитет за мобилна мрежа на: %s + Променет е стандардниот квалитет за Wi-Fi на: %s + Променет е квалитетот на Shorts за мобилна мрежа на: %s + Квалитетот на Shorts Wi-Fi е променет во: %s Прикажи копче за брзина на репродукција Допрете и задржете за да ја вратите брзината на стандардна Прикажи копче за квалитет Допрете и задржете за да го вратите квалитетот на стандарден + Прикажи ја резолуцијата на копчето за квалитет + Прикажи ја резолуцијата, како на пример 1080, наместо ознака како FHD Мени за прилагодена брзина на репродукција Врати го старото мени за брзина на репродукција @@ -779,33 +1403,252 @@ Second \"item\" text" Известувања за промена на брзината Покажува известување кога се зачува нова стандардна брзина на репродукција Стандардна брзина на репродукција + Стандардна брзина на репродукција за Shorts + Запомни ја брзината на репродукција во Shorts + Го применува вашиот рачен избор на брзина за Shorts на сите идни Shorts. Ако е исклучено, брзината се враќа на стандардната по затворање на Shorts плеерот + Прикажи ја брзината на репродукција во менито за Shorts + Додава ставка за брзина на репродукција во менито на Shorts плеерот + Брзина на репродукција · %s Променета е стандардната брзина на репродукција на: %s + Користи нормална брзина за музика + Секогаш пуштај музички видеа со брзина од 1.0x + Бела листа за брзина на канали + Видеоклиповите од каналите на белaта листа секогаш се репродуцираат со брзина од 1.0x + Додај канал на белата листа за брзина + Отстрани канал од белата листа за брзина + Овозможи контроли за висина на аудиото + Стандардна висина на аудиото при репродукција + Променето е стандардниот аудио висок тон на: %s + Растегнување на аудио времето + Овозможува независно поставување на брзината и високиот тон, што може да предизвика аудио артефакти + Запомни ја позицијата на репродукција на стрим во живо + За тековните стримови во живо, ја запомнува и ја продолжува вашата позиција на репродукција наместо да се префрли на моменталната позиција во живо + Оваа функција не работи со 24/7 стримови во живо и други стримови кои се \'секогаш вклучени\' Исклучи HDR видео + Форсирај HDR видео + Го принудува приложението да го препознае овој уред како компабилен со HDR + "Користете го ова ако HDR декодирањето работи, но YouTube не нуди HDR, како кај некои HyperOS уреди што го кријат HLG. + +Ограничувања: +• Боите може да изгледаат погрешно ако дисплејот всушност не може да прикаже HDR +• Репродукцијата може да не успее ако декодерот не може да ракува со HDR +• HDR сè уште зависи од клиентот за лажирање на видео стримовите" + Видео кодек Стандардно + За да присилите AVC, променете го \"Симулирај видео стримови\" во \"Android Studio\", \"Android VR\" или \"visionOS\" + "VP9 +• Може да го подобри траењето на батеријата и да го поправи прекинувањето при репродукција +• Користи повеќе интернет податоци од AV1 + +AVC (H.264) +• Потребен е клиент за симулирање на видео стрим 'Android Studio', 'Android VR' или 'visionOS' +• Може дополнително да го подобри траењето на батеријата +• Може да ги врати недостасувачките видео резолуции на постари уреди +• Максималната резолуција е 1080p +• HDR видеата може да не користат AVC +• Некои уреди не можат да присилат AVC" + Отвори го менито за напреден квалитет + Го прескокнува менито за избор на квалитет и го отвора директно менито за напреден квалитет Скриј опции за квалитет на Premium + Приоритет на квалитетот на видеото + Дава приоритет на највисокиот достапен квалитет на видеото пред ефикасноста на кодекот + Ако оваа поставка е исклучена, постарите видеа може да не прикажуваат опции за квалитет на видеото + Дозволи \'Android VR\' / visionOS AV1 + Овозможува софтверско AV1 декодирање при користење на \'Android VR\' / visionOS клиентот + "Овозможувањето на оваа поставка може да користи софтверско AV1 декодирање. + +Репродукцијата на видео со AV1 може да прекинува или да испушта кадри" Несакани ефекти од спојување + • Репродукцијата може да не успее по 1 минута + • Менуто „Audio Track“ недостасува • Нема видео кодек AV1 • Недостапен имерзивен режим со 360° VR • Недоступен стабилен волумен + • Репродукцијата може да заврши 1 секунда порано • Ако деобфускацијата не успее, репродукцијата може да не успее • Недостапен принуден оригинален звук + • Потребен е најавување во Android VR + • Квалитетот може да биде ограничен на 720p Варијанта на JavaScript на плеерот + Прикажи симулирање во „Stats for nerds“ + Враќање на старата лента за акции на видеото + "Поправени се копчињата за видео акции кои не можеа да се скријат + +Ограничување: Се враќа на стариот стил на прекривање „Повеќе видеа“" + Стил на иконата на плеерот Автоматски Стандардно + Тенка + Дебела По нарачка + Стил на иконата за Shorts + Следење серии + Следете ги плејлистите како серии и запомнете го вашето место + Овозможи следење серии + Додава страница за Серии во Историјата и опција за Следење серии во плејлистите + Прикажи Серии во навигациската лента + Прикажи копче за следење серии + Додава копче во плеерот за следење или отворање на серијата на моменталното видео + Запиши го напредокот на следените серии + Запиши го репродукцијата на овој телефон за зачуваните серии и обележувачи Откажи + Зачувај + Зачувај обележувач + Име на серијата + Опции за серија + Опции за библиотека + Освежи епизоди + Преименувај + Отвори ја листата за 재생 во YouTube + Отстрани од библиотеката + Линк или ID на плејлиста + Избриши го целиот локален напредок + Избриши ја историјата + Дали да се избрише целиот локален напредок? + Ова ги брише позициите, следените пребришувања и селекциите за продолжување за секое видео. Зачуваните серии и нивните каталози остануваат. Новото репродуцирање ќе се снима ако снимањето е овозможено + Обележи како гледано + Обележи како негледано + Користи автоматски статус + Рестартирај од 0:00 + Пушти повторно од 0:00 + Статусот е зачуван + Повторно обиди се зачувување + Претходни 100 + Следни 100 + Отвори зачувано видео + Локалниот напредок е избришан + Отвори Историја од \"Вие\" за да ги видите вашите серии + Историја на гледање + Серии + Вчитување зачувани серии… + Следете листа за репродукција за да ги следите нејзините епизоди + Зазнака за плејлиста + Освежување епизоди… + Сите серии + Изгледано + Вчитување епизоди… + Повеќе опции + "Снимајте напредок само за вашите зачувани серии и означени видеа. + +Оваа библиотека се чува на овој уред и се споделува помеѓу сметките на YouTube. Контролите за паузирање и бришење на историјата на YouTube не ја менуваат таа. Користете Серии $\rightarrow$ Повеќе $\rightarrow$ Избриши локален напредок за да ја избришете. + +Автоматското снимање запира во инкогнито режим, кога идентитетот на сметката не е достапен или кога ќе преминете на друга сметка. Овозможете го следењето повторно за да го користите со таа сметка" + Следењето не може да се овозможи во инкогнито режим или пред YouTube да ја вчита тековната состојба на сметката. Вратете се на Почетна и обидете се повторно. Не е потребно најавување + Овозможи локално следење + Снимањето на напредокот е исклучено + Продолжи од %1$s + %1$s / %2$s изгледани епизоди + Епизода %1$s · %2$s + Епизоди %1$s-%2$s од %3$s + Освежете за да се обидете да ги вчитате епизодите. Приватните плејлисти бараат пристап до сметката; нивниот зачуван линк сè уште се отвора во YouTube + YouTube не успеа да ја отвори оваа епизода + YouTube не успеа да ја отвори оваа плејлиста + Изберете епизода + Ажурирано + Нема епизоди што можат да се пуштат + Зачувано видео · %1$s + Следете серија + Прегледај серија + Продолжи + Започни одтука + Зачувано почетна точка + Гледано дотука + Епизоди означени како гледани Врати + Овозможи + %1$s нови епизоди + Запиши го напредокот на овој уред + "Вашите серии и напредокот остануваат на овој уред, заеднички за сметките на YouTube. Контролите за историјата на YouTube не ги менуваат. Избришете го напредокот од менито за Серии. + +Автоматското снимање е опционално. Тоа запира во инкогнито режим или кога сметката се менува" + Епизодите не можеа да се вчитаат. Можете да ја зачувате плејлистата како зачуван линк + Премногу незачувани промени. Прво обидете се повторно да зачувате + Зачувувањето е во тек. Обидете се повторно + Вчитањето на епизодата е во тек. Обиди се повторно + Складиштето е недостапно. Вашите зачувани податоци се зачувани + Промените не можеа да се зачуваат. Обидете се повторно да зачувате пред да го затворите YouTube + Оваа серија е веќе зачувана или нејзината листа за 재생 е променета. Отворете ја повторно + Оваа серија е променета или е отстранета. Отворете ја библиотеката повторно + Листата за 재생 е променета. Изберете ја епизодата повторно + Оваа епизода е недостапна. Изберете друга + Внесете име на серијата + Внесете линк до YouTube плејлиста или ID на плејлиста + Ве молиме почекајте пред повторно да освежите + Епизодите не можеа да се вчитаат. Обидете се повторно подоцна + Промената не може да се заврши. Обидете се повторно + Возобнувањето не може да се потврди. Проверете го плеерот и обидете се повторно + Следени серии + Ова видео не е во оваа плејлиста + Се пребаруваат плејлисти… + Залепи линк до плејлиста + Не можевме да најдеме плејлиста + Нема достапни епизоди + Скриј ги изгледаните + Означи ги претходните епизоди како изгледани + Започнува со: %1$s + Редослед на епизоди + Редослед на листа за 재생 + Обратен редослед на листа за 재생 + %1$s прегледи + %1$s преглед + Означи ги епизодите како гледани + Најмалку %1$s%% изгледано со најмногу 90 секунди што остануваат, или %2$s секунди преостанати + Автоматски означи епизода како изгледана по кој било од праговите. Правилото за процент исто така бара да не остануваат повеќе од 90 секунди, за крајот на долгите епизоди да не се прескокне. Правилото за преостанато време се применува за видеа подолги од една минута; поставете го на 0 за да го исклучите. Промените се применуваат на идните ажурирања на напредокот. Рачните избори за изгледано/неизгледано се зачувуваат + Процент на изгледано (1-100) + Преостанати секунди (0-300; 0 исклучува) + Внесете вредност од %1$s до %2$s + Користи YouTube напредок + Продолжи со следените серии по гледање на друг уред + Читај го статусот на изгледано и позициите за продолжување за вашите следени серии од тековната YouTube сметка. Само одговарачките епизоди се зачувуваат на овој телефон. Проверките се извршуваат додека библиотеката на серии е отворена и пред Продолжи. Историјата на YouTube мора да биде овозможена на вашиот друг уред + Проверка на YouTube напредокот… + Напредокот на YouTube е недостапен. Се користи зачуваниот напредок + Дел од напредокот на YouTube е недостапен. Зачуваните позиции се задржани + Резервно копирање и враќање + Зачувајте ја вашата библиотека со серии во датотека или вратете резервна копија + Зачувува следени серии, позиции на епизоди и избори за изгледано. Дозволите за снимање и напредок на YouTube не се вклучени + Зачувај резервна копија + Врати резервна копија + Додај %1$s сериии. %2$s сериии што веќе се во вашата библиотека се задржани. Постоечкиот напредок на видеото е задржан + Резервната копија на сериите е зачувана + Вратени се %1$s сериии + Овој фајл не е поддржана резервна копија на сериии или е преголем. Вашата библиотека не е променета + Не може да се зачува резервната копија. Обидете се со друга локација + Серијата е отстранета + Оваа епизода сега се појавува повеќе од еднаш. Изберете од која да продолжите + Вашата зачувана епизода повеќе не е во оваа листа за 재생. Вашата зачувана позиција е задржана + Продолжи со епизода %1$s · %2$s + Можете да изберете и која било епизода подолу. Претходните епизоди не се променети + Оваа листа за 재생 се промени. Отворете ги сериите за да изберете каде да продолжите + Нема повеќе епизоди во овој правец + Продолжи со %1$s + Пребарај серии + Исчисти пребарување + Не се најдени серии + Епизода без наслов + Недоступна епизода + Niconico коментари + Прикажи го живиот чет и повторувањето на четтото како коментари што се движат низ видеото + Прикажува копче прекривање во видео плеерот. Допрете за да ги прикажете или сокриете коментарите, допрете и задржете за брзи поставки + Големина на фонтот на коментарите + Големина на текстот на коментарите (12-40 sp) + Проѕирност на коментарите + Проѕирност на текстот на коментарите и позадините + Времетраење на движењето + Колку секунди им треба на коментарите за да го поминат екранот. Помалите вредности се движат побрзо + Максимална должина на коментарот + Ги крие коментарите со повеќе карактери од оваа вредност. Секој емотикон се брои како еден карактер + Без ограничување diff --git a/patches/src/main/resources/addresources/values-mn-rMN/music/strings.xml b/patches/src/main/resources/addresources/values-mn-rMN/music/strings.xml index ccfea4ed71..fc3312c361 100644 --- a/patches/src/main/resources/addresources/values-mn-rMN/music/strings.xml +++ b/patches/src/main/resources/addresources/values-mn-rMN/music/strings.xml @@ -17,58 +17,674 @@ Second \"item\" text" --> + Хамгийн сүүлд тоглосон + Бүртгэлүүд + Видеонууд + Бусад үр дүн + Хуульсан дуунууд + Хуульсан цомгууд + Хуульсан уран бүтээлчид + Түр зогсоох болон үргэлжлүүлэх функцийг ашиглахын тулд хүлээн авагчийн Jam патчийг шинэчлээрэй + Jam дарааллын хуваалцах + Ойр orchin-д байгаа Jam-ийг эхлүүлэх эсвэл нэгдэж, Jam Layer-ээр дамжуулан дарааллын жагсаалтыг засварлаарай + Jam дарааллын хуваалцахыг идэвхжүүлэх + YouTube Music дахин ачаалсны дараа Jam интеграцчлалыг идэвхжүүлнэ + Ажиллуулахын тулд Jam Layer-ийг татаж авах + Jam Layer-ийн хамгийн сүүлийн хувилбарыг хөтөч дээрээ нээнэ + Jam удирдлага + Ойр орчны Jam-ыг зохион байгуулах эсвэл нэгдэж, сессийг удирдах + Jam Layer багц + Ойр proximity-д дарааллыг хуваалцахад ашиглах суурилуулсан туслах аппликейшныг сонгоно уу + Суурилуулсан туслах багцыг ашиглах. Хослоход таны зөвшөөрөл болон дотоод чадварх токен шаардлагатай + Wi-Fi асаах уу? + Wi-Fi-г асаагаад, Jam-ыг дахин эхлүүлэх эсвэл нэгдэхийг оролдоно уу + Wi-Fi тохиргоог нээх + Цуцлах + Багцыг ашиглах + Android багцын зөв нэрийг оруулна уу + Багцыг хадгаллалаа. Холбогдохын тулд Jam Layer-ийг тохируулна уу + Jam холболт тасарлаа + Эхлээд Jam Layer-ийг идэвхжүүлнэ үү + Тохиргооноос Jam дарааллын хуваалтыг идэвхжүүлээд, YouTube Music-ийг дахин ачаална уу + Эхлээд сонгосон Jam Layer багцыг суулгана уу + Хост дээр гарсан кодыг оруулна уу. Wi-Fi Aware нь LAN сүлжээ нь өөр байсан ч ойрхон байгаа төхөөрөмжүүдийг холбох боломжтой + Кодоор нэгдэх + Нэгдэх + QR унших + Найман тэмдэгттэй код оруулна уу эсвэл урилгыг хуулж тавина уу + Jam-д нэгдэх + Кодыг хуулах + Jam код + QR кодыг харуулах + Дууссан + Jam-даа урих + Энэхүү дарааллаа joining-д нэгдэж, засварлахын тулд скан хийнэ үү. Jam-ыг зогсоовол урилга хүчингүй болно + Холбоосыг хуулах + Jam урилга + Jam-ыг дахин холбож байна; түр хугацааны дараа дахин оролдоно уу + Jam-д нэмэгдлээ + Jam-ыг ашиглах боломжгүй; трекийг нэмж чадсангүй + Таны Jam + Хамт сонсож байна + Jam-д нэгдэж байна + Хамт сонсох + Jam-ийг шинэчилж байна… + Wi-Fi-аас хост-ыг хайж байна… + Хүмүүсийг хүлээж байна + %s хүн холбогдлоо + %s хүн холбогдлоо + Энэхүү кодыг ойр proximity-д байгаа төхөөрөмж дээр оруулна уу. Wi-Fi Aware нь дэмжигдсэн тохиолдолд тусдаа LAN-уудын хооронд ажилладаг. %s минутын дараа хүчингүй болно + %s рүү шилжүүлэх үү? + Засвар нээлттэй + Засвар түгжигдсэн + Wi-Fi Aware-аар холбогдсон + Wi-Fi-аар холбогдсон + Bluetooth-ээр холбогдлоо + Jam-ын үеэр дэмжигдээгүй + Нэгдэх үйлдлийг цуцалсан + Хосттойгоо дахин холбогдож байна… + Дарааллыг хуваалцаж, хүн бүрт дуу нэмэх боломжийг олгоно уу + Хүмүүсийг урих + Зочид дарааллыг засаж болно + Jam-ыг дуусгах + Host-ийн тоглож буй хөгжмийг сонсоно. YouTube Music-ийн дарааллын дууг шууд нэмэх, зөөх эсвэл устгах боломжтой + Хоёр төхөөрөмжөө ойрхон байлгана уу. Host-ийг хайж байх хооронд та цуцлах боломжтой + Нэгдэхийг цуцлах + Jam-аас гарах + Jam эхлүүлэх + Jam Layer-ийг тохируулах + Удирдах + Дэлгэрэнгүй + Нээх + Дууны сонголтуудыг ачаалж байна… + Дууны сонголтууд боломжгүй; дахин оролдоно уу + Дууны сонголтуудыг нээж чадсангүй + Jam Layer татах боломжтой хөтөч олдоогүй + Host-ийн дарааллыг хүлээж байна + Эхлээд YouTube Music дээр дуу нээнэ үү + Host дээр тоглуулах + Jam-аас гарч локалаар тоглуулах + Jam-д дараагийнхыг тоглуулах + Jam-ийн дараалалд нэмэх + Энэхүү Jam трекийг тоглуулах + Хаана тоглуулахаа сонгоно уу + Хүлээн авагчийг энд шилжүүлнэ үү + Дууг урагшлуулах бэлэн болоогүй байна + Дарааллын жагсаалт өөрчлөгдлөө; дохиог дахин оролдоно уу + Толгой хэсгийн лого + Үндсэн + Өөрчлөх + Альбомын аудио трекүүдийг тоглуулах + Дууны клипийн оронд альбомын аудио хувилбарыг тоглуулах. \'Видео урсгалыг хуурах\' тохиргоо шаардлагатай + Аудио/видео шилжүүлэгчийг идэвхжүүлэх + Аудио трекий хувилбар болон албан ёсны хөгжмийн клипийг шилжүүлж үзэх + Гуравдагч талын татаж авах аппликейшн ашиглан хөгжмийн бичлэг, подкаст эсвэл дуу татаж авах + Татаж авах цэсэнд гаднах татаж авагчийг ашиглах + Цэсийн Татаж авах сонголт нь гаднах татаж авагчийг нээх болно + Татаж авсан файлууд + "Татаж авсан файлгүй байна +Татаж авсан трекүүд энд гарч ирнэ" + %1$s • %2$s • %3$s МБ + %1$s трек • %2$s МБ + Татаж авснаас хайх + Эрэмбэлэх + Уран бүтээгчээр эрэмбэлэх + Гарчигаар эрэмбэлэх + Хэмжээгээр эрэмбэлэх + Татаж авсан бүх файлыг устгах + Татаж авсан бүх файлыг устгах уу? + Татаж авсан бүх трекүүдийг энэ төхөөрөмжөөс устгана + Татаж авсан файлуудыг устгалаа + Юу ч олдсонгүй + %s-д зориулсан үйлдлүүд + Тоглуулах + Тоглуулах эсвэл түр зогсоох + Өмнөх + Дараах + Түр зогсоох + Хаах + Устгах + Татаж авсан файлыг устгах + Татаж авсныг устгах уу? + %s файлыг энэ төхөөрөмжөөс устгах болно + Татаж авсан файл устгагдлаа + Татаж авсан файлыг устгаж чадсангүй + Татаж авсан файлыг тоглуулж чадсангүй + Аудио татаж авах + Оффлайн тоглуулах + Татаж авах трекийг тодорхойлж чадсангүй + Татаж авах үйл явц аль хэдийн эхэлсэн байна + Аль хэдийн татаж авсан + Татаж авалт эхэллээ + Татаж авалт эхлүүлж байна… + Татаж байна %s%% + Татаж авалт дууслаа + Татаж авалт амжилтгүй боллоо + Morphe-ийн тухай + Зар + Мэдээг feeds + Ерөнхий + Тоглуулагч + Дууны үг + Бусад + YouTube Music-ийн хэл + Системийн хэл + Android 13 эсвэл түүнээс дээш хувилбар шаардлагатай + \'Music Premium авах\' шошгыг нуух + Music Premium-ийн сурталчилгааг нуух + Видео сурталчилгааг нуух + Аудио / Видео шилжүүлэгчийг нуух + Татах товчийг нуух + Подкаст шоуны Татах товчийг нууна + Хайгуурын хэсгийг нуух + Шүүлтүүрийн хэсгийг нуух + Торхэл хэсгүүдийг нуух + Хэвтээ тавиурыг нуух + Жагсаалтын тавиурыг нуух + Дууны үгний хэсэг + Хуваалцах товчийг нуух + Орчуулах товчийг нуух + \'Шинэ\' тавиурыг нуух + Т playlist-ийн талбайг нуух + Давтах товчийг нуух + Холих товчийг нуух + \"Хурдан залгах\" талбайг нуух + Сэтгэгдэл + Сэтгэгдэл хэсэгт аль хэсгүүдийг харуулахыг сонгох + Олон нийтийн удирдамжийг нуух + Контекст зурвасыг нуух + Эможи товчийг нуух + Мэдээлэл товчийг нуух + Цаг хугацааны тэмдэглэгээний товчийг нуух + Кросфейд + Дуунуудыг хооронд нь зөөлөн шилжүүлэх + Кросфейдыг идэвхжүүлэх + Fade curve + Equal power + Subtle hold + Gentle ease + Smooth S-curve + Гарах + Орох + Crossfade duration + 250 milliseconds + 500 milliseconds + 750 milliseconds + 1 second + 2 секунд + 3 секунд + 4 секунд + 5 секунд + 6 секунд + 7 секунд + 8 секунд + 9 секунд + 10 секунд + 11 секунд + 12 секунд + Гараар алгасах үед crossfade хийх + Дараагийн трек рүү гараар шилжэхэд кроссфейд эффект хэрэглэнэ + Автоматаар шилжих үед crossfade хийх + Трек дуусах үед кроссфейд эффект хэрэглэнэ + Сешн хяналт + Кроссфейдийг түр зогсоох эсвэл сэргээхийн тулд хольж тоглуулах товчлуурыг удаан дарна уу + Энэ сешн дээр кроссфейдийг түр зогсоолоо + Кроссфейдийг сэргээлээ + Кроссфейд идэвхжсэн үед видео горимыг ашиглах боломжгүй + SABR тоглолтын үед Crossfade идэвхгүй байна + Casting хийж байх үед Crossfade идэвхгүй байна + Хэрхэн ажилладаг вэ + "Crossfade нь таны тохируулсан хугацаа болон муруйг ашиглан нэг дууны төгсгөлийг дараагийн дууны эхлэлтэй давхцуулдаг + +Энэ нь автоматаар дараагийн дуу руу шилжих болон гараар алгасах үед ажиллах бөгөөд тус бүр нь өөрийн тохиргоотой. Автоматаар шилжих үед дуу дуусахаас хэдэн секундын өмнө холилт эхэлнэ" + Хязгаарлалтууд + "Crossfade асаалттай үед хөгжмийн клипүүд зөвхөн аудиогаар тоглогдоно. Crossfade-ийг зогсоож видео үзэхийн тулд shuffle товчийг удаан дарна уу + +Тоглуулалт эхэлсэн хөгжмийн клип нь дараагийн дууг хүртэл видео горимд хэвээр байна + +Fade-ийн хугацаанаас богино дуунууд crossfade хийгдэхгүй + +Түгжээтэй дэлгэц эсвэл мэдэгдэл нь шилжилтийн явцад түр зуур хуучин төлөвийг харуулж болно + +Хөгжмийн клип үзэхийн тулд crossfade-ийг зогсооход зарим видео дэлгэц хар болж болно. Дуугаа сонгохоос өмнө crossfade-ийг зогсоосноор үүнийг зайлсгаж болно" + Дэмжигдээгүй + "Аудиог дамжуулах эсвэл толинд тусгах (Chromecast, дэлгэц эсвэл аудио толь) + +SABR тоглуулалт (Видео урсгалыг дуурайх функцийг унтраасан үед)" + Зохиогч + Crossfade функцийг VazerOG бүтээсэн + Давтах төлөвийг санаж байх + Холих төлөвийг санаж байх + Тоглуулах хурд + Бүх трект ашиглагдах хурд. 100% дээр аппликейшны хурд, жишээ нь подкаст тоглуулах хурдыг ашиглана + Хурдтай хамт өнгийг өөрчлөх + Тааламжгүй гэх чиглүүлэлтийг идэвхгүй болгох + Дуу таалагдахгүй товчийг дарахад дараагийн дуу руу шилжихээс сэргийлнэ + Плейлистийн трек дээр товслох үйлдэл + Плейлистийг тоглуулах + Зөвхөн сонгосон трекийг тоглуулах + Сонгосон трекийг дарааллын жагсаалтад нэмэх + Дарааллаа нэмлээ + Cast товчийг нуух + Түүх товчийг нуух + Мэдэгдлийн товчийг нуух + Хайлт товчийг нуух + Дуу хоолойгоор хайх товчийг нуух + Дууны хайлтын товчийг нуух + Шинэ товчийг нуух + Сан хэсэг дэх Шинэ товчийг нууна + Үйлдэл хийх товчнууд + Тоглуулагчийн доор аль үйлдэл хийх товчнууд харагдахыг сонгоно уу + Үйлдэл хийх хэсгийг нуух + Таалагдах болон Таалагдахгүй товчийг нуух + Сэтгэгдэл товчийг нуух + \'Шууд чатын давталт\' товчийг нуух + Дууны үг товчийг нуух + Хуваалцах товчийг нуух + Хадгалах товчийг нуух + Татаж авах товчийг нуух + \'Радио эхлүүлэх\' товчийг нуух + Flyout цэс + Тоглуулагч болон дарааллын flyout цэсэнд аль хэсгүүдийг харуулахыг сонгох + 3 баганата компонентыг нуух + Таалагдсан болон Таалагдаагүй товчнуудыг нуух + \'Taste match\' цэснүүдийг нуух + \'Дараа сонсох\' жагсаалтад нэмэх цэсийг нуух + \'Дараалланд нэмэх\' цэсийг нуух + Хавсралт цэс нуух + \'Т playlist-ийг устгах\' цэсийг нуух + \'Дарааллыг цуцлах\' цэсийг нуух + \'Энэ уран бүтээгчийг санал болгохгүй\' цэсийг нуух + Татаж авах цэс нуух + \'Playlist-ийг засах\' цэсийг нуух + \'Альбум руу очих\' цэсийг нуух + \'Уран бүтээгч руу очих\' цэсийг нуух + \'Эпизод руу очих\' цэсийг нуух + \'Подкаст руу очих\' цэсийг нуух + Тусламж цэс нуух + \'Эпизодыг сонссон гэж тэмдэглэх\' цэсийг нуух + \'Сонирхзываетгүй\' цэсийг нуух + \'Хурдан залгах дугаарт хавсаргах\' цэсийг нуух + \'Дараагийг тоглуулах\' цэсийг нуух + Чанарын цэс нуух + \'Сангаас устгах\' цэсийг нуух + \'Жагсаалтаас устгах\' цэсийг нуух + Мэдэгдэх цэс нуух + \'Эпизодыг дараа харах\'-ыг / \'Сан руу хадгалах\' цэсийг нуух + \'Playlist-д хадгалах\' цэсийг нуух + Хуваалцах цэс нуух + \'Санамсаргүй тоглуулах\' цэсийг нуух + \'Унтлагын цаг\'-ын цэсийг нуух + \'Радио эхлүүлэх\'-ий цэсийг нуух + \'Мэргэжилтний статистик\'-ий цэсийг нуух + Бүртгүүлэх цэс нуух + \'Хурдан залгах жагсаалтаас хасах\'-ий цэсийг нуух + \'Дууны зохиогчдыг харах\'-ий цэсийг нуух + Эхлэх хуудас + Үндсэн + Чартууд + Дараа сонсох цувралууд + Хайх + Түүх + Сан + Таалагдсан хөгжим + Жагсаалтууд + Хайх + Бүртгэлүүд + Босоо чиглэлийг албадах + Төхөөрөмжийг эргүүлсэн ч аппликейшнийг босоо горимд байлгах + Мини-тоглуулагчийн өнгийг өөрчлөх + Жижиг тоглуулагчийн өнгийг бүтэн дэлгэцийн тоглуулагчийнхтай тааруулна + Навигацийн зурвасны өнгийг өөрчлөх + Навигацийн зурвасны өнгийг дүүрэн дэлгэцийн тоглуулагчид тохируулна + Миниплеер дээр дараагийн товчийг харуулах + Миниплеер дээр өмнөх товчийг харуулах + Миниплеерыг албадан идэвхжүүлэх + Шинэ дуу эхлэхэд тоглуулагчийг автоматаар бүтэн дэлгэц рүү тэлэхээс сэргийлнэ + Мини-тоглуулагчийг гүйлгэж хаахыг идэвхжүүлэх + Мини-тоглуулагч дээр доошоо гүйлгэж хаах боломжийг олгоно + Навигацийн хэсэг + Доод навигацийн хэсэгт аль товчнууд харагдахыг сонгоно уу + Нүүр хуудасны товчийг нуух + Дээжүүдийн товчийг нуух + Хайгуулах товчийг нуух + Сангийн товчийг нуух + Сайжруулах товчийг нуух + Навигацийн хэсгийг нуух + Навигацийн товчлуурын шошгыг нуух + Нэвтрээгүй байна. Тохируулахын тулд дарна уу + Гарах + Системээс гарлаа + scrobbling идэвхжүүлэх + \'Одоо тоглоод байна\' төлөвийг илгээх + Дууны хамгийн бага хугацаа + Үүнээс богино хугацаатай дуунуудыг scrobble хийхгүй + Scrobble хоцролтын хувь + Дууны энэ хувиар сонссоны дараа scrobble-ыг эхлүүлнэ + Scrobble хоцролтын секунд + Сонсож эхлээд хэдэн секундын дараа scrobble хийхийг тохируулах + ListenBrainz хаяг + Нэвтэрсэн байна. Хаягаа удирдахын тулд дарна уу + scrobbling идэвхжүүлэхийн тулд ListenBrainz хэрэглэгчийн токеноо оруулна уу + Токен + Баталгаажуулах + Токен оруулна уу + Баталгаажуулж байна… + Зөв токен! Хэрэглэгч: %s + Буруу токен: %s + тодорхойгүй + Баталгаажуулалт амжилтгүй боллоо: %s + Токен устгагдлаа + Токен хадгалагдлаа + Анхааруулга: Хадгалсан токен хүчингүй байна! + Таалагдсан дуунуудад \'Love\' тэмдэглэх + YouTube Music дээр таалагдсан дуунуудыг Last.fm дээр автоматаар \'Love\' тэмдэглэх (мөн таалагдахгүй болсон эсвэл татгалзсан тохиолдолд \'Love\' тэмдэглэгээг устгах) + Last.fm хаяг + %s хэрэглэгчээр нэвтэрсэн байна. Хаягаа удирдахын тулд дарна уу + Нэвтрэхийн тулд Last.fm-ийн нэвтрэх мэдээллээ оруулна уу + Хэрэглэгчийн нэр + Нууц үг + Нэвтрэх + Хэрэглэгчийн нэр болон нууц үгээ оруулна уу + Нэвтэрч байна… + Амжилттай нэвтэрлээ! + Last.fm-ээс сешн авах үйлдэл амжилтгүй боллоо + Нэвтрэхэд алдаа гарлаа: %s + Last.fm-ийн нэвтрэх мэдээлэл хадгалагдлаа! + Мета өгөгдлийг цэвэрлэх + Мета өгөгдлийг цэвэрлэх + Scrobbling хийхээс өмнө гарчиг, уран бүтээлч болон альбомаас түгээмэл илүүдэл тэмдэгтүүдийг (жишээ нь: \"Official Video\", \"Remaster\", \"Live\") автоматаар цэвэрлэх + Regex ашиглан цэвэрлэх + Дууны гарчиг, уран бүтээлч эсвэл альбомоос нэмэлт текстийг устгахын тулд өөрийн гэсэн регуляр илэрхийлэл оруулна уу. Жишээ нь: (?i)\\s*\\((clean|explicit)\\) + Гарчигнаас уран бүтээлч болон трекийг задлах + Видеоны гарчигнаас \" - \" тусгаарлагчийг ашиглан уран бүтээлч болон трекийн нэрийг салгаж авах. Сувгийн нэрийг уран бүтээлчийн нэр болгон ашигласан дахин оруулсан контентод хэрэгтэй + Дутуу байгаа цомгийг нөхөх + Хэрэв хөгжимд цомгийн мэдээлэл байхгүй бол Last.fm-ээс хайж scrobble-ыг тань гүйцээх болно + "Scrobbling нь таны YouTube Music-ийн ашиглалтыг Last.fm эсвэл ListenBrainz профильтай автоматаар синхрончилна + +Ард фондоор тоглуулах үед scrobbling синхрончлол хэвийн явахын тулд YouTube Music аппликейшний батерейны хэрэглээг \"Хязгааргүй\" болгож тохируулаарай" + Гуравдагч талын дууны үг ашиглах + Гуравдагч талын үйлчилгээнээс дууны үг харуулна, хэрэв олдсонгүй бол сууриглагдсан дууны үгийг ашиглана + Дууны үгийг үзэж байх үед дэлгэцийг асаалттай байлгах + Дууны үгний хэсэг нээлттэй байх үед дэлгэц унтрахаас сэргийлнэ + Дууны үгний үйлчилгээ + Харуулах + Синхрончлол + Панель + AI эгц + Бүх нийлүүлэгчид идэвхгүй болсон - суурилуулсан болон быцгадсан дууны үгс + Нийлүүлэгчийн дарааллыг шинэчлэх үү? + Нийлүүлэгчдийг анхны тэргүүлэх дарааллаар нь сэргээх + Хадгалагдсан дууны үгсийг устгах + Энэ төхөөрөмж дээр хадгалагдсан дууны үгсийг устгах + %1$s МБ цэвэрлэгдлээ + Токен авах + Apple Music Media-user-token + "music.apple.com сайтаас media-user-token кукиг хуулж тавина уу + +Алхмууд: +1. music.apple.com сайт руу гишүүн хэрэглэгчээр нэвтрэх +2. DevTools → Application → Cookies → music.apple.com цэсийг нээх +3. \"media-user-token\" утгыг хуулах" + Apple Music: media-user-token хадгалагдлаа + Apple Music: media-user-token устгагдлаа + Apple Music: media-user-token буруу байна - шалгаад дахин оролдоно уу + Spotify SpDc + "Spotify-аас sp_dc кукиг хуулж тавина уу. + +Алхмууд: +1. open.spotify.com сайт руу нэвтрэх +2. DevTools → Application → Cookies → open.spotify.com цэсийг нээх +3. \"sp_dc\" утгыг хуулах" + Spotify: sp_dc хадгалагдлаа + Spotify: sp_dc устгагдлаа + Spotify: sp_dc буруу байна - шалгаад дахин оролдоно уу + Apple Music токен нь сонголтоор оруулна. Токенгүй бол Lyrically-г нөөц хувилбараар ашиглана + Deezer ARL + "Deezer-ийн arl cookie-г хуулж тавина уу. + +Алхмууд: +1. deezer.com дээр нэвтрэх +2. DevTools → Application → Cookies → deezer.com руу орох +3. \"arl\" утгыг хуулах" + Deezer: arl хадгалагдлаа + Deezer: arl устгагдлаа + Deezer: arl буруу байна - шалгаад дахин оролдоно уу + Musixmatch токен + Зөвхөн өөрийн бүртгэлийг ашиглахын тулд токен нэмнэ үү. Энэ нь Musixmatch Android аппликейшнаас авсан байх ёстой: musixmatch.com-оос хуулсан токенууд нь хуучин десктоп клиентэд хамаардаг тул API-аас татгалзана + Musixmatch нь токенгүйгээр ажиллах боломжтой тул энэ хэсгийг хоосон орхиж болно + хэрэглэгчийн token + Musixmatch: хэрэглэгчийн token хадгалагдлаа + Musixmatch: хэрэглэгчийн token устгагдлаа + Musixmatch: хэрэглэгчийн токен буруу байна - шалгаад дахин оролдоно уу + YouTube Cookies + "YouTube-ээс хадмал орчуулга болон цаг хугацааны текстийг хүсэхийн тулд YouTube куки-г энд наанаа. + +Алхмууд: +1. Хөтөч дээр youtube.com сайтыг нээж, нэвтрэх +2. DevTools (F12) → Application → Cookies → youtube.com цэсийг нээх +3. Куки-ийн бүх мөрийг (цэгцтэй хагас цэгээр тусгаарлагдсан key=value хосууд) хуулж авах + +Шаардлагатай куки-уд: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPISID, LOGIN_INFO" + YouTube: куки-г хадгаллаа + YouTube: куки-г устгалаа + YouTube: кукис буруу байна - шалгаад дахин оролдоно уу + Мөрөн дээр дарж түрээсээр байлгах + Дууны тухайн хэсэг рүү шилжихэд мөрөн дээр дарна уу, хурууны доорх үгийг хуулж авахад удаан дарна уу, эсвэл үг дээр бус товчлуур дээр дарсан бол бүхэл бүтэн мөрийг хуулна. Унтрааснаар эдгээр үйлдлүүдийн аль аль нь ажиллахгүй болно + Хэсгийн шовнуудыг харуулах + Дууны үгний хэсэг бүрийг \"Verse\" эсвэл \"Chorus\" гэх мэт жижиг шовногоор тэмдэглэнэ + Хуулах товчлуурыг харуулах + Эх хувь дууны үгийг энгийн текст хэлбэрээр хуулахын тулд дарна уу; түүхий эх файлыг Татаж авах хавтсанд хадгалахын тулд удаан дарна уу + Орчуулах товчийг харуулах + Орчуулгыг идэвхжүүлэхийн тулд товшино; зөвхөн орчуулга харахын тулд удаан дарна. Үйлчилгээ үзүүлэгчийн орчуулга, Google Translate эсвэл OpenAI-тай нийцсэн API ашиглана + Romanize товчийг харуулах + Романизациг идэвхжүүлэхийн тулд товшино; зөвхөн романизаци харахын тулд удаан дарна. Үйлчилгээ үзүүлэгчийн романизаци, Google Translate эсвэл OpenAI-тай нийцсэн API ашиглана + Дотоод романизаци болон тохируулга + Үйлчилгээ үзүүлэгч, таны загвар болон Google Translate-ийн алдаа гарсан үед төхөөрөмж дээр романизаци хийнэ. Уншилт нь цаг хугацаагаар тохируулсан дууны үг бүрийн дараа гарна; үг синхрончлол идэвхтэй байх ёстой. Хятад, Кантон, Япон, Солонгос, Грек, Орос, Георги, Армени; ойролцоогоор 120 МБ + %1$s уншиж байна: %2$d%% + Татаж авахад алдаа гарлаа - дахин оролдохын тулд үүнийг унтраагаад асаана уу + Refresh товчийг харуулах + Дараагийн дууны үг рүү шилжихэд товшино; дууны нэр болон уран бүтээгчээр хайхын тулд удаан дарна + Дууны үг хайх + Дууны нэр + Уран бүтээлч + Дууны нэр болон уран бүтээлчийг оруулна уу + Идэвхжүүлсэн нийлүүлэгчдийн хэн нь ч бусад дууг хайж чадахгүй байна + Хадгалагдсан + Цаг хугацааны синхрончлолтой үг эсвэл үег тодруулах + Боломжтой үед үг бүрээрх дууны үгийг давуу эрхээр харуулна. Дууны үг нь үг бүрийн хугацааг агуулсан тохиолдолд хөгжимтэй зэрэгцэн үг эсвэл үе бүрийг тодруулна + Дуурьссан үгсийг нуух + Одоо тоглож буй мөрөөс дээгүүрх дууны үгсийг нуух + Дуулаагүй үгсийг нуух + Одоо тоглож буй мөрөөс доогуурх дууны үгсийг нуух + Түгээн дэлгэц дээр MediaSession-ий дууны үгийг харуулах + Түгжээтэй дэлгэц, Android Auto болон Bluetooth дээр одоогийн мөрийг харуулна. Android 11 эсвэл түүнээс шинэ хувилбар шаардлагатай + Мини-тоглуулагчийн дууны үгийг харуулах + Мини-плейер дээр одоогийн мөрийг харуулна, трек нь хавсрагт гарчиг руу шилжинэ + Дууны нэр/уран бүтээлчийн формат + Мини-тоглуулагч болон MediaSession-д дууны үгтэй хамт харуулах трекийн формат. {t} нь гарчиг, {a} нь уран бүтээлч, {v} нь хувилбар (жишээ нь: Acoustic) юм. Хоёр цэгтэй хэсгийн текст нь доторх бүх хувьсагчид утгатай үед л харагдах бөгөөд энгийн цэг тавихын тулд \\: гэж бичнэ. Дууны үг өөрчлөгдөх бүрт мөр бүр дарааллын дагуу харагдана + Дууны үгийн мөрийн формат + Мини-тоглуулагч болон MediaSession-д харуулах дууны үгний мөрийн формат. {lrc} нь мөр, {n} нь тухайн мөрийг хэдэн удаа дараалан дуулсныг заана. Колонны хоорондох текст нь зөвхөн мөр давтагдах үед харагдана. Колон ашиглах бол \\: гэж бичнэ + Формат нь {lrc}-ийг заавал агуулсан байх ёстой + Уран бүтээгчийн мөрийн загвар болон хөдөлгөөний хугацаа + Дэлгэц дээр харуулах хамгийн бага хугацаа (мс) + Дэлгэц дээр харуулах хамгийн их хугацаа (мс) + Зөвхөн олон мөр бүхий хөдөлгөөнд зориулсан. Фрэйм бүрийг хамгийн багадаа заасан хугацаагаар харуулах. Хамгийн их хугацаандаа хүрмэгц дууны үг хэвээр байсан ч уран бүтээгчийн мөрийг урагшлуулах. + Хамгийн бага утга нь 0 эсвэл түүнээс их байх ёстой. Хамгийн их утга нь 0-ээс их бөгөөд хамгийн бага утгаас багагүй байх ёстой. + Мөрний уртын хязгаар + Дууны үг болон уран бүтээлчийн мөрний хязгаар, мөн бүтэн өргөн тэмдэгтийн жин + "Дууны үгний мөрний хязгаар +Урт дууны үгийг тэнцвэртэй хоёр хувааж зааглана. 0 нь хуваах үйлдлийг идэвхгүй болгоно." + "Уран бүтээлчийн мөрний хязгаар +Бөглөсөн трекийн форматыг ээлжлэн гардаг фреймүүдэд хуваана. 0 нь хуваах үйлдлийг идэвхгүй болгоно." + "Бүрэн өргөний тэмдэгтийн жин +Хоёр хязгаарлалтад хоёуланд нь хамаарна. 1.75 дээр дөрвөн бүрэн өргөний тэмдэгтийг 7 гэж тооцно." + 0-оос 10000 хүртэлх бүхэл тоон хязгаарлалт болон 0-ээс их тэмдэгтийн жинг оруулна уу + Хөгжмийн завсарлага + Хөгжмийн завсарлага үеэр Miniplayer болон MediaSession-д харагддаг. Мөр бүр нь нэг хөдөлгөөний фрейм юм. {t} нь гарчиг, {a} нь уран бүтээлч, {lrc} нь дууны сүүлчийн мөр юм + Miniplayer болон MediaSession-ий хугацааны зөрүү + Miniplayer болон MediaSession дээр дараагийн мөр харагдах хугацааг шилжүүлнэ. Хэрэв мөрүүд хожуу гарч байвал, жишээ нь түгээн дэлгэц дээр, сөрөг утга ашиглана уу + Шүүлтүүрүүд + Мета өгөгдлийн regex + Дууны үгийг хайх болон хоёр гарчгийг харьцуулах үед гарчиг, уран бүтээлч, альбомыг шүүхэд ашиглагдах регуляр илэрхийлэл. Идэвхгүй болгохын тулд хоосон орхино. Анхны тохиргоо нь \"Official Video\", \"Remaster\", \"【插曲】\", \"- Topic\" гэх мэт түгээмэл илүүдлийг устгана + Дууны үг шүүлтүүрийн regex + Зөвхөн эх хувилбарын дууны үгэнд (орчуулга эсвэл романчлал биш) хэрэглэгдэх регүүлэр экспрешн. Идэвхгүй болгохын тулд хоосон орхино уу + Мэдээллийн мөрийн тэмдэгтүүд + Хэрэв дууны үг буруу шүүгдэж доор нь гарч байгаа бол үүнийг тохируулна уу эсвэл устгана уу. Таслалаар тусгаарласан тэмдэглэгээнүүд: тэмдэглэгээ болон цэгтэй тэмдэгт агуулсан мөрүүдийг доор нь зээрийн мэдээлэл болгон харуулна. Идэвхгүй болгохын тулд хоосон орхино уу + Эхний үсгийг том болгох + Дууны үг бүрийн мөрийн эхний үсгийг том болгоно + Төгсгөлийн цэг тэмдэгтүүдийг устгах + Дууны үг бүрийн мөрийн төгсгөлдある цэг, таслал, хоёр цэг болон цэгтэй таслалыг устгана. Асуух тэмдэг, анхааруулга тэмдэг, нуршуу цэг, хашилт болон хаалтыг хэвээр үлдээнэ + Текстийн хэмжээ + Дууны үгний текстийн хэмжээ + Цаг хугацааны ерөнхий зөрүү + Синхрончлосон дууны үг тоглуулалтыг дагана. Хэрэв зөрүү гарвал хугацааны зөрүүг тохируулна уу. Синхрончлосон үгийг урагш эсвэл ухрааж шилжүүлнэ. Хэрэв дууны үг нь дууны урсгалаас түрүүлж байвал эерэг утга ашиглана уу + Дууны үгний зөрүүдлийг SponsorBlock сегментүүдийг ашиглан тохируулах + SponsorBlock-оор алгассан хэсгүүдийг хасаж тохируулж, дууны үгийг гаргасан аудио дээр шилжүүлнэ. Off тохиргоо нь SponsorBlock-ийг бүрэн үл тоомсорлоно + This track has no video id to submit + Эх сурвалж: %s + Энэхүү LRC-ийг хадгалахын тулд хуулах товчийг удаан дарна уу; хэрэв та цаг хугацааг нарийвчлан засаад нийгэмлэг нийлүүлэгчдэд хуваалцвал бид талархах болно + Google-ээр орчуулсан + Google-ээр романизаци хийсэн + %s-ээр орчуулсан + %s-ээр романизаци хийсэн + %s-ээр романизаци хийсэн + Орчуулагдсан + Орчуулгын хэл + Романизацийн хэл + Орчуулж байна… + Дууны үгийг орчуулж чадсангүй + Романизаци хийгдсэн + Автомат + Романизаци хийж байна… + Дууны үгийг романизаци хийж чадсангүй + Зөвхөн орчуулга + Зөвхөн латин галиг + Хос горим + Санах ойд хуулагдлаа + Шинэчилж байна + Шинэчиллээ + QuickLRC API түлхүүр + QuickLRC нь одоогийн трекийн дууны үгийг хөрвүүлдэг эсвэл хугацаанд нь тааруулдаг. Энэ нь дээрх OpenAI-тай нийцтэй төгсгөл цэгтэй хамааралгүй тусдаа үйлчилгээ юм + QuickLRC түлхүүрийг хадгаллаа. Дууны үг хайх цонхны \"align\" товчлуур эсвэл трект дууны үггүй үед гарч ирэх панелийн зааврыг ашиглана уу. + QuickLRC API түлхүүрийг хадгаллалаа + QuickLRC уг түлхүүрийг хүлээн зөвшөөрөөгүй + QuickLRC API түлхүүрийг устгалаа + QuickLRC нь дууны үггүй трекүүдийг кредит зарцуулж хугацаанд нь тааруулдаг эсвэл трекээс хөрвүүлдэг. Түлхүүр тохируулсан тохиолдолд дууны үггүй трекүүд хөрвүүлэлт санал болгох бөгөөд хайлт хийх цонхонд alignment товчлуур нэмэгдэнэ. + QuickLRC-ийн тэнхрууллыг хүсэх + Тэнхруулж байна + Тэнхрууллыг хэрэглэх + QuickLRC-ийн транскрипцийг хүсэх + Транскрипци хийж байна + Тэмдэглэлийг цуцаллаа + Зөвхөн YouTube-ээс тоглосон дуунуудыг тэмдэглэх боломжтой + Тааруулах дууны үг алга - оронд нь хөрвүүлж байна + QuickLRC энэ дууг боловсруулж чадсангүй + Энэ дууны хувьд QuickLRC-ийн эрх дууссан байна + "Дууны үгийг гарчиг болон уран бүтээлчийн нэрээр хайх бөгөөд мета өгөгдөл буруу байх тохиолдолд хайлт амжилтгүй болох эсвэл буруу хувилбар гарч ирж болзошгүй. Хувилбарыг солихын тулд шинэчлэхийг ашиглана уу. * тэмдэглэгээтэй нийлүүлэгчдэд токен шаардлагатай. Идэвхжүүлсэн бүх нийлүүлэгчдэд таны заасан дарааллаар хүсэлт илгээхгүй бөгөөд хариу өгөх хурдаас нь хамаарч харуулах болно.\n\nДууны үг нь Morphe-тэй хамааралгүй, түүний ивээн тэтгэлгэсэн эсвэл дэмжсэн бугуй хөндлөнгийн үйлчилгээнээс ирдэг. Үйлчилгээ бүрийг байгаагаар нь ашиглах бөгөөд дууны үг, тэдгээрийн хүртээмж болон лиценз нь тухайн үйлчилгээ болон лиценз эзэмшигчийнх бөгөөд тэдгээрийг ашиглах нь тухайн үйлчилгээний үйлчилгээний нөхцөлд зохицуулагдана. Идэвхжүүлсэн үйлчилгээнд зөвхөн таны хүсэлт болсон хайлт буюу гарчиг, уран бүтээлч болон шаардлагатай тохиолдолд видео ID илгээгдэнэ. Нэмэлт AI эгц болон QuickLRC транскрипц нь таны тохиргооны дараа л ажиллах бөгөөд дууны үгийн текст эсвэл трекийн видео URL-ыг хүлээн авна." + Зөвхөн ашигладаг Źүрүүлэгчүүдээ идэвхжүүлж эсвэл анхны тохиргоогоор үлдээгээрэй - байнгын хүсэлтүүд нь жижиг олон нийтийн дууны үгний сайтуудыг хэт ачаалж болзошгүй + Зээрийн мөрийг нуух + Дууны үгний доор харагдаж буй зээрийн мөрийг нуух + Орчуулга болон романизацийг солих + ЧАЛУУНААСАН үед романизацийг дууны үгний доор, орчуулгыг дээр нь байрлуулна; УНТРУУЛСАН үед романизацийг дээр, орчуулгыг доор нь байлгана + Орчуулга + Романизаци + Орчуулгын үйлчилгээ үзүүлэгч + Google (Үндсэн, Үнэгүй) + OpenAI-тай нийцтэй (Заалттай) + Романизацийн үйлчилгээ үзүүлэгч + Google (Өгөгдすることで) + Дотоод / Офлайн тольбиур + OpenAI-тай нийцсэн (Захиалгаар) + AI хэсэг + Зөвхөн нийлүүлэгч Chorus эсвэл Verse гэх мэт хэсгийн шовруудыг илгээгээгүй, OpenAI-тэй нийцэх загвар чинь тэдгээрийг илрүүлдэг тохиолдолд ашиглана + AI & Cloud Endpoints + AI үйлчилгээ үзүүлэгчдийн API түлхүүр, модель, endpoint болон prompt загварыг тохируулна. + API endpoint URL (жишээ нь: https://api.openai.com/v1/chat/completions) + Prompt загвар + Орлуулах утгууд: {language}, {task}, {title}, {artist}, {count}, {lines} + Промпт хадгалагдлаа + Заавар хадгалагдсан боловч AI төгсгөл цэг хараахан ашиглах боломжгүй байна. Эхлээд AI тохиргоо хэсэгт тохируулна уу. + Промптыг шинэчлэлээ + JSON хүчингүй байна. Хадгалахын өмнө засах эсвэл талбарыг хойвгоруулна уу + Орчуулга & Романизацийн тохиргоо хадгалагдлаа + Тохиргоог анхны төлөвт нь буцаалаа + Үндсэн URL нь http:// эсвэл https://-ээр эхлэх ёстой + Холболтыг шалгах + Төгсгөлийн цэгүүдийг тохируулах + Бүх төгсгөлийн цэгүүдийг харуулах + Холболтыг шалгаж байна… + Орчуулгын үйлчилгээ үзүүлэгч + Романизацийн үйлчилгээ үзүүлэгч + Загваруудыг татах + Загваруудыг татаж байна… + Загвар сонгоно уу + Моделүүдийг татах боломжгүй боллоо. API түлхүүр эсвэл холболтоо шалгана уу. + Gemini API-ийн тохиргоо + OpenAI-тай нийцэх (Custom Endpoint) + DeepL API-ийн тохиргоо + API түлхүүр + Загвар + Үндсэн URL + API түлхүүр (сонгомжтой) + Орчуулгын заавар + Нэмэлт хүсэлтийн бие (сонгомжтой JSON) + Баталгаажуулалтын түлхүүр (Free эсвэл Pro) + API түлхүүр, загвар, төгсгөл цэг болон заавруудыг тохируулах + 8.23.51 - Тод дүрсүүдийг идэвхгүй болгох + 9.13.51 - Тоглуулагчийн Дараагийн, Дууны үг болон Холбоотой цэсүүдийг сэргээх diff --git a/patches/src/main/resources/addresources/values-mn-rMN/reddit/strings.xml b/patches/src/main/resources/addresources/values-mn-rMN/reddit/strings.xml index cf7776fdaf..46633898dc 100644 --- a/patches/src/main/resources/addresources/values-mn-rMN/reddit/strings.xml +++ b/patches/src/main/resources/addresources/values-mn-rMN/reddit/strings.xml @@ -15,4 +15,17 @@ Second \"item\" text" --> - + + Reddit-ийн нууцлалын бодлого + Google-ээр нэвтрэх боломжгүй байна. Имэйл ашиглан нэвтэрнэ үү + Өөрийн загварчласан фонт + Доорх замаас өөрийн загварчласан TTF эсвэл OTF фонт файлыг ачаална. Үүнийг идэвхжүүлснээр \"Системийн фонтыг албадлах\" সেটিнг унтраана + Өөрийн загварчласан фонт файл + TTF эсвэл OTF фонт файл сонгохын тулд дарна уу + Сонгосон фонт - %s + Эхлээд өөрийн загварчласан фонт файл сонгоно уу + Тухайн .ttf эсвэл .otf фонт файлыг сонгоно уу + Өөрийн загварчласан фонтыг ачаалахад алдаа гарлаа. Өөр файл сонгоно уу + Системийн фонтыг албадлах + Reddit Sans эсвэл Roboto-ийн оронд төхөөрөмжийн системийн фонтыг ашиглана. Үүнийг идэвхжүүлснээр \"Өөрийн загварчласан фонт\" সেটিнг унтраана + diff --git a/patches/src/main/resources/addresources/values-mn-rMN/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-mn-rMN/shared-youtube/strings.xml index fd007eeb78..615d21ba90 100644 --- a/patches/src/main/resources/addresources/values-mn-rMN/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-mn-rMN/shared-youtube/strings.xml @@ -18,46 +18,298 @@ Second \"item\" text" --> + Эхлэлийн хเคลื่อนхүү дүрслэл + Идэвхгүй + Өнгө + Монохром + + Тохиргоог дуусгахын тулд дахин ачаална уу + Апп-ын нэр + Харанхуй сэвийн арын дэвсгэр + Цэвэр хар + Material You (Цэвэр хар) + Сонгодог YouTube + Баримгяа ягаан + Баримгяа цэнхэр + Баримгяа ногоон + Баримгяа шар + Баримгяа улбар шар + Баримлсан улаан + Бүтээлчээр тохируулсан бараан дэвсгэр + Бараан сэвэргийн дэвсгэр өнгийг тохируулах + Аппликейшний үндсэн + Material You (Тэглүүр) + Material You (Үндсэн) + Material You (Нэмэлт) + Material You (Гуравдагч) + Бүтээлчээр тохируулсан + Татаж авах + Татах горим + Өгөгдрьсөн + Апп дотор татах + Гадаад татах программ + Татаж авах программын багцын нэр + Таны суулгасан гадны татаж авах апп-ын багцын нэр + Багцын нэрийг оруулна уу + Бусад + Апп суулгаагүй байна + %s суулгаагүй байна. Та суулгана уу + "Багцын нэрээр суулгасан апп-ыг олж чадсангүй: %s + +Багцын нэр зөв эсэхийг болон апп суулгасан эсэхийг шалгана уу" + Багцын нэр хоосон байж болохгүй + Захиалгаар + Аппликейшны дүрс + Эх хувилбар + Morphe хар + Morphe бараан + Morphe цайвар + Morphe play light + Morphe play хар + Захиалгаар + "Энэ нь таны апп лунчер дээр харагдах дүрсгэлийг өөрчилнө. + +Энэхүү тохиргоо нь дараах газруудын аль алинд нь дүрсгэлийг өөрчлөхгүй: +• Төхөөрөмжийн системийн тохиргооны апп-ын жагсаалт +• Зарим түлхлэлт мэдэгдэл + +• 'Open with' апп сонгогч +• Android Auto дүрсгэл + +Дүрсгэлийг бүх газарт өөрчлөхийн тулд дахин патч хийж, 'App icon' патч сонголтыг өөрчилнө үү" + Мэдэгдлийн дүрс + Апп-ын дүрстэй тааруулах + Morphe play + Morphe-ийн мэдэгдэл + Таны үзсэн түүх хадгалагдахгүй байна.<br><br>Энэ нь ихэвчлэн DNS зар шугалуулагч эсвэл сүлжээний прокси-оос болсон байж болно.<br><br>Үүнийг засахын тулд <b>s.youtube.com</b> хаягийг зөвшөөрөгдсөн жагсаалтад нэмэх эсвэл бүх DNS шугалуулагч болон прокси-г унтраана уу + Дахин бүү харуул + Видео урсгалыг дуурайх + Тоглуулах үеийн алдаанаас сэргийлэхийн тулд клиентийн хуурамч дүрслэлийг тохируулах + "Тохиромжтой видео урсгалыг хүсэж, ихэнх хэрэглэгчдийн тоглуулах асуудлыг засна + +Хэрэв та YouTube Premium хэрэглэгч бол энэ тохиргоо шаардлагагүй байж болно" + Клиент стриймүүдийг татаж авж чадсангүй + Үндсэн клиент ажиллахгүй байна + "%1$s дарааллан хэд хэдэн удаа ажиллаж чадсангүй, харин %2$s ажилласан байна. + +%2$s-ийг үндсэн клиент болгох уу?" + Сэлгэх + Энэ тохиргоог унтраах нь тоглуулалтын алдаа үүсгэж болзошгүй + Дараах тохиргоог идэвхжүүлж үзнэ үү: Видео стрийм дуурайх > Android VR-т нэвтрэх + Үндсэн клиент + Android VR-т нэвтрэх + Android VR-т нэвтрээгүй байна. Нэвтрэхийн тулд энд дарна уу + Одоогоор Android VR-т нэвтэрсэн байна + Энэ функцийг ашиглахын тулд \'Үндсэн клиент\'-ийг Android VR болгож өөрчилнө үү + Android VR-т нэвтрэх + "Android VR-т нэвтрэх нь зарим хэрэглэгчдийн тоглуулах үйлдлийн алдааг засах боломжтой + +Нэвтрэхийн тулд доорх үргэлжлүүлэх товчийг дарна уу" + Гарах + Зааварчилгаа + "Идэвхжүүлэлтийн код: %s + +1. \"Төхөөрөмж холбох\" вэбсайтыг нээхийн тулд доорх 'Вэбсайт нээх' дээр дарна уу +2. Дээрх идэвхжүүлэлтийн кодыг хуулж тавина уу (эсвэл гараар бичнэ үү) +3. Шаардлагатай бол Google-д нэвтэрнэ үү. Хэрэв танд олон бүртгэл байгаа бол энэхүү YouTube бүртгэлийг сонгоно уу +4. 'YouTube VR'-д зөвшөөрөл олгох дээр дарна уу +5. 'Төхөөрөмж холбогдлоо' гэсэн мэдээлэл гарсны дараа энэ аппликейшн руу буцна уу" + Идэвхжүүлэлтийн код олгоход алдаа гарлаа + Баталгаажуулалтын токен олгоход алдаа гарлаа + Амжилттай нэвтэрлээ + "Баталгаажуулалт хадгалагдлаа + +• Таны Morphe тохиргоо Android VR баталгаажуулалтыг импортлах/экспортлохгүй +• Хэрэв энэ аппликейшнийг 6 сарын турш ашиглаагүй бол Google энэ баталгаажуулалтыг устгах болно +• Хэрэв та Google нууц үгээ сольсон эсвэл Google тохиргооноосоо энэ төхөөрөмжийг хассан бол дахин нэвтрэх шаардлагатай болно" + OAuth2 түр хугацаагаар боломжгүй байна (API-ийн хугацаа дууссан) + Android VR нэвтрэх хугацаа дууссан байна, VR-т дахин нэвтрэхийг оролдоно уу + OAuth2 түр хугацаагаар боломжгүй байна (төлөв %s) + OAuth2 түр хугацаагаар боломжгүй байна + VR-т нэвтэрч чадсангүй, вебсайтын баталгаажуулалт зөвшөөрөгдөөгүй байна + VR-т нэвтэрч чадсангүй: %s + Android VR-ээс гарлаа + VR нэвтрэх токен хүчингүй байна, VR-т дахин нэвтрэхийг оролдоно уу + Мэдэгдлийн медиа удирдлага + Түгээн дэлгэц болон мэдэгдлийн хэсэгт харагдах тоглуулах удирдлагуудыг тохируулна уу + Өмнөх & Дараагийн товчлууруудыг нуух + Медиа мэдэгдэл, түгээн дэлгэц болон чихэвчний удирдлага дээрх Өмнөх болон Дараагийн товчлууруудыг нууна + Хайх түүрийг идэвхгүй болгох + Түгээн дэлгэц болон мэдэгдлийн хэсэг дээрх хайх түүрийг чирж өөрчлөхийг хязгаарлана + Өмнөх & Дараах товчлууруудаар ухраах/урагшлуулах + Мэдэгдэл, түгжээтэй дэлгэц, чихэвчний удирдлагыг видеог солихын оронд ухраах/урагшлуулах болгон өөрчилнө + Ухраах хугацаа + Өмнөх товчлуураар ухраах секунд + Урагшлуулах хугацаа + Дараах товчлуураар урагшлуулах секунд + Хайлтын хугацаа 1-ээс %s секундын хооронд байх ёстой. Үндсэн утгыг ашиглана уу + QUIC протоколыг идэвхгүй болгох + DRC аудиог идэвхгүй болгох + Эх хэлний аудиог албадан ашиглах + Дууршуулалтын хувилбарын оронд видеоны эх аудио замыг үргэлж тоглуулах + Чимээгүй хэсгийг алгасах + Видеоны дууны чимээгүй хэсгүүдийг автоматаар алгасна + Түр зогсолтын хамгийн бага хугацаа + Зөвхөн энэ хугацаанаас дээшх тэмдэглэгээг богиносгоно. Дараагийн видео эхлэхэд хэрэгжиж эхэлнэ + Сүлжээний прокси + Дэмжигдсэн сүлжээний хүсэлтүүдийг проксигоор дамжуулах + Проксиг идэвхжүүлэх + Сүлжээний хүсэлтүүдийг тохируулсан HTTP эсвэл HTTPS проксигоор дамжуулна + Прокси хост + Прокси серверийн хост нэр эсвэл IP хаяг + Прокси порт + Прокси серверийн порт + HTTPS прокси ашиглах + Прокси сервертэй холбогдохдоо HTTP-ийн оронд HTTPS ашиглах + Прокси баталгаажуулалт + Прокси хүсэлттэй хамт Basic баталгаажуулалтыг илгээнэ + Прокси хэрэглэгчийн нэр + Прокси-ийн Basic баталгаажуулалтын хэрэглэгчийн нэр + Прокси нууц үг + Прокси-ийн Basic баталгаажуулалтын нууц үг + Шууд холболтод шилжүүлэхийг зөвшөөрөх + Прокси алдаа гарвал шууд холбогдох + Дүүрэн дэлгэцэн заруудыг нуух + Энэ функцийг ашиглахын тулд \'Spoof video streams\'-ийг \'Android Studio\' болон \'Android VR Downgraded\'-ээс бусад клиент рүү өөрчилнө үү + Хуваалцах холбоосыг цэвэрлэх + Хуваалцсан холбоосуудаас хяналтын хайлтын параметрэг устгана + Хуваалцах холбоосыг youtube.com болгон өөрчлөх + Хуваалцсан холбоосууд дахь music.youtube.com-ыг youtube.com-оор сольно + Хуваалцах холбоосуудыг youtu.be болгож өөрчлөх + Видеог youtu.be богино URL форматаар ашиглахыг шаардана + Debug log protocol buffer-ийг шалгах + Хөгжүүлэгчид апп-ын бүрэлдэхүүн хэсгүүдийг тодорхойлохын тулд нэмэлт лог мэдээлэл нэмнэ + "Энэ тохиргоог идэвхжүүлснээр зарим UI бүрэлдэхүүн хэсгүүдийн дэлгэц дээрх текст зэрэг нэмэлт бүтцийн өгөгдлийг бүртгэнэ. + +Энэ нь тусгай шүүлтүүр үүсгэхэд бүрэлдэхүүн хэсгүүдийг тодорхойлоход тусална. + +Гэвч үүнийг идэвхжүүлснээр таны IP хаяг гэх мэт зарим хэрэглэгчийн өгөгдлийг мөн бүртгэх болно" + Тусгай шүүлтүүр + Ямар ч бүрэлдэхүүн хэсгийг нуухын тулд тусгай шүүлтүүрийг тохируулна уу + Тусгай шүүлтүүрийг идэвхжүүлэх + Шүүлтүүрт ашиглах компонент замын бүтээгчийн мөрүүдийн жагсаалтыг шинэ мөрөөр тусгаарлан оруулна уу + Буруу төгсгөлгүй шүүлтүүр: %s + Тохиргооны цэсний шүүлтүүр + Аппликейшний үндсэн тохиргооны дэлгэц дээрх зүйлсийг харагдах нэрээр нь нуух + Жагсаалтыг бөглөхийн тулд аппликейшний үндсэн тохиргооны дэлгэцийг нэг удаа нээнэ үү + Үзэгчийн анхааруулга хайрцгийг устгах + Энэ нь насны хязгаарлалтыг алгасахгүй. Зөвхөн автоматаар зөвшөөрөх болно + PoToken нийлүүлэгч + Тоглуулах үеийн алдаанаас сэргийлэхийн тулд PoToken нийлүүлэгчийг тохируулна уу + Гадаад PoToken нийлүүлэгч + "Гадаад PoToken minter ашиглан тоглолтын алдааг засна (Туршилттай) + +Хэрэв та YouTube Premium хэрэглэгч бол энэ тохиргоо шаардлагагүй байж магадгүй" + "Гадаад PoToken minter ашиглан тоглуулах алдааг засах (Туршилтын) + +Энэхүү функцийг ашиглахын тулд 'Видео урсгалыг хуурах' тохиргоог унтраана уу" + Хэрэв ажиллахгүй бол MicroG-ийг албадан хааж үзнэ үү + PotHelper тохиргоог нээх + "Энэхүү функц нь одоогоор хөгжүүлэлтийн шатандаа байгаа тул ажиллахгүй байж магадгүй + +PotHelper татахын тулд энд дарна уу" + PotHelper-ийн хугацаа дууссан + "PotHelper-ийн энэ хувилбарт суурилуулсан токен ажиллахаа больсон + +Тоглуулалтыг засахын тулд 'Видео урсгалыг хуурах' тохиргоог асаана уу" + Асаах + Аппликейшны хувилбарыг хууран мэхлэх + Хуучин UI функцуудыг сэргээхийн тулд таныг аппликейшны хуучин хувилбар ашиглаж байна гэж серверт итгүүлнэ + "Аппликейшны хувилбарыг хуучин хувилбар болгон хууран мэхлэнэ. + +Энэ нь аппликейшны харагдац болон функцийг өөрчлөх боловч тодорхойгүй гаж нөлөө гарч болзошгүй. + +Хэрэв дараа нь унтраавал UI алдаанаас урьдчилан сэргийлэхийн тулд аппликейшны өгөгдлийг устгахыг зөвлөж байна" + Аппликейшны хувилбарыг хуурах + Аппликейшны шинэчлэлтийн хурд + Үндсэн + %s FPS + Шинэчлэлтийн хурдыг өөрчлөх + Үргэлж + Босоо тоглуулах үед + Тоглуулалтыг бүтэн дэлгэцээр + Тоглуулалтыг босоо болон бүтэн дэлгэцээр + Фонт + Үндсэн + Төхөөрөмжийн системийн фонт + Өөрийн фонт файл + Өөрийн фонт файл + TTF эсвэл OTF фонт файл сонгоно уу + Сонгосон файл нь TTF эсвэл OTF фонт биш байна + Загвар фонтыг ачаалж чадсангүй тул унтраалаа + Таалагдаагүй үзүүлэлт түр зуур боломжгүй (API хугацаа дууссан) + Таалагдаагүйцүүдийг ашиглах боломжгүй (төлөв %s) + Таалагдаагүй үзүүлэлт боломжгүй (client API хязгаарлалт) + Үгүйсгэлийг ашиглах боломжгүй (%s) + Эзэмшигчээр нуугдсан + Энэ бичлэгийг бусад %s хүнтэй хамт таалахгүй гэх + Энэ бичлэгийг таалахгүй гэх, сануудын %s нь таалагдаагүй байна + Return YouTube Dislike-ийг идэвхжүүлэх + Үгүйсгэлийг хувиар харуулах + Үгүйсгэлийг тоогоор бус, таалагдсан тооны хувиар харуулна + Таалагдсан тооны тооцооллыг харуулах + Зохиогч таалагдсан тоог идэвхгүй болгосон видеонуудад таалагдсан тооны тооцооллыг харуулна + Холболтын алдааны мэдэгдлийг харуулах + Return YouTube Dislike серверийн холболтгүй үед мэдэгдэл харуулна + "Return YouTube Dislike нь YouTube-ийн 2021 онд нуусан dislike-ийн тоог сэргээдэг. Тооцоолол нь устгахаас өмнөх архивлагдсан өгөгдлөөс хамаарах бөгөөд шинэ видеонуудын хувьд хэрэглэгчдийн саналаас тооцоолдог тул ойролцоо байх бөгөөд хэдэн өдрөөр шинэчлэгдэнэ + +Таны үзэж буй видеоны ID-г Return YouTube Dislike API-руу илгээх бөгөөд таны like болон dislike-ийн мэдээллийг тооцооллыг сайжруулахын тулд нэрээ нууцлан илгээнэ" + Энэ төхөөрөмжийн Return YouTube Dislike API статистик + API-ийн хариу өгөх хугацааны дундаж + API-ийн хариу өгөх хугацааны хамгийн бага + API-ийн хариу өгөх хугацааны хамгийн их + API-ийн хариу өгөх хугацаа, сүүлчийн медиа + Таалагдаагүй тоо түр хугацаагаар боломжгүй байна - Client API-ийн хүсэлтийн хязгаарлалт хэрэгжиж байна + API-аас санал авах дуудлагын тоо + Сүлжээний дуудлага хийгээгүй + %s сүлжээний дуудлага хийсэн + API-аас саавал авах, цаг хугацаа дууссан тоо + Сүлжээний дуудлага цаг хугацаа дуусаагүй + %s сүлжээний дуудлагын хугацаа дууссан + API клиентийн хүсэлтийн хязгаарлалт + Клиентийн хүсэлтийн хязгаарлалтад тулгаагүй + Клиентийн хүсэлтийн хязгаарлалт %s удаа гарсан + %s миллисекунд diff --git a/patches/src/main/resources/addresources/values-mn-rMN/shared/strings.xml b/patches/src/main/resources/addresources/values-mn-rMN/shared/strings.xml index 31cdbe4dcb..7479f83f93 100644 --- a/patches/src/main/resources/addresources/values-mn-rMN/shared/strings.xml +++ b/patches/src/main/resources/addresources/values-mn-rMN/shared/strings.xml @@ -17,21 +17,219 @@ Second \"item\" text" --> + Тохиргоо + Та үнэнхүү үргэлжлүүлэх хүсэлтэй байна уу? + Хадгалах + Сэргээх + Өнгийг сэргээх + Буруу өнгө + Дахин ачаалах шаардлагатай + Энэхүү өөрчлөлтийг хэрэгжүүлэхийн тулд аппликейшныг дахин ачааллана уу + Дахин ачаалах + Импортлох + Хуулах + Morphe тохиргоог анхны хэвээр сэргээлээ + %s тохиргоог импорт хийлээ + Импортлж чадсангүй: %s + Дээшээ шилжих + Тохиргоо хайх + Тухай + \'%s\' гэсэн үр дүн олдсонгүй + Өөр түлхүүр үг туршаад үзнэ үү + Сүүлийн хайлтууд + Хайлтын түүхээс устгах уу? + Хайлтын түүхээс устгах + Хайлтын түүхийг цэвэрлэх + Хайлтын бүх түүхийг цэвэрлэх гэж байгаадаа итгэлтэй байна уу? + Хайлтын зөвлөмж + "• Зам дээр дарж очих +• Тохиргоог удаан дарж очих +• Enter дарж хайлтын хүсэлтээ түүхэнд хадгалах +• Хайлт нь том жижиг үсэг болон цэг тэмдэгт харгалзахгүй +• Эх тохиргоо нь идэвхгүй дотоод тохиргоонуудын дээр гарч ирнэ" + Хайлтын түүх хоосон байна + Хайлтын түүхийг хадгалахын тулд хайлтын хүсэлтээ бичиж Enter дарна уу + Тохиргооны хайлтын түүхийг харуулах + Morphe тохиргооны нэр + Анхны + Нэмэлт + Засвар + Хувирлын хугацаа + Өөрийн гэсэн + Нэрээ оруулна уу + Нэрийг хоосон орхиж болохгүй + Morphe тохиргооны дүржлэгүүдийг харуулах + Хууллагдлаа: %s + Morphe хэл + "Зарим хэлний орчуулга дутуу эсвэл бүрэн буш байж болзошгүй. + +Шинэ хэл дээр орчуулах эсвэл байгаа орчуулгаа сайжруулахын тулд morphe.software/translate хаягаар зочилно уу" + Аппликейшний хэл + Импорт / Экспорт + Morphe тохиргоог импортлох / экспортлох + Файлаас импортлох + Тохиргоог амжилттай импортлолоо, хэрэгжүүлэхийн тулд Хадгалах товчийг дарна уу + Тохиргоог импортлох үйлдэл амжилтгүй боллоо + Файл руу экспортлох + Тохиргоог амжилттай экспортлолоо + Тохиргоог экспортлох үйлдэл амжилтгүй боллоо + Та Morphe Patches-ийн хамгийн сүүлийн <i>%s</i> хувилбарыг ашиглаж байна + Та Morphe Patches-ийн <i>%1$s</i> хувилбарыг ашиглаж байна<br><br><b>Шинэчлэлт боломжтой: <i>%2$s</i></b><br><br>Патч-аа шинэчлэхийн тулд Morphe-ийг ашиглан энэ аппликейшнийг дахин патчлахыг хүснэ үү + Тэмдэглэл + Энэхүү хувилбар нь албан ёсны бус хувилбар тул гэнэтийн алдаа гарч болзошгүй + Албан ёсны холбоосууд + Вэбсайт + Хандийлах + Орчуулга + Зохиогчид + Шинэчлэгдсэн + Шинэчлэл боломжтой + Одоогийн хөгжүүлэлт + Өмнөх хөгжүүлэлт + Идэвхтэй хөгжүүлэлт + 2022 ‑ 2025 + 2022 ‑ 2025 + 2019 ‑ 2022 + Лицензийн мэдэгдэл + Тảiжи байна… + Алдаа засах + Хөгжүүлэгчийн ахисан түвшний тохиргоо болон оношилгооны логуудыг тохируулах + Дибаг логгینگ-ийг идэвхжүүлэх + Debug лог стек трейс + Алдааг илрүүлэхэд туслах үүднээс дибаг логт стек трейс-ийг багтаана + Morphe-ийн алдаа гарсан үед мэдэгдэл харуулах + "Алдааны мэдэгдлүүдийг унтрааснаар Morphe-ийн бүх алдааны мэдэгдлүүд хаана. + +Таноо гэнэтийн аливаа үйл явдлын талаар мэдэгдэхгүй байх болно" + Debug логуудыг экспортлох + Morphe-ийн алдааг засах логуудыг экспортлох + Алдааг засах логуудыг амжилттай экспортлолоо + Алдааг засах логуудыг экспортлоход алдаа гарлаа + Алдааны лог хайх… + Цэвэрлэх + Алдаа засах лог бүртгэл идэвхгүй байна + Алдааг засах лог олдсонгүй + Алдааг засах логуудыг хууллаа + Алдааг засах логуудыг экспортлоход алдаа гарлаа: %s + Алдааг засах логуудыг цэвэрлэлээ + Боломжуудын тохиргооны менежер + Boolean төрлийн боломжуудын тохиргоог удирдах + Боломжуудын төгөөлтийг хайх… + Идэвхгүй (%s) + Идэвхтэй (%s) + Блоклогдсон (%s) + Албадсан (%s) + Автомат + Үгүй + Тийм + Албадан асаах + Блоклох + Устгах + Боломжийн төлөвүүдийг хадгаллаа + Функцын төлөвүүдийг шинэчлэлээ + Боломжийн төлөвүүдийг санах ой руу хууллаа + Энэ тохиргоог ашиглахын тулд апп-ыг дахин ачаална уу + Экспорт болон импорт + %s функцүүдийг импортлолоо + Функцийн төг олдсонгүй + Функцийн тэмдэглэгээг амжилттай экспортлолоо + Функцийн тэмдэглэгээг экспортлоход алдаа гарлаа + Функцийн тэмдэглэгээг ачааллаа, хэрэглэхийн тулд Хадгалах товчийг дарна уу + Файлыг уншиж чадсангүй + Бинари хайлт + "Аппликейшны үйлдлийг ямар функц-түлхүүр (feature flag) үүсгэж байгааг илрүүлнэ + +%s төлөвүүдийн тал нь хаагдах бөгөөд дахин ачалах болгонд уг үйлдэл хэвээр байгаа эсэхийг та хэлнэ + +Эхлэхээсээ өмнө үйлдлийг нэг удаа давтан гаргаарай, эсвэл аппликейшны хайлтаас хараагүй төлөвүүд дутагдсан байх болно" + Эхлүүлэх + "Хайх функцийн төг байхгүй байна + +Зөвхөн аппликейшн ашигласан бөгөөд одоогоор албадан идэвхжүүлээгүй төгүүдийг хоёртын хайлтад ашиглаж болно" + "Алхам %1$s + +%2$s төг үлдлээ, үүнээс %3$s төг энэ алхамд хаагдсан байна + +Аппликейшнийг дахин ачаалж, тухайн шинж чанар илэрч байгаа эсэхийг шалгаад, доорх асуултад хариулна уу" + Функцийн төг хайж байна + Шинж чанар илэрч байна + Шинж чанар илэрээгүй байна + Хоёртын хайлтыг цуцлах + Хоёртын хайлт цуцлагдлаа + "%s функцийн төг уг үйлдлийг үүсгэж байна + +Одоо үүнийг хаасан" + "Функцийн төг олж чадсангүй + +Аппликейшны ажиллагаа нь функцийн төгөөр удирдагдахгүй байгаа бололтой" + "Ганц функцийн төг олж чадсангүй + +Хайлтын явцад ажиллагаа нь өмнө нь алга болсон тул нэгээс илүү төг хамааралтай байх эсвэл хариултуудын аль нэг нь буруу байх магадлалтай" + MicroG-ийн тохиргоог нээх + MicroG суулгаагүй байна. Суулгана уу + Үйлдэл шаардлагатай + "Таны MicroG v%1s хувилбар хуучирсан тул асуудал үүсгэж болзошгүй. + +MicroG-ийг хамгийн сүүлийн v%2s хувилбар руу шинэчлэнэ үү" + Шинэчлэх + Үл тоомсорлох + "Суулгасан MicroG нь Morphe-ийн зөвлөсөн хувилбар биш байна: + +%1s + +Устгахын тулд дарж, дараа нь энэ апп-ыг дахин нээж MicroG RE-ийг суулгаарай" + Устгах + "MicroG-д арын горимд ажиллах зөвшөөрөл байхгүй байна. + +Өөрийн утсанд зориулсан \"Don't kill my app\" зааврыг дагаж, MicroG суулгалтандаа хэрэгжүүлнэ үү. + +Энэ нь апп ажиллахад шаардлагатай" + Вэбсайтыг нээх + "Асуудал үүсгэхээс сэргийлэхийн тулд MicroG-ийн батерейны оновчлолыг идэвхгүй болгох ёстой. + +MicroG-ийн батерейны оновчлолыг идэвхгүй болгох нь батерейны хэрэглээнд сөргөөр нөлөөлөхгүй. + +Үргэлжлүүлэх товчийг дарж, оновчлолын өөрчлөлтийг зөвшөөрнө үү" + Үргэлжлүүлэх + MicroG-ийн батерей оновчлолын цонхыг харуулах + "Үүнийг идэвхгүй болгосноор MicroG-ийн батарейг оптимизошлох саналжийг нуух болно. + +Хэрэв батарейны оптимизошлолыг өөрчлөхгүй бол дараах зүйлс гарч болзошгүй: +• Аппликейшн гэнэт хаагдах +• Мэдэгдэл ирэхгүй байх +• Ард тал дээр тоглуулах үйлдэл ажиллахгүй байх" + Апп-ын холбоос хариуцлалт + Холбоосыг энэ апп-д нээх тохиргоо + Алхам 1: Эх хувилбарын апп холбоосыг идэвхгүй болгох + Эх аппликейшны тохиргоог нээгээд бүх домэйн холбоосыг идэвхгүй болгоно уу + Алхам 2: Патч хийсэн апп-ын холбоосыг идэвхжүүлэх + Патч хийсэн апп-ын тохиргоог нээгээд бүх домэйн холбоосыг идэвхжүүлнэ үү + Тохиргоог нээх + 🚨 Туршилтын сэтег 🚨 + "Та ⚠️ <b>%1$s</b> апп-ын туршилтын хувилбарыг ашиглаж байна +<br/><br/> +🔧 Энэхүү хувилбарын засуулалтуудыг нарийвчлан тохируулах явцад апп-ын үйл ажиллагааг хачирхалтай эсвэл тодорхойгүй алдаанууд гарч болзошгүйг анхаарна уу. +<br/><br/> +Хэрэв та хамгийн найдвартай туршлагыг хүсэж байгаа бол үүнийг <b>устгаад</b>, зөвлөсөн ✅ <b>%2$s</b> хувилбарыг суулгана уу" + ⚠️ Би туршилтын хувилбарыг хүсэж байна + ✅ Би тогтвортой хувилбарыг хүсэж байна + Системийн алдааны лог хэрэглэгчийн баримт бичигт хадгалагдлаа: %s + Системийн алдааны лог хадгалагдлаа: %s diff --git a/patches/src/main/resources/addresources/values-mn-rMN/sponsorblock/strings.xml b/patches/src/main/resources/addresources/values-mn-rMN/sponsorblock/strings.xml index 9d2d4ab7d5..d50c260275 100644 --- a/patches/src/main/resources/addresources/values-mn-rMN/sponsorblock/strings.xml +++ b/patches/src/main/resources/addresources/values-mn-rMN/sponsorblock/strings.xml @@ -16,7 +16,9 @@ Second \"item\" text" --> + SponsorBlock товчлуур болон хянах хэрэгслүүдэд дөрвөлжин булан ашиглана + Уншиж байна… diff --git a/patches/src/main/resources/addresources/values-mn-rMN/youtube/strings.xml b/patches/src/main/resources/addresources/values-mn-rMN/youtube/strings.xml index 70ce03fc67..fd8170e6ab 100644 --- a/patches/src/main/resources/addresources/values-mn-rMN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-mn-rMN/youtube/strings.xml @@ -18,213 +18,1637 @@ Second \"item\" text" + Morphe-ийн тухай + Зар + Мэдээлэлийн урсгал + Ерөнхий + Тоглуулагч + Shorts + Гүйлгээний зөвлүүр + Гүйлгэх удирдлага + Бусад + Видео + YouTube-ийн хуучин тохиргооны дэлгэцийг сэргээх + Арын горимын тоглуулалтын хязгаарлалтыг авч хаяха + Ард фондоор тоглуулахыг идэвхгүй болгох + Төхөөрөмж түгжигдэхэд автоматаар зогсоох + Унтраах + Түгжигдэх үед үргэлж зогсоох + Унаган аудио холбогдоогүй бол зогсоох + Гүйлгэх хурдны хязгаарыг идэвхгүй болгох + Layout шинэчлэлтийг идэвхгүй болгох + Серверээс апп-ын бүтцийг шинэчлэхийг хязгаарлана + "Аппликейшны бүтэц анх суулгахад ашигласан бүтэц рүү буцна. + +Серверийн зарим бүтэц буцахгүй байж болно. + +Өөрчлөлтүүдэд дараах зүйлс багтана: +• Тоглуулагчийн дэлгэц цэсний бүрэлдэхүүн хэсгүүд (эсвэл холбогдох тохиргоо) ажиллахгүй байж болно +• Тоог гүйлгэх хөдөлгөөн ажиллахгүй +• Санганы таб харагдана +• Видео тайлбарын хөгжмийн хэсэг ажиллахгүй байж болно +• Тоглуулагчийн дэлгэц цэсний элементүүдийг нуух боломжгүй +• Санганы таб дээр хаяг солих товчлуур гарч ирэхгүй байж болно" + Фийдний дэлгэрээ цэсний шүүлтүүрийг идэвхжүүлэх + Фийдний дэлгэрээ цэсний шүүлтүүр + Шүүлтүүрлөх цэсний нэрсийг шинэ мөрөөр тусгаарлан оруулна уу + Сан / Та доод цэсний шүүлтүүрийг идэвхжүүлэх + Сан / Та доод цэсний шүүлтүүр + Шүүх Сан / Та доод цэсний нэрсийг шинэ мөрөөр тусгаарлан оруулна уу + Альбомын картуудыг нуух + Уран бүтээлчийн тайлбарын доорх цомгийн картуудыг нуух + Уран бүтээгчийн картыг нуух + Хайлтын үр дүнгийн дээд хэсэгт байх уран бүтээлчийн картыг нуух + Сувгийн товчнуудыг нуух + Хайлтын үр дүнд гарч ирэх сувгийн профайл дээрх Бүртгүүлэх, Сувгийг үзэх, Нийгэмлэг, YouTube Music зэрэг товчнуудыг нууна + Олон нийтийн нийтлэлийг нуух + Компакт баннеруудыг нуух + Жижиг баннерууд болон зарим хэсгүүдийн хоорондох зайг нуух + Видеоны доорх дэлгэрээж болдог картууд + Бүгдийг харах + Зөвхөн хураангуй картыг нуух + Бүгдийг нуух + Хөвж буй микрофоны товчийг нуух + Хайлтын санал болгох дэлгэцийн баруун доод буланд байрлах микрофоны хөвөгч товчийг нууна + \'Premium авах\' товчийг нуух + Түүхийн хэсгийг нуух + Хэвтээ жагсаалтуудыг нуух + Сүүлийн үеийн мэдээ, Танд таалагдаж магадгүй олон нийтийн нийтлэл, Үргэлжлүүлэн үзэх, Бусад сувгийг судлах, Бусад сэдвүүдийг судлах, Танд зориулсан, Хамгийн сүүлийн видеонууд, Хамгийн холбоотой, Үйлдвэрлэгчийн нийтлэлүүд, Худалдан авалт болон Дахин үзэх гэх мэт хэвтээ жагсаалтуудыг нуух + Зургийн жагсаалтыг нуух + Хайлтын үр дүнд гарч ирэх зургийн жагсаалтыг нууна + Бусдыг зурвас илгээхэд урих картыг нуух + Мэдэгдлийнแทб-ын Зурвас хэсэгт гардаг урилгын картыг нуух + \'Хамгийн сүүлийн видеонुуд\' товчийг нуух + Нүүр хуудасны feed-ийн дээд хэсэгт гардаг \'Хамгийн сүүлийн видеонुуд\' гэсэн цэнхэр баннерыг нууна + Шууд дамжуулалтыг нуух + Нүүр хуудасны фийд болон хайлтын үр дүнгээс одоо шууд дамжуулж буй видеонуудыг нууна + Микс тоглолтын жагсаалтуудыг нуух + Кино хэсгийг нуух + Мэдэгдлийн цэсний гарчгийг нуух + Хонхны товчийг да랐 үед Мэдэгдлийн цэсний гарчгийг нууна + \"Надаг мэдэгдэх\" товчийг нуух + Хуваарьт шууд дамжуулалт болон нээлтүүдийн сануулах товчлуурыг нууна + Тоглоомуудыг нуух + Тоглоом тоглох боломжтой функцийг нууна + Хайлтын түлхүүр үгний miniature зургуудыг нуух + Хайлтын авто-нөхцлөлийн жагсаалт дахь жижиг зургийн урьдчилан харагдлыг нууна + \"Дэлгэрэнгүй үзэх\" товчийг нуух + Хайлтын үр дүнгийн видеонуудын доор байрлах дэлгэх товчийг нууна + Бүртгүүлсэн сувгуудын хэсгийг нуух + Бүртгүүлсэн сувгуудын нэрийг нуух + Бүртгүүлсэн сувгуудын хэсэг дэх сувгийн нэрсийг нууна + Санал асуулгыг нуух + Feed-ийн зөвлөмжийн нарийвчлалын талаарх судалгаа болон сануулгуудыг нуух + Тасалбарын хэсгийг нуух + + \'Боломжгүй видеонуудыг нуусан\' баннерыг нуух + Жагсаалтадある \'Боломжгүй видеонуудыг нуусан\' баннерыг нууна + Тảiлбэр дээрх хугацааг нуух + "Хязгаарлалт: +• Shorts талбайн жагсаалт, сувгийн хуудас болон хайлтын үр дүнд байршуулсан хугацаа харагдаж магадгүй +• Автомашины загварт ажиллахгүй" + Видеоны miniature-ийг нуух + Видео шошгонуудыг нуух + \'Автоматаар орчуулсан\' / \'Орчуулсан\', \'Шимтгэл авдаг\', \'Хэт тодотгосон\', \'Бусад хүмүүс мөн үзсэн\', \'бүтээгдэхүүн(үүд)\' болон \'Танд бас таалагдаж магадгүй\' гэх мэт шошгонуудыг нууна + Үзэлтийн тоог нуух + "Хязгаарлалт: +• Shorts талбайн жагсаалт, сувгийн хуудас болон хайлтын үр дүнд үзэлтийн тоо харагдаж магадгүй +• Автомашины загварт ажиллахгүй" + Веб хайлтын үр дүнг нуух + Хайлтын үр дүнгээс \'Вэбээс гарсан үр дүн\' хэсгийг нуух + \'Танд таалагдаж магадгүй\' хэсгийг нуух + YouTube Doodles-ийг нуух + YouTube лого дээрх улирлын болон арга хэмжээний анимэцийг нууна + YouTube Doodles-ийг харуулах нь тусгай толгой хэсгүүдийг харуулахаас сэргийлж болзошгүй + Сувгийн хэсгийг нуух + Сувгийн усан тэмдгийг нуух + Бүлгүүд болон \'Энэ бичлэгт\' товчийг нуух + Кроудфандинг хэсгийг нуух + Онцгой байдлын мэдээллийн хэсгийг нуух + Мэдээлэл харуулах хэсгийг нуух + Анагаах ухааны мэдээлэл гэх мэт мэдээлэл өгөх контекст панелуудыг нуух + \"Нэгдэх\" товчийг нуух + \'Шууд чатын давталт\' товчийг нуух + Бүрэн дэлгэцийн горимд чатын давталтын товчийг нуух + Эрүүл мэндийн мэдээллийн хэсгийг нуух + Тоглуулагчийн дохио заавруудыг нуух + Тоглогчийн дохио зааврын \'Нарийвчлалтай хайхын тулд дээшээ татна\', \'Цуцлахын тулд тавь\' болон \'2x\' гэх мэт зүйлсийг нуух + Снэкбарыг нуух + Бүртгүүлэгчдийн удирдамжийг нуух + \'Бичлэгтэй синхрончлох\' товчийг нуух + Дараад тэрийлгэх градиентыг нуух + Хурдшруулахын тулд хуруугаа дараад чирж эхлэхэд тоглуулах хяналтын градиентыг нуух + Хугацаатай урвалгуудыг нуух + Сэтгэгдэл болон шууд чат хэсэгт эможи-оор хариу үйлдэл үзүүлэх функцийг нуух + \'Тусламж & санал хүсэлт\' цэсийг нуух + Хайлтын үр дүн гэх мэт дээрх гурав цэгтэй товчлуурыг ашиглах үед \'Тусламж & санал хүсэлт\' цэсийг нуух + Хандлагын нэрийг нуух + \'Та\' таб доторх таны сувгийн хандлагын нэрийг нууна + Видеоны гарчиг нуух + Бүрэн дэлгэцийн горимд видеоны гарчигыг нуух + Видео хадмалтыг цэвэрлэх + Видеоны хадмал орчуулгаас \'#\', \'Fundraiser\', \'product(s)\', \'Shop\' гэх мэт хэллэгүүдийг цэвэрлэнэ + \'Gemini-гээс асуух\' товчийг нуух + \'Энэ видеоны талаар асуух\' хэсгийн доор байрлах \'Gemini-ээс асуух\' товчийг нуух + Видео тайлбар + Видео тайлбарт аль хэсгүүдийг харуулахыг сонгох + \'AI-аар үүсгэсэн видеоны хураангуй\' хэсгийг нуух + Асуух хэсгийг нуух + Шинж чанарын хэсгийг нуух + Онцгой газрууд, Тоглоом, Хөгжим болон Дурдсан хүмүүсийн хэсгүүдийг нуух + Сувгийн холбоосуудын хэсгийг нуух + Бүлгүүдийн хэсгийг нуух + Засварлах хэсгийг нуух + \'Хичээлийн явц\' хэсгийг нуух + Explore хэсгийг нуух + \'Энэ хичээлийг судлах\' болон \'Подкастыг судлах\' хэсгүүдийг нуух болно + \'Энэ хичээлийг судлах\' хэсгийг нуух + \'Подкастыг судлах\' хэсгийг нуух + Онцгой сувгуудын хэсгийг нуух + Онцгой холбоосуудын хэсгийг нуух + Онцгой тоглооцны хэсгийг нуух + Онцгой видеонуудын хэсгийг нуух + Хаштаг хэсгийг нуух + \'Хэрхэн хийгдсэн бэ\' хэсгийг нуух + Hype оноог нуух + Мэдээллийн картуудын хэсгийг нуух + \'Гол ойлголтууд\' хэсгийг нуух + Асуулт хариултын хэсгийг нуух + \'Энэ видео дотроос хайх\' хэсгийг нуух + \"Бүртгэх\" товчийг нуух + \"Тэмдэглэлийн\" хэсгийг нуух + \"Видеоны дэлгэрэнгүй\" хэсгийг нуух + Шүүлтүүрийн хэсэг + Гүйлгэдэг шүүлтүүрийн хэсгийг хаана нуухыг сонгоно уу + Суваг хуудсан дээр нуух + Сэтгэгдэл хэсэгт нуух + Фийд дээр нуух + Холбоотой видеонууд дээр нуух + Хайлтын үр дүнгээс нуух + Үзсэн түүхээс нуух + Сувгийн хуудас + Сувгийн хуудсан дээр аль хэсгүүдийг харуулахыг сонгоно уу + Сувгийн табын шүүлтүүрийг идэвхжүүлэх + Сувгийн табын шүүлтүүр + Шүүх сувгийн таб нэрсийг шинэ мөрөөр тусгаарлан оруулна уу + Олон нийтийн товчийг нуух + Нэгдэх товчийг нуух + Холбоосын урьдчилан харахыг нуух + Сувгийн толгой хэсэгт байх дарж орж болох вэб холбоосуудыг нууна + \'Зөвхөн гишүүд\'-ийн тэмдэглэгээг нуух + Сувгийн шүүлтүүрийн хэсгээс \'Зөвхөн гишүүд\'-ийн тэмдэглэгээг нуух болно + Гишүүдийн хэсгийг нуух + Сувгийн гишүүдийн жагсаалтыг нууна + Постуудын хэсгийг нуух + Дэлгүүрийн товчийг нуух + Бүртгүүлэх товчийг нуух + Хайгуул цэс + Хаяглал цэсэнд аль бүрэлдэхүүн хэсгүүдийг харуулахыг сонгоно уу + Курсуудын цэсийг нуух + Хаяглал товчийг нуух + \'Загвар & Гоо сайхан\' цэсийг нуух + Тоглоомын цэсийг нуух + Hype цэсийг нуух + Суралцах цэснээс нуух + Шууд дамжуулалтын цэсийг нуух + Гишүүнчлэлийн цэсийг нуух + \'Кино & ТВ\' цэсийг нуух + Хөгжим цэсийг нуух + Мэдээний цэсийг нуух + Playables цэсийг нуух + Подкаст цэсийг нуух + Дэлгүүрийн цэсийг нуух + Спортын цэсийг нуух + YouTube Premium цэсийг нуух + YouTube Studio цэсийг нуух + YouTube Music цэсийг нуух + YouTube Kids цэсийг нуух + YouTube Create цэснүүдийг нуух + YouTube-ийг хэрхэн ажиллуулдаг вэ цэснээс нуух + Хууль эрх зүй болон нууцлалын доод хэсгээс нуух + Сэтгэгдэл + Сэтгэгдлийн хэсэгт аль хэсгүүдийг харуулахыг сонгоно уу + Сэтгэгдлийн гүйлгээн шүүлтүүрийг идэвхжүүлэх + "• Карусель элементийн яг зөв нэрийг оруулна уу (жишээ нь: Ask, Hype this video, Listen on YouTube Music) +• Карусель элементүүдийг нуух нь санаандгүй сөрөг нөлөө үзүүлж болзошгүй +• Хэрэв бүх карусель элементүүдийг нуувал сэтгэгдэл хэсэг бүхэлдээ нуугдана" + Сэтгэгдлийн гүйлгээн шүүлтүүр + Шүүх сэтгэгдлийн каруселийн нэрсийг шинэ мөрөөр тусгаарлан оруулна уу + AI чатын хураангуйг нуух + Сувгийн удирдамжийг нуух + Контекст зурвасуудыг нуух + \'Гишүүдийн сэтгэгдэл\' гарчигийг нуух + Сэтгэгдлийн хэсгийг нуух + Нүүр хуудасны feed дээрх сэтгэгдлийн хэсгийг нуух + Олон нийтийн удирдамжийг нуух + \"Богино видео үүсгэх\" товчийг нуух + Таалагдахгүй товчийг нуух + Emoji товчийг нуух + Шүүлтүүрийн мөрийн сонголтуудыг нуух + \'Шилдэг\' болон \'Хамгийн сүүлийн\' гэсэн сонголтоос бусад бүх шүүлтүүрийн мөрийн сонголтуудыг нууна + Бэлгийн хเคลื่อนгөлгөө болон картуудыг нуух + Бэлгийн товчийг нуух + Мэдээллийн товчийг нуух + Шууд чатын хандийлэгчдийн хэсгийг нуух + Live чатын хамгийн доошоо гүйлгэх товчийг нуух + Шууд чатын туслах зөвлөмжүүдийг нуух + Чат бичих хэсгийн дэргэд гардаг бүртгэлтэй хэрэглэгчийн чатын мэдэгдэл зэрэг туслах зөвлөмжийн цонхнуудыг нууна + Цэсний товчийг нуух + Сэтгэгдэл бүрийн 3 цэгийн цэсний товчийг нуух + Сэтгэгдлийн урьдчилгаа харалтыг нуух + Сэтгэгдлийн бүрэн хэсгийг нээхээс өмнө видеоны доорх сэтгэгдлийн урьдчилан харуулах хэсгийг нууна + Сэтгэгдлийг Morphe нуусан… + Минималист сэтгэгдлийн товчлуурыг идэвхжүүлэх + Сэтгэгдлийн товчлууран дээр харагддаг гулсдаг карт болон хуудас сонгогчийг нууна + \"Баярлалаа\" товчийг нуух + Цаг хугацааны товчийг нуух + \'Шилдэг шүтэн бишрэгчид\' товчийг нуух + Орчуулгын товчийг нуух + \'Одоогоор сэтгэгдэлгүй байна\' гэсэн мэдэгдлийг нуух + Хайлтын онцгой тэмдэглэгээтэй холбоосуудыг цэвэрлэх + Хайлтын онцгой тэмдэглэгээтэй холбоосуудыг цэвэрлэж, зөвхөн эх alkuр бяцхан текстийг үлдээнэ + Түлхүүр үгээр шүүх + Фийд болон хайлтаас видеонуудыг нуухын тулд тодорхой үгс эсвэл хэллэгийг сонгоно уу + Түлхүүр үгээр сэтгэгдлийг нуух + Нүүр хуудасны видеог түлхүүр үгээр нуух + Хайлтын үр дүнг түлхүүр үгээр нуух + Бүртгэлтэй видеог түлхүүр үгээр нуух + Нуух түлхүүр үгнүүд + "Нуух түлхүүр үг болон хэллэгийг шинэ мөрөөр тусгаарлан оруулна уу + +Түлхүүр үг нь бичлэгийн гарчиг эсвэл сувгийн нэрэнд харагдах ямар ч текст байж болно + +Дундаа том үсэгтэй үгнүүд нь яг таг тохирхautoconfigure (жишээ нь: iPhone, TikTok, LeBlanc)" + "Нүүр / Бүрийн төлбөр / Хайлт / Сэтгэгдэл хэсгүүдэд түлхүүр үгтэй тохирсон контентыг нуух шүүлтүүр ажиллана + +Хязгаарлалт: +• Shorts-ийг сувгийн нэрээр нуух боломжгүй +• Зарим UI бүрэлдэхүүн хэсгүүд нуугдахгүй байж болно +• Түлхүүр үгээр хайх үед илэрцгүй байж болно" + Бүрэн үгээр хайх + Түлхүүр үг/хэллэгийг давхар хашилтаар хүрээлбэл видеоны гарчиг болон сувгийн нэрс дээрх хэсэгчилсэн тохирлуудыг сэргийлнэ<br><br>Жишээлбэл,<br><b>\"ai\"</b> гэвэл: <b>How does AI work?</b> видеог нуух болно,<br>харин: <b>What does fair use mean?</b> гэснийг нуухгүй + Түлхүүр үгийг ашиглах боломжгүй: %s + Түлхүүр үгийг ашиглахын тулд хашилт нэмнэ үү: %s + Түлхүүр үг зөрчилтэй тодорхойлолттой байна: %s + Түлхүүр үг хэтэрхий богино тул хашилт шаардлагатай: %s + Түлхүүр үг бүх видеог нуух болно: %s + "• Нүүр: %s +• Бүртгэлүүд: %s +• Хайлт: %s +• Сэтгэгдэл: %s +• Нийт: %s" + Статистик + Сүүлийн 24 цагт нуугдсан видеонууд + Бүх цаг үеийн нуугдсан видеонууд + Сүүлийн 24 цагийн тоолуурыг тэглэх үү? + Нийт хугацааны тоолуурыг тэглэх үү? + AI суваг шүүлтүүр + AiSList хамт олны жагсаалтаар AI-аар бүтээгдсэн контент гэж тэмдэглэгдсэн сувгуудын видеог нуух + Хориглох жагсаалт: Өндөр итгэлцэлтэй + Анхааруулга жагсаалт: Дундаж итгэлцэлтэй + Нүүр хуудасны видеог нуух + Хайлт дахь видеог нуух + "• Нүүр: %s +• Хайлт: %s +• Нийт: %s" + Хар жагсаалтын сувгууд + Анхааруулгын жагсаалтын сувгууд + Сувгуудыг ачаалахын тулд шүүлтүүрийг идэвхжүүлнэ үү + Сувгуудыг ачаалж байна… + "Нүүр хуудас болон хайлтын үр дүнгээс AiSList-ээр тэмдэглэгдсэн сувгуудын видеог нуух шүүлтүүр ашиглаж байна + +Хязгаарлалт: +• Шүүгдсэн AI Shorts-ууд хоосон зай болж харагдаж болно +• Зарим AI видеонууд харагдаж магадгүй" + "AiSList нь сайн дурын хүмүүсийн хяналт дорх, хиймэл оюун ухаанаар бүтээсэн контент нийтэлдэг YouTube сувгуудын нээлттэй эх сурвалж бүхий жагсаалт юм. Сувгуудыг хэрэглэгчид мэдээлж, хянаж шалгасны дараа нэмдэг + +Жагсаалтыг GitHub-аас татаж авдаг бөгөөд видеонуудыг таны төхөөрөмж дээр шүүж харуулна" + AI сувгуудыг мэдээлэх + Видео цэсэнд тайлангийн бичлэг нэмэх + Видеоны сувгийг AiSList-д мэдээлэх бичлэгийг видео цэсэнд нэмэх + Илгээгчийн нэр + Нийтийн AiSList-ийн тэргүүллэлийн жагсаалтад таны тайлангийн хажууд харагдах нэр. Энэ нь хоосон байх үед тайлан илгээх боломжгүй + AI сувгийг мэдээлэх + "Энэхүү видеог нотолгоо болгон ашиглан %s-г AiSList-д илгээх үү? + +Зөвхөн дараах сувгуудыг мэдээлээрэй: +• AI дуу хоолой, дүрслэл, зохиол эсвэл хөгжмийг үндсэн контент болгон ашигладаг +• AI хэрэгслүүдийг ашиглан хүний оролцоо багатайгаар контент үйлдвэрлэдэг + +Зөвхөн AI-ийн талаар хэлэлцдэг эсвэл видеоныхоо зарим хэсэгт AI ашигладаг сувгуудыг мэдээлэхгүй байхыг хүсье + +Мэдээллүүд нь нээлттэй байх бөгөөд сувгийг жагсаалтад нэмэхээс өмнө хянаж үзнэ. Устгалт нь зөвхөн GitHub-ээр хүсэлт гаргах замаар хийгдэнэ" + Энэ суваг аль хэдийн AiSList-ийн хоригдсон жагсаалтад байна + Энэ суваг аль хэдийн AiSList-ийн анхааруулга өгөх жагсаалтад байна + Мэдээлэх + Сувгуудыг мэдээлэхийн тулд AI сувгийн шүүлтүүрийн тохиргоонд илгээгчийн нэрийг тохируулна уу + Сувгийг хайж байна… + Энэхүү видеоны сувгийг олж чадсангүй + Сувгийг мэдээлсэн бөгөөд хяналт хийхийг хүлээж байна + AiSList тайланг хадгалаж чадсангүй, дараа дахин оролдоно уу + Хэт олон тайлан илгээгдсэн байна, дараа дахин оролдоно уу + AiSList тайланг цуцаллаа + Тайлан илгээхэд алдаа гарлаа, холболтоо шалгана уу + Сувгийн зар сурталчилгааны зөвшөөрөгдсөн жагсаалт + Цагаан жагсаалтад байгаа сувгуудын бичлэгүүд сурталчилгаатай тоглуулагдах бөгөөд ингэснээр тэдгээрийн бүтээгчид төлбөрөө авна. Бусад бүх сувгуудад сурталчилгаа хаагдсан хэвээр байх болно + Сувгийг сурталчилгааны цагаан жагсаалтад нэмэх + Сувгийг сурталчилгааны цагаан жагсаалтаас хасах + Үйлдвэрлэгчийн дэлгүүрийн талбайг нуух + Төгсгөлийн дэлгэцийн дэлгүүрийн баннерыг нуух + Ерөнхий сурталчилгааг нуух + Барааны баннеруудыг нуух + Төлбөртэй сурталчилгааны шовруудыг нуух + Тоглуулагчийн поп-ап сурталчилгааг нуух + Өөрийн ивээн тэтгэсэн картуудыг нуух + Худалдан авалтын холбоосуудыг нуух + Видео сурталчилгааг нуух + YouTube Premium-ийн сурталчилгааг нуух + Уншиж байна… + Одоогийн суваг: + Цагаан жагсаалтад нэмэх + Хасах + Цагаан жагсаалтад байгаа сувгууд + Цагаан жагсаалтад суваг байхгүй байна + Сувгийг цагаан жагсаалтад нэмлээ + Сувгийг цагаан жагсаалтаас хаслаа + Сувгаас хайх + Сувгийн хуудсанд зөвхөн тухайн сувгаас хайхための хоёр дахь хайлтын товчийг нэмнэ + Суваг хайх + Сувгийн хайлтын үр дүн + Энэ сувган дээр юу ч олдсонгүй + Энэ сувгаас хайлт хийж чадсангүй + Видео + Пост + Энэ суваг дээр тохирох пост олдсонгүй + Холбогдох байдал + Хамгийн их үзсэн + Хамгийн бага үзсэн + Хамгийн сүүлийн + Хамгийн эртний + Хамгийн богино + Хамгийн урт + Холбоосыг санах ой руу хууллаа + Цаг хугацаатай холбоосыг санах ой руу хууллаа + Видео холбоос хуулах товчийг харуулах + Видео холбоосыг хуулахын тулд товшоорой, цаг хугацаатай хуулахын тулд удаан дарна уу + Цаг хугацааг автоматаар хуулах + \'Видео холбоос хуулах\' товчны товших болон удаан дарах үйлдлүүдийг сольно + Видеоны гарчигийг хуулах + Видеоны гарчиг дээрээ удаан дарж хуулна уу + Видеоны гарчигийг санах ой руу хууллаа + Сэтгэгдлийг хуулах + Сэтгэгдэл дээрээ удаан дарж хуулна уу + Сэтгэгдлийг санах ой руу хууллаа + Видео цэсийг нээхдээ дараад тэрийлгэнэ үү + Фидийн видеоны хаана ч дараад тэрийлгэхэд цэс нь нээгдэнэ + Ногадуур дээр \"Дараа үзэх\" товчийг харуулах + Фидийн видеоны ногадуур дээр \"Дараа үзэх\" товч харагдана. Үүнийг дараад тэрийлгэхэд \"Жагсаалтад хадгалах\" цэс нээгдэнэ + Постын зургуудыг татаж авах + Зураг эсвэл GIF бүхий постудын Хуваалцах товчлуурын хажууд татаж авах товч харагдах бөгөөд тэдгээрийн цэсэнд татаж авах сонголт нэмэгдэнэ + Зургуудыг татаж авах + Зураг татах + %s зураг татаж байна… + Зураг татаж байна… + Зураг %s хавтсанд хадгалагдлаа + Зургийг татаж авж чадсангүй + Энэхүү нийтлэлээс зураг олдсонгүй + Дэлгэрэнгүй цэсэнд Асуух хэсгийг нэмэх + Фидийн дэлгэрэнгүй цэсэнд видеог нэгтгэн дүгнэх хэсгийг нэмнэ + "Асуух функцийг эхлээд ашиглахын тулд Асуух боломжтой улсад бүртгэлтэй хаягаар нэвтэрсэн байх шаардлагатай. + +Асуух товчлууртай видеог нэг удаа нээсний дараа, одоогийн улсад Асуух функц байхгүй байсан ч бүх видео дээр ажиллах болно." + Видеог хураангуйлах + Энэ видео дээр Асуух функцийг нээж чадсангүй + Асуух функц нь таны хаяг эсвэл одоогийн улсад боломжгүй байна + Thumbnail дээр үзэлтийн тоог харуулах + Фийдийн видеонуудын үзэлтийн тоог гарчиг доор биш, харин thumbnail-ийн зүүн доод хэсэгт харуулна. Зарим видеонууд, жишээ нь талбайн видеонуудад үзэлтийн тоо харагдахгүй байж болно + Фидийн миниатюрын лайк тоог харуулах + Фидийн миниатюрын лайк хувийг харуулах + Сэтгэгдлээс хайх + Сэтгэгдэл хэсгийн хайх товчийг дарж, хайсан бүх үг агуулсан ачаалсан сэтгэгдлүүдийг харуулна. Хэллэгийг хайхын тулд үгсийг хашилтанд бичнэ үү + Сэтгэгдэл хайх + Хайх үгс + Хайх + Цэвэрлэх + Эрэмбэлэлтийн дарааллыг өөрчлөх эсвэл сэтгэгдлийг дахин нээж хайх + Хайлт цэвэрлэгдлээ. Бүх сэтгэгдлийг харуулахын тулд эрэмбэлэлтийн дарааллыг өөрчлөх эсвэл сэтгэгдлийг дахин нээх + \'ТВ-д нэвтрэх\' цонхыг идэвхгүй болгох + Төхөөрөмжүүдээ холбох цонхыг идэвхгүй болгох + Бүлгийг алгасах хоёр удаагийн товшилт идэвхгүй болгох + Бүлгийг алгасах хоёр хуруугаар хоёр удаа товших үйлдлийг идэвхгүй болгоно. Энэ нь нэг хуруугаар товхдоход санамсаргүйгээр ажиллаж магадгүй юм + Гуравдагч талын татаж авах аппликейшн ашиглан видео, подкаст эсвэл дуу татаж авах + Тоглуулагчийн overlay татах товчийг харуулах + Видео тоглуулагч дээр гадаад татуургчийг нээх товчийг харуулна + Товшиод шуурхай татах + Ард тал дээр татах санал болгоно. Татаж барьснаар татуургчийг нээнэ. YTDLnis болон YTindir-тэй хамт ажиллана + Татсан файлуудыг үзсэн гэж тэмдэглэх + Гадаад татаж авагчаар татаж авсан видеонууд таны YouTube үзсэн түүхэнд нэмэгдэнэ + Full HD (1080p) + Дунд (192 kbps) + Хурдан татаж авах + Видео + Аудио + Бүх сонголтууд + Shorts цэсэнд татаж авах товчийг харуулах + Shorts тоглуулагчид гадаад татаж авах цэсний хэсгийг нэмнэ + Татаж авах + Дууг орчуулах + Видеоны хадмал орчуулгыг төхөөрөмжийн текстээс яриа хөрвүүлэгчийг ашиглан, тоглуулалттай синхрончилж унших + Видео тоглуулагчид давхарлагдсан товчлуур харуулна. Орчуулгыг идэвхжүүлэхэд товшиж, хурдан тохиргоо руу орохын тулд удаан дарна уу + Хэл + Орчуулгын үйлчилгээ үзүүлэгч + Эх дууны чангаяа + Орчуулга уншиж байх үеийн YouTube-ийн эх дууны чангаяа + Орчуулгын дууны хэмжээ + Дуу найруулсан орчуулгын дууны хэмжээ + Ярианы дээд хурд + Яриаг хадмал орчуулгын зайруунд багтаах үеийн хурдны дээд үржүүлэгч + "Үнэгүй орчуулгын API, API түлхүүр шаардлагагүй. Үнэгүй түвшинд өдөрт 5,000 тэмдэгт, эсвэл имэйл хаягтай бол өдөрт 50,000 тэмдэгт хүртэл орчуулна. + +MyMemory.translated.net сайтыг нээхээр энд дарна уу" + MyMemory имэйл + Сонголтоор. Өдөр тутмын хязгаарыг 5,000-аас 50,000 тэмдэгт болгож нэмэгдүүлэхийн тулд MyMemory имэйл хаягаа оруулна уу. Нэргүй хэрэглэгчийн хязгаарыг ашиглах бол хоосон орхино уу + "OpenRouter-ээр дамжуулан хийх AI орчуулгын төлбөрт үйлчилгээ. Нэг удаа цэнэглээд мянга мянган видео орчуулна. Mistral Nemo нь хамгийн хямд, Gemma 4 нь хамгийн өндөр чанартай сонголт юм. + +OpenRouter.ai сайтыг нээхээр энд дарна уу" + OpenRouter API түлхүүр + Таны OpenRouter API түлхүүр. OpenRouter.ai > Workspace > API Keys хэсгээс түлхүүр үүсгэнэ үү + OpenRouter модель + Захиалгаар + OpenRouter тохируулаагүй байна + Энэхүү үйлчилгээг сонгохын өмнө тохиргоо хэсэгт OpenRouter API түлхүүрээ оруулна уу + Үнэгүй + 100 видео цагт %s + Дуу хоолой + Систем + Олон хэлний эрэгтэй + Олон хэлний эмэгтэй + Эрэгтэй + Эмэгтэй + Морфинг хийх цаг боллоо! + Орчуулга боломжгүй + "Орчуулгын үйлчилгээ %s HTTP алдааг буцаалаа. Таны IP хаягийг 24 цагаас дээш хугацаагаар хязгаарласан байх магадлалтай. + +Орчуулгыг үргэлжлүүлэн ашиглахын тулд тохиргооноос өөр орчуулгын үйлчилгээг сонгох эсвэл VPN ашиглана уу" + Дахин бүү харуул + "Таны тохиргоонд OpenRouter API түлхүүр байна. + +Таны API түлхүүрийг хэзээ ч бусадтай хуваалж болохгүй. Үүнд хандалттай хэн ч таны OpenRouter кредитийг ашиглах боломжтой" + OpenRouter кредитийн хязгаарт хүрлээ + Таны OpenRouter кредитийн хязгаар дууссан байна. OpenRouter.ai дээрх үлдэгдлээ нөхөх эсвэл түлхүүрийн зардалтын хязгаарыг нэмэгдүүлнэ үү + OpenRouter.ai руу орох + OpenRouter орчуулга амжилтгүй боллоо (HTTP %s) + MyMemory-ийн өдрийн хязгаарт хүрлээ + Дараагийн боломж %s-д + Та өнөөдрийн MyMemory-ийн үнэгүй орчуулгын квот (5,000 тэмдэгт)-ыг ашиглан дуусгалаа. Өдрийн хязгаарыг 50,000 тэмдэгт болгож нэмэгдүүлэхийн тулд тохиргоонд MyMemory-ийн имэйл хаягаа нэмэх, эсвэл өөр орчуулгын үйлчилгээ рүү шилжээрэй + Та өнөөдрийн MyMemory-ийн орчуулгын квот (50,000 тэмдэгт)-ыг ашиглан дуусгалаа. Өөр орчуулгын үйлчилгээ рүү шилжих эсвэл маргааш дахин оролдоно уу + MyMemory орчуулга амжилтгүй боллоо (төлөв %s) + Видеог давтаж тоглуулахыг идэвхжүүлэх + Видео давталтыг санаагүй + Видео давталтын одоогийн төлөвийг дараагийн видеонуудад хадгалахаас сэргийлнэ + Видео давтах товчийг харуулах + Видеог давтаж тоглуулах идэвхтэй байна + Видео давталт унтраалаа + Давталтын муж + Эхлэх + Дуусах + [цц:]мм:сс + [ц:]м:с (хоосон = видеоны төгсгөл) + Тохируулах + Цэвэрлэх + %1$s - %2$s хэсгийг давтаж байна + Хугацааны интервал буруу байна + Дүрсийг дүрсийн дотор харуулах товчийг харуулах + Дүрсийг дүрсийн дотор харуулах цэсийг харуулах + Дүрсийг дүрсийн дотор харуулах горимд шилжих + Бүгдийг тоглуулах товчийг харуулах + "Сувгийн бүх видеог жагсаалтаар үүсгэхийн тулд товхлоно уу, цуцлахын тулд удаан дарна уу + +Мэдээлэл: Шууд дамжуулалтын үеэр ажиллахгүй байж магадгүй" + Жагсаалт үүсгэх горим + Бүх агуулга (Цагаар эрэмбэлэх, өссөнөөр) + Бүх агуулга (Цагаар эрэмбэлэх) + Бүх агуулга (Түгээмэл байдлаар эрэмбэлэх) + Зөвхөн видео (Цагаар эрэмбэлэх) + Зөвхөн видео (Түгээмэл байдлаар эрэмбэлэх) + Зөвхөн Shorts (Цагаар эрэмбэлэх) + Зөвхөн Shorts (Түгээмэл байдлаар эрэмбэлэх) + Зөвхөн шууд дамжуулалтын видео (Цагаар эрэмбэлэх) + Зөвхөн шууд дамжуулалтын видео (Түгээмэл байдлаар эрэмбэлэх) + Зөвхөн гишүүдэд зориулсан бүх контент + Зөвхөн гишүүдэд зориулсан видео + Зөвхөн гишүүдэд зориулсан Shorts + Зөвхөн гишүүдэд зориулсан шууд дамжуулалтууд + Сувгийн ID зөрүүтэй байгаа тул тоглоцны жагсаалт үүсгэх боломжгүй байна + Видеог дахин ачаалах товчийг харуулах + Видео ID-г олж чадсангүй. Видеог гараар дахин ачааллана уу + \"Дараа үзэх\"-д хадгалах товчийг харуулах + \"Дараа үзэх\"-д хадгалахын тулд товшиж, дарааллын сонголтуудын тулд удаан дарна уу + Видеог \"Дараа үзэх\" жагсаалтад амжилттай хадгаллалаа + Энэ видеог \"Дараа үзэх\" жагсаалтад хадгалчихсан байна + Энэ видеог \"Дараа үзэх\" жагсаалтад хадгалах боломжгүй + Дараа үзэх хэсэгт хадгалах + Тоглуулагчийн тохиргооны цэсэнд \"Дараа үзэх\" хэсэгт хадгалах товчийг харуулах + Видео тохиргооны цэснээс одоо үзэж буй видеог \"Дараа үзэх\" хэсэгт хадгалах боломжийг олгоно + Хүүхдийн видеонд \"Дараа үзэх хэсэгт хадгалах\" товчийг харуулах + Цэснээс хүүхдэд зориулсан видеог \"Дараа үзэх\" хэсэгт хадгалах боломжийг олгоно + Shorts-ийн цэсэнд \"Дараа үзэх\" хэсэгт хадгалах товчийг харуулах + Цэснээс Shorts-ийг \"Дараа үзэх\" хэсэгт хадгалах боломжийг олгоно + \"Дараа үзэх\"-ийг тоглолтын жагсаалрууг шилжүүлэх + Дараа үзэх + Morphe дараалald нэмэх + Дарааллыг сешен хооронд хадгалах + Аппликейшнийг дахин ачаалласны дараа дараалал хадгалагдана + Хадгалах болон Дараалалд нэмэх үйлдлүүдийг солих + Дарааллын сонголтуудыг харахын тулд товш, \"Дараа үзэх хэсэгт\" хадгалахын тулд удаан дар + Дараагийн торын төрөл + Үндсэн + Системийн тор + Morphe тор + Morphe queue клиент тал + Feed-ийн хөгжсөн цэсэнд дарааллын цэс нэмэх + Нэмэлт хөгжсөн цэс нэмнэ. Хэрэв \"Дараалald нэмэх\" гэсэн сонголт харагдахгүй бол үүнийг идэвхжүүлнэ + Дарааллал + Дараалал (%s) + Бүгдийг цэвэрлэх + Дарааллалд юу ч байхгүй байна + Дурын видео дээр \'Дарааллын дараа тоглуулах\'-ыг ашиглана уу + Одоо тоглуулагдаж буй + Дарааллын дараа тоглуулах + Дарааллын хамгийн сүүлийнхийг тоглуулах + Дарааллыг өөрчлөх + Устгах + Дарааллын жагсаалтад нэмэгдлээ + Энэхүү видеог дарааллаа нэмж чадсангүй + Т playlist-үүдийг дарааллын жагсаалтад нэмэх боломжгүй + Дараалал дүүрэн байна + Видеог эхлүүлж чадсангүй + Хурдан цэсний зар сурталчилгааны зөвшөөрлийн жагсаалт + Тухайн видеоны суваг дээр зар сурталчилгааг тоглуулах боломжтой хурдан цэсний элемент нэмнэ + Энэхүү функц нь PotHelper ашиглах эсвэл \"Видео урсгалыг хуурах\" тохиргоог TV эсвэл Android VR нэвтрэлт болгож өөрчлөхийг шаардаж болзошгүй + Хурдан цэсний хурдны зөвшөөрлийн жагсаалт + Тухайн видеоны сувгийг үргэлж 1.0x хурдтайгаар тоглуулах хурдан цэсний элемент нэмнэ + Нэвтрээгүй + Түত্বের жагсаалтын ID байхгүй + Дарааллын жагсаалт хоосон байна + Видео ID байхгүй байна + Алдаа гарлаа: %s + Дарааллын жагсаалтад нэмэхэд алдаа гарлаа + Дарааллын жагсаалт үүсгэхэд алдаа гарлаа + Дарааллын жагсаалтаас хасахад алдаа гарлаа + Дарааллыг хадгалахад алдаа гарлаа + Дараалалд нэмлээ + Дарааллыг үүсгэлээ + Дарааллаас хассан + Дарааллыг %s дотор хадгаллаа + Morphe дараалал + Дараалалд нэмэх + Дараалалд нэмээд нээх + Дараалалд нэмээд тоглуулах + Нэмээд видеог дахин ачаалах + Дарааллаас хасах + Хасаад дарааллыг нээх + Видеог устгаж, дахин ачаалах + Дарааллыг нээх + Дарааллыг тоглолтын жагсаалтад хадгалах + Жагсаалт хадгалагдлаа + %s хэсэгт хадгалагдлаа + Дарааллыг цэвэрлэх + Дараалал цэвэрлэгдлээ + Дараалал + Дарааллын хамгийн сүүлд тоглуулах + Дарааллын дараагийнхыг тоглуулах + YouTube дарааллыг харуулах боломжгүй байна. + Энэхүү видеод дараалал бэлэн биш байна. Видео тоглуулагчийг нээж дахин оролдоно уу. + Mix-ийг дараалалд нэмэх үү эсвэл Mix-ийг үзэх үү? + Үзэх + Дарааланд нэмэх + Mix ачаалж байна… + Шууд дамжуулалтын аватарнаас сувгийг нээх + Сувгийн аватар дээр дарах үед шууд дамжуулалт нээгдэхийг зогсооно + Нарийвчилсан хайх жест-ийг идэвхгүй болгох + Хайлтын горимыг дээш татах үед идэвхждэг нарийвчилсан хайлтын горимыг идэвхгүй болгоно + Шууд дамжуулалтын DVR-ийг идэвхжүүлэх + Ердийн тохиргоогоор хязгаарлагдсан байдаг шууд дамжуулалтууд дээр ч ухрааж үзэх боломжийг олгоно + Seekbar-ийн миниатюрыг идэвхжүүлэх + Seekbar-ийг чирж байх үед видеоны миниатюрыг харуулна + Хамгийн их давтан үзсэн + "'Шууд дамжуулалтын DVR-ийг идэвхжүүлэх' боломжгүй байж магадгүй + +Үүнийг засахын тулд 'Видео урсгалыг дуурайлгах' тохиргоог асааж, 'Үндсэн клиент'-ийг 'TV Simply' эсвэл 'visionOS' болгон өөрчилнө үү" + Шууд дамжуулалтын DVR-ийн хугацааг өргөтгэх + Шууд дамжуулалтыг сүүлийн 7 хоногийн турш ухрааж үзэх боломжийг олгоно + "'Шууд дамжуулалтын DVR-ийн хугацааг өргөтгэх' боломжгүй байж магадгүй + +Үүнийг засахын тулд 'Видео урсгалыг хуурах' функцийг асааж, 'Үндсэн клиент'-ийг 'TV Simply' эсвэл 'visionOS' болгон өөрчилнө үү" + Гүйлгэж хайхыг идэвхтэй болгох + Видео тоглуулагчийг дарж тэвэл 때 2х хурдтай тоглуулахын оронд гүйлгээ хийж хайхыг идэвхжүүлнэ + Товшиж хайх функцийг идэвхжүүлэх + Чигүүргүйг чирэхгүйгээр тухайн хэсэг рүү очихын тулд seekbar дээр товшолно уу + Зүүн тийш гүйлгэх дохио + Баруун тийш гүйлгэх дохио + Дээшээ гүйлгэх дохио + Зүүн/Баруун бүсийн өргөн + Дэлгэцийн ирмэгийн зүүн болон баруун тийш гүйлгэх дохионд зориулсан зайны хувь (5-50) + Гэрэлтүүлэг + Дууны хэмжээ + Уугуул + Унтраах + Түгжигдсэн үед гүйлгэх дохиог үл тоомсорлох + Тоглуулагчийг \'Дэлгэцийг түгжих\' функцээр түгжсэн үед гүйлгэх дохиог үл тоомсорлоно + Дарж гүйлгэх дохиог идэвхжүүлэх + Гүйлгэх дохиог танихын өмнө дарж тэсрэх шаардлагатай + Хөртлөг хариу үйлдэл идэвхжүүлэх + Гэрэлтүүлгийг хадгалах ба сэргээх + Дүүрэн дэлгэцийн горимд орох / гарах үед гэрлийн түвшинг санадаг + Гэрэлтүүлгийн авто горимын дохиог идэвхжүүлэх + Гэрлийг хамгийн бага утга руу гүйлгэхэд системийн автоматаар гэрэл тохируулах функц идэвхжнэ + Авто + Гүйлгэх давхарлалын хугацаа + Давхарлал харагдах хугацаа миллисекундээр + Гүйлгэх давхарлалын арын дэвсгэрийн тунгалаг байдал + Тунгалаг байдлын түвшинг тохируулах (0 нь тунгалаг, 100 нь бүрхэг) + Гүйлгэж тохируулах гэрлийн өнгө + Гэрэлтүүлгийн хяналтын явц илтгэгчийн өнгийг тохируулах + Гүйлгэж тохируулах дууны өнгө + Дууны хэмжээний хяналтын дэвжээний өнгийг тохируулах + Гүйлгэх давхарлагчийн хурдны өнгө + Тоглуулах хурдны удирдлагын явцын түүжүүрийн өнгийг тохируулна уу + Гүйлгэж тохируулах текстийн хэмжээ + Дэлгэцэн дээрх текстийн хэмжээг тохируулах (1-30) + Гүйлгэх хөдөлгөөний босго + Тохиргоог идэвхжүүлэхед шаардлагатай гүйлгэх зайг тохируулах + Дууны хэмжээг гүйлгэж өөрчлөх мэдрэмж + Өндөр утга нь дууны хэмжээний нэг шат өөрчлөхөд илүү их гүйлгэх шаардлагатай болгоно + Дууны хэмжээг өөрчлөх алхам + Дууны түвшний нэмэгдэл + Хамгийн дээд дууны түвшнээс дээшлүүлэн гүйлгэхэд дууг нэмэгдүүлдэг бөгөөд энэ нь чанга дууг гажигдуулах эрсдэлтэй + Төхөөрөмжийн утга + Гэрэлтүүлгийн гүйлгэх мэдрэмж + Өндөр утга нь гэрэлтүүлгийн алхам бүрт илүү их гүйлгэх шаардлагатай + Хурдны гүйлгэх мэдрэмж + Өндөр утга нь хурдны алхам бүрт илүү их гүйлгэх шаардлагатай + Хурдны алхам + Хурдны бүсийн өндөр + Хурдны дохионы бүсийн өндрийг тоглуулагчийн өндрийн хувиар (5-75) заана + Хурд + Гүйлгэж тохируулах давхаргын загвар + Хэвтээ давхарга + Хэвтээ давхарга (минимал - дээр) + Хэвтээ давхарга (минимал - төв) + Дугуй давхарга + Дугуй давхарга (минималист) + Босоо давхарга + Босоо давхарга (минималист) + Видеог солихын тулд гүйлгэхийг идэвхжүүлэх + Дараагийн эсвэл өмнөх видео руу шилжихдээ бүтэн дэлгэц горимд гүйлгэх + Үйлдэл хийх товчнууд + Босоо болон бүтэн дэлгэц горимд аль үйлдэл товчнууд харагдахыг сонгох + Босоо дэлгэцийн товчнууд + \"Like\" болон \"Subscribe\" товчлуурын гэрлийг идэвхгүй болгох + Үйл бүтээгч дурдах үед \"Like\" болон \"Subscribe\" товчнууд гэрэлтэхээс сэргийлнэ + Үйлдэл гүйцэтгэх мөрийг нуух + Like болон Dislike товчийг нуух + Хуваалцах товчийг нуух + \"Зар зогсоох\" товчийг нуух + Сэтгэгдэл нуух товчийг нуух + \"Холбох\" товчийг нуух + \"Мэдэгдэх\" товчийг нуух + \"Ремикс\" товчийг нуух + \"Татаж авах\" товчийг нуух + \"Hype\" товчийг нуух + \"Сурталчлах\" товчийг нуух + \"Баярлалаа\" товчийг нуух + \"Асуух\" товчийг нуух + Суваг болон Бүртгүүлэх товчлуурыг нуух + Үйлдэл гүйцэтгэх мөрний эхэнд байрлах сувгийн аватар болон Бүртгүүлэх товчлуурыг нууна + Бүртгэх товчийг нуух + Үйлдэл гүйцэтгэх хэсгийн эхэнд байгаа сувгийн дүрсийн хажуугийн Бүртгэх болон Нэгдэх товчнуудыг нууна + \"Clip\" товчийг нуух + Энэ нь зарим хэрэглэгчийн бүртгэл дээр ажиллахгүй байж магадгүй + \"Дэлгүүр\" товчийг нуух + \"Хадгалах\" товчийг нуух + \"Илүү」 товчийг нуух + Бүрэн дэлгэцийн товчнууд + Таалагдаагүй товчийг нуух + Таалагдсан товчийг нуух + \"Шууд чат\" товчийг нуух + Хольж тоглох товчийг нуух + \'Илүү их видео\' товчийг нуух + Жагсаалтын товчийг нуух + Хурдан үйлдлүүдийн дээд зай + Хурдан үйлдлүүдийн хэрэглүүрийн дээд зайг тохируулах (0-32 dp) + Хаяг + Автомат хадмал орчуулга болон хадмал орчуулгын кукиг удирдах + Автомат хадмалтын харагдац + Үргэлж харуулах + Хэзээ ч харуулахгүй + Дууг нээхэд харуулах + Дууг хаасан үед харуулах + Харуулах дээрх хэдэн хэлний сонголт + Эхлээд нийлүүлэгчийн харуулах дээрх бичвэрийг сонгож, дараа нь сонгосон хэл рүү автоматаар орчуулна + Унтраах + Автоматаар орчуулах нэмэлт хэлнүүдийг нэмэх + Автоматаар орчуулах цэсэнд илүү олон хэл нэмнэ + Хаягллын cookie-г тохируулах + "Cookies дутуугаас болж YouTube-ийн автоматаар орчуулсан хадмал гарчиг ажиллахгүй байх алдааг засна + +'Хадмал гарчгийн cookies'-ийг заавал оруулна уу" + Хаягны куки + Timed Text API (Caption Data API) хүсэлтүүдэд тохируулах cookie-нууд + Хаягны кукиг авах + Cookie-г татахын тулд энд дарна уу + Кукиг татаж авж чадсангүй + Cookie-нууд аль хэдийн байна + Cookie-г амжилттай шинэчиллээ + Тэмдэглэлийг засах + Тэмдэглэлийг ачаалахад саад болдог урьдчилсан шалгалтын алдааг засна + Дүрсэн зураг доторх хадмал гарчгийг харуулах + Навигацийн товчлуурууд + Доод навигацийн хэсэгт аль товчнуудыг харуулахыг сонгоно уу + Нүүр хуудасны товчийг нуух + Shorts товчийг нуух + Үүсгэх товчийг нуух + Бүртгэлтэй товчийг нуух + Мэдэгдэл товчийг нуух + Хайлт товчийг харуулах + Хайлт товчны байрлал + Тохиргоо товчийг харуулах + Тохиргоо товчны байрлал + Тохиргооны товчны үйлдэл + Идэвхжүүлсэн үед Morphe-ийн тохиргоог, идэвхгүй болгосон үед YouTube-ийн тохиргоог нээнэ + \"Үүсгэх\" товчийг \"Мэдэгдэл\" товчтой сольно + Хэрэв энэ тохиргоог өөрчилсөн ч нөлөөлөхгүй бол Инкогнито горим руу шилжүүлж үзнэ үү + Навигацийн товчлуурын шовруудыг нуух + Шинэ контентын цэгийг нуух + Навигацийн мөрийг нуух + "Энэ нь 'Та' табв зэрэг навигацийн хэсгийг бүрэн нуух болно. + +Тохиргоонд хандахын тулд General → Toolbar → Show Settings хэсгээс Тохиргооны товчийг идэвхжүүлнэ үү" + Нарийн навигацийн товчнуудыг идэвхжүүлэх + Товчлууруудын хорзон зайн зайг багасгана + Тайлбаргүй навигацийн товчнуудыг идэвхгүй болгох + YouTube-ээс тайлбаргүй илгээсэн товчнуудыг тайлбартай харуулна + Баруун дээд хэсгийн навигацийн табуудыг идэвхгүй болгох + YouTube-ийг Нүүр хуудасны дээд хэсэгт таб болгон шилжүүлсэн тохиолдолд Навигацийн хэсэгт Бүртгэлүүдийг харуулна + Навигацийн хэсгийн анимэйшнийг идэвхжүүлэх + Tab-уудын хооронд шилжих үед хөдөлгөөнт эффект нэмнэ + Навигацийн цувраас автоматаар нуухыг идэвхгүй болгох + Гүйлгэх үед навигацийн цуврайг харагдахуйц байлгана + Навигацийн зурвасын загвар + Үндсэн + Хагас тунгалаг + Тунгалаг + Тунгалаг бус + Энэхүү загварыг таны төхөөрөмж эсвэл YouTube-ийн хувилбар дэмжихгүй байж болох бөгөөд энэ нь бүтцийн алдаа эсвэл програм гацахад хүргэж болзошгүй + Баруун дээд хэсгийн цэс + Хэрэгсэлний мөрөнд (toolbar) аль бүрэлдэхүүн хэсгүүдийг харуулахыг сонгоно уу + Cast товчийг нуух + Чат товчийг нуух + Микрофон товчийг нуух + Хайлт товчийг нуух + Идэвхжүүлсэн үед товчлох үед Morphe-ийн тохиргоо, удаан дарах үед YouTube-ийн тохиргоог нээнэ; солихын тулд идэвхгүй болгоно уу + Гар цэс + Хөвөгц цэсэнд аль хэсгүүд гарч ирэхийг сонгоно уу + \"Нэмэлт тохиргоо\" / Илүү цэснээс нуух + \"Орчин тойрнгийн горим\" цэснээс нуух + "'Audio Track' цэс нуугдсан байна + +'Audio Track' цэсийг харуулахын тулд 'Spoof video streams'-ийг 'Android Studio' болон 'Android VR Downgraded'-ээс бусад клиент рүү өөрчилнө үү" + \'Дууны зам\' цэсийг нуух + \'Дууны зам\' цэсний доод хэсгийг нуух + Хартулга цэсийг нуух + Хартулга цэсний доод хэсгийг нуух + Хартулга цэсний дээд хэсгийг нуух + \"Тусламж & санал хүсэлт\" цэснээс нуух + \'YouTube Music-ээр сонсох\' цэснүүдийг нуух + \'Дэлгэцийг түгжих\' цэснүүдийг нуух + \"Видеог давтах\" цэснээс нуух + \'Явдал зуур\' цэсийг нуух + Чанарын цэснүүдийг нуух + Чанарын цэсний доод хэсгийг нуух + Чанарын цэсний дээд хэсгийг нуух + \"Унтах цаг\" цэснээс нуух + \'Тоглуулах хурд\' цэснүүдийг нуух + \"Тогтмол дууны түвшин\" цэснээс нуух + \'VR-аар үзэх\' цэснүүдийг нуух + Автоматаар тоглуулах урьдчилан хаラルтыг нуух + Автоматаар тоглуулах эхлэхээс өмнө дараагийн видеоны урьдчилгааг бүтэн дэлгэцээр нуух + Давхлагдсан товчнууд + Видео тоглуулагчийн дээр аль товчнуудыг харуулахыг сонгоно уу + Автоматаар тоглуулах товчийг нуух + Хаягдлыг нуух товчлуур + Cast товчлуурыг нуух + Хумигдуулгах товчлуурыг нуух + Дүүрэн дэлгэцийн товчлуурыг нуух + Тоглуулагчийн удирдлагын арын дэвсгэрийг нуух + Видео тоглуулах удирдлагын эргэн тойрон дахь бараан сүүдрийг нуух + Удирдлагын товчнуудын арын дэвсгэрийн тунгалаг байдал + Тоглуулах удирдлагын товчлуур болон хугацааны зурхын ард байгаа дэвсгэрийн тунгалаг байдлыг тохируулах (0 нь үл үзэгдэх, 100 нь бүрэн харагдах) + Өмнөх & Дараах товчлууруудыг нуух + Тохиргооны товчлуурыг нуух + Төгсгөлийн дэлгэцийн картуудыг нуух + Орчин тойрны горим + Ambient горимын харагдах байдал болон тэжээлийн хязгаарлалтыг удирдах + Орчин тойрны горимын хязгаарлалтыг алгасах + Батерей хэмнэх горимд Ambient горимыг идэвхтэй байлгах боломжийг олгоно + Ambient горимыг идэвхгүй болгох + Бүрэн дэлгэц дээрх Ambient горимыг идэвхгүй болгох + Мэдээллийн картуудыг нуух + Тоон гүйлгээнх хөдөлгөөг идэвхгүй болгох + Like болон үзэлтийн тооны тооллогын хөдөлгөөнийг зогсооно + Видео тоглуулагчийн гүйлгэгчийг нуух + Видео miniature-ийн гүйлгэгчийг нуух + Таны үзсэн видеонуудын miniature дээр гардаг үзэлтийн явцын түүхийг нуух + Дэлгэц дүүрэн том гүйлгэгчийг идэвхжүүлэх + Shorts-ыг нуух + Сувгийн хуудаснаас нуух + Нүүр хуудасны feed & холбоотой дүрсүүдээс нуух + Хайлтын үр дүнгээс нуух + Бүртгэлтэй feed-ээс нуух + Видео тайлбараас нуух + Үзсэн түүхээс нуух + Shorts тоглуулагч + Shorts тоглуулагчид аль бүрэлдэхүүн хэсгүүдийг харуулахыг сонгох + Хоёр удаа товшлон лайк дарахыг идэвхгүй болгох + AI товчийг нуух + \'Автоматаар орчуулсан\' шовгийг нуух + Сувгийн хэсгийг нуух + Сэтгэгдэл товчийг нуух + Эффект товчийг нуух + Видео холбоосын шовгийг нуух + Дохио заагчийг нуух + \'Ногоон дэлгэц\' товчийг нуух + Хаштаг товчийг нуух + Мэдээллийн хэсгийг нуух + Нэгдэх товчийг нуух + Танд таалагдсан товчийг нуух + Танд таалагдсан товчны урсгал хөдөлгөөнийг нуух + Шууд дамжуулалтын урьдчилан харалтыг нуух + Байршлын шовжийг нуух + Навигацийн хэсгийг нуух + \'Шинэ нийтлэл\' товчийг нуух + Түр зогсоох үеийн давхаргуур товчнуудыг нуух + Сэтгэгдлийн урьдчилгаа харах хэсгийг нуух + Ремикс товчийг нуух + Хадгалах товчийг нуух + \'Хөгжим хадгалах\' товчийг нуух + Хайлтын санал болголтыг нуух + Хуваалцах товчийг нуух + Дэлгүүр товчийг нуух + Дууны товчийг нуух + Дууны мета өгөгдлийн шовгийг нуух + Бүртгүүлэх товчийг нуух + \'Super Thanks худалдаж авах\' товчийг нуух + Тэмдэглэгдсэн бүтээгдэхүүнийг нуух + Удахгүй товчийг нуух + \'Энэхүү дууг ашиглах\' товчийг нуух + \'Энэхүү загварыг ашиглах\' товчийг нуух + Видеоны гарчгийг нуух + Төгсгөлийн дэлгэцийн санал болгосон видеог нуух + "Видео дууссаны дараа гарч ирэх санал болгох видеоны давхаргаыг нуух + +Энэ тохиргоо нь зөвхөн автоматаар тоглуулах функц унтраалттай үед ажиллана. Та үүнийг YouTube дээр өөрчилж болно: +Тохиргоо → Тоглуулах (эсвэл Автоматаар тоглуулах) → Дараагийн видеог автоматаар тоглуулах" + Холбоотой дүрсүүдийг нуух + \'Илүү олон видео\' overlay-г нуух + Дүүрэн дэлгэцийн горимд дээшээ гүйлгэхэд гарч ирэх \'Илүү олон видео\' давхарлагчийг нуух + Видеоны цаг хугацааг нуух + Тоглуулагчийн поп-ап панелуудыг идэвхгүй болгох + Жагсаалт болон шууд чатын панелыг автоматаар нээгдэхээс сэргийлнэ + Жагсаалтын автоматаар тоглуулалтыг идэвхгүй болгох + Түүнэглэлийг дараагийн видео руу автоматаар шилжүүлэхээс сэргийлнэ + \'Үргэлжлүүлэн үзэх үү?\' гэсэн сануулгыг идэвхгүй болгох + Видео дуусах үед дүүрэн дэлгэцийн горимоос гарах + Идэвхгүй болгох + Зөвхөн босоо + Зөвхөн хэвтээ + Босоо болон хэвтээ + Бүрэн дэлгэц дээр хэвтээ горимыг албадах + Бүрэн дэлгэц рүү шилжихэд тоглуулагчийг хэвтээ горимд эргүүлнэ. Бүрэн дэлгэц дээр хэвтээ горимоор нээгддэг жижиг дэлгэцтэй төхөөрөмж дээр ажиллахгүй + Бүрэн дэлгэцийн дохиолт + Дүүрэн дэлгэцийн горим дахь тоглуулах дохио + Босоо дэлгэцийн дээшээ гүйлгэх дохиолтыг идэвхгүй болгох + Босоо дэлгэцийн доошоо гүйлгэх дохиолтыг идэвхгүй болгох + Хэвтээ горимд доошоо гүйлгэх дохиог идэвхгүй болгох + Томруулах дохиог идэвхгүй болгох + Хуруугаар чихэж томруулахыг сэргээх + Видеог бүтэн дэлгэц горимоор нээх + Идэвхгүй + Босоо + Хэвтээ + Видеог бүтэн дэлгэцээр томруулах хэмжээ + Үндсэн + Сунах (дэлгэцийг дүүргэх) + Томруулах (дүүргэж таслах) + Видеоны хэмжээг өөрчлөх товчийг харуулах + Үндсэн, Сунгах, Томруулах горимуудын хооронд шилжүүлэхээр дарна уу + Зөвхөн бүтэн дэлгэцээр харуулах + Босоо тоглуулагч дээрх видеоны хэмжээг өөрчлөх товчийг нуух + Дууг хаах товчийг харуулах + Тоглуулагчийн давхарлалтын бүдэг байдал + Тунгалаг байдлын түвшинг тохируулах (0 нь тунгалаг, 100 нь бүрэн харагддаг) + Хайлтын мөр + Идэвхгүй + Өргөн + Маш өргөн + Төхөөрөмжийн бүтэц + Үндсэн + Утас + Таблет + Автомашин + "Өөрчлөлтүүдэд дараах зүйлс багтсан: + +Таблетын бүтэц +• Нийгэмлэгийн нийтлэлүүдийг нуусан +• Feed дээр тоглуулах тохиргоо боломжгүй +• Shorts-т Remix болон Дууны товчлуур байхгүй +• 'Илүү видео' давхарлага боломжгүй байж болно +• Хуучин видеоны үйлдэлний бариул нь нуугдаагүй хэвээр харагдана +• Цэсний зүйлс зөв ажиллахгүй байж болно + +Автомашины бүтэц +• Feed-ийг сэдэв болон сувгуудаар ангилсан +• Feed дээр тоглуулах тохиргоо боломжгүй +• Shorts-т Remix болон Дууны товчлуур байхгүй +• 'Илүү видео' давхарлага боломжгүй байж болно +• Shorts-ыг ердийн тоглуулагчаар нээх +• Хуучин видеоны үйлдэлний бариул нь нуугдаагүй хэвээр харагдана" + Тоглуулагчид таблетын бүтцийг идэвхжүүлэх + "Тоглуулагчийн хуучин бүтцийг сэргээнэ + +Хязгаарлалт: Хэрэв 'Видеоны хуучин үйлдлийн мөрийг сэргээнэ' гэсэн тохиргоог унтраасан бол 'Илүү олон видео' гэсэн давхарга ажиллахгүй байж болно" + 21.11.48 - Хуучин хадмал орчуулгын үйлдлийг сэргээх + 20.39.41 - Хуучин хадгалалтыг тоглолтын жагсаалтад сэргээх + 20.28.41 - Тод дүрсгүүдийг идэвхгүй болгох + 20.13.41 - Видео тоглуулагчийн доор сувгийн нэрийг сэргээх + Системийн хуваалцах цэсийг нээх + Агуулга хуваалцах үед апп доторх цэсний оронд системийн хуваалцах цэсийг нээнэ + YouTube Music-ийн товчнуудыг даромжлуулах + Үндсэн YouTube Music апп-ын оронд таны илүүд үзэх хөгжмийн апп-ыг нээнэ + Хөгжмийн апп-ын багцын нэр + Таны суулгасан хөгжмийн апп-ын багцын нэр + Эхлэх хуудас + Өгөгдрийн + Бүх бүртгэлүүд + Сувгуудыг үзэх + Курс / Суралцах + Хайгуул + Загвар & гоо сайхан + Тоглоом + Түүх + Сан + Таалагдсан видеонууд + Шууд + Кино + Хөгжим + Мэдээ + Мэдэгдэл + Жагсаалтууд + Хайх + Дэлгүүр + Shorts + Спорт + Бүртгэлүүд + Трэнд + Виртуал бодит байдал + Дараа үзэх + Таны клипүүд + Эхлэхэд үргэлжлүүлэн тоглуулахыг идэвхгүй болгох + Shorts-ийг дараах хэрэгслээр нээх + Shorts тоглуулагч + Ердийн тоглуулагч + Егөөрийн тоглуулагчийн бүтэн дэлгэц + Shorts-ыг автоматаар тоглуулах + Одоогийн Short-ыг давтахын оронд дараагийн Short руу автоматаар шилжинэ + Ард фондод автоматаар тоглуулах + Ард фондод тоглуулах үед дараагийн Short руу автоматаар шилжинэ + Live Shorts-ийг алгасах + Shorts feed-д гарч ирэх шууд дамжуулалтыг алгасах + Feed дээр тоглуулах + Үргэлж асаалттай + Зөвхөн Wi-Fi + Унтраах + Эх alkuу гарчигуудыг сэргээх + Автоматаар орчуулсан гарчиг, тайлбаруудын оронд видеоны эх хувь гарчиг, видео болон сувгийн тайлбаруудыг харуулна + Гарчиг ачаалж байна… + Feed-ийн авто шинэчлэлтийг идэвхгүй болгох + Feed-үүдийг хуучирсны дараа автоматаар шинэчлэгдэхээс сэргийлнэ + Фийдийн картын бүтэц + Сонгодог + Картууд + Таалагдах, сэтгэгдэл үлдээх болон хадгалах товчлууртэй картууд + Сэтгэгдэл + Мини тоглуулагчийг үргэлжлүүлэн ажиллуулахыг идэвхгүй болгох + Аппликейшн эхлүүлэхэд гардаг YouTube Premium-ийн боломж болох \'Үргэлжлүүлэн үзэх\' минитоглуулагчийг идэвхгүй болгоно + Жижиг тоглуулагч + Багассан видео тоглуулагчийн үзэмж болон үйлдлийг тохируулах + Жижиг тоглуулагчийн төрөл + Гарчиг болон сувгийг нуух + Идэвхгүй болгосон + Үндсэн + Минималист + Минимал 2 + Таблет + Орчин үеийн 1 + Орчин үеийн 2 + Орчин үеийн 3 + Орчин үеийн 4 + Дугуй булангуудыг идэвхгүй болгох + Чирж байршуулахыг идэвхгүй болгох + Багассан тоглуулагчийг дэлгэцийн бусад буланд чирж зөөхөөс сэргийлнэ + Хэвтээ чирэх дохиог идэвхгүй болгох + Мини тоглуулагчийг дэлгэцээс хэвтээгээр гүйлгэж хаахаас сэргийлнэ + Хэвтээ чирж тоглуулалтыг зогсоох + Жижиг тоглуулагчийг дэлгэцээс гаргаж чирвэл тоглуулалт түр зогсоно + Чирсэн байрлалын төлөвийг санаж байх + Мини тоглуулагчийг дэлгэцнээс гаргаж чирвэл түүний UI төлөв хадгалагдана + Давхлагдсан товчнуудыг нуух + Үндсэн хэмжээ + Дэлгэцэн дээрх эхлэх хэмжээг пикселээр тохируулна + Пикселийн хэмжээ %1$s ба %2$s-ын хооронд байх ёстой + Дахин бүрхэлийн тунгалаг байдал + Тунгалаг байдлын түвшинг тохируулна (0 нь тунгалаг, 100 нь бүрэн харагдах) + Буцах товчлуур фидийг хаана + Буцах товчийг дарах үед фидийн дээд хэсэг рүү гүйлгэхийн оронд аппликейшнээс шууд гарна + Анимацийг албадан ажиллуулах + Өгөгдсөн + Үргэлж асаалттай + Үргэлж унтраалттай + Төлөвийн мэдэгдлийн зургийг нуух + Системийн төлөвийн мэдэгдлийн зургийг нууна. Түр зуур харахын тулд дээд ирмэгээс доошоо гүйлгээрэй + Статус баар дээд хэсгийг халхлахыг засах + Статус баарыг тогтмол байлгаж, дээд хэсгийг халхлахгүй, тоглуулагчийн дээр хоосон зай үлдээхгүй болгоно. Хэрэв апп гацах эсвэл дээд хэсэг буруу харагдах бол үүнийг унтраагаарай + Гэрэлтэл + Энэ төхөөрөмж Glow splash screen-ийг дэмжихгүй байна + Цайвар сэвийн арын дэвсгэр + Цагаан + Material You (Цагаан) + Цайвар ягаан + Цайвар цэнхэр + Цайвар ногоон + Цайвар шар + Цайвар улбар шар + Цайвар улаан + Цайвар дэвсгэрийн өөрийн тохиргоо + Цайвар сэвийн дэвсгэрийн өнгийг тохируулах + Урд талын өнгийг өөрчлөх + Текст болон дүрс тэмдэгтүүдэд сэвийн өнгийг ашиглах + Градиент ачааллын дэлгэцийг идэвхжүүлэх + Тảiлолтын дэлгэц дээр нэг өнгөний оронд градиент фон харуулна + Seekbar-ын өөрийн гэсэн өнгийг идэвхжүүлэх + Урсгал заагчийн өнгө + Видеоны явцын түүхэнийн үндсэн өнгийг тохируулах + Урсгал заагчийн тодотгох өнгө + Видеоны явцын хэрэглүүрийн туслах өнгийг тохируулна + Seekbar-ийн өнгөний утга буруу байна + Эх alkuу аналог + Эх бялуу + Эх металл + Толгой хэсгийн лого + Өгөгдсөн + Энгийн + Засах + Зургийн бүс нутгийн хязгаарлалтыг алгасах + Хязгаарлалттай улс орнуудад зураг харагдахгүй байх асуудлыг засахын тулд thumbnail-ийн хүсэлтийг өөр сервер рүү шилжүүлнэ + DeArrow гарчиг + Нүүр хуудас + Бүртгэлтэй хуудас + Та + Тоглуулагчийн жагсаалт & санал болгох + Хайлтын үр дүн + DeArrow дүрс тэмдгийг харуулах + Олон нийтийн тусламжтайгаар бүтээсэн видео гарчиг дээр DeArrow дүрс тэмдгийг харуулна + Чөлөөтэй горим + Хэрэв DeArrow-ийн хэрэглэгчид эх хувийн гарчиг нь илүү таалагдсан гэж санал илэрхийлсэн бол DeArrow-ийн гарчгийн оронд эх хувийн гарчгийг харуулна + Эх хувийн гарчгийг харуулах хамгийн бага чөлөөтэй санал + Эх хувийн гарчгийг харуулахад шаардлагатай хөгжилтэй эсвэл бүтээлч гэх мэт нэг ангиллын чөлөөтэй саналын тоо + DeArrow-ийн miniature-үүд + Эх хувийн miniature-ууд + DeArrow & Эх хувийн miniature-ууд + DeArrow & Хөдөлгөөнгүй зураг + Хөдөлгөөнгүй зураг авна + "DeArrow нь кликбейт гарчиг болон нүүр зургуудыг илүү нарийвчлалтай, бага сенсацичлагдсан, олон нийтийн оролцоотой хувилбараар сольдог. Гарчгуудыг олон нийт бичиж, санал хураадаг бөгөөд нүүр зургуудыг видеоноос авсан кадруудаар сольдог + +Видеонуудыг хашилагдсан видео ID-ийн богино урьдчилсан хэсгээр хүсдэг тул сервер таны яг аль видеог үзэж байгааг мэдэх боломжгүй. Хэрэв видео DeArrow-ын нүүр зураггүй бол alkuз эсвэл зогсоосон кадрыг харуулна" + Холболтын алдааны мэдэгдлийг харуулах + DeArrow сервер ажиллахгүй үед мэдэгдэл харуулна + API төгсгөл + DeArrow-ийн thumbnail кэшийн төгсгөлийн URL хаяг + Видеоны хөдөлгөөнгүй дүрстүүд + Статик зургуудыг видео бүрийн эхлэл / дунд / төгсгөлөөс авдаг. Эдгээр зургууд нь YouTube-д суурилуулсан бөгөөд гадны API ашигладаггүй + Видеоны фрэймийг гаргаж авах + Эхлэл + Дунд + Төгсгөл + DeArrow түр хугацаагаар боломжгүй байна (төлөвийн код: %s) + DeArrow түр хугацаагаар боломжгүй байна + Тоглуулагчийн хуучин товчлуурын загварыг сэргээх + Видео тоглуулагчийн товчлууруудыг өмнөх нарийн загвар руу буцаана + Төхөөрөмжийн хэмжээг хуурах + Үүнийг идэвхжүүлснээр видеоны илүү өндөр чанаруудыг нээх боломжтой + Үүнийг идэвхжүүлснээр видео тоглуулах үед гацалт үүсэх, батерейны нас буурах болон бусад тодорхойгүй сөрөг нөлөөг үзүүлж болзошгүй + Хөдөлгөөний хариу үйлдэл + Төхөөрөмжийн чичиргээг идэвхжүүлэх үйлдлүүдийг сонгох + Бүлэг хэсгүүдийн хөдөлгөөний хариу үйлдлийг идэвхгүй болгох + Нарийвчилсан хайлтын хөдөлгөөний хариу үйлдлийг идэвхгүй болгох + Хайлт буцаах хөдөлгөөний хариу үйлдлийг идэвхгүй болгох + Дээр нь дарж барих тактиль хариу үйлдлийг идэвхгүй болгох + Томруулах хөдөлгөөний хариу үйлдлийг идэвхгүй болгох + Хэрэв та саяхан бүртгэлийн мэдээллээ өөрчилсөн бол MicroG-г устгаж, дахин суулгана уу + Холбоосын дахин чиглүүлэлтийг алгасах + Хяналтын холбоосыг (youtube.com/redirect) алгасаад, эх холбоосыг шууд нээнэ + Холбоосыг хөтөч дээр нээх + Холбоосыг апп доторх хөтөчтэй бус гадны хөтөчөөр нээнэ + Тоглуулах буферын хэмжээ + Өгөгдсөн + Бага + Дунд + Хамгийн их + Видео хоцроолт + Bluetooth чихэвч зэрэг дуу хоолой хоцролттой үед видеог синхрончлохын тулд хоцроолт оруулна (0-5000 ms) + Зөвхөн Bluetooth аудиотой үед хоцроолт оруулах + Зөвхөн Bluetooth аудио холбогдсон үед видеог хоцроосноор утасны чанга яригчийн синхрончлол хэвээр ханана + Видео хоцролтын товчийг харуулах + Хоцролтыг 0 мс болгон шинэчлэхийн тулд дарж түрээсгэ + Видео хоцролтын цэс харуулах + Хоцролтыг миллисекундээр + Автомат + Видео чанарыг санахаа + Таны гараар сонгосон чанарыг цаашдаа бүх видеонд хэрэглэнэ + Чанарын өөрчлөлтийн мэдэгдэл + Шинэ үндсэн чанар хадгалагдсан үед мэдэгдэл харуулах + Wi-Fi сүлжээ дээрх видеоны стандарт чанар + Мобайл сүлжээний видеоны үндсэн чанар + Shorts чанарыг санахаа + Таны гараар сонгосон чанарыг цаашдын бүх Shorts-д хэрэглэнэ + Wi-Fi сүлжээний Shorts-ын үндсэн чанар + Мобайл сүлжээний Shorts-ын үндсэн чанар + Мобайл төхөөрөмжийн үндсэн чанарыг %s болгож өөрчиллөө + Wi-Fi-ийн үндсэн чанарыг %s болгож өөрчиллөө + Shorts-ийн мобайл чанарыг %s болгож өөрчиллөө + Shorts-ийн Wi-Fi чанарыг %s болгож өөрчиллөө + Тоглуулах хурдны товчийг харуулах + Хурдыг анхны утгаар нь сэргээхийн тулд даран удаан барина уу + Чанарын товчийг харуулах + Чанарыг анхны утгаар нь сэргээхийн тулд даран удаан барина уу + Чанарын товчлууран дээр нарийвчлалыг харуулах + FHD гэх мэт шошгоны оронд 1080 гэх мэт нарийвчлалыг харуулна + Тоглолтын хурдны тусгай цэс + Тоглолтын хурдны хуучин цэсийг сэргээх + Тоглолтын хурдны тохиргоо + Өөрийн хүссэн тоглуулах хурдыг оруулна уу + Тохируулсан тоглуулах хурд нь %s-ээс бага байх ёстой + Тоглолтын хурдны тусгай тохиргоо буруу байна + Автомат + Тоглуулах хурдыг даран удаан барих + Дэлгэц дээр даран удаан барих үеийн хурдны үржвэрийг тохируулна уу, идэвхгүй болгохын тулд 0 гэж тохируулна уу + Тоглуулах хурдыг санаж байх + Таны гараар сонгосон хурдыг цаашдын бүх видеод хэрэглэнэ + Хурд өөрчлөлтийн мэдэгдэл + Шинэ үндсэн тоглуулах хурд хадгалагдсан үед мэдэгдэл харуулах + Тоглолтын үндсэн хурд + Shorts-ын тоглуулалтын үндсэн хурд + Shorts-ийн тоглуулах хурдыг санахаа + Таны Shorts-ийн хурдыг гараар сонгосон тохиргоог цаашдын бүх Shorts-т хэрэглэнэ. Хэрэв унтраасан бол Shorts тоглуулагчийг хаах үед хурд анхны тохиргоо руугаа шилжинэ + Shorts цэсэнд тоглуулах хурдыг харуулах + Shorts тоглуулагчийн цэсэнд тоглуулах хурдны хэсгийг нэмнэ + Тоглуулах хурд · %s + Тоглуулах хурдыг дараах байдлаар өөрчиллөө: %s + Хөгжимд хэвийн хурдыг ашиглах + Хөгжмийн бичлэгийг үргэлж 1.0x хурдаар тоглуулах + Сувгийн хурдны зөвшөөрлийн жагсаалт + Зөвшөөрлийн жагсаалтад байгаа сувгуудын видеонууд үргэлж 1.0x хурдтайгаар тоглогдоно + Сувгийг хурдны зөвшөөрлийн жагсаалтад нэмэх + Сувгийг хурдны зөвшөөрлийн жагсаалтаас хасах + Аудио өнгийн хяналтыг идэвхжүүлэх + Тоглуулалтын аудио өнгийн анхны тохиргоо + Аудио өнгийн анхны тохиргоог %s болгож өөрчиллөө + Аудио хугацааг сунгах + Хурд болон өнгийг тус тусад нь тохируулах боломжийг олгоно, энэ нь аудио дохио алдаа үүсгэж болзошгүй + Шууд дамжуулалтын тоглуулах байрлалыг санахаа + Үргэлжилж буй шууд дамжуулалтын хувьд одоогийн шууд байрлал руу шилжих вместо тоглуулах байрлалыг санаж, тэндээс үргэлжлүүлнэ + Энэхүү функц нь 24/7 шууд дамжуулалт болон бусад \"үргэлж идэвхтэй\" стримүүд дээр ажиллахгүй + HDR видеог идэвхгүй болгох + HDR видеог албадлагаар ажиллуулах + Аппликейшнийг энэ төхөөрөмжийг HDR дэмждэг гэж таниулна + "HDR тайлал ажиллаж байгаа боловч HLG-г нуусан зарим HyperOS төхөөрөмжүүдийнхээ адил YouTube-ээс HDR санал болгохгүй байгаа тохиолдолд үүнийг ашиглана. + +Хязгаарлалт: +• Дэлгэц HDR-ийг бодитоор харуулах боломжгүй бол өнгө алдаж харагдаж болно +• Тайлагч HDR-ийг дэмжихгүй бол тоглуулалт амжилтгүй болж болно +• HDR нь видео урсгалыг хуурамчаалах клиентээс хамаарагдсаар байх болно" + Видео кодек + Үндсэн + AVC-г албадлаар идэвхжүүлэхийн тулд \'Видео урсгалыг дуурайх\'-ыг \'Android Studio\', \'Android VR\' эсвэл \'visionOS\' болгон өөрчилнө үү + "VP9 +• Баттерейны ашиглалтыг сайжруулж, тоглуулах үеийн гацах үзэгдлийг засаж болно +• AV1-ээс илүү интернэт өгөгдөл ашиглана + +AVC (H.264) +• 'Android Studio', 'Android VR', эсвэл 'visionOS' Видео урсгалыг дуурайх клиент шаардлагатай +• Баттерейны ашиглалтыг улам сайжруулж болно +• Хуучин төхөөрөмжүүд дээрх дутуу видео нарийвчлалыг сэргээж болно +• Хамгийн дээд нарийвчлал нь 1080p +• HDR видеонууд AVC-г ашиглахгүй байж болно +• Зарим төхөөрөмжүүд AVC-г албадлаар идэвхжүүлж чаддаггүй" + Нарийвчилсан чанарын цэс нээх + Чанарын сонголтын цэснээс алгасаж, Нарийвчилсан чанарын цэсийг шууд нээнэ + Premium чанарын сонголтуудыг нуух + Видеоны чанарыг тэргүүн эрэмбэjéх + Кодекийн үр ашгаас илүү боломжтой хамгийн өндөр видео чанарыг тэргүүн эрэмбэжүүлнэ + Хэрэв энэ тохиргоог унтраавал хуучин видеонууд дээр видеоны чанарын сонголтууд харагдахгүй байж болно + \'Android VR\' / visionOS AV1-ийг зөвшөөрөх + \'Android VR\' / visionOS клиент ашиглах үед програм хангамжийн AV1 декодчилолтыг зөвшөөрнө + "Энэ тохиргоог идэвхжүүлснээр AV1 программ хангамжийн декодинг ашиглаж магадгүй. + +AV1-тэй видеог тоглуулах үед гацалт үүсэх эсвэл кадр алдагдах магадлалтай" + Хуурамчлахын гаж нөлөө + • Тоглуулах явцад 1 минутын дараа алдаа гарч болзошгүй + • \'Аудио трек\' цэс байхгүй байна + • AV1 видео кодек байхгүй + • 360° VR immersive горим боломжгүй + • Тогтмол дууны түвшин боломжгүй + • Тоглуулалт 1 секундын өмнө дуусах магадлалтай + • Хэрэв деобфускаци амжилтгүй болвол тоглуулах үйлдэл алдаа зааж болзошгүй + • Эх хувилбарын аудиог албадах боломжгүй + • Android VR-т нэвтрэх шаардлагатай + • Чанар нь 720p-ээр хязгаарлагдаж болно + Тоглуулагчийн JavaScript хувилбар + \'Stats for nerds\'-д хуурамчаж харуулах + Хуучин видеоны үйлдлийн мөрийг сэргээх + "Видеоны үйлдлийн товчнуудыг нууж чаддаггүй алдааг засна + +Хязгаарлалт: Хуучин загварын 'Илүү видео' давхарга руу шилжинэ" + Тоглуулагчийн дүрсний загвар + Автомат + Өгөгдсөн + Нарийн + Бүдүүн + Засаж өөрчлөх + Shorts дүрсгийн загвар + Цувралын хяналт + Гарчигт цуврал хэлбэрээр дагаж, үзсэн хэсгээ хадгалаарай + Цувралын хяналтыг идэвхжүүлэх + Түүх хэсэгт \"Цуврал\" хуудас нэмж, гарчигт цувралыг дагах сонголтыг нэмнэ + Навигацийн мөрөнд цувралуудыг харуулах + Цувралын хяналтын товчийг харуулах + Одоогийн видеоны цувралыг дагах эсвэл нээх тоглуулагчийн товчийг нэмнэ + Дагаж буй цувралын явцыг тэмдэглэх + Хадгалсан цуврал болон тэмдэглэгээний тоглуулалтыг энэ утсан дээр тэмдэглэнэ + Цуцлах + Хадгалах + Тэмдэглэгээг хадгалах + Цувралын нэр + Цувралын тохиргоо + Сангийн тохиргоо + Ангиудыг шинэчлэх + Нэрийг өөрчлөх + YouTube-ээс тоглогын жагсаалтыг нээх + Сангаас устгах + Тоглолтын жагсаалтын холбоос эсвэл ID + Орон нутгийн бүх явцыг устгах + Түүхийг устгах + Орон нутгийн бүх явцыг устгах уу? + Энэ нь бүх видеоны тоглолтын байрлал, үзсэн тэмдэглэгээ болон үргэлжлүүлэн үзэх сонголтуудыг устгана. Хадгалсан цуврал болон тэдгээрийн каталого хэвээр үлдэнэ. Хэрэв бүртгэлийг идэвхжүүлсэн бол шинэ тоглолтын түүх бүртгэгдэнэ + Үзсэн гэж тэмдэглэх + Үзээгүй гэж тэмдэглэх + Автомат төлөвийг ашиглах + 0:00-аас дахин эхлүүлэх + 0:00-аас дахин тоглуулах + Төлөвийг хадгаллаа + Хадгалахыг дахин оролдох + Өмнөх 100 + Дараагийн 100 + Хадгалсан видеог нээх + Дотоод дэвшлиг устгагдлаа + Цувралуудаа үзэхийн тулд \"Та\" хэсгээс Түүхийг нээ + Үзсэн түүх + Цуврал + Хадгалсан цувралуудыг ачаалж байна... + Цувралын ангиудыг хянахын тулд тоглуулах жагсаалтыг дагана уу + Жагсаалтын тэмдэглэгээ + Ангиудыг шинэчилж байна... + Бүх цуврал + Үзсэн + Ангиудыг ачаалж байна… + Нэмэлт сонголтууд + "Зөвхөн хадгалсан цуврал болон тэмдэглэсэн видеонуудынхаа явцыг бүртгэнэ + +Энэхүү сан нь энэ төхөөрөмж дээр хадгалагдах бөгөөд YouTube-ийн бүх бүртгэлүүдээр хуваалцана. YouTube-ийн түүхийг түр зогсоох болон устгах тохиргоо үүнд нөлөөлөхгүй. Үүнийг устгахын тулд Цуврал → Илүү → Орон нутгийн явцыг устгах цэсийг ашиглана уу + +Нууц горимд, бүртгэлийн мэдээлэл олдохгүй үед эсвэл өөр бүртгэл рүү шилжсэн үед автоматаар бүртгэх функц зогсоно. Тухайн бүртгэлээр ашиглахын тулд хяналтыг дахин идэвхжүүлнэ үү" + Нууц горимд эсвэл YouTube одоогийн бүртгэлийн төлөвийг ачаалаагүй үед хяналтыг идэвхжүүлэх боломжгүй. Нүүр хуудас руу буцаж дахин оролдоно уу. Нэвтрэх шаардлагагүй. + Дотоод хяналтыг идэвхжүүлэх + Прогрессийн бүртгэл унтарсан байна + %1$s хэсгээс үргэлжлүүлэх + %1$s / %2$s анги үзсэн + %1$s-р анги · %2$s + %3$s-ын %1$s-%2$s ангиуд + Ангиудыг ачаалж үзэхийн тулд шинэчилнэ үү. Хувийн жагсаалтуудад бүртгэлийн хандалт шаардлагатай; тэдгээрийн тэмдэглэгээ YouTube дээр нээгдсээр байх болно + YouTube энэ ангиллыг нээж чадсангүй + YouTube энэ тоглолтын жагсаалтыг нээж чадсангүй + Анги сонгоно уу + Шинэчлэгдсэн + Үзэх боломжтой анги байхгүй байна + Хадгалагдсан видео · %1$s + Цувралыг дагах + Цувралыг үзэх + Үргэлжлүүлэх + Эндээс эхлэх + Эхлэх цэгийг хадгаллаад байна + Энэ хүртэл үзсэн + Эпизодуудыг үзсэн гэж тэмдэглэсэн + Буцаах + Идэвхжүүлэх + Энэ төхөөрөмж дээр явцыг тэмдэглэх + "Таны цуврал болон ахиц дэвшил энэ төхөөрөмж дээр хадгалагдах бөгөөд YouTube-ийн бүртгэлүүдээр хуваалдانا. YouTube-ийн түүхийллийн хяналт тэдгээрийг өөрчлөхгүй. Цувралын цэснээс ахиц дэвшилээ устгаж болно. + +Автомат бүртгэл нь сонголтоор хийгдэнэ. Нууц горимд эсвэл бүртгэл өөрчлөгдөхөд энэ нь зогсдог" + Ангиллуудыг ачаалж чадсангүй. Та тоглолтын жагсаалтыг хадгалах тэмдэглэгээ болгон хадгалж болно + Хадгалагдаагүй өөрчлөлт хэтэрхий их байна. Эхлээд дахин хадгалахыг оролдоно уу + Хадгалах үйлдэл хийгдэж байна. Дахин оролдоно уу + Ангиллыг ачаалж байна. Дахин оролдоно уу + Хадгалах сан боломжгүй байна. Таны хадгалсан өгөгдлийг хэвээр үлдээлээ + Өөрчлөлтийг хадгалах боломжгүй боллоо. YouTube-ийг хаахаас өмнө дахин хадгалахыг оролдоно уу + Энэ цуврал аль хэдийн хадгалагдсан эсвэл түүний тоглох жагсаалт өөрчлөгдсөн байна. Дахин нээнэ үү + Энэ цуврал өөрчлөгдсөн эсвэл устгагдсан байна. Номын санг дахин нээнэ үү + Тоглох жагсаалт өөрчлөгдсөн байна. Ангиа дахин сонгоно уу + Энэ анги боломжгүй байна. Өөр анги сонгоно уу + Цувралын нэрийг оруулна уу + YouTube-ийн тоглох жагсаалтын холбоос эсвэл ID-г оруулна уу + Дахин шинэчлэхээс өмнө түр хүлээнэ үү + Цагцгуудыг ачаалах боломжгүй байна. Дараа дахин оролдоно уу + Өөрчлөлтийг хийж чадсангүй. Дахин оролдоно уу + Үргэлжлүүлэхийг баталгаажуулж чадсангүй. Тоглуулагчийг шалгаад дахин оролдоно уу + Дагаж буй цувралууд + Энэхүү видео нь энэ тоглолтын жагсаалтад байхгүй байна + Т playlist-уудыг хайж байна… + Жагсаалтын холбоосыг буулгах + Тжсгээн олдохгүй байна + Боломжит анги байхгүй байна + Үзсэнийг нуух + Өмнөх ангиудыг үзсэн гэж тэмдэглэх + Эхлэх хэсэг: %1$s + Ангийн дараалал + Тоглолтын жагсаалтын дараалал + Жагсаалтын дарааллыг эргүүлэх + %1$s үзэлттэй + %1$s үзэлт + Үзсэн цувралуудыг тэмдэглэх + Дэнслэлт нь %1$s%%-иас багагүй үзсэн бөгөөд хамгийн ихдээ 90 секунд үлдсэн, эсвэл %2$s секунд үлдсэн + Аль нэг босго үзүүлэлтэд хүрэхэд цуврааг автоматаар үзсэн гэж тэмдэглэнэ. Хувийн дүрэм нь 90 секундаас илүү үлдэхгүй байх шаардлагатай бөгөөд ингэснээр урт цуврааны төгсгөлийг алгасахгүй. Үлдсэн хугацааны дүрэм нь нэг минутаас урт видеонуудад хамаарна; идэвхгүй болгохын тулд 0 гэж тохируулна. Өөрчлөлтүүд нь ирээдүйн явцад хамаарна. Гар аргаар үзсэн/үзээгүй гэж сонгосон тохиргоо хэвээр үлдэнэ + Үзсэн хувь (1-100) + Үлдсэн секунд (0-300; 0 бол идэвхгүй) + %1$s-оос %2$s-ийн хооронд утга оруулна уу + YouTube-ийн явцыг ашиглах + Өөр төхөөрөмж дээр үзсэн цувралынхаа явцыг үргэлжлүүлэн үзэх + Таны дагаж буй цувралуудын үзсэн төлөв болон үргэлжлүүлэх байрлалыг одоогийн YouTube бүртгэлээс уншина. Зөвхөн тохирох цувралуудыг энэ утсанд хадгална. Шалгалт нь цувралын сан нээлттэй үед болон \"Үргэлжлүүлэх\" товчийг дарахаас өмнө хийгдэнэ. Таны нөгөө төхөөрөмж дээр YouTube түүхийг идэвхжүүлсэн байх ёстой + YouTube-ийн явцыг шалгаж байна… + YouTube-ийн явц олдохгүй байна. Хадгалагдсан явцыг ашиглана уу + YouTube-ийн зарим явц олдохгүй байна. Хадгалагдсан байрлалуудыг хэвээр үлдээв + Нөөцлөх ба сэргээх + Цувралын сангаа файл болгон хадгалах эсвэл нөөцлөлтөө сэргээх + Дагаж буй цуврал, цувралын байрлал болон үзсэн сонголтуудыг хадгална. Бичлэг хийх болон YouTube-ийн явцын зөвшөөрлүүд багтаагүй болно + Нөөцлөлтийг хадгалах + Нөөцлөлтийг сэргээх + %1$s цуврал нэмэх. Таны санд байгаа %2$s цуврал хэвээр үлдэнэ. Видеоны одоогийн явц хэвээр үлдэнэ + Цувралын нөөцлөлт хадгалагдлаа + %1$s цувралыг сэргээв + Энэ файл нь дэмжигдээгүй цувралын нөөцлөлт эсвэл хэтэрхий том байна. Таны сан өөрчлөгдөөгүй + Нөөц хуулбарыг хадгалах боломжгүй боллоо. Өөр байршил сонгоно уу + Цувралыг устгалаа + Энэ анги одоо нэгээс олон удаа гарч байна. Альнээс нь үргэлжлүүлэхийг сонгоно уу + Таны хадгалсан анги уг тоглолтын жагсаалтад байхгүй байна. Таны хадгалсан байрлалыг хэвээр үлдээлээ + %1$s · %2$s ангиасаа үргэлжлүүлэх + Та доорх дурын ангийг сонгох боломжтой. Өмнөх ангиудад өөрчлөлт орохгүй + Энэхүү тоглолтын жагсаалт өөрчлөгдсөн байна. Хаанаас үргэлжлүүлэхээ сонгохын тулд цувралыг нээнэ үү + Энэ чиглэлд дахин цуврал байхгүй байна + %1$s-ийг үргэлжлүүлэх + Цуврал хайх + Хайлт цэвэрлэх + Цуврал олдсонгүй + Гарчиггүй анги + Боломжгүй анги + Niconico сэтгэгдэл + Шууд чат болон чатын давталтыг видео дээр гүйдэг сэтгэгдэл хэлбэрээр харуулах + Видео тоглуулагч дээр давхарлуулсан товчлуур харуулна. Сэтгэгдлийг харуулах эсвэл нуухын тулд товшилно, хурдан тохиргоо руу орохын тулд татаж барьна + Сэтгэгдлийн үсгийн хэмжээ + Сэтгэгдлийн текстийн хэмжээ (12-40 sp) + Сэтгэгдлийн тунгалаг байдал + Сэтгэгдлийн текст болон арын дэвсгэлийн тунгалаг байдал + Аялах хугацаа + Сэтгэгдэл дэлгэцийг огтлохыг хэдэн секунд болгохыг заана. Бага утга нь илүү хурдан хөдөлнө + Сэтгэгдлийн дээд урт + Үүнээс илүү тэмдэгттэй сэтгэгдлүүдийг нуух. Эможи бүрийг нэг тэмдэгт гэж тооцно + Хязгаарлахгүй diff --git a/patches/src/main/resources/addresources/values-mr-rIN/music/strings.xml b/patches/src/main/resources/addresources/values-mr-rIN/music/strings.xml index 3924a379ab..8fe2f6d763 100644 --- a/patches/src/main/resources/addresources/values-mr-rIN/music/strings.xml +++ b/patches/src/main/resources/addresources/values-mr-rIN/music/strings.xml @@ -17,18 +17,156 @@ Second \"item\" text" --> + शेवटी प्ले केलेले + सबस्क्रिप्शन्स + व्हिडिओ + इतर निकाल + अपलोड केलेली गाणी + अपलोड केलेले अल्बम्स + अपलोड केलेले कलाकार + पॉज आणि रिझ्युम वापरण्यासाठी होस्टचा Jam पॅच अपडेट करा + जॅम क्यू शेअरिंग + जवळच्या Jam ला होस्ट करा किंवा जॉइन करा आणि Jam लेयरद्वारे त्याचा क्यू एडिट करा + Jam क्यू शेअरिंग सक्षम करा + YouTube Music पुन्हा सुरू झाल्यावर Jam इंटिग्रेशन सक्षम करते + काम सुरू करण्यासाठी Jam Layer डाउनलोड करा + तुमच्या ब्राउझरमध्ये Jam Layer ची नवीनतम रिलीज उघडा + Jam कंट्रोल्स + जवळच्या Jam चा होस्ट बना किंवा त्यात सामील व्हा आणि सेशन व्यवस्थापित करा + Jam Layer पॅकेज + जवळच्या क्यू शेअरिंगसाठी वापरले जाणारे इंस्टॉल केलेले कॉम्पेनियन ॲप निवडा + इंस्टॉल केलेले कॉम्पेनियन पॅकेज वापरा. पेअरिंगसाठी तुमची मंजुरी आणि स्थानिक क्षमता टोकन आवश्यक आहे + Wi-Fi चालू करायचे आहे का? + Wi-Fi सुरू करा, त्यानंतर Jam पुन्हा सुरू करण्याचा किंवा सामील होण्याचा प्रयत्न करा + Wi-Fi सेटिंग्ज उघडा + रद्द करा + पॅकेज वापरा + वैध Android पॅकेज नाव प्रविष्ट करा + पॅकेज सेव्ह झाले. कनेक्ट करण्यासाठी Jam Layer सेटअप करा + Jam डिस्कनेक्ट झाले + प्रथम Jam Layer सक्षम करा + सेटिंग्जमध्ये Jam queue sharing सक्षम करा, त्यानंतर YouTube Music रीस्टार्ट करा + प्रथम निवडलेले Jam Layer पॅकेज इंस्टॉल करा + होस्टवर दिसलेला कोड प्रविष्ट करा. Wi-Fi Aware मुळे त्यांच्या LAN वेगळे असले तरीही जवळची उपकरणे कनेक्ट होऊ शकतात + कोड वापरून सामील व्हा + सामील व्हा + QR स्कॅन करा + आठ अक्षरी कोड प्रविष्ट करा किंवा आमंत्रण पेस्ट करा + तुमच्या Jam मध्ये सामील व्हा + कोड कॉपी करा + Jam कोड + QR दाखवा + पूर्ण झाले + तुमच्या Jam मध्ये आमंत्रित करा + जॉइन करण्यासाठी आणि हा क्यू एडिट करण्यासाठी स्कॅन करा. Jam संपवल्यास आमंत्रण रद्द होईल + लिंक कॉपी करा + Jam आमंत्रण + Jam पुन्हा कनेक्ट होत आहे; थोड्या वेळात पुन्हा प्रयत्न करा + Jam मध्ये जोडले + Jam उपलब्ध नाही; ट्रॅक जोडला गेला नाही + तुमचा Jam + एकत्र ऐकत आहोत + Jam मध्ये सामील होत आहे + एकत्र ऐका + जाम अपडेट होत आहे… + Wi-Fi वर तुमच्या होस्टचा शोध घेत आहे… + लोकांची प्रतीक्षा करत आहे + %s लोक कनेक्ट झाले आहेत + जवळच्या डिव्हाइसवर हा कोड प्रविष्ट करा. सपोर्ट असल्यास Wi-Fi Aware वेगवेगळ्या LAN वर कार्य करते. %s मिनिटांत समाप्त होईल + %s पर्यंत जायचे आहे का? + संपादन खुले आहे + संपादन लॉक केले आहे + Wi-Fi Aware द्वारे कनेक्ट झाले + Wi-Fi द्वारे कनेक्ट झाले + ब्लूटूथद्वारे कनेक्ट झाले + Jam दरम्यान समर्थित नाही + सहभाग रद्द केला + तुमच्या होस्टशी पुन्हा कनेक्ट होत आहे… + क्यू शेअर करा. प्रत्येकाला गाणी जोडू द्या + लोकांना आमंत्रित करा + पाहुणे रांगेत बदल करू शकतात + Jam समाप्त करा + होस्ट संगीत प्ले करतो. YouTube Music क्यूमध्ये थेट गाणी जोडा, हलवा किंवा काढून टाका + दोन्ही डिव्हाइसेस जवळ ठेवा. आम्ही होस्ट शोधत असताना तुम्ही रद्द करू शकता + सहभाग रद्द करा + Jam सोडा + Jam सुरू करा + जाम लेयर सेटअप करा + व्यवस्थापित करा + तपशील + उघडा + गाण्याचे पर्याय लोड होत आहेत… + गाण्याचे पर्याय उपलब्ध नाहीत; पुन्हा प्रयत्न करा + गाण्याचे पर्याय उघडता आले नाहीत + Jam Layer डाउनलोड करण्यासाठी कोणताही ब्राउझर उपलब्ध नाही + होस्ट क्यूची प्रतीक्षा करत आहे + प्रथम YouTube Music मध्ये गाणे उघडा + होस्टवर प्ले करा + Jam मधून बाहेर पडा आणि स्थानिक पातळीवर प्ले करा + Jam मध्ये पुढील प्ले करा + Jam रांगेत जोडा + हा जॅम ट्रॅक प्ले करा + कुठे प्ले करायचे ते निवडा + होस्टला येथे हलवा + गाणे सीक करण्यासाठी तयार झाले नाही + क्यू बदलला आहे; जेस्चर पुन्हा करून पहा हेडर लोगो डीफॉल्ट कस्टम + अल्बमचे ऑडिओ ट्रॅक्स प्ले करा + म्युझिक व्हिडिओऐवजी गाण्यांची फक्त अल्बम ऑडिओ आवृत्ती प्ले करा. यासाठी \'व्हिडिओ स्ट्रीम्स स्पूफ करा\' आवश्यक आहे + ऑडिओ/व्हिडिओ स्विच सक्षम करा + ऑडिओ ट्रॅक आवृत्ती आणि अधिकृत म्युझिक व्हिडिओ स्विच करण्याचा प्रयत्न करा तृतीय-पक्ष डाउनलोडिंग ॲप्स वापरून संगीत व्हिडिओ, पॉडकास्ट किंवा गाणी डाउनलोड करा + डाउनलोड मेनूसाठी बाह्य डाउनलोडर वापरा + मेनूचा डाउनलोड पर्याय त्याऐवजी तुमचा बाह्य डाउनलोडर उघडतो + डाउनलोड्स + "काहीही डाउनलोड केलेले नाही +डाउनलोड केलेले ट्रॅक्स येथे दिसतील" + %1$s • %2$s • %3$s MB + %1$s ट्रॅक्स • %2$s MB + डाउनलोड्समध्ये शोधा + क्रमवारी + कलाकाराप्रमाणे क्रमवारी लावा + शीर्षकानुसार क्रमवारी लावा + आकारानुसार क्रमवारी लावा + सर्व डाउनलोड्स हटवा + सर्व डाउनलोड्स हटवायचे आहेत का? + सर्व डाउनलोड केलेले ट्रॅक्स या डिव्हाइसमधून हटवले जातील + डाउनलोड्स हटवले + काहीही सापडले नाही + %s साठी क्रिया + प्ले + प्ले किंवा पॉज + मागील + पुढील + पॉज + बंद करा + हटवा + डाउनलोड हटवा + डाउनलोड हटवायचे आहे का? + %s या डिव्हाइसमधून हटवले जाईल + डाउनलोड हटवले + डाउनलोड हटवता आले नाही + डाउनलोड प्ले करता आले नाही + ऑडिओ डाउनलोड्स + ऑफलाइन प्लेबॅक + डाउनलोड करण्यासाठी ट्रॅक ओळखता आला नाही + डाउनलोड आधीच सुरू आहे + आधीच डाउनलोड केले आहे + डाउनलोड सुरू झाले + डाउनलोड सुरू होत आहे… + डाउनलोड होत आहे %s%% + डाउनलोड पूर्ण झाले + डाउनलोड अयशस्वी झाले अबाऊट @@ -38,12 +176,17 @@ Second \"item\" text" \u092a\u094d\u0932\u0948\u092f\u0930 गीत इतर + YouTube Music भाषा + सिस्टम भाषा + Android 13 किंवा त्यानंतरची आवृत्ती आवश्यक \'Get Music Premium\' लेबल लपवा संगीत प्रीमियम जाहिराती लपवा व्हिडिओ जाहिराती लपवा ऑडिओ / व्हिडिओ टॉगल लपवा + डाउनलोड बटण लपवा + पॉडकास्ट शोमधील डाउनलोड बटण लपवते एक्स्प्लोर शेल्फ लपवा फिल्टर बार लपवा ग्रिड शेल्फ लपवा @@ -101,7 +244,26 @@ Second \"item\" text" या सत्रासाठी क्रॉसफेड थांबवले क्रॉसफेड पुन्हा सुरू झाले क्रॉसफेड सक्षम असताना व्हिडिओ मोड उपलब्ध नाही + SABR प्लेबॅकसह क्रॉसफेड बंद आहे + कास्टिंग दरम्यान क्रॉसफेड बंद आहे हे कसे कार्य करते + "क्रॉसफेड तुमच्या कालावधी आणि कर्व्हचा वापर करून एका गाण्याचा शेवट दुसऱ्या गाण्याच्या सुरुवातीशी जोडतो + +हे ऑटो-ॲडव्हान्स आणि मॅन्युअल स्किपवर चालते, प्रत्येकासाठी स्वतंत्र टॉगल आहे. ऑटो-ॲडव्हान्सवर, गाणे संपण्यापूर्वी काही सेकंद आधी ब्लेंड सुरू होतो" + मर्यादा + "क्रॉसफेड चालू असताना म्युझिक व्हिडिओ फक्त ऑडिओ म्हणून प्ले होतात. क्रॉसफेड थांबवण्यासाठी आणि व्हिडिओ पाहण्यासाठी शफलवर लाँग-प्रेस करा + +प्लेबॅक सुरू झालेला म्युझिक व्हिडिओ पुढच्या गाण्यापर्यंत व्हिडिओ मोडमध्येच राहतो + +फेड ड्युरेशनपेक्षा कमी कालावधीची गाणी क्रॉसफेड होत नाहीत + +ट्रान्झिशनच्या दरम्यान लॉक स्क्रीन किंवा नोटिफिकेशनमध्ये थोडा वेळ जुनी स्थिती दिसू शकते + +म्युझिक व्हिडिओ पाहण्यासाठी क्रॉसफेड पॉज केल्याने काही व्हिडिओ ब्लॅक होऊ शकतात. गाणे निवडण्यापूर्वी क्रॉसफेड पॉज केल्यास हे टाळता येईल" + सपोर्टेड नाही + "ऑडिओ कास्टिंग किंवा मिररिंग (Chromecast, स्क्रीन किंवा ऑडिओ मिररिंग) + +SABR प्लेबॅक (Spoof व्हिडिओ स्ट्रीम्स बंद असताना)" श्रेय Crossfade वैशिष्ट्य VazerOG द्वारे @@ -109,16 +271,28 @@ Second \"item\" text" शफल स्थिती लक्षात ठेवा + प्लेबॅक वेग + सर्व ट्रॅकसाठी वापरला जाणारा वेग. १००% वर ॲपचा वेग वापरला जातो, जसे की पॉडकास्ट प्लेबॅक वेग + गतीसह पिच बदला अनापसंद पुनर्निर्देशन अक्षम करा अनापसंद बटण दाबल्यावर पुढील गाण्यावर जाण्यापासून प्रतिबंधित करते + प्लेलिस्ट ट्रॅक टॅप ॲक्शन + प्लेलिस्ट प्ले करा + फक्त निवडलेले ट्रॅक प्ले करा + निवडलेले ट्रॅक रांगेत जोडा + रांगेत जोडले \'कास्ट\' बटण लपवा \'इतिहास\' बटण लपवा \'सूचना\' बटण लपवा \'शोध\' बटण लपवा + व्हॉइस सर्च बटण लपवा + ध्वनी शोध बटण लपवा + \'नवीन\' बटण लपवा + लायब्ररी टॅबमधील \'नवीन\' बटण लपवते ॲक्शन बटणे प्लेअरच्या खाली कोणती ॲक्शन बटणे दिसू द्यायची ते निवडा @@ -126,6 +300,7 @@ Second \"item\" text" लाईक आणि डिसलाईक बटण लपवा टिप्पण्या बटण लपवा + \'लाईव्ह चॅट रिप्ले\' बटण लपवा गीत (Lyrics) बटण लपवा @@ -135,6 +310,7 @@ Second \"item\" text" डाउनलोड बटण लपवा + \'रेडिओ सुरू करा\' बटण लपवा फ्लयआउट मेनू प्लेअर आणि रांगेतील फ्लयआउट मेनूमध्ये कोणते घटक दिसू द्यायचे ते निवडा @@ -185,6 +361,8 @@ Second \"item\" text" शोध सबस्क्रिप्शन + पोर्ट्रेट ओरिएंटेशन सक्तीने लागू करा + डिवाइस फिरवल्यावरही ॲप पोर्ट्रेट ओरिएंटेशनमध्येच ठेवते मिनीप्लेअर रंग बदला मिनीप्लेअरचा रंग फुलस्क्रीन प्लेअरशी जुळवतो @@ -219,12 +397,19 @@ Second \"item\" text" लॉग आउट करा लॉग आउट केले + Scrobbling सक्षम करा ‘आता ऐकत आहे’ स्टेटस सबमिट करा + गाण्याची किमान कालावधी + यापेक्षा कमी कालावधीची गाणी scrobble केली जाणार नाहीत + Scrobble विलंब टक्केवारी + गाण्याच्या या टक्केवारीपर्यंत ऐकल्यानंतर scrobble ट्रिगर करा + Scrobble विलंब सेकंद + इतके सेकंद ऐकल्यानंतर scrobble सुरू करा लिसेनब्रेनझ खाते लॉग इन केले. खाते व्यवस्थापित करण्यासाठी टॅप करा @@ -260,6 +445,9 @@ Second \"item\" text" Custom regex स्वच्छता एक सानुकूल रेग्युलर एक्सप्रेशन एंटर करा जेणेकरून गाण्याचे शीर्षक, कलाकार किंवा अल्बममधून अतिरिक्त मजकूर काढता येईल. उदाहरण: (?i)\\s*\\((clean|explicit)\\) शीर्षकावरून कलाकार आणि ट्रॅक पार्स करा + व्हिडिओ शीर्षकामधून \" - \" विभाजक वापरून कलाकार आणि ट्रॅकचे नाव वेगळे करा. रि-अपलोड केलेल्या कंटेंटसाठी उपयुक्त आहे जिथे चॅनेलचे नाव कलाकार म्हणून वापरले जाऊ शकते + गहाळ अल्बम भरा + जेव्हा संगीतात अल्बम समाविष्ट नसतो, तेव्हा तो Last.fm वर शोधा जेणेकरून तुमचे scrobble पूर्ण होईल "स्क्रोब्लिंग तुमच्या YouTube Music वापराला तुमच्या Last.fm किंवा ListenBrainz प्रोफाइलसोबत आपोआप सिंक करते. @@ -267,16 +455,235 @@ Second \"item\" text" तृतीय-पक्ष गीतांचा वापर करा तृतीय-पक्ष सेवेतून गीत दर्शवते, जेव्हा कोणतेही गीत उपलब्ध नसेल तेव्हा अंगभूत गीतांवर परत येते + गिते पाहताना स्क्रीन सुरू ठेवा + गितेंचे पॅनेल उघडे असताना डिस्प्ले बंद होण्यापासून रोखते + लिरिक्स सेवा + डिस्प्ले + सिंक्रोनाइझेशन + पॅनेल + AI एंडपॉइंट + सर्व प्रोव्हायडर्स अक्षम आहेत - बिल्ट-इन लिरिक्स आणि एम्बेडेड लिरिक्स + प्रोव्हायडर क्रम रीसेट करायचा आहे का? + प्रोव्हायडर्स डीफॉल्ट प्रायोरिटी क्रमात पुनर्संचयित करा + स्टोअर केलेले लिरिक्स हटवा + या डिव्हाइसवर ठेवलेले लिरिक्स डिलीट करा + %1$s MB क्लिअर केले + टोकन मिळवा + Apple Music Media-user-token + "music.apple.com वरून media-user-token कुकी पेस्ट करा + +पायऱ्या: +1. music.apple.com वर सदस्य वापरकर्ता म्हणून साइन इन करा +2. DevTools → Application → Cookies → music.apple.com उघडा +3. \"media-user-token\" ची व्हॅल्यू कॉपी करा" + Apple Music: media-user-token सेव्ह झाले + Apple Music: media-user-token हटवले + Apple Music: अवैध media-user-token - कृपया तपासा आणि पुन्हा प्रयत्न करा + Spotify SpDc + "Spotify वरून sp_dc कुकी पेस्ट करा. + +पायऱ्या: +१. open.spotify.com वर साइन इन करा +२. DevTools → Application → Cookies → open.spotify.com उघडा +३. \"sp_dc\" ची व्हॅल्यू कॉपी करा" + Spotify: sp_dc सेव्ह झाले + Spotify: sp_dc हटवले + Spotify: अवैध sp_dc - कृपया तपासा आणि पुन्हा प्रयत्न करा + Apple Music टोकन ऐच्छिक आहे. त्याशिवाय, Lyrically चा वापर फालबॅक म्हणून केला जाईल + Deezer ARL + "Deezer कडील arl कुकी पेस्ट करा. + +पायऱ्या: +१. deezer.com वर साइन इन करा +२. DevTools → Application → Cookies → deezer.com उघडा +३. \"arl\" ची व्हॅल्यू कॉपी करा" + Deezer: arl जतन केले + Deezer: arl हटवले + Deezer: अवैध arl - कृपया तपासा आणि पुन्हा प्रयत्न करा + Musixmatch टोकन + तुमचे स्वतःचे खाते वापरण्यासाठीच टोकन जोडा. ते Musixmatch Android ॲपमधून आलेले असावे: musixmatch.com वरून कॉपी केलेले टोकन जुन्या डेस्कटॉप क्लायंटचे असतात आणि API त्यांना नाकारते + Musixmatch टोकनशिवाय देखील कार्य करते, त्यामुळे हे क्षेत्र रिकामे राहू शकते + वापरकर्ता टोकन + Musixmatch: वापरकर्ता टोकन जतन केले + Musixmatch: वापरकर्ता टोकन हटवले + Musixmatch: अवैध युजर टोकन - कृपया तपासा आणि पुन्हा प्रयत्न करा + YouTube कुकीज + "YouTube वरून कॅप्शन्स आणि टाइमटेक्स्ट क्वेरी करण्यासाठी तुमचे YouTube कुकीज पेस्ट करा. + +पायऱ्या: +१. ब्राउझरमध्ये youtube.com उघडा आणि साइन इन करा +२. DevTools (F12) → Application → Cookies → youtube.com उघडा +३. पूर्ण कुकी स्ट्रिंग कॉपी करा (सेमीकोलनने वेगळे केलेले key=value जोड्या) + +आवश्यक कुकीज: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPISID, LOGIN_INFO" + YouTube: कुकीज जतन केल्या + YouTube: कुकीज हटवल्या + YouTube: अवैध कुकीज - कृपया तपासा आणि पुन्हा प्रयत्न करा + एका ओळीवर टॅप करून धरून ठेवा + गाण्यातील त्या क्षणी जाण्यासाठी ओळीवर टॅप करा, तुमच्या बोटाखालील शब्द कॉपी करण्यासाठी तो दीर्घकाळ दाबा, किंवा जेव्हा दाबलेला भाग शब्दावर नसेल तेव्हा संपूर्ण ओळ कॉपी करा. \'Off\' केल्यास दोन्ही जेस्चर्स बंद होतील + सेक्शन टॅग दाखवा + गीतिकांमधील प्रत्येक नवीन सेक्शनला Verse किंवा Chorus सारख्या लहान टॅगने लेबल करा कॉपी बटण दाखवा + मूळ लिरिक्स प्लेन टेक्स्ट म्हणून कॉपी करण्यासाठी टॅप करा; रॉ लिरिक्स सोर्स फाईल डाउनलोड्समध्ये सेव्ह करण्यासाठी दीर्घकाळ दाबा भाषांतर बटण दाखवा + अनुवाद टॉगल करण्यासाठी टॅप करा; केवळ अनुवादासाठी दीर्घकाळ दाबा. हे प्रोव्हायडर ट्रान्सलेशन, Google Translate किंवा OpenAI-सुसंगत API वापरते + रोमनाइझ बटण दाखवा + रोमनायझेशन टॉगल करण्यासाठी टॅप करा; फक्त रोमनायझेशनसाठी लाँग-प्रेस करा. प्रोव्हायडर रोमनायझेशन, Google Translate किंवा OpenAI-सुसंगत API वापरते + स्थानिक रोमनकरण आणि अलाइनमेंट + जेव्हा प्रदाता, तुमचे मॉडेल आणि Google Translate अयशस्वी होतात तेव्हा डिव्हाइसवर रोमनकरण करा. वाचन हे वेळेनुसार दिलेल्या प्रत्येक शब्दांनतर येते; शब्द सिंक्रोनाइझेशन चालू असणे आवश्यक आहे. चिनी, कॅन्टोनीज, जपानी, कोरियन, ग्रीक, रशियन, जॉर्जियन, आर्मेनियन; सुमारे १२० MB + वाचत आहे %1$s: %2$d%% + वाचन तयार आहे (%1$d नोंदी) + डाउनलोड अयशस्वी - पुन्हा प्रयत्न करण्यासाठी हे बंद करून पुन्हा चालू करा + रिफ्रेश बटण दाखवा + पुढील लिरिक्स उमेदवाराकडे जाण्यासाठी टॅप करा; गाण्याचे शीर्षक आणि कलाकाराद्वारे लिरिक्स शोधण्यासाठी दीर्घकाळ दाबा + गीत शोधा + गाण्याचे शीर्षक + कलाकार + गाण्याचे शीर्षक आणि कलाकार प्रविष्ट करा + सक्षम केलेल्यापैकी कोणताही प्रदाता इतर गाणी शोधू शकत नाही + जतन केले + सिंक केलेले शब्द किंवा शब्दांश हायलाइट करा + शब्द-दर-शब्द लिरिक्स उपलब्ध असल्यास त्यांना प्राधान्य द्या. लिरिक्समध्ये शब्दांच्या वेळेची माहिती असल्यास, संगीताच्या तालात प्रत्येक शब्द किंवा अक्षराला हायलाइट करा + वाजवलेले लिरिक्स लपवा + सध्या वाजत असलेल्या ओळीच्या वरच्या लिरिक्स ओळी लपवा + न वाजवलेले लिरिक्स लपवा + सध्या वाजत असलेल्या ओळीच्या खालील लिरिक्स ओळी लपवा + लॉक स्क्रीनवर MediaSession लिरिक्स दाखवा + लॉक स्क्रीन, Android Auto आणि ब्लूटूथवर सध्याची ओळ दर्शवते. यासाठी Android ११ किंवा नवीन आवृत्ती आवश्यक आहे + मिनीप्लेअर लिरिक्स दाखवा + मिनीप्लेयरमध्ये सध्याची ओळ दर्शवते, ज्यामध्ये ट्रॅक उपशीर्षकावर हलवला जातो + गाण्याचे शीर्षक/कलाकार स्वरूप + Miniplayer आणि MediaSession मध्ये लिरिक्ससह दाखवल्या जाणाऱ्या ट्रॅकचे स्वरूप. {t} हे शीर्षक आहे, {a} हा कलाकार आहे आणि {v} ही आवृत्ती आहे, जसे की Acoustic, जेव्हा लिरिक्स स्रोत एखादी आवृत्ती प्रदान करतो. दोन कोलन्समधील मजकूर तेव्हाच दाखवला जातो जेव्हा त्यातील प्रत्येक व्हेरिएबलला मूल्य असते; साध्या कोलनसाठी \\: लिहा. प्रत्येक ओळ वेळोवेळी दाखवली जाते, जेव्हा लिरिक्स बदलतात तेव्हा पुढील ओळीवर जा + लिरिक्स ओळ स्वरूप + मिनीप्लेयर आणि MediaSession मधील लिरिक्स लाइनचे स्वरूप. {lrc} ही ओळ आहे आणि {n} हे ती ओळ किती वेळा सलग गायली जाते हे आहे. कोलनमधील मजकूर केवळ ओळ पुन्हा पुन्हा येत असल्यास दाखवला जातो. कोलनसाठी \\: वापरा + स्वरूपामध्ये {lrc} असणे आवश्यक आहे + कलाकार-ओळ टेम्पलेट आणि अ‍ॅनिमेशन टायमिंग + किमान स्क्रीन वेळ (ms) + कमाल स्क्रीन वेळ (ms) + केवळ मल्टिलाइन अ‍ॅनिमेशनसाठी. प्रत्येक फ्रेम किमान वेळेसाठी ठेवा. कमाल वेळेला, लिरिक्स सारखे असतानाही कलाकार ओळ पुढे सरकवा. + किमान 0 किंवा त्यापेक्षा जास्त असावे. कमाल 0 पेक्षा जास्त आणि किमान मूल्याइतके किंवा त्यापेक्षा जास्त असावे. + ओळीच्या लांबीची मर्यादा + गीत आणि कलाकार ओळ मर्यादा, आणि पूर्ण-रुंदीच्या कॅरेक्टरचे वजन + "गीतांच्या ओळींची मर्यादा +लांब गीतांना संतुलित अर्ध्या भागात विभाजित करा. 0 विभाजन बंद करते." + "कलाकार ओळ मर्यादा +भरलेल्या ट्रॅक फॉरमॅटला सायकलिंग फ्रेम्समध्ये विभाजित करा. 0 विभाजन बंद करते." + "पूर्ण-रुंदीच्या अक्षरांचे वजन +दोन्ही मर्यादांना लागू होते. १.७५ वर, चार पूर्ण-रुंदीची अक्षरे ७ म्हणून मोजली जातात." + ० ते १०००० पर्यंतच्या पूर्ण-संख्या मर्यादा आणि ० पेक्षा जास्त अक्षरांचे वजन प्रविष्ट करा + इन्स्ट्रुमेंटल ब्रेक + इन्स्ट्रुमेंटल ब्रेक दरम्यान मिनीप्लेयर आणि MediaSession मध्ये दाखवले जाते. प्रत्येक ओळ एक ॲनिमेशन फ्रेम आहे. {t} हे शीर्षक आहे, {a} हा कलाकार आहे आणि {lrc} ही शेवटची लिरिक्स ओळ आहे + मिनीप्लेयर आणि MediaSession टायमिंग ऑफसेट + miniplayer आणि MediaSession मध्ये पुढची ओळ कधी दाखवली जावे याचे वेळेतील बदल. जर ओळी उशिरा दिसत असतील, तर ऋण मूल्य वापरा, उदाहरणार्थ लॉक स्क्रीनवर + फिल्टर्स + मेटाडेटा regex + गीते शोधण्यापूर्वी आणि दोन शीर्षके जुळत आहेत की नाही हे तपासताना शीर्षके, कलाकार आणि अल्बम्सना लागू केलेले रेग्युलर एक्सप्रेशन. दोन्ही बंद करण्यासाठी रिकामे ठेवा. डीफॉल्टनुसार \"Official Video\", \"Remaster\", \"【插曲】\", आणि \"- Topic\" सारखा सामान्य गोंधळ काढून टाकला जातो + लिरिक्स फिल्टर regex + केवळ मूळ लिरिक्स मजकुराला लागू केलेले रेग्युलर एक्सप्रेशन (अनुवाद किंवा रोमनलायझेशनला नाही). अक्षम करण्यासाठी रिकामे ठेवा + माहिती ओळ मार्कर्स + जर लिरिक्स चुकीच्या पद्धतीने खाली फिल्टर झाले असतील, तर हे समायोजित करा किंवा साफ करा. स्वल्पविराम-विभक्त मार्कर्स: मार्कर आणि त्यानंतर कोलन असलेल्या ओळी खाली क्रेडिट माहिती म्हणून दाखवल्या जातात. अक्षम करण्यासाठी रिकामे ठेवा + पहिले अक्षर कॅपिटलाइज करा + गीतांच्या प्रत्येक ओळीचे पहिले अक्षर कॅपिटलाइज करते + शेवटची विरामचिन्हे काढून टाका + गीतांच्या ओळींच्या शेवटी असलेले पूर्णविराम, स्वल्पविराम, कोलन आणि सेमीकोलन काढून टाकते. प्रश्नचिन्ह, उद्गारवाचक चिन्ह, दीर्घविराम, अवतरण चिन्हे आणि कंस तसेच ठेवले जातात टेक्स्ट आकार लिरिक्स टेक्स्टचा आकार + ग्लोबल टाइमिंग ऑफसेट + सिंक केलेले लिरिक्स प्लेबॅकचे अनुसरण करतात. जर ते विचलित झाले, तर टाइमिंग ऑफसेट समायोजित करा. सिंक केलेले लिरिक्स पुढे किंवा मागे सरकवा. जर लिरिक्स गाण्यापेक्षा पुढे असतील तर सकारात्मक मूल्य वापरा + लिरिक्स ऑफसेटसाठी SponsorBlock सेगमेंट्स वापरा + SponsorBlock वगळत असलेल्या सेगमेंट्सशिवाय मॅच करा आणि लिरिक्स रिलीज झालेल्या ऑडिओवर शिफ्ट करा. \'ऑफ\' असल्यास SponsorBlock पूर्णपणे दुर्लक्षित केले जाते + सबमिट करण्यासाठी या ट्रॅकचा कोणताही व्हिडिओ आयडी नाही स्रोत: %s + हा LRC सेव्ह करण्यासाठी कॉपी बटण दीर्घकाळ दाबा; जर तुम्ही वेळेचे अचूक नियोजन करून ते कम्युनिटी प्रोव्हायडर्ससोबत शेअर करू शकलात, तर आम्ही तुमचे ऋणी राहू + Google द्वारे अनुवादित + Google द्वारे रोमनलाईज्ड + %s द्वारे अनुवादित + %s द्वारे रोमनलिपिमध्ये रूपांतरित + %s द्वारे रोमनलाइज्ड + अनुवादित + अनुवादाची भाषा + रोमनायझेशन भाषा भाषांतर करत आहे… लिरिक्सचे भाषांतर करू शकलो नाही + रोमनाइझ्ड + स्वयंचलित + रोमनाइझिंग… + गीतांचे रोमनाइझेशन होऊ शकले नाही + केवळ अनुवादित + केवळ रोमनलिपि + ड्युअल मोड क्लिपबोर्डवर कॉपी केले + रिफ्रेश होत आहे + रिफ्रेश झाले + QuickLRC API की + QuickLRC सध्याच्या ट्रॅकसाठी लिरिक्स ट्रान्सक्राइब करते किंवा टाइम-अलाइन करते. ही एक स्वतंत्र सेवा आहे, ज्याचा वर दिलेल्या OpenAI-सुसंगत एंडपॉइंटशी संबंध नाही + QuickLRC की सेव्ह केली. लिरिक्स सर्च डायलॉगमधील अलाइन बटण वापरा, किंवा जेव्हा ट्रॅकला लिरिक्स नसतील तेव्हा पॅनेल प्रॉम्प्ट वापरा + QuickLRC API की जतन केली + QuickLRC ने ती की स्वीकारली नाही + QuickLRC API की हटवली + QuickLRC अशा गाण्यांच्या लिरिक्सची वेळ ठरवते ज्यांना लिरिक्स नाहीत, किंवा क्रेडिट्सच्या बदल्यात ट्रॅकवरून त्यांचे ट्रान्सक्रिप्शन करते. की सेट केल्यावर, ज्या ट्रॅक्सना लिरिक्स नाहीत त्यांना ट्रान्सक्रिप्शन उपलब्ध होते आणि सर्च डायलॉगमध्ये अलाइनमेंट बटण येते + QuickLRC अलाइनमेंटची विनंती करा + अलाइन होत आहे + अलाइनमेंट लागू करा + QuickLRC ट्रान्स्क्रिप्शनची विनंती करा + ट्रान्स्क्राइब होत आहे + ट्रान्स्क्रिप्शन रद्द केले + केवळ YouTube वरून प्ले केलेल्या ट्रॅक्सचे ट्रान्स्क्रिप्शन केले जाऊ शकते + अलाइन करण्यासाठी लिरिक्स उपलब्ध नाहीत - त्याऐवजी ट्रान्सक्रिप्शन केले जात आहे + QuickLRC या ट्रॅकवर प्रक्रिया करू शकले नाही + या ट्रॅकसाठी QuickLRC क्रेडिट्स संपले आहेत + "गीते शीर्षक आणि कलाकाराद्वारे मॅच केली जातात, असामान्य मेटाडेटा मुळे काहीही न मिळणे किंवा चुकीची आवृत्ती मिळणे शक्य आहे. आवृत्ती बदलण्यासाठी रिफ्रेश वापरा. * चिनित प्रोव्हायडर्सना टोकनची आवश्यकता असते. सर्व सक्षम प्रोव्हायडर्सना नेहमी तुमच्या क्रमाने विनंती केली जात नाही, तर ते त्यांच्या वेगावर आधारित प्रतिसाद देतात.\n\nगीते तृतीय-पक्ष सेवांकडून येतात ज्या Morphe शी संलग्न, प्रायोजित किंवा समर्थित नाहीत. प्रत्येक सेवा जशी आहे तशीच वापरली जाते: गीते, त्यांची उपलब्धता आणि त्यांचे लायसेंसिंग त्या सेवांकडे आणि त्यांच्या लायसेंसर्सकडे राहते आणि त्यांचा वापर त्यांच्या सेवाशर्तींच्या अधीन असू शकतो. सक्षम सेवेपर्यंत केवळ तुम्ही विचारलेली शोध माहिती पोहोचते - शीर्षक, कलाकार आणि ज्या सेवेला गरज असेल तिथे व्हिडिओ आयडी. पर्यायी AI एंडपॉइंट आणि QuickLRC ट्रान्स्क्रिप्शन केवळ तुम्ही कॉन्फिगर केल्यानंतरच चालतात आणि त्यांना गीतांचा मजकूर किंवा ट्रॅकची व्हिडिओ URL मिळते." + तुम्ही वापरत असलेल्या प्रोव्हायडर्सनाच सक्षम करा, किंवा डीफॉल्ट ठेवा - सततच्या विनंत्यांमुळे लहान कम्युनिटी लिरिक्स साइट्सवर ताण येऊ शकतो + क्रेडिट लाईन लपवा + गीतांच्या खाली दिसणारी क्रेडिट लाईन लपवा + अनुवाद आणि रोमनाइझेशनची अदलाबदल करा + ON केल्यास रोमनाइझेशन गीतांच्या खाली आणि अनुवाद वर येईल; OFF केल्यास रोमनाइझेशन वर आणि अनुवाद खाली राहील + भाषांतर + रोमनायझेशन + भाषांतर प्रदाता + Google (बाय डीफॉल्ट, मोफत) + OpenAI सुसंगत (कस्टम) + रोमनायझेशन प्रदाता + Google (डीफॉल्ट) + स्थानिक / ऑफलाइन डिक्शनरी + OpenAI सुसंगत (कस्टम) + AI सेक्शन + केवळ तेव्हाच वापरले जाते जेव्हा प्रोव्हायडर कोरस किंवा व्हर्स सारखी सेक्शन लेबल्स पाठवत नाही, जी तुमचे OpenAI-सुसंगत मॉडेल नंतर शोधते + AI & Cloud Endpoints + AI प्रदात्यांसाठी API की, मॉडेल्स, एंडपॉइंट्स आणि प्रॉम्प्ट टेम्पलेट्स कॉन्फिगर करा. + API एंडपॉइंट URL (उदा. https://api.openai.com/v1/chat/completions) + प्रॉम्प्ट टेम्पलेट + प्लेसहोल्डर्स: {language}, {task}, {title}, {artist}, {count}, {lines} + प्रॉम्प्ट सेव्ह केला + प्रॉम्ट सेव्ह झाला आहे, परंतु AI एंडपॉइंट अजून वापरण्यायोग्य नाही. प्रथम AI सेटिंग्जमध्ये त्याचे कॉन्फिगरेशन करा + प्रॉम्ट रीसेट केला + वैध JSON नाही. सेव्ह करण्यापूर्वी ते दुरुस्त करा किंवा फील्ड रिकामे करा + भाषांतर आणि रोमनायझेशन सेटिंग्ज जतन केल्या + सेटिंग्ज डीफॉल्टवर रिसेट केल्या + बेस URL ची सुरुवात http:// किंवा https:// ने झाली पाहिजे + कनेक्शन तपासा + एंडपॉइंट्स कॉन्फिगर करा + सर्व एंडपॉइंट्स दाखवा + कनेक्शनची पडताळणी करत आहे… + भाषांतर प्रदाता + रोमनाइझेशन प्रदाता + मॉडेल्स मिळवा + मॉडेल्स मिळवत आहे… + मॉडेल निवडा + मॉडेल्स मिळवण्यात अयशस्वी. API की किंवा कनेक्शन तपासा. + Gemini API सेटिंग्ज + OpenAI सुसंगत (कस्टम एंडपॉइंट) + DeepL API सेटिंग्ज + API की + मॉडेल + बेस URL + API की (पर्यायी) + अनुवाद प्रॉम्प्ट + अतिरिक्त विनंती बॉडी (पर्यायी JSON) + प्रमाणीकरण की (Free किंवा Pro) + API की, मॉडेल्स, एंडपॉइंट्स आणि प्रॉम्प्ट्स कॉन्फिगर करा 8.23.51 - ठळक चिन्ह अक्षम करा 9.13.51 - प्लेयर अप नेक्स्ट, गीत आणि संबंधित टॅब पुनर्संचयित करा diff --git a/patches/src/main/resources/addresources/values-mr-rIN/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-mr-rIN/shared-youtube/strings.xml index 598aa1e34a..5619555acf 100644 --- a/patches/src/main/resources/addresources/values-mr-rIN/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-mr-rIN/shared-youtube/strings.xml @@ -18,6 +18,11 @@ Second \"item\" text" --> + स्टार्टअप ॲनिमेशन + बंद + रंग + मोनोक्रोम + सेटअप पूर्ण करण्यासाठी रीस्टार्ट करा @@ -40,12 +45,19 @@ Second \"item\" text" मटेरियल यू (प्राथमिक) मटेरियल यू (दुय्यम) मटेरियल यू (तृतीयक) + सानुकूल + डाउनलोड्स + डाउनलोड मोड + डिफॉल्ट + इन-ॲप डाउनलोड करा + बाह्य डाउनलोडर डाउनलोडर ॲपचे पॅकेज नाव आपल्या इन्स्टॉल केलेल्या बाह्य डाउनलोडर ॲपचे पॅकेज नाव \u092a\u093f\u0915\u093f\u091c\u093e \u0928\u093e\u0935 \u092a\u094d\u0926\u094d\u0928\u093f इतर ॲप इन्स्टॉल केलेले नाही + %s इंस्टॉल केलेले नाही. कृपया ते इंस्टॉल करा "पॅकेज नावाचे ॲप इन्स्टॉल केलेले आढळले नाही: %s पॅकेज नाव बरोबर आहे आणि ॲप इन्स्टॉल केलेले आहे का ते तपासा." @@ -54,6 +66,7 @@ Second \"item\" text" सानुकूल ॲप चिन्ह मूळ + Morphe काळा मॉर्फे गडद मॉर्फे फिकट @@ -62,11 +75,21 @@ Second \"item\" text" Morphe खेळ काळा सानुकूल + "यामुळे तुमच्या ॲप लाँचरमध्ये दिसणारे आयकॉन बदलते. + +ही सेटिंग खालीलपैकी कोणत्याही ठिकाणी आयकॉन बदलणार नाही: +• डिव्हाइस सिस्टम सेटिंग्ज ॲप लिस्ट +• काही पुश नोटिफिकेशन्स +• 'Open with' ॲप पिकर +• Android Auto आयकॉन + +सर्वत्र आयकॉन बदलण्यासाठी, पुन्हा पॅच करा आणि 'App icon' पॅच पर्याय बदला" सूचना चिन्ह ॲप आयकॉन जुळवा Morphe प्ले मॉर्फे सूचना + तुमचा वॉच हिस्ट्री सेव्ह केला जात नाही.<br><br>हे बहुधा DNS ॲड ब्लॉकर किंवा नेटवर्क प्रॉक्सीमुळे झाले आहे.<br><br>हे ठीक करण्यासाठी, <b>s.youtube.com</b> व्हाईटलिस्ट करा किंवा सर्व DNS ब्लॉकर आणि प्रॉक्सी बंद करा पुन्हा दाखवू नका व्हिडिओ स्ट्रीम्सचा देखावा करा @@ -76,6 +99,12 @@ Second \"item\" text" जर तुम्ही YouTube Premium वापरकर्ता असाल, तर ही सेटिंग आवश्यक नसेल." कोणत्याही क्लायंट स्ट्रीम्स मिळवता आल्या नाहीत + डिफॉल्ट क्लायंट काम करत नाही + "%1$s सलग अनेक वेळा काम केले नाही, परंतु %2$s ने केले. + +%2$s ला डिफॉल्ट क्लायंट बनवायचे का?" + बदला + ही सेटिंग बंद केल्यामुळे प्लेबॅक समस्या उद्भवू शकतात \'Android VR साइन इन\' सक्षम करण्याचा प्रयत्न करा > @@ -110,12 +139,26 @@ Second \"item\" text" • तुम्ही तुमचे Google पासवर्ड बदलल्यास किंवा तुमच्या Google सेटिंग्जमधून हे डिव्हाइस अनरजिस्टर केल्यास तुम्हाला पुन्हा साइन इन करावे लागेल" OAuth2 तात्पुरते उपलब्ध नाही (API टाइम आउट) Android VR साइन इन एक्सपायर झाले, VR मध्ये पुन्हा साइन इन करण्याचा प्रयत्न करा + OAuth2 तात्पुरते उपलब्ध नाही (स्थिती %s) OAuth2 तात्पुरते उपलब्ध नाही VR मध्ये साइन इन करू शकत नाही, वेबसाइट ऑथेंटिकेशन मंजूर नाही VR मध्ये साइन इन करू शकत नाही: %s Android VR मधून लॉग आउट झाले Invalid VR साइन इन टोकन, VR मध्ये पुन्हा साइन इन करण्याचा प्रयत्न करा + नोटिफिकेशन मीडिया कंट्रोल्स + तुमच्या लॉक स्क्रीन आणि नोटिफिकेशन पॅनेलवर दिसणारे प्लेबॅक कंट्रोल्स कस्टमाइज करा + मागील & पुढील बटणे लपवा + मीडिया नोटिफिकेशन, लॉक स्क्रीन आणि हेडफोन कंट्रोल्समधील मागील आणि पुढील बटणे लपवते + सीकबार अक्षम करा + लॉक स्क्रीन आणि नोटिफिकेशन पॅनेलवर सीकबार ड्रॅग करणे रोखते + Previous & Next बटणांसह सीक करा + व्हिडिओ बदलण्याऐवजी मीडिया नोटिफिकेशन, लॉक स्क्रीन आणि हेडफोनवरील नियंत्रणे सीक करण्यासाठी बदल करा + रिवाइंड लांबी + Previous बटणाद्वारे रिवाइंड करण्यासाठी सेकंद + फास्ट फॉरवर्ड लांबी + Next बटणाद्वारे फास्ट-फॉरवर्ड करण्यासाठी सेकंद + सीक लांबी १ आणि %s सेकंदांच्या दरम्यान असावी. डीफॉल्ट वापरत आहे QUIC प्रोटोकॉल अक्षम करा @@ -124,6 +167,10 @@ Second \"item\" text" मूळ ऑडिओ भाषेचा वापर करा डब केलेल्या आवृत्तीऐवजी व्हिडिओचा मूळ ऑडिओ ट्रॅक नेहमी प्ले करा + शांतता वगळा + व्हिडिओ ऑडिओमधील शांत विरामांना आपोआप वगळते + किमान विरामाची लांबी + केवळ किमान इतक्या लांबीचे विरामच कमी केले जातात. पुढील व्हिडिओ सुरू झाल्यावर याचा प्रभाव पडेल नेटवर्क प्रॉक्सी प्रॉक्सीद्वारे नेटवर्क विनंत्या पाठवा @@ -147,6 +194,7 @@ Second \"item\" text" पूर्ण स्क्रीन जाहिराती लपवा + हे वैशिष्ट्य वापरण्यासाठी, \'Spoof video streams\' बदलून \'Android Studio\' आणि \'Android VR Downgraded\' व्यतिरिक्त कोणताही क्लायंट निवडा सामायिक लिंक संपादित करा सामायिक लिंक्समधून ट्रॅकिंग क्वेरी पॅरामीटर्स काढून टाकते @@ -157,6 +205,11 @@ Second \"item\" text" डीबग लॉग प्रोटोकॉल बफर डेव्हलपरसाठी ॲप घटक ओळखण्यासाठी अतिरिक्त लॉगिंग माहिती जोडते + "ही सेटिंग सक्षम केल्यावर काही UI घटकांसाठी स्क्रीनवरील मजकुरासह अतिरिक्त लेआउट डेटा लॉग केला जाईल. + +कस्टम फिल्टर्स तयार करताना घटक ओळखण्यासाठी याची मदत होऊ शकते. + +तथापि, हे सक्षम केल्याने तुमचा IP पत्ता यांसारखा काही वापरकर्ता डेटा देखील लॉग केला जाईल" सानुकूल फिल्टर कोणतेही घटक लपवण्यासाठी सानुकूल फिल्टर कॉन्फिगर करा @@ -172,25 +225,65 @@ Second \"item\" text" व्ह्यूअर डिसcretion संवाद काढा हे वय निर्बंध बायपास करत नाही. हे फक्त ते आपोआप स्वीकारते + PoToken प्रदाता + प्लेबॅक समस्या टाळण्यासाठी PoToken प्रदाता कॉन्फिगर करा + External PoToken प्रदाता + "External PoToken minter वापरून प्लेबॅक समस्यांचे निराकरण करते (प्रायोगिक) + +जर तुम्ही YouTube Premium वापरकर्ता असाल, तर या सेटिंगची आवश्यकता नसेल" "बाह्य PoToken मिंटर वापरून प्लेबॅक समस्या दुरुस्त करते (प्रायोगिक) ही सुविधा वापरण्यासाठी, 'व्हिडिओ स्ट्रीम्स स्पूफ' बंद करा" + जर हे काम करत नसेल, तर MicroG फोर्स-क्लोज करण्याचा प्रयत्न करा + PotHelper सेटिंग्ज उघडा + "हे फीचर सध्या विकसित होत आहे आणि कदाचित काम करणार नाही + +PotHelper डाउनलोड करण्यासाठी येथे टॅप करा" + PotHelper आता काम करत नाही + "PotHelper च्या या आवृत्तीत अंतर्निर्मित टोकन आता काम करत नाही + +प्लेबॅक दुरुस्त करण्यासाठी 'व्हिडिओ स्ट्रीम्स स्पूफ करा' चालू करा" + चालू करा ॲपची आवृत्ती बदलणे सर्व्हरला जुन्या ॲप आवृत्ती वापरत आहात असे दर्शवून जुन्या UI वैशिष्ट्यांची पुनर्संचयित करणे + "ॲपची आवृत्ती जुन्या आवृत्तीप्रमाणे दाखवली जाईल. + +यामुळे ॲपचे स्वरूप आणि वैशिष्ट्ये बदलतील, परंतु अज्ञात दुष्परिणाम होऊ शकतात. + +नंतर हे बंद केल्यास, UI बग्स टाळण्यासाठी ॲप डेटा क्लियर करण्याची शिफारस केली जाते" ॲप आवृत्ती बदलण्याचे लक्ष्य + ॲप रिफ्रेश रेट + डिफॉल्ट + %s FPS + रिफ्रेश रेट ओव्हरराइड करा + नेहमी + प्लेबॅक पोर्ट्रेट + प्लेबॅक फुलस्क्रीन + प्लेबॅक पोर्ट्रेट आणि फुलस्क्रीन + फॉन्ट + डिफॉल्ट + डिव्हाइस सिस्टम फॉन्ट + कस्टम फॉन्ट फाईल + कस्टम फॉन्ट फाईल + TTF किंवा OTF फॉन्ट फाईल निवडा + निवडलेली फाईल TTF किंवा OTF फॉन्ट नाही + कस्टम फॉन्ट लोड होऊ शकला नाही आणि तो बंद करण्यात आला आहे dislikes तात्पुरते उपलब्ध नाहीत (API वेळेत समाप्त) + Dislikes उपलब्ध नाहीत (स्थिती %s) dislikes उपलब्ध नाहीत (क्लायंट API मर्यादा) dislikes उपलब्ध नाहीत (%s) मालकाने लपवले + इतर %s लोकांसोबत या व्हिडिओला नापसंद करा + या व्हिडिओला नापसंद करा, %s मतं नापसंद आहेत Return YouTube Dislike सक्षम करा आवड म्हणून dislikes दर्शवा dislikes ची संख्या ऐवजी likes च्या टक्केवारीनुसार दर्शवते @@ -198,6 +291,9 @@ Second \"item\" text" निर्मात्याने likes अक्षम केलेल्या व्हिडिओवर अंदाजित like संख्या दर्शवते कनेक्शन त्रुटी सूचना दर्शवा Return YouTube Dislike सर्व्हर उपलब्ध नसल्यास सूचना दर्शवते + "Return YouTube Dislike द्वारे २०२१ मध्ये YouTube ने लपवलेली डिसलाईक संख्या पुन्हा उपलब्ध होते. ही संख्या काढण्यापूर्वी संग्रहित केलेल्या डेटावरून येते आणि नवीन व्हिडिओंसाठी वापरकर्त्यांच्या मतांवरून अंदाज लावला जातो, त्यामुळे ते अंदाजित असतात आणि दर काही दिवसांनी अपडेट होतात + +तुम्ही पाहत असलेल्या व्हिडिओचा ID Return YouTube Dislike API ला पाठवला जातो, आणि अंदाजांमध्ये सुधारणा करण्यासाठी तुमचे लाईक्स आणि डिसलाईक्स निनावीपणे पाठवले जातात" Return YouTube Dislike API या डिव्हाइसची आकडेवारी API प्रतिसाद वेळ, सरासरी @@ -207,8 +303,11 @@ Second \"item\" text" dislikes तात्पुरते उपलब्ध नाहीत - क्लायंट API दर मर्यादा लागू आहे API fetch votes, कॉलची संख्या नेटवर्क कॉल केलेले नाहीत + %s नेटवर्क कॉल्स केले API fetch votes, टाइमआउटची संख्या नेटवर्क कॉल टाइमआउट झाले नाहीत API क्लायंट दर मर्यादा क्लायंट दर मर्यादा आढळली नाही + क्लायंट रेट लिमिट %s वेळा आढळली + %s मिलीसेकंद diff --git a/patches/src/main/resources/addresources/values-mr-rIN/shared/strings.xml b/patches/src/main/resources/addresources/values-mr-rIN/shared/strings.xml index 176dcf61b1..1a846d21f6 100644 --- a/patches/src/main/resources/addresources/values-mr-rIN/shared/strings.xml +++ b/patches/src/main/resources/addresources/values-mr-rIN/shared/strings.xml @@ -24,17 +24,22 @@ Second \"item\" text" रंग रीसेट करा अवैध रंग पुनरारंभ आवश्यक + हा बदल लागू करण्यासाठी अॅप रीस्टार्ट करा पुनरारंभ करा आयात करा कॉपी करा Morphe सेटिंग्ज डीफॉल्टवर रीसेट करा + %s सेटिंग्ज इंपोर्ट केल्या आयात अयशस्वी: %s + वर नेव्हिगेट करा सेटिंग्ज शोधा + बद्दल \'%s\' साठी कोणतेही परिणाम आढळले नाहीत दुसरा कीवर्ड वापरून पहा नवीनतम शोध शोध इतिहासमधून काढायचे आहे? + शोध इतिहासातून काढून टाका शोध इतिहास साफ करा तुम्ही सर्व शोध इतिहास साफ करू इच्छिता? शोध टिप्स @@ -46,6 +51,14 @@ Second \"item\" text" शोध इतिहास रिक्त आहे शोध इतिहास जतन करण्यासाठी, शोध क्वेरी टाइप करा आणि एंटर दाबा शोध इतिहास दर्शवा + Morphe सेटिंग्जचे नाव + डिफॉल्ट + इतर + ट्विक्स + मॉर्फिंग वेळ + कस्टम + नाव प्रविष्ट करा + नाव रिकामे असू शकत नाही Morphe सेटिंग्ज चिन्ह दर्शवा कॉपी केले: %s Morphe भाषा @@ -88,6 +101,9 @@ Second \"item\" text" डीबग लॉग स्टॅक ट्रेस त्रुटी ओळखण्यात मदत करण्यासाठी डीबग लॉगमध्ये स्टॅक ट्रेस समाविष्ट करते Morphe त्रुटीवर टोस्ट दर्शवा + "एरर टोस्ट बंद केल्यामुळे सर्व Morphe एरर नोटिफिकेशन्स लपवले जातील. + +कोणत्याही अनपेक्षित घटनांबद्दल तुम्हाला सूचित केले जाणार नाही" डीबग लॉग एक्सपोर्ट करा मॉर्फे डीबग लॉग एक्सपोर्ट करा डीबग लॉग यशस्वीरित्या एक्सपोर्ट केले @@ -103,23 +119,91 @@ Second \"item\" text" बूलियन वैशिष्ट्ये व्यवस्थापित करा वैशिष्ट्य ध्वज शोधा… + सक्रिय नाही (%s) + सक्रिय (%s) + ब्लॉक केले (%s) + फोर्स केले (%s) + स्वयंचलित + बंद + चालू + फोर्स ऑन करा + ब्लॉक करा + काढून टाका फीचर फ्लॅग सेव्ह केले फीचर फ्लॅग रीसेट केले फीचर फ्लॅग क्लिपबोर्डवर कॉपी केले हे सेटिंग वापरण्यासाठी ॲप रीस्टार्ट करा + एक्सपोर्ट आणि इम्पोर्ट करा + %s फीचर फ्लॅग्स इम्पोर्ट केले + कोणतेही फीचर फ्लॅग्स सापडले नाहीत + फीचर फ्लॅग्स यशस्वीरित्या एक्सपोर्ट झाले + फीचर फ्लॅग्स एक्सपोर्ट करण्यात अपयश आले + फीचर फ्लॅग्स लोड झाले, लागू करण्यासाठी \'सेव्ह\' वर टॅप करा + फाईल वाचण्यात अपयश आले + बायनरी सर्च + "कोणत्या फीचर फ्लॅगमुळे ॲपचे वर्तन घडते ते शोधा. + +%s फ्लॅगपैकी अर्धे ब्लॉक केले आहेत आणि प्रत्येक रीस्टार्टनंतर तुम्ही सांगता की ते वर्तन अजूनही आहे का. + +सुरू करण्यापूर्वी एकदा वर्तनाची पुनरावृत्ती करा, अन्यथा ॲपने अद्याप विचारलेल्या फ्लॅग्स शोधातून गहाळ होतील" + सुरू करा + "शोधण्यासाठी कोणतेही फीचर फ्लॅग नाहीत. + +केवळ तेच फ्लॅग बायनरी सर्चमध्ये वापरले जाऊ शकतात जे ॲपने हिट केले आहेत आणि सध्या सक्तीने चालू केलेले नाहीत" + "पायरी %1$s + +%2$s फ्लॅग शिल्लक आहेत, त्यापैकी %3$s या पायरीसाठी ब्लॉक केले आहेत. + +ॲप रीस्टार्ट करा, वर्तन आहे की नाही ते तपासा, त्यानंतर खाली उत्तर द्या" + फीचर फ्लॅग शोध सुरू आहे + वर्तन आहे + वर्तन नाही + बायनरी सर्च रद्द करा + बायनरी सर्च रद्द केला + "Feature flag %s मुळे हे वर्तन होत आहे. + +ते आता ब्लॉक केले आहे" + "कोणताही feature flag सापडला नाही. + +अॅपचे वर्तन feature flag द्वारे नियंत्रित केले जात असल्याचे दिसत नाही" + "एकही feature flag सापडला नाही. + +शोध प्रक्रियेत हे वर्तन आधीच नाहीसे झाले, त्यामुळे एकापेक्षा जास्त फ्लॅग संबंधित असू शकतात किंवा एखादे उत्तर चुकीचे असू शकते" MicroG सेटिंग्ज उघडा + MicroG इंस्टॉल केलेले नाही. ते इंस्टॉल करा कृती आवश्यक + "तुमचे MicroG v%1s जुने झाले आहे आणि त्यामुळे समस्या येऊ शकतात. + +कृपया MicroG नवीनतम आवृत्ती v%2s मध्ये अपडेट करा" + अपडेट करा + दुर्लक्ष करा + "इन्स्टॉल केलेले MicroG Morphe द्वारे शिफारस केलेले नाही: + +%1s + +ते अनइन्स्टॉल करण्यासाठी टॅप करा, त्यानंतर हे अॅप पुन्हा उघडा आणि MicroG RE इन्स्टॉल करा" + अनइन्स्टॉल करा + "MicroG ला बॅकग्राउंडमध्ये चालण्याची परवानगी नाही. + +तुमच्या फोनसाठी \"Don't kill my app\" मार्गदर्शक फॉलो करा आणि तुमच्या MicroG इन्स्टॉलेशनसाठी त्या सूचना लागू करा. + +अॅप चालू राहण्यासाठी हे आवश्यक आहे" वेबसाइट उघडा + "समस्या टाळण्यासाठी MicroG बॅटरी ऑप्टिमायझेशन बंद करणे आवश्यक आहे. + +MicroG साठी बॅटरी ऑप्टिमायझेशन बंद केल्याने बॅटरीच्या वापरावर नकारात्मक परिणाम होणार नाही. + +कंटिन्यू बटणावर टॅप करा आणि ऑप्टिमायझेशन बदलांना परवानगी द्या" सुरू ठेवा MicroG बॅटरी ऑप्टिमायझेशन डायलॉग दाखवा "हे अक्षम केल्यास MicroG बॅटरी ऑप्टिमायझेशन प्रॉम्प्ट लपविला जाईल. @@ -146,4 +230,6 @@ Second \"item\" text" ⚠️ मला प्रायोगिक हवे आहे ✅ मला स्थिर हवे आहे + क्रॅश लॉग वापरकर्त्याच्या डॉक्युमेंट्समध्ये जतन केले: %s + क्रॅश लॉग जतन केला: %s diff --git a/patches/src/main/resources/addresources/values-mr-rIN/youtube/strings.xml b/patches/src/main/resources/addresources/values-mr-rIN/youtube/strings.xml index 3979ab1aeb..53dbbb50e5 100644 --- a/patches/src/main/resources/addresources/values-mr-rIN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-mr-rIN/youtube/strings.xml @@ -32,10 +32,26 @@ Second \"item\" text" पार्श्वभूमीतील प्ले निर्बंध काढून टाका पार्श्वभूमीतील प्लेबॅक बंद करा + डिवाइस लॉक झाल्यावर ऑटो पॉज + बंद + लॉक झाल्यावर नेहमी पॉज करा + वायरलेस ऑडिओ कनेक्ट नसल्यास पॉज करा + स्क्रोलिंग गती मर्यादा अक्षम करा लेआउट अपडेट अक्षम करा सर्व्हरला ॲप लेआउट अपडेट करण्यापासून प्रतिबंधित करते + "ॲप लेआउट पहिल्यांदा इंस्टॉल केल्यावर वापरलेल्या लेआउटवर परत जातो. + +काही सर्व्हर-साइड लेआउट परत येणार नाहीत. + +बदलांमध्ये समाविष्ट आहे: +• प्लेयर फ्लायआउट मेनूमधील (किंवा संबंधित सेटिंग्जमधील) घटक काम करणार नाहीत +• रोलिंग नंबर्स ॲनिमेट होणार नाहीत +• लायब्ररी टॅब दिसतो +• व्हिडिओ वर्णनातील संगीत विभाग काम करणार नाही +• प्लेयर फ्लायआउट मेनू आयटम्स लपवता येणार नाहीत +• लायब्ररी टॅबवर अकाउंट स्विच बटण दिसणार नाही" अ‍ॅक्टिव्ह फीड फ्लाईआउट मेनू फिल्टर फीड फ्लाईआउट मेनू फिल्टर @@ -47,6 +63,8 @@ Second \"item\" text" कलाकाराच्या वर्णनाखालील अल्बम कार्ड लपवते कलाकार कार्ड्स लपवा शोध परिणामांच्या शीर्षस्थानी कलाकाराचे कार्ड लपवते + चॅनेल बटणे लपवा + शोध निकालांमधील चॅनेल प्रोफाइलमधून Subscribe, View Channel, Community आणि YouTube Music सारखी बटणे लपवते समुदाय पोस्ट्स लपवा कॉम्पॅक्ट बॅनर लपवा कॉम्पॅक्ट बॅनर आणि काही घटकांमधील अंतर लपवते @@ -57,6 +75,7 @@ Second \"item\" text" फ्लोटिंग मायक्रोफोन बटण लपवा शोध सूचना स्क्रीनच्या खालच्या उजव्या कोपऱ्यातील फ्लोटिंग मायक्रोफोन बटण लपवते \'Get Premium\' बटण लपवा + हिस्ट्री शेल्फ लपवा आडव्या रांगा लपवा ब्रेकिंग न्यूज, कम्युनिटी पोस्ट्स तुम्हाला आवडतील अशा, पाहणे सुरू ठेवा, अधिक चॅनेल एक्सप्लोर करा, अधिक विषय एक्सप्लोर करा, तुमच्यासाठी, नवीनतम व्हिडिओ, सर्वाधिक संबंधित, निर्मात्याच्या समुदायातील पोस्ट्स, खरेदी, आणि पुन्हा पहा यांसारखे क्षैतिज शेल्फ लपवा इमेज शेल्फ लपवा @@ -69,12 +88,17 @@ Second \"item\" text" This button usually appears in the Home feed for refreshing the feed. --> \'नवीनतम व्हिडिओ\' बटन लपवा होम फीडच्या शीर्षस्थानी दिसणारे \'नवीनतम व्हिडिओ\' चे निळे बॅनर लपवते + लाईव्ह स्ट्रीम्स लपवा + होम फीड आणि शोध निकालांमध्ये सध्या लाईव्ह असलेले व्हिडिओ लपवते मिक्स प्लेलिस्ट लपवा चित्रपट विभाग लपवा + सूचना मेनू हेडर लपवा + बेल बटणावर क्लिक केल्यावर सूचना मेनू हेडर लपवते \'मला सूचित करा\' बटण लपवा + शेड्युल केलेल्या लाईव्ह स्ट्रीम्स आणि प्रीमियरसाठी रिमाइंडर बटण लपवते Playables लपवा गेम खेळण्याची परवानगी देणारे वैशिष्ट्य लपवते शोध शब्दांचे थंबनेल लपवा @@ -84,9 +108,14 @@ Second \"item\" text" \'अधिक दाखवा\' बटण लपवा शोध परिणामांमधील व्हिडिओंच्या खालील विस्तार बटण लपवते सबस्क्राईब केलेले चॅनेल बार लपवा + सबस्क्राइब केलेल्या चॅनेलची नावे लपवा + सबस्क्राईब केलेल्या चॅनेल बारमधील चॅनेलची नावे लपवते सर्वेक्षणे लपवा फीड शिफारस अचूकतेबद्दल सर्वेक्षण आणि सूचना लपवा तिकीट शेल्फ लपवा + + \'अनुपलब्ध व्हिडिओ लपवले आहेत\' हे बॅनर लपावा + प्लेलिस्टमधील \'अनुपलब्ध व्हिडिओ लपवले आहेत\' हे बॅनर लपवते अपलोड वेळ लपवा "मर्यादा: @@ -94,6 +123,8 @@ Second \"item\" text" • हे ऑटोमोटिव्ह लेआउटमध्ये कार्य करत नाही." व्हिडिओ थंबनेल लपवा + व्हिडिओ लेबल्स लपवा + \'ऑटो-डब केलेले\' / \'डब केलेले\', \'कमिशन मिळवते\', \'हायप्ड\', \'लोकांनी हे देखील पाहिले\', \'उत्पादन(उत्पादने)\' आणि \'तुम्हाला हे देखील आवडेल\' सारखी लेबल्स लपवते व्ह्यू काउंट लपवा "मर्यादा: @@ -105,6 +136,7 @@ Second \"item\" text" YouTube Doodles लपवा YouTube लोगोवरील हंगामी आणि इव्हेंट-आधारित ॲनिमेशन लपवते + YouTube Doodles दाखवल्यास सानुकूल हेडर दिसण्यास अडथळा येऊ शकतो चॅनेल बार लपवा चॅनेल वॉटरमार्क लपवा अध्याय आणि \'या व्हिडिओमध्ये\' बटण लपवा @@ -127,18 +159,28 @@ Second \"item\" text" स्नॅकबार् लपवा सदस्य मार्गदर्शक तत्त्वे लपवा \"सिंक टू व्हिडिओ\" बटण लपवा + टॅप आणि होल्ड ग्रेडियंट लपवा + वेग वाढवण्यासाठी होल्ड करून ठेवताना बोट सरकवल्यावर प्लेयर कंट्रोल ग्रेडियंट लपवला जातो वेळेनुसार प्रतिक्रिया लपवा टिप्पणी विभागात आणि लाइव्ह चॅटमध्ये इमोजीसह प्रतिक्रिया देण्याची परवानगी देणारे वैशिष्ट्य लपवते + \'मदत आणि अभिप्राय\' मेनू लपवा + थ्री-डॉट बटण वापरताना, जसे की शोध निकालांवर, \'मदत आणि अभिप्राय\' मेनू लपवते + हँडल लपवा + \'तुम्ही\' टॅबमध्ये तुमच्या चॅनेलचे हँडल लपवते व्हिडिओ शीर्षक लपवा फुलस्क्रीन मोडमध्ये व्हिडिओ शीर्षक लपवते व्हिडिओ सबटायटल सॅनिटाईझ करा + व्हिडिओ सबटायटलमधून \'#\', \'निधी संकलन\', \'उत्पादन(उत्पादने)\', आणि \'शॉप\' सारखे वाक्यांश काढून टाकते + \'Gemini ला विचारा\' बटण लपवा + \'या व्हिडिओबद्दल विचारा\' पॅनेलच्या तळाशी असलेले \'Gemini ला विचारा\' बटण लपवते व्हिडिओ वर्णन व्हिडिओ वर्णनात कोणते घटक दिसतात ते निवडा \'AI- व्युत्पन्न व्हिडिओ सारांश\' विभाग लपवा \'विचारा\' विभाग लपवा \'गुणधर्म\' विभाग लपवा ठरलेल्या ठिकाणे, गेम्स, संगीत आणि लोकांचे उल्लेख असलेले विभाग लपवते + चॅनेल लिंक्स विभाग लपवा \'अध्याय\' विभाग लपवा \'सुधारणा\' विभाग लपवा \'अभ्यासक्रमाची प्रगती\' विभाग लपवा @@ -151,8 +193,10 @@ Second \"item\" text" ठळक प्लेलिस्ट विभाग लपवा \'ठरलेल्या व्हिडिओंचा\' विभाग लपवा हाश्शाष हाश्शाष वाबाल + \'हे कसे बनवले\' विभाग लपवा \'उत्साह बिंदू\' लपवा \'माहिती कार्ड्स\' विभाग लपवा + \'मुख्य संकल्पना\' विभाग लपवा \'क्विझ\' विभाग लपवा \"Search inside this video\" विभाग लपवा \'सदस्यता बटण\' लपवा @@ -160,6 +204,7 @@ Second \"item\" text" \'व्हिडिओ तपशील\' विभाग लपवा फिल्टर बार स्क्रोलिंग फिल्टर बार लपवण्यासाठी ठिकाण निवडा. + चॅनेल पृष्ठात लपा टिप्पण्यांमध्ये लपवा फीडमध्ये लपवा संबंधित व्हिडिओमध्ये लपवा @@ -176,6 +221,8 @@ Second \"item\" text" सहभागी व्हा बटण लपवा लिंक्सचा पूर्वावलोकन लपवा चॅनेल हेडरमधील क्लिक करण्यायोग्य वेब लिंक्स लपवते + \'केवळ सदस्य\' चिप लपा + चॅनल फिल्टर बारमधील \'Members only\' चिप लपवते सदस्य शेल्फ लपवा चॅनेल सदस्यांची यादी लपवते पोस्ट्स शेल्फ लपवा @@ -223,6 +270,7 @@ Second \"item\" text" होम फीडमध्ये कमेंट सेक्शन लपवा समुदाय मार्गदर्शक तत्त्वे लपवा \'शॉर्ट तयार करा\' बटण लपवा + \'नापसंत\' बटण लपवा इमोजी बटण लपवा फिल्टर बार पर्याय लपवा \'टॉप\' आणि \'नवीनतम\' वगळता सर्व फिल्टर बार पर्याय लपवते @@ -230,10 +278,21 @@ Second \"item\" text" \',\'\'गिफ्ट\' बटण लपवा\',\' माहिती बटण लपवा लाइव्ह चॅट देणगीदारांचा बार लपवा + लाईव्ह चॅट \'स्क्रोल टू बॉटम\' बटण लपवा + लाईव्ह चॅट टूलटिप्स लपवा + चॅट इनपुट बारजवळ दिसणारे टूलटिप पॉपअप्स लपवते, जसे की केवळ सबस्क्रायबर्ससाठी चॅटची सूचना + मेनू बटण लपवा + प्रत्येक कमेंटचे ३-बिंदूंचे मेनू बटण लपवते पूर्वावलोकन टिप्पणी लपवा पूर्ण टिप्पणी विभाग उघडण्यापूर्वी व्हिडिओच्या खाली टिप्पणी पूर्वावलोकन लपवते + Morphe कडून लपवलेली टिप्पणी... + मिनिमल कमेंट्स बटण सक्षम करा + कमेंट्स बटणावर दिसणारे स्लाइडिंग कार्ड्स आणि त्याचा पेज सिलेक्टर लपवते धन्यवाद बटण लपवा टाइमस्टॅम्प बटण लपवा + \'Top fans\' बटण लपवा + Translate बटण लपवा + \'अद्याप कोणतीही टिप्पणी नाही\' संदेश लपवा हायलाइट केलेल्या शोध दुव्यांचे शुद्धीकरण करा हायलाइट केलेले शोध दुवे शुद्ध करते, फक्त मूळ साधा मजकूर ठेवून कीवर्ड फिल्टर @@ -250,6 +309,12 @@ Second \"item\" text" कीवर्ड व्हिडिओ शीर्षकांमध्ये किंवा चॅनेल नावांमध्ये दर्शविलेला कोणताही मजकूर असू शकतो मध्यभागी अप्परकेस अक्षरे असलेले शब्द अगदी जुळले पाहिजेत (उदा., iPhone, TikTok, LeBlanc)" + "कीवर्ड वाक्यांश जुळणारे साहित्य लपवण्यासाठी होम / सबस्क्रिप्शन्स / सर्च / कमेंट्स फिल्टर केले जातात + +मर्यादा: +• Shorts चॅनेलच्या नावावरून लपवता येत नाहीत +• काही UI घटक कदाचित लपवले जाणार नाहीत +• कीवर्ड शोधल्यास कदाचित कोणतेही निकाल दिसणार नाहीत" संपूर्ण शब्द जुळवा कीवर्ड/वाक्यांशला दुहेरी अवतरणांमध्ये टाकल्यास व्हिडिओ शीर्षक आणि चॅनेल नावांचे आंशिक जुळण टाळता येईल<br><br>उदाहरणार्थ,<br><b>\"ai\"</b> व्हिडिओ लपवेल: <b>AI कसे कार्य करते?</b><br>पण लपवणार नाही: <b> fair use चा अर्थ काय आहे?</b> @@ -287,7 +352,40 @@ Second \"item\" text" मर्यादा: • फिल्टर केलेले AI शॉर्ट्स रिक्त जागेच्या रूपात दिसू शकतात • काही AI व्हिडिओ अजूनही दिसू शकतात" + "AiSList ही YouTube चॅनल्सची एक ओपन सोर्स यादी आहे जी AI-जनरेटेड कंटेंट प्रकाशित करतात, जी स्वयंसेवकांद्वारे राखली जाते. वापरकर्त्यांकडून चॅनल्सची तक्रार केली जाते आणि ती जोडण्यापूर्वी त्यांची पुनरावलोकन केली जातात + +ही यादी GitHub वरून डाउनलोड केली जाते आणि व्हिडिओ तुमच्या डिव्हाइसवर फिल्टर केले जातात" + AI चॅनेल रिपोर्ट करा + व्हिडिओ मेनूमध्ये रिपोर्ट एन्ट्री जोडा + व्हिडिओ मेनूमध्ये अशी एक एन्ट्री जोडा जी व्हिडिओचे चॅनेल AiSList ला रिपोर्ट करेल + सबमिट करणाऱ्याचे नाव + सार्वजनिक AiSList लीडरबोर्डवर तुमच्या रिपोर्टच्या बाजूला दिसणारे नाव. हे रिकामे असताना रिपोर्टिंग उपलब्ध नसते + AI चॅनेल रिपोर्ट करा + "या व्हिडिओचा पुरावा म्हणून वापरून %s ला AiSList मध्ये सबमिट करायचे आहे का? + +केवळ अशा चॅनेलची तक्रार करा जे: +• AI आवाज, व्हिज्युअल्स, स्क्रिप्ट किंवा संगीत मुख्य सामग्री म्हणून वापरतात +• AI टूल्स वापरून आणि कमीत कमी मानवी श्रमांनी मोठ्या प्रमाणावर सामग्री तयार करतात + +केवळ AI बद्दल चर्चा करणाऱ्या किंवा त्यांच्या व्हिडिओंमधील काही भागांसाठी AI वापरणाऱ्या चॅनेलची तक्रार करू नका. + +तक्रारी सार्वजनिक असतात आणि चॅनेल सूचीमध्ये जोडण्यापूर्वी त्यांची पुनरावलोकन केले जाते. GitHub वरील विनंतीद्वारेच ते काढून टाकणे शक्य आहे" + हे चॅनेल आधीच AiSList ब्लॉकलिस्टमध्ये आहे + हे चॅनेल आधीच AiSList वॉर्नलिस्टमध्ये आहे + तक्रार करा + चॅनेलची तक्रार करण्यासाठी AI चॅनेल फिल्टर सेटिंग्जमध्ये सबमिटरचे नाव सेट करा + चॅनेल शोधत आहे… + या व्हिडिओचे चॅनेल मिळू शकले नाही + चॅनेलची तक्रार करण्यात आली आहे आणि पुनरावलोकनाची प्रतीक्षा आहे + AiSList तक्रार जतन करू शकले नाही, कृपया नंतर पुन्हा प्रयत्न करा + खूप जास्त तक्रारी पाठवल्या गेल्या आहेत, कृपया नंतर पुन्हा प्रयत्न करा + AiSList ने तक्रार नाकारली आहे + तक्रार अयशस्वी झाली, तुमचे कनेक्शन तपासा + चॅनेल जाहिरात व्हाईटलिस्ट + व्हाइलिस्ट केलेल्या चॅनेलवरील व्हिडिओ जाहिरातींसह प्ले होतात, जेणेकरून त्यांच्या निर्मात्यांना पैसे मिळतील. इतर सर्व चॅनेलवर जाहिराती ब्लॉक राहतील + चॅनेल जाहिरात व्हाइलिस्टमध्ये जोडा + चॅनेल जाहिरात व्हाइलिस्टमधून काढून टाका क्रिएटर स्टोअर शेल्फ लपवा एंड स्क्रीन स्टोअर बॅनर लपवा सामान्य जाहिराती लपवा @@ -300,9 +398,33 @@ Second \"item\" text" YouTube Premium जाहिरात वृत्तपत्रे लपवा + लोड होत आहे… + सध्याचे चॅनेल: + व्हाइलिस्टमध्ये जोडा + काढा + व्हाइलिस्ट केलेली चॅनेल्स + कोणतेही चॅनेल व्हाइलिस्ट केलेले नाही + चॅनेल व्हाइलिस्टमध्ये जोडले + चॅनेल व्हाइलिस्टमधून काढले + चॅनेलमध्ये शोधा + केवळ त्या चॅनेलवर शोधण्यासाठी चॅनेल पेजेसवर दुसरे शोध बटण जोडते + चॅनेल शोधा + चॅनेल शोध निकाल + या चॅनेलवर काहीही आढळले नाही + या चॅनेलवर शोधता आले नाही + व्हिडिओ + पोस्ट + या चॅनेलवरील कोणत्याही पोस्ट जुळत नाहीत + सुसंगतता + सर्वात जास्त पाहिलेले + सर्वात कमी पाहिलेले + सर्वात नवीन + सर्वात जुने + सर्वात लहान Short + सर्वात लांब लिंक क्लिपबोर्डवर कॉपी केली टाइमस्टॅम्पसह लिंक क्लिपबोर्डवर कॉपी केली @@ -311,16 +433,56 @@ Second \"item\" text" डीफॉल्टनुसार टाइमस्टॅम्प कॉपी करा \'कॉपी व्हिडिओ लिंक\' बटणाच्या टॅप आणि टॅप-अँड-होल्ड क्रिया उलटवते + व्हिडिओ शीर्षक कॉपी करा + व्हिडिओचे शीर्षक कॉपी करण्यासाठी त्यावर दाबून धरा + व्हिडिओ शीर्षक क्लिपबोर्डवर कॉपी केले + कमेंट्स कॉपी करा + कमेंट कॉपी करण्यासाठी त्यावर दाबून धरा + कमेंट क्लिपबोर्डवर कॉपी केली + व्हिडिओ मेनू उघडण्यासाठी दाबून धरा + फीड व्हिडिओवर कोठेही दाबून धरल्यास त्याचे मेनू उघडते + थंबनेलवर \'नंतर पहा\' बटण दाखवा + फीड व्हिडिओच्या थंबनेलवर \'नंतर पहा\' बटण दाखवले जाते. ते दाबून धरल्यास \'प्लेलिस्टमध्ये जतन करा\' पर्याय उघडतो + पोस्टची चित्रे डाउनलोड करा + चित्रे किंवा GIFs असलेल्या पोस्टच्या शेअर बटणाजवळ डाउनलोड बटण दिसते आणि त्यांच्या मेनूमध्ये डाउनलोड पर्याय जोडला जातो + चित्रे डाउनलोड करा + चित्र डाउनलोड करा + %s चित्रे डाउनलोड होत आहेत… + चित्र डाउनलोड होत आहे… + %1$s प्रतिमा %2$s मध्ये जतन केल्या + प्रतिमा %s मध्ये जतन केली + प्रतिमा डाउनलोड करता आली नाही + या पोस्टमध्ये कोणतीही प्रतिमा आढळली नाही + फ्लाईआउट मेनूमध्ये \'Ask\' आयटम जोडा + फीड फ्लाईआउट मेनूमध्ये व्हिडिओ सारांश आयटम जोडते + "Ask साठी सुरुवातीला अशा देशातील साइन-इन केलेले खाते आवश्यक आहे जिथे Ask उपलब्ध आहे. + +एकदा Ask बटण असलेला व्हिडिओ उघडला की, सध्याच्या देशात Ask फीचर उपलब्ध नसले तरीही, हे फीचर सर्व व्हिडिओंसाठी कार्य करते." + व्हिडिओचा सारांश द्या + या व्हिडिओसाठी Ask उघडता आले नाही + Ask तुमच्या खात्यासाठी किंवा सध्याच्या देशासाठी उपलब्ध नाही + थंबनेलवर व्ह्यू काउंट दाखवा + फीड व्हिडिओंचा व्ह्यू काउंट शीर्षकाखाली ऐवजी त्यांच्या थंबनेलच्या खालच्या डाव्या बाजूला दिसतो. काही व्हिडिओ, जसे की शेल्व्हमधील, कदाचित व्ह्यू काउंट दाखवणार नाहीत + फीड थंबनेल लाईक संख्या दाखवा + फीड थंबनेल लाईक टक्केवारी दाखवा + कमेंट्स शोधा + केवळ शोधलेले सर्व शब्द असलेल्या लोड केलेल्या कमेंट्स पाहण्यासाठी कमेंट्समधील सर्च बटणावर टॅप करा. एखाद्या विशिष्ट वाक्यासाठी शब्द अवतरण चिन्हात लिहा + कमेंट्स शोधा + शोधण्यासाठी शब्द + शोधा + साफ करा + क्रमवारी बदला किंवा कमेंट्स शोधण्यासाठी त्या पुन्हा उघडा + शोध हटवला आहे. सर्व कमेंट्स दाखवण्यासाठी क्रमवारी बदला किंवा कमेंट्स पुन्हा उघडा \'टीव्हीवर साइन इन करा\' पॉपअप अक्षम करा \'कनेक्ट युवर डिव्हाइसेस\' पॉपअप अक्षम करा @@ -329,9 +491,25 @@ Second \"item\" text" अध्याय वगळण्यासाठी दोन-बोटांच्या डबल-टॅप जेस्चर अक्षम करा, जे एका बोटाने चुकून ट्रिगर होऊ शकते तृतीय-पक्ष डाउनलोडिंग ॲप्स वापरून व्हिडिओ, पॉडकास्ट किंवा गाणी डाउनलोड करा + प्लेयर ओव्हरले डाउनलोड बटण दाखवा + व्हिडिओ प्लेयरमध्ये एक बटण दाखवते जे तुमचे एक्सटर्नल डाउनलोडर उघडते + टॅप केल्यावर त्वरित डाउनलोड करा + बॅकग्राउंडमध्ये डाउनलोड करण्यासाठी प्रॉम्प्ट्स मिळवा. डाउनलोडर उघडण्यासाठी टॅप करून धरून ठेवा. YTDLnis आणि YTindir सोबत कार्य करते + डाउनलोड्सना \'पाहिले\' म्हणून मार्क करा + एक्सटर्नल डाउनलोडरद्वारे डाउनलोड केलेले व्हिडिओ तुमच्या YouTube वॉच हिस्ट्रीमध्ये जोडले जातात + फुल एचडी (1080p) + मध्यम (192 kbps) + त्वरित डाउनलोड + व्हिडिओ + ऑडिओ + सर्व पर्याय + Shorts मेनूमध्ये डाउनलोड दाखवा + Shorts प्लेयरमध्ये बाह्य डाउनलोडर फ्लाईआउट मेनू आइटम जोडते + डाउनलोड व्हॉईस ओव्हर भाषांतर डिव्हाइसवरील टेक्स्ट-टू-स्पीच वापरून व्हिडिओ कॅप्शन मोठ्याने वाचा, जे प्लेबॅकसह सिंक्रोनाइझ केलेले असेल. + व्हिडिओ प्लेयरमध्ये एक ओव्हरले बटण दाखवते. ट्रान्सलेशन चालू/बंद करण्यासाठी टॅप करा, क्विक सेटिंग्जसाठी टॅप करून धरा भाषा भाषांतर प्रदाता मूळ ऑडिओचे प्रमाण @@ -377,6 +555,7 @@ OpenRouter.ai उघडण्यासाठी येथे टॅप करा OpenRouter.ai उघडा OpenRouter भाषांतर अयशस्वी झाले (HTTP %s) MyMemory दैनिक मर्यादा पोहोचली + पुढील उपलब्धता %s रोजी तुम्ही आजचे विनामूल्य MyMemory भाषांतर कोटा (5,000 वर्ण) वापरले आहे. दररोजची मर्यादा 50,000 वर्णांपर्यंत वाढवण्यासाठी सेटिंग्जमध्ये MyMemory ईमेल जोडा, किंवा भिन्न भाषांतर सेवा स्विच करा तुम्ही आजचे MyMemory भाषांतर कोटा (50,000 वर्ण) वापरले आहे. भिन्न भाषांतर सेवा स्विच करा किंवा उद्या पुन्हा प्रयत्न करा MyMemory भाषांतर अयशस्वी झाले (स्थिती %s) @@ -398,6 +577,9 @@ OpenRouter.ai उघडण्यासाठी येथे टॅप करा %1$s ते %2$s दरम्यान लूपिंग अवैध वेळ श्रेणी + पिक्चर-इन-पिक्चर बटण दाखवा + पिक्चर-इन-पिक्चर मेनू दाखवा + पिक्चर-इन-पिक्चरवर स्विच करा \'प्ले ऑल\' बटण दर्शवा "चॅनेलवरील सर्व व्हिडिओंची प्लेलिस्ट तयार करण्यासाठी टॅप करा, पूर्ववत करण्यासाठी टॅप आणि होल्ड करा @@ -415,6 +597,7 @@ OpenRouter.ai उघडण्यासाठी येथे टॅप करा स्ट्रीम केलेले व्हिडिओ फक्त (लोकप्रियतेनुसार क्रमवारी) सर्व सदस्यांच्या सामग्री फक्त सदस्यांचे फक्त व्हिडिओ + केवळ सदस्यांसाठी Shorts सदस्यांचे फक्त थेट प्रवाह चॅनेल आयडी जुळत नसल्यामुळे प्लेलिस्ट तयार करणे शक्य नाही @@ -427,15 +610,48 @@ OpenRouter.ai उघडण्यासाठी येथे टॅप करा हा व्हिडिओ ‘नंतर पहा’ प्लेलिस्टमध्ये आधीच जतन केला आहे हा व्हिडिओ ‘नंतर पहा’ प्लेलिस्टमध्ये जतन करू शकत नाही नंतर पाहण्यासाठी सेव्ह करा + प्लेयर सेटिंग्ज फ्लाईआउटमध्ये \'नंतर पहा\' मध्ये जतन करा बटण दाखवा + व्हिडिओ सेटिंग्ज फ्लाईआउटमधून सध्या पाहत असलेला व्हिडिओ \'नंतर पहा\' मध्ये जतन करण्याची परवानगी देते मुलांच्या व्हिडिओंमध्ये नंतर पाहण्यासाठी सेव्ह करा बटण दाखवा + फ्लाईआउट मेनूमधून मुलांशी संबंधित व्हिडिओ \'नंतर पहा\' मध्ये जतन करण्याची परवानगी देते + शॉर्ट्स फ्लाईआउटमध्ये \'नंतर पहा\' मध्ये जतन करा बटण दाखवा + फ्लाईआउट मेनूमधून शॉर्ट्स \'नंतर पहा\' मध्ये जतन करण्याची परवानगी देते + Watch later ला प्लेलिस्टवर रिडायरेक्ट करा + नंतर पहा Morphe कतारमध्ये जोडा सत्रांमध्ये रांग ठेवा ॲप रीस्टार्ट केल्यानंतर रांग ठेवल्या जाईल स्वॅप सेव्ह आणि क्यू ॲक्शन + क्यू पर्यायांसाठी टॅप करा, \'नंतर पहा\' मध्ये जतन करण्यासाठी टॅप करून धरा + रांगेत पुढील प्ले करा प्रकार + डिफॉल्ट + नेटिव्ह रांग + Morphe रांग + Morphe रांग क्लायंट साइड फीड फ्लायआउट रांग मेनू आयटम जोडा एक अतिरिक्त फ्लायआउट आयटम जोडते. \'रांगेत जोडा\' दिसत नसल्यास हे सक्षम करा. + रॅंक/रांग + रांग (%s) + सर्व हटवा + रांगेत काहीही नाही + कोणत्याही व्हिडिओसाठी रांगेत पुढील प्ले करा वापरा + आता प्ले होत आहे + रांगेत पुढील प्ले करा + रांगेतील शेवटचा प्ले करा + पुनर्क्रमित करा + काढा + रांगेत जोडले + हा व्हिडिओ क्यूमध्ये जोडता आला नाही + प्लेलिस्ट क्यूमध्ये जोडता येत नाहीत + रांग पूर्ण भरली आहे + व्हिडिओ सुरू होऊ शकला नाही + फ्लाईआउट मेनूमध्ये जाहिरात व्हाईटलिस्ट + एक फ्लाईआउट आयटम जोडते ज्यामुळे त्या व्हिडिओच्या चॅनेलवर जाहिराती प्ले होऊ शकतात + या वैशिष्ट्यासाठी PotHelper वापरणे किंवा \"Spoof video streams\" बदलून TV किंवा Android VR साइन-इन करणे आवश्यक असू शकते + फ्लाईआउट मेनूमध्ये स्पीड व्हाईटलिस्ट + एक फ्लाईआउट आयटम जोडतो जो नेहमी त्या व्हिडिओच्या चॅनेलला 1.0x वेगाने प्ले करतो लॉग इन केलेले नाही प्लेलिस्ट आयडी नाही क्यू रिकामी आहे @@ -460,6 +676,19 @@ OpenRouter.ai उघडण्यासाठी येथे टॅप करा क्यू उघडा प्लेलिस्टमध्ये क्यू जतन करा + प्लेलिस्ट जतन केली + %s मध्ये जतन केले + रांग रिकामी करा + रांग रिकामी केली + रांग + रांगेतील शेवटचे प्ले करा + रांगेतील पुढचे प्ले करा + YouTube क्यू प्रदर्शित करू शकले नाही. + या व्हिडिओसाठी क्यू तयार नाही. व्हिडिओ प्लेयर उघडा आणि पुन्हा प्रयत्न करा. + मिक्स क्यूमध्ये जोडा की मिक्स पहा? + पहा + रांगेत जोडा + मिक्स लोड होत आहे… लाइव्ह अवतारमधून चॅनेल उघडा चॅनेलच्या अवतारवर टॅप केल्यावर लाइव्ह स्ट्रीम उघडण्यापासून प्रतिबंधित करते @@ -467,12 +696,22 @@ OpenRouter.ai उघडण्यासाठी येथे टॅप करा अचूक शोधण्याची हावभाव अक्षम करा सीकबार (seekbar) वर खेचल्यावर सक्रिय होणारा अचूक शोध मोड अक्षम करा + लाईव्ह स्ट्रीम DVR सक्षम करा + सर्व लाईव्ह स्ट्रीम्सवर सीकिंग करण्याची परवानगी देते, अगदी अशा स्ट्रीम्सवर देखील ज्या सहसा त्यावर निर्बंध घालतात सीकबार थंबनेल सक्षम करा सीकबार ड्रॅग करताना व्हिडिओ थंबनेल दर्शवते सर्वाधिक प्ले केलेले + "'लाईव्ह स्ट्रीम DVR सक्षम करा' उपलब्ध नसेल + +हे ठीक करण्यासाठी, 'व्हिडिओ स्ट्रीम्स स्पूफ करा' सुरू करा आणि 'डिफॉल्ट क्लायंट' बदलून 'TV Simply' किंवा 'visionOS' करा" + लाईव्ह स्ट्रीम DVR कालावधी वाढवा + तुम्हाला लाईव्ह ब्रॉडकास्ट्स ७ दिवसांपर्यंत मागे घेऊन पाहण्याची परवानगी मिळते + "'लाईव्ह स्ट्रीम DVR कालावधी वाढवा' उपलब्ध नसेल + +हे ठीक करण्यासाठी, 'व्हिडिओ स्ट्रीम्स स्पूफ करा' चालू करा आणि 'डिफॉल्ट क्लायंट' बदलून 'TV Simply' किंवा 'visionOS' करा" स्लाइड टू सीक सक्षम करा व्हिडिओ प्लेयरवर दाबून धरल्यावर 2x वेगाने प्ले करण्याऐवजी स्लाइड टू सीक सक्षम करा @@ -480,10 +719,18 @@ OpenRouter.ai उघडण्यासाठी येथे टॅप करा टॅप टू सीक सक्षम करा सिBar ला ड्रॅग न करता त्या स्थितीत जाण्यासाठी फक्त टॅप करा + डावीकडे स्वाइप करण्याची कृती + उजवीकडे स्वाइप करण्याची कृती + वरच्या बाजूला स्वाइप करण्याची कृती + डावा/उजवा झोन रुंदी + डावा आणि उजवा स्वाइप जेस्चरसाठी आरक्षित प्रत्येक स्क्रीन कडेची टक्केवारी (५-५०) चमक आवाज मूळ + बंद + लॉक असताना स्वाइपकडे दुर्लक्ष करा + \'स्क्रीन लॉक करा\' वापरून प्लेयर लॉक असताना स्वाइप जेस्चर्सकडे दुर्लक्ष केले जाते प्रेस टू स्वाइप हावभाव सक्षम करा स्वाइप जेश्चर ओळखण्यापूर्वी दाबून ठेवणे आवश्यक आहे हॅप्टिक फीडबॅक सक्षम करा @@ -506,6 +753,12 @@ OpenRouter.ai उघडण्यासाठी येथे टॅप करा पुढील किंवा मागील व्हिडिओवर जाण्यासाठी फुलस्क्रीन मोडमध्ये स्वाइप करा स्वाइप परिमाण थ्रेशोल्ड तळाच्या नेव्हिगेशन बारमध्ये कोणते बटण दिसतात ते निवडा + व्हॉल्यूम स्वाइप संवेदनशीलता + उच्च मूल्यांसाठी व्हॉल्यूम स्टेप प्रति अधिक स्वाइपिंगची आवश्यकता असते + व्हॉल्यूम स्टेप्स + व्हॉल्यूम बूस्ट + जास्तीत जास्त व्हॉल्यूमच्या पुढे वर स्वाइप केल्याने तो बूस्ट होतो, ज्यामुळे मोठा आवाज विरूपित होऊ शकतो + डिव्हाइस डीफॉल्ट चमक स्वाइप संवेदनशीलता जास्त व्हॅल्यू प्रत्येक चमक बदलासाठी जास्त स्वाइपिंगची आवश्यकता असते वेग स्वाइप संवेदनशीलता @@ -557,6 +810,10 @@ OpenRouter.ai उघडण्यासाठी येथे टॅप करा \'विचारा\' बटण लपवा + चॅनेल आणि Subscribe बटण लपवा + ॲक्शन बारच्या सुरुवातीला चॅनेल अवतार आणि Subscribe बटण लपवते + Subscribe बटण लपवा + ॲक्शन बारच्या सुरुवातीला चॅनेल अवतारच्या शेजारील Subscribe आणि Join बटणे लपवते \'क्लिप\' बटण लपवा काही वापरकर्ता खात्यांसाठी हे कार्य करू शकत नाही. @@ -584,7 +841,12 @@ OpenRouter.ai उघडण्यासाठी येथे टॅप करा अनम्यूट केल्यावर दर्शवा शांत असताना दाखवा + पसंतीची कॅप्शन भाषा + प्रथम प्रोव्हायडर सबटायटल्स निवडते, त्यानंतर आपोआप पसंतीच्या भाषेत अनुवाद करते + बंद + अतिरिक्त ऑटो-ट्रान्सलेट भाषा जोडा + ऑटो-ट्रान्सलेट मेनूमध्ये अधिक भाषा जोडते कॅपशन कुकीज सेट करा "स्टॉक YouTube बग दुरुस्त करते जिथे स्वयंचलित भाषांतर मथळे कुकीजच्या कमतरतेमुळे उपलब्ध नसू शकतात @@ -600,6 +862,7 @@ OpenRouter.ai उघडण्यासाठी येथे टॅप करा ट्रान्सक्रिप्ट सुधारा ट्रान्सक्रिप्ट लोड होण्यापासून प्रतिबंधित करणारी अट तपासणी अपयश दूर करते + पिक्चर-इन-पिक्चरमध्ये कॅप्शन्स दाखवा नेव्हिगेशन बटणे तळाच्या नेव्हिगेशन बारमध्ये कोणते घटक दिसतात ते निवडा @@ -622,13 +885,27 @@ OpenRouter.ai उघडण्यासाठी येथे टॅप करा क्रिएट (Create) बटण आणि नोटिफिकेशन (Notifications) बटण बदला जर ही सेटिंग बदलल्याचा परिणाम झाला नाही, तर इन्कॉग्निटो मोड वापरून पहा. नेव्हिगेशन बटण लेबल्स लपवा + नवीन सामग्रीचा डॉट लपवा नेव्हिगेशन बार लपवा + "हे 'तुम्ही' टॅबसह नेव्हिगेशन बार पूर्णपणे लपवते. + +सेटिंग्जमध्ये जाण्यासाठी, General → Toolbar → Show Settings द्वारे सेटिंग्ज बटण सक्षम करा" नॅरो नेव्हिगेशन बटणे सक्षम करा बटणांमधील क्षैतिज अंतर कमी करते + लेबल नसलेली नेव्हिगेशन बटणे अक्षम करा + जर YouTube लेबलशिवाय बटणे पाठवत असेल, तर हे नेव्हिगेशन बटणे लेबल्ससह दाखवते + टूलबार नेव्हिगेशन टॅब अक्षम करा + जर YouTube ने Subscriptions होमच्या वरच्या टॅबमध्ये हलवले, तर ते नेव्हिगेशन बारमध्ये दाखवा नेव्हिगेशन बार एनिमेशन सक्रिय करा टॅब दरम्यान स्विच करताना ॲनिमेशन ऑटो-लपविणे अक्षम करा स्क्रोलिंग करताना नेव्हिगेशन बार दृश्यमान ठेवा + नेव्हिगेशन बार शैली + डिफॉल्ट + ट्रान्सल्युसंट + पारदर्शक + अपारदर्शक + ही शैली तुमच्या डिव्हाइसद्वारे किंवा YouTube आवृत्तीद्वारे समर्थित नसावी, ज्यामुळे लेआउट समस्या किंवा क्रॅश होऊ शकतात टूलबार टूलबारमध्ये कोणते घटक दिसतात ते निवडा \'कास्ट\' (Cast) बटण लपवा @@ -645,6 +922,9 @@ OpenRouter.ai उघडण्यासाठी येथे टॅप करा \'एम्बियंट मोड\' (Ambient mode) मेनू लपवा + "'ऑडिओ ट्रॅक' मेनू लपवलेला आहे + +'ऑडिओ ट्रॅक' मेनू दिसण्यासाठी, 'Spoof video streams' बदलून 'Android Studio' आणि 'Android VR Downgraded' व्यतिरिक्त कोणताही क्लायंट निवडा" \'ऑडिओ ट्रॅक\' मेनू लपवा \'ऑडिओ ट्रॅक\' मेनू फुटर लपवा @@ -661,6 +941,7 @@ OpenRouter.ai उघडण्यासाठी येथे टॅप करा \'व्हिडिओ लूप करा\' (Loop video) मेनू लपवा + \'On-the-go\' मेनू लपवा क्वालिटी मेनू लपवा क्वालिटी मेनू फुटर लपवा @@ -687,6 +968,8 @@ OpenRouter.ai उघडण्यासाठी येथे टॅप करा फुलस्क्रीन बटण लपवा प्लेअर नियंत्रणांचे पार्श्वभूमी लपवा व्हिडिओ प्लेयर नियंत्रणाभोवतीची गडद सावली लपवते + कंट्रोल बटणांच्या बॅकग्राउंडची ओपॅसिटी + प्लेअर कंट्रोल बटणे आणि टाइम बारच्या मागचा बॅकग्राउंड ओपॅसिटी समायोजित करा (0 म्हणजे अदृश्य, 100 म्हणजे घन) मागील आणि पुढील बटणे लपवा सेटिंग्ज बटण लपवा @@ -719,6 +1002,7 @@ OpenRouter.ai उघडण्यासाठी येथे टॅप करा व्हिडिओ वर्णनात लपवा इतिहासामध्ये लपवा Shorts प्लेयर + Shorts प्लेयरमध्ये कोणते घटक दिसावेत ते निवडा डबल-टॅप करून लाईक करणे अक्षम करा कृत्रिम बुद्धिमत्तेचे बटण लपवा \'ऑटो-डब\' लेबल लपवा @@ -726,6 +1010,7 @@ OpenRouter.ai उघडण्यासाठी येथे टॅप करा टिप्पणी बटण लपवा इफेक्ट बटण व्हिडिओ लिंक लेबल लपवा + जेस्चर हिंट्स लपावा \'ग्रीन स्क्रीन\' बटन लपवा हॅशटॅग बटण माहिती पॅनेल लपवा @@ -775,7 +1060,10 @@ OpenRouter.ai उघडण्यासाठी येथे टॅप करा प्लेअर पॉपअप पॅनेल अक्षम करा स्वयंचलितपणे उघडणाऱ्या प्लेलिस्ट आणि लाइव्ह चॅट पॅनेल अक्षम करते + प्लेलिस्ट ऑटोप्ले अक्षम करा + प्लेलिस्टना आपोआप पुढच्या व्हिडिओवर जाण्यापासून रोखते + \'पाहणे सुरू ठेवायचे का?\' प्रॉम्प्ट अक्षम करा व्हिडिओ समाप्त झाल्यावर फुलस्क्रीन मोडमधून बाहेर पडा अक्षम @@ -783,6 +1071,8 @@ OpenRouter.ai उघडण्यासाठी येथे टॅप करा लँडस्केप फक्त पोर्ट्रेट (Portrait) आणि लँडस्केप (Landscape) + फुलस्क्रीनमध्ये लँडस्केप सक्ती करा + फुलस्क्रीनमध्ये प्रवेश करताना प्लेयर लँडस्केपमध्ये फिरवा. लहान स्क्रीनवर उपलब्ध नाही, जे आधीच लँडस्केपमध्ये फुलस्क्रीन उघडतात पूर्ण स्क्रीन हावभाव फुलस्क्रीन मोडमधील प्लेयर जेश्चर @@ -790,18 +1080,31 @@ OpenRouter.ai उघडण्यासाठी येथे टॅप करा पोर्ट्रेट स्वाइप-डाउन हावभाव अक्षम करा लँडस्केप स्वाइप-डाउन हावभाव अक्षम करा पिंच-टू-झूम जेश्चर अक्षम करा + पिंच टू झूम पुन्हा स्थापित करा व्हिडिओ फुलस्क्रीन मोडमध्ये उघडा अक्षम पोर्ट्रेट लँडस्केप + फुलस्क्रीन व्हिडिओ स्केल + डिफॉल्ट + स्ट्रेच (स्क्रीन भरा) + झूम (भरण्यासाठी क्रॉप करा) + व्हिडिओ स्केल बटण दाखवा + डिफॉल्ट, स्ट्रेच आणि झूम दरम्यान स्विच करण्यासाठी टॅप करा + केवळ फुलस्क्रीनमध्ये दाखवा + पोर्ट्रेट प्लेयरमधील व्हिडिओ स्केल बटण लपवा म्यूट बटण दाखवा प्लेअर ओव्हरले अपारदर्शकता ओव्हरले (Overlay) पातळी समायोजित करा (0 म्हणजे पारदर्शक, 100 म्हणजे अपारदर्शक) + शोध बार + अक्षम + रुंद + अतिशय रुंद डिव्हाइस लेआउट @@ -809,12 +1112,30 @@ OpenRouter.ai उघडण्यासाठी येथे टॅप करा फोन टॅबलेट ऑटोमोटिव्ह + "बदलांमध्ये खालील गोष्टींचा समावेश आहे: + +टॅब्लेट लेआउट +• कम्युनिटी पोस्ट्स लपवल्या आहेत +• फीडमधील प्लेबॅक सेटिंग उपलब्ध नाही +• Shorts मध्ये रिमिक्स बटण आणि साउंड बटण उपलब्ध नाहीत +• 'अधिक व्हिडिओ' ओव्हरले उपलब्ध नसू शकतो +• नॉन कोलॅप्स झालेला जुना व्हिडिओ ॲक्शन बार दिसतो +• फ्लाईआउट मेनू आयटम्स कदाचित योग्यरित्या काम करणार नाहीत + +ऑटोमोटिव्ह लेआउट +• फीड विषयानुसार आणि चॅनेलनुसार आयोजित केले आहे +• फीडमधील प्लेबॅक सेटिंग उपलब्ध नाही +• Shorts मध्ये रिमिक्स बटण आणि साउंड बटण उपलब्ध नाहीत +• 'अधिक व्हिडिओ' ओव्हरले उपलब्ध नसू शकतो +• Shorts नियमित प्लेयरमध्ये उघडतात +• नॉन कोलॅप्स झालेला जुना व्हिडिओ ॲक्शन बार दिसतो" प्लेअरमध्ये टॅबलेट लेआउट सक्षम करा "जुना प्लेयर लेआउट पुनर्स्थापित करते मर्यादा: जर 'जुना व्हिडिओ ॲक्शन बार पुनर्स्थापित करा' बंद केले असेल, तर 'अधिक व्हिडिओ' ओव्हरले उपलब्ध नसू शकते" + २१.११.४८ - कॅप्शनचे जुने वर्तन पुन्हा स्थापित करा 20.39.41 - प्लेलिस्टमध्ये जुने सेव्ह वैशिष्ट्य परत आणा 20.28.41 - बोल्ड आयकॉन अक्षम करा 20.13.41 - व्हिडिओ प्लेयरच्या खाली चॅनेलचे नाव पुन्हा स्थापित करा @@ -824,6 +1145,8 @@ OpenRouter.ai उघडण्यासाठी येथे टॅप करा YouTube Music बटणांना ओव्हरराइड करा तुमच्या पसंतीचे म्युझिक ॲप उघडते, स्टॉक YouTube Music ॲपऐवजी + संगीत ॲप पॅकेजचे नाव + तुमच्या इंस्टॉल केलेल्या संगीत ॲपचे पॅकेज नाव सुरुवात पृष्ठ डीफॉल्ट @@ -863,23 +1186,42 @@ OpenRouter.ai उघडण्यासाठी येथे टॅप करा वर्तमान शॉर्ट (Short) पुन्हा न बजावता पुढील शॉर्ट (Short) वर स्वयंचलितपणे जाते पार्श्वभूमीवर (Background) ऑटोप्ले (Autoplay) करा पार्श्वभूमीवर (Background) प्लेबॅक (Playback) करताना पुढील शॉर्ट (Short) वर स्वयंचलितपणे जाते + लाईव्ह Shorts वगळा + Shorts फीडमध्ये दिसणारे लाईव्ह स्ट्रीम्स वगळते \u092b\u094d\u0928 \u092b\u0940\u0926\u0932\u093e\u0930\u093e\u0928 \u092e\u093e\u0926\u094d\u092e\u093e\u0932\u093f \u0926\u093e\u0938 \u092b\u094d\u0928\u093e\u0932\u093f \u0932\u093e\u0938\u093e\u0928\u0937\u0930\u093e \u092a\u093e\u0935\u0930 \u0932\u093e\u0938\u093e\u0928\u0937\u0930 Wi-Fi \u092b\u093e\u0935\u093e\u0928\u093f \u092c\u0926\u094d\u0926 + मूळ शीर्षके पुनर्संचयित करा + ऑटो-ट्रान्सलेटेड शीर्षकाऐवजी मूळ व्हिडिओ शीर्षके, व्हिडिओ वर्णने आणि चॅनेल वर्णने दर्शवते + शीर्षक लोड होत आहे… + ऑटो फीड रिफ्रेश अक्षम करा + फीड्स जुने झाल्यावर ते आपोआप रिफ्रेश होण्यापासून रोखते + फीड कार्ड लेआउट + क्लासिक + कार्ड्स + लाईक, कमेंट आणि सेव्ह बटणांसह कार्ड्स + कमेंट्स मिनीप्लेअर पुन्हा सुरू करणे अक्षम करा \'Continue watching\' मिनीप्लेअर अक्षम करते, YouTube Premium चे वैशिष्ट्य जे ॲप सुरू झाल्यावर दिसते. मिनीप्लेअर मिनीप्लेअरचे (Miniplayer) स्वरूप आणि वर्तन सानुकूलित (Customize) करा मिनीप्लेअर प्रकार + शीर्षक आणि चॅनेल लपवा अक्षम डीफॉल्ट किमान + मिनिमल 2 + टॅब्लेट + मॉडर्न 1 + मॉडर्न 2 + मॉडर्न 3 + मॉडर्न 4 गोलाकार कोपरे अक्षम करा ड्रॅग आणि ड्रॉप अक्षम करा मिनीप्लेअरला (Miniplayer) स्क्रीनच्या वेगवेगळ्या कोपऱ्यात ड्रॅग (Drag) करण्यापासून प्रतिबंधित (Prevent) करते @@ -896,11 +1238,24 @@ OpenRouter.ai उघडण्यासाठी येथे टॅप करा ओव्हरले अपारदर्शकता ओव्हरले (Overlay) पातळी समायोजित (Adjust) करा (0 म्हणजे पारदर्शक, 100 म्हणजे अपारदर्शक) + बॅक बटण फीडमधून बाहेर पडते + बॅक दाबल्यास फीडच्या वरच्या भागावर स्क्रोल करण्याऐवजी नेहमी ॲपमधून बाहेर पडले जाते + अ‍ॅनिमेशन सक्तीने लागू करा + डिफॉल्ट + नेहमी चालू + नेहमी बंद + स्टेटस बार लपवा + सिस्टम स्टेटस बार लपवते. ते थोड्या वेळासाठी पाहण्यासाठी वरच्या कडेकडून खाली स्वाइप करा + स्टेटस बार टॉप बार्सना झाकणे ठीक करा + स्टेटस बारची लेआउट सुसंगत ठेवते, जेणेकरून तो टॉप बार्सना झाकणार नाही आणि प्लेयरच्या वर रिकामी जागा राहणार नाही. जर ॲप क्रॅश झाले किंवा टॉप बार्स चुकीचे दिसत असतील तर हे बंद करा + ग्लो + या डिव्हाइसद्वारे ग्लो स्प्लॅश स्क्रीनला सपोर्ट मिळत नाही लाइट थीम पार्श्वभूमी पांढरा + मटेरिअल यू (पांढरा) हलका गुलाबी हलका निळा हलका हिरवा @@ -910,6 +1265,7 @@ OpenRouter.ai उघडण्यासाठी येथे टॅप करा सानुकूल हलकी पार्श्वभूमी हलक्या थीमची पार्श्वभूमी रंग सेट करा अग्रभागाचा रंग बदला + मजकूर आणि चिन्हांसाठी थीम रंगाचा वापर करा ग्रेडियंट लोडिंग स्क्रीन सक्षम करा लोडिंग (Loading) स्क्रीनवर सॉलिड (Solid) रंगाऐवजी ग्रेडियंट (Gradient) पार्श्वभूमी दर्शवते सानुकूलseekbar रंग सक्षम करा @@ -919,6 +1275,9 @@ OpenRouter.ai उघडण्यासाठी येथे टॅप करा व्हिडिओ प्रगती बारचा दुय्यम रंग सेट करा अवैध seekbar रंग मूल्य + मूळ ॲनालॉग + ओरिजिनल केक + ओरिजिनल मेटल हेडर लोगो डीफॉल्ट @@ -930,15 +1289,32 @@ OpenRouter.ai उघडण्यासाठी येथे टॅप करा प्रतिबंधित (Restricted) देशांमधील गहाळ (Missing) प्रतिमांसाठी पर्यायी सर्व्हरवर थंबनेल (Thumbnail) विनंत्या पुनर्निर्देशित (Redirect) करते + DeArrow शीर्षके + होम टॅब + सबस्क्रिप्शन्स टॅब + तुम्ही टॅब + प्लेयर प्लेलिस्ट आणि शिफारसी + शोध परिणाम + DeArrow आयकॉन दाखवा + क्राउडसोर्स्ड व्हिडिओ शीर्षकांवर DeArrow आयकॉन दाखवते + कॅज्युअल मोड + जर DeArrow वापरकर्त्यांनी मूळ शीर्षक आवडल्याचे मत दिले असेल, तर DeArrow शीर्षकाऐवजी मूळ शीर्षक दाखवते + मूळ शीर्षक दाखवण्यासाठी किमान कॅज्युअल मते + मूळ शीर्षक दाखवण्यासाठी आवश्यक असलेल्या एका श्रेणीतील (उदा. मजेशीर किंवा सर्जनशील) कॅज्युअल मतांची संख्या + DeArrow थंबनेल्स मूळ थंबनेल DeArrow आणि मूळ थंबनेल DeArrow आणि स्टिल कॅप्चर स्टिल कॅप्चर + "DeArrow क्लिकबेट शीर्षके आणि थंबनेल्सना क्राउडसोर्स केलेल्या अधिक अचूक आणि कमी सनसनाटी पर्यायांनी बदलतो. शीर्षके समुदायाद्वारे लिहिली आणि मतदानाद्वारे निवडली जातात, आणि थंबनेल्स व्हिडिओमधून घेतलेली फ्रेम्स असतात + +व्हिडिओची विनंती हॅश केलेल्या व्हिडिओ ID च्या एका लहान प्रीफिक्सद्वारे केली जाते, जेणेकरून सर्व्हरला तुम्ही नेमका कोणता व्हिडिओ पाहत आहात हे सांगता येत नाही. जर एखाद्या व्हिडिओला DeArrow थंबनेल नसेल, तर मूळ किंवा स्थिर कॅप्चर दाखवला जातो" कनेक्शन त्रुटी सूचना दर्शवा DeArrow सर्व्हर उपलब्ध नसल्यास सूचना दर्शवते + API एंडपॉइंट DeArrow थंबनेल कॅशे एंडपॉइंटचा URL व्हिडिओ कॅप्चर व्हिडिओच्या सुरुवाती / मध्यात / शेवटी stills घेतले जातात. ही चित्रे YouTube मध्ये तयार केली जातात आणि कोणत्याही बाह्य API चा वापर केला जात नाही @@ -956,6 +1332,7 @@ OpenRouter.ai उघडण्यासाठी येथे टॅप करा डिव्हाइसचे आकारमान Spoof करा हे सक्षम केल्याने उच्च व्हिडिओ गुणवत्ता अनलॉक होऊ शकते + हे सक्षम केल्यामुळे व्हिडिओ प्लेबॅक मध्ये अडथळे, बॅटरीचे आयुष्य कमी होणे आणि अज्ञात दुष्परिणाम होऊ शकतात Haptic feedback उपकरणाच्या (Device) कंपनांना (Vibrations) ट्रिगर (Trigger) करणारे इंटरॅक्शन (Interaction) निवडा @@ -965,6 +1342,7 @@ OpenRouter.ai उघडण्यासाठी येथे टॅप करा टॅप आणि होल्ड हॅप्टिक्स अक्षम करा Zoom Haptics अक्षम करा + जर तुम्ही अलीकडेच तुमच्या खात्याच्या लॉगिन तपशीलात बदल केले असतील, तर MicroG अनइन्स्टॉल करून पुन्हा इन्स्टॉल करा लिंक रीडायरेक्ट्स बायपास करा ट्रॅकिंग लिंक (youtube.com/redirect) बायपास करून मूळ लिंक थेट उघडते @@ -972,7 +1350,20 @@ OpenRouter.ai उघडण्यासाठी येथे टॅप करा ब्राउझरमध्ये लिंक उघडा इन-ॲप ब्राउझरऐवजी (in-app browser) बाह्य ब्राउझरमध्ये (external browser) लिंक्स उघडते + प्लेबॅक बफर आकार + डीफॉल्ट + कमी + मध्यम + कमाल + व्हिडिओ विलंब + ब्लूटूथ हेडफोन्स सारख्या लॅगी ऑडिओसोबत सिंक ठेवण्यासाठी व्हिडिओला विलंब करते (0-5000 ms) + फक्त ब्लूटूथ ऑडिओसह विलंब करा + केवळ ब्लूटूथ ऑडिओ कनेक्ट असताना व्हिडिओला विलंब करते, जेणेकरून फोन स्पीकर सिंकमध्ये राहतो + व्हिडिओ विलंब बटण दाखवा + विलंब 0 ms वर रिसेट करण्यासाठी दाबून ठेवा + व्हिडिओ विलंब मेनू दाखवा + मिलीसेकंदात विलंब स्वयंचलित @@ -996,6 +1387,8 @@ OpenRouter.ai उघडण्यासाठी येथे टॅप करा व्हिडिओ गुणवत्ता बटण दर्शवा बटण दाबून धरल्यास (Tap and hold) डीफॉल्ट (Default) गुणवत्तेत रीसेट (Reset) करा + Quality बटणावर रिझोल्यूशन दाखवा + FHD सारख्या लेबलऐवजी 1080 सारखे रिझोल्यूशन दाखवा कस्टम प्लेबॅक गती मेनू जुना प्लेबॅक गती मेनू पुनर्संचयित करा @@ -1012,13 +1405,53 @@ OpenRouter.ai उघडण्यासाठी येथे टॅप करा गती बदल सूचना दर्शवा नवीन डीफॉल्ट प्लेबॅक वेग (playback speed) जतन झाल्यावर सूचना दर्शवते डीफॉल्ट प्लेबॅक गती + Shorts वरील डीफॉल्ट प्लेबॅक स्पीड + Shorts वरील प्लेबॅक स्पीड लक्षात ठेवा + तुमची मॅन्युअल Shorts स्पीड निवड सर्व भविष्यातील Shorts ला लागू होते. जर हे बंद असेल, तर Shorts प्लेअर बंद केल्यानंतर स्पीड पुन्हा डिफॉल्टवर येतो + Shorts मेनूमध्ये प्लेबॅक स्पीड दाखवा + Shorts प्लेयर मेनूमध्ये प्लेबॅक स्पीड आइटम जोडते + प्लेबॅक स्पीड · %s डीफॉल्ट प्लेबॅक स्पीड बदलला: %s संगीतासाठी सामान्य गती वापरा संगीत व्हिडिओ नेहमी 1.0x वेगाने प्ले करा + चॅनेल स्पीड व्हाईटलिस्ट + व्हाईटलिस्ट केलेल्या चॅनेलवरील व्हिडिओ नेहमी 1.0x वेगाने प्ले होतात + स्पीड व्हाईटलिस्टमध्ये चॅनेल जोडा + स्पीड व्हाईटलिस्टमधून चॅनेल काढून टाका + ऑडिओ पिच कंट्रोल्स सक्षम करा + डिफॉल्ट प्लेबॅक ऑडिओ पिच + डिफॉल्ट ऑडिओ पिच बदलून %s केले + ऑडिओ टाइम स्ट्रेचिंग + वेग आणि पिच स्वतंत्रपणे सेट करण्याची परवानगी देते, ज्यामुळे ऑडिओ आर्टिफॅक्ट्स होऊ शकतात + लाईव्ह स्ट्रीम प्लेबॅक स्थिती लक्षात ठेवा + सुरू असलेल्या लाईव्ह स्ट्रीम्ससाठी, हे सध्याच्या लाईव्ह स्थितीवर जाण्याऐवजी तुमची प्लेबॅक स्थिती लक्षात ठेवते आणि तिथूनच पुन्हा सुरू करते + हे वैशिष्ट्य २४/७ लाईव्ह स्ट्रीम्स आणि इतर \'नेहमी सुरू\' असलेल्या स्ट्रीम्ससाठी कार्य करत नाही एचडीआर व्हिडिओ अक्षम करा + Force HDR व्हिडिओ + अॅपला हे डिव्हाइस HDR-सक्षम म्हणून ओळखण्यास भाग पाडते + "जर HDR डिकोडिंग काम करत असेल पण YouTube HDR ऑफर करत नसेल, तर याचा वापर करा, जसे की काही HyperOS डिव्हाइसेस जे HLG लपवतात. + +मर्यादा: +• जर डिस्प्ले प्रत्यक्षात HDR दाखवू शकत नसेल तर रंग चुकीचे दिसू शकतात +• जर डिकोडर HDR हाताळू शकत नसेल तर प्लेबॅक अयशस्वी होऊ शकतो +• HDR अजूनही स्पूफ व्हिडिओ स्ट्रीम्स क्लायंटवर अवलंबून असते" + व्हिडिओ कोडेक + डिफॉल्ट + AVC सक्तीने लागू करण्यासाठी, \'Spoof video streams\' बदलून \'Android Studio\', \'Android VR\' किंवा \'visionOS\' करा + "VP9 +• बॅटरी आयुष्य सुधारू शकते आणि प्लेबॅकमधील अडथळे दूर करू शकते +• AV1 पेक्षा जास्त इंटरनेट डेटा वापरते + +AVC (H.264) +• 'Android Studio', 'Android VR', किंवा 'visionOS' Spoof video stream क्लायंटची आवश्यकता आहे +• बॅटरी आयुष्य अधिक सुधारू शकते +• जुन्या उपकरणांवरील गहाळ झालेले व्हिडिओ रिझोल्यूशन पुन्हा मिळवू शकते +• कमाल रिझोल्यूशन 1080p आहे +• HDR व्हिडिओंसाठी AVC वापरले जाणार नाही +• काही उपकरणे AVC सक्तीने लागू करू शकत नाहीत" प्रगत गुणवत्ता मेनू उघडा गुणवत्ता निवड मेनू वगळून थेट \"Advanced\" गुणवत्ता मेनू उघडतो @@ -1031,21 +1464,193 @@ OpenRouter.ai उघडण्यासाठी येथे टॅप करा \'Android VR\' / visionOS AV1 ला परवानगी द्या \'Android VR\' / visionOS क्लायंट वापरताना सॉफ्टवेअर AV1 डीकोडिंगची परवानगी देते. + "ही सेटिंग सक्षम केल्यामुळे सॉफ्टवेअर AV1 डिकोडिंग वापरले जाऊ शकते. + +AV1 सह व्हिडिओ प्लेबॅक अडखळू शकतो किंवा फ्रेम्स ड्रॉप होऊ शकतात" स्पूफिंगचे दुष्परिणाम + • १ मिनिटाला प्लेबॅक अयशस्वी होऊ शकतो • \'ऑडिओ ट्रॅक\' मेनू गहाळ आहे • AV1 व्हिडिओ कोडेक नाही • 360° VR इमर्सिव्ह मोड उपलब्ध नाही • स्थिर व्हॉल्यूम उपलब्ध नाही + • प्लेबॅक १ सेकंद लवकर संपू शकतो • डीओब्फस्केशन अयशस्वी झाल्यास, प्लेबॅक अयशस्वी होऊ शकते • मूळ ऑडिओ सक्ती उपलब्ध नाही + • Android VR मध्ये साइन इन करणे आवश्यक आहे • गुणवत्ता 720p पर्यंत मर्यादित असू शकते प्लेअर जावास्क्रिप्ट प्रकार \'Stats for nerds\' मध्ये स्पूफिंग दाखवा व्हिडिओ ॲक्शन बार पूर्वीसारखे पुनर्संचयित करा + "व्हिडिओ ॲक्शन बटणे लपवता येत नाहीत हे दुरुस्त करते + +मर्यादा: जुन्या शैलीतील 'More videos' ओव्हरलेवर परत जाते" + प्लेअर आयकॉनची शैली + स्वयंचलित + डिफॉल्ट + पातळ + ठळक + कस्टम + Shorts आयकॉनची शैली + सिरीज ट्रॅकर + प्लेलिस्ट्सना सिरीज म्हणून फॉलो करा आणि तुमचे स्थान लक्षात ठेवा + सिरीज ट्रॅकिंग सक्षम करा + हिस्ट्रीमध्ये सिरीज पेज आणि प्लेलिस्टमध्ये सिरीज फॉलो करण्याचा पर्याय जोडते + नेव्हिगेशन बारमध्ये \'सिरीज\' दाखवा + सिरीज ट्रॅकर बटण दाखवा + सध्याच्या व्हिडिओची सिरीज फॉलो करण्यासाठी किंवा उघडण्यासाठी प्लेयर बटण जोडते + फॉलो केलेल्या सिरीजची प्रगती रेकॉर्ड करा + सेव्ह केलेल्या सिरीज आणि बुकमार्क्ससाठी या फोनवर प्लेबॅक रेकॉर्ड करा + रद्द करा + सेव्ह करा + बुकमार्क सेव्ह करा + सिरीजचे नाव + सिरीज पर्याय + लायब्ररी पर्याय + एपिसोड्स रिफ्रेश करा + नाव बदला + YouTube मध्ये प्लेलिस्ट उघडा + लायब्ररीमधून काढून टाका + प्लेलिस्ट लिंक किंवा ID + सर्व स्थानिक प्रगती पुसा + इतिहास पुसा + सर्व स्थानिक प्रगती पुसायची का? + हे प्रत्येक व्हिडिओसाठी पोझिशन्स, वॉचड ओव्हरराइड्स आणि रिझ्युम सिलेक्शन्स क्लियर करते. सेव्ह केलेले सिरीज आणि त्यांचे कॅटलॉग राहतात. जर रेकॉर्डिंग सक्षम असेल तर नवीन प्लेबॅक रेकॉर्ड केला जाईल + पाहिले म्हणून मार्क करा + न पाहिलेले म्हणून मार्क करा + स्वयंचलित स्थिती वापरा + 0:00 पासून पुन्हा सुरू करा + 0:00 पासून पुन्हा प्ले करा + स्थिती जतन केली + जतन करण्याचा पुन्हा प्रयत्न करा + मागील १०० + पुढील १०० + जतन केलेला व्हिडिओ उघडा + स्थानिक प्रगती पुसली + तुमच्या सिरीज पाहण्यासाठी \'You\' मधून \'History\' उघडा + वॉच हिस्ट्री + सिरीज + सेव्ह केलेल्या सिरीज लोड होत आहेत… + एपिसोड्स ट्रॅक करण्यासाठी प्लेलिस्ट फॉलो करा + प्लेलिस्ट बुकमार्क + एपिसोड्स रिफ्रेश होत आहेत… + सर्व सिरीज + पाहिलेल्या + एपिसोड्स लोड होत आहेत… + अधिक पर्याय + "केवळ तुमच्या सेव्ह केलेल्या सिरीज आणि बुकमार्क केलेल्या व्हिडिओंसाठी प्रोग्रेस रेकॉर्ड करा. + +ही लायब्ररी या डिव्हाइसवर स्टोअर केली जाते आणि YouTube खात्यांमध्ये शेअर केली जाते. YouTube चे पॉज आणि क्लियर हिस्ट्री कंट्रोल्स यामध्ये बदल करत नाहीत. ते हटवण्यासाठी Series → More → Clear local progress वापरा. + +इन्कॉग्निटोमध्ये, जेव्हा खात्याची ओळख उपलब्ध नसते, किंवा जेव्हा तुम्ही दुसऱ्या खात्यावर स्विच करता तेव्हा ऑटोमॅटिक रेकॉर्डिंग थांबते. त्या खात्यासोबत याचा वापर करण्यासाठी ट्रॅकिंग पुन्हा सक्षम करा" + इन्कग्निटो मोडमध्ये किंवा YouTube ने सध्याची खाते स्थिती लोड करण्यापूर्वी ट्रॅकिंग सक्षम करता येणार नाही. होमवर परत जा आणि पुन्हा प्रयत्न करा. साइन-इनची आवश्यकता नाही + लोकल ट्रॅकिंग सक्षम करा + प्रगतीची नोंदणी बंद आहे + %1$s ला पुन्हा सुरू करा + %1$s / %2$s भाग पाहिले + भाग %1$s · %2$s + भाग %1$s-%2$s / एकूण %3$s + भाग लोड करण्याचा प्रयत्न करण्यासाठी रिफ्रेश करा. खाजगी प्लेलिस्टसाठी खात्याच्या प्रवेशाची आवश्यकता आहे; त्यांचे बुकमार्क अजूनही YouTube मध्ये उघडतात + YouTube हा भाग उघडू शकला नाही + YouTube ही प्लेलिस्ट उघडू शकला नाही + एक भाग निवडा + अद्ययावत + प्ले करण्यायोग्य भाग नाहीत + सेव्ह केलेला व्हिडिओ · %1$s + मालिका फॉलो करा + मालिका पहा + चालू ठेवा + येथून सुरुवात करा + सुरुवातीचा बिंदू सेव्ह केला + इथपर्यंत पाहिले + पाहिले म्हणून मार्क केलेले भाग + रद्द करा + सक्षम करा + %1$s नवीन भाग + या डिव्हाइसवर प्रगती नोंदवा + "तुमची मालिका आणि प्रगती या डिव्हाइसवर राहतात, ज्या YouTube खात्यांमध्ये सामायिक केल्या जातात. YouTube इतिहास नियंत्रणे त्या बदलत नाहीत. सिरीज मेनूमधून प्रगती क्लियर करा. + +स्वयंचलित रेकॉर्डिंग ऐच्छिक आहे. इनकॉग्निटोमध्ये किंवा खाते बदलल्यावर ते थांबते" + एपिसोड लोड होऊ शकले नाहीत. तुम्ही प्लेलिस्ट बुकमार्क म्हणून जतन करू शकता + खूप जास्त न जतन केलेले बदल आहेत. प्रथम जतन करण्याचा पुन्हा प्रयत्न करा + सेव्हिंगची प्रक्रिया सुरू आहे. पुन्हा प्रयत्न करा + एपिसोड लोड होण्याची प्रक्रिया सुरू आहे. पुन्हा प्रयत्न करा + स्टोरेज उपलब्ध नाही. तुमचा जतन केलेला डेटा सुरक्षित ठेवला आहे + बदल जतन होऊ शकले नाहीत. YouTube बंद करण्यापूर्वी जतन करण्याचा पुन्हा प्रयत्न करा + ही सिरीज आधीच जतन केली आहे, किंवा तिची प्लेलिस्ट बदलली आहे. ती पुन्हा उघडा + ही मालिका बदलली आहे किंवा काढून टाकली आहे. लायब्ररी पुन्हा उघडा + प्लेलिस्ट बदलली आहे. पुन्हा एपिसोड निवडा + हा एपिसोड उपलब्ध नाही. दुसरा निवडा + मालिकेचे नाव प्रविष्ट करा + YouTube प्लेलिस्ट लिंक किंवा प्लेलिस्ट ID प्रविष्ट करा + पुन्हा रिफ्रेश करण्यापूर्वी कृपया प्रतीक्षा करा + एपिसोड लोड होऊ शकले नाहीत. कृपया नंतर पुन्हा प्रयत्न करा + बदल पूर्ण होऊ शकला नाही. पुन्हा प्रयत्न करा + रिझ्युमची पुष्टी होऊ शकली नाही. प्लेयर तपासा आणि पुन्हा प्रयत्न करा + फॉलो केलेले सिरीज + हा व्हिडिओ या प्लेलिस्टमध्ये नाही + प्लेलिस्ट शोधत आहे… + प्लेलिस्ट लिंक पेस्ट करा + प्लेलिस्ट मिळू शकली नाही + कोणतेही भाग उपलब्ध नाहीत + पाहिलेले लपवा + मागील भाग पाहिलेले म्हणून चिन्हांकित करा + याने सुरू होते: %1$s + भागांचा क्रम + प्लेलिस्टचा क्रम + प्लेलिस्टचा क्रम उलट करा + %1$s व्ह्यूज + %1$s व्ह्यू + भाग पाहिलेले म्हणून चिन्हांकित करा + कमीत कमी %1$s%% पाहिलेले आणि जास्तीत जास्त ९० सेकंद शिल्लक, किंवा %2$s सेकंद उर्वरित + दोन्हीपैकी कोणत्याही मर्यादेनंतर एपिसोड आपोआप \'पाहिलेला\' म्हणून मार्क करा. टक्केवारीच्या नियमासाठी ९० सेकंदांपेक्षा जास्त वेळ शिल्लक नसावा लागतो, जेणेकरून लांब एपिसोडचा शेवट वगळला जाणार नाही. उर्वरित-वेळेचा नियम एक मिनिटापेक्षा जास्त लांबीच्या व्हिडिओंना लागू होतो; तो अक्षम करण्यासाठी ० सेट करा. बदल भविष्यातील प्रगती अपडेट्सना लागू होतील. मॅन्युअल पाहिलेले/न पाहिलेले पर्याय तसेच ठेवले जातात + पाहिलेली टक्केवारी (१-१००) + उर्वरित सेकंद (०-३००; ० अक्षम करते) + %1$s ते %2$s दरम्यान मूल्य प्रविष्ट करा + YouTube प्रोग्रेस वापरा + दुसऱ्या डिव्हाइसवर पाहिल्यानंतर फॉलो केलेल्या सिरीज तिथूनच सुरू करा + तुमच्या फॉलो केलेल्या सिरीजसाठी सध्याच्या YouTube खात्यातून पाहिलेले स्टेटस आणि रिझ्युम पोझिशन्स वाचा. फक्त जुळणारे एपिसोड या फोनवर सेव्ह केले जातात. सिरीज लायब्ररी उघडलेले असताना आणि \'Continue\' करण्यापूर्वी तपासणी केली जाते. तुमच्या दुसऱ्या डिव्हाइसवर YouTube हिस्ट्री सक्षम असणे आवश्यक आहे + YouTube प्रोग्रेस तपासत आहे… + YouTube प्रोग्रेस उपलब्ध नाही. सेव्ह केलेली प्रोग्रेस वापरत आहे + काही YouTube प्रोग्रेस उपलब्ध नाही. सेव्ह केलेली पोझिशन्स ठेवल्या आहेत + बॅकअप आणि रिस्टोर + तुमची सिरीज लायब्ररी फाईलमध्ये सेव्ह करा किंवा बॅकअप रिस्टोर करा + फॉलो केलेल्या सिरीज, एपिसोड पोझिशन्स आणि वॉच्ड चॉइसेस सेव्ह करते. रेकॉर्डिंग आणि YouTube प्रोग्रेस परवानग्यांचा समावेश नाही + बॅकअप सेव्ह करा + बॅकअप रिस्टोर करा + %1$s सिरीज जोडा. तुमच्या लायब्ररीमधील %2$s सिरीज ठेवल्या आहेत. अस्तित्वात असलेली व्हिडिओ प्रोग्रेस ठेवली आहे + सिरीज बॅकअप सेव्ह झाला + %1$s सिरीज रिस्टोर केल्या + ही फाईल सपोर्टेड सिरीज बॅकअप नाही, किंवा खूप मोठी आहे. तुमच्या लायब्ररीमध्ये कोणताही बदल करण्यात आला नाही + बॅकअप सेव्ह करता आला नाही. दुसरे लोकेशन वापरून पहा + सिरीज हटवली + हा एपिसोड आता एकापेक्षा जास्त वेळा दिसत आहे. कुठून पुढे सुरू करायचे ते निवडा + तुमचा सेव्ह केलेला एपिसोड आता या प्लेलिस्टमध्ये नाही. तुमची सेव्ह केलेली स्थिती जपली आहे + एपिसोड %1$s · %2$s सह सुरू ठेवा + तुम्ही खालीलपैकी कोणताही एपिसोड निवडू शकता. आधीचे एपिसोड बदलले नाहीत + ही प्लेलिस्ट बदलली आहे. कोठून सुरू करायचे हे निवडण्यासाठी सिरीज उघडा + या दिशेने आता कोणतेही एपिसोड्स नाहीत + %1$s सुरू ठेवा + मालिका शोधा + शोध हटवा + कोणतीही मालिका सापडली नाही + शीर्षक नसलेला भाग + अनुपलब्ध भाग + Niconico टिप्पण्या + लाईव्ह चॅट आणि चॅट रिप्ले व्हिडिओवरून स्क्रोल होणाऱ्या टिप्पण्या म्हणून दाखवा + व्हिडिओ प्लेयरमध्ये एक ओव्हरले बटण दाखवते. टिप्पण्या दाखवण्यासाठी किंवा लपवण्यासाठी टॅप करा, क्विक सेटिंग्ससाठी टॅप करून धरा + टिप्पणी फॉन्ट आकार + टिप्पण्यांचा मजकूर आकार (12-40 sp) + टिप्पणी अपारदर्शकता + टिप्पणी मजकूर आणि पार्श्वभूमीची अपारदर्शकता + प्रवासाचा कालावधी + टिप्पण्यांना स्क्रीन ओलांडण्यासाठी किती सेकंद लागतात. कमी मूल्ये अधिक वेगाने हलतात + कमेंटची कमाल लांबी + यापेक्षा जास्त अक्षरे असलेल्या कमेंट्स लपवते. प्रत्येक इमोजी एक अक्षर मानला जातो + कोणतीही मर्यादा नाही diff --git a/patches/src/main/resources/addresources/values-ms-rMY/music/strings.xml b/patches/src/main/resources/addresources/values-ms-rMY/music/strings.xml index 427688f919..a199457ad6 100644 --- a/patches/src/main/resources/addresources/values-ms-rMY/music/strings.xml +++ b/patches/src/main/resources/addresources/values-ms-rMY/music/strings.xml @@ -17,19 +17,157 @@ Second \"item\" text" --> + Terakhir dimainkan Langganan + Video + Keputusan lain + Lagu yang dimuat naik + Album yang dimuat naik + Artis yang dimuat naik + Kemas kini tampalan Jam hos untuk menggunakan jeda dan sambung semula + Perkongsian giliran Jam + Hos atau sertai Jam berdekatan dan edit gilirannya melalui Lapisan Jam + Dayakan perkongsian giliran Jam + Mengaktifkan integrasi Jam selepas YouTube Music dimulakan semula + Muat turun Lapisan Jam untuk mula berfungsi + Buka keluaran Jam Layer terkini dalam pelayar anda + Kawalan Jam + Hos atau sertai Jam berdekatan dan uruskan sesi tersebut + Pakej Jam Layer + Pilih aplikasi pendamping yang dipasang untuk perkongsian giliran berdekatan + Gunakan pakej pendamping yang dipasang. Pemadanan memerlukan kelulusan anda dan token keupayaan tempatan + Hidupkan Wi-Fi? + Hidupkan Wi-Fi, kemudian cuba mulakan atau sertai Jam sekali lagi + Buka tetapan Wi-Fi + Batal + Gunakan pakej + Masukkan nama pakej Android yang sah + Pakej disimpan. Sediakan Jam Layer untuk menyambung + Jam terputus + Dayakan Jam Layer terlebih dahulu + Dayakan perkongsian giliran Jam dalam tetapan, kemudian mulakan semula YouTube Music + Pasang pakej Lapisan Jam yang dipilih terlebih dahulu + Masukkan kod yang dipaparkan pada hos. Wi-Fi Aware boleh menyambungkan peranti berdekatan walaupun LAN mereka berbeza + Sertai dengan kod + Sertai + Imbas QR + Masukkan kod lapan aksara atau tampal jemputan + Sertai Jam anda + Salin kod + Kod Jam + Tunjuk QR + Selesai + Jemput ke Jam anda + Imbas untuk menyertai dan mengedit barisan ini. Menamatkan Jam akan membatalkan jemputan + Salin pautan + Jemputan Jam + Jam sedang menyambung semula; cuba lagi sebentar lagi + Ditambah ke Jam + Jam tidak tersedia; trek tidak ditambah + Jam anda + Mendengar bersama + Menyertai Jam + Dengar bersama + Mengemas kini Jam… + Mencari hos anda pada Wi-Fi… + Menunggu orang + %s orang disambungkan + %s orang disambungkan + Masukkan kod ini pada peranti berdekatan. Wi-Fi Aware berfungsi merentasi LAN berasingan jika disokong. Tamat dalam %s minit + Cari ke %s? + Suntingan dibuka + Suntingan dikunci + Tersambung melalui Wi-Fi Aware + Tersambung melalui Wi-Fi + Tersambung melalui Bluetooth + Tidak disokong semasa Jam + Penyertaan dibatalkan + Menyambung semula ke hos anda… + Kongsi giliran. Benarkan semua orang menambah lagu + Jemput orang + Tetamu boleh edit giliran + Tamatkan Jam + Hos memainkan muzik. Tambah, pindahkan atau buang lagu terus dalam giliran YouTube Music + Pastikan kedua-dua peranti berada berdekatan. Anda boleh membatalkan sementara kami mencari hos + Batal sertai + Tinggalkan Jam + Mulakan Jam + Sediakan Lapisan Jam + Urus + Butiran + Buka + Memuatkan pilihan lagu… + Opsyen lagu tidak tersedia; cuba lagi + Tidak dapat membuka opsyen lagu + Tiada pelayar tersedia untuk memuat turun Jam Layer + Menunggu giliran hos + Buka lagu dalam YouTube Music terlebih dahulu + Mainkan pada hos + Keluar daripada Jam dan mainkan secara tempatan + Mainkan seterusnya dalam Jam + Tambah ke giliran Jam + Mainkan trek Jam ini + Pilih tempat untuk memainkan + Pindahkan hos ke sini + Lagu tidak bersedia untuk mencari + Giliran telah berubah; cuba semula gerak isyarat tersebut Logo pengepala Lalai Tersuai + Mainkan trek audio album + Mainkan versi audio album sahaja untuk lagu berbanding video muzik. Memerlukan \'Spoof strim video\' + Dayakan suis audio/video + Cuba tukar Versi Trek Audio dan Video Muzik Rasmi + Muat turun video muzik, podcast, atau lagu dengan aplikasi muat turun pihak ketiga + Gunakan pemuat turun luaran untuk menu Muat turun + Pilihan Muat turun pada menu akan membuka pemuat turun luaran anda + Muat turun + "Tiada muat turun +Jejak yang dimuat turun akan muncul di sini" + %1$s • %2$s • %3$s MB + %1$s trek • %2$s MB + Cari muat turun Isih + Isih mengikut artis + Isih mengikut tajuk + Isih mengikut saiz + Padam semua muat turun + Padam semua muat turun? + Semua trek yang dimuat turun akan dipadamkan daripada peranti ini + Muat turun dipadamkan + Tiada apa yang dijumpai + Tindakan untuk %s + Main + Main atau jeda + Sebelumnya + Seterusnya + Jeda + Tutup + Padam + Padam muat turun + Padam muat turun? + %s akan dipadamkan daripada peranti ini + Muat turun dipadam + Tidak dapat memadam muat turun + Tidak dapat memainkan muat turun + Muat turun audio + Mainan luar talian + Tidak dapat mengenal pasti trek untuk dimuat turun + Muat turun sudah dalam proses + Sudah dimuat turun + Muat turun bermula + Memulakan muat turun… + Memuat turun %s%% + Muat turun selesai + Muat turun gagal Mengenai @@ -37,12 +175,39 @@ Second \"item\" text" Suapan Umum Pemain + Lirik Lain-lain + Bahasa YouTube Music + Bahasa sistem + Memerlukan Android 13 atau yang lebih baharu Sorokkan label \'Dapatkan Muzik Premium\' + Sorokkan promosi Music Premium Sorokkan iklan video + Sorokkan suis Audio / Video + Sorokkan butang Muat Turun + Menyembunyikan butang Muat Turun dalam pertunjukan podcast + Sorokkan rak Teroka + Sorokkan bar penapis + Sorokkan rak grid Sorokkan laci mengufuk + Sorokkan laci senarai + Panel lirik + Sorokkan butang Kongsi + Sorokkan butang Terjemah + Sorokkan laci \'Baharu daripada\' + Sorokkan laci senarai main + Sorokkan butang Ulang + Sorokkan butang Rawak + Sorokkan laci \'Dail pantas\' + Komen + Pilih komponen yang akan muncul dalam bahagian komen + Sorokkan Garis Panduan Komuniti + Sorokkan mesej konteks + Sorokkan butang Emoji + Sorokkan butang Info + Sorokkan butang Cap Masa Pudar silang Mengadunkan runut di antara satu sama lain dengan lancar @@ -80,24 +245,110 @@ Second \"item\" text" Pudar silang dijedakan untuk sesi ini Pudar silang disambung semula Mod video tidak tersedia semasa pudar silang didayakan + Crossfade dimatikan dengan mainan semula SABR + Crossfade dimatikan semasa casting Bagaimana pudar silang berfungsi + "Crossfade bertindih hujung satu lagu dengan permulaan lagu seterusnya, menggunakan tempoh dan lengkung anda + +Ia berjalan pada kemajuan automatik dan pada langkau manual, masing-masing dengan suis sendiri. Pada kemajuan automatik, adunan bermula beberapa saat sebelum lagu berakhir" + Had + "Video muzik dimainkan sebagai audio sahaja apabila crossfade dihidupkan. Tekan lama shuffle untuk menjeda crossfade dan menonton video + +Video muzik yang memulakan mainan kekal dalam mod video sehingga lagu seterusnya + +Lagu yang lebih pendek daripada tempoh fade tidak akan mengalami crossfade + +Skrin kunci atau pemberitahuan mungkin menunjukkan keadaan lama sebentar semasa peralihan + +Menjeda crossfade untuk menonton video muzik boleh menyebabkan beberapa video menjadi hitam. Menjeda crossfade sebelum anda memilih lagu dapat mengelakkannya" + Tidak disokong + "Casting atau mirroring audio (Chromecast, mirroring skrin atau audio) + +Mainan SABR (Aliran video spoof dimatikan)" Kredit Ciri pudar silang oleh VazerOG + Ingat keadaan ulangan + Ingat status rawak + Kelajuan main balik + Kelajuan yang digunakan untuk semua trek. Pada 100%, kelajuan aplikasi digunakan, seperti kelajuan main balik podcast + Tukar pic dengan kelajuan + Nyahaktifkan penghalaan tidak suka + Menghalang peralihan ke trek seterusnya apabila butang tidak suka ditekan + Tindakan ketik trek senarai main + Mainkan senarai main + Mainkan trek yang dipilih sahaja + Tambah trek yang dipilih ke dalam barisan + Ditambah ke barisan + Sorokkan butang siaran + Sorokkan butang sejarah + Sorokkan butang pemberitahuan + Sorokkan butang Carian + Sorokkan butang carian Suara + Sorokkan butang carian Bunyi + Sorokkan butang Baru + Menyembunyikan butang Baharu dalam tab Perpustakaan + Butang tindakan + Pilih butang tindakan yang muncul di bawah pemain + Sorokkan bar tindakan + Sorokkan butang Suka dan Tidak Suka + Sorokkan butang Komen + Sorokkan butang \'Main semula sembang langsung\' + Sorokkan butang Lirik + Sorokkan butang Kongsi + Sorokkan butang Simpan + Sorokkan butang Muat turun + Sorokkan butang \'Mulakan radio\' + Menu flyout + Pilih komponen yang muncul dalam menu flyout pemain dan giliran + Sembunyikan komponen 3 lajur + Sembunyikan butang Suka dan Tidak Suka + Sembunyikan menu \'Padanan cita rasa\' + Sembunyikan menu \'Tambah ke senarai main \"Dengar kemudian\"\' + Sorokkan menu \'Tambah ke giliran\' + Sembunyikan menu Sari Kata + Sorokkan menu \'Padam senarai main\' + Sorokkan menu \'Hentikan giliran\' + Sorokkan menu \'Jangan syorkan artis ini\' + Sembunyikan menu Muat Turun + Sorokkan menu \'Edit senarai main\' + Sorokkan menu \'Pergi ke album\' + Sorokkan menu \'Pergi ke artis\' + Sorokkan menu \'Pergi ke episod\' + Sorokkan menu \'Pergi ke podcast\' + Sembunyikan menu Bantuan + Sorokkan menu \'Tanda episod sebagai dimainkan\' + Sorokkan menu \'Tidak berminat\' + Sembunyikan menu \'Sematkan ke dail pantas\' + Sembunyikan menu \'Mainkan seterusnya\' + Sembunyikan menu Kualiti + Sembunyikan menu \'Alih keluar daripada pustaka\' + Sembunyikan menu \'Alih keluar daripada senarai main\' + Sembunyikan menu Lapor + Sembunyikan menu \'Simpan episod untuk kemudian\' / \'Simpan ke pustaka\' + Sembunyikan menu \'Simpan ke senarai main\' + Sembunyikan menu Kongsi + Sembunyikan menu \'Main rawak\' + Sembunyikan menu \'Pemasa tidur\' + Sembunyikan menu \'Mulakan radio\' + Sembunyikan menu \'Statistik untuk pakar\' + Sembunyikan menu Langgan + Sembunyikan menu \'Nyahpin daripada dail pantas\' + Sembunyikan menu \'Lihat kredit lagu\' Laman mula Lalai @@ -111,9 +362,13 @@ Second \"item\" text" Carian Langganan + Paksa orientasi potret + Mengekalkan aplikasi dalam orientasi potret apabila peranti dipusingkan Tukar warna pemain mini Padankan warna pemain mini dengan pemain skrin penuh + Tukar warna bar navigasi + Padankan warna bar navigasi dengan pemain skrin penuh Tunjukkan butang seterusnya dalam pemain mini Tunjukkan butang sebelumnya dalam pemain mini @@ -139,20 +394,298 @@ Second \"item\" text" Sorokkan bar pandu arah Sorokkan label butang pandu arah + Tidak log masuk. Ketik untuk konfigurasikan + Log keluar Telah log keluar Aktifkan scrobbling + Hantar status \'Sedang Dimainkan\' + Tempoh minimum lagu + Lagu yang lebih pendek daripada ini tidak akan discrobble + Peratusan kelewatan scrobble + Cetuskan scrobble selepas mendengar peratusan lagu ini + Saat kelewatan scrobble + Cetuskan scrobble selepas mendengar selama saat ini + Akaun ListenBrainz + Telah log masuk. Ketik untuk urus akaun + Masukkan Token Pengguna ListenBrainz anda untuk mengaktifkan scrobbling + Token + Sahkan + Sila masukkan token + Mengesahkan… + Token sah! Pengguna: %s + Token tidak sah: %s tidak diketahui + Pengesahan gagal: %s + Token dikosongkan + Token disimpan + Amaran: Token yang disimpan tidak sah! + Love trek yang disukai + Love trek secara automatik di Last.fm apabila anda menyukainya di YouTube Music (dan buang love apabila tidak disukai atau tidak digemari) + Akaun Last.fm + Log masuk sebagai %s. Ketik untuk urus akaun + Masukkan kredential Last.fm anda untuk log masuk + Nama pengguna + Kata laluan + Log masuk + Sila masukkan nama pengguna dan kata laluan + Log masuk… + Log masuk berjaya! + Gagal mendapatkan sesi daripada Last.fm + Log masuk gagal: %s + Kredential Last.fm disimpan! + Pembersihan metadata + Bersihkan metadata + Bersihkan serabut biasa secara automatik (seperti \"Official Video\", \"Remaster\", \"Live\") daripada tajuk, artis, dan album sebelum scrobbling + Pembersihan regex tersuai + Masukkan ungkapan nalar tersuai untuk membuang teks tambahan daripada tajuk lagu, artis, atau album. Contoh: (?i)\\s*\\((clean|explicit)\\) + Huraikan artis dan trek daripada tajuk + Ekstrak nama artis dan trek daripada tajuk video menggunakan pemisah \" - \". Berguna untuk kandungan yang dimuat naik semula di mana nama saluran mungkin digunakan sebagai artis + Isi album yang hilang + Apabila muzik tidak menyertakan album, cari di Last.fm supaya scrobble anda lengkap + "Scrobbling menyinkronkan penggunaan YouTube Music anda secara automatik dengan profil Last.fm atau ListenBrainz anda + +Untuk memastikan scrobbling disinkronkan semasa memainkan latar belakang, pastikan penggunaan bateri aplikasi YouTube Music ditetapkan kepada \"Tanpa Had\"" + Gunakan lirik pihak ketiga + Menunjukkan lirik daripada perkhidmatan pihak ketiga, kembali ke lirik terbina dalam apabila tiada yang ditemui + Kekalkan skrin aktif semasa melihat lirik + Menghalang paparan daripada terpadam semasa panel lirik dibuka + Perkhidmatan lirik + Paparan + Penyinkronan + Panel + Titik akhir AI + Semua pembekal dinyahaktifkan - lirik terbina dalam dan lirik terbenam + Set semula urutan pembekal? + Pulihkan pembekal ke urutan keutamaan lalai + Kosongkan lirik tersimpan + Padam lirik yang disimpan pada peranti ini + %1$s MB telah dikosongkan + Dapatkan token + Token-pengguna-media Apple Music + "Tampalkan kuki media-user-token daripada music.apple.com + +Langkah: +1. Log masuk di music.apple.com sebagai pengguna ahli +2. Buka DevTools → Application → Cookies → music.apple.com +3. Salin nilai \"media-user-token\"" + Apple Music: media-user-token disimpan + Apple Music: media-user-token dikosongkan + Apple Music: media-user-token tidak sah - sila semak dan cuba lagi + Spotify SpDc + "Tampal kuki sp_dc daripada Spotify. + +Langkah: +1. Log masuk di open.spotify.com +2. Buka DevTools → Application → Cookies → open.spotify.com +3. Salin nilai \"sp_dc\"" + Spotify: sp_dc disimpan + Spotify: sp_dc dikosongkan + Spotify: sp_dc tidak sah - sila semak dan cuba lagi + Token Apple Music adalah pilihan. Tanpanya, Lyrically akan digunakan sebagai sandaran + Deezer ARL + "Tampal kuki arl daripada Deezer. + +Langkah: +1. Log masuk di deezer.com +2. Buka DevTools → Application → Cookies → deezer.com +3. Salin nilai \"arl\"" + Deezer: arl disimpan + Deezer: arl dikosongkan + Deezer: arl tidak sah - sila semak dan cuba lagi + Token Musixmatch + Tambah token hanya untuk menggunakan akaun anda sendiri. Ia mestilah datang daripada aplikasi Android Musixmatch: token yang disalin daripada musixmatch.com adalah milik klien desktop yang telah bersara dan API menolaknya + Musixmatch berfungsi tanpa token, jadi medan ini boleh dibiarkan kosong + token pengguna + Musixmatch: token pengguna disimpan + Musixmatch: token pengguna dikosongkan + Musixmatch: token pengguna tidak sah - sila semak dan cuba lagi + Kuki YouTube + "Tampal kuki YouTube anda untuk menyemak kapsyen dan teks masa daripada YouTube. + +Langkah: +1. Buka youtube.com dalam pelayar dan log masuk +2. Buka DevTools (F12) → Application → Cookies → youtube.com +3. Salin rentetan kuki penuh (pasangan kunci=nilai yang dipisahkan oleh titik koma) + +Kuki yang diperlukan: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPISID, LOGIN_INFO" + YouTube: kuki disimpan + YouTube: kuki dikosongkan + YouTube: kuki tidak sah - sila semak dan cuba lagi + Ketik dan tahan baris + Ketik baris untuk melompat ke saat tersebut dalam lagu, tekan lama untuk menyalin perkataan di bawah jari anda, atau keseluruhan baris apabila tekanan bukan pada perkataan. \'Mati\' akan membuang kedua-dua isyarat tersebut + Tunjukkan tag bahagian + Labelkan setiap bahagian baharu lirik dengan tag kecil, seperti Rangkap atau Korus + Tunjukkan butang Salin + Ketik untuk menyalin lirik asal sebagai teks biasa; tekan lama untuk menyimpan fail sumber lirik mentah ke Muat Turun + Tunjukkan butang Terjemah + Ketik untuk menukar terjemahan; tekan lama untuk terjemahan sahaja. Menggunakan terjemahan penyedia, Google Translate, atau API yang serasi dengan OpenAI + Tunjukkan butang Romanize + Ketik untuk menukar romanisasi; tekan lama untuk romanisasi sahaja. Menggunakan romanisasi penyedia, Google Translate, atau API yang serasi dengan OpenAI + Romanisasi dan Penjajaran Tempatan + Romanisasi pada peranti apabila pembekal, model anda dan Google Translate gagal. Bacaan mengikuti setiap perkataan lirik mengikut masa; penyinkronan perkataan mesti dihidupkan. Cina, Kantonis, Jepun, Korea, Greek, Rusia, Georgia, Armenia; kira-kira 120 MB + Membaca %1$s: %2$d%% + Bacaan sedia (%1$d entri) + Muat turun gagal - matikan dan hidupkan semula pilihan ini untuk mencuba lagi + Tunjukkan butang Segarkan + Ketik untuk tukar ke calon lirik seterusnya; tekan lama untuk cari lirik mengikut tajuk lagu dan artis + Cari lirik + Tajuk lagu + Artis + Masukkan tajuk lagu dan artis + Tiada penyedia yang didayakan boleh mencari lagu lain + Disimpan + Serlahkan perkataan atau suku kata yang disinkronkan + Utamakan lirik perkataan demi perkataan jika tersedia. Apabila lirik membawa pemasaan perkataan, ia akan menyerlahkan setiap perkataan atau suku kata mengikut masa muzik + Sembunyikan lirik yang telah dimainkan + Sembunyikan baris lirik di atas baris yang sedang dimainkan + Sembunyikan lirik yang belum dimainkan + Sembunyikan baris lirik di bawah baris yang sedang dimainkan + Tunjukkan Lirik MediaSession pada skrin kunci + Menunjukkan baris semasa pada skrin kunci, Android Auto dan Bluetooth. Memerlukan Android 11 atau lebih baharu + Tunjukkan Lirik miniplayer + Menunjukkan baris semasa dalam pemain mini, dengan trek dipindahkan ke sari kata + Format tajuk lagu/artis + Format trek yang dipaparkan bersama lirik dalam miniplayer dan MediaSession. {t} ialah tajuk, {a} ialah artis dan {v} ialah versi, seperti Akustik, apabila sumber lirik menyediakannya. Teks antara dua titik bertindih hanya dipaparkan apabila setiap pemboleh ubah di dalamnya mempunyai nilai; tulis \\: untuk titik bertindih biasa. Setiap baris dipaparkan secara bergilir, bertukar apabila lirik berubah + Format baris lirik + Format baris lirik dalam miniplayer dan MediaSession. {lrc} ialah baris dan {n} ialah berapa kali ia dinyanyikan berturut-turut. Teks antara titik bertindih hanya dipaparkan apabila baris berulang. Gunakan \\: untuk titik bertindih + Format mestilah mengandungi {lrc} + Templat baris artis dan pemasaan animasi + Masa skrin minimum (ms) + Masa skrin maksimum (ms) + Untuk animasi pelbagai baris sahaja. Kekalkan setiap bingkai selama sekurang-kurangnya masa minimum. Pada tahap maksimum, majukan baris artis walaupun lirik kekal sama. + Minimum mestilah 0 atau lebih besar. Maksimum mestilah lebih besar daripada 0 dan sekurang-kurangnya nilai minimum. + Had panjang baris + Had baris lirik dan artis, serta pemberat aksara lebar penuh + "Had baris lirik +Bahagikan lirik yang lebih panjang kepada dua bahagian yang seimbang. 0 menyahaktifkan pembahagian." + "Had baris artis +Bahagikan format trek yang diisi kepada bingkai kitaran. 0 menyahaktifkan pembahagian." + "Berat aksara lebar penuh +Terpakai untuk kedua-dua had. Pada 1.75, empat aksara lebar penuh dikira sebagai 7." + Masukkan had nombor bulat dari 0 hingga 10000 dan berat aksara yang lebih besar daripada 0 + Hentian instrumental + Dipaparkan dalam Miniplayer dan MediaSession semasa rehat instrumental. Setiap baris ialah satu bingkai animasi. {t} ialah tajuk, {a} ialah artis dan {lrc} ialah baris lirik terakhir + Imbasan masa Miniplayer dan MediaSession + Anjakan apabila baris seterusnya dipaparkan dalam Miniplayer dan MediaSession. Gunakan nilai negatif jika baris muncul lewat, contohnya pada skrin kunci + Penapis + Regex metadata + Ekspresi nalar yang digunakan pada tajuk, artis, dan album sebelum mencari lirik dan semasa menentukan sama ada dua tajuk sepadan. Biarkan kosong untuk menyahaktifkan kedua-duanya. Tetapan lalai membuang gangguan biasa seperti \"Official Video\", \"Remaster\", \"【插曲】\", dan \"- Topic\" + Regex penapis lirik + Ekspresi nalar yang digunakan pada teks lirik asal sahaja (bukan terjemahan atau romanisasi). Biarkan kosong untuk menyahaktifkan + Penanda baris info + Jika lirik ditapis secara tidak betul ke bahagian bawah, laraskan atau kosongkan ini. Penanda dipisahkan dengan koma: baris yang mengandungi penanda diikuti oleh titik bertindih akan dipaparkan sebagai maklumat kredit di bahagian bawah. Biarkan kosong untuk menyahaktifkan + Kapitalkan huruf pertama + Mengapitalkan huruf pertama bagi setiap baris lirik + Alihkan tanda baca di hujung + Mengalihkan titik, koma, kolon dan titik koma pada akhir baris lirik. Tanda soal, tanda seru, elipsis, petikan dan kurungan dikekalkan + Saiz teks + Saiz teks lirik + Imbasan masa global + Lirik yang disinkronkan mengikut mainan semula. Jika ia terpesong, laraskan offset pemasaan. Anjakkan lirik yang disinkronkan ke hadapan atau ke belakang. Gunakan nilai positif jika lirik berjalan lebih awal daripada lagu + Gunakan segmen SponsorBlock untuk imbasan lirik + Padankan tanpa segmen yang dilangkau oleh SponsorBlock dan anjakkan lirik ke audio yang dikeluarkan. Matikan untuk mengabaikan SponsorBlock sepenuhnya + Trek ini tidak mempunyai id video untuk dihantar + Sumber: %s + Tekan lama butang salin untuk menyimpan LRC ini; jika anda boleh memperhalusi pemasaan dan berkongsinya dengan pembekal komuniti, kami akan sangat menghargainya + Diterjemah oleh Google + Diromankan oleh Google + Diterjemah oleh %s + Diromanisasikan oleh %s + Diromanizasikan oleh %s + Diterjemah + Bahasa terjemahan + Bahasa romanisasi + Menterjemah… + Tidak dapat menterjemah lirik + Diromankan Automatik + Meromankan… + Tidak dapat meromankan lirik + Diterjemah sahaja + Diromankan sahaja + Mod dwi + Disalin ke papan keratan + Menyegarkan + Disegarkan + Kunci API QuickLRC + QuickLRC mentranskripsi atau menyelaraskan masa lirik untuk trek semasa. Perkhidmatan berasingan, tidak berkaitan dengan titik akhir yang serasi dengan OpenAI di atas + Kunci QuickLRC disimpan. Gunakan butang selaras dalam dialog carian lirik, atau gesaan panel apabila trek tidak mempunyai lirik + Kunci API QuickLRC disimpan + QuickLRC tidak menerima kunci tersebut + Kunci API QuickLRC dikosongkan + QuickLRC menetapkan masa lirik yang tiada, atau mentranskripsikannya daripada trek, dengan kos kredit. Dengan kunci yang ditetapkan, trek tanpa lirik menawarkan transkripsi dan dialog carian mendapat butang penyelarasan + Minta penjajaran QuickLRC + Menjajarkan + Gunakan penjajaran + Minta transkripsi QuickLRC + Mentranskripsi + Transkripsi dibatalkan + Hanya trek yang dimainkan daripada YouTube boleh ditranskripsi + Tiada lirik untuk diselaraskan - mentranskripsi sebagai ganti + QuickLRC tidak dapat memproses trek ini + Kredit QuickLRC telah habis digunakan untuk trek ini + "Lirik dipadankan mengikut tajuk dan artis, metadata yang tidak biasa mungkin tidak menemui apa-apa atau versi yang salah. Gunakan segar semula untuk menukar versi. Penyedia yang ditanda * memerlukan token. Semua penyedia yang didayakan tidak sentiasa diminta mengikut turutan anda, tetapi bertindak balas berdasarkan kelajuan mereka.\n\nLirik datang daripada perkhidmatan pihak ketiga yang tidak bergabung, ditaja, atau disahkan oleh Morphe. Setiap satu digunakan seadanya: lirik, ketersediaannya, dan pelesenannya kekal dengan perkhidmatan tersebut dan pemberi lesen mereka, dan menggunakannya boleh tertakluk kepada terma perkhidmatan mereka. Apa yang sampai ke perkhidmatan yang didayakan hanyalah carian yang anda minta - tajuk, artis, dan, jika perkhidmatan itu memerlukannya, ID video. Titik akhir AI pilihan dan transkripsi QuickLRC hanya berjalan selepas anda mengkonfigurasinya, dan menerima teks lirik atau URL video trek tersebut." + Hanya aktifkan pembekal yang anda gunakan, atau kekalkan tetapan lalai - permintaan berterusan boleh membebankan laman lirik komuniti yang kecil + Sembunyikan baris kredit + Sembunyikan baris kredit yang dipaparkan di bawah lirik + Tukar terjemahan dan romanisasi + HIDUP meletakkan romanisasi di bawah lirik dan terjemahan di atas; MATI mengekalkan romanisasi di atas dan terjemahan di bawah + Terjemahan + Romanisasi + Pembekal terjemahan + Google (Lalai, Percuma) + Serasi OpenAI (Kastam) + Pembekal romanisasi + Google (Lalai) + Kamus Tempatan / Luar Talian + Serasi OpenAI (Tersuai) + Bahagian AI + Hanya digunakan apabila pembekal tidak menghantar label bahagian seperti Chorus atau Verse, yang kemudiannya dikesan oleh model serasi OpenAI anda + Titik Akhir AI & Awan + Konfigurasikan kunci API, model, titik akhir, dan templat gesaan untuk penyedia AI. + URL titik akhir API (cth. https://api.openai.com/v1/chat/completions) + Templat Prompt + Placeholder: {language}, {task}, {title}, {artist}, {count}, {lines} + Prompt disimpan + Gesaan disimpan, tetapi titik akhir AI belum boleh digunakan. Konfigurasikannya di bawah Tetapan AI terlebih dahulu + Prompt ditetapkan semula + JSON tidak sah. Betulkan atau kosongkan medan sebelum menyimpan + Tetapan Terjemahan & Romanisasi disimpan + Tetapan ditetapkan semula ke lalai + URL asas mesti bermula dengan http:// atau https:// + Uji Sambungan + Konfigurasi Titik Akhir + Tunjukkan Semua Titik Akhir + Menyahkan sambungan… + Pembekal Terjemahan + Pembekal Romanisasi + Ambil Model + Mengambil model… + Pilih Model + Gagal mengambil model. Semak kunci API atau sambungan. + Tetapan API Gemini + Serasi OpenAI (Titik Akhir Tersuai) + Tetapan API DeepL + Kunci API + Model + URL Asas + Kunci API (pilihan) + Prompt terjemahan + Badan permintaan tambahan (JSON pilihan) + Kunci pengesahan (Percuma atau Pro) + Konfigurasikan kunci API, model, titik akhir, dan prompt + 8.23.51 - Nyahaktifkan ikon tebal + 9.13.51 - Pulihkan tab Seterusnya, Lirik, dan Berkaitan Pemain diff --git a/patches/src/main/resources/addresources/values-ms-rMY/reddit/strings.xml b/patches/src/main/resources/addresources/values-ms-rMY/reddit/strings.xml index e44ee3ef19..a5b545cf38 100644 --- a/patches/src/main/resources/addresources/values-ms-rMY/reddit/strings.xml +++ b/patches/src/main/resources/addresources/values-ms-rMY/reddit/strings.xml @@ -16,10 +16,22 @@ Second \"item\" text" --> + Dasar privasi Reddit + Log masuk Google tidak tersedia. Gunakan e-mel untuk log masuk Iklan Bar pandu arah Bar sisi + Fon tersuai + Muat naik fail fon TTF atau OTF tersuai daripada laluan di bawah. Mengaktifkan ini juga akan mematikan Paksa fon sistem + Fail fon tersuai + Ketik untuk memilih fail fon TTF atau OTF + Fon terpilih - %s + Pilih fail fon tersuai terlebih dahulu + Sila pilih fail fon .ttf atau .otf + Gagal memuatkan fon tersuai. Pilih fail yang lain Lumpuhkan antara muka laman utama moden + Paksa fon sistem + Gunakan fon sistem peranti sebagai ganti Reddit Sans atau Roboto. Mengaktifkan ini juga akan mematikan Fon tersuai Lain-lain Mengenai Lalai diff --git a/patches/src/main/resources/addresources/values-ms-rMY/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-ms-rMY/shared-youtube/strings.xml index d915a2ec76..c5c4348059 100644 --- a/patches/src/main/resources/addresources/values-ms-rMY/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ms-rMY/shared-youtube/strings.xml @@ -18,43 +18,193 @@ Second \"item\" text" --> + Animasi permulaan Dilumpuhkan + Warna Monokrom + + Mati + Mulakan semula untuk menghabiskan penyediaan + Nama aplikasi + Latar belakang tema gelap + Hitam pekat + Material You (Hitam pekat) + YouTube Klasik + Merah jambu gelap + Biru gelap + Hijau gelap + Kuning tua + Oren tua + Merah tua + Latar belakang gelap tersuai + Tetapkan warna latar belakang untuk tema gelap App default + Material You (Neutral) Material You (Primary) + Material You (Sekunder) + Material You (Tertier) Tersuai + Muat turun + Mod muat turun Lalai + Muat turun dalam aplikasi + Pemuat turun luaran + Nama pakej pemuat turun + Nama pakej aplikasi pemuat turun luaran yang dipasang + Masukkan nama pakej + Lain-lain + Aplikasi tidak dipasang + %s tidak dipasang. Sila pasangkannya + "Tidak dapat mencari aplikasi yang dipasang dengan nama pakej: %s + +Sahkan nama pakej adalah betul dan aplikasi telah dipasang" + Nama pakej tidak boleh kosong Tersuai + Ikon aplikasi + Asal + Morphe hitam + Morphe gelap + Morphe cerah + Morphe play light + Morphe play hitam Tersuai + "Ini mengubah ikon yang dipaparkan dalam pelancar aplikasi anda. + +Tetapan ini tidak akan mengubah ikon di mana-mana lokasi berikut: +• Senarai aplikasi tetapan sistem peranti +• Pemberitahuan tolak tertentu +• Pemilih aplikasi 'Buka dengan' +• Ikon Android Auto + +Untuk mengubah ikon di semua tempat, tampal semula dan ubah pilihan tampalan 'Ikon aplikasi'" Ikon pemberitahuan + Padankan ikon aplikasi + Morphe play + Notis Morphe + Sejarah tontonan anda tidak disimpan.<br><br>Ini kemungkinan besar disebabkan oleh penyekat iklan DNS atau proksi rangkaian.<br><br>Untuk membetulkannya, senarai putih <b>s.youtube.com</b> atau matikan semua penyekat DNS dan proksi + Jangan tunjukkan lagi + Tiru penstriman video + Konfigurasikan penyamaran klien untuk mengelakkan masalah main balik + "Memperbaiki isu main balik untuk kebanyakan pengguna dengan meminta strim video yang serasi + +Jika anda pengguna YouTube Premium, tetapan ini mungkin tidak diperlukan" + Tidak dapat mengambil sebarang penstriman klien + Klien lalai tidak berfungsi + "%1$s tidak berfungsi beberapa kali berturut-turut, tetapi %2$s berfungsi. + +Jadikan %2$s sebagai klien lalai?" + Tukar + Mematikan tetapan ini mungkin menyebabkan masalah main balik + Cuba dayakan: Spoof penstriman video > Log masuk VR Android + Klien lalai + Log masuk VR Android + Tidak log masuk ke VR Android. Ketik di sini untuk log masuk + Kini log masuk ke Android VR + Untuk menggunakan ciri ini, tukar \'Klien lalai\' kepada Android VR + Log masuk ke Android VR + "Log masuk ke Android VR boleh membaiki masalah main balik bagi sesetengah pengguna + +Untuk log masuk, tekan teruskan di bawah" + Log keluar Panduan + "Kod pengaktifan: %s + +1. Ketik 'Buka laman web' di bawah untuk membuka laman web 'Sambungkan peranti' +2. Tampal daripada papan klip (atau taip) kod pengaktifan yang dipaparkan di atas +3. Log masuk ke Google jika diminta. Jika anda mempunyai beberapa akaun, pilih akaun YouTube ini +4. Ketik 'Benarkan akses YouTube VR' +5. Kembali ke aplikasi ini selepas mencapai mesej 'Peranti disambungkan'" + Gagal mengeluarkan kod pengaktifan + Gagal mengeluarkan token pengesahan + Berjaya log masuk + "Pengesahan disimpan + +• Tetapan Morphe anda tidak akan mengimport/mengeksport pengesahan Android VR +• Jika aplikasi ini tidak digunakan selama 6 bulan, Google akan membuang pengesahan ini +• Anda perlu log masuk semula jika anda menukar kata laluan Google anda atau menyahdaftar peranti ini daripada tetapan Google anda" + OAuth2 tidak tersedia buat sementara waktu (API tamat masa) + Log masuk VR Android telah tamat tempoh, cuba log masuk ke VR sekali lagi + OAuth2 tidak tersedia buat sementara waktu (status %s) + OAuth2 tidak tersedia buat sementara waktu + Tidak dapat log masuk ke VR, pengesahan laman web tidak diluluskan + Tidak dapat log masuk ke VR: %s + Log keluar daripada Android VR + Token log masuk VR tidak sah, cuba log masuk ke VR sekali lagi + Kawalan media pemberitahuan + Sesuaikan kawalan main balik yang dipaparkan pada skrin kunci dan panel pemberitahuan + Sembunyikan butang Sebelumnya & Seterusnya + Menyembunyikan butang Sebelumnya dan Seterusnya dalam pemberitahuan media, skrin kunci, dan kawalan fon kepala + Kecilkan seekbar + Menghalang penarikan seekbar pada skrin kunci dan panel pemberitahuan + Cari dengan butang Sebelumnya & Seterusnya + Mengubah kawalan untuk pemberitahuan media, skrin kunci, dan fon kepala untuk mencari daripada menukar video + Panjang undur + Saat untuk mengundur dengan butang Sebelumnya + Panjang lungsur pantas + Saat untuk melungsur pantas dengan butang Seterusnya + Panjang carian mestilah antara 1 dan %s saat. Menggunakan lalai + Nyahaktifkan protokol QUIC + Nyahaktifkan audio DRC + Paksa bahasa audio asal + Sentiasa mainkan trek audio asal video berbanding versi alih suara + Langkau senyap + Melangkau jeda senyap secara automatik dalam audio video + Panjang jeda minimum + Hanya jeda yang sekurang-kurangnya sepanjang ini akan dipendekkan. Berkesan apabila video seterusnya bermula + Proksi rangkaian + Hala permintaan rangkaian yang disokong melalui proksi + Dayakan proksi + Menghala permintaan rangkaian yang disokong melalui proksi HTTP atau HTTPS yang dikonfigurasi + Hos proksi + Nama hos atau alamat IP pelayan proksi + Port proksi + Port pelayan proksi + Gunakan proksi HTTPS + Gunakan HTTPS untuk menyambung ke pelayan proksi dan bukannya HTTP + Pengesahan proksi + Menghantar pengesahan Asas dengan permintaan proksi + Nama pengguna proksi + Nama pengguna untuk pengesahan proksi Asas + Kata laluan proksi + Kata laluan untuk pengesahan proksi Asas + Benarkan sandaran terus + Sambung secara terus jika proksi gagal + Sembunyikan iklan skrin penuh + Untuk menggunakan ciri ini, tukar \'Spoof video streams\' kepada mana-mana klien kecuali \'Android Studio\' dan \'Android VR Downgraded\' + Bersihkan pautan perkongsian + Alihkan parameter pertanyaan penjejakan daripada pautan yang dikongsi + Tukar pautan kongsi ke youtube.com + Menggantikan music.youtube.com kepada youtube.com dalam pautan yang dikongsi + Tukar pautan kongsi kepada youtu.be + Memaksa video menggunakan format URL youtu.be yang dipendekkan + Protokol penimbal log debug Menambahkan maklumat log tambahan bagi pembangun untuk mengenal pasti komponen aplikasi "Mendayakan tetapan ini akan mencatat data tataletak tambahan, termasuklah teks pada skrin untuk beberapa komponen UI. @@ -62,21 +212,104 @@ Ini boleh membantu mengenal pasti komponen apabila mencipta penapis tersuai. Walau bagaimanapun, mendayakan tetapan ini juga akan mencatat beberapa data pengguna seperti alamat IP anda" + Penapis tersuai + Konfigurasikan penapis tersuai untuk menyembunyikan sebarang komponen + Dayakan penapis tersuai + Senarai rentetan pembina laluan komponen untuk ditapis, dipisahkan oleh baris baharu + Penapis tersuai tidak sah: %s + Penapis menu Tetapan + Sembunyikan item pada skrin tetapan apl standard mengikut nama kelihatan + Buka skrin tetapan apl standard sekali untuk mengisi senarai + Alih keluar dialog budi bicara penonton Ini tidak akan memirau sekatan umur dan hanya menerimanya secara automatik + Pembekal PoToken + Konfigurasikan pembekal PoToken untuk mengelakkan masalah main balik + Pembekal PoToken luaran + "Membaiki isu main balik menggunakan minter PoToken luaran (Eksperimen) + +Jika anda pengguna YouTube Premium, tetapan ini mungkin tidak diperlukan" + "Membaiki isu main balik menggunakan minter PoToken luaran (Eksperimen) + +Untuk menggunakan ciri ini, matikan 'Spoof strim video'" + Jika ia tidak berfungsi, cuba tutup paksa MicroG + Buka tetapan PotHelper + "Ciri ini kini dalam pembangunan dan mungkin tidak berfungsi + +Ketik di sini untuk memuat turun PotHelper" + PotHelper tidak lagi berfungsi + "Token yang terbina dalam versi PotHelper ini tidak lagi berfungsi + + hidupkan 'Spoof video streams' untuk membaiki main balik" + Hidupkan + Tiru versi apl + Menipu pelayan supaya menganggap anda menggunakan versi apl yang lebih lama untuk memulihkan ciri UI legasi + "Versi aplikasi akan ditiru kepada versi yang lebih lama. + +Ini akan mengubah penampilan dan ciri-ciri aplikasi, tetapi kesan sampingan yang tidak diketahui mungkin berlaku. + +Jika dimatikan kemudian, anda disyorkan untuk mengosongkan data aplikasi untuk mengelakkan pepijat UI" + Tiru sasaran versi aplikasi + Kadar segar semula aplikasi Lalai + %s FPS + Atasi kadar penyegaran + Sentiasa + Main balik potret + Main balik skrin penuh + Main balik potret dan skrin penuh + Fon Lalai + Fon sistem peranti + Fail fon tersuai + Fail fon tersuai + Pilih fail fon TTF atau OTF + Fail yang dipilih bukan fon TTF atau OTF + Fon tersuai tidak dapat dimuatkan dan telah dimatikan + Dislike tidak tersedia buat sementara waktu (API tamat masa) + Tidak suka tidak tersedia (status %s) + Dislike tidak tersedia (had API klien) + Dislike tidak tersedia (%s) + Disembunyikan oleh pemilik + Tidak suka video ini, bersama %s orang lain + Tidak suka video ini, %s daripada undian adalah tidak suka + Dayakan Return YouTube Dislike + Tunjukkan tidak suka sebagai peratusan + Menunjukkan tidak suka sebagai peratusan daripada suka dan bukannya nombor + Tunjukkan anggaran suka + Menunjukkan anggaran jumlah suka pada video di mana pencipta telah menyahaktifkan suka + Tunjukkan amaran ralat sambungan + Menunjukkan pemberitahuan apabila pelayan Return YouTube Dislike tidak tersedia + "Return YouTube Dislike mengembalikan jumlah tidak suka yang disembunyikan oleh YouTube pada 2021. Jumlah tersebut datang daripada data yang diarkibkan sebelum pembuangan dan dianggar untuk video yang lebih baharu daripada undian penggunanya, jadi ia adalah anggaran dan dikemas kini setiap beberapa hari + +ID video yang anda tonton dihantar ke API Return YouTube Dislike, dan suka serta tidak suka anda dihantar secara tanpa nama untuk menambah baik anggaran tersebut" + Statistik API Return YouTube Dislike peranti ini + Masa tindak balas API, purata + Masa tindak balas API, minimum + Masa tindak balas API, maksimum + Masa tindak balas API, media terakhir + Tidak suka tidak tersedia buat sementara waktu - Had kadar API Klien sedang berkuat kuasa + API ambil undian, jumlah panggilan + Tiada panggilan rangkaian dibuat + %s panggilan rangkaian telah dibuat + API ambil undian, jumlah masa tamat + Tiada panggilan rangkaian yang tamat masa + %s panggilan rangkaian telah tamat masa + Had kadar klien API + Tiada had kadar klien ditemui + Had kadar klien ditemui %s kali + %s milisaat diff --git a/patches/src/main/resources/addresources/values-ms-rMY/shared/strings.xml b/patches/src/main/resources/addresources/values-ms-rMY/shared/strings.xml index 96da222730..bb4afc23e0 100644 --- a/patches/src/main/resources/addresources/values-ms-rMY/shared/strings.xml +++ b/patches/src/main/resources/addresources/values-ms-rMY/shared/strings.xml @@ -18,30 +18,218 @@ Second \"item\" text" Tetapan + Adakah anda pasti ingin meneruskan? + Simpan + Set semula + Set semula warna + Warna tidak sah + Mula semula diperlukan + Mulakan semula apl untuk perubahan ini berkuat kuasa + Mula semula Import + Salin + Tetapan Morphe set semula ke lalai + Import %s tetapan berjaya + Import gagal: %s + Navigasi ke atas + Cari tetapan Mengenai + Tiada keputusan ditemui untuk \'%s\' + Cuba kata kunci lain + Carian terbaru + Alih keluar daripada sejarah carian? + Alih keluar daripada sejarah carian + Kosongkan sejarah carian + Adakah anda pasti ingin mengosongkan semua sejarah carian? + Petua Carian + "• Ketik laluan untuk menavigasi ke sana +• Tekan lama tetapan untuk menavigasi ke sana +• Tekan Enter untuk menyimpan kueri carian ke sejarah +• Carian mengabaikan huruf besar/kecil dan tanda baca +• Tetapan induk muncul di atas tetapan anak yang dinyahaktifkan" + Sejarah carian adalah kosong + Untuk menyimpan sejarah carian, taip kueri carian dan tekan Enter + Tunjukkan sejarah carian tetapan + Nama tetapan Morphe Lalai + Ekstra + Ubah suai + Masa morphing Tersuai + Masukkan nama + Nama tidak boleh kosong + Tunjukkan ikon tetapan Morphe + Disalin: %s + Bahasa Morphe + "Terjemahan untuk beberapa bahasa mungkin hilang atau tidak lengkap. + +Untuk menterjemah bahasa baharu atau menambah baik terjemahan sedia ada, layari morphe.software/translate" + Bahasa aplikasi + Import / Eksport + Import / Eksport tetapan Morphe + Import daripada fail + Tetapan telah diimport dengan jayanya, ketik Simpan untuk apply + Gagal mengimport tetapan + Export ke fail + Tetapan telah dieksport dengan jayanya + Gagal untuk membawa keluar tetapan + Anda menggunakan versi Morphe Patches terkini <i>%s</i> + Anda menggunakan Morphe Patches versi <i>%1$s</i><br><br><b>Kemas kini tersedia: <i>%2$s</i></b><br><br>Untuk mengemas kini tampalan anda, gunakan Morphe untuk menampal semula aplikasi ini + Nota + Versi ini adalah pra-rilis dan anda mungkin mengalami masalah yang tidak dijangka + Pautan rasmi + Laman web + Derma + Terjemahan + Kredit + Kemas kini + Kemas kini tersedia + Pembangunan semasa + Pembangunan terdahulu + Pembangunan aktif + 2022 ‑ 2025 + 2022 ‑ 2025 + 2019 ‑ 2022 + Notis lesen + Memuatkan… + Penyahpepijatan + Konfigurasikan pilihan pembangun lanjutan dan log diagnostik + Dayakan log debug + Jejak timbunan log debug + Sertakan jejak timbunan dalam log debug untuk membantu mengenal pasti ralat + Tunjukkan pemberitahuan pada ralat Morphe + "Menutup pemberitahuan ralat menyembunyikan semua pemberitahuan ralat Morphe. + +Anda tidak akan diberitahu tentang sebarang peristiwa tidak dijangka" + Bawa keluar log debug + Bawa keluar log debug Morphe + Log debug berjaya dibawa keluar + Gagal untuk mengeksport log debug + Cari log debug… + Kosongkan + Log penyahpepijatan telah dinyahaktifkan + Tiada log debug ditemui + Log debug telah disalin + Gagal untuk mengeksport log debug: %s + Log debug telah dikosongkan + Pengurus bendera ciri + Urus bendera ciri boolean + Cari bendera ciri… + Tidak aktif (%s) + Aktif (%s) + Disekat (%s) + Dipaksa (%s) Automatik Mati Hidup + Paksa hidupkan + Sekat Keluarkan + Bendera ciri telah disimpan + Bendera ciri telah ditetapkan semula + Bendera ciri disalin ke papan keratan + Mulakan semula aplikasi untuk menggunakan tetapan ini + Eksport dan import + %s bendera ciri diimport + Tiada bendera ciri ditemui + Bendera ciri berjaya dieksport + Gagal untuk mengeksport bendera ciri + Bendera ciri dimuatkan, ketik Simpan untuk guna + Gagal membaca fail + Carian binari + "Mencari bendera ciri yang menyebabkan kelakuan aplikasi. + +Separuh daripada %s bendera disekat, dan selepas setiap mula semula anda memberitahu jika kelakuan tersebut masih ada. + +Ulang kelakuan tersebut sekali sebelum bermula, jika tidak bendera yang belum diminta oleh aplikasi akan hilang daripada carian" + Mula + "Tiada bendera ciri untuk dicari. + +Hanya bendera yang telah dicapai oleh aplikasi dan tidak dipaksa aktif pada masa ini boleh digunakan dalam carian binari" + "Langkah %1$s + +%2$s bendera tinggal, %3$s daripadanya disekat untuk langkah ini. + +Mulakan semula aplikasi, semak jika kelakuan tersebut ada, kemudian jawab di bawah" + Carian bendera ciri sedang berjalan + Tingkah laku wujud + Tingkah laku tidak wujud + Batal carian binari + Carian binari dibatalkan + "Bendera ciri %s menyebabkan tingkah laku tersebut. + +Ia kini disekat" + "Tidak dapat mencari bendera ciri. + +Tingkah laku aplikasi nampaknya tidak dikawal oleh bendera ciri" + "Tidak dapat mencari satu bendera ciri. + +Tingkah laku tersebut hilang lebih awal dalam carian, jadi lebih daripada satu bendera mungkin terlibat atau salah satu jawapan adalah salah" + Buka tetapan MicroG + MicroG tidak dipasang. Pasangnya + Tindakan diperlukan + "MicroG v%1s anda sudah lapuk dan mungkin menyebabkan masalah. + +Sila kemas kini MicroG ke versi terbaru v%2s" + Kemas kini + Abaikan + "MicroG yang dipasang tidak disyorkan oleh Morphe: + +%1s + +Ketik untuk menyahpasangnya, kemudian buka semula aplikasi ini dan pasang MicroG RE" + Nyahpasang + "MicroG tidak mempunyai kebenaran untuk berjalan di latar belakang. + +Ikuti panduan \"Don't kill my app\" untuk telefon anda, dan apply arahan tersebut pada pemasangan MicroG anda. + +Ini diperlukan agar aplikasi dapat berfungsi" + Buka laman web + "Pengoptimuman bateri MicroG mesti dinyahaktifkan untuk mengelakkan masalah. + +Menyahaktifkan pengoptimuman bateri untuk MicroG tidak akan menjejaskan penggunaan bateri secara negatif. + +Ketik butang teruskan dan benarkan perubahan pengoptimuman" Teruskan + Tunjukkan dialog pengoptimuman bateri MicroG + "Menyahaktifkan ini akan menyembunyikan gesaan pengoptimuman bateri MicroG. + +Jika pengoptimuman bateri tidak diubah, anda mungkin mengalami: +• Aplikasi mungkin tertutup secara rawak +• Notifikasi mungkin tidak berfungsi +• Main balik latar belakang mungkin tidak berfungsi" + Pengendalian pautan aplikasi + Konfigurasikan pautan untuk dibuka dalam aplikasi ini + Langkah 1: Matikan pautan aplikasi asal + Buka tetapan aplikasi asal dan matikan semua pautan domain + Langkah 2: Dayakan pautan aplikasi yang telah ditampal + Buka tetapan aplikasi yang telah ditampal dan dayakan semua pautan domain + Buka tetapan + 🚨 Amaran Uji kaji 🚨 + "Anda sedang menggunakan versi aplikasi uji kaji ⚠️ <b>%1$s</b> +<br/><br/> +🔧 Jangkakan tingkah laku aplikasi yang pelik atau pepijat yang tidak dikenal pasti sementara kami memperhalusi tampalan untuk versi aplikasi ini. +<br/><br/> +Jika anda mahukan pengalaman yang paling bebas masalah, sila <b>nyahpasang</b> ini dan tampalkan versi aplikasi yang disyorkan ✅ <b>%2$s</b>" + ⚠️ Saya mahu uji kaji + ✅ Saya mahu stabil + Log kerosakan disimpan ke dokumen pengguna: %s + Log kerosakan disimpan: %s diff --git a/patches/src/main/resources/addresources/values-ms-rMY/sponsorblock/strings.xml b/patches/src/main/resources/addresources/values-ms-rMY/sponsorblock/strings.xml index cc2b254b1c..cf290ae579 100644 --- a/patches/src/main/resources/addresources/values-ms-rMY/sponsorblock/strings.xml +++ b/patches/src/main/resources/addresources/values-ms-rMY/sponsorblock/strings.xml @@ -17,6 +17,7 @@ Second \"item\" text" --> Penampilan + Menggunakan penjuru segi empat untuk butang dan kawalan SponsorBlock 1 saat 2 saat 3 saat @@ -36,4 +37,5 @@ Second \"item\" text" + Memuatkan… diff --git a/patches/src/main/resources/addresources/values-ms-rMY/youtube/strings.xml b/patches/src/main/resources/addresources/values-ms-rMY/youtube/strings.xml index 1d3a966f66..d8929ca2b2 100644 --- a/patches/src/main/resources/addresources/values-ms-rMY/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ms-rMY/youtube/strings.xml @@ -32,11 +32,26 @@ Second \"item\" text" Keluarkan sekatan main di latar belakang Lumpuhkan main di latar belakang + Jeda automatik apabila peranti dikunci Mati + Sentiasa jeda apabila dikunci + Jeda kecuali audio tanpa wayar disambungkan + Nyahaktifkan had kelajuan tatalan Lumpuhkan kemas kini tataletak Mengelakkan pelayan daripada mengemas kini tataletak aplikasi + "Tataletak aplikasi kembali kepada tataletak yang digunakan semasa pertama kali dipasang. + +Beberapa tataletak sisi pelayan mungkin tidak kembali. + +Perubahan termasuk: +• Komponen di menu terbang keluar pemain (atau tetapan berkaitan) mungkin tidak berfungsi +• Nombor yang berpusing tidak dianimasikan +• Tab Pustaka ditunjukkan +• Bahagian muzik bagi perihalan video mungkin tidak berfungsi +• Item menu terbang keluar pemain tidak dapat disorokkan +• Butang penukaran akaun mungkin tidak muncul di tab Pustaka" Dayakan penapis menu terbang keluar suapan Penapis menu terbang keluar suapan @@ -48,61 +63,342 @@ Second \"item\" text" Sorokkan kad album di bawah perihalan artis Sorokkan kad artis Sorokkan kad artis di bahagian atas hasil carian + Sorokkan butang saluran + Menyembunyikan butang seperti Langgan, Lihat Saluran, Komuniti, dan YouTube Music daripada profil saluran dalam hasil carian Sorokkan kiriman komuniti Sorokkan sepanduk padat Sorokkan sepanduk padat dan ruangan di antara beberapa komponen Kad boleh kembang di bawah video Tunjukkan semua + Sembunyikan kad ringkasan sahaja Sorokkan semua Sorokkan butang mikrofon terapung Sorokkan butang mikrofon terapung di sudut kanan bawah skrin cadangan hasil + Sorokkan butang \'Dapatkan Premium\' + Sorokkan rak Sejarah Sorokkan laci mengufuk + Menyembunyikan rak mendatar seperti Berita terkini, Pos komuniti yang mungkin anda suka, Teruskan menonton, Teroka lebih banyak saluran, Teroka lebih banyak topik, Untuk Anda, Video terbaharu, Paling relevan, Pos daripada Komuniti Pencipta, Membeli-belah, dan Tonton sekali lagi Sorokkan laci imej Menyorokkan laci imej di hasil carian + Sembunyikan kad \'Jemput orang lain untuk menghantar mesej\' + Menyembunyikan kad jemputan yang dipaparkan dalam bahagian Mesej tab Pemberitahuan Sorokkan butang \'Video terkini\' Menyorokkan sepanduk \'Video terkini\' biru yang muncul di bahagian atas suapan Laman Utama + Sembunyikan penstriman langsung + Menyembunyikan video yang sedang bersiaran langsung dalam suapan utama dan hasil carian Sorokkan senarai main adun Sorokkan bahagian filem + Sorokkan pengepala menu Pemberitahuan + Menyembunyikan pengepala menu Pemberitahuan apabila mengklik butang loceng Sorokkan butang \'Beritahu saya\' + Menyorokkan butang peringatan bagi penstriman langsung dan tayangan perdana yang dijadualkan Sorokkan \'Boleh main\' Menyorokkan ciri yang membenarkan anda bermain permainan Menyorokkan lakaran kecil istilah carian Menyorokkan pratayang lakaran kecil di senarai autolengkap carian + Sorokkan butang \'Tunjukkan lebih banyak\' + Menyembunyikan butang kembang di bawah video dalam hasil carian + Sorokkan bar saluran dilanggan + Sorokkan nama saluran yang dilanggan + Menyembunyikan nama saluran dalam bar saluran yang dilanggan + Sorokkan tinjauan + Sembunyikan tinjauan dan dorongan tentang ketepatan syor feed + Sorokkan laci tiket + + Sembunyikan sepanduk \'Video yang tidak tersedia disembunyikan\' + Menyembunyikan sepanduk \'Video yang tidak tersedia disembunyikan\' dalam senarai main + Sorokkan masa muat naik + "Batasan: +• Laci Shorts, laman saluran dan hasil carian mungkin masih menunjukkan masa muat naik +• Tidak berfungsi dengan tataletak automotif" + Sorokkan imej kenit video + Sorokkan label video + Menyembunyikan label seperti \'Alih suara automatik\' / \'Dialih suara\', \'Mendapat komisen\', \'Hype\', \'Orang lain juga menonton\', \'produk(s)\' dan \'Anda mungkin juga suka\' + Sorokkan jumlah tontonan + "Batasan: +• Laci Shorts, laman saluran dan hasil carian mungkin masih menunjukkan kiraan tontonan +• Tidak berfungsi dengan tataletak automotif" + Sorokkan hasil carian web + Sembunyikan bahagian \'Hasil daripada web\' daripada hasil carian + Sembunyikan bahagian \'Anda mungkin suka\' + Sorokkan YouTube Doodles + Menyembunyikan animasi bermusim dan berasaskan acara pada logo YouTube Menunjukkan Doodles YouTube mungkin mengelakkan pengepala tersuai daripada dipaparkan + Sorokkan bar saluran + Sorokkan tanda air saluran + Sembunyikan Bab dan butang \'Dalam video ini\' + Sorokkan kotak pembiayaan khalayak + Sorokkan kotak kecemasan + Sorokkan panel info + Menyembunyikan panel konteks maklumat, seperti maklumat perubatan + Sorokkan butang Sertai + Sorokkan butang \'Main semula sembang langsung\' + Menyembunyikan butang ulangan sembang dalam mod skrin penuh + Sorokkan panel perubatan + Sembunyikan petunjuk gerak isyarat pemain + Menyembunyikan petunjuk yang dipaparkan semasa gerak isyarat pemain, seperti \'Tarik ke atas untuk pencarian tepat\', \'Lepaskan untuk batal\' dan \'2x\' + Sembunyikan snackbar + Sorokkan garis panduan pelanggan Sorokkan butang \'Segerakkan ke video\' + Sembunyikan gradien tekan dan tahan + Menyembunyikan gradien kawalan pemain apabila menggerakkan jari semasa menahan untuk mempercepatkan + Sorokkan reaksi bertimer + Menyembunyikan ciri yang membolehkan pengguna memberi reaksi dengan emoji dalam bahagian komen dan sembang langsung + Sembunyikan menu \'Bantuan & maklum balas\' + Menyembunyikan menu \'Bantuan & maklum balas\' apabila menggunakan butang tiga titik, seperti pada hasil carian + Sembunyikan pemegang + Menyembunyikan pemegang saluran anda dalam tab \'Anda\' + Sorokkan tajuk video + Menyembunyikan tajuk video dalam mod skrin penuh + Sanitasi sari kata video + Membersihkan frasa seperti \'#\', \'Pengumpul dana\', \'produk(s)\', dan \'Kedai\' daripada sari kata video + Sorokkan butang \'Tanya Gemini\' + Menyembunyikan butang \'Tanya Gemini\' di bahagian bawah panel \'Tanya tentang video ini\' + Keterangan video + Pilih komponen yang muncul dalam penerangan video + Sorokkan bahagian \'Ringkasan video dijana AI\' + Sorokkan bahagian Tanya + Sorokkan bahagian Atribut + Menyembunyikan bahagian Tempat terpilih, Permainan, Muzik, dan Orang yang disebut + Sorokkan bahagian pautan Saluran + Sorokkan bahagian Bab + Sorokkan bahagian Pembetulan + Sorokkan bahagian \'Kemajuan kursus\' + Sorokkan bahagian Teroka + Menyembunyikan bahagian Teroka kursus ini dan Teroka podcast + Sorokkan bahagian \'Teroka kursus ini\' + Sorokkan bahagian \'Teroka podcast\' + Sembunyikan bahagian saluran Pilihan + Sorokkan bahagian Pautan terpilih + Sorokkan bahagian Senarai main terpilih + Sorokkan bahagian video Pilihan + Sorokkan bahagian Hashtag + Sembunyikan bahagian \'Cara ini dibuat\' + Sorokkan mata Hype + Sorokkan bahagian kad Info + Sembunyikan bahagian \'Konsep Utama\' + Sorokkan bahagian Kuiz + Sorokkan bahagian \'Cari dalam video ini\' + Sorokkan butang Langgan + Sorokkan bahagian Transkrip + Sorokkan bahagian \'Butiran video\' + Bar tapis + Pilih tempat untuk menyembunyikan bar penapis tatal + Sembunyikan dalam halaman saluran + Sembunyikan dalam komen + Sembunyikan dalam suapan + Sembunyikan dalam video berkaitan + Sembunyikan dalam keputusan carian + Sembunyikan dalam sejarah tontonan + Halaman saluran + Pilih komponen yang akan dipaparkan pada halaman saluran + Dayakan penapis tab saluran + Penapis tab saluran + Masukkan nama tab saluran untuk menapis, dipisahkan oleh baris baru + Sorokkan butang Komuniti + Sorokkan butang Sertai + Sorokkan pratonton pautan + Menyembunyikan pautan web yang boleh diklik dalam pengepala saluran + Sembunyikan cip \'Ahli sahaja\' + Menyembunyikan cip \'Ahli sahaja\' dalam bar penapis saluran + Sorokkan laci ahli + Menyembunyikan senarai ahli saluran + Sorokkan laci hantaran + Sorokkan butang Kedai + Sorokkan butang Langgan + Menu Teroka + Pilih komponen yang muncul dalam menu teroka + Sorokkan menu Kursus Sorokkan butang \'Teroka\' + Sorokkan menu \'Fesyen & Kecantikan\' + Sorokkan menu Permainan + Sorokkan menu Hype + Sembunyikan menu Pembelajaran + Sorokkan menu Langsung + Sorokkan menu Keahlian + Sembunyikan menu \'Filem & TV\' + Sembunyikan menu Muzik + Sorokkan menu Berita + Sembunyikan menu Playables + Sembunyikan menu Podcast + Sembunyikan menu Shopping + Sembunyikan menu Sukan + Sembunyikan menu YouTube Premium + Sembunyikan menu YouTube Studio + Sembunyikan menu YouTube Music + Sembunyikan menu YouTube Kids + Sembunyikan menu YouTube Create + Sembunyikan menu \'Cara YouTube berfungsi\' + Sembunyikan footer undang-undang dan privasi + Komen + Pilih komponen yang akan muncul dalam bahagian komen + Dayakan penapis carousel komen + "• Sila pastikan nama yang tepat dimasukkan untuk item karusel (contohnya Tanya, Naikkan video ini, Dengarkan di YouTube Music) +• Menyorokkan item karusel mungkin mempunyai kesan sampingan yang tidak diingini +• Jika semua item karusel disorokkan, keseluruhan bahagian komen juga akan disembunyikan" + Penapis carousel komen + Masukkan nama korsel komen untuk menapis, dipisahkan oleh baris baru + Sorokkan ringkasan sembang AI + Sorokkan garis panduan saluran + Sorokkan mesej konteks + Sorokkan pengepala \'Komen oleh ahli\' + Sorokkan bahagian komen + Sembunyikan bahagian komen dalam suapan Utama + Sorokkan Garis Panduan Komuniti + Sorokkan butang \'Buat Short\' + Sorokkan butang Tidak Suka + Sorokkan butang Emoji + Sembunyikan pilihan bar penapis + Menyembunyikan semua pilihan bar penapis kecuali \'Utama\' dan \'Terbaharu\' + Sorokkan animasi dan kad hadiah + Sorokkan butang Hadiah + Sorokkan butang Info + Sembunyikan bar penderma sembang langsung + Sembunyikan butang skrol ke bawah sembang langsung + Sembunyikan tip alat sembang langsung + Menyembunyikan popup tip yang dipaparkan berhampiran bar input sembang, seperti notis sembang hanya untuk pelanggan + Sorokkan butang Menu + Menyembunyikan butang menu 3-titik bagi setiap komen + Sorokkan pratonton komen + Menyembunyikan pratonton komen di bawah video sebelum membuka bahagian komen penuh + Komen disembunyikan oleh Morphe… + Dayakan butang komen minimal + Menyembunyikan kad lungsur dan pemilih halamannya, yang dipaparkan pada butang komen + Sorokkan butang Terima Kasih + Sorokkan butang Cap Masa + Sorokkan butang \'Peminat utama\' + Sorokkan butang Terjemah + Sembunyikan mesej \'Belum ada komen\' + Bersihkan pautan carian yang diserlahkan + Menyahjangkit pautan carian yang disorot, meninggalkan teks biasa yang asal sahaja + Penapis kata kunci + Pilih perkataan atau frasa tertentu untuk menyembunyikan video dalam feed dan carian + Sorokkan komen melalui kata kunci + Sorokkan video Laman Utama mengikut kata kunci + Sorokkan keputusan carian mengikut kata kunci + Sorokkan video Langganan mengikut kata kunci + Kata kunci untuk disorokkan + "Masukkan kata kunci dan frasa untuk disorokkan, dipisahkan oleh baris baharu + +Kata kunci boleh jadi sebarang teks yang dipaparkan dalam tajuk video atau nama saluran + +Perkataan dengan huruf besar di tengah mestilah sepadan dengan tepat (contohnya iPhone, TikTok, LeBlanc)" + "Laman Utama / Langganan / Carian / Komen ditapis untuk menyembunyikan kandungan yang sepadan dengan frasa kata kunci + +Had: +• Shorts tidak boleh disembunyikan melalui nama saluran +• Sesetengah komponen UI mungkin tidak disembunyikan +• Mencari kata kunci mungkin tidak menunjukkan sebarang hasil" + Padankan keseluruhan perkataan + Mengelilingi kata kunci/frasa dengan tanda petik dua akan mengelakkan padanan separa tajuk video dan nama saluran<br><br>Sebagai contoh,<br><b>\"ai\"</b> akan menyembunyikan video: <b>How does AI work?</b><br>tetapi tidak akan menyembunyikan: <b>What does fair use mean?</b> + Tidak boleh menggunakan kata kunci: %s + Tambah tanda petik untuk menggunakan kata kunci: %s + Kata kunci mempunyai pengisytiharan yang bercanggah: %s + Kata kunci terlalu pendek dan memerlukan tanda petik: %s + Kata kunci akan menyembunyikan semua video: %s + "• Utama: %s +• Langganan: %s +• Carian: %s +• Komen: %s +• Jumlah: %s" + Statistik + Video disembunyikan, 24 jam terakhir + Video disembunyikan, sepanjang masa + Set semula kaunter 24 jam terakhir? + Set semula kaunter sepanjang masa? + Penapis saluran AI + Sembunyikan video daripada saluran yang ditandakan sebagai kandungan dijana AI oleh senarai komuniti AiSList + Senarai sekat: Keyakinan tinggi + Senarai amaran: Keyakinan sederhana + Sembunyikan video di Laman Utama + Sembunyikan video dalam carian + "• Laman Utama: %s +• Carian: %s +• Jumlah: %s" + Saluran dalam senarai sekatan + Saluran dalam senarai amaran + Dayakan penapis untuk memuatkan saluran + Memuatkan saluran… + "Halaman Utama dan hasil carian ditapis untuk menyembunyikan video daripada saluran yang ditandakan oleh AiSList + +Had: +• Shorts AI yang ditapis mungkin kelihatan sebagai ruang kosong +• Sesetengah video AI mungkin masih dipaparkan" + "AiSList ialah senarai sumber terbuka saluran YouTube yang menerbitkan kandungan jana AI, yang diselenggara oleh sukarelawan. Saluran dilaporkan oleh pengguna dan disemak sebelum ditambah + +Senarai ini dimuat turun daripada GitHub dan video ditapis pada peranti anda" + Laporkan saluran AI + Tambah entri laporan ke menu video + Tambah entri ke menu video yang melaporkan saluran video kepada AiSList + Nama penghantar + Nama yang dipaparkan bersebelahan laporan anda pada papan pendahulu awam AiSList. Pelaporan tidak tersedia selagi ini kosong + Laporkan saluran AI + "Hantar %s ke AiSList, menggunakan video ini sebagai bukti? + +Hanya laporkan saluran yang: +• Menggunakan suara, visual, skrip atau muzik AI sebagai kandungan utama mereka +• Menghasilkan kandungan secara besar-besaran dengan alatan AI dan sedikit kerja manusia + +Jangan laporkan saluran yang hanya membincangkan AI, atau menggunakan AI untuk beberapa bahagian dalam video mereka. + +Laporan adalah awam dan disemak sebelum saluran ditambah ke senarai. Penyingkiran hanya boleh dilakukan melalui permintaan di GitHub" + Saluran ini sudah berada dalam senarai sekatan AiSList + Saluran ini sudah berada dalam senarai amaran AiSList + Laporkan + Tetapkan nama penghantar dalam tetapan penapis saluran AI untuk melaporkan saluran + Mencari saluran… + Tidak dapat mencari saluran video ini + Saluran telah dilaporkan dan menunggu semakan + AiSList tidak dapat menyimpan laporan, cuba lagi kemudian + Terlalu banyak laporan dihantar, cuba lagi kemudian + AiSList menolak laporan tersebut + Laporan gagal, sila semak sambungan anda + Senarai putih iklan saluran + Video daripada saluran dalam senarai putih akan dimainkan dengan iklan, supaya penciptanya dibayar. Iklan tetap disekat pada setiap saluran lain + Tambah saluran ke senarai putih iklan + Keluarkan saluran daripada senarai putih iklan + Sorokkan rak stor pencipta + Sorokkan sepanduk stor skrin akhir + Sorokkan iklan umum + Sorokkan sepanduk barangan + Sorokkan label promosi berbayar + Sembunyikan iklan timbul pemain + Sorokkan kad tajaan sendiri + Sorokkan pautan beli-belah Sorokkan iklan video + Sorokkan promosi YouTube Premium + Memuatkan… Saluran semasa: Tambahkan ke senarai putih @@ -113,59 +409,330 @@ Second \"item\" text" Saluran dikeluarkan daripada senarai putih + Cari dalam saluran + Menambah butang carian kedua pada halaman saluran untuk mencari saluran itu sahaja + Cari saluran + Hasil carian saluran + Tiada apa yang dijumpai pada saluran ini + Tidak dapat mencari saluran ini + Video + Hantaran + Tiada hantaran pada saluran ini yang sepadan + Relevan + Paling banyak ditonton + Paling kurang dilihat + Terbaharu + Terlama + Short terpendek + Terpanjang Pautan disalin ke papan keratan Pautan dengan cap masa disalin ke papan keratan Tunjukkan butang \'Salin pautan video\' Ketik untuk menyalin pautan video, ketik dan tahan untuk menyalin dengan cap masa + Salin penanda masa secara lalai Membalikkan tindakan ketik serta ketik dan tahan bagi butang \'Salin pautan video\' + Salin tajuk video + Ketik dan tahan tajuk video untuk menyalinnya + Tajuk video disalin ke papan keratan + Salin komen + Ketik dan tahan komen untuk menyalinnya + Komen disalin ke papan keratan + Ketik dan tahan untuk membuka menu video + Mengetik dan menahan di mana-mana pada video suapan akan membuka menunya + Tunjukkan butang Tonton kemudian pada imej kenit + Butang Tonton kemudian ditunjukkan pada imej kenit video suapan. Mengetik dan menahannya akan membuka Simpan ke senarai main + Muat turun imej hantaran + Butang muat turun dipaparkan bersebelahan dengan butang Kongsi bagi hantaran dengan imej atau GIF, dan item muat turun ditambah ke menu mereka + Muat turun imej + Muat turun imej + Memuat turun %s imej… + Memuat turun imej… + %1$s imej disimpan ke %2$s + Imej disimpan ke %s + Tidak dapat memuat turun imej + Tiada imej ditemui dalam hantaran ini + Tambah item Tanya ke menu flyout + Menambah item ringkasan video ke menu flyout feed + "Ask pada mulanya memerlukan akaun yang telah log masuk di negara yang tersedia Ask. + +Selepas video dengan butang Ask dibuka sekali, ciri ini berfungsi dengan semua video walaupun negara semasa tidak mempunyai ciri Ask." + Ringkaskan video + Tidak dapat membuka Ask untuk video ini + Ask tidak tersedia untuk akaun atau negara semasa anda + Tunjukkan jumlah tontonan pada imej kecil + Jumlah tontonan video feed dipaparkan di bahagian bawah kiri imej kecil dan bukannya di bawah tajuk. Sesetengah video, seperti dalam rak, mungkin tidak memaparkan jumlah tontonan + Tunjukkan jumlah suka pada pengecil feed + Tunjukkan peratusan suka pada pengecil feed + Cari komen + Ketik butang cari dalam komen untuk memaparkan hanya komen yang dimuatkan yang mengandungi semua perkataan yang dicari. Letakkan perkataan dalam tanda petik untuk mencari frasa + Cari komen + Perkataan untuk dicari Carian + Kosongkan + Tukar urutan isihan atau buka semula komen untuk mencarinya + Carian dikosongkan. Tukar urutan isihan atau buka semula komen untuk memaparkan semua komen + Nyahaktifkan popup \'Log masuk ke TV\' + Nyahaktifkan popup \'Sambungkan peranti anda\' + Nyahaktifkan langkau bab ketik dua kali + Nyahaktifkan gerak isyarat ketik dua kali dengan dua jari yang melangkau bab, yang mungkin dicetuskan secara tidak sengaja dengan satu jari + Muat turun video, podcast atau lagu dengan apl muat turun pihak ketiga + Tunjukkan butang muat turun tindanan pemain + Menunjukkan butang dalam pemain video yang membuka pemuat turun luaran anda + Muat turun pantas dengan satu ketikan + Prompt untuk muat turun di latar belakang. Ketik dan tahan untuk membuka pemuat turun. Berfungsi dengan YTDLnis dan YTindir + Tandakan muat turun sebagai telah ditonton + Video yang dimuat turun dengan pemuat turun luaran akan ditambah ke sejarah tontonan YouTube anda + Full HD (1080p) + Sederhana (192 kbps) + Muat turun pantas Video + Audio + Semua pilihan + Tunjukkan muat turun dalam menu Shorts + Menambah item menu flyout pemuat turun luaran ke pemain Shorts + Muat turun + Terjemahan alihan suara + Baca sari kata video dengan kuat menggunakan teks-ke-ucapan peranti, diselaraskan dengan main balik + Menunjukkan butang tindihan dalam pemain video. Ketik untuk tukar terjemahan, ketik dan tahan untuk tetapan pantas + Bahasa + Pembekal terjemahan + Volum audio asal + Volum audio YouTube asal semasa terjemahan sedang bercakap + Volum terjemahan + Volum suara terjemahan alih suara + Kadar pertuturan maks + Pengganda kelajuan maksimum apabila menyesuaikan pertuturan ke dalam slot sari kata + "API terjemahan percuma, tiada kunci API diperlukan. Sehingga 5,000 aksara sehari untuk peringkat percuma, atau sehingga 50,000 aksara sehari dengan alamat e-mel. + +Ketik di sini untuk membuka MyMemory.translated.net" + E-mel MyMemory + Pilihan. Masukkan alamat e-mel MyMemory anda untuk meningkatkan had harian daripada 5,000 kepada 50,000 aksara. Biarkan kosong untuk menggunakan had tanpa nama + "Terjemahan AI berbayar melalui OpenRouter. Tambah nilai sekali dan terjemah ribuan video. Mistral Nemo adalah pilihan termurah, Gemma 4 menawarkan kualiti tertinggi. + +Ketik di sini untuk membuka OpenRouter.ai" + Kunci API OpenRouter + Kunci API OpenRouter anda. Cipta kunci di OpenRouter.ai > Workspace > API Keys + Model OpenRouter Tersuai + OpenRouter tidak dikonfigurasikan + Masukkan kunci API OpenRouter anda dalam tetapan sebelum memilih penyedia ini + Percuma + %s bagi setiap 100 jam video + Suara Sistem + Lelaki pelbagai bahasa + Perempuan pelbagai bahasa + Lelaki + Perempuan + Masa untuk morphing! + Terjemahan tidak tersedia + "Perkhidmatan terjemahan mengembalikan ralat HTTP %s. Alamat IP anda berkemungkinan telah dihadkan kadar sehingga 24 jam. + +Untuk terus menggunakan terjemahan, tukar ke penyedia terjemahan yang berbeza dalam tetapan atau gunakan VPN" + Jangan tunjukkan lagi + "Tetapan anda mengandungi kunci API OpenRouter. + +Kunci API anda tidak seharusnya dikongsi. Sesiapa yang mempunyai akses kepadanya boleh menggunakan kredit OpenRouter anda" + Had kredit OpenRouter telah dicapai + Had kredit OpenRouter anda telah dicapai. Tambah nilai baki anda atau tingkatkan had perbelanjaan kunci anda di OpenRouter.ai + Buka OpenRouter.ai + Terjemahan OpenRouter gagal (HTTP %s) + Had harian MyMemory telah dicapai + Seterusnya tersedia pada %s + Anda telah menggunakan kuota terjemahan percuma MyMemory hari ini (5,000 aksara). Tambah e-mel MyMemory dalam tetapan untuk meningkatkan had harian kepada 50,000 aksara, atau tukar ke perkhidmatan terjemahan yang lain + Anda telah menggunakan kuota terjemahan MyMemory hari ini (50,000 aksara). Tukar ke perkhidmatan terjemahan yang lain atau cuba lagi esok + Terjemahan MyMemory gagal (status %s) + Dayakan gelung video + Jangan ingat video gelung + Halang keadaan semasa video gelung daripada disimpan untuk video seterusnya + Tunjukkan butang \'Ulang video\' + Gelung video diaktifkan + Ulang video dimatikan + Julat gelung + Mula + Tamat + [hh:]mm:ss + [hh:]mm:ss (kosong = akhir video) + Tetapkan + Kosongkan + Mengulang %1$s - %2$s + Julat masa tidak sah + Tunjukkan butang Gambar dalam Gambar + Tunjukkan menu Gambar dalam Gambar + Tukar ke Gambar-dalam-Gambar + Tunjukkan butang \'Mainkan semua\' + "Ketik untuk menjana senarai main semua video daripada saluran, ketik dan tahan untuk batalkan + +Info: Mungkin tidak berfungsi pada strim langsung" + Mod jana senarai main + Semua kandungan (Isih mengikut masa, Menaik) + Semua kandungan (Isih mengikut masa) + Semua kandungan (Isih mengikut populariti) + Video sahaja (Isih mengikut masa) + Video sahaja (Isih mengikut populariti) + Shorts sahaja (Isih mengikut masa) + Shorts sahaja (Isih mengikut populariti) + Video penstriman sahaja (Isih mengikut masa) + Video penstriman sahaja (Isih mengikut populariti) + Kandungan Ahli sahaja + Video Ahli sahaja + Shorts Ahli sahaja + Penstriman langsung Ahli sahaja + Tidak dapat menghasilkan senarai main kerana ketidakpadanan ID saluran + Tunjukkan butang \'Muat semula video\' + ID Video tidak tersedia. Sila muat semula video secara manual + Tunjukkan butang Simpan ke Tonton kemudian + Ketik untuk simpan ke Tonton kemudian, ketik dan tahan untuk pilihan giliran + Video berjaya disimpan dalam senarai main tonton kemudian + Video ini sudah disimpan dalam senarai main tonton kemudian + Tidak dapat menyimpan video ini dalam senarai main tonton kemudian + Simpan ke Tonton kemudian + Tunjukkan butang Simpan untuk Tonton kemudian dalam flyout tetapan pemain + Membolehkan penyimpanan video yang sedang ditonton dalam \'Tonton kemudian\' daripada flyout tetapan video + Tunjukkan butang Simpan ke Tonton kemudian dalam video kanak-kanak + Membolehkan penyimpanan video berkaitan kanak-kanak dalam \'Tonton kemudian\' daripada menu flyout + Tunjukkan butang Simpan untuk Tonton kemudian dalam flyout shorts + Membolehkan penyimpanan shorts dalam \'Tonton kemudian\' daripada menu flyout + Alihkan Tonton kemudian ke senarai main + Tonton kemudian + Tambah ke giliran Morphe + Kekalkan giliran antara sesi + Giliran dikekalkan selepas memulakan semula aplikasi + Tukar tindakan Simpan dan Antre + Ketik untuk pilihan baris gilir, ketik dan tahan untuk simpan ke Tonton kemudian + Jenis mainkan seterusnya dalam baris gilir Lalai + Baris gilir asli + Baris gilir Morphe + Baris gilir Morphe sebelah klien + Tambah item menu giliran flyout suapan + Menambah item flyout tambahan. Dayakan ini jika \"Tambah ke giliran\" tidak dipaparkan + Barisan + Barisan (%s) + Kosongkan semua + Tiada dalam barisan + Gunakan Main seterusnya dalam giliran pada mana-mana video + Sedang dimainkan + Main seterusnya dalam giliran + Main terakhir dalam giliran + Susun semula Keluarkan + Ditambah ke giliran + Tidak dapat menambah video ini ke dalam giliran + Senarai main tidak boleh ditambah ke dalam giliran + Giliran sudah penuh + Tidak dapat memulakan video + Senarai putih iklan dalam menu flyout + Menambah item flyout yang membenarkan iklan dimainkan pada saluran video tersebut + Ciri ini mungkin memerlukan penggunaan PotHelper atau menukar \"Spoof video streams\" kepada log masuk TV atau Android VR + Senarai putih kelajuan dalam menu flyout + Menambah item flyout yang sentiasa memainkan saluran video tersebut pada kelajuan 1.0x + Tidak log masuk + Tiada ID senarai main + Giliran kosong + Tiada ID video + Gagal: %s + Gagal untuk menambah ke giliran + Gagal untuk mencipta giliran + Gagal untuk mengalih keluar daripada giliran + Gagal untuk menyimpan giliran + Ditambah ke giliran + Giliran dicipta + Dialih keluar daripada giliran + Giliran disimpan ke %s Baris gilir Morphe + Tambah ke giliran + Tambah ke giliran dan buka + Tambah ke giliran dan main + Tambah dan muat semula video + Alih keluar daripada giliran + Alih keluar dan buka giliran + Alih keluar dan muat semula video + Buka giliran + Simpan giliran ke senarai main + Senarai main disimpan + Disimpan ke %s + Kosongkan baris gilir + Barisan telah dikosongkan + Baris gilir + Main terakhir dalam giliran + Mainkan seterusnya dalam barisan + YouTube tidak dapat memaparkan barisan. + Barisan tidak sedia untuk video ini. Buka pemain video dan cuba lagi. + Tambah Mix ke barisan atau tonton Mix? + Tonton + Tambah ke barisan + Memuatkan Mix… + Buka saluran daripada avatar langsung + Menghalang strim langsung daripada dibuka apabila mengetik avatar saluran + Nyahaktifkan gerak isyarat pencarian tepat + Nyahaktifkan mod pencarian tepat yang diaktifkan apabila menarik ke atas bar pencarian + Dayakan DVR strim langsung + Membenarkan carian pada semua strim langsung, termasuk strim yang biasanya menyekatnya + Dayakan gambar kenit seekbar + Menunjukkan gambar kenit video semasa menyeret seekbar + Paling banyak dimainkan semula + "'Dayakan DVR strim langsung' mungkin tidak tersedia + +Untuk membetulkannya, hidupkan 'Spoof strim video' dan tukar 'Klien lalai' kepada 'TV Simply' atau 'visionOS'" + Luaskan tempoh DVR strim langsung + Membolehkan anda mengundur siaran langsung sehingga 7 hari ke belakang + "'Kembangkan tempoh DVR strim langsung' mungkin tidak tersedia + +Untuk membetulkannya, hidupkan 'Spoof video streams' dan tukar 'Klien lalai' kepada 'TV Simply' atau 'visionOS'" + Dayakan gelongsor untuk cari + Aktifkan luncur untuk mencari dan bukannya memainkan pada kelajuan 2x apabila menekan dan menahan pemain video + Dayakan ketik untuk cari + Hanya ketik bar pencarian untuk melompat ke kedudukan tersebut tanpa menyeretnya + Gerakan leret kiri + Gerakan leret kanan + Gerak isyarat leret atas + Lebar zon Kiri/Kanan + Peratusan bagi setiap sisi skrin yang disediakan untuk gerak isyarat leret kiri dan kanan (5-50) Kecerahan Kelantangan Asli Mati + Abaikan leretan apabila dikunci + Gerakan leret diabaikan semasa pemain dikunci dengan \'Kunci skrin\' Dayakan gerak isyarat tekan untuk leret + Memerlukan gerakan tekan dan tahan sebelum gerak isyarat leret dikenal pasti Dayakan maklum balas sentuhan Simpan dan pulihkan kecerahan Ingat tahap kecerahan apabila masuk/keluar daripada mod skrin penuh @@ -179,192 +746,912 @@ Second \"item\" text" Leret warna tindihan atas kecerahan Tetapkan warna bar kemajuan kawalan kecerahan Leret warna tindihan atas kelantangan + Tetapkan warna bar kemajuan kawalan volum + Leret warna tindihan atas kelajuan + Tetapkan warna bar kemajuan kawalan kelajuan main balik + Saiz teks tindihan leret + Laraskan saiz teks pada skrin (1-30) + Ambang magnitud leretan + Tetapkan jarak leretan yang diperlukan sebelum pelarasan dicetuskan + Sensitiviti leretan volum + Nilai yang lebih tinggi memerlukan lebih banyak leretan bagi setiap langkah volum + Langkah volum + Penguatan volum + Meleret ke atas melepasi volum maksimum akan menguatkannya, yang boleh menyebabkan herotan audio yang kuat + Lalai peranti + Kepekaan leretan kecerahan + Nilai yang lebih tinggi memerlukan lebih banyak leretan bagi setiap langkah kecerahan + Kepekaan leretan kelajuan + Nilai yang lebih tinggi memerlukan lebih banyak leretan bagi setiap langkah kelajuan + Langkah kelajuan + Ketinggian zon kelajuan + Ketinggian zon isyarat kelajuan sebagai peratusan ketinggian pemain (5-75) + Kelajuan + Gaya tindihan leret + Tindihan mendatar + Tindihan mendatar (minimal - atas) + Tindihan mendatar (minimal - tengah) + Pertindihan bulat + Pertindihan bulat (minimalis) + Pertindihan menegak + Pertindihan menegak (minimalis) + Dayakan leret untuk tukar video + Leret dalam mod skrin penuh untuk melangkau ke video seterusnya atau sebelumnya + Butang tindakan + Pilih butang tindakan yang muncul dalam mod potret dan skrin penuh + Butang potret + Nyahdayakan cahaya Like dan Subscribe + Menghalang butang Suka dan Langgan daripada bercahaya apabila pencipta menyebutnya + Sorokkan bar tindakan + Sorokkan butang Suka dan Tidak Suka + Sorokkan butang Kongsi + Sorokkan butang \'Hentikan iklan\' + Sorokkan butang Komen Sorokkan butang \'Sambung\' + Sorokkan butang Laporkan + Sorokkan butang Remix + Sorokkan butang Muat turun + Sorokkan butang Hype + Sorokkan butang Promosikan + Sorokkan butang Terima Kasih + Sorokkan butang Tanya + Sorokkan saluran dan butang Langgan + Menyembunyikan avatar saluran dan butang Langgan pada permulaan bar tindakan + Sorokkan butang Langgan + Menyembunyikan butang Langgan dan Sertai bersebelahan avatar saluran pada permulaan bar tindakan + Sorokkan butang Klip Ini mungkin tidak berfungsi untuk sesetengah akaun pengguna + Sorokkan butang Kedai + Sorokkan butang Simpan Sorokkan butang \'Lebih banyak\' + Butang skrin penuh + Sorokkan butang Tidak suka + Sorokkan butang Suka + Sorokkan butang \'Sembang langsung\' + Sorokkan butang Mix + Sorokkan butang \'Lebih banyak video\' + Sorokkan butang Senarai main + Margin atas tindakan pantas + Tetapkan margin atas bar tindakan pantas (0-32 dp) + Sarikata + Urus sari kata automatik dan kuki sari kata + Keterlihatan sari kata automatik + Sentiasa tunjuk + Jangan tunjukkan + Tunjuk apabila tidak disenyapkan + Tunjukkan apabila disenyapkan + Bahasa sari kata pilihan + Memilih sari kata pembekal terlebih dahulu, kemudian menterjemah secara automatik ke bahasa pilihan Mati + Tambah bahasa terjemahan automatik tambahan + Menambah lebih banyak bahasa ke menu terjemahan automatik + Tetapkan kuki sari kata + "Memperbaiki pepijat stok YouTube di mana sarikata terjemahan automatik mungkin tidak tersedia disebabkan kuki yang hilang + +'Kuki sarikata' mesti disediakan" + Kuki sarikata + Kuki untuk ditetapkan dalam permintaan API Teks Bertimed (API Data Sari Kata) + Dapatkan kuki sarikata + Klik di sini untuk mengambil kuki + Gagal mengambil kuki + Kuki sudah wujud + Kuki berjaya dikemas kini + Betulkan transkrip + Menyelesaikan kegagalan pemeriksaan prasyarat yang boleh menghalang transkrip daripada dimuatkan + Tunjukkan kapsyen dalam gambar-dalam-gambar + Butang pandu arah Memilih butang yang akan muncul pada bar pandu arah bawah Sorokkan butang \'Laman Utama\' + Sorokkan butang Shorts + Sorokkan butang Cipta + Sorokkan butang Langganan + Sorokkan butang Pemberitahuan + Tunjukkan butang Carian + Kedudukan butang Carian + Tunjukkan butang Tetapan + Kedudukan butang Tetapan + Tindakan butang Tetapan + Membuka tetapan Morphe apabila didayakan, atau tetapan YouTube apabila dinyahdayakan + Tukar butang Cipta dengan butang Pemberitahuan Jika mengubah tetapan ini tidak berkesan, cuba tukar ke mod penyamaran Sorokkan label butang pandu arah + Sorokkan titik kandungan baharu Sorokkan bar pandu arah + "Ini menyembunyikan bar navigasi sepenuhnya, termasuk tab 'Anda'. + +Untuk mengakses Tetapan, dayakan butang Tetapan melalui Umum → Bar Alat → Tunjukkan Tetapan" + Dayakan butang navigasi sempit + Mengurangkan jarak mendatar antara butang + Nyahaktifkan butang pandu arah tanpa label + Menunjukkan butang pandu arah dengan label, jika YouTube menghantarnya tanpa label + Nyahaktifkan tab navigasi bar alat + Memaparkan Langganan dalam bar navigasi, jika YouTube memindahkannya ke tab di bahagian atas Laman Utama + Dayakan animasi bar pandu arah + Menganimasikan peralihan apabila bertukar antara tab + Nyahdayakan sorokkan bar pandu arah secara automatik + Kekalkan bar navigasi kelihatan semasa menatal + Gaya bar pandu arah Lalai + Lut sinar separa + Lut sinar + Legap + Gaya ini mungkin tidak disokong oleh peranti atau versi YouTube anda, dan boleh menyebabkan masalah susun atur atau kegagalan fungsi + Bar alatan + Pilih komponen yang akan muncul pada bar alatan + Sorokkan butang Siaran + Sorokkan butang Sembang + Sorokkan butang Mikrofon + Sorokkan butang carian + Apabila didayakan, ketik untuk membuka tetapan Morphe dan tekan lama untuk membuka tetapan YouTube, nyahdayakan untuk tukar + Menu flyout + Pilih komponen yang akan muncul pada menu flyout + Sembunyikan menu \'Tetapan tambahan\' / Lagi + Sembunyikan menu \'Mod ambien\' + "Menu 'Audio Track' disembunyikan + +Untuk memaparkan menu 'Audio Track', tukar 'Spoof video streams' kepada mana-mana klien kecuali 'Android Studio' dan 'Android VR Downgraded'" + Sembunyikan menu \' trek audio\' + Sembunyikan footer menu \'Jejak Audio\' + Sembunyikan menu Sari kata + Sembunyikan footer menu Sarikata + Sembunyikan header menu Sarikata + Sembunyikan menu \'Bantuan & maklum balas\' + Sembunyikan menu \'Dengar dengan YouTube Music\' + Sembunyikan menu \'Kunci skrin\' + Sembunyikan menu \'Ulang video\' + Sorokkan menu \'Sambil bergerak\' + Sembunyikan menu Kualiti + Sembunyikan footer menu Kualiti + Sembunyikan header menu Kualiti + Sembunyikan menu \'Pemasa tidur\' + Sembunyikan menu \'Kelajuan main balik\' + Sembunyikan menu \'Volum stabil\' + Sembunyikan menu \'Tonton dalam VR\' + Sorokkan pratonton main automatik + Menyembunyikan pratonton video akan datang dalam skrin penuh sebelum autoplay bermula + Butang tindanan + Pilih butang yang akan muncul di atas pemain video + Sorokkan butang Main Automatik + Sorokkan butang Sari kata + Sorokkan butang siaran + Sorokkan butang Kecilkan + Sorokkan butang Skrin penuh + Sorokkan latar belakang kawalan pemain + Menyembunyikan lorekan gelap di sekeliling kawalan pemain video + Kelegapan latar belakang butang kawalan + Laraskan kelegapan latar belakang di belakang butang kawalan pemain dan bar masa (0 tidak kelihatan, 100 legap) + Sorokkan butang Sebelumnya & Seterusnya + Sorokkan butang Tetapan + Sorokkan kad skrin akhir + Mod ambien + Urus kebolehlihatan mod Ambien dan sekatan kuasa + Pintas sekatan mod Ambient + Membenarkan mod Ambien kekal aktif dalam mod penjimat bateri + Nyahaktifkan mod Ambient + Nyahaktifkan mod Ambien dalam skrin penuh + Sorokkan kad info + Nyahaktifkan animasi nombor bergulir + Menghentikan animasi pengiraan pada jumlah suka dan jumlah tontonan + Sorokkan seekbar pemain video + Sorokkan seekbar kenit video + Sembunyikan bar kemajuan tontonan yang dipaparkan pada imej kecil video yang telah anda tonton + Dayakan seekbar besar skrin penuh + Sembunyikan Shorts + Sorokkan dalam halaman saluran + Sorokkan dalam suapan Utama & video berkaitan + Sorokkan dalam hasil carian + Sorokkan dalam suapan Langganan + Sorokkan dalam penerangan video + Sorokkan dalam sejarah tontonan + Pemain Shorts + Pilih komponen yang muncul dalam pemain Shorts + Matkan ketik dua kali untuk suka + Sorokkan butang AI + Sorokkan label \'Alih suara automatik\' + Sorokkan bar saluran + Sorokkan butang Komen + Sorokkan butang Kesan + Sorokkan label pautan video + Sembunyikan petunjuk gerak isyarat + Sorokkan butang \'Skrin hijau\' + Sorokkan butang Hashtag + Sorokkan panel maklumat + Sorokkan butang Sertai + Sorokkan butang Suka + Sorokkan animasi pancutan butang Suka + Sorokkan pratonton langsung + Sorokkan label lokasi Sorokkan bar pandu arah + Sorokkan butang \'Hantaran baharu\' + Sorokkan butang overlay jeda + Sorokkan komen pratonton + Sorokkan butang Remix + Sorokkan butang Simpan + Sorokkan butang \'Simpan muzik\' + Sorokkan cadangan carian + Sorokkan butang Kongsi + Sorokkan butang Kedai + Sorokkan butang Bunyi + Sorokkan label metadata bunyi + Sorokkan butang Langgan + Sorokkan butang \'Beli Super Thanks\' + Sorokkan produk yang ditag + Sorokkan butang Akan Datang + Sorokkan butang \'Gunakan bunyi ini\' + Sorokkan butang \'Gunakan templat ini\' + Sorokkan tajuk video + Sorokkan video yang dicadangkan pada skrin akhir + "Menyembunyikan tindihan video yang dicadangkan yang muncul selepas video berakhir + +Tetapan ini hanya berfungsi jika main automatik dimatikan. Anda boleh mengubahnya dalam YouTube: +Tetapan → Main balik (atau Main automatik) → Main automatik video seterusnya" + Sorokkan video berkaitan + Sembunyikan hamparan \'Lagi video\' + Menyembunyikan hamparan \'Lagi video\' yang dipaparkan apabila meleret ke atas dalam mod skrin penuh + Sorokkan penanda masa video + Nyahdayakan panel timbul pemain + Menyahaktifkan panel senarai main dan sembang langsung daripada dibuka secara automatik + Nyahaktifkan putar automatik senarai main + Menghalang senarai main daripada beralih ke video seterusnya secara automatik + Nyahaktifkan gesaan \'Teruskan menonton\' + Keluar daripada mod skrin penuh apabila video tamat Dilumpuhkan + Potret sahaja + Landskap sahaja + Potret dan landskap + Paksa landskap dalam skrin penuh + Putar pemain ke landskap apabila memasuki skrin penuh. Tidak tersedia pada skrin kecil, yang sudah membuka skrin penuh dalam landskap + Gerak isyarat skrin penuh + Gerakan pemain dalam mod skrin penuh + Nyahdayakan gerak isyarat leret ke atas potret + Nyahdayakan gerak isyarat leret ke bawah potret + Nyahdayakan gerak isyarat leret ke bawah landskap + Nyahaktifkan gerakan cubit-untuk-zum + Pulihkan cubit untuk zum Buka video dalam mod skrin penuh Dilumpuhkan Potret Landskap + Skala video skrin penuh Lalai + Regang (isi skrin) + Zum (potong untuk isi) + Tunjukkan butang skala video + Ketik untuk kitaran Lalai, Regang, dan Zum + Tunjukkan dalam skrin penuh sahaja + Sembunyikan butang skala video dalam pemain potret + Tunjukkan butang Senyapkan + Kelegapan tindanan pemain Melaraskan tahap kelutsinaran (0 ialah lut sinar, 100 ialah legap) + Bar carian Dilumpuhkan + Lebar + Ekstra lebar + Reka letak peranti Lalai + Telefon + Tablet + Automotif + "Perubahan termasuk: + +Tata letak tablet +• Hantaran komuniti disembunyikan +• Tetapan Main balik dalam suapan tidak tersedia +• Butang Remix dan butang Bunyi tidak tersedia dalam Shorts +• Hamparan 'Lebih banyak video' mungkin tidak tersedia +• Bar tindakan video lama yang tidak dikecilkan dipaparkan +• Item menu flyout mungkin tidak berfungsi dengan betul + +Tata letak automotif +• Suapan disusun mengikut topik dan saluran +• Tetapan Main balik dalam suapan tidak tersedia +• Butang Remix dan butang Bunyi tidak tersedia dalam Shorts +• Hamparan 'Lebih banyak video' mungkin tidak tersedia +• Shorts dibuka dalam pemain biasa +• Bar tindakan video lama yang tidak dikecilkan dipaparkan" Dayakan tata letak tablet di pemain + "Memulihkan tataletak pemain lama + +Keterbatasan: Jika 'Memulihkan bar tindakan video lama' dimatikan, tindihan atas 'Lebih banyak video' mungkin tidak tersedia" + 21.11.48 - Pulihkan tingkah laku sari kata lama + 20.39.41 - Pulihkan simpanan lama ke senarai main + 20.28.41 - Nyahaktifkan ikon tebal + 20.13.41 - Pulihkan nama saluran di bawah pemain video + Buka helaian kongsi sistem + Membuka helaian perkongsian sistem dan bukannya helaian dalam apl apabila berkongsi kandungan + Atasi butang YouTube Music + Membuka aplikasi muzik pilihan anda berbanding aplikasi stok YouTube Music + Nama pakej aplikasi muzik + Nama pakej aplikasi muzik yang anda pasang Laman mula Lalai + Semua langganan + Semak saluran + Kursus / Pembelajaran Teroka + Fesyen & Kecantikan + Permainan Sejarah Pustaka + Video yang disukai + Langsung + Filem + Muzik + Berita + Pemberitahuan Senarai main Carian + Beli-belah Shorts + Sukan Langganan + Tular + Realiti Maya + Tonton kemudian + Klip anda + Nyahaktifkan sambung semula semasa permulaan + Buka Shorts dengan + Player Shorts + Player biasa + Player biasa skrin penuh + Main automatik Shorts + Berpindah ke Short seterusnya secara automatik dan bukannya mengulang Short semasa + Autoplay di latar belakang + Beralih ke Short seterusnya secara automatik semasa main balik latar belakang + Langkau Shorts langsung + Melangkau penstriman langsung yang muncul dalam suapan Shorts + Main balik dalam suapan + Sentiasa aktif + Wi-Fi sahaja Mati + Pulihkan tajuk asal + Menunjukkan tajuk video, penerangan video dan penerangan saluran yang asal dan bukannya yang diterjemah secara automatik + Memuatkan tajuk… + Nyahaktifkan penyegaran suapan automatik + Menghalang suapan daripada disegarkan secara automatik selepas ia menjadi lapuk + Tata letak kad suapan + Klasik + Kad + Kad dengan butang suka, komen dan simpan + Komen + Nyahaktifkan sambung semula pemain mini + Nyahdayakan miniplayer \'Teruskan menonton\', ciri YouTube Premium yang muncul semasa aplikasi bermula + Miniplayer + Sesuaikan rupa dan kelakuan pemain video yang dikecilkan + Jenis pemain mini + Sorokkan tajuk dan saluran Dilumpuhkan Lalai + Minimalis + Minimal 2 + Tablet + Moden 1 + Moden 2 + Moden 3 + Moden 4 + Nyahaktifkan bucu bulat + Nyahaktifkan seret dan lepas + Menghalang pemain yang dikecilkan daripada ditarik ke penjuru skrin yang berbeza + Nyahaktifkan gerak isyarat seret mendatar + Menghalang penutupan pemain mini dengan meleretkannya secara mendatar keluar dari skrin + Jeda main balik dengan seretan mendatar + Main balik akan dijeda apabila pemain mini ditarik keluar skrin + Ingat kedudukan keadaan seretan + Menarik pemain mini keluar skrin akan mengekalkan keadaan UI pemain tersebut + Sorokkan butang tindihan + Saiz lalai + Tetapkan saiz permulaan pada skrin, dalam piksel + Saiz piksel mestilah antara %1$s dan %2$s + Kelegapan tindihan Melaraskan tahap kelutsinaran (0 ialah lut sinar, 100 ialah legap) + Butang kembali keluar dari suapan + Menekan kembali sentiasa keluar dari aplikasi dan bukannya skrol ke bahagian atas suapan + Paksa animasi Lalai + Sentiasa hidup + Sentiasa mati + Sorokkan bar status + Sembunyikan bar status sistem. Leret ke bawah dari tepi atas untuk memaparkannya seketika + Betulkan bar status yang menutup bar atas + Kekalkan susun atur bar status agar konsisten, supaya ia tidak menutup bar atas dan tidak meninggalkan ruang kosong di atas pemain. Matikan fungsi ini jika aplikasi terhenti atau bar atas kelihatan tidak betul + Bercahaya + Skrin percikan Glow tidak disokong oleh peranti ini Light theme background White + Material You (Putih) + Merah jambu muda + Biru muda + Hijau muda + Kuning muda + Oren muda + Merah muda + Latar belakang cerah tersuai + Tetapkan warna latar belakang tema cerah + Tukar warna latar hadapan + Gunakan warna tema untuk teks dan ikon + Dayakan skrin pemuatan gradien + Memaparkan latar belakang kecerunan pada skrin pemuatan berbanding warna pepejal + Dayakan warna seekbar tersuai + Warna seekbar + Tetapkan warna utama bar kemajuan video + Warna aksen seekbar + Tetapkan warna sekunder bar kemajuan video + Nilai warna seekbar tidak sah + Analog asal + Kek asal + Logam asal Logo pengepala Lalai + Biasa Tersuai + Pintas sekatan wilayah imej + Menghalakan permintaan gambar kecil ke pelayan alternatif untuk membaiki imej yang hilang di negara yang disekat + Tajuk DeArrow + Tab Utama + Tab Langganan + Tab Anda + Senarai main & syor pemain + Hasil carian + Tunjukkan ikon DeArrow + Menunjukkan ikon DeArrow pada tajuk video crowdsourced + Mod santai + Menunjukkan tajuk asal dan bukannya tajuk DeArrow jika pengguna DeArrow mengundi bahawa mereka menyukai tajuk asal + Undian santai minimum untuk menunjukkan tajuk asal + Bilangan undian santai dalam satu kategori, seperti lucu atau kreatif, yang diperlukan untuk menunjukkan tajuk asal + Thumbnail DeArrow + Thumbnail asal + Thumbnail DeArrow & Asal + DeArrow & Tangkapan pegun + Tangkapan pegun + "DeArrow menggantikan tajuk dan imej kenit clickbait dengan tajuk hasil sumber ramai yang lebih tepat dan kurang sensasi. Tajuk ditulis dan diundi oleh komuniti, dan imej kenit adalah bingkai yang diambil daripada video + +Video diminta melalui awalan pendek ID video yang dihash, jadi pelayan tidak dapat mengenal pasti dengan tepat video yang anda lihat. Jika video tidak mempunyai imej kenit DeArrow, imej asal atau tangkapan pegun akan dipaparkan" + Tunjukkan amaran ralat sambungan + Memaparkan pemberitahuan apabila pelayan DeArrow tidak tersedia + Titik akhir API + URL titik akhir cache imej kecil DeArrow + Tangkapan video pegun + Tangkapan pegun masih diambil dari permulaan / tengah / akhir setiap video. Imej ini terbina dalam YouTube dan tiada API luaran digunakan + Ekstrak bingkai video daripada + Permulaan + Tengah + Akhir + DeArrow tidak tersedia buat sementara waktu (kod status: %s) + DeArrow tidak tersedia buat sementara waktu + Pulihkan gaya butang pemain lama + Mengembalikan kawalan pemain video yang tebal kepada gaya visual lama yang lebih nipis + Tiru dimensi peranti + Mengaktifkan ini boleh membuka kunci kualiti video yang lebih tinggi + Mendayakan ciri ini boleh menyebabkan gangguan pada mainan balik video, jangka hayat bateri yang lebih teruk, dan kesan sampingan yang tidak diketahui + Maklum balas sentuhan + Pilih interaksi yang mencetuskan getaran peranti + Matikan haptik bab + Matikan haptik carian tepat + Matikan haptik undo carian + Nyahdayakan haptik ketik dan tahan + Matikan haptik zum + Jika anda baru sahaja menukar butiran log masuk akaun anda, sila nyahpasang dan pasang semula MicroG Memirau ubah hala pautan Membuka pautan asli secara terus, memirau pautan penjejakan (youtube.com/redirect) + Buka pautan dalam pelayar web + Membuka pautan dalam pelayar luaran dan bukannya pelayar dalam aplikasi + Saiz penimbal main balik Lalai + Rendah + Sederhana + Maksimum + Kelewatan video + Melewatkan video untuk memastikan ia segerak dengan audio yang lambat, seperti fon kepala Bluetooth (0-5000 ms) + Hanya lewatkan dengan audio Bluetooth + Hanya melewatkan video semasa audio Bluetooth disambungkan, supaya pembesar suara telefon kekal segerak + Tunjukkan butang kelewatan Video + Ketik dan tahan untuk menetapkan semula kelewatan kepada 0 ms + Tunjukkan menu kelewatan Video + Kelewatan dalam milisaat Automatik + Ingat kualiti video + Menggunakan pilihan kualiti manual anda untuk semua video masa hadapan + Notifikasi perubahan kualiti + Menunjukkan pemberitahuan apabila kualiti lalai baharu disimpan + Kualiti video lalai pada rangkaian Wi-Fi + Kualiti video lalai pada rangkaian mudah alih + Ingat kualiti Shorts + Gunakan pilihan kualiti manual anda untuk semua Shorts akan datang + Kualiti Shorts lalai pada rangkaian Wi-Fi + Kualiti Shorts lalai pada rangkaian mudah alih + Kualiti mudah alih lalai ditukar kepada: %s + Kualiti Wi-Fi lalai ditukar kepada: %s + Kualiti mudah alih Shorts ditukar kepada: %s + Kualiti Wi-Fi Shorts ditukar kepada: %s + Tunjukkan butang Kelajuan main balik + Ketik dan tahan untuk tetapkan semula kelajuan ke lalai + Tunjukkan butang Kualiti + Ketik dan tahan untuk tetapkan semula kualiti ke lalai + Tunjukkan resolusi pada butang Kualiti + Tunjukkan resolusi, seperti 1080, berbanding label seperti FHD + Menu kelajuan main balik tersuai + Pulihkan menu kelajuan main balik lama + Kelajuan main balik tersuai + Masukkan pilihan kelajuan main balik pilihan anda + Kelajuan main balik tersuai mestilah kurang daripada %s + Kelajuan main balik tersuai tidak sah Automatik + Ketik dan tahan kelajuan main balik + Tetapkan pengganda kelajuan apabila menekan dan menahan skrin, tetapkan kepada 0 untuk menyahaktifkan + Ingat kelajuan main balik + Menggunakan pemilihan kelajuan manual anda untuk semua video akan datang + Notifikasi perubahan kelajuan + Menunjukkan pemberitahuan apabila kelajuan main balik lalai baharu disimpan + Kelajuan main balik lalai + Kelajuan main balik lalai pada Shorts + Ingat kelajuan main balik pada Shorts + Gunakan pilihan kelajuan Shorts manual anda untuk semua Shorts akan datang. Jika dimatikan, kelajuan akan kembali ke lalai selepas menutup pemain Shorts + Tunjukkan kelajuan Main balik dalam menu Shorts + Menambah item kelajuan Main balik kepada menu pemain Shorts + Kelajuan main balik · %s + Kelajuan main balik lalai ditukar kepada: %s Gunakan kelajuan biasa untuk muzik Sentiasa mainkan video muzik pada kelajuan 1.0x + Senarai putih kelajuan saluran + Video daripada saluran dalam senarai putih sentiasa dimainkan pada kelajuan 1.0x + Tambah saluran ke senarai putih kelajuan + Alih keluar saluran daripada senarai putih kelajuan + Dayakan kawalan pic audio + Pic audio main balik lalai + Pic audio lalai telah diubah kepada: %s + Regangan masa audio + Membenarkan penetapan kelajuan dan pic secara berasingan, mungkin menyebabkan artifak audio + Ingat kedudukan main balik strim langsung + Untuk strim langsung yang sedang berlangsung, mengingati dan menyambung semula kedudukan main balik anda dan bukannya mencari kedudukan langsung semasa + Ciri ini tidak berfungsi dengan penstriman langsung 24/7 dan penstriman lain yang \'sentiasa aktif\' + Nyahaktifkan video HDR + Paksa video HDR + Memaksa aplikasi untuk mengecam peranti ini sebagai mampu HDR + "Gunakan ini jika penyahkodan HDR berfungsi tetapi YouTube tidak menawarkan HDR, seperti beberapa peranti HyperOS yang menyembunyikan HLG. + +Had: +• Warna mungkin kelihatan salah jika paparan tidak benar-benar boleh menunjukkan HDR +• Main balik mungkin gagal jika penyahkod tidak dapat mengendalikan HDR +• HDR masih bergantung pada klien penyamaran strim video" + Kod video Lalai + Untuk memaksa AVC, tukar \'Spoof strim video\' kepada \'Android Studio\', \'Android VR\' atau \'visionOS\' + "VP9 +• Boleh meningkatkan jangka hayat bateri dan membetulkan gangguan main balik +• Menggunakan lebih banyak data internet berbanding AV1 + +AVC (H.264) +• Memerlukan klien penyamaran strim video 'Android Studio', 'Android VR', atau 'visionOS' +• Boleh meningkatkan jangka hayat bateri dengan lebih banyak +• Boleh memulihkan resolusi video yang hilang pada peranti lama +• Resolusi maksimum ialah 1080p +• Video HDR mungkin tidak menggunakan AVC +• Sesetengah peranti tidak boleh memaksa AVC" + Buka menu kualiti Lanjutan + Melangkau menu pemilihan kualiti dan membuka menu kualiti Lanjutan secara terus + Sorokkan pilihan kualiti Premium + Utamakan kualiti video + Mengutamakan kualiti video tertinggi yang tersedia berbanding kecekapan codec + Jika tetapan ini dimatikan, video lama mungkin tidak menunjukkan pilihan kualiti video + Benarkan \'Android VR\' / visionOS AV1 + Membenarkan penyahkodan perisian AV1 apabila menggunakan klien \'Android VR\' / visionOS + "Mengaktifkan tetapan ini mungkin menggunakan penyahkodan perisian AV1. + +Playback video dengan AV1 mungkin tersekat-sekat atau mengalami keguguran bingkai" + Kesan sampingan penyamaran + • Main balik mungkin gagal pada minit ke-1 + • Menu \'Jejak Audio\' hilang + • Tiada codec video AV1 + • Mod imersif VR 360° tidak tersedia + • Volum stabil tidak tersedia + • Mainan balik mungkin berakhir 1 saat lebih awal + • Jika penyahkaburan gagal, mainan balik mungkin gagal + • Paksa audio asal tidak tersedia + • Log masuk ke Android VR diperlukan + • Kualiti mungkin terhad kepada 720p + Varian JavaScript Pemain + Tunjukkan penyamaran dalam \'Statistik untuk pakar\' Pulihkan bar tindakan video lama + "Memperbaiki butang tindakan video tidak boleh disorokkan + +Keterbatasan: Mengembalikan kepada tindihan atas gaya lama bagi 'Lebih banyak video'" + Gaya ikon Pemain Automatik Lalai + Nipis + Tebal Tersuai + Gaya ikon Shorts + Penjejak Siri + Ikuti senarai main sebagai siri dan kekalkan kedudukan anda + Dayakan penjejakan siri + Menambah halaman Siri ke Sejarah dan pilihan Ikuti siri ke senarai main + Tunjukkan Siri dalam bar navigasi + Tunjukkan butang Penjejak Siri + Menambah butang pemain untuk mengikuti atau membuka siri video semasa + Rekod kemajuan siri yang diikuti + Rekod main balik pada telefon ini untuk siri dan penanda buku yang disimpan + Batal + Simpan + Simpan penanda buku + Nama siri + Pilihan siri + Pilihan perpustakaan + Segarkan episod + Namakan semula + Buka senarai main dalam YouTube + Alih keluar daripada perpustakaan + Pautan atau ID senarai main + Kosongkan semua kemajuan tempatan + Kosongkan sejarah + Kosongkan semua kemajuan tempatan? + Ini akan mengosongkan kedudukan, override tontonan dan pilihan sambung semula untuk setiap video. Siri yang disimpan dan katalognya tetap kekal. Playback baharu akan direkodkan jika rakaman diaktifkan + Tanda telah ditonton + Tanda belum ditonton + Gunakan status automatik + Mulakan semula dari 0:00 + Main semula dari 0:00 + Status disimpan + Cuba simpan semula + 100 sebelumnya + 100 seterusnya + Buka video yang disimpan + Kemajuan tempatan telah dikosongkan + Buka Sejarah daripada Anda untuk melihat siri anda + Sejarah tontonan + Siri + Memuatkan siri yang disimpan… + Ikuti senarai main untuk menjejaki episodnya + Penanda buku senarai main + Menyegarkan episod… + Semua siri + Ditonton + Memuatkan episod… + Lebih banyak pilihan + "Rekod kemajuan hanya untuk siri yang disimpan dan video yang ditandakan anda. + +Perpustakaan ini disimpan pada peranti ini dan dikongsi merentasi akaun YouTube. Kawalan jeda dan kosongkan sejarah YouTube tidak mengubahnya. Gunakan Siri → Lebih Lanjut → Kosongkan kemajuan tempatan untuk memadamkannya. + +Rakaman automatik terhenti dalam mod inkognito, apabila identiti akaun tidak tersedia, atau apabila anda bertukar ke akaun yang berbeza. Aktifkan penjejakan semula untuk menggunakannya dengan akaun tersebut" + Penjejakan tidak boleh diaktifkan dalam mod inkognito atau sebelum YouTube memuatkan keadaan akaun semasa. Kembali ke Laman Utama dan cuba lagi. Log masuk tidak diperlukan + Dayakan penjejakan tempatan + Rakaman kemajuan dimatikan + Sambung semula pada %1$s + %1$s / %2$s episod ditonton + Episod %1$s · %2$s + Episod %1$s-%2$s daripada %3$s + Segarkan untuk cuba memuatkan episod. Senarai main peribadi memerlukan akses akaun; penanda buku mereka masih dibuka dalam YouTube + YouTube tidak dapat membuka episod ini + YouTube tidak dapat membuka senarai main ini + Pilih episod + Terkini + Tiada episod yang boleh dimainkan + Video disimpan · %1$s + Ikuti siri + Lihat siri Teruskan + Mula di sini + Titik permulaan disimpan + Telah ditonton sehingga sini + Episod ditandakan sebagai telah ditonton + Undo + Dayakan + 1 episod baharu + %1$s episod baharu + Rekod kemajuan pada peranti ini + "Siri dan kemajuan anda kekal pada peranti ini, dikongsi merentas akaun YouTube. Kawalan sejarah YouTube tidak mengubahnya. Kosongkan kemajuan daripada menu Siri. + +Rakaman automatik adalah pilihan. Ia terhenti dalam mod inkognito atau apabila akaun berubah" + Episod tidak dapat dimuatkan. Anda boleh menyimpan senarai main sebagai penanda buku + Terlalu banyak perubahan yang tidak disimpan. Cuba simpan semula terlebih dahulu + Proses menyimpan sedang sibuk. Cuba lagi + Pemuatan episod sedang sibuk. Cuba lagi + Storan tidak tersedia. Data anda yang disimpan telah dikekalkan + Perubahan tidak dapat disimpan. Cuba simpan semula sebelum menutup YouTube + Siri ini sudah disimpan, atau senarai mainnya telah berubah. Buka semula siri ini + Siri ini telah berubah atau dialih keluar. Buka semula pustaka + Senarai main telah berubah. Pilih episod sekali lagi + Episod ini tidak tersedia. Pilih episod lain + Masukkan nama siri + Masukkan pautan senarai main YouTube atau ID senarai main + Sila tunggu sebelum menyegarkan semula + Episod tidak dapat dimuatkan. Cuba lagi kemudian + Perubahan tidak dapat dilengkapkan. Cuba lagi + Sambungan semula tidak dapat disahkan. Semak pemain dan cuba lagi + Siri yang diikuti + Video ini tiada dalam senarai main ini + Mencari senarai main… + Tampal pautan senarai main + Tidak dapat mencari senarai main + Tiada episod tersedia + Sembunyikan yang telah ditonton + Tandakan episod sebelumnya sebagai telah ditonton + Bermula dengan: %1$s + Urutan episod + Urutan senarai main + Terbalikkan urutan senarai main + %1$s tontonan + %1$s tontonan + Tanda episod telah ditonton + Sekurang-kurangnya %1$s%% ditonton dengan maksimum 90 saat berbaki, atau %2$s saat berbaki + Tandakan episod sebagai telah ditonton secara automatik selepas mana-mana ambang dicapai. Peraturan peratusan juga memerlukan baki tidak lebih daripada 90 saat, supaya pengakhiran episod yang panjang tidak dilangkau. Peraturan masa berbaki terpakai untuk video yang lebih panjang daripada satu minit; tetapkan kepada 0 untuk menyahaktifkannya. Perubahan terpakai pada kemas kini kemajuan masa hadapan. Pilihan ditonton/tidak ditonton secara manual akan dikekalkan + Peratusan ditonton (1-100) + Saat berbaki (0-300; 0 untuk menyahaktifkan) + Masukkan nilai dari %1$s hingga %2$s + Gunakan kemajuan YouTube + Sambung siri yang diikuti selepas menonton pada peranti lain + Baca status ditonton dan kedudukan sambung semula untuk siri yang anda ikuti daripada akaun YouTube semasa. Hanya episod yang sepadan disimpan pada telefon ini. Semakan dijalankan semasa perpustakaan siri dibuka dan sebelum Sambung. Sejarah YouTube mesti diaktifkan pada peranti anda yang lain + Menyemak kemajuan YouTube… + Kemajuan YouTube tidak tersedia. Menggunakan kemajuan yang disimpan + Beberapa kemajuan YouTube tidak tersedia. Kedudukan yang disimpan dikekalkan + Sandaran dan pulihkan + Simpan perpustakaan siri anda ke dalam fail atau pulihkan sandaran + Menyimpan siri yang diikuti, kedudukan episod dan pilihan tontonan. Kebenaran rakaman dan kemajuan YouTube tidak disertakan + Simpan sandaran + Pulihkan sandaran + Tambah %1$s siri. %2$s siri yang sedia ada dalam perpustakaan anda dikekalkan. Kemajuan video sedia ada dikekalkan + Sandaran siri disimpan + %1$s siri telah dipulihkan + Fail ini bukan sandaran siri yang disokong, atau terlalu besar. Perpustakaan anda tidak berubah + Tidak dapat menyimpan sandaran. Cuba lokasi lain + Siri dialih keluar + Episod ini kini muncul lebih daripada sekali. Pilih yang mana satu untuk diteruskan + Episod yang anda simpan tidak lagi berada dalam senarai main ini. Kedudukan simpanan anda dikekalkan + Sambung dengan episod %1$s · %2$s + Anda juga boleh memilih mana-mana episod di bawah. Episod sebelumnya tidak berubah + Senarai main ini telah berubah. Buka siri untuk memilih tempat untuk menyambung + Tiada lagi episod dalam arah ini + Teruskan %1$s + Cari siri + Kosongkan carian + Tiada siri ditemui + Episod tanpa tajuk + Episod tidak tersedia + Komen Niconico + Tunjukkan sembang langsung dan ulangan sembang sebagai komen yang skrol merentasi video + Menunjukkan butang tindanan dalam pemain video. Ketik untuk menunjukkan atau menyembunyikan komen, ketik dan tahan untuk tetapan pantas + Saiz fon komen + Saiz teks komen (12-40 sp) + Kelegapan komen + Kelegapan teks dan latar belakang komen + Tempoh perjalanan + Berapa saat yang diambil oleh komen untuk melintasi skrin. Nilai yang lebih rendah bergerak lebih pantas + Panjang komen maksimum + Sembunyikan komen dengan aksara yang lebih banyak daripada ini. Setiap emoji dikira sebagai satu aksara + Tiada had diff --git a/patches/src/main/resources/addresources/values-my-rMM/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-my-rMM/shared-youtube/strings.xml index fd007eeb78..77635ba31c 100644 --- a/patches/src/main/resources/addresources/values-my-rMM/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-my-rMM/shared-youtube/strings.xml @@ -18,6 +18,7 @@ Second \"item\" text" --> + diff --git a/patches/src/main/resources/addresources/values-my-rMM/shared/strings.xml b/patches/src/main/resources/addresources/values-my-rMM/shared/strings.xml index 5ed2ec60d7..31cdbe4dcb 100644 --- a/patches/src/main/resources/addresources/values-my-rMM/shared/strings.xml +++ b/patches/src/main/resources/addresources/values-my-rMM/shared/strings.xml @@ -17,7 +17,6 @@ Second \"item\" text" --> - မ်ားလာေၞး diff --git a/patches/src/main/resources/addresources/values-my-rMM/youtube/strings.xml b/patches/src/main/resources/addresources/values-my-rMM/youtube/strings.xml index 70ce03fc67..4ec76b1150 100644 --- a/patches/src/main/resources/addresources/values-my-rMM/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-my-rMM/youtube/strings.xml @@ -31,6 +31,7 @@ Second \"item\" text" This button usually appears in the Subscriptions feed for future live streams or unreleased videos. --> + diff --git a/patches/src/main/resources/addresources/values-nb-rNO/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-nb-rNO/shared-youtube/strings.xml index d353fabb63..9c52d9b9df 100644 --- a/patches/src/main/resources/addresources/values-nb-rNO/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-nb-rNO/shared-youtube/strings.xml @@ -21,9 +21,8 @@ Second \"item\" text" Oppstartsanimasjon Deaktivert Farge - Farget Morphe Monokrom - Monokrom Morphe + Start på nytt for å fullføre oppsettet diff --git a/patches/src/main/resources/addresources/values-nb-rNO/youtube/strings.xml b/patches/src/main/resources/addresources/values-nb-rNO/youtube/strings.xml index ac1ef27c3c..226da9ac4c 100644 --- a/patches/src/main/resources/addresources/values-nb-rNO/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-nb-rNO/youtube/strings.xml @@ -113,6 +113,9 @@ Endringer inkluderer: Skjul undersøkelser Skjuler undersøkelser og oppfordringer om nøyaktigheten til mates anbefalinger Skjul billetthylle + + Skjul banneret \"Utilgjengelige videoer er skjult\" + Skjuler banneret \"Utilgjengelige videoer er skjult\" i spillelister Skjul opplastningstidspunkt "Begrensninger: @@ -1249,6 +1252,7 @@ Begrensning: Hvis 'Gjenopprett gammel videoaksjonslinje' er slått av, er det mu Holder utseendet til statuslinjen konsistent, slik at den ikke dekker topparene og etterlater ingen tom plass over avspilleren. Skru dette av hvis appen krasjer eller topparene ser feil ut Glød + Glow-sprutskjerm støttes ikke av denne enheten Bakgrunn for lyst tema Hvit Material You (Hvit) diff --git a/patches/src/main/resources/addresources/values-ne-rIN/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-ne-rIN/shared-youtube/strings.xml index fd007eeb78..77635ba31c 100644 --- a/patches/src/main/resources/addresources/values-ne-rIN/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ne-rIN/shared-youtube/strings.xml @@ -18,6 +18,7 @@ Second \"item\" text" --> + diff --git a/patches/src/main/resources/addresources/values-ne-rIN/youtube/strings.xml b/patches/src/main/resources/addresources/values-ne-rIN/youtube/strings.xml index 70ce03fc67..4ec76b1150 100644 --- a/patches/src/main/resources/addresources/values-ne-rIN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ne-rIN/youtube/strings.xml @@ -31,6 +31,7 @@ Second \"item\" text" This button usually appears in the Subscriptions feed for future live streams or unreleased videos. --> + diff --git a/patches/src/main/resources/addresources/values-nl-rNL/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-nl-rNL/shared-youtube/strings.xml index aecad96aef..a4cfc6a41c 100644 --- a/patches/src/main/resources/addresources/values-nl-rNL/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-nl-rNL/shared-youtube/strings.xml @@ -21,9 +21,9 @@ Second \"item\" text" Opstartanimatie Uitgeschakeld Kleur - Kleur Morphe Monochroom - Monochroom Morphe + + Uit Herstarten om de installatie te voltooien diff --git a/patches/src/main/resources/addresources/values-nl-rNL/youtube/strings.xml b/patches/src/main/resources/addresources/values-nl-rNL/youtube/strings.xml index a703ffac4f..834b205ef8 100644 --- a/patches/src/main/resources/addresources/values-nl-rNL/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-nl-rNL/youtube/strings.xml @@ -113,6 +113,9 @@ Wijzigingen zijn: Enquêtes verbergen Verbergt enquêtes en aanmoedigingen over de nauwkeurigheid van feed-aanbevelingen Ticketsectie verbergen + + Verberg banner \'Niet beschikbare video\'s zijn verborgen\' + Verbergt de banner \'Niet beschikbare video\'s zijn verborgen\' in afspeellijsten Verberg uploadtijd "Beperkingen: @@ -1247,6 +1250,9 @@ Beperking: Als 'Herstel oude video acties balk' is uitgeschakeld, is de \"Meer v Houdt de lay-out van de statusbalk consistent, zodat deze de bovenste balken niet bedekt en er geen lege ruimte boven de speler overblijft. Schakel dit uit als de app crasht of als de bovenste balken verkeerd eruitzien Gloed + Morphe Glow 1 + Morphe Glow 2 + Glow splash screen wordt niet ondersteund door dit apparaat Achtergrond van het lichtthema Wit Material You (Wit) diff --git a/patches/src/main/resources/addresources/values-or-rIN/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-or-rIN/shared-youtube/strings.xml index fd007eeb78..77635ba31c 100644 --- a/patches/src/main/resources/addresources/values-or-rIN/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-or-rIN/shared-youtube/strings.xml @@ -18,6 +18,7 @@ Second \"item\" text" --> + diff --git a/patches/src/main/resources/addresources/values-or-rIN/youtube/strings.xml b/patches/src/main/resources/addresources/values-or-rIN/youtube/strings.xml index 70ce03fc67..4ec76b1150 100644 --- a/patches/src/main/resources/addresources/values-or-rIN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-or-rIN/youtube/strings.xml @@ -31,6 +31,7 @@ Second \"item\" text" This button usually appears in the Subscriptions feed for future live streams or unreleased videos. --> + diff --git a/patches/src/main/resources/addresources/values-pa-rIN/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-pa-rIN/shared-youtube/strings.xml index fd007eeb78..77635ba31c 100644 --- a/patches/src/main/resources/addresources/values-pa-rIN/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-pa-rIN/shared-youtube/strings.xml @@ -18,6 +18,7 @@ Second \"item\" text" --> + diff --git a/patches/src/main/resources/addresources/values-pa-rIN/youtube/strings.xml b/patches/src/main/resources/addresources/values-pa-rIN/youtube/strings.xml index 70ce03fc67..4ec76b1150 100644 --- a/patches/src/main/resources/addresources/values-pa-rIN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-pa-rIN/youtube/strings.xml @@ -31,6 +31,7 @@ Second \"item\" text" This button usually appears in the Subscriptions feed for future live streams or unreleased videos. --> + diff --git a/patches/src/main/resources/addresources/values-pl-rPL/music/strings.xml b/patches/src/main/resources/addresources/values-pl-rPL/music/strings.xml index 4f22f46ccf..9b7fcf9f53 100644 --- a/patches/src/main/resources/addresources/values-pl-rPL/music/strings.xml +++ b/patches/src/main/resources/addresources/values-pl-rPL/music/strings.xml @@ -550,9 +550,9 @@ Wymagane ciasteczka: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPI Ukryj nieodtworzony tekst Ukrywa linijki tekstu znajdujące się poniżej tej aktualnie odtwarzanej Pokaż teksty MediaSession na ekranie blokady - Pokazuje bieżący wers na ekranie blokady, w Android Auto i przez Bluetooth. Wymaga systemu Android 11 lub nowszego + Wyświetla bieżący wers na ekranie blokady, w Android Auto i przez Bluetooth. Wymaga systemu Android 11 lub nowszego Pokaż teksty w miniodtwarzaczu - Pokazuje bieżący wers w miniodtwarzaczu, przenosząc nazwę utworu do podtytułu + Wyświetla bieżący wers w miniodtwarzaczu, przenosząc nazwę utworu do podtytułu Format tytułu i wykonawcy utworu Format informacji o utworze wyświetlanych wraz z tekstem w miniodtwarzaczu i MediaSession. {t} to tytuł, {a} to wykonawca, a {v} to wersja (np. akustyczna), jeśli źródło tekstu ją udostępnia. Tekst między dwoma dwukropkami jest pokazywany tylko wtedy, gdy każda zawarta w nim zmienna ma wartość. Wpisz \:, aby wstawić zwykły dwukropek. Każdy wiersz jest wyświetlany po kolei i zmienia się wraz ze zmianą wersu tekstu Format wersu tekstu diff --git a/patches/src/main/resources/addresources/values-pl-rPL/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-pl-rPL/shared-youtube/strings.xml index d598291b3e..42232f8f45 100644 --- a/patches/src/main/resources/addresources/values-pl-rPL/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-pl-rPL/shared-youtube/strings.xml @@ -21,9 +21,11 @@ Second \"item\" text" Animacja startowa Wyłączona Kolorowa - Kolorowy Morphe + Kolorowa Morphe Monochromatyczna - Monochromatyczny Morphe + Monochromatyczna Morphe + + Wyłączone Uruchom ponownie, aby dokończyć konfigurację diff --git a/patches/src/main/resources/addresources/values-pl-rPL/youtube/strings.xml b/patches/src/main/resources/addresources/values-pl-rPL/youtube/strings.xml index 3bba86d9ec..4dfb6ca55c 100644 --- a/patches/src/main/resources/addresources/values-pl-rPL/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-pl-rPL/youtube/strings.xml @@ -113,6 +113,9 @@ Zmiany obejmują: Ukryj ankiety Ukrywa ankiety i monity dotyczące trafności rekomendacji Ukryj sekcję biletów + + Ukryj baner \"Niedostępne filmy są ukryte\" + Ukrywa baner \"Niedostępne filmy są ukryte\" w playlistach Ukryj datę publikacji "Ograniczenia: @@ -500,8 +503,8 @@ Zgłoszenia są publiczne i podlegają weryfikacji przed dodaniem kanału do lis Film Dźwięk Wszystkie opcje - Pokaż pobieranie w menu Short - Dodaje zewnętrzny element menu flyout pobierania do odtwarzacza Short + Pokaż pobieranie w menu Shorts + Dodaje opcję zewnętrznej aplikacji do pobierania do menu odtwarzacza Shorts Pobierz Tłumaczenie głosowe @@ -613,7 +616,7 @@ Uwaga: Może nie działać na transmisjach na żywo" Umożliwia zapisywanie filmów przeznaczonych dla dzieci na liście \"Do obejrzenia\" z poziomu menu Pokaż przycisk \"Zapisz do obejrzenia\" w menu Shorts Umożliwia zapisywanie filmów Short na liście \"Do obejrzenia\" z poziomu menu - Przekieruj \"Do obejrzenia później\" do playlisty + Przekieruj \"Do obejrzenia\" na playlistę Do obejrzenia Dodaj do kolejki Morphe Zachowaj kolejkę po wyjściu @@ -697,7 +700,7 @@ Uwaga: Może nie działać na transmisjach na żywo" Umożliwia przewijanie wszystkich transmisji na żywo, nawet tych, które normalnie to blokują Włącz podgląd na pasku postępu - Pokazuje podgląd filmu podczas przeciągania paska postępu + Wyświetla podgląd filmu podczas przeciągania paska postępu Najczęściej odtwarzane "Opcja \"Włącz przewijanie transmisji na żywo\" może być niedostępna @@ -891,8 +894,8 @@ Aby przejść do ustawień, włącz przycisk w menu: Ogólne → Pasek narzędzi Zmniejsza poziome odstępy między przyciskami Wyłącz przyciski nawigacji bez etykiet Wyświetla przyciski nawigacji z etykietami, jeśli YouTube przesyła je bez nich - Wyłącz karty nawigacji na pasku narzędzi - Wyświetla Subskrypcje na pasku nawigacji, jeśli YouTube przeniesie je do karty na górze strony głównej + Wyłącz zakładki nawigacji na pasku narzędzi + Wyświetla Subskrypcje na pasku nawigacji, jeśli YouTube przeniesie je do zakładki na górze strony głównej Włącz animacje paska nawigacji Animuje przejścia podczas przełączania między kartami Wyłącz automatyczne ukrywanie paska nawigacji @@ -1111,20 +1114,20 @@ To ustawienie działa tylko wtedy, gdy autoodtwarzanie jest wyłączone. Możesz "Zmiany obejmują: Układ tabletowy -• Posty społeczności są ukryte -• Ustawienie odtwarzania w kanałach nie jest dostępne -• Przyciski Remiks i Dźwięk nie są dostępne w Shorts -• Nakładka „Więcej filmów” może nie być dostępna -• Pokazany jest nierozwinięty stary pasek akcji filmu -• Elementy menu wyskakującego mogą nie działać poprawnie +• Posty są ukryte +• Ustawienie odtwarzania na stronie głównej nie jest dostępne +• Przyciski Remiks i Dźwięk nie są dostępne w Shortsach +• Nakładka \"Więcej filmów\" może nie być dostępna +• Stary pasek akcji filmu nie jest zwinięty +• Elementy menu mogą nie działać prawidłowo Układ samochodowy -• Kanały są uporządkowane według tematów i kanałów -• Ustawienie odtwarzania w kanałach nie jest dostępne -• Przyciski Remiks i Dźwięk nie są dostępne w Shorts -• Nakładka „Więcej filmów” może nie być dostępna -• Shorts otwierają się w standardowym odtwarzaczu -• Pokazany jest nierozwinięty stary pasek akcji filmu" +• Strona główna jest uporządkowana według tematów i kanałów +• Ustawienie odtwarzania na stronie głównej nie jest dostępne +• Przyciski Remiks i Dźwięk nie są dostępne w Shortsach +• Nakładka \"Więcej filmów\" może nie być dostępna +• Filmy Short otwierają się w zwykłym odtwarzaczu +• Stary pasek akcji filmu nie jest zwinięty" Włącz układ tabletowy w odtwarzaczu "Przywraca stary układ odtwarzacza @@ -1248,6 +1251,9 @@ Ograniczenie: Jeśli opcja \"Przywróć stary pasek akcji filmu\" jest wyłączo Utrzymuje spójny układ paska stanu, dzięki czemu nie zasłania on górnych pasków ani nie zostawia pustego miejsca nad odtwarzaczem. Wyłącz tę opcję, jeśli aplikacja ulega awarii lub górne paski wyświetlają się nieprawidłowo Poświata + Poświata Morphe 1 + Poświata Morphe 2 + Animacja startowa \"Poświata\" nie jest obsługiwana przez to urządzenie Tło jasnego motywu Białe Material You (Biały) @@ -1270,9 +1276,9 @@ Ograniczenie: Jeśli opcja \"Przywróć stary pasek akcji filmu\" jest wyłączo Ustaw dodatkowy kolor paska postępu Nieprawidłowa wartość koloru - Oryginał analogowy - Oryginalny tort - Oryginalny metal + Oryginalna analogowa + Oryginalna ciasto + Oryginalna metalowa Logo w nagłówku Domyślne @@ -1637,16 +1643,16 @@ Ograniczenie: Przywraca stary wygląd nakładki \"Więcej filmów\"" Odcinek bez tytułu Odcinek niedostępny - Komentarze Niconico - Wyświetlaj czat na żywo oraz powtórkę czatu jako komentarze przesuwające się przez wideo - Wyświetla przycisk nakładki w odtwarzaczu wideo. Dotknij, aby pokazać lub ukryć komentarze, przytrzymaj, aby otworzyć szybkie ustawienia + Komentarze w stylu Niconico + Pokaż czat na żywo oraz powtórkę czatu jako komentarze przewijające się przez ekran filmu + Wyświetla przycisk w nakładce odtwarzacza. Dotknij, aby pokazać lub ukryć komentarze. Przytrzymaj, aby otworzyć szybkie ustawienia Rozmiar czcionki komentarzy Rozmiar tekstu komentarzy (12-40 sp) - Przezroczystość komentarzy - Przezroczystość tekstu i tła komentarzy - Czas trwania podróży - Ile sekund komentarze potrzebują, aby przemieścić się przez ekran. Niższe wartości oznaczają szybszy ruch + Krycie komentarzy + Krycie tekstu oraz tła komentarzy + Czas przelotu + Liczba sekund potrzebna komentarzom na pokonanie ekranu. Mniejsze wartości oznaczają szybszy ruch Maksymalna długość komentarza - Ukrywa komentarze, które mają więcej znaków niż ta wartość. Każdy emoji liczy się jako jeden znak + Ukrywa komentarze przekraczające podaną liczbę znaków. Każda emotka liczy się jako jeden znak Bez limitu diff --git a/patches/src/main/resources/addresources/values-pt-rBR/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-pt-rBR/shared-youtube/strings.xml index e551998867..f877158b74 100644 --- a/patches/src/main/resources/addresources/values-pt-rBR/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-pt-rBR/shared-youtube/strings.xml @@ -21,9 +21,9 @@ Second \"item\" text" Animação de inicialização Desativado Cor - Morphe Colorido Monocromático - Morphe Monocromático + + Desativado Reinicie para finalizar a configuração diff --git a/patches/src/main/resources/addresources/values-pt-rBR/youtube/strings.xml b/patches/src/main/resources/addresources/values-pt-rBR/youtube/strings.xml index 71a485a460..8a1593695b 100644 --- a/patches/src/main/resources/addresources/values-pt-rBR/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-pt-rBR/youtube/strings.xml @@ -113,6 +113,9 @@ As mudanças incluem: Ocultar pesquisas Ocultar pesquisas e avisos sobre a precisão das recomendações do feed Ocultar seção de ingressos + + Ocultar banner \"Vídeos indisponíveis estão ocultos\" + Oculta o banner \"Vídeos indisponíveis estão ocultos\" em playlists Ocultar tempo de upload "Limitações: @@ -1248,6 +1251,7 @@ Limitação: Se 'Restaurar barra de ações de vídeo antiga' estiver desativado Mantém o layout da barra de status consistente, para que não cubra as barras superiores e não deixe espaço vazio acima do player. Desative esta opção se o aplicativo travar ou se as barras superiores parecerem incorretas Brilho + A tela de abertura Glow não é suportada por este dispositivo Fundo do tema claro Branco Material You (Branco) diff --git a/patches/src/main/resources/addresources/values-pt-rPT/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-pt-rPT/shared-youtube/strings.xml index 5fe5f53ade..e3d6cac676 100644 --- a/patches/src/main/resources/addresources/values-pt-rPT/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-pt-rPT/shared-youtube/strings.xml @@ -21,9 +21,8 @@ Second \"item\" text" Animação de inicialização Desativado Cor - Morphe Colorido Monocromático - Morphe Monocromático + Reiniciar para finalizar a configuração diff --git a/patches/src/main/resources/addresources/values-pt-rPT/youtube/strings.xml b/patches/src/main/resources/addresources/values-pt-rPT/youtube/strings.xml index cc27ce33e2..282b02d06d 100644 --- a/patches/src/main/resources/addresources/values-pt-rPT/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-pt-rPT/youtube/strings.xml @@ -113,6 +113,9 @@ As alterações incluem: Ocultar pesquisas Oculta enquetes e avisos sobre a precisão da recomendação de feed Ocultar prateleira de ingressos + + Ocultar banner \"Vídeos indisponíveis estão ocultos\" + Oculta o banner \"Vídeos indisponíveis estão ocultos\" em playlists Ocultar tempo de upload "Limitações: @@ -1249,6 +1252,7 @@ Limitação: Se 'Restaurar a barra de ação de vídeo antiga' estiver desativad Mantém o layout da barra de status consistente, para que não cubra as barras superiores e não deixe espaço vazio acima do player. Desative isso se o aplicativo travar ou se as barras superiores estiverem incorretas Brilho + A tela de abertura Glow não é suportada por este dispositivo Cor de fundo do tema claro Branco Material You (Branco) diff --git a/patches/src/main/resources/addresources/values-ro-rRO/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-ro-rRO/shared-youtube/strings.xml index 64364fcb66..e5704a8698 100644 --- a/patches/src/main/resources/addresources/values-ro-rRO/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ro-rRO/shared-youtube/strings.xml @@ -21,9 +21,9 @@ Second \"item\" text" Animație de pornire Dezactivat Culoare - Morphe Color Monocrom - Morphe Monocrom + + Oprit Reporniți pentru a finaliza configurarea diff --git a/patches/src/main/resources/addresources/values-ro-rRO/youtube/strings.xml b/patches/src/main/resources/addresources/values-ro-rRO/youtube/strings.xml index 803c76a7b9..d9e6e598cb 100644 --- a/patches/src/main/resources/addresources/values-ro-rRO/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ro-rRO/youtube/strings.xml @@ -113,6 +113,9 @@ Modificările includ: Ascunde sondajele Ascunde sondaje și sugestii despre acuratețea recomandărilor pentru fluxul de conținut Ascunde raftul de bilete + + Ascunde bannerul „Videoclipurile indisponibile sunt ascunse” + Ascunde bannerul „Videoclipurile indisponibile sunt ascunse” din playlist-uri Ascunde ora de încărcare "Limitări: @@ -1249,6 +1252,7 @@ Limitare: Dacă 'Restaurare bara de acțiuni video veche' este dezactivată, str Păstrează aspectul fermei de stare constant, astfel încât să nu acopere barele superioare și să lase spațiu liber deasupra player-ului. Dezactivează această opțiune dacă aplicația se blochează sau dacă barele superioare arată incorect Strălucire + Ecranul de pornire Glow nu este suportat de acest dispozitiv Fundalul temei luminoase Alb Material You (Alb) diff --git a/patches/src/main/resources/addresources/values-ru-rRU/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-ru-rRU/shared-youtube/strings.xml index 5223fc4e04..8ccad55aaa 100644 --- a/patches/src/main/resources/addresources/values-ru-rRU/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ru-rRU/shared-youtube/strings.xml @@ -21,9 +21,11 @@ Second \"item\" text" Анимация запуска Выключено Цветной - Цветной Morphe + Morphe Цвет Монохромный - Монохромный Morphe + Morphe Монохромный + + Выключено Перезагрузите для завершения настройки diff --git a/patches/src/main/resources/addresources/values-ru-rRU/youtube/strings.xml b/patches/src/main/resources/addresources/values-ru-rRU/youtube/strings.xml index 311bd56b6c..41634a5c7d 100644 --- a/patches/src/main/resources/addresources/values-ru-rRU/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ru-rRU/youtube/strings.xml @@ -113,6 +113,9 @@ Second \"item\" text" Скрыть опросы Скрывает опросы и подсказки о точности рекомендаций ленты Скрыть секцию билетов + + Скрыть баннер «Недоступные видео скрыты» + Скрывает баннер «Недоступные видео скрыты» в плейлистах Скрыть время загрузки "Ограничения: @@ -1209,6 +1212,8 @@ Second \"item\" text" Отключает мини-плеер \"Продолжить просмотр\", функцию YouTube Premium, которая появляется при запуске приложения Миниплеер Настройка внешнего вида и поведения мини-плеера + Дополнительные мини плееры + YouTube удалил все дополнительные мини плеера. Чтобы восстановить больше типов мини плееров, установите патч для YouTube 21.28.208 или более ранней версии Тип миниплеера Скрыть название и канал Отключено @@ -1250,6 +1255,9 @@ Second \"item\" text" Сохраняет неизменным расположение строки состояния, поэтому она не закрывает верхние панели и не оставляет пустого пространства над проигрывателем. Отключите, если приложение выходит из строя или верхние панели выглядят неправильно. Свечение + Morphe Свечение 1 + Morphe Свечение 2 + Этот девайс не поддерживает эффект свечения на заставке Фон светлой темы Белый Material You (белый) @@ -1567,7 +1575,6 @@ AVC (H.264) Эпизоды отмечены как просмотренные Вернуть Включить - 1 новый эпизод %1$s новые эпизоды Записывать прогресс на этом устройстве "Ваши сериалы и прогресс остаются на этом устройстве, общая для учетных записей YouTube. Элементы истории YouTube не меняют их. Очищайте прогресс из меню «Серии». diff --git a/patches/src/main/resources/addresources/values-si-rLK/music/strings.xml b/patches/src/main/resources/addresources/values-si-rLK/music/strings.xml index 434ef037fa..ccfea4ed71 100644 --- a/patches/src/main/resources/addresources/values-si-rLK/music/strings.xml +++ b/patches/src/main/resources/addresources/values-si-rLK/music/strings.xml @@ -17,17 +17,7 @@ Second \"item\" text" --> - අවසන් ධාවනය - ග්‍රාහකත්වයන් - දෘශ්‍ය පට - වෙනත් ප්‍රතිඵල - උඩුගත කළ ගීත - උඩුගත කළ ඇල්බම් - උඩුගත කළ කලාකරුවන් - නැවත්වීමට සහ නැවත පටන් ගැනීමට host එකේ Jam patch එක යාවත්කාලින කරන්න - Jam පෝලිම් හවුල්කරණය - විවෘත @@ -80,6 +70,5 @@ Second \"item\" text" - ස්වයංක්‍රීය diff --git a/patches/src/main/resources/addresources/values-si-rLK/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-si-rLK/shared-youtube/strings.xml index e6cb21c4aa..77635ba31c 100644 --- a/patches/src/main/resources/addresources/values-si-rLK/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-si-rLK/shared-youtube/strings.xml @@ -18,7 +18,7 @@ Second \"item\" text" --> - විසන්ධි කර ඇත + diff --git a/patches/src/main/resources/addresources/values-si-rLK/shared/strings.xml b/patches/src/main/resources/addresources/values-si-rLK/shared/strings.xml index 088ba3c656..31cdbe4dcb 100644 --- a/patches/src/main/resources/addresources/values-si-rLK/shared/strings.xml +++ b/patches/src/main/resources/addresources/values-si-rLK/shared/strings.xml @@ -26,9 +26,7 @@ Second \"item\" text" - අක්‍රීයයි - සක්‍රීයයි diff --git a/patches/src/main/resources/addresources/values-si-rLK/youtube/strings.xml b/patches/src/main/resources/addresources/values-si-rLK/youtube/strings.xml index 7d16d4737d..4ec76b1150 100644 --- a/patches/src/main/resources/addresources/values-si-rLK/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-si-rLK/youtube/strings.xml @@ -19,7 +19,6 @@ Second \"item\" text" - අක්‍රීයයි @@ -32,6 +31,7 @@ Second \"item\" text" This button usually appears in the Subscriptions feed for future live streams or unreleased videos. --> + @@ -57,7 +57,6 @@ Second \"item\" text" - දෘශ්‍ය පට @@ -93,7 +92,6 @@ Second \"item\" text" - අක්‍රීයයි @@ -116,7 +114,6 @@ Second \"item\" text" - අක්‍රීයයි @@ -174,7 +171,6 @@ Second \"item\" text" - විසන්ධි කර ඇත @@ -186,7 +182,6 @@ Second \"item\" text" - අක්‍රීයයි @@ -231,7 +226,6 @@ Second \"item\" text" - ස්වයංක්‍රීය diff --git a/patches/src/main/resources/addresources/values-sk-rSK/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-sk-rSK/shared-youtube/strings.xml index 89120f44c1..f1053e5214 100644 --- a/patches/src/main/resources/addresources/values-sk-rSK/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-sk-rSK/shared-youtube/strings.xml @@ -21,9 +21,9 @@ Second \"item\" text" Animácia spustenia Vypnutý Farebná - Farebné Morphe Monochromatické - Monochromatické Morphe + + Vypnuté Reštartujte aplikáciu, aby sa dokončila konfigurácia diff --git a/patches/src/main/resources/addresources/values-sk-rSK/youtube/strings.xml b/patches/src/main/resources/addresources/values-sk-rSK/youtube/strings.xml index 4d6e5bcaf0..e955ba7221 100644 --- a/patches/src/main/resources/addresources/values-sk-rSK/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-sk-rSK/youtube/strings.xml @@ -113,6 +113,9 @@ Zmeny zahŕňajú: Skryť prieskumy Skryje prieskumy a návrhy týkajúce sa presnosti odporúčania kanála Skryť policu lístkov + + Skryť banner „Nedostupné videá sú skryté“ + Skryje banner „Nedostupné videá sú skryté“ v zoznamoch prevzatia Skryť čas nahrania "Obmedzenia: @@ -1249,6 +1252,7 @@ Obmedzenie: Ak je vypnutá možnosť 'Obnoviť starú lištu akcií videa', vrst Udržuje konzistentné rozloženie stavového riadka, takže nezakrýva horné panely a nenecháva prázdny priestor nad prehrávačom. Toto vypnite, ak sa aplikácia zrúti alebo horné panely vyzerajú nesprávne Žiarenie + Tento zariadenie nepodporuje žiarivý úvodný obrazovku Pozadie svetlého motívu Biela Material You (biely) diff --git a/patches/src/main/resources/addresources/values-sl-rSI/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-sl-rSI/shared-youtube/strings.xml index 761fcb443f..b12d13f3ab 100644 --- a/patches/src/main/resources/addresources/values-sl-rSI/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-sl-rSI/shared-youtube/strings.xml @@ -21,9 +21,8 @@ Second \"item\" text" Zagonska animacija Onemogočeno Barva - Barvni Morphe Enobarvno - Enobarvni Morphe + Za dokončanje nastavitve znova zaženite aplikacijo diff --git a/patches/src/main/resources/addresources/values-sl-rSI/youtube/strings.xml b/patches/src/main/resources/addresources/values-sl-rSI/youtube/strings.xml index 25c80f00a1..0fa1b33694 100644 --- a/patches/src/main/resources/addresources/values-sl-rSI/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-sl-rSI/youtube/strings.xml @@ -113,6 +113,9 @@ Spremembe vključujejo: Skrij ankete Skrije ankete in spodbude glede natančnosti priporočil za vir Skrij polico s karticami + + Skrij pasico »Nedostopni videoposnetki so skriti« + Skrije pasico »Nedostopni videoposnetki so skriti« v seznamih predvajanja Skrij čas nalaganja "Omejitve: @@ -1249,6 +1252,7 @@ Omejitev: Če je možnost »Ponastavi staro vrstico za videoposnetke« izkloplje Ohranja dosledno zasnovo vrstice stanja, tako da ne prekriva zgornjih vrstic in ne pušča praznega prostora nad predvajalnikom. To funkcijo izklopite, če se aplikacija sesuje ali zgornje vrstice ne izgledajo pravilno Sijaj + Ta naprava ne podpira zaslona s sijajem Ozadje svetle teme Bela Material You (bela) diff --git a/patches/src/main/resources/addresources/values-sr-rCS/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-sr-rCS/shared-youtube/strings.xml index d33449ff7f..fc5c70033b 100644 --- a/patches/src/main/resources/addresources/values-sr-rCS/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-sr-rCS/shared-youtube/strings.xml @@ -21,9 +21,9 @@ Second \"item\" text" Animacija pri pokretanju Onemogućen Boja - Kolorni Morphe Monohromski - Monohromni Morphe + + Isključen Ponovo pokrenite da biste završili postavljanje diff --git a/patches/src/main/resources/addresources/values-sr-rCS/youtube/strings.xml b/patches/src/main/resources/addresources/values-sr-rCS/youtube/strings.xml index dcec0e2ee3..a39a9e84f8 100644 --- a/patches/src/main/resources/addresources/values-sr-rCS/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-sr-rCS/youtube/strings.xml @@ -113,6 +113,9 @@ Promene uključuju: Sakrij ankete Sakriva ankete i predloge u vezi preciznosti preporuka za sadržaj Sakrij policu sa kartama + + Sakrij baner \"Nedostupni video snimci su sakriveni\" + Sakriva baner \"Nedostupni video snimci su sakriveni\" u plejlistama Sakrij vreme otpremanja "Ograničenja: @@ -1249,6 +1252,7 @@ Ograničenje: Ako je \"Vrati staru traku sa akcijama za video\" isključen, prek Održava dosledan izgled statusne trake, tako da ne prekrije gornje trake i ostavlja prazan prostor iznad plejera. Isključite ovo ako aplikacija padne ili gornje trake izgledaju pogrešno Sjaj + Ovaj uređaj ne podržava Glow početni ekran Pozadina svetle teme Bela Material You (Bela) diff --git a/patches/src/main/resources/addresources/values-sr-rSP/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-sr-rSP/shared-youtube/strings.xml index 8c631f1752..e0c31531d0 100644 --- a/patches/src/main/resources/addresources/values-sr-rSP/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-sr-rSP/shared-youtube/strings.xml @@ -21,9 +21,8 @@ Second \"item\" text" Анимација покретања Онемогућен Боја - Боја Morphe Монохромски - Монохромни Morphe + Поново покрените да бисте завршили постављање diff --git a/patches/src/main/resources/addresources/values-sr-rSP/youtube/strings.xml b/patches/src/main/resources/addresources/values-sr-rSP/youtube/strings.xml index f5d3131f3e..8ca6779285 100644 --- a/patches/src/main/resources/addresources/values-sr-rSP/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-sr-rSP/youtube/strings.xml @@ -113,6 +113,9 @@ Second \"item\" text" Сакриј анкете Сакрива анкете и подстицаје у вези са тачношћу препорука фида Сакриј полицу са картама + + Сакриј банер „Недоступни видео снимци су сакривени” + Скрива банер „Недоступни видео снимци су сакривени” у плејлистама Сакриј време отпремања "Ограничења: @@ -1247,6 +1250,7 @@ Second \"item\" text" Одржава конзистентан распоред статусне траке, тако да не прекрива горње траке и не оставља празан простор изнад плејера. Исключи ово ако се апликација уруши или горње траке изгледају лоше Сјај + Овај уређај не подржава Glow екран за почетак Позадина светле теме Бела Material You (бела) diff --git a/patches/src/main/resources/addresources/values-sv-rSE/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-sv-rSE/shared-youtube/strings.xml index 06ca483623..9edeef8377 100644 --- a/patches/src/main/resources/addresources/values-sv-rSE/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-sv-rSE/shared-youtube/strings.xml @@ -21,9 +21,8 @@ Second \"item\" text" Startanimation Inaktiverad Färg - Färg Morphe Monokrom - Monokrom Morphe + Starta om för att slutföra konfigureringen diff --git a/patches/src/main/resources/addresources/values-sv-rSE/youtube/strings.xml b/patches/src/main/resources/addresources/values-sv-rSE/youtube/strings.xml index 13800a48b1..dd14310e65 100644 --- a/patches/src/main/resources/addresources/values-sv-rSE/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-sv-rSE/youtube/strings.xml @@ -113,6 +113,9 @@ Vissa serverbaserade layouts kanske inte återgår. Dölj enkäter Dölj enkäter och puffar om hur korrekta rekommendationerna är i flödet Dölj biljetthylla + + Dölj bannern \"Otillgängliga videor är dolda\" + Döljer bannern \"Otillgängliga videor är dolda\" i spellistor Dölj uppladdningstid "Begränsningar: @@ -1249,6 +1252,7 @@ Begränsning: Om 'Återställ gammalt videoåtgärdsfält' är avstängt kan öv Behåller statusradens layout konsekvent, så att den inte täcker de övre fälten och lämnar inget tomt utrymme ovanför spelaren. Stäng av detta om appen kraschar eller om de övre fälten ser fel ut Glöd + Glow-stänkbild skärm är inte supported av den här enheten Bakgrundsfärg för ljust tema Vit Material You (Vit) diff --git a/patches/src/main/resources/addresources/values-sw-rKE/music/strings.xml b/patches/src/main/resources/addresources/values-sw-rKE/music/strings.xml index ccfea4ed71..857c173125 100644 --- a/patches/src/main/resources/addresources/values-sw-rKE/music/strings.xml +++ b/patches/src/main/resources/addresources/values-sw-rKE/music/strings.xml @@ -17,58 +17,674 @@ Second \"item\" text" --> + Imepigwa mara ya mwisho + Usajili + Video + Matokeo mengine + Nyimbo zilizopakiwa + Albamu zilizopakiwa + Wasanii waliopakiwa + Sasisha patch ya Jam ya mwenyeji ili kutumia pause na resume + Ushirikishaji wa foleni ya Jam + Anza au jiunge na Jam ya karibu na uhariri mfuatano wake kupitia Jam Layer + Washa ushirikishaji wa foleni ya Jam + Huwezesha utangamano wa Jam baada ya YouTube Music kuanza upya + Pakia Jam Layer ili ianze kufanya kazi + Fungua toleo la hivi karibuni la Jam Layer kwenye kivinjari chako + Vidhibiti vya Jam + Andaa au jiunge na Jam ya karibu na usimamizi wa kikao + Kifurushi cha Jam Layer + Chagua programu ya msaidizi iliyosakinishwa inayotumika kwa kushiriki orodha ya kusubiri ya karibu + Tumia kifurushi cha msaidizi kilichosakinishwa. Kuunganisha kunahitaji idhini yako na tokeni ya uwezo wa ndani + Washa Wi-Fi? + Washa Wi-Fi, kisha jaribu kuanzisha au kujiunga na Jam tena + Fungua mipangilio ya Wi-Fi + Ghairi + Tumia kifurushi + Ingiza jina sahihi la kifurushi cha Android + Kifurushi kimehifadhiwa. Sanidi Jam Layer ili kuunganisha + Jam imetengwa + Washa Jam Layer kwanza + Washa ushirikishaji wa foleni ya Jam kwenye mipangilio, kisha anzisha tena YouTube Music + Sakinisha kifurushi cha Jam Layer kilichochaguliwa kwanza + Ingiza nambari inayoonekana kwenye kifaa kinachohost. Wi-Fi Aware inaweza kuunganisha vifaa vya karibu hata kama LAN zao zinatofautiana + Jiunge kwa kutumia nambari ya siri + Jiunge + Skeni QR + Ingiza nambari ya siri ya herufi nane au bandika mwaliko + Jiunge na Jam yako + Nakili nambari + Kodi ya Jam + Onyesha QR + Tayari + Alika kwenye Jam yako + Skani ili kujiunga na kuhariri mfuatano huu. Kumaliza Jam kunafuta mwaliko + Nakili kiungo + Mwaliko wa Jam + Jam inaunganishwa tena; jaribu tena baada ya muda mfupi + Imeongezwa kwenye Jam + Jam haipatikani; nyimbo haikuongezwa + Jam Yako + Kusikiliza pamoja + Kuungana na Jam + Sikilizeni pamoja + Inasasisha Jam… + Inatafuta mwenyeji wako kwenye Wi-Fi… + Inasubiri watu + mtu %s ameunganishwa + watu %s wameunganishwa + Ingiza nambari hii kwenye kifaa cha karibu. Wi-Fi Aware inafanya kazi kwenye LAN tofauti inapoungwa mkono. Itaisha katika dakika %s + Hama hadi %s? + Marekebisho yamefunguliwa + Marekebisho yamefungwa + Imeunganishwa kupitia Wi-Fi Aware + Imeunganishwa kupitia Wi-Fi + Imeunganishwa kupitia Bluetooth + Haiungwi mkono wakati wa Jam + Kujiunga kumeghairiwa + Inaunganisha tena na mwenyeji wako… + Shiriki mfuatano. Ruhusu kila mtu aongeze nyimbo + Alika watu + Wageni wanaweza kuhariri foleni + Maliza Jam + Mwenyeji anacheza muziki. Ongeza, hamisha au ondoa nyimbo moja kwa moja kwenye mfuatano wa YouTube Music + Weka vifaa vyote viwili karibu. Unaweza kufuta wakati tunamtafuta mwenyeji + Ghairi kujiunga + Ondoka kwenye Jam + Anza Jam + Sanidi Tabaka la Jam + Simamia + Maelezo + Fungua + Inapakia chaguzi za wimbo… + Chaguzi za wimbo hazipatikani; jaribu tena + Hakuweza kufungua chaguzi za wimbo + Hakuna kivinjari kinachopatikana ili kupakua Jam Layer + Inasubiri foleni ya mwenyeji + Fungua wimbo kwenye YouTube Music kwanza + Cheza kwenye mwenyeji + Ondoka kwenye Jam na ucheze ndani + Cheza inayofuata kwenye Jam + Ongeza kwenye foleni ya Jam + Cheza wimbo huu wa Jam + Chagua mahali pa kucheza + Hamisha mwenyeji hapa + Wimbo haukuwa tayari kutafutwa + Foleni imebadilika; jaribu ishara tena + Nembo ya kichwa + Kawaida + Maalum + Cheza nyimbo za sauti za albamu + Cheza toleo la sauti pekee la albamu la nyimbo badala ya video ya muziki. Inahitaji \'Igize video streams\' + Wezesha swichi ya sauti/video + Jaribu kubadilisha Toleo la Nyimbo za Sauti na Video Rasmi ya Muziki + Pakua video za muziki, podkasti, au nyimbo kwa kutumia programu za kupakulia za upande wa tatu + Tumia kipakuzi cha nje kwa menyu ya Pakua + Chaguo la Pakua la menyu litafungua kipakuzi chako cha nje badala yake + Pakua + "Hakuna mapakua +Nyimbo zilizopakuliwa zinaonekana hapa" + %1$s • %2$s • %3$s MB + Tafuta mapakua + Panga + Panga kwa msanii + Panga kwa kichwa + Panga kwa ukubwa + Futa pakua zote + Futa pakua zote? + Nyimbo zote zilizopakuliwa zitafutwa kwenye kifaa hiki + Pakua zimefutwa + Hakuna kilichopatikana + Hatua kwa %s + Cheza + Cheza au pause + Iliyopita + Inayofuata + Pause + Funga + Futa + Futa pakia + Futa pakia? + %s itafutwa kutoka kwenye kifaa hiki + Pakia imefutwa + Hakuwezekana kufuta pakia + Hakuwezekana kucheza faili lililopakuliwa + Pakua sauti + Uchezaji wakiwa nje ya mtandao + Hakuweza kutambua wimbo wa kupakua + Upakuaji tayari unaendelea + Tayari imepakuliwa + Upakuaji umeanza + Inaanza kupakua… + Inapakia %s%% + Pakia kamili + Kupakia kumeshindwa + Kuhusu Morphe + Matangazo + Feed + Kawaida + Kicheza + Mashairi + Mengineyo + Lugha ya YouTube Music + Lugha ya mfumo + Inahitaji Android 13 au baadaye + Ficha lebo ya \'Pata Music Premium\' + Ficha matangazo ya Music Premium + Ficha matangazo ya video + Ficha kitufe cha Sauti / Video + Ficha kitufe cha Pakua + Huficha kitufe cha Pakua katika onyesho la podcast + Ficha rafu ya Explore + Ficha upau wa kichujio + Ficha rafu za gridi + Ficha rafu za mlalo + Ficha rafu za orodha + Paneli ya mashairi + Ficha kitufe cha Shiriki + Ficha kitufe cha Tafsiri + Ficha rafu ya \"New from\" + Ficha rafu za orodha ya nyimbo + Ficha kitufe cha Kurudia + Ficha kitufe cha Kuchanganya + Ficha rafu ya \"Speed dial\" + Maoni + Chagua ni vipengele gani vionekane katika sehemu ya maoni + Ficha Mwongozo wa Jumuiya + Ficha ujumbe wa muktadha + Ficha kitufe cha Emoji + Ficha kitufe cha Info + Ficha kitufe cha Muda wa Wakati + Crossfade + Unganisha nyimbo kwa ulaini moja baada ya nyingine + Washa crossfade + Mstari wa kufifia + Nguvu sawa + Shikilia kidogo + Rahisisha kwa upole + Mstari laini wa S + Inayotoka + Inayoingia + Muda wa crossfade + mili-sekunde 250 + mili-sekunde 500 + mili-sekunde 750 + sekunde 1 + sekunde 2 + sekunde 3 + sekunde 4 + sekunde 5 + sekunde 6 + sekunde 7 + sekunde 8 + sekunde 9 + sekunde 10 + sekunde 11 + sekunde 12 + Crossfade wakati wa kuruka kwa mkono + Hutumia athari ya crossfade unaporuka kwenda kwenye wimbo unaofuata kwa mkono + Crossfade wakati wa kusonga mbele kiotomatiki + Hutumia athari ya crossfade wakati wimbo unapoisha kawaida + Udhibiti wa kikao + Bonyeza kwa muda kitufe cha shuffle ili kusitisha au kuendelea na crossfade + Crossfade imesitishwa kwa kipindi hiki + Crossfade imeendelea + Hali ya video haipatikani wakati crossfade imewashwa + Crossfade imezimwa wakati wa uchezaji wa SABR + Crossfade imezimwa wakati wa kutuma + Inavyofanya kazi + "Crossfade huingiliana mwisho wa wimbo mmoja na mwanzo wa wimbo unaofuata, kwa kutumia muda na msururu wako + +Inafanya kazi katika utendaji wa auto-advance na kuruka kwa mkono, kila moja ikiwa na kitufe chake. Katika auto-advance, mchanganyiko huanza sekunde chache kabla ya wimbo kuisha" + Vikomo + "Video za muziki hucheza kama sauti pekee wakati crossfade imewashwa. Bonyeza na ushikilie shuffle ili kusitisha crossfade na kutazama video + +Video ya muziki inayoanza kucheza hubaki katika hali ya video hadi wimbo unaofuata + +Wimbo mfupi kuliko muda wa fade haufanyiwi crossfade + +Skrini ya kufunga au taarifa inaweza kuonyesha hali ya zamani kwa muda mfupi katikati ya mpito + +Kusitisha crossfade ili kutazama video ya muziki kunaweza kuacha baadhi ya video zikiwa nyeusi. Kusitisha crossfade kabla ya kuchagua wimbo kunazuia jambo hili" + Haiungwi mkono + "Kurusha au kuakisi sauti (Chromecast, kuakisi skrini au sauti) + +Uchezaji wa SABR (mitiririko ya video inayofananishwa imezimwa)" + Sifa + Kipengele cha Crossfade na VazerOG + Kumbuka hali ya kurudia + Kumbuka hali ya kuchanganya + Kasi ya uchezaji + Kasi inayotumika kwa nyimbo zote. Katika 100% kasi ya programu hutumiwa, kama vile kasi ya uchezaji wa podcast + Badilisha sauti kulingana na kasi + Zima upoeshaji wa kutopenda + Huzuia kurukia wimbo unaofuata wakati kitufe cha kutopenda kinapobonyezwa + Kitendo cha kugusa wimbo wa orodha ya nyimbo + Cheza orodha ya nyimbo + Cheza wimbo ulioteuliwa pekee + Ongeza wimbo ulioteuliwa kwenye foleni + Imeongezwa kwenye foleni + Ficha kitufe cha Cast + Ficha kitufe cha Historia + Ficha kitufe cha Taarifa + Ficha kitufe cha Utafutaji + Ficha kitufe cha utafutaji wa sauti + Ficha kitufe cha utafutaji wa sauti ya muziki + Ficha kitufe cha Upya + Huficha kitufe cha Upya katika tabo ya Maktaba + Vitufe vya utendaji + Chagua vitufe gani vya utendaji vionekane chini ya kidhibiti + Ficha upau wa utendaji + Ficha kitufe cha Kupenda na Kutokupenda + Ficha kitufe cha Maoni + Ficha kitufe cha \'Rudia soga ya moja kwa moja\' + Ficha kitufe cha Mashairi + Ficha kitufe cha Shiriki + Ficha kitufe cha Hifadhi + Ficha kitufe cha Pakua + Ficha kitufe cha \'Anza redio\' + Menyu ya flyout + Chagua ni vipengele vipi vionekane kwenye menyu za flyout za kidchezaji na foleni + Ficha kipengele cha safu 3 + Ficha vitufe vya Kupenda na Kutopenda + Ficha menyu ya \'Oanisha ladha\' + Ficha menyu ya \'Ongeza kwenye orodha ya kucheza ya Sikiliza baadaye\' + Ficha menyu ya \'Ongeza kwenye foleni\' + Ficha menyu ya Maelezo + Ficha menyu ya \'Futa orodha ya kucheza\' + Ficha menyu ya \'Ondoa foleni\' + Ficha menyu ya \'Usipendekeze msanii huyu\' + Ficha menyu ya Pakua + Ficha menyu ya \'Hariri orodha ya nyimbo\' + Ficha menyu ya \'Nenda kwenye albamu\' + Ficha menyu ya \'Nenda kwa msanii\' + Ficha menyu ya \'Nenda kwenye kipindi\' + Ficha menyu ya \'Nenda kwenye podikasti\' + Ficha menyu ya Msaada + Ficha menyu ya \'Weka kipindi kama kilichochezwa\' + Ficha menyu ya \'Sivuti\' + Ficha menyu ya \'Pin to speed dial\' + Ficha menyu ya \'Cheza inayofuata\' + Ficha menyu ya Ubora + Ficha menyu ya \'Ondoa kwenye maktaba\' + Ficha menyu ya \'Ondoa kwenye orodha ya nyimbo\' + Ficha menyu ya Ripoti + Ficha menyu ya \'Hifadhi kipindi kwa ajili ya baadaye\' / \'Hifadhi kwenye maktaba\' + Ficha menyu ya \'Hifadhi kwenye orodha ya nyimbo\' + Ficha menyu ya Shiriki + Ficha menyu ya \'Cheza kwa kubadilisha\' + Ficha menyu ya \'Kipima muda cha kulala\' + Ficha menyu ya \'Anza redio\' + Ficha menyu ya \'Takwimu kwa wataalamu\' + Ficha menyu ya Jisajili + Ficha menyu ya \'Ondoa kwenye namba za haraka\' + Ficha menyu ya \'Angalia sifa za wimbo\' + Ukurasa wa kuanzia + Kawaida + Chati + Vipindi vya baadaye + Gundua + Historia + Maktaba + Muziki ulioipenda + Orodha za nyimbo + Tafuta + Usajili + Lazimisha mwelekeo wa wima + Huweka programu katika mwelekeo wa wima wakati kifaa kinapozungushwa + Badilisha rangi ya miniplayer + Inalinganisha rangi ya miniplayer na ya kicheza cha skrini nzima + Badilisha rangi ya baa ya urambazaji + Oanisha rangi ya baa ya urambazaji na kicheza muziki cha skrini nzima + Onyesha kitufe cha inayofuata kwenye miniplayer + Onyesha kitufe cha iliyopita kwenye miniplayer + Washa miniplayer ya kulazimisha + Inazuia kicheza kupanuka kiotomatiki hadi skrini nzima wakati wimbo mpya unapoanza + Washa utelezi ili kuondoa miniplayer + Inaruhusu kufunga kidchezaji kidogo kwa kukisogeza chini + Upau wa urambazaji + Chagua ni vitufe gani vionekane kwenye upau wa urambazaji wa chini + Ficha kitufe cha Nyumbani + Ficha kitufe cha Sampuli + Ficha kitufe cha Gundua + Ficha kitufe cha Maktaba + Ficha kitufe cha Boresha + Ficha upau wa urambazaji + Ficha lebo za vitufe vya urambazaji + Hujaingia. Gusa ili kusanidi + Toka + Umetoka + Washa scrobbling + Tuma hali ya \'Inacheza Sasa\' + Muda wa chini wa wimbo + Nyimbo fupi kuliko hapa hazita-scrobbled + Asilimia ya ucheleweshaji wa scrobble + Anzisha scrobble baada ya kusikiliza asilimia hii ya wimbo + Sekunde za ucheleweshaji wa scrobble + Anzisha scrobble baada ya kusikiliza kwa sekunde hizi + Akaunti ya ListenBrainz + Umeingia. Gusa ili kusimamia akaunti + Ingiza Token ya Mtumiaji ya ListenBrainz ili kuwezesha scrobbling + Token + Thibitisha + Tafadhali weka tokeni + Inathibitisha… + Tokeni halali! Mtumiaji: %s + Tokeni batili: %s + haijulikani + Uhakiki umeshindwa: %s + Token imefutwa + Token imehifadhiwa + Onyo: Token iliyohifadhiwa si sahihi! + Penda nyimbo zilizopendwa + Penda nyimbo kwenye Last.fm kiotomatiki unapozipenda kwenye YouTube Music (na ondoa upendo unapoziondoa au kuzichukia) + Akaunti ya Last.fm + Umeingia kama %s. Gusa ili kudhibiti akaunti + Ingiza sifa zako za Last.fm ili kuingia + Jina la mtumiaji + Nywila + Ingia + Tafadhali weka jina la mtumiaji na nywila + Inaingia… + Umeingia kwa mafanikio! + Imeshindwa kupata kikao kutoka Last.fm + Kuingia imeshindwa: %s + Sifa za Last.fm zimehifadhiwa! + Usafishaji wa metadata + Safisha metadata + Safisha kiotomatiki machafuko ya kawaida (kama \"Official Video\", \"Remaster\", \"Live\") kutoka kwenye vichwa vya habari, wasanii, na albamu kabla ya scrobbling + Usafishaji wa regex wa kulingana na mahitaji + Ingiza expression ya kawaida ya kistari ili kuondoa maandishi ya ziada kutoka kwenye majina ya nyimbo, wasanii, au albamu. Mfano: (?i)\\s*\\((clean|explicit)\\) + Changanisha msanii na wimbo kutoka kwenye kichwa + Toa jina la msanii na wimbo kutoka kwenye kichwa cha video kwa kutumia kitenganishi cha \" - \". Inafaa kwa maudhui yaliyopakwa tena ambapo jina la chaneli linaweza kutumika kama msanii + Jaza albamu inayokosekana + Muziki unapokuwa hauna albamu, itafute kwenye Last.fm ili scrobble yako ikamilike + "Scrobbling husawazisha kiotomatiki matumizi yako ya YouTube Music na wasifu wako wa Last.fm au ListenBrainz + +Ili kuhakikisha scrobbling inasawazishwa wakati wa uchezaji wa nyuma, hakikisha kuwa matumizi ya betri ya programu ya YouTube Music yamewekwa kuwa \"Haina vizuizi\"" + Tumia mashairi ya upande wa tatu + Huonyesha mashairi kutoka kwa huduma ya upande wa tatu, na kurudi kwenye mashairi yaliyojengwa ndani wakati hakuna yanayopatikana + Weka skrini ikiwa wazi wakati wa kutazama mashairi + Huzuia kioo kuzima wakati jopo la mashairi likiwa wazi + Huduma ya mashairi + Onyesha + Usawazishaji + Jopo + Kituo cha AI + Watoa huduma wote wamezimwa - mashairi yaliyojengwa ndani na mashairi yaliyojumuishwa + Weka upya mpangilio wa watoa huduma? + Rudisha watoa huduma katika mpangilio wa kipaumbele wa asili + Futa mashairi yaliyohifadhiwa + Futa mashairi yaliyohifadhiwa kwenye kifaa hiki + Imefuta %1$s MB + Pata tokeni + Token ya Mtumiaji wa Media ya Apple Music + "Bandika kuki ya media-user-token kutoka music.apple.com + +Hatua: +1. Ingia kwenye music.apple.com kama mtumiaji mwanachama +2. Fungua DevTools → Application → Cookies → music.apple.com +3. Nakili thamani ya \"media-user-token\"" + Apple Music: media-user-token imehifadhiwa + Apple Music: media-user-token imefutwa + Apple Music: media-user-token si sahihi - tafadhali angalia na ujaribu tena + Spotify SpDc + "Bandika cookie ya sp_dc kutoka Spotify. + +Hatua: +1. Ingia kwenye open.spotify.com +2. Fungua DevTools → Application → Cookies → open.spotify.com +3. Nakili thamani ya \"sp_dc\"" + Spotify: sp_dc imehifadhiwa + Spotify: sp_dc imefutwa + Spotify: sp_dc si sahihi - tafadhali angalia na ujaribu tena + Token ya Apple Music ni ya hiari. Bila hiyo, Lyrically itatumika kama mbadala + Deezer ARL + "Bandika cookie ya arl kutoka Deezer. + +Hatua: +1. Ingia kwenye deezer.com +2. Fungua DevTools → Application → Cookies → deezer.com +3. Nakili thamani ya \"arl\"" + Deezer: arl imehifadhiwa + Deezer: arl imefutwa + Deezer: arl si sahihi - tafadhali angalia na ujaribu tena + Token ya Musixmatch + Ongeza tokeni ili kutumia akaunti yako mwenyewe pekee. Lazima itoke kwenye programu ya Musixmatch ya Android: tokeni zilizonakiliwa kutoka musixmatch.com ni za programu ya zamani ya kompyuta na API hazikubali + Musixmatch inafanya kazi bila tokeni, hivyo sehemu hii inaweza kubaki wazi + token ya mtumiaji + Musixmatch: token ya mtumiaji imehifadhiwa + Musixmatch: token ya mtumiaji imefutwa + Musixmatch: token ya mtumiaji si sahihi - tafadhali angalia na ujaribu tena + Kuki za YouTube + "Bandika kuki za YouTube ili kutafuta maelezo ya maandishi na muda wa maandishi kutoka YouTube. + +Hatua: +1. Fungua youtube.com kwenye kivinjari na uingie +2. Fungua DevTools (F12) → Application → Cookies → youtube.com +3. Nakili mfululizo kamili wa kuki (jozi za key=value zinazotenganishwa na nukta mkato) + +Kuki zinazohitajika: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPISID, LOGIN_INFO" + YouTube: kuki zimehifadhiwa + YouTube: kuki zimefutwa + YouTube: kuki hazifai - tafadhali kagua na ujaribu tena + Gusa na ushikilie mstari + Gusa mstari ili uruke hadi wakati huo katika wimbo, bonyeza kwa muda mrefu ili kunakili neno lililo chini ya kidole chako, au mstari mzima wakati bonyezo halipo kwenye neno. Kuzima huondoa ishara zote mbili + Onyesha lebo za sehemu + Weka lebo katika kila sehemu mpya ya mashairi kwa lebo ndogo, kama vile Verse au Chorus + Onyesha kitufe cha Kunakili + Gusa ili kunakili mashairi ya awali kama maandishi ya kawaida; bonyeza kwa muda mrefu ili kuhifadhi faili ya chanzo cha mashairi ghafi kwenye Pakua + Onyesha kitufe cha Kutafsiri + Gusa ili kubadilisha tafsiri; bonyeza kwa muda mrefu kwa ajili ya tafsiri pekee. Hutumia tafsiri za mtoa huduma, Google Translate, au API inayooana na OpenAI + Onyesha kitufe cha Romanize + Gusa ili kubadilisha uromanizaji; bonyeza kwa muda mrefu kwa ajili ya uromanizaji pekee. Hutumia uromanizaji wa mtoa huduma, Google Translate, au API inayooana na OpenAI + Uromanizaji na Ulinganifu wa Ndani + Badili kuwa herufi za Kilatini kwenye kifaa wakati mtoa huduma, modeli yako na Google Translate vinaposhindwa. Usomaji hufuata kila neno la mashairi yenye muda; usawazishaji wa maneno lazima uwe umewashwa. Kichina, Kikantoni, Kijapani, Kikorea, Kigiriki, Kirusi, Kijorgia, Kiarmenia; takriban 120 MB + Inasoma %1$s: %2$d%% + Usomaji uko tayari (%1$d entries) + Pakua imefeli - zima na uwashe tena ili kujaribu tena + Onyesha kitufe cha Refresh + Gusa ili kubadilisha kwenda kwa mshindani mwingine wa mashairi; bonyeza kwa muda mrefu ili kutafuta mashairi kwa kichwa cha wimbo na msanii + Tafuta mashairi + Kichwa cha wimbo + Msanii + Ingiza kichwa cha wimbo na msanii + Hakuna mtoa huduma aliyewezeshwa anayeweza kutafuta nyimbo zingine + Imehifadhiwa + Angazia maneno au silabi zilizooanishwa + Toa kipaumbele kwa mashairi ya neno-kwa-neno yanapopatikana. Mashairi yanapokuwa na muda wa neno, yanangazia kila neno au silabi kulingana na wakati wa muziki + Ficha mashairi yaliyochezwa + Ficha mistari ya mashairi iliyo juu ya mstari unaochezwa sasa + Ficha mashairi ambayo hayajachezwa + Ficha mistari ya mashairi chini ya mstari unaocheza sasa + Onyesha Mashairi ya MediaSession kwenye skrini ya kufunga + Inaonyesha mstari wa sasa kwenye skrini ya kufunga, Android Auto na Bluetooth. Inahitaji Android 11 au mpya zaidi + Onyesha Mashairi ya Miniplayer + Inaonyesha mstari wa sasa katika miniplayer, huku wimbo ukihamishiwa kwenye kichwa kidogo + Muundo wa kichwa cha wimbo/msanii + Muundo wa wimbo unaoonyeshwa na mashairi katika Miniplayer na MediaSession. {t} ni kichwa, {a} ni msanii na {v} ni toleo, kama vile Acoustic, wakati chanzo cha mashairi kinapotoa. Maandishi kati ya koloni mbili huonyeshwa tu wakati kila kigezo ndani yake kina thamani; andika \\: kwa ajili ya koloni ya kawaida. Kila mstari huonyeshwa kwa zamu, ukisonga mbele kila mashairi yanapobadilika + Muundo wa mstari wa shairi + Muundo wa mstari wa mashairi katika Miniplayer na MediaSession. {lrc} ni mstari na {n} ni idadi ya mara unapoimbwa mfululizo. Maandishi kati ya koloni huonyeshwa tu wakati mstari unajirudia. Tumia \\: kwa ajili ya koloni + Muundo lazima uwe na {lrc} + Kiolezo cha mstari wa msanii na muda wa uhuishaji + Muda wa chini wa skrini (ms) + Muda wa juu wa skrini (ms) + Kwa uhuishaji wa mistari mingi pekee. Weka kila fremu kwa angalau muda wa chini. Katika kiwango cha juu, songesha mstari wa msanii hata kama mashairi yanabaki vile vile. + Kiwango cha chini lazima kiwe 0 au zaidi. Kiwango cha juu lazima kiwe zaidi ya 0 na angalau sawa na kiwango cha chini. + Mipaka ya urefu wa mstari + Mipaka ya mistari ya mashairi na msanii, na uzito wa herufi pana + "Mpaka wa mstari wa shairi +と呼ばれるGawanya mashairi marefu katika nusu zenye usawa. 0 huzima ugawanyaji." + "Mpaka wa mstari wa msanii +Gawanya muundo wa nyimbo uliotumiwa katika fremu zinazozunguka. 0 huzima ugawanyaji." + "Uzito wa herufi pana +Inatumika kwa mipaka yote miwili. Katika 1.75, herufi nne pana zinahesabiwa kama 7." + Ingiza mipaka ya namba nzima kuanzia 0 hadi 10000 na uzito wa herufi zaidi ya 0 + Mapumziko ya ala + Inaonyeshwa kwenye Miniplayer na MediaSession wakati wa mapumziko ya ala. Kila mstari ni fremu moja ya uhuishaji. {t} ni kichwa, {a} ni msanii na {lrc} ni mstari wa mwisho wa mashairi + Offset ya muda ya Miniplayer na MediaSession + Inasogeza wakati mstari unaofuata unapoonyeshwa kwenye Miniplayer na MediaSession. Tumia thamani hasi ikiwa mistari inaonekana kwa kuchelewa, kwa mfano kwenye skrini ya kufungia + Vichujio + Regex ya metadata + Kielezo cha kawaida (Regular expression) kinachotumika kwenye vichwa, wasanii, na albamu kabla ya kutafuta mashairi na wakati wa kukadiria ikiwa vichwa viwili vinapatana. Acha wazi ili kuzizima zote mbili. Chaguo-msingi huondoa machafuko ya kawaida kama \"Official Video\", \"Remaster\", \"【插曲】\", na \"- Topic\" + Regex ya kichujio cha mashairi + Regular expression inayotumika kwenye maandishi ya asili ya mashairi pekee (si tafsiri au romanizations). Acha wazi ili kuzima + Alama za mstari wa maelezo + Ikiwa mashairi yamechujwa vibaya hadi chini, rekebisha au futa hili. Alama zinazotenganishwa na mkato: mistari yenye alama inayofuatiwa na nukta pacha inaonyeshwa kama taarifa za sifa chini. Acha ikiwa imebaki tupu ili kuzima + Anza na herufi kubwa + Inaanza na herufi kubwa kila mstari wa mashairi + Ondoa alama za uakifishaji za mwisho + Huondoa nukta, koma, koloni na nukta mkato mwishoni mwa mistari ya mashairi. Alama za kuuliza, alama za mshangao, nukta tatu, alama za nukuu na mabano hubaki + Ukubwa wa maandishi + Ukubwa wa maandishi ya mashairi + Offset ya muda ya kimataifa + Mashairi yaliyosawazishwa yanafuata uchezaji. Ikiwa yanapishana, rekebisha kiasi cha muda. Sogea mashairi yaliyosawazishwa mbele au nyuma. Tumia thamani chanya ikiwa mashairi yanatangulia wimbo + Tumia sehemu za SponsorBlock kwa offset ya mashairi + Linganisha bila sehemu ambazo SponsorBlock huruka na kusogeza mashairi kwenye sauti iliyotolewa. Ikiwa imezimwa, inajipuuza SponsorBlock kabisa + Wimbo huu hauna id ya video ya kuwasilisha + Chanzo: %s + Bonyeza na ushikilie kitufe cha kunakili ili kuhifadhi LRC hii; ikiwa unaweza kurekebisha muda na kuyashiriki na watoa huduma wa jamii, tutashukuru + Imetafsiriwa na Google + Imeandikwa kwa herufi za Kilatini na Google + Imetafsiriwa na %s + Imeromanishwa na %s + Imeandikwa kwa herufi za Kilatini na %s + Imetafsiriwa + Lugha ya tafsiri + Lugha ya uandishi wa herufi za Kilatini + Inatafsiri… + Hakuweza kutafsiri mashairi + Imeandikwa kwa herufi za Kilatini + Kiotomatiki + Inaandikwa kwa herufi za Kilatini… + Imeshindikana kuandika mashairi kwa herufi za Kilatini + Imetafsiriwa pekee + Iliyoandikwa kwa herufi za Kilatini pekee + Hali pacha + Imenakiliwa kwenye ubao wa kunakili + Inaboresha + Imesasishwa + Ufunguo wa API ya QuickLRC + QuickLRC hutoa nakala au husawazisha muda wa mashairi kwa wimbo wa sasa. Huduma tofauti, isiyohusiana na kituo kinachoendana na OpenAI hapo juu + Ufunguo wa QuickLRC umehifadhiwa. Tumia kitufe cha kusawazisha katika dialog ya utafutaji wa mashairi, au prompt ya jopo wakati wimbo hauna mashairi + Ufunguo wa API ya QuickLRC umehifadhiwa + QuickLRC haijakubali ufunguo huo + Ufunguo wa API ya QuickLRC umefutwa + QuickLRC huweka muda wa mashairi ambayo hayana, au hutoa nakala kutoka kwa wimbo, kwa gharama ya credits. Ukiwa na ufunguo uliwekwa, nyimbo zisizo na mashairi hutoa transcription na dialog ya utafutaji hupata kitufe cha usawazishaji + Omba mpangilio wa QuickLRC + Inapanga + Tumia mpangilio + Omba nakala ya QuickLRC + Inatengeneza nakala + Nakala imefutwa + Ni nyimbo tu zinazochezwa kutoka YouTube zinazoweza kunukuliwa + Hakuna mashairi ya kusawazisha - badala yake hutoa nakala + QuickLRC haikuweza kuchakata wimbo huu + Kredi za QuickLRC zimeisha kwa wimbo huu + "Mashairi yanalinganishwa kwa kichwa na msanii, metadata zisizo za kawaida zinaweza zisipate chochote au kupata toleo lisilo sahihi. Tumia refresh ili kubadilisha matoleo. Watoa huduma walioashiriwa na * wanahitaji token. Watoa huduma wote walioidhinishwa hawajombwi kila wakati kwa utaratibu wako, bali wanajibu kulingana na kasi yao.\n\nMashairi yanatoka kwa huduma za upande wa tatu ambazo hazihusiani, hazifadhiliwi, au hazijaidhinishwa na Morphe. Kila moja hutumika kama ilivyo: mashairi, upatikanaji wake, na leseni zake zinabaki na huduma hizo na wenye leseni zao, na kuzitumia kunaweza kuwa chini ya masharti yao ya huduma. Kinachofika kwenye huduma iliyoidhinishwa ni utafutaji tu uliouomba - kichwa, msanii, na, pale huduma hiyo inapohitaji, ID ya video. Endpoint ya AI ya hiari na transcription ya QuickLRC hufanya kazi tu baada ya kuzisanidi, na kupokea maandishi ya mashairi au URL ya video ya wimbo." + Washa tu watoa huduma unaowatumia, au baki na chaguo-msingi - maombi ya mara kwa mara yanaweza kulemea tovuti ndogo za mashairi za jamii + Ficha mstari wa sifa + Ficha mstari wa sifa unaoonekana chini ya mashairi + Badilisha nafasi ya tafsiri na uandishi wa kimatamshi + WASHI huweka uandishi wa kimatamshi chini ya mashairi na tafsiri juu; ZIMA huweka uandishi wa kimatamshi juu na tafsiri chini + Tafsiri + Uromanizaji + Mtoa huduma wa tafsiri + Google (Kawaida, Bure) + Inayooana na OpenAI (Kibinafsi) + Mtoa huduma wa Romanization + Google (Chaguo-msingi) + Kamusi ya Ndani / Nje ya Mtandao + Inayooana na OpenAI (Kutengeneza) + Sehemu ya AI + Inatumika tu wakati mtoa huduma hatumi lebo za sehemu kama Chorus au Verse, ambazo kisha zinatambuliwa na modeli yako inayooana na OpenAI + AI & Vituo vya Wingu + Sanidi funguo za API, modeli, vituo, na templeti za prompt kwa watoa huduma wa AI. + URL ya kituo cha API (mfano https://api.openai.com/v1/chat/completions) + Kiolezo cha Prompt + Viashiria: {language}, {task}, {title}, {artist}, {count}, {lines} + Prompt imehifadhiwa + Prompt imehifadhiwa, lakini kituo cha AI hakiwezi kutumiwa bado. Kusanidi kwanza chini ya Mipangilio ya AI + Kuweka upya prompt + JSON si sahihi. Irekebishe au safisha uwanja huu kabla ya kuhifadhi + Mipangilio ya Tafsiri & Romanization imehifadhiwa + Mipangilio imerejeshwa katika chaguo-msingi + URL ya msingi lazima ianze na http:// au https:// + Jaribu Muunganisho + Sanidi Vituo vya Mwisho + Onyesha Vituo Vyote vya Mwisho + Inathibitisha muunganisho… + Mtoa Huduma ya Tafsiri + Mtoa Huduma ya Romanization + Lete Model + Inachukua mifano… + Chagua Mfano + Imeshindwa kuchukua mifano. Angalia ufunguo wa API au muunganisho. + Mipangilio ya Gemini API + Inayooana na OpenAI (Endpoint ya Kijumla) + Mipangilio ya DeepL API + Ufunguo wa API + Mfano + URL ya Msingi + Ufunguo wa API (hiari) + Ombi la tafsiri + Mwili wa ziada wa ombi (JSON ya hiari) + Ufunguo wa uthibitishaji (Bure au Pro) + Sanidi funguo za API, mifumo, endpoints, na maombi + 8.23.51 - Zima aikoni nzito + 9.13.51 - Rudisha tabu za Player Up next, Mashairi, na Zinazohusiana diff --git a/patches/src/main/resources/addresources/values-sw-rKE/reddit/strings.xml b/patches/src/main/resources/addresources/values-sw-rKE/reddit/strings.xml index cf7776fdaf..dcd2274b23 100644 --- a/patches/src/main/resources/addresources/values-sw-rKE/reddit/strings.xml +++ b/patches/src/main/resources/addresources/values-sw-rKE/reddit/strings.xml @@ -15,4 +15,17 @@ Second \"item\" text" --> - + + Sera ya faragha ya Reddit + Kuingia kwa Google hakupatikani. Tumia barua pepe kuingia + Font ya kistaili + Pakia faili la font la TTF au OTF la kistaili kutoka kwenye njia iliyo hapa chini. Kuziwezesha hizi pia kuzima Lazimisha font ya mfumo + Faili la font ya kistaili + Gusa ili kuchagua faili la font la TTF au OTF + Font iliyochaguliwa - %s + Chagua faili la font la kistaili kwanza + Tafadhali chagua faili la font la .ttf au .otf + Imeshindwa kupakia font ya kistaili. Chagua faili tofauti + Lazimisha fonti ya mfumo + Tumia font ya mfumo wa kifaa badala ya Reddit Sans au Roboto. Kuziwezesha hizi pia kuzima Font ya kistaili + diff --git a/patches/src/main/resources/addresources/values-sw-rKE/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-sw-rKE/shared-youtube/strings.xml index fd007eeb78..e454f779df 100644 --- a/patches/src/main/resources/addresources/values-sw-rKE/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-sw-rKE/shared-youtube/strings.xml @@ -18,46 +18,297 @@ Second \"item\" text" --> + Uchochezi wa kuanza + Imezimwa + Rangi + Rangi moja + + Anzisha upya ili kumalizia usanidi + Jina la programu + Rangi ya nyuma ya mandhari nyeusi + Nyeusi kabisa + Material You (Nyeusi kabisa) + YouTube ya Kawaida + Pinki nzito + Bluu nzito + Kijani kizito + Njano nzito + Chungwa nzito + Nyekundu iliyokolea + Mandhari ya giza ya kiserikali + Weka rangi ya mandhari ya mandhari ya giza + Chaguo-msingi la programu + Material You (Kawaida) + Material You (Kuu) + Material You (Sekondari) + Material You (Tati) + Kiserikali + Pakua + Hali ya kupakua + Ya kawaida + Pakua ndani ya programu + Kipakua cha nje + Jina la kifurushi cha kipakuzi + Jina la kifurushi cha programu yako ya kipakuzi ya nje iliyosakinishwa + Ingiza jina la kifurushi + Nyingine + Programu haijasakinishwa + %s haijasakinishwa. Tafadhali isakinishe + "Hakuwezekana kupata programu iliyosakinishwa yenye jina la kifurushi: %s + +Hakiki ikiwa jina la kifurushi ni sahihi na programu imesakinishwa" + Jina la kifurushi haliwezi kuwa tupu + Kutengeneza + Aikoni ya programu + Asilia + Morphe nyeusi + Morphe nyeusi nzito + Morphe nyeupe + Morphe play light + Morphe play nyeusi + Kibinafsi + "Hii inabadilisha aikoni inayoonyeshwa kwenye kizinduli cha programu yako. + +Mpangilio huu hautabadilisha aikoni katika maeneo haya yote: +• Orodha ya programu ya mipangilio ya mfumo wa kifaa +• Arifa fulani za push +• Chagua programu ya 'Fungua kwa' +• Aikoni ya Android Auto + +Ili kubadilisha aikoni kila mahali, patch tena na ubadilishe chaguo la patch la 'Aikoni ya Programu'" + Aikoni ya taarifa + Linganisha na aikoni ya programu + Morphe play + Ilani ya Morphe + Historia yako ya kutazama haihifadhiwi.<br><br>Hii inasababishwa zaidi na kizuia matangazo cha DNS au proksi ya mtandao.<br><br>Ili kurekebisha hili, weka <b>s.youtube.com</b> kwenye orodha nyeupe au zima vizuia DNS na proksi zote + Usionyeshe tena + Ghushi mkondo wa video + Sanidi uigaji wa mteja ili kuzuia matatizo ya uchezaji + "Hurekebisha matatizo ya uchezaji kwa watumiaji wengi kwa kuomba mkirimo wa video unaoendana + +Ikiwa wewe ni mtumiaji wa YouTube Premium, mpangilio huu unaweza usihitajike" + Hakuweza kupata mikondo yoyote ya kliyenti + Klijenti ya kawaida haifanyi kazi + "%1$s haikufanya kazi mara kadhaa mfululizo, lakini %2$s ilifanya. + +Je, uiweke %2$s kuwa klijenti ya kawaida?" + Badilisha + Kuzima mpangilio huu kunaweza kusababisha matatizo ya uchezaji + Jaribu kuwezesha: Ghushi utiririko wa video > Ingia kwenye Android VR + Kliyenti ya kawaida + Ingia kwenye Android VR + Hujaingia kwenye Android VR. Gusa hapa ili uingie + Hivi sasa umeingia kwenye Android VR + Ili kutumia kipengele hiki, badilisha \'Klijenti ya chaguo-msingi\' kuwa Android VR + Ingia kwenye Android VR + "Kuingia kwenye Android VR kunaweza kurekebisha matatizo ya uchezaji kwa baadhi ya watumiaji + +Ili kuingia, bonyeza endelea hapa chini" + Toka + Maelekezo + "Kodi ya uanzishaji: %s + +1. Gusa 'Fungua tovuti' hapa chini ili kufungua tovuti ya 'Unganisha kifaa' +2. Bandika kutoka kwenye ubao wa kunakili (au andika) kodi ya uanzishaji inayoonyeshwa hapo juu +3. Ingia kwenye Google ukihitajika. Ikiwa una akaunti nyingi, chagua akaunti hii ya YouTube +4. Gusa 'Ruhusu YouTube VR' kupata ufikiaji +5. Rudi kwenye programu hii baada ya kufika kwenye ujumbe wa 'Kifaa kimeunganishwa'" + Imeshindikana kutoa kodi ya uanzishaji + Imeshindikana kutoa tokeni ya uthibitishaji + Umeingia kikamilifu + "Uthibitisho umehifadhiwa + +• Mipangilio yako ya Morphe haitaagiza/kuhamisha uthibitisho wa Android VR +• Ikiwa programu hii haitatumika kwa miezi 6 basi Google itaondoa uthibitisho huu +• Utahitajika kuingia tena ikiwa utabadilisha nywila yako ya Google au kuondoa kifaa hiki kwenye mipangilio yako ya Google" + OAuth2 haipatikani kwa muda (API imeisha muda) + Muda wa kuingia kwenye Android VR umeisha, jaribu kuingia kwenye VR tena + OAuth2 haipatikani kwa muda (hali %s) + OAuth2 haipatikani kwa muda + Huwezi kuingia kwenye VR, uthibitishaji wa tovuti haukukubaliwa + Huwezi kuingia kwenye VR: %s + Umetoka kwenye Android VR + Tokeni ya kuingia kwenye VR si sahihi, jaribu kuingia tena kwenye VR + Vidhibiti vya vyombo vya habari vya taarifa + Badilisha vidhibiti vya uchezaji vinavyoonyeshwa kwenye skrini yako ya kufulia na paneli ya taarifa + Ficha vitufe vya Nyuma & Itakuja + Huficha vitufe vya Nyuma na Itakuja katika taarifa ya vyombo vya habari, skrini ya kufulia, na vidhibiti vya spika za masikioni + Zima seekbar + Huzuia kuburuta seekbar kwenye skrini ya kufulia na paneli ya taarifa + Tafuta kwa vitufe vya Nyuma & Fuata + Inabadilisha vidhibiti vya arifa ya media, skrini ya kufunga, na headphones ili kutafuta badala ya kubadilisha video + Urefu wa kurudisha nyuma + Sekunde za kurudisha nyuma kwa kifungo cha Nyuma + Urefu wa kusogeza mbele + Sekunde za kusogeza mbele kwa kifungo cha Fuata + Urefu wa utafutaji lazima uwe kati ya sekunde 1 na %s. Inatumia chaguo-msingi + Zima itifaki ya QUIC + Zima sauti ya DRC + Lazimisha lugha asilia ya sauti + Daima hucheza mkondo wa sauti asilia wa video badala ya toleo lililotafsiriwa + Ruka ukimya + Huruka kiotomatiki vipindi vya ukimya katika sauti ya video + Urefu wa chini wa pause + Ni mapumziko ya urefu huu au zaidi pekee ndiyo yanayofupishwa. Itaanza kufanya kazi video inayofuata inapoanza + Proksi ya mtandao + Elekeza maombi ya mtandao yanayoungwa mkono kupitia proksi + Washa proksi + Inaelekeza maombi ya mtandao yanayoungwa mkono kupitia proksi ya HTTP au HTTPS iliyosanidiwa + Host ya proxy + Jina la host au anwani ya IP ya seva ya proxy + Bandari ya proxy + Bandari ya seva ya proxy + Tumia proxy ya HTTPS + Tumia HTTPS kuunganisha kwenye seva ya proxy badala ya HTTP + Uthibitisho wa proxy + Hutuma uthibitisho wa Msingi na maombi ya proxy + Jina la mtumiaji wa proksi + Jina la mtumiaji kwa uthibitisho wa msingi wa proksi + Nywila ya proksi + Nywila kwa uthibitisho wa msingi wa proksi + Ruhusu kurudi moja kwa moja + Ungana moja kwa moja ikiwa proksi itashindwa + Ficha matangazo ya skrini nzima + Ili kutumia kipengele hiki, badilisha \'Spoof video streams\' kuwa mteja yeyote isipokuwa \'Android Studio\' na \'Android VR Downgraded\' + Safisha viungo vya kushiriki + Huondoa vigezo vya ufuatiliaji kutoka kwenye viunganishi vilivyoshirikiwa + Badilisha viungo vya kushiriki kuwa youtube.com + Inabadilisha music.youtube.com kuwa youtube.com katika viungo vilivyoshirikishwa + Badilisha viunganishi vya kushiriki kuwa youtu.be + Inalazimisha video kutumia muundo mfupi wa URL wa youtu.be + Itifaki ya kurekodi hitilafu ya protobuffer + Huongeza taarifa za ziada za kurekodi kwa ajili ya watengenezaji ili kutambua vipengele vya programu + "Kuwasha mpangilio huu kutarekodi data ya ziada ya mpangilio, ikiwa ni pamoja na maandishi kwenye skrini kwa baadhi ya vipengele vya UI. + +Hii inaweza kusaidia kutambua vipengele wakati wa kuunda vichujio vya kistaili. + +Hata hivyo, kuwasha hili pia kutarekodi baadhi ya data ya mtumiaji kama vile anwani yako ya IP" + Kichujio cha kistaili + Sanidi vichujio vya kistaili ili kuficha kipengele chochote + Wezesha kichujio cha kistaili + Orodha ya nyuzi za mjenzi wa njia ya kipengele za kuchuja, zilizotenganishwa kwa mistari mipya + Chujio cha kawaida kisicho sahihi: %s + Chujio la menyu ya mipangilio + Ficha vipengele kwenye skrini ya kawaida ya mipangilio ya programu kwa jina lao linaloonekana + Fungua skrini ya kawaida ya mipangilio ya programu mara moja ili kujaza orodha + Ondoa kisanduku cha tahadhari ya mtazamaji + Hii hairuhusu kupuuza kizuizi cha umri. Inakubali tu kiotomatiki + Mtoa huduma wa PoToken + Sanidi mtoa huduma wa PoToken ili kuzuia matatizo ya uchezaji + Mtoa huduma wa PoToken wa nje + "Hutatua matatizo ya uchezaji kwa kutumia minter ya nje ya PoToken (Ya majaribio) + +Ikiwa wewe ni mtumiaji wa YouTube Premium, mpangilio huu huenda usihitajike" + "Hurekebisha matatizo ya uchezaji kwa kutumia minter ya PoToken ya nje (Majaribio) + +Ili kutumia kipengele hiki, zima 'Spoof video streams'" + Ikiwa haifanyi kazi, jaribu kufunga MicroG kwa lazima + Fungua mipangilio ya PotHelper + "Kipengele hiki kwa sasa kiko katika hatua ya uundaji na huenda kisifanye kazi + +Gusa hapa kupakua PotHelper" + PotHelper haifanyi kazi tena + "Tokeni iliyojengwa ndani ya toleo hili la PotHelper haifanyi kazi tena + +Washa 'Ghushi mifuatisho ya video' ili kurekebisha uchezaji" + Washa + Ghushi toleo la programu + Huidanganya seva ifikirie kuwa unatumia toleo la zamani la programu ili kurejesha vipengele vya zamani vya UI + "Toleo la programu litafanyiwa uigizaji wa toleo la zamani. + +Hii itabadilisha mwonekano na vipengele vya programu, lakini athari zisizojulikana zinaweza kutokea. + +Ikiwa itazimwa baadaye, inapendekezwa kufuta data ya programu ili kuzuia hitilafu za UI" + Ghushi toleo la programu linalolengwa + Kiwango cha kuburudisha cha programu + Chaguo-msingi + %s FPS + Ghadhabishia kiwango cha kuburudisha + Daima + Uchezaji wa wima + Uchezaji wa skrini nzima + Uchezaji wa wima na skrini nzima + Fonti + Kawaida + Fonti ya mfumo wa kifaa + Faili la fonti ya kiserikali + Faili la fonti ya kiserikali + Chagua faili la fonti la TTF au OTF + Faili lililochaguliwa si fonti ya TTF au OTF + Font ya kistaili haikuweza kupakiwa na imezimwa + Kutopenda hazipatikani kwa muda (API imekwepa muda) + Kutopenda hakupatikanika (hali %s) + Kutopenda hazipatikani (kikomo cha API ya mteja) + Kutopenda hazipatikani (%s) + Imefichwa na mmiliki + Hapendi video hii, pamoja na watu wengine %s + Hapendi video hii, %s ya kura ni kutopenda + Washa Return YouTube Dislike + Onyesha kutopenda kama asilimia + Inaonyesha kutopenda kama asilimia ya kupenda badala ya namba + Onyesha makadirio ya kupenda + Inaonyesha makadirio ya idadi ya kupenda kwenye video ambapo muundaji amezima kupenda + Onyesha tahadhari za hitilafu ya muunganisho + Inaonyesha taarifa wakati seva ya Return YouTube Dislike inapopatikana + "Return YouTube Dislike hurudisha idadi ya kutopenda ambayo YouTube ilificha mnamo 2021. Idadi hizi zinatokana na data zilizohifadhiwa kabla ya kuondolewa na zinakadiriwa kwa video mpya kutokana na kura za watumiaji wake, hivyo ni za takriban na husasishwa kila baada ya siku chache + +ID ya video unayotazama hutumwa kwenye Return YouTube Dislike API, na likes na dislikes zako hutumwa bila utambulisho ili kuboresha makadirio" + Rudisha takwimu za API za Return YouTube Dislike za kifaa hiki + Wakati wa majibu ya API, wastani + Wakati wa majibu ya API, kiwango cha chini + Wakati wa majibu ya API, kiwango cha juu + Wakati wa majibu ya API, media ya mwisho + Dislikes hazipatikani kwa muda - Kikomo cha kiwango cha API cha mteja kiko kwenye utendaji + Kupata kura za API, idadi ya wito + Hakuna wito wa mtandao uliofanywa + miito %s ya mtandao imefanyika + API fetch votes, idadi ya timeouts + Hakuna wito wa mtandao uliopata timeout + miito %s ya mtandao imekwisha muda + Vikomo vya kiwango cha mteja wa API + Hakuna vikomo vya kiwango cha mteja vilivyopatikana + Kikomo cha kiwango cha mteja kimekutana mara %s + milisekunde %s diff --git a/patches/src/main/resources/addresources/values-sw-rKE/shared/strings.xml b/patches/src/main/resources/addresources/values-sw-rKE/shared/strings.xml index 31cdbe4dcb..d149ac578e 100644 --- a/patches/src/main/resources/addresources/values-sw-rKE/shared/strings.xml +++ b/patches/src/main/resources/addresources/values-sw-rKE/shared/strings.xml @@ -17,21 +17,218 @@ Second \"item\" text" --> + Mipangilio + Je, una uhakika unataka kuendelea? + Hifadhi + Weka upya + Weka upya rangi + Rangi batili + Kuanzisha upya kunahitajika + Anzisha upya programu ili mabadiliko haya yaweze kufanya kazi + Anzisha upya + Ingiza + Nakili + Mipangilio ya Morphe imewekwa upya hadi ya awali + Mipangilio ya %s imeingizwa + Uingizaji umeshindwa: %s + Nenda juu + Tafuta mipangilio + Kuhusu + Hakuna matokeo yaliyopatikana kwa \'%s\' + Jaribu neno lingine la utafutaji + Utafutaji wa hivi karibuni + Ondoa kwenye historia ya utafutaji? + Ondoa kwenye historia ya utafutaji + Futa historia ya utafutaji + Je, una uhakika unataka kufuta historia yote ya utafutaji? + Vidokezo vya Utafutaji + "• Gusa njia ili kuielekea +• Bonyeza na ushikilie mpangilio ili kuuelekea +• Bonyeza Enter ili kuhifadhi ombi la utafutaji kwenye historia +• Utafutaji hautofautishi herufi kubwa na ndogo au alama za uakifishaji +• Mipangilio mikuu huonekana juu ya mipangilio midogo isiyofanya kazi" + Historia ya utafutaji haina kitu + Ili kuhifadhi historia ya utafutaji, andika ombi la utafutaji na ubonyeze Enter + Onyesha historia ya utafutaji wa mipangilio + Jina la mipangilio ya Morphe + Chaguo-msingi + Ziada + Marekebisho + Muda wa Morphing + Kibinafsi + Ingiza jina + Jina haliwezi kuwa tupu + Onyesha aikoni za mipangilio ya Morphe + Imenakiliwa: %s + Lugha ya Morphe + "Tafsiri za baadhi ya lugha zinaweza kuwa hazipo au hazijakamilika. + +Ili kutafsiri lugha mpya au kuboresha tafsiri zilizopo, tembelea morphe.software/translate" + Lugha ya programu + Agiza / Toa + Agiza / Toa mipangilio ya Morphe + Import kutoka kwenye faili + Mipangilio imeingizwa kwa mafanikio, gusa Hifadhi ili kutumia + Imeshindwa kuingiza mipangilio + Export kwenda kwenye faili + Mipangilio imetolewa kwa mafanikio + Imeshindwa kutoa mipangilio + Unatumia toleo la hivi punde la Morphe Patches <i>%s</i> + Unatumia Morphe Patches toleo la <i>%1$s</i><br><br><b>Sasisho lipo: <i>%2$s</i></b><br><br>Ili kusasisha patch zako, tumia Morphe kuweka patch tena kwa programu hii + Kumbuka + Toleo hili ni la awali na unaweza kukumbana na matatizo yasiyotarajiwa + Viungo rasmi + Tovuti + Changia + Tafsiri + Kredi + Imesasishwa + Sasisho lipo + Maendeleo ya sasa + Maendeleo ya awali + Maendeleo yanayoendelea + 2022 ‑ 2025 + 2022 ‑ 2025 + 2019 ‑ 2022 + Ilani za leseni + Inapakia… + Kutambua hitilafu + Sanidi chaguzi za juu za msanidi programu na logi za utambuzi + Washa urejeshaji wa debug + Kumbukumbu ya kosa la debug (stack trace) + Inajumuisha stack traces katika logi za debug ili kusaidia kutambua makosa + Onyesha taarifu ya toast wakati wa hitilafu ya Morphe + "Kuzima toast za makosa huficha arifa zote za makosa za Morphe. + +Hautataarifiwa kuhusu matukio yoyote yasiyotarajiwa" + Hamisha logi za debug + Exporta rekodi za debug za Morphe + Kumbukumbu za debug zimehamishwa kikamilifu + Imeshindwa kuhamisha kumbukumbu za debug + Tafuta kumbukumbu za debug… + Futa + Kurekodi hitilafu kumezimwa + Hakuna kumbukumbu za debug zilizopatikana + Kumbukumbu za debug zimenakiliwa + Imeshindwa kuhamisha kumbukumbu za debug: %s + Kumbukumbu za debug zimefutwa + Meneja wa alama za vipengele + Simamia alama za boolean za vipengele + Tafuta bendera za vipengele… + Haitumiki (%s) + Inatumika (%s) + Imezuiliwa (%s) + Ime forced (%s) + Kiotomatiki + Zima + Washa + Washa kwa nguvu + Zuia + Ondoa + Alama za vipengele zimehifadhiwa + Alama za vipengele zimerejeshwa + Viwashio vya vipengele vimenakiliwa kwenye ubao wa kunakili + Washa upya programu ili kutumia mpangilio huu + Export na import + Hakuna feature flags zilizopatikana + Vishika-bendera vya vipengele vimehamishwa kwa mafanikio + Imeshindwa ku-export feature flags + Feature flags zimepakia, gusa Hifadhi ili kutumia + Imeshindikana kusoma faili + Utafutaji wa binary + "Inatambua ni bendera ya sifa ipi inasababisha tabia ya programu. + +Nusu ya bendera %s zimezuiliwa, na baada ya kila kuanzisha upya unataja ikiwa tabia hiyo bado ipo. + +Rudia tabia hiyo mara moja kabla ya kuanza, vinginevyo bendera ambazo programu haijaomba bado hazitakuwepo kwenye utafutaji" + Anza + "Hakuna alama za kipengele za kutafuta. + +Ni alama tu ambazo programu imefikia na ambazo kwa sasa hazijalazimishwa kuwa zimewashwa ndizo zinazoweza kutumika katika utafutaji wa binary" + "Hatua %1$s + +Alama %2$s zimebaki, %3$s kati ya hizo zimezuiwa kwa hatua hii. + +Anzisha programu upya, angalia kama tabia hiyo ipo, kisha jibu hapa chini" + Utafutaji wa alama ya kipengele unaendelea + Tabia ipo + Tabia haipo + Ghairi utafutaji wa binary + Utafutaji wa binary umeghairiwa + "Alama ya kipengele %s inasababisha tabia hiyo. + +Sasa imezuiwa" + "Kushindwa kupata bendera ya kipengele. + +Tabia ya programu haionekani kudhibitiwa na bendera ya kipengele" + "Kushindwa kupata bendera moja ya kipengele. + +Tabia hiyo ilitoweka mapema katika utafutaji, hivyo inawezekana bendera zaidi ya moja zinahusika au moja ya majibu yalikuwa makosa" + Fungua mipangilio ya MicroG + MicroG haijasakinishwa. Isakinishe + Hatua inahitajika + "MicroG v%1s yako imepitwa na wakati na inaweza kusababisha matatizo. + +Tafadhali sasisha MicroG hadi toleo jipya v%2s" + Sasisha + Puuza + "MicroG iliyosakinishwa haipendekezwi na Morphe: + +%1s + +Gusa ili kuiondoa, kisha fungua tena programu hii na usakinishe MicroG RE" + Ondoa programu + "MicroG haina ruhusa ya kufanya kazi nyuma ya pazia. + +Fuata mwongozo wa \"Don't kill my app\" kwa simu yako, na utumie maelekezo hayo kwenye usanidi wako wa MicroG. + +Hii inahitajika ili programu ifanye kazi" + Fungua tovuti + "Uboreshaji wa betri wa MicroG lazima uzimwe ili kuzuia matatizo. + +Kuzima uboreshaji wa betri kwa MicroG hakutaathiri matumizi ya betri vibaya. + +Gusa kitufe cha kuendelea na uruhusu mabadiliko ya uboreshaji" + Endelea + Onyesha dialog ya uboreshaji wa betri ya MicroG + "Kuzima hili kutaficha ombi la uboreshaji wa betri la MicroG. + +Ikiwa uboreshaji wa betri hautabadilishwa basi unaweza kupata: +• Programu inaweza kufungwa bila kutarajia +• Arifa zinaweza zisifanye kazi +• Uchezaji wa nyuma unaweza usifanye kazi" + Usimamizi wa viunganishi vya programu + Sanidi viungo vifunguliwe katika programu hii + Hatua ya 1: Zima viunganishi vya programu asili + Fungua mipangilio ya programu asili na uzime viunganishi vyote vya kikoa + Hatua ya 2: Washa viunganishi vya programu iliyorekebishwa + Fungua mipangilio ya programu iliyorekebishwa na uwashe viunganishi vyote vya kikoa + Fungua mipangilio + 🚨 Tahadhari ya Majaribio 🚨 + "Unatumia toleo la majaribio la programu la ⚠️ <b>%1$s</b> +<br/><br/> +🔧 Tarajia tabia isiyo ya kawaida ya programu au hitilafu zisizojulikana tunapoboresha viraka vya toleo hili la programu. +<br/><br/> +Ikiwa unataka uzoefu usio na matatizo zaidi, basi <b>ondoa</b> hii na weka toleo la programu linalopendekezwa la ✅ <b>%2$s</b>" + ⚠️ Nataka ya majaribio + ✅ Nataka thabiti + Kumbukumbu ya hitilafu imehifadhiwa kwenye nyaraka za mtumiaji: %s + Kumbukumbu ya hitilafu imehifadhiwa: %s diff --git a/patches/src/main/resources/addresources/values-sw-rKE/sponsorblock/strings.xml b/patches/src/main/resources/addresources/values-sw-rKE/sponsorblock/strings.xml index 9d2d4ab7d5..2faa0867ab 100644 --- a/patches/src/main/resources/addresources/values-sw-rKE/sponsorblock/strings.xml +++ b/patches/src/main/resources/addresources/values-sw-rKE/sponsorblock/strings.xml @@ -16,7 +16,9 @@ Second \"item\" text" --> + Hutumia pembe za mraba kwa vitufe na vidhibiti vya SponsorBlock + Inapakia… diff --git a/patches/src/main/resources/addresources/values-sw-rKE/youtube/strings.xml b/patches/src/main/resources/addresources/values-sw-rKE/youtube/strings.xml index 70ce03fc67..e1d7586419 100644 --- a/patches/src/main/resources/addresources/values-sw-rKE/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-sw-rKE/youtube/strings.xml @@ -18,213 +18,1639 @@ Second \"item\" text" + Kuhusu Morphe + Matangazo + Feed + Jumla + Kicheza + Shorts + Seekbar + Vidhibiti vya kutelezesha + Mengineyo + Video + Rejesha skrini ya zamani ya mipangilio ya YouTube + Ondoa vizuizi vya uchezaji wa nyuma + Zima uchezaji wa nyuma + Sitanisha kiotomatiki kifaa kinapofungwa + Zima + Sitanisha kila wakati kifaa kinapofungwa + Sitanisha isipokuwa sauti ya wireless ikiwa imeunganishwa + Zima kikomo cha kasi ya kusogeza + Zima sasisho za mpangilio + Huzuia seva isisasishe mpangilio wa programu + "Mpangilio wa programu unarudi katika mpangilio uliotumika wakati wa kusakinisha kwa mara ya kwanza. + +Baadhi ya mipangilio ya upande wa seva inaweza isirudi. + +Mabadiliko ni pamoja na: +• Vipengele katika menyu ya ziada ya kicheza (au mipangilio inayohusiana) vinaweza kisifanye kazi +• Namba zinazojirudia hazina uhuishaji +• Tab ya Maktaba inaonekana +• Sehemu ya muziki ya maelezo ya video inaweza isifanye kazi +• Vitu vya menyu ya ziada ya kicheza haviwezi kufichwa +• Kitufe cha kubadilisha akaunti kinaweza kisitionekane kwenye tab ya Maktaba" + Washa chujio cha menyu ya flyout ya feed + Chujio cha menyu ya flyout ya feed + Ingiza majina ya menyu ya flyout ya kuchuja, yakitenganishwa kwa mistari mipya + Washa chujio cha menyu ya chini ya Maktaba / Wewe + Chujio cha menyu ya chini ya Maktaba / Wewe + Ingiza majina ya menyu ya chini ya Maktaba / Wewe ya kuchuja, yakitenganishwa kwa mistari mipya + Ficha kadi za albamu + Huficha kadi za albamu chini ya maelezo ya msanii + Ficha kadi za msanii + Huficha kadi ya msanii juu ya matokeo ya utafutaji + Ficha vitufe vya chaneli + Huficha vitufe kama vile Jisajili, Angalia Chaneli, Jamii, na YouTube Music kutoka kwenye wasifu wa chaneli katika matokeo ya utafutaji + Ficha machapisho ya jumuiya + Ficha bango fupi + Huficha mabana madogo na nafasi kati ya baadhi ya vipengele + Kadi zinazotanuka chini ya video + Onyesha zote + Ficha kadi ya muhtasari pekee + Ficha zote + Ficha kitufe cha maikrofoni kinachoelea + Huficha kitufe cha maikrofoni kinachoelea katika kona ya chini kulia ya skrini ya mapendekezo ya utafutaji + Ficha kitufe cha \"Pata Premium\" + Ficha rafu ya Historia + Ficha rafu za mlalo + Huficha rafu za mlalo kama vile Habari za hivi punde, Chapisho za jumuiya unazoweza kupenda, Endelea kutazama, Gundua chaneli zaidi, Gundua mada zaidi, Kwa Ajili Yako, Video za hivi punde, Zinazohusika zaidi, Chapisho kutoka kwa Jumuiya ya Muumbaji, Ununuzi, na Tazama tena + Ficha rafu ya picha + Huficha rafu za picha katika matokeo ya utafutaji + Ficha kadi ya \'Alika wengine kutuma ujumbe\' + Huficha kadi ya mwaliko inayoonyeshwa katika sehemu ya Ujumbe ya tabo ya Arifa + Ficha kitufe cha \'Video za hivi punde\' + Huficha bango la bluu la \'Video za hivi punde\' linalotokea juu ya feed ya Nyumbani + Ficha mirutubisho ya moja kwa moja + Huficha video ambazo zinafanya utiririshaji wa moja kwa moja sasa kwenye feed ya nyumbani na matokeo ya utafutaji + Ficha orodha za nyimbo mchanganyiko + Ficha sehemu ya sinema + Ficha kichwa cha menyu ya Arifa + Huficha kichwa cha menyu ya Arifa unapobonyeza kitufe cha kengele + Ficha kitufe cha \"Nijulishe\" + Huficha kitufe cha kikumbusho kwa utiririshaji wa moja kwa moja uliopangwa na uzinduko wa video + Ficha Playables + Huficha kipengele kinachokuruhusu kucheza michezo + Ficha picha ndogo za maneno ya utafutaji + Huficha vionjo vya picha ndogo katika orodha ya utafutaji inayojikamilisha + Ficha kitufe cha \"Onyesha zaidi\" + Huficha kitufe cha panua chini ya video katika matokeo ya utafutaji + Ficha upau wa chaneli zilizosajiliwa + Ficha majina ya chaneli ulizosajili + Huficha majina ya chaneli kwenye upau wa chaneli ulizosajili + Ficha tafiti + Ficha tafiti na kusukumwa kuhusu usahihi wa mapendekezo ya feed + Ficha rafu ya tiketi + + Ficha bango la \'Video zisizopatikana zimefichwa\' + Huficha bango la \'Video zisizopatikana zimefichwa\' katika orodha za video + Ficha muda wa kupakia + "Vikomo: +• Rafu za Shorts, kurasa za chaneli, na matokeo ya utafutaji bado zinaweza kuonyesha muda wa kupakia +• Haifanyi kazi na mpangilio wa magari" + Ficha picha ya awali ya video + Ficha lebo za video + Huficha lebo kama \'Imetafsiriwa kiotomatiki\' / \'Imetafsiriwa\', \'Hupata kamisheni\', \'Yenye msisimko\', \'Watu pia walitazama\', \'bidhaa\' na \'Unaweza pia kupenda\' + Ficha idadi ya utazamaji + "Vikomo: +• Rafu za Shorts, kurasa za chaneli, na matokeo ya utafutaji bado zinaweza kuonyesha idadi ya mitazamo +• Haifanyi kazi na mpangilio wa magari" + Ficha matokeo ya utafutaji wa wavuti + Huficha sehemu ya \'Matokeo kutoka kwenye wavuti\' kutoka kwenye matokeo ya utafutaji + Ficha sehemu ya \'Unaweza kupenda\' + Ficha YouTube Doodles + Huficha michoro ya msimu na ya matukio kwenye nembo ya YouTube + Kuonyesha YouTube Doodles kunaweza kuzuia vichwa vya habari vya kioo visionyeshwe + Ficha upau wa chaneli + Ficha alama ya maji ya chaneli + Ficha Sura na kitufe cha \'Katika video hii\' + Ficha kisanduku cha ufadhili wa umma + Ficha visanduku vya dharura + Ficha paneli za habari + Huficha paneli za muktadha wa habari, kama vile habari za matibabu + Ficha kitufe cha Jiunge + Ficha kitufe cha \'Rudia soga ya moja kwa moja\' + Huficha kitufe cha kurudia soga katika hali ya skrini nzima + Ficha paneli za matibabu + Ficha vidokezo vya ishara za kivinjari + Huficha vidokezo vinavyoonyeshwa wakati wa ishara za kivinjari, kama vile \'Vuta juu ili kutafuta kwa usahihi\', \'Achilia ili ghairi\' na \'2x\' + Ficha snackbar + Ficha miongozo ya wanachama + Ficha kitufe cha \'Onyesha kulingana na video\' + Ficha mwelekeo wa rangi wa kugusa na kushikilia + Huficha mwelekeo wa rangi wa vidhibiti vya kivinjari wakati unatelezesha kidole chako ukishikilia ili kuongeza kasi + Ficha miitikio ya muda + Huficha kipengele kinachoruhusu watumiaji kuitikia kwa emoji katika sehemu ya maoni na soga ya moja kwa moja + Ficha menyu ya \'Msaada & maoni\' + Huficha menyu ya \'Msaada & maoni\' unapotumia kitufe cha nukta tatu, kama vile kwenye matokeo ya utafutaji + Ficha jina la utambulisho + Huficha jina la utambulisho wa chaneli yako kwenye tabo ya \'Wewe\' + Ficha kichwa cha video + Huficha kichwa cha video katika hali ya skrini nzima + Safisha manukuu ya video + Husafisha virai kama \'#\', \'Fundraiser\', \'product(s)\', na \'Shop\' kutoka kwenye maelezo ya video + Ficha kitufe cha \'Uliza Gemini\' + Huficha kitufe cha \'Uliza Gemini\' chini ya paneli ya \'Uliza kuhusu video hii\' + Maelezo ya video + Chagua ni vipengele gani vionekane katika maelezo ya video + Ficha sehemu ya \'Muhtasari wa video uliotengenezwa na AI\' + Ficha sehemu ya Uliza + Ficha sehemu ya Sifa + Huficha sehemu za Maeneo yanayozingatiwa, Michezo, Muziki, na Watu waliotajwa + Ficha sehemu ya viungo vya Channel + Ficha sehemu ya Sura + Ficha sehemu ya Marekebisho + Ficha sehemu ya \'Maendeleo ya kozi\' + Ficha sehemu ya Explore + Huficha sehemu za Explore this course na Explore the podcast + Ficha sehemu ya \'Explore this course\' + Ficha sehemu ya \'Explore the podcast\' + Ficha sehemu ya chaneli zinazofaa + Ficha sehemu ya Viungo vilivyochaguliwa + Ficha sehemu ya Orodha za nyimbo zinazozingatia + Ficha sehemu ya Video zilizochaguliwa + Ficha sehemu ya Hashtag + Ficha sehemu ya \'Hii ilivyotengenezwa\' + Ficha pointi za Hype + Ficha sehemu ya Kadi za taarifa + Ficha sehemu ya \'Dhana Muhimu\' + Ficha sehemu ya Maswali + Ficha sehemu ya \'Tafuta ndani ya video hii\' + Ficha kitufe cha Jisajili + Ficha sehemu ya Nakala + Ficha sehemu ya \'Maelezo ya video\' + Upau wa kuchuja + Chagua mahali pa kuficha upau wa vichujio unaoteleza + Ficha kwenye ukurasa wa chaneli + Ficha kwenye maoni + Ficha kwenye mifumo ya kutoa maudhui + Ficha kwenye video zinazohusiana + Ficha kwenye matokeo ya utafutaji + Ficha kwenye historia ya utazamaji + Ukurasa wa chaneli + Chagua vipengele vinavyoonekana kwenye ukurasa wa chaneli + Washa chujio cha tab ya chaneli + Chujio cha tab ya chaneli + Ingiza majina ya tabo za chaneli za kuchuja, yakitenganishwa kwa mistari mipya + Ficha kitufe cha Jamii + Ficha kitufe cha Jiunge + Ficha muonekano wa viungo + Huficha viungo vya wavuti vinavyoweza kubonyeza kwenye kichwa cha chaneli + Ficha chip ya \"Wanachama pekee\" + Huficha chip ya \'Wanachama pekee\' kwenye baa ya kichujio cha chaneli + Ficha rafu ya wanachama + Huficha orodha ya wanachama wa chaneli + Ficha rafu ya machapisho + Ficha kitufe cha Duka + Ficha kitufe cha Jisajili + Menyu ya utafutaji + Chagua vipengele gani vionekane kwenye menyu ya utafutaji + Ficha menyu ya Kozi + Ficha kitufe cha Explore + Ficha menyu ya \'Fashion & Beauty\' + Ficha menyu ya Gaming + Ficha menyu ya Hype + Ficha menyu ya Kujifunza + Ficha menyu ya Live + Ficha menyu ya Memberships + Ficha menyu ya \'Filamu & TV\' + Ficha menyu ya Muziki + Ficha menyu ya News + Ficha menyu ya Playables + Ficha menyu ya Podcasts + Ficha menyu ya Shopping + Ficha menyu ya Sports + Ficha menyu ya YouTube Premium + Ficha menyu ya YouTube Studio + Ficha menyu ya YouTube Music + Ficha menyu ya YouTube Kids + Ficha menyu ya YouTube Create + Ficha menyu ya \'Jinsi YouTube inavyofanya kazi\' + Ficha footer za kisheria na faragha + Maoni + Chagua vipengele vinavyoonekana katika sehemu ya maoni + Washa kichujio cha mzunguko wa maoni + "• Hakikisha kuwa umeingiza jina sahihi la kipengele cha carousel (mfano, Uliza, Hype video hii, Sikiliza kwenye YouTube Music) +• Kuficha vipengele vya carousel kunaweza kuwa na madhara yasiyokusudiwa +• Ikiwa vipengele vyote vya carousel vimefichwa, sehemu nzima ya maoni pia itafichwa" + Kichujio cha mzunguko wa maoni + Ingiza majina ya carousel ya maoni ya kuchuja, yakitenganishwa kwa mistari mipya + Ficha muhtasari wa AI wa soga + Ficha miongozo ya chaneli + Ficha jumbe za muktadha + Ficha kichwa cha \"Maoni ya wanachama\" + Ficha sehemu ya maoni + Ficha sehemu ya maoni katika feed ya Nyumbani + Ficha Mwongozo wa Jamii + Ficha kitufe cha \"Tengeneza Short\" + Ficha kitufe cha Kutopenda + Ficha kitufe cha Emoji + Ficha chaguo za upau wa kichujio + Huficha chaguo zote za upau wa kichujio isipokuwa \'Top\' na \'Newest\' + Ficha uhuishaji wa zawadi na kadi + Ficha kitufe cha Zawadi + Ficha kitufe cha Maelezo + Ficha upau wa wafadhili wa soga ya moja kwa moja + Ficha kitufe cha kukuskanya hadi chini cha soga ya moja kwa moja + Ficha vidokezo vya soga ya moja kwa moja + Huficha madirisha madogo ya vidokezo yanayoonekana karibu na upau wa kuingiza soga, kama vile ilani ya soga kwa wanajisajili pekee + Ficha kitufe cha Menyu + Huficha kitufe cha menyu cha nukta 3 cha kila maoni + Ficha maoni ya awali + Huficha muonekano wa maoni chini ya video kabla ya kufungua sehemu kamili ya maoni + Maoni yamefichwa na Morphe… + Washa kitufe cha maoni ya k minimalistic + Huficha kadi zinazoteleza na kichaguzi chake cha ukurasa, kinachoonekana kwenye kitufe cha maoni + Ficha kitufe cha Asante + Ficha kitufe cha Muda + Ficha kitufe cha \'Mashabiki wakuu\' + Ficha kitufe cha Tafsiri + Ficha ujumbe wa \'Hakuna maoni bado\' + Safisha viungo vya utafutaji vilivyotiwa rangi + Husafisha viungo vya utafutaji vilivyotiwa rangi, na kuacha maandishi ya kawaida ya asili pekee + Chujio cha neno muhimu + Chagua maneno au virai maalum ili kuficha video kwenye feed na utafutaji + Ficha maoni kwa kutumia maneno muhimu + Ficha video za Nyumbani kwa maneno muhimu + Ficha matokeo ya utafutaji kwa maneno muhimu + Ficha video za Usajili kwa maneno muhimu + Maneno muhimu ya kuficha + "Ingiza maneno muhimu na virai vya kuficha, vitenganishwe kwa mistari mipya + +Maneno muhimu yanaweza kuwa maandishi yoyote yanayoonekana katika vichwa vya video au majina ya chaneli + +Maneno yenye herufi kubwa katikati lazima yalingane sawasawa (mfano, iPhone, TikTok, LeBlanc)" + "Nyumbani / Usajili / Tafuta / Maoni yanachujwa ili kuficha maudhui yanayofanana na virai vya maneno muhimu + +Vikwazo: +• Shorts haziwezi kufichwa kwa jina la chaneli +• Baadhi ya vipengele vya UI vinaweza kutofichwa +• Kutafuta neno muhimu kunaweza kutoa matokeo sifuri" + Linganisha maneno kamili + Kuzunguka neno kuu/kirai kwa alama za nukuu mbili kutazuia ulinganifu wa sehemu ya vichwa vya video na majina ya chaneli<br><br>Kwa mfano,<br><b>\"ai\"</b> itaficha video: <b>How does AI work?</b><br>lakini haitaficha: <b>What does fair use mean?</b> + Huwezi kutumia neno kuu: %s + Ongeza alama za nukuu ili kutumia neno kuu: %s + Neno kuu lina matamko yanayopingana: %s + Neno kuu ni fupi mno na linahitaji alama za nukuu: %s + Neno muhimu litaficha video zote: %s + "• Nyumbani: %s +• Usajili: %s +• Tafuta: %s +• Maoni: %s +• Jumla: %s" + Takwimu + Video zilizofichwa, saa 24 zilizopita + Video zilizofichwa, muda wote + Rudisha kigawanyo cha saa 24 zilizopita? + Rudisha kigawanyo cha muda wote? + Chujio cha chaneli za AI + Ficha video kutoka kwa chaneli zilizowekwa alama kama maudhui yaliyozalishwa na AI na orodha ya jamii ya AiSList + Orodha ya kuzuia: Uhakika wa juu + Orodha ya onyo: Uhakika wa wastani + Ficha video kwenye Nyumbani + Ficha video kwenye utafutaji + "• Nyumbani: %s +• Utafutaji: %s +• Jumla: %s" + Chaneli za orodha ya kuzuia + Chaneli za orodha ya onyo + Washa kichujio ili kupakia chaneli + Inapakia chaneli… + "Nyumbani na matokeo ya utafutaji yanachujwa ili kuficha video kutoka kwa chaneli zilizowekwa alama na AiSList + +Vikomo: +• Shorts za AI zilizochujwa zinaweza kuonekana kama nafasi tupu +• Baadhi ya video za AI zinaweza kuendelea kuonekana" + "AiSList ni orodha ya chanzo wazi ya chaneli za YouTube zinazochapa maudhui yanayozalishwa na AI, inayosimamiwa na watu wanaojitolea. Chaneli huripotiwa na watumiaji na kukaguliwa kabla ya kuongezwa + +Orodha hii hupakuliwa kutoka GitHub na video huchujwa kwenye kifaa chako" + Ripoti channel za AI + Ongeza kipengele cha ripoti kwenye menyu ya video + Ongeza kipengele kwenye menyu ya video kinachoripoti chaneli ya video kwa AiSList + Jina la mtumaji + Jina linaloonekana kando ya ripoti zako kwenye ubao wa viongozi wa umma wa AiSList. Kuripoti hakupatikani wakati hapa pakiwa wazi + Ripoti chaneli ya AI + "Tuma %s kwa AiSList, ukitumia video hii kama ushahidi? + +Ripoti tu chaneli ambazo: +• Hutumia sauti, picha, miswada au muziki wa AI kama maudhui yao kuu +• Huzalisha maudhui kwa wingi kwa zana za AI na kazi ndogo ya binadamu + +Usiripoti chaneli zinazozungumzia AI pekee, au zinazotumia AI kwa sehemu fulani za video zao. + +Ripoti ni za hadhara na hupitiwa kabla ya chaneli kuongezwa kwenye orodha. Kuondoa inawezekana tu kupitia ombi kwenye GitHub" + Chaneli hii tayari ipo kwenye orodha ya kuzuia ya AiSList + Chaneli hii tayari ipo kwenye orodha ya onyo ya AiSList + Ripoti + Weka jina la mtumaji kwenye mipangilio ya chujio cha chaneli ya AI ili kuripoti chaneli + Inatafuta chaneli… + Hakuwezi kupata chaneli ya video hii + Chaneli imeripotiwa na inasubiri mapitio + AiSList haikuweza kuhifadhi ripoti, jaribu tena baadaye + Ripoti nyingi sana zimetumwa, jaribu tena baadaye + AiSList imekataa ripoti + Ripoti imeshindwa, angalia muunganisho wako + Orodha nyeupe ya matangazo ya chaneli + Video kutoka kwa chaneli zilizoruhusiwa hucheza na matangazo, ili watengenezaji wake walipwe. Matangazo yanabaki yamezuiwa kwenye chaneli nyingine zote + Ongeza chaneli kwenye orodha ya kuruhusu matangazo + Ondoa chaneli kwenye orodha ya kuruhusu matangazo + Ficha rafu ya duka la mbunifu + Ficha bango la duka la skrini ya mwisho + Ficha matangazo ya jumla + Ficha mabango ya bidhaa + Ficha lebo za matangazo ya kulipwa + Ficha matangazo ya popup ya kicheza + Ficha kadi za udhamini binafsi + Ficha viungo vya ununuzi + Ficha matangazo ya video + Ficha matangazo ya YouTube Premium + Inapakia… + Chaneli ya sasa: + Ongeza kwenye orodha ya kuruhusu + Ondoa + Chaneli zilizoruhusiwa + Hakuna chaneli zilizoruhusiwa + Chaneli imeongezwa kwenye orodha ya kuruhusu + Chaneli imeondolewa kwenye orodha ya kuruhusu + Tafuta ndani ya chaneli + Huongeza kitufe cha pili cha utafutaji kwenye kurasa za chaneli ili kutafuta chaneli hiyo pekee + Tafuta chaneli + Matokeo ya utafutaji wa chaneli + Hakuna kilichopatikana kwenye chaneli hii + Hakuwezekana kutafuta kwenye chaneli hii + Video + Machapisho + Hakuna machapisho katika chaneli hii yanayolingana + Uhusiano + Zilizotazamwa zaidi + Zilizotazamwa kidogo zaidi + Mpya zaidi + Za zamani zaidi + Fupi zaidi + Ndefu zaidi + Kiungo kimenakiliwa kwenye ubao wa kunakili + Kiungo chenye alama ya muda kimenakiliwa kwenye ubao wa kunakili + Onyesha kitufe cha Nakili kiungo cha video + Gusa ili kunakili kiungo cha video, gusa na ushikilie ili kunakili chenye alama ya muda + Nakili alama ya muda kwa chaguo-msingi + Hubadilisha vitendo vya kugusa na kugusa-na-kushikilia vya kitufe cha \'Nakili kiungo cha video\' + Nakili kichwa cha video + Gusa na ushikilie kichwa cha video ili ukinakili + Kichwa cha video kimenakiliwa kwenye ubao cha kunakili + Nakili maoni + Gusa na ushikilie maoni ili uyanakili + Maoni yamenakiliwa kwenye ubao wa kunakili + Gusa na ushikilie ili kufungua menyu ya video + Kugusa na kushikilia popote kwenye feed ya video hufungua menyu yake + Onyesha kitufe cha Tazama baadaye kwenye picha ndogo + Kitufe cha Tazama baadaye huonyeshwa kwenye picha ndogo za video za feed. Kugusa na kukishikilia hufungua Hifadhi kwenye orodha ya uchaguzi + Pakua picha za chapisho + Kitufe cha kupakua kinaonyeshwa kando ya kitufe cha Shiriki cha machapisho yenye picha au GIF, na kipengele cha kupakua kimeongezwa kwenye menyu yao + Pakua picha + Pakua picha + Inapakua picha %s… + Inapakua picha… + Picha imehifadhiwa kwenye %s + Hakuwezekana kupakua picha + Hakuna picha iliyopatikana katika chapisho hili + Ongeza kipengele cha Uliza kwenye menyu ya flyout + Huongeza kipengele cha muhtasari wa video kwenye menyu ya flyout ya feed + "Ask inaanza kwa kuhitaji akaunti iliyoingia katika nchi ambayo Ask inapatikana. + +Baada ya video yenye kitufe cha Ask kufunguliwa mara moja, kipengele hiki hufanya kazi na video zote hata kama nchi ya sasa haina kipengele cha Ask." + Fupisha video + Hakuwezekana kufungua Ask kwa video hii + Ask haipatikani kwa akaunti yako au nchi ya sasa + Onyesha idadi ya utazamaji kwenye picha ndogo + Idadi ya utazamaji ya video za feed inaonyeshwa chini kushoto ya picha ndogo badala ya chini ya kichwa cha habari. Baadhi ya video, kama vile katika rafu, zinaweza zisioonyesha idadi ya utazamaji + Onyesha idadi ya like za thumbnail ya feed + Onyesha asilimia ya like za thumbnail ya feed + Tafuta maoni + Gusa kitufe cha utafutaji kwenye maoni ili kuonyesha tu maoni yaliyopakia ambayo yana maneno yote yaliyotafutwa. Weka maneno kwenye alama za nukuu ili kutafuta fungu la maneno + Tafuta maoni + Maneno ya kutafuta + Tafuta + Futa + Badilisha utaratibu wa kupanga au fungua maoni tena ili kuyatafuta + Tafutio limefutwa. Badilisha utaratibu wa kupanga au fungua maoni tena ili kuonyesha maoni yote + Zima popup ya \'Ingia kwenye TV\' + Zima popup ya \'Unganisha vifaa vyako\' + Zima urukaji wa sura kwa kugusa mara mbili + Huondoa ishara ya kugusa mara mbili kwa vidole viwili inayuruka sura, ambayo inaweza kuchochewa kwa bahati mbaya kwa kidole kimoja + Pakua video, podikasti, au nyimbo kwa kutumia programu za kupakulia za upande wa tatu + Onyesha kitufe cha kupakua cha tabaka la kivinjari + Huonyesha kitufe kwenye kivinjari cha video ambacho hufungua kipakuzi chako cha nje + Pakua haraka kwa kugusa + Huchochea kupakua kwa nyuma. Gusa na ushikilie ili kufungua kipakua. Inafanya kazi na YTDLnis na YTindir + Weka pakua kama zilizotazamwa + Video zilizopakuliwa kwa kipakua cha nje huongezwa kwenye historia yako ya kutazama ya YouTube + HD Kamili (1080p) + Kati (192 kbps) + Pakua haraka + Video + Sauti + Chaguo zote + Onyesha pakua kwenye menyu ya Shorts + Huongeza kipengele cha menyu ya pakua ya nje kwenye kidchezaji cha Shorts + Pakua + Tafsiri ya sauti + Soma maelezo ya video kwa sauti kwa kutumia ubadilishaji wa maandishi kuwa sauti kwenye kifaa, ikiwa imesawazishwa na uchezaji + Inaonyesha kitufe cha jalada kwenye kichezaji cha video. Gusa ili kubadilisha tafsiri, gusa na ushikilie kwa mipangilio ya haraka + Lugha + Mtoa huduma wa tafsiri + Sauti ya sauti asilia + Sauti ya sauti asilia ya YouTube wakati tafsiri inasema + Sauti ya tafsiri + Sauti ya sauti ya tafsiri iliyodubwa + Kasi ya juu ya kuzungumza + Kizidishi cha kasi ya juu wakati wa kuingiza mazungumzo kwenye nafasi ya kimaandishi + "API ya tafsiri ya bure, hakuna ufunguo wa API unaohitajika. Hadi wahusika 5,000 kwa siku kwenye ngazi ya bure, au hadi wahusika 50,000 kwa siku ukiwa na anwani ya barua pepe. + +Gusa hapa ili kufungua MyMemory.translated.net" + Barua pepe ya MyMemory + Hiari. Ingiza anwani yako ya barua pepe ya MyMemory ili kuongeza kikomo cha kila siku kutoka herufi 5,000 hadi 50,000. Acha wazi ili kutumia kikomo cha bila jina + "Tafsiri ya AI ya kulipia kupitia OpenRouter. Ongeza salio mara moja na utafsiri maelfu ya video. Mistral Nemo ni chaguo rahisi zaidi, Gemma 4 inatoa ubora wa juu zaidi. + +Gusa hapa kufungua OpenRouter.ai" + Siri ya API ya OpenRouter + Siri yako ya API ya OpenRouter. Unda siri kwenye OpenRouter.ai > Workspace > API Keys + Muundo wa OpenRouter + Kibinafsi + OpenRouter haijasanidiwa + Ingiza ufunguo wako wa OpenRouter API kwenye mipangilio kabla ya kuchagua mtoa huduma huyu + Bure + %s kwa kila saa 100 za video + Sauti + Mfumo + Mwanamume anayezungumza lugha nyingi + Mwanamke anayezungumza lugha nyingi + Mwanamume + Mwanamke + Ni wakati wa kubadilika! + Tafsiri haipatikani + "Huduma ya tafsiri imerudisha hitilafu ya HTTP %s. Inaonekana anwani yako ya IP imezuiwa kwa muda wa hadi saa 24. + +Ili kuendelea kutumia tafsiri, badilisha kwenda kwa mtoa huduma mwingine wa tafsiri katika mipangilio au tumia VPN" + Usionyeshe tena + "Mipangilio yako ina ufunguo wa API wa OpenRouter. + +Ufunguo wako wa API usishirikishwe kamwe. Mtu yeyote mwenye ufikiaji nao anaweza kutumia salio lako la OpenRouter" + Kikomo cha salio la OpenRouter kimefikiwa + Kikomo chako cha mikopo ya OpenRouter kimefikishwa. Ongeza salio lako au ongeza kikomo cha matumizi ya ufunguo wako kwenye OpenRouter.ai + Fungua OpenRouter.ai + Tafsiri ya OpenRouter imeshindwa (HTTP %s) + Kikomo cha kila siku cha MyMemory kimefikishwa + Inayofuata itapatikana mnamo %s + Umetumia quota ya tafsiri ya bure ya MyMemory ya leo (herufi 5,000). Ongeza barua pepe ya MyMemory kwenye mipangilio ili kuongeza kikomo cha kila siku hadi herufi 50,000, au badilisha kwenda huduma nyingine ya tafsiri + Umetumia quota ya tafsiri ya MyMemory ya leo (herufi 50,000). Badilisha kwenda huduma nyingine ya tafsiri au jaribu tena kesho + Tafsiri ya MyMemory imeshindwa (hali %s) + Washa mzunguko wa video + Usikumbuke video inayojirudia + Zuia hali ya sasa ya video inayojirudia isihifadhiwe kwa video zinazofuata + Onyesha kitufe cha kurudia video + Mzunguko wa video umewashwa + Kujirudia kwa video kuzimwa + Kipindi cha mzunguko + Anza + Mwisho + [hh:]mm:ss + [hh:]mm:ss (tupu = mwisho wa video) + Weka + Futa + Inarudia %1$s - %2$s + Kipindi cha muda kisicho sahihi + Onyesha kitufe cha Picha-ndani-ya-Picha + Onyesha menyu ya Picha-ndani-ya-Picha + Badilisha kwenda Picha-ndani-ya-Picha + Onyesha kitufe cha Cheza zote + "Gusa ili kutengeneza orodha ya nyimbo ya video zote kutoka kwenye chaneli, gusa na ushikilie ili kubatilisha + +Info: Inaweza isifanye kazi kwenye mirutubisho ya moja kwa moja" + Hali ya kuzalisha orodha ya nyimbo + Maudhui yote (Panga kwa muda, Kupanda) + Maudhui yote (Panga kwa muda) + Maudhui yote (Panga kwa maarufu) + Video pekee (Panga kwa muda) + Video pekee (Panga kwa maarufu) + Shorts pekee (Panga kwa muda) + Shorts pekee (Panga kwa maarufu) + Video za kurushwa mubashara pekee (Panga kwa muda) + Video za kurushwa mubashara pekee (Panga kwa maarufu) + Maudhui ya Wanachama pekee yote + Video za Wanachama pekee + Shorts za wanachama pekee + Kurusha moja kwa moja kwa wanachama pekee + Hatuwezi kutengeneza orodha ya kucheza kwa sababu ya kutofautiana kwa ID ya chaneli + Onyesha kitufe cha kupakia upya video + ID ya video haipatikani. Tafadhali pakia upya video kwa mkono + Onyesha kitufe cha Hifadhi ili utazame baadaye + Gusa ili uhifadhi kwenye Tazama baadaye, gusa na ushikilie kwa chaguzi za foleni + Video imehifadhiwa kikamilifu katika orodha ya kuangalia baadaye + Video hii tayari imehifadhiwa kwenye orodha ya kuangalia baadaye + Huwezi kuhifadhi video hii kwenye orodha ya kuangalia baadaye + Hifadhi kwenye Tazama baadaye + Onyesha kitufe cha Hifadhi ili utazame baadaye kwenye menyu ya mipangilio ya kidchezaji + Inaruhusu kuhifadhi video inayotazamwa sasa katika \'Tazama baadaye\' kutoka kwenye menyu ya mipangilio ya video + Onyesha kitufe cha Hifadhi ili utazame baadaye kwenye video za watoto + Inaruhusu kuhifadhi video zinazohusiana na watoto katika \'Tazama baadaye\' kutoka kwenye menyu ya flyout + Onyesha kitufe cha Hifadhi katika Tazama baadaye kwenye flyout ya shorts + Inaruhusu kuhifadhi shorts katika \'Tazama baadaye\' kutoka kwenye menyu ya flyout + Elekeza \"Tazama baadaye\" kwenye orodha ya kucheza + Tazama baadaye + Ongeza kwenye foleni ya Morphe + Hifadhi foleni kati ya vipindi + Foleni huhifadhiwa baada ya kuanzisha upya programu + Badilisha nafasi ya vitendo vya Hifadhi na Foleni + Gusa kwa chaguo za foleni, gusa na ushikilie ili hifadhi katika Tazama baadaye + Cheza inayofuata kwenye foleni + Chaguo-msingi + Foleni asilia + Foleni ya Morphe + Foleni ya Morphe upande wa mteja + Ongeza kipengele cha menyu ya foleni ya flyout ya feed + Huongeza kipengele cha ziada cha flyout. Wezesha hii ikiwa \"Ongeza kwenye foleni\" haionekani + Foleni + Foleni (%s) + Futa zote + Hakuna kilichowekwa kwenye foleni + Tumia Cheza inayofuata kwenye foleni kwenye video yoyote + Inacheza sasa + Cheza inayofuata kwenye foleni + Cheza ya mwisho kwenye foleni + Panga upya + Ondoa + Imeongezwa kwenye foleni + Hakuwezekana kuongeza video hii kwenye foleni + Orodha za nyimbo haziwezi kuongezwa kwenye foleni + Foleni imejaa + Hakuweza kuanza video + Orodha nyeupe ya matangazo kwenye menyu ya flyout + Huongeza kipengele cha flyout kinachoruhusu matangazo kuchezwa kwenye chaneli ya video hiyo + Kipengele hiki kinaweza kuhitaji kutumia PotHelper au kubadilisha \"Spoof video streams\" kuwa TV au kuingia kwa Android VR + Orodha nyeupe ya kasi kwenye menyu ya flyout + Huongeza kipengele cha flyout kinachocheza chaneli ya video hiyo kila wakati kwa kasi ya 1.0x + Hujaingia + Hakuna ID ya orodha ya nyimbo + Folio iko tupu + Hakuna ID ya video + Imeshindwa: %s + Imeshindwa kuongeza kwenye foleni + Imeshindwa kuunda foleni + Imeshindwa kuondoa kwenye foleni + Imeshindikana kuhifadhi foleni + Imeongezwa kwenye foleni + Foleni imeundwa + Imeondolewa kwenye foleni + Foleni imehifadhiwa kwenye %s + Morphe Queue + Ongeza kwenye foleni + Ongeza kwenye foleni na ufungue + Ongeza kwenye foleni na ucheze + Ongeza na upakie tena video + Ondoa kwenye foleni + Ondoa na ufungue foleni + Ondoa na upakie tena video + Fungua foleni + Hifadhi foleni kwenye orodha ya nyimbo + Orodha ya nyimbo imehifadhiwa + Imehifadhiwa kwenye %s + Safisha foleni + Foleni imefutwa + Foleni + Cheza ya mwisho kwenye foleni + Cheza inayofuata kwenye foleni + YouTube imeshindwa kuonyesha foleni. + Foleni haiko tayari kwa video hii. Fungua kiduaji video na ujaribu tena. + Ongeza Mix kwenye foleni au utazame Mix? + Tazama + Ongeza kwenye foleni + Inapakia Mix… + Fungua chaneli kutoka kwenye avatar ya moja kwa moja + Huzuia utiririko wa moja kwa moja usifunguke unapogusa avatar ya chaneli + Zima ishara ya utafutaji sahihi + Huondoa hali ya utafutaji sahihi inayowashwa unapovuta juu ya bar ya utafutaji + Washa DVR ya utiririshaji wa moja kwa moja + Inaruhusu kutafuta kwenye utiririshaji wote wa moja kwa moja, hata wale ambao kwa kawaida huzuia jambo hilo + Washa picha ndogo za seekbar + Huonyesha picha ndogo ya video unapovuta seekbar + Imerejewa zaidi + "'Washa DVR ya utiririshaji wa moja kwa moja' inaweza isipatikane + +Ili kurekebisha hili, washa 'Ghushi utiririshaji wa video' na ubadilishe 'Kiteka default' kuwa 'TV Simply' au 'visionOS'" + Panua muda wa DVR ya utiririshaji wa moja kwa moja + Inakuruhusu kurudisha nyuma matangazo ya moja kwa moja hadi siku 7 zilizopita + "'Panua muda wa DVR wa utiririko wa moja kwa moja' huenda isipatikane + +Ili kurekebisha hili, washa 'Ghadhabu ya utiririko wa video' na ubadilishe 'Klijenti ya chaguo-msingi' kuwa 'TV Simply' au 'visionOS'" + Washa utelezaji ili kutafuta + Huwezesha kutelezesha ili kutafuta badala ya kucheza kwa kasi ya 2x unapobonyeza na kushikilia kicheza video + Washa kugusa ili kutafuta + Gusa tu seekbar ili uruke hadi nafasi hiyo bila kuivuta + Ishara ya kutelezesha kushoto + Ishara ya kutelezesha kulia + Ishara ya kutelezesha juu + Upana wa kifuniko cha kushoto/kulia + Asilimia ya kila ukingo wa skrini iliyotengwa kwa ishara za kupapasa kushoto na kulia (5-50) + Mwangaza + Sauti + Asilia + Zima + Puuza papaso wakati ikiwa imefungwa + Ishara za kupapasa hupuzwa wakati kidchezaji kikiwa kimefungwa kwa \'Funga skrini\' + Wezesha ishara ya kubonyeza ili kutelezesha + Inahitaji kubonyeza na kushikilia kabla ya ishara ya kutelezesha kutambuliwa + Wezesha mrejesho wa haptiki + Hifadhi na urejeshe mng\'ao + Hukumbuka kiwango cha mwanga unapoingia / unatoka katika hali ya skrini nzima + Washa ishara ya mng\'ao wa kiotomatiki + Kutelezesha mwanga hadi thamani yake ya chini kabisa huwezesha mwanga wa mfumo wa kiotomatiki + Kiotomatiki + Muda wa mwisho wa tabaka la kutelezesha + Kiasi cha milisekunde ambapo tabaka linaonekana + Uwazi wa rangi ya nyuma ya tabaka la kutelezesha + Rekebisha kiwango cha uwazi (0 ni wazi, 100 ni isiyopenyeza) + Rangi ya mng\'ao wa tabaka la kutelezesha + Weka rangi ya baa ya maendeleo ya vidhibiti vya mwanga + Rangi ya sauti ya tabaka la kutelezesha + Weka rangi ya baa ya maendeleo ya vidhibiti vya sauti + Rangi ya kasi ya overlay ya swipe + Weka rangi ya baa ya maendeleo ya vidhibiti vya kasi ya uchezaji + Ukubwa wa maandishi ya tabaka la kutelezesha + Rekebisha ukubwa wa maandishi kwenye skrini (1-30) + Kiwango cha ukubwa wa kutelezesha + Weka umbali wa kutelezesha unaohitajika kabla ya marekebisho kuanza + Usikivu wa kutelezesha sauti + Thamani za juu zinahitaji kutelezesha zaidi kwa kila hatua ya sauti + Hatua za sauti + Kuongeza sauti + Kupapasa juu kupita sauti ya juu zaidi huiongeza, jambo ambalo linaweza kuharibu sauti kubwa + Chaguo-msingi la kifaa + Usikivu wa swipe ya mwangaza + Thamani za juu zinahitaji swipe nyingi zaidi kwa kila hatua ya mwangaza + Usikivu wa swipe ya kasi + Thamani za juu zinahitaji swipe nyingi zaidi kwa kila hatua ya kasi + Hatua ya kasi + Urefu wa eneo la kasi + Urefu wa eneo la ishara ya kasi kama asilimia ya urefu wa kichezaji (5-75) + Kasi + Mtindo wa tabaka la kutelezesha + Tabaka la mlalo + Tabaka la mlalo (minimal - juu) + Tabaka la mlalo (minimal - katikati) + Tabaka la duara + Tabaka ya duara (minimalistiki) + Tabaka la wima + Tabaka la wima (minimal) + Washa kutelezesha ili kubadilisha video + Telezesha katika hali ya skrini nzima ili kuruka kwenda video inayofuata au ya awali + Vitufe vya utendaji + Chagua vitufe gani vya utendaji vionekane katika hali ya wima na skrini nzima + Vitufe vya mkao wa wima + Zima mng\'ao wa Kupenda na Kujiunga + Huzuia vitufe vya Like na Subscribe kung\'aa wakati muundaji anapovitaja + Ficha baa ya utendaji + Ficha kitufe cha Like na Dislike + Ficha kitufe cha Shiriki + Ficha kitufe cha \'Simamisha matangazo\' + Ficha kitufe cha Maoni + Ficha kitufe cha Unganisha + Ficha kitufe cha \'Ripoti\' + Ficha kitufe cha \'Remix\' + Ficha kitufe cha \'Pakua\' + Ficha kitufe cha \'Hype\' + Ficha kitufe cha \'Tangaza\' + Ficha kitufe cha Asante + Ficha kitufe cha Uliza + Ficha chaneli na kitufe cha Jisajili + Huficha avatar ya chaneli na kitufe cha Jisajili mwanzoni mwa baa ya utendaji + Ficha kitufe cha Jisajili + Huficha vitufe vya Jisajili na Jiunge kando ya avatar ya chaneli mwanzoni mwa upau wa utendaji + Ficha kitufe cha Kipande + Hii inaweza isifanye kazi kwa baadhi ya akaunti za watumiaji + Ficha kitufe cha Duka + Ficha kitufe cha Hifadhi + Ficha kitufe cha Zaidi + Vitufe vya skrini nzima + Ficha kitufe cha Hapendi + Ficha kitufe cha Like + Ficha kitufe cha \"Chati ya moja kwa moja\" + Ficha kitufe cha Mix + Ficha kitufe cha \'Video zaidi\' + Ficha kitufe cha Orodha ya kucheza + Margin ya juu ya vitendo vya haraka + Weka margin ya juu ya baa ya vitendo vya haraka (0-32 dp) + Maelezo + Simamia maelezo ya maandishi ya kiotomatiki na kuki za maelezo + Uonekano wa maelezo ya kiotomatiki + Onyesha kila wakati + Usionyeshe kamwe + Onyesha wakati sauti imewashwa + Onyesha wakati imezimwa sauti + Lugha inayopendelewa ya maelezo + Huchagua subtitles za mtoa huduma kwanza, kisha hutafsiri kiotomatiki kwenda kwenye lugha inayopendelewa + Zima + Ongeza lugha za ziada za kutafsiri kiotoma + Huongeza lugha zaidi kwenye menyu ya kutafsiri kiotoma + Weka kuki za maelezo ya chini + "Hurekebisha hitilafu ya stock ya YouTube ambapo manukuu ya kutafsiri kiotomatiki yanaweza yasipatikane kwa sababu ya kukosekana kwa kuki + +'Kuki za manukuu' lazima zitolewe" + Kuki za maelezo + Kuki za kuwekwa katika maombi ya Timed Text API (Caption Data API) + Pata kuki za maelezo + Bonyeza hapa ili kupata kuki + Imeshindikana kupata kuki + Kuki tayari zipo + Kuki zimesasishwa kikamilifu + Rekebisha nakala ya maandishi + Hutatua hitilafu ya ukaguzi wa sharti ambalo linaweza kuzuia nakala ya maandishi isipakie + Onyesha maelezo ya maandishi katika picha-ndani-ya-picha + Vitufe vya urambazaji + Chagua ni vitufe gani vionekane kwenye chapa ya urambazaji ya chini + Ficha kitufe cha Nyumbani + Ficha kitufe cha Shorts + Ficha kitufe cha Unda + Ficha kitufe cha Usajili + Ficha kitufe cha Arifa + Onyesha kitufe cha Tafuta + Nafasi ya kitufe cha Tafuta + Onyesha kitufe cha Mipangilio + Nafasi ya kitufe cha Mipangilio + Utendaji wa kitufe cha mipangilio + Inafungua mipangilio ya Morphe inapowezeshwa, au mipangilio ya YouTube inapozimwa + Badilisha kitufe cha Create na kitufe cha Arifa + Ikiwa kubadilisha mpangilio huu hakuleti matokeo, jaribu kubadilisha kwenda kwenye hali ya Incognito + Ficha lebo za vitufe vya urambazaji + Ficha nukta ya maudhui mapya + Ficha baa ya urambazaji + "Hii inaficha kabisa baa ya urambazaji, ikiwa ni pamoja na tab ya 'Wewe'. + +Ili kufikia Mipangilio, wezesha kitufe cha Mipangilio kupitia General → Toolbar → Show Settings" + Washa vitufe vya urambazaji vyembamba + Inapunguza nafasi ya mlalo kati ya vitufe + Zima vitufe vya urambazaji visivyo na lebo + Inaonyesha vitufe vya urambazaji vyenye lebo, ikiwa YouTube vitatuma bila lebo + Zima vichupo vya urambazaji vya chombo cha zana + Inaonyesha Usajili kwenye upau wa urambazaji, ikiwa YouTube itaihamishia kwenye kichupo cha juu ya Nyumbani + Washa vichocheo vya baa ya urambazaji + Huonyesha uhamisho wa michoro unapo badilisha kati ya tabo + Zima kuficha kiotomatiki upau wa urambazaji + Huweka baa ya urambazaji ionekane wakati wa kusogeza + Mtindo wa baa ya urambazaji + Chaguo-msingi + Nusu-uwazi + Uwazi + Isiyopitisha mwanga + Mtindo huu huenda usiungwe mkono na kifaa chako au toleo la YouTube, na unaweza kusababisha matatizo ya mpangilio au kufunga kwa programu + Upau wa zana + Chagua ni vipengele gani vionekane kwenye baa ya zana + Ficha kitufe cha Cast + Ficha kitufe cha Chat + Ficha kitufe cha Maikrofoni + Ficha kitufe cha Tafuta + Inapowezeshwa, kugusa hufungua mipangilio ya Morphe na kubonyeza kwa muda mrefu hufungua mipangilio ya YouTube, zima ili kubadilisha + Menyu ya flyout + Chagua ni vipengele gani vionekane kwenye menyu ya flyout + Ficha \'Mipangilio ya ziada\' / Menyu ya Zaidi + Ficha menyu ya \'Hali ya mazingira\' + "Menyu ya 'Audio Track' imefichwa + +Ili kuonyesha menyu ya 'Audio Track', badilisha 'Spoof video streams' kuwa mteja yeyote isipokuwa 'Android Studio' na 'Android VR Downgraded'" + Ficha menyu ya \'Nyjia ya sauti\' + Ficha sehemu ya chini ya menyu ya \'Nyjia ya sauti\' + Ficha menyu ya Maelezo + Ficha sehemu ya chini ya menyu ya Maelezo + Ficha sehemu ya juu ya menyu ya Maelezo + Ficha menyu ya \'Msaada & maoni\' + Ficha menyu ya \'Sikiliza kwa YouTube Music\' + Ficha menyu ya \'Funga skrini\' + Ficha menyu ya \'Rudia video\' + Ficha menyu ya \'Ukiwa njiani\' + Ficha menyu ya Ubora + Ficha sehemu ya chini ya menyu ya Ubora + Ficha sehemu ya juu ya menyu ya Ubora + Ficha menyu ya \'Kipima muda cha kulala\' + Ficha menyu ya \'Kasi ya uchezaji\' + Ficha menyu ya \'Sauti thabiti\' + Ficha menyu ya \'Tazama katika VR\' + Ficha onyo la kucheza kiotomatiki + Huficha onyo la video inayokuja katika skrini nzima kabla ya autoplay kuanza + Vitufe vya overlay + Chagua vitufe gani vionekane juu ya kicheza video + Ficha kitufe cha Autoplay + Ficha kitufe cha Maelezo + Ficha kitufe cha Cast + Ficha kitufe cha Kunyoshea + Ficha kitufe cha Skrini nzima + Ficha mandhari ya vidhibiti vya kichezaji + Huficha kivuli cheusi kinachozunguka vidhibiti vya kicheza video + Opacity ya mandhari ya vitufe vya udhibiti + Rekebisha uwazi wa mandharinyuma ya vitufe vya udhibiti wa kicheza na baa ya muda (0 haionekani, 100 ni nzito) + Ficha vitufe vya Nyuma & Fuata + Ficha kitufe cha Mipangilio + Ficha kadi za skrini ya mwisho + Hali ya Ambient + Dhibiti uonekano wa hali ya Ambient na vizuizi vya nguvu + Ruka vizuizi vya Hali ya Ambient + Inaruhusu hali ya Ambient kuendelea kuwa hai katika hali ya kuokoa betri + Zima Hali ya Ambient + Zima hali ya Ambient kwenye skrini nzima + Ficha kadi za maelezo + Zima vichocheo vya namba zinazozunguka + Inasimamisha uhuishaji wa kuhesabu kwenye idadi ya vipendasho na idadi ya utazamaji + Ficha seekbar ya kichezaji cha video + Ficha seekbar ya picha ndogo za video + Huficha upau wa maendeleo ya kutazama unaoonyeshwa kwenye thumbnails za video ulizotazama tayari + Wezesha seekbar kubwa ya skrini nzima + Ficha Shorts + Ficha kwenye ukurasa wa chaneli + Ficha kwenye firidiji ya Nyumbani & video zinazohusiana + Ficha kwenye matokeo ya utafutaji + Ficha katika feed ya Usajili + Ficha katika maelezo ya video + Ficha katika historia ya kutazama + Kicheza Shorts + Chagua vipengele vinavyoonekana kwenye kichezaji cha Shorts + Zima kugusa mara mbili ili kupenda + Ficha kitufe cha AI + Ficha lebo ya \'Imeongezewa sauti kiotomatiki\' + Ficha upau wa chaneli + Ficha kitufe cha Maoni + Ficha kitufe cha Athari + Ficha lebo ya kiungo cha video + Ficha vidokezo vya ishara + Ficha kitufe cha \'Skrini ya kijani\' + Ficha kitufe cha Hashtag + Ficha jopo la habari + Ficha kitufe cha Jiunge + Ficha kitufe cha Like + Ficha uhuishaji wa chemchemi ya kitufe cha Like + Ficha onyo la moja kwa moja + Ficha lebo ya eneo + Ficha upau wa urambazaji + Ficha kitufe cha \'Posti mpya\' + Ficha vitufe vya tabaka la kusimama + Ficha maoni ya utangulizi + Ficha kitufe cha Remix + Ficha kitufe cha Hifadhi + Ficha kitufe cha \'Hifadhi muziki\' + Ficha mapendekezo ya utafutaji + Ficha kitufe cha Shiriki + Ficha kitufe cha Duka + Ficha kitufe cha Sauti + Ficha lebo ya metadata ya sauti + Ficha kitufe cha Jisajili + Ficha kitufe cha \'Nunua Super Thanks\' + Ficha bidhaa zilizotagwa + Ficha kitufe cha Inayokuja + Ficha kitufe cha \'Tumia sauti hii\' + Ficha kitufe cha \'Tumia kiolezo hiki\' + Ficha kichwa cha video + Ficha video inayopendekezwa kwenye skrini ya mwisho + "Huficha tabaka la video inayopendekezwa linalotokea baada ya video kuisha + +Mpangilio huu unafanya kazi tu ikiwa autoplay imezimwa. Unaweza kuibadilisha kwenye YouTube: +Settings → Playback (au Autoplay) → Autoplay next video" + Ficha video zinazohusiana + Ficha tabaka la \'Video zaidi\' + Huficha kielezo cha \'Video zaidi\' kinachoonekana unapoteleza juu katika hali ya kioo kizima + Ficha muhula wa video + Zima paneli za popup za kicheza + Huzima paneli za orodha ya kucheza na soga ya moja kwa moja zisifunguke kiotomatiki + Zima uchezaji wa autoplay wa orodha ya nyimbo + Huzuia orodha za nyimbo zisihamie kwenye video inayofuata kiotomatiki + Zima ombi la \'Endelea kutazama\' + Toka katika hali ya skrini kamili video inapokwisha + Imezimwa + Wima pekee + Mlalo pekee + Wima na mlalo + Lazimisha hali ya mlalo katika skrini kamili + Zungusha kicheza video kuwa katika hali ya mlalo unapoingia kwenye skrini kamili. Haipatikani kwenye skrini ndogo, ambazo tayari hufungua skrini kamili katika hali ya mlalo + Ishara za skrini nzima + Ishara za kidirisha katika hali ya skrini kamili + Zima ishara ya kupapasa juu katika hali ya wima + Zima ishara ya kupapasa chini katika hali ya wima + Zima ishara ya kutelezesha chini katika hali ya mlalo + Zima ishara ya kutanua ili kukuza + Rudisha kunyoosha ili kukuza + Fungua video katika hali ya skrini nzima + Imezimwa + Wima + Mlalo + Kipimo cha video ya skrini nzima + Kawaida + Tanua (jaza skrini) + Zoom (kata ili ujaze) + Onyesha kitufe cha ukubwa wa video + Gusa ili ubadilishe kati ya Chaguo-msingi, Tanua, na Zoom + Onyesha katika skrini kamili pekee + Ficha kitufe cha ukubwa wa video kwenye kicheza cha wima + Onyesha kitufe cha Kuzima sauti + Ufifu wa tabaka la juu la kinawiri + Rekebisha kiwango cha uwazi (0 ni wazi, 100 ni isiyopitisha mwanga) + Upau wa utafutaji + Imezimwa + Upana + Upana zaidi + Mpangilio wa kifaa + Chaguo-msingi + Simu + Tableti + Kiotomati + "Mabadiliko ni pamoja na: + +Mpangilio wa tablet +• Machapisho ya jamii yamefichwa +• Mpangilio wa uchezaji katika feed haupatikani +• Kitufe cha Remix na kitufe cha Sauti havipatikani katika Shorts +• Tabaka la 'Video zaidi' linaweza lisipatikane +• Upau wa vitendo wa video ya zamani usiofupishwa unaonyeshwa +• Vitu vya menyu ya flyout vinaweza visiwork kwa usahihi + +Mpangilio wa Automotive +• Feed imepangwa kwa mada na chaneli +• Mpangilio wa uchezaji katika feed haupatikani +• Kitufe cha Remix na kitufe cha Sauti havipatikani katika Shorts +• Tabaka la 'Video zaidi' linaweza lisipatikane +• Shorts hufunguliwa katika kicheza cha kawaida +• Upau wa vitendo wa video ya zamani usiofupishwa unaonyeshwa" + Washa mpangilio wa tablet kwenye kicheza + "Hurudisha muundo wa zamani wa kicheza + +Kikomo: Ikiwa 'Rudisha upau wa zamani wa vitendo vya video' imezimwa, tabaka la 'Video zaidi' linaweza lisipatikane" + 21.11.48 - Rudisha tabia ya zamani ya maelezo ya video + 20.39.41 - Rudisha hifadhi ya zamani kwenye orodha ya nyimbo + 20.28.41 - Zima aikoni nzito + 20.13.41 - Rudisha jina la chaneli chini ya kicheza video + Fungua ukurasa wa kushiriki wa mfumo + Hufungua ukurasa wa kushiriki wa mfumo badala ya ule wa ndani ya programu wakati wa kushiriki maudhui + Badili vitufe vya YouTube Music + Hufungua programu yako ya muziki unayopendelea badala ya programu ya stock ya YouTube Music + Jina la kifurushi cha programu ya muziki + Jina la kifurushi cha programu yako ya muziki iliyosakinishwa + Ukurasa wa kuanzia + Chaguo-msingi + Usajili wote + Vinjari chaneli + Kozi / Kujifunza + Gundua + Mitindo & Urembo + Michezo ya Video + Historia + Maktaba + Video zilizopendwa + Moja kwa moja + Filamu + Muziki + Habari + Arifa + Orodha za video + Tafuta + Ununuzi + Shorts + Michezo + Usajili + Yaliyovuma + Uhalisia Pepe + Tazama baadaye + Vipande vyako + Zima kuendelea pale programu inapoanza + Fungua Shorts kwa + Kicheza Shorts + Kicheza kawaida + Kicheza kawaida cha skrini nzima + Cheza Shorts kiotomatiki + Huwendelea kwenye Short inayofuata kiotomatiki badala ya kurudia ya sasa + Cheza kiotomatiki nyuma + Huwendelea kwenye Short inayofuata kiotomatiki wakati wa uchezaji wa nyuma + Ruka Shorts za moja kwa moja + Ruka Viratilizi vinavyotokea kwenye feed ya Shorts + Uchezaji katika feed + Kila wakati + Wi-Fi pekee + Zima + Rejesha vichwa vya habari vya asili + Inaonyesha vichwa vya asili vya video, maelezo ya video na maelezo ya chaneli badala ya yale yaliyotafsiriwa kiotomatiki + Inapakia kichwa cha habari… + Zima urejesho wa feed kiotomatiki + Huzuia feed zisijihuishishe kiotomatiki baada ya kupitwa na wakati + Mpangilio wa kadi ya feed + Kawaida + Kadi + Kadi zenye vitufe vya kupenda, kuongeza maoni na kuhifadhi + Maoni + Zima kurejea kwa miniplayer + Huondoa miniplayer ya \'Endelea kutazama\', kipengele cha YouTube Premium kinachotokea wakati programu inaanza + Kicheza kidogo + Badilisha mwonekano na utendaji wa kivinjari cha video kilichopunguzwa + Aina ya kicheza kidogo + Ficha kichwa na chaneli + Zimwa + Chaguo-msingi + Kiwize + Minimal 2 + Tableti + Modern 1 + Modern 2 + Modern 3 + Modern 4 + Zima pembe zilizozungushwa + Zima buruta na uachilie + Huzuia kuburuta kivinjari kilichopunguzwa kwenda kwenye pembe tofauti za skrini + Zima ishara ya kuburuta kwa mlalo + Huzuia kufunga miniplayer kwa kuipunguzia kwa mlalo nje ya skrini + Sitisha uchezaji kwa kuburudisha kwa mlalo + Uchezaji utasimama wakati miniplayer inapoivutwa nje ya skrini + Kumbuka nafasi ya hali ya kuburudisha + Kuburudisha kidchezaji kidogo nje ya skrini kutahifadhi hali yake ya UI + Ficha vitufe vya overlay + Ukubwa wa kawaida + Weka ukubwa wa kuanzia kwenye skrini, kwa pikseli + Ukubwa wa pikseli lazima uwe kati ya %1$s na %2$s + Uwazi wa overlay + Rekebisha kiwango cha uwazi (0 ni wazi, 100 ni isiyo wazi) + Kitufe cha kurudi kinatoka kwenye feed + Kubonyeza kurudi kunatoka kwenye programu kila wakati badala ya kusogeza juu ya feed + Lazimisha uhuishaji + Kawaida + Iwe imewashwa kila wakati + Iwe imezimwa kila wakati + Ficha upau wa hali + Huficha upau wa hali wa mfumo. Teleza chini kutoka ukingo wa juu ili kuuonyesha kwa muda + Rekebisha upau wa hali unaofunika upau wa juu + Huifanya mpangilio ya upau wa hali kuwa thabiti, ili isifunike upau wa juu na isiache nafasi tupu juu ya kichezaji. Zima hii ikiwa programu itafunga ghafla au upau wa juu ukionekana vibaya + Mng\'ao + Skrini ya kuanzia ya mwangaza haiungwiwa mkono na kifaa hiki + Rangi ya nyuma ya mandhari ya mwanga + Nyeupe + Material You (Nyeupe) + Rangi ya pinki hafifu + Bluu hafifu + Kijani hafifu + Njano hafifu + Chungwa hafifu + Nyekundu hafifu + Mandhari hafifu ya kibinafsi + Weka rangi ya mandhari ya rangi hafifu + Badilisha rangi ya mbele + Tumia rangi ya mandhari kwa maandishi na ikoni + Washa skrini ya kupakia ya gradient + Inaonyesha mandhari ya rangi inayofifia kwenye skrini ya kupakia badala ya rangi moja + Washa rangi ya kioo cha kutafutia ya kioo + Rangi ya seekbar + Weka rangi kuu ya baa ya maendeleo ya video + Rangi ya mkazo ya seekbar + Weka rangi ya pili ya baa ya maendeleo ya video + Thamani ya rangi ya seekbar si sahihi + Analog ya asili + Keki asilia + Metali asilia + Nembo ya kichwa + Kawaida + Ya kawaida + Kistaili + Ruka vizuizi vya kanda ya picha + Inahamishia maombi ya picha ndogo kwenye seva mbadala ili kurekebisha picha zilizokosekana katika nchi zenye vizuizi + Vichwa vya DeArrow + Tab ya Nyumbani + Tab ya Usajili + Tab ya Wewe + Orodha za nyimbo za kicheza & mapendekezo + Matokeo ya utafutaji + Onyesha aikoni ya DeArrow + Huonyesha aikoni ya DeArrow kwenye vichwa vya video vilivyokusanywa kutoka kwa jamii + Hali ya kawaida + Inaonyesha kichwa asilia badala ya kichwa cha DeArrow ikiwa watumiaji wa DeArrow walipiga kura kwamba wanapenda kichwa asilia + Kura za chini za kawaida ili kuonyesha kichwa asilia + Idadi ya kura za kawaida katika kundi moja, kama vile ya kuchekesha au ya kibunifu, zinazohitajika ili kuonyesha kichwa asilia + Vitupande vya DeArrow + Vichogo asilia + Vichogo vya DeArrow & asilia + DeArrow & picha tuli + Picha tulivu + "DeArrow inabadilisha vichwa vya habari na picha za jalada za kuvutia watu na zile zinazotolewa na jamii ambazo ni sahihi zaidi na hazina msisimko wa kupitiliza. Vichwa vya habari huandikwa na kupigiwa kura na jamii, na picha za jalada ni fremu zilizotoka kwenye video + +Video huombwa kwa kutumia kiambishi kifupi cha ID ya video iliyofichwa, hivyo seva haiwezi kujua hasa ni video gani unaitazama. Ikiwa video haina picha ya jalada ya DeArrow, picha ya asili au ya picha tuli huonyeshwa" + Onyesha tahadhari za hitilafu ya muunganisho + Inaonyesha taarifa wakati seva ya DeArrow isipopatikana + Ncha ya API + URL ya kidokezo cha cache ya DeArrow thumbnail + Nakala tulivu za video + Picha tulivu bado zinachukuliwa kutoka mwanzo / katikati / mwisho wa kila video. Picha hizi zimejengwa ndani ya YouTube na hakuna API ya nje inayotumika + Toa fremu ya video kutoka + Mwanzo + Kati + Mwisho + DeArrow haipatikani kwa muda (msimbo wa hali: %s) + DeArrow haipatikani kwa muda + Rejesha mtindo wa zamani wa kitufe cha kicheza + Hurudisha vidhibiti vya kidirisha cha video vilivyo nene kuwa mtindo wa zamani wa mwonekano mwembamba + Ighushi vipimo vya kifaa + Kuwasha hili kunaweza kufungua ubora wa juu zaidi wa video + Kuwezesha hii kunaweza kusababisha kukwama kwa uchezaji wa video, maisha ya betri kuwa mabaya, na athari zingine zisizojulikana + Mrejesho wa haptiki + Chagua mwingiliano gani uchochee mtetemo wa kifaa + Zima haptiki za sura + Zima haptiki za utafutaji sahihi + Zima haptiki za kubatilisha utafutaji + Zima haptiki za kugusa na kushikilia + Zima haptiki za kukuza + Ikiwa hivi karibuni umebadilisha maelezo ya kuingia kwenye akaunti yako, basi ondoa na usakinishe tena MicroG + Ruka upoeshaji wa viungo + Inafungua kiungo asilia moja kwa moja, ikiruka kiungo cha ufuatiliaji (youtube.com/redirect) + Fungua viungo kwenye kivinjari + Hufungua viungo katika kivinjari cha nje badala ya kivinjari cha ndani ya programu + Ukubwa wa bafa ya uchezaji + Kawaida + Ndogo + Kati + Kiwango cha juu + Kuchelewa kwa video + Huichelewesha video ili iende sambamba na sauti inayochelewa, kama vile headphones za Bluetooth (0-5000 ms) + Chelewesha tu ukiwa na sauti ya Bluetooth + Huichelewesha video tu wakati sauti ya Bluetooth imeunganishwa, ili spika ya simu iende sambamba + Onyesha kitufe cha kuchelewesha Video + Gusa na ushikilie ili kurudisha ucheleweshaji kuwa 0 ms + Onyesha menyu ya kuchelewesha Video + Ucheleweshaji katika milisekunde + Kiotomatiki + Kumbuka ubora wa video + Hutumia uteuzi wako wa ubora wa mkono kwa video zote zijazo + Arifa za mabadiliko ya ubora + Huonyesha taarifa wakati ubora mpya wa chaguo-msingi unapohifadhiwa + Ubora wa video wa kawaida kwenye mtandao wa Wi-Fi + Ubora wa video wa kawaida kwenye mtandao wa simu + Kumbuka ubora wa Shorts + Hutumia uchaguzi wako wa ubora wa mwongozo kwa Shorts zote zijazo + Ubora wa kawaida wa Shorts kwenye mtandao wa Wi-Fi + Ubora wa kawaida wa Shorts kwenye mtandao wa simu + Ubora wa kawaida wa mtandao wa simu umebadilishwa kuwa: %s + Ubora wa kawaida wa Wi-Fi umebadilishwa kuwa: %s + Ubora wa Shorts wa mtandao wa simu umebadilishwa kuwa: %s + Ubora wa Wi-Fi wa Shorts umebadilishwa kuwa: %s + Onyesha kitufe cha Kasi ya uchezaji + Gusa na ushikilie ili kurudisha kasi kwenye chaguo-msingi + Onyesha kitufe cha Ubora + Gusa na ushikilie ili kurudisha ubora kwenye chaguo-msingi + Onyesha azimio kwenye kitufe cha Ubora + Onyesha azimio, kama vile 1080, badala ya lebo kama FHD + Menyu ya kasi ya uchezaji ya kulingana na hitaji + Rudisha menyu ya zamani ya kasi ya uchezaji + Kasi za uchezaji za kulingana na hitaji + Ingiza chaguo la kasi ya uchezaji unayopendelea + Kasi za uchezaji za kipekee lazima ziwe chini ya %s + Kasi za uchezaji za kulingana na hitaji haziko sahihi + Kiotomatiki + Gusa na ushikilie kasi ya uchezaji + Weka kizidishaji cha kasi unapobonyeza na kushikilia skrini, weka 0 ili kuzima + Kumbuka kasi ya uchezaji + Hutumia uteuzi wako wa kasi ya mwongozo kwa video zote zijazo + Arifa za mabadiliko ya kasi + Huonyesha taarifa wakati kasi mpya ya uchezaji ya chaguo-msingi inapohifadhiwa + Kasi ya uchezaji ya kawaida + Kasi ya kawaida ya uchezaji kwenye Shorts + Kumbuka kasi ya uchezaji kwenye Shorts + Hutumia uteuzi wako wa mwongozo wa kasi ya Shorts kwa Shorts zote zijazo. Ikiwa imezimwa, kasi hurudi kwenye chaguo-msingi baada ya kufunga kicheza cha Shorts + Onyesha Kasi ya uchezaji kwenye menyu ya Shorts + Huongeza kipengele cha kasi ya uchezaji kwenye menyu ya kidchezaji cha Shorts + Kasi ya uchezaji · %s + Kasi ya uchezaji ya chaguo-msingi imebadilishwa kuwa: %s + Tumia kasi ya kawaida kwa muziki + Cheza video za muziki kila wakati kwa kasi ya 1.0x + Orodha nyeupe ya kasi ya chaneli + Video kutoka kwa chaneli zilizomo kwenye orodha nyeupe huchezwa kila wakati kwa kasi ya 1.0x + Ongeza chaneli kwenye orodha nyeupe ya kasi + Ondoa chaneli kwenye orodha nyeupe ya kasi + Washa vidhibiti vya pitch ya sauti + Pitch ya sauti ya kawaida ya uchezaji + Pitch ya kawaida ya sauti imebadilishwa kuwa: %s + Kunyoosha muda wa sauti + Inaruhusu kuweka kasi na pitch bila kutegemeana, inaweza kusababisha hitilafu za sauti + Kumbuka nafasi ya uchezaji wa utiririko wa moja kwa moja + Kwa utiririko wa moja kwa moja unaoendelea, hukumbuka na kuendelea na nafasi yako ya uchezaji badala ya kutafuta nafasi ya moja kwa moja ya sasa + Kipengele hiki hakifanyi kazi na utiririko wa moja kwa moja wa 24/7 na utiririko mwingine ambao \'uko wazi kila wakati\' + Zima video ya HDR + Lazimisha video ya HDR + Hulazimisha programu kutambua kifaa hiki kuwa kina uwezo wa HDR + "Tumia hii ikiwa udekodi wa HDR unafanya kazi lakini YouTube haitoi HDR, kama vile baadhi ya vifaa vya HyperOS vinavyoficha HLG. + +Vikomo: +• Rangi zinaweza kuonekana vibaya ikiwa kioo hakiwezi kuonyesha HDR +• Uchezaji unaweza kushindwa ikiwa didekoda haiwezi kushughulikia HDR +• HDR bado inategemea mteja wa kuiga mitiririko ya video ya HDR" + Kodi ya video + Ya kawaida + Ili kulazimisha AVC, badilisha \'Iga video streams\' kuwa \'Android Studio\', \'Android VR\' au \'visionOS\' + "VP9 +• Inaweza kuboresha maisha ya betri na kurekebisha kukwama kwa uchezaji +• Inatumia data nyingi zaidi ya mtandao kuliko AV1 + +AVC (H.264) +• Inahitaji mteja wa Iga video stream wa 'Android Studio', 'Android VR', au 'visionOS' +• Inaweza kuboresha maisha ya betri zaidi +• Inaweza kurudisha azimio la video lililokosekana kwenye vifaa vya zamani +• Azimio la juu zaidi ni 1080p +• Video za HDR zinaweza zisitumie AVC +• Baadhi ya vifaa haviwezi kulazimisha AVC" + Fungua menyu ya ubora wa juu + Ruka menyu ya uteuzi wa ubora na ufungue menyu ya Ubora wa Juu moja kwa moja + Ficha chaguzi za ubora wa Premium + Toa kipaumbele kwa ubora wa video + Hutoa kipaumbele kwa ubora wa juu zaidi wa video unaopatikana badala ya ufanisi wa codec + Ikiwa mpangilio huu umezimwa, basi video za zamani zinaweza zisioonyeshe chaguzi zozote za ubora wa video + Ruhusu \'Android VR\' / visionOS AV1 + Inaruhusu udekodishaji wa programu ya AV1 wakati unapotumia mteja wa \'Android VR\' / visionOS + "Kuwezesha mpangilio huu kunaweza kutumia udekodi wa programu ya AV1. + +Uchezaji wa video kwa AV1 unaweza kukwama au kupoteza fremu" + Athari za kulingana na uigizaji + • Uchezaji unaweza kushindwa baada ya dakika 1 + • Menyu ya \'Audio Track\' haipo + • Hakuna codec ya video ya AV1 + • Hali ya VR ya 360° immersive haipatikani + • Sauti thabiti haipatikani + • Uchezaji unaweza kuishia sekunde 1 mapema + • Ikiwa deobfuscation itashindwa, uchezaji unaweza kushindwa + • Kulazimisha sauti halisi haipatikani + • Kuingia kwenye Android VR kunahitajika + • Ubora unaweza kuwa na kikomo cha 720p + Tofauti ya JavaScript ya kicheza + Onyesha uigizaji katika \'Stats for nerds\' + Rejesha upau wa vitendo wa video wa zamani + "Inatengeneza vitufe vya vitendo vya video visivyoweza kufichwa + +Ukomo: Inarudi katika mtindo wa zamani wa overlay ya 'Video zaidi'" + Mtindo wa aikoni ya kicheza + Kiotomatiki + Kawaida + Nyembamba + Kukoleza + Kibinafsi + Mtindo wa aikoni ya Shorts + Kifuatilia Mfululizo + Fuata orodha za nyimbo kama mfululizo na uhifadhi ulipo + Washa ufuatiliaji wa mfululizo + Huongeza ukurasa wa Mfululizo kwenye Historia na chaguo la Kufuatilia mfululizo kwenye orodha za nyimbo + Onyesha Mfululizo kwenye bar ya urambazaji + Onyesha kitufe cha Kifuatilia Mfululizo + Huongeza kitufe cha mchezaji ili kufuatilia au kufungua mfululizo wa video ya sasa + Rekodi maendeleo ya mfululizo unaofuatiliwa + Rekodi uchezaji kwenye simu hii kwa mfululizo uliohifadhiwa na alama za vitabu + Ghairi + Hifadhi + Hifadhi alama ya kitabu + Jina la mfululizo + Chaguzi za mfululizo + Chaguzi za maktaba + Osha vipindi + Badilisha jina + Fungua orodha ya nyimbo kwenye YouTube + Ondoa kwenye maktaba + Kiungo au ID ya orodha ya nyimbo + Futa maendeleo yote ya ndani + Futa historia + Futa maendeleo yote ya ndani? + Hii inafuta nafasi, mabadiliko ya kutazamwa na uteuzi wa kuendelea kwa kila video. Mfululizo uliohifadhiwa na katalogi zao zinabaki. Uchezaji mpya utarekodiwa ikiwa rekodi imewezeshwa + Weka alama ya imetazamwa + Ondoa alama ya imetazamwa + Tumia hali ya kiotomatiki + Anza upya kutoka 0:00 + Cheza tena kuanzia 0:00 + Hali imehifadhiwa + Jaribu kuhifadhi tena + Zilizopita 100 + Zinazofuata 100 + Fungua video iliyohifadhiwa + Maendeleo ya ndani yamefutwa + Fungua Historia kutoka kwako ili kuona mfululizo wako + Historia ya utazamaji + Mfululizo + Inapakia mfululizo ulihifadhiwa… + Fuata orodha ya uchaguzi ili kufuatilia vipindi vyake + Alama ya orodha ya nyimbo + Inaboresha vipindi… + Mfululizo wote + Imetazamwa + Inapakia vipindi… + Chaguzi zaidi + "Rekodi maendeleo kwa mfululizo wako uliohifadhiwa na video ulizoweka alama tu. + +Maktaba hii huhifadhiwa kwenye kifaa hiki na kushirikiwa katika akaunti za YouTube. Vidhibiti vya YouTube vya kusitisha na kufuta historia havibadilishi jambo hili. Tumia Mfululizo → Zaidi → Futa maendeleo ya ndani ili kuifuta. + +Kurekodi kiotomatiki kunasimama katika hali ya incognito, wakati utambulisho wa akaunti haupatikani, au unapohamia akaunti tofauti. Wezesha ufuatiliaji tena ili kuutumia na akaunti hiyo" + Ufuatiliaji hauwezi kuwashwa katika hali ya siri au kabla ya YouTube kupakia hali ya sasa ya akaunti. Rudi Nyumbani na ujaribu tena. Hakuna hitaji la kuingia + Washa ufuatiliaji wa ndani + Kurekodi maendeleo kuzimwa + Endelea kwenye %1$s + %1$s / %2$s vipindi vimetazamwa + Kipindi %1$s · %2$s + Vipindi %1$s-%2$s kati ya %3$s + Refresh ili ujaribu kupakia vipindi. Orodha za nyimbo za siri zinahitaji ufikiaji wa akaunti; alama zao bado zinafunguliwa katika YouTube + YouTube haikuweza kufungua sehemu hii + YouTube haikuweza kufungua orodha hii ya nyimbo + Chagua kipindi + Imekamilika + Hakuna sehemu zinazoweza kuchezwa + Video iliyohifadhiwa · %1$s + Fuata mfululizo + Tazama mfululizo + Endelea + Anza hapa + Pointi ya kuanzia imehifadhiwa + Umetazama hadi hapa + Sehemu zimeashiriwa kuwa zimetazamwa + Ondoa + Washa + kipindi 1 kipya + {%1$s} vipindi vipya + Rekodi maendeleo kwenye kifaa hiki + "Mfululizo na maendeleo yako yanabaki kwenye kifaa hiki, yakishirikishwa katika akaunti za YouTube. Vidhibiti vya historia ya YouTube havibadilishi mambo haya. Futa maendeleo kutoka kwenye menyu ya Mfululizo. + +Kurekodi kiotomatiki ni hiari. Inasimama katika hali ya siri au wakati akaunti inapobadilishwa" + Vipindi havikuweza kupakia. Unaweza kuhifadhi orodha ya nyimbo kama alama ya ukurasa + Mabadiliko mengi ambayo hayajahifadhiwa. Jaribu kuhifadhi kwanza + Uhifadhi uko busy. Jaribu tena + Upakiaji wa kipindi uko busy. Jaribu tena + Hifadhi haipatikani. Data yako iliyohifadhiwa imetunzwa + Mabadiliko hayakuweza kuhifadhiwa. Jaribu kuhifadhi tena kabla ya kufunga YouTube + Mfululizo huu tayari umehifadhiwa, au orodha yake ya kucheza imebadilika. Ifungue tena + Mfululizo huu umebadilika au umeondolewa. Fungua tena maktaba + Orodha ya kucheza imebadilika. Chagua sehemu tena + Sehemu hii haipatikani. Chagua nyingine + Ingiza jina la mfululizo + Ingiza kiungo cha orodha ya kucheza ya YouTube au ID ya orodha ya kucheza + Tafadhali subiri kabla ya kuburudisha tena + Vipindi havikuweza kupakiwa. Jaribu tena baadaye + Mabadiliko hayakuweza kukamilika. Jaribu tena + Kuendelea hakikuweza kuthibitishwa. Angalia kinawasha na ujaribu tena + Mfululizo unaofuatwa + Video hii haimo katika orodha hii ya nyimbo + Inatafuta orodha za nyimbo… + Bandika kiungo cha orodha ya nyimbo + Imeshindwa kupata orodha ya nyimbo + Hakuna vipindi vinavyopatikana + Ficha vilivyoonekana + Weka alama vipindi vya awali vimetazamwa + Inaanza na: %1$s + Mpangilio wa vipindi + Mpangilio wa orodha ya nyimbo + Geuza mpangilio wa orodha ya nyimbo + %1$s mtazamo + tazama %1$s + Weka alama ya vipindi vilivyotazamwa + Angalau %1$s%% imetazamwa kukiwa na sekunde zisizozidi 90 zilizobaki, au sekunde %2$s zimebaki + Weka alama ya kipindi kama kilichotazamwa kiotomatiki baada ya kufikia kiwango chochote. Sheria ya asilimia pia inahitaji kusiwe na zaidi ya sekunde 90 zilizobaki, ili mwisho wa vipindi virefu usirukwe. Sheria ya muda uliobaki inatumika kwa video ndefu kuliko dakika moja; iweke 0 ili kuizima. Mabadiliko yanatumika kwa sasisho za maendeleo za baadaye. Chaguzi za mwongozo za kutazamwa/kutotazamwa zinahifadhiwa + Asilimia iliyotazamwa (1-100) + Sekunde zilizobaki (0-300; 0 huzima) + Ingiza thamani kuanzia %1$s hadi %2$s + Tumia maendeleo ya YouTube + Endelea na mfululizo uliofuata baada ya kutazama kwenye kifaa kingine + Soma hali ya kutazamwa na nafasi za kuendelea kwa mfululizo wa vipindi unavyofuata kutoka kwenye akaunti ya sasa ya YouTube. Ni vipindi vinavyolingana tu ndivyo huhifadhiwa kwenye simu hii. Ukaguzi hufanyika wakati maktaba ya mfululizo ikiwa wazi na kabla ya Kuendelea. Historia ya YouTube lazima iwe imewashwa kwenye kifaa chako kingine + Inakagua maendeleo ya YouTube… + Maendeleo ya YouTube hayapatikani. Tunatumia maendeleo yaliyohifadhiwa + Baadhi ya maendeleo ya YouTube hayapatikani. Nafasi zilizohifadhiwa zimebaki + Nakili na urejeshe + Hifadhi maktaba yako ya mfululizo kwenye faili au urejeshe nakala ya awali + Huhifadhi mfululizo wa vipindi vinavyofuatwa, nafasi za vipindi na chaguzi za kutazamwa. Ruhusa za kurekodi na maendeleo ya YouTube hazijajumuishwa + Hifadhi nakala + Rejesha nakala + Ongeza mfululizo %1$s. Mfululizo %2$s uliokuwepo tayari kwenye maktaba yako umehifadhiwa. Maendeleo ya video yaliyopo yamehifadhiwa + Nakala ya mfululizo imehifadhiwa + Imerejesha mfululizo %1$s + Faili hili si nakala ya usalama ya mfululizo inayokubalika, au ni kubwa mno. Maktaba yako haikubadilishwa + Hakuwezekana kuhifadhi nakala ya usalama. Jaribu eneo lingine + Mfululizo umeondolewa + Kipindi hiki sasa kinaonekana zaidi ya mara moja. Chagua kipi cha kuendelea nacho + Kipindi chako kilichohifadhiwa hakimo tena kwenye orodha hii ya nyimbo. Nafasi yako iliyohifadhiwa imebaki + Endelea na kipindi %1$s · %2$s + Pia unaweza kuchagua kipindi chochote hapa chini. Vipindi vya awali havibadilishwi + Orodha hii ya nyimbo imebadilika. Fungua mfululizo ili kuchagua mahali pa kuendelea + Hakuna vipindi zaidi katika upande huu + Endelea %1$s + Tafuta mfululizo + Futa utafutaji + Hakuna mfululizo uliopatikana + Kipindi kisicho na kichwa + Kipindi kisichopatikana + Maoni ya Niconico + Onyesha soga ya moja kwa moja na marudio ya soga kama maoni yanayopita kwenye video + Inaonyesha kitufe cha juu katika kicheza video. Gusa ili kuonyesha au kuficha maoni, gusa na ushikilie kwa mipangilio ya haraka + Ukubwa wa herufi za maoni + Ukubwa wa maandishi ya maoni (12-40 sp) + Uwazi wa maoni + Uwazi wa maandishi ya maoni na mandharinyuma + Muda wa kusafiri + Sekunde ngapi maoni yanachukua kuvuka skrini. Thamani ndogo husonga haraka zaidi + Urefu wa juu wa maoni + Huficha maoni yenye herufi nyingi kuliko hizi. Kila emoji huhesabiwa kama herufi moja + Hakuna kikomo diff --git a/patches/src/main/resources/addresources/values-ta-rIN/music/strings.xml b/patches/src/main/resources/addresources/values-ta-rIN/music/strings.xml index 1af6cc6c05..ccfea4ed71 100644 --- a/patches/src/main/resources/addresources/values-ta-rIN/music/strings.xml +++ b/patches/src/main/resources/addresources/values-ta-rIN/music/strings.xml @@ -27,10 +27,6 @@ Second \"item\" text" - ஊட்டம் - பொது - பிளேயர் - இதரவை diff --git a/patches/src/main/resources/addresources/values-ta-rIN/reddit/strings.xml b/patches/src/main/resources/addresources/values-ta-rIN/reddit/strings.xml index 198698d566..cf7776fdaf 100644 --- a/patches/src/main/resources/addresources/values-ta-rIN/reddit/strings.xml +++ b/patches/src/main/resources/addresources/values-ta-rIN/reddit/strings.xml @@ -15,7 +15,4 @@ Second \"item\" text" --> - - விளம்பரங்கள் - விபரம் - + diff --git a/patches/src/main/resources/addresources/values-ta-rIN/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-ta-rIN/shared-youtube/strings.xml index 32465cf89a..77635ba31c 100644 --- a/patches/src/main/resources/addresources/values-ta-rIN/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ta-rIN/shared-youtube/strings.xml @@ -18,7 +18,7 @@ Second \"item\" text" --> - முடக்கப்பட்டது + diff --git a/patches/src/main/resources/addresources/values-ta-rIN/shared/strings.xml b/patches/src/main/resources/addresources/values-ta-rIN/shared/strings.xml index 2f050b786d..31cdbe4dcb 100644 --- a/patches/src/main/resources/addresources/values-ta-rIN/shared/strings.xml +++ b/patches/src/main/resources/addresources/values-ta-rIN/shared/strings.xml @@ -18,7 +18,6 @@ Second \"item\" text" - விபரம் diff --git a/patches/src/main/resources/addresources/values-ta-rIN/sponsorblock/strings.xml b/patches/src/main/resources/addresources/values-ta-rIN/sponsorblock/strings.xml index 9a00eda3a1..9d2d4ab7d5 100644 --- a/patches/src/main/resources/addresources/values-ta-rIN/sponsorblock/strings.xml +++ b/patches/src/main/resources/addresources/values-ta-rIN/sponsorblock/strings.xml @@ -16,7 +16,6 @@ Second \"item\" text" --> - பொது diff --git a/patches/src/main/resources/addresources/values-ta-rIN/youtube/strings.xml b/patches/src/main/resources/addresources/values-ta-rIN/youtube/strings.xml index 539b66cd1a..4ec76b1150 100644 --- a/patches/src/main/resources/addresources/values-ta-rIN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ta-rIN/youtube/strings.xml @@ -18,16 +18,6 @@ Second \"item\" text" - விபரம் - விளம்பரங்கள் - ஊட்டம் - பொது - பிளேயர் - Shorts - தேடல்பட்டை - ஸ்வைப் கட்டுப்பாடுகள் - இதரவை - காணொளி @@ -41,6 +31,7 @@ Second \"item\" text" This button usually appears in the Subscriptions feed for future live streams or unreleased videos. --> + @@ -81,7 +72,6 @@ Second \"item\" text" - காணொளி @@ -177,12 +167,10 @@ Second \"item\" text" - முடக்கப்பட்டது - முடக்கப்பட்டது diff --git a/patches/src/main/resources/addresources/values-te-rIN/music/strings.xml b/patches/src/main/resources/addresources/values-te-rIN/music/strings.xml index 25de5c2e4e..ccfea4ed71 100644 --- a/patches/src/main/resources/addresources/values-te-rIN/music/strings.xml +++ b/patches/src/main/resources/addresources/values-te-rIN/music/strings.xml @@ -18,8 +18,6 @@ Second \"item\" text" - రద్దుచేయి - తేరు @@ -27,12 +25,8 @@ Second \"item\" text" - మూయండి - డిలీట్ - ఫీడ్ - సాధారణం @@ -76,6 +70,5 @@ Second \"item\" text" - స్వయం చాలకం diff --git a/patches/src/main/resources/addresources/values-te-rIN/reddit/strings.xml b/patches/src/main/resources/addresources/values-te-rIN/reddit/strings.xml index d2b0fb5722..cf7776fdaf 100644 --- a/patches/src/main/resources/addresources/values-te-rIN/reddit/strings.xml +++ b/patches/src/main/resources/addresources/values-te-rIN/reddit/strings.xml @@ -15,6 +15,4 @@ Second \"item\" text" --> - - డిలీట్ - + diff --git a/patches/src/main/resources/addresources/values-te-rIN/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-te-rIN/shared-youtube/strings.xml index 234ba91e74..77635ba31c 100644 --- a/patches/src/main/resources/addresources/values-te-rIN/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-te-rIN/shared-youtube/strings.xml @@ -18,7 +18,7 @@ Second \"item\" text" --> - నిలిపివే + diff --git a/patches/src/main/resources/addresources/values-te-rIN/shared/strings.xml b/patches/src/main/resources/addresources/values-te-rIN/shared/strings.xml index 0956fbc829..31cdbe4dcb 100644 --- a/patches/src/main/resources/addresources/values-te-rIN/shared/strings.xml +++ b/patches/src/main/resources/addresources/values-te-rIN/shared/strings.xml @@ -18,7 +18,6 @@ Second \"item\" text" - మా గురించి @@ -27,10 +26,7 @@ Second \"item\" text" - ఆపు - చేయి - తొలగించు diff --git a/patches/src/main/resources/addresources/values-te-rIN/youtube/strings.xml b/patches/src/main/resources/addresources/values-te-rIN/youtube/strings.xml index 1a0ad87c32..4ec76b1150 100644 --- a/patches/src/main/resources/addresources/values-te-rIN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-te-rIN/youtube/strings.xml @@ -18,12 +18,7 @@ Second \"item\" text" - మా గురించి - ప్రకటనలు - ఫీడ్ - సాధారణం - ఆపు @@ -36,6 +31,7 @@ Second \"item\" text" This button usually appears in the Subscriptions feed for future live streams or unreleased videos. --> + @@ -59,7 +55,6 @@ Second \"item\" text" - తొలగించు @@ -74,8 +69,6 @@ Second \"item\" text" - శోధించు - క్లియర్ చేయి @@ -88,7 +81,6 @@ Second \"item\" text" - తొలగించు @@ -100,7 +92,6 @@ Second \"item\" text" - ఆపు @@ -123,7 +114,6 @@ Second \"item\" text" - ఆపు @@ -181,7 +171,6 @@ Second \"item\" text" - నిలిపివే @@ -193,7 +182,6 @@ Second \"item\" text" - ఆపు @@ -238,9 +226,6 @@ Second \"item\" text" - స్వయం చాలకం - రద్దుచేయి - చర్య రద్దు diff --git a/patches/src/main/resources/addresources/values-th-rTH/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-th-rTH/shared-youtube/strings.xml index 4838b0fcaf..98d671f8b3 100644 --- a/patches/src/main/resources/addresources/values-th-rTH/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-th-rTH/shared-youtube/strings.xml @@ -21,9 +21,9 @@ Second \"item\" text" แอนิเมชันเริ่มต้น ปิดการใช้งาน สี - สี Morphe โมโนโครม - Morphe แบบโมโนโครม + + ปิด รีสตาร์ทเพื่อดำเนินการตั้งค่าให้เสร็จสิ้น diff --git a/patches/src/main/resources/addresources/values-th-rTH/youtube/strings.xml b/patches/src/main/resources/addresources/values-th-rTH/youtube/strings.xml index 3b21b425f5..f896d0322a 100644 --- a/patches/src/main/resources/addresources/values-th-rTH/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-th-rTH/youtube/strings.xml @@ -113,6 +113,9 @@ Second \"item\" text" ซ่อนแบบสำรวจ ซ่อนแบบสำรวจและการแนะนำเกี่ยวกับความแม่นยำของการแนะนำฟีด ซ่อนชั้นวางตั๋ว + + ซ่อนแบนเนอร์ \"ซ่อนวิดีโอที่ไม่พร้อมใช้งานแล้ว\" + ซ่อนแบนเนอร์ \"ซ่อนวิดีโอที่ไม่พร้อมใช้งานแล้ว\" ในเพลย์ลิสต์ ซ่อนเวลาอัปโหลด "ข้อจำกัด: @@ -1249,6 +1252,7 @@ Second \"item\" text" คงรูปแบบของแถบสถานะให้สอดคล้องกัน เพื่อไม่ให้เกิดการบังแถบด้านบนและเหลือพื้นที่ว่างเปล่าเหนือเครื่องเล่น เปิดการใช้งานนี้หากแอปค้างหรือแถบด้านบนแสดงผลผิดปกติ เรืองแสง + อุปกรณ์นี้ไม่รองรับหน้าจอเปิดตัวแบบเรืองแสง พื้นหลังธีมสว่าง สีขาว Material You (สีขาว) diff --git a/patches/src/main/resources/addresources/values-tr-rTR/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-tr-rTR/shared-youtube/strings.xml index 27a87cdc8c..abdce1ff3f 100644 --- a/patches/src/main/resources/addresources/values-tr-rTR/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-tr-rTR/shared-youtube/strings.xml @@ -21,9 +21,13 @@ Second \"item\" text" Başlangıç animasyonu Devre dışı Renkli - Renkli Morphe - Tek renk - Tek renkli Morphe + Morphe Renkli + Tek Renk + Morphe Tek Renk + + Yeni ayarları vurgula + Bir güncelleme ile eklenen ayar ve seçeneklerin yanında bir noktanın gösterileceği gün sayısı + Kapalı Kurulumu tamamlamak için yeniden başlatın diff --git a/patches/src/main/resources/addresources/values-tr-rTR/sponsorblock/strings.xml b/patches/src/main/resources/addresources/values-tr-rTR/sponsorblock/strings.xml index f93f9a8a45..ac1f0d9033 100644 --- a/patches/src/main/resources/addresources/values-tr-rTR/sponsorblock/strings.xml +++ b/patches/src/main/resources/addresources/values-tr-rTR/sponsorblock/strings.xml @@ -58,6 +58,8 @@ Second \"item\" text" Okudum Göster Genel + İndirilen videoların kısımlarını kaydet + İndirilen videoların kısımlarını çevrim dışı iken atlamaya olanak tanır Bağlantı hatası uyarılarını göster SponsorBlock sunucusu kullanılamadığında bir bildirim gösterir Atlama sayısı takibini etkinleştir diff --git a/patches/src/main/resources/addresources/values-tr-rTR/youtube/strings.xml b/patches/src/main/resources/addresources/values-tr-rTR/youtube/strings.xml index f83f2b4578..233e53ceee 100644 --- a/patches/src/main/resources/addresources/values-tr-rTR/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-tr-rTR/youtube/strings.xml @@ -114,6 +114,9 @@ Değişiklikler şunları içerir: Anketleri gizle Akış öneri doğruluğuyla ilgili anketleri ve hatırlatmaları gizler Bilet rafını gizle + + \"Kullanılamayan videolar gizlendi\" banner\'ını gizle + Oynatma listelerindeki \"Kullanılamayan videolar gizlendi\" banner\'ını gizler Yüklenme zamanını gizle "Sınırlamalar: @@ -1209,6 +1212,8 @@ Sınırlamalar: 'Eski video eylem çubuğunu geri yükle' kapatılırsa, 'Daha f Uygulama başlangıcında görünen YouTube Premium özelliği \'İzlemeye devam edin\' mini oynatıcısını devre dışı bırakır Mini oynatıcı Küçültülmüş video oynatıcısının görünümünü ve davranışını özelleştirin + Ek mini oynatıcılar + YouTube tüm ek mini oynatıcıları kaldırdı. Daha fazla mini oynatıcı tiplerini geri getirmek için YouTube 21.28.208 veya daha eskisini yamalayın Mini oynatıcı tipi Başlığı ve kanalı gizle Devre dışı @@ -1250,6 +1255,9 @@ Sınırlamalar: 'Eski video eylem çubuğunu geri yükle' kapatılırsa, 'Daha f Üst barları kapatmaması ve oynatıcının üzerinde boş alan bırakmaması için durum çubuğu düzenini tutarlı hale getirir. Uygulama çöktüğünde veya üst barlar yanlış göründüğünde bu özelliği kapatın Işıltı + Morphe Işıltı 1 + Morphe Işıltı 2 + Işıltılı açılış ekranı bu cihaz tarafından desteklenmiyor Açık tema arka plan Beyaz Material You (Beyaz) diff --git a/patches/src/main/resources/addresources/values-uk-rUA/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-uk-rUA/shared-youtube/strings.xml index 23943250af..81d0c14784 100644 --- a/patches/src/main/resources/addresources/values-uk-rUA/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-uk-rUA/shared-youtube/strings.xml @@ -21,9 +21,11 @@ Second \"item\" text" Анімація запуску Вимкнено Кольорова - Кольоровий Morphe + Morphe кольорова Монохромна - Монохромний Morphe + Morphe монохромна + + Вимк. Перезавантажте, щоб завершити налаштування diff --git a/patches/src/main/resources/addresources/values-uk-rUA/youtube/strings.xml b/patches/src/main/resources/addresources/values-uk-rUA/youtube/strings.xml index 707ea26819..7f50b1dc60 100644 --- a/patches/src/main/resources/addresources/values-uk-rUA/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-uk-rUA/youtube/strings.xml @@ -113,6 +113,9 @@ Second \"item\" text" Приховати опитування Приховує опитування та підказки щодо точності рекомендацій у стрічці Приховати панель квитків + + Приховати банер \"Недоступні відео приховано\" + Приховує банер \"Недоступні відео приховано\" у списках відтворення Приховати час завантаження "Обмеження: @@ -1248,6 +1251,9 @@ Second \"item\" text" Підтримує сталий макет рядка стану, щоб він не перекривав верхні панелі та не залишав порожнього місця над плеєром. Вимкніть це, якщо застосунок аварійно завершує роботу або верхні панелі виглядають неправильно Сяйво + Morphe сяйво 1 + Morphe сяйво 2 + Заставка \"Сяйво\" не підтримується цим пристроєм Фон світлої теми Білий Material You (білий) @@ -1270,9 +1276,9 @@ Second \"item\" text" Встановлює додатковий колір смуги прогресу відтворення відео Недійсне значення кольору смуги відтворення - Оригінальний аналог - Оригінальний торт - Оригінальний метал + Оригінальна - аналог + Оригінальна - торт + Оригінальна - метал Логотип заголовка Стандартний diff --git a/patches/src/main/resources/addresources/values-ur-rIN/reddit/strings.xml b/patches/src/main/resources/addresources/values-ur-rIN/reddit/strings.xml index d3cce42054..cf7776fdaf 100644 --- a/patches/src/main/resources/addresources/values-ur-rIN/reddit/strings.xml +++ b/patches/src/main/resources/addresources/values-ur-rIN/reddit/strings.xml @@ -15,7 +15,4 @@ Second \"item\" text" --> - - محفوظ کریں - ریسیٹ - + diff --git a/patches/src/main/resources/addresources/values-ur-rIN/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-ur-rIN/shared-youtube/strings.xml index fd007eeb78..77635ba31c 100644 --- a/patches/src/main/resources/addresources/values-ur-rIN/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ur-rIN/shared-youtube/strings.xml @@ -18,6 +18,7 @@ Second \"item\" text" --> + diff --git a/patches/src/main/resources/addresources/values-ur-rIN/shared/strings.xml b/patches/src/main/resources/addresources/values-ur-rIN/shared/strings.xml index b5e8f6e369..31cdbe4dcb 100644 --- a/patches/src/main/resources/addresources/values-ur-rIN/shared/strings.xml +++ b/patches/src/main/resources/addresources/values-ur-rIN/shared/strings.xml @@ -17,71 +17,9 @@ Second \"item\" text" --> - ترتیبات - کیا آپ یقینی طور پر آگے بڑھنا چاہتے ہیں؟ - محفوظ کریں - ریسیٹ - ریسیٹ رنگ - غلط رنگ - ری اسٹارٹ درکار ہے - ری اسٹارٹ - امپورٹ - کاپی کریں - Morphe کی ترتیبات کو ریسیٹ کریں - امپورٹ کردہ %s ترتیبات - امپورٹ ناکام: %s - ترتیبات کو تلاش کریں - \'%s\' کے متعلق کوئی نتیجہ نہیں ملا - کسی دوسرے لفظ سے کوشش کریں - حالیہ تلاش کردہ - تلاش کردہ کے ریکارڈ سے مٹائیں؟ - تلاش کردہ کا ریکارڈ ختم کریں - کیا آپ بالیقین تمام تلاش کردہ کا ریکارڈ ختم کرنا چاہتے ہیں؟ - تلاش کی معلومات - "• کسی پاتھ تک جانے کے لئے اس پر ٹچ کریں -• کسی ترتیب پر جانے کے لئے لانگ پریس کریں -• تلاش کردہ بات کو محفوظ کرنے کے لئے Enter دبائیں -• تلاش میں کیسنگ (چھوٹے، بڑے حروف) اور اوقاف و اعراب کو نظرانداز کردیا جاتا ہے -• اصولی ترتیبات فروعی غیر فعال ترتیبات کے اوپر نظر آئینگی" - تلاش کا ریکارڈ خالی ہے - تلاش کا ریکارڈ محفوظ کرنے کے لئے اپنا مطلوبہ استفسار لکھ کر Enter دبائیں - ترتیبات کی تلاش کا ریکارڈ ظاہر کریں - Morphe کی ترتیبات کا آئیکن ظاہر کریں - Morphe کی زبان - "کچھ زبانوں کے تراجم مفقود یا نامکمل ہو سکتے ہیں۔ - -نئی زبانوں کا ترجمہ کرنے یا موجودہ تراجم کو بہتر بنانے کے لئے ، morphe.software/translate پر جائیں" - ایپ کی زبان - درآمد/برآمد - اMorphe کی ترتیبات کی درآمد/برآمد کریں - فائل سے درآمد کریں - ترتیبات کی درآمدی کامیابی سے مکمل ہوگئی، اطلاق کرنے کے لئے Save دبائیں - ترتیبات کی درآمدی ناکام ہو گئی - فائل میں برآمد کریں - ترتیبات کی برآمدی مکمل ہو چکی - ترتیبات کی برآمدی ناکام ہو گئی - آپ Morphe Patches کا جدید ورژن استعمال کر رہے ہیں <i>%s</i> - آپ Morphe Patches کا ورژن <i>%1$s</i><br><br><b> استعمال کررہے ہیں اپڈیٹ دستیاب ہے: <i>%2$s</i></b><br><br> پیچ کو اپڈیٹ کرنے کے لئے، Morphe کے استعمال سے اس ایپ کو دوبارہ پیچ کریں - نوٹ - یہ ورژن ایک pre-release ہے اور آپکو کچھ غیر متوقع مسائل کا سامنا کرنا پڑ سکتا ہے - آفیشل لنک - ویبسائٹ - عطیہ - تراجم - انتسابات - تا تاریخ - اپڈیٹ دستیاب ہے - حالیہ ڈیویلپمنٹ - گزشتہ ڈیویلپمنٹ - فعال ڈیویلپمنٹ - 2022 ‑ 2025 - 2022 ‑ 2025 - 2019 ‑ 2022 - لائسنس نوٹس - ڈیبگنگ @@ -93,16 +31,7 @@ Second \"item\" text" - जारी रखें - 🚨 تجربی انتباہ 🚨 - "آپ تجرباتی ایپ ورژن استعمال کر رہے ہیں ⚠️ <b>%1$s</b> -<br/><br/> -🔧 توقع کریں کہ اس ایپ ورژن کے لیے ہم پیچوں کو درست کر رہے ہیں، تو ایپ کا رو سلوک یا غیر شناخت شدہ بگ آئیں گے۔ -<br/><br/> -اگر آپ سب سے زیادہ پریشانی سے پاک تجربہ چاہتے ہیں، تو <b>ان انسٹال کریں</b> اور ✅ <b>%2$s</b> کے تجویز کردہ ایپ ورژن کو پیچ کریں۔" - ⚠️ میں تجرباتی چاہتا ہوں - ✅ میں استحکام چاہتا ہوں diff --git a/patches/src/main/resources/addresources/values-ur-rIN/youtube/strings.xml b/patches/src/main/resources/addresources/values-ur-rIN/youtube/strings.xml index 1ec5f0786e..4ec76b1150 100644 --- a/patches/src/main/resources/addresources/values-ur-rIN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ur-rIN/youtube/strings.xml @@ -31,6 +31,7 @@ Second \"item\" text" This button usually appears in the Subscriptions feed for future live streams or unreleased videos. --> + @@ -226,8 +227,5 @@ Second \"item\" text" - محفوظ کریں - تا تاریخ - जारी रखें diff --git a/patches/src/main/resources/addresources/values-uz-rUZ/music/strings.xml b/patches/src/main/resources/addresources/values-uz-rUZ/music/strings.xml index 017422c1d5..b1a4ad80f5 100644 --- a/patches/src/main/resources/addresources/values-uz-rUZ/music/strings.xml +++ b/patches/src/main/resources/addresources/values-uz-rUZ/music/strings.xml @@ -17,8 +17,101 @@ Second \"item\" text" --> + Oxirgi marta ijro etilgan Obunalar + Videolar + Boshqa natijalar + Yuklangan qo\'shiqlar + Yuklangan albomlar + Yuklangan ijrochilar + Tanaffus va davom ettirishdan foydalanish uchun mezbonning Jam patchini yangilang + Jam navbatini baham ko\"rish + Yaqindagi Jam-ga mezbonlik qiling yoki qo\'shiling va Jam qatlami orqali uning navbatini tahrirlang + Jam navbatini baham ko\'rishni yoqish + YouTube Music qayta ishga tushirilgandan so\'ng Jam integratsiyasini yoqadi + Ishga tushirish uchun Jam Layer-ni yuklab oling + Brauzeringizda Jam Layer-ning oxirgi versiyasini oching + Jam boshqaruvi + Yaqin atrofdagi Jam-ni boshqaring yoki unga qo\'shiling va sessiyani boshqaring + Jam Layer paketi + Yaqin atrofdagi navbatni baham ko\'rish uchun foydalaniladigan o\'rnatilgan hamroh ilovani tanlang + O\'rnatilgan hamroh paketidan foydalaning. Juftlash uchun sizning roziligingiz va mahalliy imkoniyat tokeni talab qilinadi + Wi-Fi yoqilsinmi? + Wi-Fi-ni yoqing, so\'ngra Jam-ni qaytadan boshlashga yoki unga qo\'shilishga harakat qiling + Wi-Fi sozlamalarini ochish + Bekor qilish + Paketdan foydalanish + Android paketining to\'g\'ri nomini kiriting + Paket saqlandi. Ulanish uchun Jam Layer-ni sozlang + Jam uzildi + Avval Jam Layer-ni yoqing + Sozlamalarda Jam navbati bilan bo\"lishishni yoqing, so\"ng YouTube Music​ni qayta ishga tushiring + Avval tanlangan Jam Layer paketini o\"rnating + Xostda ko\'rsatilgan kodni kiriting. Wi-Fi Aware mahalliy tarmoqlar turlicha bo\'lsa ham yaqin atrofdagi qurilmalarni ulash imkonini beradi + Kod orqali qo\"shilish + Qo\"shilish + QR-kodni skanerlash + Sakkiz belgili kodni kiriting yoki taklifnomani joylang + Jam-ga qo\"shiling + Kodni nusxalash + Jam kodi + QR-kodni ko\"rsatish + Tayyor + Jam sessiyangizga taklif qilish + Ushbu navbatga qo\'shilish va uni tahrirlash uchun skaner qiling. Jam sessiyasini yakunlash taklifnomani bekor qiladi + Havolani nusxalash + Jam taklifi + Jam qayta ulanmoqda; birozdan keyin qayta urinib ko\'ring + Jamga qo\'shildi + Jam mavjud emas; trek qo\'shilmadi + Sizning Jam sessiyangiz + Birgalikda tinglash + Jamga qo\'shilmoqda + Birgalikda tinglash + Jam yangilanmoqda… + Wi-Fi orqali xost qidirilmoqda… + Odamlar kutilmoqda + %s kishi ulandi + %s kishi ulandi + Ushbu kodni yaqindagi qurilmaga kiriting. Wi-Fi Aware qo\'llab-quvvatlanganda alohida LANlar orqali ishlaydi. Amal qilish muddati %s daqiqa + %s ga o\'tish kerakmi? + Tahrirlash ochiq + Tahrirlash bloklangan + Wi-Fi Aware orqali ulandi + Wi-Fi orqali ulandi + Bluetooth orqali ulandi + Jam paytida qo\'llab-quvvatlanmaydi + Birlashish bekor qilindi + Mezbon bilan qayta bog\'lanilmoqda… + Navbatni ulashing. Hammaga qo\'shiqlar qo\'shish imkonini bering + Odamlarni taklif qilish + Mehmonlar navbatni tahrirlashi mumkin + Jamni tugatish + Musiqani mezbon ijro etadi. YouTube Music navbatiga to\'g\'ridan-to\'g\'ri qo\'shiqlar qo\'shing, ko\'chiring yoki olib tashlang + Ikkala qurilmani yaqin holda saqlang. Mezbonni qidirayotganimizda bekor qilishingiz mumkin + Birlashishni bekor qilish + Jamdan chiqish + Jamni boshlash + Jam qatlamini sozlash + Boshqarish + Tafsilotlar + Ochish + Qo\"shiq variantlari yuklanmoqda… + Qo\"shiq variantlari mavjud emas; qaytadan urinib ko\"ring + Qo\"shiq variantlarini ochib bo\"lmadi + Jam Layer-ni yuklab olish uchun brauzer mavjud emas + Host navbati kutilmoqda + Avval YouTube Music-da qo\"shiqni oching + Host-da ijro etish + Jam-dan chiqish va lokal tarzda ijro etish + Jamda keyingisini ijro etish + Jam navbatiga qo\'shish + Ushbu Jam trekini ijro etish + Qayerda ijro etishni tanlang + Mezbonni bu yerga ko\'chirish + Qo\'shiqni boshqatirish uchun tayyor emas + Navbat o\'zgardi; imo-ishorani qaytadan urinib ko\'ring Sarlavha logotipi @@ -29,9 +122,51 @@ Second \"item\" text" Qo\'shiqlarning faqat audio versiyasini musiqiy video o\'rniga ijro eting. \'Video oqimlarini soxtalashtirish\' talab qiladi. + Audio/video o\'zgartirgichni yoqish + Audio trek versiyasi va rasmiy musiqa videosini almashtirishga harakat qilib ko\'ring Uchinchi tomon yuklab olish ilovalari yordamida musiqa videolari, podkastlar yoki qo‘shiqlarni yuklab oling + Yuklab olish menyusi uchun tashqi yuklovchidan foydalanish + Menyulardagi Yuklab olish opsiyasi o\'rniga tashqi yuklovchingiz ochiladi + Yuklab olishlar + "Yuklab olishlar yo\"q +Yuklab olingan treklar shu yerda ko\"rinadi" + %1$s • %2$s • %3$s MB + Yuklab olishlar orasidan qidirish + Saralash + Ijrochi bo\"yicha saralash + Sarlavha bo\"yicha saralash + Hajmi bo\"yicha saralash + Barcha yuklab olishlarni o\"chirish + Barcha yuklab olishlarni o\"chirmoqchimisiz? + Barcha yuklab olingan treklar ushbu qurilmadan o\'chiriladi + Yuklamalar o\'chirildi + Hech narsa topilmadi + %s uchun harakatlar + Ijro etish + Ijro etish yoki to\'xtatish + Oldingi + Keyingi + Tanaffus + Yopish + O\'chirish + Yuklamani o\'chirish + Yuklamani o\'chirasizmi? + %s ushbu qurilmadan o\'chiriladi + Yuklama o\'chirildi + Yuklamani o\'chirib bo\'lmadi + Yuklamani ijro etib bo\'lmadi + Audio yuklamalar + Oflayn ijro + Yuklab olish uchun trekni aniqlab bo\'lmadi + Yuklab olish allaqachon davom etmoqda + Allaqachon yuklab olingan + Yuklab olish boshlandi + Yuklab olish boshlanmoqda… + Yuklab olinmoqda %s%% + Yuklab olish yakunlandi + Yuklab olish muvaffaqiyatsiz tugadi Biz haqimizda @@ -41,6 +176,9 @@ Second \"item\" text" Player Matnlar Qo‘shimcha + YouTube Music tili + Tizim tili + Android 13 yoki undan keyingi versiya talab qilinadi \'Premiumga ulanish\' yozuvini yashirish Musiqa Premium aksiyalarini yashirish @@ -48,6 +186,7 @@ Second \"item\" text" Audio / Video ni yashirish tugmasini ko‘rsatish/yashirish Yuklab olish tugmasini yashirish + Podkast shousida Yuklab olish tugmasini yashiradi Kashf qilish javoni ni yashirish Filtr panelini yashirish Tarmoq javonlarini yashirish @@ -105,7 +244,26 @@ Second \"item\" text" Krosfeyd ushbu seans davomida pauza qilindi Krosfeyd tiklandi Krosfeyd yoqilganda video rejimidan foydalanib bo‘lmaydi + SABR ijrosi bilan Crossfade o\"chirilgan + Translyatsiya paytida Crossfade o\"chirilgan Qanday ishlaydi + "Krosfeyd bir qo‘shiqning oxirini keyingi qo‘shiqning boshlanishi bilan ustma-ust qo‘yadi, bunda sizning davomiyligingiz va egri chizig‘ingizdan foydalaniladi + +U avtomatik o‘tishda va qo‘lda o‘tkazishda ishlaydi, ularning har biri uchun alohida yoqish/o‘chirish tugmasi mavjud. Avtomatik o‘tishda aralashtirish qo‘shiq tugashidan bir necha soniya oldin boshlanadi" + Cheklovlar + "Krossfeyd yoqilganda musiqa videolari faqat audio ko'rinishida ijro etiladi. Krossfeydni to'xtatish va videoni tomosha qilish uchun \"shuffle\" tugmasini uzoq bosing + +Ijro boshlangan musiqa videosi keyingi qo'shiqkacha video rejimida qoladi + +Davomiyeti feydlash vaqtidan qisqa bo'lgan qo'shiqlar krossfeyd qilinmaydi + +Blok ekran yoki bildirishnoma o'tish jarayonida qisqa muddatga eski holatni ko'rsatishi mumkin + +Musiqa videosini tomosha qilish uchun krossfeydni to'xtatish ba'zi videolarni qora ekranda qoldirishi mumkin. Qo'shiqni tanlashdan oldin krossfeydni to'xtatish bunga yo'l qo'ymaydi" + Qo\'llab-quvvatlanmaydi + "Audioni translyatsiya qilish yoki ko'rish (Chromecast, ekranni yoki audioni ko'rish) + +SABR ijrosi (Video oqimlarini taqlid qilish o'chirilgan)" Mualliflar Krosfeyd funksiyasi VazerOG tomonidan yaratilgan @@ -113,16 +271,28 @@ Second \"item\" text" Tasodifiy rejimini eslab qolish + Ijro tezligi + Barcha treklar uchun foydalaniladigan tezlik. 100% da ilova tezligi, masalan, podkast ijro tezligi ishlatiladi + Tezlik bilan ohangni o\"zgartirish \"Yoqtirmadi\" tugmasi yo‘naltirilishini o‘chirish Yoqmaslik tugmasi bosilganda keyingi trekga o\'tishning oldi olinadi + Pleylist trekini bosish harakati + Pleylistni ijro etish + Faqat tanlangan trekni ijro etish + Tanlangan trekni navbatga qo\'shish + Navbatga qo\'shildi Chromecast tugmasini yashirish Ko‘rishlar tarixi tugmasini yashirish Bildirishnoma tugmasini yashirish Qidiruv tugmasini yashirish + Ovozli qidiruv tugmasini yashirish + Ovoz qidiruv tugmasini yashirish + Yangi tugmasini yashirish + Kutubxona vkladkasidagi Yangi tugmasini yashiradi Amallar tugmalari Pleyer ostida qaysi amal tugmalari ko‘rinishini tanlang @@ -191,6 +361,8 @@ Second \"item\" text" Qidiruv Obunalar + Portret orientatsiyasini majburlash + Qurilma aylantirilganda ilovani portret yo\"nalishida saqlaydi Miniplayerning rangini o\'zgartirish Miniplayerning rangini to\'liq ekran ijrochiga moslashtiradi @@ -273,6 +445,9 @@ Second \"item\" text" Maxsusregex tozalash Ro\'yxatdan o\'chirish uchun qo\'shiq nomlari, ijrochilar yoki albomlardan qo\'shimcha matnni olib tashlash uchun moslashtirilgan muntazam ifodani kiriting. Misol: (?i)\\s*\\((clean|explicit)\\) Ijroni titrdan ajratish + \" - \" ajratgichidan foydalanib, video sarlavhasidan ijrochi va trek nomini ajratib olish. Bu kanal nomi ijrochi sifatida ishlatilgan qayta yuklangan kontentlar uchun foydali + Yetishmayotgan albumni to\'ldirish + Musiqada albom ko\'rsatilmagan bo\'lsa, scrobble to\'liq bo\'lishi uchun uni Last.fm dan qidirib toping "\"Scrobbling\" sizning YouTube Music foydalanish ma'lumotlaringizni avtomatik ravishda sizning Last.fm yoki ListenBrainz profilingiz bilan sinxronlashtiradi. @@ -280,17 +455,236 @@ Foydalanish ma'lumotlari fon tartibda ham sinxronlashini ta'minlash uchun, YouTu Uchinchi tomon matnlaridan foydalanish Uchinchi tomon xizmatidan matnlar ko‘rsatiladi, agar topilmasa, o‘rnatilgan matnlarga qaytadi + Matnlarni ko\'rayotganda ekranni yoqiq holda saqlash + Matnlar paneli ochiq bo\'lganda ekranning o\'chishini oldini oladi + Matnlar xizmati + Ko\'rinishi + Sinxronizatsiya + Panel + AI endpoint + Barcha provayderlar o\'chirildi - ichki va joylashtirilgan matnlar + Provayderlar tartibini tiklash? + Provayderlarni standart ustuvorlik tartibiga qaytarish + Saqlangan matnlarni tozalash + Ushbu qurilmada saqlangan matnlarni o\'chirish + %1$s MB tozalandi Tokenni olish + Apple Music Media-user-token + "music.apple.com saytidan \"media-user-token\" cookie-sini joylashtiring + +Qadamlar: +1. music.apple.com saytiga a'zo foydalanuvchi sifatida kiring +2. DevTools → Application → Cookies → music.apple.com bo'limini oching +3. \"media-user-token\" qiymatini nusxalang" + Apple Music: media-user-token saqlandi + Apple Music: media-user-token tozalandi + Apple Music: noto\'g\'ri media-user-token - iltimos, tekshiring va qayta urunib ko\'ring + Spotify SpDc + "Spotify +dan sp_dc cookie-ni joylang. + +Qadamlar: +1. open.spotify.com saytida tizimga kiring +2. DevTools → Application → Cookies → open.spotify.com bo'limini oching +3. \"sp_dc\" qiymatini nusxalang" + Spotify: sp_dc saqlandi + Spotify: sp_dc tozalandi + Spotify: noto\'g\'ri sp_dc - iltimos, tekshiring va qayta urunib ko\'ring + Apple Music tokeni ixtiyoriy. Usiz Lyrically zaxira variant sifatida ishlatiladi + Deezer ARL + "Deezer +dan arl cookie-ni joylang. + +Qadamlar: +1. deezer.com saytida tizimga kiring +2. DevTools → Application → Cookies → deezer.com bo'limini oching +3. \"arl\" qiymatini nusxalang" + Deezer: arl saqlandi + Deezer: arl tozalandi + Deezer: noto\'g\'ri arl - iltimos, tekshiring va qaytadan urunib ko\'ring + Musixmatch Tokeni + O\'z hisobingizdan foydalanish uchun token qo\'shing. U Musixmatch Android ilovasidan olingan bo\'lishi kerak: musixmatch.com saytidan nusxalangan tokenlar eskirgan ish stoliga mo\'ljallangan mijozga tegishli va API ularni rad etadi + Musixmatch tokensiz ham ishlaydi, shuning uchun ushbu maydon bo\'sh qolishi mumkin + foydalanuvchi tokeni + Musixmatch: foydalanuvchi tokeni saqlandi + Musixmatch: foydalanuvchi tokeni tozalandi + Musixmatch: noto\'g\'ri foydalanuvchi tokeni - iltimos, tekshiring va qaytadan urunib ko\'ring + YouTube Cookie-lari + "YouTube dan subtitrlar va vaqt matnlarini so¢rov qilish uchun YouTube cookies ma lumotlarini joylang. + +Qadamlar: +1. Brauzerda youtube.com saytini oching va tizimga kiring +2. DevTools (F12) → Application → Cookies → youtube.com bo¢limini oching +3. To¢liq cookie satrini nusxalang (nuqtali vergul bilan ajratilgan kalit=qiymat juftliklari) + +Talab qilinadigan cookies: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPISID, LOGIN_INFO" + YouTube: cookies saqlandi + YouTube: cookies tozalandi + YouTube: noto\'g\'ri kukilar - iltimos, tekshiring va qaytadan urunib ko\'ring + Satrni bosib turing + Qo\'shiqning o\'sha lahzasiga o\'tish uchun satrni bosing, barmog\'ingiz ostidagi so\'zni yoki bosish so\'z ustida bo\'lmaganda butun satrni nusxalash uchun uni uzoq bosing. O\'chirish har ikkala imo-ishorani olib tashlaydi + Bo\'lim teglarini ko\'rsatish + Matnning har bir yangi bo\'limini kichik teglar bilan belgilang, masalan, Kuplet yoki Naqorat Nusxalash tugmasini ko‘rsatish + Asl matnni oddiy matn sifatida nusxalash uchun bosing; xom matn manba faylini Yuklab olishlar papkasiga saqlash uchun uzoq bosib turing Tarjima tugmasini ko‘rsatish + Tarjimani yoqish/o\'chirish uchun bosing; faqat tarjima uchun uzoq bosib turing. Provayder tarjimalari, Google Translate yoki OpenAI-ga mos API dan foydalaniladi + Romanizatsiya tugmasini ko¢rsatish + Romanizatsiyani yoqish/o\'chirish uchun bosing; faqat romanizatsiya uchun uzoq bosib turing. Provayder romanizatsiyasi, Google Translate yoki OpenAI-ga mos API dan foydalaniladi + Mahalliy romanizatsiya va tekislash + Provayder, sizning modelingiz va Google Translate ishlamagan taqdirda, qurilmada romanizatsiya qiling. O\'qilishi vaqtga moslangan matnning har bir so\'zidan keyin keladi; so\'zlar sinxronizatsiyasi yoqilgan bo\'lishi kerak. Xitoy, kanton, yapon, koreys, grek, rus, gruzin, arman tillari; taxminan 120 MB + %1$s o\'qilmoqda: %2$d%% + O\'qilishlar tayyor (%1$d ta yozuv) + Yuklab olish muvaffaqiyatsiz tugadi - qayta urinish uchun buni o\'chirib, yana yoqing + Yangilash tugmasini ko¢rsatish + Keyingi matn nomzodiga o\'tish uchun bosing; qo\'shiq nomi va ijrochi bo\'yicha matnni qidirish uchun uzoq bosib turing + Matnlarni qidirish + Qo\"shiq nomi + Ijrochi + Qo\"shiq nomi va ijrochini kiriting + Yoqilgan provayderlarning hech biri boshqa qo\"shiqlarni qidira olmaydi + Saqlandi + Sinxronizatsiya qilingan so¢zlarni yoki bo¢g­inlarni belgilash + Mavjud bo\'lganda, so\'zma-so\'z matnlarga ustunlik bering. Matnlar so\'z vaqti ma\'lumotlarini o\'z ichiga olgan bo\'lsa, musiqaga mos ravishda har bir so\'z yoki bo\'g\'inni belgilaydi + Ijro etilgan matnlarni yashirish + Hozirda ijro etilayotgan qatordan yuqoridagi matn qatorlarini yashirish + Ijro etilmagan matnlarni yashirish + Hozirda ijro etilayotgan qatordan pastdagi matn qatorlarini yashirish + Blokirovka ekranida MediaSession matnlarini ko\'rsatish + Bloklash ekrani, Android Auto va Bluetooth-da joriy satrni ko\'rsatadi. Android 11 yoki undan yangiroq versiya talab qilinadi + Miniplayer matnlarini ko\'rsatish + Miniplayerda joriy satrni ko\'rsatadi, trek subtitle qismiga ko\'chirilgan + Qo\"shiq nomi/ijrochi formati + Miniplayer va MediaSession-da matnlar bilan ko\'rsatiladigan trek formati. {t} - bu nomi, {a} - ijrochi va {v} - versiya, masalan, Acoustic, agar matnlar manbai uni taqdim etsa. Ikki nuqta orasidagi matn faqat ichidagi barcha o\'zgaruvchilar qiymatga ega bo\'lgandagina ko\'rsatiladi; oddiy ikki nuqta uchun \\: yozing. Har bir qator navbat bilan ko\'rsatiladi va matn o\'zgarganda keyingisiga o\'tadi + Matn qatori formati + Format {lrc} ni o\'z ichiga olishi kerak + Artist qatori shabloni va animatsiya vaqti + Minimal ekran vaqti (ms) + Maksimal ekran vaqti (ms) + Faqat ko\'p qatorli animatsiyalar uchun. Har bir kadrni kamida minimal vaqt davomida saqlang. Maksimal vaqtda, hatto matn o\'zgarmasa ham, artist qatorini oldinga suring. + Minimal qiymat 0 yoki undan katta bo\'lishi kerak. Maksimal qiymat 0 dan katta va kamida minimal qiymatga teng bo\'lishi kerak. + Satr uzunligi chegaralari + Lirika va ijrochi satri chegaralari hamda to\'liq kenglikdagi belgilarning vazni + "Lirika satri chegarasi +Uzunroq lirikani muvozanatli ikkita qismga bo'lish. 0 bo'lishni o'chiradi." + "Ijrochi satri chegarasi +To'ldirilgan trek formatini siklli freymlarga bo'lish. 0 bo'lishni o'chiradi." + "To'liq kenglikdagi belgilar vazni +Ikkala chegaraga ham ta'sir qiladi. 1.75 da to'rtta to'liq kenglikdagi belgi 7 ga teng hisoblanadi." + 0 dan 10000 gacha bo\'lgan butun sonli chegaralarni va 0 dan katta belgi vaznini kiriting + Instrumental tanaffus + Instrumental tanafas paytida Miniplayer va MediaSession-da ko\'rsatiladi. Har bir qator bitta animatsiya kadridir. {t} - sarlavha, {a} - ijrochi va {lrc} - oxirgi matn qatori + Miniplayer va MediaSession vaqt offseti + Miniplayer va MediaSession-da keyingi qator ko\'rsatiladigan vaqtni suradi. Agar qatorlar kech ko\'rinsa, masalan, blokirovka ekranida, manfiy qiymatdan foydalaning + Filtrlar + Metadata regex + Matnlarni qidirishdan oldin va ikkita sarlavha mos kelishini aniqlashda sarlavhalar, ijrochilar va albomlarga qo\'llaniladigan muntazam ifoda. Ikkalasini ham o\'chirish uchun bo\'sh qoldiring. Standart sozlama \"Official Video\", \"Remaster\", \"【插曲】\" va \"- Topic\" kabi keng tarqalgan ortiqcha ma\'lumotlarni olib tashlaydi. + Matnlar filtri regex + Faqat asl matnlarga qo\'llaniladigan muntazam ifoda (tarjimalar yoki romanizatsiyaga emas). O\'chirish uchun bo\'sh qoldiring + Ma\'lumot qatori belgilari + Agar matnlar noto\'g\'ri tarzda pastga filtrlangan bo\'lsa, buni sozlang yoki tozalang. Vergul bilan ajratilgan belgilar: belgi va undan keyin ikki nuqta bo\'lgan qatorlar pastda kredit ma\'lumotlari sifatida ko\'rsatiladi. O\'chirish uchun bo\'sh qoldiring + Birinchi harfni katta qilish + Matnning har bir qatorining birinchi harfini katta qiladi + Oxirgi tinish belgilarini olib tashlash + Lyrics qatorlari oxiridagi nuqta, vergul, ikki nuqta va nuqta-vergullarni olib tashlaydi. So\'roq belgilari, undov belgilari, ko\'p nuqtalar, qo\'shtirnoqlar va qavslar saqlab qolinadi Matn hajmi Lirikalar matnining hajmi + Global vaqt siljishi + Sinxronizatsiya qilingan matnlar ijroga mos keladi. Agar ular siljisa, vaqt offsetini sozlang. Sinxronizatsiya qilingan matnlarni oldinga yoki orqaga suring. Agar matnlar qo\'shiqdan oldin ketsa, musbat qiymatdan foydalaning + Matn siljishi uchun SponsorBlock segmentlaridan foydalanish + SponsorBlock tashlab ketadigan segmentlarsiz moslashtirish va matnlarni chiqarilgan audyoga surish. O\"chirilgan holatda SponsorBlock butunlay e\'tiborsiz qoldiriladi + Ushbu trekda yuborish uchun video ID mavjud emas Manba: %s + Ushbu LRCni saqlash uchun nusxalash tugmasini uzoq bosing; agar siz vaqtlarni aniqlashtirib, ularni hamjamiyat provayderlari bilan baham ko\'rsangiz, minnatdor bo\'lar edik + Google tomonidan tarjima qilingan + Google tomonidan romanizatsiya qilingan + %s tomonidan tarjima qilingan + %s tomonidan romanizatsiya qilingan + %s tomonidan romanizatsiya qilingan + Tarjima qilindi + Tarjima tili + Romanizatsiya tili Tarjima qilinmoqda… Lirikalarni tarjima qilib bo‘lmadi + Romanizatsiya qilindi + Avtomatik + Romanizatsiya qilinmoqda… + Matnni romanizatsiya qilib bo\'lmadi + Faqat tarjima qilingan + Faqat romanizatsiya qilingan + Dual rejim Nusxalash quvuriga nusxalandi + Yangilanmoqda + Yangilandi + QuickLRC API kaliti + QuickLRC joriy trek uchun matnlarni transkripsiya qiladi yoki vaqtini moslaydi. Bu yuqoridagi OpenAI-mos keluvchi endpointga bog\'liq bo\'lmagan alohida servisdir + QuickLRC kaliti saqlandi. Matnlarni qidirish dialogidagi moslash tugmasidan yoki trekda matnlar bo\'lmaganda panel taklifidan foydalaning + QuickLRC API kaliti saqlandi + QuickLRC ushbu kalitni qabul qilmadi + QuickLRC API kaliti tozalandi + QuickLRC kreditlar evaziga matni bo\'lmagan qo\'shiqlarning vaqtini belgilaydi yoki ularni trekdan transkripsiya qiladi. Kalit o\'rnatilganda, matni bo\'lmagan treklar transkripsiyani taklif qiladi va qidiruv dialogida moslash tugmasi paydo bo\'ladi + QuickLRC tekisligini so\'rash + Tekislanmoqda + Tekislikni qo\'llash + QuickLRC transkripsiyasini so\'rash + Transkripsiya qilinmoqda + Transkripsiya bekor qilindi + Faqat YouTube dan ijro etilgan treklar transkripsiya qilinishi mumkin + Moslash uchun matnlar yo\'q - buning o\'rniga transkripsiya qilinmoqda + QuickLRC ushbu trekni qayta ishlay olmadi + Ushbu trek uchun QuickLRC kreditlari tugadi + "Matnlar sarlavha va ijrochi bo'yicha moslashtiriladi, g'ayritabiiy metadata hech narsa topmasligi yoki noto'g'ri versiyani topishi mumkin. Versiyalarni almashtirish uchun yangilashdan foydalaning. * bilan belgilangan provayderlar token talab qiladi. Barcha yoqilgan provayderlar har doim sizning tartibingizda so'ralmaydi, balki ularning tezligiga qarab javob berishadi.\n\nMatnlar Morphe bilan bog'lanmagan, homiylik qilinmagan yoki tasdiqlanmagan uchinchi tomon xizmatlaridan keladi. Ularning har biri boricha ishlatiladi: matnlar, ularning mavjudligi va litsenziyalari ushbu xizmatlar va ularning litsenziyantlarida qoladi va ulardan foydalanish ularning xizmat ko'rsatish shartlariga bog'liq bo'lishi mumkin. Yoqilgan xizmatga faqat siz so'ragan qidiruv — sarlavha, ijrochi va ushbu xizmatga kerak bo'lgan joyda video ID yuboriladi. Ixtiyoriy AI nuqtasi va QuickLRC transkripsiyasi faqat siz ularni sozlaganingizdan keyin ishga tushadi va matnlar matnini yoki trekning video URL manzilini qabul qiladi." + Faqat foydalanadigan provayderlaringizni yoqing yoki standart holatda qoldiring - doimiy so\'rovlar kichik hamjamiyat matnlari saytlarini yuklama bilan to\'ldirishi mumkin + Mualliflik qatorini yashirish + Matn ostida ko\'rsatilgan mualliflik qatorini yashirish + Tarjima va romanizatsiyani almashtirish + YOQILSAL: romanizatsiya matn ostida, tarjima esa tepada bo\'ladi; O\'CHIRILSA: romanizatsiya tepada, tarjima esa pastda qoladi + Tarjima + Romanizatsiya + Tarjima provayderi + Google (Standart, bepul) + OpenAI bilan mos (maxsus) + Romanizatsiya provayderi + Google (Standart) + Mahalliy / Oflayn lugưat + OpenAI bilan mos keluvchi (maxsus) + AI bo\'limi + Faqat provayder Naqorat yoki Kuplet kabi bo\'lim belgilarini taqdim etmaganda va sizning OpenAI- bilan mos keladigan modelingiz ularni aniqlaganda ishlatiladi + AI & bulutli nuqtalar + AI provayderlari uchun API kalitlari, modellar, nuqtalar va prompt shablonlarini sozlang. + API nuqtasining URL manzili (masalan, https://api.openai.com/v1/chat/completions) + Prompt shabloni + Pleysholderlar: {language}, {task}, {title}, {artist}, {count}, {lines} + Prompt saqlandi + So\'rov saqlandi, ammo AI endpoint hali ishlamayapti. Avval uni AI Sozlamalari bo\'limida sozlang + So\'rov qayta tiklandi + JSON haqiqiy emas. Saqlashdan oldin uni tuzating yoki maydonni tozalang + Tarjima & romanizatsiya sozlamalari saqlandi + Sozlamalar standart holatiga qaytarildi + Asosiy URL http:// yoki https:// bilan boshlanishi kerak + Aloqani tekshirish + Nuqtalarni sozlash + Barcha endpointlarni ko\'rsatish + Ulanish tekshirilmoqda… + Tarjima provayderi + Romanizatsiya provayderi + Modellarni yuklash + Modellar yuklanmoqda… + Modelni tanlang + Modellarni yuklab bo\'lmadi. API kalitini yoki ulanishni tekshiring. + Gemini API sozlamalari + OpenAI bilan mos keluvchi (maxsus endpoint) + DeepL API sozlamalari + API kaliti + Model + Asosiy URL + API kaliti (ixtiyoriy) + Tarjima prompti + Qo\'shimcha so\'rov tanasi (ixtiyoriy JSON) + Autentifikatsiya kaliti (Bepul yoki Pro) + API kalitlari, modellar, endpointlar va promptlarni sozlash 8.23.51 - Qalin ikonkalarni o\'chirish 9.13.51 - Playerning Keyingi, Qo\'shiq matni va Tegishli yorliqlarini tiklash diff --git a/patches/src/main/resources/addresources/values-uz-rUZ/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-uz-rUZ/shared-youtube/strings.xml index 4938d34d12..af7e54667f 100644 --- a/patches/src/main/resources/addresources/values-uz-rUZ/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-uz-rUZ/shared-youtube/strings.xml @@ -18,6 +18,11 @@ Second \"item\" text" --> + Ishga tushirish animatsiyasi + O\"chirilgan + Rang + Monoxrom + Sozlashni tugatish uchun qayta ishga tushirish @@ -42,12 +47,17 @@ Second \"item\" text" Material You (Uchinchi darajali) Maxsus + Yuklab olishlar + Yuklab olish rejimi Standart + Ilova ichida yuklab olish + Tashqi yuklovchi Yuklab olish paketi nomi O\'rnatilgan tashqi yuklab olish ilovangizning paket nomi Paket nomini kiriting Boshqa Ilova o\'rnatilmagan + %s o\"rnatilmagan. Iltimos, uni o\"rnating "Paket nomi bilan o'rnatilgan ilovani topolmadi: %s Paket nomi to'g'ri ekanligini va ilova o'rnatilganligini tekshiring" @@ -65,11 +75,21 @@ Paket nomi to'g'ri ekanligini va ilova o'rnatilganligini tekshiring" Morphe play qora Maxsus + "Bu ilovalar ishga tushiruvchisida ko'rsatilgan belgini o'zgartiradi. + +Ushbu sozlama quyidagi joylarda belgini o'zgartirmaydi: +• Qurilma tizim sozlamalari ilovalar ro'yxati +• Ba'zi push-bildirishnomalar +• \"Buning yordamida ochish\" ilova tanlagichi +• Android Auto belgisi + +Belgini hamma joyda o'zgartirish uchun, patchni qayta bajaring va \"Ilova belgisi\" patch variantini o'zgartiring" Bildirishnoma belgisi Ilova belgisi Morphe play Morphe bildirishnomasi + Tomosha tarixingiz saqlanmayapti.<br><br>Bu katta ehtimol bilan DNS reklama blokirovkasi yoki tarmoq proksisi tufayli yuzaga kelgan.<br><br>Buni tuzatish uchun <b>s.youtube.com</b> manzilini oq ro\"yxatga qo\"shing yoki barcha DNS blokirovkachi va proksilarni o\"chiring Yana ko‘rsatmaslik Videotranslyatsiyalarni qalbakilashtirish @@ -79,6 +99,12 @@ Paket nomi to'g'ri ekanligini va ilova o'rnatilganligini tekshiring" Agar siz YouTube Premium foydalanuvchisi bo‘lsangiz, ushbu sozlash talab qilinmasligi mumkin" Klient oqimlari topilmadi + Standart mijoz ishlamayapti + "%1$s ketma-ket bir necha marta ishlamadi, ammo %2$s ishladi. + +%2$s ni standart mijoz qilishni xohlaysizmi?" + Almashtirish + Ushbu sozlamani o\"chirish ijro bilan bog\"liq muammolarga olib kelishi mumkin Agar mijozlar topilmasa: Android VR ga kirish imkoniyatini sinab ko‘ring @@ -113,12 +139,26 @@ Kirish uchun quyida 'Davom etish' tugmasini bosing" • Google sozlamalaringizda shaxsiy ma’lumotlaringizni bekor qilganingiz yoki Google parolizni o‘zgartirganingizda yana kiringishingiz kerak" OAuth2 vaqtinchalik mavjud emas (API vaqti tugadi) Android VR ga kirish muddati tugadi, VR ga yana kirishni sinab ko‘ring + OAuth2 vaqtincha mavjud emas (status %s) OAuth2 vaqtinchalik mavjud emas VR ga kirishning iloji yo‘q, veb-sayt kimlik tasdiqlanishiga ruxsat berilmagan VR ga kirishning iloji yo‘q: %s Android VR dan chiqib ketdi Yaroqsiz VR kirish tokeni, VR ga yana kirishni sinab ko‘ring + Bildirishnoma media boshqaruvlari + Blokirovka ekrani va bildirishnomalar panelida ko\"rsatiladigan ijro boshqaruvlarini sozlang + Oldingi & Keyingi tugmalarini yashirish + Media bildirishnoma, blokirovka ekrani va quloqchin boshqaruvlarida Oldingi va Keyingi tugmalarini yashiradi + Seekbarni o\'chirish + Blokirovka ekrani va bildirishnomalar panelida seekbarni surishni cheklaydi + Oldingi & Keyingi tugmalari bilan qidirish + Media bildirishnomasi, bloklangan ekran va quloqchinlar uchun boshqaruvni videolarni o\'zgartirish o\'rniga qidirishga o\'zgartiradi + Orqaga qaytarish uzunligi + \"Oldingi\" tugmasi bilan orqaga qaytarish soniyalari + Oldinga tezlashtirish davomiyligi + \"Keyingi\" tugmasi bilan oldinga tezlashtirish soniyalari + Qidiruv davomiyligi 1 va %s soniya oralig\'ida bo\'lishi kerak. Standart qiymat ishlatilmoqda QUIC protokolini o‘chirish @@ -127,6 +167,10 @@ Kirish uchun quyida 'Davom etish' tugmasini bosing" Asl audio tilini majburlash Doimo videoning asl tovush yo‘lagini dublyaj qilingan versiyasi o‘rniga ijro etadi + Jimlikni tashlab o\'tish + Video audiosidagi jim tanaffuslarni avtomatik ravishda tashlab o\'tadi + Minimal tanaffus uzunligi + Faqat kamida shu uzunlikdagi tanaffuslar qisqartiriladi. Keyingi video boshlanganda kuchga kiradi Tarmoq proksisi Proksi orqali qo‘llab-quvvatlanadigan tarmoq so‘rovlarini yo‘naltirish @@ -161,6 +205,11 @@ Kirish uchun quyida 'Davom etish' tugmasini bosing" Xatolikni aniqlash protokoli buferi Dasturchilarga ilovalarni aniqlash uchun qo\'shimcha ma\'lumotlarni kiritadi + "Ushbu sozlamani yoqish ba'zi UI komponentlari uchun ekrandagi matnlarni o'z ichiga olgan qo'shimcha maket ma'lumotlarini logga yozadi. + +Bu maxsus filtrlarni yaratishda komponentlarni aniqlashga yordam beradi. + +Biroq, buni yoqish sizning IP manzilingiz kabi ba'zi foydalanuvchi ma'lumotlarini ham logga yozadi" Maxsus filtr Yashirish uchun maxsus filtrni sozlang @@ -192,22 +241,48 @@ Bu xususiyatdan foydalanish uchun 'Video oqimlarini soxtalashtirish' ni o'chirin "Bu funksiya hozirda ishlab chiqilmoqda va ishlamasligi mumkin PotHelper-ni yuklab olish uchun shu yerga bosing" + PotHelper endi ishlamaydi + "PotHelperning ushbu versiyasiga o'rnatilgan token endi ishlamaydi + +Ijroni tuzatish uchun \"Video oqimlarini soxtalashtirish\" funksiyasini yoqing" + Yoqish Ilova versiyasini buzish Serverni eski ilova versiyasidan foydalanayotganingizni o\'ylashga majburlaydi, bu esa eski UI xususiyatlarini qaytarish uchun + "Ilova versiyasi eski versiyaga taqlid qilinadi. + +Bu ilovaning ko'rinishi va funksiyalarini o'zgartiradi, ammo noma'lum nojo'ya ta'sirlar yuzaga kelishi mumkin. + +Keyinchalik o'chirilsa, UI xatolarining oldini olish uchun ilova ma'lumotlarini tozalash tavsiya etiladi" Ilova versiyasini buzish manzili + Ilova yangilanish tezligi Standart + %s FPS + Yangilanish tezligini o\'zgartirish + Har doim + Portret rejimidagi ijro + To\'liq ekran rejimidagi ijro + Portret va to\'liq ekran rejimidagi ijro + Shrift Standart + Qurilma tizim shrifti + Maxsus shrift fayli + Maxsus shrift fayli + TTF yoki OTF shrift faylini tanlang + Tanlangan fayl TTF yoki OTF shrifti emas + Maxsus shrift yuklanmadi va o\"chirildi Dislikes vaqtinchalik mavjud emas (API vaqtini oshirib yubordi) + Yomon baholashlar mavjud emas (status %s) Dislikes mavjud emas (klient API chegarasi) Dislikes mavjud emas (%s) Egasi tomonidan yashirilgan + Ushbu videoga yoqtirmaslik, ovozlarning %s qismi yoqtirmaganlar Return YouTube Dislike ni yoqish Dislike foiz sifatida ko‘rsatish Son o\'rniga foiz ko\'rinishida dislike ko\'rsatadi @@ -215,6 +290,9 @@ PotHelper-ni yuklab olish uchun shu yerga bosing" Yaratuvchi Like funksiyasini o\'chirgan videolar uchun taxminiy Like sonini ko\'rsatadi Ulanish xatoligi haqida bildirishnomalar ko\'rsatish Return YouTube Dislike serveri mavjud bo\'lmaganda bildirishnoma ko\'rsatadi + "Return YouTube Dislike YouTube 2021-yilda yashirgan yoqtirmasliklar sonini qaytaradi. Sonlar o'chirilishdan oldin arxivlangan ma'lumotlardan olingan va yangiroq videolar uchun foydalanuvchilarning ovozlari asosida hisoblangan, shuning uchun ular taxminiy bo'lib, har bir necha kunda yangilanadi + +Siz tomosha qilayotgan videoning ID-si Return YouTube Dislike API-ga yuboriladi, shuningdek, hisob-kitoblarni yaxshilash uchun sizning yoqtirgan va yoqtirmagan ma'lumotlaringiz anonim tarzda yuboriladi" Return YouTube Dislike API statistikasi (ushbu qurilma) API javob vaqti, o\'rtacha @@ -224,8 +302,12 @@ PotHelper-ni yuklab olish uchun shu yerga bosing" Dislikes vaqtinchalik mavjud emas - Klient API tezligi chegarasi kuchga kirdi API ovoz olish, chaqiruvlar soni Tarmoq chaqiruvlari amalga oshirilmagan + %s ta tarmoq chaqiruvlari amalga oshirildi API ovoz olish, xatoliklarning soni Tarmoq chaqiruvlarida xatoliklar bo\'lmagan + %s ta tarmoq chaqiruvlarining vaqti tugadi API klient tezligi chegaralari Klient tezligi chegaralariga duch kelinmagan + Mijozning so\'rovlar limiti %s marta bajarildi + %s millisekund diff --git a/patches/src/main/resources/addresources/values-uz-rUZ/shared/strings.xml b/patches/src/main/resources/addresources/values-uz-rUZ/shared/strings.xml index d9132238ae..4b89671e46 100644 --- a/patches/src/main/resources/addresources/values-uz-rUZ/shared/strings.xml +++ b/patches/src/main/resources/addresources/values-uz-rUZ/shared/strings.xml @@ -24,18 +24,22 @@ Second \"item\" text" Rangni taqimlash Noto\'g\'ri rang Qayta ishga tushirish talab qilinadi + Ushbu o\'zgarish kuchga kirishi uchun ilovani qayta ishga tushiring Qayta ishga tushirish Import Nusxa ko\'chirish Morphe sozlamalari standart holatga qaytarildi + %s ta sozlama import qilindi Import xatosi: %s + Yuqoriga qaytish Sozlamalarni qidirish Biz haqimizda \'%s\' uchun natijalar topilmadi Boshqa kalit so\'zni sinab ko\'ring Yaqinda qidirilganlar Qidiruv tarixidan o\'chirilsinmi? + Qidiruv tarixidan o\'chirish Qidiruv tarixini tozalash Barcha qidiruv tarixini tozalashga ishonasizmi? Qidiruv maslahatlari @@ -47,8 +51,14 @@ Second \"item\" text" Qidiruv tarixi bo\'sh Qidiruv tarixini saqlash uchun qidiruv so\'rovini yozing va Enter tugmasini bosing Sozlamalar qidiruv tarixini ko\'rsatish + Morphe sozlamalari nomi Standart + Qo\'shimchalar + Tuzatishlar + Morfing vaqti Maxsus + Nom kiriting + Nom bo\'sh bo\'lishi mumkin emas Morphe sozlamalari belgilarini ko\'rsatish Nusxalandi: %s Morphe tili @@ -91,6 +101,9 @@ Yangi tillarni tarjima qilish yoki mavjud tarjimalarni yaxshilash uchun morphe.s Debug log stack trace Xatolarni aniqlashga yordam berish uchun debug loglariga stack trace-larni qo\'shadi Morphe xatosi yuz berganda toast-xabarnomani ko\'rsatish + "Xatolik toastlarini o'chirish barcha Morphe xatolik bildirishnomalarini yashiradi. + +Sizga hech qanday kutilmagan voqealar haqida xabar berilmaydi" Debug loglarini eksport qilish Morphe debug loglarini eksport qilish Debug loglari muvaffaqiyatli eksport qilindi @@ -166,9 +179,31 @@ Xatti-harakat qidiruvda avvalroq yo'qolgan edi, shuning uchun bir nechta bayroq MicroG sozlamalarini ochish + MicroG o\'rnatilmagan. Uni o\'rnating Harakat talab etiladi + "Sizning MicroG v%1s versiyangiz eskirgan va muammolarni keltirib chiqarishi mumkin. + +Iltimos, MicroGni eng so\"nggi v%2s versiyasiga yangilang" + Yangilash + E\'tiborsiz qoldirish + "O\"rnatilgan MicroG Morphe tomonidan tavsiya etilmaydi: + +%1s + +Uni o\"chirish uchun bosing, so\"ngra ushbu ilovani qayta oching va MicroG RE ni o\"rnating" + O\'chirish + "MicroGda fonda ishlash uchun ruxsat yo\"q. + +Telefoningiz uchun \"Don't kill my app\" qo\"llanmasiga amal qiling va ko\"rsatmalarni MicroG o\"rnatilishiga tatbiq eting. + +Bu ilovaning ishlashi uchun zarur" Veb-saytni ochish + "Muammolarning oldini olish uchun MicroG batareya optimallashtiruvi o\"chirilgan bo\"lishi kerak. + +MicroG uchun batareya optimallashtiruvini o\"chirish batareya sarfiga salbiy ta\"sir ko\"rsatmaydi. + +Davom etish tugmasini bosing va optimallashtirish o\"zgarishlariga ruxsat bering" Davom etish MicroG batareya optimallashtiruvi dialogini ko\'rsatish "Buni o'chirish MicroG batareya optimallashtiruvi haqidagi bildirishnomani yashiradi. @@ -195,4 +230,6 @@ Agar siz eng muammosiz tajribani xohlasangiz, unda buni <b>o'chirib tashla ⚠️ Menga eksperimental versiya kerak ✅ Menga barqaror versiya kerak + Xatolik logi foydalanuvchi hujjatlariga saqlandi: %s + Xatolik logi saqlandi: %s diff --git a/patches/src/main/resources/addresources/values-uz-rUZ/youtube/strings.xml b/patches/src/main/resources/addresources/values-uz-rUZ/youtube/strings.xml index c1a94bcdbf..ea1690cddd 100644 --- a/patches/src/main/resources/addresources/values-uz-rUZ/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-uz-rUZ/youtube/strings.xml @@ -32,11 +32,26 @@ Second \"item\" text" Fondan oʻynash cheklovlarini olib tashlash Shorts\'da fonda oʻynashni oʻchirish + Qurilma qulflanganda avtomatik to\'xtatish + O\'chirilgan + Qulflanganda har doim to\'xtatish + Simsiz audio ulangan bo\'lmasa to\'xtatish Aylantirish tezligi cheklovini o\'chirish Tartib sozlamalarini oʻzgartirishni oʻchirish Ilovaning tartibini yangilash oldini oladi + "Ilova maketi ilk bor o'rnatilgandagi maketga qaytariladi. + +Ba'zi server tomonidagi maketlar qaytarilmasligi mumkin. + +O'zgarishlar quyidagilarni o'z ichiga oladi: +• Pleyerning yoyiluvchi menyusidagi komponentlar (yoki tegishli sozlamalar) ishlamasligi mumkin +• Raqamlar animatsiyasiz ko'rsatiladi +• Kutubxona tabi ko'rsatiladi +• Video tavsifining musiqa bo'limi ishlamasligi mumkin +• Pleyer yoyiluvchi menyusi elementlarini yashirib bo'lmaydi +• Kutubxona tabida hisobni almashtirish tugmasi ko'rinmasligi mumkin" Oziqlantirish menyusini filtrlashni yoqish Oziqlantirish menyusini filtrlash @@ -48,6 +63,8 @@ Second \"item\" text" Ijodkor tavsifi ostidagi albom kartalarini yashiradi Ijodkor kartalarini yashirish Qidiruv natijalari tepasida ijodkor kartasini yashiradi + Kanal tugmalarini yashirish + Qidiruv natijalaridagi kanal profillaridan Obuna bo‘lish, Kanalni ko‘rish, Hamjamiyat va YouTube Music kabi tugmalarni yashiradi Hamjamiyat e\'lonlarini yashirish Qisqa bannerlarni yashirish Siqishli bannerlarni va baʼzi komponentlar orasidagi masofani yashiradi @@ -58,6 +75,7 @@ Second \"item\" text" Oqmayotgan mikrofon tugmasini yashirish Qidiruv takliflari ekranining oʻng tomonidagi suzuvchi mikrofon tugmasini yashiradi \'Get Premium\' tugmasini yashirish + Tarix javonini yashirish Gorizontal javonlarni yashirish Yangiliklar, sizga yoqishi mumkin bo\'lgan jamoa xabarlari, tomosha qilishda davom eting, ko\'proq kanallarni o\'rganing, mavzularni o\'rganing, siz uchun, so\'nggi videolar, eng mos keladigan, ijodkorning jamoasidan xabarlar, savdo va yana kuzatish uchun videolar kabi gorizontal javonlarni yashiradi Tasvir javonini yashirish @@ -70,6 +88,8 @@ Second \"item\" text" This button usually appears in the Home feed for refreshing the feed. --> “Eng yangi videolar” tugmasini yashirish Bosh sahifa oziqlantirishning tepasida koʻrinadigan \'Eng soʻnggi videolar\' bannerini yashiradi + Jonli efirlarni yashirish + Bosh sahifa lentasi va qidiruv natijalarida ayni damda jonli efirda bo\'lgan videolarni yashiradi Miks pleyllarni yashirish Filmlar bo\'limini yashirish @@ -78,6 +98,7 @@ Second \"item\" text" “Bildirishnomalar” tugmasini yashirish + Rejalashtirilgan jonli efirlar va premyeralar uchun eslatma tugmasini yashiradi Playable’larni yashirish Oʻyin oʻynash imkoniyatini yashiradi Qidiruv atamasi tasvirchalarini yashirish @@ -87,9 +108,14 @@ Second \"item\" text" “Ko\'proq ko\'rsatish” tugmasini yashirish Qidiruv natijalari videolari ostidagi \'Koʻproq koʻrsatish\' tugmasini yashiradi Obunalar panelini yashirish + Obuna bo‘lingan kanal nomlarini yashirish + Obuna bo‘lingan kanallar panelidagi kanal nomlarini yashiradi So\'rovnomalarni yashirish Ozuqaning aniqligi haqidagi so\'rovlar va rag\'batlarni yashiradi Chipta javonini yashirish + + \"Mavjud bo\'lmagan videolar yashirilgan\" bannerini yashirish + Pleylistlardagi \"Mavjud bo\'lmagan videolar yashirilgan\" bannerini yashiradi Yuklash vaqtini yashirish "Cheklovlar: @@ -97,6 +123,8 @@ Second \"item\" text" • Avtomobil tartibi bilan ishlamaydi" Video muqovasini yashirish + Video yorliqlarini yashirish + \'Avtomatik dublyaj qilingan\' / \'Dublyaj qilingan\', \'Komissiya oladi\', \'Hype\', \'Boshqalar shuni tomosha qilishdi\', \'mahsulot(lar)\' va \'Sizga yoqishi mumkin\' kabi yorliqlarni yashiradi Ko\'rish sonini yashirish "Cheklovlar: @@ -108,6 +136,7 @@ Second \"item\" text" YouTube Doodles’larni yashirish YouTube logotipidagi mavsumiy va tadbirga asoslangan animatsiyani yashiradi + YouTube Doodles’larini ko‘rsatish maxsus sarlavhalarning ko‘rsatilishiga to‘sqinlik qilishi mumkin Kanal paneli Kanal suv belgisini yashirish Boblar va \'Ushbu videoda\' tugmasini yashirish @@ -130,18 +159,28 @@ Second \"item\" text" Saqlash va navbat harakatlarini almashtirish Obunachilar hamjamiyatiga oid qoidalarni yashirish Izohlarning chat donorlari panelini yashirish + Bosib turish gradientini yashirish + Tezlashtirish uchun bosib turgan holda barmog\'ingizni surganingizda pleyer boshqaruvi gradientini yashiradi Vaqtli reaksiya bildirishlarni yashirish Izohlar boʻlimida va jonli chatda emoji bilan munosabat bildiruvchi funksiyani yashiradi + \"Yordam va fikr-mulohazalar\" menyusini yashirish + Qidiruv natijalari kabi joylarda uch nuqtali tugma bosilganda \"Yordam va fikr-mulohazalar\" menyusini yashiradi + Handle-ni yashirish + \"Siz\" vkladkasida kanalingiz handle-ini yashiradi Video sarlavhasini yashirish Videoni toʻliq ekran rejimida koʻrsatmaslik Videoning subtitrini tozalash + Video subtitrlaridan \'#\', \'Fond yig\'ish\', \'mahsulot(lar)\' va \'Do\'kon\' kabi iboralarni olib tashlaydi + \'Gemini dan so»ra³\' tugmasini yashirish + \'Ushbu video haqida so»ra³\' panelining pastki qismidagi \'Gemini dan so»ra³\' tugmasini yashiradi Video tavsifi Videoning tavsifida paydo boʻladigan komponentlarni tanlang \'AI tomonidan yaratilgan video tavsifi\' boʻlimini yashiring Soʻrovlar boʻlimini yashiring Attributlar boʻlimini yashiring Tanlangan joylar, oʻyinlar, musiqalar va odamlar boʻlimini yashiradi + Kanal havolalari bo\"limini yashirish Boblarni yashiring Tuzatishlar boʻlimini yashiring \'Kurs davomida harakat\' boʻlimini yashiring @@ -157,6 +196,7 @@ Second \"item\" text" Bu qanday yaratilgan bo\'limini yashirish Hype ballarini yashirish Maʼlumot kartalari boʻlimini yashiring + \'Asosiy tushunchalar\' bo\'limini yashirish \'Savollar\' boʻlimini yashiring \'Ushbu videodagi qidiruv\' bo\'limini yashirish Obuna tugmasini yashirish @@ -230,6 +270,7 @@ Second \"item\" text" Uy sahifasidagi izohlar bo\'limini yashirish Jamoa yo‘riqnomasini yashirish “Qisqa video yaratish” tugmasini yashirish + Yoq지maslik tugmasini yashirish Emoji tugmasini yashirish Filtruvar panelidagi variantlarni yashirish Faqat \'Yuqori\' va \'Eng so\'nggi\' variantlari qoldiriladi @@ -237,14 +278,21 @@ Second \"item\" text" Sovg\'a tugmasini yashirish Maʼlumot tugmasini yashirish Jonli chat donorlari panelini yashiring + Jonli chatning pastga tushirish tugmasini yashirish Jonli chat maslahatlarini yashirish Chat kiritish paneli yaqinida ko\'rsatiladigan maslahat oynalarini, masalan, obunachilar chat xabarnomasini yashiradi + Menyu tugmasini yashirish + Har bir izohning 3 nuqtali menyu tugmasini yashiradi Oldindan ko\'rsatish izohini yashirish Toʻliq izohlar boʻlimini ochishdan oldin videoning ostidagi sharh old koʻrinishini yashiradi + Izoh Morphe tomonidan yashirildi… + Minimalistik izohlar tugmasini yoqish + Izohlar tugmasida ko\'rsatiladigan sirpanuvchi kartalar va ularning sahifalar selektorini yashiradi “Rahmat” tugmasini yashirish Vaqt belgisi tugmasini yashirish \'Top muxlislar\' tugmasini yashirish Tarjima tugmasini yashirish + \"Hali izohlar yo\"q\" xabarini yashirish Sanitarlangan ajratilgan havola Faqat asl matnni qoldirib, sanatlangan ajratilgan havola Kalit soʻz filtri @@ -301,7 +349,40 @@ O'rtasida katta harflar bo'lgan so'zlar aniq mos kelishi kerak (masalan, iPhone, Cheklovlar: • Shorts javonlari hamon bo'sh joy ko'rsatishi mumkin • Ba'zi AI videolari ko'rsatilishini to'xtatib bo'lmaydi" + "AiSList — bu ko'ngillilar tomonidan yuritiladigan, sun'iy intellekt tomonidan yaratilgan kontentni nashr etadigan YouTube kanallarining ochiq manbali ro'yxatidir. Kanallar foydalanuvchilar tomonidan xabar qilinadi va qo'shilishidan oldin ko'rib chiqiladi + +Ro'yxat GitHub'dan yuklab olinadi va videolar qurilmangizda filtrlanadi" + AI kanallari haqida xabar berish + Video menyusiga hisobot yozuvini qo\'shish + Video menyusiga videoning kanalini AiSList\'ga xabar qiluvchi yozuvni qo\'shing + Yuboruvchi ismi + Ommaviy AiSList reytingida hisobotlaringiz yonida ko\'rsatiladigan ism. Bu bo\'sh bo\'lganda hisobot yuborish imkoniyati mavjud emas + AI kanalini xabar qilish + "Ushbu videodan dalil sifatida foydalangan holda %s ni AiSList ga yuborishni xohlaysizmi? + +Faqat quyidagilar uchun kanallarni xabar qiling: +• Asosiy kontent sifatida AI ovozlari, vizuallari, ssenariylari yoki musiqasidan foydalanadiganlar +• AI vositalari yordamida va minimal insoniy mehnat bilan kontentni ommaviy ravishda ishlab chiqaradiganlar + +Faqat AI haqida gapiradigan yoki videolarining ba'zi qismlarida AI dan foydalanadigan kanallarni xabar qilmang. + +Hisobotlar ochiq va kanal ro'yxatga qo'shilishidan oldin ko'rib chiqiladi. O'chirish faqat GitHub orqali so'rov yuborish orqali amalga oshiriladi" + Ushbu kanal allaqachon AiSList bloklar ro\'yxatiga kiritilgan + Ushbu kanal allaqachon AiSList ogohlantirishlar ro\'yxatiga kiritilgan + Xabar qilish + Kanallarni xabar qilish uchun AI kanal filtri sozlamalarida yuboruvchi ismini belgilang + Kanal qidirilmoqda… + Ushbu videoning kanalini topib bo\'lmadi + Kanal haqida xabar yuborildi va ko\'rib chiqilmoqda + AiSList xabarni saqlay olmadi, keyinroq qayta urinib ko\'ring + Juda ko\'p xabarlar yuborildi, keyinroq qayta urinib ko\'ring + AiSList xabarni rad etdi + Xabar yuborish muvaffaqiyatsiz tugadi, ulanishni tekshiring + Kanal reklamalari oq ro\'yxati + Oq ro\'yxatga olingan kanallarning videolari reklamalar bilan ijro etiladi, shuning uchun ularning mualliflari haq oladilar. Boshqa barcha kanallarda reklamalar bloklangan holda qoladi + Kanalni reklama oq ro\'yxatiga qo\'shish + Kanalni reklama oq ro\'yxatidan olib tashlash Ijodkor do\'konini yashirish End ekrandagi do\'kon bannerini yashirish Reklamalarni yashirish @@ -314,9 +395,33 @@ Cheklovlar: YouTube Premium reklamasini yashirish + Yuklanmoqda… + Joriy kanal: + Oq ro\'yxatga qo\'shish + Olib tashlash + Oq ro\'yxatga olingan kanallar + Oq ro\'yxatda kanallar yo\'q + Kanal oq ro\'yxatga qo\'shildi + Kanal oq ro\'yxatdan olib tashlandi + Kanal ichidan qidirish + Kanal sahifalariga faqat o\'sha kanal bo\'yicha qidirish uchun ikkinchi qidiruv tugmasini qo\'shadi + Kanalni qidirish + Kanal qidiruv natijalari + Ushbu kanalda hech narsa topilmadi + Ushbu kanal bo\"yicha qidiruv amalga oshirilmadi + Videolar + Postlar + Ushbu kanalda mos keladigan postlar topilmadi + Relevantlik + Eng ko\'p ko\'rilganlar + Eng kam ko\'rilganlar + Eng yangilari + Eng eskisi + Eng qisqa Short + Eng uzuni Havola quvvatlantirildi Vaqt muhrini o\'z ichiga olgan havola quvvatlantirildi @@ -325,17 +430,54 @@ Cheklovlar: Vaqtni nusxalash tugmasini sudrish \'Video havolasini nuqul qilish\' tugmasining harakatlarini teskari tartibda o\'zgartiradi + Video sarlavhasini nusxalash + Nusxalash uchun videoning sarlavhasini bosib turing + Video sarlavhasi buferga nusxalandi + Izohlarni nusxalash + Nusxalash uchun izohni bosib turing + Izoh buferga nusxalandi + Video menyusini ochish uchun bosing va ushlab turing + Lenta videosining istalgan joyini bosib ushlab tursangiz, uning menyusi ochiladi + Miniyaturada \"Keyinroq ko\'rish\" tugmasini ko\'rsatish + Lenta videolarining kichik rasmlarida \"Keyinroq ko\'rish\" tugmasi ko\'rsatiladi. Uni bosib turish \"Pleylistga saqlash\" menyusini ochadi + Post rasmlarini yuklab olish + Rasmlar yoki GIF-lar mavjud bo\'lgan postlarning \"Ulash\" tugmasi yonida yuklab olish tugmasi ko\'rsatiladi va ularning menyusiga yuklab olish bandi qo\'shiladi + Rasmlarni yuklab olish + Rasmni yuklab olish + Rasm yuklanmoqda… + Rasm %s manziliga saqlandi + Rasmni yuklab olib bo\"lib bo\"lmadi + Ushbu postda rasm topilmadi + Yoyilma menyuga \"So\'rash\" elementini qo\'shish + Lenta yoyilma menyusiga videoni umumlashtirish elementini qo\'shadi + "\"So'rash\" funksiyasi uchun dastlab \"So'rash\" mavjud bo'lgan mamlakatdagi tizimga kirgan hisob kerak. + +\"So'rash\" tugmasi bo'lgan video bir marta ochilgandan so'ng, ushbu funksiya barcha videolarda ishlaydi, hatto hozirgi mamlakatda \"So'rash\" funksiyasi mavjud bo'lmasa ham." + Videoni umumlashtirish + Ushbu video uchun \"So\'rash\" funksiyasini ochib bo\'lmadi + \"So\" hisobingiz yoki hozirgi mamlakatingiz uchun mavjud emas + Miniatyuralarda ko\"rilganlar sonini ko\"rsatish + Lenta videolarining ko\"rilganlar soni sarlavha ostida emas, balki miniatyuralarning pastki chap qismida ko\"rsatiladi. Ba\"zi videolar, masalan, javonlardagilar, ko\"rilganlar sonini ko\"rsatmasligi mumkin + Lenta kichik rasmlarida layklar sonini ko\'rsatish + Lenta miniatyuralarida layklar foizini ko\'rsatish + Izohlarni qidirish + Faqat qidirilgan barcha so\"zlarni o\"z ichiga olgan yuklangan izohlarni ko\"rsatish uchun izohlardagi qidiruv tugmasini bosing. Frazani qidirish uchun so\"zlarni qo\"tirnoq ichiga o\"ling\" + Izohlarni qidirish + Qidiriladigan so\"zlar\" Qidiruv + Tozalash + Saralash tartibini o\"zgartiring yoki izohlarni qidirish uchun ularni qaytadan oching\" + Qidiruv tozalandi. Barcha izohlarni ko\'rish uchun saralash tartibini o\'zgartiring yoki izohlarni qayta oching “Televizorga kirish”ni o\'chirish \'Connect your devices\' oynatish xabarini o\'chirish @@ -344,9 +486,25 @@ Cheklovlar: Boblarni o\'tkazib yuboradigan ikki barmoqli ikki marta bosish harakatini o\'chirish, bu bir barmoq bilan tasodifan ishga tushirilishi mumkin Uchinchi tomon ilovalari yordamida videolar, podkastlar yoki qo\'shiqlarni yuklab oling + Pleyer ustidagi yuklab olish tugmasini ko\'rsatish + Video pleyerda tashqi yuklovchini ochadigan tugmani ko\'rsatadi + Bosilganda tezkor yuklab olish + Fonda yuklab olishni taklif qiladi. Yuklovchini ochish uchun bosib turing. YTDLnis va YTindir bilan ishlaydi + Yuklab olinganlarni ko\'rilgan deb belgilash + Tashqi yuklovchi yordamida yuklab olingan videolar YouTube ko\'rish tarixingizga qo\'shiladi + Full HD (1080p) + O\"rta (192 kbps) + Tezkor yuklab olish + Video + Audio + Barcha variantlar + Shorts menyusida yuklab olishni ko\'rsatish + Shorts pleyeriga tashqi yuklovchi flyout menyu elementini qo\'shadi + Yuklab olish Ovozli tarjima O\'ynayotganda matnni ovozga aylantirish orqali video sarlavhalarini o\'qish, o\'ynatish bilan sinxronlashtirilgan + Video pleyerdagi ustma-ust tugmani ko\'rsatadi. Tarjimani yoqish/o\'chirish uchun bosing, tezkor sozlamalar uchun esa bosib turing Til Tarjima provayderi Asosiy audio balandligi @@ -390,6 +548,7 @@ Sizning API kalitingiz hech kim bilan baham ko'rilmasligi kerak. Uni kirish huqu OpenRouter.ai ni oching OpenRouter tarjimasi muvaffaqiyatsiz (%s HTTP) MyMemory kunlik limiti yetib keldi + Keyingisi %s da mavjud Siz bugungi kungi MyMemory tarjima kvotasini (5000 ta belgilar) ishlatdingiz. Kunlik limitni 50 000 gacha oshirish uchun sozlamalarda MyMemory elektron pochta manzilini kiriting yoki boshqa tarjima xizmatidan foydalaning. Siz MyMemory tarjima kvotasini (50 000 ta belgilar) ishlatdingiz. Boshqa tarjima xizmatidan foydalaning yoki ertaga urinib koʻring. MyMemory tarjimasi muvaffaqiyatsiz (holat %s) @@ -411,6 +570,9 @@ Sizning API kalitingiz hech kim bilan baham ko'rilmasligi kerak. Uni kirish huqu Takrorlanmoqda %1$s - %2$s Noto\'g\'ri vaqt diapazoni + \"Rasm rasmda\" tugmasini ko\'rsatish + \"Rasm rasmda\" menyusini ko\'rsatish + \"Rasm rasmda\" rejimiga o\'tish Oʻynatish barcha tugmasini koʻrsatish "Kanalning barcha videolaridan o'yin ro'yxatini yaratish uchun bosing, bekor qilish uchun ushlab turish @@ -441,17 +603,48 @@ Ma'lumot: Jonli efirlarda ishlamasligi mumkin" Ushbu video hali Watch Later pleyistida saqlangan Ushbu videoni Watch Later ga saqlab boʻlmadi Keyinroq ko\'rish uchun saqlash + Pleyer sozlamalari menyusida \"Keyinroq ko\'rish\"ga saqlash tugmasini ko\'rsatish + Video sozlamalari menyusidan joriy ko\'rilayotgan videoni \"Keyinroq ko\'rish\" bo\'limiga saqlash imkonini beradi Bolalar videolarida keyinroq ko\'rish uchun saqlash tugmasini ko\'rsatish + Menyu orqali bolalar uchun mo\'ljallangan videolarni \"Keyinroq ko\'rish\" bo\'limiga saqlash imkonini beradi + Shorts menyusida \"Keyinroq ko\'rish\"ga saqlash tugmasini ko\'rsatish + Menyu orqali shorts videolarni \"Keyinroq ko\'rish\" bo\'limiga saqlash imkonini beradi + \"Keyinroq ko\'rish\"ni pleylistga yo\'naltirish + Keyinroq ko\'rish Morphe navbatiga qo\'sh Ilovaning qayta ishga tushirilgandan so\'ng navbat saqlanadi Kirish qilinmagan Saqlash uchun teging, navbatga qo\'shish uchun bosib ushlab turing Navbat opsiyalari uchun bosing, \'Keyinroq ko\'rish\'ga saqlash uchun bosib turing + Navbatda keyingisini ijro etish turi Standart + Tizim navbati + Morphe navbati + Morphe navbatining mijoz tomoni Lenta ochiladigan menyusiga navbat bandini qo\'shish Qo\'shimcha ochiladigan menyu bandini qo\'shadi. Agar \"Navbatga qo\'shish\" ko\'rsatilmasa, buni yoqing + Navbat + Navbat (%s) + Barchasini tozalash + Navbatda hech narsa yo¶q + Istalgan videoda \'Navbatda keyingisini ijro etish\' funksiyasidan foydalaning + Hozir ijro etilmoqda + Navbatda keyingisini ijro etish + Navbatdagi oxirgisini ijro etish + Tartibni o¶zgartirish + Olib tashlash + Navbatga qo\'shildi + Ushbu video navbatga qo\'shilmadi + Pleylistlarni navbatga qo\'shib bo\'lmaydi + Navbat to\'la + Videoni boshlab bo\'lmadi + Yoyiluvchi menyuda reklama oq ro\'yxati + Ushbu videoning kanalida reklamalar ijro etilishiga imkon beruvchi yoyilma elementni qo\'shadi + Ushbu funksiya PotHelperdan foydalanishni yoki \"Video oqimlarini spoof qilish\"ni TV yoki Android VR tizimiga kirishga o\'zgartirishni talab qilishi mumkin + Yoyilma menyudagi tezlik oq ro\'yxati + Ushbu videoning kanalini doimo 1.0x tezlikda ijro etuvchi yoyma elementni qo\'shadi Tizimga kirmagan Navbat bo\'sh Navbat boʻsh @@ -476,6 +669,19 @@ Ma'lumot: Jonli efirlarda ishlamasligi mumkin" Navbatni pleylistga saqlash Snackbar ni yashirish + Pleylist saqlandi + %s ga saqlandi + Navbatni tozalash + Navbat tozalandi + Navbat + Navbatdagi oxirgisini ijro etish + Navbatdagi keyingisini ijro etish + YouTube navbatni ko\'rsata olmadi. + Ushbu video uchun navbat tayyor emas. Video pleyerni oching va qaytadan urinib ko\'ring. + Miksni navbatga qo\'shishmi yoki ko\'rishmi? + Ko\'rish + Navbatga qo\'shish + Miks yuklanmoqda… Jonli efir avatarka dan kanalni ochish Kanal avatarkasini bosganda jonli efirning ochilishining oldini olish @@ -483,12 +689,22 @@ Ma'lumot: Jonli efirlarda ishlamasligi mumkin" Aniq qidirish harakatini o\'chirish Progress barni tortib olganda faollashtiradigan aniq izlash rejimini o\'chirish + Jonli efir DVR funksiyasini yoqish + Hatto odatda cheklangan jonli efirlarda ham orqaga qaytarish imkonini beradi Seekbar uchun miniatyuralarni yoqish Seekbar\'ni sudrab borayotganda video miniatyurasini ko\'rsatadi Eng ko\'p qayta ko\'rilgan + "\"Jonli efir DVR funksiyasini yoqish\" mavjud bo'lmasligi mumkin + +Buni tuzatish uchun \"Video oqimlarini soxtalashtirish\" funksiyasini yoqing va \"Standart mijoz\"ni \"TV Simply\" yoki \"visionOS\"ga o'zgartiring" + Jonli efir DVR davomiyligini kengaytirish + Jonli efirlarni 7 kungacha orqaga qaytarish imkonini beradi + "\"Jonli efir DVR davomiyligini kengaytirish\" mavjud bo'lmasligi mumkin + +Buni tuzatish uchun \"Video oqimlarini soxtalashtirish\" funksiyasini yoqing va \"Standart mijoz\"ni \"TV Simply\" yoki \"visionOS\"ga o'zgartiring" Sekish harakatini yoqish Video pleerni bosib ushlab turganingizda 2 barobar tezlikda ijro qilish o\'rniga siljishni yoqish @@ -530,6 +746,12 @@ Ma'lumot: Jonli efirlarda ishlamasligi mumkin" On-ekrandagi matnning oʻlchamini sozlash (1–30) Sirt bilan surish harakatining chegarasi \'Yoqish\' va \'Obuna\' tugmalari ijodkor ularni aytganda porlamasligi + Ovozni surish sezgirligi + Yuqori qiymatlar ovoz bosqichi uchun ko\'proq surishni talab qiladi + Ovoz bosqichlari + Ovozni kuchaytirish + Maksimal ovozdan yuqoriga surish uni yanada kuchaytiradi, bu esa baland audio tovushlarini buzishi mumkin + Qurilma standarti Yorqinlik siljish sezgirligi Yorqinlik bosqichlari uchun ko\'proq siljish kerak bo\'lgan qiymatlar yuqori Tezlik siljish sezgirligi @@ -581,6 +803,10 @@ Ma'lumot: Jonli efirlarda ishlamasligi mumkin" \'Saqlash\' tugmasini yashirish + Kanal va Obuna tugmasini yashirish + Harakatlar panelining boshida kanal avatarini va Obuna tugmasini yashiradi + Obuna tugmasini yashirish + Harakatlar paneli boshidagi kanal avatari yonidagi Obuna va A\'zo bo\'lish tugmalarini yashiradi \'Soʻrov tugmasini\' yashirish Bu ba\'zi foydalanuvchi hisoblarida ishlamasligi mumkin @@ -608,7 +834,12 @@ Ma'lumot: Jonli efirlarda ishlamasligi mumkin" \'Hype tugmasini\' yashirish Ovozsiz paytda ko\'rsatish + Afzal ko\'rilgan subtitrlarning tili + Avval provayder subtitrlarini tanlaydi, so\'ngra avtomatik ravishda afzal ko\'rilgan tilga tarjima qiladi + O\'chirilgan + Qo\'shimcha avtomatik tarjima tillarini qo\'shish + Avtomatik tarjima menyusiga ko\'proq tillarni qo\'shadi Sarlavha kuki-larini belgilash "Standart YouTube xatosini tuzatadi, bunda avtomatik tarjima subtitrlari cookie'lar yetishmagani sababli mavjud bo'lmasligi mumkin @@ -624,6 +855,7 @@ Ma'lumot: Jonli efirlarda ishlamasligi mumkin" Transkriptni tuzatish \'Doʻkon tugmasini\' yashirish + Rasmdagi rasm rejimida subtitrlarni ko\'rsatish Navigatsiya tugmalari Sozlamalar ekrani @@ -646,14 +878,27 @@ Ma'lumot: Jonli efirlarda ishlamasligi mumkin" Yaratish tugmasini bildirishnoma tugmasi bilan almashtirish Agar ushbu sozlamani o\'zgartirish ta\'sir qilmasa, inkognito rejimiga o\'ting Navigatsiya tugma yorliqlarini yashirish + Yangi kontent nuqtasini yashirish Navigatsiya panelini yashirish + "Bu navigatsiya panelini, jumladan 'Siz' vkladkasini butunlay yashiradi. + +Sozlamalarga kirish uchun Umumiy → Asboblar paneli → Sozlamalarni ko'rsatish orqali Sozlamalar tugmasini yoqing" Tor navigatsiya tugmalarini yoqish Navigatsiya tugmachalari orasidagi gorizontal masofani kamaytiradi + Yorliqsiz navigatsiya tugmalarini o\'chirish + Agar YouTube navigatsiya tugmalarini yorliqlarsiz yuborsa, ularni yorliqlar bilan ko\'rsatadi + Asboblar paneli navigatsiya vkladkalarini o\"chirish\" + Agar YouTube uni Bosh sahifaning yuqori qismidagi vkladkaga ko\"chirdiki, navigatsiya panelasida Obunalar bo\"limini ko\"rsatadi\" Navigatsiya paneli animatsiyalarini yoqish Varaqlar oʻrtasida almashishda animatsiyali oʻtishlarni yaratadi Avtomatik yashiriladigan navigatsiya panelini oʻchirish Prokrul paytida navigatsiya panelini koʻrinishda ushlab turadi + Navigatsiya paneli uslubi Standart + Yarim shaffof + Shaffof + Yopiq + Ushbu uslub qurilmangiz yoki YouTube versiyasi tomonidan qo\'llab-quvvatlanmasligi mumkin va bu maket muammolariga yoki ilovaning yopilib qolishiga olib kelishi mumkin Navigatsiya panelini yashirish Pleerdagi paydo boʻladigan tugmalarni tanlang \'Audio Track\' menyusini yashirish @@ -689,6 +934,7 @@ Ma'lumot: Jonli efirlarda ishlamasligi mumkin" \'Sifat\' menyusining tepa qismini yashirish + \'Yo\'lda\' menyusini yashirish Fonda oʻynashni oʻchirish Fonda oʻynashni yoqish @@ -715,6 +961,8 @@ Ma'lumot: Jonli efirlarda ishlamasligi mumkin" Yuklanish ekranida gradient fonini koʻrsatish Pleyer boshqaruv tugmalarining fonini yashirish Monoxrom + Boshqaruv tugmalari fonining shaffofligi + Pleer boshqaruv tugmalari va vaqt paneli orqasidagi fon shaffofligini sozlang (0 - ko\'rinmas, 100 - to\'liq) Oldingi va keyingi tugmalarni yashirish Video progress barining asosiy rangini sozlang @@ -755,6 +1003,7 @@ Ma'lumot: Jonli efirlarda ishlamasligi mumkin" Navigatsiya tugmalarini yashirish Effekt tugmasini yashirish Video havolasi yorlig\'ini yashirish + Ishora yordamlarini yashirish “Super rahmat” tugmasini yashirish Hashtag tugmasini yashirish End ekrandagi taklif qiluvchi videolarni yashirish @@ -807,6 +1056,7 @@ Sozlamalar → Oynatish (yoki Avtoijro) → Keyingi videoni avtomatik ravishda i Pleylist avtomatik ijrosini o\'chirish Pleylistlarning avtomatik ravishda keyingi videoga o\'tishini oldini oladi + \"Davom etishni xohlaysizmi?\" so\'rovini o\'chirish Sharhni yashirish O\'chirilgan @@ -814,6 +1064,8 @@ Sozlamalar → Oynatish (yoki Avtoijro) → Keyingi videoni avtomatik ravishda i Shorts tugmasini yashirish Portret va landshaft + To\'liq ekranda albom rejimini majburiy qilish + To\'liq ekranga o\'tganda pleyerni albom rejimiga aylantiradi. To\'liq ekranda allaqachon albom rejimida ochiladigan kichik ekranlarda mavjud emas To\'liq ekran gestlari To\'liq ekran rejimidagi pleyer imo-ishoralari @@ -821,6 +1073,7 @@ Sozlamalar → Oynatish (yoki Avtoijro) → Keyingi videoni avtomatik ravishda i Portret uchun pastga surish gestini o\'chirish Manzarali uchun pastga surish gestini o\'chirish Siqish orqali kattalashtirish imo-ishorasini o\'chirish + Siqib masshtabni oshirishni tiklash Videolarni to\'liq ekran rejimida ochish O\'chirilgan @@ -833,6 +1086,8 @@ Sozlamalar → Oynatish (yoki Avtoijro) → Keyingi videoni avtomatik ravishda i Kattalashtirish (kesib to‘ldirish) Video o‘lchami tugmasini ko‘rsatish Bosilganda Default, Stretch va Zoom o‘rtasida almashadi + Faqat to\'liq ekranda ko\'rsatish + Portret pleyerda video masshtab tugmasini yashirish Ovozsiz tugmani ko\'rsat @@ -850,12 +1105,30 @@ Sozlamalar → Oynatish (yoki Avtoijro) → Keyingi videoni avtomatik ravishda i Telefon Planshet Avtomobil + "O'zgarishlar quyidagilarni o'z ichiga oladi: + +Planshet maketi +• Hamjamiyat postlari yashirilgan +• Lentada ijro etish sozlamasi mavjud emas +• Shorts'da Remix tugmasi va Ovoz tugmasi mavjud emas +• \"Ko'proq videolar\" qatlami mavjud bo'lmasligi mumkin +• Yopilmagan eski video amalari paneli ko'rsatiladi +• Yoyilma menyu elementlari to'g'ri ishlamasligi mumkin + +Avtomobil maketi +• Lenta mavzular va kanallar bo'yicha tashkil etilgan +• Lentada ijro etish sozlamasi mavjud emas +• Shorts'da Remix tugmasi va Ovoz tugmasi mavjud emas +• \"Ko'proq videolar\" qatlami mavjud bo'lmasligi mumkin +• Shorts oddiy pleyerda ochiladi +• Yopilmagan eski video amalari paneli ko'rsatiladi" O\'yinchi ichida tablet tartibini yoqish "Eski o'yinchi tartibini tiklash Cheklov: Agar 'Eski video harakatlar panelini tiklash' o'chirilgan bo'lsa, 'Ko'proq videolar' qoplamasi mavjud bo'lmasligi mumkin" + 21.11.48 - Subtitrlarning eski ishlash tartibini tiklash 20.39.41 - Eski pleyer saqlashni tiklash 20.28.41 - Mubham belgilarini o\'chirish 20.13.41 - Video pleer ostida kanal nomini tiklash @@ -906,23 +1179,42 @@ Cheklov: Agar 'Eski video harakatlar panelini tiklash' o'chirilgan bo'lsa, 'Ko'p Hozirgi Short takrorlanish o\'rniga keyingi Shortga avtomatik ravishda o\'tiladi Fonda avtomatik ijro etish Fonda ijro etish vaqtida keyingi Shortga avtomatik ravishda o\'tiladi + Jonli Shorts videolarni o\'tkazib yuborish + Shorts lentasida paydo bo\'ladigan jonli efirlarni o\'tkazib yuboradi Oqimda ijro etish Doimo yoqilgan Faqat Wi-Fi Oʻchirilgan + Asl sarlavhalarni tiklash + Avtomatik tarjima qilinganlar o\'rniga videoning asl sarlavhalarini, video tavsiflarini va kanal tavsiflarini ko\'rsatadi + Sarlavha yuklanmoqda… + Lentani avtomatik yangilashni o\'chirish + Lentalar eskirgandan keyin ularning avtomatik ravishda yangilanishini oldini oladi + Lenta kartochkalari maketi + Klassik + Kartochkalar + Laynash, izoh qoldirish va saqlash tugmalari bo\'lgan kartochkalar + Izohlar Mini-pleerni qayta boshlashni oʻchirish YouTube Premium xususiyati bo\'lgan mini-pleyerning qayta tiklanishini o\'chirish Mini pleyer Kichraytirilgan video pleerni ko\'rinishi va xulq-atvorini sozlash Mini pleyer turi + Sarlavha va kanalni yashirish O\'chirilgan Standart Minimal + Minimal 2 + Planshet + Zamonaviy 1 + Zamonaviy 2 + Zamonaviy 3 + Zamonaviy 4 Dumaloq burchaklarni o\'chirish Surib yurishni o\'chirish Kichraytirilgan pleerni ekranning turli burchaklariga sudrab o\'tkazishning oldini olish @@ -942,9 +1234,18 @@ Cheklov: Agar 'Eski video harakatlar panelini tiklash' o'chirilgan bo'lsa, 'Ko'p Orqaga tugmasi lentadan chiqadi Orqaga qaytarish tugmasini bosganda lenta tepasiga qaytarish o\'rniga har doim ilovadan chiqib ketiladi + Animatsiyalarni majburiy yoqish Standart + Doimo yoqilgan + Doimo o\"chirilgan + Holat panelini yashirish + Tizim holat panelini yashiradi. Uni bir lahzaga ko\'rish uchun yuqori chetdan pastga suring + Holat paneli yuqori panellarni yopishini tuzatish + Holat panelining maketini izchil saqlaydi, shunda u yuqori panellarni yopmaydi va pleyer tepasida bo\'sh joy qoldirmaydi. Agar ilova ishdan chiqsa yoki yuqori panellar noto\'g\'ri ko\'rinsa, buni o\'chiring + Yorug²lik + Ushbu qurilma Glow splash ekrani uslubini qo\'llab-quvvatlamaydi Yorug\' mavzu foni Oq Material You (Oq) @@ -967,6 +1268,9 @@ Cheklov: Agar 'Eski video harakatlar panelini tiklash' o'chirilgan bo'lsa, 'Ko'p Video progress barining ikkilamchi rangini belgilash Yaroqsiz sekbar rangi + Original analog + Original tort + Original metall Bosh logotip Standart @@ -978,15 +1282,32 @@ Cheklov: Agar 'Eski video harakatlar panelini tiklash' o'chirilgan bo'lsa, 'Ko'p Tasvirni yuklash so\'rovlarini cheklangan mamlakatlardagi yo\'qolgan tasvirlarni tuzatish uchun muqbil serverga yo\'naltirish + DeArrow sarlavhalari + Bosh sahifa vkladkasi + Obunalar vkladkasi + \"Siz\" vkladkasi + Pleer pleylistlari & tavsiyalar + Qidiruv natijalari + DeArrow belgisini ko‘rsatish + Kroudsorsing orqali yaratilgan video sarlavhalarida DeArrow belgisini ko‘rsatadi + Oddiy rejim + Agar DeArrow foydalanuvchilari asl sarlavhani yoqtirishgan bo\'lsalar, DeArrow sarlavhasi o\'rniga asl sarlavhani ko\'rsatadi + Asl sarlavhani ko\'rsatish uchun minimal oddiy ovozlar soni + Asl sarlavhani ko\'rsatish uchun kulgili yoki ijodiy kabi bitta toifadagi oddiy ovozlar soni + DeArrow miniatyuralari Asl tasvirchalar DeArrow va original rasmlar DeArrow va statik rasmlar Statik rasmlar + "DeArrow klikbeyt sarlavhalar va muqovalarini aniqroq va kamroq shov-shuvli, jamoaviy yig'ilgan variantlar bilan almashtiradi. Sarlavhalar hamjamiyat tomonidan yoziladi va ovoz beriladi, muqovalar esa videodan olingan kadrlar hisoblanadi + +Videolar xeshlangan video ID'sining qisqa prefiksi orqali so'raladi, shuning uchun server siz aynan qaysi videoni ko'rayotganingizni aniq ayta olmaydi. Agar videoning DeArrow muqovasi bo'lmasa, asl nusxa yoki kadr ko'rsatiladi" Ulanish xatoligi haqida ogohlantirishlarni ko\'rsatish DeArrow server mavjud bo\'lmaganda bildirishnoma ko\'rsatadi + API endpoint DeArrow rasmlar keshini olish manzil Statik video rasmlar Statik rasmlar YouTube ichida yaratilgan va tashqi API ishlatilmaydi @@ -1004,6 +1325,7 @@ Cheklov: Agar 'Eski video harakatlar panelini tiklash' o'chirilgan bo'lsa, 'Ko'p Manipulyatsiya qilish Buni yoqish yuqori sifatli videolarni ochishi mumkin + Buni yoqish video ijrosining qotishiga, batareya quvvatining tezroq tugashiga va noma\'lum nojo\'ya ta\'sirlarga olib kelishi mumkin • Audio izlash yo\'q Qaysi harakatlar qurilmani tebratishni talab qilishini sozlash @@ -1013,6 +1335,7 @@ Cheklov: Agar 'Eski video harakatlar panelini tiklash' o'chirilgan bo'lsa, 'Ko'p Tezkor harakatga javob harakatlarini oʻchirish • Audio izlash yo\'q + Agar yaqinda hisobingiz kirish ma\'lumotlarini o\'zgartirgan bo\'lsangiz, MicroG ilovasini o\'chirib, qaytadan o\'rnating Havola yo\'nalishlarini o\'rab o\'tish Asosiy havolani to\'g\'ridan-to\'g\'ri ochadi, izlash havolasidan (youtube.com/redirect) o\'tib ketmaydi @@ -1020,8 +1343,20 @@ Cheklov: Agar 'Eski video harakatlar panelini tiklash' o'chirilgan bo'lsa, 'Ko'p Brauzerda ochish Ilova ichidagi brauzer o\'rniga tashqi brauzerda havolalarni ochadi + Ijro buferi hajmi Standart + Past + O\'rtacha + Maksimal + Video kechikishi + Videoni Bluetooth quloqchinlar kabi kechikishga ega audio bilan sinxronlash uchun uni kechiktiradi (0-5000 ms) + Faqat Bluetooth audio bilan kechiktirish + Faqat Bluetooth audio ulanganida videoni kechiktiradi, shunda telefon dinamiki bilan sinxronizatsiya saqlanib qoladi + Video kechiktirish tugmasini ko\'rsatish + Kechiktirishni 0 ms ga qaytarish uchun bosing va ushlab turing + Video kechiktirish menyusini ko\'rsatish + Kechiktirish millisekundlarda Harakatdan o\'rinchik @@ -1045,6 +1380,8 @@ Cheklov: Agar 'Eski video harakatlar panelini tiklash' o'chirilgan bo'lsa, 'Ko'p Sifat tugmasini ko\'rsatish Standart sifatni qaytarish uchun bosing va ushlab turish + Sifat tugmasida ruxsatni ko\'rsatish + FHD kabi yorliq o\'rniga, 1080 kabi ruxsatni ko\'rsatadi Maxsus tezlik menyusi Eski tezlik menyusini tiklash @@ -1061,22 +1398,48 @@ Cheklov: Agar 'Eski video harakatlar panelini tiklash' o'chirilgan bo'lsa, 'Ko'p Tezlik o\'zgarishi haqidagi bildirishnomalar Yangi standart ijro tezligi saqlanganda bildirishnoma ko\'rsatadi Oddiy videoning sifatini belgilash + Shorts uchun standart ijro tezligi + Shorts​da ijro tezligini eslab qolish + Siz tanlagan Shorts tezligi barcha kelgusidagi Shorts videolariga tatbiq etiladi. Agar o\"chirilgan bo\"lsa, Shorts pleyeri yopilgandan so\"ng tezlik standart holatga qaytadi + Shorts menyusida ijro etish tezligini ko\"rsatish + Shorts pleyeri menyusiga ijro etish tezligi bandini qo\"shadi + Ijro etish tezligi · %s Standart oʻqish tezligi: %s ga oʻzgartirildi Musiqa uchun tezlikni o\'chirish Musiqa videolari doimo 1.0x tezlikda ijro etilsin + Kanal tezligi oq ro\'yxati + Oq ro\'yxatga kiritilgan kanallardagi videolar doimo 1.0x tezlikda ijro etiladi + Kanalni tezlik oq ro\'yxatiga qo\'shish + Kanalni tezlik oq ro\'yxatidan olib tashlash Audio pitch boshqaruvini yoqish Standart qaytuvchi audio pitch Standart audio pitch quyidagiga o\'zgartirildi: %s Audio vaqtni cho\'zish Tezlik va pitchni mustaqil ravishda sozlash imkonini beradi, audio artefaktlarga olib kelishi mumkin + Jonli efirni ko\'rish joyini eslab qolish + Davom etayotgan jonli efirlar uchun, joriy jonli pozitsiyaga o\'tish o\'rniga sizning ko\'rish joyingizni eslab qoladi va u yerdan davom ettiradi + Ushbu funksiya 24/7 jonli efirlar va doimiy yoqilgan boshqa strimlar bilan ishlamaydi HDR videoni o\'chirish HDR videoni majburlash Ilovani ushbu qurilmani HDR qobiliyatli deb tan olishga majbur qiladi "Agar HDR dekodlash ishlasa, lekin YouTube HDR taklif qilmasa, masalan, HLG ni yashiradigan ba'zi HyperOS qurilmalarida, buni ishlating.\n\nCheklovlar:\n• Agar displey haqiqatan ham HDR ko'rsata olmasa, ranglar noto'g'ri ko'rinishi mumkin\n• Agar dekoder HDR bilan ishlay olmasa, ijro etish muvaffaqiyatsiz bo'lishi mumkin\n• HDR hali ham video oqimlarini soxtalashtiruvchi mijozga bog'liq" + Video kodeki Standart + AVCni majburiy qilish uchun \'Video oqimlarini soxtalashtirish\'ni \'Android Studio\', \'Android VR\' yoki \'visionOS\'ga o\'zgartiring + "VP9 +• Batareya quvvatini yaxshilashi va ijro paytidagi qotishlarni tuzatishi mumkin +• AV1 ga qaraganda ko'proq internet ma'lumotlarini sarflaydi + +AVC (H.264) +• 'Android Studio', 'Android VR' yoki 'visionOS' video oqimini taqlid qilish mijozini talab qiladi +• Batareya quvvatini yanada yaxshilashi mumkin +• Eski qurilmalarda yo'qolgan video sifatlarini tiklashi mumkin +• Maksimal sifat 1080p +• HDR videolari AVC dan foydalanmasligi mumkin +• Ba'zi qurilmalarda AVC ni majburan yoqib bo'lmaydi" Kengaytirilgan sifat menyusini ochish Sifat tanlash menyusini o\'tkib uzatadi va to\'g\'ridan-to\'g\'ri ilg\'or sifat menyusini ochadi @@ -1089,11 +1452,16 @@ Cheklov: Agar 'Eski video harakatlar panelini tiklash' o'chirilgan bo'lsa, 'Ko'p Android VR / visionOS AV1 ruxsat berish Android VR / visionOS mijozidan foydalanilganda dasturiy AV1 dekodlashga ruxsat beradi + "Ushbu sozlamani yoqish dasturiy AV1 dekodlashidan foydalanishga olib kelishi mumkin. + +AV1 bilan video ijrosi qotishi yoki kadrlar tushib qolishi mumkin" Manipulyatsiya qilish effektlari + • 1-daqiqada ijro xatosi yuz berishi mumkin • \'Audio Track\' menyusi mavjud emas • AV1 video kodeki yo\'q • 360° VR ajoyib rejim mavjud emas • Barqaror ovoz mavjud emas + • Ijro 1 soniya oldin tugashi mumkin • Agar kod yozish muvaffaqiyatsizlikka uchrasa, o\'ynash muvaffaqiyatsizlikka uchraydi • Asl ovozni majburlash mavjud emas @@ -1104,9 +1472,173 @@ Cheklov: Agar 'Eski video harakatlar panelini tiklash' o'chirilgan bo'lsa, 'Ko'p \'Stats for nerds\' da soxtalashtirishni ko\'rsatish Eski video harakatlar panelini tiklash + "Videodagi amal tugmalarini yashirib bo‘lmaslik muammosi tuzatildi + +Cheklov: Eski uslubdagi \"Boshqa videolar\" qatlami qaytariladi" + Pleer belgisi uslubi + Avtomatik Standart + Yupqa + Qalin Maxsus + Shorts belgisi uslubi + Seriallar kuzatuvchisi + Pleylistlarni seriallar sifatida kuzating va to\'xtagan joyingizni saqlang + Seriallar kuzatuvchisini yoqish + Tarixga \"Seriallar\" sahifasini va pleylistlarga \"Serialni kuzatish\" variantini qo\'shadi + Navigatsiya panelida Seriallarni ko\'rsatish + Seriallar kuzatuvchisi tugmasini ko\'rsatish + Joriy videoning seriyasini kuzatish yoki ochish uchun pleerga tugma qo\'shadi + Kuzatilayotgan seriyalar progressini qayd etish + Saqlangan seriallar va xatlovlar uchun ushbu telefonda ijro etishni yozib boring + Bekor qilish + Saqlash + Xatlovni saqlash + Serial nomi + Serial variantlari + Kutubxona variantlari + Epizodlarni yangilash + Qayta nomlash + Pleylistni YouTube­da ochish + Kutubxonadan o´chirish + Pleylist havolasi yoki ID­si + Barcha mahalliy progressni tozalash + Tarixni tozalash + Barcha mahalliy progressni tozalash? + Bu barcha videolar uchun pozitsiyalar, ko\'rilgan overrides va davom ettirish tanlovlarini tozalaydi. Saqlangan seriallar va ularning kataloglari saqlanib qoladi. Agar yozish yoqilgan bo\'lsa, yangi ijro yozib olinadi + Ko´rilgan deb belgilash + Ko´rilmagan deb belgilash + Avtomatik holatdan foydalanish + 0:00 dan qayta boshlash + 0:00 dan qayta ko\'rish + Holat saqlandi + Saqlashga qayta urinish + Oldingi 100 ta + Keyingi 100 ta + Saqlangan videoni ochish + Mahalliy progress tozalandi + Seriallaringizni ko\'rish uchun \"Siz\" bo\'limidan Tarixni oching + Tomosha tarixi + Seriallar + Saqlangan seriallar yuklanmoqda… + Epizodlarni kuzatish uchun pleylistga obuna bo\'ling + Pleylist xatboshimi + Epizodlar yangilanmoqda… + Barcha seriallar + Tomosha qilindi + Epizodlar yuklanmoqda… + Boshqa variantlar + "Faqat saqlangan seriallaringiz va xatboshiga qo'shilgan videolar uchun progressni yozib boring. + +Ushbu kutubxona ushbu qurilmada saqlanadi va YouTube akkauntlari o'rtasida baham ko'riladi. YouTube'ning pauza va tarixni tozalash boshqaruvi uni o'zgartirmaydi. Uni o'chirish uchun Seriallar → Yana → Mahalliy progressni tozalash bo'limidan foydalaning. + +Avtomatik yozish inkognito rejimida, akkaunt identifikatori mavjud bo'lmaganda yoki boshqa akkauntga o'tganda to'xtaydi. Ushbu akkaunt bilan foydalanish uchun kuzatuvni qayta yoqing" + Kuzatuvni inkognito rejimida yoki YouTube joriy akkaunt holatini yuklaguniga qadar yoqib bo\'lmaydi. Bosh sahifaga qayting va qayta urinib ko\'ring. Tizimga kirish talab qilinmaydi + Mahalliy kuzatuvni yoqish + Progressni yozish o\'chirilgan + %1$s da davom ettirish + %1$s / %2$s epizod ko\'rildi + %1$s-qism · %2$s + %3$s tadan %1$s-%2$s qismlar + Qismlarni yuklashga urinib ko\'rish uchun yangilang. Shaxsiy pleylistlar uchun akkauntga kirish kerak; ularning xatboshisi hali ham YouTube-da ochiladi + YouTube ushbu qismni ocha olmadi + YouTube ushbu pleylistni ocha olmadi + Epizodni tanlang + Oxirgi holatgacha ko\'rildi + Ijro etiladigan epizodlar yo\'q + Saqlangan video · %1$s + Serialni kuzatish + Serialni ko\'rish + Davom ettirish + Bu yerdan boshlang + Boshlang\'ich nuqta saqlandi + Shu joygacha ko\'rildi + Ko‘rilgan deb belgilangan epizodlar + Bekor qilish + Yoqish + %1$s ta yangi qism + Ushbu qurilmada progressni yozib borish + "Sizning seriallaringiz va progressingiz ushbu qurilmada qoladi va YouTube akkauntlari o'rtasida baham ko'riladi. YouTube tarixini boshqarish vositalari ularni o'zgartirmaydi. Progressni Seriallar menyusidan tozalang. + +Avtomatik yozib olish ixtiyoriy. U inkognito rejimida yoki akkaunt o'zgarganda to'xtaydi" + Epizodlar yuklanmadi. Pleylistni xatbosh sifatida saqlashingiz mumkin + Saqlanmagan o\'zgarishlar juda ko\'p. Avval saqlashni qaytadan urinib ko\'ring + Saqlash jarayoni band. Qaytadan urinib ko\'ring + Epizodlarni yuklash jarayoni band. Qaytadan urinib ko\'ring + Xotira mavjud emas. Saqlangan ma\'lumotlaringiz saqlandi + O\'zgarishlar saqlanmadi. YouTube ilovasini yopishdan oldin saqlashni qaytadan urinib ko\'ring + Ushbu serial allaqachon saqlangan yoki uning pleylisti o\'zgargan. Uni qayta oching + Ushbu serial o\'zgargan yoki o\'chirilgan. Kutubxonani qayta oching + Pleylist o\'zgardi. Epizodni qaytadan tanlang + Ushbu qism mavjud emas. Boshqasini tanlang + Serial nomini kiriting + YouTube pleylists havolasi yoki pleylist ID-sini kiriting + Qayta yangilashdan oldin iltimos kuting + Qismlarni yuklab bo\'lmadi. Keyinroq qayta urunib ko\'ring + O\'zgartirish bajarilmadi. Qayta urunib ko\'ring + Davom ettirish tasdiqlanmadi. Pleyerni tekshiring va qayta urunib ko\'ring + Kuzatilayotgan seriallar + Ushbu video pleylistda mavjud emas + Pleylistlar qidirilmoqda… + Pleylist havolasini joylang + Pleylist topilmadi + Epizodlar mavjud emas + Ko‘rilganlarni yashirish + Avvalgi epizodlarni ko‘rildi deb belgilash + Boshlanishi: %1$s + Epizodlar tartibi + Pleylist tartibi + Pleylist tartibini teskari qilish + %1$s ta ko‘rish + %1$s ta ko‘rish + Epizodlarni ko‘rildi deb belgilash + Kamida %1$s%% ko\'rilgan va ko\'pi bilan 90 soniya qolgan, yoki %2$s soniya qoldi + Belgilangan chegaralardan biriga yetganda epizod avtomatik ravishda ko\'rilgan deb belgilanadi. Foiz qoidasi, shuningdek, 90 sekunddan ortiq vaqt qolmaganligini talab qiladi, shuning uchun uzun epizodlarning yakuni tashlab ketilmaydi. Qolgan vaqt qoidasi bir daqiqadan uzun videolarga nisbatan qo\'llaniladi; uni o\'chirish uchun 0 ga sozlang. O\'zgarishlar kelgusidagi progress yangilanishlariga ta\'sir qiladi. Qo\'lda tanlangan ko\'rilgan/ko\'rilmagan holatlari saqlanib qoladi + Ko\'rilgan foizi (1-100) + Qolgan sekundlar (0-300; 0 o\'chiradi) + %1$s dan %2$s gacha bo\'lgan qiymatni kiriting + YouTube progressidan foydalanish + Boshqa qurilmada ko\'rilgan kuzatilayotgan seriallarni davom ettirish + Siz kuzatgan seriallarning ko\'rilgan holati va davom ettirish pozitsiyalarini joriy YouTube akkauntidan o\'qing. Faqat mos keluvchi epizodlar ushbu telefonda saqlanadi. Tekshiruvlar seriallar kutubxonasi ochiqligida va \"Davom ettirish\"dan oldin amalga oshiriladi. Boshqa qurilmangizda YouTube tarixi yoqilgan bo\'lishi kerak + YouTube progress tekshirilmoqda… + YouTube progressi mavjud emas. Saqlangan progressdan foydalanilmoqda + Ba\'zi YouTube progresslari mavjud emas. Saqlangan pozitsiyalar saqlab qolindi + Zaxiralash va tiklash + Seriallar kutubxonangizni faylga saqlang yoki zaxiradan tiklang + Kuzatilgan seriallar, qismlar holati va ko\'rilgan tanlovlarni saqlaydi. Yozish va YouTube progressi ruxsatnomalari kirmaydi + Zaxirani saqlash + Zaxirani tiklash + %1$s ta serial qo\'shiladi. Kutubxonangizdagi %2$s ta serial saqlab qolinadi. Mavjud video progressi saqlanadi + Seriallar zaxirasi saqlandi + %1$s ta serial tiklandi + Ushbu fayl qo\'llab-quvvatlanadigan seriallar zaxirasi emas yoki juda katta. Kutubxonangizga o\'zgartirish kiritilmadi + Zaxira nusxasi saqlanmadi. Boshqa joyni sinab ko\'ring + Serial olib tashlandi + Ushbu qism endi bir necha marta ko\'rinmoqda. Qaysi biridan davom etishni tanlang + Siz saqlagan qism endi ushbu pleylistda mavjud emas. Saqlangan holatingiz saqlab qolindi + %1$s-qismdan davom etish · %2$s + Siz quyidagilardan istalgan qismni tanlashingiz ham mumkin. Avvalgi qismlar o\'zgartirilmagan + Ushbu pleylist o\'zgardi. Qayerdan davom etishni tanlash uchun serialni oching + Bu yo\'nalishda boshqa qismlar yo\'q + %1$s ni davom ettirish + Seriallarni qidirish + Qidiruvni tozalash + Seriallar topilmadi + Nomsiz qism + Mavjud bo\'lmagan qism + Niconico izohlari + Jonli chat va chat takrorini video bo\"yicha harakatlanuvchi izohlar ko\"rinishida ko\"rsatish + Video pleyerda ustki tugmani ko\"rsatadi. Izohlarni ko\"rsatish yoki yashirish uchun bosing, tezkor sozlamalar uchun esa bosib turing + Izohlar shrifti o\"lchami + Izohlarning matn o\"lchami (12-40 sp) + Izohlar shaffofligi + Izoh matni va fonining shaffofligi + Harakat davomiyligi + Izohlarning ekran bo\'ylab o\'tishi uchun necha soniya kerakligi. Kichikroq qiymatlar tezroq harakatlanishini ta\'minlaydi + Izohning maksimal uzunligi + Ushbu miqdordan ko\'p belgiga ega bo\'lgan izohlarni yashiradi. Har bir emoji bitta belgi hisoblanadi + Cheklov yo\'q diff --git a/patches/src/main/resources/addresources/values-vi-rVN/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-vi-rVN/shared-youtube/strings.xml index 2732f93ab9..96b66f57c7 100644 --- a/patches/src/main/resources/addresources/values-vi-rVN/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-vi-rVN/shared-youtube/strings.xml @@ -21,9 +21,9 @@ Second \"item\" text" Hoạt ảnh khởi động Tắt Màu - Morphe Có Màu Đơn sắc - Morphe Đơn sắc + + Tắt Khởi động lại để hoàn tất thiết lập diff --git a/patches/src/main/resources/addresources/values-vi-rVN/youtube/strings.xml b/patches/src/main/resources/addresources/values-vi-rVN/youtube/strings.xml index 2dd1947c3a..88941f08f0 100644 --- a/patches/src/main/resources/addresources/values-vi-rVN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-vi-rVN/youtube/strings.xml @@ -113,6 +113,9 @@ Các thay đổi bao gồm: Ẩn khảo sát Ẩn khảo sát ​​và đánh giá độ phù hợp của đề xuất trang video Ẩn kệ vé + + Ẩn biểu ngữ \"Đã ẩn các video không khả dụng\" + Ẩn biểu ngữ \"Đã ẩn các video không khả dụng\" trong danh sách phát Ẩn ngày tải lên "Hạn chế: @@ -1249,6 +1252,7 @@ Hạn chế: Nếu tùy chọn \"Khôi phục thanh thao tác video cũ\" bị t Giữ cho bố cục của thanh trạng nhất quán, để nó không che các thanh trên cùng và không để khoảng trống phía trên trình phát. Tắt tùy chọn này nếu ứng dụng bị đóng băng hoặc các thanh trên cùng trông sai Phát sáng + Màn hình chào hiệu ứng phát sáng không được hỗ trợ bởi thiết bị này Nền chủ đề sáng Trắng Material You (Trắng) diff --git a/patches/src/main/resources/addresources/values-zh-rCN/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-zh-rCN/shared-youtube/strings.xml index e97860a1e8..0270cea6d6 100644 --- a/patches/src/main/resources/addresources/values-zh-rCN/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-zh-rCN/shared-youtube/strings.xml @@ -21,9 +21,8 @@ Second \"item\" text" 启动动画 已禁用 颜色 - 彩色 Morphe 单色 - 单色 Morphe + 重启以完成设置 diff --git a/patches/src/main/resources/addresources/values-zh-rCN/youtube/strings.xml b/patches/src/main/resources/addresources/values-zh-rCN/youtube/strings.xml index 5cf3f7a153..771e995d19 100644 --- a/patches/src/main/resources/addresources/values-zh-rCN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-zh-rCN/youtube/strings.xml @@ -113,6 +113,9 @@ Second \"item\" text" 隐藏调查 隐藏关于信息流推荐准确性的调查和提示 隐藏票券架 + + 隐藏“已隐藏无法播放的视频”横幅 + 隐藏播放列表中的“已隐藏无法播放的视频”横幅 隐藏上传时间 "限制: @@ -1249,6 +1252,7 @@ Second \"item\" text" 保持状态栏布局一致,使其不覆盖顶部栏且播放器上方无空白区域。如果应用崩溃或顶部栏显示异常,请关闭此选项 发光 + 此设备不支持发光启动页 浅色主题背景 白色 Material You(白色) diff --git a/patches/src/main/resources/addresources/values-zh-rTW/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-zh-rTW/shared-youtube/strings.xml index a0da7129fe..87fb7da6c6 100644 --- a/patches/src/main/resources/addresources/values-zh-rTW/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-zh-rTW/shared-youtube/strings.xml @@ -21,9 +21,9 @@ Second \"item\" text" 啟動動畫 已停用 顏色 - 彩色 Morphe 單色 - 單色 Morphe + + 關閉 重新啟動以完成設定 diff --git a/patches/src/main/resources/addresources/values-zh-rTW/youtube/strings.xml b/patches/src/main/resources/addresources/values-zh-rTW/youtube/strings.xml index 6a3a8c9248..2eb4a008a4 100644 --- a/patches/src/main/resources/addresources/values-zh-rTW/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-zh-rTW/youtube/strings.xml @@ -113,6 +113,9 @@ Second \"item\" text" 隱藏問卷調查 隱藏關於資訊流推薦準確度的調查和提示 隱藏票券架 + + 隱藏「已隱藏無法播放的影片」橫幅 + 隱藏播放清單中的「已隱藏無法播放的影片」橫幅 隱藏「上載時間」 "限制: @@ -1249,6 +1252,7 @@ Second \"item\" text" 保持狀態列的版面配置一致,使其不會遮蓋頂部工具列,且在播放器上方不會留有空白。如果應用程式發生當機或頂部工具列顯示異常,請停用此選項 發光 + 此裝置不支援發光啟動畫面 淺色主題背景 白色 動態顏色(白色) diff --git a/patches/src/main/resources/addresources/values-zu-rZA/music/strings.xml b/patches/src/main/resources/addresources/values-zu-rZA/music/strings.xml index d1955539c0..f10f9378e1 100644 --- a/patches/src/main/resources/addresources/values-zu-rZA/music/strings.xml +++ b/patches/src/main/resources/addresources/values-zu-rZA/music/strings.xml @@ -545,8 +545,6 @@ I-cookies ezidingekayo: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1P Kugcinwe Gqamisa amagama noma iziluku ezihambisana nomculo Nikela amagama amagama lapho etholakala. Lapho amazwi enesikhathi segama, agqamisa igama ngalinye noma isiluku ngesikhathi somculo - Khombisa amazwi asemva ngokuhlukile - Kuhambisa izwi elingemuva elibhalwe ezibakweni, njengokuthi \"(Oh) I want you\", kukhishwe emugqeni oculwayo kuye emugqeni wawo ocashile Fihla amazwi adlalwe Fihla imigqa yamazwi engaphezulu komugqa odlalwa njengamanje Fihla amazwi angakadalwa diff --git a/patches/src/main/resources/addresources/values-zu-rZA/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-zu-rZA/shared-youtube/strings.xml index 324777b962..794c960e76 100644 --- a/patches/src/main/resources/addresources/values-zu-rZA/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-zu-rZA/shared-youtube/strings.xml @@ -21,6 +21,8 @@ Second \"item\" text" I-animation yokuqala Idisayizelwe Umbala + Imibala emifana + Qala kabusha ukuze uqedele ukusetha diff --git a/patches/src/main/resources/addresources/values-zu-rZA/youtube/strings.xml b/patches/src/main/resources/addresources/values-zu-rZA/youtube/strings.xml index 3fcced6949..f242982aec 100644 --- a/patches/src/main/resources/addresources/values-zu-rZA/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-zu-rZA/youtube/strings.xml @@ -113,6 +113,9 @@ Izinguquko zihlanganisa: Fihla imibuzo Fihla imibuzo nokukhuthaza mayelana nokunemba kokunikezelwa kwe-feed Fihla ishelufu yamathikithi + + Fihla ibhena elithi \'Amavidiyo angatholakali afihliwe\' + Lifihla ibhena elithi \'Amavidiyo angatholakali afihliwe\' ohlelweni lwamavidiyo Fihla isikhathi sokukhuphula "Imikhawulo: @@ -395,6 +398,7 @@ Izibiko zivulekile futhi zibuyekezwa ngaphambi kokuba ishネル yengeke ohlwini. Fihla imikhangiso ye-YouTube Premium + Kulodwa… Ishanali yamanje: Yengeza ohlweni lwe-whitelist @@ -499,6 +503,9 @@ Ngemuva kokuvulwa kwevidiyo enenkinobho ethi Buza kanye, le mpawu isebenza kuzo Ividiyo Umsindo Zonke izinketho + Khombisa ukulandela ku-menu yama-Shorts + Yengeza ithemba loku-downloader langaphandle ku-menu yomdlali wama-Shorts + Landa Ukuhumusha ngezwi Funda imibhalo yevidiyo ngokuzwakalayo usebenzisa i-text-to-speech edivaisini, ihambisana nokudlalwa kwevidiyo @@ -609,6 +616,8 @@ Ulwazi: Kungase kungasebenzi kuma-live streams" Kuvumela ukuvimba ama-vidiyo ahlobene nezingane ku-\'Buka kamuva\' kusuka ku-flyout menu Khombisa inkinobho ye-Save to Watch later ku-shorts flyout Kuvumela ukuvimba ama-shorts ku-\'Buka kamuva\' kusuka ku-flyout menu + Dlulisela uBuka kamuva ku-playlist + Buka kamuva Yengeza ohlwini lweMorphe Gcina uhlu lokulinda phakathi kwezikhathi zokusetshenziswa Uhlu lokulinda lugcinwa ngemva kokuvula futhi uhlelo lokusebenza @@ -1103,6 +1112,23 @@ Settings $\rightarrow$ Playback (noma Autoplay) $\rightarrow$ Autoplay next vide Ucingo Ithebhulethi Ezimoto + "Izinguquko zihlanganisa: + +Indlela yokuhlela yethabhulethi +• Izithamo zomphakathi zifihliwe +• Isetingu sokudlala ema-feed asitholakali +• Inkinobho ye-Remix nenkinobho ye-Sound azitholakali kuma-Shorts +• I-overlay ethi 'Amavidiyo amaningi' kungenzeka ingatholakali +• I-action bar yevidiyo endala engagobisiwe iyakhonjiswa +• Izinto ze-flyout menu kungenzeka zingasebenzi ngendlela efanele + +Indlela yokuhlela yezimoto +• I-feed ihlelwe ngokwezihloko namashaneli +• Isetingu sokudlala ema-feed asitholakali +• Inkinobho ye-Remix nenkinobho ye-Sound azitholakali kuma-Shorts +• I-overlay ethi 'Amavidiyo amaningi' kungenzeka ingatholakali +• Ama-Shorts avuleka kumdlali ojwayelekile +• I-action bar yevidiyo endala engagobisiwe iyakhonjiswa" Vumela ukulungiseka kwe-tablet kudlali "Buyisela ukulungiselelwa kwedlali yakudala @@ -1225,6 +1251,8 @@ Ukukhawulelwa: Uma 'Buyisela ibha yezenzo yevidiyo yakudala' icishiwe, i-overlay Lungisa i-status bar emboza imigqa engenhla Gcina ukuhleleka kwe-status bar kukufana, ukuze ingabozi imigqa engenhla futhi ingashiyi isikhala esingenalutho ngaphezu kwedlala. Cima lokhu uma i-app i-crash noma imigqa engenhla ibukeka ngendlela engalungile + Khanya + Isikrini sokuqala esikhanyayo asisekelwa yileli device Isizinda sethimu ekhanyayo Mhlophe Material You (Mhlophe) @@ -1247,6 +1275,9 @@ Ukukhawulelwa: Uma 'Buyisela ibha yezenzo yevidiyo yakudala' icishiwe, i-overlay Setha umbala wesibili we-video progress bar Inani lombala lwesekibar elingelona + I-analog yokuqala + Ikhekhe lokuqala + Insimbi yokuqala Ulogo we-Header Okujwayelekile @@ -1539,7 +1570,6 @@ Ukuqopshwa ngokuzenzakalelayo kuyama ku-incognito, lapho ubunikazi be-account lu Izinhlelo ezimakiwe njengezibukwe Buyisa Vula - {count, plural, one {iqonda elisha 1} other {iziqonda ezintsha {count}}} {%1$s} episodes ezintsha Qopha inqubekelaphambili kuleli device "I-series yakho nenqubekela phambili ihlala kuleli device, ihlanganyelwa phakathi kwe-accounts ze-YouTube. Izilawuli zomlando we-YouTube azizishintshi. Sula inqubekela phambili kume-nu ye-Series. @@ -1611,4 +1641,16 @@ Ukuqopha ngokuzenzakalelayo kuyinketho. Kuyama ku-incognito noma lapho i-account Isiqephi esingenaisihloko Isiqephi esingatholakali + Imibhalo ye-Niconico + Khombisa i-live chat kanye ne-chat replay njengemibhalo ehambayo phakathi kwevidiyo + Khombisa inkinobho engaphezulu kwe-video player. Thinta ukuze ukhombise noma ufihle imibhalo, thinta ubambe ukuze uthole izilungiselelo ezisheshayo + Usayizi wefonti yombhalo + Usayizi wombhalo wemibhalo (12-40 sp) + Ukucaca kombhalo + Ukucaca kombhalo wemibhalo kanye nezizinda + Isikhathi sokuhamba + Imizuzwana embalwa imibhalo ithatha ukundiza esikrinini. Amanani aphansi ahamba ngokushesha + Ubude obukhulu bembhalo + Ifihla imibhalo enezinhlamu ezingaphezu kwalena. I-emoji ngayinye ibalwa njengohlamvu olulodwa + Alikho unyamezelo From cc9b10b1692ba83e05fdd20334782fb719ddd1bb Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Thu, 29 Oct 2026 18:33:31 +0000 Subject: [PATCH 167/189] chore: Release v2.0.0-dev.1 [skip ci] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## [2.0.0-dev.1](https://github.com/MorpheApp/morphe-patches/compare/v1.49.0-dev.11...v2.0.0-dev.1) (2026-10-29) ### ⚠ BREAKING CHANGES * Patches now use Morphe Patcher 2.0.0 * Treat breaking changes as major releases ([c596d42](https://github.com/MorpheApp/morphe-patches/commit/c596d42afb48dd79be022f81f3b731eda8870941)) ### 🐛 Bug Fixes * **YouTube - Download post images:** Match posts with texts of many lines or a single emoji ([#4167](https://github.com/MorpheApp/morphe-patches/issues/4167)) ([0c5dc17](https://github.com/MorpheApp/morphe-patches/commit/0c5dc17e215ad63b748570adea5894553d489dd6)) ### ✨ New Features * **YouTube:** Add offline sponsorblock segment support for in-app downloads ([#4140](https://github.com/MorpheApp/morphe-patches/issues/4140)) ([136851e](https://github.com/MorpheApp/morphe-patches/commit/136851e5a5690fd9e2488eb3f0e57537bdfffc2a)) * **YouTube:** Highlight new in-app settings ([#4162](https://github.com/MorpheApp/morphe-patches/issues/4162)) ([a3b34d7](https://github.com/MorpheApp/morphe-patches/commit/a3b34d75d399fcc7dd3f9cb2c68f32fc112d979b)) ### 🚀 Updated App Support * **YouTube Music:** Add support for `9.30.53` ([f049c91](https://github.com/MorpheApp/morphe-patches/commit/f049c91c540f74da8dffe462a8fa5b10cb073305)) --- CHANGELOG.md | 21 +++ README.md | 6 +- gradle.properties | 2 +- patches-bundle.json | 8 +- patches-list.json | 345 +++++++++++++++++++++++++++++++++++++++++++- 5 files changed, 373 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d2464322a1..256f0c7489 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,24 @@ +## [2.0.0-dev.1](https://github.com/MorpheApp/morphe-patches/compare/v1.49.0-dev.11...v2.0.0-dev.1) (2026-10-29) + +### ⚠ BREAKING CHANGES + +* Patches now use Morphe Patcher 2.0.0 + +* Treat breaking changes as major releases ([c596d42](https://github.com/MorpheApp/morphe-patches/commit/c596d42afb48dd79be022f81f3b731eda8870941)) + +### 🐛 Bug Fixes + +* **YouTube - Download post images:** Match posts with texts of many lines or a single emoji ([#4167](https://github.com/MorpheApp/morphe-patches/issues/4167)) ([0c5dc17](https://github.com/MorpheApp/morphe-patches/commit/0c5dc17e215ad63b748570adea5894553d489dd6)) + +### ✨ New Features + +* **YouTube:** Add offline sponsorblock segment support for in-app downloads ([#4140](https://github.com/MorpheApp/morphe-patches/issues/4140)) ([136851e](https://github.com/MorpheApp/morphe-patches/commit/136851e5a5690fd9e2488eb3f0e57537bdfffc2a)) +* **YouTube:** Highlight new in-app settings ([#4162](https://github.com/MorpheApp/morphe-patches/issues/4162)) ([a3b34d7](https://github.com/MorpheApp/morphe-patches/commit/a3b34d75d399fcc7dd3f9cb2c68f32fc112d979b)) + +### 🚀 Updated App Support + +* **YouTube Music:** Add support for `9.30.53` ([f049c91](https://github.com/MorpheApp/morphe-patches/commit/f049c91c540f74da8dffe462a8fa5b10cb073305)) + ## [1.49.0-dev.11](https://github.com/MorpheApp/morphe-patches/compare/v1.49.0-dev.10...v1.49.0-dev.11) (2026-10-29) ### ⚠ BREAKING CHANGES diff --git a/README.md b/README.md index 092775f673..72534b6be5 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ All modifications made by Morphe, along with their dates, can be found in the Gi ## 🩹 Patches list -> **[v1.49.0-dev.11](https://github.com/MorpheApp/morphe-patches/releases/tag/v1.49.0-dev.11)**  •  `dev`  •  193 patches total +> **[v2.0.0-dev.1](https://github.com/MorpheApp/morphe-patches/releases/tag/v2.0.0-dev.1)**  •  `dev`  •  193 patches total

    📦 YouTube  •  108 patches
    @@ -164,8 +164,8 @@ All modifications made by Morphe, along with their dates, can be found in the Gi **🎯 Supported versions:** -| 🧪 9.43.51 | 🧪 9.42.52 | 🧪 9.41.52 | 9.20.53 | 9.15.51 | -| :---: | :---: | :---: | :---: | :---: | +| 🧪 9.43.51 | 🧪 9.42.52 | 🧪 9.41.52 | 9.30.53 | 9.20.53 | 9.15.51 | +| :---: | :---: | :---: | :---: | :---: | :---: | | 💊 Patch | 📜 Description | ⚙️ Options | |----------|----------------|-----------| diff --git a/gradle.properties b/gradle.properties index b5f91d873a..030930fc64 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,4 +5,4 @@ android.useAndroidX = true # Extensions are Java only. Prevents AGP from adding Kotlin compilation and the Kotlin stdlib. android.builtInKotlin = false kotlin.code.style = official -version = 1.49.0-dev.11 +version = 2.0.0-dev.1 diff --git a/patches-bundle.json b/patches-bundle.json index 82015a8cc9..b6b15ab603 100644 --- a/patches-bundle.json +++ b/patches-bundle.json @@ -1,8 +1,8 @@ { - "created_at": "2026-10-29T08:20:48", - "description": "## [1.49.0-dev.11](https://github.com/MorpheApp/morphe-patches/compare/v1.49.0-dev.10...v1.49.0-dev.11) (2026-10-29)\n\n### ⚠ BREAKING CHANGES\n\n* **YouTube - Theme:** Patches now use Morphe Patcher 2.0.0\n\nCo-authored-by: MarcaDian <152095496+MarcaDian@users.noreply.github.com>\n\n### 🐛 Bug Fixes\n\n* **YouTube - Ambient mode:** Ambient mode is replaced by a black background with the new watch page experiment ([43a9e4c](https://github.com/MorpheApp/morphe-patches/commit/43a9e4c8935627af8f1748071438c25b653deefb))\n* **YouTube - Ambient mode:** Bypass Ambient mode restrictions does not work with 21.43 ([aeac8c9](https://github.com/MorpheApp/morphe-patches/commit/aeac8c9c15f93127a41326a3e5ae913a7c75dbae))\n* **YouTube - Feed card layout:** Movies converted to cards are not hidden when \"Hide movies section\" is enabled ([5c6357f](https://github.com/MorpheApp/morphe-patches/commit/5c6357fed1f52c5ed5d0236a393392b860e8d5ae))\n* **YouTube - Feed view count:** View count badge of a card that enters from the bottom loads only when its title row is shown ([26715b1](https://github.com/MorpheApp/morphe-patches/commit/26715b1723f5755dc13a64c8b41be4a499f4f2cb))\n* **YouTube - Hide layout components:** \"Visit the community\" entry point is not hidden in the comments of a video opened from a playlist ([e11da94](https://github.com/MorpheApp/morphe-patches/commit/e11da94ca64632956222074390f67f7f13bd82f9))\n* **YouTube - Video quality:** Buffered video is cleared with the placeholder playback times of live streams after selecting a quality ([36e3612](https://github.com/MorpheApp/morphe-patches/commit/36e3612c0bfa7435e47fa9529b12a819b11ac843))\n* **YouTube - Video quality:** Extend the background resume fix of regular videos to Shorts ([958aa55](https://github.com/MorpheApp/morphe-patches/commit/958aa55c56ccef1e36ea1003f0f869203cc828b7))\n* **YouTube - Video quality:** Quality selected for a regular video is lost when the video reloads ([16d4e60](https://github.com/MorpheApp/morphe-patches/commit/16d4e602c7c8961b35bd737b5e5770312ef1781c))\n* **YouTube - Video quality:** Regular video returns to the default quality when it resumes from the background ([e5cd752](https://github.com/MorpheApp/morphe-patches/commit/e5cd7523ba2963b29fff6c3169cc71e1a58f405d))\n* **YouTube - Video quality:** Short loop restarts with the previous quality when the quality is selected in its first segment ([a325744](https://github.com/MorpheApp/morphe-patches/commit/a325744ef3434bf0fe40525d4e2da9b696a2f2cb))\n* **YouTube - Video quality:** Short plays the previous quality segments again when it loops ([328b85a](https://github.com/MorpheApp/morphe-patches/commit/328b85acb6fe32b7722dda7fd41ff2a8988e9c7d))\n* **YouTube - Video quality:** Video restarts or plays faster after selecting a quality ([c000b53](https://github.com/MorpheApp/morphe-patches/commit/c000b53eeae02941f82a37e1c66c12a06c3d1059))\n* **YouTube Music - Change miniplayer color:** Miniplayer color does not update after track changes ([e1410e3](https://github.com/MorpheApp/morphe-patches/commit/e1410e3189531c1fb4ab41c576c4fdd20aa89e36))\n* **YouTube:** Feed thumbnail badges are shown on the page header of a channel that is live ([394ab97](https://github.com/MorpheApp/morphe-patches/commit/394ab97af76d788d6b13fc4c47f8e2ba4d892cfc))\n* **YouTube:** Feed thumbnail badges disappear while scrolling, are missing on cards with replaced titles and are shown on live videos ([4344fab](https://github.com/MorpheApp/morphe-patches/commit/4344fab6ef2e8aa6694072f38478238b84e0f42a))\n* **YouTube:** Fix TalkBack not reading captions ([#4136](https://github.com/MorpheApp/morphe-patches/issues/4136)) ([b22841c](https://github.com/MorpheApp/morphe-patches/commit/b22841c5a6ddf6ff38b49f262ca5aa893348a600))\n* **YouTube:** Prevent TalkBack crashes caused by Litho hover recursion ([#4129](https://github.com/MorpheApp/morphe-patches/issues/4129)) ([4c00210](https://github.com/MorpheApp/morphe-patches/commit/4c002101e3472e9700acb8f92a63291f69b61116))\n* **YouTube:** Watch history search bar from getting hidden when selected ([#4130](https://github.com/MorpheApp/morphe-patches/issues/4130)) ([a4ecfcf](https://github.com/MorpheApp/morphe-patches/commit/a4ecfcfc3ebe99ba452e5e87c9767a83c4760c25))\n\n### ✨ New Features\n\n* **YouTube - Hide layout components:** Add \"Hide 'Unavailable videos are hidden' banner\" setting ([#4134](https://github.com/MorpheApp/morphe-patches/issues/4134)) ([a7ce357](https://github.com/MorpheApp/morphe-patches/commit/a7ce357d2b654aed2a241c901bf2bc3df2ce7274))\n* **YouTube - Minimal miniplayer:** Swipe the bar up to move it to the top of the screen instead of expanding the player ([f8180da](https://github.com/MorpheApp/morphe-patches/commit/f8180da70c848560c4fe17106afb49ff6852e564))\n* **YouTube - Theme:** Add \"Glow Morphe\" experimental splash screen type ([#4135](https://github.com/MorpheApp/morphe-patches/issues/4135)) ([6dbdf82](https://github.com/MorpheApp/morphe-patches/commit/6dbdf82457c5f8c7906b56f04640baf7e908060a))\n* **YouTube Music - Third-party lyrics:** Per-syllable romanization layout, dynamic secondary vocals, and typography refinements ([#4090](https://github.com/MorpheApp/morphe-patches/issues/4090)) ([a5d943b](https://github.com/MorpheApp/morphe-patches/commit/a5d943b40369e1a6050db6b5a51a5104a2190ab9))\n\n### 🚀 Updated App Support\n\n* **Reddit:** Add experimental support for `2026.44.0` ([793f044](https://github.com/MorpheApp/morphe-patches/commit/793f0447ce2ec70d7c02c8ca005b09000a0f70b0))\n* **YouTube:** Add support for `21.30.216` ([89defd5](https://github.com/MorpheApp/morphe-patches/commit/89defd5d039e9dbcc139866cb5941cb3b592559d))", - "download_url": "https://github.com/MorpheApp/morphe-patches/releases/download/v1.49.0-dev.11/patches-1.49.0-dev.11.mpp", + "created_at": "2026-10-29T18:31:17", + "description": "## [2.0.0-dev.1](https://github.com/MorpheApp/morphe-patches/compare/v1.49.0-dev.11...v2.0.0-dev.1) (2026-10-29)\n\n### ⚠ BREAKING CHANGES\n\n* Patches now use Morphe Patcher 2.0.0\n\n* Treat breaking changes as major releases ([c596d42](https://github.com/MorpheApp/morphe-patches/commit/c596d42afb48dd79be022f81f3b731eda8870941))\n\n### 🐛 Bug Fixes\n\n* **YouTube - Download post images:** Match posts with texts of many lines or a single emoji ([#4167](https://github.com/MorpheApp/morphe-patches/issues/4167)) ([0c5dc17](https://github.com/MorpheApp/morphe-patches/commit/0c5dc17e215ad63b748570adea5894553d489dd6))\n\n### ✨ New Features\n\n* **YouTube:** Add offline sponsorblock segment support for in-app downloads ([#4140](https://github.com/MorpheApp/morphe-patches/issues/4140)) ([136851e](https://github.com/MorpheApp/morphe-patches/commit/136851e5a5690fd9e2488eb3f0e57537bdfffc2a))\n* **YouTube:** Highlight new in-app settings ([#4162](https://github.com/MorpheApp/morphe-patches/issues/4162)) ([a3b34d7](https://github.com/MorpheApp/morphe-patches/commit/a3b34d75d399fcc7dd3f9cb2c68f32fc112d979b))\n\n### 🚀 Updated App Support\n\n* **YouTube Music:** Add support for `9.30.53` ([f049c91](https://github.com/MorpheApp/morphe-patches/commit/f049c91c540f74da8dffe462a8fa5b10cb073305))", + "download_url": "https://github.com/MorpheApp/morphe-patches/releases/download/v2.0.0-dev.1/patches-2.0.0-dev.1.mpp", "signature_download_url": "", - "version": "1.49.0-dev.11", + "version": "2.0.0-dev.1", "public_key": "g8Ys9leTu2yeixojeR35HrNkK6eOSLpA6nNIizRutqI=" } diff --git a/patches-list.json b/patches-list.json index 6e0df41732..4cf1406aec 100644 --- a/patches-list.json +++ b/patches-list.json @@ -1,6 +1,6 @@ { "NOTE": "Do NOT manually edit this file. This file is automatically updated when semantic release (release.yml) runs. Manually editing this file can break your releases and break third party tools that use this file.", - "version": "1.49.0-dev.11", + "version": "2.0.0-dev.1", "patches": [ { "name": "AI channel filter", @@ -257,6 +257,13 @@ "minSdk": 26, "description": null }, + { + "version": "9.30.53", + "versionCodes": null, + "isExperimental": false, + "minSdk": 26, + "description": null + }, { "version": "9.20.53", "versionCodes": null, @@ -379,6 +386,13 @@ "minSdk": 26, "description": null }, + { + "version": "9.30.53", + "versionCodes": null, + "isExperimental": false, + "minSdk": 26, + "description": null + }, { "version": "9.20.53", "versionCodes": null, @@ -869,6 +883,13 @@ "minSdk": 26, "description": null }, + { + "version": "9.30.53", + "versionCodes": null, + "isExperimental": false, + "minSdk": 26, + "description": null + }, { "version": "9.20.53", "versionCodes": null, @@ -1043,6 +1064,13 @@ "minSdk": 26, "description": null }, + { + "version": "9.30.53", + "versionCodes": null, + "isExperimental": false, + "minSdk": 26, + "description": null + }, { "version": "9.20.53", "versionCodes": null, @@ -1101,6 +1129,13 @@ "minSdk": 26, "description": null }, + { + "version": "9.30.53", + "versionCodes": null, + "isExperimental": false, + "minSdk": 26, + "description": null + }, { "version": "9.20.53", "versionCodes": null, @@ -1375,6 +1410,13 @@ "minSdk": 26, "description": null }, + { + "version": "9.30.53", + "versionCodes": null, + "isExperimental": false, + "minSdk": 26, + "description": null + }, { "version": "9.20.53", "versionCodes": null, @@ -1751,6 +1793,13 @@ "minSdk": 26, "description": null }, + { + "version": "9.30.53", + "versionCodes": null, + "isExperimental": false, + "minSdk": 26, + "description": null + }, { "version": "9.20.53", "versionCodes": null, @@ -1809,6 +1858,13 @@ "minSdk": 26, "description": null }, + { + "version": "9.30.53", + "versionCodes": null, + "isExperimental": false, + "minSdk": 26, + "description": null + }, { "version": "9.20.53", "versionCodes": null, @@ -2089,6 +2145,13 @@ "minSdk": 26, "description": null }, + { + "version": "9.30.53", + "versionCodes": null, + "isExperimental": false, + "minSdk": 26, + "description": null + }, { "version": "9.20.53", "versionCodes": null, @@ -2491,6 +2554,13 @@ "minSdk": 26, "description": null }, + { + "version": "9.30.53", + "versionCodes": null, + "isExperimental": false, + "minSdk": 26, + "description": null + }, { "version": "9.20.53", "versionCodes": null, @@ -2629,6 +2699,13 @@ "minSdk": 26, "description": null }, + { + "version": "9.30.53", + "versionCodes": null, + "isExperimental": false, + "minSdk": 26, + "description": null + }, { "version": "9.20.53", "versionCodes": null, @@ -2975,6 +3052,13 @@ "minSdk": 26, "description": null }, + { + "version": "9.30.53", + "versionCodes": null, + "isExperimental": false, + "minSdk": 26, + "description": null + }, { "version": "9.20.53", "versionCodes": null, @@ -4089,6 +4173,13 @@ "minSdk": 26, "description": null }, + { + "version": "9.30.53", + "versionCodes": null, + "isExperimental": false, + "minSdk": 26, + "description": null + }, { "version": "9.20.53", "versionCodes": null, @@ -4219,6 +4310,13 @@ "minSdk": 26, "description": null }, + { + "version": "9.30.53", + "versionCodes": null, + "isExperimental": false, + "minSdk": 26, + "description": null + }, { "version": "9.20.53", "versionCodes": null, @@ -4277,6 +4375,13 @@ "minSdk": 26, "description": null }, + { + "version": "9.30.53", + "versionCodes": null, + "isExperimental": false, + "minSdk": 26, + "description": null + }, { "version": "9.20.53", "versionCodes": null, @@ -4407,6 +4512,13 @@ "minSdk": 26, "description": null }, + { + "version": "9.30.53", + "versionCodes": null, + "isExperimental": false, + "minSdk": 26, + "description": null + }, { "version": "9.20.53", "versionCodes": null, @@ -4465,6 +4577,13 @@ "minSdk": 26, "description": null }, + { + "version": "9.30.53", + "versionCodes": null, + "isExperimental": false, + "minSdk": 26, + "description": null + }, { "version": "9.20.53", "versionCodes": null, @@ -4523,6 +4642,13 @@ "minSdk": 26, "description": null }, + { + "version": "9.30.53", + "versionCodes": null, + "isExperimental": false, + "minSdk": 26, + "description": null + }, { "version": "9.20.53", "versionCodes": null, @@ -5077,6 +5203,13 @@ "minSdk": 26, "description": null }, + { + "version": "9.30.53", + "versionCodes": null, + "isExperimental": false, + "minSdk": 26, + "description": null + }, { "version": "9.20.53", "versionCodes": null, @@ -5207,6 +5340,13 @@ "minSdk": 26, "description": null }, + { + "version": "9.30.53", + "versionCodes": null, + "isExperimental": false, + "minSdk": 26, + "description": null + }, { "version": "9.20.53", "versionCodes": null, @@ -5465,6 +5605,13 @@ "minSdk": 26, "description": null }, + { + "version": "9.30.53", + "versionCodes": null, + "isExperimental": false, + "minSdk": 26, + "description": null + }, { "version": "9.20.53", "versionCodes": null, @@ -5878,6 +6025,13 @@ "minSdk": 26, "description": null }, + { + "version": "9.30.53", + "versionCodes": null, + "isExperimental": false, + "minSdk": 26, + "description": null + }, { "version": "9.20.53", "versionCodes": null, @@ -6144,6 +6298,13 @@ "minSdk": 26, "description": null }, + { + "version": "9.30.53", + "versionCodes": null, + "isExperimental": false, + "minSdk": 26, + "description": null + }, { "version": "9.20.53", "versionCodes": null, @@ -6410,6 +6571,13 @@ "minSdk": 26, "description": null }, + { + "version": "9.30.53", + "versionCodes": null, + "isExperimental": false, + "minSdk": 26, + "description": null + }, { "version": "9.20.53", "versionCodes": null, @@ -6540,6 +6708,13 @@ "minSdk": 26, "description": null }, + { + "version": "9.30.53", + "versionCodes": null, + "isExperimental": false, + "minSdk": 26, + "description": null + }, { "version": "9.20.53", "versionCodes": null, @@ -6734,6 +6909,13 @@ "minSdk": 26, "description": null }, + { + "version": "9.30.53", + "versionCodes": null, + "isExperimental": false, + "minSdk": 26, + "description": null + }, { "version": "9.20.53", "versionCodes": null, @@ -7424,6 +7606,13 @@ "minSdk": 26, "description": null }, + { + "version": "9.30.53", + "versionCodes": null, + "isExperimental": false, + "minSdk": 26, + "description": null + }, { "version": "9.20.53", "versionCodes": null, @@ -7618,6 +7807,13 @@ "minSdk": 26, "description": null }, + { + "version": "9.30.53", + "versionCodes": null, + "isExperimental": false, + "minSdk": 26, + "description": null + }, { "version": "9.20.53", "versionCodes": null, @@ -7948,6 +8144,13 @@ "minSdk": 26, "description": null }, + { + "version": "9.30.53", + "versionCodes": null, + "isExperimental": false, + "minSdk": 26, + "description": null + }, { "version": "9.20.53", "versionCodes": null, @@ -8078,6 +8281,13 @@ "minSdk": 26, "description": null }, + { + "version": "9.30.53", + "versionCodes": null, + "isExperimental": false, + "minSdk": 26, + "description": null + }, { "version": "9.20.53", "versionCodes": null, @@ -8227,6 +8437,13 @@ "minSdk": 26, "description": null }, + { + "version": "9.30.53", + "versionCodes": null, + "isExperimental": false, + "minSdk": 26, + "description": null + }, { "version": "9.20.53", "versionCodes": null, @@ -9069,6 +9286,13 @@ "minSdk": 26, "description": null }, + { + "version": "9.30.53", + "versionCodes": null, + "isExperimental": false, + "minSdk": 26, + "description": null + }, { "version": "9.20.53", "versionCodes": null, @@ -9343,6 +9567,13 @@ "minSdk": 26, "description": null }, + { + "version": "9.30.53", + "versionCodes": null, + "isExperimental": false, + "minSdk": 26, + "description": null + }, { "version": "9.20.53", "versionCodes": null, @@ -9555,6 +9786,13 @@ "minSdk": 26, "description": null }, + { + "version": "9.30.53", + "versionCodes": null, + "isExperimental": false, + "minSdk": 26, + "description": null + }, { "version": "9.20.53", "versionCodes": null, @@ -9613,6 +9851,13 @@ "minSdk": 26, "description": null }, + { + "version": "9.30.53", + "versionCodes": null, + "isExperimental": false, + "minSdk": 26, + "description": null + }, { "version": "9.20.53", "versionCodes": null, @@ -10015,6 +10260,13 @@ "minSdk": 26, "description": null }, + { + "version": "9.30.53", + "versionCodes": null, + "isExperimental": false, + "minSdk": 26, + "description": null + }, { "version": "9.20.53", "versionCodes": null, @@ -10073,6 +10325,13 @@ "minSdk": 26, "description": null }, + { + "version": "9.30.53", + "versionCodes": null, + "isExperimental": false, + "minSdk": 26, + "description": null + }, { "version": "9.20.53", "versionCodes": null, @@ -10131,6 +10390,13 @@ "minSdk": 26, "description": null }, + { + "version": "9.30.53", + "versionCodes": null, + "isExperimental": false, + "minSdk": 26, + "description": null + }, { "version": "9.20.53", "versionCodes": null, @@ -10325,6 +10591,13 @@ "minSdk": 26, "description": null }, + { + "version": "9.30.53", + "versionCodes": null, + "isExperimental": false, + "minSdk": 26, + "description": null + }, { "version": "9.20.53", "versionCodes": null, @@ -10527,6 +10800,13 @@ "minSdk": 26, "description": null }, + { + "version": "9.30.53", + "versionCodes": null, + "isExperimental": false, + "minSdk": 26, + "description": null + }, { "version": "9.20.53", "versionCodes": null, @@ -10657,6 +10937,13 @@ "minSdk": 26, "description": null }, + { + "version": "9.30.53", + "versionCodes": null, + "isExperimental": false, + "minSdk": 26, + "description": null + }, { "version": "9.20.53", "versionCodes": null, @@ -10923,6 +11210,13 @@ "minSdk": 26, "description": null }, + { + "version": "9.30.53", + "versionCodes": null, + "isExperimental": false, + "minSdk": 26, + "description": null + }, { "version": "9.20.53", "versionCodes": null, @@ -11197,6 +11491,13 @@ "minSdk": 26, "description": null }, + { + "version": "9.30.53", + "versionCodes": null, + "isExperimental": false, + "minSdk": 26, + "description": null + }, { "version": "9.20.53", "versionCodes": null, @@ -11617,6 +11918,13 @@ "minSdk": 26, "description": null }, + { + "version": "9.30.53", + "versionCodes": null, + "isExperimental": false, + "minSdk": 26, + "description": null + }, { "version": "9.20.53", "versionCodes": null, @@ -11747,6 +12055,13 @@ "minSdk": 26, "description": null }, + { + "version": "9.30.53", + "versionCodes": null, + "isExperimental": false, + "minSdk": 26, + "description": null + }, { "version": "9.20.53", "versionCodes": null, @@ -11877,6 +12192,13 @@ "minSdk": 26, "description": null }, + { + "version": "9.30.53", + "versionCodes": null, + "isExperimental": false, + "minSdk": 26, + "description": null + }, { "version": "9.20.53", "versionCodes": null, @@ -12158,6 +12480,13 @@ "minSdk": 26, "description": null }, + { + "version": "9.30.53", + "versionCodes": null, + "isExperimental": false, + "minSdk": 26, + "description": null + }, { "version": "9.20.53", "versionCodes": null, @@ -12424,6 +12753,13 @@ "minSdk": 26, "description": null }, + { + "version": "9.30.53", + "versionCodes": null, + "isExperimental": false, + "minSdk": 26, + "description": null + }, { "version": "9.20.53", "versionCodes": null, @@ -12629,6 +12965,13 @@ "minSdk": 26, "description": null }, + { + "version": "9.30.53", + "versionCodes": null, + "isExperimental": false, + "minSdk": 26, + "description": null + }, { "version": "9.20.53", "versionCodes": null, From 5d571a07c37c0231b46651dd46fb2cbeef24713a Mon Sep 17 00:00:00 2001 From: RAMiK <60112744+RAMiK4@users.noreply.github.com> Date: Thu, 29 Oct 2026 22:34:59 +0400 Subject: [PATCH 168/189] feat(YouTube): Add `Translate posts` patch (#4163) --- .../youtube/patches/PostTranslatePatch.java | 1043 +++++++++++++++++ .../patches/utils/FeedVideoCardsPatch.java | 5 +- .../youtube/patches/utils/FlyoutUtils.java | 10 + .../extension/youtube/settings/Settings.java | 6 +- .../posttranslate/PostTranslatePatch.kt | 62 + .../addresources/values/youtube/arrays.xml | 162 +++ .../addresources/values/youtube/strings.xml | 10 + 7 files changed, 1296 insertions(+), 2 deletions(-) create mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/PostTranslatePatch.java create mode 100644 patches/src/main/kotlin/app/morphe/patches/youtube/interaction/posttranslate/PostTranslatePatch.kt diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/PostTranslatePatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/PostTranslatePatch.java new file mode 100644 index 0000000000..cb88a6080d --- /dev/null +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/PostTranslatePatch.java @@ -0,0 +1,1043 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/4163 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.extension.youtube.patches; + +import static app.morphe.extension.shared.StringRef.str; + +import android.app.Dialog; +import android.content.ActivityNotFoundException; +import android.content.Context; +import android.content.Intent; +import android.content.res.Configuration; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.text.Layout; +import android.text.SpannableString; +import android.text.Spanned; +import android.text.StaticLayout; +import android.text.TextDirectionHeuristics; +import android.text.TextUtils; +import android.text.style.CharacterStyle; +import android.text.style.ClickableSpan; +import android.text.style.ForegroundColorSpan; +import android.text.style.ImageSpan; +import android.text.style.ReplacementSpan; +import android.util.Pair; +import android.util.SparseArray; +import android.view.View; +import android.view.ViewParent; +import android.widget.LinearLayout; + +import androidx.annotation.GuardedBy; +import androidx.annotation.Nullable; + +import com.facebook.litho.ComponentHost; +import com.facebook.litho.TextContent; + +import java.lang.reflect.Array; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.WeakHashMap; +import java.util.regex.Pattern; + +import app.morphe.extension.shared.Logger; +import app.morphe.extension.shared.ResourceUtils; +import app.morphe.extension.shared.Utils; +import app.morphe.extension.shared.patches.components.ContextInterface; +import app.morphe.extension.shared.settings.BaseSettings; +import app.morphe.extension.shared.spoof.SpoofAppVersionPatch; +import app.morphe.extension.shared.translation.TextTranslator; +import app.morphe.extension.shared.ui.CustomDialog; +import app.morphe.extension.shared.ui.Dim; +import app.morphe.extension.youtube.patches.utils.FeedVideoCardsPatch; +import app.morphe.extension.youtube.patches.utils.FlyoutUtils; +import app.morphe.extension.youtube.patches.utils.ProtoNode; +import app.morphe.extension.youtube.settings.Settings; + +/** + * Translates the text of posts. + *

    + * Posts are drawn by Litho, so a post has no view of its own. The element of a post includes + * its whole text, while a long text is shown cut off. When the element is parsed, its texts are + * kept. When the menu of a post is opened, the text of the post is found from the texts the post + * shows, and a Translate item is added to the menu. + *

    + * The translation is shown in place of the text, by giving the text drawable of the post a layout + * of the translation with the same size. Litho can mount the drawable again, such as after the + * post is scrolled away, so the translation is given again whenever the post is laid out. + * Texts that Litho creates again are replaced with their translation as well. + *

    + * If the text can not be translated, it is sent to the translation app of the device. + */ +@SuppressWarnings("unused") +public final class PostTranslatePatch { + + public static final boolean ENABLED = isPatchIncluded() && Settings.POST_TRANSLATE.get(); + + private static final boolean USE_EXPERIMENTAL_ICONS = VersionCheckPatch.IS_20_31_OR_GREATER + && !SpoofAppVersionPatch.isSpoofingToLessThan("20.31.00"); + + private static final String[] TRANSLATE_ICONS = USE_EXPERIMENTAL_ICONS + ? new String[]{"yt_outline_experimental_translate_vd_theme_24", "yt_outline_translate_black_24"} + : new String[]{"yt_outline_translate_black_24", "yt_outline_experimental_translate_vd_theme_24"}; + + private static final String GOOGLE_TRANSLATE_PACKAGE = "com.google.android.apps.translate"; + + private static final int MIN_TEXT_LENGTH = 2; + /** A shown text cut off this far or further is the start of a post text. */ + private static final int MIN_CUT_OFF_LENGTH = 20; + /** Dates and counts of a post, such as "2 days ago", are not longer than this. */ + private static final int MAX_METADATA_LENGTH = 30; + private static final int MAX_TEXTS = 3000; + private static final int MAX_TRANSLATIONS = 100; + /** Depth of the fields of a host that are searched for its text drawables. */ + private static final int MAX_FIELD_DEPTH = 5; + /** Limits the search of the text drawables of a host, which can reference many objects. */ + private static final int MAX_SEARCHED_OBJECTS = 2000; + /** Texts that overlap the first text by more than this are in the header of the post. */ + private static final int HEADER_TOLERANCE_DP = 2; + + private static final byte[] POST_BYTES = "post".getBytes(StandardCharsets.US_ASCII); + + /** The shown text can join the lines of the post text, or show invisible format characters. */ + private static final Pattern WHITESPACE_PATTERN = Pattern.compile("[\\s\\p{Cf}]+"); + private static final Pattern LETTER_PATTERN = Pattern.compile("\\p{L}"); + private static final Pattern DIGIT_PATTERN = Pattern.compile("\\p{Nd}"); + + /** Normalized text of a post element -> the text with its line breaks. Oldest texts are removed first. */ + @GuardedBy("itself") + private static final Map postTexts = Utils.createSizeRestrictedMap(MAX_TEXTS, true); + + /** Normalized original text -> its translation. */ + @GuardedBy("itself") + private static final Map translations = Utils.createSizeRestrictedMap(MAX_TRANSLATIONS, true); + + /** Normalized translation -> the normalized original text. */ + @GuardedBy("translations") + private static final Map originals = new HashMap<>(); + + /** Drawables that show a translation -> what they showed before. Used only on the main thread. */ + private static final Map translatedDrawables = new WeakHashMap<>(); + + /** Fields of the text drawable classes, found once for each class. Used only on the main thread. */ + private static final Map, DrawableFields> drawableFields = new HashMap<>(); + + /** Normalized original texts that are being translated. Used only on the main thread. */ + private static final Set pendingTranslations = Collections.newSetFromMap(new HashMap<>()); + + /** + * A text that a post shows. + * + * @param drawable The drawable that shows the text, or null if it is not known. + * @param bounds The bounds of the text in the root host, or null if they are not known. + */ + private record ShownText(String text, @Nullable Drawable drawable, @Nullable Rect bounds) { + } + + /** + * The text of a post. + * + * @param original The whole text, with its line breaks. + * @param key The normalized whole text. + * @param showsTranslation If the shown text is the translation. + */ + private record PostText(ShownText shown, String original, String key, boolean showsTranslation) { + } + + /** + * Fields of a text drawable, found by their type as their names are obfuscated. + */ + private record DrawableFields(Field layout, Field text, List spans) { + } + + /** + * What a drawable that shows a translation showed before. + * + * @param originalSpans Span field -> the spans of the original text. + */ + private record TranslatedDrawable(Layout originalLayout, CharSequence originalText, + Map originalSpans, Layout translatedLayout, String key) { + } + + public static boolean isPatchIncluded() { + return false; // Modified during patching. + } + + private PostTranslatePatch() { + } + + private static String normalize(CharSequence text) { + return WHITESPACE_PATTERN.matcher(text).replaceAll(" ").trim(); + } + + private static boolean isPostText(String text) { + return text.length() >= MIN_TEXT_LENGTH + && !text.contains(".eml") + && !text.startsWith("http") + && LETTER_PATTERN.matcher(text).find(); + } + + /** + * @return If the text is a short text with a number, such as the date of the post + * or the count of its likes, which is shown in the language of the app. + */ + private static boolean isMetadataText(String text) { + return text.length() <= MAX_METADATA_LENGTH && DIGIT_PATTERN.matcher(text).find(); + } + + /** + * @return The start of a text cut off with an ellipsis, such as a long post text + * followed by Read more, or null if the text is not cut off. + */ + @Nullable + private static String getCutOffStart(String text) { + int index = text.indexOf("..."); + if (index < 0) { + index = text.indexOf('…'); + } + return index >= MIN_CUT_OFF_LENGTH ? normalize(text.substring(0, index)) : null; + } + + // region Elements + + /** + * Injection point. + *

    + * Called when an element is parsed. The element is not changed. + */ + public static byte[] onElementLoaded(byte[] bytes) { + if (!ENABLED || bytes == null || !FeedVideoCardsPatch.contains(bytes, POST_BYTES)) { + return bytes; + } + try { + List root = ProtoNode.parse(bytes); + if (root == null) { + return bytes; + } + + // Post texts with line breaks are not parsed as text fields. + List texts = ProtoNode.texts(root); + + String identifier = null; + for (String text : texts) { + if (text.contains(".eml")) { + identifier = text; + break; + } + } + if (identifier == null || !identifier.contains("post")) { + return bytes; + } + + synchronized (postTexts) { + for (String text : texts) { + String key = normalize(text); + if (isPostText(key)) { + postTexts.put(key, text); + } + } + } + } catch (Exception ex) { + Logger.printException(() -> "onElementLoaded failure", ex); + } + return bytes; + } + + /** + * @return The whole text of a post the shown text is, or the start of, or null if none. + */ + @Nullable + private static String findPostText(String shown) { + String text = normalize(shown); + synchronized (postTexts) { + String original = postTexts.get(text); + if (original != null) { + return original; + } + // A long post text is cut off and shows only its start. + String start = getCutOffStart(shown); + if (start == null) { + return null; + } + String found = null; + for (Map.Entry entry : postTexts.entrySet()) { + if (entry.getKey().startsWith(start) + && (found == null || entry.getValue().length() > found.length())) { + found = entry.getValue(); + } + } + return found; + } + } + + // endregion + + // region Texts + + /** + * Injection point. + *

    + * Called when Litho creates a text, such as when a post is shown again. + */ + public static CharSequence onLithoTextLoaded(ContextInterface contextInterface, CharSequence text) { + if (!ENABLED || text == null) { + return text; + } + try { + String translation; + synchronized (translations) { + if (translations.isEmpty()) { + return text; + } + translation = translations.get(normalize(text)); + } + if (translation != null) { + Logger.printDebug(() -> "Replaced post text with its translation"); + return createTranslationText(text, translation); + } + } catch (Exception ex) { + Logger.printException(() -> "onLithoTextLoaded failure", ex); + } + return text; + } + + /** + * @return The translation with the styles of the original text, such as its color, + * which the app gives to the text as spans and not to the paint. + * Links and images are not used, as they are not in the translation. + */ + private static SpannableString createTranslationText(CharSequence original, String translation) { + SpannableString text = new SpannableString(translation); + if (!(original instanceof Spanned spanned) || translation.isEmpty()) { + return text; + } + final int length = spanned.length(); + final int end = translation.length(); + boolean hasColor = false; + for (CharacterStyle span : spanned.getSpans(0, length, CharacterStyle.class)) { + if (isLinkOrImage(span) || spanned.getSpanStart(span) > 0 || spanned.getSpanEnd(span) < length) { + continue; + } + text.setSpan(span, 0, end, Spanned.SPAN_INCLUSIVE_INCLUSIVE); + hasColor |= span instanceof ForegroundColorSpan; + } + if (!hasColor) { + // The color of the first letter, if the text has many colors, such as the color of links. + for (int i = 0; i < length; i++) { + if (!Character.isLetter(spanned.charAt(i))) { + continue; + } + for (ForegroundColorSpan span : spanned.getSpans(i, i + 1, ForegroundColorSpan.class)) { + if (!isLinkOrImage(span)) { + text.setSpan(span, 0, end, Spanned.SPAN_INCLUSIVE_INCLUSIVE); + break; + } + } + break; + } + } + return text; + } + + private static boolean isLinkOrImage(Object span) { + return span instanceof ClickableSpan || span instanceof ReplacementSpan; + } + + @Nullable + private static String getTranslation(String key) { + synchronized (translations) { + return translations.get(key); + } + } + + /** + * @return The normalized original text of the shown translation, or null if it is not a translation. + */ + @Nullable + private static String getOriginalKey(String shown) { + synchronized (translations) { + return originals.get(normalize(shown)); + } + } + + private static boolean hasTranslations() { + synchronized (translations) { + return !translations.isEmpty(); + } + } + + // endregion + + // region Drawables + + @Nullable + private static DrawableFields getDrawableFields(Drawable drawable) { + Class drawableClass = drawable.getClass(); + if (drawableFields.containsKey(drawableClass)) { + return drawableFields.get(drawableClass); + } + + List items = ((TextContent) drawable).getTextItems(); + CharSequence shownText = items == null || items.isEmpty() ? null : items.get(0); + + Field layoutField = null; + Field textField = null; + List spanFields = new ArrayList<>(); + try { + for (Class type = drawableClass; type != null && !isFrameworkClass(type); type = type.getSuperclass()) { + for (Field field : type.getDeclaredFields()) { + if (Modifier.isStatic(field.getModifiers())) { + continue; + } + Class fieldType = field.getType(); + if (Layout.class.isAssignableFrom(fieldType)) { + field.setAccessible(true); + if (layoutField == null || field.get(drawable) != null) { + layoutField = field; + } + } else if (fieldType == CharSequence.class) { + field.setAccessible(true); + // The field of the shown text, if the drawable has other texts. + if (textField == null || (shownText != null && field.get(drawable) == shownText)) { + textField = field; + } + } else if (fieldType == ClickableSpan[].class || fieldType == ImageSpan[].class) { + field.setAccessible(true); + spanFields.add(field); + } + } + } + } catch (Exception ex) { + Logger.printException(() -> "Could not find the fields of the text drawable", ex); + layoutField = null; + } + + DrawableFields fields = layoutField == null || textField == null + ? null + : new DrawableFields(layoutField, textField, spanFields); + final Field foundLayout = layoutField; + final Field foundText = textField; + Logger.printDebug(() -> "Text drawable " + drawableClass.getName() + + " layout: " + foundLayout + " text: " + foundText); + drawableFields.put(drawableClass, fields); + return fields; + } + + private static boolean isFrameworkClass(Class type) { + String name = type.getName(); + return name.startsWith("java.") || name.startsWith("javax.") + || name.startsWith("android.") || name.startsWith("kotlin."); + } + + /** + * Collects the texts of the host and its child hosts, with the drawables that show them. + */ + private static void collectShownTexts(ComponentHost root, ComponentHost host, List texts) { + List drawables = new ArrayList<>(); + try { + findTextDrawables(host, drawables); + } catch (Exception ex) { + Logger.printInfo(() -> "Could not find the text drawables", ex); + } + + List drawableTexts = new ArrayList<>(); + for (Drawable drawable : drawables) { + List items = ((TextContent) drawable).getTextItems(); + if (items == null || items.isEmpty() || items.get(0) == null) { + continue; + } + CharSequence text = items.get(0); + drawableTexts.add(text); + Rect bounds = new Rect(drawable.getBounds()); + root.offsetDescendantRectToMyCoords(host, bounds); + texts.add(new ShownText(text.toString(), drawable, bounds)); + } + + // Texts whose drawable was not found. + TextContent textContent = host.getTextContent(); + if (textContent != null) { + for (CharSequence text : textContent.getTextItems()) { + if (text != null && !drawableTexts.contains(text)) { + texts.add(new ShownText(text.toString(), null, null)); + } + } + } + + for (int i = 0, count = host.getChildCount(); i < count; i++) { + View child = host.getChildAt(i); + if (child.getVisibility() == View.VISIBLE && child instanceof ComponentHost childHost) { + collectShownTexts(root, childHost, texts); + } + } + } + + /** + * Finds the text drawables mounted in the host. The host keeps its mounted contents + * in obfuscated fields, so the fields are searched for drawables that show texts, + * the nearest fields first. + */ + private static void findTextDrawables(ComponentHost host, List drawables) throws Exception { + Set visited = Collections.newSetFromMap(new IdentityHashMap<>()); + List level = new ArrayList<>(); + addFieldValues(host, ComponentHost.class, level); + + for (int depth = 1; depth <= MAX_FIELD_DEPTH && !level.isEmpty(); depth++) { + List next = new ArrayList<>(); + for (Object object : level) { + if (object == null || object instanceof View || object instanceof Context + || object instanceof CharSequence || object instanceof Number + || object instanceof Class || !visited.add(object)) { + continue; + } + if (visited.size() > MAX_SEARCHED_OBJECTS) { + Logger.printDebug(() -> "Stopped searching the text drawables of a host"); + return; + } + if (object instanceof Drawable drawable) { + // A mounted drawable is drawn by its host. + if (drawable instanceof TextContent && drawable.getCallback() == host + && !drawables.contains(drawable)) { + drawables.add(drawable); + } + } else if (object instanceof Object[] array) { + next.addAll(Arrays.asList(array)); + } else if (object instanceof Iterable iterable) { + for (Object item : iterable) { + next.add(item); + } + } else if (object instanceof Map map) { + next.addAll(map.values()); + } else if (object instanceof SparseArray sparseArray) { + for (int i = 0, size = sparseArray.size(); i < size; i++) { + next.add(sparseArray.valueAt(i)); + } + } else if (!object.getClass().isArray()) { + for (Class type = object.getClass(); type != null && !isFrameworkClass(type); + type = type.getSuperclass()) { + addFieldValues(object, type, next); + } + } + } + level = next; + } + } + + private static void addFieldValues(Object object, Class type, List values) throws Exception { + for (Field field : type.getDeclaredFields()) { + if (Modifier.isStatic(field.getModifiers()) || field.getType().isPrimitive()) { + continue; + } + field.setAccessible(true); + values.add(field.get(object)); + } + } + + /** + * @return A layout of the text with the size of the layout, cut off with an ellipsis if it is longer. + */ + private static Layout createLayout(Layout layout, CharSequence text) { + return StaticLayout.Builder.obtain(text, 0, text.length(), layout.getPaint(), Math.max(1, layout.getWidth())) + .setAlignment(layout.getAlignment()) + .setLineSpacing(layout.getSpacingAdd(), layout.getSpacingMultiplier()) + .setIncludePad(layout.getTopPadding() != 0 || layout.getBottomPadding() != 0) + .setTextDirection(TextDirectionHeuristics.FIRSTSTRONG_LTR) + .setMaxLines(Math.max(1, layout.getLineCount())) + .setEllipsize(TextUtils.TruncateAt.END) + .build(); + } + + /** + * Shows the translation in the drawable. + * + * @return If the translation is shown. + */ + private static boolean showTranslation(Drawable drawable, String key, String translation) { + try { + DrawableFields fields = getDrawableFields(drawable); + if (fields == null) { + return false; + } + Layout layout = (Layout) fields.layout().get(drawable); + CharSequence text = (CharSequence) fields.text().get(drawable); + if (layout == null || text == null) { + return false; + } + TranslatedDrawable translated = translatedDrawables.get(drawable); + if (translated != null && translated.translatedLayout() == layout) { + return true; // Already shown. + } + // The drawable can be used again by another post while the text is translated. + String original = findPostText(text.toString()); + if (original == null || !normalize(original).equals(key)) { + Logger.printDebug(() -> "The drawable shows another text"); + return false; + } + + SpannableString translationText = createTranslationText(text, translation); + Layout translatedLayout = createLayout(layout, translationText); + Map originalSpans = new HashMap<>(); + for (Field spanField : fields.spans()) { + originalSpans.put(spanField, spanField.get(drawable)); + // The links and images of the original text are not in the translation. + spanField.set(drawable, Array.newInstance(spanField.getType().getComponentType(), 0)); + } + fields.layout().set(drawable, translatedLayout); + fields.text().set(drawable, translationText); + translatedDrawables.put(drawable, + new TranslatedDrawable(layout, text, originalSpans, translatedLayout, key)); + drawable.invalidateSelf(); + return true; + } catch (Exception ex) { + Logger.printException(() -> "Could not show the translation", ex); + } + return false; + } + + /** + * Shows the original text in the drawable. + */ + private static void showOriginal(Drawable drawable, String original) { + try { + DrawableFields fields = getDrawableFields(drawable); + if (fields == null) { + return; + } + Layout layout = (Layout) fields.layout().get(drawable); + if (layout == null) { + return; + } + TranslatedDrawable translated = translatedDrawables.remove(drawable); + if (translated != null && translated.translatedLayout() == layout) { + fields.layout().set(drawable, translated.originalLayout()); + fields.text().set(drawable, translated.originalText()); + for (Map.Entry entry : translated.originalSpans().entrySet()) { + entry.getKey().set(drawable, entry.getValue()); + } + } else { + // The translation was created by Litho. + SpannableString originalText = createTranslationText( + (CharSequence) fields.text().get(drawable), original); + fields.layout().set(drawable, createLayout(layout, originalText)); + fields.text().set(drawable, originalText); + } + drawable.invalidateSelf(); + } catch (Exception ex) { + Logger.printException(() -> "Could not show the original text", ex); + } + } + + // endregion + + // region Posts + + /** + * @return The text of the post that contains the view, or null if the post has no text. + */ + @Nullable + private static PostText findPostText(View sender, ComponentHost root) { + // The hosts around the view are checked from the inside out, as a root host + // can show many posts, such as a shelf of posts. + ViewParent parent = sender instanceof ComponentHost host ? host : sender.getParent(); + while (parent instanceof View view) { + if (view instanceof ComponentHost host) { + PostText postText = findPostTextOfHost(host); + if (postText != null || host == root) { + return postText; + } + } + parent = view.getParent(); + } + return null; + } + + /** + * @return The text of the post the host shows, or null if it does not show the text of a post. + */ + @Nullable + private static PostText findPostTextOfHost(ComponentHost root) { + List shownTexts = new ArrayList<>(); + collectShownTexts(root, root, shownTexts); + + List candidates = new ArrayList<>(); + for (ShownText shown : shownTexts) { + String text = shown.text().trim(); + String original; + String key = getOriginalKey(text); + final boolean showsTranslation = key != null; + if (showsTranslation) { + synchronized (postTexts) { + original = postTexts.get(key); + } + if (original == null) { + original = key; + } + } else { + if (!isPostText(normalize(text))) { + continue; + } + original = findPostText(text); + if (original == null || isMetadataText(original)) { + continue; + } + key = normalize(original); + } + candidates.add(new PostText(shown, original, key, showsTranslation)); + } + if (candidates.isEmpty()) { + return null; + } + + // The name of the channel and the date are in the header, above the text of the post. + Rect header = null; + for (ShownText shown : shownTexts) { + Rect bounds = shown.bounds(); + if (bounds != null && LETTER_PATTERN.matcher(shown.text()).find() + && (header == null || bounds.top < header.top)) { + header = bounds; + } + } + + PostText best = null; + for (PostText candidate : candidates) { + Rect bounds = candidate.shown().bounds(); + if (header != null) { + if (bounds == null || bounds.top < header.bottom - Dim.dp(HEADER_TOLERANCE_DP)) { + continue; + } + // The text of the post is the first text below the header, such as above a poll. + //noinspection ConstantConditions + if (best == null || bounds.top < best.shown().bounds().top) { + best = candidate; + } + } else if (best == null || candidate.original().length() > best.original().length()) { + best = candidate; + } + } + + if (BaseSettings.DEBUG.get()) { + for (PostText candidate : candidates) { + final boolean isBest = candidate == best; + Logger.printDebug(() -> (isBest ? "Post text: " : "Other post text: ") + + candidate.shown().bounds() + " drawable: " + (candidate.shown().drawable() != null) + + " text: " + + candidate.original().substring(0, Math.min(40, candidate.original().length()))); + } + } + return best; + } + + /** + * Called by {@link FeedVideoCardsPatch} once a root host was laid out. + * Shows the translations again, as Litho can mount the drawables of the post again. + * Must be called on the main thread. + */ + public static void onRootLaidOut(ComponentHost root) { + if (!ENABLED || !hasTranslations()) { + return; + } + try { + if (!showsTranslatedText(root)) { + return; + } + List shownTexts = new ArrayList<>(); + collectShownTexts(root, root, shownTexts); + for (ShownText shown : shownTexts) { + if (shown.drawable() == null || getOriginalKey(shown.text()) != null) { + continue; + } + String original = findPostText(shown.text()); + if (original == null) { + continue; + } + String key = normalize(original); + String translation = getTranslation(key); + if (translation != null) { + showTranslation(shown.drawable(), key, translation); + } + } + } catch (Exception ex) { + Logger.printException(() -> "onRootLaidOut failure", ex); + } + } + + /** + * @return If the host or its child hosts show an original text that has a translation. + */ + private static boolean showsTranslatedText(ComponentHost host) { + TextContent textContent = host.getTextContent(); + if (textContent != null) { + for (CharSequence text : textContent.getTextItems()) { + if (text == null || text.length() < MIN_TEXT_LENGTH) { + continue; + } + String original = findPostText(text.toString()); + if (original != null && getTranslation(normalize(original)) != null) { + return true; + } + } + } + for (int i = 0, count = host.getChildCount(); i < count; i++) { + if (host.getChildAt(i) instanceof ComponentHost child && showsTranslatedText(child)) { + return true; + } + } + return false; + } + + // endregion + + // region Menu + + @Nullable + private static Drawable getTranslateIcon() { + for (String name : TRANSLATE_ICONS) { + if (ResourceUtils.getDrawableIdentifier(name) != 0) { + return ResourceUtils.getDrawable(name); + } + } + return null; + } + + /** + * Adds Translate or Show original to the menu of a post with a text. + * + * @param sender The view that opened the menu. + */ + public static int addFlyoutButton(Object flyoutPanel, int index, @Nullable View sender) { + if (!ENABLED || sender == null) { + return index; + } + try { + ComponentHost root = FeedVideoCardsPatch.findTappedRootHost(sender); + if (root == null) { + return index; + } + PostText postText = findPostText(sender, root); + if (postText == null) { + return index; + } + + final boolean showsTranslation = postText.showsTranslation(); + final int nextIndex = FlyoutUtils.addFlyoutButton( + flyoutPanel, + getTranslateIcon(), + str(showsTranslation + ? "morphe_post_translate_flyout_original" + : "morphe_post_translate_flyout"), + v -> { + FlyoutUtils.dismissFlyout(); + if (showsTranslation) { + showOriginal(postText); + } else { + translate(postText, sender.getContext()); + } + }, + index + ); + return nextIndex < 0 ? index : nextIndex; + } catch (Exception ex) { + Logger.printException(() -> "addFlyoutButton failure", ex); + } + return index; + } + + // endregion + + // region Translation + + /** + * @return The language the texts are translated to, as a Google Translate language code. + */ + private static String getTargetLanguage() { + String language; + if (Settings.POST_TRANSLATE_LANGUAGE.isSetToDefault()) { + Configuration configuration = Utils.getContext().getResources().getConfiguration(); + Locale locale = configuration.getLocales().isEmpty() + ? Locale.getDefault() + : configuration.getLocales().get(0); + language = getLanguageCode(locale); + } else { + language = Settings.POST_TRANSLATE_LANGUAGE.get(); + } + return switch (language.toLowerCase(Locale.ROOT)) { + case "fil" -> "tl"; + case "nb", "nn" -> "no"; + case "in" -> "id"; + case "iw" -> "he"; + case "ji" -> "yi"; + default -> language; + }; + } + + private static String getLanguageCode(Locale locale) { + String language = locale.getLanguage(); + if ("zh".equals(language)) { + String country = locale.getCountry(); + return "Hant".equals(locale.getScript()) || "TW".equals(country) + || "HK".equals(country) || "MO".equals(country) ? "zh-TW" : "zh-CN"; + } + if ("pt".equals(language) && "PT".equals(locale.getCountry())) { + return "pt-PT"; + } + return language.isEmpty() ? "en" : language; + } + + private static void translate(PostText postText, Context context) { + final String key = postText.key(); + String translation = getTranslation(key); + if (translation != null) { + showTranslation(postText, translation, context); + return; + } + if (!pendingTranslations.add(key)) { + return; + } + Utils.showToastShort(str("morphe_post_translate_started")); + + final String original = postText.original(); + final String language = getTargetLanguage(); + Utils.runOnBackgroundThread(() -> { + String result = null; + try { + List lines = Arrays.asList(original.split("\n", -1)); + StringBuilder builder = new StringBuilder(); + for (List batch : TextTranslator.splitByCharacterBudget( + lines, TextTranslator.MAXIMUM_BATCH_CHARACTERS)) { + for (String line : TextTranslator.translate(batch, language)) { + //noinspection SizeReplaceableByIsEmpty + if (builder.length() > 0) { + builder.append('\n'); + } + builder.append(line); + } + } + result = builder.toString().trim(); + } catch (Exception ex) { + Logger.printInfo(() -> "Could not translate the post to: " + language, ex); + } + + final String translated = result; + Utils.runOnMainThread(() -> { + pendingTranslations.remove(key); + if (translated == null || translated.isEmpty()) { + Utils.showToastShort(str("morphe_post_translate_failed")); + openTranslationApp(context, original); + return; + } + if (normalize(translated).equals(key)) { + Logger.printDebug(() -> "Translation to " + language + " is the same text: " + key); + Utils.showToastShort(str("morphe_post_translate_same_language")); + return; + } + synchronized (translations) { + String previous = translations.put(key, translated); + if (previous != null) { + originals.remove(normalize(previous)); + } + originals.put(normalize(translated), key); + // The oldest translations are removed from the size restricted map. + originals.values().retainAll(translations.keySet()); + } + showTranslation(postText, translated, context); + }); + }); + } + + private static void showTranslation(PostText postText, String translation, Context context) { + Drawable drawable = postText.shown().drawable(); + if (drawable == null || getDrawableFields(drawable) == null) { + Logger.printDebug(() -> "The translation can not be shown in place"); + showTranslationDialog(context, translation); + return; + } + // If the post is not shown anymore, the translation is shown once it is laid out again. + showTranslation(drawable, postText.key(), translation); + } + + private static void showOriginal(PostText postText) { + final String key = postText.key(); + synchronized (translations) { + String translation = translations.remove(key); + if (translation != null) { + originals.remove(normalize(translation)); + } + } + Drawable drawable = postText.shown().drawable(); + if (drawable != null) { + showOriginal(drawable, postText.original()); + } + // Other drawables that show the translation, such as the same post in another feed. + for (Map.Entry entry : new ArrayList<>(translatedDrawables.entrySet())) { + if (entry.getValue().key().equals(key)) { + showOriginal(entry.getKey(), postText.original()); + } + } + } + + private static void showTranslationDialog(Context context, String translation) { + try { + Pair dialogPair = CustomDialog.create( + context, + str("morphe_post_translate_flyout"), // Title. + translation, // Message. + null, // No EditText. + null, // OK button text. + () -> {}, // OK button action (dismiss only). + null, // No cancel button. + null, // No neutral button. + null, // No neutral button action. + true // Dismiss dialog when onNeutralClick. + ); + dialogPair.first.show(); + } catch (Exception ex) { + Logger.printException(() -> "Could not show the translation", ex); + } + } + + /** + * Sends the text to the translation app of the device, or else to any app that takes text. + */ + private static void openTranslationApp(Context context, String text) { + Intent processText = new Intent(Intent.ACTION_PROCESS_TEXT) + .setType("text/plain") + .putExtra(Intent.EXTRA_PROCESS_TEXT, text) + .putExtra(Intent.EXTRA_PROCESS_TEXT_READONLY, true) + .setPackage(GOOGLE_TRANSLATE_PACKAGE) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + try { + context.startActivity(processText); + return; + } catch (ActivityNotFoundException ex) { + Logger.printDebug(() -> "Google Translate is not installed"); + } catch (Exception ex) { + Logger.printInfo(() -> "Could not open Google Translate", ex); + } + + try { + Intent send = new Intent(Intent.ACTION_SEND) + .setType("text/plain") + .putExtra(Intent.EXTRA_TEXT, text); + context.startActivity(Intent.createChooser(send, str("morphe_post_translate_flyout")) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); + } catch (Exception ex) { + Logger.printException(() -> "Could not send the text to a translation app", ex); + } + } + + // endregion +} diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/FeedVideoCardsPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/FeedVideoCardsPatch.java index 5cb2e7b600..ba338a8cd1 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/FeedVideoCardsPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/FeedVideoCardsPatch.java @@ -45,6 +45,7 @@ import app.morphe.extension.youtube.patches.FeedVideoActionsPatch; import app.morphe.extension.youtube.patches.FeedViewCountPatch; import app.morphe.extension.youtube.patches.PostImageDownloadPatch; +import app.morphe.extension.youtube.patches.PostTranslatePatch; import app.morphe.extension.youtube.patches.VideoInformation; import app.morphe.extension.youtube.settings.Settings; import app.morphe.extension.youtube.shared.PlayerType; @@ -68,7 +69,8 @@ public final class FeedVideoCardsPatch { private static final boolean ENABLED = FeedEngagementCountsPatch.ENABLED || FeedViewCountPatch.ENABLED || Settings.FEED_WATCH_LATER_BUTTON.get() - || PostImageDownloadPatch.ENABLED; + || PostImageDownloadPatch.ENABLED + || PostTranslatePatch.ENABLED; /** * A card must be as wide as a card of a grid of up to three columns, such as the feed in landscape. @@ -906,6 +908,7 @@ public static boolean scheduleRetry(ComponentHost root) { FeedViewCountPatch.onCardLaidOut(root, cardVideoId); FeedVideoActionsPatch.onCardLaidOut(root, cardVideoId); PostImageDownloadPatch.onRootLaidOut(root); + PostTranslatePatch.onRootLaidOut(root); } } catch (Exception ex) { Logger.printException(() -> "updateRoots failure", ex); diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/FlyoutUtils.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/FlyoutUtils.java index 606a86e9e7..595d6d8341 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/FlyoutUtils.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/FlyoutUtils.java @@ -63,6 +63,7 @@ import app.morphe.extension.youtube.patches.LocalQueuePatch; import app.morphe.extension.youtube.patches.PictureinPictureButtonPatch; import app.morphe.extension.youtube.patches.PostImageDownloadPatch; +import app.morphe.extension.youtube.patches.PostTranslatePatch; import app.morphe.extension.youtube.patches.SaveToWatchLaterPatch; import app.morphe.extension.youtube.patches.VideoDelayPatch; import app.morphe.extension.youtube.patches.VideoInformation; @@ -630,6 +631,15 @@ private static int addVideoFlyoutButtons(Object flyoutPanel, int nextButtonIndex ); } + // Only added to the menus of posts with a text. + if (PostTranslatePatch.ENABLED) { + nextButtonIndex = PostTranslatePatch.addFlyoutButton( + flyoutPanel, + nextButtonIndex, + senderViewRef.get() + ); + } + // region to show the following buttons only for specific flyout menus. final String allButtonsVideoId; if (!flyoutVideoId.isEmpty()) { diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/Settings.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/Settings.java index cc941914a9..7f170f9c51 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/Settings.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/Settings.java @@ -368,7 +368,11 @@ public class Settings extends SharedYouTubeSettings { public static final BooleanSetting VIDEO_QUALITY_DIALOG_BUTTON_RESOLUTION = new BooleanSetting("morphe_video_quality_dialog_button_resolution", FALSE, parent(VIDEO_QUALITY_DIALOG_BUTTON)); // Post image download - public static final BooleanSetting POST_IMAGE_DOWNLOAD = new BooleanSetting("morphe_post_image_download", TRUE); + public static final BooleanSetting POST_IMAGE_DOWNLOAD = new BooleanSetting("morphe_post_image_download", TRUE, true); + + // Post translate + public static final BooleanSetting POST_TRANSLATE = new BooleanSetting("morphe_post_translate", TRUE, true); + public static final StringSetting POST_TRANSLATE_LANGUAGE = new StringSetting("morphe_post_translate_language", "default", parent(POST_TRANSLATE)); // Feed view count public static final BooleanSetting FEED_VIEW_COUNT = new BooleanSetting("morphe_feed_view_count", FALSE, true); diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/posttranslate/PostTranslatePatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/posttranslate/PostTranslatePatch.kt new file mode 100644 index 0000000000..400107cb62 --- /dev/null +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/posttranslate/PostTranslatePatch.kt @@ -0,0 +1,62 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/4163 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.patches.youtube.interaction.posttranslate + +import app.morphe.patcher.patch.bytecodePatch +import app.morphe.patches.all.misc.resources.addResourcesPatch +import app.morphe.patches.shared.misc.proto.hookElement +import app.morphe.patches.shared.misc.settings.preference.ListPreference +import app.morphe.patches.shared.misc.settings.preference.SwitchPreference +import app.morphe.patches.shared.misc.textcomponent.hookLithoSpannableString +import app.morphe.patches.shared.misc.textcomponent.lithoSpannableStringPatch +import app.morphe.patches.youtube.layout.flyout.flyoutPatch +import app.morphe.patches.youtube.misc.extension.sharedExtensionPatch +import app.morphe.patches.youtube.misc.feedcards.feedVideoCardsHookPatch +import app.morphe.patches.youtube.misc.proto.elementProtoParserHookPatch +import app.morphe.patches.youtube.misc.settings.PreferenceScreen +import app.morphe.patches.youtube.misc.settings.settingsPatch +import app.morphe.patches.youtube.shared.Constants.COMPATIBILITY_YOUTUBE +import app.morphe.util.setExtensionIsPatchIncluded + +private const val EXTENSION_CLASS = + "Lapp/morphe/extension/youtube/patches/PostTranslatePatch;" + +@Suppress("unused") +val postTranslatePatch = bytecodePatch( + name = "Translate posts", + description = "Adds a menu item to translate the text of posts.", +) { + dependsOn( + sharedExtensionPatch, + settingsPatch, + addResourcesPatch, + elementProtoParserHookPatch, + lithoSpannableStringPatch, + // Adds the item to the menu of the post. + flyoutPatch, + // Shows the translation again when the post is laid out again. + feedVideoCardsHookPatch, + ) + + compatibleWith(COMPATIBILITY_YOUTUBE) + + execute { + PreferenceScreen.FEED.addPreferences( + SwitchPreference("morphe_post_translate", summary = true), + ListPreference("morphe_post_translate_language"), + ) + + // The element of a post includes its whole text, while a long text is shown cut off. + hookElement("$EXTENSION_CLASS->onElementLoaded") + + // Texts that Litho creates again are replaced with their translation. + hookLithoSpannableString(EXTENSION_CLASS) + + setExtensionIsPatchIncluded(EXTENSION_CLASS) + } +} diff --git a/patches/src/main/resources/addresources/values/youtube/arrays.xml b/patches/src/main/resources/addresources/values/youtube/arrays.xml index a70b226b84..9c228c3599 100644 --- a/patches/src/main/resources/addresources/values/youtube/arrays.xml +++ b/patches/src/main/resources/addresources/values/youtube/arrays.xml @@ -636,6 +636,168 @@ zh-TW + + + @string/morphe_language_DEFAULT + 한국어 + English + 日本語 + Afrikaans + Azərbaycan + Bahasa Melayu + Basa Jawa + Basa Sunda + Bokmål + Български + Bosanski + Català + Čeština + Српски + Cymraeg + Dansk + Deutsch + Eesti + Ελληνικά + Español + Filipino + Français + Gaeilge + Galego + Hrvatski + Indonesia + isiZulu + Íslenska + Italiano + Kiswahili + Қазақша + Latviešu + Lietuvių + Македонски + Magyar + Malti + Монгол + Nederlands + Oʻzbekcha + Polski + Português (Brasil) + Português (Portugal) + Română + Shqip + Slovenčina + Slovenščina + Soomaali + Suomi + Svenska + Tiếng Việt + Türkçe + Русский + Українська + አማርኛ + বাংলা + ગુજરાતી + हिन्दी + ᐃᓄᒃᑎᑐᑦ + ქართული + ភាសាខ្មែរ + ಕನ್ನಡ + ລາວ + മലയാളം + मराठी + မြန်မာစာ + नेपाली + සිංහල + தமிழ் + తెలుగు + ไทย + 中文 (简体) + 中文 (繁體) + العربية + فارسی + עברית + پښتو + اردو + + + default + ko + en + ja + af + az + ms + jv + su + nb + bg + bs + ca + cs + sr + cy + da + de + et + el + es + fil + fr + ga + gl + hr + id + zu + is + it + sw + kk + lv + lt + mk + hu + mt + mn + nl + uz + pl + pt-BR + pt-PT + ro + sq + sk + sl + so + fi + sv + vi + tr + ru + uk + am + bn + gu + hi + iu + ka + km + kn + lo + ml + mr + my + ne + si + ta + te + th + zh-CN + zh-TW + ar + fa + he + ps + ur + + @string/morphe_open_videos_fullscreen_entry_1 diff --git a/patches/src/main/resources/addresources/values/youtube/strings.xml b/patches/src/main/resources/addresources/values/youtube/strings.xml index 8e3f21997d..7d3becde0a 100644 --- a/patches/src/main/resources/addresources/values/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values/youtube/strings.xml @@ -490,6 +490,16 @@ Reports are public and are reviewed before a channel is added to a list. Removal Could not download the image No image found in this post + + Translate posts + Adds a flyout menu item to translate content creator posts + Post translation language + Translate + Show original + Translating… + Could not translate the post + The post is already in the translation language + Add Ask item to flyout menu Adds a video summarize item to the feed flyout menu From 82103d36cacc93eeaed307f75975c492fb610b24 Mon Sep 17 00:00:00 2001 From: ILoveOpenSourceApplications <117499019+ILoveOpenSourceApplications@users.noreply.github.com> Date: Fri, 30 Oct 2026 00:18:54 +0530 Subject: [PATCH 169/189] fix(YouTube - Navigation bar): Navigation bar transluency not working (#4156) --- .../youtube/patches/FixPagePaddingPatch.java | 100 +++++++++++++++++ .../youtube/patches/NavigationBarPatch.java | 62 ++--------- .../layout/buttons/navigation/Fingerprints.kt | 71 +----------- .../buttons/navigation/NavigationBarPatch.kt | 101 ++---------------- .../youtube/misc/fix/padding/Fingerprints.kt | 74 +++++++++++-- .../misc/fix/padding/FixPagePaddingPatch.kt | 79 +++++++++++++- 6 files changed, 262 insertions(+), 225 deletions(-) create mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FixPagePaddingPatch.java diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FixPagePaddingPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FixPagePaddingPatch.java new file mode 100644 index 0000000000..ec5066c8f6 --- /dev/null +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FixPagePaddingPatch.java @@ -0,0 +1,100 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to Morphe contributions. + */ + +package app.morphe.extension.youtube.patches; + +import android.app.Activity; +import android.os.Build; +import android.view.View; +import android.view.WindowInsets; + +import app.morphe.extension.shared.Logger; +import app.morphe.extension.shared.Utils; +import app.morphe.extension.youtube.shared.PlayerType; + +@SuppressWarnings("unused") +public final class FixPagePaddingPatch { + + private static volatile boolean keyboardMarginRemoved; + + public static boolean isKeyboardMarginRemoved() { + return keyboardMarginRemoved; + } + + /** + * Injection point. + *

    + * Sets the bottom margin of the layout that holds the page, the player and the navigation bar + * while the keyboard is open and a text field is focused. + * With edge-to-edge / translucent navigation bar flags enabled, the app calculates the keyboard + * height and sets it as the bottom margin of the watch while coordinator layout. This shrinks + * the layout and moves the bottom navigation bar up on top of the soft keyboard. + * Replacing the keyboard height with no margin (0) keeps the navigation bar at the bottom + * behind the keyboard, except while the legacy miniplayer is shown. + * + * @param original The keyboard height the app computed for the margin. + * @return 0 to keep the layout at the bottom, or the keyboard height if legacy miniplayer is active. + */ + public static int getKeyboardMargin(int original) { + try { + if (PlayerType.getCurrent() == PlayerType.WATCH_WHILE_MINIMIZED + && !MiniplayerPatch.getCurrentMiniplayerType().isModern() + && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + Activity activity = Utils.getActivity(); + WindowInsets insets = activity == null + ? null + : activity.getWindow().getDecorView().getRootWindowInsets(); + if (insets != null) { + return Math.max(original, insets.getInsets(WindowInsets.Type.ime()).bottom); + } + } + } catch (Exception ex) { + Logger.printException(() -> "getKeyboardMargin failure", ex); + } + + keyboardMarginRemoved = true; + return 0; + } + + /** + * Injection point. + *

    + * When a feed or browse list is laid out behind the navigation bar or window insets are applied, + * the bottom padding of the list holds the bottom window inset. With the soft keyboard open, + * that inset holds the keyboard too, so the list padding grows by the keyboard height. + * At the same time, the page layout margin may also account for the keyboard, or the list + * cannot scroll, causing the top items (such as the watch history search bar and shelf) + * to be squished and hidden. The keyboard height is removed from the bottom padding. + * + * @param view The RecyclerView of the feed/browse list. + * @param bottomPadding The computed bottom padding before adjustment. + * @return The adjusted bottom padding with keyboard height subtracted if applicable. + */ + public static int getAdjustedFeedListBottomPadding(View view, int bottomPadding) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { + return bottomPadding; + } + + try { + WindowInsets insets = view.getRootWindowInsets(); + if (insets != null) { + int imeBottom = insets.getInsets(WindowInsets.Type.ime()).bottom; + if (bottomPadding >= imeBottom && imeBottom > 0) { + int systemBarsBottom = insets.getInsets(WindowInsets.Type.systemBars()).bottom; + int keyboardHeight = imeBottom - systemBarsBottom; + if (keyboardHeight > 0) { + return bottomPadding - keyboardHeight; + } + } + } + } catch (Exception ex) { + Logger.printException(() -> "getAdjustedFeedListBottomPadding failure", ex); + } + + return bottomPadding; + } +} diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/NavigationBarPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/NavigationBarPatch.java index 9e14b5fb4d..cd18260b3c 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/NavigationBarPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/NavigationBarPatch.java @@ -70,7 +70,6 @@ import app.morphe.extension.youtube.patches.spoof.SpoofOSNamePatch; import app.morphe.extension.youtube.settings.Settings; import app.morphe.extension.youtube.shared.NavigationBar; -import app.morphe.extension.youtube.shared.PlayerType; @SuppressWarnings("unused") public final class NavigationBarPatch { @@ -232,11 +231,11 @@ public static void hideNavigationBar(View view) { * and the pages make room for the system navigation buttons, which is only right when * the window is drawn behind them. That comes from the edge to edge top bar feature flag, * so without it the room is added twice: the navigation bar is raised and an empty bar is left - * at the bottom (https://github.com/MorpheApp/morphe-patches/issues/3825). + * at the bottom (...). *

    * The app draws the window behind the system navigation buttons only on Android 12 and later, * so on older versions the flags are always turned off, or the room is added twice - * (https://github.com/MorpheApp/morphe-patches/issues/3864). + * (...). *

    * With {@link NavigationBarStyle#DEFAULT} the flags are left to the account. */ @@ -284,7 +283,7 @@ public static boolean isTranslucentNavigationBarForced() { * With the navigation bar covering the system navigation buttons, the room below the bar fixed * at the bottom of some pages, such as the reply bar of the comment replies, already holds * the system navigation buttons. The flag adds the navigation bar height to it as well, so an - * empty band is left between the two bars (https://github.com/MorpheApp/morphe-patches/issues/2300). + * empty band is left between the two bars (...). */ public static boolean useFixedBottomBarRoom(boolean original) { return original && !translucentNavigationBarForced; @@ -336,7 +335,7 @@ public static boolean isTranslucentNavigationBarBackgroundForced() { * surface is drawn twice in every frame, at two positions. Android 12 and 13 send the two * positions to the surface from two threads at the same time, and the app crashes while * the player moves, such as when it is slowly dragged down to the miniplayer - * (https://github.com/MorpheApp/morphe-patches/pull/3684#issuecomment-6322984709). + * (...). * With {@link NavigationBarStyle#TRANSPARENT} and {@link NavigationBarStyle#OPAQUE} the copy * is not drawn, so the bar keeps its translucent color, without the blur. * @@ -349,64 +348,23 @@ public static boolean useNavigationBarBlur(boolean hardwareAccelerated) { } /** - * If the keyboard height was ever removed from the bottom margin of the layout that holds - * the page, the player and the navigation bar, so the keyboard covers them. - */ - private static volatile boolean keyboardMarginRemoved; - - /** - * Injection point. - *

    - * The bottom margin of the layout that holds the page, the player and the navigation bar while - * the keyboard is open. With the feature flags turned on by - * {@link #useTranslucentNavigationBar(boolean)}, the keyboard height is removed from it, so - * the keyboard covers them again. - *

    - * The legacy miniplayer, such as the minimal type before 21.29, rests at the bottom of that - * layout, so while it is shown the layout ends at the top of the keyboard instead - * (https://github.com/MorpheApp/morphe-patches/issues/4003). The margin of the app leaves - * the height of the system navigation buttons for the navigation bar, which is hidden while - * the keyboard is open, so the full keyboard height is used. The other miniplayers are moved - * above the keyboard by {@link MinimalMiniplayerPatch}. - * - * @param original The keyboard height the app uses as the margin. + * @deprecated Handled by {@link FixPagePaddingPatch#getKeyboardMargin(int)}. */ + @Deprecated public static int getKeyboardMargin(int original) { - if (!translucentNavigationBarForced) { - return original; - } - - try { - if (PlayerType.getCurrent() == PlayerType.WATCH_WHILE_MINIMIZED - && !MiniplayerPatch.getCurrentMiniplayerType().isModern() - && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - Activity activity = Utils.getActivity(); - WindowInsets insets = activity == null - ? null - : activity.getWindow().getDecorView().getRootWindowInsets(); - if (insets != null) { - return Math.max(original, insets.getInsets(WindowInsets.Type.ime()).bottom); - } - } - } catch (Exception ex) { - Logger.printException(() -> "getKeyboardMargin failure", ex); - } - - keyboardMarginRemoved = true; - return 0; + return FixPagePaddingPatch.getKeyboardMargin(original); } static boolean isKeyboardMarginRemoved() { - return keyboardMarginRemoved; + return FixPagePaddingPatch.isKeyboardMarginRemoved(); } - /** * Injection point. *

    * When the navigation bar covers the system navigation buttons, the bottom window inset is * added to its height and bottom padding. With the keyboard open, that inset also holds * the keyboard, so the bar grows as tall as the keyboard and covers the page - * (https://github.com/MorpheApp/morphe-patches/issues/3909). The page above then ends + * (...). The page above then ends * the height of the system bars below the top of the keyboard, so only that much room is left. * Only with the feature flags turned on by {@link #useTranslucentNavigationBar(boolean)}. */ @@ -535,7 +493,7 @@ public static void watchSlimStatusBar(View bottomBarContainer) { * navigation buttons, the navigation bar makes room for them, and the pages end above * the navigation bar. With the navigation bar hidden in the Shorts player, nothing makes that * room, so the Shorts player is drawn behind the system navigation buttons - * (https://github.com/MorpheApp/morphe-patches/issues/3908). The Shorts player then makes + * (...). The Shorts player then makes * the room itself. *

    * The room the Shorts player reserves for the navigation bar depends on how it's opened, diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/buttons/navigation/Fingerprints.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/buttons/navigation/Fingerprints.kt index 25531be44a..51fed063ce 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/buttons/navigation/Fingerprints.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/buttons/navigation/Fingerprints.kt @@ -148,7 +148,8 @@ internal object CommentReplyPaddingFeatureFlagFingerprint : Fingerprint( */ internal val TRANSLUCENT_NAVIGATION_BAR_FEATURE_FLAGS = listOf( 45630927L, // Blurred, translucent background of the navigation bar. - NAVIGATION_BAR_COVERS_SYSTEM_BUTTONS_FLAG + NAVIGATION_BAR_COVERS_SYSTEM_BUTTONS_FLAG, + 45631257L // Feed laid out behind the navigation bar instead of above it. ) /** @@ -191,74 +192,6 @@ internal object NavigationBarBottomPaddingFingerprint : Fingerprint( ) ) -/** - * Sets the bottom margin of the layout that holds the page and the navigation bar. - * With the keyboard open and a text field focused, the margin is the keyboard height, - * otherwise the bottom of the window insets. - */ -internal object PageBottomMarginFingerprint : Fingerprint( - returnType = "V", - parameters = listOf("Landroid/graphics/Rect;"), - filters = listOf( - methodCall(smali = "Landroid/view/View;->findFocus()Landroid/view/View;"), - methodCall( - smali = "Landroid/view/View;->onCheckIsTextEditor()Z", - location = MatchAfterWithin(5) - ), - fieldAccess( - opcode = Opcode.IGET, - definingClass = "Landroid/graphics/Rect;", - name = "bottom" - ) - ) -) - -/** - * Sets the padding of a feed list. When the feed is laid out behind the navigation bar, - * the bottom padding holds the navigation bar height and the bottom window inset. - */ -internal object FeedListPaddingFingerprint : Fingerprint( - returnType = "V", - parameters = listOf(), - filters = listOf( - methodCall( - definingClass = "Landroid/support/v7/widget/RecyclerView;", - name = "setClipToPadding" - ), - methodCall( - definingClass = "Landroid/support/v7/widget/RecyclerView;", - name = "setPadding", - location = MatchAfterImmediately() - ) - ) -) - -/** - * Sets the bottom padding of the list of a browse page laid out behind the navigation bar, - * to the navigation bar height plus the bottom window inset, once the window insets are known. - */ -internal object BrowseListBottomPaddingFingerprint : Fingerprint( - returnType = "V", - parameters = listOf("Ljava/lang/Object;"), - filters = listOf( - methodCall( - definingClass = "Landroid/support/v7/widget/RecyclerView;", - name = "getPaddingRight" - ), - fieldAccess( - opcode = Opcode.IGET, - definingClass = "Landroid/graphics/Rect;", - name = "bottom", - location = MatchAfterWithin(6) - ), - methodCall( - definingClass = "Landroid/support/v7/widget/RecyclerView;", - name = "setPadding", - location = MatchAfterWithin(3) - ) - ) -) - /** * Draws the blurred background of the translucent navigation bar. If the canvas is hardware * accelerated, a copy of the watch layout, with the player, is drawn into a blurred render node, diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/buttons/navigation/NavigationBarPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/buttons/navigation/NavigationBarPatch.kt index beb29c8418..2a01f7152e 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/buttons/navigation/NavigationBarPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/buttons/navigation/NavigationBarPatch.kt @@ -40,6 +40,7 @@ import app.morphe.patches.youtube.misc.contexthook.Endpoint import app.morphe.patches.youtube.misc.contexthook.addOSNameHook import app.morphe.patches.youtube.misc.contexthook.clientContextHookPatch import app.morphe.patches.youtube.misc.extension.sharedExtensionPatch +import app.morphe.patches.youtube.misc.fix.padding.BrowseListBottomPaddingFingerprint import app.morphe.patches.youtube.misc.navigation.addBottomBarContainerHook import app.morphe.patches.youtube.misc.navigation.hookNavigationButtonCreated import app.morphe.patches.youtube.misc.navigation.navigationBarHookPatch @@ -57,14 +58,12 @@ import app.morphe.util.containsLiteralInstruction import app.morphe.util.findElementByAttributeValueOrThrow import app.morphe.util.fiveRegisters import app.morphe.util.indexOfFirstInstructionOrThrow -import app.morphe.util.indexOfFirstInstructionReversedOrThrow import app.morphe.util.insertLiteralOverride import app.morphe.util.removeFromParent import app.morphe.util.setExtensionIsPatchIncluded import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation -import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction21t import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction @@ -224,12 +223,12 @@ val navigationBarPatch = bytecodePatch( // they break. Every fix checks that the flags are turned on by this patch, so the fixes do // nothing if the flags are left to the account, as with the default navigation bar style. - // The translucent navigation bar comes from two feature flags that not every account + // The translucent navigation bar comes from three feature flags that not every account // gets, so they are turned on to make the bar look the same on every account, and the opaque // style below paints over the same translucent bar everywhere. The flag that makes the bar // cover the system navigation buttons is turned on only when the window is drawn edge to - // edge. The blur does not change the room left for the system - // navigation buttons, so it is also turned on without it. + // edge. The blur and the feed behind the bar do not change the room left for the system + // navigation buttons, so they are also turned on without it. TranslucentNavigationBarFeatureFlagsFingerprint.matchAll().forEach { match -> match.method.apply { TRANSLUCENT_NAVIGATION_BAR_FEATURE_FLAGS.forEach { flag -> @@ -329,99 +328,15 @@ val navigationBarPatch = bytecodePatch( // below it while it is shown. addBottomBarContainerHook("$EXTENSION_CLASS->watchSlimStatusBar(Landroid/view/View;)V") - // With the translucent navigation bar, the keyboard shrinks the layout that holds the page - // and the navigation bar to the top of the keyboard, so the navigation bar is moved above - // the keyboard. Without it, the keyboard covers the bottom of the page and the navigation bar - // stays at the bottom of the screen. The keyboard height is replaced with no margin, - // so the keyboard covers them again, except while the legacy miniplayer is shown. - PageBottomMarginFingerprint.let { - it.method.apply { - // The keyboard height and the window inset are put in the same register, - // and the window inset is read right after the branch of the keyboard height. - val insetIndex = it.instructionMatches.last().index - val marginRegister = getInstruction(insetIndex).registerA - val keyboardBranchIndex = indexOfFirstInstructionReversedOrThrow(insetIndex) { - opcode == Opcode.IF_EQZ && - (this as BuilderInstruction21t).target.location.index == insetIndex - } - val keyboardRegister = getInstruction(keyboardBranchIndex).registerA - - addInstructionsAtControlFlowLabel( - insetIndex + 1, - """ - if-eqz v$keyboardRegister, :keyboard_hidden - invoke-static/range { v$marginRegister .. v$marginRegister }, $EXTENSION_CLASS->getKeyboardMargin(I)I - move-result v$marginRegister - :keyboard_hidden - nop - """ - ) - } - } - - // When the feed is laid out behind the navigation bar, the bottom padding of its list also - // holds the bottom window inset. With the keyboard open, that inset holds the keyboard too, - // so the padding grows, while the keyboard leaves the page as it is. - // While the page loads, as after tapping the search field of the watch history, the list - // does not scroll and its items only get the height left above the padding. With a tall - // keyboard nothing is left, and the items are hidden. The keyboard is removed from - // the padding, as for the navigation bar above. - FeedListPaddingFingerprint.let { - it.method.apply { - val index = it.instructionMatches.last().index - val paddingInstruction = getInstruction(index) - val listRegister = paddingInstruction.registerC - val bottomPaddingRegister = paddingInstruction.registerG - val freeRegisters = getFreeRegisterProvider(index, 3) - val insetsRegister = freeRegisters.getFreeRegister4Bit() - val imeRegister = freeRegisters.getFreeRegister4Bit() - val systemBarsRegister = freeRegisters.getFreeRegister4Bit() - - addInstructionsWithLabels( - index, - $$""" - invoke-static { }, $$EXTENSION_CLASS->isTranslucentNavigationBarBackgroundForced()Z - move-result v$$imeRegister - if-eqz v$$imeRegister, :keep_padding - # The inset types are only on Android 11 and later. - sget v$$imeRegister, Landroid/os/Build$VERSION;->SDK_INT:I - const/16 v$$systemBarsRegister, 0x1e - if-lt v$$imeRegister, v$$systemBarsRegister, :keep_padding - invoke-virtual { v$$listRegister }, Landroid/view/View;->getRootWindowInsets()Landroid/view/WindowInsets; - move-result-object v$$insetsRegister - if-eqz v$$insetsRegister, :keep_padding - # The keyboard inset is empty when the keyboard is hidden. - invoke-static { }, Landroid/view/WindowInsets$Type;->ime()I - move-result v$$imeRegister - invoke-virtual { v$$insetsRegister, v$$imeRegister }, Landroid/view/WindowInsets;->getInsets(I)Landroid/graphics/Insets; - move-result-object v$$imeRegister - iget v$$imeRegister, v$$imeRegister, Landroid/graphics/Insets;->bottom:I - # The padding does not hold the bottom inset, as when the feed is not behind the navigation bar. - if-lt v$$bottomPaddingRegister, v$$imeRegister, :keep_padding - invoke-static { }, Landroid/view/WindowInsets$Type;->systemBars()I - move-result v$$systemBarsRegister - invoke-virtual { v$$insetsRegister, v$$systemBarsRegister }, Landroid/view/WindowInsets;->getInsets(I)Landroid/graphics/Insets; - move-result-object v$$systemBarsRegister - iget v$$systemBarsRegister, v$$systemBarsRegister, Landroid/graphics/Insets;->bottom:I - # Keyboard height above the system bars. - sub-int/2addr v$$imeRegister, v$$systemBarsRegister - if-lez v$$imeRegister, :keep_padding - sub-int/2addr v$$bottomPaddingRegister, v$$imeRegister - :keep_padding - nop - """ - ) - } - } - // When a browse page is laid out behind the navigation bar, its list is set to draw into // its padding, and the padding is set later, once the window insets are known. If the list // is refreshed in between, as when coming back from the Shorts player, it's set to clip to // its padding again, so the bottom padding is left as an empty band between the feed and // the navigation bar. The list is set to draw into its padding again with the padding. - BrowseListBottomPaddingFingerprint.let { - it.method.apply { - val index = it.instructionMatches.last().index + BrowseListBottomPaddingFingerprint.apply { + clearMatch() + method.apply { + val index = instructionMatches.last().index val listRegister = getInstruction(index).registerC val freeRegister = getFreeRegisterProvider(index + 1, 1).getFreeRegister4Bit() diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/misc/fix/padding/Fingerprints.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/fix/padding/Fingerprints.kt index 6eb5f832de..fcd7acdb21 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/misc/fix/padding/Fingerprints.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/fix/padding/Fingerprints.kt @@ -8,8 +8,12 @@ package app.morphe.patches.youtube.misc.fix.padding import app.morphe.patcher.Fingerprint -import app.morphe.patcher.anyInstruction +import app.morphe.patcher.InstructionLocation.MatchAfterImmediately +import app.morphe.patcher.InstructionLocation.MatchAfterWithin +import app.morphe.patcher.fieldAccess import app.morphe.patcher.literal +import app.morphe.patcher.methodCall +import com.android.tools.smali.dexlib2.Opcode /** * Changes how the height of the navigation bar and the miniplayer is given to the pages, @@ -23,15 +27,69 @@ internal object IncognitoSearchPaddingFeatureFlagFingerprint : Fingerprint( ) /** - * Lays out the feed behind the navigation bar instead of above it, which triggers - * dynamic runtime insets/padding recalculations on the RecyclerView that cause watch history - * items (and other top horizontal carousels) to briefly detach and vanish (#3135). + * Sets the bottom margin of the layout that holds the page and the navigation bar. + * With the keyboard open and a text field focused, the margin is the keyboard height, + * otherwise the bottom of the window insets. */ -internal object FeedBehindNavigationBarFeatureFlagsFingerprint : Fingerprint( +internal object PageBottomMarginFingerprint : Fingerprint( + returnType = "V", + parameters = listOf("Landroid/graphics/Rect;"), filters = listOf( - anyInstruction( - literal(45631257L), - literal(45708229L) + methodCall(smali = "Landroid/view/View;->findFocus()Landroid/view/View;"), + methodCall( + smali = "Landroid/view/View;->onCheckIsTextEditor()Z", + location = MatchAfterWithin(5) + ), + fieldAccess( + opcode = Opcode.IGET, + definingClass = "Landroid/graphics/Rect;", + name = "bottom" + ) + ) +) + +/** + * Sets the padding of a feed list. When the feed is laid out behind the navigation bar, + * the bottom padding holds the navigation bar height and the bottom window inset. + */ +internal object FeedListPaddingFingerprint : Fingerprint( + returnType = "V", + parameters = listOf(), + filters = listOf( + methodCall( + definingClass = "Landroid/support/v7/widget/RecyclerView;", + name = "setClipToPadding" + ), + methodCall( + definingClass = "Landroid/support/v7/widget/RecyclerView;", + name = "setPadding", + location = MatchAfterImmediately() + ) + ) +) + +/** + * Sets the bottom padding of the list of a browse page laid out behind the navigation bar, + * to the navigation bar height plus the bottom window inset, once the window insets are known. + */ +internal object BrowseListBottomPaddingFingerprint : Fingerprint( + returnType = "V", + parameters = listOf("Ljava/lang/Object;"), + filters = listOf( + methodCall( + definingClass = "Landroid/support/v7/widget/RecyclerView;", + name = "getPaddingRight" + ), + fieldAccess( + opcode = Opcode.IGET, + definingClass = "Landroid/graphics/Rect;", + name = "bottom", + location = MatchAfterWithin(6) + ), + methodCall( + definingClass = "Landroid/support/v7/widget/RecyclerView;", + name = "setPadding", + location = MatchAfterWithin(6) ) ) ) diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/misc/fix/padding/FixPagePaddingPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/fix/padding/FixPagePaddingPatch.kt index 1865ca03c7..666eb08e8c 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/misc/fix/padding/FixPagePaddingPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/fix/padding/FixPagePaddingPatch.kt @@ -7,17 +7,33 @@ package app.morphe.patches.youtube.misc.fix.padding +import app.morphe.patcher.extensions.InstructionExtensions.addInstructions +import app.morphe.patcher.extensions.InstructionExtensions.getInstruction import app.morphe.patcher.patch.bytecodePatch import app.morphe.patcher.util.matchAllMethodIndicesForEach +import app.morphe.patches.youtube.misc.extension.sharedExtensionPatch import app.morphe.patches.youtube.misc.playservice.is_21_07_or_greater import app.morphe.patches.youtube.misc.playservice.versionCheckPatch import app.morphe.patches.youtube.shared.Constants.COMPATIBILITY_YOUTUBE +import app.morphe.util.addInstructionsAtControlFlowLabel +import app.morphe.util.indexOfFirstInstructionReversedOrThrow import app.morphe.util.insertLiteralOverride +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction21t +import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction + +private const val EXTENSION_CLASS = + "Lapp/morphe/extension/youtube/patches/FixPagePaddingPatch;" internal val fixPagePaddingPatch = bytecodePatch( description = "Fixes layout padding glitches in incognito search and watch history." ) { - dependsOn(versionCheckPatch) + dependsOn( + sharedExtensionPatch, + versionCheckPatch + ) compatibleWith(COMPATIBILITY_YOUTUBE) execute { @@ -27,8 +43,65 @@ internal val fixPagePaddingPatch = bytecodePatch( } } - FeedBehindNavigationBarFeatureFlagsFingerprint.matchAllMethodIndicesForEach(requireMatches = false) { index -> - insertLiteralOverride(index, false) + // With the edge-to-edge/translucent navigation bar, the keyboard shrinks the layout that holds + // the page and the navigation bar to the top of the keyboard, so the navigation bar is moved above + // the keyboard. The keyboard height is replaced with no margin (0), so the navigation bar stays + // at the bottom behind the keyboard, except while the legacy miniplayer is shown. + PageBottomMarginFingerprint.let { + it.method.apply { + val insetIndex = it.instructionMatches.last().index + val marginRegister = getInstruction(insetIndex).registerA + val keyboardBranchIndex = indexOfFirstInstructionReversedOrThrow(insetIndex) { + opcode == Opcode.IF_EQZ && + (this as BuilderInstruction21t).target.location.index == insetIndex + } + val keyboardRegister = getInstruction(keyboardBranchIndex).registerA + + addInstructionsAtControlFlowLabel( + insetIndex + 1, + """ + if-eqz v$keyboardRegister, :keyboard_hidden + invoke-static/range { v$marginRegister .. v$marginRegister }, $EXTENSION_CLASS->getKeyboardMargin(I)I + move-result v$marginRegister + :keyboard_hidden + nop + """ + ) + } + } + + FeedListPaddingFingerprint.let { + it.method.apply { + val index = it.instructionMatches.last().index + val paddingInstruction = getInstruction(index) + val listRegister = paddingInstruction.registerC + val bottomPaddingRegister = paddingInstruction.registerG + + addInstructions( + index, + """ + invoke-static { v$listRegister, v$bottomPaddingRegister }, $EXTENSION_CLASS->getAdjustedFeedListBottomPadding(Landroid/view/View;I)I + move-result v$bottomPaddingRegister + """ + ) + } + } + + BrowseListBottomPaddingFingerprint.let { + it.method.apply { + val index = it.instructionMatches.last().index + val paddingInstruction = getInstruction(index) + val listRegister = paddingInstruction.registerC + val bottomPaddingRegister = paddingInstruction.registerG + + addInstructions( + index, + """ + invoke-static { v$listRegister, v$bottomPaddingRegister }, $EXTENSION_CLASS->getAdjustedFeedListBottomPadding(Landroid/view/View;I)I + move-result v$bottomPaddingRegister + """ + ) + } } } } From b266aeed3cbe7990a742b717581cf944068bf3c6 Mon Sep 17 00:00:00 2001 From: ILoveOpenSourceApplications <117499019+ILoveOpenSourceApplications@users.noreply.github.com> Date: Fri, 30 Oct 2026 01:41:07 +0530 Subject: [PATCH 170/189] chore(YouTube - Navigation bar): Remove unnecessary warning --- .../morphe/extension/youtube/patches/NavigationBarPatch.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/NavigationBarPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/NavigationBarPatch.java index cd18260b3c..a2b483214d 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/NavigationBarPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/NavigationBarPatch.java @@ -347,10 +347,6 @@ public static boolean useNavigationBarBlur(boolean hardwareAccelerated) { || NAVIGATION_BAR_STYLE == NavigationBarStyle.OPAQUE)); } - /** - * @deprecated Handled by {@link FixPagePaddingPatch#getKeyboardMargin(int)}. - */ - @Deprecated public static int getKeyboardMargin(int original) { return FixPagePaddingPatch.getKeyboardMargin(original); } From ba1ac73757dc8935c60434de8d0362b311d73442 Mon Sep 17 00:00:00 2001 From: Tolkien <41752496+itolkien@users.noreply.github.com> Date: Thu, 29 Oct 2026 21:16:56 +0100 Subject: [PATCH 171/189] fix(YouTube - Captions): Stabilize automatic captions and restore translation languages (#4149) --- extensions/youtube/build.gradle.kts | 4 + .../youtube/patches/AutoCaptionsPatch.java | 16 +- .../patches/AutoTranslateCaptionsPatch.java | 52 ++- .../youtube/patches/CaptionInputReplay.java | 54 +++ .../youtube/patches/CaptionViewportPatch.java | 112 +++++ .../patches/CaptionViewportPolicy.java | 55 +++ .../extension/youtube/patches/CaptionXml.java | 95 +++++ .../patches/FullscreenVideoScalePatch.java | 1 + .../PreferredCaptionLanguagePatch.java | 73 +--- .../patches/StableCaptionBlockPolicy.java | 351 ++++++++++++++++ .../youtube/patches/StableCaptionBlocks.java | 385 ++++++++++++++++++ .../patches/TranslatedCaptionPensPatch.java | 64 ++- .../TranslatedRollingCaptionPolicy.java | 165 ++++++++ .../extension/youtube/settings/Settings.java | 4 + .../AutoTranslateCaptionsPatchTest.java | 36 +- .../patches/CaptionInputReplayTest.java | 137 +++++++ .../patches/CaptionViewportPolicyTest.java | 130 ++++++ .../patches/PreferredCaptionRoutingTest.java | 24 ++ .../patches/StableCaptionBlockPolicyTest.java | 306 ++++++++++++++ .../TranslatedRollingCaptionPolicyTest.java | 292 +++++++++++++ .../layout/captions/CaptionViewportPatch.kt | 45 ++ .../captions/CaptionWindowMeasurePatch.kt | 41 ++ .../youtube/layout/captions/CaptionsPatch.kt | 3 + .../youtube/layout/captions/Fingerprints.kt | 214 ++++++++++ .../captions/StableCaptionBlocksPatch.kt | 280 +++++++++++++ .../captions/TranslatedCaptionPensPatch.kt | 12 +- .../addresources/values/youtube/strings.xml | 2 + 27 files changed, 2865 insertions(+), 88 deletions(-) create mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/CaptionInputReplay.java create mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/CaptionViewportPatch.java create mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/CaptionViewportPolicy.java create mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/CaptionXml.java create mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/StableCaptionBlockPolicy.java create mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/StableCaptionBlocks.java create mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/TranslatedRollingCaptionPolicy.java create mode 100644 extensions/youtube/src/test/java/app/morphe/extension/youtube/patches/CaptionInputReplayTest.java create mode 100644 extensions/youtube/src/test/java/app/morphe/extension/youtube/patches/CaptionViewportPolicyTest.java create mode 100644 extensions/youtube/src/test/java/app/morphe/extension/youtube/patches/StableCaptionBlockPolicyTest.java create mode 100644 extensions/youtube/src/test/java/app/morphe/extension/youtube/patches/TranslatedRollingCaptionPolicyTest.java create mode 100644 patches/src/main/kotlin/app/morphe/patches/youtube/layout/captions/CaptionViewportPatch.kt create mode 100644 patches/src/main/kotlin/app/morphe/patches/youtube/layout/captions/CaptionWindowMeasurePatch.kt create mode 100644 patches/src/main/kotlin/app/morphe/patches/youtube/layout/captions/StableCaptionBlocksPatch.kt diff --git a/extensions/youtube/build.gradle.kts b/extensions/youtube/build.gradle.kts index 7a2fff6a11..1c48977a87 100644 --- a/extensions/youtube/build.gradle.kts +++ b/extensions/youtube/build.gradle.kts @@ -7,6 +7,7 @@ plugins { dependencies { testImplementation(libs.junit) testImplementation("org.json:json:20240303") + testImplementation(libs.morphe.extensions.library) compileOnly(libs.annotation) compileOnly(libs.morphe.extensions.library) @@ -23,6 +24,9 @@ configure { defaultConfig { minSdk = 26 } + + // Logger calls android.util.Log, which throws in local unit tests unless stubbed. + testOptions.unitTests.isReturnDefaultValues = true } protobuf { diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/AutoCaptionsPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/AutoCaptionsPatch.java index 83d695c641..ac42a912bd 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/AutoCaptionsPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/AutoCaptionsPatch.java @@ -10,9 +10,10 @@ package app.morphe.extension.youtube.patches; -import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; + +import android.os.SystemClock; -import app.morphe.extension.shared.Utils; import app.morphe.extension.youtube.settings.Settings; @SuppressWarnings("unused") @@ -25,7 +26,7 @@ public enum AutoCaptionsStyle { WITHOUT_VOLUME_ONLY } - private static final AtomicBoolean captionsButtonStatus = new AtomicBoolean(false); + private static final AtomicLong captionGuardUntil = new AtomicLong(Long.MAX_VALUE); /** * Injection point. @@ -39,7 +40,7 @@ public static boolean disableAutoCaptions(boolean original) { } // After the guard window (150ms), respect the user's manual CC button toggle. - if (captionsButtonStatus.get()) { + if (SystemClock.uptimeMillis() >= captionGuardUntil.get()) { return original; } @@ -51,7 +52,7 @@ public static boolean disableAutoCaptions(boolean original) { /** * Injection point. *

    - * Note: 'captionsButtonStatus' field check is not needed here + * Note: 'captionGuardUntil' field check is not needed here * because it's only related to 'disableAutoCaptions()' method * in order to prevent auto-captioning with volume enabled */ @@ -66,7 +67,7 @@ public static boolean disableMuteAutoCaptions(boolean original) { * Called before {@link #disableAutoCaptions(boolean)}. */ public static void newVideoStarted(VideoInformation.PlaybackController ignoredPlayerController) { - captionsButtonStatus.set(false); + captionGuardUntil.set(Long.MAX_VALUE); } /** @@ -74,6 +75,7 @@ public static void newVideoStarted(VideoInformation.PlaybackController ignoredPl * Called after {@link #disableAutoCaptions(boolean)}. */ public static void videoInformationLoaded() { - Utils.runOnMainThreadDelayed(() -> captionsButtonStatus.compareAndSet(false, true), 150); + // Repeated loaded events must not re-lock a video after its initial guard expired. + captionGuardUntil.compareAndSet(Long.MAX_VALUE, SystemClock.uptimeMillis() + 150); } } diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/AutoTranslateCaptionsPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/AutoTranslateCaptionsPatch.java index f162359c09..178c343ff9 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/AutoTranslateCaptionsPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/AutoTranslateCaptionsPatch.java @@ -92,13 +92,18 @@ static List addTranslationLanguages(List fields = captionParameters(url); + if (fields == null || !"asr".equals(fields.get("kind"))) return false; + String target = fields.get("tlang"); + return translated + ? target != null && LANGUAGE_CODE_PATTERN.matcher(target).matches() + && vss.endsWith("." + target) + : !fields.containsKey("tlang"); + } + /** Keep YouTube URL validation shared by original and native translated tracks. */ private static Map captionParameters(String url) { if (url == null) return null; @@ -328,19 +347,30 @@ private static boolean originalVss(String value, String prefix) { && VSS_ID_SUFFIX_PATTERN.matcher(value.substring(prefix.length() + 1)).matches()); } + /** Canonical aliases while retaining the region/script for exact track selection. */ + static String normalizeLanguageCode(String languageCode) { + if (languageCode == null) return ""; + String code = languageCode.trim().replace('_', '-').toLowerCase(Locale.ROOT); + int dash = code.indexOf('-'); + String primary = dash < 0 ? code : code.substring(0, dash); + String canonical = switch (primary) { + case "iw" -> "he"; + case "in" -> "id"; + case "ji" -> "yi"; + case "tl" -> "fil"; + case "nb" -> "no"; + default -> primary; + }; + return canonical + code.substring(primary.length()); + } + /** * @return Key that is the same for all codes of a language. */ - private static String getLanguageKey(String languageCode) { - String code = languageCode.replace('_', '-').toLowerCase(Locale.ROOT); + static String getLanguageKey(String languageCode) { + String code = normalizeLanguageCode(languageCode); String primary = code.split("-")[0]; switch (primary) { - // Legacy ISO 639 codes used by Java Locale and YouTube. - case "iw": return "he"; - case "in": return "id"; - case "ji": return "yi"; - case "tl": return "fil"; - case "nb": return "no"; // Traditional and Simplified Chinese are different translations. case "zh": // Script takes priority over region. diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/CaptionInputReplay.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/CaptionInputReplay.java new file mode 100644 index 0000000000..8cbd21656e --- /dev/null +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/CaptionInputReplay.java @@ -0,0 +1,54 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/4149 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.extension.youtube.patches; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.SequenceInputStream; + +/** Bounded buffering with byte-exact replay and parser-owned stream lifetime. */ +final class CaptionInputReplay { + private CaptionInputReplay() {} + + public static final class Copy { + public final InputStream input; + public final byte[] bytes; + public final boolean complete; + + Copy(InputStream input, byte[] bytes, boolean complete) { + this.input = input; + this.bytes = bytes; + this.complete = complete; + } + } + + /** Bound collection; replay the prefix and remaining stream byte-for-byte. */ + public static Copy copy(InputStream input, int limit) throws IOException { + if (limit < 0 || limit == Integer.MAX_VALUE) throw new IllegalArgumentException("limit"); + ByteArrayOutputStream output = new ByteArrayOutputStream(); + byte[] buffer = new byte[8192]; + while (output.size() <= limit) { + int n = input.read(buffer, 0, Math.min(buffer.length, limit + 1 - output.size())); + if (n < 0) { + byte[] bytes = output.toByteArray(); + // Closing the replay also closes the original parser-owned stream. + return new Copy( + new SequenceInputStream(new ByteArrayInputStream(bytes), input), + bytes, + true); + } + if (n == 0) continue; + output.write(buffer, 0, n); + } + byte[] bytes = output.toByteArray(); + return new Copy( + new SequenceInputStream(new ByteArrayInputStream(bytes), input), bytes, false); + } +} diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/CaptionViewportPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/CaptionViewportPatch.java new file mode 100644 index 0000000000..5223cf4e3e --- /dev/null +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/CaptionViewportPatch.java @@ -0,0 +1,112 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/4149 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.extension.youtube.patches; + +import android.content.res.Configuration; +import android.view.View; + +import java.lang.ref.WeakReference; +import java.util.List; + +import app.morphe.extension.shared.Logger; +import app.morphe.extension.shared.Utils; +import app.morphe.extension.youtube.settings.Settings; +import app.morphe.extension.youtube.shared.PlayerType; + +/** Preserve YouTube's font formula and preference scale, using known fitted content height. */ +@SuppressWarnings("unused") +public final class CaptionViewportPatch { + private static final CaptionViewportPolicy policy = new CaptionViewportPolicy(); + private static volatile WeakReference latestView = new WeakReference<>(null); + private static boolean lastFullscreen; + private static FullscreenVideoScalePatch.VideoScaleMode lastScale; + + static { + PlayerType.getOnChange().addObserver(type -> onPresentationChanged()); + } + + private CaptionViewportPatch() {} + + /** + * Injection point. + */ + public static void onVideoId(String videoId) { + policy.select(videoId); + remeasure(); + } + + /** + * Injection point. + */ + public static List onFormats( + String videoId, List formats) { + try { + long bestArea = 0; + int bestWidth = 0, bestHeight = 0; + for (VideoFormat.FormatInterface format : formats) { + String mimeType = format.patch_getMimeType(); + if (mimeType == null || !mimeType.startsWith("video/")) continue; + int width = format.patch_getWidth(), height = format.patch_getHeight(); + long area = (long) width * height; + if (area > bestArea) { + bestArea = area; + bestWidth = width; + bestHeight = height; + } + } + if (bestArea > 0) { + policy.record(videoId, bestWidth, bestHeight); + remeasure(); + } + } catch (Exception ex) { + Logger.printException(() -> "onFormats failure", ex); + } + return formats; + } + + /** + * Injection point. + * + * @param scale Caption size preference scale. + * @param width Width of the subtitle overlay. + * @param height Height of the subtitle overlay. + * @return Height to calculate the caption font size with. + */ + public static int normalizeHeight(View view, float scale, int width, int height) { + try { + if (latestView.get() != view) latestView = new WeakReference<>(view); + PlayerType type = PlayerType.getCurrent(); + final boolean landscapeTransition = type == PlayerType.WATCH_WHILE_SLIDING_MAXIMIZED_FULLSCREEN + && view.getResources().getConfiguration().orientation + == Configuration.ORIENTATION_LANDSCAPE; + final boolean fullscreenFit = (type == PlayerType.WATCH_WHILE_FULLSCREEN || landscapeTransition) + && Settings.FULLSCREEN_VIDEO_SCALE.get() == FullscreenVideoScalePatch.VideoScaleMode.DEFAULT; + return policy.height(width, height, fullscreenFit, landscapeTransition); + } catch (Exception ex) { + Logger.printException(() -> "normalizeHeight failure", ex); + return height; + } + } + + /** A paused cue must be resized even when no new subtitle or format arrives. */ + public static synchronized void onPresentationChanged() { + boolean fullscreen = PlayerType.getCurrent() == PlayerType.WATCH_WHILE_FULLSCREEN; + FullscreenVideoScalePatch.VideoScaleMode scale = Settings.FULLSCREEN_VIDEO_SCALE.get(); + if (fullscreen == lastFullscreen && scale == lastScale) return; + lastFullscreen = fullscreen; + lastScale = scale; + remeasure(); + } + + private static void remeasure() { + View view = latestView.get(); + if (view != null) { + Utils.runOnMainThreadNowOrLater(view::requestLayout); + } + } +} diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/CaptionViewportPolicy.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/CaptionViewportPolicy.java new file mode 100644 index 0000000000..0ea407cfd2 --- /dev/null +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/CaptionViewportPolicy.java @@ -0,0 +1,55 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/4149 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.extension.youtube.patches; + +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; + +/** Encoded content geometry belongs to its video, never to the last prefetched response. */ +final class CaptionViewportPolicy { + private final LinkedHashMap aspects = new LinkedHashMap<>(32, 0.75f, true); + private String selected; + + synchronized void select(String videoId) { + selected = videoId != null && videoId.matches("[A-Za-z0-9_-]{11}") ? videoId : null; + } + + synchronized void record(String videoId, int width, int height) { + if (videoId == null + || !videoId.matches("[A-Za-z0-9_-]{11}") + || width <= 0 + || height <= 0 + || width > 16384 + || height > 16384) return; + double aspect = width / (double) height; + if (aspect < 0.1 || aspect > 10) return; + aspects.put(videoId, aspect); + if (aspects.size() > 32) { + Iterator> iterator = aspects.entrySet().iterator(); + while (iterator.hasNext()) { + if (!iterator.next().getKey().equals(selected)) { + iterator.remove(); + break; + } + } + } + } + + synchronized int height( + int width, int height, boolean fullscreenFit, boolean landscapeTransition) { + if (!fullscreenFit || width <= 0 || height <= 0 || selected == null) return height; + // Animated bounds cross the stable portrait/near-square guards. The target + // configuration, rather than one intermediate frame, identifies landscape. + if (!landscapeTransition && (width < height || 4L * height < 3L * width)) return height; + Double aspect = aspects.get(selected); + if (aspect == null) return height; + return (int) Math.min(height, Math.max(1L, Math.round(width / aspect))); + } + +} diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/CaptionXml.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/CaptionXml.java new file mode 100644 index 0000000000..3e514580ad --- /dev/null +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/CaptionXml.java @@ -0,0 +1,95 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/4149 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.extension.youtube.patches; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; +import org.xml.sax.SAXParseException; +import org.xml.sax.helpers.DefaultHandler; + +import java.io.StringReader; +import java.nio.ByteBuffer; +import java.nio.charset.CodingErrorAction; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.xml.parsers.DocumentBuilderFactory; + +import app.morphe.extension.shared.Logger; + +/** Bounded, fail-closed srv3 reader shared by source capture and translated-caption repair. */ +final class CaptionXml { + static final int MAX_BYTES = 4 * 1024 * 1024; + private static final Pattern ENCODING = + Pattern.compile( + "^\\s*<\\?xml\\s+[^?]*\\bencoding\\s*=\\s*(['\"])([^'\"]+)\\1", + Pattern.CASE_INSENSITIVE); + + private CaptionXml() {} + + static List children(Element parent) { + List elements = new ArrayList<>(); + for (Node node = parent.getFirstChild(); node != null; node = node.getNextSibling()) + if (node instanceof Element) elements.add((Element) node); + return elements; + } + + static Document read(byte[] input) { + if (input == null || input.length == 0 || input.length > MAX_BYTES) return null; + try { + String source = + StandardCharsets.UTF_8 + .newDecoder() + .onMalformedInput(CodingErrorAction.REPORT) + .onUnmappableCharacter(CodingErrorAction.REPORT) + .decode(ByteBuffer.wrap(input)) + .toString(); + if (source.startsWith("\uFEFF")) source = source.substring(1); + Matcher encoding = ENCODING.matcher(source); + //noinspection ConstantConditions + if (encoding.find() + && !encoding.group(2).equalsIgnoreCase("UTF-8") + && !encoding.group(2).equalsIgnoreCase("UTF8")) return null; + if (source.indexOf('\0') >= 0 + || source.contains(" { + throw new SAXException("External caption entity"); + }); + builder.setErrorHandler( + new DefaultHandler() { + @Override + public void error(SAXParseException error) throws SAXException { + throw error; + } + + @Override + public void fatalError(SAXParseException error) throws SAXException { + throw error; + } + }); + // Parse the same validated characters; the declaration cannot reinterpret bytes. + Document document = builder.parse(new InputSource(new StringReader(source))); + Element root = document.getDocumentElement(); + return root.getTagName().equals("timedtext") && root.getAttribute("format").equals("3") + ? document + : null; + } catch (Exception ex) { + Logger.printInfo(() -> "Unsupported caption document", ex); + return null; + } + } +} diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FullscreenVideoScalePatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FullscreenVideoScalePatch.java index 9f346471d7..61f0970bdd 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FullscreenVideoScalePatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FullscreenVideoScalePatch.java @@ -157,6 +157,7 @@ public static List setVideoAspectRatio(@NonNull String videoId, } public static void applyScale() { + CaptionViewportPatch.onPresentationChanged(); Utils.runOnMainThread(FullscreenVideoScalePatch::applyScaleOnMainThread); } diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/PreferredCaptionLanguagePatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/PreferredCaptionLanguagePatch.java index 4158763c61..12ab4e16db 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/PreferredCaptionLanguagePatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/PreferredCaptionLanguagePatch.java @@ -7,14 +7,19 @@ package app.morphe.extension.youtube.patches; +import static app.morphe.extension.youtube.patches.AutoTranslateCaptionsPatch.getLanguageKey; +import static app.morphe.extension.youtube.patches.AutoTranslateCaptionsPatch.normalizeLanguageCode; + +import android.os.SystemClock; + import androidx.annotation.Nullable; import java.util.List; import java.util.Locale; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; import app.morphe.extension.shared.Logger; -import app.morphe.extension.shared.Utils; import app.morphe.extension.shared.requests.Requester; import app.morphe.extension.youtube.settings.Settings; @@ -75,7 +80,7 @@ public interface CaptionTrackInterface { private static final long USER_SELECTION_GRACE_PERIOD_MILLISECONDS = 500; private static final AtomicBoolean userSelectedTrack = new AtomicBoolean(false); - private static final AtomicBoolean userInteractionAllowed = new AtomicBoolean(false); + private static final AtomicLong userInteractionAllowedAt = new AtomicLong(Long.MAX_VALUE); private static volatile long videoStartTime; @Nullable @@ -191,7 +196,7 @@ public static CaptionTrackInterface onSetSubtitleTrack(SubtitleManagerInterface @Nullable CaptionTrackInterface track, @Nullable Enum selectType) { try { - final boolean interactionAllowed = userInteractionAllowed.get(); + final boolean interactionAllowed = SystemClock.uptimeMillis() >= userInteractionAllowedAt.get(); Logger.printDebug(() -> "onSetSubtitleTrack track: " + track + " selectType: " + selectType + " interactionAllowed: " + interactionAllowed @@ -200,7 +205,7 @@ public static CaptionTrackInterface onSetSubtitleTrack(SubtitleManagerInterface // Respect explicit user selection only after the initial video load window. if (interactionAllowed && selectType != null && PREFERRED_TRACK_SELECT_TYPE.equals(selectType.name()) - && System.currentTimeMillis() - videoStartTime > USER_SELECTION_GRACE_PERIOD_MILLISECONDS) { + && SystemClock.uptimeMillis() - videoStartTime > USER_SELECTION_GRACE_PERIOD_MILLISECONDS) { if (isSameTrack(track, lastProgrammaticTrackLang, lastProgrammaticTrackVss)) { Logger.printDebug(() -> "Ignoring PREFERRED_TRACK as it matches programmatic selection"); return track; @@ -237,10 +242,10 @@ public static CaptionTrackInterface onSetSubtitleTrack(SubtitleManagerInterface * Injection point: Video start hook. */ public static void newVideoStarted(VideoInformation.PlaybackController ignoredController) { - userInteractionAllowed.set(false); + userInteractionAllowedAt.set(Long.MAX_VALUE); userSelectedTrack.set(false); recordProgrammaticSelection(null); - videoStartTime = System.currentTimeMillis(); + videoStartTime = SystemClock.uptimeMillis(); Logger.printDebug(() -> "newVideoStarted, user interaction locked startTime: " + videoStartTime); } @@ -248,11 +253,8 @@ public static void newVideoStarted(VideoInformation.PlaybackController ignoredCo * Injection point: Video information loaded hook. */ public static void videoInformationLoaded() { - Logger.printDebug(() -> "videoInformationLoaded, scheduling 300ms unlock"); - Utils.runOnMainThreadDelayed(() -> { - userInteractionAllowed.set(true); - Logger.printDebug(() -> "User interaction unlocked"); - }, 300); + // Only the first loaded event starts the window; later events must not re-lock selection. + userInteractionAllowedAt.compareAndSet(Long.MAX_VALUE, SystemClock.uptimeMillis() + 300); } private static String getAppLanguage() { @@ -300,49 +302,16 @@ private static boolean isSameTrack(@Nullable CaptionTrackInterface track, && (!hasTargetVss || vss.isEmpty()); } - private static String normalizeLanguageCode(@Nullable String code) { - if (code == null) return ""; - String s = code.trim().toLowerCase(Locale.ROOT).replace('_', '-'); - - // Legacy ISO 639 codes used by Java Locale and YouTube. - if (s.equals("iw") || s.startsWith("iw-")) { - return "he" + s.substring(2); - } - if (s.equals("in") || s.startsWith("in-")) { - return "id" + s.substring(2); - } - if (s.equals("ji") || s.startsWith("ji-")) { - return "yi" + s.substring(2); - } - return s; - } - private static boolean matchesLanguage(@Nullable String trackLang, String targetLang) { if (trackLang == null) return false; - String t1 = normalizeLanguageCode(trackLang); - String t2 = normalizeLanguageCode(targetLang); - if (t1.equals(t2) || t1.startsWith(t2 + "-") || t2.startsWith(t1 + "-")) { - return true; - } - - // Prevent cross-matching between Simplified and Traditional Chinese. - final boolean t1Traditional = isTraditionalChinese(t1); - final boolean t1Simplified = isSimplifiedChinese(t1); - final boolean t2Traditional = isTraditionalChinese(t2); - final boolean t2Simplified = isSimplifiedChinese(t2); - if ((t1Simplified && t2Traditional) || (t1Traditional && t2Simplified)) { - return false; - } - - return getPrimaryLanguage(t1).equals(getPrimaryLanguage(t2)); - } - - private static boolean isTraditionalChinese(String code) { - return code.contains("hant") || code.contains("tw") || code.contains("hk"); - } - - private static boolean isSimplifiedChinese(String code) { - return code.contains("hans") || code.contains("cn"); + String first = getLanguageKey(trackLang), second = getLanguageKey(targetLang); + if (first.equals(second)) return true; + String primary = getPrimaryLanguage(first); + if (!primary.equals(getPrimaryLanguage(second))) return false; + // Only known, opposing Chinese scripts block the usual regional/generic fallback. + return !primary.equals("zh") + || !(first.equals("zh-hans") && second.equals("zh-hant") + || first.equals("zh-hant") && second.equals("zh-hans")); } private static String getPrimaryLanguage(String code) { diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/StableCaptionBlockPolicy.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/StableCaptionBlockPolicy.java new file mode 100644 index 0000000000..9a86c9a6ee --- /dev/null +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/StableCaptionBlockPolicy.java @@ -0,0 +1,351 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/4149 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.extension.youtube.patches; + +import static app.morphe.extension.youtube.patches.CaptionXml.children; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import app.morphe.extension.shared.Logger; + +/** Source-timed complete blocks; the renderer supplies the actual two-line capacity. */ +final class StableCaptionBlockPolicy { + static final int MAX_ATOMS = 20000, MAX_BLOCK_CHARS = 512; + static final long MAX_PREVIEW_MS = 8000, MIN_DURATION_MS = 1500, MAX_GAP_MS = 2000; + + interface Fit { + boolean fits(String text); + } + + static final class Block { + final long start, end; + final String text; + final boolean fits; + + Block(long start, long end, String text, boolean fits) { + this.start = start; + this.end = end; + this.text = text; + this.fits = fits; + } + } + + private static final class Atom { + final long start, end; + final String text; + final boolean barrierBefore; + + Atom(long start, long end, String text, boolean barrier) { + this.start = start; + this.end = end; + this.text = text; + this.barrierBefore = barrier; + } + } + + private final List atoms; + private final int[] characters; + + private StableCaptionBlockPolicy(List atoms) { + this.atoms = List.copyOf(atoms); + characters = new int[atoms.size() + 1]; + for (int i = 0; i < atoms.size(); i++) + characters[i + 1] = + characters[i] + atoms.get(i).text.codePointCount(0, atoms.get(i).text.length()); + } + + static StableCaptionBlockPolicy parse(Document d) { + try { + if (d == null || d.getElementsByTagName("pen").getLength() != 0) return null; + Element root = d.getDocumentElement(), body = null; + for (Element e : children(root)) + if (e.getTagName().equals("body")) { + if (body != null) return null; + body = e; + } + if (body == null) return null; + String window = null; + List atoms = new ArrayList<>(); + long previousParagraph = -1; + boolean barrier = false; + for (Element e : children(body)) { + if (e.getTagName().equals("w")) { + if (window != null || number(e, "t") != 0 || e.getAttribute("id").isEmpty()) + return null; + window = e.getAttribute("id"); + continue; + } + if (window == null + || !e.getTagName().equals("p") + || !window.equals(e.getAttribute("w")) + || !attributes(e, "t", "d", "w", "a", "p")) return null; + if (e.getAttribute("a").equals("1")) { + if (!e.getTextContent().equals("\n") || !children(e).isEmpty()) return null; + continue; + } + if (!e.getAttribute("a").isEmpty() && !e.getAttribute("a").equals("0")) return null; + long start = number(e, "t"), duration = number(e, "d"); + if (start < 0 + || start <= previousParagraph + || start > 7200000 + || duration <= 0 + || duration > 600000) return null; + previousParagraph = start; + List segments = children(e); + if (segments.size() > 2048) return null; + int before = atoms.size(); + long previousOffset = -1; + boolean first = true; + StringBuilder whitespace = new StringBuilder(); + if (segments.isEmpty()) { + if (!add(atoms, start, start + duration, e.getTextContent(), true, barrier)) + return null; + } else { + for (Node n = e.getFirstChild(); n != null; n = n.getNextSibling()) + if ((n.getNodeType() == Node.TEXT_NODE + || n.getNodeType() == Node.CDATA_SECTION_NODE) + && !n.getTextContent().isBlank()) return null; + for (Element segment : segments) { + if (!segment.getTagName().equals("s") + || !children(segment).isEmpty() + || !attributes(segment, "t", "p", "ac")) return null; + long offset = segment.hasAttribute("t") ? number(segment, "t") : 0; + if (offset < 0 || offset < previousOffset || offset >= duration) return null; + previousOffset = offset; + String text = segment.getTextContent(); + if (text.isBlank()) { + if (text.indexOf('\n') >= 0 || text.indexOf('\r') >= 0) return null; + whitespace.append(text); + } else { + if (!add( + atoms, + start + offset, + start + duration, + whitespace + text, + first, + first && barrier)) return null; + whitespace.setLength(0); + first = false; + } + } + } + if (atoms.size() == before && !atoms.isEmpty()) { + Atom last = atoms.get(atoms.size() - 1); + if (last.start >= start) return null; + if (last.end > start) + atoms.set( + atoms.size() - 1, + new Atom(last.start, start, last.text, last.barrierBefore)); + barrier = true; + } else if (atoms.size() > before) { + barrier = false; + } + } + return atoms.isEmpty() ? null : new StableCaptionBlockPolicy(atoms); + } catch (RuntimeException ex) { + Logger.printException(() -> "parse failure", ex); + return null; + } + } + + /** + * @return False if the source is too complex for stable blocks. + */ + private static boolean add( + List atoms, + long time, + long end, + String text, + boolean paragraph, + boolean barrier) { + if (text.length() > 4096 || text.indexOf('\n') >= 0 || text.indexOf('\r') >= 0) + return false; + if (text.isBlank()) return true; + if (!atoms.isEmpty()) { + Atom last = atoms.get(atoms.size() - 1); + // Overlapping source words. + if (time < last.start) return false; + if (paragraph + && !Character.isWhitespace(last.text.charAt(last.text.length() - 1)) + && !Character.isWhitespace(text.charAt(0)) + && ".,!?;:)]}".indexOf(text.charAt(0)) < 0) text = " " + text; + if (time == last.start) { + if (last.text.length() + text.length() > 4096) return false; + atoms.set( + atoms.size() - 1, + new Atom( + time, + Math.max(end, last.end), + last.text + text, + last.barrierBefore)); + return true; + } + } + if (atoms.size() >= MAX_ATOMS) return false; + atoms.add(new Atom(time, end, text, barrier)); + return true; + } + + private String text(int from, int to) { + StringBuilder s = new StringBuilder(); + for (int i = from; i < to; i++) s.append(atoms.get(i).text); + return s.toString().strip().replaceAll("[ \\t]+", " "); + } + + private boolean bounded(int from, int to) { + return atoms.get(to - 1).start - atoms.get(from).start <= MAX_PREVIEW_MS + && characters[to] - characters[from] <= MAX_BLOCK_CHARS; + } + + private Block block(int from, int to, boolean fits) { + long end = atoms.get(to - 1).end; + if (to < atoms.size()) { + Atom next = atoms.get(to); + end = + !next.barrierBefore && next.start - end <= MAX_GAP_MS + ? next.start + : Math.min(end, next.start); + } + return new Block(atoms.get(from).start, end, text(from, to), fits); + } + + private boolean boundary(int at) { + if (at >= atoms.size()) return true; + String before = atoms.get(at - 1).text, raw = atoms.get(at).text; + if (!Character.isWhitespace(before.charAt(before.length() - 1)) + && !Character.isWhitespace(raw.charAt(0))) return false; + String next = raw.stripLeading(); + if (next.isEmpty()) return false; + int first = next.codePointAt(0), type = Character.getType(first); + return ".,!?;:)]}".indexOf(first) < 0 + && type != Character.END_PUNCTUATION + && type != Character.FINAL_QUOTE_PUNCTUATION; + } + + List plan(Fit fit) { + int size = atoms.size(); + int[] next = new int[size]; + boolean[] supported = new boolean[size]; + long[] shortfall = new long[size + 1]; + double[] cost = new double[size + 1]; + for (int from = size - 1; from >= 0; from--) { + int limit = from + 1; + while (limit < size + && !atoms.get(limit).barrierBefore + && atoms.get(limit).start - atoms.get(limit - 1).end <= MAX_GAP_MS + && bounded(from, limit + 1)) limit++; + int low = from + 1, high = limit, best = from; + while (low <= high) { + int mid = (low + high) >>> 1; + if (bounded(from, mid) && fit.fits(text(from, mid))) { + best = mid; + low = mid + 1; + } else high = mid - 1; + } + long bestShort = Long.MAX_VALUE; + double bestCost = Double.POSITIVE_INFINITY; + for (int to = from + 1; to <= best; to++) { + if (!boundary(to)) continue; + Block candidate = block(from, to, true); + long duration = candidate.end - candidate.start; + if (duration <= 0) continue; + long missing = shortfall(candidate) + shortfall[to]; + int chars = candidate.text.codePointCount(0, candidate.text.length()); + // Relative reading load, not a language-specific characters-per-second limit. + char last = candidate.text.charAt(candidate.text.length() - 1); + double value = + cost[to] + + chars * (double) chars * 1000 / duration + + 40 + + (Character.isLetterOrDigit(last) ? 20 : 0); + if (missing < bestShort || (missing == bestShort && value < bestCost)) { + bestShort = missing; + bestCost = value; + next[from] = to; + supported[from] = true; + } + } + if (next[from] == 0) { + int to = from + 1; + while (to < size && !boundary(to)) to++; + next[from] = to; + Block nativeBlock = block(from, to, false); + bestShort = shortfall(nativeBlock) + shortfall[to]; + bestCost = cost[to] + 40; + } + shortfall[from] = bestShort; + cost[from] = bestCost; + } + List out = new ArrayList<>(); + for (int from = 0; from < size; from = next[from]) { + Block b = block(from, next[from], supported[from]); + out.add(b.fits && !fit.fits(b.text) ? block(from, next[from], false) : b); + } + return List.copyOf(out); + } + + /** Native timeline template used only while source text or a bounded borrowed gap is active. */ + long templateTime(long time) { + int low = 0, high = atoms.size() - 1, found = -1; + while (low <= high) { + int mid = (low + high) >>> 1; + if (atoms.get(mid).start <= time) { + found = mid; + low = mid + 1; + } else high = mid - 1; + } + if (found < 0) return -1; + Atom last = atoms.get(found); + if (time < last.end) return last.start; + if (found + 1 < atoms.size()) { + Atom next = atoms.get(found + 1); + if (!next.barrierBefore && next.start - last.end <= MAX_GAP_MS && time < next.start) + return last.start; + } + return -1; + } + + private static long shortfall(Block b) { + return Math.max(0, MIN_DURATION_MS - (b.end - b.start)); + } + + static Block at(List blocks, long time) { + int low = 0, high = blocks.size() - 1; + while (low <= high) { + int mid = (low + high) >>> 1; + Block b = blocks.get(mid); + if (time < b.start) high = mid - 1; + else if (time >= b.end) low = mid + 1; + else return b; + } + return null; + } + + /** + * @return The time attribute, or -1 if it is missing or invalid. + */ + private static long number(Element e, String key) { + String s = e.getAttribute(key); + return s.matches("[0-9]{1,10}") ? Long.parseLong(s) : -1; + } + + private static boolean attributes(Element e, String... allowed) { + List keys = Arrays.asList(allowed); + NamedNodeMap a = e.getAttributes(); + for (int i = 0; i < a.getLength(); i++) + if (!keys.contains(a.item(i).getNodeName())) return false; + return true; + } +} diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/StableCaptionBlocks.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/StableCaptionBlocks.java new file mode 100644 index 0000000000..d7408cd99c --- /dev/null +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/StableCaptionBlocks.java @@ -0,0 +1,385 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/4149 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ +package app.morphe.extension.youtube.patches; + +import android.os.Build; +import android.text.Layout; +import android.text.SpannableStringBuilder; +import android.text.Spanned; +import android.text.StaticLayout; +import android.text.TextPaint; +import android.view.View; + +import androidx.annotation.Nullable; + +import org.w3c.dom.Document; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.WeakHashMap; +import java.util.concurrent.CancellationException; + +import app.morphe.extension.shared.Logger; +import app.morphe.extension.shared.Utils; +import app.morphe.extension.youtube.settings.Settings; + +/** Native source timing and phone paint, with complete immutable text between block boundaries. */ +@SuppressWarnings("unused") +public final class StableCaptionBlocks { + private static final ThreadLocal parsing = new ThreadLocal<>(); + private static final Object FIXED_PARAGRAPH = new Object(); + + /** + * Text paint measurement comparison requires Android 10. + * The setting requires restarting the app. + */ + private static final boolean ENABLED = isPatchIncluded() + && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q + && Settings.STABLE_CAPTION_BLOCKS.get(); + + private StableCaptionBlocks() {} + + private static boolean isPatchIncluded() { + return false; // Modified during patching. + } + + public interface Timeline { + // Methods are added during patching. + List patch_getOriginalCaptions(long time); + + @Nullable + State patch_getBlockState(); + } + + public interface Cue { + // Methods are added during patching. + CharSequence patch_getOriginalCaptionVisible(); + + Frame patch_getBlockFrame(); + + Object patch_copyBlockFrame(Frame frame); + } + + public interface Window { + // Methods are added during patching. + Object patch_getCaptionCue(); + + Object patch_getCaptionMeasureRow(); + + List patch_getFullCaptionLines(); + + List patch_getVisibleCaptionLines(); + } + + public interface Row { + // Methods are added during patching. + void patch_setCaptionText(CharSequence text); + + StaticLayout patch_getCaptionLayout(); + } + + public static final class Frame { + final State state; + final long time, key; + + Frame(State state, long time, long key) { + this.state = state; + this.time = time; + this.key = key; + } + + @Override + public boolean equals(Object other) { + return other instanceof Frame f && f.state == state && f.key == key; + } + + @Override + public int hashCode() { + return 31 * System.identityHashCode(state) + Long.hashCode(key); + } + } + + public static final class State { + StableCaptionBlockPolicy policy; + private final Map caches = new WeakHashMap<>(); + // Keep inner, outer portrait and outer landscape plans across native view recreation. + private final List ready = new ArrayList<>(); + } + + private static final class Cache { + final int width; + final TextPaint paint; + volatile List blocks; + volatile boolean cancelled; + StableCaptionBlockPolicy.Block last; + CharSequence text; + List nativeFull, nativeVisible; + + Cache(int width, TextPaint paint) { + this.width = width; + this.paint = new TextPaint(); + this.paint.set(paint); + } + + boolean matches(StaticLayout probe) { + return width == probe.getWidth() && paint.equalsForTextMeasurement(probe.getPaint()); + } + + StaticLayout layout(String text) { + if (cancelled) throw new CancellationException(); + return blockLayout(text, paint, width).setIncludePad(true).build(); + } + } + + /** + * Called before a caption track is parsed. + */ + static void setAutomaticTrack(boolean automatic) { + if (!ENABLED) return; + parsing.remove(); + if (automatic) { + parsing.set(new State()); + } + } + + static boolean isCapturing() { + return parsing.get() != null; + } + + static void capture(Document document) { + State state = parsing.get(); + if (state != null) state.policy = StableCaptionBlockPolicy.parse(document); + } + + /** + * Injection point. + * Called at the end of the caption timeline constructor. + */ + @Nullable + public static State takeState() { + if (!ENABLED) return null; + State state = parsing.get(); + parsing.remove(); + return state != null && state.policy != null ? state : null; + } + + /** Native player stages recreate cues, but retain their styled full-text sequence. */ + public static CharSequence carryFrame(@Nullable CharSequence text, @Nullable Frame frame) { + //noinspection SizeReplaceableByIsEmpty + if (text == null || text.length() == 0 || frame == null) return text; + SpannableStringBuilder copy = new SpannableStringBuilder(text); + for (Frame previous : copy.getSpans(0, copy.length(), Frame.class)) + copy.removeSpan(previous); + copy.setSpan(frame, 0, copy.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); + return copy; + } + + @Nullable + public static Frame frameOf(@Nullable CharSequence text) { + //noinspection SizeReplaceableByIsEmpty + if (!(text instanceof Spanned spanned) || text.length() == 0) return null; + Frame[] frames = spanned.getSpans(0, text.length(), Frame.class); + return frames.length == 1 ? frames[0] : null; + } + + /** + * Injection point. + * Called instead of the caption sampler, if the timeline has a block state. + */ + public static List present(Timeline timeline, long time) { + List original = timeline.patch_getOriginalCaptions(time); + try { + State state = timeline.patch_getBlockState(); + if (state == null || original == null || original.size() > 1) return original; + long template = state.policy.templateTime(time); + if (template < 0) return Collections.emptyList(); + + List source = original; + //noinspection SizeReplaceableByIsEmpty + if (source.isEmpty() || (source.size() == 1 && source.get(0) instanceof Cue c + && (c.patch_getOriginalCaptionVisible() == null + || c.patch_getOriginalCaptionVisible().length() == 0))) { + source = timeline.patch_getOriginalCaptions(template); + } + if (source == null || source.size() != 1 || !(source.get(0) instanceof Cue cue)) + return original; + Object copy = cue.patch_copyBlockFrame(new Frame(state, time, template)); + return copy == null ? original : Collections.singletonList(copy); + } catch (Exception ex) { + Logger.printException(() -> "present failure", ex); + return original; + } + } + + /** + * Player subclasses may recreate cue placement; only marked ASR blocks use default center + * placement. + */ + public static List centerBlocks(@Nullable List original) { + if (!ENABLED || original == null || original.size() != 1 + || !(original.get(0) instanceof Cue cue)) { + return original; + } + try { + Frame frame = cue.patch_getBlockFrame(); + if (frame == null) return original; + Object copy = cue.patch_copyBlockFrame(frame); + return copy == null || copy == cue ? original : Collections.singletonList(copy); + } catch (Exception ex) { + Logger.printException(() -> "centerBlocks failure", ex); + return original; + } + } + + /** + * Called before native window measurement, after its font/typeface/style have been configured. + */ + public static void prepare(View view, int widthSpec) { + if (!ENABLED) return; + try { + if (!(view instanceof Window window) + || !(window.patch_getCaptionCue() instanceof Cue cue)) return; + Frame frame = cue.patch_getBlockFrame(); + if (frame == null) return; + Cache cache = frame.state.caches.get(view); + List full = window.patch_getFullCaptionLines(), + visible = window.patch_getVisibleCaptionLines(); + List nativeFull, nativeVisible; + if (marked(full) || marked(visible)) { + if (cache == null || cache.nativeFull == null) return; + nativeFull = cache.nativeFull; + nativeVisible = cache.nativeVisible; + full.clear(); + full.addAll(nativeFull); + visible.clear(); + visible.addAll(nativeVisible); + } else { + nativeFull = new ArrayList<>(full); + nativeVisible = new ArrayList<>(visible); + } + Object nativeRow = window.patch_getCaptionMeasureRow(); + if (!(nativeRow instanceof Row row) || !(nativeRow instanceof View measure)) return; + int width = + View.MeasureSpec.getSize(widthSpec) + - view.getPaddingLeft() + - view.getPaddingRight(); + if (width <= 0) return; + // EXACTLY discovers native legacy/new padding semantics without guessing row fields. + row.patch_setCaptionText("M"); + measure.measure( + View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY), + View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)); + StaticLayout probe = row.patch_getCaptionLayout(); + if (probe == null || probe.getWidth() <= 0) return; + if (cache == null || !cache.matches(probe)) { + if (cache != null) cache.cancelled = true; + cache = new Cache(probe.getWidth(), probe.getPaint()); + frame.state.caches.put(view, cache); + Cache ready = null; + for (Cache prepared : frame.state.ready) { + if (prepared.matches(probe)) { + ready = prepared; + break; + } + } + if (ready != null) { + frame.state.ready.remove(ready); + frame.state.ready.add(0, ready); + cache.blocks = ready.blocks; + } else { + Cache next = cache; + WeakReference redraw = new WeakReference<>(view); + Utils.runOnBackgroundThread( + () -> { + try { + next.blocks = + frame.state.policy.plan( + text -> next.layout(text).getLineCount() <= 2); + } catch (CancellationException ex) { + // A newer text layout replaced this cache. + next.blocks = List.of(); + } catch (Exception ex) { + Logger.printException(() -> "plan failure", ex); + next.blocks = List.of(); + } + Utils.runOnMainThread( + () -> { + if (next.cancelled) return; + if (!next.blocks.isEmpty()) { + frame.state.ready.removeIf( + prepared -> + prepared.width == next.width + && prepared.paint + .equalsForTextMeasurement( + next.paint)); + frame.state.ready.add(0, next); + if (frame.state.ready.size() > 3) + frame.state.ready.remove(3); + } + View target = redraw.get(); + if (target != null) target.requestLayout(); + }); + }); + } + } + cache.nativeFull = nativeFull; + cache.nativeVisible = nativeVisible; + if (cache.blocks == null) return; + StableCaptionBlockPolicy.Block block = + StableCaptionBlockPolicy.at(cache.blocks, frame.time); + if (block == null || !block.fits) return; + if (cache.last != block) { + SpannableStringBuilder text = new SpannableStringBuilder(block.text); + text.setSpan(FIXED_PARAGRAPH, 0, text.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); + cache.text = text; + cache.last = block; + } + full.clear(); + full.add(cache.text); + visible.clear(); + visible.add(cache.text); + } catch (Exception ex) { + Logger.printException(() -> "prepare failure", ex); + } + } + + private static StaticLayout.Builder blockLayout(CharSequence text, TextPaint paint, int width) { + return StaticLayout.Builder.obtain(text, 0, text.length(), paint, width) + .setAlignment(Layout.Alignment.ALIGN_CENTER) + .setBreakStrategy(Layout.BREAK_STRATEGY_BALANCED) + .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NONE); + } + + private static boolean marked(List lines) { + for (CharSequence text : lines) + if (text instanceof Spanned spans && spans.getSpanStart(FIXED_PARAGRAPH) >= 0) + return true; + return false; + } + + /** Preserve the native constructor for every unmarked caption, including prepared tracks. */ + @SuppressWarnings("deprecation") + public static StaticLayout createLayout( + CharSequence text, + TextPaint paint, + int width, + Layout.Alignment alignment, + float spacingMult, + float spacingAdd, + boolean includePad) { + if (ENABLED && text instanceof Spanned spans && spans.getSpanStart(FIXED_PARAGRAPH) >= 0) { + return blockLayout(text, paint, width) + .setLineSpacing(spacingAdd, spacingMult) + .setIncludePad(includePad) + .build(); + } + return new StaticLayout(text, paint, width, alignment, spacingMult, spacingAdd, includePad); + } +} diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/TranslatedCaptionPensPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/TranslatedCaptionPensPatch.java index 8345cb8b46..d406ece0f4 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/TranslatedCaptionPensPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/TranslatedCaptionPensPatch.java @@ -10,6 +10,15 @@ import androidx.annotation.Nullable; +import org.w3c.dom.Document; + +import java.io.ByteArrayInputStream; +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; + +import app.morphe.extension.shared.Logger; + @SuppressWarnings("unused") public class TranslatedCaptionPensPatch { @@ -39,6 +48,11 @@ public interface CaptionRequestInterface { */ private static final ThreadLocal isAutoTranslatedTrack = new ThreadLocal<>(); + /** + * If the caption track parsed on the current thread is an auto-translated automatic caption track. + */ + private static final ThreadLocal isTranslatedAutomaticTrack = new ThreadLocal<>(); + /** * Injection point. * @@ -48,16 +62,52 @@ public interface CaptionRequestInterface { */ public static void setParsedRequest(@Nullable Object request) { boolean translated = false; + boolean automatic = false; try { - translated = request instanceof CaptionRequestInterface captionRequest - && captionRequest.patch_getCaptionTrack() instanceof PreferredCaptionLanguagePatch.CaptionTrackInterface track - // Auto-translated vss ids are "t" + source vss id + "." + language code. - && track.patch_getVssId().startsWith("t"); - } catch (RuntimeException | LinkageError ignored) { - // Unknown native layouts must not retain the previous decoder's styling. + if (request instanceof CaptionRequestInterface captionRequest + && captionRequest.patch_getCaptionTrack() instanceof PreferredCaptionLanguagePatch.CaptionTrackInterface track) { + String vssId = track.patch_getVssId(); + // Auto-translated vss ids are "t" + source vss id + "." + language code. + translated = vssId.startsWith("t"); + automatic = track instanceof AutoTranslateCaptionsPatch.CaptionTrackInterface autoTrack + && AutoTranslateCaptionsPatch.isAutomaticCaption(vssId, autoTrack.patch_getUrl()); + } + } catch (RuntimeException | LinkageError ex) { + Logger.printException(() -> "setParsedRequest failure", ex); } - // A failed native getter must not leave the previous translated state enabled. + // A failed native getter must not leave the previous track state enabled. isAutoTranslatedTrack.set(translated); + isTranslatedAutomaticTrack.set(translated && automatic); + StableCaptionBlocks.setAutomaticTrack(automatic); + } + + /** + * Injection point. + * + * Called before a caption track file is decoded. + */ + public static InputStream repairInput(@Nullable InputStream input) throws IOException { + boolean repair = Boolean.TRUE.equals(isTranslatedAutomaticTrack.get()); + if (input == null || (!repair && !StableCaptionBlocks.isCapturing())) return input; + CaptionInputReplay.Copy copy = CaptionInputReplay.copy(input, CaptionXml.MAX_BYTES); + if (!copy.complete) return copy.input; + Document document = CaptionXml.read(copy.bytes); + // Capture the source captions before the repair removes events from the document. + StableCaptionBlocks.capture(document); + byte[] repaired = repair + ? TranslatedRollingCaptionPolicy.repair(copy.bytes, document) + : copy.bytes; + if (repaired == copy.bytes) return copy.input; + return new FilterInputStream(new ByteArrayInputStream(repaired)) { + @Override + public void close() throws IOException { + try { + super.close(); + } finally { + input.close(); + } + } + }; } /** diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/TranslatedRollingCaptionPolicy.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/TranslatedRollingCaptionPolicy.java new file mode 100644 index 0000000000..0194bd1928 --- /dev/null +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/TranslatedRollingCaptionPolicy.java @@ -0,0 +1,165 @@ +/* + * Copyright 2026 Morphe. + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.extension.youtube.patches; + +import static app.morphe.extension.youtube.patches.CaptionXml.children; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NamedNodeMap; + +import java.io.ByteArrayOutputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Pattern; + +import javax.xml.transform.OutputKeys; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; + +import app.morphe.extension.shared.Logger; + +/** Remove orphan ASR append events, never normalize sampled caption strings. */ +final class TranslatedRollingCaptionPolicy { + private TranslatedRollingCaptionPolicy() {} + + static byte[] repair(byte[] input, Document doc) { + try { + if (doc == null) return input; + Element root = doc.getDocumentElement(); + Element head = onlyChild(root, "head"), body = onlyChild(root, "body"); + if (head == null || body == null) return input; + Map styles = new HashMap<>(); + Set duplicateStyles = new HashSet<>(); + for (Element e : children(head)) + if (e.getTagName().equals("ws")) { + String id = e.getAttribute("id"); + if (id.trim().isEmpty()) continue; + if (styles.put(id, "2".equals(e.getAttribute("mh"))) != null) + duplicateStyles.add(id); + } + Map windows = new HashMap<>(); + Set duplicateWindows = new HashSet<>(); + Map> events = new HashMap<>(); + for (Element e : children(body)) { + if (e.getTagName().equals("w")) { + String id = e.getAttribute("id"); + if (id.trim().isEmpty()) continue; + if (windows.put(id, e.getAttribute("ws")) != null) duplicateWindows.add(id); + } else if (e.getTagName().equals("p")) { + String id = e.getAttribute("w"); + if (!id.isEmpty()) + events.computeIfAbsent(id, k -> new ArrayList<>()).add(new Event(e)); + } + } + List remove = new ArrayList<>(); + for (Map.Entry> entry : events.entrySet()) { + String window = entry.getKey(), style = windows.get(window); + if (style == null || style.trim().isEmpty()) continue; + if (duplicateWindows.contains(window) + || duplicateStyles.contains(style) + || !Boolean.TRUE.equals(styles.get(style))) continue; + List list = entry.getValue(); + boolean valid = true; + long previous = -1; + // Untimed/unknown events are local barriers, not grounds to disable unrelated valid + // runs. + for (Event e : list) + if (e.valid) { + if (e.start < previous) { + valid = false; + break; + } + previous = e.start; + } + if (!valid) continue; + int i = 1; + while (i < list.size()) { + Event prior = list.get(i - 1); + if (!prior.text || !list.get(i).newline) { + i++; + continue; + } + int end = i; + while (end < list.size() && list.get(end).newline) end++; + if (end - i >= 2 && end < list.size()) { + Event next = list.get(end), first = list.get(i), last = list.get(end - 1); + if (next.text + && prior.end <= next.start + && first.end == prior.end + && prior.start < first.start + && last.start < next.start + && last.end > next.start) { + for (int n = i; n < end; n++) remove.add(list.get(n).element); + } + } + i = end; + } + } + if (remove.isEmpty()) return input; + for (Element e : remove) body.removeChild(e); + Transformer transformer = TransformerFactory.newInstance().newTransformer(); + transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8"); + transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes"); + transformer.setOutputProperty(OutputKeys.INDENT, "no"); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + transformer.transform(new DOMSource(doc), new StreamResult(out)); + return out.toByteArray(); + } catch (Exception ex) { + Logger.printException(() -> "repair failure", ex); + return input; + } + } + + private static Element onlyChild(Element parent, String tag) { + Element result = null; + for (Element e : children(parent)) + if (e.getTagName().equals(tag)) { + if (result != null) return null; + result = e; + } + return result; + } + + private static final Pattern TIME_PATTERN = Pattern.compile("[0-9]{1,15}"); + + private static final class Event { + final Element element; + long start, end; + boolean valid, text, newline; + + Event(Element element) { + this.element = element; + String time = element.getAttribute("t"), durationText = element.getAttribute("d"); + // Untimed events are not valid. + if (!TIME_PATTERN.matcher(time).matches() || !TIME_PATTERN.matcher(durationText).matches()) { + return; + } + start = Long.parseLong(time); + long duration = Long.parseLong(durationText); + if (duration <= 0) return; + end = start + duration; + valid = true; + String content = element.getTextContent(), append = element.getAttribute("a"); + text = (append.isEmpty() || append.equals("0")) && !content.trim().isEmpty(); + newline = append.equals("1") && content.equals("\n") && children(element).isEmpty(); + NamedNodeMap attrs = element.getAttributes(); + for (int i = 0; i < attrs.getLength(); i++) { + String name = attrs.item(i).getNodeName(); + if (!name.equals("t") + && !name.equals("d") + && !name.equals("w") + && !name.equals("a")) newline = false; + } + } + } +} diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/Settings.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/Settings.java index 7f170f9c51..fb44265433 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/Settings.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/Settings.java @@ -12,6 +12,8 @@ import static app.morphe.extension.shared.sponsorblock.objects.CategoryBehaviour.SKIP_AUTOMATICALLY; import static app.morphe.extension.shared.sponsorblock.objects.CategoryBehaviour.SKIP_AUTOMATICALLY_ONCE; +import android.os.Build; + import java.util.List; import app.morphe.extension.shared.Logger; @@ -410,6 +412,8 @@ public class Settings extends SharedYouTubeSettings { public static final StringSetting CAPTION_COOKIES = new StringSetting("morphe_caption_cookies", "", true, parent(SET_CAPTION_COOKIES)); public static final BooleanSetting FIX_TRANSCRIPT = new BooleanSetting("morphe_fix_transcript", TRUE, true); public static final BooleanSetting CAPTIONS_IN_PIP = new BooleanSetting("morphe_captions_in_pip", TRUE); + public static final BooleanSetting STABLE_CAPTION_BLOCKS = new BooleanSetting("morphe_stable_caption_blocks", FALSE, true, + () -> Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q); // Miniplayer public static final BooleanSetting MINIPLAYER_DISABLE_RESUMING = new BooleanSetting("morphe_miniplayer_disable_resuming", FALSE, true); diff --git a/extensions/youtube/src/test/java/app/morphe/extension/youtube/patches/AutoTranslateCaptionsPatchTest.java b/extensions/youtube/src/test/java/app/morphe/extension/youtube/patches/AutoTranslateCaptionsPatchTest.java index 8cb4a2da76..f66255409a 100644 --- a/extensions/youtube/src/test/java/app/morphe/extension/youtube/patches/AutoTranslateCaptionsPatchTest.java +++ b/extensions/youtube/src/test/java/app/morphe/extension/youtube/patches/AutoTranslateCaptionsPatchTest.java @@ -88,19 +88,35 @@ private static CaptionTrackInterface find(List tracks, St assertEquals(URL + "&tlang=el&fmt=srv3#opaque", find(result, "el").patch_getUrl()); } - @Test public void nativeTranslationsMustShareSourceBeforeExpansion() { - Track english = new Track("de", "https://www.youtube.com/api/timedtext?v=abcdefghijk&lang=en&sig=x%2By&tlang=de", + @Test public void mixedSourcesFromTheSameVideoStillExpandWithoutReplacingNativeTargets() { + for (String video : List.of("bYsvNgA-tEE", "l8Sgq4zw1MY")) { + String base = "https://www.youtube.com/api/timedtext?v=" + video; + Track german = new Track("de", base + "&lang=en&kind=asr&sig=x%2By&tlang=de&fmt=srv3#opaque", + "ta.en.de", "German"); + String source = video.equals("bYsvNgA-tEE") ? "de" : "tr"; + Track english = new Track("en", base + "&lang=" + source + "&kind=asr&sig=other&tlang=en", + "ta." + source + ".en", "English"); + List result = add(List.of(german, english), Locale.ENGLISH); + assertEquals(156, result.size()); + assertSame(german, find(result, "de")); + assertSame(english, find(result, "en")); + assertEquals(base + "&lang=en&kind=asr&sig=x%2By&tlang=tr&fmt=srv3#opaque", + find(result, "tr").patch_getUrl()); + assertEquals("ta.en.tr", find(result, "tr").patch_getVssId()); + } + } + + @Test public void mixedSourceExpansionRequiresTheSameUnambiguousVideo() { + Track german = new Track("de", "https://www.youtube.com/api/timedtext?v=abcdefghijk&lang=en&tlang=de", "ta.en.de", "German"); - Track french = new Track("fr", "https://www.youtube.com/api/timedtext?v=abcdefghijk&lang=fr&sig=x%2By&tlang=fr", - "ta.fr.fr", "French"); - Track otherVideo = new Track("fr", "https://www.youtube.com/api/timedtext?v=zyxwvutsrqp&lang=en&sig=x%2By&tlang=fr", - "ta.en.fr", "French"); - for (Track other : List.of(french, otherVideo)) { - List nativeList = List.of(english, other); + for (String url : List.of( + "https://www.youtube.com/api/timedtext?v=zyxwvutsrqp&lang=tr&tlang=en", + "https://www.youtube.com/api/timedtext?lang=tr&tlang=en", + "https://www.youtube.com/api/timedtext?v=abcdefghijk&v=zyxwvutsrqp&lang=tr&tlang=en", + "https://www.youtube.com/api/timedtext?v=abcdefghijk&lang=tr&tlang=en&tlang=fr")) { + List nativeList = List.of(german, new Track("en", url, "ta.tr.en", "English")); assertSame(nativeList, add(nativeList, Locale.ENGLISH)); } - assertSame(english, find(add(List.of(english), Locale.ENGLISH), "de")); - find(add(List.of(english), Locale.ENGLISH), "tr"); } @Test public void nativeTranslationTargetMustBeUnambiguousAndInTheActualQuery() { diff --git a/extensions/youtube/src/test/java/app/morphe/extension/youtube/patches/CaptionInputReplayTest.java b/extensions/youtube/src/test/java/app/morphe/extension/youtube/patches/CaptionInputReplayTest.java new file mode 100644 index 0000000000..513024ee60 --- /dev/null +++ b/extensions/youtube/src/test/java/app/morphe/extension/youtube/patches/CaptionInputReplayTest.java @@ -0,0 +1,137 @@ +package app.morphe.extension.youtube.patches; + +import static org.junit.Assert.*; + +import org.junit.Test; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; + +public class CaptionInputReplayTest { + + @Test + public void preservesEveryInputByteIncludingNewlines() throws Exception { + byte[] bytes = "

    A\n\nB\n

    ".getBytes("UTF-8"); + CaptionInputReplay.Copy copy = + CaptionInputReplay.copy(new ByteArrayInputStream(bytes), 1024); + assertTrue(copy.complete); + assertArrayEquals(bytes, copy.bytes); + assertArrayEquals(bytes, read(copy.input)); + } + + @Test + public void oversizedInputStillReachesParserUnchanged() throws Exception { + byte[] bytes = new byte[8192]; + for (int i = 0; i < bytes.length; i++) bytes[i] = (byte) i; + CaptionInputReplay.Copy copy = CaptionInputReplay.copy(new ByteArrayInputStream(bytes), 31); + assertFalse(copy.complete); + assertTrue(copy.bytes.length <= 32); + assertArrayEquals(bytes, read(copy.input)); + } + + @Test + public void preservesEmptyAndExactLimitInput() throws Exception { + assertTrue(CaptionInputReplay.copy(new ByteArrayInputStream(new byte[0]), 10).complete); + byte[] bytes = new byte[10]; + CaptionInputReplay.Copy copy = CaptionInputReplay.copy(new ByteArrayInputStream(bytes), 10); + assertTrue(copy.complete); + assertArrayEquals(bytes, read(copy.input)); + } + + @Test(expected = IOException.class) + public void preservesReadFailures() throws Exception { + CaptionInputReplay.copy( + new InputStream() { + @Override + public int read() throws IOException { + throw new IOException("test failure"); + } + }, + 1024); + } + + @Test + public void sharedTrackRoutingRepairsInputAndRetainsParserOwnedClose() throws Exception { + byte[] source = + ("" + + "" + + "

    one

    " + + "

    \n

    " + + "

    \n

    " + + "

    two

    ") + .getBytes("UTF-8"); + boolean[] closed = {false}; + InputStream original = + new ByteArrayInputStream(source) { + @Override + public void close() { + closed[0] = true; + } + }; + Track track = new Track(); + TranslatedCaptionPensPatch.setParsedRequest( + (TranslatedCaptionPensPatch.CaptionRequestInterface) () -> track); + assertEquals(1, track.vssReads); + InputStream repaired = TranslatedCaptionPensPatch.repairInput(original); + assertEquals(2, CaptionXml.read(read(repaired)).getElementsByTagName("p").getLength()); + assertFalse(closed[0]); + repaired.close(); + assertTrue(closed[0]); + // A non-caption request must clear the previous translated decoder state. + TranslatedCaptionPensPatch.setParsedRequest(null); + InputStream next = new ByteArrayInputStream(source); + assertSame(next, TranslatedCaptionPensPatch.repairInput(next)); + } + + @Test + public void failedSourceGetterClearsRepairAndPreservesTranslatedStyle() throws Exception { + Track track = new Track(); + TranslatedCaptionPensPatch.CaptionRequestInterface request = () -> track; + try { + TranslatedCaptionPensPatch.setParsedRequest(request); + track.urlUnavailable = true; + TranslatedCaptionPensPatch.setParsedRequest(request); + assertEquals(Integer.MIN_VALUE, TranslatedCaptionPensPatch.getPenId(7)); + InputStream input = new ByteArrayInputStream(new byte[] {1}); + assertSame(input, TranslatedCaptionPensPatch.repairInput(input)); + } finally { + TranslatedCaptionPensPatch.setParsedRequest(null); + } + } + + private static final class Track implements AutoTranslateCaptionsPatch.CaptionTrackInterface { + int vssReads; + boolean urlUnavailable; + @Override + public String patch_getLanguageCode() { + return "de"; + } + + @Override + public String patch_getVssId() { + vssReads++; + return "ta.en.de"; + } + + @Override + public String patch_getUrl() { + if (urlUnavailable) throw new NoSuchMethodError("URL getter unavailable"); + return "https://www.youtube.com/api/timedtext?kind=asr&lang=en&tlang=de"; + } + + @Override + public AutoTranslateCaptionsPatch.CaptionTrackInterface patch_copyTranslation( + String languageCode, String url, String vssId, CharSequence label) { + throw new UnsupportedOperationException(); + } + } + + private static byte[] read(InputStream input) throws IOException { + java.io.ByteArrayOutputStream output = new java.io.ByteArrayOutputStream(); + byte[] buffer = new byte[113]; + int n; + while ((n = input.read(buffer)) != -1) output.write(buffer, 0, n); + return output.toByteArray(); + } +} diff --git a/extensions/youtube/src/test/java/app/morphe/extension/youtube/patches/CaptionViewportPolicyTest.java b/extensions/youtube/src/test/java/app/morphe/extension/youtube/patches/CaptionViewportPolicyTest.java new file mode 100644 index 0000000000..1b2a692a15 --- /dev/null +++ b/extensions/youtube/src/test/java/app/morphe/extension/youtube/patches/CaptionViewportPolicyTest.java @@ -0,0 +1,130 @@ +package app.morphe.extension.youtube.patches; + +import static org.junit.Assert.*; + +import org.junit.Test; + +public class CaptionViewportPolicyTest { + private static final String A = "videoA00001", B = "videoB00002"; + + @Test + public void fitsRealContentHeightOnFoldFullscreen() { + CaptionViewportPolicy p = new CaptionViewportPolicy(); + p.select(A); + p.record(A, 1920, 1080); + assertEquals(1229, p.height(2184, 1968, true, false)); + p.record(A, 1440, 1080); + assertEquals(1638, p.height(2184, 1968, true, false)); + p.record(A, 1080, 1080); + assertEquals(1968, p.height(2184, 1968, true, false)); + p.record(A, 1080, 1920); + assertEquals(1968, p.height(2184, 1968, true, false)); + } + + @Test + public void preservesOrdinaryPortraitInlineAndUnknownGeometry() { + CaptionViewportPolicy p = new CaptionViewportPolicy(); + p.select(A); + assertEquals(1968, p.height(2184, 1968, true, false)); + p.record(A, 1920, 1080); + assertEquals(608, p.height(1080, 608, true, false)); + assertEquals(1080, p.height(2520, 1080, true, false)); + assertEquals(2184, p.height(1968, 2184, true, false)); + assertEquals(1968, p.height(2184, 1968, false, false)); + assertEquals(0, p.height(2184, 0, true, false)); + } + + @Test + public void prefetchedFormatsNeverReplaceSelectedVideo() { + CaptionViewportPolicy p = new CaptionViewportPolicy(); + p.select(A); + p.record(A, 1920, 1080); + p.record(B, 1440, 1080); + assertEquals(1229, p.height(2184, 1968, true, false)); + p.select(B); + assertEquals(1638, p.height(2184, 1968, true, false)); + p.select("unknown0001"); + assertEquals(1968, p.height(2184, 1968, true, false)); + } + + @Test + public void coldStartSupportsFormatsBeforeAndAfterSelection() { + CaptionViewportPolicy p = new CaptionViewportPolicy(); + p.record(A, 1920, 1080); + p.select(A); + assertEquals(1229, p.height(2184, 1968, true, false)); + p.select(B); + assertEquals(1968, p.height(2184, 1968, true, false)); + p.record(B, 1920, 1080); + assertEquals(1229, p.height(2184, 1968, true, false)); + } + + @Test + public void malformedMetadataDoesNotOverrideValidSnapshot() { + CaptionViewportPolicy p = new CaptionViewportPolicy(); + p.select(A); + p.record(A, 1920, 1080); + p.record(A, 0, 1080); + p.record(A, -1, 1080); + p.record(A, 100000, 1); + assertEquals(1229, p.height(2184, 1968, true, false)); + p.select(null); + assertEquals(1968, p.height(2184, 1968, true, false)); + } + + @Test + public void boundedPrefetchCacheRetainsSelectedSnapshot() { + CaptionViewportPolicy p = new CaptionViewportPolicy(); + p.select(A); + p.record(A, 1920, 1080); + for (int i = 0; i < 100; i++) p.record(String.format("%011d", i), 1440, 1080); + assertEquals(1229, p.height(2184, 1968, true, false)); + p.select(String.format("%011d", 68)); + assertEquals(1968, p.height(2184, 1968, true, false)); + p.select(String.format("%011d", 69)); + assertEquals(1638, p.height(2184, 1968, true, false)); + p.select(String.format("%011d", 99)); + assertEquals(1638, p.height(2184, 1968, true, false)); + } + + @Test + public void landscapeTransitionFitsEveryAnimatedFrameWithoutThresholdJump() { + CaptionViewportPolicy p = new CaptionViewportPolicy(); + p.select(A); + p.record(A, 1920, 1080); + // Physical Fold exit trace, in reverse for entry as well. + int[][] frames = { + {2184, 1968}, + {2089, 1807}, + {1974, 1613}, + {1849, 1403}, + {1810, 1336}, + {1722, 1187}, + {1599, 979}, + {1531, 864}, + {1528, 859} + }; + for (int[] f : frames) { + int expected = Math.min(f[1], Math.round(f[0] * 9f / 16f)); + assertEquals( + "animated frame " + f[0] + "x" + f[1], + expected, + p.height(f[0], f[1], true, true)); + } + // Target orientation is landscape even if an intermediate viewport is portrait. + assertEquals(1107, p.height(1968, 2184, true, true)); + assertEquals(2184, p.height(1968, 2184, true, false)); + assertEquals(1968, p.height(2184, 1968, false, true)); + } + + @Test + public void transitionKeepsUnknownGeometryAndNonWideContentNative() { + CaptionViewportPolicy p = new CaptionViewportPolicy(); + p.select(A); + assertEquals(1968, p.height(2184, 1968, true, true)); + p.record(A, 1080, 1920); + assertEquals(1968, p.height(2184, 1968, true, true)); + p.record(A, 1440, 1080); + assertEquals(1638, p.height(2184, 1968, true, true)); + } +} diff --git a/extensions/youtube/src/test/java/app/morphe/extension/youtube/patches/PreferredCaptionRoutingTest.java b/extensions/youtube/src/test/java/app/morphe/extension/youtube/patches/PreferredCaptionRoutingTest.java index 476659635b..1232cc14e1 100644 --- a/extensions/youtube/src/test/java/app/morphe/extension/youtube/patches/PreferredCaptionRoutingTest.java +++ b/extensions/youtube/src/test/java/app/morphe/extension/youtube/patches/PreferredCaptionRoutingTest.java @@ -67,4 +67,28 @@ private static PreferredCaptionLanguagePatch.CaptionTrackInterface find( Track hebrew = new Track("iw", "ta.en.iw"); assertSame(hebrew, find(List.of(hebrew), "he")); } + @Test public void chineseRegionRulesDoNotRejectOtherLanguages() { + Track english = new Track("en-TW", "ta.ar.en-TW"); + Track german = new Track("de-CN", "ta.ar.de-CN"); + assertSame(english, find(List.of(english), "en-CN")); + assertSame(german, find(List.of(german), "de-HK")); + } + + @Test public void explicitChineseScriptTakesPriorityOverRegion() { + Track simplified = new Track("zh-Hans-TW", "ta.en.zh-Hans-TW"); + Track traditional = new Track("zh-Hant-CN", "ta.en.zh-Hant-CN"); + assertSame(simplified, find(List.of(simplified), "zh-CN")); + assertSame(traditional, find(List.of(traditional), "zh-TW")); + assertNull(find(List.of(simplified), "zh-Hant")); + assertNull(find(List.of(traditional), "zh-Hans")); + } + + @Test public void legacyAliasesPreserveExactRegionalSelection() { + Track generic = new Track("fil", "ta.en.fil"); + Track regional = new Track("tl-PH", "ta.en.tl-PH"); + Track norwegian = new Track("nb", "ta.en.nb"); + assertSame(regional, find(List.of(generic, regional), "fil-PH")); + assertSame(norwegian, find(List.of(norwegian), "no")); + } + } diff --git a/extensions/youtube/src/test/java/app/morphe/extension/youtube/patches/StableCaptionBlockPolicyTest.java b/extensions/youtube/src/test/java/app/morphe/extension/youtube/patches/StableCaptionBlockPolicyTest.java new file mode 100644 index 0000000000..0d0bf0a90c --- /dev/null +++ b/extensions/youtube/src/test/java/app/morphe/extension/youtube/patches/StableCaptionBlockPolicyTest.java @@ -0,0 +1,306 @@ +package app.morphe.extension.youtube.patches; + +import static org.junit.Assert.*; + +import org.junit.Test; + +import java.nio.charset.StandardCharsets; +import java.util.List; + +public class StableCaptionBlockPolicyTest { + static byte[] xml(String body) { + return ("" + + body + + "") + .getBytes(StandardCharsets.UTF_8); + } + + static String p(int t, int d, String s) { + return "

    " + s + "

    "; + } + + static String s(int t, String text) { + return "" + text + ""; + } + + private static StableCaptionBlockPolicy parse(byte[] input) { + return StableCaptionBlockPolicy.parse(CaptionXml.read(input)); + } + + static StableCaptionBlockPolicy policy(String body) { + StableCaptionBlockPolicy p = parse(xml(body)); + assertNotNull(p); + return p; + } + + @Test + public void alreadyCenteredFrameRetainsItsNativeList() { + var frame = new StableCaptionBlocks.Frame(new StableCaptionBlocks.State(), 0, 0); + var cue = new StableCaptionBlocks.Cue() { + @Override public CharSequence patch_getOriginalCaptionVisible() { return "native"; } + @Override public StableCaptionBlocks.Frame patch_getBlockFrame() { return frame; } + @Override public Object patch_copyBlockFrame(StableCaptionBlocks.Frame requested) { + assertSame(frame, requested); + return this; + } + }; + List original = List.of(cue); + assertSame(original, StableCaptionBlocks.centerBlocks(original)); + } + + @Test + public void sharedDocumentKeepsSourcePlanWhenRepairRemovesOrphanEvents() { + byte[] source = + ("" + + "" + + p(0, 2000, "one") + + "

    \n

    " + + "

    \n

    " + + p(2000, 4000, "two") + + "
    ") + .getBytes(StandardCharsets.UTF_8); + var document = CaptionXml.read(source); + assertNotNull(document); + var policy = StableCaptionBlockPolicy.parse(document); + assertNotNull(policy); + var before = policy.plan(text -> true); + byte[] repaired = TranslatedRollingCaptionPolicy.repair(source, document); + assertNotSame(source, repaired); + assertEquals(2, document.getElementsByTagName("p").getLength()); + assertEquals(4, CaptionXml.read(source).getElementsByTagName("p").getLength()); + var after = policy.plan(text -> true); + assertEquals(before.size(), after.size()); + for (int i = 0; i < before.size(); i++) { + assertEquals(before.get(i).start, after.get(i).start); + assertEquals(before.get(i).end, after.get(i).end); + assertEquals(before.get(i).text, after.get(i).text); + } + assertSame(repaired, TranslatedRollingCaptionPolicy.repair(repaired, CaptionXml.read(repaired))); + assertSame(source, TranslatedRollingCaptionPolicy.repair(source, null)); + } + + @Test + public void preservesSeparatelyTimedWhitespaceWithoutInventingWordTimes() { + var policy = policy(p(0, 4000, s(0, "hello") + s(500, " ") + s(1000, "world"))); + var blocks = policy.plan(text -> true); + assertEquals("hello world", blocks.get(0).text); + assertEquals(0, blocks.get(0).start); + assertEquals(1000, policy.templateTime(1100)); + } + + @Test + public void joinsAdjacentSourceParagraphsIntoOneReadableBlock() { + var p = + policy( + p(0, 4000, s(0, "one ") + s(1000, "two")) + + p(2000, 5000, s(0, "three ") + s(1000, "four"))); + var b = p.plan(text -> text.length() <= 24); + assertEquals(1, b.size()); + assertEquals("one two three four", b.get(0).text); + assertEquals(0, b.get(0).start); + assertEquals(7000, b.get(0).end); + } + + @Test + public void usesSourceBoundariesAndKeepsWholeBlockStable() { + var p = + policy( + p( + 0, + 6000, + s(0, "one ") + + s(1000, "two ") + + s(2000, "three ") + + s(3000, "four ") + + s(4000, "five"))); + var b = p.plan(text -> text.length() <= 14); + assertTrue(b.size() > 1); + for (var block : b) { + assertEquals(0, block.start % 1000); + assertTrue(block.fits); + assertTrue(block.text.length() <= 14); + assertTrue(block.end > block.start); + } + assertEquals( + "one two three four five", + String.join(" ", b.stream().map(x -> x.text).toArray(String[]::new))); + } + + @Test + public void redistributesShortTailWhenBothPiecesStillFit() { + var p = + policy( + p( + 0, + 6500, + s(0, "a ") + + s(1500, "b ") + + s(3000, "c ") + + s(4500, "d ") + + s(6000, "e"))); + var b = p.plan(text -> text.length() <= 7); + assertEquals(2, b.size()); + assertTrue(b.get(0).end - b.get(0).start >= 1500); + assertTrue(b.get(1).end - b.get(1).start >= 1500); + } + + @Test + public void sameTimeAtomsCannotBecomeTwoDifferentBlocks() { + var p = policy(p(0, 4000, s(0, "one ") + s(0, "two ") + s(2000, "three"))); + var b = p.plan(text -> text.length() <= 5); + assertEquals("one two", b.get(0).text); + assertFalse(b.get(0).fits); + assertEquals(2000, b.get(1).start); + } + + @Test + public void sourceGapsStayEmptyAndPreviewIsBounded() { + var p = + policy( + p(0, 1000, s(0, "first")) + + p(4000, 2000, s(0, "second")) + + p(20000, 1000, s(0, "third"))); + var b = p.plan(text -> true); + assertEquals(3, b.size()); + assertEquals(1000, b.get(0).end); + assertEquals(4000, b.get(1).start); + var slow = policy(p(0, 14000, s(0, "first ") + s(10000, "second"))).plan(text -> true); + assertEquals(2, slow.size()); + assertEquals(10000, slow.get(0).end); + } + + @Test + public void preservesUnicodeWithoutSplittingAtomicText() { + for (String text : List.of("שלום עולם", "مرحبا بالعالم", "日本語字幕", "👩🏽‍💻 merhaba")) { + var b = policy(p(0, 3000, s(0, text))).plan(value -> true); + assertEquals(text, b.get(0).text); + } + } + + @Test + public void unsupportedStructureFallsBackBeforePresentation() { + assertNull(parse(xml("" + p(0, 1000, "hello")))); + assertNull(parse(xml(p(0, 1000, "styled")))); + assertNull(parse(xml(p(0, 1000, s(500, "a ") + s(100, "b"))))); + } + + @Test + public void recognizesOnlyVerifiedAutomaticTracks() { + assertTrue( + AutoTranslateCaptionsPatch.isAutomaticCaption( + "a.en", "https://www.youtube.com/api/timedtext?kind=asr")); + assertTrue( + AutoTranslateCaptionsPatch.isAutomaticCaption( + "ta.en.tr", "https://www.youtube.com/api/timedtext?kind=asr&tlang=tr")); + assertFalse( + AutoTranslateCaptionsPatch.isAutomaticCaption( + ".en", "https://www.youtube.com/api/timedtext?kind=asr")); + assertFalse( + AutoTranslateCaptionsPatch.isAutomaticCaption( + "a.en", "https://evil.example/api/timedtext?kind=asr")); + } + + @Test + public void planIsSeekableAndDoesNotInventSubspanTimes() { + var p = policy(p(0, 16000, s(0, "a ") + s(4000, "b ") + s(8000, "c ") + s(12000, "d"))); + var plan = p.plan(text -> true); + assertEquals(2, plan.size()); + assertSame(plan.get(0), StableCaptionBlockPolicy.at(plan, 100)); + assertSame(plan.get(1), StableCaptionBlockPolicy.at(plan, 12500)); + assertSame(plan.get(0), StableCaptionBlockPolicy.at(plan, 100)); + assertNull(StableCaptionBlockPolicy.at(plan, 16000)); + assertEquals( + "a b c d", String.join(" ", plan.stream().map(b -> b.text).toArray(String[]::new))); + } + + @Test + public void longUnbreakableSegmentIsMarkedForNativeFallback() { + var plan = policy(p(0, 4000, s(0, "unbreakable"))).plan(text -> false); + assertEquals(1, plan.size()); + assertFalse(plan.get(0).fits); + assertEquals("unbreakable", plan.get(0).text); + } + + @Test + public void styledAndOversizedAtomicSourcesRemainNative() { + String styled = + "" + + p(0, 1000, "style") + + ""; + assertNull(parse(styled.getBytes(StandardCharsets.UTF_8))); + assertNull( + parse( + xml(p(0, 1000, s(0, "x".repeat(3000)) + s(0, "y".repeat(3000)))))); + } + + @Test + public void sourcePunctuationIsNotSeparatedIntoAnOrphanToken() { + var plan = + policy(p(0, 3000, "hello") + p(1000, 3000, ".") + p(2000, 3000, "world")) + .plan(text -> true); + assertEquals("hello. world", plan.get(0).text); + } + + @Test + public void emptySourceParagraphCreatesAnActualGap() { + var p = + policy( + p(0, 5000, s(0, "first")) + + p(1000, 2000, "") + + p(3000, 2000, s(0, "second"))); + var blocks = p.plan(text -> true); + assertEquals(2, blocks.size()); + assertEquals(1000, blocks.get(0).end); + assertNull(StableCaptionBlockPolicy.at(blocks, 2000)); + } + + @Test + public void balancesReadingLoadBeforeDenseTranslationAfterShortGap() { + String first = + s(0, "Aksi ") + + s(320, "takdirde, ") + + s(640, "geri ") + + s(960, "kalanını ") + + s(1280, "olduğu ") + + s(1600, "gibi ") + + s(1920, "bırakın"); + String[] words = + "bunu yapmanıza izin vermeden önce Windows'taki yönetici erişimine kabaca eşdeğer olan sudo'yu gerektirerek size bunu hatırlatacaktır." + .split(" "); + StringBuilder dense = new StringBuilder(); + for (int i = 0; i < words.length; i++) dense.append(s(i * 101, words[i] + " ")); + var p = + policy( + p(505359, 4400, first) + + p(507440, 4959, s(0, "ve ") + s(2159, "Linux,")) + + p(514159, 3281, dense.toString())); + var blocks = p.plan(text -> text.length() <= 110); + double worst = 0; + for (var b : blocks) worst = Math.max(worst, b.text.length() * 1000.0 / (b.end - b.start)); + assertTrue("dense translation must borrow reading time, worst=" + worst, worst < 40); + } + + @Test + public void borrowsShortUnclearedGapButNeverRevivesExplicitClear() { + var p = policy(p(0, 1000, "first") + p(2500, 1000, "second")); + var blocks = p.plan(text -> text.length() <= 6); + assertEquals(2500, blocks.get(0).end); + assertEquals(0, p.templateTime(2000)); + assertNotNull(StableCaptionBlockPolicy.at(blocks, 2000)); + var cleared = policy(p(0, 3000, "first") + p(1000, 1000, "") + p(2500, 1000, "second")); + assertEquals(-1, cleared.templateTime(2000)); + assertNull(StableCaptionBlockPolicy.at(cleared.plan(text -> true), 2000)); + assertEquals(-1, cleared.templateTime(-1)); + assertEquals(-1, cleared.templateTime(4000)); + } + + @Test + public void neverCutsTimedPartsInsideOneWord() { + var blocks = + policy(p(0, 3000, s(0, "win") + s(1000, "dows") + s(2000, "."))) + .plan(text -> text.length() <= 4); + assertEquals(1, blocks.size()); + assertEquals("windows.", blocks.get(0).text); + assertFalse(blocks.get(0).fits); + } +} diff --git a/extensions/youtube/src/test/java/app/morphe/extension/youtube/patches/TranslatedRollingCaptionPolicyTest.java b/extensions/youtube/src/test/java/app/morphe/extension/youtube/patches/TranslatedRollingCaptionPolicyTest.java new file mode 100644 index 0000000000..2f9bf80c22 --- /dev/null +++ b/extensions/youtube/src/test/java/app/morphe/extension/youtube/patches/TranslatedRollingCaptionPolicyTest.java @@ -0,0 +1,292 @@ +package app.morphe.extension.youtube.patches; + +import static org.junit.Assert.*; + +import org.junit.Test; +import org.w3c.dom.Element; +import org.w3c.dom.NodeList; + +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +import javax.xml.parsers.DocumentBuilderFactory; + +public class TranslatedRollingCaptionPolicyTest { + private static byte[] repair(byte[] input) { + return TranslatedRollingCaptionPolicy.repair(input, CaptionXml.read(input)); + } + + private static byte[] utf(String s) { + return s.getBytes(StandardCharsets.UTF_8); + } + + private static String document(String body) { + return "" + + body + + ""; + } + + private static String p(int t, int d, String text) { + return "

    " + text + "

    "; + } + + private static String br(int t, int d) { + return "

    \n

    "; + } + + private static List paragraphs(byte[] bytes) throws Exception { + NodeList ps = + DocumentBuilderFactory.newInstance() + .newDocumentBuilder() + .parse(new ByteArrayInputStream(bytes)) + .getElementsByTagName("p"); + List out = new ArrayList<>(); + for (int i = 0; i < ps.getLength(); i++) { + Element e = (Element) ps.item(i); + out.add( + e.getAttribute("t") + + "|" + + e.getAttribute("d") + + "|" + + e.getAttribute("w") + + "|" + + e.getAttribute("a") + + "|" + + e.getTextContent()); + } + return out; + } + + @Test + public void removesCapturedOrphanRunsButKeepsNormalTransitionsAndSpans() throws Exception { + String a = p(8080, 4400, "A & X"); + String b = p(12480, 5360, "B C"); + String c = p(17840, 5519, "Türkçe 😀"); + String d = p(20160, 5199, "D"); + byte[] source = + utf( + document( + a + + br(11030, 1450) + + br(12470, 2970) + + b + + br(15430, 2410) + + br(17830, 2330) + + c + + br(20150, 3209) + + d)); + byte[] fixed = repair(source); + assertNotSame(source, fixed); + assertEquals(paragraphs(utf(document(a + b + c + br(20150, 3209) + d))), paragraphs(fixed)); + String text = new String(fixed, StandardCharsets.UTF_8); + assertTrue(text.contains("p=\"3\"")); + assertTrue(text.contains("p=\"4\"")); + assertTrue(text.contains("t=\"226\"")); + assertSame(fixed, repair(fixed)); + } + + @Test + public void preservesLegitimateSingleNewlineByteForByte() { + byte[] source = utf(document(p(100, 1000, "A") + br(490, 610) + p(500, 1000, "B"))); + assertSame(source, repair(source)); + } + + @Test + public void preservesActualNewlinesInsideText() { + byte[] source = utf(document(p(100, 1000, "A\n\nB"))); + assertSame(source, repair(source)); + } + + @Test + public void doesNotMixInterleavedWindows() throws Exception { + String other = "

    Other

    "; + byte[] source = + utf( + document( + p(8080, 4400, "A") + + br(11030, 1450) + + other + + br(12470, 2970) + + p(12480, 5360, "B"))); + List fixed = paragraphs(repair(source)); + assertEquals(3, fixed.size()); + assertTrue(fixed.toString().contains("Other")); + } + + @Test + public void doesNotTouchNonRollingOrUnknownWindows() { + String body = p(8080, 4400, "A") + br(11030, 1450) + br(12470, 2970) + p(12480, 5360, "B"); + for (String xml : + new String[] { + document(body).replace("mh='2'", "mh='0'"), + document(body).replace("", ""), + document(body).replace("", "") + }) { + byte[] b = utf(xml); + assertSame(b, repair(b)); + } + } + + @Test + public void ambiguousWindowDefinitionsAreUnchanged() { + byte[] source = + utf( + document( + "" + + p(8080, 4400, "A") + + br(11030, 1450) + + br(12470, 2970) + + p(12480, 5360, "B"))); + assertSame(source, repair(source)); + } + + @Test + public void missingOrBlankMetadataNeverIdentifiesRollingWindows() { + String body = p(8080, 4400, "A") + br(11030, 1450) + br(12470, 2970) + p(12480, 5360, "B"); + String source = document(body); + for (String xml : + new String[] { + source.replace("", "") + .replace("", ""), + source.replace("", "") + .replace("ws='1'", "ws=''"), + source.replace("", "") + .replace("ws='1'", "ws=' '"), + source.replace("", ""), + source.replace("id='1' ws='1'", "id='' ws='1'") + }) { + byte[] bytes = utf(xml); + assertSame(xml, bytes, repair(bytes)); + } + } + + @Test + public void preservesRunsWithUnknownAttributesOrActivePriorText() { + String body = p(8080, 4400, "A") + br(11030, 1450) + br(12470, 2970) + p(12480, 5360, "B"); + for (String s : + new String[] { + body.replace("a='1'", "a='1' p='3'"), + body.replace("t='8080' d='4400'", "t='8080' d='5000'"), + body.replace("t='11030' d='1450'", "t='11030' d='1440'") + }) { + byte[] b = utf(document(s)); + assertSame(b, repair(b)); + } + } + + @Test + public void malformedTimesAndUnterminatedRunsAreUnchanged() { + String body = p(8080, 4400, "A") + br(11030, 1450) + br(12470, 2970) + p(12480, 5360, "B"); + for (String s : + new String[] { + body.replace("t='12470'", "t='invalid'"), + body.replace("d='2970'", "d='-1'"), + p(8080, 4400, "A") + br(11030, 1450) + br(12470, 2970) + }) { + byte[] b = utf(document(s)); + assertSame(b, repair(b)); + } + } + + @Test + public void malformedUnknownAndDoctypeInputsPassThrough() { + for (String s : + new String[] { + "", + "", + "]>&x;" + }) { + byte[] b = utf(s); + assertSame(b, repair(b)); + } + } + + @Test + public void unrelatedUntimedAppendDoesNotDisableEarlierRepair() throws Exception { + String a = p(8080, 4400, "A"), b = p(12480, 5360, "B"); + String untimed = "

    \n

    "; + byte[] source = utf(document(a + br(11030, 1450) + br(12470, 2970) + b + untimed)); + assertEquals(paragraphs(utf(document(a + b + untimed))), paragraphs(repair(source))); + } + + @Test + public void requestScopeRecognizesBothOriginalAndTranslatedAsr() { + assertTrue( + AutoTranslateCaptionsPatch.isAutomaticCaption( + "ta.en.tr", + "https://www.youtube.com/api/timedtext?v=any&kind=asr&tlang=tr")); + assertTrue( + AutoTranslateCaptionsPatch.isAutomaticCaption( + "a.en", "https://www.youtube.com/api/timedtext?v=any&kind=asr")); + assertFalse( + AutoTranslateCaptionsPatch.isAutomaticCaption( + "t.en.tr", "https://www.youtube.com/api/timedtext?v=any&tlang=tr")); + assertFalse( + AutoTranslateCaptionsPatch.isAutomaticCaption( + "ta.en.tr", "https://example.org/api/timedtext?kind=asr&tlang=tr")); + } + + @Test + public void automaticSourceUsesCanonicalIdentityValidation() { + String base = "https://www.youtube.com/api/timedtext?kind=asr&tlang=de"; + assertTrue(AutoTranslateCaptionsPatch.isAutomaticCaption("ta.en.de", base)); + for (String url : + new String[] { + base + "&kind=asr", + base + "&v=one&v=two", + base.replace("kind=", "%6bind="), + base.replace("/timedtext?", "/%74imedtext?"), + base.replace("tlang=de", "tlang=fr"), + base.replace("www.youtube.com", "www.youtube.com.example.org") + }) assertFalse(url, AutoTranslateCaptionsPatch.isAutomaticCaption("ta.en.de", url)); + } + + @Test + public void nonUtf8XmlNeverReinterpretsTheValidatedBytes() throws Exception { + String body = p(8080, 4400, "A") + br(11030, 1450) + br(12470, 2970) + p(12480, 5360, "B"); + byte[] encoded = + ("" + document(body)) + .getBytes(java.nio.charset.Charset.forName("CP037")); + assertSame(encoded, repair(encoded)); + byte[] declared = utf("" + document(body)); + assertSame(declared, repair(declared)); + } + + @Test + public void encodedExternalEntityIsNeverExpanded() throws Exception { + java.nio.file.Path marker = java.nio.file.Files.createTempFile("caption-entity-", ".txt"); + try { + java.nio.file.Files.write(marker, utf("PRIVATE_TEST_MARKER")); + String xml = + "" + + "]>" + + document( + p(8080, 4400, "&x;") + + br(11030, 1450) + + br(12470, 2970) + + p(12480, 5360, "B")); + byte[] source = xml.getBytes(java.nio.charset.Charset.forName("CP037")); + assertSame(source, repair(source)); + } finally { + java.nio.file.Files.delete(marker); + } + } + + @Test + public void utf8XmlDeclarationAndBomStillAllowRepair() { + String body = p(8080, 4400, "A") + br(11030, 1450) + br(12470, 2970) + p(12480, 5360, "B"); + for (String prefix : + new String[] { + "", + "\uFEFF" + }) { + byte[] source = utf(prefix + document(body)); + assertNotSame(source, repair(source)); + } + } +} diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/captions/CaptionViewportPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/captions/CaptionViewportPatch.kt new file mode 100644 index 0000000000..144949faad --- /dev/null +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/captions/CaptionViewportPatch.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/4149 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.patches.youtube.layout.captions + +import app.morphe.patcher.extensions.InstructionExtensions.addInstructions +import app.morphe.patcher.patch.bytecodePatch +import app.morphe.patches.youtube.misc.playertype.playerTypeHookPatch +import app.morphe.patches.youtube.misc.playservice.is_21_10_or_greater +import app.morphe.patches.youtube.misc.playservice.versionCheckPatch +import app.morphe.patches.youtube.video.format.hookAdaptiveFormat +import app.morphe.patches.youtube.video.format.videoFormatPatch +import app.morphe.patches.youtube.video.videoid.hookVideoId +import app.morphe.patches.youtube.video.videoid.videoIdPatch + +private const val EXTENSION_CLASS = + "Lapp/morphe/extension/youtube/patches/CaptionViewportPatch;" + +internal val captionViewportPatch = bytecodePatch( + description = "Keeps fullscreen captions proportional to the fitted video on near-square screens.", +) { + dependsOn(versionCheckPatch, videoFormatPatch, videoIdPatch, playerTypeHookPatch) + + execute { + if (!is_21_10_or_greater) { + return@execute + } + + hookVideoId("$EXTENSION_CLASS->onVideoId(Ljava/lang/String;)V") + hookAdaptiveFormat("$EXTENSION_CLASS->onFormats") + + // Font size is calculated from the overlay size. Use the height of the fitted video instead. + CaptionFontSizeFingerprint.method.addInstructions( + 0, + """ + invoke-static/range { p0 .. p3 }, $EXTENSION_CLASS->normalizeHeight(Landroid/view/View;FII)I + move-result p3 + """ + ) + } +} diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/captions/CaptionWindowMeasurePatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/captions/CaptionWindowMeasurePatch.kt new file mode 100644 index 0000000000..2cb4d83ccb --- /dev/null +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/captions/CaptionWindowMeasurePatch.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/4149 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.patches.youtube.layout.captions + +import app.morphe.patcher.extensions.InstructionExtensions.getInstruction +import app.morphe.patcher.extensions.InstructionExtensions.replaceInstruction +import app.morphe.patcher.patch.bytecodePatch +import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction22t +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction + +/** + * The subtitle window keeps the height of its measure spec if the width mode is EXACTLY, + * instead of the height mode. Use the height mode for the height. + */ +internal val captionWindowMeasurePatch = bytecodePatch( + description = "Fixes the caption window height using the width measure mode.", +) { + execute { + CaptionWindowMeasureFingerprint.let { + val heightModeRegister = it.instructionMatches[4] + .getInstruction().registerA + val branchIndex = it.instructionMatches.last().index + val branch = it.method.getInstruction(branchIndex) + + it.method.replaceInstruction( + branchIndex, + BuilderInstruction22t( + branch.opcode, + heightModeRegister, + branch.registerB, + branch.target + ) + ) + } + } +} diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/captions/CaptionsPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/captions/CaptionsPatch.kt index eacf128bd1..f07974688b 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/captions/CaptionsPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/captions/CaptionsPatch.kt @@ -62,6 +62,9 @@ val captionsPatch = bytecodePatch( captionCookiesPatch, transcriptPatch, translatedCaptionPensPatch, + captionWindowMeasurePatch, + stableCaptionBlocksPatch, + captionViewportPatch, captionsInPipPatch, captionsAfterSeekPatch, ) diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/captions/Fingerprints.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/captions/Fingerprints.kt index 8400cca323..ddb6c40031 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/captions/Fingerprints.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/captions/Fingerprints.kt @@ -13,6 +13,7 @@ import app.morphe.patcher.InstructionLocation.MatchAfterImmediately import app.morphe.patcher.InstructionLocation.MatchAfterWithin import app.morphe.patcher.InstructionLocation.MatchFirst import app.morphe.patcher.StringComparisonType +import app.morphe.patcher.anyInstruction import app.morphe.patcher.fieldAccess import app.morphe.patcher.literal import app.morphe.patcher.methodCall @@ -627,3 +628,216 @@ internal fun getCaptionChunkLoaderFingerprint(loaderType: String) = object : Fin methodCall(smali = "Ljava/util/concurrent/Executor;->execute(Ljava/lang/Runnable;)V") ) ) {} + +internal const val SUBTITLE_WINDOW_VIEW_CLASS = + "Lcom/google/android/libraries/youtube/player/subtitles/ui/SubtitleWindowView;" + +/** + * Caption cue of a subtitle window. Matches the id, time, text, full text and settings fields. + */ +internal object CaptionCueToStringFingerprint : Fingerprint( + name = "toString", + returnType = "Ljava/lang/String;", + parameters = listOf(), + filters = listOf( + fieldAccess(opcode = Opcode.IGET, definingClass = "this", type = "I"), + fieldAccess(opcode = Opcode.IGET_WIDE, definingClass = "this", type = "J"), + fieldAccess( + opcode = Opcode.IGET_OBJECT, + definingClass = "this", + type = "Ljava/lang/CharSequence;" + ), + fieldAccess( + opcode = Opcode.IGET_OBJECT, + definingClass = "this", + type = "Ljava/lang/CharSequence;", + location = MatchAfterImmediately() + ), + fieldAccess( + opcode = Opcode.IGET_OBJECT, + definingClass = "this", + location = MatchAfterImmediately() + ), + string("id: %d time: %d text: %s fullText: %s settings: %s") + ) +) + +internal object CaptionCueConstructorFingerprint : Fingerprint( + classFingerprint = CaptionCueToStringFingerprint, + name = "", + returnType = "V", + parameters = listOf("I", "J", "Ljava/lang/CharSequence;", "Ljava/lang/CharSequence;", "L") +) + +/** + * Matches at the default caption window settings. + */ +internal fun getCaptionDefaultSettingsFingerprint(settingsType: String) = object : Fingerprint( + definingClass = settingsType, + name = "", + returnType = "V", + filters = listOf( + fieldAccess(opcode = Opcode.SPUT_OBJECT, definingClass = "this", type = settingsType) + ) +) {} + +/** + * Returns the caption cues shown at a video time. + */ +internal fun getCaptionSamplerFingerprint(cueType: String) = object : Fingerprint( + classFingerprint = CaptionWindowsConstructorFingerprint, + accessFlags = listOf(AccessFlags.PUBLIC, AccessFlags.FINAL), + returnType = "Ljava/util/List;", + parameters = listOf("J"), + filters = listOf( + newInstance(cueType), + methodCall(definingClass = cueType, name = "") + ) +) {} + +/** + * Sets the cue of a subtitle window and splits its text into lines. + */ +internal fun getCaptionWindowSetterFingerprint(cueType: String) = object : Fingerprint( + definingClass = SUBTITLE_WINDOW_VIEW_CLASS, + returnType = "V", + parameters = listOf(cueType), + filters = listOf( + fieldAccess(opcode = Opcode.IPUT_OBJECT, definingClass = "this", type = cueType), + // Visible text lines. + fieldAccess( + opcode = Opcode.IGET_OBJECT, + definingClass = "this", + type = "Ljava/util/List;", + location = MatchAfterImmediately() + ), + methodCall(smali = "Ljava/util/List;->clear()V", location = MatchAfterImmediately()), + // Full text lines. + fieldAccess( + opcode = Opcode.IGET_OBJECT, + definingClass = "this", + type = "Ljava/util/List;", + location = MatchAfterImmediately() + ), + methodCall(smali = "Ljava/util/List;->clear()V", location = MatchAfterImmediately()), + // Split the visible text, then the full text. + methodCall( + opcode = Opcode.INVOKE_STATIC, + parameters = listOf("Ljava/util/List;", "Ljava/lang/CharSequence;"), + returnType = "V" + ), + methodCall( + opcode = Opcode.INVOKE_STATIC, + parameters = listOf("Ljava/util/List;", "Ljava/lang/CharSequence;"), + returnType = "V", + location = MatchAfterWithin(2) + ) + ) +) {} + +/** + * Subtitle window constructor. Matches at the row used to measure caption text. + */ +internal fun getCaptionWindowConstructorFingerprint(rowType: String) = object : Fingerprint( + definingClass = SUBTITLE_WINDOW_VIEW_CLASS, + name = "", + returnType = "V", + parameters = listOf("Landroid/content/Context;"), + filters = listOf( + fieldAccess(opcode = Opcode.IPUT_OBJECT, definingClass = "this", type = rowType) + ) +) {} + +/** + * The height measure spec branch incorrectly checks the width measure mode. + */ +internal object CaptionWindowMeasureFingerprint : Fingerprint( + definingClass = SUBTITLE_WINDOW_VIEW_CLASS, + name = "onMeasure", + returnType = "V", + parameters = listOf("I", "I"), + filters = listOf( + methodCall(smali = $$"Landroid/view/View$MeasureSpec;->getMode(I)I"), + opcode(Opcode.MOVE_RESULT, MatchAfterImmediately()), + literal(0x40000000, location = MatchAfterImmediately()), + methodCall(smali = $$"Landroid/view/View$MeasureSpec;->getMode(I)I"), + opcode(Opcode.MOVE_RESULT, MatchAfterImmediately()), + // Height mode is AT_MOST. + opcode(Opcode.IF_NE, MatchAfterImmediately()), + // Width mode is EXACTLY. + anyInstruction( + opcode(Opcode.IF_EQ), + opcode(Opcode.IF_NE), + location = MatchAfterWithin(4) + ) + ) +) + +/** + * Text layout of a caption row. + */ +internal object CaptionRowLayoutFingerprint : Fingerprint( + accessFlags = listOf(AccessFlags.PUBLIC, AccessFlags.FINAL), + returnType = "Landroid/text/StaticLayout;", + parameters = listOf(), + superclass = "Landroid/view/View;", + filters = listOf( + string("Edge type %d is not supported.") + ) +) + +internal object CaptionRowSetTextFingerprint : Fingerprint( + classFingerprint = CaptionRowLayoutFingerprint, + accessFlags = listOf(AccessFlags.PUBLIC, AccessFlags.FINAL), + returnType = "V", + parameters = listOf("Ljava/lang/CharSequence;") +) + +internal const val CAPTION_STATIC_LAYOUT_PARAMETERS = + $$"Ljava/lang/CharSequence;Landroid/text/TextPaint;ILandroid/text/Layout$Alignment;FFZ" + +/** + * Creates the fill and the outline text layouts of a caption row. + */ +internal object CaptionRowLayoutFactoryFingerprint : Fingerprint( + classFingerprint = CaptionRowLayoutFingerprint, + accessFlags = listOf(AccessFlags.PRIVATE, AccessFlags.FINAL), + returnType = "Landroid/text/StaticLayout;", + parameters = listOf("I"), + filters = listOf( + newInstance("Landroid/text/StaticLayout;"), + methodCall( + smali = "Landroid/text/StaticLayout;->(${CAPTION_STATIC_LAYOUT_PARAMETERS})V" + ) + ) +) + +/** + * Shows the caption cues of the subtitle overlay. + */ +internal fun getCaptionOverlayPresenterFingerprint(cueType: String) = object : Fingerprint( + accessFlags = listOf(AccessFlags.PUBLIC), + returnType = "V", + parameters = listOf("Ljava/util/List;"), + filters = listOf( + methodCall( + definingClass = SUBTITLE_WINDOW_VIEW_CLASS, + parameters = listOf(cueType), + returnType = "V" + ) + ) +) {} + +/** + * Caption font size, calculated from the size of the subtitle overlay. + */ +internal object CaptionFontSizeFingerprint : Fingerprint( + returnType = "F", + parameters = listOf("F", "I", "I"), + filters = listOf( + methodCall(smali = "Ljava/lang/Math;->min(II)I"), + fieldAccess(smali = "Landroid/util/DisplayMetrics;->scaledDensity:F"), + literal(0.0625f), + literal(13.0f) + ) +) diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/captions/StableCaptionBlocksPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/captions/StableCaptionBlocksPatch.kt new file mode 100644 index 0000000000..04921aca6d --- /dev/null +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/captions/StableCaptionBlocksPatch.kt @@ -0,0 +1,280 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/4149 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.patches.youtube.layout.captions + +import app.morphe.patcher.extensions.InstructionExtensions.addInstruction +import app.morphe.patcher.extensions.InstructionExtensions.addInstructions +import app.morphe.patcher.extensions.InstructionExtensions.addInstructionsWithLabels +import app.morphe.patcher.extensions.InstructionExtensions.getInstruction +import app.morphe.patcher.extensions.InstructionExtensions.removeInstruction +import app.morphe.patcher.extensions.InstructionExtensions.replaceInstruction +import app.morphe.patcher.patch.bytecodePatch +import app.morphe.patcher.util.getReference +import app.morphe.patcher.util.proxy.mutableTypes.MutableField.Companion.toMutable +import app.morphe.patches.shared.misc.settings.preference.SwitchPreference +import app.morphe.patches.youtube.misc.playservice.is_21_10_or_greater +import app.morphe.patches.youtube.misc.playservice.versionCheckPatch +import app.morphe.util.cloneMutable +import app.morphe.util.indexOfFirstInstructionReversedOrThrow +import app.morphe.util.setExtensionIsPatchIncluded +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.reference.FieldReference +import com.android.tools.smali.dexlib2.iface.reference.MethodReference +import com.android.tools.smali.dexlib2.immutable.ImmutableField +import java.util.logging.Logger + +private const val EXTENSION_CLASS = + "Lapp/morphe/extension/youtube/patches/StableCaptionBlocks;" +private const val EXTENSION_STATE_CLASS = + $$"Lapp/morphe/extension/youtube/patches/StableCaptionBlocks$State;" +private const val EXTENSION_FRAME_CLASS = + $$"Lapp/morphe/extension/youtube/patches/StableCaptionBlocks$Frame;" +private const val EXTENSION_CUE_INTERFACE = + $$"Lapp/morphe/extension/youtube/patches/StableCaptionBlocks$Cue;" +private const val EXTENSION_ROW_INTERFACE = + $$"Lapp/morphe/extension/youtube/patches/StableCaptionBlocks$Row;" +private const val EXTENSION_WINDOW_INTERFACE = + $$"Lapp/morphe/extension/youtube/patches/StableCaptionBlocks$Window;" +private const val EXTENSION_TIMELINE_INTERFACE = + $$"Lapp/morphe/extension/youtube/patches/StableCaptionBlocks$Timeline;" + +internal val stableCaptionBlocksPatch = bytecodePatch( + description = "Adds an option to show automatic captions in stable two-line blocks.", +) { + // The window measure method must be patched first, since this inserts instructions before it. + dependsOn(versionCheckPatch, translatedCaptionPensPatch, captionWindowMeasurePatch) + + execute { + if (!is_21_10_or_greater) { + return@execute Logger.getLogger(this::class.java.name).warning( + "Stable caption blocks requires YouTube 21.10+" + ) + } + + settingsMenuCaptionGroup.add( + SwitchPreference("morphe_stable_caption_blocks", summary = true) + ) + + setExtensionIsPatchIncluded(EXTENSION_CLASS) + + // Cue fields in the order: id, time, visible text, full text, settings. + val cueFields = CaptionCueToStringFingerprint.instructionMatches.take(5).map { + it.instruction.getReference()!! + } + val (idField, timeField, visibleTextField, fullTextField, settingsField) = cueFields + val cueType = idField.definingClass + val cueConstructor = CaptionCueConstructorFingerprint.method + val defaultSettingsField = getCaptionDefaultSettingsFingerprint(settingsField.type) + .instructionMatches.first().instruction.getReference()!! + + // Cues of a block are copies with a frame span, and with the default settings + // so blocks always use the default placement. + mutableClassDefBy(cueType).apply { + interfaces.add(EXTENSION_CUE_INTERFACE) + + addHelperMethod( + "patch_getOriginalCaptionVisible", + "Ljava/lang/CharSequence;", + smali = """ + iget-object v0, p0, $visibleTextField + return-object v0 + """ + ) + + addHelperMethod( + "patch_getBlockFrame", + EXTENSION_FRAME_CLASS, + smali = """ + iget-object v0, p0, $fullTextField + invoke-static { v0 }, $EXTENSION_CLASS->frameOf(Ljava/lang/CharSequence;)$EXTENSION_FRAME_CLASS + move-result-object v0 + return-object v0 + """ + ) + + addHelperMethod( + "patch_copyBlockFrame", + "Ljava/lang/Object;", + parameters = listOf(EXTENSION_FRAME_CLASS), + freeRegisters = 7, + smali = """ + # Reuse this cue if it already shows the frame with the default settings. + iget-object v6, p0, $settingsField + sget-object v0, $defaultSettingsField + if-ne v6, v0, :copy_frame + invoke-virtual { p0 }, $type->patch_getBlockFrame()$EXTENSION_FRAME_CLASS + move-result-object v0 + if-ne v0, p1, :copy_frame + iget-object v0, p0, $visibleTextField + invoke-static { v0 }, $EXTENSION_CLASS->frameOf(Ljava/lang/CharSequence;)$EXTENSION_FRAME_CLASS + move-result-object v0 + if-ne v0, p1, :copy_frame + return-object p0 + :copy_frame + new-instance v0, $type + iget v1, p0, $idField + iget-wide v2, p0, $timeField + iget-object v4, p0, $visibleTextField + iget-object v5, p0, $fullTextField + invoke-static { v4, p1 }, $EXTENSION_CLASS->carryFrame(Ljava/lang/CharSequence;$EXTENSION_FRAME_CLASS)Ljava/lang/CharSequence; + move-result-object v4 + invoke-static { v5, p1 }, $EXTENSION_CLASS->carryFrame(Ljava/lang/CharSequence;$EXTENSION_FRAME_CLASS)Ljava/lang/CharSequence; + move-result-object v5 + sget-object v6, $defaultSettingsField + invoke-direct/range { v0 .. v6 }, $cueConstructor + return-object v0 + """ + ) + } + + // Row used to measure caption text. + val rowType = CaptionRowLayoutFingerprint.classDef.type + mutableClassDefBy(rowType).apply { + interfaces.add(EXTENSION_ROW_INTERFACE) + + addHelperMethod( + "patch_getCaptionLayout", + "Landroid/text/StaticLayout;", + smali = """ + invoke-virtual { p0 }, ${CaptionRowLayoutFingerprint.method} + move-result-object v0 + return-object v0 + """ + ) + + addHelperMethod( + "patch_setCaptionText", + "V", + parameters = listOf("Ljava/lang/CharSequence;"), + smali = """ + invoke-virtual { p0, p1 }, ${CaptionRowSetTextFingerprint.method} + return-void + """ + ) + } + + // Fill and outline text layouts of blocks use balanced line breaks. + CaptionRowLayoutFactoryFingerprint.matchAll().forEach { match -> + val newInstanceIndex = match.instructionMatches[0].index + val constructorIndex = match.instructionMatches[1].index + val register = match.method.getInstruction(newInstanceIndex).registerA + + match.method.apply { + replaceInstruction( + constructorIndex, + "invoke-static/range { v${register + 1} .. v${register + 7} }, " + + "$EXTENSION_CLASS->createLayout($CAPTION_STATIC_LAYOUT_PARAMETERS)Landroid/text/StaticLayout;" + ) + addInstruction(constructorIndex + 1, "move-result-object v$register") + removeInstruction(newInstanceIndex) + } + } + + // Window lines are replaced with the block text before the window is measured. + getCaptionWindowSetterFingerprint(cueType).let { + val windowFields = listOf( + "patch_getCaptionCue" to it.instructionMatches[0], + "patch_getVisibleCaptionLines" to it.instructionMatches[1], + "patch_getFullCaptionLines" to it.instructionMatches[3], + ).map { (name, match) -> name to match.instruction.getReference()!! } + + ("patch_getCaptionMeasureRow" to getCaptionWindowConstructorFingerprint(rowType) + .instructionMatches.first().instruction.getReference()!!) + + mutableClassDefBy(SUBTITLE_WINDOW_VIEW_CLASS).apply { + interfaces.add(EXTENSION_WINDOW_INTERFACE) + + windowFields.forEach { (name, field) -> + addHelperMethod( + name, + if (field.type == "Ljava/util/List;") field.type else "Ljava/lang/Object;", + smali = """ + iget-object v0, p0, $field + return-object v0 + """ + ) + } + } + } + + CaptionWindowMeasureFingerprint.method.addInstructions( + 0, + "invoke-static/range { p0 .. p1 }, $EXTENSION_CLASS->prepare(Landroid/view/View;I)V" + ) + + // Player subclasses can recreate the cue placement. + getCaptionOverlayPresenterFingerprint(cueType).method.addInstructions( + 0, + """ + invoke-static { p1 }, $EXTENSION_CLASS->centerBlocks(Ljava/util/List;)Ljava/util/List; + move-result-object p1 + """ + ) + + // Each parsed track keeps the block state of its source captions. + CaptionWindowsConstructorFingerprint.let { + val timelineType = it.classDef.type + val stateField = "$timelineType->patch_blockState:$EXTENSION_STATE_CLASS" + + mutableClassDefBy(timelineType).apply { + interfaces.add(EXTENSION_TIMELINE_INTERFACE) + + instanceFields.add( + ImmutableField( + type, + "patch_blockState", + EXTENSION_STATE_CLASS, + AccessFlags.PRIVATE.value, + null, + null, + null + ).toMutable() + ) + + addHelperMethod( + "patch_getBlockState", + EXTENSION_STATE_CLASS, + smali = """ + iget-object v0, p0, $stateField + return-object v0 + """ + ) + + it.method.apply { + addInstructions( + indexOfFirstInstructionReversedOrThrow(Opcode.RETURN_VOID), + """ + invoke-static {}, $EXTENSION_CLASS->takeState()$EXTENSION_STATE_CLASS + move-result-object v0 + iput-object v0, p0, $stateField + """ + ) + } + + val sampler = getCaptionSamplerFingerprint(cueType).method + methods.add(sampler.cloneMutable(name = "patch_getOriginalCaptions")) + + sampler.addInstructionsWithLabels( + 0, + """ + # Method uses many registers, and p0 is not a 4-bit register. + move-object/from16 v0, p0 + iget-object v0, v0, $stateField + if-eqz v0, :original + invoke-static/range { p0 .. p2 }, $EXTENSION_CLASS->present(${EXTENSION_TIMELINE_INTERFACE}J)Ljava/util/List; + move-result-object v0 + return-object v0 + :original + nop + """ + ) + } + } + } +} diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/captions/TranslatedCaptionPensPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/captions/TranslatedCaptionPensPatch.kt index dadd62e3ad..aaa18bf4bf 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/captions/TranslatedCaptionPensPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/captions/TranslatedCaptionPensPatch.kt @@ -34,7 +34,7 @@ internal val translatedCaptionPensPatch = bytecodePatch( description = "Fixes auto-translated captions of styled caption tracks showing an empty background.", ) { // Adds the caption track interface. - dependsOn(sharedExtensionPatch, preferredCaptionLanguagePatch) + dependsOn(sharedExtensionPatch, autoTranslateCaptionsPatch) execute { // Track whether the caption track that is parsed is auto-translated. @@ -76,6 +76,16 @@ internal val translatedCaptionPensPatch = bytecodePatch( ) } + // Translated automatic captions can have orphan line break events, + // and stable caption blocks use the source timing of automatic captions. + CaptionFileParserFingerprint.instructionMatches.last().getMethodCalled().addInstructions( + 0, + """ + invoke-static { p1 }, $EXTENSION_CLASS->repairInput(Ljava/io/InputStream;)Ljava/io/InputStream; + move-result-object p1 + """ + ) + // Every pen lookup of the timed text parser (,

    and elements) uses the same method. TimedTextSegmentElementFingerprint.instructionMatches[2].getMethodCalled().addInstructions( 0, diff --git a/patches/src/main/resources/addresources/values/youtube/strings.xml b/patches/src/main/resources/addresources/values/youtube/strings.xml index 7d3becde0a..b840e69b72 100644 --- a/patches/src/main/resources/addresources/values/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values/youtube/strings.xml @@ -945,6 +945,8 @@ To fix this, turn on \'Spoof video streams\' and change the \'Default client\' t Fix transcript Resolves a precondition check failure that can prevent the transcript from loading Show captions in picture-in-picture + Stable caption blocks + Group automatic captions into stable, readable blocks. Some words may appear before they are spoken. Restart required. Navigation buttons From 035718bf911540f36ba768e5db78cae4ac4caf23 Mon Sep 17 00:00:00 2001 From: 0xrxL <127248639+0xrxL@users.noreply.github.com> Date: Fri, 30 Oct 2026 05:42:48 +0100 Subject: [PATCH 172/189] feat(YouTube): Show Morphe messages in snackbar instead of a toast (#4148) --- .../youtube/patches/AddToQueuePatch.java | 6 +- .../youtube/patches/CopyTextPatch.java | 2 +- .../youtube/patches/LoadVideoPatch.java | 4 +- .../youtube/patches/LocalQueuePatch.java | 8 +- .../youtube/patches/MiniplayerPatch.java | 4 +- .../youtube/patches/NativeSnackbarPatch.java | 91 +++++++++++++++++++ .../patches/PostImageDownloadPatch.java | 6 +- .../patches/SaveToWatchLaterPatch.java | 8 +- .../announcements/AnnouncementsPatch.java | 2 +- .../patches/ask/AskFlyoutMenuPatch.java | 10 +- .../components/CommentsSearchFilter.java | 4 +- .../components/KeywordContentFilter.java | 10 +- .../youtube/patches/dearrow/DeArrowPatch.java | 4 +- .../quality/RememberVideoQualityPatch.java | 2 +- .../speed/CustomPlaybackSpeedPatch.java | 4 +- .../speed/RememberPlaybackSpeedPatch.java | 14 +-- .../queue/NativePlaylistQueuePatch.java | 42 ++++----- .../patches/theme/SeekbarColorPatch.java | 2 +- .../patches/utils/AiSListSubmitDialog.java | 10 +- .../youtube/patches/utils/PlaylistPatch.java | 22 ++--- .../VoiceOverTranslationPatch.java | 4 +- .../youtube/series/BackupPreference.java | 4 +- .../extension/youtube/series/HistoryUi.java | 2 +- .../youtube/series/LibraryDialog.java | 2 +- .../youtube/series/PlayerSeriesAction.java | 4 +- .../youtube/series/RecordingPreference.java | 4 +- .../youtube/series/SeriesPlayback.java | 4 +- .../youtube/series/SyncPreference.java | 4 +- .../youtube/series/TrackerRuntime.java | 2 +- .../extension/youtube/settings/Settings.java | 1 + .../GetCaptionCookiesPreference.java | 6 +- .../WatchLaterPlaylistPreference.java | 2 +- .../sponsorblock/SponsorBlockSettings.java | 14 +-- .../sponsorblock/SponsorBlockUtils.java | 18 ++-- .../SponsorBlockApiUrlPreference.java | 6 +- .../SponsorBlockPrivateUserIdPreference.java | 2 +- .../SponsorBlockSegmentStepPreference.java | 4 +- .../SponsorBlockStatsPreferenceCategory.java | 2 +- .../SwipeControlsConfigurationProvider.java | 6 +- .../videoplayer/CopyVideoLinkButton.java | 8 +- .../youtube/videoplayer/LoopVideoButton.java | 12 +-- .../youtube/videoplayer/PlayAllButton.java | 2 +- .../youtube/whitelist/ChannelWhitelist.java | 4 +- .../whitelist/ChannelWhitelistDialog.java | 2 +- gradle/libs.versions.toml | 2 +- .../youtube/misc/settings/Fingerprints.kt | 20 ++++ .../youtube/misc/settings/SettingsPatch.kt | 41 +++++++++ .../addresources/values/youtube/strings.xml | 2 + .../QueueRuntimeBoundaryTest.kt | 2 +- 49 files changed, 298 insertions(+), 143 deletions(-) create mode 100644 extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/NativeSnackbarPatch.java diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/AddToQueuePatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/AddToQueuePatch.java index dc97be88d6..4f4d30ea33 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/AddToQueuePatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/AddToQueuePatch.java @@ -164,7 +164,7 @@ public static void onListBound(ViewGroup itemList) { Logger.printDebug(() -> "Queue row " + buttonName + " video id: '" + videoId + "'"); item.setOnClickListener(v -> { if (videoId.isEmpty()) { - Utils.showToastShort(str(FlyoutUtils.getFlyoutPlaylistId().isEmpty() + Utils.showSnackbarShort(str(FlyoutUtils.getFlyoutPlaylistId().isEmpty() ? "morphe_local_queue_unavailable" : "morphe_local_queue_playlist_unsupported")); FlyoutUtils.dismissFlyout(); @@ -248,7 +248,7 @@ public static Runnable replaceButtonRunnable(Runnable original) { Logger.printDebug(() -> "Cannot replace on item click, flyoutVideoId is empty"); if (LocalQueuePatch.isEnabled() && queueButtonOriginalNames.contains(FlyoutUtils.getCurrentButtonName())) { return () -> { - Utils.showToastShort(str("morphe_local_queue_playlist_unsupported")); + Utils.showSnackbarShort(str("morphe_local_queue_playlist_unsupported")); FlyoutUtils.dismissFlyout(); }; } @@ -365,7 +365,7 @@ private static Runnable getNativeQueueAction(String buttonName, String videoId) // An unsupported/stale menu context cannot silently invoke a Premium or // account-backed queue when the user explicitly selected Native queue. return action != null ? action : () -> { - Utils.showToastShort(str("morphe_native_playlist_queue_not_ready")); + Utils.showSnackbarShort(str("morphe_native_playlist_queue_not_ready")); FlyoutUtils.dismissFlyout(); }; } diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/CopyTextPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/CopyTextPatch.java index f03931b922..814376bc3b 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/CopyTextPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/CopyTextPatch.java @@ -86,7 +86,7 @@ protected boolean removeEldestEntry(Map.Entry eldest) { } Utils.setClipboard(text); - Utils.showToastShort(str(touchedToastKey)); + Utils.showSnackbarShort(str(touchedToastKey)); return true; }; diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/LoadVideoPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/LoadVideoPatch.java index 397e522fc2..422c73d982 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/LoadVideoPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/LoadVideoPatch.java @@ -166,7 +166,7 @@ public static void initializeReloadVideo() { PlayerInterface playerInterface = playerInterfaceRef.get(); if (playerInterface == null) { - Utils.showToastShort(str("morphe_dismiss_player_not_available_toast")); + Utils.showSnackbarShort(str("morphe_dismiss_player_not_available_toast")); return; } @@ -205,7 +205,7 @@ public static void initializeReloadVideo() { public static PlayerInterface getPlayerInterface() { PlayerInterface playerInterface = playerInterfaceRef.get(); if (playerInterface == null) { - Utils.showToastShort(str("morphe_dismiss_player_not_available_toast")); + Utils.showSnackbarShort(str("morphe_dismiss_player_not_available_toast")); return null; } return playerInterface; diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/LocalQueuePatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/LocalQueuePatch.java index 6675eff325..98d06acb52 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/LocalQueuePatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/LocalQueuePatch.java @@ -141,7 +141,7 @@ public static void add(String videoId, boolean playNext) { final boolean playNow = PlayerType.getCurrent().isNoneOrHidden() && size() == 0; if (playNow) { if (!startPlayback(videoId)) { - Utils.showToastShort(str("morphe_local_queue_play_failed")); + Utils.showSnackbarShort(str("morphe_local_queue_play_failed")); } return; } @@ -154,7 +154,7 @@ public static void add(String videoId, boolean playNext) { item = items.remove(existing); } else { if (items.size() >= MAX_ITEMS) { - Utils.showToastShort(str("morphe_local_queue_full")); + Utils.showSnackbarShort(str("morphe_local_queue_full")); return; } item = new Item(videoId, null, null); @@ -164,7 +164,7 @@ public static void add(String videoId, boolean playNext) { fetchMetadataIfNeeded(item); } - Utils.showToastShort(str("morphe_local_queue_added")); + Utils.showSnackbarShort(str("morphe_local_queue_added")); notifyChanged(); } catch (Exception ex) { Logger.printException(() -> "add failure", ex); @@ -213,7 +213,7 @@ public static void play(String videoId) { lastAdvanceTime = System.currentTimeMillis(); remove(videoId); } else { - Utils.showToastShort(str("morphe_local_queue_play_failed")); + Utils.showSnackbarShort(str("morphe_local_queue_play_failed")); } } diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/MiniplayerPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/MiniplayerPatch.java index 991123bbcd..7d94e9f2aa 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/MiniplayerPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/MiniplayerPatch.java @@ -115,7 +115,7 @@ public boolean isModern() { // 170 seems to be the smallest that can be used and using less makes no difference. final int WIDTH_DIP_MIN = 170; // Seems to be the smallest that works. final int HORIZONTAL_PADDING_DIP = 15; // Estimated padding. - // Round down to the nearest 5 pixels, to keep any error toasts easier to read. + // Round down to the nearest 5 pixels, to keep any error messages easier to read. final int estimatedWidthDipMax = 5 * ((deviceDipWidth - HORIZONTAL_PADDING_DIP) / 5); // On some ultra-low-end devices the pixel width and density are the same number, // which causes the estimate to always give a value of 1. @@ -128,7 +128,7 @@ public boolean isModern() { int dipWidth = Settings.MINIPLAYER_WIDTH_DIP.get(); if (dipWidth < WIDTH_DIP_MIN || dipWidth > WIDTH_DIP_MAX) { - Utils.showToastLong(str("morphe_miniplayer_width_dip_invalid_toast", + Utils.showSnackbarLong(str("morphe_miniplayer_width_dip_invalid_toast", WIDTH_DIP_MIN, WIDTH_DIP_MAX)); // Instead of resetting, clamp the size at the bounds. diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/NativeSnackbarPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/NativeSnackbarPatch.java new file mode 100644 index 0000000000..28899a801f --- /dev/null +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/NativeSnackbarPatch.java @@ -0,0 +1,91 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/4148 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.extension.youtube.patches; + +import android.app.Activity; + +import java.lang.ref.WeakReference; +import java.util.Map; +import java.util.WeakHashMap; + +import app.morphe.extension.shared.Logger; +import app.morphe.extension.shared.Utils; +import app.morphe.extension.youtube.innertube.queue.PlaylistQueueRenderer.AddToToastAction; +import app.morphe.extension.youtube.innertube.queue.PlaylistQueueRenderer.NavigationEndpoint; +import app.morphe.extension.youtube.innertube.queue.PlaylistQueueRenderer.QueueToastItem; +import app.morphe.extension.youtube.innertube.queue.PlaylistQueueRenderer.QueueToastRenderer; +import app.morphe.extension.youtube.innertube.queue.PlaylistQueueRenderer.Text; +import app.morphe.extension.youtube.settings.Settings; + +/** + * Shows the messages of {@link Utils#showSnackbar} in the YouTube snackbar, by running the same + * command a server response uses to show one. + */ +@SuppressWarnings("unused") +public final class NativeSnackbarPatch { + + /** + * Interface to use the obfuscated command router of an activity. + */ + public interface CommandRouter { + // Method is added during patching. + void patch_executeCommand(byte[] command); + } + + /** + * Each activity has its own router, which can only show a snackbar in that activity. + * The routers hold their activity, so they are not kept strongly by the key. + */ + private static final Map> routers = new WeakHashMap<>(); + + private static final Utils.SnackbarPresenter PRESENTER = (message, longDuration) -> { + // A hidden YouTube snackbar would hide the message as well. + if (!Settings.NATIVE_SNACKBAR.get() || Settings.HIDE_SNACKBAR.get()) { + return false; + } + + for (Map.Entry> entry : routers.entrySet()) { + Activity activity = entry.getKey(); + CommandRouter router = entry.getValue().get(); + // Stopped behind another screen, such as the Morphe settings, or in the background, + // where nobody would see the snackbar. + if (router == null || activity == null || activity.isFinishing() + || !activity.getWindow().getDecorView().isShown()) { + continue; + } + + // The app has no duration for it, YouTube shows every snackbar for the same time. + router.patch_executeCommand(NavigationEndpoint.newBuilder() + .setAddToToastAction(AddToToastAction.newBuilder() + .setItem(QueueToastItem.newBuilder() + .setToastRenderer(QueueToastRenderer.newBuilder() + .setResponseText(Text.newBuilder().setSimpleText(message))))) + .build() + .toByteArray()); + return true; + } + + return false; + }; + + /** + * Injection point. + *

    + * Called when an activity creates its command router. + */ + public static void setCommandRouter(CommandRouter router, Activity activity) { + try { + Utils.runOnMainThreadNowOrLater(() -> { + routers.put(activity, new WeakReference<>(router)); + Utils.setSnackbarPresenter(PRESENTER); + }); + } catch (Exception ex) { + Logger.printException(() -> "setCommandRouter failure", ex); + } + } +} diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/PostImageDownloadPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/PostImageDownloadPatch.java index fc7eb986b8..0b80770344 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/PostImageDownloadPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/PostImageDownloadPatch.java @@ -989,10 +989,10 @@ public static int addFlyoutButton(Object flyoutPanel, int index, @Nullable View private static void download(@Nullable List images) { if (images == null || images.isEmpty()) { - Utils.showToastShort(str("morphe_post_image_download_not_found")); + Utils.showSnackbarShort(str("morphe_post_image_download_not_found")); return; } - Utils.showToastShort(images.size() == 1 + Utils.showSnackbarShort(images.size() == 1 ? str("morphe_post_image_download_started_single") : str("morphe_post_image_download_started", images.size())); @@ -1031,7 +1031,7 @@ private static void download(@Nullable List images) { } else { toast = str("morphe_post_image_download_saved", savedCount, folder); } - Utils.showToastShort(toast); + Utils.showSnackbarShort(toast); }); }); } diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/SaveToWatchLaterPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/SaveToWatchLaterPatch.java index 361340147d..1e966a437f 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/SaveToWatchLaterPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/SaveToWatchLaterPatch.java @@ -86,11 +86,11 @@ public static void saveVideo(String videoId) { return; } if (AuthUtils.isNotLoggedIn()) { - Utils.showToastShort(str("morphe_queue_manager_check_failed_auth")); + Utils.showSnackbarShort(str("morphe_queue_manager_check_failed_auth")); return; } if (TextUtils.isEmpty(videoId)) { - Utils.showToastShort(str("morphe_queue_manager_check_failed_video_id")); + Utils.showSnackbarShort(str("morphe_queue_manager_check_failed_video_id")); return; } try { @@ -101,7 +101,7 @@ public static void saveVideo(String videoId) { ); Boolean result = saveToWatchLaterRequest.getResult(); if (result != null) { - Utils.showToastShort(str(result + Utils.showSnackbarShort(str(result ? "morphe_save_to_watch_later_success_toast" : "morphe_save_to_watch_later_already_exists_toast")); } else { @@ -110,7 +110,7 @@ public static void saveVideo(String videoId) { }); } catch (Exception ex) { Logger.printDebug(() -> "Could not fetch save to Watch later request", ex); - Utils.showToastShort(str("morphe_save_to_watch_later_error_toast")); + Utils.showSnackbarShort(str("morphe_save_to_watch_later_error_toast")); } } } diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/announcements/AnnouncementsPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/announcements/AnnouncementsPatch.java index f00a5a8635..78af7d672a 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/announcements/AnnouncementsPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/announcements/AnnouncementsPatch.java @@ -42,7 +42,7 @@ private static boolean isLatestAlready() throws IOException { return true; Settings.ANNOUNCEMENT_LAST_ID.resetToDefault(); - Utils.showToastLong(str("morphe_announcements_connection_failed")); + Utils.showSnackbarLong(str("morphe_announcements_connection_failed")); return true; } diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/ask/AskFlyoutMenuPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/ask/AskFlyoutMenuPatch.java index 6541079d7e..eaf88b0805 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/ask/AskFlyoutMenuPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/ask/AskFlyoutMenuPatch.java @@ -568,12 +568,12 @@ private static void openAskPanel(String videoId) { Utils.runOnMainThread(() -> { if (needsTemplate) { if (panel == null) { - Utils.showToastLong(str("morphe_ask_summarize_unavailable")); + Utils.showSnackbarLong(str("morphe_ask_summarize_unavailable")); return; } AskPanelCommand.saveTemplate(panel); if (!AskPanelCommand.hasTemplate()) { - Utils.showToastLong(str("morphe_ask_summarize_failed")); + Utils.showSnackbarLong(str("morphe_ask_summarize_failed")); return; } } @@ -633,13 +633,13 @@ private static void showAskPanel(String videoId) { CommandExecutorAccess commandExecutor = commandExecutorRef.get(); if (commandExecutor == null) { Logger.printDebug(() -> "No command executor"); - Utils.showToastLong(str("morphe_ask_summarize_failed")); + Utils.showSnackbarLong(str("morphe_ask_summarize_failed")); return; } AskPanelCommand.Panel panel = AskPanelCommand.build(videoId); if (panel == null) { - Utils.showToastLong(str("morphe_ask_summarize_failed")); + Utils.showSnackbarLong(str("morphe_ask_summarize_failed")); return; } @@ -663,7 +663,7 @@ private static void showAskPanel(String videoId) { AskPanelKeyboard.handleKeyboardInsets(Utils.getActivity()); } catch (Exception ex) { Logger.printException(() -> "showAskPanel failure", ex); - Utils.showToastLong(str("morphe_ask_summarize_failed")); + Utils.showSnackbarLong(str("morphe_ask_summarize_failed")); } } } diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/components/CommentsSearchFilter.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/components/CommentsSearchFilter.java index cd2bd6e362..8ff959b34a 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/components/CommentsSearchFilter.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/components/CommentsSearchFilter.java @@ -560,7 +560,7 @@ private static void showSearchDialog(Context context, View searchButton) { updateSearchButtonIcon(searchButton); if (!reloadPanelList(searchButton)) { // Such as only quotes, which are not a search. - Utils.showToastLong(str(!isSearching() + Utils.showSnackbarLong(str(!isSearching() ? "morphe_search_comments_cleared" : "morphe_search_comments_applied")); } @@ -571,7 +571,7 @@ private static void showSearchDialog(Context context, View searchButton) { setSearch(""); updateSearchButtonIcon(searchButton); if (!reloadPanelList(searchButton)) { - Utils.showToastLong(str("morphe_search_comments_cleared")); + Utils.showSnackbarLong(str("morphe_search_comments_cleared")); } }, true diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/components/KeywordContentFilter.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/components/KeywordContentFilter.java index 370fb7ac46..186a051c18 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/components/KeywordContentFilter.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/components/KeywordContentFilter.java @@ -128,7 +128,7 @@ private synchronized void parseKeywords() { // Must be synchronized since Litho // language that does not use spaces between words. // Do not reset the setting. Keep the invalid keywords so the user can fix the mistake. - Utils.showToastLong(str("morphe_hide_keyword_toast_invalid_length", phrase, MINIMUM_KEYWORD_LENGTH)); + Utils.showSnackbarLong(str("morphe_hide_keyword_toast_invalid_length", phrase, MINIMUM_KEYWORD_LENGTH)); continue; } else { wholeWordMatching = false; @@ -159,14 +159,14 @@ private synchronized void parseKeywords() { // Must be synchronized since Litho if (phrasesWillHideAllVideos(phraseVariations, wholeWordMatching)) { String toastMessage; - // If whole word matching is off, but would pass with on, then show a different toast. + // If whole word matching is off, but would pass with on, then show a different message. if (!wholeWordMatching && !phrasesWillHideAllVideos(phraseVariations, true)) { toastMessage = "morphe_hide_keyword_toast_invalid_common_whole_word_required"; } else { toastMessage = "morphe_hide_keyword_toast_invalid_common"; } - Utils.showToastLong(str(toastMessage, phrase)); + Utils.showSnackbarLong(str(toastMessage, phrase)); continue; } @@ -176,7 +176,7 @@ private synchronized void parseKeywords() { // Must be synchronized since Litho if (existing == null) { keywords.put(variation, wholeWordMatching); } else if (existing != wholeWordMatching) { - Utils.showToastLong(str("morphe_hide_keyword_toast_invalid_conflicting", phrase)); + Utils.showSnackbarLong(str("morphe_hide_keyword_toast_invalid_conflicting", phrase)); break; } } @@ -274,7 +274,7 @@ protected String matchBuffer(byte[] buffer, StringFilterGroup matchedGroup, @Override protected void onBroadFilterDetected(@Nullable String matched) { - Utils.showToastLong(str("morphe_hide_keyword_toast_invalid_broad", matched)); + Utils.showSnackbarLong(str("morphe_hide_keyword_toast_invalid_broad", matched)); } private void recordHide(StringFilterGroup matchedGroup, byte[] buffer, ContextInterface contextInterface) { diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/dearrow/DeArrowPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/dearrow/DeArrowPatch.java index 8ac3151984..00342cd543 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/dearrow/DeArrowPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/dearrow/DeArrowPatch.java @@ -262,7 +262,7 @@ private static Uri validateSettings() { // Cannot use unsecured 'http', otherwise the connections fail to start and no callbacks hooks are made. String scheme = apiURI.getScheme(); if (scheme == null || scheme.equals("http") || apiURI.getHost() == null) { - Utils.showToastLong("Invalid DeArrow API URL. Using default"); + Utils.showSnackbarLong("Invalid DeArrow API URL. Using default"); Settings.DEARROW_API_URL.resetToDefault(); return validateSettings(); } @@ -445,7 +445,7 @@ static void handleDeArrowError(String url, int statusCode) { String toastMessage = (statusCode != 0) ? str("morphe_dearrow_error", statusCode) : str("morphe_dearrow_error_generic"); - Utils.showToastLong(toastMessage); + Utils.showSnackbarLong(toastMessage); } // Load the DeArrow thumbnails again, which now use the fallback thumbnails. LithoRelayoutPatch.remountListViews(); diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/playback/quality/RememberVideoQualityPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/playback/quality/RememberVideoQualityPatch.java index 09f36dc298..c73827d7b9 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/playback/quality/RememberVideoQualityPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/playback/quality/RememberVideoQualityPatch.java @@ -305,7 +305,7 @@ public static void saveDefaultQuality(int qualityResolution) { } else { toastStringId = "morphe_remember_video_quality_toast_wifi"; } - Utils.showToastShort(str(toastStringId, qualityResolution + "p")); + Utils.showSnackbarShort(str(toastStringId, qualityResolution + "p")); } } diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/playback/speed/CustomPlaybackSpeedPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/playback/speed/CustomPlaybackSpeedPatch.java index 36fee643e0..76d5119d24 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/playback/speed/CustomPlaybackSpeedPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/playback/speed/CustomPlaybackSpeedPatch.java @@ -210,7 +210,7 @@ public static CharSequence onSeekEduOverlayLoaded(ContextInterface context, Char } private static void showInvalidCustomSpeedToast() { - Utils.showToastLong(str("morphe_custom_playback_speeds_invalid", VideoInformation.PLAYBACK_SPEED_MAXIMUM)); + Utils.showSnackbarLong(str("morphe_custom_playback_speeds_invalid", VideoInformation.PLAYBACK_SPEED_MAXIMUM)); } private static float[] loadCustomSpeeds() { @@ -245,7 +245,7 @@ private static float[] loadCustomSpeeds() { return speeds; } catch (Exception ex) { Logger.printInfo(() -> "Parse error", ex); - Utils.showToastShort(str("morphe_custom_playback_speeds_parse_exception")); + Utils.showSnackbarShort(str("morphe_custom_playback_speeds_parse_exception")); Settings.CUSTOM_PLAYBACK_SPEEDS.resetToDefault(); return loadCustomSpeeds(); } diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/playback/speed/RememberPlaybackSpeedPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/playback/speed/RememberPlaybackSpeedPatch.java index c64174fad3..073367ed3f 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/playback/speed/RememberPlaybackSpeedPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/playback/speed/RememberPlaybackSpeedPatch.java @@ -217,8 +217,8 @@ public static void userSelectedPlaybackSpeed(float playbackSpeed) { ? Settings.SHORTS_PLAYBACK_SPEED_DEFAULT : Settings.PLAYBACK_SPEED_DEFAULT; - // Prevent toast spamming if using the 0.05x adjustments. - // Show exactly one toast after the user stops interacting with the speed menu. + // Prevent message spamming if using the 0.05x adjustments. + // Show exactly one message after the user stops interacting with the speed menu. final long now = System.currentTimeMillis(); lastTimeSpeedChanged = now; @@ -237,7 +237,7 @@ public static void userSelectedPlaybackSpeed(float playbackSpeed) { defaultSpeedSetting.save(finalPlaybackSpeed); if (Settings.REMEMBER_PLAYBACK_SPEED_LAST_SELECTED_TOAST.get()) - Utils.showToastShort(str("morphe_remember_playback_speed_toast", + Utils.showSnackbarShort(str("morphe_remember_playback_speed_toast", (String.format(java.util.Locale.US, "%.2fx", finalPlaybackSpeed)))); }, TOAST_DELAY_MILLISECONDS); } catch (Exception ex) { @@ -258,8 +258,8 @@ public static void userSelectedPlaybackAudioPitch(float playbackAudioPitch) { // Will already be in range, below line is just a fail-safe. playbackAudioPitch = Math.min(playbackAudioPitch, VideoInformation.PLAYBACK_AUDIO_PITCH_MAXIMUM); - // Prevent toast spamming if using the 0.05x adjustments. - // Show exactly one toast after the user stops interacting with the pitch menu. + // Prevent message spamming if using the 0.05x adjustments. + // Show exactly one message after the user stops interacting with the pitch menu. final long now = System.currentTimeMillis(); lastTimePitchChanged = now; @@ -277,9 +277,9 @@ public static void userSelectedPlaybackAudioPitch(float playbackAudioPitch) { } Settings.PLAYBACK_AUDIO_PITCH_DEFAULT.save(finalPlaybackAudioPitch); - // Sharing toast with video speed + // Sharing message with video speed if (Settings.REMEMBER_PLAYBACK_SPEED_LAST_SELECTED_TOAST.get()) { - Utils.showToastShort(str("morphe_remember_playback_audio_pitch_toast", + Utils.showSnackbarShort(str("morphe_remember_playback_audio_pitch_toast", (String.format(java.util.Locale.US, "%.2fx", finalPlaybackAudioPitch)))); } }, TOAST_DELAY_MILLISECONDS); diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/queue/NativePlaylistQueuePatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/queue/NativePlaylistQueuePatch.java index 2e43789682..60078cda28 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/queue/NativePlaylistQueuePatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/queue/NativePlaylistQueuePatch.java @@ -548,7 +548,7 @@ private static void updateController(PlaylistController controller) { // Keep the old snapshot until publish attaches the changed rows to the // native backend; preassigning queue would conceal the item-list change. if (publish(controller, queue.playNextAndSelect(videoId), false)) { - Utils.showToastShort(str("morphe_queue_manager_fetch_succeeded_add")); + Utils.showSnackbarShort(str("morphe_queue_manager_fetch_succeeded_add")); completeMetadata(); } @@ -1337,13 +1337,13 @@ public static MessageLite onQueueMenuCommand(MessageLite endpoint, Map con } NativePlayback playback = playbackFor(controller); if (pendingSelection != null || dragSession != null || playback == null) { - Utils.showToastShort(str("morphe_native_playlist_queue_unavailable")); + Utils.showSnackbarShort(str("morphe_native_playlist_queue_unavailable")); return null; } PlaylistQueue before = queue.withRepeatMode(playback.patch_getRepeatMode()); if (!playback.patch_clearQueue()) { - Utils.showToastShort(str("morphe_native_playlist_queue_unavailable")); + Utils.showSnackbarShort(str("morphe_native_playlist_queue_unavailable")); return null; } @@ -1373,7 +1373,7 @@ public static MessageLite onQueueMenuCommand(MessageLite endpoint, Map con return null; } catch (Exception ex) { Logger.printException(() -> "onQueueMenuCommand failure", ex); - Utils.showToastShort(str("morphe_native_playlist_queue_unavailable")); + Utils.showSnackbarShort(str("morphe_native_playlist_queue_unavailable")); return null; } } @@ -1989,7 +1989,7 @@ private static void addToQueue(String videoId, boolean playLast) { if (playLast && queue == null) { // A menu callback can outlive the queue that made Play last available. - Utils.showToastShort(str("morphe_native_playlist_queue_not_ready")); + Utils.showSnackbarShort(str("morphe_native_playlist_queue_not_ready")); return; } @@ -2006,13 +2006,13 @@ private static void addToQueue(String videoId, boolean playLast) { try { if (playlist != null) { if (dragSession != null || queue != null) { - Utils.showToastShort(str("morphe_native_playlist_queue_not_ready")); + Utils.showSnackbarShort(str("morphe_native_playlist_queue_not_ready")); return; } NativePlayback playback = playbackFor(playlist); if (playback == null) { - Utils.showToastShort(str("morphe_native_playlist_queue_unavailable")); + Utils.showSnackbarShort(str("morphe_native_playlist_queue_unavailable")); return; } @@ -2036,7 +2036,7 @@ private static void addToQueue(String videoId, boolean playLast) { refreshPlayerControls(playlist.patch_getNavigator()); QueueFullscreenActions.refresh(); FlyoutUtils.dismissFlyout(); - Utils.showToastShort(str("morphe_queue_manager_fetch_succeeded_add")); + Utils.showSnackbarShort(str("morphe_queue_manager_fetch_succeeded_add")); completeMetadata(); } @@ -2057,7 +2057,7 @@ private static void addToQueue(String videoId, boolean playLast) { PlaylistController controller = readyController(); if (controller == null || dragSession != null) { - Utils.showToastShort(str("morphe_native_playlist_queue_not_ready")); + Utils.showSnackbarShort(str("morphe_native_playlist_queue_not_ready")); return; } @@ -2077,12 +2077,12 @@ private static void addToQueue(String videoId, boolean playLast) { ? candidate : playLast ? candidate.playLast(videoId) : candidate.playNext(videoId); if (publish(controller, updated, true)) { FlyoutUtils.dismissFlyout(); - Utils.showToastShort(str("morphe_queue_manager_fetch_succeeded_add")); + Utils.showSnackbarShort(str("morphe_queue_manager_fetch_succeeded_add")); completeMetadata(); } } catch (Exception ex) { Logger.printException(() -> "Could not add video to native playlist queue", ex); - Utils.showToastShort(str("morphe_native_playlist_queue_unavailable")); + Utils.showSnackbarShort(str("morphe_native_playlist_queue_unavailable")); } } @@ -2177,7 +2177,7 @@ private static void tryStartQueue() { refreshRepeatHeaders(); refreshPlayerControls(controller.patch_getNavigator()); } else { - Utils.showToastShort(str("morphe_queue_manager_fetch_succeeded_add")); + Utils.showSnackbarShort(str("morphe_queue_manager_fetch_succeeded_add")); } completeMetadata(); } @@ -2294,7 +2294,7 @@ public static boolean onQueueMixTap(@Nullable MessageLite endpoint, @Nullable Ma return false; } pendingMixAddition = choice; - Utils.showToastShort(str("morphe_native_playlist_queue_loading_mix")); + Utils.showSnackbarShort(str("morphe_native_playlist_queue_loading_mix")); Utils.runOnBackgroundThread(() -> { if (pendingMixAddition != choice) { return; @@ -2341,7 +2341,7 @@ public static boolean onQueueMixTap(@Nullable MessageLite endpoint, @Nullable Ma Logger.printException(() -> "onQueueMixTap failure", ex); // A failed prompt must not silently discard the playing queue. if (intercepted) { - Utils.showToastShort(str("morphe_native_playlist_queue_unavailable")); + Utils.showSnackbarShort(str("morphe_native_playlist_queue_unavailable")); } return intercepted; } @@ -2387,7 +2387,7 @@ private static void completeMixAddition(MixChoice choice, @Nullable List "Could not append Mix to native queue", ex); - Utils.showToastShort(str("morphe_queue_manager_fetch_failed_add")); + Utils.showSnackbarShort(str("morphe_queue_manager_fetch_failed_add")); } } @@ -2477,7 +2477,7 @@ private static boolean publish(PlaylistController controller, PlaylistQueue cand NativePlayback playback = playbackFor(controller); if (playback == null) { if (reportFailure) { - Utils.showToastShort(str("morphe_native_playlist_queue_unavailable")); + Utils.showSnackbarShort(str("morphe_native_playlist_queue_unavailable")); } return false; } @@ -2486,7 +2486,7 @@ private static boolean publish(PlaylistController controller, PlaylistQueue cand acceptedModel = controller.patch_updatePanel(panel); if (acceptedModel == null) { if (reportFailure) { - Utils.showToastShort(str("morphe_native_playlist_queue_unavailable")); + Utils.showSnackbarShort(str("morphe_native_playlist_queue_unavailable")); } return false; } @@ -2508,7 +2508,7 @@ private static boolean publish(PlaylistController controller, PlaylistQueue cand if (!playback.patch_attachQueue(panel, currentQueueCommand(candidate))) { controller.patch_restorePanel(acceptedModel, previousModel); if (reportFailure) { - Utils.showToastShort(str("morphe_native_playlist_queue_unavailable")); + Utils.showSnackbarShort(str("morphe_native_playlist_queue_unavailable")); } return false; } @@ -2550,7 +2550,7 @@ private static boolean publish(PlaylistController controller, PlaylistQueue cand Logger.printException(() -> "Could not display native playlist queue", ex); if (reportFailure) { - Utils.showToastShort(str("morphe_native_playlist_queue_unavailable")); + Utils.showSnackbarShort(str("morphe_native_playlist_queue_unavailable")); } return false; } finally { diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/theme/SeekbarColorPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/theme/SeekbarColorPatch.java index 9a3606595b..5233f910b6 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/theme/SeekbarColorPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/theme/SeekbarColorPatch.java @@ -89,7 +89,7 @@ private static int loadCustomSeekbarColor() { return color; } catch (Exception ex) { - Utils.showToastShort(str("morphe_seekbar_custom_color_invalid")); + Utils.showSnackbarShort(str("morphe_seekbar_custom_color_invalid")); Settings.SEEKBAR_CUSTOM_COLOR_PRIMARY.resetToDefault(); Settings.SEEKBAR_CUSTOM_COLOR_ACCENT.resetToDefault(); diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/AiSListSubmitDialog.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/AiSListSubmitDialog.java index dd28cc5cfe..ac1d66fe0f 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/AiSListSubmitDialog.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/AiSListSubmitDialog.java @@ -76,16 +76,16 @@ public static void show(String videoId) { if (username.isEmpty()) { // The name appears on the public submitter leaderboard, so it is never // filled in silently and the user sets it in the settings instead. - Utils.showToastLong(str("morphe_aislist_submit_no_username")); + Utils.showSnackbarLong(str("morphe_aislist_submit_no_username")); return; } - Utils.showToastShort(str("morphe_aislist_submit_resolving")); + Utils.showSnackbarShort(str("morphe_aislist_submit_resolving")); Utils.runOnBackgroundThread(() -> { String handle = AiSListSubmitRequest.fetchChannelHandle(videoId); if (handle == null) { - Utils.showToastShort(str("morphe_aislist_submit_failed_channel")); + Utils.showSnackbarShort(str("morphe_aislist_submit_failed_channel")); return; } @@ -94,7 +94,7 @@ public static void show(String videoId) { if (listedIn != null) { // Each list names itself in a whole sentence, because a language that // inflects the list name cannot build one from a substituted noun. - Utils.showToastShort(str(listedIn == AiSListFilter.ListedIn.BLOCKLIST + Utils.showSnackbarShort(str(listedIn == AiSListFilter.ListedIn.BLOCKLIST ? "morphe_aislist_submit_already_blocklisted" : "morphe_aislist_submit_already_warnlisted")); return; @@ -135,7 +135,7 @@ private static void submit(String videoId, String handle, String username) { String messageKey = AiSListSubmitRequest.submit( handle, "https://youtu.be/" + videoId, username); - Utils.showToastShort(str(messageKey)); + Utils.showSnackbarShort(str(messageKey)); }); } } diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/PlaylistPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/PlaylistPatch.java index e598812994..b7b6c29eba 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/PlaylistPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/PlaylistPatch.java @@ -219,7 +219,7 @@ private static void fetchQueue(Context context, boolean remove, boolean openPlay Settings.QUEUE_PLAYLIST_ID.save(createdPlaylistId); } lastVideoIds.putIfAbsent(currentVideoId, setVideoId); - showToast(fetchSucceededCreate); + showSnackbar(fetchSucceededCreate); Logger.printDebug(() -> "Queue created, playlistId: " + createdPlaylistId + ", setVideoId: " + setVideoId); if (openPlaylist) { @@ -229,7 +229,7 @@ private static void fetchQueue(Context context, boolean remove, boolean openPlay } } } - showToast(fetchFailedCreate); + showSnackbar(fetchFailedCreate); } else { String setVideoId = lastVideoIds.get(currentVideoId); EditPlaylistRequest.fetchRequestIfNeeded(currentVideoId, currentPlaylistId, @@ -242,20 +242,20 @@ private static void fetchQueue(Context context, boolean remove, boolean openPlay if ("".equals(fetchedSetVideoId)) { lastVideoIds.remove(currentVideoId, setVideoId); EditPlaylistRequest.clearVideoId(currentVideoId); - showToast(fetchSucceededRemove); + showSnackbar(fetchSucceededRemove); if (openPlaylist) { openQueue(context, currentVideoId, openVideo, reload); } return; } - showToast(fetchFailedRemove); + showSnackbar(fetchFailedRemove); } else { if (fetchedSetVideoId == null || fetchedSetVideoId.isEmpty()) { Logger.printDebug(() -> "Playlist not available fetchedSetVideoId: " + fetchedSetVideoId); if (!retry) { // Already retried, give up. - showToast(fetchFailedAdd); + showSnackbar(fetchFailedAdd); return; } // Clear saved playlist and try again. @@ -267,7 +267,7 @@ private static void fetchQueue(Context context, boolean remove, boolean openPlay lastVideoIds.putIfAbsent(currentVideoId, fetchedSetVideoId); EditPlaylistRequest.clearVideoId(currentVideoId); Logger.printDebug(() -> "Video added, setVideoId: " + fetchedSetVideoId); - showToast(fetchSucceededAdd); + showSnackbar(fetchSucceededAdd); if (openPlaylist) { openQueue(context, currentVideoId, openVideo, reload); } @@ -354,11 +354,11 @@ private static void saveToPlaylist(@Nullable String libraryId, @Nullable String Utils.runOnBackgroundThread(() -> { Boolean result = request.getResult(); if (Boolean.TRUE.equals(result)) { - showToast(String.format(fetchSucceededSave, libraryTitle)); + showSnackbar(String.format(fetchSucceededSave, libraryTitle)); SavePlaylistRequest.clear(); return; } - showToast(fetchFailedSave); + showSnackbar(fetchFailedSave); }); } catch (Exception ex) { Logger.printException(() -> "saveToPlaylist failure", ex); @@ -410,11 +410,11 @@ private static void openQueue(Context context, String currentVideoId, boolean op } private static void handleCheckError(String reason) { - showToast(String.format(checkFailedGeneric, reason)); + showSnackbar(String.format(checkFailedGeneric, reason)); } - private static void showToast(String reason) { - Utils.showToastShort(reason); + private static void showSnackbar(String reason) { + Utils.showSnackbarShort(reason); } public enum QueueManager { diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/voiceovertranslation/VoiceOverTranslationPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/voiceovertranslation/VoiceOverTranslationPatch.java index 3a93b5869d..7eff2b38ca 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/voiceovertranslation/VoiceOverTranslationPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/voiceovertranslation/VoiceOverTranslationPatch.java @@ -924,7 +924,7 @@ static void notifyOpenRouterError(int httpCode, String errorBody) { showOpenRouterCreditsDialog(activity); }); } else { - Utils.showToastLong(str("morphe_vot_openrouter_error", httpCode)); + Utils.showSnackbarLong(str("morphe_vot_openrouter_error", httpCode)); } } @@ -969,7 +969,7 @@ static void notifyMyMemoryError(int responseStatus, @Nullable String details) { showMyMemoryQuotaDialog(activity, nextAvailableAt); }); } else { - Utils.showToastLong(str("morphe_vot_mymemory_error", responseStatus)); + Utils.showSnackbarLong(str("morphe_vot_mymemory_error", responseStatus)); } } diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/series/BackupPreference.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/series/BackupPreference.java index 3e69d52bf3..5a5ac7ab94 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/series/BackupPreference.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/series/BackupPreference.java @@ -63,7 +63,7 @@ private static void confirmRestore( sheet.action("morphe_series_tracker_backup_restore", () -> { sheet.dialog.dismiss(); service.restoreBackup(data, - added -> Utils.showToastLong( + added -> Utils.showSnackbarLong( UiText.format("morphe_series_tracker_backup_restored", added)), BackupPreference::toast); }, true); @@ -76,6 +76,6 @@ private static boolean isShowing(AbstractPreferenceFragment fragment) { } private static void toast(String key) { - Utils.showToastLong(UiText.get(key)); + Utils.showSnackbarLong(UiText.get(key)); } } diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/series/HistoryUi.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/series/HistoryUi.java index 13879d810f..83981be948 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/series/HistoryUi.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/series/HistoryUi.java @@ -145,7 +145,7 @@ private static void navigate(Context c) { Logger.printException(() -> "Could not open History", e); openSeriesUntil = 0; openSeriesId = ""; - Utils.showToastLong(UiText.get("morphe_series_tracker_ui_open_history_from_you_to_view_your_series")); + Utils.showSnackbarLong(UiText.get("morphe_series_tracker_ui_open_history_from_you_to_view_your_series")); } } diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/series/LibraryDialog.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/series/LibraryDialog.java index e50e9559b2..8e89dc0ecd 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/series/LibraryDialog.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/series/LibraryDialog.java @@ -984,7 +984,7 @@ private void error(String message) { } private void toast(String message) { - Utils.showToastLong(UiText.get(message)); + Utils.showSnackbarLong(UiText.get(message)); } private LinearLayout column() { diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/series/PlayerSeriesAction.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/series/PlayerSeriesAction.java index e35e8afd04..8807405f7b 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/series/PlayerSeriesAction.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/series/PlayerSeriesAction.java @@ -54,7 +54,7 @@ static void open(Context context) { } java.util.function.Consumer done = id -> - Utils.showToastShort(UiText.get("morphe_series_tracker_followed")); + Utils.showSnackbarShort(UiText.get("morphe_series_tracker_followed")); if (playlist.isEmpty()) { java.util.List saved = new java.util.ArrayList<>(); @@ -74,7 +74,7 @@ static void open(Context context) { message -> { pending.clear(); if (!activity.isFinishing() && !activity.isDestroyed()) - Utils.showToastLong(UiText.get(message)); + Utils.showSnackbarLong(UiText.get(message)); }); } diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/series/RecordingPreference.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/series/RecordingPreference.java index 93aa37b703..172879d6cd 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/series/RecordingPreference.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/series/RecordingPreference.java @@ -58,7 +58,7 @@ protected void onClick() { static void requestEnable(Context context, Runnable done) { if (!RecordingPrivacy.canEnable()) { - Utils.showToastLong(UiText.get("morphe_series_tracker_identity_unavailable")); + Utils.showSnackbarLong(UiText.get("morphe_series_tracker_identity_unavailable")); return; } long consentGeneration = RecordingPrivacy.generation(); @@ -70,7 +70,7 @@ static void requestEnable(Context context, Runnable done) { () -> { boolean enabled = RecordingPrivacy.enable(consentGeneration); if (!enabled) - Utils.showToastLong(UiText.get("morphe_series_tracker_identity_unavailable")); + Utils.showSnackbarLong(UiText.get("morphe_series_tracker_identity_unavailable")); done.run(); }); } diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/series/SeriesPlayback.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/series/SeriesPlayback.java index 6b323c90b5..f7e564b077 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/series/SeriesPlayback.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/series/SeriesPlayback.java @@ -103,7 +103,7 @@ static boolean navigate(Enum intent) { navigating = false; // Autoplay at the end is consumed, so it does not continue with recommendations. if (!autoplay) { - Utils.showToastLong(UiText.get("morphe_series_tracker_playback_boundary")); + Utils.showSnackbarLong(UiText.get("morphe_series_tracker_playback_boundary")); } return; } @@ -145,7 +145,7 @@ private static boolean valid(Session active, @Nullable PlaybackBridge.Source sou private static void fail(String message) { clear(); - Utils.showToastLong(UiText.get(message)); + Utils.showSnackbarLong(UiText.get(message)); } private SeriesPlayback() {} diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/series/SyncPreference.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/series/SyncPreference.java index 8db0d3615b..72ed69ab59 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/series/SyncPreference.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/series/SyncPreference.java @@ -58,7 +58,7 @@ protected void onClick() { static void requestEnable(Context context, Runnable done) { if (!RecordingPrivacy.canEnable()) { - Utils.showToastLong(UiText.get("morphe_series_tracker_identity_unavailable")); + Utils.showSnackbarLong(UiText.get("morphe_series_tracker_identity_unavailable")); return; } long consentGeneration = RecordingPrivacy.generation(); @@ -70,7 +70,7 @@ static void requestEnable(Context context, Runnable done) { () -> { boolean enabled = RecordingPrivacy.enableSync(consentGeneration); if (!enabled) - Utils.showToastLong(UiText.get("morphe_series_tracker_identity_unavailable")); + Utils.showSnackbarLong(UiText.get("morphe_series_tracker_identity_unavailable")); if (enabled) TrackerService.get(context).syncYouTube(true, () -> {}); done.run(); }); diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/series/TrackerRuntime.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/series/TrackerRuntime.java index 8a8a2a8c83..0aba86f9fe 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/series/TrackerRuntime.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/series/TrackerRuntime.java @@ -124,7 +124,7 @@ public void run() { if (!waiting) return; if (SystemClock.elapsedRealtime() - requestedAt >= 15000) { cancelLaunch(); - Utils.showToastLong(UiText.get("morphe_series_tracker_error_resume")); + Utils.showSnackbarLong(UiText.get("morphe_series_tracker_error_resume")); } else main.postDelayed(this, 250); } }, diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/Settings.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/Settings.java index fb44265433..8ef0dcc997 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/Settings.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/Settings.java @@ -557,6 +557,7 @@ public class Settings extends SharedYouTubeSettings { // General (Layout) public static final BooleanSetting DISABLE_LAYOUT_UPDATES = new BooleanSetting("morphe_disable_layout_updates", FALSE, true,"morphe_disable_layout_updates_user_dialog_message"); public static final BooleanSetting RESTORE_OLD_SETTINGS_MENUS = new BooleanSetting("morphe_restore_old_settings_menus", FALSE, true); + public static final BooleanSetting NATIVE_SNACKBAR = new BooleanSetting("morphe_native_snackbar", FALSE); public static final EnumSetting CHANGE_FORM_FACTOR = new EnumSetting<>("morphe_change_form_factor", FormFactor.DEFAULT, true, "morphe_change_form_factor_user_dialog_message"); public static final BooleanSetting TABLET_LAYOUT_IN_PLAYER = new BooleanSetting("morphe_tablet_layout_in_player", FALSE, true, new TabletLayoutInPlayerAvailability()); public static final BooleanSetting BYPASS_IMAGE_REGION_RESTRICTIONS = new BooleanSetting("morphe_bypass_image_region_restrictions", FALSE, true); diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/preference/GetCaptionCookiesPreference.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/preference/GetCaptionCookiesPreference.java index cdc38154f9..364cee504e 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/preference/GetCaptionCookiesPreference.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/preference/GetCaptionCookiesPreference.java @@ -72,12 +72,12 @@ public boolean onPreferenceClick(Preference preference) { AbstractPreferenceFragment.settingImportInProgress = false; AbstractPreferenceFragment.setRestartNeeded(); - Utils.showToastShort(str("morphe_get_caption_cookies_success")); + Utils.showSnackbarShort(str("morphe_get_caption_cookies_success")); } else { - Utils.showToastShort(str("morphe_get_caption_cookies_duplicate")); + Utils.showSnackbarShort(str("morphe_get_caption_cookies_duplicate")); } } else { - Utils.showToastShort(str("morphe_get_caption_cookies_failed")); + Utils.showSnackbarShort(str("morphe_get_caption_cookies_failed")); } return true; diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/preference/WatchLaterPlaylistPreference.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/preference/WatchLaterPlaylistPreference.java index 48474eb03a..f80ddab0d5 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/preference/WatchLaterPlaylistPreference.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/preference/WatchLaterPlaylistPreference.java @@ -36,7 +36,7 @@ public WatchLaterPlaylistPreference(Context context, AttributeSet attrs) { @Override protected void onClick() { if (AuthUtils.isNotLoggedIn()) { - Utils.showToastShort(str("morphe_queue_manager_check_failed_auth")); + Utils.showSnackbarShort(str("morphe_queue_manager_check_failed_auth")); return; } diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/sponsorblock/SponsorBlockSettings.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/sponsorblock/SponsorBlockSettings.java index b3c3668da6..517b509530 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/sponsorblock/SponsorBlockSettings.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/sponsorblock/SponsorBlockSettings.java @@ -68,9 +68,9 @@ public static void importDesktopSettings(@NonNull String json) { final int desktopValue = categorySelectionObject.getInt("option"); CategoryBehaviour behaviour = CategoryBehaviour.byDesktopKeyValue(desktopValue); if (behaviour == null) { - Utils.showToastLong(categoryKey + " unknown behavior key: " + desktopValue); + Utils.showSnackbarLong(categoryKey + " unknown behavior key: " + desktopValue); } else if (category == SegmentCategory.HIGHLIGHT && behaviour == CategoryBehaviour.SKIP_AUTOMATICALLY_ONCE) { - Utils.showToastLong("Skip-once behavior not allowed for " + category.keyValue); + Utils.showSnackbarLong("Skip-once behavior not allowed for " + category.keyValue); category.setBehaviour(CategoryBehaviour.SKIP_AUTOMATICALLY); // Use the closest match. } else { category.setBehaviour(behaviour); @@ -117,10 +117,10 @@ public static void importDesktopSettings(@NonNull String json) { Settings.SB_LOCAL_TIME_SAVED_MILLISECONDS.save((long) (minutesSaved * 60 * 1000)); } - Utils.showToastLong(str("morphe_sb_settings_import_successful")); + Utils.showSnackbarLong(str("morphe_sb_settings_import_successful")); } catch (Exception ex) { - Logger.printInfo(() -> "failed to import settings", ex); // Use info level, as we are showing our own toast. - Utils.showToastLong(str("morphe_sb_settings_import_failed", ex.getMessage())); + Logger.printInfo(() -> "failed to import settings", ex); // Use info level, as we are showing our own message. + Utils.showSnackbarLong(str("morphe_sb_settings_import_failed", ex.getMessage())); } } @@ -167,8 +167,8 @@ public static String exportDesktopSettings() { return json.toString(2); } catch (Exception ex) { - Logger.printInfo(() -> "failed to export settings", ex); // Use info level, as we are showing our own toast. - Utils.showToastLong(str("morphe_sb_settings_export_failed", ex)); + Logger.printInfo(() -> "failed to export settings", ex); // Use info level, as we are showing our own message. + Utils.showSnackbarLong(str("morphe_sb_settings_export_failed", ex)); return ""; } } diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/sponsorblock/SponsorBlockUtils.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/sponsorblock/SponsorBlockUtils.java index 40eda4cb6a..60191992a1 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/sponsorblock/SponsorBlockUtils.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/sponsorblock/SponsorBlockUtils.java @@ -73,7 +73,7 @@ public void onClick(DialogInterface dialog, int which) { SegmentCategory category = SegmentCategory.categoriesWithoutUnsubmitted()[which]; final boolean enableButton; if (category.behaviour == CategoryBehaviour.IGNORE) { - Utils.showToastLong(str("morphe_sb_new_segment_disabled_category")); + Utils.showSnackbarLong(str("morphe_sb_new_segment_disabled_category")); enableButton = false; } else { newUserCreatedSegmentCategory = category; @@ -244,7 +244,7 @@ public static void onPublishClicked() { final boolean hasEnd = newSponsorSegmentEndMillis >= 0; if (!hasStart && !hasEnd) { - Utils.showToastShort(str("morphe_sb_new_segment_mark_locations_first")); + Utils.showSnackbarShort(str("morphe_sb_new_segment_mark_locations_first")); return; } @@ -270,12 +270,12 @@ public static void onPublishClicked() { // Both start and end marked. if (newSponsorSegmentStartMillis >= newSponsorSegmentEndMillis) { - Utils.showToastShort(str("morphe_sb_new_segment_start_is_before_end")); + Utils.showSnackbarShort(str("morphe_sb_new_segment_start_is_before_end")); return; } if (!newSponsorSegmentPreviewed) { - Utils.showToastShort(str("morphe_sb_new_segment_preview_segment_first")); + Utils.showSnackbarShort(str("morphe_sb_new_segment_preview_segment_first")); return; } @@ -320,7 +320,7 @@ public static void onVotingClicked(Context context) { // Button is hidden if no segments exist. // But if prior video had segments, and current video does not, // then the button persists until the overlay fades out (this is intentional, as abruptly hiding the button is jarring). - Utils.showToastShort(str("morphe_sb_vote_no_segments")); + Utils.showSnackbarShort(str("morphe_sb_vote_no_segments")); return; } @@ -354,7 +354,7 @@ public static void onVotingClicked(Context context) { } if (titleList.isEmpty()) { - Utils.showToastShort(str("morphe_sb_vote_no_segments")); + Utils.showSnackbarShort(str("morphe_sb_vote_no_segments")); return; } @@ -394,9 +394,9 @@ public static void onPreviewClicked() { try { Utils.verifyOnMainThread(); if (newSponsorSegmentStartMillis < 0 || newSponsorSegmentEndMillis < 0) { - Utils.showToastShort(str("morphe_sb_new_segment_mark_locations_first")); + Utils.showSnackbarShort(str("morphe_sb_new_segment_mark_locations_first")); } else if (newSponsorSegmentStartMillis >= newSponsorSegmentEndMillis) { - Utils.showToastShort(str("morphe_sb_new_segment_start_is_before_end")); + Utils.showSnackbarShort(str("morphe_sb_new_segment_start_is_before_end")); } else { SegmentPlaybackController.removeUnsubmittedSegments(); // If user hits preview more than once before playing. SegmentPlaybackController.addUnsubmittedSegment( @@ -548,7 +548,7 @@ public void saveTime(boolean useCurrentTime) { } else { time = parseSegmentTime(editText.getText().toString()); if (time < 0) { - Utils.showToastLong(str("morphe_sb_new_segment_edit_by_hand_parse_error")); + Utils.showSnackbarLong(str("morphe_sb_new_segment_edit_by_hand_parse_error")); return; } } diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/sponsorblock/preferences/SponsorBlockApiUrlPreference.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/sponsorblock/preferences/SponsorBlockApiUrlPreference.java index 5aa12b9e2a..3d2809168d 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/sponsorblock/preferences/SponsorBlockApiUrlPreference.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/sponsorblock/preferences/SponsorBlockApiUrlPreference.java @@ -65,17 +65,17 @@ private void installClickListener() { () -> { String serverAddress = editText.getText().toString(); if (!SponsorBlockHelpers.isValidSBServerAddress(serverAddress)) { - Utils.showToastLong(str("morphe_sb_api_url_invalid")); + Utils.showSnackbarLong(str("morphe_sb_api_url_invalid")); } else if (!serverAddress.equals(Settings.SB_API_URL.get())) { Settings.SB_API_URL.save(serverAddress); - Utils.showToastLong(str("morphe_sb_api_url_changed")); + Utils.showSnackbarLong(str("morphe_sb_api_url_changed")); } }, () -> {}, str("morphe_settings_reset"), () -> { Settings.SB_API_URL.resetToDefault(); - Utils.showToastLong(str("morphe_sb_api_url_reset")); + Utils.showSnackbarLong(str("morphe_sb_api_url_reset")); }, true ); diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/sponsorblock/preferences/SponsorBlockPrivateUserIdPreference.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/sponsorblock/preferences/SponsorBlockPrivateUserIdPreference.java index 428894aa6e..e7e10870ff 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/sponsorblock/preferences/SponsorBlockPrivateUserIdPreference.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/sponsorblock/preferences/SponsorBlockPrivateUserIdPreference.java @@ -55,7 +55,7 @@ private void installChangeListener() { setOnPreferenceChangeListener((p, newValue) -> { String newUUID = newValue.toString(); if (!SponsorBlockHelpers.isValidSBUserID(newUUID)) { - Utils.showToastLong(str("morphe_sb_private_user_id_Do_Not_Share_invalid")); + Utils.showSnackbarLong(str("morphe_sb_private_user_id_Do_Not_Share_invalid")); return false; } return true; diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/sponsorblock/preferences/SponsorBlockSegmentStepPreference.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/sponsorblock/preferences/SponsorBlockSegmentStepPreference.java index b99f56540a..e7800e7c07 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/sponsorblock/preferences/SponsorBlockSegmentStepPreference.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/sponsorblock/preferences/SponsorBlockSegmentStepPreference.java @@ -18,7 +18,7 @@ /** * Numeric input for the "create new segment" time-adjustment step. Rejects zero and noninteger - * values with a toast, matching the legacy SponsorBlock preferences behavior. + * values with a message, matching the legacy SponsorBlock preferences behavior. */ @SuppressWarnings({"unused", "deprecation"}) public class SponsorBlockSegmentStepPreference extends ResettableEditTextPreference { @@ -52,7 +52,7 @@ private void installValidator() { } catch (NumberFormatException ex) { Logger.printInfo(() -> "Invalid new segment step", ex); } - Utils.showToastLong(str("morphe_sb_create_new_segment_step_invalid")); + Utils.showSnackbarLong(str("morphe_sb_create_new_segment_step_invalid")); return false; }); } diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/sponsorblock/ui/SponsorBlockStatsPreferenceCategory.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/sponsorblock/ui/SponsorBlockStatsPreferenceCategory.java index acde80fed7..1af6b4c222 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/sponsorblock/ui/SponsorBlockStatsPreferenceCategory.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/sponsorblock/ui/SponsorBlockStatsPreferenceCategory.java @@ -110,7 +110,7 @@ private void addUserStats(Preference loadingPlaceholder, @Nullable UserStats sta if (errorMessage == null) { preference.setTitle(Html.fromHtml(str("morphe_sb_stats_username", newUserName), Html.FROM_HTML_MODE_COMPACT)); preference.setText(newUserName); - Utils.showToastLong(str("morphe_sb_stats_username_changed")); + Utils.showSnackbarLong(str("morphe_sb_stats_username_changed")); } else { preference.setText(userName); // revert to previous SponsorBlockUtils.showErrorDialog(errorMessage); diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/SwipeControlsConfigurationProvider.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/SwipeControlsConfigurationProvider.java index 449a0d5b1f..dc0352d855 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/SwipeControlsConfigurationProvider.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/swipecontrols/SwipeControlsConfigurationProvider.java @@ -381,7 +381,7 @@ public int getOverlayBackgroundOpacity() { /** * The color of the progress bar in the overlay for brightness. - * Resets to default and shows a toast if the color string is invalid or empty. + * Resets to default and shows a message if the color string is invalid or empty. */ public int getOverlayBrightnessProgressColor() { return getSettingColor(Settings.SWIPE_OVERLAY_BRIGHTNESS_COLOR); @@ -389,7 +389,7 @@ public int getOverlayBrightnessProgressColor() { /** * The color of the progress bar in the overlay for volume. - * Resets to default and shows a toast if the color string is invalid or empty. + * Resets to default and shows a message if the color string is invalid or empty. */ public int getOverlayVolumeProgressColor() { return getSettingColor(Settings.SWIPE_OVERLAY_VOLUME_COLOR); @@ -411,7 +411,7 @@ private static int getSettingColor(StringSetting setting) { // If a user imports bad data, the color picker preference resets the // bad color before this method can be called. Logger.printDebug(() -> "Could not parse color: " + setting, ex); - Utils.showToastLong(str("morphe_settings_color_invalid")); + Utils.showSnackbarLong(str("morphe_settings_color_invalid")); setting.resetToDefault(); return getSettingColor(setting); // Recursively return. } diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/videoplayer/CopyVideoLinkButton.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/videoplayer/CopyVideoLinkButton.java index 30356d85b3..98462de55e 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/videoplayer/CopyVideoLinkButton.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/videoplayer/CopyVideoLinkButton.java @@ -86,12 +86,12 @@ public static void copyLink(boolean withTimestamp) { final long currentVideoTimeInSeconds = appendCurrentVideoTimeInSeconds(withTimestamp, builder); Utils.setClipboard(builder.toString()); - // Do not show a toast if using Android 13+ as it shows its own toast. - // But if the user copied with a timestamp then show a toast. - // Unfortunately this will show 2 toasts on Android 13+, but no way around this. + // Do not show a message if using Android 13+ as it shows its own toast. + // But if the user copied with a timestamp then show a message. + // Unfortunately this will show 2 messages on Android 13+, but no way around this. if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2 || (withTimestamp && currentVideoTimeInSeconds > 0)) { - Utils.showToastShort(withTimestamp && currentVideoTimeInSeconds > 0 + Utils.showSnackbarShort(withTimestamp && currentVideoTimeInSeconds > 0 ? str("morphe_share_copy_link_with_timestamp_button_success") : str("morphe_share_copy_link_button_success")); } diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/videoplayer/LoopVideoButton.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/videoplayer/LoopVideoButton.java index 56bbd9b12f..9eecc3d149 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/videoplayer/LoopVideoButton.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/videoplayer/LoopVideoButton.java @@ -110,7 +110,7 @@ private static void handleShortClick(View buttonView) { final boolean newState = !Settings.LOOP_VIDEO.get(); setLoopButton(newState); - Utils.showToastShort(str(newState + Utils.showSnackbarShort(str(newState ? "morphe_loop_video_button_toast_on" : "morphe_loop_video_button_toast_off")); @@ -174,7 +174,7 @@ private static void showRangeDialog(Context context) { private static void applyRange(String startInput, String endInput, long videoLengthMs) { final long startMs = parseTime(startInput); if (startMs < 0) { - Utils.showToastShort(str(videoRangeInvalidTimeStringName)); + Utils.showSnackbarShort(str(videoRangeInvalidTimeStringName)); return; } @@ -182,14 +182,14 @@ private static void applyRange(String startInput, String endInput, long videoLen boolean endIsVideoEnd = endInput.trim().isEmpty(); if (endIsVideoEnd) { if (videoLengthMs <= 0) { - Utils.showToastShort(str(videoRangeInvalidTimeStringName)); + Utils.showSnackbarShort(str(videoRangeInvalidTimeStringName)); return; } endMs = videoLengthMs; } else { final long parsed = parseTime(endInput); if (parsed < 0) { - Utils.showToastShort(str(videoRangeInvalidTimeStringName)); + Utils.showSnackbarShort(str(videoRangeInvalidTimeStringName)); return; } if (videoLengthMs > 0 && parsed >= videoLengthMs) { @@ -201,7 +201,7 @@ private static void applyRange(String startInput, String endInput, long videoLen } if (endMs <= startMs) { - Utils.showToastShort(str(videoRangeInvalidTimeStringName)); + Utils.showSnackbarShort(str(videoRangeInvalidTimeStringName)); return; } @@ -212,7 +212,7 @@ private static void applyRange(String startInput, String endInput, long videoLen LoopVideoPatch.setRange(startMs, endMs, endIsVideoEnd); Settings.LOOP_VIDEO.save(true); - Utils.showToastShort(str("morphe_loop_video_range_toast_on", + Utils.showSnackbarShort(str("morphe_loop_video_range_toast_on", formatTime(startMs), formatTime(endMs))); updateButtonIcon(); } diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/videoplayer/PlayAllButton.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/videoplayer/PlayAllButton.java index 200d2f0d78..8e070ac440 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/videoplayer/PlayAllButton.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/videoplayer/PlayAllButton.java @@ -102,7 +102,7 @@ private static void openVideo(View view, @Nullable PlaylistIDPrefix playlistIdPr sb.append(baseId); } else { Logger.printDebug(() -> "Play all button: Invalid or missing Channel ID: " + channelId); - Utils.showToastShort(str("morphe_play_all_button_not_available_toast")); + Utils.showSnackbarShort(str("morphe_play_all_button_not_available_toast")); return; } } else { diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/whitelist/ChannelWhitelist.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/whitelist/ChannelWhitelist.java index ef1a5979f5..bdcdb606ea 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/whitelist/ChannelWhitelist.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/whitelist/ChannelWhitelist.java @@ -155,13 +155,13 @@ public static boolean isEmpty(WhitelistType type) { public static void toggleChannel(WhitelistType type, String channelId, @Nullable String channelName) { if (isChannelWhitelisted(type, channelId)) { removeChannel(type, channelId); - Utils.showToastShort(str("morphe_channel_whitelist_channel_removed")); + Utils.showSnackbarShort(str("morphe_channel_whitelist_channel_removed")); } else { Map channels = new LinkedHashMap<>(getWhitelistedChannels(type)); if (!channelId.isEmpty() && channels.put(channelId, channelName == null ? "" : channelName) == null) { save(type, channels); } - Utils.showToastShort(str("morphe_channel_whitelist_channel_added")); + Utils.showSnackbarShort(str("morphe_channel_whitelist_channel_added")); } } diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/whitelist/ChannelWhitelistDialog.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/whitelist/ChannelWhitelistDialog.java index 1a3747ce7a..28faeef69a 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/whitelist/ChannelWhitelistDialog.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/whitelist/ChannelWhitelistDialog.java @@ -123,7 +123,7 @@ private static ScrollView createChannelList(Context context, WhitelistType type, false, () -> { ChannelWhitelist.removeChannel(type, channelId); - Utils.showToastShort(str("morphe_channel_whitelist_channel_removed")); + Utils.showSnackbarShort(str("morphe_channel_whitelist_channel_removed")); buildContent(context, type, container); } )); diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d19e200f90..51c1614f74 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,7 +2,7 @@ [versions] morphe-patcher = "2.0.0" -morphe-patches-library = "2.0.2-dev.1" +morphe-patches-library = "2.1.0-dev.1" smali = "4220ae75f7" agp = "9.1.0" annotation = "1.11.0" diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/misc/settings/Fingerprints.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/settings/Fingerprints.kt index 6b678e2b0e..a806f49e99 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/misc/settings/Fingerprints.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/settings/Fingerprints.kt @@ -43,3 +43,23 @@ internal object SettingsPreferenceScreenSyntheticFingerprint : Fingerprint( opcode(Opcode.RETURN_VOID) ) ) + +/** + * Looks up the account scoped command router, which only the command router of an activity does. + */ +internal object CommandRouterFragmentFingerprint : Fingerprint( + accessFlags = listOf(AccessFlags.PUBLIC, AccessFlags.STATIC), + strings = listOf("com.google.android.apps.youtube.app.endpoint.routers.AccountScopeCommandRouterFragment") +) + +/** + * The command router of an activity, created with it. + */ +internal object CommandRouterConstructorFingerprint : Fingerprint( + classFingerprint = CommandRouterFragmentFingerprint, + name = "", + parameters = listOf("Landroid/app/Activity;", "L", "Ljava/util/concurrent/Executor;"), + filters = listOf( + methodCall(smali = "Ljava/lang/Object;->()V") + ) +) diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/misc/settings/SettingsPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/settings/SettingsPatch.kt index 1e6343d307..ca4596f870 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/misc/settings/SettingsPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/settings/SettingsPatch.kt @@ -11,6 +11,7 @@ package app.morphe.patches.youtube.misc.settings import app.morphe.patcher.Fingerprint +import app.morphe.patcher.extensions.InstructionExtensions.addInstruction import app.morphe.patcher.extensions.InstructionExtensions.addInstructions import app.morphe.patcher.extensions.InstructionExtensions.getInstruction import app.morphe.patcher.patch.BytecodePatchContext @@ -28,6 +29,7 @@ import app.morphe.patches.all.misc.resources.setAddResourceLocale import app.morphe.patches.shared.BoldIconsFeatureFlagFingerprint import app.morphe.patches.shared.GoogleApiActivityOnCreateFingerprint import app.morphe.patches.shared.layout.branding.addLicensePatch +import app.morphe.patches.shared.misc.bridge.addBridge import app.morphe.patches.shared.misc.checks.experimentalAppNoticePatch import app.morphe.patches.shared.misc.initialization.initializationPatch import app.morphe.patches.shared.misc.settings.MORPHE_SETTINGS_INTENT @@ -47,6 +49,7 @@ import app.morphe.patches.shared.misc.settings.preference.PreferenceScreenPrefer import app.morphe.patches.shared.misc.settings.preference.SwitchPreference import app.morphe.patches.shared.misc.settings.preference.TextPreference import app.morphe.patches.shared.misc.settings.settingsPatch +import app.morphe.patches.youtube.interaction.nativeplaylistqueue.parseProtoInstructions import app.morphe.patches.youtube.misc.extension.sharedExtensionPatch import app.morphe.patches.youtube.misc.fix.accessibility.fixCaptionsAccessibilityPatch import app.morphe.patches.youtube.misc.fix.accessibility.fixShortsAccessibilityPatch @@ -80,6 +83,9 @@ import com.android.tools.smali.dexlib2.util.MethodUtil private const val BASE_ACTIVITY_HOOK_CLASS = "Lapp/morphe/extension/shared/settings/BaseActivityHook;" private const val YOUTUBE_ACTIVITY_HOOK_CLASS = "Lapp/morphe/extension/youtube/settings/YouTubeActivityHook;" +private const val NATIVE_SNACKBAR_CLASS = "Lapp/morphe/extension/youtube/patches/NativeSnackbarPatch;" +private const val COMMAND_ROUTER_INTERFACE = + $$"Lapp/morphe/extension/youtube/patches/NativeSnackbarPatch$CommandRouter;" private val preferences = mutableSetOf() @@ -271,6 +277,7 @@ val settingsPatch = bytecodePatch( PreferenceScreen.GENERAL.addPreferences( SwitchPreference("morphe_settings_search_history"), SwitchPreference("morphe_show_menu_icons"), + SwitchPreference("morphe_native_snackbar", summary = true), customSettingsNamePreference() ) @@ -358,6 +365,40 @@ val settingsPatch = bytecodePatch( } } + // The command router of an activity runs the command a server response shows a snackbar with, + // so the extension shows its messages in the YouTube snackbar instead of a toast. + CommandRouterConstructorFingerprint.let { + val routerClass = it.classDef + // The method of the router interface that runs one command. + val executeCommandMethod = routerClass.interfaces.flatMap { type -> classDefBy(type).methods } + .single { method -> + method.returnType == "V" && method.parameterTypes.size == 2 && + method.parameterTypes[0].toString() != "Ljava/util/List;" && + method.parameterTypes[1].toString() == "Ljava/util/Map;" + } + val commandClass = executeCommandMethod.parameterTypes.first().toString() + + routerClass.interfaces.add(COMMAND_ROUTER_INTERFACE) + routerClass.addBridge( + "patch_executeCommand", listOf("[B"), "V", 4, + """ + ${parseProtoInstructions(commandClass, bytes = "p1", result = "v0", registry = "v1")} + check-cast v0, $commandClass + new-instance v1, Ljava/util/HashMap; + invoke-direct { v1 }, Ljava/util/HashMap;->()V + invoke-interface { p0, v0, v1 }, $executeCommandMethod + return-void + """ + ) + + // Right after the super constructor, before the activity register can be reused. + it.method.addInstruction( + it.instructionMatches.first().index + 1, + "invoke-static { p0, p1 }, $NATIVE_SNACKBAR_CLASS->" + + "setCommandRouter(${COMMAND_ROUTER_INTERFACE}Landroid/app/Activity;)V" + ) + } + modifyActivityForSettingsInjection( GoogleApiActivityOnCreateFingerprint, YOUTUBE_ACTIVITY_HOOK_CLASS, diff --git a/patches/src/main/resources/addresources/values/youtube/strings.xml b/patches/src/main/resources/addresources/values/youtube/strings.xml index b840e69b72..d73579670a 100644 --- a/patches/src/main/resources/addresources/values/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values/youtube/strings.xml @@ -31,6 +31,8 @@ Second \"item\" text" Miscellaneous Video Restore old YouTube settings screen + Show messages in YouTube snackbar + Morphe messages, such as the confirmation of a player button, are shown in the YouTube snackbar instead of a popup Remove background play restrictions diff --git a/patches/src/test/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/QueueRuntimeBoundaryTest.kt b/patches/src/test/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/QueueRuntimeBoundaryTest.kt index c284ccab27..c973dcf2f4 100644 --- a/patches/src/test/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/QueueRuntimeBoundaryTest.kt +++ b/patches/src/test/kotlin/app/morphe/patches/youtube/interaction/nativeplaylistqueue/QueueRuntimeBoundaryTest.kt @@ -424,7 +424,7 @@ class QueueRuntimeBoundaryTest { public static android.app.Activity getActivity() { return new android.app.Activity(); } public static int toasts; public static boolean failToast; - public static void showToastShort(String text) { + public static void showSnackbarShort(String text) { if (failToast) throw new IllegalStateException("Unavailable feedback surface"); toasts++; } From 4dc345b93f4007d42c124dfb021fb9cbeb221575 Mon Sep 17 00:00:00 2001 From: RAMiK <60112744+RAMiK4@users.noreply.github.com> Date: Fri, 30 Oct 2026 11:37:43 +0400 Subject: [PATCH 173/189] fix(YouTube - Return YouTube Dislike): Feed thumbnail like count blinks, moves, and is missing after returning to the feed (#4176) --- .../patches/FeedEngagementCountsPatch.java | 32 +++++++- .../patches/utils/FeedVideoCardsPatch.java | 73 +++++++++++++++++-- 2 files changed, 96 insertions(+), 9 deletions(-) diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FeedEngagementCountsPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FeedEngagementCountsPatch.java index 20fbffd9f1..0a853ef294 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FeedEngagementCountsPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FeedEngagementCountsPatch.java @@ -147,10 +147,17 @@ public static void onCardLaidOut(ComponentHost host, @Nullable String videoId) { } CountsBadge badge = badges.get(host); if (videoId == null) { - if (badge != null) { - badge.detach(host); - badges.remove(host); + if (badge == null) { + return; + } + // While the card is laid out again, its video can be not found for a moment. + // The card still shows the same video if it was not detached, and the badge is + // kept until the card is checked again, so it does not blink. + if (badge.confirmed && badge.hasContent() && FeedVideoCardsPatch.isRetryPending(host)) { + return; } + badge.detach(host); + badges.remove(host); return; } if (badge == null) { @@ -193,6 +200,16 @@ private static final class CountsBadge extends Drawable implements View.OnAttach * are not shown on the next one. */ private boolean confirmed = true; + /** + * Where the badge was placed from the duration badge of the video. The duration badge + * is not always found, such as when the host of the whole thumbnail draws it, and the + * place found from the thumbnail can differ, so the badge would move. + */ + private int durationEdge; + private int durationBottom; + private int durationThumbnailTop; + /** Width of the card when the place was found, or -1 if it is not known. */ + private int durationHostWidth = -1; CountsBadge(ComponentHost host) { hostRef = new WeakReference<>(host); @@ -226,6 +243,7 @@ void bind(ComponentHost host, String newVideoId) { ratioText = null; likeFraction = -1; fetching = false; + durationHostWidth = -1; cancelPendingFetch(); endFadeIn(); } @@ -417,6 +435,14 @@ void place(ComponentHost host) { thumbnailTop = duration.bottom + px(RIGHT_MARGIN_DP) - Math.round(thumbnailWidth * FeedVideoCardsPatch.THUMBNAIL_RATIO); } + durationEdge = edge; + durationBottom = bottom; + durationThumbnailTop = thumbnailTop; + durationHostWidth = host.getWidth(); + } else if (durationHostWidth == host.getWidth()) { + edge = durationEdge; + bottom = durationBottom; + thumbnailTop = durationThumbnailTop; } else if (thumbnail != null) { edge = rtl ? thumbnail.left + px(RIGHT_MARGIN_DP) : thumbnail.right - px(RIGHT_MARGIN_DP); bottom = thumbnail.bottom - px(ABOVE_DURATION_DP); diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/FeedVideoCardsPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/FeedVideoCardsPatch.java index ba338a8cd1..fa86dfe300 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/FeedVideoCardsPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/FeedVideoCardsPatch.java @@ -146,6 +146,18 @@ public final class FeedVideoCardsPatch { private static final Map textVideoIds = Utils .createSizeRestrictedMap(MAX_TEXTS, true); + /** Titles of the cards whose video was found that are kept. */ + private static final int MAX_FOUND_TITLES = 1000; + + /** + * Title of a card whose video was found -> its video id, or {@link #AMBIGUOUS}. + * While other pages are browsed, the texts of their elements replace the texts of the feed, + * and the feed can be shown again without its elements being parsed again. + * Used only on the main thread. + */ + private static final Map foundTitleVideoIds = Utils + .createSizeRestrictedMap(MAX_FOUND_TITLES, true); + /** * Icon of the badge of a live video on its thumbnail, such as 'yt_outline_experimental_live_black_24'. * The live badge text depends on the language, and the avatar of a channel that is live @@ -390,12 +402,19 @@ public static void addVideoText(String videoId, @Nullable String text) { return; } synchronized (textVideoIds) { - String previous = textVideoIds.get(normalized); - if (previous == null) { - textVideoIds.put(normalized, videoId); - } else if (!previous.equals(videoId)) { - textVideoIds.put(normalized, AMBIGUOUS); - } + putVideoText(textVideoIds, normalized, videoId); + } + } + + /** + * Adds the text of the video, or marks it as {@link #AMBIGUOUS} if another video shows it too. + */ + private static void putVideoText(Map textVideoIds, String text, String videoId) { + String previous = textVideoIds.get(text); + if (previous == null) { + textVideoIds.put(text, videoId); + } else if (!previous.equals(videoId)) { + textVideoIds.put(text, AMBIGUOUS); } } @@ -713,6 +732,36 @@ public static String findVideoId(ComponentHost root) { } // The texts of a mix or a playlist, which are never shared with a video. // A single short text can belong to an element of another video that was not seen. + String videoId = findBestMatch(scores); + if (videoId != null) { + for (String text : texts) { + if (text.length() >= TITLE_TEXT_LENGTH) { + putVideoText(foundTitleVideoIds, text, videoId); + } + } + return videoId; + } + return scores.isEmpty() ? findVideoIdByFoundTitles(texts) : null; + } + + /** + * The texts of the element of the card were removed, so the card is found by the titles + * of the cards that were found before. A long text of the card can be shared with another + * video of the same channel that was found, such as the channel and the view count, + * so at least two texts must match. + */ + @Nullable + private static String findVideoIdByFoundTitles(List texts) { + Map scores = new HashMap<>(); + for (String text : texts) { + if (text.length() < TITLE_TEXT_LENGTH) { + continue; + } + String videoId = foundTitleVideoIds.get(text); + if (videoId != null && !videoId.equals(AMBIGUOUS)) { + scores.merge(videoId, 1, Integer::sum); + } + } return findBestMatch(scores); } @@ -790,6 +839,8 @@ private static ComponentHost findRootHost(ComponentHost host) { /** Root host -> number of checks since its last layout. Used only on the main thread. */ private static final Map retries = new WeakHashMap<>(); + /** Cards that are checked again after a delay. Used only on the main thread. */ + private static final Map pendingRetries = new WeakHashMap<>(); /** * Root hosts that are checked again when the list scrolls. Litho mounts only the part of a card @@ -846,6 +897,14 @@ public static boolean scheduleScrollCheck(ComponentHost root) { return partlyShown; } + /** + * @return If the card is checked again later, such as while its video is not found yet. + * Must be called on the main thread. + */ + public static boolean isRetryPending(ComponentHost root) { + return pendingRetries.containsKey(root); + } + /** * Checks the card again a little later, such as when what is drawn on it was not found yet, * and when the list scrolls while the card is partly shown. @@ -862,7 +921,9 @@ public static boolean scheduleRetry(ComponentHost root) { return partlyShown; } retries.put(root, attempt + 1); + pendingRetries.put(root, Boolean.TRUE); root.postDelayed(() -> { + pendingRetries.remove(root); if (root.isAttachedToWindow()) { scheduleUpdate(root); } From 54b7371f5252dfcd287d2360e37cdacefb5ad3c2 Mon Sep 17 00:00:00 2001 From: RAMiK <60112744+RAMiK4@users.noreply.github.com> Date: Fri, 30 Oct 2026 11:37:59 +0400 Subject: [PATCH 174/189] fix(YouTube - Feed video actions): Watch later button is shown above the video of posts, and is missing after the app language is changed (#4172) --- .../patches/FeedVideoActionsPatch.java | 208 +++++++++++++++--- 1 file changed, 176 insertions(+), 32 deletions(-) diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FeedVideoActionsPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FeedVideoActionsPatch.java index 88897130d7..205333d8c4 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FeedVideoActionsPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FeedVideoActionsPatch.java @@ -18,6 +18,7 @@ import android.graphics.drawable.Drawable; import android.os.Build; import android.os.Handler; +import android.os.LocaleList; import android.os.Looper; import android.os.SystemClock; import android.os.VibrationEffect; @@ -116,11 +117,18 @@ public final class FeedVideoActionsPatch { private static final int BADGE_PRESSED_ALPHA = 0xFF; private static final int BADGE_ICON_COLOR = 0xFFFFFFFF; + /** Space between the duration badge and the bottom of the thumbnail. */ + private static final int DURATION_BOTTOM_MARGIN_DP = 8; + /** The menu button of a video is at most this far below the thumbnail. */ + private static final int MAX_MENU_BUTTON_BELOW_THUMBNAIL_DP = 64; + /** Movement of the video on the screen that cancels a long press. */ private static final int HOST_MOVE_TOLERANCE_DP = 2; /** Video texts shorter than this are not removed from the menu button description. */ private static final int MIN_VIDEO_TEXT_LENGTH = 3; + /** Separates the language from the learned pattern, which has only letters and spaces. */ + private static final char LANGUAGE_SEPARATOR = '|'; /** Length of the fixed start or end a description must share with the learned one. */ private static final int MIN_SHARED_LENGTH = 6; @@ -129,6 +137,7 @@ public final class FeedVideoActionsPatch { /** A short badge in capital letters, such as the live badge. */ private static final Pattern BADGE_PATTERN = Pattern.compile("^\\p{Lu}{2,12}$"); private static final Pattern NOT_LETTERS_PATTERN = Pattern.compile("[^\\p{L}]+"); + private static final Pattern LETTERS_PATTERN = Pattern.compile("\\p{L}{2,}"); private static final Pattern VIDEO_ID_PATTERN = Pattern.compile("^[\\w-]{11}$"); /** Time the menu click has to open the menu, before a Watch later tap is forgotten. */ @@ -200,6 +209,8 @@ private static final class VideoCard { WeakReference menuButtonRef = new WeakReference<>(null); final WatchLaterBadge badge = new WatchLaterBadge(); boolean badgeAdded; + /** Top of the thumbnail in the host, or -1 if it is not known. */ + int thumbnailTop = -1; @Nullable View getMenuButton() { @@ -209,9 +220,9 @@ View getMenuButton() { void placeBadge(ComponentHost host) { final int size = dp(host, BADGE_SIZE_DP); - final int margin = dp(host, BADGE_MARGIN_DP); + final int top = thumbnailTop + dp(host, BADGE_MARGIN_DP); final int right = host.getWidth() - dp(host, BADGE_RIGHT_MARGIN_DP); - badge.setBounds(right - size, margin, right, margin + size); + badge.setBounds(right - size, top, right, top + size); badge.hostRef = new WeakReference<>(host); if (!badgeAdded) { host.getOverlay().add(badge); @@ -219,6 +230,13 @@ void placeBadge(ComponentHost host) { } } + void removeBadge(ComponentHost host) { + if (badgeAdded) { + host.getOverlay().remove(badge); + badgeAdded = false; + } + } + boolean isBadgeHit(ComponentHost host, float x, float y) { if (!badgeAdded || FeedVideoCardsPatch.isCardPlayingInline(host) || FeedVideoCardsPatch.isPlaylistCard(host)) { @@ -360,21 +378,9 @@ private static boolean hasVideoBadge(ComponentHost root) { } private static boolean hasVideoBadge(ComponentHost root, View view, int thumbnailBottom, Rect rect) { - List texts = null; - if (view instanceof ComponentHost host) { - TextContent textContent = host.getTextContent(); - if (textContent != null) { - texts = textContent.getTextItems(); - } - } else if (view instanceof TextView textView) { - texts = List.of(textView.getText()); - } - + List texts = getTexts(view); if (texts != null && !texts.isEmpty()) { - rect.set(0, 0, view.getWidth(), view.getHeight()); - if (view != root) { - root.offsetDescendantRectToMyCoords(view, rect); - } + setRectInRoot(root, view, rect); final boolean inThumbnail = rect.bottom <= thumbnailBottom; for (CharSequence text : texts) { @@ -401,6 +407,133 @@ && hasVideoBadge(root, child, thumbnailBottom, rect)) { return false; } + /** + * @return The texts drawn by the view itself, or null if it draws none. + */ + @Nullable + private static List getTexts(View view) { + if (view instanceof ComponentHost host) { + TextContent textContent = host.getTextContent(); + return textContent == null ? null : textContent.getTextItems(); + } + if (view instanceof TextView textView) { + return List.of(textView.getText()); + } + return null; + } + + private static void setRectInRoot(ComponentHost root, View view, Rect rect) { + rect.set(0, 0, view.getWidth(), view.getHeight()); + if (view != root) { + root.offsetDescendantRectToMyCoords(view, rect); + } + } + + /** + * @return The bottom of the lowest duration badge in the host, or -1 if none is shown. + */ + private static int findDurationBottom(ComponentHost root, View view, Rect rect) { + int bottom = -1; + List texts = getTexts(view); + if (texts != null) { + for (CharSequence text : texts) { + if (text != null && DURATION_PATTERN.matcher(text.toString().trim()).matches()) { + setRectInRoot(root, view, rect); + bottom = rect.bottom; + break; + } + } + } + + if (view instanceof ViewGroup group) { + for (int i = 0, count = group.getChildCount(); i < count; i++) { + View child = group.getChildAt(i); + if (child.getVisibility() == View.VISIBLE) { + bottom = Math.max(bottom, findDurationBottom(root, child, rect)); + } + } + } + return bottom; + } + + /** + * @return If the host shows a text above the position, such as the header + * and the text of a post above its video. + */ + private static boolean hasTextAbove(ComponentHost root, View view, int y, Rect rect) { + List texts = getTexts(view); + if (texts != null) { + for (CharSequence text : texts) { + if (text != null && LETTERS_PATTERN.matcher(text).find()) { + setRectInRoot(root, view, rect); + if (rect.bottom <= y) { + return true; + } + break; + } + } + } + + if (view instanceof ViewGroup group) { + for (int i = 0, count = group.getChildCount(); i < count; i++) { + View child = group.getChildAt(i); + if (child.getVisibility() == View.VISIBLE && hasTextAbove(root, child, y, rect)) { + return true; + } + } + } + return false; + } + + /** + * The thumbnail of a video card is at the top, but the video of a post is below the + * header and the text of the post. The duration badge is at the bottom of the thumbnail. + * Thumbnails can be square, such as of music, so a thumbnail is at the top of the card + * unless a text is shown above it. + * + * @return The top of the thumbnail in the host, or -1 if it is not known. + */ + private static int findThumbnailTop(ComponentHost host, View menuButton) { + final int width = host.getWidth(); + final int thumbnailHeight = Math.round(width * THUMBNAIL_RATIO); + Rect rect = new Rect(); + final int durationBottom = findDurationBottom(host, host, rect); + if (durationBottom >= 0) { + final int top = durationBottom + dp(host, DURATION_BOTTOM_MARGIN_DP) - thumbnailHeight; + return top > 0 && hasTextAbove(host, host, top, rect) ? top : 0; + } + + // Live videos have no duration. The thumbnail is above the menu button, + // and at most as tall as it is wide. + setRectInRoot(host, menuButton, rect); + final int highestTop = rect.top - width - dp(host, MAX_MENU_BUTTON_BELOW_THUMBNAIL_DP); + return highestTop > 0 && hasTextAbove(host, host, highestTop, rect) ? -1 : 0; + } + + /** + * @return The language of the app, which the description of the menu button is in. + */ + private static String getLanguage(View view) { + LocaleList locales = view.getResources().getConfiguration().getLocales(); + return (locales.isEmpty() ? Locale.getDefault() : locales.get(0)).toLanguageTag(); + } + + /** + * The pattern is learned in the language of the app, so a pattern of another language, + * such as after the language of the app is changed, is not used until it is learned again. + * + * @return The learned pattern of the menu button description, or an empty string if it is + * not learned in the language of the app. + */ + private static String getLearnedPattern(View view) { + String learned = Settings.FEED_MENU_BUTTON_DESCRIPTION.get(); + final int separator = learned.indexOf(LANGUAGE_SEPARATOR); + if (separator < 0 || !learned.substring(0, separator).equals(getLanguage(view))) { + return ""; + } + return learned.substring(separator + 1); + } + /** * The description of the menu button contains the title of its video, such as * "Action menu: [title]", so it differs on every video. The texts of the video are @@ -483,8 +616,7 @@ private static void collectVideoTexts(ComponentHost host, List texts) { * of posts, never are. */ @Nullable - private static View findMenuButton(ComponentHost root) { - String learned = Settings.FEED_MENU_BUTTON_DESCRIPTION.get(); + private static View findMenuButton(ComponentHost root, String learned) { List videoTexts = learned.isEmpty() ? List.of() : collectVideoTexts(root); View[] bestMatch = new View[1]; if (findMenuButton(root, root, learned, videoTexts, new Rect(), bestMatch)) { @@ -527,12 +659,15 @@ && findMenuButton(root, childGroup, learned, videoTexts, rect, bestMatch)) { @Nullable private static VideoCard updateCard(ComponentHost host) { VideoCard card = cards.get(host); - View menuButton = isVideoShaped(host) ? findMenuButton(host) : null; - // Until the menu button is learned, buttons are found only by their shape, - // which posts with an image also have, so only videos with a badge are used. - if (menuButton != null && Settings.FEED_MENU_BUTTON_DESCRIPTION.get().isEmpty() - && !hasVideoBadge(host)) { - menuButton = null; + View menuButton = null; + if (isVideoShaped(host)) { + String learned = getLearnedPattern(host); + menuButton = findMenuButton(host, learned); + // Until the menu button is learned, buttons are found only by their shape, + // which posts with an image also have, so only videos with a badge are used. + if (menuButton != null && learned.isEmpty() && !hasVideoBadge(host)) { + menuButton = null; + } } if (menuButton == null) { @@ -544,10 +679,7 @@ private static VideoCard updateCard(ComponentHost host) { if (card.badgeAdded && isVideoShaped(host) && FeedVideoCardsPatch.scheduleScrollCheck(host)) { return card; } - if (card.badgeAdded) { - host.getOverlay().remove(card.badge); - card.badgeAdded = false; - } + card.removeBadge(host); cards.remove(host); } return null; @@ -557,9 +689,20 @@ private static VideoCard updateCard(ComponentHost host) { card = new VideoCard(); cards.put(host, card); } + final boolean sameMenuButton = card.menuButtonRef.get() == menuButton; card.menuButtonRef = new WeakReference<>(menuButton); if (WATCH_LATER_BUTTON) { - card.placeBadge(host); + final int thumbnailTop = findThumbnailTop(host, menuButton); + // Litho mounts only the part of a card that is shown, so the duration badge + // may not be mounted. The thumbnail of the same video does not move. + if (thumbnailTop >= 0 || !sameMenuButton) { + card.thumbnailTop = thumbnailTop; + } + if (card.thumbnailTop >= 0) { + card.placeBadge(host); + } else { + card.removeBadge(host); + } } return card; } @@ -943,10 +1086,11 @@ private static void learnMenuButtonDescription() { if (pattern.isEmpty()) { return; } - if (!pattern.equals(Settings.FEED_MENU_BUTTON_DESCRIPTION.get())) { - Logger.printDebug(() -> "Learned feed menu button pattern: " + pattern + String learned = getLanguage(root) + LANGUAGE_SEPARATOR + pattern; + if (!learned.equals(Settings.FEED_MENU_BUTTON_DESCRIPTION.get())) { + Logger.printDebug(() -> "Learned feed menu button pattern: " + learned + " (description: " + description + ")"); - Settings.FEED_MENU_BUTTON_DESCRIPTION.save(pattern); + Settings.FEED_MENU_BUTTON_DESCRIPTION.save(learned); } } From d6983da6ed69327d68ce3b188a8626ef99255d06 Mon Sep 17 00:00:00 2001 From: Anurag Rai <129479513+anuraggr@users.noreply.github.com> Date: Fri, 30 Oct 2026 13:09:34 +0530 Subject: [PATCH 175/189] feat(YouTube): Apply offline SponsorBlock segments to playlist downloads (#4180) --- .../SponsorBlockOfflineStorage.java | 39 +++++++++++++++---- .../shared/spoof/SpoofVideoStreamsPatch.java | 4 ++ .../shared/spoof/js/JavaScriptManager.java | 11 +++++- .../shared/spoof/potoken/BotGuardManager.java | 4 +- .../shared/spoof/potoken/PoTokenManager.java | 5 +++ .../spoof/requests/StreamingDataRequest.java | 21 +++++++++- .../youtube/patches/InAppDownloadsPatch.java | 28 ++++++++++--- 7 files changed, 94 insertions(+), 18 deletions(-) diff --git a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/sponsorblock/SponsorBlockOfflineStorage.java b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/sponsorblock/SponsorBlockOfflineStorage.java index f63f8a6329..9a349771e5 100644 --- a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/sponsorblock/SponsorBlockOfflineStorage.java +++ b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/sponsorblock/SponsorBlockOfflineStorage.java @@ -19,7 +19,10 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import app.morphe.extension.shared.Logger; import app.morphe.extension.shared.Utils; @@ -41,6 +44,11 @@ public final class SponsorBlockOfflineStorage { private static final String JSON_KEY_END = "end"; private static final String JSON_KEY_LOCKED = "locked"; + /** + * Video IDs currently being fetched, to prevent duplicate concurrent fetches. + */ + private static final Set fetchingVideoIds = Collections.newSetFromMap(new ConcurrentHashMap<>()); + private SponsorBlockOfflineStorage() {} private static SettingsAdapter settings() { @@ -68,15 +76,15 @@ public static boolean hasSavedSegments(String videoId) { * Fetches the segments of a video that is downloaded, and saves them. */ public static void fetchAndSaveSegments(String videoId) { - if (!isEnabled() || !Utils.isNetworkConnected()) { + if (!isEnabled() || !Utils.isNetworkConnected() || hasSavedSegments(videoId)) { + return; + } + if (!fetchingVideoIds.add(videoId)) { + Logger.printDebug(() -> "Already fetching segments for: " + videoId); return; } - // The categories to fetch are loaded when the player is created, - // which has not happened yet if the video is downloaded from the feed. - SegmentCategory.loadAllCategoriesFromSettings(); - - Utils.runOnBackgroundThread(() -> { + Runnable fetch = () -> { try { SponsorSegment[] segments = SBRequester.getSegments(videoId); if (segments.length > 0) { @@ -84,8 +92,25 @@ public static void fetchAndSaveSegments(String videoId) { } } catch (Exception ex) { Logger.printException(() -> "fetchAndSaveSegments failure", ex); + } finally { + fetchingVideoIds.remove(videoId); } - }); + }; + + if (Utils.isCurrentlyOnMainThread()) { + // The categories to fetch are loaded when the player is created, + // which has not happened yet if the video is downloaded from the feed. + SegmentCategory.loadAllCategoriesFromSettings(); + Utils.runOnBackgroundThread(fetch); + } else { + // updateEnabledCategories() asserts the main thread, and this may be + // called from the offline save pipeline thread of a playlist download. + // Chained, so the categories are loaded before the request is built. + Utils.runOnMainThread(() -> { + SegmentCategory.loadAllCategoriesFromSettings(); + Utils.runOnBackgroundThread(fetch); + }); + } } /** diff --git a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/spoof/SpoofVideoStreamsPatch.java b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/spoof/SpoofVideoStreamsPatch.java index 7817eb2749..b7c459523d 100644 --- a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/spoof/SpoofVideoStreamsPatch.java +++ b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/spoof/SpoofVideoStreamsPatch.java @@ -352,6 +352,10 @@ public static boolean useMediaSessionFeatureFlag(boolean original) { public static void fetchStreams(String url, Map requestHeaders) { if (SPOOF_VIDEO_STREAMS) { try { + if (!Utils.isNetworkConnected()) { + return; + } + Uri uri = Uri.parse(url); String path = uri.getPath(); if (path == null || !path.contains("player")) { diff --git a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/spoof/js/JavaScriptManager.java b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/spoof/js/JavaScriptManager.java index ca66b6ef15..81cf3e3362 100644 --- a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/spoof/js/JavaScriptManager.java +++ b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/spoof/js/JavaScriptManager.java @@ -152,7 +152,7 @@ private JavaScriptManager() { * instead of after its response. */ public static void warmUpInBackground() { - if (cachedPlayerDataExtractor != null || !warmUpStarted.compareAndSet(false, true)) { + if (!Utils.isNetworkConnected() || cachedPlayerDataExtractor != null || !warmUpStarted.compareAndSet(false, true)) { return; } Utils.runOnBackgroundThread(() -> { @@ -183,8 +183,10 @@ private static PlayerDataExtractor getPlayerDataExtractor() { String playerJs = getPlayerJs(); if (isNotEmpty(playerJs)) { cachedPlayerDataExtractor = new PlayerDataExtractor(playerJs, Objects.requireNonNull(cachedPlayerJsHash)); - } else { + } else if (Utils.isNetworkConnected()) { Logger.printException(() -> "playerJs not found"); + } else { + Logger.printInfo(() -> "playerJs not found (network disconnected)"); } } @@ -222,6 +224,11 @@ private static String getPlayerJsLocked() { // There is no player JavaScript save in the cache, // or if there is a player JavaScript, it has been more than 3 days since it was last saved. // Download the latest player JavaScript. + if (!Utils.isNetworkConnected()) { + Logger.printInfo(() -> "Cannot download playerJs (network disconnected)"); + return null; + } + String playerJs = downloadUrl(playerJsUrl); if (isNotEmpty(playerJs)) { cachedPlayerJs = playerJs; diff --git a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/spoof/potoken/BotGuardManager.java b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/spoof/potoken/BotGuardManager.java index dda0b6b8d2..7f45f1197b 100644 --- a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/spoof/potoken/BotGuardManager.java +++ b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/spoof/potoken/BotGuardManager.java @@ -352,7 +352,7 @@ private static String downloadUrl(@NonNull String url) { handleConnectionError("Network error", ex); } } else { - handleConnectionError("No internet connection: " + url, null); + Logger.printInfo(() -> "No internet connection: " + url); } return null; @@ -365,7 +365,7 @@ private static JSONArray fetchIntegrityToken(@Nullable String botGuardResult) { return null; } if (!Utils.isNetworkConnected()) { - handleConnectionError("No internet connection", null); + Logger.printInfo(() -> "No internet connection"); return null; } try { diff --git a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/spoof/potoken/PoTokenManager.java b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/spoof/potoken/PoTokenManager.java index 67392e7f54..62de56436b 100644 --- a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/spoof/potoken/PoTokenManager.java +++ b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/spoof/potoken/PoTokenManager.java @@ -8,6 +8,7 @@ package app.morphe.extension.shared.spoof.potoken; import app.morphe.extension.shared.Logger; +import app.morphe.extension.shared.Utils; import app.morphe.extension.shared.spoof.ClientType; import app.morphe.extension.shared.spoof.requests.VisitorIdRequester; @@ -22,6 +23,10 @@ public static void reset() { } public static PoTokenResult getAndUpdatePoTokenIfNeeded(ClientType clientType, String videoId) { + if (!Utils.isNetworkConnected()) { + return null; + } + if (poTokenResult != null && !poTokenResult.isExpired()) { return poTokenResult; } diff --git a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/spoof/requests/StreamingDataRequest.java b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/spoof/requests/StreamingDataRequest.java index 6b52423bef..6891092a6c 100644 --- a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/spoof/requests/StreamingDataRequest.java +++ b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/spoof/requests/StreamingDataRequest.java @@ -337,9 +337,17 @@ else if (clientType.supportsOAuth2 && clientType.requireLogin) { + responseCode + " response: " + connection.getResponseMessage(), null, showErrorToasts); } catch (SocketTimeoutException ex) { - handleConnectionError("Connection timeout", ex, showErrorToasts); + if (Utils.isNetworkConnected()) { + handleConnectionError("Connection timeout", ex, showErrorToasts); + } else { + Logger.printInfo(() -> "Connection timeout while network disconnected", ex); + } } catch (IOException ex) { - handleConnectionError("Network error", ex, showErrorToasts); + if (Utils.isNetworkConnected()) { + handleConnectionError("Network error", ex, showErrorToasts); + } else { + Logger.printInfo(() -> "Network error while network disconnected", ex); + } } catch (Exception ex) { Logger.printException(() -> "send failed", ex); } finally { @@ -485,6 +493,11 @@ private static boolean skipClient(ClientType client) { private static StreamData fetch(String videoId, boolean isInline, Map playerHeaders, boolean includeVideoDetails, ClientType[] clientOrder, boolean isDownload) { + if (!Utils.isNetworkConnected()) { + Logger.printDebug(() -> "Network not connected, skipping stream fetch for: " + videoId); + return null; + } + final boolean debugEnabled = BaseSettings.DEBUG.get(); final long fetchStartTime = System.currentTimeMillis(); String authorization = playerHeaders.get(AUTHORIZATION_HEADER); @@ -541,6 +554,10 @@ private static StreamData fetch(String videoId, boolean isInline, Map "isOfflinePlayerResponse: playerProto is not MessageLite: " + + (playerProto != null ? playerProto.getClass().getName() : "null")); return false; } OfflinePlayerResponse response = OfflinePlayerResponse.parseFrom(message.toByteArray()); - if (response.hasOfflineState() - && response.getOfflineState().getActionValue() == OfflineStateAction.OFFLINE_STATE_ACTION_OK_VALUE) { - Logger.printDebug(() -> "Detected playable offline video: " + videoId); - return true; + if (response.hasOfflineState()) { + int action = response.getOfflineState().getActionValue(); + if (action == OfflineStateAction.OFFLINE_STATE_ACTION_OK_VALUE) { + Logger.printDebug(() -> "Detected playable offline video: " + videoId); + return true; + } else { + Logger.printDebug(() -> "Offline video state action not OK: " + action + " for: " + videoId); + } + } else { + Logger.printDebug(() -> "OfflinePlayerResponse has no offline state for: " + videoId); } } catch (Exception ex) { Logger.printException(() -> "isOfflinePlayerResponse failure", ex); @@ -284,13 +293,20 @@ public static byte[] replaceDeclinedOfflinePlayerResponse(@Nullable Object playe if (!original.hasOfflineState()) { return null; } + + String videoId = original.hasVideoDetails() + ? original.getVideoDetails().getVideoId() + : ""; + final boolean allowed = original.getOfflineState().getActionValue() == OfflineStateAction.OFFLINE_STATE_ACTION_OK_VALUE; if (allowed && original.getStreamingData().getAdaptiveFormatsCount() > 0) { + if (!videoId.isEmpty()) { + SponsorBlockOfflineStorage.fetchAndSaveSegments(videoId); + } return null; } - String videoId = original.getVideoDetails().getVideoId(); if (videoId.isEmpty()) { Logger.printDebug(() -> "Declined offline player response has no video id"); return null; @@ -318,6 +334,8 @@ public static byte[] replaceDeclinedOfflinePlayerResponse(@Nullable Object playe builder.setPlayerConfig(playerConfig.toBuilder().setExoPlayerConfig( playerConfig.getExoPlayerConfig().toBuilder().setUseExoPlayer(true))); + SponsorBlockOfflineStorage.fetchAndSaveSegments(videoId); + Logger.printDebug(() -> "Replaced declined offline player response of: " + videoId + " savable formats: " + countSavableFormats(builder.getStreamingData()) + " of: " + builder.getStreamingData().getAdaptiveFormatsCount()); From 86fb1b0e30868e1617f6b9c0bc688e6b29d5454c Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Fri, 30 Oct 2026 08:00:44 +0000 Subject: [PATCH 176/189] chore: Update translations from Crowdin --- .../values-af-rZA/shared-youtube/strings.xml | 4 ++++ .../values-af-rZA/sponsorblock/strings.xml | 2 ++ .../values-af-rZA/youtube/strings.xml | 13 +++++++++++++ .../values-am-rET/shared-youtube/strings.xml | 5 +++++ .../values-am-rET/sponsorblock/strings.xml | 2 ++ .../values-am-rET/youtube/strings.xml | 16 ++++++++++++++++ .../values-ar-rSA/shared-youtube/strings.xml | 2 ++ .../values-ar-rSA/youtube/strings.xml | 11 +++++++++++ .../values-as-rIN/shared-youtube/strings.xml | 5 +++++ .../values-as-rIN/sponsorblock/strings.xml | 2 ++ .../values-as-rIN/youtube/strings.xml | 15 +++++++++++++++ .../values-az-rAZ/youtube/strings.xml | 11 +++++++++++ .../values-be-rBY/shared-youtube/strings.xml | 4 ++++ .../values-be-rBY/sponsorblock/strings.xml | 2 ++ .../values-be-rBY/youtube/strings.xml | 14 ++++++++++++++ .../values-bg-rBG/shared-youtube/strings.xml | 5 +++++ .../values-bg-rBG/sponsorblock/strings.xml | 2 ++ .../values-bg-rBG/youtube/strings.xml | 15 +++++++++++++++ .../values-bn-rBD/shared-youtube/strings.xml | 5 +++++ .../values-bn-rBD/sponsorblock/strings.xml | 2 ++ .../values-bn-rBD/youtube/strings.xml | 15 +++++++++++++++ .../values-bs-rBA/shared-youtube/strings.xml | 5 +++++ .../values-bs-rBA/sponsorblock/strings.xml | 5 +++++ .../values-bs-rBA/youtube/strings.xml | 16 ++++++++++++++++ .../values-ca-rES/shared-youtube/strings.xml | 4 ++++ .../values-ca-rES/sponsorblock/strings.xml | 2 ++ .../values-ca-rES/youtube/strings.xml | 16 ++++++++++++++++ .../values-ckb-rIR/youtube/strings.xml | 1 + .../values-cs-rCZ/shared-youtube/strings.xml | 5 +++++ .../values-cs-rCZ/sponsorblock/strings.xml | 2 ++ .../values-cs-rCZ/youtube/strings.xml | 13 +++++++++++++ .../values-da-rDK/shared-youtube/strings.xml | 5 +++++ .../values-da-rDK/sponsorblock/strings.xml | 2 ++ .../values-da-rDK/youtube/strings.xml | 13 +++++++++++++ .../values-de-rDE/shared-youtube/strings.xml | 5 +++++ .../values-de-rDE/sponsorblock/strings.xml | 2 ++ .../values-de-rDE/youtube/strings.xml | 13 +++++++++++++ .../values-el-rGR/shared-youtube/strings.xml | 4 ++++ .../values-el-rGR/sponsorblock/strings.xml | 2 ++ .../values-el-rGR/youtube/strings.xml | 13 +++++++++++++ .../values-es-rES/shared-youtube/strings.xml | 2 ++ .../values-es-rES/youtube/strings.xml | 17 ++++++++++++++++- .../values-et-rEE/shared-youtube/strings.xml | 5 +++++ .../values-et-rEE/sponsorblock/strings.xml | 2 ++ .../values-et-rEE/youtube/strings.xml | 17 +++++++++++++++++ .../values-eu-rES/shared-youtube/strings.xml | 5 +++++ .../values-eu-rES/sponsorblock/strings.xml | 3 +++ .../values-eu-rES/youtube/strings.xml | 16 +++++++++++++++- .../values-fa-rIR/shared-youtube/strings.xml | 4 ++++ .../values-fa-rIR/sponsorblock/strings.xml | 2 ++ .../values-fa-rIR/youtube/strings.xml | 15 +++++++++++++++ .../values-fi-rFI/shared-youtube/strings.xml | 5 +++++ .../values-fi-rFI/sponsorblock/strings.xml | 2 ++ .../values-fi-rFI/youtube/strings.xml | 13 +++++++++++++ .../values-fil-rPH/shared-youtube/strings.xml | 4 ++++ .../values-fil-rPH/sponsorblock/strings.xml | 2 ++ .../values-fil-rPH/youtube/strings.xml | 15 +++++++++++++++ .../values-fr-rFR/shared-youtube/strings.xml | 4 +++- .../values-fr-rFR/youtube/strings.xml | 11 +++++++++++ .../values-ga-rIE/shared-youtube/strings.xml | 2 ++ .../values-ga-rIE/sponsorblock/strings.xml | 2 ++ .../values-ga-rIE/youtube/strings.xml | 11 +++++++++++ .../values-gl-rES/shared-youtube/strings.xml | 4 ++++ .../values-gl-rES/sponsorblock/strings.xml | 2 ++ .../values-gl-rES/youtube/strings.xml | 15 +++++++++++++++ .../values-gu-rIN/youtube/strings.xml | 1 + .../values-hi-rIN/shared-youtube/strings.xml | 4 ++++ .../values-hi-rIN/sponsorblock/strings.xml | 2 ++ .../values-hi-rIN/youtube/strings.xml | 15 +++++++++++++++ .../values-hr-rHR/shared-youtube/strings.xml | 4 ++++ .../values-hr-rHR/sponsorblock/strings.xml | 2 ++ .../values-hr-rHR/youtube/strings.xml | 18 +++++++++++++++++- .../values-hu-rHU/shared-youtube/strings.xml | 4 ++++ .../values-hu-rHU/sponsorblock/strings.xml | 2 ++ .../values-hu-rHU/youtube/strings.xml | 13 +++++++++++++ .../values-hy-rAM/shared-youtube/strings.xml | 9 +++++++++ .../values-hy-rAM/sponsorblock/strings.xml | 2 ++ .../values-hy-rAM/youtube/strings.xml | 15 +++++++++++++++ .../values-in-rID/shared-youtube/strings.xml | 4 ++++ .../values-in-rID/sponsorblock/strings.xml | 2 ++ .../values-in-rID/youtube/strings.xml | 15 +++++++++++++++ .../values-is-rIS/shared-youtube/strings.xml | 5 +++++ .../values-is-rIS/sponsorblock/strings.xml | 2 ++ .../values-is-rIS/youtube/strings.xml | 15 +++++++++++++++ .../values-it-rIT/shared-youtube/strings.xml | 2 ++ .../values-it-rIT/youtube/strings.xml | 11 +++++++++++ .../values-iw-rIL/shared-youtube/strings.xml | 4 ++++ .../values-iw-rIL/sponsorblock/strings.xml | 2 ++ .../values-iw-rIL/youtube/strings.xml | 15 ++++++++++++++- .../values-ja-rJP/shared-youtube/strings.xml | 4 ++++ .../values-ja-rJP/sponsorblock/strings.xml | 2 ++ .../values-ja-rJP/youtube/strings.xml | 13 +++++++++++++ .../values-ka-rGE/shared-youtube/strings.xml | 5 +++++ .../values-ka-rGE/sponsorblock/strings.xml | 2 ++ .../values-ka-rGE/youtube/strings.xml | 14 ++++++++++++++ .../values-kk-rKZ/youtube/strings.xml | 1 + .../values-km-rKH/youtube/strings.xml | 1 + .../values-kmr-rTR/youtube/strings.xml | 1 + .../values-kn-rIN/youtube/strings.xml | 1 + .../values-ko-rKR/youtube/strings.xml | 15 ++++++++++++++- .../values-ky-rKG/youtube/strings.xml | 1 + .../values-lo-rLA/youtube/strings.xml | 1 + .../values-lt-rLT/shared-youtube/strings.xml | 4 ++++ .../values-lt-rLT/sponsorblock/strings.xml | 2 ++ .../values-lt-rLT/youtube/strings.xml | 13 +++++++++++++ .../values-lv-rLV/youtube/strings.xml | 1 + .../values-mai-rIN/youtube/strings.xml | 1 + .../values-mk-rMK/youtube/strings.xml | 1 + .../values-mn-rMN/youtube/strings.xml | 1 + .../values-mr-rIN/youtube/strings.xml | 1 + .../values-ms-rMY/youtube/strings.xml | 1 + .../values-my-rMM/youtube/strings.xml | 1 + .../values-nb-rNO/shared-youtube/strings.xml | 5 +++++ .../values-nb-rNO/sponsorblock/strings.xml | 2 ++ .../values-nb-rNO/youtube/strings.xml | 15 +++++++++++++++ .../values-ne-rIN/youtube/strings.xml | 1 + .../values-nl-rNL/shared-youtube/strings.xml | 4 ++++ .../values-nl-rNL/sponsorblock/strings.xml | 2 ++ .../values-nl-rNL/youtube/strings.xml | 13 +++++++++++++ .../values-or-rIN/youtube/strings.xml | 1 + .../values-pa-rIN/youtube/strings.xml | 1 + .../values-pl-rPL/shared-youtube/strings.xml | 2 ++ .../values-pl-rPL/sponsorblock/strings.xml | 2 ++ .../values-pl-rPL/youtube/strings.xml | 13 +++++++++++++ .../values-pt-rBR/shared-youtube/strings.xml | 4 ++++ .../values-pt-rBR/sponsorblock/strings.xml | 2 ++ .../values-pt-rBR/youtube/strings.xml | 15 +++++++++++++++ .../values-pt-rPT/shared-youtube/strings.xml | 5 +++++ .../values-pt-rPT/sponsorblock/strings.xml | 2 ++ .../values-pt-rPT/youtube/strings.xml | 15 +++++++++++++++ .../values-ro-rRO/shared-youtube/strings.xml | 4 ++++ .../values-ro-rRO/sponsorblock/strings.xml | 2 ++ .../values-ro-rRO/youtube/strings.xml | 15 +++++++++++++++ .../values-ru-rRU/shared-youtube/strings.xml | 2 ++ .../values-ru-rRU/sponsorblock/strings.xml | 2 ++ .../values-ru-rRU/youtube/strings.xml | 12 ++++++++++++ .../values-si-rLK/youtube/strings.xml | 1 + .../values-sk-rSK/shared-youtube/strings.xml | 4 ++++ .../values-sk-rSK/sponsorblock/strings.xml | 2 ++ .../values-sk-rSK/youtube/strings.xml | 15 +++++++++++++++ .../values-sl-rSI/shared-youtube/strings.xml | 5 +++++ .../values-sl-rSI/sponsorblock/strings.xml | 2 ++ .../values-sl-rSI/youtube/strings.xml | 15 +++++++++++++++ .../values-sr-rCS/shared-youtube/strings.xml | 4 ++++ .../values-sr-rCS/sponsorblock/strings.xml | 2 ++ .../values-sr-rCS/youtube/strings.xml | 15 +++++++++++++++ .../values-sr-rSP/shared-youtube/strings.xml | 5 +++++ .../values-sr-rSP/sponsorblock/strings.xml | 2 ++ .../values-sr-rSP/youtube/strings.xml | 15 +++++++++++++++ .../values-sv-rSE/shared-youtube/strings.xml | 5 +++++ .../values-sv-rSE/sponsorblock/strings.xml | 2 ++ .../values-sv-rSE/youtube/strings.xml | 15 +++++++++++++++ .../values-sw-rKE/youtube/strings.xml | 1 + .../values-ta-rIN/youtube/strings.xml | 1 + .../values-te-rIN/youtube/strings.xml | 1 + .../values-th-rTH/shared-youtube/strings.xml | 4 ++++ .../values-th-rTH/sponsorblock/strings.xml | 2 ++ .../values-th-rTH/youtube/strings.xml | 15 +++++++++++++++ .../values-tr-rTR/youtube/strings.xml | 11 +++++++++++ .../values-uk-rUA/shared-youtube/strings.xml | 2 ++ .../values-uk-rUA/sponsorblock/strings.xml | 2 ++ .../values-uk-rUA/youtube/strings.xml | 15 +++++++++++++++ .../values-ur-rIN/youtube/strings.xml | 1 + .../values-uz-rUZ/youtube/strings.xml | 1 + .../values-vi-rVN/shared-youtube/strings.xml | 4 ++++ .../values-vi-rVN/sponsorblock/strings.xml | 2 ++ .../values-vi-rVN/youtube/strings.xml | 15 +++++++++++++++ .../values-zh-rCN/shared-youtube/strings.xml | 5 +++++ .../values-zh-rCN/sponsorblock/strings.xml | 2 ++ .../values-zh-rCN/youtube/strings.xml | 15 +++++++++++++++ .../values-zh-rTW/shared-youtube/strings.xml | 4 ++++ .../values-zh-rTW/sponsorblock/strings.xml | 2 ++ .../values-zh-rTW/youtube/strings.xml | 15 +++++++++++++++ .../values-zu-rZA/shared-youtube/strings.xml | 5 +++++ .../values-zu-rZA/sponsorblock/strings.xml | 2 ++ .../values-zu-rZA/youtube/strings.xml | 18 +++++++++++++++++- 176 files changed, 1103 insertions(+), 7 deletions(-) diff --git a/patches/src/main/resources/addresources/values-af-rZA/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-af-rZA/shared-youtube/strings.xml index 62a06c4402..0028ca7424 100644 --- a/patches/src/main/resources/addresources/values-af-rZA/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-af-rZA/shared-youtube/strings.xml @@ -21,8 +21,12 @@ Second \"item\" text" Opstart-animasie Deaktiveer Kleur + Morphe Kleur Monochroom + Morphe Monochroom + Liggooi nuwe instellings + Aantal dae wat \'n kolletjie langs instellings en opsies getoon word wat deur \'n opdatering bygevoeg is Af Herbegin om die opstelling te voltooi diff --git a/patches/src/main/resources/addresources/values-af-rZA/sponsorblock/strings.xml b/patches/src/main/resources/addresources/values-af-rZA/sponsorblock/strings.xml index 35d6e01837..6445c7cdb9 100644 --- a/patches/src/main/resources/addresources/values-af-rZA/sponsorblock/strings.xml +++ b/patches/src/main/resources/addresources/values-af-rZA/sponsorblock/strings.xml @@ -58,6 +58,8 @@ Second \"item\" text" Reeds gelees Wys my Algemeen + Stoor segmente van afgelaaide video\'s + Laat dit toe om segmente van afgelaaide video\'s oor te slaan terwyl jy vanlyn is Wys konneksiefoutwaarskuwings Wys \'n kennisgewing wanneer die SponsorBlock-bediener nie beskikbaar is nie Aktiveer oorslaan telling dop diff --git a/patches/src/main/resources/addresources/values-af-rZA/youtube/strings.xml b/patches/src/main/resources/addresources/values-af-rZA/youtube/strings.xml index f738319c38..30c0ad7f3f 100644 --- a/patches/src/main/resources/addresources/values-af-rZA/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-af-rZA/youtube/strings.xml @@ -455,6 +455,15 @@ Verslae is openbaar en word nagegaan voordat 'n kanaal by 'n lys gevoeg word. Ve Beeld gestoor in %s Kon nie die beeld aflaai nie Geen beeld in hierdie plasing gevind nie + + Vertaal plasings + Voeg \'n uitvou-spyskaartitem by om inhoudskeppers se plasings te vertaal + Plasingvertalings taal + Vertaal + Wys oorspronklike + Besig om te vertaal… + Kon nie die plasing vertaal nie + Die plasing is reeds in die vertalings taal Voeg Ask-item by vlieëmenu Voeg \'n video-opvattingsitem by die voervaartuig-vlieëmenu @@ -863,6 +872,8 @@ Om dit reg te stel, skakel 'Vervals videostrome' aan en verander die 'Verstekkli Regstel transkripsie Resolves a precondition check failure that can prevent the transcript from loading Wys onderskrifte in prent-in-prent + Stabiele byskrifblokke + Groepeer outomatiese onderskrifte in stabiele, leesbare blokke. Sommige woorde mag verskyn voordat dit gespreek word. Herbegin word vereis. Navigasieknoppies Choose which components appear in the toolbar @@ -1211,6 +1222,8 @@ Beperking: Indien 'Herstel ou video-aksiebalk' afgeskakel is, mag die 'Meer vide Deaktiveer die \'Gaan voort om te kyk\' minispeler, \'n YouTube Premium-funksie wat op app-opstart verskyn Mini-speler Pas die voorkoms en gedrag van die verkleinde videospeler aan + Addisionele mini-spelers + YouTube het alle addisionele mini-spelers verwyder. Om meer mini-speler-tipes te herstel, patch YouTube 21.28.208 of ouer Mini-speler tipe Versteek titel en kanaal Gedeaktiveer diff --git a/patches/src/main/resources/addresources/values-am-rET/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-am-rET/shared-youtube/strings.xml index 40447a5b9b..e8b0ee704e 100644 --- a/patches/src/main/resources/addresources/values-am-rET/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-am-rET/shared-youtube/strings.xml @@ -21,8 +21,13 @@ Second \"item\" text" የመጀመሪያ ማስጀመሪያ አኒሜሽን ተዘግቷል ቀለም + Morphe ቀለም ሞኖክሮም + Morphe ሞኖክሮም + አዳዲስ ቅንብሮችን አድምቆ አሳይ + በማሻሻያ የተጨመሩ ቅንብሮች እና አማራጮች አጠገብ ነጥብ የሚታይባቸው የቀናት ብዛት + ጠፍ ቅንብሩን ለማጠናቀቅ እንደገና ያስነሱ diff --git a/patches/src/main/resources/addresources/values-am-rET/sponsorblock/strings.xml b/patches/src/main/resources/addresources/values-am-rET/sponsorblock/strings.xml index 7badcf605c..70da4a4891 100644 --- a/patches/src/main/resources/addresources/values-am-rET/sponsorblock/strings.xml +++ b/patches/src/main/resources/addresources/values-am-rET/sponsorblock/strings.xml @@ -58,6 +58,8 @@ Second \"item\" text" አነበብኩት አሳየኝ ጠቅላላ + የተጫኑ ቪዲዮዎችን ክፍሎች ያስቀምጡ + በኢንተርኔት ግንኙነት இல்லாதበት ጊዜ የተጫኑ ቪዲዮዎችን ክፍሎች ለመዝለል ያስችላል የግንኙነት ስህተት ማስጠንቀቂያዎችን አሳይ የSponsorBlock ሰርቨር በማይገኝበት ጊዜ ማሳወቂያ ያሳያል የማለፍ ብዛት ክትትልን አግብር diff --git a/patches/src/main/resources/addresources/values-am-rET/youtube/strings.xml b/patches/src/main/resources/addresources/values-am-rET/youtube/strings.xml index 97a1dedb2e..d1c8fbd2cc 100644 --- a/patches/src/main/resources/addresources/values-am-rET/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-am-rET/youtube/strings.xml @@ -455,6 +455,15 @@ Second \"item\" text" ምስሉ በ %s ተቀምጧል ምስሉን ማውረድ አልተቻለም በዚህ ፖስት ውስጥ ምንም ምስል አልተገኘም + + ፖስቶችን ተርጉም + የይዘት ፈጣሪዎችን ጽሑፎች ለመተርጎም የፍላይአውት ሜኑ आइटम ይጨምራል + የጽሑፍ ትርጉም ቋንቋ + ተርጉም + ዋናውን አሳይ + በመተርጎም ላይ… + ጽሑፉን መተርጎም አልተቻለም + ጽሑፉ ቀድሞውኑ በትረጉም ቋንቋው ውስጥ ነው \'ጠይቅ\' የሚለውን ንጥል ወደ ዝርዝር ሜኑ አክል የቪዲዮ ማጠቃለያ ንጥልን ወደ ፊድ ዝርዝር ሜኑ ይጨምራል @@ -863,6 +872,8 @@ OpenRouter.aiን ለመክፈት እዚህ ይጫኑ" የንግግር ጽሁፉን አስተካክል የጽሑፍ ትርጉም እንዳይጫን ሊያደርግ የሚችልን ቅድመ ሁኔታ የፍተሻ ስህተት ይፈታዋል በምስል ውስጥ ምስል ውስጥ የግርጌ ጽሁፎችን አሳይ + የተረጋጋ የርዕስ ብሎኮች + ራስ-ሰር የርዕስ ገለጻዎችን ወደ ተረጋጉ እና ሊነበቡ ወደሚችሉ ብሎኮች ያደራጃል። አንዳንድ ቃላት ከመነገራቸው በፊት ሊታዩ ይችላሉ። እንደገና ማስጀመር ያስፈልጋል። የናቪጌሽን ቁልፎች በታችኛው የናቪጌሽን ባር ላይ የትኞቹ ቁልፎች እንደሚታዩ ይምረጡ @@ -1211,6 +1222,8 @@ OpenRouter.aiን ለመክፈት እዚህ ይጫኑ" በመተግበሪያው መጀመሪያ ላይ የሚታየውን የYouTube Premium ባህሪ የሆነውን \'መመልከት ይቀጥሉ\' የሚለውን ሚኒ-ፕሌየር ያጠፋል ትንሽ ተጫዋች የተቀነሰውን የቪዲዮ ማጫወቻ ገጽታ እና ባህሪ ያስተካክሉ + ተጨማሪ ሚኒ-ፕሌየሮች + YouTube ሁሉንም ተጨማሪ ሚኒ-ፕሌየሮች አስወግዷል። ተጨማሪ የሚኒ-ፕሌየር ዓይነቶችን ለመመለስ፣ YouTube 21.28.208 ወይም ከዚያ በበለጠ የድሮ ስሪትን ፓች ያድርጉ የትንሽ ተጫዋች ዓይነት ርዕስን እና ቻናሉን ደብቅ ተዘግቷል @@ -1252,6 +1265,8 @@ OpenRouter.aiን ለመክፈት እዚህ ይጫኑ" የሁኔታ ማሳወቂያ ስትሪክ አቀማመጥ ወጥ እንዲሆን በማድረግ የላይኞቹን ባሮች እንዳይሸፍን እና ከቪዲዮ ማጫወቻው በላይ ምንም ባዶ ቦታ እንዳይኖር ያደርጋል። መተግበሪያው ካቆመ ወይም የላይኞቹ ባሮች የተሳሳተ አቀማመጥ ካሳዩ ይህንን ያጥፉት ብልጭታ + Morphe Glow 1 + Morphe Glow 2 ይህ መሣሪያ የGlow ስፕላሽ ስክሪንን አይደግፍም የቀላል ጭብጥ ዳራ ነጭ @@ -1570,6 +1585,7 @@ AVC (H.264) ክፍሎች እንደተመለከቱ ተሰምረዋል ቀይር አግብ + 1 አዲስ ክፍል %1$s አዳዲስ ክፍሎች በዚህ መሳሪያ ላይ ሂደትን መዝግብ "የተከታተሏቸው ተከታታይ ክፍሎች እና የደረሱበት ደረጃ በዚህ መሳሪያ ላይ ይቀመራሉ፣ እንዲሁም በYouTube አካውንቶች መካከል ይጋራሉ። የYouTube ታሪክ ቁጥጥሮች እነዚህን አይለውጡም። የደረሱበትን ደረጃ ከተከታታይ ክፍሎች ማውጫ ውስጥ ያጽዱ diff --git a/patches/src/main/resources/addresources/values-ar-rSA/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-ar-rSA/shared-youtube/strings.xml index deea54e52d..6a4365eece 100644 --- a/patches/src/main/resources/addresources/values-ar-rSA/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ar-rSA/shared-youtube/strings.xml @@ -25,6 +25,8 @@ Second \"item\" text" أحادي اللون Morphe أحادي اللون + تمييز الإعدادات الجديدة + عدد الأيام التي تظهر فيها النقطة بجانب الإعدادات والخيارات التي أُضيفت بواسطة التحديث متوقف إعادة التشغيل لإنهاء الإعداد diff --git a/patches/src/main/resources/addresources/values-ar-rSA/youtube/strings.xml b/patches/src/main/resources/addresources/values-ar-rSA/youtube/strings.xml index be31ec16de..af86b8693c 100644 --- a/patches/src/main/resources/addresources/values-ar-rSA/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ar-rSA/youtube/strings.xml @@ -455,6 +455,15 @@ Second \"item\" text" تم حفظ الصورة في %s تعذر تنزيل الصورة لم يتم العثور على صورة في هذا المنشور + + ترجمة المنشورات + يُضيف عنصر بالقائمة المنبثقة لترجمة منشورات صانعي المحتوى + لغة ترجمة المنشور + ترجمة + عرض الأصل + جارٍ الترجمة... + تعذّر ترجمة المنشور + المنشور موجود بالفعل باللغة المحددة للترجمة إضافة عنصر اسأل إلى القائمة المنبثقة إضافة عنصر تلخيص الفيديو إلى قائمة الخلاصة المنبثقة @@ -867,6 +876,8 @@ Mistral Nemo هو الخيار الأرخص، بينما توفر Gemma 4 أعل إصلاح النصوص يحل مشكلة فشل التحقق من شرط مسبق قد يمنع تحميل النص عرض الترجمة في وضع صورة داخل صورة + تنسيق الترجمة الآلية + تجميع الترجمة الآلية في كتل ثابتة وواضحة للقراءة. قد تظهر بعض الكلمات قبل نطقها. يلزم إعادة تشغيل التطبيق. أزرار التنقل اختر الأزرار التي تظهر في شريط التنقل السفلي diff --git a/patches/src/main/resources/addresources/values-as-rIN/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-as-rIN/shared-youtube/strings.xml index 0febfa4b77..5d1c8ab75b 100644 --- a/patches/src/main/resources/addresources/values-as-rIN/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-as-rIN/shared-youtube/strings.xml @@ -21,8 +21,13 @@ Second \"item\" text" ষ্টাৰ্টআপ এনিমেশ্বন নিষ্ক্ৰীয় ৰং + Morphe ৰং মোন’ক্ৰোম + Morphe মনোক্ৰোম + নতুন ছেটিংছ হাইলাইট কৰক + আপডেটৰ দ্বাৰা যোগ কৰা ছেটিংছ আৰু বিকল্পসমূহৰ কাষত কেইদিনলৈ এটা ডট দেখুৱাব + বন্ধ ছেটিং আপ শেষ কৰিবলৈ পুনৰ আৰম্ভ কৰক diff --git a/patches/src/main/resources/addresources/values-as-rIN/sponsorblock/strings.xml b/patches/src/main/resources/addresources/values-as-rIN/sponsorblock/strings.xml index 59110fdf6d..da4e5a0526 100644 --- a/patches/src/main/resources/addresources/values-as-rIN/sponsorblock/strings.xml +++ b/patches/src/main/resources/addresources/values-as-rIN/sponsorblock/strings.xml @@ -58,6 +58,8 @@ Second \"item\" text" আগতেই পঢ়া হৈছে মোক দেখুৱাওক সাধাৰণ + ডাউনলোড কৰা ভিডিঅ\'ৰ খণ্ডসমূহ সংৰক্ষণ কৰক + অফলাইনত থকাৰ সময়ত ডাউনলোড কৰা ভিডিঅ\'ৰ খণ্ডসমূহ স্কিপ কৰিবলৈ অনুমতি দিয়ে সংযোগৰ ত্ৰুটিৰ সতৰ্কবাৰ্তাসমূহ দেখুৱাওক SponsorBlock চাৰ্ভাৰটো উপলব্ধ নোহোৱাৰ সময়ত এটা জাননী দেখুৱায় স্কিপ কাউণ্ট ট্ৰেকিং সক্ষম কৰক diff --git a/patches/src/main/resources/addresources/values-as-rIN/youtube/strings.xml b/patches/src/main/resources/addresources/values-as-rIN/youtube/strings.xml index 1feedab53e..bd1b0a6137 100644 --- a/patches/src/main/resources/addresources/values-as-rIN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-as-rIN/youtube/strings.xml @@ -455,6 +455,15 @@ Second \"item\" text" ছবিখন %s ত সংৰক্ষণ কৰা হৈছে ছবিখন ডাউনলোড কৰিব নোৱাৰিল এই পোষ্টটোত কোনো ছবি পোৱা নগ\'ল + + পোষ্টসমূহ অনুবাদ কৰক + কন্টেন্ট ক্ৰিয়েটৰৰ পোষ্টসমূহ অনুবাদ কৰিবলৈ এটা ফ্লাইআউট মেনু আইটেম যোগ কৰে + পোষ্ট অনুবাদৰ ভাষা + অনুবাদ কৰক + মূলটো দেখুওক + অনুবাদ কৰা হৈছে… + পোষ্টটো অনুবাদ কৰিব নোৱাৰিল + পোষ্টটো ইতিমধ্যে অনুবাদৰ ভাষাতে আছে ফ্লাইআউট মেনুলৈ Ask আইটেম যোগ কৰক ফিড ফ্লাইআউট মেনুলৈ এটা ভিডিঅ\' সামৰিপণ আইটেম যোগ কৰে @@ -863,6 +872,8 @@ OpenRouter.ai খুলিবলৈ ইয়াত টিপক" ট্ৰান্সক্ৰিপ্ট ঠিক কৰক এটা প্ৰাক-চৰ্ত পৰীক্ষাৰ বিফলতা সমাধান কৰে যাৰ বাবে ট্ৰান্সক্ৰিপ্ট লোড হোৱাত বাধা আহিব পাৰে পিকচাৰ-ইন-পিকচাৰত কেপচন দেখুৱাওক + ষ্টেবল কেপচন ব্লক + স্বয়ংক্ৰীয় কেপচনসমূহক সুস্থিৰ, পঢ়িব পৰা ব্লকত গ্ৰুপ কৰক। কিছুমান শব্দ কোৱাৰ আগতেই দেখা যাব পাৰে। পুনৰ আৰম্ভ কৰা প্ৰয়োজনীয়। নেভিগেশ্বন বুটামসমূহ তলৰ নেভিগেছন বাৰত কোনবোৰ বুটাম দেখুৱাব সেইটো বাছনি কৰক @@ -1211,6 +1222,8 @@ Settings → Playback (বা Autoplay) → Autoplay next video" \'Continue watching\' মিনিপ্লেয়াৰটো নিষ্ক্ৰীয় কৰে, যিটো এটা YouTube Premium বৈশিষ্ট্য আৰু এপটো আৰম্ভ হোৱাৰ সময়ত দেখা যায় মিনিপ্লেয়াৰ মিনিমাইজ কৰা ভিডিঅ\' প্লেয়াৰৰ ৰূপ আৰু আচৰণ পছন্দ অনুযায়ী সজাই লওক + অতিৰিক্ত মিনিপ্লেয়াৰসমূহ + YouTube-এ সকলো অতিৰিক্ত মিনিপ্লেয়াৰ আঁতৰাই পেলাইছে। অধিক মিনিপ্লেয়াৰ প্ৰকাৰসমূহ পুনৰুদ্ধাৰ কৰিবলৈ, YouTube 21.28.208 বা তাতকৈ পুৰণি সংস্কৰণ পেচ কৰক মিনিপ্লেয়াৰৰ প্ৰকাৰ শিৰোনাম আৰু চেনেল লুকুৱাওক অক্ষম কৰা হৈছে @@ -1252,6 +1265,8 @@ Settings → Playback (বা Autoplay) → Autoplay next video" ষ্টেটাছ বাৰৰ লেআউটো সংগত কৰি ৰাখে, যাতে ই টপ বাৰসমূহক ঢাকি নেৰাখে আৰু প্লেয়াৰৰ ওপৰত কোনো খালী ঠাই নেৰে। যদি এপটো ক্ৰাশ কৰে বা টপ বাৰবোৰ ভুলকৈ দেখায় তেন্তে ইয়াক বন্ধ কৰক গ্লৌ + Morphe Glow 1 + Morphe Glow 2 এই ডিভাইচটোৱে গ্লৌ স্প্ল্যাশ স্ক্ৰীনৰ সহায় নকৰে লাইট থিমৰ পৃষ্ঠভূমি বগা diff --git a/patches/src/main/resources/addresources/values-az-rAZ/youtube/strings.xml b/patches/src/main/resources/addresources/values-az-rAZ/youtube/strings.xml index 895ebad399..89731e18e4 100644 --- a/patches/src/main/resources/addresources/values-az-rAZ/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-az-rAZ/youtube/strings.xml @@ -455,6 +455,15 @@ Məhdudiyyətlər: Şəkil %s qovluğunda saxlanıldı Şəkli yükləmək olmadı Bu postda şəkil tapılmadı + + Postları tərcümə et + Məzmun yaradıcısı postlarını tərcümə etmək üçün açılan menyu elementi əlavə edir + Post tərcümə dili + Tərcümə et + Orijinalı göstər + Tərcümə olunur… + Postları tərcümə etmək olmadı + Post artıq tərcümə dilindədir Soruş elementini açılan menyuya əlavə et Axın açılan menyusuna video xülasəsi elementi əlavə edir @@ -863,6 +872,8 @@ Bunu düzəltmək üçün 'Video yayımlarını saxtalaşdır'-ı aktivləşdir Transkripti düzəlt Transkripti yükləməyə mane ola biləcək ilkin status yoxlanılması uğursuzluğun həll edir Şəkil içində şəkildə titrləri göstər + Stabil titr blokları + Avtomatik titrləri stabil, oxunaqlı bloklara qruplaşdırın. Bəzi sözlər tələffüz edilməmişdən əvvəl görünə bilər. Yenidən başlatmaq tələb olunur. Fəaliyyət düymələri Aşağı fəaliyyət cərgəsində hansı düymələr görünür seç diff --git a/patches/src/main/resources/addresources/values-be-rBY/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-be-rBY/shared-youtube/strings.xml index 211df85f2f..de9209443a 100644 --- a/patches/src/main/resources/addresources/values-be-rBY/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-be-rBY/shared-youtube/strings.xml @@ -21,8 +21,12 @@ Second \"item\" text" Анімацыя запуску Адключана Колер + Morphe Колер Манахромны + Morphe Манахром + Выдзяляць новыя налады + Колькасць дзён, на працягу якіх кропка паказваецца побач з наладамі і опцыямі, даданымі ў абнаўленні Выкл. Перазагрузіце прыладу, каб завяршыць налады diff --git a/patches/src/main/resources/addresources/values-be-rBY/sponsorblock/strings.xml b/patches/src/main/resources/addresources/values-be-rBY/sponsorblock/strings.xml index 724045390b..2a2bd248e9 100644 --- a/patches/src/main/resources/addresources/values-be-rBY/sponsorblock/strings.xml +++ b/patches/src/main/resources/addresources/values-be-rBY/sponsorblock/strings.xml @@ -58,6 +58,8 @@ Second \"item\" text" Ужо чытаў Паказаць Агульнае + Захаваць сегменты спампоўных відэа + Дазваляе прапускаць сегменты спампоўных відэа ў аўтаномным рэжыме Паказваць паведамленні пра памылкі падключэння Паказвае паведамленне, калі сервер SponsorBlock недаступны Уключаць адследжванне колькасці прапушчаных фрагментаў diff --git a/patches/src/main/resources/addresources/values-be-rBY/youtube/strings.xml b/patches/src/main/resources/addresources/values-be-rBY/youtube/strings.xml index 9cb9149d8d..57ab231aa8 100644 --- a/patches/src/main/resources/addresources/values-be-rBY/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-be-rBY/youtube/strings.xml @@ -455,6 +455,15 @@ Second \"item\" text" Выява захавана ў %s Не ўдалося спампаваць выяву У гэтым пасе выявы не знойдзены + + Перакласці пасты + Дадае пункт у выпадаючыя меню для перакладу пастоў стваральнікаў кантэнту + Мова перакладу пастоў + Перакласці + Паказаць арыгінал + Пераклад... + Не ўдалося перакласці паст + Паст ужо напрымаўленыя мовай перакладу Дадаць пункт «Спытаць» у выпадаючае меню Дадае пункт «Сціснуць відэа» у выпадаюе меню стужкі @@ -863,6 +872,8 @@ Second \"item\" text" Выправіць транскрыпцыю Выпраўляе памылку праверкі папярэдніх умоў, якая можа перашкодзіць загрузцы тэксту відэа Паказваць субтытры ў рэжыме „карціна ў карціне“ + Стабільныя блокі субтытраў + Групаваць аўтаматычныя субтытры ў стабільныя, чытэльныя блокі. Некаторыя словы могуць з\'явіцца раней, чым яны будуць вымаўлены. Патрабуецца перазапуск. Кнопкі навігацыі Выберыце, якія кнопкі будуць адлюстроўвацца ў ніжняй панэлі навігацыі @@ -1211,6 +1222,8 @@ Second \"item\" text" Выключае мініплэер \"Працягнуць прагляд\", функцыю YouTube Premium, якая з\'яўляецца пры запуску праграмы Мініплэер Настройце выгляд і паводзіны зведзенага відэаплэера + Дадатковыя мініплэеры + YouTube выдаліў усе дадатковыя мініплэеры. Каб аднавіць больш тыпаў мініплэераў, патчыце YouTube 21.28.208 або больш старую версію Тып мініплэера Схаваць загаловак і канал Адключана @@ -1572,6 +1585,7 @@ AVC (H.264) Эпізоды пазначаны як праглядзеныя Скасаваць Уключыць + 1 новы эпізод %1$s новых серый Запісвайце прагрэс на гэтай прыладзе "Вашы серыялы і прагрэс застаюцца на гэтай прыладзе і будуць сумеснымі для розных улікоў YouTube. Спрыміяныя налады гісторыі YouTube не ўплываюць на іх. Ачысціце прагрэс у меню «Серыялы». diff --git a/patches/src/main/resources/addresources/values-bg-rBG/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-bg-rBG/shared-youtube/strings.xml index 47d5c9302b..c9a8ba99e5 100644 --- a/patches/src/main/resources/addresources/values-bg-rBG/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-bg-rBG/shared-youtube/strings.xml @@ -21,8 +21,13 @@ Second \"item\" text" Анимация при стартиране Изключено Цвят + Morphe Color Монохром + Morphe Monochrome + Маркиране на нови настройки + Брой дни, през които се показва точка до настройките и опциите, добавени чрез актуализация + Изкл Рестартирайте, за да завършите настройката diff --git a/patches/src/main/resources/addresources/values-bg-rBG/sponsorblock/strings.xml b/patches/src/main/resources/addresources/values-bg-rBG/sponsorblock/strings.xml index 8c00584482..b70dc58fb9 100644 --- a/patches/src/main/resources/addresources/values-bg-rBG/sponsorblock/strings.xml +++ b/patches/src/main/resources/addresources/values-bg-rBG/sponsorblock/strings.xml @@ -58,6 +58,8 @@ Second \"item\" text" Дублирането на видеа се показва Скрий дублиране на видеа Основни + Запазване на сегменти от изтеглени видеоклипове + Позволява пропускане на сегменти от изтеглени видеоклипове, докато сте офлайн Покажи предупреждения за грешка в свързването Показва известие, когато сървърът на SponsorBlock не е достъпен Активиране на проследяването на броя на прескачанията diff --git a/patches/src/main/resources/addresources/values-bg-rBG/youtube/strings.xml b/patches/src/main/resources/addresources/values-bg-rBG/youtube/strings.xml index 4badbdf109..11c0d6a76d 100644 --- a/patches/src/main/resources/addresources/values-bg-rBG/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-bg-rBG/youtube/strings.xml @@ -29,6 +29,8 @@ Second \"item\" text" Други Видео Възстановяване на старите менюта за настройки + Показвай съобщенията в изскачащата лента на YouTube + Съобщенията от Morphe, като например потвърждението при натискане на бутон в плейъра, се показват в изскачащата лента на YouTube вместо в изскачащ прозорец. Премахване на ограниченията за възпроизвеждане във фонов режим Забраняване на възпроизвеждането във фонов режим @@ -455,6 +457,15 @@ Second \"item\" text" Изображението е запазено в %s Изображението не можа да бъде изтеглено В този пост не е намерено изображение + + Превод на publicaciones + Добавя елемент в изскачащото меню за превод на публикации на създатели на съдържание + Език за превод на публикацията + Превод + Покажи оригинала + Превеждане… + Публикацията не можа да бъде преведена + Публикацията вече е на езика за превод Добавяне на елемент Ask в изплъзгащото се меню Добавяне на елемент за обобщение на видео към изплъзгащото се меню на емисията @@ -863,6 +874,8 @@ Second \"item\" text" Изправи препис Отстранява грешка при проверка на предварително условие, която може да попречи на зареждането на транскрипцията Показване на надписи в режим \"карта върху карта\" + Стабилни блокове за описание + Групиране на автоматичните субтитри в стабилни, лесни за четене блокове. Някои думи може да се появят преди да бъдат изречени. Изисква се рестартиране. Думиращи бутони Скрий футъра на менюто „Аудио писта“ @@ -1211,6 +1224,8 @@ Second \"item\" text" Деактивира мини плейъра „Продължи да гледаш“, функция на YouTube Premium, която се появява при стартиране на приложението Миниплеер Персонализирайте външния вид и поведението на минимизирания видео плейър + Допълнителни миниплеъри + YouTube премахна всички допълнителни миниплейъри. За да възстановите повече типове миниплейъри, пачнете YouTube 21.28.208 или по-стара версия Тип на миниплеера Скриване на заглавието и канала Изключен diff --git a/patches/src/main/resources/addresources/values-bn-rBD/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-bn-rBD/shared-youtube/strings.xml index 2499b5085b..c75ecf4e57 100644 --- a/patches/src/main/resources/addresources/values-bn-rBD/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-bn-rBD/shared-youtube/strings.xml @@ -21,8 +21,13 @@ Second \"item\" text" স্টার্টআপ অ্যানিমেশন বন্ধ রাখুন রং + Morphe কালার মনোক্রোম + Morphe মনোক্রোম + নতুন সেটিংস হাইলাইট করুন + আপডেটের মাধ্যমে যুক্ত সেটিংস এবং অপশনগুলোর পাশে কত দিন একটি ডট দেখানো হবে + বন্ধ সেটআপ সম্পন্ন করতে রিস্টার্ট করুন diff --git a/patches/src/main/resources/addresources/values-bn-rBD/sponsorblock/strings.xml b/patches/src/main/resources/addresources/values-bn-rBD/sponsorblock/strings.xml index 90302426d1..db26dca74e 100644 --- a/patches/src/main/resources/addresources/values-bn-rBD/sponsorblock/strings.xml +++ b/patches/src/main/resources/addresources/values-bn-rBD/sponsorblock/strings.xml @@ -58,6 +58,8 @@ Second \"item\" text" ইতোমধ্যে পড়া হয়েছে আমাকে দেখান সাধারণ + ডাউনলোড করা ভিডিওর সেগমেন্টগুলো সেভ করুন + অফলাইনে থাকাকালীন ডাউনলোড করা ভিডিওর সেগমেন্টগুলো স্কিপ করার অনুমতি দেয় সংযোগ ত্রুটি সতর্কতা জানান SponsorBlock সার্ভার উপলব্ধ না থাকলে একটি বিজ্ঞপ্তি দেখায় স্কিপ কাউন্ট ট্র্যাকিং সক্রিয় করুন diff --git a/patches/src/main/resources/addresources/values-bn-rBD/youtube/strings.xml b/patches/src/main/resources/addresources/values-bn-rBD/youtube/strings.xml index fb2123392c..82a9910c84 100644 --- a/patches/src/main/resources/addresources/values-bn-rBD/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-bn-rBD/youtube/strings.xml @@ -455,6 +455,15 @@ Second \"item\" text" ছবিটি %s-এ সংরক্ষণ করা হয়েছে ছবিটি ডাউনলোড করা সম্ভব হয়নি এই পোস্টে কোনো ছবি পাওয়া যায়নি + + পোস্ট অনুবাদ করুন + কনটেন্ট ক্রিয়েটরদের পোস্ট অনুবাদ করার জন্য একটি ফ্লাইআউট মেনু আইটেম যোগ করে + পোস্ট অনুবাদের ভাষা + অনুবাদ করুন + মূলটি দেখুন + অনুবাদ করা হচ্ছে… + পোস্টটি অনুবাদ করা সম্ভব হয়নি + পোস্টটি ইতিমধ্যেই অনুবাদের ভাষায় রয়েছে ফ্লাইআউট মেনুতে \'Ask\' আইটেম যোগ করুন ফিড ফ্লাইআউট মেনুতে ভিডিও সামারাইজ আইটেম যোগ করা হয় @@ -863,6 +872,8 @@ OpenRouter.ai ওপেন করতে এখানে ট্যাপ কর ট্রান্সক্রিপ্ট সংশোধন একটি প্রিকন্ডিশন চেক ব্যর্থতা সমাধান করে যা ট্রান্সক্রিপ্ট লোড হওয়া থেকে আটকাতে পারে পি-ইন-পি-তে ক্যাপশন দেখান + স্থিতিশীল ক্যাপশন ব্লক + স্বয়ংক্রিয় ক্যাপশনগুলোকে স্থিতিশীল এবং পঠনযোগ্য ব্লকে গ্রুপ করুন। কিছু শব্দ উচ্চারণের আগেই উপস্থিত হতে পারে। রিস্টার্ট করা প্রয়োজন। নেভিগেশন বোতাম নিচের নেভিগেশন বার-এ কোন বোতামগুলো প্রদর্শিত হবে তা নির্বাচন করুন @@ -1211,6 +1222,8 @@ OpenRouter.ai ওপেন করতে এখানে ট্যাপ কর \'কন্টিনিউ ওয়াচিং\' মিনিপ্লেয়ারকে নিষ্ক্রিয় করে, যা একটি ইউটিউব প্রিমিয়াম বৈশিষ্ট্য যা অ্যাপ চালু করার সময় প্রদর্শিত হয় মিনিপ্লেয়ার সংক্ষেপিত ভিডিও প্লেয়ারের চেহারা এবং আচরণ কাস্টমাইজ করুন + অতিরিক্ত মিনিপ্লেয়ার + YouTube সমস্ত অতিরিক্ত মিনিপ্লেয়ার সরিয়ে দিয়েছে। আরও মিনিপ্লেয়ার টাইপ পুনরুদ্ধার করতে, YouTube 21.28.208 বা তার আগের সংস্করণ প্যাচ করুন মিনিপ্লেয়ার ধরণ শিরোনাম এবং চ্যানেল লুকান নিষ্ক্রিয় হয়েছে @@ -1252,6 +1265,8 @@ OpenRouter.ai ওপেন করতে এখানে ট্যাপ কর স্ট্যাটাস বারের লেআউট ধ্রুবক রাখে, তাই এটি উপরের বারগুলি ঢাকবে না এবং প্লেয়ারের উপরে কোনো খালি জায়গা থাকবে না। অ্যাপটি ক্র্যাশ হলে বা উপরের বারগুলো ভুল দেখালে এই সেটিংটি বন্ধ করুন গ্লো + Morphe Glow 1 + Morphe Glow 2 এই ডিভাইসটি গ্লো স্প্ল্যাশ স্ক্রিন সমর্থন করে না হালকা থিমের পটভূমি সাদা diff --git a/patches/src/main/resources/addresources/values-bs-rBA/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-bs-rBA/shared-youtube/strings.xml index 099235b2d3..90f7aa4d27 100644 --- a/patches/src/main/resources/addresources/values-bs-rBA/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-bs-rBA/shared-youtube/strings.xml @@ -21,8 +21,13 @@ Second \"item\" text" Animacija pokretanja Onemogućeno Boja + Morphe u boji Monohromatska + Morphe monohromski + Istakni nova podešavanja + Broj dana tokom kojih će tačka biti prikazana pored podešavanja i opcija dodanih ažuriranjem + Isključeno Ponovo pokreni za završetak konfiguracije diff --git a/patches/src/main/resources/addresources/values-bs-rBA/sponsorblock/strings.xml b/patches/src/main/resources/addresources/values-bs-rBA/sponsorblock/strings.xml index 5aed16449e..8db93b5912 100644 --- a/patches/src/main/resources/addresources/values-bs-rBA/sponsorblock/strings.xml +++ b/patches/src/main/resources/addresources/values-bs-rBA/sponsorblock/strings.xml @@ -58,6 +58,8 @@ Second \"item\" text" Već pročitano Pokaži mi Opširno + Sačuvaj segmente preuzetih videozapisa + Omogućava preskakanje segmenata preuzetih videozapisa dok ste offline Prikaži upozorenja o grešci u povezivanju Prikazuje obavijest kada SponsorBlock server nije dostupan Omogući praćenje broja preskakanja @@ -223,6 +225,9 @@ Ako ne šaljete istaknuti dio, dodirnite Otkaži i označite drugu lokaciju"Preskočili ste <b>%s</b> segmenata To je <b>%s</b> Resetujte brojač preskočenih segmenata? + %1$s sati %2$s minuta + %1$s minuta %2$s sekundi + %s sekundi Prozirnost: Boja: "SponsorBlock je crowdsourced baza podataka video segmenata kao što su sponzori, uvodi, završnici i podsjetnici za pretplatu. Segmente šalje i glasa zajednica, a baza je otvorena i besplatna za preuzimanje diff --git a/patches/src/main/resources/addresources/values-bs-rBA/youtube/strings.xml b/patches/src/main/resources/addresources/values-bs-rBA/youtube/strings.xml index fa3e910cd0..9c10fa978a 100644 --- a/patches/src/main/resources/addresources/values-bs-rBA/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-bs-rBA/youtube/strings.xml @@ -455,6 +455,15 @@ Prijava je javna i pregledana prije nego što kanal bude dodat na listu. Uklanja Slika je sačuvana u %s Slika nije mogla biti preuzeta U ovoj objavi nije pronađena slika + + Prevedi objave + Dodaje stavku u iskačući izbornik za prevođenje objava kreatora sadržaja + Jezik za prevođenje objava + Prevedi + Prikaži original + Prevođenje… + Objava nije mogla biti prevedena + Objava je već na jeziku na koji se prevodi Dodaj stavku Pitaj u flyout meni Dodaje stavku za sažetak videa u flyout meni feed-a @@ -863,6 +872,8 @@ Da biste ovo popravili, uključite 'Spoof video streamova' i promijenite 'Podraz Popravi transkript Rješava neuspjeh provjere preduvjeta koji može spriječiti učitavanje transkripta Prikaži titlove u slika-u-slici načinu + Stabilni blokovi titlova + Grupiše automatske titlove u stabilne, čitljive blokove. Neke riječi se mogu pojaviti prije nego što budu izgovorene. Potrebno je ponovno pokretanje. Navigacijski gumbi Odaberite koje će se dugmad pojaviti u donjoj navigacijskoj traci @@ -1211,6 +1222,8 @@ Ograničenje: Ako je 'Vrati staru traku s akcijama videa' isključeno, preklop ' Onemogućava mini-player \'Nastavi gledati\', YouTube Premium funkciju koja se pojavljuje pri pokretanju aplikacije Mini-player Prilagodite izgled i ponašanje minimiziranog video plejera + Dodatni mini-plejeri + YouTube je uklonio sve dodatne mini-plejere. Da biste vratili više tipova mini-plejera, patched-ujte YouTube 21.28.208 ili stariju verziju Tip mini-playera Sakrij naslov i kanal Onemogućeno @@ -1252,6 +1265,8 @@ Ograničenje: Ako je 'Vrati staru traku s akcijama videa' isključeno, preklop ' Održava konzistentan izgled statusne trake, tako da ne prekriva gornje trake i ne ostavlja prazan prostor iznad plejera. Isključite ovo ako aplikacija puca ili ako gornje trake izgledaju pogrešno Sjaj + Morphe Glow 1 + Morphe Glow 2 Svijetleći početni ekran nije podržan na ovom uređaju Pozadina svijetle teme Bijela @@ -1570,6 +1585,7 @@ Automatsko snimanje prestaje u inkognito modu, kada identitet računa nije dostu Epizode označene kao gledane Poništi Omogući + 1 nova epizoda %1$s novih epizoda Zapisuj napredak na ovom uređaju "Vaše serije i napredak ostaju na ovom uređaju, zajednički za YouTube račune. YouTube kontrole historije ih ne mijenjaju. Obrišite napredak iz menija Serije. diff --git a/patches/src/main/resources/addresources/values-ca-rES/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-ca-rES/shared-youtube/strings.xml index 2f02d82a8b..162f052d3f 100644 --- a/patches/src/main/resources/addresources/values-ca-rES/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ca-rES/shared-youtube/strings.xml @@ -21,8 +21,12 @@ Second \"item\" text" Animació d\'inici Deshabilitat Color + Morphe Color Monocrom + Morphe Monochrome + Destacar els nous ajustos + Nombre de dies que es mostra un punt al costat dels ajustos i opcions afegits per una actualització Desactivat Reinicia per acabar la configuració diff --git a/patches/src/main/resources/addresources/values-ca-rES/sponsorblock/strings.xml b/patches/src/main/resources/addresources/values-ca-rES/sponsorblock/strings.xml index 0da66df96b..b06f624ead 100644 --- a/patches/src/main/resources/addresources/values-ca-rES/sponsorblock/strings.xml +++ b/patches/src/main/resources/addresources/values-ca-rES/sponsorblock/strings.xml @@ -58,6 +58,8 @@ Second \"item\" text" Ja l\'he llegit Mostra\'m-ho General + Desarma segments de vídeos descarregats + Permet saltar segments de vídeos descarregats mentre estis fora de línia Mostrar alertes d\'error de connexió Mostra una notificació quan el servidor de SponsorBlock no està disponible Habilitar el seguiment del recompte de salts diff --git a/patches/src/main/resources/addresources/values-ca-rES/youtube/strings.xml b/patches/src/main/resources/addresources/values-ca-rES/youtube/strings.xml index 752f495912..125617f78d 100644 --- a/patches/src/main/resources/addresources/values-ca-rES/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ca-rES/youtube/strings.xml @@ -455,6 +455,15 @@ Els informes són públics i es revisen abans d'afegir un canal a una llista. L' Imatge guardada a %s No s\'ha pogut descarregar la imatge No s\'ha trobat cap imatge en aquesta publicació + + Traduir publicacions + Afegeix un element al menú desplegable per traduir les publicacions dels creadors de contingut + Idioma de traducció de la publicació + Traduïr + Mostrar l\'original + Traduint… + No s\'ha pogut traduir la publicació + La publicació ja està en l\'idioma de traducció Afegeix l\'element Pregunta al menú desplegable Afegeix un element de resum de vídeo al menú desplegable del feed @@ -863,6 +872,8 @@ Cal proporcionar les 'cookies de subtítols'" Corrigir transcripció Soluciona un error de comprovació de precondició que pot impedir que es carregui la transcripció Mostrar els subtítols en el mode picture-in-picture + Blocs de subtítols estables + Agrupa els subtítols automàtics en blocs estables i llegibles. Alguns mots poden aparèixer abans de ser pronunciats. Cal reiniciar. Botons de navegació Tria quins botons apareixen a la barra de navigació inferior @@ -1211,6 +1222,8 @@ Limitació: Si 'Restaura la barra d'accions de vídeo antiga' està desactivada, Desactiva el miniplayer de \"Continuar veient\", una funció de YouTube Premium que apareix en iniciar l\'aplicació Miniplayer Personalitza l\'aparença i el comportament del reproductor de vídeo minimitzat + Miniplayers addicionals + YouTube ha eliminat tots els miniplayers addicionals. Per restaurar més tipus de miniplayer, apliqueu el patch a YouTube 21.28.208 o anteriors Tipus de miniplayer Ocultar títol i canal Deshabilitat @@ -1553,6 +1566,7 @@ El registre automàtic s'atura en mode incògnit, quan la identitat del compte n Habilitar el seguiment local El registre del progrés està desactivat Reprèn a %1$s + %1$s / %2$s episodis vistos Episodi %1$s · %2$s Episodis %1$s-%2$s de %3$s Actualitza per intentar carregar els episodis. Les llistes de reproducció privades requereixen accés al compte; el seu marcador encara s\'obre a YouTube @@ -1571,6 +1585,8 @@ El registre automàtic s'atura en mode incògnit, quan la identitat del compte n Episodis marcats com a vistos Desfer Habilitar + 1 episodi + %1$s episodi Registrar el progrés en aquest dispositiu "Les teves sèries i el teu progrés es mantenen en aquest dispositiu, compartits entre comptes de YouTube. Els controls de l'historial de YouTube no els modifiquen. Esborra el progrés des del menú de Sèries. diff --git a/patches/src/main/resources/addresources/values-ckb-rIR/youtube/strings.xml b/patches/src/main/resources/addresources/values-ckb-rIR/youtube/strings.xml index 71e44045f1..15c5ee8ba5 100644 --- a/patches/src/main/resources/addresources/values-ckb-rIR/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ckb-rIR/youtube/strings.xml @@ -456,6 +456,7 @@ Second \"item\" text" وێنەکە پاشکەوت کرا بۆ %s نەتوانرا وێنەکە دابەزێنرێت هیچ وێنەیەک لەم پۆستەدا نەدۆزرایەوە + بەنem­کەری Ask زیاد بکە بۆ مێنیو­ی flyout بەنem­کەری کورتکردنەوەی ڤیدیۆ زیاد دەکات بۆ مێنیو­ی flyout­ی فیدەکە diff --git a/patches/src/main/resources/addresources/values-cs-rCZ/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-cs-rCZ/shared-youtube/strings.xml index a40928309a..d39ed5a772 100644 --- a/patches/src/main/resources/addresources/values-cs-rCZ/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-cs-rCZ/shared-youtube/strings.xml @@ -21,8 +21,13 @@ Second \"item\" text" Animace při spuštění Vypnuto Barva + Morphe Barevný Monochrom + Morphe Monochromatický + Označit nová nastavení + Počet dní, po které se vedle nastavení a možností přidaných aktualizací zobrazuje tečka + Vypnuto Restartovat pro dokončení nastavení diff --git a/patches/src/main/resources/addresources/values-cs-rCZ/sponsorblock/strings.xml b/patches/src/main/resources/addresources/values-cs-rCZ/sponsorblock/strings.xml index dce2c80a9e..3bf9192656 100644 --- a/patches/src/main/resources/addresources/values-cs-rCZ/sponsorblock/strings.xml +++ b/patches/src/main/resources/addresources/values-cs-rCZ/sponsorblock/strings.xml @@ -58,6 +58,8 @@ Second \"item\" text" Již přečteno Ukaž mi Obecné + Ukládat segmenty stažených videí + Umožňuje přeskakovat segmenty stažených videí v režimu offline Zobrazit upozornění na chybu připojení Zobrazí oznámení, když není server SponsorBlock dostupný Povolit sledování počtu přeskočení diff --git a/patches/src/main/resources/addresources/values-cs-rCZ/youtube/strings.xml b/patches/src/main/resources/addresources/values-cs-rCZ/youtube/strings.xml index 69201bc5cd..5ceab88aab 100644 --- a/patches/src/main/resources/addresources/values-cs-rCZ/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-cs-rCZ/youtube/strings.xml @@ -455,6 +455,15 @@ Hlášení jsou veřejná a před přidáním kanálu do seznamu jsou kontrolov Obrázek byl uložen do %s Obrázek se nepodařilo stáhnout V tomto příspěvku nebyl nalezen žádný obrázek + + Překládat příspěvky + Přidá do vysouvacího menu položku pro překlad příspěvků tvůrců obsahu + Jazyk překladu příspěvku + Přeložit + Zobrazit originál + Překládání… + Příspěvek se nepodařilo přeložit + Příspěvek je již v cílovém jazyce překladu Přidat položku Ask do rozbalovací nabídky Přidá položku shrnutí videa do rozbalovací nabídky v kanálu @@ -861,6 +870,8 @@ Chcete-li to opravit, zapněte 'Spoof video streams' a změňte 'Default client' Opravte přepis Vyřeší problém se selháním kontroly předpokladů, který může zabránit načtení přepisu Zobrazovat titulky v režimu obraz v obraze + Stabilní bloky titulků + Seskupuje automatické titulky do stabilních, čitelných bloků. Některá slova se mohou objevit dříve, než jsou vyslovena. Vyžaduje restart. Navigační tlačítka Vyberte, které komponenty se zobrazí v panelu nástrojů @@ -1209,6 +1220,8 @@ Omezení: Pokud je vypnutá možnost 'Obnovit starou lištu akcí videa', překr Vypne minipřehrávač \'Pokračovat ve sledování\', funkci YouTube Premium, která se zobrazí při spuštění aplikace Mini přehrávač Přizpůsobte vzhled a chování minimalizovaného přehrávače videa + Další mini přehrávače + YouTube odstranil všechny další mini přehrávače. Chcete-li obnovit více typů mini přehrávačů, patchujte YouTube 21.28.208 nebo starší verzi Typ mini přehrávače Skrýt název a kanál Vypnuto diff --git a/patches/src/main/resources/addresources/values-da-rDK/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-da-rDK/shared-youtube/strings.xml index 4cc2b49dac..9bdcac2030 100644 --- a/patches/src/main/resources/addresources/values-da-rDK/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-da-rDK/shared-youtube/strings.xml @@ -21,8 +21,13 @@ Second \"item\" text" Startanimation Deaktiveret Farve + Morphe Farve Monokrom + Morphe Monokrom + Fremhæv nye indstillinger + Antal dage en prik vises ved siden af indstillinger og valgmuligheder tilføjet af en opdatering + Fra Genstart for at færdiggøre opsætningen diff --git a/patches/src/main/resources/addresources/values-da-rDK/sponsorblock/strings.xml b/patches/src/main/resources/addresources/values-da-rDK/sponsorblock/strings.xml index 90c845e4e3..1361f97767 100644 --- a/patches/src/main/resources/addresources/values-da-rDK/sponsorblock/strings.xml +++ b/patches/src/main/resources/addresources/values-da-rDK/sponsorblock/strings.xml @@ -58,6 +58,8 @@ Second \"item\" text" Allerede læst Vis mig Generelt + Gem segmenter af downloadede videoer + Gør det muligt at springe segmenter over i downloadede videoer, når man er offline Vis forbindelsesfejlalarmer Viser en notifikation, når SponsorBlock-serveren er utilgængelig Aktiver sporing af overspringstæller diff --git a/patches/src/main/resources/addresources/values-da-rDK/youtube/strings.xml b/patches/src/main/resources/addresources/values-da-rDK/youtube/strings.xml index a60de6ee8c..08fca21717 100644 --- a/patches/src/main/resources/addresources/values-da-rDK/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-da-rDK/youtube/strings.xml @@ -453,6 +453,15 @@ Rapporter er offentlige og gennemgås, før en kanal tilføjes til en liste. Fje Billede gemt i %s Kunne ikke downloade billedet Intet billede fundet i dette opslag + + Oversæt indlæg + Tilføjer et punkt i flyout-menuen til at oversætte indlæg fra indholdsskabere + Sprog til oversættelse af indlæg + Oversæt + Vis original + Oversætter… + Kunne ikke oversætte indlægget + Indlægget er allerede på oversættelsessproget Tilføj \"Ask\"-emne til flyout-menuen Tilføjer et emne til videofeed-sammenfatning i feedets flyout-menu @@ -859,6 +868,8 @@ For at løse dette skal du slå 'Spoof video streams' til og ændre 'Standardkli Ret transkription Skjul Settings knap Vis undertekster i billede-i-billede + Stabile tekstblokke + Gruppér automatiske undertekster i stabile, læsbare blokke. Nogle ord kan vises, før de bliver talt. Genstart påkrævet. Navigationsknapper Administrer Ambient mode synlighed og strømbegrænsninger @@ -1207,6 +1218,8 @@ Begrænsning: Hvis 'Gendan gammel video-handlingslinje' er slået fra, er 'Flere Deaktiverer \'Fortsæt med at se\'-miniafspilleren, en YouTube Premium-funktion, der vises, når appen startes Mini-afspiller Tilpas udseendet og adfærden af den minimerede videoplayer + Yderligere miniplayere + YouTube fjernede alle yderligere miniplayere. For at genoprette flere miniplayer-typer skal du patche YouTube 21.28.208 eller ældre Type af miniplayer Skjul titel og kanal Deaktiveret diff --git a/patches/src/main/resources/addresources/values-de-rDE/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-de-rDE/shared-youtube/strings.xml index 68de89c93d..857143a9eb 100644 --- a/patches/src/main/resources/addresources/values-de-rDE/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-de-rDE/shared-youtube/strings.xml @@ -21,8 +21,13 @@ Second \"item\" text" Startanimation Deaktiviert Farbe + Morphe Farbe Einfarbig + Morphe Monochrom + Neue Einstellungen hervorheben + Anzahl der Tage, an denen ein Punkt neben den durch ein Update hinzugefügten Einstellungen und Optionen angezeigt wird + Aus Starte die App neu, um die Einrichtung abzuschließen diff --git a/patches/src/main/resources/addresources/values-de-rDE/sponsorblock/strings.xml b/patches/src/main/resources/addresources/values-de-rDE/sponsorblock/strings.xml index 8a4a6ae69e..49514affac 100644 --- a/patches/src/main/resources/addresources/values-de-rDE/sponsorblock/strings.xml +++ b/patches/src/main/resources/addresources/values-de-rDE/sponsorblock/strings.xml @@ -58,6 +58,8 @@ Second \"item\" text" Bereits gelesen Anzeigen Allgemein + Segmente von heruntergeladenen Videos speichern + Erlaubt das Überspringen von Segmenten heruntergeladener Videos, während sie offline sind Warnungen zu Verbindungsfehlern anzeigen Zeigt eine Benachrichtigung an, wenn der SponsorBlock-Server nicht verfügbar ist Skip-Zählungsverfolgung aktivieren diff --git a/patches/src/main/resources/addresources/values-de-rDE/youtube/strings.xml b/patches/src/main/resources/addresources/values-de-rDE/youtube/strings.xml index 016f178874..ff4a074097 100644 --- a/patches/src/main/resources/addresources/values-de-rDE/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-de-rDE/youtube/strings.xml @@ -456,6 +456,15 @@ Meldungen sind öffentlich und werden überprüft, bevor ein Kanal zu einer List Bild unter %s gespeichert Das Bild konnte nicht heruntergeladen werden In diesem Beitrag wurde kein Bild gefunden + + Beiträge übersetzten + Fügt einen Flyout-Menüpunkt hinzu, um Beiträge zum Erstellen von Inhalten zu übersetzen + Sprache der Beitragsübersetzung + Übersetzen + Original anzeigen + Übersetze... + Beitrag konnte nicht übersetzt werden + Der Beitrag ist bereits in der richtigen Sprache Ask-Element zum Flyoutmenü hinzufügen Fügt einen Artikel zur Zusammenfassung von Videos im Feed-Flyoutmenü hinzu @@ -864,6 +873,8 @@ Um das zu beheben, aktiviere 'Video-Streams vortäuschen' und ändere den 'Stand Transkript reparieren Behebt einen Fehler bei der Vorbedingungsprüfung, der das Laden des Transkripts verhindern kann. Untertitel im Picture-in-Picture anzeigen + Stabile Untertitelblöcke + Automatische Untertitel in stabilen, lesbaren Blöcken gruppieren. Einige Wörter können erscheinen, bevor sie gesprochen werden. Neustart erforderlich. Navigationstasten Wähle aus, welche Schaltflächen in der unteren Navigationsleiste angezeigt werden sollen @@ -1212,6 +1223,8 @@ Einschränkung: Wenn \"Alte Video-Aktionsleiste wiederherstellen\" deaktiviert i Deaktiviert den Miniplayer „Weiter ansehen“, ein YouTube Premium-Feature, das beim Starten der App angezeigt wird. Minispieler Passen Sie das Erscheinungsbild und das Verhalten des minimierten Videoplayers an + Zusätzliche Miniplayer + YouTube hat alle zusätzlichen Miniplayer entfernt. Um weitere Miniplayer-Typen wiederherzustellen, patchen Sie YouTube 21.28.208 oder eine ältere Version Minispielertyp Titel und Kanal ausblenden Deaktiviert diff --git a/patches/src/main/resources/addresources/values-el-rGR/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-el-rGR/shared-youtube/strings.xml index 77f73e129f..ab4a6e9541 100644 --- a/patches/src/main/resources/addresources/values-el-rGR/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-el-rGR/shared-youtube/strings.xml @@ -21,8 +21,12 @@ Second \"item\" text" Κινούμενο σχέδιο εκκίνησης Απενεργοποιημένο Χρωματιστή + Morphe Έγχρωμο Μονόχρωμο + Morphe Μονόχρωμο + Επισημάνιση νέων ρυθμίσεων + Αριθμός ημερών που εμφανίζεται μια τελεία δίπλα στις ρυθμίσεις και τις επιλογές που προστέθηκαν μέσω μιας ενημέρωσης Ανενεργό Επανεκκινήστε για να ολοκληρωθεί η διαδικασία diff --git a/patches/src/main/resources/addresources/values-el-rGR/sponsorblock/strings.xml b/patches/src/main/resources/addresources/values-el-rGR/sponsorblock/strings.xml index 6b1182e431..2751a261f6 100644 --- a/patches/src/main/resources/addresources/values-el-rGR/sponsorblock/strings.xml +++ b/patches/src/main/resources/addresources/values-el-rGR/sponsorblock/strings.xml @@ -58,6 +58,8 @@ Second \"item\" text" Τις έχω διαβάσει Δείξτε μου Γενικά + Αποθήκευση τμημάτων ληφθέντων βίντεο + Επιτρέπει την παράκαμψη τμημάτων ληφθέντων βίντεο κατά την εκτός σύνδεσης χρήση Εμφάνιση ειδοποιήσεων σφάλματος σύνδεσης Εμφανίζει μια ειδοποίηση όταν ο διακομιστής SponsorBlock δεν είναι διαθέσιμος Mετρητής παραλείψεων τμημάτων diff --git a/patches/src/main/resources/addresources/values-el-rGR/youtube/strings.xml b/patches/src/main/resources/addresources/values-el-rGR/youtube/strings.xml index 4827b39d99..8848cea4c3 100644 --- a/patches/src/main/resources/addresources/values-el-rGR/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-el-rGR/youtube/strings.xml @@ -455,6 +455,15 @@ Second \"item\" text" Η εικόνα αποθηκεύτηκε στο %s Δεν ήταν δυνατή η λήψη της εικόνας Δεν βρέθηκε εικόνα σε αυτή τη δημοσίευση + + Μετάφραση αναρτήσεων + Προσθέτει ένα στοιχείο στο αναδυόμενο μενού για τη μετάφραση αναρτήσεων δημιουργών περιεχομένου + Γλώσσα μετάφρασης αναρτήσεων + Μετάφραση + Εμφάνιση πρωτότυπου + Μετάφραση… + Δεν ήταν δυνατή η μετάφραση της ανάρτησης + Η ανάρτηση είναι ήδη στη γλώσσα μετάφρασης Προσθήκη στοιχείου Ask στο μενού flyout Προσθέτει στοιχείο σύνοψης βίντεο στο μενού flyout της ροής @@ -863,6 +872,8 @@ Second \"item\" text" Επљеφρώσε τη μεταγραφή Απόκρυψη κουμπιού Ειδοποιήσεων Εμφάνιση υποτίτλων σε εικόνα μέσα σε εικόνα + Σταθερά τμήματα λεζάντας + Ομαδοποίηση αυτόματων λεζάντας σε σταθερά, αναγνώσιμα τμήματα. Ορισμένες λέξεις ενδέχεται να εμφανιστούν πριν προφερθούν. Απαιτείται επανεκκίνηση. Κουμπιά γραμμής πλοήγησης Απόκρυψη κουμπιού Αναζήτησης @@ -1211,6 +1222,8 @@ Second \"item\" text" Απενεργοποιεί το μίνι player \"Συνέχιση παρακολούθησης\", μια λειτουργία YouTube Premium που εμφανίζεται κατά την εκκίνηση της εφαρμογής Ελαχιστοποιημένη οθόνη αναπαραγωγής Προσαρμόστε την εμφάνιση και τη συμπεριφορά του μικρογραφικού αναπαραγωγέα βίντεο + Πρόσθετοι miniplayers + Το YouTube αφαίρεσε όλους τους πρόσθετους miniplayers. Για να επαναφέρετε περισσότερους τύπους miniplayer, κάντε patch στο YouTube 21.28.208 ή παλαιότερη έκδοση Τύπος ελαχιστοποιημένης οθόνης αναπαραγωγής Απόκρυψη τίτλου και καναλιού Απενεργοποιημένη diff --git a/patches/src/main/resources/addresources/values-es-rES/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-es-rES/shared-youtube/strings.xml index c98bf9befb..e612fa1092 100644 --- a/patches/src/main/resources/addresources/values-es-rES/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-es-rES/shared-youtube/strings.xml @@ -21,7 +21,9 @@ Second \"item\" text" Animación de inicio Desactivado Color + Color de Morphe Monocromático + Morphe Monocromo Destacar nuevos ajustes Número de días que se muestra un punto junto a los ajustes y las opciones añadidos por una actualización diff --git a/patches/src/main/resources/addresources/values-es-rES/youtube/strings.xml b/patches/src/main/resources/addresources/values-es-rES/youtube/strings.xml index 69372ead21..3e8ed9b6db 100644 --- a/patches/src/main/resources/addresources/values-es-rES/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-es-rES/youtube/strings.xml @@ -455,6 +455,15 @@ Los reportes son públicos y se revisan antes de añadir un canal a una lista. L Imagen guardada en %s No se pudo descargar la imagen No se encontró ninguna imagen en esta publicación + + Traducir publicaciones + Añade un elemento de menú desplegable para traducir las publicaciones de los creadores de contenido + Idioma de traducción de publicaciones + Traducir + Mostrar original + Traduciendo… + No se ha podido traducir la publicación + La publicación ya está en el idioma de traducción Añadir elemento «Preguntar» al menú desplegable Añade un elemento de resumen de vídeo al menú desplegable del feed @@ -863,6 +872,8 @@ Se deben proporcionar 'cookies de subtítulos'" Corregir transcripción Soluciona un fallo de la comprobación de un requisito previo que puede impedir la carga de la transcripción Mostrar subtítulos en «imagen en imagen» + Bloques de subtítulos estables + Agrupa los subtítulos automáticos en bloques estables y legibles. Es posible que algunas palabras aparezcan antes de que se pronuncien. Es necesario reiniciar. Botones de navegación Elige qué botones aparecen en la barra de navegación inferior @@ -1210,6 +1221,8 @@ Limitación: si la opción 'Restaurar antigua barra de acciones de vídeo' está Desactiva el reproductor en miniatura de \'Seguir viendo\', una función de YouTube Premium que aparece al iniciar la aplicación. Reproductor en miniatura Personaliza la apariencia y el comportamiento del reproductor de vídeo minimizado + Mini-reproductores adicionales + YouTube eliminó todos los reproductores en miniatura adicionales. Para restaurar más tipos de reproductores en miniatura, parchea YouTube 21.28.208 o anterior Tipo de reproductor en miniatura Ocultar título y canal Desactivado @@ -1251,6 +1264,8 @@ Limitación: si la opción 'Restaurar antigua barra de acciones de vídeo' está Mantiene la uniformidad del diseño de la barra de estado, de modo que no cubra las barras superiores y no deje ningún espacio vacío encima del reproductor. Desactiva esta opción si la aplicación se bloquea o las barras superiores no se ven bien. Resplandor + Morphe Glow 1 + Morphe Glow 2 La pantalla de inicio con efecto de resplandor no es compatible con este dispositivo Fondo del tema claro Blanco @@ -1570,7 +1585,7 @@ La grabación automática se detiene en modo incógnito, cuando la identidad de Deshacer Activar 1 nuevo episodio - %1$s nuevos episodios + %1$s nuevos episodios Registrar progreso en este dispositivo "Tus series y el progreso se mantienen en este dispositivo, compartidos entre cuentas de YouTube. Los controles de historial de YouTube no los modifican. Borra el progreso desde el menú de Series. diff --git a/patches/src/main/resources/addresources/values-et-rEE/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-et-rEE/shared-youtube/strings.xml index 49e48272f7..2636e9f6ff 100644 --- a/patches/src/main/resources/addresources/values-et-rEE/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-et-rEE/shared-youtube/strings.xml @@ -21,8 +21,13 @@ Second \"item\" text" Käivitusanimatsioon Välja lülitatud Värv + Morphe Color Monokroomne + Morphe Monochrome + Rõtusta uusi seadeid + Päevade arv, mille jooksul kuvatakse uuendusega lisatud seadete ja valikute kõrval punkt + Väljas Seadistamise lõpetamiseks taaskäivitage diff --git a/patches/src/main/resources/addresources/values-et-rEE/sponsorblock/strings.xml b/patches/src/main/resources/addresources/values-et-rEE/sponsorblock/strings.xml index 9af20d3629..a920f5f425 100644 --- a/patches/src/main/resources/addresources/values-et-rEE/sponsorblock/strings.xml +++ b/patches/src/main/resources/addresources/values-et-rEE/sponsorblock/strings.xml @@ -58,6 +58,8 @@ Second \"item\" text" Juba lugenud Näita mulle Üldine + Salvesta allalaaditud videote segmente + Võimaldab allalaaditud videote segmente offline režiimis飛ばada Kuva ühendusvigade teavitused Kuvab teavituse, kui SponsorBlock server ei ole saadaval Luba möödaminekute arvamist diff --git a/patches/src/main/resources/addresources/values-et-rEE/youtube/strings.xml b/patches/src/main/resources/addresources/values-et-rEE/youtube/strings.xml index cf23c5c038..c3997b1701 100644 --- a/patches/src/main/resources/addresources/values-et-rEE/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-et-rEE/youtube/strings.xml @@ -451,9 +451,19 @@ Teated on avalikud ja neid ülevaadatakse enne kanali nimekirja lisamist. Eemald Laadi pilt alla {one, one:\"Laaditakse pilti…\", other:\"Laaditakse %s pilti…\"} Laaditakse pilti… + %1$s pilti salvestatud asukohta %2$s Pilt salvestati asukohta %s Pildi allalaadimine ebaõnnestus Selle postitusest ei leitud ühtki pilti + + Tõlgi postitusi + Lisab riputmenu elementi sisuloojate postituste tõlmeks + Postituse tõlke keel + Tõlgi + Näita algset + Tõlgitakse… + Postitust ei õnnestunud tõlkida + Postitus on juba tõlke keeles Lisa \"Küsige\" üksus riputmenuusse Lisab video kokkuvõtte üksuse feedi riputmenuusse @@ -862,6 +872,8 @@ Selle parandamiseks lülita sisse \"Videovoogude imiteerimine\" ja muuda \"Vaiki Paranda transkriptsiooni Lahendab eeltingimuste kontrolli ebaõnnestumise, mis võib takistada transkriptsiooni laadimist Kuva altekstid pildis-pildis + Stabiilsed pealkirjade plokid + Grupeerib automaatsed pealkirjad stabiilsetesse ja loetavatesse plokkidesse. Mõned sõnad võivad ilmuda enne, kui neid on hääldatud. Vajalik on taaskäivitus. Navigeerimisnupud Valige, millised nupud alumises navigatsioonisribas kuvatakse @@ -1210,6 +1222,8 @@ Piirang: kui \"Taasta vana video tegevusriba\" on välja lülitatud, ei prugu ol Lülitab välja \'Jätka vaatamist\' minimängija, mis on YouTube Premium funktsioon ja ilmub rakenduse käivitamisel Minipleier Kohandage minimeeritud videopleieri välimust ja käitumist + Täiendavad minisinuletid + YouTube eemaldas kõik täiendavad minisinuletid. Enam minisinuleti tüüpide taastamiseks paigutage YouTube\'i versioon 21.28.208 või vanem Minipleieri tüüp Peita pealkiri ja kanal Välja lülitatud @@ -1251,6 +1265,8 @@ Piirang: kui \"Taasta vana video tegevusriba\" on välja lülitatud, ei prugu ol Hoiab staatusriba paigutuse ühiseks, et see ei kataks ülaribasid ega jätaks mängija kohale tühja ruumi. Lülitage see välja, kui rakendus tõrkes või kui ülaribad näevad valesti Kere + Morphe Glow 1 + Morphe Glow 2 Seda seadet ei toeta säraefektiga avamise ekraan Helle teema taust Valge @@ -1569,6 +1585,7 @@ Automaatne salvestamine peatub incognito režiimis, kui konto identiteet pole sa Episoodid märgitud vaadatudaks Undoi Luba + 1 uus episood %1$s uut episoodi Registreeri edenemine selles seadmes "Teie sarjad ja edukujutlus jäävad sellele seadmele ja on jagatud erinevate YouTube'i kontode vahel. YouTube'i ajaloo juhtimised neid ei muuda. Puhastage edukujutlus sarjade menüüst. diff --git a/patches/src/main/resources/addresources/values-eu-rES/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-eu-rES/shared-youtube/strings.xml index dbb14a267e..feceb8e465 100644 --- a/patches/src/main/resources/addresources/values-eu-rES/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-eu-rES/shared-youtube/strings.xml @@ -21,8 +21,13 @@ Second \"item\" text" Hasi-animazioa Desaktibatuta Kolorea + Morphe Kolorea Monokromoa + Morphe Monokromoa + Kokatu konfigurazio berriak + Egunen kopurua eguneratze baten bidez gehitako konfigurazio eta aukeren ondoan puntua erakutsiko dena + Desaktibatu Berrifiziatu konfiguratzea amaitzeko diff --git a/patches/src/main/resources/addresources/values-eu-rES/sponsorblock/strings.xml b/patches/src/main/resources/addresources/values-eu-rES/sponsorblock/strings.xml index 8a05bdc4a4..73611a0646 100644 --- a/patches/src/main/resources/addresources/values-eu-rES/sponsorblock/strings.xml +++ b/patches/src/main/resources/addresources/values-eu-rES/sponsorblock/strings.xml @@ -58,6 +58,8 @@ Second \"item\" text" Irkurria Erakutsi Orokorra + Deskargatutako bideoen segmentuak gorde coffersen deliverablesen + Offline egon zehar deskargatutako bideoen segmentuak saltzea ahalbidetzen du Erakutsi konexio-erroreen alertak Jakinarazpen bat erakutsi SponsorBlock zerbitzaria eskuragarri ez denean Gaitu salto-zenbakizuna jarraitzeko aukera @@ -214,6 +216,7 @@ Nabarmenik bidaltzen ez baduzu, sakatu Utzi eta markatu bigarren kokapena"Erabiltzaile-izena ezin da alda tu: Egoera: %1$s %2$s Erabiltzaile izena arrakastetsunez aldatua Zure ospea <b>%.2f</b> da + <b>%s</b> segmentu sortu dituzu Sakatu hemen zure segmentuak ikusteko SponsorBlock-en sailburua Jendeari <b>%s</b> segmentu saltsi dizkiokizkie diff --git a/patches/src/main/resources/addresources/values-eu-rES/youtube/strings.xml b/patches/src/main/resources/addresources/values-eu-rES/youtube/strings.xml index 7939414eab..a25a9fa894 100644 --- a/patches/src/main/resources/addresources/values-eu-rES/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-eu-rES/youtube/strings.xml @@ -456,6 +456,15 @@ Txostenak publikoak dira eta kanal bat zerrendara gehitu aurretik berrikusi egit Irudia %snon gorde da Ezin izan da irudia deskargatu Ez da irurik aurkitu post honetan + + Postak itzuli + Eduki sortzaileen postak itzulitzeko menu bateko elementua gehitzen du + Postaren itzulpen hizkuntza + Itzuli + Erakutsi jatorrizkoa + Itzulitzen... + Ezin izan da posta itzuli + Posta dagoすでに itzulpen hizkuntzan Galdetu elementua gehitu menu zabagarria Bideoaren laburpen elementu bat gehitzen du feed-eko menu zabagarrian @@ -864,6 +873,8 @@ Hona konponduz, aktibu 'Bideo-emisioak imitatzea' eta alda 'Beherako bezeroa' 'T Konpondu transkripzioa Transkripzioa kargatzea eragoindu ditzakeen baldintza aurreko egiaztapen hutsbaten arazoa konpondu Erakutsi subtituluak irudi-irudian + Kaptuen blokak egonkorrak + Kaptuak automatikoak blok egonkor eta irakurgarretan sailatu. Batzuetan hitzak esan baino lehen agertu daitezke. Berrifundatzea beharrezkoa da. Nabigazio botoiak Aukeratu zein botoiak agertu behar diren behera navigazio barra @@ -1212,6 +1223,8 @@ Limitazioa: 'Bideo ekintza barra zaharra berreskuratu' tắt dago bada, 'Bideo g Desaktibatzen du \'Jarraitu ikusten\' miniplayer-a, app-a hasterean agertzen den YouTube Premium ezaugarri bat Mini-jokalaria Personalizatu bideo pantaila txikiagoaren itxura eta jokapena + Mini-erreproduzitzaile gehigarriak + YouTube-k mini-erreproduzitzaile gehigarri guztiak kendu ditu. Mini-erreproduzitzaile mota gehiago berreskuratzeko, patchatu YouTube 21.28.208 bertsioa edo lehenagokoak Mini-jokalariaren mota Ezkutatu izenburua eta kanala Desaktibatuta @@ -1573,7 +1586,8 @@ Grabaketa automatikoa gelditu egiten da inkognito moduan, kontuen identitatea es Episodioak ikusita markatu dira Utzi Gaitu - {%1$s} episodico berri + one episodio berri + %1$s episodico berri Grabatu progresua gailu honetan "Zure sailak eta progresua gaildu honetan geratzen dira, YouTube kontu guztien artean partekatuz. YouTube historiako kontrolak ez dituzte aldatzen. Series menutik garbitu progresua. diff --git a/patches/src/main/resources/addresources/values-fa-rIR/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-fa-rIR/shared-youtube/strings.xml index a20de9a779..ec4b2d7fdc 100644 --- a/patches/src/main/resources/addresources/values-fa-rIR/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-fa-rIR/shared-youtube/strings.xml @@ -21,8 +21,12 @@ Second \"item\" text" انیمیشن راه‌اندازی غیرفعال رنگی + Morphe رنگی تک‌رنگ + Morphe تک‌رنگ + برجسته کردن تنظیمات جدید + تعداد روزهایی که یک نقطه در کنار تنظیمات و گزینه‌های اضافه‌شده توسط به‌روزرسانی نمایش داده می‌شود خاموش برای پایان راه‌اندازی، مجدداً راه‌اندازی کنید diff --git a/patches/src/main/resources/addresources/values-fa-rIR/sponsorblock/strings.xml b/patches/src/main/resources/addresources/values-fa-rIR/sponsorblock/strings.xml index baf823b11c..ade1b5fe36 100644 --- a/patches/src/main/resources/addresources/values-fa-rIR/sponsorblock/strings.xml +++ b/patches/src/main/resources/addresources/values-fa-rIR/sponsorblock/strings.xml @@ -58,6 +58,8 @@ Second \"item\" text" قبلاً خوانده‌ام نمایش بده عمومی + ذخیره بخش‌های ویدیوهای دانلود شده + امکان رد کردن بخش‌های ویدیوهای دانلود شده در حالت آفلاین نمایش هشدارهای خطای اتصال وقتی سرور SponsorBlock در دسترس نباشد، اعلانی نمایش می‌دهد فعال‌کردن ردیابی تعداد ردکردن‌ها diff --git a/patches/src/main/resources/addresources/values-fa-rIR/youtube/strings.xml b/patches/src/main/resources/addresources/values-fa-rIR/youtube/strings.xml index 075a1277dd..30e60f5c33 100644 --- a/patches/src/main/resources/addresources/values-fa-rIR/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-fa-rIR/youtube/strings.xml @@ -455,6 +455,15 @@ Second \"item\" text" تصویر در %s ذخیره شد دانلود تصویر امکان‌پذیر نبود تصویری در این پست یافت نشد + + ترجمهٔ پست‌ها + یک گزینه به منوی flyout برای ترجمهٔ پست‌های تولیدکنندگان محتوا اضافه می‌کند + زبان ترجمهٔ پست + ترجمه + نمایش متن اصلی + در حال ترجمه… + ترجمهٔ پست ممکن نشد + پست در حال حاضر به زبان ترجمه است افزودن آیتم Ask به منوی پرواز افزودن آیتم خلاصه‌سازی ویدیو به منوی پرواز فید @@ -863,6 +872,8 @@ Second \"item\" text" رفع متن پیاده‌شده شکست بررسی پیش‌شرطی را که می‌تواند مانع بارگذاری متن پیاده‌شده شود رفع می‌کند نمایش زیرنویس‌ها در تصویر در تصویر + بلاک‌های کپشن پایدار + دسته‌بندی کپشن‌های خودکار در بلاک‌های پایدار و خوانا. برخی کلمات ممکن است پیش از گفته شدن ظاهر شوند. نیاز به راه‌اندازی مجدد است. دکمه‌های پیمایش انتخاب کنید کدام دکمه‌ها در نوار پیمایش پایین نمایش داده شوند @@ -1210,6 +1221,8 @@ Second \"item\" text" مینی‌پخش‌کنندهٔ «ادامهٔ تماشا» را که یکی از قابلیت‌های YouTube Premium است و هنگام راه‌اندازی برنامه ظاهر می‌شود غیرفعال می‌کند مینی‌پخش‌کننده ظاهر و رفتار پخش‌کنندهٔ ویدئوی کوچک‌شده را سفارشی کنید + مینی‌پلیرهای اضافی + YouTube همهٔ مینی‌پخش‌کننده‌های اضافی را حذف کرده است. برای بازیابی انواع بیشتر مینی‌پخش‌کننده، YouTube نسخهٔ 21.28.208 یا قدیمی‌تر را پچ کنید نوع مینی‌پخش‌کننده پنهان کردن عنوان و کانال غیرفعال @@ -1251,6 +1264,8 @@ Second \"item\" text" چیدمان نوار وضعیت را یکپارچه نگه می‌دارد، بنابراین بر نوارهای بالایی پوشانده نمی‌شود و فضای خالی در بالای پخش‌کننده باقی نمی‌گذارد. اگر برنامه کرش کرد یا نوارهای بالایی به درستی نمایش داده نشدند، این گزینه را غیرفعال کنید درخشش + Morphe Glow 1 + Morphe Glow 2 صفحه شروع Glow توسط این دستگاه پشتیبانی نمی‌شود پس‌زمینهٔ طرح روشن سفید diff --git a/patches/src/main/resources/addresources/values-fi-rFI/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-fi-rFI/shared-youtube/strings.xml index 8ca950b362..aacdb17270 100644 --- a/patches/src/main/resources/addresources/values-fi-rFI/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-fi-rFI/shared-youtube/strings.xml @@ -21,8 +21,13 @@ Second \"item\" text" Käynnistysanimaatio Pois käytöstä Väri + Morphe Color Monokromi + Morphe Monochrome + Korosta uusia asetuksia + Päivien määrä, jonka ajan piste näkyy päivityksellä lisättyjen asetusten ja vaihtoehtojen vieressä + pois Käynnistä uudelleen valituksen suorittamiseksi diff --git a/patches/src/main/resources/addresources/values-fi-rFI/sponsorblock/strings.xml b/patches/src/main/resources/addresources/values-fi-rFI/sponsorblock/strings.xml index bf1f0410b7..61f0746d49 100644 --- a/patches/src/main/resources/addresources/values-fi-rFI/sponsorblock/strings.xml +++ b/patches/src/main/resources/addresources/values-fi-rFI/sponsorblock/strings.xml @@ -58,6 +58,8 @@ Second \"item\" text" Luettu jo Näytä minulle Yleiset + Tallenna ladattujen videoiden segmentit + Sallii ladattujen videoiden segmenttien ohittamisen offline-tilassa Näytä yhteyden virheilmoitukset Näyttää ilmoituksen, kun SponsorBlock-palvelin ei ole käytettävissä Ota ohitusten lukumäärän seuranta käyttöön diff --git a/patches/src/main/resources/addresources/values-fi-rFI/youtube/strings.xml b/patches/src/main/resources/addresources/values-fi-rFI/youtube/strings.xml index 3f81f88e63..0a8cc2b501 100644 --- a/patches/src/main/resources/addresources/values-fi-rFI/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-fi-rFI/youtube/strings.xml @@ -455,6 +455,15 @@ Raportit ovat julkisia ja ne tarkistetaan ennen kuin kanava lisätään listaan. Kuva tallennettu kohteeseen %s Kuvaa ei voitu ladata Tästä julkaisusta ei löydetty kuvaa + + Käännä julkaisut + Lisää ponnahdusvalikkoon kohdan sisällöntuottajien julkaisujen kääntämiseksi + Julkaisun käännöskieli + Käännä + Näytä alkuperäinen + Käännetään… + Julkaisua ei voitu kääntää + Julkaisu on jo käännöskielellä Lisää \"Kysy\"-kohde ponnahdusvalikkoon Lisää videon tiivistäminen-sivunne kohteen syötteen ponnahdusvalikossa @@ -863,6 +872,8 @@ Korjaa tämä ottamalla käyttöön 'Väärennä videovirrat' ja vaihtamalla 'Ol Korjaa transkriptio Valitse, mitkä komponentit näkyvät työkalupalkissa Näytä tekstitykset kuvan-kuvassa + Vakaat tekstityspalikat + Ryhmittele automaattiset tekstitykset vakaisiin, luettaviin palikoihin. Jotkin sanat saattavat ilmestyä ennen kuin niitä puhutaan. Käynnistys uudelleen vaaditaan. Navigointipainikkeet Piilota Luo-painike @@ -1211,6 +1222,8 @@ Rajoitus: Jos \"Palauta vanha videotoimintopalkki\" on poissa käytöstä, \"Lis Poistaa käytöstä ’Jatka katselua’ -minisoittimen, YouTube Premium -ominaisuuden, joka ilmestyy sovelluksen käynnistyessä Minisoitin Muokkaa pienennetyn videotoistimen ulkoasua ja toimintaa + Lisäminisoittimet + YouTube poisti kaikki lisäminisoittimet. Palauttaaksesi useampia minisoitin-tyyppejä, päivitä YouTube-versio 21.28.208 tai sitä vanhempi Minisoittimen tyyppi Piilota otsikko ja kanava Pois käytöstä diff --git a/patches/src/main/resources/addresources/values-fil-rPH/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-fil-rPH/shared-youtube/strings.xml index 914312aeca..383d6d34ba 100644 --- a/patches/src/main/resources/addresources/values-fil-rPH/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-fil-rPH/shared-youtube/strings.xml @@ -21,8 +21,12 @@ Second \"item\" text" Animasyon sa pagsisimula Nakapatay Kulay + Morphe Color Monokromo + Morphe Monochrome + I-highlight ang mga bagong setting + Bilang ng mga araw na ipapakita ang tuldok sa tabi ng mga setting at opsyon na idinagdag ng isang update Naka-off I-restart upang tapusin ang pag-setup diff --git a/patches/src/main/resources/addresources/values-fil-rPH/sponsorblock/strings.xml b/patches/src/main/resources/addresources/values-fil-rPH/sponsorblock/strings.xml index fd21402227..27e3c16146 100644 --- a/patches/src/main/resources/addresources/values-fil-rPH/sponsorblock/strings.xml +++ b/patches/src/main/resources/addresources/values-fil-rPH/sponsorblock/strings.xml @@ -58,6 +58,8 @@ Second \"item\" text" Nabasa ko na Ipakita sa akin Pangkalahatan + I-save ang mga segment ng mga na-download na video + Pinapayagan ang paglaktaw sa mga segment ng mga na-download na video habang offline Ipakita ang mga alerto ng pagkonekta Ipinapakita ang isang notification kapag ang SponsorBlock server ay hindi magagamit I-enable ang pagsubaybay sa bilang ng skip diff --git a/patches/src/main/resources/addresources/values-fil-rPH/youtube/strings.xml b/patches/src/main/resources/addresources/values-fil-rPH/youtube/strings.xml index 3070aeea80..735ebc30f7 100644 --- a/patches/src/main/resources/addresources/values-fil-rPH/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-fil-rPH/youtube/strings.xml @@ -455,6 +455,15 @@ Pampubliko ang mga ulat at sinusuri ang mga ito bago idagdag ang isang channel s Na-save ang imahe sa %s Hindi ma-download ang imahe Walang nakitang imahe sa post na ito + + Isalin ang mga post + Nagdaragdag ng flyout menu item para isalin ang mga post ng content creator + Wika ng pagsasalin ng post + Isalin + Ipakita ang orihinal + Isinasalin… + Hindi maisalin ang post + Ang post ay nasa wika na ng pagsasalin Idagdag ang Ask item sa flyout menu Idinadagdag ang video summarize item sa feed flyout menu @@ -863,6 +872,8 @@ Dapat ibigay ang 'Caption cookies'" Ayusin ang transcript Nilulutas ang isang failure ng precondition check na maaaring pumigil sa pag-load ng transcript Ipakita ang mga caption sa picture-in-picture + Stable na mga caption block + Igrupo ang mga automatic caption sa mga stable at madaling basahing block. Maaaring lumitaw ang ilang salita bago pa man mabigkas. Kailangang i-restart. Mga pindutan ng nabigasyon Pumili kung aling mga component ang lilitaw sa toolbar @@ -1209,6 +1220,8 @@ Limitasyon: Kung ang 'Ibalik ang lumang video action bar' ay naka-off, maaaring Hindi pinapagana ang \'Continue watching\' miniplayer, isang tampok ng YouTube Premium na lumalabas sa paglunsad ng app. Miniplayer I-customize ang hitsura at pag-uugali ng minimize video player + Karagdagang mga miniplayer + Inalis ng YouTube ang lahat ng karagdagang mga miniplayer. Upang maibalik ang higit pang mga uri ng miniplayer, i-patch ang YouTube 21.28.208 o mas luma Uri ng miniplayer Itago ang pamagat at channel Na-disable @@ -1250,6 +1263,8 @@ Limitasyon: Kung ang 'Ibalik ang lumang video action bar' ay naka-off, maaaring Pinapanatiling consistent ang layout ng status bar upang hindi ito sumakop sa mga itaas na bar at walang walang laman na espasyo sa itaas ng player. I-off ito kung crash ang app o magmukha nang mali ang mga itaas na bar Kislap + Morphe Glow 1 + Morphe Glow 2 Hindi suportado ng device na ito ang glow splash screen Likuran ng maliwanag na tema Puti diff --git a/patches/src/main/resources/addresources/values-fr-rFR/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-fr-rFR/shared-youtube/strings.xml index 5bc729a229..c414babda1 100644 --- a/patches/src/main/resources/addresources/values-fr-rFR/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-fr-rFR/shared-youtube/strings.xml @@ -25,7 +25,9 @@ Second \"item\" text" Monochrome Morphe Monochrome - Désactivé + Mettre en évidence les nouveaux paramètres + Nombre de jours pendant lesquels un point est affiché à côté des paramètres et options ajoutés par une mise à jour + Désactivée Redémarrez pour terminer la configuration diff --git a/patches/src/main/resources/addresources/values-fr-rFR/youtube/strings.xml b/patches/src/main/resources/addresources/values-fr-rFR/youtube/strings.xml index d0a4dc45a8..619f3a4905 100644 --- a/patches/src/main/resources/addresources/values-fr-rFR/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-fr-rFR/youtube/strings.xml @@ -455,6 +455,15 @@ Les signalements sont publics et sont examinés avant qu'une chaîne ne soit ajo Image enregistrée dans %s Impossible de télécharger l\'image Aucune image trouvée dans ce post + + Traduire les posts + Ajoute un élément au menu déroulant pour traduire les posts des créateurs de contenu + Langue de traduction des posts + Traduire + Afficher l\'originale + Traduction… + Impossible de traduire le post + Le post est déjà dans la langue de traduction Ajouter l\'élément \'Posez une question\' au menu déroulant Ajoute un élément de résumé vidéo au menu déroulant du flux @@ -863,6 +872,8 @@ Les 'Cookies des sous-titres' doivent être fournis" Corriger la transcription Résout une erreur de vérification de précondition qui peut empêcher le chargement de la transcription Afficher les sous-titres en mode image dans l\'image + Blocs de sous-titres stables + Groupez les sous-titres automatiques en blocs stables et lisibles. Certains mots peuvent apparaître avant d\'être prononcés. Redémarrage requis. Boutons de navigation Choisissez les boutons qui apparaissent dans la barre de navigation inférieure diff --git a/patches/src/main/resources/addresources/values-ga-rIE/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-ga-rIE/shared-youtube/strings.xml index 2f1e6806fa..1aef5da356 100644 --- a/patches/src/main/resources/addresources/values-ga-rIE/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ga-rIE/shared-youtube/strings.xml @@ -25,6 +25,8 @@ Second \"item\" text" Monacrómach Morphe Monacrómach + Cuir socruithe nua i radharc + Líon laethanta a thaispeántar പോइंट in aice le socruithe agus roghanna a chuirfí isteach trí nuashonrú As Atosaigh chun an socrú a chríochnú diff --git a/patches/src/main/resources/addresources/values-ga-rIE/sponsorblock/strings.xml b/patches/src/main/resources/addresources/values-ga-rIE/sponsorblock/strings.xml index 6d690e1e81..d7e77bd2ca 100644 --- a/patches/src/main/resources/addresources/values-ga-rIE/sponsorblock/strings.xml +++ b/patches/src/main/resources/addresources/values-ga-rIE/sponsorblock/strings.xml @@ -58,6 +58,8 @@ Second \"item\" text" Léite cheana féin Taispeáin dom Ginearálta + Sábháil segmenteda de fhíseáin íoslódáilte + Tugann sé cead segmenteda de fhíseáin íoslódáilte a sheipeáil nuair atá tú as líne Taispeáin foláirimh earráide nasc Taispeánann sé fógra nuair nach bhfuil freastalaí SponsorBlock ar fáil Cumasaigh rianú comhaireamh scipeála diff --git a/patches/src/main/resources/addresources/values-ga-rIE/youtube/strings.xml b/patches/src/main/resources/addresources/values-ga-rIE/youtube/strings.xml index 4e3c5b5ddd..b716b7b3c2 100644 --- a/patches/src/main/resources/addresources/values-ga-rIE/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ga-rIE/youtube/strings.xml @@ -455,6 +455,15 @@ Tá tuairiscí poiblí agus déantar athbhreithniú orthu sula gcuirtear cainéa Íomhá sábháilte chuig %s Níorbh fhéidir an íomhá a íoslódáil Níor aimsíodh aon íomhá sa phostáil seo + + Postaí a aistriú + Cuireannpromoting item 메뉴 flyout le postaí cruthaitheora ábhair a aistriú + Teanga aistriúcháin postaí + Aistrigh + Taispeáin an bunleagan + Ag aistriú… + Ní féidir an post a aistriú + Tá an post cheers sa teanga aistriúcháin cheana féin Cuir an mhír Fiafraigh leis an roghchlár aníos Cuireann sé mír achoimrithe físe leis an roghchlár aníos sa bhfotha @@ -863,6 +872,8 @@ Ní mór 'fianáin fotheidil' a sholáthar." Deisigh an tras-scríbhinn Réitíonn sé teip seiceála réamhchoinníoll a d’fhéadfadh cosc ​​a chur ar an trascríbhinn ó lódáil Taispeáin fotheistí i bpirseal pictiúr-i-bpictiúr + Bloclar fotheideal seasmhacha + Cruaigh fotheidil uathomatacha i mbloclar seasmhacha, léighfeartha. Seansár go bhfeicfear roinnt focal sula labharthaíodh iad. Teastaíonn atosú. Cnaipí nascleanúna Roghnaigh cé na cnaipí a thaispeántar sa bharra nascleanúna bun diff --git a/patches/src/main/resources/addresources/values-gl-rES/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-gl-rES/shared-youtube/strings.xml index 493e3dda36..c89c76c87d 100644 --- a/patches/src/main/resources/addresources/values-gl-rES/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-gl-rES/shared-youtube/strings.xml @@ -21,8 +21,12 @@ Second \"item\" text" Animación de inicio Desactivado Cor + Color de Morphe Monocromo + Morphe Monocromo + Resaltar novas configuracións + Número de días que se mostra un punto xunto ás configuracións e opcións engadidas por unha actualización Apagado Reinicia para rematar a configuración diff --git a/patches/src/main/resources/addresources/values-gl-rES/sponsorblock/strings.xml b/patches/src/main/resources/addresources/values-gl-rES/sponsorblock/strings.xml index 3d85f7e86e..4c5b86c645 100644 --- a/patches/src/main/resources/addresources/values-gl-rES/sponsorblock/strings.xml +++ b/patches/src/main/resources/addresources/values-gl-rES/sponsorblock/strings.xml @@ -58,6 +58,8 @@ Second \"item\" text" Xa lido Amósame Xeneral + Gardar segmentos de vídeos descargados + Permite saltar segmentos de vídeos descargados mentres se está sen conexión Amosar alertas de erro de conexión Amosa unha notificación cando o servidor de SponsorBlock non está dispoñible Activar o seguimento do recunto de saltos diff --git a/patches/src/main/resources/addresources/values-gl-rES/youtube/strings.xml b/patches/src/main/resources/addresources/values-gl-rES/youtube/strings.xml index 7ad4f6e4a7..6fd081632d 100644 --- a/patches/src/main/resources/addresources/values-gl-rES/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-gl-rES/youtube/strings.xml @@ -455,6 +455,15 @@ Os reportes son públicos e revísanse antes de que unha canle sexa engadida a u Imaxe gardada en %s Non foi posible descargar a imaxe Non se atopou ningunha imaxe nesta publicación + + Traducir publicacións + Engade un elemento ao menú desplegable para traducir as publicacións do creador de contidos + Idioma de tradución da publicación + Traducir + Amosar orixinal + Traducindo… + Non foi posible traducir a publicación + A publicación xa está no idioma de tradución Engadir o elemento Preguntar ao menú desplegable Engade un elemento de resumir vídeo ao menú desplegable do feed @@ -863,6 +872,8 @@ Deben proporcionarse as 'cookies de subtitulacións'" Corrixir transcrición Resolve un fallo na comprobación de precondicións que pode impedir a carga da transcrición Mostrar subtítulos en picture-in-picture + Bloques de subtítulos estables + Agrupa os subtítulos automáticos en bloques estables e lexibles. Algunhas palabras poden aparecer antes de ser pronunciadas. É necesario reiniciar. Botóns de navegación Escolla que botóns aparecen na barra de navegación inferior @@ -1211,6 +1222,8 @@ Limitación: Se 'Restaura a barra de acción de vídeo antiga' está desactivada Desactiva o miniplayer de \'Continuar vendo\', unha función de YouTube Premium que aparece ao iniciar a aplicación Mini-reprodutor Personaliza a aparencia e o comportamento do reproductor de vídeo minimizado + Miniplayers adicionais + YouTube eliminou todos os miniplayers adicionais. Para restaurar máis tipos de miniplayer, parchea YouTube 21.28.208 ou anterior Tipo de miniplayer Ocultar título e canle Desactivado @@ -1252,6 +1265,8 @@ Limitación: Se 'Restaura a barra de acción de vídeo antiga' está desactivada Mantén a disposición da barra de estado consistente, para que non cubra as barras superiores e non deixe espazo vacío sobre o reprodutor. Desactiva isto se a aplicación falla ou as barras superiores se ven mal Brillo + Morphe Glow 1 + Morphe Glow 2 Este dispositivo non admite a pantalla de inicio Glow Fondo do tema claro Branco diff --git a/patches/src/main/resources/addresources/values-gu-rIN/youtube/strings.xml b/patches/src/main/resources/addresources/values-gu-rIN/youtube/strings.xml index 4ec76b1150..063697995e 100644 --- a/patches/src/main/resources/addresources/values-gu-rIN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-gu-rIN/youtube/strings.xml @@ -61,6 +61,7 @@ Second \"item\" text" + diff --git a/patches/src/main/resources/addresources/values-hi-rIN/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-hi-rIN/shared-youtube/strings.xml index d3a39cd9a4..0d03163e00 100644 --- a/patches/src/main/resources/addresources/values-hi-rIN/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-hi-rIN/shared-youtube/strings.xml @@ -21,8 +21,12 @@ Second \"item\" text" स्टार्टअप एनीमेशन बंद किया गया रंगीन + Morphe कलर एक रंग का + Morphe मोनोक्रोम + नई सेटिंग्स को हाइलाइट करें + दिनों की संख्या जब अपडेट द्वारा जोड़ी गई सेटिंग्स और विकल्पों के बगल में एक डॉट दिखाया जाता है बंद सेटिंग को पूरा करने के लिए रीस्टार्ट करें diff --git a/patches/src/main/resources/addresources/values-hi-rIN/sponsorblock/strings.xml b/patches/src/main/resources/addresources/values-hi-rIN/sponsorblock/strings.xml index c68c0413ba..6a81dec778 100644 --- a/patches/src/main/resources/addresources/values-hi-rIN/sponsorblock/strings.xml +++ b/patches/src/main/resources/addresources/values-hi-rIN/sponsorblock/strings.xml @@ -58,6 +58,8 @@ Second \"item\" text" पहले से पढ़ लिया है मुझे दिखाएँ सामान्य + डाउनलोड किए गए वीडियो के सेगमेंट सहेजें + ऑफलाइन होने पर डाउनलोड किए गए वीडियो के सेगमेंट छोड़ने की अनुमति देता है कनेक्शन एरर के अलर्ट्स दिखाएँ जब SponsorBlock सर्वर डाउन हो तो नोटिफिकेशन दिखाए स्किप काउंट ट्रैकिंग चालू करें diff --git a/patches/src/main/resources/addresources/values-hi-rIN/youtube/strings.xml b/patches/src/main/resources/addresources/values-hi-rIN/youtube/strings.xml index 86d4b758bf..a9542ee322 100644 --- a/patches/src/main/resources/addresources/values-hi-rIN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-hi-rIN/youtube/strings.xml @@ -455,6 +455,15 @@ Second \"item\" text" छवि %s में सहेजी गई छवि डाउनलोड नहीं की जा सकी इस पोस्ट में कोई छवि नहीं मिली + + पोस्ट का अनुवाद करें + कंटेंट क्रिएटर की पोस्ट का अनुवाद करने के लिए एक फ्लाईआउट मेनू आइटम जोड़ता है + पोस्ट अनुवाद भाषा + अनुवाद करें + मूल दिखाएं + अनुवाद हो रहा है… + पोस्ट का अनुवाद नहीं किया जा सका + पोस्ट पहले से ही अनुवाद भाषा में है Ask आइटम को फ्लाउट मेनू में जोड़ें फीड फ्लाउट मेनू में वीडियो सारांश आइटम जोड़ता है @@ -863,6 +872,8 @@ OpenRouter.ai खोलने के लिए यहां टैप करे ट्रांसक्रिप्ट को ठीक करें एक पूर्व शर्त जांच विफलता को हल करता है जो ट्रांसक्रिप्ट को लोड होने से रोक सकता है पिक्चर-इन-पिक्चर में सबटाइटल्स दिखाएं + स्थिर कैप्शन ब्लॉक + स्वचालित कैप्शन को स्थिर, पढ़ने योग्य ब्लॉकों में समूहित करें। कुछ शब्द बोले जाने से पहले दिखाई दे सकते हैं। रीस्टार्ट आवश्यक है। नविगेशन बटन टूलबार में प्रदर्शित होने वाले घटकों को चुनें @@ -1211,6 +1222,8 @@ OpenRouter.ai खोलने के लिए यहां टैप करे \'जारी रखें देखें\' मिनीप्लेयर को अक्षम करता है, एक YouTube प्रीमियम सुविधा जो ऐप शुरू होने पर दिखाई देती है। मिनीप्लेयर मिनीकृत प्लेयर को कस्टमाइज़ करें + अतिरिक्त मिनीप्लेयर + YouTube ने सभी अतिरिक्त मिनीप्लेयर हटा दिए हैं। अधिक मिनीप्लेयर प्रकारों को रिस्टोर करने के लिए, YouTube 21.28.208 या उससे पुराने वर्ज़न को पैच करें मिनीप्लेयर प्रकार शीर्षक और चैनल छिपाएँ अक्षम @@ -1252,6 +1265,8 @@ OpenRouter.ai खोलने के लिए यहां टैप करे स्टेटस बार की लेआउट को स्थिर रखता है, ताकि यह टॉप बार्स को न कवर करे और प्लेयर के ऊपर खाली जगह न छोड़े। अगर ऐप क्रैश हो जाए या टॉप बार्स गलत दिखें तो इसे बंद करें ग्लो + Morphe ग्लो 1 + Morphe ग्लो 2 यह डिवाइस ग्लो स्प्लैश स्क्रीन का समर्थन नहीं करता है लाइट थीम की पृष्ठभूमि सफेद diff --git a/patches/src/main/resources/addresources/values-hr-rHR/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-hr-rHR/shared-youtube/strings.xml index f0c16d25fc..fd3560c88a 100644 --- a/patches/src/main/resources/addresources/values-hr-rHR/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-hr-rHR/shared-youtube/strings.xml @@ -21,8 +21,12 @@ Second \"item\" text" Animacija pokretanja Onemogućeno Boja + Morphe u boji Monokromatski + Morphe monokromatski + Istakni nove postavke + Broj dana tijekom kojih se točka prikazuje pokraj postavki i opcija dodanih ažuriranjem Isključeno Ponovno pokrenite da biste dovršili postavljanje diff --git a/patches/src/main/resources/addresources/values-hr-rHR/sponsorblock/strings.xml b/patches/src/main/resources/addresources/values-hr-rHR/sponsorblock/strings.xml index 9bd0a737ee..1d44f59334 100644 --- a/patches/src/main/resources/addresources/values-hr-rHR/sponsorblock/strings.xml +++ b/patches/src/main/resources/addresources/values-hr-rHR/sponsorblock/strings.xml @@ -58,6 +58,8 @@ Second \"item\" text" Već upute korisnika Prikaži Općenito + Spremi segmente preuzetih videozapisa + Omogućuje preskakanje segmenata preuzetih videozapisa dok ste izvan mreže Prikaži upozorenja o pogrešci povezivanja Prikazuje obavijest kada SponsorBlock poslužitelj nije dostupan Omogući praćenje broja preskakanja diff --git a/patches/src/main/resources/addresources/values-hr-rHR/youtube/strings.xml b/patches/src/main/resources/addresources/values-hr-rHR/youtube/strings.xml index 9f91f45c16..9fbf152391 100644 --- a/patches/src/main/resources/addresources/values-hr-rHR/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-hr-rHR/youtube/strings.xml @@ -445,6 +445,15 @@ Prijave su javne i pregledavaju se prije nego što se kanal doda na popis. Uklan Slika sačuvana u %s Slika nije mogla biti preuzeta U ovoj objavi nije pronađena slika + + Prevedi objave + Dodaje stavku u izbornik za prevođenje objava kreatora sadržaja + Jezik za prijevod objava + Prevedi + Prikaži original + Prevođenje… + Objava se nije mogla prevesti + Objava je već na jeziku za prijevod Dodaj stavku Aska u izbornik flyout Dodaje stavku za sažetak videa u izbornik flyout naloga @@ -853,6 +862,8 @@ Da biste to popravili, uključite \"Lažiranje video streamova\" i promijenite \ Popravi transkript Rješava neuspjeh provjere preduvjeta koji može spriječiti učitavanje transkripta Prikaži titlove u slici u slici + Stabilni blokovi titlova + Grupirajte automatske titlove u stabilne, čitljive blokove. Neke riječi se mogu pojaviti prije nego što budu izgovorene. Potrebno je ponovno pokretanje. Traka navigacije Odaberite koje će tipke pojaviti u donjoj navigacijskoj traci @@ -1199,6 +1210,8 @@ Ograničenje: Ako je 'Vrati staru traku s radnjama za videozapise' isključeno, Onemogućuje miniplayer \'Nastavi gledati\', značajku YouTube Premiuma koja se pojavljuje pri pokretanju aplikacije Mini pregleđ Prilagodite izgled i ponašanje minimiziranog video playera + Dodatni miniplayeri + YouTube je uklonio sve dodatne miniplayere. Kako biste vratili više vrsta miniplayera, zakrpite YouTube 21.28.208 ili stariju verziju Mini pregleđ Sakrij naslov i kanal Onemogućeno @@ -1240,6 +1253,8 @@ Ograničenje: Ako je 'Vrati staru traku s radnjama za videozapise' isključeno, Održava dosljednost rasporeda statusne trake, tako da ne prekriva gornje trake i ne ostavlja prazan prostor iznad playera. Isključite ovo ako aplikacija zapne ili gornje trake izgledaju pogrešno Sjaj + Morphe Glow 1 + Morphe Glow 2 Ovaj uređaj ne podržava Glow zaslon s početnim prikazom Boja pozadine u svijetloj temi Bijela @@ -1552,7 +1567,8 @@ Ograničenje: Vraća se na stari stil 'More videos' prekrivača" Epizode obilježene kao gledane Poništi Omogući - %1$s nove epizode + 1 nova epizoda + %1$s nove epizode Snimanje napretka na ovom uređaju "Vaše serije i napredak ostaju na ovom uređaju, dijeleći se između YouTube računa. Kontrola povijesti YouTuba ne mijenja te podatke. Očistite napredak iz izbornika Serije.\n\nAutomatsko snimanje je opcionalno. Zaustavlja se u incognito načinu rada ili kada se račun promijeni" Epizode nisu mogle biti učitane. Playlistu možete spremiti kao oznaku diff --git a/patches/src/main/resources/addresources/values-hu-rHU/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-hu-rHU/shared-youtube/strings.xml index 6c448ee61a..c64c4c62ef 100644 --- a/patches/src/main/resources/addresources/values-hu-rHU/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-hu-rHU/shared-youtube/strings.xml @@ -21,8 +21,12 @@ Second \"item\" text" Indítási animáció Kikapcsolva Színes + Morphe Szín Monokróm + Morphe Monokróm + Új beállítások kiemelése + Napok száma, amelyek alatt egy pont jelenik meg a frissítéssel hozzáadott beállítások és opciók mellett Ki Indíts újra a beállítások befejezéséhez diff --git a/patches/src/main/resources/addresources/values-hu-rHU/sponsorblock/strings.xml b/patches/src/main/resources/addresources/values-hu-rHU/sponsorblock/strings.xml index 698b6d1965..b4f71b7fd5 100644 --- a/patches/src/main/resources/addresources/values-hu-rHU/sponsorblock/strings.xml +++ b/patches/src/main/resources/addresources/values-hu-rHU/sponsorblock/strings.xml @@ -58,6 +58,8 @@ Second \"item\" text" Már elolvastam Mutasd Általános + Letöltött videók szegmenseinek mentése + Lehetővé teszi a letöltött videók szegmenseinek átugrását offline állapotban Hálózati hibaüzenetek megjelenítése Értesítést jelenít meg, ha a SponsorBlock szerver nem érhető el Átugrásszámláló bekapcsolása diff --git a/patches/src/main/resources/addresources/values-hu-rHU/youtube/strings.xml b/patches/src/main/resources/addresources/values-hu-rHU/youtube/strings.xml index e2dd2b2a5a..ef0322c448 100644 --- a/patches/src/main/resources/addresources/values-hu-rHU/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-hu-rHU/youtube/strings.xml @@ -455,6 +455,15 @@ A jelentések nyilvánosak, és felülvizsgálják őket, mielőtt egy csatorna Kép mentve a %s mappába A kép letöltése nem sikerült Ebben a bejegyzésben nem található kép + + Bejegyzések fordítása + Kiegészíti a kislegyit menúvel a tartalomgyártók bejegyzéseinek fordításához + Bejegyzés fordítási nyelve + Fordítás + Eredeti megjelenítése + Fordítás folyamatban… + Nem sikerült lefordítani a bejegyzést + A bejegyzés már a fordítási nyelvben van Ask elem hozzáadása a kizúgó menühez Video-összefoglaló elem hozzáadása az adatfolyam kizúgó menüjéhez @@ -863,6 +872,8 @@ A javításhoz kapcsolja be a „Videostreamek meghamisítása” beállítást, Átirat javítása Megoldja egy előfeltétel-ellenőrzési hibát, amely megakadályozhatja az átirat betöltését Képekben a feliratok megjelenítése pip-ben + Stabil feliratsorok + Csoportosítja az automatikus feliratokat stabil, jól olvasható blokkokba. Egyes szavak megjelenhetnek, mielőtt kimondanák őket. Újindítás szükséges. Navigációs gombok Válassza ki, mely gombok jelenjenek meg a felső eszköztáron @@ -1211,6 +1222,8 @@ Korlátozás: Ha a „Régi videós eszköztár visszaállítása” ki van kapc Letiltja a „Folytatás” minilejátszót, a YouTube Premium funkciót, amely az alkalmazás indításakor jelenik meg. Minilejátszó A minimalizált videólejátszó megjelenésének és viselkedésének testreszabása + További minimalizált lejátszók + A YouTube eltávolította az összes további minimalizált lejátszót. További minimalizált lejátszó típusok visszaállításához módosítsa a YouTube 21.28.208 vagy korábbi verzióját Minilejátszó típus Cím és csatorna elrejtése Kikapcsolva diff --git a/patches/src/main/resources/addresources/values-hy-rAM/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-hy-rAM/shared-youtube/strings.xml index a71a712c36..213c2075b6 100644 --- a/patches/src/main/resources/addresources/values-hy-rAM/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-hy-rAM/shared-youtube/strings.xml @@ -21,8 +21,13 @@ Second \"item\" text" Մեկնարկային անիմացիա Անջատված Գույն + Morphe-ի գույնը Մոնոխրոմ + Morphe Monochrome + Ընդգծել նոր կարգավորումները + Օրերի քանակը, երբ կետը ցուցադրվում է թարմացման միջոցով ավելացված կարգավորումների և ընտրությունների կողքին + Անջատված Վերագործարկեք կարգավորումներն ավարտելու համար @@ -303,8 +308,12 @@ Second \"item\" text" Անհավանությունները ժամանակավորապես հասանելի չեն. գործում է Client API-ի հարցումների սահմանափակումը rate limit API-ով ձայների ստացում, կանչերի քանակը Ցանցային կանչեր չեն կատարվել + կատարվել է %s ցանցային կանչ API-ով ձայների ստացում, ժամանակաժամկետի լիքացման timeout քանակը Ցանցային կանչերից ոչ մեկի ժամանակաժամկետը չի լիքացել + %s ցանցային կանչի ժամանակը սպառվել է API client-ի հարցումների սահմանափակումներ Client-ի հարցումների սահմանափակումներ չեն հանդիպել + Կլիենտի հարցումների սահմանափակումը հ encountered է եղել %s անգամ + %s միլիվայրկյան diff --git a/patches/src/main/resources/addresources/values-hy-rAM/sponsorblock/strings.xml b/patches/src/main/resources/addresources/values-hy-rAM/sponsorblock/strings.xml index 89c2bc62ea..246998036e 100644 --- a/patches/src/main/resources/addresources/values-hy-rAM/sponsorblock/strings.xml +++ b/patches/src/main/resources/addresources/values-hy-rAM/sponsorblock/strings.xml @@ -58,6 +58,8 @@ Second \"item\" text" Արդեն կարդացել եմ Ցույց տրվի Ընդհանուր + Պահպանել ներբեռնված տեսանյութերի հատվածները + Թույլ է տալիս բաց թողնել ներբեռնված տեսանյութերի հատվածները օֆլայն վիճակում Ցույց տալ կապի սխալների մասին ծանուցումները Ցույց է տալ ծանուցում, երբ SponsorBlock սերվերը հասանելի չէ Միացնել բաց թողնումների քանակի հետևումը diff --git a/patches/src/main/resources/addresources/values-hy-rAM/youtube/strings.xml b/patches/src/main/resources/addresources/values-hy-rAM/youtube/strings.xml index 98cbf834da..8df7182ea1 100644 --- a/patches/src/main/resources/addresources/values-hy-rAM/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-hy-rAM/youtube/strings.xml @@ -455,6 +455,15 @@ Second \"item\" text" Պատկերը պահպանվեց %s-ում Չհաջողվեց ներբեռնել պատկերը Այս հրապարակման մեջ պատկերներ չեն գտնվել + + Թարգմանել գրառումները + Ավելացնում է ցանկի կետ թերթարկող մենյուում՝ բովանդակության ստեղծողների գրառումները թարգմանելու համար + Գրառման թարգմանության լեզուն + Թարգմանել + Ցուցադրել բնօրինակը + Թարգմանվում է… + Չհաջողվեց թարգմանել գրառումը + Գրառնն արդեն թարգմանության լեզվով է Ավելացնել «Հարցրու» տարրը լողացող մենյուում Ավելացնում է տեսանյութի ամփոփման տարրը ленտայի լողացող մենյուում @@ -863,6 +872,8 @@ Second \"item\" text" Ուղղել տառադարձումը Լուծում է նախնական պայմանի ստուգման սխալը, որը կարող է խոչընդոտել տեքստի բեռնմանը Ցուցադրել ենթագրերը պատկերը-պատկերի մեջ + Կայուն ենթագրերի բլոկներ + Խմբավորում է ավտոմատ ենթագրերը կայուն, ընթեռնելի բլոկների մեջ: Որոշ բառեր կարող են հայտնվել նախքան արտասանվելը: Պահանջվում է վերագործարկում: Նավիգացիայի կոճակներ Ընտրեք, թե որ կոճirները հայտնվեն ներքևի նավիգացիոն վահանակում @@ -1211,6 +1222,8 @@ Second \"item\" text" Անջատում է \"Շարունակել դիտումը\" մինիպլեյերը՝ YouTube Premium-ի հնարավորությունը, որը հայտնվում է հավելվածի միացման ժամանակ Մինի-ընտռայց Անհատականացրեք փոքրացված տեսանյութերի նվագարկիչի տեսքն ու վարքը + Լրացուցիչ մինիպլեյերներ + YouTube-ը հեռացրել է բոլոր լրացուցիչ մինիպլեյերները: Ավելի շատ մինիպլեյերի տիպեր վերականգնելու համար փաթեթավորեք (patch) YouTube 21.28.208 կամ ավելի հին տարբերակը Մինի-ընտռայցի տիպը Թաքցնել վերնագիրը և ալիքը Անջատված @@ -1572,6 +1585,8 @@ AV1-ով տեսանյութի վերարտադրությունը կարող է Էպիզոդները նշված են որպես դիտված Չեղարկել Միացնել + 1 նոր էպիզոդ + %1$s նոր էպիզոդ Գրանցել առաջընթացը այս սարքի վրա "Ձեր սերիալները և առաջընթացը մնում են այս սարքի վրա՝ հասանելի լինելով YouTube-ի հաշիվների համար: YouTube-ի պատմության կառավարման գործիքները չեն փոխում դրանք: Մաքրեք առաջընթացը «Սերիալներ» մենյուից: diff --git a/patches/src/main/resources/addresources/values-in-rID/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-in-rID/shared-youtube/strings.xml index f6dbb56316..c8f3c0e850 100644 --- a/patches/src/main/resources/addresources/values-in-rID/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-in-rID/shared-youtube/strings.xml @@ -21,8 +21,12 @@ Second \"item\" text" Animasi startup Dinonaktifkan Berwarna + Morphe Berwarna Monokrom + Morphe Monokrom + Sorot setelan baru + Jumlah hari titik ditampilkan di samping setelan dan opsi yang ditambahkan oleh pembaruan Nonaktif Mulai ulang untuk menyelesaikan penyiapan diff --git a/patches/src/main/resources/addresources/values-in-rID/sponsorblock/strings.xml b/patches/src/main/resources/addresources/values-in-rID/sponsorblock/strings.xml index 67c2ef408d..be020f3c81 100644 --- a/patches/src/main/resources/addresources/values-in-rID/sponsorblock/strings.xml +++ b/patches/src/main/resources/addresources/values-in-rID/sponsorblock/strings.xml @@ -58,6 +58,8 @@ Second \"item\" text" Sudah dibaca Tunjukkan Umum + Simpan segmen video yang diunduh + Memungkinkan melewati segmen video yang diunduh saat luring Tampilkan peringatan kesalahan sambungan Menampilkan notifikasi saat server SponsorBlock tidak tersedia Aktifkan pelacakan melewati hitungan diff --git a/patches/src/main/resources/addresources/values-in-rID/youtube/strings.xml b/patches/src/main/resources/addresources/values-in-rID/youtube/strings.xml index 05c506f1af..0633cf6e69 100644 --- a/patches/src/main/resources/addresources/values-in-rID/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-in-rID/youtube/strings.xml @@ -455,6 +455,15 @@ Laporan bersifat publik dan ditinjau sebelum saluran ditambahkan ke daftar. Peng Gambar disimpan ke %s Tidak dapat mengunduh gambar Tidak ditemukan gambar dalam postingan ini + + Terjemahkan postingan + Menambahkan item menu flyout untuk menerjemahkan postingan pembuat konten + Bahasa terjemahan postingan + Terjemahkan + Tampilkan yang asli + Menerjemahkan… + Tidak dapat menerjemahkan postingan + Postingan sudah dalam bahasa terjemahan Tambahkan item Tanya ke menu flyout Menambahkan item ringkasan video ke menu flyout feed @@ -863,6 +872,8 @@ Untuk mengatasinya, aktifkan 'Palsukan aliran video' dan ubah 'Klien bawaan' men Perbaiki transkrip Memperbaiki prasyarat kegagalan pemeriksaan yang dapat menghalangi pemuatan transkrip Tampilkan teks bantuan dalam picture-in-picture + Blok teks tetap + Kelompokkan teks otomatis ke dalam blok yang stabil dan mudah dibaca. Beberapa kata mungkin muncul sebelum diucapkan. Diperlukan mulai ulang. Tombol navigasi Pilih tombol mana yang akan ditampilkan di bilah navigasi bawah @@ -1210,6 +1221,8 @@ Batasan: Jika 'Kembalikan bilah tindakan video lama' dinonaktifkan, hamparan 'Vi Menonaktifkan pemutar mini \'Lanjutkan menonton\', fitur YouTube Premium yang muncul saat aplikasi dimulai Pemutar mini Kustomisasi tampilan dan perilaku pemutar video yang diperkecil + Miniplayer tambahan + YouTube telah menghapus semua pemutar mini tambahan. Untuk mengembalikan lebih banyak jenis pemutar mini, terapkan tambalan pada YouTube 21.28.208 atau yang lebih lama Jenis pemutar mini Sembunyikan judul dan saluran Dinonaktifkan @@ -1251,6 +1264,8 @@ Batasan: Jika 'Kembalikan bilah tindakan video lama' dinonaktifkan, hamparan 'Vi Menjaga tata letak status bar konsisten, sehingga tidak menutupi bar atas dan tidak meninggalkan ruang kosong di atas pemutar. Matikan ini jika aplikasi mengalami crash atau bar atas terlihat salah Bercahaya + Morphe Glow 1 + Morphe Glow 2 Layar splash glow tidak didukung oleh perangkat ini Latar belakang tema terang Putih diff --git a/patches/src/main/resources/addresources/values-is-rIS/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-is-rIS/shared-youtube/strings.xml index e8d02363be..0ac4dd8189 100644 --- a/patches/src/main/resources/addresources/values-is-rIS/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-is-rIS/shared-youtube/strings.xml @@ -21,8 +21,13 @@ Second \"item\" text" Ræsingarhreyfimynd Óvirkt Litur + Morphe Litir Einstafengur + Morphe Einlitur + Áhersla á nýjar stillingar + Fjöldi daga sem punktur birtist við stillingar og valmögla sem bættust við í uppfærslu + Slökkt Endurræstu til að lokar á uppsetninguna diff --git a/patches/src/main/resources/addresources/values-is-rIS/sponsorblock/strings.xml b/patches/src/main/resources/addresources/values-is-rIS/sponsorblock/strings.xml index 504070294d..3f786035b4 100644 --- a/patches/src/main/resources/addresources/values-is-rIS/sponsorblock/strings.xml +++ b/patches/src/main/resources/addresources/values-is-rIS/sponsorblock/strings.xml @@ -58,6 +58,8 @@ Second \"item\" text" Þegar búið Sýndu mér Almennt + Vista hluta af niðurhlaðnum myndböndum + Leyfir að sleppa hluta af niðurhlaðnum myndböndum þegar enginn internetaðgangur er Sýna villuskilaboð um tengingu Sýnir tilkynningu þegar SponsorBlock netþjónninn er óskár Virkja fylgdarhald á sleppatali diff --git a/patches/src/main/resources/addresources/values-is-rIS/youtube/strings.xml b/patches/src/main/resources/addresources/values-is-rIS/youtube/strings.xml index 66923ba52e..921850ae2e 100644 --- a/patches/src/main/resources/addresources/values-is-rIS/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-is-rIS/youtube/strings.xml @@ -445,6 +445,15 @@ Tilkynningar eru opinberar og endurskoðaðar áður en rás er bætt við lista Mynd vist í %s Gat ekki hlaðið niður myndinni Engin mynd fannst í þessu færslunni + + Þýða færslur + Bætir valmuni í útsleppivalmynd til að þýða færslur efnisstofnara + Þýðingarmál færslu + Þýða + Sýna upprunalega + Þýðing vinnur... + Gat ekki þýtt færsluna + Færslan er þegar á þýðingarmálinu Bæta Ask hlut í útflytvalmynd Bætir myndbandssamantektarefni í færiborðsútflytvalynduna @@ -851,6 +860,8 @@ Til að laga þetta skaltu kveikja á 'Spoofa myndstreymi' og breyta 'Sjálfgefn Lagfæra afrit Leysir vandamál með forskotaskilyrði sem getur komið í veg fyrir að afritirinn hlaðist Sýna texta í mynd-mi mynd-tilstandi + Stöðugar textablokkir + Hópar sjálfvirkar textabreytur í stöðugum, læsilegum blokkum. Sum orð gætu komið fram áður en þau eru outgrowth. Endurræsing nauðsynleg. Stýrihnappar Veldu hvaða hnappa þú vilt sýna í neðri stjórnarborðinu @@ -1179,6 +1190,8 @@ Takmörkun: Ef 'Endurskapa gamla stjórnbara myndbands' er slökkt, gæti 'Fleir Gerir óvirkt \'Halda áfram að horfa\' litla spilara, YouTube Premium eiginleika sem birtist þegar appið ræsir Mini spilari Sérsnið útlit og hegðun minnkaðs myndavörpunar + Auknir smáspilarar + YouTube fjarlægði alla aukna smáspilara. Til að endursetja fleiri gerðir af smáspilarum, vinsamlegast pútta YouTube 21.28.208 eða eldri útgáfu Gerð Mini spilara Fela titil og rás Gerðu óvirkt @@ -1220,6 +1233,8 @@ Takmörkun: Ef 'Endurskapa gamla stjórnbara myndbands' er slökkt, gæti 'Fleir Heldir útliti stöðustikunnar í samræmi, svo hún þeki ekki efri tækin og skilur eftir sig enginn tómarúm ofan við spilarann. Slökktu á þessu ef forritið brotnar eða efri tækin líta ranglega út Glóð + Morphe Glow 1 + Morphe Glow 2 Glow-skjárstýring er ekki studduð af þessu tæki Ljósþemabakgrunnur Hvítur diff --git a/patches/src/main/resources/addresources/values-it-rIT/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-it-rIT/shared-youtube/strings.xml index 61a6514499..8cb80d72cd 100644 --- a/patches/src/main/resources/addresources/values-it-rIT/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-it-rIT/shared-youtube/strings.xml @@ -25,6 +25,8 @@ Second \"item\" text" Monocromatico Morphe Monocromatico + Evidenzia nuove impostazioni + Numero di giorni in cui viene mostrato un punto accanto alle impostazioni e opzioni aggiunte tramite un aggiornamento Spento Riavvia per completare la configurazione diff --git a/patches/src/main/resources/addresources/values-it-rIT/youtube/strings.xml b/patches/src/main/resources/addresources/values-it-rIT/youtube/strings.xml index cb7843d1e5..6db47af623 100644 --- a/patches/src/main/resources/addresources/values-it-rIT/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-it-rIT/youtube/strings.xml @@ -455,6 +455,15 @@ Le segnalazioni sono pubbliche e vengono esaminate prima che un canale venga agg Immagine salvata in %s Impossibile scaricare l\'immagine Nessuna immagine trovata in questo post + + Traduci i post + Aggiunge una voce al menu a comparsa per tradurre i post dei creatori di contenuti + Lingua di traduzione del post + Traduci + Mostra originale + Traduzione in corso… + Impossibile tradurre il post + Il post è già nella lingua di traduzione Aggiungi elemento Chiedi al menu a discesa Aggiunge un elemento di sintesi video al menu a discesa del feed @@ -863,6 +872,8 @@ Per risolvere, attiva 'Spoof dei flussi video' e cambia il 'Client predefinito' Correggi trascrizione Risolve un errore di controllo dei prerequisiti che potrebbe impedire il caricamento della trascrizione Mostra i sottotitoli in picture-in-picture + Blocchi sottotitoli stabili + Raggruppa i sottotitoli automatici in blocchi stabili e leggibili. Alcune parole potrebbero apparire prima di essere pronunciate. È necessario riavviare. Pulsanti di navigazione Scegli quali pulsanti visualizzare nella barra di navigazione inferiore diff --git a/patches/src/main/resources/addresources/values-iw-rIL/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-iw-rIL/shared-youtube/strings.xml index c24a7a412c..285bf90bbc 100644 --- a/patches/src/main/resources/addresources/values-iw-rIL/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-iw-rIL/shared-youtube/strings.xml @@ -21,8 +21,12 @@ Second \"item\" text" אנימציית אתחול מושבת צבע + Morphe צבעוני מונוכרום + Morphe מונוכרומטי + הדגשת הגדרות חדשות + מספר הימים שבהם נקודה תוצג לצד הגדרות ואפשרויות שנוספו בעדכון כבוי הפעל מחדש כדי לסיים את ההגדרות diff --git a/patches/src/main/resources/addresources/values-iw-rIL/sponsorblock/strings.xml b/patches/src/main/resources/addresources/values-iw-rIL/sponsorblock/strings.xml index 5ca07490ca..fde948cd5d 100644 --- a/patches/src/main/resources/addresources/values-iw-rIL/sponsorblock/strings.xml +++ b/patches/src/main/resources/addresources/values-iw-rIL/sponsorblock/strings.xml @@ -58,6 +58,8 @@ Second \"item\" text" כבר קראתי תראה לי כללי + שמור מקטעים של סרטונים שהורדו + מאפשר לדלג על מקטעים של סרטונים שהורדו בזמן שהמכשיר אינו מחובר לרשת הצג התראות שגיאת חיבור מציג התראה כאשר שרת SponsorBlock אינו זמין הפעל מעקב ספירת דילוגים diff --git a/patches/src/main/resources/addresources/values-iw-rIL/youtube/strings.xml b/patches/src/main/resources/addresources/values-iw-rIL/youtube/strings.xml index f54f0bb73f..96653aa873 100644 --- a/patches/src/main/resources/addresources/values-iw-rIL/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-iw-rIL/youtube/strings.xml @@ -455,6 +455,15 @@ Second \"item\" text" התמונה נשמרה ב-%s לא ניתן להוריד את התמונה לא נמצאה תמונה בפוסט זה + + תרגום פוסטים + מוסיף פריט לתפריט הצף לתרגום פוסטים של יוצרי תוכן + שפת תרגום הפוסט + תרגום + הצג מקור + מתרגם... + לא ניתן היה לתרגם את הפוסט + הפוסט כבר כתוב בשפת התרגום הוספת פריט שאל לתפריט קיבול מוסיף פריט סיכום וידאו לתפריט קיבול הזרם @@ -863,6 +872,8 @@ Second \"item\" text" תיקון תמליל פותר כשל בבדיקת תנאי מוקדם שעלול למנוע טעינה של התמלול הצג כתוביות במופע בתוך תמונה + בלוקים יציבים של כתוביות + קיבוץ כתוביות אוטומטיות לבלוקים יציבים וקריאים. ייתכן שחלק מהמילים יופיעו לפני שנאמרו. נדרשת הפעלה מחדש. לחצני ניווט בחר אילו כפתורים יופיעו בסרגל הניווט התחתון @@ -1211,6 +1222,8 @@ Second \"item\" text" משבית את הנגן המיני \'המשך לצפות\', תכונה בתשלום של YouTube Premium שמופיעה בעת הפעלת האפליקציה מיני-נגן התאם אישית את המראה וההתנהגות של נגן הווידאו הממוזער + נגני מיניเพิ่มเติม + YouTube הסירה את כל נגני המיני הנוספים. כדי לשחזר סוגי נגני מיני נוספים, יש להפעיל פאץ\' לגרסה 21.28.208 של YouTube או גרסה ישנה יותר סוג מיני-נגן הסתר כותרת וערוץ מושבת @@ -1572,7 +1585,7 @@ AVC (H.264) פרקים מסומנים כנצפים בטל הפעל - 1 פרק חדש + 1 פרק חדש %1$s פרקים חדשים תעד התקדמות במכשיר זה "הסדרה שלך וההתקדמות נשארים על מכשיר זה, משותפים בין חשבונות של YouTube. בקרות היסטוריית YouTube אינה משנות אותם. נקה התקדמות מהתפריט סדרה. diff --git a/patches/src/main/resources/addresources/values-ja-rJP/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-ja-rJP/shared-youtube/strings.xml index 4a8e595355..b489db6338 100644 --- a/patches/src/main/resources/addresources/values-ja-rJP/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ja-rJP/shared-youtube/strings.xml @@ -21,8 +21,12 @@ Second \"item\" text" 起動時のアニメーション 無効 カラー + Morphe カラー モノクロ + Morphe モノクローム + 新しい設定を強調表示 + アップデートで追加された設定やオプションの横にドットが表示される日数 オフ 設定を完了するには再起動してください diff --git a/patches/src/main/resources/addresources/values-ja-rJP/sponsorblock/strings.xml b/patches/src/main/resources/addresources/values-ja-rJP/sponsorblock/strings.xml index 5e04b9461e..d4c2e32a5d 100644 --- a/patches/src/main/resources/addresources/values-ja-rJP/sponsorblock/strings.xml +++ b/patches/src/main/resources/addresources/values-ja-rJP/sponsorblock/strings.xml @@ -58,6 +58,8 @@ Second \"item\" text" 既読 表示する その他 + ダウンロード済みビデオのセグメントを保存 + オフライン時にダウンロード済みビデオのセグメントをスキップできるようにします 接続エラーの通知を表示 SponsorBlock サーバーが利用できない場合に通知を表示します スキップデータの収集を有効化 diff --git a/patches/src/main/resources/addresources/values-ja-rJP/youtube/strings.xml b/patches/src/main/resources/addresources/values-ja-rJP/youtube/strings.xml index 6e7f8f8c69..31f313a876 100644 --- a/patches/src/main/resources/addresources/values-ja-rJP/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ja-rJP/youtube/strings.xml @@ -456,6 +456,15 @@ AI について扱っているだけのチャンネルや、動画の一部に A %s に画像を保存しました 画像のダウンロードに失敗しました この投稿には画像がありません + + 投稿を翻訳 + コンテンツクリエイターの投稿を翻訳するためのフライアウトメニュー項目を追加します + 投稿の翻訳言語 + 翻訳 + 原文を表示 + 翻訳中… + 投稿を翻訳できませんでした + 投稿はすでに翻訳先の言語で記述されています 「動画を要約して」をフライアウト メニューに追加 「この動画について質問する」欄を、欄内の「動画を要約して」がタップされた状態で開く項目を、フィードのフライアウト メニューに追加します @@ -864,6 +873,8 @@ APIキーは決して共有しないでください。アクセス権を持つ 文字起こしの不具合を修正 文字起こしの読み込み失敗の原因となる前提条件チェックのエラーを修正します ピクチャー イン ピクチャーで字幕を表示 + 安定した字幕ブロック + 自動字幕を安定して読みやすいブロックにグループ化します。一部の単語は話される前に表示される場合があります。再起動が必要です。 ナビゲーション ボタン 画面最下部のバーであるナビゲーション バーに表示するボタンを設定します @@ -1212,6 +1223,8 @@ Automotive レイアウト アプリの起動にミニプレーヤーで動画を再開する YouTube Premium 機能「続きを見る」を無効にします ミニプレーヤー アプリ内に表示される小さな画面のプレーヤーの外観と動作をカスタマイズします + 追加のミニプレーヤー + YouTube によってすべての追加のミニプレーヤーが削除されました。より多くのミニプレーヤーの種類を復元するには、YouTube 21.28.208 またはそれ以前のバージョンにパッチを適用してください ミニプレーヤーの種類 タイトルとチャンネル名を非表示 無効 diff --git a/patches/src/main/resources/addresources/values-ka-rGE/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-ka-rGE/shared-youtube/strings.xml index c1370fa392..7f9697b053 100644 --- a/patches/src/main/resources/addresources/values-ka-rGE/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ka-rGE/shared-youtube/strings.xml @@ -21,8 +21,13 @@ Second \"item\" text" გაშვების ანიმაცია გამორთული ფერი + Morphe ფერადი მონოქრომული + Morphe მონოქრომული + ახალი პარამეტრების გამოყოფა + დღეების რაოდენობა, რომლის განმავლობაშიც განახლებით დამატებულ პარამეტრებთან და ოფციებთან გამოჩნდება წერტილი + გამორთვრა გადატვირთეთ კონფიგურაციის დასასრულებლად diff --git a/patches/src/main/resources/addresources/values-ka-rGE/sponsorblock/strings.xml b/patches/src/main/resources/addresources/values-ka-rGE/sponsorblock/strings.xml index 4fc880f93f..a507a11056 100644 --- a/patches/src/main/resources/addresources/values-ka-rGE/sponsorblock/strings.xml +++ b/patches/src/main/resources/addresources/values-ka-rGE/sponsorblock/strings.xml @@ -58,6 +58,8 @@ Second \"item\" text" უკვე წაკითხულია მაჩვენე ზოგადი + ჩამოტვირთული ვიდეოების სეგმენტების შენახვა + აძლევს საშუალებას, ოფლაინ რეჟიმში ჩამოტვირთული ვიდეოების სეგმენტები გადაახტეთ კავშირის შეცდომის შეტყობინებების ჩვენება აჩვენებს შეტყობინებას, როდესაც SponsorBlock-ის სერვერი მიუწვდომელია გამოტოვების რაოდენობის თვალყურის დევნების ჩართვა diff --git a/patches/src/main/resources/addresources/values-ka-rGE/youtube/strings.xml b/patches/src/main/resources/addresources/values-ka-rGE/youtube/strings.xml index a9de56cb06..fae6bdb01d 100644 --- a/patches/src/main/resources/addresources/values-ka-rGE/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ka-rGE/youtube/strings.xml @@ -455,6 +455,15 @@ Second \"item\" text" სურათი შენახულია %s-ში სურათის ჩამოტვირთვა ვერ მოხერხდა ამ პოსტში სურათი ვერ მოიძებნა + + პოსტების თარგმნა + ამატებს ჩამოსაშლელი მენიუს ელემენტს კონტენტის ავტორების პოსტების სვთარგმნელად + პოსტის თარგმნის ენა + თარგმნა + ორიგინალის ჩვენება + თარგმნა მიმდინარეობს… + პოსტის თარგმნა ვერ მოხერხდა + პოსტი უკვე არის თარგმნის ენაზე Ask-ის ელემენტის დამატება ჩამოსაშლელ მენიუში ამატებს ვიდეოს შეჯამების ელემენტს ფიდის გასაშლელ მენიუში @@ -863,6 +872,8 @@ Second \"item\" text" ტრანსკრიპტის გასწორება ასწორებს წინაპირობის შემოწმების შეცდომას, რამაც შესაძლოა ტრანსკრიპტის ჩატვირთვა წაშალოს სუბტიტრების ჩვენება სურათში-სურათის რეჟიმში + სუბტიტრების სტაბილური ბლოკები + დააჯგუფეთ ავტომატური სუბტიტრები სტაბილურ, წაკითხვად ბლოკებად. ზოგიერთი სიტყვა შესაძლოა გამოჩნდეს მანამ, სანამ წარმოთქმულად იქნება. საჭიროა გადატვირთვა. ნავიგაციის ღილაკები აირჩიეთ, რომელი ღილაკები გამოჩნდეს ქვედა навіგაციის პანელზე @@ -1211,6 +1222,8 @@ Second \"item\" text" გათიშავს „გააგრძელეთ ყურება“-ს მინი-პლეერს, YouTube Premium-ის ფუნქციას, რომელიც ჩნდება აპლიკაციის გაშვებისას მინიპლეიერი მორგეთ მინიმალისტური ვიდეო პლეერის გარეგნული სახე და ქცევა + დამატებითი მინი-პლეიერები + YouTube-მა წაშალა ყველა დამატებითი მინი-პლეიერი. მეტი მინი-პლეიერის ტიპის აღსადგენად, დააპატჩეთ YouTube 21.28.208 ან უფრო ძველი ვერსია მინიპლეიერის ტიპი სათაურისა და არხის დამალვა გამორთვებული @@ -1572,6 +1585,7 @@ AV1-ით ვიდეოს უკუკვევა შესაძლოა ეპიზოდები მონიშნულია როგორც ნანახი გაუქმება ჩართვა + 1 ახალი ეპიზოდი %1$s ახალი ეპიზოდი პროგრესის ჩაწერა ამ მოწყობილობაზე "თქვენი სერიალები და პროგრესი რჩება ამ მოწყობილობაზე და გაზიარებულია YouTube-ის ანგარიშებს შორის. YouTube-ის ისტორიის მართვის ფუნქციები მათ არ ცვლის. პროგრესის გასუფთავება მოახდინეთ სერიალების მენიუდან. diff --git a/patches/src/main/resources/addresources/values-kk-rKZ/youtube/strings.xml b/patches/src/main/resources/addresources/values-kk-rKZ/youtube/strings.xml index e29dadd823..b8338d4b40 100644 --- a/patches/src/main/resources/addresources/values-kk-rKZ/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-kk-rKZ/youtube/strings.xml @@ -454,6 +454,7 @@ Second \"item\" text" Кескіні %s жеріне сақталды Кескінді жүктеп алу мүмкін болмады Бұл жазбада кескіндер табылмады + Жалпы мәзірге Сұрау элементін қосу Лентаның жалпы мәзіріне бейнені жинақтау элементін қосады diff --git a/patches/src/main/resources/addresources/values-km-rKH/youtube/strings.xml b/patches/src/main/resources/addresources/values-km-rKH/youtube/strings.xml index 4ec76b1150..063697995e 100644 --- a/patches/src/main/resources/addresources/values-km-rKH/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-km-rKH/youtube/strings.xml @@ -61,6 +61,7 @@ Second \"item\" text" + diff --git a/patches/src/main/resources/addresources/values-kmr-rTR/youtube/strings.xml b/patches/src/main/resources/addresources/values-kmr-rTR/youtube/strings.xml index fe027f966b..de5da71acc 100644 --- a/patches/src/main/resources/addresources/values-kmr-rTR/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-kmr-rTR/youtube/strings.xml @@ -455,6 +455,7 @@ Rapor giştî ne û berî ku kanal bikeve lîsteyê tên kontrolkirin. Jêbirin Wêne hat savekirin di %s Nekir hat wêne danê Di vê postê de tu wêne nehat dîtin + Karekê Askê lê menuyê flyout zêde bike Karekê kurteya vîdyoyê lê menuyê flyout ê feed zêde dike diff --git a/patches/src/main/resources/addresources/values-kn-rIN/youtube/strings.xml b/patches/src/main/resources/addresources/values-kn-rIN/youtube/strings.xml index 4ec76b1150..063697995e 100644 --- a/patches/src/main/resources/addresources/values-kn-rIN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-kn-rIN/youtube/strings.xml @@ -61,6 +61,7 @@ Second \"item\" text" + diff --git a/patches/src/main/resources/addresources/values-ko-rKR/youtube/strings.xml b/patches/src/main/resources/addresources/values-ko-rKR/youtube/strings.xml index b4908cb262..c9cd2825d7 100644 --- a/patches/src/main/resources/addresources/values-ko-rKR/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ko-rKR/youtube/strings.xml @@ -29,6 +29,8 @@ Second \"item\" text" 기타 동영상 이전 YouTube 설정 메뉴 복원하기 + YouTube 스낵바에 메시지 표시하기 + 플레이어 버튼 확인 메시지와 같은 Morphe 메시지를 팝업창 대신 YouTube 스낵바에 표시합니다 백그라운드 재생 제한 제거하기 백그라운드 재생 비활성화하기 @@ -156,7 +158,7 @@ Second \"item\" text" \'세밀하게 보려면 위로 당기세요\', \'손가락을 떼어 취소\', \'2x\'와 같은 플레이어 제스처 중에 표시되는 힌트를 숨깁니다 - 스낵바 (팝업메시지바) 숨기기 + 스낵바 숨기기 구독자를 위한 커뮤니티 가이드라인 숨기기 \'동영상에 동기화\' 버튼 숨기기 길게 탭하기 그라데이션 숨기기 @@ -455,6 +457,15 @@ Second \"item\" text" 이미지가 %s에 다운로드됨 이미지를 다운로드할 수 없습니다 이 게시물에서 이미지를 찾을 수 없습니다 + + 게시물 번역 + 크리에이터 게시물을 번역할 수 있는 메뉴 항목을 추가합니다 + 게시물 번역 언어 + 번역 + 원본 보기 + 번역하는 중… + 게시물을 번역할 수 없습니다 + 게시물은 이미 번역하려는 언어로 작성되어 있습니다 메뉴 구성요소에 \'질문하기\' 항목 추가하기 피드 메뉴 구성요소에 동영상을 요약할 수 있는 \'질문하기\' 항목을 추가합니다 @@ -865,6 +876,8 @@ API 키는 절대로 다른 사람과 공유해서는 안 됩니다. API 키에 스크립트 수정하기 스크립트가 불러오지 않을 수 있는 사전 조건 검사 실패 문제를 해결할 수 있습니다 PIP 모드에 자막 표시하기 + 안정적인 자막 블록 + 자동 자막을 안정적이고 읽기 쉬운 블록으로 그룹화합니다. 일부 단어는 음성으로 나오기 전에 표시될 수 있습니다. 활성화하려면 앱을 다시 시작해야 합니다 하단바 버튼 하단바에 표시할 버튼을 설정할 수 있습니다 diff --git a/patches/src/main/resources/addresources/values-ky-rKG/youtube/strings.xml b/patches/src/main/resources/addresources/values-ky-rKG/youtube/strings.xml index ff600cc82f..e33857b5e1 100644 --- a/patches/src/main/resources/addresources/values-ky-rKG/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ky-rKG/youtube/strings.xml @@ -455,6 +455,7 @@ AI жөнүндө гана талкуулаган же видеолорунун Сүрөт %s сакталды Сүрөттү жүктөп алуу мүмкүн болгон жок Бул постто сүрөт табылган жок + Суроо пунктун тез менюга кошуу Лентанын тез менюсуна видеону кыскача баяндоо пунктун кошот diff --git a/patches/src/main/resources/addresources/values-lo-rLA/youtube/strings.xml b/patches/src/main/resources/addresources/values-lo-rLA/youtube/strings.xml index 196457ac7d..218bf022a5 100644 --- a/patches/src/main/resources/addresources/values-lo-rLA/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-lo-rLA/youtube/strings.xml @@ -455,6 +455,7 @@ Second \"item\" text" ບັນທຶກຮູບພາບໃສ່ %s ແລ້ວ ບໍ່ສາມາດດາວໂຫຼດຮູບພາບໄດ້ ບໍ່ພົບຮູບພາບໃນໂພສນີ້ + ເພີ່ມລາຍການ Ask ໄປยังເມນູ flyout ເພີ່ມລາຍການສະຫຼຸບວິດີໂອໄປยังເມນູ flyout ຂອງ feed diff --git a/patches/src/main/resources/addresources/values-lt-rLT/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-lt-rLT/shared-youtube/strings.xml index a9b0e03764..b0526638d6 100644 --- a/patches/src/main/resources/addresources/values-lt-rLT/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-lt-rLT/shared-youtube/strings.xml @@ -21,8 +21,12 @@ Second \"item\" text" Paleidimo animacija Išjungta Spalva + Morphe spalvota versija Vienspalvė + Morphe monochrominė versija + Paryškinti naujus nustatymus + Kelių dienų laikotarpiu šalia nustatymų ir parinkčių, pridėtų atnaujinimo metu, bus rodomas taškas Išjungta Paleiskite iš naujo, kad užbaigtumėte sąranką diff --git a/patches/src/main/resources/addresources/values-lt-rLT/sponsorblock/strings.xml b/patches/src/main/resources/addresources/values-lt-rLT/sponsorblock/strings.xml index b47ccb76a9..da6c37ae1b 100644 --- a/patches/src/main/resources/addresources/values-lt-rLT/sponsorblock/strings.xml +++ b/patches/src/main/resources/addresources/values-lt-rLT/sponsorblock/strings.xml @@ -58,6 +58,8 @@ Second \"item\" text" Jau skaičiau Rodyti Bendras + Išsaugoti atsisiųstų vaizdo įrašų segmentus + Leidžia praleisti atsisiųstų vaizdo įrašų segmentus būnant be interneto Rodyti ryšio klaidų įspėjimus Rodo pranešimą, kai SponsorBlock serveris nepasiekiamas Įjungti praleidimų skaičiaus sekimą diff --git a/patches/src/main/resources/addresources/values-lt-rLT/youtube/strings.xml b/patches/src/main/resources/addresources/values-lt-rLT/youtube/strings.xml index 14fa40547d..cf26094a32 100644 --- a/patches/src/main/resources/addresources/values-lt-rLT/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-lt-rLT/youtube/strings.xml @@ -455,6 +455,15 @@ Pranešimai yra vieši ir peržiūrimi prieš pridedant kanalą į sąrašą. Pa Vaizdas išsaugotas į %s Nepavyko atsisiųsti vaizdo Šiame įraše vaizdo nerasta + + Vertinti įrašus + Prideda išskleidžiamojo meniu elementą turinio kūrėjų įrašams vertinti + Įrašo vertimo kalba + Vertinti + Rodyti originalą + Vertinama… + Nepavyko išversti įrašo + Įrašas jau yra vertimo kalba Pridėti „Ask“ elementą į iškylančiąjį meniu Pridėti vaizdo įrašo apibendrinimo elementą į feed\'o iškylančiąjį meniu @@ -863,6 +872,8 @@ Turi būti pateikti „Subtitrų slapukai“" Pataisyti transkripciją Išsprendžia išankstinės sąlygos patikros gedimą, kuris gali neleisti įkelti transkripcijos Rodyti antraštes vaizdo režime „vaizdas už vaizdo“ + Stabilių aprašymų blokai + Grupuoti automatinis subtitrus į stabilius, lengvai skaitomus blokus. Kai kurie žodžiai gali pasirodyti anksčiau, nei jie bus ištarti. Reikia perkrauti. Naršymo mygtukai Pasirinkite, kokie mygtukai turėtų būti rodomi apačioje esančioje naravimo juostoje @@ -1211,6 +1222,8 @@ Apribojimas: jei parinktis „Atkurti senąją vaizdo įrašo veiksmų juostą Išjungia mini grotuvą „Tęsti žiūrėjimą“, „YouTube Premium“ funkciją, rodomą paleidžiant programą Mini grotuvas Tinkinkite sumažinto vaizdo grotuvo išvaizdą ir elgesį + Papildomi minižaidalai + YouTube pašalino visus papildomus minižaidalus. Norėdami atstatyti daugiau minižaidalų tipų, pakeiskite YouTube 21.28.208 arba senesnę versiją Mini grotuvo tipas Slėpti pavadinimą ir kanalą Išjungta diff --git a/patches/src/main/resources/addresources/values-lv-rLV/youtube/strings.xml b/patches/src/main/resources/addresources/values-lv-rLV/youtube/strings.xml index f28922fb39..de7d1fff13 100644 --- a/patches/src/main/resources/addresources/values-lv-rLV/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-lv-rLV/youtube/strings.xml @@ -453,6 +453,7 @@ Ziņojumi ir publiski un tiek pārskatīti pirms kanāla pievienošanas saraksta Attēls saglabāts %s Attēla lejupielāde neizdevās Šajā ierakstā attēli netika atrasti + Pievienot \"Ask\" vienumu izlaidnotifiera izvēlnei Pievieno video kopsav𝟐uma vienumu plūsmas izlaidnotifiera izvēlnei diff --git a/patches/src/main/resources/addresources/values-mai-rIN/youtube/strings.xml b/patches/src/main/resources/addresources/values-mai-rIN/youtube/strings.xml index 4ec76b1150..063697995e 100644 --- a/patches/src/main/resources/addresources/values-mai-rIN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-mai-rIN/youtube/strings.xml @@ -61,6 +61,7 @@ Second \"item\" text" + diff --git a/patches/src/main/resources/addresources/values-mk-rMK/youtube/strings.xml b/patches/src/main/resources/addresources/values-mk-rMK/youtube/strings.xml index 85abb3038b..a09790249d 100644 --- a/patches/src/main/resources/addresources/values-mk-rMK/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-mk-rMK/youtube/strings.xml @@ -455,6 +455,7 @@ Second \"item\" text" Сликата е зачувана во %s Не може да се преземе сликата Во оваа објава нема пронајдено слики + Додај ставка „Прашај“ во менито Додава ставка за резиме на видео во менито на фидот diff --git a/patches/src/main/resources/addresources/values-mn-rMN/youtube/strings.xml b/patches/src/main/resources/addresources/values-mn-rMN/youtube/strings.xml index fd8170e6ab..928abe9223 100644 --- a/patches/src/main/resources/addresources/values-mn-rMN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-mn-rMN/youtube/strings.xml @@ -454,6 +454,7 @@ Second \"item\" text" Зураг %s хавтсанд хадгалагдлаа Зургийг татаж авж чадсангүй Энэхүү нийтлэлээс зураг олдсонгүй + Дэлгэрэнгүй цэсэнд Асуух хэсгийг нэмэх Фидийн дэлгэрэнгүй цэсэнд видеог нэгтгэн дүгнэх хэсгийг нэмнэ diff --git a/patches/src/main/resources/addresources/values-mr-rIN/youtube/strings.xml b/patches/src/main/resources/addresources/values-mr-rIN/youtube/strings.xml index 53dbbb50e5..ab3ad5d121 100644 --- a/patches/src/main/resources/addresources/values-mr-rIN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-mr-rIN/youtube/strings.xml @@ -455,6 +455,7 @@ Second \"item\" text" प्रतिमा %s मध्ये जतन केली प्रतिमा डाउनलोड करता आली नाही या पोस्टमध्ये कोणतीही प्रतिमा आढळली नाही + फ्लाईआउट मेनूमध्ये \'Ask\' आयटम जोडा फीड फ्लाईआउट मेनूमध्ये व्हिडिओ सारांश आयटम जोडते diff --git a/patches/src/main/resources/addresources/values-ms-rMY/youtube/strings.xml b/patches/src/main/resources/addresources/values-ms-rMY/youtube/strings.xml index d8929ca2b2..8c49d0f23f 100644 --- a/patches/src/main/resources/addresources/values-ms-rMY/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ms-rMY/youtube/strings.xml @@ -455,6 +455,7 @@ Laporan adalah awam dan disemak sebelum saluran ditambah ke senarai. Penyingkira Imej disimpan ke %s Tidak dapat memuat turun imej Tiada imej ditemui dalam hantaran ini + Tambah item Tanya ke menu flyout Menambah item ringkasan video ke menu flyout feed diff --git a/patches/src/main/resources/addresources/values-my-rMM/youtube/strings.xml b/patches/src/main/resources/addresources/values-my-rMM/youtube/strings.xml index 4ec76b1150..063697995e 100644 --- a/patches/src/main/resources/addresources/values-my-rMM/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-my-rMM/youtube/strings.xml @@ -61,6 +61,7 @@ Second \"item\" text" + diff --git a/patches/src/main/resources/addresources/values-nb-rNO/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-nb-rNO/shared-youtube/strings.xml index 9c52d9b9df..39408a84f0 100644 --- a/patches/src/main/resources/addresources/values-nb-rNO/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-nb-rNO/shared-youtube/strings.xml @@ -21,8 +21,13 @@ Second \"item\" text" Oppstartsanimasjon Deaktivert Farge + Morphe Farge Monokrom + Morphe Monokrom + Marker nye innstillinger + Antall dager et punkt vises ved siden av innstillinger og alternativer lagt til av en oppdatering + Av Start på nytt for å fullføre oppsettet diff --git a/patches/src/main/resources/addresources/values-nb-rNO/sponsorblock/strings.xml b/patches/src/main/resources/addresources/values-nb-rNO/sponsorblock/strings.xml index 9e18f23177..0ba660da86 100644 --- a/patches/src/main/resources/addresources/values-nb-rNO/sponsorblock/strings.xml +++ b/patches/src/main/resources/addresources/values-nb-rNO/sponsorblock/strings.xml @@ -58,6 +58,8 @@ Second \"item\" text" Allerede lest Vis meg Generelt + Lagre segmenter av nedlastede videoer + Tillater hopping over segmenter av nedlastede videoer når man er offline Vis tilkoblingsfeilvarsler Viser en varsling når SponsorBlock-serveren ikke er tilgjengelig Aktiver sporing av antall hopp diff --git a/patches/src/main/resources/addresources/values-nb-rNO/youtube/strings.xml b/patches/src/main/resources/addresources/values-nb-rNO/youtube/strings.xml index 226da9ac4c..8913f22af6 100644 --- a/patches/src/main/resources/addresources/values-nb-rNO/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-nb-rNO/youtube/strings.xml @@ -455,6 +455,15 @@ Rapporter er offentlige og blir gjennomgått før en kanal legges til en liste. Bilde lagret i %s Kunne ikke laste ned bildet Ingen bilder funnet i dette innlegget + + Oversett innlegg + Legger til et element i flyout-menyen for å oversette innlegg fra innholdsskapere + Oversettelsesspråk for innlegg + Oversett + Vis original + Oversetter… + Kunne ikke oversette innlegget + Innlegget er allerede på oversettelsesspråket Legg til Ask-objekt i uttrekksmenyen Legger til et videooppsummeringsobjekt i strømmens uttrekksmeny @@ -863,6 +872,8 @@ For å fikse dette, slå på 'Forfalsk videostrømmer' og endre 'Standardklient' Fiks transkripsjon Løser en forhåndsvilkårsjekkfeil som kan hindre transkripsjonen i å lastes inn Vis undertekster i bilde-ibilde + Stabile tekstingsblokker + Grupperer automatiske undertekster i stabile, lettleselige blokker. Enkelte ord kan vises før de blir sagt. Omstart kreves. Navigasjonsknapper Velg hvilke komponenter som vises i verktøylinjen @@ -1211,6 +1222,8 @@ Begrensning: Hvis 'Gjenopprett gammel videoaksjonslinje' er slått av, er det mu Deaktiverer minispilleren «Fortsett å se på», en YouTube Premium-funksjon som vises ved oppstart av appen Minispiller Tilpass utseendet og oppførselen til den minimerte videospilleren + Ytterligere minispillere + YouTube fjernet alle ytterligere minispillere. For å gjenopprette flere minispillertyper, må du patche YouTube 21.28.208 eller eldre Minispiller type Skjul tittel og kanal Deaktivert @@ -1252,6 +1265,8 @@ Begrensning: Hvis 'Gjenopprett gammel videoaksjonslinje' er slått av, er det mu Holder utseendet til statuslinjen konsistent, slik at den ikke dekker topparene og etterlater ingen tom plass over avspilleren. Skru dette av hvis appen krasjer eller topparene ser feil ut Glød + Morphe Glow 1 + Morphe Glow 2 Glow-sprutskjerm støttes ikke av denne enheten Bakgrunn for lyst tema Hvit diff --git a/patches/src/main/resources/addresources/values-ne-rIN/youtube/strings.xml b/patches/src/main/resources/addresources/values-ne-rIN/youtube/strings.xml index 4ec76b1150..063697995e 100644 --- a/patches/src/main/resources/addresources/values-ne-rIN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ne-rIN/youtube/strings.xml @@ -61,6 +61,7 @@ Second \"item\" text" + diff --git a/patches/src/main/resources/addresources/values-nl-rNL/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-nl-rNL/shared-youtube/strings.xml index a4cfc6a41c..272bc03200 100644 --- a/patches/src/main/resources/addresources/values-nl-rNL/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-nl-rNL/shared-youtube/strings.xml @@ -21,8 +21,12 @@ Second \"item\" text" Opstartanimatie Uitgeschakeld Kleur + Morphe Kleur Monochroom + Morphe Monochrome + Nieuwe instellingen markeren + Aantal dagen dat een stip wordt getoond naast instellingen en opties die door een update zijn toegevoegd Uit Herstarten om de installatie te voltooien diff --git a/patches/src/main/resources/addresources/values-nl-rNL/sponsorblock/strings.xml b/patches/src/main/resources/addresources/values-nl-rNL/sponsorblock/strings.xml index c804ffe9cf..3383bd4253 100644 --- a/patches/src/main/resources/addresources/values-nl-rNL/sponsorblock/strings.xml +++ b/patches/src/main/resources/addresources/values-nl-rNL/sponsorblock/strings.xml @@ -58,6 +58,8 @@ Second \"item\" text" Al gelezen Toon mij Algemeen + Segmenten van gedownloade video\'s opslaan + Hiermee kunnen segmenten van gedownloade video\'s worden overgeslagen terwijl u offline bent Toon verbindingsfoutmeldingen Toont een melding wanneer de SponsorBlock-server niet beschikbaar is Schakel het bijhouden van het aantal overslaan in diff --git a/patches/src/main/resources/addresources/values-nl-rNL/youtube/strings.xml b/patches/src/main/resources/addresources/values-nl-rNL/youtube/strings.xml index 834b205ef8..96134e86a2 100644 --- a/patches/src/main/resources/addresources/values-nl-rNL/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-nl-rNL/youtube/strings.xml @@ -455,6 +455,15 @@ Rapporten zijn openbaar en worden beoordeeld voordat een kanaal aan een lijst wo Afbeelding opgeslagen in %s De afbeelding kon niet worden gedownload Geen afbeelding gevonden in dit bericht + + Berichten vertalen + Voegt een item toe aan het fly-outmenu om berichten van contentmakers te vertalen + Vertalingstaal voor berichten + Vertalen + Origineel tonen + Vertalen... + Het bericht kon niet worden vertaald + Het bericht is al in de vertalingstaal Ask-item aan uitklapmenu toevoegen Videofatsummarisatie-item toevoegen aan het feed-uitklapmenu @@ -861,6 +870,8 @@ Om dit op te lossen, schakel 'Videostreams vervalsen' in en wijzig de 'Standaard Transcriptie corrigeren Lost een voorwaarde-controlefout op die kan voorkomen dat het transcript wordt geladen Bijschriften weergeven in picture-in-picture + Stabiele bijschriftblokken + Groepeer automatische ondertiteling in stabiele, leesbare blokken. Sommige woorden kunnen verschijnen voordat ze worden uitgesproken. Herstarten is vereist. Navigatieknoppen Kies welke componenten er in de werkbalk verschijnen @@ -1209,6 +1220,8 @@ Beperking: Als 'Herstel oude video acties balk' is uitgeschakeld, is de \"Meer v Schakelt de \'Doorgaan met kijken\'-minispeler uit, een functie van YouTube Premium die verschijnt bij het opstarten van de app Minispeler Pas het uiterlijk en het gedrag van de geminimaliseerde videospeler aan + Extra miniplayers + YouTube heeft alle extra miniplayers verwijderd. Om meer miniplayer-typen te herstellen, patch dan YouTube 21.28.208 of ouder \"miniplayer\"-type Verberg titel en kanaal Uitgeschakeld diff --git a/patches/src/main/resources/addresources/values-or-rIN/youtube/strings.xml b/patches/src/main/resources/addresources/values-or-rIN/youtube/strings.xml index 4ec76b1150..063697995e 100644 --- a/patches/src/main/resources/addresources/values-or-rIN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-or-rIN/youtube/strings.xml @@ -61,6 +61,7 @@ Second \"item\" text" + diff --git a/patches/src/main/resources/addresources/values-pa-rIN/youtube/strings.xml b/patches/src/main/resources/addresources/values-pa-rIN/youtube/strings.xml index 4ec76b1150..063697995e 100644 --- a/patches/src/main/resources/addresources/values-pa-rIN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-pa-rIN/youtube/strings.xml @@ -61,6 +61,7 @@ Second \"item\" text" + diff --git a/patches/src/main/resources/addresources/values-pl-rPL/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-pl-rPL/shared-youtube/strings.xml index 42232f8f45..ac317c470f 100644 --- a/patches/src/main/resources/addresources/values-pl-rPL/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-pl-rPL/shared-youtube/strings.xml @@ -25,6 +25,8 @@ Second \"item\" text" Monochromatyczna Monochromatyczna Morphe + Wyróżnij nowe ustawienia + Liczba dni, przez które kropka jest wyświetlana obok ustawień i opcji dodanych w aktualizacji Wyłączone Uruchom ponownie, aby dokończyć konfigurację diff --git a/patches/src/main/resources/addresources/values-pl-rPL/sponsorblock/strings.xml b/patches/src/main/resources/addresources/values-pl-rPL/sponsorblock/strings.xml index 6aec035eaa..aff4b2f6d9 100644 --- a/patches/src/main/resources/addresources/values-pl-rPL/sponsorblock/strings.xml +++ b/patches/src/main/resources/addresources/values-pl-rPL/sponsorblock/strings.xml @@ -58,6 +58,8 @@ Second \"item\" text" Przeczytano Pokaż mi Ogólne + Zapisuj segmenty pobranych filmów + Pozwala na pomijanie segmentów pobranych filmów w trybie offline Pokaż powiadomienia o błędach połączenia Wyświetla powiadomienie, gdy serwer SponsorBlock jest niedostępny Włącz śledzenie liczby pominięć diff --git a/patches/src/main/resources/addresources/values-pl-rPL/youtube/strings.xml b/patches/src/main/resources/addresources/values-pl-rPL/youtube/strings.xml index 4dfb6ca55c..cd101d434a 100644 --- a/patches/src/main/resources/addresources/values-pl-rPL/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-pl-rPL/youtube/strings.xml @@ -455,6 +455,15 @@ Zgłoszenia są publiczne i podlegają weryfikacji przed dodaniem kanału do lis Zapisano obraz w %s Nie udało się pobrać obrazu Nie znaleziono obrazu w tym poście + + Tłumacz posty + Dodaje element do menu rozwijanego w celu tłumaczenia postów twórców treści + Język tłumaczenia postów + Przetłumacz + Pokaż oryginał + Tłumaczenie… + Nie udało się przetłumaczyć posta + Post jest już w języku tłumaczenia Dodaj opcję \"Zapytaj\" do menu Dodaje opcję podsumowania filmu do menu na liście @@ -863,6 +872,8 @@ Uwaga: Może nie działać na transmisjach na żywo" Napraw transkrypcję Rozwiązuje błąd weryfikacji warunków wstępnych, który może uniemożliwiać wczytanie transkrypcji Pokaż napisy w trybie \"Obraz w obrazie\" + Stabilne bloki napisów + Grupuj automatyczne napisy w stabilne, czytelne bloki. Niektóre słowa mogą pojawić się przed ich wypowiedzeniem. Wymagany restart. Przyciski nawigacji Zdecyduj, które przyciski będą wyświetlane na dolnym pasku nawigacji @@ -1210,6 +1221,8 @@ Ograniczenie: Jeśli opcja \"Przywróć stary pasek akcji filmu\" jest wyłączo Wyłącza miniodtwarzacz \"Kontynuuj oglądanie\", funkcję YouTube Premium wyświetlaną przy starcie aplikacji Miniodtwarzacz Dostosuj wygląd i zachowanie miniodtwarzacza + Dodatkowe miniodtwarzacze + YouTube usunął wszystkie dodatkowe miniodtwarzacze. Aby przywrócić więcej typów miniodtwarzaczy, zmodyfikuj YouTube w wersji 21.28.208 lub starszej Typ miniodtwarzacza Ukryj tytuł i kanał Wyłączony diff --git a/patches/src/main/resources/addresources/values-pt-rBR/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-pt-rBR/shared-youtube/strings.xml index f877158b74..8c28d4ed37 100644 --- a/patches/src/main/resources/addresources/values-pt-rBR/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-pt-rBR/shared-youtube/strings.xml @@ -21,8 +21,12 @@ Second \"item\" text" Animação de inicialização Desativado Cor + Cor do Morphe Monocromático + Morphe Monocromático + Destacar novas configurações + Número de dias que um ponto é exibido ao lado de configurações e opções adicionadas por uma atualização Desativado Reinicie para finalizar a configuração diff --git a/patches/src/main/resources/addresources/values-pt-rBR/sponsorblock/strings.xml b/patches/src/main/resources/addresources/values-pt-rBR/sponsorblock/strings.xml index 2172455dcb..a697727a25 100644 --- a/patches/src/main/resources/addresources/values-pt-rBR/sponsorblock/strings.xml +++ b/patches/src/main/resources/addresources/values-pt-rBR/sponsorblock/strings.xml @@ -58,6 +58,8 @@ Second \"item\" text" Já lido Me mostre Geral + Salvar segmentos de vídeos baixados + Permite pular segmentos de vídeos baixados enquanto estiver offline Mostrar alertas de erro de conexão Mostra uma notificação quando o servidor SponsorBlock estiver indisponível Ativar rastreamento de contagem de pulos diff --git a/patches/src/main/resources/addresources/values-pt-rBR/youtube/strings.xml b/patches/src/main/resources/addresources/values-pt-rBR/youtube/strings.xml index 8a1593695b..0ec76452d0 100644 --- a/patches/src/main/resources/addresources/values-pt-rBR/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-pt-rBR/youtube/strings.xml @@ -455,6 +455,15 @@ As denúncias são públicas e passam por revisão antes de um canal ser adicion Imagem salva em %s Não foi possível baixar a imagem Nenhuma imagem encontrada nesta publicação + + Traduzir publicações + Adiciona um item ao menu suspenso para traduzir publicações de criadores de conteúdo + Idioma de tradução da publicação + Traduzir + Mostrar original + Traduzindo… + Não foi possível traduzir a publicação + A publicação já está no idioma de tradução Adicionar item Perguntar ao menu flutuante Adiciona um item de resumo de vídeo ao menu flutuador do feed @@ -863,6 +872,8 @@ Os 'cookies de legenda' devem ser fornecidos" Corrigir transcrição Resolve uma falha na verificação de pré-condições que pode impedir o carregamento da transcrição Mostrar legendas em modo picture-in-picture + Blocos de legendas estáveis + Agrupa legendas automáticas em blocos estáveis e legíveis. Algumas palavras podem aparecer antes de serem faladas. Reinicialização necessária. Botões de navegação Escolha quais botões aparecem na barra de navegação inferior @@ -1210,6 +1221,8 @@ Limitação: Se 'Restaurar barra de ações de vídeo antiga' estiver desativado Desativa o miniplayer \'Continuar assistindo\', um recurso do YouTube Premium que aparece na inicialização do app Miniplayer Personalize a aparência e o comportamento do mini player minimizado + Miniplayers adicionais + O YouTube removeu todos os miniplayers adicionais. Para restaurar mais tipos de miniplayer, faça o patch do YouTube 21.28.208 ou versões anteriores Tipo de miniplayer Ocultar título e canal Desativado @@ -1251,6 +1264,8 @@ Limitação: Se 'Restaurar barra de ações de vídeo antiga' estiver desativado Mantém o layout da barra de status consistente, para que não cubra as barras superiores e não deixe espaço vazio acima do player. Desative esta opção se o aplicativo travar ou se as barras superiores parecerem incorretas Brilho + Morphe Glow 1 + Morphe Glow 2 A tela de abertura Glow não é suportada por este dispositivo Fundo do tema claro Branco diff --git a/patches/src/main/resources/addresources/values-pt-rPT/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-pt-rPT/shared-youtube/strings.xml index e3d6cac676..b4754c0b64 100644 --- a/patches/src/main/resources/addresources/values-pt-rPT/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-pt-rPT/shared-youtube/strings.xml @@ -21,8 +21,13 @@ Second \"item\" text" Animação de inicialização Desativado Cor + Morphe Color Monocromático + Morphe Monocromático + Destacar novas configurações + Número de dias que um ponto é exibido ao lado de configurações e opções adicionadas por uma atualização + Desativado Reiniciar para finalizar a configuração diff --git a/patches/src/main/resources/addresources/values-pt-rPT/sponsorblock/strings.xml b/patches/src/main/resources/addresources/values-pt-rPT/sponsorblock/strings.xml index 56ef24bc43..ff4b5090c3 100644 --- a/patches/src/main/resources/addresources/values-pt-rPT/sponsorblock/strings.xml +++ b/patches/src/main/resources/addresources/values-pt-rPT/sponsorblock/strings.xml @@ -58,6 +58,8 @@ Second \"item\" text" Já lido Mostre-me Geral + Salvar segmentos de vídeos baixados + Permite pular segmentos de vídeos baixados enquanto estiver offline Mostrar alertas de erro de conexão Mostra uma notificação quando o servidor SponsorBlock não estiver disponível Ativar rastreamento de ignorar contagem diff --git a/patches/src/main/resources/addresources/values-pt-rPT/youtube/strings.xml b/patches/src/main/resources/addresources/values-pt-rPT/youtube/strings.xml index 282b02d06d..4e986c45be 100644 --- a/patches/src/main/resources/addresources/values-pt-rPT/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-pt-rPT/youtube/strings.xml @@ -455,6 +455,15 @@ As denúncias são públicas e são analisadas antes de um canal ser adicionado Imagem salva em %s Não foi possível baixar a imagem Nenhuma imagem encontrada nesta publicação + + Traduzir publicações + Adiciona um item ao menu suspenso para traduzir publicações de criadores de conteúdo + Idioma de tradução da publicação + Traduzir + Mostrar original + Traduzindo… + Não foi possível traduzir a publicação + A publicação já está no idioma de tradução Adicionar item Perguntar ao menu suspenso Adiciona um item de resumo de vídeo ao menu suspenso do feed @@ -863,6 +872,8 @@ Para corrigir isso, ative 'Falsificar streams de vídeo' e altere o 'Cliente pad Corrigir transcrição Resolve uma falha na verificação de pré-condição que pode impedir o carregamento da transcrição Mostrar legendas em modo de imagem sobre imagem + Blocos de legendas estáveis + Agrupa legendas automáticas em blocos estáveis e legíveis. Algumas palavras podem aparecer antes de serem faladas. Reinicialização necessária. Botões de navegação Escolha quais botões aparecem na barra de ferramentas @@ -1211,6 +1222,8 @@ Limitação: Se 'Restaurar a barra de ação de vídeo antiga' estiver desativad Desativa o miniplayer \"Continuar assistindo\", um recurso do YouTube Premium que aparece na inicialização do aplicativo Minijogador Personalize a aparência e o comportamento do miniplayer minimizado + Miniplayers adicionais + O YouTube removeu todos os miniplayers adicionais. Para restaurar mais tipos de miniplayer, faça o patch do YouTube 21.28.208 ou anterior Tipo de minijogador Ocultar título e canal Desativado @@ -1252,6 +1265,8 @@ Limitação: Se 'Restaurar a barra de ação de vídeo antiga' estiver desativad Mantém o layout da barra de status consistente, para que não cubra as barras superiores e não deixe espaço vazio acima do player. Desative isso se o aplicativo travar ou se as barras superiores estiverem incorretas Brilho + Morphe Glow 1 + Morphe Glow 2 A tela de abertura Glow não é suportada por este dispositivo Cor de fundo do tema claro Branco diff --git a/patches/src/main/resources/addresources/values-ro-rRO/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-ro-rRO/shared-youtube/strings.xml index e5704a8698..eaaaa0de99 100644 --- a/patches/src/main/resources/addresources/values-ro-rRO/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ro-rRO/shared-youtube/strings.xml @@ -21,8 +21,12 @@ Second \"item\" text" Animație de pornire Dezactivat Culoare + Culoarea Morphe Monocrom + Morphe Monocrom + Evidențiază setările noi + Numărul de zile în care un punct este afișat lângă setările și opțiunile adăugate printr-o actualizare Oprit Reporniți pentru a finaliza configurarea diff --git a/patches/src/main/resources/addresources/values-ro-rRO/sponsorblock/strings.xml b/patches/src/main/resources/addresources/values-ro-rRO/sponsorblock/strings.xml index d3cfa3770f..1a9bbaa419 100644 --- a/patches/src/main/resources/addresources/values-ro-rRO/sponsorblock/strings.xml +++ b/patches/src/main/resources/addresources/values-ro-rRO/sponsorblock/strings.xml @@ -58,6 +58,8 @@ Second \"item\" text" Deja citite Arată-mi Generalități + Salvează segmentele din videoclipurile descărcate + Permite sărirea peste segmentele videoclipurilor descărcate în modul offline Afișează alerte de eroare de conexiune Afișează o notificare atunci când serverul SponsorBlock este indisponibil Activați omiterea peste urmărire diff --git a/patches/src/main/resources/addresources/values-ro-rRO/youtube/strings.xml b/patches/src/main/resources/addresources/values-ro-rRO/youtube/strings.xml index d9e6e598cb..952e792ecf 100644 --- a/patches/src/main/resources/addresources/values-ro-rRO/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ro-rRO/youtube/strings.xml @@ -455,6 +455,15 @@ Rapoartele sunt publice și sunt analizate înainte ca un canal să fie adăugat Imaginea a fost salvată în %s Imaginea nu a putut fi descărcată Nu s-a găsit nicio imagine în această postare + + Traducere postări + Adaugă un element în meniul flyout pentru a traduce postările creatorilor de conținut + Limba de traducere a postării + Traduce + Afișează originalul + Se traduce… + Postarea nu a putut fi tradusă + Postarea este deja în limba de traducere Adăugarea elementului Ask la meniul flyout Adaugă un element de sumarizare video în meniul flyout al șirului de alimentare @@ -863,6 +872,8 @@ Trebuie furnizate „Cookie-uri pentru subtitrări”" Corectează transcriere Elimină o eroare de verificare a precondițiilor care poate împiedica încărcarea transcrierii Arată subtitrările în picture-in-picture + Blocuri de subtitrări stabile + Gruparea subtitrărilor automate în blocuri stabile și lizibile. Unele cuvinte pot apărea înainte de a fi rostite. Este necesară repornirea. Butoane de navigare Ascunde subsolul meniului Pista audio @@ -1211,6 +1222,8 @@ Limitare: Dacă 'Restaurare bara de acțiuni video veche' este dezactivată, str Dezactivează playerul mini „Urmărește mai departe”, o funcție YouTube Premium care apare la pornirea aplicației Minijucător Personalizează aspectul și comportamentul miniplayerului mic + Miniplayere suplimentare + YouTube a eliminat toate miniplayerele suplimentare. Pentru a restaura mai multe tipuri de miniplayer, patch-uiește YouTube 21.28.208 sau o versiune mai veche Tip minijucător Ascunde titlul și canalul Dezactivat @@ -1252,6 +1265,8 @@ Limitare: Dacă 'Restaurare bara de acțiuni video veche' este dezactivată, str Păstrează aspectul fermei de stare constant, astfel încât să nu acopere barele superioare și să lase spațiu liber deasupra player-ului. Dezactivează această opțiune dacă aplicația se blochează sau dacă barele superioare arată incorect Strălucire + Morphe Glow 1 + Morphe Glow 2 Ecranul de pornire Glow nu este suportat de acest dispozitiv Fundalul temei luminoase Alb diff --git a/patches/src/main/resources/addresources/values-ru-rRU/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-ru-rRU/shared-youtube/strings.xml index 8ccad55aaa..a283b571a3 100644 --- a/patches/src/main/resources/addresources/values-ru-rRU/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ru-rRU/shared-youtube/strings.xml @@ -25,6 +25,8 @@ Second \"item\" text" Монохромный Morphe Монохромный + Подсвечивать новые настройки + Количество дней, в течение которых точка отображается рядом с настройками и опциями, добавленными в обновлении Выключено Перезагрузите для завершения настройки diff --git a/patches/src/main/resources/addresources/values-ru-rRU/sponsorblock/strings.xml b/patches/src/main/resources/addresources/values-ru-rRU/sponsorblock/strings.xml index e623d93bbe..8a9fe353fb 100644 --- a/patches/src/main/resources/addresources/values-ru-rRU/sponsorblock/strings.xml +++ b/patches/src/main/resources/addresources/values-ru-rRU/sponsorblock/strings.xml @@ -58,6 +58,8 @@ Second \"item\" text" Уже прочитано Показать Общие настройки + Сохранять сегменты загруженных видео + Позволяет пропускать сегменты загруженных видео в автономном режиме Показывать оповещения об ошибках подключения Показывает уведомление, когда сервер SponsorBlock недоступен Включить подсчет количества пропусков diff --git a/patches/src/main/resources/addresources/values-ru-rRU/youtube/strings.xml b/patches/src/main/resources/addresources/values-ru-rRU/youtube/strings.xml index 41634a5c7d..5eb3587f69 100644 --- a/patches/src/main/resources/addresources/values-ru-rRU/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ru-rRU/youtube/strings.xml @@ -455,6 +455,15 @@ Second \"item\" text" Изображение сохранено в %s Не удалось загрузить изображение В этом посте изображения не найдены + + Перевод публикаций + Добавляет пункт в выпадающее меню для перевода публикаций авторов контента + Язык перевода публикаций + Перевести + Показать оригинал + Перевод... + Не удалось перевести публикацию + Публикация уже на языке перевода Добавить Ask в меню Добавляет элемент сводки видео в меню ленты @@ -865,6 +874,8 @@ Second \"item\" text" Исправить транскрипт Устраняет сбой проверки предварительных условий, который может помешать загрузке транскрипта Показывать субтитры в режиме \"картинка в картинке\" + Стабильные блоки субтитров + Группирует автоматические субтитры в стабильные, читаемые блоки. Некоторые слова могут появляться до того, как они будут произнесены. Требуется перезапуск. Кнопки навигации Выберите, какие кнопки отображаются на панели инструментов @@ -1575,6 +1586,7 @@ AVC (H.264) Эпизоды отмечены как просмотренные Вернуть Включить + 1 новый эпизод %1$s новые эпизоды Записывать прогресс на этом устройстве "Ваши сериалы и прогресс остаются на этом устройстве, общая для учетных записей YouTube. Элементы истории YouTube не меняют их. Очищайте прогресс из меню «Серии». diff --git a/patches/src/main/resources/addresources/values-si-rLK/youtube/strings.xml b/patches/src/main/resources/addresources/values-si-rLK/youtube/strings.xml index 4ec76b1150..063697995e 100644 --- a/patches/src/main/resources/addresources/values-si-rLK/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-si-rLK/youtube/strings.xml @@ -61,6 +61,7 @@ Second \"item\" text" + diff --git a/patches/src/main/resources/addresources/values-sk-rSK/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-sk-rSK/shared-youtube/strings.xml index f1053e5214..0f8b773622 100644 --- a/patches/src/main/resources/addresources/values-sk-rSK/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-sk-rSK/shared-youtube/strings.xml @@ -21,8 +21,12 @@ Second \"item\" text" Animácia spustenia Vypnutý Farebná + Morphe Farba Monochromatické + Morphe Monochromatický + Zdôrazniť nové nastavenia + Počet dní, počas ktorých sa vedľa nastavení a možností pridaných aktualizáciou zobrazuje bod Vypnuté Reštartujte aplikáciu, aby sa dokončila konfigurácia diff --git a/patches/src/main/resources/addresources/values-sk-rSK/sponsorblock/strings.xml b/patches/src/main/resources/addresources/values-sk-rSK/sponsorblock/strings.xml index 23b369f7b4..640256ccd4 100644 --- a/patches/src/main/resources/addresources/values-sk-rSK/sponsorblock/strings.xml +++ b/patches/src/main/resources/addresources/values-sk-rSK/sponsorblock/strings.xml @@ -58,6 +58,8 @@ Second \"item\" text" Už som si prečítal Zobraziť mi Všeobecné + Ukladať segmenty stiahnutých videí + Umožňuje preskakovať segmenty stiahnutých videí v režime offline Zobraziť upozornenia na chyby pripojenia Zobrazuje upozornenie, keď server SponsorBlock nie je k dispozícii Povoliť sledovanie počtu preskočení diff --git a/patches/src/main/resources/addresources/values-sk-rSK/youtube/strings.xml b/patches/src/main/resources/addresources/values-sk-rSK/youtube/strings.xml index e955ba7221..b677083cad 100644 --- a/patches/src/main/resources/addresources/values-sk-rSK/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-sk-rSK/youtube/strings.xml @@ -455,6 +455,15 @@ Hlásenia sú verejné a pred pridaním kanála do zoznamu sa posudzujú. Odstr Obrázok uložený do %s Nebolo možné sťahovať obrázok V tomto príspevku nebol nájdený žiadny obrázok + + Prekladať príspevky + Pridáva položku do vysúvacieho menu na preklad príspevkov tvorcov obsahu + Jazyk prekladu príspevku + Preložiť + Zobraziť originál + Preklad prebieha… + Nepodarilo sa preložiť príspevok + Príspevok je už v cieľovom jazyku prekladu Pridať položku Opýtať sa do rozbaľovacieho menu Pridá položku na zhrnutie videa do rozbaľovacieho menu predplatného @@ -863,6 +872,8 @@ Je potrebné poskytnúť 'Cookies pre titulky'" Opraviť prepis Rieši zlyhanie kontroly predpokladov, ktoré môže zabrániť načítaniu prepisu Zobraziť titulky v režime obraz v obraze + Stabilné bloky popisov + Automatické titulky zгрупujte do stabilných a čitateľných blokov. Niektoré slová sa môžu zobraziť predtým, než budú vyslovené. Vyžaduje sa restart. Navigačné tlačidlá Vyberte, ktoré tlačidlá sa zobrazia v paneli nástrojov @@ -1211,6 +1222,8 @@ Obmedzenie: Ak je vypnutá možnosť 'Obnoviť starú lištu akcií videa', vrst Zakáže miniplayer s pokračovaním sledovania, funkciu YouTube Premium, ktorá sa zobrazuje pri spustení aplikácie Miniprehrávač Prispôsobte vzhľad a správanie zmenšeného prehrávača videa + Ďalšie miniplayerov + YouTube odstránilo všetky ďalšie miniplayery. Ak chcete obnoviť viac typov miniplayerov, patchujte YouTube 21.28.208 alebo staršie verzie Typ miniprehrávača Skryť názov a kanál Vypnutý @@ -1252,6 +1265,8 @@ Obmedzenie: Ak je vypnutá možnosť 'Obnoviť starú lištu akcií videa', vrst Udržuje konzistentné rozloženie stavového riadka, takže nezakrýva horné panely a nenecháva prázdny priestor nad prehrávačom. Toto vypnite, ak sa aplikácia zrúti alebo horné panely vyzerajú nesprávne Žiarenie + Morphe Glow 1 + Morphe Glow 2 Tento zariadenie nepodporuje žiarivý úvodný obrazovku Pozadie svetlého motívu Biela diff --git a/patches/src/main/resources/addresources/values-sl-rSI/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-sl-rSI/shared-youtube/strings.xml index b12d13f3ab..d437982e59 100644 --- a/patches/src/main/resources/addresources/values-sl-rSI/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-sl-rSI/shared-youtube/strings.xml @@ -21,8 +21,13 @@ Second \"item\" text" Zagonska animacija Onemogočeno Barva + Morphe v barvah Enobarvno + Morphe monochromaticna + Izpostavi nove nastavitve + Število dni, ko je ob nastavitvah in možnostih, dodanih z posodobitvijo, prikazana piska + Izklopljeno Za dokončanje nastavitve znova zaženite aplikacijo diff --git a/patches/src/main/resources/addresources/values-sl-rSI/sponsorblock/strings.xml b/patches/src/main/resources/addresources/values-sl-rSI/sponsorblock/strings.xml index 178256a500..c06335b0cb 100644 --- a/patches/src/main/resources/addresources/values-sl-rSI/sponsorblock/strings.xml +++ b/patches/src/main/resources/addresources/values-sl-rSI/sponsorblock/strings.xml @@ -58,6 +58,8 @@ Second \"item\" text" Že sem prebral Pokaži mi Splošno + Shrani segmente prenesenih videoposnetkov + Omogoča preskakovanje segmentov prenesenih videoposnetkov brez povezave Pokaži opozorila o napaki povezave Pokaže obvestilo, ko je strežnik SponsorBlock nedostopen Omogoči sledenje števila preskokov diff --git a/patches/src/main/resources/addresources/values-sl-rSI/youtube/strings.xml b/patches/src/main/resources/addresources/values-sl-rSI/youtube/strings.xml index 0fa1b33694..765424a580 100644 --- a/patches/src/main/resources/addresources/values-sl-rSI/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-sl-rSI/youtube/strings.xml @@ -455,6 +455,15 @@ Prijave so javne in so pregledane, preden je kanal dodan na seznam. Odstranitev Slika shranjena v %s Slike ni bilo mogoče prenesti V tem objavi ni bilo najdene slike + + Prevedi objave + Dodaja možnost v pojavni meni za prevajanje objav ustvarjalcev vsebine + Jezik za prevajanje objav + Prevedi + Prikaži izvirnik + Prevajanje… + Objave ni bilo mogoče prevajati + Objava je že v jeziku prevajanja Dodaj element Zastavi v meni za hitro klicanje V meni za hitro klicanje glavnega toka dodajte element za povzetek videa @@ -863,6 +872,8 @@ Opozorilo: morda ne deluje na prenosih v živo" Popravi prepis Odpravlja preverjanje pogojev, ki lahko prepreči nalaganje transkripcije Prikaži podnapise v načinu slika v sliki + Stabilni bloki podnapisov + Avtomatske podnapise združi v stabilne, berljive bloke. Nekatere besede se morda pojavijo, preden so izgovorene. Zaževalni ponoven zagon je potreben. Nadzorni gumbi Izberite, kateri elementi se pojavljajo v orodni vrstici @@ -1211,6 +1222,8 @@ Omejitev: Če je možnost »Ponastavi staro vrstico za videoposnetke« izkloplje Onemogoči mini predvajalnik »Nadaljuj z gledanjem«, funkcijo YouTube Premium, ki se prikaže ob zagonu aplikacije Mini predvajalnik Prilagodite videz in obnašanje minimiziranega predajalnika videa + Dodatni mini predvoravatelji + YouTube je odstranil vse dodatne mini predvoravatelje. Če želite ponovno pridobiti več vrst mini predvoravateljev, nato zakrpajte YouTube 21.28.208 ali starejšjo različico Vrsta predvajalnika Skrij naslov in kanal Onemogočen @@ -1252,6 +1265,8 @@ Omejitev: Če je možnost »Ponastavi staro vrstico za videoposnetke« izkloplje Ohranja dosledno zasnovo vrstice stanja, tako da ne prekriva zgornjih vrstic in ne pušča praznega prostora nad predvajalnikom. To funkcijo izklopite, če se aplikacija sesuje ali zgornje vrstice ne izgledajo pravilno Sijaj + Morphe Glow 1 + Morphe Glow 2 Ta naprava ne podpira zaslona s sijajem Ozadje svetle teme Bela diff --git a/patches/src/main/resources/addresources/values-sr-rCS/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-sr-rCS/shared-youtube/strings.xml index fc5c70033b..ac5cf1d160 100644 --- a/patches/src/main/resources/addresources/values-sr-rCS/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-sr-rCS/shared-youtube/strings.xml @@ -21,8 +21,12 @@ Second \"item\" text" Animacija pri pokretanju Onemogućen Boja + Morphe boja Monohromski + Morphe Monohrom + Istakni nova podešavanja + Broj dana tokom kojih se tačka prikazuje pored podešavanja i opcija dodatih ažuriranjem Isključen Ponovo pokrenite da biste završili postavljanje diff --git a/patches/src/main/resources/addresources/values-sr-rCS/sponsorblock/strings.xml b/patches/src/main/resources/addresources/values-sr-rCS/sponsorblock/strings.xml index 23735717bd..2085ce6ba2 100644 --- a/patches/src/main/resources/addresources/values-sr-rCS/sponsorblock/strings.xml +++ b/patches/src/main/resources/addresources/values-sr-rCS/sponsorblock/strings.xml @@ -58,6 +58,8 @@ Second \"item\" text" Već pročitano Pokaži mi Opšte + Sačuvaj segmente preuzetih video snimaka + Omogućava preskakanje segmenata preuzetih video snimaka dok ste van mreže Prikaži upozorenja o grešci povezivanja Prikazuje obaveštenje kada je server SponsorBlock nedostupan Omogući praćenje broja preskakanja diff --git a/patches/src/main/resources/addresources/values-sr-rCS/youtube/strings.xml b/patches/src/main/resources/addresources/values-sr-rCS/youtube/strings.xml index a39a9e84f8..2435b6f9d7 100644 --- a/patches/src/main/resources/addresources/values-sr-rCS/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-sr-rCS/youtube/strings.xml @@ -455,6 +455,15 @@ Prijave su javne i pregledaju se pre nego što kanal bude dodat na listu. Uklanj Slika je sačuvana u %s Nije bilo moguće preuzeti sliku U ovoj objavi nije pronađena slika + + Prevedi objave + Dodaje stavku u flyout meni za prevođenje objava kreatora sadržaja + Jezik za prevođenje objava + Prevedi + Prikaži original + Prevođenje… + Objava nije mogla biti prevedena + Objava je već na jeziku na koji se prevodi Dodajte stavku Ask u flyout meni Dodaje stavku za brzo pregledavanje videa u feed flyout meniju @@ -863,6 +872,8 @@ Da biste to popravili, uključite 'Spoof video strimovi' i promenite 'Podrazumev Popravi transkript Rešava grešku u proveri preduslova koja može sprečiti učitavanje transkripta Prikaži titlove u slici-u-slici + Stabilni blokovi titlova + Grupiše automatske titlove u stabilne, čitljive blokove. Neke reči se mogu pojaviti pre nego što budu izgovorene. Potrebno je ponovno pokretanje. Dugmad navigacije Izaberite koja komponenta se pojavljuje u traci sa alatkama @@ -1211,6 +1222,8 @@ Ograničenje: Ako je \"Vrati staru traku sa akcijama za video\" isključen, prek Onemogućava \'Nastavi gledanje\' miniplayer, YouTube Premium funkciju koja se pojavljuje pri pokretanju aplikacije Mini-plejer Prilagodi izgled i ponašanje smanjenog video plejera + Dodatni mini-plejeri + YouTube je uklonio sve dodatne mini-plejere. Da biste vratili više tipova mini-plejera, zakrpite YouTube 21.28.208 ili stariju verziju Tip mini-plejera Sakrij naslov i kanal Onemogućen @@ -1252,6 +1265,8 @@ Ograničenje: Ako je \"Vrati staru traku sa akcijama za video\" isključen, prek Održava dosledan izgled statusne trake, tako da ne prekrije gornje trake i ostavlja prazan prostor iznad plejera. Isključite ovo ako aplikacija padne ili gornje trake izgledaju pogrešno Sjaj + Morphe Glow 1 + Morphe Glow 2 Ovaj uređaj ne podržava Glow početni ekran Pozadina svetle teme Bela diff --git a/patches/src/main/resources/addresources/values-sr-rSP/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-sr-rSP/shared-youtube/strings.xml index e0c31531d0..6b443d3268 100644 --- a/patches/src/main/resources/addresources/values-sr-rSP/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-sr-rSP/shared-youtube/strings.xml @@ -21,8 +21,13 @@ Second \"item\" text" Анимација покретања Онемогућен Боја + Morphe боја Монохромски + Morphe монохром + Истакни нова подешавања + Број дана током којих се тачка приказује поред подешавања и опција додатих ажурирањем + Искључено Поново покрените да бисте завршили постављање diff --git a/patches/src/main/resources/addresources/values-sr-rSP/sponsorblock/strings.xml b/patches/src/main/resources/addresources/values-sr-rSP/sponsorblock/strings.xml index 11abbfa0f4..47efd2bfee 100644 --- a/patches/src/main/resources/addresources/values-sr-rSP/sponsorblock/strings.xml +++ b/patches/src/main/resources/addresources/values-sr-rSP/sponsorblock/strings.xml @@ -58,6 +58,8 @@ Second \"item\" text" Већ прочитано Покажи ми Опште + Сачувај сегменте преузетих видео-записа + Омогућава прескакање сегмената преузетих видео-записа док сте офлајн Прикажи упозорења о грешци повезивања Приказује обавештење када је сервер SponsorBlock недоступан Омогући праћење броја прескакања diff --git a/patches/src/main/resources/addresources/values-sr-rSP/youtube/strings.xml b/patches/src/main/resources/addresources/values-sr-rSP/youtube/strings.xml index 8ca6779285..a4f6961909 100644 --- a/patches/src/main/resources/addresources/values-sr-rSP/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-sr-rSP/youtube/strings.xml @@ -455,6 +455,15 @@ Second \"item\" text" Слика је сачувана у %s Није било могућности да се преузме слика У овој објави нема слика + + Преведи објаве + Додаје ставку у flyout мени за превођење објава креатора садржаја + Језик за превод објава + Преведи + Прикажи оригинал + Превођење… + Није било могућности да се објава преведе + Објава је већ на језику за превод Додај ставку Питај у изборни мени Додаје ставку за сумирање видеа у изборни мени канала @@ -861,6 +870,8 @@ Second \"item\" text" Popravi transkript Решава проблем са претпоставком која може да спречи учитавање транскрипта. Прикажи титлове унутар слике-у-слици + Стабилни блокови титлова + Групише аутоматске титлове у стабилне, читљиве блокове. Неке речи се могу појавити пре него што буду изговорене. Потребан је рестарт. Дугмад навигације Одаберите који дугмићи ће се појавити на доњој навигационој траци. @@ -1209,6 +1220,8 @@ Second \"item\" text" Онемогућава мини-плејер „Настави гледање“, функцију YouTube Premium која се појављује при покретању апликације Мини-плејер Прилагодите изглед и понашање мини-плејера + Додатни миниплејери + YouTube је уклонио све додатне миниплејере. Да бисте вратили више типова миниплејера, пачирајте YouTube 21.28.208 или старију верзију Тип мини-плејера Сакриј наслов и канал Онемогућен @@ -1250,6 +1263,8 @@ Second \"item\" text" Одржава конзистентан распоред статусне траке, тако да не прекрива горње траке и не оставља празан простор изнад плејера. Исключи ово ако се апликација уруши или горње траке изгледају лоше Сјај + Morphe Glow 1 + Morphe Glow 2 Овај уређај не подржава Glow екран за почетак Позадина светле теме Бела diff --git a/patches/src/main/resources/addresources/values-sv-rSE/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-sv-rSE/shared-youtube/strings.xml index 9edeef8377..fb79f70899 100644 --- a/patches/src/main/resources/addresources/values-sv-rSE/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-sv-rSE/shared-youtube/strings.xml @@ -21,8 +21,13 @@ Second \"item\" text" Startanimation Inaktiverad Färg + Morphe-färg Monokrom + Morphe Monokrom + Markera nya inställningar + Antal dagar en prick visas bredvid inställningar och alternativ som lagts till via en uppdatering + Av Starta om för att slutföra konfigureringen diff --git a/patches/src/main/resources/addresources/values-sv-rSE/sponsorblock/strings.xml b/patches/src/main/resources/addresources/values-sv-rSE/sponsorblock/strings.xml index 79fee2f1c2..f134075307 100644 --- a/patches/src/main/resources/addresources/values-sv-rSE/sponsorblock/strings.xml +++ b/patches/src/main/resources/addresources/values-sv-rSE/sponsorblock/strings.xml @@ -58,6 +58,8 @@ Second \"item\" text" Redan läst Visa mig Allmänt + Spara segment av nedladdade videor + Möjliggör att hoppa över segment av nedladdade videor när man är offline Visa meddelanden om anslutningsfel Visar ett meddelande när SponsorBlock-servern inte är tillgänglig Aktivera spårning av antalet överhoppningar diff --git a/patches/src/main/resources/addresources/values-sv-rSE/youtube/strings.xml b/patches/src/main/resources/addresources/values-sv-rSE/youtube/strings.xml index dd14310e65..eb1b95b344 100644 --- a/patches/src/main/resources/addresources/values-sv-rSE/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-sv-rSE/youtube/strings.xml @@ -455,6 +455,15 @@ Rapporter är offentliga och granskas innan en kanal läggs till i en lista. Bor Bilden sparades i %s Det gick inte att ladda ned bilden Ingen bild hittades i det här inlägget + + Översätt inlägg + Lägger till ett alternativ i flytmenyn för att översätta inlägg från innehållsskapare + Språk för översättning av inlägg + Översätt + Visa original + Översätter… + Kunde inte översätta inlägget + Inlägget är redan på målspråket Lägg till Ask-objekt i flygutmatningsmenyn Lägger till ett objekt för sammanfattning av video i flödesflygutmatningsmenyn @@ -863,6 +872,8 @@ För att åtgärda detta, aktivera 'Spoofa videoströmmar' och ändra 'Standardk Åtgärda transkription Åtgärdar ett fel i en villkorskontroll som kan förhindra att transkriptionen läses in Visa bild-i-bild-undertexter + Stabila bildtextblock + Gruppera automatiska undertexter i stabila, läsbara block. Vissa ord kan visas innan de uttalas. Omstart krävs. Navigeringsknappar Välj vilka knappar som visas i navigeringsfältet längst ned @@ -1211,6 +1222,8 @@ Begränsning: Om 'Återställ gammalt videoåtgärdsfält' är avstängt kan öv Inaktiverar minispelaren \'Fortsätt titta\', en YouTube Premium-funktion som visas vid appstart Minispelare Anpassa den minimerade videospelarens utseende och beteende + Ytterligare minispelare + YouTube tog bort alla ytterligare minispelare. För att återställa fler typer av minispelare, patcha YouTube 21.28.208 eller äldre Minispelartyp Dölj titel och kanal Inaktiverad @@ -1252,6 +1265,8 @@ Begränsning: Om 'Återställ gammalt videoåtgärdsfält' är avstängt kan öv Behåller statusradens layout konsekvent, så att den inte täcker de övre fälten och lämnar inget tomt utrymme ovanför spelaren. Stäng av detta om appen kraschar eller om de övre fälten ser fel ut Glöd + Morphe Glow 1 + Morphe Glow 2 Glow-stänkbild skärm är inte supported av den här enheten Bakgrundsfärg för ljust tema Vit diff --git a/patches/src/main/resources/addresources/values-sw-rKE/youtube/strings.xml b/patches/src/main/resources/addresources/values-sw-rKE/youtube/strings.xml index e1d7586419..9534e31d42 100644 --- a/patches/src/main/resources/addresources/values-sw-rKE/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-sw-rKE/youtube/strings.xml @@ -454,6 +454,7 @@ Ripoti ni za hadhara na hupitiwa kabla ya chaneli kuongezwa kwenye orodha. Kuond Picha imehifadhiwa kwenye %s Hakuwezekana kupakua picha Hakuna picha iliyopatikana katika chapisho hili + Ongeza kipengele cha Uliza kwenye menyu ya flyout Huongeza kipengele cha muhtasari wa video kwenye menyu ya flyout ya feed diff --git a/patches/src/main/resources/addresources/values-ta-rIN/youtube/strings.xml b/patches/src/main/resources/addresources/values-ta-rIN/youtube/strings.xml index 4ec76b1150..063697995e 100644 --- a/patches/src/main/resources/addresources/values-ta-rIN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ta-rIN/youtube/strings.xml @@ -61,6 +61,7 @@ Second \"item\" text" + diff --git a/patches/src/main/resources/addresources/values-te-rIN/youtube/strings.xml b/patches/src/main/resources/addresources/values-te-rIN/youtube/strings.xml index 4ec76b1150..063697995e 100644 --- a/patches/src/main/resources/addresources/values-te-rIN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-te-rIN/youtube/strings.xml @@ -61,6 +61,7 @@ Second \"item\" text" + diff --git a/patches/src/main/resources/addresources/values-th-rTH/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-th-rTH/shared-youtube/strings.xml index 98d671f8b3..5252520b13 100644 --- a/patches/src/main/resources/addresources/values-th-rTH/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-th-rTH/shared-youtube/strings.xml @@ -21,8 +21,12 @@ Second \"item\" text" แอนิเมชันเริ่มต้น ปิดการใช้งาน สี + Morphe แบบมีสี โมโนโครม + Morphe แบบขาวดำ + ไฮไลต์การตั้งค่าใหม่ + จำนวนวันที่จุดจะปรากฏข้างการตั้งค่าและตัวเลือกที่เพิ่มเข้ามาจากการอัปเดต ปิด รีสตาร์ทเพื่อดำเนินการตั้งค่าให้เสร็จสิ้น diff --git a/patches/src/main/resources/addresources/values-th-rTH/sponsorblock/strings.xml b/patches/src/main/resources/addresources/values-th-rTH/sponsorblock/strings.xml index f255c7c55d..fb7378a230 100644 --- a/patches/src/main/resources/addresources/values-th-rTH/sponsorblock/strings.xml +++ b/patches/src/main/resources/addresources/values-th-rTH/sponsorblock/strings.xml @@ -58,6 +58,8 @@ Second \"item\" text" อ่านแล้ว แสดงให้ฉันดู ทั่วไป + บันทึกเซกเมนต์ของวิดีโอที่ดาวน์โหลดไว้ + อนุญาตให้ข้ามเซกเมนต์ของวิดีโอที่ดาวน์โหลดไว้ขณะออฟไลน์ แสดงการแจ้งเตือนข้อผิดพลาดในการเชื่อมต่อ แสดงการแจ้งเตือนเมื่อเซิร์ฟเวอร์ SponsorBlock ไม่สามารถใช้งานได้ เปิดใช้งานการติดตามการนับข้าม diff --git a/patches/src/main/resources/addresources/values-th-rTH/youtube/strings.xml b/patches/src/main/resources/addresources/values-th-rTH/youtube/strings.xml index f896d0322a..22ea877082 100644 --- a/patches/src/main/resources/addresources/values-th-rTH/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-th-rTH/youtube/strings.xml @@ -455,6 +455,15 @@ Second \"item\" text" บันทึกรูปภาพไปยัง %s แล้ว ไม่สามารถดาวน์โหลดรูปภาพได้ ไม่พบรูปภาพในโพสต์นี้ + + แปลโพสต์ + เพิ่มรายการในเมนู flyout เพื่อแปลโพสต์ของผู้สร้างเนื้อหา + ภาษาสำหรับแปลโพสต์ + แปลภาษา + แสดงต้นฉบับ + กำลังแปล... + ไม่สามารถแปลโพสต์ได้ + โพสต์นี้เป็นภาษาที่ใช้ในการแปลอยู่แล้ว เพิ่มรายการ Ask ไปยังเมนูแบบเลื่อนออก เพิ่มรายการสรุปวิดีโอไปยังเมนูแบบเลื่อนออกของฟีด @@ -863,6 +872,8 @@ Second \"item\" text" แก้ไขข้อความถอดเสียง แก้ไขข้อผิดพลาดในการตรวจสอบเงื่อนไขเบื้องต้นที่อาจทำให้การโหลดข้อความถอดเสียงไม่สำเร็จ แสดงคำบรรยายในภาพซ้อนภาพ + บล็อกคำบรรยายแบบเสถียร + จัดกลุ่มคำบรรยายอัตโนมัติให้เป็นบล็อกที่เสถียรและอ่านง่าย คำบางคำอาจปรากฏขึ้นก่อนที่จะมีการพูด จำเป็นต้องรีสตาร์ท ปุ่มนำทาง เลือกปุ่มที่จะปรากฏในแถบนำทางด้านล่าง @@ -1211,6 +1222,8 @@ Second \"item\" text" ปิดใช้งานเครื่องเล่นขนาดเล็ก \'ดูต่อ\' ซึ่งเป็นคุณสมบัติพิเศษสำหรับ YouTube Premium ที่ปรากฏขึ้นเมื่อเริ่มต้นแอป มินิเพลเยอร์ ปรับแต่งรูปลักษณ์และพฤติกรรมของเครื่องเล่นวิดีโอขนาดเล็ก + มินิเพลเยอร์เพิ่มเติม + YouTube ได้ลบมินิเพลเยอร์เพิ่มเติมทั้งหมดออกแล้ว หากต้องการคืนค่าประเภทมินิเพลเยอร์เพิ่มเติม โปรดลงแพตช์ YouTube เวอร์ชัน 21.28.208 หรือเก่ากว่า ประเภทของมินิเพลเยอร์ ซ่อนชื่อและช่อง ปิดการใช้งานอยู่ @@ -1252,6 +1265,8 @@ Second \"item\" text" คงรูปแบบของแถบสถานะให้สอดคล้องกัน เพื่อไม่ให้เกิดการบังแถบด้านบนและเหลือพื้นที่ว่างเปล่าเหนือเครื่องเล่น เปิดการใช้งานนี้หากแอปค้างหรือแถบด้านบนแสดงผลผิดปกติ เรืองแสง + Morphe Glow 1 + Morphe Glow 2 อุปกรณ์นี้ไม่รองรับหน้าจอเปิดตัวแบบเรืองแสง พื้นหลังธีมสว่าง สีขาว diff --git a/patches/src/main/resources/addresources/values-tr-rTR/youtube/strings.xml b/patches/src/main/resources/addresources/values-tr-rTR/youtube/strings.xml index 233e53ceee..b0c6341e3c 100644 --- a/patches/src/main/resources/addresources/values-tr-rTR/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-tr-rTR/youtube/strings.xml @@ -456,6 +456,15 @@ Raporlar herkese açıktır ve bir kanal listeye eklenmeden önce incelenir. Kal Görsel %s konumuna kaydedildi Görsel indirilemedi Bu gönderide görsel bulunamadı + + Gönderileri çevir + İçerik oluşturucu gönderilerini çevirmek için bir açılır menü öğesi ekler + Gönderi çeviri dili + Çevir + Orijinalini göster + Çevriliyor… + Gönderi çevrilemedi + Gönderi zaten çeviri dilinde Soru ögesini açılır menüye ekle Akış açılır menüsüne bir video özetlendirme ögesi ekler @@ -864,6 +873,8 @@ Bunu düzeltmek için 'Video akışlarını taklit et' seçeneğini etkinleştir Transkript\'i düzelt Transkriptin yüklenmesini engelleyebilecek bir ön koşul denetimi hatasını giderir Resim içinde resim modunda altyazıları göster + Sabit altyazı blokları + Otomatik altyazıları sabit, okunabilir bloklar halinde gruplandırın. Bazı kelimeler konuşulmadan önce görünebilir. Yeniden başlatma gerekir. Gezinme düğmeleri Alt gezinme çubuğunda hangi düğmelerin görüneceğini seçin diff --git a/patches/src/main/resources/addresources/values-uk-rUA/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-uk-rUA/shared-youtube/strings.xml index 81d0c14784..72d9cb5e64 100644 --- a/patches/src/main/resources/addresources/values-uk-rUA/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-uk-rUA/shared-youtube/strings.xml @@ -25,6 +25,8 @@ Second \"item\" text" Монохромна Morphe монохромна + Підсвічувати нові налаштування + Кількість днів, протягом яких біля налаштувань і параметрів, доданих оновленням, показується крапка Вимк. Перезавантажте, щоб завершити налаштування diff --git a/patches/src/main/resources/addresources/values-uk-rUA/sponsorblock/strings.xml b/patches/src/main/resources/addresources/values-uk-rUA/sponsorblock/strings.xml index 45db3579ae..26cf183eaf 100644 --- a/patches/src/main/resources/addresources/values-uk-rUA/sponsorblock/strings.xml +++ b/patches/src/main/resources/addresources/values-uk-rUA/sponsorblock/strings.xml @@ -58,6 +58,8 @@ Second \"item\" text" Вже прочитано Показати Загальні + Зберігати сегменти завантажених відео + Дозволяє пропускати сегменти завантажених відео в режимі офлайн Показувати сповіщення про помилки з’єднання Показує сповіщення, коли сервер SponsorBlock недоступний Відстеження кількості пропусків diff --git a/patches/src/main/resources/addresources/values-uk-rUA/youtube/strings.xml b/patches/src/main/resources/addresources/values-uk-rUA/youtube/strings.xml index 7f50b1dc60..6b21a13b1d 100644 --- a/patches/src/main/resources/addresources/values-uk-rUA/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-uk-rUA/youtube/strings.xml @@ -29,6 +29,8 @@ Second \"item\" text" Різне Відео Відновити старий екран налаштувань YouTube + Показувати повідомлення в снекбарі YouTube + Повідомлення Morphe, як-от підтвердження дії кнопки плеєра, показуються в снекбарі YouTube замість спливаючого повідомлення Зняти обмеження фонового відтворення Вимкнути фонове відтворення @@ -455,6 +457,15 @@ Second \"item\" text" Зображення збережено у %s Не вдалося завантажити зображення У цьому дописі не знайдено зображення + + Перекладати дописи + Додає пункт у спливне меню для перекладу дописів авторів контенту + Мова перекладу дописів + Перекласти + Оригінал + Переклад… + Не вдалося перекласти допис + Допис уже написано мовою перекладу Додати пункт \"Запитати\" у спливне меню Додає пункт підсумовування відео до спливного меню стрічки @@ -863,6 +874,8 @@ Second \"item\" text" Виправити транскрипцію Усуває помилку перевірки передумов, яка може перешкоджати завантаженню транскрипції Показувати субтитри в режимі \"Картинка в картинці\" + Стабільні блоки субтитрів + Групує автоматичні субтитри в стабільні, зручні для читання блоки. Деякі слова можуть з\'являтися до того, як їх вимовлять. Потрібен перезапуск Кнопки панелі навігації Виберіть, які кнопки будуть показуватися на нижній панелі навігації @@ -1210,6 +1223,8 @@ Second \"item\" text" Вимикає мініплеєр \"Продовжити перегляд\", функцію YouTube Premium, що з\'являється при запуску застосунку Мініплеєр Налаштування вигляду та поведінки мінімізованого відеоплеєра + Додаткові мініплеєри + YouTube прибрав усі додаткові мініплеєри. Щоб відновити більше типів мініплеєрів, пропатчте YouTube 21.28.208 або старіший Тип мініплеєра Приховати назву та канал Вимкнено diff --git a/patches/src/main/resources/addresources/values-ur-rIN/youtube/strings.xml b/patches/src/main/resources/addresources/values-ur-rIN/youtube/strings.xml index 4ec76b1150..063697995e 100644 --- a/patches/src/main/resources/addresources/values-ur-rIN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ur-rIN/youtube/strings.xml @@ -61,6 +61,7 @@ Second \"item\" text" + diff --git a/patches/src/main/resources/addresources/values-uz-rUZ/youtube/strings.xml b/patches/src/main/resources/addresources/values-uz-rUZ/youtube/strings.xml index ea1690cddd..cfc3a79634 100644 --- a/patches/src/main/resources/addresources/values-uz-rUZ/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-uz-rUZ/youtube/strings.xml @@ -450,6 +450,7 @@ Hisobotlar ochiq va kanal ro'yxatga qo'shilishidan oldin ko'rib chiqiladi. O'chi Rasm %s manziliga saqlandi Rasmni yuklab olib bo\"lib bo\"lmadi Ushbu postda rasm topilmadi + Yoyilma menyuga \"So\'rash\" elementini qo\'shish Lenta yoyilma menyusiga videoni umumlashtirish elementini qo\'shadi diff --git a/patches/src/main/resources/addresources/values-vi-rVN/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-vi-rVN/shared-youtube/strings.xml index 96b66f57c7..c6fdfe7878 100644 --- a/patches/src/main/resources/addresources/values-vi-rVN/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-vi-rVN/shared-youtube/strings.xml @@ -21,8 +21,12 @@ Second \"item\" text" Hoạt ảnh khởi động Tắt Màu + Morphe Có màu Đơn sắc + Morphe Đơn sắc + Làm nổi bật các cài đặt mới + Số ngày hiển thị dấu chấm bên cạnh các cài đặt và tùy chọn được thêm vào thông qua bản cập nhật Tắt Khởi động lại để hoàn tất thiết lập diff --git a/patches/src/main/resources/addresources/values-vi-rVN/sponsorblock/strings.xml b/patches/src/main/resources/addresources/values-vi-rVN/sponsorblock/strings.xml index 4207f28162..9549385325 100644 --- a/patches/src/main/resources/addresources/values-vi-rVN/sponsorblock/strings.xml +++ b/patches/src/main/resources/addresources/values-vi-rVN/sponsorblock/strings.xml @@ -58,6 +58,8 @@ Second \"item\" text" Đã đọc Hiện cho tôi Chung + Lưu các đoạn của video đã tải xuống + Cho phép bỏ qua các đoạn của video đã tải xuống khi ngoại tuyến Hiện cảnh báo lỗi kết nối Hiển thị thông báo khi máy chủ SponsorBlock không khả dụng Bật theo dõi số lần bỏ qua diff --git a/patches/src/main/resources/addresources/values-vi-rVN/youtube/strings.xml b/patches/src/main/resources/addresources/values-vi-rVN/youtube/strings.xml index 88941f08f0..7099f0033a 100644 --- a/patches/src/main/resources/addresources/values-vi-rVN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-vi-rVN/youtube/strings.xml @@ -455,6 +455,15 @@ Báo cáo được công khai và được xem xét trước khi một kênh đ Hình ảnh đã được lưu vào %s Không thể tải hình ảnh Không tìm thấy hình ảnh nào trong bài viết này + + Dịch bài viết + Thêm một mục menu flyout để dịch các bài viết của người sáng tạo nội dung + Ngôn ngữ dịch bài viết + Dịch + Hiển thị bản gốc + Đang dịch… + Không thể dịch bài viết + Bài viết đã ở ngôn ngữ dịch Thêm mục Hỏi vào menu bay ra Thêm mục tóm tắt video vào menu bay ra của nguồn cấp dữ liệu @@ -863,6 +872,8 @@ Cần thiết lập \"Cookie phụ đề\"" Sửa bản chép lời Khắc phục lỗi kiểm tra tiên quyết có thể ngăn tải bản chép lời Hiển thị phụ đề trong chế độ hình ảnh trong hình + Các khối phụ đề ổn định + Nhóm các phụ đề tự động thành các khối ổn định, dễ đọc. Một số từ có thể xuất hiện trước khi được nói. Yêu cầu khởi động lại. Nút điều hướng Chọn các nút sẽ hiển thị trong thanh điều hướng dưới cùng @@ -1211,6 +1222,8 @@ Hạn chế: Nếu tùy chọn \"Khôi phục thanh thao tác video cũ\" bị t Ngăn không cho trình phát thu nhỏ \"tiếp tục xem\" khi mở ứng dụng (một tính năng Premium) Trình phát thu nhỏ Tùy chỉnh giao diện và hành vi của trình phát thu nhỏ + Các trình phát thu nhỏ bổ sung + YouTube đã xóa tất cả các trình phát thu nhỏ bổ sung. Để khôi phục nhiều loại trình phát thu nhỏ hơn, hãy vá YouTube 21.28.208 hoặc cũ hơn Loại trình phát thu nhỏ Ẩn tiêu đề và kênh Tắt @@ -1252,6 +1265,8 @@ Hạn chế: Nếu tùy chọn \"Khôi phục thanh thao tác video cũ\" bị t Giữ cho bố cục của thanh trạng nhất quán, để nó không che các thanh trên cùng và không để khoảng trống phía trên trình phát. Tắt tùy chọn này nếu ứng dụng bị đóng băng hoặc các thanh trên cùng trông sai Phát sáng + Morphe Glow 1 + Morphe Glow 2 Màn hình chào hiệu ứng phát sáng không được hỗ trợ bởi thiết bị này Nền chủ đề sáng Trắng diff --git a/patches/src/main/resources/addresources/values-zh-rCN/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-zh-rCN/shared-youtube/strings.xml index 0270cea6d6..948a0df15d 100644 --- a/patches/src/main/resources/addresources/values-zh-rCN/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-zh-rCN/shared-youtube/strings.xml @@ -21,8 +21,13 @@ Second \"item\" text" 启动动画 已禁用 颜色 + Morphe 彩色 单色 + Morphe 单色 + 高亮显示新设置 + 在更新添加的设置和选项旁边显示圆点的天数 + 关闭 重启以完成设置 diff --git a/patches/src/main/resources/addresources/values-zh-rCN/sponsorblock/strings.xml b/patches/src/main/resources/addresources/values-zh-rCN/sponsorblock/strings.xml index 3d69207628..217922b217 100644 --- a/patches/src/main/resources/addresources/values-zh-rCN/sponsorblock/strings.xml +++ b/patches/src/main/resources/addresources/values-zh-rCN/sponsorblock/strings.xml @@ -58,6 +58,8 @@ Second \"item\" text" 已读 展示 常规 + 保存已下载视频的分段 + 允许在离线状态下跳过已下载视频的分段 显示连接错误提示 当 SponsorBlock 服务器不可用时显示通知 启用跳过次数跟踪 diff --git a/patches/src/main/resources/addresources/values-zh-rCN/youtube/strings.xml b/patches/src/main/resources/addresources/values-zh-rCN/youtube/strings.xml index 771e995d19..65fc10b771 100644 --- a/patches/src/main/resources/addresources/values-zh-rCN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-zh-rCN/youtube/strings.xml @@ -455,6 +455,15 @@ Second \"item\" text" 图片已保存至 %s 无法下载图片 此帖子中未找到图片 + + 翻译帖子 + 为翻译内容创作者的帖子添加一个飞出菜单项 + 帖子翻译目标语言 + 翻译 + 显示原文 + 正在翻译…… + 无法翻译该帖子 + 该帖子已经是翻译目标语言 将Ask项目添加到弹出菜单 在动态推送弹出菜单中添加视频摘要项目 @@ -863,6 +872,8 @@ Second \"item\" text" 修复文本 保持导航栏在滚动时可见 在画中画模式下显示字幕 + 稳定字幕块 + 将自动生成的字幕分组为稳定且可读的块。某些词可能会在被说出之前出现。需要重启。 导航栏按钮 使状态栏完全不透明 @@ -1211,6 +1222,8 @@ Second \"item\" text" 禁用“继续观看”迷你播放器,这是 YouTube Premium 的一项功能,该功能会在应用启动时显示 小型播放器 自定义最小化视频播放器的外观和行为 + 其他迷你播放器 + YouTube 移除了所有其他迷你播放器。要恢复更多迷你播放器类型,请补丁 YouTube 21.28.208 或更早版本 小型播放器类型 隐藏标题和频道 已禁用 @@ -1252,6 +1265,8 @@ Second \"item\" text" 保持状态栏布局一致,使其不覆盖顶部栏且播放器上方无空白区域。如果应用崩溃或顶部栏显示异常,请关闭此选项 发光 + Morphe Glow 1 + Morphe Glow 2 此设备不支持发光启动页 浅色主题背景 白色 diff --git a/patches/src/main/resources/addresources/values-zh-rTW/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-zh-rTW/shared-youtube/strings.xml index 87fb7da6c6..743acd47de 100644 --- a/patches/src/main/resources/addresources/values-zh-rTW/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-zh-rTW/shared-youtube/strings.xml @@ -21,8 +21,12 @@ Second \"item\" text" 啟動動畫 已停用 顏色 + Morphe 彩色 單色 + Morphe 單色 + 高亮顯示新設定 + 更新後,設定和選項旁的圓點顯示天數 關閉 重新啟動以完成設定 diff --git a/patches/src/main/resources/addresources/values-zh-rTW/sponsorblock/strings.xml b/patches/src/main/resources/addresources/values-zh-rTW/sponsorblock/strings.xml index a07f15f825..2cf3b8c87f 100644 --- a/patches/src/main/resources/addresources/values-zh-rTW/sponsorblock/strings.xml +++ b/patches/src/main/resources/addresources/values-zh-rTW/sponsorblock/strings.xml @@ -58,6 +58,8 @@ Second \"item\" text" 已閱讀 顯示 一般 + 儲存已下載影片的分段 + 允許在離線狀態下跳過已下載影片的分段 顯示連線錯誤警示 在 SponsorBlock 伺服器無法使用時顯示通知 啟用跳過次數追蹤 diff --git a/patches/src/main/resources/addresources/values-zh-rTW/youtube/strings.xml b/patches/src/main/resources/addresources/values-zh-rTW/youtube/strings.xml index 2eb4a008a4..30f6742f01 100644 --- a/patches/src/main/resources/addresources/values-zh-rTW/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-zh-rTW/youtube/strings.xml @@ -455,6 +455,15 @@ Second \"item\" text" 圖片已儲存至 %s 無法下載圖片 此貼文中未找到圖片 + + 翻譯貼文 + 新增一個飛出式選單項目,用以翻譯內容創作者的貼文 + 貼文翻譯語言 + 翻譯 + 顯示原文 + 翻譯中…… + 無法翻譯此貼文 + 貼文已是翻譯目標語言 在彈出式選單中加入「Ask」項目 在資訊流彈出式選單中加入影片摘要項目 @@ -863,6 +872,8 @@ Second \"item\" text" 修正轉錄稿 解決可能阻止載入字幕的預先檢查失敗問題 在畫中畫模式下顯示字幕 + 穩定字幕區塊 + 將自動字幕分組為穩定且易讀的區塊。某些單字可能會在說出之前出現。需要重新啟動。 導覽列按鈕 選擇要在工具列中顯示哪些元件 @@ -1211,6 +1222,8 @@ Second \"item\" text" 停用「繼續觀看」迷你播放器,這是 YouTube Premium 的功能,會在應用程式啟動時顯示 迷你播放器 自訂迷你影片播放器的外觀和行為 + 額外的縮小播放器 + YouTube 移除了所有額外的縮小播放器。若要恢復更多縮小播放器類型,請修補 YouTube 21.28.208 或更早版本 迷你播放器類型 隱藏標題與頻道 停用 @@ -1252,6 +1265,8 @@ Second \"item\" text" 保持狀態列的版面配置一致,使其不會遮蓋頂部工具列,且在播放器上方不會留有空白。如果應用程式發生當機或頂部工具列顯示異常,請停用此選項 發光 + Morphe Glow 1 + Morphe Glow 2 此裝置不支援發光啟動畫面 淺色主題背景 白色 diff --git a/patches/src/main/resources/addresources/values-zu-rZA/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-zu-rZA/shared-youtube/strings.xml index 794c960e76..b696f964e0 100644 --- a/patches/src/main/resources/addresources/values-zu-rZA/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-zu-rZA/shared-youtube/strings.xml @@ -21,8 +21,13 @@ Second \"item\" text" I-animation yokuqala Idisayizelwe Umbala + Imibala ye-Morphe Imibala emifana + Morphe Monochrome + Gqamisa izilungiselelo ezintsha + Isibalo sezinsuku lapho uchungechunge lubonakala eduze kwezilungiselelo nezinketho ezengeziwe ngoku-update + Thisha Qala kabusha ukuze uqedele ukusetha diff --git a/patches/src/main/resources/addresources/values-zu-rZA/sponsorblock/strings.xml b/patches/src/main/resources/addresources/values-zu-rZA/sponsorblock/strings.xml index 2ad3459533..cae6530ef2 100644 --- a/patches/src/main/resources/addresources/values-zu-rZA/sponsorblock/strings.xml +++ b/patches/src/main/resources/addresources/values-zu-rZA/sponsorblock/strings.xml @@ -58,6 +58,8 @@ Second \"item\" text" Sengifundile Ngikhombise Okujwayelekile + Londoloza izingxenye zamavidiyo elandwe + Kuvumela ukunqamula izingxenye zamavidiyo elandwe ngenkathi ungekho ku-internet Khombisa izixwayiso zephutha lokuxhumana Ikhombisa isaziso lapho iserba le-SponsorBlock ingatholakali Vumela ukulandelela isibalo sokugxumela diff --git a/patches/src/main/resources/addresources/values-zu-rZA/youtube/strings.xml b/patches/src/main/resources/addresources/values-zu-rZA/youtube/strings.xml index f242982aec..d2af4fd1c5 100644 --- a/patches/src/main/resources/addresources/values-zu-rZA/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-zu-rZA/youtube/strings.xml @@ -455,6 +455,15 @@ Izibiko zivulekile futhi zibuyekezwa ngaphambi kokuba ishネル yengeke ohlwini. Isithombe sigciniwe ku %s Kukwazalekanga ukulanda isithombe Alikho isithombe esitholakala kule post + + Humusha izincwadi + Yengeza ithemba lemenyu yokuhumusha izithัมba zabashayeli okuqukudala + Ulimi lokuhumusha isithัมba + Humusha + Khombisa okungukuqala + Kuhunyushwa… + Kungekenzi kahle ukuhumusha isithัมba + Isithัมba sesele kululimi lokuhumusha Yenge item ye-Ask emenyu ye-flyout Yenge item yokufingqa ividiyo emenyu ye-flyout ye-feed @@ -863,6 +872,8 @@ Kumele kunikezwe ama-'Caption cookies'" Lungisa i-transcript Ixazulula ukwehluleka kokuhlola isimo esidingekayo okungavimbela ukuthi i-transcript ilandwe Khombisa imibhalo efanekisayo ku-picture-in-picture + Amabhulokhi ezincazelo azinzile + Hlanganisa izincazelo ezizenzakalelayo emabhulokini azinzile, afundekayo. Amanye amagama angase avele ngaphambi kokuba akhulunywe. Kudingeka ukuqala kabusha. Izinkinobho zokuhamba Khetha ukuthi yiziphi izinkinobho ezivela kubala lokuqondisa laphansi @@ -1211,6 +1222,8 @@ Ukukhawulelwa: Uma 'Buyisela ibha yezenzo yevidiyo yakudala' icishiwe, i-overlay Kukhishwa ukusebenza kwe-miniplayer ye-\'Qhubeka nokubuka\', okuyisici se-YouTube Premium esivela lapho kuvulwa i-app Umdlali omncane Lungisa ukubonakala nokusebenza komdlali wevidiyo ocishwe + Omunye miniplayer + I-YouTube isusile yonke iminiplayer eyengeziwe. Ukuze ubuyise izinhlobo ze-miniplayer eziningi, patch i-YouTube 21.28.208 noma endala kakhulu Uhlobo lomdlali omncane Fihla isihloko neshannel Kukhishwa @@ -1252,6 +1265,8 @@ Ukukhawulelwa: Uma 'Buyisela ibha yezenzo yevidiyo yakudala' icishiwe, i-overlay Gcina ukuhleleka kwe-status bar kukufana, ukuze ingabozi imigqa engenhla futhi ingashiyi isikhala esingenalutho ngaphezu kwedlala. Cima lokhu uma i-app i-crash noma imigqa engenhla ibukeka ngendlela engalungile Khanya + uMorphe Glow 1 + uMorphe Glow 2 Isikrini sokuqala esikhanyayo asisekelwa yileli device Isizinda sethimu ekhanyayo Mhlophe @@ -1570,7 +1585,8 @@ Ukuqopshwa ngokuzenzakalelayo kuyama ku-incognito, lapho ubunikazi be-account lu Izinhlelo ezimakiwe njengezibukwe Buyisa Vula - {%1$s} episodes ezintsha + 1 i-episode entsha + %1$s episodes ezintsha Qopha inqubekelaphambili kuleli device "I-series yakho nenqubekela phambili ihlala kuleli device, ihlanganyelwa phakathi kwe-accounts ze-YouTube. Izilawuli zomlando we-YouTube azizishintshi. Sula inqubekela phambili kume-nu ye-Series. From dc2d6e2d433943d80b4250293b2959353b108d9a Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Fri, 30 Oct 2026 08:05:20 +0000 Subject: [PATCH 177/189] chore: Release v2.0.0-dev.2 [skip ci] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## [2.0.0-dev.2](https://github.com/MorpheApp/morphe-patches/compare/v2.0.0-dev.1...v2.0.0-dev.2) (2026-10-30) ### 🐛 Bug Fixes * **YouTube - Captions:** Stabilize automatic captions and restore translation languages ([#4149](https://github.com/MorpheApp/morphe-patches/issues/4149)) ([ba1ac73](https://github.com/MorpheApp/morphe-patches/commit/ba1ac73757dc8935c60434de8d0362b311d73442)) * **YouTube - Feed video actions:** Watch later button is shown above the video of posts, and is missing after the app language is changed ([#4172](https://github.com/MorpheApp/morphe-patches/issues/4172)) ([54b7371](https://github.com/MorpheApp/morphe-patches/commit/54b7371f5252dfcd287d2360e37cdacefb5ad3c2)) * **YouTube - Navigation bar:** Navigation bar transluency not working ([#4156](https://github.com/MorpheApp/morphe-patches/issues/4156)) ([82103d3](https://github.com/MorpheApp/morphe-patches/commit/82103d36cacc93eeaed307f75975c492fb610b24)) * **YouTube - Return YouTube Dislike:** Feed thumbnail like count blinks, moves, and is missing after returning to the feed ([#4176](https://github.com/MorpheApp/morphe-patches/issues/4176)) ([4dc345b](https://github.com/MorpheApp/morphe-patches/commit/4dc345b93f4007d42c124dfb021fb9cbeb221575)) ### ✨ New Features * **YouTube:** Add `Translate posts` patch ([#4163](https://github.com/MorpheApp/morphe-patches/issues/4163)) ([5d571a0](https://github.com/MorpheApp/morphe-patches/commit/5d571a07c37c0231b46651dd46fb2cbeef24713a)) * **YouTube:** Apply offline SponsorBlock segments to playlist downloads ([#4180](https://github.com/MorpheApp/morphe-patches/issues/4180)) ([d6983da](https://github.com/MorpheApp/morphe-patches/commit/d6983da6ed69327d68ce3b188a8626ef99255d06)) * **YouTube:** Show Morphe messages in snackbar instead of a toast ([#4148](https://github.com/MorpheApp/morphe-patches/issues/4148)) ([035718b](https://github.com/MorpheApp/morphe-patches/commit/035718bf911540f36ba768e5db78cae4ac4caf23)) --- CHANGELOG.md | 15 +++++++++ README.md | 5 +-- gradle.properties | 2 +- patches-bundle.json | 8 ++--- patches-list.json | 74 ++++++++++++++++++++++++++++++++++++++++++++- 5 files changed, 96 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 256f0c7489..41671cf7c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,18 @@ +## [2.0.0-dev.2](https://github.com/MorpheApp/morphe-patches/compare/v2.0.0-dev.1...v2.0.0-dev.2) (2026-10-30) + +### 🐛 Bug Fixes + +* **YouTube - Captions:** Stabilize automatic captions and restore translation languages ([#4149](https://github.com/MorpheApp/morphe-patches/issues/4149)) ([ba1ac73](https://github.com/MorpheApp/morphe-patches/commit/ba1ac73757dc8935c60434de8d0362b311d73442)) +* **YouTube - Feed video actions:** Watch later button is shown above the video of posts, and is missing after the app language is changed ([#4172](https://github.com/MorpheApp/morphe-patches/issues/4172)) ([54b7371](https://github.com/MorpheApp/morphe-patches/commit/54b7371f5252dfcd287d2360e37cdacefb5ad3c2)) +* **YouTube - Navigation bar:** Navigation bar transluency not working ([#4156](https://github.com/MorpheApp/morphe-patches/issues/4156)) ([82103d3](https://github.com/MorpheApp/morphe-patches/commit/82103d36cacc93eeaed307f75975c492fb610b24)) +* **YouTube - Return YouTube Dislike:** Feed thumbnail like count blinks, moves, and is missing after returning to the feed ([#4176](https://github.com/MorpheApp/morphe-patches/issues/4176)) ([4dc345b](https://github.com/MorpheApp/morphe-patches/commit/4dc345b93f4007d42c124dfb021fb9cbeb221575)) + +### ✨ New Features + +* **YouTube:** Add `Translate posts` patch ([#4163](https://github.com/MorpheApp/morphe-patches/issues/4163)) ([5d571a0](https://github.com/MorpheApp/morphe-patches/commit/5d571a07c37c0231b46651dd46fb2cbeef24713a)) +* **YouTube:** Apply offline SponsorBlock segments to playlist downloads ([#4180](https://github.com/MorpheApp/morphe-patches/issues/4180)) ([d6983da](https://github.com/MorpheApp/morphe-patches/commit/d6983da6ed69327d68ce3b188a8626ef99255d06)) +* **YouTube:** Show Morphe messages in snackbar instead of a toast ([#4148](https://github.com/MorpheApp/morphe-patches/issues/4148)) ([035718b](https://github.com/MorpheApp/morphe-patches/commit/035718bf911540f36ba768e5db78cae4ac4caf23)) + ## [2.0.0-dev.1](https://github.com/MorpheApp/morphe-patches/compare/v1.49.0-dev.11...v2.0.0-dev.1) (2026-10-29) ### ⚠ BREAKING CHANGES diff --git a/README.md b/README.md index 72534b6be5..e6eaead70e 100644 --- a/README.md +++ b/README.md @@ -35,9 +35,9 @@ All modifications made by Morphe, along with their dates, can be found in the Gi ## 🩹 Patches list -> **[v2.0.0-dev.1](https://github.com/MorpheApp/morphe-patches/releases/tag/v2.0.0-dev.1)**  •  `dev`  •  193 patches total +> **[v2.0.0-dev.2](https://github.com/MorpheApp/morphe-patches/releases/tag/v2.0.0-dev.2)**  •  `dev`  •  194 patches total

    -📦 YouTube  •  108 patches +📦 YouTube  •  109 patches
    **🎯 Supported versions:** @@ -151,6 +151,7 @@ All modifications made by Morphe, along with their dates, can be found in the Gi | [Spoof video streams](#spoof-video-streams) | Adds options to spoof the client video streams to fix playback. | | | [Swipe controls](#swipe-controls) | Adds options to enable and configure volume and brightness swipe controls. | | | [Theme](#theme) | Adds options for theming, and settings to change the app foreground and background colors. | • Dark theme color
    • Light theme color | +| [Translate posts](#translate-posts) | Adds a menu item to translate the text of posts. | | | [Video delay](#video-delay) | Adds an option to delay the video, to keep it in sync with laggy Bluetooth audio. | | | [Video quality](#video-quality) | Adds options to set default video qualities and always use the advanced video quality menu. | | | [Voice over translation](#voice-over-translation) | Adds additional voice over languages using text-to-speech synchronized to the video playback. | | diff --git a/gradle.properties b/gradle.properties index 030930fc64..312b59b194 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,4 +5,4 @@ android.useAndroidX = true # Extensions are Java only. Prevents AGP from adding Kotlin compilation and the Kotlin stdlib. android.builtInKotlin = false kotlin.code.style = official -version = 2.0.0-dev.1 +version = 2.0.0-dev.2 diff --git a/patches-bundle.json b/patches-bundle.json index b6b15ab603..c434802468 100644 --- a/patches-bundle.json +++ b/patches-bundle.json @@ -1,8 +1,8 @@ { - "created_at": "2026-10-29T18:31:17", - "description": "## [2.0.0-dev.1](https://github.com/MorpheApp/morphe-patches/compare/v1.49.0-dev.11...v2.0.0-dev.1) (2026-10-29)\n\n### ⚠ BREAKING CHANGES\n\n* Patches now use Morphe Patcher 2.0.0\n\n* Treat breaking changes as major releases ([c596d42](https://github.com/MorpheApp/morphe-patches/commit/c596d42afb48dd79be022f81f3b731eda8870941))\n\n### 🐛 Bug Fixes\n\n* **YouTube - Download post images:** Match posts with texts of many lines or a single emoji ([#4167](https://github.com/MorpheApp/morphe-patches/issues/4167)) ([0c5dc17](https://github.com/MorpheApp/morphe-patches/commit/0c5dc17e215ad63b748570adea5894553d489dd6))\n\n### ✨ New Features\n\n* **YouTube:** Add offline sponsorblock segment support for in-app downloads ([#4140](https://github.com/MorpheApp/morphe-patches/issues/4140)) ([136851e](https://github.com/MorpheApp/morphe-patches/commit/136851e5a5690fd9e2488eb3f0e57537bdfffc2a))\n* **YouTube:** Highlight new in-app settings ([#4162](https://github.com/MorpheApp/morphe-patches/issues/4162)) ([a3b34d7](https://github.com/MorpheApp/morphe-patches/commit/a3b34d75d399fcc7dd3f9cb2c68f32fc112d979b))\n\n### 🚀 Updated App Support\n\n* **YouTube Music:** Add support for `9.30.53` ([f049c91](https://github.com/MorpheApp/morphe-patches/commit/f049c91c540f74da8dffe462a8fa5b10cb073305))", - "download_url": "https://github.com/MorpheApp/morphe-patches/releases/download/v2.0.0-dev.1/patches-2.0.0-dev.1.mpp", + "created_at": "2026-10-30T08:03:57", + "description": "## [2.0.0-dev.2](https://github.com/MorpheApp/morphe-patches/compare/v2.0.0-dev.1...v2.0.0-dev.2) (2026-10-30)\n\n### 🐛 Bug Fixes\n\n* **YouTube - Captions:** Stabilize automatic captions and restore translation languages ([#4149](https://github.com/MorpheApp/morphe-patches/issues/4149)) ([ba1ac73](https://github.com/MorpheApp/morphe-patches/commit/ba1ac73757dc8935c60434de8d0362b311d73442))\n* **YouTube - Feed video actions:** Watch later button is shown above the video of posts, and is missing after the app language is changed ([#4172](https://github.com/MorpheApp/morphe-patches/issues/4172)) ([54b7371](https://github.com/MorpheApp/morphe-patches/commit/54b7371f5252dfcd287d2360e37cdacefb5ad3c2))\n* **YouTube - Navigation bar:** Navigation bar transluency not working ([#4156](https://github.com/MorpheApp/morphe-patches/issues/4156)) ([82103d3](https://github.com/MorpheApp/morphe-patches/commit/82103d36cacc93eeaed307f75975c492fb610b24))\n* **YouTube - Return YouTube Dislike:** Feed thumbnail like count blinks, moves, and is missing after returning to the feed ([#4176](https://github.com/MorpheApp/morphe-patches/issues/4176)) ([4dc345b](https://github.com/MorpheApp/morphe-patches/commit/4dc345b93f4007d42c124dfb021fb9cbeb221575))\n\n### ✨ New Features\n\n* **YouTube:** Add `Translate posts` patch ([#4163](https://github.com/MorpheApp/morphe-patches/issues/4163)) ([5d571a0](https://github.com/MorpheApp/morphe-patches/commit/5d571a07c37c0231b46651dd46fb2cbeef24713a))\n* **YouTube:** Apply offline SponsorBlock segments to playlist downloads ([#4180](https://github.com/MorpheApp/morphe-patches/issues/4180)) ([d6983da](https://github.com/MorpheApp/morphe-patches/commit/d6983da6ed69327d68ce3b188a8626ef99255d06))\n* **YouTube:** Show Morphe messages in snackbar instead of a toast ([#4148](https://github.com/MorpheApp/morphe-patches/issues/4148)) ([035718b](https://github.com/MorpheApp/morphe-patches/commit/035718bf911540f36ba768e5db78cae4ac4caf23))", + "download_url": "https://github.com/MorpheApp/morphe-patches/releases/download/v2.0.0-dev.2/patches-2.0.0-dev.2.mpp", "signature_download_url": "", - "version": "2.0.0-dev.1", + "version": "2.0.0-dev.2", "public_key": "g8Ys9leTu2yeixojeR35HrNkK6eOSLpA6nNIizRutqI=" } diff --git a/patches-list.json b/patches-list.json index 4cf1406aec..13b014067b 100644 --- a/patches-list.json +++ b/patches-list.json @@ -1,6 +1,6 @@ { "NOTE": "Do NOT manually edit this file. This file is automatically updated when semantic release (release.yml) runs. Manually editing this file can break your releases and break third party tools that use this file.", - "version": "2.0.0-dev.1", + "version": "2.0.0-dev.2", "patches": [ { "name": "AI channel filter", @@ -12991,6 +12991,78 @@ ], "options": [] }, + { + "name": "Translate posts", + "description": "Adds a menu item to translate the text of posts.", + "default": true, + "category": null, + "compatiblePackages": [ + { + "packageName": "com.google.android.youtube", + "name": "YouTube", + "description": null, + "apkFileType": "APK_REQUIRED", + "appIconColor": "#FF0033", + "signatures": [ + "5aad2bee6db95d17e05a08d7d1e64c10a1511879154483916b6ae6c7fd9cb0c6", + "3d7a1223019aa39d9ea0e3436ab7c0896bfb4fb679f4de5fe7c23f326c8f994a" + ], + "formFactor": "MOBILE", + "targets": [ + { + "version": "21.43.523", + "versionCodes": null, + "isExperimental": true, + "minSdk": 29, + "description": null + }, + { + "version": "21.42.241", + "versionCodes": null, + "isExperimental": true, + "minSdk": 29, + "description": null + }, + { + "version": "21.30.216", + "versionCodes": null, + "isExperimental": false, + "minSdk": 29, + "description": null + }, + { + "version": "21.28.208", + "versionCodes": null, + "isExperimental": false, + "minSdk": 29, + "description": null + }, + { + "version": "21.23.492", + "versionCodes": null, + "isExperimental": false, + "minSdk": 28, + "description": null + }, + { + "version": "20.51.39", + "versionCodes": null, + "isExperimental": false, + "minSdk": 28, + "description": null + }, + { + "version": "20.26.46", + "versionCodes": null, + "isExperimental": false, + "minSdk": 26, + "description": null + } + ] + } + ], + "options": [] + }, { "name": "Video delay", "description": "Adds an option to delay the video, to keep it in sync with laggy Bluetooth audio.", From a33a0120a16d83452275483fa02907b954cf5922 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Fri, 30 Oct 2026 08:27:36 +0000 Subject: [PATCH 178/189] chore: Update dependency --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 51c1614f74..c08e1b5f62 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,7 +2,7 @@ [versions] morphe-patcher = "2.0.0" -morphe-patches-library = "2.1.0-dev.1" +morphe-patches-library = "2.1.0-dev.2" smali = "4220ae75f7" agp = "9.1.0" annotation = "1.11.0" From b6e0a643677d7a975413ab8d1566745cac15127c Mon Sep 17 00:00:00 2001 From: BingoKingo <24961267+BingoKingo@users.noreply.github.com> Date: Fri, 30 Oct 2026 16:41:53 +0800 Subject: [PATCH 179/189] feat(YouTube Music - Third-party lyrics): UI changes, partial feature regression (#4182) --- .../music/patches/lyrics/DisplayState.java | 9 +- .../music/patches/lyrics/LyricsManager.java | 692 ++++++----- .../music/patches/lyrics/LyricsMatch.java | 2 +- .../patches/lyrics/LyricsPrefetcher.java | 30 +- .../patches/lyrics/LyricsPresentation.java | 14 +- .../music/patches/lyrics/LyricsProviders.java | 3 +- .../music/patches/lyrics/SessionQueue.java | 27 +- .../music/patches/lyrics/SkipSegments.java | 70 +- .../patches/lyrics/derive/AiProviders.java | 195 +++- .../patches/lyrics/derive/GeminiClient.java | 156 --- .../lyrics/derive/LyricsRomanizer.java | 914 +++++++++++---- .../lyrics/derive/LyricsTranslator.java | 172 ++- .../patches/lyrics/derive/OpenAIClient.java | 276 +++-- .../patches/lyrics/derive/QuickLRCClient.java | 147 ++- .../lyrics/derive/SectionDetector.java | 29 +- .../music/patches/lyrics/model/Lyrics.java | 169 ++- .../lyrics/model/LyricsBracketSplitter.java | 814 ++++--------- .../patches/lyrics/model/LyricsFilter.java | 24 +- .../lyrics/model/LyricsFilterDefaults.java | 16 +- .../patches/lyrics/model/LyricsLine.java | 63 +- .../patches/lyrics/model/LyricsMerge.java | 22 +- .../lyrics/model/LyricsPreference.java | 28 +- .../lyrics/model/LyricsTextFormatter.java | 37 +- .../patches/lyrics/model/MetadataCleaner.java | 49 +- .../lyrics/model/MiniLyricsFormatter.java | 194 ++-- .../lyrics/model/MiniLyricsTimeline.java | 53 +- .../lyrics/model/PairedPunctuation.java | 62 +- .../patches/lyrics/model/TrackVersion.java | 14 +- .../music/patches/lyrics/model/Word.java | 3 +- .../patches/lyrics/parsers/LRCParser.java | 124 +- .../lyrics/parsers/LyricifyParser.java | 41 +- .../patches/lyrics/parsers/LyricsCrypto.java | 22 +- .../lyrics/parsers/LyricsFileParser.java | 10 +- .../patches/lyrics/parsers/TTMLParser.java | 512 +++++---- .../patches/lyrics/requests/AMLLProvider.java | 48 +- .../lyrics/requests/AppleMusicProvider.java | 685 +++++------ .../lyrics/requests/BinimumProvider.java | 197 ++-- .../lyrics/requests/BlyricsProvider.java | 2 +- .../lyrics/requests/CaptionsFetcher.java | 297 +++-- .../lyrics/requests/DeezerProvider.java | 324 +++--- .../lyrics/requests/GeniusProvider.java | 142 +-- .../lyrics/requests/KuGouProvider.java | 414 +++---- .../lyrics/requests/LRCLIBProvider.java | 38 +- .../lyrics/requests/LocalLyricsFetcher.java | 21 +- .../lyrics/requests/LrcRedProvider.java | 304 ++--- .../lyrics/requests/LunaBeatProvider.java | 61 +- .../patches/lyrics/requests/LunaProvider.java | 106 +- .../lyrics/requests/LyricifyProvider.java | 215 ++-- .../lyrics/requests/LyricsProvider.java | 112 +- .../lyrics/requests/LyricsRequests.java | 558 +++++---- .../lyrics/requests/MusixmatchProvider.java | 413 ++++--- .../lyrics/requests/NetEaseProvider.java | 210 ++-- .../lyrics/requests/PetitLyricsProvider.java | 890 ++++++++------- .../patches/lyrics/requests/QQProvider.java | 245 ++-- .../patches/lyrics/requests/RMMProvider.java | 172 +-- .../lyrics/requests/ScoredSongProvider.java | 22 +- .../lyrics/requests/SimpMusicProvider.java | 180 +-- .../lyrics/requests/SpotifyProvider.java | 573 +++++----- .../lyrics/requests/UnisonProvider.java | 142 +-- .../lyrics/requests/YouTubeMusicProvider.java | 184 +-- .../lyrics/romanizers/AlphabetRomanizer.java | 51 +- .../lyrics/romanizers/ArmenianRomanizer.java | 76 -- .../lyrics/romanizers/CantoneseDictStore.java | 148 --- .../lyrics/romanizers/CldrTransforms.java | 143 +++ .../romanizers/CldrTransformsStore.java | 526 +++++++++ .../lyrics/romanizers/CyrillicRomanizer.java | 112 -- .../lyrics/romanizers/GeorgianRomanizer.java | 66 -- .../lyrics/romanizers/GreekRomanizer.java | 75 -- .../lyrics/romanizers/HanRomanizer.java | 18 +- .../lyrics/romanizers/HangulRomanizer.java | 36 +- .../lyrics/romanizers/JapaneseDictStore.java | 31 +- .../lyrics/romanizers/KanaRomanizer.java | 13 +- .../lyrics/romanizers/LocalRomanizer.java | 242 ++-- .../lyrics/romanizers/PinyinDictStore.java | 259 +---- .../lyrics/romanizers/PinyinRomanizer.java | 2 +- .../lyrics/romanizers/RimeDictStore.java | 1008 +++++++++++++++++ ...tpingRomanizer.java => RimeRomanizer.java} | 15 +- .../lyrics/romanizers/RomanizationDict.java | 155 +-- .../lyrics/romanizers/ScriptDetector.java | 75 +- .../lyrics/romanizers/UniDictStore.java | 419 +++++++ .../lyrics/romanizers/WordReadingTable.java | 81 +- .../lyrics/romanizers/WordTableFile.java | 44 +- .../lyrics/session/LockScreenLyrics.java | 46 +- .../lyrics/session/MiniplayerLyrics.java | 44 +- .../patches/lyrics/storage/LyricsCache.java | 178 +-- .../lyrics/storage/LyricsFileSaver.java | 18 +- .../lyrics/storage/TrackOffsetStore.java | 12 +- .../lyrics/ui/KaraokeHighlightState.java | 61 +- .../music/patches/lyrics/ui/LyricsColors.java | 18 +- .../music/patches/lyrics/ui/LyricsFit.java | 94 +- .../patches/lyrics/ui/LyricsLineView.java | 32 +- .../lyrics/ui/LyricsPanelInstaller.java | 63 +- .../patches/lyrics/ui/LyricsPanelView.java | 312 ++++- .../patches/lyrics/ui/LyricsSearchUI.java | 21 +- .../patches/lyrics/ui/LyricsSpanBuilder.java | 150 +-- .../patches/lyrics/ui/LyricsViewSupport.java | 90 +- .../patches/lyrics/ui/SectionDotsMotion.java | 51 +- .../ui/SponsorBlockMusicSubmitDialog.java | 77 +- .../extension/music/settings/Settings.java | 95 +- .../preference/LyricsAboutPreference.java | 9 +- .../preference/LyricsAiConfigPreference.java | 430 ------- .../preference/LyricsAiPromptPreference.java | 24 +- .../LyricsClearCachePreference.java | 53 +- .../preference/LyricsFieldsDialog.java | 23 + .../LyricsLocalRomanizationDownloads.java | 369 ++++++ .../LyricsLocalRomanizationPreference.java | 161 --- .../LyricsOrderedListPreference.java | 828 ++++++++++++-- .../LyricsProviderConfigDialog.java | 533 +++++++++ .../preference/LyricsRimeDictDialog.java | 610 ++++++++++ .../LyricsTargetLanguagePreference.java | 264 +++++ .../LyricsTokenDialogPreference.java | 2 +- .../LyricsTrackFormatPreference.java | 21 +- .../LyricsTranscribeKeyPreference.java | 60 +- .../model/LyricsBracketSplitterTest.java | 78 -- .../model/LyricsLineWordTimingsTest.java | 61 + .../patches/lyrics/model/TrackFormatTest.java | 22 +- .../music/layout/lyrics/LyricsPatch.kt | 105 +- .../values-af-rZA/music/strings.xml | 2 + .../values-ar-rSA/music/strings.xml | 2 + .../values-az-rAZ/music/strings.xml | 2 + .../values-bg-rBG/music/strings.xml | 2 + .../values-bn-rBD/music/strings.xml | 2 + .../values-cs-rCZ/music/strings.xml | 2 + .../values-da-rDK/music/strings.xml | 2 + .../values-de-rDE/music/strings.xml | 2 + .../values-el-rGR/music/strings.xml | 2 + .../values-es-rES/music/strings.xml | 2 + .../values-fa-rIR/music/strings.xml | 2 + .../values-fi-rFI/music/strings.xml | 2 + .../values-fil-rPH/music/strings.xml | 2 + .../values-fr-rFR/music/strings.xml | 2 + .../values-ga-rIE/music/strings.xml | 2 + .../values-hi-rIN/music/strings.xml | 2 + .../values-hr-rHR/music/strings.xml | 2 + .../values-hu-rHU/music/strings.xml | 2 + .../values-in-rID/music/strings.xml | 2 + .../values-is-rIS/music/strings.xml | 2 + .../values-it-rIT/music/strings.xml | 2 + .../values-iw-rIL/music/strings.xml | 2 + .../values-ja-rJP/music/strings.xml | 2 + .../values-ko-rKR/music/strings.xml | 2 + .../values-lt-rLT/music/strings.xml | 2 + .../values-nb-rNO/music/strings.xml | 2 + .../values-nl-rNL/music/strings.xml | 2 + .../values-pl-rPL/music/strings.xml | 2 + .../values-pt-rBR/music/strings.xml | 2 + .../values-pt-rPT/music/strings.xml | 2 + .../values-ro-rRO/music/strings.xml | 2 + .../values-ru-rRU/music/strings.xml | 2 + .../values-sk-rSK/music/strings.xml | 2 + .../values-sl-rSI/music/strings.xml | 2 + .../values-sr-rCS/music/strings.xml | 2 + .../values-sr-rSP/music/strings.xml | 2 + .../values-sv-rSE/music/strings.xml | 2 + .../values-th-rTH/music/strings.xml | 2 + .../values-tr-rTR/music/strings.xml | 2 + .../values-uk-rUA/music/strings.xml | 2 + .../values-vi-rVN/music/strings.xml | 2 + .../values-zh-rCN/music/strings.xml | 4 +- .../values-zh-rTW/music/strings.xml | 2 + .../addresources/values/music/arrays.xml | 92 +- .../addresources/values/music/strings.xml | 119 +- 162 files changed, 12392 insertions(+), 8065 deletions(-) delete mode 100644 extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/derive/GeminiClient.java delete mode 100644 extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/ArmenianRomanizer.java delete mode 100644 extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/CantoneseDictStore.java create mode 100644 extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/CldrTransforms.java create mode 100644 extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/CldrTransformsStore.java delete mode 100644 extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/CyrillicRomanizer.java delete mode 100644 extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/GeorgianRomanizer.java delete mode 100644 extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/GreekRomanizer.java create mode 100644 extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/RimeDictStore.java rename extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/{JyutpingRomanizer.java => RimeRomanizer.java} (58%) create mode 100644 extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/UniDictStore.java delete mode 100644 extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsAiConfigPreference.java create mode 100644 extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsLocalRomanizationDownloads.java delete mode 100644 extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsLocalRomanizationPreference.java create mode 100644 extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsProviderConfigDialog.java create mode 100644 extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsRimeDictDialog.java create mode 100644 extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsTargetLanguagePreference.java delete mode 100644 extensions/music/src/test/java/app/morphe/extension/music/patches/lyrics/model/LyricsBracketSplitterTest.java create mode 100644 extensions/music/src/test/java/app/morphe/extension/music/patches/lyrics/model/LyricsLineWordTimingsTest.java diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/DisplayState.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/DisplayState.java index c92d169fbb..c50fd817d9 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/DisplayState.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/DisplayState.java @@ -21,11 +21,14 @@ final class DisplayState { final boolean wordSync; /** An explicit search waits for every stage, so the best result of the run is the one shown. */ final boolean exhaustive; - @Nullable Lyrics published; + @Nullable + Lyrics published; long firstPaintAt; - @Nullable Lyrics bestResult; + @Nullable + Lyrics bestResult; int bestDisplayRank; - @Nullable Lyrics bestFallback; + @Nullable + Lyrics bestFallback; int bestFallbackRank; boolean windowClosed; diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/LyricsManager.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/LyricsManager.java index b399e525be..d2f389a626 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/LyricsManager.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/LyricsManager.java @@ -36,6 +36,7 @@ import app.morphe.extension.music.patches.lyrics.derive.SectionDetector; import app.morphe.extension.music.patches.lyrics.model.Lyrics; +import app.morphe.extension.music.patches.lyrics.model.LyricsBracketSplitter; import app.morphe.extension.music.patches.lyrics.model.LyricsFilter; import app.morphe.extension.music.patches.lyrics.model.LyricsFilterDefaults; import app.morphe.extension.music.patches.lyrics.model.LyricsLine; @@ -59,86 +60,38 @@ import app.morphe.extension.shared.Utils; /** - * Fetches lyrics for the currently playing track and tracks playback position. + * Fetches lyrics for the current track and tracks playback position. * - *

    The position is extrapolated from the last {@link PlaybackState} update so synced - * lyrics stay accurate to a few tens of milliseconds between updates. The player time hook - * ({@link VideoInformation#getVideoTime()}, which ticks roughly once per second) only - * re-anchors when it is ahead of that extrapolation, or when it is far behind and - * {@link PlaybackState} itself has gone quiet — a slightly stale videoTime sample must never - * pull playback backwards. A seek or play/pause also re-anchors via - * {@link #onSetPlaybackState}. + *

    Position is extrapolated from the last {@link PlaybackState} update; the ~1 Hz + * {@link VideoInformation#getVideoTime()} hook only re-anchors when ahead, or far behind + * while {@link PlaybackState} is quiet. Seeks and play/pause re-anchor via {@link #onSetPlaybackState}. */ public final class LyricsManager { - public enum State { - IDLE, - LOADING, - LOADED, - NOT_FOUND, - ERROR - } - - public interface Listener { - - /** Called on the main thread whenever the state or the lyrics change. */ - void onLyricsChanged(State state, @Nullable Lyrics lyrics); - } - - record ScoredCandidate(int score, int rank, Lyrics lyrics) - implements Comparable { - @Override - public int compareTo(ScoredCandidate other) { - if (score != other.score) return Integer.compare(other.score, score); - if (rank != other.rank) return Integer.compare(other.rank, rank); - return 0; - } - } - - private record PendingTrack(TrackInfo track, String rawTitle, String rawArtist, - @Nullable Uri mediaUri, String videoId) { - } - private static final LyricsManager INSTANCE = new LyricsManager(); - private static final long PENDING_COMMIT_MS = 1500; - private static final long REBINDING_CONFIRM_MS = 1500; - + /** + * A sample further than this from the estimate is a seek, which the estimate jumps to + * instead of catching up with. + */ + private static final long SEEK_THRESHOLD_MS = 1000; + /** A longer frame gap, such as after the app was in the background, is not extrapolated over. */ + private static final long MAX_FRAME_STEP_MS = 100; + private static final int FIRST_PAINT_HOLD_MS = 800; private final ExecutorService lookupExecutor = Executors.newFixedThreadPool(8); - private final ExecutorService fetchExecutor = Executors.newFixedThreadPool(16); - private final Set> activeFetches = ConcurrentHashMap.newKeySet(); - - private Future submitFetch(int id, CompletionService service, - java.util.concurrent.Callable fetch) { - Future future = service.submit(() -> id == requestId.get() ? fetch.call() : null); - activeFetches.add(future); - // The track can change between submission and registration. - if (id != requestId.get()) { - future.cancel(true); - activeFetches.remove(future); - } - return future; - } - - private void cancelPendingFetches() { - for (Future future : activeFetches) { - future.cancel(true); - activeFetches.remove(future); - } - } - - private void finishFetches(List> futures) { - for (Future future : futures) { - if (!future.isDone()) future.cancel(true); - activeFetches.remove(future); - } - } - private final List listeners = new ArrayList<>(2); - + /** + * Incremented for every track change so that a late response for a previous + * track is discarded instead of being shown for the current one. + */ + private final AtomicInteger requestId = new AtomicInteger(); + private final PriorityQueue candidateQueue = new PriorityQueue<>(); + private final Set shownFingerprints = ConcurrentHashMap.newKeySet(); + private final Map filteredCache = + java.util.Collections.synchronizedMap(Utils.createSizeRestrictedMap(32)); @Nullable private volatile TrackInfo currentTrack; @@ -192,25 +145,9 @@ private void finishFetches(List> futures) { private volatile String dialogPreviewVideoId; private volatile State state = State.IDLE; - - /** - * Incremented for every track change so that a late response for a previous - * track is discarded instead of being shown for the current one. - */ - private final AtomicInteger requestId = new AtomicInteger(); - private long lastVideoTimeSample = -1; private float playbackSpeed = 1f; private boolean playing; - - /** - * A sample further than this from the estimate is a seek, which the estimate jumps to - * instead of catching up with. - */ - private static final long SEEK_THRESHOLD_MS = 1000; - /** A longer frame gap, such as after the app was in the background, is not extrapolated over. */ - private static final long MAX_FRAME_STEP_MS = 100; - // The position is sampled a few times a second, and estimated in between on every frame // so the highlight moves smoothly. The estimate only ever moves forward while playing. private long lastSamplePosition = -1; @@ -222,8 +159,116 @@ private void finishFetches(List> futures) { private boolean frameCallbackScheduled; /** Frames are only followed while something reads the position, such as an open lyrics panel. */ private boolean positionReadSinceFrame; - + private int temporaryOffsetMs = 0; + private volatile boolean phase2Done; + /** + * Set to the id of a lookup whose job is only to fill the queue, because the lyric + * remembered for the track is already on screen. Cleared as soon as the user asks for + * a different candidate. + */ + private volatile int suppressForRequest = -1; + /** Fingerprints of the candidates the user left in order for this track, in that order. */ + @Nullable + private volatile List rememberedQueue; private final Choreographer.FrameCallback frameCallback = this::onFrame; + /** Custom search terms remembered for this track, or null when the default terms were used. */ + @Nullable + private volatile String searchQueryTitle; + @Nullable + private volatile String searchQueryArtist; + /** + * Fingerprint the remembered lyric had before it was written to disk. Writing drops the raw + * text the fingerprint was taken from, so this is what tells a later lookup that the lyric + * it just found is the one already on screen. + */ + @Nullable + private volatile String preferredFingerprint; + /** + * The video id the running lookup belongs to, captured on the thread that starts it: + * {@link VideoInformation} is not volatile, so a background thread must not read it after + * another video has started. It names the remembered preference of the playback. + */ + @Nullable + private volatile String preferenceVideoId; + /** + * The video id the running load belongs to, so {@link #ensureLoadedForCurrentVideo} + * can tell that playback reached another video without one of the track events + * starting a load for it: metadata can arrive empty or not at all on an auto-switch. + */ + private String loadedForVideoId = ""; + /** + * The video the lyrics on screen belong to. {@link #currentVideoId} is the video the + * current track was committed for, which is not the same one while a rebinding still + * keeps the lyric of the video that played before. + */ + private String lyricsVideoId = ""; + @Nullable + private String unconfirmedVideoId; + private int rebindingGeneration; + private boolean staleLyricsDropped; + + private LyricsManager() { + VideoInformation.addVideoIdListener(videoId -> reloadCurrentTrack()); + LyricsPanelInstaller.registerSettingsListener(); + runOnFetchThread(LunaBeatProvider::preloadIndex); + runOnFetchThread(() -> { + MetadataCleaner.resolveSettingBlocking(LyricsFilterDefaults.LYRICS_CUSTOM_REGEX.get()); + MetadataCleaner.resolveSettingBlocking(LyricsFilterDefaults.LYRICS_TEXT_FILTER.get()); + MetadataCleaner.resolveSettingBlocking(LyricsFilterDefaults.LYRICS_CREDIT_LINE_REGEX.get()); + }); + } + + private static void runContained(Runnable body) { + try { + body.run(); + } catch (Throwable ignored) { + } + } + + public static LyricsManager getInstance() { + return INSTANCE; + } + + /** + * Returns true for tracks backed by a local file ({@code file://} or {@code content://}) as + * opposed to a streamed YouTube video ({@code http(s)://}). Only local files can carry + * embedded lyrics in their tags. + */ + private static boolean isLocalUri(@Nullable Uri uri) { + return uri != null && ("file".equals(uri.getScheme()) || "content".equals(uri.getScheme())); + } + + @Nullable + public static Uri parseMediaUri(@NonNull MediaMetadata metadata) { + String uri = metadata.getString(MediaMetadata.METADATA_KEY_MEDIA_URI); + return (uri != null) ? Uri.parse(uri) : null; + } + + private Future submitFetch(int id, CompletionService service, + java.util.concurrent.Callable fetch) { + Future future = service.submit(() -> id == requestId.get() ? fetch.call() : null); + activeFetches.add(future); + // The track can change between submission and registration. + if (id != requestId.get()) { + future.cancel(true); + activeFetches.remove(future); + } + return future; + } + + private void cancelPendingFetches() { + for (Future future : activeFetches) { + future.cancel(true); + activeFetches.remove(future); + } + } + + private void finishFetches(List> futures) { + for (Future future : futures) { + if (!future.isDone()) future.cancel(true); + activeFetches.remove(future); + } + } private void onFrame(long frameTimeNanos) { frameCallbackScheduled = false; @@ -291,19 +336,6 @@ private void stopFrameCallback() { } } - private int temporaryOffsetMs = 0; - - private LyricsManager() { - VideoInformation.addVideoIdListener(videoId -> reloadCurrentTrack()); - LyricsPanelInstaller.registerSettingsListener(); - runOnFetchThread(LunaBeatProvider::preloadIndex); - runOnFetchThread(() -> { - MetadataCleaner.resolveSettingBlocking(LyricsFilterDefaults.LYRICS_CUSTOM_REGEX.get()); - MetadataCleaner.resolveSettingBlocking(LyricsFilterDefaults.LYRICS_TEXT_FILTER.get()); - MetadataCleaner.resolveSettingBlocking(LyricsFilterDefaults.LYRICS_CREDIT_LINE_REGEX.get()); - }); - } - /** * Looks up the track's version on Deezer, only when the miniplayer or the media session * show the track format and it has {v}. If the lookup fails, the title shows without {v}. @@ -314,9 +346,9 @@ private void requestVersion(TrackInfo track) { || !MiniLyricsFormatter.usesVariant(Settings.LYRICS_MINI_TRACK_FORMAT.get())) { return; } - TrackInfo query = TrackVersion.query(track, currentRawTitle, currentRawArtist); + final TrackInfo query = TrackVersion.query(track, currentRawTitle, currentRawArtist); runOnLookupThread(() -> { - TrackVersion version; + final TrackVersion version; try { version = DeezerProvider.fetchVersion(query); } catch (Exception ex) { @@ -337,78 +369,6 @@ private void runOnFetchThread(Runnable body) { fetchExecutor.execute(() -> runContained(body)); } - private static void runContained(Runnable body) { - try { - body.run(); - } catch (Throwable ignored) { - } - } - - private final PriorityQueue candidateQueue = new PriorityQueue<>(); - private final Set shownFingerprints = ConcurrentHashMap.newKeySet(); - private volatile boolean phase2Done; - - /** - * Set to the id of a lookup whose job is only to fill the queue, because the lyric - * remembered for the track is already on screen. Cleared as soon as the user asks for - * a different candidate. - */ - private volatile int suppressForRequest = -1; - - /** Fingerprints of the candidates the user left in order for this track, in that order. */ - @Nullable - private volatile List rememberedQueue; - - /** Custom search terms remembered for this track, or null when the default terms were used. */ - @Nullable - private volatile String searchQueryTitle; - @Nullable - private volatile String searchQueryArtist; - - /** - * Fingerprint the remembered lyric had before it was written to disk. Writing drops the raw - * text the fingerprint was taken from, so this is what tells a later lookup that the lyric - * it just found is the one already on screen. - */ - @Nullable - private volatile String preferredFingerprint; - - /** - * The video id the running lookup belongs to, captured on the thread that starts it: - * {@link VideoInformation} is not volatile, so a background thread must not read it after - * another video has started. It names the remembered preference of the playback. - */ - @Nullable - private volatile String preferenceVideoId; - - /** - * The video id the running load belongs to, so {@link #ensureLoadedForCurrentVideo} - * can tell that playback reached another video without one of the track events - * starting a load for it: metadata can arrive empty or not at all on an auto-switch. - */ - private String loadedForVideoId = ""; - - /** - * The video the lyrics on screen belong to. {@link #currentVideoId} is the video the - * current track was committed for, which is not the same one while a rebinding still - * keeps the lyric of the video that played before. - */ - private String lyricsVideoId = ""; - - @Nullable - private String unconfirmedVideoId; - - private int rebindingGeneration; - - private boolean staleLyricsDropped; - - private final Map filteredCache = - java.util.Collections.synchronizedMap(Utils.createSizeRestrictedMap(32)); - - public static LyricsManager getInstance() { - return INSTANCE; - } - public void addListener(Listener listener) { Utils.verifyOnMainThread(); if (!listeners.contains(listener)) { @@ -448,6 +408,7 @@ public boolean hasLyrics() { Lyrics current = currentLyrics; return state == State.LOADED && current != null && !current.isEmpty(); } + private void resetPosition() { stopFrameCallback(); lastVideoTimeSample = -1; @@ -505,7 +466,9 @@ private long computeExpectedPositionMs() { return Math.max(0, smoothPosition); } - public int getTemporaryOffsetMs() { return temporaryOffsetMs; } + public int getTemporaryOffsetMs() { + return temporaryOffsetMs; + } public void setTemporaryOffsetMs(int ms) { temporaryOffsetMs = ms; @@ -514,7 +477,7 @@ public void setTemporaryOffsetMs(int ms) { /** Called from the panel when the track is about to change or to go away, not per drag frame. */ public void persistTemporaryOffsetMs() { - TrackInfo track = currentTrack; + final TrackInfo track = currentTrack; if (track == null) { return; } @@ -635,7 +598,7 @@ private void ensureLoadedForCurrentVideo() { || unconfirmedVideoId != null) { return; } - String videoId = VideoInformation.getVideoId(); + final String videoId = VideoInformation.getVideoId(); if (videoId.isEmpty() || videoId.equals(loadedForVideoId)) { return; } @@ -645,10 +608,8 @@ private void ensureLoadedForCurrentVideo() { /** * Holds the track while playback reached a video the metadata events have not - * described yet. In video mode the session keeps the metadata of the video that - * played before and only then comes back empty, so the next event is given its - * chance to name the new video before anything is committed to it, which is what - * keeps a previous lyric from staying on screen for the wrong one. + * described yet: the session reports the previous video's metadata first, so + * deferring lets the next event name the new video before anything is committed. */ private void deferTrackRebinding(String videoId, String rawTitle, String rawArtist, @Nullable Uri mediaUri) { @@ -668,12 +629,12 @@ private void confirmTrackRebinding(String videoId, int generation, String rawTit if (generation != rebindingGeneration || unconfirmedVideoId == null) { return; } - String target = unconfirmedVideoId; + final String target = unconfirmedVideoId; unconfirmedVideoId = null; if (!target.equals(VideoInformation.getVideoId())) { return; } - TrackInfo track = currentTrack; + final TrackInfo track = currentTrack; if (track == null) { return; } @@ -681,7 +642,7 @@ private void confirmTrackRebinding(String videoId, int generation, String rawTit } private void dropStaleLyrics() { - String videoId = VideoInformation.getVideoId(); + final String videoId = VideoInformation.getVideoId(); if (videoId.isEmpty()) { return; } @@ -698,7 +659,7 @@ private void dropStaleLyrics() { suppressForRequest = -1; currentVideoId = videoId; loadedForVideoId = videoId; - Lyrics shown = currentLyrics; + final Lyrics shown = currentLyrics; if (shown != null && !shown.isEmpty()) { staleLyricsDropped = true; } @@ -827,7 +788,7 @@ public void onDisplayedTrackChanged(@Nullable String title, @Nullable String art return; } - TrackInfo previous = currentTrack; + final TrackInfo previous = currentTrack; TrackInfo track = new TrackInfo(cleanedTitle, cleanedArtist, previous == null ? "" : previous.album(), previous == null ? 0 : previous.durationSeconds()); @@ -842,21 +803,6 @@ public void onDisplayedTrackChanged(@Nullable String title, @Nullable String art setPendingTrack(track, title, artist, mediaUri, VideoInformation.getVideoId()); } - /** - * Returns true for tracks backed by a local file ({@code file://} or {@code content://}) as - * opposed to a streamed YouTube video ({@code http(s)://}). Only local files can carry - * embedded lyrics in their tags. - */ - private static boolean isLocalUri(@Nullable Uri uri) { - return uri != null && ("file".equals(uri.getScheme()) || "content".equals(uri.getScheme())); - } - - @Nullable - public static Uri parseMediaUri(@NonNull MediaMetadata metadata) { - String uri = metadata.getString(MediaMetadata.METADATA_KEY_MEDIA_URI); - return (uri != null) ? Uri.parse(uri) : null; - } - private void load(TrackInfo track, boolean keepLyrics) { final int id = requestId.incrementAndGet(); cancelPendingFetches(); @@ -908,14 +854,14 @@ public void fetchNextCandidate() { setState(State.LOADING, current); runOnLookupThread(() -> { - String sbVideoId = skipSegmentsVideoId(); - List segs = SkipSegments.await(sbVideoId, + final String sbVideoId = skipSegmentsVideoId(); + final List segs = SkipSegments.await(sbVideoId, track.durationSeconds() * 1000L); if (id == requestId.get()) { displaySegsVideoId = sbVideoId; displaySegs = segs; } - TrackInfo searchTrack = LyricsMatch.withEffectiveDuration(track, segs); + final TrackInfo searchTrack = LyricsMatch.withEffectiveDuration(track, segs); phase2Done = false; if (pollAndPublishNext(id, track)) { return; @@ -980,7 +926,7 @@ private void collectRemainingCandidates(int id, TrackInfo track, if (id != requestId.get()) { return; } - TrackInfo candidateTrack = LyricsRequests.isCustomMatchMode() + final TrackInfo candidateTrack = LyricsRequests.isCustomMatchMode() ? new TrackInfo(track.title(), track.artist(), "", 0) : track; Set existing = new HashSet<>(shownFingerprints); synchronized (candidateQueue) { @@ -1067,21 +1013,22 @@ private void processCandidateFuture(Future> f, int id, Logger.printDebug(() -> "Failed to process candidate result", ex); } } + /** * Shows the lyric the user landed on for this track and arms the queue to be filled * around it instead of replacing it. Runs on a lookup thread, and reports whether the * lookup that called it has to treat everything it finds as queue material. */ private boolean applyRememberedPreference(int id, TrackInfo track) { - LyricsPreference preference = LyricsCache.getPreference(preferenceVideoId, track); + final LyricsPreference preference = LyricsCache.getPreference(preferenceVideoId, track); if (preference == null || preference.preferred() == null) { return false; } - Lyrics raw = preference.preferred(); - List queue = preference.queue(); - String queryTitle = preference.queryTitle(); - String queryArtist = preference.queryArtist(); - String storedFingerprint = preference.fingerprint(); + final Lyrics raw = preference.preferred(); + final List queue = preference.queue(); + final String queryTitle = preference.queryTitle(); + final String queryArtist = preference.queryArtist(); + final String storedFingerprint = preference.fingerprint(); if (!LyricsMatch.isValidLyrics(raw, track)) { Logger.printInfo(() -> "LyricsPref skipped: filters keep out the remembered lyric"); return false; @@ -1108,7 +1055,7 @@ private boolean applyRememberedPreference(int id, TrackInfo track) { rememberPreferenceDisarmed(); return false; } - Lyrics display = preferred; + final Lyrics display = preferred; Utils.runOnMainThread(() -> { if (id != requestId.get()) { return; @@ -1152,92 +1099,88 @@ private TrackInfo customQueryTrack(TrackInfo track) { return new TrackInfo(title, artist, track.album(), track.durationSeconds()); } -/** - * Shows lyrics that were computed outside any provider - by transcription or by aligning timings - * onto text the user already had - as the version for this track, and moves everything that was - * showing or waiting behind it. - * - *

    This is what the other paths converge on, so the panel sees an ordinary load: the same - * normalisation {@link #prepareForDisplay} applies, and the same remembered preference that pins - * a chosen version across launches. Because the new version joins {@link #shownFingerprints}, - * refresh skips it like any other shown version and lands on what was pushed back. - */ -public void publishPreferred(@Nullable Lyrics injected) { - Utils.verifyOnMainThread(); - if (injected == null || injected == Lyrics.NOT_FOUND || injected.isEmpty()) { - return; - } + /** + * Shows lyrics computed outside any provider - transcription, or timings aligned onto text the + * user already had - as the version for this track, pushing what was showing or waiting behind + * it. Same {@link #prepareForDisplay} path and remembered preference as any load; + * {@link #shownFingerprints} keeps refresh from landing on it again. + */ + public void publishPreferred(@Nullable Lyrics injected) { + Utils.verifyOnMainThread(); + if (injected == null || injected == Lyrics.NOT_FOUND || injected.isEmpty()) { + return; + } - Lyrics display; - try { - display = prepareForDisplay(injected); - } catch (Throwable ignored) { - return; - } - if (display == null || display == Lyrics.NOT_FOUND || display.isEmpty()) { - return; - } + final Lyrics display; + try { + display = prepareForDisplay(injected); + } catch (Throwable ignored) { + return; + } + if (display == null || display == Lyrics.NOT_FOUND || display.isEmpty()) { + return; + } - // Everything lookup-related is about a version this one replaces. - final int id = requestId.incrementAndGet(); - cancelPendingFetches(); + // Everything lookup-related is about a version this one replaces. + final int id = requestId.incrementAndGet(); + cancelPendingFetches(); - List pushedBack = new ArrayList<>(); - Lyrics previous = currentLyrics; - if (previous != null && !previous.isEmpty() && previous != Lyrics.NOT_FOUND) { - pushedBack.add(previous); - } - synchronized (candidateQueue) { - for (ScoredCandidate candidate : new ArrayList<>(candidateQueue)) { - pushedBack.add(candidate.lyrics()); + final List pushedBack = new ArrayList<>(); + final Lyrics previous = currentLyrics; + if (previous != null && !previous.isEmpty() && previous != Lyrics.NOT_FOUND) { + pushedBack.add(previous); + } + synchronized (candidateQueue) { + for (ScoredCandidate candidate : new ArrayList<>(candidateQueue)) { + pushedBack.add(candidate.lyrics()); + } + candidateQueue.clear(); } - candidateQueue.clear(); - } - String chosen = LyricsMatch.fingerprint(display); - shownFingerprints.clear(); - shownFingerprints.add(chosen); - rememberedQueue = null; - preferredFingerprint = chosen; - // Holds back anything still arriving for this request from taking the screen instead. - suppressForRequest = id; + final String chosen = LyricsMatch.fingerprint(display); + shownFingerprints.clear(); + shownFingerprints.add(chosen); + rememberedQueue = null; + preferredFingerprint = chosen; + // Holds back anything still arriving for this request from taking the screen instead. + suppressForRequest = id; - setState(State.LOADED, display); + setState(State.LOADED, display); - for (Lyrics candidate : pushedBack) { - if (candidate == null || candidate.isEmpty() || candidate == Lyrics.NOT_FOUND) { - continue; - } - String fp = LyricsMatch.fingerprint(candidate); - if (fp.equals(chosen)) { - continue; - } - synchronized (candidateQueue) { - boolean alreadyQueued = false; - for (ScoredCandidate queued : candidateQueue) { - if (fp.equals(LyricsMatch.fingerprint(queued.lyrics()))) { - alreadyQueued = true; - break; - } + for (Lyrics candidate : pushedBack) { + if (candidate == null || candidate.isEmpty() || candidate == Lyrics.NOT_FOUND) { + continue; + } + final String fp = LyricsMatch.fingerprint(candidate); + if (fp.equals(chosen)) { + continue; } - if (!alreadyQueued) { - candidateQueue.add(new ScoredCandidate( - LyricsRequests.composite(LyricsRequests.NEUTRAL, candidate, 0), - LyricsRequests.syncRank(candidate), candidate)); + synchronized (candidateQueue) { + boolean alreadyQueued = false; + for (ScoredCandidate queued : candidateQueue) { + if (fp.equals(LyricsMatch.fingerprint(queued.lyrics()))) { + alreadyQueued = true; + break; + } + } + if (!alreadyQueued) { + candidateQueue.add(new ScoredCandidate( + LyricsRequests.composite(LyricsRequests.NEUTRAL, candidate, 0), + LyricsRequests.syncRank(candidate), candidate)); + } } } - } - LyricsPanelInstaller.enableLyricsButton(); - Utils.runOnMainThreadDelayed(LyricsPanelInstaller::onLyricsPanelDetected, 300); - rememberPreference(display); -} + LyricsPanelInstaller.enableLyricsButton(); + Utils.runOnMainThreadDelayed(LyricsPanelInstaller::onLyricsPanelDetected, 300); + rememberPreference(display); + } -/** + /** * Puts the queue back in the order the user left it: candidates they had not reached yet * keep their remembered place, and anything new arrives after them in score order. */ -private void reorderQueueByRemembered() { + private void reorderQueueByRemembered() { List remembered = rememberedQueue; if (remembered == null || remembered.isEmpty()) { return; @@ -1279,15 +1222,15 @@ private List orderedQueueFingerprints() { } private void rememberPreference(Lyrics shown) { - TrackInfo track = currentTrack; + final TrackInfo track = currentTrack; if (track == null || shown == null || shown == Lyrics.NOT_FOUND || shown.isEmpty()) { return; } - String videoId = preferenceVideoId; - String queryTitle = searchQueryTitle; - String queryArtist = searchQueryArtist; - String fingerprint = LyricsMatch.fingerprint(shown); - List queue = orderedQueueFingerprints(); + final String videoId = preferenceVideoId; + final String queryTitle = searchQueryTitle; + final String queryArtist = searchQueryArtist; + final String fingerprint = LyricsMatch.fingerprint(shown); + final List queue = orderedQueueFingerprints(); preferredFingerprint = fingerprint; runOnLookupThread(() -> LyricsCache.putPreference(videoId, track, queryTitle, queryArtist, shown, queue, fingerprint)); @@ -1337,6 +1280,7 @@ private void enqueueWhileSuppressed(Lyrics lyrics, Lyrics prepared) { LyricsRequests.composite(LyricsRequests.NEUTRAL, lyrics, 0), sync, lyrics)); } } + public boolean hasSearchProviders() { return !LyricsProviders.searchableProviders( LyricsProviders.providersInOrder(Settings.LYRICS_SOURCE.get())).isEmpty(); @@ -1362,13 +1306,13 @@ public String[] rememberedSearchTerms() { */ public void searchWithCustomQuery(String title, String artist, boolean defaultTerms) { Utils.verifyOnMainThread(); - TrackInfo track = currentTrack; + final TrackInfo track = currentTrack; if (track == null) { return; } cancelPendingTrack(); - String queryTitle = title != null ? title.trim() : ""; - String queryArtist = artist != null ? artist.trim() : ""; + final String queryTitle = title != null ? title.trim() : ""; + final String queryArtist = artist != null ? artist.trim() : ""; if (queryTitle.isEmpty() || queryArtist.isEmpty()) { return; } @@ -1391,9 +1335,9 @@ public void searchWithCustomQuery(String title, String artist, boolean defaultTe // Video id providers only know the playing track, so they are asked only when the // terms are that track's own metadata. - List enabledProviders = + final List enabledProviders = LyricsProviders.providersInOrder(Settings.LYRICS_SOURCE.get()); - List providers = defaultTerms + final List providers = defaultTerms ? enabledProviders : LyricsProviders.searchableProviders(enabledProviders); if (providers.isEmpty()) { @@ -1402,15 +1346,15 @@ public void searchWithCustomQuery(String title, String artist, boolean defaultTe } runOnLookupThread(() -> { - String sbVideoId = skipSegmentsVideoId(); - List segs = SkipSegments.await(sbVideoId, + final String sbVideoId = skipSegmentsVideoId(); + final List segs = SkipSegments.await(sbVideoId, track.durationSeconds() * 1000L); if (id == requestId.get()) { displaySegsVideoId = sbVideoId; displaySegs = segs; } - TrackInfo searchTrack = LyricsMatch.withEffectiveDuration(track, segs); - TrackInfo queryTrack = defaultTerms + final TrackInfo searchTrack = LyricsMatch.withEffectiveDuration(track, segs); + final TrackInfo queryTrack = defaultTerms ? searchTrack : new TrackInfo(queryTitle, queryArtist, searchTrack.album(), searchTrack.durationSeconds()); @@ -1457,14 +1401,14 @@ public void searchWithCustomQuery(String title, String artist, boolean defaultTe private void runProviderLookup(int id, TrackInfo trackParam) { if (id != requestId.get()) return; - String sbVideoId = skipSegmentsVideoId(); - List segs = SkipSegments.await(sbVideoId, + final String sbVideoId = skipSegmentsVideoId(); + final List segs = SkipSegments.await(sbVideoId, trackParam.durationSeconds() * 1000L); if (id == requestId.get()) { displaySegsVideoId = sbVideoId; displaySegs = segs; } - TrackInfo track = LyricsMatch.withEffectiveDuration(trackParam, segs); + final TrackInfo track = LyricsMatch.withEffectiveDuration(trackParam, segs); final boolean suppressed = applyRememberedPreference(id, track); // Local files take priority: read embedded LYRICS/LYRIC tags before hitting providers. @@ -1485,7 +1429,7 @@ private void runProviderLookup(int id, TrackInfo trackParam) { String order = Settings.LYRICS_SOURCE.get(); List providers = LyricsProviders.providersInOrder(order); - TrackInfo queryTrack = customQueryTrack(track); + final TrackInfo queryTrack = customQueryTrack(track); if (queryTrack != null) { // The queue is being rebuilt from custom terms, which the providers keyed by // video id cannot answer. The cache is keyed by the playing track and holds what @@ -1706,11 +1650,6 @@ private Uri localUriFor(TrackInfo track) { track.title(), track.artist(), track.durationSeconds(), currentRawTitle, currentRawArtist); } - private record VariantQuery(TrackInfo track, int penalty) {} - - /** The two query tiers of a lookup: exact metadata first, spelling variants after. */ - private record Tiers(List original, List derived) {} - /** * Builds the query tiers for a lookup. */ @@ -1755,13 +1694,6 @@ private Tiers buildTiers(TrackInfo track, @Nullable String rawTitle) { return new Tiers(originalTier, derivedTier); } - private record LookupResult(@Nullable Lyrics best, - List scored, - Set providersWithResults, - Set providersFailed, - Set providersAttempted, - boolean queueFillPending) {} - private LookupResult fetchFromProviders(int id, List originalTier, List derivedTier, boolean[] failed, @@ -1783,8 +1715,8 @@ private LookupResult fetchFromProviders(int id, List originalTier, } } final int stage1Count = Math.min(4, nonBlind.size()); - List stage1Providers = nonBlind.subList(0, stage1Count); - List stage2Providers = nonBlind.subList( + final List stage1Providers = nonBlind.subList(0, stage1Count); + final List stage2Providers = nonBlind.subList( stage1Count, nonBlind.size()); CompletionService cs = new ExecutorCompletionService<>(fetchExecutor); @@ -1795,7 +1727,7 @@ private LookupResult fetchFromProviders(int id, List originalTier, AtomicBoolean threadFailed = new AtomicBoolean(false); List scoreCandidates = new ArrayList<>(); - DisplayState display = new DisplayState(wordSync, exhaustive); + final DisplayState display = new DisplayState(wordSync, exhaustive); for (VariantQuery vq : originalTier) { for (LyricsProvider provider : stage1Providers) { @@ -1954,15 +1886,13 @@ private int pollStage(int id, CompletionService cs, return completed; } - private static final int FIRST_PAINT_HOLD_MS = 800; - /** * The word-timed version of the same lyric usually lands moments after the line-synced one, * so a first result that carries only line timing waits out {@link #FIRST_PAINT_HOLD_MS} * rather than render the song twice. */ private void publishBest(int id, DisplayState display) { - Lyrics available = display.bestResult != null + final Lyrics available = display.bestResult != null ? display.bestResult : display.bestFallback; if (available == null || available == display.published) { return; @@ -1982,8 +1912,6 @@ private void publishBest(int id, DisplayState display) { display.published = available; publishFromLookup(id, available); } - record ProviderFetch(@Nullable Lyrics lyrics, int matchScore, boolean highMatch, - boolean blind, int penalty) {} private ProviderFetch fetchOne(LyricsProvider provider, VariantQuery vq, Set attempted, Set withResults, @@ -2022,8 +1950,8 @@ private void publishFromLookup(int id, Lyrics lyrics) { } catch (Throwable ignored) { display = lyrics; } - Lyrics raw = lyrics; - Lyrics prepared = display; + final Lyrics raw = lyrics; + final Lyrics prepared = display; Utils.runOnMainThread(() -> { Lyrics current = currentLyrics; if (id == requestId.get() && state == State.LOADED @@ -2117,7 +2045,7 @@ public void clearDialogSegment() { } public void clearDialogSegment(@NonNull String videoId, long startMs, long endMs) { - SkipSegments.Seg seg = dialogPreviewSeg; + final SkipSegments.Seg seg = dialogPreviewSeg; if (seg != null && videoId.equals(dialogPreviewVideoId) && seg.startMs() == startMs && seg.endMs() == endMs) { clearDialogSegment(); @@ -2125,14 +2053,10 @@ public void clearDialogSegment(@NonNull String videoId, long startMs, long endMs } /** - * The segments playback time is stripped of before it is compared with lyric - * timestamps. Null while the segment switch is off - nothing is read or requested - * then - or when the current lyrics are written against the video timeline - * themselves or playback is local. Every frame reads the freshest answer: an own fetch - * landing in the cache replaces what the SponsorBlock controller provided before it, - * and the previous latch only serves while no answer exists at all - no path that - * reads the position ever waits for one. An open submit dialog's candidate folds - * into the answer so its A-B range skips live, before anything is submitted. + * Segments playback time is stripped of before comparing with lyric timestamps. Null when + * the switch is off, the lyrics are video-timeline-matched, or playback is local. Reads the + * freshest answer every frame - an own fetch replaces the controller's once cached, the + * previous latch only serves while no answer exists, and a dialog's candidate folds in live. */ @Nullable private List syncSegs() { @@ -2142,48 +2066,54 @@ private List syncSegs() { if (LyricsMatch.isVideoIdMatchedLyrics(currentLyrics)) { return null; } - String videoId = skipSegmentsVideoId(); + final String videoId = skipSegmentsVideoId(); if (videoId == null) { return null; } - TrackInfo track = currentTrack; + final TrackInfo track = currentTrack; List base = SkipSegments.peek(videoId, track == null ? 0 : track.durationSeconds() * 1000L); if (base != null) { displaySegsVideoId = videoId; displaySegs = base; } else { - List latched = displaySegs; + final List latched = displaySegs; base = latched != null && videoId.equals(displaySegsVideoId) ? latched : null; if (base == null) { SkipSegments.prefetch(videoId); } } - SkipSegments.Seg preview = dialogPreviewSeg; + final SkipSegments.Seg preview = dialogPreviewSeg; if (preview == null || !videoId.equals(dialogPreviewVideoId)) { return base; } return SkipSegments.withSegment(base, preview); } + /** * Maps a lyric timestamp onto the video timeline a seek has to target: the inverse of * the subtraction {@link #getPositionMs()} applies before it reads a lyric time. */ public long toVideoTimeMs(long lyricTimeMs) { - List segs = syncSegs(); + final List segs = syncSegs(); if (segs == null || segs.isEmpty()) { return lyricTimeMs; } return SkipSegments.remapTimestamp(lyricTimeMs, segs); } + /** * Filters out credit lines and text the user asked to hide, then normalizes the timings * of synced lyrics, consulting the short-lived cache of already filtered results. */ private Lyrics prepareForDisplay(Lyrics lyrics) { + final boolean splitBackground = Settings.LYRICS_SPLIT_BACKGROUND.get(); String cacheKey = null; if (lyrics != null && lyrics != Lyrics.NOT_FOUND && !lyrics.isEmpty()) { - StringBuilder sb = new StringBuilder(lyrics.providerName()).append('|'); + // The split changes the lines, so it belongs in the key or toggling it hands back + // the other result. + StringBuilder sb = new StringBuilder(lyrics.providerName()) + .append(splitBackground ? "|bg" : "|"); List lines = lyrics.lines(); int limit = Math.min(3, lines.size()); for (int i = 0; i < limit; i++) { @@ -2199,6 +2129,10 @@ private Lyrics prepareForDisplay(Lyrics lyrics) { if (cacheKey == null || lyrics != filteredCache.get(cacheKey)) { lyrics = LyricsFilter.filterCreditLines(lyrics, currentTrack); lyrics = LyricsFilter.filterLyricsText(lyrics); + // After both filters, so a pattern the user wrote sees the lines as they arrived. + if (splitBackground) { + lyrics = LyricsBracketSplitter.apply(lyrics); + } if (cacheKey != null) { filteredCache.put(cacheKey, lyrics); } @@ -2228,9 +2162,9 @@ private void scheduleQueueFill(int id, boolean pending, List origi if (id != requestId.get() || !pending) { return; } - List fillOriginal = List.copyOf(original); - List fillDerived = List.copyOf(derived); - List fillProviders = List.copyOf(providers); + final List fillOriginal = List.copyOf(original); + final List fillDerived = List.copyOf(derived); + final List fillProviders = List.copyOf(providers); runOnLookupThread( () -> fillQueueInBackground(id, fillOriginal, fillDerived, fillProviders)); } @@ -2295,29 +2229,29 @@ private void detectSections(@Nullable Lyrics lyrics) { if (!SectionDetector.needsDetection(lyrics)) { return; } - TrackInfo track = currentTrack; - Lyrics target = lyrics; + final TrackInfo track = currentTrack; + final Lyrics target = lyrics; if (track == null || target == null) { return; } - String fingerprint = LyricsMatch.fingerprint(target); + final String fingerprint = LyricsMatch.fingerprint(target); if (fingerprint.equals(sectionsPendingFingerprint)) { return; } sectionsPendingFingerprint = fingerprint; SectionDetector.detect(track, target, () -> fingerprint.equals(sectionsPendingFingerprint), sections -> { - if (sectionsPendingFingerprint != null - && Objects.equals(sectionsPendingFingerprint, fingerprint)) { - sectionsPendingFingerprint = null; - } - Lyrics shown = currentLyrics; - boolean stillShowing = shown != null && fingerprint.equals(LyricsMatch.fingerprint(shown)); - if (sections == null || !stillShowing || currentTrack != track) { - return; - } - setState(State.LOADED, SectionDetector.applySections(shown, sections)); - }); + if (sectionsPendingFingerprint != null + && Objects.equals(sectionsPendingFingerprint, fingerprint)) { + sectionsPendingFingerprint = null; + } + Lyrics shown = currentLyrics; + boolean stillShowing = shown != null && fingerprint.equals(LyricsMatch.fingerprint(shown)); + if (sections == null || !stillShowing || currentTrack != track) { + return; + } + setState(State.LOADED, SectionDetector.applySections(shown, sections)); + }); } public boolean areLyricsSynced() { @@ -2341,4 +2275,52 @@ public String getCurrentLineText() { currentRawTitle, currentRawArtist); } + public enum State { + IDLE, + LOADING, + LOADED, + NOT_FOUND, + ERROR + } + + public interface Listener { + + /** Called on the main thread whenever the state or the lyrics change. */ + void onLyricsChanged(State state, @Nullable Lyrics lyrics); + } + + record ScoredCandidate(int score, int rank, Lyrics lyrics) + implements Comparable { + @Override + public int compareTo(ScoredCandidate other) { + if (score != other.score) return Integer.compare(other.score, score); + if (rank != other.rank) return Integer.compare(other.rank, rank); + return 0; + } + } + + private record PendingTrack(TrackInfo track, String rawTitle, String rawArtist, + @Nullable Uri mediaUri, String videoId) { + } + + private record VariantQuery(TrackInfo track, int penalty) { + } + + /** The two query tiers of a lookup: exact metadata first, spelling variants after. */ + private record Tiers(List original, List derived) { + } + + private record LookupResult(@Nullable Lyrics best, + List scored, + Set providersWithResults, + Set providersFailed, + Set providersAttempted, + boolean queueFillPending) { + } + + record ProviderFetch(@Nullable Lyrics lyrics, int matchScore, boolean highMatch, + boolean blind, int penalty) { + } + + } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/LyricsMatch.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/LyricsMatch.java index 5eec60ee8a..4ca5f589e4 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/LyricsMatch.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/LyricsMatch.java @@ -40,7 +40,7 @@ public static String cacheSourceFor(@Nullable Lyrics lyrics) { if (lyrics == null) { return ""; } - String providerName = lyrics.providerName(); + final String providerName = lyrics.providerName(); for (String id : LyricsProviders.PROVIDER_ORDER) { if (providerName.equals(id)) { return id; diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/LyricsPrefetcher.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/LyricsPrefetcher.java index 6eb5f961ea..4629245cb2 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/LyricsPrefetcher.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/LyricsPrefetcher.java @@ -24,18 +24,13 @@ import app.morphe.extension.shared.Utils; /** - * Fetches the lyrics of the track queued behind the current one, so the switch to it - * finds them in the cache instead of waiting for the network. + * Fetches the lyrics of the track queued behind the current one, so the switch finds them + * in the cache instead of waiting on the network. * - *

    The work runs on a thread of its own, which is the whole point of it: a prefetch - * must not be able to delay, cancel or take part in the lookup of the track that is - * actually playing. It therefore never touches the lookup request id, the in-flight - * fetches or the state, and it only ever adds to the lyrics cache. - * - *

    Only what the lookup itself would store is written, so a prefetch never decides - * which provider the user gets: the cache is keyed by provider, and a track no - * provider answered for keeps its miss, which leaves the usual search running when the - * track is reached. + *

    Work runs on its own thread and must never delay the lookup of the playing track: + * it never touches the request id, in-flight fetches or the state, and only adds to the + * lyrics cache. The cache is keyed by provider, so a prefetch never decides which provider + * the user gets, and a track no provider answered for keeps its miss. */ final class LyricsPrefetcher { @@ -48,9 +43,8 @@ final class LyricsPrefetcher { }); /** - * The video whose successor is being fetched, which is also the generation of the - * running task: a newer track replaces it, and the older task stops at its next - * checkpoint instead of fetching lyrics nobody asked for anymore. + * The video whose successor is being fetched, also the generation of the running task: + * a newer track replaces it, and the older task stops at its next checkpoint. */ private static volatile String prefetchingFor; @@ -69,7 +63,7 @@ static void prefetchSuccessorOf(@Nullable String currentVideoId) { if (!Settings.LYRICS_ENABLED.get()) return; if (!Utils.isNetworkConnected()) return; - Future previous; + final Future previous; synchronized (LyricsPrefetcher.class) { if (currentVideoId.equals(prefetchingFor)) return; prefetchingFor = currentVideoId; @@ -79,7 +73,7 @@ static void prefetchSuccessorOf(@Nullable String currentVideoId) { // Frees the single thread for the track that is playing now. previous.cancel(true); } - Future task = executor.submit(() -> runPrefetch(currentVideoId)); + final Future task = executor.submit(() -> runPrefetch(currentVideoId)); synchronized (LyricsPrefetcher.class) { if (currentVideoId.equals(prefetchingFor)) { running = task; @@ -105,7 +99,7 @@ private static void runPrefetch(String currentVideoId) { private static void prefetch(String currentVideoId) { if (superseded(currentVideoId)) return; - String[] nextVideoId = new String[1]; + final String[] nextVideoId = new String[1]; TrackInfo track = SessionQueue.nextTrackOf(currentVideoId, nextVideoId); if (superseded(currentVideoId)) return; if (track == null) return; @@ -120,7 +114,7 @@ private static void prefetch(String currentVideoId) { // The providers keyed by the video of the playing track only know that track, // so the successor is asked with the ones that search by title and artist. - List providers = LyricsProviders.searchableProviders( + final List providers = LyricsProviders.searchableProviders( LyricsProviders.providersInOrder(Settings.LYRICS_SOURCE.get())); if (providers.isEmpty()) return; if (allKnown(track, providers)) return; diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/LyricsPresentation.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/LyricsPresentation.java index a0cb7fad29..21ff10d0cb 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/LyricsPresentation.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/LyricsPresentation.java @@ -28,12 +28,12 @@ */ public final class LyricsPresentation { + private static final ArtistLineCycle artistCycle = new ArtistLineCycle(); @Nullable private static Lyrics timelineLyrics; private static long timelineDurationMs; @Nullable private static MiniLyricsTimeline timeline; - @Nullable private static LyricsLine chunksLine; private static long chunksEndMs; @@ -43,12 +43,6 @@ public final class LyricsPresentation { private static float chunksMultiplier = 1; private static List chunks = List.of(); - private static final ArtistLineCycle artistCycle = new ArtistLineCycle(); - - /** The lyric shown with a track format frame. The line tells apart repeats of the same text. */ - private record FrameKey(@Nullable LyricsLine line, @Nullable String text) { - } - private LyricsPresentation() { } @@ -103,7 +97,7 @@ private static String present(@Nullable String text, @Nullable LyricsLine line, * The text the miniplayer and the media session show at a playback position. * * @return Null if no lyric applies and the track title should be shown instead. - * An empty string is a blank instrumental frame the user configured. + * An empty string is a blank instrumental frame the user configured. */ @Nullable public static String miniLine(Lyrics lyrics, long durationMs, long positionMs, @@ -159,4 +153,8 @@ private static String vocalText(List lines, MiniLyricsTimeline.Vocal return MiniLyricsFormatter.current(chunks, positionMs, Settings.LYRICS_MINI_ENCAPSULATION.get(), MiniLyricsFormatter.repeatCount(lines, vocal.index())); } + + /** The lyric shown with a track format frame. The line tells apart repeats of the same text. */ + private record FrameKey(@Nullable LyricsLine line, @Nullable String text) { + } } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/LyricsProviders.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/LyricsProviders.java index 30a58f8ddd..da6c0f5848 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/LyricsProviders.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/LyricsProviders.java @@ -68,7 +68,8 @@ public static List searchableProviders(List prov List result = new ArrayList<>(providers.size()); for (LyricsProvider provider : providers) { switch (provider.name()) { - case "YTMusic", "Captions", "Unison", "SimpMusic" -> { } + case "YTMusic", "Captions", "Unison", "SimpMusic" -> { + } default -> result.add(provider); } } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/SessionQueue.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/SessionQueue.java index 8b2dd30fe5..8b557c6ba9 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/SessionQueue.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/SessionQueue.java @@ -20,15 +20,13 @@ import app.morphe.extension.shared.Logger; /** - * Reads the track that plays after a given one from the queue the app published to its - * own media session. + * Reads the track that plays after a given one from the queue the app published to its own + * media session. * - *

    The media session is part of the platform, so this needs no other patch: whatever - * the app hands the system, the controller of that same session hands back. An app that - * publishes none simply yields nothing here, which leaves the caller as it was. - * - *

    Reading it means calling across the session binder, so it blocks and must not run - * on the main thread. + *

    The media session is part of the platform, so no other patch is needed: whatever the app + * hands the system, the controller of that same session hands back; an app that publishes + * none yields nothing here. Reading it crosses the session binder, so it blocks and must not + * run on the main thread. */ final class SessionQueue { @@ -36,13 +34,10 @@ private SessionQueue() { } /** - * Matching is by the item media id, so the answer stays right while the session is - * still catching up with a switch. The item after the last one is the first one, - * which is where autoplay and a repeat of a single track land. - * - *

    {@code videoIdOut} receives the media id of the successor. Comparing it with - * the playing id tells a repeat from a successor, and any other value is only ever - * a title to prefetch. + * Matching is by item media id, so the answer stays right while the session catches up + * with a switch. The item after the last is the first - where autoplay and single-track + * repeat land. {@code videoIdOut} receives the successor's media id: equal to the playing + * id it is a repeat, any other value is only a title to prefetch. */ @Nullable static TrackInfo nextTrackOf(@NonNull String currentVideoId, @@ -87,7 +82,7 @@ private static List queueItems() { @Nullable private static TrackInfo trackOf(@Nullable MediaSession.QueueItem item, - @NonNull String[] videoIdOut) { + @NonNull String[] videoIdOut) { if (item == null) return null; videoIdOut[0] = mediaIdOf(item); diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/SkipSegments.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/SkipSegments.java index 2f6285672e..2278908662 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/SkipSegments.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/SkipSegments.java @@ -39,33 +39,21 @@ /** * SponsorBlock skip segments used to align lyrics with playback. * - *

    Those lyrics are timed against the released audio, but the player reports video time, so - * segments have to be dropped from playback time ({@link #contentMs}), added back on a seek - * ({@link #remapTimestamp}) and counted out of the duration asked with ({@link - * #effectiveDurationMs}). - * - *

    Everything follows the lyrics SponsorBlock switch: off means nothing is requested and - * every answer is null. Failures are silent and retried after a cooldown, so an unreachable - * service costs one background round trip; until an own answer exists, the segments - * SponsorBlock itself downloaded are used ({@link #fromController}). - * - *

    {@link #await} blocks, so only lookup threads may call it. {@link #peek} never blocks. + *

    Lyrics are timed against the released audio but the player reports video time, so segments + * are dropped from playback time ({@link #contentMs}), added back on a seek + * ({@link #remapTimestamp}) and counted out of the duration asked with + * ({@link #effectiveDurationMs}). Off means nothing is requested and every answer is null. + * Failures are silent and retried after a cooldown; until an own answer exists the segments + * SponsorBlock itself downloaded are used ({@link #fromController}). {@link #await} blocks, so + * only lookup threads may call it; {@link #peek} never blocks. */ public final class SkipSegments { - public record Seg(long startMs, long endMs, @NonNull String category, - long reportedDurationMs) { - } - private static final long REPORTED_DURATION_TOLERANCE_MS = 5_000; - private static final int TIMEOUT_TCP_MS = 7000; private static final int TIMEOUT_HTTP_MS = 10000; - private static final long AWAIT_TIMEOUT_MS = 4000; - private static final int CACHE_MAX_ENTRIES = 16; - /** * Every music relevant category, URL-encoded the way {@code SegmentCategory} builds it. * All of them separate the video from the released audio, so all count: whether the @@ -74,25 +62,18 @@ public record Seg(long startMs, long endMs, @NonNull String category, private static final String FETCH_CATEGORIES = "[%22sponsor%22,%22selfpromo%22," + "%22interaction%22,%22intro%22,%22outro%22,%22preview%22,%22hook%22," + "%22filler%22,%22music_offtopic%22]"; - private static final Object LOCK = new Object(); - /** Video id to merged segments; empty when the fetch found none. */ private static final Map> CACHE = Utils .createSizeRestrictedMap(CACHE_MAX_ENTRIES, true); - private static final Map>> TASKS = new HashMap<>(); - /** * When the next attempt after a failed fetch may start. A failure is never cached, so * without this the playback path would request every frame. */ private static final Map RETRY_AFTER = new HashMap<>(); - private static final Map FETCH_EPOCH = new HashMap<>(); - private static final long RETRY_COOLDOWN_MS = 15_000; - private static final ExecutorService EXECUTOR = Executors.newSingleThreadExecutor(runnable -> { Thread thread = new Thread(runnable, "MorpheSkipSegments"); thread.setDaemon(true); @@ -129,7 +110,7 @@ public static void invalidate(@NonNull String videoId) { * * @param durationMs A segment recorded against a different duration is stale, dropped. * @return The merged segments; empty when the video has none, null when unavailable or - * off. Both mean no correction applies. + * off. Both mean no correction applies. */ @Nullable public static List await(@Nullable String videoId, long durationMs) { @@ -244,11 +225,10 @@ private static long unionMs(@NonNull List segs) { } /** - * The check every public answer passes: no answer stays null, a video without segments - * is answered with an empty list, and a segment whose reported video duration - * disagrees with the playing one is stale - the video was recut since it was submitted - * - and is dropped. Nothing is discarded by coverage: identity by video id is exact, - * and a long intro and outro can legitimately take half the video. + * The check every public answer passes: no answer stays null, a video without segments is + * answered with an empty list, and a segment whose reported video duration disagrees with + * the playing one is stale - the video was recut - and dropped. Nothing is discarded by + * coverage: identity by video id is exact, and a long intro/outro can take half the video. */ @Nullable private static List applicable(@Nullable List segs, long durationMs) { @@ -272,7 +252,7 @@ private static List applicable(@Nullable List segs, long durationMs) { if (!stale) { return segs; } - List kept = new ArrayList<>(segs.size()); + final List kept = new ArrayList<>(segs.size()); for (Seg seg : segs) { final long reported = seg.reportedDurationMs(); if (reported > 0 && Math.abs(reported - durationMs) > REPORTED_DURATION_TOLERANCE_MS) { @@ -284,13 +264,11 @@ private static List applicable(@Nullable List segs, long durationMs) { } /** - * The segments SponsorBlock downloaded for the video that is currently playing. They - * are the same answer the own fetch asks for, available whenever SponsorBlock itself - * resolved this video - the fallback for a fetch that has not answered yet. Only the - * controller's own video id vouches for them: the controller clears its data when the - * video changes, but a reader can still race that clear, so an id of its own is what - * makes the segments usable and anything else is ignored. Not cached: the controller - * owns the data and refreshes it when its own fetch lands. + * The segments SponsorBlock downloaded for the playing video - the same answer the own + * fetch asks for, available before it answers. Only the controller's own video id vouches + * for them: the controller clears its data on video change but a reader can race that + * clear, so anything else is ignored. Not cached: the controller owns the data and + * refreshes it when its own fetch lands. */ @Nullable private static List fromController(@NonNull String videoId) { @@ -356,7 +334,7 @@ private static List fetchFor(String videoId, int epoch) { /** * Asks the SponsorBlock API for the segments of a video. Empty means the video has - * none; null means the service could not answer, which is not an answer at all. + * none; null means the service could not answer - not an answer at all. */ @Nullable private static List fetch(String videoId) throws IOException, JSONException { @@ -393,9 +371,9 @@ private static List fetch(String videoId) throws IOException, JSONException } /** - * The segments of {@code base} with one more segment folded in, stored the way the - * cache keeps them: sorted and merged. A null or empty base leaves only the extra - * segment. Used by the submit dialog to preview its candidate before submission. + * The segments of {@code base} with one more folded in, stored sorted and merged like the + * cache. A null or empty base leaves only the extra segment. Used by the submit dialog to + * preview its candidate before submission. */ @NonNull public static List withSegment(@Nullable List base, @NonNull Seg extra) { @@ -431,4 +409,8 @@ private static List merge(@NonNull List sorted) { merged.add(current); return Collections.unmodifiableList(merged); } + + public record Seg(long startMs, long endMs, @NonNull String category, + long reportedDurationMs) { + } } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/derive/AiProviders.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/derive/AiProviders.java index d65290a71f..859f6ea678 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/derive/AiProviders.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/derive/AiProviders.java @@ -9,57 +9,196 @@ import androidx.annotation.Nullable; +import java.util.ArrayList; +import java.util.HashSet; import java.util.List; +import java.util.Set; import app.morphe.extension.music.settings.Settings; /** - * The translation and romanization providers a user can pick, and the one place a request is - * sent to the AI model the picked provider stands for. + * Translation and romanization provider ids, and the AI request path they share. Provider + * settings are comma separated priority lists; a leading {@code -} disables an entry. */ public final class AiProviders { + /** Translation and romanization already included in the lyrics source. */ + public static final String EMBEDDED = "embedded"; public static final String GOOGLE = "google"; public static final String LOCAL = "local"; - public static final String GEMINI = "gemini"; + /** The Rime word tables, the user's own lists included. Named "Rime Wordlist" in the settings. */ + public static final String RIME = "rime"; public static final String OPENAI_COMPATIBLE = "openai_compatible"; public static final String DEEPL = "deepl"; - /** What a model answered, and the model that answered it. */ - record Result(List lines, String model) { - } + /** Brand names, kept out of the string resources so no translation can restate them. */ + public static final String EMBEDDED_LABEL = "Embedded"; + public static final String GOOGLE_LABEL = "Google Translate"; + public static final String OPENAI_COMPATIBLE_LABEL = "OpenAI Compatible"; + public static final String DEEPL_LABEL = "DeepL"; + + public static final List TRANSLATION_ORDER = + List.of(EMBEDDED, GOOGLE, OPENAI_COMPATIBLE, DEEPL); + public static final List ROMANIZATION_ORDER = + List.of(EMBEDDED, GOOGLE, LOCAL, RIME, OPENAI_COMPATIBLE); + + public static final String DEFAULT_TRANSLATION_ORDER = + "embedded,google,-openai_compatible,-deepl"; + public static final String DEFAULT_ROMANIZATION_ORDER = + "embedded,google,-local,-rime,-openai_compatible"; + /** Endpoints offered by name, in the order they are listed. */ + public static final List ENDPOINT_PRESETS = List.of( + EndpointPreset.of("OpenAI", "https://api.openai.com/v1"), + EndpointPreset.of("Anthropic Claude", "https://api.anthropic.com/v1"), + // Its compatibility layer lists no models and wants the "models/" form. + EndpointPreset.of("Gemini", + "https://generativelanguage.googleapis.com/v1beta/openai", + "models/gemini-2.5-flash"), + EndpointPreset.of("OpenRouter", "https://openrouter.ai/api/v1"), + EndpointPreset.of("DeepSeek", "https://api.deepseek.com/v1"), + EndpointPreset.of("Groq", "https://api.groq.com/openai/v1"), + EndpointPreset.of("Mistral", "https://api.mistral.ai/v1"), + EndpointPreset.of("xAI Grok", "https://api.x.ai/v1"), + EndpointPreset.of("Together AI", "https://api.together.xyz/v1"), + EndpointPreset.of("Qwen (DashScope)", + "https://dashscope.aliyuncs.com/compatible-mode/v1"), + EndpointPreset.of("Z.AI (GLM)", "https://open.bigmodel.cn/api/paas/v4"), + EndpointPreset.of("Moonshot (Kimi)", "https://api.moonshot.cn/v1"), + EndpointPreset.of("Pollinations (free)", "https://text.pollinations.ai/openai")); private AiProviders() { } - public static boolean isAi(@Nullable String provider) { - return GEMINI.equals(provider) || OPENAI_COMPATIBLE.equals(provider); + /** Parse the stored list; a provider missing from it is enabled by default. */ + public static List parseOrder(@Nullable String stored, List known) { + List entries = new ArrayList<>(known.size()); + Set seen = new HashSet<>(known.size()); + + if (stored != null) { + for (String raw : stored.split(",")) { + String token = raw.trim(); + if (token.isEmpty()) { + continue; + } + boolean enabled = true; + if (token.startsWith("-")) { + enabled = false; + token = token.substring(1).trim(); + } + if (token.isEmpty() || !known.contains(token) || !seen.add(token)) { + continue; + } + entries.add(new ProviderEntry(token, enabled)); + } + } + + for (String id : known) { + if (!seen.contains(id)) { + // Embedded readings used to be unconditional, so keep them ahead of older lists. + if (EMBEDDED.equals(id)) { + entries.add(0, new ProviderEntry(id, true)); + } else if (RIME.equals(id)) { + // Rime Wordlist fetches word lists; leave it off for stored lists that predate + // it rather than downloading data an upgrade did not ask for. + entries.add(afterLocal(entries), new ProviderEntry(id, false)); + } else { + entries.add(new ProviderEntry(id, true)); + } + } + } + return entries; } - /** - * Sends the prompt to the model of an AI provider. Null when the provider is not an AI one, - * has no key yet, or gave no usable answer, so the caller falls back to Google. - */ - @Nullable - static Result mapLines(String provider, String prompt, List lines) { - String model; - List mapped; - switch (provider) { - case GEMINI -> { - String apiKey = Settings.LYRICS_GEMINI_API_KEY.get(); - if (apiKey.isEmpty()) return null; - model = Settings.LYRICS_GEMINI_MODEL.get(); - mapped = GeminiClient.mapLines(apiKey, model, prompt, lines); + /** Where a provider the stored list never named belongs: just after the local dictionaries. */ + private static int afterLocal(List entries) { + for (int i = 0; i < entries.size(); i++) { + if (LOCAL.equals(entries.get(i).id())) { + return i + 1; } - case OPENAI_COMPATIBLE -> { - model = Settings.LYRICS_AI_MODEL.get(); - mapped = OpenAIClient.mapLines(Settings.LYRICS_AI_BASE_URL.get(), - Settings.LYRICS_AI_API_TOKEN.get(), model, prompt, lines); + } + return entries.size(); + } + + public static List enabledProviders(@Nullable String stored, List known) { + List enabled = new ArrayList<>(); + for (ProviderEntry entry : parseOrder(stored, known)) { + if (entry.enabled()) { + enabled.add(entry.id()); } - default -> { - return null; + } + return enabled; + } + + public static String serializeOrder(List entries) { + StringBuilder sb = new StringBuilder(); + for (ProviderEntry entry : entries) { + if (sb.length() > 0) { + sb.append(','); + } + if (!entry.enabled()) { + sb.append('-'); } + sb.append(entry.id()); } + return sb.toString(); + } + + /** Rewrites one provider's flag, returning {@code stored} untouched when it already matches. */ + public static String withProviderEnabled(@Nullable String stored, List known, + String id, boolean enabled) { + final List entries = parseOrder(stored, known); + boolean changed = false; + final List updated = new ArrayList<>(entries.size()); + for (ProviderEntry entry : entries) { + if (entry.id().equals(id) && entry.enabled() != enabled) { + updated.add(new ProviderEntry(id, enabled)); + changed = true; + } else { + updated.add(entry); + } + } + return changed ? serializeOrder(updated) : stored; + } + + private static String currentId(String id) { + return switch (id) { + // An early build named the entry after the endpoint it talks to. + case "openai", "openrouter" -> OPENAI_COMPATIBLE; + default -> id; + }; + } + + + public static boolean isAi(@Nullable String provider) { + return OPENAI_COMPATIBLE.equals(provider); + } + + /** Null when the provider cannot answer, so the caller can try the next one. */ + @Nullable + static Result mapLines(String provider, String prompt, List lines) { + if (!OPENAI_COMPATIBLE.equals(provider)) { + return null; + } + final String model = Settings.LYRICS_AI_MODEL.get(); + final List mapped = OpenAIClient.mapLines(Settings.LYRICS_AI_BASE_URL.get(), + Settings.LYRICS_AI_API_TOKEN.get(), model, prompt, lines); return mapped == null ? null : new Result(mapped, model); } + + public record ProviderEntry(String id, boolean enabled) { + } + + /** A service offered by name; the URL is stored as the client will call it. */ + public record EndpointPreset(String label, String baseUrl, String model) { + public static EndpointPreset of(String label, String baseUrl) { + return new EndpointPreset(label, OpenAIClient.chatUrl(baseUrl), ""); + } + + public static EndpointPreset of(String label, String baseUrl, String model) { + return new EndpointPreset(label, OpenAIClient.chatUrl(baseUrl), model); + } + } + + record Result(List lines, String model) { + } } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/derive/GeminiClient.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/derive/GeminiClient.java deleted file mode 100644 index ccee474c68..0000000000 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/derive/GeminiClient.java +++ /dev/null @@ -1,156 +0,0 @@ -/* - * Copyright 2026 Morphe. - * https://github.com/MorpheApp/morphe-patches/pull/3877 - * - * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. - */ - -package app.morphe.extension.music.patches.lyrics.derive; - -import androidx.annotation.Nullable; - -import org.json.JSONArray; -import org.json.JSONObject; - -import java.io.OutputStream; -import java.net.HttpURLConnection; -import java.net.URL; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.List; - -import app.morphe.extension.shared.Logger; - -/** - * The Gemini API speaks its own protocol, not the OpenAI one, so it has a client of its own. - * The prompt and the parsing of the answer are shared with {@link OpenAIClient}. - */ -public final class GeminiClient { - - private static final int CONNECT_TIMEOUT_MS = 10_000; - private static final int READ_TIMEOUT_MS = 45_000; - - private static final String BASE_URL = "https://generativelanguage.googleapis.com/v1beta"; - private static final String MODEL_PREFIX = "models/"; - /** Sent as a header rather than in the query, where it would end up in request logs. */ - private static final String API_KEY_HEADER = "x-goog-api-key"; - - private GeminiClient() { - } - - @Nullable - public static List mapLines(String apiKey, String model, String prompt, List sourceLines) { - if (!OpenAIClient.fitsOneRequest(sourceLines)) { - return null; - } - return OpenAIClient.mapResponse(request(apiKey, model, prompt), sourceLines); - } - - @Nullable - private static String request(String apiKey, String model, String prompt) { - try { - String cleanModel = model.startsWith(MODEL_PREFIX) ? model.substring(MODEL_PREFIX.length()) : model; - - JSONObject root = new JSONObject(); - JSONObject userContent = new JSONObject() - .put("role", "user") - .put("parts", new JSONArray().put(new JSONObject().put("text", prompt))); - root.put("contents", new JSONArray().put(userContent)); - root.put("generationConfig", new JSONObject().put("temperature", 0.2)); - - byte[] payload = root.toString().getBytes(StandardCharsets.UTF_8); - - HttpURLConnection conn = (HttpURLConnection) new URL( - BASE_URL + "/" + MODEL_PREFIX + cleanModel + ":generateContent").openConnection(); - try { - conn.setRequestMethod("POST"); - conn.setConnectTimeout(CONNECT_TIMEOUT_MS); - conn.setReadTimeout(READ_TIMEOUT_MS); - conn.setRequestProperty(API_KEY_HEADER, apiKey); - conn.setRequestProperty("Content-Type", "application/json; charset=utf-8"); - conn.setDoOutput(true); - - try (OutputStream os = conn.getOutputStream()) { - os.write(payload); - } - - int code = conn.getResponseCode(); - if (code != 200) { - Logger.printDebug(() -> "Gemini API error code: " + code); - return null; - } - - JSONObject resp = new JSONObject(OpenAIClient.readResponse(conn)); - JSONArray candidates = resp.optJSONArray("candidates"); - if (candidates == null || candidates.length() == 0) { - return null; - } - JSONObject content = candidates.getJSONObject(0).optJSONObject("content"); - if (content == null) { - return null; - } - JSONArray parts = content.optJSONArray("parts"); - if (parts == null || parts.length() == 0) { - return null; - } - return parts.getJSONObject(0).optString("text", null); - } finally { - conn.disconnect(); - } - } catch (Exception ex) { - Logger.printDebug(() -> "Gemini request failed", ex); - return null; - } - } - - /** The Gemini models that can generate text, empty when none could be listed. */ - public static List fetchModels(String apiKey) { - List models = new ArrayList<>(); - if (apiKey.isEmpty()) { - return models; - } - try { - HttpURLConnection conn = (HttpURLConnection) new URL(BASE_URL + "/models").openConnection(); - try { - conn.setRequestMethod("GET"); - conn.setConnectTimeout(CONNECT_TIMEOUT_MS); - conn.setReadTimeout(READ_TIMEOUT_MS); - conn.setRequestProperty(API_KEY_HEADER, apiKey); - - if (conn.getResponseCode() != 200) { - return models; - } - - JSONArray modelArr = new JSONObject(OpenAIClient.readResponse(conn)).optJSONArray("models"); - if (modelArr == null) { - return models; - } - for (int i = 0; i < modelArr.length(); i++) { - JSONObject m = modelArr.getJSONObject(i); - String name = m.optString("name", ""); - if (name.startsWith(MODEL_PREFIX) && name.contains("gemini") - && supportsGenerateContent(m.optJSONArray("supportedGenerationMethods"))) { - models.add(name.substring(MODEL_PREFIX.length())); - } - } - } finally { - conn.disconnect(); - } - } catch (Exception ex) { - Logger.printDebug(() -> "Gemini fetchModels failed", ex); - } - return models; - } - - private static boolean supportsGenerateContent(@Nullable JSONArray methods) { - if (methods == null) { - return false; - } - for (int i = 0; i < methods.length(); i++) { - if ("generateContent".equals(methods.optString(i))) { - return true; - } - } - return false; - } -} diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/derive/LyricsRomanizer.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/derive/LyricsRomanizer.java index d42b569233..6cdf43e98a 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/derive/LyricsRomanizer.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/derive/LyricsRomanizer.java @@ -25,10 +25,9 @@ import app.morphe.extension.music.patches.lyrics.model.Word; import app.morphe.extension.music.patches.lyrics.requests.LyricsRequests; import app.morphe.extension.music.patches.lyrics.romanizers.HangulRomanizer; -import app.morphe.extension.music.patches.lyrics.romanizers.JapaneseDictStore; import app.morphe.extension.music.patches.lyrics.romanizers.KanaRomanizer; import app.morphe.extension.music.patches.lyrics.romanizers.LocalRomanizer; -import app.morphe.extension.music.patches.lyrics.romanizers.PinyinDictStore; +import app.morphe.extension.music.patches.lyrics.romanizers.RimeDictStore; import app.morphe.extension.music.patches.lyrics.romanizers.ScriptDetector; import app.morphe.extension.music.patches.lyrics.storage.LyricsCache; import app.morphe.extension.music.settings.Settings; @@ -38,15 +37,9 @@ public final class LyricsRomanizer { - public interface Callback { - void onRomanized(@Nullable List romanizedLines, - boolean fromGoogle, boolean fromAI, @Nullable String aiModel, - boolean perWord, @Nullable String localDictionary); - - } - private static final String RULE_BASED_DICTIONARY = "Local (Rule-based)"; - + /** A provider reading must cover at least this share of the lyric to be usable. */ + private static final double MIN_READING_COVERAGE = 0.40; private static final ExecutorService executor = Executors.newSingleThreadExecutor(); private LyricsRomanizer() { @@ -55,6 +48,7 @@ private LyricsRomanizer() { @Nullable public static List getEmbeddedRomanization(Lyrics lyrics) { if (lyrics == null) return null; + final boolean allowTransliteration = Settings.LYRICS_ALLOW_TRANSLITERATION.get(); final int lineCount = lyrics.lines() != null ? lyrics.lines().size() : 0; List embedded = lyrics.romanization(); if (embedded != null && embedded.size() != lineCount) { @@ -67,40 +61,224 @@ public static List getEmbeddedRomanization(Lyrics lyrics) { LyricsMerge.hasBackgroundTrack(lyrics, romanizations)); } } + // Kana and hangul are phonetic scripts, so a reading written in them is still a + // romanization. Other non-Latin readings are transliterations and wait for the gate. + embedded = normalizeReadingScripts(embedded); if (LyricsMerge.hasText(embedded)) { return embedded; } // Otherwise the line reading is built from the readings of its words. if (lyrics.lines() != null && LyricsMerge.anyWordHasRomaji(lyrics.lines())) { List synthesized = new ArrayList<>(lyrics.lines().size()); + int totalWordChars = 0; + int usableWordChars = 0; for (LyricsLine l : lyrics.lines()) { if (l.hasWords()) { StringBuilder sb = new StringBuilder(); for (Word w : l.words()) { - String r = w.romaji(); - String part = (r != null && !r.trim().isEmpty() && !ScriptDetector.containsCjk(r) && !isDegradedRomajiToken(r)) - ? r.trim() - : (ScriptDetector.containsKana(w.text()) ? KanaRomanizer.romanize(w.text()) : ""); + String text = w.text() != null ? w.text().trim() : ""; + final boolean counted = hasLetterOrDigit(text); + if (counted) { + totalWordChars += text.length(); + } + String part = wordReading(w, allowTransliteration); part = stripParentheses(part).trim(); - if (!part.isEmpty() && !ScriptDetector.containsCjk(part)) { + if (!part.isEmpty() && (allowTransliteration || isPurelyLatinScript(part))) { sb.append(part); if (w.endsWithSpace() && !part.endsWith(" ")) { sb.append(' '); } + if (counted) { + usableWordChars += text.length(); + } } } - synthesized.add(new LyricsLine(l.startTimeMs(), sb.toString().trim())); + synthesized.add(new LyricsLine(l.startTimeMs(), l.endTimeMs(), sb.toString().trim(), + Collections.emptyList(), l.agentId(), l.isDuet(), l.isBG(), l.songPart())); } else { - synthesized.add(new LyricsLine(l.startTimeMs(), "")); + synthesized.add(new LyricsLine(l.startTimeMs(), l.endTimeMs(), "", + Collections.emptyList(), l.agentId(), l.isDuet(), l.isBG(), l.songPart())); } } - if (LyricsMerge.hasText(synthesized)) { + // A word track that only covers a few particles is not a reading. Fall back to a + // whole-line provider instead of showing mostly empty lines. + if (totalWordChars > 0 + && (double) usableWordChars / totalWordChars >= MIN_READING_COVERAGE + && LyricsMerge.hasText(synthesized)) { return synthesized; } } return null; } + /** + * The text to show for one word's shipped reading. Kana and hangul are romanized, a + * non-Latin reading is kept only when the user allows transliteration, and a Latin reading + * is kept unless it looks like a broken fragment. + */ + public static String wordReading(Word word, boolean allowTransliteration) { + String reading = word.romaji(); + if (reading != null && !reading.trim().isEmpty()) { + reading = reading.trim(); + // A tilde is only an artifact when the written word does not have one. + if (!isDegradedRomajiToken(reading, containsTilde(word.text()))) { + if (ScriptDetector.contains(reading, Character.UnicodeScript.HAN)) { + if (allowTransliteration) { + return reading; + } + // Not allowed: fall through to the word itself, which may still have + // kana or hangul that can be romanized. + } else if (ScriptDetector.contains(reading, Character.UnicodeScript.HANGUL)) { + reading = HangulRomanizer.romanize(reading).trim(); + } else if (ScriptDetector.containsKana(reading)) { + reading = KanaRomanizer.romanize(reading).trim(); + } + if (allowTransliteration || isPurelyLatinScript(reading)) { + return reading; + } + } + } + // Fall back to the word itself when it is a phonetic script with no reading. + String original = word.text(); + if (original != null && !ScriptDetector.contains(original, Character.UnicodeScript.HAN)) { + String fallback = null; + if (ScriptDetector.contains(original, Character.UnicodeScript.HANGUL)) { + fallback = HangulRomanizer.romanize(original).trim(); + } else if (ScriptDetector.containsKana(original)) { + fallback = KanaRomanizer.romanize(original).trim(); + } + if (fallback != null && (allowTransliteration || isPurelyLatinScript(fallback))) { + return fallback; + } + } + return ""; + } + + /** + * Romanizes provider readings written in kana or hangul. Only a line made up entirely of + * that phonetic script is converted, so mixed text is never left half romanized. + */ + @Nullable + private static List normalizeReadingScripts(@Nullable List lines) { + if (lines == null || lines.isEmpty()) { + return lines; + } + List out = null; + for (int i = 0; i < lines.size(); i++) { + LyricsLine line = lines.get(i); + String text = line.text(); + if (text == null || text.isEmpty()) { + continue; + } + String converted = text; + if (isHangulOnly(text)) { + String romanized = HangulRomanizer.romanize(text); + if (!ScriptDetector.contains(romanized, Character.UnicodeScript.HANGUL)) { + converted = romanized; + } + } else if (isKanaOnly(text)) { + String romanized = KanaRomanizer.romanize(text); + if (!ScriptDetector.containsKana(romanized)) { + converted = romanized; + } + } + if (!converted.equals(text)) { + if (out == null) { + out = new ArrayList<>(lines); + } + // Word timings belong to the original line, so the converted reading keeps + // only the line's own timing. + out.set(i, new LyricsLine(line.startTimeMs(), line.endTimeMs(), converted, + Collections.emptyList(), line.agentId(), line.isDuet(), line.isBG(), + line.songPart())); + } + } + return out != null ? out : lines; + } + + private static boolean isKanaOnly(String text) { + boolean hasKana = false; + for (int i = 0; i < text.length(); ) { + int cp = Character.codePointAt(text, i); + i += Character.charCount(cp); + Character.UnicodeScript script = Character.UnicodeScript.of(cp); + if (script == Character.UnicodeScript.HIRAGANA + || script == Character.UnicodeScript.KATAKANA) { + hasKana = true; + continue; + } + if (cp == 0x30FC || cp == 0x2014) { + continue; // Prolonged sound mark and dash. + } + if (Character.isLetter(cp)) { + return false; + } + } + return hasKana; + } + + private static boolean isHangulOnly(String text) { + boolean hasHangul = false; + for (int i = 0; i < text.length(); ) { + int cp = Character.codePointAt(text, i); + i += Character.charCount(cp); + if (Character.UnicodeScript.of(cp) == Character.UnicodeScript.HANGUL) { + hasHangul = true; + continue; + } + if (Character.isLetter(cp)) { + return false; + } + } + return hasHangul; + } + + /** Whether any word ships a reading that the current setting allows. */ + private static boolean hasUsableWordReading(@Nullable List lines, + boolean allowTransliteration) { + if (lines == null) { + return false; + } + for (LyricsLine line : lines) { + for (Word word : line.words()) { + if (!wordReading(word, allowTransliteration).isEmpty()) { + return true; + } + } + } + return false; + } + + /** Whether one line's word readings supply an allowed reading. */ + private static boolean hasUsableWordReadingForLine(LyricsLine line, + @Nullable LyricsLine original, + boolean allowTransliteration) { + List words = line.words().isEmpty() && original != null + ? original.words() : line.words(); + for (Word word : words) { + if (!wordReading(word, allowTransliteration).isEmpty()) { + return true; + } + } + return false; + } + + /** Whether a token has anything worth measuring, rather than only punctuation. */ + private static boolean hasLetterOrDigit(String text) { + for (int i = 0; i < text.length(); ) { + int cp = Character.codePointAt(text, i); + i += Character.charCount(cp); + if (Character.isLetterOrDigit(cp)) { + return true; + } + } + return false; + } + + private static boolean containsTilde(@Nullable String text) { + return text != null && (text.contains("~") || text.contains("~")); + } + public static void romanize(TrackInfo track, Lyrics lyrics, String source, Callback callback) { Utils.verifyOnMainThread(); @@ -113,14 +291,29 @@ public static void romanize(TrackInfo track, Lyrics lyrics, String source, Callb } final boolean isWordSynced = anyLineHasWords; - // A reading the provider ships with wins, unless it is broken into fragments. - List embedded = getEmbeddedRomanization(lyrics); - final boolean perWord = LyricsMerge.anyWordHasRomaji(lyrics.lines()); - if ((LyricsMerge.hasText(embedded) || perWord) - && !isProviderRomajiDegraded(embedded, lyrics.lines(), perWord)) { - List result = alignIfWordSynced(lyrics, embedded, isWordSynced); - boolean hasWordRomaji = perWord || (isWordSynced && result != null && LyricsMerge.anyWordHasRomaji(result)); - Utils.runOnMainThread(() -> callback.onRomanized(result, false, false, null, hasWordRomaji, null)); + // The source can include its own reading; it is just another provider in the list. + final boolean allowTransliteration = Settings.LYRICS_ALLOW_TRANSLITERATION.get(); + final List providers = AiProviders.enabledProviders( + Settings.LYRICS_ROMANIZATION_PROVIDERS.get(), + AiProviders.ROMANIZATION_ORDER); + if (providers.isEmpty()) { + callback.onRomanized(null, false, false, null, false, null); + return; + } + + final boolean sourceEnabled = providers.contains(AiProviders.EMBEDDED); + final boolean perWord = sourceEnabled && LyricsMerge.anyWordHasRomaji(lyrics.lines()); + final ProviderResult embedded = sourceEnabled + ? embeddedRomanization(lyrics, isWordSynced, perWord, allowTransliteration) + : null; + + // Embedded readings are already in memory, so keep the old synchronous result. + if (AiProviders.EMBEDDED.equals(providers.get(0)) && embedded != null) { + List result = embedded.lines(); + boolean hasWordRomaji = (perWord + && hasUsableWordReading(lyrics.lines(), allowTransliteration)) + || (isWordSynced && hasUsableWordReading(result, allowTransliteration)); + callback.onRomanized(result, false, false, null, hasWordRomaji, null); return; } @@ -131,100 +324,279 @@ public static void romanize(TrackInfo track, Lyrics lyrics, String source, Callb executor.execute(() -> { try { - String provider = Settings.LYRICS_ROMANIZATION_PROVIDER.get(); - - if (AiProviders.LOCAL.equals(provider)) { - if (!JapaneseDictStore.isReady()) JapaneseDictStore.ensure(null); - if (!PinyinDictStore.isReady()) PinyinDictStore.ensure(null); - - LocalRomanizer.Result local = LocalRomanizer.romanize(lines); - List result = local != null ? toLines(local.lines) : null; - String dictionary = local != null ? local.dictionary : null; - if (!LyricsMerge.hasText(result)) { - result = ruleBased(lines); - dictionary = result != null ? RULE_BASED_DICTIONARY : null; - } - if (LyricsMerge.hasText(result)) { - List finalResult = alignIfWordSynced(lyrics, result, isWordSynced); - String finalDictionary = dictionary; - final boolean hasWordRomaji = isWordSynced && finalResult != null && LyricsMerge.anyWordHasRomaji(finalResult); + for (String provider : providers) { + try { + final boolean fromSource = AiProviders.EMBEDDED.equals(provider); + ProviderResult result = fromSource + ? embedded + : romanizeWithProvider(provider, track, lyrics, source, lines, + isWordSynced, allowTransliteration); + if (result == null || !LyricsMerge.hasText(result.lines())) { + continue; + } + if (!fromSource && !hasUsableRomanization( + result.lines(), lyrics, allowTransliteration)) { + continue; + } + + final List finalResult = result.lines(); + final boolean hasWordRomaji = (isWordSynced + && hasUsableWordReading(finalResult, allowTransliteration)) + || (fromSource && perWord && hasUsableWordReading( + lyrics.lines(), allowTransliteration)); Utils.runOnMainThread(() -> callback.onRomanized( - finalResult, false, false, null, hasWordRomaji, finalDictionary)); + finalResult, result.fromGoogle(), result.fromAI(), result.model(), + hasWordRomaji, result.localDictionary())); return; + } catch (Exception ex) { + Logger.printInfo(() -> "romanization provider failed: " + provider, ex); } - // Local romanization yielded no text; fall through to Google/online fallback. } - if (AiProviders.isAi(provider)) { - List aiCached = LyricsCache.getRomanizationAI(track, source, lines); - if (aiCached != null) { - List aligned = alignIfWordSynced(lyrics, aiCached, isWordSynced); - final boolean hasWordRomaji = isWordSynced && aligned != null && LyricsMerge.anyWordHasRomaji(aligned); - Utils.runOnMainThread(() -> callback.onRomanized(aligned, false, true, provider, hasWordRomaji, null)); - return; - } + Utils.runOnMainThread( + () -> callback.onRomanized(null, false, false, null, false, null)); + } catch (Exception ex) { + Logger.printInfo(() -> "romanize failure", ex); + Utils.runOnMainThread( + () -> callback.onRomanized(null, false, false, null, false, null)); + } + }); + } - String prompt = OpenAIClient.renderPrompt(Settings.LYRICS_AI_ROMANIZATION_PROMPT.get(), - "romanization", LyricsRequests.romanizationLanguage(), - track.title(), track.artist(), lines); - AiProviders.Result ai = AiProviders.mapLines(provider, prompt, lines); - if (ai != null && ai.lines() != null && !ai.lines().isEmpty()) { - List aiLines = alignIfWordSynced(lyrics, toLines(ai.lines()), isWordSynced); - if (LyricsMerge.hasText(aiLines)) { - LyricsCache.putRomanizationAI(track, source, lines, aiLines); - final boolean hasWordRomaji = isWordSynced && LyricsMerge.anyWordHasRomaji(aiLines); - Utils.runOnMainThread(() -> callback.onRomanized(aiLines, false, true, ai.model(), hasWordRomaji, null)); - return; - } - } - } + /** Null when the lyrics source has no usable reading of its own. */ + @Nullable + private static ProviderResult embeddedRomanization(Lyrics lyrics, boolean isWordSynced, + boolean perWord, boolean allowTransliteration) { + List embedded = getEmbeddedRomanization(lyrics); + if (!LyricsMerge.hasText(embedded) + || isProviderRomajiDegraded(embedded, lyrics.lines(), perWord, + allowTransliteration)) { + return null; + } + return new ProviderResult(alignIfWordSynced(lyrics, embedded, isWordSynced), + false, false, null, null); + } - List romanized; - boolean online; - if (isWordSynced) { - romanized = romanizeWordSynced(lyrics); - online = LyricsMerge.hasText(romanized); - } else { - romanized = LyricsCache.getRomanization(track, source, lines); - online = romanized != null; - if (romanized == null) { - romanized = romanizeLineSynced(lyrics); - if (LyricsMerge.hasText(romanized)) { - LyricsCache.putRomanization(track, source, lines, romanized); - online = true; - } - } + @Nullable + private static ProviderResult romanizeWithProvider( + String provider, TrackInfo track, Lyrics lyrics, String source, + List lines, boolean isWordSynced, boolean allowTransliteration) { + if (AiProviders.LOCAL.equals(provider)) { + return romanizeLocal(lyrics, lines, isWordSynced); + } + if (AiProviders.RIME.equals(provider)) { + return romanizeRime(lyrics, lines, isWordSynced); + } + if (AiProviders.isAi(provider)) { + return romanizeAi(provider, track, lyrics, source, lines, isWordSynced, + allowTransliteration); + } + if (AiProviders.GOOGLE.equals(provider)) { + return romanizeGoogle(track, lyrics, source, lines, isWordSynced, + allowTransliteration); + } + return null; + } + + /** + * The Rime Wordlist provider: the Han characters of the language are read by its word table, the + * scripts around them by the same local dictionaries. Nothing is read when the language has no + * word table, so the next provider is given the turn instead. + */ + @Nullable + private static ProviderResult romanizeRime(Lyrics lyrics, List lines, + boolean isWordSynced) { + if (!RimeDictStore.isActive()) { + return null; + } + // Fetching happens only when the row is turned on or a choice is saved, never here. + if (!RimeDictStore.isReady()) { + return null; + } + + LocalRomanizer.Result local = LocalRomanizer.romanizeRime(lines); + List result = local != null ? toLines(local.lines) : null; + String dictionary = local != null ? local.dictionary : null; + if (!LyricsMerge.hasText(result)) { + return null; + } + List aligned = alignIfWordSynced(lyrics, result, isWordSynced); + return new ProviderResult(aligned, false, false, null, dictionary); + } + + @Nullable + private static ProviderResult romanizeLocal(Lyrics lyrics, List lines, + boolean isWordSynced) { + // The Han characters of a language with a Rime word table are the Rime Wordlist provider's to + // read. Reading them here as well would give the line the Mandarin reading, and since + // this provider usually comes first the word table would never be used at all. The whole + // lyric is left to that provider, which reads the scripts around the Han the same way. + if (rimeMapEnabled() && RimeDictStore.isActive()) { + return null; + } + + // Reading is rule-based until the dictionaries the user asked for are on disk; fetching + // happens only when the row is turned on or a choice is saved, never from playback. A + // store that is running keeps feeding readings in as it goes. + LocalRomanizer.Result local = LocalRomanizer.romanize(lines); + List result = local != null ? toLines(local.lines) : null; + String dictionary = local != null ? local.dictionary : null; + if (!LyricsMerge.hasText(result)) { + result = ruleBased(lines); + dictionary = result != null ? RULE_BASED_DICTIONARY : null; + } + if (!LyricsMerge.hasText(result)) { + return null; + } + List aligned = alignIfWordSynced(lyrics, result, isWordSynced); + return new ProviderResult(aligned, false, false, null, dictionary); + } + + /** Whether the Rime Wordlist provider is switched on in the stored list. */ + private static boolean rimeMapEnabled() { + return AiProviders.enabledProviders(Settings.LYRICS_ROMANIZATION_PROVIDERS.get(), + AiProviders.ROMANIZATION_ORDER).contains(AiProviders.RIME); + } + + @Nullable + private static ProviderResult romanizeAi(String provider, TrackInfo track, Lyrics lyrics, + String source, List lines, + boolean isWordSynced, boolean allowTransliteration) { + List aiCached = LyricsCache.getRomanizationAI( + track, source, lines, provider); + if (hasUsableRomanization(aiCached, lyrics, allowTransliteration)) { + // The cache key includes the model that produced it. + return new ProviderResult( + alignIfWordSynced(lyrics, aiCached, isWordSynced), + false, true, Settings.LYRICS_AI_MODEL.get(), null); + } + + String prompt = OpenAIClient.renderPrompt( + Settings.LYRICS_AI_ROMANIZATION_PROMPT.get(), "romanization", + LyricsRequests.romanizationLanguage(), track.title(), track.artist(), lines); + AiProviders.Result ai = AiProviders.mapLines(provider, prompt, lines); + if (ai == null || ai.lines() == null || ai.lines().isEmpty()) { + return null; + } + + List aiRaw = toLines(ai.lines()); + if (lyrics.lines() == null || aiRaw.size() != lyrics.lines().size()) { + // Reject short answers before word-sync padding, so the next provider can try. + return null; + } + List aiLines = alignIfWordSynced(lyrics, aiRaw, isWordSynced); + if (!hasUsableRomanization(aiLines, lyrics, allowTransliteration)) { + return null; + } + LyricsCache.putRomanizationAI(track, source, lines, aiLines, provider); + return new ProviderResult(aiLines, false, true, ai.model(), null); + } + + @Nullable + private static ProviderResult romanizeGoogle(TrackInfo track, Lyrics lyrics, String source, + List lines, boolean isWordSynced, + boolean allowTransliteration) { + List result; + if (isWordSynced) { + result = romanizeWordSynced(lyrics); + if (result == null) { + return null; + } + } else { + result = LyricsCache.getRomanization(track, source, lines); + if (result == null || !hasUsableRomanization(result, lyrics, allowTransliteration)) { + result = romanizeLineSynced(lyrics); + if (result == null) { + return null; } + if (hasUsableRomanization(result, lyrics, allowTransliteration)) { + LyricsCache.putRomanization(track, source, lines, result); + } + } + } - // The last resort. Its reading is not stored, so an online one is used again - // as soon as there is one. - LocalRomanizer.Result local = !LyricsMerge.hasText(romanized) - && (Settings.LYRICS_LOCAL_ROMANIZATION.get() || AiProviders.LOCAL.equals(provider) || AiProviders.isAi(provider)) - ? LocalRomanizer.romanize(lines) : null; + if (!hasUsableRomanization(result, lyrics, allowTransliteration)) { + return null; + } + List aligned = alignIfWordSynced(lyrics, result, isWordSynced); + return new ProviderResult(aligned, true, false, null, null); + } - List result = romanized; - if (local != null) { - result = alignIfWordSynced(lyrics, toLines(local.lines), isWordSynced); + /** + * Rejects a reading that is only the original line, which older versions could store after + * a failed online request. With transliteration off, a non-Latin line is usable only when + * word-synced readings replace it with Latin text. + */ + private static boolean hasUsableRomanization(@Nullable List result, Lyrics lyrics, + boolean allowTransliteration) { + if (!LyricsMerge.hasText(result)) { + return false; + } + List original = lyrics.lines(); + if (original == null) { + return true; + } + if (result.size() != original.size()) { + return false; + } + if (!allowTransliteration) { + for (int i = 0; i < result.size(); i++) { + String reading = result.get(i).text(); + reading = reading != null ? reading.trim() : ""; + if (reading.isEmpty() || isPurelyLatinScript(reading)) { + continue; } - if (!LyricsMerge.hasText(result)) { - List ruleBased = ruleBased(lines); - if (ruleBased != null) { - result = alignIfWordSynced(lyrics, ruleBased, isWordSynced); + // Word-synced requests can leave the line text unchanged when only the syllable + // request succeeded. The per-word readings are still a usable romanization. + boolean usable = false; + if (original.get(i).hasWords() && !result.get(i).words().isEmpty()) { + for (Word word : result.get(i).words()) { + String romaji = wordReading(word, false); + String written = word.text() == null ? "" : word.text().trim(); + if (!romaji.isEmpty() && !romaji.equals(written) + && isPurelyLatinScript(romaji)) { + usable = true; + break; + } } } - - List finalResult = result; - final boolean fromGoogle = LyricsMerge.hasText(result) && online && local == null; - final boolean hasWordRomaji = isWordSynced && finalResult != null && LyricsMerge.anyWordHasRomaji(finalResult); - Utils.runOnMainThread(() -> callback.onRomanized( - finalResult, fromGoogle, false, null, hasWordRomaji, - local != null ? local.dictionary : null)); - } catch (Exception ex) { - Logger.printException(() -> "romanize failure", ex); - Utils.runOnMainThread( - () -> callback.onRomanized(null, false, false, null, false, null)); + if (!usable) { + return false; + } } - }); + return true; + } + boolean nonLatin = false; + for (int i = 0; i < result.size(); i++) { + String source = original.get(i).text(); + if (source == null || source.isEmpty()) { + continue; + } + // Compare against the same text the online request sends, so an outer parenthetical + // wrapper does not look like a reading. + source = stripParentheses(source).trim(); + if (source.isEmpty() || isPurelyLatinScript(source)) { + continue; + } + nonLatin = true; + String reading = result.get(i).text(); + if (reading != null) { + reading = reading.trim(); + if (!reading.isEmpty() && !reading.equals(source)) { + return true; + } + } + // Word-synced requests can leave the line text unchanged when only the syllable + // request succeeded. The per-word readings are still a usable romanization. + for (Word word : result.get(i).words()) { + String romaji = wordReading(word, allowTransliteration); + String written = word.text() == null ? "" : word.text().trim(); + if (!romaji.isEmpty() && !romaji.equals(written)) { + return true; + } + } + } + return !nonLatin; } /** @@ -254,8 +626,11 @@ private static List alignIfWordSynced(Lyrics lyrics, @Nullable List< return isWordSynced && lines != null ? alignLinesToWordSync(lyrics, lines) : lines; } + @Nullable private static List romanizeWordSynced(Lyrics lyrics) { - if (lyrics.lines() == null) return Collections.emptyList(); + if (lyrics.lines() == null) { + return Collections.emptyList(); + } List origLines = lyrics.lines(); List allLines = new ArrayList<>(origLines.size()); @@ -273,15 +648,29 @@ private static List romanizeWordSynced(Lyrics lyrics) { } } - List fullLineResults = romanizeTexts(allLines); - List isolatedResults = !allSyllables.isEmpty() ? romanizeTexts(allSyllables) : Collections.emptyList(); + TextRomanization full = romanizeTexts(allLines); + TextRomanization isolated = !allSyllables.isEmpty() + ? romanizeTexts(allSyllables) + : new TextRomanization(Collections.emptyList(), true); + final boolean useFull = full.complete(); + final boolean useIsolated = isolated.complete(); + if (!useFull && !useIsolated) { + // Both requests failed. Tell the caller so the next provider can run instead of + // passing the original script off as a reading. + return null; + } + + List fullLineResults = full.lines(); + List isolatedResults = isolated.lines(); List resultLines = new ArrayList<>(origLines.size()); int globalSyllableIndex = 0; for (int i = 0; i < origLines.size(); i++) { LyricsLine line = origLines.get(i); - String fullLineRom = (i < fullLineResults.size() && fullLineResults.get(i) != null) ? fullLineResults.get(i) : ""; + String fullLineRom = useFull + && i < fullLineResults.size() && fullLineResults.get(i) != null + ? fullLineResults.get(i) : allLines.get(i); int count = lineStructure[i]; List origWords = line.words(); @@ -291,8 +680,10 @@ private static List romanizeWordSynced(Lyrics lyrics) { continue; } - int endSyllableIndex = Math.min(globalSyllableIndex + count, isolatedResults.size()); - List romanizedGuides = (globalSyllableIndex < isolatedResults.size()) + int endSyllableIndex = useIsolated + ? Math.min(globalSyllableIndex + count, isolatedResults.size()) + : globalSyllableIndex; + List romanizedGuides = useIsolated && globalSyllableIndex < isolatedResults.size() ? new ArrayList<>(isolatedResults.subList(globalSyllableIndex, endSyllableIndex)) : new ArrayList<>(); while (romanizedGuides.size() < count) { @@ -327,8 +718,11 @@ private static List romanizeWordSynced(Lyrics lyrics) { return resultLines; } + @Nullable private static List romanizeLineSynced(Lyrics lyrics) { - if (lyrics.lines() == null) return Collections.emptyList(); + if (lyrics.lines() == null) { + return Collections.emptyList(); + } List origLines = lyrics.lines(); List texts = new ArrayList<>(origLines.size()); @@ -337,14 +731,18 @@ private static List romanizeLineSynced(Lyrics lyrics) { texts.add(text != null ? stripParentheses(text).trim() : ""); } - List romanizedResults = romanizeTexts(texts); + TextRomanization romanizedResults = romanizeTexts(texts); + if (!romanizedResults.complete()) { + return null; + } + List fetched = romanizedResults.lines(); List result = new ArrayList<>(origLines.size()); for (int i = 0; i < origLines.size(); i++) { LyricsLine origLine = origLines.get(i); String origText = texts.get(i); - String roma = (i < romanizedResults.size() && romanizedResults.get(i) != null) - ? romanizedResults.get(i) : origText; + String roma = (i < fetched.size() && fetched.get(i) != null) + ? fetched.get(i) : origText; if (roma.isEmpty()) { roma = origText; } @@ -355,11 +753,8 @@ private static List romanizeLineSynced(Lyrics lyrics) { return result; } - /** - * Romanizes the texts online. A text already in Latin script, or one that could not be - * read, is returned as it is. - */ - private static List romanizeTexts(List texts) { + /** Romanizes non-Latin lines; an incomplete result lets the caller try the next provider. */ + private static TextRomanization romanizeTexts(List texts) { List indices = new ArrayList<>(); List toFetch = new ArrayList<>(); List result = new ArrayList<>(texts.size()); @@ -372,10 +767,10 @@ private static List romanizeTexts(List texts) { } } if (toFetch.isEmpty()) { - return result; + return new TextRomanization(result, true); } - String language = Settings.LYRICS_ROMANIZE_LANGUAGE.get(); + final String language = Settings.LYRICS_ROMANIZE_LANGUAGE.get(); List fetched = LyricsMerge.mapLinesOnline(toFetch, batch -> { try { return TextTranslator.romanize(batch, language); @@ -383,12 +778,13 @@ private static List romanizeTexts(List texts) { throw new RuntimeException(ex); } }); - if (fetched != null) { - for (int k = 0; k < indices.size(); k++) { - result.set(indices.get(k), fetched.get(k)); - } + if (fetched == null || fetched.size() != indices.size()) { + return new TextRomanization(result, false); } - return result; + for (int k = 0; k < indices.size(); k++) { + result.set(indices.get(k), fetched.get(k)); + } + return new TextRomanization(result, true); } private static List collectMatchingRomanizations( @@ -530,26 +926,27 @@ private static boolean isMandarinPinyinToken(@Nullable String t) { || s.endsWith("iong")) { return true; } - if (s.equals("ceng") || s.equals("cuan") || s.equals("zuan") || s.equals("zhe") - || s.equals("jin") || s.equals("dong") || s.equals("sheng") || s.equals("chuang") + return s.equals("ceng") || s.equals("cuan") || s.equals("zuan") || s.equals("zhe") + || s.equals("dong") || s.equals("sheng") || s.equals("chuang") || s.equals("xian") || s.equals("qian") || s.equals("tian") || s.equals("lian") || s.equals("jian") || s.equals("bian") || s.equals("pian") || s.equals("mian") || s.equals("dian") || s.equals("nian") || s.equals("huan") || s.equals("guan") || s.equals("kuan") || s.equals("duan") || s.equals("tuan") || s.equals("luan") || s.equals("ruan") || s.equals("xuan") || s.equals("quan") || s.equals("juan") || s.equals("yang") || s.equals("ying") || s.equals("yong") || s.equals("wang") - || s.equals("weng") || s.equals("gu") || s.equals("xi")) { - return true; - } - return false; + || s.equals("weng") || s.equals("xi"); } - private static boolean isDegradedRomajiToken(@Nullable String t) { + /** + * @param allowTilde Whether the written word already has a tilde. A tilde in the reading + * is then part of the lyric, not a broken mora marker. + */ + private static boolean isDegradedRomajiToken(@Nullable String t, boolean allowTilde) { if (t == null || t.isEmpty()) return false; String s = t.trim().toLowerCase(Locale.ROOT); if (s.contains(" ")) { for (String part : s.split("\\s+")) { - if (isDegradedRomajiToken(part)) return true; + if (isDegradedRomajiToken(part, allowTilde)) return true; } return false; } @@ -558,41 +955,49 @@ private static boolean isDegradedRomajiToken(@Nullable String t) { return true; } // A sokuon written as a symbol, such as ~tsu or -tsu. - if (s.contains("~tsu") || s.contains("-tsu") || s.contains("~tsu") - || s.endsWith("~") || s.startsWith("~") || s.contains("~") || s.contains("~")) { + if (s.contains("-tsu")) { return true; } - // A symbol or punctuation mark on its own. - if (s.equals("-") || s.equals("~") || s.equals("~") || s.equals("^") || s.equals("_")) { + if (!allowTilde && (s.contains("~tsu") || s.contains("~tsu") + || s.endsWith("~") || s.startsWith("~") || s.contains("~") || s.contains("~"))) { return true; } - // A pinyin syllable, which has no place in a Japanese reading. - if (isMandarinPinyinToken(s)) { + // A symbol or punctuation mark on its own. + if (s.equals("-") || s.equals("^") || s.equals("_") + || (!allowTilde && (s.equals("~") || s.equals("~")))) { return true; } + // Pinyin is checked at the track level, where a majority of syllables is needed. A + // single token is not enough evidence and would reject valid Japanese readings. + // A doubled consonant on its own, which is a sokuon split from its mora. - if (s.equals("pp") || s.equals("tt") || s.equals("kk") || s.equals("ss") + return s.equals("pp") || s.equals("tt") || s.equals("kk") || s.equals("ss") || s.equals("cc") || s.equals("dd") || s.equals("bb") || s.equals("ff") || s.equals("gg") || s.equals("hh") || s.equals("jj") || s.equals("mm") - || s.equals("rr") || s.equals("ww") || s.equals("yy") || s.equals("zz")) { - return true; - } - return false; + || s.equals("rr") || s.equals("ww") || s.equals("yy") || s.equals("zz"); } + /** + * Whether a reading shipped by the provider is too broken to use. + * + *

    Checks that say the data itself is wrong, such as mora fragments, stray symbols or + * missing lines, always apply. With transliteration off, a non-Latin reading passes only + * if its word readings supply the text. + */ public static boolean isProviderRomajiDegraded(@Nullable List embedded, @Nullable List originalLines, - boolean perWord) { + boolean perWord, + boolean allowTransliteration) { if (!perWord && (embedded == null || embedded.isEmpty())) { return true; } int totalTokens = 0; - int shortTokens = 0; int totalChars = 0; - int cjkCount = 0; int totalChecked = 0; int allSingleLetterTokens = 0; + int japaneseLatinTokens = 0; + int pinyinTokens = 0; if (embedded != null && !embedded.isEmpty()) { for (int li = 0; li < embedded.size(); li++) { @@ -601,13 +1006,24 @@ public static boolean isProviderRomajiDegraded(@Nullable List embedd if (text == null || text.trim().isEmpty()) { continue; } - totalChecked++; - if (ScriptDetector.containsCjk(text)) { - cjkCount++; - } LyricsLine origLine = (originalLines != null && li < originalLines.size()) ? originalLines.get(li) : null; + if (!allowTransliteration && !isPurelyLatinScript(text) + && (!perWord || !hasUsableWordReadingForLine(line, origLine, false))) { + return true; + } + totalChecked++; + String origText = origLine != null ? origLine.text() : null; + final boolean japaneseSource = ScriptDetector.containsKana(origText); + + // A pure kana or hangul reading was romanized above. Either script left in + // the line means the provider sent mixed or unreadable text rather than a + // reading. + if (ScriptDetector.containsKana(text) + || ScriptDetector.contains(text, Character.UnicodeScript.HANGUL)) { + return true; + } // A tilde the lyric does not have is an artifact of the reading. for (int ci = 0; ci < text.length(); ci++) { @@ -622,28 +1038,43 @@ public static boolean isProviderRomajiDegraded(@Nullable List embedd int singleLetterTokens = 0; String[] tokens = text.trim().split("\\s+"); for (String t : tokens) { - if (!t.isEmpty()) { - if (isDegradedRomajiToken(t)) { - return true; // trigger for pp 1-ri etc - } - totalTokens++; - totalChars += t.length(); - if (t.length() <= 2) { - shortTokens++; - } - if (t.length() == 1 && Character.isLetter(t.charAt(0))) { - singleLetterTokens++; - allSingleLetterTokens++; + if (t.isEmpty()) { + continue; + } + if (isDegradedRomajiToken(t, containsTilde(origText))) { + return true; // trigger for pp 1-ri etc + } + // The length checks below were written for Latin readings. A Chinese + // character is one UTF-16 unit and `Character.isLetter`, so counting it + // would reject every transliteration as a line of single letters. + if (ScriptDetector.containsCjk(t)) { + continue; + } + if (!hasLetterOrDigit(t)) { + continue; + } + totalTokens++; + totalChars += t.length(); + if (t.length() == 1 && Character.isLetter(t.charAt(0))) { + singleLetterTokens++; + allSingleLetterTokens++; + } + if (japaneseSource && ScriptDetector.contains(t, Character.UnicodeScript.LATIN)) { + japaneseLatinTokens++; + if (isMandarinPinyinToken(t)) { + pinyinTokens++; } } } // Several lone letters mean the morae were split apart. - if (singleLetterTokens >= 3 && origText != null && ScriptDetector.containsKana(origText)) { + if (singleLetterTokens >= 3 && japaneseSource) { return true; } } if (totalChecked == 0) return true; - if ((double) cjkCount / totalChecked > 0.20) { + // A pinyin reading against Japanese lyrics is the wrong song. Two syllables are + // required so that one English word ending in "ng" cannot trigger it. + if (pinyinTokens >= 2 && pinyinTokens * 2 >= japaneseLatinTokens) { return true; } if (originalLines != null) { @@ -653,7 +1084,8 @@ public static boolean isProviderRomajiDegraded(@Nullable List embedd origNonEmpty++; } } - if (origNonEmpty > 0 && (double) totalChecked / origNonEmpty < 0.40) { + if (origNonEmpty > 0 + && (double) totalChecked / origNonEmpty < MIN_READING_COVERAGE) { return true; } } @@ -662,12 +1094,14 @@ public static boolean isProviderRomajiDegraded(@Nullable List embedd // The per word readings are checked the same way. if (perWord && originalLines != null) { int totalWordTokens = 0; - int shortWordTokens = 0; int charWordTokens = 0; int allSingleWordTokens = 0; + int japaneseWordLatinTokens = 0; + int pinyinWordTokens = 0; for (LyricsLine l : originalLines) { if (!l.hasWords()) continue; String origLineText = l.text(); + final boolean japaneseSource = ScriptDetector.containsKana(origLineText); int singleWordTokens = 0; for (Word w : l.words()) { String r = w.romaji(); @@ -684,27 +1118,40 @@ public static boolean isProviderRomajiDegraded(@Nullable List embedd String[] tokens = r.trim().split("\\s+"); for (String t : tokens) { - if (!t.isEmpty()) { - if (isDegradedRomajiToken(t)) { - return true; // trigger for 1-ri pp etc - } - totalWordTokens++; - charWordTokens += t.length(); - if (t.length() <= 2) { - shortWordTokens++; - } - if (t.length() == 1 && Character.isLetter(t.charAt(0))) { - singleWordTokens++; - allSingleWordTokens++; + if (t.isEmpty()) { + continue; + } + if (isDegradedRomajiToken(t, containsTilde(origWord))) { + return true; // trigger for 1-ri pp etc + } + if (ScriptDetector.containsCjk(t)) { + continue; + } + if (!hasLetterOrDigit(t)) { + continue; + } + totalWordTokens++; + charWordTokens += t.length(); + if (t.length() == 1 && Character.isLetter(t.charAt(0))) { + singleWordTokens++; + allSingleWordTokens++; + } + if (japaneseSource && ScriptDetector.contains(t, Character.UnicodeScript.LATIN)) { + japaneseWordLatinTokens++; + if (isMandarinPinyinToken(t)) { + pinyinWordTokens++; } } } } } - if (singleWordTokens >= 3 && origLineText != null && ScriptDetector.containsKana(origLineText)) { + if (singleWordTokens >= 3 && japaneseSource) { return true; } } + if (pinyinWordTokens >= 2 && pinyinWordTokens * 2 >= japaneseWordLatinTokens) { + return true; + } if (totalWordTokens >= 2) { double avgWordLen = (double) charWordTokens / totalWordTokens; double singleLetterRatio = (double) allSingleWordTokens / totalWordTokens; @@ -718,9 +1165,7 @@ public static boolean isProviderRomajiDegraded(@Nullable List embedd if (totalTokens >= 2) { double avgLen = (double) totalChars / totalTokens; double singleLetterRatio = (double) allSingleLetterTokens / totalTokens; - if (avgLen < 1.4 || singleLetterRatio > 0.5) { - return true; - } + return avgLen < 1.4 || singleLetterRatio > 0.5; } return false; @@ -816,32 +1261,28 @@ private static int levenshteinDistance(String s1, String s2) { return prev[len2]; } - private static List alignRomanizationAnchors( @Nullable List origWords, @Nullable String fullText) { if (origWords == null || origWords.isEmpty()) return Collections.emptyList(); final int count = origWords.size(); + final boolean allowTransliteration = Settings.LYRICS_ALLOW_TRANSLITERATION.get(); + final boolean targetHasCjk = ScriptDetector.containsCjk(fullText); List originalSyllables = new ArrayList<>(count); List romanizedGuides = new ArrayList<>(count); for (Word w : origWords) { String text = w.text() != null ? w.text() : ""; originalSyllables.add(text); - String guide = w.romaji(); - if (guide == null || guide.trim().isEmpty() || ScriptDetector.containsCjk(guide)) { - if (isPurelyLatinScript(text)) { - guide = text.trim(); - } else if (ScriptDetector.containsKana(text)) { - guide = KanaRomanizer.romanize(text).trim(); - } else if (ScriptDetector.contains(text, Character.UnicodeScript.HANGUL)) { - guide = HangulRomanizer.romanize(text).trim(); - } else { - guide = ""; - } - } else { - guide = guide.trim(); + String guide = wordReading(w, allowTransliteration); + // A Han word reading cannot guide a Latin line reading. Fall back to the word's + // own pronunciation, the same reading the line is likely built from. + if (!targetHasCjk && ScriptDetector.containsCjk(guide)) { + guide = wordReading(w, false); } - romanizedGuides.add(guide != null ? guide : ""); + if (guide.isEmpty() && isPurelyLatinScript(text)) { + guide = text.trim(); + } + romanizedGuides.add(guide); } return alignRomanizationAnchors(fullText != null ? fullText : "", romanizedGuides, originalSyllables); } @@ -853,7 +1294,7 @@ private static List alignRomanizationAnchors( String fullText, List romanizedGuides, List originalSyllables) { - String target = fullText; + final String target = fullText; final int N = target.length(); final int M = romanizedGuides.size(); @@ -881,6 +1322,13 @@ private static List alignRomanizationAnchors( return chunks; } else if (split.length > 0) { if (split.length < M) { + // When the source words are all Han/kana and the reading has fewer + // space-separated pieces than words, split the reading by characters. + // Otherwise a one-word reading such as "你的名字" would be assigned + // entirely to the last word and the other words would show the lyric. + if (ScriptDetector.containsCjk(target) || isAllCjk(originalSyllables)) { + return splitReadingByCharacter(target, M); + } for (int i = 0; i < M; i++) { int tokStart = (i * split.length) / M; int tokEnd = ((i + 1) * split.length) / M; @@ -986,6 +1434,52 @@ private static List alignRomanizationAnchors( return results; } + /** Whether every written word is in a script that does not put spaces between words. */ + private static boolean isAllCjk(List syllables) { + if (syllables.isEmpty()) { + return false; + } + for (String syllable : syllables) { + if (syllable == null || syllable.trim().isEmpty() + || !ScriptDetector.containsCjk(syllable)) { + return false; + } + } + return true; + } + + /** + * Splits a reading across words by code point. Used when a CJK reading has fewer spaces + * than there are words, since a Han syllable or a romanized kana syllable is a character + * rather than a whitespace-delimited token. + */ + private static List splitReadingByCharacter(String text, int count) { + StringBuilder compact = new StringBuilder(text.length()); + for (int i = 0; i < text.length(); ) { + int cp = Character.codePointAt(text, i); + i += Character.charCount(cp); + if (!Character.isWhitespace(cp)) { + compact.appendCodePoint(cp); + } + } + text = compact.toString(); + + List chunks = new ArrayList<>(count); + final int total = text.codePointCount(0, text.length()); + int at = 0; + for (int i = 0; i < count; i++) { + int endCodePoint = (int) ((long) total * (i + 1) / count); + int end = text.offsetByCodePoints(0, endCodePoint); + String chunk = text.substring(at, end); + if (!chunk.isEmpty() && i < count - 1) { + chunk += " "; + } + chunks.add(chunk); + at = end; + } + return chunks; + } + private static double calculateMatchCost(String candidate, String guide, boolean isLatin) { String cTrim = candidate.trim(); String gTrim = guide.trim(); @@ -1006,4 +1500,20 @@ private static double calculateMatchCost(String candidate, String guide, boolean return dist + (lenDiff * 0.8) + bonus; } + + public interface Callback { + void onRomanized(@Nullable List romanizedLines, + boolean fromGoogle, boolean fromAI, @Nullable String aiModel, + boolean perWord, @Nullable String localDictionary); + + } + + private record ProviderResult(List lines, boolean fromGoogle, + boolean fromAI, @Nullable String model, + @Nullable String localDictionary) { + } + + /** The result of romanizing text online, and whether every non-Latin line was fetched. */ + private record TextRomanization(List lines, boolean complete) { + } } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/derive/LyricsTranslator.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/derive/LyricsTranslator.java index 6b00bc1689..d5e445a756 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/derive/LyricsTranslator.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/derive/LyricsTranslator.java @@ -30,11 +30,6 @@ public final class LyricsTranslator { private static final ExecutorService executor = Executors.newSingleThreadExecutor(); - public interface Callback { - void onTranslated(@Nullable List translatedLines, - boolean fromGoogle, boolean fromAI, @Nullable String aiModel); - } - private LyricsTranslator() { } @@ -45,7 +40,7 @@ private static List embeddedTranslation(Lyrics lyrics, String target, in return null; } String targetLang = LyricsRequests.primarySubtag(target); - String targetScript = LyricsRequests.resolvedScript(target); + final String targetScript = LyricsRequests.resolvedScript(target); for (Map.Entry> entry : byLang.entrySet()) { String key = entry.getKey(); if (key.startsWith(LyricsMerge.BACKGROUND_TRACK_PREFIX)) { @@ -67,9 +62,9 @@ private static List embeddedTranslation(Lyrics lyrics, String target, in return null; } - /** Elsewhere the fallback would put the sung line's own words under different ones. */ + /** For background lines without a translation of their own, fall back to the parent line. */ private static List embeddedTranslationLines(Lyrics lyrics, List lines, - @Nullable List backgroundLines) { + @Nullable List backgroundLines) { List out = new ArrayList<>(lines.size()); for (LyricsLine line : lines) { String text = line.text(); @@ -81,7 +76,7 @@ private static List embeddedTranslationLines(Lyrics lyrics, List embeddedTranslationLines(Lyrics lyrics, List toLyricsLines(List textLines) { - if (textLines == null) return java.util.Collections.emptyList(); - List lines = new ArrayList<>(textLines.size()); - for (String t : textLines) { - lines.add(new LyricsLine(LyricsLine.NO_TIME, t)); - } - return lines; - } - public static void translate(TrackInfo track, Lyrics lyrics, String source, Callback callback) { Utils.verifyOnMainThread(); @@ -120,56 +106,125 @@ public static void translate(TrackInfo track, Lyrics lyrics, String source, Call } String language = LyricsRequests.translationLanguage(); + List providers = AiProviders.enabledProviders( + Settings.LYRICS_TRANSLATION_PROVIDERS.get(), + AiProviders.TRANSLATION_ORDER); + if (providers.isEmpty()) { + callback.onTranslated(null, false, false, null); + return; + } + + final List embedded = providers.contains(AiProviders.EMBEDDED) + ? embeddedTranslation(lyrics, language, lines.size()) + : null; - List embedded = embeddedTranslation(lyrics, language, lines.size()); - if (embedded != null) { - Utils.runOnMainThread(() -> callback.onTranslated(embedded, false, false, null)); + // Embedded translation is already in memory, so keep the old synchronous result. + if (AiProviders.EMBEDDED.equals(providers.get(0)) && embedded != null) { + callback.onTranslated(embedded, false, false, null); return; } executor.execute(() -> { try { - String provider = Settings.LYRICS_TRANSLATION_PROVIDER.get(); - if (!AiProviders.GOOGLE.equals(provider)) { - List aiCached = LyricsCache.getTranslationAI( - track, source, language, lines); - if (aiCached != null) { - Utils.runOnMainThread(() -> callback.onTranslated(aiCached, false, true, provider)); - return; - } - - AiProviders.Result ai = AiProviders.DEEPL.equals(provider) - ? deepLTranslate(lines, language) - : AiProviders.mapLines(provider, OpenAIClient.renderPrompt( - Settings.LYRICS_AI_TRANSLATION_PROMPT.get(), "translation", - language, track.title(), track.artist(), lines), lines); - if (ai != null && ai.lines() != null && !ai.lines().isEmpty() && LyricsMerge.hasText(toLyricsLines(ai.lines()))) { - LyricsCache.putTranslationAI(track, source, language, lines, ai.lines()); - Utils.runOnMainThread(() -> callback.onTranslated(ai.lines(), false, true, ai.model())); - return; - } - // AI / DeepL request failed or returned empty; gracefully fall through to Google Translate. - } - - List translated = LyricsCache.getTranslation( - track, source, language, lines); - if (translated == null) { - translated = translateOnline(lines, language); - if (translated != null) { - LyricsCache.putTranslation(track, source, language, lines, translated); + for (String provider : providers) { + try { + ProviderTranslation result = AiProviders.EMBEDDED.equals(provider) + ? (embedded == null ? null + : new ProviderTranslation(embedded, false, false, null)) + : translateWithProvider( + provider, track, source, language, lines); + if (result != null) { + Utils.runOnMainThread(() -> callback.onTranslated( + result.lines(), result.fromGoogle(), result.fromAI(), + result.model())); + return; + } + } catch (Exception ex) { + Logger.printInfo(() -> "translation provider failed: " + provider, ex); } } - - List result = translated; - Utils.runOnMainThread( - () -> callback.onTranslated(result, result != null, false, null)); + Utils.runOnMainThread(() -> callback.onTranslated(null, false, false, null)); } catch (Exception ex) { - Logger.printException(() -> "translate failure", ex); + Logger.printInfo(() -> "translate failure", ex); Utils.runOnMainThread(() -> callback.onTranslated(null, false, false, null)); } }); } + @Nullable + private static ProviderTranslation translateWithProvider( + String provider, TrackInfo track, String source, String language, + List lines) { + if (AiProviders.GOOGLE.equals(provider)) { + List translated = LyricsCache.getTranslation( + track, source, language, lines); + if (hasTranslation(translated, lines)) { + return new ProviderTranslation(sanitizeTranslation(translated), true, false, null); + } + + translated = translateOnline(lines, language); + if (hasTranslation(translated, lines)) { + translated = sanitizeTranslation(translated); + LyricsCache.putTranslation(track, source, language, lines, translated); + return new ProviderTranslation(translated, true, false, null); + } + return null; + } + + final boolean deepl = AiProviders.DEEPL.equals(provider); + if (!deepl && !AiProviders.isAi(provider)) { + return null; + } + + List aiCached = LyricsCache.getTranslationAI( + track, source, language, lines, provider); + if (hasTranslation(aiCached, lines)) { + // The cache key includes the model that produced it; DeepL has no model. + return new ProviderTranslation(sanitizeTranslation(aiCached), false, true, + deepl ? "DeepL" : Settings.LYRICS_AI_MODEL.get()); + } + + AiProviders.Result ai = deepl + ? deepLTranslate(lines, language) + : AiProviders.mapLines(provider, OpenAIClient.renderPrompt( + Settings.LYRICS_AI_TRANSLATION_PROMPT.get(), "translation", + language, track.title(), track.artist(), lines), lines); + if (ai == null || ai.lines() == null || ai.lines().isEmpty() + || !hasTranslation(ai.lines(), lines)) { + return null; + } + + List translated = sanitizeTranslation(ai.lines()); + LyricsCache.putTranslationAI(track, source, language, lines, translated, provider); + return new ProviderTranslation(translated, false, true, ai.model()); + } + + private static List sanitizeTranslation(List lines) { + List clean = new ArrayList<>(lines.size()); + for (String line : lines) { + clean.add(line == null ? "" : line); + } + return clean; + } + + /** A translation is usable if at least one line differs from the source. */ + private static boolean hasTranslation(@Nullable List translated, + List source) { + if (translated == null || translated.size() != source.size()) { + return false; + } + for (int i = 0; i < translated.size(); i++) { + String text = translated.get(i); + if (text != null) { + text = text.trim(); + if (!text.isEmpty() && !text.equals(source.get(i).trim())) { + return true; + } + } + } + return false; + } + @Nullable private static AiProviders.Result deepLTranslate(List lines, String language) { String apiKey = Settings.LYRICS_DEEPL_API_KEY.get(); @@ -191,4 +246,13 @@ private static List translateOnline(List lines, String language) } }); } + + public interface Callback { + void onTranslated(@Nullable List translatedLines, + boolean fromGoogle, boolean fromAI, @Nullable String aiModel); + } + + private record ProviderTranslation(List lines, boolean fromGoogle, + boolean fromAI, @Nullable String model) { + } } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/derive/OpenAIClient.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/derive/OpenAIClient.java index 1fd60736e0..9459209f84 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/derive/OpenAIClient.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/derive/OpenAIClient.java @@ -32,11 +32,74 @@ public final class OpenAIClient { + /** + * The prompt template sent to the AI endpoint. One template covers both tasks; + * {@link #renderPrompt} fills in its variables: + *

      + *
    • {@code {task}} - "translation" or "romanization"
    • + *
    • {@code {language}} - the target language
    • + *
    • {@code {title}}, {@code {artist}} - the song
    • + *
    • {@code {count}} - the number of lines
    • + *
    • {@code {lines}} - the lyric lines, one per line
    • + *
    + * Unknown variables are left as typed. + */ + public static final String DEFAULT_PROMPT = """ + You are a professional lyrics {task} engine. Perform {task} on every line below for {language}, keeping the original meaning, tone and punctuation. + Song: {title} by {artist} + + Output format: Number every result on its own line, like: + 1. first result + 2. second result + + CRITICAL RULES: + - Output exactly {count} numbered lines (same as input count) + - Number format: "N. text" (number, period, space, text) + - Do NOT include the original lines in your output + - Do NOT use "Line N:" format + - Follow the standard {language} conventions for {task}; no annotations, notes or tone marks + - If a line already needs no {task}, output one "SKIP" for it + - Output ONLY the {task}. No reasoning, no explanations, no step-by-step thinking. + + {lines}"""; + /** + * The prompt template for the section task. Where {@link #DEFAULT_PROMPT} rewrites each line, + * this one labels it: only the line opening a section is named, every other line is skipped, + * so the returned list maps onto {@code LyricsLine.songPart()} without any post-processing. + */ + public static final String SECTION_PROMPT = """ + You are a music structure analyst. Analyze the lyrics below and label the song structure. + Song: {title} by {artist} + + For every line, output the section that line opens. Only the FIRST line of a section carries its name; every other line that continues the same section is output as "SKIP". + + Use these exact section names: + Intro, Verse, Pre-Chorus, Chorus, Hook, Refrain, Bridge, Instrumental, Transition, False-Chorus, Outro, Reprise + + CRITICAL RULES: + - Output exactly {count} numbered lines (same as input count) + - Number format: "N. text" (number, period, space, text) + - First line of a section: its name. All other lines: "SKIP" + - Repeated sections are numbered in order: Verse 1, Verse 2, Chorus 1, Chorus 2 + - Pick the most specific name from the list above + - Output ONLY the labels. No reasoning, no explanations. + + {lines}"""; private static final int CONNECT_TIMEOUT_MS = 10_000; private static final int READ_TIMEOUT_MS = 45_000; private static final int MAX_CHARS = 3000; - private static final String MODELS_PATH_SUFFIX = "/models"; + private static final String CHAT_PATH = "/chat/completions"; + private static final String COMPLETIONS_PATH = "/completions"; + /** "/v1", "/v1beta/openai", "/compatible-mode/v1" and the like. */ + private static final Pattern VERSION_SEGMENT = + Pattern.compile("(?:/[^/]+)*/v\\d+(?:[a-z]+)?(?:/[^/]+)*"); + private static final Pattern NUMBERED_LINE_PATTERN = + Pattern.compile("^(?:Line\\s+)?(\\d{1,4})[.)\\]:\\-]\\s*(.*)$", Pattern.CASE_INSENSITIVE); + private static final Pattern PROMPT_VARIABLE = Pattern.compile("\\{(\\w+)\\}"); + + private OpenAIClient() { + } public static boolean validateConfig(String baseUrl, String apiToken, String model) { if (baseUrl == null || baseUrl.trim().isEmpty() @@ -50,10 +113,10 @@ public static boolean validateConfig(String baseUrl, String apiToken, String mod body.put("messages", new JSONArray().put( new JSONObject().put("role", "user").put("content", "hi"))); body.put("max_tokens", 1); - // Asked what the feature will ask it, so a body the feature needs is one the endpoint accepts. + // Validate with the same body the feature will send, so what works here works there. applyExtraBody(body); - conn = (HttpURLConnection) new URL(baseUrl.trim()).openConnection(); + conn = (HttpURLConnection) new URL(chatUrl(baseUrl)).openConnection(); conn.setRequestMethod("POST"); conn.setConnectTimeout(CONNECT_TIMEOUT_MS); conn.setReadTimeout(READ_TIMEOUT_MS); @@ -61,6 +124,8 @@ public static boolean validateConfig(String baseUrl, String apiToken, String mod if (apiToken != null && !apiToken.isEmpty()) { conn.setRequestProperty("Authorization", "Bearer " + apiToken); } + // Applied last, so user headers override defaults. + applyExtraHeaders(conn); conn.setDoOutput(true); byte[] payload = body.toString().getBytes(StandardCharsets.UTF_8); @@ -81,7 +146,7 @@ public static boolean validateConfig(String baseUrl, String apiToken, String mod } public static String[] fetchReasoningEfforts(String baseUrl, String apiToken, String model) { - String models = listModels(baseUrl, apiToken); + final String models = listModels(baseUrl, apiToken); if (models == null) { return new String[0]; } @@ -96,7 +161,7 @@ public static String[] fetchReasoningEfforts(String baseUrl, String apiToken, St /** The ids of the models the endpoint lists, empty when it lists none. */ public static List fetchModels(String baseUrl, @Nullable String apiToken) { List ids = new ArrayList<>(); - String models = listModels(baseUrl, apiToken); + final String models = listModels(baseUrl, apiToken); if (models == null) { return ids; } @@ -119,7 +184,7 @@ public static List fetchModels(String baseUrl, @Nullable String apiToken /** The raw response of the endpoint's model list, or null if it has none. */ @Nullable private static String listModels(String baseUrl, @Nullable String apiToken) { - String url = modelsUrl(baseUrl); + final String url = modelsUrl(chatUrl(baseUrl)); if (url == null) { return null; } @@ -132,6 +197,7 @@ private static String listModels(String baseUrl, @Nullable String apiToken) { if (apiToken != null && !apiToken.isEmpty()) { conn.setRequestProperty("Authorization", "Bearer " + apiToken); } + applyExtraHeaders(conn); if (conn.getResponseCode() != 200) { return null; } @@ -145,43 +211,60 @@ private static String listModels(String baseUrl, @Nullable String apiToken) { } } - private static String modelsUrl(String baseUrl) { + /** + * The address to call, whether the user gave the endpoint or the service root. A path ending in + * a version segment gets the rest of the endpoint appended; any other path is left alone. + */ + static String chatUrl(@Nullable String baseUrl) { if (baseUrl == null) { return null; } String base = baseUrl.trim(); + if (base.isEmpty()) { + return null; + } while (base.endsWith("/")) { base = base.substring(0, base.length() - 1); } + if (base.endsWith(CHAT_PATH) || base.endsWith(COMPLETIONS_PATH)) { + return base; + } final int scheme = base.indexOf("://"); final int slash = base.indexOf('/', scheme + 3); if (slash < 0) { + return base + CHAT_PATH; + } + final String path = base.substring(slash); + if (path.isEmpty() || VERSION_SEGMENT.matcher(path).matches()) { + return base + CHAT_PATH; + } + return base; + } + + private static String modelsUrl(@Nullable String chatUrl) { + if (chatUrl == null) { return null; } - String host = base.substring(0, slash); - String path = base.substring(slash); - for (String chatPath : new String[]{"/chat/completions", "/completions"}) { - if (path.endsWith(chatPath)) { - path = path.substring(0, path.length() - chatPath.length()); + String path = chatUrl; + for (String suffix : new String[]{CHAT_PATH, COMPLETIONS_PATH}) { + if (path.endsWith(suffix)) { + path = path.substring(0, path.length() - suffix.length()); break; } } while (path.endsWith("/")) { path = path.substring(0, path.length() - 1); } - if (path.endsWith(MODELS_PATH_SUFFIX)) { - return base; - } - return host + path + MODELS_PATH_SUFFIX; + return path.endsWith(MODELS_PATH_SUFFIX) ? path : path + MODELS_PATH_SUFFIX; } private static String[] parseSupportedEfforts(String json, String model) throws JSONException { - String trimmed = json.trim(); + final String trimmed = json.trim(); JSONArray entries; if (trimmed.startsWith("[")) { entries = new JSONArray(trimmed); } else { - JSONObject root = new JSONObject(trimmed); + final JSONObject root = new JSONObject(trimmed); entries = root.optJSONArray("data"); if (entries == null) { entries = root.optJSONArray("models"); @@ -191,23 +274,23 @@ private static String[] parseSupportedEfforts(String json, String model) throws return new String[0]; } for (int i = 0; i < entries.length(); i++) { - JSONObject entry = entries.optJSONObject(i); + final JSONObject entry = entries.optJSONObject(i); if (entry == null || !describesSameModel(entry, model)) { continue; } - Object reasoning = entry.opt("reasoning"); + final Object reasoning = entry.opt("reasoning"); if (!(reasoning instanceof JSONObject detail)) { // A plain true/false says the model reasons but not how hard, so there is nothing // to choose from and the parameter is not worth offering. return new String[0]; } - JSONArray efforts = detail.optJSONArray("supported_efforts"); + final JSONArray efforts = detail.optJSONArray("supported_efforts"); if (efforts == null || efforts.length() == 0) { return new String[0]; } - List values = new ArrayList<>(efforts.length()); + final List values = new ArrayList<>(efforts.length()); for (int j = 0; j < efforts.length(); j++) { - String value = efforts.optString(j, "").trim(); + final String value = efforts.optString(j, "").trim(); if (!value.isEmpty() && !values.contains(value)) { values.add(value); } @@ -221,9 +304,9 @@ private static boolean describesSameModel(JSONObject entry, String model) { if (model == null) { return false; } - String wanted = model.trim(); + final String wanted = model.trim(); for (String key : new String[]{"id", "name", "canonical_slug"}) { - String candidate = entry.optString(key, "").trim(); + final String candidate = entry.optString(key, "").trim(); if (candidate.isEmpty()) { continue; } @@ -232,7 +315,7 @@ private static boolean describesSameModel(JSONObject entry, String model) { return true; } } - JSONArray aliases = entry.optJSONArray("aliases"); + final JSONArray aliases = entry.optJSONArray("aliases"); if (aliases != null) { for (int i = 0; i < aliases.length(); i++) { if (aliases.optString(i, "").equalsIgnoreCase(wanted)) { @@ -251,7 +334,7 @@ private static String stripVariant(String id) { /** The stored effort, sent as typed, or empty when nothing is stored. */ @SuppressWarnings("ConstantValue") public static String reasoningEffort() { - String configured = Settings.LYRICS_AI_REASONING_EFFORT.get(); + final String configured = Settings.LYRICS_AI_REASONING_EFFORT.get(); if (configured != null && !configured.trim().isEmpty()) { return configured.trim(); } @@ -261,7 +344,7 @@ public static String reasoningEffort() { /** An array or a bare value has no keys to lay over a body, so neither counts as one. */ public static boolean isValidExtraBody(@Nullable String body) { if (body == null) return true; - String trimmed = body.trim(); + final String trimmed = body.trim(); if (trimmed.isEmpty()) return true; try { new JSONObject(trimmed); @@ -271,41 +354,91 @@ public static boolean isValidExtraBody(@Nullable String body) { } } + /** + * A header list has the same shape as a body, but a value has to fit on one line and a name + * cannot be blank, so those are refused here rather than left to fail when a request is made. + */ + public static boolean isValidExtraHeaders(@Nullable String headers) { + if (headers == null) return true; + final String trimmed = headers.trim(); + if (trimmed.isEmpty()) return true; + try { + final JSONObject parsed = new JSONObject(trimmed); + final Iterator keys = parsed.keys(); + while (keys.hasNext()) { + final String key = keys.next(); + final Object value = parsed.get(key); + if (key.trim().isEmpty() || !(value instanceof String text) + || text.indexOf('\r') >= 0 || text.indexOf('\n') >= 0) { + return false; + } + } + return true; + } catch (JSONException e) { + return false; + } + } + + /** + * Adds the headers the endpoint asks for. An empty value removes a header we would otherwise + * send, so an endpoint that wants its own key instead of a bearer token can drop ours. + */ + private static void applyExtraHeaders(HttpURLConnection conn) { + final String extra = Settings.LYRICS_AI_EXTRA_HEADERS.get(); + if (extra == null) return; + final String trimmed = extra.trim(); + if (trimmed.isEmpty()) return; + + try { + final JSONObject overlay = new JSONObject(trimmed); + final Iterator keys = overlay.keys(); + while (keys.hasNext()) { + final String key = keys.next(); + final String value = overlay.optString(key, ""); + if (value.isEmpty()) { + conn.setRequestProperty(key, null); + } else { + conn.setRequestProperty(key, value); + } + } + } catch (JSONException e) { + // A header list that will not parse is ignored; it must not take the feature down. + Logger.printDebug(() -> "The stored extra request headers are not valid JSON"); + } + } + /** * Two keys stay ours: the model, which has a setting the endpoint was validated against, and the * messages, whose length decides where the answer is cut into lines. */ private static void applyExtraBody(JSONObject body) { - String extra = Settings.LYRICS_AI_EXTRA_BODY.get(); + final String extra = Settings.LYRICS_AI_EXTRA_BODY.get(); if (extra == null) return; - String trimmed = extra.trim(); + final String trimmed = extra.trim(); if (trimmed.isEmpty()) return; try { - JSONObject overlay = new JSONObject(trimmed); - Iterator keys = overlay.keys(); + final JSONObject overlay = new JSONObject(trimmed); + final Iterator keys = overlay.keys(); while (keys.hasNext()) { - String key = keys.next(); + final String key = keys.next(); if (key.equals("model") || key.equals("messages")) continue; // get rather than opt, so an explicit null is sent as the null it was written as. body.put(key, overlay.get(key)); } } catch (JSONException e) { - // A body that will not parse is left out rather than allowed to take the feature down with it. + // An invalid stored body is ignored; it must not take the feature down. Logger.printDebug(() -> "The stored extra request body is not valid JSON"); } } - private OpenAIClient() { - } - @Nullable static String request(String baseUrl, String apiToken, String model, String userPrompt) { try { JSONObject body = new JSONObject(); body.put("model", model); - String effort = reasoningEffort(); + final String effort = reasoningEffort(); if (!effort.isEmpty()) { body.put("reasoning_effort", effort); } @@ -316,12 +449,12 @@ static String request(String baseUrl, String apiToken, String model, .put("content", userPrompt)); body.put("messages", messages); - // Laid over last, so what the user wrote wins even where it disagrees with the effort. + // Applied last, so user body wins even where it disagrees with the effort. applyExtraBody(body); byte[] payload = body.toString().getBytes(StandardCharsets.UTF_8); - URL url = new URL(baseUrl); + URL url = new URL(chatUrl(baseUrl)); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); try { conn.setRequestMethod("POST"); @@ -331,6 +464,7 @@ static String request(String baseUrl, String apiToken, String model, if (apiToken != null && !apiToken.isEmpty()) { conn.setRequestProperty("Authorization", "Bearer " + apiToken); } + applyExtraHeaders(conn); conn.setDoOutput(true); try (OutputStream os = conn.getOutputStream()) { @@ -447,9 +581,6 @@ public static boolean isSkipToken(@Nullable String s) { || t.equalsIgnoreCase("SKIP."); } - private static final Pattern NUMBERED_LINE_PATTERN = - Pattern.compile("^(?:Line\\s+)?(\\d{1,4})[.)\\]:\\-]\\s*(.*)$", Pattern.CASE_INSENSITIVE); - @Nullable private static List parseNumberedLines(String text, int expectedCount) { if (text == null || expectedCount <= 0) return null; @@ -529,7 +660,7 @@ private static boolean isNoteOrEmptyLine(String text) { @Nullable public static List mapLines(String baseUrl, String apiToken, String model, - String prompt, List sourceLines) { + String prompt, List sourceLines) { if (!fitsOneRequest(sourceLines)) { return null; } @@ -568,63 +699,6 @@ static List mapResponse(@Nullable String response, List sourceLi return anyValid ? out : null; } - /** - * The prompt template sent to the AI endpoint. One template covers both tasks; - * {@link #renderPrompt} fills in its variables: - *
      - *
    • {@code {task}} - "translation" or "romanization"
    • - *
    • {@code {language}} - the target language
    • - *
    • {@code {title}}, {@code {artist}} - the song
    • - *
    • {@code {count}} - the number of lines
    • - *
    • {@code {lines}} - the lyric lines, one per line
    • - *
    - * Unknown variables are left as typed. - */ - public static final String DEFAULT_PROMPT = """ - You are a professional lyrics {task} engine. Perform {task} on every line below for {language}, keeping the original meaning, tone and punctuation. - Song: {title} by {artist} - - Output format: Number every result on its own line, like: - 1. first result - 2. second result - - CRITICAL RULES: - - Output exactly {count} numbered lines (same as input count) - - Number format: "N. text" (number, period, space, text) - - Do NOT include the original lines in your output - - Do NOT use "Line N:" format - - Follow the standard {language} conventions for {task}; no annotations, notes or tone marks - - If a line already needs no {task}, output one "SKIP" for it - - Output ONLY the {task}. No reasoning, no explanations, no step-by-step thinking. - - {lines}"""; - - /** - * The prompt template for the section task. Where {@link #DEFAULT_PROMPT} rewrites each line, - * this one labels it: only the line opening a section is named, every other line is skipped, - * so the returned list maps onto {@code LyricsLine.songPart()} without any post-processing. - */ - public static final String SECTION_PROMPT = """ - You are a music structure analyst. Analyze the lyrics below and label the song structure. - Song: {title} by {artist} - - For every line, output the section that line opens. Only the FIRST line of a section carries its name; every other line that continues the same section is output as "SKIP". - - Use these exact section names: - Intro, Verse, Pre-Chorus, Chorus, Hook, Refrain, Bridge, Instrumental, Transition, False-Chorus, Outro, Reprise - - CRITICAL RULES: - - Output exactly {count} numbered lines (same as input count) - - Number format: "N. text" (number, period, space, text) - - First line of a section: its name. All other lines: "SKIP" - - Repeated sections are numbered in order: Verse 1, Verse 2, Chorus 1, Chorus 2 - - Pick the most specific name from the list above - - Output ONLY the labels. No reasoning, no explanations. - - {lines}"""; - - private static final Pattern PROMPT_VARIABLE = Pattern.compile("\\{(\\w+)\\}"); - public static String renderPrompt(String template, String task, String language, String title, String artist, List lines) { if (template == null || template.isEmpty()) { diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/derive/QuickLRCClient.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/derive/QuickLRCClient.java index b20cbfac41..7222ffbb35 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/derive/QuickLRCClient.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/derive/QuickLRCClient.java @@ -33,11 +33,9 @@ public final class QuickLRCClient { + public static final String PROVIDER_NAME = "QuickLRC"; private static final ExecutorService executor = Executors.newSingleThreadExecutor(); - private static final String ENDPOINT = "https://quicklrc.com/api/v1/transcribe"; - public static final String PROVIDER_NAME = "QuickLRC"; - private static final int CONNECT_TIMEOUT_MS = 15_000; private static final int READ_TIMEOUT_MS = 180_000; @@ -45,38 +43,16 @@ public final class QuickLRCClient { private static volatile Future running; - /** Why a run produced nothing, so the caller can say something more useful than "failed". */ - public enum Failure { - CANCELLED, - NO_KEY, - UNSUPPORTED_SOURCE, - OUT_OF_CREDITS, - FAILED - } - - /** - * What a key check concluded. {@link #REJECTED} is the only one that should refuse a save. - */ - public enum KeyCheck { - /** The service read the key and objected to something else. */ - ACCEPTED, - /** The service refused the key itself. */ - REJECTED, - /** The service could not be asked, so the key is neither proven nor disproven. */ - UNREACHABLE - } - - public interface KeyCheckCallback { - void onChecked(KeyCheck result); + private QuickLRCClient() { } public static void checkKey(String apiKey, KeyCheckCallback callback) { - String key = apiKey == null ? "" : apiKey.trim(); + final String key = apiKey == null ? "" : apiKey.trim(); if (key.isEmpty()) { callback.onChecked(KeyCheck.REJECTED); return; } - Thread worker = new Thread(() -> { + final Thread worker = new Thread(() -> { KeyCheck result; HttpURLConnection conn = null; try { @@ -99,33 +75,29 @@ public static void checkKey(String apiKey, KeyCheckCallback callback) { conn.disconnect(); } } - KeyCheck answer = result; + final KeyCheck answer = result; Utils.runOnMainThread(() -> callback.onChecked(answer)); }, "QuickLRC key check"); worker.setDaemon(true); worker.start(); } - public interface Callback { - void onTranscribed(@Nullable Lyrics lyrics, @Nullable Failure failure); - } - - private QuickLRCClient() { - } - @SuppressWarnings("ConstantValue") public static boolean isConfigured() { - String key = Settings.LYRICS_TRANSCRIBE_KEY.get(); + if (!Settings.LYRICS_QUICKLRC_ENABLED.get()) { + return false; + } + final String key = Settings.LYRICS_TRANSCRIBE_KEY.get(); return key != null && !key.trim().isEmpty(); } public static boolean isRunning() { - Future current = running; + final Future current = running; return current != null && !current.isDone(); } public static void cancel() { - Future current = running; + final Future current = running; running = null; if (current != null) { current.cancel(true); @@ -135,17 +107,17 @@ public static void cancel() { @SuppressWarnings("ConstantValue") public static void transcribe(String videoId, @Nullable String lyrics, BooleanSupplier stillWanted, Callback callback) { - String key = Settings.LYRICS_TRANSCRIBE_KEY.get(); + final String key = Settings.LYRICS_TRANSCRIBE_KEY.get(); if (key == null || key.trim().isEmpty()) { Utils.runOnMainThread(() -> callback.onTranscribed(null, Failure.NO_KEY)); return; } - String source = videoId == null ? "" : videoId.trim(); + final String source = videoId == null ? "" : videoId.trim(); if (source.isEmpty()) { Utils.runOnMainThread(() -> callback.onTranscribed(null, Failure.UNSUPPORTED_SOURCE)); return; } - String text = lyrics == null ? "" : lyrics.trim(); + final String text = lyrics == null ? "" : lyrics.trim(); cancel(); running = executor.submit(() -> { @@ -153,7 +125,7 @@ public static void transcribe(String videoId, @Nullable String lyrics, Lyrics result = null; try { if (stillWanted.getAsBoolean()) { - String ttml = post(source, text, key.trim()); + final String ttml = post(source, text, key.trim()); if (ttml == null) { failure = Failure.FAILED; } else { @@ -176,21 +148,15 @@ public static void transcribe(String videoId, @Nullable String lyrics, } } - Lyrics deliver = result; - Failure reason = failure; + final Lyrics deliver = result; + final Failure reason = failure; Utils.runOnMainThread(() -> callback.onTranscribed(deliver, reason)); }); } - private static final class OutOfCreditsException extends IOException { - OutOfCreditsException() { - super("QuickLRC credits exhausted"); - } - } - private static String post(String videoId, String lyrics, String apiKey) throws IOException, JSONException { - JSONObject body = new JSONObject(); + final JSONObject body = new JSONObject(); body.put("fileUrl", "https://www.youtube.com/watch?v=" + videoId); body.put("format", "lrc"); body.put("isWordLevel", true); @@ -198,19 +164,19 @@ private static String post(String videoId, String lyrics, String apiKey) body.put("lyrics", lyrics); } - String json = body.toString(); + final String json = body.toString(); String target = ENDPOINT; for (int hop = 0; hop <= MAX_REDIRECTS; hop++) { - Attempt attempt = sendOnce(target, json, apiKey); + final Attempt attempt = sendOnce(target, json, apiKey); final int code = attempt.code(); if (code == 307 || code == 308 || code == 301 || code == 302) { - String location = attempt.location(); + final String location = attempt.location(); if (location == null || location.isEmpty()) { return null; } - String next = resolve(target, location); + final String next = resolve(target, location); if (next == null) { return null; } @@ -238,13 +204,6 @@ private static String post(String videoId, String lyrics, String apiKey) return null; } - private record Attempt(int code, String message, String contentType, int contentLength, - String location, String body, String errorBody) { - public Attempt { - if (message == null) message = ""; - } - } - private static Attempt sendOnce(String url, String json, String apiKey) throws IOException { HttpURLConnection conn = null; try { @@ -258,15 +217,15 @@ private static Attempt sendOnce(String url, String json, String apiKey) throws I conn.setDoOutput(true); conn.setInstanceFollowRedirects(false); - byte[] payload = json.getBytes(StandardCharsets.UTF_8); + final byte[] payload = json.getBytes(StandardCharsets.UTF_8); try (java.io.OutputStream out = conn.getOutputStream()) { out.write(payload); } final int code = conn.getResponseCode(); - String location = conn.getHeaderField("Location"); + final String location = conn.getHeaderField("Location"); if (code == 200) { - String body = Requester.parseString(conn); + final String body = Requester.parseString(conn); return new Attempt(code, conn.getResponseMessage(), conn.getContentType(), conn.getContentLength(), location, body, null); } @@ -282,7 +241,7 @@ private static Attempt sendOnce(String url, String json, String apiKey) throws I @Nullable private static String resolve(String base, String location) { try { - java.net.URL resolved = new URL(new URL(base), location); + final java.net.URL resolved = new URL(new URL(base), location); return resolved.getProtocol() + "://" + resolved.getAuthority() + resolved.getPath() + (resolved.getQuery() == null ? "" : "?" + resolved.getQuery()); } catch (java.net.MalformedURLException ex) { @@ -292,13 +251,11 @@ private static String resolve(String base, String location) { /** * Drops the gaps the service puts between CJK tokens, as in {@code 我 靠 , 我}. - * - *

    On the body, so the cleaned text becomes the raw format and the cache and the - * exporter store it. Hand-scanned: both sides have to agree, and no regex form that looks - * that far past the tag matches. + * Applied to the body so the cleaned text is what the raw format, cache and exporter store; + * hand-rolled because no regex looks that far past the tags. */ private static String dropCjkSeparators(String lrc) { - StringBuilder out = new StringBuilder(lrc.length()); + final StringBuilder out = new StringBuilder(lrc.length()); for (int i = 0; i < lrc.length(); ) { if (Character.isWhitespace(lrc.charAt(i)) && isCjkGap(lrc, i)) { i = skipWhitespace(lrc, i); @@ -380,8 +337,8 @@ private static boolean isCjk(char c) { private static Lyrics toLyrics(String lrc) { try { // Cleaned first, so the raw format and everything reading it are the clean one. - String body = dropCjkSeparators(lrc); - List lines = LRCParser.parseSynced(body); + final String body = dropCjkSeparators(lrc); + final List lines = LRCParser.parseSynced(body); if (lines == null || lines.isEmpty()) { return null; } @@ -412,4 +369,46 @@ private static Lyrics toLyrics(String lrc) { return null; } } + + /** Why a run produced nothing, so the caller can say something more useful than "failed". */ + public enum Failure { + CANCELLED, + NO_KEY, + UNSUPPORTED_SOURCE, + OUT_OF_CREDITS, + FAILED + } + + /** + * What a key check concluded. {@link #REJECTED} is the only one that should refuse a save. + */ + public enum KeyCheck { + /** The service read the key and objected to something else. */ + ACCEPTED, + /** The service refused the key itself. */ + REJECTED, + /** The service could not be asked, so the key is neither proven nor disproven. */ + UNREACHABLE + } + + public interface KeyCheckCallback { + void onChecked(KeyCheck result); + } + + public interface Callback { + void onTranscribed(@Nullable Lyrics lyrics, @Nullable Failure failure); + } + + private static final class OutOfCreditsException extends IOException { + OutOfCreditsException() { + super("QuickLRC credits exhausted"); + } + } + + private record Attempt(int code, String message, String contentType, int contentLength, + String location, String body, String errorBody) { + public Attempt { + if (message == null) message = ""; + } + } } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/derive/SectionDetector.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/derive/SectionDetector.java index 6b4fdd59e8..bbc59da83f 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/derive/SectionDetector.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/derive/SectionDetector.java @@ -26,12 +26,9 @@ import app.morphe.extension.shared.Utils; /** - * Asks the AI which part of the song each lyric line belongs to, for lyrics that ship without any - * section of their own. A label the provider published beats one inferred from the text, so only - * lyrics that carry none at all are asked about. - * - *

    The answer arrives one entry per line, with only the line opening a section named, so the - * list maps straight onto {@link LyricsLine#songPart()}. + * Asks the AI which part of the song each lyric line belongs to, only for lyrics that ship + * without any section of their own: a provider-published label beats one inferred from the text. + * The answer names only the line opening a section, mapping straight onto {@link LyricsLine#songPart()}. */ public final class SectionDetector { @@ -39,10 +36,6 @@ public final class SectionDetector { private static final String SKIP = "SKIP"; - public interface Callback { - void onSections(@Nullable List sections); - } - private SectionDetector() { } @@ -60,13 +53,13 @@ public static boolean needsDetection(@Nullable Lyrics lyrics) { } public static void detect(TrackInfo track, Lyrics lyrics, BooleanSupplier stillWanted, - Callback callback) { + Callback callback) { List lines = lyrics.lines(); - List texts = new ArrayList<>(lines.size()); + final List texts = new ArrayList<>(lines.size()); for (LyricsLine line : lines) { texts.add(line.text()); } - String source = lyrics.providerName(); + final String source = lyrics.providerName(); executor.execute(() -> { try { @@ -76,7 +69,7 @@ public static void detect(TrackInfo track, Lyrics lyrics, BooleanSupplier stillW } List cached = LyricsCache.getSectionAI(track, source, texts); if (cached != null) { - List result = cached; + final List result = cached; Utils.runOnMainThread(() -> callback.onSections(result)); return; } @@ -89,7 +82,7 @@ public static void detect(TrackInfo track, Lyrics lyrics, BooleanSupplier stillW if (detected != null) { LyricsCache.putSectionAI(track, source, texts, detected); } - List result = detected; + final List result = detected; Utils.runOnMainThread(() -> callback.onSections(result)); } catch (Exception ex) { Logger.printException(() -> "detect failure", ex); @@ -100,7 +93,7 @@ public static void detect(TrackInfo track, Lyrics lyrics, BooleanSupplier stillW @Nullable private static List aiDetect(List lines, String title, String artist, - String baseUrl, String apiToken, String model) { + String baseUrl, String apiToken, String model) { String prompt = OpenAIClient.renderPrompt(Settings.LYRICS_AI_SECTION_PROMPT.get(), "section", "", title, artist, lines); return OpenAIClient.mapLines(baseUrl, apiToken, model, prompt, lines); @@ -149,4 +142,8 @@ private static String getLabel(@NonNull List sections, int i) { } return label; } + + public interface Callback { + void onSections(@Nullable List sections); + } } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/Lyrics.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/Lyrics.java index d7e6904c12..88ea3c129c 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/Lyrics.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/Lyrics.java @@ -19,19 +19,19 @@ /** * Lyrics of a single track, either synced (each line carries a timestamp) or plain. * - * @param providerName Provider name shown to the user, for example {@code LRCLIB}. - * @param translations Embedded translations keyed by BCP-47 language tag (e.g. {@code zh}, - * {@code en}, {@code zh-Hans}). A translation entry is a line-level list - * aligned 1:1 with {@link #lines()}; an empty string marks a line with no - * translation. {@code null} when the source ships none. - * @param romanization Legacy single-language romanization, kept for backward compatibility. + * @param providerName Provider name shown to the user, for example {@code LRCLIB}. + * @param translations Embedded translations keyed by BCP-47 language tag (e.g. {@code zh}, + * {@code en}, {@code zh-Hans}). A translation entry is a line-level list + * aligned 1:1 with {@link #lines()}; an empty string marks a line with no + * translation. {@code null} when the source ships none. + * @param romanization Legacy single-language romanization, kept for backward compatibility. * @param romanizations Multi-language romanizations keyed by BCP-47 language tag. - * @param songwriters Songwriter/credits names extracted from TTML metadata. - * @param rawFormat Decrypted raw lyrics text in the provider's original format (e.g. LRC, QRC, TTML). - * @param formatType File extension for the raw format (e.g. {@code "lrc"}, {@code "qrc"}, {@code "ttml"}). - * @param sourceUrl Optional URL to the song page on the provider's platform, opened when the - * source label is clicked. - * @param about Song description, shown under the credits on the info line. + * @param songwriters Songwriter/credits names extracted from TTML metadata. + * @param rawFormat Decrypted raw lyrics text in the provider's original format (e.g. LRC, QRC, TTML). + * @param formatType File extension for the raw format (e.g. {@code "lrc"}, {@code "qrc"}, {@code "ttml"}). + * @param sourceUrl Optional URL to the song page on the provider's platform, opened when the + * source label is clicked. + * @param about Song description, shown under the credits on the info line. */ public record Lyrics(List lines, String providerName, boolean synced, @Nullable List romanization, @@ -43,6 +43,11 @@ public record Lyrics(List lines, String providerName, boolean synced @Nullable String sourceUrl, @Nullable String about) { + /** Marker for a track that was looked up successfully but has no lyrics anywhere. */ + public static final Lyrics NOT_FOUND = new Lyrics(Collections.emptyList(), "", false, + null, null, null, null, null, null, null); + public static final String CAPTIONS_PROVIDER = "Captions"; + /** Every provider but Genius ships no description, so most call sites keep the shorter form. */ public Lyrics(List lines, String providerName, boolean synced, @Nullable List romanization, @@ -56,20 +61,6 @@ public Lyrics(List lines, String providerName, boolean synced, songwriters, rawFormat, formatType, sourceUrl, null); } - public record ScoredLyrics(int score, Lyrics lyrics) { - } - - public static List sortScoredByScore(List scored) { - scored.sort((a, b) -> b.score - a.score); - return scored; - } - - /** Marker for a track that was looked up successfully but has no lyrics anywhere. */ - public static final Lyrics NOT_FOUND = new Lyrics(Collections.emptyList(), "", false, - null, null, null, null, null, null, null); - - public static final String CAPTIONS_PROVIDER = "Captions"; - public Lyrics { lines = Collections.unmodifiableList(lines); romanization = (romanization == null) ? null : Collections.unmodifiableList(romanization); @@ -77,6 +68,11 @@ public static List sortScoredByScore(List scored) { romanizations = (romanizations == null) ? null : unmodifiableTranslations(romanizations); } + public static List sortScoredByScore(List scored) { + scored.sort((a, b) -> b.score - a.score); + return scored; + } + private static Map> unmodifiableTranslations( Map> in) { Map> out = new HashMap<>(2 * in.size()); @@ -87,64 +83,6 @@ private static Map> unmodifiableTranslations( return Collections.unmodifiableMap(out); } - public boolean isEmpty() { - return lines.isEmpty(); - } - - /** - * Index of the line that should be highlighted at the given playback position, - * or -1 if playback has not reached the first line yet. - * - *

    No binary search: between two calls the position advances by one line at most, - * so the hint answers almost every call in a few comparisons. - * - * @param positionMs Current playback position. - * @param hintIndex Index returned by the previous call, or -1. - */ - public int indexForPosition(long positionMs, int hintIndex) { - if (!synced || lines.isEmpty()) { - return -1; - } - - final int size = lines.size(); - int result = -1; - - // Fast path: still inside the hinted line, or moved into the next one. - if (hintIndex >= 0 && hintIndex < size) { - if (positionMs >= lines.get(hintIndex).startTimeMs()) { - final int next = hintIndex + 1; - if (next >= size || positionMs < lines.get(next).startTimeMs()) { - result = hintIndex; - } else { - final int nextNext = next + 1; - if (nextNext >= size || positionMs < lines.get(nextNext).startTimeMs()) { - result = next; - } - } - } - } - - if (result < 0) { - for (int i = 0; i < size; i++) { - if (lines.get(i).startTimeMs() > positionMs) { - break; - } - result = i; - } - } - - while (result >= 0 && lines.get(result).isBG()) { - result--; - } - return result; - } - - @NonNull - @Override - public String toString() { - return "Lyrics{" + providerName + ", synced=" + synced + ", lines=" + lines.size() + "}"; - } - public static List fixAnomalousWordTimestamps(List lines) { final int size = lines.size(); if (size == 0) return lines; @@ -242,7 +180,7 @@ public static List clampLastWordEnds(List lines) { long effectiveEnd = lastWord.endMs(); long nextStart = LyricsLine.NO_TIME; for (int j = i + 1; j < size; j++) { - LyricsLine candidate = out.get(j); + final LyricsLine candidate = out.get(j); if (candidate.isBG()) { continue; } @@ -276,4 +214,65 @@ public static List clampLastWordEnds(List lines) { } return Collections.unmodifiableList(out); } + + public boolean isEmpty() { + return lines.isEmpty(); + } + + /** + * Index of the line that should be highlighted at the given playback position, + * or -1 if playback has not reached the first line yet. + * + *

    No binary search: between two calls the position advances by one line at most, + * so the hint answers almost every call in a few comparisons. + * + * @param positionMs Current playback position. + * @param hintIndex Index returned by the previous call, or -1. + */ + public int indexForPosition(long positionMs, int hintIndex) { + if (!synced || lines.isEmpty()) { + return -1; + } + + final int size = lines.size(); + int result = -1; + + // Fast path: still inside the hinted line, or moved into the next one. + if (hintIndex >= 0 && hintIndex < size) { + if (positionMs >= lines.get(hintIndex).startTimeMs()) { + final int next = hintIndex + 1; + if (next >= size || positionMs < lines.get(next).startTimeMs()) { + result = hintIndex; + } else { + final int nextNext = next + 1; + if (nextNext >= size || positionMs < lines.get(nextNext).startTimeMs()) { + result = next; + } + } + } + } + + if (result < 0) { + for (int i = 0; i < size; i++) { + if (lines.get(i).startTimeMs() > positionMs) { + break; + } + result = i; + } + } + + while (result >= 0 && lines.get(result).isBG()) { + result--; + } + return result; + } + + @NonNull + @Override + public String toString() { + return "Lyrics{" + providerName + ", synced=" + synced + ", lines=" + lines.size() + "}"; + } + + public record ScoredLyrics(int score, Lyrics lyrics) { + } } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/LyricsBracketSplitter.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/LyricsBracketSplitter.java index d37abf607c..477e0b7004 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/LyricsBracketSplitter.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/LyricsBracketSplitter.java @@ -22,13 +22,7 @@ public final class LyricsBracketSplitter { private static final LyricsLine BLANK = new LyricsLine(LyricsLine.NO_TIME, ""); private static final Pattern NOT_SUNG_RUN = Pattern.compile( - "(?i)^[\\s()()\\[\\]]*(?:x\\s*\\d+|\\d+\\s*x|repeat(?:\\s*x?\\d+)?|chorus|verse(?:\\s*\\d+)?|pre[-\\s]*chorus|post[-\\s]*chorus|intro|outro|instrumental|bridge|hook|interlude|solo|music|guitar\\s*solo)[\\s()()\\[\\]]*$"); - - private record Run(int open, int close) { - } - - private record Cut(LyricsLine sung, LyricsLine background) { - } + "(?i)\\s*(?:x\\s*\\d+|\\d+\\s*x|repeat|chorus|verse|pre\\s*chorus|post\\s*chorus)\\s*"); private LyricsBracketSplitter() { } @@ -38,351 +32,159 @@ public static Lyrics apply(Lyrics lyrics) { return lyrics; } - List lines = lyrics.lines(); + final List lines = lyrics.lines(); final int count = lines.size(); - Cut[] cuts = new Cut[count]; - LyricsLine[] normalizedBg = new LyricsLine[count]; - int[] room = new int[count]; + final int[] room = new int[count]; int extra = 0; - boolean anyNormalized = false; - for (int i = 0; i < count; i++) { - Cut c = cut(lines, i); - if (c != null) { - cuts[i] = c; - room[i] = 2; - extra++; - } else { - room[i] = 1; - LyricsLine norm = normalizeStandaloneBg(lines, i); - if (norm != null) { - normalizedBg[i] = norm; - anyNormalized = true; - } - } + room[i] = cut(lines, i) == null ? 1 : 2; + extra += room[i] - 1; } - - if (extra == 0 && !anyNormalized) { + if (extra == 0) { return lyrics; } - List split = new ArrayList<>(count + extra); + final List split = new ArrayList<>(count + extra); for (int i = 0; i < count; i++) { - Cut c = cuts[i]; - if (c != null) { - split.add(c.sung()); - split.add(c.background()); - } else if (normalizedBg[i] != null) { - split.add(normalizedBg[i]); - } else { + final Cut cut = cut(lines, i); + if (cut == null) { split.add(lines.get(i)); + } else { + split.add(cut.sung()); + split.add(cut.background()); } } - List finalLines = split; - if (lyrics.synced() && !finalLines.isEmpty()) { - finalLines = Lyrics.clampLastWordEnds(Lyrics.fixAnomalousWordTimestamps(finalLines)); - } - - return new Lyrics( - finalLines, - lyrics.providerName(), - lyrics.synced(), - widen(lyrics.romanization(), count, room, cuts, normalizedBg, true), - widenMap(lyrics.translations(), count, room, cuts, normalizedBg, false), - widenMap(lyrics.romanizations(), count, room, cuts, normalizedBg, true), - lyrics.songwriters(), - lyrics.rawFormat(), - lyrics.formatType(), - lyrics.sourceUrl(), - lyrics.about()); + return new Lyrics(split, lyrics.providerName(), lyrics.synced(), + widen(lyrics.romanization(), count, room), + widen(lyrics.translations(), count, room), + widen(lyrics.romanizations(), count, room), + lyrics.songwriters(), lyrics.rawFormat(), lyrics.formatType(), + lyrics.sourceUrl(), lyrics.about()); } - /** - * Splits a line sung partly in round brackets, half or full width, into the lead outside - * them and the background vocal inside. - */ + /** Reading brackets on top of a stated split would put two background lines under one sung line. */ @Nullable private static Cut cut(List lines, int index) { - LyricsLine line = lines.get(index); - String text = line.text(); - // A background line stays one: cutting it would turn the part outside the brackets into a lead. - if (line.isBG() || text.trim().isEmpty() || hasTagBrackets(text)) { - return null; - } - - List runs = findSungBracketRuns(text); - if (runs.isEmpty()) { + final LyricsLine line = lines.get(index); + if (line.isBG() || line.text().trim().isEmpty()) { return null; } - - String outsideText = extractOutsideText(text, runs); - String insideText = extractInsideText(text, runs); - if (!sung(outsideText) || !sung(insideText)) { + if (index + 1 < lines.size() && lines.get(index + 1).isBG()) { return null; } - - if (line.hasWords()) { - Cut wordCut = cutWordTimed(line, runs, outsideText, insideText); - if (wordCut != null) { - return wordCut; - } - } - return cutLineTimed(line, runs, outsideText, insideText); + return line.hasWords() ? cutWordTimed(line) : cutLineTimed(line); } - /** - * A line written whole in brackets, such as "(Ooh, yeah)", becomes a background line of the lead - * before it, and a background line loses the brackets it still carries in its text or words. - */ @Nullable - private static LyricsLine normalizeStandaloneBg(List lines, int index) { - LyricsLine line = lines.get(index); - String raw = line.text(); - if (raw.trim().isEmpty() || hasTagBrackets(raw)) { - return null; - } - String trimmed = raw.trim(); - final boolean outerParens = hasBalancedOuterBrackets(trimmed); - if (!line.isBG() && (!outerParens || index == 0)) { - return null; - } - String strippedText = stripOuterBrackets(trimmed); - if (!sung(strippedText)) { + private static Cut cutWordTimed(LyricsLine line) { + final String text = line.text(); + final List words = line.words(); + // The renderer reads the words back out of the line text, so the two have to cover the + // same characters for the offsets below to mean anything. + if (lengthOf(words) != text.length()) { return null; } - // A bracketed line with no lead above it is the lead itself. - if (!line.isBG()) { - boolean hasLeadBefore = false; - for (int p = index - 1; p >= 0; p--) { - LyricsLine prev = lines.get(p); - if (!prev.isBG() && !hasBalancedOuterBrackets(prev.text().trim())) { - hasLeadBefore = true; - break; - } - } - if (!hasLeadBefore) { - return null; - } - } - List origWords = line.words(); - List cleanedWords = origWords.isEmpty() ? origWords : trimBrackets(origWords); - final boolean textChanged = !strippedText.equals(raw); - final boolean bgChanged = !line.isBG(); - boolean wordsChanged = cleanedWords.size() != origWords.size(); - if (!wordsChanged) { - for (int i = 0; i < cleanedWords.size(); i++) { - if (!cleanedWords.get(i).text().equals(origWords.get(i).text())) { - wordsChanged = true; - break; - } - } - } - if (!textChanged && !bgChanged && !wordsChanged) { + final Run head = headRun(text); + final Run tail = tailRun(text); + if (head == null && tail == null) { return null; } - return new LyricsLine( - line.startTimeMs(), - line.endTimeMs(), - strippedText, - cleanedWords, - line.agentId(), - line.isDuet(), - true, - line.songPart()); - } - - @Nullable - private static Cut cutWordTimed(LyricsLine line, List runs, - String fallbackSungText, String fallbackBgText) { - String text = line.text(); - List words = line.words(); - if (words.isEmpty()) { + final int sungFrom = head == null ? 0 : head.close() + 1; + final int sungTo = tail == null ? text.length() : tail.open(); + if (sungFrom >= sungTo || !sung(text.substring(sungFrom, sungTo)) + || (head != null && !sung(text.substring(head.open(), head.close() + 1))) + || (tail != null && !sung(text.substring(tail.open(), tail.close() + 1)))) { return null; } - List sung = new ArrayList<>(words.size()); - List background = new ArrayList<>(words.size()); - int searchFrom = 0; - - for (int wi = 0; wi < words.size(); wi++) { - Word word = words.get(wi); - String wText = word.text(); - if (wText == null || wText.isEmpty()) { - continue; - } - - while (searchFrom < text.length() && Character.isWhitespace(text.charAt(searchFrom))) { - searchFrom++; - } - - int found = text.indexOf(wText, searchFrom); - int matchLen = wText.length(); - if (found < 0) { - String trimmedW = wText.trim(); - if (!trimmedW.isEmpty()) { - found = text.indexOf(trimmedW, searchFrom); - matchLen = trimmedW.length(); - } - } - - final int from; - final int to; - if (found >= 0) { - from = found; - to = Math.min(text.length(), found + matchLen); - searchFrom = to; - } else { - from = Math.min(searchFrom, text.length()); - to = Math.min(text.length(), from + wText.trim().length()); - searchFrom = to; - } - - final boolean spaceAfterInText = to < text.length() && Character.isWhitespace(text.charAt(to)); - final boolean hasSpace = word.endsWithSpace() || spaceAfterInText; + final List sung = new ArrayList<>(words.size()); + final List background = new ArrayList<>(words.size()); + int offset = 0; + for (Word word : words) { + final int from = offset; + final int to = offset + word.text().length(); + offset = to; + final int inSung = Math.max(0, Math.min(to, sungTo) - Math.max(from, sungFrom)); int inBackground = 0; - for (int ri = 0; ri < runs.size(); ri++) { - Run r = runs.get(ri); - inBackground += Math.max(0, Math.min(to, r.close() + 1) - Math.max(from, r.open())); + if (head != null) { + inBackground += Math.max(0, Math.min(to, head.close() + 1) - from); + } + if (tail != null) { + inBackground += Math.max(0, to - Math.max(from, tail.open())); } - final int spanLen = Math.max(0, to - from); - final int inSung = Math.max(0, spanLen - inBackground); if (inSung <= 0 && inBackground <= 0) { - // Fallback: if the word itself starts and ends with brackets, it belongs to background. - String wt = wText.trim(); - if (!wt.isEmpty() && (isOpen(wt.charAt(0)) || isClose(wt.charAt(wt.length() - 1)))) { - background.add(new Word(word.startMs(), word.endMs(), wText, word.romaji(), hasSpace)); - } else { - sung.add(new Word(word.startMs(), word.endMs(), wText, word.romaji(), hasSpace)); - } continue; } - - if (inBackground == 0) { - sung.add(new Word(word.startMs(), word.endMs(), wText, word.romaji(), hasSpace)); - } else if (inSung == 0) { - background.add(new Word(word.startMs(), word.endMs(), wText, word.romaji(), hasSpace)); + // Halving a straddling word would cost it its timing, so it goes whole to the bigger side. + if (inSung > inBackground || (inSung == inBackground && inSung > 0)) { + sung.add(word); } else { - // Single Word token straddles a bracket boundary (e.g. "you(yeah)" or "(ooh)baby"). - StringBuilder sungPart = new StringBuilder(); - StringBuilder bgPart = new StringBuilder(); - boolean startsInBg = false; - for (int ci = from; ci < to && ci < text.length(); ci++) { - final boolean charInBg = isIndexInsideRuns(ci, runs); - if (ci == from) { - startsInBg = charInBg; - } - if (charInBg) { - bgPart.append(text.charAt(ci)); - } else { - sungPart.append(text.charAt(ci)); - } - } - String sStr = stripAllBrackets(sungPart.toString()).trim(); - String bStr = stripAllBrackets(bgPart.toString()).trim(); - final long wStart = word.startMs(); - final long wEnd = word.endMs(); - final long dur = (wStart != LyricsLine.NO_TIME && wEnd > wStart) ? (wEnd - wStart) : 0L; - final long splitTime = (dur > 0 && spanLen > 0) - ? wStart + (dur * (startsInBg ? inBackground : inSung)) / spanLen - : wStart; - - if (!sStr.isEmpty()) { - final long sStart = startsInBg ? splitTime : wStart; - final long sEnd = startsInBg ? wEnd : splitTime; - sung.add(new Word(sStart, sEnd, sStr, word.romaji(), !startsInBg || hasSpace)); - } - if (!bStr.isEmpty()) { - final long bStart = startsInBg ? wStart : splitTime; - final long bEnd = startsInBg ? splitTime : wEnd; - background.add(new Word(bStart, bEnd, bStr, null, startsInBg || hasSpace)); - } + background.add(word); } } - if (sung.isEmpty() || background.isEmpty()) { - return null; - } + return finish(line, sung, background); + } + + /** Both halves keep the line's one timing, there being none to divide. */ + @Nullable + private static Cut cutLineTimed(LyricsLine line) { + final String text = line.text(); - List trimmedSung = trimBrackets(sung); - List trimmedBackground = trimBrackets(background); - if (trimmedSung.isEmpty() || trimmedBackground.isEmpty()) { + final Run head = headRun(text); + final Run tail = tailRun(text); + if (head == null && tail == null) { return null; } - String sungText = textOf(trimmedSung); - String backgroundText = textOf(trimmedBackground); - if (sungText.trim().isEmpty()) { - sungText = fallbackSungText; - } - if (backgroundText.trim().isEmpty()) { - backgroundText = fallbackBgText; - } - if (!sung(sungText) || !sung(backgroundText)) { + final int sungFrom = head == null ? 0 : head.close() + 1; + final int sungTo = tail == null ? text.length() : tail.open(); + if (sungFrom >= sungTo) { return null; } - final long bgStart = firstValidStart(trimmedBackground, line.startTimeMs()); - final long bgEnd = lastValidEnd(trimmedBackground, line.endTimeMs()); - final long sungWordStart = firstValidStart(trimmedSung, line.startTimeMs()); - final long sungWordEnd = lastValidEnd(trimmedSung, line.endTimeMs()); - - // Parent sungLine's line-level [startTimeMs, endTimeMs] spans the full cluster so - // Lyrics.indexForPosition activates the parent line even when bgBeforeMain starts early, - // while sungLine.words() and bgLine.words() keep their exact individual word timings. - long parentStart = sungWordStart; - if (line.startTimeMs() != LyricsLine.NO_TIME && parentStart != LyricsLine.NO_TIME) { - parentStart = Math.min(line.startTimeMs(), parentStart); - } - if (bgStart != LyricsLine.NO_TIME && parentStart != LyricsLine.NO_TIME) { - parentStart = Math.min(parentStart, bgStart); + final StringBuilder backgroundText = new StringBuilder(); + if (head != null) { + backgroundText.append(text, head.open() + 1, head.close()); } - long parentEnd = sungWordEnd; - if (line.endTimeMs() != LyricsLine.NO_TIME && parentEnd != LyricsLine.NO_TIME) { - parentEnd = Math.max(line.endTimeMs(), parentEnd); - } - if (bgEnd != LyricsLine.NO_TIME && parentEnd != LyricsLine.NO_TIME) { - parentEnd = Math.max(parentEnd, bgEnd); + if (tail != null) { + if (backgroundText.length() > 0) { + backgroundText.append(' '); + } + backgroundText.append(text, tail.open() + 1, tail.close()); } - LyricsLine sungLine = new LyricsLine( - parentStart, parentEnd, sungText, trimmedSung, - line.agentId(), line.isDuet(), false, line.songPart()); - LyricsLine bgLine = new LyricsLine( - bgStart, bgEnd, backgroundText, trimmedBackground, - line.agentId(), line.isDuet(), true, line.songPart()); - return new Cut(sungLine, bgLine); + return finishText(line, text.substring(sungFrom, sungTo), backgroundText.toString()); } @Nullable - private static Cut cutLineTimed(LyricsLine line, List runs, - String outsideText, String insideText) { - String text = line.text(); - int firstNonSpace = 0; - while (firstNonSpace < text.length() && Character.isWhitespace(text.charAt(firstNonSpace))) { - firstNonSpace++; - } - final boolean headBeforeMain = runs.size() == 1 && runs.get(0).open() == firstNonSpace; - return finishText(line, outsideText, insideText, headBeforeMain); + private static Cut finish(LyricsLine line, List sung, List background) { + if (sung.isEmpty() || background.isEmpty()) { + return null; + } + final List trimmed = trimBrackets(sung); + final List trimmedBackground = trimBrackets(background); + final String sungText = textOf(trimmed); + final String backgroundText = textOf(trimmedBackground); + if (sungText.trim().isEmpty() || backgroundText.trim().isEmpty()) { + return null; + } + return new Cut(plain(line, sungText, trimmed, false), + plain(line, backgroundText, trimmedBackground, true)); } @Nullable - private static Cut finishText(LyricsLine line, String sungText, String backgroundText, - boolean headBeforeMain) { - String kept = stripAllBrackets(sungText).trim(); - String bgKept = stripAllBrackets(backgroundText).trim(); - if (kept.isEmpty() || bgKept.isEmpty() || !sung(kept) || !sung(bgKept)) { + private static Cut finishText(LyricsLine line, String sungText, String backgroundText) { + final String kept = sungText.trim(); + if (kept.isEmpty() || !sung(backgroundText)) { return null; } - LyricsLine bgLine = headBeforeMain && line.startTimeMs() > 0 - ? new LyricsLine(line.startTimeMs() - 1L, line.endTimeMs(), - bgKept, List.of(), line.agentId(), - line.isDuet(), true, line.songPart()) - : plain(line, bgKept, List.of(), true); - return new Cut(plain(line, kept, List.of(), false), bgLine); + return new Cut(plain(line, kept, List.of(), false), + plain(line, backgroundText.trim(), List.of(), true)); } private static LyricsLine plain(LyricsLine line, String text, List words, boolean isBG) { @@ -390,195 +192,73 @@ private static LyricsLine plain(LyricsLine line, String text, List words, line.agentId(), line.isDuet(), isBG, line.songPart()); } - private static long firstValidStart(List words, long fallback) { - for (int i = 0; i < words.size(); i++) { - final long s = words.get(i).startMs(); - if (s != LyricsLine.NO_TIME && s >= 0) { - return s; - } - } - return fallback; - } - - private static long lastValidEnd(List words, long fallback) { - for (int i = words.size() - 1; i >= 0; i--) { - final long e = words.get(i).endMs(); - if (e != LyricsLine.NO_TIME && e > 0) { - return e; - } - } - return fallback; - } - - /** - * Strips leading/trailing round brackets from every word while preserving {@code endsWithSpace()}. - */ + /** Whitespace goes first and last too, or a bracket touching the text is never reached. */ private static List trimBrackets(List words) { - List out = new ArrayList<>(words.size()); + final List out = new ArrayList<>(words.size()); for (int i = 0; i < words.size(); i++) { - Word word = words.get(i); - String body = word.text(); - if (body == null) { - continue; - } - final boolean hadTrailingWhitespace = !body.isEmpty() - && Character.isWhitespace(body.charAt(body.length() - 1)); - body = TextTrim.stripLeading(body); - while (!body.isEmpty() && isOpen(body.charAt(0))) { - body = TextTrim.stripLeading(body.substring(1)); + String body = words.get(i).text(); + if (i == 0) { + body = TextTrim.stripLeading(body); + while (isOpen(body.charAt(0))) { + body = TextTrim.stripLeading(body.substring(1)); + } } - body = TextTrim.stripTrailing(body); - while (!body.isEmpty() && isClose(body.charAt(body.length() - 1))) { - body = TextTrim.stripTrailing(body.substring(0, body.length() - 1)); + if (i == words.size() - 1) { + body = TextTrim.stripTrailing(body); + while (!body.isEmpty() && isClose(body.charAt(body.length() - 1))) { + body = TextTrim.stripTrailing(body.substring(0, body.length() - 1)); + } } - // Also strip any stray internal round brackets left in a token. - body = stripAllBrackets(body).trim(); if (body.isEmpty()) { continue; } - String roma = word.romaji(); - if (roma != null && !roma.isEmpty()) { - roma = stripAllBrackets(roma).trim(); - if (roma.isEmpty()) { - roma = null; - } - } - final boolean endsWithSpace = word.endsWithSpace() || hadTrailingWhitespace; - out.add(new Word(word.startMs(), word.endMs(), body, roma, endsWithSpace)); - } - if (!out.isEmpty()) { - final int lastIdx = out.size() - 1; - Word last = out.get(lastIdx); - if (last.endsWithSpace()) { - out.set(lastIdx, new Word(last.startMs(), last.endMs(), - last.text(), last.romaji(), false)); - } + final Word word = words.get(i); + out.add(new Word(word.startMs(), word.endMs(), body, + word.romaji(), body.endsWith(" "))); } return out; } - private static List findSungBracketRuns(String text) { - List runs = new ArrayList<>(2); - int i = 0; - final int len = text.length(); - while (i < len) { - final char c = text.charAt(i); - if (isOpen(c)) { - Run run = match(text, i, len, true); - if (run != null) { - String inner = text.substring(run.open() + 1, run.close()); - if (sung(inner)) { - runs.add(run); - i = run.close() + 1; - continue; - } - } - } - i++; - } - return runs; - } - - private static boolean isIndexInsideRuns(int charIndex, List runs) { - for (int i = 0; i < runs.size(); i++) { - Run r = runs.get(i); - if (charIndex >= r.open() && charIndex <= r.close()) { - return true; - } - } - return false; - } - - private static String extractOutsideText(String text, List runs) { - if (runs.isEmpty()) { - return text.trim(); - } - StringBuilder sb = new StringBuilder(text.length()); - int last = 0; - for (int i = 0; i < runs.size(); i++) { - Run r = runs.get(i); - if (r.open() > last) { - sb.append(text, last, r.open()); - } - last = r.close() + 1; - } - if (last < text.length()) { - sb.append(text, last, text.length()); + /** A run running straight into a word is not a vocal of its own. */ + @Nullable + private static Run headRun(String text) { + int open = 0; + while (open < text.length() && Character.isWhitespace(text.charAt(open))) { + open++; } - return sb.toString() - .replaceAll("\\s+([!?,.:;])", "$1") - .replaceAll("\\s{2,}", " ") - .trim(); - } - - private static String extractInsideText(String text, List runs) { - if (runs.isEmpty()) { - return ""; + if (open >= text.length() || !isOpen(text.charAt(open))) { + return null; } - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < runs.size(); i++) { - Run r = runs.get(i); - String part = stripAllBrackets(text.substring(r.open() + 1, r.close())).trim(); - if (!part.isEmpty()) { - //noinspection SizeReplaceableByIsEmpty - if (sb.length() > 0) { - sb.append(' '); - } - sb.append(part); - } + final Run run = match(text, open, text.length(), true); + if (run == null) { + return null; } - return sb.toString().replaceAll("\\s{2,}", " ").trim(); + return isWide(text.charAt(run.open())) + || run.close() + 1 == text.length() + || Character.isWhitespace(text.charAt(run.close() + 1)) ? run : null; } - /** Lenticular brackets hold a title or credit tag, never a vocal. */ - public static boolean hasTagBrackets(String text) { - return text.indexOf('【') >= 0 || text.indexOf('】') >= 0; - } - - /** A whole line in round brackets, which is how a background vocal is usually written. */ - public static boolean isBracketedVocal(@Nullable String text) { - return text != null && !hasTagBrackets(text) && hasBalancedOuterBrackets(text); - } - - public static boolean hasBalancedOuterBrackets(@Nullable String text) { - if (text == null) { - return false; - } - String trimmed = text.trim(); - if (trimmed.length() < 2) { - return false; - } - final char first = trimmed.charAt(0); - if (!isOpen(first)) { - return false; + @Nullable + private static Run tailRun(String text) { + int close = text.length(); + while (close > 0 && Character.isWhitespace(text.charAt(close - 1))) { + close--; } - Run matched = match(trimmed, 0, trimmed.length(), true); - return matched != null && matched.close() == trimmed.length() - 1; - } - - public static String stripOuterBrackets(@Nullable String text) { - if (text == null || text.isEmpty()) { - return ""; + if (close == 0 || !isClose(text.charAt(close - 1))) { + return null; } - String trimmed = text.trim(); - while (trimmed.length() >= 2 && hasBalancedOuterBrackets(trimmed)) { - trimmed = trimmed.substring(1, trimmed.length() - 1).trim(); + final Run run = match(text, close - 1, 0, false); + if (run == null) { + return null; } - return trimmed; + return isWide(text.charAt(run.open())) + || run.open() == 0 + || Character.isWhitespace(text.charAt(run.open() - 1)) ? run : null; } - public static String stripAllBrackets(@Nullable String text) { - if (text == null || text.isEmpty()) { - return ""; - } - StringBuilder sb = new StringBuilder(text.length()); - for (int i = 0; i < text.length(); i++) { - final char c = text.charAt(i); - if (!isOpen(c) && !isClose(c)) { - sb.append(c); - } - } - return sb.toString(); + /** A full-width bracket stands on a cell of its own, so it needs no space around it. */ + private static boolean isWide(char c) { + return c == '(' || c == ')'; } /** Nesting is counted so an inner bracket does not close the run early; "(oh]" is no run at all. */ @@ -587,9 +267,6 @@ private static Run match(String text, int from, int toward, boolean forward) { final char first = text.charAt(from); final char opener = forward ? first : openerOf(first); final char closer = forward ? closerOf(first) : first; - if (opener == 0 || closer == 0) { - return null; - } // Scanning back starts on the closing bracket, so that one is the deeper side. final char deeper = forward ? opener : closer; final char shallower = forward ? closer : opener; @@ -619,19 +296,25 @@ private static boolean isClose(char c) { } private static char closerOf(char open) { - return switch (open) { - case '(' -> ')'; - case '(' -> ')'; - default -> 0; - }; + switch (open) { + case '(': + return ')'; + case '(': + return ')'; + default: + return 0; + } } private static char openerOf(char close) { - return switch (close) { - case ')' -> '('; - case ')' -> '('; - default -> 0; - }; + switch (close) { + case ')': + return '('; + case ')': + return '('; + default: + return 0; + } } /** A repeat count or section name is bracketed too, but it is not sung. */ @@ -639,7 +322,7 @@ private static boolean sung(String text) { if (text == null) { return false; } - String body = text.trim(); + final String body = text.trim(); if (body.isEmpty() || NOT_SUNG_RUN.matcher(body).matches()) { return false; } @@ -651,134 +334,109 @@ private static boolean sung(String text) { return false; } - private static boolean isContinuousCjk(@Nullable String s) { - if (s == null || s.isEmpty()) { - return false; - } - final int cp = s.codePointBefore(s.length()); - Character.UnicodeBlock b = Character.UnicodeBlock.of(cp); - return b == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS - || b == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_A - || b == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS_EXTENSION_B - || b == Character.UnicodeBlock.CJK_COMPATIBILITY_IDEOGRAPHS - || b == Character.UnicodeBlock.CJK_SYMBOLS_AND_PUNCTUATION - || b == Character.UnicodeBlock.HIRAGANA - || b == Character.UnicodeBlock.KATAKANA; - } - private static String textOf(List words) { - StringBuilder text = new StringBuilder(); - for (int i = 0; i < words.size(); i++) { - Word word = words.get(i); - String wText = word.text(); - if (wText == null || wText.isEmpty()) { - continue; - } - text.append(wText); - if (i < words.size() - 1 && !wText.endsWith(" ")) { - String nextText = words.get(i + 1).text(); - final boolean cjkPair = isContinuousCjk(wText) && isContinuousCjk(nextText); - if (word.endsWithSpace() || !cjkPair) { - text.append(' '); - } - } + final StringBuilder text = new StringBuilder(); + for (Word word : words) { + text.append(word.text()); } - return text.toString().trim(); + return text.toString(); } - @Nullable - private static String synthesizeRomajiFromWords(List words) { - if (words == null || words.isEmpty()) { - return null; - } - StringBuilder sb = new StringBuilder(); - boolean any = false; - for (int i = 0; i < words.size(); i++) { - Word w = words.get(i); - String r = w.romaji(); - if (r != null && !r.trim().isEmpty()) { - any = true; - sb.append(r.trim()); - if (i < words.size() - 1 && !r.endsWith(" ")) { - sb.append(' '); - } - } + private static int lengthOf(List words) { + int total = 0; + for (Word word : words) { + total += word.text().length(); } - return any ? sb.toString().trim() : null; + return total; } - /** A track written against other lines stays as it is: shorter than the lyrics is the mismatch - * the readers already check for before trusting one. */ + /** + * A track written against other lines stays as it is: shorter than the lyrics is the mismatch + * the readers already check for before trusting one. + */ @Nullable - private static List widen( - @Nullable List aligned, - int count, - int[] room, - Cut[] cuts, - LyricsLine[] normalizedBg, - boolean isRomanization) { + private static List widen(@Nullable List aligned, int count, + int[] room) { if (aligned == null || aligned.isEmpty() || aligned.size() != count) { return aligned; } - List out = new ArrayList<>(count + 8); + final List out = new ArrayList<>(count); for (int i = 0; i < count; i++) { - LyricsLine item = aligned.get(i); - Cut mainCut = cuts[i]; - if (room[i] == 2 && mainCut != null) { - String itemText = item != null && item.text() != null ? item.text() : ""; - List itemRuns = findSungBracketRuns(itemText); - if (!itemRuns.isEmpty()) { - String outPart = extractOutsideText(itemText, itemRuns); - String inPart = extractInsideText(itemText, itemRuns); - if (!outPart.isEmpty() && !inPart.isEmpty()) { - out.add(new LyricsLine(item.startTimeMs(), outPart)); - out.add(new LyricsLine(item.startTimeMs(), inPart)); - continue; - } - } - if (isRomanization) { - String sungRoma = synthesizeRomajiFromWords(mainCut.sung().words()); - String bgRoma = synthesizeRomajiFromWords(mainCut.background().words()); - if (sungRoma != null || bgRoma != null) { - out.add(new LyricsLine(LyricsLine.NO_TIME, - sungRoma != null ? sungRoma : stripAllBrackets(itemText).trim())); - out.add(new LyricsLine(LyricsLine.NO_TIME, - bgRoma != null ? bgRoma : "")); - continue; - } - } - out.add(item); + out.add(aligned.get(i)); + for (int slot = 1; slot < room[i]; slot++) { out.add(BLANK); - } else if (normalizedBg[i] != null && item != null && item.text() != null) { - String cleaned = stripOuterBrackets(item.text()); - out.add(cleaned.equals(item.text()) - ? item - : new LyricsLine(item.startTimeMs(), item.endTimeMs(), cleaned, - item.words() != null ? trimBrackets(item.words()) : List.of(), - item.agentId(), item.isDuet(), true, item.songPart())); - } else { - out.add(item); } } return out; } @Nullable - private static Map> widenMap( - @Nullable Map> tracks, - int count, - int[] room, - Cut[] cuts, - LyricsLine[] normalizedBg, - boolean isRomanization) { + private static Map> widen(@Nullable Map> tracks, + int count, int[] room) { if (tracks == null || tracks.isEmpty()) { return tracks; } - Map> out = new HashMap<>(2 * tracks.size()); + final Map> out = new HashMap<>(2 * tracks.size()); for (Map.Entry> entry : tracks.entrySet()) { - out.put(entry.getKey(), - widen(entry.getValue(), count, room, cuts, normalizedBg, isRomanization)); + out.put(entry.getKey(), widen(entry.getValue(), count, room)); } return out; } + + /** Lenticular brackets hold a title or credit tag, never a vocal. */ + public static boolean hasTagBrackets(String text) { + return text.indexOf('【') >= 0 || text.indexOf('】') >= 0; + } + + /** A whole line in round brackets, which is how a background vocal is usually written. */ + public static boolean isBracketedVocal(@Nullable String text) { + return text != null && !hasTagBrackets(text) && hasBalancedOuterBrackets(text); + } + + public static boolean hasBalancedOuterBrackets(@Nullable String text) { + if (text == null) { + return false; + } + final String trimmed = text.trim(); + if (trimmed.length() < 2) { + return false; + } + final char first = trimmed.charAt(0); + if (!isOpen(first)) { + return false; + } + final Run matched = match(trimmed, 0, trimmed.length(), true); + return matched != null && matched.close() == trimmed.length() - 1; + } + + public static String stripOuterBrackets(@Nullable String text) { + if (text == null || text.isEmpty()) { + return ""; + } + String trimmed = text.trim(); + while (trimmed.length() >= 2 && hasBalancedOuterBrackets(trimmed)) { + trimmed = trimmed.substring(1, trimmed.length() - 1).trim(); + } + return trimmed; + } + + public static String stripAllBrackets(@Nullable String text) { + if (text == null || text.isEmpty()) { + return ""; + } + final StringBuilder sb = new StringBuilder(text.length()); + for (int i = 0; i < text.length(); i++) { + final char c = text.charAt(i); + if (!isOpen(c) && !isClose(c)) { + sb.append(c); + } + } + return sb.toString(); + } + + private record Run(int open, int close) { + } + + private record Cut(LyricsLine sung, LyricsLine background) { + } } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/LyricsFilter.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/LyricsFilter.java index d36dd3806d..dcc22ce2ee 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/LyricsFilter.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/LyricsFilter.java @@ -21,30 +21,23 @@ import app.morphe.extension.music.patches.lyrics.requests.LyricsRequests; /** - * Drops the lines a lyrics lookup should not show: the credit block a provider pastes at the - * end, and whatever {@link LyricsFilterDefaults#LYRICS_TEXT_FILTER} names. - * - *

    Every method is static and holds no state beyond the variant cache, so filtering the - * same lyrics twice costs nothing beyond re-running the rules. Metadata itself is cleaned - * separately by {@link MetadataCleaner}. + * Drops provider credit blocks and lines matching {@link LyricsFilterDefaults#LYRICS_TEXT_FILTER}. + * All methods are static and stateless beyond a variant cache; {@link MetadataCleaner} handles metadata. */ public final class LyricsFilter { - private LyricsFilter() { - } - private static final int MAX_ARTIST_SONG_LINE_LENGTH = 80; - private static final Pattern ARTIST_SONG_PATTERN = Pattern.compile("^[^\\-]+\\s*-\\s*[^\\-]+$"); - private static final int CREDIT_LINE_GAP_TOLERANCE = 3; - @Nullable private static volatile String cachedCreditMarkers; @Nullable private static volatile List cachedCreditVariants; + private LyricsFilter() { + } + public static Lyrics filterCreditLines(Lyrics lyrics, TrackInfo track) { if (lyrics == null || lyrics == Lyrics.NOT_FOUND || lyrics.isEmpty()) { return lyrics; @@ -402,10 +395,9 @@ private static boolean containsAnyVariant(List haystacks, List n /** * Applies {@link LyricsFilterDefaults#LYRICS_TEXT_FILTER} to the original lyrics text only. - * Translations and romanizations are not run through the filter, but when the filter - * drops a line they drop with it so a translation stays under the line it belongs to. - * A line the filter does not fire on keeps its provider text rather than a normalized - * copy, so fullwidth punctuation is not folded to ASCII. + * Translations and romanizations are not filtered themselves, but drop with a filtered line so + * a translation stays under the line it belongs to. Unfiltered lines keep their provider text, + * so fullwidth punctuation is not folded to ASCII. */ public static Lyrics filterLyricsText(Lyrics lyrics) { if (lyrics == null || lyrics == Lyrics.NOT_FOUND) { diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/LyricsFilterDefaults.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/LyricsFilterDefaults.java index f2fab8a03c..3726b93209 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/LyricsFilterDefaults.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/LyricsFilterDefaults.java @@ -13,13 +13,9 @@ import app.morphe.extension.shared.settings.StringSetting; /** - * The user-configurable rules lyrics are cleaned and filtered with, together with the values - * they start out at. - * - *

    Kept out of {@link Settings} so the rules and the code applying them - * ({@link LyricsFilter}) read as one unit. {@link Settings} calls {@link #register()} from a - * static block: touching this class is what puts the three settings into the framework's - * registry, which the preference screen looks keys up in. + * User-configurable lyrics cleaning/filtering rules and their defaults. Kept out of + * {@link Settings} so the rules and {@link LyricsFilter} read as one unit; {@link Settings} + * calls {@link #register()} from a static block, which loads this class and registers the settings. */ public final class LyricsFilterDefaults { @@ -139,6 +135,9 @@ public final class LyricsFilterDefaults { new StringSetting("morphe_music_lyrics_credit_line_regex", DEFAULT_LYRICS_CREDIT_LINE_REGEX, false, parent(Settings.LYRICS_ENABLED)); + private LyricsFilterDefaults() { + } + /** * Constructs the three settings above by loading this class. * @@ -148,7 +147,4 @@ public final class LyricsFilterDefaults { */ public static void register() { } - - private LyricsFilterDefaults() { - } } \ No newline at end of file diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/LyricsLine.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/LyricsLine.java index b9a442a0ac..a243c2aca5 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/LyricsLine.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/LyricsLine.java @@ -110,36 +110,16 @@ public LyricsLine(long startTimeMs, String text) { this(startTimeMs, NO_TIME, text, List.of(), null, false, false, null); } - public boolean hasWords() { - return !words.isEmpty(); - } - - /** - * Whether the words say more than the line itself does. - * - *

    A lone word covering the whole line, or one that never ends, is the line's own timing - * written back out as a word. Counting it as word sync would have the line read as karaoke - * while it can only ever be sung in one go, and would rank it above lyrics that really do - * carry word timings. - */ - public boolean hasWordTimings() { - if (words.isEmpty()) { + public static boolean isCenteredAgentId(@Nullable String agentId) { + if (agentId == null) { return false; } - if (words.size() == 1) { - Word single = words.get(0); - if (single.endMs() <= single.startMs()) { - return false; - } - if (hasMultipleWords(text) && compactText(single.text()).equalsIgnoreCase(compactText(text))) { - return false; - } - } - return true; + final String id = agentId.trim(); + return "v1000".equalsIgnoreCase(id) || "v2000".equalsIgnoreCase(id); } private static String compactText(String value) { - StringBuilder builder = new StringBuilder(value.length()); + final StringBuilder builder = new StringBuilder(value.length()); for (int i = 0; i < value.length(); i++) { final char c = value.charAt(i); if (!Character.isWhitespace(c)) { @@ -149,7 +129,6 @@ private static String compactText(String value) { return builder.toString(); } - // ponytail: whitespace word counting; upgrade to BreakIterator if space-less scripts mix dummy spans. private static boolean hasMultipleWords(String value) { boolean inWord = false; int wordCount = 0; @@ -167,6 +146,38 @@ private static boolean hasMultipleWords(String value) { return false; } + public boolean hasWords() { + return !words.isEmpty(); + } + + /** Whether Apple's group/other vocal agent is centered instead of placed on a duet side. */ + public boolean isCentered() { + return isCenteredAgentId(agentId); + } + + /** + * Whether the words say more than the line itself does. + * + *

    A lone word covering the whole line, or one that never ends, is the line's own timing + * written back out as a word. Counting it as word sync would have the line read as karaoke + * while it can only ever be sung in one go, and would rank it above lyrics that really do + * carry word timings. + */ + public boolean hasWordTimings() { + if (words.isEmpty()) { + return false; + } + if (words.size() == 1) { + Word single = words.get(0); + // A span over several words is the line's own timing written back out. + if (hasMultipleWords(text)) { + return false; + } + return single.endMs() > single.startMs(); + } + return true; + } + @NonNull @Override public String toString() { diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/LyricsMerge.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/LyricsMerge.java index d016a495b0..8d8d95f771 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/LyricsMerge.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/LyricsMerge.java @@ -28,11 +28,15 @@ */ public final class LyricsMerge { - /** Only TTML states background lines separately, so only there does the prefix mean anything. */ + /** + * Only TTML states background lines separately, so only there does the prefix mean anything. + */ public static final String BACKGROUND_TRACK_PREFIX = "bg:"; - /** A sung line matches anywhere before the next one starts, which would let a background line - * take the following line's own. */ + /** + * A sung line matches anywhere before the next one starts, which would let a background line + * take the following line's own. + */ private static final long BACKGROUND_MATCH_WINDOW_MS = 300L; private LyricsMerge() { @@ -45,7 +49,7 @@ public static List backgroundTrack( } public static boolean hasBackgroundTrack(Lyrics lyrics, - @Nullable Map> tracks) { + @Nullable Map> tracks) { if ("ttml".equals(lyrics.formatType())) { return true; } @@ -64,7 +68,7 @@ public static String textAt(@Nullable List track, int index) { if (track == null || index < 0 || index >= track.size()) { return ""; } - String text = track.get(index).text(); + final String text = track.get(index).text(); return text == null ? "" : text.trim(); } @@ -107,7 +111,7 @@ public static Map> singleLanguageTranslations( @Nullable public static List mapLinesOnline(List lines, - Function, List> batch) { + Function, List> batch) { if (!Utils.isNetworkConnected()) { return null; } @@ -169,7 +173,7 @@ public static Lyrics addSongwriter(@Nullable Lyrics lyrics, @Nullable String ent } public static List mergeRomanization(List original, - @Nullable List auxiliary) { + @Nullable List auxiliary) { List result = new ArrayList<>(original.size()); if (auxiliary == null || auxiliary.isEmpty()) { for (LyricsLine line : original) { @@ -241,7 +245,9 @@ public static List mergeRomanization(List original, return result; } - /** Lines sharing a start would leave an empty window, matching nothing and stranding an entry. */ + /** + * Lines sharing a start would leave an empty window, matching nothing and stranding an entry. + */ private static long windowEnd(List lines, int index, long winStart) { final long next = (index + 1 < lines.size()) ? lines.get(index + 1).startTimeMs() diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/LyricsPreference.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/LyricsPreference.java index 99ca28c9f4..c9a5ae38e3 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/LyricsPreference.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/LyricsPreference.java @@ -14,20 +14,20 @@ /** * What the user picked for one track. * - * @param queryTitle custom title search terms, or null when the default terms were used - * @param queryArtist custom artist search terms, or null when the default terms were used - * @param preferred the lyric to show first next time - * @param queue fingerprints of the rest, in order - * @param fingerprint fingerprint before the lines were written, which drops the raw text it - * was taken from. Null when it was never computed. - * @param videoId the video the lyric was picked for, or null when it was written without - * one, which is the case for the entries meant to answer any video. + * @param queryTitle custom title search terms, or null when the default terms were used + * @param queryArtist custom artist search terms, or null when the default terms were used + * @param preferred the lyric to show first next time + * @param queue fingerprints of the rest, in order + * @param fingerprint fingerprint before the lines were written, which drops the raw text it + * was taken from. Null when it was never computed. + * @param videoId the video the lyric was picked for, or null when it was written without + * one, which is the case for the entries meant to answer any video. */ public record LyricsPreference(@Nullable String queryTitle, - @Nullable String queryArtist, - Lyrics preferred, - List queue, - @Nullable String fingerprint, - @Nullable String trackKey, - @Nullable String videoId) { + @Nullable String queryArtist, + Lyrics preferred, + List queue, + @Nullable String fingerprint, + @Nullable String trackKey, + @Nullable String videoId) { } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/LyricsTextFormatter.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/LyricsTextFormatter.java index 78c64699b6..d379624512 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/LyricsTextFormatter.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/LyricsTextFormatter.java @@ -11,23 +11,18 @@ /** Final, display-only lyric cleanup, including an old-to-new UTF-16 offset map. */ public final class LyricsTextFormatter { - public record Result(String text, int[] offsets) { - public int offset(int original) { - return original < 0 ? original : offsets[Math.min(original, offsets.length - 1)]; - } + private LyricsTextFormatter() { } - private LyricsTextFormatter() { } - public static Result format(String input, boolean capitalize, boolean strip) { - String text = input == null ? "" : input; - boolean[] remove = new boolean[text.length()]; + final String text = input == null ? "" : input; + final boolean[] remove = new boolean[text.length()]; if (strip) { int lineStart = 0; while (lineStart <= text.length()) { int end = text.indexOf('\n', lineStart); if (end < 0) end = text.length(); - for (int at = end; at > lineStart;) { + for (int at = end; at > lineStart; ) { final int cp = text.codePointBefore(at); final int start = at - Character.charCount(cp); if (!isPunctuation(cp) && !Character.isWhitespace(cp) @@ -41,15 +36,15 @@ public static Result format(String input, boolean capitalize, boolean strip) { lineStart = end + 1; } } - StringBuilder out = new StringBuilder(text.length()); - int[] offsets = new int[text.length() + 1]; + final StringBuilder out = new StringBuilder(text.length()); + final int[] offsets = new int[text.length() + 1]; boolean firstCased = true; - for (int at = 0; at < text.length();) { + for (int at = 0; at < text.length(); ) { final int cp = text.codePointAt(at); final int end = at + Character.charCount(cp); for (int i = at; i < end; i++) offsets[i] = out.length(); if (!remove[at]) { - String own = text.substring(at, end); + final String own = text.substring(at, end); if (capitalize && firstCased && (Character.isLowerCase(cp) || Character.isUpperCase(cp) || Character.isTitleCase(cp))) { out.append(own.toUpperCase(Locale.ROOT)); @@ -68,9 +63,9 @@ public static Result format(String input, boolean capitalize, boolean strip) { private static boolean isPunctuation(int cp) { return switch (Character.getType(cp)) { case Character.CONNECTOR_PUNCTUATION, Character.DASH_PUNCTUATION, - Character.START_PUNCTUATION, Character.END_PUNCTUATION, - Character.INITIAL_QUOTE_PUNCTUATION, Character.FINAL_QUOTE_PUNCTUATION, - Character.OTHER_PUNCTUATION -> true; + Character.START_PUNCTUATION, Character.END_PUNCTUATION, + Character.INITIAL_QUOTE_PUNCTUATION, Character.FINAL_QUOTE_PUNCTUATION, + Character.OTHER_PUNCTUATION -> true; default -> false; }; } @@ -79,7 +74,7 @@ private static boolean isPunctuation(int cp) { private static boolean isRedundant(int cp) { if (Character.getType(cp) != Character.OTHER_PUNCTUATION || Character.isMirrored(cp) || cp == '\'' || cp == '"') return false; - String name = Character.getName(cp); + final String name = Character.getName(cp); if (name == null || name.contains("QUESTION") || name.contains("EXCLAMATION") || name.contains("INTERROBANG") || name.contains("ELLIPSIS") || name.contains("QUOTATION") || name.contains("APOSTROPHE")) return false; @@ -89,9 +84,15 @@ private static boolean isRedundant(int cp) { } private static boolean isEllipsisDot(String text, int start, int end, int cp) { - String name = Character.getName(cp); + final String name = Character.getName(cp); if (cp != 0x2024 && (name == null || !name.contains("FULL STOP"))) return false; return (start > 0 && text.codePointBefore(start) == cp) || (end < text.length() && text.codePointAt(end) == cp); } + + public record Result(String text, int[] offsets) { + public int offset(int original) { + return original < 0 ? original : offsets[Math.min(original, offsets.length - 1)]; + } + } } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/MetadataCleaner.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/MetadataCleaner.java index 222b98bbb1..182ad5fcb4 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/MetadataCleaner.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/MetadataCleaner.java @@ -49,6 +49,10 @@ public final class MetadataCleaner { private static final ConcurrentMap pendingResolves = new ConcurrentHashMap<>(); private static final ExecutorService resolveExecutor = Executors.newFixedThreadPool(1); private static final ConcurrentMap compiledPatterns = new ConcurrentHashMap<>(); + private static final String DASH_SEPARATOR = " - "; + private static final String[] ARTIST_SEPARATORS = + {" & ", ", ", " x ", " X ", " feat. ", " feat ", " ft. ", " ft ", " с ", " 和 ", "/", "×"}; + private static final Pattern WHITESPACE = Pattern.compile("\\s+"); private MetadataCleaner() { } @@ -91,13 +95,6 @@ public static void resolveSettingBlocking(@Nullable String value) { } } - /** - * Splits a setting value into the value a caller returns as is and whether it is a remote - * URL that still has to be resolved; {@code local} holds the resolved answer in that case. - */ - private record SettingLookup(String trimmed, boolean remote, @Nullable String local) { - } - private static SettingLookup classifySetting(@Nullable String value) { if (value == null || value.trim().isEmpty()) { return new SettingLookup("", false, value == null ? "" : value); @@ -147,7 +144,7 @@ static String applyRegex(String input, String regex) { if (regex == null || regex.trim().isEmpty()) { return input; } - Pattern pattern = compiled(regex); + final Pattern pattern = compiled(regex); if (pattern == null) { return input; } @@ -160,22 +157,20 @@ static String applyRegex(String input, String regex) { } /** - * Like {@link #applyRegex}, but a pattern that matches nothing returns the input - * untouched instead of its normalized form. Lyrics line text passes through this - * before display, so rebuilding an untouched line from the normalizer output would - * fold its fullwidth punctuation ({@code ,!?~()} …) to ASCII even though the - * filter never fired on it. + * Like {@link #applyRegex}, but returns the input untouched when the pattern matches nothing. + * Used on lyrics lines before display: normalizing an untouched line would fold its + * fullwidth punctuation ({@code ,!?~()} …) to ASCII even when the filter never fired. */ public static String applyRegexPreserveOriginal(String input, String regex) { if (regex == null || regex.trim().isEmpty()) { return input; } - Pattern pattern = compiled(regex); + final Pattern pattern = compiled(regex); if (pattern == null) { return input; } try { - String normalized = CharactersConverter.normalizePreserveCase(input); + final String normalized = CharactersConverter.normalizePreserveCase(input); if (!pattern.matcher(normalized).find()) { return input; } @@ -204,8 +199,6 @@ private static Pattern compiled(String regex) { } } - private static final String DASH_SEPARATOR = " - "; - @Nullable private static String[] splitDashRaw(@Nullable String rawTitle) { if (rawTitle == null) { @@ -220,16 +213,16 @@ private static String[] splitDashRaw(@Nullable String rawTitle) { if (left.isEmpty() || right.isEmpty()) { return null; } - return new String[]{ left, right }; + return new String[]{left, right}; } public static String[] parseCleanTitleAndArtist(@Nullable String rawTitle, @Nullable String rawArtist) { - return new String[]{ cleanArtist(rawArtist), cleanTitle(rawTitle) }; + return new String[]{cleanArtist(rawArtist), cleanTitle(rawTitle)}; } @Nullable public static TrackInfo trustedDashSplit(@Nullable String rawTitle, @Nullable String rawArtist, - @Nullable String album, int durationSeconds) { + @Nullable String album, int durationSeconds) { String[] sides = splitDashRaw(rawTitle); if (sides == null || rawArtist == null || rawArtist.trim().isEmpty()) { return null; @@ -247,7 +240,7 @@ public static TrackInfo trustedDashSplit(@Nullable String rawTitle, @Nullable St @Nullable public static TrackInfo anyDashSplit(@Nullable String rawTitle, @Nullable String album, - int durationSeconds) { + int durationSeconds) { String[] sides = splitDashRaw(rawTitle); if (sides == null) { return null; @@ -257,7 +250,7 @@ public static TrackInfo anyDashSplit(@Nullable String rawTitle, @Nullable String @Nullable public static TrackInfo anyDashSplitReversed(@Nullable String rawTitle, @Nullable String album, - int durationSeconds) { + int durationSeconds) { String[] sides = splitDashRaw(rawTitle); if (sides == null) { return null; @@ -280,9 +273,6 @@ private static boolean approxArtistEqual(String side, String rawArtist) { LyricsRequests.normalizeForMatch(rawArtist)); } - private static final String[] ARTIST_SEPARATORS = - {" & ", ", ", " x ", " X ", " feat. ", " feat ", " ft. ", " ft ", " с ", " 和 ", "/", "×"}; - private static int indexOfFirstSeparator(String artist) { int result = -1; for (String separator : ARTIST_SEPARATORS) { @@ -306,8 +296,6 @@ public static String[] splitArtists(String artist) { return parts.toArray(new String[0]); } - private static final Pattern WHITESPACE = Pattern.compile("\\s+"); - @NonNull private static String download(String urlStr) throws Exception { HttpURLConnection conn = (HttpURLConnection) new URL(urlStr).openConnection(); @@ -353,6 +341,13 @@ private static StringBuilder readBody(HttpURLConnection conn, String charset) th return sb; } + /** + * Splits a setting value into the value a caller returns as is and whether it is a remote + * URL that still has to be resolved; {@code local} holds the resolved answer in that case. + */ + private record SettingLookup(String trimmed, boolean remote, @Nullable String local) { + } + private static final class ResolveTask { final String url; final CountDownLatch latch = new CountDownLatch(1); diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/MiniLyricsFormatter.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/MiniLyricsFormatter.java index 432b3399ab..1e65ca419a 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/MiniLyricsFormatter.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/MiniLyricsFormatter.java @@ -67,24 +67,56 @@ public final class MiniLyricsFormatter { public static final String ARTIST_FIRST_TRACK_FORMAT = "{a} - {t} :({v}):"; public static final String DEFAULT_ENCAPSULATION = "{lrc} :(x{n}):"; public static final String DEFAULT_INSTRUMENTAL = "♪\n♪♪\n♪♪♪"; + /** Marks where a line without spaces was split. */ + public static final String SPLIT_ELLIPSIS = "…"; private static final String LYRIC_PLACEHOLDER = "{lrc}"; private static final String VARIANT_PLACEHOLDER = "{v}"; /** Placeholder names, written inside braces in a template. */ private static final String[] PLACEHOLDERS = {"lrc", "t", "a", "v", "n"}; - /** Marks where a line without spaces was split. */ - public static final String SPLIT_ELLIPSIS = "…"; /** How many times a track format line is halved at most. */ private static final int MAX_FRAME_SPLIT_DEPTH = 6; /** An extended grapheme cluster, so a character with combining marks or an emoji is not split. */ private static final Pattern GRAPHEME = Pattern.compile("\\X"); private static final Pattern LINE_BREAK = Pattern.compile("\\R"); private static final Pattern REPEATED_SPACES = Pattern.compile("[ \\t\\u00A0]{2,}"); - - public record Chunk(String text, long startMs) { - } - - private record Unit(int start, int end, long timeMs) { - } + /** + * Unicode 17 EastAsianWidth.txt W/F properties, merged into inclusive ranges. + * https://www.unicode.org/Public/17.0.0/ucd/EastAsianWidth.txt + * Distributed under Unicode License V3 (see the file header). + */ + private static final int[] FULL_WIDTH_RANGES = { + 0x1100, 0x115F, 0x231A, 0x231B, 0x2329, 0x232A, 0x23E9, 0x23EC, + 0x23F0, 0x23F0, 0x23F3, 0x23F3, 0x25FD, 0x25FE, 0x2614, 0x2615, + 0x2630, 0x2637, 0x2648, 0x2653, 0x267F, 0x267F, 0x268A, 0x268F, + 0x2693, 0x2693, 0x26A1, 0x26A1, 0x26AA, 0x26AB, 0x26BD, 0x26BE, + 0x26C4, 0x26C5, 0x26CE, 0x26CE, 0x26D4, 0x26D4, 0x26EA, 0x26EA, + 0x26F2, 0x26F3, 0x26F5, 0x26F5, 0x26FA, 0x26FA, 0x26FD, 0x26FD, + 0x2705, 0x2705, 0x270A, 0x270B, 0x2728, 0x2728, 0x274C, 0x274C, + 0x274E, 0x274E, 0x2753, 0x2755, 0x2757, 0x2757, 0x2795, 0x2797, + 0x27B0, 0x27B0, 0x27BF, 0x27BF, 0x2B1B, 0x2B1C, 0x2B50, 0x2B50, + 0x2B55, 0x2B55, 0x2E80, 0x2E99, 0x2E9B, 0x2EF3, 0x2F00, 0x2FD5, + 0x2FF0, 0x303E, 0x3041, 0x3096, 0x3099, 0x30FF, 0x3105, 0x312F, + 0x3131, 0x318E, 0x3190, 0x31E5, 0x31EF, 0x321E, 0x3220, 0x3247, + 0x3250, 0xA48C, 0xA490, 0xA4C6, 0xA960, 0xA97C, 0xAC00, 0xD7A3, + 0xF900, 0xFAFF, 0xFE10, 0xFE19, 0xFE30, 0xFE52, 0xFE54, 0xFE66, + 0xFE68, 0xFE6B, 0xFF01, 0xFF60, 0xFFE0, 0xFFE6, 0x16FE0, 0x16FE4, + 0x16FF0, 0x16FF6, 0x17000, 0x18CD5, 0x18CFF, 0x18D1E, 0x18D80, 0x18DF2, + 0x1AFF0, 0x1AFF3, 0x1AFF5, 0x1AFFB, 0x1AFFD, 0x1AFFE, 0x1B000, 0x1B122, + 0x1B132, 0x1B132, 0x1B150, 0x1B152, 0x1B155, 0x1B155, 0x1B164, 0x1B167, + 0x1B170, 0x1B2FB, 0x1D300, 0x1D356, 0x1D360, 0x1D376, 0x1F004, 0x1F004, + 0x1F0CF, 0x1F0CF, 0x1F18E, 0x1F18E, 0x1F191, 0x1F19A, 0x1F200, 0x1F202, + 0x1F210, 0x1F23B, 0x1F240, 0x1F248, 0x1F250, 0x1F251, 0x1F260, 0x1F265, + 0x1F300, 0x1F320, 0x1F32D, 0x1F335, 0x1F337, 0x1F37C, 0x1F37E, 0x1F393, + 0x1F3A0, 0x1F3CA, 0x1F3CF, 0x1F3D3, 0x1F3E0, 0x1F3F0, 0x1F3F4, 0x1F3F4, + 0x1F3F8, 0x1F43E, 0x1F440, 0x1F440, 0x1F442, 0x1F4FC, 0x1F4FF, 0x1F53D, + 0x1F54B, 0x1F54E, 0x1F550, 0x1F567, 0x1F57A, 0x1F57A, 0x1F595, 0x1F596, + 0x1F5A4, 0x1F5A4, 0x1F5FB, 0x1F64F, 0x1F680, 0x1F6C5, 0x1F6CC, 0x1F6CC, + 0x1F6D0, 0x1F6D2, 0x1F6D5, 0x1F6D8, 0x1F6DC, 0x1F6DF, 0x1F6EB, 0x1F6EC, + 0x1F6F4, 0x1F6FC, 0x1F7E0, 0x1F7EB, 0x1F7F0, 0x1F7F0, 0x1F90C, 0x1F93A, + 0x1F93C, 0x1F945, 0x1F947, 0x1F9FF, 0x1FA70, 0x1FA7C, 0x1FA80, 0x1FA8A, + 0x1FA8E, 0x1FAC6, 0x1FAC8, 0x1FAC8, 0x1FACD, 0x1FADC, 0x1FADF, 0x1FAEA, + 0x1FAEF, 0x1FAF8, 0x20000, 0x2FFFD, 0x30000, 0x3FFFD, + }; private MiniLyricsFormatter() { } @@ -110,15 +142,15 @@ public static String trackLabel(@Nullable String template, @Nullable String titl */ public static String trackLabel(@Nullable String template, @Nullable String title, @Nullable String artist, @Nullable String variant) { - Map values = new HashMap<>(); + final Map values = new HashMap<>(); values.put("t", title == null ? "" : title); values.put("a", artist == null ? "" : artist); values.put("v", variant == null ? "" : variant.trim()); - String rendered = render(template == null ? DEFAULT_TRACK_FORMAT : template, values, true); - StringBuilder out = new StringBuilder(); + final String rendered = render(template == null ? DEFAULT_TRACK_FORMAT : template, values, true); + final StringBuilder out = new StringBuilder(); for (String line : LINE_BREAK.split(rendered, -1)) { // A hidden section leaves the spaces around it behind. - String tidy = REPEATED_SPACES.matcher(line).replaceAll(" ").trim(); + final String tidy = REPEATED_SPACES.matcher(line).replaceAll(" ").trim(); if (tidy.isEmpty()) continue; if (out.length() > 0) out.append('\n'); out.append(tidy); @@ -134,8 +166,8 @@ public static String trackLabel(@Nullable String template, @Nullable String titl public static List trackFrames(@Nullable String template, @Nullable String title, @Nullable String artist, @Nullable String variant, int limit, double fullWidthMultiplier) { - List frames = new ArrayList<>(); - String label = trackLabel(template, title, artist, variant); + final List frames = new ArrayList<>(); + final String label = trackLabel(template, title, artist, variant); for (String line : label.split("\n")) { if (line.isEmpty()) continue; if (limit <= 0 || width(line, fullWidthMultiplier) <= limit) { @@ -155,17 +187,17 @@ private static void splitFrame(String frame, int limit, double fullWidthMultipli frames.add(frame); return; } - List halves = chunks(new LyricsLine(0, frame), -1, limit, false, false, fullWidthMultiplier); + final List halves = chunks(new LyricsLine(0, frame), -1, limit, false, false, fullWidthMultiplier); if (halves.size() < 2) { frames.add(frame); return; } for (Chunk half : halves) { - String text = half.text(); + final String text = half.text(); if (text.isEmpty()) continue; // An ellipsis added at a split does not count against the limit, or a two-character // part would never fit a limit of two. - String bare = text.endsWith(SPLIT_ELLIPSIS) + final String bare = text.endsWith(SPLIT_ELLIPSIS) ? text.substring(0, text.length() - SPLIT_ELLIPSIS.length()) : text; if (bare.equals(frame) || width(bare, fullWidthMultiplier) <= limit) { frames.add(text); @@ -175,8 +207,9 @@ private static void splitFrame(String frame, int limit, double fullWidthMultipli splitFrame(bare, limit, fullWidthMultiplier, frames, depth + 1); if (!bare.equals(text) && frames.size() > before) { final int last = frames.size() - 1; - String lastFrame = frames.get(last); - if (!lastFrame.endsWith(SPLIT_ELLIPSIS)) frames.set(last, lastFrame + SPLIT_ELLIPSIS); + final String lastFrame = frames.get(last); + if (!lastFrame.endsWith(SPLIT_ELLIPSIS)) + frames.set(last, lastFrame + SPLIT_ELLIPSIS); } } } @@ -187,7 +220,7 @@ private static void splitFrame(String frame, int limit, double fullWidthMultipli */ private static String replacePlaceholders(String template, @Nullable String title, @Nullable String artist, @Nullable String lyric) { - StringBuilder out = new StringBuilder(); + final StringBuilder out = new StringBuilder(); for (int i = 0, length = template.length(); i < length; ) { if (template.startsWith("{t}", i)) { out.append(title == null ? "" : title); @@ -210,8 +243,8 @@ private static String replacePlaceholders(String template, @Nullable String titl * the count is above 1; a backslash before a colon keeps it literal. */ public static String encapsulate(@Nullable String template, String lyric, int repeat) { - String source = validEncapsulation(template) ? template : DEFAULT_ENCAPSULATION; - Map values = new HashMap<>(); + final String source = validEncapsulation(template) ? template : DEFAULT_ENCAPSULATION; + final Map values = new HashMap<>(); values.put("lrc", lyric); values.put("n", String.valueOf(repeat)); return render(source, values, repeat > 1); @@ -224,8 +257,8 @@ public static String encapsulate(@Nullable String template, String lyric, int re * {@code bareShown} is set. A backslash before a colon keeps it literal. */ static String render(String template, Map values, boolean bareShown) { - StringBuilder out = new StringBuilder(); - StringBuilder segment = new StringBuilder(); + final StringBuilder out = new StringBuilder(); + final StringBuilder segment = new StringBuilder(); boolean inside = false; boolean populated = true; boolean placeholders = false; @@ -245,9 +278,9 @@ static String render(String template, Map values, boolean bareSh i++; continue; } - String name = c == '{' ? placeholderAt(template, i, values) : null; + final String name = c == '{' ? placeholderAt(template, i, values) : null; if (name != null) { - String value = values.get(name); + final String value = values.get(name); if (inside) { placeholders = true; if (!isPopulated(name, value)) populated = false; @@ -266,7 +299,8 @@ static String render(String template, Map values, boolean bareSh @Nullable private static String placeholderAt(String template, int index, Map values) { for (String name : PLACEHOLDERS) { - if (values.containsKey(name) && template.startsWith("{" + name + "}", index)) return name; + if (values.containsKey(name) && template.startsWith("{" + name + "}", index)) + return name; } return null; } @@ -278,7 +312,7 @@ private static boolean isPopulated(String name, @Nullable String value) { /** How many times in a row the line at the index is sung, counting up to and including it. */ public static int repeatCount(List lines, int index) { - String text = lines.get(index).text(); + final String text = lines.get(index).text(); int count = 1; for (int i = index - 1; i >= 0 && lines.get(i).text().equals(text); i--) { count++; @@ -293,50 +327,12 @@ public static int repeatCount(List lines, int index) { public static double width(String text, double fullWidthMultiplier) { final double wide = Double.isFinite(fullWidthMultiplier) && fullWidthMultiplier > 0 ? fullWidthMultiplier : 1; - Matcher matcher = GRAPHEME.matcher(text); + final Matcher matcher = GRAPHEME.matcher(text); double total = 0; while (matcher.find()) total += isFullWidth(text.codePointAt(matcher.start())) ? wide : 1; return total; } - /** Unicode 17 EastAsianWidth.txt W/F properties, merged into inclusive ranges. - * https://www.unicode.org/Public/17.0.0/ucd/EastAsianWidth.txt - * Distributed under Unicode License V3 (see the file header). - */ - private static final int[] FULL_WIDTH_RANGES = { - 0x1100, 0x115F, 0x231A, 0x231B, 0x2329, 0x232A, 0x23E9, 0x23EC, - 0x23F0, 0x23F0, 0x23F3, 0x23F3, 0x25FD, 0x25FE, 0x2614, 0x2615, - 0x2630, 0x2637, 0x2648, 0x2653, 0x267F, 0x267F, 0x268A, 0x268F, - 0x2693, 0x2693, 0x26A1, 0x26A1, 0x26AA, 0x26AB, 0x26BD, 0x26BE, - 0x26C4, 0x26C5, 0x26CE, 0x26CE, 0x26D4, 0x26D4, 0x26EA, 0x26EA, - 0x26F2, 0x26F3, 0x26F5, 0x26F5, 0x26FA, 0x26FA, 0x26FD, 0x26FD, - 0x2705, 0x2705, 0x270A, 0x270B, 0x2728, 0x2728, 0x274C, 0x274C, - 0x274E, 0x274E, 0x2753, 0x2755, 0x2757, 0x2757, 0x2795, 0x2797, - 0x27B0, 0x27B0, 0x27BF, 0x27BF, 0x2B1B, 0x2B1C, 0x2B50, 0x2B50, - 0x2B55, 0x2B55, 0x2E80, 0x2E99, 0x2E9B, 0x2EF3, 0x2F00, 0x2FD5, - 0x2FF0, 0x303E, 0x3041, 0x3096, 0x3099, 0x30FF, 0x3105, 0x312F, - 0x3131, 0x318E, 0x3190, 0x31E5, 0x31EF, 0x321E, 0x3220, 0x3247, - 0x3250, 0xA48C, 0xA490, 0xA4C6, 0xA960, 0xA97C, 0xAC00, 0xD7A3, - 0xF900, 0xFAFF, 0xFE10, 0xFE19, 0xFE30, 0xFE52, 0xFE54, 0xFE66, - 0xFE68, 0xFE6B, 0xFF01, 0xFF60, 0xFFE0, 0xFFE6, 0x16FE0, 0x16FE4, - 0x16FF0, 0x16FF6, 0x17000, 0x18CD5, 0x18CFF, 0x18D1E, 0x18D80, 0x18DF2, - 0x1AFF0, 0x1AFF3, 0x1AFF5, 0x1AFFB, 0x1AFFD, 0x1AFFE, 0x1B000, 0x1B122, - 0x1B132, 0x1B132, 0x1B150, 0x1B152, 0x1B155, 0x1B155, 0x1B164, 0x1B167, - 0x1B170, 0x1B2FB, 0x1D300, 0x1D356, 0x1D360, 0x1D376, 0x1F004, 0x1F004, - 0x1F0CF, 0x1F0CF, 0x1F18E, 0x1F18E, 0x1F191, 0x1F19A, 0x1F200, 0x1F202, - 0x1F210, 0x1F23B, 0x1F240, 0x1F248, 0x1F250, 0x1F251, 0x1F260, 0x1F265, - 0x1F300, 0x1F320, 0x1F32D, 0x1F335, 0x1F337, 0x1F37C, 0x1F37E, 0x1F393, - 0x1F3A0, 0x1F3CA, 0x1F3CF, 0x1F3D3, 0x1F3E0, 0x1F3F0, 0x1F3F4, 0x1F3F4, - 0x1F3F8, 0x1F43E, 0x1F440, 0x1F440, 0x1F442, 0x1F4FC, 0x1F4FF, 0x1F53D, - 0x1F54B, 0x1F54E, 0x1F550, 0x1F567, 0x1F57A, 0x1F57A, 0x1F595, 0x1F596, - 0x1F5A4, 0x1F5A4, 0x1F5FB, 0x1F64F, 0x1F680, 0x1F6C5, 0x1F6CC, 0x1F6CC, - 0x1F6D0, 0x1F6D2, 0x1F6D5, 0x1F6D8, 0x1F6DC, 0x1F6DF, 0x1F6EB, 0x1F6EC, - 0x1F6F4, 0x1F6FC, 0x1F7E0, 0x1F7EB, 0x1F7F0, 0x1F7F0, 0x1F90C, 0x1F93A, - 0x1F93C, 0x1F945, 0x1F947, 0x1F9FF, 0x1FA70, 0x1FA7C, 0x1FA80, 0x1FA8A, - 0x1FA8E, 0x1FAC6, 0x1FAC8, 0x1FAC8, 0x1FACD, 0x1FADC, 0x1FADF, 0x1FAEA, - 0x1FAEF, 0x1FAF8, 0x20000, 0x2FFFD, 0x30000, 0x3FFFD, - }; - static boolean isFullWidth(int cp) { int lo = 0, hi = FULL_WIDTH_RANGES.length / 2 - 1; while (lo <= hi) { @@ -349,7 +345,7 @@ static boolean isFullWidth(int cp) { } public static int characterCount(String text) { - Matcher matcher = GRAPHEME.matcher(text); + final Matcher matcher = GRAPHEME.matcher(text); int count = 0; while (matcher.find()) count++; return count; @@ -383,15 +379,15 @@ public static List chunks(LyricsLine line, long intervalEndMs, int limit, */ public static List chunks(LyricsLine line, long intervalEndMs, int limit, boolean capitalize, boolean strip, double fullWidthMultiplier) { - LyricsTextFormatter.Result cleaned = LyricsTextFormatter.format(line.text(), false, strip); - String text = cleaned.text(); + final LyricsTextFormatter.Result cleaned = LyricsTextFormatter.format(line.text(), false, strip); + final String text = cleaned.text(); if (limit <= 0 || width(text, fullWidthMultiplier) <= limit) { return List.of(new Chunk(LyricsTextFormatter.format(text, capitalize, false).text(), line.startTimeMs())); } - List timed = timedUnits(line, cleaned); - List units = timed.isEmpty() ? untimedUnits(text) : timed; - List groups = new ArrayList<>(2); + final List timed = timedUnits(line, cleaned); + final List units = timed.isEmpty() ? untimedUnits(text) : timed; + final List groups = new ArrayList<>(2); final int middle = (units.size() + 1) / 2; if (middle > 0) { groups.add(new Unit(units.get(0).start(), units.get(middle - 1).end(), units.get(0).timeMs())); @@ -400,26 +396,36 @@ public static List chunks(LyricsLine line, long intervalEndMs, int limit, groups.add(new Unit(units.get(middle).start(), units.get(units.size() - 1).end(), units.get(middle).timeMs())); } - List result = new ArrayList<>(groups.size()); + final List result = new ArrayList<>(groups.size()); final int total = Math.max(1, characterCount(text)); long previousTime = line.startTimeMs(); + final PairedPunctuation.Pair[] crossed = new PairedPunctuation.Pair[groups.size()]; + for (int i = 0; i + 1 < groups.size(); i++) { + crossed[i] = PairedPunctuation.crossedAt(text, groups.get(i).end()); + } for (int i = 0; i < groups.size(); i++) { - Unit group = groups.get(i); + final Unit group = groups.get(i); long start = group.timeMs(); if (i == 0) start = line.startTimeMs(); else if (start < line.startTimeMs() || start < previousTime) { start = intervalEndMs > line.startTimeMs() && line.startTimeMs() >= 0 ? line.startTimeMs() + (intervalEndMs - line.startTimeMs()) - * characterCount(text.substring(0, group.start())) / total + * characterCount(text.substring(0, group.start())) / total : -1; } if (start >= 0) previousTime = start; String formatted = LyricsTextFormatter.format( text.substring(group.start(), group.end()), capitalize, strip).text(); - // The ellipsis marks the split itself, so it goes inside any quotes closed around the part. - if (i + 1 < groups.size() && !hasWhitespace(text) && !formatted.endsWith(SPLIT_ELLIPSIS)) { + final boolean spaced = hasWhitespace(text); + if (i + 1 < groups.size() && !spaced && !formatted.endsWith(SPLIT_ELLIPSIS)) { formatted += SPLIT_ELLIPSIS; } + if (spaced && crossed[i] != null) { + formatted += SPLIT_ELLIPSIS + crossed[i].closing(); + } + if (spaced && i > 0 && crossed[i - 1] != null) { + formatted = crossed[i - 1].opening() + SPLIT_ELLIPSIS + formatted; + } result.add(new Chunk(PairedPunctuation.restore(text, formatted, group.start(), group.end()), start)); } return List.copyOf(result); @@ -436,21 +442,21 @@ public static long selectionPosition(long playbackMs, int offsetMs) { */ public static String instrumental(@Nullable String template, long startMs, long endMs, long positionMs, @Nullable String title, @Nullable String artist, @Nullable String lastLyric) { - String[] frames = LINE_BREAK.split(template == null ? DEFAULT_INSTRUMENTAL : template, -1); + final String[] frames = LINE_BREAK.split(template == null ? DEFAULT_INSTRUMENTAL : template, -1); final long duration = endMs - startMs; final int index = endMs == Long.MAX_VALUE || duration <= 0 ? 0 : (int) Math.min(frames.length - 1, - Math.max(0, positionMs - startMs) / (double) duration * frames.length); + Math.max(0, positionMs - startMs) / (double) duration * frames.length); return replacePlaceholders(frames[index], title, artist, lastLyric == null ? "" : lastLyric); } private static List timedUnits(LyricsLine line, LyricsTextFormatter.Result cleaned) { if (!line.hasWordTimings()) return List.of(); - List units = new ArrayList<>(); + final List units = new ArrayList<>(); int cursor = 0; final boolean spaced = hasWhitespace(line.text()); for (Word word : line.words()) { - String own = word.text().trim(); + final String own = word.text().trim(); if (own.isEmpty()) continue; final int start = line.text().indexOf(own, cursor); if (start < 0 || word.startMs() < 0) return List.of(); @@ -459,7 +465,7 @@ private static List timedUnits(LyricsLine line, LyricsTextFormatter.Result return List.of(); } final int end = start + own.length(); - Unit next = new Unit(cleaned.offset(start), cleaned.offset(end), word.startMs()); + final Unit next = new Unit(cleaned.offset(start), cleaned.offset(end), word.startMs()); if (!units.isEmpty() && spaced && !hasWhitespace(line.text().substring(cursor, start))) { Unit previous = units.remove(units.size() - 1); units.add(new Unit(previous.start(), next.end(), previous.timeMs())); @@ -474,9 +480,9 @@ private static List timedUnits(LyricsLine line, LyricsTextFormatter.Result || hasLetterOrDigit(cleaned.text().substring(units.get(units.size() - 1).end()))) { return List.of(); } - List covering = new ArrayList<>(units.size()); + final List covering = new ArrayList<>(units.size()); for (int i = 0; i < units.size(); i++) { - Unit unit = units.get(i); + final Unit unit = units.get(i); int end = i + 1 < units.size() ? units.get(i + 1).start() : cleaned.text().length(); while (end > unit.end()) { final int cp = cleaned.text().codePointBefore(end); @@ -502,16 +508,16 @@ private static boolean hasWhitespace(String text) { * dictionary words are the units. Punctuation joins the word before it. */ private static List untimedUnits(String text) { - List units = new ArrayList<>(); + final List units = new ArrayList<>(); final boolean spaced = hasWhitespace(text); - BreakIterator words = BreakIterator.getWordInstance(Locale.ROOT); + final BreakIterator words = BreakIterator.getWordInstance(Locale.ROOT); words.setText(text); int start = words.first(); // Opening punctuation is held back and joins the word after it. int opening = -1; int worded = 0; for (int end = words.next(); end != BreakIterator.DONE; start = end, end = words.next()) { - String piece = text.substring(start, end); + final String piece = text.substring(start, end); if (!hasNonWhitespace(piece)) continue; final boolean letters = hasLetterOrDigit(piece); final boolean joined = start > 0 && !isWhitespace(text.codePointBefore(start)); @@ -522,7 +528,7 @@ private static List untimedUnits(String text) { final int from = opening >= 0 ? opening : start; opening = -1; if (!units.isEmpty() && joined && from == start && (spaced || !letters)) { - Unit previous = units.remove(units.size() - 1); + final Unit previous = units.remove(units.size() - 1); units.add(new Unit(previous.start(), end, -1)); } else { units.add(new Unit(from, end, -1)); @@ -532,14 +538,14 @@ private static List untimedUnits(String text) { if (opening >= 0) { if (units.isEmpty()) units.add(new Unit(opening, text.length(), -1)); else { - Unit previous = units.remove(units.size() - 1); + final Unit previous = units.remove(units.size() - 1); units.add(new Unit(previous.start(), text.length(), -1)); } } if (worded < 2 && !spaced) { // One dictionary word longer than the limit: fall back to characters. units.clear(); - Matcher matcher = GRAPHEME.matcher(text); + final Matcher matcher = GRAPHEME.matcher(text); while (matcher.find()) units.add(new Unit(matcher.start(), matcher.end(), -1)); } return units; @@ -560,4 +566,10 @@ private static boolean isWhitespace(int cp) { private static boolean hasNonWhitespace(String text) { return text.codePoints().anyMatch(cp -> !isWhitespace(cp)); } + + public record Chunk(String text, long startMs) { + } + + private record Unit(int start, int end, long timeMs) { + } } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/MiniLyricsTimeline.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/MiniLyricsTimeline.java index ff9166683e..cf754ce344 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/MiniLyricsTimeline.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/MiniLyricsTimeline.java @@ -21,26 +21,8 @@ */ public final class MiniLyricsTimeline { - public record Vocal(LyricsLine line, int index, long startMs, long endMs) { - } - - /** - * @param previous The line sung before the gap, or null if the gap is before the first line. - */ - public record Gap(long startMs, long endMs, @Nullable Vocal previous) { - } - - /** - * At most one of vocal and gap is set. - * - * @param previous If neither is set, the line that started last, or null if none did yet. - */ - public record Selection(@Nullable Vocal vocal, @Nullable Gap gap, @Nullable Vocal previous) { - } - private static final int MUSICAL_NOTE = "🎵".codePointAt(0); private static final int MULTIPLE_MUSICAL_NOTES = "🎶".codePointAt(0); - private final List vocals = new ArrayList<>(); private final List gaps = new ArrayList<>(); /** The latest end time of the vocals up to and including each index. */ @@ -122,15 +104,6 @@ public MiniLyricsTimeline(List lines, long durationMs) { } } - private void addGaps(List classified, long start, long end, - @Nullable Vocal previous) { - for (SectionDotsMotion.Gap gap : classified) { - long from = Math.max(start, gap.gapStartMs()); - long to = Math.min(end, gap.gapEndMs()); - if (from < to) gaps.add(new Gap(from, to, previous)); - } - } - /** * If a line is empty or only has music notes, such as the ♪♪♪ some providers use. */ @@ -152,6 +125,15 @@ public static boolean isInstrumentalMarker(String text) { return note; } + private void addGaps(List classified, long start, long end, + @Nullable Vocal previous) { + for (SectionDotsMotion.Gap gap : classified) { + long from = Math.max(start, gap.gapStartMs()); + long to = Math.min(end, gap.gapEndMs()); + if (from < to) gaps.add(new Gap(from, to, previous)); + } + } + public Selection at(long positionMs) { long position = Math.max(0, positionMs); if (durationMs > 0) position = Math.min(position, durationMs - 1); @@ -187,4 +169,21 @@ public Selection at(long positionMs) { return new Selection(null, null, high >= 0 ? vocals.get(high) : null); } + + public record Vocal(LyricsLine line, int index, long startMs, long endMs) { + } + + /** + * @param previous The line sung before the gap, or null if the gap is before the first line. + */ + public record Gap(long startMs, long endMs, @Nullable Vocal previous) { + } + + /** + * At most one of vocal and gap is set. + * + * @param previous If neither is set, the line that started last, or null if none did yet. + */ + public record Selection(@Nullable Vocal vocal, @Nullable Gap gap, @Nullable Vocal previous) { + } } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/PairedPunctuation.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/PairedPunctuation.java index e24c1a37a7..2ffd085df5 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/PairedPunctuation.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/PairedPunctuation.java @@ -59,9 +59,6 @@ * Restores only complete enclosing punctuation shells that a split crosses. */ public final class PairedPunctuation { - public record Pair(String opening, String closing) { - } - public static final List PAIRS = List.of( new Pair("\"", "\""), new Pair("'", "'"), @@ -148,9 +145,6 @@ public record Pair(String opening, String closing) { new Pair("「", "」") ); - private record Shell(Pair pair, int openEnd, int closeStart) { - } - private PairedPunctuation() { } @@ -190,6 +184,56 @@ private static boolean space(int cp) { return Character.isWhitespace(cp) || Character.isSpaceChar(cp); } + /** + * Excludes whole-line shells, which {@link #restore} completes without an ellipsis. + */ + static Pair crossedAt(String whole, int mid) { + for (int at = mid; at > 0; ) { + final int cp = whole.codePointBefore(at); + final int start = at - Character.charCount(cp); + final String ch = whole.substring(start, at); + for (Pair pair : PAIRS) { + if (!pair.opening().equals(ch)) continue; + int closeAt = findMatchingClose(whole, pair, start + pair.opening().length(), mid); + if (closeAt >= 0 && closeAt >= mid && closesAtEnd(whole, pair, start + pair.opening().length(), closeAt) + && !wholeLineShell(whole, pair, start, closeAt)) { + return pair; + } + } + at = start; + } + return null; + } + + private static int findMatchingClose(String whole, Pair pair, int scanStart, int minClose) { + int depth = 1; + for (int i = scanStart; i < whole.length(); ) { + int cp = whole.codePointAt(i); + int len = Character.charCount(cp); + String ch = whole.substring(i, i + len); + if (pair.opening().equals(ch)) depth++; + else if (pair.closing().equals(ch)) { + depth--; + if (depth == 0) { + if (i >= minClose) return i; + else return -1; // matched too early; don't use for split after mid + } + } + i += len; + } + return -1; + } + + private static boolean wholeLineShell(String whole, Pair pair, int openAt, int closeAt) { + int first = 0; + while (first < whole.length() && space(whole.codePointAt(first))) + first += Character.charCount(whole.codePointAt(first)); + int last = whole.length(); + while (last > first && space(whole.codePointBefore(last))) + last -= Character.charCount(whole.codePointBefore(last)); + return openAt == first && closeAt + pair.closing().length() == last; + } + private static boolean escaped(String text, int at) { int count = 0; while (at > 0 && text.charAt(--at) == '\\') count++; @@ -219,4 +263,10 @@ private static boolean closesAtEnd(String text, Pair pair, int at, int closing) } return false; } + + public record Pair(String opening, String closing) { + } + + private record Shell(Pair pair, int openEnd, int closeStart) { + } } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/TrackVersion.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/TrackVersion.java index b65a581955..8858091653 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/TrackVersion.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/TrackVersion.java @@ -35,7 +35,7 @@ public static TrackInfo query(TrackInfo track, @Nullable String rawTitle, @Nulla public static String clean(String version) { String s = version.strip(); while (!s.isEmpty()) { - String core = core(s); + final String core = core(s); final int first = s.codePointAt(0); if (isEdgePunctuation(first) && !hasPartner(first, core)) { s = s.substring(Character.charCount(first)).strip(); @@ -71,15 +71,15 @@ private static boolean isEdge(int cp) { private static boolean isEdgePunctuation(int cp) { return switch (Character.getType(cp)) { case Character.CONNECTOR_PUNCTUATION, Character.DASH_PUNCTUATION, - Character.START_PUNCTUATION, Character.END_PUNCTUATION, - Character.INITIAL_QUOTE_PUNCTUATION, Character.FINAL_QUOTE_PUNCTUATION, - Character.OTHER_PUNCTUATION -> true; + Character.START_PUNCTUATION, Character.END_PUNCTUATION, + Character.INITIAL_QUOTE_PUNCTUATION, Character.FINAL_QUOTE_PUNCTUATION, + Character.OTHER_PUNCTUATION -> true; default -> false; }; } private static boolean hasPartner(int cp, String core) { - String symbol = new String(Character.toChars(cp)); + final String symbol = new String(Character.toChars(cp)); for (PairedPunctuation.Pair pair : PairedPunctuation.PAIRS) { if (pair.opening().equals(symbol) && core.contains(pair.closing()) || pair.closing().equals(symbol) && core.contains(pair.opening())) { @@ -95,8 +95,8 @@ private static boolean hasPartner(int cp, String core) { */ public static boolean matchesStructuredTitle(String displayed, String full, String shortTitle, String version) { if (shortTitle.isBlank()) return false; - String wanted = normalize(displayed); - String variant = clean(version); + final String wanted = normalize(displayed); + final String variant = clean(version); if (variant.isEmpty()) { return wanted.equals(normalize(shortTitle)) && normalize(full).equals(normalize(shortTitle)); } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/Word.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/Word.java index 9c9c27613d..3288d09382 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/Word.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/model/Word.java @@ -9,7 +9,8 @@ import androidx.annotation.Nullable; -public record Word(long startMs, long endMs, String text, @Nullable String romaji, boolean endsWithSpace) { +public record Word(long startMs, long endMs, String text, @Nullable String romaji, + boolean endsWithSpace) { public Word(long startMs, long endMs, String text) { this(startMs, endMs, text, null, false); diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/parsers/LRCParser.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/parsers/LRCParser.java index 7dc6516521..a24cc47b52 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/parsers/LRCParser.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/parsers/LRCParser.java @@ -30,27 +30,23 @@ */ public final class LRCParser { + public static final Set CREDIT_META_KEYS = Set.of("ti", "ar", "al", "au"); + public static final Pattern LRC_META = Pattern.compile("^\\[(\\w+):([^]]*)]$"); /** Runs of whitespace inside a heading, collapsed so {@code [Pre - Chorus]} reads as one phrase. */ private static final Pattern HEADING_SPACES = Pattern.compile("\\s+"); - /** Tags such as {@code [ar:Artist]} that are not timestamps. */ private static final String METADATA_TAG_CHARACTERS = "abcdefghijklmnopqrstuvwxyz"; - private static final String BG_TAG = "bg:1"; - - public static final Set CREDIT_META_KEYS = Set.of("ti", "ar", "al", "au"); - - public static final Pattern LRC_META = Pattern.compile("^\\[(\\w+):([^]]*)]$"); - - public static final class LrcParseResult { - public final List lines; - public final List creditLines; - - LrcParseResult(List lines, List creditLines) { - this.lines = lines; - this.creditLines = creditLines; - } - } + private static final Pattern SPEAKER_TAG_PATTERN = + Pattern.compile("^(bg|v\\d+|[FMD]|duet|male|female)$", Pattern.CASE_INSENSITIVE); + /** A gap shorter than this is sung through, so the line lasts until the next one starts. */ + private static final long HELD_GAP_MS = 11_000; + private static final long MIN_HOLD_MS = 5000; + private static final long HOLD_PER_CHAR_MS = 180; + /** How long before the next line a long instrumental gap is shown as such. */ + private static final long GAP_LEAD_IN_MS = 7000; + /** {@code '1'} where the line text puts a space after that word, {@code '0'} where it does not. */ + private static final char SEPARATED = '1'; private LRCParser() { } @@ -111,9 +107,6 @@ public static String cleanTextForComparison(@Nullable String text) { return text.replaceAll("[\"'`.,!?:;~()()\\[\\]{}<>«»“”‘’]+", "").trim().toLowerCase(Locale.ROOT); } - private static final Pattern SPEAKER_TAG_PATTERN = - Pattern.compile("^(bg|v\\d+|[FMD]|duet|male|female)$", Pattern.CASE_INSENSITIVE); - /** * Parses synced LRC content. * @@ -284,15 +277,6 @@ private static boolean isBreakMarker(String text) { return trimmed.isEmpty() || "♪".equals(trimmed) || "♪♪".equals(trimmed); } - /** A line repeated right after itself is a backing vocal echoing the lead. */ - private static final long ECHO_WINDOW_MS = 3500; - /** A gap shorter than this is sung through, so the line lasts until the next one starts. */ - private static final long HELD_GAP_MS = 11_000; - private static final long MIN_HOLD_MS = 5000; - private static final long HOLD_PER_CHAR_MS = 180; - /** How long before the next line a long instrumental gap is shown as such. */ - private static final long GAP_LEAD_IN_MS = 7000; - /** * When a line that has no end of its own stops being sung: at the next line if that follows * soon, otherwise after a hold that grows with the text, so a long instrumental gap shows as one. @@ -387,15 +371,17 @@ private static void resolveDuet(List lines) { lastPersonIsDuet = isDuet; } + if (LyricsLine.isCenteredAgentId(sp)) { + // Keep the alternation state so surrounding lines keep their side. + isDuet = false; + } + lines.set(i, new LyricsLine( orig.startTimeMs(), orig.endTimeMs(), orig.text(), orig.words(), sp, isDuet, orig.isBG(), orig.songPart())); } } - private record BodyParse(String text, List words) { - } - private static BodyParse parseBody(String body, long lineStartMs) { if (body.indexOf('<') < 0 && body.indexOf('[') < 0) { return new BodyParse(body, List.of()); @@ -477,7 +463,7 @@ private static BodyParse parseBody(String body, long lineStartMs) { * between two of them. Dropped, it would be gone from the next write. */ private static void addWord(List words, long startMs, String written) { - String body = written.trim(); + final String body = written.trim(); if (body.isEmpty()) { markSpaceAfter(words); return; @@ -493,7 +479,7 @@ private static void markSpaceAfter(List words) { if (words.isEmpty()) { return; } - Word previous = words.get(words.size() - 1); + final Word previous = words.get(words.size() - 1); if (!previous.endsWithSpace()) { words.set(words.size() - 1, new Word(previous.startMs(), previous.endMs(), previous.text(), previous.romaji(), true)); @@ -535,6 +521,9 @@ private static List stripAgentMarker(List words, String agentMarker) } private static boolean isDuetAgent(String agentId) { + if (LyricsLine.isCenteredAgentId(agentId)) { + return false; + } return (agentId.charAt(agentId.length() - 1) - '0') % 2 == 0; } @@ -557,11 +546,11 @@ public static String formatLine(LyricsLine line) { builder.append("[duet]"); } if (line.hasWords()) { - List words = line.words(); - String marks = spacesAfter(line.text(), words); + final List words = line.words(); + final String marks = spacesAfter(line.text(), words); Word lastWord = null; for (int i = 0; i < words.size(); i++) { - Word word = words.get(i); + final Word word = words.get(i); builder.append('<') .append(formatWordTimestamp(word.startMs())) .append('>') @@ -582,20 +571,17 @@ public static String formatLine(LyricsLine line) { return builder.toString(); } - /** {@code '1'} where the line text puts a space after that word, {@code '0'} where it does not. */ - private static final char SEPARATED = '1'; - /** * A cached line is written as its words and the text is rebuilt from them when it is read * back, so a space only the text carries would not survive the round trip. The text decides * where it can be placed; a word it does not hold keeps the flag it came in with. */ private static String spacesAfter(String text, List words) { - StringBuilder marks = new StringBuilder(words.size()); + final StringBuilder marks = new StringBuilder(words.size()); int at = 0; for (int i = 0; i < words.size(); i++) { - Word word = words.get(i); - String body = word.text(); + final Word word = words.get(i); + final String body = word.text(); if (body.isEmpty()) { marks.append(word.endsWithSpace() ? SEPARATED : '0'); continue; @@ -619,42 +605,29 @@ private static String spacesAfter(String text, List words) { * writing the space again would double it. */ private static boolean carriesOwnSpace(List words, int index) { - String body = words.get(index).text(); + final String body = words.get(index).text(); if (Character.isWhitespace(body.charAt(body.length() - 1))) { return true; } if (index + 1 >= words.size()) { return false; } - String next = words.get(index + 1).text(); + final String next = words.get(index + 1).text(); return !next.isEmpty() && Character.isWhitespace(next.charAt(0)); } /** * Parses plain (unsynced) lyrics, one line per text line. * - *

    A line that is entirely a bracketed tag (for example {@code [Chorus]}, as Genius and - * several other providers publish) is a section heading rather than a lyric. It is dropped - * from the returned lines and attached to {@link LyricsLine#songPart()} of the next line - * that has content, so the view can show it as a label instead of singing it. Brackets that - * only appear inside a line, such as {@code Hello [world]}, stay plain text. + *

    A line that is entirely a bracketed tag (e.g. {@code [Chorus]}, as Genius and others + * publish) is a section heading, not a lyric: dropped from the lines and attached to + * {@link LyricsLine#songPart()} of the next content line so the view shows it as a label. + * Brackets only inside a line, such as {@code Hello [world]}, stay plain text. */ public static List parsePlain(@Nullable String plain) { return parsePlainWithSlots(plain).lines(); } - /** - * Plain lyrics together with the position every source line ended up in. - * - * @param lines Parsed lines, with headings removed. - * @param slots For each line of the source text, the index it became in {@code lines}, or - * {@code -1} when it was a heading that was dropped. Callers aligning a - * translation against the source text need this, because a heading in the - * original shifts every following line down by one. - */ - public record PlainLyrics(List lines, int[] slots) { - } - /** * Same parse as {@link #parsePlain}, also reporting where each source line landed. Both go * through one loop so the mapping can never drift from the parser that produced it. @@ -697,8 +670,8 @@ public static PlainLyrics parsePlainWithSlots(@Nullable String plain) { /** * @return {@code true} if the whole line is a bracketed heading, such as {@code [Verse 1]}. - * Bracketed metadata such as {@code [ar:Artist]} never reaches plain lyrics, and an - * empty {@code []} is too short to be a heading. + * Bracketed metadata such as {@code [ar:Artist]} never reaches plain lyrics, and an + * empty {@code []} is too short to be a heading. */ private static boolean isSectionHeader(String trimmed) { return trimmed.length() > 2 && trimmed.charAt(0) == '[' @@ -824,4 +797,29 @@ public static void inferMissingWordEnds(List words, long lastFallbackMs) { } } } + + public static final class LrcParseResult { + public final List lines; + public final List creditLines; + + LrcParseResult(List lines, List creditLines) { + this.lines = lines; + this.creditLines = creditLines; + } + } + + private record BodyParse(String text, List words) { + } + + /** + * Plain lyrics together with the position every source line ended up in. + * + * @param lines Parsed lines, with headings removed. + * @param slots For each line of the source text, the index it became in {@code lines}, or + * {@code -1} when it was a heading that was dropped. Callers aligning a + * translation against the source text need this, because a heading in the + * original shifts every following line down by one. + */ + public record PlainLyrics(List lines, int[] slots) { + } } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/parsers/LyricifyParser.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/parsers/LyricifyParser.java index bc32c0ad9f..6e9d7bd651 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/parsers/LyricifyParser.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/parsers/LyricifyParser.java @@ -38,22 +38,22 @@ public static List parseLines(String text, int offsetMs) { return Collections.emptyList(); } - List lines = new ArrayList<>(); + final List lines = new ArrayList<>(); for (String raw : text.split("\\r?\\n")) { - String line = raw.trim(); + final String line = raw.trim(); if (line.isEmpty()) { continue; } - Matcher m = LINE_TIMING.matcher(line); + final Matcher m = LINE_TIMING.matcher(line); if (!m.matches()) { continue; } final long startMs = Long.parseLong(Objects.requireNonNull(m.group(1))) + offsetMs; final long endMs = Long.parseLong(Objects.requireNonNull(m.group(2))) + offsetMs; - String body = Objects.requireNonNull(m.group(3)).trim(); + final String body = Objects.requireNonNull(m.group(3)).trim(); if (body.isEmpty()) { continue; } @@ -74,15 +74,15 @@ public static List parseSyllable(String text, int offsetMs) { return Collections.emptyList(); } - List lines = new ArrayList<>(); + final List lines = new ArrayList<>(); for (String raw : text.split("\\r?\\n")) { - String line = raw.trim(); + final String line = raw.trim(); if (line.isEmpty()) { continue; } - String body; + final String body; if (line.startsWith("[")) { final int close = line.indexOf(']'); body = (close > 0) ? line.substring(close + 1) : line; @@ -90,12 +90,12 @@ public static List parseSyllable(String text, int offsetMs) { body = line; } - List words = parseSyllableWords(body, offsetMs); + final List words = parseSyllableWords(body, offsetMs); if (words.isEmpty()) { continue; } - String fullText = joinWordTexts(words); + final String fullText = joinWordTexts(words); final long startMs = words.get(0).startMs(); final long endMs = words.get(words.size() - 1).endMs(); lines.add(new LyricsLine(startMs, endMs, fullText, words)); @@ -110,11 +110,11 @@ public static List parseSyllable(String text, int offsetMs) { } private static List parseSyllableWords(String body, int offsetMs) { - List words = new ArrayList<>(); - Matcher m = WORD_TIMING.matcher(body); + final List words = new ArrayList<>(); + final Matcher m = WORD_TIMING.matcher(body); while (m.find()) { - String wordText = Objects.requireNonNull(m.group(1)); + final String wordText = Objects.requireNonNull(m.group(1)); final long startMs = Long.parseLong(Objects.requireNonNull(m.group(2))) + offsetMs; final long durMs = Long.parseLong(Objects.requireNonNull(m.group(3))); final long endMs = startMs + durMs; @@ -132,7 +132,7 @@ private static List parseSyllableWords(String body, int offsetMs) { } private static String joinWordTexts(List words) { - StringBuilder sb = new StringBuilder(); + final StringBuilder sb = new StringBuilder(); for (Word w : words) { sb.append(w.text()); } @@ -144,22 +144,22 @@ public static List parseTranslation(String trans, int offsetMs) { return Collections.emptyList(); } - List entries = new ArrayList<>(); + final List entries = new ArrayList<>(); for (String raw : trans.split("\\r?\\n")) { - String line = raw.trim(); + final String line = raw.trim(); if (line.isEmpty()) { continue; } - Matcher m = LRC_TIMESTAMP.matcher(line); + final Matcher m = LRC_TIMESTAMP.matcher(line); if (!m.matches()) { continue; } final long min = Long.parseLong(Objects.requireNonNull(m.group(1))); - String secPart = Objects.requireNonNull(m.group(2)); - String text = Objects.requireNonNull(m.group(3)).trim(); + final String secPart = Objects.requireNonNull(m.group(2)); + final String text = Objects.requireNonNull(m.group(3)).trim(); if (text.isEmpty()) { continue; } @@ -174,7 +174,7 @@ public static List parseTranslation(String trans, int offsetMs) { entries.sort(Comparator.comparingLong(TranslatedEntry::timeMs)); - List result = new ArrayList<>(entries.size()); + final List result = new ArrayList<>(entries.size()); for (TranslatedEntry e : entries) { result.add(e.text()); } @@ -195,5 +195,6 @@ private static long parseLrcSeconds(long minutes, String secPart) { return (minutes * 60 + seconds) * 1000 + fractionMs; } - private record TranslatedEntry(long timeMs, String text) {} + private record TranslatedEntry(long timeMs, String text) { + } } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/parsers/LyricsCrypto.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/parsers/LyricsCrypto.java index 95fe4de608..dd79fb04bd 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/parsers/LyricsCrypto.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/parsers/LyricsCrypto.java @@ -32,6 +32,17 @@ */ public final class LyricsCrypto { + private static final int[][] QRC_SBOX = { + {14, 4, 13, 1, 2, 15, 11, 8, 3, 10, 6, 12, 5, 9, 0, 7, 0, 15, 7, 4, 14, 2, 13, 1, 10, 6, 12, 11, 9, 5, 3, 8, 4, 1, 14, 8, 13, 6, 2, 11, 15, 12, 9, 7, 3, 10, 5, 0, 15, 12, 8, 2, 4, 9, 1, 7, 5, 11, 3, 14, 10, 0, 6, 13}, + {15, 1, 8, 14, 6, 11, 3, 4, 9, 7, 2, 13, 12, 0, 5, 10, 3, 13, 4, 7, 15, 2, 8, 15, 12, 0, 1, 10, 6, 9, 11, 5, 0, 14, 7, 11, 10, 4, 13, 1, 5, 8, 12, 6, 9, 3, 2, 15, 13, 8, 10, 1, 3, 15, 4, 2, 11, 6, 7, 12, 0, 5, 14, 9}, + {10, 0, 9, 14, 6, 3, 15, 5, 1, 13, 12, 7, 11, 4, 2, 8, 13, 7, 0, 9, 3, 4, 6, 10, 2, 8, 5, 14, 12, 11, 15, 1, 13, 6, 4, 9, 8, 15, 3, 0, 11, 1, 2, 12, 5, 10, 14, 7, 1, 10, 13, 0, 6, 9, 8, 7, 4, 15, 14, 3, 11, 5, 2, 12}, + {7, 13, 14, 3, 0, 6, 9, 10, 1, 2, 8, 5, 11, 12, 4, 15, 13, 8, 11, 5, 6, 15, 0, 3, 4, 7, 2, 12, 1, 10, 14, 9, 10, 6, 9, 0, 12, 11, 7, 13, 15, 1, 3, 14, 5, 2, 8, 4, 3, 15, 0, 6, 10, 10, 13, 8, 9, 4, 5, 11, 12, 7, 2, 14}, + {2, 12, 4, 1, 7, 10, 11, 6, 8, 5, 3, 15, 13, 0, 14, 9, 14, 11, 2, 12, 4, 7, 13, 1, 5, 0, 15, 10, 3, 9, 8, 6, 4, 2, 1, 11, 10, 13, 7, 8, 15, 9, 12, 5, 6, 3, 0, 14, 11, 8, 12, 7, 1, 14, 2, 13, 6, 15, 0, 9, 10, 4, 5, 3}, + {12, 1, 10, 15, 9, 2, 6, 8, 0, 13, 3, 4, 14, 7, 5, 11, 10, 15, 4, 2, 7, 12, 9, 5, 6, 1, 13, 14, 0, 11, 3, 8, 9, 14, 15, 5, 2, 8, 12, 3, 7, 0, 4, 10, 1, 13, 11, 6, 4, 3, 2, 12, 9, 5, 15, 10, 11, 14, 1, 7, 6, 0, 8, 13}, + {4, 11, 2, 14, 15, 0, 8, 13, 3, 12, 9, 7, 5, 10, 6, 1, 13, 0, 11, 7, 4, 9, 1, 10, 14, 3, 5, 12, 2, 15, 8, 6, 1, 4, 11, 13, 12, 3, 7, 14, 10, 15, 6, 8, 0, 5, 9, 2, 6, 11, 13, 8, 1, 4, 10, 7, 9, 5, 0, 15, 14, 2, 3, 12}, + {13, 2, 8, 4, 6, 15, 11, 1, 10, 9, 3, 14, 5, 0, 12, 7, 1, 15, 13, 8, 10, 3, 7, 4, 12, 5, 6, 11, 0, 14, 9, 2, 7, 11, 4, 1, 9, 12, 14, 2, 0, 6, 10, 13, 15, 3, 5, 8, 2, 1, 14, 7, 4, 10, 8, 13, 15, 12, 9, 0, 3, 5, 6, 11} + }; + private LyricsCrypto() { } @@ -110,17 +121,6 @@ public static byte[] tripleDesEcbDecrypt(byte[] data, String key) { return out; } - private static final int[][] QRC_SBOX = { - {14, 4, 13, 1, 2, 15, 11, 8, 3, 10, 6, 12, 5, 9, 0, 7, 0, 15, 7, 4, 14, 2, 13, 1, 10, 6, 12, 11, 9, 5, 3, 8, 4, 1, 14, 8, 13, 6, 2, 11, 15, 12, 9, 7, 3, 10, 5, 0, 15, 12, 8, 2, 4, 9, 1, 7, 5, 11, 3, 14, 10, 0, 6, 13}, - {15, 1, 8, 14, 6, 11, 3, 4, 9, 7, 2, 13, 12, 0, 5, 10, 3, 13, 4, 7, 15, 2, 8, 15, 12, 0, 1, 10, 6, 9, 11, 5, 0, 14, 7, 11, 10, 4, 13, 1, 5, 8, 12, 6, 9, 3, 2, 15, 13, 8, 10, 1, 3, 15, 4, 2, 11, 6, 7, 12, 0, 5, 14, 9}, - {10, 0, 9, 14, 6, 3, 15, 5, 1, 13, 12, 7, 11, 4, 2, 8, 13, 7, 0, 9, 3, 4, 6, 10, 2, 8, 5, 14, 12, 11, 15, 1, 13, 6, 4, 9, 8, 15, 3, 0, 11, 1, 2, 12, 5, 10, 14, 7, 1, 10, 13, 0, 6, 9, 8, 7, 4, 15, 14, 3, 11, 5, 2, 12}, - {7, 13, 14, 3, 0, 6, 9, 10, 1, 2, 8, 5, 11, 12, 4, 15, 13, 8, 11, 5, 6, 15, 0, 3, 4, 7, 2, 12, 1, 10, 14, 9, 10, 6, 9, 0, 12, 11, 7, 13, 15, 1, 3, 14, 5, 2, 8, 4, 3, 15, 0, 6, 10, 10, 13, 8, 9, 4, 5, 11, 12, 7, 2, 14}, - {2, 12, 4, 1, 7, 10, 11, 6, 8, 5, 3, 15, 13, 0, 14, 9, 14, 11, 2, 12, 4, 7, 13, 1, 5, 0, 15, 10, 3, 9, 8, 6, 4, 2, 1, 11, 10, 13, 7, 8, 15, 9, 12, 5, 6, 3, 0, 14, 11, 8, 12, 7, 1, 14, 2, 13, 6, 15, 0, 9, 10, 4, 5, 3}, - {12, 1, 10, 15, 9, 2, 6, 8, 0, 13, 3, 4, 14, 7, 5, 11, 10, 15, 4, 2, 7, 12, 9, 5, 6, 1, 13, 14, 0, 11, 3, 8, 9, 14, 15, 5, 2, 8, 12, 3, 7, 0, 4, 10, 1, 13, 11, 6, 4, 3, 2, 12, 9, 5, 15, 10, 11, 14, 1, 7, 6, 0, 8, 13}, - {4, 11, 2, 14, 15, 0, 8, 13, 3, 12, 9, 7, 5, 10, 6, 1, 13, 0, 11, 7, 4, 9, 1, 10, 14, 3, 5, 12, 2, 15, 8, 6, 1, 4, 11, 13, 12, 3, 7, 14, 10, 15, 6, 8, 0, 5, 9, 2, 6, 11, 13, 8, 1, 4, 10, 7, 9, 5, 0, 15, 14, 2, 3, 12}, - {13, 2, 8, 4, 6, 15, 11, 1, 10, 9, 3, 14, 5, 0, 12, 7, 1, 15, 13, 8, 10, 3, 7, 4, 12, 5, 6, 11, 0, 14, 9, 2, 7, 11, 4, 1, 9, 12, 14, 2, 0, 6, 10, 13, 15, 3, 5, 8, 2, 1, 14, 7, 4, 10, 8, 13, 15, 12, 9, 0, 3, 5, 6, 11} - }; - private static int qrcBitnum(byte[] bytes, int b, int c) { int byteIndex = (b / 32) * 4 + 3 - ((b % 32) / 8); if (byteIndex >= bytes.length) { diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/parsers/LyricsFileParser.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/parsers/LyricsFileParser.java index b69178e4f1..9756a22c43 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/parsers/LyricsFileParser.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/parsers/LyricsFileParser.java @@ -34,7 +34,7 @@ public static Lyrics parse(@Nullable String yaml, String providerName) { @Nullable public static Lyrics parse(@Nullable String yaml, String providerName, - @Nullable String sourceUrl) { + @Nullable String sourceUrl) { if (yaml == null || yaml.isEmpty()) { return null; } @@ -193,10 +193,6 @@ private static long asLong(Object value) { return LyricsLine.NO_TIME; } - private static final class Cursor { - int index; - } - private static Object readValue(List lines, Cursor cursor, int indent) { if (cursor.index >= lines.size()) { return new LinkedHashMap<>(); @@ -318,4 +314,8 @@ private static int indentOf(String line) { } return count; } + + private static final class Cursor { + int index; + } } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/parsers/TTMLParser.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/parsers/TTMLParser.java index fe1b6cf9af..0232685c5e 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/parsers/TTMLParser.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/parsers/TTMLParser.java @@ -52,26 +52,28 @@ public final class TTMLParser { private static final String NS_XML = "http://www.w3.org/XML/1998/namespace"; private static final String NS_TTS = "http://www.w3.org/ns/ttml#styling"; private static final String NS_AMLL = "http://www.example.com/ns/amll"; + private static final String NS_COMPOSER = "https://composer.betterlyrics.org/ttml"; + private static final String NS_COMPOSER_LEGACY = "https://composer.boidu.dev/ttml"; private static final Set AMLL_EXCLUDED_KEYS = Set.of( "ncmMusicId", "qqMusicId", "spotifyId", "appleMusicId", "isrc", "ttmlAuthorGithub" ); + private static final Set COMPOSER_EXCLUDED_KEYS = Set.of( + "isrc", "songwriter", "songwriters", "ytVideoId", "language" + ); + /** IoU minimum overlap threshold for romanization alignment. */ private static final double MIN_IOU = 0.1; /** Fast-track tolerance: if start times differ by ≤2ms, consider them aligned. */ private static final long FAST_TRACK_MS = 2; + private static final String AGENT_TYPE_PERSON = "person"; + private static final String AGENT_TYPE_GROUP = "group"; + private static final String AGENT_TYPE_OTHER = "other"; private TTMLParser() { } - private record HeadMetadata( - List songwriters, - List amllCredits, - Map agentTypes, - Map> sidecarRoman, - Map sidecarTrans) {} - private static HeadMetadata parseHeadMetadata(String ttml) { List songwriters = new ArrayList<>(); List amllCredits = new ArrayList<>(); @@ -80,9 +82,9 @@ private static HeadMetadata parseHeadMetadata(String ttml) { Map sidecarTrans = new HashMap<>(); try { - XmlPullParserFactory factory = XmlPullParserFactory.newInstance(); + final XmlPullParserFactory factory = XmlPullParserFactory.newInstance(); factory.setNamespaceAware(true); - XmlPullParser p = factory.newPullParser(); + final XmlPullParser p = factory.newPullParser(); p.setInput(new StringReader(ttml)); boolean inHead = false; @@ -96,7 +98,7 @@ private static HeadMetadata parseHeadMetadata(String ttml) { int event = p.getEventType(); while (event != XmlPullParser.END_DOCUMENT) { if (event == XmlPullParser.START_TAG) { - String local = localName(p.getName()); + final String local = localName(p.getName()); if ("head".equals(local)) { inHead = true; } else if (inHead) { @@ -109,20 +111,15 @@ private static HeadMetadata parseHeadMetadata(String ttml) { // one would leave songwritersDepth above zero at // and latch inSongwriters, which swallows // the transliterations below. - String name = readTextContent(p); + final String name = readTextContent(p); if (!name.trim().isEmpty()) { songwriters.add(name.trim()); } } else { songwritersDepth++; } - } else if ("meta".equals(local) && NS_AMLL.equals(p.getNamespace())) { - String key = getAttr(p, NS_AMLL, "key", "amll:key"); - String value = getAttr(p, NS_AMLL, "value", "amll:value"); - if (key != null && value != null && !key.isEmpty() && !value.isEmpty() - && !AMLL_EXCLUDED_KEYS.contains(key)) { - amllCredits.add(key + ": " + value.trim()); - } + } else if ("meta".equals(local) && isMetaNamespace(p)) { + collectMetaCredit(p, amllCredits); } else if ("agent".equals(local)) { agentId = getAttr(p, NS_TTM, "id", "xml:id"); agentType = getAttr(p, NS_TTM, "type", "ttm:type"); @@ -132,7 +129,7 @@ private static HeadMetadata parseHeadMetadata(String ttml) { agentName = null; inAgent = true; } else if (inAgent && "name".equals(local)) { - String name = readTextContent(p); + final String name = readTextContent(p); if (!name.trim().isEmpty()) { agentName = name.trim(); } @@ -143,7 +140,7 @@ private static HeadMetadata parseHeadMetadata(String ttml) { } } } else if (event == XmlPullParser.END_TAG) { - String local = localName(p.getName()); + final String local = localName(p.getName()); if ("head".equals(local)) { inHead = false; } else if (inSongwriters && "songwriters".equals(local)) { @@ -170,34 +167,15 @@ private static HeadMetadata parseHeadMetadata(String ttml) { return new HeadMetadata(songwriters, amllCredits, agentTypes, sidecarRoman, sidecarTrans); } - private static final String AGENT_TYPE_PERSON = "person"; - private static final String AGENT_TYPE_GROUP = "group"; - private static final String AGENT_TYPE_OTHER = "other"; - - public record TtmlResult(List lines, - @Nullable List romanization, - @Nullable Map> translations, - @Nullable Map> romanizations, - @Nullable List songwriters, - @Nullable List amllCreditLines, - @Nullable Map agentNames) {} - - private record RomajiSyllable(long startMs, long endMs, String text) {} - - private record AgentInfo(String type, @Nullable String name) {} - - private record SidecarTranslation(String text, @Nullable List words, - @Nullable String bgText, @Nullable List bgWords) {} - @Nullable public static TtmlResult parse(String ttml) { if (ttml == null || ttml.isEmpty()) { return null; } try { - HeadMetadata hm = parseHeadMetadata(ttml); + final HeadMetadata hm = parseHeadMetadata(ttml); - List songwriters; + final List songwriters; if (!hm.songwriters().isEmpty()) { StringBuilder sb = new StringBuilder("Written by: "); for (int i = 0; i < hm.songwriters().size(); i++) { @@ -208,20 +186,20 @@ public static TtmlResult parse(String ttml) { } else { songwriters = null; } - List amllCreditLines = hm.amllCredits(); - Map agentTypes = hm.agentTypes(); - Map> sidecarRoman = hm.sidecarRoman(); - Map sidecarTrans = hm.sidecarTrans(); + final List amllCreditLines = hm.amllCredits(); + final Map agentTypes = hm.agentTypes(); + final Map> sidecarRoman = hm.sidecarRoman(); + final Map sidecarTrans = hm.sidecarTrans(); - XmlPullParserFactory factory = XmlPullParserFactory.newInstance(); + final XmlPullParserFactory factory = XmlPullParserFactory.newInstance(); factory.setNamespaceAware(true); - XmlPullParser p = factory.newPullParser(); + final XmlPullParser p = factory.newPullParser(); p.setInput(new StringReader(ttml)); - List lines = new ArrayList<>(); - List romanization = new ArrayList<>(); - Map> translations = new HashMap<>(); - Map> romanizations = new HashMap<>(); + final List lines = new ArrayList<>(); + final List romanization = new ArrayList<>(); + final Map> translations = new HashMap<>(); + final Map> romanizations = new HashMap<>(); int mainLineCount; boolean noTiming = false; @@ -232,7 +210,7 @@ public static TtmlResult parse(String ttml) { int event = p.getEventType(); while (event != XmlPullParser.END_DOCUMENT) { if (event == XmlPullParser.START_TAG) { - String local = localName(p.getName()); + final String local = localName(p.getName()); if ("tt".equals(local)) { noTiming = "none".equals( @@ -247,35 +225,35 @@ public static TtmlResult parse(String ttml) { divSongPart = getAttr(p, NS_ITUNES, "songPart", "itunes:song-part"); } } else if ("p".equals(local) && inBody && !inHead) { - String lineId = getAttr(p, NS_ITUNES, "key", "itunes:key"); - String agentId = getAttr(p, NS_TTM, "agent", "ttm:agent"); + final String lineId = getAttr(p, NS_ITUNES, "key", "itunes:key"); + final String agentId = getAttr(p, NS_TTM, "agent", "ttm:agent"); final long pBegin = noTiming ? 0 : parseTime(getAttr(p, null, "begin", "begin")); final long pEnd = noTiming ? 0 : parseTime(getAttr(p, null, "end", "end")); - ParsedLine pl = processPElement(p, pBegin, pEnd); + final ParsedLine pl = processPElement(p, pBegin, pEnd); if (pl != null && (!pl.text().trim().isEmpty() || !pl.bgLines().isEmpty())) { - List lineSidecar = + final List lineSidecar = findSidecarRoman(lineId, sidecarRoman); // Read off the sidecar before the words carry a reading, or the // line loses the parts no word matched. - String romaText = buildLineRomaji(pl.words(), lineSidecar); + final String romaText = buildLineRomaji(pl.words(), lineSidecar); // The panel draws a reading over the words, not beside them. if (lineSidecar != null && !lineSidecar.isEmpty()) { alignRomajiToWords(pl.words(), lineSidecar); } if (!pl.text().trim().isEmpty()) { - LyricsLine line = new LyricsLine( + final LyricsLine line = new LyricsLine( pl.begin(), pl.end(), pl.text(), pl.words(), agentId, false, false, divSongPart); lines.add(line); } mainLineCount = lines.size(); - String bgAgent = pl.bgAgentId() != null ? pl.bgAgentId() : agentId; + final String bgAgent = pl.bgAgentId() != null ? pl.bgAgentId() : agentId; for (LyricsLine bgSrc : pl.bgLines()) { - LyricsLine bgLine = new LyricsLine( + final LyricsLine bgLine = new LyricsLine( bgSrc.startTimeMs(), bgSrc.endTimeMs(), bgSrc.text(), bgSrc.words(), bgAgent, false, true, divSongPart); @@ -298,10 +276,10 @@ public static TtmlResult parse(String ttml) { if (pl.inlineTranslations() != null) { for (Map.Entry e : pl.inlineTranslations().entrySet()) { - String lang = e.getKey(); - String text = e.getValue(); + final String lang = e.getKey(); + final String text = e.getValue(); if (text.isEmpty()) continue; - List langLines = translations.computeIfAbsent( + final List langLines = translations.computeIfAbsent( lang, k -> new ArrayList<>()); while (langLines.size() < mainLineCount - 1) { langLines.add(new LyricsLine(LyricsLine.NO_TIME, "")); @@ -317,10 +295,10 @@ public static TtmlResult parse(String ttml) { if (pl.inlineRomanizations() != null) { for (Map.Entry e : pl.inlineRomanizations().entrySet()) { - String lang = e.getKey(); - String text = e.getValue(); + final String lang = e.getKey(); + final String text = e.getValue(); if (text.isEmpty()) continue; - List langLines = romanizations.computeIfAbsent( + final List langLines = romanizations.computeIfAbsent( lang, k -> new ArrayList<>()); while (langLines.size() < mainLineCount - 1) { langLines.add(new LyricsLine(LyricsLine.NO_TIME, "")); @@ -331,10 +309,10 @@ public static TtmlResult parse(String ttml) { if (pl.bgInlineTranslations() != null) { for (Map.Entry e : pl.bgInlineTranslations().entrySet()) { - String lang = "bg:" + e.getKey(); - String text = e.getValue(); + final String lang = "bg:" + e.getKey(); + final String text = e.getValue(); if (text.isEmpty()) continue; - List langLines = translations.computeIfAbsent( + final List langLines = translations.computeIfAbsent( lang, k -> new ArrayList<>()); while (langLines.size() < mainLineCount) { langLines.add(new LyricsLine(LyricsLine.NO_TIME, "")); @@ -345,10 +323,10 @@ public static TtmlResult parse(String ttml) { if (pl.bgInlineRomanizations() != null) { for (Map.Entry e : pl.bgInlineRomanizations().entrySet()) { - String lang = "bg:" + e.getKey(); - String text = e.getValue(); + final String lang = "bg:" + e.getKey(); + final String text = e.getValue(); if (text.isEmpty()) continue; - List langLines = romanizations.computeIfAbsent( + final List langLines = romanizations.computeIfAbsent( lang, k -> new ArrayList<>()); while (langLines.size() < mainLineCount) { langLines.add(new LyricsLine(LyricsLine.NO_TIME, "")); @@ -377,16 +355,16 @@ public static TtmlResult parse(String ttml) { padDerivedTo(translations, lines.size()); padDerivedTo(romanizations, lines.size()); - Map> transOut = + final Map> transOut = translations.isEmpty() ? null : translations; - Map> romaOut = + final Map> romaOut = romanizations.isEmpty() ? null : romanizations; - Map agentNames; + final Map agentNames; if (agentTypes.isEmpty()) { agentNames = null; } else { - Map names = new HashMap<>(); + final Map names = new HashMap<>(); for (Map.Entry e : agentTypes.entrySet()) { if (e.getValue().name() != null) { names.put(e.getKey(), e.getValue().name()); @@ -412,8 +390,8 @@ private static void resolveDuet(List lines, Map a boolean lastPersonIsDuet = false; for (int i = 0; i < lines.size(); i++) { - LyricsLine original = lines.get(i); - String agentId = original.agentId(); + final LyricsLine original = lines.get(i); + final String agentId = original.agentId(); if (agentId == null) { continue; } @@ -422,10 +400,10 @@ private static void resolveDuet(List lines, Map a boolean isDuet; if (agentNum > 0) { - isDuet = (agentNum % 2 == 0); + isDuet = !LyricsLine.isCenteredAgentId(agentId) && (agentNum % 2 == 0); } else { - AgentInfo info = agentTypes.get(agentId); - String type = info != null ? info.type() : AGENT_TYPE_PERSON; + final AgentInfo info = agentTypes.get(agentId); + final String type = info != null ? info.type() : AGENT_TYPE_PERSON; if (AGENT_TYPE_GROUP.equals(type)) { isDuet = false; @@ -466,7 +444,7 @@ private static int extractAgentNumber(String agentId) { } private static void collectSidecarTransliterations(XmlPullParser p, - Map> map) + Map> map) throws XmlPullParserException, IOException { int depth = 1; while (depth > 0) { @@ -475,9 +453,9 @@ private static void collectSidecarTransliterations(XmlPullParser p, break; } if (event == XmlPullParser.START_TAG) { - String local = localName(p.getName()); + final String local = localName(p.getName()); if ("transliteration".equals(local)) { - String lang = getAttr(p, NS_XML, "lang", "xml:lang"); + final String lang = getAttr(p, NS_XML, "lang", "xml:lang"); collectOneTransliteration(p, lang, map); continue; } @@ -489,8 +467,8 @@ private static void collectSidecarTransliterations(XmlPullParser p, } private static void collectOneTransliteration(XmlPullParser p, - @Nullable String lang, - Map> map) + @Nullable String lang, + Map> map) throws XmlPullParserException, IOException { int depth = 1; while (depth > 0) { @@ -499,13 +477,13 @@ private static void collectOneTransliteration(XmlPullParser p, break; } if (event == XmlPullParser.START_TAG) { - String local = localName(p.getName()); + final String local = localName(p.getName()); if ("text".equals(local)) { - String forKey = getAttr(p, null, "for", "for"); + final String forKey = getAttr(p, null, "for", "for"); if (forKey != null && !forKey.isEmpty()) { - List syllables = parseTransliterationText(p); + final List syllables = parseTransliterationText(p); if (!syllables.isEmpty()) { - String key = (lang != null && !lang.isEmpty()) + final String key = (lang != null && !lang.isEmpty()) ? lang + ":" + forKey : forKey; map.put(key, syllables); continue; @@ -523,7 +501,7 @@ private static void collectOneTransliteration(XmlPullParser p, private static List parseTransliterationText(XmlPullParser p) throws XmlPullParserException, IOException { - List syllables = new ArrayList<>(); + final List syllables = new ArrayList<>(); int depth = 1; while (depth > 0) { final int event = p.next(); @@ -531,12 +509,12 @@ private static List parseTransliterationText(XmlPullParser p) break; } if (event == XmlPullParser.START_TAG) { - String local = localName(p.getName()); + final String local = localName(p.getName()); if ("span".equals(local)) { - String begin = getAttr(p, null, "begin", "begin"); - String end = getAttr(p, null, "end", "end"); + final String begin = getAttr(p, null, "begin", "begin"); + final String end = getAttr(p, null, "end", "end"); if (begin != null && end != null) { - String text = normalizeText(readTextContent(p)); + final String text = normalizeText(readTextContent(p)); if (!text.isEmpty()) { syllables.add(new RomajiSyllable( parseTime(begin), parseTime(end), text)); @@ -550,9 +528,9 @@ private static List parseTransliterationText(XmlPullParser p) } else if (event == XmlPullParser.END_TAG) { depth--; } else if (event == XmlPullParser.TEXT) { - String text = p.getText(); + final String text = p.getText(); if (text != null) { - String trimmed = normalizeText(text); + final String trimmed = normalizeText(text); if (!trimmed.isEmpty()) { syllables.add(new RomajiSyllable(0, 0, trimmed)); } @@ -563,7 +541,7 @@ private static List parseTransliterationText(XmlPullParser p) } private static void collectSidecarTranslations(XmlPullParser p, - Map map) + Map map) throws XmlPullParserException, IOException { int depth = 1; int eventCount = 0; @@ -575,9 +553,9 @@ private static void collectSidecarTranslations(XmlPullParser p, } if (event == XmlPullParser.START_TAG) { - String local = localName(p.getName()); + final String local = localName(p.getName()); if ("translation".equals(local)) { - String lang = getAttr(p, NS_XML, "lang", "xml:lang"); + final String lang = getAttr(p, NS_XML, "lang", "xml:lang"); if (lang != null && !lang.isEmpty()) { collectOneTranslation(p, lang, map); continue; @@ -593,7 +571,7 @@ private static void collectSidecarTranslations(XmlPullParser p, } private static void collectOneTranslation(XmlPullParser p, String lang, - Map map) + Map map) throws XmlPullParserException, IOException { int depth = 1; while (depth > 0) { @@ -602,11 +580,11 @@ private static void collectOneTranslation(XmlPullParser p, String lang, break; } if (event == XmlPullParser.START_TAG) { - String local = localName(p.getName()); + final String local = localName(p.getName()); if ("text".equals(local)) { - String forKey = getAttr(p, null, "for", "for"); + final String forKey = getAttr(p, null, "for", "for"); if (forKey != null && !forKey.isEmpty()) { - SidecarTranslation st = parseTranslationText(p); + final SidecarTranslation st = parseTranslationText(p); if (!st.text().isEmpty()) { map.put(lang + ":" + forKey, st); } @@ -624,19 +602,19 @@ private static void collectOneTranslation(XmlPullParser p, String lang, private static SidecarTranslation parseTranslationText(XmlPullParser p) throws XmlPullParserException, IOException { - StringBuilder mainFullText = new StringBuilder(); - List mainWords = new ArrayList<>(); - StringBuilder bgFullText = new StringBuilder(); - List bgWords = new ArrayList<>(); + final StringBuilder mainFullText = new StringBuilder(); + final List mainWords = new ArrayList<>(); + final StringBuilder bgFullText = new StringBuilder(); + final List bgWords = new ArrayList<>(); boolean inBg = false; int bgWrapperDepth = 0; boolean inMainWord = false; - StringBuilder mainWordBuf = new StringBuilder(); + final StringBuilder mainWordBuf = new StringBuilder(); long mainWordBegin = 0, mainWordEnd = 0; boolean inBgWord = false; - StringBuilder bgWordBuf = new StringBuilder(); + final StringBuilder bgWordBuf = new StringBuilder(); long bgWordBegin = 0, bgWordEnd = 0; int depth = 1; @@ -648,14 +626,14 @@ private static SidecarTranslation parseTranslationText(XmlPullParser p) } if (event == XmlPullParser.START_TAG) { depth++; - String local = localName(p.getName()); - String role = getAttr(p, NS_TTM, "role", "ttm:role"); + final String local = localName(p.getName()); + final String role = getAttr(p, NS_TTM, "role", "ttm:role"); if ("x-bg".equals(role)) { inBg = true; } else if ("span".equals(local)) { - String beginAttr = getAttr(p, null, "begin", "begin"); - String endAttr = getAttr(p, null, "end", "end"); + final String beginAttr = getAttr(p, null, "begin", "begin"); + final String endAttr = getAttr(p, null, "end", "end"); if (beginAttr != null && endAttr != null) { if (inBg) { inBgWord = true; @@ -681,15 +659,15 @@ private static SidecarTranslation parseTranslationText(XmlPullParser p) if (depth <= 1) { if (inBgWord) { inBgWord = false; - String normalized = normalizeTextRaw(bgWordBuf.toString()); + final String normalized = normalizeTextRaw(bgWordBuf.toString()); final boolean startsWithSpace = !normalized.isEmpty() && Character.isWhitespace(normalized.charAt(0)); final boolean endsWithSpace = !normalized.isEmpty() && Character.isWhitespace(normalized.charAt(normalized.length() - 1)); - String text = normalized.trim(); + final String text = normalized.trim(); bgFullText.append(normalized); if (startsWithSpace && !bgWords.isEmpty()) { - Word prev = bgWords.get(bgWords.size() - 1); + final Word prev = bgWords.get(bgWords.size() - 1); bgWords.set(bgWords.size() - 1, new Word(prev.startMs(), prev.endMs(), prev.text(), prev.romaji(), true)); @@ -700,15 +678,15 @@ private static SidecarTranslation parseTranslationText(XmlPullParser p) } } else if (inMainWord) { inMainWord = false; - String normalized = normalizeTextRaw(mainWordBuf.toString()); + final String normalized = normalizeTextRaw(mainWordBuf.toString()); final boolean startsWithSpace = !normalized.isEmpty() && Character.isWhitespace(normalized.charAt(0)); final boolean endsWithSpace = !normalized.isEmpty() && Character.isWhitespace(normalized.charAt(normalized.length() - 1)); - String text = normalized.trim(); + final String text = normalized.trim(); mainFullText.append(normalized); if (startsWithSpace && !mainWords.isEmpty()) { - Word prev = mainWords.get(mainWords.size() - 1); + final Word prev = mainWords.get(mainWords.size() - 1); mainWords.set(mainWords.size() - 1, new Word(prev.startMs(), prev.endMs(), prev.text(), prev.romaji(), true)); @@ -724,7 +702,7 @@ private static SidecarTranslation parseTranslationText(XmlPullParser p) } } } else if (event == XmlPullParser.TEXT) { - String raw = p.getText(); + final String raw = p.getText(); if (raw == null || raw.isEmpty()) continue; if (inBgWord) { bgWordBuf.append(raw); @@ -734,7 +712,7 @@ private static SidecarTranslation parseTranslationText(XmlPullParser p) bgFullText.append(raw); // Whitespace between bg word spans if (!raw.contains("\n") && raw.trim().isEmpty() && !bgWords.isEmpty()) { - Word prev = bgWords.get(bgWords.size() - 1); + final Word prev = bgWords.get(bgWords.size() - 1); if (!prev.endsWithSpace()) { bgWords.set(bgWords.size() - 1, new Word( prev.startMs(), prev.endMs(), @@ -745,7 +723,7 @@ private static SidecarTranslation parseTranslationText(XmlPullParser p) mainFullText.append(raw); // Whitespace between main word spans if (!raw.contains("\n") && raw.trim().isEmpty() && !mainWords.isEmpty()) { - Word prev = mainWords.get(mainWords.size() - 1); + final Word prev = mainWords.get(mainWords.size() - 1); if (!prev.endsWithSpace()) { mainWords.set(mainWords.size() - 1, new Word( prev.startMs(), prev.endMs(), @@ -756,17 +734,17 @@ private static SidecarTranslation parseTranslationText(XmlPullParser p) } } - String main = normalizeText(mainFullText.toString()); - String bg = normalizeText(bgFullText.toString()); + final String main = normalizeText(mainFullText.toString()); + final String bg = normalizeText(bgFullText.toString()); if (!mainWords.isEmpty()) { - Word first = mainWords.get(0); + final Word first = mainWords.get(0); if (first.text().startsWith(" ")) { mainWords.set(0, new Word(first.startMs(), first.endMs(), first.text().substring(1).trim(), first.romaji(), first.endsWithSpace())); } final int lastIdx = mainWords.size() - 1; - Word last = mainWords.get(lastIdx); + final Word last = mainWords.get(lastIdx); if (last.text().endsWith(" ") || last.endsWithSpace()) { mainWords.set(lastIdx, new Word(last.startMs(), last.endMs(), last.text().trim(), last.romaji(), false)); @@ -774,23 +752,23 @@ private static SidecarTranslation parseTranslationText(XmlPullParser p) } if (!bgWords.isEmpty()) { - Word first = bgWords.get(0); + final Word first = bgWords.get(0); if (first.text().startsWith(" ")) { bgWords.set(0, new Word(first.startMs(), first.endMs(), first.text().substring(1).trim(), first.romaji(), first.endsWithSpace())); } final int lastIdx = bgWords.size() - 1; - Word last = bgWords.get(lastIdx); + final Word last = bgWords.get(lastIdx); if (last.text().endsWith(" ") || last.endsWithSpace()) { bgWords.set(lastIdx, new Word(last.startMs(), last.endMs(), last.text().trim(), last.romaji(), false)); } } - List finalMainWords = (mainWords.size() == 1 + final List finalMainWords = (mainWords.size() == 1 && mainWords.get(0).startMs() == 0 && mainWords.get(0).endMs() == 0) ? null : (mainWords.isEmpty() ? null : mainWords); - List finalBgWords = (bgWords.size() == 1 + final List finalBgWords = (bgWords.size() == 1 && bgWords.get(0).startMs() == 0 && bgWords.get(0).endMs() == 0) ? null : (bgWords.isEmpty() ? null : bgWords); @@ -798,60 +776,52 @@ private static SidecarTranslation parseTranslationText(XmlPullParser p) bg.isEmpty() ? null : bg, finalBgWords); } - private record ParsedLine(long begin, long end, String text, List words, - List bgLines, - @Nullable String bgAgentId, - @Nullable Map inlineTranslations, - @Nullable Map inlineRomanizations, - @Nullable Map bgInlineTranslations, - @Nullable Map bgInlineRomanizations) {} - private static ParsedLine processPElement(XmlPullParser p, long pBegin, long pEnd) throws XmlPullParserException, IOException { - List words = new ArrayList<>(); - StringBuilder fullText = new StringBuilder(); + final List words = new ArrayList<>(); + final StringBuilder fullText = new StringBuilder(); boolean inTranslation = false; - StringBuilder transBuf = new StringBuilder(); + final StringBuilder transBuf = new StringBuilder(); String transLang = null; boolean inRoman = false; - StringBuilder romanBuf = new StringBuilder(); + final StringBuilder romanBuf = new StringBuilder(); String romanLang = null; - Map inlineTrans = new HashMap<>(); - Map inlineRoma = new HashMap<>(); - Map bgInlineTrans = new HashMap<>(); - Map bgInlineRoma = new HashMap<>(); + final Map inlineTrans = new HashMap<>(); + final Map inlineRoma = new HashMap<>(); + final Map bgInlineTrans = new HashMap<>(); + final Map bgInlineRoma = new HashMap<>(); boolean inBg = false; - List bgWords = new ArrayList<>(); - StringBuilder bgFullText = new StringBuilder(); - StringBuilder bgWordBuf = new StringBuilder(); + final List bgWords = new ArrayList<>(); + final StringBuilder bgFullText = new StringBuilder(); + final StringBuilder bgWordBuf = new StringBuilder(); long bgWordBegin = 0, bgWordEnd = 0; int bgWrapperDepth = 0; String bgAgentId = null; long bgBeginMs = 0, bgEndMs = 0; - StringBuilder bgTransBuf = new StringBuilder(); - StringBuilder bgRomanBuf = new StringBuilder(); + final StringBuilder bgTransBuf = new StringBuilder(); + final StringBuilder bgRomanBuf = new StringBuilder(); boolean inBgTranslation = false; boolean inBgRoman = false; boolean inBgWord = false; - List bgLines = new ArrayList<>(); + final List bgLines = new ArrayList<>(); boolean inWord = false; - StringBuilder wordBuf = new StringBuilder(); + final StringBuilder wordBuf = new StringBuilder(); long wordBegin = 0, wordEnd = 0; int wrapperDepth = 0; boolean inRubyContainer = false; boolean inRubyBase = false; - StringBuilder rubyBaseBuf = new StringBuilder(); + final StringBuilder rubyBaseBuf = new StringBuilder(); boolean inRubyTextContainer = false; boolean inRubyText = false; - StringBuilder rubyTextBuf = new StringBuilder(); + final StringBuilder rubyTextBuf = new StringBuilder(); long rubyTextBegin = 0, rubyTextEnd = 0; - List rubyTags = new ArrayList<>(); + final List rubyTags = new ArrayList<>(); int depth = 1; @@ -862,15 +832,15 @@ private static ParsedLine processPElement(XmlPullParser p, long pBegin, long pEn } if (event == XmlPullParser.START_TAG) { depth++; - String local = localName(p.getName()); + final String local = localName(p.getName()); if (!"span".equals(local)) { continue; } - String role = getAttr(p, NS_TTM, "role", "ttm:role"); - String ruby = getAttr(p, NS_TTS, "ruby", "tts:ruby"); - String beginAttr = getAttr(p, null, "begin", "begin"); - String endAttr = getAttr(p, null, "end", "end"); + final String role = getAttr(p, NS_TTM, "role", "ttm:role"); + final String ruby = getAttr(p, NS_TTS, "ruby", "tts:ruby"); + final String beginAttr = getAttr(p, null, "begin", "begin"); + final String endAttr = getAttr(p, null, "end", "end"); // Ruby is settled first: an annotation sits inside the vocal it reads, // so the background branch below would drop its base text. @@ -928,34 +898,34 @@ private static ParsedLine processPElement(XmlPullParser p, long pBegin, long pEn wrapperDepth++; } } else if (event == XmlPullParser.END_TAG) { - String local = localName(p.getName()); + final String local = localName(p.getName()); if ("span".equals(local)) { if (inBgTranslation) { inBgTranslation = false; - String btText = bgTransBuf.toString().trim(); + final String btText = bgTransBuf.toString().trim(); if (!btText.isEmpty()) { bgInlineTrans.merge("bg", btText, (a, b) -> a + " " + b); } } else if (inBgRoman) { inBgRoman = false; - String brText = bgRomanBuf.toString().trim(); + final String brText = bgRomanBuf.toString().trim(); if (!brText.isEmpty()) { bgInlineRoma.merge("bg", brText, (a, b) -> a + " " + b); } } else if (inBgWord) { inBgWord = false; - String rawBgWord = bgWordBuf.toString(); - String normalized = normalizeTextRaw(rawBgWord); + final String rawBgWord = bgWordBuf.toString(); + final String normalized = normalizeTextRaw(rawBgWord); final boolean isFormatting = rawBgWord.contains("\n"); final boolean startsWithSpace = !isFormatting && !normalized.isEmpty() && Character.isWhitespace(normalized.charAt(0)); final boolean endsWithSpace = !isFormatting && !normalized.isEmpty() && Character.isWhitespace(normalized.charAt(normalized.length() - 1)); - String text = normalized.trim(); + final String text = normalized.trim(); bgFullText.append(normalized); if (startsWithSpace && !bgWords.isEmpty()) { - Word prev = bgWords.get(bgWords.size() - 1); + final Word prev = bgWords.get(bgWords.size() - 1); bgWords.set(bgWords.size() - 1, new Word(prev.startMs(), prev.endMs(), prev.text(), prev.romaji(), true)); @@ -971,7 +941,7 @@ private static ParsedLine processPElement(XmlPullParser p, long pBegin, long pEn inBg = false; } else if (inRubyText && inRubyContainer) { inRubyText = false; - String text = normalizeText(rubyTextBuf.toString()); + final String text = normalizeText(rubyTextBuf.toString()); if (!text.isEmpty()) { rubyTags.add(new RomajiSyllable( rubyTextBegin, rubyTextEnd, text)); @@ -982,7 +952,7 @@ private static ParsedLine processPElement(XmlPullParser p, long pBegin, long pEn inRubyBase = false; } else if (inRubyContainer) { inRubyContainer = false; - String baseText = normalizeText(rubyBaseBuf.toString()); + final String baseText = normalizeText(rubyBaseBuf.toString()); if (!baseText.isEmpty()) { long rBegin = 0, rEnd = 0; if (!rubyTags.isEmpty()) { @@ -996,7 +966,7 @@ private static ParsedLine processPElement(XmlPullParser p, long pBegin, long pEn rRoma.append(rs.text()); } //noinspection SizeReplaceableByIsEmpty - String romaji = rRoma.length() > 0 ? rRoma.toString() : null; + final String romaji = rRoma.length() > 0 ? rRoma.toString() : null; // The base is the sung text either way, only the line differs. if (inBg) { @@ -1017,29 +987,29 @@ private static ParsedLine processPElement(XmlPullParser p, long pBegin, long pEn inBg = false; } else if (inTranslation) { inTranslation = false; - String tText = transBuf.toString().trim(); + final String tText = transBuf.toString().trim(); if (!tText.isEmpty() && transLang != null) { inlineTrans.merge(transLang, tText, (a, b) -> a + " " + b); } } else if (inRoman) { inRoman = false; - String rText = romanBuf.toString().trim(); + final String rText = romanBuf.toString().trim(); if (!rText.isEmpty() && romanLang != null) { inlineRoma.merge(romanLang, rText, (a, b) -> a + " " + b); } } else if (inWord) { inWord = false; - String rawWord = wordBuf.toString(); - String normalized = normalizeTextRaw(rawWord); + final String rawWord = wordBuf.toString(); + final String normalized = normalizeTextRaw(rawWord); final boolean isFormatting = rawWord.contains("\n"); final boolean startsWithSpace = !isFormatting && !normalized.isEmpty() && Character.isWhitespace(normalized.charAt(0)); final boolean endsWithSpace = !isFormatting && !normalized.isEmpty() && Character.isWhitespace(normalized.charAt(normalized.length() - 1)); - String text = normalized.trim(); + final String text = normalized.trim(); fullText.append(normalized); if (startsWithSpace && !words.isEmpty()) { - Word prev = words.get(words.size() - 1); + final Word prev = words.get(words.size() - 1); words.set(words.size() - 1, new Word(prev.startMs(), prev.endMs(), prev.text(), prev.romaji(), true)); @@ -1054,7 +1024,7 @@ private static ParsedLine processPElement(XmlPullParser p, long pBegin, long pEn } depth--; } else if (event == XmlPullParser.TEXT) { - String raw = p.getText(); + final String raw = p.getText(); if (raw == null || raw.isEmpty()) continue; final boolean isWhitespace = raw.trim().isEmpty(); final boolean hasNewline = raw.contains("\n"); @@ -1072,7 +1042,7 @@ private static ParsedLine processPElement(XmlPullParser p, long pBegin, long pEn } else if (inBg && bgWrapperDepth == 0) { if (isWhitespace) { if (!bgWords.isEmpty()) { - Word prev = bgWords.get(bgWords.size() - 1); + final Word prev = bgWords.get(bgWords.size() - 1); final boolean cjk = isContinuousCjk(prev.text()); if (!hasNewline || !cjk) { if (bgFullText.length() > 0 && bgFullText.charAt(bgFullText.length() - 1) != ' ') { @@ -1097,7 +1067,7 @@ private static ParsedLine processPElement(XmlPullParser p, long pBegin, long pEn } else if (!inBg && !inRubyContainer) { if (isWhitespace) { if (!words.isEmpty()) { - Word prev = words.get(words.size() - 1); + final Word prev = words.get(words.size() - 1); final boolean cjk = isContinuousCjk(prev.text()); if (!hasNewline || !cjk) { if (fullText.length() > 0 && fullText.charAt(fullText.length() - 1) != ' ') { @@ -1117,12 +1087,12 @@ private static ParsedLine processPElement(XmlPullParser p, long pBegin, long pEn } } - String rawLineText = normalizeText(fullText.toString()); + final String rawLineText = normalizeText(fullText.toString()); String lineText = rawLineText; if (lineText.indexOf(' ') < 0 && words.size() > 1 && !isContinuousCjk(words.get(0).text())) { StringBuilder reconstructed = new StringBuilder(); for (int i = 0; i < words.size(); i++) { - Word w = words.get(i); + final Word w = words.get(i); if (i > 0) reconstructed.append(' '); reconstructed.append(w.text()); final boolean shouldEndWithSpace = (i < words.size() - 1); @@ -1172,13 +1142,13 @@ private static ParsedLine processPElement(XmlPullParser p, long pBegin, long pEn } if (!words.isEmpty()) { - Word first = words.get(0); + final Word first = words.get(0); if (first.text().startsWith(" ")) { words.set(0, new Word(first.startMs(), first.endMs(), first.text().substring(1).trim(), first.romaji(), first.endsWithSpace())); } final int lastIdx = words.size() - 1; - Word last = words.get(lastIdx); + final Word last = words.get(lastIdx); if (last.text().endsWith(" ") || last.endsWithSpace()) { words.set(lastIdx, new Word(last.startMs(), last.endMs(), last.text().trim(), last.romaji(), false)); @@ -1221,7 +1191,7 @@ private static void flushBgLine( if (cleanBgText.indexOf(' ') < 0 && bgWords.size() > 1 && !isContinuousCjk(bgWords.get(0).text())) { StringBuilder reconstructed = new StringBuilder(); for (int i = 0; i < bgWords.size(); i++) { - Word w = bgWords.get(i); + final Word w = bgWords.get(i); if (i > 0) reconstructed.append(' '); reconstructed.append(w.text()); final boolean shouldEndWithSpace = (i < bgWords.size() - 1); @@ -1243,16 +1213,16 @@ private static void flushBgLine( private static void stripBgWordParens(List bgWords) { if (bgWords.isEmpty()) return; - Word first = bgWords.get(0); - String strippedFirst = TextTrim.stripLeading( + final Word first = bgWords.get(0); + final String strippedFirst = TextTrim.stripLeading( first.text().replaceAll("^[((]+", "")); if (!strippedFirst.equals(first.text())) { bgWords.set(0, new Word(first.startMs(), first.endMs(), strippedFirst, first.romaji(), first.endsWithSpace())); } final int lastIdx = bgWords.size() - 1; - Word last = bgWords.get(lastIdx); - String strippedLast = TextTrim.stripTrailing( + final Word last = bgWords.get(lastIdx); + final String strippedLast = TextTrim.stripTrailing( last.text().replaceAll("[))]+$", "")); if (!strippedLast.equals(last.text())) { bgWords.set(lastIdx, new Word(last.startMs(), last.endMs(), @@ -1274,7 +1244,7 @@ public static boolean isContinuousCjk(@Nullable String s) { } private static void alignRomajiToWords(List words, - List sidecar) { + List sidecar) { if (words.isEmpty() || sidecar.isEmpty()) { return; } @@ -1282,14 +1252,14 @@ private static void alignRomajiToWords(List words, int romanSearchStart = 0; for (int i = 0; i < words.size(); i++) { - Word main = words.get(i); + final Word main = words.get(i); double maxIou = 0; int bestIdx = -1; boolean fastMatched = false; int j = romanSearchStart; while (j < sidecar.size()) { - RomajiSyllable sub = sidecar.get(j); + final RomajiSyllable sub = sidecar.get(j); // Fast track: start times match within 2ms if (Math.abs(main.startMs() - sub.startMs()) <= FAST_TRACK_MS) { @@ -1322,7 +1292,7 @@ private static void alignRomajiToWords(List words, } if (!fastMatched && bestIdx >= 0 && maxIou >= MIN_IOU) { - RomajiSyllable sub = sidecar.get(bestIdx); + final RomajiSyllable sub = sidecar.get(bestIdx); words.set(i, new Word(main.startMs(), main.endMs(), main.text(), sub.text(), main.endsWithSpace())); romanSearchStart = bestIdx + 1; @@ -1332,11 +1302,11 @@ private static void alignRomajiToWords(List words, @Nullable private static List findSidecarRoman(String lineId, - Map> sidecarRoman) { - List exact = sidecarRoman.get(lineId); + Map> sidecarRoman) { + final List exact = sidecarRoman.get(lineId); if (exact != null) return exact; for (Map.Entry> e : sidecarRoman.entrySet()) { - String key = e.getKey(); + final String key = e.getKey(); final int sep = key.indexOf(':'); if (sep > 0 && key.substring(sep + 1).equals(lineId)) { return e.getValue(); @@ -1348,18 +1318,18 @@ private static List findSidecarRoman(String lineId, private static String buildLineRomaji(List words, @Nullable List sidecar) { if (sidecar != null && !sidecar.isEmpty()) { - StringBuilder sb = new StringBuilder(); + final StringBuilder sb = new StringBuilder(); for (RomajiSyllable s : sidecar) { if (s.text().isEmpty()) continue; sb.append(s.text()); } - String result = sb.toString().trim(); + final String result = sb.toString().trim(); if (!result.isEmpty() && LyricsRomanizer.isPurelyLatinScript(result)) { return result; } } - StringBuilder perWord = new StringBuilder(); + final StringBuilder perWord = new StringBuilder(); boolean hasPerWord = false; for (Word w : words) { if (w.romaji() != null && !w.romaji().isEmpty() && LyricsRomanizer.isPurelyLatinScript(w.romaji())) { @@ -1386,15 +1356,15 @@ private static String buildLineRomaji(List words, } private static void buildRomanizations(String lineId, - Map> sidecarRoman, - List words, - Map> romanizations, - int mainIndex, int bgCount) { + Map> sidecarRoman, + List words, + Map> romanizations, + int mainIndex, int bgCount) { // Keys may be "lang:lineId" or just "lineId" (no language) for (Map.Entry> entry : sidecarRoman.entrySet()) { - String key = entry.getKey(); - String forId; - String lang; + final String key = entry.getKey(); + final String forId; + final String lang; final int sep = key.indexOf(':'); if (sep > 0) { @@ -1407,17 +1377,17 @@ private static void buildRomanizations(String lineId, if (!lineId.equals(forId)) continue; - List sidecar = entry.getValue(); + final List sidecar = entry.getValue(); if (sidecar.isEmpty()) continue; if (sidecar.size() == 1 && sidecar.get(0).startMs() == 0 && sidecar.get(0).endMs() == 0) { continue; } - List alignedWords = new ArrayList<>(words); + final List alignedWords = new ArrayList<>(words); alignRomajiToWords(alignedWords, sidecar); - StringBuilder sb = new StringBuilder(); + final StringBuilder sb = new StringBuilder(); for (Word w : alignedWords) { if (w.romaji() != null && !w.romaji().isEmpty()) { sb.append(w.romaji()); @@ -1426,9 +1396,9 @@ private static void buildRomanizations(String lineId, } } } - String text = sb.toString().trim(); - String langKey = lang != null ? lang : "romaji"; - List langLines = romanizations.computeIfAbsent( + final String text = sb.toString().trim(); + final String langKey = lang != null ? lang : "romaji"; + final List langLines = romanizations.computeIfAbsent( langKey, k -> new ArrayList<>()); while (langLines.size() < mainIndex) { langLines.add(new LyricsLine(LyricsLine.NO_TIME, "")); @@ -1453,21 +1423,21 @@ private static void padDerivedTo(Map> derived, int size } private static void buildTranslations(String lineId, - Map sidecarTrans, - Map> translations, int mainIndex, int bgCount) { + Map sidecarTrans, + Map> translations, int mainIndex, int bgCount) { - Map mainByLang = new HashMap<>(); - Map bgByLang = new HashMap<>(); + final Map mainByLang = new HashMap<>(); + final Map bgByLang = new HashMap<>(); for (Map.Entry entry : sidecarTrans.entrySet()) { - String key = entry.getKey(); + final String key = entry.getKey(); final int sep = key.indexOf(':'); if (sep < 0) continue; - String lang = key.substring(0, sep); - String forId = key.substring(sep + 1); + final String lang = key.substring(0, sep); + final String forId = key.substring(sep + 1); if (!lineId.equals(forId)) continue; - SidecarTranslation st = entry.getValue(); + final SidecarTranslation st = entry.getValue(); mainByLang.put(lang, st); if (st.bgText() != null) { bgByLang.put(lang, st); @@ -1475,9 +1445,9 @@ private static void buildTranslations(String lineId, } for (Map.Entry entry : mainByLang.entrySet()) { - String lang = entry.getKey(); - SidecarTranslation st = entry.getValue(); - List langLines = translations.computeIfAbsent( + final String lang = entry.getKey(); + final SidecarTranslation st = entry.getValue(); + final List langLines = translations.computeIfAbsent( lang, k -> new ArrayList<>()); while (langLines.size() < mainIndex) { langLines.add(new LyricsLine(LyricsLine.NO_TIME, "")); @@ -1488,16 +1458,16 @@ private static void buildTranslations(String lineId, } for (Map.Entry entry : bgByLang.entrySet()) { - String lang = "bg:" + entry.getKey(); - SidecarTranslation st = entry.getValue(); - List langLines = translations.computeIfAbsent( + final String lang = "bg:" + entry.getKey(); + final SidecarTranslation st = entry.getValue(); + final List langLines = translations.computeIfAbsent( lang, k -> new ArrayList<>()); final int firstBg = mainIndex + 1; while (langLines.size() < firstBg) { langLines.add(new LyricsLine(LyricsLine.NO_TIME, "")); } if (langLines.size() == firstBg) { - String bgText = st.bgText() == null ? "" : st.bgText(); + final String bgText = st.bgText() == null ? "" : st.bgText(); langLines.add(bgText.isEmpty() ? new LyricsLine(LyricsLine.NO_TIME, "") : new LyricsLine(LyricsLine.NO_TIME, bgText, st.bgWords())); @@ -1509,7 +1479,7 @@ private static void buildTranslations(String lineId, private static String readTextContent(XmlPullParser p) throws XmlPullParserException, IOException { - StringBuilder sb = new StringBuilder(); + final StringBuilder sb = new StringBuilder(); int depth = 1; while (depth > 0) { final int event = p.next(); @@ -1521,7 +1491,7 @@ private static String readTextContent(XmlPullParser p) } else if (event == XmlPullParser.END_TAG) { depth--; } else if (event == XmlPullParser.TEXT) { - String text = p.getText(); + final String text = p.getText(); if (text != null) { sb.append(text); } @@ -1548,13 +1518,13 @@ private static void skipElement(XmlPullParser p) public static long parseTime(@Nullable String raw) { if (raw == null || raw.isEmpty()) return 0; - String trimmed = raw.trim(); + final String trimmed = raw.trim(); // Try colon-separated format first: HH:MM:SS.mmm or MM:SS.mmm - Matcher cm = TIME_COLON.matcher(trimmed); + final Matcher cm = TIME_COLON.matcher(trimmed); if (cm.matches()) { - String g1 = cm.group(1); // hours (HH:MM:SS) or minutes (MM:SS) - String g2 = cm.group(2); // minutes (HH:MM:SS only) + final String g1 = cm.group(1); // hours (HH:MM:SS) or minutes (MM:SS) + final String g2 = cm.group(2); // minutes (HH:MM:SS only) // Seconds are the only mandatory group of TIME_COLON. final double sec = Double.parseDouble(Objects.requireNonNull(cm.group(3))); if (g1 != null && g2 != null) { @@ -1570,7 +1540,7 @@ public static long parseTime(@Nullable String raw) { } // Try unit-suffixed format: 3.5s, 100ms, etc. - Matcher m = TIME_UNIT.matcher(trimmed); + final Matcher m = TIME_UNIT.matcher(trimmed); if (m.find()) { final double value = Double.parseDouble(Objects.requireNonNull(m.group(1))); return switch (Objects.requireNonNull(m.group(2))) { @@ -1603,21 +1573,43 @@ private static String localName(String name) { return colon >= 0 ? name.substring(colon + 1) : name; } + private static boolean isMetaNamespace(XmlPullParser p) { + final String ns = p.getNamespace(); + if (NS_AMLL.equals(ns) || NS_COMPOSER.equals(ns) || NS_COMPOSER_LEGACY.equals(ns)) { + return true; + } + final String raw = p.getName(); + return raw != null && raw.startsWith("composer:"); + } + + private static void collectMetaCredit(XmlPullParser p, List credits) { + final String ns = p.getNamespace(); + final String key = getAttr(p, ns, "key", "key"); + final String value = getAttr(p, ns, "value", "value"); + if (key == null || value == null || key.isEmpty() || value.isEmpty()) { + return; + } + final Set excluded = NS_AMLL.equals(ns) ? AMLL_EXCLUDED_KEYS : COMPOSER_EXCLUDED_KEYS; + if (!excluded.contains(key)) { + credits.add(key + ": " + value.trim()); + } + } + private static String getAttr(XmlPullParser p, @Nullable String ns, - String local, @Nullable String fallbackQName) { + String local, @Nullable String fallbackQName) { if (ns != null) { - String val = p.getAttributeValue(ns, local); + final String val = p.getAttributeValue(ns, local); if (val != null) return val; } if (fallbackQName != null) { - String val = p.getAttributeValue(null, fallbackQName); + final String val = p.getAttributeValue(null, fallbackQName); if (val != null) return val; } for (int i = 0, n = p.getAttributeCount(); i < n; i++) { - String name = p.getAttributeName(i); + final String name = p.getAttributeName(i); if (name != null) { final int colon = name.indexOf(':'); - String attrLocal = colon >= 0 ? name.substring(colon + 1) : name; + final String attrLocal = colon >= 0 ? name.substring(colon + 1) : name; if (attrLocal.equals(local)) { return p.getAttributeValue(i); } @@ -1628,7 +1620,7 @@ private static String getAttr(XmlPullParser p, @Nullable String ns, @Nullable public static Lyrics ttmlToLyrics(String ttml, String providerName, - @Nullable String sourceUrl) { + @Nullable String sourceUrl) { if (ttml == null || ttml.isEmpty()) return null; TtmlResult result = parse(ttml); if (result == null || result.lines().isEmpty()) return null; @@ -1643,7 +1635,7 @@ public static Lyrics ttmlToLyrics(String ttml, String providerName, @Nullable private static List combineCredits(@Nullable List existing, - @Nullable List amllCredits) { + @Nullable List amllCredits) { if (existing == null && amllCredits == null) { return null; } @@ -1657,4 +1649,40 @@ private static List combineCredits(@Nullable List existing, combined.addAll(amllCredits); return combined; } + + private record HeadMetadata( + List songwriters, + List amllCredits, + Map agentTypes, + Map> sidecarRoman, + Map sidecarTrans) { + } + + public record TtmlResult(List lines, + @Nullable List romanization, + @Nullable Map> translations, + @Nullable Map> romanizations, + @Nullable List songwriters, + @Nullable List amllCreditLines, + @Nullable Map agentNames) { + } + + private record RomajiSyllable(long startMs, long endMs, String text) { + } + + private record AgentInfo(String type, @Nullable String name) { + } + + private record SidecarTranslation(String text, @Nullable List words, + @Nullable String bgText, @Nullable List bgWords) { + } + + private record ParsedLine(long begin, long end, String text, List words, + List bgLines, + @Nullable String bgAgentId, + @Nullable Map inlineTranslations, + @Nullable Map inlineRomanizations, + @Nullable Map bgInlineTranslations, + @Nullable Map bgInlineRomanizations) { + } } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/AMLLProvider.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/AMLLProvider.java index 496b1cb47f..af3933bbb3 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/AMLLProvider.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/AMLLProvider.java @@ -29,6 +29,29 @@ public final class AMLLProvider implements LyricsProvider { private static final AtomicLong lastRequestTime = new AtomicLong(0); + @Nullable + private static String sourceUrl(JSONObject getData, JSONObject best) { + String file = firstPath(getData); + if (file == null) { + file = firstPath(best); + } + if (file == null || file.isEmpty()) { + return null; + } + return "https://amlldb.bikonoo.com/view-lyric.html?file=" + file; + } + + @Nullable + private static String firstPath(JSONObject object) { + for (String key : new String[]{"path", "file", "filename", "filePath"}) { + final String value = LyricsRequests.optString(object, key); + if (value != null) { + return value; + } + } + return null; + } + @Override public String name() { return "AMLL"; @@ -41,7 +64,7 @@ public FetchResult fetch(TrackInfo track) throws Exception { return null; } - JSONObject best = searchBest(track.title(), track.artist(), track.album()); + final JSONObject best = searchBest(track.title(), track.artist(), track.album()); if (best == null) { return null; } @@ -80,29 +103,6 @@ public FetchResult fetch(TrackInfo track) throws Exception { } } - @Nullable - private static String sourceUrl(JSONObject getData, JSONObject best) { - String file = firstPath(getData); - if (file == null) { - file = firstPath(best); - } - if (file == null || file.isEmpty()) { - return null; - } - return "https://amlldb.bikonoo.com/view-lyric.html?file=" + file; - } - - @Nullable - private static String firstPath(JSONObject object) { - for (String key : new String[]{"path", "file", "filename", "filePath"}) { - String value = LyricsRequests.optString(object, key); - if (value != null) { - return value; - } - } - return null; - } - @Nullable private JSONObject searchBest(String title, String artist, String album) { HttpURLConnection searchConnection = null; diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/AppleMusicProvider.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/AppleMusicProvider.java index 3d9e48db59..742716cdf4 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/AppleMusicProvider.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/AppleMusicProvider.java @@ -58,7 +58,331 @@ public final class AppleMusicProvider implements LyricsProvider { @GuardedBy("TOKEN_LOCK") private static String[] cachedSupportedLanguages; - private record ResolvedContext(String userToken, String storefront, String language) {} + private static String titleFromSong(JSONObject song) { + JSONObject attributes = song.optJSONObject("attributes"); + return attributes != null ? attributes.optString("name", "") : ""; + } + + private static String artistFromSong(JSONObject song) { + JSONObject attributes = song.optJSONObject("attributes"); + return attributes != null ? attributes.optString("artistName", "") : ""; + } + + private static long durationFromSong(JSONObject song) { + JSONObject attributes = song.optJSONObject("attributes"); + if (attributes == null) return 0; + final long durationMs = attributes.optLong("durationInMillis", 0); + return durationMs > 0 ? durationMs / 1000 : 0; + } + + private static int scoreCandidate(JSONObject item, TrackInfo track) { + JSONObject attributes = item.optJSONObject("attributes"); + if (attributes == null) { + return 0; + } + String title = attributes.optString("name", ""); + String artist = attributes.optString("artistName", ""); + String album = attributes.optString("collectionName", ""); + final long durationMs = attributes.optLong("durationInMillis", 0); + return LyricsRequests.scoreTrackCandidate(title, artist, + durationMs > 0 ? durationMs / 1000 : 0, album, track); + } + + public static boolean validateToken(String userToken) { + synchronized (TOKEN_LOCK) { + if (cachedDevToken == null) { + cachedDevToken = fetchDevToken(); + } + } + if (cachedDevToken == null) { + return false; + } + + HttpURLConnection connection = null; + try { + connection = openApi("https://amp-api.music.apple.com/v1/me/storefront", userToken); + final int code = connection.getResponseCode(); + if (code != 200) { + return false; + } + JSONObject root = LyricsRequests.parseGzipJsonObject(connection); + JSONArray data = root.optJSONArray("data"); + return data != null && data.length() > 0; + } catch (Exception ex) { + Logger.printDebug(() -> "Could not validate Apple Music user token", ex); + return false; + } finally { + if (connection != null) { + connection.disconnect(); + } + } + } + + @Nullable + private static String fetchDevToken() { + try { + HttpURLConnection browseConn = LyricsRequests.openConnection(BROWSE_URL); + browseConn.setRequestProperty("User-Agent", BROWSER_USER_AGENT); + final int browseCode = browseConn.getResponseCode(); + if (browseCode != 200) { + return null; + } + String html = LyricsRequests.parseGzipString(browseConn); + + Matcher jsMatcher = JS_BUNDLE.matcher(html); + if (jsMatcher.find()) { + String jsPath = jsMatcher.group(0); + String js = fetchJsBundle(BROWSE_URL + jsPath); + if (js != null) { + return extractJwt(js); + } + } + + return null; + } catch (IOException ex) { + Logger.printDebug(() -> "Could not fetch Apple Music dev token", ex); + return null; + } + } + + @Nullable + private static String fetchJsBundle(String jsUrl) { + try { + HttpURLConnection jsConn = LyricsRequests.openConnection(jsUrl); + jsConn.setRequestProperty("User-Agent", BROWSER_USER_AGENT); + return LyricsRequests.parseGzipString(jsConn); + } catch (IOException ex) { + Logger.printDebug(() -> "Could not fetch Apple Music JS bundle", ex); + return null; + } + } + + @Nullable + private static String extractJwt(String text) { + if (text == null) { + return null; + } + Matcher m = JWT.matcher(text); + return m.find() ? m.group(0) : null; + } + + @Nullable + private static String lyricsLanguageParam() { + final String target = LyricsRequests.translationLanguage(); + final String matched = matchSupportedLanguage(Locale.forLanguageTag(target.replace('_', '-'))); + return matched != null ? matched : matchSupportedLanguage(Locale.getDefault()); + } + + @Nullable + private static String matchSupportedLanguage(Locale locale) { + synchronized (TOKEN_LOCK) { + if (locale == null || cachedSupportedLanguages == null) { + return null; + } + String lang = locale.getLanguage(); + String country = locale.getCountry(); + if (lang.isEmpty()) { + return null; + } + String prefix = lang + "-"; + String languageMatch = null; + for (String tag : cachedSupportedLanguages) { + if (tag.startsWith(prefix) || tag.equals(lang)) { + if (!country.isEmpty() && tag.contains(country)) { + return tag; + } + if (languageMatch == null) { + languageMatch = tag; + } + } + } + return languageMatch; + } + } + + private static int rankOfLyrics(@Nullable Lyrics lyrics) { + if (lyrics == null) { + return 0; + } + int totalWords = 0; + int linesWithWords = 0; + for (LyricsLine line : lyrics.lines()) { + if (line.hasWords()) { + linesWithWords++; + totalWords += line.words().size(); + } + } + if (totalWords > linesWithWords) { + return 2; + } + return lyrics.synced() ? 1 : 0; + } + + @Nullable + private static String extractTtmlFromAttributes(@Nullable JSONObject attributes) { + if (attributes == null) { + return null; + } + String ttml = LyricsRequests.optString(attributes, "ttmlLocalizations"); + if (ttml == null || ttml.isEmpty()) { + ttml = LyricsRequests.optString(attributes, "ttml"); + } + return (ttml == null || ttml.isEmpty()) ? null : ttml; + } + + /** + * The first entry of a response's {@code data} array, which is the song or storefront a + * single-object endpoint answers with. Null when the array is missing, empty or its first + * element is not an object. + */ + @Nullable + private static JSONObject firstEntry(JSONObject response) { + JSONArray data = response.optJSONArray("data"); + if (data == null || data.length() == 0) { + return null; + } + return data.optJSONObject(0); + } + + @Nullable + private static String extractTtmlFromRelationship(@Nullable JSONObject relationship) { + if (relationship != null) { + JSONObject item = firstEntry(relationship); + if (item != null) { + if (item != null) { + return extractTtmlFromAttributes(item.optJSONObject("attributes")); + } + } + } + return null; + } + + private static HttpURLConnection openApi(String url, String userToken) throws IOException { + HttpURLConnection connection = LyricsRequests.openConnection(url); + String language; + synchronized (TOKEN_LOCK) { + language = cachedLanguage != null ? cachedLanguage : "en-US"; + } + connection.setRequestProperty("User-Agent", BROWSER_USER_AGENT); + connection.setRequestProperty("Authorization", "Bearer " + cachedDevToken); + connection.setRequestProperty("media-user-token", userToken); + connection.setRequestProperty("content-type", "application/json;charset=utf-8"); + connection.setRequestProperty("origin", "https://music.apple.com"); + connection.setRequestProperty("referer", "https://music.apple.com/"); + connection.setRequestProperty("accept", "application/json"); + connection.setRequestProperty("accept-encoding", "gzip, deflate"); + connection.setRequestProperty("accept-language", language + ",en;q=0.9"); + connection.setRequestProperty("Cookie", "media-user-token=" + userToken); + return connection; + } + + @Nullable + private static JSONObject searchItunes(TrackInfo track) { + HttpURLConnection connection = null; + try { + String term = LyricsRequests.encode(track.title() + " " + track.artist()); + String url = LyricsRequests.ITUNES_SEARCH_URL + "?term=" + term + "&entity=song&limit=5"; + connection = LyricsRequests.openConnection(url); + final int code = connection.getResponseCode(); + if (code != 200) { + return null; + } + JSONObject root = LyricsRequests.parseGzipJsonObject(connection); + JSONArray results = root.optJSONArray("results"); + if (results == null || results.length() == 0) { + return null; + } + + String title = track.title().toLowerCase(Locale.ROOT).trim(); + String artist = track.artist().toLowerCase(Locale.ROOT).trim(); + JSONObject bestItem = null; + int bestScore = -1; + + for (int i = 0; i < results.length(); i++) { + JSONObject item = results.optJSONObject(i); + if (item == null) { + continue; + } + String itemTitle = item.optString("trackName", ""); + String itemArtist = item.optString("artistName", ""); + final long itemId = item.optLong("trackId", 0); + if (itemId == 0) { + continue; + } + int score = LyricsRequests.scoreTrackCandidate(itemTitle, itemArtist, + item.optLong("trackTimeMillis", 0) / 1000, track); + boolean exact = itemTitle.toLowerCase(Locale.ROOT).contains(title) + && itemArtist.toLowerCase(Locale.ROOT).contains(artist); + if (score < LyricsRequests.SOFT_MIN) { + continue; + } + if (exact || score > bestScore) { + bestScore = score; + bestItem = item; + if (exact) { + break; + } + } + } + return bestItem; + } catch (Exception ex) { + Logger.printDebug(() -> "Could not search iTunes for song", ex); + return null; + } finally { + if (connection != null) { + connection.disconnect(); + } + } + } + + @Nullable + private static String fetchLyricly(String trackId) { + String baseUrl = LYRICALLY_BASE + + "/apple-music/lyrics?id=" + trackId; + String result = fetchLyriclyFromUrl(baseUrl); + if (result != null) { + return result; + } + return fetchLyriclyFromUrl(baseUrl + "&skip_cache=true"); + } + + @Nullable + private static String fetchLyriclyFromUrl(String url) { + HttpURLConnection connection = null; + try { + connection = LyricsRequests.openConnection(url); + connection.setRequestProperty("Accept", "application/json"); + final int code = connection.getResponseCode(); + if (code != 200) { + return null; + } + JSONObject root = LyricsRequests.parseGzipJsonObject(connection); + + String ttmlContent = root.optString("ttmlContent", ""); + if (!ttmlContent.isEmpty()) { + return ttmlContent; + } + + String elrcMulti = root.optString("elrcMultiPerson", ""); + if (!elrcMulti.isEmpty()) { + return elrcMulti; + } + + String elrc = root.optString("elrc", ""); + if (!elrc.isEmpty()) { + return elrc; + } + + return null; + } catch (Exception ex) { + Logger.printDebug(() -> "Could not fetch Lyrically lyrics from URL", ex); + return null; + } finally { + if (connection != null) { + connection.disconnect(); + } + } + } @Nullable private ResolvedContext resolveContext() { @@ -100,7 +424,7 @@ public boolean hasCandidates() { @Nullable @Override public FetchResult fetch(TrackInfo track) throws Exception { - ResolvedContext ctx = resolveContext(); + final ResolvedContext ctx = resolveContext(); if (ctx == null) { return fetchViaLyrically(track); } @@ -168,36 +492,6 @@ public List fetchCandidates(TrackInfo track) throws Excepti return Lyrics.sortScoredByScore(scored); } - private static String titleFromSong(JSONObject song) { - JSONObject attributes = song.optJSONObject("attributes"); - return attributes != null ? attributes.optString("name", "") : ""; - } - - private static String artistFromSong(JSONObject song) { - JSONObject attributes = song.optJSONObject("attributes"); - return attributes != null ? attributes.optString("artistName", "") : ""; - } - - private static long durationFromSong(JSONObject song) { - JSONObject attributes = song.optJSONObject("attributes"); - if (attributes == null) return 0; - final long durationMs = attributes.optLong("durationInMillis", 0); - return durationMs > 0 ? durationMs / 1000 : 0; - } - - private static int scoreCandidate(JSONObject item, TrackInfo track) { - JSONObject attributes = item.optJSONObject("attributes"); - if (attributes == null) { - return 0; - } - String title = attributes.optString("name", ""); - String artist = attributes.optString("artistName", ""); - String album = attributes.optString("collectionName", ""); - final long durationMs = attributes.optLong("durationInMillis", 0); - return LyricsRequests.scoreTrackCandidate(title, artist, - durationMs > 0 ? durationMs / 1000 : 0, album, track); - } - private List searchAllSongs(TrackInfo track, String userToken, String storefront) { HttpURLConnection connection = null; try { @@ -231,101 +525,23 @@ private List searchAllSongs(TrackInfo track, String userToken, Strin scored.add(item); } } - scored.sort((a, b) -> scoreCandidate(b, track) - scoreCandidate(a, track)); - List passed = new ArrayList<>(); - for (JSONObject item : scored) { - if (scoreCandidate(item, track) >= LyricsRequests.SOFT_MIN) { - passed.add(item); - } - } - return passed.isEmpty() && !scored.isEmpty() - ? List.of(scored.get(0)) : passed; - } catch (Exception ex) { - Logger.printDebug(() -> "Could not search Apple Music candidates", ex); - return new ArrayList<>(); - } finally { - if (connection != null) { - connection.disconnect(); - } - } - } - - public static boolean validateToken(String userToken) { - synchronized (TOKEN_LOCK) { - if (cachedDevToken == null) { - cachedDevToken = fetchDevToken(); - } - } - if (cachedDevToken == null) { - return false; - } - - HttpURLConnection connection = null; - try { - connection = openApi("https://amp-api.music.apple.com/v1/me/storefront", userToken); - final int code = connection.getResponseCode(); - if (code != 200) { - return false; - } - JSONObject root = LyricsRequests.parseGzipJsonObject(connection); - JSONArray data = root.optJSONArray("data"); - return data != null && data.length() > 0; - } catch (Exception ex) { - Logger.printDebug(() -> "Could not validate Apple Music user token", ex); - return false; - } finally { - if (connection != null) { - connection.disconnect(); - } - } - } - - @Nullable - private static String fetchDevToken() { - try { - HttpURLConnection browseConn = LyricsRequests.openConnection(BROWSE_URL); - browseConn.setRequestProperty("User-Agent", BROWSER_USER_AGENT); - final int browseCode = browseConn.getResponseCode(); - if (browseCode != 200) { - return null; - } - String html = LyricsRequests.parseGzipString(browseConn); - - Matcher jsMatcher = JS_BUNDLE.matcher(html); - if (jsMatcher.find()) { - String jsPath = jsMatcher.group(0); - String js = fetchJsBundle(BROWSE_URL + jsPath); - if (js != null) { - return extractJwt(js); - } - } - - return null; - } catch (IOException ex) { - Logger.printDebug(() -> "Could not fetch Apple Music dev token", ex); - return null; - } - } - - @Nullable - private static String fetchJsBundle(String jsUrl) { - try { - HttpURLConnection jsConn = LyricsRequests.openConnection(jsUrl); - jsConn.setRequestProperty("User-Agent", BROWSER_USER_AGENT); - return LyricsRequests.parseGzipString(jsConn); - } catch (IOException ex) { - Logger.printDebug(() -> "Could not fetch Apple Music JS bundle", ex); - return null; - } - } - - @Nullable - private static String extractJwt(String text) { - if (text == null) { - return null; + scored.sort((a, b) -> scoreCandidate(b, track) - scoreCandidate(a, track)); + List passed = new ArrayList<>(); + for (JSONObject item : scored) { + if (scoreCandidate(item, track) >= LyricsRequests.SOFT_MIN) { + passed.add(item); + } + } + return passed.isEmpty() && !scored.isEmpty() + ? List.of(scored.get(0)) : passed; + } catch (Exception ex) { + Logger.printDebug(() -> "Could not search Apple Music candidates", ex); + return new ArrayList<>(); + } finally { + if (connection != null) { + connection.disconnect(); + } } - Matcher m = JWT.matcher(text); - return m.find() ? m.group(0) : null; } private void resolveStorefront(String userToken) { @@ -377,13 +593,6 @@ private void resolveStorefront(String userToken) { fetchAllSupportedLanguages(userToken); } - @Nullable - private static String lyricsLanguageParam() { - String target = LyricsRequests.translationLanguage(); - String matched = matchSupportedLanguage(Locale.forLanguageTag(target.replace('_', '-'))); - return matched != null ? matched : matchSupportedLanguage(Locale.getDefault()); - } - private void fetchAllSupportedLanguages(String userToken) { LinkedHashSet merged = new LinkedHashSet<>(); HttpURLConnection connection = null; @@ -424,33 +633,6 @@ private void fetchAllSupportedLanguages(String userToken) { } } - @Nullable - private static String matchSupportedLanguage(Locale locale) { - synchronized (TOKEN_LOCK) { - if (locale == null || cachedSupportedLanguages == null) { - return null; - } - String lang = locale.getLanguage(); - String country = locale.getCountry(); - if (lang.isEmpty()) { - return null; - } - String prefix = lang + "-"; - String languageMatch = null; - for (String tag : cachedSupportedLanguages) { - if (tag.startsWith(prefix) || tag.equals(lang)) { - if (!country.isEmpty() && tag.contains(country)) { - return tag; - } - if (languageMatch == null) { - languageMatch = tag; - } - } - } - return languageMatch; - } - } - @Nullable private Lyrics fetchLyrics(String userToken, String storefront, String language, String songId) { String sourceUrl = "https://music.apple.com/song/" + songId; @@ -469,36 +651,6 @@ private Lyrics fetchLyrics(String userToken, String storefront, String language, : dedicatedRank >= includeRank ? dedicatedResult : includeResult; } - private static int rankOfLyrics(@Nullable Lyrics lyrics) { - if (lyrics == null) { - return 0; - } - int totalWords = 0; - int linesWithWords = 0; - for (LyricsLine line : lyrics.lines()) { - if (line.hasWords()) { - linesWithWords++; - totalWords += line.words().size(); - } - } - if (totalWords > linesWithWords) { - return 2; - } - return lyrics.synced() ? 1 : 0; - } - - @Nullable - private static String extractTtmlFromAttributes(@Nullable JSONObject attributes) { - if (attributes == null) { - return null; - } - String ttml = LyricsRequests.optString(attributes, "ttmlLocalizations"); - if (ttml == null || ttml.isEmpty()) { - ttml = LyricsRequests.optString(attributes, "ttml"); - } - return (ttml == null || ttml.isEmpty()) ? null : ttml; - } - @Nullable private Lyrics fetchLyricsFromEndpoint(String userToken, String url, String sourceUrl, String logContext) { HttpURLConnection connection = null; @@ -562,33 +714,6 @@ private Lyrics fetchLyricsDedicated(String userToken, String storefront, String } } - /** - * The first entry of a response's {@code data} array, which is the song or storefront a - * single-object endpoint answers with. Null when the array is missing, empty or its first - * element is not an object. - */ - @Nullable - private static JSONObject firstEntry(JSONObject response) { - JSONArray data = response.optJSONArray("data"); - if (data == null || data.length() == 0) { - return null; - } - return data.optJSONObject(0); - } - - @Nullable - private static String extractTtmlFromRelationship(@Nullable JSONObject relationship) { - if (relationship != null) { - JSONObject item = firstEntry(relationship); - if (item != null) { - if (item != null) { - return extractTtmlFromAttributes(item.optJSONObject("attributes")); - } - } - } - return null; - } - @Nullable private Lyrics fetchLyricsInclude(String userToken, String storefront, String language, String songId, String sourceUrl) { @@ -631,25 +756,6 @@ private Lyrics fetchLyricsInclude(String userToken, String storefront, String la } } - private static HttpURLConnection openApi(String url, String userToken) throws IOException { - HttpURLConnection connection = LyricsRequests.openConnection(url); - String language; - synchronized (TOKEN_LOCK) { - language = cachedLanguage != null ? cachedLanguage : "en-US"; - } - connection.setRequestProperty("User-Agent", BROWSER_USER_AGENT); - connection.setRequestProperty("Authorization", "Bearer " + cachedDevToken); - connection.setRequestProperty("media-user-token", userToken); - connection.setRequestProperty("content-type", "application/json;charset=utf-8"); - connection.setRequestProperty("origin", "https://music.apple.com"); - connection.setRequestProperty("referer", "https://music.apple.com/"); - connection.setRequestProperty("accept", "application/json"); - connection.setRequestProperty("accept-encoding", "gzip, deflate"); - connection.setRequestProperty("accept-language", language + ",en;q=0.9"); - connection.setRequestProperty("Cookie", "media-user-token=" + userToken); - return connection; - } - @Nullable private FetchResult fetchViaLyrically(TrackInfo track) { if (track.title().isEmpty() || track.artist().isEmpty()) { @@ -677,111 +783,6 @@ private FetchResult fetchViaLyrically(TrackInfo track) { } } - @Nullable - private static JSONObject searchItunes(TrackInfo track) { - HttpURLConnection connection = null; - try { - String term = LyricsRequests.encode(track.title() + " " + track.artist()); - String url = LyricsRequests.ITUNES_SEARCH_URL + "?term=" + term + "&entity=song&limit=5"; - connection = LyricsRequests.openConnection(url); - final int code = connection.getResponseCode(); - if (code != 200) { - return null; - } - JSONObject root = LyricsRequests.parseGzipJsonObject(connection); - JSONArray results = root.optJSONArray("results"); - if (results == null || results.length() == 0) { - return null; - } - - String title = track.title().toLowerCase(Locale.ROOT).trim(); - String artist = track.artist().toLowerCase(Locale.ROOT).trim(); - JSONObject bestItem = null; - int bestScore = -1; - - for (int i = 0; i < results.length(); i++) { - JSONObject item = results.optJSONObject(i); - if (item == null) { - continue; - } - String itemTitle = item.optString("trackName", ""); - String itemArtist = item.optString("artistName", ""); - final long itemId = item.optLong("trackId", 0); - if (itemId == 0) { - continue; - } - int score = LyricsRequests.scoreTrackCandidate(itemTitle, itemArtist, - item.optLong("trackTimeMillis", 0) / 1000, track); - boolean exact = itemTitle.toLowerCase(Locale.ROOT).contains(title) - && itemArtist.toLowerCase(Locale.ROOT).contains(artist); - if (score < LyricsRequests.SOFT_MIN) { - continue; - } - if (exact || score > bestScore) { - bestScore = score; - bestItem = item; - if (exact) { - break; - } - } - } - return bestItem; - } catch (Exception ex) { - Logger.printDebug(() -> "Could not search iTunes for song", ex); - return null; - } finally { - if (connection != null) { - connection.disconnect(); - } - } - } - - @Nullable - private static String fetchLyricly(String trackId) { - String baseUrl = LYRICALLY_BASE - + "/apple-music/lyrics?id=" + trackId; - String result = fetchLyriclyFromUrl(baseUrl); - if (result != null) { - return result; - } - return fetchLyriclyFromUrl(baseUrl + "&skip_cache=true"); - } - - @Nullable - private static String fetchLyriclyFromUrl(String url) { - HttpURLConnection connection = null; - try { - connection = LyricsRequests.openConnection(url); - connection.setRequestProperty("Accept", "application/json"); - final int code = connection.getResponseCode(); - if (code != 200) { - return null; - } - JSONObject root = LyricsRequests.parseGzipJsonObject(connection); - - String ttmlContent = root.optString("ttmlContent", ""); - if (!ttmlContent.isEmpty()) { - return ttmlContent; - } - - String elrcMulti = root.optString("elrcMultiPerson", ""); - if (!elrcMulti.isEmpty()) { - return elrcMulti; - } - - String elrc = root.optString("elrc", ""); - if (!elrc.isEmpty()) { - return elrc; - } - - return null; - } catch (Exception ex) { - Logger.printDebug(() -> "Could not fetch Lyrically lyrics from URL", ex); - return null; - } finally { - if (connection != null) { - connection.disconnect(); - } - } + private record ResolvedContext(String userToken, String storefront, String language) { } } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/BinimumProvider.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/BinimumProvider.java index 7637ecabc8..91606ac77d 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/BinimumProvider.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/BinimumProvider.java @@ -37,6 +37,97 @@ public final class BinimumProvider implements LyricsProvider { "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " + "(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"; + @Nullable + private static LyricsLine buildLineFromCandidates( + List candidates, + long lineStart, + long lineEnd, + long lineDuration, + @Nullable String rawFallbackText, + @Nullable String agentId, + boolean isDuet, + boolean isBackground, + @Nullable String songPart + ) { + if (candidates.isEmpty()) { + return null; + } + + List words = new ArrayList<>(candidates.size()); + StringBuilder textBuf = new StringBuilder(); + + for (WordCandidate candidate : candidates) { + long startTime = candidate.syl.optLong("time"); + long duration = candidate.syl.optLong("duration"); + long endTime = startTime + duration; + String text = candidate.syl.optString("text", ""); + boolean endsWithSpace = text.endsWith(" ") || text.endsWith("\t"); + + String cleanWord = isBackground ? text.replaceAll("[()()]", "").trim() : text.trim(); + String cleanRoma = candidate.romaji; + if (isBackground && cleanRoma != null) { + cleanRoma = cleanRoma.replaceAll("[()()]", "").trim(); + } + + if (!cleanWord.isEmpty()) { + words.add(new Word(startTime, endTime, cleanWord, cleanRoma, endsWithSpace)); + textBuf.append(cleanWord); + if (endsWithSpace) { + textBuf.append(' '); + } + } + } + + String fullText = textBuf.toString().trim(); + + // Words with no spaces between them in a spaced script are joined with spaces. + if (fullText.indexOf(' ') < 0 && words.size() > 1 && !TTMLParser.isContinuousCjk(words.get(0).text())) { + StringBuilder reconstructed = new StringBuilder(); + for (int i = 0; i < words.size(); i++) { + Word word = words.get(i); + if (i > 0) { + reconstructed.append(' '); + } + reconstructed.append(word.text()); + boolean hasSpace = (i < words.size() - 1); + words.set(i, new Word(word.startMs(), word.endMs(), word.text(), word.romaji(), hasSpace)); + } + fullText = reconstructed.toString(); + } + + // The last word ends the line, so it has no space after it. + if (!words.isEmpty()) { + Word lastWord = words.get(words.size() - 1); + if (lastWord.endsWithSpace()) { + words.set(words.size() - 1, new Word(lastWord.startMs(), lastWord.endMs(), lastWord.text(), lastWord.romaji(), false)); + } + } + + if (fullText.isEmpty() && rawFallbackText != null && !rawFallbackText.isEmpty()) { + fullText = rawFallbackText.trim(); + } + + if (fullText.isEmpty() && words.isEmpty()) { + return null; + } + + long start = words.isEmpty() ? lineStart : words.get(0).startMs(); + long end = words.isEmpty() ? lineEnd : words.get(words.size() - 1).endMs(); + if (end < start && lineDuration > 0) { + end = start + lineDuration; + } + + return new LyricsLine(start, end, fullText, words, agentId, isDuet, isBackground, songPart); + } + + private static boolean isAgentDuet(@Nullable String agentId) { + if (agentId == null || !agentId.matches("v\\d{1,6}")) { + return false; + } + int num = Integer.parseInt(agentId.substring(1)); + return num > 0 && num % 2 == 0; + } + @Override public String name() { return "BiniLyrics"; @@ -60,7 +151,7 @@ public FetchResult fetch(TrackInfo track) throws Exception { } // A title with a video tag is asked again without it. - String cleanTitle = track.title() + final String cleanTitle = track.title() .replace("(Official Video)", "") .replace("(Official Music Video)", "") .replace("(Official Audio)", "") @@ -272,107 +363,6 @@ private Lyrics parseLyricsJson(JSONObject root, String sourceUrl) { sourceUrl); } - @Nullable - private static LyricsLine buildLineFromCandidates( - List candidates, - long lineStart, - long lineEnd, - long lineDuration, - @Nullable String rawFallbackText, - @Nullable String agentId, - boolean isDuet, - boolean isBackground, - @Nullable String songPart - ) { - if (candidates.isEmpty()) { - return null; - } - - List words = new ArrayList<>(candidates.size()); - StringBuilder textBuf = new StringBuilder(); - - for (WordCandidate candidate : candidates) { - long startTime = candidate.syl.optLong("time"); - long duration = candidate.syl.optLong("duration"); - long endTime = startTime + duration; - String text = candidate.syl.optString("text", ""); - boolean endsWithSpace = text.endsWith(" ") || text.endsWith("\t"); - - String cleanWord = isBackground ? text.replaceAll("[()()]", "").trim() : text.trim(); - String cleanRoma = candidate.romaji; - if (isBackground && cleanRoma != null) { - cleanRoma = cleanRoma.replaceAll("[()()]", "").trim(); - } - - if (!cleanWord.isEmpty()) { - words.add(new Word(startTime, endTime, cleanWord, cleanRoma, endsWithSpace)); - textBuf.append(cleanWord); - if (endsWithSpace) { - textBuf.append(' '); - } - } - } - - String fullText = textBuf.toString().trim(); - - // Words with no spaces between them in a spaced script are joined with spaces. - if (fullText.indexOf(' ') < 0 && words.size() > 1 && !TTMLParser.isContinuousCjk(words.get(0).text())) { - StringBuilder reconstructed = new StringBuilder(); - for (int i = 0; i < words.size(); i++) { - Word word = words.get(i); - if (i > 0) { - reconstructed.append(' '); - } - reconstructed.append(word.text()); - boolean hasSpace = (i < words.size() - 1); - words.set(i, new Word(word.startMs(), word.endMs(), word.text(), word.romaji(), hasSpace)); - } - fullText = reconstructed.toString(); - } - - // The last word ends the line, so it has no space after it. - if (!words.isEmpty()) { - Word lastWord = words.get(words.size() - 1); - if (lastWord.endsWithSpace()) { - words.set(words.size() - 1, new Word(lastWord.startMs(), lastWord.endMs(), lastWord.text(), lastWord.romaji(), false)); - } - } - - if (fullText.isEmpty() && rawFallbackText != null && !rawFallbackText.isEmpty()) { - fullText = rawFallbackText.trim(); - } - - if (fullText.isEmpty() && words.isEmpty()) { - return null; - } - - long start = words.isEmpty() ? lineStart : words.get(0).startMs(); - long end = words.isEmpty() ? lineEnd : words.get(words.size() - 1).endMs(); - if (end < start && lineDuration > 0) { - end = start + lineDuration; - } - - return new LyricsLine(start, end, fullText, words, agentId, isDuet, isBackground, songPart); - } - - private static boolean isAgentDuet(@Nullable String agentId) { - if (agentId == null || !agentId.matches("v\\d{1,6}")) { - return false; - } - int num = Integer.parseInt(agentId.substring(1)); - return num > 0 && num % 2 == 0; - } - - private static class WordCandidate { - final JSONObject syl; - final @Nullable String romaji; - - WordCandidate(JSONObject syl, @Nullable String romaji) { - this.syl = syl; - this.romaji = romaji; - } - } - @Nullable private String resolveLyricsUrl(TrackInfo track) throws IOException, JSONException { if (track.title().isEmpty() || track.artist().isEmpty()) { @@ -417,4 +407,7 @@ private String resolveLyricsUrl(TrackInfo track) throws IOException, JSONExcepti } } } + + private record WordCandidate(JSONObject syl, @Nullable String romaji) { + } } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/BlyricsProvider.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/BlyricsProvider.java index 22a0d26d1d..3eba77d680 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/BlyricsProvider.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/BlyricsProvider.java @@ -14,8 +14,8 @@ import java.net.HttpURLConnection; import app.morphe.extension.music.patches.lyrics.model.TrackInfo; -import app.morphe.extension.shared.requests.Requester; import app.morphe.extension.music.patches.lyrics.parsers.TTMLParser; +import app.morphe.extension.shared.requests.Requester; public final class BlyricsProvider implements LyricsProvider { diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/CaptionsFetcher.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/CaptionsFetcher.java index c53a7b54a5..4981440040 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/CaptionsFetcher.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/CaptionsFetcher.java @@ -53,10 +53,10 @@ public final class CaptionsFetcher { "https://www.youtube.com/api/timedtext"; /* - * Naming the ASR track apart is what lets the footer say which caption track is on - * screen. It has to remain a prefix-extension of CAPTIONS_PROVIDER, because that is how - * the rest of the lyrics code tells a video-timeline result from a title-matched one. - */ + * Naming the ASR track apart is what lets the footer say which caption track is on + * screen. It has to remain a prefix-extension of CAPTIONS_PROVIDER, because that is how + * the rest of the lyrics code tells a video-timeline result from a title-matched one. + */ private static final String ASR_PROVIDER_NAME = Lyrics.CAPTIONS_PROVIDER + " (ASR)"; private static final String CAPTION_USER_AGENT = LyricsRequests.CHROME_DESKTOP_USER_AGENT; @@ -68,149 +68,26 @@ public final class CaptionsFetcher { private static final List REQUIRED_USER_COOKIE_KEYS = Arrays.asList( "SID", "HSID", "SSID", "SAPISID", "__Secure-3PAPISID", "__Secure-1PAPISID", "LOGIN_INFO" ); - private static volatile String cachedCookies = null; - private static volatile long cachedCookiesTime = 0; private static final long COOKIE_CACHE_DURATION_MS = 30L * 24 * 60 * 60 * 1000; - private static final Pattern BRACKETS_PATTERN = Pattern.compile("\\[[^]]*]"); private static final Pattern PARENTHESES_PATTERN = Pattern.compile("\\([^)]*\\)"); - + private static volatile String cachedCookies = null; + private static volatile long cachedCookiesTime = 0; private static volatile String lastFetchedVideoId = null; private CaptionsFetcher() { } - private record CaptionTrack(String url, String langCode, String vssId, boolean isAsr, - boolean isTranslatable, boolean isTranslation, boolean hasTlang, - String displayName) { - - @Override - @NonNull - public String toString() { - return langCode + (isAsr ? " (ASR)" : " (manual)") - + " vssId=" + vssId - + (isTranslation ? " [trans]" : " [orig]") - + (hasTlang ? " [tlang]" : "") - + " t=" + isTranslatable - + " '" + displayName + "'"; - } - } - - public static final class CaptionsOutcome { - static final CaptionsOutcome ALLOW_PROVIDERS = new CaptionsOutcome(null, null, null, false, null, null); - - public final @Nullable Lyrics lyrics; - public final @Nullable Lyrics translationLyrics; - public final @Nullable Lyrics asrLyrics; - public final boolean suppressProviders; - public final @Nullable TrackInfo innertubeTrack; - public final @Nullable String errorReason; - - private CaptionsOutcome(@Nullable Lyrics lyrics, @Nullable Lyrics translationLyrics, - @Nullable Lyrics asrLyrics, boolean suppressProviders, - @Nullable TrackInfo innertubeTrack, - @Nullable String errorReason) { - this.lyrics = lyrics; - this.translationLyrics = translationLyrics; - this.asrLyrics = asrLyrics; - this.suppressProviders = suppressProviders; - this.innertubeTrack = innertubeTrack; - this.errorReason = errorReason; - } - - static CaptionsOutcome captions(Lyrics lyrics, @Nullable TrackInfo innertubeTrack) { - return new CaptionsOutcome(lyrics, null, null, false, innertubeTrack, null); - } - - static CaptionsOutcome captions(Lyrics lyrics, @Nullable Lyrics translationLyrics, - @Nullable Lyrics asrLyrics, - @Nullable TrackInfo innertubeTrack) { - return new CaptionsOutcome(lyrics, translationLyrics, asrLyrics, false, innertubeTrack, null); - } - - static CaptionsOutcome withError(@Nullable String reason, @Nullable TrackInfo innertubeTrack) { - return new CaptionsOutcome(null, null, null, false, innertubeTrack, reason); - } - } - - public static final class CaptionsProvider implements LyricsProvider { - @Override - public String name() { - return "Captions"; - } - - @Override - @Nullable - public FetchResult fetch(TrackInfo track) throws Exception { - CaptionsOutcome outcome = CaptionsFetcher.fetch(); - - if (outcome.errorReason != null) { - return null; - } - - if (outcome.lyrics == null || outcome.lyrics.isEmpty()) { - return null; - } - - Lyrics result = outcome.lyrics; - if (outcome.translationLyrics != null && !outcome.translationLyrics.isEmpty()) { - Map> translations = new HashMap<>(); - translations.put(LyricsRequests.translationLanguage(), - outcome.translationLyrics.lines()); - result = new Lyrics(result.lines(), result.providerName(), result.synced(), - result.romanization(), translations, - result.romanizations(), result.songwriters(), - result.rawFormat(), result.formatType(), - result.sourceUrl()); - } - - return FetchResult.of(result); - } - - /* - * The ASR track is worth a candidate slot because it is the only caption track that - * carries a per-word timeline. It does not take over the screen on its own: the lookups - * that reach this provider have already put the hand written text there. - */ - @Override - public boolean hasCandidates() { - return true; - } - - @Override - public List fetchCandidates(TrackInfo track) { - CaptionsOutcome outcome = CaptionsFetcher.fetch(true); - List candidates = new ArrayList<>(2); - if (outcome.errorReason == null) { - if (outcome.lyrics != null && !outcome.lyrics.isEmpty()) { - candidates.add(scored(outcome.lyrics, track)); - } - if (outcome.asrLyrics != null && !outcome.asrLyrics.isEmpty()) { - candidates.add(scored(outcome.asrLyrics, track)); - } - } - return candidates; - } - - /* The manager takes the reported score back apart, so both tracks have to report on the - * same match and sync scale or their ranking would compare two different quantities. */ - private static Lyrics.ScoredLyrics scored(Lyrics lyrics, TrackInfo track) { - FetchResult result = FetchResult.of(lyrics); - int score = result.matchScore(track) + LyricsRequests.syncRank(lyrics); - return new Lyrics.ScoredLyrics(score, lyrics); - } - } - public static CaptionsOutcome fetch() { return fetch(false); } /* - * @param includeAsr also pull the ASR track of the same song. Only the candidate pass - * asks for it: the ordinary one already spent its request on the hand - * written track, and paying for a second one on every lookup would buy a - * candidate the user has to refresh his way to. - */ + * @param includeAsr also pull the ASR track of the same song. Only the candidate pass + * asks for it: the ordinary one already spent its request on the hand + * written track, and paying for a second one on every lookup would buy a + * candidate the user has to refresh his way to. + */ private static CaptionsOutcome fetch(boolean includeAsr) { String videoId = readVideoIdWithRetry(); if (videoId == null || videoId.isEmpty()) { @@ -284,7 +161,7 @@ private static Lyrics fetchTrackLyrics(CaptionTrack track, @Nullable String poTo @Nullable private static Lyrics fetchTrackLyrics(CaptionTrack track, @Nullable String poToken, - String providerName) { + String providerName) { try { String url = buildCaptionUrl(track.url(), poToken); String json = fetchCaptionUrl(url); @@ -307,7 +184,7 @@ private static String buildCaptionUrl(String baseUrl, @Nullable String poToken) } private static CaptionsOutcome tryTimedtext(String videoId, @Nullable String poToken, - @Nullable TrackInfo innertubeTrack) { + @Nullable TrackInfo innertubeTrack) { Lyrics timed = fetchViaTimedtext(videoId, poToken); if (timed != null && !timed.isEmpty()) { lastFetchedVideoId = videoId; @@ -363,12 +240,6 @@ private static String readVideoIdWithRetry() { return null; } - private record CaptionListResult(boolean structurePresent, List tracks, - @Nullable String poToken, @Nullable TrackInfo innertubeTrack, - @Nullable String sourceLangCode, @Nullable String videoLangCode, - @Nullable String errorReason) { - } - @Nullable private static CaptionListResult findCaptionList(String videoId) { String json = fetchInnertubePlayer(videoId); @@ -455,10 +326,10 @@ private static Set extractTranslationLanguages(String json) { @Nullable private static String determineSourceLanguage(List tracks, - @Nullable String videoLangCode, - @Nullable String microLang, - @Nullable String audioTracksLang, - Set translationLangs) { + @Nullable String videoLangCode, + @Nullable String microLang, + @Nullable String audioTracksLang, + Set translationLangs) { for (CaptionTrack track : tracks) { if (track.isAsr()) { return track.langCode(); @@ -976,7 +847,7 @@ private static String postInnertubePlayer(String bodyJson, @Nullable String cook public static boolean validateYouTubeCookies(String cookies) { if (cookies == null || cookies.trim().isEmpty()) return false; - String normalized = normalizeCookies(cookies); + final String normalized = normalizeCookies(cookies); if (!hasRequiredCookieKeys(normalized)) { Logger.printDebug(() -> "YouTube cookies are missing required keys"); return false; @@ -1131,7 +1002,7 @@ private static List parseJson3(String json) throws Exception { } private static List wordTimings(JSONObject event, JSONArray segments, - long startTimeMs) throws JSONException { + long startTimeMs) throws JSONException { final int count = segments.length(); if (count < 2) { return List.of(); @@ -1165,7 +1036,7 @@ private static List wordTimings(JSONObject event, JSONArray segments, } Word last = words.get(total - 1); words.set(total - 1, new Word(last.startMs(), - Math.max(eventEndMs, last.startMs()), last.text(), + eventEndMs > last.startMs() ? eventEndMs : last.startMs(), last.text(), null, last.endsWithSpace())); return words; } @@ -1200,4 +1071,132 @@ private static String fetchCaptionUrl(String urlStr) throws Exception { } } } + + private record CaptionTrack(String url, String langCode, String vssId, boolean isAsr, + boolean isTranslatable, boolean isTranslation, boolean hasTlang, + String displayName) { + + @Override + @NonNull + public String toString() { + return langCode + (isAsr ? " (ASR)" : " (manual)") + + " vssId=" + vssId + + (isTranslation ? " [trans]" : " [orig]") + + (hasTlang ? " [tlang]" : "") + + " t=" + isTranslatable + + " '" + displayName + "'"; + } + } + + public static final class CaptionsOutcome { + static final CaptionsOutcome ALLOW_PROVIDERS = new CaptionsOutcome(null, null, null, false, null, null); + + public final @Nullable Lyrics lyrics; + public final @Nullable Lyrics translationLyrics; + public final @Nullable Lyrics asrLyrics; + public final boolean suppressProviders; + public final @Nullable TrackInfo innertubeTrack; + public final @Nullable String errorReason; + + private CaptionsOutcome(@Nullable Lyrics lyrics, @Nullable Lyrics translationLyrics, + @Nullable Lyrics asrLyrics, boolean suppressProviders, + @Nullable TrackInfo innertubeTrack, + @Nullable String errorReason) { + this.lyrics = lyrics; + this.translationLyrics = translationLyrics; + this.asrLyrics = asrLyrics; + this.suppressProviders = suppressProviders; + this.innertubeTrack = innertubeTrack; + this.errorReason = errorReason; + } + + static CaptionsOutcome captions(Lyrics lyrics, @Nullable TrackInfo innertubeTrack) { + return new CaptionsOutcome(lyrics, null, null, false, innertubeTrack, null); + } + + static CaptionsOutcome captions(Lyrics lyrics, @Nullable Lyrics translationLyrics, + @Nullable Lyrics asrLyrics, + @Nullable TrackInfo innertubeTrack) { + return new CaptionsOutcome(lyrics, translationLyrics, asrLyrics, false, innertubeTrack, null); + } + + static CaptionsOutcome withError(@Nullable String reason, @Nullable TrackInfo innertubeTrack) { + return new CaptionsOutcome(null, null, null, false, innertubeTrack, reason); + } + } + + public static final class CaptionsProvider implements LyricsProvider { + /* The manager takes the reported score back apart, so both tracks have to report on the + * same match and sync scale or their ranking would compare two different quantities. */ + private static Lyrics.ScoredLyrics scored(Lyrics lyrics, TrackInfo track) { + FetchResult result = FetchResult.of(lyrics); + int score = result.matchScore(track) + LyricsRequests.syncRank(lyrics); + return new Lyrics.ScoredLyrics(score, lyrics); + } + + @Override + public String name() { + return "Captions"; + } + + @Override + @Nullable + public FetchResult fetch(TrackInfo track) throws Exception { + CaptionsOutcome outcome = CaptionsFetcher.fetch(); + + if (outcome.errorReason != null) { + return null; + } + + if (outcome.lyrics == null || outcome.lyrics.isEmpty()) { + return null; + } + + Lyrics result = outcome.lyrics; + if (outcome.translationLyrics != null && !outcome.translationLyrics.isEmpty()) { + Map> translations = new HashMap<>(); + translations.put(LyricsRequests.translationLanguage(), + outcome.translationLyrics.lines()); + result = new Lyrics(result.lines(), result.providerName(), result.synced(), + result.romanization(), translations, + result.romanizations(), result.songwriters(), + result.rawFormat(), result.formatType(), + result.sourceUrl()); + } + + return FetchResult.of(result); + } + + /* + * The ASR track is worth a candidate slot because it is the only caption track that + * carries a per-word timeline. It does not take over the screen on its own: the lookups + * that reach this provider have already put the hand written text there. + */ + @Override + public boolean hasCandidates() { + return true; + } + + @Override + public List fetchCandidates(TrackInfo track) { + CaptionsOutcome outcome = CaptionsFetcher.fetch(true); + List candidates = new ArrayList<>(2); + if (outcome.errorReason == null) { + if (outcome.lyrics != null && !outcome.lyrics.isEmpty()) { + candidates.add(scored(outcome.lyrics, track)); + } + if (outcome.asrLyrics != null && !outcome.asrLyrics.isEmpty()) { + candidates.add(scored(outcome.asrLyrics, track)); + } + } + return candidates; + } + } + + private record CaptionListResult(boolean structurePresent, List tracks, + @Nullable String poToken, @Nullable TrackInfo innertubeTrack, + @Nullable String sourceLangCode, + @Nullable String videoLangCode, + @Nullable String errorReason) { + } } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/DeezerProvider.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/DeezerProvider.java index 0832ab16ac..2b5b398f1c 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/DeezerProvider.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/DeezerProvider.java @@ -55,62 +55,12 @@ public final class DeezerProvider extends ScoredSongProvider { private static final long REQUEST_THROTTLE_MS = 250; private static final AtomicLong lastRequestTime = new AtomicLong(0); - + /** Lookups that succeeded, including tracks without a version. Failed requests are retried. */ + private static final Map versions = Utils.createSizeRestrictedMap(64, true); private static String cachedArl; private static String cachedJwt; private static long jwtExpiryMs; - @Override - public String name() { - return "Deezer"; - } - - @Override - protected List fetchScoredSongs(TrackInfo track) throws Exception { - String arl = getArl(); - if (arl == null) { - return Collections.emptyList(); - } - - String jwt = getJwt(arl); - if (jwt == null) { - return Collections.emptyList(); - } - - JSONArray searchResults = searchTracks(track); - if (searchResults == null || searchResults.length() == 0) { - return Collections.emptyList(); - } - - List sorted = sortCandidates(searchResults, track); - - List scored = new ArrayList<>(); - for (JSONObject item : sorted) { - if (scored.size() >= LyricsRequests.MAX_CANDIDATES) break; - - final long trackId = item.optLong("id", -1); - if (trackId <= 0) continue; - - try { - Lyrics lyrics = fetchLyricsByTrackId(trackId, arl); - if (lyrics != null) { - String title = item.optString("title", ""); - JSONObject artistObj = item.optJSONObject("artist"); - String artist = artistObj != null ? artistObj.optString("name", "") : ""; - long durationSec = item.optInt("duration", 0); - int score = LyricsRequests.scoreLyricsCandidate( - title, artist, durationSec, lyrics, track); - scored.add(new ScoredSong(score, lyrics, title, artist, durationSec)); - } - } catch (Exception ex) { - Logger.printDebug(() -> "Could not fetch Deezer lyrics by track ID", ex); - } - } - - scored.sort((a, b) -> Integer.compare(b.score(), a.score())); - return scored; - } - private static int scoreCandidate(JSONObject item, TrackInfo track) { String title = item.optString("title", ""); JSONObject artistObj = item.optJSONObject("artist"); @@ -192,9 +142,6 @@ private static synchronized void dropJwt() { jwtExpiryMs = 0; } - /** Lookups that succeeded, including tracks without a version. Failed requests are retried. */ - private static final Map versions = Utils.createSizeRestrictedMap(64, true); - /** * The version of the track, from the title_short and title_version fields of Deezer's own * search results. The variant is empty when Deezer has no exact match or the track has no @@ -223,8 +170,8 @@ private static TrackVersion lookupVersion(TrackInfo track) throws Exception { JSONArray data = getJson(url).optJSONArray("data"); if (data == null) throw new IOException("Deezer search has no results array"); - String wantedTitle = TrackVersion.normalize(track.title()); - String wantedArtist = TrackVersion.normalize(track.artist()); + final String wantedTitle = TrackVersion.normalize(track.title()); + final String wantedArtist = TrackVersion.normalize(track.artist()); // The full title is matched first. Short title equality alone would confuse the // original recording with a remix or acoustic version of that same song. for (int pass = 0; pass < 2; pass++) { @@ -235,9 +182,9 @@ private static TrackVersion lookupVersion(TrackInfo track) throws Exception { if (artist == null || !wantedArtist.equals(TrackVersion.normalize(artist.optString("name", "")))) { continue; } - String full = item.optString("title", ""); - String shortTitle = item.optString("title_short", ""); - String version = item.optString("title_version", ""); + final String full = item.optString("title", ""); + final String shortTitle = item.optString("title_short", ""); + final String version = item.optString("title_version", ""); final boolean matches = pass == 0 ? wantedTitle.equals(TrackVersion.normalize(full)) : TrackVersion.matchesStructuredTitle(track.title(), full, shortTitle, version); @@ -254,75 +201,17 @@ private static JSONObject getJson(String url) throws Exception { HttpURLConnection connection = LyricsRequests.openConnection(url, Map.of("Accept", "application/json")); try { final int httpCode = connection.getResponseCode(); - if (httpCode != Requester.HTTP_STATUS_CODE_SUCCESS) throw new IOException("Deezer HTTP " + httpCode); + if (httpCode != Requester.HTTP_STATUS_CODE_SUCCESS) + throw new IOException("Deezer HTTP " + httpCode); JSONObject json = Requester.parseJSONObject(connection); - if (json.has("error")) throw new IOException("Deezer error: " + json.optString("error")); + if (json.has("error")) + throw new IOException("Deezer error: " + json.optString("error")); return json; } finally { connection.disconnect(); } } - @Nullable - private JSONArray searchTracks(TrackInfo track) throws Exception { - LyricsRequests.throttle(lastRequestTime, REQUEST_THROTTLE_MS); - - String query = track.artist() + " - " + track.title(); - String url = SEARCH_URL - + "?q=" + LyricsRequests.encode(query) - + "&limit=10" - + "&output=json"; - - HttpURLConnection connection; - try { - connection = LyricsRequests.openConnection(url, Map.of("Accept", "application/json")); - } catch (IOException ex) { - Logger.printDebug(() -> "Could not open Deezer search connection", ex); - return null; - } - - try { - final int httpCode = connection.getResponseCode(); - if (httpCode != Requester.HTTP_STATUS_CODE_SUCCESS) return null; - JSONObject response = Requester.parseJSONObject(connection); - return response.optJSONArray("data"); - } catch (IOException ex) { - Logger.printDebug(() -> "Could not parse Deezer search response", ex); - return null; - } finally { - connection.disconnect(); - } - } - - @Nullable - private Lyrics fetchLyricsByTrackId(long trackId, String arl) throws Exception { - JSONObject response = postPipe(payload(trackId, LYRICS_QUERY), arl, true); - if (response != null && response.has("errors")) { - // The credit fields are not in every schema version; ask for the lyrics alone. - response = postPipe(payload(trackId, LYRICS_QUERY_MINIMAL), arl, true); - } - JSONObject lyrics = response == null - ? null : LyricsRequests.optPath(response, "data", "track", "lyrics"); - if (lyrics == null) { - return null; - } - - JSONArray syncedLines = lyrics.optJSONArray("synchronizedLines"); - if (syncedLines != null && syncedLines.length() > 0) { - String rawFormat = syncedLines.toString(); - Lyrics synced = parseSyncedLyrics(syncedLines, trackId, rawFormat, creditLines(lyrics)); - if (synced != null) { - return synced; - } - } - - String text = LyricsRequests.optString(lyrics, "text"); - if (text != null) { - return parsePlainText(text, trackId, text, creditLines(lyrics)); - } - return null; - } - private static String payload(long trackId, String query) throws Exception { JSONObject variables = new JSONObject(); variables.put("trackId", String.valueOf(trackId)); @@ -374,11 +263,11 @@ private static JSONObject postPipe(String body, String arl, boolean retryOnExpir @Nullable private static List creditLines(JSONObject lyrics) { List credits = new ArrayList<>(2); - String writers = flatten(lyrics.opt("writers")); + final String writers = flatten(lyrics.opt("writers")); if (writers != null) { credits.add("Writer(s): " + writers); } - String copyright = LyricsRequests.optString(lyrics, "copyright"); + final String copyright = LyricsRequests.optString(lyrics, "copyright"); if (copyright != null) { credits.add("Copyright: " + copyright); } @@ -394,7 +283,7 @@ private static String flatten(@Nullable Object value) { if (value instanceof JSONArray array) { StringBuilder builder = new StringBuilder(); for (int i = 0; i < array.length(); i++) { - String entry = array.optString(i, "").trim(); + final String entry = array.optString(i, "").trim(); if (entry.isEmpty()) continue; //noinspection SizeReplaceableByIsEmpty if (builder.length() > 0) builder.append(", "); @@ -406,42 +295,6 @@ private static String flatten(@Nullable Object value) { return null; } - @Nullable - private Lyrics parseSyncedLyrics(JSONArray syncedLines, long trackId, String rawFormat, - @Nullable List creditLines) { - List lines = new ArrayList<>(); - - for (int i = 0; i < syncedLines.length(); i++) { - JSONObject item = syncedLines.optJSONObject(i); - if (item == null) continue; - - final long startMs = item.optLong("milliseconds", 0); - String text = item.optString("line", ""); - if (text.isEmpty()) continue; - - lines.add(new LyricsLine(startMs, text)); - } - - if (lines.isEmpty()) return null; - - LRCParser.fillEstimatedEnds(lines); - - String sourceUrl = "https://www.deezer.com/track/" + trackId; - return new Lyrics(lines, name(), true, null, null, null, creditLines, - rawFormat, "dzr.json", sourceUrl); - } - - @Nullable - private Lyrics parsePlainText(String text, long trackId, String rawFormat, - @Nullable List creditLines) { - List lines = LyricsRequests.parsePlainTextLines(text); - if (lines.isEmpty()) return null; - - String sourceUrl = "https://www.deezer.com/track/" + trackId; - return new Lyrics(lines, name(), false, null, null, null, creditLines, - rawFormat, "txt", sourceUrl); - } - public static boolean validateArl(String arl) { if (arl == null || arl.trim().isEmpty() || "null".equals(arl)) return false; return accountId(arl) > 0; @@ -449,7 +302,7 @@ public static boolean validateArl(String arl) { /** * @return the account behind the arl, or 0 when the cookie is rejected. Deezer answers - * an anonymous session instead of an error, so the user id is what tells them apart. + * an anonymous session instead of an error, so the user id is what tells them apart. */ private static long accountId(String arl) { try { @@ -479,4 +332,151 @@ private static long accountId(String arl) { return 0; } } + + @Override + public String name() { + return "Deezer"; + } + + @Override + protected List fetchScoredSongs(TrackInfo track) throws Exception { + String arl = getArl(); + if (arl == null) { + return Collections.emptyList(); + } + + String jwt = getJwt(arl); + if (jwt == null) { + return Collections.emptyList(); + } + + JSONArray searchResults = searchTracks(track); + if (searchResults == null || searchResults.length() == 0) { + return Collections.emptyList(); + } + + List sorted = sortCandidates(searchResults, track); + + List scored = new ArrayList<>(); + for (JSONObject item : sorted) { + if (scored.size() >= LyricsRequests.MAX_CANDIDATES) break; + + final long trackId = item.optLong("id", -1); + if (trackId <= 0) continue; + + try { + Lyrics lyrics = fetchLyricsByTrackId(trackId, arl); + if (lyrics != null) { + String title = item.optString("title", ""); + JSONObject artistObj = item.optJSONObject("artist"); + String artist = artistObj != null ? artistObj.optString("name", "") : ""; + long durationSec = item.optInt("duration", 0); + int score = LyricsRequests.scoreLyricsCandidate( + title, artist, durationSec, lyrics, track); + scored.add(new ScoredSong(score, lyrics, title, artist, durationSec)); + } + } catch (Exception ex) { + Logger.printDebug(() -> "Could not fetch Deezer lyrics by track ID", ex); + } + } + + scored.sort((a, b) -> Integer.compare(b.score(), a.score())); + return scored; + } + + @Nullable + private JSONArray searchTracks(TrackInfo track) throws Exception { + LyricsRequests.throttle(lastRequestTime, REQUEST_THROTTLE_MS); + + String query = track.artist() + " - " + track.title(); + String url = SEARCH_URL + + "?q=" + LyricsRequests.encode(query) + + "&limit=10" + + "&output=json"; + + HttpURLConnection connection; + try { + connection = LyricsRequests.openConnection(url, Map.of("Accept", "application/json")); + } catch (IOException ex) { + Logger.printDebug(() -> "Could not open Deezer search connection", ex); + return null; + } + + try { + final int httpCode = connection.getResponseCode(); + if (httpCode != Requester.HTTP_STATUS_CODE_SUCCESS) return null; + JSONObject response = Requester.parseJSONObject(connection); + return response.optJSONArray("data"); + } catch (IOException ex) { + Logger.printDebug(() -> "Could not parse Deezer search response", ex); + return null; + } finally { + connection.disconnect(); + } + } + + @Nullable + private Lyrics fetchLyricsByTrackId(long trackId, String arl) throws Exception { + JSONObject response = postPipe(payload(trackId, LYRICS_QUERY), arl, true); + if (response != null && response.has("errors")) { + // The credit fields are not in every schema version; ask for the lyrics alone. + response = postPipe(payload(trackId, LYRICS_QUERY_MINIMAL), arl, true); + } + JSONObject lyrics = response == null + ? null : LyricsRequests.optPath(response, "data", "track", "lyrics"); + if (lyrics == null) { + return null; + } + + JSONArray syncedLines = lyrics.optJSONArray("synchronizedLines"); + if (syncedLines != null && syncedLines.length() > 0) { + String rawFormat = syncedLines.toString(); + Lyrics synced = parseSyncedLyrics(syncedLines, trackId, rawFormat, creditLines(lyrics)); + if (synced != null) { + return synced; + } + } + + String text = LyricsRequests.optString(lyrics, "text"); + if (text != null) { + return parsePlainText(text, trackId, text, creditLines(lyrics)); + } + return null; + } + + @Nullable + private Lyrics parseSyncedLyrics(JSONArray syncedLines, long trackId, String rawFormat, + @Nullable List creditLines) { + List lines = new ArrayList<>(); + + for (int i = 0; i < syncedLines.length(); i++) { + JSONObject item = syncedLines.optJSONObject(i); + if (item == null) continue; + + final long startMs = item.optLong("milliseconds", 0); + String text = item.optString("line", ""); + if (text.isEmpty()) continue; + + lines.add(new LyricsLine(startMs, text)); + } + + if (lines.isEmpty()) return null; + + LRCParser.fillEstimatedEnds(lines); + + String sourceUrl = "https://www.deezer.com/track/" + trackId; + return new Lyrics(lines, name(), true, null, null, null, creditLines, + rawFormat, "dzr.json", sourceUrl); + } + + @Nullable + private Lyrics parsePlainText(String text, long trackId, String rawFormat, + @Nullable List creditLines) { + List lines = LyricsRequests.parsePlainTextLines(text); + if (lines.isEmpty()) return null; + + String sourceUrl = "https://www.deezer.com/track/" + trackId; + return new Lyrics(lines, name(), false, null, null, null, creditLines, + rawFormat, "txt", sourceUrl); + } } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/GeniusProvider.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/GeniusProvider.java index 853d382394..38d0e78032 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/GeniusProvider.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/GeniusProvider.java @@ -145,64 +145,6 @@ private static String languageCode(String name) { return mapped != null ? mapped : trimmed; } - @Override - public String name() { - return "Genius"; - } - - /** One search, one page, no candidate fan-out: only the first accepted hit is read. */ - @Nullable - @Override - public FetchResult fetch(TrackInfo track) throws Exception { - JSONObject hit = findBestHit(track); - if (hit == null) { - return null; - } - String title = hit.optString("title", ""); - String artist = primaryArtistName(hit); - String url = hit.optString("url", ""); - if (url.isEmpty()) { - return null; - } - - long songId = hit.optLong("id", 0L); - JSONObject[] holder = new JSONObject[1]; - Thread recordThread = new Thread(() -> holder[0] = songRecord(songId), - "Genius song record"); - recordThread.start(); - - String html = fetchPage(url); - try { - recordThread.join(RECORD_JOIN_MS); - } catch (InterruptedException ex) { - Thread.currentThread().interrupt(); - } - JSONObject record = holder[0]; - - if (html == null) { - return null; - } - String text = lyricsText(html); - if (text == null || text.isEmpty()) { - return null; - } - LRCParser.PlainLyrics parsed = LRCParser.parsePlainWithSlots(text); - List lines = parsed.lines(); - if (lines.isEmpty()) { - return null; - } - - List credits = record != null ? credits(record) : null; - Map> translations = record != null - ? translations(record, lines, text, parsed.slots()) - : null; - - return FetchResult.of( - new Lyrics(lines, "Genius", false, null, translations, null, - credits, text, "txt", url, null), - title, artist, 0L, track); - } - /** * The first hit the shared matcher accepts. Keeping that one rather than the highest score * is what holds the lookup to a single page request. @@ -339,8 +281,8 @@ private static void addCredit(List credits, String label, */ @Nullable private static Map> translations(JSONObject record, - List lines, String text, - int[] slots) { + List lines, String text, + int[] slots) { String target = LyricsRequests.primarySubtag(LyricsRequests.translationLanguage()); if (target.isEmpty()) { return null; @@ -469,8 +411,8 @@ private static List align(String original, String translation, int lineC /** * @return Where source line {@code rawIndex} ended up in the parsed lyrics, or {@code -1} - * when it has no line there because it was a heading, was past the end of the list, - * or the index was out of range. + * when it has no line there because it was a heading, was past the end of the list, + * or the index was out of range. */ private static int lineOf(int rawIndex, int[] slots, int lineCount) { if (rawIndex < 0 || rawIndex >= slots.length) { @@ -480,9 +422,6 @@ private static int lineOf(int rawIndex, int[] slots, int lineCount) { return at >= 0 && at < lineCount ? at : -1; } - private record Section(int start, List lines) { - } - private static List

    sections(String text) { List
    out = new ArrayList<>(); List current = new ArrayList<>(); @@ -653,11 +592,6 @@ private static boolean sleepBeforeRetry(long waitMs) { return !Thread.currentThread().isInterrupted(); } - private interface Reader { - @Nullable - T read(HttpURLConnection connection) throws Exception; - } - /** The wait Genius asks for, capped so a hostile header cannot stall the lookup. */ private static long retryAfterMs(HttpURLConnection connection) { String value = connection.getHeaderField("Retry-After"); @@ -738,7 +672,7 @@ static String lyricsText(@Nullable String html) { } else { if (excludedAt < 0 && Objects.requireNonNull(tag.group(3)).contains( - "data-exclude-from-selection=\"true\"")) { + "data-exclude-from-selection=\"true\"")) { excludedAt = depth; } depth++; @@ -764,4 +698,70 @@ static String lyricsText(@Nullable String html) { } return BLANK_RUN.matcher(joined).replaceAll("\n\n").trim(); } + + @Override + public String name() { + return "Genius"; + } + + /** One search, one page, no candidate fan-out: only the first accepted hit is read. */ + @Nullable + @Override + public FetchResult fetch(TrackInfo track) throws Exception { + JSONObject hit = findBestHit(track); + if (hit == null) { + return null; + } + String title = hit.optString("title", ""); + String artist = primaryArtistName(hit); + String url = hit.optString("url", ""); + if (url.isEmpty()) { + return null; + } + + long songId = hit.optLong("id", 0L); + JSONObject[] holder = new JSONObject[1]; + Thread recordThread = new Thread(() -> holder[0] = songRecord(songId), + "Genius song record"); + recordThread.start(); + + String html = fetchPage(url); + try { + recordThread.join(RECORD_JOIN_MS); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + JSONObject record = holder[0]; + + if (html == null) { + return null; + } + String text = lyricsText(html); + if (text == null || text.isEmpty()) { + return null; + } + LRCParser.PlainLyrics parsed = LRCParser.parsePlainWithSlots(text); + List lines = parsed.lines(); + if (lines.isEmpty()) { + return null; + } + + List credits = record != null ? credits(record) : null; + Map> translations = record != null + ? translations(record, lines, text, parsed.slots()) + : null; + + return FetchResult.of( + new Lyrics(lines, "Genius", false, null, translations, null, + credits, text, "txt", url, null), + title, artist, 0L, track); + } + + private interface Reader { + @Nullable + T read(HttpURLConnection connection) throws Exception; + } + + private record Section(int start, List lines) { + } } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/KuGouProvider.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/KuGouProvider.java index b49c78fa4a..1cb836221a 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/KuGouProvider.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/KuGouProvider.java @@ -55,190 +55,6 @@ public final class KuGouProvider implements LyricsProvider { 64, 71, 97, 119, 94, 50, 116, 71, 81, 54, 49, 45, (byte) 206, (byte) 210, 110, 105 }; - @Override - public String name() { - return "KuGou"; - } - - @Override - public boolean hasCandidates() { - return true; - } - - @Nullable - @Override - public FetchResult fetch(TrackInfo track) throws Exception { - SongInfo songInfo = resolveHash(track); - if (songInfo == null || songInfo.hash().isEmpty()) { - return null; - } - String hash = songInfo.hash(); - String id = songInfo.id(); - if (id.isEmpty()) { - id = hash; - } - - JSONArray candidates = searchCandidates(hash, true); - if (candidates == null || candidates.length() == 0) { - return null; - } - - JSONObject candidate = candidates.optJSONObject(0); - if (candidate == null) { - return null; - } - - String sourceUrl = "https://www.kugou.com/song/" + id + ".html"; - Lyrics lyrics = fetchFromCandidate(candidate, sourceUrl, true); - if (lyrics == null) { - return null; - } - return FetchResult.of(lyrics, songInfo.title(), songInfo.artist(), - songInfo.durationSec(), track); - } - - @Override - public List fetchCandidates(TrackInfo track) throws Exception { - SongInfo songInfo = resolveHash(track); - if (songInfo == null || songInfo.hash().isEmpty()) { - return new ArrayList<>(); - } - String hash = songInfo.hash(); - String id = songInfo.id(); - if (id.isEmpty()) { - id = hash; - } - - JSONArray candidates = searchCandidates(hash, false); - if (candidates == null || candidates.length() == 0) { - return new ArrayList<>(); - } - - List scored = new ArrayList<>(); - for (int i = 0; i < candidates.length(); i++) { - if (scored.size() >= LyricsRequests.MAX_CANDIDATES) { - break; - } - JSONObject candidate = candidates.optJSONObject(i); - if (candidate == null) { - continue; - } - try { - String sourceUrl = "https://www.kugou.com/song/" + id + ".html"; - Lyrics lyrics = fetchFromCandidate(candidate, sourceUrl, false); - if (lyrics != null) { - int score = LyricsRequests.scoreSingleResult(lyrics); - scored.add(new Lyrics.ScoredLyrics(score, lyrics)); - } - } catch (Exception ex) { - Logger.printDebug(() -> "Could not fetch KuGou lyrics for a candidate", ex); - } - } - - return Lyrics.sortScoredByScore(scored); - } - - /** - * The candidates a hash search answers with, or null when the search itself failed. Only the - * single-candidate path reports that, for the reason {@link #fetchFromCandidate} gives. - */ - @Nullable - private JSONArray searchCandidates(String hash, boolean logFailure) throws Exception { - String searchUrl = SEARCH_URL + LyricsRequests.encode(hash); - HttpURLConnection searchConnection = LyricsRequests.openConnection(searchUrl); - if (searchConnection.getResponseCode() != Requester.HTTP_STATUS_CODE_SUCCESS) { - if (logFailure) { - LyricsRequests.logFailure(name(), searchConnection); - } - return null; - } - JSONObject searchResponse = Requester.parseJSONObject(searchConnection); - return searchResponse.optJSONArray("candidates"); - } - - /** - * The lyric one search candidate carries. The single-candidate path reports a failed download, - * since nothing else will try that candidate; the queue path stays quiet, because it is about - * to try the next one. - */ - @Nullable - private Lyrics fetchFromCandidate(JSONObject candidate, @Nullable String sourceUrl, - boolean logFailure) throws Exception { - String id = candidate.optString("id", ""); - String accessKey = candidate.optString("accesskey", ""); - if (id.isEmpty() || accessKey.isEmpty()) { - return null; - } - - String downloadUrl = DOWNLOAD_URL + "&id=" + LyricsRequests.encode(id) + "&accesskey=" + LyricsRequests.encode(accessKey); - HttpURLConnection downloadConnection = LyricsRequests.openConnection(downloadUrl); - if (downloadConnection.getResponseCode() != Requester.HTTP_STATUS_CODE_SUCCESS) { - if (logFailure) { - LyricsRequests.logFailure(name(), downloadConnection); - } - return null; - } - - JSONObject downloadResponse = Requester.parseJSONObject(downloadConnection); - String content = downloadResponse.optString("content", ""); - if (content.isEmpty()) { - return null; - } - - DecodedKrc decoded = decodeContent(content); - if (decoded == null) { - return null; - } - - return new Lyrics(decoded.lines(), name(), true, decoded.attachedRomanization(), - decoded.translations(), null, decoded.creditLines(), decoded.rawFormat(), - decoded.formatType(), sourceUrl); - } - - /** The decoded payload of a download, ready to be turned into lyrics. */ - private record DecodedKrc(List lines, @Nullable List creditLines, - @Nullable List attachedRomanization, - @Nullable Map> translations, - String rawFormat, String formatType) { - } - - @Nullable - private DecodedKrc decodeContent(String content) throws IOException { - byte[] raw = Base64.decode(content, Base64.DEFAULT); - KrcResult krcResult; - String rawFormat; - String formatType; - if (raw.length > 4 && raw[0] == 'k' && raw[1] == 'r' && raw[2] == 'c' && raw[3] == '1') { - rawFormat = decryptKrc(raw); - krcResult = parseKrc(rawFormat); - formatType = "krc"; - } else { - rawFormat = new String(raw, StandardCharsets.UTF_8); - List metadataCreditLines = LRCParser.extractCreditMetadata(rawFormat); - krcResult = new KrcResult(LRCParser.parseSynced(rawFormat), metadataCreditLines, null, null); - formatType = "lrc"; - } - List lines = krcResult.lines(); - if (lines.isEmpty()) { - return null; - } - List creditLines = new ArrayList<>(krcResult.creditLines()); - final boolean simplifiedChinese = LyricsRequests.isSimplifiedChinese(); - List attachedRomanization = null; - Map> translations = null; - if (simplifiedChinese) { - List romanization = LyricsMerge.mergeRomanization(lines, krcResult.romanization()); - if (LyricsMerge.hasText(romanization)) { - attachedRomanization = romanization; - } - List translation = LyricsMerge.mergeRomanization(lines, krcResult.translation()); - translations = LyricsMerge.singleLanguageTranslations( - translation, LyricsRequests.SIMPLIFIED_CHINESE); - } - return new DecodedKrc(lines, creditLines.isEmpty() ? null : creditLines, - attachedRomanization, translations, rawFormat, formatType); - } - @Nullable private static SongInfo resolveHash(TrackInfo track) throws IOException, JSONException { String keyword = track.artist() + " " + track.title(); @@ -310,28 +126,6 @@ private static String decryptKrc(byte[] raw) throws IOException { return out.toString(StandardCharsets.UTF_8.name()); } - private record SongInfo(String hash, String id, String title, String artist, - int durationSec) { - SongInfo { - id = id != null ? id : ""; - title = title != null ? title : ""; - artist = artist != null ? artist : ""; - } - } - - private record KrcResult(List lines, List creditLines, - @Nullable List romanization, - @Nullable List translation) { - KrcResult { - creditLines = creditLines != null ? creditLines : List.of(); - } - } - - /** Romanization (type 0) and translation (type 1) extracted from a KRC {@code [language]} tag. */ - private record KrcAuxiliary(@Nullable List romanization, - @Nullable List translation) { - } - private static KrcResult parseKrc(String krc) { List creditLines = new ArrayList<>(); if (krc == null || krc.isEmpty()) { @@ -378,7 +172,7 @@ private static KrcResult parseKrc(String krc) { auxiliary == null ? null : auxiliary.translation()); } - /** + /** * Applies the file's {@code [offset]} tag, which belongs to the lyrics themselves rather * than to the offset the user configures. The caller has already negated it, because a * positive tag means the lyrics are shown earlier. @@ -510,4 +304,210 @@ private static String joinKrcRomaEntry(@Nullable JSONArray entry) { } return builder.toString(); } + + @Override + public String name() { + return "KuGou"; + } + + @Override + public boolean hasCandidates() { + return true; + } + + @Nullable + @Override + public FetchResult fetch(TrackInfo track) throws Exception { + SongInfo songInfo = resolveHash(track); + if (songInfo == null || songInfo.hash().isEmpty()) { + return null; + } + String hash = songInfo.hash(); + String id = songInfo.id(); + if (id.isEmpty()) { + id = hash; + } + + JSONArray candidates = searchCandidates(hash, true); + if (candidates == null || candidates.length() == 0) { + return null; + } + + JSONObject candidate = candidates.optJSONObject(0); + if (candidate == null) { + return null; + } + + String sourceUrl = "https://www.kugou.com/song/" + id + ".html"; + Lyrics lyrics = fetchFromCandidate(candidate, sourceUrl, true); + if (lyrics == null) { + return null; + } + return FetchResult.of(lyrics, songInfo.title(), songInfo.artist(), + songInfo.durationSec(), track); + } + + @Override + public List fetchCandidates(TrackInfo track) throws Exception { + SongInfo songInfo = resolveHash(track); + if (songInfo == null || songInfo.hash().isEmpty()) { + return new ArrayList<>(); + } + String hash = songInfo.hash(); + String id = songInfo.id(); + if (id.isEmpty()) { + id = hash; + } + + JSONArray candidates = searchCandidates(hash, false); + if (candidates == null || candidates.length() == 0) { + return new ArrayList<>(); + } + + List scored = new ArrayList<>(); + for (int i = 0; i < candidates.length(); i++) { + if (scored.size() >= LyricsRequests.MAX_CANDIDATES) { + break; + } + JSONObject candidate = candidates.optJSONObject(i); + if (candidate == null) { + continue; + } + try { + String sourceUrl = "https://www.kugou.com/song/" + id + ".html"; + Lyrics lyrics = fetchFromCandidate(candidate, sourceUrl, false); + if (lyrics != null) { + int score = LyricsRequests.scoreSingleResult(lyrics); + scored.add(new Lyrics.ScoredLyrics(score, lyrics)); + } + } catch (Exception ex) { + Logger.printDebug(() -> "Could not fetch KuGou lyrics for a candidate", ex); + } + } + + return Lyrics.sortScoredByScore(scored); + } + + /** + * The candidates a hash search answers with, or null when the search itself failed. Only the + * single-candidate path reports that, for the reason {@link #fetchFromCandidate} gives. + */ + @Nullable + private JSONArray searchCandidates(String hash, boolean logFailure) throws Exception { + String searchUrl = SEARCH_URL + LyricsRequests.encode(hash); + HttpURLConnection searchConnection = LyricsRequests.openConnection(searchUrl); + if (searchConnection.getResponseCode() != Requester.HTTP_STATUS_CODE_SUCCESS) { + if (logFailure) { + LyricsRequests.logFailure(name(), searchConnection); + } + return null; + } + JSONObject searchResponse = Requester.parseJSONObject(searchConnection); + return searchResponse.optJSONArray("candidates"); + } + + /** + * The lyric one search candidate carries. The single-candidate path reports a failed download, + * since nothing else will try that candidate; the queue path stays quiet, because it is about + * to try the next one. + */ + @Nullable + private Lyrics fetchFromCandidate(JSONObject candidate, @Nullable String sourceUrl, + boolean logFailure) throws Exception { + String id = candidate.optString("id", ""); + String accessKey = candidate.optString("accesskey", ""); + if (id.isEmpty() || accessKey.isEmpty()) { + return null; + } + + String downloadUrl = DOWNLOAD_URL + "&id=" + LyricsRequests.encode(id) + "&accesskey=" + LyricsRequests.encode(accessKey); + HttpURLConnection downloadConnection = LyricsRequests.openConnection(downloadUrl); + if (downloadConnection.getResponseCode() != Requester.HTTP_STATUS_CODE_SUCCESS) { + if (logFailure) { + LyricsRequests.logFailure(name(), downloadConnection); + } + return null; + } + + JSONObject downloadResponse = Requester.parseJSONObject(downloadConnection); + String content = downloadResponse.optString("content", ""); + if (content.isEmpty()) { + return null; + } + + DecodedKrc decoded = decodeContent(content); + if (decoded == null) { + return null; + } + + return new Lyrics(decoded.lines(), name(), true, decoded.attachedRomanization(), + decoded.translations(), null, decoded.creditLines(), decoded.rawFormat(), + decoded.formatType(), sourceUrl); + } + + @Nullable + private DecodedKrc decodeContent(String content) throws IOException { + byte[] raw = Base64.decode(content, Base64.DEFAULT); + KrcResult krcResult; + String rawFormat; + String formatType; + if (raw.length > 4 && raw[0] == 'k' && raw[1] == 'r' && raw[2] == 'c' && raw[3] == '1') { + rawFormat = decryptKrc(raw); + krcResult = parseKrc(rawFormat); + formatType = "krc"; + } else { + rawFormat = new String(raw, StandardCharsets.UTF_8); + List metadataCreditLines = LRCParser.extractCreditMetadata(rawFormat); + krcResult = new KrcResult(LRCParser.parseSynced(rawFormat), metadataCreditLines, null, null); + formatType = "lrc"; + } + List lines = krcResult.lines(); + if (lines.isEmpty()) { + return null; + } + List creditLines = new ArrayList<>(krcResult.creditLines()); + final boolean simplifiedChinese = LyricsRequests.isSimplifiedChinese(); + List attachedRomanization = null; + Map> translations = null; + if (simplifiedChinese) { + List romanization = LyricsMerge.mergeRomanization(lines, krcResult.romanization()); + if (LyricsMerge.hasText(romanization)) { + attachedRomanization = romanization; + } + List translation = LyricsMerge.mergeRomanization(lines, krcResult.translation()); + translations = LyricsMerge.singleLanguageTranslations( + translation, LyricsRequests.SIMPLIFIED_CHINESE); + } + return new DecodedKrc(lines, creditLines.isEmpty() ? null : creditLines, + attachedRomanization, translations, rawFormat, formatType); + } + + /** The decoded payload of a download, ready to be turned into lyrics. */ + private record DecodedKrc(List lines, @Nullable List creditLines, + @Nullable List attachedRomanization, + @Nullable Map> translations, + String rawFormat, String formatType) { + } + + private record SongInfo(String hash, String id, String title, String artist, + int durationSec) { + SongInfo { + id = id != null ? id : ""; + title = title != null ? title : ""; + artist = artist != null ? artist : ""; + } + } + + private record KrcResult(List lines, List creditLines, + @Nullable List romanization, + @Nullable List translation) { + KrcResult { + creditLines = creditLines != null ? creditLines : List.of(); + } + } + + /** Romanization (type 0) and translation (type 1) extracted from a KRC {@code [language]} tag. */ + private record KrcAuxiliary(@Nullable List romanization, + @Nullable List translation) { + } } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/LRCLIBProvider.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/LRCLIBProvider.java index a2439a93eb..585b23e863 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/LRCLIBProvider.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/LRCLIBProvider.java @@ -47,6 +47,24 @@ public final class LRCLIBProvider implements LyricsProvider { private static final Map HEADERS = Map.of("Lrclib-App-Id", APP_ID); + /** + * Orders candidates so every synced one comes before every plain one, by score within + * each group. Word timings rank above line timings. + */ + static List syncedFirst(List scored) { + List ordered = new ArrayList<>(Lyrics.sortScoredByScore(scored)); + // List.sort is stable, so the score order survives within each timing rank. + ordered.sort((a, b) -> LyricsRequests.syncRank(b.lyrics()) - LyricsRequests.syncRank(a.lyrics())); + return ordered; + } + + private static int scoreCandidate(JSONObject item, TrackInfo track) { + String title = item.optString("trackName", ""); + String artist = item.optString("artistName", ""); + return LyricsRequests.scoreTrackCandidate(title, artist, + item.optInt("duration", 0), track); + } + @Override public String name() { return "LRCLIB"; @@ -153,24 +171,6 @@ public List fetchCandidates(TrackInfo track) throws Excepti : ordered; } - /** - * Orders candidates so every synced one comes before every plain one, by score within - * each group. Word timings rank above line timings. - */ - static List syncedFirst(List scored) { - List ordered = new ArrayList<>(Lyrics.sortScoredByScore(scored)); - // List.sort is stable, so the score order survives within each timing rank. - ordered.sort((a, b) -> LyricsRequests.syncRank(b.lyrics()) - LyricsRequests.syncRank(a.lyrics())); - return ordered; - } - - private static int scoreCandidate(JSONObject item, TrackInfo track) { - String title = item.optString("trackName", ""); - String artist = item.optString("artistName", ""); - return LyricsRequests.scoreTrackCandidate(title, artist, - item.optInt("duration", 0), track); - } - @Nullable private FetchResult fetchExact(TrackInfo track) throws Exception { StringBuilder url = new StringBuilder(BASE_URL); @@ -258,7 +258,7 @@ private Lyrics toLyrics(JSONObject response) { } final int id = response.optInt("id", -1); - String sourceUrl = id > 0 ? "https://lrclib.net/tracks/" + id : null; + final String sourceUrl = id > 0 ? "https://lrclib.net/tracks/" + id : null; String lyricsFile = LyricsRequests.optString(response, "lyricsFile"); if (lyricsFile == null) { diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/LocalLyricsFetcher.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/LocalLyricsFetcher.java index 6db359cf39..99e1e449cb 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/LocalLyricsFetcher.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/LocalLyricsFetcher.java @@ -27,26 +27,25 @@ import app.morphe.extension.music.patches.lyrics.model.Lyrics; import app.morphe.extension.music.patches.lyrics.model.LyricsLine; -import app.morphe.extension.shared.Logger; -import app.morphe.extension.shared.Utils; import app.morphe.extension.music.patches.lyrics.parsers.CharactersConverter; import app.morphe.extension.music.patches.lyrics.parsers.LRCParser; +import app.morphe.extension.shared.Logger; +import app.morphe.extension.shared.Utils; public final class LocalLyricsFetcher { - private LocalLyricsFetcher() { - } - /** * Value of {@code MediaMetadataRetriever.METADATA_KEY_LYRICS}. Kept as a literal because the * music modules compile SDK does not expose the constant symbol. The key is only honored by * the framework on API 30+, so it is used only as a best-effort fallback after direct parsing. */ private static final int METADATA_KEY_LYRICS = 0x104; - private static final int MAX_TAG_BYTES = 16 * 1024 * 1024; - private static final int M4A_TAIL_BYTES = 4 * 1024 * 1024; + private static final int MAX_M4A_DEPTH = 32; + + private LocalLyricsFetcher() { + } @Nullable public static Lyrics fetch(@Nullable Uri mediaUri) { @@ -144,11 +143,11 @@ public static Uri resolveMediaStoreUri(String title, String artist, int duration } String selection = MediaStore.Audio.Media.TITLE + " LIKE ? ESCAPE '\\'"; Cursor cursor = resolver.query(base, projection, selection, - new String[]{ likeEscape(t) }, null); + new String[]{likeEscape(t)}, null); if (cursor != null && cursor.getCount() == 0) { cursor.close(); cursor = resolver.query(base, projection, selection, - new String[]{ "%" + likeEscape(t) + "%" }, null); + new String[]{"%" + likeEscape(t) + "%"}, null); } if (cursor == null) { continue; @@ -351,7 +350,7 @@ private static String parseId3v2(byte[] d) { final int size = (headerLen == 6) ? (((d[pos + 3] & 0xFF) << 16) | ((d[pos + 4] & 0xFF) << 8) | (d[pos + 5] & 0xFF)) : (syncsafeSize ? syncsafe(d[pos + 4], d[pos + 5], d[pos + 6], d[pos + 7]) - : readInt32BE(d, pos + 4)); + : readInt32BE(d, pos + 4)); final int bodyOff = pos + headerLen; if (size < 0 || bodyOff + size > d.length) { break; @@ -720,8 +719,6 @@ private static String parseVorbisCommentFromPacket(String payload, int off) { return null; } - private static final int MAX_M4A_DEPTH = 32; - @Nullable private static String walkM4a(byte[] d, int start, int end, int depth) { if (depth > MAX_M4A_DEPTH) { diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/LrcRedProvider.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/LrcRedProvider.java index 1f25f167f6..052d992423 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/LrcRedProvider.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/LrcRedProvider.java @@ -52,6 +52,158 @@ public final class LrcRedProvider implements LyricsProvider { "[(x\\[](?:official(?:\\s+(?:music\\s+)?video|\\s+audio)?|music video|video|audio|lyrics?|visualizer|album version|explicit)[^)\\]]*[)\\]]", Pattern.CASE_INSENSITIVE); + private static int scoreCandidate(JSONObject item, TrackInfo track) { + int targetDuration = track.durationSeconds(); + int itemDuration = item.optInt("duration", 0); + + String itemTrackName = item.optString("track_name", ""); + String itemArtistName = item.optString("artist_name", ""); + String itemAlbumName = item.optString("album_name", ""); + String itemTimingType = item.optString("timing_type", ""); + + String targetFullTitle = normalize(track.title()); + String targetBaseTitle = cleanBaseTitle(track.title()); + String targetArtist = normalize(track.artist()); + String targetAlbum = normalize(track.album()); + + String itemFullTitle = normalize(itemTrackName); + String itemBaseTitle = cleanBaseTitle(itemTrackName); + String itemArtist = normalize(itemArtistName); + String itemAlbum = normalize(itemAlbumName); + + int score = 0; + + // Title. + if (!targetFullTitle.isEmpty() && targetFullTitle.equals(itemFullTitle)) { + score += 50; + } else if (!targetBaseTitle.isEmpty() && !itemBaseTitle.isEmpty()) { + if (targetBaseTitle.equals(itemBaseTitle)) { + score += 35; + } else if (itemBaseTitle.contains(targetBaseTitle) || targetBaseTitle.contains(itemBaseTitle)) { + score += 15; + } else { + score -= 40; + } + } + + // Artist. + if (!targetArtist.isEmpty() && !itemArtist.isEmpty()) { + if (targetArtist.equals(itemArtist)) { + score += 30; + } else if (itemArtist.contains(targetArtist) || targetArtist.contains(itemArtist)) { + score += 15; + } else { + List targetArtists = extractArtists(track.artist()); + List itemArtists = extractArtists(itemArtistName); + boolean hasOverlap = false; + for (String ta : targetArtists) { + for (String ia : itemArtists) { + if (ia.equals(ta) || ia.contains(ta) || ta.contains(ia)) { + hasOverlap = true; + break; + } + } + if (hasOverlap) { + break; + } + } + if (hasOverlap) { + score += 10; + } else { + score -= 50; + } + } + } + + // Album. + if (!targetAlbum.isEmpty() && !itemAlbum.isEmpty()) { + if (targetAlbum.equals(itemAlbum)) { + score += 35; + } else if (itemAlbum.contains(targetAlbum) || targetAlbum.contains(itemAlbum)) { + score += 20; + } + + boolean targetIsSingle = targetAlbum.contains("single"); + boolean itemIsSingle = itemAlbum.contains("single"); + if (!targetIsSingle && itemIsSingle) { + score -= 15; + } else if (targetIsSingle && itemIsSingle) { + score += 10; + } + } + + // Duration. + if (targetDuration > 0 && itemDuration > 0) { + int diff = Math.abs(itemDuration - targetDuration); + if (diff == 0) { + score += 25; + } else if (diff <= 1) { + score += 20; + } else if (diff <= 2) { + score += 15; + } else if (diff <= 5) { + score += 5; + } else if (diff > 15) { + score -= 30; + } + } + + // Word sync is preferred over line sync. + if ("word".equalsIgnoreCase(itemTimingType)) { + score += 15; + } else if ("line".equalsIgnoreCase(itemTimingType)) { + score += 8; + } else { + score -= 10; + } + + // A remix, cover or other variant the track is not. + String trackLower = (track.title() + " " + track.album()).toLowerCase(Locale.ROOT); + String itemLower = (itemTrackName + " " + itemAlbumName).toLowerCase(Locale.ROOT); + for (String kw : MODIFIER_KEYWORDS) { + if (itemLower.contains(kw) && !trackLower.contains(kw)) { + score -= 30; + } + } + + return score; + } + + private static String normalize(String str) { + if (str == null) { + return ""; + } + return str.toLowerCase(Locale.ROOT) + .replaceAll("[^\\p{L}\\p{N}\\s]", " ") + .replaceAll("\\s+", " ") + .trim(); + } + + private static String cleanBaseTitle(String str) { + if (str == null) { + return ""; + } + String s = BRACKETED_TAGS.matcher(str).replaceAll(""); + s = TRAILING_FEAT.matcher(s).replaceAll(""); + s = VIDEO_TAGS.matcher(s).replaceAll(""); + return normalize(s); + } + + private static List extractArtists(String str) { + if (str == null || str.isEmpty()) { + return Collections.emptyList(); + } + String[] parts = str.split("(?i)[,&/]|\\b(?:feat\\.?|ft\\.?|with|x)\\b"); + List result = new ArrayList<>(parts.length); + for (String p : parts) { + String norm = normalize(p); + if (!norm.isEmpty()) { + result.add(norm); + } + } + return result; + } + @Override public String name() { return "LrcRed"; @@ -206,123 +358,6 @@ private List searchApi(String query, TrackInfo track) { } } - private static int scoreCandidate(JSONObject item, TrackInfo track) { - int targetDuration = track.durationSeconds(); - int itemDuration = item.optInt("duration", 0); - - String itemTrackName = item.optString("track_name", ""); - String itemArtistName = item.optString("artist_name", ""); - String itemAlbumName = item.optString("album_name", ""); - String itemTimingType = item.optString("timing_type", ""); - - String targetFullTitle = normalize(track.title()); - String targetBaseTitle = cleanBaseTitle(track.title()); - String targetArtist = normalize(track.artist()); - String targetAlbum = normalize(track.album()); - - String itemFullTitle = normalize(itemTrackName); - String itemBaseTitle = cleanBaseTitle(itemTrackName); - String itemArtist = normalize(itemArtistName); - String itemAlbum = normalize(itemAlbumName); - - int score = 0; - - // Title. - if (!targetFullTitle.isEmpty() && targetFullTitle.equals(itemFullTitle)) { - score += 50; - } else if (!targetBaseTitle.isEmpty() && !itemBaseTitle.isEmpty()) { - if (targetBaseTitle.equals(itemBaseTitle)) { - score += 35; - } else if (itemBaseTitle.contains(targetBaseTitle) || targetBaseTitle.contains(itemBaseTitle)) { - score += 15; - } else { - score -= 40; - } - } - - // Artist. - if (!targetArtist.isEmpty() && !itemArtist.isEmpty()) { - if (targetArtist.equals(itemArtist)) { - score += 30; - } else if (itemArtist.contains(targetArtist) || targetArtist.contains(itemArtist)) { - score += 15; - } else { - List targetArtists = extractArtists(track.artist()); - List itemArtists = extractArtists(itemArtistName); - boolean hasOverlap = false; - for (String ta : targetArtists) { - for (String ia : itemArtists) { - if (ia.equals(ta) || ia.contains(ta) || ta.contains(ia)) { - hasOverlap = true; - break; - } - } - if (hasOverlap) { - break; - } - } - if (hasOverlap) { - score += 10; - } else { - score -= 50; - } - } - } - - // Album. - if (!targetAlbum.isEmpty() && !itemAlbum.isEmpty()) { - if (targetAlbum.equals(itemAlbum)) { - score += 35; - } else if (itemAlbum.contains(targetAlbum) || targetAlbum.contains(itemAlbum)) { - score += 20; - } - - boolean targetIsSingle = targetAlbum.contains("single"); - boolean itemIsSingle = itemAlbum.contains("single"); - if (!targetIsSingle && itemIsSingle) { - score -= 15; - } else if (targetIsSingle && itemIsSingle) { - score += 10; - } - } - - // Duration. - if (targetDuration > 0 && itemDuration > 0) { - int diff = Math.abs(itemDuration - targetDuration); - if (diff == 0) { - score += 25; - } else if (diff <= 1) { - score += 20; - } else if (diff <= 2) { - score += 15; - } else if (diff <= 5) { - score += 5; - } else if (diff > 15) { - score -= 30; - } - } - - // Word sync is preferred over line sync. - if ("word".equalsIgnoreCase(itemTimingType)) { - score += 15; - } else if ("line".equalsIgnoreCase(itemTimingType)) { - score += 8; - } else { - score -= 10; - } - - // A remix, cover or other variant the track is not. - String trackLower = (track.title() + " " + track.album()).toLowerCase(Locale.ROOT); - String itemLower = (itemTrackName + " " + itemAlbumName).toLowerCase(Locale.ROOT); - for (String kw : MODIFIER_KEYWORDS) { - if (itemLower.contains(kw) && !trackLower.contains(kw)) { - score -= 30; - } - } - - return score; - } - @Nullable private Lyrics downloadLyrics(String lyricsUrl) { if (lyricsUrl == null || lyricsUrl.trim().isEmpty()) { @@ -370,41 +405,6 @@ private Lyrics downloadLyrics(String lyricsUrl) { return null; } - private static String normalize(String str) { - if (str == null) { - return ""; - } - return str.toLowerCase(Locale.ROOT) - .replaceAll("[^\\p{L}\\p{N}\\s]", " ") - .replaceAll("\\s+", " ") - .trim(); - } - - private static String cleanBaseTitle(String str) { - if (str == null) { - return ""; - } - String s = BRACKETED_TAGS.matcher(str).replaceAll(""); - s = TRAILING_FEAT.matcher(s).replaceAll(""); - s = VIDEO_TAGS.matcher(s).replaceAll(""); - return normalize(s); - } - - private static List extractArtists(String str) { - if (str == null || str.isEmpty()) { - return Collections.emptyList(); - } - String[] parts = str.split("(?i)[,&/]|\\b(?:feat\\.?|ft\\.?|with|x)\\b"); - List result = new ArrayList<>(parts.length); - for (String p : parts) { - String norm = normalize(p); - if (!norm.isEmpty()) { - result.add(norm); - } - } - return result; - } - private record ScoredCandidate(JSONObject candidate, int score) { } } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/LunaBeatProvider.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/LunaBeatProvider.java index 717b1885c0..968302a05b 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/LunaBeatProvider.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/LunaBeatProvider.java @@ -28,9 +28,9 @@ import app.morphe.extension.music.patches.lyrics.model.Lyrics; import app.morphe.extension.music.patches.lyrics.model.TrackInfo; import app.morphe.extension.music.patches.lyrics.parsers.TTMLParser; +import app.morphe.extension.music.settings.Settings; import app.morphe.extension.shared.Logger; import app.morphe.extension.shared.requests.Requester; -import app.morphe.extension.music.settings.Settings; public final class LunaBeatProvider extends ScoredSongProvider { @@ -41,15 +41,10 @@ public final class LunaBeatProvider extends ScoredSongProvider { private static final long REQUEST_THROTTLE_MS = 500; private static final AtomicLong lastRequestTime = new AtomicLong(0); - - private static volatile List lunabeatSongs = Collections.emptyList(); - private static volatile String lunabeatCachedRevision; private static final CountDownLatch lunabeatIndexLatch = new CountDownLatch(1); private static final AtomicBoolean lunabeatLoadStarted = new AtomicBoolean(false); - - record Song(String id, String title, String[] artists, String album, - String path, String sha256) { - } + private static volatile List lunabeatSongs = Collections.emptyList(); + private static volatile String lunabeatCachedRevision; public static void preloadIndex() { if (!Settings.LYRICS_ENABLED.get() || !LyricsProviders.isProviderEnabled("LunaBeat")) { @@ -151,6 +146,29 @@ private static List fetchSongIndex() { } } + private static void ensureIndexLoaded() { + if (lunabeatSongs != null && !lunabeatSongs.isEmpty()) { + return; + } + startIndexLoad(); + try { + boolean completed = lunabeatIndexLatch.await(5, TimeUnit.SECONDS); + if (!completed) { + Logger.printDebug(() -> "Timeout waiting for LunaBeat index latch"); + } + } catch (InterruptedException ex) { + Logger.printDebug(() -> "Interrupted waiting for LunaBeat index latch", ex); + Thread.currentThread().interrupt(); + } + } + + private static String normalizeLunabeat(@Nullable String s) { + if (s == null) return ""; + return Normalizer.normalize(s, Normalizer.Form.NFKC) + .toLowerCase(Locale.ROOT) + .replaceAll("[\\s·・.\\-_]", ""); + } + @Override public String name() { return "LunaBeat"; @@ -185,29 +203,6 @@ protected List fetchScoredSongs(TrackInfo track) { return scored; } - private static void ensureIndexLoaded() { - if (lunabeatSongs != null && !lunabeatSongs.isEmpty()) { - return; - } - startIndexLoad(); - try { - boolean completed = lunabeatIndexLatch.await(5, TimeUnit.SECONDS); - if (!completed) { - Logger.printDebug(() -> "Timeout waiting for LunaBeat index latch"); - } - } catch (InterruptedException ex) { - Logger.printDebug(() -> "Interrupted waiting for LunaBeat index latch", ex); - Thread.currentThread().interrupt(); - } - } - - private static String normalizeLunabeat(@Nullable String s) { - if (s == null) return ""; - return Normalizer.normalize(s, Normalizer.Form.NFKC) - .toLowerCase(Locale.ROOT) - .replaceAll("[\\s·・.\\-_]", ""); - } - private List searchLunabeatIndex(TrackInfo track) { String nt = normalizeLunabeat(track.title()); String na = normalizeLunabeat(track.artist()); @@ -258,4 +253,8 @@ private Lyrics fetchLunabeatLyrics(Song song) { if (conn != null) conn.disconnect(); } } + + record Song(String id, String title, String[] artists, String album, + String path, String sha256) { + } } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/LunaProvider.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/LunaProvider.java index 03e57959e2..964cffbf0a 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/LunaProvider.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/LunaProvider.java @@ -44,6 +44,59 @@ public final class LunaProvider extends ScoredSongProvider { private static final long REQUEST_THROTTLE_MS = 250; private static final AtomicLong lastRequestTime = new AtomicLong(0); + private static String firstArtistName(JSONObject trackObj) { + JSONArray artists = trackObj.optJSONArray("artists"); + if (artists != null && artists.length() > 0) { + JSONObject first = artists.optJSONObject(0); + if (first != null) { + return first.optString("name", ""); + } + } + return ""; + } + + private static int scoreCandidate(JSONObject trackObj, TrackInfo track) { + String title = trackObj.optString("name", ""); + String artist = ""; + JSONArray artists = trackObj.optJSONArray("artists"); + if (artists != null && artists.length() > 0) { + JSONObject first = artists.optJSONObject(0); + if (first != null) { + artist = first.optString("name", ""); + } + } + final long durationMs = trackObj.optLong("duration", 0); + return LyricsRequests.scoreTrackCandidate(title, artist, + durationMs > 0 ? durationMs / 1000 : 0, track); + } + + private static boolean isOriginalTrack(JSONObject trackObj) { + JSONObject label = trackObj.optJSONObject("label_info"); + return label != null && label.optBoolean("is_original", false); + } + + @Nullable + private static HttpURLConnection openConnection(String url, String userAgent) { + try { + HttpURLConnection connection = (HttpURLConnection) new java.net.URL(url).openConnection(); + connection.setRequestMethod("GET"); + connection.setRequestProperty("User-Agent", userAgent); + connection.setRequestProperty("Accept", "*/*"); + connection.setConnectTimeout(10000); + connection.setReadTimeout(15000); + return connection; + } catch (Exception ex) { + Logger.printDebug(() -> "Could not open Luna GET connection", ex); + return null; + } + } + + private static String generateClientId() { + ThreadLocalRandom random = ThreadLocalRandom.current(); + return String.valueOf(random.nextLong(10_000_000, 99_999_999)) + + random.nextLong(10_000_000, 99_999_999); + } + @Override public String name() { return "Luna"; @@ -82,37 +135,6 @@ protected List fetchScoredSongs(TrackInfo track) throws Exception { return scored; } - private static String firstArtistName(JSONObject trackObj) { - JSONArray artists = trackObj.optJSONArray("artists"); - if (artists != null && artists.length() > 0) { - JSONObject first = artists.optJSONObject(0); - if (first != null) { - return first.optString("name", ""); - } - } - return ""; - } - - private static int scoreCandidate(JSONObject trackObj, TrackInfo track) { - String title = trackObj.optString("name", ""); - String artist = ""; - JSONArray artists = trackObj.optJSONArray("artists"); - if (artists != null && artists.length() > 0) { - JSONObject first = artists.optJSONObject(0); - if (first != null) { - artist = first.optString("name", ""); - } - } - final long durationMs = trackObj.optLong("duration", 0); - return LyricsRequests.scoreTrackCandidate(title, artist, - durationMs > 0 ? durationMs / 1000 : 0, track); - } - - private static boolean isOriginalTrack(JSONObject trackObj) { - JSONObject label = trackObj.optJSONObject("label_info"); - return label != null && label.optBoolean("is_original", false); - } - private List searchTracks(TrackInfo track) throws Exception { LyricsRequests.throttle(lastRequestTime, REQUEST_THROTTLE_MS); @@ -366,26 +388,4 @@ private List extractSongwriters(JSONObject response) { return songwriters.isEmpty() ? null : songwriters; } - - @Nullable - private static HttpURLConnection openConnection(String url, String userAgent) { - try { - HttpURLConnection connection = (HttpURLConnection) new java.net.URL(url).openConnection(); - connection.setRequestMethod("GET"); - connection.setRequestProperty("User-Agent", userAgent); - connection.setRequestProperty("Accept", "*/*"); - connection.setConnectTimeout(10000); - connection.setReadTimeout(15000); - return connection; - } catch (Exception ex) { - Logger.printDebug(() -> "Could not open Luna GET connection", ex); - return null; - } - } - - private static String generateClientId() { - ThreadLocalRandom random = ThreadLocalRandom.current(); - return String.valueOf(random.nextLong(10_000_000, 99_999_999)) - + random.nextLong(10_000_000, 99_999_999); - } } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/LyricifyProvider.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/LyricifyProvider.java index 8a79d49004..b5a55afa88 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/LyricifyProvider.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/LyricifyProvider.java @@ -48,7 +48,84 @@ public final class LyricifyProvider implements LyricsProvider { private static final ConcurrentHashMap resolvedCache = new ConcurrentHashMap<>(); - private record Resolved(String isrc, List titles, List artists) {} + private static HttpURLConnection openApi(String url) throws IOException { + final HttpURLConnection conn = + (HttpURLConnection) new java.net.URL(url).openConnection(); + conn.setRequestMethod("GET"); + conn.setRequestProperty("User-Agent", USER_AGENT); + conn.setRequestProperty("Accept", "application/json"); + conn.setRequestProperty("Accept-Encoding", "gzip"); + conn.setConnectTimeout(5000); + conn.setReadTimeout(8000); + return conn; + } + + @Nullable + private static Resolved resolveFromCreditsFm(String title, String artist) { + HttpURLConnection connection = null; + try { + final String url = "https://api.credits.fm/v1/resolve/track"; + LyricsRequests.throttle(lastRequestTime, REQUEST_THROTTLE_MS); + connection = (HttpURLConnection) new java.net.URL(url).openConnection(); + connection.setRequestMethod("POST"); + connection.setRequestProperty("Content-Type", "application/json"); + connection.setRequestProperty("Accept", "application/json"); + connection.setRequestProperty("Accept-Encoding", "identity"); + connection.setRequestProperty("User-Agent", USER_AGENT); + connection.setConnectTimeout(5000); + connection.setReadTimeout(15000); + connection.setDoOutput(true); + + final String body = new JSONObject() + .put("name", title) + .put("artist", artist) + .toString(); + final byte[] bodyBytes = body.getBytes(StandardCharsets.UTF_8); + connection.setFixedLengthStreamingMode(bodyBytes.length); + try (OutputStream output = connection.getOutputStream()) { + output.write(bodyBytes); + } + + final int code = connection.getResponseCode(); + if (code == 200) { + final String responseBody = LyricsRequests.parseGzipString(connection); + if (responseBody.isEmpty()) { + return null; + } + JSONObject response = new JSONObject(responseBody); + final String isrc = LyricsRequests.optString(response, "isrc"); + if (isrc == null || isrc.isEmpty()) { + return null; + } + final List titles = new ArrayList<>(2); + final String recordingTitle = + LyricsRequests.optString(response, "recording_title"); + if (recordingTitle != null) { + titles.add(recordingTitle); + } + final String songTitle = LyricsRequests.optString(response, "song_title"); + if (songTitle != null && !titles.contains(songTitle)) { + titles.add(songTitle); + } + return new Resolved(isrc, titles, + LyricsRequests.stringList(response, "artist_names")); + } + } catch (Exception e) { + Logger.printDebug(() -> "Could not read the ISRC", e); + } finally { + if (connection != null) connection.disconnect(); + } + return null; + } + + private static String generateUsername() { + final int len = 5 + RNG.nextInt(8); + final StringBuilder sb = new StringBuilder(len); + for (int i = 0; i < len; i++) { + sb.append((char) ('a' + RNG.nextInt(26))); + } + return sb.toString(); + } @Override public String name() { @@ -58,7 +135,7 @@ public String name() { @Nullable @Override public FetchResult fetch(TrackInfo track) throws Exception { - String cacheKey = track.title().toLowerCase(Locale.ROOT) + final String cacheKey = track.title().toLowerCase(Locale.ROOT) + "|" + track.artist().toLowerCase(Locale.ROOT); Resolved resolved = resolvedCache.get(cacheKey); if (resolved == null) { @@ -70,19 +147,19 @@ public FetchResult fetch(TrackInfo track) throws Exception { if (resolved == null) { return null; } - String isrc = resolved.isrc(); + final String isrc = resolved.isrc(); - String username = generateUsername(); - String isrcParam = Base64.encodeToString( + final String username = generateUsername(); + final String isrcParam = Base64.encodeToString( isrc.getBytes(StandardCharsets.UTF_8), Base64.NO_WRAP); - String isrcEncoded = LyricsRequests.encode(isrcParam); + final String isrcEncoded = LyricsRequests.encode(isrcParam); - String url = API_BASE + final String url = API_BASE + "?username=" + username + "&isrc=" + isrcEncoded; LyricsRequests.throttle(lastRequestTime, REQUEST_THROTTLE_MS); - HttpURLConnection conn = openApi(url); + final HttpURLConnection conn = openApi(url); final int code = conn.getResponseCode(); if (code != 200) { @@ -91,7 +168,7 @@ public FetchResult fetch(TrackInfo track) throws Exception { return null; } - String json = LyricsRequests.parseGzipString(conn); + final String json = LyricsRequests.parseGzipString(conn); conn.disconnect(); if (json.isEmpty()) { @@ -104,17 +181,17 @@ public FetchResult fetch(TrackInfo track) throws Exception { return null; } - String text = LyricsRequests.optString(response, "text"); + final String text = LyricsRequests.optString(response, "text"); if (text == null || text.isEmpty()) { return null; } final int offset = response.optInt("offset", 0); - String writer = LyricsRequests.optString(response, "writer"); - String trans = LyricsRequests.optString(response, "trans"); + final String writer = LyricsRequests.optString(response, "writer"); + final String trans = LyricsRequests.optString(response, "trans"); final boolean isSyllable = text.contains("[from:AppleSyllable]"); - List lines; + final List lines; if (isSyllable) { lines = LyricifyParser.parseSyllable(text, offset); } else { @@ -125,23 +202,23 @@ public FetchResult fetch(TrackInfo track) throws Exception { return null; } - List creditLines = new ArrayList<>(); + final List creditLines = new ArrayList<>(); if (writer != null && !writer.isEmpty()) { creditLines.add("Written by " + writer); } - Map> translations; + final Map> translations; if ("zh".equals(LyricsRequests.primarySubtag(LyricsRequests.translationLanguage())) && trans != null && !trans.isEmpty()) { - List translatedTexts = LyricifyParser.parseTranslation(trans, offset); + final List translatedTexts = LyricifyParser.parseTranslation(trans, offset); if (!translatedTexts.isEmpty()) { while (translatedTexts.size() < lines.size()) { translatedTexts.add(""); } - List translationLines = new ArrayList<>(lines.size()); + final List translationLines = new ArrayList<>(lines.size()); for (int i = 0; i < lines.size(); i++) { - LyricsLine original = lines.get(i); - String tText = translatedTexts.get(i); + final LyricsLine original = lines.get(i); + final String tText = translatedTexts.get(i); if (tText.isEmpty()) { translationLines.add(new LyricsLine( original.startTimeMs(), original.endTimeMs(), "", List.of())); @@ -158,97 +235,21 @@ public FetchResult fetch(TrackInfo track) throws Exception { translations = null; } - String formatType = isSyllable ? "lys" : "lyl"; + final String formatType = isSyllable ? "lys" : "lyl"; return FetchResult.searched(new Lyrics( - lines, - name(), - true, - null, - translations, - null, - creditLines.isEmpty() ? null : creditLines, - text, - formatType, - null), + lines, + name(), + true, + null, + translations, + null, + creditLines.isEmpty() ? null : creditLines, + text, + formatType, + null), resolved.titles(), resolved.artists(), 0L, track); } - private static HttpURLConnection openApi(String url) throws IOException { - HttpURLConnection conn = - (HttpURLConnection) new java.net.URL(url).openConnection(); - conn.setRequestMethod("GET"); - conn.setRequestProperty("User-Agent", USER_AGENT); - conn.setRequestProperty("Accept", "application/json"); - conn.setRequestProperty("Accept-Encoding", "gzip"); - conn.setConnectTimeout(5000); - conn.setReadTimeout(8000); - return conn; - } - - @Nullable - private static Resolved resolveFromCreditsFm(String title, String artist) { - HttpURLConnection connection = null; - try { - String url = "https://api.credits.fm/v1/resolve/track"; - LyricsRequests.throttle(lastRequestTime, REQUEST_THROTTLE_MS); - connection = (HttpURLConnection) new java.net.URL(url).openConnection(); - connection.setRequestMethod("POST"); - connection.setRequestProperty("Content-Type", "application/json"); - connection.setRequestProperty("Accept", "application/json"); - connection.setRequestProperty("Accept-Encoding", "identity"); - connection.setRequestProperty("User-Agent", USER_AGENT); - connection.setConnectTimeout(5000); - connection.setReadTimeout(15000); - connection.setDoOutput(true); - - String body = new JSONObject() - .put("name", title) - .put("artist", artist) - .toString(); - byte[] bodyBytes = body.getBytes(StandardCharsets.UTF_8); - connection.setFixedLengthStreamingMode(bodyBytes.length); - try (OutputStream output = connection.getOutputStream()) { - output.write(bodyBytes); - } - - final int code = connection.getResponseCode(); - if (code == 200) { - String responseBody = LyricsRequests.parseGzipString(connection); - if (responseBody.isEmpty()) { - return null; - } - JSONObject response = new JSONObject(responseBody); - String isrc = LyricsRequests.optString(response, "isrc"); - if (isrc == null || isrc.isEmpty()) { - return null; - } - List titles = new ArrayList<>(2); - String recordingTitle = - LyricsRequests.optString(response, "recording_title"); - if (recordingTitle != null) { - titles.add(recordingTitle); - } - String songTitle = LyricsRequests.optString(response, "song_title"); - if (songTitle != null && !titles.contains(songTitle)) { - titles.add(songTitle); - } - return new Resolved(isrc, titles, - LyricsRequests.stringList(response, "artist_names")); - } - } catch (Exception e) { - Logger.printDebug(() -> "Could not read the ISRC", e); - } finally { - if (connection != null) connection.disconnect(); - } - return null; - } - - private static String generateUsername() { - final int len = 5 + RNG.nextInt(8); - StringBuilder sb = new StringBuilder(len); - for (int i = 0; i < len; i++) { - sb.append((char) ('a' + RNG.nextInt(26))); - } - return sb.toString(); + private record Resolved(String isrc, List titles, List artists) { } } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/LyricsProvider.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/LyricsProvider.java index 2856bc346f..bb577ffe5e 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/LyricsProvider.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/LyricsProvider.java @@ -25,6 +25,34 @@ public interface LyricsProvider { */ String name(); + /** + * Fetches lyrics. Always called off the main thread. + * + * @return result, or {@code null} if this provider has none for the track. + */ + @Nullable + FetchResult fetch(TrackInfo track) throws Exception; + + /** + * Returns scored candidate lyrics for the track, the best first. + * Scores use the shared match+sync scale from {@link LyricsRequests}. + */ + default List fetchCandidates(TrackInfo track) throws Exception { + FetchResult result = fetch(track); + if (result == null || result.lyrics() == null || result.lyrics() == Lyrics.NOT_FOUND) { + return Collections.emptyList(); + } + int score = result.matchScore(track) + LyricsRequests.syncRank(result.lyrics()); + return Collections.singletonList(new Lyrics.ScoredLyrics(score, result.lyrics())); + } + + /** + * Whether this provider supports multiple candidates per track. + */ + default boolean hasCandidates() { + return false; + } + record FetchResult(Lyrics lyrics, @Nullable String sourceTitle, @Nullable String sourceArtist, @@ -66,6 +94,34 @@ public static FetchResult of(@Nullable Lyrics lyrics, String title, String artis return new FetchResult(lyrics, title, artist, durationSec, queriedVariant, false); } + @Nullable + public static FetchResult searched(@Nullable Lyrics lyrics, + @Nullable List titles, + @Nullable List artists, + long durationSec, TrackInfo track) { + if (lyrics == null) { + return null; + } + if (titles == null || titles.isEmpty()) { + return blind(lyrics); + } + final List artistNames = + artists == null || artists.isEmpty() ? List.of("") : artists; + for (String title : titles) { + if (title == null || title.isEmpty()) { + continue; + } + for (String artist : artistNames) { + FetchResult result = new FetchResult(lyrics, title, + artist == null ? "" : artist, durationSec, null, false); + if (result.isHighMatch(track)) { + return result; + } + } + } + return blind(lyrics); + } + public int matchScore(TrackInfo track) { if (sourceTitle != null && !sourceTitle.isEmpty()) { LyricsRequests.MatchVerdict v = LyricsRequests.evaluate(sourceTitle, sourceArtist, @@ -104,61 +160,5 @@ public boolean isBlind() { && queriedVariant == null && !videoIdKeyed; } - - @Nullable - public static FetchResult searched(@Nullable Lyrics lyrics, - @Nullable List titles, - @Nullable List artists, - long durationSec, TrackInfo track) { - if (lyrics == null) { - return null; - } - if (titles == null || titles.isEmpty()) { - return blind(lyrics); - } - List artistNames = - artists == null || artists.isEmpty() ? List.of("") : artists; - for (String title : titles) { - if (title == null || title.isEmpty()) { - continue; - } - for (String artist : artistNames) { - FetchResult result = new FetchResult(lyrics, title, - artist == null ? "" : artist, durationSec, null, false); - if (result.isHighMatch(track)) { - return result; - } - } - } - return blind(lyrics); - } - } - - /** - * Fetches lyrics. Always called off the main thread. - * - * @return result, or {@code null} if this provider has none for the track. - */ - @Nullable - FetchResult fetch(TrackInfo track) throws Exception; - - /** - * Returns scored candidate lyrics for the track, the best first. - * Scores use the shared match+sync scale from {@link LyricsRequests}. - */ - default List fetchCandidates(TrackInfo track) throws Exception { - FetchResult result = fetch(track); - if (result == null || result.lyrics() == null || result.lyrics() == Lyrics.NOT_FOUND) { - return Collections.emptyList(); - } - int score = result.matchScore(track) + LyricsRequests.syncRank(result.lyrics()); - return Collections.singletonList(new Lyrics.ScoredLyrics(score, result.lyrics())); - } - - /** - * Whether this provider supports multiple candidates per track. - */ - default boolean hasCandidates() { - return false; } } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/LyricsRequests.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/LyricsRequests.java index 7555a7c2c8..5d46597b19 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/LyricsRequests.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/LyricsRequests.java @@ -48,42 +48,43 @@ */ public final class LyricsRequests { + public static final String SIMPLIFIED_CHINESE = "zh-Hans"; + /** + * For the endpoints that turn away an unrecognised client. Providers that answer only + * to a mobile agent keep one of their own. + */ + public static final String CHROME_DESKTOP_USER_AGENT = + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " + + "(KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36"; + /** Open to anyone, in need of no token. */ + public static final String ITUNES_SEARCH_URL = "https://itunes.apple.com/search"; + public static final int SOFT_MIN = 2; + public static final int VIDEO_ID_TRUST = 10; + public static final int NEUTRAL = 6; static final int MAX_CANDIDATES = 5; private static final int CONNECT_TIMEOUT_MILLISECONDS = 5 * 1000; private static final int READ_TIMEOUT_MILLISECONDS = 5 * 1000; - private static volatile boolean customMatchMode; + private static volatile String decorationRegexSource; + private static volatile java.util.regex.Pattern decorationPattern; private LyricsRequests() { } - public static void setCustomMatchMode(boolean on) { - customMatchMode = on; - } - public static boolean isCustomMatchMode() { return customMatchMode; } + public static void setCustomMatchMode(boolean on) { + customMatchMode = on; + } + static String userAgent() { return "Morphe/" + Utils.getAppVersionName() + " (" + Utils.getPatchesReleaseVersion() + ")" + " https://github.com/MorpheApp/morphe-patches"; } - public static final String SIMPLIFIED_CHINESE = "zh-Hans"; - - /** - * For the endpoints that turn away an unrecognised client. Providers that answer only - * to a mobile agent keep one of their own. - */ - public static final String CHROME_DESKTOP_USER_AGENT = - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " - + "(KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36"; - - /** Open to anyone, in need of no token. */ - public static final String ITUNES_SEARCH_URL = "https://itunes.apple.com/search"; - public static String deviceLanguage() { return Locale.getDefault().getLanguage(); } @@ -119,23 +120,23 @@ public static String primarySubtag(@Nullable String language) { @NonNull public static String resolvedScript(@Nullable String language) { - String declared = declaredScript(language); + final String declared = declaredScript(language); if (!declared.isEmpty()) { return declared; } if (!"zh".equals(primarySubtag(language))) { return ""; } - String region = chineseScriptOfRegion(regionSubtag(language)); + final String region = chineseScriptOfRegion(regionSubtag(language)); if (!region.isEmpty()) { return region; } - Locale device = Locale.getDefault(); - String script = device.getScript(); + final Locale device = Locale.getDefault(); + final String script = device.getScript(); if (script != null && !script.isEmpty()) { return script; } - String country = chineseScriptOfRegion(device.getCountry()); + final String country = chineseScriptOfRegion(device.getCountry()); // Chinese with no region settles is read as the mainland writes it. return country.isEmpty() ? "Hans" : country; } @@ -175,9 +176,9 @@ private static String regionSubtag(@Nullable String language) { return ""; } // The leading subtag is the language, so it is skipped. - String[] parts = language.split("[-_]"); + final String[] parts = language.split("[-_]"); for (int i = 1; i < parts.length; i++) { - String part = parts[i]; + final String part = parts[i]; if (part.length() == 2 && Character.isLetter(part.charAt(0)) && Character.isLetter(part.charAt(1))) { return part.toUpperCase(Locale.ROOT); @@ -367,7 +368,7 @@ static String optString(JSONObject object, String key) { if (object.isNull(key)) { return null; } - String value = object.optString(key, ""); + final String value = object.optString(key, ""); return value.trim().isEmpty() ? null : value; } @@ -375,9 +376,9 @@ static List stringList(JSONArray array) { if (array == null) { return List.of(); } - List out = new ArrayList<>(array.length()); + final List out = new ArrayList<>(array.length()); for (int i = 0; i < array.length(); i++) { - String value = array.optString(i, "").trim(); + final String value = array.optString(i, "").trim(); if (!value.isEmpty()) { out.add(value); } @@ -390,12 +391,12 @@ static List stringList(JSONObject object, String key) { } static String parseGzipString(HttpURLConnection connection) throws IOException { - InputStream raw = connection.getInputStream(); - InputStream stream = "gzip".equalsIgnoreCase(connection.getContentEncoding()) + final InputStream raw = connection.getInputStream(); + final InputStream stream = "gzip".equalsIgnoreCase(connection.getContentEncoding()) ? new GZIPInputStream(raw) : raw; try (BufferedReader reader = new BufferedReader( new InputStreamReader(stream, StandardCharsets.UTF_8))) { - StringBuilder sb = new StringBuilder(); + final StringBuilder sb = new StringBuilder(); String line; while ((line = reader.readLine()) != null) { sb.append(line).append('\n'); @@ -410,12 +411,12 @@ static JSONObject parseGzipJsonObject(HttpURLConnection connection) } static List parsePlainTextLines(String text) { - List lines = new ArrayList<>(); + final List lines = new ArrayList<>(); if (text == null || text.isEmpty()) { return lines; } for (String line : text.split("\\r?\\n")) { - String trimmed = line.trim(); + final String trimmed = line.trim(); if (!trimmed.isEmpty()) { lines.add(new LyricsLine(LyricsLine.NO_TIME, trimmed)); } @@ -439,6 +440,236 @@ static void throttle(AtomicLong lastRequestTime, long minIntervalMs) { lastRequestTime.set(System.currentTimeMillis()); } + public static PreparedTrack prepare(TrackInfo track) { + return new PreparedTrack(track); + } + + /** + * Normalizes a string for matching: NFKC, lowercase, canonical Chinese spelling, decoration strip, + * fullwidth punctuation fold and whitespace collapse. Semantic decorations + * (years, part numbers, live/acoustic/feat, …) are kept so that distinct + * versions do not collapse into a false EQUAL. + */ + public static String normalizeForMatch(@Nullable String raw) { + if (raw == null) { + return ""; + } + String s = raw.trim(); + if (s.isEmpty()) { + return ""; + } + s = java.text.Normalizer.normalize(s, java.text.Normalizer.Form.NFKC); + s = s.toLowerCase(Locale.ROOT); + // Providers spell metadata Traditional or Simplified, so both are compared. ICU does + // not map 妳 to 你, so that observed title variant is folded explicitly. + s = CharactersConverter.toSimplified(s).replace('妳', '你'); + s = stripDecorations(s); + s = foldPunctuation(s); + s = s.replaceAll("\\s+", " ").trim(); + return s; + } + + private static String stripDecorations(String s) { + String regex = LyricsFilterDefaults.LYRICS_CUSTOM_REGEX.get(); + if (regex.trim().isEmpty()) { + return s; + } + java.util.regex.Pattern pattern = decorationPattern; + if (pattern == null || !regex.equals(decorationRegexSource)) { + try { + pattern = java.util.regex.Pattern.compile(regex); + } catch (Exception ex) { + Logger.printDebug(() -> "Invalid lyrics decoration regex", ex); + return s; + } + decorationPattern = pattern; + decorationRegexSource = regex; + } + return pattern.matcher(s).replaceAll(""); + } + + private static String foldPunctuation(String s) { + final StringBuilder sb = new StringBuilder(s.length()); + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (c >= 0xFF01 && c <= 0xFF5E) { + c = (char) (c - 0xFEE0); + } + if (c == '·' || c == '•' || c == '・') { + continue; + } + sb.append(c); + } + return sb.toString(); + } + + public static boolean containsEither(String a, String b) { + if (a.isEmpty() || b.isEmpty()) { + return false; + } + if (a.contains(b) || b.contains(a)) { + final String shorter = a.length() <= b.length() ? a : b; + final String longer = a.length() <= b.length() ? b : a; + if (shorter.length() < 4 && !isCjk(shorter.charAt(0))) { + return longer.length() <= shorter.length() + 6; + } + if (shorter.length() < 2 && isCjk(shorter.charAt(0))) { + return longer.length() <= shorter.length() + 4; + } + return true; + } + return false; + } + + private static boolean splitArtistHits(String candidateArtist, String queryArtist) { + final String[] parts = candidateArtist.split("\\s*(?:和|&|feat\\.?|ft\\.?|,|/|×)\\s*"); + for (String part : parts) { + final String p = part.trim(); + if (p.isEmpty()) { + continue; + } + if (p.equals(queryArtist)) { + return true; + } + if (p.length() >= 2 || queryArtist.length() < 4) { + if (containsEither(p, queryArtist)) { + return true; + } + } + } + return false; + } + + public static boolean isCjk(char c) { + return Character.UnicodeScript.of(c) == Character.UnicodeScript.HAN; + } + + static double jaccard(String a, String b) { + final java.util.Set ta = tokens(a); + final java.util.Set tb = tokens(b); + if (ta.isEmpty() || tb.isEmpty()) { + return 0; + } + int inter = 0; + for (String t : ta) { + if (tb.contains(t)) { + inter++; + } + } + final int union = ta.size() + tb.size() - inter; + return union == 0 ? 0 : (double) inter / union; + } + + private static java.util.Set tokens(String s) { + final java.util.Set out = new java.util.LinkedHashSet<>(); + final StringBuilder cur = new StringBuilder(); + for (int i = 0; i < s.length(); ) { + final int cp = s.codePointAt(i); + i += Character.charCount(cp); + if (Character.isLetterOrDigit(cp)) { + cur.appendCodePoint(cp); + } else { + //noinspection SizeReplaceableByIsEmpty + if (cur.length() > 0) { + out.add(cur.toString()); + cur.setLength(0); + } + } + } + //noinspection SizeReplaceableByIsEmpty + if (cur.length() > 0) { + out.add(cur.toString()); + } + return out; + } + + /** + * High-match gate for first display / fallback eligibility. + * Channel A: aggregate score ≥ 7; Channel B: exact title with no counter-evidence. + * Artist mismatch is a hard veto. + */ + public static boolean isHighMatch(MatchVerdict v) { + if (customMatchMode) { + return true; + } + if (v.title() == Evidence.NONE || v.title() == Evidence.MISSING) { + return false; + } + if (v.artist() == Evidence.MISMATCH) { + return false; + } + if (v.score() >= 7) { + return v.duration() != Evidence.MISMATCH || v.artist() == Evidence.MATCH; + } + return v.title() == Evidence.EQUAL + && v.duration() != Evidence.MISMATCH + && v.album() != Evidence.MISMATCH; + } + + public static MatchVerdict evaluate(String title, String artist, long durationSec, + TrackInfo track) { + return prepare(track).evaluate(title, artist, durationSec, null); + } + + public static int scoreTrackCandidate(String title, String artist, long durationSec, + TrackInfo track) { + return scoreTrackCandidate(title, artist, durationSec, null, track); + } + + public static int scoreTrackCandidate(String title, String artist, long durationSec, + @Nullable String album, TrackInfo track) { + // Evaluated before adapters swap the candidate metadata for the query metadata + // through FetchResult.of(lyrics, track), so a counter-matching title is still rejected. + MatchVerdict verdict = prepare(track).evaluate(title, artist, durationSec, album); + if (customMatchMode) { + return rankScore(verdict); + } + return isHighMatch(verdict) ? verdict.score() : -1; + } + + static int rankScore(MatchVerdict v) { + if (v.artist() == Evidence.MISMATCH) { + return 2; + } + return Math.max(v.score(), 5); + } + + public static int syncRank(Lyrics lyrics) { + for (LyricsLine line : lyrics.lines()) { + if (line.hasWordTimings()) { + return 2; + } + } + return lyrics.synced() ? 1 : 0; + } + + public static int scoreLyricsCandidate(String title, String artist, long durationSec, + Lyrics lyrics, TrackInfo track) { + int score = scoreTrackCandidate(title, artist, durationSec, track); + return score < 0 ? score : score + syncRank(lyrics); + } + + /** + * Queue composite: match score dominates; sync rank breaks ties; + * a query-variant penalty keeps original metadata ahead of derived variants. + */ + public static int composite(int matchScore, Lyrics lyrics, int variantPenalty) { + return matchScore * 10 + syncRank(lyrics) - variantPenalty; + } + + public static int scoreSingleResult(Lyrics lyrics) { + return 6 + syncRank(lyrics); + } + + public enum Evidence { + EQUAL, + MATCH, + PARTIAL, + MISSING, + MISMATCH, + NONE + } + public static final class MusicBrainzClient { private static final String BASE_URL = "https://musicbrainz.org/ws/2/"; @@ -564,29 +795,14 @@ private static String phrase(String field, String query) { } } - public static final int SOFT_MIN = 2; - - public static final int VIDEO_ID_TRUST = 10; - - public static final int NEUTRAL = 6; - - public enum Evidence { - EQUAL, - MATCH, - PARTIAL, - MISSING, - MISMATCH, - NONE - } - /** * Result of evaluating a candidate's metadata against a prepared track. * - * @param score 0–11 ranking score (title 0–5 + artist 0–3 + duration 0–2 + album 0–1) - * @param title title evidence - * @param artist artist evidence + * @param score 0–11 ranking score (title 0–5 + artist 0–3 + duration 0–2 + album 0–1) + * @param title title evidence + * @param artist artist evidence * @param duration duration evidence (MISSING when either side unknown) - * @param album album evidence + * @param album album evidence */ public record MatchVerdict(int score, Evidence title, Evidence artist, Evidence duration, Evidence album) { @@ -614,11 +830,11 @@ public static final class PreparedTrack { public MatchVerdict evaluate(@Nullable String title, @Nullable String artist, long durationSec, @Nullable String album) { - String t = normalizeForMatch(title); - String a = normalizeForMatch(artist); - String al = normalizeForMatch(album); + final String t = normalizeForMatch(title); + final String a = normalizeForMatch(artist); + final String al = normalizeForMatch(album); - Evidence titleEv; + final Evidence titleEv; int titleScore; if (!titleValid) { titleEv = Evidence.MISSING; @@ -646,7 +862,7 @@ public MatchVerdict evaluate(@Nullable String title, @Nullable String artist, } } - Evidence artistEv; + final Evidence artistEv; int artistScore; if (a.isEmpty()) { artistEv = Evidence.MISSING; @@ -668,7 +884,7 @@ public MatchVerdict evaluate(@Nullable String title, @Nullable String artist, artistScore = 0; } - Evidence durationEv; + final Evidence durationEv; int durationScore; if (durationSec <= 0 || this.durationSec <= 0) { durationEv = Evidence.MISSING; @@ -690,7 +906,7 @@ public MatchVerdict evaluate(@Nullable String title, @Nullable String artist, } } - Evidence albumEv; + final Evidence albumEv; int albumScore; if (al.isEmpty() || this.album.isEmpty()) { albumEv = Evidence.MISSING; @@ -707,228 +923,4 @@ public MatchVerdict evaluate(@Nullable String title, @Nullable String artist, titleEv, artistEv, durationEv, albumEv); } } - - public static PreparedTrack prepare(TrackInfo track) { - return new PreparedTrack(track); - } - - /** - * Normalizes a string for matching: NFKC, lowercase, canonical Chinese spelling, decoration strip, - * fullwidth punctuation fold and whitespace collapse. Semantic decorations - * (years, part numbers, live/acoustic/feat, …) are kept so that distinct - * versions do not collapse into a false EQUAL. - */ - public static String normalizeForMatch(@Nullable String raw) { - if (raw == null) { - return ""; - } - String s = raw.trim(); - if (s.isEmpty()) { - return ""; - } - s = java.text.Normalizer.normalize(s, java.text.Normalizer.Form.NFKC); - s = s.toLowerCase(Locale.ROOT); - // Providers spell metadata Traditional or Simplified, so both are compared. ICU does - // not map 妳 to 你, so that observed title variant is folded explicitly. - s = CharactersConverter.toSimplified(s).replace('妳', '你'); - s = stripDecorations(s); - s = foldPunctuation(s); - s = s.replaceAll("\\s+", " ").trim(); - return s; - } - - private static volatile String decorationRegexSource; - private static volatile java.util.regex.Pattern decorationPattern; - - private static String stripDecorations(String s) { - String regex = LyricsFilterDefaults.LYRICS_CUSTOM_REGEX.get(); - if (regex.trim().isEmpty()) { - return s; - } - java.util.regex.Pattern pattern = decorationPattern; - if (pattern == null || !regex.equals(decorationRegexSource)) { - try { - pattern = java.util.regex.Pattern.compile(regex); - } catch (Exception ex) { - Logger.printDebug(() -> "Invalid lyrics decoration regex", ex); - return s; - } - decorationPattern = pattern; - decorationRegexSource = regex; - } - return pattern.matcher(s).replaceAll(""); - } - - private static String foldPunctuation(String s) { - StringBuilder sb = new StringBuilder(s.length()); - for (int i = 0; i < s.length(); i++) { - char c = s.charAt(i); - if (c >= 0xFF01 && c <= 0xFF5E) { - c = (char) (c - 0xFEE0); - } - if (c == '·' || c == '•' || c == '・') { - continue; - } - sb.append(c); - } - return sb.toString(); - } - - public static boolean containsEither(String a, String b) { - if (a.isEmpty() || b.isEmpty()) { - return false; - } - if (a.contains(b) || b.contains(a)) { - String shorter = a.length() <= b.length() ? a : b; - String longer = a.length() <= b.length() ? b : a; - if (shorter.length() < 4 && !isCjk(shorter.charAt(0))) { - return longer.length() <= shorter.length() + 6; - } - if (shorter.length() < 2 && isCjk(shorter.charAt(0))) { - return longer.length() <= shorter.length() + 4; - } - return true; - } - return false; - } - - private static boolean splitArtistHits(String candidateArtist, String queryArtist) { - String[] parts = candidateArtist.split("\\s*(?:和|&|feat\\.?|ft\\.?|,|/|×)\\s*"); - for (String part : parts) { - String p = part.trim(); - if (p.isEmpty()) { - continue; - } - if (p.equals(queryArtist)) { - return true; - } - if (p.length() >= 2 || queryArtist.length() < 4) { - if (containsEither(p, queryArtist)) { - return true; - } - } - } - return false; - } - - public static boolean isCjk(char c) { - return Character.UnicodeScript.of(c) == Character.UnicodeScript.HAN; - } - - static double jaccard(String a, String b) { - java.util.Set ta = tokens(a); - java.util.Set tb = tokens(b); - if (ta.isEmpty() || tb.isEmpty()) { - return 0; - } - int inter = 0; - for (String t : ta) { - if (tb.contains(t)) { - inter++; - } - } - final int union = ta.size() + tb.size() - inter; - return union == 0 ? 0 : (double) inter / union; - } - - private static java.util.Set tokens(String s) { - java.util.Set out = new java.util.LinkedHashSet<>(); - StringBuilder cur = new StringBuilder(); - for (int i = 0; i < s.length(); ) { - final int cp = s.codePointAt(i); - i += Character.charCount(cp); - if (Character.isLetterOrDigit(cp)) { - cur.appendCodePoint(cp); - } else { - //noinspection SizeReplaceableByIsEmpty - if (cur.length() > 0) { - out.add(cur.toString()); - cur.setLength(0); - } - } - } - //noinspection SizeReplaceableByIsEmpty - if (cur.length() > 0) { - out.add(cur.toString()); - } - return out; - } - - /** - * High-match gate for first display / fallback eligibility. - * Channel A: aggregate score ≥ 7; Channel B: exact title with no counter-evidence. - * Artist mismatch is a hard veto. - */ - public static boolean isHighMatch(MatchVerdict v) { - if (customMatchMode) { - return true; - } - if (v.title() == Evidence.NONE || v.title() == Evidence.MISSING) { - return false; - } - if (v.artist() == Evidence.MISMATCH) { - return false; - } - if (v.score() >= 7) { - return v.duration() != Evidence.MISMATCH || v.artist() == Evidence.MATCH; - } - return v.title() == Evidence.EQUAL - && v.duration() != Evidence.MISMATCH - && v.album() != Evidence.MISMATCH; - } - - public static MatchVerdict evaluate(String title, String artist, long durationSec, - TrackInfo track) { - return prepare(track).evaluate(title, artist, durationSec, null); - } - - public static int scoreTrackCandidate(String title, String artist, long durationSec, - TrackInfo track) { - return scoreTrackCandidate(title, artist, durationSec, null, track); - } - - public static int scoreTrackCandidate(String title, String artist, long durationSec, - @Nullable String album, TrackInfo track) { - // Evaluated before adapters swap the candidate metadata for the query metadata - // through FetchResult.of(lyrics, track), so a counter-matching title is still rejected. - MatchVerdict verdict = prepare(track).evaluate(title, artist, durationSec, album); - if (customMatchMode) { - return rankScore(verdict); - } - return isHighMatch(verdict) ? verdict.score() : -1; - } - - static int rankScore(MatchVerdict v) { - if (v.artist() == Evidence.MISMATCH) { - return 2; - } - return Math.max(v.score(), 5); - } - - public static int syncRank(Lyrics lyrics) { - for (LyricsLine line : lyrics.lines()) { - if (line.hasWordTimings()) { - return 2; - } - } - return lyrics.synced() ? 1 : 0; - } - - public static int scoreLyricsCandidate(String title, String artist, long durationSec, - Lyrics lyrics, TrackInfo track) { - int score = scoreTrackCandidate(title, artist, durationSec, track); - return score < 0 ? score : score + syncRank(lyrics); - } - - /** - * Queue composite: match score dominates; sync rank breaks ties; - * a query-variant penalty keeps original metadata ahead of derived variants. - */ - public static int composite(int matchScore, Lyrics lyrics, int variantPenalty) { - return matchScore * 10 + syncRank(lyrics) - variantPenalty; - } - - public static int scoreSingleResult(Lyrics lyrics) { - return 6 + syncRank(lyrics); - } } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/MusixmatchProvider.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/MusixmatchProvider.java index 0dca06a862..966bf19977 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/MusixmatchProvider.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/MusixmatchProvider.java @@ -49,9 +49,179 @@ public final class MusixmatchProvider extends ScoredSongProvider { private static final AtomicLong lastRequestTime = new AtomicLong(0); private static final Object TOKEN_LOCK = new Object(); + private static final Pattern LRC_LINE_PATTERN = + Pattern.compile("\\[(\\d{2}):(\\d{2})\\.\\d{2}]\\s?(.*)"); private static String cachedToken = null; private static String rejectedUserToken = null; + @Nullable + private static String extractCopyright(JSONObject macro) { + JSONObject lyricsMsg = obj(obj(macro, "track.lyrics.get"), "message"); + JSONObject lyricsBody = obj(obj(lyricsMsg, "body"), "lyrics"); + if (lyricsBody != null) { + final String c = LyricsRequests.optString(lyricsBody, "lyrics_copyright"); + if (c != null && !c.trim().isEmpty()) { + return c.trim(); + } + } + JSONObject subMsg = obj(obj(macro, "track.subtitles.get"), "message"); + JSONObject subBody = obj(subMsg, "body"); + final JSONArray subList = subBody != null ? subBody.optJSONArray("subtitle_list") : null; + if (subList != null && subList.length() > 0) { + JSONObject subItem = subList.optJSONObject(0); + JSONObject subtitle = subItem != null ? subItem.optJSONObject("subtitle") : null; + if (subtitle != null) { + final String c = LyricsRequests.optString(subtitle, "lyrics_copyright"); + if (c != null && !c.trim().isEmpty()) { + return c.trim(); + } + } + } + JSONObject richMsg = obj(obj(macro, "track.richsync.get"), "message"); + JSONObject richBody = obj(obj(richMsg, "body"), "richsync"); + if (richBody != null) { + final String c = LyricsRequests.optString(richBody, "lyrics_copyright"); + if (c != null && !c.trim().isEmpty()) { + return c.trim(); + } + } + return null; + } + + @Nullable + private static String extractBacklinkUrl(JSONObject macro) { + JSONObject lyricsMsg = obj(obj(macro, "track.lyrics.get"), "message"); + JSONObject lyricsBody = obj(obj(lyricsMsg, "body"), "lyrics"); + if (lyricsBody != null) { + final String url = LyricsRequests.optString(lyricsBody, "backlink_url"); + if (url != null && !url.isEmpty()) { + return stripTrackingParams(url); + } + } + return null; + } + + @Nullable + private static Lyrics appendCopyright(@Nullable Lyrics lyrics, @Nullable String copyright) { + if (lyrics == null || copyright == null || copyright.isEmpty()) { + return lyrics; + } + final List songwriters = new ArrayList<>(); + if (lyrics.songwriters() != null) { + songwriters.addAll(lyrics.songwriters()); + } + songwriters.add(copyright); + return new Lyrics(lyrics.lines(), lyrics.providerName(), lyrics.synced(), null, + lyrics.translations(), null, songwriters, lyrics.rawFormat(), + lyrics.formatType(), lyrics.sourceUrl()); + } + + private static String stripTrackingParams(String url) { + final int queryStart = url.indexOf('?'); + if (queryStart < 0) return url; + final String base = url.substring(0, queryStart); + final String query = url.substring(queryStart + 1); + final StringBuilder filtered = new StringBuilder(); + for (String param : query.split("&")) { + if (!param.toLowerCase(Locale.ROOT).startsWith("utm")) { + //noinspection SizeReplaceableByIsEmpty + if (filtered.length() > 0) filtered.append('&'); + filtered.append(param); + } + } + //noinspection SizeReplaceableByIsEmpty + return filtered.length() > 0 ? base + "?" + filtered : base; + } + + private static String requestId() { + return UUID.randomUUID().toString().replace("-", ""); + } + + private static void rejectToken(String hint) { + synchronized (TOKEN_LOCK) { + if ("renew".equalsIgnoreCase(hint) + && cachedToken != null + && cachedToken.equals(Settings.MUSIXMATCH_TOKEN.get())) { + rejectedUserToken = cachedToken; + } + cachedToken = null; + } + } + + public static void invalidateToken() { + synchronized (TOKEN_LOCK) { + cachedToken = null; + rejectedUserToken = null; + } + } + + private static boolean isUsableToken(String token) { + if (token == null || token.isEmpty() || "null".equals(token)) { + return false; + } + for (int i = 0; i < token.length(); i++) { + if (token.charAt(i) != '0') { + return true; + } + } + return false; + } + + private static int headerStatus(JSONObject root) { + JSONObject header = obj(obj(root, "message"), "header"); + return header == null ? 200 : header.optInt("status_code", 200); + } + + @Nullable + private static String headerHint(JSONObject root) { + JSONObject header = obj(obj(root, "message"), "header"); + return header != null ? LyricsRequests.optString(header, "hint") : null; + } + + private static JSONObject obj(JSONObject o, String key) { + return o == null ? null : o.optJSONObject(key); + } + + public static boolean validateToken(String token) { + if (!isUsableToken(token)) return false; + try { + final String url = SEARCH_URL + + "?page_size=1&page=1&q_track=a&q_artist=a" + + "&usertoken=" + LyricsRequests.encode(token) + + "&format=json" + + "&app_id=" + APP_ID + + "&t=" + requestId(); + final HttpURLConnection connection = LyricsRequests.openConnection(url); + connection.setRequestProperty("User-Agent", USER_AGENT); + connection.setRequestProperty("Cookie", COOKIE); + final int httpCode = connection.getResponseCode(); + if (httpCode != 200) { + connection.disconnect(); + return false; + } + JSONObject root = Requester.parseJSONObject(connection); + final int status = headerStatus(root); + final String hint = headerHint(root); + connection.disconnect(); + return status != 401 || !"renew".equalsIgnoreCase(hint); + } catch (Exception ex) { + Logger.printDebug(() -> "Could not validate Musixmatch token", ex); + return true; + } + } + + private static String joinWords(List words) { + final StringBuilder sb = new StringBuilder(); + for (int i = 0; i < words.size(); i++) { + final Word w = words.get(i); + sb.append(w.text()); + if (w.endsWithSpace() && i < words.size() - 1) { + sb.append(' '); + } + } + return sb.toString(); + } + @Override public String name() { return "Musixmatch"; @@ -59,12 +229,12 @@ public String name() { @Override protected List fetchScoredSongs(TrackInfo track) throws Exception { - String token = ensureToken(); + final String token = ensureToken(); if (token == null) { return Collections.emptyList(); } - List trackObjs; + final List trackObjs; try { trackObjs = searchTracks(track, token); } catch (Exception ex) { @@ -75,12 +245,12 @@ protected List fetchScoredSongs(TrackInfo track) throws Exception { return Collections.emptyList(); } - List scored = new ArrayList<>(); + final List scored = new ArrayList<>(); ScoredSong firstAccepted = null; for (JSONObject trackObj : trackObjs) { final int trackId = trackObj.optInt("track_id", -1); if (trackId <= 0) continue; - Lyrics lyrics; + final Lyrics lyrics; try { lyrics = fetchLyricsByTrackId(trackId, token); } catch (Exception ex) { @@ -114,7 +284,7 @@ protected List fetchScoredSongs(TrackInfo track) throws Exception { @Nullable private String ensureToken() throws IOException, JSONException { synchronized (TOKEN_LOCK) { - String userToken = Settings.MUSIXMATCH_TOKEN.get(); + final String userToken = Settings.MUSIXMATCH_TOKEN.get(); if (!userToken.trim().isEmpty() && isUsableToken(userToken) && !userToken.equals(rejectedUserToken)) { cachedToken = userToken; @@ -125,12 +295,12 @@ private String ensureToken() throws IOException, JSONException { return cachedToken; } - String url = TOKEN_URL + final String url = TOKEN_URL + "?user_language=en" + "&app_id=" + APP_ID + "&t=" + requestId(); - HttpURLConnection connection = openApi(url); + final HttpURLConnection connection = openApi(url); try { final int httpCode = connection.getResponseCode(); if (httpCode != 200) { @@ -145,7 +315,7 @@ private String ensureToken() throws IOException, JSONException { JSONObject body = obj(obj(root, "message"), "body"); if (body != null) { - String t = LyricsRequests.optString(body, "user_token"); + final String t = LyricsRequests.optString(body, "user_token"); if (isUsableToken(t)) { cachedToken = t; } @@ -162,7 +332,7 @@ private List searchTracks(TrackInfo track, String token) final double durationSec = track.durationSeconds() > 0 ? (double) track.durationSeconds() : 0; - StringBuilder url = new StringBuilder(SEARCH_URL) + final StringBuilder url = new StringBuilder(SEARCH_URL) .append("?page_size=10&page=1&s_track_rating=desc") .append("&q_track=").append(LyricsRequests.encode(track.title())) .append("&q_artist=").append(LyricsRequests.encode(track.artist())) @@ -175,7 +345,7 @@ private List searchTracks(TrackInfo track, String token) } LyricsRequests.throttle(lastRequestTime, REQUEST_THROTTLE_MS); - HttpURLConnection connection = openApi(url.toString()); + final HttpURLConnection connection = openApi(url.toString()); try { final int httpCode = connection.getResponseCode(); if (httpCode != 200) { @@ -184,7 +354,7 @@ private List searchTracks(TrackInfo track, String token) JSONObject root = Requester.parseJSONObject(connection); final int status = headerStatus(root); - String hint = headerHint(root); + final String hint = headerHint(root); if (status == 401) { rejectToken(hint); return Collections.emptyList(); @@ -194,12 +364,12 @@ private List searchTracks(TrackInfo track, String token) } JSONObject body = obj(obj(root, "message"), "body"); - JSONArray trackList = body != null ? body.optJSONArray("track_list") : null; + final JSONArray trackList = body != null ? body.optJSONArray("track_list") : null; if (trackList == null || trackList.length() == 0) { return Collections.emptyList(); } - List result = new ArrayList<>(); + final List result = new ArrayList<>(); for (int i = 0; i < trackList.length(); i++) { JSONObject item = trackList.optJSONObject(i); JSONObject trackObj = item != null ? item.optJSONObject("track") : null; @@ -216,14 +386,11 @@ private List searchTracks(TrackInfo track, String token) } } - private static final Pattern LRC_LINE_PATTERN = - Pattern.compile("\\[(\\d{2}):(\\d{2})\\.\\d{2}]\\s?(.*)"); - @Nullable private Lyrics fetchLyricsByTrackId(int trackId, String token) throws IOException, JSONException { - String url = MACRO_URL + final String url = MACRO_URL + "?namespace=lyrics_richsynched" + "&optional_calls=track.richsync" + "&subtitle_format=lrc" @@ -235,7 +402,7 @@ private Lyrics fetchLyricsByTrackId(int trackId, String token) + "&t=" + requestId(); LyricsRequests.throttle(lastRequestTime, REQUEST_THROTTLE_MS); - HttpURLConnection connection = openApi(url); + final HttpURLConnection connection = openApi(url); try { final int httpCode = connection.getResponseCode(); if (httpCode != 200) { @@ -244,7 +411,7 @@ private Lyrics fetchLyricsByTrackId(int trackId, String token) JSONObject root = Requester.parseJSONObject(connection); final int status = headerStatus(root); - String hint = headerHint(root); + final String hint = headerHint(root); if (status == 401) { rejectToken(hint); return null; @@ -261,8 +428,8 @@ private Lyrics fetchLyricsByTrackId(int trackId, String token) return null; } - String copyright = extractCopyright(macroCalls); - String sourceUrl = extractBacklinkUrl(macroCalls); + final String copyright = extractCopyright(macroCalls); + final String sourceUrl = extractBacklinkUrl(macroCalls); JSONObject richMsg = obj(obj(macroCalls, "track.richsync.get"), "message"); JSONObject richHeader = obj(richMsg, "header"); @@ -270,7 +437,7 @@ private Lyrics fetchLyricsByTrackId(int trackId, String token) if (richHeader != null && richStatus == 200) { JSONObject richBody = obj(obj(richMsg, "body"), "richsync"); if (richBody != null) { - String richBodyStr = LyricsRequests.optString(richBody, "richsync_body"); + final String richBodyStr = LyricsRequests.optString(richBody, "richsync_body"); if (richBodyStr != null && !richBodyStr.isEmpty()) { return appendCopyright(parseRichsync(richBodyStr, sourceUrl), copyright); } @@ -282,14 +449,14 @@ private Lyrics fetchLyricsByTrackId(int trackId, String token) final int subStatus = subHeader != null ? subHeader.optInt("status_code", 0) : 0; if (subHeader != null && subStatus == 200) { JSONObject subBody = obj(subMsg, "body"); - JSONArray subList = subBody != null + final JSONArray subList = subBody != null ? subBody.optJSONArray("subtitle_list") : null; if (subList != null && subList.length() > 0) { JSONObject subItem = subList.optJSONObject(0); JSONObject subtitle = subItem != null ? subItem.optJSONObject("subtitle") : null; if (subtitle != null) { - String subtitleBody = LyricsRequests.optString(subtitle, "subtitle_body"); + final String subtitleBody = LyricsRequests.optString(subtitle, "subtitle_body"); if (subtitleBody != null && !subtitleBody.isEmpty()) { return appendCopyright(parseSubtitles(subtitleBody, sourceUrl), copyright); } @@ -303,7 +470,7 @@ private Lyrics fetchLyricsByTrackId(int trackId, String token) if (lyricsHeader != null && lyricsStatus == 200) { JSONObject lyricsBody = obj(obj(lyricsMsg, "body"), "lyrics"); if (lyricsBody != null) { - String lyricsText = LyricsRequests.optString(lyricsBody, "lyrics_body"); + final String lyricsText = LyricsRequests.optString(lyricsBody, "lyrics_body"); if (lyricsText != null && !lyricsText.isEmpty()) { return appendCopyright(parseLyrics(lyricsText, sourceUrl), copyright); } @@ -316,93 +483,14 @@ private Lyrics fetchLyricsByTrackId(int trackId, String token) } } - @Nullable - private static String extractCopyright(JSONObject macro) { - JSONObject lyricsMsg = obj(obj(macro, "track.lyrics.get"), "message"); - JSONObject lyricsBody = obj(obj(lyricsMsg, "body"), "lyrics"); - if (lyricsBody != null) { - String c = LyricsRequests.optString(lyricsBody, "lyrics_copyright"); - if (c != null && !c.trim().isEmpty()) { - return c.trim(); - } - } - JSONObject subMsg = obj(obj(macro, "track.subtitles.get"), "message"); - JSONObject subBody = obj(subMsg, "body"); - JSONArray subList = subBody != null ? subBody.optJSONArray("subtitle_list") : null; - if (subList != null && subList.length() > 0) { - JSONObject subItem = subList.optJSONObject(0); - JSONObject subtitle = subItem != null ? subItem.optJSONObject("subtitle") : null; - if (subtitle != null) { - String c = LyricsRequests.optString(subtitle, "lyrics_copyright"); - if (c != null && !c.trim().isEmpty()) { - return c.trim(); - } - } - } - JSONObject richMsg = obj(obj(macro, "track.richsync.get"), "message"); - JSONObject richBody = obj(obj(richMsg, "body"), "richsync"); - if (richBody != null) { - String c = LyricsRequests.optString(richBody, "lyrics_copyright"); - if (c != null && !c.trim().isEmpty()) { - return c.trim(); - } - } - return null; - } - - @Nullable - private static String extractBacklinkUrl(JSONObject macro) { - JSONObject lyricsMsg = obj(obj(macro, "track.lyrics.get"), "message"); - JSONObject lyricsBody = obj(obj(lyricsMsg, "body"), "lyrics"); - if (lyricsBody != null) { - String url = LyricsRequests.optString(lyricsBody, "backlink_url"); - if (url != null && !url.isEmpty()) { - return stripTrackingParams(url); - } - } - return null; - } - - @Nullable - private static Lyrics appendCopyright(@Nullable Lyrics lyrics, @Nullable String copyright) { - if (lyrics == null || copyright == null || copyright.isEmpty()) { - return lyrics; - } - List songwriters = new ArrayList<>(); - if (lyrics.songwriters() != null) { - songwriters.addAll(lyrics.songwriters()); - } - songwriters.add(copyright); - return new Lyrics(lyrics.lines(), lyrics.providerName(), lyrics.synced(), null, - lyrics.translations(), null, songwriters, lyrics.rawFormat(), - lyrics.formatType(), lyrics.sourceUrl()); - } - - private static String stripTrackingParams(String url) { - final int queryStart = url.indexOf('?'); - if (queryStart < 0) return url; - String base = url.substring(0, queryStart); - String query = url.substring(queryStart + 1); - StringBuilder filtered = new StringBuilder(); - for (String param : query.split("&")) { - if (!param.toLowerCase(Locale.ROOT).startsWith("utm")) { - //noinspection SizeReplaceableByIsEmpty - if (filtered.length() > 0) filtered.append('&'); - filtered.append(param); - } - } - //noinspection SizeReplaceableByIsEmpty - return filtered.length() > 0 ? base + "?" + filtered : base; - } - @Nullable private Lyrics parseRichsync(String body, @Nullable String sourceUrl) throws JSONException { - JSONArray lines = new JSONArray(body); + final JSONArray lines = new JSONArray(body); if (lines.length() == 0) { return null; } - List result = new ArrayList<>(); + final List result = new ArrayList<>(); for (int i = 0; i < lines.length(); i++) { JSONObject line = lines.optJSONObject(i); if (line == null) { @@ -412,10 +500,10 @@ private Lyrics parseRichsync(String body, @Nullable String sourceUrl) final double lineTe = line.optDouble("te", lineTs); final long lineStartMs = (long) (lineTs * 1000); final long lineEndMs = (long) (lineTe * 1000); - String x = LyricsRequests.optString(line, "x"); + final String x = LyricsRequests.optString(line, "x"); List words = null; - JSONArray lArr = line.optJSONArray("l"); + final JSONArray lArr = line.optJSONArray("l"); if (lArr != null && lArr.length() > 0) { words = new ArrayList<>(); int xCursor = 0; @@ -424,7 +512,7 @@ private Lyrics parseRichsync(String body, @Nullable String sourceUrl) if (w == null) { continue; } - String rawChunk = LyricsRequests.optString(w, "c"); + final String rawChunk = LyricsRequests.optString(w, "c"); if (rawChunk == null || rawChunk.isEmpty()) { continue; } @@ -494,7 +582,7 @@ private Lyrics parseRichsync(String body, @Nullable String sourceUrl) } } - String text; + final String text; if (words != null && !words.isEmpty()) { text = joinWords(words); } else { @@ -525,15 +613,15 @@ private Lyrics parseSubtitles(String body, @Nullable String sourceUrl) @Nullable private Lyrics parseSubtitlesLrc(String body, @Nullable String sourceUrl) { - List result = new ArrayList<>(); + final List result = new ArrayList<>(); for (String raw : body.split("\\n")) { - String line = raw.trim(); + final String line = raw.trim(); if (line.isEmpty()) continue; - Matcher m = LRC_LINE_PATTERN.matcher(line); + final Matcher m = LRC_LINE_PATTERN.matcher(line); if (!m.matches()) continue; final int min = Integer.parseInt(Objects.requireNonNull(m.group(1))); final int sec = Integer.parseInt(Objects.requireNonNull(m.group(2))); - String text = m.group(3); + final String text = m.group(3); if (text == null || text.isEmpty()) continue; final long startMs = min * 60_000L + sec * 1_000L; result.add(new LyricsLine(startMs, text)); @@ -545,11 +633,11 @@ private Lyrics parseSubtitlesLrc(String body, @Nullable String sourceUrl) { @Nullable private Lyrics parseSubtitlesJson(String body, @Nullable String sourceUrl) throws JSONException { - JSONArray lines = new JSONArray(body); + final JSONArray lines = new JSONArray(body); if (lines.length() == 0) { return null; } - List result = new ArrayList<>(); + final List result = new ArrayList<>(); for (int i = 0; i < lines.length(); i++) { JSONObject line = lines.optJSONObject(i); if (line == null) { @@ -573,7 +661,7 @@ private Lyrics parseSubtitlesJson(String body, @Nullable String sourceUrl) @Nullable private Lyrics parseLyrics(String body, @Nullable String sourceUrl) { - List result = LyricsRequests.parsePlainTextLines(body); + final List result = LyricsRequests.parsePlainTextLines(body); if (result.isEmpty()) { return null; } @@ -581,98 +669,9 @@ private Lyrics parseLyrics(String body, @Nullable String sourceUrl) { } private HttpURLConnection openApi(String url) throws IOException { - HttpURLConnection connection = LyricsRequests.openConnection(url); + final HttpURLConnection connection = LyricsRequests.openConnection(url); connection.setRequestProperty("User-Agent", USER_AGENT); connection.setRequestProperty("Cookie", COOKIE); return connection; } - - private static String requestId() { - return UUID.randomUUID().toString().replace("-", ""); - } - - private static void rejectToken(String hint) { - synchronized (TOKEN_LOCK) { - if ("renew".equalsIgnoreCase(hint) - && cachedToken != null - && cachedToken.equals(Settings.MUSIXMATCH_TOKEN.get())) { - rejectedUserToken = cachedToken; - } - cachedToken = null; - } - } - - public static void invalidateToken() { - synchronized (TOKEN_LOCK) { - cachedToken = null; - rejectedUserToken = null; - } - } - - private static boolean isUsableToken(String token) { - if (token == null || token.isEmpty() || "null".equals(token)) { - return false; - } - for (int i = 0; i < token.length(); i++) { - if (token.charAt(i) != '0') { - return true; - } - } - return false; - } - - private static int headerStatus(JSONObject root) { - JSONObject header = obj(obj(root, "message"), "header"); - return header == null ? 200 : header.optInt("status_code", 200); - } - - @Nullable - private static String headerHint(JSONObject root) { - JSONObject header = obj(obj(root, "message"), "header"); - return header != null ? LyricsRequests.optString(header, "hint") : null; - } - - private static JSONObject obj(JSONObject o, String key) { - return o == null ? null : o.optJSONObject(key); - } - - public static boolean validateToken(String token) { - if (!isUsableToken(token)) return false; - try { - String url = SEARCH_URL - + "?page_size=1&page=1&q_track=a&q_artist=a" - + "&usertoken=" + LyricsRequests.encode(token) - + "&format=json" - + "&app_id=" + APP_ID - + "&t=" + requestId(); - HttpURLConnection connection = LyricsRequests.openConnection(url); - connection.setRequestProperty("User-Agent", USER_AGENT); - connection.setRequestProperty("Cookie", COOKIE); - final int httpCode = connection.getResponseCode(); - if (httpCode != 200) { - connection.disconnect(); - return false; - } - JSONObject root = Requester.parseJSONObject(connection); - final int status = headerStatus(root); - String hint = headerHint(root); - connection.disconnect(); - return status != 401 || !"renew".equalsIgnoreCase(hint); - } catch (Exception ex) { - Logger.printDebug(() -> "Could not validate Musixmatch token", ex); - return true; - } - } - - private static String joinWords(List words) { - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < words.size(); i++) { - Word w = words.get(i); - sb.append(w.text()); - if (w.endsWithSpace() && i < words.size() - 1) { - sb.append(' '); - } - } - return sb.toString(); - } } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/NetEaseProvider.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/NetEaseProvider.java index ea19cacff1..77e667ce60 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/NetEaseProvider.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/NetEaseProvider.java @@ -68,114 +68,12 @@ public final class NetEaseProvider implements LyricsProvider { private static final String clientSign = generateClientSign(); private static final String osver = "Microsoft-Windows-10--build-" + (20000 + RANDOM.nextInt(10000)) + "-64bit"; private static final String mode = MOTHERBOARD_MODES[RANDOM.nextInt(MOTHERBOARD_MODES.length)]; - private static boolean initialized; private static final Map cookieJar = new HashMap<>(); - private static final Pattern YRC_LINE = Pattern.compile("^\\[(\\d+),(\\d+)](.*)"); private static final Pattern YRC_WORD = Pattern.compile("\\((\\d+),(\\d+),\\d+\\)([^()]*)"); private static final Pattern LRC_TIME = Pattern.compile("\\[(\\d+):(\\d{2})(?:[.:](\\d{1,3}))?]"); private static final Pattern RICH_JSON = Pattern.compile("^\\s*\\{\""); - - @Override - public String name() { - return "NetEase"; - } - - @Override - public boolean hasCandidates() { - return true; - } - - @Nullable - @Override - public FetchResult fetch(TrackInfo track) throws Exception { - String keyword = track.title() + " " + track.artist(); - JSONObject song = searchBest(keyword, track); - if (song == null || !song.has("id")) { - return null; - } - Lyrics lyrics = fetchFromSong(song); - if (lyrics == null) { - return null; - } - final long durationMs = song.optLong("duration", 0); - return FetchResult.of(lyrics, - song.optString("name", ""), - song.optString("artist", ""), - durationMs > 0 ? durationMs / 1000 : 0, - track); - } - - @Override - public List fetchCandidates(TrackInfo track) throws Exception { - String keyword = track.title() + " " + track.artist(); - List songs = searchAll(keyword, track); - - List scored = new ArrayList<>(); - for (JSONObject song : songs) { - if (scored.size() >= LyricsRequests.MAX_CANDIDATES) { - break; - } - if (song == null || !song.has("id")) { - continue; - } - try { - Lyrics lyrics = fetchFromSong(song); - if (lyrics != null) { - final long durationMs = song.optLong("duration", 0); - int score = LyricsRequests.scoreLyricsCandidate( - song.optString("name", ""), song.optString("artist", ""), - durationMs > 0 ? durationMs / 1000 : 0, lyrics, track); - scored.add(new Lyrics.ScoredLyrics(score, lyrics)); - } - } catch (Exception ex) { - Logger.printDebug(() -> "Could not fetch NetEase lyrics for a song", ex); - } - } - - return Lyrics.sortScoredByScore(scored); - } - - @Nullable - private Lyrics fetchFromSong(JSONObject song) throws Exception { - JSONObject root = eapiRequest("/eapi/song/lyric/v1", new JSONObject() - .put("id", song.getLong("id")) - .put("lv", "-1") - .put("tv", "-1") - .put("rv", "-1") - .put("yv", "-1")); - - String yrc = optLyric(root, "yrc"); - String lrc = optLyric(root, "lrc"); - String tlyric = optLyric(root, "tlyric"); - String romalrc = optLyric(root, "romalrc"); - - List lines = parseNeteaseOriginalLyrics(yrc, lrc); - if (lines.isEmpty()) { - return null; - } - - List creditLines = parseYrcCredits(yrc); - - List romaLines = romalrc.isEmpty() ? null : LRCParser.parseSynced(romalrc); - List romanization = LyricsMerge.mergeRomanization(lines, romaLines); - - // NetEase writes this translation itself, in the Chinese the mainland uses. - Map> translations = null; - if (LyricsRequests.isSimplifiedChinese()) { - List transLines = tlyric.isEmpty() ? null : LRCParser.parseSynced(tlyric); - List translation = LyricsMerge.mergeRomanization(lines, transLines); - translations = LyricsMerge.singleLanguageTranslations( - translation, LyricsRequests.SIMPLIFIED_CHINESE); - } - - String rawFormat = filterJsonLines(!yrc.isEmpty() ? yrc : lrc); - String formatType = !yrc.isEmpty() ? "yrc" : "lrc"; - long songId = song.getLong("id"); - String sourceUrl = "https://music.163.com/song?id=" + songId; - return new Lyrics(lines, name(), true, romanization, translations, null, - creditLines.isEmpty() ? null : creditLines, rawFormat, formatType, sourceUrl); - } + private static boolean initialized; /** YRC files carry the credits as JSON lines mixed in with the timed lines. */ private static List parseYrcCredits(String yrc) { @@ -778,8 +676,6 @@ private static long parseTimeMs(String minutes, String seconds, String fraction) return (min * 60 + sec) * 1000 + Long.parseLong(fractionText.substring(0, 3)); } - private record Item(long start, String text) {} - private static String randomChars(int length, String alphabet) { StringBuilder builder = new StringBuilder(length); for (int i = 0; i < length; i++) { @@ -802,4 +698,108 @@ private static String randomMac() { private static String generateClientSign() { return randomMac() + "@@@" + randomChars(8, "ABCDEFGHIJKLMNOPQRSTUVWXYZ") + "@@@@@@" + randomChars(64, "0123456789abcdef"); } + + @Override + public String name() { + return "NetEase"; + } + + @Override + public boolean hasCandidates() { + return true; + } + + @Nullable + @Override + public FetchResult fetch(TrackInfo track) throws Exception { + String keyword = track.title() + " " + track.artist(); + JSONObject song = searchBest(keyword, track); + if (song == null || !song.has("id")) { + return null; + } + Lyrics lyrics = fetchFromSong(song); + if (lyrics == null) { + return null; + } + final long durationMs = song.optLong("duration", 0); + return FetchResult.of(lyrics, + song.optString("name", ""), + song.optString("artist", ""), + durationMs > 0 ? durationMs / 1000 : 0, + track); + } + + @Override + public List fetchCandidates(TrackInfo track) throws Exception { + String keyword = track.title() + " " + track.artist(); + List songs = searchAll(keyword, track); + + List scored = new ArrayList<>(); + for (JSONObject song : songs) { + if (scored.size() >= LyricsRequests.MAX_CANDIDATES) { + break; + } + if (song == null || !song.has("id")) { + continue; + } + try { + Lyrics lyrics = fetchFromSong(song); + if (lyrics != null) { + final long durationMs = song.optLong("duration", 0); + int score = LyricsRequests.scoreLyricsCandidate( + song.optString("name", ""), song.optString("artist", ""), + durationMs > 0 ? durationMs / 1000 : 0, lyrics, track); + scored.add(new Lyrics.ScoredLyrics(score, lyrics)); + } + } catch (Exception ex) { + Logger.printDebug(() -> "Could not fetch NetEase lyrics for a song", ex); + } + } + + return Lyrics.sortScoredByScore(scored); + } + + @Nullable + private Lyrics fetchFromSong(JSONObject song) throws Exception { + JSONObject root = eapiRequest("/eapi/song/lyric/v1", new JSONObject() + .put("id", song.getLong("id")) + .put("lv", "-1") + .put("tv", "-1") + .put("rv", "-1") + .put("yv", "-1")); + + String yrc = optLyric(root, "yrc"); + String lrc = optLyric(root, "lrc"); + String tlyric = optLyric(root, "tlyric"); + String romalrc = optLyric(root, "romalrc"); + + List lines = parseNeteaseOriginalLyrics(yrc, lrc); + if (lines.isEmpty()) { + return null; + } + + List creditLines = parseYrcCredits(yrc); + + List romaLines = romalrc.isEmpty() ? null : LRCParser.parseSynced(romalrc); + List romanization = LyricsMerge.mergeRomanization(lines, romaLines); + + // NetEase writes this translation itself, in the Chinese the mainland uses. + Map> translations = null; + if (LyricsRequests.isSimplifiedChinese()) { + List transLines = tlyric.isEmpty() ? null : LRCParser.parseSynced(tlyric); + List translation = LyricsMerge.mergeRomanization(lines, transLines); + translations = LyricsMerge.singleLanguageTranslations( + translation, LyricsRequests.SIMPLIFIED_CHINESE); + } + + String rawFormat = filterJsonLines(!yrc.isEmpty() ? yrc : lrc); + String formatType = !yrc.isEmpty() ? "yrc" : "lrc"; + long songId = song.getLong("id"); + String sourceUrl = "https://music.163.com/song?id=" + songId; + return new Lyrics(lines, name(), true, romanization, translations, null, + creditLines.isEmpty() ? null : creditLines, rawFormat, formatType, sourceUrl); + } + + private record Item(long start, String text) { + } } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/PetitLyricsProvider.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/PetitLyricsProvider.java index 78dae9a17e..ea551e5100 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/PetitLyricsProvider.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/PetitLyricsProvider.java @@ -55,250 +55,11 @@ public final class PetitLyricsProvider implements LyricsProvider { private static final int LSY_MAX_LINES = 10_000; private static final int LSY_WRAP_CS = 65_536; - @Override - public String name() { - return "PetitLyrics"; - } - - @Nullable - @Override - public FetchResult fetch(TrackInfo track) throws Exception { - Stash stash = new Stash(); - - collectTier(track, TIER_WORD_SYNC, stash); - Lyrics result = tryWordSync(stash); - if (result != null) { - return FetchResult.of(result, stash.sourceTitle, stash.sourceArtist, stash.sourceDurationSec, track); - } - - collectTier(track, TIER_LINE_SYNC, stash); - result = tryWordSync(stash); - if (result != null) { - return FetchResult.of(result, stash.sourceTitle, stash.sourceArtist, stash.sourceDurationSec, track); - } - result = tryLineSync(track, stash); - if (result != null) { - return FetchResult.of(result, stash.sourceTitle, stash.sourceArtist, stash.sourceDurationSec, track); - } - - if (stash.plainText == null && !stash.plainRequested) { - collectTier(track, TIER_UNSYNCED, stash); - } - if (stash.plainText == null && stash.zipText != null) { - stash.plainText = stash.zipText; - if (stash.sourceLyricsId == null) { - stash.sourceLyricsId = stash.zipLyricsId; - } - } - result = tryText(stash); - if (result != null) { - return FetchResult.of(result, stash.sourceTitle, stash.sourceArtist, stash.sourceDurationSec, track); - } - return null; - } - - private void collectTier(TrackInfo track, int lyricsType, Stash stash) { - if (lyricsType == TIER_UNSYNCED) { - if (stash.plainRequested) { - return; - } - stash.plainRequested = true; - } - try { - Response response = request(track, lyricsType); - collect(response, track, stash); - } catch (Exception ex) { - Logger.printDebug( - () -> "PetitLyrics request failed for type " + lyricsType, ex); - } - } - - private void collect(@Nullable Response response, TrackInfo track, Stash stash) { - if (response == null || response.songs.isEmpty()) { - return; - } - List sorted = new ArrayList<>(response.songs); - sorted.sort((a, b) -> Integer.compare(scoreOf(b, track), scoreOf(a, track))); - List gated = new ArrayList<>(); - for (Song song : sorted) { - if (scoreOf(song, track) >= LyricsRequests.SOFT_MIN) { - gated.add(song); - } - } - if (gated.isEmpty() && !sorted.isEmpty()) { - gated.add(sorted.get(0)); - } - sorted = gated; - - if (stash.creditLines == null) { - for (Song song : sorted) { - List credits = creditLinesOf(song); - if (!credits.isEmpty()) { - stash.creditLines = credits; - if (stash.sourceLyricsId == null) { - stash.sourceLyricsId = song.lyricsId; - } - break; - } - } - } - - if (stash.lsyLyricsId != null && stash.zipText == null) { - Song idMatch = null; - for (Song song : sorted) { - if (song.lyricsData == null || song.lyricsData.isEmpty()) { - continue; - } - if (stash.lsyLyricsId.equals(song.lyricsId)) { - idMatch = song; - break; - } - } - if (idMatch == null && response.songs.size() == 1) { - idMatch = response.songs.get(0); - } - if (idMatch != null && idMatch.lyricsData != null - && !idMatch.lyricsData.isEmpty()) { - try { - byte[] payload = Base64.decode(idMatch.lyricsData, Base64.DEFAULT); - if (payload != null && payload.length > 0 - && classifyPayload(payload) == TIER_UNSYNCED) { - String text = toUtf8Raw(payload); - if (!text.trim().isEmpty()) { - stash.zipText = text; - stash.zipLyricsId = idMatch.lyricsId; - stash.sourceTitle = idMatch.title; - stash.sourceArtist = idMatch.artist; - stash.sourceDurationSec = idMatch.durationMs / 1000; - if (stash.sourceLyricsId == null) { - stash.sourceLyricsId = idMatch.lyricsId; - } - } - } - } catch (Exception ex) { - Logger.printDebug(() -> "PetitLyrics zip text decode failed", ex); - } - } - } - - for (Song song : sorted) { - if (song.lyricsData == null || song.lyricsData.isEmpty()) { - continue; - } - byte[] payload; - try { - payload = Base64.decode(song.lyricsData, Base64.DEFAULT); - } catch (Exception ex) { - Logger.printDebug(() -> "PetitLyrics base64 decode failed", ex); - continue; - } - if (payload == null || payload.length == 0) { - continue; - } - int tier = classifyPayload(payload); - switch (tier) { - case TIER_WORD_SYNC -> { - if (stash.wordLines == null) { - List lines = parseWsy(payload); - if (!lines.isEmpty()) { - stash.wordLines = lines; - stash.wsyRaw = toUtf8(payload); - stash.sourceLyricsId = song.lyricsId; - stash.sourceTitle = song.title; - stash.sourceArtist = song.artist; - stash.sourceDurationSec = song.durationMs / 1000; - } - } - } - case TIER_LINE_SYNC -> { - if (stash.lsyTimings == null) { - List timings = decodeLsyTimings(payload); - if (!timings.isEmpty()) { - stash.lsyTimings = timings; - stash.lsyLyricsId = song.lyricsId; - stash.sourceLyricsId = song.lyricsId; - stash.sourceTitle = song.title; - stash.sourceArtist = song.artist; - stash.sourceDurationSec = song.durationMs / 1000; - } - } - } - default -> { - if (stash.plainText == null) { - String text = toUtf8Raw(payload); - if (!text.trim().isEmpty()) { - stash.plainText = text; - stash.sourceLyricsId = song.lyricsId; - stash.sourceTitle = song.title; - stash.sourceArtist = song.artist; - stash.sourceDurationSec = song.durationMs / 1000; - } - } - } - } - } - } - private static int scoreOf(Song song, TrackInfo track) { return LyricsRequests.scoreTrackCandidate( song.title, song.artist, song.durationMs / 1000, track); } - @Nullable - private Lyrics tryWordSync(Stash stash) { - if (stash.wordLines == null || stash.wordLines.isEmpty()) { - return null; - } - String wsyRaw = stash.wsyRaw != null && !stash.wsyRaw.isEmpty() - ? stash.wsyRaw - : null; - return new Lyrics(stash.wordLines, name(), true, null, null, null, - stash.creditLines, - wsyRaw, wsyRaw != null ? "wsy" : null, sourceUrl(stash.sourceLyricsId)); - } - - @Nullable - private Lyrics tryLineSync(TrackInfo track, Stash stash) { - if (stash.lsyTimings == null || stash.lsyTimings.isEmpty()) { - return null; - } - if (stash.zipText == null) { - collectTier(track, TIER_UNSYNCED, stash); - if (stash.zipText == null) { - return null; - } - } - List lines = zipLineSync(stash.lsyTimings, stash.zipText); - if (lines.isEmpty()) { - return null; - } - String lrc = buildLrc(lines); - return new Lyrics(lines, name(), true, null, null, null, stash.creditLines, - lrc, "lrc", sourceUrl(stash.sourceLyricsId)); - } - - @Nullable - private Lyrics tryText(Stash stash) { - if (stash.plainText == null || stash.plainText.isEmpty()) { - return null; - } - String text = stash.plainText; - LRCParser.LrcParseResult lrc = LRCParser.parseSyncedWithCreditLines(text); - if (!lrc.lines.isEmpty()) { - List credits = mergeCredits(stash.creditLines, lrc.creditLines); - assert credits != null; - return new Lyrics(lrc.lines, name(), true, null, null, null, - credits.isEmpty() ? null : credits, - text, "lrc", sourceUrl(stash.sourceLyricsId)); - } - List plain = LRCParser.parsePlain(text); - if (plain.isEmpty()) { - return null; - } - return new Lyrics(plain, name(), false, null, null, null, stash.creditLines, - text, "txt", sourceUrl(stash.sourceLyricsId)); - } - @Nullable private static String sourceUrl(@Nullable String lyricsId) { if (lyricsId == null || lyricsId.trim().isEmpty()) { @@ -329,7 +90,7 @@ private static void addCredit(List credits, String label, @Nullable Stri @Nullable private static List mergeCredits(@Nullable List primary, - @Nullable List secondary) { + @Nullable List secondary) { if (primary == null || primary.isEmpty()) { return secondary == null || secondary.isEmpty() ? null : secondary; } @@ -347,40 +108,11 @@ private static List mergeCredits(@Nullable List primary, } @Nullable - private Response request(TrackInfo track, int lyricsType) throws Exception { - LyricsRequests.throttle(lastRequestTime, REQUEST_THROTTLE_MS); - - String form = "clientAppId=" + LyricsRequests.encode(CLIENT_APP_ID) + - "&terminalType=" + LyricsRequests.encode(TERMINAL_TYPE) + - "&lyricsType=" + lyricsType + - "&key_title=" + LyricsRequests.encode(track.title()) + - "&key_artist=" + LyricsRequests.encode(track.artist()) + - "&key_album=" + LyricsRequests.encode(track.album()); - - var connection = LyricsRequests.postForm(API_URL, form); - int httpCode = connection.getResponseCode(); - if (httpCode != Requester.HTTP_STATUS_CODE_SUCCESS) { - LyricsRequests.logFailure(name(), connection); - return null; - } - - String xml; - try { - xml = LyricsRequests.parseGzipString(connection); - } catch (Exception ex) { - Logger.printDebug(() -> "PetitLyrics response read failed", ex); - connection.disconnect(); - return null; - } - return parseResponse(xml); - } - - @Nullable - private static Response parseResponse(String xml) { - try { - XmlPullParser parser = XmlPullParserFactory.newInstance().newPullParser(); - parser.setInput(new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8)), - "UTF-8"); + private static Response parseResponse(String xml) { + try { + XmlPullParser parser = XmlPullParserFactory.newInstance().newPullParser(); + parser.setInput(new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8)), + "UTF-8"); Response response = new Response(); Song current = null; @@ -429,14 +161,16 @@ private static Response parseResponse(String xml) { } } case "lyricsData" -> current.lyricsData = value.trim(); - default -> { } + default -> { + } } } else if ("status".equals(tag)) { response.status = text.toString(); } field = null; } - default -> { } + default -> { + } } event = parser.next(); } @@ -529,18 +263,6 @@ private static boolean isValidUtf8(byte[] raw) { } } - private record RawToken(String text, long startMs, long endMs) {} - - private static class RawWsyLine { - final String text; - final List words; - - RawWsyLine(String text, List words) { - this.text = text; - this.words = words; - } - } - private static boolean isWhitespaceToken(@Nullable String s) { if (s == null || s.isEmpty()) return true; for (int i = 0; i < s.length(); i++) { @@ -657,7 +379,8 @@ private static List parseWsy(byte[] payload) { case "wordstring" -> wordText.append(parser.getText()); case "starttime" -> wordStart = parseLong(parser.getText()); case "endtime" -> wordEnd = parseLong(parser.getText()); - default -> { } + default -> { + } } } else if (inLine && "linestring".equals(tag)) { lineText.append(parser.getText()); @@ -733,7 +456,8 @@ private static List parseWsy(byte[] payload) { } tag = null; } - default -> { } + default -> { + } } event = parser.next(); } @@ -788,191 +512,465 @@ private static List parseWsy(byte[] payload) { } } } - - // A bracketed part at the start or the end of a line is a second vocal. - int pOpen = indexOfAny(text, "((["); - int pClose = lastIndexOfAny(text, "))]"); - if (pOpen > 0 && pClose > pOpen && pClose >= text.length() - 2) { - int splitWi = -1; - for (int wi = 0; wi < words.size(); wi++) { - if (hasOpen(words.get(wi).text())) { - splitWi = wi; - break; + + // A bracketed part at the start or the end of a line is a second vocal. + int pOpen = indexOfAny(text, "((["); + int pClose = lastIndexOfAny(text, "))]"); + if (pOpen > 0 && pClose > pOpen && pClose >= text.length() - 2) { + int splitWi = -1; + for (int wi = 0; wi < words.size(); wi++) { + if (hasOpen(words.get(wi).text())) { + splitWi = wi; + break; + } + } + if (splitWi > 0) { + List mainWords = new ArrayList<>(words.subList(0, splitWi)); + Word lastMain = mainWords.get(mainWords.size() - 1); + mainWords.set(mainWords.size() - 1, new Word( + lastMain.startMs(), lastMain.endMs(), lastMain.text(), lastMain.romaji(), false)); + List secWords = new ArrayList<>(words.subList(splitWi, words.size())); + String mainText = text.substring(0, pOpen).trim(); + String secText = text.substring(pOpen).trim(); + addSanitizedLine(lines, mainText, mainWords, false); + addSanitizedLine(lines, secText, secWords, true); + i++; + continue; + } + } else if (pOpen == 0 && 0 < pClose && pClose < text.length() - 2) { + int splitWi = -1; + for (int wi = 0; wi < words.size(); wi++) { + if (hasClose(words.get(wi).text())) { + splitWi = wi + 1; + break; + } + } + if (splitWi > 0 && splitWi < words.size()) { + List secWords = new ArrayList<>(words.subList(0, splitWi)); + Word lastSec = secWords.get(secWords.size() - 1); + secWords.set(secWords.size() - 1, new Word( + lastSec.startMs(), lastSec.endMs(), lastSec.text(), lastSec.romaji(), false)); + List mainWords = new ArrayList<>(words.subList(splitWi, words.size())); + String secText = text.substring(0, pClose + 1).trim(); + String mainText = text.substring(pClose + 1).trim(); + addSanitizedLine(lines, secText, secWords, true); + addSanitizedLine(lines, mainText, mainWords, false); + i++; + continue; + } + } + + boolean isBg = LRCParser.hasOuterParentheses(text); + addSanitizedLine(lines, text, words, isBg); + i++; + } + + lines.sort(Comparator.comparingLong(LyricsLine::startTimeMs)); + LRCParser.markEchoesAsBackground(lines); + return lines; + } catch (Exception ex) { + Logger.printDebug(() -> "PetitLyrics WSY parse failed", ex); + return List.of(); + } + } + + private static long parseLong(String value) { + try { + return Long.parseLong(value.trim()); + } catch (Exception ex) { + return 0; + } + } + + private static List decodeLsyTimings(byte[] raw) { + if (raw.length < LSY_MIN_HEADER_LEN) { + return List.of(); + } + ByteBuffer buf = ByteBuffer.wrap(raw).order(ByteOrder.LITTLE_ENDIAN); + int lineCount = buf.getInt(LSY_LINE_COUNT_OFF); + if (lineCount <= 0 || lineCount > LSY_MAX_LINES) { + return List.of(); + } + int end = LSY_PAYLOAD_START + 2 * lineCount; + if (raw.length < end) { + return List.of(); + } + int lineLength = buf.getShort(LSY_LINE_LENGTH_OFF) & 0xFFFF; + if (lineLength > 0) { + int want = LSY_PAYLOAD_START + 2 * lineCount + lineLength * lineCount; + if (raw.length != want) { + return List.of(); + } + } + boolean switchFlag = raw[LSY_KEY_SWITCH_FLAG_OFF] != 0; + int protectionId = buf.getShort(LSY_PROTECTION_ID_OFF) & 0xFFFF; + int key = deriveLsyKey(protectionId, switchFlag); + + List out = new ArrayList<>(lineCount); + int wraps = 0; + int prev = 0; + for (int i = 0; i < lineCount; i++) { + int cs = (buf.getShort(LSY_PAYLOAD_START + 2 * i) & 0xFFFF) ^ key; + if (i > 0 && cs < prev) { + wraps++; + } + prev = cs; + out.add(cs + (long) wraps * LSY_WRAP_CS); + } + return out; + } + + private static int deriveLsyKey(int protectionId, boolean switchFlag) { + if (!switchFlag) { + return protectionId; + } + int k = protectionId; + return (k & 0x0003) + | ((k & 0x000c) << 2) + | ((k & 0x0030) >> 2) + | ((k & 0x00c0) << 2) + | ((k & 0x0300) >> 2) + | ((k & 0x0c00) << 2) + | ((k & 0x3000) >> 2) + | (k & 0xc000); + } + + private static List zipLineSync(List timingsCs, String text) { + String normalized = text.replace("\r\n", "\n"); + String[] rows = normalized.split("\n", -1); + int count = rows.length; + if (count != timingsCs.size()) { + while (count > 0 && rows[count - 1].trim().isEmpty()) { + count--; + } + } + if (count != timingsCs.size()) { + final int finalCount = count; + Logger.printDebug(() -> "PetitLyrics line-sync mismatch: " + + timingsCs.size() + " timings vs " + finalCount + " lines"); + return List.of(); + } + List lines = new ArrayList<>(count); + for (int i = 0; i < count; i++) { + String row = rows[i].replace('\u2005', ' ').replace('\u3000', ' ').replace('\u00A0', ' ').trim(); + if (row.isEmpty()) { + continue; + } + long startMs = timingsCs.get(i) * 10; + long endMs = (i + 1 < count) ? timingsCs.get(i + 1) * 10 : startMs + 3000; + if (endMs <= startMs) { + endMs = startMs + 3000; + } + boolean isBg = LRCParser.hasOuterParentheses(row); + String lineText = row; + if (isBg && ((row.startsWith("(") && row.endsWith(")")) + || (row.startsWith("(") && row.endsWith(")")) + || (row.startsWith("[") && row.endsWith("]")))) { + lineText = row.substring(1, row.length() - 1).trim(); + } + lines.add(new LyricsLine(startMs, endMs, lineText, List.of(), null, false, isBg, null)); + } + LRCParser.markEchoesAsBackground(lines); + return lines; + } + + private static String buildLrc(List lines) { + StringBuilder sb = new StringBuilder(50 * lines.size()); + for (LyricsLine line : lines) { + sb.append(LRCParser.formatLine(line)) + .append('\n'); + } + return sb.toString(); + } + + private static String toUtf8(byte[] payload) { + try { + return new String(payload, StandardCharsets.UTF_8).trim(); + } catch (Exception ex) { + return ""; + } + } + + private static String toUtf8Raw(byte[] payload) { + try { + return new String(payload, StandardCharsets.UTF_8); + } catch (Exception ex) { + return ""; + } + } + + @Override + public String name() { + return "PetitLyrics"; + } + + @Nullable + @Override + public FetchResult fetch(TrackInfo track) throws Exception { + Stash stash = new Stash(); + + collectTier(track, TIER_WORD_SYNC, stash); + Lyrics result = tryWordSync(stash); + if (result != null) { + return FetchResult.of(result, stash.sourceTitle, stash.sourceArtist, stash.sourceDurationSec, track); + } + + collectTier(track, TIER_LINE_SYNC, stash); + result = tryWordSync(stash); + if (result != null) { + return FetchResult.of(result, stash.sourceTitle, stash.sourceArtist, stash.sourceDurationSec, track); + } + result = tryLineSync(track, stash); + if (result != null) { + return FetchResult.of(result, stash.sourceTitle, stash.sourceArtist, stash.sourceDurationSec, track); + } + + if (stash.plainText == null && !stash.plainRequested) { + collectTier(track, TIER_UNSYNCED, stash); + } + if (stash.plainText == null && stash.zipText != null) { + stash.plainText = stash.zipText; + if (stash.sourceLyricsId == null) { + stash.sourceLyricsId = stash.zipLyricsId; + } + } + result = tryText(stash); + if (result != null) { + return FetchResult.of(result, stash.sourceTitle, stash.sourceArtist, stash.sourceDurationSec, track); + } + return null; + } + + private void collectTier(TrackInfo track, int lyricsType, Stash stash) { + if (lyricsType == TIER_UNSYNCED) { + if (stash.plainRequested) { + return; + } + stash.plainRequested = true; + } + try { + Response response = request(track, lyricsType); + collect(response, track, stash); + } catch (Exception ex) { + Logger.printDebug( + () -> "PetitLyrics request failed for type " + lyricsType, ex); + } + } + + private void collect(@Nullable Response response, TrackInfo track, Stash stash) { + if (response == null || response.songs.isEmpty()) { + return; + } + List sorted = new ArrayList<>(response.songs); + sorted.sort((a, b) -> Integer.compare(scoreOf(b, track), scoreOf(a, track))); + List gated = new ArrayList<>(); + for (Song song : sorted) { + if (scoreOf(song, track) >= LyricsRequests.SOFT_MIN) { + gated.add(song); + } + } + if (gated.isEmpty() && !sorted.isEmpty()) { + gated.add(sorted.get(0)); + } + sorted = gated; + + if (stash.creditLines == null) { + for (Song song : sorted) { + List credits = creditLinesOf(song); + if (!credits.isEmpty()) { + stash.creditLines = credits; + if (stash.sourceLyricsId == null) { + stash.sourceLyricsId = song.lyricsId; + } + break; + } + } + } + + if (stash.lsyLyricsId != null && stash.zipText == null) { + Song idMatch = null; + for (Song song : sorted) { + if (song.lyricsData == null || song.lyricsData.isEmpty()) { + continue; + } + if (stash.lsyLyricsId.equals(song.lyricsId)) { + idMatch = song; + break; + } + } + if (idMatch == null && response.songs.size() == 1) { + idMatch = response.songs.get(0); + } + if (idMatch != null && idMatch.lyricsData != null + && !idMatch.lyricsData.isEmpty()) { + try { + byte[] payload = Base64.decode(idMatch.lyricsData, Base64.DEFAULT); + if (payload != null && payload.length > 0 + && classifyPayload(payload) == TIER_UNSYNCED) { + String text = toUtf8Raw(payload); + if (!text.trim().isEmpty()) { + stash.zipText = text; + stash.zipLyricsId = idMatch.lyricsId; + stash.sourceTitle = idMatch.title; + stash.sourceArtist = idMatch.artist; + stash.sourceDurationSec = idMatch.durationMs / 1000; + if (stash.sourceLyricsId == null) { + stash.sourceLyricsId = idMatch.lyricsId; + } + } + } + } catch (Exception ex) { + Logger.printDebug(() -> "PetitLyrics zip text decode failed", ex); + } + } + } + + for (Song song : sorted) { + if (song.lyricsData == null || song.lyricsData.isEmpty()) { + continue; + } + byte[] payload; + try { + payload = Base64.decode(song.lyricsData, Base64.DEFAULT); + } catch (Exception ex) { + Logger.printDebug(() -> "PetitLyrics base64 decode failed", ex); + continue; + } + if (payload == null || payload.length == 0) { + continue; + } + int tier = classifyPayload(payload); + switch (tier) { + case TIER_WORD_SYNC -> { + if (stash.wordLines == null) { + List lines = parseWsy(payload); + if (!lines.isEmpty()) { + stash.wordLines = lines; + stash.wsyRaw = toUtf8(payload); + stash.sourceLyricsId = song.lyricsId; + stash.sourceTitle = song.title; + stash.sourceArtist = song.artist; + stash.sourceDurationSec = song.durationMs / 1000; + } + } + } + case TIER_LINE_SYNC -> { + if (stash.lsyTimings == null) { + List timings = decodeLsyTimings(payload); + if (!timings.isEmpty()) { + stash.lsyTimings = timings; + stash.lsyLyricsId = song.lyricsId; + stash.sourceLyricsId = song.lyricsId; + stash.sourceTitle = song.title; + stash.sourceArtist = song.artist; + stash.sourceDurationSec = song.durationMs / 1000; } } - if (splitWi > 0) { - List mainWords = new ArrayList<>(words.subList(0, splitWi)); - Word lastMain = mainWords.get(mainWords.size() - 1); - mainWords.set(mainWords.size() - 1, new Word( - lastMain.startMs(), lastMain.endMs(), lastMain.text(), lastMain.romaji(), false)); - List secWords = new ArrayList<>(words.subList(splitWi, words.size())); - String mainText = text.substring(0, pOpen).trim(); - String secText = text.substring(pOpen).trim(); - addSanitizedLine(lines, mainText, mainWords, false); - addSanitizedLine(lines, secText, secWords, true); - i++; - continue; - } - } else if (pOpen == 0 && 0 < pClose && pClose < text.length() - 2) { - int splitWi = -1; - for (int wi = 0; wi < words.size(); wi++) { - if (hasClose(words.get(wi).text())) { - splitWi = wi + 1; - break; + } + default -> { + if (stash.plainText == null) { + String text = toUtf8Raw(payload); + if (!text.trim().isEmpty()) { + stash.plainText = text; + stash.sourceLyricsId = song.lyricsId; + stash.sourceTitle = song.title; + stash.sourceArtist = song.artist; + stash.sourceDurationSec = song.durationMs / 1000; } } - if (splitWi > 0 && splitWi < words.size()) { - List secWords = new ArrayList<>(words.subList(0, splitWi)); - Word lastSec = secWords.get(secWords.size() - 1); - secWords.set(secWords.size() - 1, new Word( - lastSec.startMs(), lastSec.endMs(), lastSec.text(), lastSec.romaji(), false)); - List mainWords = new ArrayList<>(words.subList(splitWi, words.size())); - String secText = text.substring(0, pClose + 1).trim(); - String mainText = text.substring(pClose + 1).trim(); - addSanitizedLine(lines, secText, secWords, true); - addSanitizedLine(lines, mainText, mainWords, false); - i++; - continue; - } } - - boolean isBg = LRCParser.hasOuterParentheses(text); - addSanitizedLine(lines, text, words, isBg); - i++; } - - lines.sort(Comparator.comparingLong(LyricsLine::startTimeMs)); - LRCParser.markEchoesAsBackground(lines); - return lines; - } catch (Exception ex) { - Logger.printDebug(() -> "PetitLyrics WSY parse failed", ex); - return List.of(); } } - private static long parseLong(String value) { - try { - return Long.parseLong(value.trim()); - } catch (Exception ex) { - return 0; + @Nullable + private Lyrics tryWordSync(Stash stash) { + if (stash.wordLines == null || stash.wordLines.isEmpty()) { + return null; } + String wsyRaw = stash.wsyRaw != null && !stash.wsyRaw.isEmpty() + ? stash.wsyRaw + : null; + return new Lyrics(stash.wordLines, name(), true, null, null, null, + stash.creditLines, + wsyRaw, wsyRaw != null ? "wsy" : null, sourceUrl(stash.sourceLyricsId)); } - private static List decodeLsyTimings(byte[] raw) { - if (raw.length < LSY_MIN_HEADER_LEN) { - return List.of(); - } - ByteBuffer buf = ByteBuffer.wrap(raw).order(ByteOrder.LITTLE_ENDIAN); - int lineCount = buf.getInt(LSY_LINE_COUNT_OFF); - if (lineCount <= 0 || lineCount > LSY_MAX_LINES) { - return List.of(); - } - int end = LSY_PAYLOAD_START + 2 * lineCount; - if (raw.length < end) { - return List.of(); - } - int lineLength = buf.getShort(LSY_LINE_LENGTH_OFF) & 0xFFFF; - if (lineLength > 0) { - int want = LSY_PAYLOAD_START + 2 * lineCount + lineLength * lineCount; - if (raw.length != want) { - return List.of(); - } + @Nullable + private Lyrics tryLineSync(TrackInfo track, Stash stash) { + if (stash.lsyTimings == null || stash.lsyTimings.isEmpty()) { + return null; } - boolean switchFlag = raw[LSY_KEY_SWITCH_FLAG_OFF] != 0; - int protectionId = buf.getShort(LSY_PROTECTION_ID_OFF) & 0xFFFF; - int key = deriveLsyKey(protectionId, switchFlag); - - List out = new ArrayList<>(lineCount); - int wraps = 0; - int prev = 0; - for (int i = 0; i < lineCount; i++) { - int cs = (buf.getShort(LSY_PAYLOAD_START + 2 * i) & 0xFFFF) ^ key; - if (i > 0 && cs < prev) { - wraps++; + if (stash.zipText == null) { + collectTier(track, TIER_UNSYNCED, stash); + if (stash.zipText == null) { + return null; } - prev = cs; - out.add((long) (cs + wraps * LSY_WRAP_CS)); } - return out; - } - - private static int deriveLsyKey(int protectionId, boolean switchFlag) { - if (!switchFlag) { - return protectionId; + List lines = zipLineSync(stash.lsyTimings, stash.zipText); + if (lines.isEmpty()) { + return null; } - int k = protectionId; - return (k & 0x0003) - | ((k & 0x000c) << 2) - | ((k & 0x0030) >> 2) - | ((k & 0x00c0) << 2) - | ((k & 0x0300) >> 2) - | ((k & 0x0c00) << 2) - | ((k & 0x3000) >> 2) - | (k & 0xc000); + String lrc = buildLrc(lines); + return new Lyrics(lines, name(), true, null, null, null, stash.creditLines, + lrc, "lrc", sourceUrl(stash.sourceLyricsId)); } - private static List zipLineSync(List timingsCs, String text) { - String normalized = text.replace("\r\n", "\n"); - String[] rows = normalized.split("\n", -1); - int count = rows.length; - if (count != timingsCs.size()) { - while (count > 0 && rows[count - 1].trim().isEmpty()) { - count--; - } + @Nullable + private Lyrics tryText(Stash stash) { + if (stash.plainText == null || stash.plainText.isEmpty()) { + return null; } - if (count != timingsCs.size()) { - final int finalCount = count; - Logger.printDebug(() -> "PetitLyrics line-sync mismatch: " - + timingsCs.size() + " timings vs " + finalCount + " lines"); - return List.of(); + String text = stash.plainText; + LRCParser.LrcParseResult lrc = LRCParser.parseSyncedWithCreditLines(text); + if (!lrc.lines.isEmpty()) { + List credits = mergeCredits(stash.creditLines, lrc.creditLines); + assert credits != null; + return new Lyrics(lrc.lines, name(), true, null, null, null, + credits.isEmpty() ? null : credits, + text, "lrc", sourceUrl(stash.sourceLyricsId)); } - List lines = new ArrayList<>(count); - for (int i = 0; i < count; i++) { - String row = rows[i].replace('\u2005', ' ').replace('\u3000', ' ').replace('\u00A0', ' ').trim(); - if (row.isEmpty()) { - continue; - } - long startMs = timingsCs.get(i) * 10; - long endMs = (i + 1 < count) ? timingsCs.get(i + 1) * 10 : startMs + 3000; - if (endMs <= startMs) { - endMs = startMs + 3000; - } - boolean isBg = LRCParser.hasOuterParentheses(row); - String lineText = row; - if (isBg && ((row.startsWith("(") && row.endsWith(")")) - || (row.startsWith("(") && row.endsWith(")")) - || (row.startsWith("[") && row.endsWith("]")))) { - lineText = row.substring(1, row.length() - 1).trim(); - } - lines.add(new LyricsLine(startMs, endMs, lineText, List.of(), null, false, isBg, null)); + List plain = LRCParser.parsePlain(text); + if (plain.isEmpty()) { + return null; } - LRCParser.markEchoesAsBackground(lines); - return lines; + return new Lyrics(plain, name(), false, null, null, null, stash.creditLines, + text, "txt", sourceUrl(stash.sourceLyricsId)); } - private static String buildLrc(List lines) { - StringBuilder sb = new StringBuilder(50 * lines.size()); - for (LyricsLine line : lines) { - sb.append(LRCParser.formatLine(line)) - .append('\n'); + @Nullable + private Response request(TrackInfo track, int lyricsType) throws Exception { + LyricsRequests.throttle(lastRequestTime, REQUEST_THROTTLE_MS); + + String form = "clientAppId=" + LyricsRequests.encode(CLIENT_APP_ID) + + "&terminalType=" + LyricsRequests.encode(TERMINAL_TYPE) + + "&lyricsType=" + lyricsType + + "&key_title=" + LyricsRequests.encode(track.title()) + + "&key_artist=" + LyricsRequests.encode(track.artist()) + + "&key_album=" + LyricsRequests.encode(track.album()); + + var connection = LyricsRequests.postForm(API_URL, form); + int httpCode = connection.getResponseCode(); + if (httpCode != Requester.HTTP_STATUS_CODE_SUCCESS) { + LyricsRequests.logFailure(name(), connection); + return null; } - return sb.toString(); - } - private static String toUtf8(byte[] payload) { + String xml; try { - return new String(payload, StandardCharsets.UTF_8).trim(); + xml = LyricsRequests.parseGzipString(connection); } catch (Exception ex) { - return ""; + Logger.printDebug(() -> "PetitLyrics response read failed", ex); + connection.disconnect(); + return null; } + return parseResponse(xml); } - private static String toUtf8Raw(byte[] payload) { - try { - return new String(payload, StandardCharsets.UTF_8); - } catch (Exception ex) { - return ""; - } + private record RawToken(String text, long startMs, long endMs) { + } + + private record RawWsyLine(String text, List words) { } private static final class Stash { @@ -1003,8 +1001,8 @@ private static final class Stash { } private static final class Response { - String status; final List songs = new ArrayList<>(); + String status; } private static final class Song { diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/QQProvider.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/QQProvider.java index a1aa4176e4..7291eeb218 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/QQProvider.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/QQProvider.java @@ -45,6 +45,12 @@ public final class QQProvider implements LyricsProvider { private static final String MUSICU_URL = "https://u.y.qq.com/cgi-bin/musicu.fcg"; private static final String QRC_KEY = "!@#)(*$%123ZXC!@!@#)(NHL"; + private static final Pattern QRC_XML = Pattern.compile( + ""); + private static final Pattern NUMERIC_ENTITY = Pattern.compile("&#(\\d+);"); + private static final Pattern QRC_LINE = Pattern.compile("^\\[(\\d+),(\\d+)](.*)"); + private static final Pattern QRC_WORD = Pattern.compile("\\((\\d+),(\\d+)\\)"); + private static final Pattern QRC_WHOLE_LINE_COMMENT = Pattern.compile("//"); private static JSONObject musicuComm() throws JSONException { JSONObject comm = new JSONObject(); @@ -58,129 +64,6 @@ private static JSONObject musicuComm() throws JSONException { return comm; } - private static final Pattern QRC_XML = Pattern.compile( - ""); - private static final Pattern NUMERIC_ENTITY = Pattern.compile("&#(\\d+);"); - private static final Pattern QRC_LINE = Pattern.compile("^\\[(\\d+),(\\d+)](.*)"); - private static final Pattern QRC_WORD = Pattern.compile("\\((\\d+),(\\d+)\\)"); - private static final Pattern QRC_WHOLE_LINE_COMMENT = Pattern.compile("//"); - - @Override - public String name() { - return "QQ"; - } - - @Override - public boolean hasCandidates() { - return true; - } - - @Nullable - @Override - public FetchResult fetch(TrackInfo track) throws Exception { - String keyword = track.title() + " " + track.artist(); - JSONObject song = searchBest(keyword, track); - if (song == null || !song.has("id")) { - return null; - } - Lyrics lyrics = fetchFromSong(song); - if (lyrics == null) { - return null; - } - return FetchResult.of(lyrics, - song.optString("title", ""), - singers(song), - song.optInt("interval", 0), - track); - } - - @Override - public List fetchCandidates(TrackInfo track) throws Exception { - String keyword = track.title() + " " + track.artist(); - List candidates = searchAll(keyword, track); - - List scored = new ArrayList<>(); - for (JSONObject song : candidates) { - if (scored.size() >= LyricsRequests.MAX_CANDIDATES) { - break; - } - if (song == null || !song.has("id")) { - continue; - } - try { - Lyrics lyrics = fetchFromSong(song); - if (lyrics != null) { - int score = LyricsRequests.scoreLyricsCandidate( - song.optString("title", ""), singers(song), - song.optInt("interval", 0), lyrics, track); - scored.add(new Lyrics.ScoredLyrics(score, lyrics)); - } - } catch (Exception ex) { - Logger.printDebug(() -> "Could not fetch QQ lyrics for a song", ex); - } - } - - return Lyrics.sortScoredByScore(scored); - } - - @Nullable - private Lyrics fetchFromSong(JSONObject song) throws Exception { - JSONObject data = fetchLyricData(song); - if (data == null) { - return null; - } - - final long songId = song.optLong("id"); - String sourceUrl = "https://y.qq.com/n/ryqq/songDetail/" + songId; - - String original = decodeQqLyricPayload(data.optString("lyric", "")); - Matcher qrcXmlMatcher = QRC_XML.matcher(original); - if (qrcXmlMatcher.find()) { - original = decodeXmlEntities(qrcXmlMatcher.group(1)); - } - - long[] offsetHolder = new long[1]; - List qrcMetadata = parseQrcMetadata(original, offsetHolder); - long qrcOffsetMs = offsetHolder[0]; - - List lines = parseQrcFormat(original); - if (lines.isEmpty()) { - lines = LRCParser.parseSynced(original); - } - if (lines.isEmpty()) { - return null; - } - - if (qrcOffsetMs != 0) { - lines = LyricsRequests.applyOffset(lines, qrcOffsetMs); - } - - if (lines.isEmpty()) { - return null; - } - - String romaPayload = decodeQqLyricPayload(data.optString("roma", "")); - List romaLines = romaPayload.isEmpty() ? null : parseQrcFormat(romaPayload); - List romanization = LyricsMerge.mergeRomanization(lines, romaLines); - - // QQ writes this translation itself, in the Chinese the mainland uses. - Map> translations = null; - if (LyricsRequests.isSimplifiedChinese()) { - String transPayload = decodeQqLyricPayload(data.optString("trans", "")); - List transLines = transPayload.isEmpty() ? null : parseQrcFormat(transPayload); - if (transLines == null || transLines.isEmpty()) { - transLines = LRCParser.parseSynced(transPayload); - } - transLines.removeIf(line -> "//".equals(line.text().trim())); - List translation = LyricsMerge.mergeRomanization(lines, transLines); - translations = LyricsMerge.singleLanguageTranslations( - translation, LyricsRequests.SIMPLIFIED_CHINESE); - } - - return new Lyrics(lines, name(), true, romanization, translations, null, - qrcMetadata.isEmpty() ? null : qrcMetadata, original, "qrc", sourceUrl); - } - @Nullable private static JSONObject searchBest(String keyword, TrackInfo track) throws IOException, JSONException { List songs = searchAll(keyword, track); @@ -512,4 +395,120 @@ private static List parseQrcFormat(String text) { lines.sort(Comparator.comparingLong(LyricsLine::startTimeMs)); return lines; } + + @Override + public String name() { + return "QQ"; + } + + @Override + public boolean hasCandidates() { + return true; + } + + @Nullable + @Override + public FetchResult fetch(TrackInfo track) throws Exception { + String keyword = track.title() + " " + track.artist(); + JSONObject song = searchBest(keyword, track); + if (song == null || !song.has("id")) { + return null; + } + Lyrics lyrics = fetchFromSong(song); + if (lyrics == null) { + return null; + } + return FetchResult.of(lyrics, + song.optString("title", ""), + singers(song), + song.optInt("interval", 0), + track); + } + + @Override + public List fetchCandidates(TrackInfo track) throws Exception { + String keyword = track.title() + " " + track.artist(); + List candidates = searchAll(keyword, track); + + List scored = new ArrayList<>(); + for (JSONObject song : candidates) { + if (scored.size() >= LyricsRequests.MAX_CANDIDATES) { + break; + } + if (song == null || !song.has("id")) { + continue; + } + try { + Lyrics lyrics = fetchFromSong(song); + if (lyrics != null) { + int score = LyricsRequests.scoreLyricsCandidate( + song.optString("title", ""), singers(song), + song.optInt("interval", 0), lyrics, track); + scored.add(new Lyrics.ScoredLyrics(score, lyrics)); + } + } catch (Exception ex) { + Logger.printDebug(() -> "Could not fetch QQ lyrics for a song", ex); + } + } + + return Lyrics.sortScoredByScore(scored); + } + + @Nullable + private Lyrics fetchFromSong(JSONObject song) throws Exception { + JSONObject data = fetchLyricData(song); + if (data == null) { + return null; + } + + final long songId = song.optLong("id"); + final String sourceUrl = "https://y.qq.com/n/ryqq/songDetail/" + songId; + + String original = decodeQqLyricPayload(data.optString("lyric", "")); + Matcher qrcXmlMatcher = QRC_XML.matcher(original); + if (qrcXmlMatcher.find()) { + original = decodeXmlEntities(qrcXmlMatcher.group(1)); + } + + long[] offsetHolder = new long[1]; + List qrcMetadata = parseQrcMetadata(original, offsetHolder); + long qrcOffsetMs = offsetHolder[0]; + + List lines = parseQrcFormat(original); + if (lines.isEmpty()) { + lines = LRCParser.parseSynced(original); + } + if (lines.isEmpty()) { + return null; + } + + if (qrcOffsetMs != 0) { + lines = LyricsRequests.applyOffset(lines, qrcOffsetMs); + } + + if (lines.isEmpty()) { + return null; + } + + String romaPayload = decodeQqLyricPayload(data.optString("roma", "")); + List romaLines = romaPayload.isEmpty() ? null : parseQrcFormat(romaPayload); + List romanization = LyricsMerge.mergeRomanization(lines, romaLines); + + // QQ writes this translation itself, in the Chinese the mainland uses. + Map> translations = null; + if (LyricsRequests.isSimplifiedChinese()) { + String transPayload = decodeQqLyricPayload(data.optString("trans", "")); + List transLines = transPayload.isEmpty() ? null : parseQrcFormat(transPayload); + if (transLines == null || transLines.isEmpty()) { + transLines = LRCParser.parseSynced(transPayload); + } + transLines.removeIf(line -> "//".equals(line.text().trim())); + List translation = LyricsMerge.mergeRomanization(lines, transLines); + translations = LyricsMerge.singleLanguageTranslations( + translation, LyricsRequests.SIMPLIFIED_CHINESE); + } + + return new Lyrics(lines, name(), true, romanization, translations, null, + qrcMetadata.isEmpty() ? null : qrcMetadata, original, "qrc", sourceUrl); + } } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/RMMProvider.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/RMMProvider.java index 7a56018133..0fe0d1431e 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/RMMProvider.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/RMMProvider.java @@ -43,80 +43,6 @@ private static String lrcFormat(List lines) { return sb.toString(); } - @Override - public String name() { - return "RMM"; - } - - @Override - public boolean hasCandidates() { - return true; - } - - @Nullable - @Override - public FetchResult fetch(TrackInfo track) throws Exception { - List candidates = searchItunesCandidates(track); - if (candidates.isEmpty()) { - return null; - } - JSONObject best = candidates.get(0); - final long trackId = best.optLong("trackId", 0); - if (trackId == 0) { - return null; - } - JSONObject root = fetchRmmJson(trackId); - if (root == null) { - return null; - } - Lyrics lyrics = buildLyrics(root, trackId); - if (lyrics == null) { - return null; - } - return FetchResult.of(lyrics, - best.optString("trackName", ""), - best.optString("artistName", ""), - best.optLong("trackTimeMillis", 0) / 1000, - track); - } - - @Override - public List fetchCandidates(TrackInfo track) throws Exception { - List candidates = searchItunesCandidates(track); - List scored = new ArrayList<>(); - for (JSONObject item : candidates) { - if (scored.size() >= LyricsRequests.MAX_CANDIDATES) { - break; - } - final long trackId = item.optLong("trackId", 0); - if (trackId == 0) { - continue; - } - try { - JSONObject root = fetchRmmJson(trackId); - if (root == null) { - continue; - } - Lyrics lyrics = buildLyrics(root, trackId); - if (lyrics == null) { - continue; - } - int score = LyricsRequests.scoreLyricsCandidate( - item.optString("trackName", ""), - item.optString("artistName", ""), - item.optLong("trackTimeMillis", 0) / 1000, - lyrics, track); - scored.add(new Lyrics.ScoredLyrics(score, lyrics)); - if (score >= HIGH_MATCH_SCORE) { - break; - } - } catch (Exception ex) { - Logger.printDebug(() -> "Could not fetch RMM Revival lyrics for a candidate", ex); - } - } - return Lyrics.sortScoredByScore(scored); - } - /** * Public iTunes search, scored with the shared match scale. Unlike the Apple catalog * search this needs no token, so the provider works for anyone who enables it. @@ -137,8 +63,8 @@ private static List searchItunesCandidates(TrackInfo track) { return new ArrayList<>(); } - String title = track.title().toLowerCase(Locale.ROOT).trim(); - String artist = track.artist().toLowerCase(Locale.ROOT).trim(); + final String title = track.title().toLowerCase(Locale.ROOT).trim(); + final String artist = track.artist().toLowerCase(Locale.ROOT).trim(); List scored = new ArrayList<>(); for (int i = 0; i < results.length(); i++) { @@ -185,9 +111,6 @@ private static List searchItunesCandidates(TrackInfo track) { } } - private record ScoredItem(JSONObject item, int score, boolean exact) { - } - @Nullable private static JSONObject fetchRmmJson(long trackId) { HttpURLConnection connection = null; @@ -210,13 +133,6 @@ private static JSONObject fetchRmmJson(long trackId) { } } - /** - * The community uploader, when the entry is a SpicyLyrics community sync rather than - * Apple lyrics relayed through the same API. - */ - private record Uploader(String username, @Nullable String url) { - } - /** SpicyLyrics hands the url over without a scheme, which nothing can open. */ @Nullable private static String usableUrl(@Nullable String url) { @@ -385,4 +301,88 @@ private static Lyrics buildLyrics(JSONObject root, long trackId) { return null; } + + @Override + public String name() { + return "RMM"; + } + + @Override + public boolean hasCandidates() { + return true; + } + + @Nullable + @Override + public FetchResult fetch(TrackInfo track) throws Exception { + List candidates = searchItunesCandidates(track); + if (candidates.isEmpty()) { + return null; + } + JSONObject best = candidates.get(0); + final long trackId = best.optLong("trackId", 0); + if (trackId == 0) { + return null; + } + JSONObject root = fetchRmmJson(trackId); + if (root == null) { + return null; + } + Lyrics lyrics = buildLyrics(root, trackId); + if (lyrics == null) { + return null; + } + return FetchResult.of(lyrics, + best.optString("trackName", ""), + best.optString("artistName", ""), + best.optLong("trackTimeMillis", 0) / 1000, + track); + } + + @Override + public List fetchCandidates(TrackInfo track) throws Exception { + List candidates = searchItunesCandidates(track); + List scored = new ArrayList<>(); + for (JSONObject item : candidates) { + if (scored.size() >= LyricsRequests.MAX_CANDIDATES) { + break; + } + final long trackId = item.optLong("trackId", 0); + if (trackId == 0) { + continue; + } + try { + JSONObject root = fetchRmmJson(trackId); + if (root == null) { + continue; + } + Lyrics lyrics = buildLyrics(root, trackId); + if (lyrics == null) { + continue; + } + int score = LyricsRequests.scoreLyricsCandidate( + item.optString("trackName", ""), + item.optString("artistName", ""), + item.optLong("trackTimeMillis", 0) / 1000, + lyrics, track); + scored.add(new Lyrics.ScoredLyrics(score, lyrics)); + if (score >= HIGH_MATCH_SCORE) { + break; + } + } catch (Exception ex) { + Logger.printDebug(() -> "Could not fetch RMM Revival lyrics for a candidate", ex); + } + } + return Lyrics.sortScoredByScore(scored); + } + + private record ScoredItem(JSONObject item, int score, boolean exact) { + } + + /** + * The community uploader, when the entry is a SpicyLyrics community sync rather than + * Apple lyrics relayed through the same API. + */ + private record Uploader(String username, @Nullable String url) { + } } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/ScoredSongProvider.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/ScoredSongProvider.java index d08c2e790b..7f1dd41d4b 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/ScoredSongProvider.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/ScoredSongProvider.java @@ -10,26 +10,18 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import app.morphe.extension.music.patches.lyrics.model.Lyrics; -import app.morphe.extension.music.patches.lyrics.model.TrackInfo; - import java.util.ArrayList; import java.util.List; +import app.morphe.extension.music.patches.lyrics.model.Lyrics; +import app.morphe.extension.music.patches.lyrics.model.TrackInfo; + /** * A provider whose own search already ranks its results, so the top one answers the plain lookup * and the rest fill the candidate queue. Subclasses supply the search and nothing else. */ abstract class ScoredSongProvider implements LyricsProvider { - /** - * One hit of the provider's own search, with the metadata it came under. {@code durationSec} - * is 0 when the provider reports none, which leaves the duration filter to the other fields. - */ - protected record ScoredSong(int score, Lyrics lyrics, String title, String artist, - long durationSec) { - } - /** The hits for this track, best first, empty when the search found none. */ @NonNull protected abstract List fetchScoredSongs(TrackInfo track) throws Exception; @@ -59,4 +51,12 @@ public List fetchCandidates(TrackInfo track) throws Excepti } return scored; } + + /** + * One hit of the provider's own search, with the metadata it came under. {@code durationSec} + * is 0 when the provider reports none, which leaves the duration filter to the other fields. + */ + protected record ScoredSong(int score, Lyrics lyrics, String title, String artist, + long durationSec) { + } } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/SimpMusicProvider.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/SimpMusicProvider.java index 6e373b3abb..49432686d8 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/SimpMusicProvider.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/SimpMusicProvider.java @@ -39,6 +39,84 @@ public final class SimpMusicProvider implements LyricsProvider { private static final Pattern HTML_ENTITY = Pattern.compile("&#x([0-9a-fA-F]+);|&#(\\d+);"); + private static String sourceUrl(String videoId) { + return "https://lyrics.simpmusic.org/#/video/" + videoId; + } + + private static String sanitizeLrc(String lrc, long[] offsetHolder) { + String[] rawLines = lrc.split("\\r?\\n"); + StringBuilder sb = new StringBuilder(lrc.length()); + + for (String rawLine : rawLines) { + String trimmed = rawLine.trim(); + if (trimmed.startsWith("[offset:")) { + int close = trimmed.indexOf(']'); + if (close > 0) { + String val = trimmed.substring(8, close).trim(); + try { + offsetHolder[0] = (long) (Double.parseDouble(val) * 1000); + } catch (NumberFormatException ex) { + Logger.printDebug(() -> "Could not parse offset in SimpMusic LRC", ex); + } + } + continue; + } + sb.append(rawLine).append('\n'); + } + + String result = decodeHtmlEntities(sb.toString()); + result = result.replaceAll("[ \t]{2,}", " "); + result = result.replaceAll(">[ \t]+<", "><"); + result = result.replaceAll(">[ \t]+", ">"); + return result.trim(); + } + + private static String decodeHtmlEntities(String text) { + Matcher matcher = HTML_ENTITY.matcher(text); + StringBuilder builder = new StringBuilder(); + int last = 0; + while (matcher.find()) { + builder.append(text, last, matcher.start()); + try { + if (matcher.group(1) != null) { + builder.append((char) Integer.parseInt(Objects.requireNonNull(matcher.group(1)), 16)); + } else { + builder.append((char) Integer.parseInt(Objects.requireNonNull(matcher.group(2)))); + } + } catch (NumberFormatException ex) { + Logger.printDebug(() -> "Could not parse HTML entity in SimpMusic", ex); + builder.append(matcher.group(0)); + } + last = matcher.end(); + } + builder.append(text, last, text.length()); + return builder.toString() + .replace("&", "&") + .replace("<", "<") + .replace(">", ">") + .replace(""", "\"") + .replace("'", "'"); + } + + private static List collapseSpaces(List lines) { + List result = new ArrayList<>(lines.size()); + for (LyricsLine line : lines) { + String text = line.text().replaceAll(" {2,}", " ").trim(); + if (!text.equals(line.text())) { + result.add(new LyricsLine(line.startTimeMs(), line.endTimeMs(), text, line.words(), + line.agentId(), line.isDuet(), line.isBG(), line.songPart())); + } else { + result.add(line); + } + } + return result; + } + + private static Lyrics withContributor(@Nullable Lyrics lyrics, @Nullable String contributor) { + return LyricsMerge.addSongwriter(lyrics, + contributor == null ? null : "Submitted by " + contributor); + } + @Override public String name() { return "SimpMusic"; @@ -47,7 +125,7 @@ public String name() { @Nullable @Override public FetchResult fetch(TrackInfo track) throws Exception { - String videoId = VideoInformation.getVideoId(); + final String videoId = VideoInformation.getVideoId(); if (videoId.isEmpty()) { return null; } @@ -58,7 +136,7 @@ public FetchResult fetch(TrackInfo track) throws Exception { private Lyrics fetchByVideoId(String videoId) { HttpURLConnection connection = null; try { - String url = BASE_URL + LyricsRequests.encode(videoId); + final String url = BASE_URL + LyricsRequests.encode(videoId); connection = LyricsRequests.openConnection(url); connection.setRequestProperty("User-Agent", CLIENT_USER_AGENT); connection.setRequestProperty("Accept", "application/json"); @@ -76,7 +154,7 @@ private Lyrics fetchByVideoId(String videoId) { if (!root.optBoolean("success", false)) { return null; } - JSONArray data = root.optJSONArray("data"); + final JSONArray data = root.optJSONArray("data"); if (data == null || data.length() == 0) { return null; } @@ -91,10 +169,6 @@ private Lyrics fetchByVideoId(String videoId) { } } - private static String sourceUrl(String videoId) { - return "https://lyrics.simpmusic.org/#/video/" + videoId; - } - @Nullable private Lyrics pickBestEntry(JSONArray data, String videoId) { Lyrics best = null; @@ -108,7 +182,7 @@ private Lyrics pickBestEntry(JSONArray data, String videoId) { if (vote < bestVote) { continue; } - Lyrics lyrics = parseEntry(entry, videoId); + final Lyrics lyrics = parseEntry(entry, videoId); if (lyrics != null && !lyrics.isEmpty()) { best = lyrics; bestVote = vote; @@ -119,18 +193,18 @@ private Lyrics pickBestEntry(JSONArray data, String videoId) { @Nullable private Lyrics parseEntry(JSONObject entry, String videoId) { - String contributor = LyricsRequests.optString(entry, "contributor"); - String richSync = LyricsRequests.optString(entry, "richSyncLyrics"); + final String contributor = LyricsRequests.optString(entry, "contributor"); + final String richSync = LyricsRequests.optString(entry, "richSyncLyrics"); if (richSync != null) { return withContributor(parseLrc(richSync, videoId), contributor); } - String synced = LyricsRequests.optString(entry, "syncedLyrics"); + final String synced = LyricsRequests.optString(entry, "syncedLyrics"); if (synced != null) { return withContributor(parseLrc(synced, videoId), contributor); } - String plain = LyricsRequests.optString(entry, "plainLyric"); + final String plain = LyricsRequests.optString(entry, "plainLyric"); if (plain != null) { - List lines = LRCParser.parsePlain(plain); + final List lines = LRCParser.parsePlain(plain); if (!lines.isEmpty()) { return withContributor(new Lyrics(lines, name(), false, null, null, null, null, plain, "plain", sourceUrl(videoId)), contributor); @@ -141,9 +215,9 @@ private Lyrics parseEntry(JSONObject entry, String videoId) { @Nullable private Lyrics parseLrc(String lrc, String videoId) { - long[] offsetHolder = {0}; - String sanitized = sanitizeLrc(lrc, offsetHolder); - LRCParser.LrcParseResult result = LRCParser.parseSyncedWithCreditLines(sanitized); + final long[] offsetHolder = {0}; + final String sanitized = sanitizeLrc(lrc, offsetHolder); + final LRCParser.LrcParseResult result = LRCParser.parseSyncedWithCreditLines(sanitized); if (result.lines.isEmpty()) { return null; } @@ -156,78 +230,4 @@ private Lyrics parseLrc(String lrc, String videoId) { result.creditLines.isEmpty() ? null : result.creditLines, sanitized, "lrc", sourceUrl(videoId)); } - - private static String sanitizeLrc(String lrc, long[] offsetHolder) { - String[] rawLines = lrc.split("\\r?\\n"); - StringBuilder sb = new StringBuilder(lrc.length()); - - for (String rawLine : rawLines) { - String trimmed = rawLine.trim(); - if (trimmed.startsWith("[offset:")) { - int close = trimmed.indexOf(']'); - if (close > 0) { - String val = trimmed.substring(8, close).trim(); - try { - offsetHolder[0] = (long) (Double.parseDouble(val) * 1000); - } catch (NumberFormatException ex) { - Logger.printDebug(() -> "Could not parse offset in SimpMusic LRC", ex); - } - } - continue; - } - sb.append(rawLine).append('\n'); - } - - String result = decodeHtmlEntities(sb.toString()); - result = result.replaceAll("[ \t]{2,}", " "); - result = result.replaceAll(">[ \t]+<", "><"); - result = result.replaceAll(">[ \t]+", ">"); - return result.trim(); - } - - private static String decodeHtmlEntities(String text) { - Matcher matcher = HTML_ENTITY.matcher(text); - StringBuilder builder = new StringBuilder(); - int last = 0; - while (matcher.find()) { - builder.append(text, last, matcher.start()); - try { - if (matcher.group(1) != null) { - builder.append((char) Integer.parseInt(Objects.requireNonNull(matcher.group(1)), 16)); - } else { - builder.append((char) Integer.parseInt(Objects.requireNonNull(matcher.group(2)))); - } - } catch (NumberFormatException ex) { - Logger.printDebug(() -> "Could not parse HTML entity in SimpMusic", ex); - builder.append(matcher.group(0)); - } - last = matcher.end(); - } - builder.append(text, last, text.length()); - return builder.toString() - .replace("&", "&") - .replace("<", "<") - .replace(">", ">") - .replace(""", "\"") - .replace("'", "'"); - } - - private static List collapseSpaces(List lines) { - List result = new ArrayList<>(lines.size()); - for (LyricsLine line : lines) { - String text = line.text().replaceAll(" {2,}", " ").trim(); - if (!text.equals(line.text())) { - result.add(new LyricsLine(line.startTimeMs(), line.endTimeMs(), text, line.words(), - line.agentId(), line.isDuet(), line.isBG(), line.songPart())); - } else { - result.add(line); - } - } - return result; - } - - private static Lyrics withContributor(@Nullable Lyrics lyrics, @Nullable String contributor) { - return LyricsMerge.addSongwriter(lyrics, - contributor == null ? null : "Submitted by " + contributor); - } } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/SpotifyProvider.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/SpotifyProvider.java index 0cd2afad00..a717e77c71 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/SpotifyProvider.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/SpotifyProvider.java @@ -57,7 +57,12 @@ public final class SpotifyProvider implements LyricsProvider { private static final long MAX_RETRY_AFTER_MS = 30_000; private static final String USER_AGENT = LyricsRequests.CHROME_DESKTOP_USER_AGENT; - + private static final long SECRET_REFRESH_INTERVAL_MS = 60 * 60 * 1000; + private static final int[] FALLBACK_SECRET_DATA = { + 99, 111, 47, 88, 49, 56, 118, 65, 52, 67, 50, 104, + 117, 101, 55, 94, 95, 75, 94, 49, 69, 36, 85, 64, 74, 60 + }; + private static final String FALLBACK_TOTP_VERSION = "19"; @Nullable private static volatile String cachedAccessToken; @Nullable @@ -70,13 +75,240 @@ public final class SpotifyProvider implements LyricsProvider { @Nullable private static String cachedTotpVersion; private static long lastSecretFetchTime = 0; - private static final long SECRET_REFRESH_INTERVAL_MS = 60 * 60 * 1000; - private static final int[] FALLBACK_SECRET_DATA = { - 99, 111, 47, 88, 49, 56, 118, 65, 52, 67, 50, 104, - 117, 101, 55, 94, 95, 75, 94, 49, 69, 36, 85, 64, 74, 60 - }; - private static final String FALLBACK_TOTP_VERSION = "19"; + private static HttpURLConnection openSearchConnection(String accessToken, + @Nullable String clientToken, + byte[] bodyBytes) throws IOException { + final HttpURLConnection connection = (HttpURLConnection) + new java.net.URL(PARTNER_GRAPHQL_URL).openConnection(); + connection.setRequestMethod("POST"); + connection.setConnectTimeout(5000); + connection.setReadTimeout(8000); + connection.setDoOutput(true); + connection.setRequestProperty("Authorization", "Bearer " + accessToken); + connection.setRequestProperty("Content-Type", "application/json"); + connection.setRequestProperty("Accept", "application/json"); + connection.setRequestProperty("app-platform", "WebPlayer"); + connection.setRequestProperty("User-Agent", USER_AGENT); + connection.setRequestProperty("Referer", "https://open.spotify.com/"); + connection.setRequestProperty("Origin", "https://open.spotify.com"); + connection.setRequestProperty("spotify-app-version", CLIENT_VERSION); + if (clientToken != null) { + connection.setRequestProperty("client-token", clientToken); + } + + connection.setFixedLengthStreamingMode(bodyBytes.length); + try (java.io.OutputStream os = connection.getOutputStream()) { + os.write(bodyBytes); + } + return connection; + } + + private static List artistNames(JSONObject node) { + final List out = new ArrayList<>(); + final JSONArray artists = node.optJSONArray("artists"); + if (artists != null) { + for (int i = 0; i < artists.length(); i++) { + final JSONObject artist = artists.optJSONObject(i); + if (artist == null) continue; + final String name = LyricsRequests.optString(artist, "name"); + if (name != null) { + out.add(name); + } + } + if (!out.isEmpty()) { + return out; + } + } + final JSONObject connection = node.optJSONObject("artists"); + if (connection != null) { + final JSONArray edges = connection.optJSONArray("edges"); + if (edges != null) { + for (int i = 0; i < edges.length(); i++) { + final JSONObject edge = edges.optJSONObject(i); + final JSONObject artistNode = + edge != null ? edge.optJSONObject("node") : null; + final String name = artistNode != null + ? LyricsRequests.optString(artistNode, "name") : null; + if (name != null) { + out.add(name); + } + } + } + } + if (out.isEmpty()) { + final String artist = LyricsRequests.optString(node, "artist"); + if (artist != null) { + out.add(artist); + } + } + return out; + } + + private static List parseSyllables(JSONArray syllablesArr, String lineText) { + final List words = new ArrayList<>(syllablesArr.length()); + int charOffset = 0; + + for (int j = 0; j < syllablesArr.length(); j++) { + JSONObject syllable = syllablesArr.optJSONObject(j); + if (syllable == null) { + continue; + } + + final long startMs = parseStartTimeMs(syllable); + final long endMs = syllable.optLong("endTimeMs", startMs); + final int numChars = syllable.optInt("numChars", 0); + if (numChars <= 0) { + continue; + } + + // Spaces before the syllable are not part of it. + while (charOffset < lineText.length() && Character.isWhitespace(lineText.charAt(charOffset))) { + charOffset++; + } + if (charOffset >= lineText.length()) { + break; + } + + final int end = Math.min(charOffset + numChars, lineText.length()); + final String wordText = lineText.substring(charOffset, end); + final boolean spaceAfter = (end < lineText.length() && Character.isWhitespace(lineText.charAt(end))); + words.add(new Word(startMs, endMs, wordText, null, spaceAfter)); + charOffset = end; + } + + return words; + } + + private static long parseStartTimeMs(JSONObject obj) { + final String raw = LyricsRequests.optString(obj, "startTimeMs"); + if (raw == null || raw.isEmpty()) { + return LyricsLine.NO_TIME; + } + try { + return Long.parseLong(raw); + } catch (NumberFormatException ex) { + Logger.printDebug(() -> "Could not parse start time in Spotify lyrics line", ex); + return LyricsLine.NO_TIME; + } + } + + private static HttpURLConnection openServerTimeConnection(String spDc, int readTimeoutMs) throws IOException { + HttpURLConnection connection = (HttpURLConnection) new java.net.URL(SERVER_TIME_URL).openConnection(); + connection.setRequestMethod("GET"); + connection.setConnectTimeout(5000); + connection.setReadTimeout(readTimeoutMs); + connection.setRequestProperty("User-Agent", USER_AGENT); + connection.setRequestProperty("Origin", "https://open.spotify.com/"); + connection.setRequestProperty("Referer", "https://open.spotify.com/"); + connection.setRequestProperty("Cookie", "sp_dc=" + spDc); + return connection; + } + + private static String generateTotp(long timestampMs, String secret) + throws NoSuchAlgorithmException, InvalidKeyException { + final long epochSeconds = timestampMs / 1000; + final long counter = epochSeconds / 30; + + final byte[] counterBytes = new byte[8]; + long tmp = counter; + for (int i = 7; i >= 0; i--) { + counterBytes[i] = (byte) (tmp & 0xFF); + tmp >>= 8; + } + + final Mac mac = Mac.getInstance("HmacSHA1"); + final byte[] secretBytes = secret.getBytes(StandardCharsets.UTF_8); + mac.init(new SecretKeySpec(secretBytes, "HmacSHA1")); + final byte[] hash = mac.doFinal(counterBytes); + + final int offset = hash[hash.length - 1] & 0x0F; + final int binary = ((hash[offset] & 0x7F) << 24) + | ((hash[offset + 1] & 0xFF) << 16) + | ((hash[offset + 2] & 0xFF) << 8) + | (hash[offset + 3] & 0xFF); + + final int otp = binary % 1_000_000; + return String.format(Locale.US, "%06d", otp); + } + + @Nullable + private static String fetchSecrets() { + for (int attempt = 0; attempt <= SECRET_FETCH_RETRIES; attempt++) { + HttpURLConnection connection = null; + try { + connection = (HttpURLConnection) + new java.net.URL(SECRETS_URL).openConnection(); + connection.setRequestMethod("GET"); + connection.setConnectTimeout(5000); + connection.setReadTimeout(8000); + connection.setRequestProperty("User-Agent", USER_AGENT); + + final int code = connection.getResponseCode(); + if (code == 200) { + return Requester.parseString(connection); + } + } catch (IOException ex) { + Logger.printDebug(() -> "Could not read the response", ex); + } finally { + if (connection != null) connection.disconnect(); + } + if (attempt < SECRET_FETCH_RETRIES) { + try { + Thread.sleep(1000L * (attempt + 1)); + } catch (InterruptedException ex) { + Logger.printDebug(() -> "Interrupted during secret fetch retry sleep", ex); + Thread.currentThread().interrupt(); + return null; + } + } + } + return null; + } + + private static String decimalStringFromInts(int[] values) { + final StringBuilder sb = new StringBuilder(values.length); + for (int v : values) { + sb.append(v); + } + return sb.toString(); + } + + private static long parseRetryAfter(HttpURLConnection connection) { + final String retryAfter = connection.getHeaderField("Retry-After"); + if (retryAfter != null) { + try { + final long seconds = Long.parseLong(retryAfter.trim()); + if (seconds > 0) { + return Math.min(seconds, MAX_RETRY_AFTER_MS / 1000) * 1000; + } + } catch (NumberFormatException ex) { + Logger.printDebug(() -> "Could not parse Retry-After header", ex); + } + } + return 3000; // default 3 seconds + } + + public static void invalidateToken() { + cachedAccessToken = null; + cachedClientToken = null; + clientTokenExpiry = 0; + cachedClientId = null; + } + + public static boolean validateToken(String spDc) { + if (spDc == null || spDc.trim().isEmpty()) return false; + HttpURLConnection connection = null; + try { + connection = openServerTimeConnection(spDc, 8000); + return connection.getResponseCode() == Requester.HTTP_STATUS_CODE_SUCCESS; + } catch (Exception ex) { + Logger.printDebug(() -> "Could not validate Spotify token", ex); + return false; + } finally { + if (connection != null) connection.disconnect(); + } + } @Override public String name() { @@ -86,7 +318,7 @@ public String name() { @Nullable @Override public FetchResult fetch(TrackInfo track) throws Exception { - String spDc = Settings.SPOTIFY_TOKEN.get(); + final String spDc = Settings.SPOTIFY_TOKEN.get(); if (spDc.trim().isEmpty()) { return null; } @@ -96,32 +328,30 @@ public FetchResult fetch(TrackInfo track) throws Exception { return null; } - String sourceUrl = "https://open.spotify.com/track/" + hit.id(); + final String sourceUrl = "https://open.spotify.com/track/" + hit.id(); JSONObject lyricsResponse = fetchLyrics(spDc, hit.id()); if (lyricsResponse == null) { return null; } - String rawJson = lyricsResponse.toString(); + final String rawJson = lyricsResponse.toString(); return FetchResult.searched(parseLyrics(lyricsResponse, rawJson, sourceUrl), List.of(hit.title()), hit.artists(), 0L, track); } - private record SearchHit(String id, String title, List artists) {} - @Nullable private SearchHit searchTrack(String spDc, String title, String artist) throws Exception { - String accessToken = getAccessToken(spDc); + final String accessToken = getAccessToken(spDc); if (accessToken == null) { return null; } - String clientToken = getClientToken(spDc); + final String clientToken = getClientToken(spDc); - String query = title + " " + artist; + final String query = title + " " + artist; for (String hash : SEARCH_HASHES) { - String body = new JSONObject() + final String body = new JSONObject() .put("extensions", new JSONObject() .put("persistedQuery", new JSONObject() .put("sha256Hash", hash) @@ -146,7 +376,7 @@ private SearchHit searchTrack(String spDc, String title, String artist) throws E @Nullable private SearchHit executeSearch(String accessToken, @Nullable String clientToken, String body) throws Exception { - byte[] bodyBytes = body.getBytes(StandardCharsets.UTF_8); + final byte[] bodyBytes = body.getBytes(StandardCharsets.UTF_8); long retryAfterMs = 0; for (int attempt = 0; attempt <= SEARCH_RETRIES; attempt++) { @@ -160,13 +390,13 @@ private SearchHit executeSearch(String accessToken, @Nullable String clientToken } } - HttpURLConnection connection = + final HttpURLConnection connection = openSearchConnection(accessToken, clientToken, bodyBytes); final int code = connection.getResponseCode(); if (code == 200) { - String json = Requester.parseString(connection); + final String json = Requester.parseString(connection); connection.disconnect(); SearchHit hit = parseSearchResult(json); @@ -189,34 +419,6 @@ private SearchHit executeSearch(String accessToken, @Nullable String clientToken return null; } - private static HttpURLConnection openSearchConnection(String accessToken, - @Nullable String clientToken, - byte[] bodyBytes) throws IOException { - HttpURLConnection connection = (HttpURLConnection) - new java.net.URL(PARTNER_GRAPHQL_URL).openConnection(); - connection.setRequestMethod("POST"); - connection.setConnectTimeout(5000); - connection.setReadTimeout(8000); - connection.setDoOutput(true); - connection.setRequestProperty("Authorization", "Bearer " + accessToken); - connection.setRequestProperty("Content-Type", "application/json"); - connection.setRequestProperty("Accept", "application/json"); - connection.setRequestProperty("app-platform", "WebPlayer"); - connection.setRequestProperty("User-Agent", USER_AGENT); - connection.setRequestProperty("Referer", "https://open.spotify.com/"); - connection.setRequestProperty("Origin", "https://open.spotify.com"); - connection.setRequestProperty("spotify-app-version", CLIENT_VERSION); - if (clientToken != null) { - connection.setRequestProperty("client-token", clientToken); - } - - connection.setFixedLengthStreamingMode(bodyBytes.length); - try (java.io.OutputStream os = connection.getOutputStream()) { - os.write(bodyBytes); - } - return connection; - } - @Nullable private SearchHit parseSearchResult(String json) { try { @@ -263,13 +465,13 @@ private SearchHit extractSearchHit(JSONArray items) { String id = LyricsRequests.optString(node, "id"); if (id == null || id.isEmpty()) { - String uri = LyricsRequests.optString(node, "uri"); + final String uri = LyricsRequests.optString(node, "uri"); if (uri != null && uri.startsWith("spotify:track:")) { id = uri.substring("spotify:track:".length()); } } if (id != null && !id.isEmpty()) { - String title = LyricsRequests.optString(node, "name"); + final String title = LyricsRequests.optString(node, "name"); return new SearchHit(id, title != null ? title : "", artistNames(node)); } } @@ -277,55 +479,14 @@ private SearchHit extractSearchHit(JSONArray items) { return null; } - private static List artistNames(JSONObject node) { - List out = new ArrayList<>(); - JSONArray artists = node.optJSONArray("artists"); - if (artists != null) { - for (int i = 0; i < artists.length(); i++) { - JSONObject artist = artists.optJSONObject(i); - if (artist == null) continue; - String name = LyricsRequests.optString(artist, "name"); - if (name != null) { - out.add(name); - } - } - if (!out.isEmpty()) { - return out; - } - } - JSONObject connection = node.optJSONObject("artists"); - if (connection != null) { - JSONArray edges = connection.optJSONArray("edges"); - if (edges != null) { - for (int i = 0; i < edges.length(); i++) { - JSONObject edge = edges.optJSONObject(i); - JSONObject artistNode = - edge != null ? edge.optJSONObject("node") : null; - String name = artistNode != null - ? LyricsRequests.optString(artistNode, "name") : null; - if (name != null) { - out.add(name); - } - } - } - } - if (out.isEmpty()) { - String artist = LyricsRequests.optString(node, "artist"); - if (artist != null) { - out.add(artist); - } - } - return out; - } - @Nullable private JSONObject fetchLyrics(String spDc, String trackId) throws Exception { - String accessToken = getAccessToken(spDc); + final String accessToken = getAccessToken(spDc); if (accessToken == null) { return null; } - String url = LYRICS_URL + trackId + "?format=json&market=from_token"; + final String url = LYRICS_URL + trackId + "?format=json&market=from_token"; HttpURLConnection connection = LyricsRequests.openConnection(url); connection.setRequestProperty("Authorization", "Bearer " + accessToken); connection.setRequestProperty("User-Agent", USER_AGENT); @@ -337,7 +498,7 @@ private JSONObject fetchLyrics(String spDc, String trackId) throws Exception { return null; } - String json = LyricsRequests.parseGzipString(connection); + final String json = LyricsRequests.parseGzipString(connection); return new JSONObject(json); } @@ -348,11 +509,11 @@ private Lyrics parseLyrics(JSONObject response, String rawJson, @Nullable String return null; } - String syncType = lyricsObj.optString("syncType", "UNSYNCED"); - String provider = LyricsRequests.optString(lyricsObj, "provider"); - String providerName = (provider != null && !provider.isEmpty()) + final String syncType = lyricsObj.optString("syncType", "UNSYNCED"); + final String provider = LyricsRequests.optString(lyricsObj, "provider"); + final String providerName = (provider != null && !provider.isEmpty()) ? name() + " (via " + provider + ")" : name(); - JSONArray linesArr = lyricsObj.optJSONArray("lines"); + final JSONArray linesArr = lyricsObj.optJSONArray("lines"); if (linesArr == null || linesArr.length() == 0) { return null; } @@ -368,7 +529,7 @@ private Lyrics parseLyrics(JSONObject response, String rawJson, @Nullable String @Nullable private Lyrics parseSyncedLines(JSONArray linesArr, String providerName, String rawJson, @Nullable String sourceUrl) { - List lines = new ArrayList<>(linesArr.length()); + final List lines = new ArrayList<>(linesArr.length()); for (int i = 0; i < linesArr.length(); i++) { JSONObject lineObj = linesArr.optJSONObject(i); @@ -377,7 +538,7 @@ private Lyrics parseSyncedLines(JSONArray linesArr, String providerName, String } final long startTimeMs = parseStartTimeMs(lineObj); - String text = lineObj.optString("words", "").trim(); + final String text = lineObj.optString("words", "").trim(); if (text.isEmpty()) { continue; } @@ -396,7 +557,7 @@ private Lyrics parseSyncedLines(JSONArray linesArr, String providerName, String @Nullable private Lyrics parseSyllableLines(JSONArray linesArr, String providerName, - String rawJson, @Nullable String sourceUrl) { + String rawJson, @Nullable String sourceUrl) { List lines = new ArrayList<>(linesArr.length()); for (int i = 0, length = linesArr.length(); i < length; i++) { @@ -431,58 +592,10 @@ private Lyrics parseSyllableLines(JSONArray linesArr, String providerName, return new Lyrics(lines, providerName, true, null, null, null, null, rawJson, "sp.json", sourceUrl); } - private static List parseSyllables(JSONArray syllablesArr, String lineText) { - List words = new ArrayList<>(syllablesArr.length()); - int charOffset = 0; - - for (int j = 0; j < syllablesArr.length(); j++) { - JSONObject syllable = syllablesArr.optJSONObject(j); - if (syllable == null) { - continue; - } - - final long startMs = parseStartTimeMs(syllable); - final long endMs = syllable.optLong("endTimeMs", startMs); - final int numChars = syllable.optInt("numChars", 0); - if (numChars <= 0) { - continue; - } - - // Spaces before the syllable are not part of it. - while (charOffset < lineText.length() && Character.isWhitespace(lineText.charAt(charOffset))) { - charOffset++; - } - if (charOffset >= lineText.length()) { - break; - } - - final int end = Math.min(charOffset + numChars, lineText.length()); - String wordText = lineText.substring(charOffset, end); - final boolean spaceAfter = (end < lineText.length() && Character.isWhitespace(lineText.charAt(end))); - words.add(new Word(startMs, endMs, wordText, null, spaceAfter)); - charOffset = end; - } - - return words; - } - - private static long parseStartTimeMs(JSONObject obj) { - String raw = LyricsRequests.optString(obj, "startTimeMs"); - if (raw == null || raw.isEmpty()) { - return LyricsLine.NO_TIME; - } - try { - return Long.parseLong(raw); - } catch (NumberFormatException ex) { - Logger.printDebug(() -> "Could not parse start time in Spotify lyrics line", ex); - return LyricsLine.NO_TIME; - } - } - @Nullable private Lyrics parsePlainLines(JSONArray linesArr, String providerName, - String rawJson, @Nullable String sourceUrl) { - List lines = new ArrayList<>(linesArr.length()); + String rawJson, @Nullable String sourceUrl) { + final List lines = new ArrayList<>(linesArr.length()); for (int i = 0; i < linesArr.length(); i++) { JSONObject lineObj = linesArr.optJSONObject(i); @@ -490,7 +603,7 @@ private Lyrics parsePlainLines(JSONArray linesArr, String providerName, continue; } - String text = lineObj.optString("words", "").trim(); + final String text = lineObj.optString("words", "").trim(); if (text.isEmpty()) { continue; } @@ -512,16 +625,16 @@ private synchronized String getAccessToken(String spDc) { HttpURLConnection connection = null; try { ensureTotpSecrets(); - String[] totpSecrets = cachedTotpSecrets; + final String[] totpSecrets = cachedTotpSecrets; if (totpSecrets == null || totpSecrets.length == 0) { return null; } final long serverTimeMs = getServerTime(spDc); - String totpValue = generateTotp(serverTimeMs, totpSecrets[0]); + final String totpValue = generateTotp(serverTimeMs, totpSecrets[0]); - String url = TOKEN_URL + final String url = TOKEN_URL + "?reason=init&productType=web-player" + "&totp=" + totpValue + "&totpVer=" + cachedTotpVersion @@ -543,15 +656,15 @@ private synchronized String getAccessToken(String spDc) { return null; } - String body = Requester.parseString(connection); + final String body = Requester.parseString(connection); JSONObject json = new JSONObject(body); - String token = json.optString("accessToken", ""); + final String token = json.optString("accessToken", ""); if (token.trim().isEmpty()) { return null; } cachedAccessToken = token; - String clientId = LyricsRequests.optString(json, "clientId"); + final String clientId = LyricsRequests.optString(json, "clientId"); if (clientId != null && !clientId.isEmpty()) { cachedClientId = clientId; } @@ -573,17 +686,17 @@ private synchronized String getClientToken(String spDc) { } try { - String accessToken = getAccessToken(spDc); + final String accessToken = getAccessToken(spDc); if (accessToken == null) { return null; } - String clientId = cachedClientId; + final String clientId = cachedClientId; if (clientId == null || clientId.isEmpty()) { return null; } - String deviceId = UUID.randomUUID().toString(); + final String deviceId = UUID.randomUUID().toString(); JSONObject jsSdkData = new JSONObject() .put("device_brand", "unknown") .put("device_model", "unknown") @@ -600,7 +713,7 @@ private synchronized String getClientToken(String spDc) { JSONObject requestBody = new JSONObject() .put("client_data", clientData); - HttpURLConnection connection = (HttpURLConnection) + final HttpURLConnection connection = (HttpURLConnection) new java.net.URL(CLIENT_TOKEN_URL).openConnection(); connection.setRequestMethod("POST"); connection.setConnectTimeout(5000); @@ -612,7 +725,7 @@ private synchronized String getClientToken(String spDc) { connection.setRequestProperty("Origin", "https://open.spotify.com"); connection.setRequestProperty("Referer", "https://open.spotify.com"); - byte[] bodyBytes = requestBody.toString().getBytes(StandardCharsets.UTF_8); + final byte[] bodyBytes = requestBody.toString().getBytes(StandardCharsets.UTF_8); connection.setFixedLengthStreamingMode(bodyBytes.length); try (java.io.OutputStream os = connection.getOutputStream()) { os.write(bodyBytes); @@ -624,7 +737,7 @@ private synchronized String getClientToken(String spDc) { return null; } - String body = Requester.parseString(connection); + final String body = Requester.parseString(connection); JSONObject json = new JSONObject(body); JSONObject grantedToken = json.optJSONObject("granted_token"); if (grantedToken == null) { @@ -632,7 +745,7 @@ private synchronized String getClientToken(String spDc) { return null; } - String token = grantedToken.optString("token", ""); + final String token = grantedToken.optString("token", ""); if (token.isEmpty()) { connection.disconnect(); return null; @@ -650,25 +763,13 @@ private synchronized String getClientToken(String spDc) { } } - private static HttpURLConnection openServerTimeConnection(String spDc, int readTimeoutMs) throws IOException { - HttpURLConnection connection = (HttpURLConnection) new java.net.URL(SERVER_TIME_URL).openConnection(); - connection.setRequestMethod("GET"); - connection.setConnectTimeout(5000); - connection.setReadTimeout(readTimeoutMs); - connection.setRequestProperty("User-Agent", USER_AGENT); - connection.setRequestProperty("Origin", "https://open.spotify.com/"); - connection.setRequestProperty("Referer", "https://open.spotify.com/"); - connection.setRequestProperty("Cookie", "sp_dc=" + spDc); - return connection; - } - private long getServerTime(String spDc) { HttpURLConnection connection = null; try { connection = openServerTimeConnection(spDc, 5000); final int code = connection.getResponseCode(); if (code == 200) { - String body = Requester.parseString(connection); + final String body = Requester.parseString(connection); JSONObject json = new JSONObject(body); final long serverTime = json.optLong("serverTime", 0); if (serverTime > 0) { @@ -691,7 +792,7 @@ private void ensureTotpSecrets() { } try { - String body = fetchSecrets(); + final String body = fetchSecrets(); if (body != null) { JSONObject secrets = new JSONObject(body); @@ -710,20 +811,20 @@ private void ensureTotpSecrets() { } if (newestVersion >= 0) { - String previousVersion = cachedTotpVersion; + final String previousVersion = cachedTotpVersion; cachedTotpVersion = String.valueOf(newestVersion); - JSONArray arr = secrets.getJSONArray(cachedTotpVersion); - int[] secretData = new int[arr.length()]; + final JSONArray arr = secrets.getJSONArray(cachedTotpVersion); + final int[] secretData = new int[arr.length()]; for (int i = 0; i < arr.length(); i++) { secretData[i] = arr.getInt(i); } - int[] xored = new int[secretData.length]; + final int[] xored = new int[secretData.length]; for (int i = 0; i < secretData.length; i++) { xored[i] = secretData[i] ^ ((i % 33) + 9); } - cachedTotpSecrets = new String[]{ decimalStringFromInts(xored) }; + cachedTotpSecrets = new String[]{decimalStringFromInts(xored)}; if (previousVersion != null && !cachedTotpVersion.equals(previousVersion)) { cachedAccessToken = null; @@ -738,114 +839,12 @@ private void ensureTotpSecrets() { } if (cachedTotpSecrets == null || cachedTotpVersion == null) { - cachedTotpSecrets = new String[]{ decimalStringFromInts(FALLBACK_SECRET_DATA) }; + cachedTotpSecrets = new String[]{decimalStringFromInts(FALLBACK_SECRET_DATA)}; cachedTotpVersion = FALLBACK_TOTP_VERSION; lastSecretFetchTime = now; } } - private static String generateTotp(long timestampMs, String secret) - throws NoSuchAlgorithmException, InvalidKeyException { - final long epochSeconds = timestampMs / 1000; - final long counter = epochSeconds / 30; - - byte[] counterBytes = new byte[8]; - long tmp = counter; - for (int i = 7; i >= 0; i--) { - counterBytes[i] = (byte) (tmp & 0xFF); - tmp >>= 8; - } - - Mac mac = Mac.getInstance("HmacSHA1"); - byte[] secretBytes = secret.getBytes(StandardCharsets.UTF_8); - mac.init(new SecretKeySpec(secretBytes, "HmacSHA1")); - byte[] hash = mac.doFinal(counterBytes); - - final int offset = hash[hash.length - 1] & 0x0F; - final int binary = ((hash[offset] & 0x7F) << 24) - | ((hash[offset + 1] & 0xFF) << 16) - | ((hash[offset + 2] & 0xFF) << 8) - | (hash[offset + 3] & 0xFF); - - final int otp = binary % 1_000_000; - return String.format(Locale.US, "%06d", otp); - } - - @Nullable - private static String fetchSecrets() { - for (int attempt = 0; attempt <= SECRET_FETCH_RETRIES; attempt++) { - HttpURLConnection connection = null; - try { - connection = (HttpURLConnection) - new java.net.URL(SECRETS_URL).openConnection(); - connection.setRequestMethod("GET"); - connection.setConnectTimeout(5000); - connection.setReadTimeout(8000); - connection.setRequestProperty("User-Agent", USER_AGENT); - - final int code = connection.getResponseCode(); - if (code == 200) { - return Requester.parseString(connection); - } - } catch (IOException ex) { - Logger.printDebug(() -> "Could not read the response", ex); - } finally { - if (connection != null) connection.disconnect(); - } - if (attempt < SECRET_FETCH_RETRIES) { - try { - Thread.sleep(1000L * (attempt + 1)); - } catch (InterruptedException ex) { - Logger.printDebug(() -> "Interrupted during secret fetch retry sleep", ex); - Thread.currentThread().interrupt(); - return null; - } - } - } - return null; - } - - private static String decimalStringFromInts(int[] values) { - StringBuilder sb = new StringBuilder(values.length); - for (int v : values) { - sb.append(v); - } - return sb.toString(); - } - - private static long parseRetryAfter(HttpURLConnection connection) { - String retryAfter = connection.getHeaderField("Retry-After"); - if (retryAfter != null) { - try { - final long seconds = Long.parseLong(retryAfter.trim()); - if (seconds > 0) { - return Math.min(seconds, MAX_RETRY_AFTER_MS / 1000) * 1000; - } - } catch (NumberFormatException ex) { - Logger.printDebug(() -> "Could not parse Retry-After header", ex); - } - } - return 3000; // default 3 seconds - } - - public static void invalidateToken() { - cachedAccessToken = null; - cachedClientToken = null; - clientTokenExpiry = 0; - cachedClientId = null; - } - - public static boolean validateToken(String spDc) { - if (spDc == null || spDc.trim().isEmpty()) return false; - HttpURLConnection connection = null; - try { - connection = openServerTimeConnection(spDc, 8000); - return connection.getResponseCode() == Requester.HTTP_STATUS_CODE_SUCCESS; - } catch (Exception ex) { - Logger.printDebug(() -> "Could not validate Spotify token", ex); - return false; - } finally { - if (connection != null) connection.disconnect(); - } + private record SearchHit(String id, String title, List artists) { } } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/UnisonProvider.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/UnisonProvider.java index 9cf16d7160..e76e84f892 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/UnisonProvider.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/UnisonProvider.java @@ -43,6 +43,67 @@ public final class UnisonProvider implements LyricsProvider { "0000000000000000000000000000000000000000000000000000000000000000"; private static volatile String cachedKeyId; + private static String getKeyId() { + String cached = cachedKeyId; + if (cached != null) { + return cached; + } + synchronized (UnisonProvider.class) { + if (cachedKeyId != null) { + return cachedKeyId; + } + cachedKeyId = computeKeyId(); + return cachedKeyId; + } + } + + private static String computeKeyId() { + try { + final KeyPairGenerator generator = KeyPairGenerator.getInstance("EC"); + generator.initialize(new ECGenParameterSpec("secp256r1")); + final ECPublicKey publicKey = (ECPublicKey) generator.generateKeyPair().getPublic(); + final String x = base64Url(publicKey.getW().getAffineX()); + final String y = base64Url(publicKey.getW().getAffineY()); + final String canonical = "{\"crv\":\"P-256\",\"kty\":\"EC\",\"x\":\"" + x + "\",\"y\":\"" + y + "\"}"; + final MessageDigest digest = MessageDigest.getInstance("SHA-256"); + final byte[] hash = digest.digest(canonical.getBytes(StandardCharsets.UTF_8)); + return toHex(hash); + } catch (Exception ex) { + Logger.printDebug(() -> "Could not compute Unison key ID", ex); + return FALLBACK_KEY_ID; + } + } + + private static String base64Url(BigInteger value) { + final byte[] bytes = value.toByteArray(); + final byte[] coordinate = new byte[32]; + int srcPos = 0; + if (bytes.length > 32 && bytes[0] == 0) { + srcPos = 1; + } + final int copyLen = Math.min(32, bytes.length - srcPos); + System.arraycopy(bytes, srcPos, coordinate, 32 - copyLen, copyLen); + return Base64.encodeToString(coordinate, + Base64.URL_SAFE | Base64.NO_PADDING | Base64.NO_WRAP); + } + + private static String toHex(byte[] bytes) { + final StringBuilder builder = new StringBuilder(bytes.length * 2); + for (byte b : bytes) { + builder.append(Character.forDigit((b >> 4) & 0xF, 16)); + builder.append(Character.forDigit(b & 0xF, 16)); + } + return builder.toString(); + } + + private static String sourceUrl(String videoId) { + return "https://unison.boidu.dev/song/" + videoId; + } + + private static Lyrics withSubmitter(@Nullable Lyrics lyrics, @Nullable String submitter) { + return LyricsMerge.addSongwriter(lyrics, submitter == null ? null : "Submitted by " + submitter); + } + @Override public String name() { return "Unison"; @@ -51,17 +112,17 @@ public String name() { @Nullable @Override public FetchResult fetch(TrackInfo track) throws Exception { - String videoId = VideoInformation.getVideoId(); + final String videoId = VideoInformation.getVideoId(); if (videoId.isEmpty()) { return null; } - String title = track.title() != null ? track.title() : ""; - String artist = track.artist() != null ? track.artist() : ""; + final String title = track.title() != null ? track.title() : ""; + final String artist = track.artist() != null ? track.artist() : ""; final long videoLengthMs = VideoInformation.getVideoLength(); final int duration = videoLengthMs > 0 ? (int) (videoLengthMs / 1000) : track.durationSeconds(); - String album = track.album(); + final String album = track.album(); return FetchResult.of(fetchByVideoId(videoId, title, artist, duration, album)); } @@ -71,7 +132,7 @@ private Lyrics fetchByVideoId(String videoId, String title, String artist, int duration, String album) { HttpURLConnection connection = null; try { - StringBuilder url = new StringBuilder(BASE_URL) + final StringBuilder url = new StringBuilder(BASE_URL) .append("?v=").append(LyricsRequests.encode(videoId)); if (!title.isEmpty()) { url.append("&song=").append(LyricsRequests.encode(title)); @@ -102,13 +163,13 @@ private Lyrics fetchByVideoId(String videoId, String title, String artist, if (data == null) { return null; } - String format = LyricsRequests.optString(data, "format"); - String lyrics = LyricsRequests.optString(data, "lyrics"); + final String format = LyricsRequests.optString(data, "format"); + final String lyrics = LyricsRequests.optString(data, "lyrics"); if (format == null || lyrics == null) { return null; } - JSONObject submitter = data.optJSONObject("submitter"); - String submitterName = submitter != null + final JSONObject submitter = data.optJSONObject("submitter"); + final String submitterName = submitter != null ? LyricsRequests.optString(submitter, "displayName") : null; return parseLyrics(format, lyrics, videoId, submitterName); @@ -122,63 +183,6 @@ private Lyrics fetchByVideoId(String videoId, String title, String artist, } } - private static String getKeyId() { - String cached = cachedKeyId; - if (cached != null) { - return cached; - } - synchronized (UnisonProvider.class) { - if (cachedKeyId != null) { - return cachedKeyId; - } - cachedKeyId = computeKeyId(); - return cachedKeyId; - } - } - - private static String computeKeyId() { - try { - KeyPairGenerator generator = KeyPairGenerator.getInstance("EC"); - generator.initialize(new ECGenParameterSpec("secp256r1")); - ECPublicKey publicKey = (ECPublicKey) generator.generateKeyPair().getPublic(); - String x = base64Url(publicKey.getW().getAffineX()); - String y = base64Url(publicKey.getW().getAffineY()); - String canonical = "{\"crv\":\"P-256\",\"kty\":\"EC\",\"x\":\"" + x + "\",\"y\":\"" + y + "\"}"; - MessageDigest digest = MessageDigest.getInstance("SHA-256"); - byte[] hash = digest.digest(canonical.getBytes(StandardCharsets.UTF_8)); - return toHex(hash); - } catch (Exception ex) { - Logger.printDebug(() -> "Could not compute Unison key ID", ex); - return FALLBACK_KEY_ID; - } - } - - private static String base64Url(BigInteger value) { - byte[] bytes = value.toByteArray(); - byte[] coordinate = new byte[32]; - int srcPos = 0; - if (bytes.length > 32 && bytes[0] == 0) { - srcPos = 1; - } - final int copyLen = Math.min(32, bytes.length - srcPos); - System.arraycopy(bytes, srcPos, coordinate, 32 - copyLen, copyLen); - return Base64.encodeToString(coordinate, - Base64.URL_SAFE | Base64.NO_PADDING | Base64.NO_WRAP); - } - - private static String toHex(byte[] bytes) { - StringBuilder builder = new StringBuilder(bytes.length * 2); - for (byte b : bytes) { - builder.append(Character.forDigit((b >> 4) & 0xF, 16)); - builder.append(Character.forDigit(b & 0xF, 16)); - } - return builder.toString(); - } - - private static String sourceUrl(String videoId) { - return "https://unison.boidu.dev/song/" + videoId; - } - @Nullable private Lyrics parseLyrics(String format, String lyrics, String videoId, @Nullable String submitter) { @@ -196,7 +200,7 @@ private Lyrics parseLyrics(String format, String lyrics, String videoId, result.creditLines.isEmpty() ? null : result.creditLines, lyrics, "lrc", sourceUrl(videoId)), submitter); case "plain": - List plain = LRCParser.parsePlain(lyrics); + final List plain = LRCParser.parsePlain(lyrics); if (plain.isEmpty()) { return null; } @@ -206,8 +210,4 @@ private Lyrics parseLyrics(String format, String lyrics, String videoId, return null; } } - - private static Lyrics withSubmitter(@Nullable Lyrics lyrics, @Nullable String submitter) { - return LyricsMerge.addSongwriter(lyrics, submitter == null ? null : "Submitted by " + submitter); - } } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/YouTubeMusicProvider.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/YouTubeMusicProvider.java index b2c6a4f3a2..549a1b352f 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/YouTubeMusicProvider.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/requests/YouTubeMusicProvider.java @@ -41,44 +41,6 @@ public final class YouTubeMusicProvider implements LyricsProvider { private static final String ANDROID_MUSIC_VERSION = "7.21.50"; private static final String INNERTUBE_USER_AGENT = LyricsRequests.CHROME_DESKTOP_USER_AGENT; - @Override - public String name() { - return "YTMusic"; - } - - @Nullable - @Override - public FetchResult fetch(TrackInfo track) throws Exception { - String videoId = VideoInformation.getVideoId(); - if (videoId.isEmpty()) { - return null; - } - - String browseId = fetchLyricsBrowseId(videoId); - if (browseId == null) { - return null; - } - - Lyrics plainResult = fetchLyrics(browseId, false); - if (plainResult != null && !plainResult.isEmpty()) { - if (plainResult.synced()) { - return FetchResult.of(plainResult); - } - Lyrics timedResult = fetchLyrics(browseId, true); - if (timedResult != null && timedResult.synced()) { - return FetchResult.of(timedResult); - } - return FetchResult.of(plainResult); - } - - Lyrics androidResult = fetchLyrics(browseId, true); - if (androidResult != null && !androidResult.isEmpty()) { - return FetchResult.of(androidResult); - } - - return null; - } - @Nullable private static String fetchLyricsBrowseId(String videoId) { try { @@ -87,7 +49,7 @@ private static String fetchLyricsBrowseId(String videoId) { body.put("videoId", videoId); body.put("playlistId", "RDAMVM" + videoId); - HttpURLConnection conn = postInnertube(NEXT_URL, body.toString(), + final HttpURLConnection conn = postInnertube(NEXT_URL, body.toString(), WEB_REMIX_VERSION, true); try { final int code = conn.getResponseCode(); @@ -109,7 +71,7 @@ private static String fetchLyricsBrowseId(String videoId) { @Nullable private static String extractLyricsBrowseId(JSONObject response) { try { - JSONArray tabs = response + final JSONArray tabs = response .getJSONObject("contents") .getJSONObject("singleColumnMusicWatchNextResultsRenderer") .getJSONObject("tabbedRenderer") @@ -137,47 +99,18 @@ private static String extractLyricsBrowseId(JSONObject response) { return null; } - @Nullable - private Lyrics fetchLyrics(String browseId, boolean useAndroidClient) { - try { - String clientVersion = useAndroidClient - ? ANDROID_MUSIC_VERSION : WEB_REMIX_VERSION; - JSONObject body = new JSONObject(); - body.put("context", buildContext( - useAndroidClient ? "ANDROID_MUSIC" : "WEB_REMIX", clientVersion)); - body.put("browseId", browseId); - - HttpURLConnection conn = postInnertube(BROWSE_URL, body.toString(), - clientVersion, false); - try { - final int code = conn.getResponseCode(); - if (code != 200) { - conn.disconnect(); - return null; - } - JSONObject response = Requester.parseJSONObject(conn); - return parseLyricsResponse(response); - } finally { - conn.disconnect(); - } - } catch (Exception ex) { - Logger.printDebug(() -> "Could not fetch YTMusic lyrics", ex); - return null; - } - } - @Nullable private static Lyrics parseLyricsResponse(JSONObject response) { JSONObject lyricsData = navigateTimedLyricsModel(response); - String viaProvider = extractSourceProvider(lyricsData, response); - String sourceTag = viaProvider != null + final String viaProvider = extractSourceProvider(lyricsData, response); + final String sourceTag = viaProvider != null ? "YouTube Music (via " + viaProvider + ")" : "YouTube Music"; if (lyricsData != null) { - JSONArray timedArray = lyricsData.optJSONArray("timedLyricsData"); + final JSONArray timedArray = lyricsData.optJSONArray("timedLyricsData"); if (timedArray != null && timedArray.length() > 0) { - List lines = parseTimedLyrics(timedArray); + final List lines = parseTimedLyrics(timedArray); if (!lines.isEmpty()) { boolean hasTimestamp = false; for (LyricsLine line : lines) { @@ -190,27 +123,27 @@ private static Lyrics parseLyricsResponse(JSONObject response) { return new Lyrics(lines, sourceTag, true, null, null, null, null, timedArray.toString(), "ytm.json", null); } - StringBuilder sb = new StringBuilder(); + final StringBuilder sb = new StringBuilder(); for (int i = 0; i < lines.size(); i++) { if (i > 0) sb.append('\n'); sb.append(lines.get(i).text()); } - String plainText = sb.toString(); + final String plainText = sb.toString(); return new Lyrics(LyricsRequests.parsePlainTextLines(plainText), sourceTag, false, null, null, null, null, plainText, "txt", null); } } - String instrumental = lyricsData.optString("lyrics", ""); + final String instrumental = lyricsData.optString("lyrics", ""); if (instrumental.contains("[Instrumental]") || instrumental.contains("♪")) { return Lyrics.NOT_FOUND; } } - String plainText = extractPlainTextLyrics(response); + final String plainText = extractPlainTextLyrics(response); if (plainText != null && !plainText.isEmpty()) { - List lines = LyricsRequests.parsePlainTextLines(plainText); + final List lines = LyricsRequests.parsePlainTextLines(plainText); if (!lines.isEmpty()) { return new Lyrics(lines, sourceTag, false, null, null, null, null, plainText, "txt", null); @@ -224,8 +157,8 @@ private static Lyrics parseLyricsResponse(JSONObject response) { private static String extractSourceProvider(@Nullable JSONObject lyricsData, JSONObject response) { if (lyricsData != null) { - String source = lyricsData.optString("sourceMessage", ""); - String parsed = parseSourceProvider(source); + final String source = lyricsData.optString("sourceMessage", ""); + final String parsed = parseSourceProvider(source); if (parsed != null) { return parsed; } @@ -238,7 +171,7 @@ private static String extractSourceProvider(@Nullable JSONObject lyricsData, .getJSONObject(0) .getJSONObject("musicDescriptionShelfRenderer") .getJSONObject("footer"); - String source = footer.getJSONArray("runs") + final String source = footer.getJSONArray("runs") .getJSONObject(0).optString("text", ""); return parseSourceProvider(source); } catch (Exception ex) { @@ -250,7 +183,7 @@ private static String extractSourceProvider(@Nullable JSONObject lyricsData, @Nullable private static String parseSourceProvider(String sourceMessage) { if (sourceMessage.startsWith("Source: ")) { - String provider = sourceMessage.substring(8).trim(); + final String provider = sourceMessage.substring(8).trim(); return provider.isEmpty() ? null : provider; } return null; @@ -277,7 +210,7 @@ private static JSONObject navigateTimedLyricsModel(JSONObject response) { @Nullable private static String extractPlainTextLyrics(JSONObject response) { try { - JSONArray sectionContents = response + final JSONArray sectionContents = response .getJSONObject("contents") .getJSONObject("sectionListRenderer") .getJSONArray("contents"); @@ -294,9 +227,9 @@ private static String extractPlainTextLyrics(JSONObject response) { if (description == null) { continue; } - JSONArray runs = description.optJSONArray("runs"); + final JSONArray runs = description.optJSONArray("runs"); if (runs != null && runs.length() > 0) { - StringBuilder sb = new StringBuilder(); + final StringBuilder sb = new StringBuilder(); for (int r = 0; r < runs.length(); r++) { JSONObject run = runs.optJSONObject(r); if (run != null) { @@ -313,13 +246,13 @@ private static String extractPlainTextLyrics(JSONObject response) { } private static List parseTimedLyrics(JSONArray timedArray) { - List lines = new ArrayList<>(); + final List lines = new ArrayList<>(); for (int i = 0; i < timedArray.length(); i++) { JSONObject entry = timedArray.optJSONObject(i); if (entry == null) { continue; } - String text = entry.optString("lyricLine", "").trim(); + final String text = entry.optString("lyricLine", "").trim(); if (text.isEmpty()) { continue; } @@ -335,7 +268,7 @@ private static long parseTimestamp(JSONObject cueRange, String key) { if (cueRange == null) { return LyricsLine.NO_TIME; } - String raw = cueRange.optString(key, ""); + final String raw = cueRange.optString(key, ""); if (raw.isEmpty()) { return LyricsLine.NO_TIME; } @@ -362,9 +295,9 @@ private static JSONObject buildContext(String clientName, String clientVersion) } private static HttpURLConnection postInnertube(String url, String jsonBody, - String clientVersion, - boolean sendAuth) throws Exception { - HttpURLConnection conn = Requester.openConnection(url); + String clientVersion, + boolean sendAuth) throws Exception { + final HttpURLConnection conn = Requester.openConnection(url); conn.setRequestMethod("POST"); conn.setConnectTimeout(CONNECT_TIMEOUT_MS); conn.setReadTimeout(READ_TIMEOUT_MS); @@ -385,11 +318,78 @@ private static HttpURLConnection postInnertube(String url, String jsonBody, } } - byte[] bytes = jsonBody.getBytes(java.nio.charset.StandardCharsets.UTF_8); + final byte[] bytes = jsonBody.getBytes(java.nio.charset.StandardCharsets.UTF_8); conn.setFixedLengthStreamingMode(bytes.length); try (java.io.OutputStream out = conn.getOutputStream()) { out.write(bytes); } return conn; } + + @Override + public String name() { + return "YTMusic"; + } + + @Nullable + @Override + public FetchResult fetch(TrackInfo track) throws Exception { + final String videoId = VideoInformation.getVideoId(); + if (videoId.isEmpty()) { + return null; + } + + final String browseId = fetchLyricsBrowseId(videoId); + if (browseId == null) { + return null; + } + + final Lyrics plainResult = fetchLyrics(browseId, false); + if (plainResult != null && !plainResult.isEmpty()) { + if (plainResult.synced()) { + return FetchResult.of(plainResult); + } + final Lyrics timedResult = fetchLyrics(browseId, true); + if (timedResult != null && timedResult.synced()) { + return FetchResult.of(timedResult); + } + return FetchResult.of(plainResult); + } + + final Lyrics androidResult = fetchLyrics(browseId, true); + if (androidResult != null && !androidResult.isEmpty()) { + return FetchResult.of(androidResult); + } + + return null; + } + + @Nullable + private Lyrics fetchLyrics(String browseId, boolean useAndroidClient) { + try { + final String clientVersion = useAndroidClient + ? ANDROID_MUSIC_VERSION : WEB_REMIX_VERSION; + JSONObject body = new JSONObject(); + body.put("context", buildContext( + useAndroidClient ? "ANDROID_MUSIC" : "WEB_REMIX", clientVersion)); + body.put("browseId", browseId); + + final HttpURLConnection conn = postInnertube(BROWSE_URL, body.toString(), + clientVersion, false); + try { + final int code = conn.getResponseCode(); + if (code != 200) { + conn.disconnect(); + return null; + } + JSONObject response = Requester.parseJSONObject(conn); + return parseLyricsResponse(response); + } finally { + conn.disconnect(); + } + } catch (Exception ex) { + Logger.printDebug(() -> "Could not fetch YTMusic lyrics", ex); + return null; + } + } } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/AlphabetRomanizer.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/AlphabetRomanizer.java index 2c2c2666c5..692ea56b52 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/AlphabetRomanizer.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/AlphabetRomanizer.java @@ -17,12 +17,7 @@ */ final class AlphabetRomanizer { - interface Script { - boolean contains(char c); - } - private static final int COMBINING_DIAERESIS = 0x0308; - private final Script script; private final Map letters; private final Map digraphs; @@ -34,13 +29,29 @@ interface Script { * @param stripAccents Whether accents only mark stress and are dropped before the lookup. */ AlphabetRomanizer(Script script, Map letters, Map digraphs, - boolean stripAccents) { + boolean stripAccents) { this.script = script; this.letters = letters; this.digraphs = digraphs; this.stripAccents = stripAccents; } + private static String decompose(char c) { + return Normalizer.normalize(String.valueOf(c), Normalizer.Form.NFD); + } + + /** A capital letter keeps its capital in the reading. */ + private static String cased(String reading, char source) { + if (reading.isEmpty() || !Character.isUpperCase(source)) return reading; + return Character.toUpperCase(reading.charAt(0)) + reading.substring(1); + } + + private static void append(StringBuilder out, String word) { + if (word.isEmpty()) return; + if (out.length() > 0) out.append(' '); + out.append(word); + } + String romanize(String line) { StringBuilder out = new StringBuilder(line.length() * 2); StringBuilder word = new StringBuilder(); @@ -60,16 +71,16 @@ String romanize(String line) { continue; } - String base = base(c); + final String base = base(c); if (i + 1 < line.length() && pairsWith(line.charAt(i + 1))) { - String pair = digraphs.get(base + base(line.charAt(i + 1))); + final String pair = digraphs.get(base + base(line.charAt(i + 1))); if (pair != null) { word.append(cased(pair, c)); i += 2; continue; } } - String reading = letters.get(base); + final String reading = letters.get(base); word.append(reading != null ? cased(reading, c) : String.valueOf(c)); i++; } @@ -77,6 +88,11 @@ String romanize(String line) { return out.toString().trim(); } + /** How many readings the tables hold, for the settings row's entry count. */ + int size() { + return letters.size() + digraphs.size(); + } + /** A diaeresis says the vowel is sounded on its own, so it never joins the one before. */ private boolean pairsWith(char next) { if (!script.contains(next)) return false; @@ -92,20 +108,7 @@ private String base(char c) { return String.valueOf(lower); } - private static String decompose(char c) { - return Normalizer.normalize(String.valueOf(c), Normalizer.Form.NFD); - } - - /** A capital letter keeps its capital in the reading. */ - private static String cased(String reading, char source) { - if (reading.isEmpty() || !Character.isUpperCase(source)) return reading; - return Character.toUpperCase(reading.charAt(0)) + reading.substring(1); - } - - private static void append(StringBuilder out, String word) { - if (word.isEmpty()) return; - //noinspection SizeReplaceableByIsEmpty - if (out.length() > 0) out.append(' '); - out.append(word); + interface Script { + boolean contains(char c); } } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/ArmenianRomanizer.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/ArmenianRomanizer.java deleted file mode 100644 index 2af821a19c..0000000000 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/ArmenianRomanizer.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright 2026 Morphe. - * https://github.com/MorpheApp/morphe-patches/pull/3851 - * - * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. - */ - -package app.morphe.extension.music.patches.lyrics.romanizers; - -import java.util.HashMap; -import java.util.Map; - -/** - * Armenian writes every vowel in full, so this is rules, not a dictionary; only the pairs spelling - * one sound with two letters need a second table. - */ -public final class ArmenianRomanizer { - - private static final Map LETTERS = new HashMap<>(); - private static final Map DIGRAPHS = new HashMap<>(); - - static { - LETTERS.put("ա", "a"); - LETTERS.put("բ", "b"); - LETTERS.put("գ", "g"); - LETTERS.put("դ", "d"); - LETTERS.put("ե", "e"); - LETTERS.put("զ", "z"); - LETTERS.put("է", "e"); - LETTERS.put("ը", "y"); - LETTERS.put("թ", "t"); - LETTERS.put("ժ", "zh"); - LETTERS.put("ի", "i"); - LETTERS.put("լ", "l"); - LETTERS.put("խ", "kh"); - LETTERS.put("ծ", "ts"); - LETTERS.put("կ", "k"); - LETTERS.put("հ", "h"); - LETTERS.put("ձ", "dz"); - LETTERS.put("ղ", "gh"); - LETTERS.put("ճ", "ch"); - LETTERS.put("մ", "m"); - LETTERS.put("յ", "y"); - LETTERS.put("ն", "n"); - LETTERS.put("շ", "sh"); - LETTERS.put("ո", "o"); - LETTERS.put("չ", "ch"); - LETTERS.put("պ", "p"); - LETTERS.put("ջ", "j"); - LETTERS.put("ռ", "r"); - LETTERS.put("ս", "s"); - LETTERS.put("վ", "v"); - LETTERS.put("տ", "t"); - LETTERS.put("ր", "r"); - LETTERS.put("ց", "ts"); - LETTERS.put("ւ", "v"); - LETTERS.put("փ", "p"); - LETTERS.put("ք", "k"); - LETTERS.put("օ", "o"); - LETTERS.put("ֆ", "f"); - // One ligature that stands for two letters. - LETTERS.put("և", "ev"); - - DIGRAPHS.put("ու", "u"); - } - - private static final AlphabetRomanizer READER = new AlphabetRomanizer( - ScriptDetector::isArmenian, LETTERS, DIGRAPHS, false); - - private ArmenianRomanizer() { - } - - public static String romanize(String line) { - return READER.romanize(line); - } -} diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/CantoneseDictStore.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/CantoneseDictStore.java deleted file mode 100644 index b55ad6666c..0000000000 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/CantoneseDictStore.java +++ /dev/null @@ -1,148 +0,0 @@ -/* - * Copyright 2026 Morphe. - * https://github.com/MorpheApp/morphe-patches/pull/3851 - * - * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. - */ - -package app.morphe.extension.music.patches.lyrics.romanizers; - -import androidx.annotation.Nullable; - -import java.io.BufferedInputStream; -import java.io.BufferedReader; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStreamReader; -import java.nio.charset.StandardCharsets; - -import app.morphe.extension.shared.Logger; - -/** - * The Cantonese side of the same Unihan download as {@link PinyinDictStore}, which builds this - * table too since it deletes the archive as soon as it is unpacked. - */ -public final class CantoneseDictStore { - - static final String FIELD = "kCantonese"; - private static final String TABLE_NAME = "cantonese_readings_" + PinyinDictStore.VERSION + ".txt"; - private static final String NOTICE_NAME = "cantonese_readings.NOTICE.txt"; - - /** The commit is what names the version: the file is updated in place, and the table - * has to be rebuilt when it changes. */ - private static final String WORDS_REVISION = "259f0e48bba840c3a2e0d117539e96937f3d89bc"; - private static final String WORDS_URL = - "https://raw.githubusercontent.com/rime/rime-cantonese/" + WORDS_REVISION - + "/jyut6ping3.words.dict.yaml"; - private static final String WORDS_ARCHIVE = "cantonese_words.yaml"; - private static final String WORDS_NOTICE_NAME = "cantonese_words.NOTICE.txt"; - private static final WordTableFile WORDS = new WordTableFile( - "cantonese_words_" + WORDS_REVISION + ".bin", - new byte[] {'M', 'C', 'Y', 'W', '0', '0', '0', '1'}, 1 << 20); - - @Nullable - private static volatile CharReadingTable characters; - - private CantoneseDictStore() { - } - - /** Loads the table on first use, since only the settings row downloads it. */ - public static boolean isReady() { - if (characters != null) return true; - File table = tableFile(); - if (table == null || !table.isFile()) return false; - try { - characters = CharReadingTable.load(table); - } catch (IOException ex) { - Logger.printDebug(() -> "Could not read the Cantonese table", ex); - return false; - } - return true; - } - - @Nullable - static File tableFile() { - File directory = RomanizationDict.directory(); - return directory == null ? null : new File(directory, TABLE_NAME); - } - - @Nullable - public static String reading(char c) { - CharReadingTable table = characters; - return table == null ? null : table.reading(c); - } - - static void writeNotice(File directory) throws IOException { - RomanizationDict.writeNotice(directory, NOTICE_NAME, - PinyinDictStore.noticeTextFor(FIELD)); - } - - /** Null leaves the line to be read a character at a time. */ - @Nullable - public static WordReadingTable word() { - return WORDS.open(); - } - - /** The dictionary lists no single characters, which is what the character table is for. */ - static void buildWords(File directory, RomanizationDict.Listener listener) throws IOException { - WORDS.build(directory, WORDS_URL, WORDS_ARCHIVE, CantoneseDictStore::addWords, listener); - RomanizationDict.writeNotice(directory, WORDS_NOTICE_NAME, wordsNoticeText()); - } - - /** A word may list the readings it shares with others in brackets; the first is the one - * wanted. */ - private static void addWords(File archive, WordReadingTable.Builder builder, - RomanizationDict.Listener listener) throws IOException { - int seen = 0; - try (BufferedReader reader = new BufferedReader(new InputStreamReader( - new BufferedInputStream(new FileInputStream(archive), 1 << 16), - StandardCharsets.UTF_8), 1 << 16)) { - String line; - while ((line = reader.readLine()) != null) { - int tab = line.indexOf('\t'); - if (tab <= 0) continue; - - String reading = line.substring(tab + 1); - int cut = reading.indexOf('['); - if (cut >= 0) reading = reading.substring(0, cut); - reading = reading.trim(); - if (reading.isEmpty()) continue; - - builder.put(line.substring(0, tab), reading); - if (++seen % WordTableFile.PROGRESS_EVERY == 0) { - listener.onBuilding(Math.min(99, seen / WordTableFile.PROGRESS_EVERY)); - } - } - listener.onBuilding(100); - } - } - - private static String wordsNoticeText() { - return "Rime Cantonese word readings\n" - + WORDS_URL + "\n\n" - + "From https://github.com/rime/rime-cantonese\n\n" - + "Licence: Creative Commons Attribution 4.0 International (CC BY 4.0)\n" - + "https://creativecommons.org/licenses/by/4.0/legalcode\n\n" - + "Creator: CanCLID, the Cantonese Linguistics Infrastructure Development Group\n" - + "https://github.com/CanCLID/rime-cantonese-upstream\n\n" - + "The work is provided as-is and the contributors make no warranties of any kind\n" - + "concerning it.\n\n" - + "What is stored here is the Jyutping reading of each word, taken from\n" - + "jyut6ping3.words.dict.yaml in the file above.\n"; - } - - @Nullable - public static String wordsNotice() { - File directory = RomanizationDict.directory(); - return directory == null ? null - : RomanizationDict.readNotice(directory, WORDS_NOTICE_NAME); - } - - @Nullable - public static String notice() { - File directory = RomanizationDict.directory(); - return directory == null ? null - : RomanizationDict.readNotice(directory, NOTICE_NAME); - } -} diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/CldrTransforms.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/CldrTransforms.java new file mode 100644 index 0000000000..98d05a8121 --- /dev/null +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/CldrTransforms.java @@ -0,0 +1,143 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/3750 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.extension.music.patches.lyrics.romanizers; + +import androidx.annotation.Nullable; + +import java.io.BufferedInputStream; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; + +import app.morphe.extension.shared.Logger; + +/** + * The CLDR transliteration rules, one {@link AlphabetRomanizer} per script: a script written as + * it sounds is read letter by letter, everything else whole. Adding a table touches + * {@link CldrTransformsStore#TRANSLITERATIONS}, {@link #STRIP_ACCENTS} and {@link #scriptPredicate}; + * the Slavic BGN tables share the Cyrillic blocks and predicate. + */ +final class CldrTransforms { + + /** The reader of each script the store has written a table for. */ + private static final Map READERS = new HashMap<>(); + + private static final Map STRIP_ACCENTS = new HashMap<>(); + + static { + STRIP_ACCENTS.put("greek", Boolean.TRUE); + STRIP_ACCENTS.put("cyrillic", Boolean.FALSE); + STRIP_ACCENTS.put("russian", Boolean.FALSE); + STRIP_ACCENTS.put("ukrainian", Boolean.FALSE); + STRIP_ACCENTS.put("belarusian", Boolean.FALSE); + STRIP_ACCENTS.put("bulgarian", Boolean.FALSE); + STRIP_ACCENTS.put("macedonian", Boolean.FALSE); + STRIP_ACCENTS.put("serbian", Boolean.FALSE); + STRIP_ACCENTS.put("georgian", Boolean.FALSE); + STRIP_ACCENTS.put("armenian", Boolean.FALSE); + STRIP_ACCENTS.put("hebrew", Boolean.FALSE); + STRIP_ACCENTS.put("arabic", Boolean.FALSE); + STRIP_ACCENTS.put("amharic", Boolean.FALSE); + STRIP_ACCENTS.put("syriac", Boolean.FALSE); + } + + private CldrTransforms() { + } + + /** The reader of a script, or null while its table is still being fetched. */ + @Nullable + static AlphabetRomanizer reader(String script) { + AlphabetRomanizer loaded = READERS.get(script); + if (loaded != null) return loaded; + File file = tableFile(script); + if (file == null || !file.isFile()) return null; + try { + loaded = readTable(file, script); + READERS.put(script, loaded); + return loaded; + } catch (IOException ex) { + Logger.printDebug(() -> "Could not read the CLDR table " + script, ex); + return null; + } + } + + /** Forgets the readers so a deleted table is fetched again. */ + static void clear() { + READERS.clear(); + } + + @Nullable + private static File tableFile(String script) { + File directory = RomanizationDict.directory(); + return directory == null ? null : new File(directory, script + "_transforms.txt"); + } + + private static AlphabetRomanizer readTable(File file, String script) throws IOException { + Map letters = new HashMap<>(); + Map digraphs = new HashMap<>(); + try (BufferedReader reader = new BufferedReader(new InputStreamReader( + new BufferedInputStream(new FileInputStream(file), 1 << 16), + StandardCharsets.UTF_8), 1 << 16)) { + String line; + while ((line = reader.readLine()) != null) { + int tab = line.indexOf('\t'); + if (tab <= 0) continue; + String source = line.substring(0, tab); + String reading = line.substring(tab + 1).trim(); + // An empty reading spells the letter away: the alef and the hard and soft + // signs are read as nothing at all. + if (source.length() == 1) { + letters.put(source, reading); + } else if (source.length() == 2) { + digraphs.put(source, reading); + } + } + } + return new AlphabetRomanizer(scriptPredicate(script), letters, digraphs, + stripAccents(script)); + } + + private static AlphabetRomanizer.Script scriptPredicate(String script) { + switch (script) { + case "greek": + return ScriptDetector::isGreek; + case "cyrillic": + case "russian": + case "ukrainian": + case "belarusian": + case "bulgarian": + case "macedonian": + case "serbian": + return ScriptDetector::isCyrillic; + case "georgian": + return ScriptDetector::isGeorgian; + case "armenian": + return ScriptDetector::isArmenian; + case "hebrew": + return ScriptDetector::isHebrew; + case "arabic": + return ScriptDetector::isArabic; + case "amharic": + return ScriptDetector::isEthiopic; + case "syriac": + return ScriptDetector::isSyriac; + default: + return c -> false; + } + } + + private static boolean stripAccents(String script) { + Boolean strip = STRIP_ACCENTS.get(script); + return strip != null && strip; + } +} diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/CldrTransformsStore.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/CldrTransformsStore.java new file mode 100644 index 0000000000..d80525306f --- /dev/null +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/CldrTransformsStore.java @@ -0,0 +1,526 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/3750 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.extension.music.patches.lyrics.romanizers; + +import androidx.annotation.Nullable; + +import java.io.BufferedInputStream; +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.zip.GZIPInputStream; + +import app.morphe.extension.shared.Logger; + +/** + * The CLDR {@code -Latin} transliteration rules, fetched as one archive. Only literal rules are + * kept: contextual rules and filters need ICU machinery the lyric reader lacks, and combining + * marks are dropped (not sung; a marks-only reading loses the letter, like the Hebrew alef). + * Indic, Thai and Myanmar are excluded, as they reshape letters in context. Downloaded, since + * the patch merges only XML resources and the rules move with the standard. + */ +public final class CldrTransformsStore { + + /** The CLDR release the rules were taken from. */ + private static final String VERSION = "49.0.0"; + + private static final String URL = + "https://registry.npmjs.org/cldr-transforms/-/cldr-transforms-" + VERSION + ".tgz"; + private static final String ARCHIVE = "cldr-transforms-" + VERSION + ".tgz"; + private static final String NOTICE_NAME = "cldr_transforms.NOTICE.txt"; + + /** + * The CLDR transform per table, named without its extension. The Slavic BGN tables stand + * apart from the general Cyrillic one, since a Russian lyric reads better with the BGN + * letters. + */ + private static final String[][] TRANSLITERATIONS = { + {"Greek-Latin", "greek"}, + {"Cyrillic-Latin", "cyrillic"}, + {"Russian-Latin-BGN", "russian"}, + {"Ukrainian-Latin-BGN", "ukrainian"}, + {"Belarusian-Latin-BGN", "belarusian"}, + {"Bulgarian-Latin-BGN", "bulgarian"}, + {"Macedonian-Latin-BGN", "macedonian"}, + {"Serbian-Latin-BGN", "serbian"}, + {"Georgian-Latin", "georgian"}, + {"Armenian-Latin-BGN", "armenian"}, + {"Hebrew-Latin", "hebrew"}, + {"Arabic-Latin", "arabic"}, + {"Amharic-Latin-BGN", "amharic"}, + {"Syriac-Latin", "syriac"}, + }; + private static final String TABLE_SUFFIX = "_transforms.txt"; + + /** + * A reading the CLDR standard writes one way and a lyric wants another. The key is the + * letter or pair in the original script; the value wins over the downloaded rule. + */ + private static final String[][] OVERRIDES = { + // Modern Greek is read as it is said: CLDR follows the ancient spelling. + {"greek", "β", "v", "η", "i", "φ", "f", + "ου", "u", "αι", "ai", "ει", "i", "αυ", "av", "ευ", "ev", + "ηυ", "iv", "ωυ", "o", "μπ", "mp", "ντ", "nd", "γκ", "gk", + "γχ", "ch", "τσ", "ts", "τζ", "tz"}, + // Slavic letters read the commonest way: CLDR follows the transliteration standard. + {"cyrillic", "ё", "yo", "й", "y", "ї", "yi", "ў", "w", "ќ", "kj", "ѓ", "gj", + "ѐ", "yo", "ѝ", "i", "ж", "zh", "х", "kh", "ц", "ts", "ч", "ch", + "ш", "sh", "щ", "shch", "ю", "yu", "я", "ya", "є", "ye", "љ", "lj", + "њ", "nj", "џ", "dz", "ђ", "dj", "ѕ", "dz"}, + // Georgian differs from CLDR only in the ejective apostrophe, which is not sounded. + {"georgian", "კ", "k", "პ", "p", "ტ", "t", "ყ", "q", "წ", "ts", "ჭ", "ch", + "ჳ", "ui"}, + // Armenian BGN aspirates with an apostrophe, which a lyric does not sound. + {"armenian", "թ", "t", "չ", "ch", "ռ", "r", "ց", "ts", "փ", "p", "ք", "k", + "ւ", "v", "և", "ev"}, + }; + + private static final ExecutorService WORKER = Executors.newSingleThreadExecutor(runnable -> { + Thread thread = new Thread(runnable, "lyrics-cldr-transforms"); + thread.setDaemon(true); + return thread; + }); + + private static final AtomicBoolean RUNNING = new AtomicBoolean(); + + private static final RomanizationDict.Channel CHANNEL = new RomanizationDict.Channel(); + + private CldrTransformsStore() { + } + + /** Loads the tables on first use, since only the settings row downloads them. */ + public static boolean isReady() { + for (String[] transliteration : TRANSLITERATIONS) { + if (CldrTransforms.reader(transliteration[1]) == null) return false; + } + return true; + } + + /** + * Removes the tables and the licence notice. Called when the user turns the + * transliteration rules off. + */ + public static void delete() { + CldrTransforms.clear(); + File directory = RomanizationDict.directory(); + if (directory == null) return; + for (String[] transliteration : TRANSLITERATIONS) { + //noinspection ResultOfMethodCallIgnored + new File(directory, tableName(transliteration[1])).delete(); + } + //noinspection ResultOfMethodCallIgnored + new File(directory, NOTICE_NAME).delete(); + //noinspection ResultOfMethodCallIgnored + new File(directory, ARCHIVE).delete(); + //noinspection ResultOfMethodCallIgnored + new File(directory, ARCHIVE + ".part").delete(); + } + + /** The licence notice, read back from the cache where the build put it. */ + @Nullable + public static String notice() { + File directory = RomanizationDict.directory(); + return directory == null ? null : RomanizationDict.readNotice(directory, NOTICE_NAME); + } + + /** Starts a fetch if the tables are not loaded. Safe to call whenever the setting is on. */ + public static void ensure(RomanizationDict.Listener listener) { + if (isReady()) { + listener.onReady(entries()); + return; + } + CHANNEL.attach(listener, RUNNING.get()); + if (!RUNNING.compareAndSet(false, true)) return; + + WORKER.execute(() -> { + try { + build(CHANNEL); + } catch (Exception ex) { + Logger.printDebug(() -> "CLDR transforms build failed", ex); + CHANNEL.onFailed(); + } finally { + RUNNING.set(false); + CHANNEL.detach(); + } + }); + } + + private static int entries() { + int total = 0; + for (String[] transliteration : TRANSLITERATIONS) { + AlphabetRomanizer reader = CldrTransforms.reader(transliteration[1]); + if (reader != null) total += reader.size(); + } + return total; + } + + private static void build(RomanizationDict.Listener listener) throws IOException { + File directory = RomanizationDict.directory(); + if (directory == null) throw new IOException("no cache directory"); + + File archive = new File(directory, ARCHIVE); + if (!archive.isFile()) { + RomanizationDict.download(URL, archive, ARCHIVE, listener); + } + extract(archive, listener); + //noinspection ResultOfMethodCallIgnored + archive.delete(); + + RomanizationDict.writeNotice(directory, NOTICE_NAME, noticeText()); + listener.onReady(entries()); + } + + /** + * Reads the literal rules out of the archive and writes one table per script, each a line of + * {@code "source\ttarget"} per rule. + */ + private static void extract(File archive, RomanizationDict.Listener listener) + throws IOException { + File directory = RomanizationDict.directory(); + if (directory == null) throw new IOException("no cache directory"); + + int done = 0; + try (GZIPInputStream gzip = new GZIPInputStream( + new BufferedInputStream(new FileInputStream(archive), 1 << 16)); + ByteArrayOutputStream tar = new ByteArrayOutputStream()) { + byte[] buffer = new byte[1 << 16]; + int read; + while ((read = gzip.read(buffer)) != -1) { + tar.write(buffer, 0, read); + } + byte[] tarBytes = tar.toByteArray(); + + for (String[] transliteration : TRANSLITERATIONS) { + String entry = "package/transforms/" + transliteration[0] + ".txt"; + byte[] content = TarReader.entry(tarBytes, entry); + if (content == null) { + throw new IOException("no " + entry + " in the CLDR archive"); + } + writeTable(directory, transliteration[1], content, listener); + listener.onBuilding(50 + done++ * 50 / TRANSLITERATIONS.length); + } + } + listener.onBuilding(100); + } + + /** Writes one script's literal rules, with the overrides folded in. */ + private static void writeTable(File directory, String script, byte[] content, + RomanizationDict.Listener listener) throws IOException { + Map rules = parseRules(content, script); + + File built = new File(directory, tableName(script) + ".building"); + try (Writer out = new BufferedWriter(new OutputStreamWriter( + new FileOutputStream(built), StandardCharsets.UTF_8), 1 << 16)) { + for (Map.Entry rule : rules.entrySet()) { + out.write(rule.getKey()); + out.write('\t'); + out.write(rule.getValue()); + out.write('\n'); + } + } + RomanizationDict.commit(built, new File(directory, tableName(script))); + } + + /** + * Reads one transform file's literal rules as {@code letter -> reading}: a left side of + * one or two letters and a right side with no set, context or pipe is all the + * {@link AlphabetRomanizer} can use. + */ + private static Map parseRules(byte[] content, String script) + throws IOException { + Map rules = new HashMap<>(); + Map variables = new HashMap<>(); + + try (BufferedReader reader = new BufferedReader(new InputStreamReader( + new ByteArrayInputStream(content), StandardCharsets.UTF_8), 1 << 16)) { + String line; + while ((line = reader.readLine()) != null) { + String trimmed = line.trim(); + if (trimmed.isEmpty() || trimmed.charAt(0) == '#' + || trimmed.startsWith("::")) { + continue; + } + + int variable = variableName(trimmed); + if (variable > 0) { + int end = trimmed.indexOf(';', variable); + if (end > 0) { + variables.put(trimmed.substring(1, variable).trim(), + trimmed.substring(variable + 1, end).trim()); + } + continue; + } + + int arrow = arrow(trimmed); + if (arrow < 0) continue; + String lhs = left(trimmed, arrow); + if (lhs == null || lhs.length() > 2) continue; + String rhs = expand(trimmed.substring(arrow + 1), variables); + if (rhs == null) continue; + rhs = stripMarks(rhs).trim(); + // A capital rule would never be wanted: the small letter's rule reads both. + // An empty reading spells the letter away: the alef and the hard and soft + // signs are read as nothing at all, since no syllable sounds them. + if (lhs.toLowerCase(Locale.ROOT).equals(lhs)) { + rules.put(lhs, rhs); + } + } + } + applyOverrides(rules, script); + return rules; + } + + /** The index of the {@code =} in a {@code $variable = value} line, or -1. */ + private static int variableName(String line) { + if (line.charAt(0) != '$') return -1; + for (int i = 1; i < line.length(); i++) { + char c = line.charAt(i); + if (c == '=') return i; + if (c == ' ') continue; + if (!Character.isLetterOrDigit(c) && c != '_') return -1; + } + return -1; + } + + /** + * The index of the arrow in a rule read left to right, or -1. Only the arrows that spell + * the source script on the left are wanted: a reverse rule (Latin on the left, {@code ←}) + * would otherwise put a reading on a letter the engine never looks up. + */ + private static int arrow(String line) { + for (int i = 0; i < line.length() - 1; i++) { + char c = line.charAt(i); + if ((c == '↔' || c == '→') && i > 0 + && line.charAt(i - 1) != '\\') { + return i; + } + } + return -1; + } + + /** + * The left side of a rule: a quoted character, or the bare characters up to the arrow. + * Null when the side is contextual, writes a set, or is not letters: the engine reads + * letters, not the punctuation the standard transliterates as a courtesy. + */ + @Nullable + private static String left(String line, int arrow) { + String side = line.substring(0, arrow).trim(); + if (side.startsWith("'")) { + int close = side.indexOf('\'', 1); + if (close < 0) return null; + side = side.substring(1, close); + } else { + if (side.indexOf('{') >= 0 || side.indexOf('[') >= 0) return null; + if (side.length() > 2) return null; + // A bare escaped character such as \; is punctuation, not a reading. + if (side.length() == 1 && side.charAt(0) == '\\') return null; + } + for (int i = 0; i < side.length(); i++) { + if (!Character.isLetter(side.charAt(i))) return null; + } + return side.isEmpty() ? null : side; + } + + /** + * The right side with its {@code $variables} expanded, or null when it holds a set, a + * context or a pipe. + */ + @Nullable + private static String expand(String side, Map variables) { + int end = side.indexOf(';'); + if (end >= 0) side = side.substring(0, end); + StringBuilder out = new StringBuilder(side.length()); + for (int i = 0; i < side.length(); i++) { + char c = side.charAt(i); + if (c == '{' || c == '[' || c == '|' || c == '}') return null; + if (c == '$') { + int nameEnd = i + 1; + while (nameEnd < side.length() + && (Character.isLetterOrDigit(side.charAt(nameEnd)) + || side.charAt(nameEnd) == '_')) { + nameEnd++; + } + String name = side.substring(i + 1, nameEnd); + String value = variables.get(name); + if (value == null) return null; + String expanded = unescape(value); + // A variable's value can carry a set or context of its own. + for (int j = 0; j < expanded.length(); j++) { + char ec = expanded.charAt(j); + if (ec == '{' || ec == '[' || ec == '|' || ec == '}') return null; + } + out.append(expanded); + i = nameEnd - 1; + continue; + } + if (c == '\\') { + // A \\uXXXX escape names a character the file cannot hold as itself. + if (i + 5 < side.length() && side.charAt(i + 1) == 'u') { + out.append((char) Integer.parseInt( + side.substring(i + 2, i + 6), 16)); + i += 5; + continue; + } + // Any other backslash is the CLDR escape for punctuation such as \: + // which the reading does not want. + i++; + continue; + } + if (c != '\'') out.append(c); + } + return out.toString().trim(); + } + + /** A variable value with its {@code \\uXXXX} escapes decoded, or the value whole. */ + private static String unescape(String value) { + if (value.indexOf("\\u") < 0) return value; + StringBuilder out = new StringBuilder(value.length()); + for (int i = 0; i < value.length(); i++) { + char c = value.charAt(i); + if (c == '\\' && i + 5 < value.length() && value.charAt(i + 1) == 'u') { + out.append((char) Integer.parseInt(value.substring(i + 2, i + 6), 16)); + i += 5; + continue; + } + out.append(c); + } + return out.toString(); + } + + /** + * Keeps only the letters of a reading. The combining marks and modifier letters CLDR + * writes to keep the reading reversible are not sounds a lyric sings, and a space is + * not kept either: the engine spaces the syllables itself. + */ + private static String stripMarks(String value) { + StringBuilder out = new StringBuilder(value.length()); + for (int i = 0; i < value.length(); i++) { + char c = value.charAt(i); + if (Character.isLetter(c) + && Character.getType(c) != Character.MODIFIER_LETTER) { + out.append(c); + } + } + return out.toString(); + } + + /** The modern readings that win over the downloaded rules. */ + private static void applyOverrides(Map rules, String script) { + for (String[] overrides : OVERRIDES) { + if (!script.equals(overrides[0])) continue; + for (int i = 1; i < overrides.length; i += 2) { + rules.put(overrides[i], overrides[i + 1]); + } + } + } + + private static String tableName(String script) { + return script + TABLE_SUFFIX; + } + + private static String noticeText() { + return "CLDR transliteration rules\n" + + URL + "\n\n" + + "From https://www.unicode.org/cldr/\n\n" + + "Licence: Unicode Licence V3 (data files and software)\n" + + "https://www.unicode.org/license.txt\n\n" + + "The CLDR data files are provided \"as is\", without warranty of any kind,\n" + + "express or implied, including but not limited to the warranties of\n" + + "merchantability, fitness for a particular purpose and noninfringement.\n\n" + + "The rules here are the literal letter-to-letter rules of the listed CLDR\n" + + "transform files, with the combining marks of the standard dropped and a few\n" + + "readings a lyric wants replacing the standard's where they differ.\n"; + } + + /** + * Reads one entry out of a tar archive held in memory, without the tar library Android + * does not ship. Only the uStar format CLDR's package is written in is read, and only + * regular files, which is all the archive holds. + */ + private static final class TarReader { + + private static final int BLOCK = 512; + private static final int NAME = 0; + private static final int NAME_LENGTH = 100; + private static final int SIZE = 124; + private static final int SIZE_LENGTH = 12; + private static final int PREFIX = 345; + private static final int PREFIX_LENGTH = 155; + + private TarReader() { + } + + @Nullable + static byte[] entry(byte[] tar, String wanted) throws IOException { + int offset = 0; + while (offset + BLOCK <= tar.length) { + String name = name(tar, offset); + if (name.isEmpty()) break; + String full = name(tar, offset, PREFIX, PREFIX_LENGTH); + if (!full.isEmpty()) { + name = full + '/' + name; + } + long size = size(tar, offset); + if (name.equals(wanted)) { + if (size > tar.length - offset - BLOCK) { + throw new IOException("truncated tar entry " + wanted); + } + byte[] content = new byte[(int) size]; + System.arraycopy(tar, offset + BLOCK, content, 0, content.length); + return content; + } + long total = BLOCK + ((size + BLOCK - 1) / BLOCK) * BLOCK; + offset += (int) total; + } + return null; + } + + private static String name(byte[] tar, int offset, int field, int length) + throws IOException { + int end = offset + field + length; + if (end > tar.length) throw new IOException("truncated tar header"); + int stop = offset + field; + while (stop < end && tar[stop] != 0) { + stop++; + } + return new String(tar, offset + field, stop - offset - field, + StandardCharsets.UTF_8); + } + + private static String name(byte[] tar, int offset) throws IOException { + return name(tar, offset, NAME, NAME_LENGTH); + } + + private static long size(byte[] tar, int offset) throws IOException { + String octal = name(tar, offset, SIZE, SIZE_LENGTH); + long size = 0; + for (int i = 0; i < octal.length(); i++) { + char c = octal.charAt(i); + if (c == ' ' || c == 0) break; + size = size * 8 + (c - '0'); + } + return size; + } + } +} diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/CyrillicRomanizer.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/CyrillicRomanizer.java deleted file mode 100644 index 3b5b85f809..0000000000 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/CyrillicRomanizer.java +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright 2026 Morphe. - * https://github.com/MorpheApp/morphe-patches/pull/3851 - * - * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. - */ - -package app.morphe.extension.music.patches.lyrics.romanizers; - -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; - -/** - * Cyrillic is written the way it sounds, so this is rules, not a dictionary. A lyric says nothing of - * the language behind it, so letters the Slavic languages disagree on are read the commonest way: a - * spelling changes, never a letter count. - */ -public final class CyrillicRomanizer { - - private static final Map LETTERS = new HashMap<>(); - - static { - LETTERS.put("а", "a"); - LETTERS.put("б", "b"); - LETTERS.put("в", "v"); - LETTERS.put("г", "g"); - LETTERS.put("д", "d"); - LETTERS.put("е", "e"); - LETTERS.put("ё", "yo"); - LETTERS.put("ж", "zh"); - LETTERS.put("з", "z"); - LETTERS.put("и", "i"); - LETTERS.put("й", "y"); - LETTERS.put("к", "k"); - LETTERS.put("л", "l"); - LETTERS.put("м", "m"); - LETTERS.put("н", "n"); - LETTERS.put("о", "o"); - LETTERS.put("п", "p"); - LETTERS.put("р", "r"); - LETTERS.put("с", "s"); - LETTERS.put("т", "t"); - LETTERS.put("у", "u"); - LETTERS.put("ф", "f"); - LETTERS.put("х", "kh"); - LETTERS.put("ц", "ts"); - LETTERS.put("ч", "ch"); - LETTERS.put("ш", "sh"); - LETTERS.put("щ", "shch"); - // The two signs break a cluster apart and are said on their own nowhere. - LETTERS.put("ъ", ""); - LETTERS.put("ы", "y"); - LETTERS.put("ь", ""); - LETTERS.put("э", "e"); - LETTERS.put("ю", "yu"); - LETTERS.put("я", "ya"); - - LETTERS.put("і", "i"); - LETTERS.put("ї", "yi"); - LETTERS.put("є", "ye"); - LETTERS.put("ґ", "g"); - LETTERS.put("ў", "w"); - LETTERS.put("ј", "j"); - LETTERS.put("љ", "lj"); - LETTERS.put("њ", "nj"); - LETTERS.put("ћ", "c"); - LETTERS.put("ќ", "kj"); - LETTERS.put("џ", "dz"); - LETTERS.put("ђ", "dj"); - LETTERS.put("ѓ", "gj"); - LETTERS.put("ѕ", "dz"); - LETTERS.put("ѐ", "yo"); - LETTERS.put("ѝ", "i"); - - // Letters the Slavs write alone and the Turkic tongues borrow, so none comes back untranslated. - LETTERS.put("ѡ", "o"); - LETTERS.put("ѣ", "ye"); - LETTERS.put("ѥ", "ye"); - LETTERS.put("ѧ", "a"); - LETTERS.put("ѩ", "a"); - LETTERS.put("ѫ", "a"); - LETTERS.put("ѭ", "a"); - LETTERS.put("ѯ", "ae"); - LETTERS.put("ѱ", "ps"); - LETTERS.put("ѳ", "f"); - LETTERS.put("ѵ", "y"); - LETTERS.put("ѷ", "i"); - LETTERS.put("ѹ", "u"); - LETTERS.put("ѻ", "o"); - LETTERS.put("ѽ", "oe"); - LETTERS.put("ѿ", "o"); - LETTERS.put("ғ", "g"); - LETTERS.put("қ", "q"); - LETTERS.put("ң", "ng"); - LETTERS.put("ү", "u"); - LETTERS.put("ұ", "u"); - LETTERS.put("һ", "h"); - LETTERS.put("ҷ", "j"); - LETTERS.put("ә", "a"); - } - - private static final AlphabetRomanizer READER = new AlphabetRomanizer( - ScriptDetector::isCyrillic, LETTERS, Collections.emptyMap(), false); - - private CyrillicRomanizer() { - } - - public static String romanize(String line) { - return READER.romanize(line); - } -} diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/GeorgianRomanizer.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/GeorgianRomanizer.java deleted file mode 100644 index cff006ff2c..0000000000 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/GeorgianRomanizer.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright 2026 Morphe. - * https://github.com/MorpheApp/morphe-patches/pull/3851 - * - * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. - */ - -package app.morphe.extension.music.patches.lyrics.romanizers; - -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; - -/** - * Georgian writes every vowel in full, so one letter is one sound; the capitalised letters of the - * Georgian Extended block fold onto the lower-case ones. - */ -public final class GeorgianRomanizer { - - private static final Map LETTERS = new HashMap<>(); - - static { - LETTERS.put("ა", "a"); - LETTERS.put("ბ", "b"); - LETTERS.put("გ", "g"); - LETTERS.put("დ", "d"); - LETTERS.put("ე", "e"); - LETTERS.put("ვ", "v"); - LETTERS.put("ზ", "z"); - LETTERS.put("თ", "t"); - LETTERS.put("ი", "i"); - LETTERS.put("კ", "k"); - LETTERS.put("ლ", "l"); - LETTERS.put("მ", "m"); - LETTERS.put("ნ", "n"); - LETTERS.put("ო", "o"); - LETTERS.put("პ", "p"); - LETTERS.put("ჟ", "zh"); - LETTERS.put("რ", "r"); - LETTERS.put("ს", "s"); - LETTERS.put("ტ", "t"); - LETTERS.put("უ", "u"); - LETTERS.put("ფ", "p"); - LETTERS.put("ქ", "k"); - LETTERS.put("ღ", "gh"); - LETTERS.put("ყ", "q"); - LETTERS.put("შ", "sh"); - LETTERS.put("ჩ", "ch"); - LETTERS.put("ც", "ts"); - LETTERS.put("ძ", "dz"); - LETTERS.put("წ", "ts"); - LETTERS.put("ხ", "kh"); - LETTERS.put("ჯ", "j"); - LETTERS.put("ჰ", "h"); - } - - private static final AlphabetRomanizer READER = new AlphabetRomanizer( - ScriptDetector::isGeorgian, LETTERS, Collections.emptyMap(), false); - - private GeorgianRomanizer() { - } - - public static String romanize(String line) { - return READER.romanize(line); - } -} diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/GreekRomanizer.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/GreekRomanizer.java deleted file mode 100644 index 3c79947778..0000000000 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/GreekRomanizer.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright 2026 Morphe. - * https://github.com/MorpheApp/morphe-patches/pull/3851 - * - * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. - */ - -package app.morphe.extension.music.patches.lyrics.romanizers; - -import java.util.HashMap; -import java.util.Map; - -/** - * Modern Greek is written the way it sounds, so this is a set of rules and not a dictionary. - * Accents come off first, since they mark stress and carry no sound, and readings come out one - * word at a time, because cutting syllables would need to know where a word begins. - */ -public final class GreekRomanizer { - - private static final Map LETTERS = new HashMap<>(); - private static final Map DIGRAPHS = new HashMap<>(); - - static { - LETTERS.put("α", "a"); - LETTERS.put("β", "v"); - LETTERS.put("γ", "g"); - LETTERS.put("δ", "d"); - LETTERS.put("ε", "e"); - LETTERS.put("ζ", "z"); - LETTERS.put("η", "i"); - LETTERS.put("θ", "th"); - LETTERS.put("ι", "i"); - LETTERS.put("κ", "k"); - LETTERS.put("λ", "l"); - LETTERS.put("μ", "m"); - LETTERS.put("ν", "n"); - LETTERS.put("ξ", "x"); - LETTERS.put("ο", "o"); - LETTERS.put("π", "p"); - LETTERS.put("ρ", "r"); - LETTERS.put("σ", "s"); - LETTERS.put("ς", "s"); - LETTERS.put("τ", "t"); - LETTERS.put("υ", "y"); - LETTERS.put("φ", "f"); - LETTERS.put("χ", "ch"); - LETTERS.put("ψ", "ps"); - LETTERS.put("ω", "o"); - - // A vowel pair writes one vowel between them, which is why it cannot be read letter by letter. - DIGRAPHS.put("ου", "u"); - DIGRAPHS.put("αι", "ai"); - DIGRAPHS.put("ει", "i"); - DIGRAPHS.put("αυ", "av"); - DIGRAPHS.put("ευ", "ev"); - DIGRAPHS.put("ηυ", "iv"); - DIGRAPHS.put("ωυ", "o"); - DIGRAPHS.put("μπ", "mp"); - DIGRAPHS.put("ντ", "nd"); - DIGRAPHS.put("γκ", "gk"); - DIGRAPHS.put("γχ", "ch"); - DIGRAPHS.put("τσ", "ts"); - DIGRAPHS.put("τζ", "tz"); - } - - private static final AlphabetRomanizer READER = new AlphabetRomanizer( - ScriptDetector::isGreek, LETTERS, DIGRAPHS, true); - - private GreekRomanizer() { - } - - public static String romanize(String line) { - return READER.romanize(line); - } -} diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/HanRomanizer.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/HanRomanizer.java index 2ec76f4489..11e5bdad8b 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/HanRomanizer.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/HanRomanizer.java @@ -16,14 +16,6 @@ */ final class HanRomanizer { - interface Readings { - @Nullable - String word(String word); - - @Nullable - String character(char c); - } - private HanRomanizer() { } @@ -75,7 +67,7 @@ static String romanize(String line, Readings readings) { * @return how many characters the word took, 0 when none is known. */ private static int appendLongestWord(String line, int index, Readings readings, - StringBuilder out) { + StringBuilder out) { int longest = Math.min(WordReadingTable.MAX_WORD_LENGTH, line.length() - index); for (int length = longest; length >= 1; length--) { String reading = readings.word(line.substring(index, index + length)); @@ -97,4 +89,12 @@ private static void append(StringBuilder out, @Nullable String reading) { if (out.length() > 0) out.append(' '); out.append(reading); } + + interface Readings { + @Nullable + String word(String word); + + @Nullable + String character(char c); + } } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/HangulRomanizer.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/HangulRomanizer.java index fdbb72752d..3e6100a152 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/HangulRomanizer.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/HangulRomanizer.java @@ -8,18 +8,10 @@ package app.morphe.extension.music.patches.lyrics.romanizers; /** - * Revised Romanization for Korean, written the way Google writes it, so that a line does not - * change spelling between the online path and this one. Two conventions follow Google rather - * than the textbook: a coda before a silent ㅇ is written out with a hyphen, as in 시간이 - * {@code sigan-i}, and ㄹ always reads as {@code l}. - * - *

    The tables were read off the Unicode names of the jamo rather than typed, since the - * nineteen initial slots start at ㄱ with the silent ㅇ at slot eleven and the coda slot the - * formula produces is one higher than the jamo index it stands for. - * - *

    Left out, because they need a morphological analysis rather than a romanization: tensing, - * as in 빛나는 {@code bichnaneun}, and the extra {@code h} Google writes into some joins, as - * in 않은. + * Revised Romanization for Korean, as Google writes it so both paths spell a line the same: + * a coda before a silent ㅇ is written out with a hyphen (시간이 {@code sigan-i}), ㄹ always reads + * as {@code l}, and the tables come from the Unicode jamo names. Tensing and Google's extra + * {@code h} joins are left out: they need morphological analysis, not a romanization. */ public final class HangulRomanizer { @@ -61,8 +53,19 @@ private HangulRomanizer() { } public static String romanize(String text) { + return romanize(text, text); + } + + /** + * The reading of {@code text}, which must be the opening of {@code line}. + * + *

    A coda depends on the syllable after it, so a single syllable read alone would end where + * the line does not. A line fed to itself reads exactly as {@link #romanize(String)}. + */ + public static String romanize(String text, String line) { StringBuilder out = new StringBuilder(text.length() * 2); int[] syllable = new int[3]; + final boolean alone = text.length() == 1; for (int i = 0; i < text.length(); i++) { char c = text.charAt(i); @@ -76,7 +79,14 @@ public static String romanize(String text) { if (coda == NO_CODA) continue; // A coda before a silent ㅇ is written out, and the hyphen marks the join. - char next = i + 1 < text.length() ? text.charAt(i + 1) : 0; + final char next; + if (i + 1 < text.length()) { + next = text.charAt(i + 1); + } else if (alone && line.length() > text.length()) { + next = line.charAt(text.length()); + } else { + next = 0; + } if (startsOnSilent(next)) { out.append(LATERAL[coda]).append('-'); } else { diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/JapaneseDictStore.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/JapaneseDictStore.java index fdc5ebc72d..5fd7b544a0 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/JapaneseDictStore.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/JapaneseDictStore.java @@ -47,7 +47,7 @@ public final class JapaneseDictStore { */ private static final WordTableFile TABLE = new WordTableFile( "japanese_readings_" + VERSION + ".bin", - new byte[] {'M', 'J', 'P', 'W', '0', '0', '0', '2'}, 1 << 23); + new byte[]{'M', 'J', 'P', 'W', '0', '0', '0', '2'}, 1 << 23); private static final String NOTICE_NAME = "japanese_readings.NOTICE.txt"; /** How many entries go in before the row is worth moving. */ @@ -70,6 +70,21 @@ public static boolean isReady() { return table() != null; } + /** Removes the table and its licence notice. Called when the user turns Japanese off. */ + public static void delete() { + TABLE.clear(); + File file = TABLE.file(); + if (file != null && file.exists()) { + //noinspection ResultOfMethodCallIgnored + file.delete(); + } + File directory = RomanizationDict.directory(); + if (directory != null) { + //noinspection ResultOfMethodCallIgnored + new File(directory, NOTICE_NAME).delete(); + } + } + /** Starts a build if the table is missing. Safe to call whenever the setting is on. */ public static void ensure(RomanizationDict.Listener listener) { if (isReady()) { @@ -135,7 +150,7 @@ private static void build(RomanizationDict.Listener listener) throws IOException } private static void addSource(WordReadingTable.Builder builder, File archive, - RomanizationDict.Listener listener) throws IOException { + RomanizationDict.Listener listener) throws IOException { int seen = 0; try (ZipInputStream zip = new ZipInputStream( @@ -184,8 +199,10 @@ private static int indexOf(String[] header, String name) { return -1; } - /** A field may be quoted and hold commas, so a plain split would shift every - * column after it. */ + /** + * A field may be quoted and hold commas, so a plain split would shift every + * column after it. + */ private static String[] split(String line) { if (line == null) return new String[0]; @@ -237,8 +254,10 @@ private static String displayName(String fileName) { return fileName.replace("_lex", "").replace('_', ' '); } - /** Downloaded rather than shipped, so the licence travels with the data into the - * cache and is shown from the About row. */ + /** + * Downloaded rather than shipped, so the licence travels with the data into the + * cache and is shown from the About row. + */ private static String noticeText() { return "SudachiDict reading data\n" + "https://github.com/WorksApplications/SudachiDict\n\n" diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/KanaRomanizer.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/KanaRomanizer.java index 63929ba518..361376a568 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/KanaRomanizer.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/KanaRomanizer.java @@ -14,15 +14,10 @@ import java.util.Map; /** - * Hepburn romaji for a run of kana, which is all a phonetic script needs: the readings a - * dictionary hands back are katakana and come through here too. The work is done a mora at a - * time, since a character alone says neither whether a small kana belongs to it nor whether it - * palatalises. - * - *

    The table is read out of the Unicode names, since a kana is named after its own reading - * and a hand-written table is silently corruptible. Two conventions here are Google's rather - * than Hepburn's, so that a song does not change shape between the online path and this one: - * no apostrophe before a vowel, and n before a labial written as m. + * Hepburn romaji for a run of kana. Work is done a mora at a time: a character alone says + * neither whether a small kana belongs to it nor whether it palatalises. Tables come from the + * Unicode names (hand-written tables are silently corruptible); two Google conventions are kept, + * so both paths spell a song the same: no apostrophe before a vowel, n before a labial as m. */ public final class KanaRomanizer { diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/LocalRomanizer.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/LocalRomanizer.java index 50b2d9d12b..d6219a199a 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/LocalRomanizer.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/LocalRomanizer.java @@ -13,6 +13,7 @@ import java.util.Arrays; import java.util.HashSet; import java.util.List; +import java.util.Locale; import java.util.Set; import app.morphe.extension.music.patches.lyrics.model.LyricsLine; @@ -23,43 +24,22 @@ /** * Romanizes lyrics on the device, for the lines no service could romanize. * - *

    This runs last, after the provider, the AI endpoint and Google have all come back empty, - * so what it produces is close to, but not the same as, what a service produces. A line in a - * script nothing here covers is passed through rather than guessed at. + *

    Runs after the provider, the AI endpoint and Google have all come back empty. A line in + * a script nothing here covers is passed through untouched. * - *

    A word the dictionary does not have is left in the script it was written in, since - * dropping the line would lose the words around it. + *

    A word the dictionary does not have stays in the script it was written in; dropping + * the line would lose the words around it. */ public final class LocalRomanizer { - public static final class Result { - /** One line per input line. */ - public final List lines; - - /** The name shown under the source. */ - @Nullable - public final String dictionary; - - Result(List lines, @Nullable String dictionary) { - this.lines = lines; - this.dictionary = dictionary; - } - } - - private record Read(String text, @Nullable String dictionary) { - } - private static final String PINYIN = "UniDict, pinyin-pro"; - private static final String JYUTPING = "UniDict, Rime Cantonese"; private static final String RULE = "Built-in Romanization"; private static final String JAPANESE = "SudachiDict"; - /** No syllables at all, and a word said with none of them. */ private static final int[][] NO_TOKENS = new int[0][]; private static final int[] NO_SYLLABLES = {0, 0}; - - /** Words ending in the er character where it is a word of its own rather than the erhua suffix. */ + /** Words ending in er where er is a word of its own, not the erhua suffix. */ private static final Set PLAIN_ERHUA = new HashSet<>(Arrays.asList( "女儿", "男儿", "婴儿", "幼儿", "孤儿", "少儿", "胎儿", "健儿", "残儿", "弃儿", "新生儿", "低能儿", "弱智儿", "托儿", "孕儿")); @@ -70,16 +50,44 @@ private LocalRomanizer() { /** Null when nothing here applies, which tells the caller to leave the lyrics alone. */ @Nullable public static Result romanize(List lines) { + return romanize(lines, false); + } + + /** + * Same reading, but the Han characters come from the Rime word table of the chosen + * language instead of the Mandarin one. This is the Rime Wordlist provider's reading; + * the local dictionaries still cover the scripts around the Han. + */ + @Nullable + public static Result romanizeRime(List lines) { + return romanize(lines, true); + } + + /** + * The reading of one syllable, or null when it is not read here. + * + * @param line The line the syllable opens, so a Korean coda reads the way the line reads it. + */ + @Nullable + public static String syllableReading(String syllable, @Nullable String line) { + if (syllable == null || syllable.isEmpty()) return null; + final Script script = ScriptDetector.detect(syllable); + if (script == null || script == Script.OTHER) return null; + final String reading = readAs(syllable, script, false, line).text().trim(); + return reading.isEmpty() || reading.equals(syllable) ? null : reading; + } + + @Nullable + private static Result romanize(List lines, boolean rime) { if (lines == null || lines.isEmpty()) return null; if (ScriptDetector.detect(lines) == Script.OTHER) return null; - final boolean cantonese = cantonese(); boolean changed = false; String dictionary = null; List result = new ArrayList<>(lines.size()); for (String line : lines) { - Read read = readAs(line, ScriptDetector.detect(line), cantonese); + final Read read = readAs(line, ScriptDetector.detect(line), rime); if (read.dictionary() != null) dictionary = read.dictionary(); if (!read.text().equals(line)) changed = true; result.add(read.text()); @@ -89,24 +97,26 @@ public static Result romanize(List lines) { } /** - * Only the reading is added, and only to the lines the source had split: a line it did not + * Adds only the reading, and only to the lines the source split. A line the source did not * split keeps the whole-line reading passed in. + * + *

    Han is read by the word table when the language has one, which is the Rime Wordlist reading. */ @Nullable public static List alignWords(List lines, - List wholeLine) { + List wholeLine) { if (lines == null || wholeLine == null || lines.size() != wholeLine.size()) return null; - final boolean cantonese = cantonese(); List result = new ArrayList<>(lines.size()); boolean aligned = false; for (int i = 0; i < lines.size(); i++) { - LyricsLine line = lines.get(i); + final LyricsLine line = lines.get(i); // Kana is what marks a line Japanese; a word of kanji alone says only Han, and would // then be looked up in the wrong dictionary. - Script script = ScriptDetector.detect(line.text()); - List read = exactReadings(line.words(), script, cantonese); + final Script script = ScriptDetector.detect(line.text()); + List read = exactReadings(line.words(), script, RimeDictStore.isActive(), + line.text()); if (read == null) { read = splitReading(line, wholeLine.get(i).text()); } @@ -127,13 +137,20 @@ public static List alignWords(List lines, /** Null when not one word could be read, leaving the line to {@link #splitReading}. */ @Nullable private static List exactReadings(List words, @Nullable Script script, - boolean cantonese) { + boolean rime) { + return exactReadings(words, script, rime, null); + } + + /** @param line The line the words sit in, or null when only the words are known. */ + @Nullable + private static List exactReadings(List words, @Nullable Script script, + boolean rime, @Nullable String line) { if (words.isEmpty() || script == null) return null; - List read = new ArrayList<>(words.size()); + final List read = new ArrayList<>(words.size()); boolean any = false; for (Word word : words) { - String reading = readAs(word.text(), script, cantonese).text(); + final String reading = readAs(word.text(), script, rime, line).text(); if (reading.equals(word.text())) { read.add(word); continue; @@ -147,33 +164,33 @@ private static List exactReadings(List words, @Nullable Script scrip } /** - * A whole reading laid onto the words the source split the line into. Only two shapes place without - * guessing: one syllable per spoken character, or one per word; anything else keeps the whole line, - * a reading on the wrong word being worse than none. + * A whole reading laid onto the words the source split the line into. Only two shapes + * place without guessing: one syllable per spoken character, or one per word. Anything + * else keeps the whole line; a reading on the wrong word is worse than none. */ @Nullable private static List splitReading(LyricsLine line, @Nullable String wholeLine) { - List words = line.words(); + final List words = line.words(); if (wholeLine == null || words.isEmpty()) return null; - String original = line.text(); - String reading = wholeLine.trim(); + final String original = line.text(); + final String reading = wholeLine.trim(); if (reading.isEmpty() || original == null || reading.equalsIgnoreCase(original.trim())) { return null; } - int[][] tokens = readingTokens(wholeLine); - int[][] spread = spreadReading(words, wholeLine); + final int[][] tokens = readingTokens(wholeLine); + final int[][] spread = spreadReading(words, wholeLine); if (tokens.length == 0 || spread == null) return null; - List read = new ArrayList<>(words.size()); + final List read = new ArrayList<>(words.size()); for (int i = 0; i < words.size(); i++) { - Word word = words.get(i); - int[] range = spread[i]; + final Word word = words.get(i); + final int[] range = spread[i]; if (range[1] <= range[0]) { read.add(word); continue; } - StringBuilder syllables = new StringBuilder(); + final StringBuilder syllables = new StringBuilder(); for (int at = range[0]; at < range[1]; at++) { if (syllables.length() > 0) syllables.append(' '); syllables.append(reading, tokens[at][0], tokens[at][1]); @@ -192,32 +209,32 @@ public static int[][] readingTokens(@Nullable String reading) { if (reading == null) { return NO_TOKENS; } - List tokens = new ArrayList<>(); + final List tokens = new ArrayList<>(); int at = 0; while (at < reading.length()) { while (at < reading.length() && Character.isWhitespace(reading.charAt(at))) at++; final int start = at; while (at < reading.length() && !Character.isWhitespace(reading.charAt(at))) at++; if (at > start && speaks(reading.substring(start, at))) { - tokens.add(new int[] {start, at}); + tokens.add(new int[]{start, at}); } } return tokens.isEmpty() ? NO_TOKENS : tokens.toArray(new int[0][]); } /** - * Which of those syllables each word is said with, as {@code [from, to)} token indices and an empty - * pair for a word of marks only. Null when the reading fits the words neither one syllable per - * spoken character nor one per word. + * Which of those syllables each word is said with, as {@code [from, to)} token indices and an + * empty pair for a word of marks only. Null when the reading fits the words neither one + * syllable per spoken character nor one per word. * *

    A caller holding only timings gets the same placement by handing over one word per timing. */ @Nullable public static int[][] spreadReading(List words, @Nullable String reading) { - int[][] tokens = readingTokens(reading); + final int[][] tokens = readingTokens(reading); if (tokens.length == 0 || words.isEmpty()) return null; - int[] spoken = new int[words.size()]; + final int[] spoken = new int[words.size()]; int syllables = 0; int carried = 0; for (int i = 0; i < words.size(); i++) { @@ -229,7 +246,7 @@ public static int[][] spreadReading(List words, @Nullable String reading) final int length = tokens.length; if (length != syllables && length != carried) return null; - int[][] spread = new int[words.size()][]; + final int[][] spread = new int[words.size()][]; int token = 0; int placed = 0; for (int i = 0; i < words.size(); i++) { @@ -237,16 +254,16 @@ public static int[][] spreadReading(List words, @Nullable String reading) if (spoken[i] <= 0) continue; placed += spoken[i]; final int end = length == syllables ? placed : token + 1; - spread[i] = new int[] {token, end}; + spread[i] = new int[]{token, end}; token = end; } return spread; } /** - * How much of a whole reading one word is said with. Han, Hangul and kana are a syllable to the - * character, so their count is the characters; every other writing counts as the whole word, - * letters being no syllables there and reading them apart cutting a word into pieces. + * How much of a whole reading one word is said with. Han, Hangul and kana are a syllable + * per character; any other writing counts as one word, since splitting letters apart would + * cut a word into pieces. */ private static int units(String word) { int characters = 0; @@ -257,8 +274,9 @@ private static int units(String word) { if (isOnePerSyllable(c)) characters++; } if (characters == 0) return letters > 0 ? 1 : 0; - // The erhua suffix rides on the syllable before it, so such a word is said one fewer than written, - // unless the er is a word of its own: anything else beside it keeps its syllable, or the er has none. + // The erhua suffix rides on the syllable before it, so such a word is said one fewer + // than written, unless er is a word of its own: anything else beside it keeps its + // syllable, or the er has none. if (characters > 1 && letters > 0 && word.endsWith("儿") && !PLAIN_ERHUA.contains(word)) { characters--; } @@ -274,10 +292,10 @@ private static boolean isOnePerSyllable(char c) { private static void attachPunctuation(List read) { for (int i = 0; i < read.size(); i++) { if (read.get(i).romaji() != null) continue; - String text = read.get(i).text().trim(); + final String text = read.get(i).text().trim(); if (text.isEmpty() || speaks(text)) continue; for (int at = i - 1; at >= 0; at--) { - Word previous = read.get(at); + final Word previous = read.get(at); // One that already gave its marks away has nothing left to carry more. if (previous.romaji() == null || previous.romaji().isEmpty()) continue; if (!previous.romaji().endsWith(text)) { @@ -299,12 +317,18 @@ private static boolean speaks(String text) { return false; } - private static Read readAs(String text, Script script, boolean cantonese) { + private static Read readAs(String text, Script script, boolean rime) { + return readAs(text, script, rime, null); + } + + /** @param line The line the text came from, or null when it is read on its own. */ + private static Read readAs(String text, Script script, boolean rime, @Nullable String line) { switch (script) { case HAN: - if (cantonese) { - return CantoneseDictStore.isReady() - ? new Read(JyutpingRomanizer.romanize(text), JYUTPING) + if (rime) { + return RimeDictStore.isReady() + ? new Read(RimeRomanizer.romanize(text), + "UniDict, " + RimeDictStore.dictionaryName()) : new Read(text, null); } return PinyinDictStore.isReady() @@ -314,23 +338,62 @@ private static Read readAs(String text, Script script, boolean cantonese) { // Kana read by rule are not the dictionary's work, so nothing is credited. return new Read(japanese(text), JapaneseDictStore.isReady() ? JAPANESE : null); case KOREAN: - return new Read(HangulRomanizer.romanize(text), RULE); + return new Read(HangulRomanizer.romanize(text, + line != null ? line : text), RULE); case GREEK: - return new Read(GreekRomanizer.romanize(text), RULE); + return new Read(transliterate(text, "greek"), RULE); case CYRILLIC: - return new Read(CyrillicRomanizer.romanize(text), RULE); + return new Read(transliterate(text, cyrillicScript()), RULE); case GEORGIAN: - return new Read(GeorgianRomanizer.romanize(text), RULE); + return new Read(transliterate(text, "georgian"), RULE); case ARMENIAN: - return new Read(ArmenianRomanizer.romanize(text), RULE); + return new Read(transliterate(text, "armenian"), RULE); + case HEBREW: + return new Read(transliterate(text, "hebrew"), RULE); + case ARABIC: + return new Read(transliterate(text, "arabic"), RULE); + case ETHIOPIC: + return new Read(transliterate(text, "amharic"), RULE); + case SYRIAC: + return new Read(transliterate(text, "syriac"), RULE); default: return new Read(text, null); } } - /** Cantonese rather than Chinese, which the Han text itself cannot say. */ - private static boolean cantonese() { - return "YUE".equalsIgnoreCase(Settings.LYRICS_ROMANIZE_LANGUAGE.get()); + /** + * The transliteration of a script read by rule, or the text itself while the CLDR rules + * it needs are still being fetched. + */ + private static String transliterate(String text, String script) { + AlphabetRomanizer reader = CldrTransforms.reader(script); + return reader != null ? reader.romanize(text) : text; + } + + /** + * The Cyrillic table that reads the language the user asked for: the BGN tables are per + * language, and the general table reads whatever remains. + */ + private static String cyrillicScript() { + final String language = Settings.LYRICS_ROMANIZE_LANGUAGE.get(); + if (language == null) return "cyrillic"; + final String subtag = language.toLowerCase(Locale.ROOT).split("-", 2)[0]; + switch (subtag) { + case "ru": + return "russian"; + case "uk": + return "ukrainian"; + case "be": + return "belarusian"; + case "bg": + return "bulgarian"; + case "mk": + return "macedonian"; + case "sr": + return "serbian"; + default: + return "cyrillic"; + } } /** Kana by rule, everything else out of the dictionary. */ @@ -387,10 +450,12 @@ private static String japanese(String line) { return out.toString().trim(); } - /** A run of kana, cut into words: the rule reads it either way, but without the words a - * line reads as {@code chottomatte} where Google gives {@code Chotto matte}. */ + /** + * A run of kana, cut into words: the rule reads either way, but without word breaks a + * line reads as {@code chottomatte} where Google gives {@code Chotto matte}. + */ private static void kana(String line, int from, int to, WordReadingTable table, - StringBuilder out) { + StringBuilder out) { int at = from; while (at < to) { int length = to - at; @@ -413,4 +478,21 @@ private static void append(StringBuilder out, String word) { if (out.length() > 0) out.append(' '); out.append(word); } + + public static final class Result { + /** One line per input line. */ + public final List lines; + + /** The name shown under the source. */ + @Nullable + public final String dictionary; + + Result(List lines, @Nullable String dictionary) { + this.lines = lines; + this.dictionary = dictionary; + } + } + + private record Read(String text, @Nullable String dictionary) { + } } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/PinyinDictStore.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/PinyinDictStore.java index 8a3a9da204..3e15fe803f 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/PinyinDictStore.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/PinyinDictStore.java @@ -11,46 +11,31 @@ import java.io.BufferedInputStream; import java.io.BufferedReader; -import java.io.BufferedWriter; import java.io.File; import java.io.FileInputStream; -import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStreamReader; -import java.io.OutputStreamWriter; -import java.io.Writer; import java.nio.charset.StandardCharsets; -import java.util.Locale; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicBoolean; -import java.util.zip.ZipEntry; -import java.util.zip.ZipInputStream; import app.morphe.extension.shared.Logger; /** - * The Mandarin reading of every Han character, taken from the Unicode Han Database, which - * publishes one reading per character: the one it is given when no word says otherwise. That is - * the right starting point for a lyric, since the readings that differ belong to words, and those - * come from {@link #word()} instead. - * - *

    Downloaded rather than shipped, because the patch can only merge XML resources and because - * a table of every character goes stale as soon as the database moves on. + * Mandarin word readings from pinyin-pro, which knows words the {@link UniDictStore} character + * tables do not; character readings come from {@link UniDictStore#pinyinReading(char)}, this + * store only adds how a word is read. Downloaded, since the patch merges only XML resources and + * a word list goes stale as the source moves on. */ public final class PinyinDictStore { public static final String VERSION = "18.0.0"; - private static final String URL = - "https://www.unicode.org/Public/" + VERSION + "/ucd/Unihan.zip"; - private static final String ARCHIVE = "Unihan.zip"; - private static final String READINGS_ENTRY = "Unihan_Readings.txt"; - private static final String TABLE_NAME = "pinyin_readings_toned_" + VERSION + ".txt"; - private static final String NOTICE_NAME = "pinyin_readings.NOTICE.txt"; - - /** The commit is what names the version: the file is updated in place, and the table - * has to be rebuilt when it changes. */ + /** + * The commit is what names the version: the file is updated in place, and the table + * has to be rebuilt when it changes. + */ private static final String WORDS_REVISION = "b0b2c2f93be09e76b8970db1563e943032bfb805"; private static final String WORDS_URL = "https://raw.githubusercontent.com/chinese-data/pinyin-pro-data/" + WORDS_REVISION @@ -59,11 +44,7 @@ public final class PinyinDictStore { private static final String WORDS_NOTICE_NAME = "pinyin_words.NOTICE.txt"; private static final WordTableFile WORDS = new WordTableFile( "pinyin_words_" + WORDS_REVISION + ".bin", - new byte[] {'M', 'P', 'Y', 'W', '0', '0', '0', '1'}, 1 << 20); - - /** How much a reading field is trusted, best first. */ - private static final String[] SOURCES = - {"kMandarin", "kHanyuPinlu", "kHanyuPinyin", "kXHC1983"}; + new byte[]{'M', 'P', 'Y', 'W', '0', '0', '0', '1'}, 1 << 20); private static final ExecutorService WORKER = Executors.newSingleThreadExecutor(runnable -> { Thread thread = new Thread(runnable, "lyrics-pinyin-dict"); @@ -75,37 +56,39 @@ public final class PinyinDictStore { private static final RomanizationDict.Channel CHANNEL = new RomanizationDict.Channel(); - @Nullable - private static volatile CharReadingTable characters; - private PinyinDictStore() { } - /** Loads the table on first use, since only the settings row downloads it. */ + /** + * Loads on first use; the character tables must be there too, or per-character readings + * a word table does not cover would be missing. + */ public static boolean isReady() { - if (characters != null) return true; - File table = tableFile(); - if (table == null || !table.isFile()) return false; - try { - characters = CharReadingTable.load(table); - } catch (IOException ex) { - Logger.printDebug(() -> "Could not read the pinyin table", ex); - return false; - } - return true; + return UniDictStore.isReady() && word() != null; } - @Nullable - private static File tableFile() { + /** + * Removes the word table and its licence notice. The character table is + * {@link UniDictStore}'s to remove, and is left alone. + */ + public static void delete() { + WORDS.clear(); + File file = WORDS.file(); + if (file != null && file.exists()) { + //noinspection ResultOfMethodCallIgnored + file.delete(); + } File directory = RomanizationDict.directory(); - return directory == null ? null : new File(directory, TABLE_NAME); + if (directory != null) { + //noinspection ResultOfMethodCallIgnored + new File(directory, WORDS_NOTICE_NAME).delete(); + } } - /** The reading of a character, or null when the database has none for it. */ + /** The reading of a character, delegated to the shared character tables. */ @Nullable public static String reading(char c) { - CharReadingTable table = characters; - return table == null ? null : table.reading(c); + return UniDictStore.pinyinReading(c); } /** Null leaves the line to be read a character at a time. */ @@ -114,9 +97,17 @@ public static WordReadingTable word() { return WORDS.open(); } + /** + * Whether the word table has been built; the character tables are this store's by way of + * {@link UniDictStore}, and are checked through {@link UniDictStore#isReady()}. + */ + public static boolean isComplete() { + return isReady(); + } + /** Starts a fetch if the table is not loaded. Safe to call whenever the setting is on. */ public static void ensure(RomanizationDict.Listener listener) { - if (isReady() && word() != null && CantoneseDictStore.word() != null) { + if (isComplete()) { listener.onReady(entries()); return; } @@ -139,48 +130,29 @@ public static void ensure(RomanizationDict.Listener listener) { @Nullable public static String notice() { File directory = RomanizationDict.directory(); - return directory == null ? null : RomanizationDict.readNotice(directory, NOTICE_NAME); + return directory == null ? null : RomanizationDict.readNotice(directory, WORDS_NOTICE_NAME); + } + + private static int entries() { + WordReadingTable words = word(); + return words == null ? 0 : words.size(); } private static void build(RomanizationDict.Listener listener) throws IOException { File directory = RomanizationDict.directory(); if (directory == null) throw new IOException("no cache directory"); - File table = tableFile(); - if (!table.isFile()) { - File archive = new File(directory, ARCHIVE); - if (!archive.isFile()) { - RomanizationDict.download(URL, archive, ARCHIVE, listener); - } - extract(archive, table, listener); - //noinspection ResultOfMethodCallIgnored - archive.delete(); - } - - characters = CharReadingTable.load(table); - RomanizationDict.writeNotice(directory, NOTICE_NAME, noticeTextFor("kMandarin")); - CantoneseDictStore.writeNotice(directory); - WORDS.build(directory, WORDS_URL, WORDS_ARCHIVE, PinyinDictStore::addWords, listener); RomanizationDict.writeNotice(directory, WORDS_NOTICE_NAME, wordsNoticeText()); - CantoneseDictStore.buildWords(directory, listener); listener.onReady(entries()); } - private static int entries() { - WordReadingTable pinyinWords = word(); - WordReadingTable cantoneseWords = CantoneseDictStore.word(); - return characters.size() - + (pinyinWords == null ? 0 : pinyinWords.size()) - + (cantoneseWords == null ? 0 : cantoneseWords.size()); - } - /** * Read rather than parsed: a quote followed by a colon appears in a word and nowhere else, * which is enough to tell a key from a reading however the file happens to be laid out. */ private static void addWords(File archive, WordReadingTable.Builder builder, - RomanizationDict.Listener listener) throws IOException { + RomanizationDict.Listener listener) throws IOException { String word = null; String reading = null; int seen = 0; @@ -220,138 +192,16 @@ private static String quoted(String line, int from) { return close < 0 ? null : line.substring(open + 1, close); } - /** The erhua suffix rides on the syllable before it, since such a word has one syllable - * fewer than it has characters. */ + /** + * The erhua suffix rides on the syllable before it, since such a word has one syllable + * fewer than it has characters. + */ private static String withErhua(String reading) { int cut = reading.lastIndexOf(' '); if (cut < 0 || !"er".equals(reading.substring(cut + 1))) return reading; return reading.substring(0, cut) + "r"; } - /** - * Reads the readings out of the archive and writes one line per character, holding only - * the candidates of the character being read: taking the first line instead would pick - * kHanyuPinlu, which is the poorer source and comes first alphabetically. - */ - private static void extract(File archive, File table, - RomanizationDict.Listener listener) throws IOException { - File built = new File(table.getPath() + ".building"); - File cantoneseTable = CantoneseDictStore.tableFile(); - File cantoneseBuilt = new File(cantoneseTable.getPath() + ".building"); - long size = archive.length(); - - try (ZipInputStream zip = new ZipInputStream( - new BufferedInputStream(new FileInputStream(archive), 1 << 16)); - Writer out = new BufferedWriter(new OutputStreamWriter( - new FileOutputStream(built), StandardCharsets.UTF_8), 1 << 16); - Writer cantoneseOut = new BufferedWriter(new OutputStreamWriter( - new FileOutputStream(cantoneseBuilt), StandardCharsets.UTF_8), 1 << 16)) { - ZipEntry entry; - while ((entry = zip.getNextEntry()) != null) { - if (entry.isDirectory() || !READINGS_ENTRY.equals(entry.getName())) continue; - - BufferedReader reader = new BufferedReader( - new InputStreamReader(zip, StandardCharsets.UTF_8), 1 << 16); - StringBuilder pending = new StringBuilder(); - char pendingChar = 0; - int pendingRank = SOURCES.length; - - String line; - long done = 0; - while ((line = reader.readLine()) != null) { - done += line.length(); - - int firstTab = line.indexOf('\t'); - int secondTab = firstTab < 0 ? -1 : line.indexOf('\t', firstTab + 1); - if (secondTab < 0) continue; - - String field = line.substring(firstTab + 1, secondTab); - char c = codePoint(line.substring(0, firstTab)); - if (c == 0) continue; - - // The archive is deleted once unpacked, so the Cantonese table leaves in this same pass. - if (CantoneseDictStore.FIELD.equals(field)) { - String syllable = first(line.substring(secondTab + 1)); - if (syllable != null) { - cantoneseOut.append(c).append(' ').append(syllable).append('\n'); - } - continue; - } - - int rank = rank(field); - if (rank < 0) continue; - - if (c != pendingChar) { - out.append(pending); - pending.setLength(0); - pendingChar = c; - pendingRank = SOURCES.length; - } - if (rank >= pendingRank) continue; - - String reading = first(line.substring(secondTab + 1)); - if (reading != null) { - pending.append(c).append(' ').append(reading).append('\n'); - pendingRank = rank; - } - - if (size > 0 && done % (1 << 20) < line.length()) { - listener.onBuilding((int) Math.min(100, done * 100 / size)); - } - } - out.append(pending); - listener.onBuilding(100); - } - } - RomanizationDict.commit(built, table); - RomanizationDict.commit(cantoneseBuilt, cantoneseTable); - } - - private static int rank(String field) { - for (int i = 0; i < SOURCES.length; i++) { - if (SOURCES[i].equals(field)) return i; - } - return -1; - } - - private static char codePoint(String field) { - if (field.length() < 3 || field.charAt(0) != 'U' || field.charAt(1) != '+') return 0; - try { - int value = Integer.parseInt(field.substring(2), 16); - return value > 0 && value <= Character.MAX_VALUE ? (char) value : 0; - } catch (NumberFormatException ex) { - return 0; - } - } - - /** The first reading of a field, minus its frequency or location code, as in {@code wo(23213)}. */ - @Nullable - private static String first(String value) { - for (String candidate : value.split(",")) { - String reading = candidate.trim(); - int cut = reading.indexOf(':'); - if (cut >= 0) reading = reading.substring(cut + 1).trim(); - cut = reading.indexOf('('); - if (cut >= 0) reading = reading.substring(0, cut).trim(); - cut = reading.indexOf(' '); - if (cut > 0) reading = reading.substring(0, cut); - //noinspection SizeReplaceableByIsEmpty - if (reading.length() > 0) return reading.toLowerCase(Locale.ROOT); - } - return null; - } - - static String noticeTextFor(String field) { - return "Unicode Han Database readings\n" - + "https://www.unicode.org/Public/" + VERSION + "/ucd/Unihan.zip\n\n" - + "Licence: Unicode License, Version 3\n" - + "https://www.unicode.org/license.txt\n\n" - + "Copyright (c) 1991-2026 Unicode, Inc. All rights reserved.\n" - + "Distributed under the Terms of Use in https://www.unicode.org/copyright.txt\n\n" - + "What is stored here is the " + field + " reading of each character, taken from\n" - + "Unihan_Readings.txt in the file above.\n"; - } - private static String wordsNoticeText() { return "pinyin-pro word readings\n" + WORDS_URL + "\n\n" @@ -376,11 +226,4 @@ private static String wordsNoticeText() { + "What is stored here is the reading of each word, taken from data/complete.json\n" + "in the file above, with the erhua suffix folded into the syllable before it.\n"; } - - @Nullable - public static String wordsNotice() { - File directory = RomanizationDict.directory(); - return directory == null ? null - : RomanizationDict.readNotice(directory, WORDS_NOTICE_NAME); - } } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/PinyinRomanizer.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/PinyinRomanizer.java index 29ec8e3d00..9e19406845 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/PinyinRomanizer.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/PinyinRomanizer.java @@ -36,7 +36,7 @@ private PinyinRomanizer() { /** Pinyin for a Chinese line, with a space between readings. */ public static String romanize(String line) { - WordReadingTable table = PinyinDictStore.word(); + final WordReadingTable table = PinyinDictStore.word(); return HanRomanizer.romanize(line, new HanRomanizer.Readings() { @Nullable @Override diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/RimeDictStore.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/RimeDictStore.java new file mode 100644 index 0000000000..6e5950da23 --- /dev/null +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/RimeDictStore.java @@ -0,0 +1,1008 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/3851 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.extension.music.patches.lyrics.romanizers; + +import androidx.annotation.Nullable; + +import java.io.BufferedInputStream; +import java.io.Closeable; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +import app.morphe.extension.music.settings.Settings; +import app.morphe.extension.shared.Logger; +import app.morphe.extension.shared.ResourceUtils; + +/** + * The word readings for the languages a Rime dictionary knows, taken from Rime word lists, + * which know a word the {@link UniDictStore} character tables do not. The character reading of a + * Han character comes from {@link UniDictStore}; this store only adds what a word is read as. + * + *

    One table per language, named by the BCP-47 primary language tag. The language the user + * asked to romanize picks the table, so a new language is one row in {@link #BUILTIN} or one line + * of {@link Settings#LYRICS_RIME_CUSTOM_DICTS}. + * + *

    Only Cantonese ships. Every other word list is added by the user, who supplies its URL + * and therefore judges its licence and freshness; the patch never bundles a list whose terms + * it cannot vouch for. Downloaded rather than shipped, because the patch can only merge XML + * resources and because a word list goes stale as soon as the source moves on. + */ +public final class RimeDictStore { + + /** The character table a word table falls back to for the characters a word does not cover. */ + public static final int CHAR_SOURCE_AUTO = 0; + public static final int CHAR_SOURCE_CANTONESE = 1; + public static final int CHAR_SOURCE_PINYIN = 2; + public static final int CHAR_SOURCE_NONE = 3; + /** Progress on the character tables is the Local provider's row to report, not this one's. */ + public static final RomanizationDict.Listener SILENT = new RomanizationDict.Listener() { + @Override + public void onDownload(String fileName, int percent) { + } + + @Override + public void onBuilding(int percent) { + } + + @Override + public void onReady(int entries) { + } + + @Override + public void onFailed() { + } + }; + /** The Rime dictionaries read by this store, each the word list of one language. */ + private static final List BUILTIN = List.of( + // The commit is what names the version: the file is updated in place, and the table + // has to be rebuilt when it changes. + new RimeLang("yue", + "https://raw.githubusercontent.com/rime/rime-cantonese/" + + "259f0e48bba840c3a2e0d117539e96937f3d89bc/jyut6ping3.words.dict.yaml", + "cantonese_words.yaml", + "cantonese_words_259f0e48bba840c3a2e0d117539e96937f3d89bc.bin", + "cantonese_words.NOTICE.txt", + "粤语", + new byte[]{'M', 'C', 'Y', 'W', '0', '0', '0', '1'}, + 1 << 20, CHAR_SOURCE_CANTONESE, false)); + /** The word table files, one per language, opened on first use. */ + private static final Map TABLES = new HashMap<>(); + private static final ExecutorService WORKER = Executors.newSingleThreadExecutor(runnable -> { + Thread thread = new Thread(runnable, "lyrics-rime-dict"); + thread.setDaemon(true); + return thread; + }); + private static final AtomicBoolean RUNNING = new AtomicBoolean(); + /** Set while a saved choice is fetching its tables one after another. */ + private static final AtomicBoolean BATCHING = new AtomicBoolean(); + private static final RomanizationDict.Channel CHANNEL = new RomanizationDict.Channel(); + /** A word table of a few hundred thousand lines is already far past a useful size. */ + private static final int MAX_LINES = 4_000_000; + /** + * The characters read from one source. {@link #MAX_LINES} alone still lets a tiny archive + * that unpacks to the same four million long lines cost minutes of reading, so the total + * read is bounded as well. The largest real list seen is about twenty million characters. + */ + private static final long MAX_SOURCE_CHARS = 64L << 20; + /** + * What a user-supplied word list may download as. The largest Rime list seen is twenty + * megabytes of text; anything far past that is not a word list, and a zip could otherwise + * be made to fill the disk. + */ + private static final long MAX_SOURCE_BYTES = 64L << 20; + /** Forwards progress only: a per-table "ready" would end the batch early. */ + private static final RomanizationDict.Listener BATCH_PROGRESS = new RomanizationDict.Listener() { + @Override + public void onDownload(String fileName, int percent) { + CHANNEL.onDownload(fileName, percent); + } + + @Override + public void onBuilding(int percent) { + CHANNEL.onBuilding(percent); + } + + @Override + public void onReady(int entries) { + } + + @Override + public void onFailed() { + } + }; + /** The size the built-in table is known to download at, shown before it is fetched. */ + private static final String FALLBACK_BUILTIN_MB = "3 MB"; + /** + * A single line longer than this is not a word list, and reading it would cost the heap + * far more than the entry could ever be worth. + */ + private static final int MAX_LINE_LENGTH = 1 << 16; + /** + * The languages whose Han characters are read by a romanizer this build already has. These + * are reserved even when the language list cannot be read: a word table under one of them + * would shadow a reading that works. + */ + private static final Set HAN_READING_LANGUAGES = Set.of("ja", "zh", "yue"); + private static volatile Snapshot snapshot; + /** The tags the romanization language list offers, read once. */ + private static volatile Set shippedLanguages; + + private RimeDictStore() { + } + + /** Whether the language the user asked for has a word table here. */ + public static boolean isActive() { + return lang() != null; + } + + /** + * Loads on first use; the character tables must be there too, or per-character readings + * a word table does not cover would be missing. + */ + public static boolean isReady() { + RimeLang lang = lang(); + return lang != null && characterTablesReady(lang) && table(lang).open() != null; + } + + /** Whether the chosen language needs a character table at all, and has it. */ + private static boolean characterTablesReady(RimeLang lang) { + return lang.charSource == CHAR_SOURCE_NONE || UniDictStore.isReady(); + } + + /** + * Removes one language's word table, for when the user takes that list away. Must run while + * the settings still name the language, since the file names come from that row. + */ + public static void deleteTable(String code) { + RimeLang lang = langs().get(code); + // A reserved row, or a built-in table that is switched off, has left the table map but its + // files are still named after it. + if (lang == null) lang = storedLang(code); + if (lang != null) { + removeFiles(lang); + } + } + + /** The row of a language that parsed but is not a reading source right now. */ + @Nullable + private static RimeLang storedLang(String code) { + for (RimeLang lang : loaded().stored) { + if (lang.code.equals(code)) return lang; + } + for (RimeLang lang : BUILTIN) { + if (lang.code.equals(code)) return lang; + } + return null; + } + + private static void removeFiles(RimeLang lang) { + synchronized (TABLES) { + WordTableFile table = TABLES.remove(lang.code); + if (table != null) { + table.clear(); + } + } + File directory = RomanizationDict.directory(); + if (directory == null) { + return; + } + //noinspection ResultOfMethodCallIgnored + new File(directory, lang.noticeName).delete(); + //noinspection ResultOfMethodCallIgnored + new File(directory, lang.tableName).delete(); + //noinspection ResultOfMethodCallIgnored + new File(directory, lang.archive).delete(); + //noinspection ResultOfMethodCallIgnored + new File(directory, lang.archive + ".part").delete(); + } + + /** Forgets the cached tables and the parsed settings, so the next read sees a change. */ + public static void reset() { + snapshot = null; + synchronized (TABLES) { + for (WordTableFile table : TABLES.values()) { + table.clear(); + } + TABLES.clear(); + } + } + + /** The reading of a character, delegated to the shared character tables. */ + @Nullable + public static String reading(char c) { + RimeLang lang = lang(); + if (lang == null) return null; + switch (lang.charSource) { + case CHAR_SOURCE_CANTONESE: + return UniDictStore.cantoneseReading(c); + case CHAR_SOURCE_PINYIN: + return UniDictStore.pinyinReading(c); + default: + return null; + } + } + + /** Null leaves the line to be read a character at a time. */ + @Nullable + public static WordReadingTable word() { + RimeLang lang = lang(); + return lang == null ? null : table(lang).open(); + } + + /** Starts a fetch if the table is not loaded. Safe to call whenever the setting is on. */ + public static void ensure(RomanizationDict.Listener listener) { + if (isReady()) { + listener.onReady(entries()); + return; + } + // A saved choice is already fetching every ticked table, this one among them. Starting a + // second build beside it would queue the same work twice behind the single worker. + if (BATCHING.get()) { + CHANNEL.attach(listener, false); + return; + } + CHANNEL.attach(listener, RUNNING.get()); + if (!RUNNING.compareAndSet(false, true)) return; + + // The language is read once, here: a build that finished after the user switched + // language must not report the table it built as the new language's. + final RimeLang lang = lang(); + if (lang == null) { + // Nothing to fetch for this language; say so rather than leaving the listener + // waiting, which would hold the settings row's progress at "downloading". + RUNNING.set(false); + CHANNEL.onReady(0); + CHANNEL.detach(); + return; + } + WORKER.execute(() -> { + try { + build(lang, CHANNEL); + } catch (Exception ex) { + Logger.printDebug(() -> "Rime dictionary build failed", ex); + CHANNEL.onFailed(); + } finally { + RUNNING.set(false); + CHANNEL.detach(); + } + }); + } + + /** + * Fetches every ticked table, one after another, so a Save downloads what the boxes promised + * rather than only the table of the chosen language. Progress goes through the same listener + * a single fetch uses, but only the end of the last table reports ready. + * + *

    A fetch already on its way is not cancelled: the worker is single threaded, so this + * batch waits behind it and reattaches its listener when its turn comes. + */ + public static void ensureSelected(RomanizationDict.Listener listener) { + if (anySelectedNeedsCharacterTable() + && !UniDictStore.isRunning() && !UniDictStore.isReady()) { + UniDictStore.ensure(SILENT); + } + final List targets = new ArrayList<>(langs().values()); + if (targets.isEmpty()) { + listener.onReady(0); + return; + } + BATCHING.set(true); + CHANNEL.attach(listener, false); + WORKER.execute(() -> { + boolean failed = false; + int entries = 0; + try { + // A one-off fetch may have run in between and let go of the channel. + CHANNEL.attach(listener, false); + for (RimeLang target : targets) { + try { + if (table(target).open() == null) { + build(target, BATCH_PROGRESS); + } + WordReadingTable open = table(target).open(); + if (open != null) entries = open.size(); + } catch (Exception ex) { + Logger.printDebug(() -> "Rime dictionary build failed: " + target.code, ex); + failed = true; + } + } + if (failed) { + CHANNEL.onFailed(); + } else { + CHANNEL.onReady(entries); + } + } finally { + BATCHING.set(false); + CHANNEL.detach(); + } + }); + } + + /** The name shown under a reading, for the dictionary that supplied it. */ + @Nullable + public static String dictionaryName() { + RimeLang lang = lang(); + return lang == null ? null : lang.dictionaryName; + } + + /** + * Every notice, one per added list, so the about screen credits everything downloaded + * even when the current language is another. + */ + @Nullable + public static String allNotices() { + File directory = RomanizationDict.directory(); + if (directory == null) return null; + StringBuilder text = new StringBuilder(); + for (RimeLang lang : langs().values()) { + String part = RomanizationDict.readNotice(directory, lang.noticeName); + if (part == null) continue; + if (text.length() > 0) text.append("\n\n"); + text.append(part); + } + return text.length() > 0 ? text.toString() : null; + } + + /** The languages the user added, for the settings row, in the order they were added. */ + public static List customLanguages() { + List codes = new ArrayList<>(); + for (RimeLang lang : loaded().stored) { + codes.add(lang.code); + } + return codes; + } + + /** The built-in tables whose switch is on, read from the comma separated setting. */ + private static Set parseBuiltin(@Nullable String stored) { + Set on = new HashSet<>(); + if (stored == null || stored.isBlank()) return on; + for (String token : stored.split(",")) { + final String code = token.trim().toLowerCase(Locale.ROOT); + if (!code.isEmpty()) on.add(code); + } + return on; + } + + /** Records whether a built-in table is ticked, so it is fetched only when asked for. */ + public static void setBuiltinSelected(String code, boolean selected) { + final String stored = Settings.LYRICS_RIME_BUILTIN_SELECTED.get(); + final StringBuilder kept = new StringBuilder(); + if (stored != null && !stored.isBlank()) { + for (String token : stored.split(",")) { + final String value = token.trim(); + if (value.isEmpty() || value.equalsIgnoreCase(code)) continue; + if (kept.length() > 0) kept.append(','); + kept.append(value); + } + } + if (selected) { + if (kept.length() > 0) kept.append(','); + kept.append(code); + } + Settings.LYRICS_RIME_BUILTIN_SELECTED.save(kept.toString()); + reset(); + } + + /** Whether the switch of a built-in table is on, for the dialog to draw its box. */ + public static boolean isBuiltinRowSelected(String code) { + return loaded().builtinOn.contains(code.toLowerCase(Locale.ROOT)); + } + + /** + * The stored line of a user-added list, so a caller can rewrite the setting without + * rebuilding the line from fields it would have to know the order of. + */ + @Nullable + public static String customRow(String code) { + final String stored = Settings.LYRICS_RIME_CUSTOM_DICTS.get(); + if (stored == null || stored.isBlank()) return null; + for (String line : stored.split("\n")) { + if (line.isBlank()) continue; + String[] parts = line.split("\\|", 2); + if (parts.length > 0 && parts[0].trim().equalsIgnoreCase(code)) { + return line; + } + } + return null; + } + + /** + * The tables that ship already added, in the order they are listed. Cantonese is the example: + * it is what a user-added list is meant to look like, and it is already in the list rather + * than something that has to be added. + */ + public static List builtinLanguages() { + List codes = new ArrayList<>(); + for (RimeLang lang : BUILTIN) { + codes.add(lang.code); + } + return codes; + } + + /** The name of a built-in table, or null when the language has none. */ + @Nullable + public static String builtinLabel(String code) { + for (RimeLang lang : BUILTIN) { + if (lang.code.equals(code)) return lang.dictionaryName; + } + return null; + } + + /** The name the user gave a language, or null when it is not one of theirs. */ + @Nullable + public static String customLabel(String code) { + for (RimeLang lang : loaded().stored) { + if (lang.code.equals(code)) return lang.dictionaryName; + } + return null; + } + + /** + * Whether a user-supplied row names a language this build already reads, which is a row + * that is kept but never used for reading. + */ + public static boolean isReserved(String code) { + return isShippedLanguage(code); + } + + /** + * The size and source shown under a row: the table's size on disk and the repository it came + * from, as "~3 MB, rime/rime-cantonese". The size of a built-in table is known before it is + * fetched, so it reads the same either way; an added list has no size until it is on disk, + * and shows only its repository until then. + * + * @return the summary, or null when the row names no table. + */ + @Nullable + public static String summaryFor(String code) { + final RimeLang lang = storedLang(code); + if (lang == null) return null; + + final String repo = repositoryOf(lang.url); + final long bytes = downloadedBytes(lang); + final String megabytes = bytes > 0 + ? Math.max(1, Math.round(bytes / (1024.0 * 1024.0))) + " MB" + : (!lang.custom ? FALLBACK_BUILTIN_MB : null); + if (megabytes == null) { + return repo; + } + return repo == null ? "~" + megabytes : "~" + megabytes + ", " + repo; + } + + /** The table file's size, or 0 when it has not been built. */ + private static long downloadedBytes(RimeLang lang) { + final File directory = RomanizationDict.directory(); + if (directory == null) return 0; + final File table = new File(directory, lang.tableName); + return table.isFile() ? table.length() : 0; + } + + /** The "owner/repo" a git URL names: the first two non-empty path segments. */ + @Nullable + private static String repositoryOf(String url) { + try { + java.net.URL parsed = new java.net.URL(url); + String path = parsed.getPath(); + if (path == null || path.isEmpty()) return null; + // The owner/repo is the first two non-empty path segments; the file and its + // branch live after them (e.g., raw.githubusercontent.com/owner/repo/branch/file). + String[] parts = path.split("/"); + String owner = null, repo = null; + for (String part : parts) { + if (part.isEmpty()) continue; + if (owner == null) owner = part; + else if (repo == null) { + repo = part; + break; + } + } + if (owner == null || repo == null) return null; + return owner + "/" + repo; + } catch (Exception ex) { + return null; + } + } + + /** Whether the table of the language in use needs the character tables beside it. */ + public static boolean needsCharacterTable() { + RimeLang lang = lang(); + return lang != null && lang.charSource != CHAR_SOURCE_NONE; + } + + /** Whether any ticked table reads characters through the character tables. */ + private static boolean anySelectedNeedsCharacterTable() { + for (RimeLang lang : langs().values()) { + if (lang.charSource != CHAR_SOURCE_NONE) return true; + } + return false; + } + + /** + * Starts the character tables and then the word table, for the Rime Wordlist provider. The word + * table alone is not enough to read a line: the characters no word covers come from + * {@link UniDictStore}, so both are fetched before the table reports ready. + */ + public static void ensureForProvider(RomanizationDict.Listener listener) { + if (needsCharacterTable() && !UniDictStore.isRunning() && !UniDictStore.isReady()) { + UniDictStore.ensure(SILENT); + } + ensure(listener); + } + + /** + * Whether the language already has a table, the built-in one included. A built-in table that is + * switched off still counts: a second list under the same code could never be read. + */ + public static boolean hasLanguage(String code) { + if (langs().containsKey(code)) return true; + for (RimeLang lang : BUILTIN) { + if (lang.code.equals(code)) return true; + } + return false; + } + + /** + * Whether the language is read through a table right now: a built-in table whose switch is on, + * or one of the user's lists. A built-in table that is switched off is not a reading source. + */ + public static boolean isSelected(String code) { + return langs().containsKey(code); + } + + /** Whether a language may get a user-supplied list: one this build does not already read. */ + public static boolean canAddLanguage(String code) { + return !hasLanguage(code) && !isShippedLanguage(code); + } + + private static int entries() { + WordReadingTable words = word(); + return words == null ? 0 : words.size(); + } + + private static void build(RimeLang lang, RomanizationDict.Listener listener) + throws IOException { + File directory = RomanizationDict.directory(); + if (directory == null) throw new IOException("no cache directory"); + + // A language taken away while the fetch ran has nothing to write and nothing to say. + if (!langs().containsKey(lang.code)) return; + + WordTableFile table = table(lang); + // A word list larger than the table it is read into cannot add anything the table + // would keep, so a list past this size is not fetched at all. + table.build(directory, lang.url, lang.archive, RimeDictStore::addWords, listener, + lang.custom ? MAX_SOURCE_BYTES : Long.MAX_VALUE); + RomanizationDict.writeNotice(directory, lang.noticeName, noticeText(lang)); + WordReadingTable built = table.open(); + listener.onReady(built == null ? 0 : built.size()); + } + + /** + * Reads a word list into the table. A list may arrive as the YAML itself or as a zip holding + * it; the zip is read from its entry rather than unpacked, so nothing it names can be written + * outside the dictionary directory. + * + *

    A word may list the readings it shares with others in brackets; the first is the one + * wanted. The table itself keeps the first reading of a surface form, which is the one a list + * sorted by frequency puts first. + */ + private static void addWords(File archive, WordReadingTable.Builder builder, + RomanizationDict.Listener listener) throws IOException { + // The address decides the name, but the bytes decide how to read it: an archive served + // from an address without ".zip" at the end is still an archive, and reading it as text + // would fill the table with the zip header instead of failing. + if (!isArchive(archive)) { + try (InputStream in = new BufferedInputStream(new FileInputStream(archive), 1 << 16)) { + readWords(in, builder, listener); + } + return; + } + + // A Rime archive holds other YAML beside the list, so the list proper is looked for + // first and any YAML is only a fallback. + if (readArchive(archive, builder, listener, true)) return; + if (readArchive(archive, builder, listener, false)) return; + throw new IOException("no .dict.yaml inside " + archive.getName()); + } + + /** Whether the file starts with the bytes a zip starts with. */ + private static boolean isArchive(File file) throws IOException { + try (InputStream in = new FileInputStream(file)) { + return in.read() == 'P' && in.read() == 'K'; + } + } + + /** Reads the first word list in the archive whose name passes {@link #isWordList}. */ + private static boolean readArchive(File archive, WordReadingTable.Builder builder, + RomanizationDict.Listener listener, boolean dictOnly) throws IOException { + try (ZipInputStream zip = new ZipInputStream(new BufferedInputStream( + new FileInputStream(archive), 1 << 16))) { + ZipEntry entry; + while ((entry = zip.getNextEntry()) != null) { + if (entry.isDirectory()) continue; + final String name = entry.getName(); + if (dictOnly ? !isDictFile(name) : !isWordList(name)) continue; + readWords(zip, builder, listener); + return true; + } + } + return false; + } + + /** A file named as a Rime dictionary, rather than some other YAML beside it. */ + private static boolean isDictFile(String name) { + final String lower = name.toLowerCase(Locale.ROOT); + return lower.endsWith(".dict.yaml") || lower.endsWith(".dict.yml"); + } + + private static boolean isWordList(String name) { + final String lower = name.toLowerCase(Locale.ROOT); + return isDictFile(lower) || lower.endsWith(".yaml") || lower.endsWith(".yml"); + } + + private static void readWords(InputStream in, WordReadingTable.Builder builder, + RomanizationDict.Listener listener) throws IOException { + int seen = 0; + try (LineReader reader = new LineReader(in)) { + String line; + while ((line = reader.readLine()) != null) { + if (++seen > MAX_LINES || reader.consumed() > MAX_SOURCE_CHARS) break; + + int tab = line.indexOf('\t'); + if (tab <= 0) continue; + + String reading = line.substring(tab + 1); + // A list may add a frequency or code column after the reading; the reading is + // the first column only, so a word is never read as "tam\t50%". + int next = reading.indexOf('\t'); + if (next >= 0) reading = reading.substring(0, next); + int cut = reading.indexOf('['); + if (cut >= 0) reading = reading.substring(0, cut); + reading = reading.trim(); + if (reading.isEmpty()) continue; + + builder.put(line.substring(0, tab), reading); + if (seen % WordTableFile.PROGRESS_EVERY == 0) { + listener.onBuilding(Math.min(99, seen / WordTableFile.PROGRESS_EVERY)); + } + } + } + listener.onBuilding(100); + } + + private static String noticeText(RimeLang lang) { + if (!lang.custom) { + return "Rime " + lang.code + " word readings\n" + + lang.url + "\n\n" + + "From https://github.com/rime/rime-cantonese\n\n" + + "Licence: Creative Commons Attribution 4.0 International (CC BY 4.0)\n" + + "https://creativecommons.org/licenses/by/4.0/legalcode\n\n" + + "Creator: CanCLID, the Cantonese Linguistics Infrastructure Development Group\n" + + "https://github.com/CanCLID/rime-cantonese-upstream\n\n" + + "The work is provided as-is and the contributors make no warranties of any kind\n" + + "concerning it.\n\n" + + "What is stored here is the Jyutping reading of each word, taken from\n" + + "jyut6ping3.words.dict.yaml in the file above.\n"; + } + return "Rime " + lang.code + " word readings\n" + + lang.url + "\n\n" + + "Added by the user, who chose this word list and supplies it from that address.\n" + + "Its licence, provenance and correctness are the user's to confirm; nothing\n" + + "about it is bundled with or endorsed by this patch.\n\n" + + "What is stored here is the reading of each word, in the form the list gives it.\n"; + } + + /** The table of the language, or a new one named by the row. */ + private static WordTableFile table(RimeLang lang) { + synchronized (TABLES) { + WordTableFile table = TABLES.get(lang.code); + if (table == null) { + table = new WordTableFile(lang.tableName, lang.magic, lang.slotCount); + TABLES.put(lang.code, table); + } + return table; + } + } + + /** The language the user asked to romanize, as a BCP-47 primary subtag. */ + @Nullable + private static RimeLang lang() { + final String language = Settings.LYRICS_ROMANIZE_LANGUAGE.get(); + if (language == null) return null; + final String subtag = language.toLowerCase(Locale.ROOT).split("-", 2)[0]; + return langs().get(subtag); + } + + /** The built-in tables plus the user's, parsed once per settings value. */ + private static Map langs() { + return loaded().langs; + } + + private static Snapshot loaded() { + String stored = Settings.LYRICS_RIME_CUSTOM_DICTS.get(); + if (stored == null) stored = ""; + String builtinRaw = Settings.LYRICS_RIME_BUILTIN_SELECTED.get(); + if (builtinRaw == null) builtinRaw = ""; + + Snapshot held = snapshot; + if (held != null && held.raw.equals(stored) && held.builtinRaw.equals(builtinRaw)) { + return held; + } + + final List parsed = parseCustom(stored); + final Set builtinOn = parseBuiltin(builtinRaw); + Map map = new LinkedHashMap<>(); + for (RimeLang lang : BUILTIN) { + // A built-in table is read only while its switch is on, so an upgrade spends no data + // on a table nobody asked for. + if (builtinOn.contains(lang.code)) { + map.put(lang.code, lang); + } + } + for (RimeLang lang : parsed) { + // The built-in table of a language is never replaced by a downloaded one, and a + // language the build already reads keeps its reading: a word list under "ja" would + // otherwise take Japanese away from the dictionary that ships with it. The row is + // still kept in the setting, so it can be seen and removed rather than stranded. + if (isShippedLanguage(lang.code)) continue; + map.putIfAbsent(lang.code, lang); + } + map = java.util.Collections.unmodifiableMap(map); + held = new Snapshot(stored, builtinRaw, java.util.Collections.unmodifiableList(parsed), + java.util.Collections.unmodifiableSet(builtinOn), map); + snapshot = held; + return held; + } + + /** + * One line per list, as {@code subtag|label|url|charSource}. A malformed line is skipped + * rather than taking the good ones down with it. + */ + private static List parseCustom(@Nullable String stored) { + List parsed = new ArrayList<>(); + if (stored == null || stored.isEmpty()) return parsed; + + for (String line : stored.split("\n")) { + if (line.isBlank()) continue; + String[] parts = line.split("\\|", 4); + if (parts.length < 4) continue; + + final String code = parts[0].trim().toLowerCase(Locale.ROOT); + final String label = parts[1].trim(); + final String url = parts[2].trim(); + if (!isValidCode(code) || label.isEmpty() || !isSecure(url)) continue; + + // Two lines for one language would show the same row twice, and only the first could + // ever supply a reading. The first line wins, as it does in the table map. + boolean seen = false; + for (RimeLang already : parsed) { + if (already.code.equals(code)) { + seen = true; + break; + } + } + if (seen) continue; + + parsed.add(new RimeLang(code, url, archiveName(code, url), + tableName(code, url), noticeName(code), label, magicFor(code), + 1 << 20, parseCharSource(parts[3]), true)); + } + return parsed; + } + + /** Whether the build already reads this language through its own list. */ + public static boolean isShippedLanguage(String code) { + Set cached = shippedLanguages; + if (cached == null) { + cached = readShippedLanguages(); + shippedLanguages = cached; + } + return cached.contains(code); + } + + private static Set readShippedLanguages() { + try { + final Set read = new HashSet<>(); + for (String value : ResourceUtils.getStringArray( + "morphe_music_lyrics_romanize_language_entry_values")) { + if (value != null && !value.isBlank()) { + read.add(value.trim().toLowerCase(Locale.ROOT)); + } + } + if (!read.isEmpty()) return read; + } catch (Exception ex) { + Logger.printDebug(() -> "Could not read the romanization language list", ex); + } + // The list could not be read, so the languages that read Han are reserved by hand + // rather than leaving them open to a list that would take their reading away. + return HAN_READING_LANGUAGES; + } + + /** + * A BCP-47 primary tag this store can match: two or three lowercase letters. A region + * suffix is not accepted, because the language is chosen by primary subtag and a row keyed + * by a longer tag would never be selected. The tag ends up in a file name, so nothing else + * may get through. + */ + private static boolean isValidCode(String code) { + if (code.length() < 2 || code.length() > 3) return false; + for (int i = 0; i < code.length(); i++) { + char c = code.charAt(i); + if (c < 'a' || c > 'z') return false; + } + return true; + } + + /** Only a plain HTTPS address is fetched: the app carries no authority to vouch for more. */ + private static boolean isSecure(String url) { + return url.startsWith("https://") && url.length() > "https://".length(); + } + + /** + * The stored names carry the address, so pointing a language at another list rebuilds it + * instead of reusing the old table. The suffix follows the address, which is only a hint: + * the reader tells a zip from a plain word list by its bytes. + */ + private static String archiveName(String code, String url) { + final String suffix = isZip(url) ? ".zip" : ".yaml"; + return "rime_" + code + "_" + Integer.toHexString(url.hashCode()) + suffix; + } + + /** + * The address's own name, ignoring a query string, as the only thing that says a zip was + * asked for. + */ + private static boolean isZip(String url) { + final String path = url.split("[?#]", 2)[0]; + return path.toLowerCase(Locale.ROOT).endsWith(".zip"); + } + + private static String tableName(String code, String url) { + return "rime_" + code + "_" + Integer.toHexString(url.hashCode()) + ".bin"; + } + + private static String noticeName(String code) { + return "rime_" + code + ".NOTICE.txt"; + } + + /** + * Eight bytes naming the table's writer; a table built for another language or address + * would not match and is rebuilt. + */ + private static byte[] magicFor(String code) { + byte[] magic = {'M', 'R', 'I', 'M', 'E', 0, 0, 0}; + int hash = code.hashCode(); + magic[5] = (byte) hash; + magic[6] = (byte) (hash >>> 8); + magic[7] = (byte) (hash >>> 16); + return magic; + } + + private static int parseCharSource(String value) { + switch (value.trim().toUpperCase(Locale.ROOT)) { + case "CANTONESE": + return CHAR_SOURCE_CANTONESE; + case "PINYIN": + return CHAR_SOURCE_PINYIN; + case "NONE": + return CHAR_SOURCE_NONE; + default: + return CHAR_SOURCE_AUTO; + } + } + + /** One language's word table: where it is fetched from and what it is stored as. */ + private record RimeLang(String code, String url, String archive, String tableName, + String noticeName, String dictionaryName, byte[] magic, int slotCount, + int charSource, boolean custom) { + } + + /** + * The tables the user added, with the settings text they were parsed from. Kept as one + * snapshot because {@link #lang()} runs for every character read, which is far too often to + * parse the setting each time. + * + * @param raw The custom-list setting this was parsed from. + * @param builtinRaw The built-in switch setting this was parsed from, so ticking one is seen at once. + * @param stored Every row that parsed, including the ones whose language is reserved. + * @param builtinOn The built-in tables whose switch is on. + * @param langs The tables a language may actually be read with. + */ + private record Snapshot(String raw, String builtinRaw, List stored, + Set builtinOn, Map langs) { + } + + /** + * A line reader whose memory is bounded by the line length, not by the stream: {@link + * BufferedReader#readLine} would grow a line as long as a decompression bomb says. + */ + private static final class LineReader implements Closeable { + private final Reader reader; + private final char[] buffer = new char[1 << 13]; + private int at; + private int end; + /** Characters read from the stream, which is what bounds the work a source can ask for. */ + private long consumed; + /** Set after a '\r', so the '\n' of a CRLF pair does not end a second, empty line. */ + private boolean afterCarriageReturn; + + LineReader(InputStream in) { + reader = new InputStreamReader(in, StandardCharsets.UTF_8); + } + + /** Characters read so far, whatever became of them. */ + long consumed() { + return consumed; + } + + /** + * Null at the end of the stream. A line longer than {@link #MAX_LINE_LENGTH} is + * consumed and comes back empty, which the caller skips like any other blank line. + */ + @Nullable + String readLine() throws IOException { + StringBuilder line = new StringBuilder(64); + boolean tooLong = false; + + while (true) { + if (at >= end) { + end = reader.read(buffer); + at = 0; + if (end <= 0) { + return tooLong ? null : line.toString(); + } + } + final char c = buffer[at++]; + consumed++; + + if (afterCarriageReturn) { + afterCarriageReturn = false; + if (c == '\n') continue; + } + if (c == '\r') { + afterCarriageReturn = true; + return tooLong ? "" : line.toString(); + } + if (c == '\n') { + return tooLong ? "" : line.toString(); + } + if (tooLong) continue; + if (line.length() >= MAX_LINE_LENGTH) { + line.setLength(0); + tooLong = true; + continue; + } + line.append(c); + } + } + + @Override + public void close() throws IOException { + reader.close(); + } + } + +} diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/JyutpingRomanizer.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/RimeRomanizer.java similarity index 58% rename from extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/JyutpingRomanizer.java rename to extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/RimeRomanizer.java index 031682cae4..e3bb379368 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/JyutpingRomanizer.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/RimeRomanizer.java @@ -10,17 +10,18 @@ import androidx.annotation.Nullable; /** - * Jyutping for a line of Cantonese: {@link CantoneseDictStore#word()} reads it a word at a time, - * and {@link CantoneseDictStore#reading(char)} reads the rest a character at a time. A character - * neither has is kept as it is, since dropping it would lose the characters around it. + * The reading of a line of Han characters in the romanization language's script: {@link + * RimeDictStore#word()} reads it a word at a time, and {@link RimeDictStore#reading(char)} reads + * the rest a character at a time. A character neither has is kept as it is, since dropping it + * would lose the characters around it. */ -public final class JyutpingRomanizer { +public final class RimeRomanizer { - private JyutpingRomanizer() { + private RimeRomanizer() { } public static String romanize(String line) { - WordReadingTable table = CantoneseDictStore.word(); + final WordReadingTable table = RimeDictStore.word(); return HanRomanizer.romanize(line, new HanRomanizer.Readings() { @Nullable @Override @@ -31,7 +32,7 @@ public String word(String word) { @Nullable @Override public String character(char c) { - return CantoneseDictStore.reading(c); + return RimeDictStore.reading(c); } }); } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/RomanizationDict.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/RomanizationDict.java index 0ccf04064c..7ecb04247b 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/RomanizationDict.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/RomanizationDict.java @@ -36,81 +36,30 @@ public final class RomanizationDict { private static final int PROGRESS_STEP_PERCENT = 2; - /** How the settings row follows along with a dictionary being fetched and built. */ - public interface Listener { - void onDownload(String fileName, int percent); - void onBuilding(int percent); - void onReady(int entries); - void onFailed(); - } - private RomanizationDict() { } - static final class Channel implements Listener { - - private volatile Listener to; - private volatile String fileName; - private volatile int percent; - private volatile boolean building; - - /** Takes over as the listener, and is told where the build is if one is going. */ - void attach(Listener listener, boolean running) { - to = listener; - if (!running) return; - if (building) { - listener.onBuilding(percent); - } else if (fileName != null) { - listener.onDownload(fileName, percent); - } - } - - /** Lets go, because holding the listener past the build would keep its page alive. */ - void detach() { - to = null; - fileName = null; - building = false; - } - - @Override - public void onDownload(String fileName, int percent) { - this.fileName = fileName; - this.percent = percent; - this.building = false; - Listener current = to; - if (current != null) current.onDownload(fileName, percent); - } - - @Override - public void onBuilding(int percent) { - this.percent = percent; - this.building = true; - Listener current = to; - if (current != null) current.onBuilding(percent); - } - - @Override - public void onReady(int entries) { - Listener current = to; - if (current != null) current.onReady(entries); - } - - @Override - public void onFailed() { - Listener current = to; - if (current != null) current.onFailed(); - } - } - @Nullable static File directory() { return LyricsDirectories.directory(LyricsDirectories.ROMAN_DICT); } - /** Fetches a file under a name that says it is unfinished, so an interrupted - * download is never mistaken for a whole file. */ + /** + * Fetches a file under a name that says it is unfinished, so an interrupted + * download is never mistaken for a whole file. + */ static void download(String url, File target, String reportedName, Listener listener) throws IOException { + download(url, target, reportedName, listener, Long.MAX_VALUE); + } + + /** + * As {@link #download(String, File, String, Listener)}, refusing a file larger than + * {@code maxBytes}. A user-supplied address is not a promise of a reasonable size, and a + * stream with no length would otherwise fill the disk before anything could object. + */ + static void download(String url, File target, String reportedName, Listener listener, + long maxBytes) throws IOException { File partial = new File(target.getPath() + DOWNLOAD_SUFFIX); HttpURLConnection connection = null; try { @@ -125,6 +74,10 @@ static void download(String url, File target, String reportedName, Listener list } long expected = connection.getContentLength(); + if (expected > maxBytes) { + throw new IOException("download of " + reportedName + " is larger than " + + maxBytes + " bytes"); + } long written = 0; int lastPercent = -1; @@ -137,8 +90,12 @@ static void download(String url, File target, String reportedName, Listener list byte[] buffer = new byte[BUFFER_BYTES]; int read; while ((read = in.read(buffer)) != -1) { - out.write(buffer, 0, read); written += read; + if (written > maxBytes) { + throw new IOException("download of " + reportedName + + " is larger than " + maxBytes + " bytes"); + } + out.write(buffer, 0, read); if (expected > 0) { int percent = (int) (written * 100 / expected); if (percent - lastPercent >= PROGRESS_STEP_PERCENT || percent >= 100) { @@ -195,4 +152,70 @@ static void commit(File built, File target) throws IOException { throw new IOException("could not put the table in place"); } } + + /** How the settings row follows along with a dictionary being fetched and built. */ + public interface Listener { + void onDownload(String fileName, int percent); + + void onBuilding(int percent); + + void onReady(int entries); + + void onFailed(); + } + + static final class Channel implements Listener { + + private volatile Listener to; + private volatile String fileName; + private volatile int percent; + private volatile boolean building; + + /** Takes over as the listener, and is told where the build is if one is going. */ + void attach(Listener listener, boolean running) { + to = listener; + if (!running) return; + if (building) { + listener.onBuilding(percent); + } else if (fileName != null) { + listener.onDownload(fileName, percent); + } + } + + /** Lets go, because holding the listener past the build would keep its page alive. */ + void detach() { + to = null; + fileName = null; + building = false; + } + + @Override + public void onDownload(String fileName, int percent) { + this.fileName = fileName; + this.percent = percent; + this.building = false; + Listener current = to; + if (current != null) current.onDownload(fileName, percent); + } + + @Override + public void onBuilding(int percent) { + this.percent = percent; + this.building = true; + Listener current = to; + if (current != null) current.onBuilding(percent); + } + + @Override + public void onReady(int entries) { + Listener current = to; + if (current != null) current.onReady(entries); + } + + @Override + public void onFailed() { + Listener current = to; + if (current != null) current.onFailed(); + } + } } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/ScriptDetector.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/ScriptDetector.java index a60f088004..0e50b6da7b 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/ScriptDetector.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/ScriptDetector.java @@ -20,25 +20,10 @@ */ public final class ScriptDetector { - public enum Script { - /** Han with too little kana to be Japanese, which is how Chinese reads. */ - HAN, - JAPANESE, - KOREAN, - CYRILLIC, - GEORGIAN, - ARMENIAN, - GREEK, - /** Latin script, digits or punctuation only: nothing here needs a romanizer. */ - OTHER - } - private static final Script[] SCRIPTS = Script.values(); - /** Kana below this, in a line of Han characters, is a loanword rather than Japanese. */ private static final int MIN_KANA = 3; private static final int KANA_PER_HAN = 6; - /** Hiragana is written for grammar and never for a loanword, so its count settles a line. */ private static final int HIRAGANA = SCRIPTS.length; private static final int COUNTS = SCRIPTS.length + 1; @@ -80,6 +65,14 @@ private static int[] count(CharSequence line) { found[Script.ARMENIAN.ordinal()]++; } else if (isGreek(c)) { found[Script.GREEK.ordinal()]++; + } else if (isHebrew(c)) { + found[Script.HEBREW.ordinal()]++; + } else if (isArabic(c)) { + found[Script.ARABIC.ordinal()]++; + } else if (isEthiopic(c)) { + found[Script.ETHIOPIC.ordinal()]++; + } else if (isSyriac(c)) { + found[Script.SYRIAC.ordinal()]++; } } return found; @@ -99,6 +92,10 @@ private static Script classify(int[] found) { int georgian = found[Script.GEORGIAN.ordinal()]; int armenian = found[Script.ARMENIAN.ordinal()]; int greek = found[Script.GREEK.ordinal()]; + int hebrew = found[Script.HEBREW.ordinal()]; + int arabic = found[Script.ARABIC.ordinal()]; + int ethiopic = found[Script.ETHIOPIC.ordinal()]; + int syriac = found[Script.SYRIAC.ordinal()]; if (hangul > 0 && hangul * 2 >= han + kana) return Script.KOREAN; // Kana with no Han at all is Japanese: nothing else that uses kana has none. Hiragana settles a @@ -113,7 +110,11 @@ private static Script classify(int[] found) { if (cyrillic > 0) return Script.CYRILLIC; if (georgian > 0) return Script.GEORGIAN; if (armenian > 0) return Script.ARMENIAN; - return greek > 0 ? Script.GREEK : Script.OTHER; + if (greek > 0) return Script.GREEK; + if (hebrew > 0) return Script.HEBREW; + if (arabic > 0) return Script.ARABIC; + if (ethiopic > 0) return Script.ETHIOPIC; + return syriac > 0 ? Script.SYRIAC : Script.OTHER; } public static boolean contains(@Nullable CharSequence text, Character.UnicodeScript... scripts) { @@ -121,7 +122,7 @@ public static boolean contains(@Nullable CharSequence text, Character.UnicodeScr for (int i = 0; i < text.length(); ) { final int cp = Character.codePointAt(text, i); i += Character.charCount(cp); - Character.UnicodeScript script = Character.UnicodeScript.of(cp); + final Character.UnicodeScript script = Character.UnicodeScript.of(cp); for (Character.UnicodeScript wanted : scripts) { if (script == wanted) return true; } @@ -203,4 +204,44 @@ static boolean isGeorgian(char c) { static boolean isArmenian(char c) { return c >= 0x0530 && c <= 0x058F; } + + static boolean isHebrew(char c) { + return c >= 0x0590 && c <= 0x05FF; + } + + static boolean isArabic(char c) { + return (c >= 0x0600 && c <= 0x06FF) + || (c >= 0x0750 && c <= 0x077F) + || (c >= 0x08A0 && c <= 0x08FF) + || (c >= 0xFB50 && c <= 0xFDFF) + || (c >= 0xFE70 && c <= 0xFEFF); + } + + static boolean isEthiopic(char c) { + return (c >= 0x1200 && c <= 0x137F) + || (c >= 0x1380 && c <= 0x139F) + || (c >= 0x2D80 && c <= 0x2DDF) + || (c >= 0xAB00 && c <= 0xAB2F); + } + + static boolean isSyriac(char c) { + return c >= 0x0700 && c <= 0x074F; + } + + public enum Script { + /** Han with too little kana to be Japanese, which is how Chinese reads. */ + HAN, + JAPANESE, + KOREAN, + CYRILLIC, + GEORGIAN, + ARMENIAN, + GREEK, + HEBREW, + ARABIC, + ETHIOPIC, + SYRIAC, + /** Latin script, digits or punctuation only: nothing here needs a romanizer. */ + OTHER + } } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/UniDictStore.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/UniDictStore.java new file mode 100644 index 0000000000..c48001b2eb --- /dev/null +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/UniDictStore.java @@ -0,0 +1,419 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/3750 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.extension.music.patches.lyrics.romanizers; + +import androidx.annotation.Nullable; + +import java.io.BufferedInputStream; +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.nio.charset.StandardCharsets; +import java.util.Locale; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +import app.morphe.extension.shared.Logger; + +/** + * One reading per Han character, from the Unicode Han Database, for each language that reads a + * Han lyric; one download serves every table. Each {@link #TABLES} row is a language by its + * BCP-47 tag, its Unihan fields best first; turning this off removes the character tables, so a + * language falls back to its word tables alone. + */ +public final class UniDictStore { + + /** The Unicode release the tables were built from. */ + public static final String VERSION = "18.0.0"; + + private static final String URL = + "https://www.unicode.org/Public/" + VERSION + "/ucd/Unihan.zip"; + private static final String ARCHIVE = "Unihan.zip"; + private static final String READINGS_ENTRY = "Unihan_Readings.txt"; + /** The languages whose character readings are extracted. */ + private static final ReadingTable[] TABLES = { + new ReadingTable("zh", + new String[]{"kMandarin", "kHanyuPinlu", "kHanyuPinyin", "kXHC1983"}, + "kMandarin", "pinyin_readings_toned_" + VERSION + ".txt", + "pinyin_readings.NOTICE.txt"), + new ReadingTable("yue", + new String[]{"kCantonese"}, + "kCantonese", "cantonese_readings_" + VERSION + ".txt", + "cantonese_readings.NOTICE.txt"), + }; + private static final ExecutorService WORKER = Executors.newSingleThreadExecutor(runnable -> { + Thread thread = new Thread(runnable, "lyrics-unidict"); + thread.setDaemon(true); + return thread; + }); + private static final AtomicBoolean RUNNING = new AtomicBoolean(); + private static final RomanizationDict.Channel CHANNEL = new RomanizationDict.Channel(); + + private UniDictStore() { + } + + /** Whether the tables are being downloaded or built right now. */ + public static boolean isRunning() { + return RUNNING.get(); + } + + /** Loads the tables on first use, since only the settings row downloads them. */ + public static boolean isReady() { + for (ReadingTable table : TABLES) { + if (table(table) == null) return false; + } + return true; + } + + /** Removes every character table and its notice; the user turned the readings off. */ + public static void delete() { + for (ReadingTable table : TABLES) { + table.table = null; + } + File directory = RomanizationDict.directory(); + if (directory == null) return; + for (ReadingTable table : TABLES) { + //noinspection ResultOfMethodCallIgnored + new File(directory, table.tableName).delete(); + //noinspection ResultOfMethodCallIgnored + new File(directory, table.noticeName).delete(); + } + //noinspection ResultOfMethodCallIgnored + new File(directory, ARCHIVE).delete(); + //noinspection ResultOfMethodCallIgnored + new File(directory, ARCHIVE + ".part").delete(); + } + + /** Starts a build if any table is missing. Safe to call whenever the setting is on. */ + public static void ensure(RomanizationDict.Listener listener) { + if (isReady()) { + listener.onReady(entries()); + return; + } + CHANNEL.attach(listener, RUNNING.get()); + if (!RUNNING.compareAndSet(false, true)) return; + + WORKER.execute(() -> { + try { + build(CHANNEL); + } catch (Exception ex) { + Logger.printDebug(() -> "UniDict build failed", ex); + CHANNEL.onFailed(); + } finally { + RUNNING.set(false); + CHANNEL.detach(); + } + }); + } + + /** The Mandarin reading of a character, or null when the database has none for it. */ + @Nullable + public static String pinyinReading(char c) { + return reading("zh", c); + } + + /** The Cantonese reading of a character, or null when the database has none for it. */ + @Nullable + public static String cantoneseReading(char c) { + return reading("yue", c); + } + + /** The reading from the table already loaded, or null to leave the character alone. */ + @Nullable + private static String reading(String code, char c) { + ReadingTable table = tableFor(code); + if (table == null) return null; + CharReadingTable loaded = table.table; + return loaded == null ? null : loaded.reading(c); + } + + private static int entries() { + int entries = 0; + for (ReadingTable table : TABLES) { + CharReadingTable loaded = table.table; + if (loaded != null) entries += loaded.size(); + } + return entries; + } + + /** The table of a registered language, loading it if the file is there. */ + @Nullable + private static CharReadingTable table(ReadingTable table) { + CharReadingTable loaded = table.table; + if (loaded != null) return loaded; + File file = tableFile(table.tableName); + if (file == null || !file.isFile()) return null; + try { + loaded = CharReadingTable.load(file); + } catch (IOException ex) { + Logger.printDebug(() -> "Could not read the " + table.code + " table", ex); + return null; + } + table.table = loaded; + return loaded; + } + + @Nullable + private static ReadingTable tableFor(String code) { + for (ReadingTable table : TABLES) { + if (table.code.equals(code)) return table; + } + return null; + } + + @Nullable + private static File tableFile(String name) { + File directory = RomanizationDict.directory(); + return directory == null ? null : new File(directory, name); + } + + private static void build(RomanizationDict.Listener listener) throws IOException { + File directory = RomanizationDict.directory(); + if (directory == null) throw new IOException("no cache directory"); + + boolean missing = false; + for (ReadingTable table : TABLES) { + File file = tableFile(table.tableName); + if (file != null && file.isFile()) { + table.table = CharReadingTable.load(file); + } else { + missing = true; + } + } + if (!missing) { + listener.onReady(entries()); + return; + } + + File archive = tableFile(ARCHIVE); + if (archive == null) throw new IOException("no cache directory"); + if (!archive.isFile()) { + RomanizationDict.download(URL, archive, ARCHIVE, listener); + } + extract(archive, listener); + //noinspection ResultOfMethodCallIgnored + archive.delete(); + + for (ReadingTable table : TABLES) { + RomanizationDict.writeNotice(directory, table.noticeName, + noticeTextFor(table.noticeField)); + } + listener.onReady(entries()); + } + + /** + * Reads the readings out of the archive, one line per character per language. A language + * that reads one field is written straight out; one that reads several keeps the best + * candidate of the character being read, since taking the first line instead would pick + * kHanyuPinlu, which is the poorer source and comes first alphabetically. + */ + private static void extract(File archive, RomanizationDict.Listener listener) + throws IOException { + File directory = RomanizationDict.directory(); + if (directory == null) throw new IOException("no cache directory"); + + File[] built = new File[TABLES.length]; + File[] targets = new File[TABLES.length]; + Writer[] writers = new Writer[TABLES.length]; + try { + for (int i = 0; i < TABLES.length; i++) { + File target = tableFile(TABLES[i].tableName); + built[i] = new File(target.getPath() + ".building"); + targets[i] = target; + writers[i] = new BufferedWriter(new OutputStreamWriter( + new FileOutputStream(built[i]), StandardCharsets.UTF_8), 1 << 16); + } + long size = archive.length(); + + // The best candidate of the character being read, one state per table. + StringBuilder[] pending = new StringBuilder[TABLES.length]; + char[] pendingChar = new char[TABLES.length]; + int[] pendingRank = new int[TABLES.length]; + for (int i = 0; i < TABLES.length; i++) { + pendingRank[i] = TABLES[i].sources.length; + } + + try (ZipInputStream zip = new ZipInputStream( + new BufferedInputStream(new FileInputStream(archive), 1 << 16))) { + ZipEntry entry; + while ((entry = zip.getNextEntry()) != null) { + if (entry.isDirectory() || !READINGS_ENTRY.equals(entry.getName())) continue; + + BufferedReader reader = new BufferedReader( + new InputStreamReader(zip, StandardCharsets.UTF_8), 1 << 16); + String line; + long done = 0; + while ((line = reader.readLine()) != null) { + done += line.length(); + + int firstTab = line.indexOf('\t'); + int secondTab = firstTab < 0 ? -1 : line.indexOf('\t', firstTab + 1); + if (secondTab < 0) continue; + + String field = line.substring(firstTab + 1, secondTab); + char c = codePoint(line.substring(0, firstTab)); + if (c == 0) continue; + + int row = row(field); + if (row < 0) continue; + + ReadingTable table = TABLES[row]; + if (table.sources.length == 1) { + String syllable = first(line.substring(secondTab + 1)); + if (syllable != null) { + writers[row].append(c).append(' ').append(syllable).append('\n'); + } + continue; + } + + int rank = rank(table, field); + if (rank < 0) continue; + + StringBuilder held = pending[row]; + if (held == null) { + held = new StringBuilder(); + pending[row] = held; + } + if (c != pendingChar[row]) { + writers[row].append(held); + held.setLength(0); + pendingChar[row] = c; + pendingRank[row] = table.sources.length; + } + if (rank >= pendingRank[row]) continue; + + String reading = first(line.substring(secondTab + 1)); + if (reading != null) { + held.append(c).append(' ').append(reading).append('\n'); + pendingRank[row] = rank; + } + + if (size > 0 && done % (1 << 20) < line.length()) { + listener.onBuilding((int) Math.min(100, done * 100 / size)); + } + } + for (int i = 0; i < TABLES.length; i++) { + if (pending[i] != null) writers[i].append(pending[i]); + } + listener.onBuilding(100); + } + } + } finally { + for (Writer writer : writers) { + if (writer != null) writer.close(); + } + } + + for (int i = 0; i < TABLES.length; i++) { + RomanizationDict.commit(built[i], targets[i]); + } + } + + /** The row of {@link #TABLES} a reading field belongs to, or -1 when it feeds none. */ + private static int row(String field) { + for (int i = 0; i < TABLES.length; i++) { + for (String source : TABLES[i].sources) { + if (source.equals(field)) return i; + } + } + return -1; + } + + /** How much a reading field is trusted within its table, best first. */ + private static int rank(ReadingTable table, String field) { + for (int i = 0; i < table.sources.length; i++) { + if (table.sources[i].equals(field)) return i; + } + return -1; + } + + private static char codePoint(String field) { + if (field.length() < 3 || field.charAt(0) != 'U' || field.charAt(1) != '+') return 0; + try { + int value = Integer.parseInt(field.substring(2), 16); + return value > 0 && value <= Character.MAX_VALUE ? (char) value : 0; + } catch (NumberFormatException ex) { + return 0; + } + } + + /** The first reading of a field, minus its frequency or location code, as in {@code wo(23213)}. */ + @Nullable + private static String first(String value) { + for (String candidate : value.split(",")) { + String reading = candidate.trim(); + int cut = reading.indexOf(':'); + if (cut >= 0) reading = reading.substring(cut + 1).trim(); + cut = reading.indexOf('('); + if (cut >= 0) reading = reading.substring(0, cut).trim(); + cut = reading.indexOf(' '); + if (cut > 0) reading = reading.substring(0, cut); + //noinspection SizeReplaceableByIsEmpty + if (reading.length() > 0) return reading.toLowerCase(Locale.ROOT); + } + return null; + } + + static String noticeTextFor(String field) { + return "Unicode Han Database readings\n" + + "https://www.unicode.org/Public/" + VERSION + "/ucd/Unihan.zip\n\n" + + "Licence: Unicode License, Version 3\n" + + "https://www.unicode.org/license.txt\n\n" + + "Copyright (c) 1991-2026 Unicode, Inc. All rights reserved.\n" + + "Distributed under the Terms of Use in https://www.unicode.org/copyright.txt\n\n" + + "What is stored here is the " + field + " reading of each character, taken from\n" + + "Unihan_Readings.txt in the file above.\n"; + } + + @Nullable + public static String notice() { + File directory = RomanizationDict.directory(); + if (directory == null) return null; + StringBuilder text = new StringBuilder(); + for (ReadingTable table : TABLES) { + String part = RomanizationDict.readNotice(directory, table.noticeName); + if (part == null) continue; + if (text.length() > 0) text.append("\n\n"); + text.append(part); + } + return text.length() > 0 ? text.toString() : null; + } + + /** + * One language's character readings: the Unihan fields that say them, best first, and + * what the table is stored and shown as. + */ + private static final class ReadingTable { + final String code; + final String[] sources; + final String noticeField; + final String tableName; + final String noticeName; + + @Nullable + volatile CharReadingTable table; + + ReadingTable(String code, String[] sources, String noticeField, String tableName, + String noticeName) { + this.code = code; + this.sources = sources; + this.noticeField = noticeField; + this.tableName = tableName; + this.noticeName = noticeName; + } + } +} diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/WordReadingTable.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/WordReadingTable.java index 584aefa41a..9fe55c7ff9 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/WordReadingTable.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/WordReadingTable.java @@ -19,12 +19,11 @@ import app.morphe.extension.shared.Logger; /** - * The reading of a word, held in a file rather than in the heap. There are hundreds of thousands - * of them per language and a lyric touches a few hundred, so the table is a hash table on disk - * and opening it maps the file, leaving the paging to the kernel. + * The reading of a word, held in a hash table on disk rather than in the heap: a language has + * hundreds of thousands of words and a lyric touches a few hundred, so opening maps the file and + * leaves the paging to the kernel. Readings pack one byte per kana, with an escape outside the + * katakana block; three bytes per kana would cost twenty megabytes more. * - *

    A reading is stored as one byte per kana, with an escape for anything outside the katakana - * block; three bytes per kana would have cost twenty megabytes more. *

    The file layout, all integers little endian: *

      *   0   magic, eight bytes, which say what dictionary wrote the file
    @@ -39,6 +38,7 @@
      */
     public final class WordReadingTable implements AutoCloseable {
     
    +    public static final int MAX_WORD_LENGTH = 12;
         static final int MAGIC_OFFSET = 0;
         static final int VERSION_OFFSET = 8;
         static final int SLOT_COUNT_OFFSET = 12;
    @@ -46,14 +46,10 @@ public final class WordReadingTable implements AutoCloseable {
         static final int RECORDS_OFFSET_FIELD = 20;
         static final int RECORDS_LENGTH_FIELD = 24;
         static final int HEADER_BYTES = 32;
    -
         // Clearing what the largest table asks for, since passing either ceiling drops the entries
         // that no longer fit rather than failing, and they would only be missed later.
         static final int MAX_FILE_BYTES = 128 << 20;
         private static final int MAX_SLOT_COUNT = 1 << 23;
    -
    -    public static final int MAX_WORD_LENGTH = 12;
    -
         private final MappedByteBuffer buffer;
         private final int recordsOffset;
         private final int entryCount;
    @@ -80,7 +76,8 @@ public static WordReadingTable open(Path file, byte[] magic) {
                         if (mapped.get(MAGIC_OFFSET + i) != magic[i]) return null;
                     }
                     int slots = mapped.getInt(SLOT_COUNT_OFFSET);
    -                if (slots <= 0 || slots > MAX_SLOT_COUNT || Integer.bitCount(slots) != 1) return null;
    +                if (slots <= 0 || slots > MAX_SLOT_COUNT || Integer.bitCount(slots) != 1)
    +                    return null;
                     return new WordReadingTable(mapped, slots);
                 }
             } catch (IOException ex) {
    @@ -89,6 +86,15 @@ public static WordReadingTable open(Path file, byte[] magic) {
             }
         }
     
    +    private static int hash(byte[] bytes) {
    +        int value = 0x811C9DC5;
    +        for (byte b : bytes) {
    +            value ^= b & 0xFF;
    +            value *= 0x01000193;
    +        }
    +        return value & 0x7FFFFFFF;
    +    }
    +
         /** The reading in katakana, empty for a word listed in kana, or null when absent. */
         public String reading(String surface) {
             byte[] query = surface.getBytes(StandardCharsets.UTF_8);
    @@ -143,17 +149,10 @@ public void close() {
             // still reachable.
         }
     
    -    private static int hash(byte[] bytes) {
    -        int value = 0x811C9DC5;
    -        for (byte b : bytes) {
    -            value ^= b & 0xFF;
    -            value *= 0x01000193;
    -        }
    -        return value & 0x7FFFFFFF;
    -    }
    -
    -    /** Writes a table. Words go in as they are read, so a dictionary of a few hundred
    -     * megabytes never has to be held anywhere. */
    +    /**
    +     * Writes a table. Words go in as they are read, so a dictionary of a few hundred
    +     * megabytes never has to be held anywhere.
    +     */
         public static final class Builder {
     
             private final Path file;
    @@ -185,6 +184,26 @@ public Builder(Path file, byte[] magic, int slotCount) throws IOException {
                 buffer.putInt(RECORDS_OFFSET_FIELD, recordsOffset);
             }
     
    +        private static byte[] pack(String reading) {
    +            byte[] out = new byte[reading.length() * 3];
    +            int length = 0;
    +            for (int i = 0; i < reading.length(); i++) {
    +                char c = reading.charAt(i);
    +                if (KanaRomanizer.isPackable(c)) {
    +                    out[length++] = KanaRomanizer.pack(c);
    +                } else {
    +                    if (length + 3 > out.length) break;
    +                    // Low byte first, which is the order a character is read back in.
    +                    out[length++] = 0;
    +                    out[length++] = (byte) c;
    +                    out[length++] = (byte) (c >>> 8);
    +                }
    +            }
    +            byte[] packed = new byte[length];
    +            System.arraycopy(out, 0, packed, 0, length);
    +            return packed;
    +        }
    +
             /** The first reading of a surface form wins: the one a listener is likelier to mean. */
             public void put(String surface, String reading) {
                 if (surface.isEmpty()) return;
    @@ -233,26 +252,6 @@ private boolean matches(int start, byte[] query, int length) {
                 return true;
             }
     
    -        private static byte[] pack(String reading) {
    -            byte[] out = new byte[reading.length() * 3];
    -            int length = 0;
    -            for (int i = 0; i < reading.length(); i++) {
    -                char c = reading.charAt(i);
    -                if (KanaRomanizer.isPackable(c)) {
    -                    out[length++] = KanaRomanizer.pack(c);
    -                } else {
    -                    if (length + 3 > out.length) break;
    -                    // Low byte first, which is the order a character is read back in.
    -                    out[length++] = 0;
    -                    out[length++] = (byte) c;
    -                    out[length++] = (byte) (c >>> 8);
    -                }
    -            }
    -            byte[] packed = new byte[length];
    -            System.arraycopy(out, 0, packed, 0, length);
    -            return packed;
    -        }
    -
             public int size() {
                 return entryCount;
             }
    diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/WordTableFile.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/WordTableFile.java
    index 7ac056a5a0..285bc99453 100644
    --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/WordTableFile.java
    +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/romanizers/WordTableFile.java
    @@ -15,19 +15,11 @@
     /** A {@link WordReadingTable} in the dictionary directory, opened on first use and kept open. */
     final class WordTableFile {
     
    -    /** Reads a downloaded source into the table being built. */
    -    interface Source {
    -        void addTo(File archive, WordReadingTable.Builder builder,
    -                RomanizationDict.Listener listener) throws IOException;
    -    }
    -
         /** How many entries a source reads between two progress reports. */
         static final int PROGRESS_EVERY = 20000;
    -
         private final String name;
         private final byte[] magic;
         private final int slotCount;
    -
         @Nullable
         private volatile WordReadingTable table;
     
    @@ -64,29 +56,57 @@ WordReadingTable open() {
             return open;
         }
     
    +    /** Drops the cached table so a later open re-reads from disk, or finds nothing. */
    +    void clear() {
    +        table = null;
    +    }
    +
         WordReadingTable.Builder newBuilder(File built) throws IOException {
             return new WordReadingTable.Builder(built.toPath(), magic, slotCount);
         }
     
         /** Downloads the source unless an earlier attempt left it behind, and builds the table. */
         void build(File directory, String url, String archiveName, Source source,
    -            RomanizationDict.Listener listener) throws IOException {
    +               RomanizationDict.Listener listener) throws IOException {
    +        build(directory, url, archiveName, source, listener, Long.MAX_VALUE);
    +    }
    +
    +    /**
    +     * As {@link #build(File, String, String, Source, RomanizationDict.Listener)}, refusing a
    +     * source larger than {@code maxBytes}.
    +     */
    +    void build(File directory, String url, String archiveName, Source source,
    +               RomanizationDict.Listener listener, long maxBytes) throws IOException {
             File target = new File(directory, name);
             if (target.isFile()) return;
     
             File archive = new File(directory, archiveName);
             if (!archive.isFile()) {
    -            RomanizationDict.download(url, archive, archiveName, listener);
    +            RomanizationDict.download(url, archive, archiveName, listener, maxBytes);
             }
     
             File built = new File(directory, name + ".building");
             WordReadingTable.Builder builder = newBuilder(built);
    -        source.addTo(archive, builder, listener);
    -        builder.finish(0);
    +        try {
    +            source.addTo(archive, builder, listener);
    +            builder.finish(0);
    +        } catch (IOException | RuntimeException ex) {
    +            // A source that cannot be read must not leave a partial table behind that the next
    +            // open would take for a finished one.
    +            //noinspection ResultOfMethodCallIgnored
    +            built.delete();
    +            throw ex;
    +        }
             //noinspection ResultOfMethodCallIgnored
             archive.delete();
     
             RomanizationDict.commit(built, target);
             table = null;
         }
    +
    +    /** Reads a downloaded source into the table being built. */
    +    interface Source {
    +        void addTo(File archive, WordReadingTable.Builder builder,
    +                   RomanizationDict.Listener listener) throws IOException;
    +    }
     }
    diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/session/LockScreenLyrics.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/session/LockScreenLyrics.java
    index 65c7be86f3..04b5dc9f4a 100644
    --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/session/LockScreenLyrics.java
    +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/session/LockScreenLyrics.java
    @@ -29,25 +29,19 @@
      * lock screen, Android Auto and Bluetooth displays. The artist field is rewritten to
      * {@code "artist - title"} so the track identity is preserved.
      *
    - * 

    The app's {@link MediaSession#setMetadata(MediaMetadata)} call site is observed to - * capture the {@link MediaSession} instance and the original metadata. Modified metadata is - * then pushed from a ticker via the captured session. Because that push goes through the - * framework directly, it does not re-enter the hooked app call site, so the lyrics and - * scrobbling observers (which read the original metadata at that site) are never affected. + *

    Observed at the app's {@link MediaSession#setMetadata(MediaMetadata)} call site to capture + * the session and the original metadata; modified metadata is pushed from a ticker via the + * framework directly, so it never re-enters the hooked call site and the lyrics/scrobbling + * observers reading the original metadata are unaffected. Other fields, notably album art, are + * preserved by copying the original metadata, including the display variants of title and artist. * - *

    All fields other than title and artist, notably the album art, are preserved by copying - * the original metadata with {@link MediaMetadata.Builder}. The display variants of the title - * and of the artist are rewritten as well, because the lock screen prefers them over the plain - * ones when both are present. - * - *

    The MediaSession push only supports Android 11 and above: Android 10 lock screens render - * the media notification (whose text is baked into the RemoteViews at post time) instead of - * reading session metadata, so a line pushed here would never be shown there. Android 10 and - * older are not adapted. + *

    Android 11+ only: Android 10 lock screens render the media notification (text baked into the + * RemoteViews at post time) instead of session metadata, so a pushed line would never be shown. */ @SuppressWarnings("unused") public final class LockScreenLyrics { + private static final long MAX_NUDGE_RECEIPT_AGE_MS = 6 * 60 * 60 * 1000L; @Nullable private static volatile WeakReference sessionRef; @Nullable @@ -58,24 +52,21 @@ public final class LockScreenLyrics { private static volatile String realTitle; @Nullable private static volatile String realArtist; - @Nullable private static String cachedCleanedTitle; @Nullable private static String cachedCleanedArtist; - /** Title and artist pushed on the last tick, to avoid redundant {@code setMetadata} calls. */ @Nullable private static volatile String lastPushedTitle; @Nullable private static String lastPushedArtist; - /** Set when the app pushes fresh metadata, so the next tick pushes it again. */ private static volatile boolean needsRepush; - /** Drives the periodic check that mirrors the current line into the MediaSession. */ private static final LyricsTicker ticker = new LyricsTicker(LockScreenLyrics::tick); - + @Nullable + private static volatile PlaybackState playbackState; /** * Re-arms the ticker whenever lyrics arrive or disappear, so a late lookup does not * leave the mirror stuck on the track title. @@ -85,15 +76,9 @@ public final class LockScreenLyrics { ticker.schedule(); } }; - - @Nullable - private static volatile PlaybackState playbackState; - private static volatile long observedPositionMs; private static volatile long observedAtElapsedMs; - private static final long MAX_NUDGE_RECEIPT_AGE_MS = 6 * 60 * 60 * 1000L; - private LockScreenLyrics() { } @@ -228,11 +213,10 @@ private static void push() { /** * Re-dispatches the app's playback state with a rebased position so media surfaces that - * refresh their metadata only when the playback state changes pick up the line that was - * just pushed. The position continues from the receipt-time reading on the - * {@link SystemClock#elapsedRealtime} timebase that {@link PlaybackState} mandates, so - * the system seek bar keeps advancing instead of jumping backwards. The push goes - * through the framework directly and does not re-enter the hooked app call site. + * refresh only on a state change pick up the line just pushed. The position continues from + * the receipt-time reading on the {@link SystemClock#elapsedRealtime} timebase + * {@link PlaybackState} mandates, so the seek bar keeps advancing instead of jumping + * backwards. Pushed through the framework, so the hooked app call site is not re-entered. */ private static void nudgePlaybackState(MediaSession session) { PlaybackState captured = playbackState; @@ -325,4 +309,6 @@ private static MediaMetadata buildMetadata(String title, String artist) { builder.putString(MediaMetadata.METADATA_KEY_DISPLAY_SUBTITLE, artist); return builder.build(); } + + } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/session/MiniplayerLyrics.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/session/MiniplayerLyrics.java index 018fafc979..68f62a29e9 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/session/MiniplayerLyrics.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/session/MiniplayerLyrics.java @@ -29,11 +29,9 @@ /** * Mirrors the currently sung lyric line into the in-app miniplayer: the title shows the current - * line and the subtitle shows {@code "artist - title"}. When no synced or word-level lyrics are - * available the app's own title and artist are left untouched. - * - *

    The miniplayer view hierarchy is captured from the constructor injection point, and a ticker - * updates the two {@link TextView}s as playback progresses. + * line and the subtitle shows {@code "artist - title"}. Without synced or word-level lyrics the + * app's own title and artist are left untouched. The views are captured at the constructor + * injection point and updated from a ticker as playback progresses. */ @SuppressWarnings("unused") public final class MiniplayerLyrics { @@ -74,16 +72,6 @@ public final class MiniplayerLyrics { @Nullable private static String lastMirrorSubtitle; - /** Drives the periodic check that mirrors the current line into the mini player. */ - private static final LyricsTicker ticker = new LyricsTicker(MiniplayerLyrics::tick); - - /** Listener that re-schedules the ticker immediately when lyrics finish loading. */ - private static final LyricsManager.Listener lyricsListener = (state, lyrics) -> { - if (state == LyricsManager.State.LOADED || state == LyricsManager.State.NOT_FOUND) { - ticker.schedule(); - } - }; - private MiniplayerLyrics() { } @@ -92,6 +80,9 @@ private static void disableFeature() { ticker.stop(); LyricsManager.getInstance().removeListener(lyricsListener); } + + /** Drives the periodic check that mirrors the current line into the mini player. */ + private static final LyricsTicker ticker = new LyricsTicker(MiniplayerLyrics::tick); /** * Starts or stops mirroring to match the current settings. Called when a lyrics setting @@ -132,6 +123,13 @@ public static void onMediaSessionSetMetadata(MediaSession session, MediaMetadata Logger.printException(() -> "onMediaSessionSetMetadata failure", ex); } } + + /** Listener that re-schedules the ticker immediately when lyrics finish loading. */ + private static final LyricsManager.Listener lyricsListener = (state, lyrics) -> { + if (state == LyricsManager.State.LOADED || state == LyricsManager.State.NOT_FOUND) { + ticker.schedule(); + } + }; /** * Injection point. Captures the miniplayer title and subtitle TextViews and (re)starts the @@ -255,13 +253,13 @@ private static void restoreIfMirrored() { /** The title the app itself shows: the untouched metadata title when it is known. */ private static String displayTitleOr(TrackInfo track) { - String raw = rawDisplayTitle; + final String raw = rawDisplayTitle; return raw != null && !raw.trim().isEmpty() ? raw : track.title(); } /** The artist the app itself shows: the untouched metadata artist when it is known. */ private static String displayArtistOr(TrackInfo track) { - String raw = rawDisplayArtist; + final String raw = rawDisplayArtist; return raw != null && !raw.trim().isEmpty() ? raw : track.artist(); } @@ -271,8 +269,8 @@ private static String displayArtistOr(TrackInfo track) { * fullwidth punctuation to ASCII, which must not leak into what is displayed. */ private static String buildSubtitle(TrackInfo track) { - String rawTitle = rawDisplayTitle; - String rawArtist = rawDisplayArtist; + final String rawTitle = rawDisplayTitle; + final String rawArtist = rawDisplayArtist; if (rawTitle == null || rawTitle.trim().isEmpty() || rawArtist == null || rawArtist.trim().isEmpty()) { return LyricsPresentation.trackLabel(track.title(), track.artist()); @@ -281,16 +279,18 @@ private static String buildSubtitle(TrackInfo track) { } private static void restoreField(TextView view, @Nullable String lastMirrored, - @Nullable CharSequence preMirror, @Nullable String fallback) { - CharSequence current = view.getText(); + @Nullable CharSequence preMirror, @Nullable String fallback) { + final CharSequence current = view.getText(); if (!TextUtils.equals(current, lastMirrored)) { return; } - CharSequence restore = (preMirror != null && !preMirror.toString().isEmpty()) + final CharSequence restore = (preMirror != null && !preMirror.toString().isEmpty()) ? preMirror : fallback; if (restore != null && !TextUtils.equals(current, restore)) { view.setText(restore); } } + + } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/storage/LyricsCache.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/storage/LyricsCache.java index 026ead0ed7..2dba8da4ca 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/storage/LyricsCache.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/storage/LyricsCache.java @@ -53,9 +53,6 @@ public final class LyricsCache { private static final int DISK_ENTRIES = 5000; private static final int TRIM_EVERY_WRITES = 64; - - private static int writesSinceTrim; - private static final String HEADER_PROVIDER = "#provider="; private static final String HEADER_SYNCED = "#synced="; private static final String HEADER_SOURCE_URL = "#sourceUrl="; @@ -70,7 +67,6 @@ public final class LyricsCache { private static final String HEADER_TRACK = "#track="; private static final String NOT_FOUND_MARKER = "#notfound"; private static final String RAW_SUFFIX = ".raw"; - private static final Map memoryCache = Collections.synchronizedMap( new LinkedHashMap<>(MEMORY_INITIAL_CAPACITY, 0.75f, true) { @Override @@ -78,20 +74,20 @@ protected boolean removeEldestEntry(Map.Entry eldest) { return size() > MEMORY_MAX_ENTRIES; } }); - /** * The video each entry was stored for, per cache key, mirroring {@link #memoryCache}. Null * means the entry was written without one and stays valid for whatever plays next. */ private static final Map videoIdByKey = Collections.synchronizedMap(new HashMap<>()); + private static int writesSinceTrim; private LyricsCache() { } @Nullable public static Lyrics get(TrackInfo track, String source, @Nullable String videoId) { - String key = key(track, source); + final String key = key(track, source); Lyrics lyrics = memoryCache.get(key); if (lyrics == null) { lyrics = readFromDisk(key); @@ -168,7 +164,7 @@ public static LyricsPreference getPreference(@Nullable String videoId, TrackInfo + " videoFile=" + name(videoFile) + " trackFile=" + name(file)); return null; } - File source = file; + final File source = file; Logger.printInfo(() -> "LyricsPref read: " + source.getName()); LyricsPreference preference = readPreferenceFile(source, @@ -248,16 +244,15 @@ private static LyricsPreference readPreferenceFile(File file, String prefKey) { } /** - * Stores what the user picked for this track. Read and written on a background thread. - * The lyric is written under the video id, which one playback never changes, and again - * under the track alone, which answers while the video id is not known yet. Both name - * the video they were written for, so neither answers a different upload of the same - * metadata, whose lyrics are the provider's to pick again. + * Stores what the user picked for this track, read and written on a background thread. The + * lyric is written under the video id, which one playback never changes, and again under the + * track alone, which answers before the video id is known. Both name the video they were + * written for, so neither answers a different upload of the same metadata. */ public static void putPreference(@Nullable String videoId, TrackInfo track, - @Nullable String queryTitle, - @Nullable String queryArtist, Lyrics preferred, - List queue, @Nullable String fingerprint) { + @Nullable String queryTitle, + @Nullable String queryArtist, Lyrics preferred, + List queue, @Nullable String fingerprint) { File file = preferenceFile(videoId, track); File fileByTrack = preferenceFile(null, track); if ((file == null && fileByTrack == null) || preferred == null @@ -374,7 +369,7 @@ private static List sanitizeTranslationList(@Nullable List list, boolean anyMeaningful = false; List sanitized = new ArrayList<>(list.size()); for (String s : list) { - if (OpenAIClient.isSkipToken(s)) { + if (s == null || OpenAIClient.isSkipToken(s)) { sanitized.add(""); } else { sanitized.add(s); @@ -390,19 +385,19 @@ private static List sanitizeTranslationList(@Nullable List list, @Nullable public static List getTranslation(TrackInfo track, - String source, - String language, - List sourceLines) { + String source, + String language, + List sourceLines) { File file = translationFile(track, source, language, sourceLines); List raw = readStringList(file, sourceLines); return sanitizeTranslationList(raw, file); } public static void putTranslation(TrackInfo track, - String source, - String language, - List sourceLines, - List lines) { + String source, + String language, + List sourceLines, + List lines) { File file = translationFile(track, source, language, sourceLines); List clean = sanitizeTranslationList(lines, null); if (clean != null) { @@ -414,15 +409,27 @@ public static void putTranslation(TrackInfo track, @Nullable public static List getTranslationAI(TrackInfo track, String source, - String language, List sourceLines) { - File file = aiTranslationFile(track, source, language, sourceLines); + String language, List sourceLines) { + return getTranslationAI(track, source, language, sourceLines, null); + } + + @Nullable + public static List getTranslationAI(TrackInfo track, String source, + String language, List sourceLines, @Nullable String provider) { + File file = aiTranslationFile(track, source, language, sourceLines, provider); List raw = readStringList(file, sourceLines); return sanitizeTranslationList(raw, file); } public static void putTranslationAI(TrackInfo track, String source, - String language, List sourceLines, List lines) { - File file = aiTranslationFile(track, source, language, sourceLines); + String language, List sourceLines, List lines) { + putTranslationAI(track, source, language, sourceLines, lines, null); + } + + public static void putTranslationAI(TrackInfo track, String source, + String language, List sourceLines, List lines, + @Nullable String provider) { + File file = aiTranslationFile(track, source, language, sourceLines, provider); List clean = sanitizeTranslationList(lines, null); if (clean != null) { writeStringList(file, clean); @@ -433,38 +440,51 @@ public static void putTranslationAI(TrackInfo track, String source, @Nullable public static List getSectionAI(TrackInfo track, String source, - List sourceLines) { + List sourceLines) { return readStringList(aiSectionFile(track, source, sourceLines), sourceLines); } public static void putSectionAI(TrackInfo track, String source, - List sourceLines, List sections) { + List sourceLines, List sections) { writeStringList(aiSectionFile(track, source, sourceLines), sections); } @Nullable public static List getRomanization(TrackInfo track, - String source, - List sourceLines) { + String source, + List sourceLines) { return readLyricsLineList(romanizationFile(track, source, sourceLines), sourceLines); } public static void putRomanization(TrackInfo track, - String source, - List sourceLines, - List lines) { + String source, + List sourceLines, + List lines) { writeLyricsLineList(romanizationFile(track, source, sourceLines), lines); } @Nullable public static List getRomanizationAI(TrackInfo track, String source, - List sourceLines) { - return readLyricsLineList(aiRomanizationFile(track, source, sourceLines), sourceLines); + List sourceLines) { + return getRomanizationAI(track, source, sourceLines, null); + } + + @Nullable + public static List getRomanizationAI(TrackInfo track, String source, + List sourceLines, @Nullable String provider) { + return readLyricsLineList( + aiRomanizationFile(track, source, sourceLines, provider), sourceLines); + } + + public static void putRomanizationAI(TrackInfo track, String source, + List sourceLines, List lines) { + putRomanizationAI(track, source, sourceLines, lines, null); } public static void putRomanizationAI(TrackInfo track, String source, - List sourceLines, List lines) { - writeLyricsLineList(aiRomanizationFile(track, source, sourceLines), lines); + List sourceLines, List lines, @Nullable String provider) { + writeLyricsLineList( + aiRomanizationFile(track, source, sourceLines, provider), lines); } @Nullable @@ -487,7 +507,7 @@ private static void writeStringList(@Nullable File file, List lines) { @Nullable private static List readLyricsLineList(@Nullable File file, - List sourceLines) { + List sourceLines) { return readCachedLines(file, sourceLines.size(), "Could not read lyrics line list from cache", line -> new LyricsLine(LyricsLine.NO_TIME, line)); @@ -499,7 +519,7 @@ private static List readLyricsLineList(@Nullable File file, */ @Nullable private static List readCachedLines(@Nullable File file, int expectedCount, - String logTag, Function mapper) { + String logTag, Function mapper) { if (file == null || !file.exists()) { return null; } @@ -545,15 +565,15 @@ private static void writeLyricsLineList(@Nullable File file, List li @Nullable private static File translationFile(TrackInfo track, String source, String language, - List sourceLines) { + List sourceLines) { return derivedCacheFile(track, source, "." + language + ".txt", sourceLines); } @Nullable private static File romanizationFile(TrackInfo track, String source, - List sourceLines) { + List sourceLines) { // Only a chosen language changes the result, so "automatic" keeps the file it had. - String language = Settings.LYRICS_ROMANIZE_LANGUAGE.get(); + final String language = Settings.LYRICS_ROMANIZE_LANGUAGE.get(); return derivedCacheFile(track, source, LyricsRequests.isDefaultRomanizationLanguage() ? ".rom.txt" : ".rom." + language.toLowerCase(Locale.ROOT) + ".txt", @@ -562,15 +582,18 @@ private static File romanizationFile(TrackInfo track, String source, @Nullable private static File aiTranslationFile(TrackInfo track, String source, String language, - List sourceLines) { + List sourceLines, @Nullable String provider) { + // A result cached from one provider must not be read back as another provider's answer. + final String providerPrefix = provider == null ? "" : provider + "."; return derivedCacheFile(track, source, - ".ai." + aiConfigKey(Settings.LYRICS_AI_TRANSLATION_PROMPT.get()) + ".ai." + providerPrefix + + aiConfigKey(Settings.LYRICS_AI_TRANSLATION_PROMPT.get()) + "." + language + ".txt", sourceLines); } @Nullable private static File aiSectionFile(TrackInfo track, String source, - List sourceLines) { + List sourceLines) { return derivedCacheFile(track, source, ".ai." + aiConfigKey(Settings.LYRICS_AI_SECTION_PROMPT.get()) + ".sec.txt", sourceLines); @@ -578,10 +601,13 @@ private static File aiSectionFile(TrackInfo track, String source, @Nullable private static File aiRomanizationFile(TrackInfo track, String source, - List sourceLines) { - // The prompt names the target language, so one file per language is needed. + List sourceLines, @Nullable String provider) { + // The prompt names the target language, so one file per language is needed. The + // provider is in the name because each provider answers with its own model. + final String providerPrefix = provider == null ? "" : provider + "."; return derivedCacheFile(track, source, - ".ai." + aiConfigKey(Settings.LYRICS_AI_ROMANIZATION_PROMPT.get()) + ".ai." + providerPrefix + + aiConfigKey(Settings.LYRICS_AI_ROMANIZATION_PROMPT.get()) + ".rom." + LyricsRequests.romanizationLanguage() + ".txt", sourceLines); } @@ -594,7 +620,7 @@ private static String aiConfigKey(String prompt) { @Nullable private static File derivedCacheFile(TrackInfo track, String source, String suffix, - List sourceLines) { + List sourceLines) { File directory = offlineDirectory(); if (directory == null) { return null; @@ -810,13 +836,6 @@ private static Lyrics parseContent(File file, List lines, int contentSta sidecarKey, header.about()); } - /** The header lines of a cached lyrics file and the index its content starts at. */ - private record Header(boolean notFound, String provider, boolean synced, - @Nullable String sourceUrl, @Nullable String formatType, - @Nullable List songwriters, @Nullable String about, - @Nullable String videoId, int contentStart) { - } - private static Header parseHeaders(List lines, int from) { String provider = ""; boolean synced = false; @@ -883,7 +902,7 @@ private static Lyrics parseContentLines(String content, boolean synced, String p // An LRC line cannot hold a reading on the word or the reading keyed by language, and // both are gone for good once written as one. Reading the file back brings them. if (TTML_FORMAT_TYPE.equals(formatType) && rawFormat != null && !rawFormat.isEmpty()) { - Lyrics reread = TTMLParser.ttmlToLyrics(rawFormat, provider, sourceUrl); + final Lyrics reread = TTMLParser.ttmlToLyrics(rawFormat, provider, sourceUrl); if (reread != null && !reread.isEmpty()) { return withAbout(reread, about); } @@ -957,7 +976,7 @@ private static void writeToDisk(String key, Lyrics lyrics) { try { List fileLines = new ArrayList<>(); - String videoId = videoIdByKey.get(key); + final String videoId = videoIdByKey.get(key); if (lyrics == Lyrics.NOT_FOUND || lyrics.isEmpty()) { // The marker ends the headers, so the video has to be named before it. appendVideoId(fileLines, videoId); @@ -1013,12 +1032,10 @@ private static String readRaw(File file) { } /** - * Deletes the oldest files once the cache grows past {@link #DISK_ENTRIES}. - *

    - * Providers are queried in parallel and each result is cached, so trims can overlap. A sort - * needs keys that hold still, and a file written or deleted meanwhile changes its modification - * time, so each time is read once before sorting. Trims also run one at a time, so two of them - * never delete the files the other is ordering. + * Deletes the oldest files once the cache grows past {@link #DISK_ENTRIES}. Providers are + * queried in parallel, so trims can overlap: a sort needs keys that hold still, and a file + * written or deleted meanwhile changes its modification time, so each time is read once + * before sorting. Trims run one at a time, so two never delete the files the other is ordering. */ private static synchronized void noteCacheWrite() { if (++writesSinceTrim < TRIM_EVERY_WRITES) { @@ -1055,8 +1072,8 @@ private static synchronized void trimDiskCache() { return; } - long[] modified = new long[mains.size()]; - Integer[] oldestFirst = new Integer[mains.size()]; + final long[] modified = new long[mains.size()]; + final Integer[] oldestFirst = new Integer[mains.size()]; for (int i = 0; i < mains.size(); i++) { modified[i] = mains.get(i).lastModified(); oldestFirst[i] = i; @@ -1125,12 +1142,11 @@ private static String preferenceKey(@Nullable String videoId, TrackInfo track) { * @return how many bytes were freed. */ public static long clear() { - long freed = 0; - File directory = offlineDirectory(); - File[] files = directory == null ? null : directory.listFiles(); + long freed = size(); + final File directory = offlineDirectory(); + final File[] files = directory == null ? null : directory.listFiles(); if (files != null) { for (File file : files) { - freed += file.isDirectory() ? 0 : file.length(); //noinspection ResultOfMethodCallIgnored file.delete(); } @@ -1139,8 +1155,28 @@ public static long clear() { return freed; } + /** How many bytes the stored lyrics occupy, measured the same way {@link #clear()} frees. */ + public static long size() { + long total = 0; + final File directory = offlineDirectory(); + final File[] files = directory == null ? null : directory.listFiles(); + if (files != null) { + for (File file : files) { + total += file.isDirectory() ? 0 : file.length(); + } + } + return total; + } + @Nullable private static File offlineDirectory() { return LyricsDirectories.directory(LyricsDirectories.OFFLINE); } + + /** The header lines of a cached lyrics file and the index its content starts at. */ + private record Header(boolean notFound, String provider, boolean synced, + @Nullable String sourceUrl, @Nullable String formatType, + @Nullable List songwriters, @Nullable String about, + @Nullable String videoId, int contentStart) { + } } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/storage/LyricsFileSaver.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/storage/LyricsFileSaver.java index 7db63230c7..ad758f4025 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/storage/LyricsFileSaver.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/storage/LyricsFileSaver.java @@ -157,10 +157,10 @@ private static String rebuildLrc(List lines) { StringBuilder sb = new StringBuilder(50 * lines.size()); for (LyricsLine line : lines) { sb.append('[') - .append(LRCParser.formatCentiseconds(line.startTimeMs())) - .append(']') - .append(line.text()) - .append('\n'); + .append(LRCParser.formatCentiseconds(line.startTimeMs())) + .append(']') + .append(line.text()) + .append('\n'); } return sb.toString(); } @@ -170,9 +170,9 @@ private static String rebuildLyricifyLines(List lines) { sb.append("[type:LyricifyLines]\n"); for (LyricsLine line : lines) { sb.append('[').append(line.startTimeMs()) - .append(',').append(line.endTimeMs()) - .append(']').append(line.text()) - .append('\n'); + .append(',').append(line.endTimeMs()) + .append(']').append(line.text()) + .append('\n'); } return sb.toString(); } @@ -189,8 +189,8 @@ private static String rebuildLyricifySyllable(List lines) { Word word = words.get(j); long durMs = word.endMs() - word.startMs(); sb.append(word.text()) - .append('(').append(word.startMs()) - .append(',').append(durMs).append(')'); + .append('(').append(word.startMs()) + .append(',').append(durMs).append(')'); } } else { sb.append(line.text()); diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/storage/TrackOffsetStore.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/storage/TrackOffsetStore.java index add760e13c..ccae0edd7e 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/storage/TrackOffsetStore.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/storage/TrackOffsetStore.java @@ -23,11 +23,9 @@ * The timing offset the user dragged out for one track, kept so a loop, a trip to another song * and a restart all find it again. * - *

    Keyed by the track alone: a different provider or a refreshed cache must not silently drop a - * value the user dialled in, and one that no longer fits is corrected by dragging as before. - * - *

    Kept apart from the lyrics on purpose: those are trimmed to a fixed number of entries, and an - * offset the user dialled in should not be one of the ones that goes. + *

    Keyed by the track alone: a different provider or a refreshed cache must not silently drop + * a value the user dialled in. Kept apart from the lyrics on purpose, since those are trimmed to + * a fixed number of entries. */ public final class TrackOffsetStore { @@ -58,7 +56,7 @@ public static void put(@NonNull TrackInfo track, int ms) { if (preferences == null) { return; } - String key = key(track); + final String key = key(track); SharedPreferences.Editor editor = preferences.edit(); if (ms == 0) { editor.remove(key); @@ -76,7 +74,7 @@ public static void put(@NonNull TrackInfo track, int ms) { private static List order(SharedPreferences preferences, String key, boolean present) { List order = new ArrayList<>(); - String stored = preferences.getString(KEY_ORDER, null); + final String stored = preferences.getString(KEY_ORDER, null); if (stored != null) { for (String entry : stored.split(SEPARATOR)) { if (!entry.isEmpty() && !entry.equals(key)) { diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/KaraokeHighlightState.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/KaraokeHighlightState.java index b6bec61fca..3e4a2f9465 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/KaraokeHighlightState.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/KaraokeHighlightState.java @@ -17,32 +17,9 @@ */ final class KaraokeHighlightState { - /** Word timings of the built lines, read without holding their views. */ - interface Lines { - int size(); - - boolean wordsEmpty(int line); - - long firstStart(int line); - - long lastEnd(int line); - } - - enum Kind { - /** Paint the word colors at a position, propagating to background lines. */ - PAINT, - /** Drop the word overlay, background lines included. */ - CLEAR, - CLEAR_FADE, - /** Fade to the inactive alpha, word overlay untouched. */ - FADE, - /** Restore non-karaoke colors while word sync is being turned off. */ - RESET_ACTIVE, - RESET_INACTIVE - } - - /** One queued effect on one line. */ - record Action(Kind kind, int line, long posMs, boolean allSung) {} + private int lastLine = -1; + private int pendingLine = -1; + private boolean wasEnabled = true; static boolean stillLit(Lines lines, int line, long pos) { if (line < 0 || line >= lines.size() || lines.wordsEmpty(line)) { @@ -51,10 +28,6 @@ static boolean stillLit(Lines lines, int line, long pos) { return pos < lines.lastEnd(line) && pos >= lines.firstStart(line); } - private int lastLine = -1; - private int pendingLine = -1; - private boolean wasEnabled = true; - /** * Runs one tick, appending actions in the order the view has to run them. The caller * owns {@code out} and clears it after execution. @@ -157,4 +130,32 @@ void resetContent() { void syncEnabled(boolean enabled) { wasEnabled = enabled; } + + enum Kind { + /** Paint the word colors at a position, propagating to background lines. */ + PAINT, + /** Drop the word overlay, background lines included. */ + CLEAR, + CLEAR_FADE, + /** Fade to the inactive alpha, word overlay untouched. */ + FADE, + /** Restore non-karaoke colors while word sync is being turned off. */ + RESET_ACTIVE, + RESET_INACTIVE + } + + /** Word timings of the built lines, read without holding their views. */ + interface Lines { + int size(); + + boolean wordsEmpty(int line); + + long firstStart(int line); + + long lastEnd(int line); + } + + /** One queued effect on one line. */ + record Action(Kind kind, int line, long posMs, boolean allSung) { + } } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsColors.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsColors.java index 7325e29e42..5721cb932d 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsColors.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsColors.java @@ -8,6 +8,7 @@ package app.morphe.extension.music.patches.lyrics.ui; import android.graphics.Color; + import app.morphe.extension.shared.ResourceType; import app.morphe.extension.shared.ResourceUtils; import app.morphe.extension.shared.theme.ThemeUtils; @@ -21,14 +22,18 @@ final class LyricsColors { /** Dimmed until the word is sung. */ static final float UNSUNG_ALPHA = 0.35f; - /** The reading of the active line once it is lit, or all along when it is not read word by word. */ + /** Smaller than the lyric it reads, so at the same weight the two would read as one line. */ static final float ACTIVE_READING_ALPHA = 0.85f; - /** The translation of the active line. */ + /** The translation sits between the lyric and its reading in prominence. */ static final float ACTIVE_TRANSLATION_ALPHA = 0.6f; - /** A background vocal while it is shown. */ + /** A background vocal is drawn on its own, so it is not dimmed with the lead line. */ static final float ACTIVE_BG_ALPHA = 0.8f; + private static int cachedLineColor; + private static int cachedUnsungColor; + private static int cachedSecondaryColor; + private static boolean colorCacheValid; static int secondaryTextColor() { ensureColorCache(); @@ -40,14 +45,9 @@ private static int computeSecondaryColor(int base) { if (secondary != 0) { return secondary; } - return Color.argb(0x99, Color.red(base), Color.green(base), Color.blue(base)); + return Color.argb(0x66, Color.red(base), Color.green(base), Color.blue(base)); } - private static int cachedLineColor; - private static int cachedUnsungColor; - private static int cachedSecondaryColor; - private static boolean colorCacheValid; - private static void ensureColorCache() { if (!colorCacheValid) { final int base = computeLineColor(); diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsFit.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsFit.java index 83054bbd5b..b8cf8337dc 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsFit.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsFit.java @@ -12,7 +12,9 @@ import android.text.StaticLayout; import android.text.TextPaint; import android.widget.TextView; + import androidx.annotation.Nullable; + import java.util.Objects; final class LyricsFit { @@ -22,8 +24,9 @@ final class LyricsFit { private static final float FIT_MIN_LETTER_SPACING_EM = -0.05f; private static final float FIT_WIDTH_AXIS_STEP = 1.5f; private static final float FIT_MIN_WIDTH_AXIS = 85f; + private static Boolean widthAxisSupport; -private static boolean hasOrphanTail(Layout layout, CharSequence text) { + private static boolean hasOrphanTail(Layout layout, CharSequence text) { final int length = text.length(); int segmentStart = 0; while (segmentStart < length) { @@ -70,13 +73,13 @@ private static boolean hasOrphanTail(Layout layout, CharSequence text) { } private static Layout buildCandidateLayout(TextView view, int sizePx, int width, - CharSequence text, float letterSpacingEm, @Nullable String fontVariation) { - TextPaint paint = new TextPaint(view.getPaint()); + CharSequence text, float letterSpacingEm, @Nullable String fontVariation) { + final TextPaint paint = new TextPaint(view.getPaint()); paint.setTextSize(sizePx); paint.setLetterSpacing(letterSpacingEm); paint.setFontVariationSettings(fontVariation); - Layout current = view.getLayout(); - Layout.Alignment alignment = current != null + final Layout current = view.getLayout(); + final Layout.Alignment alignment = current != null ? current.getAlignment() : Layout.Alignment.ALIGN_NORMAL; return StaticLayout.Builder.obtain(text, 0, text.length(), paint, width) .setAlignment(alignment) @@ -86,13 +89,11 @@ private static Layout buildCandidateLayout(TextView view, int sizePx, int width, .build(); } - private static Boolean widthAxisSupport; - private static boolean widthAxisSupported(TextPaint paint) { if (widthAxisSupport == null) { - TextPaint probe = new TextPaint(paint); + final TextPaint probe = new TextPaint(paint); probe.setTextSize(100f); - String[] sample = {"WAVEWIDTH", "MOMENT", "LONGERSONGTEXT"}; + final String[] sample = {"WAVEWIDTH", "MOMENT", "LONGERSONGTEXT"}; float before = 0f; for (String part : sample) { before += probe.measureText(part); @@ -107,28 +108,16 @@ private static boolean widthAxisSupported(TextPaint paint) { return widthAxisSupport; } - private record CompressionFit(float letterSpacingEm, @Nullable String fontVariation) {} - - static final class FittedState { - int width = -1; - int textSizePx; - float letterSpacingEm; - @Nullable - String fontVariation; - @Nullable - CharSequence text; - } - @Nullable private static CompressionFit findCompression(TextView view, int sizePx, int width, - CharSequence text) { + CharSequence text) { if (!widthAxisSupported(view.getPaint())) { return null; } for (int i = 1; i <= FIT_MAX_STEPS; i++) { final float spacing = Math.max(FIT_MIN_LETTER_SPACING_EM, -i * FIT_LETTER_SPACING_STEP_EM); - String variation = "'wdth' " + final String variation = "'wdth' " + Math.max(FIT_MIN_WIDTH_AXIS, 100f - i * FIT_WIDTH_AXIS_STEP); if (!hasOrphanTail(buildCandidateLayout(view, sizePx, width, text, spacing, variation), text)) { @@ -139,7 +128,7 @@ private static CompressionFit findCompression(TextView view, int sizePx, int wid } static boolean applyOrphanFit(TextView view, int width, CharSequence text, - FittedState state) { + FittedState state) { final int sizePx = (int) view.getTextSize(); if (width == state.width && sizePx == state.textSizePx && view.getLetterSpacing() == state.letterSpacingEm @@ -150,16 +139,18 @@ static boolean applyOrphanFit(TextView view, int width, CharSequence text, if (base == null) { return false; } - if (view.getLetterSpacing() != 0f || view.getFontVariationSettings() != null) { + final String currentVariation = view.getFontVariationSettings(); + final String weightVariation = extractWeightVariation(currentVariation); + if (view.getLetterSpacing() != 0f || currentVariation != null) { base = buildCandidateLayout(view, sizePx, width, text, 0f, null); } float spacing = 0f; - String variation = null; + String variation = weightVariation; if (hasOrphanTail(base, text)) { - CompressionFit fit = findCompression(view, sizePx, width, text); + final CompressionFit fit = findCompression(view, sizePx, width, text); if (fit != null) { spacing = fit.letterSpacingEm; - variation = fit.fontVariation; + variation = combineAxes(weightVariation, fit.fontVariation); } } final boolean changed = view.getLetterSpacing() != spacing @@ -181,6 +172,51 @@ static boolean applyOrphanFit(TextView view, int width, CharSequence text, return changed; } + /** Pulls the bold weight set by {@code applyLyricsBoldPaint} out of a variation setting. */ + @Nullable + private static String extractWeightVariation(@Nullable String settings) { + if (settings == null) { + return null; + } + final int tag = settings.indexOf("'wght'"); + if (tag < 0) { + return null; + } + int valueStart = tag + "'wght'".length(); + while (valueStart < settings.length() + && Character.isWhitespace(settings.charAt(valueStart))) { + valueStart++; + } + int valueEnd = valueStart; + while (valueEnd < settings.length() + && (Character.isDigit(settings.charAt(valueEnd)) || settings.charAt(valueEnd) == '.')) { + valueEnd++; + } + return valueEnd == valueStart ? null : settings.substring(tag, valueEnd); + } + + /** Keeps the bold weight when a compression fit sets its own width axis. */ + @Nullable + private static String combineAxes(@Nullable String weight, @Nullable String fit) { + if (weight == null) { + return fit; + } + return fit == null ? weight : fit + ", " + weight; + } + + private record CompressionFit(float letterSpacingEm, @Nullable String fontVariation) { + } + + static final class FittedState { + int width = -1; + int textSizePx; + float letterSpacingEm; + @Nullable + String fontVariation; + @Nullable + CharSequence text; + } + static final class FittingTextView extends TextView { private final FittedState fittedState = new FittedState(); @@ -205,7 +241,7 @@ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { if (width <= 0) { return; } - CharSequence text = getText(); + final CharSequence text = getText(); if (text == null || text.length() == 0) { return; } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsLineView.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsLineView.java index 45361a817b..b128241018 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsLineView.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsLineView.java @@ -197,6 +197,9 @@ private void updateSecondaryMargin(int marginPx) { * @param instant Skips the animation, for a row that was only just built. */ void animateSecondaryExpansion(boolean expand, boolean instant) { + if (!Settings.LYRICS_SPLIT_BACKGROUND.get()) { + return; + } // Called on every highlight tick, so one already on its way there is left to finish. if (!instant && expand == secondaryExpanded && (secondaryAnimator != null || secondaryFraction == (expand ? 1f : 0f))) { @@ -683,7 +686,7 @@ private void markSecondaryLines(Layout layout, int start, int end, boolean[] fla } private void drawTextRun(Canvas canvas, Layout layout, int firstLine, int lastLine, - int alpha, int tintColor) { + int alpha, int tintColor) { if (firstLine > lastLine || alpha <= 0) { return; } @@ -757,8 +760,8 @@ private boolean drawBaseText(Canvas canvas) { final int secondaryAlpha = Math.round(lineAlpha * secondaryBaseAlpha * contentNow); final float readingBaseAlpha = activeLine ? (perWordReading && Settings.LYRICS_WORD_SYNC.get() - ? LyricsColors.UNSUNG_ALPHA * 255f - : LyricsColors.ACTIVE_READING_ALPHA * 255f) + ? LyricsColors.UNSUNG_ALPHA * 255f + : LyricsColors.ACTIVE_READING_ALPHA * 255f) : Color.alpha(LyricsColors.secondaryTextColor()); final int romaAlpha = Math.round(lineAlpha * readingBaseAlpha * contentNow); @@ -1099,12 +1102,13 @@ protected void onDraw(Canvas canvas) { // syllable it reads and pushes the rest of the line along to make room. /** A word timing and where it sits in the laid out lyric. */ - private record AlignedSyllable(LyricsSpanBuilder.WordTiming syllable, int startOffset, int endOffset) { + private record AlignedSyllable(LyricsSpanBuilder.WordTiming syllable, int startOffset, + int endOffset) { } /** One syllable's reading, laid out on its own. */ private record RomanizedWordCluster(int wordIndex, int startIndex, int lineIdx, - StaticLayout subLayout, float romanWidth) { + StaticLayout subLayout, float romanWidth) { } /** One character of the lyric as the main layout places it. */ @@ -1113,7 +1117,7 @@ private record ClusterMeta(int start, int end, int lineIdx, float x, float width /** A run of the lyric drawn in one piece: a whole syllable, or a character between syllables. */ private record VisualCluster(int lineIdx, float x, float y, float width, StaticLayout subLayout, - int syllableIndex, int shiftIndex) { + int syllableIndex, int shiftIndex) { } /** Where each visual cluster is drawn once the readings have pushed it along. */ @@ -1150,12 +1154,12 @@ private static final class SyllableRomanCache { final int totalHeightPx; SyllableRomanCache(int widthPx, float fontSizePx, boolean hasSyllableRomanization, - @Nullable String mainText, int lineCount, List alignedSyllables, - int extraRomanHeightPx, List clusters, ClusterStatics statics, - RomanLineStatics[] romanLines, int[] sylLine, - float[] sylShiftedLeadX, float[] sylShiftedTrailX, - @Nullable String transText, @Nullable StaticLayout transLayout, - int transTopY, int mainTopY, int totalHeightPx) { + @Nullable String mainText, int lineCount, List alignedSyllables, + int extraRomanHeightPx, List clusters, ClusterStatics statics, + RomanLineStatics[] romanLines, int[] sylLine, + float[] sylShiftedLeadX, float[] sylShiftedTrailX, + @Nullable String transText, @Nullable StaticLayout transLayout, + int transTopY, int mainTopY, int totalHeightPx) { this.widthPx = widthPx; this.fontSizePx = fontSizePx; this.hasSyllableRomanization = hasSyllableRomanization; @@ -2000,8 +2004,8 @@ private void drawSyllableRomanizedLine(Canvas canvas, SyllableRomanCache cache) final int transAlpha = Math.round(lineAlpha * secondaryBaseAlpha * contentNow * transReveal); final float readingBaseAlpha = activeLine ? (Settings.LYRICS_WORD_SYNC.get() && unsungColor != 0 && !wordTimings.isEmpty() - ? LyricsColors.UNSUNG_ALPHA * 255f - : LyricsColors.ACTIVE_READING_ALPHA * 255f) + ? LyricsColors.UNSUNG_ALPHA * 255f + : LyricsColors.ACTIVE_READING_ALPHA * 255f) : Color.alpha(LyricsColors.secondaryTextColor()); final int romaUnsungAlpha = Math.round(lineAlpha * readingBaseAlpha * contentNow * romaReveal); diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsPanelInstaller.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsPanelInstaller.java index 65778b310e..94a99b159f 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsPanelInstaller.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsPanelInstaller.java @@ -58,23 +58,15 @@ public final class LyricsPanelInstaller { /** How long the panel is waited for before the attempt is given up. */ private static final long INSTALL_TIMEOUT_MILLISECONDS = 2000; - + private static final long[] ENABLE_BUTTON_DELAYS_MS = {0, 150, 500, 1000, 2000}; + private static final long ENABLE_BUTTON_MIN_INTERVAL_MS = 2000; /** Collapses the many component callbacks of one panel opening into one attempt. */ private static boolean installPending; - private static WeakReference panelReference = new WeakReference<>(null); - /** Panel the app currently has in the engagement panel container. */ private static WeakReference currentPanelReference = new WeakReference<>(null); - /** Panel the lyrics were last built into, kept to recognize it when it comes back. */ private static WeakReference lyricsPanelReference = new WeakReference<>(null); - - private LyricsPanelInstaller() { - } - - private static boolean settingsListenerRegistered; - private static final SharedPreferences.OnSharedPreferenceChangeListener SETTINGS_LISTENER = (prefs, key) -> { if (key == null || !key.startsWith("morphe_music_lyrics")) { @@ -82,6 +74,12 @@ private LyricsPanelInstaller() { } Utils.runOnMainThreadNowOrLater(() -> onLyricsSettingChanged(key)); }; + private static boolean settingsListenerRegistered; + private static long lastEnableButtonWalkUptimeMs; + private static boolean enableButtonWalkPending; + + private LyricsPanelInstaller() { + } public static void registerSettingsListener() { if (settingsListenerRegistered) { @@ -107,6 +105,7 @@ private static void onLyricsSettingChanged(String key) { return; } if (Settings.LYRICS_SOURCE.key.equals(key) + || Settings.LYRICS_SPLIT_BACKGROUND.key.equals(key) || LyricsFilterDefaults.LYRICS_CUSTOM_REGEX.key.equals(key) || LyricsFilterDefaults.LYRICS_TEXT_FILTER.key.equals(key) || LyricsFilterDefaults.LYRICS_CREDIT_LINE_REGEX.key.equals(key)) { @@ -138,18 +137,30 @@ private static void onLyricsSettingChanged(String key) { LockScreenLyrics.onSettingsChanged(); return; } - LyricsPanelView panelView = panelReference.get(); + final LyricsPanelView panelView = panelReference.get(); if (panelView == null) { return; } + final boolean translateButtonOff = + Settings.LYRICS_SHOW_TRANSLATE_BUTTON.key.equals(key) + && !Settings.LYRICS_SHOW_TRANSLATE_BUTTON.get(); + final boolean romanizeButtonOff = + Settings.LYRICS_SHOW_ROMANIZE_BUTTON.key.equals(key) + && !Settings.LYRICS_SHOW_ROMANIZE_BUTTON.get(); + if (translateButtonOff || romanizeButtonOff) { + panelView.releaseOnlyModesForHiddenButtons(translateButtonOff, romanizeButtonOff); + } if (Settings.LYRICS_SHOW_COPY_BUTTON.key.equals(key) || Settings.LYRICS_SHOW_TRANSLATE_BUTTON.key.equals(key) || Settings.LYRICS_SHOW_ROMANIZE_BUTTON.key.equals(key) || Settings.LYRICS_SHOW_REFRESH_BUTTON.key.equals(key)) { panelView.applyToolbarButtonSettings(); - } else if (Settings.LYRICS_TEXT_SIZE.key.equals(key)) { + } else if (Settings.LYRICS_TEXT_SIZE.key.equals(key) + || Settings.LYRICS_LINE_SCALE.key.equals(key)) { panelView.applyTextSize(); - } else if (formatting || Settings.LYRICS_SWAP_TRANS_ROMA.key.equals(key) + panelView.applyLineScale(); + } else if (formatting || Settings.LYRICS_FORCE_CENTER.key.equals(key) + || Settings.LYRICS_SWAP_TRANS_ROMA.key.equals(key) || Settings.LYRICS_TAP_TO_SEEK.key.equals(key)) { // These are read while the line is laid out, so the lines need rebuilding. panelView.applyLyricLayoutSettings(); @@ -170,7 +181,7 @@ private static void onLyricsSettingChanged(String key) { private static void uninstallLyricsPanel() { installPending = false; - LyricsPanelView panelView = panelReference.get(); + final LyricsPanelView panelView = panelReference.get(); if (panelView == null) { return; } @@ -376,7 +387,7 @@ public static boolean isLyricsPanelOpen() { return false; } TextView title = findForegroundTitle(activity.getWindow().getDecorView()); - return title != null && isLyricsTitle(title); + return isLyricsTitle(title); } public static boolean isOtherPanelForeground() { @@ -518,12 +529,11 @@ private static T findForegroundView(View view, int targetId, Re } /** - * The heading the app gives the lyrics panel, in the language on screen. - * - *

    Read through the resources that drew it rather than through {@link ResourceUtils}: the app - * can be set to a language of its own, and a string then comes back in whichever language its - * context was configured with, which is not always the one being drawn in. Not cached across - * calls either, since a language change would leave a stale answer behind. + * The heading the app gives the lyrics panel, in the language on screen. Read through the + * resources that drew it rather than {@link ResourceUtils}: the app can be set to a language + * of its own, and a string then comes back in whichever language its context was configured + * with, which is not always the one being drawn. Not cached, since a language change would + * leave a stale answer behind. */ @Nullable private static String lyricsTitle(@Nullable Context context) { @@ -543,13 +553,6 @@ private static String lyricsTitle(@Nullable Context context) { } } - private static final long[] ENABLE_BUTTON_DELAYS_MS = {0, 150, 500, 1000, 2000}; - - private static final long ENABLE_BUTTON_MIN_INTERVAL_MS = 2000; - - private static long lastEnableButtonWalkUptimeMs; - private static boolean enableButtonWalkPending; - public static void enableLyricsButton() { try { walkForLyricsButton(); @@ -575,13 +578,13 @@ private static void walkForLyricsButton() { if (title == null) { return; } - View root = activity.getWindow().getDecorView(); + final View root = activity.getWindow().getDecorView(); enableButtonWalkPending = true; lastEnableButtonWalkUptimeMs = now; scheduleEnableButtonPass(root, title, 0); } - private static void scheduleEnableButtonPass(View root, String title, int step) { + private static void scheduleEnableButtonPass(final View root, final String title, final int step) { final long delay = step == 0 ? ENABLE_BUTTON_DELAYS_MS[0] : ENABLE_BUTTON_DELAYS_MS[step] - ENABLE_BUTTON_DELAYS_MS[step - 1]; diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsPanelView.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsPanelView.java index 550f0971c2..6323bad6b3 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsPanelView.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsPanelView.java @@ -30,6 +30,7 @@ import android.os.Looper; import android.os.SystemClock; import android.text.Layout; +import android.text.method.LinkMovementMethod; import android.text.Spannable; import android.text.SpannableString; import android.text.Spanned; @@ -68,8 +69,10 @@ import app.morphe.extension.music.patches.lyrics.LyricsManager; import app.morphe.extension.music.patches.lyrics.LyricsPresentation; +import app.morphe.extension.music.patches.lyrics.derive.AiProviders; import app.morphe.extension.music.patches.lyrics.derive.LyricsRomanizer; import app.morphe.extension.music.patches.lyrics.derive.LyricsTranslator; +import app.morphe.extension.music.patches.lyrics.romanizers.RimeDictStore; import app.morphe.extension.music.patches.lyrics.derive.QuickLRCClient; import app.morphe.extension.music.patches.lyrics.model.Lyrics; import app.morphe.extension.music.patches.lyrics.model.LyricsBracketSplitter; @@ -85,6 +88,7 @@ import app.morphe.extension.shared.Logger; import app.morphe.extension.shared.ResourceUtils; import app.morphe.extension.shared.Utils; +import app.morphe.extension.shared.settings.BaseSettings; import app.morphe.extension.shared.settings.SharedYouTubeSettings; import app.morphe.extension.shared.theme.ThemeUtils; import app.morphe.extension.shared.ui.CustomDialog; @@ -224,7 +228,7 @@ public final class LyricsPanelView extends FrameLayout implements LyricsManager. @Nullable private TextView dialogAlignButton; - enum OnlyMode { NONE, TRANS, ROMA, BOTH } + enum OnlyMode {NONE, TRANS, ROMA, BOTH} private OnlyMode onlyMode = OnlyMode.NONE; @@ -493,6 +497,7 @@ public LyricsPanelView(Context context) { footerView = new LyricsFit.FittingTextView(context); LyricsViewSupport.applyFooterStyle(footerView); + footerView.setMovementMethod(LinkMovementMethod.getInstance()); footerView.setVisibility(GONE); // Same order as the buttons the app draws under its own lyrics. @@ -724,7 +729,7 @@ private void onOffsetLabelClicked() { } String videoId = manager.skipSegmentsVideoId(); if (videoId == null) { - Utils.showToastShort(str("morphe_music_lyrics_sb_no_video")); + toastError(str("morphe_music_lyrics_sb_no_video")); return; } sbRecording = true; @@ -801,7 +806,7 @@ public void onLyricsChanged(LyricsManager.State state, @Nullable Lyrics newLyric && newLyrics != null && !newLyrics.isEmpty()) { return; } -if (state == LyricsManager.State.LOADED && newLyrics == lyrics + if (state == LyricsManager.State.LOADED && newLyrics == lyrics && newLyrics != null && !newLyrics.isEmpty()) { if (refreshInProgress) { refreshInProgress = false; @@ -1243,13 +1248,13 @@ private void buildLyrics(Lyrics newLyrics) { boolean lineRtl = ScriptDetector.isLineRtl(l.text()); boolean isDuetLine = l.isDuet() || (isSecFlags[i] && curParentForAlign >= 0 - && newLyrics.lines().get(curParentForAlign).isDuet()); + && newLyrics.lines().get(curParentForAlign).isDuet()); lineRightAligned[i] = isDuetSong - ? (lineRtl ? !isDuetLine : isDuetLine) + ? (lineRtl != isDuetLine) : lineRtl; } - final int duetSideInset = Math.max(Dim.dp(48), - (int) (context.getResources().getDisplayMetrics().widthPixels * 0.18f)); + final int duetSideInset = Math.max(Dim.dp(24), + (int) (context.getResources().getDisplayMetrics().widthPixels * 0.09f)); LinearLayout currentParentRow = null; int currentParentIdx = -1; @@ -1678,7 +1683,6 @@ private void buildLyrics(Lyrics newLyrics) { currentSourceUrl = newLyrics.sourceUrl(); updateFooter(); - footerView.setOnClickListener(view -> onSourceClicked()); List songwriters = newLyrics.songwriters(); String about = newLyrics.about(); @@ -1773,7 +1777,7 @@ public boolean onPreDraw() { - (preserved ? Math.round(preserveScreenY) : scrollView.getHeight() - / SCROLL_OFFSET_FRACTION))); + / SCROLL_OFFSET_FRACTION))); scrollView.scrollTo(0, y); lastScrollTarget = y; growthAnchorValid = true; @@ -1889,7 +1893,7 @@ private void onTranslateClicked() { translatedAiModel = model; } if (lines == null) { - Utils.showToastShort(str("morphe_music_lyrics_translate_failed")); + toastError(str("morphe_music_lyrics_translate_failed")); } showLyrics(current); if (translatedLines != null) { @@ -2023,7 +2027,7 @@ private void requestTranslateOnly(Lyrics current) { translatedAiModel = model; } if (translatedLines == null) { - Utils.showToastShort(str("morphe_music_lyrics_translate_failed")); + toastError(str("morphe_music_lyrics_translate_failed")); fallBackFromDualMode(false); return; } @@ -2051,6 +2055,43 @@ private void requestTranslateOnly(Lyrics current) { }); } + private void requestNormalTranslate(Lyrics current, TrackInfo track) { + Settings.LYRICS_TRANSLATE.save(true); + Settings.LYRICS_TRANSLATE_ONLY.save(false); + translateInProgress = true; + final int requestId = ++translateRequestId; + setButtonLabel(translateView, str("morphe_music_lyrics_translating"), true); + + LyricsTranslator.translate(track, current, current.providerName(), + (lines, fromGoogle, fromAI, model) -> { + if (requestId != translateRequestId || !translateInProgress) { + return; + } + translateInProgress = false; + + // The track moved on while the translation was in flight. + if (lyrics != current) { + return; + } + + translatedLines = LyricsViewSupport.hasTranslation(lines, current.lines()) ? lines : null; + translatedFromGoogle = translatedLines != null && fromGoogle; + translatedFromAI = translatedLines != null && fromAI; + if (translatedFromAI && model != null) { + translatedAiModel = model; + } + if (lines == null) { + toastError(str("morphe_music_lyrics_translate_failed")); + } + showLyrics(current); + if (translatedLines != null) { + flashButtonLabel(translateView, + str("morphe_music_lyrics_translate_hide"), + updateTranslateLabelRunnable, 3000); + } + }); + } + private void cancelTranslateOnly() { clearTranslateOnlyState(); Lyrics current = lyrics; @@ -2083,6 +2124,49 @@ private void clearRomanizeOnlyState() { updateFooter(); } + /** Clears every only mode and its flags, then rebuilds from the lyrics alone. */ + private void exitOnlyModes() { + Settings.LYRICS_TRANSLATE_ONLY.save(false); + Settings.LYRICS_ROMANIZE_ONLY.save(false); + Settings.LYRICS_TRANSLATE.save(false); + Settings.LYRICS_ROMANIZE.save(false); + cancelNormalTranslate(); + cancelNormalRomanize(); + onlyMode = OnlyMode.NONE; + applyOnlyModeState(); + updateDualPill(); + final Lyrics current = lyrics; + if (current != null) { + showLyrics(current); + } + updateTranslateLabel(); + updateRomanizeLabel(); + updateFooter(); + } + + /** + * Leaves the only modes held by a hidden button, so a mode reached from a button goes when + * the button does. + */ + public boolean releaseOnlyModesForHiddenButtons(boolean translateOff, boolean romanizeOff) { + if (!translateOff && !romanizeOff) { + return false; + } + final boolean translateClaims = Settings.LYRICS_TRANSLATE_ONLY.get() + || onlyMode == OnlyMode.TRANS + || onlyMode == OnlyMode.BOTH || dualPillActive; + final boolean romanizeClaims = Settings.LYRICS_ROMANIZE_ONLY.get() + || onlyMode == OnlyMode.ROMA + || onlyMode == OnlyMode.BOTH || dualPillActive; + if (!(translateOff && translateClaims) && !(romanizeOff && romanizeClaims)) { + return false; + } + // Every mode that needed a button is left together: one side of a dual mode cannot stay + // while the pill it was shown under is gone. + exitOnlyModes(); + return true; + } + /** * Leaves the dual mode, the original coming back with whichever side survived as its small * run. Entering claims both flags before either side is known to have arrived, so both are @@ -2142,8 +2226,8 @@ private void fallBackFromDualMode(boolean romanizeFailed) { } /** - * Enters the dual mode, where both only-flags stand at once and neither side clears the other. - */ + * Enters the dual mode, where both only-flags stand at once and neither side clears the other. + */ private void enterDualMode() { Settings.LYRICS_TRANSLATE_ONLY.save(true); Settings.LYRICS_TRANSLATE.save(false); @@ -2233,7 +2317,10 @@ private void updateFooter() { translatedLines != null, translatedFromGoogle, translatedFromAI, translatedAiModel, romanizedLines != null || perWordRomaji, romanizedFromGoogle, romanizedFromAI, romanizedAiModel, onlyMode, - romanizedLocalDictionary)); + romanizedLocalDictionary, + () -> onTargetLanguageChanged(true), + () -> onTargetLanguageChanged(false), + () -> onSourceClicked())); } private boolean normalTranslateActive() { @@ -2249,6 +2336,69 @@ private boolean normalRomanizeActive() { || perWordRomaji; } + private void onTargetLanguageChanged(boolean translation) { + final Lyrics current = lyrics; + final TrackInfo track = LyricsManager.getInstance().getCurrentTrack(); + if (current == null || track == null) { + updateFooter(); + return; + } + try { + if (translation) { + if (translatedLines == null && !translateInProgress) { + updateFooter(); + return; + } + // A fresh request, not a toggle: the shown translation is the old language. + translateRequestId++; + translateInProgress = false; + translatedLines = null; + translatedFromGoogle = false; + translatedFromAI = false; + translatedAiModel = null; + if (onlyMode != OnlyMode.NONE) { + requestTranslateOnly(current); + } else { + requestNormalTranslate(current, track); + } + } else { + // Keep the Rime row in step with the language, so the provider list needs no trip + // to the settings. + setRimeProviderEnabled(RimeDictStore.isActive()); + if (romanizedLines == null && !perWordRomaji && !romanizeInProgress) { + updateFooter(); + return; + } + romanizeRequestId++; + romanizeInProgress = false; + romanizedLines = null; + perWordRomaji = false; + romanizedFromGoogle = false; + romanizedFromAI = false; + romanizedAiModel = null; + romanizedLocalDictionary = null; + if (onlyMode != OnlyMode.NONE) { + requestRomanizeOnly(current); + } else { + requestNormalRomanize(current, track); + } + } + } catch (Exception ex) { + Logger.printException(() -> "onTargetLanguageChanged failure", ex); + updateFooter(); + } + } + + /** Saves only when the flag actually changes, so an untouched list is never rewritten. */ + private static void setRimeProviderEnabled(boolean enabled) { + final String stored = Settings.LYRICS_ROMANIZATION_PROVIDERS.get(); + final String updated = AiProviders.withProviderEnabled( + stored, AiProviders.ROMANIZATION_ORDER, AiProviders.RIME, enabled); + if (!Objects.equals(updated, stored)) { + Settings.LYRICS_ROMANIZATION_PROVIDERS.save(updated); + } + } + private void applyOnlyModeState() { captureOnlyModeScrollAnchor(); karaokeState.resetContent(); @@ -2471,7 +2621,7 @@ private void onRomanizeClicked() { // whole-line side came back empty would blank the line with nothing said. perWordRomaji = perWord; if (!romaOk && !perWord) { - Utils.showToastShort(str("morphe_music_lyrics_romanize_failed")); + toastError(str("morphe_music_lyrics_romanize_failed")); } showLyrics(current); if (romaOk) { @@ -2600,7 +2750,7 @@ private void requestRomanizeOnly(Lyrics current) { romanizedLocalDictionary = romaOk ? localDictionary : null; perWordRomaji = perWord; if (!romaOk && !perWord) { - Utils.showToastShort(str("morphe_music_lyrics_romanize_failed")); + toastError(str("morphe_music_lyrics_romanize_failed")); fallBackFromDualMode(true); return; } @@ -2630,6 +2780,48 @@ private void requestRomanizeOnly(Lyrics current) { }); } + private void requestNormalRomanize(Lyrics current, TrackInfo track) { + Settings.LYRICS_ROMANIZE.save(true); + Settings.LYRICS_ROMANIZE_ONLY.save(false); + romanizeInProgress = true; + final int requestId = ++romanizeRequestId; + setButtonLabel(romanizeView, str("morphe_music_lyrics_romanizing"), true); + + LyricsRomanizer.romanize(track, current, current.providerName(), + (lines, fromGoogle, fromAI, model, perWord, localDictionary) -> { + if (requestId != romanizeRequestId || !romanizeInProgress) { + return; + } + romanizeInProgress = false; + + // The track moved on while the romanization was in flight. + if (lyrics != current) { + return; + } + + final boolean romaOk = LyricsMerge.hasText(lines); + romanizedLines = romaOk ? lines : null; + romanizedFromGoogle = romaOk && fromGoogle; + romanizedFromAI = romaOk && fromAI; + if (romanizedFromAI && model != null) { + romanizedAiModel = model; + } + romanizedLocalDictionary = romaOk ? localDictionary : null; + // A reading on the words stands on its own: dropping it whenever the + // whole-line side came back empty would blank the line with nothing said. + perWordRomaji = perWord; + if (!romaOk && !perWord) { + toastError(str("morphe_music_lyrics_romanize_failed")); + } + showLyrics(current); + if (romaOk) { + flashButtonLabel(romanizeView, + str("morphe_music_lyrics_romanize_hide"), + updateRomanizeLabelRunnable, 3000); + } + }); + } + private void cancelRomanizeOnly() { clearRomanizeOnlyState(); Lyrics current = lyrics; @@ -2768,7 +2960,7 @@ private void startTranscription(@Nullable String lyrics) { transcribeBusy = false; if (failure != null) { if (failure != QuickLRCClient.Failure.CANCELLED) { - Utils.showToastShort(str(failure == QuickLRCClient.Failure.OUT_OF_CREDITS + toastError(str(failure == QuickLRCClient.Failure.OUT_OF_CREDITS ? "morphe_music_lyrics_transcribe_out_of_credits" : "morphe_music_lyrics_transcribe_failed")); } @@ -2776,7 +2968,7 @@ private void startTranscription(@Nullable String lyrics) { return; } if (result == null) { - Utils.showToastShort(str("morphe_music_lyrics_transcribe_failed")); + toastError(str("morphe_music_lyrics_transcribe_failed")); updateTranscribeAffordances(); return; } @@ -2835,8 +3027,8 @@ private void updateTranscribeAffordances() { alignButton.setText(pendingAlignment != null ? str("morphe_music_lyrics_transcribe_apply_align") : running - ? str("morphe_music_lyrics_transcribe_aligning") - : str("morphe_music_lyrics_transcribe_align")); + ? str("morphe_music_lyrics_transcribe_aligning") + : str("morphe_music_lyrics_transcribe_align")); } } @@ -2857,7 +3049,7 @@ private void openSearchDialog() { return; } if (!manager.hasSearchProviders()) { - Utils.showToastShort(str("morphe_music_lyrics_search_no_providers")); + toastError(str("morphe_music_lyrics_search_no_providers")); return; } Context context = getContext(); @@ -2912,30 +3104,30 @@ private void openSearchDialog() { transcribeButton.setText(pendingAlignment != null ? str("morphe_music_lyrics_transcribe_apply_align") : QuickLRCClient.isRunning() - ? str("morphe_music_lyrics_transcribe_aligning") - : str("morphe_music_lyrics_transcribe_align")); + ? str("morphe_music_lyrics_transcribe_aligning") + : str("morphe_music_lyrics_transcribe_align")); dialogAlignButton = transcribeButton; transcribeButton.setOnClickListener(view -> { - if (pendingAlignment != null) { - applyPendingAlignment(); - return; - } - if (QuickLRCClient.isRunning()) { - cancelTranscription(true); - return; - } - Lyrics current = lyrics; - String text = null; - if (current != null && !current.isEmpty() && current != Lyrics.NOT_FOUND) { - StringBuilder joined = createJoined(current); - text = joined.toString(); - } - if (text == null || text.trim().isEmpty()) { - Utils.showToastShort(str("morphe_music_lyrics_transcribe_no_text")); - text = null; - } - startTranscription(text); + if (pendingAlignment != null) { + applyPendingAlignment(); + return; + } + if (QuickLRCClient.isRunning()) { + cancelTranscription(true); + return; + } + Lyrics current = lyrics; + String text = null; + if (current != null && !current.isEmpty() && current != Lyrics.NOT_FOUND) { + StringBuilder joined = createJoined(current); + text = joined.toString(); + } + if (text == null || text.trim().isEmpty()) { + Utils.showToastShort(str("morphe_music_lyrics_transcribe_no_text")); + text = null; + } + startTranscription(text); }); } @@ -2959,7 +3151,8 @@ private void openSearchDialog() { && artist.equals(defaultArtist); manager.searchWithCustomQuery(title, artist, defaultTerms); }, - () -> { }, + () -> { + }, null, null, false @@ -3009,6 +3202,13 @@ private void openSearchDialog() { }); } + /** A failure the app hit on its own: it still reaches the log, the toast follows the debug switch. */ + private static void toastError(String message) { + if (BaseSettings.DEBUG_TOAST_ON_ERROR.get()) { + Utils.showToastShort(message); + } + } + @SuppressWarnings("unused") @NonNull private static StringBuilder createJoined(Lyrics current) { @@ -3277,7 +3477,7 @@ private void shrinkRegionRows(boolean trans, Runnable onEnd) { } final int targetLayoutH = start == 0 ? layout.getHeight() - - layout.getLineBottom(layout.getLineForOffset(end - 1)) + - layout.getLineBottom(layout.getLineForOffset(end - 1)) : layout.getLineTop(layout.getLineForOffset(start)); final int targetH = targetLayoutH + tv.getPaddingTop() + tv.getPaddingBottom() + LyricsViewSupport.rowHeightOutsideText(row, tv); @@ -3951,8 +4151,8 @@ private int targetSectionSlotHeight() { * belongs to. * * @return {@code null} when the line opens no section. The label is always built when there - * is one, so flipping the setting on can reveal it without rebuilding the lyrics; - * whether it is shown is decided by its visibility. + * is one, so flipping the setting on can reveal it without rebuilding the lyrics; + * whether it is shown is decided by its visibility. */ @Nullable private static TextView createSectionTag(Context context, @Nullable String songPart) { @@ -4155,6 +4355,19 @@ public void applyTextSize() { } } + public void applyLineScale() { + final boolean enabled = Settings.LYRICS_LINE_SCALE.get(); + for (TextView textView : lineViews) { + if (!(textView instanceof LyricsLineView view)) { + continue; + } + view.lineScale = enabled && view.lineAlpha < 1f + ? LyricsViewSupport.INACTIVE_LINE_SCALE + : LyricsViewSupport.ACTIVE_LINE_SCALE; + LyricsViewSupport.applyLineScaleTransform(view, view.lineScale); + } + } + @SuppressWarnings("SizeReplaceableByIsEmpty") public void applyHideInfo() { CharSequence info = creditView.getText(); @@ -4320,7 +4533,7 @@ private void settleDualPill() { setButtonLabel(pill, null, true); } - + private void restoreButtonsFromPill() { TextView translate = translateView; TextView romanize = romanizeView; @@ -4353,7 +4566,7 @@ private void restoreButtonsFromPill() { }).start(); } - + private void updateDualPill() { if (onlyMode == OnlyMode.BOTH) { if (!dualPillActive) { @@ -4611,6 +4824,9 @@ private boolean isClusterStillActive(Lyrics current, int leadIdx, long posMs) { /** Open while its lead line is the active one, and while it is sung itself. */ private boolean shouldBgLineBeVisible(Lyrics current, int activeLeadIdx, int bgIdx, long posMs) { + if (!Settings.LYRICS_SPLIT_BACKGROUND.get()) { + return true; + } if (!current.synced()) { return true; } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsSearchUI.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsSearchUI.java index bdce138067..8d7a6ee983 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsSearchUI.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsSearchUI.java @@ -17,13 +17,18 @@ import android.widget.ArrayAdapter; import android.widget.AutoCompleteTextView; import android.widget.TextView; + import androidx.annotation.NonNull; + +import java.util.List; + import app.morphe.extension.shared.theme.ThemeUtils; import app.morphe.extension.shared.ui.Dim; -import java.util.List; final class LyricsSearchUI { + private static final float SUGGESTION_TEXT_SIZE_SP = 14; + static AutoCompleteTextView createSearchInput(Context context, String hint) { AutoCompleteTextView input = new AutoCompleteTextView(context); input.setHint(hint); @@ -40,7 +45,7 @@ static AutoCompleteTextView createSearchInput(Context context, String hint) { input.setClipToOutline(true); input.addOnLayoutChangeListener((v, left, top, right, bottom, - oldLeft, oldTop, oldRight, oldBottom) -> { + oldLeft, oldTop, oldRight, oldBottom) -> { int width = v.getWidth(); if (width > 0) { input.setDropDownWidth(width); @@ -50,8 +55,6 @@ static AutoCompleteTextView createSearchInput(Context context, String hint) { return input; } - private static final float SUGGESTION_TEXT_SIZE_SP = 14; - private static TextView createSuggestionRow(Context context) { TextView row = new TextView(context); row.setTextSize(TypedValue.COMPLEX_UNIT_SP, SUGGESTION_TEXT_SIZE_SP); @@ -62,6 +65,11 @@ private static TextView createSuggestionRow(Context context) { return row; } + static void setSuggestionAdapter(Context context, AutoCompleteTextView input, + List suggestions) { + input.setAdapter(new SuggestionAdapter(context, suggestions, input)); + } + private static final class SuggestionAdapter extends ArrayAdapter { private final AutoCompleteTextView input; @@ -89,9 +97,4 @@ public View getDropDownView(int position, View convertView, @NonNull ViewGroup p } } - static void setSuggestionAdapter(Context context, AutoCompleteTextView input, - List suggestions) { - input.setAdapter(new SuggestionAdapter(context, suggestions, input)); - } - } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsSpanBuilder.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsSpanBuilder.java index a8a63e0a7e..8e7a4c8e30 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsSpanBuilder.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsSpanBuilder.java @@ -33,12 +33,6 @@ import app.morphe.extension.music.patches.lyrics.model.LyricsLine; import app.morphe.extension.music.patches.lyrics.model.LyricsTextFormatter; import app.morphe.extension.music.patches.lyrics.model.Word; -import app.morphe.extension.music.patches.lyrics.romanizers.ArmenianRomanizer; -import app.morphe.extension.music.patches.lyrics.romanizers.CyrillicRomanizer; -import app.morphe.extension.music.patches.lyrics.romanizers.GeorgianRomanizer; -import app.morphe.extension.music.patches.lyrics.romanizers.GreekRomanizer; -import app.morphe.extension.music.patches.lyrics.romanizers.HangulRomanizer; -import app.morphe.extension.music.patches.lyrics.romanizers.KanaRomanizer; import app.morphe.extension.music.patches.lyrics.romanizers.LocalRomanizer; import app.morphe.extension.music.patches.lyrics.romanizers.ScriptDetector; import app.morphe.extension.music.settings.Settings; @@ -53,7 +47,7 @@ final class LyricsSpanBuilder { * the readings of two words do not run together. */ record WordTiming(int start, int end, long startMs, long endMs, - @Nullable String romaji, boolean hasTrailingSpace) { + @Nullable String romaji, boolean hasTrailingSpace) { /** A reading ending in a space stands for the space after its word. */ WordTiming(int start, int end, long startMs, long endMs, @Nullable String romaji) { @@ -69,24 +63,24 @@ record WordTiming(int start, int end, long startMs, long endMs, * offsets are measured from. Zero when an only mode drops it. */ record BuildResult(Spannable text, @Nullable ForegroundColorSpan unsungSpan, - int transStart, int transEnd, int romaStart, int romaEnd, - @Nullable List readingSpans, boolean smallRunHoldsWords, - boolean showSyllableRomanization, int originalStart) { + int transStart, int transEnd, int romaStart, int romaEnd, + @Nullable List readingSpans, boolean smallRunHoldsWords, + boolean showSyllableRomanization, int originalStart) { BuildResult(Spannable text, @Nullable ForegroundColorSpan unsungSpan, - int transStart, int transEnd, int romaStart, int romaEnd) { + int transStart, int transEnd, int romaStart, int romaEnd) { this(text, unsungSpan, transStart, transEnd, romaStart, romaEnd, null); } BuildResult(Spannable text, @Nullable ForegroundColorSpan unsungSpan, - int transStart, int transEnd, int romaStart, int romaEnd, - @Nullable List readingSpans) { + int transStart, int transEnd, int romaStart, int romaEnd, + @Nullable List readingSpans) { this(text, unsungSpan, transStart, transEnd, romaStart, romaEnd, readingSpans, false); } BuildResult(Spannable text, @Nullable ForegroundColorSpan unsungSpan, - int transStart, int transEnd, int romaStart, int romaEnd, - @Nullable List readingSpans, boolean smallRunHoldsWords) { + int transStart, int transEnd, int romaStart, int romaEnd, + @Nullable List readingSpans, boolean smallRunHoldsWords) { this(text, unsungSpan, transStart, transEnd, romaStart, romaEnd, readingSpans, smallRunHoldsWords, false, 0); } @@ -146,7 +140,7 @@ private static final class UniformLineSpacingSpan implements LineHeightSpan { @Override public void chooseHeight(CharSequence text, int start, int end, int spanstartv, int v, - Paint.FontMetricsInt fm) { + Paint.FontMetricsInt fm) { if (end >= text.length()) { return; } @@ -277,8 +271,8 @@ record Reading(@Nullable String text, @Nullable List spans) { * whole-line reading is spread over. */ static Reading readingFor(LyricsLine line, int index, - @Nullable List romanizedLines, boolean perWordRomaji, - @Nullable List timings) { + @Nullable List romanizedLines, boolean perWordRomaji, + @Nullable List timings) { String orig = displayLineText(line).trim(); if (orig.isEmpty() || LyricsRomanizer.isPurelyLatinScript(orig)) { return new Reading(null, null); @@ -349,7 +343,7 @@ static List buildSyllableRomanizedTimings( String cleanMain = stripBidiControls(mainText).trim(); if (cleanMain.isEmpty() || ScriptDetector.containsRtl(cleanMain) || (ScriptDetector.detect(cleanMain) == ScriptDetector.Script.OTHER - && !ScriptDetector.containsCjk(cleanMain)) + && !ScriptDetector.containsCjk(cleanMain)) || LyricsRomanizer.isPurelyLatinScript(cleanMain)) { return null; } @@ -437,7 +431,7 @@ static List buildSyllableRomanizedTimings( for (int i = 0; i < n; i++) { String own = ownWords[i]; syllables.add(own); - guides.add(phoneticGuideFor(own, mainText, timings.get(i).end(), isLatinWord[i])); + guides.add(phoneticGuideFor(own, mainText, isLatinWord[i])); } if (wholeRoman != null && !wholeRoman.trim().isEmpty()) { @@ -506,7 +500,7 @@ static List buildSyllableRomanizedTimings( || (e < mainText.length() && Character.isWhitespace(mainText.charAt(e))) || (e > s && Character.isWhitespace(mainText.charAt(e - 1))) || (i + 1 < n && timings.get(i + 1).start() > e - && mainText.substring(e, Math.min(mainText.length(), timings.get(i + 1).start())).indexOf(' ') >= 0); + && mainText.substring(e, Math.min(mainText.length(), timings.get(i + 1).start())).indexOf(' ') >= 0); final boolean trailSpace = !r.isEmpty() && (mainHasTrailingSpace || romTrailingSpaces[i]); if (!r.isEmpty()) { @@ -523,55 +517,15 @@ static List buildSyllableRomanizedTimings( * A local reading of one syllable, so the alignment compares Latin letters with Latin letters * and never with the script being read. */ - private static String phoneticGuideFor( - String ownWord, - String fullLineText, - int wordEndOffset, - boolean isLatin) { + private static String phoneticGuideFor(String ownWord, String fullLineText, boolean isLatin) { if (ownWord == null || ownWord.isEmpty()) { return ""; } if (isLatin) { return ownWord; } - if (ScriptDetector.contains(ownWord, Character.UnicodeScript.HANGUL)) { - // A lone Hangul syllable is read with the one after it, which can change how it ends. - if (ownWord.length() == 1 && wordEndOffset < fullLineText.length()) { - final char nextC = fullLineText.charAt(wordEndOffset); - if (nextC >= 0xAC00 && nextC <= 0xD7A3) { - String twoChar = HangulRomanizer.romanize(ownWord + nextC); - String nextOnly = HangulRomanizer.romanize(String.valueOf(nextC)); - if (twoChar.length() > nextOnly.length() && twoChar.endsWith(nextOnly)) { - return twoChar.substring(0, twoChar.length() - nextOnly.length()).replace("-", ""); - } - } - } - return HangulRomanizer.romanize(ownWord).replace("-", "").trim(); - } - if (ScriptDetector.containsCjk(ownWord)) { - LocalRomanizer.Result local = LocalRomanizer.romanize(Collections.singletonList(ownWord)); - if (local != null && !local.lines.isEmpty()) { - String r = local.lines.get(0); - if (r != null && !r.isEmpty() && !ScriptDetector.containsCjk(r)) { - return r.trim(); - } - } - if (ScriptDetector.containsKana(ownWord)) { - return KanaRomanizer.romanize(ownWord).trim(); - } - return ""; - } - ScriptDetector.Script script = ScriptDetector.detect(ownWord); - if (script != null) { - return switch (script) { - case CYRILLIC -> CyrillicRomanizer.romanize(ownWord).trim(); - case GREEK -> GreekRomanizer.romanize(ownWord).trim(); - case GEORGIAN -> GeorgianRomanizer.romanize(ownWord).trim(); - case ARMENIAN -> ArmenianRomanizer.romanize(ownWord).trim(); - default -> ""; - }; - } - return ""; + final String reading = LocalRomanizer.syllableReading(ownWord, fullLineText); + return reading != null ? reading : ""; } /** @@ -717,7 +671,7 @@ private static double phoneticMatchCost(String candidate, String guide, boolean String gCompact = compactPhonetic(gTrim, false); final int rawDist = levenshtein(cCompact, gCompact); final int phonDist = levenshtein(compactPhonetic(cTrim, true), compactPhonetic(gTrim, true)); - final double dist = Math.min((double) rawDist, phonDist + 0.25); + final double dist = Math.min(rawDist, phonDist + 0.25); final int lenDiff = Math.abs(cCompact.length() - gCompact.length()); final double spaceBonus = candidate.endsWith(" ") ? -0.35 : 0.0; return dist * 2.0 + lenDiff * 0.8 + spaceBonus; @@ -773,7 +727,7 @@ private static int levenshtein(String s1, String s2) { * against the original. */ private static Reading wholeLineReading(LyricsLine line, int index, - @Nullable List romanizedLines, @Nullable List timings) { + @Nullable List romanizedLines, @Nullable List timings) { String whole = lineRomanizationAt(line, index, romanizedLines); if (whole == null || timings == null || timings.isEmpty()) { return new Reading(whole, null); @@ -797,7 +751,7 @@ private static boolean anyReading(List words) { * word they spell and must not be read apart. */ private static boolean sameWrittenWord(int from, int start, - @Nullable String previous, String current) { + @Nullable String previous, String current) { if (previous == null || previous.isEmpty() || current.isEmpty() || start != from) { return false; } @@ -817,10 +771,12 @@ private static boolean isCjkAt(String text, int index) { return isCjkCodePoint(c); } - /** An aligned reading rides in the romanization's own lines; the lyric's own words are read - * only when they already carry one, as a provider's ruby does. */ + /** + * An aligned reading rides in the romanization's own lines; the lyric's own words are read + * only when they already carry one, as a provider's ruby does. + */ private static List readingWords(LyricsLine line, int index, - @Nullable List romanizedLines) { + @Nullable List romanizedLines) { if (romanizedLines != null && index < romanizedLines.size()) { List words = romanizedLines.get(index).words(); if (!words.isEmpty()) { @@ -841,7 +797,7 @@ private static List closeReadingGaps(List spans, LyricsL } closed.add(end > span.endMs() ? new WordTiming(span.start(), span.end(), span.startMs(), end, - span.romaji(), span.hasTrailingSpace()) + span.romaji(), span.hasTrailingSpace()) : span); } return closed; @@ -858,7 +814,7 @@ private static boolean hasTimedWord(LyricsLine line) { } static List computeWordTimings(List lines, int index, - boolean splitWordlessLine) { + boolean splitWordlessLine) { LyricsLine line = lines.get(index); String lineText = displayLineText(line); if (countWords(lineText) == 1 && hasTimedWord(line)) { @@ -889,7 +845,7 @@ static List computeWordTimings(List lines, int index, final long lineStart = line.startTimeMs(); final long lineEnd = line.endTimeMs() > lineStart ? line.endTimeMs() : index + 1 < lines.size() ? lines.get(index + 1).startTimeMs() - : LyricsLine.NO_TIME; + : LyricsLine.NO_TIME; if (lineStart >= 0 && lineEnd > lineStart) { List synthesized = synthesizeLineWords(lineText, lineStart, lineEnd); @@ -1012,8 +968,8 @@ private static int[] clusterBoundaries(String text) { List bounds = new ArrayList<>(); bounds.add(0); for (int boundary = iterator.first(); - boundary != BreakIterator.DONE; - boundary = iterator.next()) { + boundary != BreakIterator.DONE; + boundary = iterator.next()) { if (boundary > 0) { bounds.add(boundary); } @@ -1214,7 +1170,7 @@ private static void fillUntimedGap( i++; } if (i > s) { - spans.add(new int[] {s, i}); + spans.add(new int[]{s, i}); } } if (spans.isEmpty()) { @@ -1264,8 +1220,8 @@ private static boolean isPurePunctuation(String s) { * this TextView, which left the highlight invisible. */ static BuildResult buildLineText(LyricsLine line, List timings, int index, - boolean perWordRomaji, List romanizedLines, List translatedLines, - LyricsPanelView.OnlyMode onlyMode, Paint basePaint, boolean isRightAligned) { + boolean perWordRomaji, List romanizedLines, List translatedLines, + LyricsPanelView.OnlyMode onlyMode, Paint basePaint, boolean isRightAligned) { Paint.FontMetricsInt baseFm = new Paint.FontMetricsInt(); basePaint.getFontMetricsInt(baseFm); String original = displayLineText(line); @@ -1416,18 +1372,19 @@ private static void attachUniformSpacing(SpannableString text, Spacing spacing) * button reads the same call, so the two cannot disagree about which side is the main line. * * @param smallStart Where the small run starts, -1 when the line carries only one side. - * @param wordSpans Null unless something shown here is spoken word by word; when it is not, - * its offsets already measure against {@code text}. + * @param wordSpans Null unless something shown here is spoken word by word; when it is not, + * its offsets already measure against {@code text}. */ - record DualLine(String text, boolean mainIsRoma, boolean smallIsRoma, int smallStart, int smallEnd, - @Nullable List wordSpans, boolean wordsOnSmall) { + record DualLine(String text, boolean mainIsRoma, boolean smallIsRoma, int smallStart, + int smallEnd, + @Nullable List wordSpans, boolean wordsOnSmall) { } /** Both only-flags standing, so the original goes and the two swap-aware sides take over. */ @NonNull static DualLine dualLineFor(LyricsLine line, @Nullable List lineTimings, int index, - @Nullable String translation, String original, - @Nullable List romanizedLines) { + @Nullable String translation, String original, + @Nullable List romanizedLines) { Romanized roma = romanizedWithTimings(line, index, romanizedLines, lineTimings); String romanization = roma.text(); final boolean mainIsRoma; @@ -1487,7 +1444,7 @@ private static List shiftSpans(@Nullable List spans, int /** The line's own translation, null when it has none or it only repeats the original. */ @Nullable static String translationAt(LyricsLine line, int index, - @Nullable List translatedLines) { + @Nullable List translatedLines) { if (translatedLines == null || index >= translatedLines.size()) { return null; } @@ -1514,7 +1471,7 @@ static String translationAt(LyricsLine line, int index, /** The line's own romanization, without falling back to the per-word one. */ @Nullable static String lineRomanizationAt(LyricsLine line, int index, - @Nullable List romanizedLines) { + @Nullable List romanizedLines) { if (romanizedLines == null || index >= romanizedLines.size()) { return null; } @@ -1546,8 +1503,8 @@ private record Romanized(@Nullable String text, @Nullable List spans } private static Romanized romanizedWithTimings(LyricsLine line, int index, - @Nullable List romanizedLines, - @Nullable List lineTimings) { + @Nullable List romanizedLines, + @Nullable List lineTimings) { // The provider's own per-word reading comes first: it has already said which syllable // belongs to which word, and only a reading left whole is spread by syllable count. Reading reading = readingFor(line, index, romanizedLines, true, lineTimings); @@ -1562,7 +1519,7 @@ private static Romanized romanizedWithTimings(LyricsLine line, int index, */ @Nullable private static List spreadOverWords(String whole, String text, - @Nullable List lineTimings) { + @Nullable List lineTimings) { if (lineTimings == null || lineTimings.isEmpty()) { return null; } @@ -1613,7 +1570,7 @@ private static long windowEndFor(List words, int index, long wordEndMs) { * and the lit edge, being the furthest, jumps to the word's last syllable. */ private static void spreadWordWindow(List out, int[][] tokens, - int from, int to, long startMs, long endMs) { + int from, int to, long startMs, long endMs) { int spelling = 0; for (int at = from; at < to; at++) { spelling += tokens[at][1] - tokens[at][0]; @@ -1657,7 +1614,7 @@ private static List wordsAt(String text, List lineTimings) { /** What a romanization-only line shows: its own, else the per-word one joined up. */ @Nullable static String romanizationForOnlyMode(LyricsLine line, int index, - @Nullable List romanizedLines) { + @Nullable List romanizedLines) { return romanizedWithTimings(line, index, romanizedLines, null).text(); } @@ -1669,15 +1626,15 @@ static String romanizationForOnlyMode(LyricsLine line, int index, */ @Nullable static String lineReading(LyricsLine line, int index, - @Nullable List romanizedLines, boolean perWordRomaji) { + @Nullable List romanizedLines, boolean perWordRomaji) { return readingFor(line, index, romanizedLines, perWordRomaji, null).text(); } @Nullable private static ForegroundColorSpan applySpans(SpannableString text, List timings, - int originalStart, int originalEnd, - int romaStart, int romaEnd, int transStart, int transEnd, - boolean isRightAligned) { + int originalStart, int originalEnd, + int romaStart, int romaEnd, int transStart, int transEnd, + boolean isRightAligned) { ForegroundColorSpan unsungSpan = null; if (romaStart >= 0 && romaEnd > romaStart) { text.setSpan(new RelativeSizeSpan(TRANSLATION_RELATIVE_SIZE), romaStart, romaEnd, @@ -1731,6 +1688,9 @@ static boolean isRtlText(@Nullable CharSequence text) { } public static Layout.Alignment resolveAlignment(boolean isRtl, boolean isRightAligned) { + if (Settings.LYRICS_FORCE_CENTER.get()) { + return Layout.Alignment.ALIGN_CENTER; + } if (isRightAligned) { return isRtl ? Layout.Alignment.ALIGN_NORMAL : Layout.Alignment.ALIGN_OPPOSITE; } else { diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsViewSupport.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsViewSupport.java index a857661c10..39cfcf5b0e 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsViewSupport.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/LyricsViewSupport.java @@ -22,6 +22,10 @@ import android.graphics.drawable.GradientDrawable; import android.os.Build; import android.text.Layout; +import android.text.SpannableStringBuilder; +import android.text.Spanned; +import android.text.TextPaint; +import android.text.style.ClickableSpan; import android.util.TypedValue; import android.view.Gravity; import android.view.View; @@ -40,6 +44,7 @@ import app.morphe.extension.music.patches.lyrics.model.LyricsLine; import app.morphe.extension.music.settings.Settings; +import app.morphe.extension.music.settings.preference.LyricsTargetLanguagePreference; import app.morphe.extension.shared.ui.Dim; /** All of it is static and reads no state of its own, which is why it lives outside the view. */ @@ -407,33 +412,90 @@ static boolean hasTranslation(@Nullable List translated, List computeGaps(@Nullable List lines) { firstIndex++; } if (firstIndex < lines.size()) { - LyricsLine first = lines.get(firstIndex); + final LyricsLine first = lines.get(firstIndex); if (first.startTimeMs() >= MIN_GAP_MS) { gaps = new ArrayList<>(4); gaps.add(new Gap(firstIndex, 0L, first.startTimeMs())); @@ -94,7 +72,7 @@ public static List computeGaps(@Nullable List lines) { } int previousLead = -1; for (int i = 0; i < lines.size(); i++) { - LyricsLine line = lines.get(i); + final LyricsLine line = lines.get(i); if (line == null || line.isBG()) { continue; } @@ -144,7 +122,7 @@ public static State stateAt(long gapStartMs, long gapEndMs, long positionMs) { // Staggered so the last dot finishes exactly when the row hides. final float dotSpan = Math.max(1f, (vanishStart - enterEnd) / DOT_COUNT); - float[] dotAlpha = new float[DOT_COUNT]; + final float[] dotAlpha = new float[DOT_COUNT]; for (int i = 0; i < DOT_COUNT; i++) { final float lit = DOT_UNLIT + (1f - DOT_UNLIT) * clamp((t - enterEnd - dotSpan * i) / dotSpan); @@ -170,7 +148,7 @@ private static long effectiveEndMs(@Nullable LyricsLine line) { if (end > line.startTimeMs()) { return end; } - List words = line.words(); + final List words = line.words(); for (int i = words.size() - 1; i >= 0; i--) { final long wordEnd = words.get(i).endMs(); if (wordEnd > line.startTimeMs()) { @@ -204,4 +182,23 @@ private static float smooth(float v) { final float x = clamp(v); return x * x * (3f - 2f * x); } + + /** + * A silence long enough to play the dots, anchored on the line that follows it. + * + * @param tagLineIndex The line the motion plays on, which is where its label would sit. + * @param gapStartMs Where the previous line stops, or 0 for a silence before the first line. + */ + public record Gap(int tagLineIndex, long gapStartMs, long gapEndMs) { + } + + /** + * @param pillAlpha Opacity of the label: 1 outside the gap, 0 while the dots are up. + * @param groupAlpha Opacity of the dot row. + * @param dotAlpha Per dot, lighting up and going out in turn. + * @param expand How far a line without a label opens up for the dots, 0 to 1. + */ + public record State(float pillAlpha, boolean dotsVisible, float groupAlpha, float[] dotAlpha, + float expand) { + } } \ No newline at end of file diff --git a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/SponsorBlockMusicSubmitDialog.java b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/SponsorBlockMusicSubmitDialog.java index c9b6412c64..36c581f987 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/SponsorBlockMusicSubmitDialog.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/patches/lyrics/ui/SponsorBlockMusicSubmitDialog.java @@ -85,22 +85,22 @@ static String formatOffsetMs(long offsetMs) { static void show(Context context, String videoId, long startMs, long endMs, IntConsumer onOffsetSync, Runnable onExit) { final long videoLength = VideoInformation.getVideoLength(); - long[] times = {clampTime(Math.min(startMs, endMs), videoLength), + final long[] times = {clampTime(Math.min(startMs, endMs), videoLength), clampTime(Math.max(startMs, endMs), videoLength)}; - int[] slots = {0, 0}; - int[] selected = {DEFAULT_CATEGORY_INDEX}; - boolean[] submitted = {false}; - LyricsManager manager = LyricsManager.getInstance(); - - TextView startView = createTimeView(context); - TextView endView = createTimeView(context); - TextView separator = new TextView(context); + final int[] slots = {0, 0}; + final int[] selected = {DEFAULT_CATEGORY_INDEX}; + final boolean[] submitted = {false}; + final LyricsManager manager = LyricsManager.getInstance(); + + final TextView startView = createTimeView(context); + final TextView endView = createTimeView(context); + final TextView separator = new TextView(context); separator.setText(" - "); separator.setTextSize(22); separator.setTypeface(Typeface.DEFAULT_BOLD); separator.setTextColor(ThemeUtils.getAppForegroundColor()); - OffsetSliderView offsetSlider = new OffsetSliderView(context); + final OffsetSliderView offsetSlider = new OffsetSliderView(context); offsetSlider.setValue(manager.getTemporaryOffsetMs()); offsetSlider.listener = ms -> { manager.setTemporaryOffsetMs(ms); @@ -109,7 +109,7 @@ static void show(Context context, String videoId, long startMs, long endMs, onOffsetSync.accept(ms); }; - Runnable refreshTimes = () -> { + final Runnable refreshTimes = () -> { startView.setText(formatTimeMs(times[0])); endView.setText(formatTimeMs(times[1])); // The candidate skips the lyrics the moment it exists and on every resample, @@ -121,26 +121,26 @@ static void show(Context context, String videoId, long startMs, long endMs, bindTimeView(endView, times, slots, 1, refreshTimes); refreshTimes.run(); - LinearLayout timesRow = new LinearLayout(context); + final LinearLayout timesRow = new LinearLayout(context); timesRow.setOrientation(LinearLayout.HORIZONTAL); timesRow.setGravity(Gravity.CENTER); timesRow.addView(startView); timesRow.addView(separator); timesRow.addView(endView); - Button offsetButtonA = createCircleButton(context, "A"); - Button offsetButtonB = createCircleButton(context, "B"); + final Button offsetButtonA = createCircleButton(context, "A"); + final Button offsetButtonB = createCircleButton(context, "B"); offsetButtonA.setOnClickListener(v -> applyOffsetSlot(manager, offsetSlider, times, slots, 0, refreshTimes)); offsetButtonB.setOnClickListener(v -> applyOffsetSlot(manager, offsetSlider, times, slots, 1, refreshTimes)); - LinearLayout offsetRow = new LinearLayout(context); + final LinearLayout offsetRow = new LinearLayout(context); offsetRow.setOrientation(LinearLayout.HORIZONTAL); offsetRow.setGravity(Gravity.CENTER_VERTICAL); offsetRow.addView(offsetButtonA, new LinearLayout.LayoutParams( Dim.dp36, Dim.dp36)); - LinearLayout.LayoutParams sliderParams = new LinearLayout.LayoutParams( + final LinearLayout.LayoutParams sliderParams = new LinearLayout.LayoutParams( 0, Dim.dp36, 1f); sliderParams.leftMargin = Dim.dp8; sliderParams.rightMargin = Dim.dp8; @@ -148,24 +148,24 @@ static void show(Context context, String videoId, long startMs, long endMs, offsetRow.addView(offsetButtonB, new LinearLayout.LayoutParams( Dim.dp36, Dim.dp36)); - LinearLayout content = new LinearLayout(context); + final LinearLayout content = new LinearLayout(context); content.setOrientation(LinearLayout.VERTICAL); content.addView(timesRow, new LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT)); content.addView(offsetRow, blockParams(Dim.dp28)); - String[] categoryTitles = new String[CATEGORIES.length]; - String[] categoryKeys = new String[CATEGORIES.length]; + final String[] categoryTitles = new String[CATEGORIES.length]; + final String[] categoryKeys = new String[CATEGORIES.length]; for (int i = 0; i < CATEGORIES.length; i++) { categoryTitles[i] = CATEGORIES[i].title.toString(); categoryKeys[i] = CATEGORIES[i].keyValue; } - Button categoryButton = CustomDialog.createButton(context, null, + final Button categoryButton = CustomDialog.createButton(context, null, categoryTitles[selected[0]], null, false, false); categoryButton.setOnClickListener(v -> { - Dialog picker = CustomDialogListPreference.createListDialog( + final Dialog picker = CustomDialogListPreference.createListDialog( context, str("morphe_sb_new_segment_choose_category"), categoryTitles, categoryKeys, categoryKeys[selected[0]], index -> { @@ -176,7 +176,7 @@ context, str("morphe_sb_new_segment_choose_category"), }); content.addView(categoryButton, blockParams(Dim.dp16)); - Pair dialogPair = CustomDialog.create( + final Pair dialogPair = CustomDialog.create( context, str("morphe_sb_new_segment_title"), null, @@ -190,24 +190,25 @@ context, str("morphe_sb_new_segment_choose_category"), str("morphe_sb_new_segment_start_is_before_end")); return; } - SegmentCategory category = CATEGORIES[selected[0]]; + final SegmentCategory category = CATEGORIES[selected[0]]; submitted[0] = true; Utils.runOnBackgroundThread(() -> submitSegment( videoId, category, start, end, videoLength, manager)); }, - () -> { }, + () -> { + }, str("morphe_sb_settings_copy"), () -> copySpan(context, videoId, times), false); - Dialog dialog = dialogPair.first; + final Dialog dialog = dialogPair.first; dialog.setOnDismissListener(d -> { if (!submitted[0]) { manager.clearDialogSegment(); } onExit.run(); }); - LinearLayout mainLayout = dialogPair.second; + final LinearLayout mainLayout = dialogPair.second; mainLayout.addView(content, mainLayout.getChildCount() - 1, new LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT, @@ -250,9 +251,9 @@ private static void scheduleSegmentRefresh(String videoId, LyricsManager manager private static void copySpan(Context context, String videoId, long[] times) { final long offsetMs = LyricsManager.getInstance().getTemporaryOffsetMs(); - String text = videoId + " [" + formatSeconds(times[0]) + ", " + final String text = videoId + " [" + formatSeconds(times[0]) + ", " + formatSeconds(times[1]) + "] " + (offsetMs > 0 ? "+" : "") + offsetMs; - ClipboardManager clipboard = (ClipboardManager) + final ClipboardManager clipboard = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); if (clipboard != null) { clipboard.setPrimaryClip(ClipData.newPlainText("sponsorblock", text)); @@ -261,7 +262,7 @@ private static void copySpan(Context context, String videoId, long[] times) { } private static TextView createTimeView(Context context) { - TextView view = new TextView(context); + final TextView view = new TextView(context); view.setTextSize(22); view.setTypeface(Typeface.DEFAULT_BOLD); view.setTextColor(ThemeUtils.getAppForegroundColor()); @@ -270,7 +271,7 @@ private static TextView createTimeView(Context context) { } private static LinearLayout.LayoutParams blockParams(int topMargin) { - LinearLayout.LayoutParams params = new LinearLayout.LayoutParams( + final LinearLayout.LayoutParams params = new LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT, Dim.dp36); params.topMargin = topMargin; return params; @@ -279,7 +280,7 @@ private static LinearLayout.LayoutParams blockParams(int topMargin) { @SuppressLint("ClickableViewAccessibility") private static void bindTimeView(TextView view, long[] times, int[] slots, int index, Runnable refreshTimes) { - long[] downSample = new long[1]; + final long[] downSample = new long[1]; view.setOnTouchListener((v, event) -> { if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { downSample[0] = LyricsManager.getInstance().getVideoPositionMs(); @@ -315,9 +316,9 @@ private static void swapSides(long[] times, int[] slots) { } private static Button createCircleButton(Context context, String text) { - Button button = CustomDialog.createButton(context, null, text, null, + final Button button = CustomDialog.createButton(context, null, text, null, false, false); - GradientDrawable circle = new GradientDrawable(); + final GradientDrawable circle = new GradientDrawable(); circle.setShape(GradientDrawable.OVAL); circle.setColor(ThemeUtils.getCancelOrNeutralButtonBackgroundColor()); button.setBackground(circle); @@ -380,15 +381,15 @@ private static final class OffsetSliderView extends View { ThemeUtils.getCancelOrNeutralButtonBackgroundColor())); } + private static int clamp(int ms) { + return Math.max(-RANGE_MS, Math.min(RANGE_MS, ms)); + } + void setValue(int ms) { offsetMs = clamp(ms); invalidate(); } - private static int clamp(int ms) { - return Math.max(-RANGE_MS, Math.min(RANGE_MS, ms)); - } - private void apply(int ms) { final int stepped = clamp(Math.round(ms / (float) STEP_MS) * STEP_MS); if (stepped == offsetMs) { @@ -446,7 +447,7 @@ public boolean onTouchEvent(@NonNull MotionEvent event) { protected void onDraw(@NonNull Canvas canvas) { textPaint.setTextSize(TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_SP, 16, getResources().getDisplayMetrics())); - Paint.FontMetrics metrics = textPaint.getFontMetrics(); + final Paint.FontMetrics metrics = textPaint.getFontMetrics(); canvas.drawText(formatOffsetMs(offsetMs), getWidth() / 2f, getHeight() / 2f - (metrics.ascent + metrics.descent) / 2f, textPaint); diff --git a/extensions/music/src/main/java/app/morphe/extension/music/settings/Settings.java b/extensions/music/src/main/java/app/morphe/extension/music/settings/Settings.java index c744f0f386..5350424f73 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/settings/Settings.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/settings/Settings.java @@ -209,22 +209,43 @@ public class Settings extends SharedYouTubeSettings { public static final BooleanSetting LYRICS_SHOW_COPY_BUTTON = new BooleanSetting("morphe_music_lyrics_show_copy_button", TRUE, false, parent(LYRICS_ENABLED)); public static final BooleanSetting LYRICS_SHOW_SECTION = new BooleanSetting("morphe_music_lyrics_show_section", FALSE, false, parent(LYRICS_ENABLED)); public static final BooleanSetting LYRICS_SHOW_TRANSLATE_BUTTON = new BooleanSetting("morphe_music_lyrics_show_translate_button", TRUE, false, parent(LYRICS_ENABLED)); - public static final StringSetting LYRICS_TRANSLATION_PROVIDER = new StringSetting("morphe_music_lyrics_translation_provider", AiProviders.GOOGLE, false, parentsAll(parent(LYRICS_ENABLED), parent(LYRICS_SHOW_TRANSLATE_BUTTON))); + /** A comma separated priority list; a leading '-' disables a provider. */ + public static final StringSetting LYRICS_TRANSLATION_PROVIDERS = new StringSetting("morphe_music_lyrics_translation_provider", AiProviders.DEFAULT_TRANSLATION_ORDER, false, parentsAll(parent(LYRICS_ENABLED), parent(LYRICS_SHOW_TRANSLATE_BUTTON))); public static final StringSetting LYRICS_TRANSLATION_LANGUAGE = new StringSetting("morphe_music_lyrics_translation_language", "DEFAULT", false, parentsAll(parent(LYRICS_ENABLED), parent(LYRICS_SHOW_TRANSLATE_BUTTON))); public static final BooleanSetting LYRICS_SHOW_ROMANIZE_BUTTON = new BooleanSetting("morphe_music_lyrics_show_romanize_button", TRUE, false, parent(LYRICS_ENABLED)); - public static final StringSetting LYRICS_ROMANIZATION_PROVIDER = new StringSetting("morphe_music_lyrics_romanization_provider", AiProviders.GOOGLE, false, parentsAll(parent(LYRICS_ENABLED), parent(LYRICS_SHOW_ROMANIZE_BUTTON))); + /** A comma separated priority list; a leading '-' disables a provider. */ + public static final StringSetting LYRICS_ROMANIZATION_PROVIDERS = new StringSetting("morphe_music_lyrics_romanization_provider", AiProviders.DEFAULT_ROMANIZATION_ORDER, false, parentsAll(parent(LYRICS_ENABLED), parent(LYRICS_SHOW_ROMANIZE_BUTTON))); public static final StringSetting LYRICS_ROMANIZE_LANGUAGE = new StringSetting("morphe_music_lyrics_romanize_language", "DEFAULT", false, parentsAll(parent(LYRICS_ENABLED), parent(LYRICS_SHOW_ROMANIZE_BUTTON))); - public static final StringSetting LYRICS_GEMINI_API_KEY = new StringSetting("morphe_music_lyrics_gemini_api_key", "", false, parent(LYRICS_ENABLED)); - public static final StringSetting LYRICS_GEMINI_MODEL = new StringSetting("morphe_music_lyrics_gemini_model", "gemini-2.0-flash", false, parent(LYRICS_ENABLED)); - public static final StringSetting LYRICS_DEEPL_API_KEY = new StringSetting("morphe_music_lyrics_deepl_api_key", "", false, parent(LYRICS_ENABLED)); /** - * Romanizes on the device when the provider, an OpenAI-compatible endpoint and Google - * Translate have all failed. Off by default: it needs the reading dictionaries, and a user - * who never hits an online failure should not pay for them. + * The dictionaries the user agreed to download, as comma separated {@code ja} and {@code zh} + * entries. Blank means the romanization language decides, which keeps an upgraded install + * fetching both the way earlier versions did. + */ + public static final StringSetting LYRICS_LOCAL_DICTIONARIES = new StringSetting("morphe_music_lyrics_local_dictionaries", "", false, parentsAll(parent(LYRICS_ENABLED), parent(LYRICS_SHOW_ROMANIZE_BUTTON))); + + /** + * The Rime word tables the user added, one per line, as + * {@code subtag|label|url|charSource}. They are stored rather than shipped because a word + * list's licence and freshness are the user's to judge; the built-in Cantonese table is not + * listed here. + */ + public static final StringSetting LYRICS_RIME_CUSTOM_DICTS = new StringSetting("morphe_music_lyrics_rime_custom_dicts", "", false, parentsAll(parent(LYRICS_ENABLED), parent(LYRICS_SHOW_ROMANIZE_BUTTON))); + + /** + * The built-in word tables whose switch is on, as comma separated subtags. Empty means none, + * so a fresh install downloads nothing until a table is ticked. + */ + public static final StringSetting LYRICS_RIME_BUILTIN_SELECTED = new StringSetting("morphe_music_lyrics_rime_builtin_selected", "", false, parentsAll(parent(LYRICS_ENABLED), parent(LYRICS_SHOW_ROMANIZE_BUTTON))); + + /** + * Keep non-Latin readings supplied by the source, such as a Chinese transliteration of a + * Japanese song. Off accepts only Latin letters, accented letters and tone digits. */ - public static final BooleanSetting LYRICS_LOCAL_ROMANIZATION = new BooleanSetting("morphe_music_lyrics_local_romanization", FALSE, false, parentsAll(parent(LYRICS_ENABLED), parent(LYRICS_SHOW_ROMANIZE_BUTTON))); + public static final BooleanSetting LYRICS_ALLOW_TRANSLITERATION = new BooleanSetting("morphe_music_lyrics_allow_transliteration", FALSE, false, parentsAll(parent(LYRICS_ENABLED), parent(LYRICS_SHOW_ROMANIZE_BUTTON))); + + public static final StringSetting LYRICS_DEEPL_API_KEY = new StringSetting("morphe_music_lyrics_deepl_api_key", "", false, parent(LYRICS_ENABLED)); public static final BooleanSetting LYRICS_SHOW_REFRESH_BUTTON = new BooleanSetting("morphe_music_lyrics_show_refresh_button", TRUE, false, parent(LYRICS_ENABLED)); public static final BooleanSetting LYRICS_USE_AI_SECTION = new BooleanSetting("morphe_music_lyrics_use_ai_section", FALSE, false, parentsAll(parent(LYRICS_ENABLED), parent(LYRICS_SHOW_SECTION))); public static final StringSetting LYRICS_AI_BASE_URL = new StringSetting("morphe_music_lyrics_ai_base_url", "https://api.openai.com/v1/chat/completions", false, parent(LYRICS_ENABLED)); @@ -232,24 +253,34 @@ public class Settings extends SharedYouTubeSettings { public static final StringSetting LYRICS_AI_MODEL = new StringSetting("morphe_music_lyrics_ai_model", "gpt-4o-mini", false, parent(LYRICS_ENABLED)); public static final StringSetting LYRICS_AI_REASONING_EFFORT = new StringSetting("morphe_music_lyrics_ai_reasoning_effort", "", false, parent(LYRICS_ENABLED)); public static final StringSetting LYRICS_AI_EXTRA_BODY = new StringSetting("morphe_music_lyrics_ai_extra_body", "", false, parent(LYRICS_ENABLED)); + /** A JSON object laid over the headers of every AI request; an empty value removes one. */ + public static final StringSetting LYRICS_AI_EXTRA_HEADERS = new StringSetting("morphe_music_lyrics_ai_extra_headers", "", false, parent(LYRICS_ENABLED)); public static final StringSetting LYRICS_AI_TRANSLATION_PROMPT = new StringSetting("morphe_music_lyrics_ai_prompt", OpenAIClient.DEFAULT_PROMPT, false, parentsAll(parent(LYRICS_ENABLED), parent(LYRICS_SHOW_TRANSLATE_BUTTON))); public static final StringSetting LYRICS_AI_ROMANIZATION_PROMPT = new StringSetting("morphe_music_lyrics_ai_romanization_prompt", OpenAIClient.DEFAULT_PROMPT, false, parentsAll(parent(LYRICS_ENABLED), parent(LYRICS_SHOW_ROMANIZE_BUTTON))); public static final StringSetting LYRICS_AI_SECTION_PROMPT = new StringSetting("morphe_music_lyrics_ai_section_prompt", OpenAIClient.SECTION_PROMPT, false, parent(LYRICS_ENABLED)); public static final StringSetting LYRICS_TRANSCRIBE_KEY = new StringSetting("morphe_music_lyrics_transcribe_key", "", false, parent(LYRICS_ENABLED)); + public static final BooleanSetting LYRICS_QUICKLRC_ENABLED = new BooleanSetting("morphe_music_lyrics_quicklrc_enabled", FALSE, false, parent(LYRICS_ENABLED)); public static final BooleanSetting LYRICS_HIDE_INFO = new BooleanSetting("morphe_music_lyrics_hide_info", FALSE, false, parent(LYRICS_ENABLED)); public static final BooleanSetting LYRICS_SWAP_TRANS_ROMA = new BooleanSetting("morphe_music_lyrics_swap_trans_roma", FALSE, false, parentsAll(parent(LYRICS_ENABLED), parentsAny(LYRICS_SHOW_TRANSLATE_BUTTON, LYRICS_SHOW_ROMANIZE_BUTTON))); + /** Puts every line in the middle, whatever the format and its duet sides ask for. */ + public static final BooleanSetting LYRICS_FORCE_CENTER = new BooleanSetting("morphe_music_lyrics_force_center", FALSE, false, parent(LYRICS_ENABLED)); public static final BooleanSetting LYRICS_ROMANIZE = new BooleanSetting("morphe_music_lyrics_romanize", FALSE, false, parent(LYRICS_ENABLED)); public static final BooleanSetting LYRICS_ROMANIZE_ONLY = new BooleanSetting("morphe_music_lyrics_romanize_only", FALSE, false, parent(LYRICS_ENABLED)); public static final BooleanSetting LYRICS_WORD_SYNC = new BooleanSetting("morphe_music_lyrics_word_sync", TRUE, false, parent(LYRICS_ENABLED)); + /** Shrinks every line that is not the one being sung. */ + public static final BooleanSetting LYRICS_LINE_SCALE = new BooleanSetting("morphe_music_lyrics_line_scale", TRUE, false, parent(LYRICS_ENABLED)); public static final BooleanSetting LYRICS_HIDE_PLAYED = new BooleanSetting("morphe_music_lyrics_hide_played", FALSE, false, parent(LYRICS_ENABLED)); + /** Master switch for separating background vocals and harmony from the lead line. */ + public static final BooleanSetting LYRICS_SPLIT_BACKGROUND = new BooleanSetting("morphe_music_lyrics_split_background", TRUE, false, parent(LYRICS_ENABLED)); public static final BooleanSetting LYRICS_HIDE_UNPLAYED = new BooleanSetting("morphe_music_lyrics_hide_unplayed", FALSE, false, parent(LYRICS_ENABLED)); - public static final IntegerSetting LYRICS_TEXT_SIZE = new IntegerSetting("morphe_music_lyrics_text_size", 26, false, parent(LYRICS_ENABLED)); + public static final IntegerSetting LYRICS_TEXT_SIZE = new IntegerSetting("morphe_music_lyrics_text_size", 28, false, parent(LYRICS_ENABLED)); public static final IntegerSetting LYRICS_OFFSET_MS = new IntegerSetting("morphe_music_lyrics_offset_ms", 0, false, parent(LYRICS_ENABLED)); public static final BooleanSetting LYRICS_SB_MATCHING = new BooleanSetting("morphe_music_lyrics_sb_matching", TRUE, false, parent(LYRICS_ENABLED)); public static final BooleanSetting LYRICS_MEDIASESSION = new BooleanSetting("morphe_music_lyrics_mediasession", FALSE, false, parent(LYRICS_ENABLED)); public static final BooleanSetting LYRICS_MINIPLAYER = new BooleanSetting("morphe_music_lyrics_miniplayer", FALSE, false, parent(LYRICS_ENABLED)); public static final BooleanSetting LYRICS_FORCE_CAPITALIZE = new BooleanSetting("morphe_music_lyrics_force_capitalize", FALSE, false, parent(LYRICS_ENABLED)); public static final BooleanSetting LYRICS_STRIP_TRAILING_PUNCTUATION = new BooleanSetting("morphe_music_lyrics_strip_trailing_punctuation", FALSE, false, parent(LYRICS_ENABLED)); + public static final BooleanSetting LYRICS_MINI_LINE_LIMITS = new BooleanSetting("morphe_music_lyrics_mini_line_limits", FALSE, false, false, null, parentsAll(parent(LYRICS_ENABLED), parentsAny(LYRICS_MINIPLAYER, LYRICS_MEDIASESSION))); public static final StringSetting LYRICS_MINI_TRACK_FORMAT = new StringSetting("morphe_music_lyrics_mini_track_format", MiniLyricsFormatter.DEFAULT_TRACK_FORMAT, false, parentsAll(parent(LYRICS_ENABLED), parentsAny(LYRICS_MINIPLAYER, LYRICS_MEDIASESSION))); public static final StringSetting LYRICS_MINI_ENCAPSULATION = new StringSetting("morphe_music_lyrics_mini_encapsulation", MiniLyricsFormatter.DEFAULT_ENCAPSULATION, false, parentsAll(parent(LYRICS_ENABLED), parentsAny(LYRICS_MINIPLAYER, LYRICS_MEDIASESSION))); public static final IntegerSetting LYRICS_MINI_CHAR_LIMIT = new IntegerSetting("morphe_music_lyrics_mini_char_limit", 0, false, parentsAll(parent(LYRICS_ENABLED), parentsAny(LYRICS_MINIPLAYER, LYRICS_MEDIASESSION))); @@ -304,6 +335,7 @@ public class Settings extends SharedYouTubeSettings { private static final BooleanSetting DEPRECATED_PERMANENT_REPEAT = new BooleanSetting("morphe_music_permanent_repeat", FALSE, true); private static final BooleanSetting DEPRECATED_LYRICS_USE_AI_TRANSLATION = new BooleanSetting("morphe_music_lyrics_use_ai_translation", FALSE, false); private static final BooleanSetting DEPRECATED_LYRICS_USE_AI_ROMANIZATION = new BooleanSetting("morphe_music_lyrics_use_ai_romanization", FALSE, false); + private static final BooleanSetting DEPRECATED_LYRICS_LOCAL_ROMANIZATION = new BooleanSetting("morphe_music_lyrics_local_romanization", FALSE, false); private static final BooleanSetting DEPRECATED_LYRICS_DISPLAY_ARTIST_FIRST = new BooleanSetting("morphe_music_lyrics_display_artist_first", FALSE, false); static { @@ -312,15 +344,8 @@ public class Settings extends SharedYouTubeSettings { migrateOldSettingToNew(DEPRECATED_PERMANENT_REPEAT, REMEMBER_REPEAT_STATE); migrateInAppDownloadsToDownloadMode(); migrateDownloadButtonOverride(); - migrateLyricsAiSwitchToProvider(DEPRECATED_LYRICS_USE_AI_TRANSLATION, LYRICS_TRANSLATION_PROVIDER); - migrateLyricsAiSwitchToProvider(DEPRECATED_LYRICS_USE_AI_ROMANIZATION, LYRICS_ROMANIZATION_PROVIDER); migrateLyricsArtistFirstToTrackFormat(); - if ("openrouter".equals(LYRICS_TRANSLATION_PROVIDER.get()) || "openai".equals(LYRICS_TRANSLATION_PROVIDER.get())) { - LYRICS_TRANSLATION_PROVIDER.save(AiProviders.OPENAI_COMPATIBLE); - } - if ("openrouter".equals(LYRICS_ROMANIZATION_PROVIDER.get()) || "openai".equals(LYRICS_ROMANIZATION_PROVIDER.get())) { - LYRICS_ROMANIZATION_PROVIDER.save(AiProviders.OPENAI_COMPATIBLE); - } + migrateQuickLrcEnabled(); } static { @@ -369,20 +394,6 @@ private static void migrateInAppDownloadsToDownloadMode() { DEPRECATED_MUSIC_IN_APP_DOWNLOADS.resetToDefault(); } - /** - * The External downloader mode now always replaces the Download button and menu, and the overrides - * were removed. Before, the Download button and menu were only replaced if their override was on. - * The menu overrides are not migrated to the new menu switch, which also applies to in-app downloads. - * The External downloader mode is still in use, so a marker is the only way to tell this already ran. - */ - /** The AI switch used the OpenAI-compatible endpoint, which is now one of the providers to pick. */ - private static void migrateLyricsAiSwitchToProvider(BooleanSetting oldSwitch, StringSetting provider) { - if (oldSwitch.get() && provider.isSetToDefault()) { - provider.save(AiProviders.OPENAI_COMPATIBLE); - } - oldSwitch.resetToDefault(); - } - /** The artist first switch was replaced with the track label format. */ private static void migrateLyricsArtistFirstToTrackFormat() { if (DEPRECATED_LYRICS_DISPLAY_ARTIST_FIRST.get() && LYRICS_MINI_TRACK_FORMAT.isSetToDefault()) { @@ -391,6 +402,26 @@ private static void migrateLyricsArtistFirstToTrackFormat() { DEPRECATED_LYRICS_DISPLAY_ARTIST_FIRST.resetToDefault(); } + /** + * Before the switch existed a stored key alone turned QuickLRC on, so leaving the new switch + * off would take the feature away from everyone who had configured it. + */ + private static void migrateQuickLrcEnabled() { + if (LYRICS_QUICKLRC_ENABLED.isSetToDefault()) { + return; + } + final String key = LYRICS_TRANSCRIBE_KEY.get(); + if (key != null && !key.trim().isEmpty()) { + LYRICS_QUICKLRC_ENABLED.save(true); + } + } + + /** + * The External downloader mode now always replaces the Download button and menu, and the overrides + * were removed. Before, the Download button and menu were only replaced if their override was on. + * The menu overrides are not migrated to the new menu switch, which also applies to in-app downloads. + * The External downloader mode is still in use, so a marker is the only way to tell this already ran. + */ private static void migrateDownloadButtonOverride() { if (DOWNLOAD_BUTTON_OVERRIDE_MIGRATED.get()) { return; diff --git a/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsAboutPreference.java b/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsAboutPreference.java index 91a379daec..863f60113f 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsAboutPreference.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsAboutPreference.java @@ -14,9 +14,11 @@ import android.text.TextUtils; import android.util.AttributeSet; -import app.morphe.extension.music.patches.lyrics.romanizers.CantoneseDictStore; +import app.morphe.extension.music.patches.lyrics.romanizers.CldrTransformsStore; import app.morphe.extension.music.patches.lyrics.romanizers.JapaneseDictStore; import app.morphe.extension.music.patches.lyrics.romanizers.PinyinDictStore; +import app.morphe.extension.music.patches.lyrics.romanizers.RimeDictStore; +import app.morphe.extension.music.patches.lyrics.romanizers.UniDictStore; /** * The notes on what the lyrics patch does and where it looks, with whatever the reading @@ -59,8 +61,9 @@ private void init() { private static CharSequence notices() { StringBuilder joined = new StringBuilder(); for (String notice : new String[] { - PinyinDictStore.notice(), CantoneseDictStore.notice(), JapaneseDictStore.notice(), - PinyinDictStore.wordsNotice(), CantoneseDictStore.wordsNotice() + UniDictStore.notice(), JapaneseDictStore.notice(), + PinyinDictStore.notice(), RimeDictStore.allNotices(), + CldrTransformsStore.notice() }) { if (notice == null) continue; if (joined.length() > 0) joined.append("\n\n"); diff --git a/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsAiConfigPreference.java b/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsAiConfigPreference.java deleted file mode 100644 index 3b965c2132..0000000000 --- a/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsAiConfigPreference.java +++ /dev/null @@ -1,430 +0,0 @@ -/* - * Copyright 2026 Morphe. - * https://github.com/MorpheApp/morphe-patches - * - * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. - */ - -package app.morphe.extension.music.settings.preference; - -import static app.morphe.extension.shared.StringRef.str; - -import android.app.Activity; -import android.app.Dialog; -import android.content.Context; -import android.graphics.Typeface; -import android.preference.Preference; -import android.text.InputType; -import android.util.AttributeSet; -import android.util.Pair; -import android.util.TypedValue; -import android.view.View; -import android.view.ViewGroup; -import android.view.WindowManager; -import android.widget.Button; -import android.widget.EditText; -import android.widget.LinearLayout; -import android.widget.ScrollView; -import android.widget.TextView; - -import androidx.annotation.Nullable; - -import java.util.Collections; -import java.util.List; -import java.util.function.Supplier; - -import app.morphe.extension.music.patches.lyrics.derive.AiProviders; -import app.morphe.extension.music.patches.lyrics.derive.DeepLClient; -import app.morphe.extension.music.patches.lyrics.derive.GeminiClient; -import app.morphe.extension.music.patches.lyrics.derive.OpenAIClient; -import app.morphe.extension.music.settings.Settings; -import app.morphe.extension.shared.Logger; -import app.morphe.extension.shared.Utils; -import app.morphe.extension.shared.settings.preference.CustomDialogListPreference; -import app.morphe.extension.shared.theme.ThemeUtils; -import app.morphe.extension.shared.ui.CustomDialog; -import app.morphe.extension.shared.ui.Dim; - -/** - * Holds the API keys, models, custom endpoints, and prompt templates for all AI and cloud providers. - * Translation and Romanization provider selection happens in their respective settings sections. - */ -@SuppressWarnings({"unused", "deprecation"}) -public class LyricsAiConfigPreference extends Preference { - - private static final String TRANSLATION_LABEL_PREFIX = "morphe_music_lyrics_translation_provider_"; - - private boolean dialogShowing = false; - - public LyricsAiConfigPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { - super(context, attrs, defStyleAttr, defStyleRes); - updateSummary(); - } - - public LyricsAiConfigPreference(Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - updateSummary(); - } - - public LyricsAiConfigPreference(Context context, AttributeSet attrs) { - super(context, attrs); - updateSummary(); - } - - public LyricsAiConfigPreference(Context context) { - super(context); - updateSummary(); - } - - private void updateSummary() { - setSummary(str("morphe_music_lyrics_ai_config_summary")); - } - - - @Override - protected void onClick() { - showDialog(); - } - - private void showDialog() { - if (dialogShowing) return; - Context context = getContext(); - if (!(context instanceof Activity activity) || activity.isFinishing() - || activity.isDestroyed()) { - return; - } - dialogShowing = true; - - ScrollView scrollView = new ScrollView(context); - LinearLayout content = new LinearLayout(context); - content.setOrientation(LinearLayout.VERTICAL); - scrollView.addView(content, new ScrollView.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); - - TextView instruction = new TextView(context); - instruction.setText(str("morphe_music_lyrics_ai_config_dialog_instruction")); - instruction.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14); - instruction.setTextColor(ThemeUtils.getAppForegroundColor()); - LinearLayout.LayoutParams instructionParams = new LinearLayout.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); - instructionParams.bottomMargin = Dim.dp12; - content.addView(instruction, instructionParams); - - String[] endpointFilter = {"all"}; - content.addView(createLabel(context, "morphe_music_lyrics_ai_config_endpoint_label")); - Button endpointFilterButton = createButton(context, str("morphe_music_lyrics_ai_config_show_all")); - content.addView(endpointFilterButton); - - // Gemini. - LinearLayout geminiSection = createSection(context, "morphe_music_lyrics_ai_config_gemini_section"); - EditText geminiKeyInput = addInput(geminiSection, "morphe_music_lyrics_ai_config_api_key_label", - Settings.LYRICS_GEMINI_API_KEY.get(), true); - EditText geminiModelInput = addModelInput(geminiSection, Settings.LYRICS_GEMINI_MODEL.get(), - () -> GeminiClient.fetchModels(geminiKeyInput.getText().toString().trim())); - addTestButton(geminiSection, () -> { - String key = geminiKeyInput.getText().toString().trim(); - String model = geminiModelInput.getText().toString().trim(); - if (key.isEmpty()) { - Utils.showToastShort("Enter Gemini API key first"); - return; - } - Utils.showToastShort(str("morphe_music_lyrics_ai_config_status_validating")); - Utils.runOnBackgroundThread(() -> { - List res = GeminiClient.mapLines(key, model, "Translate to Japanese:\n1. hello", Collections.singletonList("hello")); - Utils.runOnMainThread(() -> { - if (res != null && !res.isEmpty()) { - Utils.showToastShort("Gemini: Connected! (" + res.get(0) + ")"); - } else { - Utils.showToastShort("Gemini: Connection failed. Check API key and model."); - } - }); - }); - }); - content.addView(geminiSection); - - // A custom OpenAI-compatible endpoint. - LinearLayout compatibleSection = createSection(context, "morphe_music_lyrics_ai_config_openai_compat_section"); - EditText compatibleUrlInput = addInput(compatibleSection, "morphe_music_lyrics_ai_config_base_url_label", - Settings.LYRICS_AI_BASE_URL.get(), false); - compatibleUrlInput.setHint(str("morphe_music_lyrics_ai_config_base_url_hint")); - EditText compatibleTokenInput = addInput(compatibleSection, "morphe_music_lyrics_ai_config_api_token_label", - Settings.LYRICS_AI_API_TOKEN.get(), true); - EditText compatibleModelInput = addModelInput(compatibleSection, Settings.LYRICS_AI_MODEL.get(), - () -> OpenAIClient.fetchModels(compatibleUrlInput.getText().toString().trim(), - compatibleTokenInput.getText().toString().trim())); - EditText extraBodyInput = addInput(compatibleSection, "morphe_music_lyrics_ai_config_extra_body_label", - Settings.LYRICS_AI_EXTRA_BODY.get(), false); - makeMultiLine(extraBodyInput, 2, 5); - addTestButton(compatibleSection, () -> { - String url = compatibleUrlInput.getText().toString().trim(); - String token = compatibleTokenInput.getText().toString().trim(); - String model = compatibleModelInput.getText().toString().trim(); - if (url.isEmpty()) { - Utils.showToastShort(str("morphe_music_lyrics_ai_config_status_invalid_url")); - return; - } - Utils.showToastShort(str("morphe_music_lyrics_ai_config_status_validating")); - Utils.runOnBackgroundThread(() -> { - boolean ok = OpenAIClient.validateConfig(url, token, model); - Utils.runOnMainThread(() -> { - if (ok) { - Utils.showToastShort("OpenAI-Compatible: Connected successfully!"); - } else { - Utils.showToastShort("OpenAI-Compatible: Connection failed. Check URL, token, and model."); - } - }); - }); - }); - content.addView(compatibleSection); - - // DeepL. - LinearLayout deepLSection = createSection(context, "morphe_music_lyrics_ai_config_deepl_section"); - EditText deepLKeyInput = addInput(deepLSection, "morphe_music_lyrics_ai_config_deepl_key_label", - Settings.LYRICS_DEEPL_API_KEY.get(), true); - addTestButton(deepLSection, () -> { - String key = deepLKeyInput.getText().toString().trim(); - if (key.isEmpty()) { - Utils.showToastShort("Enter DeepL authentication key first"); - return; - } - Utils.showToastShort(str("morphe_music_lyrics_ai_config_status_validating")); - Utils.runOnBackgroundThread(() -> { - List res = DeepLClient.translate(Collections.singletonList("hello"), "JA", key); - Utils.runOnMainThread(() -> { - if (res != null && !res.isEmpty()) { - Utils.showToastShort("DeepL: Connected! (" + res.get(0) + ")"); - } else { - Utils.showToastShort("DeepL: Connection failed. Check authentication key."); - } - }); - }); - }); - content.addView(deepLSection); - - // AI Prompt Templates. - LinearLayout promptSection = createSection(context, "morphe_music_lyrics_ai_prompt_dialog_title"); - EditText promptInput = addInput(promptSection, "morphe_music_lyrics_ai_config_prompt_label", - Settings.LYRICS_AI_TRANSLATION_PROMPT.get(), false); - makeMultiLine(promptInput, 3, 6); - EditText promptRomajiInput = addInput(promptSection, "morphe_music_lyrics_ai_config_prompt_romaji_label", - Settings.LYRICS_AI_ROMANIZATION_PROMPT.get(), false); - makeMultiLine(promptRomajiInput, 3, 6); - EditText promptSectionInput = addInput(promptSection, "morphe_music_lyrics_ai_config_prompt_section_label", - Settings.LYRICS_AI_SECTION_PROMPT.get(), false); - makeMultiLine(promptSectionInput, 3, 6); - content.addView(promptSection); - - Runnable updateVisibility = () -> { - boolean showAll = "all".equals(endpointFilter[0]); - geminiSection.setVisibility(showAll || AiProviders.GEMINI.equals(endpointFilter[0]) ? View.VISIBLE : View.GONE); - compatibleSection.setVisibility(showAll || AiProviders.OPENAI_COMPATIBLE.equals(endpointFilter[0]) ? View.VISIBLE : View.GONE); - deepLSection.setVisibility(showAll || AiProviders.DEEPL.equals(endpointFilter[0]) ? View.VISIBLE : View.GONE); - promptSection.setVisibility(showAll || "prompts".equals(endpointFilter[0]) ? View.VISIBLE : View.GONE); - }; - updateVisibility.run(); - - endpointFilterButton.setOnClickListener(v -> { - String[] filterValues = { - "all", - AiProviders.GEMINI, - AiProviders.OPENAI_COMPATIBLE, - AiProviders.DEEPL, - "prompts" - }; - String[] filterLabels = { - str("morphe_music_lyrics_ai_config_show_all"), - str(TRANSLATION_LABEL_PREFIX + AiProviders.GEMINI), - str(TRANSLATION_LABEL_PREFIX + AiProviders.OPENAI_COMPATIBLE), - str(TRANSLATION_LABEL_PREFIX + AiProviders.DEEPL), - str("morphe_music_lyrics_ai_prompt_dialog_title") - }; - CustomDialogListPreference.createListDialog(context, - str("morphe_music_lyrics_ai_config_endpoint_label"), - filterLabels, filterValues, endpointFilter[0], index -> { - endpointFilter[0] = filterValues[index]; - endpointFilterButton.setText(filterLabels[index]); - updateVisibility.run(); - }).show(); - }); - - Pair dialogPair = CustomDialog.create( - context, - str("morphe_music_lyrics_ai_config_dialog_title"), - null, - null, - str("morphe_settings_save"), - () -> { - String extraBody = extraBodyInput.getText().toString().trim(); - if (!OpenAIClient.isValidExtraBody(extraBody)) { - Utils.showToastShort(str("morphe_music_lyrics_ai_extra_body_invalid")); - return; - } - String baseUrl = compatibleUrlInput.getText().toString().trim(); - if (!baseUrl.isEmpty() && !baseUrl.startsWith("http://") && !baseUrl.startsWith("https://")) { - Utils.showToastShort(str("morphe_music_lyrics_ai_config_status_invalid_url")); - return; - } - Settings.LYRICS_AI_EXTRA_BODY.save(extraBody); - Settings.LYRICS_GEMINI_API_KEY.save(geminiKeyInput.getText().toString().trim()); - Settings.LYRICS_GEMINI_MODEL.save(geminiModelInput.getText().toString().trim()); - Settings.LYRICS_AI_BASE_URL.save(baseUrl); - Settings.LYRICS_AI_API_TOKEN.save(compatibleTokenInput.getText().toString().trim()); - Settings.LYRICS_AI_MODEL.save(compatibleModelInput.getText().toString().trim()); - Settings.LYRICS_AI_TRANSLATION_PROMPT.save(promptInput.getText().toString().trim()); - Settings.LYRICS_AI_ROMANIZATION_PROMPT.save(promptRomajiInput.getText().toString().trim()); - Settings.LYRICS_AI_SECTION_PROMPT.save(promptSectionInput.getText().toString().trim()); - Settings.LYRICS_DEEPL_API_KEY.save(deepLKeyInput.getText().toString().trim()); - - updateSummary(); - Utils.showToastShort(str("morphe_music_lyrics_ai_config_toast_saved")); - }, - null, - str("morphe_settings_reset"), - () -> { - Settings.LYRICS_GEMINI_API_KEY.resetToDefault(); - Settings.LYRICS_GEMINI_MODEL.resetToDefault(); - Settings.LYRICS_AI_BASE_URL.resetToDefault(); - Settings.LYRICS_AI_API_TOKEN.resetToDefault(); - Settings.LYRICS_AI_MODEL.resetToDefault(); - Settings.LYRICS_AI_TRANSLATION_PROMPT.resetToDefault(); - Settings.LYRICS_AI_ROMANIZATION_PROMPT.resetToDefault(); - Settings.LYRICS_AI_SECTION_PROMPT.resetToDefault(); - Settings.LYRICS_AI_EXTRA_BODY.resetToDefault(); - Settings.LYRICS_DEEPL_API_KEY.resetToDefault(); - updateSummary(); - Utils.showToastShort(str("morphe_music_lyrics_ai_config_toast_reset")); - }, - true - ); - - Dialog dialog = dialogPair.first; - LinearLayout mainLayout = dialogPair.second; - - LinearLayout.LayoutParams scrollParams = new LinearLayout.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, 0, 1.0f); - scrollParams.bottomMargin = Dim.dp12; - mainLayout.addView(scrollView, mainLayout.getChildCount() - 1, scrollParams); - - dialog.setOnDismissListener(d -> dialogShowing = false); - - try { - dialog.show(); - } catch (WindowManager.BadTokenException ex) { - dialogShowing = false; - Logger.printException(() -> "Could not show the AI settings dialog", ex); - } - } - - private static LinearLayout createSection(Context context, String titleKey) { - LinearLayout section = new LinearLayout(context); - section.setOrientation(LinearLayout.VERTICAL); - - TextView header = new TextView(context); - header.setText(str(titleKey)); - header.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14); - header.setTypeface(Typeface.DEFAULT_BOLD); - header.setTextColor(ThemeUtils.getAppForegroundColor()); - LinearLayout.LayoutParams params = new LinearLayout.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); - params.topMargin = Dim.dp16; - section.addView(header, params); - return section; - } - - private static TextView createLabel(Context context, String textKey) { - TextView label = new TextView(context); - label.setText(str(textKey)); - label.setTextSize(TypedValue.COMPLEX_UNIT_SP, 12); - label.setTextColor(ThemeUtils.getAppForegroundColor()); - LinearLayout.LayoutParams params = new LinearLayout.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); - params.topMargin = Dim.dp8; - params.bottomMargin = Dim.dp4; - label.setLayoutParams(params); - return label; - } - - private static Button createButton(Context context, String text) { - Button button = new Button(context); - button.setText(text); - button.setAllCaps(false); - button.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14); - button.setTextColor(ThemeUtils.getAppForegroundColor()); - button.setBackground(CustomDialog.createRoundedBackground(10, ThemeUtils.getEditTextBackground())); - button.setPadding(Dim.dp12, Dim.dp8, Dim.dp12, Dim.dp8); - return button; - } - - private static EditText addInput(LinearLayout section, String labelKey, String value, boolean secret) { - Context context = section.getContext(); - section.addView(createLabel(context, labelKey)); - EditText input = ThemedInput.create(context); - if (secret) { - input.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); - } - input.setText(value); - section.addView(input, new LinearLayout.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); - return input; - } - - private static void makeMultiLine(EditText input, int minLines, int maxLines) { - input.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE); - input.setSingleLine(false); - input.setMinLines(minLines); - input.setMaxLines(maxLines); - input.setVerticalScrollBarEnabled(true); - } - - /** A model name input with a button that lists the models the provider offers. */ - private static EditText addModelInput(LinearLayout section, String value, Supplier> fetchModels) { - Context context = section.getContext(); - section.addView(createLabel(context, "morphe_music_lyrics_ai_config_model_label")); - - EditText input = ThemedInput.create(context); - input.setText(value); - Button fetch = createButton(context, str("morphe_music_lyrics_ai_config_fetch_models")); - fetch.setOnClickListener(v -> { - Utils.showToastShort(str("morphe_music_lyrics_ai_config_fetching_models")); - Utils.runOnBackgroundThread(() -> { - List models = fetchModels.get(); - Utils.runOnMainThread(() -> showModelPicker(context, models, input)); - }); - }); - - LinearLayout row = new LinearLayout(context); - row.setOrientation(LinearLayout.HORIZONTAL); - row.addView(input, new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1.0f)); - LinearLayout.LayoutParams buttonParams = new LinearLayout.LayoutParams( - ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); - buttonParams.leftMargin = Dim.dp8; - row.addView(fetch, buttonParams); - section.addView(row); - return input; - } - - private static void showModelPicker(Context context, @Nullable List models, EditText target) { - if (models == null || models.isEmpty()) { - Utils.showToastShort(str("morphe_music_lyrics_ai_config_fetch_models_failed")); - return; - } - String[] items = models.toArray(new String[0]); - CustomDialogListPreference.createListDialog(context, - str("morphe_music_lyrics_ai_config_fetch_models_success"), items, items, - target.getText().toString().trim(), index -> { - target.setText(items[index]); - target.setSelection(items[index].length()); - }).show(); - } - - private static void addTestButton(LinearLayout section, Runnable onTest) { - Context context = section.getContext(); - Button testButton = createButton(context, str("morphe_music_lyrics_ai_config_test_connection")); - testButton.setOnClickListener(v -> onTest.run()); - LinearLayout.LayoutParams params = new LinearLayout.LayoutParams( - ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); - params.topMargin = Dim.dp8; - params.bottomMargin = Dim.dp4; - section.addView(testButton, params); - } -} diff --git a/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsAiPromptPreference.java b/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsAiPromptPreference.java index 65d0737c8d..716ee9819a 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsAiPromptPreference.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsAiPromptPreference.java @@ -130,7 +130,7 @@ private void showDialog() { promptInput.setMinLines(4); promptInput.setMaxLines(8); promptInput.setVerticalScrollBarEnabled(true); - promptInput.setHint(str("morphe_music_lyrics_ai_prompt_dialog_title")); + promptInput.setHint(str("morphe_music_lyrics_ai_prompt_dialog_hint")); String currentPrompt = promptSetting.get(); if (!currentPrompt.isEmpty()) { promptInput.setText(currentPrompt); @@ -140,16 +140,6 @@ private void showDialog() { LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT)); - TextView caption = new TextView(context); - caption.setText(str("morphe_music_lyrics_ai_prompt_dialog_hint")); - caption.setTextSize(TypedValue.COMPLEX_UNIT_SP, 12); - caption.setTextColor(ThemeUtils.getAppForegroundColor() & 0xAAFFFFFF); - LinearLayout.LayoutParams captionParams = new LinearLayout.LayoutParams( - LinearLayout.LayoutParams.MATCH_PARENT, - LinearLayout.LayoutParams.WRAP_CONTENT); - captionParams.topMargin = Dim.dp8; - content.addView(caption, captionParams); - LinearLayout.LayoutParams reasoningParams = new LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT); @@ -231,7 +221,7 @@ private void enableAfterValidation(BooleanSetting switchSetting) { private static final ExecutorService VALIDATION = Executors.newSingleThreadExecutor( runnable -> { - Thread thread = new Thread(runnable, "lyrics-ai-validate"); + final Thread thread = new Thread(runnable, "lyrics-ai-validate"); thread.setDaemon(true); return thread; }); @@ -265,10 +255,10 @@ private View createReasoningRow(Context context) { row.setPadding(Dim.dp12, Dim.dp10, Dim.dp12, Dim.dp10); row.setVisibility(View.GONE); - String stored = Settings.LYRICS_AI_REASONING_EFFORT.get(); + final String stored = Settings.LYRICS_AI_REASONING_EFFORT.get(); value.setText(display(stored)); MODEL_QUERY.execute(() -> { - String[] efforts = OpenAIClient.fetchReasoningEfforts( + final String[] efforts = OpenAIClient.fetchReasoningEfforts( Settings.LYRICS_AI_BASE_URL.get(), Settings.LYRICS_AI_API_TOKEN.get(), Settings.LYRICS_AI_MODEL.get()); Utils.runOnMainThread(() -> { @@ -276,14 +266,14 @@ private View createReasoningRow(Context context) { return; } row.setVisibility(View.VISIBLE); - CharSequence[] labels = new CharSequence[efforts.length]; + final CharSequence[] labels = new CharSequence[efforts.length]; for (int i = 0; i < efforts.length; i++) { labels[i] = display(efforts[i]); } row.setOnClickListener(v -> CustomDialogListPreference.createListDialog(context, "Reasoning effort", labels, efforts, Settings.LYRICS_AI_REASONING_EFFORT.get(), index -> { - String chosen = efforts[index]; + final String chosen = efforts[index]; Settings.LYRICS_AI_REASONING_EFFORT.save(chosen); value.setText(display(chosen)); }) @@ -302,7 +292,7 @@ private static String display(String effort) { private static final ExecutorService MODEL_QUERY = Executors.newSingleThreadExecutor( runnable -> { - Thread thread = new Thread(runnable, "lyrics-ai-models"); + final Thread thread = new Thread(runnable, "lyrics-ai-models"); thread.setDaemon(true); return thread; }); diff --git a/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsClearCachePreference.java b/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsClearCachePreference.java index 006ad03e1a..86f1bdbf65 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsClearCachePreference.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsClearCachePreference.java @@ -9,7 +9,6 @@ import static app.morphe.extension.shared.StringRef.str; -import android.app.AlertDialog; import android.content.Context; import android.preference.Preference; import android.util.AttributeSet; @@ -20,6 +19,7 @@ import app.morphe.extension.music.patches.lyrics.storage.LyricsCache; import app.morphe.extension.shared.Logger; import app.morphe.extension.shared.Utils; +import app.morphe.extension.shared.ui.CustomDialog; /** * The stored lyrics are in the app's own files, so a cache clear no longer reclaims them and this @@ -55,6 +55,17 @@ private void init() { // The tap is the whole feature and the row is declared non-selectable, so this is what // lets it through. setSelectable(true); + refreshSummary(); + } + + /** The size is measured off the main thread; the settings page reopens often enough that + * a stale summary never shows for long. */ + private void refreshSummary() { + Utils.runOnBackgroundThread(() -> { + final String size = megabytes(LyricsCache.size()); + Utils.runOnMainThread(() -> setSummary(str( + "morphe_music_lyrics_clear_cache_summary", size))); + }); } @Override @@ -62,24 +73,36 @@ protected void onClick() { if (!isEnabled()) { return; } - try { - new AlertDialog.Builder(getContext()) - .setTitle(str("morphe_music_lyrics_clear_cache_title")) - .setMessage(str("morphe_music_lyrics_clear_cache_summary")) - .setPositiveButton(android.R.string.ok, (d, w) -> clear()) - .setNegativeButton(android.R.string.cancel, null) - .show(); - } catch (WindowManager.BadTokenException ex) { - Logger.printException(() -> "Could not show the clear cache dialog", ex); - } + // Thousands of files can be stored, so the size is measured off the main thread. + Utils.runOnBackgroundThread(() -> { + final String size = megabytes(LyricsCache.size()); + Utils.runOnMainThread(() -> { + try { + CustomDialog.create(getContext(), + str("morphe_music_lyrics_clear_cache_title"), + str("morphe_music_lyrics_clear_cache_summary", size), + null, + null, + LyricsClearCachePreference.this::clear, + () -> { }, + null, + null, + true).first.show(); + } catch (WindowManager.BadTokenException ex) { + Logger.printException(() -> "Could not show the clear cache dialog", ex); + } + }); + }); } /** Thousands of files can be stored, so they are deleted off the main thread. */ - private static void clear() { + private void clear() { Utils.runOnBackgroundThread(() -> { - String freed = megabytes(LyricsCache.clear()); - Utils.runOnMainThread(() -> Utils.showToastShort( - str("morphe_music_lyrics_clear_cache_done", freed))); + final String freed = megabytes(LyricsCache.clear()); + Utils.runOnMainThread(() -> { + Utils.showToastShort(str("morphe_music_lyrics_clear_cache_done", freed)); + refreshSummary(); + }); }); } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsFieldsDialog.java b/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsFieldsDialog.java index 50e30b65ff..2dc8c1c0b4 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsFieldsDialog.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsFieldsDialog.java @@ -43,6 +43,20 @@ void addLabel(String key) { content.addView(label); } + /** A field label, such as the format hint, in the dimmed note style the dictionaries use. */ + void addNoteLabel(String key) { + TextView label = new TextView(context); + label.setText(str(key)); + label.setTextColor(withAlpha(ThemeUtils.getAppForegroundColor(), 0x99)); + label.setTextSize(14); + label.setPadding(0, Dim.dp12, 0, Dim.dp8); + content.addView(label); + } + + private static int withAlpha(int color, int alpha) { + return (alpha << 24) | (color & 0x00FFFFFF); + } + EditText addInput(String labelKey, String value, int inputType) { addLabel(labelKey); EditText input = ThemedInput.create(context); @@ -52,6 +66,15 @@ EditText addInput(String labelKey, String value, int inputType) { return input; } + EditText addNoteInput(String labelKey, String value, int inputType) { + addNoteLabel(labelKey); + EditText input = ThemedInput.create(context); + input.setInputType(inputType); + input.setText(value); + content.addView(input); + return input; + } + EditText addNumberInput(String labelKey, Number value, boolean decimal) { return addInput(labelKey, value.toString(), InputType.TYPE_CLASS_NUMBER | (decimal ? InputType.TYPE_NUMBER_FLAG_DECIMAL : 0)); diff --git a/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsLocalRomanizationDownloads.java b/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsLocalRomanizationDownloads.java new file mode 100644 index 0000000000..7459d7ef12 --- /dev/null +++ b/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsLocalRomanizationDownloads.java @@ -0,0 +1,369 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.extension.music.settings.preference; + +import static app.morphe.extension.shared.StringRef.str; + +import java.util.Locale; + +import androidx.annotation.Nullable; + +import app.morphe.extension.music.patches.lyrics.romanizers.CldrTransformsStore; +import app.morphe.extension.music.patches.lyrics.romanizers.JapaneseDictStore; +import app.morphe.extension.music.patches.lyrics.romanizers.PinyinDictStore; +import app.morphe.extension.music.patches.lyrics.romanizers.RimeDictStore; +import app.morphe.extension.music.patches.lyrics.romanizers.RomanizationDict; +import app.morphe.extension.music.patches.lyrics.romanizers.UniDictStore; +import app.morphe.extension.music.settings.Settings; +import app.morphe.extension.shared.Utils; + +/** Owns the Local romanization provider's dictionary downloads and status toasts. The Rime word + * tables are not one of these: they are the Rime Wordlist provider's own download, and that provider + * fetches what it needs rather than being chosen here. */ +public final class LyricsLocalRomanizationDownloads { + + /** The providers, in the order the settings store them. Each is one download and one + * deletion unit; only {@link #UNIDICT} writes two files, the character tables the Mandarin + * and the Cantonese word tables both build on, and {@link #CLDR} writes the transliteration + * rules every script read by rule draws on. */ + public static final int SUDACHI = 0; + public static final int UNIDICT = 1; + public static final int PINYIN = 2; + public static final int CLDR = 3; + /** Receives progress text while a dialog is open; a closed dialog sees no further updates. */ + public interface ProgressSink { + void onHint(CharSequence hint); + } + + private static final Progress[] progress = {new Progress(), + new Progress(), new Progress(), new Progress()}; + + private static boolean started; + private static boolean failed; + private static boolean announced; + private static boolean startAnnounced; + + private static volatile ProgressSink sink; + + private static final class Progress { + int entries; + boolean done; + @Nullable + String label; + int percent; + boolean building; + } + + private LyricsLocalRomanizationDownloads() { + } + + /** + * Which providers the user chose, or, until they chose, the ones the romanization language + * needs. Automatic covers every script, so it asks for all of them. + */ + public static boolean[] chosen() { + final String stored = Settings.LYRICS_LOCAL_DICTIONARIES.get(); + if (stored != null && !stored.isBlank()) { + final boolean[] picked = new boolean[4]; + for (String token : stored.split(",")) { + switch (token.trim().toLowerCase(Locale.ROOT)) { + // The provider names this build stores. + case "sudachi" -> picked[SUDACHI] = true; + case "unidict" -> picked[UNIDICT] = true; + case "pinyin" -> picked[PINYIN] = true; + case "cldr" -> picked[CLDR] = true; + // A dev build stored the languages this provider split covers. The Cantonese + // word table became the Rime Wordlist provider, which fetches the character + // tables it needs itself, so "yue" is no longer a choice here either. + case "ja" -> picked[SUDACHI] = true; + case "zh" -> { + picked[UNIDICT] = true; + picked[PINYIN] = true; + } + default -> { + } + } + } + return picked; + } + final String subtag = primarySubtag(Settings.LYRICS_ROMANIZE_LANGUAGE.get()); + // Hangul is read by its own algorithm, so Korean needs no dictionary and no rules. + // The scripts read by rule from the CLDR data keep that one download on: Greek, + // Cyrillic (BGN), Georgian, Armenian, Hebrew, Arabic (also reads Persian), + // Ethiopic (also reads Tigrinya) and Syriac. + return switch (subtag) { + case "ja" -> new boolean[]{true, false, false, false}; + case "zh" -> new boolean[]{false, true, true, false}; + // Cantonese has a word table of its own, read by the Rime Wordlist provider, so no local + // dictionary reads it: the Mandarin reading would be another language's. + case "yue" -> new boolean[]{false, false, false, false}; + // The remaining languages are read by the online provider (or, for Korean, + // by rule), so none of the local dictionaries are needed. + case "ko", "th", "km", "as", "bal", "bo", "brx", "ckb", "doi", "dz", "gom", + "kk", "ks", "ky", "lo", "mai", "mn", "mni", "ps", "sd", "ug" + -> new boolean[]{false, false, false, false}; + case "el", "ru", "be", "uk", "sr", "bg", "mk", "ka", "hy", "he", "iw", "ar", "fa", + "am", "ti", "sy" -> new boolean[]{false, false, false, true}; + // A language the user added a word list for is read by the Rime Wordlist provider in the + // same way, so the local dictionaries have nothing to add there either. Apart from + // those, an unknown language keeps every reading. + default -> RimeDictStore.hasLanguage(subtag) + ? new boolean[]{false, false, false, false} + : new boolean[]{true, true, true, true}; + }; + } + + /** Stores the user's choice. Providers that were on and are now off are deleted, so nothing + * unused lingers; pinyin keeps UniDict on, since it reads the rest of a line from the + * character tables. */ + static void saveChoice(boolean sudachi, boolean unidict, boolean pinyin, boolean cldr) { + if (pinyin) { + unidict = true; + } + final boolean[] before = chosen(); + if (!sudachi && before[SUDACHI]) { + JapaneseDictStore.delete(); + } + if (!unidict && before[UNIDICT]) { + UniDictStore.delete(); + } + if (!pinyin && before[PINYIN]) { + PinyinDictStore.delete(); + } + if (!cldr && before[CLDR]) { + CldrTransformsStore.delete(); + } + final StringBuilder value = new StringBuilder(); + if (sudachi) { + value.append("sudachi"); + } + if (unidict) { + if (value.length() > 0) value.append(','); + value.append("unidict"); + } + if (pinyin) { + if (value.length() > 0) value.append(','); + value.append("pinyin"); + } + if (cldr) { + if (value.length() > 0) value.append(','); + value.append("cldr"); + } + Settings.LYRICS_LOCAL_DICTIONARIES.save(value.toString()); + } + + /** Starts only the providers the choice needs. */ + public static synchronized void ensure() { + final boolean[] picked = chosen(); + final boolean needsSudachi = picked[SUDACHI]; + final boolean needsUniDict = picked[UNIDICT]; + final boolean needsPinyin = picked[PINYIN]; + final boolean needsCldr = picked[CLDR]; + + if (!needsSudachi && !needsUniDict && !needsPinyin && !needsCldr) { + return; + } + if (!started) { + if ((!needsSudachi || JapaneseDictStore.isReady()) + && (!needsUniDict || UniDictStore.isReady()) + && (!needsPinyin || PinyinDictStore.isComplete()) + && (!needsCldr || CldrTransformsStore.isReady())) { + return; + } + start(needsSudachi, needsUniDict, needsPinyin, needsCldr); + return; + } + // Reattach in case runtime setup replaced the settings listeners. + if (needsUniDict) { + UniDictStore.ensure(listener(UNIDICT)); + } + if (needsPinyin) { + PinyinDictStore.ensure(listener(PINYIN)); + } + if (needsSudachi) { + JapaneseDictStore.ensure(listener(SUDACHI)); + } + if (needsCldr) { + CldrTransformsStore.ensure(listener(CLDR)); + } + } + + /** + * Forgets an earlier run, so a download the user asked for again reports its outcome. Without + * this the announcement latched by a failure would keep the retry silent. + */ + public static synchronized void resetForUserDownload() { + failed = false; + announced = false; + started = false; + startAnnounced = false; + progress[SUDACHI] = new Progress(); + progress[UNIDICT] = new Progress(); + progress[PINYIN] = new Progress(); + progress[CLDR] = new Progress(); + } + + /** Whether every provider the choice needs is ready. */ + public static boolean isReady() { + final boolean[] picked = chosen(); + if (picked[SUDACHI] && !JapaneseDictStore.isReady()) { + return false; + } + if (picked[UNIDICT] && !UniDictStore.isReady()) { + return false; + } + if (picked[PINYIN] && !PinyinDictStore.isComplete()) { + return false; + } + return !picked[CLDR] || CldrTransformsStore.isReady(); + } + + /** Whether a fetch the user asked for is running and not finished yet. */ + public static synchronized boolean isDownloading() { + return started && !isReady(); + } + + /** Text a dialog shows while a download runs; null once everything is ready. */ + @Nullable + public static CharSequence progressHint() { + final Progress slot = busy(); + if (slot == null || slot.label == null) { + return null; + } + return str(slot.building + ? "morphe_music_lyrics_local_romanization_dict_building" + : "morphe_music_lyrics_local_romanization_dict_downloading", + slot.label, slot.percent); + } + + @Nullable + private static Progress busy() { + for (int i = 0; i < progress.length; i++) { + if (!progress[i].done && progress[i].label != null) { + return progress[i]; + } + } + return null; + } + + public static void attach(@Nullable ProgressSink target) { + sink = target; + } + + public static void detach(@Nullable ProgressSink target) { + if (sink == target) { + sink = null; + } + } + + private static String primarySubtag(String language) { + if (language == null || language.isEmpty() + || "DEFAULT".equalsIgnoreCase(language)) { + return ""; + } + final String lower = language.toLowerCase(Locale.ROOT); + final int dash = lower.indexOf('-'); + return dash >= 0 ? lower.substring(0, dash) : lower; + } + + private static void start(boolean sudachi, boolean unidict, boolean pinyin, boolean cldr) { + started = true; + if (!startAnnounced) { + startAnnounced = true; + Utils.runOnMainThread(() -> Utils.showToastShort( + str("morphe_music_lyrics_local_romanization_download_started"))); + } + if (unidict) { + UniDictStore.ensure(listener(UNIDICT)); + } else { + progress[UNIDICT].done = true; + } + if (pinyin) { + PinyinDictStore.ensure(listener(PINYIN)); + } else { + progress[PINYIN].done = true; + } + if (sudachi) { + JapaneseDictStore.ensure(listener(SUDACHI)); + } else { + progress[SUDACHI].done = true; + } + if (cldr) { + CldrTransformsStore.ensure(listener(CLDR)); + } else { + progress[CLDR].done = true; + } + } + + private static RomanizationDict.Listener listener(int which) { + final Progress slot = progress[which]; + return new RomanizationDict.Listener() { + @Override + public void onDownload(String fileName, int percent) { + slot.label = fileName; + slot.percent = percent; + slot.building = false; + hint(); + } + + @Override + public void onBuilding(int percent) { + slot.percent = percent; + slot.building = true; + hint(); + } + + @Override + public void onReady(int entries) { + slot.entries = entries; + slot.done = true; + hint(); + announce(); + } + + @Override + public void onFailed() { + failed = true; + announce(); + } + }; + } + + /** Only a dialog that is still open is told about progress; a closed one gets the toast. */ + private static void hint() { + final ProgressSink target = sink; + if (target == null) { + return; + } + final CharSequence text = progressHint(); + if (text != null) { + Utils.runOnMainThread(() -> target.onHint(text)); + } + } + + private static synchronized void announce() { + if (announced) { + return; + } + if (failed) { + announced = true; + Utils.runOnMainThread(() -> Utils.showToastShort( + str("morphe_music_lyrics_local_romanization_dict_failed"))); + return; + } + if (!progress[SUDACHI].done || !progress[UNIDICT].done + || !progress[PINYIN].done || !progress[CLDR].done) { + return; + } + announced = true; + final int total = progress[SUDACHI].entries + progress[UNIDICT].entries + + progress[PINYIN].entries + progress[CLDR].entries; + Utils.runOnMainThread(() -> Utils.showToastShort( + str("morphe_music_lyrics_local_romanization_dict_ready", total))); + } +} diff --git a/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsLocalRomanizationPreference.java b/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsLocalRomanizationPreference.java deleted file mode 100644 index e2e65b5837..0000000000 --- a/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsLocalRomanizationPreference.java +++ /dev/null @@ -1,161 +0,0 @@ -/* - * Copyright 2026 Morphe. - * https://github.com/MorpheApp/morphe-patches - * - * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. - */ - -package app.morphe.extension.music.settings.preference; - -import static app.morphe.extension.shared.StringRef.str; - -import android.content.Context; -import android.preference.SwitchPreference; -import android.util.AttributeSet; - -import app.morphe.extension.music.patches.lyrics.romanizers.JapaneseDictStore; -import app.morphe.extension.music.patches.lyrics.romanizers.PinyinDictStore; -import app.morphe.extension.music.patches.lyrics.romanizers.RomanizationDict; -import app.morphe.extension.music.settings.Settings; -import app.morphe.extension.shared.Utils; - -/** - * The switch that turns on reading the lyrics on the device, and the row that fetches what it - * needs. A dictionary is only asked for here, when this row is built or the switch turned on, - * and a switch left on over a cache the system emptied is turned off. - */ -public class LyricsLocalRomanizationPreference extends SwitchPreference { - - private final Progress[] progress = {new Progress(), new Progress()}; - - private boolean failed; - private boolean seenProgress; - private boolean announced; - - private static final class Progress { - String label; - int percent; - int entries; - boolean building; - boolean done; - } - - public LyricsLocalRomanizationPreference(Context context, AttributeSet attrs, int defStyleAttr, - int defStyleRes) { - super(context, attrs, defStyleAttr, defStyleRes); - init(); - } - - public LyricsLocalRomanizationPreference(Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - init(); - } - - public LyricsLocalRomanizationPreference(Context context, AttributeSet attrs) { - super(context, attrs); - init(); - } - - public LyricsLocalRomanizationPreference(Context context) { - super(context); - init(); - } - - private void init() { - if (!Settings.LYRICS_LOCAL_ROMANIZATION.get()) return; - if (PinyinDictStore.isReady() && JapaneseDictStore.isReady()) { - start(); - } else { - Settings.LYRICS_LOCAL_ROMANIZATION.save(false); - } - } - - @Override - protected boolean callChangeListener(Object newValue) { - if (Boolean.TRUE.equals(newValue)) { - start(); - } - return super.callChangeListener(newValue); - } - - private void start() { - failed = false; - seenProgress = false; - announced = false; - PinyinDictStore.ensure(listener(0)); - JapaneseDictStore.ensure(listener(1)); - } - - private RomanizationDict.Listener listener(int which) { - Progress slot = progress[which]; - return new RomanizationDict.Listener() { - @Override - public void onDownload(String fileName, int percent) { - slot.label = fileName; - slot.percent = percent; - slot.building = false; - seenProgress = true; - updateSummary(); - } - - @Override - public void onBuilding(int percent) { - slot.percent = percent; - slot.building = true; - seenProgress = true; - updateSummary(); - } - - @Override - public void onReady(int entries) { - slot.entries = entries; - slot.done = true; - updateSummary(); - } - - @Override - public void onFailed() { - failed = true; - updateSummary(); - } - }; - } - - private void updateSummary() { - Utils.runOnMainThread(() -> { - setSummary(summary()); - announce(); - }); - } - - private void announce() { - if (announced) return; - if (failed) { - announced = true; - Utils.showToastShort(str("morphe_music_lyrics_local_romanization_dict_failed")); - return; - } - if (!seenProgress || !progress[0].done || !progress[1].done) return; - announced = true; - Utils.showToastShort(str("morphe_music_lyrics_local_romanization_dict_ready", - progress[0].entries + progress[1].entries)); - } - - private CharSequence summary() { - if (failed) { - return str("morphe_music_lyrics_local_romanization_dict_failed"); - } - if (progress[0].done && progress[1].done) { - return str("morphe_music_lyrics_local_romanization_dict_ready", - progress[0].entries + progress[1].entries); - } - for (Progress slot : progress) { - if (slot.done || slot.label == null) continue; - return str(slot.building - ? "morphe_music_lyrics_local_romanization_dict_building" - : "morphe_music_lyrics_local_romanization_dict_downloading", - slot.label, slot.percent); - } - return str("morphe_music_lyrics_local_romanization_summary"); - } -} diff --git a/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsOrderedListPreference.java b/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsOrderedListPreference.java index a1ac8dbb80..ceb828a2fc 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsOrderedListPreference.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsOrderedListPreference.java @@ -9,19 +9,25 @@ import static app.morphe.extension.shared.StringRef.str; -import android.app.AlertDialog; +import android.app.Dialog; import android.content.ClipData; import android.content.Context; import android.content.Intent; +import android.content.res.ColorStateList; import android.net.Uri; import android.preference.Preference; import android.text.TextUtils; import android.util.AttributeSet; +import android.util.Pair; +import android.util.TypedValue; import android.view.DragEvent; import android.view.Gravity; import android.view.View; import android.view.ViewGroup; import android.view.WindowManager; +import android.widget.Button; +import android.widget.CheckBox; +import android.widget.CompoundButton; import android.widget.LinearLayout; import android.widget.Switch; import android.widget.TextView; @@ -29,28 +35,41 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Set; +import androidx.annotation.Nullable; + +import app.morphe.extension.music.patches.lyrics.derive.AiProviders; +import app.morphe.extension.music.patches.lyrics.romanizers.RimeDictStore; import app.morphe.extension.music.settings.Settings; import app.morphe.extension.shared.Logger; +import app.morphe.extension.shared.Utils; import app.morphe.extension.shared.settings.StringSetting; import app.morphe.extension.shared.theme.ThemeUtils; +import app.morphe.extension.shared.ui.CustomDialog; import app.morphe.extension.shared.ui.Dim; /** - * Preference that shows the third-party lyrics providers inline on the settings screen and lets the - * user reorder them by long-pressing a row and dragging it, and toggle each one on or off with the - * switch on the right. The order is stored as a comma separated list of provider ids - * (their {@code LyricsProvider#name()} value) in {@link Settings#LYRICS_SOURCE}, with a leading - * {@code '-'} marking a disabled provider. + * Settings row that shows providers in priority order, with a switch for each one. Dragging + * reorders them. The same row serves lyrics sources, translation and romanization providers. + * The value is a comma separated id list; a leading {@code -} disables a provider. */ @SuppressWarnings({"unused", "deprecation"}) public final class LyricsOrderedListPreference extends Preference { - private static final List PROVIDER_ORDER = Arrays.asList( + private static final String KEY_TRANSLATION_PROVIDER = + "morphe_music_lyrics_translation_provider"; + private static final String KEY_ROMANIZATION_PROVIDER = + "morphe_music_lyrics_romanization_provider"; + + /** The fixed last row of the service list; it is not a lookup source and holds no order. */ + private static final String QUICKLRC = "QuickLRC"; + + /** The grip column, the same width on every row so the names start at one line. */ + private static final int GRIP_COLUMN = Dim.dp24; + + private static final List SOURCE_PROVIDER_ORDER = Arrays.asList( "Apple", "BiniLyrics", "LrcRed", "bLyrics", "Lyricify", "Unison", "PetitLyrics", "AMLL", "QQ", "SimpMusic", "KuGou", "Musixmatch", "YTMusic", "LRCLIB", "NetEase", "Luna", "LunaBeat", @@ -71,6 +90,7 @@ public final class LyricsOrderedListPreference extends Preference { PROVIDER_LABELS.put("Deezer", "Deezer *"); PROVIDER_LABELS.put("Genius", "Genius"); PROVIDER_LABELS.put("LrcRed", "lrc.red"); + PROVIDER_LABELS.put(QUICKLRC, "QuickLRC *"); } private static final Map PROVIDER_URLS = new HashMap<>(); @@ -93,11 +113,15 @@ public final class LyricsOrderedListPreference extends Preference { PROVIDER_URLS.put("LrcRed", "https://lrc.red/"); } - private static String providerLabel(String id) { + private static String sourceProviderLabel(String id) { String label = PROVIDER_LABELS.get(id); return label != null ? label : id; } + private enum Kind { + SOURCE, TRANSLATION, ROMANIZATION + } + private static final class Item { final String id; boolean enabled; @@ -112,6 +136,433 @@ private static int withAlpha(int color, int alpha) { return (alpha << 24) | (color & 0x00FFFFFF); } + /** + * A CheckBox defaults to the dialog theme's own label colour, which the dark app theme leaves + * black on a dark dialog. Its box follows the text colour too, so both are set from the app. + */ + private static void styleCheckBox(CheckBox box, int fg) { + box.setTextColor(fg); + final int[][] states = {{android.R.attr.state_checked}, {}}; + box.setButtonTintList(new ColorStateList(states, new int[]{fg, withAlpha(fg, 0x66)})); + } + + private Kind kind() { + final String key = getKey(); + if (KEY_TRANSLATION_PROVIDER.equals(key)) { + return Kind.TRANSLATION; + } + if (KEY_ROMANIZATION_PROVIDER.equals(key)) { + return Kind.ROMANIZATION; + } + return Kind.SOURCE; + } + + private List providerOrder() { + return switch (kind()) { + case SOURCE -> SOURCE_PROVIDER_ORDER; + case TRANSLATION -> AiProviders.TRANSLATION_ORDER; + case ROMANIZATION -> AiProviders.ROMANIZATION_ORDER; + }; + } + + private StringSetting providerSetting() { + return switch (kind()) { + case SOURCE -> Settings.LYRICS_SOURCE; + case TRANSLATION -> Settings.LYRICS_TRANSLATION_PROVIDERS; + case ROMANIZATION -> Settings.LYRICS_ROMANIZATION_PROVIDERS; + }; + } + + private String providerLabel(String id) { + final String label = switch (kind()) { + case SOURCE -> sourceProviderLabel(id); + case TRANSLATION -> switch (id) { + case AiProviders.EMBEDDED -> AiProviders.EMBEDDED_LABEL; + case AiProviders.GOOGLE -> AiProviders.GOOGLE_LABEL; + case AiProviders.OPENAI_COMPATIBLE -> AiProviders.OPENAI_COMPATIBLE_LABEL; + case AiProviders.DEEPL -> AiProviders.DEEPL_LABEL; + default -> id; + }; + case ROMANIZATION -> switch (id) { + case AiProviders.EMBEDDED -> AiProviders.EMBEDDED_LABEL; + case AiProviders.GOOGLE -> AiProviders.GOOGLE_LABEL; + case AiProviders.OPENAI_COMPATIBLE -> AiProviders.OPENAI_COMPATIBLE_LABEL; + default -> str("morphe_music_lyrics_romanization_provider_" + id); + }; + }; + // The star tells the reader that the name opens a dialog, the same as the source list. + return opensSettingsDialog(id) ? label + " *" : label; + } + + private boolean isLyricsSource() { + return kind() == Kind.SOURCE; + } + + private static boolean isLocalProvider(String id) { + return AiProviders.LOCAL.equals(id); + } + + private boolean isRimeProvider(String id) { + return kind() == Kind.ROMANIZATION && AiProviders.RIME.equals(id); + } + + /** The star marks a row that needs a token before it can be on, as in the source list. */ + private static boolean isQuickLrcKeyMissing() { + final String key = Settings.LYRICS_TRANSCRIBE_KEY.get(); + return key == null || key.trim().isEmpty(); + } + + /** Providers whose settings are entered in a dialog when their name is tapped. */ + private boolean hasConfigDialog(String id) { + if (isLyricsSource()) { + return false; + } + return switch (id) { + case AiProviders.OPENAI_COMPATIBLE -> true; + case AiProviders.DEEPL -> kind() == Kind.TRANSLATION; + default -> false; + }; + } + + /** + * Whether the name of a row opens a dialog. The star in the row is what tells the reader that, + * so it covers the offline rows too, which open their own dictionary chooser rather than + * being validated against a server. + */ + private boolean opensSettingsDialog(String id) { + return hasConfigDialog(id) || isLocalProvider(id) || isRimeProvider(id); + } + + private void showConfigDialog(String id) { + if (!(getContext() instanceof android.app.Activity activity) + || activity.isFinishing() || activity.isDestroyed()) { + return; + } + LyricsProviderConfigDialog.show(activity, id, promptSettingFor(kind()), this::updateSummary); + } + + /** The prompt an AI service is sent; DeepL translates without one. */ + @Nullable + private static StringSetting promptSettingFor(Kind kind) { + return switch (kind) { + case TRANSLATION -> Settings.LYRICS_AI_TRANSLATION_PROMPT; + case ROMANIZATION -> Settings.LYRICS_AI_ROMANIZATION_PROMPT; + case SOURCE -> null; + }; + } + + /** + * Switches the provider on only after it answered; an unusable key fails silently at playback. + */ + private void enableAfterValidation(Item item, Switch switchV, TextView name) { + final int fg = ThemeUtils.getAppForegroundColor(); + Utils.showToastShort(str("morphe_music_lyrics_ai_config_status_validating")); + LyricsProviderConfigDialog.validate(item.id, valid -> { + if (!isEnabled()) { + return; + } + // Only a failure has to undo the switch, which stayed on while the request ran. + if (!valid) { + switchV.setChecked(false); + } + item.enabled = valid; + name.setTextColor(valid ? fg : withAlpha(fg, 0x66)); + saveItems(); + updateSummary(); + if (!valid) { + Utils.showToastShort(str("morphe_music_lyrics_ai_config_status_invalid")); + } + }); + } + + /** + * Local needs its dictionary before it can romanize anything. It is asked for first and only + * switched on once downloading has started, so a refused download never leaves it enabled. + */ + private void enableLocal(Item item, Switch switchV, TextView name, + Runnable onEnabled, Runnable onRefused) { + final boolean[] chosen = LyricsLocalRomanizationDownloads.chosen(); + if (!chosen[LyricsLocalRomanizationDownloads.SUDACHI] + && !chosen[LyricsLocalRomanizationDownloads.UNIDICT] + && !chosen[LyricsLocalRomanizationDownloads.PINYIN] + && !chosen[LyricsLocalRomanizationDownloads.CLDR]) { + // Read by rule alone, so there is nothing to ask for. + enableRow(switchV, name); + onEnabled.run(); + return; + } + if (LyricsLocalRomanizationDownloads.isReady()) { + enableRow(switchV, name); + onEnabled.run(); + LyricsLocalRomanizationDownloads.ensure(); + return; + } + + final Context context = getContext(); + if (!(context instanceof android.app.Activity activity) + || activity.isFinishing() || activity.isDestroyed()) { + onRefused.run(); + return; + } + showDictionaryDialog(context, switchV, name, chosen, onEnabled, onRefused); + } + + /** The name opens the same chooser the switch asks with, without turning the row on. */ + private void showLocalSettings() { + final Context context = getContext(); + if (!(context instanceof android.app.Activity activity) + || activity.isFinishing() || activity.isDestroyed()) { + return; + } + showDictionaryDialog(context, null, null, LyricsLocalRomanizationDownloads.chosen(), + () -> { }, () -> { }); + } + + /** + * The Rime word lists are this provider's settings. Opening them never turns the row on: a + * list ticked while the provider is off waits for it. + */ + private void showRimeSettings() { + final Context context = getContext(); + if (!(context instanceof android.app.Activity activity) + || activity.isFinishing() || activity.isDestroyed()) { + return; + } + LyricsRimeDictDialog.show(context, () -> { + // A ticked table for the language above turns the row on (checklist §4.1), + // the same background-fetch + toast model the Local dictionary uses. + turnOnRimeIfTable(); + turnOffRimeWithoutTable(); + updateSummary(); + }, null); + } + + /** The name-tap Save may have ticked a table for the language above; turn the row on + * beside that fetch rather than leaving it off until the user taps the switch. */ + private void turnOnRimeIfTable() { + for (Item item : items) { + if (isRimeProvider(item.id) && !item.enabled && RimeDictStore.isActive()) { + item.enabled = true; + saveItems(); + rebuildRows(); + return; + } + } + } + + /** A Save can untick the only list the chosen language reads; turn the row off rather than + * leave it on to return nothing, and toast why. */ + private void turnOffRimeWithoutTable() { + for (Item item : items) { + if (isRimeProvider(item.id) && item.enabled && !RimeDictStore.isActive()) { + item.enabled = false; + saveItems(); + rebuildRows(); + Utils.showToastShort(str("morphe_music_lyrics_rime_dict_no_table")); + return; + } + } + } + + /** + * Rime Wordlist reads Han through the word table of the language picked above, so it can only be + * on for a language that has one. A table that is ticked but not built yet is fetched as the + * row turns on. + * + *

    With no table ticked the switch opens the list instead of refusing, the same way the + * Local row asks which dictionaries to fetch. The row is only saved on once a Save left a + * table for the chosen language. + */ + private void enableRime(Switch switchV, TextView name, Runnable onEnabled, Runnable onRefused) { + if (!RimeDictStore.isActive()) { + final Context context = getContext(); + if (!(context instanceof android.app.Activity activity) + || activity.isFinishing() || activity.isDestroyed()) { + onRefused.run(); + return; + } + final boolean[] turnedOn = {false}; + LyricsRimeDictDialog.show(context, () -> { + if (turnedOn[0] || !RimeDictStore.isActive()) { + return; + } + // The dialog's Save already fetches every ticked table, this language's + // included, so the row is turned on beside that fetch rather than by another. + turnedOn[0] = true; + enableRow(switchV, name); + onEnabled.run(); + }, () -> { + if (turnedOn[0]) { + return; + } + onRefused.run(); + // Closed without a Save, or saved with nothing ticked for the language above. + Utils.showToastShort(str("morphe_music_lyrics_rime_dict_no_table")); + }); + return; + } + enableRow(switchV, name); + onEnabled.run(); + Utils.runOnBackgroundThread( + () -> RimeDictStore.ensureForProvider(RimeDictStore.SILENT)); + } + + private static void enableRow(Switch switchV, TextView name) { + switchV.setChecked(true); + name.setTextColor(ThemeUtils.getAppForegroundColor()); + } + + /** + * Asks which readings to fetch, then shows their progress in the same dialog. Closing it stops + * the progress updates; the download itself ends with a toast either way. + * + * @param switchV null when opened from the name rather than from the switch. + */ + private void showDictionaryDialog(Context context, @Nullable Switch switchV, + @Nullable TextView name, boolean[] chosen, + Runnable onEnabled, Runnable onRefused) { + final int fg = ThemeUtils.getAppForegroundColor(); + + final TextView hint = new TextView(context); + hint.setTextSize(14); + hint.setTextColor(withAlpha(fg, 0x99)); + hint.setText(str("morphe_music_lyrics_local_romanization_summary")); + hint.setPadding(0, 0, 0, Dim.dp12); + + final CheckBox japanese = new CheckBox(context); + japanese.setText(str("morphe_music_lyrics_local_romanization_dict_japanese")); + japanese.setChecked(chosen[LyricsLocalRomanizationDownloads.SUDACHI]); + styleCheckBox(japanese, fg); + + final CheckBox unidict = new CheckBox(context); + unidict.setText(str("morphe_music_lyrics_local_romanization_dict_unidict")); + unidict.setChecked(chosen[LyricsLocalRomanizationDownloads.UNIDICT]); + styleCheckBox(unidict, fg); + + final CheckBox chinese = new CheckBox(context); + chinese.setText(str("morphe_music_lyrics_local_romanization_dict_chinese")); + chinese.setChecked(chosen[LyricsLocalRomanizationDownloads.PINYIN]); + styleCheckBox(chinese, fg); + + final CheckBox cldr = new CheckBox(context); + cldr.setText(str("morphe_music_lyrics_local_romanization_dict_cldr")); + cldr.setChecked(chosen[LyricsLocalRomanizationDownloads.CLDR]); + styleCheckBox(cldr, fg); + + // A Chinese word table reads the rest of a line from the character tables, so choosing + // one keeps the character provider on. + final CompoundButton.OnCheckedChangeListener keepUniDict = (button, checked) -> { + if (checked) { + unidict.setChecked(true); + } + }; + chinese.setOnCheckedChangeListener(keepUniDict); + + final LinearLayout content = new LinearLayout(context); + content.setOrientation(LinearLayout.VERTICAL); + content.addView(hint); + content.addView(japanese); + content.addView(unidict); + content.addView(chinese); + content.addView(cldr); + + // Attached here rather than on the dialog so a close in any way detaches it. A watched + // sink also survives a rebuild: only detach takes it away. + final LyricsLocalRomanizationDownloads.ProgressSink sink = hint::setText; + // Set once the user agrees: the dismiss listener then knows the choice was saved, so + // closing must not put the row back. Save closes right away; the download runs in the + // background and answers with a toast. + final boolean[] accepted = {false}; + + final Pair pair = CustomDialog.create(context, + str("morphe_music_lyrics_local_romanization_download_confirm_title"), + null, + null, + str("morphe_settings_save"), + () -> { }, + () -> { }, + null, + null, + false); + + final Dialog dialog = pair.first; + final LinearLayout mainLayout = pair.second; + final android.widget.ScrollView scrollView = new android.widget.ScrollView(context); + scrollView.addView(content, new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); + LinearLayout.LayoutParams scrollParams = new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); + scrollParams.bottomMargin = Dim.dp12; + mainLayout.addView(scrollView, mainLayout.getChildCount() - 1, scrollParams); + + // Save writes the choice and closes; the download runs in the background and answers with + // a toast. Cancel closes without changing anything, and the dismiss listener puts the row + // back if the dialog went away without an agreed choice. + final Button ok = button(mainLayout, str("morphe_settings_save")); + ok.setOnClickListener(v -> { + if (!japanese.isChecked() && !unidict.isChecked() + && !chinese.isChecked() && !cldr.isChecked()) { + Utils.showToastShort(str("morphe_music_lyrics_local_romanization_dict_empty")); + return; + } + LyricsLocalRomanizationDownloads.saveChoice( + japanese.isChecked(), unidict.isChecked(), + chinese.isChecked(), cldr.isChecked()); + accepted[0] = true; + if (switchV == null) { + // Opened from the name, so only a row that is already on fetches what is missing. + dialog.dismiss(); + if (isLocalEnabled()) { + LyricsLocalRomanizationDownloads.resetForUserDownload(); + LyricsLocalRomanizationDownloads.ensure(); + } + return; + } + dialog.dismiss(); + LyricsLocalRomanizationDownloads.resetForUserDownload(); + enableRow(switchV, name); + // Started before the row is saved, so the "dictionaries are missing" repair that + // save runs does not turn the row back off while its download is running. + LyricsLocalRomanizationDownloads.ensure(); + onEnabled.run(); + }); + + dialog.setOnDismissListener(d -> { + LyricsLocalRomanizationDownloads.detach(sink); + // Closing without agreeing never became a choice, so the row is put back and the + // list is left exactly as it was. + if (!accepted[0]) { + onRefused.run(); + } + }); + + LyricsLocalRomanizationDownloads.attach(sink); + dialog.show(); + final CharSequence running = LyricsLocalRomanizationDownloads.progressHint(); + if (running != null) { + // A download already under way: show it, but a choice is still what starts it again. + hint.setText(running); + } + } + + /** Found by label because the button row is reordered when the labels do not fit. */ + private static Button button(ViewGroup layout, CharSequence text) { + for (int i = 0; i < layout.getChildCount(); i++) { + final View child = layout.getChildAt(i); + if (child instanceof Button + && text.toString().contentEquals(((Button) child).getText())) { + return (Button) child; + } + if (child instanceof ViewGroup) { + final Button found = button((ViewGroup) child, text); + if (found != null) { + return found; + } + } + } + return null; + } + private final List items = new ArrayList<>(); private LinearLayout rowsContainer; private int dragIndex = -1; @@ -148,17 +599,24 @@ protected void onClick() { return; } try { - new AlertDialog.Builder(getContext()) - .setTitle(str("morphe_music_lyrics_source_reset_title")) - .setMessage(str("morphe_music_lyrics_source_reset_message")) - .setPositiveButton(android.R.string.ok, (d, w) -> { - Settings.LYRICS_SOURCE.resetToDefault(); + CustomDialog.create(getContext(), + str("morphe_music_lyrics_source_reset_title"), + str("morphe_music_lyrics_source_reset_message"), + null, + null, + () -> { + providerSetting().resetToDefault(); + if (isLyricsSource()) { + Settings.LYRICS_QUICKLRC_ENABLED.resetToDefault(); + } loadItems(); updateSummary(); rebuildRows(); - }) - .setNegativeButton(android.R.string.cancel, null) - .show(); + }, + () -> { }, + null, + null, + true).first.show(); } catch (WindowManager.BadTokenException ex) { Logger.printException(() -> "Could not show the reset dialog", ex); } @@ -167,8 +625,7 @@ protected void onClick() { @Override protected void onAttachedToHierarchy(android.preference.PreferenceManager preferenceManager) { super.onAttachedToHierarchy(preferenceManager); - setTitle(null); - // Load eagerly so the summary is correct before the row is first drawn. + // Keep the summary correct before the first draw. try { loadItems(); updateSummary(); @@ -182,8 +639,7 @@ public View getView(View convertView, ViewGroup parent) { try { return buildView(convertView, parent); } catch (Throwable ignored) { - // Bound from the settings list, where an exception would take the whole - // screen down. The sibling entry point guards itself the same way. + // A failure here would take down the whole settings screen. LinearLayout fallback = new LinearLayout(getContext()); fallback.setOrientation(LinearLayout.VERTICAL); fallback.setLayoutParams(new ViewGroup.LayoutParams( @@ -194,6 +650,7 @@ public View getView(View convertView, ViewGroup parent) { private View buildView(View convertView, ViewGroup parent) { loadItems(); + updateSummary(); Context context = getContext(); final int fg = ThemeUtils.getAppForegroundColor(); @@ -208,6 +665,18 @@ private View buildView(View convertView, ViewGroup parent) { } }); + CharSequence title = getTitle(); + if (!TextUtils.isEmpty(title)) { + TextView titleView = new TextView(context); + titleView.setText(title); + titleView.setTextSize(16); + titleView.setTextColor(fg); + LinearLayout.LayoutParams tp = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT); + tp.topMargin = Dim.dp4; + root.addView(titleView, tp); + } + CharSequence summary = getSummary(); if (!TextUtils.isEmpty(summary)) { TextView summaryView = new TextView(context); @@ -229,6 +698,54 @@ private View buildView(View convertView, ViewGroup parent) { return root; } + /** How many providers are on; a service has to keep at least one. */ + private int enabledCount() { + int count = 0; + for (Item item : items) { + if (item.enabled) { + count++; + } + } + return count; + } + + /** + * Puts a provider back on when a stored list, or a provider whose settings were cleared, left + * the service with nothing. The switch refuses to turn the last one off, so this only repairs + * a list written before that rule existed. Sources are left alone: reading no lyrics at all is + * a state the source list offers on purpose. + */ + private void keepOneEnabled() { + if (isLyricsSource() || enabledCount() > 0) { + return; + } + for (Item item : items) { + // Embedded and Google are always in the list and never need setting up, so one of + // them answers here; an unconfigured row would only be turned off again on saving. + if (!isUnconfigured(item.id)) { + item.enabled = true; + return; + } + } + // Unreachable while those two are in every order, but the list must never end up empty. + if (!items.isEmpty()) { + items.get(0).enabled = true; + } + } + + /** Whether a provider that needs setting up would still fail if it were on. */ + private boolean isUnconfigured(String id) { + if (kind() == Kind.TRANSLATION && AiProviders.DEEPL.equals(id)) { + return Settings.LYRICS_DEEPL_API_KEY.get().isBlank(); + } + return switch (id) { + case AiProviders.OPENAI_COMPATIBLE -> + Settings.LYRICS_AI_BASE_URL.get().isBlank() + || Settings.LYRICS_AI_MODEL.get().isBlank(); + default -> false; + }; + } + private static boolean isTokenRequired(String id) { StringSetting setting = switch (id) { case "Captions" -> Settings.LYRICS_CAPTION_COOKIES; @@ -278,6 +795,77 @@ private void rebuildRows() { for (int i = 0; i < items.size(); i++) { rowsContainer.addView(createRow(items.get(i), i)); } + if (isLyricsSource()) { + rowsContainer.addView(createQuickLrcRow()); + } + } + + /** + * QuickLRC gives the original lyrics rather than a lookup source, so it sits below every + * source and keeps no position of its own: it only records whether the service may be used. + */ + private View createQuickLrcRow() { + Context context = getContext(); + final int fg = ThemeUtils.getAppForegroundColor(); + final boolean locked = !isEnabled(); + final boolean enabled = Settings.LYRICS_QUICKLRC_ENABLED.get(); + + LinearLayout row = new LinearLayout(context); + row.setOrientation(LinearLayout.HORIZONTAL); + row.setGravity(Gravity.CENTER_VERTICAL); + row.setPadding(0, Dim.dp8, 0, Dim.dp8); + + row.addView(createGripSpacer(context)); + + TextView name = new TextView(context); + name.setText(providerLabel(QUICKLRC)); + name.setTextSize(16); + name.setTextColor(locked ? withAlpha(fg, 0x44) : (enabled ? fg : withAlpha(fg, 0x66))); + name.setLayoutParams(new LinearLayout.LayoutParams( + 0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)); + if (!locked) { + name.setClickable(true); + name.setOnClickListener(v -> LyricsTranscribeKeyPreference.showKeyDialog( + context, () -> rebuildRows())); + } + + Switch switchV = new Switch(context); + switchV.setChecked(enabled); + switchV.setEnabled(!locked); + switchV.setPadding(Dim.dp8, 0, 0, 0); + if (!locked) { + switchV.setOnCheckedChangeListener((v, checked) -> { + if (!checked) { + Settings.LYRICS_QUICKLRC_ENABLED.save(false); + name.setTextColor(withAlpha(fg, 0x66)); + return; + } + if (isQuickLrcKeyMissing()) { + switchV.setChecked(false); + LyricsTranscribeKeyPreference.showKeyDialog(context, () -> { + if (isQuickLrcKeyMissing()) { + return; + } + Settings.LYRICS_QUICKLRC_ENABLED.save(true); + rebuildRows(); + }); + return; + } + Settings.LYRICS_QUICKLRC_ENABLED.save(true); + name.setTextColor(fg); + }); + } + + row.addView(name); + row.addView(switchV); + return row; + } + + /** Holds the grip's place on a row that cannot be dragged, at the same width. */ + private static View createGripSpacer(Context context) { + View spacer = new View(context); + spacer.setLayoutParams(new LinearLayout.LayoutParams(GRIP_COLUMN, 1)); + return spacer; } private View createRow(Item item, int itemsIndex) { @@ -289,15 +877,26 @@ private View createRow(Item item, int itemsIndex) { row.setOrientation(LinearLayout.HORIZONTAL); row.setGravity(Gravity.CENTER_VERTICAL); row.setPadding(0, Dim.dp8, 0, Dim.dp8); - row.setLongClickable(!locked); + // A pinned row starts no drag at all, rather than one that snaps back. + final boolean draggable = !locked && !isPinnedRow(itemsIndex); + row.setLongClickable(draggable); row.setTag(itemsIndex); - TextView grip = new TextView(context); - grip.setText("⠿"); - grip.setTextSize(20); - grip.setTypeface(android.graphics.Typeface.DEFAULT); - grip.setTextColor(withAlpha(fg, locked ? 0x44 : 0xAA)); - grip.setPadding(0, 0, Dim.dp8, 0); + final View grip; + if (draggable) { + TextView handle = new TextView(context); + handle.setText("⠿"); + // An icon, so it keeps its size when the system font changes and the + // fixed column never has to grow with it. + handle.setTextSize(TypedValue.COMPLEX_UNIT_PX, Dim.dp20); + handle.setGravity(Gravity.CENTER); + handle.setTextColor(withAlpha(fg, 0xAA)); + handle.setLayoutParams(new LinearLayout.LayoutParams( + GRIP_COLUMN, LinearLayout.LayoutParams.WRAP_CONTENT)); + grip = handle; + } else { + grip = createGripSpacer(context); + } TextView name = new TextView(context); name.setText(providerLabel(item.id)); @@ -305,13 +904,22 @@ private View createRow(Item item, int itemsIndex) { name.setTextColor(locked ? withAlpha(fg, 0x44) : (item.enabled ? fg : withAlpha(fg, 0x66))); name.setLayoutParams(new LinearLayout.LayoutParams( 0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)); - if (!locked && hasTokenDialog(item.id)) { + if (!locked && isLyricsSource() && hasTokenDialog(item.id)) { name.setClickable(true); name.setOnClickListener(v -> showTokenDialogFor(item.id, null)); - } else if (!locked && PROVIDER_URLS.containsKey(item.id)) { - String url = PROVIDER_URLS.get(item.id); + } else if (!locked && isLyricsSource() && PROVIDER_URLS.containsKey(item.id)) { + final String url = PROVIDER_URLS.get(item.id); name.setClickable(true); name.setOnClickListener(v -> openUrl(url)); + } else if (!locked && isLocalProvider(item.id)) { + name.setClickable(true); + name.setOnClickListener(v -> showLocalSettings()); + } else if (!locked && isRimeProvider(item.id)) { + name.setClickable(true); + name.setOnClickListener(v -> showRimeSettings()); + } else if (!locked && hasConfigDialog(item.id)) { + name.setClickable(true); + name.setOnClickListener(v -> showConfigDialog(item.id)); } Switch switchV = new Switch(context); @@ -320,7 +928,7 @@ private View createRow(Item item, int itemsIndex) { switchV.setPadding(Dim.dp8, 0, 0, 0); if (!locked) { switchV.setOnCheckedChangeListener((v, checked) -> { - if (checked && isTokenRequired(item.id)) { + if (checked && isLyricsSource() && isTokenRequired(item.id)) { switchV.setChecked(false); showTokenDialogFor(item.id, () -> { if (isTokenRequired(item.id)) { @@ -337,6 +945,49 @@ private View createRow(Item item, int itemsIndex) { }); return; } + if (checked && hasConfigDialog(item.id)) { + // Left on until the answer arrives; moving the widget back here would + // re-enter this listener before the provider was asked. + item.enabled = true; + name.setTextColor(fg); + enableAfterValidation(item, switchV, name); + return; + } + if (checked && isLocalProvider(item.id)) { + // The download dialog decides whether the switch stays on, so the row is + // saved only once it closes. + enableLocal(item, switchV, name, () -> { + item.enabled = true; + saveItems(); + updateSummary(); + }, () -> { + item.enabled = false; + saveItems(); + updateSummary(); + switchV.setChecked(false); + }); + return; + } + if (checked && isRimeProvider(item.id)) { + enableRime(switchV, name, () -> { + item.enabled = true; + saveItems(); + updateSummary(); + }, () -> { + item.enabled = false; + saveItems(); + updateSummary(); + switchV.setChecked(false); + }); + return; + } + if (!checked && item.enabled && !isLyricsSource() && enabledCount() <= 1) { + // A service with nothing on has no way to translate or romanize, so the + // last provider cannot be turned off. + switchV.setChecked(true); + Utils.showToastShort(str("morphe_music_lyrics_providers_keep_one")); + return; + } item.enabled = checked; name.setTextColor(checked ? fg : withAlpha(fg, 0x66)); saveItems(); @@ -348,7 +999,7 @@ private View createRow(Item item, int itemsIndex) { row.addView(name); row.addView(switchV); - if (!locked) { + if (draggable) { row.setOnLongClickListener(v -> { dragIndex = (int) v.getTag(); dragReordered = false; @@ -365,8 +1016,10 @@ private View createRow(Item item, int itemsIndex) { final int target = (int) v.getTag(); if (target != dragIndex && target >= 0) { final int from = dragIndex; + // The hovered row is the destination; an insertion boundary made the + // last row unreachable. reorder(from, target); - moveRow(from, from < target ? target - 1 : target); + moveRow(from, target); dragIndex = target; } return true; @@ -392,7 +1045,8 @@ private View createRow(Item item, int itemsIndex) { } private void moveRow(int from, int to) { - if (rowsContainer == null || rowsContainer.getChildCount() != items.size()) { + final int expected = items.size() + (isLyricsSource() ? 1 : 0); + if (rowsContainer == null || rowsContainer.getChildCount() != expected) { rebuildRows(); return; } @@ -407,56 +1061,80 @@ private void moveRow(int from, int to) { } } + /** The first entry of a translation or romanization list is pinned and never moves. */ + private boolean isPinnedRow(int index) { + return index == 0 && !isLyricsSource(); + } + private void reorder(int from, int to) { if (from == to || from < 0 || to < 0 || from >= items.size() || to >= items.size()) { return; } + if (isPinnedRow(from) || isPinnedRow(to)) { + return; + } Item moved = items.remove(from); - items.add(from < to ? to - 1 : to, moved); + items.add(to, moved); dragReordered = true; } private void loadItems() { items.clear(); - String stored = Settings.LYRICS_SOURCE.get(); - Set seen = new HashSet<>(); - - if (stored.contains(",")) { - for (String raw : stored.split(",")) { - String token = raw.trim(); - if (token.isEmpty()) { - continue; - } - boolean enabled = true; - if (token.startsWith("-")) { - enabled = false; - token = token.substring(1).trim(); - } - if (!PROVIDER_ORDER.contains(token) || seen.contains(token)) { - continue; - } - seen.add(token); - items.add(new Item(token, enabled)); - } + String stored = providerSetting().get(); + if (isLyricsSource() && (stored == null || !stored.contains(","))) { + // Match LyricsProviders, which treats a legacy single source as the default order. + stored = Settings.DEFAULT_LYRICS_ORDER; } - - // Show every provider not yet loaded. - for (String id : PROVIDER_ORDER) { - if (!seen.contains(id)) { - items.add(new Item(id, true)); - } + for (AiProviders.ProviderEntry entry : AiProviders.parseOrder(stored, providerOrder())) { + items.add(new Item(entry.id(), entry.enabled())); } - if (disableTokenlessItems()) { + // A list stored before the "at least one" rule, or one whose only provider has since lost + // its settings, is repaired on the way in rather than shown with nothing on. + final boolean turnedOff = disableTokenlessItems(); + if (turnedOff || (!isLyricsSource() && enabledCount() == 0)) { saveItems(); } } + private boolean isLocalEnabled() { + for (Item item : items) { + if (isLocalProvider(item.id) && item.enabled) { + return true; + } + } + return false; + } + + /** + * A provider that cannot answer is turned off rather than left on to fail at every playback. + * For sources a missing token counts as unconfigured; for the other lists it is the settings + * its dialog holds. Local is turned off when its dictionaries are not all there: the reading + * it would give would be missing whole scripts, and nothing is downloaded here, so an off row + * is the honest state until the user fetches them from the switch. A fetch that is already + * running is not cut short; it was asked for and gets to finish. + */ private boolean disableTokenlessItems() { boolean changed = false; for (Item item : items) { - if (item.enabled && isTokenRequired(item.id)) { + if (!item.enabled) { + continue; + } + if (isLyricsSource() ? isTokenRequired(item.id) : isUnconfigured(item.id)) { + item.enabled = false; + changed = true; + continue; + } + if (isLocalProvider(item.id) && !LyricsLocalRomanizationDownloads.isReady() + && !LyricsLocalRomanizationDownloads.isDownloading()) { + item.enabled = false; + changed = true; + continue; + } + // Rime Wordlist reads through the word list of the language above, so a row with no table + // is turned off rather than left on to return an empty line. + if (isRimeProvider(item.id) && !RimeDictStore.isActive()) { item.enabled = false; changed = true; } @@ -466,28 +1144,26 @@ private boolean disableTokenlessItems() { private void saveItems() { disableTokenlessItems(); - StringBuilder sb = new StringBuilder(); + keepOneEnabled(); + List entries = new ArrayList<>(items.size()); for (Item item : items) { - //noinspection SizeReplaceableByIsEmpty - if (sb.length() > 0) { - sb.append(','); - } - if (!item.enabled) { - sb.append('-'); - } - sb.append(item.id); + entries.add(new AiProviders.ProviderEntry(item.id, item.enabled)); } - Settings.LYRICS_SOURCE.save(sb.toString()); + providerSetting().save(AiProviders.serializeOrder(entries)); } private void updateSummary() { List enabled = new ArrayList<>(); for (Item item : items) { - if (item.enabled && !isTokenRequired(item.id)) { + if (item.enabled && !(isLyricsSource() && isTokenRequired(item.id))) { enabled.add(providerLabel(item.id)); } } + if (isLyricsSource() && Settings.LYRICS_QUICKLRC_ENABLED.get()) { + enabled.add(providerLabel(QUICKLRC)); + } if (enabled.isEmpty()) { + // Only a source list can be empty here: the other lists keep one provider on. setSummary(str("morphe_music_lyrics_source_all_disabled")); } else { setSummary(TextUtils.join(" > ", enabled)); diff --git a/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsProviderConfigDialog.java b/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsProviderConfigDialog.java new file mode 100644 index 0000000000..8dc1309827 --- /dev/null +++ b/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsProviderConfigDialog.java @@ -0,0 +1,533 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.extension.music.settings.preference; + +import static app.morphe.extension.shared.StringRef.str; + +import android.app.Activity; +import android.app.Dialog; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.text.InputType; +import android.util.Pair; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.widget.Button; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.TextView; + +import androidx.annotation.Nullable; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.function.Consumer; +import java.util.function.Supplier; + +import app.morphe.extension.music.patches.lyrics.derive.AiProviders; +import app.morphe.extension.music.patches.lyrics.derive.DeepLClient; +import app.morphe.extension.music.patches.lyrics.derive.OpenAIClient; +import app.morphe.extension.music.settings.Settings; +import app.morphe.extension.shared.Logger; +import app.morphe.extension.shared.settings.StringSetting; +import app.morphe.extension.shared.Utils; +import app.morphe.extension.shared.settings.preference.CustomDialogListPreference; +import app.morphe.extension.shared.theme.ThemeUtils; +import app.morphe.extension.shared.ui.CustomDialog; +import app.morphe.extension.shared.ui.Dim; + +/** + * The dialog behind a translation or romanization provider's name. Translation and romanization + * share one configuration per service, so both lists open the same settings. + */ +final class LyricsProviderConfigDialog { + + private static final ExecutorService VALIDATION = Executors.newSingleThreadExecutor( + runnable -> { + final Thread thread = new Thread(runnable, "lyrics-provider-validate"); + thread.setDaemon(true); + return thread; + }); + + private LyricsProviderConfigDialog() { + } + + /** + * @param prompt the prompt the service is sent, or null for a service that does not read one. + */ + static void show(Activity context, String provider, @Nullable StringSetting prompt, + Runnable onSaved) { + final Dialog dialog; + try { + dialog = switch (provider) { + case AiProviders.OPENAI_COMPATIBLE -> compatibleDialog(context, prompt, onSaved); + case AiProviders.DEEPL -> deepLDialog(context, onSaved); + default -> null; + }; + } catch (WindowManager.BadTokenException ex) { + Logger.printException(() -> "Could not show the provider settings dialog", ex); + return; + } + if (dialog == null) { + return; + } + try { + dialog.show(); + } catch (WindowManager.BadTokenException ex) { + Logger.printException(() -> "Could not show the provider settings dialog", ex); + } + } + + /** Calls back with whether the provider can answer with the stored settings. */ + static void validate(String provider, Consumer callback) { + try { + VALIDATION.execute(() -> { + boolean valid = false; + try { + switch (provider) { + case AiProviders.OPENAI_COMPATIBLE -> valid = OpenAIClient.validateConfig( + Settings.LYRICS_AI_BASE_URL.get(), + Settings.LYRICS_AI_API_TOKEN.get(), + Settings.LYRICS_AI_MODEL.get()); + case AiProviders.DEEPL -> { + final String key = Settings.LYRICS_DEEPL_API_KEY.get(); + valid = !key.isEmpty() && DeepLClient.translate( + Collections.singletonList("hello"), "EN", key) != null; + } + default -> { + } + } + } catch (Exception ex) { + Logger.printDebug(() -> "provider validation failed: " + provider, ex); + } + final boolean result = valid; + Utils.runOnMainThread(() -> callback.accept(result)); + }); + } catch (Exception ex) { + Logger.printException(() -> "provider validation failure: " + provider, ex); + Utils.runOnMainThread(() -> callback.accept(false)); + } + } + + private static Dialog compatibleDialog(Activity context, @Nullable StringSetting prompt, + Runnable onSaved) { + LinearLayout content = createContent(context); + // The URL row is added last so the preset it triggers can fill the boxes above. + EditText tokenInput = addInput(content, + "morphe_music_lyrics_ai_config_api_token_label", + Settings.LYRICS_AI_API_TOKEN.get(), true); + final EditText[] urlBox = new EditText[1]; + EditText modelInput = addModelInput(content, Settings.LYRICS_AI_MODEL.get(), + () -> urlBox[0] == null ? Collections.emptyList() + : OpenAIClient.fetchModels(urlBox[0].getText().toString().trim(), + tokenInput.getText().toString().trim())); + EditText extraHeadersInput = addInput(content, + "morphe_music_lyrics_ai_config_extra_headers_label", + Settings.LYRICS_AI_EXTRA_HEADERS.get(), false); + makeMultiLine(extraHeadersInput, 2, 5); + setHint(extraHeadersInput, "morphe_music_lyrics_ai_config_extra_headers_hint"); + EditText extraBodyInput = addInput(content, + "morphe_music_lyrics_ai_config_extra_body_label", + Settings.LYRICS_AI_EXTRA_BODY.get(), false); + makeMultiLine(extraBodyInput, 2, 5); + setHint(extraBodyInput, "morphe_music_lyrics_ai_config_extra_body_hint"); + urlBox[0] = addBaseUrlInput(content, Settings.LYRICS_AI_BASE_URL.get(), preset -> { + modelInput.setText(preset.model()); + }); + final EditText urlInput = urlBox[0]; + addTestButton(context, content, () -> { + final String url = urlInput.getText().toString().trim(); + if (url.isEmpty()) { + Utils.showToastShort(str("morphe_music_lyrics_ai_config_status_invalid_url")); + return; + } + test(context, () -> OpenAIClient.validateConfig(url, + tokenInput.getText().toString().trim(), + modelInput.getText().toString().trim()) ? List.of("ok") : null, + AiProviders.OPENAI_COMPATIBLE_LABEL); + }); + final PromptField promptField = addPromptSection(content, prompt); + + return createDialog(context, + AiProviders.OPENAI_COMPATIBLE_LABEL, + content, onSaved, + () -> { + final String extraBody = extraBodyInput.getText().toString().trim(); + Settings.LYRICS_AI_EXTRA_BODY.save(extraBody); + Settings.LYRICS_AI_EXTRA_HEADERS.save( + extraHeadersInput.getText().toString().trim()); + Settings.LYRICS_AI_BASE_URL.save(urlInput.getText().toString().trim()); + Settings.LYRICS_AI_API_TOKEN.save(tokenInput.getText().toString().trim()); + Settings.LYRICS_AI_MODEL.save(modelInput.getText().toString().trim()); + promptField.save(); + }, + () -> { + Settings.LYRICS_AI_BASE_URL.resetToDefault(); + Settings.LYRICS_AI_API_TOKEN.resetToDefault(); + Settings.LYRICS_AI_MODEL.resetToDefault(); + Settings.LYRICS_AI_EXTRA_BODY.resetToDefault(); + Settings.LYRICS_AI_EXTRA_HEADERS.resetToDefault(); + promptField.reset(); + }, + () -> !OpenAIClient.isValidExtraBody( + extraBodyInput.getText().toString().trim()), + () -> !OpenAIClient.isValidExtraHeaders( + extraHeadersInput.getText().toString().trim()), + () -> { + final String baseUrl = urlInput.getText().toString().trim(); + return !baseUrl.isEmpty() + && !baseUrl.startsWith("http://") + && !baseUrl.startsWith("https://"); + }); + } + + private static final String DEEPL_DASHBOARD_URL = "https://www.deepl.com/en/pro-account"; + + private static Dialog deepLDialog(Activity context, Runnable onSaved) { + LinearLayout content = createContent(context); + EditText keyInput = addInput(content, + "morphe_music_lyrics_ai_config_deepl_key_label", + Settings.LYRICS_DEEPL_API_KEY.get(), true); + + final Button getKeyButton = CustomDialog.createButton(context, null, + str("morphe_music_token_dialog_get_token"), () -> { + try { + final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(DEEPL_DASHBOARD_URL)); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(intent); + } catch (Exception ex) { + Logger.printDebug(() -> "DeepL dashboard button failure", ex); + } + }, false, false); + final LinearLayout.LayoutParams getKeyParams = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, Dim.dp36); + getKeyParams.topMargin = Dim.dp12; + content.addView(getKeyButton, getKeyParams); + + return createDialog(context, + AiProviders.DEEPL_LABEL, + content, onSaved, + () -> Settings.LYRICS_DEEPL_API_KEY.save(keyInput.getText().toString().trim()), + () -> Settings.LYRICS_DEEPL_API_KEY.resetToDefault(), + () -> keyInput.getText().toString().trim().isEmpty()); + } + + /** The prompt box and the setting behind it; a no-op pair when the service takes no prompt. */ + private static final class PromptField { + @Nullable + private final StringSetting setting; + @Nullable + private final EditText input; + + PromptField(@Nullable StringSetting setting, @Nullable EditText input) { + this.setting = setting; + this.input = input; + } + + void save() { + if (setting == null || input == null) { + return; + } + final String text = input.getText().toString().trim(); + if (text.isEmpty()) { + setting.resetToDefault(); + } else { + setting.save(text); + } + input.setText(setting.get()); + } + + void reset() { + if (setting == null) { + return; + } + setting.resetToDefault(); + if (input != null) { + input.setText(setting.get()); + } + } + } + + /** The prompt box for a list whose service reads one; null for both members otherwise. */ + private static PromptField addPromptSection(LinearLayout content, + @Nullable StringSetting prompt) { + if (prompt == null) { + return new PromptField(null, null); + } + final EditText input = addInput(content, + "morphe_music_lyrics_ai_config_prompt_label", prompt.get(), false); + makeMultiLine(input, 4, 8); + return new PromptField(prompt, input); + } + + interface Validator { + boolean invalid(); + } + + static Dialog createDialog(Activity context, String title, LinearLayout content, + @Nullable Runnable onSaved, Runnable save, Runnable reset, + @Nullable Validator... validators) { + final Pair pair = CustomDialog.create( + context, + title, + null, + null, + str("morphe_settings_save"), + () -> { + if (validators != null) { + for (Validator validator : validators) { + if (validator.invalid()) { + Utils.showToastShort(str( + "morphe_music_lyrics_ai_config_status_invalid")); + return; + } + } + } + save.run(); + if (onSaved != null) { + onSaved.run(); + } + Utils.showToastShort(str("morphe_music_lyrics_ai_config_toast_saved")); + }, + null, + str("morphe_settings_reset"), + () -> { + reset.run(); + if (onSaved != null) { + onSaved.run(); + } + Utils.showToastShort(str("morphe_music_lyrics_ai_config_toast_reset")); + }, + true + ); + + final Dialog dialog = pair.first; + final LinearLayout mainLayout = pair.second; + final android.widget.ScrollView scrollView = new android.widget.ScrollView(context); + scrollView.addView(content, new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); + LinearLayout.LayoutParams scrollParams = new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, 0, 1.0f); + scrollParams.bottomMargin = Dim.dp12; + mainLayout.addView(scrollView, mainLayout.getChildCount() - 1, scrollParams); + return dialog; + } + + static LinearLayout createContent(Context context) { + LinearLayout content = new LinearLayout(context); + content.setOrientation(LinearLayout.VERTICAL); + return content; + } + + private static void test(Activity context, Supplier> request, String label) { + Utils.showToastShort(str("morphe_music_lyrics_ai_config_status_validating")); + Utils.runOnBackgroundThread(() -> { + List result; + try { + result = request.get(); + } catch (Exception ex) { + Logger.printDebug(() -> label + " validation failed", ex); + result = null; + } + final List response = result; + Utils.runOnMainThread(() -> { + if (response != null && !response.isEmpty()) { + Utils.showToastShort(str( + "morphe_music_lyrics_ai_config_status_connected", + label, response.get(0))); + } else { + Utils.showToastShort(str( + "morphe_music_lyrics_ai_config_status_failed", label)); + } + }); + }); + } + + /** The endpoint box and its preset button, added at the top of the form. */ + private static EditText addBaseUrlInput(LinearLayout section, String value, + Consumer onPresetPicked) { + final Context context = section.getContext(); + EditText input = ThemedInput.create(context); + input.setText(value); + input.setHint(str("morphe_music_lyrics_ai_config_base_url_hint")); + Button presets = Glyph.DOWN.create(context); + presets.setContentDescription(str("morphe_music_lyrics_ai_config_base_url_presets")); + presets.setOnClickListener(v -> { + final List items = + new ArrayList<>(AiProviders.ENDPOINT_PRESETS); + items.sort(Comparator.comparing(AiProviders.EndpointPreset::label, + String.CASE_INSENSITIVE_ORDER)); + final String[] labels = items.stream() + .map(AiProviders.EndpointPreset::label) + .toArray(String[]::new); + CustomDialogListPreference.createListDialog(context, + str("morphe_music_lyrics_ai_config_base_url_presets_title"), + labels, labels, null, index -> { + final AiProviders.EndpointPreset preset = items.get(index); + input.setText(preset.baseUrl()); + input.setSelection(input.getText().length()); + onPresetPicked.accept(preset); + }).show(); + }); + + LinearLayout row = new LinearLayout(context); + row.setOrientation(LinearLayout.HORIZONTAL); + row.addView(input, new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1.0f)); + LinearLayout.LayoutParams buttonParams = Glyph.DOWN.params(); + buttonParams.leftMargin = Dim.dp8; + row.addView(presets, buttonParams); + section.addView(row, 0); + section.addView(createLabel(context, "morphe_music_lyrics_ai_config_base_url_label"), 0); + return input; + } + + static TextView createLabel(Context context, String textKey) { + TextView label = new TextView(context); + label.setText(str(textKey)); + label.setTextSize(TypedValue.COMPLEX_UNIT_SP, 12); + label.setTextColor(ThemeUtils.getAppForegroundColor()); + LinearLayout.LayoutParams params = new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); + params.topMargin = Dim.dp8; + params.bottomMargin = Dim.dp4; + label.setLayoutParams(params); + return label; + } + + /** A triangle reads as play for the action button and as a menu for the two drop-downs. */ + private enum Glyph { + /** Filled, laid out along the line height so it reads like the app's own play icon. */ + PLAY('▶', 11), + /** A small open arrow in the line's upper half, where a drop-down arrow sits. */ + DOWN('▾', 13); + + /** Circles, so they read as icon buttons rather than as one-letter labels. */ + private static final int SIZE_DP = 36; + + final char character; + final int sizeSp; + + Glyph(char character, int sizeSp) { + this.character = character; + this.sizeSp = sizeSp; + } + + Button create(Context context) { + Button button = new Button(context); + button.setText(String.valueOf(character)); + button.setAllCaps(false); + button.setGravity(Gravity.CENTER); + button.setTextSize(TypedValue.COMPLEX_UNIT_SP, sizeSp); + button.setTextColor(ThemeUtils.getAppForegroundColor()); + button.setBackground(CustomDialog.createRoundedBackground(SIZE_DP / 2, + ThemeUtils.getEditTextBackground())); + button.setPadding(0, 0, 0, 0); + button.setMinWidth(0); + button.setMinimumWidth(0); + button.setMinHeight(0); + button.setMinimumHeight(0); + return button; + } + + LinearLayout.LayoutParams params() { + return new LinearLayout.LayoutParams(Dim.dp36, Dim.dp36); + } + } + + static EditText addInput(LinearLayout section, String labelKey, String value, boolean secret) { + Context context = section.getContext(); + section.addView(createLabel(context, labelKey)); + EditText input = ThemedInput.create(context); + if (secret) { + input.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); + } + input.setText(value); + section.addView(input, new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); + return input; + } + + static void makeMultiLine(EditText input, int minLines, int maxLines) { + input.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE); + input.setSingleLine(false); + input.setMinLines(minLines); + input.setMaxLines(maxLines); + input.setVerticalScrollBarEnabled(true); + } + + /** A hint defaults to a fixed grey the dark theme leaves unreadable, so it follows the app. */ + static void setHint(EditText input, String hintKey) { + input.setHint(str(hintKey)); + input.setHintTextColor((0x99 << 24) | (ThemeUtils.getAppForegroundColor() & 0x00FFFFFF)); + } + + /** A model name input with a button that lists the models the provider offers. */ + static EditText addModelInput(LinearLayout section, String value, + Supplier> fetchModels) { + final Context context = section.getContext(); + section.addView(createLabel(context, "morphe_music_lyrics_ai_config_model_label")); + + EditText input = ThemedInput.create(context); + input.setText(value); + Button fetch = Glyph.DOWN.create(context); + fetch.setContentDescription(str("morphe_music_lyrics_ai_config_fetch_models")); + fetch.setOnClickListener(v -> { + Utils.showToastShort(str("morphe_music_lyrics_ai_config_fetching_models")); + Utils.runOnBackgroundThread(() -> { + List models; + try { + models = fetchModels.get(); + } catch (Exception ex) { + models = null; + } + final List fetched = models; + Utils.runOnMainThread(() -> showModelPicker(context, fetched, input)); + }); + }); + + LinearLayout row = new LinearLayout(context); + row.setOrientation(LinearLayout.HORIZONTAL); + row.addView(input, new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1.0f)); + LinearLayout.LayoutParams buttonParams = Glyph.DOWN.params(); + buttonParams.leftMargin = Dim.dp8; + row.addView(fetch, buttonParams); + section.addView(row); + return input; + } + + private static void showModelPicker(Context context, @Nullable List models, EditText target) { + if (models == null || models.isEmpty()) { + Utils.showToastShort(str("morphe_music_lyrics_ai_config_fetch_models_failed")); + return; + } + String[] items = models.toArray(new String[0]); + CustomDialogListPreference.createListDialog(context, + str("morphe_music_lyrics_ai_config_fetch_models_success"), items, items, + target.getText().toString().trim(), index -> { + target.setText(items[index]); + target.setSelection(items[index].length()); + }).show(); + } + + private static void addTestButton(Context context, LinearLayout section, Runnable onTest) { + Button testButton = Glyph.PLAY.create(context); + testButton.setContentDescription(str("morphe_music_lyrics_ai_config_test_connection")); + testButton.setOnClickListener(v -> onTest.run()); + LinearLayout.LayoutParams params = Glyph.PLAY.params(); + params.topMargin = Dim.dp8; + params.bottomMargin = Dim.dp4; + section.addView(testButton, params); + } +} diff --git a/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsRimeDictDialog.java b/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsRimeDictDialog.java new file mode 100644 index 0000000000..195c688a5e --- /dev/null +++ b/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsRimeDictDialog.java @@ -0,0 +1,610 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/3851 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.extension.music.settings.preference; + +import static app.morphe.extension.shared.StringRef.str; + +import android.app.Dialog; +import android.content.Context; +import android.content.res.ColorStateList; +import android.text.InputType; +import android.util.Pair; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.widget.Button; +import android.widget.CheckBox; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.ScrollView; +import android.widget.TextView; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import androidx.annotation.Nullable; + +import app.morphe.extension.music.patches.lyrics.romanizers.RimeDictStore; +import app.morphe.extension.music.patches.lyrics.romanizers.RomanizationDict; +import app.morphe.extension.music.settings.Settings; +import app.morphe.extension.shared.Logger; +import app.morphe.extension.shared.ResourceUtils; +import app.morphe.extension.shared.Utils; +import app.morphe.extension.shared.theme.ThemeUtils; +import app.morphe.extension.shared.ui.CustomDialog; +import app.morphe.extension.shared.ui.Dim; + +/** + * The word lists the Rime Wordlist provider reads, one per language, each fetched from an address the + * user pasted. Only Cantonese ships; every other list is theirs to add and to judge, so no + * catalogue of dialects is bundled here. + * + *

    A list is fetched only while its box is ticked, and Save is what acts on the boxes and starts + * the download. Unticking a user's list removes its row and its files; the built-in list is only + * switched off, because its row is the example an added list is meant to look like. + * + *

    The lists are stored one line each, as {@code subtag|label|url|charSource}, and removed by + * rewriting that setting. A built-in list has no line, so its switch is stored separately. + */ +public final class LyricsRimeDictDialog { + + private LyricsRimeDictDialog() { + } + + /** + * Shows the lists, each with a box that says whether it is read, and the buttons that save + * the boxes and add another list. + * + * @param onChanged called after a Save changed what is read, so a caller showing a summary of + * them can read it again. + * @param onClosed called once the dialog is gone, whichever way it was closed. + */ + public static void show(Context context, @Nullable Runnable onChanged, + @Nullable Runnable onClosed) { + try { + showList(context, onChanged, onClosed); + } catch (WindowManager.BadTokenException ex) { + Logger.printException(() -> "Could not show the Rime list dialog", ex); + if (onClosed != null) { + onClosed.run(); + } + } + } + + /** The row boxes, so Save can read what the user left ticked. */ + private static final class Rows { + final Map boxes = new LinkedHashMap<>(); + + Rows() { + } + } + + private static void showList(Context context, @Nullable Runnable onChanged, + @Nullable Runnable onClosed) { + final List builtinCodes = RimeDictStore.builtinLanguages(); + final List customCodes = RimeDictStore.customLanguages(); + + final int fg = ThemeUtils.getAppForegroundColor(); + + LinearLayout content = new LinearLayout(context); + content.setOrientation(LinearLayout.VERTICAL); + final Rows rows = new Rows(); + + final Dialog[] listDialog = new Dialog[1]; + // The add form replaces this list; that handover is not the end of the session, so the + // list's own close callback is not run for it. + final boolean[] handover = {false}; + + final TextView guidance = new TextView(context); + guidance.setTextSize(14); + guidance.setTextColor(withAlpha(fg, 0x99)); + guidance.setText(str("morphe_music_lyrics_rime_dict_empty")); + guidance.setPadding(0, 0, 0, Dim.dp12); + content.addView(guidance); + // The download reports into this same hint, like the Local dictionary dialog does, so + // the progress takes the place of the summary rather than adding a block below. + final LyricsLocalRomanizationDownloads.ProgressSink sink = guidance::setText; + + final Pair pair = CustomDialog.create(context, + str("morphe_music_lyrics_rime_dict_title"), + null, + null, + str("morphe_settings_save"), + () -> { }, + () -> { }, + str("morphe_music_lyrics_rime_dict_add"), + // Closed first, so the list does not stay behind the form it opened. + () -> { + handover[0] = true; + listDialog[0].dismiss(); + showAddSafely(context, onChanged, onClosed); + }, + false); + listDialog[0] = pair.first; + + // The shipped lists come first, unticked: a table is a download, not an arrival. + for (String code : builtinCodes) { + final CheckBox box = checkBox(context, fg, + label(RimeDictStore.builtinLabel(code), code), + RimeDictStore.isBuiltinRowSelected(code)); + content.addView(box); + addNote(context, fg, content, RimeDictStore.summaryFor(code)); + rows.boxes.put(code, box); + } + // A stored row was asked for, so it opens ticked; unticking it is what removes it. + for (String code : customCodes) { + final LinearLayout row = new LinearLayout(context); + row.setOrientation(LinearLayout.VERTICAL); + final CheckBox box = checkBox(context, fg, + label(RimeDictStore.customLabel(code), code), true); + row.addView(box); + if (RimeDictStore.isReserved(code)) { + addNote(context, fg, row, + str("morphe_music_lyrics_rime_dict_reserved_row")); + } else { + addNote(context, fg, row, RimeDictStore.summaryFor(code)); + } + content.addView(row); + rows.boxes.put(code, box); + } + + ScrollView scroll = new ScrollView(context); + scroll.addView(content); + LinearLayout.LayoutParams scrollParams = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, 0, 1.0f); + scrollParams.bottomMargin = Dim.dp12; + pair.second.addView(scroll, Math.max(0, pair.second.getChildCount() - 1), scrollParams); + + // Save writes the choice and closes; the fetch runs in the background and answers with a + // toast, the same model the Local dictionary dialog uses. + final Button ok = button(pair.second, str("morphe_settings_save")); + if (ok != null) { + ok.setOnClickListener(v -> + save(rows, builtinCodes, customCodes, pair.first, sink, onChanged)); + } + + pair.first.setOnDismissListener(d -> { + LyricsLocalRomanizationDownloads.detach(sink); + if (!handover[0] && onClosed != null) { + onClosed.run(); + } + }); + LyricsLocalRomanizationDownloads.attach(sink); + pair.first.show(); + } + + /** A ticked row, in the app's own colours rather than the dialog theme's. */ + private static CheckBox checkBox(Context context, int fg, CharSequence text, boolean checked) { + CheckBox box = new CheckBox(context); + box.setText(text); + box.setChecked(checked); + box.setTextColor(fg); + final int[][] states = {{android.R.attr.state_checked}, {}}; + box.setButtonTintList(new ColorStateList(states, new int[]{fg, withAlpha(fg, 0x66)})); + return box; + } + + /** A name, with the code beside it when the two differ, so two lists stay distinguishable. */ + private static CharSequence label(@Nullable String name, String code) { + return name == null ? code : code + "|" + name; + } + + /** A second line under a row, smaller and dimmed, for the notes about what a row is. */ + private static void addNote(Context context, int fg, LinearLayout parent, + @Nullable CharSequence text) { + if (text == null || text.length() == 0) { + return; + } + TextView note = new TextView(context); + note.setText(text); + note.setTextSize(12); + note.setTextColor(withAlpha(fg, 0x99)); + note.setPadding(Dim.dp12, 0, 0, Dim.dp8); + parent.addView(note); + } + + private static int withAlpha(int color, int alpha) { + return (alpha << 24) | (color & 0x00FFFFFF); + } + + /** + * Applies the ticks. A ticked list is kept and fetched, an unticked one is taken away with its + * files, and the built-in list is only switched off because its row stays. + * + *

    The files go while the setting still names them: a list's file names come from its own + * row, so a removed row could no longer point at what it left on disk. + */ + private static void save(Rows rows, List builtinCodes, List customCodes, + Dialog dialog, LyricsLocalRomanizationDownloads.ProgressSink sink, + @Nullable Runnable onChanged) { + final Map builtinChoice = new LinkedHashMap<>(); + final List removed = new ArrayList<>(); + final List lostReading = new ArrayList<>(); + + for (String code : builtinCodes) { + final boolean on = isChecked(rows, code); + builtinChoice.put(code, on); + if (!on && RimeDictStore.isSelected(code)) { + removed.add(code); + lostReading.add(code); + } + } + final List keptCustom = new ArrayList<>(); + for (String code : customCodes) { + if (isChecked(rows, code)) { + keptCustom.add(code); + } else { + removed.add(code); + // A reserved row never supplied a reading, so taking it away must not move the + // language the user picked. + if (RimeDictStore.isSelected(code)) lostReading.add(code); + } + } + + Utils.runOnBackgroundThread(() -> { + for (String code : removed) { + RimeDictStore.deleteTable(code); + } + Utils.runOnMainThread(() -> apply(builtinChoice, keptCustom, lostReading, + dialog, sink, onChanged)); + }); + } + + private static boolean isChecked(Rows rows, String code) { + final CheckBox box = rows.boxes.get(code); + return box != null && box.isChecked(); + } + + /** Writes the choice, then fetches whatever the ticks now ask for. */ + private static void apply(Map builtinChoice, List keptCustom, + List lostReading, Dialog dialog, + LyricsLocalRomanizationDownloads.ProgressSink sink, + @Nullable Runnable onChanged) { + for (Map.Entry entry : builtinChoice.entrySet()) { + RimeDictStore.setBuiltinSelected(entry.getKey(), entry.getValue()); + } + + // Rewritten from the rows that stay, so a removed row leaves no line behind. + final StringBuilder kept = new StringBuilder(); + for (String code : keptCustom) { + final String line = RimeDictStore.customRow(code); + if (line == null) continue; + if (kept.length() > 0) kept.append('\n'); + kept.append(line); + } + Settings.LYRICS_RIME_CUSTOM_DICTS.save(kept.toString()); + RimeDictStore.reset(); + + // A language whose only table was just taken away falls back to Automatic, so the + // romanizer is not left pointed at nothing. + final String chosen = currentSubtag(); + if (!chosen.isEmpty() && lostReading.contains(chosen)) { + Settings.LYRICS_ROMANIZE_LANGUAGE.save("DEFAULT"); + } + + if (onChanged != null) { + onChanged.run(); + } + + dialog.dismiss(); + + if (!builtinChoice.containsValue(Boolean.TRUE) && keptCustom.isEmpty()) { + // Nothing ticked is a valid way to stop reading, not a mistake to undo. + return; + } + + // A toast answers when the download ends, the same model the Local dictionary uses. + RimeDictStore.ensureSelected(new RomanizationDict.Listener() { + @Override + public void onDownload(String fileName, int percent) { + Utils.runOnMainThread(() -> { + if (sink != null) { + sink.onHint(str("morphe_music_lyrics_local_romanization_dict_downloading", + fileName, percent)); + } + }); + } + + @Override + public void onBuilding(int percent) { + Utils.runOnMainThread(() -> { + if (sink != null) { + sink.onHint(str("morphe_music_lyrics_rime_dict_building", percent)); + } + }); + } + + @Override + public void onReady(int entries) { + Utils.runOnMainThread(() -> { + if (sink != null) { + sink.onHint(str("morphe_music_lyrics_rime_dict_ready")); + } + Utils.showToastShort(str("morphe_music_lyrics_rime_dict_ready_toast", + entries)); + }); + } + + @Override + public void onFailed() { + Utils.runOnMainThread(() -> { + if (sink != null) { + sink.onHint(str("morphe_music_lyrics_rime_dict_failed")); + } + Utils.showToastShort(str("morphe_music_lyrics_rime_dict_failed")); + }); + } + }); + } + + /** Found by label because the button row is reordered when the labels do not fit. */ + @Nullable + private static Button button(ViewGroup layout, CharSequence text) { + for (int i = 0; i < layout.getChildCount(); i++) { + final View child = layout.getChildAt(i); + if (child instanceof Button candidate + && text.toString().contentEquals(candidate.getText())) { + return candidate; + } + if (child instanceof ViewGroup group) { + final Button found = button(group, text); + if (found != null) return found; + } + } + return null; + } + + /** Starts the form, tolerating a settings screen that has already gone away. */ + private static void showAddSafely(Context context, @Nullable Runnable onChanged, + @Nullable Runnable onClosed) { + try { + showAdd(context, "", "", "", charSourceOptions().first[0], onChanged, onClosed); + } catch (WindowManager.BadTokenException ex) { + Logger.printException(() -> "Could not show the Rime add dialog", ex); + } + } + + /** Reopens the form with what was typed, after a refusal named the field at fault. */ + private static void showAddSafely(Context context, EditText code, EditText label, + EditText url, String charSource, @Nullable Runnable onChanged, + @Nullable Runnable onClosed) { + final String codeText = code.getText().toString(); + final String labelText = label.getText().toString(); + final String urlText = url.getText().toString(); + try { + showAdd(context, codeText, labelText, urlText, charSource, onChanged, onClosed); + } catch (WindowManager.BadTokenException ex) { + Logger.printException(() -> "Could not show the Rime add dialog", ex); + } + } + + /** Asks for the four things a list is: what language it reads, what to call it, where it is, + * and which character table to fall back to. */ + private static void showAdd(Context context, String codeText, String labelText, + String urlText, String charSourceToken, @Nullable Runnable onChanged, + @Nullable Runnable onClosed) { + final int fg = ThemeUtils.getAppForegroundColor(); + LinearLayout content = new LinearLayout(context); + content.setOrientation(LinearLayout.VERTICAL); + + // The hint sits inside the form's own scroll area, in the dimmed note style the list + // dialog uses, rather than as the dialog message that would add a second scroll area. + final TextView hint = new TextView(context); + hint.setTextSize(14); + hint.setTextColor(withAlpha(fg, 0x99)); + hint.setText(str("morphe_music_lyrics_rime_dict_add_hint")); + hint.setPadding(0, 0, 0, Dim.dp12); + content.addView(hint); + + EditText codeInput = addField(context, content, + "morphe_music_lyrics_rime_dict_field_code", + "morphe_music_lyrics_rime_dict_field_code_hint"); + codeInput.setText(codeText); + EditText labelInput = addField(context, content, + "morphe_music_lyrics_rime_dict_field_label", + "morphe_music_lyrics_rime_dict_field_label_hint"); + labelInput.setText(labelText); + EditText urlInput = addField(context, content, + "morphe_music_lyrics_rime_dict_field_url", + "morphe_music_lyrics_rime_dict_field_url_hint"); + urlInput.setText(urlText); + urlInput.setSingleLine(true); + urlInput.setInputType(InputType.TYPE_CLASS_TEXT + | InputType.TYPE_TEXT_VARIATION_URI); + + final String[] charSource = {charSourceToken}; + TextView charSourceValue = new TextView(context); + charSourceValue.setText(charSourceLabel(charSource[0])); + charSourceValue.setTextColor(ThemeUtils.getAppForegroundColor()); + charSourceValue.setPadding(Dim.dp12, Dim.dp8, Dim.dp12, Dim.dp8); + charSourceValue.setOnClickListener(v -> { + charSource[0] = nextCharSource(charSource[0]); + charSourceValue.setText(charSourceLabel(charSource[0])); + }); + addLabel(context, content, "morphe_music_lyrics_rime_dict_field_char_source"); + content.addView(charSourceValue); + + // The accent button saves and closes; a refusal toasts the faulty field and reopens the + // form with what was typed, while Cancel and back do not reopen. + final int[] outcome = {0}; // 0 closed by the user, 1 saved, 2 refused + // The OK action runs after create() returns, so it reaches the dialog through a holder. + final Pair[] pairBox = new Pair[1]; + final Pair pair = CustomDialog.create(context, + str("morphe_music_lyrics_rime_dict_add_title"), + null, + null, + str("morphe_settings_save"), + () -> { + final boolean ok = add(codeInput, labelInput, urlInput, charSource[0]); + outcome[0] = ok ? 1 : 2; + if (ok) pairBox[0].first.dismiss(); + }, + () -> {}, + null, + null, + true); + pairBox[0] = pair; + + ScrollView scroll = new ScrollView(context); + scroll.addView(content); + LinearLayout.LayoutParams scrollParams = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, 0, 1.0f); + scrollParams.bottomMargin = Dim.dp12; + pair.second.addView(scroll, Math.max(0, pair.second.getChildCount() - 1), scrollParams); + + pair.first.setOnDismissListener(d -> { + if (outcome[0] == 1) { + if (onChanged != null) { + onChanged.run(); + } + showList(context, onChanged, onClosed); + } else if (outcome[0] == 2 && filled(codeInput, labelInput, urlInput)) { + showAddSafely(context, codeInput, labelInput, urlInput, charSource[0], + onChanged, onClosed); + } else if (onClosed != null) { + onClosed.run(); + } + }); + + pair.first.show(); + } + + /** Whether anything was typed, so an empty form that was refused is not reopened. */ + private static boolean filled(EditText code, EditText label, EditText url) { + return !code.getText().toString().trim().isEmpty() + || !label.getText().toString().trim().isEmpty() + || !url.getText().toString().trim().isEmpty(); + } + + private static EditText addField(Context context, LinearLayout content, String labelKey, + String hintKey) { + addLabel(context, content, labelKey); + EditText input = ThemedInput.create(context); + input.setHint(str(hintKey)); + content.addView(input); + return input; + } + + private static void addLabel(Context context, LinearLayout content, String key) { + TextView label = new TextView(context); + label.setText(str(key)); + label.setTextSize(14); + label.setTextColor(ThemeUtils.getAppForegroundColor()); + label.setPadding(0, Dim.dp12, 0, Dim.dp8); + content.addView(label); + } + + /** + * Stores one list. Everything is checked here rather than at download time, so a typo is + * reported while the user is looking at the field that holds it. + * + * @return whether the list was added. + */ + private static boolean add(EditText codeInput, EditText labelInput, EditText urlInput, + String charSource) { + final String code = codeInput.getText().toString().trim().toLowerCase(Locale.ROOT); + final String label = labelInput.getText().toString().trim(); + final String url = urlInput.getText().toString().trim(); + + if (code.isEmpty() || label.isEmpty() || url.isEmpty()) { + Utils.showToastShort(str("morphe_music_lyrics_rime_dict_incomplete")); + return false; + } + if (!code.matches("[a-z]{2,3}")) { + Utils.showToastShort(str("morphe_music_lyrics_rime_dict_bad_code")); + return false; + } + if (!url.startsWith("https://") || url.length() <= "https://".length()) { + Utils.showToastShort(str("morphe_music_lyrics_rime_dict_bad_url")); + return false; + } + // The line format is pipe separated, so a pipe or a line break in either field would + // shift the rest of the line. + if (label.indexOf('|') >= 0 || url.indexOf('|') >= 0 + || label.indexOf('\n') >= 0 || url.indexOf('\n') >= 0) { + Utils.showToastShort(str("morphe_music_lyrics_rime_dict_bad_field")); + return false; + } + if (RimeDictStore.hasLanguage(code)) { + Utils.showToastShort(str("morphe_music_lyrics_rime_dict_duplicate", code)); + return false; + } + if (!RimeDictStore.canAddLanguage(code)) { + // A language the build already reads keeps its own reading. + Utils.showToastShort(str("morphe_music_lyrics_rime_dict_reserved", code)); + return false; + } + + final String line = code + "|" + label + "|" + url + "|" + charSource; + final String stored = Settings.LYRICS_RIME_CUSTOM_DICTS.get(); + final String joined = stored == null || stored.isBlank() ? line : stored + "\n" + line; + Settings.LYRICS_RIME_CUSTOM_DICTS.save(joined); + + // The store parses the setting lazily, so the new row is seen on the next read. + RimeDictStore.reset(); + Utils.showToastShort(str("morphe_music_lyrics_rime_dict_added", label)); + + // Nothing is fetched here: the new row comes back ticked, and the Save that keeps it is + // what starts the download. + return true; + } + + /** The BCP-47 primary subtag of the chosen romanization language, or "" for Automatic. */ + private static String currentSubtag() { + final String language = Settings.LYRICS_ROMANIZE_LANGUAGE.get(); + if (language == null || language.isBlank() + || "DEFAULT".equalsIgnoreCase(language)) { + return ""; + } + final String lower = language.toLowerCase(Locale.ROOT); + return lower.split("-", 2)[0]; + } + + /** The character source options, as saved tokens and the labels that name them. Both come + * from arrays.xml in the same order, so another option is one row in each array and nothing + * here. */ + private static Pair charSourceOptions() { + try { + final String[] tokens = ResourceUtils.getStringArray( + "morphe_music_lyrics_rime_char_source_values"); + final String[] labels = ResourceUtils.getStringArray( + "morphe_music_lyrics_rime_char_source_entries"); + if (tokens.length > 0 && tokens.length == labels.length) { + return new Pair<>(tokens, labels); + } + } catch (Exception ex) { + Logger.printDebug(() -> "Could not read the character source list", ex); + } + return new Pair<>(new String[]{"AUTO"}, new String[]{ + str("morphe_music_lyrics_rime_char_source_auto")}); + } + + private static String nextCharSource(String current) { + final String[] tokens = charSourceOptions().first; + for (int i = 0; i < tokens.length; i++) { + if (tokens[i].equalsIgnoreCase(current)) { + return tokens[(i + 1) % tokens.length]; + } + } + return tokens[0]; + } + + /** The label beside the token in the arrays, so a renamed option needs no code change. */ + private static CharSequence charSourceLabel(String token) { + final Pair options = charSourceOptions(); + for (int i = 0; i < options.first.length; i++) { + if (options.first[i].equalsIgnoreCase(token)) { + return options.second[i]; + } + } + return options.second[0]; + } +} diff --git a/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsTargetLanguagePreference.java b/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsTargetLanguagePreference.java new file mode 100644 index 0000000000..bf1caf9b89 --- /dev/null +++ b/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsTargetLanguagePreference.java @@ -0,0 +1,264 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.extension.music.settings.preference; + +import static app.morphe.extension.shared.StringRef.str; + +import android.content.Context; +import android.preference.Preference; +import android.util.AttributeSet; +import android.view.WindowManager; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +import androidx.annotation.Nullable; + +import app.morphe.extension.music.patches.lyrics.romanizers.RimeDictStore; +import app.morphe.extension.music.settings.Settings; +import app.morphe.extension.shared.Logger; +import app.morphe.extension.shared.ResourceUtils; +import app.morphe.extension.shared.settings.StringSetting; +import app.morphe.extension.shared.settings.preference.CustomDialogListPreference; + +/** + * The target language row under a translation or romanization service list. The language is + * shown as one line under the providers it belongs to rather than as a row of its own, so the + * list reads as the whole service: the providers, and the language they read into. + * + *

    Tap opens the same language list the old standalone row did. The romanization list also + * carries the languages the user added a Rime word table for, which is why it is rebuilt here + * rather than fixed by the array. + */ +@SuppressWarnings({"unused", "deprecation"}) +public class LyricsTargetLanguagePreference extends Preference { + + private static final String KEY_TRANSLATION_LANGUAGE = "morphe_music_lyrics_translation_language"; + private static final String KEY_ROMANIZE_LANGUAGE = "morphe_music_lyrics_romanize_language"; + + public LyricsTargetLanguagePreference(Context context, AttributeSet attrs, int defStyleAttr, + int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + init(); + } + + public LyricsTargetLanguagePreference(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + } + + public LyricsTargetLanguagePreference(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + public LyricsTargetLanguagePreference(Context context) { + super(context); + init(); + } + + private void init() { + // Nothing is stored here; the tap is what opens the list. + setPersistent(false); + setSelectable(true); + } + + @Override + protected void onAttachedToHierarchy(android.preference.PreferenceManager preferenceManager) { + super.onAttachedToHierarchy(preferenceManager); + refreshSummary(); + } + + /** The setting the tap writes, chosen by the key the row was declared with. */ + private StringSetting setting() { + return languageSetting(isTranslation()); + } + + private static StringSetting languageSetting(boolean translation) { + return translation ? Settings.LYRICS_TRANSLATION_LANGUAGE + : Settings.LYRICS_ROMANIZE_LANGUAGE; + } + + private boolean isTranslation() { + return KEY_TRANSLATION_LANGUAGE.equals(getKey()); + } + + /** "Translation target language: App language", or the romanization counterpart. */ + private void refreshSummary() { + final String labelKey = isTranslation() + ? "morphe_music_lyrics_translation_target_language" + : "morphe_music_lyrics_romanization_target_language"; + final String name = currentLanguageName(); + setSummary(str(labelKey, name == null ? "" : name)); + } + + /** + * Opens the language picker for the side named by {@code translation}. {@code onPicked} runs + * after the new language is saved. + */ + public static void showLanguageDialog(Context context, boolean translation, + @Nullable Runnable onPicked) { + final CharSequence[] entries = buildEntries(translation); + final CharSequence[] values = buildValues(translation); + if (entries == null || values == null || entries.length != values.length) { + return; + } + final String title = str(translation + ? "morphe_music_lyrics_translation_language_title" + : "morphe_music_lyrics_romanize_language_title"); + final String selected = languageSetting(translation).get(); + CustomDialogListPreference.createListDialog(context, title, entries, values, selected, + index -> { + final String picked = values[index].toString(); + if (picked.equals(selected)) { + return; + } + languageSetting(translation).save(picked); + if (onPicked != null) { + onPicked.run(); + } + }).show(); + } + + /** + * The display name of the stored language: the entry matching the stored value, or "App + * language" for Automatic. Null leaves the summary with nothing but the label. + */ + @Nullable + private String currentLanguageName() { + final CharSequence[] entries; + final CharSequence[] values; + try { + entries = buildEntries(); + values = buildValues(); + } catch (Exception ex) { + Logger.printDebug(() -> "Could not read the language list", ex); + return null; + } + return entryFor(values, entries, setting().get()); + } + + /** + * The label of the row a stored value names, or null when the list no longer has one. A value + * whose row is gone has to read as absent rather than as a blank summary, which is what the + * caller falls back from. + */ + @Nullable + private static String entryFor(@Nullable CharSequence[] values, @Nullable CharSequence[] entries, + @Nullable String value) { + if (value == null || values == null || entries == null + || values.length != entries.length) { + return null; + } + for (int i = 0; i < values.length; i++) { + if (value.equalsIgnoreCase(values[i].toString())) { + return entries[i].toString(); + } + } + return null; + } + + /** + * The list to show, which also names the summary: the shipped array plus, for + * romanization, one row per language the user added a Rime word table for. + */ + @Nullable + private CharSequence[] buildEntries() { + return buildEntries(isTranslation()); + } + + @Nullable + private static CharSequence[] buildEntries(boolean translation) { + final CharSequence[] base; + if (translation) { + base = ResourceUtils.getStringArray("morphe_language_entries"); + } else { + base = ResourceUtils.getStringArray(KEY_ROMANIZE_LANGUAGE + "_entries"); + } + final CharSequence[] values = translation + ? ResourceUtils.getStringArray("morphe_language_entry_values") + : ResourceUtils.getStringArray(KEY_ROMANIZE_LANGUAGE + "_entry_values"); + if (translation || base == null || values == null || base.length != values.length) { + return base; + } + final List merged = new ArrayList<>(base.length); + for (CharSequence entry : base) { + merged.add(entry); + } + final List added = addedLanguages(values); + for (String code : added) { + final String label = RimeDictStore.customLabel(code); + merged.add((label == null ? code : label) + " (Rime)"); + } + return merged.toArray(new CharSequence[0]); + } + + @Nullable + private CharSequence[] buildValues() { + return buildValues(isTranslation()); + } + + @Nullable + private static CharSequence[] buildValues(boolean translation) { + final CharSequence[] base; + if (translation) { + base = ResourceUtils.getStringArray("morphe_language_entry_values"); + } else { + base = ResourceUtils.getStringArray(KEY_ROMANIZE_LANGUAGE + "_entry_values"); + } + if (translation || base == null) { + return base; + } + final List merged = new ArrayList<>(base.length); + for (CharSequence entry : base) { + merged.add(entry); + } + for (String code : addedLanguages(base)) { + merged.add(code.toUpperCase(Locale.ROOT)); + } + return merged.toArray(new CharSequence[0]); + } + + /** A language the user added a Rime word table for, unless this build already reads it. */ + private static List addedLanguages(CharSequence[] shipped) { + final List added = new ArrayList<>(); + for (String code : RimeDictStore.customLanguages()) { + if (RimeDictStore.isReserved(code)) { + continue; + } + boolean listed = false; + for (CharSequence value : shipped) { + if (value.toString().equalsIgnoreCase(code)) { + listed = true; + break; + } + } + if (!listed) { + added.add(code); + } + } + return added; + } + + @Override + protected void onClick() { + if (!isEnabled()) { + return; + } + try { + showLanguageDialog(); + } catch (WindowManager.BadTokenException ex) { + Logger.printException(() -> "Could not show the language dialog", ex); + } + } + + private void showLanguageDialog() { + showLanguageDialog(getContext(), isTranslation(), this::refreshSummary); + } +} diff --git a/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsTokenDialogPreference.java b/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsTokenDialogPreference.java index 46aee8af83..a63107af84 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsTokenDialogPreference.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsTokenDialogPreference.java @@ -195,7 +195,7 @@ public void showDialog(@Nullable Runnable onDismissed) { return; } - String storedToken = setting.get(); + final String storedToken = setting.get(); final boolean configured = storedToken != null && !storedToken.isBlank(); LinearLayout content = new LinearLayout(context); diff --git a/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsTrackFormatPreference.java b/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsTrackFormatPreference.java index 1678bbcb46..f8078657f8 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsTrackFormatPreference.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsTrackFormatPreference.java @@ -14,7 +14,11 @@ import android.preference.Preference; import android.text.InputType; import android.util.AttributeSet; +import android.view.View; import android.widget.EditText; +import android.widget.TextView; + +import app.morphe.extension.shared.theme.ThemeUtils; import app.morphe.extension.music.settings.Settings; import app.morphe.extension.shared.Utils; @@ -30,7 +34,7 @@ public LyricsTrackFormatPreference(Context context, AttributeSet attrs) { @Override protected void onClick() { LyricsFieldsDialog dialog = new LyricsFieldsDialog(getContext()); - EditText template = dialog.addInput("morphe_music_lyrics_mini_track_format_summary", + EditText template = dialog.addNoteInput("morphe_music_lyrics_mini_track_format_summary", Settings.LYRICS_MINI_TRACK_FORMAT.get(), InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE); template.setSingleLine(false); @@ -40,7 +44,7 @@ protected void onClick() { Settings.LYRICS_MINI_ARTIST_MIN_MS.get(), false); EditText maximum = dialog.addNumberInput("morphe_music_lyrics_mini_artist_max_label", Settings.LYRICS_MINI_ARTIST_MAX_MS.get(), false); - dialog.addLabel("morphe_music_lyrics_mini_artist_timing_explanation"); + dialog.addNoteLabel("morphe_music_lyrics_mini_artist_timing_explanation"); dialog.show("morphe_music_lyrics_mini_track_format_title", () -> { final int min; @@ -66,7 +70,20 @@ protected void onClick() { }); } + @Override + protected void onBindView(View view) { + super.onBindView(view); + TextView summary = (TextView) view.findViewById(android.R.id.summary); + if (summary != null) { + summary.setTextColor(withAlpha(ThemeUtils.getAppForegroundColor(), 0x99)); + } + } + private static void showInvalidToast() { Utils.showToastShort(str("morphe_music_lyrics_mini_artist_timing_invalid")); } + + private static int withAlpha(int color, int alpha) { + return (alpha << 24) | (color & 0x00FFFFFF); + } } diff --git a/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsTranscribeKeyPreference.java b/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsTranscribeKeyPreference.java index 7b2a993811..dd4fb6b113 100644 --- a/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsTranscribeKeyPreference.java +++ b/extensions/music/src/main/java/app/morphe/extension/music/settings/preference/LyricsTranscribeKeyPreference.java @@ -36,18 +36,14 @@ import app.morphe.extension.shared.ui.Dim; /** - * The key for the service that transcribes or time-aligns the lyrics of the current track. It - * lives next to the AI endpoint settings because both are keys for a service the lyrics are sent - * to, and shares nothing with them but that. + * The key for the service that transcribes or time-aligns the lyrics of the current track, opened + * from its row in the lyrics service list. */ @SuppressWarnings({"unused", "deprecation"}) public class LyricsTranscribeKeyPreference extends Preference { private static final String DASHBOARD_URL = "https://quicklrc.com/dashboard"; - @Nullable - private Dialog activeDialog; - public LyricsTranscribeKeyPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); @@ -77,7 +73,7 @@ private void init() { @SuppressWarnings("ConstantValue") private void updateSummary() { - String key = Settings.LYRICS_TRANSCRIBE_KEY.get(); + final String key = Settings.LYRICS_TRANSCRIBE_KEY.get(); setSummary(key == null || key.trim().isEmpty() ? str("morphe_music_lyrics_transcribe_key_summary") : str("morphe_music_lyrics_transcribe_key_set_summary")); @@ -85,37 +81,38 @@ private void updateSummary() { @Override protected void onClick() { - showDialog(); + showDialog(null); + } + + /** Opens the key dialog; {@code onSaved} runs once a key the service accepted was stored. */ + void showDialog(@Nullable Runnable onSaved) { + showKeyDialog(getContext(), onSaved); } @SuppressWarnings("ConstantValue") - private void showDialog() { - Context context = getContext(); + static void showKeyDialog(Context context, @Nullable Runnable onSaved) { if (!(context instanceof Activity activity) || activity.isFinishing() || activity.isDestroyed()) { return; } - if (activeDialog != null && activeDialog.isShowing()) { - return; - } - String stored = Settings.LYRICS_TRANSCRIBE_KEY.get(); + final String stored = Settings.LYRICS_TRANSCRIBE_KEY.get(); final boolean configured = stored != null && !stored.trim().isEmpty(); - LinearLayout content = new LinearLayout(context); + final LinearLayout content = new LinearLayout(context); content.setOrientation(LinearLayout.VERTICAL); - TextView instruction = new TextView(context); + final TextView instruction = new TextView(context); instruction.setText(str("morphe_music_lyrics_transcribe_key_dialog_instruction")); instruction.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14); instruction.setTextColor(ThemeUtils.getAppForegroundColor()); - LinearLayout.LayoutParams instructionParams = new LinearLayout.LayoutParams( + final LinearLayout.LayoutParams instructionParams = new LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT); instructionParams.bottomMargin = Dim.dp12; content.addView(instruction, instructionParams); - EditText keyInput = ThemedInput.create(context); + final EditText keyInput = ThemedInput.create(context); keyInput.setHint("qlrc_..."); keyInput.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD); if (configured) { @@ -126,11 +123,11 @@ private void showDialog() { LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT)); - Button getKeyButton = CustomDialog.createButton(context, null, + final Button getKeyButton = CustomDialog.createButton(context, null, str("morphe_music_token_dialog_get_token"), () -> { try { - Intent intent = new Intent(Intent.ACTION_VIEW, + final Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(DASHBOARD_URL)); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); context.startActivity(intent); @@ -139,23 +136,22 @@ private void showDialog() { } }, false, false); - LinearLayout.LayoutParams getKeyParams = new LinearLayout.LayoutParams( + final LinearLayout.LayoutParams getKeyParams = new LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT, Dim.dp36); getKeyParams.topMargin = Dim.dp12; content.addView(getKeyButton, getKeyParams); - Pair dialogPair = CustomDialog.create( + final Pair dialogPair = CustomDialog.create( context, str("morphe_music_lyrics_transcribe_key_title"), null, null, str("morphe_settings_save"), () -> { - String key = keyInput.getText().toString().trim(); + final String key = keyInput.getText().toString().trim(); if (key.isEmpty()) { // Saving nothing clears it, which is also what the reset button does. Settings.LYRICS_TRANSCRIBE_KEY.resetToDefault(); - updateSummary(); Utils.showToastShort(str("morphe_music_lyrics_transcribe_key_cleared")); return; } @@ -168,8 +164,10 @@ private void showDialog() { return; } Settings.LYRICS_TRANSCRIBE_KEY.save(key); - updateSummary(); Utils.showToastShort(str("morphe_music_lyrics_transcribe_key_saved")); + if (onSaved != null) { + onSaved.run(); + } }); }, null, @@ -177,29 +175,21 @@ private void showDialog() { () -> { Settings.LYRICS_TRANSCRIBE_KEY.resetToDefault(); keyInput.setText(""); - updateSummary(); Utils.showToastShort(str("morphe_music_lyrics_transcribe_key_cleared")); }, false ); - Dialog dialog = dialogPair.first; - LinearLayout mainLayout = dialogPair.second; + final Dialog dialog = dialogPair.first; + final LinearLayout mainLayout = dialogPair.second; mainLayout.addView(content, mainLayout.getChildCount() - 1, new LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT)); - activeDialog = dialog; - dialog.setOnDismissListener(d -> { - if (activeDialog == d) { - activeDialog = null; - } - }); try { dialog.show(); } catch (WindowManager.BadTokenException ex) { - activeDialog = null; Logger.printDebug(() -> "Transcribe key dialog could not be shown", ex); } } diff --git a/extensions/music/src/test/java/app/morphe/extension/music/patches/lyrics/model/LyricsBracketSplitterTest.java b/extensions/music/src/test/java/app/morphe/extension/music/patches/lyrics/model/LyricsBracketSplitterTest.java deleted file mode 100644 index 7f537b93e0..0000000000 --- a/extensions/music/src/test/java/app/morphe/extension/music/patches/lyrics/model/LyricsBracketSplitterTest.java +++ /dev/null @@ -1,78 +0,0 @@ -package app.morphe.extension.music.patches.lyrics.model; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -import org.junit.Test; - -import java.util.List; - -public class LyricsBracketSplitterTest { - - private static Lyrics synced(LyricsLine... lines) { - return new Lyrics(List.of(lines), "test", true, null, null, null, null, null, null, null); - } - - private static LyricsLine background(long startMs, String text) { - return new LyricsLine(startMs, text, List.of(), null, false, true); - } - - @Test public void bracketedVocalMovesToALineOfItsOwn() { - List lines = LyricsBracketSplitter.apply( - synced(new LyricsLine(1000, "I want you (oh)"))).lines(); - assertEquals(2, lines.size()); - assertEquals("I want you", lines.get(0).text()); - assertFalse(lines.get(0).isBG()); - assertEquals("oh", lines.get(1).text()); - assertTrue(lines.get(1).isBG()); - } - - @Test public void wordTimingsFollowTheirSide() { - LyricsLine line = new LyricsLine(1000, 3000, "I want you (oh)", List.of( - new Word(1000, 1400, "I ", null, true), - new Word(1400, 1900, "want ", null, true), - new Word(1900, 2400, "you ", null, true), - new Word(2400, 3000, "(oh)"))); - List lines = LyricsBracketSplitter.apply(synced(line)).lines(); - assertEquals(2, lines.size()); - assertEquals("I want you", lines.get(0).text()); - assertEquals(3, lines.get(0).words().size()); - assertEquals("oh", lines.get(1).text()); - assertEquals(1, lines.get(1).words().size()); - assertEquals(2400, lines.get(1).startTimeMs()); - } - - @Test public void backgroundLineIsNotCutIntoALead() { - List lines = LyricsBracketSplitter.apply(synced( - new LyricsLine(0, "Hello"), background(500, "(ooh) yeah"))).lines(); - assertEquals(2, lines.size()); - assertTrue(lines.get(1).isBG()); - } - - @Test public void wholeBracketedLineBecomesBackgroundOfTheLeadAbove() { - List lines = LyricsBracketSplitter.apply(synced( - new LyricsLine(0, "Hello"), new LyricsLine(1000, "(Ooh, yeah)"))).lines(); - assertEquals(2, lines.size()); - assertEquals("Ooh, yeah", lines.get(1).text()); - assertTrue(lines.get(1).isBG()); - } - - @Test public void sectionNamesAndTagsStayAsTheyAre() { - List lines = LyricsBracketSplitter.apply(synced( - new LyricsLine(0, "Hello"), new LyricsLine(1000, "(Chorus)"), - new LyricsLine(2000, "【Live】 Hello (oh)"))).lines(); - assertEquals(3, lines.size()); - assertEquals("(Chorus)", lines.get(1).text()); - assertFalse(lines.get(1).isBG()); - assertEquals("【Live】 Hello (oh)", lines.get(2).text()); - } - - @Test public void singleWordLineWithWordTimingHasWordTimings() { - LyricsLine singleWord = new LyricsLine(1000, 2000, "Yeah", List.of(new Word(1000, 2000, "Yeah"))); - assertTrue(singleWord.hasWordTimings()); - - LyricsLine fakeWordSync = new LyricsLine(1000, 2000, "I want you", List.of(new Word(1000, 2000, "I want you"))); - assertFalse(fakeWordSync.hasWordTimings()); - } -} diff --git a/extensions/music/src/test/java/app/morphe/extension/music/patches/lyrics/model/LyricsLineWordTimingsTest.java b/extensions/music/src/test/java/app/morphe/extension/music/patches/lyrics/model/LyricsLineWordTimingsTest.java new file mode 100644 index 0000000000..5d6249238e --- /dev/null +++ b/extensions/music/src/test/java/app/morphe/extension/music/patches/lyrics/model/LyricsLineWordTimingsTest.java @@ -0,0 +1,61 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches/pull/4090 + * + * See the included NOTICE file for GPLv3 Section 7 terms that apply to this code. + */ + +package app.morphe.extension.music.patches.lyrics.model; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +import java.util.List; + +public class LyricsLineWordTimingsTest { + + @Test public void singleWordWithRealDurationHasWordTimings() { + final LyricsLine line = new LyricsLine(1000, 2000, "Yeah", + List.of(new Word(1000, 2000, "Yeah"))); + assertTrue(line.hasWordTimings()); + } + + @Test public void singleWordWithoutDurationDoesNot() { + final LyricsLine point = new LyricsLine(1000, 1000, "Yeah", + List.of(new Word(1000, 1000, "Yeah"))); + assertFalse(point.hasWordTimings()); + + final LyricsLine reversed = new LyricsLine(1000, 500, "Yeah", + List.of(new Word(1000, 500, "Yeah"))); + assertFalse(reversed.hasWordTimings()); + } + + @Test public void singleSpanCoveringAMultiWordLineDoesNot() { + final LyricsLine line = new LyricsLine(0, 1000, "hello world", + List.of(new Word(0, 1000, "hello world"))); + assertFalse(line.hasWordTimings()); + } + + /** A line made of one word repeated is still one word, so its span may be real timing. */ + @Test public void singleSpanOverARepeatedWordIsNotMistakenForALineSpan() { + final LyricsLine line = new LyricsLine(0, 1000, "la la la", + List.of(new Word(0, 1000, "la la la"))); + assertFalse(line.hasWordTimings()); + + final LyricsLine word = new LyricsLine(0, 1000, "Yeah Yeah", + List.of(new Word(0, 1000, "Yeah"))); + assertFalse(word.hasWordTimings()); + } + + /** + * Documents the known edge case: a script without spaces counts as one word, so a single + * full-line placeholder span is treated as word timing. + */ + @Test public void cjkLineWithOnePlaceholderSpanIsTreatedAsWordTimed() { + final LyricsLine line = new LyricsLine(0, 1000, "你好世界", + List.of(new Word(0, 1000, "你好世界"))); + assertTrue(line.hasWordTimings()); + } +} diff --git a/extensions/music/src/test/java/app/morphe/extension/music/patches/lyrics/model/TrackFormatTest.java b/extensions/music/src/test/java/app/morphe/extension/music/patches/lyrics/model/TrackFormatTest.java index 01e3efd9a7..3b61ade3da 100644 --- a/extensions/music/src/test/java/app/morphe/extension/music/patches/lyrics/model/TrackFormatTest.java +++ b/extensions/music/src/test/java/app/morphe/extension/music/patches/lyrics/model/TrackFormatTest.java @@ -30,7 +30,7 @@ public class TrackFormatTest { } @Test public void sectionNeedsEveryVariableInside() { - String template = "{t}: [{v} by {a}]:"; + final String template = "{t}: [{v} by {a}]:"; assertEquals("Duvet [Live by bôa]", MiniLyricsFormatter.trackLabel(template, "Duvet", "bôa", "Live")); assertEquals("Duvet", MiniLyricsFormatter.trackLabel(template, "Duvet", "bôa", null)); assertEquals("Duvet", MiniLyricsFormatter.trackLabel(template, "Duvet", "", "Live")); @@ -53,7 +53,7 @@ public class TrackFormatTest { } @Test public void longTrackLineIsSplitIntoFramesThatFit() { - List frames = MiniLyricsFormatter.trackFrames("{t} - {a}", + final List frames = MiniLyricsFormatter.trackFrames("{t} - {a}", "Smells Like Teen Spirit", "Nirvana", null, 10, 1); assertTrue(frames.size() > 2); for (String frame : frames) { @@ -64,12 +64,12 @@ public class TrackFormatTest { @Test public void splitIsDecidedFromTheWholeLine() { // Neither variable is over the limit, but the rendered line is. - List frames = MiniLyricsFormatter.trackFrames("{t} - {a}", "Duvet", "bôa", null, 8, 1); + final List frames = MiniLyricsFormatter.trackFrames("{t} - {a}", "Duvet", "bôa", null, 8, 1); assertEquals(List.of("Duvet -", "bôa"), frames); } @Test public void fullWidthCharactersCountAsTheMultiplier() { - String six = "あいうえおか"; + final String six = "あいうえおか"; assertEquals(6.0, MiniLyricsFormatter.width(six, 1), 0.001); assertEquals(10.8, MiniLyricsFormatter.width(six, 1.8), 0.001); assertEquals(1, MiniLyricsFormatter.chunks(new LyricsLine(0, six), 5000, 10, false, false, 1).size()); @@ -78,8 +78,8 @@ public class TrackFormatTest { } @Test public void unspacedSplitGetsOneEllipsis() { - String text = "あいうえおかきくけこ"; - List chunks = + final String text = "あいうえおかきくけこ"; + final List chunks = MiniLyricsFormatter.chunks(new LyricsLine(0, text), 5000, 5, false, false, 1); assertEquals(2, chunks.size()); assertTrue(chunks.get(0).text().endsWith(ELLIPSIS)); @@ -88,24 +88,24 @@ public class TrackFormatTest { } @Test public void spacedSplitHasNoEllipsis() { - List chunks = MiniLyricsFormatter.chunks( + final List chunks = MiniLyricsFormatter.chunks( new LyricsLine(0, "don't stop me now"), 5000, 10, false, false, 1); assertEquals("don't stop", chunks.get(0).text()); assertEquals("me now", chunks.get(1).text()); } @Test public void hyphenatedAndPunctuatedWordsStayWhole() { - List chunks = MiniLyricsFormatter.chunks( + final List chunks = MiniLyricsFormatter.chunks( new LyricsLine(0, "well-known words, again!"), 5000, 10, false, false, 1); assertEquals("well-known words,", chunks.get(0).text()); assertEquals("again!", chunks.get(1).text()); } @Test public void wordTimestampsDecideTheSplit() { - String text = "今日はいい天気"; - LyricsLine line = new LyricsLine(0, text, List.of( + final String text = "今日はいい天気"; + final LyricsLine line = new LyricsLine(0, text, List.of( new Word(0, 500, "今日は"), new Word(500, 1000, "いい"), new Word(1000, 1500, "天気"))); - List chunks = MiniLyricsFormatter.chunks(line, 2000, 3, false, false, 1); + final List chunks = MiniLyricsFormatter.chunks(line, 2000, 3, false, false, 1); assertEquals(2, chunks.size()); assertEquals("今日はいい" + ELLIPSIS, chunks.get(0).text()); assertEquals("天気", chunks.get(1).text()); diff --git a/patches/src/main/kotlin/app/morphe/patches/music/layout/lyrics/LyricsPatch.kt b/patches/src/main/kotlin/app/morphe/patches/music/layout/lyrics/LyricsPatch.kt index a6d7f2cfe2..56ea45c633 100644 --- a/patches/src/main/kotlin/app/morphe/patches/music/layout/lyrics/LyricsPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/music/layout/lyrics/LyricsPatch.kt @@ -25,7 +25,6 @@ import app.morphe.patches.shared.MediaSessionSetMetadataFingerprint import app.morphe.patches.shared.MediaSessionSetPlaybackStateFingerprint import app.morphe.patches.shared.misc.litho.filter.addLithoFilter import app.morphe.patches.shared.misc.settings.preference.InputType -import app.morphe.patches.shared.misc.settings.preference.ListPreference import app.morphe.patches.shared.misc.settings.preference.NonInteractivePreference import app.morphe.patches.shared.misc.settings.preference.PreferenceCategory import app.morphe.patches.shared.misc.settings.preference.PreferenceScreenPreference.Sorting @@ -99,6 +98,37 @@ val lyricsPatch = bytecodePatch( summaryKey = "morphe_music_lyrics_source_advice_summary", dependency = "morphe_music_lyrics_enabled" ), + NonInteractivePreference( + key = "morphe_music_lyrics_translation_provider", + summaryKey = null, + tag = "app.morphe.extension.music.settings.preference.LyricsOrderedListPreference", + selectable = false, + dependency = "morphe_music_lyrics_enabled" + ), + NonInteractivePreference( + key = "morphe_music_lyrics_translation_language", + titleKey = null, + summaryKey = null, + tag = "app.morphe.extension.music.settings.preference.LyricsTargetLanguagePreference", + selectable = true, + dependency = "morphe_music_lyrics_enabled" + ), + NonInteractivePreference( + key = "morphe_music_lyrics_romanization_provider", + summaryKey = null, + tag = "app.morphe.extension.music.settings.preference.LyricsOrderedListPreference", + selectable = false, + dependency = "morphe_music_lyrics_enabled" + ), + NonInteractivePreference( + key = "morphe_music_lyrics_romanize_language", + titleKey = null, + summaryKey = null, + tag = "app.morphe.extension.music.settings.preference.LyricsTargetLanguagePreference", + selectable = true, + dependency = "morphe_music_lyrics_enabled" + ), + SwitchPreference("morphe_music_lyrics_allow_transliteration", summary = true), NonInteractivePreference( key = "morphe_music_lyrics_clear_cache", tag = "app.morphe.extension.music.settings.preference.LyricsClearCachePreference", @@ -118,10 +148,21 @@ val lyricsPatch = bytecodePatch( dependency = "morphe_music_lyrics_enabled" ), SwitchPreference("morphe_music_lyrics_word_sync", summary = true), + SwitchPreference("morphe_music_lyrics_line_scale", summary = true), + SwitchPreference("morphe_music_lyrics_split_background", summary = true), SwitchPreference("morphe_music_lyrics_show_section", summary = true), + SwitchPreference( + key = "morphe_music_lyrics_use_ai_section", + summary = true, + tag = "app.morphe.extension.music.settings.preference.LyricsAiPromptPreference" + ), SwitchPreference("morphe_music_lyrics_hide_played", summary = true), SwitchPreference("morphe_music_lyrics_hide_unplayed", summary = true), - SwitchPreference("morphe_music_lyrics_hide_info", summary = true) + SwitchPreference("morphe_music_lyrics_hide_info", summary = true), + SwitchPreference("morphe_music_lyrics_force_capitalize", summary = true), + SwitchPreference("morphe_music_lyrics_strip_trailing_punctuation", summary = true), + SwitchPreference("morphe_music_lyrics_swap_trans_roma", summary = true), + SwitchPreference("morphe_music_lyrics_force_center", summary = true) ) ), PreferenceCategory( @@ -136,36 +177,6 @@ val lyricsPatch = bytecodePatch( SwitchPreference("morphe_music_lyrics_show_romanize_button", summary = true) ) ), - PreferenceCategory( - key = "morphe_music_lyrics_section_translation", - sorting = Sorting.UNSORTED, - preferences = setOf( - ListPreference( - key = "morphe_music_lyrics_translation_provider" - ), - ListPreference( - key = "morphe_music_lyrics_translation_language", - entriesKey = "morphe_language_entries", - entryValuesKey = "morphe_language_entry_values" - ) - ) - ), - PreferenceCategory( - key = "morphe_music_lyrics_section_romanization", - sorting = Sorting.UNSORTED, - preferences = setOf( - ListPreference( - key = "morphe_music_lyrics_romanization_provider" - ), - ListPreference(key = "morphe_music_lyrics_romanize_language"), - SwitchPreference( - key = "morphe_music_lyrics_local_romanization", - summary = true, - tag = "app.morphe.extension.music.settings.preference.LyricsLocalRomanizationPreference" - ), - SwitchPreference("morphe_music_lyrics_swap_trans_roma", summary = true) - ) - ), PreferenceCategory( key = "morphe_music_lyrics_section_sync", sorting = Sorting.UNSORTED, @@ -211,32 +222,6 @@ val lyricsPatch = bytecodePatch( ) ) ), - PreferenceCategory( - key = "morphe_music_lyrics_section_ai_endpoint", - sorting = Sorting.UNSORTED, - preferences = setOf( - NonInteractivePreference( - key = "morphe_music_lyrics_ai_config", - titleKey = "morphe_music_lyrics_ai_config_dialog_title", - summaryKey = null, - tag = "app.morphe.extension.music.settings.preference.LyricsAiConfigPreference", - selectable = true, - dependency = "morphe_music_lyrics_enabled" - ), - SwitchPreference( - key = "morphe_music_lyrics_use_ai_section", - summary = true - ), - NonInteractivePreference( - key = "morphe_music_lyrics_transcribe_key", - titleKey = "morphe_music_lyrics_transcribe_key_title", - summaryKey = "morphe_music_lyrics_transcribe_key_summary", - tag = "app.morphe.extension.music.settings.preference.LyricsTranscribeKeyPreference", - selectable = true, - dependency = "morphe_music_lyrics_enabled" - ) - ) - ), PreferenceCategory( key = "morphe_settings_music_lyrics_metadata", sorting = Sorting.UNSORTED, @@ -252,9 +237,7 @@ val lyricsPatch = bytecodePatch( TextPreference( key = "morphe_music_lyrics_credit_line_regex", inputType = InputType.TEXT_MULTI_LINE - ), - SwitchPreference("morphe_music_lyrics_force_capitalize", summary = true), - SwitchPreference("morphe_music_lyrics_strip_trailing_punctuation", summary = true) + ) ) ), screenInfoPreferenceCategory( diff --git a/patches/src/main/resources/addresources/values-af-rZA/music/strings.xml b/patches/src/main/resources/addresources/values-af-rZA/music/strings.xml index 3b6a9e5ac6..6860b1a72e 100644 --- a/patches/src/main/resources/addresources/values-af-rZA/music/strings.xml +++ b/patches/src/main/resources/addresources/values-af-rZA/music/strings.xml @@ -540,6 +540,8 @@ Vereiste koekies: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPISID Gestoor Raak aan ▲/▼ om die prioriteitsorde te stel, en die blokkie om elke verskaffer te aktiveer of deaktiveer. Alle verskaffers gedeaktiveer — ingeboude lirieke en ingebedde lirieke. + Wys agtergrondvokale afsonderlik + Verskuif \'n agtergrond-vokaal wat in hakies geskryf is, soos \"(Oh) ek wil hê jy\", uit die gesong lyn na \'n eie dowwer lyn Verskafferorde terugstel? Versteek liriekreëls bo die reël wat tans speel Versteek ongespeelde lirieke diff --git a/patches/src/main/resources/addresources/values-ar-rSA/music/strings.xml b/patches/src/main/resources/addresources/values-ar-rSA/music/strings.xml index 3c486eca83..e6841edb23 100644 --- a/patches/src/main/resources/addresources/values-ar-rSA/music/strings.xml +++ b/patches/src/main/resources/addresources/values-ar-rSA/music/strings.xml @@ -530,6 +530,8 @@ tظهر المسارات التي تم تنزيلها هنا" محفوظة قم بتمييز الكلمات والمقاطع اللفظية المتزامنة أعطِ الأولوية للكلمات المتزامنة كلمةً بكلمة عند توفرها. وعندما تحمل الكلمات توقيتًا للكلمات، يتم تمييز كل كلمة أو مقطع في توقيت مع الموسيقى. + إظهار الأغاني الخلفية بشكل منفصل + يُحرك غناء الخلفية المكتوب بين قوسين، مثل \"(واو) أريدك\"، من سطر الغناء إلى سطر خاص به بأقل وضوح إخفاء السطر الجاري تشغيله إخفاء سطور الكلمات الموجودة فوق السطر الحالي إخفاء الكلمات غير المشغّلة diff --git a/patches/src/main/resources/addresources/values-az-rAZ/music/strings.xml b/patches/src/main/resources/addresources/values-az-rAZ/music/strings.xml index 32fb0e5624..8a71045109 100644 --- a/patches/src/main/resources/addresources/values-az-rAZ/music/strings.xml +++ b/patches/src/main/resources/addresources/values-az-rAZ/music/strings.xml @@ -545,6 +545,8 @@ Fon səsləndirmə zamanı analiz sinxronlaşdırılmasına əmin olmaq üçün Saxlanıldı Sinxronlaşdırılmış sözləri və ya hecaları vurğula Mümkün olduqda sözbəsöz sözlərə üstünlük verir. Sözlərdə zamanlama olduqda, musiqi ilə eyni vaxtda hər bir sözü və ya hecanı vurğulayır + Fon vokallarını ayrıca göstər + ​\"(Oh) I want you\" kimi mötərizədə yazılmış arxa plan vokalını oxunan sətirdən çıxararaq ayrı, solğun sətirə keçirir Oxudulan sözləri gizlət Mövcud oxunan sətirdən yuxarıdakı söz sətirlərini gizlət Oxudulmamış sözləri gizlət diff --git a/patches/src/main/resources/addresources/values-bg-rBG/music/strings.xml b/patches/src/main/resources/addresources/values-bg-rBG/music/strings.xml index 4c2dfa6113..ccb69b6829 100644 --- a/patches/src/main/resources/addresources/values-bg-rBG/music/strings.xml +++ b/patches/src/main/resources/addresources/values-bg-rBG/music/strings.xml @@ -531,6 +531,8 @@ SABR възпроизвеждане (имитирането на видео по Запазено Открояване на синхронизираните думи или срички Дава приоритет на текст по думи, когато е наличен. Когато текстът има тайминг на думите, откроява всяка дума или сричка в синхрон с музиката. + Показвай фоновите вокали отделно + Премества фонови вокали, написани в скоби, като „(Ох) искам те“, извън пеената линия към по-бляскава линия на собствената си Скриване на вече изпетите редове Скриване на редовете от текста над реда, който се изпълнява в момента. Скриване на неизпетите редове diff --git a/patches/src/main/resources/addresources/values-bn-rBD/music/strings.xml b/patches/src/main/resources/addresources/values-bn-rBD/music/strings.xml index a7ca706aeb..96b85dd144 100644 --- a/patches/src/main/resources/addresources/values-bn-rBD/music/strings.xml +++ b/patches/src/main/resources/addresources/values-bn-rBD/music/strings.xml @@ -540,6 +540,8 @@ SABR প্লেব্যাক (ভিডিও স্ট্রিম স্ সংরক্ষিত হয়েছে সিঙ্ক্রোনাইজড শব্দ বা সিলেবল হাইলাইট করুন যখন পাওয়া যায়, শব্দে শব্দে লিরিক্সকে অগ্রাধিকার দিন। যখন লিরিক্সে শব্দের সময় থাকে, তখন সঙ্গীতের সাথে সময় মিলিয়ে প্রতিটি শব্দ বা সিলেবল হাইলাইট হয়। + ব্যাকগ্রাউন্ড ভোক্যাল আলাদাভাবে দেখান + \"(Oh)\" ইচ্ছুক অংশ বন্ধনীতে লেখা ব্যাকগ্রাউন্ড ভোক্যালকে গানের লাইন থেকে সরিয়ে একটি নিঃস্ব যমের রেখে দেয় লিরিক্স লুকান […] বর্তমানে বাজতে থাকা লাইনের উপরের লিরিক্সের লাইনগুলো লুকান যে লিরিক্স এখনো বাজানো হয়নি সেগুলো লুকান diff --git a/patches/src/main/resources/addresources/values-cs-rCZ/music/strings.xml b/patches/src/main/resources/addresources/values-cs-rCZ/music/strings.xml index 8cf5476fd4..03da3a06db 100644 --- a/patches/src/main/resources/addresources/values-cs-rCZ/music/strings.xml +++ b/patches/src/main/resources/addresources/values-cs-rCZ/music/strings.xml @@ -540,6 +540,8 @@ Požadované cookies: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAP Uloženo Zvýrazňovat synchronizovaná slova nebo slabiky Upřednostnit texty slovo po slově, pokud jsou k dispozici. Když texty obsahují načasování slov, zvýrazní každé slovo nebo slabiku v rytmu hudby. + Zobrazit vokály pozadí samostatně + Přesune pozadí vokálu uvedeného v závorkách, například „(Oh) chci tě“, z původní zpěvné linky do samostatné tišší linky Skrýt […] texty. Skrýt řádky textu nad aktuálně přehrávaným řádkem Skrýt nepřehrávané texty diff --git a/patches/src/main/resources/addresources/values-da-rDK/music/strings.xml b/patches/src/main/resources/addresources/values-da-rDK/music/strings.xml index d34ab78f5f..5ea8157b42 100644 --- a/patches/src/main/resources/addresources/values-da-rDK/music/strings.xml +++ b/patches/src/main/resources/addresources/values-da-rDK/music/strings.xml @@ -539,6 +539,8 @@ Krævede cookies: SID, HSID, SSID, SAPISID, __Secure-3PAPISID" Gemt Fremhæv synkroniserede ord eller stavelser Prioritér ord-for-ord-tekster, når de er tilgængelige. Når teksterne har ord-timing, fremhæves hvert ord eller stavelse i takt med musikken. + Vis baggrundsvokaler separat + Flytter en baggrundsvokal, der er skrevet i parenteser, som f.eks. \"(Oh) I want you\", ud af den sunge linje og over i en egen, svagere linje Skjul […] linje Skjul sangtekstlinjer over den aktuelt afspillede linje Skjul ikke-afspillede tekster diff --git a/patches/src/main/resources/addresources/values-de-rDE/music/strings.xml b/patches/src/main/resources/addresources/values-de-rDE/music/strings.xml index b8281bddbf..fd9d055f20 100644 --- a/patches/src/main/resources/addresources/values-de-rDE/music/strings.xml +++ b/patches/src/main/resources/addresources/values-de-rDE/music/strings.xml @@ -540,6 +540,8 @@ Erforderliche Cookies: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PA Gespeichert Hebe synchronisierte Wörter oder Silben hervor. Bevorzuge wortweise Songtexte, wenn verfügbar. Wenn Songtexte Wort-Timing enthalten, hebt dies jedes Wort oder jede Silbe synchron zur Musik hervor. + Hintergrundstimmen separat anzeigen + Verschiebt einen im Klammern notierten Hintergrundgesang, z. B. \"(Oh) ich will dich\", aus der gesungenen Zeile in eine eigene, gedimmte Zeile Nicht gespielte Songtexte ausblenden. Songtextzeilen oberhalb der aktuell abgespielten Zeile ausblenden Noch nicht abgespielte Songtextzeilen ausblenden diff --git a/patches/src/main/resources/addresources/values-el-rGR/music/strings.xml b/patches/src/main/resources/addresources/values-el-rGR/music/strings.xml index 8f4c2c8a14..d09520ab93 100644 --- a/patches/src/main/resources/addresources/values-el-rGR/music/strings.xml +++ b/patches/src/main/resources/addresources/values-el-rGR/music/strings.xml @@ -540,6 +540,8 @@ Second \"item\" text" Αποθηκεύτηκε Επισήμανση συγχρονισμένων λέξεων ή συλλαβών Δώστε προτεραιότητα στους στίχους λέξη-λέξη όταν είναι διαθέσιμοι. Όταν οι στίχοι έχουν χρονισμό λέξεων, επισημαίνεται κάθε λέξη ή συλλαβή σε συγχρονισμό με τη μουσική. + Εμφάνιση των φωνητικών φόντου ξεχωριστά + Μετακινεί ένα φωνητικό υπόβαθρο που γράφεται σε αγκύλες, όπως \"(Oh) I want you\", εκτός της τραγουδισμένης γραμμής σε μια ξεχωριστή γραμμή με μικρότερη ένταση Απόκρυψη γραμμής που έχει παιχτεί. Απόκρυψη γραμμών στίχων πάνω από τον τρέχοντα στίχο Απόκρυψη στίχων που δεν έχουν παιχτεί. diff --git a/patches/src/main/resources/addresources/values-es-rES/music/strings.xml b/patches/src/main/resources/addresources/values-es-rES/music/strings.xml index cce19fff2c..f5d3e33c5d 100644 --- a/patches/src/main/resources/addresources/values-es-rES/music/strings.xml +++ b/patches/src/main/resources/addresources/values-es-rES/music/strings.xml @@ -545,6 +545,8 @@ Cookies requeridas: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPIS Guardado Resaltar palabras o sílabas sincronizadas Prioriza las letras palabra por palabra cuando estén disponibles. Cuando las letras incluyen tiempos por palabra, resalta cada palabra o sílaba al ritmo de la música + Mostrar coros por separado + Mueve una línea de coros de fondo escrita entre paréntesis, como \"(Oh) te quiero\", fuera de la línea principal cantada a una línea propia con menor intensidad Ocultar letras ya reproducidas Oculta las líneas de letra por encima de la línea que se está reproduciendo Ocultar letras no reproducidas diff --git a/patches/src/main/resources/addresources/values-fa-rIR/music/strings.xml b/patches/src/main/resources/addresources/values-fa-rIR/music/strings.xml index b8328daa60..a5c29499a3 100644 --- a/patches/src/main/resources/addresources/values-fa-rIR/music/strings.xml +++ b/patches/src/main/resources/addresources/values-fa-rIR/music/strings.xml @@ -545,6 +545,8 @@ Second \"item\" text" ذخیره‌شده برجسته‌کردن واژه‌ها یا هجاهای همگام‌شده اولویت دادن به متن ترانهٔ واژه‌به‌واژه در صورت وجود. وقتی متن ترانه زمان‌بندی واژه دارد، هر واژه یا هجا را هماهنگ با موسیقی برجسته می‌کند + نمایش صداهای پس‌زمینه به صورت جداگانه + یک خط خواندن پس‌زمینه را که در پرانتز نوشته شده است، مانند «(آه) من تو را می‌خواهم»، از خط اصلی خوانده‌شده خارج کرده و به یک خط جداگانه با شدت کمتر منتقل می‌کند پنهان‌کردن متن ترانه‌های پخش‌شده پنهان‌کردن خطوط متن ترانه بالای خطِ در حال پخش پنهان‌کردن متن ترانه‌های پخشنشده diff --git a/patches/src/main/resources/addresources/values-fi-rFI/music/strings.xml b/patches/src/main/resources/addresources/values-fi-rFI/music/strings.xml index d5dd235bef..aae94db1e2 100644 --- a/patches/src/main/resources/addresources/values-fi-rFI/music/strings.xml +++ b/patches/src/main/resources/addresources/values-fi-rFI/music/strings.xml @@ -540,6 +540,8 @@ Vaaditut evästeet: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPIS Tallennettu Korosta synkronoidut sanat tai tavut Priorisoi sana sanalta -lyriikat, kun ne ovat saatavilla. Kun lyriikoissa on sanan ajoitus, korostaa jokaisen sanan tai tavun musiikin tahdissa + Näytä taustalaulu erikseen + Siirtää sulkuihin kirjoitetun taustalaulun, kuten \"(Oh) I want you\", laulun sävellysviivan ulkopuolelle omaksi himmeämmäksi rivikseen Piilota soitetut lyriikat Piilota lyriikkarivit nykyisen soittorivin yläpuolelta Piilota soittamattomat lyriikat diff --git a/patches/src/main/resources/addresources/values-fil-rPH/music/strings.xml b/patches/src/main/resources/addresources/values-fil-rPH/music/strings.xml index 6e0a932650..1b36685887 100644 --- a/patches/src/main/resources/addresources/values-fil-rPH/music/strings.xml +++ b/patches/src/main/resources/addresources/values-fil-rPH/music/strings.xml @@ -540,6 +540,8 @@ Mga kinakailangang cookie: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure Na-save I-highlight ang mga salita o pantig na naka-sync Unahin ang word-by-word na lyrics kung available. Kapag may word timing ang lyrics, ine-highlight ang bawat salita o pantig na kasabay ng musika. + Ipakita hiwalay ang background vocals + Gine-likas ang background na bokol na nakasulat sa bracket, tulad ng \"(Oh) gusto kita\", mula sa linya ng kanta upang maging hiwalay na linya na may mas mahinaw na tunog Itago ang linyang na-play na. Itago ang mga linya ng lyrics sa itaas ng linyang kasalukuyang pinapatugtog Itago ang mga lyrics na hindi pa na-play. diff --git a/patches/src/main/resources/addresources/values-fr-rFR/music/strings.xml b/patches/src/main/resources/addresources/values-fr-rFR/music/strings.xml index f5719f7626..7610721258 100644 --- a/patches/src/main/resources/addresources/values-fr-rFR/music/strings.xml +++ b/patches/src/main/resources/addresources/values-fr-rFR/music/strings.xml @@ -545,6 +545,8 @@ Cookies nécessaires : SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PA Enregistré Mettre en surbrillance les mots ou les syllabes synchronisés Privilégier les paroles mot à mot lorsqu\'elles sont disponibles. Lorsque les paroles disposent d\'un minutage mot à mot, mettre en surbrillance chaque mot ou chaque syllabe en rythme avec la musique + Afficher les voix d\'accompagnement séparément + Déplace une voix d\'accompagnement écrite entre parenthèses, comme \"(Oh) I want you\", hors de la ligne chantée pour l\'afficher sur sa propre ligne de façon plus discrète Masquer les paroles déjà lues Masquer les lignes de paroles au-dessus de la ligne en cours de lecture Masquer les paroles non lues diff --git a/patches/src/main/resources/addresources/values-ga-rIE/music/strings.xml b/patches/src/main/resources/addresources/values-ga-rIE/music/strings.xml index f864b5e562..3f05f3a03e 100644 --- a/patches/src/main/resources/addresources/values-ga-rIE/music/strings.xml +++ b/patches/src/main/resources/addresources/values-ga-rIE/music/strings.xml @@ -545,6 +545,8 @@ Fianáin riachtanacha: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PA Sábháilte Aibhsigh focail nó siollaí sioncronaithe Tabhair tosaíocht do liricí a chuirtear i láthair focal ar fhocal nuair a bhíonn siad ar fáil. Nuair a bhíonn uainiú focal ag gabháil leis na liricí, déantar gach focal nó siolla a aibhsiú i gcomhthráth leis an gceol + Taispeáin ceol cúlra go hiomlán ar leithligh + Bogann sé guth cúlra atá scríofa idir lúibíní — mar shampla \"(Oh) I want you\" — amach as an líne a chantar, á chur i líne ar leith atá níos séimhe. Folaigh na liricí a seinneadh Folaigh línte na liricí os cionn na líne atá á seinm faoi láthair Folaigh liricí nár seinneadh diff --git a/patches/src/main/resources/addresources/values-hi-rIN/music/strings.xml b/patches/src/main/resources/addresources/values-hi-rIN/music/strings.xml index 97ad7d64f8..cf13d66e29 100644 --- a/patches/src/main/resources/addresources/values-hi-rIN/music/strings.xml +++ b/patches/src/main/resources/addresources/values-hi-rIN/music/strings.xml @@ -540,6 +540,8 @@ SABR प्लेबैक (स्पूफ वीडियो स्ट्र सहेजा गया सिंक किए गए शब्दों या अक्षरों को हाइलाइट करें जब उपलब्ध हों तो शब्द-दर-शब्द गीतों को प्राथमिकता दें। जब गीतों में शब्द टाइमिंग होती है, तो संगीत के साथ समय पर प्रत्येक शब्द या अक्षर को हाइलाइट करता है + पृष्ठभूमि गायन को अलग से दिखाएं + कोष्ठक में लिखी पृष्ठभूमि वोकल, जैसे \"(ओह) मैं तुम चाहता हूँ\", को गाए गए लाइन से बाहर निकालकर अपनी खुद की धीमी लाइन में स्थानांतरित करता है बजाए गए गीत छिपाएँ वर्तमान में चल रही पंक्ति के ऊपर की गीत पंक्तियों को छिपाएँ नहीं बजाए गए गीत छिपाएँ diff --git a/patches/src/main/resources/addresources/values-hr-rHR/music/strings.xml b/patches/src/main/resources/addresources/values-hr-rHR/music/strings.xml index f4cc29064a..ea13580b45 100644 --- a/patches/src/main/resources/addresources/values-hr-rHR/music/strings.xml +++ b/patches/src/main/resources/addresources/values-hr-rHR/music/strings.xml @@ -545,6 +545,8 @@ Potrebni kolačići: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPI Spremljeno Istakni sinkronizirane riječi ili slogove Daj prednost tekstovima pjesama riječ po riječ kada su dostupni. Kada tekstovi nose vremenske oznake riječi, istakni svaku riječ ili slog u ritmu s glazbom. + Prikažite pozadinske vokale odvojeno + Premješta vokale pozadine upisane u zagradama, poput \"(Oh) I want you\", iz pjevne dionice u vlastitu liniju manjeg intenziteta Sakrij reproducirane tekstove pjesama Sakrij retke teksta pjesme iznad trenutačno reproduciranog retka. Sakrij nereproducirane tekstove pjesama diff --git a/patches/src/main/resources/addresources/values-hu-rHU/music/strings.xml b/patches/src/main/resources/addresources/values-hu-rHU/music/strings.xml index 81cb235de7..379a1ab749 100644 --- a/patches/src/main/resources/addresources/values-hu-rHU/music/strings.xml +++ b/patches/src/main/resources/addresources/values-hu-rHU/music/strings.xml @@ -545,6 +545,8 @@ Szükséges cookie-k: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAP Mentett Szinkronizált szavak vagy szótagok kiemelése Előnyben részesíti a szavankénti dalszövegeket, ha elérhetők. Ha a dalszöveg szószintű időzítést tartalmaz, a zene ütemére emeli ki az egyes szavakat vagy szótagokat. + A háttérvokál megjelenítése külön + A zárójelben megadott háttérvokált (például: „(Óh) téged szeretlek”) kiveszi a dalos sorból, és elkülönít egy halkabb, saját sort hoz létre neki. A lejátszott sor elrejtése Minilejátszó dalszövegének megjelenítése A még el nem hangzott dalszövegek elrejtése diff --git a/patches/src/main/resources/addresources/values-in-rID/music/strings.xml b/patches/src/main/resources/addresources/values-in-rID/music/strings.xml index 4568e4dc6f..d8ede69ea7 100644 --- a/patches/src/main/resources/addresources/values-in-rID/music/strings.xml +++ b/patches/src/main/resources/addresources/values-in-rID/music/strings.xml @@ -545,6 +545,8 @@ Cookie yang diperlukan: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1P Tersimpan Sorot kata atau suku kata yang disinkronkan Utamakan lirik kata per kata jika tersedia. Setiap kata atau suku kata akan disorot sesuai irama musik. + Tampilkan vokal latar secara terpisah + Memindahkan vokal latar belakang yang ditulis dalam tanda kurung, seperti \"(Oh) saya ingin kamu\", dari baris lagu ke baris redupnya sendiri Sembunyikan lirik yang telah diputar Sembunyikan baris lirik di atas garis yang sedang diputar Sembunyikan lirik yang belum diputar diff --git a/patches/src/main/resources/addresources/values-is-rIS/music/strings.xml b/patches/src/main/resources/addresources/values-is-rIS/music/strings.xml index d50d0053ea..d0b3f6dd45 100644 --- a/patches/src/main/resources/addresources/values-is-rIS/music/strings.xml +++ b/patches/src/main/resources/addresources/values-is-rIS/music/strings.xml @@ -543,6 +543,8 @@ Nauðsynlegar vafrakökur: SID, HSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPI Vistað Auðkenna samstillt orð eða atkvæði Sérsníddu texta til að sýna lög og frumtexta. + Birta bakraddir sérstaklega + Færir bakraddað söngorð sem eru í svigum, eins og \"(Ó)\", úr sönglínunni yfir á eigin deyfðari línu Fela spilaða texta Fela textalínur fyrir ofan línuna sem spilast núna Fela óspilaða texta diff --git a/patches/src/main/resources/addresources/values-it-rIT/music/strings.xml b/patches/src/main/resources/addresources/values-it-rIT/music/strings.xml index 83300dc28c..f3fe1cdbb0 100644 --- a/patches/src/main/resources/addresources/values-it-rIT/music/strings.xml +++ b/patches/src/main/resources/addresources/values-it-rIT/music/strings.xml @@ -539,6 +539,8 @@ Cookie richiesti: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPISID Salvato Lingua dei testi Mostra i testi + Mostrare le vocali di sottofondo separatamente + Sposta il coro di sottofondo scritto tra parentesi, ad esempio \"(Oh) ti voglio\", dalla parte cantata in una riga distinta con minore intensità Testi sincronizzati Nascondi le righe dei testi sopra la riga attualmente in riproduzione Copia i testi diff --git a/patches/src/main/resources/addresources/values-iw-rIL/music/strings.xml b/patches/src/main/resources/addresources/values-iw-rIL/music/strings.xml index 0403eaabbb..f578a8ed96 100644 --- a/patches/src/main/resources/addresources/values-iw-rIL/music/strings.xml +++ b/patches/src/main/resources/addresources/values-iw-rIL/music/strings.xml @@ -545,6 +545,8 @@ Second \"item\" text" נשמר הדגש מילים או הברות מסונכרנות תעדף מילים מילה-מילה כשהן זמינות. כשמילות השיר כוללות תזמון מילים, הדגש כל מילה או הברה בקצב המוזיקה + הצג ווקאלים ברקע בנפרד + מעביר ווקאל רקע הכתוב בסוגריים, כמו \"(או) אני רוצה אותך\", מקו השיר לקו עצמאי בעוצמה נמוכה יותר הסתר מילים שכבר נוגנו הסתר שורות מילים מעל השורה המושמעת כעת הסתר מילים שטרם נוגנו diff --git a/patches/src/main/resources/addresources/values-ja-rJP/music/strings.xml b/patches/src/main/resources/addresources/values-ja-rJP/music/strings.xml index 82e4d9ee9a..039e458454 100644 --- a/patches/src/main/resources/addresources/values-ja-rJP/music/strings.xml +++ b/patches/src/main/resources/addresources/values-ja-rJP/music/strings.xml @@ -538,6 +538,8 @@ SABR再生(動画ストリームのスプーフがオフ)" 保存しました 同期中の単語や音節をハイライト表示 利用可能な場合は単語ごとのタイミング情報を含む歌詞を優先します。単語ごとのタイミング情報を含む歌詞を使用する場合は、楽曲に合わせて各単語または音節をハイライト表示します + バックコーラスを分離して表示 + 「(Oh) I want you」のように括弧で囲まれたバックコーラスを、歌詞の行から分離し、専用の独立した行に薄く表示します 再生済みの歌詞を非表示 現在再生中の行より上に表示される歌詞を非表示にします 未再生の歌詞を非表示 diff --git a/patches/src/main/resources/addresources/values-ko-rKR/music/strings.xml b/patches/src/main/resources/addresources/values-ko-rKR/music/strings.xml index e404e8de31..de437a585e 100644 --- a/patches/src/main/resources/addresources/values-ko-rKR/music/strings.xml +++ b/patches/src/main/resources/addresources/values-ko-rKR/music/strings.xml @@ -547,6 +547,8 @@ LOGIN_INFO" 저장됨 동기화된 단어 또는 음절 강조 표시하기 가능한 경우 단어 단위로 표시되는 가사를 우선합니다. 가사에 단어별 타이밍 정보가 포함되어 있으면, 음악에 맞춰 각 단어 음절을 순서대로 강조 표시합니다 + 백그라운드 보컬 별도 표시하기 + \'(Oh) I want you\'와 같이 괄호로 작성된 백그라운드 보컬을 가사 줄에서 분리하여, 별도의 흐릿한 줄로 이동합니다 재생된 가사 숨기기 현재 재생 중인 줄 위의 가사 줄을 숨깁니다 재생되지 않은 가사 숨기기 diff --git a/patches/src/main/resources/addresources/values-lt-rLT/music/strings.xml b/patches/src/main/resources/addresources/values-lt-rLT/music/strings.xml index 5c8f358af7..20430fe6f0 100644 --- a/patches/src/main/resources/addresources/values-lt-rLT/music/strings.xml +++ b/patches/src/main/resources/addresources/values-lt-rLT/music/strings.xml @@ -545,6 +545,8 @@ Reikalingi slapukai: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPI Išsaugota Paryškinti sinchronizuotus žodžius ar skiemenis Teikti pirmenybę žodis po žodžio sinchronizuotiems dainų tekstams, kai jie prieinami. Kai dainų tekstai turi žodžių laiką, kiekvienas žodis ar skiemuo paryškinamas pagal muziką + Rodyti fono vokalus atskirai + Perkelia foninį vokalinį tekstą, esantį skliaustuose, pvz., „(Oh) I want you“, iš pagrindinio kūrio teksto atskira švelnesne eilute Slėpti jau atkurtus dainų tekstus Slėpti dainų teksto eilutes virš šiuo metu grojamos eilutės Slėpti neatkurtus dainų tekstus diff --git a/patches/src/main/resources/addresources/values-nb-rNO/music/strings.xml b/patches/src/main/resources/addresources/values-nb-rNO/music/strings.xml index 75a8a8fcc2..d5f7bf0ac1 100644 --- a/patches/src/main/resources/addresources/values-nb-rNO/music/strings.xml +++ b/patches/src/main/resources/addresources/values-nb-rNO/music/strings.xml @@ -543,6 +543,8 @@ Nødvendige informasjonskapsler: SID, HSID, SSID, SAPISID, __Secure-1PAPISID, __ Lagret Marker synkroniserte ord eller stavelser Prioriter ord-for-ord-tekster når de er tilgjengelige. Når teksten har tidsstempling for ord, vises hvert ord eller hver stavelse synkronisert med musikken + Vis bakgrunnsvokaler separat + Flytter en bakgrunnsvokal skrevet i klammer, slik som \"(Å) jeg vil ha deg\", ut av den sunge linjen og over i en egen, svakere linje Skjul […] linje Skjul sangtekstlinjene over linjen som spilles for øyeblikket Skjul ikke-avspilte tekster diff --git a/patches/src/main/resources/addresources/values-nl-rNL/music/strings.xml b/patches/src/main/resources/addresources/values-nl-rNL/music/strings.xml index 702c4d1e9e..ee833fbc5c 100644 --- a/patches/src/main/resources/addresources/values-nl-rNL/music/strings.xml +++ b/patches/src/main/resources/addresources/values-nl-rNL/music/strings.xml @@ -535,6 +535,8 @@ Vereiste cookies: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPISID Opgeslagen Gesynchroniseerde woorden of lettergrepen markeren Geef prioriteit aan woord-voor-woord songteksten wanneer beschikbaar. Wanneer songteksten woordtiming bevatten, wordt elk woord of elke lettergreep gemarkeerd in de maat met de muziek + Achtergrondvocalen apart weergeven + Verplaatst een achtergrondzang die tussen haakjes staat, zoals \"(Oh) I want you\", uit de gezongen regel naar een aparte zwakkere regel Verberg […] regel Verberg songtekstregels boven de regel die momenteel wordt afgespeeld Verberg niet-afgespeelde songteksten diff --git a/patches/src/main/resources/addresources/values-pl-rPL/music/strings.xml b/patches/src/main/resources/addresources/values-pl-rPL/music/strings.xml index 9b7fcf9f53..63cea35643 100644 --- a/patches/src/main/resources/addresources/values-pl-rPL/music/strings.xml +++ b/patches/src/main/resources/addresources/values-pl-rPL/music/strings.xml @@ -545,6 +545,8 @@ Wymagane ciasteczka: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPI Zapisano Podświetlaj zsynchronizowane słowa lub sylaby Nadaje priorytet tekstom synchronizowanym słowo po słowie, o ile są dostępne. Gdy tekst zawiera znaczniki czasu, podświetla każde słowo lub sylabę w rytm muzyki + Pokaż wokal w tle osobno + Przenosi wokal w tle zapisany w nawiasach, np. \"(Och) Chcę cię\", z głównego wersu do osobnej, przyciemnionej linii Ukryj odtworzony tekst Ukrywa linijki tekstu znajdujące się powyżej tej aktualnie odtwarzanej Ukryj nieodtworzony tekst diff --git a/patches/src/main/resources/addresources/values-pt-rBR/music/strings.xml b/patches/src/main/resources/addresources/values-pt-rBR/music/strings.xml index e7e5658587..f23a9ca00e 100644 --- a/patches/src/main/resources/addresources/values-pt-rBR/music/strings.xml +++ b/patches/src/main/resources/addresources/values-pt-rBR/music/strings.xml @@ -535,6 +535,8 @@ Cookies necessários: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAP Salvo Realçar palavras ou sílabas sincronizadas. Priorizar letras palavra por palavra quando disponíveis. Quando as letras têm sincronização por palavra, destaca cada palavra ou sílaba no tempo da música. + Mostrar vocais de fundo separadamente + Move uma vocal de fundo escrito entre parênteses, como \"(Oh) eu quero você\", da linha cantada para uma linha própria com menor volume Ocultar […] linha Ocultar linhas da letra acima da linha atualmente em reprodução Ocultar letras não reproduzidas. diff --git a/patches/src/main/resources/addresources/values-pt-rPT/music/strings.xml b/patches/src/main/resources/addresources/values-pt-rPT/music/strings.xml index ac3c748769..278f944a22 100644 --- a/patches/src/main/resources/addresources/values-pt-rPT/music/strings.xml +++ b/patches/src/main/resources/addresources/values-pt-rPT/music/strings.xml @@ -545,6 +545,8 @@ Cookies necessários: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAP Guardado Destacar palavras ou sílabas sincronizadas Priorizar letras sincronizadas em nível de palavra + Mostrar vocais de fundo separadamente + Remove uma vocalização de fundo escrita entre parênteses, como \"(Oh) eu quero você\", da linha cantada e a coloca em sua própria linha mais suave Ocultar a linha reproduzida Ocultar linhas da letra acima da linha em reprodução Ocultar letras não reproduzidas diff --git a/patches/src/main/resources/addresources/values-ro-rRO/music/strings.xml b/patches/src/main/resources/addresources/values-ro-rRO/music/strings.xml index b746a55b31..c5af8ecc9f 100644 --- a/patches/src/main/resources/addresources/values-ro-rRO/music/strings.xml +++ b/patches/src/main/resources/addresources/values-ro-rRO/music/strings.xml @@ -528,6 +528,8 @@ Cookie-uri necesare: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPI Salvat Evidențiați cuvintele sau silabele sincronizate Nu s-a putut face romanizarea versurilor. + Afișați vocalurile de fundal separat + Mută vocea de fundal scrisă între paranteze, cum ar fi \"(Oh) te vreau\", din linia cântată într-o linie mai slabă Ascundeți versurile. Ascundeți rândurile de versuri de mai jos. Ascundeți versurile necunoscute. diff --git a/patches/src/main/resources/addresources/values-ru-rRU/music/strings.xml b/patches/src/main/resources/addresources/values-ru-rRU/music/strings.xml index c55faf92cf..0b1566bacc 100644 --- a/patches/src/main/resources/addresources/values-ru-rRU/music/strings.xml +++ b/patches/src/main/resources/addresources/values-ru-rRU/music/strings.xml @@ -540,6 +540,8 @@ SABR-воспроизведение (подмена видеопотоков о Сохранено Подсвечивать синхронизированные слова по мере воспроизведения, если доступно Отдавать приоритет посекундному тексту, когда он доступен. Когда в тексте есть тайминги слов, подсвечивать каждое слово или слог в такт музыке. + Показывать бэк-вокал отдельно + Переносит фоновый вокал, записанный в скобках, например \"(О) я хочу тебя\", из основной строки в отдельную слабую строку Скрывать воспроизведённые строки Скрывать строки текста песни выше текущей воспроизводимой строки Скрывать невоспроизведённые строки diff --git a/patches/src/main/resources/addresources/values-sk-rSK/music/strings.xml b/patches/src/main/resources/addresources/values-sk-rSK/music/strings.xml index 8d4b5248f7..df8dbbe23b 100644 --- a/patches/src/main/resources/addresources/values-sk-rSK/music/strings.xml +++ b/patches/src/main/resources/addresources/values-sk-rSK/music/strings.xml @@ -540,6 +540,8 @@ Vyžadované cookies: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAP Uložené Kopírovať pôvodný text piesne Kopírovať preložený text piesne + Zobraziť pozadové vokály samostatne + Presunie vokál v pozadí zapísaný v zátvorkách, napríklad \"(Ó) chcem ťa\", zo speváckej línie do vlastnej tichšej línie Pôvodný text piesne Preložený text piesne Preklad bol skopírovaný diff --git a/patches/src/main/resources/addresources/values-sl-rSI/music/strings.xml b/patches/src/main/resources/addresources/values-sl-rSI/music/strings.xml index 04a35359b9..18add0c473 100644 --- a/patches/src/main/resources/addresources/values-sl-rSI/music/strings.xml +++ b/patches/src/main/resources/addresources/values-sl-rSI/music/strings.xml @@ -531,6 +531,8 @@ Zahtevani piškotki: SID, HSID, SSID, SAPISID, __Secure-3APISID, __Secure-1APISI Shranjeno Poudarite sinhronizirane besede ali zloge Dajte prednost besedilom po posameznih besedah, kadar so na voljo. Ko ima besedilo časovne podatke za besede, poudari vsako besedo ali zlog v skladu z glasbo. + Prikaži spremljalni vokal ločeno + Premakne vokale v oklepajih, kot je \"(Oh) I want you\", iz glavne besedila v ločeno, tišjo črto Skrijte že predvajane vrstice besedila Skrijte vrstice besedila nad trenutno predvajano vrstico. Skrijte še nepredvane vrstice besedila diff --git a/patches/src/main/resources/addresources/values-sr-rCS/music/strings.xml b/patches/src/main/resources/addresources/values-sr-rCS/music/strings.xml index 6d7ec60782..891447f349 100644 --- a/patches/src/main/resources/addresources/values-sr-rCS/music/strings.xml +++ b/patches/src/main/resources/addresources/values-sr-rCS/music/strings.xml @@ -536,6 +536,8 @@ Potrebni kolačići: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPI Tekst se traži po naslovu numere i izvođaču, […] Možete i da pritisnete dugme za osvežavanje da biste ručno promenili verzije. […] Svi omogućeni izvori se ne traže uvek po vašem redosledu, već odgovaraju prema svojoj brzini. Istakni sinhronizovane reči ili slogove. Daj prednost tekstu reč po reč kada je dostupan. + Prikaži pozadinske vokale posebno + Premješta pozadinski vokal zapisan u zagradama, poput \"(Oh) želim te\", iz pevane linije u zasebnu, prigušeniju liniju Sakrij odsvirane stihove. Sakrij stihove iznad reda koji se trenutno pušta. Sakrij neodsvirane stihove. diff --git a/patches/src/main/resources/addresources/values-sr-rSP/music/strings.xml b/patches/src/main/resources/addresources/values-sr-rSP/music/strings.xml index 2999039921..69629ab7dd 100644 --- a/patches/src/main/resources/addresources/values-sr-rSP/music/strings.xml +++ b/patches/src/main/resources/addresources/values-sr-rSP/music/strings.xml @@ -540,6 +540,8 @@ Da biste osigurali sinhronizaciju scrobbling-a tokom reprodukcije u pozadini, po Сачувано Истакни синхронизоване речи или слогове. Дај предност текстовима реч по реч када су доступни. Када текстови носе време, истакните сваку реч или слог у складу са музиком. + Прикажите пратеће вокале одвојено + Премешта вокал у позадини написан у заградама, попут \"(Ох) желим те\", из линије певања у самосталну линију са смањеном јачином звука Сакриј […] линију Сакријте редове текста изнад тренутно репродукованог реда Сакриј неодсвиране текстове песама. diff --git a/patches/src/main/resources/addresources/values-sv-rSE/music/strings.xml b/patches/src/main/resources/addresources/values-sv-rSE/music/strings.xml index 1585a2ea93..69213f246e 100644 --- a/patches/src/main/resources/addresources/values-sv-rSE/music/strings.xml +++ b/patches/src/main/resources/addresources/values-sv-rSE/music/strings.xml @@ -540,6 +540,8 @@ Obligatoriska cookies: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PA Sparad Markera synkroniserade ord eller stavelser Prioritera ord-för-ord-text när det finns. När texten har ordtajming markeras varje ord eller stavelse i takt med musiken. + Visa bakgrundsvokaler separat + Flyttar en bakgrundsvokal som skrivits inom parenteser, till exempel \"(Åh) jag vill ha dig\", från sånglinjen till sin egen dämpade rad Dölj […] raden. Dölj textrader ovanför den raden som spelas för tillfället Dölj ospelade texter. diff --git a/patches/src/main/resources/addresources/values-th-rTH/music/strings.xml b/patches/src/main/resources/addresources/values-th-rTH/music/strings.xml index 4d4fc258cf..ded6a8a460 100644 --- a/patches/src/main/resources/addresources/values-th-rTH/music/strings.xml +++ b/patches/src/main/resources/addresources/values-th-rTH/music/strings.xml @@ -545,6 +545,8 @@ Second \"item\" text" บันทึกแล้ว ไฮไลต์คำหรือพยางค์ที่ซิงก์ ให้ความสำคัญกับเนื้อเพลงแบบคำต่อคำเมื่อมีให้บริการ เมื่อเนื้อเพลงมีข้อมูลเวลาของคำ จะไฮไลต์แต่ละคำหรือพยางค์ให้ตรงกับจังหวะเพลง + แสดงเสียงร้องประกอบแยกต่างหาก + ย้ายเสียงร้องพื้นหลังที่เขียนอยู่ในวงเล็บ เช่น \"(โอ้) ฉันต้องการคุณ\" ออกจากท่อนร้องหลักไปยังบรรทัดแยกต่างหากที่มีความเบากว่าของตัวเอง ซ่อนบรรทัดที่เล่นแล้ว […] ซ่อนบรรทัดเนื้อเพลงที่อยู่เหนือบรรทัดที่กำลังเล่น ซ่อนเนื้อเพลงที่ยังไม่เล่น diff --git a/patches/src/main/resources/addresources/values-tr-rTR/music/strings.xml b/patches/src/main/resources/addresources/values-tr-rTR/music/strings.xml index c8ddfd2be9..d3fd62f699 100644 --- a/patches/src/main/resources/addresources/values-tr-rTR/music/strings.xml +++ b/patches/src/main/resources/addresources/values-tr-rTR/music/strings.xml @@ -545,6 +545,8 @@ Gerekli çerezler: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPISI Kaydedildi Senkronize kelimeleri veya heceleri vurgula Mümkün olduğunda kelime kelime şarkı sözlerine öncelik ver. Şarkı sözleri kelime zamanlaması içeriyorsa, her kelimeyi veya heceyi müzikle eş zamanlı olarak vurgular + Arka plan vokallerini ayrı göster + Parantez içinde yazılan arka plan vokalleri, örneğin \"(Oh) seni istiyorum\", söylenen satırdan ayrı bir daha düşük sesli satıra taşır Satırı gizle Şu anda çalınan satırın üzerindeki şarkı sözü satırlarını gizle Okunmamış şarkı sözlerini gizle diff --git a/patches/src/main/resources/addresources/values-uk-rUA/music/strings.xml b/patches/src/main/resources/addresources/values-uk-rUA/music/strings.xml index 8b1a3c7eb4..f8a1cdc662 100644 --- a/patches/src/main/resources/addresources/values-uk-rUA/music/strings.xml +++ b/patches/src/main/resources/addresources/values-uk-rUA/music/strings.xml @@ -545,6 +545,8 @@ Second \"item\" text" Збережено Підсвічувати синхронізовані слова або склади Надавати пріоритет тексту пісні по словах, коли доступно. Якщо текст пісні містить синхронізацію по словах, підсвічує кожне слово або склад у такт музиці + Показувати бек-вокал окремо + Виносить бек-вокал, записаний у дужках, наприклад \"(Oh) I want you\", із рядка основного вокалу в окремий, тьмяніший рядок Приховувати відтворені рядки тексту Приховувати рядки тексту пісні над рядком, що зараз відтворюється Приховувати ще не відтворені рядки тексту diff --git a/patches/src/main/resources/addresources/values-vi-rVN/music/strings.xml b/patches/src/main/resources/addresources/values-vi-rVN/music/strings.xml index 5a51d9d42b..c685ad246f 100644 --- a/patches/src/main/resources/addresources/values-vi-rVN/music/strings.xml +++ b/patches/src/main/resources/addresources/values-vi-rVN/music/strings.xml @@ -536,6 +536,8 @@ Các bước: Đã lưu Đánh dấu các từ hoặc âm tiết đồng bộ Ưu tiên lời bài hát từng từ khi có sẵn. Khi lời bài hát có thời gian cho từng từ, đánh dấu từng từ hoặc âm tiết theo nhạc + Hiển thị giọng hát nền riêng biệt + Di chuyển lời hát nền được viết trong ngoặc, ví dụ như \"(Ồ) anh muốn em\", khỏi dòng chính thành một dòng riêng biệt có độ mờ thấp hơn Ẩn lời bài hát đã phát Ẩn các dòng lời bài hát phía trên dòng đang phát Ẩn lời bài hát chưa phát diff --git a/patches/src/main/resources/addresources/values-zh-rCN/music/strings.xml b/patches/src/main/resources/addresources/values-zh-rCN/music/strings.xml index 04301a0d34..9f35cc92c0 100644 --- a/patches/src/main/resources/addresources/values-zh-rCN/music/strings.xml +++ b/patches/src/main/resources/addresources/values-zh-rCN/music/strings.xml @@ -545,6 +545,8 @@ SABR 播放(模拟视频流已关闭)" 已保存 高亮显示同步的单词或音节 优先考虑可用逐字歌词。当歌词带有时间标志,随着音乐及时高亮每个单词或音节 + 单独显示背景人声 + 将写在括号中的背景人声(如“(Oh) I want you”)从演唱行中分离出来,形成独立的弱音行 隐藏已播歌词 隐藏当前播放行之上的歌词行 隐藏未播歌词 @@ -642,7 +644,7 @@ SABR 播放(模拟视频流已关闭)" 隐藏署名行 隐藏显示在歌词下方的署名行 交换翻译和音译 - 开启时,音译在歌词下方、翻译在歌词上方;关闭时,音译在歌词上方、翻译在歌词下方 + 开启时,音译在歌词下方、翻译在歌词上方;关闭时,音译在歌词上方、翻译在歌词下方。逐音节读数随之移动,从音节上方移到音节下方 翻译 罗马化 翻译提供商 diff --git a/patches/src/main/resources/addresources/values-zh-rTW/music/strings.xml b/patches/src/main/resources/addresources/values-zh-rTW/music/strings.xml index 5094345f03..43077c0a50 100644 --- a/patches/src/main/resources/addresources/values-zh-rTW/music/strings.xml +++ b/patches/src/main/resources/addresources/values-zh-rTW/music/strings.xml @@ -545,6 +545,8 @@ SABR 播放(影片串流模擬已關閉)" 已儲存 強調已同步的歌詞 優先使用已同步歌詞而非未同步歌詞 + 單獨顯示背景和聲 + 將寫在括號中的背景人聲(如「(Oh) I want you」)從主唱行分離出來,變成獨立的較弱音量行 隱藏已播放行 隱藏目前播放中那一行上方的歌詞行 隱藏未播放的歌詞 diff --git a/patches/src/main/resources/addresources/values/music/arrays.xml b/patches/src/main/resources/addresources/values/music/arrays.xml index b09ea9e6e8..6761ef8c80 100644 --- a/patches/src/main/resources/addresources/values/music/arrays.xml +++ b/patches/src/main/resources/addresources/values/music/arrays.xml @@ -169,27 +169,54 @@ @string/morphe_romanize_language_DEFAULT አማርኛ العربية + অসমীয়া + بلوچی Беларуская + Български বাংলা + བོད་སྐད + बर' + سۆرانی + डोगरी + རྫོང་ཁ Ελληνικά فارسی + गोंयची कोंकणी ગુજરાતી + עברית हिन्दी + Հայերեն 日本語 + ქართული + Қазақ тілі ಕನ್ನಡ + ភាសាខ្មែរ + 한국어 + کٲشُر + Кыргызча + ລາວ മലയാളം + Македонски + मैथिली मराठी + Монгол + ꯃꯤꯇꯩꯂꯣꯟ မြန်မာစာ नेपाली ଓଡ଼ିଆ ਪੰਜਾਬੀ + پښتو Русский संस्कृतम् + سنڌي සිංහල Српски + ܣܘܪܝܝܐ (CLDR) தமிழ் తెలుగు + ไทย ትግርኛ + ئۇيغۇرچە Українська اردو 中文 @@ -199,57 +226,72 @@ DEFAULT AM AR + AS + BAL BE + BG BN + BO + BRX + CKB + DOI + DZ EL FA + GOM GU + HE HI + HY JA + KA + KK KN + KM + KO + KS + KY + LO ML + MK + MAI MR + MN + MNI MY NE OR PA + PS RU SA + SD SI SR + SY TA TE + TH TI + UG UK UR ZH YUE - - - - @string/morphe_music_lyrics_translation_provider_google - @string/morphe_music_lyrics_translation_provider_gemini - @string/morphe_music_lyrics_translation_provider_openai_compatible - @string/morphe_music_lyrics_translation_provider_deepl - - - google - gemini - openai_compatible - deepl + + + @string/morphe_music_lyrics_rime_char_source_auto + @string/morphe_music_lyrics_rime_char_source_cantonese + @string/morphe_music_lyrics_rime_char_source_pinyin + @string/morphe_music_lyrics_rime_char_source_none - - - @string/morphe_music_lyrics_romanization_provider_google - @string/morphe_music_lyrics_romanization_provider_local - @string/morphe_music_lyrics_romanization_provider_gemini - @string/morphe_music_lyrics_romanization_provider_openai_compatible - - - google - local - gemini - openai_compatible + + AUTO + CANTONESE + PINYIN + NONE + diff --git a/patches/src/main/resources/addresources/values/music/strings.xml b/patches/src/main/resources/addresources/values/music/strings.xml index f05ccdbd96..47b1f738d4 100644 --- a/patches/src/main/resources/addresources/values/music/strings.xml +++ b/patches/src/main/resources/addresources/values/music/strings.xml @@ -507,12 +507,41 @@ To ensure scrobbling is synced during background playback, ensure that YouTube M Display Synchronisation Panel -AI endpoint + All providers disabled - built-in lyrics and embedded lyrics Reset provider order? Restore providers to the default priority order + Rime Wordlist + Rime Wordlist also needs UniDict: in the Offline Dictionary provider, tick \"Chinese and Cantonese characters (UniDict)\" and let it finish downloading first. Without it, a list that reads characters yields no reading.\n\nTick the lists Rime Wordlist should read, then tap Save to download them. A list you added is removed when you untick it.\n\nEach list is your own word-to-reading table, a .dict.yaml with lines like:\n字\tham\n典\tdin2\nThe first column is the word, the second its reading; extra columns and lines without a tab are ignored. + No Rime word list is ticked for the language above. Tap this name to tick one, or pick a language that has a list + Add + Add a Rime Wordlist + A .dict.yaml file, or a zip holding one, with lines like 字\tham and 典\tdin2. The list is fetched when you tick it and tap Save, and its licence is yours to check + Language code + Two or three letters, as in hak or wuu + Name + Shown under the reading, as in Hakka (Meixian) + Word list address + https:// address of the .dict.yaml or its zip + Characters not in a word + Automatic + Cantonese characters + Mandarin characters + No character reading + Fill in the code, name and address + The code must be two or three letters, as in hak + The address must start with https:// + The name and address cannot contain a vertical bar + %1$s already has a word table + %1$s is read by the built-in romanizer and cannot take a word list + Not used: this language already has its own reading, so untick it to remove it + Added %1$s - tick it and tap Save to download it + Word lists ready + Word lists ready (%1$d entries) + Reading word list: %1$d%% + Word list download failed - tap Save again to retry Clear stored lyrics - Delete the lyrics kept on this device + Delete the %1$s MB of lyrics kept on this device Cleared %1$s MB Get token Apple Music Media-user-token @@ -576,15 +605,23 @@ Required cookies: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPISID Show Copy button Tap to copy original lyrics as plain text; long-press to save raw lyrics source file to Downloads Show Translate button - Tap to toggle translation; long-press for translation only. Uses provider translations, Google Translate, or an OpenAI-compatible API - Show Romanize button - Tap to toggle romanization; long-press for romanization only. Uses provider romanization, Google Translate, or an OpenAI-compatible API - Local Romanization and Alignment - Romanize on the device when the provider, your model and Google Translate fail. The reading follows each word of a timed lyric; word sync must be on. Chinese, Cantonese, Japanese, Korean, Greek, Russian, Georgian, Armenian; about 120 MB - %1$s: %2$d%% - Reading %1$s: %2$d%% + Tap to toggle translation; long-press for translation only. Uses the enabled translation providers in their listed order + Show Romanize or Transliterate button + Tap to toggle romanization or transliteration; long-press for romanization or transliteration only. Uses the enabled romanization providers in their listed order + Enable transliteration + Allow non-Latin readings from the lyrics source; when off, keep only Latin letters, accented letters and tone digits + Reads each word of a timed lyric (word sync on); Korean needs no download; fourteen scripts share one CLDR download; Han characters are read by Rime Wordlist when its word table covers the language + Downloading reading dictionaries… Readings ready (%1$d entries) Download failed - turn this off and on again to retry + Offline Dictionary + Japanese (SudachiDict, ~75 MB) + Chinese and Cantonese characters (UniDict, ~8 MB) + Chinese words (pinyin-pro, ~17 MB) + Greek, Slavic Cyrillic (incl. Russian, Ukrainian, Bulgarian, Serbian, Belarusian, Macedonian), Georgian, Armenian, Hebrew, Arabic, Amharic and Syriac transliteration (CLDR, ~0.5 MB) + %1$s: %2$d%% + Reading %1$s: %2$d%% + Choose at least one dictionary Show Refresh button Tap to switch to next lyrics candidate; long-press to search lyrics by song title and artist Search lyrics @@ -595,6 +632,10 @@ Required cookies: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPISID Saved Highlight synced words or syllables Prioritize word-by-word lyrics when available. When lyrics carry word timing, highlights each word or syllable in time with the music + Shrink inactive lines + Draws every line that is not currently being sung slightly smaller, on top of dimming it + Show background vocals separately + Separates background vocals and harmony from the lead line. Moves a background vocal written in brackets, such as \"(Oh) I want you\", out of the sung line into a dimmer line of its own Hide played lyrics Hide lyrics lines above the currently playing line Hide unplayed lyrics @@ -652,10 +693,13 @@ Applies to both limits. At 1.75, four full-width characters count as 7."Translated by %s Romanized by %s Romanized by %s + Romanized by Rime (%s) Translated Translation language - Romanization language + Romanization or transliteration language + Translation target language:\n%s + Romanization or transliteration target language:\n%s Translating… Could not translate the lyrics Romanized @@ -669,7 +713,7 @@ Applies to both limits. At 1.75, four full-width characters count as 7."Refreshing Refreshed QuickLRC API key - QuickLRC transcribes or time-aligns the lyrics for the current track. A separate service, unrelated to the OpenAI-compatible endpoint above + QuickLRC transcribes or time-aligns the lyrics for the current track QuickLRC key saved. Use the align button in the lyrics search dialog, or the panel prompt when a track has no lyrics QuickLRC API key saved QuickLRC did not accept that key @@ -690,56 +734,43 @@ Applies to both limits. At 1.75, four full-width characters count as 7."Hide credit line Hide the credit line shown below the lyrics Swap translation and romanization - ON puts romanization below lyrics and translation above; OFF keeps romanization above and translation below - Translation - Romanization - Translation provider - Google (Default, Free) - Gemini API - OpenAI Compatible (Custom) - DeepL API - Romanization provider - Google (Default) - Local / Offline Dictionary - Gemini API - OpenAI Compatible (Custom) - AI Section - Only used when the provider ships no section labels such as Chorus or Verse, which your OpenAI-compatible model then detects - AI & Cloud Endpoints - Configure API keys, models, endpoints, and prompt templates for AI providers. - API endpoint URL (e.g. https://api.openai.com/v1/chat/completions) + ON puts romanization or transliteration below lyrics and translation above; OFF keeps romanization or transliteration above and translation below. A reading placed over each syllable moves with it, from above its syllable to below it + Always center lyrics + ON puts every line in the middle and ignores the source alignment, including duet sides; OFF keeps the alignment the lyrics ask for + At least one provider must stay on + Translation + Romanization or transliteration + Offline Dictionary + Rime Wordlist + Detect sections with AI + Only used when the provider ships no section labels such as Chorus or Verse, which your OpenAI-compatible model then detects. Editing the row also opens its prompt + API endpoint URL (e.g. https://api.openai.com/v1/chat/completions, https://api.anthropic.com/v1/chat/completions, https://generativelanguage.googleapis.com/v1beta/openai/chat/completions) Prompt Template Placeholders: {language}, {task}, {title}, {artist}, {count}, {lines} Prompt saved Prompt saved, but the AI endpoint is not usable yet. Configure it under AI Settings first Prompt reset - Not valid JSON. Fix it or clear the field before saving Translation & Romanization settings saved Settings reset to default Base URL must start with http:// or https:// - Test Connection - Configure Endpoints - Show All Endpoints + Settings incomplete or rejected by the provider Validating connection… - Translation Provider - Romanization Provider - Fetch Models + %1$s: Connected! (%2$s) + %1$s: Connection failed. Check the settings. + Enter the API key first Fetching models… Select a Model Failed to fetch models. Check API key or connection. - Gemini API Settings - OpenAI Compatible (Custom Endpoint) - DeepL API Settings - API key + Prompt Model Base URL + Choose a service API key (optional) - Translation prompt - Romanization prompt - AI Section prompt Extra request body (optional JSON) + Extra request headers (optional JSON) + "Quote names and values, e.g. {"api-key": "abc"}. An unquoted value containing / : or a number is rejected. An empty value removes a header." + "Quote names and values, e.g. {"temperature": 0.3}. An unquoted value containing / or : is rejected. Model and messages cannot be set here." Authentication key (Free or Pro) - Configure API keys, models, endpoints, and prompts 8.23.51 - Disable bold icons From ae2887eadb794fab06f1c0cc15993cc5ecbde8b6 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Fri, 30 Oct 2026 08:44:16 +0000 Subject: [PATCH 180/189] chore: Adjust default "what's new" dot from 3 days to 2 days --- .../extension/shared/settings/SharedYouTubeSettings.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/settings/SharedYouTubeSettings.java b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/settings/SharedYouTubeSettings.java index 145bcf2f85..250268a181 100644 --- a/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/settings/SharedYouTubeSettings.java +++ b/extensions/shared-youtube/library/src/main/java/app/morphe/extension/shared/settings/SharedYouTubeSettings.java @@ -43,7 +43,7 @@ public class SharedYouTubeSettings extends BaseSettings { public static final BooleanSetting SETTINGS_SEARCH_HISTORY = new BooleanSetting("morphe_settings_search_history", TRUE, true); public static final StringSetting SETTINGS_SEARCH_ENTRIES = new StringSetting("morphe_settings_search_entries", ""); - public static final IntegerSetting SETTINGS_NEW_INDICATOR_DAYS = new IntegerSetting("morphe_settings_new_indicator_days", 3); + public static final IntegerSetting SETTINGS_NEW_INDICATOR_DAYS = new IntegerSetting("morphe_settings_new_indicator_days", 2); // Network proxy // There are multiple endpoint calls due to the spoof video streams patch. Place the proxy settings first. @@ -152,7 +152,7 @@ public class SharedYouTubeSettings extends BaseSettings { static { SeekBarPreference.register(new SeekBarConfig(SETTINGS_NEW_INDICATOR_DAYS, - 0, 30, 1, "", 1, sf("morphe_settings_new_indicator_days_off"), null)); + 0, 14, 1, "", 1, sf("morphe_settings_new_indicator_days_off"), null)); // TODO: Eventually remove these migrations migrateOldSettingToNew(DEPRECATED_SANITIZE_URL_QUERY, SANITIZE_SHARING_LINKS); From 65e9661835ebdfa9e197cbde37657a6f95ac0c99 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Fri, 30 Oct 2026 09:00:20 +0000 Subject: [PATCH 181/189] chore: Release v2.0.0-dev.3 [skip ci] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## [2.0.0-dev.3](https://github.com/MorpheApp/morphe-patches/compare/v2.0.0-dev.2...v2.0.0-dev.3) (2026-10-30) ### ✨ New Features * **YouTube Music - Third-party lyrics:** UI changes, partial feature regression ([#4182](https://github.com/MorpheApp/morphe-patches/issues/4182)) ([b6e0a64](https://github.com/MorpheApp/morphe-patches/commit/b6e0a643677d7a975413ab8d1566745cac15127c)) --- CHANGELOG.md | 6 ++++++ README.md | 2 +- gradle.properties | 2 +- patches-bundle.json | 8 ++++---- patches-list.json | 2 +- 5 files changed, 13 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 41671cf7c2..9b9d5cfeeb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## [2.0.0-dev.3](https://github.com/MorpheApp/morphe-patches/compare/v2.0.0-dev.2...v2.0.0-dev.3) (2026-10-30) + +### ✨ New Features + +* **YouTube Music - Third-party lyrics:** UI changes, partial feature regression ([#4182](https://github.com/MorpheApp/morphe-patches/issues/4182)) ([b6e0a64](https://github.com/MorpheApp/morphe-patches/commit/b6e0a643677d7a975413ab8d1566745cac15127c)) + ## [2.0.0-dev.2](https://github.com/MorpheApp/morphe-patches/compare/v2.0.0-dev.1...v2.0.0-dev.2) (2026-10-30) ### 🐛 Bug Fixes diff --git a/README.md b/README.md index e6eaead70e..58f649d040 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ All modifications made by Morphe, along with their dates, can be found in the Gi ## 🩹 Patches list -> **[v2.0.0-dev.2](https://github.com/MorpheApp/morphe-patches/releases/tag/v2.0.0-dev.2)**  •  `dev`  •  194 patches total +> **[v2.0.0-dev.3](https://github.com/MorpheApp/morphe-patches/releases/tag/v2.0.0-dev.3)**  •  `dev`  •  194 patches total

    📦 YouTube  •  109 patches
    diff --git a/gradle.properties b/gradle.properties index 312b59b194..ee3f7f20b5 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,4 +5,4 @@ android.useAndroidX = true # Extensions are Java only. Prevents AGP from adding Kotlin compilation and the Kotlin stdlib. android.builtInKotlin = false kotlin.code.style = official -version = 2.0.0-dev.2 +version = 2.0.0-dev.3 diff --git a/patches-bundle.json b/patches-bundle.json index c434802468..4b454d684e 100644 --- a/patches-bundle.json +++ b/patches-bundle.json @@ -1,8 +1,8 @@ { - "created_at": "2026-10-30T08:03:57", - "description": "## [2.0.0-dev.2](https://github.com/MorpheApp/morphe-patches/compare/v2.0.0-dev.1...v2.0.0-dev.2) (2026-10-30)\n\n### 🐛 Bug Fixes\n\n* **YouTube - Captions:** Stabilize automatic captions and restore translation languages ([#4149](https://github.com/MorpheApp/morphe-patches/issues/4149)) ([ba1ac73](https://github.com/MorpheApp/morphe-patches/commit/ba1ac73757dc8935c60434de8d0362b311d73442))\n* **YouTube - Feed video actions:** Watch later button is shown above the video of posts, and is missing after the app language is changed ([#4172](https://github.com/MorpheApp/morphe-patches/issues/4172)) ([54b7371](https://github.com/MorpheApp/morphe-patches/commit/54b7371f5252dfcd287d2360e37cdacefb5ad3c2))\n* **YouTube - Navigation bar:** Navigation bar transluency not working ([#4156](https://github.com/MorpheApp/morphe-patches/issues/4156)) ([82103d3](https://github.com/MorpheApp/morphe-patches/commit/82103d36cacc93eeaed307f75975c492fb610b24))\n* **YouTube - Return YouTube Dislike:** Feed thumbnail like count blinks, moves, and is missing after returning to the feed ([#4176](https://github.com/MorpheApp/morphe-patches/issues/4176)) ([4dc345b](https://github.com/MorpheApp/morphe-patches/commit/4dc345b93f4007d42c124dfb021fb9cbeb221575))\n\n### ✨ New Features\n\n* **YouTube:** Add `Translate posts` patch ([#4163](https://github.com/MorpheApp/morphe-patches/issues/4163)) ([5d571a0](https://github.com/MorpheApp/morphe-patches/commit/5d571a07c37c0231b46651dd46fb2cbeef24713a))\n* **YouTube:** Apply offline SponsorBlock segments to playlist downloads ([#4180](https://github.com/MorpheApp/morphe-patches/issues/4180)) ([d6983da](https://github.com/MorpheApp/morphe-patches/commit/d6983da6ed69327d68ce3b188a8626ef99255d06))\n* **YouTube:** Show Morphe messages in snackbar instead of a toast ([#4148](https://github.com/MorpheApp/morphe-patches/issues/4148)) ([035718b](https://github.com/MorpheApp/morphe-patches/commit/035718bf911540f36ba768e5db78cae4ac4caf23))", - "download_url": "https://github.com/MorpheApp/morphe-patches/releases/download/v2.0.0-dev.2/patches-2.0.0-dev.2.mpp", + "created_at": "2026-10-30T08:57:20", + "description": "## [2.0.0-dev.3](https://github.com/MorpheApp/morphe-patches/compare/v2.0.0-dev.2...v2.0.0-dev.3) (2026-10-30)\n\n### ✨ New Features\n\n* **YouTube Music - Third-party lyrics:** UI changes, partial feature regression ([#4182](https://github.com/MorpheApp/morphe-patches/issues/4182)) ([b6e0a64](https://github.com/MorpheApp/morphe-patches/commit/b6e0a643677d7a975413ab8d1566745cac15127c))", + "download_url": "https://github.com/MorpheApp/morphe-patches/releases/download/v2.0.0-dev.3/patches-2.0.0-dev.3.mpp", "signature_download_url": "", - "version": "2.0.0-dev.2", + "version": "2.0.0-dev.3", "public_key": "g8Ys9leTu2yeixojeR35HrNkK6eOSLpA6nNIizRutqI=" } diff --git a/patches-list.json b/patches-list.json index 13b014067b..3fa2afdca0 100644 --- a/patches-list.json +++ b/patches-list.json @@ -1,6 +1,6 @@ { "NOTE": "Do NOT manually edit this file. This file is automatically updated when semantic release (release.yml) runs. Manually editing this file can break your releases and break third party tools that use this file.", - "version": "2.0.0-dev.2", + "version": "2.0.0-dev.3", "patches": [ { "name": "AI channel filter", From 60daa80ebabc420946991af736cc74ab8247fee7 Mon Sep 17 00:00:00 2001 From: 0xrxL <127248639+0xrxL@users.noreply.github.com> Date: Fri, 30 Oct 2026 10:29:42 +0100 Subject: [PATCH 182/189] fix(YouTube - Navigation bar): Black band below pages and the Shorts player when the window is not drawn behind the system navigation buttons --- .../youtube/patches/HideStatusBarPatch.java | 165 ++++++++++++------ .../youtube/patches/NavigationBarPatch.java | 147 ++++++++++++---- .../extension/youtube/settings/Settings.java | 1 + .../layout/buttons/navigation/Fingerprints.kt | 12 +- .../buttons/navigation/NavigationBarPatch.kt | 57 +++--- .../layout/statusbar/HideStatusBarPatch.kt | 4 + 6 files changed, 271 insertions(+), 115 deletions(-) diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/HideStatusBarPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/HideStatusBarPatch.java index 0c13204fbc..508ad1fbc4 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/HideStatusBarPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/HideStatusBarPatch.java @@ -36,6 +36,28 @@ @SuppressWarnings({"unused", "deprecation"}) public final class HideStatusBarPatch { + /** + * The settings are read once, so the edge to edge top bar and what follows it stay the same + * until the app restarts, also if a setting is changed and the restart is postponed. + */ + private static final boolean HIDE_STATUS_BAR = Settings.HIDE_STATUS_BAR.get(); + + private static final boolean FIX_STATUS_BAR_LAYOUT = Settings.FIX_STATUS_BAR_LAYOUT.get(); + + /** + * If the edge to edge top bar feature flag is turned off for the whole session even if it is + * not read yet: with the status bar layout fix, and the status bar hidden or the window laid out + * below the status bar at a previous app start. + */ + private static final boolean EDGE_TO_EDGE_TOP_BAR_TURNED_OFF = FIX_STATUS_BAR_LAYOUT + && (HIDE_STATUS_BAR || Settings.WINDOW_BELOW_STATUS_BAR.get()); + + /** + * If the window drawn edge to edge ended above the system navigation buttons at a previous app + * start, see {@link #watchWindowPosition(Activity)}. + */ + private static final boolean WINDOW_ABOVE_NAVIGATION_BAR = Settings.WINDOW_ABOVE_NAVIGATION_BAR.get(); + /** * Activities whose status bar is hidden, so the status bar of an activity is hidden once. */ @@ -131,16 +153,6 @@ private static boolean isPatchIncluded() { return false; // Modified during patching. } - /** - * @return If the edge to edge top bar feature flag is turned off for the whole session - * even if it is not read yet: with the status bar layout fix, and the status bar hidden - * or the window laid out below the status bar. - */ - private static boolean isEdgeToEdgeTopBarTurnedOff() { - return Settings.FIX_STATUS_BAR_LAYOUT.get() - && (Settings.HIDE_STATUS_BAR.get() || Settings.WINDOW_BELOW_STATUS_BAR.get()); - } - /** * @return If the window is drawn behind the system bars, or null if the app did not read * the edge to edge top bar feature flag yet and its value is not known. @@ -148,12 +160,20 @@ private static boolean isEdgeToEdgeTopBarTurnedOff() { @Nullable static Boolean isEdgeToEdgeTopBar() { Boolean edgeToEdge = edgeToEdgeTopBarUsed; - if (edgeToEdge == null && isPatchIncluded() && isEdgeToEdgeTopBarTurnedOff()) { + if (edgeToEdge == null && isPatchIncluded() && EDGE_TO_EDGE_TOP_BAR_TURNED_OFF) { return false; } return edgeToEdge; } + /** + * @return If the window is not drawn behind the system navigation buttons even with the edge + * to edge top bar, as found at a previous app start. + */ + static boolean isWindowAboveNavigationBar() { + return isPatchIncluded() && WINDOW_ABOVE_NAVIGATION_BAR; + } + /** * Injection point. *

    @@ -171,7 +191,7 @@ static Boolean isEdgeToEdgeTopBar() { * see {@link #watchWindowPosition(Activity)}. */ public static boolean useEdgeToEdgeTopBar(boolean original) { - if (!Settings.FIX_STATUS_BAR_LAYOUT.get()) { + if (!FIX_STATUS_BAR_LAYOUT) { if (edgeToEdgeTopBarUsed == null) { edgeToEdgeTopBarUsed = original; } @@ -183,11 +203,11 @@ public static boolean useEdgeToEdgeTopBar(boolean original) { synchronized (HideStatusBarPatch.class) { value = edgeToEdgeTopBar; if (value == null) { - final boolean used = original && !Settings.HIDE_STATUS_BAR.get() - && !Settings.WINDOW_BELOW_STATUS_BAR.get(); + final boolean used = original && !EDGE_TO_EDGE_TOP_BAR_TURNED_OFF; value = used; edgeToEdgeTopBar = value; Logger.printDebug(() -> "Edge to edge top bar: " + used + " (original: " + original + + ", hide status bar: " + HIDE_STATUS_BAR + ", window below status bar: " + Settings.WINDOW_BELOW_STATUS_BAR.get() + ")"); } } @@ -208,13 +228,13 @@ public static boolean useEdgeToEdgeTopBar(boolean original) { * if the status bar is hidden. */ public static boolean useStatusBarScrollColor(boolean original) { - if (!Settings.FIX_STATUS_BAR_LAYOUT.get()) { + if (!FIX_STATUS_BAR_LAYOUT) { return original; } Boolean edgeToEdge = edgeToEdgeTopBar; if (edgeToEdge == null) { - edgeToEdge = !isEdgeToEdgeTopBarTurnedOff(); + edgeToEdge = !EDGE_TO_EDGE_TOP_BAR_TURNED_OFF; } return original && edgeToEdge; } @@ -225,9 +245,20 @@ public static boolean useStatusBarScrollColor(boolean original) { * Called when the main activity is created. */ public static void initialize(Activity activity) { - registerWindowPositionWatch(activity); + // The window position is watched once the activity is started, see watchWindowPosition(). + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + try { + if (!windowPositionLifecycleCallbacksRegistered) { + windowPositionLifecycleCallbacksRegistered = true; + activity.getApplication().registerActivityLifecycleCallbacks(WINDOW_POSITION_LIFECYCLE_CALLBACKS); + } + windowPositionActivities.add(activity); + } catch (Exception ex) { + Logger.printException(() -> "initialize window position failure", ex); + } + } - if (!Settings.HIDE_STATUS_BAR.get()) { + if (!HIDE_STATUS_BAR) { return; } @@ -254,22 +285,6 @@ public static void initialize(Activity activity) { } } - private static void registerWindowPositionWatch(Activity activity) { - if (!Settings.FIX_STATUS_BAR_LAYOUT.get() || Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { - return; - } - - try { - if (!windowPositionLifecycleCallbacksRegistered) { - windowPositionLifecycleCallbacksRegistered = true; - activity.getApplication().registerActivityLifecycleCallbacks(WINDOW_POSITION_LIFECYCLE_CALLBACKS); - } - windowPositionActivities.add(activity); - } catch (Exception ex) { - Logger.printException(() -> "registerWindowPositionWatch failure", ex); - } - } - /** * Some devices keep the window out of the status bar area, for example Samsung devices with * the app not allowed to use the camera cutout area. The edge to edge top bar still makes room @@ -279,47 +294,85 @@ private static void registerWindowPositionWatch(Activity activity) { * The flag is read before the window is laid out, so the position is saved and the flag is * turned off from the next app start. Only the full screen window in portrait with the status bar * shown is checked, where the status bar is above the window. + *

    + * The same way, a window drawn edge to edge is expected to end behind the system navigation + * buttons, which the navigation bar covering them relies on. Some devices and Android versions + * keep it above them, and the room for them is then added twice, see + * {@link NavigationBarPatch#useTranslucentNavigationBar(boolean)}. The bottom of the window is + * saved, and the flags of the translucent navigation bar are turned off from the next app start. */ @RequiresApi(api = Build.VERSION_CODES.R) private static void watchWindowPosition(Activity activity) { View decorView = activity.getWindow().getDecorView(); - // Width and height of the window last checked, so the position is only read when they change. - final int[] lastSize = {-1, -1}; + // Width, height and status bar visibility of the window last checked, + // so the position is only read when they change. + final int[] lastSize = {-1, -1, -1}; final int[] location = new int[2]; decorView.getViewTreeObserver().addOnGlobalLayoutListener(() -> { try { - // With the status bar hidden, the window can be laid out below the display cutout instead. - if (activity.isInMultiWindowMode() || activity.isInPictureInPictureMode() + WindowInsets insets = decorView.getRootWindowInsets(); + // The keyboard can resize the window on some devices. + if (insets == null || insets.isVisible(WindowInsets.Type.ime()) + || activity.isInMultiWindowMode() || activity.isInPictureInPictureMode() || activity.getResources().getConfiguration().orientation - != Configuration.ORIENTATION_PORTRAIT - || !isStatusBarVisible(decorView)) { + != Configuration.ORIENTATION_PORTRAIT) { return; } final int width = decorView.getWidth(); final int height = decorView.getHeight(); - if ((width == lastSize[0] && height == lastSize[1]) || width == 0 || height == 0) { + final boolean statusBarVisible = insets.isVisible(WindowInsets.Type.statusBars()); + final int statusBar = statusBarVisible ? 1 : 0; + if ((width == lastSize[0] && height == lastSize[1] && statusBar == lastSize[2]) + || width == 0 || height == 0) { return; } lastSize[0] = width; lastSize[1] = height; + lastSize[2] = statusBar; decorView.getLocationOnScreen(location); final int windowTop = location[1]; - final boolean belowStatusBar = windowTop > 0; - Logger.printDebug(() -> { - WindowInsets insets = decorView.getRootWindowInsets(); - return "Window top: " + windowTop + " height: " + height + (insets == null ? "" : - " status bar inset: " + insets.getInsets(WindowInsets.Type.statusBars()).top - + " stable: " + insets.getInsetsIgnoringVisibility(WindowInsets.Type.statusBars()).top - + " cutout: " + insets.getInsets(WindowInsets.Type.displayCutout()).top); - }); + final int windowBottom = windowTop + height; + final int displayHeight = activity.getWindowManager().getMaximumWindowMetrics().getBounds().height(); + Logger.printDebug(() -> "Window top: " + windowTop + " bottom: " + windowBottom + + " display height: " + displayHeight + + " status bar inset: " + insets.getInsets(WindowInsets.Type.statusBars()).top + + " stable: " + insets.getInsetsIgnoringVisibility(WindowInsets.Type.statusBars()).top + + " cutout: " + insets.getInsets(WindowInsets.Type.displayCutout()).top + + " navigation bar inset: " + insets.getInsets(WindowInsets.Type.navigationBars()).bottom); + + // Without the edge to edge top bar the app itself can lay the window out below + // the status bar (it uses the default display cutout mode on Android 14 and lower) + // and above the system navigation buttons, so only a window that was drawn edge to edge + // tells that the device keeps it there. Otherwise the flags would stay off for good + // after one app start without them. + final boolean edgeToEdge = Boolean.TRUE.equals(edgeToEdgeTopBarUsed); - if (belowStatusBar != Settings.WINDOW_BELOW_STATUS_BAR.get()) { - Logger.printDebug(() -> "Window below status bar: " + belowStatusBar - + ", applied from the next app start"); - Settings.WINDOW_BELOW_STATUS_BAR.save(belowStatusBar); + // With the status bar hidden, the window can be laid out below the display cutout instead. + if (FIX_STATUS_BAR_LAYOUT && statusBarVisible) { + final boolean belowStatusBar = windowTop > 0; + if ((!belowStatusBar || edgeToEdge) + && belowStatusBar != Settings.WINDOW_BELOW_STATUS_BAR.get()) { + Logger.printDebug(() -> "Window below status bar: " + belowStatusBar + + ", applied from the next app start"); + Settings.WINDOW_BELOW_STATUS_BAR.save(belowStatusBar); + } + } + + // Only with the system navigation buttons at the bottom of the screen. The navigation bar + // covering them can change how the window is laid out, so only a window laid out with it + // tells that the device keeps the window above them. + if (edgeToEdge && insets.isVisible(WindowInsets.Type.navigationBars()) + && insets.getInsetsIgnoringVisibility(WindowInsets.Type.navigationBars()).bottom > 0) { + final boolean aboveNavigationBar = windowBottom < displayHeight; + if ((!aboveNavigationBar || NavigationBarPatch.isNavigationBarCoveringSystemButtons()) + && aboveNavigationBar != Settings.WINDOW_ABOVE_NAVIGATION_BAR.get()) { + Logger.printDebug(() -> "Window above navigation bar: " + aboveNavigationBar + + ", applied from the next app start"); + Settings.WINDOW_ABOVE_NAVIGATION_BAR.save(aboveNavigationBar); + } } } catch (Exception ex) { Logger.printException(() -> "watchWindowPosition failure", ex); @@ -344,7 +397,7 @@ private static void hideStatusBar(Activity activity) { * The submenu toolbar is laid out below the display cutout by the submenu itself. */ public static void hideStatusBar(Dialog dialog) { - if (!Settings.HIDE_STATUS_BAR.get()) { + if (!HIDE_STATUS_BAR) { return; } @@ -381,7 +434,7 @@ private static void hideStatusBar(Window window) { * so when the status bar is hidden it covers the search bar and channel page top bar. */ public static void hideStatusBarBackground(View view) { - if (Settings.HIDE_STATUS_BAR.get()) { + if (HIDE_STATUS_BAR) { view.setVisibility(View.INVISIBLE); } } @@ -396,7 +449,7 @@ public static void hideStatusBarBackground(View view) { * are still laid out below a front camera hole, the same as with the status bar inset. */ public static void setSystemWindowInsetTop(Rect systemWindowInsets, @Nullable View view) { - if (view == null || !Settings.HIDE_STATUS_BAR.get()) { + if (view == null || !HIDE_STATUS_BAR) { return; } diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/NavigationBarPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/NavigationBarPatch.java index a2b483214d..db0d8742dc 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/NavigationBarPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/NavigationBarPatch.java @@ -104,7 +104,13 @@ public enum NavigationBarStyle { OPAQUE } - private static final NavigationBarStyle NAVIGATION_BAR_STYLE = Settings.NAVIGATION_BAR_STYLE.get(); + /** + * The flags of the translucent navigation bar are also overridden without this patch, + * see {@link #isNavigationBarBehindSystemButtons(boolean)}. They are then left to the account. + */ + private static final NavigationBarStyle NAVIGATION_BAR_STYLE = isPatchIncluded() + ? Settings.NAVIGATION_BAR_STYLE.get() + : NavigationBarStyle.DEFAULT; private static final boolean NARROW_NAVIGATION_BUTTONS = Settings.NARROW_NAVIGATION_BUTTONS.get(); @@ -224,45 +230,124 @@ public static void hideNavigationBar(View view) { } /** - * Injection point. - *

    + * Value of the navigation bar covering the system navigation buttons flag first read by the app, + * or null if not read yet. + */ + @Nullable + private static volatile Boolean accountCoversSystemButtons; + + /** + * Value of the feed behind the navigation bar flag first read by the app, or null if not read yet. + */ + @Nullable + private static volatile Boolean accountFeedBehindNavigationBar; + + /** * The translucent navigation bar comes from feature flags that not every account gets, so they - * are turned on to make the bar look the same on every account. With them, the navigation bar - * and the pages make room for the system navigation buttons, which is only right when - * the window is drawn behind them. That comes from the edge to edge top bar feature flag, - * so without it the room is added twice: the navigation bar is raised and an empty bar is left - * at the bottom (...). + * are turned on to make the bar look the same on every account. With the navigation bar covering + * the system navigation buttons and the feed laid out behind it, the navigation bar and the pages + * make room for the system navigation buttons, which is only right when the window is drawn behind + * them. That comes from the edge to edge top bar feature flag, so without it the room is added twice: + * the navigation bar is raised and an empty bar is left at the bottom + * (...), or the pages and + * the Shorts player end the height of the navigation bar above it + * (..., + * ...). *

    * The app draws the window behind the system navigation buttons only on Android 12 and later, * so on older versions the flags are always turned off, or the room is added twice * (...). *

    - * With {@link NavigationBarStyle#DEFAULT} the flags are left to the account. + * With {@link NavigationBarStyle#DEFAULT} the flags are left to the account, but they are still + * turned off without the edge to edge layout, which the Hide status bar patch can turn off + * for an account that has both. + * + * @param original The value the account first got, so all the classes that read the flag + * use the same value even if the account value changes while the app starts, + * the same as the edge to edge top bar flag. */ - public static boolean useTranslucentNavigationBar(boolean original) { - if (NAVIGATION_BAR_STYLE == NavigationBarStyle.DEFAULT) { - return original; - } - - boolean translucent = original; + private static boolean isNavigationBarBehindSystemButtons(boolean original) { try { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { - translucent = false; - } else { - Boolean edgeToEdge = HideStatusBarPatch.isEdgeToEdgeTopBar(); - // Null if not known yet, or the Hide status bar patch is not included. - if (edgeToEdge != null) { - translucent = edgeToEdge; - } + // Some devices keep the window above the system navigation buttons also with the edge + // to edge layout, as found at a previous app start by the Hide status bar patch. + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S || HideStatusBarPatch.isWindowAboveNavigationBar()) { + return false; + } + + Boolean edgeToEdge = HideStatusBarPatch.isEdgeToEdgeTopBar(); + // Null if not known yet, or the Hide status bar patch is not included. + // The edge to edge layout is then the one of the account, which the flags of the account follow. + if (edgeToEdge == null) { + return original; } + + return edgeToEdge && (original || NAVIGATION_BAR_STYLE != NavigationBarStyle.DEFAULT); } catch (Exception ex) { - Logger.printException(() -> "useTranslucentNavigationBar failure", ex); + Logger.printException(() -> "isNavigationBarBehindSystemButtons failure", ex); + return original; + } + } + + /** + * Injection point. + *

    + * The navigation bar covers the system navigation buttons, see + * {@link #isNavigationBarBehindSystemButtons(boolean)}. + */ + public static boolean useTranslucentNavigationBar(boolean original) { + Boolean accountValue = accountCoversSystemButtons; + final boolean firstRead = accountValue == null; + if (firstRead) { + accountValue = original; + accountCoversSystemButtons = accountValue; } - translucentNavigationBarForced = translucent; + final boolean translucent = isNavigationBarBehindSystemButtons(accountValue); + if (firstRead) { + Logger.printDebug(() -> "Navigation bar covers system buttons: " + translucent + " (account: " + original + + ", edge to edge: " + HideStatusBarPatch.isEdgeToEdgeTopBar() + + ", window above navigation bar: " + HideStatusBarPatch.isWindowAboveNavigationBar() + + ", style: " + NAVIGATION_BAR_STYLE + ")"); + } + translucentNavigationBarForced = NAVIGATION_BAR_STYLE != NavigationBarStyle.DEFAULT && translucent; + navigationBarCoversSystemButtons = translucent; return translucent; } + /** + * If the app was last told the navigation bar covers the system navigation buttons, + * with any navigation bar style. + */ + private static volatile boolean navigationBarCoversSystemButtons; + + static boolean isNavigationBarCoveringSystemButtons() { + return navigationBarCoversSystemButtons; + } + + /** + * Injection point. + *

    + * The feed is laid out behind the navigation bar. It follows the navigation bar covering + * the system navigation buttons: some pages add the navigation bar height below their content + * when the navigation bar covers the system navigation buttons, as they are expected to be + * laid out behind it, and the list padding for the navigation bar is only set with the edge + * to edge layout. See {@link #isNavigationBarBehindSystemButtons(boolean)}. + */ + public static boolean useFeedBehindNavigationBar(boolean original) { + Boolean accountValue = accountFeedBehindNavigationBar; + final boolean firstRead = accountValue == null; + if (firstRead) { + accountValue = original; + accountFeedBehindNavigationBar = accountValue; + } + + final boolean feedBehind = isNavigationBarBehindSystemButtons(accountValue); + if (firstRead) { + Logger.printDebug(() -> "Feed behind navigation bar: " + feedBehind + " (account: " + original + ")"); + } + return feedBehind; + } + /** * If {@link #useTranslucentNavigationBar(boolean)} turned the translucent navigation bar on. * The fixes for what its feature flags break are only needed then, and stay off if the flags @@ -292,11 +377,11 @@ public static boolean useFixedBottomBarRoom(boolean original) { /** * Injection point. *

    - * The blurred, translucent background of the navigation bar and the feed laid out behind it - * only change how the bar looks, not the room left for the system navigation buttons, so they - * are turned on from Android 12 also when the window is not drawn edge to edge. With - * {@link NavigationBarStyle#OPAQUE}, they follow {@link #useTranslucentNavigationBar(boolean)} - * instead. With {@link NavigationBarStyle#DEFAULT} they are left to the account. + * The blurred, translucent background of the navigation bar only changes how the bar looks, + * not the room left for the system navigation buttons, so it is turned on from Android 12 also + * when the window is not drawn edge to edge. With {@link NavigationBarStyle#OPAQUE}, it follows + * {@link #isNavigationBarBehindSystemButtons(boolean)} instead. With {@link NavigationBarStyle#DEFAULT} + * it is left to the account. */ public static boolean useTranslucentNavigationBarBackground(boolean original) { if (NAVIGATION_BAR_STYLE == NavigationBarStyle.DEFAULT) { @@ -307,7 +392,7 @@ public static boolean useTranslucentNavigationBarBackground(boolean original) { if (NAVIGATION_BAR_STYLE != NavigationBarStyle.OPAQUE && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { translucent = true; } else { - translucent = useTranslucentNavigationBar(original); + translucent = isNavigationBarBehindSystemButtons(original); } translucentNavigationBarBackgroundForced = translucent; diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/Settings.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/Settings.java index 8ef0dcc997..a5ab558e25 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/Settings.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/settings/Settings.java @@ -573,6 +573,7 @@ public class Settings extends SharedYouTubeSettings { public static final BooleanSetting HIDE_STATUS_BAR = new BooleanSetting("morphe_hide_status_bar", FALSE, true); public static final BooleanSetting FIX_STATUS_BAR_LAYOUT = new BooleanSetting("morphe_fix_status_bar_layout", TRUE, true); public static final BooleanSetting WINDOW_BELOW_STATUS_BAR = new BooleanSetting("morphe_window_below_status_bar", FALSE, false, false); + public static final BooleanSetting WINDOW_ABOVE_NAVIGATION_BAR = new BooleanSetting("morphe_window_above_navigation_bar", FALSE, false, false); public static final EnumSetting FORCE_ANIMATIONS = new EnumSetting<>("morphe_force_animations", AnimationsMode.DEFAULT); // Custom filter diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/buttons/navigation/Fingerprints.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/buttons/navigation/Fingerprints.kt index 51fed063ce..bbeb710cb8 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/buttons/navigation/Fingerprints.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/buttons/navigation/Fingerprints.kt @@ -149,15 +149,21 @@ internal object CommentReplyPaddingFeatureFlagFingerprint : Fingerprint( internal val TRANSLUCENT_NAVIGATION_BAR_FEATURE_FLAGS = listOf( 45630927L, // Blurred, translucent background of the navigation bar. NAVIGATION_BAR_COVERS_SYSTEM_BUTTONS_FLAG, - 45631257L // Feed laid out behind the navigation bar instead of above it. + FEED_BEHIND_NAVIGATION_BAR_FLAG ) /** - * Navigation bar also covers the system navigation buttons, the only one of - * [TRANSLUCENT_NAVIGATION_BAR_FEATURE_FLAGS] that changes the room left for them. + * Navigation bar also covers the system navigation buttons, which adds the room for them + * to the navigation bar height. */ internal const val NAVIGATION_BAR_COVERS_SYSTEM_BUTTONS_FLAG = 45632194L +/** + * Feed laid out behind the navigation bar instead of above it. The page then ends at the bottom + * of the screen, and its list gets the navigation bar height as bottom padding. + */ +internal const val FEED_BEHIND_NAVIGATION_BAR_FLAG = 45631257L + internal object TranslucentNavigationBarFeatureFlagsFingerprint : Fingerprint( filters = listOf( anyInstruction(*TRANSLUCENT_NAVIGATION_BAR_FEATURE_FLAGS.map { literal(it) }.toTypedArray()) diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/buttons/navigation/NavigationBarPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/buttons/navigation/NavigationBarPatch.kt index 2a01f7152e..93d2401934 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/buttons/navigation/NavigationBarPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/buttons/navigation/NavigationBarPatch.kt @@ -128,6 +128,34 @@ private val navigationBarResourcePatch = resourcePatch { } } +/** + * The feature flags of the translucent navigation bar. The navigation bar covering the system navigation + * buttons and the feed laid out behind it are only right with the window drawn behind the system + * navigation buttons, which comes from the edge to edge feature flag. The Hide status bar patch can turn + * that flag off, so the flags are overridden also without the Navigation bar patch, to keep them off then. + */ +internal val translucentNavigationBarFeatureFlagsPatch = bytecodePatch { + dependsOn(sharedExtensionPatch) + + execute { + TranslucentNavigationBarFeatureFlagsFingerprint.matchAll().forEach { match -> + match.method.apply { + TRANSLUCENT_NAVIGATION_BAR_FEATURE_FLAGS.forEach { flag -> + if (containsLiteralInstruction(flag)) { + val override = when (flag) { + NAVIGATION_BAR_COVERS_SYSTEM_BUTTONS_FLAG -> "useTranslucentNavigationBar" + FEED_BEHIND_NAVIGATION_BAR_FLAG -> "useFeedBehindNavigationBar" + else -> "useTranslucentNavigationBarBackground" + } + // Overrides every read of the flag in the method. + insertLiteralOverride(flag, "$EXTENSION_CLASS->$override(Z)Z") + } + } + } + } + } +} + val navigationBarPatch = bytecodePatch( name = "Navigation bar", description = "Adds options to hide and change the bottom navigation bar (such as the Shorts button) " @@ -142,6 +170,7 @@ val navigationBarPatch = bytecodePatch( toolBarHookPatch, fixProtoLibraryPatch, navigationBarResourcePatch, + translucentNavigationBarFeatureFlagsPatch, ) compatibleWith(COMPATIBILITY_YOUTUBE) @@ -219,31 +248,9 @@ val navigationBarPatch = bytecodePatch( // region Translucent navigation bar - // Turning on the feature flags of the translucent navigation bar, and the fixes for what - // they break. Every fix checks that the flags are turned on by this patch, so the fixes do - // nothing if the flags are left to the account, as with the default navigation bar style. - - // The translucent navigation bar comes from three feature flags that not every account - // gets, so they are turned on to make the bar look the same on every account, and the opaque - // style below paints over the same translucent bar everywhere. The flag that makes the bar - // cover the system navigation buttons is turned on only when the window is drawn edge to - // edge. The blur and the feed behind the bar do not change the room left for the system - // navigation buttons, so they are also turned on without it. - TranslucentNavigationBarFeatureFlagsFingerprint.matchAll().forEach { match -> - match.method.apply { - TRANSLUCENT_NAVIGATION_BAR_FEATURE_FLAGS.forEach { flag -> - if (containsLiteralInstruction(flag)) { - val override = if (flag == NAVIGATION_BAR_COVERS_SYSTEM_BUTTONS_FLAG) { - "useTranslucentNavigationBar" - } else { - "useTranslucentNavigationBarBackground" - } - // Overrides every read of the flag in the method. - insertLiteralOverride(flag, "$EXTENSION_CLASS->$override(Z)Z") - } - } - } - } + // Turning on the feature flags of the translucent navigation bar (translucentNavigationBarFeatureFlagsPatch), + // and the fixes for what they break. Every fix checks that the flags are turned on by this patch, + // so the fixes do nothing if the flags are left to the account, as with the default navigation bar style. // The blurred background of the translucent navigation bar is drawn only if the canvas is // hardware accelerated. The transparent and opaque styles leave it out, as it can crash diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/statusbar/HideStatusBarPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/statusbar/HideStatusBarPatch.kt index f3f202c205..82ceaa0cfd 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/statusbar/HideStatusBarPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/statusbar/HideStatusBarPatch.kt @@ -11,6 +11,7 @@ import app.morphe.patcher.extensions.InstructionExtensions.addInstruction import app.morphe.patcher.extensions.InstructionExtensions.getInstruction import app.morphe.patcher.patch.bytecodePatch import app.morphe.patches.shared.misc.settings.preference.SwitchPreference +import app.morphe.patches.youtube.layout.buttons.navigation.translucentNavigationBarFeatureFlagsPatch import app.morphe.patches.youtube.misc.extension.sharedExtensionPatch import app.morphe.patches.youtube.misc.playertype.playerTypeHookPatch import app.morphe.patches.youtube.misc.settings.PreferenceScreen @@ -33,6 +34,9 @@ val hideStatusBarPatch = bytecodePatch( sharedExtensionPatch, settingsPatch, playerTypeHookPatch, + // The flags of the translucent navigation bar must be turned off with the edge to edge + // top bar, also if the Navigation bar patch is not included. + translucentNavigationBarFeatureFlagsPatch, ) compatibleWith(COMPATIBILITY_YOUTUBE) From b3735342ce3c686f25c76d5e664703f39767fc76 Mon Sep 17 00:00:00 2001 From: 0xrxL <127248639+0xrxL@users.noreply.github.com> Date: Fri, 30 Oct 2026 10:29:42 +0100 Subject: [PATCH 183/189] fix(YouTube - Hide layout components): Community posts are shown below a video opened from the search results, a channel or a playlist --- .../components/LayoutComponentsFilter.java | 54 ++++++---- .../channelsearch/ChannelSearchPatch.kt | 70 ++----------- .../interaction/channelsearch/Fingerprints.kt | 41 -------- .../hide/general/HideLayoutComponentsPatch.kt | 9 ++ .../youtube/misc/navigation/Fingerprints.kt | 40 ++++++++ .../misc/navigation/NavigationBarHookPatch.kt | 98 +++++++++++++++++++ 6 files changed, 188 insertions(+), 124 deletions(-) diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/components/LayoutComponentsFilter.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/components/LayoutComponentsFilter.java index b26b4c241d..7b4e3b260c 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/components/LayoutComponentsFilter.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/components/LayoutComponentsFilter.java @@ -46,7 +46,6 @@ import app.morphe.extension.youtube.patches.VersionCheckPatch; import app.morphe.extension.youtube.patches.utils.ProtoNode; import app.morphe.extension.youtube.settings.Settings; -import app.morphe.extension.youtube.shared.EngagementPanel; import app.morphe.extension.youtube.shared.NavigationBar; import app.morphe.extension.youtube.shared.NavigationBar.NavigationButton; import app.morphe.extension.youtube.shared.PlayerType; @@ -67,14 +66,6 @@ public final class LayoutComponentsFilter extends Filter { "channel_profile" ); - /** - * The comments panel of videos. Newer app versions use the first id. - */ - private static final List COMMENTS_PANEL_IDS = List.of( - "comment-item-section", - "engagement-panel-comments-section" - ); - private static final List channelTabFilterStrings = Utils.getFilterStrings(Settings.HIDE_CHANNEL_TAB_FILTER_STRINGS); private static final List flyoutMenuFilterStrings = Utils.getFilterStrings(Settings.HIDE_FEED_FLYOUT_MENU_FILTER_STRINGS); @@ -83,6 +74,32 @@ public final class LayoutComponentsFilter extends Filter { @Nullable private static List originalChannelTabs; + /** + * Browse id of the community page of a channel, which shows only community posts. + */ + private static final String COMMUNITY_PAGE_BROWSE_ID = "FEcommunity_page"; + + /** + * Browse id of the page that is shown, or empty for the search feed. + */ + private static volatile String browseId = ""; + + /** + * Injection point. + */ + public static void setBrowseId(@Nullable String id) { + browseId = id == null ? "" : id; + } + + /** + * Injection point. + *

    + * The search feed is not a browse page. + */ + public static void clearBrowseId() { + browseId = ""; + } + private final StringTrieSearch exceptions = new StringTrieSearch(); private final StringFilterGroup channelProfileSubscribeButton; @@ -602,16 +619,15 @@ public boolean isFiltered(ContextInterface contextInterface, } if (matchedGroup == communityPosts) { - // YouTube now also shows community posts on a dedicated page for channels. - // This page is navigated with a back button, and hiding its posts - // prevents any posts from loading (the page shows a loading spinner forever). - // Only hide the posts when no back button is visible (home and subscription feeds). - // https://github.com/MorpheApp/morphe-patches/issues/4101 - // The comments of the watch page cover the page below them, whose back button - // stays visible when the video is opened from a page such as a playlist. - final boolean isCommentsPanel = PlayerType.getCurrent().isMaximizedOrFullscreen() - && COMMENTS_PANEL_IDS.contains(EngagementPanel.getLastOpenedPanelId()); - if (NavigationBar.isBackButtonVisible() && !isCommentsPanel) { + // YouTube now also shows community posts on a dedicated community page for channels, + // and hiding its posts prevents any posts from loading (the page shows a loading + // spinner forever) (https://github.com/MorpheApp/morphe-patches/issues/4101). + // The page is found by its browse id, as the back button it is navigated with is also + // visible on other pages, such as the search results, and on the page below the watch page. + // The watch page, with its related videos, comments and other panels, covers the page + // below it, also when the video is opened from the community page + // (https://github.com/MorpheApp/morphe-patches/issues/4170). + if (COMMUNITY_PAGE_BROWSE_ID.equals(browseId) && !PlayerType.getCurrent().isMaximizedOrFullscreen()) { return false; } diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/channelsearch/ChannelSearchPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/channelsearch/ChannelSearchPatch.kt index 18c897ccab..68bf047589 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/channelsearch/ChannelSearchPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/channelsearch/ChannelSearchPatch.kt @@ -7,18 +7,16 @@ package app.morphe.patches.youtube.interaction.channelsearch -import app.morphe.patcher.Fingerprint -import app.morphe.patcher.extensions.InstructionExtensions.addInstruction import app.morphe.patcher.extensions.InstructionExtensions.addInstructions import app.morphe.patcher.extensions.InstructionExtensions.addInstructionsWithLabels import app.morphe.patcher.extensions.InstructionExtensions.getInstruction -import app.morphe.patcher.fieldAccess import app.morphe.patcher.patch.bytecodePatch import app.morphe.patcher.patch.resourcePatch -import app.morphe.patcher.util.findFreeRegister -import app.morphe.patcher.util.getReference import app.morphe.patches.all.misc.resources.addResourcesPatch import app.morphe.patches.shared.misc.settings.preference.SwitchPreference +import app.morphe.patches.youtube.misc.navigation.addBrowseIdHook +import app.morphe.patches.youtube.misc.navigation.addSearchResultsHook +import app.morphe.patches.youtube.misc.navigation.browseIdHookPatch import app.morphe.patches.youtube.misc.settings.PreferenceScreen import app.morphe.patches.youtube.misc.settings.settingsPatch import app.morphe.patches.youtube.misc.toolbar.hookToolBar @@ -26,12 +24,7 @@ import app.morphe.patches.youtube.misc.toolbar.toolBarHookPatch import app.morphe.patches.youtube.shared.Constants.COMPATIBILITY_YOUTUBE import app.morphe.util.ResourceGroup import app.morphe.util.copyResources -import app.morphe.util.indexOfFirstInstructionOrThrow -import com.android.tools.smali.dexlib2.AccessFlags -import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction -import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction -import com.android.tools.smali.dexlib2.iface.reference.FieldReference private const val EXTENSION_CLASS = "Lapp/morphe/extension/youtube/patches/ChannelSearchPatch;" @@ -47,6 +40,7 @@ val channelSearchPatch = bytecodePatch( addResourcesPatch, settingsPatch, toolBarHookPatch, + browseIdHookPatch, resourcePatch { execute { copyResources( @@ -66,63 +60,11 @@ val channelSearchPatch = bytecodePatch( hookToolBar("$EXTENSION_CLASS->setSearchButtonView") // A channel page browses by its channel id, which is what the search is scoped to. - BrowseFragmentOnCreateViewFingerprint.let { - it.method.apply { - // The endpoint of the page is read just before it is checked for browse data. - val endpointIndex = it.instructionMatches[1].index - val endpointField = getInstruction(endpointIndex) - .getReference()!! - - // Browse data is an extension of the endpoint, and reading it starts with that field. - val browseDataExtensionIndex = indexOfFirstInstructionOrThrow( - endpointIndex, Opcode.SGET_OBJECT - ) - val browseDataExtensionField = getInstruction(browseDataExtensionIndex) - .getReference()!! - - // Matches two 100% identical methods in the same class, either method works here. - val browseIdMethod = Fingerprint( - accessFlags = listOf(AccessFlags.PUBLIC, AccessFlags.STATIC), - returnType = "Ljava/lang/String;", - parameters = listOf(endpointField.type), - filters = listOf( - fieldAccess(opcode = Opcode.SGET_OBJECT, reference = browseDataExtensionField) - ) - ).matchAll(1..2).first().method - - // The fragment is handed the endpoint of every page it shows, including a page that - // is returned to and served from cache, which makes no browse request to read. - Fingerprint( - definingClass = definingClass, - returnType = "V", - parameters = listOf(endpointField.type), - filters = listOf( - fieldAccess(opcode = Opcode.IPUT_OBJECT, reference = endpointField) - ) - ).method.apply { - val insertIndex = indexOfFirstInstructionOrThrow( - fieldAccess(opcode = Opcode.IPUT_OBJECT, reference = endpointField) - ) + 1 - val freeRegister = findFreeRegister(insertIndex) - - addInstructions( - insertIndex, - """ - invoke-static { p1 }, $browseIdMethod - move-result-object v$freeRegister - invoke-static { v$freeRegister }, $EXTENSION_CLASS->setBrowseId(Ljava/lang/String;)V - """ - ) - } - } - } + addBrowseIdHook("$EXTENSION_CLASS->setBrowseId(Ljava/lang/String;)V") // Leaving a channel for the search feed shows no browse page, so nothing would // otherwise replace the browse id of the channel. - SearchResultsFragmentOnCreateViewFingerprint.method.addInstruction( - 0, - "invoke-static { }, $EXTENSION_CLASS->clearBrowseId()V" - ) + addSearchResultsHook("$EXTENSION_CLASS->clearBrowseId()V") // The search box otherwise still reads as a search of all of YouTube. // Only the default hint is replaced, not the hint of Shorts or playlist search. diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/channelsearch/Fingerprints.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/channelsearch/Fingerprints.kt index cabe0f192e..4422f4c660 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/channelsearch/Fingerprints.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/interaction/channelsearch/Fingerprints.kt @@ -8,56 +8,15 @@ package app.morphe.patches.youtube.interaction.channelsearch import app.morphe.patcher.Fingerprint -import app.morphe.patcher.InstructionLocation.MatchBeforeAnywhere import app.morphe.patcher.anyInstruction -import app.morphe.patcher.fieldAccess import app.morphe.patcher.methodCall import app.morphe.patcher.opcode import app.morphe.patcher.parametersMatch import app.morphe.patcher.resource.ResourceType import app.morphe.patcher.resourceLiteral -import app.morphe.patcher.string import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode -/** - * Every browse page is shown by this one fragment, and the endpoint it is handed names the page. - * The browse request cannot be used instead, because a page served from cache makes no request. - */ -internal object BrowseFragmentOnCreateViewFingerprint : Fingerprint( - returnType = "Landroid/view/View;", - parameters = listOf( - "Landroid/view/LayoutInflater;", - "Landroid/view/ViewGroup;", - "Landroid/os/Bundle;" - ), - filters = listOf( - string("Browse Fragment was given a navigation endpoint without browse data."), - // The endpoint of the page is read before it is checked for browse data. - fieldAccess( - opcode = Opcode.IGET_OBJECT, - definingClass = "this", - location = MatchBeforeAnywhere() - ) - ) -) - -/** - * The search feed is its own fragment, so it does not go through the browse fragment when - * the user returns to it from a channel. - */ -internal object SearchResultsFragmentOnCreateViewFingerprint : Fingerprint( - returnType = "Landroid/view/View;", - parameters = listOf( - "Landroid/view/LayoutInflater;", - "Landroid/view/ViewGroup;", - "Landroid/os/Bundle;" - ), - filters = listOf( - string("search_cache_key") - ) -) - /** * Picks the hint of the search box. The default hint is loaded last, after the hints of * Shorts search and playlist search. diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/general/HideLayoutComponentsPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/general/HideLayoutComponentsPatch.kt index 70c3d75f42..61deeae518 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/general/HideLayoutComponentsPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/layout/hide/general/HideLayoutComponentsPatch.kt @@ -51,6 +51,9 @@ import app.morphe.patches.youtube.layout.hide.updatescreen.hideUpdateScreenPatch import app.morphe.patches.youtube.misc.engagement.engagementPanelHookPatch import app.morphe.patches.youtube.misc.litho.filter.lithoFilterPatch import app.morphe.patches.youtube.misc.litho.node.treeNodeElementHookPatch +import app.morphe.patches.youtube.misc.navigation.addBrowseIdHook +import app.morphe.patches.youtube.misc.navigation.addSearchResultsHook +import app.morphe.patches.youtube.misc.navigation.browseIdHookPatch import app.morphe.patches.youtube.misc.navigation.navigationBarHookPatch import app.morphe.patches.youtube.misc.playservice.is_20_31_or_greater import app.morphe.patches.youtube.misc.playservice.is_21_11_or_greater @@ -111,6 +114,7 @@ val hideLayoutComponentsPatch = bytecodePatch( settingsPatch, engagementPanelHookPatch, navigationBarHookPatch, + browseIdHookPatch, versionCheckPatch, hideHorizontalShelvesPatch, hideUpdateScreenPatch, @@ -394,6 +398,11 @@ val hideLayoutComponentsPatch = bytecodePatch( addLithoFilter(LAYOUT_COMPONENTS_FILTER) addLithoFilter(DESCRIPTION_COMPONENTS_FILTER) + + // The community posts are not hidden on the community page of a channel. + addBrowseIdHook("$LAYOUT_COMPONENTS_FILTER->setBrowseId(Ljava/lang/String;)V") + addSearchResultsHook("$LAYOUT_COMPONENTS_FILTER->clearBrowseId()V") + if (is_20_31_or_greater) { addLithoFilter(EXPLORE_MENU_FILTER) } diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/misc/navigation/Fingerprints.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/navigation/Fingerprints.kt index db69082dd5..3d5e6260bb 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/misc/navigation/Fingerprints.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/navigation/Fingerprints.kt @@ -12,7 +12,9 @@ package app.morphe.patches.youtube.misc.navigation import app.morphe.patcher.Fingerprint import app.morphe.patcher.InstructionLocation.MatchAfterWithin +import app.morphe.patcher.InstructionLocation.MatchBeforeAnywhere import app.morphe.patcher.checkCast +import app.morphe.patcher.fieldAccess import app.morphe.patcher.methodCall import app.morphe.patcher.opcode import app.morphe.patcher.resource.ResourceType @@ -170,3 +172,41 @@ internal object InitializeBottomBarContainerFingerprint : Fingerprint( ) ) ) + +/** + * Every browse page is shown by this one fragment, and the endpoint it is handed names the page. + * The browse request cannot be used instead, because a page served from cache makes no request. + */ +internal object BrowseFragmentOnCreateViewFingerprint : Fingerprint( + returnType = "Landroid/view/View;", + parameters = listOf( + "Landroid/view/LayoutInflater;", + "Landroid/view/ViewGroup;", + "Landroid/os/Bundle;" + ), + filters = listOf( + string("Browse Fragment was given a navigation endpoint without browse data."), + // The endpoint of the page is read before it is checked for browse data. + fieldAccess( + opcode = Opcode.IGET_OBJECT, + definingClass = "this", + location = MatchBeforeAnywhere() + ) + ) +) + +/** + * The search feed is its own fragment, so it does not go through the browse fragment when + * the user returns to it from a channel. + */ +internal object SearchResultsFragmentOnCreateViewFingerprint : Fingerprint( + returnType = "Landroid/view/View;", + parameters = listOf( + "Landroid/view/LayoutInflater;", + "Landroid/view/ViewGroup;", + "Landroid/os/Bundle;" + ), + filters = listOf( + string("search_cache_key") + ) +) diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/misc/navigation/NavigationBarHookPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/navigation/NavigationBarHookPatch.kt index 8b6237fbef..c9e6050d70 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/misc/navigation/NavigationBarHookPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/misc/navigation/NavigationBarHookPatch.kt @@ -10,10 +10,12 @@ package app.morphe.patches.youtube.misc.navigation +import app.morphe.patcher.Fingerprint import app.morphe.patcher.extensions.InstructionExtensions.addInstruction import app.morphe.patcher.extensions.InstructionExtensions.addInstructions import app.morphe.patcher.extensions.InstructionExtensions.getInstruction import app.morphe.patcher.extensions.InstructionExtensions.instructions +import app.morphe.patcher.fieldAccess import app.morphe.patcher.patch.PatchException import app.morphe.patcher.patch.bytecodePatch import app.morphe.patcher.patch.resourcePatch @@ -32,6 +34,7 @@ import app.morphe.patches.youtube.misc.playservice.versionCheckPatch import app.morphe.patches.youtube.shared.ActionBarSearchResultsFingerprint import app.morphe.util.ResourceGroup import app.morphe.util.copyResources +import app.morphe.util.indexOfFirstInstructionOrThrow import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation @@ -39,6 +42,7 @@ import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction import com.android.tools.smali.dexlib2.iface.instruction.Instruction import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction +import com.android.tools.smali.dexlib2.iface.reference.FieldReference import com.android.tools.smali.dexlib2.iface.reference.MethodReference import com.android.tools.smali.dexlib2.immutable.ImmutableMethod import com.android.tools.smali.dexlib2.util.MethodUtil @@ -84,6 +88,100 @@ fun hookNavigationButtonCreated(extensionClassDescriptor: String) { ) } +private lateinit var browseIdMethodRef: WeakReference +private var browseIdInsertIndex = -1 +private var browseIdRegister = -1 + +private lateinit var searchResultsMethodRef: WeakReference + +/** + * Calls the extension method with the browse id of every browse page that is shown, including + * a page that is returned to and served from cache. Channel pages use the channel id as browse id. + * + * @param descriptor Static extension method with a single String parameter, which can be null. + */ +fun addBrowseIdHook(descriptor: String) { + browseIdMethodRef.get()!!.addInstruction( + browseIdInsertIndex, + "invoke-static { v$browseIdRegister }, $descriptor" + ) +} + +/** + * Calls the extension method when the search feed is shown. It is not a browse page, + * so returning to it from a browse page does not call the hooks of [addBrowseIdHook]. + * + * @param descriptor Static extension method with no parameters. + */ +fun addSearchResultsHook(descriptor: String) { + searchResultsMethodRef.get()!!.addInstruction( + 0, + "invoke-static { }, $descriptor" + ) +} + +internal val browseIdHookPatch = bytecodePatch(description = "Hooks the browse id of the page that is shown.") { + dependsOn(sharedExtensionPatch) + + execute { + BrowseFragmentOnCreateViewFingerprint.let { + it.method.apply { + // The endpoint of the page is read just before it is checked for browse data. + val endpointIndex = it.instructionMatches[1].index + val endpointField = getInstruction(endpointIndex) + .getReference()!! + + // Browse data is an extension of the endpoint, and reading it starts with that field. + val browseDataExtensionIndex = indexOfFirstInstructionOrThrow( + endpointIndex, Opcode.SGET_OBJECT + ) + val browseDataExtensionField = getInstruction(browseDataExtensionIndex) + .getReference()!! + + // Matches two 100% identical methods in the same class, either method works here. + val browseIdMethod = Fingerprint( + accessFlags = listOf(AccessFlags.PUBLIC, AccessFlags.STATIC), + returnType = "Ljava/lang/String;", + parameters = listOf(endpointField.type), + filters = listOf( + fieldAccess(opcode = Opcode.SGET_OBJECT, reference = browseDataExtensionField) + ) + ).matchAll(1..2).first().method + + // The fragment is handed the endpoint of every page it shows, including a page that + // is returned to and served from cache, which makes no browse request to read. + Fingerprint( + definingClass = definingClass, + returnType = "V", + parameters = listOf(endpointField.type), + filters = listOf( + fieldAccess(opcode = Opcode.IPUT_OBJECT, reference = endpointField) + ) + ).method.apply { + val insertIndex = indexOfFirstInstructionOrThrow( + fieldAccess(opcode = Opcode.IPUT_OBJECT, reference = endpointField) + ) + 1 + val freeRegister = findFreeRegister(insertIndex) + + addInstructions( + insertIndex, + """ + invoke-static { p1 }, $browseIdMethod + move-result-object v$freeRegister + """ + ) + + browseIdMethodRef = WeakReference(this) + browseIdInsertIndex = insertIndex + 2 + browseIdRegister = freeRegister + } + } + } + + searchResultsMethodRef = WeakReference(SearchResultsFragmentOnCreateViewFingerprint.method) + } +} + val navigationBarHookPatch = bytecodePatch(description = "Hooks the active navigation or search bar.") { dependsOn( sharedExtensionPatch, From 9f5237c879fa7c22c830aa72e3f4a450f648df45 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Fri, 30 Oct 2026 10:36:29 +0000 Subject: [PATCH 184/189] chore: Fix string new line encoding --- .../addresources/values/music/strings.xml | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/patches/src/main/resources/addresources/values/music/strings.xml b/patches/src/main/resources/addresources/values/music/strings.xml index 47b1f738d4..dd42a30415 100644 --- a/patches/src/main/resources/addresources/values/music/strings.xml +++ b/patches/src/main/resources/addresources/values/music/strings.xml @@ -512,11 +512,18 @@ To ensure scrobbling is synced during background playback, ensure that YouTube M Reset provider order? Restore providers to the default priority order Rime Wordlist - Rime Wordlist also needs UniDict: in the Offline Dictionary provider, tick \"Chinese and Cantonese characters (UniDict)\" and let it finish downloading first. Without it, a list that reads characters yields no reading.\n\nTick the lists Rime Wordlist should read, then tap Save to download them. A list you added is removed when you untick it.\n\nEach list is your own word-to-reading table, a .dict.yaml with lines like:\n字\tham\n典\tdin2\nThe first column is the word, the second its reading; extra columns and lines without a tab are ignored. + "Rime Wordlist also needs UniDict: in the Offline Dictionary provider, tick \"Chinese and Cantonese characters (UniDict)\" and let it finish downloading first. Without it, a list that reads characters yields no reading. + +Tick the lists Rime Wordlist should read, then tap Save to download them. A list you added is removed when you untick it. + +Each list is your own word-to-reading table, a .dict.yaml with lines like: +字 ham +典 din2 +The first column is the word, the second its reading; extra columns and lines without a tab are ignored." No Rime word list is ticked for the language above. Tap this name to tick one, or pick a language that has a list Add Add a Rime Wordlist - A .dict.yaml file, or a zip holding one, with lines like 字\tham and 典\tdin2. The list is fetched when you tick it and tap Save, and its licence is yours to check + A .dict.yaml file, or a zip holding one, with lines like 字 ham and 典 din2. The list is fetched when you tick it and tap Save, and its licence is yours to check Language code Two or three letters, as in hak or wuu Name @@ -698,8 +705,10 @@ Applies to both limits. At 1.75, four full-width characters count as 7."Translated Translation language Romanization or transliteration language - Translation target language:\n%s - Romanization or transliteration target language:\n%s + "Translation target language: +%s" + "Romanization or transliteration target language: +%s" Translating… Could not translate the lyrics Romanized @@ -729,7 +738,9 @@ Applies to both limits. At 1.75, four full-width characters count as 7."No lyrics to align - transcribing instead QuickLRC could not process this track QuickLRC credits are used up for this track - "Lyrics are matched by title and artist, unusual metadata may find nothing or the wrong version. Use refresh to switch versions. Providers marked * require a token. All enabled providers are not always requested in your order, but respond based on their speed.\n\nLyrics come from third-party services that are not affiliated with, sponsored, or endorsed by Morphe. Each one is used as it is: the lyrics, their availability, and their licensing stay with those services and their licensors, and using them can be subject to their terms of service. What reaches an enabled service is only the lookup you asked for - the title, the artist, and, where that service needs it, the video ID. The optional AI endpoint and QuickLRC transcription run only after you configure them, and receive the lyrics text or the track's video URL." + "Lyrics are matched by title and artist, unusual metadata may find nothing or the wrong version. Use refresh to switch versions. Providers marked * require a token. All enabled providers are not always requested in your order, but respond based on their speed. + +Lyrics come from third-party services that are not affiliated with, sponsored, or endorsed by Morphe. Each one is used as it is: the lyrics, their availability, and their licensing stay with those services and their licensors, and using them can be subject to their terms of service. What reaches an enabled service is only the lookup you asked for - the title, the artist, and, where that service needs it, the video ID. The optional AI endpoint and QuickLRC transcription run only after you configure them, and receive the lyrics text or the track's video URL." Only enable the providers you use, or keep the default - constant requests can overwhelm small community lyrics sites Hide credit line Hide the credit line shown below the lyrics From d3a26092a184156c3b5c1c7d4b7ca947bf698eb1 Mon Sep 17 00:00:00 2001 From: 0xrxL <127248639+0xrxL@users.noreply.github.com> Date: Fri, 30 Oct 2026 11:46:33 +0100 Subject: [PATCH 185/189] chore(YouTube): Cleanup the element hooks that slow down the loading of the video page --- .../youtube/patches/FeedCardLayoutPatch.java | 21 ++--- .../patches/HidePlayerFlyoutMenuPatch.java | 7 ++ .../patches/PostImageDownloadPatch.java | 9 ++- .../youtube/patches/PostTranslatePatch.java | 10 ++- .../patches/utils/FeedVideoCardsPatch.java | 79 ++++++++++++++++--- .../youtube/patches/utils/FlyoutUtils.java | 15 +++- 6 files changed, 112 insertions(+), 29 deletions(-) diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FeedCardLayoutPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FeedCardLayoutPatch.java index 207d9e8fd3..5a98f2b25c 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FeedCardLayoutPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/FeedCardLayoutPatch.java @@ -129,6 +129,7 @@ import app.morphe.extension.youtube.innertube.FeedCardLayoutOuterClass.VideoWithContextModel; import app.morphe.extension.youtube.patches.components.LayoutComponentsFilter; import app.morphe.extension.youtube.patches.theme.SeekbarColorPatch; +import app.morphe.extension.youtube.patches.utils.FeedVideoCardsPatch; import app.morphe.extension.youtube.patches.utils.FlyoutUtils; import app.morphe.extension.youtube.patches.utils.ProtoNode; import app.morphe.extension.youtube.settings.Settings; @@ -311,13 +312,14 @@ public interface ElementItemInterface { */ private static final Pattern METADATA_SEPARATOR_PATTERN = Pattern.compile(" {2,}| · "); - private static final ByteTrieSearch elementSearch = new ByteTrieSearch( - CARD_LAYOUT - ? ByteTrieSearch.convertStringsToBytes(CLASSIC_TEMPLATE, SHELF_HEADER_TEMPLATE, - HORIZONTAL_SHELF_TEMPLATE, SHORTS_VIDEO_CELL_TEMPLATE, SHORTS_SHELF_TEMPLATE, - GRID_VIDEO_TEMPLATE) - : ByteTrieSearch.convertStringsToBytes(CARD_TEMPLATE) - ); + /** + * Templates of the elements that are converted. Every element is searched on the main thread, + * so the element is searched as a string in native code, see {@link FeedVideoCardsPatch#getElementText(byte[])}. + */ + private static final String[] elementTemplates = CARD_LAYOUT + ? new String[]{CLASSIC_TEMPLATE, SHELF_HEADER_TEMPLATE, HORIZONTAL_SHELF_TEMPLATE, + SHORTS_VIDEO_CELL_TEMPLATE, SHORTS_SHELF_TEMPLATE, GRID_VIDEO_TEMPLATE} + : new String[]{CARD_TEMPLATE}; /** * Label of the shelf headers, in the bytes of the converted elements. */ @@ -557,14 +559,15 @@ private static final class Icons { */ public static byte[] setFeedCardLayout(byte[] bytes) { try { - if (!elementSearch.matches(bytes)) { + final String elementText = FeedVideoCardsPatch.getElementText(bytes); + if (!Utils.containsAny(elementText, elementTemplates)) { // The elements of the layout of the setting give the current version of its template: // the uri and the fields are taken from the element, if it's an element of the template. // After the first element, the elements are checked once in a while, to follow a new version. VideoTemplate template = CARD_LAYOUT ? CARD : CLASSIC; final long now = System.currentTimeMillis(); if ((!template.fromElement || now - template.checkTime >= TEMPLATE_CHECK_INTERVAL_MILLIS) - && template.search.matches(bytes)) { + && elementText.contains(template.name)) { ComponentType componentType = Element.parseFrom(bytes).getType().getComponentType(); String elementUri = componentType.getTemplateConfig().getUriTemplateConfig().getUri(); if (isTemplate(elementUri, template.name)) { diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/HidePlayerFlyoutMenuPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/HidePlayerFlyoutMenuPatch.java index 745561320c..c4f384136b 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/HidePlayerFlyoutMenuPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/HidePlayerFlyoutMenuPatch.java @@ -19,6 +19,7 @@ import app.morphe.extension.shared.Utils; import app.morphe.extension.shared.patches.ConversionContext; import app.morphe.extension.youtube.innertube.NextResponseOuterClass.NewElement; +import app.morphe.extension.youtube.patches.utils.FeedVideoCardsPatch; import app.morphe.extension.youtube.settings.Settings; @SuppressWarnings("unused") @@ -104,6 +105,12 @@ public static void hideNativeBottomSheetFooter(CharSequence path, List t public static byte[] hideNativeBottomSheetHeader(byte[] bytes) { if (HIDE_PLAYER_FLYOUT_CAPTIONS_HEADER || HIDE_PLAYER_FLYOUT_QUALITY_HEADER) { try { + // Every element is parsed here otherwise, on the main thread. The identifier of + // a header is also in the bytes of its element, see FeedVideoCardsPatch.getElementText(). + if (!Utils.containsAny(FeedVideoCardsPatch.getElementText(bytes), PLAYER_FLYOUT_HEADER_IDENTIFIERS)) { + return bytes; + } + var newElement = NewElement.parseFrom(bytes).toBuilder(); var identifier = newElement.getProperties().getIdentifierProperties().getIdentifier(); diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/PostImageDownloadPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/PostImageDownloadPatch.java index 0b80770344..7da924a5cb 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/PostImageDownloadPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/PostImageDownloadPatch.java @@ -137,7 +137,10 @@ public final class PostImageDownloadPatch { */ private static final String NOT_IMAGE_POST = "not_image_post"; - private static final byte[] POST_BYTES = "post".getBytes(StandardCharsets.US_ASCII); + /** + * Included in the identifier of the post elements. + */ + private static final String POST = "post"; private static final byte[] GGPHT_BYTES = "ggpht.com/".getBytes(StandardCharsets.US_ASCII); private static final byte[] GOOGLE_USER_CONTENT_BYTES = "googleusercontent.com/".getBytes(StandardCharsets.US_ASCII); @@ -296,7 +299,7 @@ private static UrlKind getUrlKind(String text) { * Called when an element is parsed. The element is not changed. */ public static byte[] onElementLoaded(byte[] bytes) { - if (!ENABLED || bytes == null || !FeedVideoCardsPatch.contains(bytes, POST_BYTES) + if (!ENABLED || bytes == null || !FeedVideoCardsPatch.emlTextMayContain(bytes, POST) || !(FeedVideoCardsPatch.contains(bytes, GGPHT_BYTES) || FeedVideoCardsPatch.contains(bytes, GOOGLE_USER_CONTENT_BYTES))) { return bytes; @@ -317,7 +320,7 @@ public static byte[] onElementLoaded(byte[] bytes) { } } final String elementIdentifier = identifier; - if (elementIdentifier == null || !elementIdentifier.contains("post")) { + if (elementIdentifier == null || !elementIdentifier.contains(POST)) { return bytes; } diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/PostTranslatePatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/PostTranslatePatch.java index cb88a6080d..5227679360 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/PostTranslatePatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/PostTranslatePatch.java @@ -42,7 +42,6 @@ import java.lang.reflect.Array; import java.lang.reflect.Field; import java.lang.reflect.Modifier; -import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -112,7 +111,10 @@ public final class PostTranslatePatch { /** Texts that overlap the first text by more than this are in the header of the post. */ private static final int HEADER_TOLERANCE_DP = 2; - private static final byte[] POST_BYTES = "post".getBytes(StandardCharsets.US_ASCII); + /** + * Included in the identifier of the post elements. + */ + private static final String POST = "post"; /** The shown text can join the lines of the post text, or show invisible format characters. */ private static final Pattern WHITESPACE_PATTERN = Pattern.compile("[\\s\\p{Cf}]+"); @@ -221,7 +223,7 @@ private static String getCutOffStart(String text) { * Called when an element is parsed. The element is not changed. */ public static byte[] onElementLoaded(byte[] bytes) { - if (!ENABLED || bytes == null || !FeedVideoCardsPatch.contains(bytes, POST_BYTES)) { + if (!ENABLED || bytes == null || !FeedVideoCardsPatch.emlTextMayContain(bytes, POST)) { return bytes; } try { @@ -240,7 +242,7 @@ public static byte[] onElementLoaded(byte[] bytes) { break; } } - if (identifier == null || !identifier.contains("post")) { + if (identifier == null || !identifier.contains(POST)) { return bytes; } diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/FeedVideoCardsPatch.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/FeedVideoCardsPatch.java index fa86dfe300..9e46b2613d 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/FeedVideoCardsPatch.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/FeedVideoCardsPatch.java @@ -72,6 +72,18 @@ public final class FeedVideoCardsPatch { || PostImageDownloadPatch.ENABLED || PostTranslatePatch.ENABLED; + /** + * If the video of every card is needed. The posts are found by their own texts, so with only + * the post features on, the elements of the videos are not parsed and the cards are not matched + * to their videos. The elements are parsed on the main thread, while the page loads. + * {@link FeedVideoActionsPatch} is also given the video of the card for its long press menu. + */ + private static final boolean VIDEO_CARDS = ENABLED + && (FeedEngagementCountsPatch.ENABLED + || FeedViewCountPatch.ENABLED + || Settings.FEED_WATCH_LATER_BUTTON.get() + || Settings.FEED_LONG_PRESS_MENU.get()); + /** * A card must be as wide as a card of a grid of up to three columns, such as the feed in landscape. * Shelves are not used, as they are horizontal lists. @@ -277,19 +289,60 @@ private FeedVideoCardsPatch() { * @return If the bytes include the search bytes, such as an ASCII text of an element. */ public static boolean contains(byte[] bytes, byte[] search) { - final int last = bytes.length - search.length; - outer: - for (int i = 0; i <= last; i++) { - for (int j = 0; j < search.length; j++) { - if (bytes[i + j] != search[j]) { - continue outer; - } + return getElementText(bytes).contains(new String(search, StandardCharsets.ISO_8859_1)); + } + + /** + * @return If a text of the element that includes ".eml", such as the identifier of the element, + * can include the search text. Every text around ".eml" is checked as a whole, from the + * bytes before and after it that are printable ASCII, so any such text that includes + * the search text is found. Faster than parsing the element to read its texts, as words + * such as "post" are found in most elements. + */ + public static boolean emlTextMayContain(byte[] bytes, String search) { + String element = getElementText(bytes); + int index = element.indexOf(".eml"); + while (index >= 0) { + int start = index; + while (start > 0 && element.charAt(start - 1) >= 0x20 && element.charAt(start - 1) <= 0x7E) { + start--; } - return true; + int end = index + 4; + while (end < element.length() && element.charAt(end) >= 0x20 && element.charAt(end) <= 0x7E) { + end++; + } + final int found = element.indexOf(search, start); + if (found >= 0 && found + search.length() <= end) { + return true; + } + index = element.indexOf(".eml", end); } return false; } + /** + * The bytes of the element last searched on each thread, and their string. + */ + private static final ThreadLocal lastElementText = ThreadLocal.withInitial(() -> new Object[2]); + + /** + * The element hooks search every element on the main thread, while the page loads. Every byte is + * one character in ISO-8859-1, so the bytes include some bytes when the string of the bytes includes + * their string, which is searched in native code. That is much faster than a loop that is not compiled + * yet, as when the app starts. The bytes of an element are a buffer of the app that is not changed, + * and the hooks are called one after another for it, so the string is made once for all of them. + * + * @return The bytes as a string with one character for each byte. + */ + public static String getElementText(byte[] bytes) { + Object[] last = lastElementText.get(); + if (last[0] != bytes) { + last[1] = new String(bytes, StandardCharsets.ISO_8859_1); + last[0] = bytes; + } + return (String) last[1]; + } + /** * @return The text as it's matched between an element and a card. */ @@ -311,7 +364,7 @@ private static boolean isVideoText(String text) { * Called when an element is parsed. The element is not changed. */ public static byte[] onElementLoaded(byte[] bytes) { - if (!ENABLED || bytes == null || !contains(bytes, THUMBNAIL_PATH)) { + if (!VIDEO_CARDS || bytes == null || !contains(bytes, THUMBNAIL_PATH)) { return bytes; } try { @@ -394,7 +447,7 @@ public static byte[] onElementLoaded(byte[] bytes) { * of the video do not include. Can be called on any thread. */ public static void addVideoText(String videoId, @Nullable String text) { - if (!ENABLED || text == null) { + if (!VIDEO_CARDS || text == null) { return; } String normalized = normalizeText(text); @@ -945,6 +998,12 @@ public static boolean scheduleRetry(ComponentHost root) { if (!root.isAttachedToWindow() || root.getParent() instanceof ComponentHost) { continue; } + if (!VIDEO_CARDS) { + // Only the post features are on, which do not use the video of the card. + PostImageDownloadPatch.onRootLaidOut(root); + PostTranslatePatch.onRootLaidOut(root); + continue; + } String videoId = findVideoId(root); if (NOT_READY.equals(videoId) || (videoId == null && isVideoShaped(root) && !hasHorizontalList(root)) diff --git a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/FlyoutUtils.java b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/FlyoutUtils.java index 595d6d8341..4e04e7b1df 100644 --- a/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/FlyoutUtils.java +++ b/extensions/youtube/src/main/java/app/morphe/extension/youtube/patches/utils/FlyoutUtils.java @@ -416,9 +416,18 @@ public static String getFlyoutChannelName() { * via the video player's comments button. */ public static byte[] onCommentsLoaded(byte[] bytes) { - List kidsVideoElementsBytesIndexes = byteIndexesOf(bytes, KIDS_VIDEO_ELEMENTS_BYTES); - if (!kidsVideoElementsBytesIndexes.isEmpty() && - kidsVideoElementsBytesIndexes.size() == KIDS_VIDEO_ELEMENTS_BYTES.size() - 1) { + if (bytes == null) { + return null; + } + + // Every element is searched on the main thread, see FeedVideoCardsPatch.getElementText(). + int found = 0; + for (byte[] search : KIDS_VIDEO_ELEMENTS_BYTES) { + if (FeedVideoCardsPatch.contains(bytes, search)) { + found++; + } + } + if (found == KIDS_VIDEO_ELEMENTS_BYTES.size() - 1) { videoMarkedAsForKids = true; } From b6312e82ed7b6f0a15d8f87d5f6aab4b3f1b1aad Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Fri, 30 Oct 2026 10:51:01 +0000 Subject: [PATCH 186/189] chore: Fix string escaping --- .../src/main/resources/addresources/values/music/strings.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/patches/src/main/resources/addresources/values/music/strings.xml b/patches/src/main/resources/addresources/values/music/strings.xml index dd42a30415..994aea9297 100644 --- a/patches/src/main/resources/addresources/values/music/strings.xml +++ b/patches/src/main/resources/addresources/values/music/strings.xml @@ -779,8 +779,8 @@ Lyrics come from third-party services that are not affiliated with, sponsored, o API key (optional) Extra request body (optional JSON) Extra request headers (optional JSON) - "Quote names and values, e.g. {"api-key": "abc"}. An unquoted value containing / : or a number is rejected. An empty value removes a header." - "Quote names and values, e.g. {"temperature": 0.3}. An unquoted value containing / or : is rejected. Model and messages cannot be set here." + Quote names and values, e.g. {\"api-key\": \"abc\"}. An unquoted value containing / : or a number is rejected. An empty value removes a header. + Quote names and values, e.g. {\"temperature\": 0.3}. An unquoted value containing / or : is rejected. Model and messages cannot be set here. Authentication key (Free or Pro) From 46a57bc4e4960cacfa3d06e3e40d4caf9c2d63a4 Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Fri, 30 Oct 2026 10:56:48 +0000 Subject: [PATCH 187/189] chore: Update translations from Crowdin --- .../values-af-rZA/music/strings.xml | 126 ++++++++++++------ .../values-af-rZA/youtube/strings.xml | 2 + .../values-am-rET/music/strings.xml | 40 +----- .../values-ar-rSA/music/strings.xml | 126 ++++++++++++------ .../values-ar-rSA/youtube/strings.xml | 2 + .../values-as-rIN/music/strings.xml | 40 +----- .../values-az-rAZ/music/strings.xml | 45 +------ .../values-be-rBY/music/strings.xml | 41 +----- .../values-bg-rBG/music/strings.xml | 126 ++++++++++++------ .../values-bg-rBG/shared-youtube/strings.xml | 4 +- .../values-bg-rBG/youtube/strings.xml | 4 +- .../values-bn-rBD/music/strings.xml | 126 ++++++++++++------ .../values-bn-rBD/youtube/strings.xml | 2 + .../values-bs-rBA/music/strings.xml | 40 +----- .../values-ca-rES/music/strings.xml | 41 +----- .../values-ckb-rIR/music/strings.xml | 41 +----- .../values-cs-rCZ/music/strings.xml | 119 +++++++++++------ .../values-cs-rCZ/youtube/strings.xml | 2 + .../values-da-rDK/music/strings.xml | 124 +++++++++++------ .../values-da-rDK/youtube/strings.xml | 2 + .../values-de-rDE/music/strings.xml | 126 ++++++++++++------ .../values-de-rDE/youtube/strings.xml | 2 + .../values-el-rGR/music/strings.xml | 126 ++++++++++++------ .../values-el-rGR/youtube/strings.xml | 2 + .../values-es-rES/music/strings.xml | 124 +++++++++++------ .../values-es-rES/youtube/strings.xml | 2 + .../values-et-rEE/music/strings.xml | 40 +----- .../values-eu-rES/music/strings.xml | 40 +----- .../values-fa-rIR/music/strings.xml | 126 ++++++++++++------ .../values-fa-rIR/youtube/strings.xml | 2 + .../values-fi-rFI/music/strings.xml | 126 ++++++++++++------ .../values-fi-rFI/youtube/strings.xml | 2 + .../values-fil-rPH/music/strings.xml | 124 +++++++++++------ .../values-fil-rPH/youtube/strings.xml | 2 + .../values-fr-rFR/music/strings.xml | 124 +++++++++++------ .../values-fr-rFR/youtube/strings.xml | 4 +- .../values-ga-rIE/music/strings.xml | 124 +++++++++++------ .../values-ga-rIE/youtube/strings.xml | 2 + .../values-gl-rES/music/strings.xml | 41 +----- .../values-hi-rIN/music/strings.xml | 126 ++++++++++++------ .../values-hi-rIN/youtube/strings.xml | 2 + .../values-hr-rHR/music/strings.xml | 126 ++++++++++++------ .../values-hr-rHR/youtube/strings.xml | 2 + .../values-hu-rHU/music/strings.xml | 126 ++++++++++++------ .../values-hu-rHU/youtube/strings.xml | 2 + .../values-hy-rAM/music/strings.xml | 40 +----- .../values-in-rID/music/strings.xml | 126 ++++++++++++------ .../values-in-rID/youtube/strings.xml | 2 + .../values-is-rIS/music/strings.xml | 126 ++++++++++++------ .../values-is-rIS/youtube/strings.xml | 2 + .../values-it-rIT/music/strings.xml | 126 ++++++++++++------ .../values-it-rIT/youtube/strings.xml | 2 + .../values-iw-rIL/music/strings.xml | 126 ++++++++++++------ .../values-iw-rIL/youtube/strings.xml | 2 + .../values-ja-rJP/music/strings.xml | 124 +++++++++++------ .../values-ja-rJP/youtube/strings.xml | 2 + .../values-ka-rGE/music/strings.xml | 40 +----- .../values-kk-rKZ/music/strings.xml | 40 +----- .../values-kmr-rTR/music/strings.xml | 41 +----- .../values-ko-rKR/music/strings.xml | 124 +++++++++++------ .../values-ky-rKG/music/strings.xml | 41 +----- .../values-lo-rLA/music/strings.xml | 40 +----- .../values-lt-rLT/music/strings.xml | 126 ++++++++++++------ .../values-lt-rLT/youtube/strings.xml | 2 + .../values-lv-rLV/music/strings.xml | 41 +----- .../values-mk-rMK/music/strings.xml | 41 +----- .../values-mn-rMN/music/strings.xml | 40 +----- .../values-mr-rIN/music/strings.xml | 40 +----- .../values-ms-rMY/music/strings.xml | 41 +----- .../values-nb-rNO/music/strings.xml | 126 ++++++++++++------ .../values-nb-rNO/youtube/strings.xml | 2 + .../values-nl-rNL/music/strings.xml | 126 ++++++++++++------ .../values-nl-rNL/youtube/strings.xml | 2 + .../values-pl-rPL/music/strings.xml | 124 +++++++++++------ .../values-pl-rPL/youtube/strings.xml | 2 + .../values-pt-rBR/music/strings.xml | 124 +++++++++++------ .../values-pt-rBR/youtube/strings.xml | 2 + .../values-pt-rPT/music/strings.xml | 124 +++++++++++------ .../values-pt-rPT/youtube/strings.xml | 2 + .../values-ro-rRO/music/strings.xml | 124 +++++++++++------ .../values-ro-rRO/youtube/strings.xml | 2 + .../values-ru-rRU/music/strings.xml | 126 ++++++++++++------ .../values-ru-rRU/youtube/strings.xml | 2 + .../values-sk-rSK/music/strings.xml | 124 +++++++++++------ .../values-sk-rSK/youtube/strings.xml | 2 + .../values-sl-rSI/music/strings.xml | 126 ++++++++++++------ .../values-sl-rSI/youtube/strings.xml | 2 + .../values-sr-rCS/music/strings.xml | 124 +++++++++++------ .../values-sr-rCS/youtube/strings.xml | 2 + .../values-sr-rSP/music/strings.xml | 126 ++++++++++++------ .../values-sr-rSP/youtube/strings.xml | 2 + .../values-sv-rSE/music/strings.xml | 126 ++++++++++++------ .../values-sv-rSE/youtube/strings.xml | 2 + .../values-sw-rKE/music/strings.xml | 40 +----- .../values-th-rTH/music/strings.xml | 126 ++++++++++++------ .../values-th-rTH/youtube/strings.xml | 2 + .../values-tr-rTR/music/strings.xml | 124 +++++++++++------ .../values-tr-rTR/youtube/strings.xml | 2 + .../values-uk-rUA/music/strings.xml | 120 +++++++++++------ .../values-uz-rUZ/music/strings.xml | 40 +----- .../values-vi-rVN/music/strings.xml | 124 +++++++++++------ .../values-vi-rVN/youtube/strings.xml | 2 + .../values-zh-rCN/music/strings.xml | 124 +++++++++++------ .../values-zh-rCN/youtube/strings.xml | 2 + .../values-zh-rTW/music/strings.xml | 122 +++++++++++------ .../values-zh-rTW/youtube/strings.xml | 2 + .../values-zu-rZA/music/strings.xml | 40 +----- 107 files changed, 3634 insertions(+), 2671 deletions(-) diff --git a/patches/src/main/resources/addresources/values-af-rZA/music/strings.xml b/patches/src/main/resources/addresources/values-af-rZA/music/strings.xml index 6860b1a72e..cd5491c91e 100644 --- a/patches/src/main/resources/addresources/values-af-rZA/music/strings.xml +++ b/patches/src/main/resources/addresources/values-af-rZA/music/strings.xml @@ -462,12 +462,47 @@ Om te verseker dat scrobbling tydens agtergrondweergawe gesinkroniseer word, ver Vertoon Sinchronisasie Paneel - AI-eindpunt Alle verskaffers gedeaktiveer - ingeboude lirieke en ingeslote lirieke Verskaffervolgorde terugstel? Herstel verskaffers na die standaardprioriteitsorde + Rime Woordlys + "Rime Woordlys benodig ook UniDict: in die Afgeskakelde Woordeboekverskaffer, merk \"Chinese en Kantonese karakters (UniDict)\" en laat dit eers klaar aflaai. Sonder dit lewer 'n lys wat karakters lees geen lees op nie. + +Merk die lyste wat Rime Woordlys moet lees, tik dan Stoor om hulle af te laai. 'n Lys wat jy bygevoeg het, word verwyder wanneer jy dit ontmerk. + +Elke lys is jou eie woord-na-lees tabel, 'n .dict.yaml met lyne soos: +字 ham +典 din2 +Die eerste kolom is die woord, die tweede sy lees; ekstra kolomme en lyne sonder 'n tab word geignoreer." + Geen Rime-woordlys is vir die taal hierbo aangelê nie. Tik hierdie naam om een aan te vink, of kies \'n taal wat \'n lys het + Voeg by + Voeg \'n Rime-woordlys by + \'n .dict.yaml-lêer, of \'n zip wat een bevat, met lyne soos 字 ham en 典 din2. Die lys word opgespoor wanneer jy dit merk en tik Stoor, en sy lisensie is van jou om te kontroleer + Taalkode + Twee of drie letters, soos hak of wuu + Naam + Word onder die lesing vertoon, soos Hakka (Meixian) + Woordlys-adres + https:// adres van die .dict.yaml of sy zip + Karakters nie in \'n woord nie + Outomaties + Kantonese karakters + Mandaryn-karakters + Geen karakterlesing nie + Vul die kode, naam en adres in + Die kode moet twee of drie letters wees, soos hak + Die adres moet begin met https:// + Die naam en adres kan nie \'n vertikale staaf bevat nie + %1$s het reeds \'n tafeletjie + %1$s word deur die ingeboude romaniser gelees en kan nie \'n woordlys vat nie + Nie gebruik nie: hierdie taal het reeds sy eie lesing, so onvink dit om dit te verwyder + %1$s bygevoeg - merk dit aan en tik Stoor om dit af te laai + Woordlyse gereed + Woordlyse gereed (%1$d inskrywings) + Lesing woordlys: %1$d%% + Aflewering van woordlys misluk - tik Stoor weer om te probeer Skoon gestoorde lirieke - Verwyder die lirieke wat op hierdie toestel behou is + Verwyder die %1$s MB van lirieke wat op hierdie toestel bewaar word Maak %1$s MB skoon Kry token Apple Music-mediagebruikerstoken @@ -522,14 +557,22 @@ Vereiste koekies: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPISID Wys Kopièer knoppie Klik om oorspronklike lirieke as plat teks te kopieer; hou ingedruk om ruwe lirieke-bronlêer na Aflaais te stuur Wys Vertaal knoppie - Klik om vertaling te wissel; hou ingedruk vir slegs vertaling. Gebruik verskaffer se vertalings, Google Vertaling, of ’n OpenAI-kompatabele API - Wys romaniseer-knoppie - Klik om romanisering te wissel; hou ingedruk vir slegs romanisering. Gebruik verskaffer se romanisering, Google Vertaling, of ’n OpenAI-kompatabele API - Plaaslike Romanisering en Uitleg - Romaniseer op die toestel wanneer die verskaffer, jou model en Google Vertaling misluk. Die lees volg elke woord van ’n tydsgebaseerde liriekspoor; woordsinchronisering moet aan wees. Chinees, Kantonis, Japannese, Koreaans, Grieks, Russies, Georgies, Armeens; ongeveer 120 MB - Lees %1$s: %2$d%% + Tik om vertaling te skakel; lang druk vir slegs vertaling. Gebruik die aktiveringsvertaalverskaffers in hul lysvolgorde + Vertoon Romaniseer of Translitereer-knoppie + Tik om romanisering of transliterasie te skakel; langdruk vir slegs romanisering of transliterasie. Gebruik die aktiveringsromaniseringverskaffers in hul lysvolgorde + Aktiveer transliterasie + Laat nie-Latynse lesings toe vanaf die liriekbron; wanneer af, hou slegs Latynse letters, geaksenteerde letters en toonsiffers + Lees elke woord van \'n tydsyncro-liriek (woordsinkronisasie aan); Koreaans benodig geen aflaai nie; veertien skrifte deel een CLDR-aflaai; Han-karakters word deur Rime-woordlys gelees wanneer sy woordestabel die taal dek + Aflaai van lesingswoordeboeke… Lees gereed (%1$d inskrywings) Laai misluk - skakel dit af en aan weer om te herprobeer + Oorlogswoordeboek + Japannese (SudachiDict, ~75 MB) + Chinese en Kantonese karakters (UniDict, ~8 MB) + Chineesse woorde (pinyin-pro, ~17 MB) + Grieks, Slawiese Cyrillies (insluitend Russies, Oekraïens, Bulgaars, Serwies, Wit-Russies, Masedonies), Georgies, Armeens, Hebreeus, Arabies, Amharaties en Sirië transliterasie (CLDR, ~0.5 MB) + Lees %1$s: %2$d%% + Kies ten minste een woordeboek Wys verfris-knoppie Klik om na die volgende liriekkandidaat te skakel; hou ingedruk om lirieke deur titel en kunstenaar soek Soek lirieke @@ -540,8 +583,10 @@ Vereiste koekies: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPISID Gestoor Raak aan ▲/▼ om die prioriteitsorde te stel, en die blokkie om elke verskaffer te aktiveer of deaktiveer. Alle verskaffers gedeaktiveer — ingeboude lirieke en ingebedde lirieke. - Wys agtergrondvokale afsonderlik - Verskuif \'n agtergrond-vokaal wat in hakies geskryf is, soos \"(Oh) ek wil hê jy\", uit die gesong lyn na \'n eie dowwer lyn + Krimp onaktiewe lyne + Trek elke lyn wat tans nie gesing word nie effens kleiner, bo-op die vervaging daarvan + Vertoon agtergrondvokale afsonderlik + Skei agtergrondvokale en harmonie van die hooflyn. Verplaas \'n agtergrondvokaal wat in hakies geskryf is, soos \"(Oh) ek wil hê dat jy\", uit die gesong lyn na \'n eie doflyn Verskafferorde terugstel? Versteek liriekreëls bo die reël wat tans speel Versteek ongespeelde lirieke @@ -599,10 +644,15 @@ Daar word op beide limiete toegepas. By 1.75 tel vier volbreedte-karakters as 7. Vertaal deur %s Geromaniseer deur %s Gromaniseer deur %s + Romaniseer deur Rime (%s) Me […] lirieke. […] :\"Lirieke\" Vertalingstaal - Romaniseringstaal + Romanisering of transliterasietaal + "Vertaling-teiken taal: +%s" + "Romanisering of transliterering teiken taal: +%s" Besig met vertaling… Kon nie die lirieke vertaal nie Lirieke-bron? @@ -616,7 +666,7 @@ Daar word op beide limiete toegepas. By 1.75 tel vier volbreedte-karakters as 7. Besig om te verfris Lirieke-bron? QuickLRC API-sleutel - QuickLRC transkribeer of tyd-alyneer die liedtekste vir die huidige snit. \'n Afsonderlike diens, unrelated tot die OpenAI-kompatibele eindpunt hierbo + QuickLRC transkribeer of tyd-alineeër die lirieke vir die huidige snit QuickLRC-sleutel gestoor. Gebruik die alynknoppie in die liedsoekdialoog, of die paneelaansporing wanneer \'n snit geen liedtekste het nie QuickLRC API-sleutel gestoor QuickLRC het daardie sleutel nie aanvaar nie @@ -632,58 +682,50 @@ Daar word op beide limiete toegepas. By 1.75 tel vier volbreedte-karakters as 7. Geen liedtekste om te alyn nie - transkribeer eerder QuickLRC kon nie hierdie snit verwerk nie QuickLRC-krediete is vir hierdie snit gebruik - "Lirieke word ooreengestem volgens titel en kunstenaar; ongewone metadata kan dalk niks vind of die verkeerde weergawe oplewer. Gebruik verfris om wisselweergawes te omskakel. Verskaffers wat met * gemerk is, vereis 'n tokken. Nie alle geaktiveerde verskaffers word altyd in jou volgorde versoek nie, maar hulle reageer volgens hul spoed. + "Liriek word ooreenkomstig titel en kunstenaar gekoppel, ongewone metagegevens mag niks of die verkeerde weergawe vind. Gebruik vernuwingsknoppie om tussen weergawes te skakel. Verskaffers gemerk * vereis 'n token. Nie al aktief verskaffers word altyd in jou volgorde aangevra nie, maar reageer gebaseer op hul spoed. -Lirieke kom van diensverskaffers van derde partye wat nie aan Morphe gelink, deur Morphe gesponsor of deur Morphe goedgekeur is nie. Elke een word soos dit is gebruik: die lirieke, hul beskikbaarheid en hul lisensiëring bly by daardie diensverskaffers en hul licentiehouers, en die gebruik daarvan kan onderhewig wees aan hulle diensvoorwaardes. Wat aan 'n geaktiveerde diensverskaffer oorgedra word, is slegs die soektogwaarde wat jy versoek het - naamlik die titel, die kunstenaar en, waar diensverskaffer dit nodig het, die video-ID. Die opsionele AI-eindpunt en QuickLRC-transkripsie werk slegs nadat jy dit gekonfigureer het, en ontvang die liriecteks of die snit se video-URL." +Liriek kom van derdeparty diensverskaffers wat nie aan Morphe gelink, gesponsord of goedgekeur is nie. Elke een word soos dit is gebruik: die liriek, beskikbaarheid en lisensiëring bly by daardie diensverskaffers en hulle licentiegevers, en die gebruik daarvan kan onderhewig wees aan hul diensvoorwaardes. Wat na 'n geaktiveerde diens gestuur word, is slegs die soektog wat jy versoek het - die titel, die kunstenaar, en, waar diensverskaffer dit nodig het, die video-ID. Die opsionele AI-eindpunt en QuickLRC-transkripsie loop slegs nadat jy dit gekonfigureer het, en ontvang die liriekteks of die snit se video-URL." Aktiveer slegs die verskaffers wat u gebruik, of behou die verstek – konstante versoekings kan klein gemeenskapsliedjie-bladsye oorweldig Lirieke-bron? Lirieke word gesoek volgens kunstenaar en titel, […] Jy kan ook die verfris-knoppie probeer om handmatig van weergawe te wissel. […] Alle verskaffers word nie altyd in die bestelling versoek nie, maar reageer op grond van hul spoed Lirieke-bron? - Lirieke-bron? - Vertaling - Romanisering - Vertalingsverskaffer - Google (Standaard, Gratis) - OpenAI-versoenbaar (Pasgemaak) - Romanisasie-verskaffer - Google (Standaard) - Plaaslike / Offline Woordeboek - OpenAI-versoenbaar (Pasgemaak) - Kunsmatige Intelligensie-afdeling - Word slegs gebruik wanneer die verskaffer geen afdelinglabels soos Koors of Vers stuur nie, wat jou OpenAI-versoenbare model dan opspoor - KI & Wolke-eindpunte - Konfigureer API-sleutels, modelle, eindpunte en instruksie-sjablone vir KI-verskaffers. - API-eindpunt URL (bv. https://api.openai.com/v1/chat/completions) + AAN sit romanisering of transliterasie onder lirieke en vertaling bo; UIT hou romanisering of transliterasie bo en vertaling onder. \'n Lesing wat oor elke silabe plaas beweeg saam daarmee, van bo sy silabe na onder dit + Altyd lirieke sentreer + AAN plaas elke lyn in die middel en ignoreer die bronuitligting, insluitend duet-sye; UIT hou die uitligging wat die lirieke versoek + Ten minste een verskaffer moet aan bly + Vertaling + Romanisering of transliterasie + Afvalwoordeboek + Rime-Woordlys + Detecteer afdelings met AI + Slegs gebruik wanneer die verskaffer geen sektorlabelle soos Refrein of Strofe stuur nie, wat jou OpenAI-kompatibele model dan detecteer. Die redigering van die ry open ook sy spoortjie + API-eindpunt-URL (bv. https://api.openai.com/v1/chat/completions, https://api.anthropic.com/v1/chat/completions, https://generativelanguage.googleapis.com/v1beta/openai/chat/completions) Aanwysingtemplaat Kiesers: {language}, {task}, {title}, {artist}, {count}, {lines} Aanwysing gestoor Aansporing gestoor, maar die AI-eindpunt is nog nie bruikbaar nie. Stel dit eers onder AI-instellings in Aanwysing teruggestel - Nie geldige JSON nie. Reg dit of maak die veld skoon voor saving Vertalings- & Romanisering-instellings gestoor Instellings is teruggesit na verstek Basis-URL moet met http:// of https:// begin - Toets Konneksie - Konfigureer Eindpunte - Wys Alle Eindpunte + Instellings onvolledig of deur die verskapper verwerp Valideer verbinding… - Vertalingsverskaffer - Romaniseringsverskaffer - Haal Modelle op + %1$s: Aangesluit! (%2$s) + %1$s: Verbindingsmislukking. Kontroleer die instellings. + Gee die API-sleutel eerste Haal modelle op… Kies \'n Model Kon nie modelle ophaal nie. Gaan API-sleutel of konneksie na. - Gemini API-instellings - OpenAI-versoenbaar (Pasgemaakte Eindpunt) - DeepL API-instellings - API-sleutel + Spoortjie Model Basis-URL + Kies \'n diens API-sleutel (opsioneel) - Vertalingsopdrag Ekstra versoekliggaam (opsionele JSON) + Ekstra versoekhoofde (opsioneel JSON) + Sitoatname en waardes, bv. {\"api-key\": \"abc\"}. \'n Onbegrensde waarde wat / : of \'n getal bevat, word verwerp. \'n Leë waarde verwyder \'n hoofde. + Sitoatname en waardes, bv. {\"temperature\": 0.3}. \'n Onbegrensde waarde wat / of : bevat, word verwerp. Model en berigte kan hier nie gestel word nie. Outentiseringssleutel (Gratis of Pro) - Konfigureer API-sleutels, modelle, eindpunte en opdragte 8.23.51 - Deaktiveer vet ikone 9.13.51 - Herstel Speler Volgende, Lirieke en Verwante oortjies diff --git a/patches/src/main/resources/addresources/values-af-rZA/youtube/strings.xml b/patches/src/main/resources/addresources/values-af-rZA/youtube/strings.xml index 30c0ad7f3f..ae42236216 100644 --- a/patches/src/main/resources/addresources/values-af-rZA/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-af-rZA/youtube/strings.xml @@ -29,6 +29,8 @@ Second \"item\" text" Diverse Video Herstel ou YouTube instellings skerm + Vertoon boodskappe in YouTube-snackbar + Morphe-boodskappe, soos die bevestiging van \'n spelerknoppie, word in die YouTube-snackbar vertoon in plaas van \'n opspringvenster Verwyder agtergrond speel beperkings Deaktiveer agtergrondweergawe diff --git a/patches/src/main/resources/addresources/values-am-rET/music/strings.xml b/patches/src/main/resources/addresources/values-am-rET/music/strings.xml index 53d9d6d864..abe77b7f33 100644 --- a/patches/src/main/resources/addresources/values-am-rET/music/strings.xml +++ b/patches/src/main/resources/addresources/values-am-rET/music/strings.xml @@ -462,12 +462,10 @@ SABR የመጫወቻ ሂደት (የቪዲዮ ስትሪሞች ማስመሰል ተ ማሳያ ቅንጅት ፓነል - የ AI የመገናኛ ነጥብ ሁሉም አቅራቢዎች ተዘግተዋል - የተገነቡ ግጥሞች እና የተያያዙ ግጥሞች የአቅራቢዎችን ቅደም ተከተል እንደገና ላስተካክል? አቅራቢዎችን ወደነበሩበት መደበኛ የቅድሚያ ቅደም ተከተል መልስ የተቀመጡ ግጥሞችን አጥፋ - በዚህ መሳሪያ ላይ የተቀመጡ ግጥሞችን ይሰርዙ %1$s MB ተሰርዟል ቶክን ያግኙ የApple Music ሚዲያ-ተጠቃሚ-ቶክን @@ -527,14 +525,9 @@ SABR የመጫወቻ ሂደት (የቪዲዮ ስትሪሞች ማስመሰል ተ የቅጂ ቁልፍን አሳይ ኦሪጅናል ግጥሞችን እንደ ተራ ጽሑፍ ለመቅዳት ይንኩ፤ ጥሬ የግጥም ምንጭ ፋይልን ወደ ዳውንሎድስ ለማስቀመጥ ረጅም ተጭነው ይቆዩ የትርጉም ቁልፍን አሳይ - ትርጉሙን ለማብራት/ማጥፋት ይጫኑ፤ ለትርጉም ብቻ ለረጅም ጊዜ ይጫኑ። የአቅራቢ ትርጉሞችን፣ የGoogle Translateን ወይም ከOpenAI ጋር ተኳሃኝ የሆነ API ይጠቀማል - የሮማናይዝ በተን አሳይ - ሮማናይዜሽንን ለማብራት/ማጥፋት ይጫኑ፤ ለሮማናይዜሽን ብቻ ለረጅም ጊዜ ይጫኑ። የአቅራቢ ሮማናይዜሽንን፣ የGoogle Translateን ወይም ከOpenAI ጋር ተኳሃኝ የሆነ API ይጠቀማል - የአካባቢ ሮማናይዜሽን እና አሰላለፍ - አቅራቢው፣ የእርስዎ ሞዴል እና Google Translate ሳይሰሩ ሲቀሩ በመሳሪያው ላይ ሮማናይዜሽን ያድርጉ። ንባቡ በእያንዳንዱ የጊዜ ገደብ ስንኝ ቃል ይከተላል፤ የቃል ማመሳሰያ በርቶ መሆን አለበት። ቻይንኛ፣ ካንቶኒዝ፣ ጃፓናዊ፣ ኮሪያዊ፣ ግሪክ፣ ሩሲያዊ፣ ጆርጂያዊ፣ አርመንያዊ፤ ወደ 120 MB - %1$s በመነበብ ላይ ነው: %2$d%% ንባቦች ዝግጁ ናቸው (%1$d ዝርዝሮች) የማውረድ ሙከራ አልተሳካም - እንደገና ለመሞከር ይህንን አጥፍተው ያብሩት + %1$s በመነበብ ላይ ነው: %2$d%% የማደሻ በተን አሳይ ወደ ቀጣዩ የሊሪክስ እጩ ለመቀየር ይጫኑ፤ በዘፈን ርዕስ እና በድምፃዊ ስም ሊሪክስ ለመፈለግ ለረጅም ጊዜ ይጫኑ ግጥሞችን ፈልግ @@ -605,7 +598,6 @@ SABR የመጫወቻ ሂደት (የቪዲዮ ስትሪሞች ማስመሰል ተ ተተረጎመ የትርጉም ቋንቋ - የሮማናይዜሽን ቋንቋ በመተርጎም ላይ… ግጥሞቹን መተርጎም አልተቻለም ሮማይዝ ተደርጓል @@ -619,7 +611,6 @@ SABR የመጫወቻ ሂደት (የቪዲዮ ስትሪሞች ማስመሰል ተ እየታደሰ ነው ታድሷል QuickLRC API ቁልፍ - QuickLRC ለወቅታዊው ትራክ ግጥሞችን ይጽፋል ወይም የጊዜ ቅንጅት ያደርጋል። ይህም ከላይ ከተጠቀሰው OpenAI-ተኳሃኝ endpoint ጋር የማይገናኝ የተለየ አገልግሎት ነው የQuickLRC ቁልፍ ተቀምጧል። በግጥም ፍለጋው ዲያሎግ ውስጥ ያለውን የቅንጅት ቁልፍ፣ ወይም ትራኩ ግጥም በሌለውበት ጊዜ የሚመጣውን የፓነል ማሳሰቢያ ይጠቀሙ QuickLRC API ቁልፍ ተቀምጧል QuickLRC ያንን ቁልፍ አልተቀበለውም @@ -635,56 +626,27 @@ SABR የመጫወቻ ሂደት (የቪዲዮ ስትሪሞች ማስመሰል ተ የሚቀናጅ ግጥም የለም - በምትኩ እየተጻፈ ነው QuickLRC ይህንን ትራክ ማቀናበር አልቻለም ለዚህ ትራክ የ QuickLRC ክሬዲቶች አልቀዋል - "ግጥሞች በርዕስ እና በዘፋኝ ይዛመዳሉ፣ ያልተለመዱ ሜታዳታዎች ምንም ነገር ወይም የተሳሳተ ስሪት ሊያገኙ ይችላሉ። ስሪቶችን ለመቀየር ማደሻን (refresh) ይጠቀሙ። በ * ምልክት የተደረገላቸው አቅራቢዎች ቶከን ይፈልጋሉ። ሁሉም የነቃ አቅራቢዎች ሁልጊዜ በጠየቁት ቅደም ተከተል ላይ እንዲጠየቁ አይደረግም፣ ነገር ግን እንደ ፍጥነታቸው ምላሽ ይሰጣሉ።\n\nግጥሞች ከMorphe ጋር ግንኙነት በሌላቸው፣ በስፖንሰርሺፕ ወይም በድጋፍ በማይሰሩ ሶስተኛ ወገን አገልግሎቶች ይመጣሉ። እያንዳንዳቸው እንዳሉ ጥቅም ላይ ይውላሉ፡ ግጥሞቹ፣ ተገኝነታቸው እና የፈቃድ ስምምነታቸው በእነዚያ አገልግሎቶች እና ፈቃድ ሰጪዎቻቸው ዘንድ ይቀራሉ። እነሱን መጠቀምም በአገልግሎት ውላቸው ላይ የተመሰረተ ሊሆን ይችላል። ወደ ነቃው አገልግሎት የሚደርሰው የጠየቁት ፍለጋ ብቻ ነው - ርዕሱ፣ ዘፋኙ እና አገልግሎቱ በሚፈልግበት ጊዜ የቪዲዮ መታወቂያው (video ID)። አማራጭ የ AI endpoint እና የ QuickLRC ትራንስክሪፕሽን የሚሰሩት እርስዎ ካ yapıዳቸው በኋላ ብቻ ነው፣ እና የግጥሞቹን ጽሑፍ ወይም የትራኩን የቪዲዮ URL ይቀበላሉ።" የሚጠቀሙባቸውን አቅራቢዎች ብቻ ያብሩ፣ ወይም ነባሩን ይጠቀሙ - ተከታታይ ጥያቄዎች ትናንሽ የማህበረሰብ ግጥም ድረ-ገጾችን ሊያጨናንቁ ይችላሉ የብድር መስመሩን ደብቅ ከግጥሞቹ በታች የሚታየውን የብድር መስመር ደብቅ ትርጉም እና ሮማናይዜሽንን ቀያይር - ON ሮማናይዜሽንን ከግጥሞች በታች እና ትርጉሙን በላይ ያደርጋል፤ OFF ሮማናይዜሽንን በላይ እና ትርጉሙን በታች ያደርጋል - ትርጉም - ሮማናይዜሽን - የትርጉም አቅራቢ - Google (መደበኛ፣ ነፃ) - ከOpenAI ጋር ተኳሃኝ (ብጁ) - የሮማናይዜሽን አቅራቢ - Google (መደበኛ) - የአካባቢ / ያለ መስመር ዲክሽነሪ - ከOpenAI ጋር ተኳሃኝ (ልዩ) - የ AI ክፍል - አቅራቢው እንደ Chorus ወይም Verse ያሉ የክፍል ምልክቶችን ካላቀረበ እና የ OpenAI-ተኳሃኝ ሞዴልዎ እነዚህን ሲያገኝ ብቻ ጥቅም ላይ ይውላል - AI እና የክላውድ Endpoints - ለ AI አቅራቢዎች የ API ቁልፎችን፣ ሞዴሎችን፣ ኤንድፖይንቶችን እና የ prompted ንድፎችን ያዋቅሩ። - የ API ኤንድፖይንት URL (ለምሳሌ https://api.openai.com/v1/chat/completions) የPrompt ንድፍ ቦታ ማስያዣዎች: {language}, {task}, {title}, {artist}, {count}, {lines} መመሪያው ተቀምጧል ማሳሰቢያው ተቀምጧል፣ ነገር ግን የAI endpoint ገና ጥቅም ላይ ሊውል አይችልም። በመጀመሪያ በAI Settings ውስጥ ያዋቅሩት መመሪያው ተቀንሷል - ትክክለኛ JSON አይደለም። ከመቀመጡ በፊት ያስተካክሉት ወይም መስኩን ያጽዱት የትርጉም & የሮማናይዜሽን ቅንብሮች ተቀምጠዋል ቅንብሮች ወደ መነሻው ተመልሰዋል መሰረታዊ URL በ http:// ወይም https:// መጀመር አለበት - ግንኙነቱን ይሞክሩ - ኤንድፖይንቶችን ያዋቅሩ - ሁሉንም ኤንድፖይንቶች አሳይ ግንኙነት እየተረጋገጠ ነው… - የትርጉም አቅራቢ - የሮማናይዜሽን አቅራቢ - ሞዴሎችን አምጣ ሞዴሎች እየተፈለጉ ነው… ሞዴል ይምረጡ ሞዴሎችን ማምጣት አልተቻለም። እባክዎ የAPI ቁልፉን ወይም ግንኙነቱን ያረጋግጡ። - የGemini API ቅንብሮች - OpenAI ተኳሃኝ (ልዩ Endpoint) - የDeepL API ቅንብሮች - የAPI ቁልፍ ሞዴል መሰረታዊ URL የAPI ቁልፍ (አማራጭ) - የትርጉም መጠየቂያ ተጨማሪ የጥያቄ አካል (አማራጭ JSON) የማረጋገጫ ቁልፍ (ነፃ ወይም ፕሮ) - የAPI ቁልፎችን፣ ሞዴሎችን፣ የመጨረሻ ነጥቦችን እና መጠየቂያዎችን ያዋቅሩ 8.23.51 - ደመቅ ያሉ ስዕላዊ ምልክቶችን አጥፋ 9.13.51 - የቀጥሎቹ፣ የዘፈን ግጥሞች እና ተዛማጅ ታቦችን መልስ diff --git a/patches/src/main/resources/addresources/values-ar-rSA/music/strings.xml b/patches/src/main/resources/addresources/values-ar-rSA/music/strings.xml index e6841edb23..7970bde80d 100644 --- a/patches/src/main/resources/addresources/values-ar-rSA/music/strings.xml +++ b/patches/src/main/resources/addresources/values-ar-rSA/music/strings.xml @@ -462,12 +462,47 @@ tظهر المسارات التي تم تنزيلها هنا" العرض المزامنة لوحة - نقطة نهاية الذكاء الاصطناعي جميع مزودي الخدمة معطّلون - كلمات الأغاني المدمجة وكلمات الأغاني المضمنة إعادة تعيين ترتيب المزوّدين؟ استعادة الطلب الافتراضي للمزوّدين. + قائمة كلمات Rime + "قائمة كلمات ريم تتطلب أيضاً UniDict: في مزوّد القاموس غير المتصل، حدّد \"محرفي الصينية والكantonese (UniDict)\" ودعّه ينتهي من التحميل أولاً. بدونه، القائمة التي تقرأ المحرفات لن تنتج أي قراءة. + +حدّد القوائم التي يجب أن تقرأها قائمة كلمات ريم، ثم اضغط حفظ لتحميلها. تتم إزالة القائمة التي أضفتها عندما تلغي تحديدك لها. + +كل قائمة هي جدول خاص بك للكلمة-إلى-القراءة، ملف .dict.yaml مع أسطر مثل: +字 ham +典 din2 +العمود الأول هو الكلمة، والثاني قراءتها؛ يتم تجاهل الأعمدة والأسطر الإضافية بدون علامة تبويب." + لم يتم تحديد أي قائمة كلمات Rime للغة المذكورة أعلاه. انقر على هذا الاسم لتحديد واحد، أو اختر لغة تحتوي على قائمة + إضافة + إضافة قائمة كلمات Rime + ملف .dict.yaml، أو أرشيف zip يحتوي على واحد، مع أسطر مثل 字 ham و 典 din2. يتم جلب القائمة عندما تحددها وتضغط حفظ، وترخيصها خاص بك لفحصه + رمز اللغة + اثنان أو ثلاثة أحرف، كما في hak أو wuu + الاسم + يظهر تحت القراءة، كما في Hakka (Meixian) + عنوان قائمة الكلمات + عنوان https:// لملف .dict.yaml أو ملفه المضغوط + أحرف ليست ضمن كلمة + تلقائي + أحرف كانتونية + أحرف ماندرين + لا توجد قراءة للحرف + املأ الرمز والاسم والعنوان + يجب أن يتكون الرمز من حرفين أو ثلاثة أحرف، كما في hak + يجب أن يبدأ العنوان بـ https:// + لا يمكن أن يحتوي الاسم والعنوان على خط عمودي + %1$s يمتلك بالفعل جدول كلمات + %1$s يتم قراءته بواسطة المترجم الروماني المدمج ولا يمكن أن يقبل قائمة كلمات + غير مستخدم: هذه اللغة تمتلك بالفعل قراءتها الخاصة، لذا قم بإلغاء تحديدها لإزالة ذلك + تمت إضافة %1$s - حدده وانقر فوق حفظ لتحميله + قوائم الكلمات جاهزة + قوائم الكلمات جاهزة (%1$d إدخال) + بناء قائمة القراءة: %1$d%% + فشل تحميل قائمة الكلمات - انقر فوق حفظ مرة أخرى للمحاولة مجددًا مسح كلمات الأغاني المخزنة - حذف الكلمات المحفوظة على هذا الجهاز + احذف %1$s ميجابايت من كلمات الأغنية المخزنة على هذا الجهاز تم مسح %1$s ميجابايت الحصول على الرمز Apple Music Media-user-token @@ -512,14 +547,22 @@ tظهر المسارات التي تم تنزيلها هنا" إظهار زر النسخ انسخ نص الكلمات الأصلي كنص عادي؛ اضغط مطولاً لحفظ ملف مصدر الكلمات الخام إلى مجلد التنزيلات إظهار زر الترجمة - انقر لتبديل الترجمة؛ اضغط مطولاً للترجمة فقط. يستخدم ترجمات المزود، ترجمة Google، أو واجهة برمجة متوافقة مع OpenAI - إظهار زر الرومنة - انقر لتبديل الكتابة بالأحرف اللاتينية؛ اضغط مطولاً للكتابة بالأحرف اللاتينية فقط. يستخدم تهجئة مزودة، ترجمة Google، أو واجهة برمجة متوافقة مع OpenAI - التهجئة المحلية والمزامنة - قم بالتهجئة على الجهاز عندما يفشل المزود والنموذج الخاص بك وترجمة Google. يتبع القراءة كل كلمة من كلمات الزمنية؛ يجب تفعيل مزامنة الكلمات. الصينية، الكانتونية، اليابانية، الكورية، اليونانية، الروسية، الجورجية، الأرمينية؛ حوالي 120 ميجابايت - جاري قراءة %1$s: %2$d%% + انقر للتبديل بين الترجمة؛ اضغط مطولاً للترجمة فقط. يستخدم مقدمي الترجمة المفعّلين بالترتيب المحدد + إظهار زر التعريب أو النقل الحرفي + انقر للتبديل بين التعريب والنقل الحرفي؛ اضغط مطولاً للتعريب أو النقل الحرفي فقط. يستخدم مقدمي التعريب المفعّلين بالترتيب المحدد + تمكين النقل الحرفي + السماح بالقراءات غير اللاتينية من مصدر كلمات الأغنية؛ عند تعطيله، احتفظ فقط بالحروف اللاتينية والحروف المشدّدة وأرقام النغمة + تقرأ كل كلمة من كلمات الأغنية الزمنية (مزامنة الكلمات مفعّلة)؛ لا تحتاج الكورية إلى تحميل؛ تسعة عشر كتابة تشترك في تنزيل واحد من CLDR؛ تُقرأ الأحرف الهانية بقائمة كلمات Rime عندما يغطي جدول الكلمات اللغة + جارٍ تحميل قواميس القراءة… القراءات جاهزة (%1$d إدخال) فشل التنزيل - قم بإيقاف هذا الأمر وتشغيله مرة أخرى للمحاولة مجددًا + قاموس غير متصل + اليابانية (SudachiDict، ~75 ميغابايت) + الأحرف الصينية والكانتونية (UniDict، ~8 ميغابايت) + الكلمات الصينية (pinyin-pro، ~17 ميغابايت) + النقل الحرفي للغات اليونانية والسلافية السييريانية (بما في ذلك الروسية والأوكرانية والبلغارية والصربية والبيلاروسية والمقدونية)، والجورجية والأرمنية والعبرية والعربية والعفرية والسريانية (CLDR، ~0.5 ميغابايت) + جاري قراءة %1$s: %2$d%% + اختر قاموسًا واحدًا على الأقل إظهار زر التحديث انقر للتبديل إلى مرشح الكلمات التالي؛ اضغط مطولاً للبحث عن الكلمات بعنوان الأغنية والفنان البحث عن الكلمات @@ -530,8 +573,10 @@ tظهر المسارات التي تم تنزيلها هنا" محفوظة قم بتمييز الكلمات والمقاطع اللفظية المتزامنة أعطِ الأولوية للكلمات المتزامنة كلمةً بكلمة عند توفرها. وعندما تحمل الكلمات توقيتًا للكلمات، يتم تمييز كل كلمة أو مقطع في توقيت مع الموسيقى. - إظهار الأغاني الخلفية بشكل منفصل - يُحرك غناء الخلفية المكتوب بين قوسين، مثل \"(واو) أريدك\"، من سطر الغناء إلى سطر خاص به بأقل وضوح + تقليص الأسطر غير النشطة + يرسم كل سطر ليس جزءًا من الغناء الحالي بشكل أصغر قليلاً، فوق تعتيمه + إظهار الأصوات الخلفية بشكل منفصل + يفصل الأصوات الخلفية والتوافق عن السطر الرئيسي. ينقل صوت خلفية مكتوب بين أقواس، مثل \"(أوه) أريدك\"، خارج السطر المغني إلى سطر أكثر تعتيمًا خاصًا به إخفاء السطر الجاري تشغيله إخفاء سطور الكلمات الموجودة فوق السطر الحالي إخفاء الكلمات غير المشغّلة @@ -589,10 +634,15 @@ tظهر المسارات التي تم تنزيلها هنا" تمت الترجمة بواسطة %s تمت الكتابة بالحروف اللاتينية بواسطة %s تم تحويل الحروف اللاتينية بواسطة %s + تم التعريب بواسطة Rime (%s) مترجَم لغة الترجمة - لغة التحويل الحروفي اللاتيني + لغة التعريب أو النقل الحرفي + "لغة هدف الترجمة: +%s" + "لغة هدف الإملاء الصوتي أو النقل الحرفي: +%s" جارٍ الترجمة... تعذر ترجمة كلمات الأغنية مُروَّن @@ -606,7 +656,7 @@ tظهر المسارات التي تم تنزيلها هنا" جارٍ التحديث تم التحديث مفتاح واجهة برمجة تطبيقات QuickLRC - يقوم QuickLRC بالنسخ أو المحاذاة الزمنية لنصوص الأغاني للمسار الحالي. خدمة منفصلة وغير مرتبطة بنقطة النهاية المتوافقة مع OpenAI المذكورة أعلاه + يقوم QuickLRC بنسخ كلمات الأغنية الحالية زمنيًا أو مزامنتها تم حفظ مفتاح QuickLRC. استخدم زر المحاذاة في مربع حوار بحث النصوص، أو تلميح اللوحة عندما يكون المسار بلا نصوص تم حفظ مفتاح واجهة برمجة تطبيقات QuickLRC رفض QuickLRC ذلك المفتاح @@ -622,58 +672,50 @@ tظهر المسارات التي تم تنزيلها هنا" لا توجد نصوص للمحاذاة - جاري النسخ بدلاً من ذلك تعذّر على QuickLRC معالجة هذا المسار نفد رصيد QuickLRC لهذا المسار - "تتم مطابقة كلمات الأغاني حسب العنوان والفنان، وقد لا تؤدي البيانات الوصفية غير المعتادة إلى العثور على شيء أو النسخة الخاطئة. استخدم التحديث للتبديل بين النسخ. تتطلب المزوّدون المذكورون بعلامة * رمز وصول. لا يتم طلب جميع المزودين المفعّلين دائماً بالترتيب الذي تحدده، بل تستجيب بناءً على سرعتها. + "يتم مطابقة الكلمات المفتاحية بالعنوان والفنان، قد لا يجد البيانات الوصفية غير العادية شيئاً أو النسخة الخاطئة. استخدم تحديث للتبديل بين النسخ. المزوّدون المعلمّون بـ * يتطلبون رمزاً. جميع المزوّدين المفعّلين ليسوا دائماً مُطلَبين بالترتيب الذي حددته، بل يستجيبون بناءً على سرعتهم. -تأتي كلمات الأغاني من خدمات طرف ثالث غير تابعة لـ Morphe ولا ترعاها أو تؤيدها. يتم استخدام كل منها كما هي: تبقى كلمات الأغاني وتوافرها وترخيصها لدى هذه الخدمات ومنحِي تراخيصها، وقد يخضع استخدامها لشروط الخدمة الخاصة بها. ما يصل إلى الخدمة المفعّلة هو فقط البحث الذي طلبته - العنوان، والفنان، ومعرّف الفيديو حيث تحتاجه الخدمة. نقطة النهاية الاختيارية للذكاء الاصطناعي ونسخ QuickLRC تعمل فقط بعد تكوينها، وتتلقى نص كلمات الأغاني أو عنوان URL لفيديو المسار." +تأتي الكلمات المفتاحية من خدمات تابعة لجهات خارجية غير مرتبطة أو مدعومة أو معتمدة من قبل Morphe. يُستخدم كل منها كما هو: تبقى الكلمات المفتاحية، ومدى توفرها، والترخيص الخاص بها لدى تلك الخدمات ومنح التراخيص لها، واستخدامها يمكن أن يكون خاضعاً لشروط الخدمة الخاصة بها. ما يصل إلى خدمة مفعّلة هو فقط الاستعلام الذي طلبته - العنوان، والفنان، ومعرّف الفيديو حيث تحتاجه تلك الخدمة. نقطة نهاية الذكاء الاصطناعي الاختيارية والنسخ السريع QuickLRC تعمل فقط بعد تكوينها، وتتلقى نص الكلمات المفتاحية أو عنوان URL للفيديو للمسار." قم بتمكين مزوّدي الخدمة الذين تستخدمهم فقط، أو احتفظ بالإعدادات الافتراضية - يمكن أن تؤدي الطلبات المستمرة إلى إرهاق مواقع الكلمات الخاصة بالمجتمعات الصغيرة إخفاء سطر الإسناد إخفاء سطر الإسناد الظاهر أسفل الكلمات. تبديل موضع الترجمة والرومنة - عند التشغيل: تكون الرومنة أسفل الكلمات والترجمة أعلاها. عند الإيقاف: تبقى الرومنة أعلى الكلمات والترجمة أسفلها. - الترجمة - الرومنة - مزوّد الترجمة - جوجل (افتراضي، مجاني) - متوافق مع OpenAI (مخصص) - مزوّد الرومنة - جوجل (افتراضي) - قاموس محلي / بدون اتصال - متوافق مع OpenAI (مخصص) - قسم الذكاء الاصطناعي - يُستخدم فقط عندما لا يوفر المزود تسميات للأقسام مثل Chorus أو Verse، والتي يقوم نموذجك المتوافق مع OpenAI باكتشافها بعد ذلك - الذكاء الاصطناعي & نقاط نهاية السحابة - تكوين مفاتيح API، والنماذج، ونقاط النهاية، وقوالب المطالبات لمزوّدي الذكاء الاصطناعي. - عنوان URL لنقطة نهاية API (على سبيل المثال https://api.openai.com/v1/chat/completions) + وضع ON يضع التعريب أو النقل الحرفي أسفل كلمات الأغنية والترجمة أعلى؛ وضع OFF يحافظ على التعريب أو النقل الحرفي أعلى والترجمة أسفل. يتم نقل القراءة الموضوعة فوق كل مقطعي معها، من فوق المقطع إلى أسفله + توسيط كلمات الأغنية دائمًا + وضع ON يضع كل سطر في المنتصف ويتجاهل محاذاة المصدر، بما في ذلك جوانب الثنائي؛ وضع OFF يحافظ على المحاذاة التي تطلبها كلمات الأغنية + يجب أن يبقى مقدم خدمة واحد على الأقل مفعّلًا + الترجمة + التعريب أو النقل الحرفي + قاموس غير متصل + قائمة كلمات Rime + اكتشاف الأقسام باستخدام الذكاء الاصطناعي + يستخدم فقط عندما لا يقدم مقدم الخدمة أي تسميات للأقسام مثل الجوقة أو الآية، والتي يكتشفها نموذجك المتوافق مع OpenAI بعد ذلك. تعديل الصف يفتح أيضًا تعليماته + URL نقطة نهاية API (مثل https://api.openai.com/v1/chat/completions, https://api.anthropic.com/v1/chat/completions, https://generativelanguage.googleapis.com/v1beta/openai/chat/completions) قالب المطالبة العناصر النائبة: {language}, {task}, {title}, {artist}, {count}, {lines} تم حفظ المطالبة تم حفظ الموجه، لكن نقطة نهاية الذكاء الاصطناعي غير صالحة للاستخدام بعد. قم بتكوينها تحت إعدادات الذكاء الاصطناعي أولاً تم إعادة تعيين المطالبة - ليس JSON صالحاً. أصلحه أو امسح الحقل قبل الحفظ تم حفظ إعدادات الترجمة وRomanization إعادة تعيين الإعدادات إلى الوضع الافتراضي يجب أن يبدأ عنوان URL الأساسي بـ http:// أو https:// - اختبار الاتصال - تكوين نقاط النهاية - إظهار جميع نقاط النهاية + الإعدادات غير مكتملة أو تم رفضها من قبل المزود جارٍ التحقق من الاتصال… - مزوّد الترجمة - مزوّد الرومنة - جلب النماذج + %1$s: متصل! (%2$s) + %1$s: فشل الاتصال. تحقق من الإعدادات. + أدخل مفتاح API أولاً جاري جلب النماذج... اختر نموذجاً فشل جلب النماذج. تحقق من مفتاح API أو الاتصال. - إعدادات Gemini API - متوافق مع OpenAI (نقطة نهاية مخصصة) - إعدادات DeepL API - مفتاح API + التعليمات النموذج عنوان URL الأساسي + اختر خدمة مفتاح API (اختياري) - تلقين الترجمة جسم طلب إضافي (JSON اختياري) + رؤوس الطلب الإضافية (JSON اختياري) + قم بوضع علامات الاقتباس على الأسماء والقيم، على سبيل المثال {\"api-key\": \"abc\"}. يتم رفض القيمة غير المشمولة بعلامتي اقتباس تحتوي على / : أو رقم. القيمة الفارغة تزيل الرأس. + قم بوضع علامات الاقتباس على الأسماء والقيم، على سبيل المثال {\"temperature\": 0.3}. يتم رفض القيمة غير المشمولة بعلامتي اقتباس تحتوي على / أو : Model و messages لا يمكن تعيينهما هنا. مفتاح المصادقة (مجاني أو احترافي) - تكوين مفاتيح API، والنماذج، ونقاط النهاية، والتلقينات 8.23.51 - تعطيل الأيقونات العريضة 9.13.51 - استعادة علامات تبويب المشغل: التالي وكلمات الأغاني والمرتبطة diff --git a/patches/src/main/resources/addresources/values-ar-rSA/youtube/strings.xml b/patches/src/main/resources/addresources/values-ar-rSA/youtube/strings.xml index af86b8693c..e138e39868 100644 --- a/patches/src/main/resources/addresources/values-ar-rSA/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ar-rSA/youtube/strings.xml @@ -29,6 +29,8 @@ Second \"item\" text" خيارات متنوعة الفيديو استعادة شاشة إعدادات YouTube القديمة + عرض الرسائل في شريط تنبيهات YouTube + يتم عرض رسائل Morphe، مثل تأكيد زر المشغل، في شريط تنبيهات YouTube بدلًا من ظهور نافذة منبثقة إزالة قيود التشغيل في الخلفية تعطيل التشغيل في الخلفية diff --git a/patches/src/main/resources/addresources/values-as-rIN/music/strings.xml b/patches/src/main/resources/addresources/values-as-rIN/music/strings.xml index 6fca42d6b7..e239ae5a60 100644 --- a/patches/src/main/resources/addresources/values-as-rIN/music/strings.xml +++ b/patches/src/main/resources/addresources/values-as-rIN/music/strings.xml @@ -462,12 +462,10 @@ SABR প্লেবেক (স্পুফ ভিডিঅ’ ষ্ট্ৰ প্ৰদৰ্শন চিঙ্ক্ৰোনাইজেচন পেনেল - AI এন্ডপইণ্ট সকলো প্ৰদানকাৰী নিষ্ক্ৰিয় কৰা হৈছে - বিল্ট-ইন লিৰিক্স আৰু এমবেডড লিৰিক্স প্ৰোভাইডাৰৰ ক্ৰম ৰিছেট কৰিবনে? প্ৰোভাইডাৰসমূহক ডিফল্ট অগ্ৰাধিকাৰ ক্ৰমত পুনৰুদ্ধাৰ কৰক সংৰক্ষিত কথাৰ স্তৰবোৰ মুছক - এই ডিভাইচত ৰখা গীতৰ কথাখিনি মচি পেলাওক %1$s MB মুছা হ\'ল টোকেন লাভ কৰক Apple Music Media-user-token @@ -527,14 +525,9 @@ SABR প্লেবেক (স্পুফ ভিডিঅ’ ষ্ট্ৰ কপি বুটাম দেখুওক মূল গীতৰ কথাখিনি প্লেইন টেক্সট হিচাপে কপি কৰিবলৈ টিপক; ৰ’ ফাইলটো ডাউনলোডচত সংৰক্ষণ কৰিবলৈ দীঘলীয়া সময়লৈ টিপি ধৰক অনুবাদ বুটাম দেখুওক - অনুবাদটো সলনি কৰিবলৈ টিপক; কেৱল অনুবাদৰ বাবে দীঘলীয়া সময়লৈ টিপি ধৰক। ই প্ৰোভাইডৰ অনুবাদ, গুগল ট্ৰেন্সলেট, বা এটা OpenAI-সংগত API ব্যৱহাৰ কৰে - Romanize বুটামটো দেখুৱাওক - ৰোমানাইজেশ্যন সলনি কৰিবলৈ টিপক; কেৱল ৰোমানাইজেশ্যনৰ বাবে দীঘলীয়া সময়লৈ টিপি ধৰক। ই প্ৰোভাইডৰ ৰোমানাইজেশ্যন, গুগল ট্ৰেন্সলেট, বা এটা OpenAI-সংগত API ব্যৱহাৰ কৰে - স্থানীয় ৰোমানাইজেশ্যন আৰু এলাইনমেণ্ট - প্ৰদানকাৰী, আপোনাৰ মডেল আৰু গুগল অনুবাদ ব্যৰ্থ হ\'লে ডিভাইচত ৰোমানাইজ কৰক। পঢ়াটো এটা টাইমড লিৰিকৰ প্ৰতিটো শব্দৰ পিছত থাকে; শব্দ সিংক অন থাকিব লাগিব। চীনা, কেন্টনিজ, জাপানী, কোৰীয়ান, গ্ৰীক, ৰুছিয়ান, জৰ্জিয়ান, আৰমেনিয়ান; প্ৰায় ১২০ MB - %1$s পঢ়ি থকা হৈছে: %2$d%% পঢ়াবোৰ সাজু আছে (%1$d খন প্ৰৱেশ) ডাউনলোড বিফল হৈছে - পুনৰ চেষ্টা কৰিবলৈ ইয়াক বন্ধ কৰি আকৌ অন কৰক + %1$s পঢ়ি থকা হৈছে: %2$d%% Refresh বুটামটো দেখুৱাওক পিছৰ লিৰিক কেণ্ডিডেটলৈ যাবলৈ টেপ কৰক; গানৰ শিৰোনাম আৰু শিল্পীৰ দ্বাৰা লিৰিক বিচাৰিবলৈ লং-প্ৰেছ কৰক গীতৰ কথা খোজো @@ -605,7 +598,6 @@ SABR প্লেবেক (স্পুফ ভিডিঅ’ ষ্ট্ৰ অনুবাদ কৰা হৈছে অনুবাদৰ ভাষা - ৰোমানাইজেশ্যন ভাষা অনুবাদ কৰা হৈছে… গীতৰ কথাৰ অনুবাদ কৰিব নোৱাৰিল ৰোমানাইজ কৰা হৈছে @@ -619,7 +611,6 @@ SABR প্লেবেক (স্পুফ ভিডিঅ’ ষ্ট্ৰ ৰিফ্ৰেচ কৰা হৈছে ৰিফ্ৰেচ কৰা হ\'ল QuickLRC API কী - QuickLRC-এ বৰ্তমানৰ ট্ৰেকটোৰ বাবে লিৰিক্সসমূহ লিপিবদ্ধ কৰে বা টাইম-এলাইন কৰে। ই ওপৰৰ OpenAI-সংগত ইণ্ডপইণ্টৰ সৈতে সম্পৰ্কহীন এক পৃথক সেৱা QuickLRC কি সংৰক্ষণ কৰা হৈছে। লিৰিক্স চাৰ্চ ডায়ালগৰ এলাইন বুটামটো, অথবা ট্ৰেকটোত লিৰিক্স নাথাকিলে পেনেল প্ৰম্পটটো ব্যৱহাৰ কৰক QuickLRC API কী সংৰক্ষিত হৈছে QuickLRC-এ সেইটো কী গ্ৰহণ কৰা নাই @@ -635,56 +626,27 @@ SABR প্লেবেক (স্পুফ ভিডিঅ’ ষ্ট্ৰ সংগত কৰিবলৈ কোনো কথাৰ স্তৰ নাই - তাৰ পৰিৱৰ্তে লিপিবদ্ধ কৰা হৈছে QuickLRC-এ এই ট্ৰেকটো প্ৰচেছ কৰিব নোৱাৰিলে এই ট্ৰেকটোৰ বাবে QuickLRC ক্ৰেডিট শেষ হৈছে - "গীতৰ কথাখিনি শিৰোনাম আৰু শিল্পীৰ সৈতে মিলাৰ হয়, অস্বাভাবিক মেটাডাটাত একো নপাব পাৰে বা ভুল সংস্কৰণ পাব পাৰে। সংস্কৰণ সলনি কৰিবলৈ refresh ব্যৱহাৰ কৰক। * চিহ্নিত প্ৰদানকাৰীৰ বাবে এটা টোকেনৰ প্ৰয়োজন। সকলো সক্ৰিয় প্ৰদানকাৰীক সদায় আপোনাৰ ক্ৰম অনুসৰি অনুৰোধ কৰা নহয়, বৰঞ্চ তেওঁলোকৰ গতিৰ ওপৰত ভিত্তি কৰি সঁহাৰি দিয়া হয়।\n\nগীতৰ কথাখিনি তৃতীয় পক্ষৰ সেৱাৰ পৰা আহে যিবোৰ Morphe ৰ সৈতে সম্পৰ্কিত, স্পনচৰ কৰা বা অনুমোদিত নহয়। প্ৰতিটো সেৱাক ইয়াৰ বৰ্তমানৰ অৱস্থাতে ব্যৱহাৰ কৰা হয়: গীতৰ কথা, ইয়াৰ উপলব্ধতা আৰু ইয়াৰ লাইচেঞ্চিং সেই সেৱাসমূহ আৰু ইয়াৰ লাইচেনছদাতাসমূহৰ হাতত থাকে, আৰু সেইবোৰ ব্যৱহাৰ কৰিলে তেওঁলোকৰ সেৱাৰ চৰ্তৰ অধীনত হ'ব পাৰে। এটা সক্ৰিয় সেৱালৈ কেৱল আপুনি বিচৰা তথ্যখিনিহে পঠোৱা হয় - শিৰোনাম, শিল্পী, আৰু য'ত সেই সেৱাৰ প্ৰয়োজন হয়, তাত ভিডিঅ' আইডি। বিকল্প AI এনডপইণ্ট আৰু QuickLRC ট্ৰান্সক্ৰিপচন কেৱল আপুনি সেইবোৰ কনফিগার কৰাৰ পিছতহে চলে, আৰু গীতৰ কথা বা ট্ৰেকৰ ভিডিঅ' URL লাভ কৰে।" আপুনি ব্যৱহাৰ কৰা প্ৰদানকাৰকহে সক্ৰিয় কৰক, অথবা ডিফল্ট ৰাখক - ধাৰাবাহিক অনুৰোধে সৰু সম্প্ৰদায়ৰ লৰিক্স సైটবোৰক অতিৰিক্ত বোজা দিব পাৰে ক্ৰেডিট লাইন লুকুৱাওক গীতৰ তলত দেখুৱা ক্ৰেডিট লাইনটো লুকুৱাওক অনুবাদ আৰু ৰোমানাইজেশ্যন সলনি কৰক - ON কৰিলে ৰোমানাইজেশ্যন গীতৰ তলত আৰু অনুবাদ ওপৰত থাকে; OFF কৰিলে ৰোমানাইজেশ্যন ওপৰত আৰু অনুবাদ তলত থাকে - অনুবাদ - ৰোমানাইজেশ্যন - অনুবাদ প্ৰদানকাৰী - Google (ডিফল্ট, বিনামূলীয়া) - OpenAI সংগত (কষ্টম) - ৰোমানাইজেশান প্ৰদানকাৰী - Google (ডিফল্ট) - স্থানীয় / অফলাইন অভিধান - OpenAI সংগত (কষ্টম) - AI ছেকচন - কেৱল তেতিয়াই ব্যৱহাৰ কৰা হয় যেতিয়া প্ৰদানকাৰীয়ে Chorus বা Verse-ৰ দৰে কোনো ছেকচন লেবেল পঠিয়ায়, যাক আপোনাৰ OpenAI-সংগত মডেলটোৱে ধৰা পেলায় - AI & Cloud Endpoints - AI প্ৰদানকাৰীৰ বাবে API কিচ, মডেল, এন্ডপইণ্ট আৰু প্ৰম্ট টেম্পলেটসমূহ কনফিগাৰ কৰক। - API এন্ডপইণ্ট URL (যেনে https://api.openai.com/v1/chat/completions) প্ৰমপ্ট টেমপ্লেট প্লেচহোল্ডাৰসমূহ: {language}, {task}, {title}, {artist}, {count}, {lines} প্ৰমপ্ট সংৰক্ষিত হৈছে প্ৰম্পট সংৰক্ষণ কৰা হৈছে, কিন্তু AI ইণ্ডপইণ্টটো এতিয়াও ব্যৱহাৰযোগ্য নহয়। প্ৰথমে ইয়াক AI Settings-ৰ অধীনত কনফিগাৰ কৰক প্ৰমপ্ট ৰিছেট কৰা হৈছে - সঠিক JSON নহয়। সংৰক্ষণ কৰাৰ আগতে ইয়াক ঠিক কৰক বা ফিল্ডটো খালী কৰক অনুবাদ আৰু ৰোমানাইজেশান ছেটিংছ সংৰক্ষণ কৰা হৈছে ছেটিংছবোৰ ডিফল্টত ৰিছেট কৰা হৈছে Base URL টো http:// বা https:// ৰে আৰম্ভ হ\'ব লাগিব - সংযোগ পৰীক্ষা কৰক - এণ্ডপইণ্টসমূহ কনফিগাৰ কৰক - সকলো এণ্ডপইণ্ট দেখুৱাওক সংযোগ বৈধতা পৰীক্ষা কৰা হৈছে… - অনুবাদ প্ৰদানকাৰী - ৰোমানাইজেশ্যন প্ৰদানকাৰী - মডেলসমূহ আহৰণ কৰক মডেলসমূহ আহৰণ কৰা হৈছে… এটা মডেল বাছনি কৰক মডেলসমূহ আহৰণ কৰাত বিফল হৈছে। API কী বা সংযোগ পৰীক্ষা কৰক। - Gemini API ছেটিংছ - OpenAI সংগতিপূৰ্ণ (কষ্টম এণ্ডপইণ্ট) - DeepL API ছেটিংছ - API কী মডেল বেছ URL API কি (ঐচ্ছিক) - অনুবাদ প্ৰম্পট অতিৰিক্ত অনুৰোধ বডী (ঐচ্ছিক JSON) প্ৰমাণীকৰণ কি (Free বা Pro) - API কি, মডেল, এন্ডপইণ্ট আৰু প্ৰম্পটসমূহ কনফিগাৰ কৰক 8.23.51 - বোল্ড আইকনসমূহ নিষ্ক্ৰিয় কৰক 9.13.51 - প্লেয়াৰৰ Up next, Lyrics, আৰু Related টেবসমূহ পুনৰুদ্ধাৰ কৰক diff --git a/patches/src/main/resources/addresources/values-az-rAZ/music/strings.xml b/patches/src/main/resources/addresources/values-az-rAZ/music/strings.xml index 8a71045109..3ea4304727 100644 --- a/patches/src/main/resources/addresources/values-az-rAZ/music/strings.xml +++ b/patches/src/main/resources/addresources/values-az-rAZ/music/strings.xml @@ -462,12 +462,13 @@ Fon səsləndirmə zamanı analiz sinxronlaşdırılmasına əmin olmaq üçün Görüntü Sinxronizasiya Panel - AI son nöqtəsi Bütün təminatçılar qapadılıb - daxili mahnı sözləri və yerləşdirilən mahnı sözləri Təminatçı sırası sıfırlansın? Təminatçıları ilkin prioritet sırasına qaytar + Əlavə et + Ad + Birbaşa Saxlanılmış mahnı sözlərini təmizlə - Bu cihazda saxlanılan sözləri sil %1$s MB Təmizləndi Tokeni əldə et Apple Music Media-user-token @@ -527,14 +528,9 @@ Fon səsləndirmə zamanı analiz sinxronlaşdırılmasına əmin olmaq üçün Köçürmə düyməsini göstər Orijinal sözləri sadə mətn kimi köçürmək üçün toxun; işlənməmiş sözlərin mənbə faylını Yükləmələrə köçürmək üçün basıb saxla Tərcümə et düyməsini göstər - Tərcüməni dəyişmək üçün toxun; yalnız tərcümə üçün uzun bas. Təminatçı tərcümələrini, Google Tərcüməni və ya OpenAI uyğun API-sini istifadə edir - Oxunuş düyməsini göstər - Transkripsiyanı dəyişmək üçün toxun; yalnız transkripsiya üçün uzun bas. Təminatçı transkripsiyasını, Google Tərcüməni və ya OpenAI uyğun API-sini istifadə edir - Yerli Transkripsiya və Uyğunlaşdırma - Təminatçı, modeliniz və Google Tərcümə uğursuz olduqda cihazda transkripsiya et. Oxunuş zamanlanmış sözlərin hər birini izləyir; söz sinxronlaşdırılması aktiv olmalıdır. Çin, Kanton, Yapon, Koreya, Yunan, Rus, Gürcü, Erməni; təxminən 120 MB - %1$s oxunur: %2$d%% Oxunuşlar hazırdır (%1$d daxilolma) ​Yükləmə alınmadı - yenidən cəhd etmək üçün bunu təkrar bağla və aç + %1$s oxunur: %2$d%% Yeniləmə düyməsini göstər Növbəti mahnı sözləri variantına keçmək üçün toxun; mahnı başlığı və ifaçıya görə axtarmaq üçün uzun bas Mahnı sözlərini axtar @@ -546,7 +542,6 @@ Fon səsləndirmə zamanı analiz sinxronlaşdırılmasına əmin olmaq üçün Sinxronlaşdırılmış sözləri və ya hecaları vurğula Mümkün olduqda sözbəsöz sözlərə üstünlük verir. Sözlərdə zamanlama olduqda, musiqi ilə eyni vaxtda hər bir sözü və ya hecanı vurğulayır Fon vokallarını ayrıca göstər - ​\"(Oh) I want you\" kimi mötərizədə yazılmış arxa plan vokalını oxunan sətirdən çıxararaq ayrı, solğun sətirə keçirir Oxudulan sözləri gizlət Mövcud oxunan sətirdən yuxarıdakı söz sətirlərini gizlət Oxudulmamış sözləri gizlət @@ -607,7 +602,6 @@ Hər iki limitə tətbiq olunur. 1.75-də dörd tam enli simvol 7 kimi sayılır Tərcümə edildi Tərcümə dili - Transkripsiya dili Tərcümə olunur… Mahnı sözlərini tərcümə etmək olmadı Transkripsiya olundu @@ -621,7 +615,6 @@ Hər iki limitə tətbiq olunur. 1.75-də dörd tam enli simvol 7 kimi sayılır Yenilənir Yeniləndi QuickLRC API açarı - QuickLRC cari trek üçün mahnı sözlərini transkripsiya edir və ya vaxta görə uyğunlaşdırır. Yuxarıdakı OpenAI uyğun son nöqtəsi ilə əlaqəsi olmayan ayrı bir xidmətdir QuickLRC açarı saxlanıldı. Mahnı sözləri axtarışı dialoqundakı uyğunlaşdırma düyməsin və ya trekin mahnı sözləri olmadıqda panel sorğusunu istifadə et QuickLRC API açarı saxlanıldı QuickLRC bu açarı qəbul etmədi @@ -637,56 +630,28 @@ Hər iki limitə tətbiq olunur. 1.75-də dörd tam enli simvol 7 kimi sayılır Uyğunlaşdırmaq üçün söz yoxdur - əvəzində transkripsiya edilir QuickLRC bu treki emal edə bilmədi QuickLRC kreditləri bu trek üçün bitib - "Mahnı sözləri başlığa və ifaçıya görə uyğunlaşdırılır, qeyri-adi şərhi məlumatlar heç nə tapmaya və ya səhv versiyanı tapa bilər. Versiyaları dəyişmək üçün yeniləməni istifadə edin. * ilə işarələnmiş təminatçılar token tələb edir. Aktiv edilmiş bütün təminatçılar həmişə sizin ardıcıllığınızla sorğulanmır, öz sürətləri əsasında cavab qaytarır.\n\nMahnı sözləri Morphe ilə əlaqəsi, sponsorluğu və ya təsdiqi olmayan üçüncü tərəf xidmətlərindən gəlir. Hər biri olduğu kimi istifadə olunur: mahnı sözləri, onların əlçatanlığı və lisenziyalaşdırılması həmin xidmətlərin və onların lisenziyaçılarının mülkiyyətində qalır və onların istifadəsi həmin xidmətin şərtlərinə tabe ola bilər. Aktiv edilmiş xidmətə yalnız istədiyiniz axtarış çatır - başlıq, ifaçı və həmin xidmətin ehtiyacı olduqda video ID-si. Seçimə bağlı Sİ son nöqtəsi və QuickLRC transkripsiyası yalnız siz onları tənzimlədikdən sonra işləyir və mahnı sözləri mətnini və ya trekin video URL-ni qəbul edir." Yalnız istifadə etdiyiniz təminatçıları aktivləşdirin və ya standartı saxlayın - fasiləsiz sorğular kiçik icma mahnı sözləri saytlarını həddindən artıq yükləyə bilər Müəllif sətrini gizlət Mahnı sözlərinin altında göstərilən müəllif sətrini gizlət Tərcümə və transkripsiyanın yerini dəyiş - AÇIQ olduqda transkripsiya sözlərin altında və tərcümə yuxarıda göstərilir; QAPALI olduqda transkripsiya üstdə, tərcümə isə altda qalır - Tərcümə - Transkripsiya - Tərcümə təminatçısı - Google (İlkin, Pulsuz) - OpenAI Uyğunluğu (Fərdi) - Transkripsiya təminatçısı - Google (İlkin) - Yerli / Oflayn Lüğət - OpenAI Uyğunluğu (Fərdi) - Süni İntellekt Bölməsi - Yalnız təminatçının Chorus və ya Verse kimi bölmə etiketləri olmadıqda istifadə olunur, bundan sonra OpenAI uyğun modeliniz onları aşkarlayır - Süni İntellekt & Bulud Son Nöqtələri - Süni intellekt təminatçıları üçün API açarlarını, modelləri, son nöqtələri və sorğu şablonlarını tənzimlə. - API son nöqtəsi URL-si (məsələn, https://api.openai.com/v1/chat/completions) + Tərcümə Sorğu Şablonu Dəyişənlər: {language}, {task}, {title}, {artist}, {count}, {lines} Sorğu saxlanıldı Sorğu saxlanıldı, lakin AI son nöqtəsi hələ istifadəyə yararlı deyil. Əvvəlcə onu AI Tənzimləmələri aşağısında tənzimlə Sorğu sıfırlandı - Etibarsız JSON. Saxlamazdan əvvəl onu düzəlt və ya xananı təmizlə Tərcümə & Transkripsiya tənzimləmələri saxlanıldı Tənzimləmələr ilkin vəziyyətə qaytarıldı Əsas URL http:// və ya https:// ilə başlamalıdır - Bağlantını Yoxla - Son Nöqtələri Tənzimlə - Bütün Son Nöqtələri Göstər Bağlantı təsdiqlənir… - Tərcümə Təminatçısı - Transkripsiya Təminatçısı - Modelləri Əldə Et Modelləri əldə edir… Model Seç Modelləri əldə etmək olmadı. API açarını və ya bağlantını yoxla. - Gemini API Tənzimləmələri - OpenAI Uyğunluğu (Fərdi Son Nöqtəsi) - DeepL API Tənzimləmələri - API açarı Model Əsas URL API açarı (istəyə bağlı) - Tərcümə sorğusu Əlavə sorğu gövdəsi (istəyə bağlı JSON) Təsdiqləmə açarı (Pulsuz və ya Pro) - API açarlarını, modellərini, son nöqtələrini və sorğularını tənzimlə 8.23.51 - Qalın simvolları qeyri-aktiv et 9.13.51 - Oynadıcının Növbəti, Mahnı sözləri və Əlaqəli panellərini bərpa et diff --git a/patches/src/main/resources/addresources/values-be-rBY/music/strings.xml b/patches/src/main/resources/addresources/values-be-rBY/music/strings.xml index aba142390e..1eec6de01f 100644 --- a/patches/src/main/resources/addresources/values-be-rBY/music/strings.xml +++ b/patches/src/main/resources/addresources/values-be-rBY/music/strings.xml @@ -462,12 +462,11 @@ Second \"item\" text" Адлюстраванне Сінхранізацыя Панэль - AI-конечная кропка Усе пастаўшчыкі адключаны — убуяваны і ўбудаваны тэксты Скінуць парадак правайдэраў? Аднавіць правайдэраў да пачатковага парадка прыярытэтаў + Аўтаматычна Ачысціць захаваныя тэксты - Выдаліць тэксты, захаваныя на гэтай прыладнасці Ачышчана %1$s МБ Атрымаць токен Apple Music Media-user-token @@ -527,14 +526,9 @@ Second \"item\" text" Паказваць кнопку «Капіяваць» Націсніце, каб скапіяваць арыгінальны тэкст як звычайны тэкст; зацісніце, каб захаваць выходны файл тэксту ў Загрузкі Паказваць кнопку «Перакласці» - Націсніце, каб пераключаць пераклад; зацісніце для толькі перакладу. Выкарыстоўваюць пераклады пастаўшчыка, Google Translate або API, сумяшчальнае з OpenAI - Паказваць кнопку раманізацыі - Націсніце, каб пераключаць раманізацыю; зацісніце для толькі раманізацыі. Выкарыстоўваюць раманізацыю пастаўшчыка, Google Translate або API, сумяшчальнае з OpenAI - Локальная раманізацыя і выраўніванне - Раманізацыя на прыладзе, калі пастаўшчык, ваша мадэль і Google Translate не спрацавалі. Чытанне слідуе за кожным словам часовых тэкстаў песень; сінхранізацыя слоў павінна быць уключана. Кітайская, кантонская, японская, карейская, грэцкая, руская, грузінская, армянская мовы; прыкладна 120 МБ - Чытанне %1$s: %2$d%% Чытанні гатовыя (%1$d запісаў) Загрузка недакладна — выключыце і зноў уключыце гэтую функцыю, каб паўтарыць спробу + Чытанне %1$s: %2$d%% Паказваць кнопку абнаўлення Націсніце, каб пераключыцца на наступны варыянт тэксту; доўгае націсканне — каб пашукаць тэкст па назве песні і выканаўцы Пошук тэкстаў песень @@ -605,7 +599,6 @@ Second \"item\" text" Перакладзена Мова перакладу - Мова раманізацыі Пераклад... Не ўдалося перакласці тэкст песні Раманізавана @@ -619,7 +612,6 @@ Second \"item\" text" Абнаўленне Абнаўлена API-ключ QuickLRC - QuickLRC транскрыбуе або выраўняе па часе тэксты для бягучага трэка. Асобны сэрвіс, які не звязаны з OpenAI-сумесным эндпоінтам вышэй Ключ QuickLRC захаваны. Выкарыстоўвайце кнопку выраўняння ў дыялогу пошуку тэкстаў або паведамленне ў панэлі, калі ў трэка няма тэксту API-ключ QuickLRC захаваны QuickLRC не прыняў гэты ключ @@ -635,56 +627,27 @@ Second \"item\" text" Няма тэкстаў для выраўняння — замест гэтага выконваецца транскрыпцыя QuickLRC не змог апрацаваць гэты трэк Крэдыты QuickLRC для гэтага трэка вычарпаны - "Тэксты песень падабираюцца па назве і выканаўцы, незвычайныя метаданыя могуць прывесці да адсутнасці вынікаў або выбару няправільнай версіі. Выкарыстоўвайце абнаўленне, каб змяніць версію. Провайдэры, пазначаныя *, патрабуюць токена. Усе ўключаныя провайдэры не заўсёды запытваюцца ў вашай паслядоўнасці, а адказыўць залежнаць ад іх хуткасці.\n\nТэксты песень паступаюць з іншых сэрвісаў, якія не звязаны з Morphe, не спонсаруюцца і не падтрымліваюцца ім. Кожны з іх выкарыстоўваецца «як ёсць»: тэксты, іх даступнасць і ліцэнзаванне застаюцца за гэтымі сэрвісамі і іх ліцэнзіярами, а іх выкарыстанне можа рэгулявацца іх умовамі абслугоўвання. У ўключаны сэрвіс перадаецца толькі ваш запыт на пошук — назва, выканаўца і, калі гэта неабходна сэрвісу, ID відэа. Дапаможны AI-эндпоінт і транскрыпцыя QuickLRC працуюць толькі пасля таго, як вы іх наладзіце, і атрымліваюць тэкст песні або URL-адрес відэа трэка." Уключайце толькі тыя крыніцы, якімі карыстаецеся, альбо пакіньце налады па змаўчанні - пастаянныя запыты могуць перагрузіць невялікія супольнасці сайтаў з тэкстамі песень Схаваць інфармацыю аб аўтарах Схаваць інфармацыю аб аўтарах, якая паказваецца пад тэкстам песні Паменять месцамі пераклад і раманізацыю - ON ставіць раманізацыю пад тэкстам, а пераклад — над ім; OFF пакідае раманізацыю над тэкстам, а пераклад — пад ім - Пераклад - Раманізацыя - Пастаўшчык перакладу - Google (па змаўчанні, бясплатна) - Сумяшчальны з OpenAI (свой) - Пастаўшчык раманізацыі - Google (па deployment) - Локальны / па-афлайн слоўнік - Сумяшчальны з OpenAI (свой) - Секцыя ШІ - Выкарыстоўваецца толькі ў тым выпадку, калі правайдэр не перадае пазначэнняў секцый, такіх як прыспеў або куплет, якія затым вызначае ваша OpenAI-сумяшчальная мадэль - AI & хмарныя эндпоінтнты - Настройце API-ключы, мадэлі, эндпоінтнты і шаблоны прамптаў для пастаўшчыкаў ШІ. - URL-адрас эндпоінтнта API (напрыклад, https://api.openai.com/v1/chat/completions) Шаблон прампта Плейсхолдэры: {language}, {task}, {title}, {artist}, {count}, {lines} Прампт захаваны Запыт захаваны, але эндпоінт AI пакуль недаступны. Спачатку наладзьце яго ў Наладках AI Прампт скінуты - Некарэктны JSON. Выправіце яго або ачысціце поле перад захаваннем Налады перакладу & раманізацыі захаваны Налады скінуты да пачатковых Базавы URL павінен пачынацца з http:// або https:// - Праверыць злучэнне - Наладзіцьы канектныя пункты - Паказаць усе эндпоінты Праверка злучэння… - Правіднік перакладу - Правіднік раманізацыі - Атрымаць мадэлі Атрыманне мадэляў… Выберыце мадэль Не удалось атрымаць спіс мадэляў. Праверце API-ключ або падключэнне. - Налады Gemini API - Сумяшчальнасць з OpenAI (карыстальніцкі endpoint) - Налады DeepL API - API-ключ Мадэль Базавы URL API-ключ (неабавязкова) - Промпт для перакладу Дадатковае цела запыту (неабавязковы JSON) Ключ аўтэнтыфікацыі (Free або Pro) - Настройце API-ключы, мадэлі, эндпоінты і прампты 8.23.51 - Адключаць тлустыя іконкі 9.13.51 - Васстанавіць укладкі «Далей», «Тэксты песень» і «Падобнае» diff --git a/patches/src/main/resources/addresources/values-bg-rBG/music/strings.xml b/patches/src/main/resources/addresources/values-bg-rBG/music/strings.xml index ccb69b6829..0dfd2a6c6a 100644 --- a/patches/src/main/resources/addresources/values-bg-rBG/music/strings.xml +++ b/patches/src/main/resources/addresources/values-bg-rBG/music/strings.xml @@ -462,12 +462,47 @@ SABR възпроизвеждане (имитирането на видео по Показване Синхронизация Панел - AI крайна точка Всички доставчици са деактивирани - вградени текстовки и вградени текстове на песни Да се нулира редът на доставчиците? Възстановяване на реда по подразбиране на доставчиците. + Списък с думи Rime + "За Rime Wordlist е необходим и UniDict: в Offline Dictionary provider отбележете „Chinese and Cantonese characters (UniDict)“ и изчакайте да приключи изтеглянето. Без него списъкът с прочитани символи няма да има значение. + +Отбележете списъците, които Rime Wordlist трябва да чете, след което натиснете Save за изтегляне им. Списък, който сте добавили, се премахва, когато го отметнете. + +Всеки списък представлява ваша собственост таблица дума-превод, .dict.yaml с редове като: +字 ham +典 din2 +Първата колона е думата, втората нейният прочит; допълнителни колони и редове без табулация се игнорират." + No Rime word list is ticked for the language above. Tap this name to tick one, or pick a language that has a list + Добавяне + Добавете списък с думи Rime + .dict.yaml файл или архив zip, съдържащ такъв, с редове като 字 ham и 典 din2. Списъкът се зарежда, когато го отметнете и натиснете Save, а лицензът ви позволява да го проверите + Езиков код + Две или три букви, например hak или wuu + Име + Показва се под прочитането, например Хакка (Meixian) + Адрес на списъка с думи + https:// адрес на .dict.yaml или неговия архив zip + Знаци, които не са в дума + Автоматично + Кантонски знаци + Мандарински знаци + Без прочитане на знака + Попълнете кода, името и адреса + Кодът трябва да бъде две или три букви, например hak + Адресът трябва да започва с https:// + Името и адресът не могат да съдържат вертикална черта + %1$s вече има таблица със думи + %1$s се чете от вградената транслитерация и не може да използва списък с думи + Не се използва: този език вече има собствен прочит, затова махнете отметката, за да го премахнете + Добавен %1$s – отметнете го и натиснете Запазване, за да го изтеглите + Списъците с думи са готови + Готови списъци с думи (%1$d записа) + Изграждане на списъка за четене: %1$d%% + Изтеглянето на списъка с думи беше неуспешно – натиснете Запазване отново, за да опитате пак Изчистване на съхраняваните текстове - Изтриване на текстовете, запазени на това устройство + Изтрийте %1$s MB текстове, запазени на това устройство Изчистени %1$s MB Вземи токен Apple Music Media-user-token @@ -513,14 +548,22 @@ SABR възпроизвеждане (имитирането на видео по Покажи бутон Копиране Докосване за копиране на оригиналните текстове като обикновен текст; дълго натискане за запазване на изходния файл с текстове в Папки с изтегляния Покажи бутон Превод - Докосване за превключване на превода; дълго натискане само за превод. Използва преводи от доставчик, Google Translate или API, съвместим с OpenAI - Показване на бутона за романизиране - Докосване за превключване на транскрипция с латиница; дълго натискане само за транскрипция с латиница. Използва транскрипция от доставчик, Google Translate или API, съвместим с OpenAI - Местна транскрипция с латиница и синхронизиране - Транскрипция на устройството, когато доставчикът, вашият модел и Google Translate не работят. Четене следва всяка дума от синхронизиран по време текст; синхронизацията на думите трябва да е включена. Китайски, кантонски, японски, корейски, гръцки, руски, грузински, арменски; около 120 MB - Четене %1$s: %2$d%% + Натиснете, за да превключвате превода; докоснете и задръжте за превод само. Използва активните доставчици на превод в изброения ред + Показване на бутон за транслитерация или романизиране + Натиснете, за да превключвате романизирането или транслитерацията; докоснете и задръжте за романизиране или транслитерация само. Използва активните доставчици на романизация в изброения ред + Активиране на транслитерация + Разрешаване на не-латински прочити от източника на текст; когато е изключено, оставят се само латински букви, букви с диакритични знаци и цифри за тон + Чете всяка дума от синхронизирана по време песен (word sync включен); корейски не изисква изтегляне; четиринайсет скрипта споделят едно изтегляне CLDR; ханските знаци се четат от списъка с думи Rime, когато неговата таблица покрива езика + Изтегляне на речници за четене… Готови четения (%1$d записи) Изтеглянето бе неуспешно – изключете и включете отново, за да опитате повторно + Офлайн речник + Японски (SudachiDict, ~75 MB) + Китайски и кантонски знаци (UniDict, ~8 MB) + Китайски думи (pinyin-pro, ~17 MB) + Гръцки, славянски кирилски (вкл. руски, украински, български, сръбски, беларуски, македонски), грузински, арменски, еврейски, арабски, амхарски и сирийска транслитерация (CLDR, ~0.5 MB) + Четене %1$s: %2$d%% + Изберете поне един речник Показване на бутона за опресняване Докосване за превключване към следващ кандидат за текстове; дълго натискане за търсене на текстове по заглавие и изпълнител Търсене на текст @@ -531,8 +574,10 @@ SABR възпроизвеждане (имитирането на видео по Запазено Открояване на синхронизираните думи или срички Дава приоритет на текст по думи, когато е наличен. Когато текстът има тайминг на думите, откроява всяка дума или сричка в синхрон с музиката. - Показвай фоновите вокали отделно - Премества фонови вокали, написани в скоби, като „(Ох) искам те“, извън пеената линия към по-бляскава линия на собствената си + Намаляване на неактивните редове + Рисува всеки ред, който в момента не се пее, малко по-малък, следователно го затъмнява леко + Показване на фоновите вокали отделно + Отделя фоновите вокали и хармонията от водещия ред. Премества фонови вокали, записани в скоби, като „(О) искам те“, извън пеенето в свой собствен затъмнен ред Скриване на вече изпетите редове Скриване на редовете от текста над реда, който се изпълнява в момента. Скриване на неизпетите редове @@ -590,10 +635,15 @@ SABR възпроизвеждане (имитирането на видео по Преведено от %s Романизирано от %s Транслирано от %s + Романизирано от Rime (%s) Преведено Език на превода - Език за транслитерация + Език за романизация или транслитерация + "Език за превод: +%s" + "Език за транслитерация или транслитерация: +%s" Превежда се… Не може да преведе текста Романизирано @@ -607,7 +657,7 @@ SABR възпроизвеждане (имитирането на видео по Опресняване Опреснено API ключ за QuickLRC - QuickLRC транскрибира или времево синхронизира лирика на текущата песен. Отделна услуга, нереалатична към OpenAI-съвместимия интерфейс по-горе + QuickLRC транскрибира или времесвързва текстове за текущата парче Ключът QuickLRC е запазен. Използвайте бутона за синхронизиране в диалоговия прозорец за търсене на лирика или подсказката на панела, когато песен няма лирика QuickLRC API ключът е запазен QuickLRC не приема този ключ @@ -623,58 +673,50 @@ SABR възпроизвеждане (имитирането на видео по Няма лирика за синхронизиране – вместо това транскрибираме QuickLRC не мога да обработи тази песен Кредитите за QuickLRC са изчерпани за тази песен - "Текстовете се свързват по заглавие и изпълнител, необичайни метаданни може да не намерят нищо или грешна версия. Използвайте обновиране, за да превключвате версии. Доставчиците, маркирани със *, изискват токен. Не винаги всички активирани доставчици са поискани във вашата подредба, но реагират според своята скорост. + "Текстовете на песните се съпоставят по заглавие и изпълнител; необичайните метаданни може да не върнат резултат или да покажат грешната версия. Използвайте refresh за смяна на версиите. Доставчиците, означени с *, изискват токен. Всички активирани доставчици не винаги се заявяват във вашия ред, но отговарят според скоростта си. -Текстовете идват от услуги на трети страни, които не са афилиирани, спонсорирани или одобрени от Morphe. Всеки от тях се използва такъв, какъвто е: самите текстове, тяхната наличност и лицензионни права остават при тези услуги и техните лицензодатели, а използването им може да подлежи на техните условия за ползване. До активирана услуга достига само заявката за справка, която сте поискали – заглавието, изпълнителят и, където услугата го изисква, ID на видеото. По желание AI крайната точка и транскрипцията QuickLRC се стартират само след като ги конфигурирате, и получават текста на текстове или URL адреса на видеото на парчето." +Текстовете идват от услуги на трети страни, които нямат връзка с Morphe, не са спонсорирани или одобрени от него. Всяка от тях се използва както е предоставена: текстовете, наличността им и лицензирането остават при тези услуги и лицензиращите ги страни, а използването им може да подлежи на техните условия за ползване. До активираната услуга достига само заявката за справка - заглавието, изпълнителят и, където е необходимо, видео ID-то. Опитивият AI endpoint и транскрипцията QuickLRC се изпълняват само след като ги конфигурирате, и получават текста на песента или URL адреса на видеото на трака." Активирайте само доставчиците, които използвате, или запазете настройките по подразбиране – постоянните заявки могат да презатворят малките общностни сайтове за текстове Скриване на реда с кредитна информация Скриване на реда с кредитна информация, показан под текста. Размяна на превода и романизацията - ВКЛ. поставя романизацията под текста и превода над него; ИЗКЛ. оставя романизацията над текста и превода под него. - Превод - Романизация - Доставчик на преводи - Google (по подразбиране, безплатно) - Съвместимо с OpenAI (персонализирано) - Доставчик на романизация - Google (по подразбиране) - Локален / офлайн речник - Съвместимо с OpenAI (персонализирано) - AI секция - Използва се само когато доставчикът не предоставя етикети за секции като Припев или Куплет, които след това се откриват от вашия OpenAI-съвместим модел - AI & облачни крайни точки - Конфигурирайте API ключове, модели, крайни точки и шаблони за подкани за AI доставчици. - URL на API крайната точка (напр. https://api.openai.com/v1/chat/completions) + ВКЛ включва романизация или транслитерация под текстовe и превод над тях; ИЗКЛ пази романизация или транслитерация над и превод под. Прочит, поставен върху всяка сричка, се движи с нея – отгоре ѝ към отдолу + Винаги центрирай текста + ВКЛ поставя всеки ред в средата и игнорира подравняването от източника, включително страни дуета; ИЗКЛ запазва подравняването, което искат текстовете + Трябва да остане най-малко един активен доставчик + Превод + Романизация или транслитерация + Офлайн речник + Списък с думи Rime + Разпознаване на секции чрез AI + Използва се само когато доставчикът няма етикети за секции като Припев или Куплет, които тогава разпознава вашият модел, съвместим с OpenAI. Редактирането на реда също отваря неговия подсказка + URL адрес на API endpoint (напр. https://api.openai.com/v1/chat/completions, https://api.anthropic.com/v1/chat/completions, https://generativelanguage.googleapis.com/v1beta/openai/chat/completions) Шаблон за подкана Запълнители: {language}, {task}, {title}, {artist}, {count}, {lines} Подканата е запазена Подканването е запазено, но AI крайният接口 все още не е годен. Конфигурирайте го първо в Настройки на AI Подканата е нулирана - Невалиден JSON. Поправете го или изчистете полето преди запазване Настройките за превод & романизация са запазени Настройките са възстановени по подразбиране Базовият URL трябва да започва с http:// или https:// - Тест на връзката - Конфигуриране на крайни точки - Покажи всички крайни точки + Настройките са непълни или отхвърлени от доставчика Проверка на връзката… - Доставчик на превод - Доставчик на романизация - Извличане на модели + %1$s: Свързан! (%2$s) + %1$s: Свързването бе неуспешно. Проверете настройките. + Първо въведете API ключ Извличане на модели… Изберете модел Неуспешно извличане на моделите. Проверете API ключа или връзката. - Настройки на Gemini API - Съвместимо с OpenAI (собствен краен пункт) - Настройки на DeepL API - API ключ + Подсказка Модел Базов URL + Избор на услуга API ключ (по желание) - Подкана за превод Допълнително тяло на заявката (опционален JSON) + Допълнителни заглавки за заявка (по желание JSON) + Заключвайте именa и стойности в кавички, напр. {\"api-key\": \"abc\"}. Незаключена стойност, съдържаща / : или число, се отхвърля. Пrazna стойност премахва заглавка. + Заключвайте именa и стойности в кавички, напр. {\"temperature\": 0.3}. Незаключена стойност, съдържаща / или :, се отхвърля. Моделът и съобщенията не могат да бъдат задавани тук. Ключ за автентификация (Free или Pro) - Конфигуриране на API ключове, модели, крайни точки и подкани 8.23.51 - Деактивиране на удебелени икони 9.13.51 - Възстановяване на разделите Up next, Lyrics и Related diff --git a/patches/src/main/resources/addresources/values-bg-rBG/shared-youtube/strings.xml b/patches/src/main/resources/addresources/values-bg-rBG/shared-youtube/strings.xml index c9a8ba99e5..3fcaf5bd80 100644 --- a/patches/src/main/resources/addresources/values-bg-rBG/shared-youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-bg-rBG/shared-youtube/strings.xml @@ -21,9 +21,9 @@ Second \"item\" text" Анимация при стартиране Изключено Цвят - Morphe Color + Morphe Цвят Монохром - Morphe Monochrome + Morphe Монохромен - черно и бяло Маркиране на нови настройки Брой дни, през които се показва точка до настройките и опциите, добавени чрез актуализация diff --git a/patches/src/main/resources/addresources/values-bg-rBG/youtube/strings.xml b/patches/src/main/resources/addresources/values-bg-rBG/youtube/strings.xml index 11c0d6a76d..4e47f1446e 100644 --- a/patches/src/main/resources/addresources/values-bg-rBG/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-bg-rBG/youtube/strings.xml @@ -1267,8 +1267,8 @@ Second \"item\" text" Поддържа последователен макет на статус бара, така че той да не покрива горните ленти и да не оставя празно пространство над плеъра. Изключете тази опция, ако приложението се счупи или горните ленти изглеждат неправилно Сяяне - Morphe Glow 1 - Morphe Glow 2 + Morphe Блясък 1 + Morphe Блясък 2 Екранът за зареждане с ефект на сияние не се поддържа от това устройство Фон на светлата тема Бяло diff --git a/patches/src/main/resources/addresources/values-bn-rBD/music/strings.xml b/patches/src/main/resources/addresources/values-bn-rBD/music/strings.xml index 96b85dd144..10ce0138d0 100644 --- a/patches/src/main/resources/addresources/values-bn-rBD/music/strings.xml +++ b/patches/src/main/resources/addresources/values-bn-rBD/music/strings.xml @@ -462,12 +462,47 @@ SABR প্লেব্যাক (ভিডিও স্ট্রিম স্ প্রদর্শন সিঙ্ক্রোনাইজেশন প্যানেল - এআই এন্ডপয়েন্ট সকল প্রদানকারী নিষ্ক্রিয় - বিল্ট-ইন লিরিক্স এবং এমবেডেড লিরিক্স প্রদানকারীর ক্রম পুনরায় সেট করবেন? প্রদানকারীদের ডিফল্ট অগ্রাধিকার ক্রমে পুনরুদ্ধার করুন + রাইম ওয়ার্ডলিস্ট + "রাইম ওয়ার্ডলিস্টের জন্য UniDict-ও প্রয়োজন: অফলাইন শব্দকোষ প্রদানকারীতে, \"চাইনিজ এবং ক্যান্টোনিজ চরিত্র (UniDict)\" টিক করুন এবং আগে ডাউনলোড শেষ হতে দিন। এটি ছাড়া একটি তালিকা যা অক্ষর পড়ে তা কোনো উচ্চারণ দেয় না। + +রাইম ওয়ার্ডলিস্ট যে তালিকাগুলো পড়বে সেগুলো টিক করুন, তারপর সেগুলোর ডাউনলোড করতে Save ট্যাপ করুন। আপনি যে তালিকাটি যোগ করেছেন তা অন্যায় করলে এটি সরিয়ে ফেলা হবে। + +প্রতিটি তালিকা হলো আপনার নিজস্ব শব্দ-থেকে-উচ্চারণ টেবিল, একটি .dict.yaml যেখানে এমন লাইন থাকে: +字 ham +典 din2 +প্রথম কলামে শব্দটি আছে, দ্বিতীয় কলামে এর উচ্চারণ; বৃহত্তর কলাম এবং ট্যাববিহীন লাইন উপেক্ষা করা হয়।" + উপরের ভাষার জন্য কোনো রাইম ওয়ার্ডলিস্ট চেক করা হয়নি। একটি চেক করতে এই নামে ট্যাপ করুন, অথবা একটি তালিকা আছে এমন ভাষা নির্বাচন করুন + যোগ করুন + একটি রাইম ওয়ার্ডলিস্ট যোগ করুন + একটি .dict.yaml ফাইল, বা একটি সিপি মধ্যে এক ফাইল, যেখানে 字 ham এবং 典 din2 এর মতো লাইন রয়েছে। আপনি এটি টিক করেন এবং Save ট্যাপ করেন তখন তালিকাটি আনা হয়, এবং এর অনুজ্ঞা আপনার কাছে যাচাই করার জন্য + ভাষার কোড + দুই বা তিন অক্ষরের, হিসেবে hak বা wuu + নাম + পাঠের নিচে দেখানো, হিসেবে Hakka (Meixian) + ওয়ার্ডলিস্ট ঠিকানা + .dict.yaml বা এর সিপিএসের https:// ঠিকানা + শব্দে ন থাকা অক্ষর + স্বয়ংক্রিয় + ক্যান্টোনিজ অক্ষর + মান্ডারিন অক্ষর + কোনো অক্ষরের পাঠ নেই + কোড, নাম এবং ঠিকানা পূরণ করুন + কোডটি দুই বা তিন অক্ষরের হতে হবে, হিসেবে hak + ঠিকানাটি https:// দিয়ে শুরু হতে হবে + নাম এবং ঠিকানা উল্লম্ব দণ্ড ধারণ করতে পারবে না + %1$s ইতিমধ্যে একটি ওয়ার্ড টেবিল রয়েছে + %1$s বিল্ট-ইন রোমানাইজার দ্বারা পড়ে এবং একটি ওয়ার্ডলিস্ট গ্রহণ করতে পারে না + ব্যবহার করা হচ্ছে না: এই ভাষার ইতিমধ্যে এর নিজস্ব পাঠ রয়েছে, তাই এটি সরানোর জন্য আনচেক করুন + %1$s যোগ করা হয়েছে - ডাউনলোড করার জন্য এটি চেক করুন এবং Save-এ ট্যাপ করুন + ওয়ার্ডলিস্ট প্রস্তুত + ওয়ার্ডলিস্ট প্রস্তুত (%1$d প্রবেশ) + পাঠ ওয়ার্ডলিস্ট গঠন হচ্ছে: %1$d%% + ওয়ার্ডলিস্ট ডাউনলোড ব্যর্থ হয়েছিল - পুনরায় চেষ্টা করার জন্য Save-এ আবার ট্যাপ করুন সংরক্ষিত লিরিক্স মুছে ফেলুন - এই ডিভাইসে সংরক্ষিত লিরিক্স মুছে দিন + এই ডিভাইসে সংরক্ষিত লিরিক্সের %1$s MB ডিলিট করুন মুছে ফেলা হয়েছে %1$s এমবি টোকেন পান Apple Music মিডিয়া-ব্যবহারকারী-টোকেন @@ -522,14 +557,22 @@ SABR প্লেব্যাক (ভিডিও স্ট্রিম স্ কপি বাটন দেখান মূল লিরিক্স সাধারণ পাঠ্য হিসেবে কপি করতে ট্যাপ করুন; কাঁচা লিরিক্স সোর্স ফাইলটি ডাউনলোডসে সেভ করতে দীর্ঘক্ষণ চাপুন অনুবাদ বাটন দেখান - অনুবাদ টগল করতে ট্যাপ করুন; শুধুমাত্র অনুবাদ করতে দীর্ঘক্ষণ চাপুন। প্রদানকারীর অনুবাদ, গুগল ট্রান্সলেশন অথবা ওপেন এইআই-সামঞ্জস্যপূর্ণ APIs ব্যবহার করে - Romanize বাটন দেখান - রোমানাইজেশন টগল করতে ট্যাপ করুন; শুধুমাত্র রোমানাইজেশন করতে দীর্ঘক্ষণ চাপুন। প্রদানকারীর রোমানাইজেশন, গুগল ট্রান্সলেশন অথবা ওপেন এইআই-সামঞ্জস্যপূর্ণ APIs ব্যবহার করে - স্থানীয় রোমানাইজেশন এবং সমন্বয় - প্রদানকারী, আপনার মডেল এবং গুগল ট্রান্সলেশন ব্যর্থ হলে ডিভাইসে রোমানাইজ করুন। এই পাঠ্য সময়যুক্ত লিরিক্সের প্রতিটি শব্দ অনুসরণ করে; শব্দ সিঙ্ক্রোনাইজেশন চালু থাকতে হবে। চীন, ক্যান্টনিজ, জাপানিজ, কোরিয়ান, গ্রীক, রাশিয়ান, জর্জিয়ান, আরমেনিয়ান; প্রায় ১২০ এমবি - পাঠ %1$s: %2$d%% + অনুবাদ টগল করতে ট্যাপ করুন; কেবল অনুবাদের জন্য লং-প্রেস করুন। সক্ষম অনুবাদ প্রদানকারীদের তাদের তালিকাভুক্ত ক্রমে ব্যবহার করে + রোমানাইজ বা ট্রান্সলিটারেট বোতাম দেখান + রোমানাইজেশন বা ট্রান্সলিটারেশন টগল করতে ট্যাপ করুন; কেবল রোমানাইজেশন বা ট্রান্সলিটারেশনের জন্য লং-প্রেস করুন। সক্ষম রোমানাইজেশন প্রদানকারীদের তাদের তালিকাভুক্ত ক্রমে ব্যবহার করে + ট্রান্সলিটারেশন সক্রিয় করুন + লিরিক্স সোর্স থেকে非-Latin পাঠ অনুমতি দিন; বন্ধ থাকলে, শুধুমাত্র Latin অক্ষর, এক্সেন্টযুক্ত অক্ষর এবং টোন ডিজিট রাখুন + সময়যুক্ত লিরিক্সের প্রতিটি শব্দ পড়ে (শব্দ সমন্বয় চালু); কোরিয়ানের কোনো ডাউনলোডের প্রয়োজন নেই; চৌদ্টি স্ক্রিপ্ট একটি CLDR ডাউনলোড ভাগ করে; হান অক্ষর রাইম ওয়ার্ডলিস্ট দ্বারা পড়ে যখন এর ওয়ার্ড টেবিল ভাষা কভার করে + পাঠ অভিধান ডাউনলোড করা হচ্ছে… পাঠ্য প্রস্তুত (%1$d এন্ট্রি) ডাউনলোড ব্যর্থ - আবার চেষ্টা করতে এটি বন্ধ এবং চালু করুন + অফলাইন অভিধান + জাপানিজ (SudachiDict, ~75 MB) + চীনা এবং ক্যান্টোনিজ অক্ষর (UniDict, ~8 MB) + চীনা শব্দ (pinyin-pro, ~17 MB) + গ্রিক, স্লাভিক সাইরিলিক (রাশিয়ান, ইউক্রেনিয়ান, বলগেরিয়ান, সার্বিয়ান, বেলারুশিয়ান, ম্যাসেডোনিয়ান সহ), জর্জিয়ান, আর্মেনিয়ান, হিব্রু, আরবি, আমহারিক এবং সিরিয়াক ট্রান্সলিটারেশন (CLDR, ~0.5 MB) + পাঠ %1$s: %2$d%% + কমপক্ষে একটি অভিধান নির্বাচন করুন রিফ্রেশ বোতাম দেখান পরবর্তী লিরিক্স প্রার্থীতে পরিবর্তন করতে ট্যাপ করুন; গানের নাম এবং শিল্পী দিয়ে লিরিক্স খুঁজতে দীর্ঘক্ষণ চাপুন গানের কথা অনুসন্ধান @@ -540,8 +583,10 @@ SABR প্লেব্যাক (ভিডিও স্ট্রিম স্ সংরক্ষিত হয়েছে সিঙ্ক্রোনাইজড শব্দ বা সিলেবল হাইলাইট করুন যখন পাওয়া যায়, শব্দে শব্দে লিরিক্সকে অগ্রাধিকার দিন। যখন লিরিক্সে শব্দের সময় থাকে, তখন সঙ্গীতের সাথে সময় মিলিয়ে প্রতিটি শব্দ বা সিলেবল হাইলাইট হয়। - ব্যাকগ্রাউন্ড ভোক্যাল আলাদাভাবে দেখান - \"(Oh)\" ইচ্ছুক অংশ বন্ধনীতে লেখা ব্যাকগ্রাউন্ড ভোক্যালকে গানের লাইন থেকে সরিয়ে একটি নিঃস্ব যমের রেখে দেয় + নিষ্ক্রিয় লাইনগুলো ছোট করুন + বর্তমানে গাওয়া হয় না এমন প্রতিটি লাইন সামান্য ছোট আঁকে, এটিকে ফিকে করে দেওয়ার উপরে + ব্যাকগ্রাউন্ড ভোক্যালস আলাদাভাবে দেখান + ব্যাকগ্রাউন্ড ভোক্যালস এবং হার্মনিকে প্রধান লাইন থেকে আলাদা করে। ব্র্যাকেটে লেখা একটি ব্যাকগ্রাউন্ড ভোক্যাল, যেমন \"(Oh) I want you\", গাওয়া লাইন থেকে সরিয়ে একটি ফিকে লাইনে নিয়ে যায় লিরিক্স লুকান […] বর্তমানে বাজতে থাকা লাইনের উপরের লিরিক্সের লাইনগুলো লুকান যে লিরিক্স এখনো বাজানো হয়নি সেগুলো লুকান @@ -599,10 +644,15 @@ SABR প্লেব্যাক (ভিডিও স্ট্রিম স্ অনুবাদ করেছেন %s রোমানাইজ করেছেন %s রোমানাইজ করেছে %s + Rime দ্বারা রোমানাইজড (%s) রোমানাইজ করা হচ্ছে… অনুবাদের ভাষা - রোমানাইজেশনের ভাষা + রোমানাইজেশন বা ট্রান্সলিটারেশন ভাষা + "অনুবাদ লক্ষ্য ভাষা: +%s" + "রোমানায়জেশন বা ট্রান্সলিটারেশন লক্ষ্য ভাষা: +%s" অনুবাদ করা হচ্ছে… লিরিক্স অনুবাদ করা যায়নি লিরিক্স রোমানাইজ করা যায়নি @@ -616,7 +666,7 @@ SABR প্লেব্যাক (ভিডিও স্ট্রিম স্ লিরিক্স খোঁজার সময় শিরোনাম, শিল্পী এবং অ্যালবাম অনুসারে একটি ট্র্যাক খুঁজে পাওয়া যায় না, অথবা ভুল সংস্করণ পাওয়া যায়। আপনি ম্যানুয়ালি সঠিক সংস্করণটি নির্বাচন করতে পারেন, অথবা অনুসন্ধানটি আবার চেষ্টা করতে পারেন। ক্রেডিট লাইন লুকান QuickLRC API কী - QuickLRC বর্তমান ট্র্যাকের লিরিক্স অনুবাদ বা সময়-সংরেখণ করে। উপরের ওপেনএআই-কম্প্যাটিবল এন্ডপয়েন্টের সাথে সম্পর্কহীন একটি আলাদা সেবা + QuickLRC বর্তমান ট্র্যাকের জন্য লিরিক্স ট্রান্সক্রাইব বা সময়-সমন্বয় করে QuickLRC কী সংরক্ষিত হয়েছে। লিরিক্স সার্চ ডায়ালগে অলাইন বোতাম ব্যবহার করুন, অথবা যখন কোনো ট্র্যাকে কোনো লিরিক্স না থাকে তখন পাanel প্রম্পট ব্যবহার করুন QuickLRC API কী সংরক্ষিত QuickLRC সেই কী গ্রহণ করেনি @@ -632,58 +682,50 @@ SABR প্লেব্যাক (ভিডিও স্ট্রিম স্ অলাইন করার মতো কোনো লিরিক্স নেই - এর পরিবর্তে অনুবাদ করা হচ্ছে QuickLRC এই ট্র্যাকটি প্রসেস করতে পারেনি এই ট্র্যাকটির জন্য QuickLRC ক্রেডিট শেষ হয়ে গেছে - "গীতবাক্য শিরোনাম এবং শিল্পীর মাধ্যমে ম্যাচ করা হয়, অস্বাভাবিক মেটাডেটা কিছুই বা ভুল সংস্করণ পেতে পারে। সংস্করণ পরিবর্তন করতে রিফ্রেশ ব্যবহার করুন। * চিহ্নিত প্রদানকারীদের জন্য টোকেন প্রয়োজন। সব সক্রিয় প্রদানকারীকে আপনার ক্রমের মধ্যে সবসময় অনুরোধ করা হয় না, কিন্তু তাদের গতির উপর ভিত্তি করে সাড়া দেয়। + "গানের কথাগুলো শিরোনাম এবং শিল্পী দ্বারা মেলা হয়, অস্বাভাবিক মিটাডাটা কিছুই বা ভুল সংস্করণ খুঁজে পেতে পারে। সংস্করণ পরিবর্তন করতে রিফ্রেশ ব্যবহার করুন। * চিহ্নিত প্রদানকারীদের জন্য একটি টোকেন প্রয়োজন। সকল সক্রিয় প্রদানকারী সবসময় আপনার ক্রমে অনুরোধ করা হয় না, তবে তাদের গতির উপর ভিত্তি করে সাড়া দেয়। -গীতবাক্য তৃতীয় পক্ষের সার্ভিস থেকে আসে যা Morphe এর সাথে যুক্ত নয়, সমর্থিত নয়, অথবা সুপারিশকৃত নয়। প্রতিটি সার্ভিসকে যেমন আছে তেমনি ব্যবহার করা হয়: গীতবাক্য, তাদের উপলব্ধতা, এবং তাদের লাইসেন্সিং সেই সার্ভিসগুলোর এবং তাদের লাইসেন্সদাতাদের কাছেই থাকে, এবং এগুলো ব্যবহার করা তাদের সেবার শর্তাবলী অনুযায়ী হতে পারে। একটি সক্রিয় সার্ভিসে যা পৌঁছায় কেবল আপনি যে অনুসন্ধান চেয়েছেন - শিরোনাম, শিল্পী, এবং যেখানে ওই সার্ভিসকে তা প্রয়োজন, ভিডিও আইডি। ঐচ্ছিক AI এন্ডপয়েন্ট এবং QuickLRC ট্রান্সক্রিপশন কেবল আপনার কনফিগার করার পর চালু হয়, এবং গীতবাক্য পাঠ্য বা ট্র্যাকের ভিডিও URL পায়।" +গানের কথাগুলো তৃতীয় পক্ষের সেবা থেকে আসে যেগুলো Morphe-এর সাথে সম্পর্কিত, সমর্থিত বা সুপারিশকৃত নয়। প্রতিটি সেবা যেমনভাবে ব্যবহার করা হয়: গানের কথাগুলো, তাদের উপলব্ধতা, এবং তাদের লাইসেন্সিং সেই সেবাগুলোর এবং তাদের লাইসেন্সদাতাদের কাছেই থাকে, এবং তাদের ব্যবহার তাদের সেবার শর্তাবলীর অধীন হতে পারে। একটি সক্রিয় সেবায় যা পৌঁছায় তা শুধু আপনি যে অনুসন্ধান চেয়েছিলেন - শিরোনাম, শিল্পী, এবং যেখানে সেবাটিকে প্রয়োজন সেই ভিডিও আইডি। ঐচ্ছিক AI এন্ডপয়েন্ট এবং QuickLRC ট্রান্সক্রিপশন শুধুমাত্র আপনার এটি কনফিগার করার পর চালু হয়, এবং এটি গানের কথা বা ট্র্যাকের ভিডিও URL গ্রহণ করে।" শুধুমাত্র যেসব প্রোভাইডার আপনি ব্যবহার করেন সেগুলিকে চালু করুন, অথবা ডিফল্ট সেট রাখুন। ধারাবাহিক অনুরোধ ছোট কমিউনিটি লিরিক্স সাইটগুলিকে ভারপ্রাপ্ত করতে পারে অনুবাদ এবং রোমানাইজেশন অদলবদল করুন ON রোমানাইজেশনকে লিরিক্সের নিচে এবং অনুবাদকে উপরে রাখে; OFF রোমানাইজেশনকে উপরে এবং অনুবাদকে নিচে রাখে অনুবাদ এবং রোমানাইজেশন অদলবদল করুন - চালু অবস্থায় রোমানাইজেশন গানের কথার নীচে এবং অনুবাদ উপরে থাকে; বন্ধ অবস্থায় রোমানাইজেশন উপরে এবং অনুবাদ নীচে থাকে - অনুবাদ - রোমানাইজেশন - অনুবাদ প্রদানকারী - Google (ডিফল্ট, বিনামূল্যে) - OpenAI সামঞ্জস্যপূর্ণ (কাস্টম) - রোমানাইজেশন প্রদানকারী - Google (ডিফল্ট) - লোকাল / অফলাইন ডিকশনারি - OpenAI সামঞ্জস্যপূর্ণ (কাস্টম) - AI সেকশন - শুধুমাত্র তখনই ব্যবহৃত হয় যখন প্রোভাইডার Chorus বা Verse-এর মতো কোনো সেকশন লেবেল প্রদান করে না, যা আপনার OpenAI-সামঞ্জস্যপূর্ণ মডেল তখন শনাক্ত করে - AI এবং ক্লাউড এন্ডপয়েন্ট - AI প্রদানকারীদের জন্য API কী, মডেল, এন্ডপয়েন্ট এবং প্রম্পট টেমপ্লেট কনফিগার করুন। - API এন্ডপয়েন্ট URL (যেমন: https://api.openai.com/v1/chat/completions) + ON রোমানাইজেশন বা ট্রান্সলিটারেশন লিরিক্সের নিচে এবং অনুবাদ উপরে রাখে; OFF রোমানাইজেশন বা ট্রান্সলিটারেশন উপরে এবং অনুবাদ নিচে রাখে। প্রতিটি সিলেবলের উপর রাখা একটি পাঠ এটি সাথে চলে, এর সিলেবল থেকে তার নিচে পর্যন্ত + সর্বদা লিরিক্স কেন্দ্রে রাখুন + ON প্রতিটি লাইন মাঝখানে রাখে এবং উৎস সমন্বয় উপেক্ষা করে, ডুয়েট সাইডসহ; OFF লিরিক্স যে সমন্বয় চায় তা রাখে + কমপক্ষে একটি প্রদানকারী চালু থাকতে হবে + অনুবাদ + রোমানাইজেশন বা ট্রান্সলিটারেশন + অফলাইন অভিধান + রাইম ওয়ার্ডলিস্ট + AI দিয়ে বিভাগ সনাক্ত করুন + শুধুমাত্র তখনই ব্যবহৃত হয় যখন প্রদানকারীর কাছে কোনো বিভাগ লেবেল যেমন Chorus বা Verse থাকে না, যা আপনার OpenAI-সঙ্গতিপন্ন মডেল তখন সনাক্ত করে। সারি সম্পাদনা করাও এর প্রম্পট খোলে + API এন্ডপয়েন্ট URL (উদাহরণস্বরূপ https://api.openai.com/v1/chat/completions, https://api.anthropic.com/v1/chat/completions, https://generativelanguage.googleapis.com/v1beta/openai/chat/completions) প্রম্পট টেমপ্লেট প্লেসহোল্ডার: {language}, {task}, {title}, {artist}, {count}, {lines} প্রম্পট সংরক্ষিত হয়েছে প্রম্পট সংরক্ষিত হয়েছে, কিন্তু AI এন্ডপয়েন্ট এখনও ব্যবহারযোগ্য নয়। প্রথমে এটি AI সেটিংস-এ কনফিগার করুন প্রম্পট রিসেট করা হয়েছে - বৈধ JSON নয়। সেভ করার আগে এটি ঠিক করুন বা ক্ষেত্রটি মুছে ফেলুন অনুবাদ & রোমানাইজেশন সেটিংস সংরক্ষিত হয়েছে সেটিংস ডিফল্টে রিসেট করা হয়েছে বেস URL http:// বা https:// দিয়ে শুরু হতে হবে - কানেকশন পরীক্ষা করুন - এন্ডপয়েন্ট কনফিগার করুন - সব এন্ডপয়েন্ট দেখুন + সেটিংস অসম্পূর্ণ বা প্রদানকারীর দ্বারা প্রত্যাখ্যাত সংযোগ যাচাই করা হচ্ছে… - অনুবাদ প্রদানকারী - রোমানাইজেশন প্রদানকারী - মডেলগুলি ফেচ করুন + %1$s: সংযুক্ত! (%2$s) + %1$s: সংযোগ ব্যর্থ। সেটিংস পরীক্ষা করুন। + প্রথমে API কী লিখুন মডেলগুলি ফেচ করা হচ্ছে… একটি মডেল নির্বাচন করুন মডেলগুলি ফেচ করতে ব্যর্থ হয়েছে। API কী বা সংযোগ পরীক্ষা করুন। - Gemini API সেটিংস - OpenAI সামঞ্জস্যপূর্ণ (কাস্টম এন্ডপয়েন্ট) - DeepL API সেটিংস - API কি + প্রম্পট মডেল বেস URL + একটি সার্ভিস নির্বাচন করুন API কি (ঐচ্ছিক) - অনুবাদ প্রম্পট অতিরিক্ত রিকোয়েস্ট বডি (ঐচ্ছিক JSON) + অতিরিক্ত অনুরোধ হেডার (ঐচ্ছিক JSON) + নাম এবং মান উদ্ধৃতি করুন, উদাহরণস্বরূপ {\"api-key\": \"abc\"}। একটি অবিকৃত মান / : বা সংখ্যা ধারণ করলে তা প্রত্যাখ্যাত হয়। একটি খালি মান একটি হেডার সরিয়ে দেয়। + নাম এবং মান উদ্ধৃতি করুন, উদাহরণস্বরূপ {\"temperature\": 0.3}। একটি অবিকৃত মান / বা : ধারণ করলে তা প্রত্যাখ্যাত হয়। মডেল এবং বার্তা এখানে সেট করা যায় না। অথেন্টিকেশন কি (ফ্রি বা প্রো) - API কি, মডেল, এন্ডপয়েন্ট এবং প্রম্পট কনফিগার করুন 8.23.51 - বোল্ড আইকন নিষ্ক্রিয় করুন 9.13.51 - প্লেয়ার আপ নেক্সট, লিরিক্স এবং সম্পর্কিত ট্যাব পুনরুদ্ধার করুন diff --git a/patches/src/main/resources/addresources/values-bn-rBD/youtube/strings.xml b/patches/src/main/resources/addresources/values-bn-rBD/youtube/strings.xml index 82a9910c84..542ab97de5 100644 --- a/patches/src/main/resources/addresources/values-bn-rBD/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-bn-rBD/youtube/strings.xml @@ -29,6 +29,8 @@ Second \"item\" text" বিবিধ ভিডিও পুরানো YouTube সেটিংস স্ক্রিন পুনরুদ্ধার করুন + YouTube স্ন্যাকবারে বার্তা দেখান + প্লেয়ার বাটনের নিশ্চিতকরণের মতো মোর্ফি বার্তা পপআপের পরিবর্তে YouTube স্ন্যাকবারে দেখানো হয় পটভূমির প্লেব্যাক সীমাবদ্ধতা সরান ব্যাকগ্রাউন্ড প্লে অক্ষম করুন diff --git a/patches/src/main/resources/addresources/values-bs-rBA/music/strings.xml b/patches/src/main/resources/addresources/values-bs-rBA/music/strings.xml index 0bdce6a4b5..7acb8af1da 100644 --- a/patches/src/main/resources/addresources/values-bs-rBA/music/strings.xml +++ b/patches/src/main/resources/addresources/values-bs-rBA/music/strings.xml @@ -462,12 +462,10 @@ Kako biste osigurali da se scrobbling sinhronizuje tokom reprodukcije u pozadini Prikaz Sinhronizacija Panel - AI krajnja tačka Svi provajderi su onemogućeni - ugrađeni tekstovi i embedded tekstovi Resetovati redoslijed pružalaca? Vratite pružalace na podrazumijevani redoslijed prioriteta Obriši sačuvane tekstove - Obrišite tekstove pjesama sačuvanih na ovom uređaju Očišćeno %1$s MB Preuzmi token Apple Music Media-user-token @@ -527,14 +525,9 @@ Potrebni kolačići: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPI Prikaži dugme Kopiraj Dodirnite da kopirate originalni tekst kao običan tekst; dugo pritisnite da sačuvate izvorni fajl teksta u Preuzimanja Prikaži dugme Prevedi - Dodirnite za promjenu prikaza prijevoda; dugo pritisnite samo za prijevod. Koristi prijevode pružatelja, Google Translate ili OpenAI kompatibilni API - Prikaži dugme za romanizaciju - Dodirnite za promjenu prikaza romanizacije; dugo pritisnite samo za romanizaciju. Koristi romanizaciju pružatelja, Google Translate ili OpenAI kompatibilni API - Lokalna romanizacija i poravnanje - Romanizirajte na uređaju kada pružalac, vaš model i Google Translate zakažu. Čitanje prati svaku riječ sinkronizovanog teksta pjesme; sinkronizacija riječi mora biti uključena. Kineski, kantonski, japanski, korejski, grčki, ruski, gruzijski, armenski; oko 120 MB - Čitanje %1$s: %2$d%% Očitanja su spremna (%1$d unosa) Preuzimanje nije uspjelo - isključite i ponovo uključite ovu opciju kako biste pokušali ponovo + Čitanje %1$s: %2$d%% Prikaži dugme za osvježavanje Dodirnite za prebacivanje na sljedećeg kandidata za tekst pjesme; dugo pritisnite za pretragu teksta pjesme po naslovu pjesme i izvođaču Pretraži tekstove pjesama @@ -605,7 +598,6 @@ Primjenjuje se na oba ograničenja. Na 1.75, četiri karaktera pune širine rač Prevedeno Jezik prevoda - Jezik romanizacije Prevođenje… Nije bilo moguće prevesti tekst pjesme Romanizovano @@ -619,7 +611,6 @@ Primjenjuje se na oba ograničenja. Na 1.75, četiri karaktera pune širine rač Osvježavanje Osvježeno QuickLRC API ključ - QuickLRC transkribuje ili vremenski usklađuje tekstove za trenutnu pjesmu. To je zasebna usluga, nepovezana sa gore navedenom OpenAI kompatibilnom krajnjom tačkom QuickLRC ključ je sačuvan. Koristite dugme za usklađivanje u dijalogu za pretragu tekstova, ili prompt u panelu kada pjesma nema tekst QuickLRC API ključ je sačuvan QuickLRC nije prihvatio taj ključ @@ -635,56 +626,27 @@ Primjenjuje se na oba ograničenja. Na 1.75, četiri karaktera pune širine rač Nema tekstova za usklađivanje - umjesto toga se vrši transkripcija QuickLRC nije mogao procesuirati ovu pjesmu QuickLRC krediti za ovu pjesmu su potrošeni - "Tekstovi pjesama se podudaraju prema naslovu i izvođaču; neobični metapodaci mogu rezultirati time da ništa ne bude pronađeno ili da bude pronađena pogrešna verzija. Koristite osvježavanje za promjenu verzija. Provajderi označeni sa * zahtijevaju token. Svi omogućeni provajderi nisu uvijek pretraživani vašim redoslijedom, već odgovaraju na osnovu svoje brzine.\n\nTekstovi pjesama dolaze iz usluga trećih strana koje nisu povezane s Morphe, sponzorisane niti podržane od strane Morphe. Svaka se koristi onakvama kakva jeste: tekstovi, njihova dostupnost i njihovo licenciranje ostaju kod tih usluga i njihovih licencera, a njihovo korištenje može biti podložno njihovim uslovima pružanja usluge. Ono što stiže do omogućene usluge je samo pretraga koju ste tražili - naslov, izvođač i, gdje ta usluga to zahtijeva, ID videa. Opcioni AI endpoint i QuickLRC transkripcija pokreću se samo nakon što ih konfigurišete i prime tekst pjesme ili URL videa traka." Omogućite samo pružaoče usluga koje koristite ili zadržite zadane postavke - konstantni zahtjevi mogu preopteretiti manje zajedničke stranice sa tekstovima pjesama Sakrij liniju kredita Sakrij liniju kredita koja se prikazuje ispod teksta pjesme Zamijeni prevod i romanizaciju - UKLJUČENO stavlja romanizaciju ispod teksta pjesme, a prevod iznad; ISKLJUČENO drži romanizaciju iznad, a prevod ispod - Prevod - Romanizacija - Provajder prevoda - Google (podrazumijevano, besplatno) - OpenAI kompatibilno (prilagođeno) - Provajder romanizacije - Google (podrazumijevano) - Lokalni / Offline rječnik - OpenAI kompatibilno (Prilagođeno) - AI sekcija - Koristi se samo kada pružatelj ne šalje oznake sekcija kao što su Refren ili Strofa, koje zatim detektuje vaš OpenAI-kompatibilni model - AI & Cloud krajnje tačke - Konfigurišite API ključeve, modele, krajnje tačke i šablone upita za AI provajdere. - URL API krajnje tačke (npr. https://api.openai.com/v1/chat/completions) Šablon prompta Placeholderi: {language}, {task}, {title}, {artist}, {count}, {lines} Prompt je sačuvan Prompt je sačuvan, ali AI krajnja tačka još uvijek nije upotrebljiva. Prvo je konfigurišite u AI postavkama Prompt je resetovan - Nije validan JSON. Popravite ga ili ispraznite polje prije spremanja Postavke prevoda & romanizacije su sačuvane Postavke su vraćene na zadane vrijednosti Bazni URL mora početi sa http:// ili https:// - Testiraj vezu - Konfigurišite krajnje tačke - Prikaži sve krajnje tačke Validiranje konekcije… - Provajder prevoda - Provajder romanizacije - Preuzmi modele Preuzimanje modela… Odaberite model Preuzimanje modela nije uspjelo. Provjerite API ključ ili vezu. - Postavke Gemini API-ja - OpenAI kompatibilno (prilagođena krajnja tačka) - Postavke DeepL API-ja - API ključ Model Osnovni URL API ključ (opcionalno) - Prompt za prevod Dodatno tijelo zahtjeva (opcionalni JSON) Autentikacijski ključ (Besplatni ili Pro) - Konfigurišite API ključeve, modele, krajnje tačke i promptove 8.23.51 - Onemogući podebljene ikone 9.13.51 - Vrati kartice Sljedeće, Tekstovi pjesama i Srodno u plejeru diff --git a/patches/src/main/resources/addresources/values-ca-rES/music/strings.xml b/patches/src/main/resources/addresources/values-ca-rES/music/strings.xml index 03bb04867f..bf7dbd5683 100644 --- a/patches/src/main/resources/addresources/values-ca-rES/music/strings.xml +++ b/patches/src/main/resources/addresources/values-ca-rES/music/strings.xml @@ -462,12 +462,11 @@ Per assegurar que el scrobbling es sincronitzi durant la reproducció en segon p Visualització Sincronització Panell - Endpoint de la IA Tots els proveïdors desactivats: lletres integrades i lletres incrustades Restaurar l\'ordre del proveïdor? Restaura els proveïdors a l\'ordre de prioritat per defecte + Automàtic Esborrar les lets guardades - Elimina les lets guardades en aquest dispositiu S\'han esborrat %1$s MB Obté el token Token d\'usuari de mitjans d\'Apple Music @@ -527,14 +526,9 @@ Cookies requerides: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPIS Mostrar botó de Copiar Toca per copiar la lletra original com a text pla; manté premut per desar el fitxer font de la lletra a Descàrregues Mostra el botó de Traduir - Toca per alternar la traducció; manté premut per veure només la traducció. Utilitza traduccions del proveïdor, Google Translate o una API compatible amb OpenAI - Mostrar botó de romanització - Toca per alternar la romanització; manté premut per a romanització només. Utilitza la romanització del proveïdor, Google Translate o una API compatible amb OpenAI - Romanització i alignació locals - Romanitza al dispositiu quan el proveïdor, el teu model i Google Translate fallen. La lectura segueix cada paraula d\'una lletra sincronitzada; la sincronització de paraules ha d\'estar activada. Xinès, cantonès, japonès, coreà, grec, rus, georgià, armèni; uns 120 MB - Llegint %1$s: %2$d%% {0, plural, one {Lectures llistes (1 entrada)} other {Lectures llistes (%1$d entrades)}} Error de descàrrega: desactivi i torni a activar aquesta opció per reintentar-ho + Llegint %1$s: %2$d%% Mostrar botó d\'actualització Toca per canviar al següent candidat de lletres; manté premut per buscar lletres pel títol de la cançó i l\'artista Cerca lets @@ -605,7 +599,6 @@ S'aplica a ambos limits. A 1,75, quatre caracters de ple ampleur compten com a 7 Traduït Idioma de traducció - Idioma de romanització Traduint… No s\'han pogut traduir les letres Romanitzat @@ -619,7 +612,6 @@ S'aplica a ambos limits. A 1,75, quatre caracters de ple ampleur compten com a 7 Actualitzant Actualitzat Clau de l\'API de QuickLRC - QuickLRC transcribeix o aligna temporalment les lletres de la pista actual. Un servei independent, no relacionat amb l\'endpoint compatible amb OpenAI d\'a dalt Clau de QuickLRC des guarded. Utilitzeu el botó d\'alineació al diàleg de cerca de lletres, o l\'avís del panell quan una pista no tingui lletres Clau de l\'API de QuickLRC des guardedada QuickLRC no ha acceptat aquesta clau @@ -635,56 +627,27 @@ S'aplica a ambos limits. A 1,75, quatre caracters de ple ampleur compten com a 7 No hi ha lletres per alinear; s\'està transcripció en el seu lloc QuickLRC no ha pogut processar aquesta pista S\'han esgotat els crèdits de QuickLRC per a aquesta pista - "Les lletres coincideixen per títol i artista; els metadades inusuals poden no trobar res o la versió incorrecta. Utilitzeu l'actualització per canviar de versió. Els proveïdors marcats amb * requereixen un token. No tots els proveïdors habilitats es solen sol·licitar en el vostre ordre, sinó que responen segons la seva velocitat.\n\nLes lletres provenen de serveis de tercers que no estan afiliats, patrocinats ni avalats per Morphe. Cadascun s'utilitza tal com és: les lletres, la seva disponibilitat i la seva llicència pertanyen a aquests serveis i als seus licenciants, i el seu ús pot estar subjecte als seus termes de servei. El que arriba a un servei habilitat és només la cerca que heu sol·licitat: el títol, l'artista i, quan aquest servei ho requereixi, l'ID del vídeo. El punt final d'IA opcional i la transcripció QuickLRC s'executen només després de configurar-los i reben el text de les lletres o la URL del vídeo de la pista." Activeu només els proveïdors que utilitzeu, o mantingueu els per defecte: les sol·licituds constants poden saturar els lletres de petits llocs comunitaris Ocultar línia de crèdits Oculta la línia de crèdits que apareix sota la lletra Intercanvia la traducció i la romanització - ACTIVAT posa la romanització sota la lletra i la traducció a sobre; DESACTIVAT manté la romanització a sobre i la traducció a sota - Traducció - Romanització - Proveïdor de traducció - Google (per defecte, gratuït) - Compatible amb OpenAI (personalitzat) - Proveïdor de romanització - Google (per defecte) - Diccionari local / fora de línia - Compatible amb OpenAI (Personalitzat) - Secció de IA - Només s\'utilitza quan el proveïdor no envia etiquetes de secció com el tornestor o la strofa, que el vostre model compatible amb OpenAI detecta posteriorment - IA & Cloud Endpoints - Configureu les claus API, els models, els punts d\'extremitat i les plantilles de prompt per als proveïdors de IA. - URL de l\'endpoint de l\'API (p. ex. https://api.openai.com/v1/chat/completions) Plantilla del prompt Marcadors de posició: {language}, {task}, {title}, {artist}, {count}, {lines} Prompt desat Prompt guardat, però l\'endpoint de la IA encara no és utilitzable. Configureu-lo primer a l\'apartat de Configuració de la IA Prompt reiniciat - JSON no vàlid. Corregiu-lo o buideu el camp abans de desar Configuració de traducció & romanització guardada Configuració restableixida per defecte L\'URL base ha de començar per http:// o https:// - Provar connexió - Configurar endpoints - Mostrar tots els endpoints Validant la connexió… - Proveïdor de traducció - Proveïdor de romanització - Obtenir models Obtint models… Seleccioneu un model No s\'han pogut obtenir els models. Comproveu la clau API o la connexió. - Configuració de l\'API de Gemini - Compatible amb OpenAI (punta final personalitzada) - Configuració de l\'API de DeepL - Clau de l\'API Model URL base Clau de l\'API (opcional) - Prompt de traducció Cos de la sol·licitud extra (JSON opcional) Clau d\'autenticació (Gratuïta o Pro) - Configureu les claus de l\'API, els models, els punts finals i els prompts 8.23.51 - Desactiva les icones en negreta 9.13.51 - Restaura les pestanyes Pròxim del reproductor, Letres i Relacionats diff --git a/patches/src/main/resources/addresources/values-ckb-rIR/music/strings.xml b/patches/src/main/resources/addresources/values-ckb-rIR/music/strings.xml index a1502a75f6..97dfa33a0b 100644 --- a/patches/src/main/resources/addresources/values-ckb-rIR/music/strings.xml +++ b/patches/src/main/resources/addresources/values-ckb-rIR/music/strings.xml @@ -462,12 +462,11 @@ Second \"item\" text" پیشاندان هاوسەنگکردن پانێڵ - کۆتایی AI هەموو دابینکەران ڕاگیراون - دەقە ناوخۆییەکان و دەقە لەناوبووەکان ڕێکخستنی دابینکەران بگەڕێنرێتەوە بۆ سەرەتایی؟ گەڕاندنەوەی دابینکەران بۆ ڕێکی پێشنیارکراوی سەرەتایی + خۆکار پاککردنەوەی تێکستە پاشکەوتکراوەکان - تێکستەکانی گۆرانی کە لەم ئامێرەدا هەڵگیراون بسڕەوە %1$s MB پاککرایەوە وەرگرتنی تۆکن تۆکنی بەکارهێنەری میدیای Apple Music @@ -527,14 +526,9 @@ Second \"item\" text" نیشاندانی دوگمەی کۆپی دابگرە بۆ کۆپیکردنی تێکستی ڕەسەنی گۆرانییەکە وەک تێکستی سادە؛ بۆ ماوەیەک دایبگرە بۆ پاشکەوتکردنی فایلەکەی سەرچاوەی تێکستەکە لە Downloads نیشاندانی دوگمەی وەرگێڕان - دابگرە بۆ گۆڕینی وەرگێڕان؛ بۆ ماوەیەک دایبگرە تەنها بۆ وەرگێڕان. وەرگێڕانی دابینکەر، Google Translate، یان APIیەکی هاوتا بە OpenAI بەکاردێنێت - نیشاندانی دوگمەی رۆمانایز - دابگرە بۆ گۆڕینی ڕۆمانایزەکردن؛ بۆ ماوەیەک دایبگرە تەنها بۆ ڕۆمانایزەکردن. ڕۆمانایزەکردنی دابینکەر، Google Translate، یان APIیەکی هاوتا بە OpenAI بەکاردێنێت - ڕۆمانایزکردن و ڕێکخستنی ناوخۆیی - لەسەر ئامێرەکە ڕۆمانایز بکە کاتێک دابینکەر، مۆدێلەکەت و Google Translate سەرکەوتوو نەبن. خوێندنەوەکە دوای هەر وشەیەکی تێکستی کاتی دەکەوێت؛ دەبێت هاوسەنگکردنی وشەکان چالاک بێت. چینی، کانتۆنی، ژاپانی، کۆری، یۆنانی، ڕوسی، جۆرجی، ئەرمەنی؛ نزیکەی ١٢٠ مێگابایت - خوێندنەوەی %1$s: %2$d%% خوێندنەوەکان ئامادەن (%1$d تۆمار) داگرتنی نەسەرکەوت + خوێندنەوەی %1$s: %2$d%% نیشاندانی دوگمەی نوێکردنەوە کلیک بکە بۆ گۆڕین بۆ کاندیدی داهاتووی تێکست؛ پەستانی درێژ بکە بۆ گەڕان بەدوای تێکستدا بەپێی ناوی گۆرانی و هونەرمەند گەڕانی تێکستەکانی گۆرانی @@ -605,7 +599,6 @@ Second \"item\" text" وەرگێڕدراوە زمانی وەرگێڕان - زمانی ڕۆمانیزەکردن وەرگێڕان دەکرێت… نەتوانرا وەرگێڕانی تێکستەکەی گۆرانییەکە بکرێت ڕۆمانیزە @@ -619,7 +612,6 @@ Second \"item\" text" نوێکردنەوە نوێکرا کلیلى API QuickLRC - QuickLRC تێکستی گۆرانییەک دەنووسێتەوە یان کاتی ڕێکدەخات بۆ تراکەکەی ئێستا. خزمەتگارییەکی جیاوازە و پەیوەندی بە خاڵی کۆتایی OpenAI کە لەسەرەوە ئاماژەی پێدراوە نییە کلیلەکەی QuickLRC پاشکەوت کرا. دوگمەی ڕێکخستن لەناو دایالۆگی گەڕانی تێکستەکان بەکاربهێنە، یان ئامۆژگاری پانێڵەکە کاتێک تراکێک تێکستی نییە کلیلەکانی APIی QuickLRC پاشکەوت کرا QuickLRC ئەو کلێلەی نەناساند @@ -635,56 +627,27 @@ Second \"item\" text" هیچ تێکستێک نییە بۆ ڕێکخستن - لەجیاتی ئەوە دەینووسێتەوە QuickLRC نەیتوانی ئەم گۆرانییە پرۆسە بکات کrédit-ەکانی QuickLRC بۆ ئەم گۆرانییە تەواو بوون - "تێکستەکان بەپێی ناونیشان و ئاهەنگساز دەگونجێن، زانیاریی گشتیی نائاسایی ڕەنگە هیچ شتێک نەدۆزێتەوە یان وەشانێکی هەڵ بدۆزێتەوە. نوێکردنەوە بەکاربهێنە بۆ گۆڕینی وەشانەکان. ئەو دابینکەرانەی کە بە * نیشانەکراون پێویستیان بە تۆکەن. هەموو دابینکەرە چالاککراوەکان هەمیشە بەپێی ڕیزبەندیی تۆ داوا ناکرێن، بەڵکو بەپێی خێرایی خۆیان وەڵام دەدەنەوە.\n\nتێکستەکان لە خزمەتگوزارییەکانی لایەنی سێیەم دێن کە پەیوەندییان بە Morphe نایە، سپۆنسەر نین یان پشتگیری ناکەن. هەر یەکێکیان بەو شێوەیەی هەین بەکاردێن: تێکستەکان، بەردەستبوونیان و مۆڵەتیان لای ئەو خزمەتگوزارییانە و مۆڵەتدەرەکانیان دەمێنێتەوە، و بەکارهێنانیان ڕەنگە مەرجەکانی خزمەتگوزارییەکانیان لێبکەوێت. ئەوەی دەگاتە خزمەتگوزارییەکی چالاککراو تەنها ئەو گەڕانەیە کە تۆ داوای کردووە - ناونیشانەکە، ئاهەنگسازەکە، و لەو شوێنەی خزمەتگوزارییەکە پێویستی پێیە، ناسنامەی ڤیدیۆکە (video ID). خاڵی کۆتایی AI ئارەزوومەندانە و گۆڕانی QuickLRC تەنها دوای ئەوەی ڕێکیان دەخەیت کار دەکەن، و تێکستی تێکستەکە یان ناونیشانی ڤیدیۆی تڕاکەکە وەردەگرن." تەنها ئەو دابینکەرانە چالاک بکە کە بەکاریان دەهێنیت، یان هەر هەر ئێستا هەرکە بنەمکە - داواکارییەکانی بەردەوام دەتوانن فشار بخەنە سەر سایتە بچووکەکانی تێکستی گۆرانی شاردنەوەی هێڵی ناسنامە شاردنەوەی هێڵی ناسنامەی کە لە خوار تێکستی گۆرانییەکەدا پیشان دەدرێت گۆڕینی شوێنی وەرگێڕان و ڕۆمانیزە - چالاک (ON) ڕۆمانیزە دەخاتە خوار تێکست و وەرگێڕان دەخاتە سەرەوە؛ ناچالاک (OFF) ڕۆمانیزە لە سەرەوە و وەرگێڕان لە خوارەوە دەهێڵێتەوە - وەرگێڕان - ڕۆمانایزەکردن - دابینکەرى وەرگێڕان - گوگڵ (پێشفرۆش، بێبەرامبەر) - گونجاو لەگەڵ OpenAI (پێشراو) - دابینکەری ڕۆمانیزەیشن - گوگڵ (پێشفرۆش) - فەرھەنگێی ناوخۆیی / ئۆفلاین - گونجاو لەگەڵ OpenAI (پێشراو) - بەشی ژیری دەستکرد AI - تەنها ئەو کاتە بەکاردێت کە دابینکەر هیچ نیشانەیەک بۆ بەشەکان ناردووبێت وەک Chorus یان Verse، کە دواتر مۆدێلی OpenAI-compatibleت دەیناسیناتەوە - AI و خاڵەکانی کۆڵاپ - ڕێکخستنی کلیلەکانی API، مۆدێلەکان، خاڵەکانی کۆڵاپ، و شابلۆنی پرۆمپت بۆ دابینکەرانی AI. - ناونیشانی URLی خاڵی کۆتایی API (بۆ نموونە https://api.openai.com/v1/chat/completions) قاڵبی پرۆمپت جێنشینەکان: {language}, {task}, {title}, {artist}, {count}, {lines} پرۆمپتەکە پاشکەوت کرا ئامۆژگارییەکە پاشکەوت کرا، بەڵام خاڵی کۆتایی AI هێشتا بەکارناهێنرێت. سەرەتا لە ڕێکخستنەکانی AI ڕێکی بخە پرۆمپتەکە سفر کرا - JSONـی دروست نییە. چاکی بکە یان کێڵەکە خاڵی بکە پێش پاشکەوتکردن ڕێکخستنەکانی وەرگێڕان & ڕۆمانیزە کردن پاشکەوتکران ڕێکخستنەکان گەڕانەوە بۆ دۆخی سەرەتایی ناونیشانی بنەڕەتی دەبێت بە http:// یان https:// دەست پێ بکات - تاقیکردنەوەی پەیوەندی - ڕێکخستنی خاڵەکانی کۆتایی - پیشاندانی هەموو خاڵەکانی کۆتایی پشکنینی پەیوەندییەکە دەکرێت… - دابینکەرى وەرگێڕان - دابینکەری ڕۆمانایزەیشن - هێنانی مۆدێلەکان هێنانى مۆدێلەکان... مۆدێلێک هەڵبژێرە کێشەیەک لە هێنانی مۆدێلەکاندا ڕوویدا. تکایە کلیلی API یان پەیوەندی ئینتەرنێتەکە بپشکنە. - ڕێکخستنەکانی Gemini API - گونجاوە لەگەڵ OpenAI (سندووقی کۆتایی تایبەت) - ڕێکخستنەکانی DeepL API - کلیلى API مۆدێل ناونیشانی بنەڕەتی (Base URL) کلیلى API (ئارەزوومەندانە) - پڕۆمپتی وەرگێڕان لەشی گەشتی زیادە (JSON ئارەزوومەندانە) کۆدی سەلماندنی ناسنامە (بێبەرامبەر یان پرۆ) - ڕێکخستنی کۆدەکانی API، مۆدێلەکان، خاڵەکانی کۆتایی و ئاماژەکان 8.23.51 - ڕاگرتنی ئایکۆنە ئەستوورەکان 9.13.51 - گەڕاندنەوەی بەسەکانی Player Up next و Lyrics و Related diff --git a/patches/src/main/resources/addresources/values-cs-rCZ/music/strings.xml b/patches/src/main/resources/addresources/values-cs-rCZ/music/strings.xml index 03da3a06db..24c9b7d9b6 100644 --- a/patches/src/main/resources/addresources/values-cs-rCZ/music/strings.xml +++ b/patches/src/main/resources/addresources/values-cs-rCZ/music/strings.xml @@ -462,12 +462,40 @@ Pro zajištění synchronizace scrobblingu během přehrávání na pozadí se u Zobrazení Synchronizace Panel - Koncový bod AI Všichni poskytovatelé vypnuti - vestavěné texty a vložené texty Obnovit výchozí pořadí poskytovatelů? Obnovit výchozí pořadí priorit poskytovatelů. + Slovník Rime + "Slovník Rime Wordlist také potřebuje UniDict: V poskytovateli offline slovníků zaškrtněte „Čínské a kantonské znaky (UniDict)" + Pro nadepsaný jazyk není označen žádný slovníkový soubor Rime. Klepnutím na tento název jeden označte, nebo zvolte jazyk se seznamem. + Přidat + Přidat slovník Rime + Soubor .dict.yaml, nebo archív ZIP obsahující takový soubor, s řádky jako 字 ham a 典 din2. Seznam je načten při zaškrtnutí a klepnutí na Uložit, licence patří vám k ověření + Kód jazyka + Dvě nebo tři písmena, například hak nebo wuu + Název + Zobrazeno pod výslovností, např. Hakka (Meixian) + Adresa slovníku + Adresa URL souboru .dict.yaml nebo jeho archivů + Znaky mimo slova + Automaticky + Kantonské znaky + Mandarinové znaky + Žádná výslovnost znaků + Vyplňte kód, název a adresu + Kód musí mít dvě nebo tři písmena, například hak + Adresa musí začínat https:// + Název a adresa nesmí obsahovat svislou čáru + %1$s již má tabulku slov + %1$s je čten vestavěným romanizátorem a nemůže přijmout slovník + Nepoužívá se: tento jazyk již má vlastní výslovnost, takže ji odškrtněte pro odstranění + Přidáno %1$s – zaškrtněte jej a klepněte na Uložit pro stažení + Slovníky připraveny + Slovníky připraveny (%1$d položek) + Výstavba slovníku výslovnosti: %1$d%% + Stahování slovníku selhalo – klepněte znovu na Uložit pro opakování Smazat uložená texty - Odstranit texty uložené na tomto zařízení + Smazat %1$s MB textů uložených v tomto zařízení Vyčištěno %1$s MB Získat token Apple Music token uživatele médií @@ -522,14 +550,22 @@ Požadované cookies: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAP Zobrazit tlačítko Kopírovat Klepnutím zkopírujte originální text jako obyčejný text; dlouhým stiskem uložte surový zdrojový soubor textů do složky Stahování Zobrazit tlačítko Přeložit - Klepnutím přepněte překlad; dlouhým stiskem pouze překlad. Používá poskytovatelské překlady, Google Translate nebo rozhraní API kompatibilní s OpenAI - Zobrazit tlačítko pro přepis do latinky. - Klepnutím přepněte transkripci; dlouhým stiskem pouze transkripci. Používá poskytovatelskou transkripci, Google Translate nebo rozhraní API kompatibilní s OpenAI - Lokální transkripce a sladěni - Transkribujte v zařízení, když selžou poskytovatel, váš model a Google Translate. Čtení sleduje každé slovo časovaného textu; synchronizace slov musí být zapnuta. Čínština, kantoneština, japonština, korejština, řečtina, ruština, gruzínština, arménština; asi 120 MB - Čtení %1$s: %2$d%% + Klepnutím přepnete překlad; dlouhým stiskem pouze překlad. Používá povolené poskytovatele překladu v uvedeném pořadí + Zobrazit tlačítko romanizace nebo transkripce + Klepnutím přepnete romanizaci nebo transkripci; dlouhým stiskem pouze romanizaci nebo transkripci. Používá povolené poskytovatele romanizace v uvedeném pořadí + Povolit transkripci + Povolit nelatinské výslovnosti ze zdroje textů; pokud je vypnuto, ponechat pouze latinská písmena, akcentovaná písmena a číslice tónů + Čte každé slovo časově synchronizovaného textu (zapnuta synchronizace slov); Korean nepotřebuje stahování; čtrnáct písem sdílí jedno stažení CLDR; han-znaky jsou čteny Slovníkem Rime, když jeho tabulka slov pokrývá jazyk + Stahování slovníků výslovnosti… Připraveno čtení (%1$d položek) Stahování se nezdařilo – vypněte a znovu zapněte tuto funkci pro opakovaný pokus + Offline slovník + Japonský (SudachiDict, ~75 MB) + Čínské a kantonské znaky (UniDict, ~8 MB) + Čínská slova (pinyin-pro, ~17 MB) + Řecká, slovanská cyrilice (včetně ruštiny, ukrajinštiny, bulharštiny, srbštiny, běloruštiny, makedonštiny), gruzínština, arménština, hebrejština, arabština, amharština a syrská transkripce (CLDR, ~0,5 MB) + Čtení %1$s: %2$d%% + Vyberte alespoň jeden slovník Klepnutím přepnete na další navrhovaný text; […] : \"Uloženo\" Klepnutím přepněte na další kandidáta textu; dlouhým stiskem hledejte text podle názvu písně a interpreta Vyhledat text písně @@ -540,8 +576,10 @@ Požadované cookies: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAP Uloženo Zvýrazňovat synchronizovaná slova nebo slabiky Upřednostnit texty slovo po slově, pokud jsou k dispozici. Když texty obsahují načasování slov, zvýrazní každé slovo nebo slabiku v rytmu hudby. - Zobrazit vokály pozadí samostatně - Přesune pozadí vokálu uvedeného v závorkách, například „(Oh) chci tě“, z původní zpěvné linky do samostatné tišší linky + Zmenšit neaktivní řádky + Každý řádek, který není právě zpíván, vykreslí mírně menší nad zesvětleným pozadím + Zobrazit záložní vokály samostatně + Oddělí záložní vokály a harmonii od hlavní linie. Přesune záložní vokál zapsaný v závorkách, např. „(Ó) chci tě“, z zpívané linie do vlastního tlumeného řádku Skrýt […] texty. Skrýt řádky textu nad aktuálně přehrávaným řádkem Skrýt nepřehrávané texty @@ -599,10 +637,15 @@ Platí pro oba limity. Při hodnotě 1,75 se čtyři znaky plné šířky počí Přeloženo službou %s Romanizováno službou %s Romanizoval %s + Romanizováno nástrojem Rime (%s) Přeloženo Jazyk překladu - Jazyk romanizace + Jazyk romanizace nebo transkripce + "Cílový jazyk překladu: +%s" + "Cílový jazyk romanizace nebo transkripce: +%s" Překládání… Text písně se nepodařilo přeložit Romanizováno @@ -616,7 +659,7 @@ Platí pro oba limity. Při hodnotě 1,75 se čtyři znaky plné šířky počí Obnovování Obnoveno Klíč API QuickLRC - QuickLRC přepisuje nebo synchronizuje sady textů písní pro aktuální stopu. Jedná se o samostatnou službu, nesouvisející s výše uvedeným koncovým bodem kompatibilním s OpenAI + QuickLRC transkribuje nebo časově zarovnává texty pro aktuální stopu Klíč QuickLRC byl uložen. Použijte tlačítko pro zarovnání v dialogu vyhledávání textů písní nebo výzvu v panelu, pokud stopa nemá žádné texty písní Klíč API QuickLRC uložen QuickLRC tento klíč nepřijal @@ -632,58 +675,50 @@ Platí pro oba limity. Při hodnotě 1,75 se čtyři znaky plné šířky počí Žádné texty k zarovnání – místo toho se provádí přepis QuickLRC tuto stopu nemohl zpracovat Pro tuto stopu došly kredity QuickLRC - "Texty jsou spárovány podle názvu a interpreta, neobvyklé metadata mohou vrátit nic nebo špatnou verzi. Použijte obnovit pro přepnutí verzí. Poskytovatelé označení * vyžadují token. Všechny povolené poskytovatelé nejsou vždy požádáni v pořadí, ale reagují na základě jejich rychlosti. + "Texty písní jsou hledány podle názvu a interpreta, netradiční metadata mohou nic nenajít nebo najít špatnou verzi. Použijte Obnovit pro přepínání verzí. Poskytovatelé označení * vyžadují token. Všichni povolení poskytovatelé nejsou vždy voláni ve vašem pořadí, ale odpovídají based on jejich rychlost. -Texty pocházejí ze služeb třetích stran, které nejsou přidruženy k Morphe, ani nepodporovány ani schváleny aplikací Morphe. Každý z nich se používá tak, jak je: texty, jejich dostupnost a licencování zůstávají u těchto služeb a jejich poskytovatelů licencí a jejich používání může být předmětem podmínek použití. Co dosáhne povolené služby, je pouze hledání, které jste požadovali - název, umělec a, kde tato služba potřebuje, video ID. Volitelný koncový bod AI a rychlá transkripce QuickLRC běží pouze po jejich nakonfigurování a přijmou text textu nebo video URL stopy." +Texty pocházejí ze služeb třetích stran, které nejsou spojeny, nesponzorované ani nepodporované společností Morphe. Každá se používá jak je: texty písní, jejich dostupnost a licencování zůstávají u těchto služeb a jejich licencentů a jejich použití může být předmětem jejich podmínek používání. Do povolené služby dorazí pouze dotaz, který jste požadovali – název, interpreta a, kde to tato služba potřebuje, ID videa. Volitelný endpoint AI a transkripce QuickLRC běží pouze po jeho konfiguraci a přijímají text písně nebo URL videa stopy." Povolte pouze poskytovatele, které používáte, nebo ponechte výchozí nastavení – neustálé požadavky mohou přetížit malé komunitní stránky s texty Skrýt řádek s uvedením zdroje Skrýt řádek s uvedením zdroje zobrazený pod textem písně. Prohodit překlad a romanizaci - Zapnuto umístí romanizaci pod texty a překlad nad texty; Vypnuto ponechá romanizaci nad texty a překlad pod texty. - Překlad - Romanizace - Poskytovatel překladu - Google (výchozí, zdarma) - Kompatibilní s OpenAI (vlastní) - Poskytovatel romanizace - Google (výchozí) - Lokální / offline slovník - Kompatibilní s OpenAI (vlastní) - Sekce AI - Používá se pouze v případě, že poskytovatel neposkytuje žádné popisky sekcí, jako je Refrén nebo Sloka, které poté detekuje váš model kompatibilní s OpenAI - AI & cloudové koncové body - Konfigurujte API klíče, modely, koncové body a šablony promptů pro poskytovatele AI. - URL koncového bodu API (např. https://api.openai.com/v1/chat/completions) + ON umisťuje romanizaci nebo transkripci pod texty a překlad nad ně; OFF ponechává romanizaci nebo transkripci nad texty a překlad pod nimi. Výslovnost umístěná nad každou slabiku se s ní pohybuje, od nad její slabikou po pod ni + Vždy centruvat texty + ON umisťuje každý řádek prostředek a ignoruje zdrojové zarovnání, včetně stran dueta; OFF ponechává zarovnání, o které texty požádají + Alespoň jeden poskytovatel musí být zapnutý + Překlad + Romanizace nebo transkripce + Offline slovník + Slovník Rime + Detekovat sekce pomocí AI + Používá se pouze tehdy, když poskytovatel neposkytuje žádné popisky sekci, jako Refrén nebo Verse, které pak detekuje váš kompatibilní model OpenAI. Úprava řádku také otevře jeho prompt + Adresy URL koncového bodu API (např. https://api.openai.com/v1/chat/completions, https://api.anthropic.com/v1/chat/completions, https://generativelanguage.googleapis.com/v1beta/openai/chat/completions) Šablona promptu Zástupné znaky: {language}, {task}, {title}, {artist}, {count}, {lines} Prompt uložen Výzva byla uložena, ale koncový bod AI zatím není použitelný. Nejprve jej nakonfigurujte pod Nastavení AI Prompt resetován - Neplatné JSON. Opravte to nebo vymažte pole před uložením Nastavení překladu & romanizace uloženo Nastavení obnoveno do výchozího stavu Základní URL musí začínat http:// nebo https:// - Testovat připojení - Konfigurovat koncové body - Zobrazit všechny koncové body + Nastavení je neúplné nebo zamítnuto poskytovatelem Ověřování připojení… - Poskytovatel překladu - Poskytovatel romanizace - Načíst modely + %1$s: Připojeno! (%2$s) + %1$s: Připojení selhalo. Zkontrolujte nastavení. + Nejdříve zadejte klíč API Načítání modelů… Vyberte model Nepodařilo se načíst modely. Zkontrolujte API klíč nebo připojení. - Nastavení Gemini API - Kompatibilní s OpenAI (vlastní koncové bod) - Nastavení DeepL API - API klíč + Příkaz Model Základní URL + Vyberte službu API klíč (volitelně) - Prompt pro překlad Extra tělo požadavku (volitelné JSON) + Extra hlavičky požadavku (volitelný JSON) + Uvozujte názvy i hodnoty, např. {\"api-key\": \"abc\"}. Neuvolněná hodnota obsahující / : nebo číslo je zamítnuta. Prázdná hodnota odstraňuje hlavičku. + Uvozujte názvy i hodnoty, např. {\"temperature\": 0.3}. Neuvolněná hodnota obsahující / nebo : je zamítnuta. Model a zprávy nelze nastavit zde. Autentizační klíč (Free nebo Pro) - Konfigurace API klíčů, modelů, koncových bodů a promptů 8.23.51 - Zakázat tučné ikony 9.13.51 - Obnovit karty Další v pořadí, Texty písní a Související diff --git a/patches/src/main/resources/addresources/values-cs-rCZ/youtube/strings.xml b/patches/src/main/resources/addresources/values-cs-rCZ/youtube/strings.xml index 5ceab88aab..911fe881d6 100644 --- a/patches/src/main/resources/addresources/values-cs-rCZ/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-cs-rCZ/youtube/strings.xml @@ -29,6 +29,8 @@ Second \"item\" text" Různé Video Obnovit starý vzhled nastavení YouTube + Zobrazit zprávy v YouTube snackbaru + Zprávy Morphe, jako potvrzení tlačítka přehrávače, se zobrazují v YouTube snackbaru místo vyskakovacího okna Odstranit omezení přehrávání na pozadí Zakázat automatické přehrávání Shorts v pozadí diff --git a/patches/src/main/resources/addresources/values-da-rDK/music/strings.xml b/patches/src/main/resources/addresources/values-da-rDK/music/strings.xml index 5ea8157b42..85d1ae9be2 100644 --- a/patches/src/main/resources/addresources/values-da-rDK/music/strings.xml +++ b/patches/src/main/resources/addresources/values-da-rDK/music/strings.xml @@ -461,12 +461,47 @@ For at sikre, at scrobbling synkroniseres under afspilning i baggrunden, skal du Vis Synkronisering Panel - AI-endepunkt Alle udbydere deaktiveret - indbygget tekst og indlejret tekst Nulstil udbyderrækkefølgen? Gendan udbyderne til standardprioriteten + Rime Ordlister + "Rime-ordliste har også brug for UniDict: i den offline ordbogsudbyder skal du bofte \"Kinesiske og kantonesiske tegn (UniDict)\" og lade den være færdig med at hente først. Uden det vil en liste, der aflæser tegn, ikke give nogen aflæsning. + +Boft de lister, som Rime-ordlisten skal aflæse, og tryk derefter på Gem for at hente dem. En tilføjede liste fjernes, når du fravælger den. + +Hver liste er dit eget ord-til-aflæsning-sæt, en .dict.yaml-fil med linjer som: +字 ham +典 din2 +Den første kolonne er ordet, den anden dets aflæsning; ekstra kolonner og linjer uden et tabulator-tegn ignoreres." + Ingen Rime-ordliste er markeret for sproget ovenfor. Tryk på dette navn for at markere en, eller vælg et sprog, der har en liste + Tilføj + Tilføj en Rime Orddliste + En .dict.yaml-fil eller en zip-mappe indeholdende en sådan fil med linjer som 字 ham og 典 din2. Listen hentes, når du bofter den og trykker på Gem, og licensen er din til at kontrollere + Sprogkode + To eller tre bogstaver, som i hak eller wuu + Navn + Vises under udtalen, som i Hakka (Meixian) + Adresse til orddlisten + https:// adressen til .dict.yaml eller dens zip + Tegn uden for et ord + Automatisk + Kantonesiske tegn + Mandarin-tegn + Ingen tegnuuttale + Udfyld kode, navn og adresse + Koden skal være to eller tre bogstaver, som i hak + Adressen skal starte med https:// + Navnet og adressen kan ikke indeholde en lodret streg + %1$s har allerede et ordbord + %1$s udtales af den indbyggede romaniserer og kan ikke tage en orddliste + Ikke anvendt: Dette sprog har allerede sin egen udtale, så afinkarker den for at fjerne den + Tilføjede %1$s - markér den og tryk Gem for at hente den + Orddlister klar + Orddlister klar (%1$d poster) + Læser orddlist: %1$d%% + Nedhentning af orddliste mislykkedes - tryk Gem igen for at prøve igen Ryd gemte tekster - Slet de tekster, der er gemt på denne enhed + Slet de %1$s MB sangtekster gemt på denne enhed Frigjorde %1$s MB Hent token Apple Music Media-bruger-token @@ -521,14 +556,22 @@ Krævede cookies: SID, HSID, SSID, SAPISID, __Secure-3PAPISID" Vis Kopier-knap Tryk for at kopiere oprindelige tekster som almindelig tekst; tryk længe for at gemme råtekstfilen til Downloads Vis Oversæt-knap - Tryk for at skifte oversættelse; tryk længe for kun oversættelse. Brugers provider-oversættelser, Google Translate eller en OpenAI-kompatibel API - Vis Romaniser-knap - Tryk for at skifte romanisering; tryk længe for kun romanisering. Bruger provider-romanisering, Google Translate eller en OpenAI-kompatibel API - Lokal romanisering og alignment - Romaniser på enheden, når provider, din model og Google Translate fejler. Læsningen følger hvert ord i en tidsstemt tekst; ordsynkronisering skal være slået til. Kinesisk, kantonesisk, japansk, koreansk, græsk, russisk, georgisk, armensk; ca. 120 MB - Læsning %1$s: %2$d%% + Tryk for at skifte oversættelse; langtrykk for kun oversættelse. Bruger de aktiverede oversættelsesudbydere i deres opregnede rækkefølge + Vis knap for Romanisering eller Transliteration + Tryk for at skifte romanisering eller translitteration; Langtryk for kun romanisering eller translitteration. Bruger de aktiverede romaniseringsudbydere i deres opregnede rækkefølge + Aktiver transliterering + Tillad ikke-Latin udtaler fra sangtekstkilden; Når slukket, behold kun Latin-bogstaver, accentbogstaver og tonecifre + Læser hvert ord i en synkroniseret sangtekst (ord-sync tændt); Korean behøver ingen download; fjorten scripter deler en CLDR-download; Han-tegne udtales af Rime Orddliste, når dets ordbord dækker sproget + Henter udtaleordbøger... Læsninger klar (%1$d poster) Hentning mislykkedes - sluk og tænd igen for at prøve igen + Offline Ordbog + Japansk (SudachiDict, ~75 MB) + Kinesiske og kantonesiske tegn (UniDict, ~8 MB) + Kinesiske ord (pinyin-pro, ~17 MB) + Grsk, slavisk kyrillisk (inkl. russisk, ukrainsk, bulgarsk, serbisk, hviderussisk, makedonsk), georgisk, armensk, hebraisk, arabisk, amharisk og syrisk translitering (CLDR, ~0,5 MB) + Læsning %1$s: %2$d%% + Vælg mindst én ordbog Vis opdateringsknap Tryk for at skifte til næste tekst kandidat; tryk længe for at søge efter tekster baseret på sangtitel og kunstner Søg efter sangtekster @@ -539,8 +582,10 @@ Krævede cookies: SID, HSID, SSID, SAPISID, __Secure-3PAPISID" Gemt Fremhæv synkroniserede ord eller stavelser Prioritér ord-for-ord-tekster, når de er tilgængelige. Når teksterne har ord-timing, fremhæves hvert ord eller stavelse i takt med musikken. + Indikere inaktive linjer + Trækker hver linje, der ikke synges lige nu, lidt mindre, ved at dæmpe den Vis baggrundsvokaler separat - Flytter en baggrundsvokal, der er skrevet i parenteser, som f.eks. \"(Oh) I want you\", ud af den sunge linje og over i en egen, svagere linje + Adskiller baggrundsvokaler og harmoni fra hovedlinjen. Flytter en baggrundsvokal skrevet i klammer, f.eks. \"(Oh) jeg vil dig\", ud af syngelinjen til en mørkere linje for sig selv Skjul […] linje Skjul sangtekstlinjer over den aktuelt afspillede linje Skjul ikke-afspillede tekster @@ -598,10 +643,15 @@ Gælder for begge grænser. Ved 1,75 tæller fire fuldbredde-tegn som 7."Oversat af %s Romaniseret af %s Romaniseret af %s + Romaniseret af Rime (%s) Oversat Oversættelsessprog - Romaniseringssprog + Sprog til romanisering eller transliteration + "Mål sprog for oversættelse: +%s" + "Mål sprog for romanisering eller translitteration: +%s" Oversætter… Kunne ikke oversætte teksten Romaniseret @@ -615,7 +665,7 @@ Gælder for begge grænser. Ved 1,75 tæller fire fuldbredde-tegn som 7."Opdaterer Opdateret QuickLRC API-nøgle - QuickLRC transskriberer eller tidstilpasser sangteksterne til det nuværende spor. En separat tjeneste, der ikke har noget at gøre med det OpenAI-kompatible endpoint ovenfor + QuickLRC transskriberer eller tidssynkroniserer sangteksten for det nuværende spor QuickLRC-nøgle gemt. Brug justeringsknappen i dialogboksen Sangtekst-søgning eller panelprompten, når et spor ikke har nogen sangtekster QuickLRC API-nøgle gemt QuickLRC accepterede ikke den nøgle @@ -631,58 +681,50 @@ Gælder for begge grænser. Ved 1,75 tæller fire fuldbredde-tegn som 7."Ingen sangtekster at justere - transskriberer i stedet QuickLRC kunne ikke behandle dette spor QuickLRC-kreditpoint er brugt op for dette spor - "Sangtekster matcher efter titel og kunstner. Unormal metadata kan give ingenting eller forkert version. Brug opdater for at skifte versioner. Udbydere mærket * kræver et token. Ikke alle aktiverede udbydere bliver altid anmodet i din rækkefølge, men svarer baseret på deres hastighed. + "Linjer matcher efter titel og artist; usædvanlig metadata kan resultere i ingen resultater eller forkert version. Brug opdatering til at skifte versioner. Udbydere mærket * kræver et token. Alle aktiverede udbydere anmodes ikke altid i rækkefølge, men svarer baseret på deres hastighed. -Sangtekster kommer fra tredjepartstjenester, der ikke er tilknyttet, sponsoreret eller godkendt af Morphe. Hver én bruges, som den er: sangteksterne, deres tilgængelighed og licensering forbliver hos disse tjenester og deres licensgivere, og brug af dem kan være underlagt deres servicevilkår. Det, der når en aktiveret tjeneste, er kun det lookup, du bad om - titlen, kunstneren og, hvor denne tjeneste har brug for det, video-ID. Den valgfrie AI-endepunkt og QuickLRC-transkription kører kun, efter at du konfigurerer dem, og modtager sangtekst eller trackets video-URL." +Linjer stammer fra tredjepartstjenester, som ikke er tilknyttet, sponsoreret eller godkendt af Morphe. Hver bruges som den er: linjerne, deres tilgængelighed og licensering forbliver hos disse tjenester og deres licensgivere, og brugen af dem kan være underlagt deres servicevilkår. Det, der sendes til en aktiv tjeneste, er kun det opslag, du anmodede om - titlen, kunstneren og, hvor den tjeneste behøver det, video-ID'et. Den valgfrie AI-endepunkt og QuickLRC-transskription kører kun efter konfiguration og modtager teksten til linjene eller sporrets video-URL." Slå kun udbyderne til, du bruger, eller behold standardindstillingerne - konstante anmodninger kan overvælde små fællesskabssangtekstwebsider Skjul kreditlinje Skjul kreditlinjen, der vises under teksterne Byt om på oversættelse og romanisering - TIL sætter romanisering under teksterne og oversættelse over; FRA beholder romanisering over og oversættelse under. - Oversættelse - Romanisering - Oversættelsesudbyder - Google (Standard, Gratis) - OpenAI-kompatibel (Brugerdefineret) - Udbyder af romanisering - Google (Standard) - Lokal / Offline ordbog - OpenAI-kompatibel (Brugerdefineret) - AI-sektion - Bruges kun, når udbyderen ikke leverer sektionsmærkater som Chorus eller Verse, som din OpenAI-kompatible model derefter detekterer - AI & Cloud-slutpunkter - Konfigurer API-nøgler, modeller, slutpunkter og prompt-skabeloner for AI-udbydere. - API-slutpunkt-URL (f.eks. https://api.openai.com/v1/chat/completions) + TIL sætter romanisering eller transliteration under sangtekst og oversættelse over; FRA holder romanisering eller transliteration over og oversættelse under. En udtale placeret over hver stavelse flytter sammen med den, fra over sin stavelse til under den + Centrér altid sangtekst + TIL sætter enhver linje midt imellem og ignorerer kildeafligningen, inklusiv duet-sider; FRA holder afligningen sangteksten beder om + Mindst én udbyder skal holdes aktiv + Oversættelse + Romanisering eller translitteration + Offline Ordbog + Rime Orddliste + Detektér sektioner med AI + Bruges kun når udbyderen leverer ingen sektionsetiketter som Omkvæd eller Vers, som din OpenAI-kompatible model derefter detektører. Redigering af rækken åbner også dens prompt + API-endepunkt URL (f.eks. https://api.openai.com/v1/chat/completions, https://api.anthropic.com/v1/chat/completions, https://generativelanguage.googleapis.com/v1beta/openai/chat/completions) Prompt-skabelon Pladsholdere: {language}, {task}, {title}, {artist}, {count}, {lines} Prompt gemt Prompt gemt, men AI-endpointet er endnu ikke brugbart. Konfigurer det under AI-indstillinger først Prompt nulstillet - Ikke gyldig JSON. Ret det eller ryd feltet før gemming Indstillinger for oversættelse & romanisering er gemt Indstillinger nulstillet til standard Base-URL skal starte med http:// eller https:// - Test forbindelse - Konfigurer endpoints - Vis alle endpoints + Indstillingerne er ufuldstændige eller blevet afvist af udbyderen Validerer forbindelse… - Oversættelsesudbyder - Romaniseringsudbyder - Hent modeller + %1$s: Forbundet! (%2$s) + %1$s: Forbindelse fejlede. Tjek indstillingerne. + Indtast først API-nøglen Henter modeller… Vælg en model Kunne ikke hente modeller. Kontroller API-nøgle eller forbindelse. - Gemini API-indstillinger - OpenAI-kompatibel (brugerdefineret endpoint) - DeepL API-indstillinger - API-nøgle + Opgave Model Base-URL + Vælg en tjeneste API-nøgle (valgfri) - Oversættelsesprompt Ekstra anmodningskrop (valgfri JSON) + Ekstra anmodningshoveder (valgfrit JSON) + Citer navne og værdier, f.eks. {\"api-key\": \"abc\"}. En uciteret værdi indeholdende / : eller et tal afvises. En tom værdi fjerner et hoved. + Citer navne og værdier, f.eks. {\"temperature\": 0.3}. En uciteret værdi indeholdende / eller : afvises. Model og beskeder kan ikke indstilles her. Autentificeringsnøgle (Free eller Pro) - Konfigurer API-nøgler, modeller, endpoints og prompts 8.23.51 - Deaktiver fede ikoner 9.13.51 - Gendan fanerne Næste, Tekster og Relaterede i Player diff --git a/patches/src/main/resources/addresources/values-da-rDK/youtube/strings.xml b/patches/src/main/resources/addresources/values-da-rDK/youtube/strings.xml index 08fca21717..9d7ce4d5b9 100644 --- a/patches/src/main/resources/addresources/values-da-rDK/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-da-rDK/youtube/strings.xml @@ -29,6 +29,8 @@ Second \"item\" text" Diverse Video Gendan gamle indstillingsmenuer + Vis beskeder i YouTube-snackbar + Morphe-beskeder, såsom bekræftelsen af en afspillerknap, vises i YouTube-snackbar i stedet for et pop op-vindue Fjern restriktioner for afspilning i baggrunden Deaktiver baggrundsafspilning diff --git a/patches/src/main/resources/addresources/values-de-rDE/music/strings.xml b/patches/src/main/resources/addresources/values-de-rDE/music/strings.xml index fd9d055f20..52d6954e7d 100644 --- a/patches/src/main/resources/addresources/values-de-rDE/music/strings.xml +++ b/patches/src/main/resources/addresources/values-de-rDE/music/strings.xml @@ -462,12 +462,47 @@ Um sicherzustellen, dass Scrobbling während der Wiedergabe im Hintergrund synch Anzeige Synchronisation Leiste - KI-Endpunkt Alle Anbieter deaktiviert - integrierte und eingebettete Liedtexte Anbieterreihenfolge zurücksetzen? Anbieter auf die Standard-Prioritätsreihenfolge zurücksetzen. + Rime-Wortliste + "Die Rime-Wortliste benötigt ebenfalls UniDict: Deaktivieren Sie im Offline-Wörterbuch-Anbieter nicht \"Chinesische und Kantonesische Zeichen (UniDict)\" und lassen Sie den Download zuerst abschließen. Ohne sie liefert eine Liste, die Zeichen liest, keine Lesung. + +Aktivieren Sie die Listen, die die Rime-Wortliste lesen soll, tippen Sie dann auf Speichern, um sie herunterzuladen. Eine hinzugefügte Liste wird entfernt, wenn Sie sie deaktivieren. + +Jede Liste ist Ihre eigene Wort-zu-Lesung-Tabelle, eine .dict.yaml mit Zeilen wie: +字 ham +典 din2 +Die erste Spalte ist das Wort, die zweite seine Lesung; zusätzliche Spalten und Zeilen ohne Tabulator werden ignoriert." + Für die obige Sprache ist keine Rime-Wortliste aktiviert. Tippen Sie auf diesen Namen, um eine zu aktivieren, oder wählen Sie eine Sprache mit einer Liste + Hinzufügen + Eine Rime-Wortliste hinzufügen + Eine .dict.yaml-Datei oder ein Zip-Archiv mit einer solchen Datei mit Zeilen wie 字 ham und 典 din2. Die Liste wird abgerufen, wenn Sie sie aktivieren und auf Speichern tippen, und ihre Lizenz können Sie selbst prüfen. + Sprachcode + Zwei oder drei Buchstaben, z. B. hak oder wuu + Name + Wird unter der Aussprache angezeigt, z. B. Hakka (Meixian) + Adresse der Wortliste + https://-Adresse der .dict.yaml-Datei oder ihres ZIP-Archivs + Zeichen in einem Wort nicht enthalten + Automatisch + Kantonesische Zeichen + Mandarin-Zeichen + Keine Zeichenaussprache + Geben Sie den Code, den Namen und die Adresse ein + Der Code muss zwei oder drei Buchstaben lang sein, z. B. hak + Die Adresse muss mit https:// beginnen + Der Name und die Adresse dürfen kein vertikales Trennzeichen enthalten + %1$s hat bereits eine Wassertabelle + %1$s wird vom integrierten Romanisierer gelesen und kann keine Wortliste verwenden + Nicht verwendet: Diese Sprache verfügt bereits über eine eigene Aussprache. Deaktivieren Sie sie, um sie zu entfernen + %1$s hinzugefügt - aktivieren Sie sie und tippen Sie auf Speichern, um sie herunterzuladen + Wortlisten bereit + Wortlisten bereit (%1$d Einträge) + Aussprache-Wortliste wird erstellt: %1$d%% + Herunterladen der Wortliste fehlgeschlagen - tippen Sie erneut auf Speichern, um es zu wiederholen Gespeicherte Liedtexte löschen - Löschen Sie die auf diesem Gerät gespeicherten Liedtexte + Löschen Sie die auf diesem Gerät gespeicherten %1$s MB Textlieder %1$s MB gelöscht Token abrufen. Apple Music Media-User-Token @@ -522,14 +557,22 @@ Erforderliche Cookies: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PA Kopieren-Schaltfläche anzeigen Tippen zum Kopieren der Original-Liedtexte als Klartext; langdrücken zum Speichern der Rohdaten-Quelle als Download Übersetzen-Schaltfläche anzeigen - Tippen zum Umschalten der Übersetzung; langdrücken für nur die Übersetzung. Verwendet Anbieterübersetzungen, Google Translate oder eine OpenAI-kompatible API - Schaltfläche „Romanisieren“ anzeigen. - Tippen zum Umschalten der Transliteration; langdrücken für nur die Transliteration. Verwendet Anbietertransliteration, Google Translate oder eine OpenAI-kompatible API - Lokale Transliteration und Ausrichtung - Transkribieren auf dem Gerät, wenn der Anbieter, Ihr Modell und Google Translate fehlschlagen. Die Lesung folgt jedem Wort eines getimten Liedtexts; Word-Sync muss aktiviert sein. Chinesisch, Kantonesisch, Japanisch, Koreanisch, Griechisch, Russisch, Georgisch, Armenisch; ca. 120 MB - Lesung %1$s: %2$d%% + Tippen Sie zum Umschalten der Übersetzung; lange drücken für nur die Übersetzung. Verwendet die aktivierten Übersetzungsanbieter in der aufgelisteten Reihenfolge + Romanisieren oder Transkribieren-Schaltfläche anzeigen + Tippen Sie zum Umschalten der Romanisierung oder Transliteration; lange drücken für nur Romanisierung oder Transliteration. Verwendet die aktivierten Romanisierungsanbieter in der aufgelisteten Reihenfolge + Transliteration aktivieren + Ermöglichen von nicht-lateinischen Aussprachen aus der Liedquelle; wenn deaktiviert, bleiben nur lateinische Buchstaben, Buchstaben mit Akzenten und Ton-Ziffern erhalten + Liest jedes Wort eines zeitgesteuerten Liedtexts (Wortsynchronisierung aktiv); Koreanisch erfordert keinen Download; vierzehn Skripten teilen sich einen CLDR-Download; Han-Zeichen werden von der Rime-Wortliste gelesen, wenn ihre Wassertabelle die Sprache abdeckt + Aussprachewörterbücher werden heruntergeladen… Lesungen bereit (%1$d Einträge) Download fehlgeschlagen - schalten Sie dies aus und wieder ein, um es erneut zu versuchen + Offline-Wörterbuch + Japanisch (SudachiDict, ~75 MB) + Chinesische und kantonesische Zeichen (UniDict, ~8 MB) + Chinesische Wörter (pinyin-pro, ~17 MB) + Griechisch, slawisches Kyrillisch (inkl. Russisch, Ukrainisch, Bulgarisch, Serbisch, Belarussisch, Mazedonisch), Georgisch, Armenisch, Hebräisch, Arabisch, Amharisch und Syrisch Transliteration (CLDR, ~0,5 MB) + Lesung %1$s: %2$d%% + Wählen Sie mindestens ein Wörterbuch Schaltfläche „Aktualisieren“ anzeigen Tippen zum Wechseln zum nächsten Liedtextkandidaten; langdrücken zum Suchen von Liedtexten nach Titel und Künstler Songtexte suchen @@ -540,8 +583,10 @@ Erforderliche Cookies: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PA Gespeichert Hebe synchronisierte Wörter oder Silben hervor. Bevorzuge wortweise Songtexte, wenn verfügbar. Wenn Songtexte Wort-Timing enthalten, hebt dies jedes Wort oder jede Silbe synchron zur Musik hervor. - Hintergrundstimmen separat anzeigen - Verschiebt einen im Klammern notierten Hintergrundgesang, z. B. \"(Oh) ich will dich\", aus der gesungenen Zeile in eine eigene, gedimmte Zeile + Inaktive Zeilen verkleinern + Zeichnet jede Zeile, die aktuell nicht gesungen wird, etwas kleiner, indem sie zusätzlich gedimmt wird + Hintergrundgesang separat anzeigen + Trennt Hintergrundgesang und Harmonie von der Hauptlinie. Verschiebt einen Hintergrundgesang in Klammern geschrieben, z. B. \"(Oh) I want you\", aus der gesungenen Linie in eine eigene, gedimmte Linie Nicht gespielte Songtexte ausblenden. Songtextzeilen oberhalb der aktuell abgespielten Zeile ausblenden Noch nicht abgespielte Songtextzeilen ausblenden @@ -599,10 +644,15 @@ Teilt das ausgefüllte Track-Format in zyklische Frames auf. 0 deaktiviert die A Übersetzt von %s Romanisiert von %s Romanisiert von %s + Von Rime romanisiert (%s) Übersetzt Übersetzungssprache - Romanisierungssprache + Sprache für Romanisierung oder Transliteration + "Zielsprache für Übersetzungen: +%s" + "Zielsprache für Romanisierung oder Transliteration: +%s" Wird übersetzt… Die Liedtexte konnten nicht übersetzt werden Romanisiert @@ -616,7 +666,7 @@ Teilt das ausgefüllte Track-Format in zyklische Frames auf. 0 deaktiviert die A Wird aktualisiert Aktualisiert. QuickLRC-API-Schlüssel - QuickLRC transkribiert oder zeitlich alignt die Lyrics für den aktuellen Track. Ein separater Dienst, der unabhängig vom oben genannten OpenAI-kompatiblen Endpunkt ist + QuickLRC transkribiert oder zeitlich alignet den Liedtext für den aktuellen Track QuickLRC-Schlüssel gespeichert. Verwenden Sie die Align-Schaltfläche im Lyrics-Suchdialog oder die Eingabeaufforderung im Panel, wenn ein Track keine Lyrics hat QuickLRC-API-Schlüssel gespeichert QuickLRC hat diesen Schlüssel nicht akzeptiert @@ -632,56 +682,50 @@ Teilt das ausgefüllte Track-Format in zyklische Frames auf. 0 deaktiviert die A Keine Lyrics zum Alignen - stattdessen transkribieren QuickLRC konnte diesen Titel nicht verarbeiten QuickLRC-Credits sind für diesen Titel aufgebraucht - "Liedtexte werden nach Titel und Interpret abgeglichen, ungewöhnliche Metadaten können nichts oder die falsche Version finden. Verwenden Sie Aktualisieren, um zwischen Versionen zu wechseln. Anbieter mit * benötigen ein Token. Nicht alle aktiven Anbieter werden immer in Ihrer Reihenfolge angefragt, sondern basierend auf ihrer Geschwindigkeit antworten.\n\nLiedtexte stammen von Drittanbieterdiensten, die keine Verbindung zu Morphe haben, nicht von Morphe gesponsert oder unterstützt werden. Jeder wird so verwendet, wie er ist: Der Liedtext, seine Verfügbarkeit und seine Lizenzierung bleiben bei diesen Diensten und ihren Lizengebern; deren Nutzung kann unterliegen deren Nutzungsbedingungen. Was einen aktivierten Dienst erreicht, ist nur die Lookup-Anfrage – Titel, Interpret und gegebenenfalls die Video-ID. Die optionale AI-Schnittstelle und die QuickLRC-Transkription werden nur ausgeführt, nachdem Sie diese konfiguriert haben, und erhalten den Liedtext oder die Video-URL des Titels." + "Liedtexte werden nach Titel und Künstler abgeglichen, ungewöhnliche Metadaten finden möglicherweise nichts oder die falsche Version. Verwenden Sie Aktualisieren, um zwischen Versionen zu wechseln. Anbieter mit * benötigen ein Token. Nicht alle aktivierten Anbieter werden immer in Ihrer Reihenfolge angefordert, sondern antworten basierend auf ihrer Geschwindigkeit. + +Liedtexte stammen von Drittanbieterdiensten, die keine Verbindung zu Morphe haben, nicht von Morphe gesponsert oder von Morphe unterstützt werden. Jeder wird so verwendet, wie er ist: Die Liedtexte, ihre Verfügbarkeit und ihre Lizenzierung verbleiben bei diesen Diensten und ihren Lizenzgebern, und deren Nutzung kann ihren Nutzungsbedingungen unterliegen. Was einen aktivierten Dienst erreicht, ist nur die Abfrage, die Sie angefordert haben - der Titel, der Künstler und, wo der Dienst es benötigt, die Video-ID. Der optionale KI-Endpunkt und die QuickLRC-Transkription laufen erst nach Ihrer Konfiguration und erhalten den Liedtext oder die Video-URL des Tracks." Aktiviere nur die Anbieter, die du nutzt, oder behalte die Standardeinstellung – ständige Anfragen können kleine Community-Liedertextseiten überlasten Creditszeile ausblenden. Blende die unter den Songtexten angezeigte Creditszeile aus. Übersetzung und Romanisierung vertauschen. - EIN setzt die Romanisierung unter die Songtexte und die Übersetzung darüber; AUS lässt die Romanisierung darüber und die Übersetzung darunter. - Übersetzung - Romanisierung - Übersetzungsanbieter - Google (Standard, Kostenlos) - OpenAI-kompatibel (Benutzerdefiniert) - Romanisierungs-Anbieter - Google (Standard) - Lokales / Offline-Wörterbuch - OpenAI-kompatibel (Benutzerdefiniert) - KI-Abschnitt - Wird nur verwendet, wenn der Anbieter keine Abschnittsbezeichnungen wie Refrain oder Strophe liefert, die dann von Ihrem OpenAI-kompatiblen Modell erkannt werden - KI & Cloud-Endpunkte - Konfigurieren Sie API-Schlüssel, Modelle, Endpunkte und Prompt-Vorlagen für KI-Anbieter. - API-Endpunkt-URL (z. B. https://api.openai.com/v1/chat/completions) + AN schaltet Romanisierung oder Transliteration unter den Liedtext und Übersetzung darüber; AUS behält Romanisierung oder Transliteration oben und Übersetzung unten. Eine über jeder Silbe platzierte Aussprache bewegt sich mit ihr, von oben ihrer Silbe nach unten + Liedtexte immer zentrieren + AN stellt jede Zeile in die Mitte und ignoriert die Quellenausrichtung, einschließlich Duett-Seiten; AUS behält die Ausrichtung, die der Liedtext verlangt + Mindestens ein Anbieter muss aktiviert bleiben + Übersetzung + Romanisierung oder Transliteration + Offline-Wörterbuch + Rime-Wortliste + Abschnitte mit KI erkennen + Wird nur verwendet, wenn der Anbieter keine Abschnittsetiketten wie Refrain oder Strophe mitbringt, die Ihr OpenAI-kompatibles Modell dann erkennt. Das Bearbeiten der Reihe öffnet auch deren Prompt + API-Endpunkt-URL (z. B. https://api.openai.com/v1/chat/completions, https://api.anthropic.com/v1/chat/completions, https://generativelanguage.googleapis.com/v1beta/openai/chat/completions) Prompt-Vorlage Platzhalter: {language}, {task}, {title}, {artist}, {count}, {lines} Prompt gespeichert Prompt gespeichert, aber der AI-Endpunkt ist noch nicht nutzbar. Konfigurieren Sie ihn zuerst unter AI-Einstellungen Prompt zurückgesetzt - Ungültiges JSON. Korrigieren Sie es oder leeren Sie das Feld vor dem Speichern Übersetzungs- & Romanisierungseinstellungen gespeichert Einstellungen auf Standardwerte zurückgesetzt Basis-URL muss mit http:// oder https:// beginnen - Verbindung testen - Endpunkte konfigurieren - Alle Endpunkte anzeigen + Einstellungen unvollständig oder vom Anbieter abgelehnt Verbindung wird überprüft… - Übersetzungsanbieter - Romanisierungsanbieter - Modelle abrufen + %1$s: Verbunden! (%2$s) + %1$s: Verbindung fehlgeschlagen. Überprüfen Sie die Einstellungen. + Geben Sie zuerst den API-Schlüssel ein Modelle werden abgerufen… Modell auswählen Modelle konnten nicht abgerufen werden. Überprüfen Sie den API-Schlüssel oder die Verbindung. - Gemini API-Einstellungen - OpenAI-kompatibel (benutzerdefinierter Endpunkt) - DeepL API-Einstellungen - API-Schlüssel + Aufforderung Modell Basis-URL + Dienste auswählen API-Schlüssel (optional) - Übersetzungsprompt Zusätzlicher Request-Body (optionales JSON) + Zusätzliche Anforderungs-Header (optionaler JSON) + Namen und Werte zitieren, z. B. {\"api-key\": \"abc\"}. Ein nicht zitierter Wert, der / : oder eine Zahl enthält, wird abgelehnt. Ein leerer Wert entfernt einen Header. + Namen und Werte zitieren, z. B. {\"temperature\": 0.3}. Ein nicht zitierter Wert, der / oder : enthält, wird abgelehnt. Modell und Nachrichten können hier nicht eingestellt werden. Authentifizierungsschlüssel (Free oder Pro) - API-Schlüssel, Modelle, Endpunkte und Prompts konfigurieren 8.23.51 - Fette Symbole deaktivieren 9.13.51 - Player-Tabs „Up next“, „Lyrics“ und „Verwandte“ wiederherstellen diff --git a/patches/src/main/resources/addresources/values-de-rDE/youtube/strings.xml b/patches/src/main/resources/addresources/values-de-rDE/youtube/strings.xml index ff4a074097..16e494685f 100644 --- a/patches/src/main/resources/addresources/values-de-rDE/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-de-rDE/youtube/strings.xml @@ -29,6 +29,8 @@ Second \"item\" text" Sonstiges Video Alte YouTube-Einstellungen wiederherstellen + Nachrichten in YouTube-Snackbar anzeigen + Morphe-Nachrichten, wie die Bestätigung einer Spieler-Schaltfläche, werden anstelle eines Popups in der YouTube-Snackbar angezeigt Einschränkungen für Hintergrundwiedergabe entfernen Hintergrundwiedergabe von Shorts deaktivieren diff --git a/patches/src/main/resources/addresources/values-el-rGR/music/strings.xml b/patches/src/main/resources/addresources/values-el-rGR/music/strings.xml index d09520ab93..ada98b6ece 100644 --- a/patches/src/main/resources/addresources/values-el-rGR/music/strings.xml +++ b/patches/src/main/resources/addresources/values-el-rGR/music/strings.xml @@ -462,12 +462,47 @@ Second \"item\" text" Εμφάνιση Συγχρονισμός Πίνακας - Τερματικό σημείο AI Όλοι οι πάροχοι απενεργοποιήθηκαν - ενσωματωμένες και εντοιχισμένες στίχοι Επαναφορά σειράς παρόχων; Επαναφορά των παρόχων στην προεπιλεγμένη σειρά προτεραιότητας + Λεξιλόγιο Rime + "Η λίστα λέξεων Rime απαιτεί επίσης το UniDict: στον πάροχο Offline Dictionary, επιλέξτε \"Chinese and Cantonese characters (UniDict)\" και αφήστε τον να ολοκληρώσει πρώτα τη λήψη. Χωρίς αυτό, μια λίστα που διαβάζει χαρακτήρες δεν παράγει ανάγνωση. + +Επιλέξτε τις λίστες που η λίστα λέξεων Rime πρέπει να διαβάσει, στη συνέχεια πατήστε Αποθήκευση για να τις κατεβάσει. Μια λίστα που προσθέσατε αφαιρείται όταν την αποεπιλέξετε. + +Κάθε λίστα είναι ο δικός σας πίνακας λέξης-ανάγνωσης, ένα .dict.yaml με γραμμές όπως: +字 ham +典 din2 +Η πρώτη στήλη είναι η λέξη, η δεύτερη η ανάγνωσή της· επιπλέον στήλες και γραμμές χωρίς tab αγνοούνται." + Καμία λίστα λεξιλογίου Rime δεν έχει επιλεγεί για τη γλώσσα παραπάνω. Πιέστε αυτό το όνομα για να επιλέξετε μια, ή επιλέξτε μια γλώσσα που έχει λίστα + Πρόσθεση + Προσθήκη Λεξιλογίου Rime + Ένα αρχείο .dict.yaml ή ένα zip που περιέχει ένα, με γραμμές όπως 字 ham και 典 din2. Η λίστα ανακτάται όταν την επιλέξετε και πατήσετε Αποθήκευση, και η άδειά της είναι δική σας για να ελέγξετε + Κωδικός γλώσσας + Δύο ή τρία γράμματα, όπως στα hak ή wuu + Όνομα + Εμφανίζεται κάτω από την ανάδευση, όπως στα Hakka (Meixian) + Διεύθυνση λίστας λεξιλογίου + Διεύθυνση https:// του .dict.yaml ή του zip του + Χαρακτήρες όχι σε λέξη + Αυτόματο + Χαρακτήρες Καντονέζικα + Χαρακτήρες Μανδαρινική + Χωρίς ανάδευση χαρακτήρα + Συμπληρώστε τον κωδικό, το όνομα και τη διεύθυνση + Ο κωδικός πρέπει να είναι δύο ή τρία γράμματα, όπως στα hak + Η διεύθυνση πρέπει να ξεκινά με https:// + Το όνομα και η διεύθυνση δεν μπορούν να περιέχουν κάθετη μπάρα + Το %1$s έχει ήδη έναν πίνακα λέξεων + Το %1$s αναλύεται από το ενσωματωμένο ρωμαϊστή και δεν μπορεί να λάβει λίστα λεξιλογίου + Αχρησιμοποίητο: αυτή η γλώσσα έχει ήδη την δική της ανάδευση, οπότε αποεπιλέξτε την για να την αφαιρέσετε + Προστέθηκε το %1$s - επιλέξτε το και πατήστε Αποθήκευση για να το κατεβάσετε + Οι λίστες λεξιλογίου είναι έτοιμες + Οι λίστες λεξιλογίου είναι έτοιμες (%1$d στοιχεία) + Κατασκευή λίστας ανάδευσης: %1$d%% + Η λήψη της λίστας λεξιλογίου απέτυχε - πιέστε ξανά την Αποθήκευση για επανάληψη Καθαρισμός αποθηκευμένων στίχων - Διαγραφή των στίχων που φυλάσσονται σε αυτή τη συσκευή + Διαγραφή %1$s MB στίχων που κρατώνται στη συσκευή Εκκαθάριση %1$s MB Λήψη token Apple Music διακριτικό χρήστη πολυμέσων @@ -522,14 +557,22 @@ Second \"item\" text" Εμφάνιση κουμπιού αντιγραφής Πατήστε για αντιγραφή των αρχικών στίχων ως απλό κείμενο· παρατεταμένο πάτημα για αποθήκευση του αρχείου πηγής ακατέργαστων στίχων στις Λήψεις Εμφάνιση κουμπιού μετάφρασης - Πατήστε για εναλλαγή μετάφρασης· παρατεταμένο πάτημα μόνο για μετάφραση. Χρησιμοποιεί μεταφράσεις παρόχου, Google Translate ή συμβατό API OpenAI - Εμφάνιση κουμπιού μεταγραφής. - Πατήστε για εναλλαγή ρωμοποίησης· παρατεταμένο πάτημα μόνο για ρωμοποίηση. Χρησιμοποιεί ρωμοποίηση παρόχου, Google Translate ή συμβατό API OpenAI - Τοπική Ρωμοποίηση και Στοίχιση - Ρωμοποίηση στη συσκευή όταν αποτύχει ο πάροχος, το μοντέλο σας και το Google Translate. Η ανάγνωση ακολουθεί κάθε λέξη ενός χρονολογημένου στίχου· η συγχρονισμένη λέξη πρέπει να είναι ενεργή. Κινέζικα, Καντονέζικα, Ιαπωνικά, Κορεάτικα, Ελληνικά, Ρωσικά, Γεωργιανά, Αρμενικά· περίπου 120 MB - Ανάγνωση %1$s: %2$d%% + Πιέστε για εναλλαγή μετάφρασης· πατήστε παρατεταμένα για μόνο μετάφραση. Χρησιμοποιεί τους ενεργοποιημένους παρόχους μετάφρασης με τη σειρά που αναφέρονται + Εμφάνιση κουμπιού Ρωμαϊσμού ή Μεταγραφής + Πιέστε για εναλλαγή ρωμαϊσμού ή μεταγραφής· πατήστε παρατεταμένα για μόνο ρωμαϊσμό ή μεταγραφή. Χρησιμοποιεί τους ενεργοποιημένους παρόχους ρωμαϊσμού με τη σειρά που αναφέρονται + Ενεργοποίηση μεταγραφής + Να επιτρέπονται μη λατινικές αναδευμένες μορφές από την πηγή στίχων· όταν είναι απενεργοποιημένη, διατηρήστε μόνο λατινικά γράμματα, τόνους και ψηφία τόνων + Διαβάζει κάθε λέξη ενός στίχου χρονοβολημένου (συγχρονισμός λέξης ενεργός). Η Κορεάτικα δεν χρειάζεται λήψη; δεκατέσσερις γραφές μοιράζονται μία λήψη CLDR. Οι χαρακτήρες Han αναλύονται από το Λεξιλόγιο Rime όταν ο πίνακας λεξιλογίου του καλύπτει τη γλώσσα + Λήψη λεξικών αναδευμένων μορφών… Αναγνώσεις έτοιμες (%1$d καταχωρήσεις) Η λήψη απέτυχε - απενεργοποιήστε και επανεκτελέστε για επανάληψη + Λεξικό Εκτός Γραμμής + Ιαπωνικά (SudachiDict, ~75 MB) + Χαρακτήρες Κινέζικα και Καντονέζικα (UniDict, ~8 MB) + Λέξεις Κινέζικα (pinyin-pro, ~17 MB) + Ρωμαϊσμός/Μεταγραφή Ελληνικά, Σλαβικά Κυριλλικά (περ. Ρωσική, Ουκρανική, Βουλγαρική, Σερβική, Λευκορωσική, Μακεδονική), Γεωργιανά, Αρμενικά, Εβραϊκά, Αραβικά, Αμχαρικά και Συριακά (CLDR, ~0.5 MB) + Ανάγνωση %1$s: %2$d%% + Επιλέξτε τουλάχιστον ένα λεξικό Εμφάνιση κουμπιού ανανέωσης Πατήστε για εναλλαγή στον επόμενο υποψήφιο στίχο· παρατεταμένο πάτημα για αναζήτηση στίχων με τίτλο τραγουδιού και καλλιτέχνη Αναζήτηση στίχων @@ -540,8 +583,10 @@ Second \"item\" text" Αποθηκεύτηκε Επισήμανση συγχρονισμένων λέξεων ή συλλαβών Δώστε προτεραιότητα στους στίχους λέξη-λέξη όταν είναι διαθέσιμοι. Όταν οι στίχοι έχουν χρονισμό λέξεων, επισημαίνεται κάθε λέξη ή συλλαβή σε συγχρονισμό με τη μουσική. - Εμφάνιση των φωνητικών φόντου ξεχωριστά - Μετακινεί ένα φωνητικό υπόβαθρο που γράφεται σε αγκύλες, όπως \"(Oh) I want you\", εκτός της τραγουδισμένης γραμμής σε μια ξεχωριστή γραμμή με μικρότερη ένταση + Συστολή αδρανών γραμμών + Σχεδιάζει κάθε γραμμή που δεν τραγουδιέται αυτή τη στιγμή ελαφρώς μικρότερη, πάνω από το σκούρωμά της + Εμφάνιση φωνητικών υποστήριξης ξεχωριστά + Διαχωρίζει τα φωνητικά υποστήριξης και τον ομόφωνο από την κύρια γραμμή. Μεταφέρει ένα φωνητικό υποστήριξης γραμμένο σε αγκύλες, όπως \"(Oh) I want you\", έξω από τη γραμμή που τραγουδιέται σε μια ξεχωριστή γραμμή με μεγαλύτερο σκούρωμα Απόκρυψη γραμμής που έχει παιχτεί. Απόκρυψη γραμμών στίχων πάνω από τον τρέχοντα στίχο Απόκρυψη στίχων που δεν έχουν παιχτεί. @@ -599,10 +644,15 @@ Second \"item\" text" Μεταφράστηκε από %s Μεταγράφηκε σε λατινικούς χαρακτήρες από %s Ρωμαιοποιήθηκε από %s + Ρωμανισμένο από το Rime (%s) Μεταφρασμένο Γλώσσα μετάφρασης - Γλώσσα ρωμαιοποίησης + Γλώσσα ρωμαϊσμού ή μεταγραφής + "Γλώσσα στόχος μετάφρασης: +%s" + "Γλώσσα στόχου ρωμαϊκής γραφής ή μεταγραφής: +%s" Μετάφραση… Δεν ήταν δυνατή η μετάφραση των στίχων Μεταγεγραμμένο @@ -616,7 +666,7 @@ Second \"item\" text" Ανανέωση Ανανεώθηκε API κλειδί QuickLRC - Το QuickLRC μεταγράφει ή χρονολογεί στίχους για το τρέχον κομμάτι. Ανεξάρτητη υπηρεσία, μη σχετική με το παραπάνω τελικό σημείο συμβατό με το OpenAI + Το QuickLRC αντιγράφει ή χρονολογεί τους στίχους για το τρέχον κομμάτι Το κλειδί του QuickLRC αποθηκεύτηκε. Χρησιμοποιήστε το κουμπί ευθυγράμμισης στον διάλογο αναζήτησης στίχων, ή την ειδοποίηση στο πάνελ όταν ένα κομμάτι δεν διαθέτει στίχους Το API κλειδί QuickLRC έχει αποθηκευτεί Το QuickLRC δεν δέχτηκε αυτό το κλειδί @@ -632,58 +682,50 @@ Second \"item\" text" Δεν υπάρχουν στίχοι προς ευθυγράμμιση - γίνεται μεταγραφή αντίθετα Το QuickLRC δεν μπόρεσε να επεξεργαστεί αυτό το κομμάτι Τα πιστωτικά μονάδες QuickLRC εξαντλήθηκαν για αυτό το κομμάτι - "Οι στίχοι ταιριάζονται με βάση τον τίτλο και τον καλλιτέχνη· τα μη τυπικά metadata μπορεί να μην βρουν τίποτα ή τη λάθος έκδοση. Χρησιμοποιήστε ανανέωση για εναλλαγή εκδόσεων. Οι πάροχοι με * απαιτούν token. Όλοι οι ενεργοποιημένοι πάροχοι δεν ζητούνται πάντα με τη σειρά σας, αλλά ανταποκρίνονται με βάση την ταχύτητά τους. + "Τα στίχοι ταιριάζουν με βάση τον τίτλο και τον καλλιτέχνη, τα ασυνήθιστα metadata μπορεί να μην βρουν τίποτα ή τη λάθος έκδοση. Χρησιμοποιήστε refresh για εναλλαγή εκδόσεων. Οι πάροχοι με * απαιτούν token. Όχι όλοι οι ενεργοποιημένοι πάροχοι ζητούνται πάντα με τη σειρά σας, αλλά ανταποκρίνονται με βάση την ταχύτητά τους. -Οι στίχοι προέρχονται από υπηρεσίες τρίτων που δεν συνδέονται, χορηγούνται ή ενθαρρύνονται από τη Morphe. Κάθε μία χρησιμοποιείται ως έχει: οι στίχοι, η διαθεσιμότητά τους και η άδειά τους παραμένουν στις αντίστοιχες υπηρεσίες και στους αδειοδότες τους, και η χρήση τους μπορεί να υπόκειται στους όρους υπηρεσίας τους. Αυτό που φτάνει σε έναν ενεργοποιημένο πάροχο είναι μόνο η αναζήτηση που ζητήσατε - ο τίτλος, ο καλλιτέχνης και, όπου χρειάζεται, το ID βίντεο. Η προαιρετική AI διεύθυνση και η μετάφραση QuickLRC εκτελούνται μόνο αφού τις διαμορφώσετε και λαμβάνουν το κείμενο των στίχων ή το URL βίντεο του κομματιού." +Τα στίχοι προέρχονται από υπηρεσίες τρίτων που δεν συνδέονται, χορηγούν ή υποστηρίζονται από το Morphe. Κάθε μία χρησιμοποιείται ως έχει: οι στίχοι, η διαθεσιμότητά τους και η αδειοδότησή τους παραμένουν σε αυτές τις υπηρεσίες και τους αδειούχους τους, και η χρήση τους μπορεί να υπόκειται στους όρους υπηρεσίας τους. Αυτό που φτάνει σε έναν ενεργοποιημένο πάροχο είναι μόνο η αναζήτηση που ζητήσατε - ο τίτλος, ο καλλιτέχνης και, όπου χρειάζεται εκείνος ο πάροχος, το ID του βίντεο. Το προαιρετικό endpoint AI και η μεταγραφή QuickLRC εκτελούνται μόνο αφού τις διαμορφώσετε, και λαμβάνουν το κείμενο των στίχων ή το URL του βίντεο του κομματιού." Ενεργοποιήστε μόνο τους παρόχους που χρησιμοποιείτε, ή αφήστε τις προεπιλογές - οι σταθερές αιτήσεις μπορεί να υπερφορτώσουν ιστοσελίδες κοινότητας με στίχους Απόκρυψη γραμμής πιστώσεων. Απόκρυψη της γραμμής πιστώσεων που εμφανίζεται κάτω από τους στίχους. Εναλλαγή μετάφρασης και μεταγραφής. - ON: τοποθετεί τη μεταγραφή κάτω από τους στίχους και τη μετάφραση πάνω· OFF: διατηρεί τη μεταγραφή πάνω και τη μετάφραση κάτω. - Μετάφραση - Ρωμαϊκή μεταγραφή - Πάροχος μετάφρασης - Google (Προεπιλεγμένο, Δωρεάν) - Συμβατό με OpenAI (Προlæτομιζόμενο) - Πάροχος ρωμανισμού - Google (Προεπιλεγμένο) - Τοπικό / Εκτός σύνδεσης Λεξικό - Συμβατό με OpenAI (Προlæτομιζόμενο) - Ενότητα AI - Χρησιμοποιείται μόνο όταν ο πάροχος δεν παρέχει ετικέτες ενοτήτων, όπως Ρεφρέν ή Στροφή, τις οποίες στη συνέχεια ανιχνεύει το μοντέλο συμβατό με το OpenAI - AI & Cloud Endpoints - Διαμόρφωση κλειδιών API, μοντέλων, endpoints και προτύπων προτροπών για παρόχους AI. - URL του API endpoint (π.χ. https://api.openai.com/v1/chat/completions) + Το ON βάζει τον ρωμαϊσμό ή τη μεταγραφή κάτω από τους στίχους και τη μετάφραση πάνω· Το OFF διατηρεί τον ρωμαϊσμό ή τη μεταγραφή πάνω και τη μετάφραση κάτω. Μια ανάδευση τοποθετημένη πάνω από κάθε σύλλαβα κινείται μαζί της, από πάνω στη σύλλαβα προς τα κάτω + Κεντράρεμα στίχων πάντα + Το ON βάζει κάθε γραμμή στο κέντρο και αγνοεί την ευθυγράμμιση της πηγής, συμπεριλαμβανομένων των πλευρών duet· Το OFF διατηρεί την ευθυγράμμιση που ζητούν οι στίχοι + Πρέπει να παραμείνει ενεργός τουλάχιστον ένας πάροχος + Μετάφραση + Ρωμαϊσμός ή μεταγραφή + Λεξικό Εκτός Γραμμής + Λεξιλόγιο Rime + Ανίχνευση τμημάτων με AI + Χρησιμοποιείται μόνο όταν ο πάροχος δεν παρέχει ετικέτες τμημάτων όπως Refrain ή Στροφή, τις οποίες τότε εντοπίζει το μοντέλο συμβατό με OpenAI. Η επεξεργασία της γραμμής ανοίγει επίσης το prompt + URL τελικού σημείου API (π.χ. https://api.openai.com/v1/chat/completions, https://api.anthropic.com/v1/chat/completions, https://generativelanguage.googleapis.com/v1beta/openai/chat/completions) Πρότυπο Prompt Υποκαταστάτες: {language}, {task}, {title}, {artist}, {count}, {lines} Το prompt αποθηκεύτηκε Η εκκίνηση αποθηκεύτηκε, αλλά το τελικό σημείο AI δεν είναι ακόμα διαθέσιμο. Πριν το χρησιμοποιήσετε, ρυθμίστε το στις Ρυθμίσεις AI Το prompt επαναφέρθηκε - Μη έγκυρο JSON. Διορθώστε το ή καθαρίστε το πεδίο πριν από την αποθήκευση Αποθηκεύτηκαν οι ρυθμίσεις Μετάφρασης & Ρωμαΐзации Οι ρυθμίσεις επαναφέρθηκαν στις προεπιλεγμένες Το βασικό URL πρέπει να ξεκινά με http:// ή https:// - Δοκιμή Σύνδεσης - Διαμόρφωση Τελικών Σημείων - Εμφάνιση Όλων των Τελικών Σημείων + Ρυθμίσεις μη συμπληρωμένες ή απορριφθείσες από τον πάροχο Έλεγχος σύνδεσης… - Πάροχος Μετάφρασης - Πάροχος Ρωμαΐзации - Ανάκτηση Μοντέλων + %1$s: Συνδέθηκε! (%2$s) + %1$s: Απέτυχε η σύνδεση. Ελέγξτε τις ρυθμίσεις. + Εισάγετε πρώτα το κλειδί API Ανάκτηση μοντέλων… Επίλεξε ένα Μοντέλο Αποτυχία ανάκτησης μοντέλων. Έλεγξε το API key ή τη σύνδεση. - Ρυθμίσεις Gemini API - Συμβατό με OpenAI (Προσαρμοσμένο Endpoint) - Ρυθμίσεις DeepL API - Κλειδί API + Prompt Μοντέλο Βασική διεύθυνση URL + Επιλογή υπηρεσίας Κλειδί API (προαιρετικό) - Προτροπή μετάφρασης Πρόσθετο σώμα αιτήματος (προαιρετικό JSON) + Επιπρόσθετες κεφαλίδες αιτήματος (προαιρετικό JSON) + Παράθεση ονομάτων και τιμών, π.χ. {\"api-key\": \"abc\"}. Μια μη παρατιθέμενη τιμή που περιέχει / : ή αριθμό απορρίπτεται. Μια κενή τιμή αφαιρεί μια κεφαλίδα. + Παράθεση ονομάτων και τιμών, π.χ. {\"temperature\": 0.3}. Μια μη παρατιθέμενη τιμή που περιέχει / ή : απορρίπτεται. Δεν μπορούν να οριστούν εδώ το Model και τα messages. Κλειδί αυθεντικοποίησης (Free ή Pro) - Διαμόρφωση κλειδιών API, μοντέλων, endpoints και προτροπών 8.23.51 - Απενεργοποίηση έντονων εικονιδίων 9.13.51 - Επαναφορά καρτελών Up next, Στίχοι και Σχετικά του Player diff --git a/patches/src/main/resources/addresources/values-el-rGR/youtube/strings.xml b/patches/src/main/resources/addresources/values-el-rGR/youtube/strings.xml index 8848cea4c3..8a242c70fd 100644 --- a/patches/src/main/resources/addresources/values-el-rGR/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-el-rGR/youtube/strings.xml @@ -29,6 +29,8 @@ Second \"item\" text" Διάφορα Βίντεο Επαναφορά του παλιού μενού ρυθμίσεων YouTube + Εμφάνιση μηνυμάτων στο snackbar του YouTube + Τα μηνύματα του Morphe, όπως η επιβεβαίωση ενός κουμπιού αναπαραγωγής, εμφανίζονται στο snackbar του YouTube αντί για παράθυρο διαλόγου Κατάργηση περιορισμών αναπαραγωγής στο παρασκήνιο Απενεργοποίηση αναπαραγωγής παρασκηνίου diff --git a/patches/src/main/resources/addresources/values-es-rES/music/strings.xml b/patches/src/main/resources/addresources/values-es-rES/music/strings.xml index f5d3e33c5d..c3255b0536 100644 --- a/patches/src/main/resources/addresources/values-es-rES/music/strings.xml +++ b/patches/src/main/resources/addresources/values-es-rES/music/strings.xml @@ -462,12 +462,47 @@ Para garantizar que la sincronización se realice durante la reproducción en se Pantalla Sincronización Panel - Endpoint de IA Todos los proveedores desactivados: letras integradas y letras incrustadas ¿Restablecer orden de proveedores? Restaura los proveedores al orden de prioridad predeterminado + Lista de palabras Rime + "La lista de palabras Rime también necesita UniDict: en el proveedor del diccionario sin conexión, marca \"Caracteres chinos y cantoneses (UniDict)\" y deja que termine la descarga primero. Sin esto, una lista que lee caracteres no proporciona lectura. + +Marca las listas que la lista de palabras Rime debe leer, luego toca Guardar para descargarlas. Una lista que agregaste se elimina cuando desmarcas. + +Cada lista es tu propia tabla de palabra-a-lectura, un .dict.yaml con líneas como: +字 ham +典 din2 +La primera columna es la palabra, la segunda su lectura; se ignoran columnas adicionales y líneas sin tabulación." + No hay ninguna lista de palabras Rime marcada para el idioma anterior. Toca este nombre para marcar una, o selecciona un idioma que tenga una lista + Añadir + Añadir una Lista de palabras Rime + Un archivo .dict.yaml, o un zip que lo contenga, con líneas como 字 ham y 典 din2. La lista se obtiene cuando la marcas y tocas Guardar, y su licencia es tuya para verificar + Código del idioma + Dos o tres letras, como en hak o wuu + Nombre + Se muestra debajo de la lectura, como en Hakka (Meixian) + Dirección de la lista de palabras + Dirección https del archivo .dict.yaml o su versión zip + Caracteres no presentes en una palabra + Automático + Caracteres cantoneses + Caracteres mandarín + Sin lectura de carácter + Introduce el código, el nombre y la dirección + El código debe tener dos o tres letras, como en hak + La dirección debe comenzar con https:// + El nombre y la dirección no pueden contener una barra vertical + %1$s ya tiene una tabla de palabras + %1$s es leído por el romanizador integrado y no puede aceptar una lista de palabras + No utilizado: este idioma ya tiene su propia lectura, por lo tanto, desmárcalo para eliminarlo + Añadido %1$s - márca-lo y toca Guardar para descargarlo + Listas de palabras listas + Listas de palabras listas (%1$d entradas) + Creando lista de lectura: %1$d%% + Descarga de la lista de palabras fallida - toca Guardar de nuevo para reintentar Eliminar letras almacenadas - Elimina las letras guardadas en este dispositivo + Eliminar %1$s MB de letras guardados en este dispositivo %1$s MB liberados Obtener token Media-user-token de Apple Music @@ -527,14 +562,22 @@ Cookies requeridas: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPIS Mostrar botón Copiar Pulsa para copiar la letra original como texto sin formato; mantén pulsado para guardar el archivo fuente de la letra sin formato en Descargas Mostrar botón Traducir - Toca para alternar la traducción; mantén pulsado solo para traducir. Usa traducciones del proveedor, Google Traductor o una API compatible con OpenAI - Mostrar botón Romanizar - Toca para alternar la romanización; mantén pulsado solo para romanizar. Usa romanización del proveedor, Google Traductor o una API compatible con OpenAI - Romanización y alineación local - Romaniza en el dispositivo cuando falla el proveedor, tu modelo y Google Traductor. La lectura sigue cada palabra de la letra sincronizada; la sincronización por palabras debe estar activada. Chino, cantonés, japonés, coreano, griego, ruso, georgiano, armenio; unos 120 MB - Lectura %1$s: %2$d%% + Toca para alternar la traducción; mantén pulsado solo para traducción. Utiliza los proveedores de traducción habilitados en su orden indicado + Mostrar botón de romanización o transliteración + Toca para alternar romanización o transliteración; mantén pulsado solo para romanización o transliteración. Utiliza los proveedores de romanización habilitados en su orden indicado + Habilitar transliteración + Permitir lecturas no latinas procedentes de la fuente de letras; cuando está desactivado, conserva solo letras latinas, letras acentuadas y dígitos de tono + Lee cada palabra de una letra sincronizada por tiempo (sincronización de palabras activada); coreano no requiere descarga; catorce scripts comparten una descarga CLDR; los caracteres Han son leídos por la Lista de palabras Rime cuando su tabla de palabras cubre el idioma + Descargando diccionarios de lectura… Lecturas listas (%1$d entradas) Descarga fallida: desactiva y activa esto para reintentar + Diccionario sin conexión + Japonés (SudachiDict, ~75 MB) + Caracteres chinos y cantoneses (UniDict, ~8 MB) + Palabras chinas (pinyin-pro, ~17 MB) + Transliteración de griego, cirílico eslavo (incl. ruso, ucraniano, búlgaro, serbio, bielorruso, macedonio), georgiano, armenio, hebreo, árabe, amhara y siríaco (CLDR, ~0.5 MB) + Lectura %1$s: %2$d%% + Selecciona al menos un diccionario Mostrar botón Actualizar Toca para cambiar a la siguiente letra candidata; mantén pulsado para buscar letras por título de canción y artista Buscar letras @@ -545,8 +588,10 @@ Cookies requeridas: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPIS Guardado Resaltar palabras o sílabas sincronizadas Prioriza las letras palabra por palabra cuando estén disponibles. Cuando las letras incluyen tiempos por palabra, resalta cada palabra o sílaba al ritmo de la música + Reducir líneas inactivas + Dibuja cada línea que no se está cantando actualmente ligeramente más pequeña, además de atenuarla Mostrar coros por separado - Mueve una línea de coros de fondo escrita entre paréntesis, como \"(Oh) te quiero\", fuera de la línea principal cantada a una línea propia con menor intensidad + Separa los coros y armonías de la línea principal. Mueve un coro escrito entre paréntesis, como \"(Oh) te quiero\", fuera de la línea cantada hacia una línea más tenue por sí misma Ocultar letras ya reproducidas Oculta las líneas de letra por encima de la línea que se está reproduciendo Ocultar letras no reproducidas @@ -604,10 +649,15 @@ Se aplica a ambos límites. A 1.75, cuatro caracteres de ancho completo cuentan Traducido por %s Romanizado por %s Romanizado por %s + Romanizado por Rime (%s) Traducido Idioma de traducción - Idioma de romanización + Idioma de romanización o transliteración + "Idioma objetivo de traducción: +%s" + "Idioma objetivo de romanización o transliteración: +%s" Traduciendo… No se pudieron traducir las letras Romanizado @@ -621,7 +671,7 @@ Se aplica a ambos límites. A 1.75, cuatro caracteres de ancho completo cuentan Actualizando Actualizado Clave API de QuickLRC - QuickLRC transcribe o ajusta temporalmente las letras de la pista actual. Un servicio separado, no relacionado con el punto final compatible con OpenAI anterior + QuickLRC transcribe o alinea temporalmente las letras de la pista actual Clave de QuickLRC guardada. Usa el botón alinear en el diálogo de búsqueda de letras, o el aviso del panel cuando una pista no tenga letras Clave API de QuickLRC guardada QuickLRC no aceptó esa clave @@ -637,58 +687,50 @@ Se aplica a ambos límites. A 1.75, cuatro caracteres de ancho completo cuentan No hay letras para alinear - transcribiendo en su lugar QuickLRC no pudo procesar esta pista Los créditos de QuickLRC se han agotado para esta pista - "Las letras se emparejan por título y artista; los metadatos inusuales pueden no encontrar nada o la versión incorrecta. Usa actualizar para cambiar de versión. Los proveedores marcados con * requieren un token. No siempre se solicitan todos los proveedores habilitados en el orden establecido, sino que responden según su velocidad. + "Las letras se emparejan por título e intérprete; metadatos inusuales pueden no encontrar nada o la versión incorrecta. Usa actualizar para cambiar de versiones. Los proveedores marcados con * requieren un token. No siempre se solicitan todos los proveedores habilitados en tu orden, pero responden según su velocidad. -Las letras provienen de servicios de terceros que no están afiliados, patrocinados ni avalados por Morphe. Cada uno se utiliza tal cual: las letras, su disponibilidad y sus licencias permanecen con esos servicios y sus concesionantes, y su uso puede estar sujeto a sus condiciones de servicio. Lo que se envía a un servicio habilitado es únicamente la búsqueda solicitada: el título, el artista y, donde el servicio lo necesite, el ID del vídeo. El punto final opcional de IA y la transcripción QuickLRC solo se ejecutan después de configurarlos y reciben el texto de la letra o la URL de vídeo de la pista." +Las letras provienen de servicios de terceros que no están afiliados, patrocinados ni respaldados por Morphe. Cada uno se utiliza tal cual: las letras, su disponibilidad y su licenciamiento permanecen en esos servicios y sus licenciantes, y su uso puede estar sujeto a sus términos de servicio. Lo que llega a un servicio habilitado es solo la búsqueda que solicitaste: el título, el artista y, donde ese servicio lo necesite, el ID de video. El punto final opcional de IA y la transcripción QuickLRC solo se ejecutan después de configurarlos, y reciben el texto de las letras o la URL de video de la pista." Activa solo los proveedores que uses, o mantén el predeterminado: las peticiones constantes pueden saturar los pequeños sitios de letras comunitarios Ocultar línea de créditos Oculta la línea de créditos que se muestra debajo de las letras Intercambiar traducción y romanización - Activado coloca la romanización debajo de las letras y la traducción arriba; desactivado mantiene la romanización arriba y la traducción abajo - Traducción - Romanización - Proveedor de traducción - Google (predeterminado, gratuito) - Compatible con OpenAI (personalizado) - Proveedor de romanización - Google (predeterminado) - Diccionario local / sin conexión - Compatible con OpenAI (personalizado) - Sección con IA - Solo se utiliza cuando el proveedor no envía etiquetas de sección, como «Estribillo» o «Verso», que tu modelo compatible con OpenAI detecta a continuación - Endpoints de IA y nube - Configure claves API, modelos, endpoints y plantillas de prompts para proveedores de IA. - URL del endpoint de la API (p. ej., https://api.openai.com/v1/chat/completions) + ON coloca romanización o transliteración debajo de las letras y traducción encima; OFF mantiene romanización o transliteración encima y traducción debajo. Una colocación sobre cada sílaba se mueve con ella, desde arriba de su sílaba hasta debajo de ella + Centrar siempre las letras + ON coloca todas las líneas en el centro e ignora la alineación original, incluidos los lados del dúo; OFF mantiene la alineación que piden las letras + Debe haber al menos un proveedor activado + Traducción + Romanización o transliteración + Diccionario sin conexión + Lista de palabras Rime + Detectar secciones con IA + Solo se utiliza cuando el proveedor no envía etiquetas de sección, como «Estribillo» o «Verso», que tu modelo compatible con OpenAI detecta a continuación. Editar la fila también abre su prompt + URL del extremo de la API (por ej. https://api.openai.com/v1/chat/completions, https://api.anthropic.com/v1/chat/completions, https://generativelanguage.googleapis.com/v1beta/openai/chat/completions) Plantilla de Prompt Marcadores de posición: {language}, {task}, {title}, {artist}, {count}, {lines} Prompt guardado Aviso guardado, pero el punto final de IA aún no es utilizable. Configúralo primero en Ajustes de IA Prompt restablecido - No es JSON válido. Corígelo o limpia el campo antes de guardar Ajustes de traducción y romanización guardados Ajustes restablecidos a predeterminados La URL base debe empezar por http:// o https:// - Probar conexión - Configurar endpoints - Mostrar todos los endpoints + Configuración incompleta o rechazada por el proveedor Validando la conexión… - Proveedor de traducción - Proveedor de romanización - Obtener modelos + %1$s: ¡Conectado! (%2$s) + %1$s: Fallo en la conexión. Verifica la configuración. + Introduce primero la clave API Obteniendo modelos... Seleccionar un modelo Error al obtener los modelos. Comprueba la clave API o la conexión. - Ajustes de la API de Gemini - Compatible con OpenAI (endpoint personalizado) - Ajustes de la API de DeepL - Clave de API + Indicación Modelo URL base + Elegir un servicio Clave de API (opcional) - Prompt de traducción Cuerpo de solicitud adicional (JSON opcional) + Encabezados de solicitud adicionales (JSON opcional) + Pon nombres y valores entre comillas, por ej. {\"api-key\": \"abc\"}. Se rechaza un valor sin comillas que contenga / : o un número. Un valor vacío elimina un encabezado. + Pon nombres y valores entre comillas, por ej. {\"temperature\": 0.3}. Se rechaza un valor sin comillas que contenga / o :. El modelo y los mensajes no se pueden configurar aquí. Clave de autenticación (Gratuita o Pro) - Configurar claves de API, modelos, endpoints y prompts 8.23.51 - Desactivar iconos en negrita 9.13.51 - Restaurar pestañas del reproductor \'A continuación\', Letra y Relacionado diff --git a/patches/src/main/resources/addresources/values-es-rES/youtube/strings.xml b/patches/src/main/resources/addresources/values-es-rES/youtube/strings.xml index 3e8ed9b6db..b4681ff76a 100644 --- a/patches/src/main/resources/addresources/values-es-rES/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-es-rES/youtube/strings.xml @@ -29,6 +29,8 @@ Second \"item\" text" Otros Vídeo Restaurar antigua pantalla de configuración de YouTube + Mostrar mensajes en el snackbar de YouTube + Los mensajes de Morphe, como la confirmación de un botón del reproductor, se muestran en el snackbar de YouTube en lugar de una ventana emergente Eliminar restricciones de reproducción en segundo plano Desactivar reproducción en segundo plano diff --git a/patches/src/main/resources/addresources/values-et-rEE/music/strings.xml b/patches/src/main/resources/addresources/values-et-rEE/music/strings.xml index c352bf37a7..056575fb25 100644 --- a/patches/src/main/resources/addresources/values-et-rEE/music/strings.xml +++ b/patches/src/main/resources/addresources/values-et-rEE/music/strings.xml @@ -462,12 +462,10 @@ Et tagada scrobblingu sünkroniseerimine taustal esituse ajal, veenduge, et YouT Kuva Sünkroniseerimine Paneel - AI lõpppunkt Kõik pakkujad on välja lülitatud - sisseehitatud lüürik ja manuseeritud lüürik Lä resetting pakkujate järjekord? Taasta pakkujate vaikimisprioriteetsed järjekord Tõrge salvestatud sõnad - Kustuta selles seadmes salvestatud lüürikad Tõhjutatud %1$s MB Hangi token Apple Music Media-user-token @@ -527,14 +525,9 @@ Kohalduvad küpsised: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAP Kuva kopeerimise nupp Puudutage, et kopeerida originaallüürikad lihtsa tekstina; hoidke all, et salvestada lüürikate allikafail kaustesse \"Downloads\" Kuva tõlkimise nupp - Puudutage tõlke lülitamiseks; hoidke all, et näidata ainult tõlget. Kasutab pakkujate tõlkeid, Google Translate\'i või OpenAI-ga yhteilva API-t - Kuva romaniseerimise nupp - Puudutage romaniseerimise lülitamiseks; hoidke all, et näidata ainult romaniseerimist. Kasutab pakkujate romaniseerimist, Google Translate\'i või OpenAI-ga yhteilva API-t - Kohalik romaniseerimine ja sünkroniseerimine - Romaniseeri seadmes, kui teenusepakkuja, sinu mudel ja Google Translate ei õnnestu. Lugemine järgneb iga ajastatud lüürika sõna; sõnasünkroniseerimine peab olema sisse lülitatud. Hiina, kantoneese, jaapani, korea, kreeka, vene, gruusia, armenia; umbes 120 MB - Loetakse %1$s: %2$d%% Lugemised on valmis (%1$d kirjet) Allalaadimine ebaõnnestus - uuesti proovimiseks lülita see välja ja siis uuesti sisse + Loetakse %1$s: %2$d%% Kuva värskendamise nupp Puuduta, et owneda järgmisele lüürika kandidaadile; hoia all, et otsida lüürikat laulu pealkirja ja artisti järgi Otsi laureate @@ -605,7 +598,6 @@ Kuvatakse mõlema piirangu puhul. Väärtuse 1,75 juures loetakse neli täislaia Tõlgitud Tõlkimise keel - Romaniseerimise keel Tõlgitakse… Lüürikute tõlkimine ebaõnnestus Romaniseeritud @@ -619,7 +611,6 @@ Kuvatakse mõlema piirangu puhul. Väärtuse 1,75 juures loetakse neli täislaia Värskendamine Värskendatud QuickLRC API võti - QuickLRC transkribeerib või sünkroonib sõnu praeguse loo jaoks. See on eraldi teenus, mis ei ole seotud ülaltoodud OpenAI-ga ühilduva lõpppunktiga QuickLRC-i võti on salvestatud. Kasutage sõnumite otsingudialogis sünkroonimise nuppu või paneeli teavitust, kui lool puuduvad sõnumed QuickLRC API võti on salvestatud QuickLRC ei võtnud seda võtit vastu @@ -635,56 +626,27 @@ Kuvatakse mõlema piirangu puhul. Väärtuse 1,75 juures loetakse neli täislaia Sõnad sünkroonimiseks puuduvad - transkribeeritakse QuickLRC ei suutnud seda pause töödelda Selle pause jaoks on QuickLRC krediidid otsa saanud - "Laulusõnad tuvastatakse pealkirja ja esitaja järgi; ebatavaline metadata võib tulemuseks anda tühja vastuse või vale versiooni. Versioonide vaheksuleks kasutage värskendamist. Teenusteks, mille kõrval on märk *, on vaja tokent. Kõiki aktiveeritud teenuseid ei päristata alati teie järjekorras, vaid vastused saabuvad vastavalt nende kiirusele.\n\nLaulusõnad tulevad kolmandate osapoolte teenustest, mis ei ole seotud Morphe'iga, ei ole selle poolt sponsoritatud ega heaks kiidetud. Iga teenus kasutatakse sellisena, nagu see on: laulusõnad, nende kättesaadavus ja litsentsid jäävad vastavate teenuste ja nende litsentieerijate valdusse ning nende kasutamine võib olla allutatud nende kasutustingimustele. Aktiveeritud teenusele edastatakse ainult teie poolt soovitud päring – pealkiri, esitaja ja juhul, kui teenus seda vajab, video-ID. Valikuline AI-punkt ja QuickLRC transkriptsioon käivituvad alles pärast nende konfigureerimist ning saavad laulusõnade teksti või loo video-URL-i." Lülitage sisse ainult need tarnijad, mida kasutate, või hoidke vaikimisseadet - pidevad päringud võivad väikseid kogukonna lüürikasaite ülelaadida Peita autoriinfo Peita lüürika all kuvatavat autoriinfot Vaheta tõlke ja romaniseerimise koht - SÜÜDA paneb romaniseerimise lüürika alla ja tõlke üles; VÄLJA jätab romaniseerimise ülesse ja tõlke alla - Tõlge - Romaniseerimine - Tõlketeenuse pakkuja - Google (vaike, tasuta) - OpenAI-ga ühilduv (kohandatud) - Romaniseerimise pakkija - Google (vaike) - Kohalik / võrdluseta sõnastik - OpenAI ühilduv (kohandatud) - AI jaotus - Kasutatakse ainult siis, kui teenusepakkuja ei edasta jaotuse märke nagu Refrään või Salm, mille teie OpenAI-ga ühilduv mudel seejärel tuvastab - AI & pilve lõpppunktid - Konfigureerige AI pakkujate API võtmed, mudelid, lõpppunktid ja tõenäidiste šabloonid. - API lõpppunkti URL (nt https://api.openai.com/v1/chat/completions) Prompti mall Asendussümbolid: {language}, {task}, {title}, {artist}, {count}, {lines} Prompt on salvestatud Prompt on salvestatud, kuid AI lõpppunkt ei ole veel kasutatav. Konfigureerige see esmalt AI seadete alt Prompt on lähtestatud - Ei ole korrektne JSON. Parandage see või tühjendage väli enne salvestamist Tõlkimise & romaniseerimise seaded on salvestatud Seaded lähtestatud vaikeseadetele Põhi-URL peab algama http:// või https:// - Testi ühendust - Konfigureeri lõpppunktid - Kuva kõik lõpppunktid Ühendust kontrollitakse… - Tõlkusteenuse pakkuja - Romaniseerimise pakkuja - Hangi mudelid Mudelite hankimine… Vali mudel Mudelite hankimine ebaõnnestus. Kontrolli API võtmet või ühendust. - Gemini API seaded - OpenAI ühilduv (kohandatud lõpppunkt) - DeepL API seaded - API võti Mudel Põhi-URL API võti (valikuline) - Tõlkimise juhise Lisa päringsisu (valikuline JSON) Autentimise võti (Free või Pro) - Konfigureerige API võtmed, mudelid, lõpppunktid ja juhised 8.23.51 - Keela paksud ikoonid 9.13.51 - Taasta mängija vahekaadid \"Järgmine\", \"Laulud\" ja \"Seotud\" diff --git a/patches/src/main/resources/addresources/values-eu-rES/music/strings.xml b/patches/src/main/resources/addresources/values-eu-rES/music/strings.xml index 2b6c64a009..0977005eb1 100644 --- a/patches/src/main/resources/addresources/values-eu-rES/music/strings.xml +++ b/patches/src/main/resources/addresources/values-eu-rES/music/strings.xml @@ -462,12 +462,10 @@ Atzealdean erreproduktu duten bitartean scrobbling-a sinkronizatua dagoela ziurt Ikusiera Sinkronizazioa Panela - AI endpoint Bidalklari guztiak desaktibatuta daude - letra integratuak eta barneko letrak Hornitzaileen ordena berreskuratu? Hornitzaileen lehenetsuatze-ordena lehenagatik restoreatu Garbitu gorde diren letrak - Ezabatu gailu honetan gordetako letrak %1$s MB garbituta Tokena lortu Apple Music-en Media-user-tokena @@ -527,14 +525,9 @@ Cookie beharrezkoak: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPI Erakutsi Kopiaren botoia Sakatu letra originalak testu soil gisa kopiatzeko; sakatu luze letra iturriko fitxategia Deskargak folderrakoa gordetzeko Erakutsi Itzulpenaren botoia - Sakatu itzulpena aktibatzeko/desaktibatzeko; sakatu luze itzulpen bakarra ikusteko. Itzultzaileak, Google Translate edo OpenAI-rekin kompatiborena erabiltzen ditu - Erromanizatze botoia erakutsi - Sakatu romanizazioa aktibatzeko/desaktibatzeko; sakatu luze romanizazio bakarra ikusteko. Romanizazio iturriak, Google Translate edo OpenAI-rekin kompatiborena erabiltzen ditu - Romanizazio eta Azeratze Lokalizatuak - Romanizatu gailuaren barruan itzultzaileak, zure modeloa eta Google Translate fallatzen direnean. Irakurkizuna letra sinkronizatuaren hitz bakoitzaren ondoren dator; hitz sinkronizazioa aktibo egon behar da. Txinoa, Kantonesa, Japoniera, Koreera, Grekera, Errusiera, Georgiana, Armeniera; 120 MB inguru - %1$s irakurtzen: %2$d%% Irakurketa prest (%1$d entradak) Deskarga huts egin du - desaktibatu eta aktibatu berriro saiatzeko + %1$s irakurtzen: %2$d%% Aktualizatzeko botoia erakutsi Sakatu hurrengo letra kandidatora aldatzeko; sakatu luze abestuaren izenaren eta artistaren arabera letra bilatzeko Bilatu letraak @@ -605,7 +598,6 @@ Muga bikoitzera aplikatzen da. 1,75-an, lau zabalera osoak karakterek 7 kontatze Itzulitakoa Itzulpen hizkuntza - Romanizazio hizkuntza Itzulizuten... Ezin izan dira letrak itzuli Romanizatua @@ -619,7 +611,6 @@ Muga bikoitzera aplikatzen da. 1,75-an, lau zabalera osoak karakterek 7 kontatze Aktualizatzen Aktualizatua QuickLRC API giltza - QuickLRC-k gai current-eko letrak transkribitzen ditu edo denborizatzen ditu. Zerbitzu berezia da, goikohako OpenAI-rekin kompatible endpoint-arekin zerik ez duen QuickLRC gakoa gordetuta. Erabili letrak bilatzeko dialogue-ko lerrokatze botoia, edo panelko gomendioa gaiak letrik ez baditu QuickLRC API giltza gorde da QuickLRC-k ez du giltza hori onartu @@ -635,56 +626,27 @@ Muga bikoitzera aplikatzen da. 1,75-an, lau zabalera osoak karakterek 7 kontatze Ez dau lyricsik lerrokatzeko - transkribatzen ari da ordez QuickLRC-k ezin du pista hau prozesatu QuickLRC kredituak agotuta daude pista honentzat - "Letak izenburua eta artista arabera bat egiten dira; metadatu ezohikoek ezer edo bertsio 잘못a aurkitu ditzakete. Bertsioak aldatzeko, freskatu botoia erabili. * markatuta dauden hornitzaileek token bat behar dute. Gaitzitutako hornitzaile guztiak ez dira beti zure ordenean eskatzen, baina haien abiaduraren arabera erantzuten dute.\n\nLetak Morphe-rekin lotura, patroinatzaile edo onarpenik ez duten hirugarren party zerbitzuek ematen dituzte. Bakoitza modu berean erabiltzen da: letak, haien eskuragarritasuna eta baimenak zerbitzu hauen eta haien baimen-emaileen esestu geratzen dira, eta haien erabilerak haien zerbitzu-baldintzak aplikatu ditzake. Gaitzitutako zerbitzu batera iristen dena eskatu zuen bilaketa soilik da: izenburua, artista eta, zerbitzu horrek behar badu, bideo IDa. Aukerako AI endpoint-a eta QuickLRC transkripzioa konfituratu ondoren bakarrikra dabilen, eta letakren testua edo pistaren bideo URL-a jasotzen dute." Gaitu soilik erabiltzen dituzun hornitzaileak, edo utzi lehenagoa - eskaera etengabeek gogor egin diezakiak komunitateko letra-gune txikiak Ezagutze-lerra ezkutatu Ezkututu letraen azpian agertzen den kreditu-lerroa Truka itzulpena eta romanizazioa - ON-ak romanizazioa letraen azpian eta itzulpena gainean jartzen ditu; OFF-ak romanizazioa gainean eta itzulpena azpian mantentzen ditu - Itzulpena - Romanizazioa - Itzulpen hornitzailea - Google (Lehenagoa, Doakoa) - OpenAIrekin kompatibila (Kustomizatua) - Romanizazio hornitzailea - Google (Lehenagoa) - Hiztegi lokala / Offline - OpenAIekin kompatibila (Personalizatua) - AI Atala - Hornitzaileak Chorus edo Verse bezalako atal-etiketarik ez bidaltzen dituenean soilik erabiltzen da, eta zure OpenAI-rekin bateragarria den modeluak detektatzen ditu orduan - AI & Hodeiko endpointak - Konfiguratu API gakoak, modeloak, endpointak eta prompt txantiloiak AI hornitzaileentzat. - API endpoint URL-a (adib. https://api.openai.com/v1/chat/completions) Prompt-txantilokia Lehenkideak: {language}, {task}, {title}, {artist}, {count}, {lines} Prompta gorde da Prompta gorde da, baina AI endpoint-a ez da erabili nahi. Konfiguratu lehen AI E設定etan Prompta berreskuratuta - JSON baligabea. Konponduzu edo garbitu eroi hau gorde aurretik Itzulpen eta romanizazio konfigurazioak gordetuta Konfigurazioak lehenagokoetara itzulitakoak Oinarrizko URLak http:// edo https://-z eta hasi behar du - Konexioa probatu - Endpointak konfiguratu - Endpoint guztiak erakutsi Konexioa egiaztatzen... - Itzulpen Hornitzailea - Romanizazio Hornitzailea - Modelak ekidatu Modeluak eskatzen ari zahiz... Aukeratu Modela bat Modeluak eskatzen huts egin du. Egituta API gakoa edo konexioa. - Gemini API-ren konfigurazioa - OpenAI-rekin kompatibila (endpoint pertsonalizatua) - DeepL API-ren konfigurazioa - API-gako giltza Modelua Oinarrizko URLa API gakoa (aukerikora) - Itzulpen-prompta Eskaeraren gorputz gehigarria (JSON aukerikora) Autentifikazio-gakoa (Doakoa edo Pro) - Konfiguratu API-gakoak, modeluak, endpoint-ak eta prompt-ak 8.23.51 - Ezgaitu ikonok bold egitea 9.13.51 - Berakutsi Playeren Hurrengoak, Letrak eta Related fitxak diff --git a/patches/src/main/resources/addresources/values-fa-rIR/music/strings.xml b/patches/src/main/resources/addresources/values-fa-rIR/music/strings.xml index a5c29499a3..83463a11d1 100644 --- a/patches/src/main/resources/addresources/values-fa-rIR/music/strings.xml +++ b/patches/src/main/resources/addresources/values-fa-rIR/music/strings.xml @@ -462,12 +462,47 @@ Second \"item\" text" نمایش همگام‌سازی پنل - پایانه هوش مصنوعی تمام ارائه‌دهندگان غیرفعال شده‌اند - متن آهنگ داخلی و تعبیه‌شده بازنشانی ترتیب ارائه‌دهنده‌ها؟ بازیابی ارائه‌دهنده‌ها به ترتیب اولویت پیش‌فرض + لیست کلمات Rime + "لیست کلمات رایم همچنین به UniDict نیاز دارد: در ارائه‌دهنده واژه‌نامه آفلاین، تیک \"حروف چینی و کانتمنی (UniDict)\" را بزنید و اجازه دهید ابتدا دانلود کامل شود. بدون آن، لیستی که حروف را می‌خواند هیچ خوانشی تولید نمی‌کند. + +لیست‌هایی که رایم باید بخواند را انتخاب کنید، سپس برای دانلود آن‌ها روی ذخیره ضربه بزنید. لیستی که اضافه کرده‌اید با برداشتن تیک آن حذف می‌شود. + +هر لیست جدول واژه-به-خوانش شخصی شماست، یک فایل .dict.yaml با خطوطی مانند: +字 ham +典 din2 +ستون اول واژه است، ستون دوم خوانش آن؛ ستون‌های اضافی و خطوط بدون تب نادیده گرفته می‌شوند." + هیچ لیست کلمه Rime برای زبان بالا انتخاب نشده است. روی این نام ضربه بزنید تا یکی را انتخاب کنید یا زبانی که دارای لیست است را انتخاب کنید + افزودن + افزودن لیست کلمه Rime + یک فایل .dict.yaml یا یک زیپ شامل آن، با خطوطی مانند 字 ham و 典 din2. لیست هنگامی که آن را انتخاب کرده و روی ذخیره ضربه می‌زنید دریافت می‌شود و مجوز آن متعلق به شماست تا بررسی کنید + کد زبان + دو یا سه حرف، مانند hak یا wuu + نام + زیر خوانش نشان داده می‌شود، مانند Hakka (Meixian) + آدرس لیست کلمات + آدرس https:// فایل .dict.yaml یا zip آن + کاراکترهای خارج از واژه + خودکار + کاراکترهای کانتونی + کاراکترهای ماندارین + بدون خوانش کاراکتر + کد، نام و آدرس را پر کنید + کد باید دو یا سه حرف باشد، مانند hak + آدرس باید با https:// شروع شود + نام و آدرس نمی‌توانند شامل خط عمودی باشند + %1$s از قبل یک جدول واژه دارد + %1$s توسط رومنایزر داخلی خوانده می‌شود و نمی‌تواند لیست کلمات داشته باشد + استفاده نشده: این زبان از قبل خوانش خود را دارد، بنابراین برای حذف آن تیک آن را بردارید + %1$s اضافه شد - آن را انتخاب کرده و روی ذخیره ضربه بزنید تا دانلود شود + لیست‌های کلمات آماده هستند + لیست‌های کلمات آماده (%1$d مورد) + در حال ساخت لیست خوانش: %1$d%% + دانلود لیست کلمات ناموفق بود - برای تلاش مجدد روی ذخیره ضربه بزنید پاک کردن حافظه پنهان ترانه‌ها - حذف ترانه‌های ذخیره‌شده در این دستگاه + حذف %1$s مگابایت متن ترانه نگهداری شده بر روی این دستگاه %1$s مگابایت پاک شد دریافت توکن توکن Apple Music Media-user-token @@ -527,14 +562,22 @@ Second \"item\" text" نمایش دکمهٔ کپی برای کپی کردن ترانه اصلی به صورت متن ساده ضربه بزنید؛ برای ذخیره فایل منبع خام ترانه‌ها در پوشه Downloads، لمس طولانی انجام دهید نمایش دکمهٔ ترجمه - برای تغییر ترجمه ضربه بزنید؛ فقط برای ترجمه لمس طولانی انجام دهید. از ارائه‌دهنده ترجمه، Google Translate یا یک API سازگار با OpenAI استفاده می‌کند - نمایش دکمهٔ لاتین‌نویسی - برای تغییر رومنیزاسیون ضربه بزنید؛ فقط برای رومنیزاسیون لمس طولانی انجام دهید. از ارائه‌دهنده رومنیزاسیون، Google Translate یا یک API سازگار با OpenAI استفاده می‌کند - رومنیزاسیون محلی و همگام‌سازی - رومنیزه کردن روی دستگاه زمانی که ارائه‌دهنده، مدل شما و Google Translate شکست بخورد. خواندن هر کلمه از ترانه زمان‌بندی‌شده دنبال می‌کند؛ هماهنگ‌سازی کلمه باید روشن باشد. چینی، کانتهنی، ژاپنی، کره‌ای، یونانی، روسی، گرجی، ارمنی؛ حدود ۱۲۰ مگابایت - خوانش %1$s: %2$d%% + برای تغییر وضعیت ترجمه ضربه بزنید؛ برای فقط ترجمه فشار طولانی دهید. از ارائه‌دهندگان ترجمه فعال به ترتیب ذکر شده استفاده می‌کند + نمایش دکمه رومنیزه کردن یا انتقال + برای تغییر وضعیت رومنیزه کردن یا انتقال ضربه بزنید؛ برای فقط رومنیزه کردن یا انتقال فشار طولانی دهید. از ارائه‌دهندگان رومنیزه کردن فعال به ترتیب ذکر شده استفاده می‌کند + فعال کردن انتقال + اجازه دادن به خوانش‌های غیرلاتین از منبع ترانه؛ وقتی خاموش است، فقط حروف لاتین، حروف با علامت diacritic و ارقام تن صدا نگه داشته می‌شوند + هر واژه از یک ترانه زمان‌بندی شده را می‌خواند (همگام‌سازی واژه روشن است)؛ کره‌ای نیاز به دانلود ندارد؛ چهارده اسکریپت یک دانلود CLDR مشترک دارند؛ کاراکترهای Han توسط لیست کلمات Rime زمانی که جدول واژه آن زبان را پوشش می‌دهد خوانده می‌شود + دانلود دیکشنری‌های خوانش… آماده برای خواندن (%1$d مورد) دانلود ناموفق - برای تلاش مجدد، این گزینه را خاموش و روشن کنید + لغت‌نامه آفلاین + ژاپنی (SudachiDict، ~75 مگابایت) + کاراکترهای چینی و کانتونی (UniDict، ~8 مگابایت) + واژه‌های چینی (pinyin-pro، ~17 مگابایت) + روتین یونانی، سریلیک اسلاو (شامل روسی، اوکراینی، بلغاری، صربی، بلاروسی، مقدونی)، گرجی، ارمنی، عبری، عربی، آمهرایی و سورانی انتقال (CLDR، ~0.5 مگابایت) + خوانش %1$s: %2$d%% + حداقل یک دیکشنری انتخاب کنید نمایش دکمهٔ تازه‌سازی برای رفتن به نامزد بعدی ترانه ضربه بزنید؛ برای جستجوی ترانه بر اساس عنوان آهنگ و هنرمند، لمس طولانی انجام دهید جستجوی متن ترانه @@ -545,8 +588,10 @@ Second \"item\" text" ذخیره‌شده برجسته‌کردن واژه‌ها یا هجاهای همگام‌شده اولویت دادن به متن ترانهٔ واژه‌به‌واژه در صورت وجود. وقتی متن ترانه زمان‌بندی واژه دارد، هر واژه یا هجا را هماهنگ با موسیقی برجسته می‌کند - نمایش صداهای پس‌زمینه به صورت جداگانه - یک خط خواندن پس‌زمینه را که در پرانتز نوشته شده است، مانند «(آه) من تو را می‌خواهم»، از خط اصلی خوانده‌شده خارج کرده و به یک خط جداگانه با شدت کمتر منتقل می‌کند + کوچک کردن خطوط غیرفعال + هر خطی که در حال حاضر خوانده نمی‌شود را کمی کوچک‌تر ترسیم می‌کند، علاوه بر کم‌رنگ کردن آن + نمایش آوازهای پس‌زمینه به صورت جداگانه + آوازهای پس‌زمینه و هارمونی را از خط اصلی جدا می‌کند. یک آواز پس‌زمینه که داخل پرانتز نوشته شده است، مانند \"(Oh) I want you\" را از خط خوانده شده خارج کرده و به یک خط کم‌رنگ‌تر اختصاصی آن منتقل می‌کند پنهان‌کردن متن ترانه‌های پخش‌شده پنهان‌کردن خطوط متن ترانه بالای خطِ در حال پخش پنهان‌کردن متن ترانه‌های پخشنشده @@ -604,10 +649,15 @@ Second \"item\" text" ترجمه توسط %s لاتین‌نویسی توسط %s روان‌نویسی شده توسط %s + رومنیزه شده توسط Rime (%s) ترجمه شد زبان ترجمه - زبان روان‌نویسی + زبان رومنیزه کردن یا انتقال + "زبان هدف ترجمه: +%s" + "زبان هدف رومنیزاسیون یا انتقال خطی: +%s" در حال ترجمه… ترجمهٔ متن آهنگ ممکن نشد لاتین‌نویسی‌شده @@ -621,7 +671,7 @@ Second \"item\" text" در حال تازه‌سازی تازه‌سازی شد کلید API QuickLRC - QuickLRC اشکال‌زدایی یا زمان‌بندی شعرهای ترک فعلی را انجام می‌دهد. یک سرویس مستقل و مرتبط نیست با نقطه پایانی سازگار با OpenAI در بالا + QuickLRC ترانسکریپت یا زمان‌همگام‌سازی ترانه‌ها برای اثر فعلی را انجام می‌دهد کلید QuickLRC ذخیره شد. از دکمه align در دیالوگ جستجوی شعر، یا پیام پنل استفاده کنید وقتی هیچ شعری برای ترک موجود نیست کلید API QuickLRC ذخیره شد QuickLRC آن کلید را قبول نکرد @@ -637,58 +687,50 @@ Second \"item\" text" بدون شعی برای تراز - در حال بازخوانی QuickLRC نمی‌تواند این قطعه را پردازش کند اعتبار QuickLRC برای این قطعه تمام شده است - "آهنگ‌ها بر اساس عنوان و خواننده تطبیق داده می‌شوند؛ اطلاعات غیرمعمول ممکن است هیچ چیزی یا نسخه اشتباه را پیدا کند. برای تغییر نسخه از تازه‌سازی استفاده کنید. ارائه‌دهندگانی که با * مشخص شده‌اند نیاز به توکن دارند. همه ارائه‌دهندگان فعال همیشه به ترتیب درخواست نمی‌شوند، بلکه بر اساس سرعتشان پاسخ می‌دهند. + "ترانه‌ها با عنوان و خواننده تطبیق داده می‌شوند؛ metadata غیرمعمول ممکن است نتیجه‌ای پیدا نکند یا نسخه اشتباه را برگرداند. برای تغییر نسخه از گزینه \"رفرش\" استفاده کنید. ارائه‌دهندگانی که با * علامت‌گذاری شده‌اند به توکن نیاز دارند. همیشه از همه ارائه‌دهندگان فعال به ترتیبی که مشخص کرده‌اید درخواست نمی‌شود، بلکه آن‌ها بر اساس سرعت پاسخ می‌دهند. -آهنگ‌ها از سرویس‌های شخص ثالثی تأمین می‌شوند که هیچ ارتباطی با Morphe ندارند، آن‌ها را حمایت یا تأیید نمی‌کنند. هر سرویس همان‌طور که هست استفاده می‌شود: متن آهنگ، دسترسی‌پذیری و مجوزهای آن نزد همان سرویس‌ها و دارندگان مجوز باقی می‌ماند و استفاده از آن‌ها ممکن است مشروط به شرایط خدمات آن‌ها باشد. تنها چیزی که به یک سرویس فعال ارسال می‌شود، جستجویی است که شما درخواست کرده‌اید - یعنی عنوان، خواننده و در صورت نیاز آن سرویس، شناسه ویدیو. نقطه پایانی AI اختیاری و رونوشت QuickLRC تنها پس از پیکربندی توسط شما اجرا می‌شوند و متن آهنگ یا URL ویدیوی ترک را دریافت می‌کنند." +ترانه‌ها از سرویس‌های شخص ثالث دریافت می‌شوند که هیچ ارتباط، حامی بودن یا تأییدی با Morphe ندارند. هر سرویس به همان شکلی که هست استفاده می‌شود: ترانه‌ها، دسترس‌پذیری و مجوز آن‌ها متعلق به آن سرویس‌ها و دارندگان مجوزشان است و استفاده از آن‌ها ممکن است مشروط به شرایط خدمات آن‌ها باشد. آنچه به یک سرویس فعال فرستاده می‌شود فقط جستجوی درخواستی شماست - عنوان، خواننده و در مواردی که سرویس نیاز دارد، شناسه ویدیو. پایانه AI اختیاری و تبدیل سریع LRC تنها پس از پیکربندی آن‌ها اجرا می‌شوند و متن ترانه یا URL ویدیوی ترک را دریافت می‌کنند." فقط ارائه‌دهندگانی که استفاده می‌کنید را فعال کنید یا پیش‌فرض را حفظ کنید - درخواست‌های مداوم می‌تواند سایت‌های متن آهنگ جامعه کوچک را دچار اشکال کند پنهان‌کردن خط اعتبار پنهان‌کردن خط اعتبار نمایش‌داده‌شده در پایین متن ترانه جابجایی ترجمه و لاتین‌نویسی - روشن: لاتین‌نویسی زیر متن ترانه و ترجمه بالای آن قرار می‌گیرد؛ خاموش: لاتین‌نویسی بالا و ترجمه پایین می‌ماند - ترجمه - رومی‌نویسی - ارائه‌دهندهٔ ترجمه - گوگل (پیش‌فرض، رایگان) - سازگار با OpenAI (سفارشی) - ارائه‌دهنده رومی‌سازی - گوگل (پیش‌فرض) - دیکشنری محلی / آفلاین - سازگار با OpenAI (سفارشی) - بخش هوش مصنوعی - تنها زمانی استفاده می‌شود که ارائه‌دهنده هیچ برچسب بخشی مانند Chorus یا Verse را ارسال نکند و مدل سازگار با OpenAI شما آن‌ها را شناس کند - نقاط پایانی AI & ابری - پیکربندی کلیدهای API، مدل‌ها، نقاط پایانی و الگوهای پرامپت برای ارائه‌دهندگان AI. - آدرس URL نقطه پایانی API (به عنوان مثال https://api.openai.com/v1/chat/completions) + روشن بودن رومنیزه کردن یا انتقال را زیر ترانه و ترجمه را بالای آن قرار می‌دهد; خاموش بودن رومنیزه کردن یا انتقال را بالای ترانه و ترجمه را پایین نگه می‌دارد. یک خوانش که روی هر سیلاب قرار می‌گیرد همراه با آن حرکت می‌کند، از بالای سیلاب به زیر آن + همیشه مرکز ترانه‌ها + روشن بودن هر خط را در وسط قرار داده و هم‌ترازی منبع را نادیده می‌گیرد، شامل طرف‌های دیوئلت; خاموش بودن هم‌ترازی درخواست شده توسط ترانه را نگه می‌دارد + حداقل یک ارائه‌دهنده باید روشن بماند + ترجمه + رومنیزه کردن یا انتقال + لغت‌نامه آفلاین + لیست کلمات Rime + تشخیص بخش‌ها با AI + فقط زمانی استفاده می‌شود که ارائه‌دهنده هیچ برچسب بخشی مانند Chorus یا Verse ندارد که مدل سازگار با OpenAI شما سپس تشخیص دهد. ویرایش ردیف همچنین پرامپت آن را باز می‌کند + URL نقطه پایانی API (مثال https://api.openai.com/v1/chat/completions, https://api.anthropic.com/v1/chat/completions, https://generativelanguage.googleapis.com/v1beta/openai/chat/completions) الگوی پرامپت جای‌گذارهای: {language}, {task}, {title}, {artist}, {count}, {lines} پرامپت ذخیره شد پرامپت ذخیره شد، اما نقطه پایانی AI هنوز قابل استفاده نیست. ابتدا آن را در تنظیمات AI پیکربندی کنید پرامپت بازنشانی شد - JSON معتبر نیست. آن را اصلاح کنید یا قبل از ذخیره فیلد را پاک کنید تنظیمات ترجمه & رومانیزه کردن ذخیره شد تنظیمات به پیش‌فرض بازنشانی شد Base URL باید با http:// یا https:// شروع شود - تست اتصال - پیکربندی نقاط پایانی - نمایش همه نقاط پایانی + تنظیمات ناقص است یا توسط ارائه‌دهنده رد شده است در حال بررسی اعتبار اتصال… - ارائه‌دهندهٔ ترجمه - ارائه‌دهنده رومی‌سازی - دریافت مدل‌ها + %1$s: متصل! (%2$s) + %1$s: اتصال ناموفق بود. تنظیمات را بررسی کنید. + ابتدا کلید API را وارد کنید در حال دریافت مدل‌ها… یک مدل را انتخاب کنید دریافت مدل‌ها ناموفق بود. کلید API یا اتصال را بررسی کنید. - تنظیمات Gemini API - سازگار با OpenAI (نقطه اتصال سفارشی) - تنظیمات DeepL API - کلید API + پرامپت مدل Base URL + یک سرویس انتخاب کنید کلید API (اختیاری) - پرامپت ترجمه بدنه اضافی درخواست (JSON اختیاری) + سربرگ‌های درخواست اضافی (JSON اختیاری) + نام‌ها و مقادیر را نقل‌قول کنید، مثلاً {\"api-key\": \"abc\"}. یک مقدار نقل‌قول نشده حاوی / : یا یک عدد رد می‌شود. یک مقدار خالی یک سربرگ را حذف می‌کند. + نام‌ها و مقادیر را نقل‌قول کنید، مثلاً {\"temperature\": 0.3}. یک مقدار نقل‌قول نشده حاوی / یا : رد می‌شود. مدل و پیام‌ها نمی‌توانند اینجا تنظیم شوند. کلید احراز هویت (رایگان یا حرفه‌ای) - پیکربندی کلیدهای API، مدل‌ها، نقاط انتهایی و پرامپت‌ها 8.23.51 - غیرفعال‌کردن آیکون‌های پررنگ 9.13.51 - بازگرداندن برگه‌های بعدی پخش‌کننده، متن آهنگ و مرتبط diff --git a/patches/src/main/resources/addresources/values-fa-rIR/youtube/strings.xml b/patches/src/main/resources/addresources/values-fa-rIR/youtube/strings.xml index 30e60f5c33..592a9d7001 100644 --- a/patches/src/main/resources/addresources/values-fa-rIR/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-fa-rIR/youtube/strings.xml @@ -29,6 +29,8 @@ Second \"item\" text" متفرقه ویدئو بازگرداندن صفحهٔ تنظیمات قدیمی YouTube + نمایش پیام‌ها در Snackbar یوتیوب + پیام‌های Morphe، مانند تأیید دکمه پخش‌کننده، به جای پنجره بازشو در Snackbar یوتیوب نمایش داده می‌شوند حذف محدودیت‌های پخش در پس‌زمینه غیرفعال‌کردن پخش در پس‌زمینه diff --git a/patches/src/main/resources/addresources/values-fi-rFI/music/strings.xml b/patches/src/main/resources/addresources/values-fi-rFI/music/strings.xml index aae94db1e2..03e6de76b3 100644 --- a/patches/src/main/resources/addresources/values-fi-rFI/music/strings.xml +++ b/patches/src/main/resources/addresources/values-fi-rFI/music/strings.xml @@ -462,12 +462,47 @@ Varmistaaksesi, että scrobbling synkronoidaan taustatoiston aikana, varmista, e Näyttö Synkronointi. Paneeli - AI-päätepiste Kaikki tarjoajat poistettu käytöstä - sisäänrakennetut sanoitukset ja upotetut sanoitukset Palautetaanko palveluntarjoajien järjestys? Palauta palveluntarjoajat oletusarvoiseen prioriteettijärjestykseen. + Rime-sananluettelo + "Rime-sanaluettelo tarvitsee myös UniDictin: valitse Offline-sanakirjapalvelusta \"Kiinan ja kantonesin merkit (UniDict)\" ja odota latauksen valmistumista. Ilman sitä lista, joka näyttää merkkejä, ei tuota ääntämistä. + +Valitse ne Rime-sanaluetelot, joita haluat lukea, ja napauta Tallenna ladataksesi ne. Lista, jonka lisäsit, poistetaan kun sen ruksi poistetaan. + +Jokainen lista on oma sana-ääntämistaulukkosi, .dict.yaml-tiedosto kuten: +字 ham +典 din2 +Ensimmäinen sarake on sana, toinen sen ääntäminen; ylimääräiset sarakkeet ja rivit ilman välilyöntiä jätetään huomiotta." + Yllä olevaa kieltä varten ei ole valittu yhtään Rime-sanaluetteloa. Napauta tätä nimeä valitaksesi luettelon tai valitse kieli, jolla on luettelo + Lisää + Lisää Rime-sanaluettelo + .dict.yaml-tiedosto tai zip, jossa se on, kuten 字 ham ja 典 din2. Lista haetaan kun sen ruksi asetetaan ja napautetaan Tallentaa, ja sen lisenssi on tarkastettavissa sinulla + Kielikoodi + Kaksi tai kolme kirjainta, kuten hak tai wuu + Nimi + Näytetään lukeman alla, kuten Hakka (Meixian) + Sanaluettelon osoite + .dict.yaml-tiedoston tai sen zip-paketin https://-osoite + Merkit, jotka eivät ole sanassa + Automaattinen + Kantonilaiset merkit + Mandariinimerkit + Ei merkkin lukemaa + Täytä koodi, nimi ja osoite + Koodin tulee olla kaksi tai kolme kirjainta, kuten hak + Osoitteen tulee alkaa https:// + Nimessä ja osoitteessa ei saa olla pystyviivaa + %1$s: Sanataulukko on jo olemassa + %1$s: Sisäinen romanisaatio käyttää tätä eikä se voi vastaanottaa sanaluetteloa + Ei käytössä: tällä kielellä on jo oma lukemansa, joten voit poistaa sen valinnasta + Lisätty %1$s – valitse se ja napauta Tallenna datan lataamiseksi + Sanaluettelot ovat valmiita + Sanaluettelot valmiit (%1$d kpl) + Lukemien luominen: %1$d%% + Sanaluettelon lataus epäonnistui – yritä uudelleen napauttamalla Tallenna Tyhjennä tallennetut sanat - Poista tässä laitteessa säilytetyt sanat + Poista laitteella säilytetyt %1$s Mt sanoituksia välimuistista Vapautettu %1$s Mt Hae tunnus. Apple Music -median käyttäjätunnus @@ -522,14 +557,22 @@ Vaaditut evästeet: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPIS Näytä kopioi -painike Napauta kopioidaksesi alkuperäiset sanat tekstimuodossa; Paina pitkää tallentaaksesi raaka-sanat-lähdetiedoston Lataukset-kansioon Näytä käännä -painike - Napauta vaihtaaksesi käännöstä; Paina pitkänä käännöstä vain. Käyttää tarjoajan käännöksiä, Google Translateä tai OpenAI-yhteensopivaa rajapintaa - Näytä romanisointipainike. - Napauta vaihtaaksesi transkriptiota; Paina pitkänä transkriptiota vain. Käyttää tarjoajan transkriptiota, Google Translateä tai OpenAI-yhteensopivaa rajapintaa - Paikallinen transkriptio ja kohdistus - Transkriboi laitteella, kun tarjoaja, mallisi ja Google Translate epäonnistuvat. Lukeminen seuraa ajastetun sanan sanaa; sanasynkin on oltava käytössä. Kiina, kantoninkiina, japani, korea, kreikka, venäjä, georgia, armenia; noin 120 Mt - Äänteet %1$s: %2$d%% + Napauta käynnistääksesi tai lopettaaksesi käännöksen; paina pitkään käynnistääksesi vain käännöksen. Käyttää aktivoidut käännösprovidereit niiden listatussa järjestyksessä + Näytä Romanisoi tai Transkriboi -painike + Napauta käynnistääksesi tai lopettaaksesi romanisoinnin/transkription; paina pitkään käynnistääksesi vain romanisoinnin/transkription. Käyttää aktivoidut romanisointi-providereit niiden listatussa järjestyksessä + Salli transkriptio + Salli latinankirjaimisten ulkopuoliset lukemat sanoituslähteestä; jos pois päältä, pidä vain latinan aakkoset, aksentit äänneasut ja tonivärit + Lukee ajastetun sanoituksen jokaisen sanan (sanarytmitys käytössä); Koreaa ei tarvitse ladata; neljäntoista kirjoitusjärjestelmän yhteinen CLDR-lataus; Han-merkit lukee Rime-sanaluettelo, kun sen sanataulukko kattaa kielen + Ladataan lukemisanastoja… Lukemiset valmiit (%1$d merkkiä) Lataus epäonnistui - kytke tämä pois päältä ja kytkä uudelleen yrittääksesi uudelleen + Offline-sanasto + Japani (SudachiDict, n. 75 Mt) + Kiina ja kantoni (UniDict, n. 8 Mt) + Kiinan sanat (pinyin-pro, n. 17 Mt) + Kreikka, slaavilaiset kyrilliset (myös venäjä, ukrainaksi, bulgariksi, serbiaksi, belorussiksi, makedoniksi), georgia, armenia, heprea, arabia, amhara ja syrian transkriptio (CLDR, n. 0,5 Mt) + Äänteet %1$s: %2$d%% + Valitse vähintään yksi sanasto Napauta vaihtaaksesi seuraavaan sanoitusehdotukseen; pitkä painallus päivittää. Napauta vaihtaaksesi seuraavaan sanojen ehdokkaaseen; Paina pitkänä hakeaksesi sanoja kappaleen nimen ja artistin mukaan Hae sanoituksia @@ -540,8 +583,10 @@ Vaaditut evästeet: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPIS Tallennettu Korosta synkronoidut sanat tai tavut Priorisoi sana sanalta -lyriikat, kun ne ovat saatavilla. Kun lyriikoissa on sanan ajoitus, korostaa jokaisen sanan tai tavun musiikin tahdissa - Näytä taustalaulu erikseen - Siirtää sulkuihin kirjoitetun taustalaulun, kuten \"(Oh) I want you\", laulun sävellysviivan ulkopuolelle omaksi himmeämmäksi rivikseen + Supista eiaktiiviset rivit + Piirtää kaikki juuri laulamattomat rivit hieman pienempinä hämärättä niitä samalla + Näytä taustalaulut erikseen + Erottaa taustalaulut ja soinnut päälinjasta. Siirtää sulkeisiin merkityn taustalaulun, kuten \"(Oh) I want you\", päälaulun ulkopuolelle omalle himmeämmälle rivilleen Piilota soitetut lyriikat Piilota lyriikkarivit nykyisen soittorivin yläpuolelta Piilota soittamattomat lyriikat @@ -599,10 +644,15 @@ Koskee molempia rajoja. Arvolla 1,75 neljä täyslevyistä merkkiä lasketaan se Kääntänyt %s Latinisoinut %s Latinisoitu tekijän %s toimesta + Romanisoitu Rimen toimesta (%s) Käännetty Käännöskieli - Latinisointikieli + Romanisoinnin tai transkription kieli + "Käännöksen kohdekieli: +%s" + "Romanoinnin tai transkription kohdekieli: +%s" Käännetään… Sanoituksia ei voitu kääntää Romanisoitu @@ -616,7 +666,7 @@ Koskee molempia rajoja. Arvolla 1,75 neljä täyslevyistä merkkiä lasketaan se Päivitetään Päivitetty QuickLRC API-avain - QuickLRC transkriboi tai ajastaa sanakirjan nykyistä kappaletta varten. Erillinen palvelu, joka ei liity edellä mainittuun OpenAI-yhteensopivaan päätepisteeseen + QuickLRC siirtää tai ajoittaa nykyisen raidan sanoitukset QuickLRC-avain tallennettu. Käytä kohdistuspainikkea kappaleen etsintävalintaikkunassa tai paneelin kehotteessa, kun kappaleessa ei ole sanakirjaa QuickLRC API-avain tallennettu QuickLRC ei hyväksynyt kyseistä avainta @@ -632,58 +682,50 @@ Koskee molempia rajoja. Arvolla 1,75 neljä täyslevyistä merkkiä lasketaan se Ei sanakirjaa kohdistettavaksi - transkriptoidaan sen sijaan QuickLRC ei pystynyt käsittelemään tätä kappaletta QuickLRC-luodot loppuivat tältä kappaleelta - "Sanojen tunnistus perustuu kappaleen nimeen ja artistiin. Epätavallinen metatieto ei välttämättä löydä oikeaa versiota. Käytä päivitä-painiketta vaihtaaksesi versiota. Tähdellä (*) merkityt tarjoajat vaativat tokenin. Kaikki käytössä olevat tarjoajat eivät aina ole samassa järjestyksessä kuin pyyntöön, vaan ne vastaavat nopeutensa mukaan. + "Laulutekstit matchataan nimellä ja artistilla, epänormaali metadata saattaa löytää mitään tai väärän version. Käytä päivitettyä näppäintä vaihtaaksesi versioita. Tähti (*)-merkillä varustetut palveluntarjoajat vaativat tokenin. Kaikki aktiiviset palveluntarjoajat eivät aina pyydä järjestyksessäsi, vaan vastaavat nopeutensa mukaan. -Sanat tulevat kolmannen osapuolen palveluista, jotka eivät liity, sponsoroi tai hyväksy Morphe-sovellusta. Jokainen palvelu käytetään sellaisenaan: sanat, niiden saatavuus ja lisenssi pysyvät palveluiden ja lisenssienantajien hallussa, ja niiden käyttö voi olla sidottu niiden käyttöehtoihin. Käytettävissä olevaan palveluun lähetetään vain etsintäpyyntösi - kappaleen nimi, artisti ja tarvittaessa videon ID. Valinnainen tekoälypäätepiste ja QuickLRC-transkriptio käynnistyvät vain, kun olet asettanut ne, ja ne saavat sanatekstin tai kappaleen video-URL:n." +Laulutekstit tulevat kolmannen osapuolen palveluista, jotka eivät ole Morphen alaisia, sponsoroimia eivätkä hyväksymiä. Jokaisessa käytetään sellaisenaan: lauluteksti, niiden saatavuus ja lisenssi pysyvät näiden palveluiden ja heidän lisenssinhaltijoiden hallussa, ja niiden käyttö voi olla sidoksissa heidän palveluehdoihinsa. Mikä tahansa mahdollistaa palvelun vastaanottaa vain kysymyksesi - nimi, artisti, ja jos palvelu tarvitsee sen, videon tunniste. Vapaaehtoinen tekoälypääte ja QuickLRC-transkriptio toimivat vain kun olet määrittänyt ne, ja saavat laulun tekstiä tai kappaleen videon URL-osoitetta." Ota käyttöön vain sinä käyttämäsi tarjoajat tai jätä oletusarvo - jatkuva pyyntö voi kuormittaa pienet yhteisöiden sanalause-sivustot Piilota tekijätietorivi Piilota lyriikoiden alla näkyvä tekijätietorivi Vaihda käännöksen ja romanisoinnin paikkaa - PÄÄLLÄ asettaa romanisoinnin lyriikoiden alle ja käännöksen yläpuolelle; POIS pitää romanisoinnin yläpuolella ja käännöksen alapuolella - Käännös - Romanisointi - Käännöspalvelu - Google (oletus, ilmainen) - OpenAI-yhteensopiva (mukautettu) - Romanisointipalvelu - Google (oletus) - Paikallinen / offline-sanakirja - OpenAI-yhteensopiva (mukautettu) - Tekoälyosio - Käytetään vain, kun palveluntarjoaja ei toimita osioiden tunnisteita, kuten Kertosäe tai Säkeistö, jolloin OpenAI-yhteensopiva mallisi tunnistaa ne - AI & Cloud Endpoints - Määritä API-avaimet, mallit, päätepisteet ja kehotepohjat tekoälypalveluille. - API-päätepisteen URL-osoite (esim. https://api.openai.com/v1/chat/completions) + ON asettaa romanisoinnin tai transkriptiot sanoitusten alle ja käännökset ylöspäin; OFF pitää romanisoinnin tai transkriptiot yläpuolella ja käännökset alapuolella. Kullekin tavulle asetettu lukema liikkuu sen mukana sen ylä- ja alapuolelle + Aseta sanoitukset aina keskelle + ON asettaa jokaisen rivin keskelle ja sivuuttaa alkuperäisen tasauksen, mukaan lukien dueton puolet; OFF pitää sanoituksen pyytämän tasauksen + Vähintään yhden providenin tulee olla aktiivisena + Käännös + Romanisointi tai transkriptio + Offline-sanasto + Rime-sanaluettelo + Tunnista osiot tekoälyn avulla + Käytetään vain silloin, kun provider ei sisällä osiotarroja, kuten Refraani tai Särje, jotka sitten avoimet AI-yhteensopivat mallit tunnistavat. Rivin muokkaaminen avaa myös sen promptin + API-päätepisteen URL (esim. https://api.openai.com/v1/chat/completions, https://api.anthropic.com/v1/chat/completions, https://generativelanguage.googleapis.com/v1beta/openai/chat/completions) Kehotepohja Paikkamerkit: {language}, {task}, {title}, {artist}, {count}, {lines} Kehote tallennettu Kehoitus tallennettu, mutta tekoälypäätepiste ei ole vielä käytettävissä. Määritä se ensin Teekohtaisista asetuksista Kehote nollattu - Ei kelvollista JSON-muotoa. Korjaa se tai tyhjennä kenttä ennen tallentamista Translation & Romanization settings saved Asetukset nollattiin Perus-URL-osoitteen on alettava http:// tai https:// - Testaa yhteys - Määritä päätepisteet - Näytä kaikki päätepisteet + Asetukset ovat puutteellisia tai provider hylkäsi ne Tarkistetaan yhteyttä… - Käännöspalvelun tarjoaja - Romanisointipalvelun tarjoaja - Hae mallit + %1$s: Yhdistetty! (%2$s) + %1$s: Yhteys epäonnistui. Tarkista asetukset. + Syötä API-avaime ensiksi Haetaan malleja… Valitse malli Mallien haku epäonnistui. Tarkista API-avain tai yhteys. - Gemini API -asetukset - OpenAI-yhteensopiva (oma päätepiste) - DeepL API -asetukset - API-avain + Prompt Malli Perus-URL + Valitse palvelu API-avain (valinnainen) - Käännöskehote Ylimääräinen pyynnön runko (valinnainen JSON) + Lisäpäällystykset (vapaaehtoinen JSON) + Laita nimet ja arvot lainausmerkkeihin, esim. {\"api-key\": \"abc\"}. Lainoittamatta jätetty arvo sisältää / : tai numeron hylätään. Tyhjä arvo poistaa päällystyksen. + Laika nimet ja arvot lainausmerkkeihin, esim. {\"temperature\": 0.3}. Lainoittamatta jätetty arvo sisältää / : hylätään. Mallia ja viestejä ei voi asettaa tässä. Autentikointiavain (Free tai Pro) - Määritä API-avaimet, mallit, päätepisteet ja kehotteet 8.23.51 - Poista lihavoidut kuvakkeet 9.13.51 - Palauta Playerin Seuraavaksi-, Sanoitukset- ja Liittyvät-välilehdet diff --git a/patches/src/main/resources/addresources/values-fi-rFI/youtube/strings.xml b/patches/src/main/resources/addresources/values-fi-rFI/youtube/strings.xml index 0a8cc2b501..086055aaa8 100644 --- a/patches/src/main/resources/addresources/values-fi-rFI/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-fi-rFI/youtube/strings.xml @@ -29,6 +29,8 @@ Second \"item\" text" Sekalaiset Video Palauta vanhat YouTube-asetusvalikot + Näytä YouTube-snackbarissa olevat viestit + Morphe-viestit, kuten pelaajan painikkeen vahvistus, näytetään YouTube-snackbarissa ponnahdusikkunan sijaan Poista taustatoiston rajoitukset Poista Shortsien taustatoisto käytöstä diff --git a/patches/src/main/resources/addresources/values-fil-rPH/music/strings.xml b/patches/src/main/resources/addresources/values-fil-rPH/music/strings.xml index 1b36685887..95abdef4d6 100644 --- a/patches/src/main/resources/addresources/values-fil-rPH/music/strings.xml +++ b/patches/src/main/resources/addresources/values-fil-rPH/music/strings.xml @@ -462,12 +462,47 @@ Para matiyak na naka-sync ang scrobbling habang nagpe-play sa background, sigura Ipakita Synchronisasyon Panel - Endpoint ng AI Lahat ng provider ay naka-disable - built-in lyrics at embedded lyrics I-reset ang pagkakasunud-sunod ng provider? Ibalik ang mga provider sa default na pagkakasunud-sunod ng priyoridad. + Talaan ng mga salita sa Rime + "Ang Rime Wordlist ay nangangailangan din ng UniDict: sa Offline Dictionary provider, piliin ang \"Chinese and Cantonese characters (UniDict)\" at hayaan itong matapos mag-download muna. Kung wala ito, isang listahan na nagbabasa ng mga karakter ay hindi magbibigay ng pagbasa. + +Piliin ang mga listahan na basahin ng Rime Wordlist, pagkatapos ay i-tap ang Save upang ma-download ang mga ito. Ang listahang idinagdag mo ay tinatanggal kapag ini-uncheck mo ito. + +Bawat listahan ay iyong sariling table ng salita-pagbasa, isang .dict.yaml na may mga linya tulad ng: +字 ham +典 din2 +Ang unang kolunma ay ang salita, ang pangalawa ay ang pagbasa nito; karagdagang mga kolunma at mga lining walang tab ay kinikilala." + Walang listang word sa Rime ang naka-tick para sa wika sa itaas. I-tap ang pangalang ito upang i-tick ang isa, o pumili ng wika na may listang + Idagdag + Magdagdag ng listang word sa Rime + Isang .dict.yaml file, o isang zip na naglalaman nito, na may mga linya tulad ng 字 ham at 典 din2. Ang listahang hinahabol kapag pinili mo ito at i-tap ang Save, at ang lisensya nito ay para sa iyo upang suriin + Code ng wika + Dalawa o tatlong letra, gaya ng hak o wuu + Pangalan + Ipapakita sa ilalim ng pagbasa, gaya ng Hakka (Meixian) + Address ng listang word + https:// address ng .dict.yaml o ng zip nito + Mga character na hindi nasa word + Awtomatiko + Mga character na Cantonese + Mga character na Mandarin + Walang pagbasa ng character + Punan ang code, pangalan at address + Dapat dalawa o tatlong letters ang code, gaya ng hak + Dapat simulan sa https:// ang address + Hindi pwedeng maglaman ng vertical bar ang pangalan at address + Mayroon nang table ng word ang %1$s + Binabasa ang %1$s ng built-in romanizer at hindi pwedeng kunin ang listang word + Hindi ginagamit: may sariling pagbasa na ang wikang ito, kaya tanggalin mo ito sa pamamagitan ng hindi pagtandaan + Idinagdag ang %1$s - tandaan mo ito at i-tap ang Save para i-download + Handa na ang mga listang word + Handa na ang mga listang word (%1$d entries) + Pagbabasa ng listang word: %1$d%% + Nabigo ang download ng listang word - i-tap muli ang Save para subukan ulit Linisin ang naka-store na lyrics - Tanggalin ang mga lyrics na nakapanatili sa device na ito + Burahin ang %1$s MB ng mga lyrics na naka-save sa device na ito Na-linisan na %1$s MB Kumuha ng token Token ng Media-user ng Apple Music @@ -522,14 +557,22 @@ Mga kinakailangang cookie: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure Ipakita ang button na Kopyahin Pindutin para kopyahin ang orihinal na lyrics bilang plain text; long-press para ilabas ang raw lyrics source file sa Downloads Ipakita ang button na Isalin - Pindutin para toggle ang translation; long-press para translation only. Gumagamit ng provider translations, Google Translate, o isang OpenAI-compatible API - Ipakita ang Romanize button - Pindutin para toggle ang romanization; long-press para romanization only. Gumagamit ng provider romanization, Google Translate, o isang OpenAI-compatible API - Local Romanization at Alignment - I-romanize sa device kapag ang provider, iyong modelo at Google Translate ay nabigo. Ang pagbabasa ay sumusunod sa bawat salita ng timed lyric; word sync must be on. Chinese, Cantonese, Japanese, Korean, Greek, Russian, Georgian, Armenian; about 120 MB - Pagbabasa %1$s: %2$d%% + I-tap para ibalik/itago ang translation; long-press para translation lang. Ginagamit ang enabled translation providers ayon sa nakasulat na order + Ipakita ang Romanize o Transliterate button + I-tap para ibalik/itago ang romanization o transliteration; long-press para romanization o transliteration lang. Ginagamit ang enabled romanization providers ayon sa nakasulat na order + Paganahin ang transliteration + Hayaan ang non-Latin readings mula sa lyrics source; kapag off, panatilihing Latin letters, accented letters at tone digits lang + Binabasa ang bawat salita ng timed lyric (word sync on); Walang kailangang download para sa Korean; sampung scripts ang naghihigugupit ng CLDR download; Ang Han characters ay binabasa ng Rime Wordlist kapag ang word table nito ay sakop ang wika + Nagde-download ng mga dictionary para sa pagbasa… Handa na ang mga basihan (%1$d na pagsunod) Nabigo ang pag-download - patayin at buuin ito muli upang subukan muli + Offline Dictionary + Japanese (SudachiDict, ~75 MB) + Mga character na Chinese at Cantonese (UniDict, ~8 MB) + Mga salitang Chinese (pinyin-pro, ~17 MB) + Transliteration para sa Greek, Slavic Cyrillic (kasama ang Russian, Ukrainian, Bulgarian, Serbian, Belarusian, Macedonian), Georgian, Armenian, Hebrew, Arabic, Amharic at Syriac (CLDR, ~0.5 MB) + Pagbabasa %1$s: %2$d%% + Pumili ng hindi bababa sa isang dictionary Ipakita ang button na Refresh Pindutin para lumipat sa susunod na lyrics candidate; long-press para maghanap ng lyrics gamit ang pamagat ng kanta at artist Maghanap ng lyrics @@ -540,8 +583,10 @@ Mga kinakailangang cookie: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure Na-save I-highlight ang mga salita o pantig na naka-sync Unahin ang word-by-word na lyrics kung available. Kapag may word timing ang lyrics, ine-highlight ang bawat salita o pantig na kasabay ng musika. + Papakinikinisan ang mga inactive lines + Ilalarawan ang bawat line na hindi kasalukuyang inawit nang bahagyang mas maliit, sa itaas ng pagdim ng ilaw nito Ipakita hiwalay ang background vocals - Gine-likas ang background na bokol na nakasulat sa bracket, tulad ng \"(Oh) gusto kita\", mula sa linya ng kanta upang maging hiwalay na linya na may mas mahinaw na tunog + Hinihiwalay ang background vocals at harmony mula sa lead line. Ginagalaw ang background vocal na nakasulat sa brackets, tulad ng \"(Oh) I want you\", labas ng sung line patungo sa sarili nitong dimmed line Itago ang linyang na-play na. Itago ang mga linya ng lyrics sa itaas ng linyang kasalukuyang pinapatugtog Itago ang mga lyrics na hindi pa na-play. @@ -599,10 +644,15 @@ Nalalapat sa parehong limitasyon. Sa 1.75, ang apat na full-width character ay m Isinalin ni %s Isina-romanisa ni %s Napag-romanisan ni %s + Romanized ng Rime (%s) Isinalin Wika ng pagsasalin - Wika ng romanisasyon + Wika para sa romanization o transliteration + "Wika ng target ng pagsasalin: +%s" + "Wika ng target ng Romanization o transliteration: +%s" Nagsasalin… Hindi ma-isalin ang lyrics Romanisado @@ -616,7 +666,7 @@ Nalalapat sa parehong limitasyon. Sa 1.75, ang apat na full-width character ay m Nagre-refresh Na-refresh QuickLRC API key - Ang QuickLRC ay nagtutranscribe o nag-a-align ng lyrics para sa kasalukuyang track. Isang hiwalay na serbisyo, walang kaugnayan sa OpenAI-compatible endpoint sa itaas. + QuickLRC ay nagttrancription o time-aligns ng mga lyrics para sa current track Na-save ang QuickLRC key. Gamitin ang align button sa lyrics search dialog, o ang panel prompt kapag may track na walang lyrics. Na-save ang QuickLRC API key Hindi tinanggap ng QuickLRC na key na iyon @@ -632,58 +682,50 @@ Nalalapat sa parehong limitasyon. Sa 1.75, ang apat na full-width character ay m Walang lyrics para i-align - imbes ay nagtutranscribe. Hindi na-processing ng QuickLRC ang track na ito Tumapos na ang QuickLRC credits para sa track na ito - "Ang mga lyrics ay ini-match base sa titulo at artist. Maaaring walang makita o maling bersyon dahil sa abnormal metadata. Gamitin ang refresh para lumipat ng mga bersyon. Mga provider na may marka * ay nangangailangan ng token. Hindi laging hinahanap ang lahat ng enabled providers ayon sa order, ngunit sumasagot ang bawat isa base sa bilis nito. + "Ang mga lyrics ay ina-match ayon sa pamagat at artist, ang hindi karaniwang metadata ay maaaring makahanap ng walang anumang bagay o mali bersyon. Gamitin ang refresh upang lumipat sa mga bersyon. Mga provider na may marka * ay nangangailangan ng token. Lahat ng pinagana na provider ay hindi laging hinahanap ayon sa iyong order, ngunit tumutugon base sa bilis nila. -Ang mga lyrics ay galing sa third-party services na hindi related, sponsored, o endorsed by Morphe. Bawat isa ay ginagamit kung ano man ito: ang mga lyrics, availability, at licensing ay nananatiling nasa serbisyo at mga licensor nila, at ang paggamit nito ay maaaring subject to their terms of service. Ano man ang maabot sa enabled service ay ang lookup na hiniling mo - ang title, artist, at, kung kinakailangan ng serbisyong iyon, video ID. Ang optional AI endpoint at QuickLRC transcription ay tumatakbo lamang matapos ka itong i-configure, at tatanggapin ang lyrics text o video URL ng track." +Ang mga lyrics ay galing sa mga third-party services na walang koneksyon, hindi sinusuportahan, o kinilala ng Morphe. Bawat isa ay ginagamit kung ano ito: ang mga lyrics, ang kanilang availability, at ang kanilang licensing ay mananatili sa mga serbisyo at ang mga tagapagbigay-lisensya, at ang paggamit ng mga ito ay maaaring magsaklaw sa mga kondisyon ng serbisyo nila. Ano ang abot sa isang enabled service ay lamang ang lookup na hinihingi mo - ang pamagat, ang artist, at, kung kinakailangan ng serbisong iyon, ang video ID. Ang opsyonal na AI endpoint at QuickLRC transcription ay tumatakbo lamang pagkatapos mong i-configure ang mga ito, at tatanggapin ang mga teksto ng lyrics o URL ng track." Gawing enabled lamang ang mga provider na ginagamit mo, o panatilihin ang default - ang tuloy-tuloy na mga request ay maaaring magpabigat sa maliit na community lyrics sites Itago ang linya ng kredito Itago ang linya ng kredito na ipinapakita sa ibaba ng lyrics. Pagpalitin ang pagsasalin at romanisasyon. - Kapag ON, nasa ibaba ng lyrics ang romanisasyon at nasa itaas ang pagsasalin; kapag OFF, nananatili sa itaas ang romanisasyon at nasa ibaba ang pagsasalin. - Pagsasalin - Romanisasyon - Tagapagbigay ng pagsasalin - Google (Default, Libre) - Compatible sa OpenAI (Custom) - Provider ng Romanization - Google (Default) - Lokal / Offline na Diksyunaryo - Compatible sa OpenAI (Custom) - Seksyon ng AI - Ginagamit lamang kapag walang kasamang mga label ng seksyon ang provider tulad ng Chorus o Verse, na tutukuyin naman ng iyong OpenAI-compatible na model - AI & Cloud Endpoints - I-configure ang mga API key, modelo, endpoint, at prompt template para sa mga AI provider. - API endpoint URL (hal. https://api.openai.com/v1/chat/completions) + ON ay naglalagay ng romanization o transliteration sa ilalim ng mga lyrics at translation sa itaas; OFF ay pinapanatili ang romanization o transliteration sa itaas at translation sa ilalim. Ang pagbasa na inilagay sa itaas ng bawat syllable ay gumagalaw kasama ito, mula sa itaas ng kanyang syllage patungo sa ilalim nito + Palaging i-center ang mga lyrics + ON ay naglalagay ng bawat line sa gitna at naiiwalat ang source alignment, kasama ang duet sides; OFF ay pinapanatili ang alignment na hiningi ng mga lyrics + Dapat napanatiling on ang hindi bababa sa isang provider + Mga Pagsasalin + Romanization o transliteration + Offline Dictionary + Talaan ng mga salita sa Rime + Detektahin ang sections gamit ang AI + Ginagamit lamang kapag ang provider ay walang label ng section tulad ng Chorus o Verse, na detektahin ng iyong OpenAI-compatible model pagkatapos. Ang pag-edit ng row ay bubukas din ng prompt nito + API endpoint URL (hal. https://api.openai.com/v1/chat/completions, https://api.anthropic.com/v1/chat/completions, https://generativelanguage.googleapis.com/v1beta/openai/chat/completions) Template ng Prompt Mga Placeholder: {language}, {task}, {title}, {artist}, {count}, {lines} Nai-save na ang prompt Na-save ang prompt, ngunit hindi pa gagamit ang AI endpoint. I-configure muna ito sa ilalim ng AI Settings. Na-reset na ang prompt - Hindi valid JSON. Ayusin ito o linisin ang field bago save Na-save ang mga setting ng Translation & Romanization Ibinalik sa default ang mga setting Ang Base URL ay dapat magsimula sa http:// o https:// - I-test ang Koneksyon - I-configure ang mga Endpoint - Ipakita ang Lahat ng Endpoint + Hindi kumpleto ang settings o tinanggihan ng provider Tinitignan ang koneksyon… - Provider ng Pagsasalin - Provider ng Romanization - Kuhanin ang mga Modelo + %1$s: Nakakonekta! (%2$s) + %1$s: Nabigo ang koneksyon. Tingnan ang settings. + Ilagay muna ang API key Kukuha ng mga modelo… Pumili ng Modelo Nabigong kuhanin ang mga modelo. Suriin ang API key o koneksyon. - Mga Setting ng Gemini API - Compatible sa OpenAI (Custom Endpoint) - Mga Setting ng DeepL API - API key + Prompt Modelo Base URL + Pumili ng serbisyo API key (opsyonal) - Prompt sa pagsasalin Karagdagang request body (opsyonal na JSON) + Extra request headers (optional JSON) + Kuwentuhin ang mga pangalan at values, hal. {\"api-key\": \"abc\"}. Ang walang quote value na naglalaman ng / : o number ay tinatanggi. Ang walang laman na value ay inaalis ng header. + Kuwentuhin ang mga pangalan at values, hal. {\"temperature\": 0.3}. Ang walang quote value na naglalaman ng / o : ay tinatanggi. Hindi maia-setup ang Model at messages dito. Authentication key (Libre o Pro) - I-configure ang mga API key, modelo, endpoint, at prompt 8.23.51 - I-disable ang mga bold na icon 9.13.51 - Ibalik ang Player Up next, Lyrics, at Related tabs diff --git a/patches/src/main/resources/addresources/values-fil-rPH/youtube/strings.xml b/patches/src/main/resources/addresources/values-fil-rPH/youtube/strings.xml index 735ebc30f7..7d48cd1c99 100644 --- a/patches/src/main/resources/addresources/values-fil-rPH/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-fil-rPH/youtube/strings.xml @@ -29,6 +29,8 @@ Second \"item\" text" Mga sari-sari Video Ibalik ang mga lumang menu ng pagpipilian + Ipakita ang mga mensahe sa YouTube snackbar + Ang mga mensahe ng Morphe, tulad ng kumpirmasyon ng button ng manlalaro, ay ipinapakita sa YouTube snackbar imbes na popup Alisin ang mga paghihigpit sa pagtugtog sa likuran Patayin ang pag-play ng Shorts sa likuran diff --git a/patches/src/main/resources/addresources/values-fr-rFR/music/strings.xml b/patches/src/main/resources/addresources/values-fr-rFR/music/strings.xml index 7610721258..7611deb900 100644 --- a/patches/src/main/resources/addresources/values-fr-rFR/music/strings.xml +++ b/patches/src/main/resources/addresources/values-fr-rFR/music/strings.xml @@ -462,12 +462,47 @@ Pour que le scrobbling continue à se synchroniser pendant la lecture en arrièr Affichage Synchronisation Panneau - Point de terminaison de l\'IA Tous les fournisseurs sont désactivés - paroles intégrées et paroles incorporées Réinitialiser l\'ordre des fournisseurs ? Rétablir les fournisseurs dans l\'ordre de priorité par défaut + Rime Wordlist + "La liste de mots Rime nécessite également UniDict : dans le fournisseur de dictionnaire hors ligne, cochez « Caractères chinois et cantonais (UniDict) » et laissez-le terminer son téléchargement. Sans cela, une liste qui lit des caractères ne fournira aucune lecture. + +Cochez les listes que la liste de mots Rime doit lire, puis appuyez sur Enregistrer pour les télécharger. Une liste que vous avez ajoutée est supprimée lorsque vous décochez sa case. + +Chaque liste est votre propre table mot-à-lecture, un fichier .dict.yaml avec des lignes comme : +字 ham +典 din2 +La première colonne est le mot, la deuxième sa lecture ; les colonnes supplémentaires et les lignes sans tabulation sont ignorées." + Aucune liste de mots Rime n\'est cochée pour la langue ci-dessus. Appuyez sur ce nom pour en cocher une, ou choisissez une langue qui possède une liste + Ajouter + Ajouter une liste Rime Wordlist + Un fichier .dict.yaml, ou un zip en contenant un, avec des lignes comme 字 ham et 典 din2. La liste est récupérée lorsque vous cochez sa case et appuyez sur Enregistrer, et sa licence est à vérifier par vos soins + Code de langue + Deux ou trois lettres, comme \'hak\' ou \'wuu\' + Nom + Affiché sous la lecture, comme pour le Hakka (Meixian) + Adresse de la liste de mots + Adresse https:// du .dict.yaml ou de son fichier zip + Caractères hors d\'un mot + Automatique + Caractères cantonais + Caractères mandarins + Aucune lecture de caractère + Renseignez le code, le nom et l\'adresse + Le code doit comporter deux ou trois lettres, comme \'hak\' + L\'adresse doit commencer par https:// + Le nom et l\'adresse ne peuvent pas contenir de barre verticale + %1$s possède déjà une table de mots + %1$s est lu par le romanisateur intégré et ne peut pas accepter de liste de mots + Non utilisé : cette langue possède déjà sa propre lecture. Décochez cette case pour le supprimer + %1$s ajouté. Cochez sa case et appuyez sur \'Enregistrer\' pour la télécharger + Listes de mots prêtes + Listes de mots prêtes (%1$d entrées) + Lecture de la liste de mots : %1$d%% + Échec du téléchargement de la liste de mots. appuyez à nouveau sur \'Enregistrer\' pour réessayer Effacer les paroles enregistrées - Supprimer les paroles enregistrées sur cet appareil + Supprimer les %1$s Mo de paroles conservées sur cet appareil %1$s Mo effacés Obtenir un jeton Apple Music Media-user-token @@ -527,14 +562,22 @@ Cookies nécessaires : SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PA Afficher un bouton \'Copier\' Appuyez pour copier les paroles originales en texte brut ; appuyez longuement pour enregistrer le fichier source brut des paroles dans \'Téléchargés\' Afficher un bouton \'Traduire\' - Appuyez pour activer/désactiver la traduction ; appuyez longuement pour afficher uniquement la traduction. Utilise les traductions du fournisseur, Google Traduction ou une API compatible avec OpenAI - Afficher un bouton \'Romaniser\' - Appuyez pour activer/désactiver la romanisation ; appuyez longuement pour afficher uniquement la romanisation. Utilise la romanisation du fournisseur, Google Traduction ou une API compatible avec OpenAI - Romanisation et Synchronisation locaux - Effectue la romanisation sur l\'appareil lorsque le fournisseur, votre modèle et Google Traduction échouent. La lecture suit chaque mot des paroles synchronisées ; la synchronisation par mot doit être activée. Chinois, Cantonais, Japonais, Coréen, Grec, Russe, Géorgien, Arménien ; environ 120 Mo - Lecture %1$s : %2$d%% + Appuyez pour basculer la traduction ; appuyez longuement pour la traduction uniquement. Utilise les fournisseurs de traduction activés dans l\'ordre indiqué + Afficher le bouton \'Romaniser\' ou \'Translitérer\' + Appuyez pour basculer la romanisation ou la translittération ; appuyez longuement pour la romanisation ou la translittération uniquement. Utilise les fournisseurs de romanisation activés dans l\'ordre indiqué + Activer la translittération + Autoriser les lectures non latines provenant de la source des paroles ; désactivée, ne conserver que les lettres latines, les lettres accentuées et les chiffres de ton + Lit chaque mot d\'une parole synchronisée (synchronisation des mots activée) ; le coréen ne nécessite aucun téléchargement ; quatorze écritures partagent un seul téléchargement CLDR ; les caractères Han sont lus par Rime Wordlist lorsque sa table de mots couvre la langue + Téléchargement des dictionnaires de lecture… Lectures prêtes (%1$d entrées) Échec du téléchargement - désactivez cette option puis réactivez-la pour réessayer + Dictionnaire hors ligne + Japonais (SudachiDict, ~75 Mo) + Caractères chinois et cantonais (UniDict, ~8 Mo) + Mots chinois (pinyin-pro, ~17 Mo) + Translittération du Grec, Cyrillique Slave (y compris le Russe, l\'Ukrainien, le Bulgare, le Serbe, le Biélorusse et le Macédonien), Géorgien, Arménien, Hébreu, Arabe, Amharique et Syriaque (CLDR, ~0,5 Mo) + Lecture %1$s : %2$d%% + Choisissez au moins un dictionnaire Afficher un bouton \'Actualiser\' Appuyez pour passer aux paroles candidates suivantes ; appuyez longuement pour rechercher les paroles par titre de chanson et artiste Rechercher des paroles @@ -545,8 +588,10 @@ Cookies nécessaires : SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PA Enregistré Mettre en surbrillance les mots ou les syllabes synchronisés Privilégier les paroles mot à mot lorsqu\'elles sont disponibles. Lorsque les paroles disposent d\'un minutage mot à mot, mettre en surbrillance chaque mot ou chaque syllabe en rythme avec la musique + Réduire les lignes inactives + Affiche chaque ligne qui n\'est pas en cours de chant dans une taille légèrement plus petite, en plus de l\'atténuer Afficher les voix d\'accompagnement séparément - Déplace une voix d\'accompagnement écrite entre parenthèses, comme \"(Oh) I want you\", hors de la ligne chantée pour l\'afficher sur sa propre ligne de façon plus discrète + Sépare les voix de fond et l\'harmonie de la ligne principale. Déplace une voix de fond écrite entre parenthèses, comme « (Oh) I want you », hors de la ligne chantée vers une ligne atténuée dédiée Masquer les paroles déjà lues Masquer les lignes de paroles au-dessus de la ligne en cours de lecture Masquer les paroles non lues @@ -604,10 +649,15 @@ S'applique aux deux limites. À 1,75, quatre caractères pleine largeur comptent Traduit par %s Romanisé par %s Romanisé par %s + Romanisé par Rime (%s) Traduit Langue de traduction - Langue de romanisation + Langue de romanisation ou de translittération + "Langue cible de traduction : +%s" + "Langue cible de romanisation ou de translittération : +%s" Traduction… Impossible de traduire les paroles Romanisé @@ -621,7 +671,7 @@ S'applique aux deux limites. À 1,75, quatre caractères pleine largeur comptent Actualisation Actualisé Clé API QuickLRC - QuickLRC transcrit ou synchronise les paroles avec le morceau en cours. Il s\'agit d\'un service distinct, sans lien avec le point de terminaison compatible avec OpenAI ci-dessus + QuickLRC transcrit ou aligne temporellement les paroles pour la piste actuelle Clé QuickLRC enregistrée. Utilisez le bouton de synchronisation dans la fenêtre de recherche des paroles, ou l\'invite du panneau lorsqu\'un morceau n\'a pas de paroles Clé API QuickLRC enregistrée QuickLRC n\'a pas accepté cette clé @@ -637,56 +687,50 @@ S'applique aux deux limites. À 1,75, quatre caractères pleine largeur comptent Aucune parole à synchroniser - transcription à la place QuickLRC n\'a pas pu traiter ce morceau Les crédits QuickLRC pour ce morceau sont épuisés - "Les paroles sont associées par titre et artiste ; des métadonnées inhabituelles peuvent ne rien donner ou renvoyer une mauvaise version. Utilisez l'actualisation pour changer de version. Les fournisseurs marqués d'un * nécessitent un jeton. Tous les fournisseurs activés ne sont pas toujours sollicités dans votre ordre, mais répondent selon leur rapidité.\n\nLes paroles proviennent de services tiers qui ne sont ni affiliés, ni sponsorisés, ni approuvés par Morphe. Chacun est utilisé tel quel : les paroles, leur disponibilité et leur licence restent la propriété de ces services et de leurs concédants, et leur utilisation peut être soumise à leurs conditions d'utilisation . Seule la recherche demandée est transmise à un service activé - le titre, l'artiste et, si le service en a besoin, l'identifiant de la vidéo. Le point de terminaison de l'IA facultatif et la transcription QuickLRC ne s'exécutent qu'après leur configuration, et reçoivent le texte des paroles ou l'URL vidéo du morceau." + "Les paroles sont associées par titre et artiste ; des métadonnées inhabituelles peuvent ne rien donner ou renvoyer la mauvaise version. Utilisez actualiser pour changer de version. Les fournisseurs marqués * nécessitent un jeton. Tous les fournisseurs activés ne sont pas toujours sollicités dans votre ordre, mais répondent selon leur rapidité. + +Les paroles proviennent de services tiers qui ne sont ni affiliés, ni sponsorisés, ni approuvés par Morphe. Chacun est utilisé tel quel : les paroles, leur disponibilité et leur licence restent la propriété de ces services et de leurs concédants, et leur utilisation peut être soumise à leurs conditions d'utilisation. Seule la recherche demandée est transmise à un service activé - le titre, l'artiste et, si ce service en a besoin, l'identifiant vidéo. Le point de terminaison IA optionnel et la transcription QuickLRC ne s'exécutent qu'après leur configuration, et reçoivent le texte des paroles ou l'URL vidéo du morceau." Activez uniquement les fournisseurs que vous utilisez, ou conservez les paramètres par défaut - des requêtes constantes peuvent submerger les petits sites communautaires de paroles Masquer la ligne de crédit Masquer la ligne de crédit affichée sous les paroles Inverser la traduction et la romanisation - ACTIVÉE place la romanisation sous les paroles et la traduction au-dessus ; DÉSACTIVÉE conserve la romanisation au-dessus et la traduction en dessous - Traduction - Romanisation - Fournisseur de traduction - Google (Par défaut, Gratuit) - Compatible avec OpenAI (Personnalisé) - Fournisseur de romanisation - Google (Par défaut) - Dictionnaire Local / Hors ligne - Compatible avec OpenAI (Personnalisé) - Section par IA - Utilisé uniquement lorsque le fournisseur ne fournit pas d\'étiquettes de section, comme \'Refrain\' ou \'Couplet\', que votre modèle compatible avec OpenAI détecte ensuite - Points de terminaison IA & Cloud - Configurez les clés API, les modèles, les points de terminaison et les modèles de prompts pour les fournisseurs d\'IA - URL du point de terminaison de l\'API (par ex. https://api.openai.com/v1/chat/completions) + ON place la romanisation ou la translittération sous les paroles et la traduction au-dessus ; OFF garde la romanisation ou la translittération au-dessus et la traduction en dessous. Une lecture placée sur chaque syllabe se déplace avec elle, passant du dessus au dessous de la syllabe + Toujours centrer les paroles + ON place chaque ligne au milieu et ignore l\'alignement de la source, y compris les côtés en duo ; OFF conserve l\'alignement demandé par les paroles + Au moins un fournisseur doit rester activé + Traduction + Romanisation ou translittération + Dictionnaire hors connexion + Liste de mots Rime + Détecter les sections avec l\'IA + Utilisé uniquement lorsque le fournisseur ne fournit pas d\'étiquettes de section telles que Refrain ou Couplet, que votre modèle compatible OpenAI détecte ensuite. La modification de la ligne ouvre également son invite + URL du point de terminaison de l\'API (par exemple https://api.openai.com/v1/chat/completions, https://api.anthropic.com/v1/chat/completions, https://generativelanguage.googleapis.com/v1beta/openai/chat/completions) Modèle de prompt Variables de remplacement : {language}, {task}, {title}, {artist}, {count}, {lines} Prompt enregistré Prompt enregistré, mais le point de terminaison de l\'IA n\'est pas encore utilisable. Configurez-le d\'abord dans \'Paramètres de l\'IA\' Prompt réinitialisé - Le JSON n\'est pas valide. Corrigez-le ou videz le champ avant d\'enregistrer Paramètres de traduction & de romanisation enregistrés Paramètres réinitialisés par défaut L\'URL de base doit commencer par http:// ou https:// - Tester la connexion - Configurer les points de terminaison - Afficher tous les points de terminaison + Paramètres incomplets ou rejetés par le fournisseur Validation de la connexion… - Fournisseur de traduction - Fournisseur de romanisation - Récupérer les modèles + %1$s : Connecté ! (%2$s) + %1$s : Échec de la connexion. Vérifiez les paramètres. + Entrez d\'abord la clé API Récupération des modèles… Sélectionnez un modèle Échec de la récupération des modèles. Vérifiez la clé API ou la connexion. - Paramètres de l\'API Gemini - Compatible avec OpenAI (Point de terminaison personnalisé) - Paramètres de l\'API DeepL - Clé API + Invite Modèle URL de base + Choisir un service Clé API (facultative) - Prompt de traduction Corps de requête supplémentaire (JSON facultatif) + En-têtes de requête supplémentaires (JSON optionnel) + Citiez les noms et valeurs, par exemple {\"api-key\": \"abc\"}. Une valeur non citée contenant / : ou un nombre est rejetée. Une valeur vide supprime un en-tête. + Citiez les noms et valeurs, par exemple {\"temperature\": 0.3}. Une valeur non citée contenant / ou : est rejetée. Le modèle et les messages ne peuvent pas être définis ici. Clé d\'authentification (Gratuit ou Pro) - Configurer les clés API, les modèles, les points de terminaison et les prompts 8.23.51 - Désactive les icônes en gras 9.13.51 – Restaure les onglets \'À suivre\', \'Paroles\' et \'Similaires\' du lecteur diff --git a/patches/src/main/resources/addresources/values-fr-rFR/youtube/strings.xml b/patches/src/main/resources/addresources/values-fr-rFR/youtube/strings.xml index 619f3a4905..75a6941c15 100644 --- a/patches/src/main/resources/addresources/values-fr-rFR/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-fr-rFR/youtube/strings.xml @@ -29,6 +29,8 @@ Second \"item\" text" Divers Vidéo Restaurer les anciens menus de paramètres de YouTube + Afficher les messages dans la barre de notification YouTube + Les messages de Morphe, tels que la confirmation de l\'appui sur un bouton du lecteur, sont affichés dans la barre de notification YouTube plutôt que dans un pop-up Supprimer les restrictions de lecture en arrière-plan Désactiver la lecture en arrière-plan @@ -873,7 +875,7 @@ Les 'Cookies des sous-titres' doivent être fournis" Résout une erreur de vérification de précondition qui peut empêcher le chargement de la transcription Afficher les sous-titres en mode image dans l\'image Blocs de sous-titres stables - Groupez les sous-titres automatiques en blocs stables et lisibles. Certains mots peuvent apparaître avant d\'être prononcés. Redémarrage requis. + Regroupe les sous-titres automatiques en blocs stables et lisibles. Certains mots peuvent apparaître avant même qu\'ils soient prononcés. Un redémarrage nécessaire. Boutons de navigation Choisissez les boutons qui apparaissent dans la barre de navigation inférieure diff --git a/patches/src/main/resources/addresources/values-ga-rIE/music/strings.xml b/patches/src/main/resources/addresources/values-ga-rIE/music/strings.xml index 3f05f3a03e..4a489bfe0e 100644 --- a/patches/src/main/resources/addresources/values-ga-rIE/music/strings.xml +++ b/patches/src/main/resources/addresources/values-ga-rIE/music/strings.xml @@ -462,12 +462,47 @@ Chun a chinntiú go bhfuil scrobbling i gcóir le linn imirt san chúlra, déan Taispeántas Sioncrónú Painéal - Críochphointe AI Gach soláthraí díchumasaithe — liricí ionsuite agus liricí leabaithe Athshocraigh ord an tsoláthraí? Athchóirigh soláthraithe chuig an ord tosaíochta réamhshocraithe + Liosta focal Rime + "Teastaíonn UniDict ó liosta focail Rime freisin: sa sholáthraí dictionary log-offlíne, cuir tic ag \"Carachtair Síneacha agus Chantaise (UniDict)\" agus lig dó deireadh a thabhairt do luchtaigh ar dtús. Gan é, ní thugann liosta léi carachtair aon léamh. + +Cuir tic ag na liostaí ba mhaith le liosta Rime a léamh, ansin tapáil Sábháil chun iad a lúchtáil. Baintear liosta a chuir tú leis nuair a bhaineann tú tic uaidh. + +Is clár focal go léitheoireacht féin thú gach liosta, .dict.yaml le línte mar: +字 ham +典 din2 +Is é an chéad cholún an focal, is é an dara ceann a léitheoireacht; déantar colúin breise agus línte gan tab a neamhaird." + Níl aon liosta focal Rime tictha don teanga thuas. Cliceáil ar an ainm seo chun ceann a thicáil nó roghnaigh teanga a bhfuil liosta aici + Cuir leis + Cuir Liosta focal Rime leis + Comhad .dict.yaml, nó zip ag coinneáil amháin, le línte mar 字 ham agus 典 din2. Faightear an liosta nuair a chuireann tú tic air agus tapáil Sábháil, agus is cead duit a sheiceáil a cheadúnas + Cód teanga + Dá o litreacha nó trí o litreacha, mar shamla hak nó wuu + Ainm + Feictear faoin léamh, mar shamla Hakka (Meixian) + Seoladh liosta focal + Sealbhóir https:// den .dict.yaml nó a zip + Carachtair nach bhfuil i bhfocal + Uathoibríoch + Carachtair Cantainnis + Carachtair Mandarinis + Gan léamh carachtair + Líon isteach an cóid, ainm agus seoladh + Ní mór an cóid a bheith comhdhéanta de dhá o nó trí o litreacha, mar shampla hak + Ní mór an seoladh tosú le https:// + Ní féidir leis an n-ainm agus an seoladh barra díreach a chur san áit + Tá liosta focal cheana féin ag %1$s + Léitear %1$s ag an romanizeoir bunaithe agus ní féidir liosta focal a ghlacadh + Níl sé úsáidte: Tá a léamh féin ag an teanga seo cheana, mar sin bain tic uaidh chun é a bhaint as + Cuireadh %1$s leis - tic é agus cliceáil Save chun é a íoslódáil + Liostaí foclacha réidh + Liostaí focal réidh (%1$d iontráil) + Ag ríomh liosta focal: %1$d%% + Theip ar íoslódáil an liosta focal - cliceáil Save arís chun triail eile a dhéanamh Glan na liricí stóráilte - Scrios na liricí atá stóráilte ar an ngléas seo + Scrios %1$s MB de liricí atá stóráilte ar an ngléas seo Glanadh %1$s MB Faigh comhartha Comhartha úsáideora meán Apple Music @@ -527,14 +562,22 @@ Fianáin riachtanacha: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PA Taispeáin an cnaipe Cóipeála Beartaigh chun na liricí bunaidh a chóipeáil mar ghnáth-théacs; brúigh fada chun an comhad foinse amh de na liricí a shábháil sa bhfillteán Íoslódálacha Taispeáin an cnaipe Aistrigh - Tapáil chun an t-aistriúchán a scoránaigh; brúigh fada chun an t-aistriúchán amháin a fháil. Úsáidtear aistriúcháin ó sholáthraithe, Google Translate, nó API atá comhoiriúnach le OpenAI. - Taispeáin an cnaipe Rómhánaithe - Beartaigh chun an rómánú a scoránaigh; brúigh fada chun an rómánú amháin a thaispeáint. Úsáidtear rómánú an tsoláthraí, Google Translate, nó API atá comhoiriúnach le OpenAI. - Rómhánú agus Ailíniú Áitiúil - Úsáid traslitriú ar an bhfeiste nuair nach n-éiríonn leis an soláthraí, le do mhúnla, ná le Google Translate. Leanann an léamh gach focal de liricí atá sioncronaithe le ham; ní mór an sioncronú focal a bheith curtha ar siúl. Sínis, Cantainis, Seapáinis, Cóiréis, Gréigis, Rúisis, Seoirsis, Airméinis; thart ar 120 MB. - Á léamh %1$s: %2$d%% + Cliceáil chun an t-aistriúchán a scoránaigh; brúigh fada chun an t-aistriúchán amháin a fháil. Úsáideann sé soláthraithe aistríochta cumasaithe as a ord liostaithe + Taispeáin cnaipe Romanize nó Transliterate + Cliceáil chun an romhánú nó traslitriú a scoránaigh; brúigh fada chun an romhánú nó traslitriú amháin a fháil. Úsáideann sé soláthraithe romhánaithe cumasaithe as a ord liostaithe + Cumasaigh traslitriú + Ceadaigh léamha neamh-Laidine ó fhoinse na liricí; nuair a bheidh sé as, coinigh litreacha Laidine amháin, litreacha accented agus digiteanna tonnach + Léann sé gach focal den liric aimsithe (faoi sioncronú focail ar); Teastaíonn Nialaí le haghaidh Íosluchtaithe; Scaipeann ceithre script ceann CLDR Íosluchtaithe; Léitear carachtair Han ag Liosta focal Rime nuair a chlúdaíonn a liosta focal an teanga + Foclóirí léamha á n-íoslódáil... Léamha réidh (%1$d iontráil) Theip ar an íoslódáil — múch é agus athshocraigh chun iarracht eile a dhéanamh + Foclóir As Líne + Seapáinis (SudachiDict, ~75 MB) + Carachtair Chíniseachas agus Cantainnis (UniDict, ~8 MB) + Foclacha Cinisineachais (pinyin-pro, ~17 MB) + Gréigis, Slavic Cyrillic (ina ghabhann sé Rúisis, Úcráinis, Bulgáiris, Seirbis, Béalaruis, Macadóinis), Seoirsis, Airméinis, Ebréis, Arabais, Amharais agus traslitriú Síríacsach (CLDR, ~0.5 MB) + Á léamh %1$s: %2$d%% + Roghnaigh ar a laghad focalóir amháin Taispeáin an cnaipe athnuachana Beartaigh chun aistriú go dtí an chéad rogha eile d’fhocail amhráin; brúigh fada chun cuardach a dhéanamh ar fhocail amhráin de réir theideal an amhráin agus an ealaíontóra Cuardaigh liricí @@ -545,8 +588,10 @@ Fianáin riachtanacha: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PA Sábháilte Aibhsigh focail nó siollaí sioncronaithe Tabhair tosaíocht do liricí a chuirtear i láthair focal ar fhocal nuair a bhíonn siad ar fáil. Nuair a bhíonn uainiú focal ag gabháil leis na liricí, déantar gach focal nó siolla a aibhsiú i gcomhthráth leis an gceol + Bearr línte neamh-ghníomhach + Tarraingíonn sé gach líne nach bhfuil á canú faoi láthair beagán níos lú, ar bharr í a dhorchaigh Taispeáin ceol cúlra go hiomlán ar leithligh - Bogann sé guth cúlra atá scríofa idir lúibíní — mar shampla \"(Oh) I want you\" — amach as an líne a chantar, á chur i líne ar leith atá níos séimhe. + Scaipeann sé guthanna cúlra agus comhcheangal ón líne tosaigh. Bogann sé guth cúlra scríofa i lúibíní, mar shampla \"(Oh) I want you\", amach as an líne a chantar go líne ar leith atá níos dorcha Folaigh na liricí a seinneadh Folaigh línte na liricí os cionn na líne atá á seinm faoi láthair Folaigh liricí nár seinneadh @@ -604,10 +649,15 @@ Baineann sé leis an dá theorainn. Ag 1.75, comhairtear ceithre charachtar lán Aistrithe ag %s Rómhánachaithe ag %s Rómhánachaithe ag %s + Rómhánaithe ag Rime (%s) Aistrithe Teanga aistriúcháin - Teanga rómháiníochta + Teanga romhánaithe nó traslitrithe + "Teanga sprioc an aistriúcháin: +%s" + "Teanga sprioc an romanization nó transliteration: +%s" Ag aistriú… Ní féidir na líricí a aistriú Rómánaithe @@ -621,7 +671,7 @@ Baineann sé leis an dá theorainn. Ag 1.75, comhairtear ceithre charachtar lán Athnuachanach Athnuachana Eochair API QuickLRC - Déanann QuickLRC tras-scríobh nó sioncronú ama ar fhocail an amhráin atá á sheinm faoi láthair. Is seirbhís ar leith í seo, nach bhfuil baint aici leis an gceann scríbe thuas atá comhoiriúnach le OpenAI + Déanann QuickLRC litríocht nó aimsithe ama ar na liricí don trealaí reatha Sábháladh eochair QuickLRC. Bain úsáid as an gcnaipe ailínithe sa dialóg cuardaigh liricí, nó as an leid sa phainéal nuair nach bhfuil aon liricí ag rian Eochair API QuickLRC sábháilte Níor ghlac QuickLRC leis an eochair sin @@ -637,56 +687,50 @@ Baineann sé leis an dá theorainn. Ag 1.75, comhairtear ceithre charachtar lán Níl aon liricí le comhoiriúnú — á n-aistriú i scríbhinn ina n-áit Níorbh fhéidir le QuickLRC an rian seo a phróiseáil Tá creidiúintí QuickLRC ídithe don rian seo - "Déantar liricí a mheaitseáil de réir teidil agus ealaíontóra, d'fhéadfadh sé nach mbeadh aon rud le fáil i meiteashonraí neamhghnácha nó go mbeadh an leagan mícheart. Úsáid athnuachan chun leaganacha a athrú. Éilíonn soláthraithe atá marcáilte le * comhartha. Ní iarrtar gach soláthraí cumasaithe i gcónaí i d'ordú, ach freagraíonn siad bunaithe ar a luas.\n\nTagann liricí ó sheirbhísí tríú páirtí nach bhfuil cleamhnaithe le, urraithe ná formhuinithe ag Morphe. Úsáidtear gach ceann mar atá sé: fanann na liricí, a n-infhaighteacht agus a gceadúnú leis na seirbhísí sin agus a gceadúnóirí, agus is féidir go mbeidh a n-úsáid faoi réir a dtéarmaí seirbhíse. Ní shroicheann seirbhís chumasaithe ach an cuardach a d'iarr tú - an teideal, an t-ealaíontóir, agus, i gcás ina bhfuil gá leis an tseirbhís sin, an ID físe. Ní ritheann an críochphointe AI roghnach agus trascríobh QuickLRC ach amháin tar éis duit iad a chumrú, agus téacs na liricí nó URL físe an riain a fháil." + "Déantar liricí a mheaitseáil de réir teidil agus ealaíontóra, d'fhéadfadh sé nach mbeadh aon rud le fáil i meiteashonraí neamhghnácha nó go mbeadh an leagan mícheart. Úsáid athnuachan chun leaganacha a athrú. Éilíonn soláthraithe atá marcáilte le * comhartha. Ní iarrtar gach soláthraí cumasaithe i gcónaí i d'ordú, ach freagraíonn siad bunaithe ar a luas. + +Tagann liricí ó sheirbhísí tríú páirtí nach bhfuil cleamhnaithe le, urraithe ná formhuinithe ag Morphe. Úsáidtear gach ceann mar atá sé: fanann na liricí, a n-infhaighteacht agus a gceadúnú leis na seirbhísí sin agus a gceadúnóirí, agus is féidir go mbeidh a n-úsáid faoi réir a dtéarmaí seirbhíse. Ní shroicheann seirbhís chumasaithe ach an cuardach a d'iarr tú - an teideal, an t-ealaíontóir, agus, i gcás ina bhfuil gá leis an tseirbhís sin, an ID físe. Ní ritheann an críochphointe AI roghnach agus trascríobh QuickLRC ach amháin tar éis duit iad a chumrú, agus téacs na liricí nó URL físe an riain a fháil." Ná cumasaigh ach na soláthraithe a úsáideann tú, nó coinnigh an réamhshocrú — is féidir le hiarratais leanúnacha an iomarca brú a chur ar shuímh bheaga liricí pobail Folaigh an líne chreidmheasa Folaigh an líne aitheantais a thaispeántar faoi na liricí Malartaigh an t-aistriúchán agus an rómánú - Cuireann AR an rómánú faoi na liricí agus an t-aistriúchán os a gcionn; coinníonn AS an rómánú os cionn na liricí agus an t-aistriúchán fúthu - Aistriúchán - Rómáinization - Soláthraí aistriúcháin - Google (Réamhshocráil, Saor) - Comhoiriúnach le OpenAI (Saincheaptha) - Soláthraí rómhánaithe - Google (Réamhshocráil) - Foclóir Áitiúil / As Líne - Comhoiriúnach le OpenAI (Saincheaptha) - Rannóg AI - Ní úsáidtear é seo ach amháin nuair nach soláthraíonn an soláthraí aon lipéid rannáin, ar nós \'Curfá\' nó \'Véarsa\'; braithfidh do mhúnla, atá comhoiriúnach le OpenAI, na lipéid sin ansin. - AI & Pointí Leoras Scamaile - Cumraigh eochracha API, samhlacha, críochphointí, agus teimpléid leid do sholáthraithe AI. - URL críochphointe API (m.sh. https://api.openai.com/v1/chat/completions) + Cuireann ON an romhánú nó traslitriú faoi na liricí agus an t-aistriúchán os a chionn; COISÍNÍONNN OFF an romhánú nó traslitriú os cionn agus an t-aistriúchán faoi. Bogann léamh curtha os cionn gach síleabaid leis, ó os a chionn go faoi + Líon na liricí i gcónaí + Cuireann ON gach líne sa lár agus déanann neart de shioncronú foinse, lena n-áirítear taobanna diúité; COISÍNÍONNN OFF an sioncronú atá ag iarraidh na liricí + Ní mór soláthraí amháin fanacht ar siúl + Aistriúchán + Romhánú nó Traslitriú + Foclóir As Líne + Liosta focal Rime + Aimsigh roinneanna le AI + Ní úsáidtear ach amháin nuair nach soláthraíonn an soláthraí aon lipéid roinne mar Curfá nó Véarsa, ar ndéanann do mhúnla atá comhoiriúnach le OpenAI iad a bhrath. Eagarlú an roinn oscailfidh freagra chomh maith + URL pointe bailí API (m.sh. https://api.openai.com/v1/chat/completions, https://api.anthropic.com/v1/chat/completions, https://generativelanguage.googleapis.com/v1beta/openai/chat/completions) Teimpléad Leide Áitchoimeádáin: {language}, {task}, {title}, {artist}, {count}, {lines} Sábháiltear leide Sábháladh an leid, ach níl an pointe críochta AI inúsáidte go fóill. Cumraigh é faoi Socruithe AI ​​ar dtús Athshocrú an leid - Ní JSON bailí é. Ceartaigh é nó glan an réimse sula sábhálann tú Socruithe aistriúcháin & Rómhálaíocht sábháilte Socruithe athshocraithe go dtí na réamhshocruithe Caithfidh URL bonn tosú le http:// nó https:// - Tástáil Nasc - Cumraigh Pointí Críche - Taispeáin Gach Críochphointe + Socruithe neamhiomlán nó diúltaithe ag an soláthraí Ag fíorú na nascachta… - Soláthraí Aistriúcháin - Soláthraí Rómhánaithe - Faigh Múnlaí + %1$s: Ceangaltach! (%2$s) + %1$s: Theip ar nasc. Fíoraigh na socruithe. + Cuir isteach an chlé API ar dtús Ag fáil samhlacha… Roghnaigh Múnla Theip ar fháil na múnlaí. Seiceáil an eochair API nó an nasc. - Socruithe API Gemini - Comhoiriúnach le OpenAI (Críochphointe Saincheaptha) - Socruithe API DeepL - Eochair API + Freagra Samhail URL Bun + Roghnaigh seirbhís Eochair API (roghnach) - Treoir aistriúcháin Comhlacht iarratais breise (JSON roghnach) + Ceanntásca iarratais breise (JSON roghnach) + Cúltáil ainmneacha agus luachanna, m.sh. {\"api-key\": \"abc\"}. Diúltófar luach neamhcúltaithe a chuimsíonn / : nó uimhir. Bíonn luach folamh ag baint le céantasc. + Cúltáil ainmneacha agus luachanna, m.sh. {\"temperature\": 0.3}. Diúltófar luach neamhcúltaithe a chuimsíonn / nó :. Ní féidir Mód agus teachtí a shocrú anseo. Eochair fhíordheimhnithe (Saor in aisce nó Pro) - Cumraigh eochracha API, samhlacha, pointí críche, agus leideanna 8.23.51 - Díchumasaigh deilbhíní troma 9.13.51 - Athchóirigh an t-imreoir Ar Aghaidh, Liricí, agus na cluaisíní Gaolmhara diff --git a/patches/src/main/resources/addresources/values-ga-rIE/youtube/strings.xml b/patches/src/main/resources/addresources/values-ga-rIE/youtube/strings.xml index b716b7b3c2..58950a167e 100644 --- a/patches/src/main/resources/addresources/values-ga-rIE/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ga-rIE/youtube/strings.xml @@ -29,6 +29,8 @@ Second \"item\" text" Ilghnéitheach Físeán Athchóirigh an scáileán socruithe YouTube sean + Teachtaireachtaí a thaispeáint i snackbar YouTube + Taispeántar teachtaireachtaí Morphe, cosúil le dearbhú cnaipe imreora, i snackbar YouTube in ionad pop-up Bain srianta súgartha cúlra Díchumasaigh súgradh sa chúlra diff --git a/patches/src/main/resources/addresources/values-gl-rES/music/strings.xml b/patches/src/main/resources/addresources/values-gl-rES/music/strings.xml index 2c39c37e61..97dc94e675 100644 --- a/patches/src/main/resources/addresources/values-gl-rES/music/strings.xml +++ b/patches/src/main/resources/addresources/values-gl-rES/music/strings.xml @@ -462,12 +462,11 @@ Para garantir que o scrobbling se sincronice durante a reprodución en segundo p Amosar Sincronización Panel - Endpoint de IA Todos os provedores desactivados: letras integradas e letras incrustadas Reestablecer a orde dos provedores? Restaurar os provedores á orde de prioridade predeterminada + Automático Limpar letras almacenadas - Eliminar as letras gardadas neste dispositivo Limparon %1$s MB Obter token Token de usuario de Apple Music Media @@ -527,14 +526,9 @@ Cookies necesarias: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPIS Amosar botón de copiar Toca para copiar a letra orixinal como texto sin formato; mantén premido para gardar o arquivo fonte da letra en Descargas Mostrar botón de Traducir - Toca para alternar a tradución; mantén premido para ver só a tradución. Utiliza traducións do provedor, Google Translate ou unha API compatible con OpenAI - Mostrar botón de Romanizar - Toca para alternar a romanización; mantén premido para ver só a romanización. Utiliza a romanización do provedor, Google Translate ou unha API compatible con OpenAI - Romanización e alineación local - Romaniza no dispositivo cando o provedor, o teu modelo e Google Translate fallen. A lectura segue a cada palabra dunha letra sincronizada; a sincronización de palabras debe estar activada. Chinés, cantonés, xaponés, coreano, grego, ruso, xeorxiano, armenio; arredor de 120 MB - Lendo %1$s: %2$d%% Lecturas listas (%1$d entradas) Erro na descarga: desactiva e activa isto de novo para reintentalo + Lendo %1$s: %2$d%% Mostrar botón de Actualizar Toca para cambiar ao seguinte candidato de letras; mantén premido para buscar letras polo título da canción e o artista Buscar letras @@ -605,7 +599,6 @@ Aplícase a ambos límites. Con 1.75, catro caracteres de ancho completo contour Traducido Lingua de tradución - Lingua de romanización Traducindo… Non foi posible traducir a letra Romanizado @@ -619,7 +612,6 @@ Aplícase a ambos límites. Con 1.75, catro caracteres de ancho completo contour Actualizando Actualizado Chave API de QuickLRC - QuickLRC transcribe ou sincroniza os tempos das letras da pista actual. Un servizo separado, sen relación co endpoint compatible con OpenAI anterior Clave de QuickLRC gardada. Usa o botón de sincronizar no diálogo de busca de letras, ou a indicación do panel cando unha pista non teña letras Chave API de QuickLRC gardada QuickLRC non aceptou esa chave @@ -635,56 +627,27 @@ Aplícase a ambos límites. Con 1.75, catro caracteres de ancho completo contour Non hai letras para sincronizar - transcribindo en seu lugar QuickLRC non puido procesar esta pista Os créditos de QuickLRC esgotáronse para esta pista - "As letras coinciden polo título e o artista; os metadatos inusuais poden non atopar nada ou a versión incorrecta. Use a actualización para cambiar de versión. Os provedores marcados con * requiren un token. Non sempre se solicita a todos os provedores activados na súa orde, senón que responden segundo a súa velocidade.\n\nAs letras provén de servizos de terceiros que non están afiliados, patrocinados nin avalados por Morphe. Cada un deles úsase tal cal é: as letras, a súa dispoñibilidade e as súas licenzas permanecen nos servizos e nos seus licenciantes, e o seu uso pode estar suxeito aos seus termos de servizo. O que chega a un servizo activado é só a busca que solicitou: o título, o artista e, cando o servizo o requira, o ID do vídeo. O endpoint de IA opcional e a transcrición de QuickLRC funcionan só despois de configuralos e reciben o texto das letras ou a URL do vídeo da pista." Activa nuras os provedores que uses, ou mantén os predefinidos - as solicitudes constantes poden saturar os sitios de letras de comunidades pequenas Ocultar liña de créditos Ocultar a liña de créditos que aparece debaxo das letras Trocar a tradución e a romanización - ACTIVO coloca a romanización abaixo da letra e a tradución arriba; DESACTIVADO mantén a romanización arriba e a tradución abaixo - Tradución - Romanización - Provedor de tradución - Google (Predeterminado, Gratuíto) - Compatible con OpenAI (Personalizado) - Proveedor de romanización - Google (Predeterminado) - Dicionario local / sen conexión - Compatible con OpenAI (Personalizado) - Sección de IA - Só se usa cando o provedor non envía etiquetas de sección como Estribillo ou Verso, que o seu modelo compatible con OpenAI detecta - AI & Puntos final da nube - Configure as claves API, modelos, puntos final e plantillas de prompts para os provedores de AI. - URL do endpoint da API (ex. https://api.openai.com/v1/chat/completions) Modelo de Prompt Marcadores de posición: {language}, {task}, {title}, {artist}, {count}, {lines} Prompt gardado Indicación gardada, pero o endpoint da IA aínda non é utilizable. Configurao primeiro en Ajustes de IA Prompt restablecido - JSON non válido. Corrixao ou limpe o campo antes de gardar Configuracións de tradución & romanización gardadas Configuracións restablecidas aos valores predeterminados A URL base debe comezar por http:// ou https:// - Probar conexión - Configurar endpoints - Amosar todos os endpoints Validando conexión… - Proveedor de tradución - Proveedor de romanización - Obter modelos Obtendo modelos… Seleccionar un modelo Erro ao obter os modelos. Comprobe a clave API ou a conexión. - Ajustes da API de Gemini - Compatible con OpenAI (Endpoint personalizado) - Ajustes da API de DeepL - Clave API Modelo URL base Clave API (opcional) - Prompt de tradución Corpo de solicitude extra (JSON opcional) Clave de autenticación (Free ou Pro) - Configurar claves API, modelos, endpoints e prompts 8.23.51 - Desactivar iconas en negrita 9.13.51 - Restaurar pestanas de A continuación, Letras e Relacionados do reprodutor diff --git a/patches/src/main/resources/addresources/values-hi-rIN/music/strings.xml b/patches/src/main/resources/addresources/values-hi-rIN/music/strings.xml index cf13d66e29..1d89a16c5f 100644 --- a/patches/src/main/resources/addresources/values-hi-rIN/music/strings.xml +++ b/patches/src/main/resources/addresources/values-hi-rIN/music/strings.xml @@ -462,12 +462,47 @@ SABR प्लेबैक (स्पूफ वीडियो स्ट्र प्रदर्शन सिंक्रनाइज़ेशन पैनल - AI एंडपॉइंट सभी प्रदाता अक्षम हैं - इनबिल्ट बोल और एम्बेडेड बोल प्रदाता क्रम रीसेट करें? प्रदाताओं को डिफ़ॉल्ट प्राथमिकता क्रम पर पुनर्स्थापित करें। + राइम शब्द सूची + "Rime Wordlist को UniDict की भी आवश्यकता है: Offline Dictionary provider में, \"Chinese and Cantonese characters (UniDict)\" को टिक करें और पहले यह डाउनलोड होना पूरा होने दें। इसके बिना, वर्ण पढ़ने वाला सूची कोई उच्चारण नहीं देती। + +उस सूचियों को टिक करें जिन्हें Rime Wordlist को पढ़ना चाहिए, फिर उन्हें डाउनलोड करने के लिए Save पर टैप करें। आपने जो सूची जोड़ी है वह उसे अनटिक करने पर हटा दी जाती है। + +प्रत्येक सूची आपके शब्द-से-उच्चारण तालिका होती है, एक .dict.yaml जिसमें लाइन्स ऐसी होती हैं: +字 ham +典 din2 +पहली कॉलम शब्द है, दूसरा उसका उच्चारण; अतिरिक्त कॉलम और एक टैब के बिना लाइन नजरअंदाज कर दिए जाते हैं।" + ऊपर दी गई भाषा के लिए कोई राइम शब्द सूची टिक नहीं की गई है। एक को टिक करने के लिए इस नाम पर टैप करें, या ऐसी भाषा चुनें जिसके पास सूची हो + जोड़ें + राइम शब्द सूची जोड़ें + एक .dict.yaml फाइल, या एक zip जिसमें एक होती है, जिसमें 字 ham और 典 din2 जैसी लाइन्स होती हैं। सूची को उस समय प्राप्त किया जाता है जब आप उसे टिक करते हैं और Save पर टैप करते हैं, और इसकी अनुज्ञप्ति आपके पास जांचने के लिए है + भाषा कोड + दो या तीन अक्षर, जैसे hak या wuu + नाम + उच्चारण के नीचे दिखाया जाता है, जैसे Hakka (Meixian) + शब्द सूची पता + .dict.yaml या उसके zip का https:// पता + शब्द में नहीं होने वाले अक्षर + स्वचालित + कैंटोनी अक्षर + मंडरिन अक्षर + कोई अक्षर उच्चारण नहीं + कोड, नाम और पता भरें + कोड दो या तीन अक्षर का होना चाहिए, जैसे hak + पता https:// से शुरू होना चाहिए + नाम और पते में ऊर्ध्वाधर बार होना चाहिए + %1$s का पहले से एक शब्द सारणी है + %1$s को इनबिल्ट रोमाइज़र द्वारा पढ़ा जाता है और इसमें शब्द सूची नहीं ले सकता + उपयोग नहीं किया जाता: इस भाषा का अपना उच्चारण पहले से है, इसलिए इसे हटाने के लिए इसे अनटिक करें + %1$s जोड़ा गया - इसे डाउनलोड करने के लिए टिक करें और \'सहेजें\' पर टैप करें + शब्द सूची तैयार + शब्द सूची तैयार (%1$d प्रविष्टियाँ) + शब्द सूची पढ़ रही है: %1$d%% + शब्द सूची डाउनलोड विफल - पुनः प्रयास करने के लिए \'सहेजें\' पर दोबारा टैप करें स्टोर की गई लिरिक्स हटाएं - इस डिवाइस पर संरक्षित लिरिक्स को हटाएं + इस उपकरण पर संग्रहीत लिरिक्स के %1$s MB कैश हटाएं %1$s MB साफ़ किया गया टोकन प्राप्त करें एप्पल म्यूज़िक मीडिया-उपयोगकर्ता-टोकन @@ -522,14 +557,22 @@ SABR प्लेबैक (स्पूफ वीडियो स्ट्र कॉपी बटन दिखाएं मूल लिरिक्स को सादे पाठ के रूप में कॉपी करने के लिए टैप करें; डाउनलोड में कच्चे लिरिक्स स्रोत फाइल को सहेजने के लिए लॉन्ग-प्रैस करें अनुवाद बटन दिखाएं - अनुवाद को टॉगल करने के लिए टैप करें; केवल अनुवाद के लिए लॉन्ग-प्रैस करें। प्रदाता अनुवाद, Google अनुवाद, या OpenAI-संगत API का उपयोग करता है - रोमनाइज़ बटन दिखाएँ - रोमनीकरण को टॉगल करने के लिए टैप करें; केवल रोमनीकरण के लिए लॉन्ग-प्रैस करें। प्रदाता रोमनीकरण, Google अनुवाद, या OpenAI-संगत API का उपयोग करता है - स्थानीय रोमनीकरण और एलाइनमेंट - जब प्रदाता, आपका मॉडल और Google अनुवाद असफल हो जाते हैं तो डिवाइस पर रोमनीकरण करें। पढ़ना timed लिरिक्स के हर शब्द का अनुसरण करती है; वर्ड सिंक चालू होना चाहिए। चीनी, कैंटोनिज़, जापानी, कोरियाई, ग्रीक, रूसी, जॉर्जियन, आर्मिनियन; लगभग 120 MB - पठन सामग्री %1$s: %2$d%% + अनुवाद टॉगल करने के लिए टैप करें; केवल अनुवाद के लिए लंबे समय तक दबाएं। सक्षम अनुवाद प्रदाताओं का उपयोग उन की सूचीबद्ध क्रम में करता है + रोमाइज़ या ट्रान्सलिटरेशन बटन दिखाएं + रोमाइजेज़न या ट्रान्सलिटरेशन टॉगल करने के लिए टैप करें; केवल रोमाइज़ेशन या ट्रान्सलिटरेशन के लिए लंबे समय तक दबाएं। सक्षम रोमाइज़ेशन प्रदाताओं का उपयोग उन की सूचीबद्ध क्रम में करता है + ट्रान्सलिटरेशन सक्षम करें + लिरिक्स स्रोत से गैर-लैटिन उच्चारण की अनुमति दें; जब बंद, तो केवल लैटिन अक्षर, एक्सেন্টेड अक्षर और टोन अंकों को रखें + एक टाइम्ड लिरिक्स के प्रत्येक शब्द को पढ़ता है (शब्द सिंक चालू); कोरियाई के लिए कोई डाउनलोड आवश्यक नहीं; चौबीस स्क्रीप्ट्स एक CLDR डाउनलोड साझा करते हैं; चीनी अक्षरों को राइम शब्द सूची द्वारा पढ़ा जाता है जब इसकी शब्द सारणी भाषा को कवर करती है + उच्चारण शब्दकोष डाउनलोड हो रहे हैं… पढ़ने तैयार (%1$d एंट्री) डाउनलोड विफल - पुनः प्रयास करने के लिए इसे बंद करें और फिर से चालू करें + ऑफ़लाइन शब्दकोष + जापानी (SudachiDict, ~75 MB) + चीनी और कैंटोनी अक्षर (UniDict, ~8 MB) + चीनी शब्द (pinyin-pro, ~17 MB) + यूनानी, स्लाविक सिरिलिक (इसे शामिल है, रूसी, यूक्रेनियाई, बुल्गारियन, सर्बियाई, बेलाarusian, मैसेडोनियन), ज्यॉर्जियन, आर्मनियाई, हिब्रू, अरबी, अहमारिक और सीरियाई ट्रान्सलिटरेशन (CLDR, ~0.5 MB) + पठन सामग्री %1$s: %2$d%% + कम से कम एक शब्दकोष चुनें रिफ़्रेश बटन दिखाएँ आगे के बोल उम्मीदवार पर जाने के लिए टैप करें; गाने के नाम और कलाकार से बोल खोजने के लिए देर तक दबाएं लिरिक्स खोजें @@ -540,8 +583,10 @@ SABR प्लेबैक (स्पूफ वीडियो स्ट्र सहेजा गया सिंक किए गए शब्दों या अक्षरों को हाइलाइट करें जब उपलब्ध हों तो शब्द-दर-शब्द गीतों को प्राथमिकता दें। जब गीतों में शब्द टाइमिंग होती है, तो संगीत के साथ समय पर प्रत्येक शब्द या अक्षर को हाइलाइट करता है - पृष्ठभूमि गायन को अलग से दिखाएं - कोष्ठक में लिखी पृष्ठभूमि वोकल, जैसे \"(ओह) मैं तुम चाहता हूँ\", को गाए गए लाइन से बाहर निकालकर अपनी खुद की धीमी लाइन में स्थानांतरित करता है + निष्क्रिय रेखाओं को संकुचित करें + वर्तमान में गाए जाने वाली रेखा को छोड़कर प्रत्येक रेखा को थोड़ा छोटा खींचता है, धुंधला करने के साथ ही + पृष्ठभूमि वोकल्स अलग से दिखाएं + पृष्ठभूमि वोकल्स और सुर को नेतृत्व वाली रेखा से अलग करता है। ब्रैकेट में लिखी गई पृष्ठभूमि वोकल, जैसे \"(Oh) I want you\", को गाए गए रेखा से निकालकर अपने खुद के धुंधली रेखा में ले जाता है बजाए गए गीत छिपाएँ वर्तमान में चल रही पंक्ति के ऊपर की गीत पंक्तियों को छिपाएँ नहीं बजाए गए गीत छिपाएँ @@ -599,10 +644,15 @@ SABR प्लेबैक (स्पूफ वीडियो स्ट्र %s द्वारा अनुवादित %s द्वारा रोमनकृत रोमनाइज़ किया: %s + राइम द्वारा रोमाइज़ed (%s) अनुवादित अनुवाद भाषा - रोमनाइज़ भाषा + रोमाइज़ेशन या ट्रान्सलिटरेशन भाषा + "अनुवाद लक्ष्य भाषा: +%s" + "Romanization या transliteration लक्ष्य भाषा: +%s" अनुवाद किया जा रहा है… गीत का अनुवाद नहीं कर सका रोमनीकृत @@ -616,7 +666,7 @@ SABR प्लेबैक (स्पूफ वीडियो स्ट्र रिफ़्रेश हो रहा है रीफ़्रेश किया गया QuickLRC API कुंजी - QuickLRC वर्तमान ट्रैक के बोलों का प्रतिलेखन या समयानुपातीकरण करता है। यह एक अलग सेवा है, जो ऊपर बताए गए ओपनएआई-संगत एंडपॉइंट से असंबंधित है + QuickLRC वर्तमान ट्रीक के लिए लिरिक्स को ट्रांसक्राइब या टाइम-अलाइन करता है QuickLRC कुली सहेजी गई। बोल खोज संवाद में संरेखण बटन का उपयोग करें, या किसी ट्रैक के बोल न होने पर पैनल प्रॉम्प्ट का उपयोग करें QuickLRC API कुंजी सहेजी गई QuickLRC ने उस कुंजी को स्वीकार नहीं किया @@ -632,56 +682,50 @@ SABR प्लेबैक (स्पूफ वीडियो स्ट्र संरेखण के लिए कोई बोल नहीं - इसके बजाय प्रतिलेखन किया जा रहा है QuickLRC इस ट्रैक को संसाधित नहीं कर सका इस ट्रैक के लिए QuickLRC क्रेडिट्स समाप्त हो चुके हैं - "लिरिक्स शीर्षक और कलाकार द्वारा मेल खाते हैं, असाधारण मेटाडेटा कुछ भी या गलत संस्करण ढूंढ सकता है। संस्करण बदलने के लिए रीफ्रेश का उपयोग करें। * चिह्नित प्रदाताओं को टोकन की आवश्यकता होती है। आपके क्रम में सभी सक्षम प्रदाता हमेशा अनुरोध नहीं किए जाते हैं, लेकिन अपनी गति के आधार पर प्रतिक्रिया देते हैं।\n\nलिरिक्स ऐसे तीसरे-पक्ष सेवाओं से आते हैं जो Morphe से affiliated, sponsored, या endorsed नहीं हैं। प्रत्येक का उपयोग वैसा ही किया जाता है जैसे वह है: लिरिक्स, उनकी उपलब्धता, और उनकी लाइसेंसिंग उन सेवाओं और उनके लाइसेंस धारकों के पास ही रहती है, और उनका उपयोग उनके सेवा शर्तों के अधीन हो सकता है। एक सक्षम सेवा तक पहुंचने वाली वस्तु केवल वह lookup है जिसके लिए आपने पूछा है - शीर्षक, कलाकार, और, जहां उस सेवा को इसकी आवश्यकता हो, वीडियो ID। वैकल्पिक AI endpoint और QuickLRC transcription केवल आप उन्हें configure करने के बाद चलते हैं, और लिरिक्स टेक्स्ट या ट्रैक के वीडियो URL प्राप्त करते हैं।" + "गाने के बोल शीर्षक और कलाकार द्वारा मिलाए जाते हैं, असामान्य metadata कुछ नहीं या गलत संस्करण ढूंढ सकता है। संस्करण बदलने के लिए refresh का उपयोग करें। * चिह्नित प्रदाताओं को एक token की आवश्यकता होती है। सभी सक्षम प्रदाताओं को हमेशा आपके क्रम में अनुरोध नहीं किया जाता है, लेकिन वे अपनी गति के आधार पर प्रतिक्रिया देते हैं। + +गाने के बोल तीसरी-पक्ष सेवाओं से आते हैं जो Morphe से संबंधित, स्폰्सर्ड, या समर्थित नहीं हैं। प्रत्येक को वैसे ही उपयोग किया जाता है: गाने के बोल, उनकी उपलब्धता, और उनकी अनुज्ञप्ति उन सेवाओं और उनके अनुज्ञापकों के साथ रहती है, और उनका उपयोग उनके सेवा शर्तों के अधीन हो सकता है। जो सक्षम सेवा को पहुंचता है वह केवल वह lookup है जिसके लिए आपने पूछा था - शीर्षक, कलाकार, और, जहां उस सेवा को यह चाहिए, वीडियो ID। वैकल्पिक AI endpoint और QuickLRC transcription केवल आपको उन्हें विन्यास करने के बाद चलते हैं, और गाने के बोल टेक्स्ट या ट्रैक का वीडियो URL प्राप्त करते हैं।" केवल उन प्रदाताओं को सक्षम करें जिन्हें आप इस्तेमाल करते हैं, या डिफ़ॉल्ट को बनाए रखें; लगातार अनुरोध छोटे कम्युनिटी लिरिक्स साइट्स को ओवरव्हेल्म कर सकते हैं क्रेडिट लाइन छिपाएँ गीतों के नीचे दिखाई जाने वाली क्रेडिट लाइन छिपाएँ अनुवाद और रोमनीकरण आपस में बदलें - चालू होने पर रोमनीकरण गीतों के नीचे और अनुवाद ऊपर रखता है; बंद होने पर रोमनीकरण ऊपर और अनुवाद नीचे रहता है - अनुवाद - रोमनाइज़ेशन - अनुवाद प्रदाता - Google (डिफ़ॉल्ट, मुफ़्त) - OpenAI संगत (कस्टम) - रोमनाइज़ेशन प्रदाता - Google (डिफ़ॉल्ट) - स्थानीय / ऑफ़लाइन डिक्शनरी - OpenAI संगत (कस्टम) - AI सेक्शन - केवल तब उपयोग किया जाता है जब प्रदाता कोरस या वर्स जैसे कोई सेक्शन लेबल नहीं भेजता है, जिसे आपका OpenAI-संगत मॉडल फिर डिटेक्ट करता है - AI & क्लाउड एंडपॉइंट्स - AI प्रदाताओं के लिए API कुंजियाँ, मॉडल, एंडपॉइंट्स और प्रॉम्प्ट टेम्पलेट्स कॉन्फ़िगर करें। - API एंडपॉइंट URL (जैसे https://api.openai.com/v1/chat/completions) + ON रोमाइज़ेशन या ट्रान्सलिटरेशन को लिरिक्स के नीचे और अनुवाद ऊपर रखता है; OFF रोमाइज़ेशन या ट्रान्सलिटरेशन को ऊपर और अनुवाद को नीचे रखता है। प्रत्येक सिल्बिल पर रखा गया उच्चारण उसके साथ चलता है, अपने सिल्बिल से नीचे से ऊपर + हमेशा लिरिक्स को केंद्रित रखें + ON प्रत्येक रेखा को मध्य में रखता है और स्रोत अनुपालन को नजरअंदाज़ करता है, ड्यूलेट पक्षों सहित; OFF लिरिक्स द्वारा मांगे गए अनुपालन को रखता है + कम से कम एक प्रदाता चालू रहना चाहिए + अनुवाद + रोमाइज़ेशन या ट्रान्सलिटरेशन + ऑफ़लाइन शब्दकोष + राइम शब्द सूची + AI के साथ अनुभागों का पता लगाएं + केवल तभी उपयोग किया जाता है जब प्रदाता किसी अनुभाग लेबल जैसे कि कोरस या वर्स नहीं भेजता, जिसका आपका OpenAI-अनुकूल मॉडेल तब पता लगाता है। पंक्ति को संपादित करना भी इसके प्रॉम्प्ट को खोलता है + API endpoint URL (उदाहरण के लिए https://api.openai.com/v1/chat/completions, https://api.anthropic.com/v1/chat/completions, https://generativelanguage.googleapis.com/v1beta/openai/chat/completions) प्रॉम्प्ट टेम्प्लेट प्लेसहोल्डर्स: {language}, {task}, {title}, {artist}, {count}, {lines} प्रॉम्प्ट सेव किया गया प्रॉम्प्ट सहेजा गया, लेकिन AI एंडपॉइंट अभी उपयोग योग्य नहीं है। पहले AI सेटिंग्स में इसका कॉन्फ़िगरेशन करें प्रॉम्प्ट रिसेट किया गया - वैध JSON नहीं। इसे ठीक करें या save करने से पहले क्षेत्र को साफ करें अनुवाद & रोमीकरण सेटिंग्स सहेज ली गईं सेटिंग्स डिफ़ॉल्ट पर रीसेट हो गईं बेस URL http:// या https:// से शुरू होना चाहिए - कनेक्शन टेस्ट करें - एंडपॉइंट्स कॉन्फ़िगर करें - सभी एंडपॉइंट्स दिखाएं + सेटिंग्स अपूर्ण हैं या प्रदाता द्वारा अस्वीकृत कनेक्शन सत्यापित किया जा रहा है… - अनुवाद प्रदाता - रोमीकरण प्रदाता - मॉडल प्राप्त करें + %1$s: जुड़ा हुआ! (%2$s) + %1$s: कनेक्शन विफल। सेटिंग्स की जाँच करें। + सबसे पहले API कुंजी दर्ज करें मॉडल लाए जा रहे हैं… एक मॉडल चुनें मॉडल लाने में विफल। API कुंजी या कनेक्शन की जाँच करें। - Gemini API सेटिंग्स - OpenAI संगत (कस्टम एंडपॉइंट) - DeepL API सेटिंग्स - API कुंजी + प्रॉम्प्ट मॉडल बेस URL + सेवा चुनें API कुंजी (वैकल्पिक) - अनुवाद प्रॉम्प्ट अतिरिक्त अनुरोध बॉडी (वैकल्पिक JSON) + अतिरिक्त अनुरोध हेडर्स (वैकल्पिक JSON) + नाम और मानों को उद्धरण करें, उदाहरण {\"api-key\": \"abc\"}. एक अधिरोहित मान / : या एक संख्या शामिल है तो अस्वीकृत हो जाता है। एक खाली मान हेडर को हटा देता है। + नाम और मानों को उद्धरण करें, उदाहरण {\"temperature\": 0.3}। एक अधिरोहित मान / या : शामिल है तो अस्वीकृत हो जाता है। मॉडल और संदेश यहां सेट नहीं किए जा सकते। प्रमाणीकरण कुंजी (Free या Pro) - API कुंजियों, मॉडल, एंडपॉइंट्स और प्रॉम्प्ट्स को कॉन्फ़िगर करें 8.23.51 - बोल्ड आइकन अक्षम करें 9.13.51 - प्लेयर के अप नेक्स्ट, गीत और संबंधित टैब को पुनर्स्थापित करें diff --git a/patches/src/main/resources/addresources/values-hi-rIN/youtube/strings.xml b/patches/src/main/resources/addresources/values-hi-rIN/youtube/strings.xml index a9542ee322..de362f695c 100644 --- a/patches/src/main/resources/addresources/values-hi-rIN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-hi-rIN/youtube/strings.xml @@ -29,6 +29,8 @@ Second \"item\" text" अन्य वीडियो पुरानी YouTube सेटिंग स्क्रीन वापस लाएँ + YouTube स्नैकबार में संदेश दिखाएं + प्लेयर बटन की पुष्टि के रूप में मोर्फे संदेश पॉपअप के बजाय YouTube स्नैकबार में दिखाए जाते हैं बैकग्राउंड प्ले से पाबंदियाँ हटाएँ बैकग्राउंड प्ले बंद करें diff --git a/patches/src/main/resources/addresources/values-hr-rHR/music/strings.xml b/patches/src/main/resources/addresources/values-hr-rHR/music/strings.xml index ea13580b45..9c9231b2be 100644 --- a/patches/src/main/resources/addresources/values-hr-rHR/music/strings.xml +++ b/patches/src/main/resources/addresources/values-hr-rHR/music/strings.xml @@ -462,12 +462,47 @@ Kako biste osigurali sinkronizaciju scrobblinga tijekom reprodukcije u pozadini, Prikaz Sinkronizacija Panel - Krajnja točka AI-ja Svi pružatelji usluga onemogućeni — ugrađeni tekstovi pjesama i tekstovi ugrađeni u datoteku Ponovno postaviti redoslijed pružatelja? Vrati pružatelje na zadani redoslijed prioriteta + Rime rječnik + "Popis riječi Rime također zahtijeva UniDict: u pružatelju usluge offline rječnika, označite \"Kineska kantonska pisma (UniDict)\" i pričekajte da se završi preuzimanje. Bez toga, popis koji čita pisma neće davati izgovor. + +Označite popise koje želite da čita Popis riječi Rime, a zatim dodirnite Spremi za preuzimanje. Popis koji ste dodali uklanja se kada ga isključite. + +Svaki popis je vaša vlastita tablica od riječi do izgovora, datoteka .dict.yaml s redakima poput: +字 ham +典 din2 +Prva stupac je riječ, druga njen izgovor; dodatni stupci i retci bez kartice se zanemaruju." + Nema odabrane Rime liste za gornji jezik. Dodirnite ime da biste odabrali ili odaberite jezik koji ima listu + Dodaj + Dodaj Rime rječnik + Datoteka .dict.yaml ili zip koji sadrži jednu, s redakima poput 字 ham i 典 din2. Popis se preuzima kada ga označite i dodirnete Spremi, a njegova licenca je vaša za provjeru + Kôd jezika + Dvije ili tri slova, kao hak ili wuu + Naziv + Prikazano ispod čitanja, kao Hakka (Meixian) + Adresa liste + https:// adresa .dict.yaml ili njezin zip + Znakovi izvan riječi + Automatski + Kantonijski znakovi + Mandarinški znakovi + Bez čitanja znaka + Unesite kôd, naziv i adresu + Kôd mora biti dvije ili tri slova, kao hak + Adresa mora početi s https:// + Naziv i adresa ne mogu sadržavati vertikalnu crtu + %1$s već ima tabelu riječi + %1$s čita ugrađeni romanizer i ne može uzeti listu riječi + Nije korišteno: ovaj jezik već ima vlastito čitanje, stoga ga odznačite da biste ga uklonili + Dodano %1$s - označite ga i dodirnite Spremi za preuzimanje + Lista riječi spremna + Lista riječi spremna (%1$d stavke) + Čitanje liste: %1$d%% + Preuzimanje liste nije uspjelo - ponovno dodirnite Spremi za pokušaj Obriši pohranjene tekstove pjesama - Izbriši tekstove pjesama pohranjene na ovom uređaju + Izbrišite %1$s MB pjesama koje su sačuvane na ovom uređaju Očišćeno %1$s MB Pribavi token Apple Music Media-user-token @@ -527,14 +562,22 @@ Potrebni kolačići: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPI Prikaži tipku za kopiranje Dodirnite za kopiranje izvornih tekstova pjesama kao običnog teksta; pritisnite dugo za spremanje izvorne datoteke s tekstovima pjesama u Preuzimanja Prikaži tipku za prijevod - Dodirnite za uključivanje/isključivanje prijevoda; pritisnite dugo samo za prijevod. Koristi prijevode davatelja usluga, Google Translate ili OpenAI kompatibilni API - Prikaži gumb za romanizaciju - Dodirnite za uključivanje/isključivanje romanizacije; pritisnite dugo samo za romanizaciju. Koristi romanizaciju davatelja usluga, Google Translate ili OpenAI kompatibilni API - Lokalna romanizacija i poravnanje - Romanizirajte na uređaju kada davatelj usluga, vaš model i Google Translate zakažu. Čitanje prati svaku riječ vremenski sinkroniziranog teksta pjesme; sinkronizacija riječi mora biti uključena. Kineski, kantonski, japanski, korejski, grčki, ruski, gruzijski, armenski; oko 120 MB - Čitanje %1$s: %2$d%% + Dodirnite za promjenu prijevoda; dugo pritisnite za samo prijevod. Koristi omogućene pružatelje prijevoda po redoslijedu + Prikaži gumb za romanizaciju ili transkripciju + Dodirnite za promjenu romanizacije ili transkripcije; dugo pritisnite za samo romanizaciju ili transkripciju. Koristi omogućene pružatelje romanizacije po redoslijedu + Omogući transkripciju + Dozvoli nestandardna čitanja izvora pjesme; kada je off, zadrži samo latinska slova, akcentirana slova i tonove + Čita svaku riječ vremenske pjesme (sinkronizacija riječi uključena); Korejski ne zahtijeva preuzimanje; četrnaest scripta dijeli jedno CLDR preuzimanje; Han znakove čita Rime Wordlist kada njegova tabela riječi pokriva jezik + Downloading reading dictionaries… Čitanja spremna (%1$d unosa) Preuzimanje nije uspjelo - isključite i ponovno uključite kako biste ponovili pokušaj + Offline rječnik + Japanski (SudachiDict, ~75 MB) + Kineski i kantonijski znakovi (UniDict, ~8 MB) + Kineske riječi (pinyin-pro, ~17 MB) + Grčki, slavenski ćirilica (uključujući ruski, ukrajinski, bugarski, srpski, bijelogorski, makedonski), gruzijski, armenski, hebrejski, arapski, amharaški i sirski transliteracija (CLDR, ~0,5 MB) + Čitanje %1$s: %2$d%% + Odaberite barem jedan rječnik Prikaži gumb za osvježavanje Dodirnite za prijelaz na sljedećeg kandidata s tekstom pjesme; pritisnite dugo za pretraživanje tekstova pjesama po nazivu pjesme i izvođaču Pretraži tekst pjesme @@ -545,8 +588,10 @@ Potrebni kolačići: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPI Spremljeno Istakni sinkronizirane riječi ili slogove Daj prednost tekstovima pjesama riječ po riječ kada su dostupni. Kada tekstovi nose vremenske oznake riječi, istakni svaku riječ ili slog u ritmu s glazbom. - Prikažite pozadinske vokale odvojeno - Premješta vokale pozadine upisane u zagradama, poput \"(Oh) I want you\", iz pjevne dionice u vlastitu liniju manjeg intenziteta + Smanjenje neaktivnih linija + Prikazuje svaku liniju koja trenutno nije pjevana malo manju, na vrhu tamnijom + Prikaži pozadinske vokale zasebno + Separates background vocals and harmony from the lead line. Moves a background vocal written in brackets, such as \"(Oh) I want you\", out of the sung line into a dimmer line of its own Sakrij reproducirane tekstove pjesama Sakrij retke teksta pjesme iznad trenutačno reproduciranog retka. Sakrij nereproducirane tekstove pjesama @@ -604,10 +649,15 @@ Primjenjuje se na oba limita. Pri 1,75, četiri znaka pune širine računaju se Prijevod: %s Romanizirano: %s Romanizirao/romanizirala %s + Romanizirao Rime (%s) Prijevod Jezik prijevoda - Jezična opcija romanizacije + Jezik romanizacije ili transkripcije + "Ciljni jezik prijevoda: +%s" + "Ciljni jezik romanizacije ili transkripcije: +%s" Prijevod u tijeku… Prijevod stihova nije uspio Romanizacija @@ -621,7 +671,7 @@ Primjenjuje se na oba limita. Pri 1,75, četiri znaka pune širine računaju se Osvježavanje… Osvježeno Ključ API-ja QuickLRC - QuickLRC transkribira ili vremenski poravnava tekstove pjesama za trenutnu skladbu. Posebna usluga koja nije povezana s gore navedenim OpenAI-sklonim kutom + QuickLRC transkribira ili vremenski poravnava pjesmu za trenutnu tračku Ključ QuickLRC spremljen. Koristite gumb za poravnavanje u dijalogu pretraživanja tekstova pjesama ili upitu panela kada skladba nema tekstove pjesama Ključ API-ja QuickLRC spremljen QuickLRC nije prihvatio taj ključ @@ -637,56 +687,50 @@ Primjenjuje se na oba limita. Pri 1,75, četiri znaka pune širine računaju se Nema tekstova pjesama za poravnanje - umjesto toga transkribira QuickLRC nije mogao obraditi ovu pjesmu Krediti QuickLRC-a su potrošeni za ovu pjesmu - "Tekstovi pjesama se podudaraju prema nazivu i izvođaču, nenavadni metapodataci mogu pronaći ništa ili pogrešnu verziju. Koristite osvježavanje za mijenjanje verzija. Davatelji usluga označeni * zahtijevaju token. Nisu svi omogućeni davatelji usluga uvijek upitani redoslijedom, već odgovaraju prema brzini.\n\nTekstovi pjesama dolaze od trećih strana koji nisu povezani, sponzorirani niti podržani od strane Morphe-a. Svaki se koristi onako kakav jest: tekstovi pjesama, njihova dostupnost i licenciranje ostaju kod tih servisa i njihovih vlasnika licenci, a korištenje može podlijegati njihovim uvjetima korištenja. Ono što stiže do omogučenog servisa je samo pretraga koju ste tražili - naziv, izvođač i, gdje to servis treba, ID videa. Opcionalna AI točka dodira i brza LRC transkripcija rade samo nakon što ih konfigurirate, i primaju tekst pjesme ili URL videa pjesme." + "Tekstovi pjesama podudaraju se prema naslovu i izvođaču; neobične metapodatke mogu ništa pronaći ili pogrešnu verziju. Koristite osvježiti za zamjenu verzija. Pružatelji označeni * zahtijevaju token. Nisu svi omogućeni pružatelji uvijek traženi redoslijedom, već odgovaraju na temelju njihove brzine. + +Tekstovi pjesama dolaze od usluga trećih strana koje nisu povezane, sponsorirane niti podržane od strane Morphe-a. Svaka se koristi takva kakva jest: tekstovi pjesama, njihova dostupnost i licenciranje ostaju kod tih usluga i njihovih davatelja licenci, a korištenje može biti podložno uvjetima uporabe. Ono što stiže do omoguene usluge je samo upit koji ste zatražili - naslov, izvođač i, gdje ta usluga treba, ID videa. Dodatna krajnja točka AI i transkripcija QuickLRC pokreće se samo nakon što ih konfigurirate i primaju tekst pjesme ili URL videa zapisa." Omogućite samo pružatelje koje koristite ili zadržite zadane — stalni zahtjevi mogu preplaviti male web-mjestima s tekstovima zajednice Sakrij redak sa zaslugama Sakrij redak sa zaslugama prikazan ispod tekstova pjesama. Zamijeni prijevod i romanizaciju - ON postavlja romanizaciju ispod tekstova pjesama, a prijevod iznad; OFF ostavlja romanizaciju iznad, a prijevod ispod. - Prijevod - Romanizacija - Davatelj prijevoda - Google (Zadano, besplatno) - OpenAI kompatibilno (Prilagođeno) - Davatelj romanizacije - Google (Zadano) - Lokalni / Offline rječnik - OpenAI kompatibilno (Prilagođeno) - AI odjeljak - Koristi se samo kada pružatelj ne šalje oznake odjeljaka kao što su Refren ili Strofa, koje zatim detektira vaš OpenAI-kompatibilni model - AI & Cloud krajnje točke - Konfigurirajte API ključeve, modele, krajnje točke i predloške upita za AI davatelje. - URL API krajnje točke (npr. https://api.openai.com/v1/chat/completions) + ON stavlja romanizaciju ili transkripciju ispod pjesme i prijevod iznad; OFF zadržava romanizaciju ili transkripciju iznad i prijevod ispod. Čitanje postavljeno preko svake sloga pomiče se s njom, od iznad sloga do ispod nje + Uvijek centriraj pjesmu + ON stavlja svaku liniju u sredinu i ignoriše poravnanje izvora, uključujući strane dueta; OFF zadržava poravnanje koje traži pjesma + Barem jedan pružatelj mora ostati uključen + Prijevod + Romanizacija ili transkripcija + Offline rječnik + Rime rječnik + Detektiraj sekcije s AI + Koristi se samo kada pružatelj nema oznake sekcija poput Refrena ili Strofa, što onda detektira vaš OpenAI kompatibilan model. Uređivanje reda otvara i njegov prompt + URL API endpointa (npr. https://api.openai.com/v1/chat/completions, https://api.anthropic.com/v1/chat/completions, https://generativelanguage.googleapis.com/v1beta/openai/chat/completions) Predložak prompta Zamjenici: {language}, {task}, {title}, {artist}, {count}, {lines} Prompt spremljen Upit spremljen, ali AI točka još nije uporabljiva. Prvo je konfigurirajte u postavkama AI-a Prompt poništen - Nije valjani JSON. Popravite ga ili očistite polje prije spremanja Postavke prijevoda & romanizacije spremljene Postavke vraćene na zadane vrijednosti Osnovni URL mora započinjati s http:// ili https:// - Testiraj vezu - Konfiguriraj krajnje točke - Prikaži sve krajnje točke + Postavke nisu dovršene ili odbijene od strane pružatelja Provjera veze… - Davatelj prijevoda - Davatelj romanizacije - Preuzmi modele + %1$s: Povezano! (%2$s) + %1$s: Spojiti nije uspjelo. Provjerite postavke. + Unesite ključ API-ja prvo Preuzimanje modela… Odaberite model Preuzimanje modela nije uspjelo. Provjerite API ključ ili vezu. - Postavke Gemini API-ja - OpenAI kompatibilno (prilagođena krajnja točka) - Postavke DeepL API-ja - API ključ + Prompt Model Osnovni URL + Odaberite uslugu API ključ (opcionalno) - Upute za prijevod Dodatno tijelo zahtjeva (opcionalni JSON) + Dodatni zahtjevi zaglavlja (opcionalni JSON) + Quote names and values, e.g. {\"api-key\": \"abc\"}. An unquoted value containing / : or a number is rejected. An empty value removes a header. + Quote names and values, e.g. {\"temperature\": 0.3}. An unquoted value containing / or : is rejected. Model and messages cannot be set here. Ključ za autentifikaciju (Free ili Pro) - Konfigurirajte API ključeve, modele, krajnje točke i upite 8.23.51 - Onemogući podebljane ikone 9.13.51 - Vrati kartice Player Zatim, Stihovi i Povezano diff --git a/patches/src/main/resources/addresources/values-hr-rHR/youtube/strings.xml b/patches/src/main/resources/addresources/values-hr-rHR/youtube/strings.xml index 9fbf152391..51a88d32db 100644 --- a/patches/src/main/resources/addresources/values-hr-rHR/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-hr-rHR/youtube/strings.xml @@ -29,6 +29,8 @@ Second \"item\" text" Razno Video Vrati stare postavke YouTube izbornika + Prikaži poruke u YouTube snackbaru + Poruke Morphea, kao što su potvrde gumba za reprodukciju, prikazuju se u YouTube snackbaru umjesto u skočnom prozoru Ukloni ograničenja reprodukcije u pozadini Onemogući reprodukciju u pozadini diff --git a/patches/src/main/resources/addresources/values-hu-rHU/music/strings.xml b/patches/src/main/resources/addresources/values-hu-rHU/music/strings.xml index 379a1ab749..218b7f7da8 100644 --- a/patches/src/main/resources/addresources/values-hu-rHU/music/strings.xml +++ b/patches/src/main/resources/addresources/values-hu-rHU/music/strings.xml @@ -462,12 +462,47 @@ A háttérlejátszás közbeni szinkronizálás biztosításához győződjön m Megjelenítés Szinkronizálás Panel - AI végpont Az összes szolgáltató letiltva – beépített és beágyazott dalszövegek Visszaállítja a szolgáltatók sorrendjét? Szolgáltatók visszaállítása az alapértelmezett prioritási sorrendre. + Rime szótár + "A Rime szólistahoz szükséges az UniDict is: a Nemzeti szótár biztosítónál pipálja be a \"Kínai és kantoni karakterek (UniDict)\" opciót, és várja meg a letöltés befejezését. Enélkül a karaktereket olvasó lista nem ad hangzást. + +Jelölje ki azokat a listákat, amelyeket a Rime szólistának felolvasnia kell, majd koppintson a Mentés gombra a letöltéshez. Egy hozzáadott lista akkor kerül eltávolításra, amikor kiejti a pipát. + +Minden lista önálló szó-hangzás táblázat, egy .dict.yaml fájl olyan sorokkal: +字 ham +典 din2 +Az első oszlop a szó, a második a hangzása; a további oszlopok és a tabulátor nélküli sorok figyelmen kívül vannak hagyva." + A fenti nyelvhez nincs kijelölve Rime szótárlista. Koppintson erre a névre egy lista kijelöléséhez, vagy válasszon olyan nyelvet, amelyhez rendelkezik listával + Hozzáadás + Rime szótárlista hozzáadása + Egy .dict.yaml fájl, vagy egy zip, amely tartalmaz ilyet, olyan sorokkal, mint a 字 ham és a 典 din2. A listát akkor veszi át, amikor kijelöli és megnyomja a Mentést, és a licenc ellenőrzése az Ön dolga + Nyelvkód + Két vagy három betű, például hak vagy wuu + Név + Megjelenik a kiejtés alatt, például Hakka (Meixian) + Szótár cím + https:// cím a .dict.yaml fájlhoz vagy a zip-hez + A szavakon kívüli karakterek + Automatikus + Kantoni karakterek + Hagyományos mandarin karakterek + Nincs karakterolvasás + Töltse ki a kódot, a nevet és a címet + A kód két vagy három betűből kell álljon, például hak + A címnek https:// kezdetűnek kell lennie + A név és a cím nem tartalmazhat függőleges vonalat + %1$s már rendelkezik szólistával + %1$s-t a beépített átiratoló olvassa, így nem fogad el szótárt + Nem használatos: ennek a nyelvnek már megvan a saját kiejtése, ezért pipálja ki, hogy eltávolítsa + %1$s hozzáadva – pipálja ki és nyomja meg a Mentést a letöltéshez + Szótárak készen állnak + Szótárak készen állnak (%1$d bejegyzéssel) + Szótár építése: %1$d%% + Szótár letöltése sikertelen – próbálja újra a Mentés gombbal Tárolt dalszövegek törlése - A készüléken tárolt dalszövegek törlése + Törölje ezt az eszközön tárolt %1$s MB dalszöveget %1$s MB törölve Token lekérése Apple Music médiafelhasználói token @@ -527,14 +562,22 @@ Szükséges cookie-k: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAP Másolás gomb megjelenítése Érintse meg az eredeti dalszöveg egyszerű szövegként történő másolásához; nyomja meg hosszan a nyers dalszöveg forrásfájának letöltéséhez a Letöltések mappába Fordítás gomb megjelenítése - Érintse meg a fordítás váltásához; nyomja meg hosszan csak a fordításhoz. Szolgáltatói fordításokat, Google Fordítást vagy egy OpenAI-kompatibilis API-t használ - Romanizálás gomb megjelenítése - Érintse meg a románosítás váltásához; nyomja meg hosszan csak a románosításhoz. Szolgáltatói románosítást, Google Fordítást vagy egy OpenAI-kompatibilis API-t használ - Helyi románosítás és igazítás - Románosítás az eszközön, ha a szolgáltató, az Ön modellje és a Google Fordítás nem működik. Az olvasás követi az időzített dalszöveg minden szóát; a szószinkronizálásnak be kell lennie. Kínai, kantoni, japán, koreai, görög, orosz, grúz, örmény; kb. 120 MB - Olvasás %1$s: %2$d%% + Koppintson a fordítás váltásához; hosszú érintés a fordítás kizárólagos megjelenítéséhez. A engedélyezett fordítószolgáltatókat azok felsorolási sorrendjében használja + Átirat vagy ábécésítési gomb megjelenítése + Koppintson az ábécésítés vagy átirat váltásához; hosszú érintés az ábécésítés vagy átirat kizárólagos megjelenítéséhez. Az engedélyezett ábécésítőszolgáltatókat azok felsorolási sorrendjében használja + Átirat engedélyezése + Engedélyezi a nem Latin ábécés írásjelekkel való kiejtések megjelenítését; kikapcsoláskor csak a Latin betűket, az ékezetes betűket és a hangjegy számokat tartja meg + Felolvassa a időzített dalszöveg minden szavát (szó szinkron be van kapcsolva); Koreához nem szükséges letöltés; tizennégy írások egyetlen CLDR letöltéssel osztják meg az ábécésítést; Han karaktereket a Rime szótár olvas fel, ha a szótárában szerepel a nyelv + Olvasószótárak letöltése folyamatban… Kész olvasások (%1$d bejegyzés) Letöltés sikertelen - próbálja újra ezt a funkcióki/bekapcsolással + Offline szótár + Japán (SudachiDict, ~75 MB) + Kínai és kantoni karakterek (UniDict, ~8 MB) + Kínai szavak (pinyin-pro, ~17 MB) + Görög, szláv cirill (beleértve az orosz, ukrán, bolgár, szerb, belorusz, macedon), grúz, örmény, héber, arab, amhara és szír ábécésítés (CLDR, ~0.5 MB) + Olvasás %1$s: %2$d%% + Válasszon legalább egy szótárt Frissítés gomb megjelenítése Érintse meg a következő dalszöveg-jelöltre váltáshoz; nyomja meg hosszan a dalszöveg kereséséhez a dal címe és előadója alapján Dalszöveg keresése @@ -545,8 +588,10 @@ Szükséges cookie-k: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAP Mentett Szinkronizált szavak vagy szótagok kiemelése Előnyben részesíti a szavankénti dalszövegeket, ha elérhetők. Ha a dalszöveg szószintű időzítést tartalmaz, a zene ütemére emeli ki az egyes szavakat vagy szótagokat. - A háttérvokál megjelenítése külön - A zárójelben megadott háttérvokált (például: „(Óh) téged szeretlek”) kiveszi a dalos sorból, és elkülönít egy halkabb, saját sort hoz létre neki. + Tükrözze a nem éneklő sorokat + Minden nem éppen énekelt sort kicsinyebb méretben ábrázol, továbbá halványítja őket + Háttérénekelt külön jelenjen meg + Felszakítja a háttérénekelt és a harmóniát a fő szólamtól. Egy zárójelben szereplő háttérénekelt, például „(Ó) téged szeretek” kifejezést kiválója az énekelt sorból egy külön, halványabb sorba helyezi A lejátszott sor elrejtése Minilejátszó dalszövegének megjelenítése A még el nem hangzott dalszövegek elrejtése @@ -604,10 +649,15 @@ Mindkét limitre vonatkozik. 1,75 esetén négy teljes szélességű karakter 7- Fordította: %s Romanizálta: %s Romanizálta: %s + Ábécésítve a Rime által (%s) Lefordítva Fordítás nyelve - Romanizálási nyelv + Ábécésítés vagy átirat nyelve + "Fordítási célnyelv: +%s" + "Átirat vagy transliteráció célnyelv: +%s" Fordítás… A dalszöveg fordítása sikertelen Romanizált @@ -621,7 +671,7 @@ Mindkét limitre vonatkozik. 1,75 esetén négy teljes szélességű karakter 7- Frissítés Frissítve QuickLRC API kulcs - A QuickLRC leírja vagy időzíti az aktuális dal dalszövegét. Ez egy különálló szolgáltatás, amely nem kapcsolódik a fenti OpenAI-kompatibilis végponthoz + A QuickLRC transzkribálja vagy időzített formába rendezi a jelenlegi zeneszám dalszövegét QuickLRC kulcs elmentve. Használja az illesztés gombot a dalszövegkereső párbeszédablakban, vagy a képernyő feliratát, ha egy számnak nincs dalszövege QuickLRC API kulcs megtartott QuickLRC nem fogadta el azt a kulcsot @@ -637,58 +687,50 @@ Mindkét limitre vonatkozik. 1,75 esetén négy teljes szélességű karakter 7- Nincs dalszöveg az illesztéshez - inkább transzkribálunk QuickLRC nem tudta ezt a számot feldolgozni QuickLRC hiteltények elfogytak erre a számra - "A dalszövegeket cím és előadó alapján illesztjük, a szokatlan metaadatok nulla találatot vagy a rossz verziót adhatják. Használja a frissítést a verziók közötti váltáshoz. A * jelzéssel ellátott szolgáltatók token requireálnak. Nem mindig kerülnek sorrendben kérésre minden engedélyezett szolgáltató, de sebességük alapján válaszolnak. + "A szövegeket címen és előadó alapján párosítjuk, a szokatlan metaadatok miatt semmi vagy a rossz verzió jelenhet meg. Használja a frissítést a verziók váltásához. A * jellel jelölt szolgáltatók tokenhez vannak kötve. Az engedélyezett szolgáltatók mindegyike nem mindig kerül sorrendben kérésre, hanem sebességük alapján válaszolnak. -A dalszövegek harmadik féltől származó szolgáltatásokból származnak, amelyek nincsenek kapcsolatban, nem támogatják és nem endorsingolták a Morphe által. Mindegyiket úgy használjuk ahogy van: a dalszövegek, elérhetőségük és licencelésük a szolgáltatások és licencelőik birtokában marad, és azok szolgáltatási feltételeinek alá vannak vetve. Ami eljut egy engedélyezett szolgáltatáshoz, az csak a lekérdezés amit kértél - a cím, az előadó és ahol a szolgáltatás szükségessége esetén, a videóazonosító. Opcionális AI végpont és QuickLRC transzkripció csak akkor fut, amikor beállítottad őket, és a dalszöveg szöveget vagy a szám videójának URL-jét kapják." +A szövegek harmadik fél szolgáltatásaiból származnak, amelyekkel a Morphe-nak nincs kapcsolata, azokot nem finanszírozza és nem tartja fenn. Minden egyes szolgáltatót úgy használunk, ahogy van: a szöveg, az elérhetőség és a licenszelés továbbra is a szolgáltatásoknál és a licenciált partnereiknél marad, használatuk pedig feltételeihez kötött. Az engedélyezett szolgáltatásokhoz csak a keresést küldjük el – a címet, az előadót és ahol szükséges, a videó ID-t. A opcionális AI végpont és QuickLRC transzkripció csak akkor fut, ha beállította őket, és csak a szövegkönyv vagy a zenei felvétel video URL-címét kapják meg." Csak engedélyezze azokat a szolgáltatókat, amelyeket használ, vagy tartsa meg az alapértelmezettet – a folyamatos kérések túlterhelhetik a kis közösségi dalszövegoldalakokat Forrásmegjelölés elrejtése A dalszöveg alatt látható forrásmegjelölés elrejtése A fordítás és a romanizáció felcserélése - BE: a romanizációt a dalszöveg alá, a fordítást fölé helyezi; KI: a romanizációt felül, a fordítást alul hagyja. - Fordítás - Romanizáció - Fordító szolgáltató - Google (Alapértelmezett, Ingyenes) - OpenAI kompatibilis (Egyedi) - Romanizáló szolgáltató - Google (Alapértelmezett) - Helyi / Offline szótár - OpenAI kompatibilis (Egyedi) - AI szakasz - Csak akkor használja, ha a szolgáltató nem küld szakaszcímkéket, mint például Korus vagy Vers, amelyeket ezután az OpenAI-kompatibilis modell érzékel - AI & felhő végpontok - API kulcsok, modellek, végpontok és prompt sablonok konfigurálása az AI szolgáltatóknak. - API végpont URL (pl. https://api.openai.com/v1/chat/completions) + BEKapcsoláskor az ábécésítés/áiratranszkripció a dalszöveg alá, a fordítás pedig fölé jelenik meg; KAPCSOLATLAN mód megtartja az ábécésítést/fordítást felül és a fordítást alul. Egy szótagra helyezett kiejtése követi azt, attól függően, hogy felette vagy alatta van + Mindig középen legyen a dalszöveg + BE kapcsolt módban minden sort középre helyez, és figyelmen kívül hagyja a forrás rendezését, beleértve a duett oldalakat is; KAPCSOLATLAN módban megtartja a dalszöveg által kért rendezést + Legalább egy szolgáltatónak aktívnak kell maradnia + Fordítás + Ábécésítés vagy átirat + Offline szótár + Rime szótár + Részek detektálása AI segítségével + Csak akkor használatos, ha a szolgáltató nem szállít részcímkéket, mint például Refrén vagy Vers, amelyeket az OpenAI-kompatibilis modelljeDetektál. A sor szerkesztése során megnyílik az adott prompt is + API végpont URL (például https://api.openai.com/v1/chat/completions, https://api.anthropic.com/v1/chat/completions, https://generativelanguage.googleapis.com/v1beta/openai/chat/completions) Prompt sablon Helyőrzők: {language}, {task}, {title}, {artist}, {count}, {lines} Prompt mentve Bejegyzés elmentve, de az AI végpont még nem használható. Először konfigurálja az AI beállításokban Prompt alapértékekre visszaállítva - Nem érvényes JSON. Javítsa ki vagy törölje a mezőt mentés előtt A fordítási & romanizációs beállítások mentve A beállítások visszaállítva az alapértelmezettre A Base URL-nek http:// vagy https:// előtaggal kell kezdődnie - Kapcsolat tesztelése - Végpontok konfigurálása - Összes végpont megjelenítése + A beállítások hiányosak vagy a szolgáltató elutasította azokat Kapcsolat ellenőrzése… - Fordító szolgáltató - Romanizáló szolgáltató - Modellek lekérése + %1$s: Csatlakoztatva! (%2$s) + %1$s: Kapcsolat sikertelen. Ellenőrizze a beállításokat. + Először adja meg az API kulcsot Modellek lekérése… Válasszon egy modellt Sikertetlen modellek lekérése. Ellenorizze az API-kulcsot vagy a kapcsolatot. - Gemini API beállítások - OpenAI kompatibilis (egyedi végpont) - DeepL API beállítások - API-kulcs + Prompt Modell Alap URL + Válasszon egy szolgáltatót API kulcs (opcionális) - Fordítási utasítás Extra kérés törzse (opcionális JSON) + Extra kérelem fejlécek (opcionális JSON) + Idézze a neveket és értékeket, például {\"api-key\": \"abc\"}. Egy idézetlen érték, amely / : vagy számot tartalmaz, elutasításra kerül. Egy üres érték eltávolítja a fejlécet. + Idézze a neveket és értékeket, például {\"temperature\": 0.3}. Egy idézetlen érték, amely / vagy : karaktert tartalmaz, elutasításra kerül. A modellt és az üzeneteket itt nem lehet beállítani. Hitelesítési kulcs (Ingyenes vagy Pro) - API kulcsok, modellek, végpontok és utasítások konfigurálása 8.23.51 - Félkövér ikonok letiltása 9.13.51 - A Lejátszó „Következő”, „Dalszövegek” és „Kapcsolódó” lapok visszaállítása diff --git a/patches/src/main/resources/addresources/values-hu-rHU/youtube/strings.xml b/patches/src/main/resources/addresources/values-hu-rHU/youtube/strings.xml index ef0322c448..f537c63db2 100644 --- a/patches/src/main/resources/addresources/values-hu-rHU/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-hu-rHU/youtube/strings.xml @@ -29,6 +29,8 @@ Second \"item\" text" Egyéb Videó Régi YouTube beállításképernyő visszaállítása + Üzenetek megjelenítése YouTube snackbarban + Morphe üzenetek, például a lejátszó gomb megerősítése, a YouTube snackbarban jelennek meg felugró ablak helyett Háttérben lejátszás korlátozásainak feloldása Háttérben történő lejátszás kikapcsolása diff --git a/patches/src/main/resources/addresources/values-hy-rAM/music/strings.xml b/patches/src/main/resources/addresources/values-hy-rAM/music/strings.xml index f3065be6be..e84504eb0e 100644 --- a/patches/src/main/resources/addresources/values-hy-rAM/music/strings.xml +++ b/patches/src/main/resources/addresources/values-hy-rAM/music/strings.xml @@ -462,12 +462,10 @@ SABR վերարտադրում (Spoof տեսանյութերի հոսքերը ա Ցուցադրում Սինխրոնիզացիա Պանել - AI վերջնակետ Բոլոր մատակարարները անջատված են՝ ներկառուցված տեքստերը և ամրացված տեքստերը Վերակնդրե՞լ տրամադրողների հերթականությունը։ Վերականգնել տրամադրողները լռելյայն առաջնահերթության հերթականությամբ Մաքրել պահպանված տեքստերը - Ջնջել այս սարքի վրա պահպանված տեքստերը Մաքրված է %1$s ՄԲ Ստանալ թոքեն Apple Music-ի մեդիա օգտատիրոջ տոկեն @@ -527,14 +525,9 @@ SABR վերարտադրում (Spoof տեսանյութերի հոսքերը ա Ցույց տալ պատճենման կոճակը Սեղմեք՝ բնօրինակ տեքստը որպես սովորական տեքստ պատճենելու համար, երկար սեղմեք՝ տեքստի աղբյուր ֆայլը Ներբեռնումներ պապկայում պահպանելու համար Ցույց տալ թարգմանության կոճակը - Սեղմեք՝ թարգմանությունը միացնելու/անջատելու համար, երկար սեղմեք՝ միայն թարգմանության համար: Օգտագործում է տրամադրողի թարգմանությունները, Google Translate-ը կամ OpenAI-ի հետ համատեղելի API-ն - Ցույց տալ Romanize կոճակը - Սեղմեք՝ տրանսլիտերացիան միացնելու/անջատելու համար, երկար սեղմեք՝ միայն տրանսլիտերացիայի համար: Օգտագործում է տրամադրողի տրանսլիտերացիան, Google Translate-ը կամ OpenAI-ի հետ համատեղելի API-ն - Տեղային տառադարձում և համապատասխանեցում - Կատարել տառադարձում սարքի վրա, երբ մատակարարը, ձեր մոդելը և Google Translate-ը չեն աշխատում: Կարդոմությունը հաջորդում է ժամանակային տեքստի յուրաքանչյուր բառին. բառերի սինխրոնացումը պետք է միացված լինի: Չինարեն, կանտոներեն, ճապոներեն, կորեերեն, հունարեն, ռուսերեն, վրացերեն, հայերեն; մոտ 120 ՄԲ - Կարդացվում է %1$s-ը՝ %2$d%% Կարդոմները պատրաստ են (%1$d գրառում) Ներբեռնումը ձախողվեց. անջատեք և նորից միացրեք սա՝ կրկնելու համար + Կարդացվում է %1$s-ը՝ %2$d%% Ցույց տալ Refresh կոճակը Հպեք՝ հաջորդ տեքստի տարբերակին անցնելու համար. սեղմեք երկար՝ ըստ երգի վերնագրի և կատարողի տեքստեր փնտրելու համար Փնտրել տեքստերը @@ -605,7 +598,6 @@ SABR վերարտադրում (Spoof տեսանյութերի հոսքերը ա Թարգմանված Թարգմանության լեզուն - Ռոմանիզացման լեզուն Թարգմանվում է… Չհաջողվեց թարգմանել բառերը Ռոմանիզացված @@ -619,7 +611,6 @@ SABR վերարտադրում (Spoof տեսանյութերի հոսքերը ա Թարմացվում է Թարմացված է QuickLRC API բանալին - QuickLRC-ը տառադարձում է կամ ժամանակային համապատասխանեցնում է ընթացիկ թրեկի տեքստերը: Սա առանձին ծառայություն է, որը կապ չունի վերոհի OpenAI-ի հետ համատեղելի վերջնակետի հետ: QuickLRC բանալին պահպանված է: Օգտագործեք համապատասխանեցման կոճակը երգերի տեքստերի որոնման պատուհանում կամ վահանակի հուշումը, երբ տրեկը չունի տեքստ QuickLRC API բանալին պահպանված է QuickLRC-ը չընդունեց այդ բանալին @@ -635,56 +626,27 @@ SABR վերարտադրում (Spoof տեսանյութերի հոսքերը ա Համապատասխանեցնելու տեքստ չկա. փոխարենը կատարվում է տրանսկրիպցիա QuickLRC-ն չկարողացավ մշակել այս տրեկը QuickLRC-ի կրեդիտները սպառված են այս տրեկի համար - "Բառերը համադրվում են ըստ վերնագրի և կատարողի, անսովոր մետատվյալները կարող են ոչինչ չգտնել կամ գտնել սխալ տարբերակը: Օգտագործեք թարմացումը տարբերակները փոխելու համար: * նշված մատակարարները պահանջում են տոկեն: Միացված բոլոր մատակարարներին միշտ չէ, որ հարցում է ուղարկվում ձեր նշած հերթականությամբ, այլ պատասխանը ստացվում է՝ կախված նրանց արագությունից:\n\nԲառերը տրամադրվում են երրորդ կողմի ծառայությունների կողմից, որոնք կապված չեն Morphe-ի հետ, չեն հովանավորվի կամ հավաստվում նրա կողմից: Յուրաքանչյուրն օգտագործվում է այնպես, ինչպես կա. բառերը, դրանց հասանելիությունը և լիցենզավորումը մնում են այդ ծառայությունների և նրանց լիցենզարուների մոտ, իսկ դրանց օգտագործումը կարող է ենթակայ լինել նրանց օգտագործման պայմաններին: Այն, ինչ հասնում է միացված ծառայությանը, միայն ձեր խնդրած որոնումն է՝ վերնագիրը, կատարողը և, այնտեղ, որտեղ այդ ծառայությանը դա անհրաժեշտ է, տեսանյութի ID-ն: Լրացուցիչ AI վերջնակետը և QuickLRC տրանսկրիպցիան աշխատում են միայն ձեր կողմից կայ alright-ացվելուց հետո և ստանում են բառերի տեքստը կամ թրեքի տեսանյութի URL-ը:" Ակտիվացրեք միայն ձեր օգտագործած մատակարարները կամ պահպանեք լռելյայն կարգավորումները. հաճախակի հարցումները կարող են przecանցել լիրիկայի փոքր համայնքային կայքերը Թաքցնել հեղինակության տողը Թաքցնել տեքստի ներքևում ցուցադրվող հեղինակության տողը Փոխել թարգմանության և տառադարձման տեղերը - ՄԻԱՑՎԱԾ՝ տառադարձումը դրվում է տեքստի ներքևում, իսկ թարգմանությունը՝ վերևում. ԱՆջԱՏՎԱԾ՝ տառադարձումը մնում է վերևում, իսկ թարգմանությունը՝ ներքևում - Թարգմանություն - Ռոմանիզացիա - Թարգմանչի տրամադրող - Google (Լռումնյա, Անվճար) - OpenAI համատեղելի (Անհատականացված) - Ռոմանիզացման մատակարար - Google (Լռելյայն) - Տեղային / អնցանց բառարան - OpenAI-ի հետ համատեղելի (Անհատական) - AI բաժին - Օգտագործվում է միայն այն դեպքում, երբ տրամադրողը չի ներառում բաժնի պիտակներ, ինչպիսիք են Կրկները կամ Տունը, որոնք այնուհետև հայտնաբերվում են ձեր OpenAI-ի հետ համատեղելի մոդելի կողմից - AI & ամպային վերջնակետեր - Կարգավորեք API բանալիները, մոդելները, վերջնակետերը և հուշումների կաղապարները AI մատակարարների համար: - API endpoint-ի URL-ը (օրինակ՝ https://api.openai.com/v1/chat/completions) Հուշման կաղապար Տեղադրնշաններ՝ {language}, {task}, {title}, {artist}, {count}, {lines} Հուշումը պահպանված է Հուշումը պահվեց, սակայն AI վերջնակետը դեռ հասանելի չէ: Նախ կայքերգավորեք այն AI կարգավորումներում Հուշումը վերակայացված է - Անվալիդ JSON: Ուղղեք այն կամ մաքրեք դաշտը նախքան պահպանելը Թարգմանության և ռոմանիզացիայի կարգավորումները պահպանված են Կարգավորումները վերականգնվեցին լռելյայն արժեքներին Հիմնական URL-ը պետք է սկսվի http:// կամ https://-ով - Ստուգել կապը - Կարգավորել endpoint-ները - Ցույց տալ բոլոր endpoint-ները Կապի ստուգում... - Թարգմանության տրամադրող - Ռոմանիզացիայի տրամադրող - Ստանալ մոդելները Մոդելների բեռնում… Ընտրեք մոդելը Չհաջողվեց բեռնել մոդելները: Ստուգեք API բանալին կամ կապը: - Gemini API կարգավորումներ - OpenAI համատեղելի (Custom Endpoint) - DeepL API կարգավորումներ - API բանալի Մոդել Հիմնական URL API բանալի (ըստ ցանկության) - Թարգմանության հուշում Հարցման լրացուցիչ մարմին (ըստ ցանկության JSON) Ավտորիզացիայի բանալի (Free կամ Pro) - Կարգավորեք API բանալիները, մոդելները, վերջնակետերը և հուշումները 8.23.51 - Անջատել թավ տառերով սիմվոլները 9.13.51 - Վերականգնել նվագարկիչի \"Հաջորդը\", \"Լիրիկան» և \"Կապված» ներդիրները diff --git a/patches/src/main/resources/addresources/values-in-rID/music/strings.xml b/patches/src/main/resources/addresources/values-in-rID/music/strings.xml index d8ede69ea7..35968870f2 100644 --- a/patches/src/main/resources/addresources/values-in-rID/music/strings.xml +++ b/patches/src/main/resources/addresources/values-in-rID/music/strings.xml @@ -462,12 +462,47 @@ Untuk memastikan scrobbling disinkronkan selama pemutaran latar belakang, pastik Tampilkan Sinkronisasi Panel - Titik akhir AI Semua penyedia dinonaktifkan - lirik bawaan dan lirik tertanam Setel ulang urutan penyedia? Kembalikan penyedia ke urutan prioritas bawaan + Daftar Kata Rime + "Daftar Kata Rime juga membutuhkan UniDict: di Penyedia Kamus Offline, centang \"Karakter Tionghoa dan Kantonis (UniDict)\" dan biarkan selesai mengunduh terlebih dahulu. Tanpanya, daftar yang membaca karakter tidak menghasilkan pembacaan. + +Centang daftar yang harus dibaca oleh Daftar Kata Rime, lalu ketuk Simpan untuk mengunduhnya. Daftar yang Anda tambahkan akan dihapus saat Anda menghapusnya. + +Setiap daftar adalah tabel kata-ke-pembacaan Anda sendiri, file .dict.yaml dengan baris seperti: +字 ham +典 din2 +Kolom pertama adalah kata, kolom kedua adalah bacanya; kolom ekstra dan baris tanpa tab diabaikan." + Tidak ada daftar kata Rime yang dicentang untuk bahasa di atas. Ketuk nama ini untuk mencentangnya, atau pilih bahasa yang memiliki daftar + Tambah + Tambah Daftar Kata Rime + File .dict.yaml, atau zip yang berisi satu file, dengan baris seperti 字 ham dan 典 din2. Daftar diambil saat Anda mencentangnya dan mengetuk Simpan, dan lisensinya milik Anda untuk diperiksa + Kode Bahasa + Dua atau tiga huruf, seperti hak atau wuu + Nama + Ditampilkan di bawah bacaan, seperti Hakka (Meixian) + Alamat daftar kata + Alamat URL https:// file .dict.yaml atau zip-nya + Karakter yang tidak ada dalam kata + Otomatis + Karakter Kantonis + Karakter Mandarin + Tanpa bacaan karakter + Isi kode, nama, dan alamat + Kode harus terdiri dari dua atau tiga huruf, seperti hak + Alamat harus dimulai dengan https:// + Nama dan alamat tidak boleh mengandung tanda vertikal bar + %1$s sudah memiliki tabel kata + %1$s dibacakan oleh romanisasi bawaan dan tidak dapat menerima daftar kata + Tidak digunakan: bahasa ini sudah memiliki bacaannya sendiri, jadi hapus centangnya untuk menghapusnya + %1$s ditambahkan - centang dan ketuk Simpan untuk mengunduhnya + Daftar kata siap + Daftar kata siap (%1$d entri) + Membangun daftar bacaan: %1$d%% + Pengunduhan daftar kata gagal - ketuk Simpan lagi untuk mencoba lagi Hapus lirik tersimpan - Hapus lirik yang disimpan di perangkat ini + Hapus %1$s MB lirik yang disimpan di perangkat ini %1$s MB telah dihapus Dapatkan token Token Pengguna Media Apple Music @@ -527,14 +562,22 @@ Cookie yang diperlukan: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1P Tampilkan tombol Salin Ketuk untuk menyalin lirik asli sebagai teks biasa; tekan lama untuk menyimpan file sumber lirik mentah ke Unduhan Tampilkan tombol Terjemahan - Ketuk untuk mengaktifkan/nonaktifkan terjemahan; tekan lama untuk hanya menampilkan terjemahan. Menggunakan penyedia terjemahan, Google Terjemahan, atau API yang kompatibel dengan OpenAI - Tampilkan tombol Romanisasi - Ketuk untuk mengaktifkan/nonaktifkan romanisasi; tekan lama untuk hanya menampilkan romanisasi. Menggunakan penyedia romanisasi, Google Terjemahan, atau API yang kompatibel dengan OpenAI - Romanisasi dan Penyelarasan Lokal - Romanisasi di perangkat saat penyedia, model Anda, dan Google Terjemahan gagal. Pembacaan mengikuti setiap kata dari lirik berwaktu; sinkronisasi kata harus aktif. Bahasa Mandarin, Kanton, Jepang, Korea, Yunani, Rusia, Georgia, Armenia; sekitar 120 MB - Membaca %1$s: %2$d%% + Ketuk untuk mengaktifkan/menonaktifkan terjemahan; tekan lama hanya untuk terjemahan. Menggunakan penyedia terjemahan yang diaktifkan sesuai urutan yang tercantum + Tampilkan tombol Romanisasi atau Transliterasi + Ketuk untuk mengaktifkan/menonaktifkan romanisasi atau transliterasi; tekan lama hanya untuk romanisasi atau transliterasi. Menggunakan penyedia romanisasi yang diaktifkan sesuai urutan yang tercantum + Aktifkan transliterasi + Izinkan bacaan non-Latin dari sumber lirik; saat dinonaktifkan, hanya pertahankan huruf Latin, huruf beraksen, dan digit nada + Membacakan setiap kata dalam lirik bertepat waktu (sinkronisasi kata aktif); Korea tidak perlu unduhan; empat belas skrip berbagi satu unduhan CLDR; Karakter Han dibacakan oleh Daftar Kata Rime ketika tabel katanya mencakup bahasanya + Mengunduh kamus bacaan… Pembacaan siap (%1$d entri) Pengunduhan gagal - matikan dan nyalakan kembali untuk mencoba lagi + Kamus Luring + Jepang (SudachiDict, ~75 MB) + Karakter Tionghoa dan Kantonis (UniDict, ~8 MB) + Kata Tionghoa (pinyin-pro, ~17 MB) + Transliterasi Yunani, Slavia Cyrillic (termasuk Rusia, Ukraina, Bulgaria, Serbia, Belarusia, Makedonia), Georgia, Armenia, Ibrani, Arab, Amharik, dan Syria (CLDR, ~0,5 MB) + Membaca %1$s: %2$d%% + Pilih setidaknya satu kamus Tampilkan tombol Segarkan Ketuk untuk beralih ke kandidat lirik berikutnya; tekan lama untuk mencari lirik berdasarkan judul lagu dan artis Cari lirik @@ -545,8 +588,10 @@ Cookie yang diperlukan: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1P Tersimpan Sorot kata atau suku kata yang disinkronkan Utamakan lirik kata per kata jika tersedia. Setiap kata atau suku kata akan disorot sesuai irama musik. - Tampilkan vokal latar secara terpisah - Memindahkan vokal latar belakang yang ditulis dalam tanda kurung, seperti \"(Oh) saya ingin kamu\", dari baris lagu ke baris redupnya sendiri + Perkecil baris yang tidak aktif + Menggambar setiap baris yang tidak sedang dinyanyikan sedikit lebih kecil, di atas efek meredupkannya + Tampilkan vokal latar belakang secara terpisah + Memisahkan vokal latar belakang dan harmoni dari garis utama. Memindahkan vokal latar belakang yang ditulis dalam kurung, seperti \"(Oh) aku menginginkanmu\", keluar dari garis yang dinyanyikan ke dalam garis redup tersendiri Sembunyikan lirik yang telah diputar Sembunyikan baris lirik di atas garis yang sedang diputar Sembunyikan lirik yang belum diputar @@ -604,10 +649,15 @@ Berlaku untuk kedua batas. Pada 1,75, empat karakter lebar penuh dihitung sebaga Diterjemahkan oleh %s Diromanisasi oleh %s Diromani oleh %s + Dromanisasi oleh Rime (%s) Diterjemahkan Bahasa terjemahan - Bahasa romanisasi + Bahasa romanisasi atau transliterasi + "Bahasa target terjemahan: +%s" + "Bahasa target romanisasi atau transliterasi: +%s" Menerjemahkan… Tidak dapat menerjemahkan lirik Diromanisasi @@ -621,7 +671,7 @@ Berlaku untuk kedua batas. Pada 1,75, empat karakter lebar penuh dihitung sebaga MediaSession Segarkan Kunci API QuickLRC - QuickLRC mentranskripsikan atau mencocokkan waktu lirik untuk trek saat ini. Layanan terpisah, tidak terkait dengan endpoint yang kompatibel dengan OpenAI di atas + QuickLRC mentranskripsikan atau menyelaraskan waktu lirik untuk trek saat ini Kunci QuickLRC disimpan. Gunakan tombol susun dalam dialog pencarian lirik, atau ajakan panel ketika trek tidak memiliki lirik Kunci API QuickLRC telah disimpan QuickLRC tidak menerima kunci tersebut @@ -637,56 +687,50 @@ Berlaku untuk kedua batas. Pada 1,75, empat karakter lebar penuh dihitung sebaga 1 lirik untuk diselaraskan - melainkan ditranskripsikan QuickLRC tidak dapat memproses trek ini Kredit QuickLRC telah habis untuk trek ini - "Lirik dicocokkan berdasarkan judul dan artis, metadata yang tidak biasa mungkin tidak menemukan apa-apa atau versi yang salah. Gunakan refresh untuk beralih versi. Penyedia yang ditandai * memerlukan token. Tidak semua penyedia yang diaktifkan selalu diminta sesuai urutan Anda, tetapi merespons berdasarkan kecepatan mereka.\n\nLirik berasal dari layanan pihak ketiga yang tidak berafiliasi, disponsori, atau didukung oleh Morphe. Masing-masing digunakan apa adanya: lirik, ketersediaan, dan lisensinya tetap berada di layanan tersebut dan pemberi lisensi, serta menggunakannya dapat tunduk pada syarat layanan mereka. Yang dikirim ke layanan yang diaktifkan hanyalah lookup yang Anda minta - yaitu judul, artis, dan, dimana layanan itu memerlukannya, ID video. Titik akhir AI opsional dan transkripsi QuickLRC hanya berjalan setelah Anda mengkonfigurasinya, dan menerima teks lirik atau URL video trek." + "Lirik dicocokkan berdasarkan judul dan artis, metadata yang tidak biasa mungkin tidak menemukan apa-apa atau versi yang salah. Gunakan refresh untuk beralih versi. Penyedia yang ditandai * memerlukan token. Tidak semua penyedia yang diaktifkan selalu diminta sesuai urutan Anda, tetapi merespons berdasarkan kecepatan mereka. + +Lirik berasal dari layanan pihak ketiga yang tidak berafiliasi, disponsori, atau didukung oleh Morphe. Masing-masing digunakan apa adanya: lirik, ketersediaan, dan lisensinya tetap berada di layanan tersebut dan pemberi lisensi, serta menggunakannya dapat tunduk pada syarat layanan mereka. Yang dikirim ke layanan yang diaktifkan hanyalah lookup yang Anda minta - yaitu judul, artis, dan, dimana layanan itu memerlukannya, ID video. Titik akhir AI opsional dan transkripsi QuickLRC hanya berjalan setelah Anda mengkonfigurasinya, dan menerima teks lirik atau URL video trek." Hanya aktifkan penyedia yang Anda gunakan, atau biarkan default - permintaan terus-menerus dapat membebani situs lirik komunitas kecil Sembunyikan baris kredit Sembunyikan baris kredit yang ditampilkan di bawah lirik Tukar terjemahan dan romanisasi - ON menempatkan romanisasi di bawah lirik dan terjemahan di atasnya; OFF membiarkan romanisasi tetap di atas dan terjemahan di bawahnya - Terjemahan - Romanisasi - Penyedia terjemahan - Google (Default, Gratis) - Kompatibel OpenAI (Kustom) - Penyedia romanisasi - Google (Default) - Kamus Lokal / Offline - Kompatibel OpenAI (Kustom) - Bagian AI - Hanya digunakan ketika provider tidak memberikan label bagian seperti Chorus atau Verse, yang kemudian akan dideteksi oleh model yang kompatibel dengan OpenAI - AI & Endpoint Cloud - Konfigurasikan kunci API, model, endpoint, dan templat prompt untuk penyedia AI. - URL endpoint API (contoh: https://api.openai.com/v1/chat/completions) + AKTIF menempatkan romanisasi atau transliterasi di bawah lirik dan terjemahan di atas; NONAKTIF mempertahankan romanisasi atau transliterasi di atas dan terjemahan di bawah. Bacaan yang ditempatkan di atas setiap suku kata bergerak bersamanya, dari atas sukukatanya ke bawahnya + Selalu tengahkan lirik + AKTIF menempatkan setiap baris di tengah dan mengabaikan penyelarasan sumber, termasuk sisi duet; NONAKTIF mempertahankan penyelarasan yang diminta oleh lirik + Setidaknya satu penyedia harus tetap aktif + Terjemahan + Romanisasi atau transliterasi + Kamus Luring + Daftar Kata Rime + Deteksi bagian dengan AI + Hanya digunakan ketika penyedia tidak menyertakan label bagian seperti Refrain atau Strofa, yang kemudian dideteksi oleh model kompatibel OpenAI Anda. Mengedit baris juga membuka promtnya + URL titik akhir API (mis. https://api.openai.com/v1/chat/completions, https://api.anthropic.com/v1/chat/completions, https://generativelanguage.googleapis.com/v1beta/openai/chat/completions) Templat Prompt Placeholder: {language}, {task}, {title}, {artist}, {count}, {lines} Prompt disimpan Permintaan disimpan, tetapi endpoint AI belum dapat digunakan. Konfigurasikan terlebih dahulu di Pengaturan AI Prompt diatur ulang - JSON tidak valid. Perbaiki atau kosongkan bidang sebelum menyimpan Setelan Terjemahan & Romanisasi disimpan Setelan diatur ke bawaan URL dasar harus diawali dengan http:// atau https:// - Uji Koneksi - Konfigurasi Endpoint - Tampilkan Semua Endpoint + Pengaturan tidak lengkap atau ditolak oleh penyedia Memvalidasi koneksi… - Penyedia Terjemahan - Penyedia Romanisasi - Ambil Model + %1$s: Terhubung! (%2$s) + %1$s: Koneksi gagal. Periksa pengaturan. + Masukkan kunci API terlebih dahulu Mengambil model… Pilih Model Gagal mengambil model. Periksa kunci API atau koneksi. - Pengaturan API Gemini - Kompatibel OpenAI (Endpoint Kustom) - Pengaturan API DeepL - Kunci API + Ajakan Model URL Dasar + Pilih layanan Kunci API (opsional) - Prompt terjemahan Body permintaan tambahan (JSON opsional) + Header permintaan ekstra (JSON opsional) + Beri nama dan nilai tanda kutip, mis. {\"api-key\": \"abc\"}. Nilai tanpa tanda kutip yang mengandung / : atau angka ditolak. Nilai kosong menghapus header. + Beri nama dan nilai tanda kutip, mis. {\"temperature\": 0.3}. Nilai tanpa tanda kutip yang mengandung / atau : ditolak. Model dan pesan tidak dapat diatur di sini. Kunci autentikasi (Gratis atau Pro) - Konfigurasikan kunci API, model, endpoint, dan prompt 8.23.51 - Nonaktifkan ikon tebal 9.13.51 - Kembalikan tab Pemutar Berikutnya, Lirik, dan Terkait diff --git a/patches/src/main/resources/addresources/values-in-rID/youtube/strings.xml b/patches/src/main/resources/addresources/values-in-rID/youtube/strings.xml index 0633cf6e69..704c331c21 100644 --- a/patches/src/main/resources/addresources/values-in-rID/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-in-rID/youtube/strings.xml @@ -29,6 +29,8 @@ Second \"item\" text" Lain-lain Video Pulihkan layar setelan lama YouTube + Tampilkan pesan di snackbar YouTube + Pesan Morphe, seperti konfirmasi tombol pemain, ditampilkan di snackbar YouTube daripada popup Hapus pembatasan pemutaran latar belakang Nonaktifkan pemutaran di latar belakang diff --git a/patches/src/main/resources/addresources/values-is-rIS/music/strings.xml b/patches/src/main/resources/addresources/values-is-rIS/music/strings.xml index d0b3f6dd45..4d2984012f 100644 --- a/patches/src/main/resources/addresources/values-is-rIS/music/strings.xml +++ b/patches/src/main/resources/addresources/values-is-rIS/music/strings.xml @@ -462,12 +462,47 @@ Til þess að tryggja að scrobbling sé samstilla á meðan spilað er í bakgr Sýning Samstilling Spjald - AI endaþáttur Allir veitendur óvirkir - innbyggðar lagatextar og innfelldir lagatextar Endurstilla forgangsröð? Endurheimta veitur í sjálfgefna forgangsröð + Rime orðalisti + "Rime orðalisti þarf einnig UniDict: í offline orðabókarveitanda, merktu „Kínverskir og kantónskir stafasnið (UniDict)“ og láttu hlaða niður fyrst. Án þess skilar listi sem les stafir engar leesur. + +Merktu listar Rime Wordlist ætti að lesa, smelltu síðan á Vista til að hlaða þeim niður. Listi sem þú bættir við er fjarlægður þegar ómerkir það. + +Hver listi er þinn eigin orð-til-leesur tafla, .dict.yaml með línum eins og: +Orð ham +Dæmi din2 +Fyrsta dálkurinn er orðið, annað Leesure; auka dálkar og línur án tab er hunsað." + Enginn Rime orðalisti er valinn fyrir tungumálið hér að ofan. Smelltu á þetta nafn til að velja einn, eða veldu tungumál sem hefur lista + Bæta við + Bæta við Rime Orðalista + .dict.yaml skrá, eða zip heldur eina, með línum eins og Orð ham og Dæmi din2. Listann er sóttur þegar þú merkist hann og smelltir á Vista, og leyfið hans er þitt að athuga + Tungmálakóði + Tvær eða þrír stafir, eins og hak eða wuu + Nafn + Sýnd undir útskrift, eins og Hakka (Meixian) + Vistfang orðalistans + https:// vistfang .dict.yaml eða zip þess + Stafir ekki í orði + Sjálfvirkt + Kantónskir stafir + Mandarin stafir + Engin stafsútskrift + Fylltu út kóða, nafn og vistfang + Kóðann verður að vera tvær eða þrír stafir, eins og hak + Vistfangið verður að byrja á https:// + Nafnið og vistfangið má ekki innihalda lóðrétt strik + %1$s hefur þegar orðatöflu + %1$s er lesið af innbyggðri rómverjun og getur ekki tekið orðalista + Notast ekki: þetta tungumál hefur nú þegar eigin útskrift, svo tjaldið af því til að fjarlægja það + Bætti %1$s - settu merki við það og smelltu á Vista til að sækja það + Orðalistanir tilbúnar + Orðalistanir tilbúnar (%1$d færslur) + Les orðalista: %1$d%% + Niðurhal orðalistans mistókst - smelltu á Vista aftur til að reyna aftur Hreinsa vistaðar lagatexta - Eyða lagatexta sem geymdir eru á þessu tæki + Eyða %1$s MB af textunum geymdum á þessu tæki Hreinsað %1$s MB Sækja token Apple Music miðlanotandamerki @@ -525,14 +560,22 @@ Nauðsynlegar vafrakökur: SID, HSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPI Sýna afritunarhnapp Ýttuðu til að afrita upprunalega lagatexta sem hreinan texta; haltu lengi til að vista hrátt lagatextaskráarskrá í Niðurhal Sýna þýðingarhnapp - Ýttuðu til að víxla þýðingu; haltu lengi fyrir aðeins þýðingu. Notar þýðingar frá veitanda, Google Translate, eða OpenAI-samkvæmt API - Sýna umritunarhnappinn - Ýttuðu til að víxla rómönnun; haltu lengi fyrir aðeins rómönnun. Notar rómönnun frá veitanda, Google Translate, eða OpenAI-samkvæmt API - Staðbundin Rómönnun og Samstilling - Rómanna á tækinu þegar veitandinn, módelið þitt og Google Translate takast ekki. Leseferðin fylgir hverju orði tímsetts lagatexta; orðasamstilling verður að vera kveikt. Kinesískt, Kantonesíska, Japanska, Kóreska, Gríska, Rússneska, Georgísku, Armeníska; um 120 MB - Les %1$s: %2$d%% + Smelltu til að víxla þýðingu; langtækja til að fá aðeins þýðingu. Notar virkar þýðiveitendur í röð þeirra + Sýna Rómverjun eða Transliterering hnapp + Smelltu til að víxla rómverjun eða translitereringu; langtækja til að fá aðeins rómverjun eða translitereringu. Notar virkar rómverjuveitendur í röð þeirra + Virkja translitereringu + Leyfa ekki-latínuskýringar frá lagatilfelli; þegar slökkt, haltu bara latínbókstöfum, bókstöfum með accent og hljóðtölum + Les hvert orð af tíma lagatilfelli (orð samstilling á); Korea þarf ekki niðurhal; fimmtán ritmál deila einum CLDR niðurhal; Han stafir eru lesnir af Rime Orðalista þegar orðatalan nær yfir tungumálið + Að sækja útskrift orðabækur… Leseferðir tilbúnar (%1$d færslur) Niðurhól mistókst - slökktu á og kveiktu aftur til að reyna aftur + Ótengd orðabók + Japanskt (SudachiDict, ~75 MB) + Kínverskir og Kantónsku stafir (UniDict, ~8 MB) + Kínverskir orð (pinyin-pro, ~17 MB) + Gríska, Slavneska kýrillískt (þ.á.m. rússneska, úkraínska, bulgaríska, serbneska, hvíta-rússneska, makedónska), georgíska, armenska, hebreska, arabíska, amhara og sýríska rómanisering (CLDR, ~0,5 MB) + Les %1$s: %2$d%% + Veldu að minnsta kosti eina orðabók Settu inn gildar vafrakökur til að leyfa YouTube-tengingu. Ýttuðu til að skipta yfir í næstu lagatextamöguleika; haltu lengi til að leita að lagatexta eftir lagi og listamanni Leita að texta @@ -543,8 +586,10 @@ Nauðsynlegar vafrakökur: SID, HSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPI Vistað Auðkenna samstillt orð eða atkvæði Sérsníddu texta til að sýna lög og frumtexta. - Birta bakraddir sérstaklega - Færir bakraddað söngorð sem eru í svigum, eins og \"(Ó)\", úr sönglínunni yfir á eigin deyfðari línu + Draga óvirkar línur + Teiknar hverja línu sem er ekki verið að syngja í augunum smám saman, ofan á dimmingu hennar + Sýna bakraddir sérstaklega + Skilur bakraddir og harmoníu frá foringjalínunni. Flytur bakradd sem skrifuð er í klombum, svo sem \"(Oh) I want you\", úr sönglinunni í sína eigin dökkari línu Fela spilaða texta Fela textalínur fyrir ofan línuna sem spilast núna Fela óspilaða texta @@ -602,10 +647,15 @@ Gildir fyrir bæði mörk. Við 1,75 teljast fjórar fullbreiddarteiknir sem 7." Þýtt af %s Umritað af %s Rómað af %s + Rómverað af Rime (%s) Þýddur Þýðingartungumál - Rómunar tungumál + Rómverjun eða transliterering tungumál + "Þýðingarmarkmál tungumáls: +%s" + "Romanization eða transliteration markmál tungumáls: +%s" Þýðing á ferð… Gat ekki þýtt textann Rómanseraður @@ -619,7 +669,7 @@ Gildir fyrir bæði mörk. Við 1,75 teljast fjórar fullbreiddarteiknir sem 7." Endurnýjun Endurnýjað QuickLRC API lykill - QuickLRC þýðir eða tímasetir texta fyrir núverandi lag. Aðgreind þjónusta, óskyld ofangreindum OpenAI-samræmanlegum endapunkti + QuickLRC transcribirer eða tímasamstillir textana fyrir núverandi lag QuickLRC lykill vistaður. Notaðu jafnaratakann í textaleitarglugganum eða spjaldtilkynningu þegar lag hefur engin texta QuickLRC API lykkil vistaður QuickLRC tók ekki við því lykli @@ -635,56 +685,50 @@ Gildir fyrir bæði mörk. Við 1,75 teljast fjórar fullbreiddarteiknir sem 7." Enginn texti til að jafna - transcribe-i þess í stað QuickLRC gat ekki unnið þessi lög QuickLRC stig eru útrunnin fyrir þessa lög - "Lagatextar eru paraðir eftir titli og listamanni, óvenjuleg metaupplýsingar gætu fundið ekkert eða rangú útgáfu. Nota endurnýjun til að skipta um útgáfur. Veitendur merktir * þurfa lykil. Allir virkir veitendur eru ekki alltaf beittir í röðinni þinni, en svara eftir hraða sínum.\n\nLagatextar koma frá þriðja-aðila þjónustum sem eru ekki tengdar, stuðlaðar eða samþykktar af Morphe. Hver ein sér notuð eins og hann er: lagatextarnir, tiltækni þeirra og leyfisréttir halda áfram hjá þeim þjónustum og leyfishöfum þeirra, og notkun þeirra getur verið háð skilmálum þeirra. Hvað nær virkan þjónustu er aðeins leitin sem þú baðst um - titillinn, listamaðurinn, og, þar sem sú þjónusta þarf hana, myndskeiðauðkennið. Valfrjálsa AI-enderingurinn og QuickLRC transskriptun keyra aðeins eftir að þú setur upp þær, og fáa lagatextann eða myndband URL-lagsins." + "Ljóð eru passað af titli og flytjanda, óeðlilegt metadata getur fundið ekkert eða rangt útgáfu. Notaðu endurnýja til að skipta um útgáfur. Veitur merkta * þurfa token. Allir virkjuðu veitur eru ekki alltaf biðin í röð þinni, en svara byggt á hraða þeirra. + +Ljóð koma frá þriðja aðila þjónustu sem eru ekki tengd, stuðluð eða viðurkennd af Morphe. Hver ein er notuð eins og hún er: ljóðin, tiltækni þeirra, og leyfiþeirra halda sig við þær þjónustur og eigendur þeirra, og notkun þeirra getur verið háð þjónustumöguleikum þeirra. Hvað nær virkjaðri þjónustu er aðeins leitarupphaf þú baðst - titillinn, flytjandinn, og, þar sem þjónusta þarf það, video ID. Optional AI endpoint og QuickLRC transcription keyra aðeins eftir að þú stillingar þau, og taka ljóð texta eða track's video URL." Virkjaðu einungis þær birgjar sem þú notar, eða haltu sjálfgefinu – stöðug beiðnur geta valdið ofhleðslu á smá samfélags síðum með textum Fela upplýsingalínu Fela upplýsingalínuna sem sýnd er fyrir neðan textann Víxla þýðingu og rómönskun - ON setur rómönskun fyrir neðan texta og þýðingu fyrir ofan; OFF heldur rómönskun fyrir ofan og þýðingu fyrir neðan - Þýðing - Rómönun - Þýðingaraðili - Google (sjálfgefið, ókostnaðarlaust) - OpenAI compatible ( sérsniðið) - Veitandi rómönunar - Google (sjálfgefið) - Staðbundin / ólínu orðabók - OpenAI compatible ( sérsniðið) - AI-hluti - Saaðst aðeins þegar birgir gefur ekki út merki fyrir hluta eins og Chorus eða Verse, sem OpenAI-samþætt líkan þitt greinir þá - AI & skýjatengiliðir - Stillingar á API lyklum, módelum, tengiliðum og fyrirsagnymyndum fyrir AI veitendur. - URL fyrir API tengilið (t.d. https://api.openai.com/v1/chat/completions) + ON setur rómverjun eða transliterering undir texta og þýðingu ofan; OFF heldur rómverjun eða transliterering ofan og þýðingu undan. Skýring sett yfir hvora staf flyst með honum, ofan frá stafnum til undan + Alltaf miðja texta + ON setur hverja línu í miðju og hunsar upprunalega samstillingu, þ.m.t. duet hluta; OFF heldur samstillingunni sem textinn biður um + Að minnsta kosti einn veitandi verður að vera á + Þýðing + Rómverjun eða transliterering + Ótengd orðabók + Rime Orðalisti + Greina hluti með AI + Aðeins notað þegar veitandinn sendir engin hlutanúmer eins og Chorus eða Verse, sem OpenAI samhæft módelið þitt greinir þá. Breytinga röðinni opnar líka spurninguna + API enda punkts URL (t.d. https://api.openai.com/v1/chat/completions, https://api.anthropic.com/v1/chat/completions, https://generativelanguage.googleapis.com/v1beta/openai/chat/completions) Sniðmát fyrir fyrirspurn Plasshaldar: {language}, {task}, {title}, {artist}, {count}, {lines} Fyrirspurn vistuð Prompt vistað, en AI-endipunktur er enn ekki notanlegur. Uppsettu hann undir AI-stillingar fyrst Fyrirspurn endurstillt - Ekki gilt JSON. Leiðréttu það eða hreinsaðu reitinn áður en þú vistar Stillingar fyrir þýðingu & rómönun vistuðar Stillingar afturstilltar í upphafsvirðum Grunnslóð verður að byrja á http:// eða https:// - Prófa tengingu - Stilla endapunktana - Sýna alla endapunkta + Stillingar ófullkomnar eða höfnunar af veitanum Sannreyni tengingu… - Þýðingaraðili - Rómönunaraðili - Sækja líkön + %1$s: Tengt! (%2$s) + %1$s: Tenging mistókst. Athugaðu stillingarnar. + Sláðu inn API lykilinn fyrst Sækist eftir líkön… Veldu líkan Mislaðist að sækja líkana. Athugið API lykil eða tengingu. - Stillingar fyrir Gemini API - OpenAI samhæft (sérsniðinn endapunktur) - Stillingar fyrir DeepL API - API lykill + Spurning Líkan Grunnur-URL + Veldu þjónustu API lykill (valfrjáls) - Tilvísun fyrir þýðingu Auka beiðnisnið (valfrjálst JSON) + Auk beiðna hausar (valfrjálst JSON) + Kotdu nöfn og gildi, t.d. {\"api-key\": \"abc\"}. Ókottrauð gildi sem innihalda / : eða tölu er hafnað. Tómt gildi fjarlægir haus. + Kotdu nöfn og gildi, t.d. {\"temperature\": 0.3}. Ókottrauð gildi sem innihalda / eða : er hafnað. Módel og skyddar geta ekki verið stillt hér. Aðgangslykill (Ókeypis eða Pro) - Stilla API lykla, líkan, endapunkt og leiðbeiningar 8.23.51 - Slökkva á feitletruðum táknum 9.13.51 - Endurheimta næst, texta og tengda flipa diff --git a/patches/src/main/resources/addresources/values-is-rIS/youtube/strings.xml b/patches/src/main/resources/addresources/values-is-rIS/youtube/strings.xml index 921850ae2e..7d90c532a6 100644 --- a/patches/src/main/resources/addresources/values-is-rIS/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-is-rIS/youtube/strings.xml @@ -29,6 +29,8 @@ Second \"item\" text" Ýmislegt Myndband Endurheimta gamla stillingaskjá YouTube + Sýna skilaboð í YouTube snackbar + Skilaboð frá Morphe, svo sem staðfesting hnapps spilara, birtast í YouTube snackbar í stað sprettiglugga Fjarlægja takmarkanir á spilun í bakgrunni Slökkva á spilun í bakgrunni diff --git a/patches/src/main/resources/addresources/values-it-rIT/music/strings.xml b/patches/src/main/resources/addresources/values-it-rIT/music/strings.xml index f3fe1cdbb0..2a67753e39 100644 --- a/patches/src/main/resources/addresources/values-it-rIT/music/strings.xml +++ b/patches/src/main/resources/addresources/values-it-rIT/music/strings.xml @@ -461,12 +461,47 @@ Per garantire che lo scrobbling sia sincronizzato durante la riproduzione in bac Visualizza Sincronizzazione Pannello - Endpoint AI Tutti i provider disabilitati - testi integrati e testi incorporati Reimpostare l\'ordine dei provider? Ripristina i provider all\'ordine di priorità predefinito + Elenco parole Rime + "L'elenco di parole Rime richiede anche UniDict: nel provider del dizionario offline, spunta \"Caratteri cinesi e cantonesi (UniDict)\" e lascia prima completare il download. Senza di esso, un elenco che legge i caratteri non restituisce alcuna lettura. + +Seleziona gli elenchi che l'elenco di parole Rime dovrebbe leggere, quindi tocca Salva per scaricarli. Un elenco aggiunto viene rimosso quando lo deselezioni. + +Ogni elenco è la tua tabella personale parola-lettura, un file .dict.yaml con righe come: +字 ham +典 din2 +La prima colonna è la parola, la seconda la sua lettura; le colonne extra e le righe senza tabulazione vengono ignorate." + Nessun elenco di parole Rime è selezionato per la lingua sopra. Tocca questo nome per selezionarne uno, o scegli una lingua che ha un elenco + Aggiungi + Aggiungi un elenco di parole Rime + Un file .dict.yaml, o uno zip che ne contiene uno, con righe come 字 ham e 典 din2. L\'elenco viene recuperato quando lo selezioni e tocchi Salva, e la sua licenza è a tua disposizione per la verifica + Codice lingua + Due o tre lettere, come in hak o wuu + Nome + Mostrato sotto la lettura, come in Hakka (Meixian) + Indirizzo dell\'elenco parole + URL HTTPS del file .dict.yaml o del relativo archivio zip + Caratteri non in una parola + Automatico + Caratteri cantonesi + Caratteri mandarino + Nessuna lettura del carattere + Inserisci il codice, il nome e l\'indirizzo + Il codice deve essere due o tre lettere, come in hak + L\'indirizzo deve iniziare con https:// + Il nome e l\'indirizzo non possono contenere una barra verticale + %1$s ha già una tabella di parole + %1$s è letto dal romanizzatore integrato e non può accettare un elenco di parole + Non utilizzato: questa lingua ha già la sua lettura, quindi deselezionala per rimuoverla + Aggiunto %1$s - selocalo e tocca Salva per scaricarlo + Elenchi parole pronti + Elenchi parole pronti (%1$d voci) + Lettura dell\'elenco delle parole: %1$d%% + Download elenco parole fallito - tocca Salva di nuovo per riprovare Elimina testi salvati - Elimina i testi memorizzati su questo dispositivo + Elimina %1$s MB di testi conservati su questo dispositivo %1$s MB eliminati Ottieni token Token utente multimediale di Apple Music @@ -521,14 +556,22 @@ Cookie richiesti: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPISID Mostra il pulsante di copia Tocca per copiare i testi originali come testo semplice; tieni premuto per salvare il file sorgente dei testi grezzi in Download Mostra il pulsante Traduci - Tocca per attivare/disattivare la traduzione; tieni premuto per solo traduzione. Usa le traduzioni del provider, Google Translate o un\'API compatibile con OpenAI - Mostra il pulsante Romanizza - Tocca per attivare/disattivare la romanizzazione; tieni premuto per solo romanizzazione. Usa la romanizzazione del provider, Google Translate o un\'API compatibile con OpenAI - Romanizzazione e allineamento locale - Romanizza sul dispositivo quando il provider, il tuo modello e Google Translate falliscono. La lettura segue ogni parola di un testo sincronizzato; la sincronizzazione delle parole deve essere attiva. Cinese, Cantonese, Giapponese, Coreano, Greco, Russo, Georgiano, Armeno; circa 120 MB - Lettura %1$s: %2$d%% + Tocca per attivare/disattivare la traduzione; pressione lunga per la sola traduzione. Utilizza i provider di traduzione abilitati nel loro ordine elencato + Mostra pulsante Romanizza o Trascrivi + Tocca per attivare/disattivare la romanizzazione o la traslitterazione; pressione lunga per la sola romanizzazione o traslitterazione. Utilizza i provider di romanizzazione abilitati nel loro ordine elencato + Abilita traslitterazione + Consenti letture non latine dalla fonte dei testi; quando disattivato, mantieni solo lettere latine, lettere accentate e cifre dei toni + Legge ciascuna parola di un testo temporizzato (sincronizzazione parole attiva); il coreano non necessita di download; quattordici script condividono un download CLDR; i caratteri Han sono letti dall\'elenco parole Rime quando la sua tabella di parole copre la lingua + Download dizionari di lettura… Letture pronte (%1$d voci) Scaricamento non riuscito: disattiva e riattiva per riprovare + Dizionario Offline + Giapponese (SudachiDict, ~75 MB) + Caratteri cinesi e cantonesi (UniDict, ~8 MB) + Parole cinesi (pinyin-pro, ~17 MB) + Traslitterazione greca, slava cirillica (incl. russo, ucraino, bulgaro, serbo, bielorusso, macedone), georgiana, armena, ebraica, araba, amarica e siriaca (CLDR, ~0.5 MB) + Lettura %1$s: %2$d%% + Scegli almeno un dizionario Mostra il pulsante Aggiorna Tocca per passare al candidato di testo successivo; tieni premuto per cercare i testi per titolo del brano e artista Cerca testi @@ -539,8 +582,10 @@ Cookie richiesti: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPISID Salvato Lingua dei testi Mostra i testi - Mostrare le vocali di sottofondo separatamente - Sposta il coro di sottofondo scritto tra parentesi, ad esempio \"(Oh) ti voglio\", dalla parte cantata in una riga distinta con minore intensità + Riduci le righe inattive + Disegna ogni riga che non sta venendo cantata al momento leggermente più piccola, sopra l\'annerirla + Mostra vocali di sottofondo separatamente + Separa le vocali di sottofondo e l\'armonia dalla linea principale. Sposta una voce di sottofondo scritta tra parentesi, come \"(Oh) voglio te\", fuori dalla linea cantata in una propria linea più scura Testi sincronizzati Nascondi le righe dei testi sopra la riga attualmente in riproduzione Copia i testi @@ -598,10 +643,15 @@ Si applica a entrambi i limiti. A 1,75, quattro caratteri a larghezza intera con Tradotto da %s Romanizzato da %s Romanizzato da %s + Romanizzato da Rime (%s) Tradotto Lingua di traduzione - Lingua di romanizzazione + Lingua di romanizzazione o traslitterazione + "Lingua di traduzione target: +%s" + "Lingua di traslitterazione o romanizzazione target: +%s" Traduzione in corso… Impossibile tradurre il testo Romanizzato @@ -615,7 +665,7 @@ Si applica a entrambi i limiti. A 1,75, quattro caratteri a larghezza intera con Aggiornamento in corso Aggiornato Chiave API QuickLRC - QuickLRC trascrive o allinea temporalmente i testi per la traccia corrente. Un servizio separato, non correlato all\'endpoint compatibile con OpenAI sopra indicato + QuickLRC trascrive o allinea temporalmente i testi per la traccia corrente Chiave QuickLRC salvata. Usa il pulsante di allineamento nella finestra di dialogo di ricerca dei testi o il prompt del pannello quando una traccia non ha testi Chiave API QuickLRC salvata QuickLRC non ha accettato quella chiave @@ -631,58 +681,50 @@ Si applica a entrambi i limiti. A 1,75, quattro caratteri a larghezza intera con Nessun testo da allineare - trascrizione al suo posto QuickLRC non ha elaborato questa traccia I crediti QuickLRC sono esauriti per questa traccia - "I testi vengono abbinati per titolo e artista; metadati insoliti potrebbero non trovare nulla o la versione sbagliata. Usa Aggiorna per cambiare versione. I provider contrassegnati con * richiedono un token. Non tutti i provider abilitati vengono sempre richiesti nel tuo ordine, ma rispondono in base alla loro velocità. + "I testi delle canzoni vengono abbinati per titolo e artista; metadati insoliti potrebbero non trovare nulla o una versione errata. Usa Aggiorna per cambiare versione. I fornitori contrassegnati da * richiedono un token. Non tutti i fornitori abilitati vengono sempre richiesti nell'ordine specificato, ma rispondono in base alla loro velocità. -I testi provengono da servizi di terze parti che non sono affiliati, sponsorizzati o approvati da Morphe. Ognuno viene utilizzato così com'è: i testi, la loro disponibilità e la loro licenza rimangono a quei servizi e ai loro licenzianti, e il loro utilizzo può essere soggetto ai loro termini di servizio. Ciò che raggiunge un servizio abilitato è solo la ricerca che hai richiesto - il titolo, l'artista e, dove il servizio ne ha bisogno, l'ID video. L'endpoint AI opzionale e la trascrizione QuickLRC vengono eseguiti solo dopo che li hai configurati, e ricevono il testo dei testi o l'URL video del brano." +I testi provengono da servizi di terze parti che non sono affiliati, sponsorizzati o approvati da Morphe. Ciascuno viene utilizzato così com'è: i testi, la loro disponibilità e la relativa licenza rimangono di proprietà di tali servizi e dei loro concessori di licenza, e il loro utilizzo può essere soggetto ai loro termini di servizio. Ciò che viene inviato a un servizio abilitato è solo la ricerca richiesta - il titolo, l'artista e, dove necessario per quel servizio, l'ID del video. L'endpoint opzionale AI e la trascrizione QuickLRC vengono eseguiti solo dopo la loro configurazione e ricevono il testo della canzone o l'URL del video della traccia." Abilita solo i provider che utilizzi, oppure mantieni le impostazioni predefinite — richieste costanti possono sovraccaricare i siti di testi della community Nascondi la riga dei crediti Caricamento testi... Scambia traduzione e romanizzazione - Riprova - Traduzione - Romanizzazione - Fornitore di traduzione - Google (Predefinito, Gratis) - Compatibile con OpenAI (Personalizzato) - Fornitore di romanizzazione - Google (Predefinito) - Dizionario locale / offline - Compatibile con OpenAI (Personalizzato) - Sezione AI - Utilizzato solo quando il provider non fornisce etichette di sezione come Ritornello o Strofa, che vengono quindi rilevate dal modello compatibile con OpenAI - AI & endpoint cloud - Configura chiavi API, modelli, endpoint e modelli di prompt per i fornitori di IA. - URL dell\'endpoint API (es. https://api.openai.com/v1/chat/completions) + ON mette la romanizzazione o la traslitterazione sotto i testi e la traduzione sopra; OFF mantiene la romanizzazione o la traslitterazione sopra e la traduzione sotto. Una lettura posizionata sopra ogni sillaba si muove con essa, dall\'alto della sua sillaba al basso di essa + Centra sempre i testi + ON mette ogni riga al centro e ignora l\'allineamento della sorgente, inclusi i lati del duetto; OFF mantiene l\'allineamento richiesto dai testi + Almeno un provider deve rimanere acceso + Traduzione + Romanizzazione o traslitterazione + Dizionario Offline + Elenco parole Rime + Rileva sezioni con IA + Utilizzato solo quando il provider non fornisce etichette di sezione come Ritornello o Strofa, che il tuo modello compatibile con OpenAI rileva. La modifica della riga apre anche il suo prompt + URL endpoint API (es. https://api.openai.com/v1/chat/completions, https://api.anthropic.com/v1/chat/completions, https://generativelanguage.googleapis.com/v1beta/openai/chat/completions) Modello di prompt Segnaposti: {language}, {task}, {title}, {artist}, {count}, {lines} Prompt salvato Prompt salvato, ma l\'endpoint AI non è ancora utilizzabile. Configurarlo prima nelle Impostazioni AI Prompt ripristinato - JSON non valido. Correggilo o svuota il campo prima di salvare Impostazioni di traduzione & romanizzazione salvate Impostazioni ripristinate ai valori predefiniti L\'URL di base deve iniziare con http:// o https:// - Testa connessione - Configura endpoint - Mostra tutti gli endpoint + Impostazioni incomplete o rifiutate dal provider Verifica della connessione… - Fornitore di traduzione - Fornitore di romanizzazione - Recupera modelli + %1$s: Connesso! (%2$s) + %1$s: Connessione fallita. Controlla le impostazioni. + Inserisci prima la chiave API Recupero modelli… Seleziona un modello Impossibile recuperare i modelli. Controlla la chiave API o la connessione. - Impostazioni API Gemini - Compatibile con OpenAI (Endpoint personalizzato) - Impostazioni API DeepL - Chiave API + Invito Modello URL di base + Scegli un servizio Chiave API (opzionale) - Prompt di traduzione Corpo della richiesta extra (JSON opzionale) + Intestazioni richiesta extra (JSON opzionale) + Cita nomi e valori, es. {\"api-key\": \"abc\"}. Un valore non citato contenente / : o un numero viene rifiutato. Un valore vuoto rimuove un\'intestazione. + Cita nomi e valori, es. {\"temperature\": 0.3}. Un valore non citato contenente / o : viene rifiutato. Model e messages non possono essere impostati qui. Chiave di autenticazione (Free o Pro) - Configura chiavi API, modelli, endpoint e prompt 8.23.51 - Disabilita icone in grassetto 9.13.51 - Ripristina le schede Successivo, Testi e Correlati del lettore diff --git a/patches/src/main/resources/addresources/values-it-rIT/youtube/strings.xml b/patches/src/main/resources/addresources/values-it-rIT/youtube/strings.xml index 6db47af623..359b2ebabb 100644 --- a/patches/src/main/resources/addresources/values-it-rIT/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-it-rIT/youtube/strings.xml @@ -29,6 +29,8 @@ Second \"item\" text" Varie Video Ripristina la vecchia schermata delle impostazioni di YouTube + Mostra messaggi nella snackbar di YouTube + I messaggi di Morphe, come la conferma relativa a un pulsante del player, vengono visualizzati nella snackbar di YouTube anziché in un popup. Rimuovi le restrizioni alla riproduzione in secondo piano Disattiva la riproduzione degli Shorts in secondo piano diff --git a/patches/src/main/resources/addresources/values-iw-rIL/music/strings.xml b/patches/src/main/resources/addresources/values-iw-rIL/music/strings.xml index f578a8ed96..0905c24d74 100644 --- a/patches/src/main/resources/addresources/values-iw-rIL/music/strings.xml +++ b/patches/src/main/resources/addresources/values-iw-rIL/music/strings.xml @@ -462,12 +462,47 @@ Second \"item\" text" תצוגה סנכרון פאנל - נקודת קצה של בינה מלאכותית כל הספקים מבוטלים - שירים מובנים ושירים מוטבעים לאפס את סדר הספקים? שחזר את הספקים לסדר העדיפות המוגדר כברירת מחדל + רשימת מילים של Rime + "רשימת המילים של Rime דורשת גם את UniDict: בספק המילי המקומי, סמן \"תווים סיניים וקנטונזיים (UniDict)\" והמתן לסיום ההורדה. בלעדיו, רשימה הקוראת תווים לא תספק הגייה. + +סמן את הרשימות ש-Rime Wordlist צריך לקרוא, ואז הקש על שמור להורדתן. רשימה שהוספת תוסר כאשר תבטל את הסימון שלה. + +כל רשימה היא טבלה משלך למילת-הגייה, קובץ .dict.yaml עם שורות כמו: +字 ham +典 din2 +העמודה הראשונה היא המילה, השנייה ההגייה שלה; עמודות נוספות ושורות ללא טאב מוזנחים." + לא מסומנת אף רשימת מילים של Rime לשפה למעלה. הקש על השם כדי לסמן אחת, או בחר שפה שיש לה רשימה + הוסף + הוסף רשימת מילים של Rime + קובץ .dict.yaml, או ארכיון zip המחזיק בו, עם שורות כמו 字 ham ו-典 din2. הרשימה נשלפת כאשר מסמנים אותה ולוחצים על שמור, והרישיון שלה בידיך לבדוק + קוד שפה + שתי או שלוש אותיות, כמו ב-hak או wuu + שם + מוצג מתחת להגייה, כמו ב-Mandarin (Meixian) + כתובת רשימת המילים + כתובת https:// של ה-.dict.yaml או ה-zip שלו + תווים שלא במילה + אוטומטי + תווים קנטונזיים + תווים מנדרינית + אין הגיית תו + מלא את הקוד, השם והכתובת + הקוד חייב להיות שתי או שלוש אותיות, כמו b-hak + הכתובת חייבת להתחיל ב-https:// + השם והכתובת לא יכולים להכיל פס אנכי + %1$s כבר יש לו טבלת מילים + %1$s מבוטא על ידי הרומניזר הבנוי ולא יכול לקחת רשימת מילים + לא משמש: השפה הזו כבר יש לה את ההגייה שלה עצמה, כך שתבטל את הסימון שלה כדי להסיר אותו + נוסף %1$s - סמן אותו ולחץ על שמור להוריד אותו + רשימות מילים מוכנות + רשימות המילים מוכנות (%1$d רשומות) + קורא רשימת מילים: %1$d%% + הורדת רשימת המילים נכשלה - לחץ על שמור שוב כדי לנסות שוב נקה שיריות מאוחסנות - מחק את השיריות שנשמרו במכשיר זה + מחק את %1$s MB של המילים שנשמרו במכשיר זה נוקו %1$s MB קבל טוקן אסימון משתמש מדיה של Apple Music @@ -527,14 +562,22 @@ Second \"item\" text" הצג לחצן העתקה לחץ להעתקת שיריות מקוריות כטקסט רגיל; לחיצה ארוכה לשמירת קובץ המקור הגולמי של השיריות להורדות הצג לחצן תרגום - לחץ כדי לסובב תרגום; לחיצה ארוכה לתרגום בלבד. משתמש בתרגומי ספק, גוגל תרגום או API תואם OpenAI - הצג את כפתור הרומניזציה - לחץ כדי לסובב רומניזציה; לחיצה ארוכה לרומניזציה בלבד. משתמש ברומניזציה של ספק, גוגל תרגום או API תואם OpenAI - רומניזציה מקומית ויישור - רומניזציה במכשיר כאשר הספק, המודל שלך וגוגל תרגום נכשלים. הקריאה עוקבת אחרי כל מילה של שיריה זמן-מתואם; סינכרון מילים חייב להיות פעיל. סינית, קנטונזית, יפנית, קוריאנית, יוונית, רוסית, גאורגית, ארמנית; כ-120 MB - קריאה %1$s: %2$d%% + הקש כדי להפעיל/לכבות תרגום; לחיצה ארוכה לתרגום בלבד. משתמש בספקי התרגום המופעלים לפי הסדר המפורט + הצג כפתור רומניזציה או האברה + הקש כדי להפעיל/לכבות רומניזציה או האברה; לחיצה ארוכה לרומניזציה או האברה בלבד. משתמש בספקי הרומניזציה המופעלים לפי הסדר המפורט + הפעל האברה + אפשר הגיות שאינן לטיניות ממקור המילים; כאשר הוא מכובה, שמור רק על אותיות לטיניות, אותיות עם ניבים ומספרי הצלילות + קורא כל מילה של מילים מזומנות (סינכרון מילים פעיל); קוריאנית אינה דורשת הורדה; ארבעה עשר סקיפטים חולקים הורדה CLDR אחת; תווים Han מבוטאים על ידי רשימת מילים של Rime כאשר טבלת המילים שלה מכסה את השפה + מוריד מילונים להגייה... קריאות מוכנות (%1$d ערכים) ההורדה נכשלה - כבה הדלק מחדש כדי לנסות שוב + מילון חינתי + יפנית (SudachiDict, ~75 MB) + תווים סיניים וקנטונזיים (UniDict, ~8 MB) + מילים סיניות (pinyin-pro, ~17 MB) + יוונית, סלאביים קיריליים (כולל רוסי, אוקראיני, בולגרי, סרבי, בלארוסי, מקדוני), גיאורגי, ארמני, עברי, ערבית, אמארית וסורית האברה (CLDR, ~0.5 MB) + קריאה %1$s: %2$d%% + בחר לפחות מילון אחד הצג את כפתור הרענון לחץ כדי לעבור למועמד השירים הבא; לחיצה ארוכה לחפש שיריות לפי שם השיר ואמן חפש מילים @@ -545,8 +588,10 @@ Second \"item\" text" נשמר הדגש מילים או הברות מסונכרנות תעדף מילים מילה-מילה כשהן זמינות. כשמילות השיר כוללות תזמון מילים, הדגש כל מילה או הברה בקצב המוזיקה - הצג ווקאלים ברקע בנפרד - מעביר ווקאל רקע הכתוב בסוגריים, כמו \"(או) אני רוצה אותך\", מקו השיר לקו עצמאי בעוצמה נמוכה יותר + כווץ קווים לא פעילים + מציר כל קו שהוא לא באופן זמני שרה קצת יותר קטן, מעל לעמעום אותו + הצג שירה ברקע בנפרד + מפריד בין שירת רקע והרמוניה לקו הראשי. מניח שירה ברקע שנכתבה בסוגריים, כמו \"(Oh) אני רוצה אותך\", מחוץ לקו שרים לקו עצמו מפויס הסתר מילים שכבר נוגנו הסתר שורות מילים מעל השורה המושמעת כעת הסתר מילים שטרם נוגנו @@ -604,10 +649,15 @@ Second \"item\" text" תורגם על ידי %s רומניזציה על ידי %s רומניזה על ידי %s + הועבר ע\"י Rime (%s) תרגום שפת התרגום - שפת רומניזציה + שפה רומניזציה או האברה + "שפת יעד לתרגום: +%s" + "שפת יעד לרומניזציה או העתקה פונטית: +%s" מתרגם… לא ניתן לתרגם את המילים רומניזציה @@ -621,7 +671,7 @@ Second \"item\" text" מרענן רוענן מפתח API של QuickLRC - QuickLRC מתרגם או מסתנכרן את המילים לרצועה הנוכחית. שירות נפרד, שאינו קשור לנקודת הקצה התואמת OpenAI הנ\"ל + QuickLRC מעתיק או מסדר זמן את המילים לפסי העכשווי מפתח QuickLRC נשמר. השתמש בכפתור הסנכרון בדיאלוג חיפוש המילים, או בהודעת הפאנל כאשר רצועה אין לה מילים מפתח API של QuickLRC נשמר QuickLRC לא קיבל את המפתח הזה @@ -637,58 +687,50 @@ Second \"item\" text" אין מילים לסנchronize - מתרגם במקום QuickLRC לא הצפה לעבד את השיר הזה קרדיטים של QuickLRC אזלו עבור השיר הזה - "Shiriot matchu be-shem v'arev, shironim matadata onyim imkin liltanot lo o et ha-versiya ha-haraga. Use refresh l'Shita versa'ot. Sfa'im mark * doresh token. Kol har-sifim h'amurim le'im yinsharu talmudim b'sederha, imkin lilshot ve'nishvu ba'zera'anam. + "שירים מתאימים לפי כותרת ואמן; מטא-נתונים חריגים עשויים לא להציג כלום או להציג גרסה שגויה. השתמש ברענון כדי להתחלף בין גרסאות. ספקים המסומנים ב-* דורשים אסימון. לא כל הספקים המופעלים נתבקשים לפי הסדר שלך, אלא מגיבים לפי מהירותם. -Shiriot ba'am hashlshimiyim she'em lo mitkadimim, mispansim, lo menuchsim by Morphe. Each one is used as it is: the lyrics, their availability, and their licensing stay with those services and their licensors, and using them can be subject to their terms of service. What reaches an enabled service is only the lookup you asked for - the title, the artist, and, where that service needs it, the video ID. The optional AI endpoint and QuickLRC transcription run only after you configure them, and receive the lyrics text or the track's video URL." +שירים מגיעים משירותים של צד שלישי שאינם שייכים, ממומנים או מאושרים על ידי Morphe. כל אחד מהם משמש כפי שהוא: השירים, זמינותם ורישיונם נשארים בידי השירותים הללו והמעניקים את הרישיון, והשימוש בהם עשוי להיות כפוף לתנאי השימוש שלהם. מה שמגיע לשירות מופעל הוא רק ביקשת החיפוש שביקשת - הכותרת, האמן, וכאשר שירות זה זקוק לכך, מזהה הווידאו. נקודת הקצה האופציונלית של הבינה המלאכותית ותמלול QuickLRC פועלים רק לאחר שתכניס אותם לתצורה, ומקבלים את טקסט השיר או כתובת ה-URL של וידאו הרצועה." הפעל רק את הספקים שבהם אתה משתמש, או שמור על ברירת המחדל - בקשות מתמשכות יכולות להעמיס על אתרי מילים לקהילות קטנות הסתר שורת קרדיט הסתר את שורת הקרדיט המוצגת מתחת למילות השיר החלף בין תרגום לרומניזציה - במצב ON הרומניזציה מופיעה מתחת למילות השיר והתרגום מעל; במצב OFF הרומניזציה נשארת מעל והתרגום מתחת - תרגום - רומניזציה - ספק תרגום - Google (ברירת מחדל, חינם) - תואם OpenAI (מותאם אישית) - ספק רומניזציה - Google (ברירת מחדל) - מילון מקומי / לא מקוון - תואם OpenAI (מותאם אישית) - חלק AI - משמש רק כאשר הספק אינו מספק תוויות חלקים כגון פזמון או בית, אותן המודל התואם ל-OpenAI שלך יזהה - AI & Cloud Endpoints - הגדר מפתחות API, מודלים, נקודות קצה ותבניות הנחיה עבור ספקי AI. - כתובת URL של נקודת קצה של API (לדוגמה https://api.openai.com/v1/chat/completions) + ON מציב רומניזציה או האברה מתחת למילים ותרגום מעל; OFF שומר רומניזציה או האברה מעל ותרגום מתחת. הגייה שמוצבת מעל כל צליל זזה איתה, מהמעל לצליל עד מתחת לצליל + תמיד למרכז מילים + ON מציב כל קו באמצע ולא מתעלם מהיישור המקורי, כולל צדדי דואט; OFF שומר על היישור שהמילים מבקשות + חייב להישאר לפחות ספק אחד פעיל + תרגום + רומניזציה או האברה + מילון חינתי + רשימת מילים של Rime + גלה קטעים עם AI + משמש רק כאשר הספק לא מכיל תוויות קטע כמו Chorus או Verse, שהדגם התואם OpenAI שלך מאתר לאחר מכן. עריכת השורה פותחת גם את הפוקט שלו + URL נקודת API (למשל https://api.openai.com/v1/chat/completions, https://api.anthropic.com/v1/chat/completions, https://generativelanguage.googleapis.com/v1beta/openai/chat/completions) תבנית הנחיה ממלאי מקום: {language}, {task}, {title}, {artist}, {count}, {lines} ההנחיה נשמרה ההנחיה נשמרה, אך נקודת הקצה של AI אינה ניתנת לשימוש עדיין. הגדר אותה תחת הגדרות AI ראשון ההנחיה אופסה - Lo valid JSON. Takanhu o nuke ha-shida leshanor sha\'mar הגדרות תרגום & רומניזציה נשמרו הגדרות אופסו לברירת מחדל כתובת ה-URL הבסיסית חייבת להתחיל ב-http:// או ב-https:// - בדיקת חיבור - הגדרת נקודות קצה - הצג את כל נקודות הקצה + הגדרות חסרות או נדחו על ידי הספק מאמת חיבור… - ספק תרגום - ספק רומניזציה - הבאת מודלים + %1$s: מחובר! (%2$s) + %1$s: חיבור נכשל. בדוק את ההגדרות. + הזן את מפתח ה-API קודם מביא מודלים... בחר מודל נכשל השליפה של המודלים. בדוק את מפתח ה-API או את החיבור. - הגדרות Gemini API - תואם OpenAI (נקודת קצה מותאמת אישית) - הגדרות DeepL API - מפתח API + פקודה מודל URL בסיסי + בחר שירות מפתח API (אופציונלי) - הנחיית תרגום גוף בקשה נוסף (JSON אופציונלי) + כותרות בקשה נוספות (JSON אפשרי) + הקף שמות וערכים בגרשיים, למשל {\"api-key\": \"abc\"}. ערך לא מוקף המכיל / : או מספר יידחה. ערך ריק מוריד כותרת. + הקף שמות וערכים בגרשיים, למשל {\"temperature\": 0.3}. ערך לא מוקף המכיל / או : יידחה. מודל והודעות לא יכולים להיות מוגדרים כאן. מפתח אימות (חינמי או Pro) - הגדרת מפתחות API, מודלים, נקודות קצה והנחיות 8.23.51 - השבת אייקונים מודגשים 9.13.51 - שחזור לשוניות הבא בתור, מילים, וקשורות בנגן diff --git a/patches/src/main/resources/addresources/values-iw-rIL/youtube/strings.xml b/patches/src/main/resources/addresources/values-iw-rIL/youtube/strings.xml index 96653aa873..f08dfc7911 100644 --- a/patches/src/main/resources/addresources/values-iw-rIL/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-iw-rIL/youtube/strings.xml @@ -29,6 +29,8 @@ Second \"item\" text" שונות סרטון שחזר את מסך הגדרות YouTube הישן + הצג הודעות ב-snackbar של YouTube + הודעות Morphe, כמו אישור כפתור נגן, מוצגות ב-snackbar של YouTube במקום חלונית קופצת הסר הגבלות השמעת רקע השבת הפעלת רקע diff --git a/patches/src/main/resources/addresources/values-ja-rJP/music/strings.xml b/patches/src/main/resources/addresources/values-ja-rJP/music/strings.xml index 039e458454..88a97d3877 100644 --- a/patches/src/main/resources/addresources/values-ja-rJP/music/strings.xml +++ b/patches/src/main/resources/addresources/values-ja-rJP/music/strings.xml @@ -462,12 +462,47 @@ SABR再生(動画ストリームのスプーフがオフ)" 表示 同期 パネル - AI エンドポイント すべてのプロバイダーが無効 - 内蔵歌詞と埋め込み歌詞 プロバイダーの順序をリセットしますか? プロバイダーを既定の優先順位に戻します + Rime 語彙リスト + "Rime 単語リストには UniDict も必要です。オフライン辞書プロバイダーで「中国語および広東語文字 (UniDict)」にチェックを入れて、ダウンロードが完了するまでお待ちください。これがなければ、文字を読み取るリストは読み方を返しません。 + +Rime 単語リストが読み取るリストにチェックを入れ、[保存] をタップしてダウンロードしてください。追加したリストはチェックを外すと削除されます。 + +各リストは独自の一対一の単語から読み方へのテーブルであり、.dict.yaml ファイルは次のような行になります: +字 ham +典 din2 +最初の列が単語、2 番目の列がその読み方です;タブがない余分な列や行は無視されます。" + 上記の言語では Rime 語彙リストがチェックされていません。これをタップしてリストをチェックするか、リストがある他の言語を選択してください + 追加 + Rime 語彙リストを追加 + 字 ham や 典 din2 のような行を含む .dict.yaml ファイル、またはそれを含む zip ファイル。リストはチェックを入れて [保存] をタップすると取得され、ライセンスは確認用にご自由にどうぞ。 + 言語コード + hakやwuuのように、2〜3文字 + 名称 + 読みの上に显示されるのは、客家語(梅県)のような形式です + 語彙リストのアドレス + .dict.yaml またはその zip ファイルの https:// アドレス + 単語に含まれない文字 + 自動 + 広東語文字 + 標準語文字 + 文字の読みなし + コード、名称、アドレスを入力してください + コードはhakのように2〜3文字である必要があります + アドレスは https:// で始まる必要があります + 名称とアドレスには縦棒を含めることはできません + %1$s は既に単語テーブルを持っています + %1$s は組み込みのローマ字変換器によって処理され、語彙リストを取ることができません + 未使用:この言語は既に独自の読みを持っているため、チェックを外して削除してください + %1$s を追加しました - チェックして保存をタップしてダウンロードしてください + 語彙リスト準備完了 + 語彙リスト準備完了(%1$d エントリ) + 読取中: %1$d%% + 語彙リストのダウンロードに失敗しました - 再試行するには保存をもう一度タップしてください 保存された歌詞を消去 - このデバイスに保存されている歌詞を削除します + このデバイスに保持されている歌詞の %1$s MB を削除します %1$s MB を消去しました トークンを取得 Apple Music media-user-token @@ -520,14 +555,22 @@ SABR再生(動画ストリームのスプーフがオフ)" コピーボタンを表示 タップして元の歌詞をプレーンテキストとしてコピー、長押しで歌詞ソースファイルをダウンロードディレクトリに保存 翻訳ボタンを表示 - タップすると翻訳を表示 / 非表示にします。長押しすると翻訳のみを表示するモードに切り替えます。翻訳には、歌詞配信サービスが提供する翻訳、Google 翻訳、または OpenAI 互換 API を使用します - ローマ字化ボタンを表示 - タップしてローマ字表記を切替、長押しでローマ字表記のみ使用。プロバイダーのローマ字表記、Google翻訳、またはOpenAI互換APIを使用 - ローカルローマ字表記とアライメント - プロバイダー、ユーザーモデル、Google翻訳が失敗した場合にデバイス上でローマ字表記を行います。読みは時間付き歌詞の各単語に従います;単語同期が有効である必要があります。中国語、広東語、日本語、朝鮮語、ギリシャ語、ロシア語、ジョージア語、アルメニア語対応;約120 MB - %1$s を読み込み中: %2$d%% + タップして翻訳を切り替えます;長押しすると翻訳のみ表示モードになります。有効になっている翻訳プロバイダーを登録順に使用します + ローマ字変換または翻記ボタンを表示 + タップしてローマ字変換または翻記を切り替えます;長押しするとローマ字変換または翻記のみ表示モードになります。有効になっているローマ字変換プロバイダーを登録順に使用します + 翻記を有効にする + 歌詞ソースからの非ラテン文字の読みを許可します;無効にすると、ラテン文字、アクセント付き文字、声調数字のみを残します + タイマー歌詞(ワード同期オン)の各単語を読み取ります;韓国語にはダウンロード不要;14種類のスクリプトで1つのCLDRダウンロードを共有します;漢字は、Rime語彙リストの単語テーブルが対象言語をカバーしている場合に読み取られます + 読取辞書をダウンロードしています… 読みデータ準備完了 (%1$d エントリ) ダウンロードに失敗しました。再試行するにはこの設定を一旦オフにしてから再度オンにしてください + オフライン辞書 + 日本語(SudachiDict、約75 MB) + 中国語・広東語文字(UniDict、約8 MB) + 中国語単語(pinyin-pro、約17 MB) + ギリシャ語、スラヴ系キリル文字(ロシア語、ウクライナ語、ブルガリア語、セルビア語、ベラルーシ語、マケドニア語を含む)、ジョージア語、アルメニア語、ヘブライ語、アラビア語、アムハラ語、シリア語の翻記(CLDR、約0.5 MB) + %1$s を読み込み中: %2$d%% + 少なくとも1つの辞書を選択してください 更新ボタンを表示 タップして次の歌詞候補に切り替え、長押しで曲名とアーティストから歌詞を検索 歌詞を検索 @@ -538,8 +581,10 @@ SABR再生(動画ストリームのスプーフがオフ)" 保存しました 同期中の単語や音節をハイライト表示 利用可能な場合は単語ごとのタイミング情報を含む歌詞を優先します。単語ごとのタイミング情報を含む歌詞を使用する場合は、楽曲に合わせて各単語または音節をハイライト表示します + 非アクティブな行を縮小 + 現在歌われていない各行を少し小さく描画し、さらに暗く表示します バックコーラスを分離して表示 - 「(Oh) I want you」のように括弧で囲まれたバックコーラスを、歌詞の行から分離し、専用の独立した行に薄く表示します + 背景ボーカルとハーモニーをリードラインから分離します。「(Oh) I want you」のように括弧で書かれた背景ボーカルを、メインの歌詞行から分離し、独自の暗い行に移動します 再生済みの歌詞を非表示 現在再生中の行より上に表示される歌詞を非表示にします 未再生の歌詞を非表示 @@ -597,10 +642,15 @@ SABR再生(動画ストリームのスプーフがオフ)" 翻訳: %s ローマ字: %s ローマ字: %s + Rime によるローマ字表記(%s) 歌詞が見つかりません 翻訳言語 - ローマ字変換言語 + ローマ字変換または翻記の言語 + "翻訳先の言語: +%s" + "ローマ字表記または翻音・転写先の言語: +%s" 翻訳中… 歌詞を翻訳できませんでした 歌詞が見つかりません @@ -614,7 +664,7 @@ SABR再生(動画ストリームのスプーフがオフ)" 更新中 更新済み QuickLRC APIキー - QuickLRCは現在のトラックの歌詞を文字起こしまたはタイムアラインします。上記のOpenAI互換エンドポイントとは無関係な別のサービスです + QuickLRC は現在のトラックの歌詞を文字起こしまたは時間軸合わせを行います QuickLRCキーが保存されました。歌詞検索ダイアログのアラインボタン、またはトラックに歌詞がない場合のパネルプロンプトで使用してください QuickLRC APIキーが保存されました QuickLRCがそのキーを受け入れませんでした @@ -630,58 +680,50 @@ SABR再生(動画ストリームのスプーフがオフ)" アラインする歌詞がありません - 代わりに文字起こししています QuickLRCはこのトラックを処理できませんでした QuickLRCのクレジットがこのトラックに対して使い切られました - "歌詞はタイトルとアーティストでマッチングされます。珍しいメタデータの場合、見つからないか間違ったバージョンになることがあります。バージョンを切り替えるには更新を使用してください。*マークのあるプロバイダーにはトークンが必要です。すべての有効なプロバイダーが常に順番に要求されるわけではありませんが、速度に基づいて応答します。 + "歌詞はタイトルとアーティストでマッチングされます。通常のメタデータ以外では何も見つからない場合や、間違ったバージョンが見つかる場合があります。バージョンを切り替えるには更新を使用してください。* マーク付きのプロバイダーにはトークンが必要です。有効化されたすべてのプロバイダーが常にリクエスト順に含まれるわけではありませんが、速度に基づいて応答します。 -歌詞はMorpheとは関係のない、後援も認可もされていないサードパーティサービスから提供されています。それぞれは現状で使用されます:歌詞、その利用可能性、ライセンスはそれらのサービスおよびその許諾者に留まり、それらを使用することはサービスの利用規約の対象となる場合があります。有効化されたサービスに送信されるのは、あなたが要求した検索情報だけです - タイトル、アーティスト、そして必要に応じてビデオID。任意のAIエンドポイントとQuickLRC文字起こしは設定後にのみ実行され、歌詞テキストまたはトラックのビデオURLを受信します。" +歌詞は Morphe に関係ない、スポンサーでもない、推奨もされていないサードパーティーサービスからのものです。それぞれのサービスはそのまま使用されます:歌詞、その利用可能性、ライセンスはすべてサービス提供元とその許諾元に属し、これらを使用することはサービスの利用規約の対象となる場合があります。有効化されたサービスに送信されるのは、検索要求のみです(タイトル、アーティスト、そのサービスが必要とする場合はビデオ ID)。オプショナルな AI エンドポイントと QuickLRC 文字起こしは、設定を行った後にのみ実行され、受け取る情報は歌詞テキストまたはトラックの動画 URL です。" 使用中のプロバイダーのみ有効にするか、デフォルトのままにしてください。定期的なリクエストにより小規模なコミュニティの歌詞サイトが overload する可能性があります クレジット行を非表示 歌詞欄最下部の、作詞作曲などのクレジット情報を表示する行を非表示にします 翻訳とローマ字表記を入れ替える - オン: 歌詞の下にローマ字表記を、上に翻訳を表示します。オフ: 歌詞の下に翻訳を、上にローマ字表記を表示します - 翻訳 - ローマ字表記 - 翻訳サービス - Google (デフォルト、無料) - OpenAI 互換 (カスタム) - ローマ字表記サービス - Google (デフォルト) - ローカル / オフライン辞書 - OpenAI 互換 (カスタム) - AI セクション - プロバイダーが「サビ」や「Aメロ」などのセクションラベルを提供せず、OpenAI 互換モデルがそれを検出する場合にのみ使用されます - AI & クラウドエンドポイント - AIプロバイダーのAPIキー、モデル、エンドポイント、およびプロンプトテンプレートを設定します。 - API エンドポイント URL (例: https://api.openai.com/v1/chat/completions) + ON にすると、ローマ字変換または翻記が歌詞の下、翻訳が上に配置されます;OFF にすると、ローマ字変換または翻記が上、翻訳が下になります。各音節の上に配置された読みはそれと一緒に移動し、音節の上から下に移動します + 常に歌詞を中央揃え + ONにすると、各行を中央に配置し、ソースの配置(デュエットのサイドを含む)を無視します;OFFにすると、歌詞が要求する配置を保持します + 少なくとも1つのプロバイダーをONにしておく必要があります + 翻訳 + ローマ字変換または翻記 + オフライン辞書 + Rime 語彙リスト + AI でセクションを検出 + プロバイダーがコーラスやヴァースなどのセクションラベルを提供しない場合にのみ使用されます。その後、あなたの OpenAI 互換モデルが検出します。行を編集するとプロンプトも開きます + API エンドポイント URL(例:https://api.openai.com/v1/chat/completions, https://api.anthropic.com/v1/chat/completions, https://generativelanguage.googleapis.com/v1beta/openai/chat/completions) プロンプト テンプレート プレースホルダー: {language}, {task}, {title}, {artist}, {count}, {lines} プロンプトを保存しました プロンプトは保存されましたが、AIエンドポイントはまだ使用できません。まずAI設定で構成してください プロンプトをリセットしました - 無効なJSONです。保存前に修正するかフィールドをクリアしてください 翻訳 & ローマ字表記の設定を保存しました 設定をデフォルトにリセットしました ベース URL は http:// または https:// で始まる必要があります - 接続テスト - エンドポイントを設定 - すべてのエンドポイントを表示 + 設定が不完全またはプロバイダーによって拒否されました 接続を確認中… - 翻訳サービス - ローマ字表記プロバイダー - モデルを取得 + %1$s:接続済み!(%2$s) + %1$s:接続に失敗しました。設定を確認してください。 + まず API キーを入力してください モデルを取得中… モデルを選択してください モデルの取得に失敗しました。APIキーまたは接続を確認してください。 - Gemini API設定 - OpenAI互換(カスタムエンドポイント) - DeepL API設定 - APIキー + プロンプト モデル ベースURL + サービスを選択 APIキー(オプション) - 翻訳プロンプト 追加のリクエストボディ(オプションのJSON) + 追加のリクエストヘッダー(オプションの JSON) + 名前と値を引用符で囲んでください。例:{\"api-key\": \"abc\"}。引用符のない / : または数字を含む値は拒否されます。空の値はヘッダーを削除します。 + 名前と値を引用符で囲んでください。例:{\"temperature\": 0.3}。引用符のない / または : を含む値は拒否されます。モデルとメッセージはここで設定できません。 認証キー(FreeまたはPro) - APIキー、モデル、エンドポイント、およびプロンプトを設定します 8.23.51 - アイコンの太字表示を無効化 9.13.51 - プレーヤーの「次のコンテンツ」「歌詞」「関連コンテンツ」タブを復元 diff --git a/patches/src/main/resources/addresources/values-ja-rJP/youtube/strings.xml b/patches/src/main/resources/addresources/values-ja-rJP/youtube/strings.xml index 31f313a876..9586638f44 100644 --- a/patches/src/main/resources/addresources/values-ja-rJP/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ja-rJP/youtube/strings.xml @@ -29,6 +29,8 @@ Second \"item\" text" その他 動画 YouTube の設定メニューを旧仕様に戻す + YouTubeのsnackbarにメッセージを表示 + プレイヤーボタンの確認など、Morpheのメッセージはポップアップの代わりにYouTubeのsnackbarで表示されます バックグラウンド再生の制限を解除 ショートのバックグラウンド再生を無効化 diff --git a/patches/src/main/resources/addresources/values-ka-rGE/music/strings.xml b/patches/src/main/resources/addresources/values-ka-rGE/music/strings.xml index aa9c984bcd..d39cebf979 100644 --- a/patches/src/main/resources/addresources/values-ka-rGE/music/strings.xml +++ b/patches/src/main/resources/addresources/values-ka-rGE/music/strings.xml @@ -462,12 +462,10 @@ SABR დაკვრა (ვიდეო ნაკადების იმი ჩვენება სინქრონიზაცია პანელი - AI ენდპოინტი ყველა პროვაიდერი გათიშულია - ჩაშენებული და ინტეგრირებული ტექსტები მ sağlayებლების თანმიმდევრობის განახლება? მ sağlayებლების პრიორიტეტების აღდგენა ნაგულისხმევ მდგომარეობაში შენახული ტექსტების გასუფთავება - წაშალეთ ამ მოწყობილობაზე შენახული ტექსტები გასუფთავებულია %1$s მბ ტოკენის მიღება Apple Music Media-user-token @@ -527,14 +525,9 @@ SABR დაკვრა (ვიდეო ნაკადების იმი კოპირების ღილაკის ჩვენება შეეხეთ ორიგინალი ტექსტის ჩვეულებრივ ტექსტად დასაკოპირებლად; გრძლად დააჭირეთ ტექსტის საწყის ფაილს ჩამოტვირთვებში შესანახად თარგმნის ღილაკის ჩვენება - დააჭირეთ თარგმანის ჩასართავად/გამოსართავად; გრძელვად დააჭირეთ მხოლოდ თარგმანისთვის. გამოიყენება პროვაიდერის თარგმანები, Google Translate ან OpenAI-ს თავსებადი API - რომაიზაციის ღილაკის ჩვენება - დააჭირეთ რომანიზაციის ჩასართავად/გამოსართავად; გრძელვად დააჭირეთ მხოლოდ რომანიზაციისთვის. გამოიყენება პროვაიდერის რომანიზაცია, Google Translate ან OpenAI-ს თავსებადი API - ლოკალური რომანიზაცია და გასწორება - გახელვეთ რომანიზაცია მოწყობილობაზე, როდესაც პროვაიდერი, თქვენი მოდელი და Google Translate ვერ იმუშავებენ. წაკითხვა მიყვება სინქრონიზებული ტექსტის თითოეულ სიტყვას; სიტყვების სინქრონიზაცია უნდა იყოს ჩართული. ჩინური, კანტონური, იაპონური, კორეული, ბერძნული, რუსული, ქართული, სომხური; დაახლ. 120 მბ - კითხვა %1$s: %2$d%% მონაცემები მზადაა (%1$d ჩანაწერი) ჩამოტვირთვა ვერ მოხერხდა - სცადეთ ხელახლა ამ ფუნქციის გამორთვა და ჩართვა + კითხვა %1$s: %2$d%% განახლების ღილაკის ჩვენება დააჭირეთ შემდეგი ტექსტის ვარიანტისთვის; გრძელვად დააჭირეთ ტექსტის მოსაძებნად სიმღერის სათაურისა და შემსრულებლის მიხედვით ტექსტების ძიება @@ -605,7 +598,6 @@ SABR დაკვრა (ვიდეო ნაკადების იმი თარგმნილია თარგმანის ენა - რომანიზაციის ენა თარგმნა... სათytuላო ტექსტის თარგმნა ვერ მოხერხდა რომანიზებულია @@ -619,7 +611,6 @@ SABR დაკვრა (ვიდეო ნაკადების იმი განახლება განახლებულია QuickLRC API გასაღები - QuickLRC ახდენს ტრეკის ტექსტის ტრანსკრიფციას ან დროში გასწორებას. ეს არის ცალკე სერვისი, რომელიც არ არის დაკავშირებული ზემოთ მოცემულ OpenAI-ს თავსებად ენდპოინტთან QuickLRC გასაღები შენახულია. გამოიყენეთ გასწორების ღილაკი ტექსტის ძიების დიალოგში, ან პანელის შეტყობინება, როდესაც ტრეკს არ აქვს ტექსტი QuickLRC API გასაღები შენახულია QuickLRC-მა არ მიიღო ეს გასაღები @@ -635,56 +626,27 @@ SABR დაკვრა (ვიდეო ნაკადების იმი სინქრონიზაციისთვის ტექსტი არ მოიპოვება - ტრანსკრიპირება მიმდინარეობს QuickLRC-მა ვერ დაამუშავა ეს ტრეკი ამ ტრეკისთვის QuickLRC-ის კრედიტები ამოწურულია - "ტექსტები სრულდება სათაურისა და შემსრულებლის მიხედვით; უჩვეულო მეტამონაცემებმა შესაძლოა არაფერი იპოვოს ან არასწორი ვერსია მოიძიოს. ვერსიების შესაცვლელად გამოიყენეთ განახლება. * ნიშნით მონიშნული პროვაიდერები საჭიროებენ ტოკენს. ყველა ჩართული პროვაიდერი ყოველთვის თქვენს მიერ განსაზღვრული თანმიმდევრობით არ მოითხოვება, არამედ პასუხობენ მათი სისწრაფის მიხედვით.\n\nტექსტები მოდის მესამე მხარის სერვისებიდან, რომლებიც არ არიან დაკავშირებულნი Morphe-თან, არ არიან მისი სპონსორები ან რეკომენდებული. თითოეული მათგანი გამოიყენება ისე, როგორც არის: ტექსტები, მათი ხელმისაწვდომობა და ლიცენზირება რჩება ამ სერვისებსა და მათ ლიცენზიარებს, ხოლო მათი გამოყენება შესაძლოა ექვემდებარებოდეს მათ მომსახურების პირობებს. ჩართულ სერვისამდე აღწევს მხოლოდ თქვენ მიერ მოთხოვნილი მოძიება - სათაური, შემსრულებელი და, იქ სადაც სერვისს ეს სჭირდება, ვიდეოს ID. სურვილისამებრ AI ენდპოინტი და QuickLRC ტრანსკრიფცია მუშაობს მხოლოდ თქვენი კონფიგურაციის შემდეგ და იღებს ტექსტის ტექსტს ან ტრეკის ვიდეო URL-ს." გააქტიურეთ მხოლოდ ის პროვაიდერები, რომლებსაც იყენებთ, ან დატოვეთ ნაგულისხმევი პარამეტრები - მუდმივმა მოთხოვნებმა შესაძლოა გადატვირთოს პატარა საზოგადოებო ტექსტების საიტები მფლობელის ხაზის დამალვა დამალეთ ტექსტის ქვემოთ მოცემული მფლობელის ხაზი თარგმანისა და რომანიზაციის ადგილების შეცვლა - ON განათავსებს რომანიზაციას ტექსტის ქვემოთ და თარგმანს ზემოთ; OFF ტოვებს რომანიზაციას ზემოთ და თარგმანს ქვემოთ - თარგმანი - რომანიზაცია - თარგმანის პროვაიდერი - Google (ნაგულისხმევი, უფასო) - OpenAI თავსებადი (საკუთარი) - რომანიზაციის პროვაიდერი - Google (ნაგულისხმევი) - ლოკალური / ოფლაინ ლექსიკონი - OpenAI თავსებადი (საკუთარი) - AI სექცია - გამოიყენება მხოლოდ მაშინ, როდესაც პროვაიდერი არ გვიგზავნის სექციის ეტიკეტებს, როგორიცაა Chorus ან Verse, რომლებსაც შემდეგ თქვენი OpenAI-ს თავსებადი მოდელი აღმოაჩენს - AI & ღრუბლოვანი Endpoints-ები - გააკონფიგურირეთ API გასაღებები, მოდელები, endpoint-ები და prompt-ის შაბლონები AI პროვაიდერებისთვის. - API endpoint-ის URL (მაგ. https://api.openai.com/v1/chat/completions) პრომპტის შაბლონი პლეისჰოლდერები: {language}, {task}, {title}, {artist}, {count}, {lines} პრომპტი შენახულია პრომპტი შენახულია, მაგრამ AI endpoint ჯერჯერობით მიუწვდომელია. ჯერ დააკონფიგურირეთ იგი AI პარამეტრებში პრომპტი გასუფთავდა - არასწორი JSON. გამოსწორეთ ან გასუფთავეთ ველი შენახვამდე თარგმნისა & რომანიზაციის პარამეტრები შენახულია პარამეტრები განახლდა ნაგულისხმევ მდგომარეობაში ბაზისური URL უნდა იწყებოდეს http:// ან https:// - კავშირის შემოწმება - Endpoint-ების კონფიგურაცია - ყველა End-point-ის ჩვენება კავშირის ვალიდაცია… - თარგმანის პროვაიდერი - რომანიზაციის პროვაიდერი - მოდელების წამოღება მოდელების ჩატვირთვა… აირჩიეთ მოდელი მოდელების ჩატვირთვა ვერ მოხერხდა. შეამოწმეთ API გასაღები ან კავშირი. - Gemini API პარამეტრები - OpenAI თავსებადი (საკუთარი End-point) - DeepL API პარამეტრები - API გასაღები მოდელი ბაზისური URL API გასაღები (არასავალდებულო) - თარგმანის პრომპტი მოთხოვნის დამატებითიტექსტი (არასავალდებულო JSON) ავთენტიფიკაციის გასაღები (Free ან Pro) - API გასაღებების, მოდელების, ენდპოინტების და პრომპტების კონფიგურაცია 8.23.51 - სქელი ხაზის ღილაკების გამორთვა 9.13.51 - აღდგინეთ პლეერის შემდეგი, ტექსტებისა და დაკავშირებული ჩანართები diff --git a/patches/src/main/resources/addresources/values-kk-rKZ/music/strings.xml b/patches/src/main/resources/addresources/values-kk-rKZ/music/strings.xml index aecaaca051..370c9e7ce9 100644 --- a/patches/src/main/resources/addresources/values-kk-rKZ/music/strings.xml +++ b/patches/src/main/resources/addresources/values-kk-rKZ/music/strings.xml @@ -461,12 +461,10 @@ SABR ойнатылымы (Видео ағындарын имитациялау Көрсету Синхрондау Панель - AI endpoint Барлық провайдерлер өшірілді - кірістірілген мәтіндер және ішкі мәтіндер Провайдерлер ретін sıфрлау? Провайдерлерді әдепкі басымдық ретіне қайтару Сақталған мәтіндерді тазалау - Осы құрылғыда сақталған мәтіндерді жойыңыз %1$s МБ тазартылды Токен алу Apple Music Media-user-token @@ -526,13 +524,8 @@ SABR ойнатылымы (Видео ағындарын имитациялау Көшіру түймесін көрсету Түпнұсқа мәтінді қарапайым мәтін ретінде көшіру үшін басыңыз; мәтіннің бастапқы дереккөзі файлын «Жүктеулер» бөліміне сақтау үшін ұзақ басыңыз Аудару түймесін көрсету - Аударманы қосу/өшіру үшін басыңыз; тек аударма үшін ұзақ басыңыз. Провайдер аудармалары, Google Translate немесе OpenAI-мен үйлесімді API қолданылады - Романизация түймесін көрсету - Романизацияны қосу/өшіру үшін басыңыз; тек романизация үшін ұзақ басыңыз. Провайдер романизациясы, Google Translate немесе OpenAI-мен үйлесімді API қолданылады - Жергілікті романизация және туралау - Провайдер, сіздің моделіңіз және Google Translate істемеген жағдайда құрылғыда романизация жасаңыз. Оқу уақыты бойынша синхрондалған ән мәтінінің әр сөзінен кейін жүреді; сөздерді синхрондау қосулы болуы керек. Қытай, кантон, жапон, корей, грек, орыс, грузин, армян тілдері; шамамен 120 МБ - Оқылуы %1$s: %2$d%% Жүктеу сәтсіз аяқталды - қайталау үшін бұл функцияны өшіріп, қайта қосыңыз + Оқылуы %1$s: %2$d%% Жаңарту түймесін көрсету Келесі мәтін нұсқасына ауысу үшін түртіңіз; ән атауы мен орындаушы бойынша мәтінді іздеу үшін ұзақ басыңыз Мәтіндерді іздеу @@ -603,7 +596,6 @@ SABR ойнатылымы (Видео ағындарын имитациялау Аударылды Аударма тілі - Романизация тілі Аударылуда… Мәтінді аудару мүмкін болмады Романизацияланды @@ -617,7 +609,6 @@ SABR ойнатылымы (Видео ағындарын имитациялау Жаңартылуда Жаңартылды QuickLRC API кілті - QuickLRC ағымдағы трек үшін мәтінді транскрипциялайды немесе уақыт бойынша сәйкестендіреді. Бұл жоғарыдағы OpenAI-мен үйлесімді endpoint-ке қатысы жоқ бөлек сервис QuickLRC кілті сақталды. Мәтінді іздеу диалогындағы сәйкестендіру түймесін немесе тректе мәтін болмаған жағдайда панельдік нұсқауды қолданыңыз QuickLRC API кілті сақталды QuickLRC бұл кілтті қабылдамады @@ -633,56 +624,27 @@ SABR ойнатылымы (Видео ағындарын имитациялау Сәйкестендіретін мәтін жоқ - оның орнына транскрипциялау жүзеге асырылуда QuickLRC бұл тректі өңдей алмады Бұл трек үшін QuickLRC кредиттері таусылды - "Мәтіндер атауы мен орындаушысы бойынша сәйкестендіріледі, ерекше метадеректер ештеңе таппауы мүмкін немесе қате нұсқаны көрсетуі мүмкін. Нұсқаларды ауыстыру үшін жаңартуды қолданыңыз. * белгісімен белгіленген провайдерлерге токен қажет. Барлық қосылған провайдерлер әрдайым сіздің ретіңізбен сұратылмайды, бірақ олардың жылдамдығына байланысты жауап береді.\n\nMәтіндер Morphe-мен байланысы жоқ, демеуленбеген немесе мақұлданбаған үшінші тарап қызметтерінен алынады. Әрқайсысы сол күйінде қолданылады: мәтіндер, олардың қолжетімділігі және лицензиялауы сол қызметтерде және олардың лицензиарларында қалады, ал оларды қолдану олардың қызмет көрсету шарттарына байланысты болуы мүмкін. Қосылған қызметке тек сіз сұраған іздеу мәліметтері ғана жетеді - атауы, орындаушысы және, егер сол қызметке қажет болса, бейне ID-і. Опционалды AI нүктесі және QuickLRC транскрипциясы тек сіз оларды баптағаннан кейін жұмыс істейді және мәтін мәтінін немесе тректің бейне URL-мекенжайын алады." Тек қолданатын провайдерлеріңізді қосыңыз немесе әдепкі мәнді қалдырыңыз - тұрақты сұраныстар кішігірім қоғамдық мәтін сайттарын шамадан тыс жүктеуі мүмкін Кредит жолын жасыру Мәтін төменінде көрсетілген кредит жолын жасыру Аударма мен романизацияны алмастыру - ҚОСИЛДЫ — романизация мәтіннен төмен, аударма жоғары орналасады; ӨШІРІЛДІ — романизация жоғары, аударма төмен қалады - Аударма - Романизация - Аударма провайдері - Google (Әдепкі, тегін) - OpenAI Compatible (Жеке) - Романизация провайдері - Google (Әдепті) - Жергілікті / Офлайн сөздік - OpenAI үйлесімді (Custom) - AI бөлімі - Тек провайдер «Қайырмасы» немесе «Куплет» сияқты бөлім белгілерін жібермеген жағдайда қолданылады, бұл белгілерді сіздің OpenAI-мен үйлесімді моделіңіз анықтайды - ЖИ & Бұлтты нүктелер - ЖИ провайдерлері үшін API кілттерін, модельдерді, нүктелерді және промпт шаблондарын баптаңыз. - API нүктесінің URL-мекенжайы (мысалы, https://api.openai.com/v1/chat/completions) Промпт үлгісі Орынтегімішілер: {language}, {task}, {title}, {artist}, {count}, {lines} Промпт сақталды Нұсқау сақталды, бірақ AI endpoint әлі қолжетімді емес. Алдымен оны AI баптаулары бөлімінде баптаңыз Промпт sıғыртылды - Жарамсыз JSON. Сақтамас бұрын оны түзетіңіз немесе өрісті тазалаңыз Аударма & романизация баптаулары сақталды Баптаулар әдепті мәнге қайтарылды Базалық URL http:// немесе https:// басталуы керек - Байланысты тексеру - Шегін точкаларын баптау - Барлық шегін точкаларын көрсету Байланыс тексерілуде… - Аударма провайдері - Романизация провайдері - Модельдерді алу Модельдер алынуда… Модельді таңдаңыз Модельдерді алу сәтсіз аяқталды. API кілтін немесе байланысты тексеріңіз. - Gemini API параметрлері - OpenAI-мен үйлесімді (Custom Endpoint) - DeepL API параметрлері - API кілті Модель Базалық URL API кілті (міндетті емес) - Аударма сұранысы Қосымша сұраныс денесі (міндетті емес JSON) Аутентификация кілті (Free немесе Pro) - API кілттерін, модельдерді, соңғы нүктелерді және сұраныстарды баптаңыз 8.23.51 - Қалың белгілерді өшіру 9.13.51 - Плеердің \"Келесі\", \"Мәтіндер» және «Ұқсас» қойындыларын қалпына келтіру diff --git a/patches/src/main/resources/addresources/values-kmr-rTR/music/strings.xml b/patches/src/main/resources/addresources/values-kmr-rTR/music/strings.xml index 256abc1697..ad204d8215 100644 --- a/patches/src/main/resources/addresources/values-kmr-rTR/music/strings.xml +++ b/patches/src/main/resources/addresources/values-kmr-rTR/music/strings.xml @@ -462,12 +462,11 @@ Ji bo ku bêyî astengiyê scrobbling li ser lîstikên paşnavê were hevdengki Nîşandan Senkronîzasyon Panel - Endpointê AI Hemû dabinkaran hatin betalkirin - lyricên avakirî û lyricên entegre Rêza dabinkeran vegerîne? Dabinkeran vegerîne rêza pêşîn a default + Otomatîk Gotinên jiyerî yên hatine tohîmkirin paqij bike - Gotincên li ser vê amûrê hatine parastin jê bibe %1$s MB hat paqijkirin Token bistîne Apple Music Media-user-token @@ -527,14 +526,9 @@ Kukiyên pêwîst: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPISI Nîşanê kopîkirinê bibîne Ji bo kopîkirina gotincên orîjînal wekî nivîsa sade betike; ji bo parastina dosyaya çavkaniyê ya gotincên xav li Downloads wê dirêj bigire Nîşanê wergerandinê bibîne - Ji bo guhertina wergerê betike; tenê ji bo wergerê wê dirêj bigire. Wergerên daberê, Google Translate, an APIyekî li gorî OpenAIyê bikar tîne - Bişkoja Romanize nîşan bide - Ji bo guhertina romanîzasyonê bitapîne; ji bo tenê romanîzasyonê dirêj bitapîne. Romanîzasyona dabinker, Google Translate, an APIyekî li gorî OpenAI bikar tîne - Romanîzasyon û Hevrêkirina Herî - Gava dabinker, modela we û Google Translate bi ser nekevin, li ser amûrê romanîzasyonê bike. Xwendin li pişt her peyvên gotinên demdar tê; hevsengiya peyvan divê vekirî be. Çînî, Kantonî, Japonî, Koreyî, Yewnanî, Rûsî, Gurucî, Ermenî; nêzîkî 120 MB - Xwendina %1$s: %2$d%% Xwendin amade ne (%1$d qedaran) Dawberdan bi ser neket - ji bo dubarekirinê vê yekê veguherîne ser \'off\' û paşê dîsa \'on\' bike + Xwendina %1$s: %2$d%% Bişkoja Nûkirinê nîşan bide Ji bo guhartina berhevê gotinên din bitapîne; ji bo lêgerîna gotinan bi navê stran û hunermend dirêj bitapîne Gotinên stranê bigere @@ -605,7 +599,6 @@ Li her du sînoran tê sepandin. Li ser 1.75, çar karakterên tam-berz wekî 7 Hat wergerandin Zimanê wergêranê - Zimanê romanîzasyonê Wergerandin… Nefibû ku nivîs were wergerandin Hat romanîzekirin @@ -619,7 +612,6 @@ Li her du sînoran tê sepandin. Li ser 1.75, çar karakterên tam-berz wekî 7 Nûkirinê Nûkirin Kliya API ya QuickLRC - QuickLRC gotinên stranê yên tracksê ya niha dinivîse an li gorî demê rast dike. Xizmeteke cuda ye, ku bi endpointa OpenAI ya li jor re tune ye Kêlîta QuickLRC hat save kirin. Butonka align a di dialoga lêgerîna gotinên stranê de, an jî agahdariya panê bikar bîne gava trackek gotinên stranê tune bin Kliya API ya QuickLRC hat parastin QuickLRC-ê ew kliya qebûl nekir @@ -635,56 +627,27 @@ Li her du sînoran tê sepandin. Li ser 1.75, çar karakterên tam-berz wekî 7 Tu gotinên stranê tune ne ku alignment lê were kirin - li şûma wê transcription tê kirin QuickLRC nikarî vê trackê proses bike Kredîtên QuickLRC ji bo vê trackê qediyan - "Gotinên stranê bi nav û hunermend re tên meşkirin, metadata ya neasayî dibe ku tiştekî nenîbe an jî versiyona şaş bibîne. Ji bo guhartina versiyonan 'refresh' bikar bîne. Dabinkeran ku bi * hatine nîşankirin tokenekî dixwazin. Hemû dabinkernên çalak her tim bi rêza we nayên daxwazkirin, lê li gorî leza xwe bersiv didin.\n\nGotinên stranê ji xizmetên ownê yên sêyem tên ku ne bi Morphe ve girêdayî ne, ne ji aliyê wê ve hatine sponzorkirin an pesendkirin. Her yek wekî ku heye tê bikaranîn: gotin, berdestbûna wan û lisansa wan bi van xizmetan û lisansdarkirên wan re dimîne, û bikaranîna wan dikare bibe mijara şert û mercên xizmetê yên wan. Tiştê ku digihîje xizmetekî çalak tenê ew lêgerîna we xwestiye ye - nav, hunermend, û li wê derê ku xizmetê pêdivî be, ID-ya vîdyoyê. Endpoint-a AI ya bijartî û transkripsiyona QuickLRC tenê piştî ku hûn wan konfîgure bikin, xebitînin û nivîsa gotinên stranê an URL-ya vîdyoya track-ê werdigirin." Tenê wan dabinkeran çalak bike yên ku tu bikar tînî, an jî yên pêşîn bihêle - daxwazên berdewam dikarin malperên lyrics ên piçûk ên civakî winda bikin Xuşana xeta kredê veşêre Hêla kredîtê ya ku li xwarê gotinên stranê tê nîşandan veşire Wergera û romanîzasyonê biguherîne - ON romanîzasyonê li jêr û wergerê li ser gotinên stranê datîne; OFF romanîzasyonê li ser û wergerê li jêr dihiştîne - Werger - Romanîzasyon - Dahêkerê wergerê - Google (Default, Belaş) - Li gorî OpenAI (Custom) - Dahêkerê romanîzasyonê - Google (Default) - Ferhenga Local / Offline - OpenAI Compatible (Custom) - Beşa AI - Tenê tê bikaranîn gava daberkar tu nîşanên beşan wekî Chorus an Verse neşîde, ku piştre modela we ya li gorî OpenAI wan tîne ziman - Endpointên AI & Cloud - Kîlleyên API, model, endpoint û şabloneyên promptê ji bo dabinkarên AI konfîgure bike. - URL-ê endpoint-ê API (mînak https://api.openai.com/v1/chat/completions) Şablonê Promptê Şûneke vekirî: {language}, {task}, {title}, {artist}, {count}, {lines} Prompt hat tomar kirin Prompt hat save kirin, lê endpointa AI hîşta neyê bikaranîn. Berê wê di Settingsên AI de configure bike Prompt hat vegerandin - JSON ne valid e. Berî savekirinê wê rast bike an qada wê paqij bike Mîhengên wergerandinê & Romanîzasyonê hatin savekirin Mîheng hatin vegerandin ser default URL-ê bingehîn divê bi http:// an https:// dest pê bike - Taqikirina Girêdanê - Endpoint-an Konfîgure Bike - Hemû Endpoint-an Nîşan Bide Gihîştan tê kontrolkirin… - Dahêkerê Wergerandinê - Dahêkerê Romanîzasyonê - Hênerîna Modelan Hêvîkirina modelan… Modelekê Hilbijêre Hêvîkirina modelan bi ser neket. Keya API-yê an jî girêdanê kontrol bike. - Mîhengên API-ya Gemini - Li gorî OpenAI (Endpoint-a Taybet) - Mîhengên API-ya DeepL - Keya API-yê Model URL-ya Bingehîn Kîleya API (valîye) - Promptê wergerê Laşê daxwaza zêde (JSON-ê valîye) Kîleya otentîkasyonê (Belaş an Pro) - Kîleyên API, model, endpoint û promptan konfîgur bike 8.23.51 - Îkonên stûr neçalak bike 9.13.51 - Vegerandina tabên Player ên \'Up next\', \'Lyrics\' û \'Related\' diff --git a/patches/src/main/resources/addresources/values-ko-rKR/music/strings.xml b/patches/src/main/resources/addresources/values-ko-rKR/music/strings.xml index de437a585e..f32543d0fd 100644 --- a/patches/src/main/resources/addresources/values-ko-rKR/music/strings.xml +++ b/patches/src/main/resources/addresources/values-ko-rKR/music/strings.xml @@ -462,12 +462,47 @@ SABR 프로토콜을 사용하여 재생하는 경우 (동영상 스트림 속 디스플레이 동기화 패널 - AI 엔드포인트 모든 제공자가 비활성화됨 - 내장된 가사 및 삽입된 가사 제공자 순서를 초기화하시겠습니까? 제공자를 기본 우선순위 순서로 복원합니다 + Rime 단어 목록 + "Rime 단어 목록에도 UniDict가 필요합니다: 오프라인 사전 제공자에서 \"중국어 및 광둥어 문자(UniDict)\"를 선택하고 먼저 다운로드가 완료되도록 하세요. 그렇지 않으면, 문자 읽기 목록은 아무것도 읽지 않습니다. + +Rime 단어 목록이 읽어야 할 목록을 선택한 후 저장을 탭하여 다운로드합니다. 추가한 목록은 선택을 해제하면 제거됩니다. + +각 목록은 자신만의 단어-읽기 표이며, .dict.yaml 파일은 다음과 같은 줄들로 구성됩니다: +字 ham +典 din2 +첫 번째 열은 단어이고 두 번째 열은 해당 읽음입니다; 탭 없는 추가 열과 줄은 무시됩니다." + 위에 선택한 언어에 대해 Rime 단어 목록이 선택되지 않았습니다. 이 이름을 탭하여 목록을 선택하거나 목록이 있는 다른 언어를 선택하세요 + 추가 + Rime 단어 목록 추가 + 字 ham과 典 din2와 같은 줄이 있는 .dict.yaml 파일이나, 이를 포함하는 zip 파일입니다. 목록을 선택하고 저장을 탭하면 목록이 가져오며, 라이선스는 사용자가 확인할 수 있습니다. + 언어 코드 + hak 또는 wuu처럼 두 자에서 세 글자 사이 + 이름 + 읽음 아래에 표시됨, 예: Hakka (Meixian) + 단어 목록 주소 + .dict.yaml 또는 그 zip의 https:// 주소 + 단어에 없는 문자 + 자동 + 광둥어 문자 + 중국어 문자 + 문자 읽음 없음 + 코드, 이름 및 주소를 입력하세요 + 코드는 hak처럼 두 글자 또는 세 글자여야 합니다 + 주소는 https://로 시작해야 합니다 + 이름과 주소에는 수직 막대 기호(|)를 포함할 수 없습니다 + %1$s에는 이미 단어 테이블이 있습니다 + %1$s는 내장 로마자 표기기로 읽히므로 단어 목록을 사용할 수 없습니다 + 사용하지 않음: 이 언어에는 이미 고유한 읽음이 있으므로, 제거하려면 체크를 해제하세요 + Added %1$s - 다운로드하려면 체크하고 저장을 탭하세요 + 단어 목록 준비 완료 + 단어 목록 준비 완료 (%1$d 항목) + 읽기 단어 목록 작성 중: %1$d%% + 단어 목록 다운로드 실패 - 다시 시도하려면 저장을 탭하세요 저장된 가사 지우기 - 이 기기에 저장된 가사를 삭제합니다 + 이 기기에 유지되는 가사의 %1$s MB 삭제 %1$s MB 지워짐 토큰 가져오기 Apple Music Media-user-token @@ -529,14 +564,22 @@ LOGIN_INFO" 복사 버튼 표시하기 버튼을 탭하여 원본 가사를 일반 텍스트로 복사할 수 있고, 길게 탭하면 원본 가사 소스 파일을 다운로드에 저장할 수 있습니다 번역 버튼 표시하기 - 버튼을 탭하여 번역을 켜거나 끌 수 있고, 길게 탭하면 번역만 표시됩니다. 제공자 번역, Google 번역 또는 OpenAI 호환 API를 사용합니다 - 로마자 표기 버튼 표시하기 - 버튼을 탭하여 로마자 표기를 켜거나 끌 수 있고, 길게 탭하면 로마자 표기만 표시됩니다. 제공자 로마자 표기, Google 번역 또는 OpenAI 호환 API를 사용합니다 - 로컬 로마자 표기 및 정렬 - 가사 제공자, 사용 중인 모델, Google 번역이 모두 실패했을 때 기기에서 직접 로마자 표기 변환합니다. 발음 표시는 시간에 맞춰 표시되는 가사의 각 단어 뒤에 표시됩니다. 이 기능을 사용하려면 단어 동기화가 켜져 있어야 합니다. 중국어, 광둥어, 일본어, 한국어, 그리스어, 러시아어, 조지아어, 아르메니아어를 지원하며, 용량은 약 120MB입니다 - %1$s 읽는 중: %2$d%% + 번역을 토글하려면 탭하세요; 번역만 보려면 길게 누르세요. 활성화된 번역 공급자를 나열된 순서대로 사용합니다 + 로마자 표기 또는 음차 버튼 표시 + 로마자 표기 또는 음차를 토글하려면 탭하세요; 로마자 표기 또는 음차만 하려면 길게 누르세요. 활성화된 로마자 표기 공급자를 나열된 순서대로 사용합니다 + 음차 허용 + 가사 소스에서의 비라틴 문자 읽음 허용; 끄면 라틴 문자, 악센트 문자 및 성조 숫자만 유지합니다 + 시간 동기화된 가사의 각 단어를 읽습니다(word sync on); 한국어는 다운로드가 필요하지 않습니다; 열네 개의 스크립트가 하나의 CLDR 다운로드를 공유합니다; 한자는 해당 언어를 커버하는 경우 Rime 단어 목록으로 읽힙니다 + 읽기 사전 다운로드 중… 읽기 준비 완료 (%1$d 항목) 다운로드 실패 - 다시 시도하려면 껐다가 다시 켜세요 + 오프라인 사전 + 일본어 (SudachiDict, ~75 MB) + 중국 및 광둥어 문자 (UniDict, ~8 MB) + 중국 단어 (pinyin-pro, ~17 MB) + 그리스어, 슬라브어 키릴 문자(러시아, 우크라이나, 불가리아, 세르비아, 벨라루시, 마케도니아 포함), 조지아어, 아르메니아어, 히브리어, 아랍어, 암하라어, 시리아어 음차 (CLDR, ~0.5 MB) + %1$s 읽는 중: %2$d%% + 최소 하나 이상의 사전을 선택하세요 새로고침 버튼 표시하기 버튼을 탭하여 다음 우선순위의 가사 제공자로 전환할 수 있고, 길게 탭하면 노래 제목과 아티스트로 가사를 검색할 수 있습니다 가사 검색 @@ -547,8 +590,10 @@ LOGIN_INFO" 저장됨 동기화된 단어 또는 음절 강조 표시하기 가능한 경우 단어 단위로 표시되는 가사를 우선합니다. 가사에 단어별 타이밍 정보가 포함되어 있으면, 음악에 맞춰 각 단어 음절을 순서대로 강조 표시합니다 + 비활성 줄 축소 + 현재 노래하지 않는 모든 줄을 흐리게 하는 것 위에 약간 작게 그립니다 백그라운드 보컬 별도 표시하기 - \'(Oh) I want you\'와 같이 괄호로 작성된 백그라운드 보컬을 가사 줄에서 분리하여, 별도의 흐릿한 줄로 이동합니다 + 배경 보컬과 하모니를 메인 라인에서 분리합니다. 괄호 안에 쓰인 배경 보컬을 메인 라인에서 별도의 흐린 줄로 이동시킵니다, 예를 들어 \"(Oh) I want you\" 재생된 가사 숨기기 현재 재생 중인 줄 위의 가사 줄을 숨깁니다 재생되지 않은 가사 숨기기 @@ -606,10 +651,15 @@ LOGIN_INFO" %s에 의해 번역됨 %s에 의해 로마자 표기됨 %s에 의해 로마자 표기됨 + Rime로 로마자 표기함 (%s) 번역됨 번역 언어 - 로마자 표기 언어 + 로마자 표기 또는 음차 언어 + "번역 대상 언어: +%s" + "로마자 표기 또는 음차 대상 언어: +%s" 번역하는 중… 가사를 번역할 수 없습니다 로마자 표기됨 @@ -623,7 +673,7 @@ LOGIN_INFO" 새로고침 중 새로고침됨 QuickLRC API 키 - QuickLRC는 현재 재생 중인 트랙의 가사를 기록하거나 가사와 오디오의 타이밍을 맞춰 줍니다. 위의 OpenAI 호환 엔드포인트와는 관련이 없는 별도의 서비스입니다 + QuickLRC는 현재 트랙의 가사를 전사하거나 시간 정렬합니다 QuickLRC 키가 저장되었습니다. 가사 검색 대화상자에서 정렬 버튼을 누르거나, 트랙에 가사가 없을 때 표시되는 패널의 프롬프트를 사용하세요 QuickLRC API 키를 저장하였습니다 QuickLRC에서 해당 키를 수락하지 않았습니다 @@ -639,56 +689,50 @@ LOGIN_INFO" 정렬할 가사 없음 - 대신 기록합니다 QuickLRC에서 이 트랙을 처리할 수 없습니다 이 트랙에 대한 QuickLRC 크레딧이 모두 소진되었습니다 - "가사는 곡 제목과 아티스트를 기준으로 검색합니다. 메타데이터가 특이한 경우 가사를 찾지 못하거나 다른 버전의 가사가 검색될 수 있습니다. 새로고침을 눌러 다른 버전으로 전환할 수 있습니다. * 표시가 있는 제공자는 토큰이 필요합니다. 활성화된 모든 제공자에 설정된 순서대로 항상 요청하는 것은 아니며, 각 제공자의 응답 속도에 따라 결과가 반환됩니다\n\n가사는 Morphe와 제휴하거나 후원 또는 보증 관계에 있지 않은 서드파티 서비스에서 제공됩니다. 각 서비스는 별도의 수정 없이 그대로 사용되며, 가사의 내용과 제공 여부 및 라이선스는 해당 서비스와 라이선스 보유자의 책임입니다. 가사를 이용할 때는 각 서비스의 이용약관이 적용될 수 있습니다. 활성화된 서비스에는 사용자가 요청한 검색에 필요한 정보만 전달됩니다. 여기에는 곡 제목과 아티스트 이름, 그리고 해당 서비스에서 필요한 경우 동영상 ID가 포함됩니다. 선택 사항인 AI 엔드포인트와 QuickLRC 받아쓰기 기능은 사용자가 직접 설정한 경우에만 실행되며, 가사 텍스트 또는 해당 곡의 동영상 URL을 전달받습니다" + "가사는 곡 제목과 아티스트를 기준으로 매칭되며, 특이한 메타데이터는 가사가 없거나 잘못된 버전을 찾을 수 있습니다. 버전을 전환하려면 새로고침을 사용하세요. * 표시가 있는 제공자는 토큰이 필요합니다. 활성화된 모든 제공자가 항상 요청 순서대로 호출되는 것은 아니며, 각 제공자의 속도에 따라 응답합니다. + +가사는 Morphe와 제휴, 후원 또는 보증 관계에 있지 않은 서드파티 서비스에서 제공됩니다. 각 서비스는 그대로 사용되며, 가사, 가사의 가용성 및 라이선스는 해당 서비스와 라이선스 소유자에게_remain_하며, 이를 사용하는 것은 해당 서비스의 이용약관에 따라 제한될 수 있습니다. 활성화된 서비스에 전달되는 것은 사용자가 요청한 조회 정보(곡 제목, 아티스트, 해당 서비스가 필요할 경우 동영상 ID)만 포함됩니다. 선택 사항인 AI 엔드포인트와 QuickLRC 받아쓰기는 사용자가 구성한 후에만 실행되며, 가사 텍스트나 트랙의 동영상 URL을 받습니다." 사용하는 제공자만 활성화하거나 기본값을 유지하세요 - 지속적인 요청은 소규모 커뮤니티 가사 사이트에 과부하를 줄 수 있습니다 크레딧 라인 숨기기 가사 아래에 표시되는 크레딧 라인을 숨깁니다 번역과 로마자 표기 위치 바꾸기 - 활성화되면 로마자 표기를 가사 아래에 표시하고, 번역은 가사 위에 표시합니다; 비활성화되면 로마자 표기를 가사 위에 표시하고, 번역은 가사 아래에 표시합니다 - 번역 - 로마자 표기 - 번역 제공자 - Google (기본값, 무료) - OpenAI 호환 (사용자 정의) - 로마자 표기 제공자 - Google (기본값) - 로컬 / 오프라인 사전 - OpenAI 호환 (사용자 정의) - AI 섹션 - 제공자가 후렴 또는 절 등의 섹션 라벨을 제공하지 않는 경우에만 사용되며, 이때 OpenAI 호환 모델이 가사의 섹션을 감지하여 라벨을 붙입니다 - AI 및 클라우드 엔드포인트 - AI 제공자를 위한 API 키, 모델, 엔드포인트 및 프롬프트 템플릿을 구성할 수 있습니다 - API 엔드포인트 URL (예시: https://api.openai.com/v1/chat/completions) + ON은 로마자 표기 또는 음차를 가사 아래에, 번역을 위에 배치합니다; OFF는 로마자 표기 또는 음차를 위에, 번역을 아래에 둡니다. 음절 위에 배치된 읽음은 음절과 함께 움직이며, 위에서 아래로 이동합니다 + 항상 가사 중앙 정렬 + ON을 선택하면 모든 줄이 가운데로 정렬되고 소스 정렬(듀엣 부분 포함)이 무시됩니다. OFF를 선택하면 가사가 요구하는 대로 정렬이 유지됩니다 + 최소 하나의 공급자가 켜져 있어야 합니다 + 번역 + 로마자 표기 또는 음차 + 오프라인 사전 + Rime 단어 목록 + AI로 섹션 감지 + 공급자가 후렴이나 절과 같은 섹션 레이블을 제공하지 않고 OpenAI 호환 모델이 이를 감지할 때만 사용됩니다. 행을 편집하면 해당 프롬프트도 열립니다 + API 엔드포인트 URL (예: https://api.openai.com/v1/chat/completions, https://api.anthropic.com/v1/chat/completions, https://generativelanguage.googleapis.com/v1beta/openai/chat/completions) 프롬프트 템플릿 자리 표시자: {language}, {task}, {title}, {artist}, {count}, {lines} 프롬프트 저장됨 프롬프트가 저장되었지만 AI 엔드포인트는 아직 사용할 수 없습니다. 먼저 AI 설정에서 구성하세요 프롬프트 초기화 - 유효한 JSON이 아닙니다. 저장하기 전에 수정하거나 필드를 지우세요 번역 및 로마자 표기 설정을 저장하였습니다 설정을 기본값으로 초기화하였습니다 기본 URL은 http:// 또는 https://로 시작해야 합니다 - 테스트 연결 - 엔드포인트 구성 - 모든 엔드포인트 표시하기 + 설정 불완전하거나 공급자에서 거부되었습니다 연결 유효성을 검사하는 중… - 번역 제공자 - 로마자 표기 제공자 - 모델 가져오기 + %1$s: 연결됨! (%2$s) + %1$s: 연결 실패. 설정을 확인하세요. + 먼저 API 키를 입력하세요 모델을 가져오는 중… 모델 선택 모델을 가져올 수 없습니다. API 키 또는 연결을 확인하세요 - Gemini API 설정 - OpenAI 호환 (사용자 정의 엔드포인트) - DeepL API 설정 - API 키 + 프롬프트 모델 기본 URL + 서비스 선택 API 키 (선택 사항) - 번역 프롬프트 추가 요청 본문 (선택 사항 JSON) + 추가 요청 헤더 (선택 사항 JSON) + 이름과 값을 따옴표로 묶으세요, 예: {\"api-key\": \"abc\"}. 따옴표 없이 / : 또는 숫자를 포함하는 값은 거부됩니다. 빈 값은 헤더를 제거합니다. + 이름과 값을 따옴표로 묶으세요, 예: {\"temperature\": 0.3}. 따옴표 없이 / 또는 :를 포함하는 값은 거부됩니다. 여기서는 모델과 메시지를 설정할 수 없습니다. 인증 키 (무료 또는 프로) - API 키, 모델, 엔드포인트 및 프롬프트를 구성할 수 있습니다 8.23.51 - 굵은 아이콘을 비활성화합니다 9.13.51 - 플레이어에서 \'다음 트랙\', \'가사\' 및 \'관련 항목\' 탭을 복원합니다 diff --git a/patches/src/main/resources/addresources/values-ky-rKG/music/strings.xml b/patches/src/main/resources/addresources/values-ky-rKG/music/strings.xml index e25bddbf86..61926126d2 100644 --- a/patches/src/main/resources/addresources/values-ky-rKG/music/strings.xml +++ b/patches/src/main/resources/addresources/values-ky-rKG/music/strings.xml @@ -462,12 +462,11 @@ SABR ойнотуусу (Видео агымдарын имитациялоо ө Көрсөтүү Синхрондоштуруу Панель - AI чекити Бардык камсыздоочулар өчүрүлгөн - камтылган тексттер жана киргизилген тексттер Провайдерлердин тартибин кайра орнотуу? Провайдерлерди стандарттык приоритеттүү тартипке келтирүү + Өзүнөн өзү Сакталган ырларды тазалоо - Бул түзүлүштө сакталган ыр сөздөрүн өчүрүү %1$s МБ тазаланды Токенди алуу Apple Music Media-user-token @@ -527,13 +526,8 @@ Steps: Көчүрүү баскычын көрсөтүү Түп нускадагы ыр сөздөрүн жөнөкөй текст катары көчүрүү үчүн басыңыз; шире файлды «Жүктүүлөр» папкасына сактоо үчүн узак басыңыз Котормо баскычын көрсөтүү - Котормону күйгүзүү/өчүрүү үчүн басыңыз; бир гана котормону көрүү үчүн узак басыңыз. Провайдерлердин котормолорун, Google Translate же OpenAI-га шайкеш келген API колдонулат - Романизация баскычын көрсөтүү - Романизацияны күйгүзүү/өчүрүү үчүн басыңыз; романизацияны гана көрсөтүү үчүн узак басыңыз. Провайдердин романизациясын, Google Translate же OpenAI-га шайкеш келген API колдонулат - Жергиликтүү романизация жана тегирөөлөө - Провайдер, сиздин моделиңиз жана Google Translate иштебей калганда, түзүлүштө романизациялоо. Окуу убактысы белгиленген ырдын ар бир сөзүнөн кийин келет; сөздөрдүн синхрондошуусу күйүк болушу керек. Кытай, кантон, жапон, корей, грек, орус, грузин, армян тилдери; болжол менен 120 МБ - Окулуп жатат %1$s: %2$d%% Жүктөө катасы - кайра аракет кылуу үчүн аны өчүрүп, кайра күйгүзүңүз + Окулуп жатат %1$s: %2$d%% Жаңыртуу баскычын көрсөтүү Кийинки ыр варианттарына өтүү үчүн басыңыз; ырдын аталышы жана artisti боюнча издөө үчүн узак басыңыз Сөздөрдү издөө @@ -604,7 +598,6 @@ Steps: Которулган Котормонун тили - Романизация тили Которууда… Текстти которуу мүмкүн болгон жок Романизацияланган @@ -618,7 +611,6 @@ Steps: Жаңыртылууда Жаңыртылды QuickLRC API ачкычы - QuickLRC учурдагы трек үчүн ырлардын тексттерин транскрипциялайт же убакытка ылайыктайт. Бул жогорудагы OpenAI-га шайкеш чекиттен көз карандысыз, өзүнчө кызмат QuickLRC ачкычы сакталды. Ырларды издөө диалогиндагы ылайыктоо баскычын же тректе ырлар жок болгондо панель сунушун колдонуңуз QuickLRC API ачкычы сакталды QuickLRC бул ачкычты кабыл алган жок @@ -634,56 +626,27 @@ Steps: Ылайыктай турган ырлар жок - ордуна транскрипцияланууда QuickLRC бул тректи иштете алган жок Бул трек үчүн QuickLRC кредиттери түгөндү - "Тексттер аталышы жана artisti боюнча дал келиştirүү аркылуу табылат, alış असामान्य метамаалыматтар эч нерсе таппай калышы мүмкүн же ката версияны көрсөтүшү мүмкүн. Версияларды алмаштыруу үчүн жаңыртууну колдонуңуз. * белгиси менен белгиленген провайдерлер үчүн токен талап кылынат. Иштетилген бардык провайдерлер дайыма сиздин тартибиңиз менен суралып турбайт, бирок алардын ылдамдыгына жараша жооп беришет.\n\nMатндуу ырлар Morphe компаниясына байланыштуу эмес, дем салбаган же колдобогон үчүнчү тараптын кызматтарынан келет. Ар бири кандай болсо ошондой колдонулат: ырлардын тексти, алардын жеткиликтүүлүгү жана лицензиялары ошол кызматтарда жана алардын лицензиарларында калат, ал эми аларды колдонуу алардын кызмат көрсөтүү шарттарына баш ийүү мүмкүнчүлүгүнө ээ. Иштетилген кызматка сиз сураган издөө гана жетет - аталышы, artisti жана ошол кызматка керек болгон жерде видеонун IDси. Кошумча AI чекитти жана QuickLRC транскрипциясы сиз аларды конфигурациялагандан кийин гана иштейт жана ырлардын текстин же тректин видео URL-дарегин алат." Сиз колдонгон провайдерлерди гана активдештириңиз же стандарттуу жөндөөлөрдү калтырыңыз - туруктуу суроо-талаптар кичинекей коомдук ырлардын сайттарын overloaded кылышы мүмкүн Кредиттик сапты жашыруу Тексттин төмөнүндө көрсөтүлгөн кредиттик сапты жашыруу Котормо менен романизацияны алмаштыруу - ON романизацияны ырлардын төмөнүнө, ал эми котормону жогорусуна жайгаштырат; OFF романизацияны жогоруда, котормону төмөндө калтырат - Котормо - Романизация - Котормо провайдери - Google (Негизги, акысыз) - OpenAI менен шайкеш (Custom) - Романизациялоо камсыздоочусу - Google (Негизги) - Жергиликтүү / Офлайн сөздүк - OpenAI менен шайкеш (Custom) - AI бөлүмү - Провайдер Кайырма же Куплетки сыяктуу бөлүмдөрдүн белгилерин жөнөтпөгөндө жана сиздин OpenAI- менен шайкеш моделиңиз аларды аныктаганда гана колдонулат - AI & Cloud Endpoints - ЖИ sağlayıcıлары үчүн API ачкычтарын, моделдерин, чекиттерин жана промпт шаблондорун жөндөңүз. - API чекитинин URL дареги (мисалы, https://api.openai.com/v1/chat/completions) Промпт шаблону Плейсхолдерлер: {language}, {task}, {title}, {artist}, {count}, {lines} Промпт сакталды Сунуш сакталды, бирок AI чекити али колдонулуучу эмес. Алдында аны AI жөндөөлөрүнөн конфигурациялаңыз Промпт кайрылды - JSON жарамдуу эмес. Сактоодон мурун аны оңдоңуз же талааны тазалаңыз Котормо & романизация жөндөөлөрү сакталды Жөндөөлөр стандарттуу абалга кайтарылды Негизги URL http:// же https:// менен башталышы керек - Байланышты текшерүү - Чекиттерди конфигурациялоо - Бардык чекиттерди көрсөтүү Байланыш текшерилүүдө… - Котормо sağlayучу - Романизация sağlayучу - Моделдерди алуу Моделдер алынууда… Моделди тандаңыз Моделдерди алуу мүмкүн болгон жок. API ачкычын же байланышты текшериңиз. - Gemini API жөндөөлөрү - OpenAI менен шайкеш (Custom Endpoint) - DeepL API жөндөөлөрү - API ачкычы Модель Негизги URL API ачкычы (факультативдүү) - Котормо суроосу Кошумча суроо-талаптын денеси (факультативдүү JSON) Аутентификациялоо ачкычы (Free же Pro) - API ачкычтарын, моделдерди, чекиттерди жана промптторду конфигурациялоо 8.23.51 - Калың белгилерди өчүрүү 9.13.51 - Плеердин \"Кийинкиси\", Лирика жана Байланыштуу өтмөктөрүн калыбына келтирүү diff --git a/patches/src/main/resources/addresources/values-lo-rLA/music/strings.xml b/patches/src/main/resources/addresources/values-lo-rLA/music/strings.xml index ec5c625d39..acc9f2d698 100644 --- a/patches/src/main/resources/addresources/values-lo-rLA/music/strings.xml +++ b/patches/src/main/resources/addresources/values-lo-rLA/music/strings.xml @@ -462,12 +462,10 @@ Second \"item\" text" ການສະແດງຜົນ ການຊິງໂຄຣໄນເຊຊັນ ແຜງຄວບຄຸມ - AI endpoint ປິດການນຳໃຊ້ຜູ້ໃຫ້ບໍລິການທັງໝົດ - ເນື້ອເພງໃນຕົວ ແລະ ເນື້ອເພງທີ່ຝັງມາ ຣີເຊັດລຳດັບຜູ້ໃຫ້ບໍລິການ? ຄືນຄ່າຜູ້ໃຫ້ບໍລິການໄປເປັນລຳດັບຄວາມສຳຄັນເລີ່ມຕົ້ນ ລຶບເນື້ອເພງທີ່ບັນທຶກໄວ້ - ລຶບເນື້ອເພງທີ່ເກັບໄວ້ໃນອຸປະກອນນີ້ ລຶບແລ້ວ %1$s MB ຮັບໂທເຄັນ Apple Music Media-user-token @@ -527,14 +525,9 @@ Second \"item\" text" ສະແດງປຸ່ມຄັດລອກ ແຕະເພື່ອຄັດລອກເນື້ອເພງຕົ້ນສະບັບເປັນຂໍ້ຄວາມທຳມະດາ; ກົດຄ້າງໄວ້ເພື່ອບັນທຶກໄຟລ໌ແຫຼ່ງເນື້ອເພງດິບລົງໃນ Downloads ສະແດງປຸ່ມແປ - ແຕະເພື່ອສະຫຼັບການແປ; ກົດຄ້າງໄວ້ເພື່ອເບິ່ງສະເພາະການແປ. ໃຊ້ການແປຈາກຜູ້ໃຫ້ບໍລິການ, Google Translate, ຫຼື OpenAI-compatible API - ສະແດງປຸ່ມ Romanize - ແຕະເພື່ອສະຫຼັບການຖອດສຽງເປັນອັກສອນໂຣມັນ; ກົດຄ້າງໄວ້ເພື່ອເບິ່ງສະເພາະການຖອດສຽງ. ໃຊ້ການຖອດສຽງຈາກຜູ້ໃຫ້ບໍລິການ, Google Translate, ຫຼື OpenAI-compatible API - ການຖອດສຽງເປັນອັກສອນໂຣມັນ ແລະ ການຈັດວາງໃຫ້ກົງກັນໃນເຄື່ອງ - ຖອດສຽງເປັນອັກສອນໂຣມັນໃນອຸປະກອນ ເມື່ອຜູ້ໃຫ້ບໍລິການ, ໂມເດລຂອງທ່ານ ແລະ Google Translate ບໍ່ສາມາດເຮັດວຽກໄດ້. ການອ່ານຈະຕາມຫຼັງແຕ່ລະຄຳຂອງເນື້ອເພງທີ່ກຳນົດເວລາໄວ້; ຕ້ອງເປີດການຊິງຄ໌ຄຳສັບ. ພາສາຈີນ, ກວາງຕຸ້ງ, ຍີ່ປຸ່ນ, ເກົາຫຼີ, ກຣີກ, ຣັດເຊຍ, ຈໍຣເຈຍ, ອາມີເນຍ; ປະມານ 120 MB - ກຳລັງອ່ານ %1$s: %2$d%% ການອ່ານພ້ອມແລ້ວ (%1$d ລາຍການ) ການດາວໂຫລດລົ້ມເຫຼວ - ໃຫ້ປິດ ແລະ ເປີດສິ່ງນີ້ອີກຄັ້ງເພື່ອລອງໃໝ່ + ກຳລັງອ່ານ %1$s: %2$d%% ສະແດງປຸ່ມ Refresh ແຕະເພື່ອປ່ຽນໄປຫາເນື້ອເພງອື່ນທີ່ເປັນໄປໄດ້; ແຕະຄ້າງໄວ້ເພື່ອຄົ້ນຫາເນື້ອເພງຕາມຊື່ເພງ ແລະ ສິລະປິນ ຄົ້ນຫາເນື້ອເພງ @@ -605,7 +598,6 @@ Second \"item\" text" ແປແລ້ວ ພາສາໃນການແປ - ພາສາໃນການຖອດລະຫັດເປັນອັກສອນໂຣມັນ ກຳລັງແປ… ບໍ່ສາມາດແປເນື້ອເພງໄດ້ ຖອດສຽງເປັນອັກສອນໂຣມັນ @@ -619,7 +611,6 @@ Second \"item\" text" ກຳລັງຣີເຟຣຊ ຣີເຟຣຊແລ້ວ ຄີ API ຂອງ QuickLRC - QuickLRC ຖອດຄວາມ ຫຼື ປັບເວລາເນື້ອເພງໃຫ້ກົງກັບແທຣັກປັດຈຸບັນ. ເປັນບໍລິການແຍກຕ່າງຫາກ, ບໍ່ກ່ຽວຂ້ອງກັບ endpoint ທີ່ເຂົ້າກັນໄດ້ກັບ OpenAI ທາງເທິງ ບັນທຶກຄີ QuickLRC ແລ້ວ. ໃຊ້ປຸ່ມ align ໃນບ່ອນຄົ້ນຫາເນື້ອເພງ, ຫຼື ໃຊ້ຄຳແນະນຳໃນແຜງຄວບຄຸມເມື່ອແທຣັກບໍ່ມີເນື້ອເພງ ບັນທຶກຄີ API ຂອງ QuickLRC ແລ້ວ QuickLRC ບໍ່ຍອມຮັບຄີນີ້ @@ -635,56 +626,27 @@ Second \"item\" text" ບໍ່ມີເນື້ອເພງທີ່ຈະຈັດຕຳແໜງ - ຈະປ່ຽນເປັນການຖອດສຽງແທນ QuickLRC ບໍ່ສາມາດປະມວນຜົນເພງນີ້ໄດ້ ເຄຣດິດ QuickLRC ສໍາລັບເພງນີ້ໄດ້ໝົດແລ້ວ - "ເນື້ອເພງຈະຖືກຈັບຄູ່ໂດຍຊື່ເພງ ແລະ ສິລະປິນ, ຂໍ້ມູນ metadata ທີ່ຜິດປົກກະຕິອາດຈະບໍ່ພົບ ຫຼື ພົບເວີຊັນທີ່ຜິດ. ໃຊ້ການຣີເຟຣຊເພື່ອປ່ຽນເວີຊັນ. ຜູ້ໃຫ້ບໍລິການທີ່ໝາຍດ້ວຍ * ຕ້ອງມີ token. ຜູ້ໃຫ້ບໍລິການທີ່ເປີດໃຊ້ງານທັງໝົດອາດຈະບໍ່ຖືກຮ້ອງຂໍຕາມລຳດັບຂອງທ່ານສະເໝີ, ແຕ່ຈະຕອບສະໜອງຕາມຄວາມໄວຂອງພວກເຂົາ.\n\nເນື້ອເພງມາຈາກບໍລິການຂອງບຸກຄົນທີສາມທີ່ບໍ່ມີສ່ວນກ່ຽວຂ້ອງ, ບໍ່ໄດ້ຮັບການອຸປະຖຳ, ຫຼື ບໍ່ໄດ້ຮັບການຮັບຮອງໂດຍ Morphe. ແຕ່ລະບໍລິການຖືກນຳໃຊ້ຕາມສະພາບທີ່ເປັນ: ເນື້ອເພງ, ຄວາມພ້ອມໃນການນຳໃຊ້ ແລະ ການໃຫ້ອະນຸຍາດຂອງພວກເຂົາຍັງຄົງເປັນຂອງບໍລິການເຫຼົ່ານັ້ນ ແລະ ຜູ້ໃຫ້ອະນຸຍາດ, ແລະ ການນຳໃຊ້ບໍລິການເຫຼົ່ານັ້ນອາດຈະຢູ່ພາຍໃຕ້ເງື່ອນໄຂການບໍລິການຂອງພວກເຂົາ. ສິ່ງທີ່ສົ່ງໄປຫາບໍລິການທີ່ເປີດໃຊ້ງານມີພຽງແຕ່ການຄົ້ນຫາທີ່ທ່ານຮ້ອງຂໍ - ຊື່ເພງ, ສິລະປິນ, ແລະ ໃນກໍລະນີທີ່ບໍລິການນັ້ນຕ້ອງການ, ID ຂອງວິດີໂອ. ຈຸດປາຍ AI ທີ່ເປັນທາງເລືອກ ແລະ ການຖອດຄວາມ QuickLRC ຈະເຮັດວຽກຫຼັງຈາກທີ່ທ່ານກຳນົດຄ່າພວກມັນເທົ່ານັ້ນ, ແລະ ຈະໄດ້ຮັບຂໍ້ຄວາມເນື້ອເພງ ຫຼື URL ວິດີໂອຂອງເພງນັ້ນ." ເປີດໃຊ້ງານສະເພາະຜູ້ໃຫ້ບໍລິການທີ່ທ່ານໃຊ້, ຫຼື ໃຊ້ຄ່າເລີ່ມຕົ້ນ - ການຮ້ອງຂໍຢ່າງຕໍ່ເນື່ອງອາດເຮັດໃຫ້ເວັບໄຊເນື້ອເພງຂອງຊຸມຊົນຂະໜາດນ້ອຍເຮັດວຽກໜັກເກີນໄປ ເຊື່ອງແຖວໃຫ້ເຄຣດິດ ເຊື່ອງແຖວໃຫ້ເຄຣດິດທີ່ສະແດງຢູ່ລຸ່ມເນື້ອເພງ ສະຫຼັບການແປ ແລະ ການຖອດສຽງ - ເປີດ ຈະວາງການຖອດສຽງໄວ້ລຸ່ມເນື້ອເພງ ແລະ ການແປໄວ້ທາງເທິງ; ປິດ ຈະວາງການຖອດສຽງໄວ້ທາງເທິງ ແລະ ການແປໄວ້ທາງລຸ່ມ - ການແປ - ການທັບສັບ - ຜູ້ໃຫ້ບໍລິການແປພາສາ - Google (ເລີ່ມຕົ້ນ, ຟຣີ) - OpenAI Compatible (ກຳນົດເອງ) - ຜູ້ໃຫ້ບໍລິການການທັບສັບ - Google (ຄ່າເລີ່ມຕົ້ນ) - ວັດຈະນານຸກົມທ້ອງຖິ່ນ / ອອຟໄລນ໌ - ທີ່ເຂົ້າກັນໄດ້ກັບ OpenAI (ກຳນົດເອງ) - ພາກສ່ວນ AI - ໃຊ້ສະເພາະໃນເວລາທີ່ຜູ້ໃຫ້ບໍລິການບໍ່ໄດ້ສົ່ງປ້າຍກຳກັບພາກສ່ວນ ເຊັ່ນ Chorus ຫຼື Verse, ເຊິ່ງຕົວແບບທີ່ເຂົ້າກັນໄດ້ກັບ OpenAI ຂອງທ່ານຈະກວດຫາໃນພາຍຫຼັງ - AI & Cloud Endpoints - ກຳນົດຄ່າ API keys, ໂມເດລ, endpoints, ແລະ ແມ່ແບບຄຳສັ່ງສຳລັບຜູ້ໃຫ້ບໍລິການ AI. - URL ຂອງ API endpoint (ຕົວຢ່າງ: https://api.openai.com/v1/chat/completions) ແມັອບແບບພຣອມພ໌ ຕົວແທນ: {language}, {task}, {title}, {artist}, {count}, {lines} ບັນທຶກພຣອມພ໌ແລ້ວ ບັນທຶກ Prompt ແລ້ວ, ແຕ່ endpoint ຂອງ AI ຍັງບໍ່ທັນສາມາດນຳໃຊ້ໄດ້. ກະລຸນາຕັ້ງຄ່າໃນ AI Settings ກ່ອນ ຣີເຊັດພຣອມພ໌ແລ້ວ - ບໍ່ແມ່ນ JSON ທີ່ຖືກຕ້ອງ. ກະລຸນາແກ້ໄຂ ຫຼື ລ້າງຊ່ອງຂໍ້ມູນກ່ອນບັນທຶກ ບັນທຶກການຕັ້ງຄ່າການແປ & ການທັບສັບແລ້ວ ຣີເຊັດການຕັ້ງຄ່າເປັນຄ່າເລີ່ມຕົ້ນແລ້ວ Base URL ຕ້ອງເລີ່ມຕົ້ນດ້ວຍ http:// ຫຼື https:// - ທົດສອບການເຊື່ອມຕໍ່ - ກຳນົດຄ່າ Endpoints - ສະແດງ Endpoints ທັງໝົດ ກຳລັງກວດສອບການເຊື່ອມຕໍ່… - ຜູ້ໃຫ້ບໍລິການການແປ - ຜູ້ໃຫ້ບໍລິການ Romanization - ດຶງຂໍ້ມູນໂມເດລ ກຳລັງດຶງຂໍ້ມູນໂມເດລ… ເລືອກໂມເດລ ດຶງຂໍ້ມູນໂມເດລບໍ່ສຳເລັດ. ກະລຸນາກວດສອບ API key ຫຼື ການເຊື່ອມຕໍ່. - ການຕັ້ງຄ່າ Gemini API - OpenAI Compatible (Custom Endpoint) - ການຕັ້ງຄ່າ DeepL API - ລະຫັດ API key ໂມເດລ Base URL ລະຫັດ API key (ທາງເລືອກ) - ຄໍາສັ່ງການແປ ເນື້ອໃນການຮ້ອງຂໍເພີ່ມເຕີມ (JSON ທາງເລືອກ) ລະຫັດຢືນຢັນຕົວຕົນ (ຟຣີ ຫຼື Pro) - ກຳນົດຄ່າ API keys, ໂມເດລ, endpoints ແລະ prompts 8.23.51 - ປິດການໃຊ້ງານໄອຄອນຕົວເຂັ້ມ 9.13.51 - ກູ້ຄືນແທັບ ຫຼັງຈາກນີ້, ເນື້ອເພງ, ແລະ ທີ່ກ່ຽວຂ້ອງ ຂອງເຄື່ອງຫຼິ້ນ diff --git a/patches/src/main/resources/addresources/values-lt-rLT/music/strings.xml b/patches/src/main/resources/addresources/values-lt-rLT/music/strings.xml index 20430fe6f0..50da243dae 100644 --- a/patches/src/main/resources/addresources/values-lt-rLT/music/strings.xml +++ b/patches/src/main/resources/addresources/values-lt-rLT/music/strings.xml @@ -462,12 +462,47 @@ Kad scrobbling būtų sinchronizuojamas foninio atkūrimo metu, įsitikinkite, k Rodinys Sinchronizacija Skydelis - AI pabaiga Visi teikėjai išjungti – integruoti žodžiai ir įterpti žodžiai Atstatyti tiekėjų eilę? Grąžinti tiekėjus į numatytąją prioriteto eilę + Rime žodynas + "Žodžių sąrašas „Rime“ taip pat reikalauja UniDict: veiklos srities „Nėra interneto ryšio žodyno teikėjas“ išrinkite „Kinų ir kantoniečių raumenys (UniDict)“ ir leiskite jam baigti atsisiuntimą. Be to, sąrašą, skaitant simbolius, gausi skaitymo. + +Išrinkite sąrašus „Rime Wordlist“ skaityti, tada spustelėkite Išsaugoti atsisiųsti juos. Sąrašas, kurį pridėjote, pašalinamas, kai jūs nurodysite jį. + +Kiekvienas sąrašas yra jūsų asmeninis žodis-į-kaip lentelės, .dict.yaml su eilėmis: +字 ham +典 din2 +Pirmas stulpelis yra žodis, antras jo skaitymas; papildomi stulpeliai ir eilutės be tabuliacijos ignoruojamos." + Viršutinėje kalboje nėra pasirinkto joks Rime žodžių sąrašas. Bakstelėkite šią pavadinimą, kad pažymėtumėte viena iš jų, arba pasirinkite kalbą, turinti sąrašą + Pridėti + Pridėti Rime žodžių sąrašą + .dict.yaml failas arba zipas laikantis vieno, su eilėmis kaip 字 ham ir 典 din2. Sąrašas gaunamas, kai išrenkate jį ir spustelėkite Išsaugoti, ir licencija yra jūsų tikrinti + Kalbos kodas + Du ar trys raidės, pvz., hak arba wuu + Pavadinimas + Rodoma po tarimu, pvz., Hakka (Meixian) + Žodžių sąrašo adresas + https:// adresas .dict.yaml failo ar jo zip archyvo + Simboliai, kurie nėra žodyje + Automatinis + Kantonų kalbos simboliai + Mandarin kalbos simboliai + Nėra jokio simbolio tarimo + Užpildykite kodą, pavadinimą ir adresą + Kodas privalo būti dviejų ar trijų raidžių, pvz., hak + Adresas turi prasidėti https:// + Pavadinimas ir adresas negali turėti vertikaliajo brūkšnio + %1$s jau turi žodžių lentelę + %1$s yra skaitomas numatytaios transkripcijos sistema ir negali naudoti žodžių sąrašo + Nenaudojama: ši kalba jau turi savo tarimą, todėl pašalinkite ją nuimdamas pažymėjimą + %1$s buvo pridėtas – pažymėkite jį ir bakstelkite Išsaugoti, kad jį atsisiųstų + Žodžių sąrašai paruošti + Žodžių sąrašai paruošti (%1$d elementų) + Tarimo žodžių sąrašas: %1$d%% + Žodžių sąrašo atsisiuntimas nepavyko – vėl bakstelkite Išsaugoti, kad kartotųsi Išvalyti išsaugotus tekstus - Ištrinkite šio įrenginio laikomus tekstus + Ištrinkite %1$s MB teksto, saugomo šiuo įrenginiu Išvalyta %1$s MB Gauti prieigos raktą „Apple Music“ media-user-token @@ -527,14 +562,22 @@ Reikalingi slapukai: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPI Rodyti kopijavimo mygtuką Spustelėkite, kad nukopijuotumėte originalius tekstus kaip paprastą tekstą; ilgai paspauskite, kad išsaugotumėte žaliavinių tekstų failą atsisiuntimų aplanke Rodyti vertimo mygtuką - Paspauskite, kad perjungtumėte vertimą; ilgai paspauskite, kad rodytumėte tik vertimą. Naudojama teikėjo vertimas, Google „Translate“ arba suderinama su OpenAI API - Rodyti romanizavimo mygtuką - Paspauskite, kad perjungtumėte transkripciją lotyniškais ramenimis; ilgai paspauskite, kad rodytumėte tik transkripciją lotyniškais raomenimis. Naudojama teikėjo transkripcija, Google „Translate“ arba suderinama su OpenAI API - Vietinė transkripcija lotyniškais raemenys ir derinimas - Transkribuokite įrenginyje, kai nepavyksta naudoti teikėjo, jūsų modelio ir Google „Translate“. Skaitymas atitinka kiekvieną laiko koduoto teksto žodį; turi būti įjungtas žodžių sinchronizavimas. Kinų, kantonių japonų, korėjų graikų rusų gruzinų armėnų; apie 120 MB - Skaitmenų %1$s: %2$d%% + Bakstelėkite, kad perjungtumėte vertimą; ilgai paspauskite, kad gautumėte tik vertimą. Naudoja įgalintus vertimo tiekėjus jų nustatytąja tvarka + Rodyti Transkripcijos arba Transliteravimo mygtuką + Bakstelėkite, kad perjungtumėte transliteravimą arba transliteraciją; ilgai paspauskite, kad gautumėte tik transliteravimą arba transliteraciją. Naudoja įgalintus transliteravimo tiekėjus jų nustatytąja tvarka + Įjungti transliteravimą + Leisti ne lotyniškus tarimus iš teksto šaltinio; kai išjungta, paliekamos tik lotyniškos raidės, raidės su diakritiniais ženklais ir tonų skaitmenys + Skaito kiekvieną laiko sinchronizavimo dainų tekstą (žodžio sinchronizavimas įjungtas); Korėjietiškam nereikia atsisiųsti; keturiolika raštų dalijasi vienu CLDR atsisiuntimu; Han rašmenis skaito Rime žodynas, kai jo žodžių lentelė apima kalbą + Atsisiunčiami tarimo žodynai… Skaitymai paruošti (%1$d įrašų) Atsisiuntimas nepavyko. Perkraukite nustatymą ir bandykite dar kartą + Nešliožiamasis žodynas + Japonų (SudachiDict, ~75 MB) + Kinų ir kantonų kalbų simboliai (UniDict, ~8 MB) + Kinų žodžiai (pinyin-pro, ~17 MB) + Graikų, slavų kirilikos (įskaitant rusų, ukrainiečių, bulgarų, serbų, baltarusių, makedonų), gruzinų, armėnų, hebrajų, arabų, amharų ir sirų kalbų transliteravimas (CLDR, ~0.5 MB) + Skaitmenų %1$s: %2$d%% + Pasirinkite bent vieną žodyną Rodyti atnaujinimo mygtuką Paspauskite, kad pereitumėte prie kitos teksto kandidatūros; ilgai paspauskite, kad ieškotumėte tekstų pagal dainos pavadinimą ir atlikėją Ieškoti dainų tekstų @@ -545,8 +588,10 @@ Reikalingi slapukai: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPI Išsaugota Paryškinti sinchronizuotus žodžius ar skiemenis Teikti pirmenybę žodis po žodžio sinchronizuotiems dainų tekstams, kai jie prieinami. Kai dainų tekstai turi žodžių laiką, kiekvienas žodis ar skiemuo paryškinamas pagal muziką - Rodyti fono vokalus atskirai - Perkelia foninį vokalinį tekstą, esantį skliaustuose, pvz., „(Oh) I want you“, iš pagrindinio kūrio teksto atskira švelnesne eilute + Suskleisti neveikiančias eilutes + Nupiešia kiekvieną eilutę, kuri dabar nėra atliekama, šiek tiek mažesnę, viršutinįjį lygį nuslopinant + Rodyti foninę vokalinę atskirai + Atskiria foninę vokalą ir harmoniją nuo pagrindinės linijos. Foninė vokalo užrašyta kabutėse, pvz., „(Oh) aš tavęs trošku“, perkeliama iš dainuojamos linijos į tamsesnę savo eilutę Slėpti jau atkurtus dainų tekstus Slėpti dainų teksto eilutes virš šiuo metu grojamos eilutės Slėpti neatkurtus dainų tekstus @@ -604,10 +649,15 @@ Taikoma abiems riboms. Nustatant 1,75, keturi pilno pločio simboliai counted ka Išvertė %s Romanizavo %s Romanizavo %s + Transkribuota Rime (%s) Išversta Vertimo kalba - Romanizacijos kalba + Transliteracijos arba transliteravimo kalba + "Verčiamo kalbos taikinys: +%s" + "Romanizavimo arba transliteracijos taikinio kalba: +%s" Verčiama… Nepavyko išversti dainų tekstų Romanizuota @@ -621,7 +671,7 @@ Taikoma abiems riboms. Nustatant 1,75, keturi pilno pločio simboliai counted ka Atnaujinama Atnaujinta QuickLRC API raktas - „QuickLRC + QuickLRC transkribuoja arba suderina dainų tekstus pagal dabartinį takelį QuickLRC raktas išsaugotas. Naudokite sutapatinimo mygtuką dainų žodžių paieškos dialoge arba skydelio užklausą, kai takelyje nėra dainų žodžių QuickLRC API raktas išsaugotas QuickLRC priėmė tą raktą @@ -637,58 +687,50 @@ Taikoma abiems riboms. Nustatant 1,75, keturi pilno pločio simboliai counted ka Nėra dainų žodžių sutapatinimui - transkribuojama vietoj to QuickLRC negalėjo apdoroti šio kūrinio Šiam kūriniui QuickLRC skaitmenys baigėsi - "Tekstai sutapatinami pagal pavadinimą ir atlikėją, neįprasti metaduomenys gali nerasti nieko ar neteisingos versijos. Naudokite atnaujinimą, kad perjungtumėte versijas. Teikėjai, pažymėti *, reikalauja tokeno. Visi įgalinti teikėjai nėra visada prašomi jūsų tvarka, bet atsako pagal jų greitį. + "Tekstai sutampa pagal titulą ir artistą, neįprasta metadata gali rasti nieką ar neteisingą versiją. Naudokite atnaujinti pereiti prie kitų versijų. Teikėjai pažymėti * reikalauja rakto. Visi įjungti teikėjai nėra visada praunami jūsų tvarka, bet reaguoja pagal jų greitį. -Tekstai pateikiami trečiųjų šalių paslaugų, kurios nėra susijusios, remiamos ar rekomenduojamos Morphe. Kiekviena naudojama tokia, kokia yra: tekstai, jų prieinamumas ir licencijavimas priklauso tiems paslaugoms ir jų leidytojam, o jų naudojimas gali būti reglamentuojamas jų paslaugų teikimo sąlygomis. Į galinę paslaugą patenkantis duomenys yra tik užklausos, kurią paprauste - pavadinimas, atlikėjas ir, kur paslauga to reikalauja, video ID. Pasirenkamas AI endpointas ir QuickLRC transkripcija veikia tik tada, kai juos sukonfigūravote, ir gaunami tekstai ar takelio video URL." +Dainų tekstai ateina iš trečiųjų paslaugų, kurios nėra susietos, remiamos ar patvirtintos Morphe. Kiekviena naudojama kaip ji yra: dainų tekstai, jų prieinamumas ir jų licencijavimas lieka tie paslaugos ir jų licencijuotojai, ir naudojant juos gali būti pavaldūs jų tarnybų terminams. Kas pasiekia įjungtą paslaugą yra tik paieška, kurią paprašėte - titulas, menininkas, ir, kur paslauga jį reikia, video ID. Pasirinktinis AI galūnės ir QuickLRC transkripcija vykdoma tik po konfigūracijos, ir gauna dainų tekstą arba takelio video URL." Įjunkite tik tuos teikėjus, kuriuos naudojate, arba palikite numatytuosius - nuolatiniai prašymai gali apsunkinti mažoms bendruomenės tekstų svetaines Slėpti autorių eilutę Slėpti žemiau dainų tekstų rodomą autorių eilutę Sukeisti vertimą ir romanizaciją - ĮJUNGUS romanizacija rodoma žemiau dainų tekstų, o vertimas viršuje; IŠJUNGUS romanizacija lieka viršuje, o vertimas apačioje - Vertimas - Romanizacija - Vertimo teikėjas - \"Google (pabažtinė, nemokama)\" - \"OpenAI suderinamas (savarankiškas)\" - \"Romanizacijos teikėjas\" - \"Google (pabažtinė)\" - \"Vietinis / neprisijungęs žodynas\" - \"OpenAI suderinamas (savarankiškas)\" - AI skiltys - Naudojama tik tada, kai teikėjas nepateikia jokių skilčių žymių, pavyzdžiui, Priedainio arba Strofos, kurias tada aptinka jūsų su OpenAI suderinamas modelis - DI & debesijos galutiniai taškai - \"Konfigūruokite API raktus, modelius, galutinius taškus ir raginimo šablonus DI teikėjams.\" - \"API galutinio taško URL (pvz., https://api.openai.com/v1/chat/completions)\" + ĮJUNGTAS nustato transliteravimą arba transliteravimą po dainų tekstu ir viršuje vertimą; IŠJUNGTAS išlaiko transliteravimą arba transliteravimą viršuje ir vertimą apačioje. Tarimas, padėtas ant kiekvieno skiemenyje, juda kartu su juo, nuo viršaus iki apačios + Visada centruoti dainų tekstą + ĮJUNGTAS leidžia kiekvienai eilučiai būti viduryje ir ignoruoja šaltinio išdėstymą, įskaitant duoletines puses; IŠJUNGTAS išlaiko tokią poziciją, kokios prašo dainų tekstas + Bent vienas tiekėjas privalo būti aktyvus + Vertimas + Transliteracija arba transliteravimas + Nešliožiamasis žodynas + Rime žodynas + Aptikti sekcijas naudojant dirbtinį intelektą + Naudojama tik tada, kai tiekėjas neturi jokių sekcijų etikečių, pavyzdžiui, Chorus arba Verse, kurios tada aptinka jūsų OpenAI suderinamas modelis. Eilutės redagavimas taip pat atveria jos promptą + API pabaigos taško URL (pvz., https://api.openai.com/v1/chat/completions, https://api.anthropic.com/v1/chat/completions, https://generativelanguage.googleapis.com/v1beta/openai/chat/completions) Uždaužo šablonas Vietiniai kintamieji: {language}, {task}, {title}, {artist}, {count}, {lines} Uždauža išsaugota Užklausa išsaugota, bet AI galinė taškolis dar naudojamasi. Pirmiausia ją nustatykite AI nustatymuose Uždauža nustatyta iš naujo - Neteisingas JSON. Pataisykite jį arba išvalykite lauką prieš išsaugodami Vertimo & romanizacijos nustatymai išsaugoti Nustatymai grąžinti į standartinius Pagrindinis URL turi prasidėti http:// arba https:// - Patikrinti ryšį - Konfigūruoti galutinius taškus - Rodyti visus galutinius taškus + Nustatymai neišsamūs arba atmesti tiekėjo Tikrinamas ryšys… - Vertimo teikėjas - Romanizacijos teikėjas - Gauti modelius + %1$s: Prijungta! (%2$s) + %1$s: Nepavyko prisijungti. Patikrinkite nustatymus. + Pirmiausia įveskite API raktą Gaunama modeliai… Pasirinkite modelį Nepavyko gauti modelių. Patikrinkite API raktą arba ryšį. - \"Gemini\" API nustatymai - Suderinama su \"OpenAI\" (Tinkstamas galutinis taškas) - \"DeepL\" API nustatymai - API raktas + Promptas Modelis Pagrindinis URL + Pasirinkite tarnybą API raktas (nebūtina) - Vertimo raginimas Papildomas užklausos kūnas (nebūtinas JSON) + Papildomi prašymo antraščių parametrai (pasirenkamas JSON) + Kvotojus pavadinimus ir reikšmes, pvz., {\"api-key\": \"abc\"}. Nekvotuojama reikšmė, turinti / : arba skaitmenis, yra atmesta. Tuščia reikšmė šalina antraštę. + Kvotojus pavadinimus ir reikšmes, pvz., {\"temperature\": 0.3}. Nekvotuojama reikšmė, turinti / arba :, yra atmesta. Modelis ir žinutės čia negali būti nustatomi. Autentifikavimo raktas (nemokamas arba „Pro“) - Konfigūruoti API raktus, modelius, galutinius taškus ir raginimus 8.23.51 – išjungti paryškintas piktogramas 9.13.51 – atkurti grotuvo skirtukus „Toliau“, „Dainų tekstai“ ir „Susiję“ diff --git a/patches/src/main/resources/addresources/values-lt-rLT/youtube/strings.xml b/patches/src/main/resources/addresources/values-lt-rLT/youtube/strings.xml index cf26094a32..bf5b0d6d2d 100644 --- a/patches/src/main/resources/addresources/values-lt-rLT/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-lt-rLT/youtube/strings.xml @@ -29,6 +29,8 @@ Second \"item\" text" Įvairūs Vaizdo įrašai Atkurti senąjį „YouTube“ nustatymų ekraną + Rodyti žinutes YouTube „snackbar\" valdyme + „Morphe\" žinutės, pavyzdžiui, grojimo mygtuko patvirtinimas, rodomos YouTube „snackbar\" valdyme, o ne švyturiame Šalinti foninio atkūrimo apribojimus Išjungti foninį atkūrimą diff --git a/patches/src/main/resources/addresources/values-lv-rLV/music/strings.xml b/patches/src/main/resources/addresources/values-lv-rLV/music/strings.xml index 2c0cdfa143..d74a94c4d5 100644 --- a/patches/src/main/resources/addresources/values-lv-rLV/music/strings.xml +++ b/patches/src/main/resources/addresources/values-lv-rLV/music/strings.xml @@ -461,12 +461,11 @@ Lai nodrošinātu, ka scrobbling tiek sinhronizēts fona atskaņošanas laikā, Rādīt Sinhronizācija Panelis - AI gala punkts Visi pakalpojumu sniedzēji ir deaktivēti - iebūvētie teksti un iekļautie teksti Atiestatīt nodrošinātāju secību? Atjaunot nodrošinātāju nokārtojumu pēc prioritātes nokārtojuma vērtībām + Automātiski Notīrīt saglabātos dziesmu tekstus - Dzēst šajā ierīcē saglabātos dziesmu tekstus Notīrīts %1$s MB Iegūt marķītāju Apple Music mediju lietotāja marķeris @@ -526,13 +525,8 @@ Obligātās cookies: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPI Rādīt kopēšanas pogu Pieskarieties, lai nokopētu oriģinālo tekstu kā plain text; ilgāk nospiediet, lai saglabātu neapstrādāto teksta avotu failu Lejupielādēs Rādīt tulkošanas pogu - Pieskarieties, lai pārslēgtu tulkojumu; ilgāk nospiediet, lai redzētu tikai tulkojumu. Izmanto pakalpojumu tulkojumus, Google Translate vai OpenAI saderīgu API - Rādīt romanizācijas pogu - Pieskarieties, lai pārslēgtu romanizāciju; ilgāk nospiediet, lai redzētu tikai romanizāciju. Izmanto pakalpojuma romanizāciju, Google Translate vai OpenAI saderīgu API - Lokāla romanizācija un izlīzināšana - Veikt romanizāciju ierīcē, ja pakalpojums, jūsu modelis un Google Translate neveiksmīgi. Lasīšana seko aiz katra vārda sinhronizētajā tekstā; vārdu sinhronizācijai jābūt ieslēgtā. Ķīniešu, kantonešu, japāņu, korejiešu, grieķu, krievu, gruzīnu, armēņu valodām; aptuveni 120 MB - Lasīšana %1$s: %2$d%% Lejupielāde neizdevās - izslēdziet un ieslēdziet šo iespēju miezēšanai + Lasīšana %1$s: %2$d%% Rādīt atjaunināšanas pogu Kliediet, lai pārslēgtos uz nākamo dziesmu teksta variantu; aizturiet, lai meklētu dziesmu tekstu pēc dziesmas nosaukuma un izpildītāja Meklēt dziesmu tekstu @@ -603,7 +597,6 @@ Sadalīt aizpildīto trekuma formātu cikliski mainīgajos kadros. 0 izslēdz sa Tulkots Tulkojuma valoda - Romanizācijas valoda Tulkošana… Neizdevās iztulkot dziesmu tekstu Romanizēts @@ -617,7 +610,6 @@ Sadalīt aizpildīto trekuma formātu cikliski mainīgajos kadros. 0 izslēdz sa Atjaunīšana Atjaunots QuickLRC API atslēga - QuickLRC transkribē vai laika ziņojumā saskaņo vārdu tekstu pašai izselei. Atsevišķs pakalpojums, kas nav saistīts ar augstāk minēto OpenAI saderīgo gala punktu QuickLRC atslēga saglabāta. Izmantojiet saskaņošanas pogu vārdu tekstu meklēšanas dialogā vai paneļa aicinājumu, ja izselei nav vārdu teksta QuickLRC API atslēga saglabāta QuickLRC nenoņēma šo atslēgu @@ -633,56 +625,27 @@ Sadalīt aizpildīto trekuma formātu cikliski mainīgajos kadros. 0 izslēdz sa Nav vārdu teksta saskaņošanai - tā vietā tiek veikta transkribēšana QuickLRC nevara apstrādāt šo treku Šim trekam QuickLRC kredīti ir izlietēti - "Dziesmu teksti tiek meklēti pēc nosaukuma un izpildītāja; neparasta metadata var izraisīt neveiksmīgu meklēšanu vai nepareizas versijas atradēšanu. Izmantojiet atjaunināšanu, lai pārslēgtos starp versijām. Pakalpojumu sniedzēji, kas apzīmēti ar *, prasa tokenu. Visi iespējotie sniedzēji ne vienmēr tiek pieprasīti jūsu norādītajā secībā, bet atbild atkarībā no to ātruma.\n\nDziesmu teksti tiek saņemti no trešo pušu pakalpojumiem, kas nav saistīti ar Morphe, nav sponsorēti vai atbalsīti no Morphe. Katrs no tiem tiek izmantots tāds, kāds tas ir: teksti, to pieejamība un licencēšana paliek šo pakalpojumu un to licencējušu personu tuvumā, un to izmantošana var būt pakļauta viņu lietošanas noteikumiem. Aktivizētajam pakalpojumam tiek nosūtīts tikai jūsu pieprasītais meklējums - nosaukums, izpildītājs un, ja pakalpojumam tas ir nepieciešams, video ID. Papildus AI gala punkts un QuickLRC transkripcija darbojas tikai pēc to konfigurēšanas un saņem dziesmu tekstu vai izpildījuma video URL." Iespējojiet tikai tos paklājējus, kurus izmantojat, vai atstājiet noklājumu - pastāvīgi pieprasījumi var pārslogīt mazus kopienas dziesmu tekstu vietnes Slēpt kredītlīniju Slēpt kredītlīniju, kas parādās zem dziesmu teksta Mainīt tulkojuma un romanizācijas vietas - IEKLĒGTS novieto romanizāciju zem teksta un tulkojumu virs; IZSLĒGTS atstāj romanizāciju virs un tulkojumu zem - Tulkojums - Romanizācija - Tulkojuma nod வழங்குējs - Google (pēc noklusējuma, bezmaksas) - OpenAI saderīgs (pielāgots) - Romanizācijas pakalpojumu sniedzējs - Google (pēc noklusējuma) - Lokālais / bezsaistes vārdnīca - OpenAI saderīgs (pielāgots) - AI sadaļa - Tiek izmantots tikai tad, ja pakalpojumu sniedzējs nenodrošina sadaļu apzīmējumus, piemēram, Prijiedienums vai Kuplets, kurus pēc tam atpozīst jūsu OpenAI saderīgas modelis - AI & mākoņpakalpojumu gala punkti - Konfigurēt API atslēgas, modeļus, gala punktus un uzvednes šablonus AI pakalpojumu sniedzējiem. - API gala punkta URL (piem. https://api.openai.com/v1/chat/completions) Uzvednes šablons Vietneces: {language}, {task}, {title}, {artist}, {count}, {lines} Uzvedne saglabāta Aicinājums saglabāts, taču AI gala punkts vēl nav izmantojams. Vispirms konfigurējiet to AI iestatījumos Uzvedne atiestatīta - Nevalids JSON. Izlabojiet to vai izdzīvējiet lauku pirms saglabāšanas Tulkojuma & romanizācijas iestatījumi saglabāti Iestatījumi atiestatīti nokārtāvējumā Pamata URL adresei jāsāk ar http:// vai https:// - Pārbaudīt savienojumu - Konfigurēt gala punktus - Rādīt visus gala punktus Savienojuma pārbaude… - Tulkojuma nodrošinātājs - Romanizācijas nodrošinātājs - Iegūt modeļus Modeļi tiek ieguvti… Izvēlieties modeli Neizdevās ieguvēt modeļus. Pārbaudiet API atslēgu vai savienojumu. - Gemini API iestatījumi - OpenAI saderīgs (pielagots gala punkts) - DeepL API iestatījumi - API atslēga Modelis Pamata URL API atslēga (neobligāta) - Tulkošanas uzvedne Papildu pieprasījuma korpuss (neobligāts JSON) Autentifikācijas atslēga (Free vai Pro) - Konfigurēt API atslēgas, modeļus, gala punktus un uzvednes 8.23.51 - Atslēgt tremūsas ikonas 9.13.51 - Atjaunot atskaņotāja cilnes Tālāk, Dziesmu teksti un Saistītie diff --git a/patches/src/main/resources/addresources/values-mk-rMK/music/strings.xml b/patches/src/main/resources/addresources/values-mk-rMK/music/strings.xml index 60933b346e..e5b1f71615 100644 --- a/patches/src/main/resources/addresources/values-mk-rMK/music/strings.xml +++ b/patches/src/main/resources/addresources/values-mk-rMK/music/strings.xml @@ -462,12 +462,11 @@ SABR репродукција (имитирање на видео стримов Приказ Синхронизација Панел - AI крајна точка Сите извори се исклучени - вградени текстови и вметнати текстови Ресетирај го редоследот на провајдерите? Врати ги провајдерите на стандардниот редослед на приоритет + Автоматски Исчисти ги зачуваните текстови - Избришете ги текстовите за песните зачувани на овој уред Исчистено %1$s MB Земи токен Apple Music Media-user-token @@ -527,13 +526,8 @@ SABR репродукција (имитирање на видео стримов Прикажи копче за копирање Допрете за да ги копирате оригиналните текстови како обичен текст; задржете за да ја зачувате изворната датотека на текстот во Downloads Прикажи копче за преведување - Допрете за да го смените претставот на преводот; задржете само за превод. Користи преводи од провајдерот, Google Translate или OpenAI-компатибилен API - Прикажи копче за романизација - Допрете за да ја смените романизацијата; задржете само за романизација. Користи романизација од провајдерот, Google Translate или OpenAI-компатибилен API - Локална романизација и усогласување - Романизирај на уредот кога провајдерот, вашиот модел и Google Translate не успеат. Читањето го следи секој збор од синхронизираниот текст; синхронизацијата на зборовите мора да биде вклучена. Кинески, кантонски, јапонски, корејски, грчки, руски, грузински, ерменски; околу 120 MB - Читање на %1$s: %2$d%% Преземањето не успеа - исклучете го и повторно вклучете го ова за да се обидете повторно + Читање на %1$s: %2$d%% Прикажи копче за освежување Допрете за да преминете на следниот кандидат за текст; задржете за пребарување текст според насловот на песната и изведувачот Пребарај текст на песни @@ -604,7 +598,6 @@ SABR репродукција (имитирање на видео стримов Преведено Јазик за превод - Јазик за романизација Преведување… Не успеа да се преведат текстовите Романизирано @@ -618,7 +611,6 @@ SABR репродукција (имитирање на видео стримов Освежување Освежено QuickLRC API клуч - QuickLRC ги транскрибира или ги усогласува по време текстовите за тековната нутра. Посебна услуга, неповрзана со OpenAI компатибилниот краен точка погоре Клучот за QuickLRC е зачуван. Користете го копчето за порамнување во дијалогот за пребарување текстови, или наведењето во панелот кога траката нема текст QuickLRC API клучот е зачуван QuickLRC не го прифати тој клуч @@ -634,56 +626,27 @@ SABR репродукција (имитирање на видео стримов Нема текст за порамнување - наместо тоа се врши транскрипција QuickLRC не може да ја обработи оваа песна Кредитите на QuickLRC се исцрпени за оваа песна - "Текстовите се совпаѓаат според насловот и изведувачот; необичните метаподатоци може да не најдат ништо или погрешна верзија. Користете го освежувањето за промена на верзиите. Давателите означени со * бараат токен. Сите овозможени даватели не се секогаш бараат според вашиот редослед, туку одговараат врз основа на нивната брзина.\n\nТекстовите доаѓаат од услуги од трети страни кои не се поврзани со Morphe, не се спонзорирани или одобрени од Morphe. Секоја од нив се користи таква каква што е: текстовите, нивната достапност и нивното лиценцирање остануваат кај тие услуги и нивните лиценциратели, а нивното користење може да подлежи на нивните услови за користење. Она што стигнува до овозможената услуга е само пребарувањето што го побаравте - насловот, изведувачот и, таму каде што таа услуга го бара, видео ИД-то. Опционалниот AI крајен точка и QuickLRC транскрипцијата се извршуваат само откако ќе ги конфигурирате, и примаат го текстот на песната или видео URL-тата на траката." Овозможете ги само провајдерите што ги користите или задржете ги стандардните - постојаните барања можат да ги преоптоварат малите заеднички сајтови за текстови Скриј ја линијата за заслуги Скриј ја линијата за заслуги прикажана под текстот Замени го преводот и романизацијата - ВКЛ ја става романизацијата под текстот и преводот над; ИСКЛ ја држи романизацијата над и преводот под - Превод - Романизација - Провајдер за превод - Google (Стандардно, Бесплатно) - Компатибилно со OpenAI (Прилагодено) - Доставувач на романизација - Google (Стандардно) - Локален / Офлајн речник - Компатибилно со OpenAI (Персонализирано) - AI секција - Се користи само кога давачот не испраќа ознаки за секции како Рефрен или Строфа, кои потоа ги детектира вашиот модел компатибилен со OpenAI - AI & Cloud крајни точки - Конфигурирајте API клучеви, модели, крајни точки и шаблони за прашања за AI дагувачи. - URL на API крајната точка (на пр. https://api.openai.com/v1/chat/completions) Шаблон за промпт Заменители: {language}, {task}, {title}, {artist}, {count}, {lines} Налогот е зачуван Наведењето е зачувано, но AI крајната точка сè уште не е употреблива. Прво конфигурирајте ја во AI поставките Налогот е ресетиран - Невалиден JSON. Поправете го или исчистете го полето пред зачувување Подесувањата за превод и & романизација се зачувани Подесувањата се ресетирани на стандардните Основната URL адреса мора да започнува со http:// или https:// - Тестирај ја конекцијата - Конфигурирајте крајни точки - Прикажи ги сите крајни точки Валидирање на конекцијата… - Дагувач за превод - Дагувач за романизација - Преземи модели Преземање модели… Изберете модел Не успеа преземањето на моделите. Проверете го API клучот или конекцијата. - Поставки за Gemini API - Компатибилно со OpenAI (Custom Endpoint) - Поставки за DeepL API - API клуч Модел Основен URL API клуч (опционално) - Нарачка за превод Дополнително тело на барањето (опционален JSON) Клуч за автентикација (Бесплатен или Pro) - Конфигурирајте API клучеви, модели, крајни точки и промптовен текст 8.23.51 - Оневозможи задебелени икони 9.13.51 - Врати ги табовите Следувачки, Текстови и Поврзано во плејачот diff --git a/patches/src/main/resources/addresources/values-mn-rMN/music/strings.xml b/patches/src/main/resources/addresources/values-mn-rMN/music/strings.xml index fc3312c361..b20faa83b9 100644 --- a/patches/src/main/resources/addresources/values-mn-rMN/music/strings.xml +++ b/patches/src/main/resources/addresources/values-mn-rMN/music/strings.xml @@ -462,12 +462,10 @@ SABR тоглуулалт (Видео урсгалыг дуурайх функц Харуулах Синхрончлол Панель - AI эгц Бүх нийлүүлэгчид идэвхгүй болсон - суурилуулсан болон быцгадсан дууны үгс Нийлүүлэгчийн дарааллыг шинэчлэх үү? Нийлүүлэгчдийг анхны тэргүүлэх дарааллаар нь сэргээх Хадгалагдсан дууны үгсийг устгах - Энэ төхөөрөмж дээр хадгалагдсан дууны үгсийг устгах %1$s МБ цэвэрлэгдлээ Токен авах Apple Music Media-user-token @@ -527,13 +525,8 @@ SABR тоглуулалт (Видео урсгалыг дуурайх функц Хуулах товчлуурыг харуулах Эх хувь дууны үгийг энгийн текст хэлбэрээр хуулахын тулд дарна уу; түүхий эх файлыг Татаж авах хавтсанд хадгалахын тулд удаан дарна уу Орчуулах товчийг харуулах - Орчуулгыг идэвхжүүлэхийн тулд товшино; зөвхөн орчуулга харахын тулд удаан дарна. Үйлчилгээ үзүүлэгчийн орчуулга, Google Translate эсвэл OpenAI-тай нийцсэн API ашиглана - Romanize товчийг харуулах - Романизациг идэвхжүүлэхийн тулд товшино; зөвхөн романизаци харахын тулд удаан дарна. Үйлчилгээ үзүүлэгчийн романизаци, Google Translate эсвэл OpenAI-тай нийцсэн API ашиглана - Дотоод романизаци болон тохируулга - Үйлчилгээ үзүүлэгч, таны загвар болон Google Translate-ийн алдаа гарсан үед төхөөрөмж дээр романизаци хийнэ. Уншилт нь цаг хугацаагаар тохируулсан дууны үг бүрийн дараа гарна; үг синхрончлол идэвхтэй байх ёстой. Хятад, Кантон, Япон, Солонгос, Грек, Орос, Георги, Армени; ойролцоогоор 120 МБ - %1$s уншиж байна: %2$d%% Татаж авахад алдаа гарлаа - дахин оролдохын тулд үүнийг унтраагаад асаана уу + %1$s уншиж байна: %2$d%% Refresh товчийг харуулах Дараагийн дууны үг рүү шилжихэд товшино; дууны нэр болон уран бүтээгчээр хайхын тулд удаан дарна Дууны үг хайх @@ -604,7 +597,6 @@ SABR тоглуулалт (Видео урсгалыг дуурайх функц Орчуулагдсан Орчуулгын хэл - Романизацийн хэл Орчуулж байна… Дууны үгийг орчуулж чадсангүй Романизаци хийгдсэн @@ -618,7 +610,6 @@ SABR тоглуулалт (Видео урсгалыг дуурайх функц Шинэчилж байна Шинэчиллээ QuickLRC API түлхүүр - QuickLRC нь одоогийн трекийн дууны үгийг хөрвүүлдэг эсвэл хугацаанд нь тааруулдаг. Энэ нь дээрх OpenAI-тай нийцтэй төгсгөл цэгтэй хамааралгүй тусдаа үйлчилгээ юм QuickLRC түлхүүрийг хадгаллаа. Дууны үг хайх цонхны \"align\" товчлуур эсвэл трект дууны үггүй үед гарч ирэх панелийн зааврыг ашиглана уу. QuickLRC API түлхүүрийг хадгаллалаа QuickLRC уг түлхүүрийг хүлээн зөвшөөрөөгүй @@ -634,56 +625,27 @@ SABR тоглуулалт (Видео урсгалыг дуурайх функц Тааруулах дууны үг алга - оронд нь хөрвүүлж байна QuickLRC энэ дууг боловсруулж чадсангүй Энэ дууны хувьд QuickLRC-ийн эрх дууссан байна - "Дууны үгийг гарчиг болон уран бүтээлчийн нэрээр хайх бөгөөд мета өгөгдөл буруу байх тохиолдолд хайлт амжилтгүй болох эсвэл буруу хувилбар гарч ирж болзошгүй. Хувилбарыг солихын тулд шинэчлэхийг ашиглана уу. * тэмдэглэгээтэй нийлүүлэгчдэд токен шаардлагатай. Идэвхжүүлсэн бүх нийлүүлэгчдэд таны заасан дарааллаар хүсэлт илгээхгүй бөгөөд хариу өгөх хурдаас нь хамаарч харуулах болно.\n\nДууны үг нь Morphe-тэй хамааралгүй, түүний ивээн тэтгэлгэсэн эсвэл дэмжсэн бугуй хөндлөнгийн үйлчилгээнээс ирдэг. Үйлчилгээ бүрийг байгаагаар нь ашиглах бөгөөд дууны үг, тэдгээрийн хүртээмж болон лиценз нь тухайн үйлчилгээ болон лиценз эзэмшигчийнх бөгөөд тэдгээрийг ашиглах нь тухайн үйлчилгээний үйлчилгээний нөхцөлд зохицуулагдана. Идэвхжүүлсэн үйлчилгээнд зөвхөн таны хүсэлт болсон хайлт буюу гарчиг, уран бүтээлч болон шаардлагатай тохиолдолд видео ID илгээгдэнэ. Нэмэлт AI эгц болон QuickLRC транскрипц нь таны тохиргооны дараа л ажиллах бөгөөд дууны үгийн текст эсвэл трекийн видео URL-ыг хүлээн авна." Зөвхөн ашигладаг Źүрүүлэгчүүдээ идэвхжүүлж эсвэл анхны тохиргоогоор үлдээгээрэй - байнгын хүсэлтүүд нь жижиг олон нийтийн дууны үгний сайтуудыг хэт ачаалж болзошгүй Зээрийн мөрийг нуух Дууны үгний доор харагдаж буй зээрийн мөрийг нуух Орчуулга болон романизацийг солих - ЧАЛУУНААСАН үед романизацийг дууны үгний доор, орчуулгыг дээр нь байрлуулна; УНТРУУЛСАН үед романизацийг дээр, орчуулгыг доор нь байлгана - Орчуулга - Романизаци - Орчуулгын үйлчилгээ үзүүлэгч - Google (Үндсэн, Үнэгүй) - OpenAI-тай нийцтэй (Заалттай) - Романизацийн үйлчилгээ үзүүлэгч - Google (Өгөгдすることで) - Дотоод / Офлайн тольбиур - OpenAI-тай нийцсэн (Захиалгаар) - AI хэсэг - Зөвхөн нийлүүлэгч Chorus эсвэл Verse гэх мэт хэсгийн шовруудыг илгээгээгүй, OpenAI-тэй нийцэх загвар чинь тэдгээрийг илрүүлдэг тохиолдолд ашиглана - AI & Cloud Endpoints - AI үйлчилгээ үзүүлэгчдийн API түлхүүр, модель, endpoint болон prompt загварыг тохируулна. - API endpoint URL (жишээ нь: https://api.openai.com/v1/chat/completions) Prompt загвар Орлуулах утгууд: {language}, {task}, {title}, {artist}, {count}, {lines} Промпт хадгалагдлаа Заавар хадгалагдсан боловч AI төгсгөл цэг хараахан ашиглах боломжгүй байна. Эхлээд AI тохиргоо хэсэгт тохируулна уу. Промптыг шинэчлэлээ - JSON хүчингүй байна. Хадгалахын өмнө засах эсвэл талбарыг хойвгоруулна уу Орчуулга & Романизацийн тохиргоо хадгалагдлаа Тохиргоог анхны төлөвт нь буцаалаа Үндсэн URL нь http:// эсвэл https://-ээр эхлэх ёстой - Холболтыг шалгах - Төгсгөлийн цэгүүдийг тохируулах - Бүх төгсгөлийн цэгүүдийг харуулах Холболтыг шалгаж байна… - Орчуулгын үйлчилгээ үзүүлэгч - Романизацийн үйлчилгээ үзүүлэгч - Загваруудыг татах Загваруудыг татаж байна… Загвар сонгоно уу Моделүүдийг татах боломжгүй боллоо. API түлхүүр эсвэл холболтоо шалгана уу. - Gemini API-ийн тохиргоо - OpenAI-тай нийцэх (Custom Endpoint) - DeepL API-ийн тохиргоо - API түлхүүр Загвар Үндсэн URL API түлхүүр (сонгомжтой) - Орчуулгын заавар Нэмэлт хүсэлтийн бие (сонгомжтой JSON) Баталгаажуулалтын түлхүүр (Free эсвэл Pro) - API түлхүүр, загвар, төгсгөл цэг болон заавруудыг тохируулах 8.23.51 - Тод дүрсүүдийг идэвхгүй болгох 9.13.51 - Тоглуулагчийн Дараагийн, Дууны үг болон Холбоотой цэсүүдийг сэргээх diff --git a/patches/src/main/resources/addresources/values-mr-rIN/music/strings.xml b/patches/src/main/resources/addresources/values-mr-rIN/music/strings.xml index 8fe2f6d763..75abb9c7c5 100644 --- a/patches/src/main/resources/addresources/values-mr-rIN/music/strings.xml +++ b/patches/src/main/resources/addresources/values-mr-rIN/music/strings.xml @@ -461,12 +461,10 @@ SABR प्लेबॅक (Spoof व्हिडिओ स्ट्रीम् डिस्प्ले सिंक्रोनाइझेशन पॅनेल - AI एंडपॉइंट सर्व प्रोव्हायडर्स अक्षम आहेत - बिल्ट-इन लिरिक्स आणि एम्बेडेड लिरिक्स प्रोव्हायडर क्रम रीसेट करायचा आहे का? प्रोव्हायडर्स डीफॉल्ट प्रायोरिटी क्रमात पुनर्संचयित करा स्टोअर केलेले लिरिक्स हटवा - या डिव्हाइसवर ठेवलेले लिरिक्स डिलीट करा %1$s MB क्लिअर केले टोकन मिळवा Apple Music Media-user-token @@ -526,14 +524,9 @@ SABR प्लेबॅक (Spoof व्हिडिओ स्ट्रीम् कॉपी बटण दाखवा मूळ लिरिक्स प्लेन टेक्स्ट म्हणून कॉपी करण्यासाठी टॅप करा; रॉ लिरिक्स सोर्स फाईल डाउनलोड्समध्ये सेव्ह करण्यासाठी दीर्घकाळ दाबा भाषांतर बटण दाखवा - अनुवाद टॉगल करण्यासाठी टॅप करा; केवळ अनुवादासाठी दीर्घकाळ दाबा. हे प्रोव्हायडर ट्रान्सलेशन, Google Translate किंवा OpenAI-सुसंगत API वापरते - रोमनाइझ बटण दाखवा - रोमनायझेशन टॉगल करण्यासाठी टॅप करा; फक्त रोमनायझेशनसाठी लाँग-प्रेस करा. प्रोव्हायडर रोमनायझेशन, Google Translate किंवा OpenAI-सुसंगत API वापरते - स्थानिक रोमनकरण आणि अलाइनमेंट - जेव्हा प्रदाता, तुमचे मॉडेल आणि Google Translate अयशस्वी होतात तेव्हा डिव्हाइसवर रोमनकरण करा. वाचन हे वेळेनुसार दिलेल्या प्रत्येक शब्दांनतर येते; शब्द सिंक्रोनाइझेशन चालू असणे आवश्यक आहे. चिनी, कॅन्टोनीज, जपानी, कोरियन, ग्रीक, रशियन, जॉर्जियन, आर्मेनियन; सुमारे १२० MB - वाचत आहे %1$s: %2$d%% वाचन तयार आहे (%1$d नोंदी) डाउनलोड अयशस्वी - पुन्हा प्रयत्न करण्यासाठी हे बंद करून पुन्हा चालू करा + वाचत आहे %1$s: %2$d%% रिफ्रेश बटण दाखवा पुढील लिरिक्स उमेदवाराकडे जाण्यासाठी टॅप करा; गाण्याचे शीर्षक आणि कलाकाराद्वारे लिरिक्स शोधण्यासाठी दीर्घकाळ दाबा गीत शोधा @@ -604,7 +597,6 @@ SABR प्लेबॅक (Spoof व्हिडिओ स्ट्रीम् अनुवादित अनुवादाची भाषा - रोमनायझेशन भाषा भाषांतर करत आहे… लिरिक्सचे भाषांतर करू शकलो नाही रोमनाइझ्ड @@ -618,7 +610,6 @@ SABR प्लेबॅक (Spoof व्हिडिओ स्ट्रीम् रिफ्रेश होत आहे रिफ्रेश झाले QuickLRC API की - QuickLRC सध्याच्या ट्रॅकसाठी लिरिक्स ट्रान्सक्राइब करते किंवा टाइम-अलाइन करते. ही एक स्वतंत्र सेवा आहे, ज्याचा वर दिलेल्या OpenAI-सुसंगत एंडपॉइंटशी संबंध नाही QuickLRC की सेव्ह केली. लिरिक्स सर्च डायलॉगमधील अलाइन बटण वापरा, किंवा जेव्हा ट्रॅकला लिरिक्स नसतील तेव्हा पॅनेल प्रॉम्प्ट वापरा QuickLRC API की जतन केली QuickLRC ने ती की स्वीकारली नाही @@ -634,56 +625,27 @@ SABR प्लेबॅक (Spoof व्हिडिओ स्ट्रीम् अलाइन करण्यासाठी लिरिक्स उपलब्ध नाहीत - त्याऐवजी ट्रान्सक्रिप्शन केले जात आहे QuickLRC या ट्रॅकवर प्रक्रिया करू शकले नाही या ट्रॅकसाठी QuickLRC क्रेडिट्स संपले आहेत - "गीते शीर्षक आणि कलाकाराद्वारे मॅच केली जातात, असामान्य मेटाडेटा मुळे काहीही न मिळणे किंवा चुकीची आवृत्ती मिळणे शक्य आहे. आवृत्ती बदलण्यासाठी रिफ्रेश वापरा. * चिनित प्रोव्हायडर्सना टोकनची आवश्यकता असते. सर्व सक्षम प्रोव्हायडर्सना नेहमी तुमच्या क्रमाने विनंती केली जात नाही, तर ते त्यांच्या वेगावर आधारित प्रतिसाद देतात.\n\nगीते तृतीय-पक्ष सेवांकडून येतात ज्या Morphe शी संलग्न, प्रायोजित किंवा समर्थित नाहीत. प्रत्येक सेवा जशी आहे तशीच वापरली जाते: गीते, त्यांची उपलब्धता आणि त्यांचे लायसेंसिंग त्या सेवांकडे आणि त्यांच्या लायसेंसर्सकडे राहते आणि त्यांचा वापर त्यांच्या सेवाशर्तींच्या अधीन असू शकतो. सक्षम सेवेपर्यंत केवळ तुम्ही विचारलेली शोध माहिती पोहोचते - शीर्षक, कलाकार आणि ज्या सेवेला गरज असेल तिथे व्हिडिओ आयडी. पर्यायी AI एंडपॉइंट आणि QuickLRC ट्रान्स्क्रिप्शन केवळ तुम्ही कॉन्फिगर केल्यानंतरच चालतात आणि त्यांना गीतांचा मजकूर किंवा ट्रॅकची व्हिडिओ URL मिळते." तुम्ही वापरत असलेल्या प्रोव्हायडर्सनाच सक्षम करा, किंवा डीफॉल्ट ठेवा - सततच्या विनंत्यांमुळे लहान कम्युनिटी लिरिक्स साइट्सवर ताण येऊ शकतो क्रेडिट लाईन लपवा गीतांच्या खाली दिसणारी क्रेडिट लाईन लपवा अनुवाद आणि रोमनाइझेशनची अदलाबदल करा - ON केल्यास रोमनाइझेशन गीतांच्या खाली आणि अनुवाद वर येईल; OFF केल्यास रोमनाइझेशन वर आणि अनुवाद खाली राहील - भाषांतर - रोमनायझेशन - भाषांतर प्रदाता - Google (बाय डीफॉल्ट, मोफत) - OpenAI सुसंगत (कस्टम) - रोमनायझेशन प्रदाता - Google (डीफॉल्ट) - स्थानिक / ऑफलाइन डिक्शनरी - OpenAI सुसंगत (कस्टम) - AI सेक्शन - केवळ तेव्हाच वापरले जाते जेव्हा प्रोव्हायडर कोरस किंवा व्हर्स सारखी सेक्शन लेबल्स पाठवत नाही, जी तुमचे OpenAI-सुसंगत मॉडेल नंतर शोधते - AI & Cloud Endpoints - AI प्रदात्यांसाठी API की, मॉडेल्स, एंडपॉइंट्स आणि प्रॉम्प्ट टेम्पलेट्स कॉन्फिगर करा. - API एंडपॉइंट URL (उदा. https://api.openai.com/v1/chat/completions) प्रॉम्प्ट टेम्पलेट प्लेसहोल्डर्स: {language}, {task}, {title}, {artist}, {count}, {lines} प्रॉम्प्ट सेव्ह केला प्रॉम्ट सेव्ह झाला आहे, परंतु AI एंडपॉइंट अजून वापरण्यायोग्य नाही. प्रथम AI सेटिंग्जमध्ये त्याचे कॉन्फिगरेशन करा प्रॉम्ट रीसेट केला - वैध JSON नाही. सेव्ह करण्यापूर्वी ते दुरुस्त करा किंवा फील्ड रिकामे करा भाषांतर आणि रोमनायझेशन सेटिंग्ज जतन केल्या सेटिंग्ज डीफॉल्टवर रिसेट केल्या बेस URL ची सुरुवात http:// किंवा https:// ने झाली पाहिजे - कनेक्शन तपासा - एंडपॉइंट्स कॉन्फिगर करा - सर्व एंडपॉइंट्स दाखवा कनेक्शनची पडताळणी करत आहे… - भाषांतर प्रदाता - रोमनाइझेशन प्रदाता - मॉडेल्स मिळवा मॉडेल्स मिळवत आहे… मॉडेल निवडा मॉडेल्स मिळवण्यात अयशस्वी. API की किंवा कनेक्शन तपासा. - Gemini API सेटिंग्ज - OpenAI सुसंगत (कस्टम एंडपॉइंट) - DeepL API सेटिंग्ज - API की मॉडेल बेस URL API की (पर्यायी) - अनुवाद प्रॉम्प्ट अतिरिक्त विनंती बॉडी (पर्यायी JSON) प्रमाणीकरण की (Free किंवा Pro) - API की, मॉडेल्स, एंडपॉइंट्स आणि प्रॉम्प्ट्स कॉन्फिगर करा 8.23.51 - ठळक चिन्ह अक्षम करा 9.13.51 - प्लेयर अप नेक्स्ट, गीत आणि संबंधित टॅब पुनर्संचयित करा diff --git a/patches/src/main/resources/addresources/values-ms-rMY/music/strings.xml b/patches/src/main/resources/addresources/values-ms-rMY/music/strings.xml index a199457ad6..398449e660 100644 --- a/patches/src/main/resources/addresources/values-ms-rMY/music/strings.xml +++ b/patches/src/main/resources/addresources/values-ms-rMY/music/strings.xml @@ -462,12 +462,11 @@ Untuk memastikan scrobbling disinkronkan semasa memainkan latar belakang, pastik Paparan Penyinkronan Panel - Titik akhir AI Semua pembekal dinyahaktifkan - lirik terbina dalam dan lirik terbenam Set semula urutan pembekal? Pulihkan pembekal ke urutan keutamaan lalai + Automatik Kosongkan lirik tersimpan - Padam lirik yang disimpan pada peranti ini %1$s MB telah dikosongkan Dapatkan token Token-pengguna-media Apple Music @@ -527,14 +526,9 @@ Kuki yang diperlukan: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAP Tunjukkan butang Salin Ketik untuk menyalin lirik asal sebagai teks biasa; tekan lama untuk menyimpan fail sumber lirik mentah ke Muat Turun Tunjukkan butang Terjemah - Ketik untuk menukar terjemahan; tekan lama untuk terjemahan sahaja. Menggunakan terjemahan penyedia, Google Translate, atau API yang serasi dengan OpenAI - Tunjukkan butang Romanize - Ketik untuk menukar romanisasi; tekan lama untuk romanisasi sahaja. Menggunakan romanisasi penyedia, Google Translate, atau API yang serasi dengan OpenAI - Romanisasi dan Penjajaran Tempatan - Romanisasi pada peranti apabila pembekal, model anda dan Google Translate gagal. Bacaan mengikuti setiap perkataan lirik mengikut masa; penyinkronan perkataan mesti dihidupkan. Cina, Kantonis, Jepun, Korea, Greek, Rusia, Georgia, Armenia; kira-kira 120 MB - Membaca %1$s: %2$d%% Bacaan sedia (%1$d entri) Muat turun gagal - matikan dan hidupkan semula pilihan ini untuk mencuba lagi + Membaca %1$s: %2$d%% Tunjukkan butang Segarkan Ketik untuk tukar ke calon lirik seterusnya; tekan lama untuk cari lirik mengikut tajuk lagu dan artis Cari lirik @@ -605,7 +599,6 @@ Terpakai untuk kedua-dua had. Pada 1.75, empat aksara lebar penuh dikira sebagai Diterjemah Bahasa terjemahan - Bahasa romanisasi Menterjemah… Tidak dapat menterjemah lirik Diromankan @@ -619,7 +612,6 @@ Terpakai untuk kedua-dua had. Pada 1.75, empat aksara lebar penuh dikira sebagai Menyegarkan Disegarkan Kunci API QuickLRC - QuickLRC mentranskripsi atau menyelaraskan masa lirik untuk trek semasa. Perkhidmatan berasingan, tidak berkaitan dengan titik akhir yang serasi dengan OpenAI di atas Kunci QuickLRC disimpan. Gunakan butang selaras dalam dialog carian lirik, atau gesaan panel apabila trek tidak mempunyai lirik Kunci API QuickLRC disimpan QuickLRC tidak menerima kunci tersebut @@ -635,56 +627,27 @@ Terpakai untuk kedua-dua had. Pada 1.75, empat aksara lebar penuh dikira sebagai Tiada lirik untuk diselaraskan - mentranskripsi sebagai ganti QuickLRC tidak dapat memproses trek ini Kredit QuickLRC telah habis digunakan untuk trek ini - "Lirik dipadankan mengikut tajuk dan artis, metadata yang tidak biasa mungkin tidak menemui apa-apa atau versi yang salah. Gunakan segar semula untuk menukar versi. Penyedia yang ditanda * memerlukan token. Semua penyedia yang didayakan tidak sentiasa diminta mengikut turutan anda, tetapi bertindak balas berdasarkan kelajuan mereka.\n\nLirik datang daripada perkhidmatan pihak ketiga yang tidak bergabung, ditaja, atau disahkan oleh Morphe. Setiap satu digunakan seadanya: lirik, ketersediaannya, dan pelesenannya kekal dengan perkhidmatan tersebut dan pemberi lesen mereka, dan menggunakannya boleh tertakluk kepada terma perkhidmatan mereka. Apa yang sampai ke perkhidmatan yang didayakan hanyalah carian yang anda minta - tajuk, artis, dan, jika perkhidmatan itu memerlukannya, ID video. Titik akhir AI pilihan dan transkripsi QuickLRC hanya berjalan selepas anda mengkonfigurasinya, dan menerima teks lirik atau URL video trek tersebut." Hanya aktifkan pembekal yang anda gunakan, atau kekalkan tetapan lalai - permintaan berterusan boleh membebankan laman lirik komuniti yang kecil Sembunyikan baris kredit Sembunyikan baris kredit yang dipaparkan di bawah lirik Tukar terjemahan dan romanisasi - HIDUP meletakkan romanisasi di bawah lirik dan terjemahan di atas; MATI mengekalkan romanisasi di atas dan terjemahan di bawah - Terjemahan - Romanisasi - Pembekal terjemahan - Google (Lalai, Percuma) - Serasi OpenAI (Kastam) - Pembekal romanisasi - Google (Lalai) - Kamus Tempatan / Luar Talian - Serasi OpenAI (Tersuai) - Bahagian AI - Hanya digunakan apabila pembekal tidak menghantar label bahagian seperti Chorus atau Verse, yang kemudiannya dikesan oleh model serasi OpenAI anda - Titik Akhir AI & Awan - Konfigurasikan kunci API, model, titik akhir, dan templat gesaan untuk penyedia AI. - URL titik akhir API (cth. https://api.openai.com/v1/chat/completions) Templat Prompt Placeholder: {language}, {task}, {title}, {artist}, {count}, {lines} Prompt disimpan Gesaan disimpan, tetapi titik akhir AI belum boleh digunakan. Konfigurasikannya di bawah Tetapan AI terlebih dahulu Prompt ditetapkan semula - JSON tidak sah. Betulkan atau kosongkan medan sebelum menyimpan Tetapan Terjemahan & Romanisasi disimpan Tetapan ditetapkan semula ke lalai URL asas mesti bermula dengan http:// atau https:// - Uji Sambungan - Konfigurasi Titik Akhir - Tunjukkan Semua Titik Akhir Menyahkan sambungan… - Pembekal Terjemahan - Pembekal Romanisasi - Ambil Model Mengambil model… Pilih Model Gagal mengambil model. Semak kunci API atau sambungan. - Tetapan API Gemini - Serasi OpenAI (Titik Akhir Tersuai) - Tetapan API DeepL - Kunci API Model URL Asas Kunci API (pilihan) - Prompt terjemahan Badan permintaan tambahan (JSON pilihan) Kunci pengesahan (Percuma atau Pro) - Konfigurasikan kunci API, model, titik akhir, dan prompt 8.23.51 - Nyahaktifkan ikon tebal 9.13.51 - Pulihkan tab Seterusnya, Lirik, dan Berkaitan Pemain diff --git a/patches/src/main/resources/addresources/values-nb-rNO/music/strings.xml b/patches/src/main/resources/addresources/values-nb-rNO/music/strings.xml index d5f7bf0ac1..e4c3f39e3c 100644 --- a/patches/src/main/resources/addresources/values-nb-rNO/music/strings.xml +++ b/patches/src/main/resources/addresources/values-nb-rNO/music/strings.xml @@ -460,12 +460,47 @@ For å sikre at scrobbling synkroniseres under avspilling i bakgrunnen, må du s Visning Synkronisering Panel - AI-endepunkt Alle leverandører deaktivert - innebygde tekster og innebygde tekster Tilbakestill leverandørrekkefølgen? Tilbakestill leverandørene til standard prioritetsrekkefølge + Rime-ordliste + "Rime-ordlisten trenger også UniDict: I Offline Dictionary-provideren, merk av \"Kinesiske og kantonesiske tegn (UniDict)\" og la den være ferdig med å laste ned først. Uten det gir en liste som leser tegn ingen utlesning. + +Merk av listene Rime-ordlisten skal lese, trykk deretter Lagre for å laste dem ned. En liste du har lagt til, fjernes når du fjerner merket fra den. + +Hver liste er ditt eget ord-til-utlestable, en .dict.yaml-fil med linjer slik som: +字 ham +典 din2 +Første kolonne er ordet, andre kolonne er utledningen; ekstra kolonner og linjer uten tabulatortegn blir ignorert." + Ingen Rime-ordliste er merket for språket ovenfor. Trykk på navnet for å merke én, eller velg et språk som har en liste + Legg til + Legg til en Rime-ordliste + En .dict.yaml-fil, eller et zip-arkiv som inneholder en slik fil, med linjer slik som 字 ham og 典 din2. Listen hentes når du markerer den og trykker Lagre, og lisensen er din å sjekke + Språkkode + To eller tre bokstaver, som hak eller wuu + Navn + Vises under uttalen, som Hakka (Meixian) + Adresse til ordlisten + https://-adresse til .dict.yaml eller dens zip + Tegn ikke i et ord + Automatisk + Kantonesiske tegn + Mandarintegn + Ingen tegnuttale + Fyll inn koden, navnet og adressen + Koden må være to eller tre bokstaver, som hak + Adressen må starte med https:// + Navnet og adressen kan ikke inneholde et vertikalt strek + %1$s har allerede en ordtabell + %1$s blir lest av den innebygde romaniseringen og kan ikke ta imot en ordliste + Ikke brukt: dette språket har allerede sin egen uttale, så fjern merkingen for å fjerne den + La til %1$s - merk den og trykk Lagre for å laste den ned + Ordlistene er klare + Ordlistene er klare (%1$d oppføringer) + Bygger ordliste til uttale: %1$d%% + Nedlasting av ordliste feilet – trykk Lagre igjen for å prøve på nytt Tøm lagrede tekster - Slett tekstene som er lagret på denne enheten + Slett %1$s MB av tekster lagret på denne enheten Fjernet %1$s MB Hent token Apple Music Media-brukertoken @@ -525,14 +560,22 @@ Nødvendige informasjonskapsler: SID, HSID, SSID, SAPISID, __Secure-1PAPISID, __ Vis kopier-knapp Trykk for å kopiere originale tekster som ren tekst; lang-trykk for å lagre råtekst-kildefilen til Nedlastinger Vis oversettelsesknapp - Trykk for å bytte oversettelse; lang-trykk for bare oversettelse. Bruker leverandørens oversettelser, Google Translate, eller en OpenAI-kompatibel API - Vis romaniseringsknappen - Trykk for å bytte romanisering; lang-trykk for bare romanisering. Bruker leverandørens romanisering, Google Translate, eller en OpenAI-kompatibel API - Lokal romanisering og synkronisering - Romansiser på enheten når leverandøren, modellen din og Google Translate feiler. Lesningen følger hvert ord i en tidsstemt tekst; ordsynkronisering må være på. Kinesisk, kantonesisk, japansk, koreansk, gresk, russisk, georgisk, armensk; omtrent 120 MB - Lesning %1$s: %2$d%% + Trykk for å veksle mellom oversettelse; lang trykk for kun oversettelse. Bruker de aktiverte oversettelsesleverandørene i den angitte rekkefølgen + Vis knappen for romanisering eller translitterasjon + Trykk for å veksle mellom romanisering eller translitterasjon; lang trykk for kun romanisering eller translitterasjon. Bruker de aktiverte romaniseringsleverandørene i den angitte rekkefølgen + Aktiver translitterasjon + Tillat ikkje-latinske uttaler fra tekstkilde; når deaktivert, behold kun latinske bokstaver, aksentuerte bokstaver og toneciffer + Lese hvert ord i en tidsstemt tekst (ordsynkronisering på); koreanske språk trenger ingen nedlasting; fjorten skriftsystemer deler én CLDR-nedlasting; kinesiske tegn leses av Rime-ordlisten når ordtabellen dekker språket + Laster inn uttaloradbøker… Lesninger klare (%1$d oppføringer) Nedlastingen mislyktes - skru dette av og på igjen for å prøve på nytt + Frakoblet ordbok + Japansk (SudachiDict, ~75 MB) + Kinesiske og kantonesiske tegn (UniDict, ~8 MB) + Kinesiske ord (pinyin-pro, ~17 MB) + Gresk, slavisk kyrillisk (inkl. russisk, ukrainsk, bulgarsk, serbisk, belarusisk, makedonsk), georgisk, armensk, hebraisk, arabisk, amharisk og syrisk translitterasjon (CLDR, ~0,5 MB) + Lesning %1$s: %2$d%% + Velg minst én ordbok Vis oppdater-knappen Trykk for å bytte til neste kandidat til tekst; lang-trykk for å søke etter tekst basert på sangtittel og artist Søk etter sangtekst @@ -543,8 +586,10 @@ Nødvendige informasjonskapsler: SID, HSID, SSID, SAPISID, __Secure-1PAPISID, __ Lagret Marker synkroniserte ord eller stavelser Prioriter ord-for-ord-tekster når de er tilgjengelige. Når teksten har tidsstempling for ord, vises hvert ord eller hver stavelse synkronisert med musikken - Vis bakgrunnsvokaler separat - Flytter en bakgrunnsvokal skrevet i klammer, slik som \"(Å) jeg vil ha deg\", ut av den sunge linjen og over i en egen, svakere linje + Skjul inaktive linjer + Tegner hver linje som ikke syns nå, litt mindre, ved å dempe den + Vis bakgrunnsvokal separat + Separerer bakgrunnsvokal og harmoni fra hovedlinjen. Flytter en bakgrunnsvokal skrevet i parentes, som \"(Åh) jeg vil ha deg\", ut av sanglinjen til en egen mørkere linje Skjul […] linje Skjul sangtekstlinjene over linjen som spilles for øyeblikket Skjul ikke-avspilte tekster @@ -602,10 +647,15 @@ Gjelder for begge grenser. Ved 1,75 teller fire tegn med full bredde som 7."Oversatt av %s Romanisert av %s Romanisert av %s + Romanisert av Rime (%s) Oversatt Oversettelsesspråk - Romaniseringsspråk + Språk for romanisering eller translitterasjon + "Målspråk for oversettelse: +%s" + "Målspråk for romanisering eller transkripteringsformål: +%s" Oversetter… Kunne ikke oversette sangteksten Romanisert @@ -619,7 +669,7 @@ Gjelder for begge grenser. Ved 1,75 teller fire tegn med full bredde som 7."Oppdaterer Oppdatert QuickLRC-API-nøkkel - QuickLRC transkriberer eller tidsjusterer sangtekstene for sporet som spilles for øyeblikket. En egen tjeneste, uten sammenheng med det nevnte OpenAI-kompatible endepunktet ovenfor + QuickLRC transkriberer eller tidsjusterer tekstene for nåvrende spor QuickLRC-nøkkel lagret. Bruk justeringsknappen i dialogen for sangerettsøk, eller panelmeldingen når et spor ingen sangtekster QuickLRC-API-nøkkel lagret QuickLRC godtok ikke den nøkkelen @@ -635,58 +685,50 @@ Gjelder for begge grenser. Ved 1,75 teller fire tegn med full bredde som 7."Ingen sangtekster å justere - transkriberer i stedet QuickLRC kunne ikke behandle dette sporet QuickLRC-kredittene er oppbrukt for dette sporet - "Tekster blir matchet basert på tittel og artist, uvanlig metadata kan gi ingen resultater eller feil versjon. Bruk oppdater for å bytte versjon. Leverandører markert * krever en token. Alle aktiverte leverandører spres ikke alltid i den rekkefølgen du har angitt, men svarer basert på deres hastighet. + "Sanger matches etter tittel og artist; unvanlig metadata kan gi treff på ingenting eller feil versjon. Bruk oppdatering for å bytte versjon. Tilbydere markert * krever et token. Ikke alle aktiverte tilbydere alltid blir forespurt i rekkefølgen din, men svarer basert på hastighet. -Tekstene kommer fra tredjepartstjenester som ikke er tilknyttet, sponset eller godkjent av Morphe. Hver brukes slik den er: tekstene, deres tilgjengelighet og lisensiering beholder eierskapet hos disse tjenestene og deres lisensgivere, og bruken kan være underlagt deres vilkår for bruk. Det som sendes til en aktivert tjeneste er bare søket du ba om - tittelen, artisten og, der tjenesten trenger det, video-ID-en. Den valgfrie AI-endepunktet og QuickLRC-transkripsjonen kjører kun etter at du har konfigurert dem, og mottar teksten til sangen eller video-URL-en." +Sanger kommer fra tredjepartstjenester som ikke er tilknyttet, sponsorerer eller godkjenner Morphe. Hver brukes slik den er: sangtekstene, deres tilgjengelighet og lisensiering forblir hos disse tjenestene og sine licenstagere, og bruk kan være underlagt deres bruksvilkår. Det som når en aktivert tjeneste er bare oppslaget du ba om - tittelen, artisten, og der det tjener behøves, video-ID. Den valgfrie AI-endepunktet og QuickLRC-transkripsjonen kjører kun etter at du har konfigurert dem, og mottar tekst eller spor video URL." Aktiver kun leverandørene du bruker, eller behold standardverdien – kontinuerlige forespørsler kan overveldet små samfunnsbaserte lyrics-nettsider Skjul krediteringslinjen Skjul krediteringslinjen som vises under teksten Bytt om oversettelse og romanisering - PÅ setter romanisering under teksten og oversettelsen; AV holder romanisering over teksten og oversettelsen - Oversettelse - Romanisering - Oversettelsesleverandør - Google (Standard, gratis) - OpenAI-kompatibel (Egendefinert) - Leverandør for romanisering - Google (Standard) - Lokal / frakoblet ordbok - OpenAI-kompatibel (Egendefinert) - AI-seksjon - Brukes kun når leverandøren ikke sender med seksjonsmerker som Refreng eller Vers, som din OpenAI-kompatible modell deretter detekterer - AI & sky-endepunkter - Konfigurer API-nøkler, modeller, endepunkter og prompt-maler for AI-leverandører. - URL for API-endepunkt (f.eks. https://api.openai.com/v1/chat/completions) + PÅ setter romanisering eller translitterasjon under tekster og oversettelse over; FRA holder romanisering eller translitterasjon over og oversettelse under. En uttale plassert over hver stavelse flytter med den, fra over stavelser til under den + Alltid sentrér tekster + PÅ setter hver linje midt i skjermen og ignorerer kildejustering, inkludert duett-sider; FRA beholder justeringen tekstene ber om + Minst én leverandør må være aktiv + Oversettelse + Romanisering eller translitterasjon + Frakoblet ordbok + Rime-ordliste + Oppdaga seksjoner med AI + Brukes kun når leverandøren ikke sender seksjonsmerkelapper som Refreng eller Vers, noe som den OpenAI-kompatible modellen deretter oppdager. Redigering av raden åpner også prompten + API-endepunkt-URL (f.eks. https://api.openai.com/v1/chat/completions, https://api.anthropic.com/v1/chat/completions, https://generativelanguage.googleapis.com/v1beta/openai/chat/completions) Malskjema for ledetekst Plassholdere: {language}, {task}, {title}, {artist}, {count}, {lines} Ledetekst lagret Oppfordring lagret, men AI-endepunktet er ikke brukbart ennå. Konfigurer det først under AI-innstillinger Ledetekst tilbakestilt - Ikke gyldig JSON. Rett det eller fjern feltet før lagring Innstillinger for oversettelse & romanisering lagret Innstillinger tilbakestilt til standard Base-URL må starte med http:// eller https:// - Test tilkobling - Konfigurer endepunkter - Vis alle endepunkter + Innstillingene er ufullstendige eller avvist av leverandøren Validerer tilkobling… - Oversettelsesleverandør - Romaniseringsleverandør - Hent modeller + %1$s: Tilkobla! (%2$s) + %1$s: Tilkobling feila. Sjekk innstillingene. + Skriv inn API-nøkkelen først Henter modeller… Velg en modell Kunne ikke hente modeller. Sjekk API-nøkkel eller tilkobling. - Gemini API-innstillinger - OpenAI-kompatibel (egendefinert endepunkt) - DeepL API-innstillinger - API-nøkkel + Oppfordring Modell Base-URL + Velg en tjeneste API-nøkkel (valgfritt) - Oversettelsesinstruks Ekstra forespørselstekst (valgfri JSON) + Ekstra forespørselsheader (valgfritt JSON) + Sitat navn og verdier, f.eks. {\"api-key\": \"abc\"}. En usitatert verdi som inneholder / : eller tall blir avvist. En tom verdi fjerner en header. + Sitatnavn og verdier, f.eks. {\"temperature\": 0.3}. En ukotiert verdi som inneholder / eller : forkastes. Modell og meldinger kan ikke angis her. Autentiseringsnøkkel (Gratis eller Pro) - Konfigurer API-nøkler, modeller, endepunkter og ledetekster 8.23.51 - Deaktiver fet ikoner 9.13.51 – Gjenopprett Spiller Neste, Tekster og Relaterte faner diff --git a/patches/src/main/resources/addresources/values-nb-rNO/youtube/strings.xml b/patches/src/main/resources/addresources/values-nb-rNO/youtube/strings.xml index 8913f22af6..5b3cccd408 100644 --- a/patches/src/main/resources/addresources/values-nb-rNO/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-nb-rNO/youtube/strings.xml @@ -29,6 +29,8 @@ Second \"item\" text" Diverse Video Gjenopprett gamle YouTube-innstillingsskjerm + Vis meldinger i YouTube-snackbar + Morphe-meldinger, for eksempel bekreftelse av en spillerknapp, vises i YouTube-snackbar i stedet for et popup-vindu Fjern restriksjoner for avspilling i bakgrunnen Deaktiver avspilling i bakgrunnen diff --git a/patches/src/main/resources/addresources/values-nl-rNL/music/strings.xml b/patches/src/main/resources/addresources/values-nl-rNL/music/strings.xml index ee833fbc5c..08d7216952 100644 --- a/patches/src/main/resources/addresources/values-nl-rNL/music/strings.xml +++ b/patches/src/main/resources/addresources/values-nl-rNL/music/strings.xml @@ -462,12 +462,47 @@ Om ervoor te zorgen dat scrobbling tijdens het afspelen op de achtergrond wordt Weergeven Synchronisatie Paneel - AI-eindpunt Alle providers uitgeschakeld - ingebouwde songteksten en ingesloten songteksten Providerorder resetten? Providers terugzetten naar de standaardprioriteitsvolgorde + Rime-woordlijst + "De Rime-wordlijst heeft ook UniDict nodig: vink in de Offline Dictionary-provider \"Chinese en Kantonese karakters (UniDict)\" aan en laat het eerst klaar downloaden. Zonder dit levert een lijst die karakters uitleest geen uitspraak. + +Vink de lijsten aan die de Rime Wordlist moet uitlezen, en tik dan op Opslaan om ze te downloaden. Een lijst die je hebt toegevoegd, wordt verwijderd als je hem uitvinkt. + +Elke lijst is jouw eigen tabel van woord naar uitspraak, een .dict.yaml met regels zoals: +字 ham +典 din2 +De eerste kolom is het woord, de tweede de uitspraak; extra kolommen en regels zonder tabblad worden genegeerd." + No Rime word list is ticked for the language above. Tap this name to tick one, or pick a language that has a list + Toevoegen + Rime-woordlijst toevoegen + Een .dict.yaml-bestand, of een zip die er één bevat, met regels zoals 字 ham en 典 din2. De lijst wordt opgehaald zodra je hem aanvinkt en op Opslaan tikt, en de licentie kun je zelf controleren + Taalcode + Twee of drie letters, bijvoorbeeld hak of wuu + Naam + Wordt weergegeven onder de uitspraak, bijvoorbeeld Hakka (Meixian) + Adres van de woordlijst + https://-adres van het .dict.yaml-bestand of de bijbehorende zip + Karakters die niet in een woord voorkomen + Automatisch + Cantonese karakters + Mandarijnkarakters + Geen karkteruitspraak + Vul de code, naam en adres in + De code moet uit twee of drie letters bestaan, bijvoorbeeld hak + Het adres moet beginnen met https:// + De naam en het adres mogen geen verticale balk bevatten + %1$s heeft al een woordenlijst + %1$s wordt gelezen door de ingebouwde romaniseerder en kan geen woordlijst gebruiken + Not used: this language already has its own reading, so untick it to remove it + %1$s is toegevoegd - vink het aan en tik op Opslaan om het te downloaden + Woordlijsten gereed + Woordlijsten gereed (%1$d items) + Uitziende woordlijst: %1$d%% + Downloaden van woordlijst mislukt - tik opnieuw op Opslaan om het opnieuw te proberen Opsgeslagen songteksten wissen - Verwijder de songteksten die op dit apparaat zijn opgeslagen + Verwijder de %1$s MB aan opgeslagen lyrics op dit apparaat %1$s MB gewist Token ophalen Apple Music Media-gebruikerstoken @@ -517,14 +552,22 @@ Vereiste cookies: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPISID Toon Kopieer knop Tik om originele songteksten als platte tekst te kopiëren; lang drukken om het ruwe songtekstbestand op te slaan in Downloads Toon Vertaal knop - Tik om vertaling aan/uit te schakelen; lang drukken voor alleen vertaling. Gebruikt providervertalingen, Google Translate of een OpenAI-compatible API - Toon romaniseerknop - Tik om transliteratie aan/uit te schakelen; lang drukken voor alleen transliteratie. Gebruikt providertransliteratie, Google Translate of een OpenAI-compatible API - Lokale transliteratie en uitlijning - Translitereren op het apparaat wanneer de provider, uw model en Google Translate falen. De lezing volgt elk woord van een tijdgebonden songtekst; woord-synchronisatie moet ingeschakeld zijn. Chinees, Kantonese, Japans, Koreaans, Grieks, Russisch, Georgisch, Armeens; ongeveer 120 MB - Uitspraak %1$s: %2$d%% + Tik om vertaling in-/uit te schakelen; lang indrukken alleen voor vertaling. Gebruikt de ingeschakelde vertaaldiensten in de volgorde zoals vermeld + Romaniseringsknop translitereren weergeven + Tik om romanisatie of transliteratie in-/uit te schakelen; lang indrukken alleen voor romanisatie of transliteratie. Gebruikt de ingeschakelde romaniseerders in de volgorde zoals vermeld + Transliteratie inschakelen + Toestaan dat uitspraken uit de lyricsonderdeel niet-latinisch zijn; indien uitgeschakeld worden alleen latinische letters, getekende letters en toongedigitalen behouden + Lees elk woord uit een getimed lyric (woorden-synchronisatie ingeschakeld); Koreaans vereist geen download; veertien scripts delen één CLDR-download; Han-karakters worden uitgelezen door de Rime-woordlijst wanneer de woordenlijst de taal ondersteunt + Uitspraakwoordenboeken downloaden… Lezingen gereed (%1$d items) Downloaden mislukt - zet dit uit en weer aan om opnieuw te proberen + Offline woordenboek + Japans (SudachiDict, ~75 MB) + Chinese en Cantonese karakters (UniDict, ~8 MB) + Chinese woorden (pinyin-pro, ~17 MB) + Grieks, Slavisch Cyrillisch (inclusief Russisch, Oekraïens, Bulgaars, Servisch, Wit-Russisch, Macedonisch), Georgisch, Armeens, Hebreeuws, Arabisch, Amharsiërs en Syrisch transliteratie (CLDR, ~0,5 MB) + Uitspraak %1$s: %2$d%% + Kies ten minste één woordenboek Vernieuwknop weergeven Tik om over te schakelen naar de volgende songtekstkandidaat; lang drukken om songteksten te zoeken op titel en artiest Zoek songteksten @@ -535,8 +578,10 @@ Vereiste cookies: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPISID Opgeslagen Gesynchroniseerde woorden of lettergrepen markeren Geef prioriteit aan woord-voor-woord songteksten wanneer beschikbaar. Wanneer songteksten woordtiming bevatten, wordt elk woord of elke lettergreep gemarkeerd in de maat met de muziek - Achtergrondvocalen apart weergeven - Verplaatst een achtergrondzang die tussen haakjes staat, zoals \"(Oh) I want you\", uit de gezongen regel naar een aparte zwakkere regel + Inactieve regels comprimeren + Maakt elke regel die momenteel niet wordt gezongen iets kleiner, bovenop het dimmen + Achtergrondzang apart tonen + Scheidt achtergrondzang en harmonie van de hoofdzang. Verplaatst een achtergrondzang die in haakjes staat, zoals \'(Oh) I want you\', uit de gezongen regel naar een aparte, gedimde regel Verberg […] regel Verberg songtekstregels boven de regel die momenteel wordt afgespeeld Verberg niet-afgespeelde songteksten @@ -594,10 +639,15 @@ Van toepassing op beide limieten. Bij 1,75 tellen vier tekens van volledige bree Vertaald door %s Geromaniseerd door %s Getranslitereerd door %s + Romaniseerd door Rime (%s) Vertaald Vertalingstaal - Taalfrequentie voor transliteratie + Taalkode romanisering of transliteratie + "Doeltaal voor vertaling: +%s" + "Doeltaal voor romanisering of transliteratie: +%s" Bezig met vertalen… Kon de songtekst niet vertalen Geromaniseerd @@ -611,7 +661,7 @@ Van toepassing op beide limieten. Bij 1,75 tellen vier tekens van volledige bree Vernieuwen Vernieuwd QuickLRC API-sleutel - QuickLRC transcribeert of time-aligns de teksten voor het huidige nummer. Een aparte service, niet gerelateerd aan de OpenAI-compatible endpoint hierboven + QuickLRC transcribeert of time-alignt de lyrics voor het huidige nummer QuickLRC-sleutel opgeslagen. Gebruik de uitlijningsknop in het zoekdialoogvenster voor teksten, of de paneelaanwijzing wanneer een nummer geen teksten heeft QuickLRC API-sleutel opgeslagen QuickLRC accepteerde die sleutel niet @@ -627,58 +677,50 @@ Van toepassing op beide limieten. Bij 1,75 tellen vier tekens van volledige bree Geen teksten om uit te lijnen - in plaats daarvan transcriberen QuickLRC kon dit nummer niet verwerken QuickLRC-credits zijn op voor dit nummer - "Songteksten worden gekoppeld op basis van titel en artiest; ongebruikelijke metadata kan leiden tot niets of de verkeerde versie. Gebruik vernieuwen om van versie te wisselen. Providers met * vereisen een token. Niet alle ingeschakelde providers worden altijd om beurt aangevraagd, maar reageren op basis van hun snelheid. + "Songteksten worden afgewerkt op titel en artiest; ongewone metadata kan resulteren in geen resultaat of de verkeerde versie. Gebruik Vernieuwen om van versie te wisselen. Providers gemarkeerd met * vereisen een token. Niet alle ingeschakelde providers worden altijd in volgorde gevraagd, maar reageren op basis van hun snelheid. -Songteksten komen van derde partijen die geen verbinding hebben gesponsord of goedgekeurd zijn door Morphe. Elk wordt gebruikt zoals het is: de songteksten, hun beschikbaarheid en licentie blijven bij die diensten en hun licentieverstrekkers, en het gebruik ervan kan onderworpen zijn aan hun servicevoorwaarden. Wat een ingeschakelde dienst bereikt, is alleen het opzoeken dat u heeft gevraagd - de titel, de artiest, en waar die dienst dat nodig heeft, de video-ID. Het optionele AI-eindpunt en QuickLRC-transcriptie werken alleen nadat u ze hebt geconfigureerd, en ontvangen de songtekst of de video-URL van het nummer." +Songteksten komen van derde-dienstverleners die niet gelieerd zijn aan, gesponsord worden door of gesteund worden door Morphe. Elke dienst wordt gebruikt zoals hij is: de songteksten, hun beschikbaarheid en hun licenties blijven bij deze diensten en hun licentiegevers, en het gebruik ervan kan onderworpen zijn aan hun servicevoorwaarden. Wat naar een ingeschakelde dienst gaat, is alleen de opvraag die je hebt gedaan - de titel, de artiest en, waar die dienst dat nodig heeft, de video-ID. Het optionele AI-eindpunt en QuickLRC-transcriptie worden alleen uitgevoerd nadat je ze hebt geconfigureerd, en ontvangen de songtekst of de video-URL van het nummer." Schakel alleen de providers in die je gebruikt, of behoud de standaardinstelling - constante aanvragen kunnen kleine community-lyricsites overweldigen Verberg creditregel Verberg de creditregel die onder de songteksten wordt weergegeven Wissel vertaling en romanisatie om - AAN zet romanisatie onder de songteksten en vertaling erboven; UIT houdt romanisatie boven en vertaling onder. - Vertaling - Romanisatie - Vertalingsprovider - Google (Standaard, Gratis) - OpenAI Compatibel (Aangepast) - Romanisatieprovider - Google (Standaard) - Lokaal / Offline Woordenboek - OpenAI Compatibel (Aangepast) - AI-sectie - Wordt alleen gebruikt wanneer de provider geen sectielabels zoals Refrein of Couplet levert, die vervolgens door je OpenAI-compatibele model worden gedetecteerd - AI & Cloud Eindpunten - Configureer API-sleutels, modellen, eindpunten en prompt-sjablonen voor AI-providers. - API-eindpunt URL (bijv. https://api.openai.com/v1/chat/completions) + ON plaatst romanisering of transliteratie onder de lyrics en vertaling bovenaan; OFF houdt romanisering of transliteratie boven en vertaling onder. Een uitspraak die boven elke lettergreep staat, bewaagt mee met de lettergreep, van boven naar beneden + Altijd lyrics centreren + ON plaatst elke regel in het midden en negeert de bronalignement, inclusief duet-partijen; OFF behoudt de alignement zoals de lyrics vragen + Minstens één aanbieder moet ingeschakeld blijven + Vertaling + Romanisering of transliteratie + Offline woordenboek + Rime-woordlijst + Secties detecteren met AI + Alleen gebruikt wanneer de aanbieder geen sectielabels zoals Refrein of Strofe aanbiedt, die uw OpenAI-compatibele model dan detecteert. Het bewerken van de rij opent ook de prompt + API-eindpunt-URL (bijv. https://api.openai.com/v1/chat/completions, https://api.anthropic.com/v1/chat/completions, https://generativelanguage.googleapis.com/v1beta/openai/chat/completions) Prompt-sjabloon Placeholders: {language}, {task}, {title}, {artist}, {count}, {lines} Prompt opgeslagen Prompt opgeslagen, maar het AI-endpoint is nog niet bruikbaar. Configureer het eerst onder AI-instellingen Prompt gereset - Geen geldige JSON. Corrigeer het of wis het veld voordat u opslaat Instellingen voor vertaling & romanisatie opgeslagen Instellingen zijn teruggezet naar de standaardinstellingen Basis-URL moet beginnen met http:// of https:// - Verbinding testen - Endpoints configureren - Alle endpoints tonen + Instellingen onvolledig of afgewezen door de aanbieder Verbinding valideren… - Vertalingsprovider - Romanisatieprovider - Modellen ophalen + %1$s: Verbonden! (%2$s) + %1$s: Verbinding mislukt. Controleer de instellingen. + Voer eerst de API-sleutel in Modellen ophalen… Selecteer een model Het ophalen van modellen is mislukt. Controleer de API-sleutel of verbinding. - Gemini API-instellingen - OpenAI-compatibel (aangepast eindpunt) - DeepL API-instellingen - API-sleutel + Prompt Model Basis-URL + Kies een service API-sleutel (optioneel) - Vertaalprompt Extra request body (optionele JSON) + Extra verzoekheaders (optionele JSON) + Citeer namen en waarden, bijv. {\"api-key\": \"abc\"}. Een niet-geciteerde waarde die / : of een cijfer bevat, wordt afgewezen. Een lege waarde verwijdert een header. + Citeer namen en waarden, bijv. {\"temperature\": 0,3}. Een niet-geciteerde waarde die / of : bevat, wordt afgewezen. Model en berichten kunnen hier niet worden ingesteld. Authenticatiesleutel (Gratis of Pro) - Configureer API-sleutels, modellen, endpoints en prompts 8.23.51 - Vetgedrukte pictogrammen uitschakelen 9.13.51 - Herstel Player Up next, Lyrics en Related tabbladen diff --git a/patches/src/main/resources/addresources/values-nl-rNL/youtube/strings.xml b/patches/src/main/resources/addresources/values-nl-rNL/youtube/strings.xml index 96134e86a2..29dc420206 100644 --- a/patches/src/main/resources/addresources/values-nl-rNL/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-nl-rNL/youtube/strings.xml @@ -29,6 +29,8 @@ Second \"item\" text" Diversen Video Herstel het oude YouTube instellingenscherm + Berichten in de YouTube-snackbar weergeven + Morphe-berichten, zoals de bevestiging van een spelerknop, worden in de YouTube-snackbar getoond in plaats van een pop-up Achtergrond afspeelbeperkingen verwijderen Schakel afspelen in achtergrond uit diff --git a/patches/src/main/resources/addresources/values-pl-rPL/music/strings.xml b/patches/src/main/resources/addresources/values-pl-rPL/music/strings.xml index 63cea35643..e214446765 100644 --- a/patches/src/main/resources/addresources/values-pl-rPL/music/strings.xml +++ b/patches/src/main/resources/addresources/values-pl-rPL/music/strings.xml @@ -462,12 +462,47 @@ Aby zapewnić ciągłość synchronizacji podczas odtwarzania w tle, upewnij si Wyświetlanie Synchronizacja Panel - Adres AI Wszyscy dostawcy wyłączeni. Teksty wbudowane i osadzone Zresetować kolejność dostawców? Przywróć domyślną kolejność priorytetów dla dostawców + Lista słów Rime + "Słownik Rime wymaga również UniDict: w usłudze Offline Dictionary zaznacz opcję „Chinese and Cantonese characters (UniDict)” i poczekaj na zakończenie pobierania. Bez tej listy lista zawierająca znaki nie zwraca czytań. + +Zaznacz listy, które słownik Rime ma odczytywać, a następnie dotknij zapisz, aby je pobrać. Lista, którą dodałeś, zostanie usunięta po odznaczeniu jej. + +Każda lista to twoja własna tabela słów do wyrażeń czytań, plik .dict.yaml z liniami takimi jak: +字 ham +典 din2 +Pierwsza kolumna to słowo, druga to jego wyrażenie; dodatkowe kolumny oraz linie bez znaku tabulacji są ignorowane." + Brak zaznaczonej listy słów Rime dla powyższego języka. Dotknij tej nazwy, aby zaznaczyć jedną, lub wybierz język, który posiada listę + Dodaj + Dodaj listę słów Rime + Plik .dict.yaml lub archiwum zip zawierający taki plik, z liniami takimi jak 字 ham i 典 din2. Lista jest pobierana po jej zaznaczeniu i kliknięciu zapisz, a licencja jest dostępna do sprawdzenia + Kod języka + Dwie lub trzy litery, np. hak lub wuu + Nazwa + Pokazywane pod wymową, np. Hakka (Meixian) + Adres listy słów + Adres https:// pliku .dict.yaml lub jego archiwum zip + Znaki niebędące częścią słowa + Automatyczny + Znaki kantońskie + Znaki mandaryńskie + Brak wymowy znaku + Wypełnij kod, nazwę i adres + Kod musi mieć dwie lub trzy litery, np. hak + Adres musi zaczynać się od https:// + Nazwa i adres nie mogą zawierać pionowej kreski + %1$s już posiada tabelę słów + %1$s jest wymawiany przez wbudowany system romanizacji i nie może używać listy słów + Nie używany: ten język już ma własną wymowę, więc odznacz go, aby go usunąć + Dodano %1$s - zaznacz go i dotknij Zapisz, aby go pobrać + Listy słów gotowe + Listy słów gotowe (%1$d wpisów) + Budowanie listy słów: %1$d%% + Pobieranie listy słów nie powiodło się - dotknij ponownie Zapisz, aby spróbować ponownie Wyczyść zapisane teksty - Usuwa teksty piosenek przechowywane na tym urządzeniu + Usuń teksty utworów przechowywane na tym urządzeniu o rozmiarze %1$s MB Wyczyszczono %1$s MB Pobierz token Apple Music Media-user-token @@ -527,14 +562,22 @@ Wymagane ciasteczka: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPI Pokaż przycisk Kopiuj Dotknij, aby skopiować oryginalny tekst jako zwykły tekst. Przytrzymaj, aby zapisać surowy plik źródłowy tekstu w folderze Pobrane Pokaż przycisk Przetłumacz - Dotknij, aby przełączyć tłumaczenie. Przytrzymaj, aby wyświetlić samo tłumaczenie. Korzysta z tłumaczeń dostawcy, Tłumacza Google lub API zgodnego z OpenAI - Pokaż przycisk Romanizuj - Dotknij, aby przełączyć romanizację. Przytrzymaj, aby wyświetlić samą romanizację. Korzysta z romanizacji dostawcy, Tłumacza Google lub API zgodnego z OpenAI - Lokalna romanizacja i dopasowanie - Romanizuje tekst na urządzeniu, gdy zawiedzie dostawca, twój model oraz Tłumacz Google. Zapis wymowy podąża za każdym słowem zsynchronizowanego tekstu, synchronizacja słów musi być włączona. Języki: chiński, kantoński, japoński, koreański, grecki, rosyjski, gruziński, ormiański; ok. 120 MB - Wczytywanie %1$s: %2$d%% + Dotknij, aby przełączyć tłumaczenie; przytrzymaj, aby wyświetlić samo tłumaczenie. Używa włączonych dostawców tłumaczeń w kolejności ich wyświetlania + Pokaż przycisk Romanizacji lub Transliteracji + Dotknij, aby przełączyć romanizację lub transliterację; przytrzymaj, aby wyświetlić samo romanizację lub transliterację. Używa włączonych dostawców romanizacji w kolejności ich wyświetlania + Włącz transliterację + Zezwalaj na wymowy inne niż łacińskie z źródła tekstu; gdy wyłączone, zachowaj tylko litery łacińskie, litery z diakrytykami i cyfry tonu + Czyta każde słowo tekstu timed (synchronizacja słów włączona); koreański nie wymaga pobierania; czternaście skryptów dzieli jedno pobranie CLDR; znaki han są czytane przez Listę słów Rime, gdy jej tabela słów obejmuje język + Pobieranie słowników wymowy… Dane wymowy gotowe (%1$d wpisów) Pobieranie się nie powiodło. Wyłącz i włącz tę opcję ponownie, aby ponowić próbę + Słownik offline + Japoński (SudachiDict, ~75 MB) + Znaki chińskie i kantońskie (UniDict, ~8 MB) + Chińskie słowa (pinyin-pro, ~17 MB) + Transliteracja greckiego, słowiańskiego cyrylicy (włącznie z rosyjskim, ukraińskim, bułgarskim, serbskim, białoruskim, macedońskim), gruzińskiego, ormiańskiego, hebrajskiego, arabskiego, amharskiego i syriackiego (CLDR, ~0,5 MB) + Wczytywanie %1$s: %2$d%% + Wybierz co najmniej jeden słownik Pokaż przycisk Odśwież Dotknij, aby przełączyć na następną propozycję tekstu. Przytrzymaj, aby wyszukać tekst według tytułu utworu i wykonawcy Wyszukaj tekst @@ -545,8 +588,10 @@ Wymagane ciasteczka: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPI Zapisano Podświetlaj zsynchronizowane słowa lub sylaby Nadaje priorytet tekstom synchronizowanym słowo po słowie, o ile są dostępne. Gdy tekst zawiera znaczniki czasu, podświetla każde słowo lub sylabę w rytm muzyki + Zmniejsz nieaktywne linie + Rysuje każdą linię, która nie jest obecnie śpiewana, nieco mniejszą, oprócz przyciemniania Pokaż wokal w tle osobno - Przenosi wokal w tle zapisany w nawiasach, np. \"(Och) Chcę cię\", z głównego wersu do osobnej, przyciemnionej linii + Oddziela wokale tła i harmonię od linii głównej. Przenosi wokal tła zapisany w nawiasach, np. \"(Och) chcę cię\", z głównego wersu do osobnej, przyciemnionej linii Ukryj odtworzony tekst Ukrywa linijki tekstu znajdujące się powyżej tej aktualnie odtwarzanej Ukryj nieodtworzony tekst @@ -604,10 +649,15 @@ Dotyczy obu limitów. Przy wartości 1.75 cztery znaki pełnej szerokości licz Przetłumaczone przez %s Zromanizowane przez %s Zromanizowane przez %s + Zromanizowane przez Rime (%s) Przetłumaczone Język tłumaczenia - Język romanizacji + Język romanizacji lub transliteracji + "Język docelowy tłumaczenia: +%s" + "Język docelowy romanizacji lub transkrypcji: +%s" Tłumaczenie… Nie udało się przetłumaczyć tekstu Zromanizowane @@ -621,7 +671,7 @@ Dotyczy obu limitów. Przy wartości 1.75 cztery znaki pełnej szerokości licz Odświeżanie Odświeżono Klucz API QuickLRC - QuickLRC dokonuje transkrypcji lub synchronizuje czasowo tekst bieżącego utworu. Jest to osobna usługa, niezwiązana z powyższym adresem API zgodnym z OpenAI + QuickLRC transkrybuje lub synchronizuje czasowo tekst dla aktualnego utworu Zapisano klucz QuickLRC. Użyj przycisku synchronizacji w oknie wyszukiwania tekstu lub komunikatu w panelu, gdy utwór nie ma tekstu Zapisano klucz API QuickLRC QuickLRC odrzucił ten klucz @@ -637,56 +687,50 @@ Dotyczy obu limitów. Przy wartości 1.75 cztery znaki pełnej szerokości licz Brak tekstu do synchronizacji. Rozpoczęto transkrypcję QuickLRC nie mógł przetworzyć tego utworu Wyczerpano kredyty QuickLRC dla tego utworu - "Teksty są dopasowywane według tytułu i wykonawcy, nietypowe metadane mogą skutkować brakiem wyników lub pobraniem niewłaściwej wersji. Użyj przycisku odświeżania, aby przełączać wersje. Dostawcy oznaczeni gwiazdką (*) wymagają tokenu. Włączeni dostawcy nie zawsze są odpytywani w ustalonej przez Ciebie kolejności, lecz odpowiadają zależnie od swojej szybkości.\n\nTeksty pochodzą z zewnętrznych usług, które nie są powiązane z Morphe, przez niego sponsorowane ani wspierane. Każda z nich jest używana w obecnym kształcie: same teksty, ich dostępność oraz licencje pozostają w gestii tych usług i ich licencjodawców, a korzystanie z nich może podlegać ich warunkom korzystania z usługi. Do włączonej usługi trafia wyłącznie zlecone przez Ciebie zapytanie: tytuł, wykonawca oraz, jeśli dana usługa tego wymaga, identyfikator filmu. Opcjonalny adres AI oraz transkrypcja QuickLRC działają tylko po ich skonfigurowaniu i otrzymują tekst piosenki lub adres filmu z utworem" + "Teksty są dopasowywane według tytułu i wykonawcy, nietypowe metadane mogą skutkować brakiem wyników lub pobraniem niewłaściwej wersji. Użyj przycisku odświeżania, aby przełączać wersje. Dostawcy oznaczeni gwiazdką (*) wymagają tokenu. Włączeni dostawcy nie zawsze są odpytywani w ustalonej przez Ciebie kolejności, lecz odpowiadają zależnie od swojej szybkości. + +Teksty pochodzą z zewnętrznych usług, które nie są powiązane z Morphe, przez niego sponsorowane ani wspierane. Każda z nich jest używana w obecnym kształcie: same teksty, ich dostępność oraz licencje pozostają w gestii tych usług i ich licencjodawców, a korzystanie z nich może podlegać ich warunkom korzystania z usługi. Do włączonej usługi trafia wyłącznie zlecone przez Ciebie zapytanie: tytuł, wykonawca oraz, jeśli dana usługa tego wymaga, identyfikator filmu. Opcjonalny adres AI oraz transkrypcja QuickLRC działają tylko po ich skonfigurowaniu i otrzymują tekst piosenki lub adres filmu z utworem." Włącz tylko tych dostawców, z których korzystasz, lub pozostaw ustawienie domyślne. Ciągłe żądania mogą przeciążyć małe, społecznościowe strony z tekstami Ukryj informacje o twórcach Ukrywa informacje o twórcach wyświetlane pod tekstem Zamień miejscami tłumaczenie i romanizację - Włączone: romanizacja znajduje się pod tekstem, a tłumaczenie nad nim. Wyłączone: romanizacja nad tekstem, a tłumaczenie pod nim - Tłumaczenie - Romanizacja - Dostawca tłumaczeń - Google (domyślny, bezpłatny) - Zgodny z OpenAI (własny) - Dostawca romanizacji - Google (domyślny) - Lokalny / słownik offline - Zgodny z OpenAI (własny) - Sekcje AI - Używane tylko wtedy, gdy dostawca nie udostępnia etykiet sekcji, takich jak Zwrotka lub Refren. W takim przypadku wykryje je Twój model zgodny z OpenAI - Adresy AI i chmury - Skonfiguruj klucze API, modele, adresy oraz szablony poleceń dla dostawców AI. - Adres API (np. https://api.openai.com/v1/chat/completions) + ON umieszcza romanizację lub transliterację poniżej tekstu, a tłumaczenie nad nim; OFF utrzymuje romanizację lub transliterację powyżej i tłumaczenie poniżej. Wymowa umieszczona nad każdą sylabą przesuwającą się z nią, od góry jej sylaby do dołu + Zawsze centruj tekst + ON umieszcza każdą linię na środku i ignoruje alineację źródła, włącznie z partiami duetowymi; OFF utrzymuje alineację żądaną przez tekst + Co najmniej jeden dostawca musi być wyłączony + Tłumaczenie + Romanizacja lub transliteracja + Słownik offline + Lista słów Rime + Wykrywaj sekcje za pomocą AI + Używane tylko wtedy, gdy dostawca nie udostępnia etykiet sekcji, takich jak Refren lub zwrotka, które wykrywa Twój model zgodny z OpenAI. Edycja wiersza otwiera również jego podpowiedź + URL punktu końcowego API (np. https://api.openai.com/v1/chat/completions, https://api.anthropic.com/v1/chat/completions, https://generativelanguage.googleapis.com/v1beta/openai/chat/completions) Szablon polecenia Zmienne: {language}, {task}, {title}, {artist}, {count}, {lines} Zapisano polecenie Zapisano polecenie, ale adres AI nie jest jeszcze gotowy do użycia. Skonfiguruj go najpierw w Ustawieniach AI Zresetowano polecenie - Nieprawidłowy format JSON. Popraw go lub wyczyść pole przed zapisaniem Zapisano ustawienia tłumaczenia i romanizacji Przywrócono ustawienia domyślne Bazowy adres musi zaczynać się od http:// lub https:// - Testuj połączenie - Konfiguruj adresy - Pokaż wszystkie adresy + Ustawienia niekompletne lub odrzucone przez dostawcę Sprawdzanie połączenia… - Dostawca tłumaczeń - Dostawca romanizacji - Pobierz modele + %1$s: Połączono! (%2$s) + %1$s: Połączenie nie powiodło się. Sprawdź ustawienia. + Wprowadź klucz API jako pierwszy Pobieranie modeli… Wybierz model Nie udało się pobrać modeli. Sprawdź klucz API lub połączenie. - Ustawienia API Gemini - Zgodny z OpenAI (własny adres API) - Ustawienia API DeepL - Klucz API + Podpowiedź Model Bazowy adres URL + Wybierz usługę Klucz API (opcjonalny) - Polecenie tłumaczenia Dodatkowa treść zapytania (opcjonalny JSON) + Nagłówki dodatkowych żądań (opcjonalny JSON) + Cytyuj nazwy i wartości, np. {\"api-key\": \"abc\"}. Niecytowana wartość zawierająca / : lub liczbę jest odrzucana. Pusta wartość usuwa nagłówek. + Cytyuj nazwy i wartości, np. {\"temperature\": 0,3}. Niecytowana wartość zawierająca / lub : jest odrzucana. Model i wiadomości nie mogą być ustawione tutaj. Klucz uwierzytelniający (Free lub Pro) - Skonfiguruj klucze API, modele, adresy API oraz polecenia 8.23.51 - Wyłącza pogrubione ikony 9.13.51 - Przywraca zakładki Następne, Tekst i Powiązane w odtwarzaczu diff --git a/patches/src/main/resources/addresources/values-pl-rPL/youtube/strings.xml b/patches/src/main/resources/addresources/values-pl-rPL/youtube/strings.xml index cd101d434a..3ef5113aad 100644 --- a/patches/src/main/resources/addresources/values-pl-rPL/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-pl-rPL/youtube/strings.xml @@ -29,6 +29,8 @@ Second \"item\" text" Pozostałe Film Przywróć stary ekran ustawień YouTube + Pokaż wiadomości w pasku snackbar YouTube + Wiadomości Morphe, takie jak potwierdzenie przycisku odtwarzania, są wyświetlane w pasku snackbar YouTube zamiast okna wyskakującego Usuń ograniczenia odtwarzania w tle Wyłącz odtwarzanie w tle diff --git a/patches/src/main/resources/addresources/values-pt-rBR/music/strings.xml b/patches/src/main/resources/addresources/values-pt-rBR/music/strings.xml index f23a9ca00e..e84256aaea 100644 --- a/patches/src/main/resources/addresources/values-pt-rBR/music/strings.xml +++ b/patches/src/main/resources/addresources/values-pt-rBR/music/strings.xml @@ -462,12 +462,47 @@ Para garantir que o scrobbling seja sincronizado durante a reprodução em segun Exibir Sincronização Painel - Endpoint de IA Todos os provedores desativados — letras integradas e letras incorporadas Redefinir ordem dos provedores? Restaurar a ordem de prioridade padrão dos provedores. + Lista de Palavras Rime + "A lista de palavras do Rime também precisa do UniDict: no provedor de dicionário offline, marque \"Chinese and Cantonese characters (UniDict)\" e espere o download terminar. Sem ele, uma lista que lê caracteres não fornece leitura. + +Marque as listas que a lista de palavras do Rime deve ler, toque em Salvar para baixá-las. Uma lista adicionada é removida quando você desmarca. + +Cada lista é sua própria tabela de palavra-para-leitura, um .dict.yaml com linhas como: +字 ham +典 din2 +A primeira coluna é a palavra, a segunda sua leitura; colunas extras e linhas sem tabulação são ignoradas." + Nenhuma lista de palavras Rime está marcada para o idioma acima. Toque neste nome para marcar uma, ou escolha um idioma que tenha uma lista + Adicionar + Adicionar uma Lista de Palavras Rime + Um arquivo .dict.yaml, ou um zip contendo um, com linhas como 字 ham e 典 din2. A lista é buscada quando você a marca e toca em Salvar, e a licença é sua para verificar + Código do idioma + Duas ou três letras, como em hak ou wuu + Nome + Mostrado sob a leitura, como em Hakka (Meixian) + Endereço da lista de palavras + Endereço https:// do .dict.yaml ou de seu arquivo zip + Caracteres não em uma palavra + Automático + Caracteres cantoneses + Caracteres mandarim + Sem leitura de caractere + Preencha o código, nome e endereço + O código deve ter duas ou três letras, como em hak + O endereço deve começar com https:// + O nome e o endereço não podem conter uma barra vertical + %1$s já tem uma tabela de palavras + %1$s é lido pelo romanizador embutido e não pode receber uma lista de palavras + Não usado: este idioma já tem sua própria leitura, então desmarque-o para removê-lo + %1$s adicionado - marque-o e toque em Salvar para baixar + Listas de palavras prontas + Listas de palavras prontas (%1$d entradas) + Construindo lista de leitura: %1$d%% + Falha no download da lista de palavras - toque em Salvar novamente para tentar novamente Limpar letras armazenadas - Excluir as letras mantidas neste dispositivo + Excluir %1$s MB de letras mantidos neste dispositivo %1$s MB limpados Obter token Token-de-usuário-de-mídia do Apple Music @@ -517,14 +552,22 @@ Cookies necessários: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAP Mostrar botão Copiar Toque para copiar as letras originais como texto puro; pressione e segure para salvar o arquivo fonte das letras brutais nas Downloads Mostrar botão Traduzir - Toque para alternar tradução; pressione e segure para apenas tradução. Usa traduções do provedor, Google Tradutor ou uma API compatível com OpenAI - Mostrar botão Romanizar. - Toque para alternar romanização; pressione e segure para apenas romanização. Usa romanização do provedor, Google Tradutor ou uma API compatível com OpenAI - Romanização e Alinhamento Local - Romanize no dispositivo quando o provedor, seu modelo e o Google Tradutor falharem. A leitura segue cada palavra de uma letra com tempo; sincronização de palavras deve estar ativada. Chinês, Cantonês, Japonês, Coreano, Grego, Russo, Georgiano, Armênio; cerca de 120 MB - Lendo %1$s: %2$d%% + Toque para alternar tradução; pressione longamente apenas para tradução. Usa os provedores de tradução habilitados na ordem listada + Mostrar botão Romanizar ou Transliterar + Toque para alternar romanização ou transliteração; pressione longamente apenas para romanização ou transliteração. Usa os provedores de romanização habilitados na ordem listada + Habilitar transliteração + Permitir leituras não latinas da fonte das letras; quando desativado, mantenha apenas letras latinas, letras acentuadas e dígitos de tom + Lê cada palavra de uma letra cronometrada (sincronização de palavra ativada); coreano não precisa de download; catorze scripts compartilham um download CLDR; caracteres Han são lidos pela Lista de Palavras Rime quando sua tabela de palavras cobre o idioma + Baixando dicionários de leitura… Leituras prontas (%1$d entradas) Falha ao baixar - desative e ative novamente para tentar novamente + Dicionário Offline + Japonês (SudachiDict, ~75 MB) + Caracteres chineses e cantoneses (UniDict, ~8 MB) + Palavras chinesas (pinyin-pro, ~17 MB) + Transliteração grega, eslava cirílica (incl. russo, ucraniano, búlgaro, sérvio, bielorrusso, macedônio), georgiano, armênio, hebraico, árabe, amárico e siríaco (CLDR, ~0,5 MB) + Lendo %1$s: %2$d%% + Escolha pelo menos um dicionário Mostrar botão para atualizar Toque para alternar para a próxima opção de letra; segure para pesquisar letras pelo título da música e artista Pesquisar letras @@ -535,8 +578,10 @@ Cookies necessários: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAP Salvo Realçar palavras ou sílabas sincronizadas. Priorizar letras palavra por palavra quando disponíveis. Quando as letras têm sincronização por palavra, destaca cada palavra ou sílaba no tempo da música. + Reduzir linhas inativas + Desenha cada linha que não está sendo cantada atualmente ligeiramente menor, sobrepondo-a ao escurecimento Mostrar vocais de fundo separadamente - Move uma vocal de fundo escrito entre parênteses, como \"(Oh) eu quero você\", da linha cantada para uma linha própria com menor volume + Separa vocais de fundo e harmonia da linha principal. Move um vocal de fundo escrito entre colchetes, como \"(Oh) eu quero você\", da linha cantada para uma linha mais escura por conta própria Ocultar […] linha Ocultar linhas da letra acima da linha atualmente em reprodução Ocultar letras não reproduzidas. @@ -594,10 +639,15 @@ Aplica-se a ambos os limites. Em 1,75, quatro caracteres de largura total contam Traduzido por %s Romanizado por %s Romanizado por %s + Romanizado por Rime (%s) Traduzido Idioma da tradução - Idioma de romanização + Idioma de romanização ou transliteração + "Idioma de destino da tradução: +%s" + "Idioma de destino da romanização ou trans literação: +%s" Traduzindo… Não foi possível traduzir a letra Romanizado @@ -611,7 +661,7 @@ Aplica-se a ambos os limites. Em 1,75, quatro caracteres de largura total contam Atualizando Atualizado Chave de API do QuickLRC - QuickLRC transcreve ou alinha temporariamente as letras da faixa atual. Um serviço separado, não relacionado ao ponto de extremidade compatível com OpenAI acima + QuickLRC transcreve ou sincroniza as letras para a faixa atual Chave QuickLRC salva. Use o botão de alinhamento na caixa de diálogo de pesquisa de letras, ou a mensagem do painel quando uma faixa não tiver letras Chave de API do QuickLRC salva O QuickLRC não aceitou essa chave @@ -627,58 +677,50 @@ Aplica-se a ambos os limites. Em 1,75, quatro caracteres de largura total contam Nenhuma letra para alinhar - transcrevendo em vez disso O QuickLRC não pôde processar esta faixa Os créditos do QuickLRC se esgotaram para esta faixa - "As letras são correspondidas por título e artista, metadados incomuns podem não encontrar nada ou a versão errada. Use atualizar para trocar de versões. Provedores marcados * requerem um token. Nem todos os provedores habilitados são sempre solicitados na sua ordem, mas respondem com base na sua velocidade. + "As letras são correspondidas por título e artista; metadados incomuns podem não encontrar nada ou a versão errada. Use atualizar para trocar de versões. Provedores marcados * requerem um token. Nem sempre todos os provedores habilitados são solicitados na sua ordem, mas respondem com base na velocidade. -As letras vêm de serviços de terceiros que não são afiliados, patrocinados ou endossados pela Morphe. Cada um é usado como está: as letras, sua disponibilidade e seu licenciamento permanecem com esses serviços e seus licenciadores, e usá-los pode estar sujeito aos seus termos de serviço. O que chega a um serviço habilitado é apenas a consulta que você pediu - o título, o artista e, onde esse serviço precisa, o ID do vídeo. O endpoint opcional de IA e a transcrição QuickLRC funcionam apenas após você configurá-los, e recebem o texto das letras ou a URL do vídeo da faixa." +As letras vêm de serviços de terceiros que não estão afiliados, patrocinados ou endossados pela Morphe. Cada um é usado conforme está: as letras, sua disponibilidade e sua licenciamento permanecem com esses serviços e seus licenciantes, e seu uso pode estar sujeito aos termos de serviço deles. O que chega a um serviço habilitado é apenas a consulta que você pediu - o título, o artista e, onde esse serviço precisar, o ID do vídeo. O endpoint opcional de IA e a transcrição QuickLRC executam somente após você configurá-los, e recebem o texto das letras ou a URL do vídeo da faixa." Habilite apenas os provedores que você usa, ou mantenha o padrão — solicitações constantes podem sobrecarregar sites de letras de pequenas comunidades Ocultar linha de créditos. Ocultar a linha de crédito mostrada abaixo das letras Trocar tradução e romanização - LIGADO coloca a romanização abaixo das letras e a tradução acima; DESLIGADO mantém a romanização acima e a tradução abaixo - Tradução - Romanização - Provedor de tradução - Google (Padrão, Gratuito) - Compatível com OpenAI (Personalizado) - Provedor de romanização - Google (Padrão) - Dicionário Local / Offline - Compatível com OpenAI (Personalizado) - Seção de IA - Usado apenas quando o provedor não envia rótulos de seção, como Refrão ou Verso, que seu modelo compatível com OpenAI então detecta - IA & Endpoints de Nuvem - Configure chaves de API, modelos, endpoints e modelos de prompt para provedores de IA. - URL do endpoint da API (ex: https://api.openai.com/v1/chat/completions) + ON coloca romanização ou transliteração abaixo das letras e tradução acima; OFF mantém romanização ou transliteração acima e tradução abaixo. Uma leitura colocada sobre cada sílaba move junto com ela, de cima para baixo da sílaba + Centralizar sempre as letras + ON coloca cada linha no meio e ignora o alinhamento da fonte, incluindo lados dueto; OFF mantém o alinhamento que as letras solicitam + Pelo menos um provedor deve permanecer ligado + Tradução + Romanização ou transliteração + Dicionário Offline + Lista de Palavras Rime + Detectar seções com IA + Usado apenas quando o provedor não envia rótulos de seção como Refrão ou Verso, que seu modelo compatível com OpenAI detecta. Editar a linha também abre seu prompt + URL do ponto de extremidade da API (ex. https://api.openai.com/v1/chat/completions, https://api.anthropic.com/v1/chat/completions, https://generativelanguage.googleapis.com/v1beta/openai/chat/completions) Template do Prompt Espaços reservados: {language}, {task}, {title}, {artist}, {count}, {lines} Prompt salvo Prompt salvo, mas o endpoint de IA ainda não está utilizável. Configure-o em Configurações de IA primeiro Prompt redefinido - Não é JSON válido. Corrija-o ou limpe o campo antes de salvar Configurações de Tradução & Romanização salvas Configurações redefinidas para o padrão A URL base deve começar com http:// ou https:// - Testar Conexão - Configurar Endpoints - Mostrar Todos os Endpoints + Configurações incompletas ou rejeitadas pelo provedor Validando conexão… - Provedor de Tradução - Provedor de Romanização - Buscar Modelos + %1$s: Conectado! (%2$s) + %1$s: Falha na conexão. Verifique as configurações. + Digite a chave de API primeiro Buscando modelos… Selecione um Modelo Falha ao buscar modelos. Verifique a chave da API ou a conexão. - Configurações da API Gemini - Compatível com OpenAI (Endpoint Personalizado) - Configurações da API DeepL - Chave da API + Solicitação Modelo URL Base + Escolher um serviço Chave da API (opcional) - Prompt de tradução Corpo de requisição extra (JSON opcional) + Cabeçalhos de solicitação extra (JSON opcional) + Cite nomes e valores, ex. {\"api-key\": \"abc\"}. Um valor sem aspas contendo / : ou número é rejeitado. Um valor vazio remove um cabeçalho. + Cite nomes e valores, ex. {\"temperature\": 0,3}. Um valor sem aspas contendo / ou : é rejeitado. Modelo e mensagens não podem ser configurados aqui. Chave de autenticação (Gratuita ou Pro) - Configure chaves de API, modelos, endpoints e prompts 8.23.51 - Desativar ícones em negrito 9.13.51 - Restaurar abas \"A seguir\", \"Letras\" e \"Relacionados\" do player diff --git a/patches/src/main/resources/addresources/values-pt-rBR/youtube/strings.xml b/patches/src/main/resources/addresources/values-pt-rBR/youtube/strings.xml index 0ec76452d0..4dbfc9645f 100644 --- a/patches/src/main/resources/addresources/values-pt-rBR/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-pt-rBR/youtube/strings.xml @@ -29,6 +29,8 @@ Second \"item\" text" Diversos Vídeo Restaurar menus de configurações antigos + Mostrar mensagens no snackbar do YouTube + As mensagens do Morphe, como a confirmação de um botão do jogador, são exibidas no snackbar do YouTube em vez de uma caixa de diálogo pop-up Remover restrições de reprodução em segundo plano Desativar reprodução em segundo plano diff --git a/patches/src/main/resources/addresources/values-pt-rPT/music/strings.xml b/patches/src/main/resources/addresources/values-pt-rPT/music/strings.xml index 278f944a22..3e6f45901e 100644 --- a/patches/src/main/resources/addresources/values-pt-rPT/music/strings.xml +++ b/patches/src/main/resources/addresources/values-pt-rPT/music/strings.xml @@ -462,12 +462,47 @@ Para garantir que o scrobbling seja sincronizado durante a reprodução em segun Exibir Sincronização Painel - Ponto de extremidade de IA Todos os provedores desativados - letras integradas e letras embutidas Redefinir ordem dos provedores? Restaurar os provedores para a ordem de prioridade padrão + Lista de Palavras Rime + "A lista de palavras Rime também precisa do UniDict: no Provedor de Dicionário Offline, marque \"Caracteres chineses e cantoneses (UniDict)\" e aguarde concluir o download primeiro. Sem ele, uma lista que lê caracteres não fornece leitura. + +Marque as listas que a lista de palavras Rime deve ler, em seguida toque em Salvar para baixá-las. Uma lista que você adicionou é removida quando você desmarca ela. + +Cada lista é sua própria tabela palavra-para-leitura, um .dict.yaml com linhas como: +字 ham +典 din2 +A primeira coluna é a palavra, a segunda sua leitura; colunas extras e linhas sem tabulação são ignoradas." + Nenhuma lista de palavras Rime está marcada para o idioma acima. Toque neste nome para marcar uma, ou escolha um idioma que tenha uma lista + Adicionar + Adicionar uma Lista de Palavras Rime + Um arquivo .dict.yaml, ou um zip contendo um, com linhas como 字 ham e 典 din2. A lista é buscada quando você marca ela e toca em Salvar, e sua licença é sua para verificar + Código do idioma + Duas ou três letras, como em hak ou wuu + Nome + Exibido sob a leitura, como em Hakka (Meixian) + Endereço da lista de palavras + Endereço https:// do .dict.yaml ou seu arquivo zip + Caracteres não em uma palavra + Automático + Caracteres cantoneses + Caracteres mandarim + Sem leitura de caractere + Preencha o código, nome e endereço + O código deve ter duas ou três letras, como em hak + O endereço deve começar com https:// + O nome e o endereço não podem conter uma barra vertical + %1$s já tem uma tabela de palavras + %1$s é lido pelo romanizador integrado e não pode aceitar uma lista de palavras + Não usado: este idioma já possui sua própria leitura, portanto, desmarque-o para removê-lo + Adicionado %1$s - marque-o e toque em Salvar para baixá-lo + Listas de palavras prontas + Listas de palavras prontas (%1$d entradas) + Construindo lista de leitura: %1$d%% + Download da lista de palavras falhou - toque em Salvar novamente para tentar novamente Limpar letras armazenadas - Excluir as letras mantidas neste dispositivo + Excluir os %1$s MB de letras mantidos neste dispositivo %1$s MB limpos Obter token Token de usuário de mídia do Apple Music @@ -527,14 +562,22 @@ Cookies necessários: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAP Mostrar o botão Copiar Toque para copiar a letra original como texto bruto; pressione longamente para salvar o arquivo de origem bruta das letras em Downloads Mostrar o botão Traduzir - Toque para alternar tradução; pressione longamente apenas para tradução. Usa traduções do fornecedor, Google Tradutor ou uma API compatível com OpenAI - Mostrar botão Romanizar - Toque para alternar romanização; pressione longamente apenas para romanização. Usa romanização do fornecedor, Google Tradutor ou uma API compatível com OpenAI - Romanização e Alinhamento Local - Romanizar no dispositivo quando o fornecedor, seu modelo e o Google Tradutor falharem. A leitura segue cada palavra da letra cronometrada; sincronização de palavras deve estar ligada. Chinês, Cantonês, Japonês, Coreano, Grego, Russo, Georgiano, Armênio; cerca de 120 MB - Lendo %1$s: %2$d%% + Toque para alternar tradução; pressione longamente apenas para tradução. Usa os provedores de tradução habilitados na ordem listada + Mostrar botão Romanizar ou Transliterar + Toque para alternar romanização ou transliteração; pressione longamente apenas para romanização ou transliteração. Usa os provedores de romanização habilitados na ordem listada + Habilitar transliteração + Permitir leituras não latinas da fonte de letras; quando desligado, manter apenas letras latinas, letras acentuadas e dígitos de tom + Lê cada palavra de uma letra sincronizada (sincronização de palavras ativada); coreano não requer download; catorze scripts compartilham um download CLDR; caracteres Han são lidos pela Lista de Palavras Rime quando sua tabela de palavras cobre o idioma + Baixando dicionários de leitura… Leituras prontas (%1$d entradas) Falha no download - desligue e ligue novamente para tentar novamente + Dicionário Offline + Japonês (SudachiDict, ~75 MB) + Caracteres chineses e cantoneses (UniDict, ~8 MB) + Palavras chinesas (pinyin-pro, ~17 MB) + Transliteração grega, eslava cirílica (incluindo russo, ucraniano, búlgaro, sérvio, bielorrusso, macedônio), georgiano, armênio, hebraico, árabe, amárico e siríaco (CLDR, ~0,5 MB) + Lendo %1$s: %2$d%% + Escolha pelo menos um dicionário Mostrar botão Atualizar Toque para alternar para a próxima candidata a letra; pressione longamente para pesquisar letras por título da música e artista Pesquisar letras @@ -545,8 +588,10 @@ Cookies necessários: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAP Guardado Destacar palavras ou sílabas sincronizadas Priorizar letras sincronizadas em nível de palavra + Encurtar linhas inativas + Desenha cada linha que não está sendo cantada atualmente ligeiramente menor, sobre ela escurecendo-a Mostrar vocais de fundo separadamente - Remove uma vocalização de fundo escrita entre parênteses, como \"(Oh) eu quero você\", da linha cantada e a coloca em sua própria linha mais suave + Separa vocais de fundo e harmonia da linha principal. Move um vocal de fundo escrito entre parênteses, como \"(Oh) Eu quero você\", fora da linha cantada para uma linha mais escura por conta própria Ocultar a linha reproduzida Ocultar linhas da letra acima da linha em reprodução Ocultar letras não reproduzidas @@ -604,10 +649,15 @@ Aplica-se a ambos os limites. Em 1,75, quatro caracteres de largura total contam Traduzido por %s Romanizado por %s Romanizado por %s + Romanizado por Rime (%s) Ocultar linha de crédito Idioma da tradução - Idioma de romanização + Idioma de romanização ou transliteração + "Idioma de destino da tradução: +%s" + "Idioma de destino da romanização ou transliteração: +%s" Traduzindo… Não foi possível traduzir a letra Romanizado @@ -621,7 +671,7 @@ Aplica-se a ambos os limites. Em 1,75, quatro caracteres de largura total contam Atualizando Atualizado Chave de API do QuickLRC - O QuickLRC transcreve ou ajusta temporalmente as letras da faixa atual. Um serviço separado, não relacionado ao endpoint compatível com OpenAI acima + QuickLRC transcreve ou ajusta temporalmente as letras para a faixa atual Chave do QuickLRC salva. Use o botão de alinhamento na caixa de diálogo de busca de letras, ou o alerta no painel quando uma faixa não tiver letras Chave de API do QuickLRC salva QuickLRC não aceitou essa chave @@ -637,56 +687,50 @@ Aplica-se a ambos os limites. Em 1,75, quatro caracteres de largura total contam Nenhuma letra para alinhar - transcrevendo em vez disso O QuickLRC não pôde processar esta faixa Os créditos do QuickLRC foram esgotados para esta faixa - "As letras são correspondidas por título e artista, metadados incomuns podem não encontrar nada ou a versão errada. Use atualizar para trocar de versões. Fornecedores marcados com * requerem um token. Nem todos os provedores habilitados são solicitados sempre na sua ordem, mas respondem baseados em sua velocidade.\n\nAs letras vêm de serviços de terceiros que não são afiliados, patrocinados ou endossados pela Morphe. Cada um é usado conforme está: as letras, sua disponibilidade e suas licenças permanecem com esses serviços e seus licenciadores, e usá-las pode estar sujeita aos termos de serviço deles. O que chega a um serviço habilitado é apenas a consulta solicitada - o título, o artista e, onde o serviço precisar, o ID do vídeo. O endpoint opcional de IA e a transcrição QuickLRC são executados apenas após configurados, e recebem o texto das letras ou a URL do vídeo da faixa." + "As letras são correspondidas por título e artista, metadados incomuns podem não encontrar nada ou a versão errada. Use atualizar para trocar versões. Provedores marcados * requerem um token. Nem todos os provedores habilitados são sempre solicitados na sua ordem, mas respondem com base na sua velocidade. + +As letras vêm de serviços de terceiros que não são afiliados, patrocinados ou endossados pela Morphe. Cada um é usado como está: as letras, sua disponibilidade e sua licenciamento ficam com esses serviços e seus licenciantes, e seu uso pode estar sujeito aos seus termos de serviço. O que alcança um serviço habilitado é apenas a consulta que você pediu - o título, o artista e, onde esse serviço precisa, o ID do vídeo. O endpoint de IA opcional e a transcrição QuickLRC executam apenas depois de você configurá-los, e recebem o texto da letra ou a URL do vídeo da faixa." Ative apenas os provedores que você usa ou mantenha os padrões - solicitações constantes podem sobrecarregar sites de letras de pequena comunidade Ocultar a linha de crédito Ocultar a linha de crédito exibida abaixo das letras Trocar tradução e romanização - ON coloca a romanização abaixo das letras e a tradução acima; OFF mantém a romanização acima e a tradução abaixo - Tradução - Romanização - Provedor de tradução - Google (Padrão, Grátis) - Compatível com OpenAI (Personalizado) - Provedor de romanização - Google (Padrão) - Dicionário Local / Offline - Compatível com OpenAI (Personalizado) - Seção de IA - Usado apenas quando o provedor não fornece rótulos de seção como Refrão ou Verso, que o seu modelo compatível com OpenAI então detecta - Endpoints de IA & Nuvem - Configure chaves de API, modelos, endpoints e modelos de prompt para provedores de IA. - URL do endpoint da API (ex: https://api.openai.com/v1/chat/completions) + ON coloca romanização ou transliteração abaixo das letras e tradução acima; OFF mantém romanização ou transliteração acima e tradução abaixo. Uma leitura colocada sobre cada sílaba se move com ela, de cima de sua sílaba para baixo dela + Centralizar sempre as letras + ON coloca cada linha no meio e ignora o alinhamento da fonte, incluindo lados dueto; OFF mantém o alinhamento que as letras solicitam + Pelo menos um provedor deve permanecer ativo + Tradução + Romanização ou transliteração + Dicionário Offline + Lista de Palavras Rime + Detectar seções com IA + Usado apenas quando o provedor não envia rótulos de seção como Refrão ou Verso, que seu modelo compatível com OpenAI detecta em seguida. Editar a linha também abre seu prompt + URL do endpoint da API (ex. https://api.openai.com/v1/chat/completions, https://api.anthropic.com/v1/chat/completions, https://generativelanguage.googleapis.com/v1beta/openai/chat/completions) Modelo de Prompt Espaços reservados: {language}, {task}, {title}, {artist}, {count}, {lines} Prompt salvo Prompt salvo, mas o endpoint da IA ainda não está utilizável. Configure-o primeiro nas Configurações de IA Prompt redefinido - Não é JSON válido. Corrija-o ou limpe o campo antes de salvar Configurações de Tradução & Romanização salvas As definições foram redefinidas para a predefinição A URL base deve começar com http:// ou https:// - Testar Conexão - Configurar Endpoints - Mostrar Todos os Endpoints + Configurações incompletas ou rejeitadas pelo provedor Validando conexão… - Provedor de Tradução - Provedor de Romanização - Obter Modelos + %1$s: Conectado! (%2$s) + %1$s: Falha na conexão. Verifique as configurações. + Insira a chave de API primeiro Obtendo modelos… Selecione um Modelo Falha ao buscar modelos. Verifique a chave da API ou a conexão. - Configurações da API do Gemini - Compatível com OpenAI (Endpoint Personalizado) - Configurações da API do DeepL - Chave da API + Mensagem de texto Modelo URL Base + Escolher um serviço Chave da API (opcional) - Prompt de tradução Corpo de requisição extra (JSON opcional) + Cabeçalhos de solicitação extras (JSON opcional) + Cite nomes e valores, ex. {\"api-key\": \"abc\"}. Um valor sem cotação contendo / : ou número é rejeitado. Um valor vazio remove um cabeçalho. + Cite nomes e valores, ex. {\"temperature\": 0.3}. Um valor sem cotação contendo / ou : é rejeitado. Modelo e mensagens não podem ser definidos aqui. Chave de autenticação (Free ou Pro) - Configure chaves de API, modelos, endpoints e prompts 8.23.51 - Desativar ícones em negrito 9.13.51 - Restaurar as abas A seguir, Letras e Relacionados do Player diff --git a/patches/src/main/resources/addresources/values-pt-rPT/youtube/strings.xml b/patches/src/main/resources/addresources/values-pt-rPT/youtube/strings.xml index 4e986c45be..515961f044 100644 --- a/patches/src/main/resources/addresources/values-pt-rPT/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-pt-rPT/youtube/strings.xml @@ -29,6 +29,8 @@ Second \"item\" text" Diversos Vídeo Restaurar menus de configurações antigos + Exibir mensagens no snackbar do YouTube + Mensagens do Morphe, como a confirmação de um botão de reprodução, são exibidas no snackbar do YouTube em vez de uma janela pop-up Remover restrições de reprodução em segundo plano Desativar reprodução de fundo de Shorts diff --git a/patches/src/main/resources/addresources/values-ro-rRO/music/strings.xml b/patches/src/main/resources/addresources/values-ro-rRO/music/strings.xml index c5af8ecc9f..d5e067f54f 100644 --- a/patches/src/main/resources/addresources/values-ro-rRO/music/strings.xml +++ b/patches/src/main/resources/addresources/values-ro-rRO/music/strings.xml @@ -462,12 +462,47 @@ Pentru a te asigura că scrobbling-ul este sincronizat în timpul redării în f Afișare Sincronizare Panou - Endpoint AI Toți furnizorii sunt dezactivați - versuri încorporate și versuri încapsulate Resetați ordinea furnizorilor? Restabiliți ordinea implicită de prioritate a furnizorilor. + Lista de cuvinte Rime + "Glossarul de cuvinte Rime necesită, de asemenea, UniDict: în furnizorul de dicționare offline, bifați „Caractere chineze și cantoneze (UniDict)” și așteptați să se termine descărcarea mai întâi. Fără acesta, o listă care citește caractere nu va genera nicio citire. + +Bifați listele pe care trebuie să le citească Glossarul de cuvinte Rime, apoi atingeți Salvare pentru a le descărca. O listă adăugată de dumneavoastră este eliminată când debifați această opțiune. + +Fiecare listă reprezintă propria dumneavoastră tabelă de corespondență cuvânt-citire, un fișier .dict.yaml cu linii de forma: +字 ham +典 din2 +Prima coloană conține cuvântul, a doua citirea acestuia; coloanele suplimentare și liniile fără tab sunt ignorate." + Nicio listă de cuvinte Rime nu este bifată pentru limba de mai sus. Apasați pe acest nume pentru a bifa una sau selectați o limbă care are o listă + Adaugă + Adăugați o listă de cuvinte Rime + Un fișier .dict.yaml, sau un arhivă zip care conține unul, cu linii de forma 字 ham și 典 din2. Lista este preluată când o bifați și atingeți Salvare, iar licența acesteia vă aparține să o verificați + Codul limbii + Două sau trei litere, precum hak sau wuu + Nume + Se afișează sub citire, precum Hakka (Meixian) + Adresa listei de cuvinte + Adresa https:// a fișierului .dict.yaml sau a arhivei sale zip + Caracterele care nu fac parte dintr-un cuvânt + Automat + Caractere cantoneze + Caractere mandarine + Nicio citire a caracterelor + Completați codul, numele și adresa + Codul trebuie să aibă două sau trei litere, precum hak + Adresa trebuie să înceapă cu https:// + Numele și adresa nu pot conține un bar vertical + %1$s are deja o tabelă de cuvinte + %1$s este citit de romanizatorul încorporat și nu poate accepta o listă de cuvinte + Nefolosit: această limbă are deja propriul său sistem de citire, deci dezactivați bifarea pentru a o elimina + Adăugată %1$s - activați bifarea și atingeți Salvare pentru a o descărca + Listele de cuvinte sunt gata + Listele de cuvinte sunt gata (%1$d intrări) + Construirea listei de citiri: %1$d%% + Descărcarea listei de cuvinte a eșuat - atingeți din nou Salvare pentru a reîncerca Golește versurile stocate - Șterge versurile păstrate pe acest dispozitiv + Ștergeți %1$s MB de versuri păstrate pe acest dispozitiv S-au goșit %1$s MB Obțineți tokenul Token de utilizator media Apple Music @@ -510,14 +545,22 @@ Cookie-uri necesare: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPI Arată butonul de copiere Atinge pentru a copia versurile originale ca text simplu; apasă lung pentru a salva fișierul sursă al versurilor brute în Descărcări Arată butonul de traducere - Atinge pentru a comuta traducerea; apasă lung pentru traducere doar. Utilizează traducerile furnizorului, Google Translate sau o API compatibilă cu OpenAI - Afișați butonul Romanize. - Atinge pentru a comuta romanizarea; apasă lung pentru romanizare doar. Utilizează romanizarea furnizorului, Google Translate sau o API compatibilă cu OpenAI - Romanizare și Aliniament Local - Romanițare pe dispozitiv când furnizorul, modelul tău și Google Translate eșuează. Citirea urmează fiecare cuvânt al unui vers sincronizat; sincronizarea cuvintelor trebuie să fie activată. Chineză, Cantonese, Japoneză, Coreeană, Greacă, Rusă, Georgiană, Armenă; aprox. 120 MB - Citire %1$s: %2$d%% + Atingeți pentru a comuta traducerea; apăsare lungă doar pentru traducere. Folosește furnizorii de traducere activati în ordinea listată + Afișați butonul Romanizare sau Transliterare + Atingeți pentru a comuta romanizarea sau transliterarea; apăsare lungă doar pentru romanizare sau transliterare. Folosește furnizorii de romanizare activati în ordinea listată + Activați transliterarea + Permiteți citirile non-latine din sursa versurilor; când este dezactivat, păstrați doar litere latine, litere diacritice și cifre de ton + Citește fiecare cuvânt dintr-un vers sincronizat (sincronizare pe cuvânt activată); Coreana nu necesită descărcare; paisprezece scrieri împart o singură descărcare CLDR; caracterele Han sunt citite de Lista de cuvinte Rime atunci când tabela sa de cuvinte acoperă limba + Se descarcă dicționarele de citiri… Citiri pregătite (%1$d intrări) Descărcare eșuată - dezactivează și activează din nou pentru a reîncerca + Dicționar Offline + Japoneză (SudachiDict, ~75 MB) + Caractere chinezești și cantoneze (UniDict, ~8 MB) + Cuvinte chinezești (pinyin-pro, ~17 MB) + Transliterare greacă, slavă chirilică (inclusiv rusă, ucraineană, bulgară, sârbă, belarusă, macedoneană), georgiană, armeană, ebraică, arabă, amharică și siriacă (CLDR, ~0,5 MB) + Citire %1$s: %2$d%% + Selectați cel puțin un dicționar Afișați butonul Reîmprospătare. Atinge pentru a comuta la candidatul următor pentru versuri; apasă lung pentru a căuta versurile după titlu și artist Căutare versuri @@ -528,8 +571,10 @@ Cookie-uri necesare: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPI Salvat Evidențiați cuvintele sau silabele sincronizate Nu s-a putut face romanizarea versurilor. + Micșorați liniile inactive + Desenează fiecare linie care nu este cântată în prezent puțin mai mică, suprapus peste estomparea acesteia Afișați vocalurile de fundal separat - Mută vocea de fundal scrisă între paranteze, cum ar fi \"(Oh) te vreau\", din linia cântată într-o linie mai slabă + Separă vocalurile de fundal și armonia de linia principală. Mută o voce de fundal scrisă între paranteze, cum ar fi „(Oh) te vreau”, din linia cântată într-o linie distinctă, mai estompată Ascundeți versurile. Ascundeți rândurile de versuri de mai jos. Ascundeți versurile necunoscute. @@ -587,10 +632,15 @@ Se aplică ambelor limite. La 1,75, patru caractere cu lățime completă sunt c Tradus de %s Romanizat de %s Romanaizat de %s + Romanizat de Rime (%s) Redare reluată. Limba de traducere - Limba de romanizare + Limba de romanizare sau transliterare + "Limba țintă pentru traducere: +%s" + "Limba țintă pentru romanizare sau transliterare: +%s" Se traduce… Nu s-au putut traduce versurile Redare reluată. @@ -604,7 +654,7 @@ Se aplică ambelor limite. La 1,75, patru caractere cu lățime completă sunt c Se încarcă… Reîmprospătat Cheie API QuickLRC - QuickLRC transcrie sau aliniază temporal versurile pentru piesa curentă. Un serviciu separat, neîn legătură cu endpoint-ul compatibil OpenAI de mai sus + QuickLRC transcrie sau aliniază temporal versurile pentru piesa curentă Cheia QuickLRC salvată. Folosește butonul de aliniere în dialogul de căutare versuri, sau promptul panoului când o piesă nu are versuri Cheie API QuickLRC salvată QuickLRC nu a acceptat acea cheie @@ -620,58 +670,50 @@ Se aplică ambelor limite. La 1,75, patru caractere cu lățime completă sunt c Niciun vers de aliniat - se face transcriere în schimb QuickLRC nu a putut procesa această piesă Creditele QuickLRC au epuizat pentru această piesă - "Versurile sunt potrivite după titlu și artist; metadatele neobișnuite pot să nu găsească nimic sau să găsească versiunea greșită. Folosește reîmprospătarea pentru a comuta versiunile. Furnizorii marcați cu * necesită un token. Nu toți furnizorii activați sunt solicitați întotdeauna în ordinea ta, ci răspund în funcție de viteză. + "Versurile sunt potrivite în funcție de titlu și artist; metadatele neobișnuite pot duce la lipsa rezultatelor sau la identificarea versiunii greșite. Utilizați reîmprospătarea pentru a comuta între versiuni. Furnizorii marcați cu * necesită un token. Nu toți furnizorii activați sunt solicitați întotdeauna în ordinea specificată de dumneavoastră, ci răspund în funcție de viteza lor. -Versurile provin de la servicii terțe care nu sunt afiliate, sponsorizate sau aprobate de Morphe. Fiecare dintre acestea este utilizat așa cum este: versurile, disponibilitatea lor și licențierea rămân cu aceste servicii și cu licențiatorii lor, iar utilizarea lor poate fi supusă termenilor și condițiilor lor de serviciu. Ceea ce ajunge la un serviciu activat este doar interogarea cerută - titlul, artistul și, acolo unde serviciul le necesita, ID-ul videoclipului. Endpoint-ul AI opțional și transcrierea QuickLRC rulează numai după ce le configurezi, și primesc textul versurilor sau URL-ul videoclipului piesei." +Versurile provin de la servicii terțe care nu sunt afiliate, sponsorizate sau endorse de Morphe. Fiecare serviciu este utilizat așa cum este: versurile, disponibilitatea lor și condițiile de licențiere rămân în proprietatea acestor servicii și a licențiatorilor lor, iar utilizarea lor poate fi supusă condițiilor de utilizare ale acestora. Ceea ce ajunge la un serviciu activat este doar interogarea solicitată - titlul, artistul și, acolo unde serviciul o cere, ID-ul videoclipului. Endpoint-ul AI opțional și transcripția QuickLRC rulează numai după configurarea lor și primesc textul versurilor sau URL-ul videoclipului piesei." Activează doar furnizorii pe care îi utilizezi sau păstrează setările implicite — solicitările constante pot copleși site-urile comunitare mici de versuri Redare în curs pe acest dispozitiv. Redare în curs pe un alt dispozitiv. Redare în curs pe acest dispozitiv. - Redare în curs pe un alt dispozitiv. - Traducere - Romanizare - Furnizor de traducere - Google (Implicit, Gratuit) - Compatibil OpenAI (Personalizat) - Furnizor de romanizare - Google (Implicit) - Dicționar Local / Offline - Compatibil OpenAI (Personalizat) - Secțiune AI - Utilizat doar atunci când furnizorul nu trimite etichete de secțiune, cum ar fi Refren sau Strofă, pe care modelul tău compatibil cu OpenAI le detectează ulterior - AI & Puncte de terminal Cloud - Configurați cheile API, modelele, punctele de terminal și șabloanele de prompt pentru furnizorii de AI. - URL-ul punctului de terminal API (ex. https://api.openai.com/v1/chat/completions) + ON plasează romanizarea sau transliterarea sub versuri și traducerea deasupra; OFF păstrează romanizarea sau transliterarea deasupra și traducerea jos. O citire plasată peste fiecare silabă se mișcă împreună cu aceasta, de deasupra silabei spre dedesubtul acesteia + Centrare forțată a versurilor + ON plasează fiecare linie în mijloc și ignora aliniamentul sursei, inclusiv părțile duetului; OFF păstrează aliniamentul cerut de versuri + Cel puțin un furnizor trebuie să rămână activ + Traducere + Romanizare sau transliterare + Dicționar Offline + Lista de cuvinte Rime + Detectarea secțiunilor cu AI + Utilizat doar atunci când furnizorul nu furnizează etichete de secțiuni precum Refren sau Vers, pe care modelul compatibil OpenAI le detectează. Editarea rândului deschide și promptul său + URL-ul endpoint-ului API (de ex. https://api.openai.com/v1/chat/completions, https://api.anthropic.com/v1/chat/completions, https://generativelanguage.googleapis.com/v1beta/openai/chat/completions) Șablon de Prompt Placeholder-e: {language}, {task}, {title}, {artist}, {count}, {lines} Prompt salvat Prompt salvat, dar endpoint-ul AI nu este încă utilizabil. Configurează-l mai întâi la Setări AI Prompt resetat - JSON nevalid. Corectează-l sau golește câmpul înainte de salvare Setările de traducere & romanizare au fost salvate Setările au fost resetate la valorile implicite URL-ul de bază trebuie să înceapă cu http:// sau https:// - Testare conexiune - Configurare puncte terminale - Afișare toate punctele terminale + Setările incomplete sau respinse de furnizor Se validează conexiunea… - Furnizor de traducere - Furnizor de romanizare - Preluare modele + %1$s: Conectat! (%2$s) + %1$s: Conexiunea a eșuat. Verificați setările. + Introduceți mai întâi cheia API Se preiau modelele… Selectați un model Eșec la preluarea modelelor. Verificați cheia API sau conexiunea. - Setări Gemini API - Compatibil OpenAI (Endpoint personalizat) - Setări DeepL API - Cheie API + Solicitare Model URL de bază + Selectați un serviciu Cheie API (opțional) - Prompt de traducere Corp de cerere suplimentar (JSON opțional) + Antete suplimentare de cerere (JSON opțional) + Cotați numele și valorile, de ex. {\"api-key\": \"abc\"}. O valoare necotată care conține / : sau o cifră este respinsă. O valoare goală elimină un antet. + Cotați numele și valorile, de ex. {\"temperature\": 0.3}. O valoare necotată care conține / sau : este respinsă. Modelul și mesajele nu pot fi setate aici. Cheie de autentificare (Free sau Pro) - Configurați cheile API, modelele, punctele finale și prompturile 8.23.51 - Dezactivează pictogramele aldine 9.13.51 - Restaurează filele Up next, Versuri și Conexe din player diff --git a/patches/src/main/resources/addresources/values-ro-rRO/youtube/strings.xml b/patches/src/main/resources/addresources/values-ro-rRO/youtube/strings.xml index 952e792ecf..eca2d57e3a 100644 --- a/patches/src/main/resources/addresources/values-ro-rRO/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ro-rRO/youtube/strings.xml @@ -29,6 +29,8 @@ Second \"item\" text" Diverse Video Restaurare meniuri setări vechi + Afișează mesajele în snackbar-ul YouTube + Mesajele Morphe, cum ar fi confirmarea unui buton al playerului, sunt afișate în snackbar-ul YouTube în loc de o fereastră pop-up Elimină restricțiile de redare în fundal Dezactivează redarea în fundal diff --git a/patches/src/main/resources/addresources/values-ru-rRU/music/strings.xml b/patches/src/main/resources/addresources/values-ru-rRU/music/strings.xml index 0b1566bacc..2e98bf0f24 100644 --- a/patches/src/main/resources/addresources/values-ru-rRU/music/strings.xml +++ b/patches/src/main/resources/addresources/values-ru-rRU/music/strings.xml @@ -462,12 +462,47 @@ SABR-воспроизведение (подмена видеопотоков о Отображение Синхронизация Панель - Конечная точка AI Все источники отключены - встроенные тексты песен и встроенные тексты Сбросить порядок сервисов? Восстановить сервисы в порядке приоритета по умолчанию + Список слов Rime + "Словарю Rime также необходим UniDict: в провайдере офлайн-словаря отметьте \"Китайские и кантонские иероглифы (UniDict)\" и дождитесь окончания загрузки. Без него список с иероглифами не будет давать транскрипцию. + +Отметьте списки, которые должен считывать Rime Wordlist, затем нажмите Сохранить для их загрузки. Список удаляется при снятии отметки. + +Каждый список — это ваша собственная таблица сопоставления слова и произношения в виде файла .dict.yaml со строками вида: +字 ham +典 din2 +Первый столбец — это слово, второй — его произношение; лишние столбцы и строки без табуляции игнорируются." + Для выбранного выше языка не отмечен ни один список слов Rime. Нажмите на это имя, чтобы отметить нужный список, или выберите язык, для которого есть список + Добавить + Добавить список слов Rime + Файл .dict.yaml или zip-архив, содержащий такой файл, со строками вида 字 ham и 典 din2. Список загружается при его отметке и нажатии кнопки Сохранить, а лицензию на него вы можете проверить самостоятельно + Языковой код + Две или три буквы, например hak или wuu + Имя + Отображается под произношением, например Hakka (Meixian) + Адрес списка слов + HTTPS-адрес файла .dict.yaml или его zip-архива + Символы вне слов + Автоматически + Кантонийские символы + Мандаринские символы + Нет символов для чтения + Заполните код, название и адрес + Код должен состоять из двух или трех букв, например hak + Адрес должен начинаться с https:// + Название и адрес не могут содержать вертикальную черту + %1$s уже имеет таблицу слов + %1$s транслитерируется встроенным инструментом и не может использовать список слов + Не используется: этот язык уже имеет свое собственное произношение, поэтому снимите с него отметку, чтобы удалить + Добавлен %1$s - отметьте его и нажмите Сохранить, чтобы скачать + Списки слов готовы + Списки слов готовы (%1$d записей) + Создание таблицы произношения: %1$d%% + Ошибка загрузки списка слов - нажмите Сохранить еще раз, чтобы повторить попытку Очистить сохраненные тексты песен - Удалить тексты песен, сохраненные на этом устройстве + Удалить тексты песен размером %1$s МБ, сохраненные на этом устройстве Очищено %1$s MB Получить токен Apple Music Media-user-token @@ -522,14 +557,22 @@ SABR-воспроизведение (подмена видеопотоков о Показать кнопку копирования Нажмите, чтобы скопировать исходный текст песни в виде простого текста; нажмите и удерживайте, чтобы сохранить исходный файл текста песни в папке \"Загрузки\" Показать кнопку перевода - Нажмите, чтобы включить или выключить перевод; нажмите и удерживайте, чтобы отобразить только перевод. Использует переводы сторонних сервисов, Google Translate или API, совместимый с OpenAI. - Показывать кнопку романизации - Нажмите, чтобы включить или выключить транслитерацию; нажмите и удерживайте, чтобы использовать только транслитерацию. Использует романизацию провайдера, Google Translate или API, совместимый с OpenAI - Локальная романизация и выравнивание - Используйте транслитерацию перевода на устройстве, если провайдер, ваша модель и Google Translate не справляются. Чтение следует за каждым словом текста песни, воспроизводимого в режиме хронометража; функция синхронизации слов должна быть включена. Китайский, кантонский, японский, корейский, греческий, русский, грузинский, армянский; около 120 MB - Чтение %1$s: %2$d%% + Нажмите для включения/выключения перевода; длительное нажатие показывает только перевод. Использует включенных провайдеров перевода в порядке их перечисления + Показать кнопку романзации или транслитерации + Нажмите для включения/выключения романзации или транслитерации; длительное нажатие включает только романзацию или только транслитерацию. Использует включенных провайдеров романзации/транслитерации в порядке их перечисления + Включить транслитерацию + Разрешить непиктографические произведения из источника текста песни; если отключено, оставляются только латинские буквы, буквы с диакритическими знаками и цифры тонов + Произносит каждое слово синхронизированной по времени песни (синхронизация слов включена); Корее не нужна загрузка; четырнадцать скриптов используют один загрузочный пакет CLDR; китайские иероглифы произносятся через список слов Rime, если таблица слов охватывает этот язык + Загрузка словарей произношения… Материалы для чтения готовы (%1$d записи) Загрузка не удалась - выключите и включите снова, чтобы повторить попытку + Офлайн-словарь + Японский (SudachiDict, ~75 МБ) + Китайские и кантонийские символы (UniDict, ~8 МБ) + Китайские слова (pinyin-pro, ~17 МБ) + Греческий, славянский кириллический (включая русский, украинский, болгарский, сербский, белорусский, македонский), грузинский, армянский, еврейский, арабский, амхарский и сирийский транслитерация (CLDR, ~0,5 МБ) + Чтение %1$s: %2$d%% + Выберите хотя бы один словарь Показывать кнопку «Обновить» Нажмите, чтобы перейти к следующему варианту текста песни; нажмите и удерживайте, чтобы выполнить поиск текста по названию песни и исполнителю Поиск текста песни @@ -540,8 +583,10 @@ SABR-воспроизведение (подмена видеопотоков о Сохранено Подсвечивать синхронизированные слова по мере воспроизведения, если доступно Отдавать приоритет посекундному тексту, когда он доступен. Когда в тексте есть тайминги слов, подсвечивать каждое слово или слог в такт музыке. - Показывать бэк-вокал отдельно - Переносит фоновый вокал, записанный в скобках, например \"(О) я хочу тебя\", из основной строки в отдельную слабую строку + Сжимать неактивные строки + Рисует каждую строку, которая в данный момент не исполняется, чуть меньше, поверх затемнения + Показывать фоновый вокал отдельно + Разделяет фоновый вокал и гармонию от главной строки. Перемещает фоновый вокал, написанный в скобках, например «(О) я хочу тебя», из исполняемой строки в отдельную затемненную строку Скрывать воспроизведённые строки Скрывать строки текста песни выше текущей воспроизводимой строки Скрывать невоспроизведённые строки @@ -599,10 +644,15 @@ SABR-воспроизведение (подмена видеопотоков о Переведено с помощью %s Транслитерировано с помощью %s Романизировано пользователем %s + Романизировано через Rime (%s) Перевод Язык перевода - Язык романизации + Язык романзации или транслитерации + "Язык перевода: +%s" + "Язык романизации или транслитерации: +%s" Перевод… Не удалось перевести текст песни Романизация @@ -616,7 +666,7 @@ SABR-воспроизведение (подмена видеопотоков о Обновление Обновлено Ключ API QuickLRC - QuickLRC транскрибирует или временнó выравнивает тексты песен для текущего трека. Это отдельный сервис, не связанный с указанной выше конечной точкой, совместимой с OpenAI + QuickLRC транскрибирует или временнó выравнивает текст песни для текущего трека Ключ QuickLRC сохранен. Используйте кнопку выравнивания в диалоговом окне поиска текстов или подсказку на панели, если у трека отсутствуют тексты Ключ API QuickLRC сохранен QuickLRC отклонил этот ключ @@ -632,56 +682,50 @@ SABR-воспроизведение (подмена видеопотоков о Нечему выравнивать — вместо этого выполняется транскрипция QuickLRC не смог обработать этот трек Кредиты QuickLRC исчерпаны для этого трека - "Текст песни сопоставляется по названию и исполнителю; при наличии необычных метаданных может ничего не быть найдено или будет найдена неверная версия. Используйте обновления, чтобы переключаться между версиями. Для поставщиков, отмеченных знаком *, требуется токен. Не все подключенные провайдеры всегда запрашиваются в вашем заказе, но они отвечают в зависимости от своей скорости.\n\nТексты песен взяты из сторонних сервисов, которые не связаны с Morphe, не спонсируются и не одобряются компанией Morphe. Каждый из них используется в том виде, в каком он есть: права на тексты песен, их доступность и условия лицензирования принадлежат данным сервисам и их лицензиарам, а их использование может регулироваться условиями предоставления услуг этих сервисов. К включенному сервису поступает только запрошенная вами информация - название, исполнитель и, если это необходимо данному сервису, видео ID. Дополнительный конечный узел ИИ и транскрипция с помощью QuickLRC запускаются только после их настройки и получения текста песни или URL видеозаписи трека." + "Тексты песен сопоставляются по названию и исполнителю; при наличии необычных метаданных может ничего не быть найдено или будет найдена неверная версия. Используйте обновления, чтобы переключаться между версиями. Для поставщиков, отмеченных знаком *, требуется токен. Не все подключенные провайдеры всегда запрашиваются в вашем заказе, но они отвечают в зависимости от своей скорости. + +Тексты песен взяты из сторонних сервисов, которые не связаны с Morphe, не спонсируются и не одобряются компанией Morphe. Каждый из них используется в том виде, в каком он есть: права на тексты песен, их доступность и условия лицензирования принадлежат данным сервисам и их лицензиарам, а их использование может регулироваться условиями предоставления услуг этих сервисов. К включенному сервису поступает только запрошенная вами информация - название, исполнитель и, если это необходимо данному сервису, видео ID. Дополнительный конечный узел ИИ и транскрипция с помощью QuickLRC запускаются только после их настройки и получения текста песни или URL видеозаписи трека." Включайте только тех провайдеров, которыми вы пользуетесь, или оставьте настройки по умолчанию — постоянные запросы могут перегружать небольшие сайты текстов песен сообщества Скрыть строку с указанием авторства Скрывать строку с указанием авторства под текстом Поменять местами перевод и романизацию - ON размещает романизацию под переводом, OFF — над ним - Перевод - Романизация - Поставщик перевода - Google (по умолчанию, бесплатно) - Совместимый с OpenAI (пользовательский) - Поставщик романизации - Google (по умолчанию) - Локальный / автономный словарь - Совместимый с OpenAI (пользовательский) - Разделы ИИ - Используется только в том случае, если провайдер не предоставляет метки разделов, таких как «Припев» или «Куплет», которые затем определяет ваша OpenAI-совместимая модель - ИИ & облачные эндпоинты - Настройте API-ключи, модели, эндпоинты и шаблоны промптов для ИИ-поставщиков. - URL-адрес эндпоинта API (например, https://api.openai.com/v1/chat/completions) + ВКЛ помещает романзацию/транслитерацию под текст песни, а перевод над ним; ВЫКЛ оставляет романзацию/транслитерацию сверху и перевод снизу. Произношение, размещенное над каждым слогом, перемещается вместе с ним, переходя сверху над слогом под него + Всегда центрировать текст песни + ВКЛ ставит каждую строку в центр и игнорирует исходное выравнивание, включая дуэтные стороны; ВЫКЛ сохраняет выравнивание, запрошенное в тексте песни + Хотя бы один провайдер должен быть включен + Перевод + Романизация или транслитерация + Офлайн-словарь + Список слов Rime + Обнаруживать секции с помощью ИИ + Используется только когда у провайдера нет меток секций, таких как Припев или Куплет, которые тогда обнаруживает ваша модель, совместимая с OpenAI. Редактирование строки также открывает её подсказку + URL конечной точки API (например, https://api.openai.com/v1/chat/completions, https://api.anthropic.com/v1/chat/completions, https://generativelanguage.googleapis.com/v1beta/openai/chat/completions) Шаблон промпта Плейсхолдеры: {language}, {task}, {title}, {artist}, {count}, {lines} Промпт сохранен Промпт сохранен, но конечная точка ИИ пока не готова к использованию. Сначала настройте её в разделе настроек ИИ Промпт сброшен - Неверный JSON. Исправьте ошибку или очистите поле перед сохранением Настройки перевода & романизации сохранены Настройки восстановлены до значений по умолчанию Базовый URL должен начинаться с http:// или https:// - Проверить соединение - Настройка эндпоинтов - Показать все эндпоинты + Настройки неполны или отклонены провайдером Проверка подключения… - Провайдер перевода - Провайдер романизации - Получить модели + %1$s: Подключено! (%2$s) + %1$s: Ошибка подключения. Проверьте настройки. + Сначала введите ключ API Загрузка моделей… Выберите модель Не удалось загрузить модели. Проверьте API-ключ или соединение. - Настройки Gemini API - Совместимо с OpenAI (пользовательская конечная точка) - Настройки DeepL API - API-ключ + Подсказка Модель Базовый URL + Выберите сервис API-ключ (необязательно) - Промпт для перевода Дополнительное тело запроса (необязательный JSON) + Дополнительные заголовки запроса (необязательный JSON) + Кавычки имен и значений, например {\"api-key\": \"abc\"}. Значение без кавычек, содержащее / : или число, отклоняется. Пустое значение удаляет заголовок. + Кавычки имен и значений, например {\"temperature\": 0.3}. Значение без кавычек, содержащее / или :, отклоняется. Модель и сообщения не могут быть установлены здесь. Ключ аутентификации (Free или Pro) - Настройка API-ключей, моделей, эндпоинтов и промптов Выключить жирные иконки 9.13.51 - Восстановить вкладки \"Далее\", \"Текст песни\" и \"Похожее\" в плеере diff --git a/patches/src/main/resources/addresources/values-ru-rRU/youtube/strings.xml b/patches/src/main/resources/addresources/values-ru-rRU/youtube/strings.xml index 5eb3587f69..b2a2abe15a 100644 --- a/patches/src/main/resources/addresources/values-ru-rRU/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-ru-rRU/youtube/strings.xml @@ -29,6 +29,8 @@ Second \"item\" text" Прочие Видео Восстановить старое меню настроек YouTube + Показывать сообщения в всплывающей панели YouTube + Сообщения Morphe, например подтверждение нажатия кнопки воспроизведения, отображаются во всплывающей панели YouTube вместо всплывающего окна Убрать ограничения на фоновое воспроизведение Отключить воспроизведение Shorts в фоновом режиме diff --git a/patches/src/main/resources/addresources/values-sk-rSK/music/strings.xml b/patches/src/main/resources/addresources/values-sk-rSK/music/strings.xml index df8dbbe23b..4484f1c916 100644 --- a/patches/src/main/resources/addresources/values-sk-rSK/music/strings.xml +++ b/patches/src/main/resources/addresources/values-sk-rSK/music/strings.xml @@ -462,12 +462,47 @@ Na zabezpečenie synchronizácie scrobblingu počas prehrievania na pozadí sa u Zobraziť Synchronizácia Panel - Koncový bod AI Všetky poskytovatelia vypnutí - zabudované texty a vložené texty Obnoviť poradie poskytovateľov? Obnoviť poskytovateľov do predvoleného poradia priority. + Zoznam slov Rime + "Zoznam slov Rime vyžaduje aj UniDict: v poskytovateľovi offline slovníkov začiarknite \"Čínske a kantonské znaky (UniDict)\" a nechajte ho najprv dokončiť sťahovanie. Bez neho zoznam, ktorý načíta znaky, neposkytne žiadnu výslovnosť. + +Začiarknite zoznamy, ktoré by mal Rime Wordlist čítať, potom kliknite na Uložiť a stiahnite ich. Zoznam, ktorý ste pridali, sa odstráni, keď ho odčiarknete. + +Každý zoznam je vaša vlastná tabuľka slovo-výslovnosť, .dict.yaml s riadkami ako: +字 ham +典 din2 +Prvý stĺpec je slovo, druhý jeho výslovnosť; extra stĺpce a riadky bez tabulátora sú ignorované." + Pre uvedený jazyk nie je začiarknutý žiadny zoznam slov Rime. Klepnutím na tento názov začiarknite jeden alebo vyberte jazyk, ktorý má zoznam + Pridať + Pridať zoznam slov Rime + Súbor .dict.yaml alebo zip obsahujúci jeden, s riadkami ako 字 ham a 典 din2. Zoznam sa načíta, keď ho začiarknete a kliknete na Uložiť, a jeho licencia je vaša na skontrolovanie + Kód jazyka + Dve až tri písmená, napríklad hak alebo wuu + Názov + Zobrazené pod čítaním, napríklad Hakka (Meixian) + Adresa zoznamu slov + Adresa https:// pre .dict.yaml alebo jeho zip + Znaky, ktoré nie sú súčasťou slova + Automaticky + Cantonese znaky + Mandarin znaky + Žiadne čítanie znaku + Vyplňte kód, názov a adresu + Kód musí mať dve alebo tri písmená, napríklad hak + Adresa musí začínať s https:// + Názov a adresa nemôžu obsahovať vertikálnu čiaru + %1$s už má tabuľku slov + %1$s je čítané zabudovaným romanizátorom a nemôže prijímať zoznam slov + Nepoužíva sa: tento jazyk už má svoje vlastné čítanie, takže ho zrušte na odstránenie + Pridané %1$s - zaškrtnite ho a klepnite na Uložiť na stiahnutie + Zoznamy slov pripravené + Zoznamy slov pripravené (%1$d položiek) + Čítanie zoznamu slov: %1$d%% + Stiahnutie zoznamu slov zlyhalo – klepnutím na Uložiť znova skúste obnoviť Vymazať uložené texty - Odstrániť texty uložené v tomto zariadení + Vymazať %1$s MB textov piesní uložených na tomto zariadení Vymazalo %1$s MB Získať token Apple Music token mediálneho používateľa @@ -522,14 +557,22 @@ Vyžadované cookies: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAP Zobraziť tlačidlo Kopírovať Klepnutím skopírujete originálne texty ako čistý text; dlhým podržím uložíte zdrojový súbor surových textov do priečinka Downloads Zobraziť tlačidlo Preložiť - Klepnutím prepnete preklad; dlhým podržím zobrazíte iba preklad. Používa preklady od poskytovateľov, Google Translate alebo OpenAI kompatibilné API - Zobraziť tlačidlo Kopírovať - Klepnutím prepnete transkripciu latinkou; dlhým podržím zobrazíte iba transkripciu latinkou. Používa transkripciu od poskytovateľov, Google Translate alebo OpenAI kompatibilné API - Lokálna transkripcia latinkou a synchronizácia - Transkripcia na zariadení, keď neposkytovatelia, váš model ani Google Translate nedostanú výsledok. Čítanie sleduje každé slovo časovo synchronizovaného textu; synchronizácia slov musí byť zapnutá. Čínština, kantónčina, japončina, kórejčina, gréčtina, ruština, gruzínčina, arménčina; cca 120 MB - Čítanie %1$s: %2$d%% + Klepnutím prepnete preklad; dlhým stlačením zobrazíte len preklad. Používa povolené poskytovateľov prekladu v poradí uvedenom v zozname + Zobraziť tlačidlo Transliterácia + Klepnutím prepnete transkrípciu alebo transliteráciu; dlhým stlačením zobrazíte len transkripciu alebo transliteráciu. Používa povolené poskytovateľov transkripcie v poradí uvedenom v zozname + Povoliť transliteráciu + Povoliť ne-latinské čítania zo zdroja textov piesní; ak je vypnuté, ponechať iba latinské písmená, písmená s diakritikou a číslice tónov + Číta každé slovo časovo synchronizovaného textu (synchronizácia slov zapnutá); jazyk kórejčina nevyžaduje stiahnutie; štrnástych scriptov zdieľa jedno stiahnutie CLDR; znaky Han sú čítané pomocou zoznamu slov Rime, keď jeho tabuľka slov pokrýva daný jazyk + Sťahovanie slovníkov... Čítania pripravené (%1$d položiek) Stiahnutie zlyhalo – vypnite a znovu zapnite toto nastavenie, aby ste skúsili opakované stiahnutie + Offline slovník + Japončina (SudachiDict, ~75 MB) + Čínske kantonské znaky (UniDict, ~8 MB) + Čínske slová (pinyin-pro, ~17 MB) + Transliterácia gréčtiny, slovanských cyrilík (vrátane ruštiny, ukrajinčiny, bulharčiny, srbčiny, bielorumčiny, macedónčiny), gruzínčiny, arménčiny, hebrejčiny, arabčiny, amharčiny a syriáčiny (CLDR, ~0,5 MB) + Čítanie %1$s: %2$d%% + Vyberte aspoň jeden slovník Preložiť text piesne Klepnutím prepnite na ďalší kandidát na text; dlhým podržím hľadajte texty podľa názvu pesničky a interpreta Vyhľadávanie textov @@ -540,8 +583,10 @@ Vyžadované cookies: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAP Uložené Kopírovať pôvodný text piesne Kopírovať preložený text piesne + Zmenšiť neaktívne riadky + Vytvorí každý riadok, ktorý sa práve nespieva, o niečo menší, pričom ho stmaví Zobraziť pozadové vokály samostatne - Presunie vokál v pozadí zapísaný v zátvorkách, napríklad \"(Ó) chcem ťa\", zo speváckej línie do vlastnej tichšej línie + Oddelí pozadové vokály a harmóniu od hlavnej línie. Presunie pozadový vokál napísaný v zátvorkách, napríklad \"(Oh) chcem ťa\", z spievanej línie do vlastného stmaveného riadku Pôvodný text piesne Preložený text piesne Preklad bol skopírovaný @@ -599,10 +644,15 @@ Platí pre oba limity. Pri hodnote 1,75 sa štyri znaky plnej šírky započíta Preložené pomocou %s Romanizované pomocou %s Latinizované pomocou %s + Transkribované Rime (%s) Skryť romanizáciu Jazyk prekladu - Jazyk latinizácie + Jazyk pre transkripciu alebo transliteráciu + "Cieľový jazyk prekladu: +%s" + "Cieľový jazyk romanizácie alebo transkripcie: +%s" Prekladám… Text piesne sa nepodarilo preložiť Romanizácia @@ -616,7 +666,7 @@ Platí pre oba limity. Pri hodnote 1,75 sa štyri znaky plnej šírky započíta Skryť prepis Prepis Kľúč API QuickLRC - QuickLRC prepisuje alebo synchronizuje texty piesní pre aktuálnu skladbu. Ide o samostatnú službu, nesúvisiacu s vyššie uvedeným endpointom kompatibilným s OpenAI + QuickLRC prepisuje alebo časovo zarovnáva texty piesní pre aktuálnu stopu Kľúč QuickLRC je uložený. Použite tlačidlo Zoradiť v dialógu vyhľadávania textov piesní alebo výzvu v paneli, ak skladba nemá žiadne texty Kľúč API QuickLRC bol uložený QuickLRC tento kľúč neakceptoval @@ -632,58 +682,50 @@ Platí pre oba limity. Pri hodnote 1,75 sa štyri znaky plnej šírky započíta Žiadne texty na zoradenie – namiesto toho sa vykonáva prepis QuickLRC nedokázal spracovať túto skladbu Kredity pre QuickLRC boli pre túto skladbu vyčerpané - "Texty sa spárovali podľa názvu a interpreta, neobvyklé metadáta môžu nezobraziť nič alebo nesprávnu verziu. Použite aktualizovať na prepínanie verzií. Poskytovatelia označení * vyžadujú token. Nie všetci povolení poskytovatelia sa vždy volajú vo vašom poradí, ale reagujú na základe svojej rýchlosti. + "Texty piesní sú porovnávané podľa názvu a interpreta, neobvyklé metadáta môžu nájsť nič alebo nesprávnu verziu. Použite obnovenie na prepínanie verzií. Poskytovatelia označení * vyžadujú token. Všetci povolení poskytovatelia nie sú vždy spúšťaní v poradí, v akom ste ich nastavili, ale reagujú podľa svojej rýchlosti. -Texty pochádzajú zo služieb tretích strán, ktoré nie sú pridružené, sponzorované ani podporované spoločnosťou Morphe. Každá sa používa tak, ako je: texty, ich dostupnosť a licencovanie zostávajú u týchto služieb a ich licenčných vydavateľov a ich používanie môže podliehať ich podmienkam používania. To, čo sa dostane k povolenej službe, je len vyhľadávanie, ktoré ste požadovali – názov, interpret a kde to táto služba potrebuje, aj video ID. Voliteľný koncový bod AI a transkripcia QuickLRC bežia iba po ich nakonfigurovaní a prijímajú text textov alebo video URL stopy." +Texty piesní pochádzajú zo služieb tretích strán, ktoré nie sú spojené, sponzorované ani schválené spoločnosťou Morphe. Každá z nich sa používa taká, aká je: texty piesní, ich dostupnosť a licenčné podmienky zostávajú týmito službám a ich licencierom a ich použitie môže byť podmienené ich podmienkami služby. To, čo sa dostane k povolenému systému, je iba hľadanie, ktoré ste požadovali - názov, interpret a tam, kde to tento systém potrebuje, ID videa. Voliteľný endpoint AI a transkripcia QuickLRC sa spustia až po ich konfigurácii a prijímajú text piesne alebo URL videa skladby." Povolte iba poskytovateľov, ktoré používate, alebo ponechajte predvolené – konštantné žiadosti môžu preťažiť malé komunitné stránky s textami Nahlásiť problém s prekladom Nahlásiť problém s textom piesne Problém bol nahlásený - Nahlásenie zlyhalo - Preklad - Romanizácia - Poskytovateľ prekladu - Google (predvolené, zadarmo) - Kompatibilné s OpenAI (vlastné) - Poskytovateľ romanizácie - Google (predvolené) - Lokálny / offline slovník - Kompatibilné s OpenAI (vlastné) - Sekcia AI - Používa sa len v prípade, že poskytovateľ neposkytne žiadne označenia sekcií, ako napríklad Refrén alebo Sloka, ktoré následne deteguje váš model kompatibilný s OpenAI - AI & cloudové koncové body - Konfigurujte API kľúče, modely, koncové body a šablóny promptov pre poskytovateľov AI. - URL koncového bodu API (napr. https://api.openai.com/v1/chat/completions) + ZAPNUTÉ umiestni transkripciu alebo transliteráciu pod texty piesní a preklad nad; VYPNUTÉ ponechá transkripciu alebo transliteráciu hore a preklad dole. Čítanie umiestnené nad každú slabiku sa s ňou posúva, zhora jej slabiky smerom dolu + Vždy strediť texty piesní + ZAPNUTÉ umiestni každý riadok doprostred a ignoruje zarovnanie zdroja vrátane strán duet; VYPNUTÉ zachová zarovnanie, ktoré požadujú texty piesní + Aspoň jeden poskytovateľ musí zostať zapnutý + Preklad + Transkripcia alebo transliterácia + Offline slovník + Zoznam slov Rime + Detekovať sekcie pomocou AI + Používa sa len vtedy, keď poskytovateľ neposkytuje štítky sekcií ako Refrén alebo Sloha, ktoré následne deteguje váš kompatibilný model OpenAI. Úprava riadku tiež otvorí jeho výzvu + URL koncového bodu API (napr. https://api.openai.com/v1/chat/completions, https://api.anthropic.com/v1/chat/completions, https://generativelanguage.googleapis.com/v1beta/openai/chat/completions) Šablóna promptu Zástupníky: {language}, {task}, {title}, {artist}, {count}, {lines} Prompt uložený Výzva bola uložená, ale AI endpoint ešte nie je použiteľný. Najprv ho nakonfigurujte pod Nastaveniami AI Prompt resetovaný - Neplatné JSON. Opravte ho alebo vyčistite pole pred uložením Nastavenia prekladu & romanizácie boli uložené Nastavenia boli obnovené na predvolené Základná URL musí začínať s http:// alebo https:// - Otestovať pripojenie - Konfigurovať koncové body - Zobraziť všetky koncové body + Nastavenia sú nekompletné alebo zamietnuté poskytovateľom Overuje sa pripojenie… - Poskytovateľ prekladu - Poskytovateľ romanizácie - Načítať modely + %1$s: Pripojené! (%2$s) + %1$s: Pripojenie zlyhalo. Skontrolujte nastavenia. + Najprv zadajte kľúč API Načítavanie modelov… Vyberte model Nepodarilo sa načítať modely. Skontrolujte API kľúč alebo pripojenie. - Nastavenia Gemini API - Kompatibilné s OpenAI (vlastný koncový bod) - Nastavenia DeepL API - API kľúč + Výzva Model Základná URL + Vyberte službu API kľúč (voliteľné) - Prompt pre preklad Dodatočné telo požiadavky (voliteľné JSON) + Dodatočné hlavičky požiadaviek (voliteľný JSON) + Uvedzte názvy a hodnoty do úvodzoviek, napr. {\"api-klic\": \"abc\"}. Neuvádzaná hodnota obsahujúca / : alebo číslo je zamietnutá. Prázdna hodnota odoberá hlavičku. + Uvedzme názvy a hodnoty v úvodzovkách, napr. {\"teplota\": 0,3}. Neopatrnená hodnota obsahujúca / alebo : je zamietnutá. Model a správy sa tu nemôžu nastaviť. Kľúč na autentifikáciu (Free alebo Pro) - Konfigurujte API kľúče, modely, koncové body a prompty 8.23.51 - Zakázať tučné ikony 9.13.51 – Obnoviť karty Prehrávača: Ďalej, Texty a Súvisiace diff --git a/patches/src/main/resources/addresources/values-sk-rSK/youtube/strings.xml b/patches/src/main/resources/addresources/values-sk-rSK/youtube/strings.xml index b677083cad..11c3c397a5 100644 --- a/patches/src/main/resources/addresources/values-sk-rSK/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-sk-rSK/youtube/strings.xml @@ -29,6 +29,8 @@ Second \"item\" text" Ostatné Video Obnoviť starú obrazovku nastavení YouTube + Zobraziť správy v snackbari YouTube + Správy aplikácie Morphe, ako je napríklad potvrdenie stlačenia tlačidla prehrávania, sa zobrazujú v snackbari YouTube namiesto vyskakovacieho okna Odstrániť obmedzenia prehrávania na pozadí Zakázať prehrávanie Shorts na pozadí diff --git a/patches/src/main/resources/addresources/values-sl-rSI/music/strings.xml b/patches/src/main/resources/addresources/values-sl-rSI/music/strings.xml index 18add0c473..3c10986150 100644 --- a/patches/src/main/resources/addresources/values-sl-rSI/music/strings.xml +++ b/patches/src/main/resources/addresources/values-sl-rSI/music/strings.xml @@ -460,12 +460,47 @@ Za zagotovitev sinhronizacije med predvajanjem v ozadju poskrbite, da je uporaba Prikaz Sinhronizacija Panel - Končna točka AI Vsi ponudniki onemogočeni - vgrajene besedila in vdelana besedila Ponastavitev vrstnega reda ponudnikov? Ponastavi ponudnike na privzeti vrstni red. + Seznam besed Rime + "Rime Wordlist prav tako potrebuje UniDict: v ponudniku zbirke besed Off-line preverite polje \"Kitajska in kantonska znaka (UniDict)\" in počakajte, da se prenese do konca. Brez tega seznam, ki bere znake, ne bo zagotavljal branja. + +Označite sezname, ki naj jih bere Rime Wordlist, nato pa pritisnite Shrani, da jih prenesete. Seznam, ki ga dodate, je odstranjen, če ga odznačite. + +Vsak seznam je vaša lastna tabela beseda-na-branje, .dict.yaml z vrsticami, kot so: +字 ham +典 din2 +Prvi stolpec je beseda, drugi njen zapis; dodatni stolpci in vrstice brez tabulatorja se prezrejo." + Zgornjemu jeziku ni označen noben seznam besed Rime. Pritisnite to ime, da označite enega, ali izberite jezik, ki ima seznam + Dodaj + Dodaj seznam besed Rime + Datoteka .dict.yaml ali zip, ki jo vsebuje, z vrsticami, kot so 字 ham in 典 din2. Seznam se pridobi, ko ga označite in pritisnete Shrani, njegova licenčna pogodba pa je na voljo za preverjanje. + Koda jezika + Dve ali tri črke, kot sta hak ali wuu + Ime + Prikazano pod načinom branja, npr. Hakka (Meixian) + Naslov seznama besed + Naslov https:// za datoteko .dict.yaml ali njen zip paket + Znaki, ki niso v besedi + Samodejno + Kantonski znaki + Mandarinovski znaki + Brez branja znakov + Izpolnite kodo, ime in naslov + Koda mora biti dve ali tri črke, kot je hak + Naslov se mora začeti z https:// + Ime in naslov ne smeta vsebovati navpične črte + %1$s že ima tabelo besed + %1$s bere vgrajeni romanizator in ne more uporabiti seznama besed + Ni uporabljeno: ta jezik že ima svoj način branja, zato ga odkljukajte, da ga odstranite + Dodan %1$s - označite ga in pritisnite Shrani za prenos + Seznami besed so pripravljeni + Seznami besed so pripravljeni (%1$d vnosi) + Gradnje seznama branja: %1$d%% + Prenos seznama besed ni uspel - znova pritisnite Shrani za poskus ponovnega prenosa Izbrishite shranjena besedila - Izbrišite besedila, ki so shranjena na tej napravi + Izbrišite %1$s MB besedil, shranjenih na tej napravi Očiščeno %1$s MB Pridobi žeton Apple Music medijski uporabniški žeton @@ -513,14 +548,22 @@ Zahtevani piškotki: SID, HSID, SSID, SAPISID, __Secure-3APISID, __Secure-1APISI Pokaži gumb Kopiraj Kliknite za kopiranje izvirnih besedil kot navadnega besedila; dolgotrajni klik shrani sirovo datoteko besedil v mapo Prenos Pokaži gumb Prevedi - Kliknite za preklop prevoda; dolgotrajni klik omogoča samo prevod. Uporablja prevode ponudnika, Google Prevajalnik ali API združljiv z OpenAI - Prikažite gumb za romanizacijo - Kliknite za preklop romanizacije; dolgotrajni klik omogoča samo romanizacijo. Uporablja romanizacijo ponudnika, Google Prevajalnik ali API združljiv z OpenAI - Lokalna romanizacija in uskladitev - Romanizirajte na napravi, ko odpovejo ponudnik, vaš model in Google Prevajalnik. Branje sledi vsaki besedi časovno sinhroniziranega besedila; sinhronizacija besed mora biti vklopljena. Kitajščina, kantonoščina, japonščina, korejščina, grščina, ruščina, gruzinščina, armenščina; približno 120 MB - Branje %1$s: %2$d%% + Pritisnite za preklop prevoda; dolgo pritisnite samo za prevod. Uporablja omogočene ponudnike prevajalnikov v vrstnem redu prikaza + Prikaži gumb Romaniziraj ali Transliteriraj + Pritisnite za preklop romanizacije ali transliteracije; dolgo pritisnite samo za romanizacijo ali transliteracijo. Uporablja omogočene ponudnike romanizacije v vrstnem redu prikaza + Omogoči transliteracijo + Dovoli nelatinske načine branja iz vira besedil; ko je izklopljeno, ohranite le latinske črke, črke z diakritičnimi znaki in številke tonov + Prebere vsako besedo časovno sinhroniziranega besedila (sinhronizacija besed vklopljena); za korejščino ni potreben prenos; štirinajst pisav deli en CLDR prenos; kitajski znaki se berejo s seznamom besed Rime, ko njegova tabela besed pokriva jezik + Prenačevanje slovarjev branja… Pripravljena branja (%1$d vnosa) Prenos ni uspel - izklopite in ponovno vklopite za poskus ponovnega prenosa + Spletni slovar + Japonščina (SudachiDict, ~75 MB) + Kitajski in kantonski znaki (UniDict, ~8 MB) + Kitajske besede (pinyin-pro, ~17 MB) + Grščina, slovanski cirilici (vključno z rusko, ukrajinsko, bolgarsko, srbsko, belorusko, makedonsko), gruzijska, armenska, hebrejska, arabska, amharska in sirijaska transliteracija (CLDR, ~0,5 MB) + Branje %1$s: %2$d%% + Izberite vsaj en slovar Prikaži gumb Osveži Kliknite za preklop na naslednjega kandidata za besedilo; dolgotrajni klik išče besedila po naslovu pesmi in izvajalcu Iskanje besedila @@ -531,8 +574,10 @@ Zahtevani piškotki: SID, HSID, SSID, SAPISID, __Secure-3APISID, __Secure-1APISI Shranjeno Poudarite sinhronizirane besede ali zloge Dajte prednost besedilom po posameznih besedah, kadar so na voljo. Ko ima besedilo časovne podatke za besede, poudari vsako besedo ali zlog v skladu z glasbo. - Prikaži spremljalni vokal ločeno - Premakne vokale v oklepajih, kot je \"(Oh) I want you\", iz glavne besedila v ločeno, tišjo črto + Stisni neaktivne vrstice + Risane so vse vrstice, ki trenutno ne spadajo med spevajoče, nekoliko manjše, ob tem zatemnjene + Prikaži zgoščeno vokalne ločeno + Loči zgoščene vokale in harmonijo od glavne vrstice. Premakne zgoščeni vokali, zapisan v oklepajih, kot je \"(Oh) želim te\", iz speva v lastno temnejšo vrstico Skrijte že predvajane vrstice besedila Skrijte vrstice besedila nad trenutno predvajano vrstico. Skrijte še nepredvane vrstice besedila @@ -590,10 +635,15 @@ Velja za obe omejitvi. Pri 1,75 štirje polnoznačajni znaki štejejo kot 7."Prevedel %s Romaniziral %s Romaniziral/a %s + Romanizirano z Rime (%s) Prevedeno Jezik prevoda - Jezik romanizacije + Jezik romanizacije ali transliteracije + "Jezik cilja prevajanja: +%s" + "Jezik cilja romanizacije ali transkripcije: +%s" Prevajanje… Besedila ni bilo mogoče prevesti. Romanizirano @@ -607,7 +657,7 @@ Velja za obe omejitvi. Pri 1,75 štirje polnoznačajni znaki štejejo kot 7."Osveževanje Osveženo API ključ QuickLRC - QuickLRC prepisuje ali časo-usklajuje besedila za trenutno skladbo. Ločena storitev, nepovezana z zgornjo združljivo končno točko OpenAI + QuickLRC transkribira ali časovno poravnava besedila za trenutno skladbo Ključ QuickLRC je shranjen. Uporabite gumb za usklajevanje v pogovornem oknu iskanja besedil ali poziv v plošči, ko skladba nima besedil Ključ API QuickLRC shranjen QuickLRC ni sprejel tega ključa @@ -623,58 +673,50 @@ Velja za obe omejitvi. Pri 1,75 štirje polnoznačajni znaki štejejo kot 7."Ni besedil za usklajevanje - namesto tega se izvaja prepis QuickLRC ni mogel obdelati te skladbe Krediti QuickLRC so za to skladbo potrošeni - "Besedila se ujmejo glede naslova in izvajalca; nenavadne metapodatke lahko najdejo nič ali napačno verzijo. Za preklapljanje verzij uporabite osveževanje. Ponudniki, označeni z *, zahtevajo žeton. Vsi omogočeni ponudniki niso vedno zahtevani v vašem zaporedju, ampak odgovarjajo glede na njihovo hitrost. + "Besedila skladb se ujamejo po naslovu in izvajalcu; nenavadni metapodatki lahko ne najdejo ničesar ali napačno verzijo. Za menjanje verzij uporabite osveževanje. Ponudniki, označeni z *, zahtevajo žeton. Vsi omogočeni ponudniki niso vedno zahtevani po vrsti, ampak odgovarjajo glede na njihovo hitrost. -Besedila izhajajo iz storitev tretjih oseb, ki niso povezane s podjetjem Morphe, jih ne sponzorirajo ali odobravajo. Vsaka se uporablja takšna, kakršna je: besedila, njihova razpoložljivost in licenciranje ostajajo pri teh storitvah in njihovih dajalcih licenc ter je lahko uporaba teh besedil predmet njihovih pogojev storitve. Do omogočene storitve pride le iskana informacija - naslov, izvajalec in, kjer ta storitev to potrebuje, ID videoposnetka. Opcijski AI konec in transkripcija QuickLRC tečeta šele po vaši konfiguraciji in prejmeta besedilo besedil ali URL videoposnetka skladbe." +Besedila prihajajo iz storitev tretjih oseb, ki niso povezane s podjetjem Morphe, ga ne podpirajo ali odobravajo. Vsaka se uporabi takšna, kakršna je: besedila, njihova razpoložljivost in licenciranje ostajajo pri tistih storitvah in njihovih licencirnikih, njihova uporaba pa je lahko predmet pogojev storitve. Tisto, kar doseže omogočeno storitev, je le iskalni klic, ki ste ga zahtevali - naslov, izvajalec in, kjer storitev to potrebuje, ID videa. Opcijski zaključek AI in transkripcija QuickLRC potekata šele po vaši konfiguraciji in prejmemo besedilo pesmi ali URL videa sledi." Vklopite le ponudnike, ki jih uporabljate, ali pustite privzete - stalne zahteve lahko obremenijo majhna spletna mesta s skupnostnimi besedili Skrijte vrstico z avtorstvom Skrijte vrstico z avtorstvom, prikazano pod besedilom. Zamenjajte prevod in romanizacijo. - Ko je vklopljeno, se romanizacija prikaže pod besedilom, prevod pa zgoraj; ko je izklopljeno, ostane romanizacija zgoraj, prevod pa spodaj - Prevod - Romanizacija - Ponudnik prevodov - Google (privzeto, brezplačno) - Združljivo z OpenAI (po meri) - Ponudnik romanizacije - Google (privzeto) - Lokalni / offline slovar - Združljivo z OpenAI (po meri) - AI razdelek - Uporabljeno samo, ko ponudnik ne pošlje oznak razdelkov, kot so Refren ali Kit, ki jih nato zazna vaš model, združljiv z OpenAI - AI & končne točke v oblaku - Konfigurirajte API ključe, modele, končne točke in predloge promptov za ponudnike umetne inteligence. - URL končne točke API (npr. https://api.openai.com/v1/chat/completions) + VKLOP postavi romanizacijo ali transliteracijo pod besedila in prevod nad; IZKLOP ohranja romanizacijo ali transliteracijo nad in prevod pod. Način branja, ki je postavljen nad vsako zlog, se premakne z njo, od zgornje strani sloga do spodnje + Vedno sredinski besedilo + VKLOP postavi vsako vrstico v sredino in ignorira poravnavo vira, vključno z duetskim stranmi; IZKLOP ohranja poravnavo, ki jo zahtevajo besedila + Vsaj eden ponudnik mora biti vklopljen + Prevajanje + Romanizacija ali transliteracija + Spletni slovar + Seznam besed Rime + Zaznaj dele z umetno inteligenco + Uporablja se samo, ko ponudnik ne prejme oznak razdelkov, kot so Refren ali Kitica, ki jih nato zazna vaš model združljiv z OpenAI-jem. Urejanje vrstice odpre tudi njen prompt + URL končne točke API (npr. https://api.openai.com/v1/chat/completions, https://api.anthropic.com/v1/chat/completions, https://generativelanguage.googleapis.com/v1beta/openai/chat/completions) Predloga za poziv Zastopniki: {language}, {task}, {title}, {artist}, {count}, {lines} Poziv shranjen Poziv je shranjen, vendar končna točka AI še ni uporabna. Najprejo jo konfigurirajte pod Nastavitve AI Poziv ponastavljen - Ni veljavno JSON. Popravite ga ali počistite polje pred shranjevanjem Nastavitve prevoda & romanizacije so shranjene Nastavitve so ponastavljene na privzete Osnovni URL se mora začeti z http:// ali https:// - Preveri povezavo - Konfiguriraj končne točke - Prikaži vse končne točke + Nastavitve nepopolne ali zavrnjene s strani ponudnika Preverjanje povezave… - Ponudnik prevoda - Ponudnik romanizacije - Pridobi modele + %1$s: Povezan! (%2$s) + %1$s: Povezava ni uspela. Preverite nastavitve. + Najprej vnesite ključ API Pridobivanje modelov… Izberite model Ni uspelo pridobiti modeljev. Preverite API ključ ali povezavo. - Nastavitve Gemini API - OpenAI kompatibilno (lastna končna točka) - Nastavitve DeepL API - API ključ + Poziv Model Osnovni URL + Izberite storitev API ključ (opcijonalno) - Navodilo za prevod Dodatno telo zahteve (opcijonalni JSON) + Dodatni zahtevani glavi (opcijsko JSON) + Citatno imena in vrednosti, npr. {\"api-key\": \"abc\"}. Neizpisana vrednost, ki vsebuje / : ali številko, je zavrnjena. Prazna vrednost odstrani glavo. + Citatno imena in vrednosti, npr. {\"temperature\": 0.3}. Neizpisana vrednost, ki vsebuje / ali :, je zavrnjena. Model in sporočila tu ne moreta biti nastavljena. Ključ za avtentikacijo (Brezplačni ali Pro) - Konfigurirajte API ključe, modele, končne točke in navodila 8.23.51 - Onemogoči krepke ikone 9.13.51 - Obnovi zavihke predvajalnika: Naslednje, Besedila in Povezano diff --git a/patches/src/main/resources/addresources/values-sl-rSI/youtube/strings.xml b/patches/src/main/resources/addresources/values-sl-rSI/youtube/strings.xml index 765424a580..94d09d6d94 100644 --- a/patches/src/main/resources/addresources/values-sl-rSI/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-sl-rSI/youtube/strings.xml @@ -29,6 +29,8 @@ Second \"item\" text" Razno Video Povrni stare nastavitve zaslona YouTube + Prikaži sporočila v YouTube snackbaru + Sporočila Morphe, kot je potrditev glosa igralca, se prikažejo v YouTube snackbaru namesto v pojavnem oknu Odstrani omejitve predvajanja v ozadju Onemogoči predvajanje v ozadju diff --git a/patches/src/main/resources/addresources/values-sr-rCS/music/strings.xml b/patches/src/main/resources/addresources/values-sr-rCS/music/strings.xml index 891447f349..150bc911e2 100644 --- a/patches/src/main/resources/addresources/values-sr-rCS/music/strings.xml +++ b/patches/src/main/resources/addresources/values-sr-rCS/music/strings.xml @@ -462,12 +462,47 @@ Da biste obezbedili sinhronizaciju scrobbling-a tokom reprodukcije u pozadini, u Prikaz Sinhronizacija Panel - Krajnja tačka veštačke inteligencije Svi izvori onemogućeni - ugrađene stihove i ugnežđene stihove Poništi redosled izvora? Vrati izvore na podrazumevani redosled prioriteta. + Rime lista reči + "Rime Wordlist takođe zahteva UniDict: u Offline Dictionary provideru, označite \"Kineski i kantonski karakteri (UniDict)\" i sačekajte da se preuzme. Bez njega, lista koja čita karaktere ne daje nijedan priručnik. + +Označite liste koje Rime Wordlist treba da čita, a zatim dodirnite Sačuvaj da biste ih preuzeli. Lista koju ste dodali uklanja se kada je isključite. + +Svaka lista je vaša tabela reči-priručnika, .dict.yaml sa linijama poput: +字 ham +典 din2 +Prva kolona je reč, druga je njen priručnik; dodatne kolone i linije bez tabulatora se ignorišu." + Ni jedna Rime lista reči nije štiklirana za gornji jezik. Dodirnite ovo ime da biste štiklirali jednu ili izaberite jezik koji ima listu + Dodaj + Dodaj Rime listu reči + .dict.yaml fajl, ili zip koji sadrži jedan, sa linijama poput 字 ham и 典 din2. Lista se dohvaća kada je označite i dodirnete Sačuvaj, a njena licenca je vaša da proverite + Oznaka jezika + Dve ili tri slova, kao što su hak ili wuu + Ime + Prikazuje se ispod čitanja, kao što je Hakka (Meixian) + Adresa liste reči + https:// adresa .dict.yaml ili njegovog zip-a + Karakteri van reči + Automatski + Kantonski karakteri + Mandarin karakteri + Nema čitanja karaktera + Popunite oznaku, ime i adresu + Oznaka mora biti dve ili tri slova, kao što je hak + Adresa mora početi sa https:// + Ime i adresa ne mogu sadržati vertikalnu crtu + %1$s već ima tabelu reči + %1$s se čita ugrađenim romanizatorom i ne može koristiti listu reči + Ne koristi se: ovaj jezik već ima svoje čitanje, pa ga odštikutajte da biste ga uklonili + Dodato %1$s - štiklirajte ga i dodirnite Sačuvaj da biste ga preuzeli + Liste reči spremne + Liste reči spremne (%1$d unos/a) + Izgradnja liste za čitanje: %1$d%% + Preuzimanje liste reči nije uspelo - dodirnite Sačuvaj ponovo da pokušate ponovo Obriši sačuvane tekstove - Obriši tekstove koji su sačuvani na ovom uređaju + Obrišite %1$s MB tekstova sačuvanih na ovom uređaju Očišćeno %1$s MB Dobij token. Apple Music token medijskog korisnika @@ -518,14 +553,22 @@ Potrebni kolačići: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPI Prikaži dugme za kopiranje Dodirnite da kopirate originalne tekstove kao običan tekst; dugo pritisnite da sačuvate sirovi izvor teksta u Preuzimanja Prikaži dugme za prevođenje - Dodirnite da uključite/isključite prevod; dugo pritisnite samo za prevod. Koristi prevode provajdera, Google Prevodilac, ili OpenAI kompatibilan API - Prikaži dugme za romanizaciju. - Dodirnite da uključite/isključuje transkripciju; dugo pritisnite samo za transkripciju. Korazi transkripciju provajdera, Google Prevodilac, ili OpenAI kompatibilan API - Lokalna transkripcija i usklađivanje - Transkribuj na uređaju kada provajder, vaš model i Google Prevodilac zakažu. Čitanje prati svaku reč vremenski usklađenog teksta; sinhronizacija reči mora biti uključena. Kineski, kantonski, japanski, korejski, grčki, ruski, gruzijski, armenski; oko 120 MB - Čitanje %1$s: %2$d%% + Dodirnite za prebacivanje prevoda; dugi pritisak samo za prevod. Koristi omogućene provajdere prevoda u njihovom navedenom redosledu + Prikaži dugme za romanizaciju ili transliteraciju + Dodirnite za prebacivanje romanizacije ili transliteracije; dugi pritisak samo za romanizaciju ili transliteraciju. Koristi omogućene provajdere romanizacije u njihovom navedenom redosledu + Omogući transliteraciju + Dozvoli necirilica čitanja iz izvora tekstova; kada je isključeno, zadržava samo latinica slova, akcentovana slova i tonove cifre + Čita svaku reč vremenski sinhronizovanog teksta (sinhronizacija reči uključena); Korejski ne zahteva preuzimanje; četrnaest pisama dele jedno CLDR preuzimanje; Kineski karakteri se čitaju pomoću Rime liste reči kada njena tabela reči pokriva jezik + Preuzimanje rečnika za čitanje… Čitanja spremna (%1$d unosa) Preuzimanje nije uspelo - isključite pa ponovo uključite kako biste pokušali ponovo + Offline rečnik + Japanski (SudachiDict, ~75 MB) + Kineski i kantonski karakteri (UniDict, ~8 MB) + Kineske reči (pinyin-pro, ~17 MB) + Grčki, slovenski ćirilica (uključujući ruski, ukrajinski, bugarski, srpski, beloruski, makedonski), gruzijski, armenski, hebrejski, arapski, amharski i sirijski transliteracija (CLDR, ~0.5 MB) + Čitanje %1$s: %2$d%% + Izaberite barem jedan rečnik Nije moguće romanizovati pesmu. Dodirnite da prebacite na sledećeg kandidata za tekst; dugo pritisnite da pretražite tekstove po naslovu pesme i izvođača Pretraga tekstova pesama @@ -536,8 +579,10 @@ Potrebni kolačići: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPI Tekst se traži po naslovu numere i izvođaču, […] Možete i da pritisnete dugme za osvežavanje da biste ručno promenili verzije. […] Svi omogućeni izvori se ne traže uvek po vašem redosledu, već odgovaraju prema svojoj brzini. Istakni sinhronizovane reči ili slogove. Daj prednost tekstu reč po reč kada je dostupan. + Skupi neaktivne redove + Crtanje svakog reda koji se trenutno ne peva nešto manjim, iznad bleđenja Prikaži pozadinske vokale posebno - Premješta pozadinski vokal zapisan u zagradama, poput \"(Oh) želim te\", iz pevane linije u zasebnu, prigušeniju liniju + Razdvaja pozadinske vokale i harmoniju od vodećeg reda. Premesta pozadinski vokal napisan u zagradama, kao što je „(Oh) želim te“, iz pevanog reda u sopstveni bliži red Sakrij odsvirane stihove. Sakrij stihove iznad reda koji se trenutno pušta. Sakrij neodsvirane stihove. @@ -595,10 +640,15 @@ Primenjuje se na oba limita. Na 1.75, četiri karaktera pune širine računaju s Preveo %s Romanizovao %s Romanizovao %s + Romanizovano od strane Rime (%s) Prevedeno Jezik prevoda - Jezik romanizacije + Jezik romanizacije ili transliteracije + "Ciljni jezik prevoda: +%s" + "Ciljni jezik romanizacije ili transkripcije: +%s" Prevođenje… Nije bilo moguće prevesti tekst Romanizovano @@ -612,7 +662,7 @@ Primenjuje se na oba limita. Na 1.75, četiri karaktera pune širine računaju s Osvežavanje Osveženo Ključ API-ja QuickLRC - QuickLRC transkribuje ili vremenski poravnava stihove za trenutnu pesmu. Poseban servis, bez veze sa gore navedenim OpenAI-kompatibilnim krajnjom tačkom + QuickLRC transkribuje ili vremenski poravnaje tekstove za trenutni track QuickLRC ključ je sačuvan. Koristite dugme za poravnavanje u dijalogu pretrage stihova ili poziv panela kada pesma nema stihove QuickLRC API ključ sačuvan QuickLRC nije prihvatio taj ključ @@ -628,58 +678,50 @@ Primenjuje se na oba limita. Na 1.75, četiri karaktera pune širine računaju s Nema stihova za poravnavanje - umesto toga transkribujemo QuickLRC nije mogao obraditi ovu numeru QuickLRC krediti su potrošeni za ovu numeru - "Tekstovi se uparuju po naslovu i izvođaču, neuobičajeni metapodaci mogu rezultirati time da ništa ne bude pronađeno ili da se dođe do pogrešne verzije. Koristite osvežavanje za prebacivanje verzija. Provajderi označeni * zahtevaju token. Nisu svi omogućeni provajderi uvek pozvani po redosledu, već odgovaraju na osnovu brzine. + "Tekstovi pesama se uparuju po naslovu i izvođaču, neuobičajeni metapodaci mogu ništa ne pronaći ili pogrešnu verziju. Koristite osvežavanje za prelazak na druge verzije. Provideri označeni * zahtevaju token. Svi omogućeni provideri nisu uvek traženi redom, već odgovaraju na osnovu svoje brzine. -Tekstovi dolaze od usluga trećih strana koje nisu afilijirane, sponzorisanе ili podržane od strane Morphe-a. Svaka se koristi takva kakva je: tekstovi, njihova dostupnost i licenciranje ostaju kod tih usluga i njihovih davalača licenci, a korišćenje može biti predmet njihovih uslova pružanja usluga. Što stigne do omogućene usluge je samo upit koji ste zatražili - naslov, izvođač, i, gde ta služba to treba, ID videa. Opcioni AI endpoint i QuickLRC transkripcija rade samo nakon što ih konfigurišete, i prime tekst pesme ili video URL tračke." +Tekstovi pesama dolaze od usluga trećih strana koje nisu povezane s Morphe-om, niti ga sponzorišu ili podržavaju. Svaka se koristi onako kako jeste: tekstovi pesama, njihova dostupnost i licenciranje ostaju kod tih usluga i njihovih davatelja dozvole, a njihovo korišćenje može biti podložno njihovim uslovima korišćenja. Ono što stigne do omogućene usluge je samo pretraga koju ste tražili - naslov, izvođač i, gde ta usluga to zahteva, video ID. Opciona AI tačka pristupa i QuickLRC transkripcija rade tek nakon što ih konfigurirate i primaju tekst pesme ili video URL trake." Uključite samo pružače koje koristite ili zadržite podrazumevano - stalni zahtevi mogu preopteretiti male zajedničke stranice sa tekstovima Sakrij red s informacijama Sakrij red s informacijama prikazan ispod stihova. Promeni redosled prikaza izvođača i naslova - Promeni redosled prikaza izvođača i naslova - Prevod - Romanizacija - Provajder prevoda - Google (podrazumevano, besplatno) - OpenAI kompatibilno (prilagođeno) - Provajder romanizacije - Google (podrazumevano) - Lokalni / oflajn rečnik - OpenAI kompatibilno (prilagođeno) - AI sekcija - Koristi se samo kada provajder ne šalje oznake sekcija kao što su Refren ili Strofa, koje zatim detektuje vaš OpenAI-kompatibilni model - AI & Cloud krajnje tačke - Konfigurišite API ključeve, modele, krajnje tačke i šablone upita za AI provajdere. - URL API krajnje tačke (npr. https://api.openai.com/v1/chat/completions) + UKLJUČENO stavlja romanizaciju ili transliteraciju ispod tekstova a prevod iznad; ISKLJUČENO drži romanizaciju ili transliteraciju iznad i prevod ispod. Čitanje postavljeno preko svake sloga se seli sa njom, od iznad njenog sloga do ispod nje + Uvek centriraj tekstove + UKLJUČENO stavlja svaki red u sredinu i ignoriše poravnanje izvora, uključujući strane dueta; ISKLJUČENO zadržava poravnanje koje tekstovi traže + Barem jedan provajder mora ostati uključen + Prevod + Romanizacija ili transliteracija + Offline rečnik + Rime lista reči + Detekcija sekcija veštačkom inteligencijom + Koristi se samo kada provajder ne isporučuje etikete sekcija kao što su Refren ili Strofa, što tada detektuje vaš model kompatibilan sa OpenAI-jem. Uredjenje reda takođe otvara njegov prompt + URL API kraja (npr. https://api.openai.com/v1/chat/completions, https://api.anthropic.com/v1/chat/completions, https://generativelanguage.googleapis.com/v1beta/openai/chat/completions) Šablon uputa Rezervacije: {language}, {task}, {title}, {artist}, {count}, {lines} Uputa sačuvana Prompt je sačuvan, ali AI krajnja tačka još uvek nije upotrebljiva. Prvo je konfigurišite u podešavanjima AI-a Uputa resetovana - Nije validan JSON. Popravite ga ili očistite polje pre čuvanja Podešavanja za prevod & romanizaciju su sačuvana Podešavanja su vraćena na podrazumevane vrednosti Osnovni URL mora početi sa http:// ili https:// - Testiraj vezu - Konfiguriši krajnje tačke - Prikaži sve krajnje tačke + Podešavanja nepotpuna ili odbijena od strane provajdera Provera veze… - Provajder prevoda - Provajder romanizacije - Preuzmi modele + %1$s: Povezano! (%2$s) + %1$s: Povezivanje nije uspelo. Proverite podešavanja. + Unesite API ključ prvo Preuzimanje modela… Izaberite model Preuzimanje modela nije uspelo. Proverite API ključ ili vezu. - Gemini API podešavanja - OpenAI kompatibilno (prilagođena krajnja tačka) - DeepL API podešavanja - API ključ + Nalog Model Bazni URL + Izaberite servis API ključ (opciono) - Prompt za prevod Dodatno telo zahteva (opcioni JSON) + Dodatni zahtevi zaglavlja (opcioni JSON) + Navodite imena i vrednosti, npr. {\"api-key\": \"abc\"}. Ne-navođena vrednost koja sadrži / : ili broj se odbija. Prazna vrednost uklanja zaglavlje. + Navodite imena i vrednosti, npr. {\"temperature\": 0.3}. Ne-navođena vrednost koja sadrži / ili : se odbija. Model i poruke ne mogu se postaviti ovde. Ključ za autentifikaciju (Free ili Pro) - Konfigurišite API ključeve, modele, krajnje tačke i prompte 8.23.51 - Onemogući podebljane ikonice 9.13.51 - Vrati kartice Sledeće, Tekstovi i Povezano za plejer diff --git a/patches/src/main/resources/addresources/values-sr-rCS/youtube/strings.xml b/patches/src/main/resources/addresources/values-sr-rCS/youtube/strings.xml index 2435b6f9d7..27e68902c9 100644 --- a/patches/src/main/resources/addresources/values-sr-rCS/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-sr-rCS/youtube/strings.xml @@ -29,6 +29,8 @@ Second \"item\" text" Razno Video Vrati stare menije podešavanja + Prikaži poruke u YouTube snackbar-u + Morphe poruke, kao što je potvrda dugmeta plejera, prikazuju se u YouTube snackbar-u umesto u iskačnom prozoru Ukloni ograničenja reprodukcije u pozadini Onemogući puštanje Shorts videa u pozadini diff --git a/patches/src/main/resources/addresources/values-sr-rSP/music/strings.xml b/patches/src/main/resources/addresources/values-sr-rSP/music/strings.xml index 69629ab7dd..20aefea8ed 100644 --- a/patches/src/main/resources/addresources/values-sr-rSP/music/strings.xml +++ b/patches/src/main/resources/addresources/values-sr-rSP/music/strings.xml @@ -462,12 +462,47 @@ Da biste osigurali sinhronizaciju scrobbling-a tokom reprodukcije u pozadini, po Приказ Синхронизација Панел - AI крајња тачка Сви пружаоци су онемогућени - уграђене речи песама и уклопљене речи песама Ресетовати редослед провајдера? Врати провајдере на подразумевани редослед приоритета. + Списак речи Рима + "Rime Wordlist такође захтева UniDict: у Offline Dictionary провајдеру, означите \"Chinese and Cantonese characters (UniDict)\" и сачекајте да се преузимање заврши. Без тога, лист којa чита карактере не даје ниjедно читање. + +Означите листове које Rime Wordlist треба да чита, затим додирните Saчuvaj да бисте их преузели. Лист који сте додали уклања се када га деозначите. + +Сваки лист је ваша влаcита табела реч-читање, .dict.yaml датотека са линијама попут: +字 ham +典 din2 +Први колона је реч, друга њено читање; додатне колоне и линије без табулатора се занемарују." + Није означен ни један списак речи Рима за горе наведен језик. Притисните ово име да означите један, или изаберите језик који има списак + Додај + Додај списак речи Рима + .dict.yaml датотека, или zip који садржи такву, са линијама попут 字 ham и 典 din2. Лист се преузима када га означите и додирнете Sačuvaj, а ваша је лиценца да проверите. + Шифра језика + Две или три слова, попут hak или wuu + Име + Приказује се испод изговора, попут Hakka (Meixian) + Адреса списка речи + https:// адреса за .dict.yaml датотеку или њену zip архиву + Знакови који нису део речи + Аутоматски + Кантонски знакови + Мандарински знакови + Без изговора знака + Попуните шифру, име и адресу + Шифра мора имати две или три слова, попут hak + Адреса мора почињати са https:// + Име и адреса не могу садржати вертикалну црту + %1$s већ има таблицу речи + %1$s се чита уграђеним романизатором и не може примити списак речи + Не користи се: овај језик већ има свој изговор, па га одозначите да бисте га уклонили + Додато %1$s - означите га и притисните Сачувај да бисте га преузели + Списци речи су спремни + Списци речи су спремни (%1$d уноса) + Изградња списка изговора: %1$d%% + Преузимање списка речи није успело - притисните Сачувај поново да покушате Обриши складишене текста - Избришите текстове чуване на овом уређају + Обришите %1$s MB текста песама складиштених на овом уређају Обрисано %1$s MB Преузми токен Apple Music медијски кориснички токен @@ -522,14 +557,22 @@ Da biste osigurali sinhronizaciju scrobbling-a tokom reprodukcije u pozadini, po Прикажи дугме за копирање Додирните да копирате оригинални текст као обичан текст; дуго притисните да сачувате изворни фајл текста у Превознике Прикажи дугме за превод - Додирните да пребаци превод; дуго притисните само за превод. Користи преводе провајдера, Google Translate, или API компатибилан са OpenAI - Прикажи дугме за романизацију - Додирните да пребаци транслитерацију; дуго притисните само за транслитерацију. Користи транслитерацију провајдера, Google Translate, или API компатибилан са OpenAI - Локална транслитерација и поравнање - Транслитерише на уређају када провајдер, ваш модел и Google Translate не успеју. Читање следи сваку реч хронометра текста; синхронизација речи мора бити укључена. Кинески, Кантонски, Јапански, Корејски, Грчки, Русијски, Грузијски, Јерменски; око 120 MB - Читање %1$s: %2$d%% + Притисните да бисте укључили/искључили превод; дуго притисните само за превод. Користи омогућене провајдере преводиоца у редоследу како су наведени + Прикажи дугме за романизацију или транслитерацију + Притисните да бисте укључили/искључили романизацију или транслитерацију; дуго притисните само за романизацију или транслитерацију. Користи омогућене провајдере романизације у редоследу како су наведени + Омогући транслитерацију + Дозволи не-латиничке изворе из текста песама; када је искључено, оставља само латиничко слово, слова са акцентом и цифре тона + Чита сваку реч синхронизованог текста песама (синхронизација речи укључена); корејски не захтева преузимање; четрнаест писмености деле једно CLDR преузимање; хан знакове чита Списак речи Рима када његова таблица речи покрива тај језик + Преузимање речника изговора… Спремне читања (%1$d ставки) Преузимање није успело - искључите и поново укључите да бисте покушали поново + Офлајн речник + Јапански (SudachiDict, ~75 MB) + Кинески и кантонски знакови (UniDict, ~8 MB) + Кинеске речи (pinyin-pro, ~17 MB) + Грчки, словенски кирилични (укљ. руски, украјински, бугарски, српски, белоруски, македонски), грузијски, Јерменски, Хебрејски, Арапски, Амхарски и Сиријски транслитерација (CLDR, ~0.5 MB) + Читање %1$s: %2$d%% + Изаберите најмање један речник Прикажите дугме за освежавање Додирните да преbacи на следећег кандидата текста; дуго притисните да претражујете текст по наслову и извођачу Претрага текстова @@ -540,8 +583,10 @@ Da biste osigurali sinhronizaciju scrobbling-a tokom reprodukcije u pozadini, po Сачувано Истакни синхронизоване речи или слогове. Дај предност текстовима реч по реч када су доступни. Када текстови носе време, истакните сваку реч или слог у складу са музиком. - Прикажите пратеће вокале одвојено - Премешта вокал у позадини написан у заградама, попут \"(Ох) желим те\", из линије певања у самосталну линију са смањеном јачином звука + Скупите неактивне линије + Црта свaku линију која тренутно није певање мало мањом, уз тамњење + Прикажи позадинске вокале засебно + Раздваја позадинске вокале и хармонију од водеће линије. Премешта позадински вокал написан у заградама, попут „(Oh) I want you”, ван певане линије у сопствену тамнију линију Сакриј […] линију Сакријте редове текста изнад тренутно репродукованог реда Сакриј неодсвиране текстове песама. @@ -599,10 +644,15 @@ Da biste osigurali sinhronizaciju scrobbling-a tokom reprodukcije u pozadini, po Превод: %s Романизација: %s Романизовао/ла %s + Романизовано од стране Рима (%s) Преведено Језик превода - Језик романизације + Језик романизације или транслитерације + "Језик мета циља преводa: +%s" + "Језик мета циља риманизациje или транслитерациje: +%s" Превођење… Није било могуће превести текстове песама Романизовано @@ -616,7 +666,7 @@ Da biste osigurali sinhronizaciju scrobbling-a tokom reprodukcije u pozadini, po Освежавање Освежено. Кључ QuickLRC API-ја - QuickLRC транскрибује или време усклађује текстове песама за тренутну композицију. Двојана услуга, нериелана са горњом крајњом тачком компатибилном са OpenAI-јем + QuickLRC транскрибује или време-усклађује текст песама за тренутну песму Кључ QuickLRC је сачуван. Користите дугме за усклађивање у дијалогу за претрагу текстова или упутство на панелу када композиција нема текстове Кључ QuickLRC API-ја је сачуван QuickLRC није прихватио тај кључ @@ -632,56 +682,50 @@ Da biste osigurali sinhronizaciju scrobbling-a tokom reprodukcije u pozadini, po Нема текстова за усклађивање - уместо тога транскрибује QuickLRC није могао да обради ову нумеру Кредити за QuickLRC су потрошени за ову нумеру - "Текст се уклапа по наслову и извођачу, необични метаподаци могу ништа или погрешну верзију да пронађу. Користите освежавање да преbacite верзије. Провалаjници означени * захтевају токен. Укључени провалаjници увек не траже у вашем редоследu, али реагују према брзини.\n\nТекст долази од трећих сервиса који нису повезани, спонзорисани или одобрени од стране Morphe. Сваки се користи како јесте: текст, његова доступност и лиценцирање остају код тих сервиса и њихових лиценцара, а њихова употреба може бити подложна њиховим условима коришћења. Оно што стиже до укљученог сервиса је само претрага коju ste traжили - наслов, извођач и, где сервис то треба, ID видеа. Опциони AI endpoint и QuickLRC трансакција ради само након што их конфигуришете, и прима текст текста или URL видеа стазе." + "Песме се упарују према насловu и извођачу, необични метатаци подаци могу наћи ништа или погрешну верзију. Користите освежавање за преласак између верзија. Провајдери означени * захтевају токен. Нису сви одабрани провајдери увек позивани у вашем редоследу, већ одговарају према своjoj брзини. + +Песме долазе од услуга треће стране које нису повезане, спонзорисане или подржане од стране Morphe. Свака се користи онако како јесте: песме, њихова доступност и лиценце остају код тих услуга и њихових лиценцодатаљa, а коришћење може бити предмет услова коришћења. Оно што стиже до укључене услуге је само упит који сте затражили - наслов, извођач, и, тамо где та услуга то захтева, ID видеа. Опциони AI endpoint и QuickLRC транскрипција покренуте су само након што их конфигуришете, и примају текст песме или URL видеа траке." Укључите само пружатеље које користите, или оставите подразумевано — стални захтеви могу оптеретити мале заједничке стране за текстове Сакриј линију са заслугама Сакриј линију са заслугама приказану испод текстова. Замени превод и романизацију. - УКЉУЧЕНО поставља романизацију испод текста, а превод изнад; ИСКЉУЧЕНО задржава романизацију изнад и превод испод. - Превод - Романизација - Пружалац превода - Google (Подразмевано, бесплатно) - OpenAI компатибилно (Прилагођено) - Пружалац романизације - Google (Подразмевано) - Локални / Офлајн речник - OpenAI компатибилно (Прилагођено) - AI секција - Користи се само када провајдер не испоручује ознаке секција као што су Рефрен или Строфа, које затим детектује ваш OpenAI-компатибилни модел - AI & Cloud krajnje tačke - Конфигуришите API кључеве, моделе, krajnje tačke и шаблоне упита за AI пружаоце. - API endpoint URL (нпр. https://api.openai.com/v1/chat/completions) + УКЉУЧЕНО ставља романизацију или транслитерацију испод текста песама, а превод изнад; ИСКЉУЧЕНО задржава романизацију или транслитерацију изнад, а превод испод. Изговор постављен над сваком слогом креће се с њим, од изнад до испод слога + Увек центрирај текст песама + УКЉУЧЕНО ставља сваку линију у сред и игнорише поравнање извора, укљ. стране дуэта; ИСКЉУЧЕНО задржава поравнање које текст песама тражи + Мора остати укључен најмање један провајдер + Превод + Романизација или транслитерација + Офлајн речник + Списак речи Рима + Откриј секције помоћу AI + Користи се само када провајдер не испоручује ознаке секција као што су Припеви или Стихови, које онда открива ваш модел компатибилан са OpenAI. Уређивање реда отвара и његов позив + URL крајње тачке API-ја (нпр. https://api.openai.com/v1/chat/completions, https://api.anthropic.com/v1/chat/completions, https://generativelanguage.googleapis.com/v1beta/openai/chat/completions) Шаблон промпта Заменичи: {language}, {task}, {title}, {artist}, {count}, {lines} Промпт је сачуван Упутство је сачувано, али AI крајња тачка још увек није доступна. Подесите је прво у подешавањима AI-ја Промпт је ресетован - Није валидан JSON. Поправите га или испразните поље пре чувања Подешавања за превод & романизацију су сачувана Подешавања су враћена на подразумеване вредности Base URL мора почети са http:// или https:// - Тестирај повезивање - Конфигуриши крајње тачке - Прикажи све крајње тачке + Подешавања су непотпуна или одбијена од стране провајдера Проверавање везе… - Пружалац превода - Пружалац романизације - Преузми моделе + %1$s: Повезано! (%2$s) + %1$s: Веза није успостављена. Проверите подешавања. + Унесите API кључ прво Преузимање модела… Изаберите модел Неуспешно преузимање модела. Проверите API кључ или повезивање. - Gemini API подешавања - OpenAI компатибилно (прилагођена krajnja tačka) - DeepL API подешавања - API кључ + Позив Модел Базни URL + Изаберите услугу API кључ (опционо) - Упутство за превод Додатно тело захтева (опционо JSON) + Додатни заглавља захтева (опционални JSON) + Наведите имена и вредности у наводницима, нпр. {\"api-key\": \"abc\"}. Вредност без наводника која садржи / : или број је одбачена. Празна вредност уклања заглавље. + Наведите имена и вредности у наводницима, нпр. {\"temperature\": 0.3}. Вредност без наводника која садржи / или : је одбачена. Модел и поруке се не могу поставити овде. Кључ за аутентификацију (Free или Pro) - Конфигуришите API кључеве, моделе, крајње тачке и упутства 8.23.51 - Онемогући подебљане иконе 9.13.51 — Врати картице „Следеће”, „Текстови” и „Повезано” у плејеру diff --git a/patches/src/main/resources/addresources/values-sr-rSP/youtube/strings.xml b/patches/src/main/resources/addresources/values-sr-rSP/youtube/strings.xml index a4f6961909..c2274e9b93 100644 --- a/patches/src/main/resources/addresources/values-sr-rSP/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-sr-rSP/youtube/strings.xml @@ -29,6 +29,8 @@ Second \"item\" text" Разно Видео Врати старе меније подешавања + Прикажи поруке у YouTube SnackBar-у + Morphe поруке, као што је потврда дугмета за репродукцију, приказују се у YouTube SnackBar-у уместо у искачућем прозору Уклоните ограничења за репродукцију у позадини Онемогући пуштање Shorts видеа у позадини diff --git a/patches/src/main/resources/addresources/values-sv-rSE/music/strings.xml b/patches/src/main/resources/addresources/values-sv-rSE/music/strings.xml index 69213f246e..b62c41f05a 100644 --- a/patches/src/main/resources/addresources/values-sv-rSE/music/strings.xml +++ b/patches/src/main/resources/addresources/values-sv-rSE/music/strings.xml @@ -462,12 +462,47 @@ För att säkerställa att scrobbling synkroniseras under bakgrundsuppspelning, Visning Synkronisering Panel - AI-slutpunkt Alla leverantörer inaktiverade - inbyggd text och inbäddad text Återställ leverantörsordningen? Återställ leverantörerna till standardprioriteringsordningen. + Rime-ordlista + "Rime Wordlist kräver också UniDict: markera \"Kinesiska och kantonesiska tecken (UniDict)\" i offline-ordbokspartiet och låt det vara klart att ladda ner först. Utan det ger en lista som läser in läsföring inget läsförande. + +Markera de listor som Rime Wordlist ska läsa in, tryck sedan Spara för att ladda ner dem. En lista du lagt till tas bort när du avmarkerar den. + +Varje lista är ditt eget ord-till-läsning-tabell, en .dict.yaml med rader som: +字 ham +典 din2 +Den första kolumnen är ordet, den andra dess läsning; extra kolumner och rader utan tabulering ignoreras." + Ingen Rime-ordlista är markerad för språket ovan. Tryck på detta namn för att markera en, eller välj ett språk som har en lista + Lägg till + Lägg till en Rime-ordlista + En .dict.yaml-fil, eller en zip med en sådan innehållande rader som 字 ham och 典 din2. Listan hämtas när du markerar den och trycker Spara, och licensen äger du att kontrollera + Språkkod + Två eller tre bokstäver, som t.ex. hak eller wuu + Namn + Visas under uttalet, t.ex. Hakka (Meixian) + Adress till ordlista + https://-adress för .dict.yaml eller dess zip-fil + Tecken inte ingående i ord + Automatiskt + Kantonesiska tecken + Mandarintecken + Inget teckenutlägg + Fyll i kod, namn och adress + Koden måste bestå av två eller tre bokstäver, t.ex. hak + Adressen måste börja med https:// + Namnet och adressen får inte innehålla en vertikal streck + %1$s har redan en ordtabell + %1$s läses upp av den inbyggda romaniseringstjänsten och kan inte ta emot en ordlista + Ej använd: detta språk har redan sitt eget utlägg, så avmarkera det för att ta bort det + Tillagd %1$s - markera den och tryck på Spara för att ladda ner den + Ordlistor redo + Ordlister redo (%1$d poster) + Bygger ordlista för utlägg: %1$d%% + Nedladdning av ordlista misslyckades - tryck på Spara igen för att försöka igen Rensa lagrad text - Ta bort texterna som finns på den här enheten + Ta bort %1$s MB texter som finns kvar på denna enhet Rensade %1$s MB Hämta token Apple Music Media-användartoken @@ -522,14 +557,22 @@ Obligatoriska cookies: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PA Visa knappen Kopiera Tryck för att kopiera originaltexten som vanlig text; långtryck för att spara råa textkällfilen till Nedladdningar Visa knappen Översätt - Tryck för att växla översättning; långtryck för endast översättning. Användar provider-översättningar, Google Översättning eller en OpenAI-kompatibel API - Visa romaniseringsknapp - Tryck för att växla romanisering; långtryck för endast romanisering. Användar provider-romanisering, Google Översättning eller en OpenAI-kompatibel API - Lokal Romanisering och Alignment - Romanisera på enheten när providern, din modell och Google Översättning misslyckas. Läsningen följer varje ord i en tidsstämmd text; ordsynkronisering måste vara på. Kinesiska, kantonesiska, japanska, koreanska, grekiska, ryska, georgiska, armeniska; cirka 120 MB - Läsning %1$s: %2$d%% + Tryck för att växla översättning; långtryck för endast översättning. Använder de aktiverade översättningstjänsterna i deras angivna ordning + Visa knappen för romanisering eller translitterering + Tryck för att växla romanisering eller translitterering; långtryck för endast romanisering eller translitterering. Använder de aktiverade romaniseringstjänsterna i deras angivna ordning + Aktivera translitterering + Tillåt icke-latinska utlägg från lyrics-källan; när avaktiverad behåll endast latinska bokstäver, bokstäver med diakritiska tecken och tonbitar + Läser upp varje ord i en synkroniserad text (ordsynkronisering på); koreanska behöver ingen nedladdning; fjorton skript delar en CLDR-nedladdning; kinesiska tecken läses upp av Rime-ordlistan när dess ordtabell täcker språket + Laddar ner utläggsordböcker… Läsningsredskap redo (%1$d poster) Nedladdning misslyckades - stäng av och på igen för att försöka igen + Offline Dictionary + Japanska (SudachiDict, ~75 MB) + Kinesiska och kantonesiska tecken (UniDict, ~8 MB) + Kinesiska ord (pinyin-pro, ~17 MB) + Grekiska, slaviska kyrilliska (inklusive ryska, ukrainska, bulgariska, serbiska, belarusiska, makedonska), georgiska, armeniska, hebreiska, arabiska, amhariska och syrisk translitterering (CLDR, ~0,5 MB) + Läsning %1$s: %2$d%% + Välj minst en ordbok Visa uppdatera-knappen Tryck för att växla till nästa textkandidat; långtryck för att söka text efter låttitel och artist Sök sångtext @@ -540,8 +583,10 @@ Obligatoriska cookies: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PA Sparad Markera synkroniserade ord eller stavelser Prioritera ord-för-ord-text när det finns. När texten har ordtajming markeras varje ord eller stavelse i takt med musiken. - Visa bakgrundsvokaler separat - Flyttar en bakgrundsvokal som skrivits inom parenteser, till exempel \"(Åh) jag vill ha dig\", från sånglinjen till sin egen dämpade rad + Dra ihop inaktiva rader + Ritar varje rad som inte sjungs just nu något mindre, ovanpå att dämpa den + Visa bakgrundssång separat + Separerar bakgrundssång och harmoni från huvudraden. Flyttar en bakgrundssång skriven inom klammerparenteser, t.ex. \"(Oh) I want you\", från sjunglinjen till en egen, mer dämpad rad Dölj […] raden. Dölj textrader ovanför den raden som spelas för tillfället Dölj ospelade texter. @@ -599,10 +644,15 @@ Gäller båda gränserna. Vid 1,75 räknas fyra helbreddstecken som 7." Översatt av %s Romaniserad av %s Romaniserat av %s + Romaniserad av Rime (%s) Översatt Översättningsspråk - Romaniseringsspråk + Språk för romanisering eller translitterering + "Målspråk för översättning: +%s" + "Målspråk för transkription eller romanisering: +%s" Översätter… Kunde inte översätta sångtexten Romaniserad @@ -616,7 +666,7 @@ Gäller båda gränserna. Vid 1,75 räknas fyra helbreddstecken som 7." Uppdaterar Uppdaterad QuickLRC API-nyckel - QuickLRC transkriberar eller tidssynkroniserar texterna till den aktuella låten. En separat tjänst, inte relaterad till OpenAI-kompatibla slutpunkten ovan + QuickLRC transkriberar eller tidsmärker texterna för det aktuella spåret QuickLRC-nyckel sparad. Använd synkroniseringsknappen i dialogrutan för lyrics-sökning, eller panelinstruktionen när en låt saknar texter QuickLRC API-nyckel sparad QuickLRC accepterade inte den nyckeln @@ -632,58 +682,50 @@ Gäller båda gränserna. Vid 1,75 räknas fyra helbreddstecken som 7." Inga texter att synkronisera - transkriberar istället QuickLRC kunde inte bearbeta detta spår QuickLRC-kreditpoäng är upbrukade för detta spår - "Lyriken matchas efter titel och artist. Onormal metadata kan ge inget resultat eller fel version. Använd uppdatering för att växla versioner. Leverantörer markerade * kräver en token. Alla aktiva leverantörer begås inte alltid i angiven ordning, men svarar baserat på sin hastighet. + "Låttexter matchas via titel och artist; onormal metadata kan inte hitta något eller fel version. Använd Uppfräschning för att växla mellan versioner. Markörer märkta * kräver en token. Alla aktiva leverantörer begärs inte alltid i din ordning, men svarar baserat på hastighet. -Texterna kommer från tredjepartstjänster som inte är anslutna till, sponsrade eller godkända av Morphe. Varje tjänst används \"som den är\": texterna, deras tillgänglighet och licensiering stannar hos dessa tjänster och deras licensgivare, och användningen kan vara föremål för deras villkor. Vad som skickas till en aktiverad tjänst är endast sökningen du begärde – titeln, artisten och, där denna tjänst behöver det, videoid:n. Det valfria AI-slutpunktsanropet och QuickLRC-transkriptionen körs endast efter att du har konfigurerat dem, och de tar emot texten eller spårets video-URL." +Låttexter kommer från tredjeparts tjänster som inte är anslutna till, sponsrade av eller rekommenderade av Morphe. Varje används precis som det är: texterna, deras tillgänglighet och deras licensiering stannar kvar hos dessa tjänster och deras licensgivare, och användningen kan vara föremål för deras användarvillkor. Det som når en aktiv tjänst är bara sökningen du begärde - titeln, artisten, och där det behövs, videoiden. Den valfria AI-slutpunkten och QuickLRC-transkriptionen körs endast efter du konfigurerat dem, och får mottagandet av texten eller videolänken till spår." Aktivera endast de leverantörer du använder, eller behåll standardinställningen - konstanta förfrågningar kan överbelasta små community-sidor för texter Dölj kreditraden Dölj kreditraden som visas under texten. Växla mellan översättning och romanisering - PÅ placerar romaniseringen under texten och översättningen ovanför; AV behåller romaniseringen ovanför och översättningen nedanför. - Översättning - Romanisering - Översättningsleverantör - Google (standard, gratis) - OpenAI-kompatibel (anpassad) - Leverantör för romanisering - Google (standard) - Lokal / offline-ordbok - OpenAI-kompatibel (anpassad) - AI-sektion - Används endast när leverantören inte skickar med sektionsetiketter som Chorus eller Verse, vilket din OpenAI-kompatibla modell sedan detekterar - AI & molnslutpunkter - Konfigurera API-nycklar, modeller, slutpunkter och promptmallar för AI-leverantörer. - API-slutpunkts-URL (t.ex. https://api.openai.com/v1/chat/completions) + PÅ placerar romanisering eller translitterering under texterna och översättningen ovanpå; AV håller romanisering eller translitterering ovanpå och översättning underst. Ett utlägg placerat över varje stavelse flyttas med den, från ovanför sin stavelse till underst den + Centrera alltid texterna + PÅ centrerar varje rad och ignorerar källans aligment, inklusive duett-sidor; AV behåller det aligment texterna ber om + Minst en tjänst måste vara påslagen + Översättning + Romanisering eller translitterering + Offline Dictionary + Rime-ordlista + Kontrollera sektioner med AI + Används endast när tjänsten inte levererar sektionsetiketter som Topp eller Vers, vilket din OpenAI-kompatibla modell då upptäcker. Att redigera raden öppnar också dess prompt + API-slutpunkts-URL (t.ex. https://api.openai.com/v1/chat/completions, https://api.anthropic.com/v1/chat/completions, https://generativelanguage.googleapis.com/v1beta/openai/chat/completions) Mall för prompt Platshållare: {language}, {task}, {title}, {artist}, {count}, {lines} Prompt sparad Prompt sparad, men AI-slutpunkten är fortfarande inte användbar. Konfigurera den under AI-inställningar först Prompt återställd - Inte giltig JSON. Rätta det eller rensa fältet innan du sparar Inställningar för översättning & romanisering sparades Inställningarna återställdes till standard Bas-URL måste börja med http:// eller https:// - Testa anslutning - Konfigurera slutpunkter - Visa alla slutpunkter + Inställningarna är ofullständiga eller har förkastats av tjänsten Validerar anslutning… - Översättningsleverantör - Leverantör för romanisering - Hämta modeller + %1$s: Ansluten! (%2$s) + %1$s: Anslutning misslyckades. Kontrollera inställningarna. + Ange API-nyckeln först Hämtar modeller… Välj en modell Det gick inte att hämta modeller. Kontrollera API-nyckeln eller anslutningen. - Inställningar för Gemini API - OpenAI-kompatibel (egen slutpunkt) - Inställningar för DeepL API - API-nyckel + Prompt Modell Bas-URL + Välj en tjänst API-nyckel (valfri) - Översättningsprompt Extra anförandekropp (valfri JSON) + Extra begärande headers (valfri JSON) + Citera namn och värden, t.ex. {\"api-key\": \"abc\"}. Ett ociterat värde som innehåller / : eller ett nummer förkastas. Ett tomt värde tar bort en header. + Citera namn och värden, t.ex. {\"temperature\": 0.3}. Ett ociterat värde som innehåller / eller : förkastas. Modell och meddelanden kan inte ställas in här. Autentiseringsnyckel (Free eller Pro) - Konfigurera API-nycklar, modeller, slutpunkter och prompter 8.23.51 – Inaktivera ikoner i fet stil 9.13.51 - Återställ spelarens flikar Upp nästa, Sångtext och Relaterade diff --git a/patches/src/main/resources/addresources/values-sv-rSE/youtube/strings.xml b/patches/src/main/resources/addresources/values-sv-rSE/youtube/strings.xml index eb1b95b344..831119bddb 100644 --- a/patches/src/main/resources/addresources/values-sv-rSE/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-sv-rSE/youtube/strings.xml @@ -29,6 +29,8 @@ Second \"item\" text" Övrigt Video Återställ gamla YouTube-inställningsskärmen + Visa meddelanden i YouTube-snackbar + Morphe-meddelanden, såsom bekräftelse av en spelarknapp, visas i YouTube-snackbar istället för ett popupfönster Ta bort begränsningar för uppspelning i bakgrunden Inaktivera uppspelning i bakgrunden diff --git a/patches/src/main/resources/addresources/values-sw-rKE/music/strings.xml b/patches/src/main/resources/addresources/values-sw-rKE/music/strings.xml index 857c173125..fd58b241ef 100644 --- a/patches/src/main/resources/addresources/values-sw-rKE/music/strings.xml +++ b/patches/src/main/resources/addresources/values-sw-rKE/music/strings.xml @@ -461,12 +461,10 @@ Ili kuhakikisha scrobbling inasawazishwa wakati wa uchezaji wa nyuma, hakikisha Onyesha Usawazishaji Jopo - Kituo cha AI Watoa huduma wote wamezimwa - mashairi yaliyojengwa ndani na mashairi yaliyojumuishwa Weka upya mpangilio wa watoa huduma? Rudisha watoa huduma katika mpangilio wa kipaumbele wa asili Futa mashairi yaliyohifadhiwa - Futa mashairi yaliyohifadhiwa kwenye kifaa hiki Imefuta %1$s MB Pata tokeni Token ya Mtumiaji wa Media ya Apple Music @@ -526,14 +524,9 @@ Kuki zinazohitajika: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPI Onyesha kitufe cha Kunakili Gusa ili kunakili mashairi ya awali kama maandishi ya kawaida; bonyeza kwa muda mrefu ili kuhifadhi faili ya chanzo cha mashairi ghafi kwenye Pakua Onyesha kitufe cha Kutafsiri - Gusa ili kubadilisha tafsiri; bonyeza kwa muda mrefu kwa ajili ya tafsiri pekee. Hutumia tafsiri za mtoa huduma, Google Translate, au API inayooana na OpenAI - Onyesha kitufe cha Romanize - Gusa ili kubadilisha uromanizaji; bonyeza kwa muda mrefu kwa ajili ya uromanizaji pekee. Hutumia uromanizaji wa mtoa huduma, Google Translate, au API inayooana na OpenAI - Uromanizaji na Ulinganifu wa Ndani - Badili kuwa herufi za Kilatini kwenye kifaa wakati mtoa huduma, modeli yako na Google Translate vinaposhindwa. Usomaji hufuata kila neno la mashairi yenye muda; usawazishaji wa maneno lazima uwe umewashwa. Kichina, Kikantoni, Kijapani, Kikorea, Kigiriki, Kirusi, Kijorgia, Kiarmenia; takriban 120 MB - Inasoma %1$s: %2$d%% Usomaji uko tayari (%1$d entries) Pakua imefeli - zima na uwashe tena ili kujaribu tena + Inasoma %1$s: %2$d%% Onyesha kitufe cha Refresh Gusa ili kubadilisha kwenda kwa mshindani mwingine wa mashairi; bonyeza kwa muda mrefu ili kutafuta mashairi kwa kichwa cha wimbo na msanii Tafuta mashairi @@ -604,7 +597,6 @@ Inatumika kwa mipaka yote miwili. Katika 1.75, herufi nne pana zinahesabiwa kama Imetafsiriwa Lugha ya tafsiri - Lugha ya uandishi wa herufi za Kilatini Inatafsiri… Hakuweza kutafsiri mashairi Imeandikwa kwa herufi za Kilatini @@ -618,7 +610,6 @@ Inatumika kwa mipaka yote miwili. Katika 1.75, herufi nne pana zinahesabiwa kama Inaboresha Imesasishwa Ufunguo wa API ya QuickLRC - QuickLRC hutoa nakala au husawazisha muda wa mashairi kwa wimbo wa sasa. Huduma tofauti, isiyohusiana na kituo kinachoendana na OpenAI hapo juu Ufunguo wa QuickLRC umehifadhiwa. Tumia kitufe cha kusawazisha katika dialog ya utafutaji wa mashairi, au prompt ya jopo wakati wimbo hauna mashairi Ufunguo wa API ya QuickLRC umehifadhiwa QuickLRC haijakubali ufunguo huo @@ -634,56 +625,27 @@ Inatumika kwa mipaka yote miwili. Katika 1.75, herufi nne pana zinahesabiwa kama Hakuna mashairi ya kusawazisha - badala yake hutoa nakala QuickLRC haikuweza kuchakata wimbo huu Kredi za QuickLRC zimeisha kwa wimbo huu - "Mashairi yanalinganishwa kwa kichwa na msanii, metadata zisizo za kawaida zinaweza zisipate chochote au kupata toleo lisilo sahihi. Tumia refresh ili kubadilisha matoleo. Watoa huduma walioashiriwa na * wanahitaji token. Watoa huduma wote walioidhinishwa hawajombwi kila wakati kwa utaratibu wako, bali wanajibu kulingana na kasi yao.\n\nMashairi yanatoka kwa huduma za upande wa tatu ambazo hazihusiani, hazifadhiliwi, au hazijaidhinishwa na Morphe. Kila moja hutumika kama ilivyo: mashairi, upatikanaji wake, na leseni zake zinabaki na huduma hizo na wenye leseni zao, na kuzitumia kunaweza kuwa chini ya masharti yao ya huduma. Kinachofika kwenye huduma iliyoidhinishwa ni utafutaji tu uliouomba - kichwa, msanii, na, pale huduma hiyo inapohitaji, ID ya video. Endpoint ya AI ya hiari na transcription ya QuickLRC hufanya kazi tu baada ya kuzisanidi, na kupokea maandishi ya mashairi au URL ya video ya wimbo." Washa tu watoa huduma unaowatumia, au baki na chaguo-msingi - maombi ya mara kwa mara yanaweza kulemea tovuti ndogo za mashairi za jamii Ficha mstari wa sifa Ficha mstari wa sifa unaoonekana chini ya mashairi Badilisha nafasi ya tafsiri na uandishi wa kimatamshi - WASHI huweka uandishi wa kimatamshi chini ya mashairi na tafsiri juu; ZIMA huweka uandishi wa kimatamshi juu na tafsiri chini - Tafsiri - Uromanizaji - Mtoa huduma wa tafsiri - Google (Kawaida, Bure) - Inayooana na OpenAI (Kibinafsi) - Mtoa huduma wa Romanization - Google (Chaguo-msingi) - Kamusi ya Ndani / Nje ya Mtandao - Inayooana na OpenAI (Kutengeneza) - Sehemu ya AI - Inatumika tu wakati mtoa huduma hatumi lebo za sehemu kama Chorus au Verse, ambazo kisha zinatambuliwa na modeli yako inayooana na OpenAI - AI & Vituo vya Wingu - Sanidi funguo za API, modeli, vituo, na templeti za prompt kwa watoa huduma wa AI. - URL ya kituo cha API (mfano https://api.openai.com/v1/chat/completions) Kiolezo cha Prompt Viashiria: {language}, {task}, {title}, {artist}, {count}, {lines} Prompt imehifadhiwa Prompt imehifadhiwa, lakini kituo cha AI hakiwezi kutumiwa bado. Kusanidi kwanza chini ya Mipangilio ya AI Kuweka upya prompt - JSON si sahihi. Irekebishe au safisha uwanja huu kabla ya kuhifadhi Mipangilio ya Tafsiri & Romanization imehifadhiwa Mipangilio imerejeshwa katika chaguo-msingi URL ya msingi lazima ianze na http:// au https:// - Jaribu Muunganisho - Sanidi Vituo vya Mwisho - Onyesha Vituo Vyote vya Mwisho Inathibitisha muunganisho… - Mtoa Huduma ya Tafsiri - Mtoa Huduma ya Romanization - Lete Model Inachukua mifano… Chagua Mfano Imeshindwa kuchukua mifano. Angalia ufunguo wa API au muunganisho. - Mipangilio ya Gemini API - Inayooana na OpenAI (Endpoint ya Kijumla) - Mipangilio ya DeepL API - Ufunguo wa API Mfano URL ya Msingi Ufunguo wa API (hiari) - Ombi la tafsiri Mwili wa ziada wa ombi (JSON ya hiari) Ufunguo wa uthibitishaji (Bure au Pro) - Sanidi funguo za API, mifumo, endpoints, na maombi 8.23.51 - Zima aikoni nzito 9.13.51 - Rudisha tabu za Player Up next, Mashairi, na Zinazohusiana diff --git a/patches/src/main/resources/addresources/values-th-rTH/music/strings.xml b/patches/src/main/resources/addresources/values-th-rTH/music/strings.xml index ded6a8a460..0add8a3eb1 100644 --- a/patches/src/main/resources/addresources/values-th-rTH/music/strings.xml +++ b/patches/src/main/resources/addresources/values-th-rTH/music/strings.xml @@ -462,12 +462,47 @@ Second \"item\" text" แสดง การซิงโครไนซ์ แผง - จุดปลายทาง AI ปิดใช้งานผู้ให้บริการทั้งหมด — เนื้อเพลงในตัวและเนื้อเพลงฝังตัว รีเซ็ตผู้ให้บริการทั้งหมดหรือไม่ คืนค่าผู้ให้บริการกลับไปเป็นลำดับความสำคัญเริ่มต้น + รายการคำ Rime + "Rime Wordlist ยังต้องการ UniDict: ในผู้ให้บริการพจนานุกรมแบบออฟไลน์ ให้ติ๊ก \"ตัวอักษรจีนและภาษาแคะ (UniDict)\" และรอให้ดาวน์โหลดเสร็จสิ้นก่อน หากไม่มีรายการนี้ รายการที่อ่านตัวอักษรจะไม่สามารถแปลเสียงได้ + +ติ๊กเลือกรายการที่ Rime Wordlist ควรอ่าน แล้วแตะบันทึกเพื่อดาวน์โหลด รายการที่คุณเพิ่มจะถูกนำออกเมื่อคุณยกเลิกการติ๊กมัน + +แต่ละรายการคือตารางจากคำไปจนถึงการอ่านของคุณเอง ซึ่งเป็นไฟล์ .dict.yaml ที่มีบรรทัดเช่น: +字 ham +典 din2 +คอลัมน์แรกคือคำ คอลัมน์ที่สองคือการอ่านของมัน; คอลัมน์เพิ่มเติมและบรรทัดที่ไม่มีแท็บจะถูกมองข้าม" + ไม่มีรายการคำ Rime สำหรับภาษาข้างต้นที่ทำเครื่องหมายไว้ แตะชื่อนี้เพื่อทำเครื่องหมายเลือกหนึ่งรายการ หรือเลือกภาษาที่มีรายการ + เพิ่ม + เพิ่มรายการคำ Rime + ไฟล์ .dict.yaml หรือ zip ที่ครอบไฟล์ดังกล่าว โดยมีบรรทัดเช่น 字 ham และ 典 din2 รายการนี้จะถูกดึงเมื่อบางคนติ๊กเลือกและแตะบันทึก และใบอนุญาตของรายการนี้เป็นสิทธิ์ของคุณที่จะตรวจสอบ + รหัสภาษา + สองหรือสามตัวอักษร เช่น hak หรือ wuu + ชื่อ + แสดงใต้เสียงอ่าน เช่น Hakka (Meixian) + ที่อยู่รายการคำ + https:// ที่อยู่ของ .dict.yaml หรือไฟล์ zip ของมัน + ตัวอักษรที่ไม่อยู่ในคำ + อัตโนมัติ + ตัวอักษรกวางตุ้ง + ตัวอักษรมัณฑาร์ริน + ไม่มีการอ่านตัวอักษร + กรอกรหัส ชื่อ และที่อยู่ + รหัสมUSTประกอบด้วยสองหรือสามตัวอักษร เช่น hak + ที่อยู่ต้องใช้ https:// เป็นจุดเริ่มต้น + ชื่อและที่อยู่ไม่สามารถมีเส้นตั้งกลางได้ + %1$s มีตารางคำอยู่แล้ว + %1$s ถูกอ่านโดยระบบแปลงอักษรโรมันภายใน ไม่สามารถรับรายการคำได้ + ไม่ได้ใช้งาน: ภาษา nàyมีเสียงอ่านของตัวเองแล้ว ดังนั้นยกเลิกการทำเครื่องหมายเพื่อลบออก + เพิ่ม %1$s - ทำเครื่องหมายเลือกและแตะบันทึกเพื่อกำเนินการดาวน์โหลด + รายการคำพร้อมใช้งาน + รายการคำพร้อมใช้งาน (%1$d รายการ) + กำลังสร้างรายการคำเสียงอ่าน: %1$d%% + ดาวน์โหลดรายการคำล้มเหลว - แตะบันทึกอีกครั้งเพื่อลองใหม่ ล้างเนื้อเพลงที่จัดเก็บไว้ - ลบเนื้อเพลงที่เก็บไว้ในอุปกรณ์นี้ + ลบเนื้อเพลงที่เก็บไว้ในอุปกรณ์นี้ขนาด %1$s MB ล้างแล้ว %1$s MB รับโทเค็น โทเค็นผู้ใช้สื่อ Apple Music @@ -527,14 +562,22 @@ Second \"item\" text" แสดงปุ่มคัดลอก แตะเพื่อคัดลอกเนื้อเพลงต้นฉบับเป็นข้อความธรรมดา กดค้างเพื่อบันทึกไฟล์แหล่งที่มาของเนื้อเพลงดิบลงในโฟลเดอร์ดาวน์โหลด แสดงปุ่มแปลภาษา - แตะเพื่อสลับแปล; กดค้างเฉพาะสำหรับการแปล ใช้การแปลของผู้ให้บริการ Google Translate หรือ API ที่เข้ากันได้กับ OpenAI - แสดงปุ่มโรมาไนซ์ - แตะเพื่อสลับการถอดอักษร; กดค้างเฉพาะสำหรับการถอดอักษร ใช้การถอดอักษรของผู้ให้บริการ Google Translate หรือ API ที่เข้ากันได้กับ OpenAI - การถอดอักษรท้องถิ่นและการจัดตำแหน่ง - ถอดอักษรบนอุปกรณ์เมื่อผู้ให้บริการ โมเดลของคุณ และ Google Translate ล้มเหลว การอ่านจะติดตามแต่ละคำของเนื้อเพลงที่มีเวลาต้องเปิดการซิงค์คำ ภาษาจีน แคะ ญี่ปุ่น เกาหลี กรีก รัสเซีย จอร์เจีย อาร์เมเนีย ประมาณ 120 MB - กำลังสร้างคำอ่าน %1$s: %2$d%% + แตะเพื่อสลับการแปล; กดค้างสำหรับการแปลเท่านั้น ใช้ผู้ให้บริการการแปลที่เปิดใช้งานตามลำดับที่ระบุ + แสดงปุ่มแปลงอักษรโรมันหรือถอดรหัส + แตะเพื่อสลับการแปลงอักษรโรมันหรือถอดรหัส; กดค้างสำหรับการแปลงอักษรโรมันหรือถอดรหัสเท่านั้น ใช้ผู้ให้บริการการแปลงอักษรโรมันที่เปิดใช้งานตามลำดับที่ระบุ + เปิดใช้การถอดรหัส + อนุญาตเสียงอ่านไม่ใช่ลาตินจากแหล่งเนื้อเพลง; เมื่อปิดอยู่ จะคงไว้เฉพาะตัวอักษรลาติน ตัวอักษรที่มีเครื่องหมายประสม และตัวเลขระดับเสียง + อ่านแต่ละคำในเนื้อเพลงตามเวลา (การซิงค์คำเปิด); ภาษาเกาหลีไม่จำเป็นต้องดาวน์โหลด; สิบสี่สคริปต์แชร์การดาวน์โหลด CLDR เดียวกัน; ตัวอักษรฮั่นถูกอ่านโดยรายการคำ Rime เมื่อตารางคำครอบคลุมภาษานั้น + กำลังดาวน์โหลดพจนานุกรมเสียงอ่าน… คำอ่านพร้อมแล้ว (%1$d รายการ) การดาวน์โหลดล้มเหลว - ปิดและเปิดฟีเจอร์นี้ใหม่เพื่อลองอีกครั้ง + พจนานุกรมออฟไลน์ + ภาษาญี่ปุ่น (SudachiDict, ~75 MB) + ตัวอักษรจีนและกวางตุ้ง (UniDict, ~8 MB) + คำภาษาจีน (pinyin-pro, ~17 MB) + การถอดรหัสภาษากรีก ไวยาيليةสลาฟ (รวมถึงรัสเซีย ยูเครน บัลแกเรีย เซอร์เบีย เบลารุส มาซิโดเนีย), จอร์เจีย อาร์เมเนียน ฮีบรู อาหรับ อัมฮาริก และซีรีแอค (CLDR, ~0.5 MB) + กำลังสร้างคำอ่าน %1$s: %2$d%% + เลือกพจนานุกรมอย่างน้อยหนึ่งรายการ แสดงปุ่มรีเฟรช แตะเพื่อสลับไปยังตัวเลือกเนื้อเพลงถัดไป; กดค้างเพื่อค้นหาเนื้อเพลงตามชื่อเพลงและศิลปิน ค้นหาเนื้อเพลง @@ -545,8 +588,10 @@ Second \"item\" text" บันทึกแล้ว ไฮไลต์คำหรือพยางค์ที่ซิงก์ ให้ความสำคัญกับเนื้อเพลงแบบคำต่อคำเมื่อมีให้บริการ เมื่อเนื้อเพลงมีข้อมูลเวลาของคำ จะไฮไลต์แต่ละคำหรือพยางค์ให้ตรงกับจังหวะเพลง - แสดงเสียงร้องประกอบแยกต่างหาก - ย้ายเสียงร้องพื้นหลังที่เขียนอยู่ในวงเล็บ เช่น \"(โอ้) ฉันต้องการคุณ\" ออกจากท่อนร้องหลักไปยังบรรทัดแยกต่างหากที่มีความเบากว่าของตัวเอง + ลดขนาดบรรทัดที่ไม่ใช้งาน + วาดทุกบรรทัดที่ยังไม่ร้องอยู่ในขณะนี้น้อยลงเล็กน้อย โดยทำให้จางลงด้วย + แสดงเสียงประสานพื้นหลังแยกต่างหาก + แยกเสียงร้องพื้นหลังและเสียงประสานออกจากทำนองหลัก ย้ายเสียงร้องพื้นหลังที่เขียนอยู่ในวงเล็บ เช่น \"(Oh) I want you\" ออกจากท่อนร้องหลักไปอยู่ในไลน์ของตัวเองที่มีความดังน้อยลง ซ่อนบรรทัดที่เล่นแล้ว […] ซ่อนบรรทัดเนื้อเพลงที่อยู่เหนือบรรทัดที่กำลังเล่น ซ่อนเนื้อเพลงที่ยังไม่เล่น @@ -604,10 +649,15 @@ Second \"item\" text" แปลโดย %s ถอดเป็นอักษรโรมันโดย %s ถอดเป็นอักษรโรมันโดย %s + แปลงอักษรโรมันโดย Rime (%s) แปลแล้ว ภาษาแปล - ภาษาสำหรับการถอดเป็นอักษรโรมัน + ภาษาการแปลงอักษรโรมันหรือถอดรหัส + "ภาษาเป้าหมายสำหรับการแปล: +%s" + "ภาษาเป้าหมายสำหรับการโรมันไนซ์หรือการทับศัพท์: +%s" กำลังแปล... ไม่สามารถแปลเนื้อเพลงได้ โรมาไนซ์แล้ว @@ -621,7 +671,7 @@ Second \"item\" text" กำลังรีเฟรช รีเฟรชแล้ว คีย์ API ของ QuickLRC - QuickLRC ช่วยถอดเสียงหรือจัดเวลาสำหรับเนื้อเพลงของแทร็กปัจจุบัน เป็นบริการแยกอิสระ ไม่เกี่ยวข้องกับจุดปลายทางที่เข้ากันได้กับ OpenAI ด้านบน + QuickLRC เขียนตัวอักษรหรือจับคู่เวลากับเนื้อเพลงสำหรับแทร็กปัจจุบัน รหัส QuickLRC ถูกบันทึกแล้ว ใช้ปุ่มจัดตำแหน่งในกล่องโต้ตอบค้นหาเนื้อเพลง หรือข้อความแจ้งเตือนบนแผงควบคุมเมื่อแทร็กไม่มีเนื้อเพลง บันทึกคีย์ API ของ QuickLRC เรียบร้อยแล้ว QuickLRC ไม่ได้ยอมรับคีย์นั้น @@ -637,56 +687,50 @@ Second \"item\" text" ไม่มีเนื้อเพลงให้จัดตำแหน่ง - กำลังถอดเสียงแทน QuickLRC ไม่สามารถประมวลผลแทร็กนี้ หน่วยเครดิตของ QuickLRC สำหรับแทร็กนี้หมดแล้ว - "เนื้อเพลงจะถูกจับคู่ตามชื่อเพลงและศิลปิน ข้อมูลเมตาที่ผิดปกติอาจทำให้ไม่พบอะไรเลยหรือเวอร์ชันที่ไม่ถูกต้อง ให้รีเฟรชเพื่อเปลี่ยนเวอร์ชัน ผู้ให้บริการที่มีเครื่องหมาย * ต้องใช้โทเค็น ผู้ให้บริการที่เปิดใช้งานทั้งหมดอาจไม่ได้ถูกร้องขอตามลำดับของคุณเสมอไป แต่จะตอบสนองตามความเร็วของแต่ละราย\n\nเนื้อเพลงมาจากบริการของบุคคลที่สาม ซึ่งไม่มีส่วนเกี่ยวข้อง สนับสนุน หรือรับรองโดย Morphe แต่อย่างใด แต่ละรายการจะถูกใช้งานตามที่เป็นอยู่: เนื้อเพลง ความพร้อมใช้งาน และลิขสิทธิ์ยังคงเป็นของบริการเหล่านั้นและผู้ให้ลิขสิทธิ์ และการใช้งานอาจอยู่ภายใต้เงื่อนไขบริการของพวกเขา สิ่งที่ส่งไปยังบริการที่เปิดใช้งานอยู่จะมีเพียงข้อมูลที่คุณร้องขอเท่านั้น - ชื่อเพลง ศิลปิน และเมื่อบริการนั้นต้องการ รหัสวิดีโอ จุดปลายทาง AI และการถอดเสียง QuickLRC ที่เป็นตัวเลือกเสริม จะทำงานหลังจากที่คุณได้ตั้งค่าและได้รับเนื้อเพลงหรือ URL วิดีโอของแทร็กแล้วเท่านั้น" + "เนื้อเพลงจะถูกจับคู่ตามชื่อเพลงและศิลปิน ข้อมูลเมตาที่ผิดปกติอาจทำให้ไม่พบอะไรเลยหรือเวอร์ชันที่ไม่ถูกต้อง ให้รีเฟรชเพื่อเปลี่ยนเวอร์ชัน ผู้ให้บริการที่มีเครื่องหมาย * ต้องใช้โทเค็น ผู้ให้บริการที่เปิดใช้งานทั้งหมดอาจไม่ได้ถูกร้องขอตามลำดับของคุณเสมอไป แต่จะตอบสนองตามความเร็วของแต่ละราย + +เนื้อเพลงมาจากบริการของบุคคลที่สาม ซึ่งไม่มีส่วนเกี่ยวข้อง สนับสนุน หรือรับรองโดย Morphe แต่ละรายการจะถูกใช้งานตามที่เป็นอยู่: เนื้อเพลง ความพร้อมใช้งาน และลิขสิทธิ์ยังคงเป็นของบริการเหล่านั้นและผู้ให้ลิขสิทธิ์ และการใช้งานอาจอยู่ภายใต้เงื่อนไขบริการของพวกเขา สิ่งที่ส่งไปยังบริการที่เปิดใช้งานอยู่จะมีเพียงข้อมูลที่คุณร้องขอเท่านั้น - ชื่อเพลง ศิลปิน และเมื่อบริการนั้นต้องการ รหัสวิดีโอ จุดปลายทาง AI และการถอดเสียง QuickLRC ที่เป็นตัวเลือกเสริม จะทำงานหลังจากที่คุณได้ตั้งค่าและได้รับเนื้อเพลงหรือ URL วิดีโอของแทร็กแล้วเท่านั้น" เปิดใช้งานเฉพาะผู้ให้บริการที่คุณใช้ หรือคงค่าเริ่มต้นไว้ เนื่องจากคำขออย่างต่อเนื่องอาจทำให้ไซต์เนื้อร้องชุมชนขนาดเล็กล้นเกิน ซ่อนบรรทัดเครดิต ซ่อนบรรทัดเครดิตที่แสดงใต้เนื้อเพลง สลับคำแปลและการโรมาไนซ์ - การเปิดจะวางการโรมาไนซ์ไว้ใต้เนื้อเพลงและคำแปลไว้ด้านบน การปิดจะให้การโรมาไนซ์อยู่ด้านบนและคำแปลอยู่ด้านล่าง - การแปล - การถอดเป็นอักษรโรมัน - ผู้ให้บริการการแปล - Google (ค่าเริ่มต้น, ฟรี) - OpenAI Compatible (กำหนดเอง) - ผู้ให้บริการถอดเป็นอักษรโรมัน - Google (ค่าเริ่มต้น) - พจนานุกรมในเครื่อง / ออฟไลน์ - OpenAI Compatible (กำหนดเอง) - ส่วน AI - ใช้เมื่อผู้ให้บริการไม่ได้ส่งป้ายกำกับส่วนต่างๆ เช่น Chorus หรือ Verse ซึ่งโมเดลที่เข้ากันได้กับ OpenAI ของคุณจะตรวจจับให้แทน - AI & จุดปลายทางคลาวด์ - กำหนดค่าคีย์ API, โมเดล, จุดปลายทาง และเทมเพลตคำสั่งสำหรับผู้ให้บริการ AI - URL จุดปลายทาง API (เช่น https://api.openai.com/v1/chat/completions) + ON ใส่การแปลงอักษรโรมันหรือถอดรหัสไว้ด้านล่างเนื้อเพลงและการแปลไว้ด้านบน; OFF คงการแปลงอักษรโรมันหรือถอดรหัสไว้ด้านบนและการแปลไว้ด้านล่าง เสียงอ่านที่วางเหนือพยางค์ใดจะเคลื่อนไปกับมัน จากเหนือพยางค์ไปยังใต้พยางค์ + จัดข้อความกลางเสมอ + ON จัดบรรทัดทุกบรรทัดตรงกลางและละเลยการจัดตำแหน่งต้นฉบับ รวมถึงด้านคู่ дуэт; OFF คงการจัดตำแหน่งตามที่ต้องการโดยเนื้อเพลง + ต้องมีผู้ให้บริการอย่างน้อยหนึ่งรายการที่เปิดอยู่ + การแปล + การแปลงอักษรโรมันหรือถอดรหัส + พจนานุกรมออฟไลน์ + รายการคำ Rime + ตรวจจับส่วนด้วย AI + ใช้ก็ต่อเมื่อผู้ให้บริการไม่มีป้ายส่วน เช่น Chorus หรือ Verse ซึ่งโมเดลที่เข้ากันได้กับ OpenAI ของคุณจะตรวจจับ การแก้ไขแถวจะเปิดพรอมต์ของมันด้วย + URL จุดสิ้นสุด API (เช่น https://api.openai.com/v1/chat/completions, https://api.anthropic.com/v1/chat/completions, https://generativelanguage.googleapis.com/v1beta/openai/chat/completions) เทมเพลตคำสั่ง ตัวแทน: {language}, {task}, {title}, {artist}, {count}, {lines} บันทึกคำสั่งแล้ว บันทึก Prompt แล้ว แต่จุดปลายทาง AI ยังใช้งานไม่ได้ โปรดกำหนดค่าภายใต้การตั้งค่า AI ก่อน รีเซ็ตคำสั่งแล้ว - ไม่ใช่ JSON ที่ถูกต้อง แก้ไขหรือล้างฟิลด์ก่อนบันทึก บันทึกการตั้งค่าการแปล & การถอดเป็นอักษรโรมันแล้ว รีเซ็ตการตั้งค่าเป็นค่าเริ่มต้นแล้ว URL ฐานต้องเริ่มต้นด้วย http:// หรือ https:// - ทดสอบการเชื่อมต่อ - กำหนดค่าจุดปลายทาง - แสดงจุดปลายทางทั้งหมด + การตั้งค่าไม่ครบถ้วนหรือถูกปฏิเสธโดยผู้ให้บริการ กำลังตรวจสอบการเชื่อมต่อ… - ผู้ให้บริการแปลภาษา - ผู้ให้บริการถอดเป็นอักษรโรมัน - ดึงข้อมูลโมเดล + %1$s: เชื่อมต่อแล้ว! (%2$s) + %1$s: การเชื่อมต่อล้มเหลว ตรวจสอบการตั้งค่า + ป้อนคีย์ API ก่อน กำลังดึงข้อมูลโมเดล… เลือกโมเดล ดึงข้อมูลโมเดลล้มเหลว โปรดตรวจสอบ API key หรือการเชื่อมต่อ - การตั้งค่า Gemini API - OpenAI Compatible (Custom Endpoint) - การตั้งค่า DeepL API - คีย์ API + พรอมต์ โมเดล Base URL + เลือกบริการ API key (ไม่บังคับ) - คำสั่งสำหรับการแปล ส่วนขยายของ request body (JSON ไม่บังคับ) + หัวเรื่องคำขอเพิ่มเติม (JSON ตัวเลือก) + ใส่ชื่อและค่าในเครื่องหมายคำพูด เช่น {\"api-key\": \"abc\"}. ค่าที่ไม่มีเครื่องหมายคำพูดซึ่งมี / : หรือตัวเลขจะถูกปฏิเสธ ค่าว่างจะลบหัวเรื่อง + ใส่ชื่อและค่าในเครื่องหมายคำพูด เช่น {\"temperature\": 0.3}. ค่าที่ไม่มีเครื่องหมายคำพูดซึ่งมี / หรือ : ถูกปฏิเสธ Model และ messages ไม่สามารถตั้งค่าที่นี่ได้ คีย์การยืนยันตัวตน (Free หรือ Pro) - กำหนดค่า API key, โมเดล, จุดปลายทาง และคำสั่ง 8.23.51 - ปิดใช้งานไอคอนตัวหนา 9.13.51 - กู้คืนแท็บถัดไป เนื้อเพลง และที่เกี่ยวข้องของเครื่องเล่น diff --git a/patches/src/main/resources/addresources/values-th-rTH/youtube/strings.xml b/patches/src/main/resources/addresources/values-th-rTH/youtube/strings.xml index 22ea877082..0129480b13 100644 --- a/patches/src/main/resources/addresources/values-th-rTH/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-th-rTH/youtube/strings.xml @@ -29,6 +29,8 @@ Second \"item\" text" เบ็ดเตล็ด วิดีโอ กู้คืนเมนูการตั้งค่าแบบเก่า + แสดงข้อความใน YouTube snackbar + ข้อความของ Morphe เช่นการยืนยันปุ่มผู้เล่น จะแสดงใน YouTube snackbar แทนป๊อปอัป ลบข้อจำกัดการเล่นในพื้นหลัง ปิดการเล่นพื้นหลัง diff --git a/patches/src/main/resources/addresources/values-tr-rTR/music/strings.xml b/patches/src/main/resources/addresources/values-tr-rTR/music/strings.xml index d3fd62f699..2699619b8c 100644 --- a/patches/src/main/resources/addresources/values-tr-rTR/music/strings.xml +++ b/patches/src/main/resources/addresources/values-tr-rTR/music/strings.xml @@ -462,12 +462,47 @@ Scrobbling'in arka planda çalma sırasında senkronize olduğundan emin olmak i Görünüm Senkronizasyon Panel - AI uç noktası Tüm sağlayıcılar devre dışı - yerleşik sözler ve gömülü sözler Sağlayıcı sırası sıfırlansın mı? Sağlayıcıları varsayılan öncelik sırasına geri yükle + Rime Kelime Listesi + "Rime Kelime Listesi için de UniDict gerekir: Çevrimdışı Sözlük sağlayıcısında \"Çince ve Kantonca karakterler (UniDict)\" kutucuğunu işaretleyin ve indirmenin bitmesini bekleyin. Bu olmadan, karakterleri okuyan bir liste hiçbir ses vermez. + +Rime Kelime Listesi'nin okuması gereken listeleri işaretleyin, sonra onlar için indirmek üzere Kaydet'e dokunun. Eklendiğiniz bir liste, işaretini kaldırdığınızda kaldırılır. + +Her liste size özel kelime-okuma tablonuzdur, .dict.yaml dosyasında şu satırlar gibi: +字 ham +典 din2 +İlk sütun kelimedir, ikinci sütunun okunuşudur; ek sütunlar ve sekme içermeyen satırlar dikkate alınmaz." + Yukarıdaki dil için seçilmiş hiç Rime kelime listesi yok. Bir tane seçmek için bu isme dokunun veya listesi olan başka bir dil seçin + Ekle + Rime Kelime Listesi Ekle + Söz dizimi gibi 字 ham ve 典 din2 satırları içeren bir .dict.yaml dosyası veya bunu içeren bir zip. Liste işaretlediğinizde ve Kaydet\'e tıkladığınızda çekilir ve lisansı sizin kontrolünüze sunulur + Dil kodu + iki veya üç harfli olmalı, örn. hak veya wuu + Ad + Hakka (Meixian) örneğindeki gibi okunmanın altında gösterilir + Kelime listesi adresi + .dict.yaml dosyasının veya onun zip dosyasının https:// adresi + Kelimede olmayan karakterler + Otomatik + Kantonca karakterler + Mandarin karakterleri + Karakter okuma yok + Kod, ad ve adresi doldurun + Kod hak örneğindeki gibi iki veya üç harfli olmalıdır + Adres https:// ile başlamalıdır + Ad ve dikey çizgi içeremez + %1$s için zaten bir kelime tablosu var + %1$s yerleşik romaniyaze tarafından okunur ve kelime listesi alamaz + Kullanılmıyor: Bu dilin zaten kendi okuması var, kaldırılması için işaretini kaldırın + %1$s eklendi - indirmek için işaretleyin ve Kaydet\'e dokunun + Kelime listeleri hazır + Kelime listeleri hazır (%1$d girdi) + Okuma kelime listesi oluşturuluyor: %1$d%% + Kelime listesi indirilemedi - yeniden denemek için tekrar Kaydet\'e dokunun Saklanan şarkı sözlerini temizle - Bu cihazda tutulan şarkı sözlerini sil + Bu cihazda saklanan şarkı sözlerinin %1$s MB\'ını sil %1$s MB temizlendi Token alın Apple Music Media-user-token @@ -527,14 +562,22 @@ Gerekli çerezler: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPISI Kopyala düğmesini göster Orijinal şarkı sözlerini düz metin olarak kopyalamak için dokunun; ham şarkı sözleri kaynak dosyasını İndirilenlere kaydetmek için basılı tutun Çevir düğmesini göster - Çeviriyi açıp kapatmak için dokunun; yalnızca çeviriyi görmek için basılı tutun. Sağlayıcı çevirileri, Google Çeviri veya bir OpenAI-uyumlu API kullanır - Okunuş butonunu göster - Romanizasyonu açıp kapatmak için dokunun; yalnızca romanizasyonu görmek için basılı tutun. Sağlayıcı romanizasyonu, Google Çeviri veya bir OpenAI-uyumlu API kullanır - Yerel Romanizasyon ve Hizalama - Sağlayıcı, modeliniz ve Google Çeviri başarısız olduğunda romanizasyonu cihaz üzerinde yap. Okuma, zamanlanmış sözlerin her kelimesini takip eder; kelime senkronizasyonu açık olmalıdır. Çince, Kantonca, Japonca, Korece, Yunanca, Rusça, Gürcüce, Ermenice; yaklaşık 120 MB - Okuma %1$s: %2$d%% + Çeviriyi açıp kapatmak için dokunun; yalnızca çeviri görmek için uzun basın. Etkin çeviri sağlayıcılarını sıralarına göre kullanır + Romanizasyon veya Transkripsiyon düğmesini göster + Romanizasyonu veya transkripsiyonu açıp kapatmak için dokunun; yalnızca romanizasyon veya transkripsiyon yapmak için uzun basın. Etkin romanizasyon sağlayıcılarını sıralarına göre kullanır + Transkripsiyonu etkinleştir + Şarkı sözü kaynağından Latin olmayan okumalara izin ver; kapalıyken yalnızca Latin harflerini, aksanlı harfleri ve ses tonu rakamlarını koru + Zamanlanmış şarkı sözünün her kelimesini okur (kelime senkronizasyonu açık); Korece için indirme gerekmez; on dört yazı sistemi tek bir CLDR indirmesini paylaşır; Han karakterleri Rime Kelime Listesi tarafından, kelime tablosu dili kapsadığında okunur + Okuma sözlükleri indiriliyor… Okumalar hazır (%1$d kayıt) İndirme başarısız oldu - yeniden denemek için bunu kapatıp tekrar açın + Çevrimdışı Sözlük + Japonca (SudachiDict, ~75 MB) + Çince ve Kantonca karakterler (UniDict, ~8 MB) + Çince kelimeler (pinyin-pro, ~17 MB) + Yunan, Slav Kiril alfabesi (Rusça, Ukraynaca, Bulgarca, Sırpça, Beyaz Rusça, Makedonca dahil), Gürcü, Ermeni, İbranice, Arapça, Amharca ve Süryanice transkripsiyon (CLDR, ~0.5 MB) + Okuma %1$s: %2$d%% + En az bir sözlük seçin Yenile düğmesini göster Sonraki şarkı sözleri adayına geçmek için dokunun, şarkı sözlerini şarkı başlığı ve sanatçı ile aramak için basılı tutun Şarkı sözü ara @@ -545,8 +588,10 @@ Gerekli çerezler: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1PAPISI Kaydedildi Senkronize kelimeleri veya heceleri vurgula Mümkün olduğunda kelime kelime şarkı sözlerine öncelik ver. Şarkı sözleri kelime zamanlaması içeriyorsa, her kelimeyi veya heceyi müzikle eş zamanlı olarak vurgular + Etkin olmayan satırları küçült + Şu an söylenmeyen her satırı biraz daha küçük çizer, üstüne karartma efekti uygular Arka plan vokallerini ayrı göster - Parantez içinde yazılan arka plan vokalleri, örneğin \"(Oh) seni istiyorum\", söylenen satırdan ayrı bir daha düşük sesli satıra taşır + Arka plan vokallerini ve uyumu ana satırdan ayırır. Parantez içinde yazılan arka plan vokallerini, örn. \"(Oh) Senistiyorum\", ana söylenen satırdan çıkarıp kendi karartılmış satırına taşır Satırı gizle Şu anda çalınan satırın üzerindeki şarkı sözü satırlarını gizle Okunmamış şarkı sözlerini gizle @@ -604,10 +649,15 @@ Her iki sınır için de geçerlidir. 1,75 değerinde, dört tam genişlikteki k Çeviren: %s Romanize eden: %s Okunuşu sağlayan: %s + Rime tarafından Romanize Edildi (%s) Çevrildi Çeviri dili - Romanizasyon dili + Romanizasyon veya Transkripsiyon Dili + "Çeviri hedef dili: +%s" + "Transliterasyon hedef dili: +%s" Çeviriliyor… Şarkı sözleri çevrilemedi Romanize @@ -621,7 +671,7 @@ Her iki sınır için de geçerlidir. 1,75 değerinde, dört tam genişlikteki k Yenileniyor Yenilendi QuickLRC API anahtarı - QuickLRC mevcut parça için sözleri transkribe eder veya zamanlandırır. Yukarıdaki OpenAI uyumlu uç noktadan bağımsız ayrı bir hizmet + QuickLRC mevcut parça için şarkı sözlerini yazıya döker veya zamanla hizalar QuickLRC anahtarı kaydedildi. Şarkı sözleri arama iletişim kutusundaki hizalama düğmesini veya şarkı sözleri olmayan bir parça olduğunda panel istemini kullanın QuickLRC API anahtarı kaydedildi QuickLRC bu anahtarı kabul etmedi @@ -637,56 +687,50 @@ Her iki sınır için de geçerlidir. 1,75 değerinde, dört tam genişlikteki k Hizalanacak şarkı sözü yok - bunun yerine transkribe ediliyor QuickLRC bu parçayı işleyemedi Bu parça için QuickLRC kredileri tükendi - "Şarkı sözleri başlık ve sanatçıyla eşleştirilir, olağandışı meta verileri hiçbir şey veya yanlış versiyon bulabilir. Versiyonları değiştirmek için yenilemeyi kullanın. * ile işaretlenen sağlayıcılar bir anahtar gerektirir. Etkinleştirilmiş her sağlayıcıdan her zaman talepte bulunulmaz; bunun yerine, sağlayıcılar hızlarına bağlı olarak yanıt verirler.\n\nŞarkı sözleri Morphe ile bağlantısı, sponsorluğu olmayan veya Morphe'nin onaylamadığı üçüncü parti hizmetlerden gelir. Her biri olduğu gibi kullanılır; sözler, kullanılabilirlikler ve lisanslamaları bu hizmetlerde ve onların lisans sağlayıcılarında kalır, kullanımları onların hizmet şartlarına tabi olabilir. Hizmete ulaşan şey sadece sizin talep ettiğinizdir - başlık, sanatçı ve gereken durumlarda video ID'si. İsteğe bağlı AI hizmeti ve QuickLRC transkripsiyonu yalnızca siz onu yapılandırdıktan sonra çalışır ve yalnızca şarkı sözlerini veya parçanın video URL'sini alır." + "Şarkı sözleri başlık ve sanatçıyla eşleştirilir, olağandışı meta veriler hiçbir şey veya yanlış versiyon bulabilir. Versiyonları değiştirmek için yenile'yi kullanın. * ile işaretlenen sağlayıcılar bir belirteç gerektirir. Etkinleştirilmiş tüm sağlayıcılardan her zaman talep edilmeyebilir, ancak hızlarına göre yanıt verirler. + +Şarkı sözleri Morphe ile bağlantısı, sponsorluğu veya onayı olmayan üçüncü taraf hizmetlerden gelir. Her biri olduğu gibi kullanılır; şarkı sözleri, kullanılabilirlikleri ve lisanslama hakları bu hizmetlerde ve lisans verenlerde kalır, kullanım hizmet şartlarına tabi olabilir. Etkinleştirilen bir hizmete ulaşan yalnızca istediğiniz arama sorgusudur - başlık, sanatçı ve hizmetin ihtiyaç duyduğu durumlarda video Kimliği. İsteğe bağlı AI uç noktası ve QuickLRC transkripsiyonu yalnızca bunları yapılandırdıktan sonra çalışır ve şarkı sözü metnini veya parçanın video URL'sini alır." Sadece kullandığınız sağlayıcıları etkinleştirin veya varsayılanı koruyun; sürekli istekler küçük topluluk şarkı sözü sitelerini aşırı yükleyebilir Yenile Şarkı sözlerinin altında gösterilen künye satırını gizle Çeviri ve okunuşun yerini değiştir - AÇIK olduğunda okunuşu şarkı sözlerinin altına ve çeviriyi üstüne yerleştirir; KAPALI olduğunda okunuşu üstte ve çeviriyi altta tutar - Çeviri - Romanizasyon - Çeviri sağlayıcısı - Google (Varsayılan, Ücretsiz) - OpenAI Uyumlu (Özel) - Romanizasyon sağlayıcısı - Google (Varsayılan) - Yerel / Çevrimdışı Sözlük - OpenAI Uyumlu (Özel) - Yapay Zeka Bölümü - Yalnızca sağlayıcı Nakarat veya Kıta gibi bölüm etiketleri göndermediğinde ve bunları OpenAI uyumlu modeliniz tespit ettiğinde kullanılır - AI & Bulut Uç Noktaları - Yapay zeka sağlayıcıları için API anahtarlarını, modelleri, uç noktaları ve istem şablonlarını yapılandırın. - API uç nokta URL\'si (örneğin https://api.openai.com/v1/chat/completions) + AÇIK romanizasyonu veya transkripsiyonu şarkı sözlerinin altına, çeviriyi üzerine koyar; KAPALI romanizasyonu veya transkripsiyonu üste, çeviriyi alta bırakır. Her heceye yerleştirilen bir okuma, o hecenin üzerinden altına doğru hareket eder + Şarkı sözlerini her zaman ortaya hizala + AÇIK tüm satırları ortaya koyar ve kaynak hizalamayı, düet yanları dahil görmezden gelir; KAPALI şarkı sözlerinin istediği hizalamayı korur + En az bir sağlayıcı açık kalmalıdır + Çeviri + Romanizasyon veya Transkripsiyon + Çevrimdışı Sözlük + Rime Kelime Listesi + Bölümleri yapay zeka ile tespit et + Sağlayıcı Nakarat veya Verse gibi bölüm etiketleri göndermediğinde yalnızca kullanılır; OpenAI-uyumlu modeliniz bunları o zaman tespit eder. Satırı düzenlemek onun prompt\'unu da açar + API uç noktası URL\'si (örn. https://api.openai.com/v1/chat/completions, https://api.anthropic.com/v1/chat/completions, https://generativelanguage.googleapis.com/v1beta/openai/chat/completions) İstem Şablonu Yer tutucular: {language}, {task}, {title}, {artist}, {count}, {lines} İstem kaydedildi İstem kaydedildi, ancak AI uç noktası henüz kullanılabilir değil. Önce AI ayarlarından yapılandırın. İstem sıfırlandı - Geçerli JSON değil. Düzeltin veya kaydetmeden önce alanı temizleyin Çeviri & Romanizasyon ayarları kaydedildi Ayarlar varsayılanlara sıfırlandı Temel URL http:// veya https:// ile başlamalıdır - Bağlantıyı Test Et - Uç Noktaları Yapılandır - Tüm Uç Noktaları Göster + Ayarlar eksik veya sağlayıcı tarafından reddedildi Bağlantı doğrulanıyor… - Çeviri Sağlayıcısı - Romanizasyon Sağlayıcısı - Modelleri Getir + %1$s: Bağlandı! (%2$s) + %1$s: Bağlantı başarısız. Ayarları kontrol edin. + Önce API anahtarını girin Modeller getiriliyor… Bir Model Seçin Modeller alınamadı. API anahtarını veya bağlantıyı kontrol edin. - Gemini API Ayarları - OpenAI Uyumlu (Özel Uç Nokta) - DeepL API Ayarları - API anahtarı + İstem Model Temel URL + Bir hizmet seçin API anahtarı (isteğe bağlı) - Çeviri istemi Ek istek gövdesi (isteğe bağlı JSON) + Ek istek başlıkları (isteğe bağlı JSON) + İsim ve değerleri tırnak içine alın, örn. {\"api-key\": \"abc\"}. Tırnak içine alınmamış / : veya sayı içeren bir değer reddedilir. Boş bir değer bir başlığı kaldırır. + İsim ve değerleri tırnak içine alın, örn. {\"temperature\": 0.3}. Tırnak içine alınmamış / veya : içeren bir değer reddedilir. Model ve mesajlar burada ayarlanamaz. Kimlik doğrulama anahtarı (Ücretsiz veya Pro) - API anahtarlarını, modelleri, uç noktaları ve istemleri yapılandırın 8.23.51 - Kalın simgeleri devre dışı bırak 9.13.51 - Çalardaki Sıradaki, Şarkı Sözleri ve İlgili sekmelerini geri yükle diff --git a/patches/src/main/resources/addresources/values-tr-rTR/youtube/strings.xml b/patches/src/main/resources/addresources/values-tr-rTR/youtube/strings.xml index b0c6341e3c..c889991495 100644 --- a/patches/src/main/resources/addresources/values-tr-rTR/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-tr-rTR/youtube/strings.xml @@ -29,6 +29,8 @@ Second \"item\" text" Çeşitli Video Eski YouTube ayarlar ekranını geri yükle + YouTube snackbar\'da mesajları göster + Morphe mesajları, örneğin bir oynatıcı düğmesinin onayı gibi, popup yerine YouTube snackbar\'da gösterilir Arka planda oynatma kısıtlamalarını kaldır Arka planda oynatmayı devre dışı bırak diff --git a/patches/src/main/resources/addresources/values-uk-rUA/music/strings.xml b/patches/src/main/resources/addresources/values-uk-rUA/music/strings.xml index f8a1cdc662..322f827d55 100644 --- a/patches/src/main/resources/addresources/values-uk-rUA/music/strings.xml +++ b/patches/src/main/resources/addresources/values-uk-rUA/music/strings.xml @@ -462,12 +462,47 @@ Second \"item\" text" Відображення Синхронізація Панель - Кінцева точка ШІ Усі постачальники вимкнено, використовуються вбудовані та вбудовані в файл тексти пісень Скинути порядок постачальників? Відновити типовий порядок пріоритету постачальників + Список слів Rime + "Для Rime Wordlist також потрібен UniDict: у постачальнику офлайн-словника позначте \"Китайські кантонські символи (UniDict)\" і дайте йому завантажитися. Без цього список із розпізнаванням символів не видасть читання. + +Позначте списки, які має читати Rime Wordlist, потім натисніть Зберегти для їхнього завантаження. Список, який ви додали, видаляється, коли знімаєте позначку. + +Кожен список — це ваша власна таблиця зіставлення слів із читанням, файл .dict.yaml з рядками виду: +字 ham +典 din2 +Перший стовпець — це слово, другий — його читання; додаткові стовпці та рядки без табуляції ігноруються." + Для мови вище не позначено жодного списку слів Rime. Натисніть на це ім\'я, щоб позначити один, або оберіть мову, для якої є список + Додати + Додати список слів Rime + Файл .dict.yaml або архів zip, що містить один такий файл, з рядками виду 字 ham та 典 din2. Список завантажується, коли ви позначаєте його та натискаєте Зберегти, а вашу ліцензію можна перевірити + Код мови + Дві або три літери, як-от hak або wuu + Назва + Показується під читанням, як-от Hakka (Meixian) + Адреса списку слів + Адреса https:// файлу .dict.yaml або його архіву zip + Символи, яких немає в слові + Автоматична + Кантонські символи + Мандаринські символи + Без читання символів + Заповніть код, назву та адресу + Код має складатися з двох або трьох літер, як-от hak + Адреса має починатися з https:// + Назва та адреса не можуть містити вертикальну риску + %1$s вже має таблицю слів + %1$s зчитується вбудованим романізатором і не може мати список слів + Не використовується: ця мова вже має власне читання, тому зніміть прапорець, щоб видалити її + Додано %1$s — позначте його й натисніть «Зберегти», щоб завантажити + Списки слів готові + Списки слів готові (%1$d записів) + Читання списку слів: %1$d%% + Завантаження списку слів не вдалося — натисніть «Зберегти» ще раз, щоб повторити Очистити збережені тексти пісень - Видалити тексти пісень, збережені на цьому пристрої + Видалити збережені на цьому пристрої тексти пісень об’ємом %1$s МБ Очищено %1$s МБ Отримати токен Apple Music Media-user-token @@ -527,14 +562,22 @@ Second \"item\" text" Показати кнопку \"Копіювати\" Натисніть, щоб скопіювати оригінальний текст пісні як звичайний текст; натисніть і утримуйте, щоб зберегти файл вихідного тексту пісні у \"Завантаження\" Показати кнопку \"Перекласти\" - Натисніть, щоб увімкнути або вимкнути переклад; натисніть і утримуйте, щоб показати лише переклад. Використовує переклади постачальника, Google Перекладач або API, сумісний з OpenAI - Показати кнопку \"Романізувати\" - Натисніть, щоб увімкнути або вимкнути романізацію; натисніть і утримуйте, щоб показати лише романізацію. Використовує романізацію постачальника, Google Перекладач або API, сумісний з OpenAI - Локальна романізація та вирівнювання - Романізувати на пристрої, коли постачальник, ваша модель і Google Перекладач не спрацьовують. Читання відповідає кожному слову синхронізованого тексту пісні; синхронізація по словах має бути ввімкнена. Китайська, кантонська, японська, корейська, грецька, російська, грузинська, вірменська; близько 120 МБ - Читання %1$s: %2$d%% + Натисніть, щоб увімкнути або вимкнути переклад; натисніть і утримуйте, щоб показати лише переклад. Використовувати увімкнених постачальників перекладу в порядку їх переліку + Показати кнопку романографії або транслітерації + Натисніть, щоб увімкнути або вимкнути романографію чи транслітерацію; натисніть і утримуйте, щоб використати лише романографію або транслітерацію. Використовувати увімкнених постачальників романографії в порядку їх переліку + Увімкнути транслітерацію + Дозволити немовні латинське читання з джерела текстів; коли вимкнено, залишати лише латинські літери, літери з апострофами та цифри тональності + Зчитує кожне слово синхронізованого тексту (увімкнено синхронізацію слів); для корейської не потрібно завантажувати; чотирнадцять сценаріїв ділять одне завантаження CLDR; китайські символи читаються за допомогою списку слів Rime, якщо його таблиця слів охоплює мову + Завантаження словників читання… Зчитування готове (записів: %1$d) Не вдалося завантажити, вимкніть і ввімкніть це знову, щоб повторити спробу + Офлайн-словник + Японська (SudachiDict, ~75 МБ) + Китайські кантонські символи (UniDict, ~8 МБ) + Китайські слова (pinyin-pro, ~17 МБ) + Грецька, слов\'янська кирилиця (включно з російською, українською, болгарською, сербською, білоруською, македонською), грузинська, вірменська, давньоарабська, арабська, амхарська та ассирійська транслітерація (CLDR, ~0,5 МБ) + Читання %1$s: %2$d%% + Оберіть щонайменше один словник Показувати кнопку \"Оновити\" Натисніть, щоб перейти до наступного варіанта тексту пісні; натисніть і утримуйте, щоб шукати текст пісні за назвою пісні та виконавцем Шукати текст пісні @@ -545,8 +588,10 @@ Second \"item\" text" Збережено Підсвічувати синхронізовані слова або склади Надавати пріоритет тексту пісні по словах, коли доступно. Якщо текст пісні містить синхронізацію по словах, підсвічує кожне слово або склад у такт музиці + Зменшувати неактивні рядки + Малювати кожен рядок, який зараз не співається, трохи меншим, поверх затемнення Показувати бек-вокал окремо - Виносить бек-вокал, записаний у дужках, наприклад \"(Oh) I want you\", із рядка основного вокалу в окремий, тьмяніший рядок + Розділяє фоновий вокал і гармонію від основного рядка. Переміщує фоновий вокал, записаний у дужках, наприклад \"(Oh) I want you\", із співного рядка в окремий, більш тьмяний рядок Приховувати відтворені рядки тексту Приховувати рядки тексту пісні над рядком, що зараз відтворюється Приховувати ще не відтворені рядки тексту @@ -604,10 +649,15 @@ Second \"item\" text" Перекладено %s Романізовано %s Романізовано %s + Романізовано Rime (%s) Перекладено Мова перекладу - Мова романізації + Мова романографії або транслітерації + "Мова перекладу: +%s" + "Цільова мова транслітерації або романізації: +%s" Переклад… Не вдалося перекласти текст пісні Романізовано @@ -621,7 +671,7 @@ Second \"item\" text" Оновлення Оновлено Ключ API QuickLRC - QuickLRC транскрибує або синхронізує за часом текст пісні для поточного треку. Окремий сервіс, не пов\'язаний із кінцевою точкою, сумісною з OpenAI, вище + QuickLRC транскрибує або часове співставлення текстів для поточної треку Ключ QuickLRC збережено. Скористайтеся кнопкою вирівнювання в діалозі пошуку тексту пісні або запитом на панелі, коли для треку немає тексту Ключ API QuickLRC збережено QuickLRC не прийняв цей ключ @@ -644,51 +694,43 @@ Second \"item\" text" Приховати рядок з інформацією про авторів Приховати рядок з інформацією про авторів, що показується під текстом пісні Поміняти місцями переклад і романізацію - Увімкнено - романізація під текстом, переклад над текстом; вимкнено - романізація над текстом, переклад під текстом - Переклад - Романізація - Постачальник перекладу - Google (типовий, безплатний) - Сумісний з OpenAI (власний) - Постачальник романізації - Google (типовий) - Локальний / офлайн-словник - Сумісний з OpenAI (власний) - Розділ ШІ - Використовується лише тоді, коли постачальник не надає позначок розділів, таких як \"Приспів\" або \"Куплет\", які тоді визначає ваша модель, сумісна з OpenAI - ШІ та хмарні кінцеві точки - Налаштуйте ключі API, моделі, кінцеві точки та шаблони запитів для постачальників ШІ - URL-адреса кінцевої точки API (наприклад, https://api.openai.com/v1/chat/completions) + ON поміщає романографію або транслітерацію під тексти й переклад над ними; OFF залишає романографію або транслітерацію зверху й переклад внизу. Читання, розміщене над кожним складом, рухається разом із ним, відразу над його складом вниз під нього + Завжди центрувати тексти + ON поміщає кожен рядок посередині й ігнорує джерело вирівнювання, включаючи сторони дуету; OFF залишає вирівнювання, яке просять тексти + Щонайменше один постачальник має бути увімкнений + Переклад + Романографія або транслітерація + Офлайн-словник + Список слів Rime + Виявити секції за допомогою ШІ + Використовується лише тоді, коли постачальник не надає позначок розділів, таких як Приспів або Куплет, які тоді виявляє ваша модель, сумісна з OpenAI. Редагування рядка також відкриває його запит + URL кінцевої точки API (наприклад, https://api.openai.com/v1/chat/completions, https://api.anthropic.com/v1/chat/completions, https://generativelanguage.googleapis.com/v1beta/openai/chat/completions) Шаблон запиту Заповнювачі: {language}, {task}, {title}, {artist}, {count}, {lines} Запит збережено Запит збережено, але кінцева точка ШІ ще непридатна до використання. Спочатку налаштуйте її в розділі \"Налаштування ШІ\" Запит скинуто - Схоже, ти вставив уже готовий переклад замість англійського оригіналу. Надішли, будь ласка, англійський рядок, і я перекладу. Налаштування перекладу та романізації збережено Налаштування скинуто до типових Базова URL-адреса має починатися з http:// або https:// - Перевірити з\'єднання - Налаштувати кінцеві точки - Показати всі кінцеві точки + Налаштування неповні або відхилені постачальником Перевірка з\'єднання… - Постачальник перекладу - Постачальник романизації - Отримати моделі + %1$s: Підключено! (%2$s) + %1$s: Помилка підключення. Перевірте налаштування. + Спочатку введіть ключ API Отримання моделей… Виберіть модель Не вдалося отримати моделі. Перевірте ключ API або з\'єднання - Налаштування Gemini API - Сумісний з OpenAI (власна кінцева точка) - Налаштування DeepL API - Ключ API + Запит Модель Базова URL-адреса + Обрати сервіс Ключ API (необов\'язково) - Запит для перекладу Додаткове тіло запиту (необов\'язковий JSON) + Додаткові заголовки запиту (необов\'язковий JSON) + Цитуйте назви та значення, наприклад, {\"api-key\": \"abc\"}. Значення без лапок, що містять / : або число, відхиляється. Порожнє значення видаляє заголовок. + Цитуйте назви та значення, наприклад, {\"temperature\": 0.3}. Значення без лапок, що містять / чи :, відхиляється. Модель та повідомлення не можна встановити тут. Ключ автентифікації (Free або Pro) - Налаштуйте ключі API, моделі, кінцеві точки та запити 8.23.51 - Вимкнути товсті іконки 9.13.51 - Відновити вкладки \"Далі\", \"Текст\" та \"Схожі\" в плеєрі diff --git a/patches/src/main/resources/addresources/values-uz-rUZ/music/strings.xml b/patches/src/main/resources/addresources/values-uz-rUZ/music/strings.xml index b1a4ad80f5..6c2f48828e 100644 --- a/patches/src/main/resources/addresources/values-uz-rUZ/music/strings.xml +++ b/patches/src/main/resources/addresources/values-uz-rUZ/music/strings.xml @@ -461,12 +461,10 @@ Foydalanish ma'lumotlari fon tartibda ham sinxronlashini ta'minlash uchun, YouTu Ko\'rinishi Sinxronizatsiya Panel - AI endpoint Barcha provayderlar o\'chirildi - ichki va joylashtirilgan matnlar Provayderlar tartibini tiklash? Provayderlarni standart ustuvorlik tartibiga qaytarish Saqlangan matnlarni tozalash - Ushbu qurilmada saqlangan matnlarni o\'chirish %1$s MB tozalandi Tokenni olish Apple Music Media-user-token @@ -528,14 +526,9 @@ Talab qilinadigan cookies: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure Nusxalash tugmasini ko‘rsatish Asl matnni oddiy matn sifatida nusxalash uchun bosing; xom matn manba faylini Yuklab olishlar papkasiga saqlash uchun uzoq bosib turing Tarjima tugmasini ko‘rsatish - Tarjimani yoqish/o\'chirish uchun bosing; faqat tarjima uchun uzoq bosib turing. Provayder tarjimalari, Google Translate yoki OpenAI-ga mos API dan foydalaniladi - Romanizatsiya tugmasini ko¢rsatish - Romanizatsiyani yoqish/o\'chirish uchun bosing; faqat romanizatsiya uchun uzoq bosib turing. Provayder romanizatsiyasi, Google Translate yoki OpenAI-ga mos API dan foydalaniladi - Mahalliy romanizatsiya va tekislash - Provayder, sizning modelingiz va Google Translate ishlamagan taqdirda, qurilmada romanizatsiya qiling. O\'qilishi vaqtga moslangan matnning har bir so\'zidan keyin keladi; so\'zlar sinxronizatsiyasi yoqilgan bo\'lishi kerak. Xitoy, kanton, yapon, koreys, grek, rus, gruzin, arman tillari; taxminan 120 MB - %1$s o\'qilmoqda: %2$d%% O\'qilishlar tayyor (%1$d ta yozuv) Yuklab olish muvaffaqiyatsiz tugadi - qayta urinish uchun buni o\'chirib, yana yoqing + %1$s o\'qilmoqda: %2$d%% Yangilash tugmasini ko¢rsatish Keyingi matn nomzodiga o\'tish uchun bosing; qo\'shiq nomi va ijrochi bo\'yicha matnni qidirish uchun uzoq bosib turing Matnlarni qidirish @@ -605,7 +598,6 @@ Ikkala chegaraga ham ta'sir qiladi. 1.75 da to'rtta to'liq kenglikdagi belgi 7 g Tarjima qilindi Tarjima tili - Romanizatsiya tili Tarjima qilinmoqda… Lirikalarni tarjima qilib bo‘lmadi Romanizatsiya qilindi @@ -619,7 +611,6 @@ Ikkala chegaraga ham ta'sir qiladi. 1.75 da to'rtta to'liq kenglikdagi belgi 7 g Yangilanmoqda Yangilandi QuickLRC API kaliti - QuickLRC joriy trek uchun matnlarni transkripsiya qiladi yoki vaqtini moslaydi. Bu yuqoridagi OpenAI-mos keluvchi endpointga bog\'liq bo\'lmagan alohida servisdir QuickLRC kaliti saqlandi. Matnlarni qidirish dialogidagi moslash tugmasidan yoki trekda matnlar bo\'lmaganda panel taklifidan foydalaning QuickLRC API kaliti saqlandi QuickLRC ushbu kalitni qabul qilmadi @@ -635,56 +626,27 @@ Ikkala chegaraga ham ta'sir qiladi. 1.75 da to'rtta to'liq kenglikdagi belgi 7 g Moslash uchun matnlar yo\'q - buning o\'rniga transkripsiya qilinmoqda QuickLRC ushbu trekni qayta ishlay olmadi Ushbu trek uchun QuickLRC kreditlari tugadi - "Matnlar sarlavha va ijrochi bo'yicha moslashtiriladi, g'ayritabiiy metadata hech narsa topmasligi yoki noto'g'ri versiyani topishi mumkin. Versiyalarni almashtirish uchun yangilashdan foydalaning. * bilan belgilangan provayderlar token talab qiladi. Barcha yoqilgan provayderlar har doim sizning tartibingizda so'ralmaydi, balki ularning tezligiga qarab javob berishadi.\n\nMatnlar Morphe bilan bog'lanmagan, homiylik qilinmagan yoki tasdiqlanmagan uchinchi tomon xizmatlaridan keladi. Ularning har biri boricha ishlatiladi: matnlar, ularning mavjudligi va litsenziyalari ushbu xizmatlar va ularning litsenziyantlarida qoladi va ulardan foydalanish ularning xizmat ko'rsatish shartlariga bog'liq bo'lishi mumkin. Yoqilgan xizmatga faqat siz so'ragan qidiruv — sarlavha, ijrochi va ushbu xizmatga kerak bo'lgan joyda video ID yuboriladi. Ixtiyoriy AI nuqtasi va QuickLRC transkripsiyasi faqat siz ularni sozlaganingizdan keyin ishga tushadi va matnlar matnini yoki trekning video URL manzilini qabul qiladi." Faqat foydalanadigan provayderlaringizni yoqing yoki standart holatda qoldiring - doimiy so\'rovlar kichik hamjamiyat matnlari saytlarini yuklama bilan to\'ldirishi mumkin Mualliflik qatorini yashirish Matn ostida ko\'rsatilgan mualliflik qatorini yashirish Tarjima va romanizatsiyani almashtirish - YOQILSAL: romanizatsiya matn ostida, tarjima esa tepada bo\'ladi; O\'CHIRILSA: romanizatsiya tepada, tarjima esa pastda qoladi - Tarjima - Romanizatsiya - Tarjima provayderi - Google (Standart, bepul) - OpenAI bilan mos (maxsus) - Romanizatsiya provayderi - Google (Standart) - Mahalliy / Oflayn lugưat - OpenAI bilan mos keluvchi (maxsus) - AI bo\'limi - Faqat provayder Naqorat yoki Kuplet kabi bo\'lim belgilarini taqdim etmaganda va sizning OpenAI- bilan mos keladigan modelingiz ularni aniqlaganda ishlatiladi - AI & bulutli nuqtalar - AI provayderlari uchun API kalitlari, modellar, nuqtalar va prompt shablonlarini sozlang. - API nuqtasining URL manzili (masalan, https://api.openai.com/v1/chat/completions) Prompt shabloni Pleysholderlar: {language}, {task}, {title}, {artist}, {count}, {lines} Prompt saqlandi So\'rov saqlandi, ammo AI endpoint hali ishlamayapti. Avval uni AI Sozlamalari bo\'limida sozlang So\'rov qayta tiklandi - JSON haqiqiy emas. Saqlashdan oldin uni tuzating yoki maydonni tozalang Tarjima & romanizatsiya sozlamalari saqlandi Sozlamalar standart holatiga qaytarildi Asosiy URL http:// yoki https:// bilan boshlanishi kerak - Aloqani tekshirish - Nuqtalarni sozlash - Barcha endpointlarni ko\'rsatish Ulanish tekshirilmoqda… - Tarjima provayderi - Romanizatsiya provayderi - Modellarni yuklash Modellar yuklanmoqda… Modelni tanlang Modellarni yuklab bo\'lmadi. API kalitini yoki ulanishni tekshiring. - Gemini API sozlamalari - OpenAI bilan mos keluvchi (maxsus endpoint) - DeepL API sozlamalari - API kaliti Model Asosiy URL API kaliti (ixtiyoriy) - Tarjima prompti Qo\'shimcha so\'rov tanasi (ixtiyoriy JSON) Autentifikatsiya kaliti (Bepul yoki Pro) - API kalitlari, modellar, endpointlar va promptlarni sozlash 8.23.51 - Qalin ikonkalarni o\'chirish 9.13.51 - Playerning Keyingi, Qo\'shiq matni va Tegishli yorliqlarini tiklash diff --git a/patches/src/main/resources/addresources/values-vi-rVN/music/strings.xml b/patches/src/main/resources/addresources/values-vi-rVN/music/strings.xml index c685ad246f..3ca1253234 100644 --- a/patches/src/main/resources/addresources/values-vi-rVN/music/strings.xml +++ b/patches/src/main/resources/addresources/values-vi-rVN/music/strings.xml @@ -462,12 +462,47 @@ Phát SABR (đã tắt giả lập luồng video)" Hiển thị Đồng bộ Bảng - Điểm cuối AI Tất cả nhà cung cấp bị vô hiệu hóa - lời ca tích hợp và lời nhúng Đặt lại thứ tự nguồn cung cấp? Khôi phục các nguồn cung cấp về thứ tự ưu tiên mặc định. + Danh sách từ Rime + "Từ điển Rime cũng cần UniDict: trong nhà cung cấp Từ điển ngoại tuyến, chọn \"Ký tự tiếng Trung và tiếng Quảng Đông (UniDict)\" và để nó hoàn tất việc tải xuống trước. Nếu không có nó, một danh sách chỉ đọc ký tự sẽ không cho kết quả đọc. + +Chọn các danh sách mà Từ điển Rime nên đọc, sau đó nhấn Lưu để tải xuống chúng. Một danh sách bạn đã thêm sẽ bị xóa khi bạn bỏ chọn nó. + +Mỗi danh sách là bảng từ-nghe của riêng bạn, một tệp .dict.yaml với các dòng như: +字 ham +典 din2 +Cột đầu tiên là từ, cột thứ hai là cách đọc; các cột và dòng bổ sung không có tab sẽ bị bỏ qua." + Không có danh sách từ Rime nào được chọn cho ngôn ngữ ở trên. Nhấn tên này để chọn một danh sách, hoặc chọn một ngôn ngữ có danh sách + Thêm + Thêm Danh sách từ Rime + Tệp .dict.yaml, hoặc một tệp zip chứa nó, với các dòng như 字 ham và 典 din2. Danh sách được lấy khi bạn chọn nó và nhấn Lưu, và giấy phép của nó thuộc quyền kiểm tra của bạn + Mã ngôn ngữ + Hai hoặc ba chữ cái, như hak hoặc wuu + Tên + Hiển thị dưới âm đọc, như Hakka (Meixian) + Địa chỉ danh sách từ + URL https:// của tệp .dict.yaml hoặc file zip của nó + Ký tự không nằm trong từ + Tự động + Ký tự Quảng Đông + Ký tự Quan thoại + Không có âm đọc cho ký tự + Nhập mã, tên và địa chỉ + Mã phải là hai hoặc ba chữ cái, như hak + Địa chỉ phải bắt đầu bằng https:// + Tên và địa chỉ không được chứa gạch dọc + %1$s đã có bảng từ + %1$s được đọc bởi bộ latin hóa tích hợp và không thể nhận danh sách từ + Không sử dụng: ngôn ngữ này đã có âm đọc riêng, vì vậy hãy bỏ chọn nó để xóa + Đã thêm %1$s - hãy đánh dấu nó và nhấn Lưu để tải xuống + Danh sách từ đã sẵn sàng + Danh sách từ đã sẵn sàng (%1$d mục) + Xây dựng danh sách từ: %1$d%% + Tải xuống danh sách từ thất bại - hãy nhấn Lưu lại để thử lại Xóa lời đã lưu - Xóa các đoạn lời được lưu trên thiết bị này + Xóa %1$s MB lời bài hát được giữ trên thiết bị này Đã xóa %1$s MB Lấy mã truy cập Mã thông tin người dùng Apple Music @@ -518,14 +553,22 @@ Các bước: Hiện nút Sao chép lời nhạc Chạm để sao chép lời gốc dưới dạng văn bản thô; nhấn giữ để lưu tệp nguồn lời thô vào thư mục Downloads Hiện nút Dịch lời nhạc - Chạm để bật/tắt bản dịch; nhấn giữ chỉ để xem bản dịch. Sử dụng bản dịch từ nhà cung cấp, Google Translate hoặc API tương thích OpenAI - Hiển thị nút Romanize - Chạm để bật/tắt phiên âm Latinh; nhấn giữ chỉ để xem phiên âm Latinh. Sử dụng phiên âm từ nhà cung cấp, Google Translate hoặc API tương thích OpenAI - Phiên âm cục bộ và căn chỉnh - Phiên âm trực tiếp trên thiết bị khi nhà cung cấp, mô hình của bạn và Google Translate đều không thành công. Việc đọc theo từng từ của lời có nhịp điệu; đồng bộ từ phải được bật. Hỗ trợ Trung Quốc, Quảng Đông, Nhật Bản, Hàn Quốc, Hy Lạp, Nga, Gruzia, Armenia; khoảng 120 MB - Đang đọc %1$s: %2$d%% + Nhấn để chuyển đổi dịch thuật; nhấn giữ lâu chỉ để dịch. Sử dụng các nhà cung cấp dịch thuật đã bật theo thứ tự liệt kê + Hiển thị nút Latinh hóa hoặc phiên âm + Nhấn để chuyển đổi latin hóa hoặc phiên âm; nhấn giữ lâu chỉ để latin hóa hoặc phiên âm. Sử dụng các nhà cung cấp latin hóa đã bật theo thứ tự liệt kê + Cho phép phiên âm + Cho phép các âm đọc không phải La-tinh từ nguồn lời bài hát; khi tắt, chỉ giữ lại các chữ cái La-tinh, chữ cái có dấu thanh và chữ số thanh + Đọc từng từ của lời bài hát có thời gian (đồng bộ từ trên); tiếng Hàn không cần tải xuống; mười bốn kịch bản chia sẻ một lần tải xuống CLDR; ký tự Hán được đọc bởi Danh sách từ Rime khi bảng từ của nó bao phủ ngôn ngữ đó + Đang tải xuống từ điển âm đọc… Đã sẵn sàng (%1$d mục) Tải xuống thất bại - hãy tắt và bật lại để thử lại + Từ điển ngoại tuyến + Tiếng Nhật (SudachiDict, ~75 MB) + Ký tự Trung Quốc và Quảng Đông (UniDict, ~8 MB) + Từ tiếng Trung (pinyin-pro, ~17 MB) + Phiên âm ngôn ngữ Hy Lạp, Cyrillic Slavơ (bao gồm Nga, Ukraina, Bungari, Serbia, Belarus, Macedonia), Georgia, Armenia, Hebrew, Ả Rập, Amharic và Syriac (CLDR, ~0.5 MB) + Đang đọc %1$s: %2$d%% + Chọn ít nhất một từ điển Hiển thị nút Làm mới Chạm để chuyển sang ứng viên lời tiếp theo; nhấn giữ để tìm kiếm lời theo tên bài hát và nghệ sĩ Tìm kiếm lời bài hát @@ -536,8 +579,10 @@ Các bước: Đã lưu Đánh dấu các từ hoặc âm tiết đồng bộ Ưu tiên lời bài hát từng từ khi có sẵn. Khi lời bài hát có thời gian cho từng từ, đánh dấu từng từ hoặc âm tiết theo nhạc + Thu nhỏ các dòng không hoạt động + Vẽ mọi dòng không được hát ngay lập tức nhỏ hơn một chút, bên trên việc làm mờ nó Hiển thị giọng hát nền riêng biệt - Di chuyển lời hát nền được viết trong ngoặc, ví dụ như \"(Ồ) anh muốn em\", khỏi dòng chính thành một dòng riêng biệt có độ mờ thấp hơn + Tách giọng hát nền và hòa âm khỏi dòng chính. Di chuyển giọng hát nền được viết trong ngoặc, chẳng hạn như \"(Ồ) Tôi muốn bạn\", khỏi dòng đang hát thành một dòng mờ hơn của riêng nó Ẩn lời bài hát đã phát Ẩn các dòng lời bài hát phía trên dòng đang phát Ẩn lời bài hát chưa phát @@ -595,10 +640,15 @@ Chia định dạng bản nhạc đã điền thành các khung hình xoay vòng Dịch bởi %s Phiên âm bởi %s Latin hóa bởi %s + Latinh hóa bởi Rime (%s) Đã dịch Ngôn ngữ bản dịch - Ngôn ngữ Latin hóa + Ngôn ngữ latin hóa hoặc phiên âm + "Ngôn ngữ đích của bản dịch: +%s" + "Ngôn ngữ đích của phiên âm latin hoặc chuyển tự: +%s" Đang dịch… Không thể dịch lời bài hát Đã chuyển tự @@ -612,7 +662,7 @@ Chia định dạng bản nhạc đã điền thành các khung hình xoay vòng Đang làm mới Đã làm mới Khóa API QuickLRC - QuickLRC chuyển ngữ hoặc căn thời gian cho lời bài hát của bài hát hiện tại. Đây là một dịch vụ riêng biệt, không liên quan đến điểm cuối tương thích OpenAI ở trên + QuickLRC chép lời hoặc căn chỉnh thời gian cho lời bài hát của bản nhạc hiện tại Khóa QuickLRC đã được lưu. Sử dụng nút căn chỉnh trong hộp thoại tìm kiếm lời bài hát, hoặc lời nhắc bảng điều khiển khi bài hát không có lời Đã lưu khóa API QuickLRC QuickLRC không chấp nhận khóa đó @@ -628,58 +678,50 @@ Chia định dạng bản nhạc đã điền thành các khung hình xoay vòng Không có lời bài hát để căn chỉnh - đang chuyển ngữ thay thế QuickLRC không thể xử lý bài hát này Tín dụng QuickLRC đã hết cho bài hát này - "Lời bài hát được khớp dựa trên tiêu đề và nghệ sĩ, siêu dữ liệu không bình thường có thể không tìm thấy gì hoặc tìm nhầm phiên bản. Sử dụng làm mới để chuyển đổi phiên bản. Các nhà cung cấp đánh dấu * yêu cầu token. Không phải tất cả các nhà cung cấp đã bật đều luôn được yêu cầu theo thứ tự của bạn, nhưng phản hồi dựa trên tốc độ của họ. + "Lời bài hát được khớp theo tiêu đề và nghệ sĩ, metadata bất thường có thể không tìm thấy gì hoặc phiên bản sai. Sử dụng làm mới để chuyển đổi phiên bản. Các nhà cung cấp đánh dấu * yêu cầu token. Không phải lúc nào tất cả các nhà cung cấp được bật cũng được yêu cầu theo thứ tự của bạn, nhưng phản hồi dựa trên tốc độ của họ. -Lời bài hát đến từ các dịch vụ bên thứ ba không liên kết, tài trợ hoặc được Morphe bảo trợ. Mỗi dịch vụ được sử dụng như nguyên trạng: lời bài hát, tính khả dụng và giấy phép thuộc về các dịch vụ đó và nhà cấp phép của họ, và việc sử dụng chúng có thể chịu sự chi phối của điều khoản dịch vụ của họ. Những gì gửi đến một dịch vụ đã bật chỉ là thông tin tra cứu bạn đã yêu cầu - tiêu đề, nghệ sĩ và, ở nơi dịch vụ đó cần nó, ID video. Điểm cuối AI tùy chọn và quá trình sao chép QuickLRC chỉ chạy sau khi bạn cấu hình chúng, và nhận được văn bản lời bài hát hoặc URL video của bài hát." +Lời bài hát đến từ các dịch vụ bên thứ ba không liên quan, tài trợ hoặc được Morphe thông qua. Mỗi dịch vụ được sử dụng nguyên trạng: lời bài hát, khả năng sẵn có và giấy phép của chúng vẫn thuộc về các dịch vụ đó và người cấp phép, và việc sử dụng chúng có thể tuân theo điều khoản dịch vụ của họ. Những gì gửi đến một dịch vụ được bật chỉ là truy vấn bạn đã yêu cầu - tiêu đề, nghệ sĩ và, nơi dịch vụ đó cần, ID video. Điểm cuối AI tùy chọn và bản ghi QuickLRC chỉ chạy sau khi bạn cấu hình chúng, và nhận văn bản lời bài hát hoặc URL video của bài hát." Chỉ kích hoạt các nhà cung cấp bạn sử dụng, hoặc giữ mặc định - các yêu cầu liên tục có thể làm quá tải các trang lời bài hát cộng đồng nhỏ Ẩn dòng ghi công Ẩn dòng ghi công hiển thị bên dưới lời bài hát Hoán đổi bản dịch và chuyển tự - BẬT đặt chuyển tự bên dưới lời bài hát và bản dịch bên trên; TẮT giữ chuyển tự bên trên và bản dịch bên dưới - Dịch thuật - Phiên âm Latin - Dịch vụ dịch thuật - Google (Mặc định, Miễn phí) - Tương thích OpenAI (Tùy chỉnh) - Nhà cung cấp phiên âm La-tinh - Google (Mặc định) - Từ điển Cục bộ / Ngoại tuyến - Tương thích OpenAI (Tùy chỉnh) - Phần AI - Chỉ được sử dụng khi nhà cung cấp không gửi nhãn phần như Chorus hoặc Verse, khi đó mô hình tương thích OpenAI của bạn sẽ phát hiện các phần này - Điểm cuối AI & Đám mây - Cấu hình khóa API, mô hình, điểm cuối và mẫu gợi ý cho các nhà cung cấp AI. - URL điểm cuối API (ví dụ: https://api.openai.com/v1/chat/completions) + BẬT đặt latin hóa hoặc phiên âm phía dưới lời bài hát và bản dịch phía trên; TẮT giữ latin hóa hoặc phiên âm phía trên và bản dịch phía dưới. Một âm đọc được đặt trên mỗi âm tiết sẽ di chuyển cùng nó, từ phía trên âm tiết đó đến phía dưới nó + Luôn căn giữa lời bài hát + BẬT đặt mọi dòng ở giữa và bỏ qua sự căn chỉnh của nguồn, bao gồm cả hai bên song ca; TẮT giữ nguyên sự căn chỉnh mà lời bài hát yêu cầu + Ít nhất một nhà cung cấp phải được bật + Dịch thuật + Latinh hóa hoặc phiên âm + Từ điển ngoại tuyến + Danh sách từ Rime + Phát hiện phần bằng AI + Chỉ được sử dụng khi nhà cung cấp không gửi nhãn phần như Chorus hoặc Verse, do mô hình tương thích OpenAI của bạn phát hiện. Chỉnh sửa hàng cũng sẽ mở lời nhắc của nó + URL điểm cuối API (ví dụ: https://api.openai.com/v1/chat/completions, https://api.anthropic.com/v1/chat/completions, https://generativelanguage.googleapis.com/v1beta/openai/chat/completions) Mẫu Prompt Trình giữ chỗ: {language}, {task}, {title}, {artist}, {count}, {lines} Đã lưu prompt Đã lưu lời nhắc, nhưng điểm cuối AI vẫn chưa thể sử dụng. Hãy cấu hình nó trong Cài đặt AI trước Đã đặt lại prompt - Không phải JSON hợp lệ. Hãy sửa hoặc xóa trường này trước khi lưu Đã lưu cài đặt Dịch thuật & Phiên âm Đặt lại cài đặt về mặc định URL cơ sở phải bắt đầu bằng http:// hoặc https:// - Kiểm tra Kết nối - Cấu hình Điểm cuối - Hiển thị Tất cả Điểm cuối + Cài đặt chưa đầy đủ hoặc bị nhà cung cấp từ chối Đang kiểm tra kết nối… - Dịch vụ dịch thuật - Nhà cung cấp La-tinh hóa - Tải các mô hình + %1$s: Đã kết nối! (%2$s) + %1$s: Kết nối thất bại. Hãy kiểm tra cài đặt. + Nhập khóa API trước Đang tải các mô hình… Chọn một mô hình Tải các mô hình thất bại. Hãy kiểm tra khóa API hoặc kết nối. - Cài đặt Gemini API - Tương thích OpenAI (Điểm cuối tùy chỉnh) - Cài đặt DeepL API - Khóa API + Lời nhắc Mẫu URL cơ sở + Chọn một dịch vụ Khóa API (tùy chọn) - Câu lệnh dịch thuật Thân yêu cầu bổ sung (JSON tùy chọn) + Tiêu đề yêu cầu bổ sung (JSON tùy chọn) + Trích dẫn tên và giá trị, ví dụ {\"api-key\": \"abc\"}. Giá trị không được trích dẫn chứa / : hoặc số sẽ bị từ chối. Giá trị rỗng sẽ xóa tiêu đề. + Trích dẫn tên và giá trị, ví dụ {\"temperature\": 0.3}. Giá trị không được trích dẫn chứa / hoặc : sẽ bị từ chối. Model và tin nhắn không thể được đặt ở đây. Khóa xác thực (Miễn phí hoặc Pro) - Cấu hình các khóa API, mô hình, điểm cuối và câu lệnh 8.23.51 - Vô hiệu hóa biểu tượng đậm 9.13.51 - Khôi phục các tab Tiếp theo, Lời nhạc và Liên quan trong trình phát diff --git a/patches/src/main/resources/addresources/values-vi-rVN/youtube/strings.xml b/patches/src/main/resources/addresources/values-vi-rVN/youtube/strings.xml index 7099f0033a..a82f7943ae 100644 --- a/patches/src/main/resources/addresources/values-vi-rVN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-vi-rVN/youtube/strings.xml @@ -29,6 +29,8 @@ Second \"item\" text" Khác Video Khôi phục màn hình cài đặt YouTube cũ + Hiển thị thông báo trong snackbar của YouTube + Các thông báo của Morphe, chẳng hạn như xác nhận nút phát lại, được hiển thị trong snackbar của YouTube thay vì cửa sổ pop-up Bỏ hạn chế phát nền Tắt phát nền diff --git a/patches/src/main/resources/addresources/values-zh-rCN/music/strings.xml b/patches/src/main/resources/addresources/values-zh-rCN/music/strings.xml index 9f35cc92c0..0ce325c273 100644 --- a/patches/src/main/resources/addresources/values-zh-rCN/music/strings.xml +++ b/patches/src/main/resources/addresources/values-zh-rCN/music/strings.xml @@ -462,12 +462,47 @@ SABR 播放(模拟视频流已关闭)" 显示 同步 面板 - AI 端点 所有提供商已禁用 - 内置歌词和嵌入歌词 重置提供程序顺序? 将提供程序恢复为默认优先级顺序 + Rime词表 + "词库还需要UniDict:在离线字典提供程序中,勾选“中文和粤语字(UniDict)”,并等待其下载完成。没有它,读字列表将无法输出读音。 + +勾选词库需要读取的列表,然后点击保存以下载。取消勾选已添加的列表时,该列表将被移除。 + +每个列表都是您自己的字词对照表,即一个 .dict.yaml 文件,包含类似以下的行: +字 ham +典 din2 +第一列是字词,第二列是读音;额外列及没有制表符的行将被忽略。" + 上方语言未勾选任何Rime词表。点击此名称以勾选一个,或选择一个有词表的语言 + 添加 + 添加Rime词表 + .dict.yaml 文件,或包含此类文件的压缩包,其中包含类似 字 ham 和 典 din2 的行。当您勾选列表并点击保存时,将获取该列表,您可以自行检查其许可协议 + 语言代码 + 两三个字母,如hak或wuu + 名称 + 显示在读音下方,如客家话(梅县) + 词表地址 + .dict.yaml或其zip的https://地址 + 非字词字符 + 自动 + 粤语字 + 普通话字 + 无字符读音 + 请填写代码、名称和地址 + 代码必须是两三个字母,如hak + 地址必须以https://开头 + 名称和地址不能包含竖线 + %1$s已有一个字词表 + %1$s由内置罗马化程序读取,无法使用词表 + 未使用:此语言已有自己的读音,因此取消勾选以移除 + 已添加%1$s - 勾选并点击保存以下载 + 词表就绪 + 词表就绪(共%1$d项) + 正在构建词表:%1$d%% + 词表下载失败 - 再次点击保存以重试 清除缓存歌词 - 删除此设备上保留的歌词 + 删除设备上保留的歌词缓存 %1$s MB 已清除 %1$s MB 获取令牌 Apple Music 媒体用户令牌 @@ -527,14 +562,22 @@ SABR 播放(模拟视频流已关闭)" 显示复制按钮 点击以纯文本形式复制原始歌词;长按将原始歌词源文件保存至下载目录 显示翻译按钮 - 点击切换翻译;长按仅显示翻译。使用提供商翻译、Google 翻译或兼容 OpenAI 的 API - 显示音译按钮 - 点击切换罗马音标注;长按仅显示罗马音。使用提供商罗马音、Google 翻译或兼容 OpenAI 的 API - 本地罗马音与对齐 - 当提供商、您的模型及 Google 翻译均失败时,在设备上执行罗马音。读取内容跟随时间轴歌词的每一词;必须开启单词同步。支持中文、粤语、日语、韩语、希腊语、俄语、格鲁吉亚语、亚美尼亚语;约 120 MB - 阅读 %1$s: %2$d%% + 点击切换翻译;长按仅翻译。使用按顺序排列的已启用翻译提供商 + 显示罗马化或意译按钮 + 点击切换罗马化或意译;长按仅进行罗马化或意译。使用按顺序排列的已启用罗马化提供商 + 启用意译 + 允许来自歌词源的非拉丁读音;关闭时,仅保留拉丁字母、带重音的字母和声调数字 + 读取时间对齐歌词(词同步开启)中的每个词;韩语无需下载;十四个脚本共享一个CLDR下载;当Rime词表的词表覆盖该语言时,汉字由Rime词表读取 + 正在下载读音字典…… 读音准备就绪(%1$d 条条目) 下载失败 - 请关闭后重新开启以重试 + 离线字典 + 日语(SudachiDict,约75 MB) + 中文及粤语字(UniDict,约8 MB) + 中文词(pinyin-pro,约17 MB) + 希腊语、斯拉夫西里尔字母(包括俄语、乌克兰语、保加利亚语、塞尔维亚语、白俄罗斯语、马其顿语)、格鲁吉亚语、亚美尼亚语、希伯来语、阿拉伯语、阿姆哈拉语和叙利亚语意译(CLDR,约0.5 MB) + 阅读 %1$s: %2$d%% + 请至少选择一个字典 显示刷新按钮 点击切换下一个歌词候选项;长按通过歌曲标题和歌手搜索歌词 搜索歌词 @@ -545,8 +588,10 @@ SABR 播放(模拟视频流已关闭)" 已保存 高亮显示同步的单词或音节 优先考虑可用逐字歌词。当歌词带有时间标志,随着音乐及时高亮每个单词或音节 + 收缩非活动行 + 将当前未演唱的每行稍微缩小,并在变暗的基础上绘制 单独显示背景人声 - 将写在括号中的背景人声(如“(Oh) I want you”)从演唱行中分离出来,形成独立的弱音行 + 将背景人声和和声从主唱行分开。将写在括号中的背景人声(例如“哦 我想要你”)移出演唱行,进入自己的一条较暗的行 隐藏已播歌词 隐藏当前播放行之上的歌词行 隐藏未播歌词 @@ -604,10 +649,15 @@ SABR 播放(模拟视频流已关闭)" 由 %s 翻译 由 %s 音译 由 %s 罗马化 + 由Rime罗马化(%s) 翻译 翻译目标语言 - 罗马化语言 + 罗马化或意译语言 + "翻译目标语言: +%s" + "罗马化或转写目标语言: +%s" 正在翻译… 无法翻译歌词 已音译 @@ -621,7 +671,7 @@ SABR 播放(模拟视频流已关闭)" 正在刷新 已刷新 QuickLRC API 密钥 - QuickLRC 为当前曲目转录或对齐歌词。这是一项独立服务,与上述兼容 OpenAI 的端点无关 + QuickLRC为当前曲目转录或时间对齐歌词 已保存 QuickLRC 密钥。使用歌词搜索对话框中的对齐按钮,或在没有歌词的曲目出现面板提示时使用 QuickLRC API 密钥已保存 QuickLRC 不接受该密钥 @@ -637,58 +687,50 @@ SABR 播放(模拟视频流已关闭)" 无可对齐的歌词 - 改为进行转录 QuickLRC 无法处理此曲目 此曲目的 QuickLRC 积分已用完 - "歌词通过标题和歌手进行匹配,异常元数据可能导致无结果或找到错误版本。使用刷新按钮切换版本。标记 * 的提供商需要令牌。并非所有启用的提供商都会按顺序请求,而是根据其响应速度返回结果。 + "歌词通过标题和艺术家进行匹配,不寻常的元数据可能导致找不到内容或找到错误版本。使用刷新切换版本。标记为 * 的提供程序需要令牌。并非总是按照您的顺序请求所有启用的提供程序,而是根据它们的响应速度返回结果。 -歌词来自第三方服务,这些服务与 Morphe 无关,未受其赞助或认可。每个服务的使用方式保持不变:歌词、可用性及许可权仍归服务商及其许可方所有,使用它们可能需遵守其服务条款。发送至启用服务的请求仅包含您查询的内容 — 标题、歌手以及该服务所需的视频 ID。可选 AI 端点和 QuickLRC 转录仅在您配置后运行,并接收歌词文本或曲目的视频 URL。" +歌词来自第三方服务,这些服务与Morphe无关,也未得到Morphe赞助或认可。每个服务按原样使用:歌词、其可用性及许可权仍归这些服务及其授权方所有,使用它们可能受其服务条款约束。发送至启用服务的查询仅包含您请求的信息——标题、艺术家以及该服务所需的视频ID。可选的AI端点和QuickLRC转录仅在配置后运行,并接收歌词文本或音轨的视频URL。" 仅启用您使用的提供者,或保留默认值 - 频繁请求可能会压垮小型社区歌词网站 隐藏署名行 隐藏显示在歌词下方的署名行 交换翻译和音译 - 开启时,音译在歌词下方、翻译在歌词上方;关闭时,音译在歌词上方、翻译在歌词下方。逐音节读数随之移动,从音节上方移到音节下方 - 翻译 - 罗马化 - 翻译提供商 - Google (默认,免费) - OpenAI 兼容 (自定义) - 罗马化提供者 - Google (默认) - 本地 / 离线词典 - OpenAI 兼容 (自定义) - AI 段落识别 - 仅在提供商未发送如副歌或主歌等段落标签,且由您的 OpenAI 兼容模型检测到时使用 - AI & 云端端点 - 为 AI 提供者配置 API 密钥、模型、端点和提示词模板。 - API 端点 URL (例如 https://api.openai.com/v1/chat/completions) + 开启时将罗马化或意译置于歌词下方,翻译置于上方;关闭时将罗马化或意译置于上方,翻译置于下方。放置在每个音节上的读音会随之移动,从其音节上方移至下方 + 始终居中歌词 + 开启时将每一行置于中间,忽略源对齐方式,包括对唱部分;关闭时保持歌词要求的对齐方式 + 必须至少保留一个提供商开启 + 翻译 + 罗马化或意译 + 离线字典 + Rime词表 + 使用AI检测段落 + 仅在提供商未提供诸如副歌或主歌等段落标签时使用,然后由你的兼容OpenAI模型进行检测。编辑该行也会打开其提示词 + API端点URL(例如 https://api.openai.com/v1/chat/completions,https://api.anthropic.com/v1/chat/completions,https://generativelanguage.googleapis.com/v1beta/openai/chat/completions) 提示词模板 占位符:{language}, {task}, {title}, {artist}, {count}, {lines} 提示词已保存 提示词已保存,但 AI 端点尚不可用。请先在 AI 设置中进行配置 提示词已重置 - 无效的 JSON。保存前请修复或清空该字段 翻译 & 罗马化设置已保存 设置已重置为默认 基础 URL 必须以 http:// 或 https:// 开头 - 测试连接 - 配置端点 - 显示所有端点 + 设置不完整或被提供商拒绝 正在验证连接… - 翻译提供商 - 罗马化提供商 - 获取模型 + %1$s:已连接!(%2$s) + %1$s:连接失败。请检查设置。 + 请先输入API密钥 正在获取模型…… 选择模型 获取模型失败。请检查 API 密钥或网络连接。 - Gemini API 设置 - OpenAI 兼容 (自定义端点) - DeepL API 设置 - API 密钥 + 提示词 型号 基础 URL + 选择服务 API 密钥 (可选) - 翻译提示词 额外请求正文(可选 JSON) + 额外请求头(可选JSON) + 引号名称和值,例如 {\"api-key\": \"abc\"}。包含/ : 或数字的未引用值将被拒绝。空值将移除标头。 + 引号名称和值,例如 {\"temperature\": 0.3}。包含/或: 的未引用值将被拒绝。模型和消息不能在此处设置。 身份验证密钥(免费版或专业版) - 配置 API 密钥、模型、端点和提示词 8.23.51 - 禁用粗体图标 9.13.51 - 恢复播放器的「接下来播放」「歌词」和「相关」标签页 diff --git a/patches/src/main/resources/addresources/values-zh-rCN/youtube/strings.xml b/patches/src/main/resources/addresources/values-zh-rCN/youtube/strings.xml index 65fc10b771..230778ffbb 100644 --- a/patches/src/main/resources/addresources/values-zh-rCN/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-zh-rCN/youtube/strings.xml @@ -29,6 +29,8 @@ Second \"item\" text" 其他 视频 恢复旧设置菜单 + 在 YouTube 信息提示栏中显示消息 + Morphe消息(如玩家按钮的确认信息)将显示在YouTube Snackbar中,而不是弹出窗口 移除后台播放限制 禁用 Shorts 后台播放 diff --git a/patches/src/main/resources/addresources/values-zh-rTW/music/strings.xml b/patches/src/main/resources/addresources/values-zh-rTW/music/strings.xml index 43077c0a50..3a3f192178 100644 --- a/patches/src/main/resources/addresources/values-zh-rTW/music/strings.xml +++ b/patches/src/main/resources/addresources/values-zh-rTW/music/strings.xml @@ -462,12 +462,47 @@ SABR 播放(影片串流模擬已關閉)" 顯示 同步 面板 - AI端點 所有提供程序已停用 - 內建歌詞和嵌入式歌詞 重設提供者順序? 將提供者還原為預設優先順序 + Rime 詞表 + "Rime 詞典也需要 UniDict:在離線字典提供者中,勾選「中文及粵語字元(UniDict)」並讓它先完成下載。沒有它,讀取字元的列表將無法提供發音。 + +勾選 Rime 詞典應該讀取的列表,然後點擊「儲存」以下載它們。當你取消勾選已添加的列表時,它會被移除。 + +每個列表都是你自己的詞到發音的表格,是一個 .dict.yaml 文件,行格式如: +字 ham +典 din2 +第一列是詞,第二列是其發音;忽略額外的列和沒有製表符的行。" + 以上語言尚未勾選任何 Rime 詞表。點擊此名稱以勾選一個詞表,或選擇擁有詞表的語言 + 新增 + 新增 Rime 詞表 + .dict.yaml 文件或包含該文件的 zip 包,行格式如 字 ham 和 典 din2。當你勾選該列表並點擊「儲存」時獲取列表,其許可證由你自行檢查 + 語言代碼 + 兩到三個字母,例如 hak 或 wuu + 名稱 + 顯示在發音下方,例如 Hakka (Meixian) + 詞表地址 + .dict.yaml 或其壓縮包的 https:// 地址 + 詞中未包含的字符 + 自動 + 粵文字符 + 漢語普通話字符 + 無字符發音 + 填寫代碼、名稱和地址 + 代碼必須是兩到三個字母,例如 hak + 地址必須以 https:// 開頭 + 名稱和地址不能包含豎直條 + %1$s 已有詞表 + %1$s 由內建羅馬拼音處理,無法使用詞表 + 未使用:此語言已有其 own 發音,因此取消勾選即可移除 + 已新增 %1$s - 勾選並點擊保存以下載 + 詞表就緒 + 詞表就緒(%1$d 項) + 正在生成發音詞表:%1$d%% + 詞表下載失敗 - 再次點擊保存以重試 清除儲存的歌詞 - 刪除本機裝置上保留的歌詞 + 刪除此設備上保留的 %1$s MB 歌詞緩存 已清除 %1$s MB 取得權杖 Apple Music Media-user-token @@ -527,14 +562,22 @@ SABR 播放(影片串流模擬已關閉)" 顯示複製按鈕 點擊可複製純文字原始歌詞;長按可將原始歌詞來源檔案儲存至下載資料夾 顯示翻譯按鈕 - 點擊切換翻譯;長按僅進行翻譯。使用提供者的翻譯、Google 翻譯或 OpenAI 相容 API - 顯示羅馬拼音按鈕 - 點擊切換羅馬拼音標示;長按僅進行羅馬拼音標示。使用提供者的羅馬拼音、Google 翻譯或 OpenAI 相容 API - 本機羅馬拼音與對齊 - 當提供者、自訂模型和 Google 翻譯失敗時,在裝置上進行羅馬拼音轉譯。讀音會對應時間軸歌詞的每個單字;必須啟用單字同步功能。支援中文、粵語、日文、韓文、希臘文、俄文、喬治亞文、亞美尼亞文;約需 120 MB - 正在建立讀音 %1$s:%2$d%% + 點擊切換翻譯;長按僅進行翻譯。按照啟用的翻譯提供者的列出順序使用 + 顯示羅馬拼音或轉寫按鈕 + 點擊切換羅馬拼音或轉寫;長按僅進行羅馬拼音或轉寫。按照啟用的羅馬拼音提供者的列出順序使用 + 啟用轉寫 + 允許來自歌詞來源的非拉丁文發音;關閉時,僅保留拉丁字母、帶音符號字母和聲調數字 + 讀取時間同步歌詞(字級同步開啟)的每個詞;韓語無需下載;十四種腳本共用一個 CLDR 下載;當 Rime 詞表的詞表涵蓋該語言時,漢字由 Rime 詞表讀取 + 正在下載閱讀字典…… 讀音就緒 (%1$d 個條目) 下載失敗 - 關閉並重新開啟此選項以重試 + 離線字典 + 日語(SudachiDict,約 75 MB) + 中文和粵文字符(UniDict,約 8 MB) + 漢語詞彙(pinyin-pro,約 17 MB) + 希臘語、斯拉夫語西里爾字母(包括俄語、烏克蘭語、保加利亞語、塞爾維亞語、白俄羅斯語、馬其頓語)、格魯吉亞語、亞美尼亞語、希伯來語、阿拉伯語、阿姆哈拉語和敘利亞語轉寫(CLDR,約 0.5 MB) + 正在建立讀音 %1$s:%2$d%% + 請至少選擇一個字典 顯示重新整理按鈕 輕觸切換至下一個歌詞候選項目;長按依歌曲標題及藝人搜尋歌詞 搜尋歌詞 @@ -545,8 +588,10 @@ SABR 播放(影片串流模擬已關閉)" 已儲存 強調已同步的歌詞 優先使用已同步歌詞而非未同步歌詞 - 單獨顯示背景和聲 - 將寫在括號中的背景人聲(如「(Oh) I want you」)從主唱行分離出來,變成獨立的較弱音量行 + 收縮非活動行 + 將當前未演唱的每一行繪製得稍小一些,並在使其變暗的同時實現這一效果 + 單獨顯示背景人聲 + 將背景人聲和諧聲從主唱線上分離。將用括號書寫的背景人聲(如「(Oh) I want you」)移出演唱線,進入獨立的一行並變暗 隱藏已播放行 隱藏目前播放中那一行上方的歌詞行 隱藏未播放的歌詞 @@ -604,10 +649,15 @@ SABR 播放(影片串流模擬已關閉)" 由 %s 進行翻譯 由 %s 進行羅馬拼音翻譯 由 %s 進行羅馬拼音翻譯 + 由 Rime 羅馬拼音化(%s) 已翻譯 翻譯語言 - 羅馬拼音語言 + 羅馬拼音或轉寫語言 + "翻譯目標語言: +%s" + "羅馬化或譯音目標語言: +%s" 正在翻譯… 無法翻譯歌詞 已翻譯成羅馬拼音 @@ -621,7 +671,7 @@ SABR 播放(影片串流模擬已關閉)" 正在重新整理 已重新整理 QuickLRC API 金鑰 - QuickLRC 會為目前曲目轉錄或對齊歌詞。這是一個獨立的服務,與上述 OpenAI 相容端點無關 + QuickLRC 為當前曲目转录或對齊歌詞時間軸 已儲存 QuickLRC 金鑰。在歌詞搜尋對話方塊中使用對齊按鈕,或在曲目無歌詞時使用面板提示 QuickLRC API 金鑰已儲存 QuickLRC 不接受該金鑰 @@ -644,51 +694,43 @@ SABR 播放(影片串流模擬已關閉)" 隱藏來源標示 在歌詞畫面中隱藏來源標示 交換翻譯和羅馬拼音 - 開啟時將羅馬拼音放在歌詞下面,翻譯放在上面; 關閉時維持羅馬拼音放在上面,翻譯放在下面 - 翻譯 - 羅馬拼音 - 翻譯提供者 - Google (預設,免費) - OpenAI相容 (自訂) - 羅馬拼音來源 - Google (預設) - 本機/離線字典 - OpenAI相容 (自訂) - AI 段落 - 僅在供應商未提供如副歌或主歌等段落標記,而由你的 OpenAI 相容模型偵測時使用 - AI及雲端終端 - 設定API金鑰、模型、終端及AI供應商的提示詞。 - API終端網址 (例如:https://api.openai.com/v1/chat/completions) + 開啟時,羅馬拼音或轉寫位於歌詞下方,翻譯位於上方;關閉時,羅馬拼音或轉寫位於上方,翻譯位於下方。放置在每個音節上方的發音會隨之移動,從音節上方移至下方 + 始終居中顯示歌詞 + 開啟時,將每行置於中間並忽略源對齊方式(包括對唱部分);關閉時,保持歌詞請求的對齊方式 + 至少必須保留一個提供者 + 翻譯 + 羅馬拼音或轉寫 + 離線字典 + Rime 詞表 + 使用 AI 檢測段落 + 僅在使用者提供的提供者未提供如副歌或主歌等段落標記,而由您的 OpenAI 相容模型檢測時使用。編輯此行也會打開其提示詞 + API 端點 URL(例如 https://api.openai.com/v1/chat/completions, https://api.anthropic.com/v1/chat/completions, https://generativelanguage.googleapis.com/v1beta/openai/chat/completions) 提示詞範本 佔位符:{language}, {task}, {title}, {artist}, {count}, {lines} 已儲存提示詞 已儲存提示,但 AI 端點尚無法使用。請先在「AI 設定」中進行配置 已重設提示詞 - 無效的 JSON。請修正或清空欄位後再儲存 已儲存翻譯及羅馬拼音設定 設定已重設為預設值 Base URL 必須以 http:// 或 https:// 開頭 - 測試連線 - 設定終端 - 顯示所有終端 + 設置不完整或被提供者拒絕 正在驗證連線… - 翻譯來源 - 羅馬拼音來源 - 獲取模型 + %1$s:已連接!(%2$s) + %1$s:連接失敗。請檢查設置。 + 請先輸入 API 金鑰 正在獲取模型… 選擇模型 無法獲取模型。請檢查API金鑰或連線狀態。 - Gemini API設定 - OpenAI相容 (自訂終端) - DeepL API設定 - API 金鑰 + 提示詞 模型 基礎網址 + 選擇服務 API金鑰(選填) - 翻譯提示詞 額外請求主體(選填JSON) + 額外請求標頭(可選 JSON) + 引用名稱和值,例如 {\"api-key\": \"abc\"}。未引用且包含 / : 或數值的值將被拒絕。空值會移除標頭。 + 引用名稱和值,例如 {\"temperature\": 0.3}。未引用且包含 / 或 : 的值將被拒絕。Model 和 messages 無法在此設置。 驗證金鑰(免費版或專業版) - 設定API金鑰、模型、終端及提示詞 8.23.51 - 停用粗體圖示 9.13.51 - 恢復播放器「接下來播放」、「歌詞」和「相關」標籤 diff --git a/patches/src/main/resources/addresources/values-zh-rTW/youtube/strings.xml b/patches/src/main/resources/addresources/values-zh-rTW/youtube/strings.xml index 30f6742f01..da177a71f5 100644 --- a/patches/src/main/resources/addresources/values-zh-rTW/youtube/strings.xml +++ b/patches/src/main/resources/addresources/values-zh-rTW/youtube/strings.xml @@ -29,6 +29,8 @@ Second \"item\" text" 其他 影片 還原舊版設定選單 + 在 YouTube Snackbar 中顯示訊息 + Morphe 訊息(如玩家按鈕的確認)會顯示於 YouTube Snackbar,而非彈出式視窗 移除背景播放限制 停用背景播放 Shorts diff --git a/patches/src/main/resources/addresources/values-zu-rZA/music/strings.xml b/patches/src/main/resources/addresources/values-zu-rZA/music/strings.xml index f10f9378e1..052bdef673 100644 --- a/patches/src/main/resources/addresources/values-zu-rZA/music/strings.xml +++ b/patches/src/main/resources/addresources/values-zu-rZA/music/strings.xml @@ -462,12 +462,10 @@ Ukuqinisekisa ukuthi i-scrobbling ihlanganiswa ngesikhathi sokudlala ngemuva, qi Khombisa Ukuhambisana Iphaneli - I-endpoint ye-AI Bonke abahlinzeki bikhanselwe - amazwi akhiwe phakathi kanye namazwi afakwe Bhala kabusha uhlelo lwezihlinzeki? Buyisela izihlinzeki ohlelweni lokuqala lwezinto ezibalulekile Sula amazwi agcinwe - Sula amazwi agcinwe kuleli device Kusuliwe u-%1$s MB Thola itokeni I-Apple Music Media-user-token @@ -527,14 +525,9 @@ I-cookies ezidingekayo: SID, HSID, SSID, SAPISID, __Secure-3PAPISID, __Secure-1P Khombisa inkinobho ye-Copy Thinta ukuze ukopishe amazwi aqala njengombhalo ojwayelekile; cindezela isikhathi eside ukuze ugcine ifayela lezisekelo zamazwi ku-Downloads Khombisa inkinobho ye-Translate - Chofoza ukuvula/ukuvala ukuhumusha; cindezela isikhathi eside ukuze uhumushe kuphela. Usebenzisa ukuhumusha komunikezeli, iGoogle Translate, noma i-API ehambisana ne-OpenAI - Khombisa inkinobho ye-Romanize - Chofoza ukuvula/ukuvala i-romanization; cindezela isikhathi eside ukuze uthole i-romanization kuphela. Usebenzisa i-romanization komunikezeli, iGoogle Translate, noma i-API ehambisana ne-OpenAI - I-Romanization ne-Alignment yasendaweni - Hlanganisa i-romanization kudivayisi lapho komunikezeli, imodeli yakho ne-Google Translate kungaphumeleli. Ukufundwa kulandela igama ngalinye lamazwi engoma; ukusingisa kwamagama kufanele kube kuvuliwe. IsiShayina, i-Cantonese, isiJaphanisi, isiKoreya, isiGreek, isiRussian, isiGeorgian, isiArmenian; cishe i-120 MB - Kufundwa %1$s: %2$d%% Ukufundwa kulungile (%1$d entries) Ukudownload kwehlulekile - cisha lokhu bese ukubuyisela ukuze uzame futhi + Kufundwa %1$s: %2$d%% Khombisa inkinobho ye-Refresh Chofoza ukushintshela kumelwe amazwi alandelayo; cindezela isikhathi eside ukuze useshe amazwi ngesihloko sengoma nomculi Funa amagama engoma @@ -605,7 +598,6 @@ Sisebenza kuzo zombili izimiso. Ku-1.75, abalingiswa abane ababanikezelwe ububan Ihhumuliwe Ulimi lokuhumusha - Ulimi lwe-Romanization Kuhunyushwa… Kukwazekile ukuhumusha amazwi engoma Hlelwe ngohlamvu lwe-Roman @@ -619,7 +611,6 @@ Sisebenza kuzo zombili izimiso. Ku-1.75, abalingiswa abane ababanikezelwe ububan Kuvuselelwa Kuvuselelwe I-API key ye-QuickLRC - I-QuickLRC ibhala phansi noma ihlanganisa izikhathi zamagama engoma yamanje. Isevisi eyodwa, engahlobani ne-endpoint ehambisana ne-OpenAI engenhla I-key ye-QuickLRC igcinwe. Sebenzisa isikhopho sokuhlanza (align) ebhokisini lokusesha amagama engoma, noma isiboniso sepaneli lapho ingoma ingenazo amagama I-API key ye-QuickLRC igcinwe I-QuickLRC ayizange iyiamukele leyo key @@ -635,56 +626,27 @@ Sisebenza kuzo zombili izimiso. Ku-1.75, abalingiswa abane ababanikezelwe ububan Ayikho amagama okuhlanza - kubhalwa phansi esikhundleni salokho I-QuickLRC ayikwazanga ukuphrosesa le ngoma Amakhredithi e-QuickLRC aphelile kule ngoma - "Amazwi ayahambisana nesihloko nomculi, imininingwane engajwayelekile ingase ingathole lutho noma ithole uhlobo olungalungile. Sebenzisa ukuvuselela ukuze ushintshe uhlobo. Abahlinzeki abamakiwe ngo * badinga itoken. Bonke abahlinzeki abuvuliwe abicelwa ngaso sonke isikhathi ngokulandelana kwenu, kodwa baphendula ngokusekelwe ngesivinyane sabo.\n\nAmazwi avela ezinsizakalonisweni zangaphandle ezingahlobene ne-Morphe, ezingaxikiswa, noma ezingathunyelwa yi-Morphe. Ngasinye sisetshenziswa njengoba sinjalo: amazwi, ukutholakala kwawo, kanye nokulicense kwawo kusala kulezo zinsizakaloniso kanye nabanikazi balicense, futhi ukusebenzisa kwazo kungaba ngaphansi kwemigomo yawo yosizo. Okufika ekuhlinzekweni okuvuliwe wukufuna okucelile kuphela - isihloko, umculi, nalapho leyo nsizakaloniso ikudinga, i-video ID. I-AI endpoint ekhethwekile kanye nokubhala kwe-QuickLRC kusetshenziswa kuphela ngemuva kokuba usilayile, futhi kuthola Umbhalo wamazwi noma i-URL ye-video yeculo." Vula kuphela abahlinzeki obasebenzisayo, noma ushiye okujwayelekile - izicelo eziqhubekayo zingaba nzima emawebhasayithi amancane amazwi omphakathi Fihla umugqa wemiklomelo Fihla umugqa wekredithi obonakala ngaphansi kwamazwi aqinile Shintshisana ngohumusho nokubhala ngama-roman - u-ON ubeka ukubhala ngama-roman ngaphansi kwamazwi nohumusho phezulu; u-OFF ukushiya ukubhala ngama-roman phezulu nohumusho ngaphansi - Ihuhlo - I-Romanization - Umhlinzeki wokuhumusha - Google (Okukhethiwe, Mahhala) - OpenAI Compatible (Okuyamathuba) - Umhlinzeki we-Romanization - Google (Okukhethiwe) - Isichazamazwi sasekhaya / Offline - OpenAI Compatible (Okuyamathuba) - Isigaba se-AI - Kusetshenziswa kuphela lapho umhlinzeki engithumeli izabelo zezigaba njenge-Chorus noma i-Verse, okutholwa ngumodeli wakho ohambisana ne-OpenAI - Ama-AI & Cloud Endpoints - Lungisa ama-API keys, ama-models, ama-endpoints, kanye nama-prompt templates abahlinzeki be-AI. - I-API endpoint URL (isib. https://api.openai.com/v1/chat/completions) Ithempulethi ye-Prompt Izindawo zokugcina: {language}, {task}, {title}, {artist}, {count}, {lines} I-Prompt igcinwe I-prompt igcinwe, kodwa i-AI endpoint ayikawukwazi ukusetshenziswa. Isethe ukuqala ngaphansi kwe-AI Settings I-Prompt ibuyiselwe emva - Akusona i-JSON esemthethweni. yilungise noma sucize inkundla ngaphambi kokulonda Izilungiselelo ze-Translation & Romanization zigcinwe Izilungiselelo sezibuyiselwe ekusetshenzweni kwazo okwemqondo I-Base URL kufanele iqale ngo http:// noma https:// - Hlola ukuxhumana - Lungisa ama-Endpoints - Khombisa wonke ama-Endpoints Kuqinisekiswa ukuxhumana… - Umhlinzeki we-Translation - Umhlinzeki we-Romanization - Thola amaModel Kutholwa amaModel… Khetha iModel Kuhleleke ukuthola amaModel. Hlola i-API key noma ukuxhumana. - Izilungiselelo ze-Gemini API - OpenAI Compatible (Custom Endpoint) - Izilungiselelo ze-DeepL API - I-API key IModel I-URL eyisisekelo I-API key (iyakhethwa) - Isicelo sohumusho Umzimba wesicelo owengeziwe (i-JSON eyakhethwa) I-key yokuqinisekisa (i-Free noma i-Pro) - Lungisa ama-API key, ama-model, ama-endpoint, kanye nezicelo 8.23.51 - Vala ama-icon agqamile 9.13.51 - Buyisela ama-tab e-Player Up next, Lyrics, kanye ne-Related From 24e6abb7d314f19b01057d67842c74ba09ff5cdf Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Fri, 30 Oct 2026 11:01:21 +0000 Subject: [PATCH 188/189] chore: Release v2.0.0-dev.4 [skip ci] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## [2.0.0-dev.4](https://github.com/MorpheApp/morphe-patches/compare/v2.0.0-dev.3...v2.0.0-dev.4) (2026-10-30) ### 🐛 Bug Fixes * **YouTube - Hide layout components:** Community posts are shown below a video opened from the search results, a channel or a playlist ([b373534](https://github.com/MorpheApp/morphe-patches/commit/b3735342ce3c686f25c76d5e664703f39767fc76)) * **YouTube - Navigation bar:** Black band below pages and the Shorts player when the window is not drawn behind the system navigation buttons ([60daa80](https://github.com/MorpheApp/morphe-patches/commit/60daa80ebabc420946991af736cc74ab8247fee7)) --- CHANGELOG.md | 7 +++++++ README.md | 2 +- gradle.properties | 2 +- patches-bundle.json | 8 ++++---- patches-list.json | 2 +- 5 files changed, 14 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b9d5cfeeb..dc27ce59b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## [2.0.0-dev.4](https://github.com/MorpheApp/morphe-patches/compare/v2.0.0-dev.3...v2.0.0-dev.4) (2026-10-30) + +### 🐛 Bug Fixes + +* **YouTube - Hide layout components:** Community posts are shown below a video opened from the search results, a channel or a playlist ([b373534](https://github.com/MorpheApp/morphe-patches/commit/b3735342ce3c686f25c76d5e664703f39767fc76)) +* **YouTube - Navigation bar:** Black band below pages and the Shorts player when the window is not drawn behind the system navigation buttons ([60daa80](https://github.com/MorpheApp/morphe-patches/commit/60daa80ebabc420946991af736cc74ab8247fee7)) + ## [2.0.0-dev.3](https://github.com/MorpheApp/morphe-patches/compare/v2.0.0-dev.2...v2.0.0-dev.3) (2026-10-30) ### ✨ New Features diff --git a/README.md b/README.md index 58f649d040..8a5ee7cca3 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ All modifications made by Morphe, along with their dates, can be found in the Gi ## 🩹 Patches list -> **[v2.0.0-dev.3](https://github.com/MorpheApp/morphe-patches/releases/tag/v2.0.0-dev.3)**  •  `dev`  •  194 patches total +> **[v2.0.0-dev.4](https://github.com/MorpheApp/morphe-patches/releases/tag/v2.0.0-dev.4)**  •  `dev`  •  194 patches total
    📦 YouTube  •  109 patches
    diff --git a/gradle.properties b/gradle.properties index ee3f7f20b5..070668d1ca 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,4 +5,4 @@ android.useAndroidX = true # Extensions are Java only. Prevents AGP from adding Kotlin compilation and the Kotlin stdlib. android.builtInKotlin = false kotlin.code.style = official -version = 2.0.0-dev.3 +version = 2.0.0-dev.4 diff --git a/patches-bundle.json b/patches-bundle.json index 4b454d684e..0d21ce7373 100644 --- a/patches-bundle.json +++ b/patches-bundle.json @@ -1,8 +1,8 @@ { - "created_at": "2026-10-30T08:57:20", - "description": "## [2.0.0-dev.3](https://github.com/MorpheApp/morphe-patches/compare/v2.0.0-dev.2...v2.0.0-dev.3) (2026-10-30)\n\n### ✨ New Features\n\n* **YouTube Music - Third-party lyrics:** UI changes, partial feature regression ([#4182](https://github.com/MorpheApp/morphe-patches/issues/4182)) ([b6e0a64](https://github.com/MorpheApp/morphe-patches/commit/b6e0a643677d7a975413ab8d1566745cac15127c))", - "download_url": "https://github.com/MorpheApp/morphe-patches/releases/download/v2.0.0-dev.3/patches-2.0.0-dev.3.mpp", + "created_at": "2026-10-30T10:59:35", + "description": "## [2.0.0-dev.4](https://github.com/MorpheApp/morphe-patches/compare/v2.0.0-dev.3...v2.0.0-dev.4) (2026-10-30)\n\n### 🐛 Bug Fixes\n\n* **YouTube - Hide layout components:** Community posts are shown below a video opened from the search results, a channel or a playlist ([b373534](https://github.com/MorpheApp/morphe-patches/commit/b3735342ce3c686f25c76d5e664703f39767fc76))\n* **YouTube - Navigation bar:** Black band below pages and the Shorts player when the window is not drawn behind the system navigation buttons ([60daa80](https://github.com/MorpheApp/morphe-patches/commit/60daa80ebabc420946991af736cc74ab8247fee7))", + "download_url": "https://github.com/MorpheApp/morphe-patches/releases/download/v2.0.0-dev.4/patches-2.0.0-dev.4.mpp", "signature_download_url": "", - "version": "2.0.0-dev.3", + "version": "2.0.0-dev.4", "public_key": "g8Ys9leTu2yeixojeR35HrNkK6eOSLpA6nNIizRutqI=" } diff --git a/patches-list.json b/patches-list.json index 3fa2afdca0..c08137e017 100644 --- a/patches-list.json +++ b/patches-list.json @@ -1,6 +1,6 @@ { "NOTE": "Do NOT manually edit this file. This file is automatically updated when semantic release (release.yml) runs. Manually editing this file can break your releases and break third party tools that use this file.", - "version": "2.0.0-dev.3", + "version": "2.0.0-dev.4", "patches": [ { "name": "AI channel filter", From 3977417216408d5bfd21f769e57738674ddbbacd Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Fri, 30 Oct 2026 11:01:52 +0000 Subject: [PATCH 189/189] ci: Simplify string check task --- .github/workflows/crowdin_pull.yml | 2 +- patches/build.gradle.kts | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/workflows/crowdin_pull.yml b/.github/workflows/crowdin_pull.yml index a506ecd592..b7f52e73db 100644 --- a/.github/workflows/crowdin_pull.yml +++ b/.github/workflows/crowdin_pull.yml @@ -58,7 +58,7 @@ jobs: - name: Check strings on crowdin env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: ./gradlew checkStringResources -PnoProguard + run: ./gradlew checkStringResources # Step 3: Checkout dev branch to prepare for merging - name: Checkout dev branch for merge diff --git a/patches/build.gradle.kts b/patches/build.gradle.kts index 25acbb1858..9f94115a47 100644 --- a/patches/build.gradle.kts +++ b/patches/build.gradle.kts @@ -84,9 +84,13 @@ tasks { register("checkStringResources") { description = "Checks resource strings for invalid formatting" - dependsOn(build) - - classpath = sourceSets["main"].runtimeClasspath + // Only the compiled patches and the raw string resources are needed. Using the full + // main runtime classpath would run processResources, which builds and minifies all extensions. + classpath = files( + sourceSets["main"].output.classesDirs, + configurations.runtimeClasspath, + layout.projectDirectory.dir("src/main/resources"), + ) mainClass.set("app.morphe.patches.util.resource.CheckStringResourcesKt") }